directus-extension-seo
Version:
Directus SEO Extension enhance Directus with powerful SEO scoring and validator and AI SEO from ChatGPT
211 lines (177 loc) • 6.21 kB
text/typescript
import { useStores } from '@directus/extensions-sdk';
import { Field, Relation, Type } from '@directus/types';
import { getRelationType } from '@directus/utils';
import isNil from 'lodash/isNil';
import { Ref, ref, watch } from 'vue';
export type FieldNode = {
name: string;
field: string;
collection: string;
relatedCollection?: string;
key: string;
path: string;
type: Type;
children?: FieldNode[];
group?: boolean;
_loading?: boolean;
};
export type FieldTreeContext = {
treeList: Ref<FieldNode[]>;
loadFieldRelations: (fieldPath: string, root?: FieldNode) => void;
refresh: (collection?: string) => void;
};
export function useFieldTree(
collection: Ref<string | null>,
inject?: Ref<{ fields: Field[]; relations: Relation[] } | null>,
filter: (field: Field, parent?: FieldNode) => boolean = () => true
): FieldTreeContext {
const { useFieldsStore, useRelationsStore } = useStores()
const fieldsStore = useFieldsStore();
const relationsStore = useRelationsStore();
const treeList = ref<FieldNode[]>([]);
const visitedPaths = ref<Set<string>>(new Set());
watch(() => collection.value, refresh, { immediate: true });
return { treeList, loadFieldRelations, refresh };
function refresh() {
visitedPaths.value = new Set();
treeList.value = getTree(collection.value) ?? [];
for (const node of treeList.value) {
if (node.relatedCollection) {
node.children = getTree(node.relatedCollection, node);
}
}
}
function getTree(collection?: string | null, parent?: FieldNode) {
const injectedFields = inject?.value?.fields.filter((field) => field.collection === collection);
const allFields = fieldsStore
.getFieldsForCollectionSorted(collection!)
.concat(injectedFields || [])
.filter(
(field) =>
field.meta?.special?.includes('group') ||
(!field.meta?.special?.includes('alias') && !field.meta?.special?.includes('no-data'))
)
.filter((field) => filter(field, parent));
const topLevelFields = allFields.filter((field) => {
if (parent?.group === true) return field.meta?.group === parent?.field;
return isNil(field.meta?.group);
});
const fieldNodes = topLevelFields.flatMap((field) => makeNode(field, parent));
return fieldNodes.length ? fieldNodes : undefined;
}
function makeNode(field: Field, parent?: FieldNode): FieldNode | FieldNode[] {
const { relationType, relatedCollections } = getRelationTypeAndRelatedCollections(field);
const pathContext = parent?.path ? parent.path + '.' : '';
const keyContext = parent?.key ? parent.key + '.' : '';
if (field?.meta?.special?.includes('group')) {
const node: FieldNode = {
name: field.name,
field: field.field,
collection: field.collection,
relatedCollection: undefined,
key: parent ? parent.key : '',
path: pathContext + field.field,
group: true,
type: field.type,
};
const children = getTree(field.collection, node);
if (children) {
for (const child of children) {
if (child.relatedCollection) {
child.children = [
{ name: 'Loading...', field: '', collection: '', key: '', path: '', type: 'alias', _loading: true },
];
}
}
}
return {
...node,
children,
};
}
if (relatedCollections.length <= 1 && relationType !== 'm2a') {
return {
name: field.name,
field: field.field,
collection: field.collection,
relatedCollection: relatedCollections[0],
key: keyContext + field.field,
path: pathContext + field.field,
type: field.type,
};
}
return relatedCollections.map((collection) => {
return {
name: `${field.name} (${collection})`,
field: `${field.field}:${collection}`,
collection: field.collection,
relatedCollection: collection,
key: keyContext + `${field.field}:${collection}`,
path: pathContext + `${field.field}:${collection}`,
type: field.type,
};
});
}
function getRelationTypeAndRelatedCollections(field: Field): {
relationType: 'o2m' | 'm2o' | 'm2a' | null;
relatedCollections: string[];
} {
const relation = getRelationForField(field);
if (!relation?.meta) return { relationType: null, relatedCollections: [] };
const relationType = getRelationType({ relation, collection: field.collection, field: field.field });
switch (relationType) {
case 'o2m':
return { relationType: 'o2m', relatedCollections: [relation!.meta!.many_collection] };
case 'm2o':
return { relationType: 'm2o', relatedCollections: [relation!.meta!.one_collection!] };
case 'm2a':
return { relationType: 'm2a', relatedCollections: relation!.meta!.one_allowed_collections! };
default:
return { relationType: null, relatedCollections: [] };
}
}
function getRelationForField(field: { collection: string; field: string }) {
const relations = [
...relationsStore.getRelationsForField(field.collection, field.field),
...(inject?.value?.relations || []),
];
return relations.find(
(relation: Relation) =>
(relation.collection === field.collection && relation.field === field.field) ||
(relation.related_collection === field.collection && relation.meta?.one_field === field.field)
);
}
function getNodeAtPath([field, ...path]: string[], root?: FieldNode[]): FieldNode | undefined {
let node = root?.find((node) => node.field === field);
if (!node) {
node = root
?.reduce<FieldNode[]>((acc, node) => {
if (node.group === true && node.children && node.children.length > 0) {
acc.push(...node.children);
}
return acc;
}, [])
.find((node) => node.field === field);
}
if (!node) return undefined;
if (path.length) {
return getNodeAtPath(path, node.children);
} else {
return node;
}
}
function loadFieldRelations(path: string) {
if (!visitedPaths.value.has(path)) {
visitedPaths.value.add(path);
const node = getNodeAtPath(path.split('.'), treeList.value);
if (node && node.children?.length === 1 && node.children[0]._loading) {
node.children = getTree(node.relatedCollection, node);
}
for (const child of node?.children || []) {
if (child?.relatedCollection) {
child.children = getTree(child.relatedCollection, child);
}
}
}
}
}