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

@@ -177,7 +177,6 @@ import DocumentUpload from '~/components/DocumentUpload.vue'
import { useProductTypes } from '~/composables/useProductTypes'
import { useProducts } from '~/composables/useProducts'
import { useToast } from '~/composables/useToast'
import { useCustomFields } from '~/composables/useCustomFields'
import { useDocuments } from '~/composables/useDocuments'
import { formatProductStructurePreview, normalizeProductStructureForSave } from '~/shared/modelUtils'
import { constructeurIdsFromLinks } from '~/shared/constructeurUtils'
@@ -186,10 +185,7 @@ import { useConstructeurLinks } from '~/composables/useConstructeurLinks'
import { useConstructeurs } from '~/composables/useConstructeurs'
import type { ProductModelStructure } from '~/shared/types/inventory'
import type { ModelType } from '~/services/modelTypes'
import {
type CustomFieldInput,
normalizeCustomFieldInputs as normalizeCustomFieldInputsFromUtils,
} from '~/shared/utils/customFieldFormUtils'
import { useCustomFieldInputs, type CustomFieldEntityType } from '~/composables/useCustomFieldInputs'
interface ProductCatalogType extends ModelType {
structure: ProductModelStructure | null
@@ -202,7 +198,6 @@ const router = useRouter()
const { productTypes, loadProductTypes, loadingProductTypes: loadingTypes } = useProductTypes()
const { createProduct } = useProducts()
const toast = useToast()
const { upsertCustomFieldValue } = useCustomFields()
const { uploadDocuments } = useDocuments()
const { canEdit } = usePermissions()
const { syncLinks } = useConstructeurLinks()
@@ -221,7 +216,14 @@ const constructeurLinks = ref<ConstructeurLinkEntry[]>([])
const selectedDocuments = ref<File[]>([])
const uploadingDocuments = ref(false)
const customFieldInputs = ref<CustomFieldInput[]>([])
const cfDefinitions = ref<any[]>([])
const createdEntityId = ref<string | null>(null)
const { fields: customFieldInputs, requiredFilled: requiredCustomFieldsFilled, saveAll: saveAllCustomFields } = useCustomFieldInputs({
definitions: cfDefinitions,
values: [] as any[],
entityType: 'product' as CustomFieldEntityType,
entityId: createdEntityId,
})
const productTypeList = computed<ProductCatalogType[]>(() =>
(productTypes.value || []) as ProductCatalogType[],
@@ -264,26 +266,17 @@ watch(selectedTypeId, (id) => {
watch(selectedType, (type) => {
if (!type) {
clearForm()
customFieldInputs.value = []
cfDefinitions.value = []
return
}
if (!creationForm.name) {
creationForm.name = type.name
}
customFieldInputs.value = normalizeCustomFieldInputsFromUtils(normalizeProductStructureForSave(type.structure))
const normalized = normalizeProductStructureForSave(type.structure)
cfDefinitions.value = normalized?.customFields ?? []
})
const requiredCustomFieldsFilled = computed(() =>
customFieldInputs.value.every((field) => {
if (!field.required) {
return true
}
if (field.type === 'boolean') {
return field.value === 'true' || field.value === 'false'
}
return field.value.trim().length > 0
}),
)
// requiredCustomFieldsFilled comes from useCustomFieldInputs composable
const canSubmit = computed(() => Boolean(
canEdit.value &&
@@ -336,7 +329,8 @@ const submitCreation = async () => {
const result = await createProduct(payload)
if (result.success && result.data?.id) {
const productId = result.data.id
const failedFields = await saveCustomFieldValues(result.data.id)
createdEntityId.value = productId
const failedFields = await saveAllCustomFields()
if (failedFields.length) {
toast.showError(`Produit créé, mais impossible d'enregistrer ${failedFields.length} champ(s): ${failedFields.join(', ')}`)
await router.replace(`/product/${result.data.id}?edit=true`)
@@ -375,39 +369,6 @@ const submitCreation = async () => {
}
}
const saveCustomFieldValues = async (productId: string) => {
const failed: string[] = []
for (const field of customFieldInputs.value) {
if (!field.name) {
continue
}
const value = field.value ?? ''
const metadata = field.customFieldId
? undefined
: { customFieldName: field.name, customFieldType: field.type, customFieldRequired: field.required }
const result = await upsertCustomFieldValue(
field.customFieldId,
'product',
productId,
String(value ?? ''),
metadata,
)
if (!result.success) {
failed.push(field.name)
} else {
const createdValue = result.data
if (createdValue?.id) {
field.customFieldValueId = createdValue.id
}
const resolvedId = createdValue?.customField?.id || field.customFieldId
if (resolvedId) {
field.customFieldId = resolvedId
}
}
}
return failed
}
// Sync constructeurIds → constructeurLinks when IDs are added via ConstructeurSelect
watch(
() => creationForm.constructeurIds,