Files
neo/frontend/pages/[objectName]/[[recordId]]/[[view]].vue

302 lines
8.6 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script setup lang="ts">
import { ref, computed, onMounted, watch, nextTick } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useApi } from '@/composables/useApi'
import { useFields, useViewState } from '@/composables/useFieldViews'
import ListView from '@/components/views/ListView.vue'
import DetailView from '@/components/views/DetailView.vue'
import EditView from '@/components/views/EditView.vue'
const route = useRoute()
const router = useRouter()
const { api } = useApi()
const { buildListViewConfig, buildDetailViewConfig, buildEditViewConfig } = useFields()
// Use breadcrumbs composable
const { setBreadcrumbs } = useBreadcrumbs()
// Get object API name from route (case-insensitive)
const objectApiName = computed(() => {
const name = route.params.objectName as string
// We'll look up the actual case-sensitive name from the backend
return name
})
const recordId = computed(() => route.params.recordId as string)
const view = computed(() => {
// If recordId is 'new', default to 'edit' view
if (route.params.recordId === 'new' && !route.params.view) {
return 'edit'
}
return (route.params.view as 'list' | 'detail' | 'edit') || 'list'
})
// State
const objectDefinition = ref<any>(null)
const loading = ref(true)
const error = ref<string | null>(null)
// Use view state composable
const {
records,
currentRecord,
loading: dataLoading,
saving,
fetchRecords,
fetchRecord,
deleteRecord,
deleteRecords,
handleSave,
} = useViewState(`/runtime/objects/${objectApiName.value}/records`)
// Compute breadcrumbs based on the current route and object data
const updateBreadcrumbs = () => {
if (!objectDefinition.value) {
return
}
const crumbs: Array<{ name: string; path?: string; isLast?: boolean }> = []
// Add app breadcrumb if object belongs to an app
if (objectDefinition.value?.app) {
crumbs.push({
name: objectDefinition.value.app.label || objectDefinition.value.app.name,
path: undefined, // No path for app grouping
})
}
// Add object breadcrumb - always use plural
const objectLabel = objectDefinition.value?.pluralLabel || objectDefinition.value?.label || objectApiName.value
crumbs.push({
name: objectLabel,
path: `/${objectApiName.value.toLowerCase()}`,
})
// Add record name if viewing/editing a specific record
if (recordId.value && recordId.value !== 'new' && currentRecord.value) {
const nameField = objectDefinition.value?.nameField
let recordName = recordId.value // fallback to ID
// Try to get the display name from the nameField
if (nameField && currentRecord.value[nameField]) {
recordName = currentRecord.value[nameField]
}
crumbs.push({
name: recordName,
isLast: true,
})
} else if (recordId.value === 'new') {
crumbs.push({
name: 'New',
isLast: true,
})
}
setBreadcrumbs(crumbs)
}
// Watch for changes that affect breadcrumbs
watch([objectDefinition, currentRecord, recordId], () => {
updateBreadcrumbs()
}, { deep: true })
// View configs
const listConfig = computed(() => {
if (!objectDefinition.value) return null
return buildListViewConfig(objectDefinition.value, {
searchable: true,
exportable: true,
filterable: true,
})
})
const detailConfig = computed(() => {
if (!objectDefinition.value) return null
return buildDetailViewConfig(objectDefinition.value)
})
const editConfig = computed(() => {
if (!objectDefinition.value) return null
return buildEditViewConfig(objectDefinition.value)
})
// Fetch object definition
const fetchObjectDefinition = async () => {
try {
loading.value = true
error.value = null
const response = await api.get(`/setup/objects/${objectApiName.value}`)
objectDefinition.value = response
} catch (e: any) {
error.value = e.message || 'Failed to load object definition'
console.error('Error fetching object definition:', e)
} finally {
loading.value = false
}
}
// Navigation handlers - use lowercase URLs
const handleRowClick = (row: any) => {
router.push(`/${objectApiName.value.toLowerCase()}/${row.id}/detail`)
}
const handleCreate = () => {
router.push(`/${objectApiName.value.toLowerCase()}/new`)
}
const handleEdit = (row?: any) => {
const id = row?.id || recordId.value
router.push(`/${objectApiName.value.toLowerCase()}/${id}/edit`)
}
const handleBack = () => {
// Navigate to list view explicitly
router.push(`/${objectApiName.value.toLowerCase()}/`)
}
const handleDelete = async (rows: any[]) => {
if (confirm(`Delete ${rows.length} record(s)? This action cannot be undone.`)) {
try {
const ids = rows.map(r => r.id)
await deleteRecords(ids)
if (view.value !== 'list') {
await router.push(`/${objectApiName.value.toLowerCase()}/`)
}
} catch (e: any) {
error.value = e.message || 'Failed to delete records'
}
}
}
const handleSaveRecord = async (data: any) => {
try {
const savedRecord = await handleSave(data)
if (savedRecord?.id) {
router.push(`/${objectApiName.value.toLowerCase()}/${savedRecord.id}/detail`)
} else {
// Fallback to list if no ID available
router.push(`/${objectApiName.value.toLowerCase()}/`)
}
} catch (e: any) {
error.value = e.message || 'Failed to save record'
}
}
const handleCancel = () => {
if (recordId.value && recordId.value !== 'new') {
router.push(`/${objectApiName.value.toLowerCase()}/${recordId.value}/detail`)
} else {
router.push(`/${objectApiName.value.toLowerCase()}/`)
}
}
// Watch for route changes
watch(() => route.params, async (newParams, oldParams) => {
// Reset current record when navigating to 'new'
if (newParams.recordId === 'new') {
currentRecord.value = null
}
// Fetch record if navigating to existing record
if (newParams.recordId && newParams.recordId !== 'new' && newParams.recordId !== oldParams.recordId) {
await fetchRecord(newParams.recordId as string)
}
// Fetch records if navigating back to list
if (!newParams.recordId && !newParams.view) {
await fetchRecords()
}
}, { deep: true })
// Initialize
onMounted(async () => {
await fetchObjectDefinition()
if (view.value === 'list') {
await fetchRecords()
} else if (recordId.value && recordId.value !== 'new') {
await fetchRecord(recordId.value)
}
// Update breadcrumbs after data is loaded
updateBreadcrumbs()
})
</script>
<template>
<NuxtLayout name="default">
<div class="object-view-container">
<!-- Page Header -->
<div v-if="!loading && !error && view === 'list'" class="mb-6">
<h1 class="text-3xl font-bold">{{ objectDefinition?.label || objectApiName }}</h1>
<p v-if="objectDefinition?.description" class="text-muted-foreground mt-2">
{{ objectDefinition.description }}
</p>
</div>
<!-- Loading State -->
<div v-if="loading" class="flex items-center justify-center min-h-screen">
<div class="text-center space-y-4">
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-primary mx-auto"></div>
<p class="text-muted-foreground">Loading {{ objectApiName }}...</p>
</div>
</div>
<!-- Error State -->
<div v-else-if="error" class="flex items-center justify-center min-h-screen">
<div class="text-center space-y-4 max-w-md">
<div class="text-destructive text-5xl"></div>
<h2 class="text-2xl font-bold">Error</h2>
<p class="text-muted-foreground">{{ error }}</p>
<Button @click="router.back()">Go Back</Button>
</div>
</div>
<!-- List View -->
<ListView
v-else-if="view === 'list' && listConfig"
:config="listConfig"
:data="records"
:loading="dataLoading"
selectable
@row-click="handleRowClick"
@create="handleCreate"
@edit="handleEdit"
@delete="handleDelete"
/>
<!-- Detail View -->
<DetailView
v-else-if="view === 'detail' && detailConfig && currentRecord"
:config="detailConfig"
:data="currentRecord"
:loading="dataLoading"
@edit="handleEdit"
@delete="() => handleDelete([currentRecord])"
@back="handleBack"
/>
<!-- Edit View -->
<EditView
v-else-if="(view === 'edit' || recordId === 'new') && editConfig"
:config="editConfig"
:data="currentRecord || {}"
:loading="dataLoading"
:saving="saving"
@save="handleSaveRecord"
@cancel="handleCancel"
@back="handleBack"
/>
</div>
</NuxtLayout>
</template>
<style scoped>
.object-view-container {
min-height: 100vh;
padding: 2rem;
}
</style>