diff --git a/api/src/database/system-data/fields/collections.yaml b/api/src/database/system-data/fields/collections.yaml index c6ef4294f5..4fbf09a4c8 100644 --- a/api/src/database/system-data/fields/collections.yaml +++ b/api/src/database/system-data/fields/collections.yaml @@ -192,6 +192,6 @@ fields: - field: item_duplication_fields special: - json - interface: code + interface: system-field-tree options: - language: JSON + collectionField: collection diff --git a/app/src/components/register.ts b/app/src/components/register.ts index 91dca94a52..8e03b3cb16 100644 --- a/app/src/components/register.ts +++ b/app/src/components/register.ts @@ -15,6 +15,7 @@ import VBreadcrumb from './v-breadcrumb'; import VButton from './v-button/'; import VCard, { VCardActions, VCardSubtitle, VCardText, VCardTitle } from './v-card'; import VCheckbox from './v-checkbox/'; +import VCheckboxTree from './v-checkbox-tree/'; import VChip from './v-chip/'; import VDetail from './v-detail'; import VDialog from './v-dialog'; @@ -60,6 +61,7 @@ export function registerComponents(app: App): void { app.component('v-card-title', VCardTitle); app.component('v-card', VCard); app.component('v-checkbox', VCheckbox); + app.component('v-checkbox-tree', VCheckboxTree); app.component('v-chip', VChip); app.component('v-detail', VDetail); app.component('v-dialog', VDialog); diff --git a/app/src/components/v-checkbox-tree/index.ts b/app/src/components/v-checkbox-tree/index.ts new file mode 100644 index 0000000000..e6d2642ec6 --- /dev/null +++ b/app/src/components/v-checkbox-tree/index.ts @@ -0,0 +1,4 @@ +import VCheckboxTree from './v-checkbox-tree.vue'; + +export { VCheckboxTree }; +export default VCheckboxTree; diff --git a/app/src/components/v-checkbox-tree/v-checkbox-tree-checkbox.vue b/app/src/components/v-checkbox-tree/v-checkbox-tree-checkbox.vue new file mode 100644 index 0000000000..d16b8fb8d6 --- /dev/null +++ b/app/src/components/v-checkbox-tree/v-checkbox-tree-checkbox.vue @@ -0,0 +1,418 @@ + + + diff --git a/app/src/components/v-checkbox-tree/v-checkbox-tree.vue b/app/src/components/v-checkbox-tree/v-checkbox-tree.vue new file mode 100644 index 0000000000..46d02fe46a --- /dev/null +++ b/app/src/components/v-checkbox-tree/v-checkbox-tree.vue @@ -0,0 +1,71 @@ + + + diff --git a/app/src/components/v-checkbox/v-checkbox.vue b/app/src/components/v-checkbox/v-checkbox.vue index 1420fa0418..37c01f4b29 100644 --- a/app/src/components/v-checkbox/v-checkbox.vue +++ b/app/src/components/v-checkbox/v-checkbox.vue @@ -2,7 +2,7 @@
- + {{ label }} - +
@@ -66,11 +66,17 @@ export default defineComponent({ type: Boolean, default: false, }, + checked: { + type: Boolean, + default: null, + }, }, setup(props, { emit }) { const internalValue = useSync(props, 'value', emit); const isChecked = computed(() => { + if (props.checked !== null) return props.checked; + if (props.modelValue instanceof Array) { return props.modelValue.includes(props.value); } @@ -93,7 +99,7 @@ export default defineComponent({ if (props.modelValue instanceof Array) { const newValue = [...props.modelValue]; - if (isChecked.value === false) { + if (props.modelValue.includes(props.value) === false) { newValue.push(props.value); } else { newValue.splice(newValue.indexOf(props.value), 1); @@ -101,7 +107,7 @@ export default defineComponent({ emit('update:modelValue', newValue); } else { - emit('update:modelValue', !isChecked.value); + emit('update:modelValue', !props.modelValue); } } }, diff --git a/app/src/composables/use-field-tree/use-field-tree.ts b/app/src/composables/use-field-tree/use-field-tree.ts index adc5145292..8ab39b6bc9 100644 --- a/app/src/composables/use-field-tree/use-field-tree.ts +++ b/app/src/composables/use-field-tree/use-field-tree.ts @@ -1,19 +1,21 @@ -import { useFieldsStore, useRelationsStore } from '@/stores/'; +import { useCollectionsStore, useFieldsStore, useRelationsStore } from '@/stores/'; import { Field, Relation } from '@/types'; import { getRelationType } from '@/utils/get-relation-type'; -import { cloneDeep } from 'lodash'; +import { cloneDeep, orderBy } from 'lodash'; import { computed, Ref, ComputedRef } from 'vue'; -type FieldOption = { name: string; field: string; key: string; children?: FieldOption[] }; +type FieldOption = { name: string; field: string; key: string; children?: FieldOption[]; group?: string }; export default function useFieldTree( collection: Ref, /** Only allow m2o relations to be nested */ strict = false, inject?: Ref<{ fields: Field[]; relations: Relation[] } | null>, - filter: (field: Field) => boolean = () => true + filter: (field: Field) => boolean = () => true, + depth = 3 ): { tree: ComputedRef } { const fieldsStore = useFieldsStore(); + const collectionsStore = useCollectionsStore(); const relationsStore = useRelationsStore(); const tree = computed(() => { @@ -24,23 +26,27 @@ export default function useFieldTree( return { tree }; function parseLevel(collection: string, parentPath: string | null, level = 0) { - const fieldsInLevel = [ - ...cloneDeep(fieldsStore.getFieldsForCollectionAlphabetical(collection)), - ...(inject?.value?.fields.filter((field) => field.collection === collection) || []), - ] - .filter((field: Field) => { - const shown = - field.meta?.special?.includes('alias') !== true && field.meta?.special?.includes('no-data') !== true; - return shown; - }) - .filter(filter) - .map((field: Field) => ({ - name: field.name, - field: field.field, - key: parentPath ? `${parentPath}.${field.field}` : field.field, - })) as FieldOption[]; + const fieldsInLevel = orderBy( + [ + ...cloneDeep(fieldsStore.getFieldsForCollectionAlphabetical(collection)), + ...(inject?.value?.fields.filter((field) => field.collection === collection) || []), + ] + .filter((field: Field) => { + const shown = + field.meta?.special?.includes('alias') !== true && field.meta?.special?.includes('no-data') !== true; + return shown; + }) + .filter(filter) + .map((field: Field) => ({ + name: field.name, + field: field.field, + key: parentPath ? `${parentPath}.${field.field}` : field.field, + sort: field.meta?.sort, + })) as FieldOption[], + 'sort' + ); - if (level >= 3) return fieldsInLevel; + if (level >= depth) return fieldsInLevel; for (const field of fieldsInLevel) { const relations = [ @@ -65,10 +71,29 @@ export default function useFieldTree( if (relationType === 'm2o') { field.children = parseLevel( - relation.related_collection, + relation.related_collection!, parentPath ? `${parentPath}.${field.field}` : field.field, level + 1 ); + } else if (relationType === 'm2a') { + field.children = []; + + for (const relatedCollection of relation.meta!.one_allowed_collections!) { + const relatedCollectionName = + collectionsStore.collections.find((collection) => collection.collection === relatedCollection)?.name || + relatedCollection; + + field.children.push( + ...parseLevel( + relatedCollection, + parentPath ? `${parentPath}.${field.field}:${relatedCollection}` : `${field.field}:${relatedCollection}`, + level + 1 + ).map((child) => ({ + ...child, + name: `${child.name} (${relatedCollectionName})`, + })) + ); + } } else if (strict === false) { field.children = parseLevel( relation.collection, diff --git a/app/src/composables/use-item/use-item.ts b/app/src/composables/use-item/use-item.ts index fae844e0d4..8239d1eb12 100644 --- a/app/src/composables/use-item/use-item.ts +++ b/app/src/composables/use-item/use-item.ts @@ -158,8 +158,12 @@ export function useItem(collection: Ref, primaryKey: Ref + + {{ t('collection_field_not_setup') }} + + + {{ t('select_a_collection') }} + +
+ +
+ + + + +