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

@@ -237,7 +237,6 @@ import ConstructeurSelect from '~/components/ConstructeurSelect.vue'
import DocumentUpload from '~/components/DocumentUpload.vue'
import DocumentPreviewModal from '~/components/DocumentPreviewModal.vue'
import { useProducts } from '~/composables/useProducts'
import { useCustomFields } from '~/composables/useCustomFields'
import { useToast } from '~/composables/useToast'
import { humanizeError } from '~/shared/utils/errorMessages'
import { useDocuments } from '~/composables/useDocuments'
@@ -250,12 +249,7 @@ import type { ConstructeurLinkEntry } from '~/shared/constructeurUtils'
import { getModelType } from '~/services/modelTypes'
import type { ProductModelStructure } from '~/shared/types/inventory'
import { canPreviewDocument } from '~/utils/documentPreview'
import {
type CustomFieldInput,
buildCustomFieldInputs,
requiredCustomFieldsFilled as _requiredCustomFieldsFilled,
saveCustomFieldValues as _saveCustomFieldValues,
} from '~/shared/utils/customFieldFormUtils'
import { useCustomFieldInputs, type CustomFieldEntityType } from '~/composables/useCustomFieldInputs'
const { canEdit } = usePermissions()
const versionRefreshKey = ref(0)
@@ -263,7 +257,6 @@ const route = useRoute()
const router = useRouter()
const toast = useToast()
const { getProduct, updateProduct } = useProducts()
const { upsertCustomFieldValue, updateCustomFieldValue } = useCustomFields()
const {
loadDocumentsByProduct,
uploadDocuments: uploadProductDocuments,
@@ -282,7 +275,19 @@ const {
const product = ref<any | null>(null)
const productType = ref<any | null>(null)
const structure = ref<ProductModelStructure | null>(null)
const customFieldInputs = ref<CustomFieldInput[]>([])
const cfDefinitions = ref<any[]>([])
const cfValues = ref<any[]>([])
const entityId = computed(() => product.value?.id ?? null)
const {
fields: customFieldInputs,
requiredFilled: requiredCustomFieldsFilled,
saveAll: saveAllCustomFields,
} = useCustomFieldInputs({
definitions: cfDefinitions,
values: cfValues,
entityType: 'product' as CustomFieldEntityType,
entityId,
})
const loading = ref(true)
const saving = ref(false)
const selectedFiles = ref<File[]>([])
@@ -311,7 +316,8 @@ const refreshCustomFieldInputs = (
) => {
const nextStructure = structureOverride ?? structure.value ?? null
const nextValues = valuesOverride ?? product.value?.customFieldValues ?? null
customFieldInputs.value = buildCustomFieldInputs(nextStructure, nextValues)
cfDefinitions.value = nextStructure?.customFields ?? []
cfValues.value = Array.isArray(nextValues) ? nextValues : []
}
const editionForm = reactive({
@@ -321,9 +327,7 @@ const editionForm = reactive({
supplierPrice: '' as string,
})
const requiredCustomFieldsFilled = computed(() =>
_requiredCustomFieldsFilled(customFieldInputs.value),
)
// requiredCustomFieldsFilled comes from useCustomFieldInputs composable
const canSubmit = computed(() =>
Boolean(canEdit.value && product.value && editionForm.name.trim().length >= 2 && requiredCustomFieldsFilled.value && !saving.value),
@@ -517,12 +521,7 @@ const submitEdition = async () => {
const result = await updateProduct(product.value.id, payload)
if (result.success && result.data?.id) {
product.value = result.data
const failedFields = await _saveCustomFieldValues(
'product',
result.data.id,
[result.data?.typeProduct?.structure?.customFields],
{ customFieldInputs, upsertCustomFieldValue, updateCustomFieldValue, toast },
)
const failedFields = await saveAllCustomFields()
if (failedFields.length) {
toast.showError(`Impossible d'enregistrer ${failedFields.length} champ(s): ${failedFields.join(', ')}`)
return