refactor(custom-fields) : unify 3 parallel implementations into 1 module

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>
This commit is contained in:
2026-04-04 13:09:27 +02:00
parent f2eff89e00
commit 894d522036
25 changed files with 861 additions and 2279 deletions

View File

@@ -319,16 +319,10 @@ import {
uniqueConstructeurIds,
parseConstructeurLinksFromApi,
} from '~/shared/constructeurUtils'
import {
resolveFieldId,
resolveFieldReadOnly,
resolveCustomFieldId,
mergeFieldDefinitionsWithValues,
dedupeMergedFields,
} from '~/shared/utils/entityCustomFieldLogic'
import { mergeDefinitionsWithValues } from '~/shared/utils/customFields'
import { useCustomFields } from '~/composables/useCustomFields'
import { useEntityDocuments } from '~/composables/useEntityDocuments'
import { useEntityProductDisplay } from '~/composables/useEntityProductDisplay'
import { useEntityCustomFields } from '~/composables/useEntityCustomFields'
const props = defineProps({
piece: { type: Object, required: true },
@@ -392,25 +386,81 @@ const {
} = useEntityProductDisplay({ entity: () => props.piece, selectedProduct })
const {
displayedCustomFields,
updateCustomField,
} = useEntityCustomFields({ entity: () => props.piece, entityType: 'piece' })
updateCustomFieldValue: updateCustomFieldValueApi,
upsertCustomFieldValue,
} = useCustomFields()
const { showSuccess, showError } = useToast()
// Parent already pre-merges standalone custom fields into props.piece.customFields
const displayedCustomFields = computed(() => {
const fields = props.piece?.customFields
return Array.isArray(fields) ? fields.filter((f) => !f.machineContextOnly) : []
})
const updateCustomField = async (field) => {
if (!field || field.readOnly) return
const e = props.piece
const fieldValueId = field.customFieldValueId
if (fieldValueId) {
const result = await updateCustomFieldValueApi(fieldValueId, { value: field.value ?? '' })
if (result.success) {
showSuccess(`Champ "${field.name}" mis à jour avec succès`)
} else {
showError(`Erreur lors de la mise à jour du champ "${field.name}"`)
}
return
}
if (!e?.id) {
showError('Impossible de créer la valeur pour ce champ')
return
}
const metadata = field.customFieldId ? undefined : {
customFieldName: field.name,
customFieldType: field.type,
customFieldRequired: field.required,
customFieldOptions: field.options,
}
const result = await upsertCustomFieldValue(
field.customFieldId,
'piece',
e.id,
field.value ?? '',
metadata,
)
if (result.success) {
const newValue = result.data
if (newValue?.id) {
field.customFieldValueId = newValue.id
field.value = newValue.value ?? field.value ?? ''
if (newValue.customField?.id) {
field.customFieldId = newValue.customField.id
}
}
showSuccess(`Champ "${field.name}" créé avec succès`)
} else {
showError(`Erreur lors de la sauvegarde du champ "${field.name}"`)
}
}
// Context fields are NOT pre-merged — merge locally
const mergedContextFields = computed(() => {
const definitions = props.piece?.contextCustomFields ?? []
const values = props.piece?.contextCustomFieldValues ?? []
if (!definitions.length && !values.length) return []
return dedupeMergedFields(
mergeFieldDefinitionsWithValues(definitions, values),
)
return mergeDefinitionsWithValues(definitions, values)
})
const queueContextCustomFieldUpdate = (field, value) => {
const linkId = props.piece?.linkId
if (!linkId || !field) return
const customFieldId = resolveCustomFieldId(field)
const customFieldValueId = resolveFieldId(field)
const customFieldId = field.customFieldId
const customFieldValueId = field.customFieldValueId
if (!customFieldId && !customFieldValueId) return
field.value = value
@@ -420,7 +470,7 @@ const queueContextCustomFieldUpdate = (field, value) => {
fieldId: customFieldId,
customFieldValueId,
value: value ?? '',
fieldName: field.name || field.customField?.name || 'Champ contextuel',
fieldName: field.name || 'Champ contextuel',
})
}
@@ -544,8 +594,8 @@ const handleProductChange = async (value) => {
// --- Custom field event handlers ---
const handleCustomFieldInput = (field, value) => {
if (resolveFieldReadOnly(field)) return
const fieldValueId = resolveFieldId(field)
if (field.readOnly) return
const fieldValueId = field.customFieldValueId
if (!fieldValueId) return
const fieldValue = props.piece.customFieldValues?.find((fv) => fv.id === fieldValueId)
if (fieldValue) fieldValue.value = value
@@ -553,7 +603,7 @@ const handleCustomFieldInput = (field, value) => {
const handleCustomFieldBlur = async (field) => {
await updateCustomField(field)
const cfId = field?.customFieldId || field?.customField?.id || null
const cfId = field?.customFieldId || null
if (cfId || field?.customFieldValueId) {
emit('custom-field-update', {
fieldId: cfId,