Files
neo/frontend/components/views/DetailView.vue
2026-01-05 07:48:22 +01:00

178 lines
5.6 KiB
Vue

<script setup lang="ts">
import { computed } from 'vue'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Separator } from '@/components/ui/separator'
import FieldRenderer from '@/components/fields/FieldRenderer.vue'
import RelatedList from '@/components/RelatedList.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'
interface Props {
config: DetailViewConfig & { relatedLists?: RelatedListConfig[] }
data: any
loading?: boolean
}
const props = withDefaults(defineProps<Props>(), {
loading: false,
})
const emit = defineEmits<{
'edit': []
'delete': []
'back': []
'action': [actionId: string]
'navigate': [objectApiName: string, recordId: string]
'createRelated': [objectApiName: string, parentId: string]
}>()
// Organize fields into sections
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)
}
</script>
<template>
<div class="detail-view 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" 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>
<!-- Content Sections -->
<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"
/>
</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"
/>
</div>
</CardContent>
</template>
</Card>
</div>
<!-- Related Lists -->
<div v-if="config.relatedLists && config.relatedLists.length > 0" class="space-y-6">
<RelatedList
v-for="relatedList in config.relatedLists"
: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>
</div>
</template>
<style scoped>
.detail-view {
width: 100%;
}
</style>