mirror of
https://github.com/directus/directus.git
synced 2026-01-26 12:28:06 -05:00
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
This commit is contained in:
@@ -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);
|
||||
});
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
</v-button>
|
||||
</template>
|
||||
|
||||
<v-list :mandatory="false" @toggle="loadFieldRelations($event.value, 1)">
|
||||
<v-list :mandatory="false" @toggle="loadFieldRelations($event.value)">
|
||||
<field-list-item
|
||||
v-for="field in availableFields"
|
||||
:key="field.key"
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
</v-input>
|
||||
</template>
|
||||
|
||||
<v-list v-if="!disabled" :mandatory="false" @toggle="loadFieldRelations($event.value, 1)">
|
||||
<v-list v-if="!disabled" :mandatory="false" @toggle="loadFieldRelations($event.value)">
|
||||
<field-list-item v-for="field in treeList" :key="field.field" :field="field" :depth="depth" @add="addField" />
|
||||
</v-list>
|
||||
</v-menu>
|
||||
|
||||
@@ -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<string, FieldInfo>;
|
||||
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<FieldTree>;
|
||||
treeList: ComputedRef<FieldOption[]>;
|
||||
loadFieldRelations: (fieldPath: string, depth?: number) => void;
|
||||
getField: (fieldPath: string) => FieldOption | undefined;
|
||||
treeToList: (tree: FieldTree, parentName?: string) => FieldOption[];
|
||||
visitedRelations: Ref<string[][]>;
|
||||
treeList: Ref<FieldNode[]>;
|
||||
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<FieldTree>({});
|
||||
const treeList = ref<FieldNode[]>([]);
|
||||
const visitedPaths = ref<Set<string>>(new Set());
|
||||
|
||||
const visitedRelations = ref<string[][]>([]);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -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)"
|
||||
>
|
||||
<template #preview>{{ getFieldPreview(element) }}</template>
|
||||
|
||||
@@ -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)"
|
||||
>
|
||||
<template v-if="inline" #prepend>
|
||||
@@ -150,20 +150,12 @@ export default defineComponent({
|
||||
|
||||
function addNode(key: string) {
|
||||
if (key === '$group') {
|
||||
innerValue.value = [
|
||||
...innerValue.value,
|
||||
{
|
||||
_and: [],
|
||||
},
|
||||
];
|
||||
innerValue.value = innerValue.value.concat({ _and: [] });
|
||||
} else {
|
||||
const filterObj = {};
|
||||
|
||||
const field = fieldsStore.getField(collection.value, key)!;
|
||||
const operator = getFilterOperatorsForType(field.type)[0];
|
||||
set(filterObj, key, { ['_' + operator]: null });
|
||||
|
||||
innerValue.value = [...innerValue.value, filterObj] as FieldFilter[];
|
||||
const node = set({}, key, { ['_' + operator]: null });
|
||||
innerValue.value = innerValue.value.concat(node);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -268,19 +268,21 @@ export const useFieldsStore = defineStore({
|
||||
*/
|
||||
getRelationalField(collection: string, fields: string) {
|
||||
const relationsStore = useRelationsStore();
|
||||
const parts = fields.split('.');
|
||||
const [field, ...path] = fields.split('.');
|
||||
if (field.includes(':')) {
|
||||
const [_, collection] = field.split(':');
|
||||
return this.getField(collection, path.join('.'));
|
||||
}
|
||||
|
||||
const relation = relationsStore
|
||||
.getRelationsForField(collection, parts[0])
|
||||
?.find(
|
||||
(relation: Relation) => relation.field === parts[0] || relation.meta?.one_field === parts[0]
|
||||
) as Relation;
|
||||
const relations = relationsStore.getRelationsForField(collection, field);
|
||||
const relation = relations?.find((relation: Relation) => {
|
||||
return relation.field === field || relation.meta?.one_field === field;
|
||||
});
|
||||
|
||||
if (relation === undefined) return false;
|
||||
|
||||
const relatedCollection = relation.field === parts[0] ? relation.related_collection : relation.collection;
|
||||
parts.shift();
|
||||
const relatedField = parts.join('.');
|
||||
const relatedCollection = relation.field === field ? relation.related_collection : relation.collection;
|
||||
const relatedField = path.join('.');
|
||||
return this.getField(relatedCollection, relatedField);
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user