From ef3da32ab35340635734570041ffa85782ebcb87 Mon Sep 17 00:00:00 2001 From: Oreille <33065839+Oreilles@users.noreply.github.com> Date: Fri, 5 Nov 2021 01:35:05 +0100 Subject: [PATCH] Properly handle M2A fields in fieldStore and useFieldTree (#9432) * Properly handle M2A fields in fieldStore and useFieldTree * Fix addNode * Rewrote use-field-tree * Remember visited paths * Fix error whith undefined relation.meta --- api/src/utils/apply-query.ts | 2 +- .../v-field-select/v-field-select.vue | 2 +- .../v-field-template/v-field-template.vue | 2 +- app/src/composables/use-field-tree.ts | 244 +++++++----------- .../system-field-tree/system-field-tree.vue | 2 +- .../_system/system-filter/nodes.vue | 2 +- .../_system/system-filter/system-filter.vue | 16 +- app/src/stores/fields.ts | 20 +- 8 files changed, 109 insertions(+), 181 deletions(-) diff --git a/api/src/utils/apply-query.ts b/api/src/utils/apply-query.ts index 08f83aa82b..09cc310a5d 100644 --- a/api/src/utils/apply-query.ts +++ b/api/src/utils/apply-query.ts @@ -221,7 +221,7 @@ export function applyFilter( .on( `${parentAlias || parentCollection}.${relation.field}`, '=', - knex.raw(`CAST(?? AS TEXT)`, `${alias}.${schema.collections[pathScope].primary}`) + knex.raw(`CAST(?? AS VARCHAR(255))`, `${alias}.${schema.collections[pathScope].primary}`) ) .andOnVal(relation.meta!.one_collection_field!, '=', pathScope); }); diff --git a/app/src/components/v-field-select/v-field-select.vue b/app/src/components/v-field-select/v-field-select.vue index 23510cecd6..5bf93c68f5 100644 --- a/app/src/components/v-field-select/v-field-select.vue +++ b/app/src/components/v-field-select/v-field-select.vue @@ -27,7 +27,7 @@ - + - + diff --git a/app/src/composables/use-field-tree.ts b/app/src/composables/use-field-tree.ts index 6f6923eb00..db8f2914aa 100644 --- a/app/src/composables/use-field-tree.ts +++ b/app/src/composables/use-field-tree.ts @@ -1,20 +1,20 @@ import { useFieldsStore, useRelationsStore } from '@/stores/'; import { Field, Relation } from '@directus/shared/types'; import { getRelationType } from '@directus/shared/utils'; -import { get, set } from 'lodash'; -import { computed, Ref, ref, ComputedRef, watch } from 'vue'; +import { Ref, ref, watch } from 'vue'; -export type FieldTree = Record; -export type FieldInfo = { name: string; field: string; children: FieldTree; collection: string; type: string }; -export type FieldOption = { name: string; field: string; key: string; children?: FieldOption[] }; +export type FieldNode = { + name: string; + field: string; + collection: string; + relatedCollection?: string; + key: string; + children?: FieldNode[]; +}; export type FieldTreeContext = { - tree: Ref; - treeList: ComputedRef; - loadFieldRelations: (fieldPath: string, depth?: number) => void; - getField: (fieldPath: string) => FieldOption | undefined; - treeToList: (tree: FieldTree, parentName?: string) => FieldOption[]; - visitedRelations: Ref; + treeList: Ref; + loadFieldRelations: (fieldPath: string, root?: FieldNode) => void; }; export function useFieldTree( @@ -25,170 +25,104 @@ export function useFieldTree( const fieldsStore = useFieldsStore(); const relationsStore = useRelationsStore(); - const tree = ref({}); + const treeList = ref([]); + const visitedPaths = ref>(new Set()); - const visitedRelations = ref([]); + watch(() => collection.value, refresh, { immediate: true }); - watch( - collection, - () => { - if (collection.value) { - tree.value = getFieldTreeForCollection(collection.value, 'any'); - visitedRelations.value = []; - Object.values(tree.value).forEach((value) => { - loadFieldRelations(value.field); - }); - } - }, - { immediate: true } - ); + return { treeList, loadFieldRelations }; - const treeList = computed(() => treeToList(tree.value)); + function refresh(collection?: string | null) { + visitedPaths.value = new Set(); + treeList.value = getTree(collection) ?? []; + for (const node of treeList.value) { + node.children = getTree(node.relatedCollection, node); + } + } - return { tree, treeList, loadFieldRelations, getField, treeToList, visitedRelations }; + function getTree(collection?: string | null, parent?: FieldNode) { + const injectedFields = inject?.value?.fields.filter((field) => field.collection === collection); + const fields = fieldsStore + .getFieldsForCollection(collection!) + .concat(injectedFields || []) + .filter((field: Field) => !field.meta?.special?.includes('alias') && !field.meta?.special?.includes('no-data')) + .filter(filter) + .flatMap((field) => makeNode(field, parent)); - function treeToList(tree: FieldTree, parentName?: string): FieldOption[] { - return Object.values(tree).map((field) => { - const fieldName = field.type === 'm2a' ? `${field.field}:${field.collection}` : field.field; - const key = parentName ? `${parentName}.${fieldName}` : fieldName; - const children = treeToList(field.children, key); + return fields.length ? fields : undefined; + } + + function getRelatedCollections(field: Field): string[] { + const relation = getRelationForField(field); + if (!relation?.meta) return []; + const relationType = getRelationType({ relation, collection: field.collection, field: field.field }); + switch (relationType) { + case 'o2m': + return [relation!.meta!.many_collection]; + case 'm2o': + return [relation!.meta!.one_collection]; + case 'm2a': + return relation!.meta!.one_allowed_collections!; + default: + return []; + } + } + + function makeNode(field: Field, parent?: FieldNode): FieldNode | FieldNode[] { + const relatedCollections = getRelatedCollections(field); + const context = parent ? parent.key + '.' : ''; + if (relatedCollections.length <= 1) { return { name: field.name, - key, - field: fieldName, - children: children.length > 0 ? children : undefined, - selectable: !children || children.length === 0, + field: field.field, + collection: field.collection, + relatedCollection: relatedCollections[0], + key: context + field.field, + }; + } + return relatedCollections.map((collection) => { + return { + name: `${field.name} (${collection})`, + field: `${field.field}:${collection}`, + collection: field.collection, + relatedCollection: collection, + key: context + `${field.field}:${collection}`, }; }); } - function getFieldTreeForCollection(collection: string, type: string) { - const fields = [ - ...fieldsStore.getFieldsForCollection(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); - - return fields.reduce((acc, field) => { - if (type === 'm2a') { - const fieldName = `${field.field}:${collection}`; - acc[fieldName] = { - field: field.field, - name: `${field.name} (${field.collection})`, - collection: field.collection, - type, - children: {}, - }; - } else { - acc[field.field] = { - field: field.field, - name: field.name, - collection: field.collection, - type, - children: {}, - }; - } - - return acc; - }, {} as FieldTree); - } - - function getField(fieldPath: string): FieldOption | undefined { - const path = fieldPath.split('.'); - - function getFieldRecursive(path: string[], list: FieldOption[]): FieldOption | undefined { - const targetField = path.shift(); - const subList = list.find((el) => el.field === targetField); - if (subList === undefined || subList.children === undefined) return undefined; - if (path.length === 0) return subList; - return getFieldRecursive(path, subList.children); - } - - return getFieldRecursive(path, treeList.value); - } - - function loadFieldRelations(fieldPath: string, depth = 0) { - _loadFieldRelations(fieldPath); - if (depth === 0) return; - const field = getField(fieldPath); - if (!field) return; - - field.children?.forEach((child) => loadFieldRelations(child.key, depth - 1)); - } - - function _loadFieldRelations(fieldPath: string) { - const path = fieldPath.replaceAll('.', '.children.'); - const field = get(tree.value, path) as FieldInfo | undefined; - if (field === undefined || Object.keys(field.children).length > 0) return; - + function getRelationForField(field: { collection: string; field: string }) { const relations = [ ...relationsStore.getRelationsForField(field.collection, field.field), ...(inject?.value?.relations || []), ]; - const relation = getRelation(relations, field.collection, field.field); - - if (relations.length === 0 || !relation || !relation.meta) return; - - if (relationVisited(relation)) return; - - const relationType = getRelationType({ relation, collection: field.collection, field: field.field }); - if (relation.meta === undefined) return; - - let children: FieldTree = {}; - - if (relationType === 'o2m') { - children = getFieldTreeForCollection(relation.meta.many_collection, relationType); - } else if (relationType === 'm2o') { - children = getFieldTreeForCollection(relation.meta.one_collection, relationType); - } else if (relationType === 'm2a') { - children = - relation.meta.one_allowed_collections?.reduce((acc, collection) => { - return { ...acc, ...getFieldTreeForCollection(collection, relationType) }; - }, {}) || {}; - } - - Object.values(children).forEach((child) => { - const relation: string[] = [field.collection, field.field, child.collection, child.field]; - const exists = visitedRelations.value.findIndex((rel) => relationEquals(rel, relation)) !== -1; - - if (exists === false) visitedRelations.value.push(relation); - }); - - set(tree.value, `${path}.children`, children); - } - - function relationVisited(relation: Relation) { - if (!relation.meta) return; - - if (relation.meta.one_collection_field !== null && relation.meta.one_allowed_collections !== null) return false; - - const simpleRelation: string[] = [ - relation.meta.many_collection, - relation.meta.one_collection, - relation.meta.many_field, - relation.meta.one_field || '', - ]; - - return visitedRelations.value.find((relation) => relationEquals(simpleRelation, relation)) !== undefined; - } - - function getRelation(relations: Relation[], collection: string, field: string) { return relations.find( (relation: Relation) => - (relation.collection === collection && relation.field === field) || - (relation.related_collection === collection && relation.meta?.one_field === field) + (relation.collection === field.collection && relation.field === field.field) || + (relation.related_collection === field.collection && relation.meta?.one_field === field.field) ); } - function relationEquals(rel1: string[], rel2: string[]) { - for (const rel of rel1) { - if (rel2.includes(rel) === false) return false; + function getNodeAtPath(path: string, root?: FieldNode[]): FieldNode | undefined { + const [field, ...follow] = path.split('.'); + for (const node of root || []) { + if (node.field === field) { + if (follow.length) { + return getNodeAtPath(follow.join('.'), node.children); + } else { + return node; + } + } + } + } + + function loadFieldRelations(path: string) { + if (!visitedPaths.value.has(path)) { + visitedPaths.value.add(path); + const node = getNodeAtPath(path, treeList.value); + for (const child of node?.children || []) { + child.children = getTree(child.relatedCollection, child); + } } - return true; } } diff --git a/app/src/interfaces/_system/system-field-tree/system-field-tree.vue b/app/src/interfaces/_system/system-field-tree/system-field-tree.vue index 2f5b8ffb14..427c098c69 100644 --- a/app/src/interfaces/_system/system-field-tree/system-field-tree.vue +++ b/app/src/interfaces/_system/system-field-tree/system-field-tree.vue @@ -14,7 +14,7 @@ item-value="key" value-combining="exclusive" @update:model-value="$emit('input', $event)" - @group-toggle="loadFieldRelations($event.value, 1)" + @group-toggle="loadFieldRelations($event.value)" /> diff --git a/app/src/interfaces/_system/system-filter/nodes.vue b/app/src/interfaces/_system/system-filter/nodes.vue index 1a2afdc1aa..bc194439b2 100644 --- a/app/src/interfaces/_system/system-filter/nodes.vue +++ b/app/src/interfaces/_system/system-filter/nodes.vue @@ -27,7 +27,7 @@ :items="fieldOptions" :mandatory="false" :groups-clickable="true" - @group-toggle="loadFieldRelations($event.value, 1)" + @group-toggle="loadFieldRelations($event.value)" @update:modelValue="updateField(index, $event)" > diff --git a/app/src/interfaces/_system/system-filter/system-filter.vue b/app/src/interfaces/_system/system-filter/system-filter.vue index cb9b71ac9a..3ebea1f5b1 100644 --- a/app/src/interfaces/_system/system-filter/system-filter.vue +++ b/app/src/interfaces/_system/system-filter/system-filter.vue @@ -33,7 +33,7 @@ :items="fieldOptions" :mandatory="false" :groups-clickable="true" - @group-toggle="loadFieldRelations($event.value, 1)" + @group-toggle="loadFieldRelations($event.value)" @update:modelValue="addNode($event)" >