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) // Define all system/auto-generated field names const systemFieldNames = ['id', 'createdAt', 'updatedAt', 'created_at', 'updated_at', 'createdBy', 'updatedBy', 'tenantId', 'ownerId'] const isAutoGeneratedField = systemFieldNames.includes(fieldDef.apiName) // Hide system fields and auto-generated fields on edit const shouldHideOnEdit = isSystemField || isAutoGeneratedField // Hide 'id' field from list view by default (check both apiName and id field) const shouldHideOnList = fieldDef.apiName === 'id' || fieldDef.label === 'Id' || fieldDef.label === 'ID' return { id: fieldDef.id, apiName: fieldDef.apiName, label: fieldDef.label, type: fieldDef.type, // Default values placeholder: fieldDef.placeholder || fieldDef.description, helpText: fieldDef.helpText || fieldDef.description, defaultValue: fieldDef.defaultValue, // Validation isRequired: fieldDef.isRequired, isReadOnly: isAutoGeneratedField || fieldDef.isReadOnly, validationRules: fieldDef.validationRules || [], // View options - only hide system and auto-generated fields by default showOnList: fieldDef.showOnList ?? !shouldHideOnList, showOnDetail: fieldDef.showOnDetail ?? true, showOnEdit: fieldDef.showOnEdit ?? !shouldHideOnEdit, sortable: fieldDef.sortable ?? true, // Field type specific options: fieldDef.options, rows: fieldDef.rows, min: fieldDef.min, max: fieldDef.max, step: fieldDef.step, accept: fieldDef.accept, relationObject: fieldDef.relationObject, relationObjects: fieldDef.relationObjects, relationDisplayField: fieldDef.relationDisplayField, relationTypeField: fieldDef.relationTypeField, // Formatting format: fieldDef.format, prefix: fieldDef.prefix, suffix: fieldDef.suffix, // Advanced dependsOn: fieldDef.uiMetadata?.dependsOn, computedValue: fieldDef.uiMetadata?.computedValue, } } /** * Build a ListView configuration from object definition * @param objectDef - The object definition containing fields * @param customConfig - Optional custom configuration * @param listLayoutConfig - Optional list view layout configuration from page_layouts */ const buildListViewConfig = ( objectDef: any, customConfig?: Partial, listLayoutConfig?: { fields: Array<{ fieldId: string; order?: number }> } | null ): ListViewConfig => { let fields = objectDef.fields?.map(mapFieldDefinitionToConfig) || [] // If a list layout is provided, filter and order fields according to it if (listLayoutConfig && listLayoutConfig.fields && listLayoutConfig.fields.length > 0) { // Sort layout fields by order const sortedLayoutFields = [...listLayoutConfig.fields].sort((a, b) => (a.order ?? 0) - (b.order ?? 0)) // Map layout fields to actual field configs, preserving order const orderedFields: FieldConfig[] = [] for (const layoutField of sortedLayoutFields) { const fieldConfig = fields.find((f: FieldConfig) => f.id === layoutField.fieldId) if (fieldConfig) { orderedFields.push(fieldConfig) } } // Use ordered fields if we found any, otherwise fall back to all fields if (orderedFields.length > 0) { fields = orderedFields } } return { objectApiName: objectDef.apiName, mode: 'list' as ViewMode, fields, pageSize: 10, maxFrontendRecords: 500, searchable: true, filterable: true, exportable: true, ...customConfig, } } /** * Build a DetailView configuration from object definition */ const buildDetailViewConfig = ( objectDef: any, customConfig?: Partial ): DetailViewConfig => { const fields = objectDef.fields?.map(mapFieldDefinitionToConfig) || [] return { objectApiName: objectDef.apiName, mode: 'detail' as ViewMode, fields, relatedLists: objectDef.relatedLists || [], ...customConfig, } } /** * Build an EditView configuration from object definition */ const buildEditViewConfig = ( objectDef: any, customConfig?: Partial ): 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 = ( apiEndpoint: string ) => { const records = ref([]) const totalCount = ref(0) const currentRecord = ref(null) const currentView = ref<'list' | 'detail' | 'edit'>('list') const loading = ref(false) const saving = ref(false) const error = ref(null) const { api } = useApi() const normalizeListResponse = (response: any) => { const payload: { data: T[]; totalCount: number; page?: number; pageSize?: number } = { data: [], totalCount: 0, } if (Array.isArray(response)) { payload.data = response payload.totalCount = response.length return payload } if (response && typeof response === 'object') { if (Array.isArray(response.data)) { payload.data = response.data } else if (Array.isArray((response as any).records)) { payload.data = (response as any).records } else if (Array.isArray((response as any).results)) { payload.data = (response as any).results } payload.totalCount = response.totalCount ?? response.total ?? response.count ?? payload.data.length ?? 0 payload.page = response.page payload.pageSize = response.pageSize } return payload } const fetchRecords = async (params?: Record, options?: { append?: boolean }) => { loading.value = true error.value = null try { const response = await api.get(apiEndpoint, { params }) const normalized = normalizeListResponse(response) totalCount.value = normalized.totalCount ?? normalized.data.length ?? 0 records.value = options?.append ? [...records.value, ...normalized.data] : normalized.data return normalized } 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) => { 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) totalCount.value += 1 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) => { 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) totalCount.value = Math.max(0, totalCount.value - 1) 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 { const useBulkEndpoint = apiEndpoint.includes('/runtime/objects/') if (useBulkEndpoint) { const response = await api.post(`${apiEndpoint}/bulk-delete`, { ids }) const deletedIds = Array.isArray(response?.deletedIds) ? response.deletedIds : ids records.value = records.value.filter(r => !deletedIds.includes(r.id!)) totalCount.value = Math.max(0, totalCount.value - deletedIds.length) return { deletedIds, deniedIds: Array.isArray(response?.deniedIds) ? response.deniedIds : [], } } await Promise.all(ids.map(id => api.delete(`${apiEndpoint}/${id}`))) records.value = records.value.filter(r => !ids.includes(r.id!)) totalCount.value = Math.max(0, totalCount.value - ids.length) return { deletedIds: ids, deniedIds: [], } } 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, totalCount, currentRecord, currentView, loading, saving, error, // Methods fetchRecords, fetchRecord, createRecord, updateRecord, deleteRecord, deleteRecords, // Navigation showList, showDetail, showEdit, handleSave, } }