From 852c4e28d265d4e1149602c58afb2c51e77bdf7c Mon Sep 17 00:00:00 2001 From: Francisco Gaona Date: Fri, 9 Jan 2026 07:49:30 +0100 Subject: [PATCH] WIP - display related lists --- .../object/models/dynamic-model.factory.ts | 2 +- backend/src/object/object.service.ts | 2 + frontend/components/RelatedList.vue | 48 ++++++++++++++++--- .../components/views/DetailViewEnhanced.vue | 1 + .../central/domains/[[recordId]]/[[view]].vue | 1 + .../central/tenants/[[recordId]]/[[view]].vue | 1 + .../central/users/[[recordId]]/[[view]].vue | 1 + frontend/types/field-types.ts | 2 + 8 files changed, 51 insertions(+), 7 deletions(-) diff --git a/backend/src/object/models/dynamic-model.factory.ts b/backend/src/object/models/dynamic-model.factory.ts index 3ac480a..3ce2c0c 100644 --- a/backend/src/object/models/dynamic-model.factory.ts +++ b/backend/src/object/models/dynamic-model.factory.ts @@ -135,7 +135,7 @@ export class DynamicModelFactory { } } - const targetTable = this.getTableName(relation.targetObjectApiName); + const targetTable = DynamicModelFactory.getTableName(relation.targetObjectApiName); if (relation.type === 'belongsTo') { mappings[relation.name] = { diff --git a/backend/src/object/object.service.ts b/backend/src/object/object.service.ts index 8c5a222..e923aeb 100644 --- a/backend/src/object/object.service.ts +++ b/backend/src/object/object.service.ts @@ -643,6 +643,7 @@ export class ObjectService { relationName: string; objectApiName: string; lookupFieldApiName: string; + parentObjectApiName: string; fields: any[]; }>> { const knex = await this.tenantDbService.getTenantKnexById(tenantId); @@ -703,6 +704,7 @@ export class ObjectService { relationName, objectApiName: lookup.childApiName, lookupFieldApiName: lookup.fieldApiName, + parentObjectApiName: objectApiName, fields: fieldsByObject.get(lookup.objectDefinitionId) || [], }; }); diff --git a/frontend/components/RelatedList.vue b/frontend/components/RelatedList.vue index 9cb7ab7..2bfca55 100644 --- a/frontend/components/RelatedList.vue +++ b/frontend/components/RelatedList.vue @@ -11,6 +11,8 @@ interface RelatedListConfig { relationName: string // e.g., 'domains', 'users' objectApiName: string // e.g., 'domains', 'users' fields: FieldConfig[] // Fields to display in the list + lookupFieldApiName?: string // Used to filter by parentId when fetching + parentObjectApiName?: string // Parent object API name, used to derive lookup field if missing canCreate?: boolean createRoute?: string // Route to create new related record } @@ -19,11 +21,11 @@ interface Props { config: RelatedListConfig parentId: string relatedRecords?: any[] // Can be passed in if already fetched - baseUrl?: string // Base API URL, defaults to '/central' + baseUrl?: string // Base API URL, defaults to runtime objects } const props = withDefaults(defineProps(), { - baseUrl: '/central', + baseUrl: '/runtime/objects', relatedRecords: undefined, }) @@ -53,14 +55,48 @@ const fetchRelatedRecords = async () => { try { // Replace :parentId placeholder in the API path - let apiPath = props.config.objectApiName.replace(':parentId', props.parentId) + const sanitizedBase = props.baseUrl.replace(/\/$/, '') + let apiPath = props.config.objectApiName.replace(':parentId', props.parentId).replace(/^\/+/, '') + const isRuntimeObjects = sanitizedBase.endsWith('/runtime/objects') + + // Default runtime object routes expect /:objectApiName/records + if (isRuntimeObjects && !apiPath.includes('/')) { + apiPath = `${apiPath}/records` + } - const response = await api.get(`${props.baseUrl}/${apiPath}`, { + const findLookupKey = () => { + if (props.config.lookupFieldApiName) return props.config.lookupFieldApiName + + const parentName = props.config.parentObjectApiName?.toLowerCase() + const fields = props.config.fields || [] + + const parentMatch = fields.find(field => { + const relation = (field as any).relationObject || (field as any).referenceObject + return relation && parentName && relation.toLowerCase() === parentName + }) + if (parentMatch?.apiName) return parentMatch.apiName + + const lookupMatch = fields.find( + field => (field.type || '').toString().toLowerCase() === 'lookup' + ) + if (lookupMatch?.apiName) return lookupMatch.apiName + + const idMatch = fields.find(field => + field.apiName?.toLowerCase().endsWith('id') + ) + if (idMatch?.apiName) return idMatch.apiName + + return 'parentId' + } + + const lookupKey = findLookupKey() + + const response = await api.get(`${sanitizedBase}/${apiPath}`, { params: { - parentId: props.parentId, + [lookupKey]: props.parentId, }, }) - records.value = response || [] + records.value = response?.data || response || [] } catch (err: any) { console.error('Error fetching related records:', err) error.value = err.message || 'Failed to fetch related records' diff --git a/frontend/components/views/DetailViewEnhanced.vue b/frontend/components/views/DetailViewEnhanced.vue index 050bf17..62551f3 100644 --- a/frontend/components/views/DetailViewEnhanced.vue +++ b/frontend/components/views/DetailViewEnhanced.vue @@ -247,6 +247,7 @@ const visibleRelatedLists = computed(() => { :config="relatedList" :parent-id="data.id" :related-records="data[relatedList.relationName]" + :base-url="baseUrl" @navigate="(objectApiName, recordId) => emit('navigate', objectApiName, recordId)" @create="(objectApiName, parentId) => emit('createRelated', objectApiName, parentId)" /> diff --git a/frontend/pages/central/domains/[[recordId]]/[[view]].vue b/frontend/pages/central/domains/[[recordId]]/[[view]].vue index e592573..e849745 100644 --- a/frontend/pages/central/domains/[[recordId]]/[[view]].vue +++ b/frontend/pages/central/domains/[[recordId]]/[[view]].vue @@ -133,6 +133,7 @@ onMounted(async () => { :config="domainDetailConfig" :data="currentRecord" :loading="dataLoading" + base-url="/central" @edit="handleEdit" @delete="() => handleDelete([currentRecord])" @back="handleBack" diff --git a/frontend/pages/central/tenants/[[recordId]]/[[view]].vue b/frontend/pages/central/tenants/[[recordId]]/[[view]].vue index 8618f64..66ac82e 100644 --- a/frontend/pages/central/tenants/[[recordId]]/[[view]].vue +++ b/frontend/pages/central/tenants/[[recordId]]/[[view]].vue @@ -168,6 +168,7 @@ onMounted(async () => { :config="tenantDetailConfig" :data="currentRecord" :loading="dataLoading" + base-url="/central" @edit="handleEdit" @delete="() => handleDelete([currentRecord])" @back="handleBack" diff --git a/frontend/pages/central/users/[[recordId]]/[[view]].vue b/frontend/pages/central/users/[[recordId]]/[[view]].vue index 2ea802b..224c40f 100644 --- a/frontend/pages/central/users/[[recordId]]/[[view]].vue +++ b/frontend/pages/central/users/[[recordId]]/[[view]].vue @@ -138,6 +138,7 @@ onMounted(async () => { :config="centralUserDetailConfig" :data="currentRecord" :loading="dataLoading" + base-url="/central" @edit="handleEdit" @delete="() => handleDelete([currentRecord])" @back="handleBack" diff --git a/frontend/types/field-types.ts b/frontend/types/field-types.ts index bc83183..7d1bce5 100644 --- a/frontend/types/field-types.ts +++ b/frontend/types/field-types.ts @@ -123,6 +123,8 @@ export interface RelatedListConfig { relationName: string; objectApiName: string; fields: FieldConfig[]; + lookupFieldApiName?: string; + parentObjectApiName?: string; canCreate?: boolean; createRoute?: string; }