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') }}
+
+
+
+
+
+
+
+
+