Add record access strategy
This commit is contained in:
189
frontend/components/RelatedList.vue
Normal file
189
frontend/components/RelatedList.vue
Normal file
@@ -0,0 +1,189 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user