278 lines
8.9 KiB
Vue
278 lines
8.9 KiB
Vue
<script setup lang="ts">
|
|
import { computed, ref, onMounted } from 'vue'
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
|
import { Button } from '@/components/ui/button'
|
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
|
import FieldRenderer from '@/components/fields/FieldRenderer.vue'
|
|
import PageLayoutRenderer from '@/components/PageLayoutRenderer.vue'
|
|
import RelatedList from '@/components/RelatedList.vue'
|
|
import RecordSharing from '@/components/RecordSharing.vue'
|
|
import { DetailViewConfig, ViewMode, FieldSection, FieldConfig, RelatedListConfig } from '@/types/field-types'
|
|
import { Edit, Trash2, ArrowLeft } from 'lucide-vue-next'
|
|
import {
|
|
Collapsible,
|
|
CollapsibleContent,
|
|
CollapsibleTrigger,
|
|
} from '@/components/ui/collapsible'
|
|
import type { PageLayoutConfig } from '~/types/page-layout'
|
|
|
|
interface Props {
|
|
config: DetailViewConfig
|
|
data: any
|
|
loading?: boolean
|
|
objectId?: string // For fetching page layout
|
|
baseUrl?: string
|
|
showSharing?: boolean
|
|
}
|
|
|
|
const props = withDefaults(defineProps<Props>(), {
|
|
loading: false,
|
|
baseUrl: '/runtime/objects',
|
|
showSharing: true,
|
|
})
|
|
|
|
const emit = defineEmits<{
|
|
'edit': []
|
|
'delete': []
|
|
'back': []
|
|
'action': [actionId: string]
|
|
'navigate': [objectApiName: string, recordId: string]
|
|
'createRelated': [objectApiName: string, parentId: string]
|
|
}>()
|
|
|
|
const { getDefaultPageLayout } = usePageLayouts()
|
|
const pageLayout = ref<PageLayoutConfig | null>(null)
|
|
const loadingLayout = ref(false)
|
|
|
|
// Fetch page layout if objectId is provided
|
|
onMounted(async () => {
|
|
if (props.objectId) {
|
|
try {
|
|
loadingLayout.value = true
|
|
const layout = await getDefaultPageLayout(props.objectId)
|
|
if (layout) {
|
|
// Handle both camelCase and snake_case
|
|
pageLayout.value = layout.layoutConfig || layout.layout_config
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading page layout:', error)
|
|
} finally {
|
|
loadingLayout.value = false
|
|
}
|
|
}
|
|
})
|
|
|
|
// Organize fields into sections (for traditional view)
|
|
const sections = computed<FieldSection[]>(() => {
|
|
if (props.config.sections && props.config.sections.length > 0) {
|
|
return props.config.sections
|
|
}
|
|
|
|
// Default section with all visible fields
|
|
return [{
|
|
title: 'Details',
|
|
fields: props.config.fields
|
|
.filter(f => f.showOnDetail !== false)
|
|
.map(f => f.apiName),
|
|
}]
|
|
})
|
|
|
|
const getFieldsBySection = (section: FieldSection) => {
|
|
return section.fields
|
|
.map(apiName => props.config.fields.find(f => f.apiName === apiName))
|
|
.filter((field): field is FieldConfig => field !== undefined)
|
|
}
|
|
|
|
// Use page layout if available, otherwise fall back to sections
|
|
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>
|
|
<div class="detail-view-enhanced space-y-6">
|
|
<!-- Header -->
|
|
<div class="flex items-center justify-between">
|
|
<div class="flex items-center gap-4">
|
|
<Button variant="ghost" size="sm" @click="emit('back')">
|
|
<ArrowLeft class="h-4 w-4 mr-2" />
|
|
Back
|
|
</Button>
|
|
<div>
|
|
<h2 class="text-2xl font-bold tracking-tight">
|
|
{{ data?.name || data?.title || config.objectApiName }}
|
|
</h2>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex items-center gap-2">
|
|
<!-- Custom Actions -->
|
|
<Button
|
|
v-for="action in config.actions"
|
|
:key="action.id"
|
|
:variant="action.variant || 'outline'"
|
|
size="sm"
|
|
@click="emit('action', action.id)"
|
|
>
|
|
{{ action.label }}
|
|
</Button>
|
|
|
|
<!-- Default Actions -->
|
|
<Button variant="outline" size="sm" @click="emit('edit')">
|
|
<Edit class="h-4 w-4 mr-2" />
|
|
Edit
|
|
</Button>
|
|
<Button variant="destructive" size="sm" @click="emit('delete')">
|
|
<Trash2 class="h-4 w-4 mr-2" />
|
|
Delete
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Loading State -->
|
|
<div v-if="loading || loadingLayout" class="flex items-center justify-center py-12">
|
|
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-primary"></div>
|
|
</div>
|
|
|
|
<!-- Tabs for Details, Related, and Sharing -->
|
|
<Tabs v-else default-value="details" class="space-y-6">
|
|
<TabsList>
|
|
<TabsTrigger value="details">Details</TabsTrigger>
|
|
<TabsTrigger v-if="visibleRelatedLists.length > 0" value="related">
|
|
Related
|
|
</TabsTrigger>
|
|
<TabsTrigger v-if="showSharing && data.id" value="sharing">
|
|
Sharing
|
|
</TabsTrigger>
|
|
</TabsList>
|
|
|
|
<!-- Details Tab -->
|
|
<TabsContent value="details" class="space-y-6">
|
|
<!-- Content with Page Layout -->
|
|
<Card v-if="usePageLayout">
|
|
<CardHeader>
|
|
<CardTitle>Details</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<PageLayoutRenderer
|
|
:fields="config.fields"
|
|
:layout="pageLayout"
|
|
:model-value="data"
|
|
:readonly="true"
|
|
/>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<!-- Traditional Section-based Layout -->
|
|
<div v-else class="space-y-6">
|
|
<Card v-for="(section, idx) in sections" :key="idx">
|
|
<Collapsible
|
|
v-if="section.collapsible"
|
|
:default-open="!section.defaultCollapsed"
|
|
>
|
|
<CardHeader>
|
|
<CollapsibleTrigger class="flex items-center justify-between w-full hover:bg-muted/50 -m-2 p-2 rounded">
|
|
<div>
|
|
<CardTitle v-if="section.title">{{ section.title }}</CardTitle>
|
|
<CardDescription v-if="section.description">
|
|
{{ section.description }}
|
|
</CardDescription>
|
|
</div>
|
|
</CollapsibleTrigger>
|
|
</CardHeader>
|
|
<CollapsibleContent>
|
|
<CardContent>
|
|
<div class="grid gap-6 md:grid-cols-2">
|
|
<FieldRenderer
|
|
v-for="field in getFieldsBySection(section)"
|
|
:key="field.id"
|
|
:field="field"
|
|
:model-value="data[field.apiName]"
|
|
:record-data="data"
|
|
:mode="ViewMode.DETAIL"
|
|
:base-url="baseUrl"
|
|
/>
|
|
</div>
|
|
</CardContent>
|
|
</CollapsibleContent>
|
|
</Collapsible>
|
|
|
|
<template v-else>
|
|
<CardHeader v-if="section.title || section.description">
|
|
<CardTitle v-if="section.title">{{ section.title }}</CardTitle>
|
|
<CardDescription v-if="section.description">
|
|
{{ section.description }}
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div class="grid gap-6 md:grid-cols-2">
|
|
<FieldRenderer
|
|
v-for="field in getFieldsBySection(section)"
|
|
:key="field?.id"
|
|
:field="field"
|
|
:model-value="data[field.apiName]"
|
|
:record-data="data"
|
|
:mode="ViewMode.DETAIL"
|
|
:base-url="baseUrl"
|
|
/>
|
|
</div>
|
|
</CardContent>
|
|
</template>
|
|
</Card>
|
|
</div>
|
|
</TabsContent>
|
|
|
|
<!-- Related Lists Tab -->
|
|
<TabsContent value="related" class="space-y-6">
|
|
<div v-if="visibleRelatedLists.length > 0">
|
|
<RelatedList
|
|
v-for="relatedList in visibleRelatedLists"
|
|
:key="relatedList.relationName"
|
|
:config="relatedList"
|
|
:parent-id="data.id"
|
|
:related-records="data[relatedList.relationName]"
|
|
@navigate="(objectApiName, recordId) => emit('navigate', objectApiName, recordId)"
|
|
@create="(objectApiName, parentId) => emit('createRelated', objectApiName, parentId)"
|
|
/>
|
|
</div>
|
|
</TabsContent>
|
|
|
|
<!-- Sharing Tab -->
|
|
<TabsContent value="sharing">
|
|
<Card>
|
|
<CardContent class="pt-6">
|
|
<RecordSharing
|
|
v-if="data.id && config.objectApiName"
|
|
:object-api-name="config.objectApiName"
|
|
:record-id="data.id"
|
|
:owner-id="data.ownerId"
|
|
/>
|
|
</CardContent>
|
|
</Card>
|
|
</TabsContent>
|
|
</Tabs>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.detail-view-enhanced {
|
|
width: 100%;
|
|
}
|
|
</style>
|