292 lines
9.1 KiB
Vue
292 lines
9.1 KiB
Vue
<script setup lang="ts">
|
|
import { ref, computed, watch } 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 { EditViewConfig, ViewMode, FieldSection, FieldValidationRule } from '@/types/field-types'
|
|
import { Save, X, ArrowLeft } from 'lucide-vue-next'
|
|
import {
|
|
Collapsible,
|
|
CollapsibleContent,
|
|
CollapsibleTrigger,
|
|
} from '@/components/ui/collapsible'
|
|
|
|
console.log('[EditView] COMPONENT MOUNTING')
|
|
|
|
interface Props {
|
|
config: EditViewConfig
|
|
data?: any
|
|
loading?: boolean
|
|
saving?: boolean
|
|
}
|
|
|
|
const props = withDefaults(defineProps<Props>(), {
|
|
data: () => ({}),
|
|
loading: false,
|
|
saving: false,
|
|
})
|
|
|
|
console.log('[EditView] Props received on mount:', JSON.stringify(props, null, 2))
|
|
|
|
const emit = defineEmits<{
|
|
'save': [data: any]
|
|
'cancel': []
|
|
'back': []
|
|
}>()
|
|
|
|
// Form data
|
|
const formData = ref<Record<string, any>>({ ...props.data })
|
|
const errors = ref<Record<string, string>>({})
|
|
|
|
console.log('[EditView] Initial props.data:', JSON.stringify(props.data, null, 2))
|
|
console.log('[EditView] props.data.id:', props.data?.id)
|
|
|
|
// Watch for data changes (useful for edit mode)
|
|
watch(() => props.data, (newData) => {
|
|
console.log('[EditView] Data changed:', JSON.stringify(newData, null, 2))
|
|
console.log('[EditView] newData.id:', newData?.id)
|
|
console.log('[EditView] Keys in newData:', Object.keys(newData))
|
|
formData.value = { ...newData }
|
|
}, { deep: true, immediate: true })
|
|
|
|
// 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
|
|
const visibleFields = props.config.fields
|
|
.filter(f => f.showOnEdit !== false)
|
|
.map(f => f.apiName)
|
|
|
|
return [{
|
|
title: 'Details',
|
|
fields: visibleFields,
|
|
}]
|
|
})
|
|
|
|
const getFieldsBySection = (section: FieldSection) => {
|
|
const fields = section.fields
|
|
.map(apiName => props.config.fields.find(f => f.apiName === apiName))
|
|
.filter(Boolean)
|
|
|
|
return fields
|
|
}
|
|
|
|
const validateField = (field: any): string | null => {
|
|
const value = formData.value[field.apiName]
|
|
|
|
// Required validation
|
|
if (field.isRequired && (value === null || value === undefined || value === '')) {
|
|
return `${field.label} is required`
|
|
}
|
|
|
|
// Custom validation rules
|
|
if (field.validationRules) {
|
|
for (const rule of field.validationRules) {
|
|
switch (rule.type) {
|
|
case 'required':
|
|
if (value === null || value === undefined || value === '') {
|
|
return rule.message || `${field.label} is required`
|
|
}
|
|
break
|
|
case 'min':
|
|
if (typeof value === 'number' && value < rule.value) {
|
|
return rule.message || `${field.label} must be at least ${rule.value}`
|
|
}
|
|
if (typeof value === 'string' && value.length < rule.value) {
|
|
return rule.message || `${field.label} must be at least ${rule.value} characters`
|
|
}
|
|
break
|
|
case 'max':
|
|
if (typeof value === 'number' && value > rule.value) {
|
|
return rule.message || `${field.label} must be at most ${rule.value}`
|
|
}
|
|
if (typeof value === 'string' && value.length > rule.value) {
|
|
return rule.message || `${field.label} must be at most ${rule.value} characters`
|
|
}
|
|
break
|
|
case 'email':
|
|
if (value && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
|
|
return rule.message || `${field.label} must be a valid email`
|
|
}
|
|
break
|
|
case 'url':
|
|
if (value && !/^https?:\/\/.+/.test(value)) {
|
|
return rule.message || `${field.label} must be a valid URL`
|
|
}
|
|
break
|
|
case 'pattern':
|
|
if (value && !new RegExp(rule.value).test(value)) {
|
|
return rule.message || `${field.label} has invalid format`
|
|
}
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
const validateForm = (): boolean => {
|
|
errors.value = {}
|
|
let isValid = true
|
|
|
|
for (const field of props.config.fields) {
|
|
const error = validateField(field)
|
|
if (error) {
|
|
errors.value[field.apiName] = error
|
|
isValid = false
|
|
}
|
|
}
|
|
|
|
return isValid
|
|
}
|
|
|
|
const handleSave = () => {
|
|
if (validateForm()) {
|
|
// Preserve id and other system fields from original data when saving
|
|
emit('save', {
|
|
id: props.data?.id, // Preserve the record ID for updates
|
|
...formData.value
|
|
})
|
|
}
|
|
}
|
|
|
|
const handleCancel = () => {
|
|
formData.value = { ...props.data }
|
|
errors.value = {}
|
|
emit('cancel')
|
|
}
|
|
|
|
const updateFieldValue = (apiName: string, value: any) => {
|
|
formData.value[apiName] = value
|
|
// Clear error for this field when user starts editing
|
|
if (errors.value[apiName]) {
|
|
delete errors.value[apiName]
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div class="edit-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?.id ? 'Edit' : 'Create' }} {{ config.objectApiName }}
|
|
</h2>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex items-center gap-2">
|
|
<Button variant="outline" @click="handleCancel" :disabled="saving">
|
|
<X class="h-4 w-4 mr-2" />
|
|
{{ config.cancelLabel || 'Cancel' }}
|
|
</Button>
|
|
<Button @click="handleSave" :disabled="saving">
|
|
<Save class="h-4 w-4 mr-2" />
|
|
{{ config.submitLabel || 'Save' }}
|
|
</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>
|
|
|
|
<!-- Form Sections -->
|
|
<form v-else @submit.prevent="handleSave" 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">
|
|
<div
|
|
v-for="field in getFieldsBySection(section)"
|
|
:key="field.id"
|
|
class="space-y-1"
|
|
>
|
|
<FieldRenderer
|
|
:field="field"
|
|
:model-value="formData[field.apiName]"
|
|
:mode="ViewMode.EDIT"
|
|
@update:model-value="updateFieldValue(field.apiName, $event)"
|
|
/>
|
|
<p v-if="errors[field.apiName]" class="text-sm text-destructive">
|
|
{{ errors[field.apiName] }}
|
|
</p>
|
|
</div>
|
|
</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">
|
|
<div
|
|
v-for="field in getFieldsBySection(section)"
|
|
:key="field.id"
|
|
class="space-y-1"
|
|
>
|
|
<FieldRenderer
|
|
:field="field"
|
|
:model-value="formData[field.apiName]"
|
|
:mode="ViewMode.EDIT"
|
|
@update:model-value="updateFieldValue(field.apiName, $event)"
|
|
/>
|
|
<p v-if="errors[field.apiName]" class="text-sm text-destructive">
|
|
{{ errors[field.apiName] }}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</template>
|
|
</Card>
|
|
|
|
<!-- Hidden submit button for form submission -->
|
|
<button type="submit" class="hidden" />
|
|
</form>
|
|
|
|
<!-- Save indicator -->
|
|
<div v-if="saving" class="fixed bottom-4 right-4 bg-primary text-primary-foreground px-4 py-2 rounded-lg shadow-lg flex items-center gap-2">
|
|
<div class="animate-spin rounded-full h-4 w-4 border-b-2 border-primary-foreground"></div>
|
|
<span>Saving...</span>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.edit-view {
|
|
width: 100%;
|
|
}
|
|
</style>
|