import type { BglFormSchemaT, Field, SchemaField } from '@bagelink/vue' import type { DefaultPathsOptions } from 'type-fest/source/paths' import type { MaybeRefOrGetter } from 'vue' import type { SortDirectionsT } from '../../types/TableSchema' import { useBglSchema, isDate, keyToLabel, formatDate } from '@bagelink/vue' import { computed, ref, watch, toValue } from 'vue' const NON_DIGIT_REGEX = /[^\d.-]/g // Components that should receive their value as a child/slot instead of a prop const SLOT_VALUE_COMPONENTS = new Set(['div', 'span', 'p']) // Components that should receive their value as src attribute const SRC_VALUE_COMPONENTS = new Set(['img', 'iframe']) // Extend the base options interface to include computed refs export interface UseTableDataOptions { data: MaybeRefOrGetter schema?: MaybeRefOrGetter | undefined> columns?: MaybeRefOrGetter useServerSort?: MaybeRefOrGetter onSort?: (field: string, direction: SortDirectionsT) => void } interface TransformedDataBase { [key: `_transformed_${string}`]: any [key: `_slot_${string}`]: boolean [key: `_src_${string}`]: boolean [key: `_original_${string}`]: any } type TransformedData = TransDataT & TransformedDataBase function autoTransform(field: Field): Field { if ((field.id === 'created_at' || field.id === 'updated_at') && !field.transform) { field.transform = (val?: any) => val ? formatDate(val) : val } return field } export function useTableData(options: UseTableDataOptions) { // Sorting state const sortField = ref('') const sortDirection = ref('ASC') // Schema loading state const schemaState = ref<'loading' | 'loaded' | 'error'>('loading') const resolvedSchema = ref([]) // Function to resolve schema function resolveSchema() { try { schemaState.value = 'loading' // Get the data safely const dataValue = toValue(options.data) || [] // Get the schema from useBglSchema const schema = useBglSchema({ schema: toValue(options.schema), columns: toValue(options.columns), data: dataValue, }) // If we have a valid schema with fields, filter out fields without an ID if (Array.isArray(schema) && schema.length > 0) { resolvedSchema.value = (schema as SchemaField[]) .filter(field => field) .map(field => field as Field) .map(autoTransform) } else if (Array.isArray(dataValue) && dataValue.length > 0) { // If no schema is provided or it's empty, generate a default schema from the data const firstItem = dataValue[0] // Create a schema based on the keys of the first item resolvedSchema.value = Object.keys(firstItem || {}) .filter(key => key !== 'id' && !key.startsWith('_')) // Exclude id and internal fields .map(key => ({ id: key as any, // Cast to any to resolve type issues with Path label: keyToLabel(key), $el: 'div', transform: (val?: any) => { // Handle date fields const dateFields = ['created_at', 'updated_at'] if (dateFields.includes(key)) { return val ? new Date(val).toLocaleString() : val } return val } })) } else { // Return an empty array if no data or schema resolvedSchema.value = [] } schemaState.value = 'loaded' } catch (error) { console.error('Error resolving schema:', error) schemaState.value = 'error' resolvedSchema.value = [] } } // Watch for changes in schema or columns and resolve schema watch([ () => toValue(options.schema), () => toValue(options.columns), options.data, ], () => { resolveSchema() }, { immediate: true }) // Create a computed property for the schema const computedSchema = computed(() => resolvedSchema.value) function transform(rowData: T): TransformedData { // TODO: only use type casting in the return statement // TODO: replace assignments with Object.assign(transformed, {[key]: value}) const transformed = { ...rowData } as TransformedData const schemaFields = computedSchema.value.filter((f: any) => f.id) for (const field of schemaFields) { const fieldId = field.id as keyof T const fieldData = rowData[fieldId] const transformKey = `_transformed_${String(fieldId)}` as keyof TransformedDataBase const slotKey = `_slot_${String(fieldId)}` as keyof TransformedDataBase const srcKey = `_src_${String(fieldId)}` as keyof TransformedDataBase const originalKey = `_original_${String(fieldId)}` as keyof TransformedDataBase // Store the original value ;(transformed as TransformedDataBase)[originalKey] = fieldData // Determine if this component should receive value as slot or src const isSlotValueComponent = typeof field.$el === 'string' && SLOT_VALUE_COMPONENTS.has(field.$el) const isSrcValueComponent = typeof field.$el === 'string' && SRC_VALUE_COMPONENTS.has(field.$el) ;(transformed as TransformedDataBase)[slotKey] = isSlotValueComponent ;(transformed as TransformedDataBase)[srcKey] = isSrcValueComponent if (field.transform) { const newFieldVal = field.transform(fieldData, rowData) // Store transformed value in _transformed_ key but keep original in the main field ;(transformed as TransformedDataBase)[transformKey] = newFieldVal } else { ;(transformed as TransformedDataBase)[transformKey] = fieldData } } return transformed } // Helper function to clean up transformed data by removing all added properties function cleanTransformedData(data: TransformedData): T { const cleanData = { ...data } as T // Remove all keys that start with underscore (these are added by the transform function) Object.keys(cleanData).forEach((key) => { if (key.startsWith('_')) { delete cleanData[key] } }) return cleanData } const computedSortField = computed(() => sortField.value ? `_transformed_${sortField.value}` : '') const computedData = computed(() => { // Get the data safely const currentData = toValue(options.data) || [] // If there's no data, return an empty array if (!Array.isArray(currentData) || currentData.length === 0) { return [] } if (!sortField.value || toValue(options.useServerSort) === true) { return currentData.map(transform) } return currentData .map(transform) .sort((a, z) => { const aValue = (a as any)[computedSortField.value] ?? '' const bValue = (z as any)[computedSortField.value] ?? '' if (isDate(aValue) && isDate(bValue)) { return sortDirection.value === 'ASC' ? new Date(aValue).getTime() - new Date(bValue).getTime() : new Date(bValue).getTime() - new Date(aValue).getTime() } const numAValue = Number.parseInt(`${aValue}`.replaceAll(NON_DIGIT_REGEX, ''), 10) const numBValue = Number.parseInt(`${bValue}`.replaceAll(NON_DIGIT_REGEX, ''), 10) if (!Number.isNaN(numAValue) && !Number.isNaN(numBValue)) { return sortDirection.value === 'ASC' ? numAValue - numBValue : numBValue - numAValue } if (typeof aValue === 'string') { return sortDirection.value === 'ASC' ? aValue.localeCompare(bValue) : bValue.localeCompare(aValue) } return sortDirection.value === 'ASC' ? (aValue < bValue ? -1 : 1) : (aValue < bValue ? 1 : -1) }) }) function toggleSort(fieldname: string) { if (sortField.value === fieldname) { if (sortDirection.value === 'ASC') { sortDirection.value = 'DESC' } else { sortField.value = '' } } else { sortField.value = fieldname sortDirection.value = 'ASC' } options.onSort?.(sortField.value, sortDirection.value) } return { computedSchema, computedData, transform, sortField: computed(() => sortField.value), sortDirection: computed(() => sortDirection.value), toggleSort, cleanTransformedData, schemaState: computed(() => schemaState.value), } }