190 lines
5.4 KiB
Vue
190 lines
5.4 KiB
Vue
<script setup lang="ts">
|
|
import { ref, computed, onMounted } from 'vue'
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
|
import { Button } from '@/components/ui/button'
|
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
|
|
import { Plus, ExternalLink } from 'lucide-vue-next'
|
|
import type { FieldConfig } from '@/types/field-types'
|
|
|
|
interface RelatedListConfig {
|
|
title: string
|
|
relationName: string // e.g., 'domains', 'users'
|
|
objectApiName: string // e.g., 'domains', 'users'
|
|
fields: FieldConfig[] // Fields to display in the list
|
|
canCreate?: boolean
|
|
createRoute?: string // Route to create new related record
|
|
}
|
|
|
|
interface Props {
|
|
config: RelatedListConfig
|
|
parentId: string
|
|
relatedRecords?: any[] // Can be passed in if already fetched
|
|
baseUrl?: string // Base API URL, defaults to '/central'
|
|
}
|
|
|
|
const props = withDefaults(defineProps<Props>(), {
|
|
baseUrl: '/central',
|
|
relatedRecords: undefined,
|
|
})
|
|
|
|
const emit = defineEmits<{
|
|
'navigate': [objectApiName: string, recordId: string]
|
|
'create': [objectApiName: string, parentId: string]
|
|
}>()
|
|
|
|
const { api } = useApi()
|
|
const records = ref<any[]>([])
|
|
const loading = ref(false)
|
|
const error = ref<string | null>(null)
|
|
|
|
// Use provided records or fetch them
|
|
const displayRecords = computed(() => {
|
|
return props.relatedRecords || records.value
|
|
})
|
|
|
|
const fetchRelatedRecords = async () => {
|
|
if (props.relatedRecords) {
|
|
// Records already provided, no need to fetch
|
|
return
|
|
}
|
|
|
|
loading.value = true
|
|
error.value = null
|
|
|
|
try {
|
|
// Replace :parentId placeholder in the API path
|
|
let apiPath = props.config.objectApiName.replace(':parentId', props.parentId)
|
|
|
|
const response = await api.get(`${props.baseUrl}/${apiPath}`, {
|
|
params: {
|
|
parentId: props.parentId,
|
|
},
|
|
})
|
|
records.value = response || []
|
|
} catch (err: any) {
|
|
console.error('Error fetching related records:', err)
|
|
error.value = err.message || 'Failed to fetch related records'
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
const handleCreateNew = () => {
|
|
emit('create', props.config.objectApiName, props.parentId)
|
|
}
|
|
|
|
const handleViewRecord = (recordId: string) => {
|
|
emit('navigate', props.config.objectApiName, recordId)
|
|
}
|
|
|
|
const formatValue = (value: any, field: FieldConfig): string => {
|
|
if (value === null || value === undefined) return '-'
|
|
|
|
// Handle different field types
|
|
if (field.type === 'date') {
|
|
return new Date(value).toLocaleDateString()
|
|
}
|
|
if (field.type === 'datetime') {
|
|
return new Date(value).toLocaleString()
|
|
}
|
|
if (field.type === 'boolean') {
|
|
return value ? 'Yes' : 'No'
|
|
}
|
|
if (field.type === 'select' && field.options) {
|
|
const option = field.options.find(opt => opt.value === value)
|
|
return option?.label || value
|
|
}
|
|
|
|
return String(value)
|
|
}
|
|
|
|
onMounted(() => {
|
|
fetchRelatedRecords()
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<Card class="related-list">
|
|
<CardHeader>
|
|
<div class="flex items-center justify-between">
|
|
<div>
|
|
<CardTitle>{{ config.title }}</CardTitle>
|
|
<CardDescription v-if="displayRecords.length > 0">
|
|
{{ displayRecords.length }} {{ displayRecords.length === 1 ? 'record' : 'records' }}
|
|
</CardDescription>
|
|
</div>
|
|
<Button
|
|
v-if="config.canCreate !== false"
|
|
size="sm"
|
|
@click="handleCreateNew"
|
|
>
|
|
<Plus class="h-4 w-4 mr-2" />
|
|
New
|
|
</Button>
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<!-- Loading State -->
|
|
<div v-if="loading" class="flex items-center justify-center py-8">
|
|
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
|
|
</div>
|
|
|
|
<!-- Error State -->
|
|
<div v-else-if="error" class="text-sm text-destructive py-4">
|
|
{{ error }}
|
|
</div>
|
|
|
|
<!-- Empty State -->
|
|
<div v-else-if="displayRecords.length === 0" class="text-center py-8 text-muted-foreground">
|
|
<p class="text-sm">No {{ config.title.toLowerCase() }} yet</p>
|
|
<Button
|
|
v-if="config.canCreate !== false"
|
|
variant="outline"
|
|
size="sm"
|
|
class="mt-4"
|
|
@click="handleCreateNew"
|
|
>
|
|
<Plus class="h-4 w-4 mr-2" />
|
|
Create First {{ config.title.slice(0, -1) }}
|
|
</Button>
|
|
</div>
|
|
|
|
<!-- Records Table -->
|
|
<div v-else class="rounded-md border">
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead v-for="field in config.fields" :key="field.id">
|
|
{{ field.label }}
|
|
</TableHead>
|
|
<TableHead class="w-[80px]">Actions</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
<TableRow v-for="record in displayRecords" :key="record.id">
|
|
<TableCell v-for="field in config.fields" :key="field.id">
|
|
{{ formatValue(record[field.apiName], field) }}
|
|
</TableCell>
|
|
<TableCell>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
@click="handleViewRecord(record.id)"
|
|
>
|
|
<ExternalLink class="h-4 w-4" />
|
|
</Button>
|
|
</TableCell>
|
|
</TableRow>
|
|
</TableBody>
|
|
</Table>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.related-list {
|
|
margin-top: 1.5rem;
|
|
}
|
|
</style>
|