344 lines
9.4 KiB
TypeScript
344 lines
9.4 KiB
TypeScript
import { computed, ref } from 'vue'
|
|
import type { FieldConfig, ListViewConfig, DetailViewConfig, EditViewConfig, ViewMode } from '@/types/field-types'
|
|
|
|
/**
|
|
* Composable for working with dynamic fields and views
|
|
* Helps convert backend field definitions to frontend field configs
|
|
*/
|
|
export const useFields = () => {
|
|
/**
|
|
* Convert backend field definition to frontend FieldConfig
|
|
*/
|
|
const mapFieldDefinitionToConfig = (fieldDef: any): FieldConfig => {
|
|
// Convert isSystem to boolean (handle 0/1 from database)
|
|
const isSystemField = Boolean(fieldDef.isSystem)
|
|
|
|
// Only truly system fields (id, createdAt, updatedAt, etc.) should be hidden on edit
|
|
const isAutoGeneratedField = ['id', 'createdAt', 'updatedAt', 'createdBy', 'updatedBy'].includes(fieldDef.apiName)
|
|
|
|
return {
|
|
id: fieldDef.id,
|
|
apiName: fieldDef.apiName,
|
|
label: fieldDef.label,
|
|
type: fieldDef.type,
|
|
|
|
// Default values
|
|
placeholder: fieldDef.uiMetadata?.placeholder || fieldDef.description,
|
|
helpText: fieldDef.uiMetadata?.helpText || fieldDef.description,
|
|
defaultValue: fieldDef.defaultValue,
|
|
|
|
// Validation
|
|
isRequired: fieldDef.isRequired,
|
|
isReadOnly: isAutoGeneratedField || fieldDef.uiMetadata?.isReadOnly,
|
|
validationRules: fieldDef.uiMetadata?.validationRules || [],
|
|
|
|
// View options - only hide auto-generated fields by default
|
|
showOnList: fieldDef.uiMetadata?.showOnList ?? true,
|
|
showOnDetail: fieldDef.uiMetadata?.showOnDetail ?? true,
|
|
showOnEdit: fieldDef.uiMetadata?.showOnEdit ?? !isAutoGeneratedField,
|
|
sortable: fieldDef.uiMetadata?.sortable ?? true,
|
|
|
|
// Field type specific
|
|
options: fieldDef.uiMetadata?.options,
|
|
rows: fieldDef.uiMetadata?.rows,
|
|
min: fieldDef.uiMetadata?.min,
|
|
max: fieldDef.uiMetadata?.max,
|
|
step: fieldDef.uiMetadata?.step,
|
|
accept: fieldDef.uiMetadata?.accept,
|
|
relationObject: fieldDef.referenceObject,
|
|
relationDisplayField: fieldDef.uiMetadata?.relationDisplayField,
|
|
|
|
// Formatting
|
|
format: fieldDef.uiMetadata?.format,
|
|
prefix: fieldDef.uiMetadata?.prefix,
|
|
suffix: fieldDef.uiMetadata?.suffix,
|
|
|
|
// Advanced
|
|
dependsOn: fieldDef.uiMetadata?.dependsOn,
|
|
computedValue: fieldDef.uiMetadata?.computedValue,
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Build a ListView configuration from object definition
|
|
*/
|
|
const buildListViewConfig = (
|
|
objectDef: any,
|
|
customConfig?: Partial<ListViewConfig>
|
|
): ListViewConfig => {
|
|
const fields = objectDef.fields?.map(mapFieldDefinitionToConfig) || []
|
|
|
|
return {
|
|
objectApiName: objectDef.apiName,
|
|
mode: 'list' as ViewMode,
|
|
fields,
|
|
pageSize: 25,
|
|
searchable: true,
|
|
filterable: true,
|
|
exportable: true,
|
|
...customConfig,
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Build a DetailView configuration from object definition
|
|
*/
|
|
const buildDetailViewConfig = (
|
|
objectDef: any,
|
|
customConfig?: Partial<DetailViewConfig>
|
|
): DetailViewConfig => {
|
|
const fields = objectDef.fields?.map(mapFieldDefinitionToConfig) || []
|
|
|
|
return {
|
|
objectApiName: objectDef.apiName,
|
|
mode: 'detail' as ViewMode,
|
|
fields,
|
|
...customConfig,
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Build an EditView configuration from object definition
|
|
*/
|
|
const buildEditViewConfig = (
|
|
objectDef: any,
|
|
customConfig?: Partial<EditViewConfig>
|
|
): EditViewConfig => {
|
|
const fields = objectDef.fields?.map(mapFieldDefinitionToConfig) || []
|
|
|
|
return {
|
|
objectApiName: objectDef.apiName,
|
|
mode: 'edit' as ViewMode,
|
|
fields,
|
|
submitLabel: 'Save',
|
|
cancelLabel: 'Cancel',
|
|
...customConfig,
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Auto-generate sections based on field types or custom logic
|
|
*/
|
|
const generateSections = (fields: FieldConfig[]) => {
|
|
// Group fields by some logic - this is a simple example
|
|
const basicFields = fields.filter(f =>
|
|
['text', 'email', 'password', 'number'].includes(f.type)
|
|
)
|
|
const relationFields = fields.filter(f =>
|
|
['belongsTo', 'hasMany', 'manyToMany'].includes(f.type)
|
|
)
|
|
const otherFields = fields.filter(f =>
|
|
!basicFields.includes(f) && !relationFields.includes(f)
|
|
)
|
|
|
|
const sections = []
|
|
|
|
if (basicFields.length > 0) {
|
|
sections.push({
|
|
title: 'Basic Information',
|
|
fields: basicFields.map(f => f.apiName),
|
|
})
|
|
}
|
|
|
|
if (relationFields.length > 0) {
|
|
sections.push({
|
|
title: 'Related Records',
|
|
fields: relationFields.map(f => f.apiName),
|
|
collapsible: true,
|
|
})
|
|
}
|
|
|
|
if (otherFields.length > 0) {
|
|
sections.push({
|
|
title: 'Additional Information',
|
|
fields: otherFields.map(f => f.apiName),
|
|
collapsible: true,
|
|
defaultCollapsed: true,
|
|
})
|
|
}
|
|
|
|
return sections
|
|
}
|
|
|
|
return {
|
|
mapFieldDefinitionToConfig,
|
|
buildListViewConfig,
|
|
buildDetailViewConfig,
|
|
buildEditViewConfig,
|
|
generateSections,
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Composable for managing view state (CRUD operations)
|
|
*/
|
|
export const useViewState = <T extends { id?: string }>(
|
|
apiEndpoint: string
|
|
) => {
|
|
const records = ref<T[]>([])
|
|
const currentRecord = ref<T | null>(null)
|
|
const currentView = ref<'list' | 'detail' | 'edit'>('list')
|
|
const loading = ref(false)
|
|
const saving = ref(false)
|
|
const error = ref<string | null>(null)
|
|
|
|
const { api } = useApi()
|
|
|
|
const fetchRecords = async (params?: Record<string, any>) => {
|
|
loading.value = true
|
|
error.value = null
|
|
try {
|
|
const response = await api.get(apiEndpoint, { params })
|
|
// Handle response - data might be directly in response or in response.data
|
|
records.value = response.data || response || []
|
|
} catch (e: any) {
|
|
error.value = e.message
|
|
console.error('Failed to fetch records:', e)
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
const fetchRecord = async (id: string) => {
|
|
loading.value = true
|
|
error.value = null
|
|
try {
|
|
const response = await api.get(`${apiEndpoint}/${id}`)
|
|
// Handle response - data might be directly in response or in response.data
|
|
currentRecord.value = response.data || response
|
|
} catch (e: any) {
|
|
error.value = e.message
|
|
console.error('Failed to fetch record:', e)
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
const createRecord = async (data: Partial<T>) => {
|
|
saving.value = true
|
|
error.value = null
|
|
try {
|
|
const response = await api.post(apiEndpoint, data)
|
|
|
|
// Handle response - it might be the data directly or wrapped in { data: ... }
|
|
const recordData = response.data || response
|
|
records.value.push(recordData)
|
|
currentRecord.value = recordData
|
|
return recordData
|
|
} catch (e: any) {
|
|
error.value = e.message
|
|
console.error('Failed to create record:', e)
|
|
throw e
|
|
} finally {
|
|
saving.value = false
|
|
}
|
|
}
|
|
|
|
const updateRecord = async (id: string, data: Partial<T>) => {
|
|
saving.value = true
|
|
error.value = null
|
|
try {
|
|
// Remove auto-generated fields that shouldn't be updated
|
|
const { id: _id, createdAt, created_at, updatedAt, updated_at, createdBy, updatedBy, ...updateData } = data as any
|
|
|
|
const response = await api.put(`${apiEndpoint}/${id}`, updateData)
|
|
// Handle response - data might be directly in response or in response.data
|
|
const recordData = response.data || response
|
|
const idx = records.value.findIndex(r => r.id === id)
|
|
if (idx !== -1) {
|
|
records.value[idx] = recordData
|
|
}
|
|
currentRecord.value = recordData
|
|
return recordData
|
|
} catch (e: any) {
|
|
error.value = e.message
|
|
console.error('Failed to update record:', e)
|
|
throw e
|
|
} finally {
|
|
saving.value = false
|
|
}
|
|
}
|
|
|
|
const deleteRecord = async (id: string) => {
|
|
loading.value = true
|
|
error.value = null
|
|
try {
|
|
await api.delete(`${apiEndpoint}/${id}`)
|
|
records.value = records.value.filter(r => r.id !== id)
|
|
if (currentRecord.value?.id === id) {
|
|
currentRecord.value = null
|
|
}
|
|
} catch (e: any) {
|
|
error.value = e.message
|
|
console.error('Failed to delete record:', e)
|
|
throw e
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
const deleteRecords = async (ids: string[]) => {
|
|
loading.value = true
|
|
error.value = null
|
|
try {
|
|
await Promise.all(ids.map(id => api.delete(`${apiEndpoint}/${id}`)))
|
|
records.value = records.value.filter(r => !ids.includes(r.id!))
|
|
} catch (e: any) {
|
|
error.value = e.message
|
|
console.error('Failed to delete records:', e)
|
|
throw e
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
const showList = () => {
|
|
currentView.value = 'list'
|
|
currentRecord.value = null
|
|
}
|
|
|
|
const showDetail = (record: T) => {
|
|
currentRecord.value = record
|
|
currentView.value = 'detail'
|
|
}
|
|
|
|
const showEdit = (record?: T) => {
|
|
currentRecord.value = record || ({} as T)
|
|
currentView.value = 'edit'
|
|
}
|
|
|
|
const handleSave = async (data: T) => {
|
|
let savedRecord
|
|
if (data.id) {
|
|
savedRecord = await updateRecord(data.id, data)
|
|
} else {
|
|
savedRecord = await createRecord(data)
|
|
}
|
|
return savedRecord
|
|
}
|
|
|
|
return {
|
|
// State
|
|
records,
|
|
currentRecord,
|
|
currentView,
|
|
loading,
|
|
saving,
|
|
error,
|
|
|
|
// Methods
|
|
fetchRecords,
|
|
fetchRecord,
|
|
createRecord,
|
|
updateRecord,
|
|
deleteRecord,
|
|
deleteRecords,
|
|
|
|
// Navigation
|
|
showList,
|
|
showDetail,
|
|
showEdit,
|
|
handleSave,
|
|
}
|
|
}
|