Merge pull request #2 from phyroslam/codex/add-dynamic-related-lists-to-detail-views
Add dynamic tenant-level related lists and page layout selection
This commit is contained in:
@@ -28,22 +28,44 @@
|
||||
</div>
|
||||
|
||||
<!-- Available Fields Sidebar -->
|
||||
<div class="w-80 border-l bg-white dark:bg-slate-950 p-4 overflow-auto">
|
||||
<h3 class="text-lg font-semibold mb-4">Available Fields</h3>
|
||||
<p class="text-xs text-muted-foreground mb-4">Click or drag to add field to grid</p>
|
||||
<div class="space-y-2" id="sidebar-fields">
|
||||
<div
|
||||
v-for="field in availableFields"
|
||||
:key="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"
|
||||
:data-field-id="field.id"
|
||||
draggable="true"
|
||||
@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="text-xs text-muted-foreground">Type: {{ field.type }}</div>
|
||||
<div class="w-80 border-l bg-white dark:bg-slate-950 p-4 overflow-auto space-y-6">
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold mb-4">Available Fields</h3>
|
||||
<p class="text-xs text-muted-foreground mb-4">Click or drag to add field to grid</p>
|
||||
<div class="space-y-2" id="sidebar-fields">
|
||||
<div
|
||||
v-for="field in availableFields"
|
||||
:key="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"
|
||||
:data-field-id="field.id"
|
||||
draggable="true"
|
||||
@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="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>
|
||||
@@ -52,26 +74,29 @@
|
||||
</template>
|
||||
|
||||
<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/dist/gridstack.min.css'
|
||||
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'
|
||||
|
||||
const props = defineProps<{
|
||||
fields: FieldConfig[]
|
||||
relatedLists?: RelatedListConfig[]
|
||||
initialLayout?: FieldLayoutItem[]
|
||||
initialRelatedLists?: string[]
|
||||
layoutName?: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
save: [layout: FieldLayoutItem[]]
|
||||
save: [layout: { fields: FieldLayoutItem[]; relatedLists: string[] }]
|
||||
}>()
|
||||
|
||||
const gridContainer = ref<HTMLElement | null>(null)
|
||||
let grid: GridStack | null = null
|
||||
const gridItems = ref<Map<string, any>>(new Map())
|
||||
const selectedRelatedLists = ref<string[]>(props.initialRelatedLists || [])
|
||||
|
||||
// Fields that are already on the grid
|
||||
const placedFieldIds = ref<Set<string>>(new Set())
|
||||
@@ -81,6 +106,10 @@ const availableFields = computed(() => {
|
||||
return props.fields.filter(field => !placedFieldIds.value.has(field.id))
|
||||
})
|
||||
|
||||
const relatedLists = computed(() => {
|
||||
return props.relatedLists || []
|
||||
})
|
||||
|
||||
const initGrid = () => {
|
||||
if (!gridContainer.value) return
|
||||
|
||||
@@ -278,7 +307,10 @@ const handleSave = () => {
|
||||
}
|
||||
})
|
||||
|
||||
emit('save', layout)
|
||||
emit('save', {
|
||||
fields: layout,
|
||||
relatedLists: selectedRelatedLists.value,
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
@@ -295,6 +327,13 @@ onBeforeUnmount(() => {
|
||||
watch(() => props.fields, () => {
|
||||
updatePlacedFields()
|
||||
}, { deep: true })
|
||||
|
||||
watch(
|
||||
() => props.initialRelatedLists,
|
||||
(value) => {
|
||||
selectedRelatedLists.value = value ? [...value] : []
|
||||
},
|
||||
)
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
@@ -87,6 +87,22 @@ const getFieldsBySection = (section: FieldSection) => {
|
||||
const usePageLayout = computed(() => {
|
||||
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>
|
||||
|
||||
<template>
|
||||
@@ -138,7 +154,7 @@ const usePageLayout = computed(() => {
|
||||
<Tabs v-else default-value="details" class="space-y-6">
|
||||
<TabsList>
|
||||
<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
|
||||
</TabsTrigger>
|
||||
<TabsTrigger v-if="showSharing && data.id" value="sharing">
|
||||
@@ -224,9 +240,9 @@ const usePageLayout = computed(() => {
|
||||
|
||||
<!-- Related Lists Tab -->
|
||||
<TabsContent value="related" class="space-y-6">
|
||||
<div v-if="config.relatedLists && config.relatedLists.length > 0">
|
||||
<div v-if="visibleRelatedLists.length > 0">
|
||||
<RelatedList
|
||||
v-for="relatedList in config.relatedLists"
|
||||
v-for="relatedList in visibleRelatedLists"
|
||||
:key="relatedList.relationName"
|
||||
:config="relatedList"
|
||||
:parent-id="data.id"
|
||||
|
||||
@@ -97,6 +97,7 @@ export const useFields = () => {
|
||||
objectApiName: objectDef.apiName,
|
||||
mode: 'detail' as ViewMode,
|
||||
fields,
|
||||
relatedLists: objectDef.relatedLists || [],
|
||||
...customConfig,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,6 +155,14 @@ const handleBack = () => {
|
||||
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[]) => {
|
||||
if (confirm(`Delete ${rows.length} record(s)? This action cannot be undone.`)) {
|
||||
try {
|
||||
@@ -279,6 +287,8 @@ onMounted(async () => {
|
||||
@edit="handleEdit"
|
||||
@delete="() => handleDelete([currentRecord])"
|
||||
@back="handleBack"
|
||||
@navigate="handleNavigate"
|
||||
@create-related="handleCreateRelated"
|
||||
/>
|
||||
|
||||
<!-- Edit View -->
|
||||
|
||||
@@ -95,6 +95,14 @@ const handleBack = () => {
|
||||
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[]) => {
|
||||
if (confirm(`Delete ${rows.length} record(s)? This action cannot be undone.`)) {
|
||||
try {
|
||||
@@ -212,6 +220,8 @@ onMounted(async () => {
|
||||
@edit="handleEdit"
|
||||
@delete="() => handleDelete([currentRecord])"
|
||||
@back="handleBack"
|
||||
@navigate="handleNavigate"
|
||||
@create-related="handleCreateRelated"
|
||||
/>
|
||||
|
||||
<!-- Edit View -->
|
||||
|
||||
@@ -178,6 +178,8 @@
|
||||
<PageLayoutEditor
|
||||
:fields="object.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"
|
||||
@save="handleSaveLayout"
|
||||
/>
|
||||
@@ -683,7 +685,7 @@ const handleCreateLayout = async () => {
|
||||
name,
|
||||
objectId: object.value.id,
|
||||
isDefault: layouts.value.length === 0,
|
||||
layoutConfig: { fields: [] },
|
||||
layoutConfig: { fields: [], relatedLists: [] },
|
||||
})
|
||||
|
||||
layouts.value.push(newLayout)
|
||||
@@ -699,12 +701,12 @@ const handleSelectLayout = (layout: PageLayout) => {
|
||||
selectedLayout.value = layout
|
||||
}
|
||||
|
||||
const handleSaveLayout = async (fields: FieldLayoutItem[]) => {
|
||||
const handleSaveLayout = async (layoutConfig: { fields: FieldLayoutItem[]; relatedLists: string[] }) => {
|
||||
if (!selectedLayout.value) return
|
||||
|
||||
try {
|
||||
const updated = await updatePageLayout(selectedLayout.value.id, {
|
||||
layoutConfig: { fields },
|
||||
layoutConfig,
|
||||
})
|
||||
|
||||
// Update the layout in the list
|
||||
|
||||
@@ -8,6 +8,7 @@ export interface FieldLayoutItem {
|
||||
|
||||
export interface PageLayoutConfig {
|
||||
fields: FieldLayoutItem[];
|
||||
relatedLists?: string[];
|
||||
}
|
||||
|
||||
export interface PageLayout {
|
||||
|
||||
Reference in New Issue
Block a user