From 37e482dd1798c2692ae7f74aa0a131fb4771a592 Mon Sep 17 00:00:00 2001 From: Tim Butterfield <124673267+timio23@users.noreply.github.com> Date: Sat, 26 Apr 2025 02:21:36 +0100 Subject: [PATCH 01/31] initial upload --- packages/related-items-bundle/README.md | 35 ++ .../docs/related-items-bundle.jpg | Bin 0 -> 100031 bytes packages/related-items-bundle/package.json | 67 ++++ .../src/related-items-endpoint/index.ts | 304 ++++++++++++++++++ .../utils/display-template.ts | 32 ++ .../src/related-items-hook/index.ts | 35 ++ .../src/related-items-interface/index.ts | 14 + .../src/related-items-interface/interface.vue | 29 ++ .../related-items-list.vue | 302 +++++++++++++++++ .../src/related-items-module/index.ts | 14 + .../related-items-module/settings-field.ts | 22 ++ .../src/related-items-module/shim.d.ts | 6 + .../utils/get-directus-app.ts | 4 + .../utils/get-directus-router.ts | 7 + .../utils/inject-related-items-field.ts | 54 ++++ .../utils/unexpected-error.ts | 19 ++ .../src/shared/alias-field.ts | 20 ++ packages/related-items-bundle/src/types.ts | 35 ++ packages/related-items-bundle/tsconfig.json | 28 ++ 19 files changed, 1027 insertions(+) create mode 100644 packages/related-items-bundle/README.md create mode 100644 packages/related-items-bundle/docs/related-items-bundle.jpg create mode 100644 packages/related-items-bundle/package.json create mode 100644 packages/related-items-bundle/src/related-items-endpoint/index.ts create mode 100644 packages/related-items-bundle/src/related-items-endpoint/utils/display-template.ts create mode 100644 packages/related-items-bundle/src/related-items-hook/index.ts create mode 100644 packages/related-items-bundle/src/related-items-interface/index.ts create mode 100644 packages/related-items-bundle/src/related-items-interface/interface.vue create mode 100644 packages/related-items-bundle/src/related-items-interface/related-items-list.vue create mode 100644 packages/related-items-bundle/src/related-items-module/index.ts create mode 100644 packages/related-items-bundle/src/related-items-module/settings-field.ts create mode 100644 packages/related-items-bundle/src/related-items-module/shim.d.ts create mode 100644 packages/related-items-bundle/src/related-items-module/utils/get-directus-app.ts create mode 100644 packages/related-items-bundle/src/related-items-module/utils/get-directus-router.ts create mode 100644 packages/related-items-bundle/src/related-items-module/utils/inject-related-items-field.ts create mode 100644 packages/related-items-bundle/src/related-items-module/utils/unexpected-error.ts create mode 100644 packages/related-items-bundle/src/shared/alias-field.ts create mode 100644 packages/related-items-bundle/src/types.ts create mode 100644 packages/related-items-bundle/tsconfig.json diff --git a/packages/related-items-bundle/README.md b/packages/related-items-bundle/README.md new file mode 100644 index 00000000..7d08d20f --- /dev/null +++ b/packages/related-items-bundle/README.md @@ -0,0 +1,35 @@ +# Related Items Bundle + +Find all collections, fields or items related to a collection even if it doesn't include the reverse fields from the supported relationships: Many to One (m2o), One to Many (o2m), Many to Many (m2m) or Many to Any (m2a). + +![Related Items Bundle](https://raw.githubusercontent.com/directus-labs/extensions/main/packages/related-items-bundle/docs/related-items-bundle.jpg) + +You can include system collections such as Users and Files and benefit from seeing all the relations to other collections. For example, when viewing a file in the File Library, you will see what collections and items reference the file and know what will be impacted if it is edited or deleted. + +## Usage + +Once installed and configured, visit an item for an included collection and scroll to the bottom of the page. By default, all related items are returned with pagination over 10 records. Use the collection filters at the top to easily see the related items in that collection. + +Click on an item to open the draw for more information or make changes to that item. However, some system tables are not supported. + +## Requirements + +- Directus 11.0.0+ +- Admin user to initialize module + +## Installation + +Refer to the Official Guide for details on installing the extension from the Marketplace or manually. + +## How to configure this module + +1. Using an Admin user, go to the project settings `/admin/settings/project` and scroll to the bottom +2. Click on the field labelled "Related Items Collections" +3. Tick the collections to include from the dropdown field +4. Save changes + +When opening the project settings for the first time, the module will automatically add the new system field at the bottom of the page. Any selection of the collections will create a new alias field within that collection's Data model. The new interface can be repositioned or customized but any changes to these fields will be lost if you choose to exclude the collection in future. Admin permissions are required during these steps. + +## Permissions + +This extension uses the current user's policy/role permissions and will only show the permitted data. Please refer to your Access Policies to ensure your users have required access. \ No newline at end of file diff --git a/packages/related-items-bundle/docs/related-items-bundle.jpg b/packages/related-items-bundle/docs/related-items-bundle.jpg new file mode 100644 index 0000000000000000000000000000000000000000..c31d96b1e53b544348f54d23f6155e886eb14010 GIT binary patch literal 100031 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%0A97%q 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 literal 0 HcmV?d00001 diff --git a/packages/related-items-bundle/package.json b/packages/related-items-bundle/package.json new file mode 100644 index 00000000..7fff3eee --- /dev/null +++ b/packages/related-items-bundle/package.json @@ -0,0 +1,67 @@ +{ + "name": "@directus-labs/related-items-bundle", + "type": "module", + "version": "1.0.0", + "description": "Show all related items across selected collections.", + "license": "MIT", + "keywords": [ + "directus", + "directus-extension", + "directus-extension-bundle" + ], + "icon": "extension", + "files": [ + "dist" + ], + "directus:extension": { + "type": "bundle", + "path": { + "app": "dist/app.js", + "api": "dist/api.js" + }, + "entries": [ + { + "type": "module", + "name": "related-items-module", + "source": "src/related-items-module/index.ts" + }, + { + "type": "endpoint", + "name": "related-items-endpoint", + "source": "src/related-items-endpoint/index.ts" + }, + { + "type": "interface", + "name": "related-items-interface", + "source": "src/related-items-interface/index.ts" + }, + { + "type": "hook", + "name": "related-items-hook", + "source": "src/related-items-hook/index.ts" + } + ], + "host": "^11.1.2" + }, + "scripts": { + "build": "directus-extension build", + "dev": "directus-extension build -w --no-minify", + "link": "directus-extension link", + "add": "directus-extension add", + "validate": "directus-extension validate" + }, + "dependencies": { + "@directus/eslint-config": "^0.1.0", + "@directus/format-title": "^12.0.1", + "vue-i18n": "^9.14.0", + "vue-router": "^4.4.5" + }, + "devDependencies": { + "@directus/constants": "^13.0.0", + "@directus/extensions-sdk": "^13.0.1", + "@directus/types": "^13.0.0", + "@directus/utils": "^13.0.0", + "typescript": "^5.6.3", + "vue": "^3.5.13" + } +} diff --git a/packages/related-items-bundle/src/related-items-endpoint/index.ts b/packages/related-items-bundle/src/related-items-endpoint/index.ts new file mode 100644 index 00000000..15049883 --- /dev/null +++ b/packages/related-items-bundle/src/related-items-endpoint/index.ts @@ -0,0 +1,304 @@ +import type { Accountability, Collection, Field, Query, Relation } from '@directus/types'; +import type { RelatedItem } from '../types'; +import { defineEndpoint } from '@directus/extensions-sdk'; +import { getFieldsFromTemplate } from '@directus/utils'; +import { displayTemplate } from './utils/display-template'; + +interface CollectionDetail { + property: Collection; + display_template: string | null | undefined; + template_fields: string[]; + field_name: string; + many_field?: string | null; + item_id: number | string | number[] | string[]; + fields: Field[]; + primaryKey: string; +} + +export default defineEndpoint({ + id: 'related-items', + handler: (router, { services, getSchema }) => { + const { + FieldsService, + CollectionsService, + ItemsService, + RelationsService, + + } = services; + + router.get('/:collection/:id', async (req, res) => { + const collection: string = req.params.collection; + const primaryId: number | string = req.params.id; + const query = req.query; + + const accountability: Accountability | null = 'accountability' in req ? req.accountability as Accountability : null; + const schema = await getSchema(); + + // Services + const collectionService = new CollectionsService({ + accountability, + schema, + }); + + const relationService = new RelationsService({ + accountability, + schema, + }); + + const fieldService = new FieldsService({ + accountability, + schema, + }); + + async function fetchItem({ collection, id, query }: { collection: string | null; id?: number | string; query?: Query }) { + if (!collection || (!id && !query)) return; + + const itemService = new ItemsService(collection, { + accountability, + schema, + }); + return id ? await itemService.readOne(id) : await itemService.readByQuery(query); + } + + // Fetch context + const requested_item: Record = await fetchItem({ collection, id: primaryId, query }); + + // Fetch all relations + const relations: Relation[] = await relationService.readAll(); + + // Extract O2M relations for current collection + const related_o2m_collections: Relation[] = relations.filter((r) => r.collection === collection); + + // Extract M2O relations for current collection + const related_m2o_collections: Relation[] = relations.filter((r) => r.related_collection === collection || r.meta?.one_allowed_collections?.includes(collection)); + + // Fetch collection metadata + async function fetchCollectionInfo({ collection, field_name, item_id, relation }: { collection: string | null; field_name: string; item_id: number | string | number[] | string[]; relation: Relation }): Promise { + if (!collection) return null; + const current_collection: Collection = await collectionService.readOne(collection); + const display_template = displayTemplate(collection, current_collection.meta?.display_template); + const template_fields = getFieldsFromTemplate(display_template).filter((t) => !t.includes('$')); + const fields: Field[] = await fieldService.readAll(collection); + const primaryKey: string = fields.find((f) => f.schema?.is_primary_key)?.field ?? 'id'; + return { property: current_collection, display_template, template_fields, field_name, many_field: relation.meta?.junction_field, item_id, fields, primaryKey }; + } + + // Iterate over "Any" collections and fetch metadata + async function fetchM2aCollectionInfo({ collections, m2m_relation, relation }: { collections: string[]; m2m_relation: Relation; relation: Relation }) { + if (collections.length === 0 || m2m_relation === null) return []; + const promises = collections.map(async (collection) => { + if (m2m_relation.meta?.many_field && m2m_relation.meta.junction_field && m2m_relation.meta?.one_collection_field) { + // Field containing the ID of the linked item + const many_field: string = m2m_relation.meta?.many_field; + // Field containing the ID of the parent item + const junction_field: string = m2m_relation.meta.junction_field; + // Field containing the collection ID + const one_collection_field: string = m2m_relation.meta?.one_collection_field; + + try { + // Fetch IDs of related content + const m2a_junction_items: Record[] = await fetchItem({ collection: m2m_relation.collection, query: { + fields: [ + many_field, + ], + filter: { + [junction_field]: { + _eq: primaryId, + }, + [one_collection_field]: { + _eq: collection, + }, + }, + limit: -1, + } }); + + // List of all item IDs belonging to the primary ID + const item_ids = m2a_junction_items.map((i) => i[many_field]) as number[] | string[]; + return await fetchCollectionInfo({ collection, field_name: relation.meta?.one_field ?? relation.field, item_id: item_ids, relation }); + } + catch (error) { + console.warn(error); + return null; + } + } + else { + return null; + } + }); + return Promise.all(promises); + } + + async function build_output({ o2m, is_m2a, is_junction, relation_type, collection, related_collection, relation }: { o2m: boolean; is_m2a: boolean; is_junction: boolean; relation_type: string; collection: CollectionDetail; related_collection?: string; relation: Relation }) { + // Build array of fields required for visual output + const itemFields = [ + relation_type === 'm2m' ? `${collection.many_field}.${collection.primaryKey}` : collection.primaryKey, + ...collection.template_fields.map((f) => relation_type === 'm2m' ? `${collection.many_field}.${f}` : f), + ...( + collection.property.collection === 'directus_files' + ? [relation_type === 'm2m' ? `${collection.many_field}.type` : 'type'] + : [] + ), + ...( + collection.property.collection === 'directus_comments' + ? ['date_created'] + : [] + ), + ...( + collection.property.collection === 'directus_panels' + ? ['dashboard.name'] + : [] + ), + ...( + collection.property.collection === 'directus_notifications' + ? ['timestamp'] + : [] + ), + ]; + + // Filters to fetch related content + const itemFilters = o2m + ? (Array.isArray(collection.item_id) + ? { + [collection.primaryKey]: { + _in: collection.item_id, + }, + } + : { + [collection.field_name]: { + _eq: collection.item_id, + }, + }) + : { + [is_junction ? collection.field_name : collection.primaryKey]: { + _eq: collection.item_id, + }, + }; + + // Top level collection info + const collectionInfo = { + collection: collection.property.collection, + fields: itemFields, + relation: relation_type, + translations: collection.property.meta?.translations, + field: collection.field_name, + junction_field: is_junction ? relation.meta?.junction_field : null, + primary_key: collection.primaryKey, + template: collection.display_template ?? `{{ ${collection.primaryKey} }}`, + } as RelatedItem; + + try { + const relatedItems = await fetchItem({ collection: is_m2a || related_collection === undefined ? collection.property.collection : related_collection, query: { + fields: itemFields, + // @ts-expect-error saying _and is missing but it's not required + filter: itemFilters, + limit: -1, + } }); + return { + ...collectionInfo, + items: relatedItems, + }; + } + catch { + return { + ...collectionInfo, + items: [], + }; + } + } + + async function relatedCollections(relations: Relation[]) { + const promises = relations.map(async (r) => { + const o2m = r.related_collection === collection; + const is_junction = r.meta?.junction_field !== null; + const is_m2a = is_junction && r.meta?.junction_field === 'item'; + + const RelationType = () => { + if (is_m2a) return 'm2a'; + if (is_junction) return 'm2m'; + if (o2m) return 'o2m'; + return 'm2o'; + }; + + const calculateField = () => { + if (is_junction && o2m && !is_m2a) return r.field; + if (o2m) return r.meta?.one_field ?? r.field; + return r.meta?.many_field ?? r.field; + }; + + const field = calculateField(); + + const relatedCollection = () => { + if (o2m) return r.collection; + if (r.related_collection) return r.related_collection; + return r.collection; + }; + + const related_collection = await fetchCollectionInfo({ + collection: relatedCollection(), + field_name: field, // M2O: Field in current table where the Many item ID lived + // O2M: Field in Many table where the current item ID lived + item_id: o2m ? primaryId : requested_item[field], + // O2M: Current Item ID M2O: ID of Item in Many table + relation: r, + }); + + const m2m_relation = is_junction ? await relationService.readOne(related_collection?.property.collection, r.meta?.junction_field) : null; + + const m2m_related_collection = m2m_relation + ? await fetchCollectionInfo({ + collection: m2m_relation ? m2m_relation.related_collection : null, + field_name: field, + item_id: field === 'item' || is_junction ? primaryId : (r.meta?.many_field ? requested_item[r.meta.many_field] : requested_item[field]), + relation: r, + }) + : null; + + const m2a_related_collections = is_m2a + ? await fetchM2aCollectionInfo({ + collections: m2m_relation.meta?.one_allowed_collections, + m2m_relation, + relation: r, + }) + : []; + + const collections: (CollectionDetail | null)[] = is_m2a + ? m2a_related_collections + : (is_junction + ? [m2m_related_collection] + : [related_collection]); + + async function processCollections(collections: (CollectionDetail | null)[], related_collection: string) { + const output_promise = collections.filter((c) => c && c.item_id).map(async (c) => await build_output({ + o2m, + is_m2a, + is_junction, + relation_type: RelationType(), + collection: c as CollectionDetail, + related_collection, + relation: r, + })); + return Promise.all(output_promise); + } + + return related_collection ? (await processCollections(collections, related_collection.property.collection)) as RelatedItem[] : []; + }); + return Promise.all(promises); + } + + const related_content: RelatedItem[] = []; + + const o2m_collection_items = await relatedCollections(related_o2m_collections); + const m2o_collection_items = await relatedCollections(related_m2o_collections); + + o2m_collection_items.forEach((c) => { + related_content.push(...c); + }); + + m2o_collection_items.forEach((c) => { + related_content.push(...c); + }); + + res.json(related_content); + }); + }, +}); diff --git a/packages/related-items-bundle/src/related-items-endpoint/utils/display-template.ts b/packages/related-items-bundle/src/related-items-endpoint/utils/display-template.ts new file mode 100644 index 00000000..48da029a --- /dev/null +++ b/packages/related-items-bundle/src/related-items-endpoint/utils/display-template.ts @@ -0,0 +1,32 @@ +export function displayTemplate(collection: string | null, display_template?: string | null) { + switch (collection) { + case 'directus_comments': { + return '{{ comment }}'; + } + case 'directus_roles': + case 'directus_dashboards': + case 'directus_policies': + case 'directus_flows': + case 'directus_operations': { + return '{{ name }}'; + } + case 'directus_panels': { + return '{{ type }}'; + } + case 'directus_activity': { + return '{{ action }} ID:{{ item }} in {{ collection }}'; + } + case 'directus_notifications': { + return '{{ subject }}'; + } + case 'directus_files': { + return '{{ title }}'; + } + case 'directus_presets': { + return '{{ collection }} {{ bookmark }}'; + } + // No default + } + + return display_template; +} diff --git a/packages/related-items-bundle/src/related-items-hook/index.ts b/packages/related-items-bundle/src/related-items-hook/index.ts new file mode 100644 index 00000000..e6e81c38 --- /dev/null +++ b/packages/related-items-bundle/src/related-items-hook/index.ts @@ -0,0 +1,35 @@ +import type { Field } from '@directus/types'; +import { defineHook } from '@directus/extensions-sdk'; +import { alias_field } from '../shared/alias-field'; + +export default defineHook(({ action }, { services }) => { + const { FieldsService } = services; + + action('settings.update', async ({ collection, payload }, { accountability, schema }) => { + if (collection === 'directus_settings' && 'related_items_collections' in payload) { + const collections: string[] = payload.related_items_collections; + + const fieldService = new FieldsService({ + accountability, + schema, + }); + + // Fetch all fields + const fields: Field[] = await fieldService.readAll(); + // Filter to existing fields + const existingFields = fields.filter((f) => f.field === 'directus_related_items_alias'); + // Filter a list of orphaned fields + const existingFieldsToDelete = existingFields.filter((f) => !collections.includes(f.collection)); + + existingFieldsToDelete.forEach(async (f) => { + // Remove orphaned fields + await fieldService.deleteField(f.collection, f.field); + }); + + collections.filter((c) => !existingFields.some((f) => f.collection === c)).forEach(async (c) => { + // Create new fields + await fieldService.createField(c, alias_field); + }); + } + }); +}); diff --git a/packages/related-items-bundle/src/related-items-interface/index.ts b/packages/related-items-bundle/src/related-items-interface/index.ts new file mode 100644 index 00000000..7bef534c --- /dev/null +++ b/packages/related-items-bundle/src/related-items-interface/index.ts @@ -0,0 +1,14 @@ +import { defineInterface } from '@directus/extensions-sdk'; +import InterfaceComponent from './interface.vue'; + +export default defineInterface({ + id: 'related-items-interface', + name: 'Related Items', + icon: 'hub', + description: 'Show related items for the current record.', + component: InterfaceComponent, + options: null, + types: ['alias'], + localTypes: ['presentation'], + group: 'relational', +}); diff --git a/packages/related-items-bundle/src/related-items-interface/interface.vue b/packages/related-items-bundle/src/related-items-interface/interface.vue new file mode 100644 index 00000000..3308e2f7 --- /dev/null +++ b/packages/related-items-bundle/src/related-items-interface/interface.vue @@ -0,0 +1,29 @@ + + + diff --git a/packages/related-items-bundle/src/related-items-interface/related-items-list.vue b/packages/related-items-bundle/src/related-items-interface/related-items-list.vue new file mode 100644 index 00000000..6c53461d --- /dev/null +++ b/packages/related-items-bundle/src/related-items-interface/related-items-list.vue @@ -0,0 +1,302 @@ + + + + + diff --git a/packages/related-items-bundle/src/related-items-module/index.ts b/packages/related-items-bundle/src/related-items-module/index.ts new file mode 100644 index 00000000..7d6ac937 --- /dev/null +++ b/packages/related-items-bundle/src/related-items-module/index.ts @@ -0,0 +1,14 @@ +import { defineModule } from '@directus/extensions-sdk'; +import { injectRelatedItemsField } from './utils/inject-related-items-field'; + +export default defineModule({ + id: 'related-items', + hidden: true, + name: 'Related Items', + icon: 'hub', + routes: [], + preRegisterCheck() { + injectRelatedItemsField(); + return true; + }, +}); diff --git a/packages/related-items-bundle/src/related-items-module/settings-field.ts b/packages/related-items-bundle/src/related-items-module/settings-field.ts new file mode 100644 index 00000000..07c36b4b --- /dev/null +++ b/packages/related-items-bundle/src/related-items-module/settings-field.ts @@ -0,0 +1,22 @@ +import type { DeepPartial, Field } from '@directus/types'; + +export const system_field: DeepPartial = { + field: 'related_items_collections', + type: 'string', + name: 'Related Items Collections', + meta: { + required: true, + interface: 'system-collection', + special: [ + 'cast-json', + ], + options: { + includeSystem: true, + includeSingleton: true, + multiple: true, + allowNone: true, + }, + note: 'Select the collections to include the related items module.', + width: 'half', + }, +}; diff --git a/packages/related-items-bundle/src/related-items-module/shim.d.ts b/packages/related-items-bundle/src/related-items-module/shim.d.ts new file mode 100644 index 00000000..afaa5e81 --- /dev/null +++ b/packages/related-items-bundle/src/related-items-module/shim.d.ts @@ -0,0 +1,6 @@ +declare module '*.vue' { + import type { DefineComponent } from 'vue'; + + const component: DefineComponent<{}, {}, any>; + export default component; +} diff --git a/packages/related-items-bundle/src/related-items-module/utils/get-directus-app.ts b/packages/related-items-bundle/src/related-items-module/utils/get-directus-app.ts new file mode 100644 index 00000000..f44cdd6f --- /dev/null +++ b/packages/related-items-bundle/src/related-items-module/utils/get-directus-app.ts @@ -0,0 +1,4 @@ +export function getDirectusApp() { + // @ts-expect-error __vue_app__ does not exist error + return document.querySelector('#app')?.__vue_app__; +} diff --git a/packages/related-items-bundle/src/related-items-module/utils/get-directus-router.ts b/packages/related-items-bundle/src/related-items-module/utils/get-directus-router.ts new file mode 100644 index 00000000..e4057d8f --- /dev/null +++ b/packages/related-items-bundle/src/related-items-module/utils/get-directus-router.ts @@ -0,0 +1,7 @@ +import type { Router } from 'vue-router'; +import { getDirectusApp } from './get-directus-app'; + +export function getDirectusRouter(): Router { + const app = getDirectusApp(); + return app?.config?.globalProperties?.$router; +} diff --git a/packages/related-items-bundle/src/related-items-module/utils/inject-related-items-field.ts b/packages/related-items-bundle/src/related-items-module/utils/inject-related-items-field.ts new file mode 100644 index 00000000..92082f28 --- /dev/null +++ b/packages/related-items-bundle/src/related-items-module/utils/inject-related-items-field.ts @@ -0,0 +1,54 @@ +import { STORES_INJECT } from '@directus/constants'; +import { computed } from 'vue'; +import { system_field } from '../settings-field'; +import { getDirectusApp } from './get-directus-app'; +import { getDirectusRouter } from './get-directus-router'; +import { unexpectedError } from './unexpected-error'; + +export function injectRelatedItemsField() { + const router = getDirectusRouter(); + + if (router) { + router.afterEach(async (to: Record) => { + if (to.name === 'settings-project') { + initializeApp(); + } + }); + } +} + +async function initializeApp(retry: number = 0) { + const titleContainer = document.querySelector('.title-container'); + + if (!titleContainer) { + if (retry < 3) { + setTimeout(() => initializeApp(retry + 1), 100); + } + + return; + } + + const directusApp = getDirectusApp(); + const stores = directusApp._container._vnode.component.provides[STORES_INJECT]; + + const { useFieldsStore, useSettingsStore, useUserStore } = stores; + const fieldStore = useFieldsStore(); + const settingsStore = useSettingsStore(); + const userStore = useUserStore(); + + const isAdmin = computed(() => userStore.currentUser?.role?.admin_access ?? userStore.currentUser?.admin_access ?? false); + + if (isAdmin) { + try { + // Exit if no settings found + if (!('related_items_collections' in settingsStore.settings)) { + // Create required fields + await fieldStore.upsertField('directus_settings', 'related_items_collections', system_field); + await settingsStore.hydrate(); + } + } + catch (error: any) { + unexpectedError(error, stores); + } + } +} diff --git a/packages/related-items-bundle/src/related-items-module/utils/unexpected-error.ts b/packages/related-items-bundle/src/related-items-module/utils/unexpected-error.ts new file mode 100644 index 00000000..442fdf13 --- /dev/null +++ b/packages/related-items-bundle/src/related-items-module/utils/unexpected-error.ts @@ -0,0 +1,19 @@ +export function unexpectedError(error: any, stores: any): void { + const { useNotificationsStore } = stores; + const store = useNotificationsStore(); + + const code = + error?.response?.data?.errors?.[0]?.extensions?.code + || error?.extensions?.code + || 'UNKNOWN'; + + console.warn(error); + + store.add({ + title: code, + type: 'error', + code, + dialog: true, + error, + }); +} diff --git a/packages/related-items-bundle/src/shared/alias-field.ts b/packages/related-items-bundle/src/shared/alias-field.ts new file mode 100644 index 00000000..4b9d82e9 --- /dev/null +++ b/packages/related-items-bundle/src/shared/alias-field.ts @@ -0,0 +1,20 @@ +import type { DeepPartial, Field } from '@directus/types'; + +export const alias_field: DeepPartial = { + field: 'directus_related_items_alias', + type: 'alias', + meta: { + interface: 'related-items-interface', + special: [ + 'alias', + 'no-data', + ], + translations: [ + { + language: 'en-US', + translation: 'Related Items', + }, + ], + width: 'full', + }, +}; diff --git a/packages/related-items-bundle/src/types.ts b/packages/related-items-bundle/src/types.ts new file mode 100644 index 00000000..e277f2e6 --- /dev/null +++ b/packages/related-items-bundle/src/types.ts @@ -0,0 +1,35 @@ +export interface RelatedItem { + collection: string; + relation: 'm2a' | 'm2m' | 'm2o' | 'o2m'; + field?: string | null; + translations: Translations[] | null; + fields: string[]; + junction_field?: string | null; + primary_key: string; + template?: string | null; + items: Record; +} + +export interface RelatedItemObject { + collection: string; + disabled: boolean; + field?: string | null; + relation: 'm2a' | 'm2m' | 'm2o' | 'o2m'; + fields: string[]; + template?: string | null; + item_id: string; + data: Record; +} + +export interface CollectionFilters { + collection: string; + name: string; + item_count: number; +} + +interface Translations { + language: string; + translation: string; + singular: string; + plural: string; +} diff --git a/packages/related-items-bundle/tsconfig.json b/packages/related-items-bundle/tsconfig.json new file mode 100644 index 00000000..6f7c4d5d --- /dev/null +++ b/packages/related-items-bundle/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "target": "ES2019", + "lib": ["ES2019", "DOM"], + "rootDir": "./src", + "moduleResolution": "node", + "resolveJsonModule": false, + "strict": true, + "strictBindCallApply": true, + "strictFunctionTypes": true, + "strictNullChecks": true, + "strictPropertyInitialization": true, + "alwaysStrict": true, + "noFallthroughCasesInSwitch": true, + "noImplicitAny": true, + "noImplicitReturns": true, + "noImplicitThis": true, + "noUncheckedIndexedAccess": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "isolatedModules": true, + "skipLibCheck": true + }, + "include": ["./src/**/*.ts"] +} From 9ad0bff146730f20d7921e39d3b036d73689871e Mon Sep 17 00:00:00 2001 From: Tim Butterfield <124673267+timio23@users.noreply.github.com> Date: Sat, 26 Apr 2025 02:23:47 +0100 Subject: [PATCH 02/31] Update pnpm-lock.yaml --- pnpm-lock.yaml | 55 +++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 50 insertions(+), 5 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 86d832ba..c46ae35a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -805,7 +805,7 @@ importers: version: 9.13.1(vue@3.4.31(typescript@5.5.3)) vue-pdf-embed: specifier: ^2.0.4 - version: 2.0.4(encoding@0.1.13)(vue@3.4.31(typescript@5.5.3)) + version: 2.0.4(vue@3.4.31(typescript@5.5.3)) packages/plausible-analytics-bundle: dependencies: @@ -823,6 +823,40 @@ importers: specifier: ^3.4.30 version: 3.5.12(typescript@5.6.3) + packages/related-items-bundle: + dependencies: + '@directus/eslint-config': + specifier: ^0.1.0 + version: 0.1.0(@typescript-eslint/utils@8.25.0(eslint@9.21.0(jiti@2.4.2))(typescript@5.8.2))(@vue/compiler-sfc@3.5.13)(eslint@9.21.0(jiti@2.4.2))(typescript@5.8.2) + '@directus/format-title': + specifier: ^12.0.1 + version: 12.0.1 + vue-i18n: + specifier: ^9.14.0 + version: 9.14.1(vue@3.5.13(typescript@5.8.2)) + vue-router: + specifier: ^4.4.5 + version: 4.5.0(vue@3.5.13(typescript@5.8.2)) + devDependencies: + '@directus/constants': + specifier: ^13.0.0 + version: 13.0.0 + '@directus/extensions-sdk': + specifier: ^13.0.1 + version: 13.0.3(@types/node@22.13.10)(@unhead/vue@2.0.0-alpha.19(vue@3.5.13(typescript@5.8.2)))(knex@3.1.0)(pinia@3.0.1(typescript@5.8.2)(vue@3.5.13(typescript@5.8.2)))(sass@1.85.1)(terser@5.39.0)(typescript@5.8.2)(vue-router@4.5.0(vue@3.5.13(typescript@5.8.2))) + '@directus/types': + specifier: ^13.0.0 + version: 13.0.0(knex@3.1.0)(vue@3.5.13(typescript@5.8.2)) + '@directus/utils': + specifier: ^13.0.0 + version: 13.0.2(vue@3.5.13(typescript@5.8.2)) + typescript: + specifier: ^5.6.3 + version: 5.8.2 + vue: + specifier: ^3.5.13 + version: 3.5.13(typescript@5.8.2) + packages/resend-operation: devDependencies: '@directus/extensions-sdk': @@ -1761,6 +1795,9 @@ packages: '@directus/format-title@12.0.0': resolution: {integrity: sha512-8hCMRjaMRRZU7DJci2ep/yNYV7wrpgJFXEeMouHnyLxEev7Tb4UO4lZv4C+p2a0E81nyemhXEwPB0UjFrlV8VA==} + '@directus/format-title@12.0.1': + resolution: {integrity: sha512-Sx0yMpVnTQWLppevNdwgyKUsrfPCZ503bshzGfsnUuZDLu7yZe4lwatvu2RSzb7tAfJLYC5VdoZg/LXNBfFTRw==} + '@directus/schema@11.0.1': resolution: {integrity: sha512-I8YaZcFdzY1Livv3fW2L0GTBan+MGIYancj9GM/AoZpfeI5PjCecqASna/ijD/WVwDlUUvx6b7aJcQ1OLXBDug==} @@ -9858,6 +9895,8 @@ snapshots: '@directus/format-title@12.0.0': {} + '@directus/format-title@12.0.1': {} + '@directus/schema@11.0.1': dependencies: knex: 3.1.0 @@ -16074,7 +16113,7 @@ snapshots: pathe@2.0.3: {} - pdfjs-dist@4.4.168(encoding@0.1.13): + pdfjs-dist@4.4.168: optionalDependencies: canvas: 2.11.2(encoding@0.1.13) path2d: 0.2.1 @@ -17354,9 +17393,16 @@ snapshots: '@vue/devtools-api': 6.6.3 vue: 3.5.13(typescript@5.7.3) - vue-pdf-embed@2.0.4(encoding@0.1.13)(vue@3.4.31(typescript@5.5.3)): + vue-i18n@9.14.1(vue@3.5.13(typescript@5.8.2)): dependencies: - pdfjs-dist: 4.4.168(encoding@0.1.13) + '@intlify/core-base': 9.14.1 + '@intlify/shared': 9.14.1 + '@vue/devtools-api': 6.6.3 + vue: 3.5.13(typescript@5.8.2) + + vue-pdf-embed@2.0.4(vue@3.4.31(typescript@5.5.3)): + dependencies: + pdfjs-dist: 4.4.168 vue: 3.4.31(typescript@5.5.3) transitivePeerDependencies: - encoding @@ -17410,7 +17456,6 @@ snapshots: dependencies: '@vue/devtools-api': 6.6.4 vue: 3.5.13(typescript@5.8.2) - optional: true vue@3.4.21(typescript@5.5.3): dependencies: From 01bb86fa704eb826720aa8be58614ffee381444e Mon Sep 17 00:00:00 2001 From: Tim Butterfield <124673267+timio23@users.noreply.github.com> Date: Sat, 26 Apr 2025 23:39:16 +0100 Subject: [PATCH 03/31] added API reference to README --- packages/related-items-bundle/README.md | 47 ++++++++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/packages/related-items-bundle/README.md b/packages/related-items-bundle/README.md index 7d08d20f..f3e461b8 100644 --- a/packages/related-items-bundle/README.md +++ b/packages/related-items-bundle/README.md @@ -32,4 +32,49 @@ When opening the project settings for the first time, the module will automatica ## Permissions -This extension uses the current user's policy/role permissions and will only show the permitted data. Please refer to your Access Policies to ensure your users have required access. \ No newline at end of file +This extension uses the current user's policy/role permissions and will only show the permitted data. Please refer to your Access Policies to ensure your users have required access. + +## API Reference + +This bundle contains an endpoint extension. The data can be queried using the following endpoint: + +``` +GET /related-items// +``` + +The response will be an array of related collections and for each one, a secondary array of any related items from that collection. For example: + +``` +{ + "collection": "directus_files", + "fields": [ + "directus_files_id.id", + "directus_files_id.title", + "directus_files_id.type" + ], + "relation": "m2m", + "translations": null, + "field": "article_id", + "junction_field": "directus_files_id", + "primary_key": "id", + "template": "{{ title }}", + "items": [ + { + "directus_files_id": { + "id": "x0x1234x-5xx6-7890-x123-xxx4xx56789x", + "title": "Annual Leave Policy", + "type": "image/jpeg" + } + }, + { + "directus_files_id": { + "id": "x9x8765x-5xx6-7890-x123-xxx4xx56789x", + "title": "Brand Guidelines", + "type": "image/png" + } + } + ] +} +``` + +_Note: The fields and primary key can be used as context when processing the items or rendering an output._ \ No newline at end of file From 7383cd1cedf0ca95e1ba5d625b0588b0ac977ff1 Mon Sep 17 00:00:00 2001 From: Tim Butterfield <124673267+timio23@users.noreply.github.com> Date: Thu, 29 May 2025 22:19:57 +0100 Subject: [PATCH 04/31] moved admin check to index --- .../src/related-items-module/index.ts | 6 +++-- .../utils/inject-related-items-field.ts | 26 +++++++------------ 2 files changed, 14 insertions(+), 18 deletions(-) diff --git a/packages/related-items-bundle/src/related-items-module/index.ts b/packages/related-items-bundle/src/related-items-module/index.ts index 7d6ac937..30440f8a 100644 --- a/packages/related-items-bundle/src/related-items-module/index.ts +++ b/packages/related-items-bundle/src/related-items-module/index.ts @@ -7,8 +7,10 @@ export default defineModule({ name: 'Related Items', icon: 'hub', routes: [], - preRegisterCheck() { + preRegisterCheck(user) { + const admin = user.admin_access; + if (!admin) return false; injectRelatedItemsField(); return true; }, -}); +}); \ No newline at end of file diff --git a/packages/related-items-bundle/src/related-items-module/utils/inject-related-items-field.ts b/packages/related-items-bundle/src/related-items-module/utils/inject-related-items-field.ts index 92082f28..05e3526a 100644 --- a/packages/related-items-bundle/src/related-items-module/utils/inject-related-items-field.ts +++ b/packages/related-items-bundle/src/related-items-module/utils/inject-related-items-field.ts @@ -1,5 +1,4 @@ import { STORES_INJECT } from '@directus/constants'; -import { computed } from 'vue'; import { system_field } from '../settings-field'; import { getDirectusApp } from './get-directus-app'; import { getDirectusRouter } from './get-directus-router'; @@ -31,24 +30,19 @@ async function initializeApp(retry: number = 0) { const directusApp = getDirectusApp(); const stores = directusApp._container._vnode.component.provides[STORES_INJECT]; - const { useFieldsStore, useSettingsStore, useUserStore } = stores; + const { useFieldsStore, useSettingsStore } = stores; const fieldStore = useFieldsStore(); const settingsStore = useSettingsStore(); - const userStore = useUserStore(); - const isAdmin = computed(() => userStore.currentUser?.role?.admin_access ?? userStore.currentUser?.admin_access ?? false); - - if (isAdmin) { - try { - // Exit if no settings found - if (!('related_items_collections' in settingsStore.settings)) { - // Create required fields - await fieldStore.upsertField('directus_settings', 'related_items_collections', system_field); - await settingsStore.hydrate(); - } - } - catch (error: any) { - unexpectedError(error, stores); + try { + // Exit if no settings found + if (!('related_items_collections' in settingsStore.settings)) { + // Create required fields + await fieldStore.upsertField('directus_settings', 'related_items_collections', system_field); + await settingsStore.hydrate(); } } + catch (error: any) { + unexpectedError(error, stores); + } } From 0389ccc1b16487cf39fc10b4b74cdf9e83a306dd Mon Sep 17 00:00:00 2001 From: Tim Butterfield <124673267+timio23@users.noreply.github.com> Date: Thu, 29 May 2025 22:51:08 +0100 Subject: [PATCH 05/31] removed comments --- .../src/related-items-endpoint/index.ts | 22 +------ .../src/related-items-hook/index.ts | 5 -- .../src/related-items-interface/interface.vue | 2 - .../utils/get-route.ts | 59 +++++++++++++++++++ .../utils/inject-related-items-field.ts | 2 - 5 files changed, 60 insertions(+), 30 deletions(-) create mode 100644 packages/related-items-bundle/src/related-items-interface/utils/get-route.ts diff --git a/packages/related-items-bundle/src/related-items-endpoint/index.ts b/packages/related-items-bundle/src/related-items-endpoint/index.ts index 15049883..0bd1032e 100644 --- a/packages/related-items-bundle/src/related-items-endpoint/index.ts +++ b/packages/related-items-bundle/src/related-items-endpoint/index.ts @@ -34,7 +34,6 @@ export default defineEndpoint({ const accountability: Accountability | null = 'accountability' in req ? req.accountability as Accountability : null; const schema = await getSchema(); - // Services const collectionService = new CollectionsService({ accountability, schema, @@ -60,19 +59,11 @@ export default defineEndpoint({ return id ? await itemService.readOne(id) : await itemService.readByQuery(query); } - // Fetch context const requested_item: Record = await fetchItem({ collection, id: primaryId, query }); - - // Fetch all relations const relations: Relation[] = await relationService.readAll(); - - // Extract O2M relations for current collection const related_o2m_collections: Relation[] = relations.filter((r) => r.collection === collection); - - // Extract M2O relations for current collection const related_m2o_collections: Relation[] = relations.filter((r) => r.related_collection === collection || r.meta?.one_allowed_collections?.includes(collection)); - // Fetch collection metadata async function fetchCollectionInfo({ collection, field_name, item_id, relation }: { collection: string | null; field_name: string; item_id: number | string | number[] | string[]; relation: Relation }): Promise { if (!collection) return null; const current_collection: Collection = await collectionService.readOne(collection); @@ -83,20 +74,15 @@ export default defineEndpoint({ return { property: current_collection, display_template, template_fields, field_name, many_field: relation.meta?.junction_field, item_id, fields, primaryKey }; } - // Iterate over "Any" collections and fetch metadata async function fetchM2aCollectionInfo({ collections, m2m_relation, relation }: { collections: string[]; m2m_relation: Relation; relation: Relation }) { if (collections.length === 0 || m2m_relation === null) return []; const promises = collections.map(async (collection) => { if (m2m_relation.meta?.many_field && m2m_relation.meta.junction_field && m2m_relation.meta?.one_collection_field) { - // Field containing the ID of the linked item const many_field: string = m2m_relation.meta?.many_field; - // Field containing the ID of the parent item const junction_field: string = m2m_relation.meta.junction_field; - // Field containing the collection ID const one_collection_field: string = m2m_relation.meta?.one_collection_field; try { - // Fetch IDs of related content const m2a_junction_items: Record[] = await fetchItem({ collection: m2m_relation.collection, query: { fields: [ many_field, @@ -112,7 +98,6 @@ export default defineEndpoint({ limit: -1, } }); - // List of all item IDs belonging to the primary ID const item_ids = m2a_junction_items.map((i) => i[many_field]) as number[] | string[]; return await fetchCollectionInfo({ collection, field_name: relation.meta?.one_field ?? relation.field, item_id: item_ids, relation }); } @@ -129,7 +114,6 @@ export default defineEndpoint({ } async function build_output({ o2m, is_m2a, is_junction, relation_type, collection, related_collection, relation }: { o2m: boolean; is_m2a: boolean; is_junction: boolean; relation_type: string; collection: CollectionDetail; related_collection?: string; relation: Relation }) { - // Build array of fields required for visual output const itemFields = [ relation_type === 'm2m' ? `${collection.many_field}.${collection.primaryKey}` : collection.primaryKey, ...collection.template_fields.map((f) => relation_type === 'm2m' ? `${collection.many_field}.${f}` : f), @@ -155,7 +139,6 @@ export default defineEndpoint({ ), ]; - // Filters to fetch related content const itemFilters = o2m ? (Array.isArray(collection.item_id) ? { @@ -174,7 +157,6 @@ export default defineEndpoint({ }, }; - // Top level collection info const collectionInfo = { collection: collection.property.collection, fields: itemFields, @@ -235,10 +217,8 @@ export default defineEndpoint({ const related_collection = await fetchCollectionInfo({ collection: relatedCollection(), - field_name: field, // M2O: Field in current table where the Many item ID lived - // O2M: Field in Many table where the current item ID lived + field_name: field, item_id: o2m ? primaryId : requested_item[field], - // O2M: Current Item ID M2O: ID of Item in Many table relation: r, }); diff --git a/packages/related-items-bundle/src/related-items-hook/index.ts b/packages/related-items-bundle/src/related-items-hook/index.ts index e6e81c38..31ae735c 100644 --- a/packages/related-items-bundle/src/related-items-hook/index.ts +++ b/packages/related-items-bundle/src/related-items-hook/index.ts @@ -14,20 +14,15 @@ export default defineHook(({ action }, { services }) => { schema, }); - // Fetch all fields const fields: Field[] = await fieldService.readAll(); - // Filter to existing fields const existingFields = fields.filter((f) => f.field === 'directus_related_items_alias'); - // Filter a list of orphaned fields const existingFieldsToDelete = existingFields.filter((f) => !collections.includes(f.collection)); existingFieldsToDelete.forEach(async (f) => { - // Remove orphaned fields await fieldService.deleteField(f.collection, f.field); }); collections.filter((c) => !existingFields.some((f) => f.collection === c)).forEach(async (c) => { - // Create new fields await fieldService.createField(c, alias_field); }); } diff --git a/packages/related-items-bundle/src/related-items-interface/interface.vue b/packages/related-items-bundle/src/related-items-interface/interface.vue index 3308e2f7..c32bcbc0 100644 --- a/packages/related-items-bundle/src/related-items-interface/interface.vue +++ b/packages/related-items-bundle/src/related-items-interface/interface.vue @@ -15,8 +15,6 @@ const props = withDefaults( ); const { collection, primaryKey, loading } = toRefs(props); - -// refreshList(primaryKey.value); - + \ No newline at end of file From d1182ead3d210b9bdf2cc98bafed380b3b1435d2 Mon Sep 17 00:00:00 2001 From: Tim Butterfield <124673267+timio23@users.noreply.github.com> Date: Sat, 31 May 2025 16:44:47 +0100 Subject: [PATCH 12/31] graceful errors for hook --- .../src/related-items-hook/index.ts | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/related-items-bundle/src/related-items-hook/index.ts b/packages/related-items-bundle/src/related-items-hook/index.ts index 31ae735c..93a8bb31 100644 --- a/packages/related-items-bundle/src/related-items-hook/index.ts +++ b/packages/related-items-bundle/src/related-items-hook/index.ts @@ -19,11 +19,21 @@ export default defineHook(({ action }, { services }) => { const existingFieldsToDelete = existingFields.filter((f) => !collections.includes(f.collection)); existingFieldsToDelete.forEach(async (f) => { - await fieldService.deleteField(f.collection, f.field); + try { + await fieldService.deleteField(f.collection, f.field); + } + catch(e){ + console.error(e); + } }); collections.filter((c) => !existingFields.some((f) => f.collection === c)).forEach(async (c) => { - await fieldService.createField(c, alias_field); + try { + await fieldService.createField(c, alias_field); + } + catch(e){ + console.error(e); + } }); } }); From 2e08ae8eb75fe5bd237bc51b627ffee474055898 Mon Sep 17 00:00:00 2001 From: Tim Butterfield <124673267+timio23@users.noreply.github.com> Date: Sat, 31 May 2025 16:47:47 +0100 Subject: [PATCH 13/31] added support for junction tables --- .../src/related-items-endpoint/index.ts | 63 ++++++++++++------- 1 file changed, 42 insertions(+), 21 deletions(-) diff --git a/packages/related-items-bundle/src/related-items-endpoint/index.ts b/packages/related-items-bundle/src/related-items-endpoint/index.ts index 0bd1032e..a68b641b 100644 --- a/packages/related-items-bundle/src/related-items-endpoint/index.ts +++ b/packages/related-items-bundle/src/related-items-endpoint/index.ts @@ -56,7 +56,13 @@ export default defineEndpoint({ accountability, schema, }); - return id ? await itemService.readOne(id) : await itemService.readByQuery(query); + try { + return id ? await itemService.readOne(id) : await itemService.readByQuery(query); + } + catch(e) { + console.error(e); + return id ? null : []; + } } const requested_item: Record = await fetchItem({ collection, id: primaryId, query }); @@ -97,12 +103,12 @@ export default defineEndpoint({ }, limit: -1, } }); - const item_ids = m2a_junction_items.map((i) => i[many_field]) as number[] | string[]; + return await fetchCollectionInfo({ collection, field_name: relation.meta?.one_field ?? relation.field, item_id: item_ids, relation }); } catch (error) { - console.warn(error); + console.error(error); return null; } } @@ -113,7 +119,7 @@ export default defineEndpoint({ return Promise.all(promises); } - async function build_output({ o2m, is_m2a, is_junction, relation_type, collection, related_collection, relation }: { o2m: boolean; is_m2a: boolean; is_junction: boolean; relation_type: string; collection: CollectionDetail; related_collection?: string; relation: Relation }) { + async function build_output({ o2m, is_m2a, is_m2a_junction, has_junction, relation_type, collection, related_collection, relation }: { o2m: boolean; is_m2a: boolean; is_m2a_junction: boolean; has_junction: boolean; relation_type: string; collection: CollectionDetail; related_collection?: string; relation: Relation }) { const itemFields = [ relation_type === 'm2m' ? `${collection.many_field}.${collection.primaryKey}` : collection.primaryKey, ...collection.template_fields.map((f) => relation_type === 'm2m' ? `${collection.many_field}.${f}` : f), @@ -139,7 +145,7 @@ export default defineEndpoint({ ), ]; - const itemFilters = o2m + const itemFilters = o2m || is_m2a || is_m2a_junction ? (Array.isArray(collection.item_id) ? { [collection.primaryKey]: { @@ -152,7 +158,7 @@ export default defineEndpoint({ }, }) : { - [is_junction ? collection.field_name : collection.primaryKey]: { + [has_junction ? collection.field_name : collection.primaryKey]: { _eq: collection.item_id, }, }; @@ -163,13 +169,13 @@ export default defineEndpoint({ relation: relation_type, translations: collection.property.meta?.translations, field: collection.field_name, - junction_field: is_junction ? relation.meta?.junction_field : null, + junction_field: has_junction ? relation.meta?.junction_field : null, primary_key: collection.primaryKey, template: collection.display_template ?? `{{ ${collection.primaryKey} }}`, } as RelatedItem; try { - const relatedItems = await fetchItem({ collection: is_m2a || related_collection === undefined ? collection.property.collection : related_collection, query: { + const relatedItems = await fetchItem({ collection: is_m2a || is_m2a_junction || related_collection === undefined ? collection.property.collection : related_collection, query: { fields: itemFields, // @ts-expect-error saying _and is missing but it's not required filter: itemFilters, @@ -191,18 +197,19 @@ export default defineEndpoint({ async function relatedCollections(relations: Relation[]) { const promises = relations.map(async (r) => { const o2m = r.related_collection === collection; - const is_junction = r.meta?.junction_field !== null; - const is_m2a = is_junction && r.meta?.junction_field === 'item'; + const has_junction = (o2m || r.field === 'item') && r.meta?.junction_field !== null; + const is_m2a = r.meta?.junction_field === 'item' && (has_junction || !r.related_collection); + const is_m2a_junction = r.field === 'item' && !r.related_collection && r.collection === collection; const RelationType = () => { - if (is_m2a) return 'm2a'; - if (is_junction) return 'm2m'; + if (is_m2a || is_m2a_junction) return 'm2a'; + if (has_junction) return 'm2m'; if (o2m) return 'o2m'; return 'm2o'; }; const calculateField = () => { - if (is_junction && o2m && !is_m2a) return r.field; + if (has_junction && o2m && !is_m2a) return r.field; if (o2m) return r.meta?.one_field ?? r.field; return r.meta?.many_field ?? r.field; }; @@ -222,28 +229,41 @@ export default defineEndpoint({ relation: r, }); - const m2m_relation = is_junction ? await relationService.readOne(related_collection?.property.collection, r.meta?.junction_field) : null; + const m2m_relation = has_junction ? await relationService.readOne(related_collection?.property.collection, r.meta?.junction_field) : null; const m2m_related_collection = m2m_relation ? await fetchCollectionInfo({ collection: m2m_relation ? m2m_relation.related_collection : null, field_name: field, - item_id: field === 'item' || is_junction ? primaryId : (r.meta?.many_field ? requested_item[r.meta.many_field] : requested_item[field]), + item_id: field === 'item' || has_junction ? primaryId : (r.meta?.many_field ? requested_item[r.meta.many_field] : requested_item[field]), relation: r, }) : null; - const m2a_related_collections = is_m2a + const m2a_allowed_collections = m2m_relation && m2m_relation.meta?.one_allowed_collections !== null + ? m2m_relation.meta?.one_allowed_collections + : is_m2a_junction && r.meta?.one_allowed_collections + ? r.meta?.one_allowed_collections + : []; + + const m2a_related_collections = is_m2a || is_m2a_junction ? await fetchM2aCollectionInfo({ - collections: m2m_relation.meta?.one_allowed_collections, - m2m_relation, + collections: m2a_allowed_collections, + m2m_relation: is_m2a_junction ? { + collection: r.collection, + meta: { + many_field: 'item', + one_collection_field: 'collection', + junction_field: 'id', + }, + } : m2m_relation, relation: r, }) : []; - const collections: (CollectionDetail | null)[] = is_m2a + const collections: (CollectionDetail | null)[] = is_m2a || is_m2a_junction ? m2a_related_collections - : (is_junction + : (has_junction ? [m2m_related_collection] : [related_collection]); @@ -251,7 +271,8 @@ export default defineEndpoint({ const output_promise = collections.filter((c) => c && c.item_id).map(async (c) => await build_output({ o2m, is_m2a, - is_junction, + is_m2a_junction, + has_junction, relation_type: RelationType(), collection: c as CollectionDetail, related_collection, From 54fd66803ec84f94a53fa08fcd06e19358e01edf Mon Sep 17 00:00:00 2001 From: Tim Butterfield <124673267+timio23@users.noreply.github.com> Date: Sat, 31 May 2025 22:43:58 +0100 Subject: [PATCH 14/31] notification on item update and stop self editing --- .../related-items-list.vue | 37 ++++++++++++------- 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/packages/related-items-bundle/src/related-items-interface/related-items-list.vue b/packages/related-items-bundle/src/related-items-interface/related-items-list.vue index 4f789d71..bb994731 100644 --- a/packages/related-items-bundle/src/related-items-interface/related-items-list.vue +++ b/packages/related-items-bundle/src/related-items-interface/related-items-list.vue @@ -1,6 +1,6 @@