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

@@ -2,7 +2,6 @@ import { computed, onMounted, reactive, ref, watch } from 'vue'
import { useRouter } from '#imports'
import { usePieceTypes } from '~/composables/usePieceTypes'
import { usePieces } from '~/composables/usePieces'
import { useCustomFields } from '~/composables/useCustomFields'
import { useApi } from '~/composables/useApi'
import { useToast } from '~/composables/useToast'
import { useDocuments } from '~/composables/useDocuments'
@@ -26,12 +25,7 @@ import {
collectNormalizedProductIds,
} from '~/shared/utils/pieceProductSelectionUtils'
import { getModelType } from '~/services/modelTypes'
import {
type CustomFieldInput,
buildCustomFieldInputs,
requiredCustomFieldsFilled as _requiredCustomFieldsFilled,
saveCustomFieldValues as _saveCustomFieldValues,
} from '~/shared/utils/customFieldFormUtils'
import { useCustomFieldInputs, type CustomFieldInput } from '~/composables/useCustomFieldInputs'
interface PieceCatalogType extends ModelType {
structure: PieceModelStructure | null
@@ -44,7 +38,6 @@ export function usePieceEdit(pieceId: string) {
const { get } = useApi()
const { pieceTypes, loadPieceTypes } = usePieceTypes()
const { updatePiece } = usePieces()
const { upsertCustomFieldValue, updateCustomFieldValue } = useCustomFields()
const toast = useToast()
const { loadDocumentsByPiece, uploadDocuments, deleteDocument } = useDocuments()
const { ensureConstructeurs } = useConstructeurs()
@@ -90,19 +83,26 @@ export function usePieceEdit(pieceId: string) {
const constructeurIdsFromForm = computed(() => constructeurIdsFromLinks(constructeurLinks.value))
const productSelections = ref<(string | null)[]>([])
const customFieldInputs = ref<CustomFieldInput[]>([])
const resolvedStructure = computed<PieceModelStructure | null>(() =>
pieceTypeDetails.value?.structure ?? selectedType.value?.structure ?? null,
)
const refreshCustomFieldInputs = (
structureOverride?: PieceModelStructure | null,
valuesOverride?: any[] | null,
) => {
const structure = structureOverride ?? resolvedStructure.value ?? null
const values = valuesOverride ?? piece.value?.customFieldValues ?? null
customFieldInputs.value = buildCustomFieldInputs(structure, values)
}
const {
fields: customFieldInputs,
requiredFilled: requiredCustomFieldsFilled,
saveAll: saveAllCustomFields,
refresh: refreshCustomFieldInputs,
} = useCustomFieldInputs({
definitions: computed(() => resolvedStructure.value?.customFields ?? []),
values: computed(() => piece.value?.customFieldValues ?? []),
entityType: 'piece',
entityId: computed(() => piece.value?.id ?? null),
onValueCreated: (newValue) => {
if (piece.value && Array.isArray(piece.value.customFieldValues)) {
piece.value.customFieldValues.push(newValue)
}
},
})
const openPreview = (doc: any) => {
if (!doc || !canPreviewDocument(doc)) {
@@ -221,10 +221,6 @@ export function usePieceEdit(pieceId: string) {
pendingProductIds = []
})
const requiredCustomFieldsFilled = computed(() =>
_requiredCustomFieldsFilled(customFieldInputs.value),
)
const canSubmit = computed(() =>
Boolean(
canEdit.value
@@ -247,9 +243,7 @@ export function usePieceEdit(pieceId: string) {
piece.value = result.data
pieceDocuments.value = Array.isArray(result.data?.documents) ? result.data.documents : []
// Use customFieldValues from entity response (enriched with customField definitions via serialization groups)
const customValues = Array.isArray(result.data?.customFieldValues) ? result.data.customFieldValues : []
refreshCustomFieldInputs(undefined, customValues)
// The watcher on useCustomFieldInputs will auto-refresh when piece.value changes
// Use cached type from loadPieceTypes() instead of separate getModelType() call
loadPieceTypeDetailsFromCache(result.data)
@@ -275,14 +269,14 @@ export function usePieceEdit(pieceId: string) {
const cachedType = (pieceTypes.value || []).find((t: any) => t.id === typeId) ?? null
if (cachedType) {
pieceTypeDetails.value = cachedType
refreshCustomFieldInputs((cachedType.structure as PieceModelStructure | null) ?? null, currentPiece?.customFieldValues ?? null)
// useCustomFieldInputs auto-refreshes via its watcher on resolvedStructure
return
}
// Fallback: fetch if not in cache (edge case)
getModelType(typeId).then((type) => {
if (type && typeof type === 'object') {
pieceTypeDetails.value = type
refreshCustomFieldInputs((type.structure as PieceModelStructure | null) ?? null, currentPiece?.customFieldValues ?? null)
// useCustomFieldInputs auto-refreshes via its watcher on resolvedStructure
}
}).catch(() => {
pieceTypeDetails.value = null
@@ -336,29 +330,21 @@ export function usePieceEdit(pieceId: string) {
pendingProductIds = []
}
// After setting selectedTypeId, read selectedType.value (now updated) instead of
// the stale destructured currentType which was captured before the ID change.
const resolvedType = selectedType.value ?? pieceTypeDetails.value ?? null
refreshCustomFieldInputs(resolvedType?.structure ?? null, currentPiece.customFieldValues)
// useCustomFieldInputs auto-refreshes via its watcher on definitions + values
initialized = true
},
{ immediate: true },
)
watch(selectedType, (currentType) => {
if (!piece.value || !currentType) {
return
}
refreshCustomFieldInputs(currentType.structure, piece.value.customFieldValues)
})
// useCustomFieldInputs auto-refreshes when selectedType changes (via resolvedStructure)
watch(resolvedStructure, (currentStructure) => {
watch(resolvedStructure, () => {
if (!piece.value) {
return
}
ensureProductSelections(structureProducts.value.length)
refreshCustomFieldInputs(currentStructure, piece.value.customFieldValues)
// useCustomFieldInputs auto-refreshes via its watcher on definitions
})
const submitEdition = async () => {
@@ -407,15 +393,10 @@ export function usePieceEdit(pieceId: string) {
try {
const result = await updatePiece(piece.value.id, payload)
if (result.success && result.data) {
const updatedPiece = result.data as Record<string, any>
await _saveCustomFieldValues(
'piece',
updatedPiece.id,
[
updatedPiece?.typePiece?.structure?.customFields,
],
{ customFieldInputs, upsertCustomFieldValue, updateCustomFieldValue, toast },
)
const failedFields = await saveAllCustomFields()
if (failedFields.length) {
toast.showError(`Erreur sur les champs : ${failedFields.join(', ')}`)
}
await syncLinks('piece', piece.value.id, originalConstructeurLinks.value, constructeurLinks.value)
originalConstructeurLinks.value = constructeurLinks.value.map(l => ({ ...l }))
toast.showSuccess('Pièce mise à jour avec succès.')