Replace ~2900 lines across 9 files with ~400 lines in 2 files: - shared/utils/customFields.ts (types + pure helpers) - composables/useCustomFieldInputs.ts (reactive composable) Migrated all consumers: - Backend: add defaultValue to API Platform serialization groups - Standalone pages: component edit/create, piece edit/create, product edit/create/detail - Machine page: MachineCustomFieldsCard, MachineInfoCard, useMachineDetailCustomFields - Hierarchy: ComponentItem, PieceItem - Shared: CustomFieldDisplay, CustomFieldInputGrid - Category editor: componentStructure.ts Deleted: - entityCustomFieldLogic.ts (335 lines) - customFieldUtils.ts (440 lines) - customFieldFormUtils.ts (404 lines) - useEntityCustomFields.ts (181 lines) - customFieldFormUtils.test.ts Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
206 lines
6.5 KiB
TypeScript
206 lines
6.5 KiB
TypeScript
/**
|
|
* Unified reactive custom field management composable.
|
|
*
|
|
* Replaces: useEntityCustomFields.ts, custom field parts of useMachineDetailCustomFields.ts,
|
|
* and inline custom field logic in useComponentEdit/useComponentCreate/usePieceEdit.
|
|
*
|
|
* DESIGN NOTE: Uses an internal mutable `ref` (not a `computed`) so that
|
|
* save operations can update `customFieldValueId` in place without being
|
|
* overwritten on the next reactivity cycle. Call `refresh()` to re-merge
|
|
* from the source definitions + values (e.g. after fetching fresh data).
|
|
*/
|
|
|
|
import { ref, watch, computed, type MaybeRef, toValue } from 'vue'
|
|
import { useCustomFields } from '~/composables/useCustomFields'
|
|
import { useToast } from '~/composables/useToast'
|
|
import {
|
|
mergeDefinitionsWithValues,
|
|
filterByContext,
|
|
formatValueForSave,
|
|
shouldPersist,
|
|
requiredFieldsFilled,
|
|
type CustomFieldDefinition,
|
|
type CustomFieldValue,
|
|
type CustomFieldInput,
|
|
} from '~/shared/utils/customFields'
|
|
|
|
export type { CustomFieldDefinition, CustomFieldValue, CustomFieldInput }
|
|
|
|
export type CustomFieldEntityType =
|
|
| 'machine'
|
|
| 'composant'
|
|
| 'piece'
|
|
| 'product'
|
|
| 'machineComponentLink'
|
|
| 'machinePieceLink'
|
|
|
|
export interface UseCustomFieldInputsOptions {
|
|
/** Custom field definitions (from ModelType structure or machine.customFields) */
|
|
definitions: MaybeRef<any[]>
|
|
/** Persisted custom field values (from entity.customFieldValues or link.contextCustomFieldValues) */
|
|
values: MaybeRef<any[]>
|
|
/** Entity type for API upsert calls */
|
|
entityType: CustomFieldEntityType
|
|
/** Entity ID for API upsert calls */
|
|
entityId: MaybeRef<string | null>
|
|
/** Filter context: 'standalone' hides machineContextOnly, 'machine' shows only machineContextOnly */
|
|
context?: 'standalone' | 'machine'
|
|
/** Optional callback to update the source values array after a save (keeps parent reactive state in sync) */
|
|
onValueCreated?: (newValue: { id: string; value: string; customField: any }) => void
|
|
}
|
|
|
|
export function useCustomFieldInputs(options: UseCustomFieldInputsOptions) {
|
|
const { entityType, context } = options
|
|
const {
|
|
updateCustomFieldValue: updateApi,
|
|
upsertCustomFieldValue,
|
|
} = useCustomFields()
|
|
const { showSuccess, showError } = useToast()
|
|
|
|
// Internal mutable state — NOT a computed, so save can mutate in place
|
|
const _allFields = ref<CustomFieldInput[]>([])
|
|
|
|
// Re-merge from source definitions + values
|
|
const refresh = () => {
|
|
const defs = toValue(options.definitions)
|
|
const vals = toValue(options.values)
|
|
_allFields.value = mergeDefinitionsWithValues(defs, vals)
|
|
}
|
|
|
|
// Auto-refresh when reactive sources change
|
|
watch(
|
|
() => [toValue(options.definitions), toValue(options.values)],
|
|
() => refresh(),
|
|
{ immediate: true, deep: true },
|
|
)
|
|
|
|
// Filtered by context (standalone vs machine)
|
|
const fields = computed<CustomFieldInput[]>(() => {
|
|
if (!context) return _allFields.value
|
|
return filterByContext(_allFields.value, context)
|
|
})
|
|
|
|
// Validation
|
|
const requiredFilled = computed(() => requiredFieldsFilled(fields.value))
|
|
|
|
// Build metadata for upsert when no customFieldId is available (legacy fallback)
|
|
const _buildMetadata = (field: CustomFieldInput) => ({
|
|
customFieldName: field.name,
|
|
customFieldType: field.type,
|
|
customFieldRequired: field.required,
|
|
customFieldOptions: field.options,
|
|
})
|
|
|
|
// Update a single field value
|
|
const update = async (field: CustomFieldInput): Promise<boolean> => {
|
|
const id = toValue(options.entityId)
|
|
if (!id) {
|
|
showError(`Impossible de sauvegarder le champ "${field.name}"`)
|
|
return false
|
|
}
|
|
|
|
const value = formatValueForSave(field)
|
|
|
|
// Update existing value
|
|
if (field.customFieldValueId) {
|
|
const result: any = await updateApi(field.customFieldValueId, { value })
|
|
if (result.success) {
|
|
showSuccess(`Champ "${field.name}" mis à jour`)
|
|
return true
|
|
}
|
|
showError(`Erreur lors de la mise à jour du champ "${field.name}"`)
|
|
return false
|
|
}
|
|
|
|
// Create new value via upsert — with metadata fallback when no ID
|
|
const metadata = field.customFieldId ? undefined : _buildMetadata(field)
|
|
const result: any = await upsertCustomFieldValue(
|
|
field.customFieldId,
|
|
entityType,
|
|
id,
|
|
value,
|
|
metadata,
|
|
)
|
|
|
|
if (result.success) {
|
|
// Mutate in place (safe — _allFields is a ref, not computed)
|
|
if (result.data?.id) {
|
|
field.customFieldValueId = result.data.id
|
|
}
|
|
if (result.data?.customField?.id) {
|
|
field.customFieldId = result.data.customField.id
|
|
}
|
|
// Notify parent to update its reactive source
|
|
if (options.onValueCreated && result.data) {
|
|
options.onValueCreated(result.data)
|
|
}
|
|
showSuccess(`Champ "${field.name}" enregistré`)
|
|
return true
|
|
}
|
|
|
|
showError(`Erreur lors de l'enregistrement du champ "${field.name}"`)
|
|
return false
|
|
}
|
|
|
|
// Save all fields that have values
|
|
const saveAll = async (): Promise<string[]> => {
|
|
const id = toValue(options.entityId)
|
|
if (!id) return ['(entity ID missing)']
|
|
|
|
const failed: string[] = []
|
|
|
|
for (const field of fields.value) {
|
|
if (!shouldPersist(field)) continue
|
|
|
|
const value = formatValueForSave(field)
|
|
|
|
if (field.customFieldValueId) {
|
|
const result: any = await updateApi(field.customFieldValueId, { value })
|
|
if (!result.success) failed.push(field.name)
|
|
continue
|
|
}
|
|
|
|
// Upsert with metadata fallback when no customFieldId
|
|
const metadata = field.customFieldId ? undefined : _buildMetadata(field)
|
|
const result: any = await upsertCustomFieldValue(
|
|
field.customFieldId,
|
|
entityType,
|
|
id,
|
|
value,
|
|
metadata,
|
|
)
|
|
|
|
if (result.success) {
|
|
if (result.data?.id) {
|
|
field.customFieldValueId = result.data.id
|
|
}
|
|
if (result.data?.customField?.id) {
|
|
field.customFieldId = result.data.customField.id
|
|
}
|
|
if (options.onValueCreated && result.data) {
|
|
options.onValueCreated(result.data)
|
|
}
|
|
} else {
|
|
failed.push(field.name)
|
|
}
|
|
}
|
|
|
|
return failed
|
|
}
|
|
|
|
return {
|
|
/** All merged fields filtered by context */
|
|
fields,
|
|
/** All merged fields (unfiltered) */
|
|
allFields: _allFields,
|
|
/** Whether all required fields have values */
|
|
requiredFilled,
|
|
/** Update a single field value via API */
|
|
update,
|
|
/** Save all fields with values, returns list of failed field names */
|
|
saveAll,
|
|
/** Re-merge from source definitions + values (call after fetching fresh data) */
|
|
refresh,
|
|
}
|
|
}
|