Add dynamic related lists with page layout support
This commit is contained in:
@@ -43,6 +43,14 @@ export interface ObjectDefinitionDTO {
|
|||||||
description?: string;
|
description?: string;
|
||||||
isSystem: boolean;
|
isSystem: boolean;
|
||||||
fields: FieldConfigDTO[];
|
fields: FieldConfigDTO[];
|
||||||
|
relatedLists?: Array<{
|
||||||
|
title: string;
|
||||||
|
relationName: string;
|
||||||
|
objectApiName: string;
|
||||||
|
fields: FieldConfigDTO[];
|
||||||
|
canCreate?: boolean;
|
||||||
|
createRoute?: string;
|
||||||
|
}>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@@ -206,6 +214,17 @@ export class FieldMapperService {
|
|||||||
.filter((f: any) => f.isActive !== false)
|
.filter((f: any) => f.isActive !== false)
|
||||||
.sort((a: any, b: any) => (a.displayOrder || 0) - (b.displayOrder || 0))
|
.sort((a: any, b: any) => (a.displayOrder || 0) - (b.displayOrder || 0))
|
||||||
.map((f: any) => this.mapFieldToDTO(f)),
|
.map((f: any) => this.mapFieldToDTO(f)),
|
||||||
|
relatedLists: (objectDef.relatedLists || []).map((list: any) => ({
|
||||||
|
title: list.title,
|
||||||
|
relationName: list.relationName,
|
||||||
|
objectApiName: list.objectApiName,
|
||||||
|
fields: (list.fields || [])
|
||||||
|
.filter((f: any) => f.isActive !== false)
|
||||||
|
.map((f: any) => this.mapFieldToDTO(f))
|
||||||
|
.filter((f: any) => f.showOnList !== false),
|
||||||
|
canCreate: list.canCreate,
|
||||||
|
createRoute: list.createRoute,
|
||||||
|
})),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -119,6 +119,47 @@ export class DynamicModelFactory {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add additional relation mappings (e.g., hasMany)
|
||||||
|
for (const relation of relations) {
|
||||||
|
if (mappings[relation.name]) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let modelClass: any = relation.targetObjectApiName;
|
||||||
|
if (getModel) {
|
||||||
|
const resolvedModel = getModel(relation.targetObjectApiName);
|
||||||
|
if (resolvedModel) {
|
||||||
|
modelClass = resolvedModel;
|
||||||
|
} else {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetTable = this.getTableName(relation.targetObjectApiName);
|
||||||
|
|
||||||
|
if (relation.type === 'belongsTo') {
|
||||||
|
mappings[relation.name] = {
|
||||||
|
relation: Model.BelongsToOneRelation,
|
||||||
|
modelClass,
|
||||||
|
join: {
|
||||||
|
from: `${tableName}.${relation.fromColumn}`,
|
||||||
|
to: `${targetTable}.${relation.toColumn}`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (relation.type === 'hasMany') {
|
||||||
|
mappings[relation.name] = {
|
||||||
|
relation: Model.HasManyRelation,
|
||||||
|
modelClass,
|
||||||
|
join: {
|
||||||
|
from: `${tableName}.${relation.fromColumn}`,
|
||||||
|
to: `${targetTable}.${relation.toColumn}`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return mappings;
|
return mappings;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -196,6 +237,9 @@ export class DynamicModelFactory {
|
|||||||
.replace(/([A-Z])/g, '_$1')
|
.replace(/([A-Z])/g, '_$1')
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.replace(/^_/, '');
|
.replace(/^_/, '');
|
||||||
|
if (snakeCase.endsWith('y')) {
|
||||||
|
return `${snakeCase.slice(0, -1)}ies`;
|
||||||
|
}
|
||||||
return snakeCase.endsWith('s') ? snakeCase : `${snakeCase}s`;
|
return snakeCase.endsWith('s') ? snakeCase : `${snakeCase}s`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -171,6 +171,25 @@ export class ModelService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (objectMetadata.relations) {
|
||||||
|
for (const relation of objectMetadata.relations) {
|
||||||
|
if (relation.targetObjectApiName) {
|
||||||
|
try {
|
||||||
|
await this.ensureModelWithDependencies(
|
||||||
|
tenantId,
|
||||||
|
relation.targetObjectApiName,
|
||||||
|
fetchMetadata,
|
||||||
|
visited,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.debug(
|
||||||
|
`Skipping registration of related model ${relation.targetObjectApiName}: ${error.message}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Now create and register this model (all dependencies are ready)
|
// Now create and register this model (all dependencies are ready)
|
||||||
await this.createModelForObject(tenantId, objectMetadata);
|
await this.createModelForObject(tenantId, objectMetadata);
|
||||||
this.logger.log(`Registered model for ${objectApiName} in tenant ${tenantId}`);
|
this.logger.log(`Registered model for ${objectApiName} in tenant ${tenantId}`);
|
||||||
|
|||||||
@@ -71,10 +71,13 @@ export class ObjectService {
|
|||||||
.first();
|
.first();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const relatedLists = await this.getRelatedListDefinitions(resolvedTenantId, apiName);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...obj,
|
...obj,
|
||||||
fields: normalizedFields,
|
fields: normalizedFields,
|
||||||
app,
|
app,
|
||||||
|
relatedLists,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -409,6 +412,17 @@ export class ObjectService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const relatedLists = await this.getRelatedListDefinitions(tenantId, apiName);
|
||||||
|
for (const relatedList of relatedLists) {
|
||||||
|
validRelations.push({
|
||||||
|
name: relatedList.relationName,
|
||||||
|
type: 'hasMany' as const,
|
||||||
|
targetObjectApiName: relatedList.objectApiName,
|
||||||
|
fromColumn: 'id',
|
||||||
|
toColumn: relatedList.lookupFieldApiName,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
apiName,
|
apiName,
|
||||||
tableName,
|
tableName,
|
||||||
@@ -566,16 +580,18 @@ export class ObjectService {
|
|||||||
f.type === 'LOOKUP' && f.referenceObject
|
f.type === 'LOOKUP' && f.referenceObject
|
||||||
) || [];
|
) || [];
|
||||||
|
|
||||||
if (lookupFields.length > 0) {
|
const relatedLists = await this.getRelatedListDefinitions(resolvedTenantId, objectApiName);
|
||||||
// Build relation expression - use singular lowercase for relation name
|
|
||||||
const relationExpression = lookupFields
|
const relationNames = [
|
||||||
|
...lookupFields
|
||||||
.map(f => f.apiName.replace(/Id$/, '').toLowerCase())
|
.map(f => f.apiName.replace(/Id$/, '').toLowerCase())
|
||||||
.filter(Boolean)
|
.filter(Boolean),
|
||||||
.join(', ');
|
...relatedLists.map(list => list.relationName),
|
||||||
|
];
|
||||||
if (relationExpression) {
|
|
||||||
query = query.withGraphFetched(`[${relationExpression}]`);
|
if (relationNames.length > 0) {
|
||||||
}
|
const relationExpression = relationNames.join(', ');
|
||||||
|
query = query.withGraphFetched(`[${relationExpression}]`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const record = await query.first();
|
const record = await query.first();
|
||||||
@@ -589,6 +605,79 @@ export class ObjectService {
|
|||||||
return filteredRecord;
|
return filteredRecord;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async getRelatedListDefinitions(
|
||||||
|
tenantId: string,
|
||||||
|
objectApiName: string,
|
||||||
|
): Promise<Array<{
|
||||||
|
title: string;
|
||||||
|
relationName: string;
|
||||||
|
objectApiName: string;
|
||||||
|
lookupFieldApiName: string;
|
||||||
|
fields: any[];
|
||||||
|
}>> {
|
||||||
|
const knex = await this.tenantDbService.getTenantKnexById(tenantId);
|
||||||
|
|
||||||
|
const relatedLookups = await knex('field_definitions as fd')
|
||||||
|
.join('object_definitions as od', 'fd.objectDefinitionId', 'od.id')
|
||||||
|
.where('fd.type', 'LOOKUP')
|
||||||
|
.andWhere('fd.referenceObject', objectApiName)
|
||||||
|
.select(
|
||||||
|
'fd.apiName as fieldApiName',
|
||||||
|
'fd.label as fieldLabel',
|
||||||
|
'fd.objectDefinitionId as objectDefinitionId',
|
||||||
|
'od.apiName as childApiName',
|
||||||
|
'od.label as childLabel',
|
||||||
|
'od.pluralLabel as childPluralLabel',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (relatedLookups.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const objectIds = Array.from(
|
||||||
|
new Set(relatedLookups.map((lookup: any) => lookup.objectDefinitionId)),
|
||||||
|
);
|
||||||
|
|
||||||
|
const relatedFields = await knex('field_definitions')
|
||||||
|
.whereIn('objectDefinitionId', objectIds)
|
||||||
|
.orderBy('label', 'asc');
|
||||||
|
|
||||||
|
const fieldsByObject = new Map<string, any[]>();
|
||||||
|
for (const field of relatedFields) {
|
||||||
|
const existing = fieldsByObject.get(field.objectDefinitionId) || [];
|
||||||
|
existing.push(this.normalizeField(field));
|
||||||
|
fieldsByObject.set(field.objectDefinitionId, existing);
|
||||||
|
}
|
||||||
|
|
||||||
|
const lookupCounts = relatedLookups.reduce<Record<string, number>>(
|
||||||
|
(acc, lookup: any) => {
|
||||||
|
acc[lookup.childApiName] = (acc[lookup.childApiName] || 0) + 1;
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
|
||||||
|
return relatedLookups.map((lookup: any) => {
|
||||||
|
const baseRelationName = this.getTableName(lookup.childApiName);
|
||||||
|
const hasMultiple = lookupCounts[lookup.childApiName] > 1;
|
||||||
|
const relationName = hasMultiple
|
||||||
|
? `${baseRelationName}_${lookup.fieldApiName.replace(/Id$/, '').toLowerCase()}`
|
||||||
|
: baseRelationName;
|
||||||
|
const baseTitle =
|
||||||
|
lookup.childPluralLabel ||
|
||||||
|
(lookup.childLabel ? `${lookup.childLabel}s` : lookup.childApiName);
|
||||||
|
const title = hasMultiple ? `${baseTitle} (${lookup.fieldLabel})` : baseTitle;
|
||||||
|
|
||||||
|
return {
|
||||||
|
title,
|
||||||
|
relationName,
|
||||||
|
objectApiName: lookup.childApiName,
|
||||||
|
lookupFieldApiName: lookup.fieldApiName,
|
||||||
|
fields: fieldsByObject.get(lookup.objectDefinitionId) || [],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async createRecord(
|
async createRecord(
|
||||||
tenantId: string,
|
tenantId: string,
|
||||||
objectApiName: string,
|
objectApiName: string,
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ export class CreatePageLayoutDto {
|
|||||||
w: number;
|
w: number;
|
||||||
h: number;
|
h: number;
|
||||||
}>;
|
}>;
|
||||||
|
relatedLists?: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
@IsString()
|
@IsString()
|
||||||
@@ -46,6 +47,7 @@ export class UpdatePageLayoutDto {
|
|||||||
w: number;
|
w: number;
|
||||||
h: number;
|
h: number;
|
||||||
}>;
|
}>;
|
||||||
|
relatedLists?: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
@IsString()
|
@IsString()
|
||||||
|
|||||||
@@ -28,22 +28,44 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Available Fields Sidebar -->
|
<!-- Available Fields Sidebar -->
|
||||||
<div class="w-80 border-l bg-white dark:bg-slate-950 p-4 overflow-auto">
|
<div class="w-80 border-l bg-white dark:bg-slate-950 p-4 overflow-auto space-y-6">
|
||||||
<h3 class="text-lg font-semibold mb-4">Available Fields</h3>
|
<div>
|
||||||
<p class="text-xs text-muted-foreground mb-4">Click or drag to add field to grid</p>
|
<h3 class="text-lg font-semibold mb-4">Available Fields</h3>
|
||||||
<div class="space-y-2" id="sidebar-fields">
|
<p class="text-xs text-muted-foreground mb-4">Click or drag to add field to grid</p>
|
||||||
<div
|
<div class="space-y-2" id="sidebar-fields">
|
||||||
v-for="field in availableFields"
|
<div
|
||||||
:key="field.id"
|
v-for="field in availableFields"
|
||||||
class="p-3 border rounded cursor-move bg-white dark:bg-slate-900 hover:border-primary hover:bg-slate-50 dark:hover:bg-slate-800 transition-colors"
|
:key="field.id"
|
||||||
:data-field-id="field.id"
|
class="p-3 border rounded cursor-move bg-white dark:bg-slate-900 hover:border-primary hover:bg-slate-50 dark:hover:bg-slate-800 transition-colors"
|
||||||
draggable="true"
|
:data-field-id="field.id"
|
||||||
@dragstart="handleDragStart($event, field)"
|
draggable="true"
|
||||||
@click="addFieldToGrid(field)"
|
@dragstart="handleDragStart($event, field)"
|
||||||
>
|
@click="addFieldToGrid(field)"
|
||||||
<div class="font-medium text-sm">{{ field.label }}</div>
|
>
|
||||||
<div class="text-xs text-muted-foreground">{{ field.apiName }}</div>
|
<div class="font-medium text-sm">{{ field.label }}</div>
|
||||||
<div class="text-xs text-muted-foreground">Type: {{ field.type }}</div>
|
<div class="text-xs text-muted-foreground">{{ field.apiName }}</div>
|
||||||
|
<div class="text-xs text-muted-foreground">Type: {{ field.type }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="relatedLists.length > 0">
|
||||||
|
<h3 class="text-lg font-semibold mb-2">Related Lists</h3>
|
||||||
|
<p class="text-xs text-muted-foreground mb-4">Select related lists to show on detail views</p>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<label
|
||||||
|
v-for="list in relatedLists"
|
||||||
|
:key="list.relationName"
|
||||||
|
class="flex items-center gap-2 text-sm"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
class="h-4 w-4"
|
||||||
|
:value="list.relationName"
|
||||||
|
v-model="selectedRelatedLists"
|
||||||
|
/>
|
||||||
|
<span>{{ list.title }}</span>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -52,26 +74,29 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, onBeforeUnmount, watch, nextTick } from 'vue'
|
import { ref, onMounted, onBeforeUnmount, watch, computed } from 'vue'
|
||||||
import { GridStack } from 'gridstack'
|
import { GridStack } from 'gridstack'
|
||||||
import 'gridstack/dist/gridstack.min.css'
|
import 'gridstack/dist/gridstack.min.css'
|
||||||
import type { FieldLayoutItem } from '~/types/page-layout'
|
import type { FieldLayoutItem } from '~/types/page-layout'
|
||||||
import type { FieldConfig } from '~/types/field-types'
|
import type { FieldConfig, RelatedListConfig } from '~/types/field-types'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
fields: FieldConfig[]
|
fields: FieldConfig[]
|
||||||
|
relatedLists?: RelatedListConfig[]
|
||||||
initialLayout?: FieldLayoutItem[]
|
initialLayout?: FieldLayoutItem[]
|
||||||
|
initialRelatedLists?: string[]
|
||||||
layoutName?: string
|
layoutName?: string
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
save: [layout: FieldLayoutItem[]]
|
save: [layout: { fields: FieldLayoutItem[]; relatedLists: string[] }]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const gridContainer = ref<HTMLElement | null>(null)
|
const gridContainer = ref<HTMLElement | null>(null)
|
||||||
let grid: GridStack | null = null
|
let grid: GridStack | null = null
|
||||||
const gridItems = ref<Map<string, any>>(new Map())
|
const gridItems = ref<Map<string, any>>(new Map())
|
||||||
|
const selectedRelatedLists = ref<string[]>(props.initialRelatedLists || [])
|
||||||
|
|
||||||
// Fields that are already on the grid
|
// Fields that are already on the grid
|
||||||
const placedFieldIds = ref<Set<string>>(new Set())
|
const placedFieldIds = ref<Set<string>>(new Set())
|
||||||
@@ -81,6 +106,10 @@ const availableFields = computed(() => {
|
|||||||
return props.fields.filter(field => !placedFieldIds.value.has(field.id))
|
return props.fields.filter(field => !placedFieldIds.value.has(field.id))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const relatedLists = computed(() => {
|
||||||
|
return props.relatedLists || []
|
||||||
|
})
|
||||||
|
|
||||||
const initGrid = () => {
|
const initGrid = () => {
|
||||||
if (!gridContainer.value) return
|
if (!gridContainer.value) return
|
||||||
|
|
||||||
@@ -278,7 +307,10 @@ const handleSave = () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
emit('save', layout)
|
emit('save', {
|
||||||
|
fields: layout,
|
||||||
|
relatedLists: selectedRelatedLists.value,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
@@ -295,6 +327,13 @@ onBeforeUnmount(() => {
|
|||||||
watch(() => props.fields, () => {
|
watch(() => props.fields, () => {
|
||||||
updatePlacedFields()
|
updatePlacedFields()
|
||||||
}, { deep: true })
|
}, { deep: true })
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.initialRelatedLists,
|
||||||
|
(value) => {
|
||||||
|
selectedRelatedLists.value = value ? [...value] : []
|
||||||
|
},
|
||||||
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
@@ -87,6 +87,22 @@ const getFieldsBySection = (section: FieldSection) => {
|
|||||||
const usePageLayout = computed(() => {
|
const usePageLayout = computed(() => {
|
||||||
return pageLayout.value && pageLayout.value.fields && pageLayout.value.fields.length > 0
|
return pageLayout.value && pageLayout.value.fields && pageLayout.value.fields.length > 0
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const visibleRelatedLists = computed<RelatedListConfig[]>(() => {
|
||||||
|
const relatedLists = props.config.relatedLists || []
|
||||||
|
if (!relatedLists.length) return []
|
||||||
|
|
||||||
|
if (!usePageLayout.value) {
|
||||||
|
return relatedLists
|
||||||
|
}
|
||||||
|
|
||||||
|
const layoutRelatedLists = pageLayout.value?.relatedLists
|
||||||
|
if (!layoutRelatedLists || layoutRelatedLists.length === 0) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
return relatedLists.filter(list => layoutRelatedLists.includes(list.relationName))
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -138,7 +154,7 @@ const usePageLayout = computed(() => {
|
|||||||
<Tabs v-else default-value="details" class="space-y-6">
|
<Tabs v-else default-value="details" class="space-y-6">
|
||||||
<TabsList>
|
<TabsList>
|
||||||
<TabsTrigger value="details">Details</TabsTrigger>
|
<TabsTrigger value="details">Details</TabsTrigger>
|
||||||
<TabsTrigger v-if="config.relatedLists && config.relatedLists.length > 0" value="related">
|
<TabsTrigger v-if="visibleRelatedLists.length > 0" value="related">
|
||||||
Related
|
Related
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger v-if="showSharing && data.id" value="sharing">
|
<TabsTrigger v-if="showSharing && data.id" value="sharing">
|
||||||
@@ -224,9 +240,9 @@ const usePageLayout = computed(() => {
|
|||||||
|
|
||||||
<!-- Related Lists Tab -->
|
<!-- Related Lists Tab -->
|
||||||
<TabsContent value="related" class="space-y-6">
|
<TabsContent value="related" class="space-y-6">
|
||||||
<div v-if="config.relatedLists && config.relatedLists.length > 0">
|
<div v-if="visibleRelatedLists.length > 0">
|
||||||
<RelatedList
|
<RelatedList
|
||||||
v-for="relatedList in config.relatedLists"
|
v-for="relatedList in visibleRelatedLists"
|
||||||
:key="relatedList.relationName"
|
:key="relatedList.relationName"
|
||||||
:config="relatedList"
|
:config="relatedList"
|
||||||
:parent-id="data.id"
|
:parent-id="data.id"
|
||||||
|
|||||||
@@ -97,6 +97,7 @@ export const useFields = () => {
|
|||||||
objectApiName: objectDef.apiName,
|
objectApiName: objectDef.apiName,
|
||||||
mode: 'detail' as ViewMode,
|
mode: 'detail' as ViewMode,
|
||||||
fields,
|
fields,
|
||||||
|
relatedLists: objectDef.relatedLists || [],
|
||||||
...customConfig,
|
...customConfig,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -155,6 +155,14 @@ const handleBack = () => {
|
|||||||
router.push(`/${objectApiName.value.toLowerCase()}/`)
|
router.push(`/${objectApiName.value.toLowerCase()}/`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleNavigate = (relatedObjectApiName: string, relatedRecordId: string) => {
|
||||||
|
router.push(`/${relatedObjectApiName.toLowerCase()}/${relatedRecordId}/detail`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCreateRelated = (relatedObjectApiName: string, _parentId: string) => {
|
||||||
|
router.push(`/${relatedObjectApiName.toLowerCase()}/new`)
|
||||||
|
}
|
||||||
|
|
||||||
const handleDelete = async (rows: any[]) => {
|
const handleDelete = async (rows: any[]) => {
|
||||||
if (confirm(`Delete ${rows.length} record(s)? This action cannot be undone.`)) {
|
if (confirm(`Delete ${rows.length} record(s)? This action cannot be undone.`)) {
|
||||||
try {
|
try {
|
||||||
@@ -279,6 +287,8 @@ onMounted(async () => {
|
|||||||
@edit="handleEdit"
|
@edit="handleEdit"
|
||||||
@delete="() => handleDelete([currentRecord])"
|
@delete="() => handleDelete([currentRecord])"
|
||||||
@back="handleBack"
|
@back="handleBack"
|
||||||
|
@navigate="handleNavigate"
|
||||||
|
@create-related="handleCreateRelated"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Edit View -->
|
<!-- Edit View -->
|
||||||
|
|||||||
@@ -95,6 +95,14 @@ const handleBack = () => {
|
|||||||
router.push(`/app/objects/${objectApiName.value}/`)
|
router.push(`/app/objects/${objectApiName.value}/`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleNavigate = (relatedObjectApiName: string, relatedRecordId: string) => {
|
||||||
|
router.push(`/app/objects/${relatedObjectApiName}/${relatedRecordId}/detail`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCreateRelated = (relatedObjectApiName: string, _parentId: string) => {
|
||||||
|
router.push(`/app/objects/${relatedObjectApiName}/new`)
|
||||||
|
}
|
||||||
|
|
||||||
const handleDelete = async (rows: any[]) => {
|
const handleDelete = async (rows: any[]) => {
|
||||||
if (confirm(`Delete ${rows.length} record(s)? This action cannot be undone.`)) {
|
if (confirm(`Delete ${rows.length} record(s)? This action cannot be undone.`)) {
|
||||||
try {
|
try {
|
||||||
@@ -212,6 +220,8 @@ onMounted(async () => {
|
|||||||
@edit="handleEdit"
|
@edit="handleEdit"
|
||||||
@delete="() => handleDelete([currentRecord])"
|
@delete="() => handleDelete([currentRecord])"
|
||||||
@back="handleBack"
|
@back="handleBack"
|
||||||
|
@navigate="handleNavigate"
|
||||||
|
@create-related="handleCreateRelated"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Edit View -->
|
<!-- Edit View -->
|
||||||
|
|||||||
@@ -132,6 +132,8 @@
|
|||||||
<PageLayoutEditor
|
<PageLayoutEditor
|
||||||
:fields="object.fields"
|
:fields="object.fields"
|
||||||
:initial-layout="(selectedLayout.layoutConfig || selectedLayout.layout_config)?.fields || []"
|
:initial-layout="(selectedLayout.layoutConfig || selectedLayout.layout_config)?.fields || []"
|
||||||
|
:related-lists="object.relatedLists || []"
|
||||||
|
:initial-related-lists="(selectedLayout.layoutConfig || selectedLayout.layout_config)?.relatedLists || []"
|
||||||
:layout-name="selectedLayout.name"
|
:layout-name="selectedLayout.name"
|
||||||
@save="handleSaveLayout"
|
@save="handleSaveLayout"
|
||||||
/>
|
/>
|
||||||
@@ -203,7 +205,7 @@ const handleCreateLayout = async () => {
|
|||||||
name,
|
name,
|
||||||
objectId: object.value.id,
|
objectId: object.value.id,
|
||||||
isDefault: layouts.value.length === 0,
|
isDefault: layouts.value.length === 0,
|
||||||
layoutConfig: { fields: [] },
|
layoutConfig: { fields: [], relatedLists: [] },
|
||||||
})
|
})
|
||||||
|
|
||||||
layouts.value.push(newLayout)
|
layouts.value.push(newLayout)
|
||||||
@@ -219,12 +221,12 @@ const handleSelectLayout = (layout: PageLayout) => {
|
|||||||
selectedLayout.value = layout
|
selectedLayout.value = layout
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSaveLayout = async (fields: FieldLayoutItem[]) => {
|
const handleSaveLayout = async (layoutConfig: { fields: FieldLayoutItem[]; relatedLists: string[] }) => {
|
||||||
if (!selectedLayout.value) return
|
if (!selectedLayout.value) return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const updated = await updatePageLayout(selectedLayout.value.id, {
|
const updated = await updatePageLayout(selectedLayout.value.id, {
|
||||||
layoutConfig: { fields },
|
layoutConfig,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Update the layout in the list
|
// Update the layout in the list
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export interface FieldLayoutItem {
|
|||||||
|
|
||||||
export interface PageLayoutConfig {
|
export interface PageLayoutConfig {
|
||||||
fields: FieldLayoutItem[];
|
fields: FieldLayoutItem[];
|
||||||
|
relatedLists?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PageLayout {
|
export interface PageLayout {
|
||||||
|
|||||||
Reference in New Issue
Block a user