refactor(components): extract shared entity utilities and simplify item components (F1.3, F1.4)
Extract 3 entity composables (useEntityCustomFields, useEntityDocuments, useEntityProductDisplay) and entityCustomFieldLogic utility shared across ComponentItem (1336→585 LOC) and PieceItem (1588→740 LOC). Improve type safety in edit/create pages with explicit casts. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
349
app/shared/utils/entityCustomFieldLogic.ts
Normal file
349
app/shared/utils/entityCustomFieldLogic.ts
Normal file
@@ -0,0 +1,349 @@
|
||||
/**
|
||||
* Pure functions for custom field resolution, merging, and deduplication.
|
||||
*
|
||||
* Extracted from ComponentItem.vue and PieceItem.vue which had ~350 LOC
|
||||
* of identical custom field logic duplicated between them.
|
||||
*/
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Field key / identity helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export function fieldKeyFromNameAndType(name: unknown, type: unknown): string | null {
|
||||
const normalizedName = typeof name === 'string' ? name.trim().toLowerCase() : ''
|
||||
const normalizedType = typeof type === 'string' ? type.trim().toLowerCase() : ''
|
||||
return normalizedName ? `${normalizedName}::${normalizedType}` : null
|
||||
}
|
||||
|
||||
export function resolveOrderIndex(field: any): number {
|
||||
if (!field || typeof field !== 'object') return 0
|
||||
if (typeof field.orderIndex === 'number') return field.orderIndex
|
||||
if (field.customField && typeof field.customField.orderIndex === 'number') return field.customField.orderIndex
|
||||
return 0
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Field accessors
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export function resolveFieldKey(field: any, index: number): string {
|
||||
return field?.id ?? field?.customFieldValueId ?? field?.customFieldId ?? field?.name ?? `field-${index}`
|
||||
}
|
||||
|
||||
export function resolveFieldId(field: any): string | null {
|
||||
return field?.customFieldValueId ?? null
|
||||
}
|
||||
|
||||
export function resolveFieldName(field: any): string {
|
||||
return field?.name ?? 'Champ'
|
||||
}
|
||||
|
||||
export function resolveFieldType(field: any): string {
|
||||
return field?.type ?? 'text'
|
||||
}
|
||||
|
||||
export function resolveFieldOptions(field: any): string[] {
|
||||
return field?.options ?? []
|
||||
}
|
||||
|
||||
export function resolveFieldRequired(field: any): boolean {
|
||||
return !!field?.required
|
||||
}
|
||||
|
||||
export function resolveFieldReadOnly(field: any): boolean {
|
||||
return !!field?.readOnly
|
||||
}
|
||||
|
||||
export function resolveCustomFieldId(field: any): string | null {
|
||||
return field?.customFieldId ?? field?.id ?? field?.customField?.id ?? null
|
||||
}
|
||||
|
||||
export function buildCustomFieldMetadata(field: any) {
|
||||
return {
|
||||
customFieldName: resolveFieldName(field),
|
||||
customFieldType: resolveFieldType(field),
|
||||
customFieldRequired: resolveFieldRequired(field),
|
||||
customFieldOptions: resolveFieldOptions(field),
|
||||
}
|
||||
}
|
||||
|
||||
export function formatFieldDisplayValue(field: any): string {
|
||||
const type = resolveFieldType(field)
|
||||
const rawValue = field?.value ?? ''
|
||||
if (type === 'boolean') {
|
||||
const normalized = String(rawValue).toLowerCase()
|
||||
if (normalized === 'true') return 'Oui'
|
||||
if (normalized === 'false') return 'Non'
|
||||
}
|
||||
return rawValue || 'Non défini'
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Custom field ID resolution against candidate pool
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export function ensureCustomFieldId(field: any, candidateFields: any[]): string | null {
|
||||
const existingId = resolveCustomFieldId(field)
|
||||
if (existingId) return existingId
|
||||
|
||||
const name = resolveFieldName(field)
|
||||
if (!name || name === 'Champ') return null
|
||||
|
||||
const matches = candidateFields.filter((candidate) => {
|
||||
if (!candidate || typeof candidate !== 'object') return false
|
||||
const candidateId = candidate.id || candidate.customFieldId
|
||||
if (candidateId && (candidateId === field?.id || candidateId === field?.customFieldId)) return true
|
||||
return typeof candidate.name === 'string' && candidate.name === name
|
||||
})
|
||||
|
||||
if (matches.length) {
|
||||
const withId = matches.find((c) => c?.id || c?.customFieldId) || matches[0]
|
||||
const id = withId?.id || withId?.customFieldId || null
|
||||
if (id) field.customFieldId = id
|
||||
if (!field.customField && typeof withId === 'object') field.customField = withId
|
||||
return id
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Structure extraction
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export function extractStructureCustomFields(structure: any): any[] {
|
||||
if (!structure || typeof structure !== 'object') return []
|
||||
const customFields = structure.customFields
|
||||
return Array.isArray(customFields) ? customFields : []
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Deduplication & merge
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export function deduplicateFieldDefinitions(definitions: any[]): any[] {
|
||||
const result: any[] = []
|
||||
const seen = new Set<string>()
|
||||
|
||||
const orderedDefinitions = (Array.isArray(definitions) ? definitions.slice() : [])
|
||||
.sort((a, b) => resolveOrderIndex(a) - resolveOrderIndex(b))
|
||||
|
||||
orderedDefinitions.forEach((field) => {
|
||||
if (!field || typeof field !== 'object') return
|
||||
const id = field.id ?? field.customFieldId ?? field.customField?.id ?? null
|
||||
const nameKey = fieldKeyFromNameAndType(field.name, field.type)
|
||||
const key = id || nameKey
|
||||
if (key && seen.has(key)) return
|
||||
if (key) seen.add(key)
|
||||
field.orderIndex = resolveOrderIndex(field)
|
||||
result.push(field)
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
export function mergeFieldDefinitionsWithValues(definitions: any[], values: any[]): any[] {
|
||||
const definitionList = Array.isArray(definitions) ? definitions : []
|
||||
const valueList = Array.isArray(values) ? values : []
|
||||
|
||||
const valueMap = new Map<string, any>()
|
||||
valueList.forEach((entry) => {
|
||||
if (!entry || typeof entry !== 'object') return
|
||||
const fieldId = entry.customField?.id ?? entry.customFieldId ?? null
|
||||
if (fieldId) valueMap.set(fieldId, entry)
|
||||
const nameKey = fieldKeyFromNameAndType(entry.customField?.name, entry.customField?.type)
|
||||
if (nameKey) valueMap.set(nameKey, entry)
|
||||
})
|
||||
|
||||
const merged = definitionList.map((field) => {
|
||||
if (!field || typeof field !== 'object') return field
|
||||
|
||||
const fieldId = resolveCustomFieldId(field)
|
||||
const nameKey = fieldKeyFromNameAndType(resolveFieldName(field), resolveFieldType(field))
|
||||
const matchedValue = (fieldId ? valueMap.get(fieldId) : undefined) ?? (nameKey ? valueMap.get(nameKey) : undefined)
|
||||
|
||||
if (!matchedValue) {
|
||||
return { ...field, value: field?.value ?? '', orderIndex: resolveOrderIndex(field) }
|
||||
}
|
||||
|
||||
const resolvedOrder = Math.min(resolveOrderIndex(field), resolveOrderIndex(matchedValue.customField))
|
||||
return {
|
||||
...field,
|
||||
customFieldValueId: matchedValue.id ?? field.customFieldValueId ?? null,
|
||||
customFieldId: matchedValue.customField?.id ?? matchedValue.customFieldId ?? fieldId ?? null,
|
||||
customField: matchedValue.customField ?? field.customField ?? null,
|
||||
value: matchedValue.value ?? field.value ?? '',
|
||||
orderIndex: resolvedOrder,
|
||||
}
|
||||
})
|
||||
|
||||
valueList.forEach((entry) => {
|
||||
if (!entry || typeof entry !== 'object') return
|
||||
|
||||
const fieldId = entry.customField?.id ?? entry.customFieldId ?? null
|
||||
const nameKey = fieldKeyFromNameAndType(entry.customField?.name, entry.customField?.type)
|
||||
|
||||
const exists = merged.some((field) => {
|
||||
if (!field || typeof field !== 'object') return false
|
||||
if (field.customFieldValueId && field.customFieldValueId === entry.id) return true
|
||||
const existingId = resolveCustomFieldId(field)
|
||||
if (fieldId && existingId && existingId === fieldId) return true
|
||||
if (!fieldId && nameKey) {
|
||||
return fieldKeyFromNameAndType(resolveFieldName(field), resolveFieldType(field)) === nameKey
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
if (!exists) {
|
||||
merged.push({
|
||||
customFieldValueId: entry.id ?? null,
|
||||
customFieldId: fieldId,
|
||||
name: entry.customField?.name ?? '',
|
||||
type: entry.customField?.type ?? 'text',
|
||||
required: entry.customField?.required ?? false,
|
||||
options: entry.customField?.options ?? [],
|
||||
value: entry.value ?? '',
|
||||
customField: entry.customField ?? null,
|
||||
orderIndex: resolveOrderIndex(entry.customField),
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return merged.sort((a, b) => resolveOrderIndex(a) - resolveOrderIndex(b))
|
||||
}
|
||||
|
||||
export function dedupeMergedFields(fields: any[]): any[] {
|
||||
if (!Array.isArray(fields) || fields.length <= 1) {
|
||||
return Array.isArray(fields)
|
||||
? fields.slice().sort((a, b) => resolveOrderIndex(a) - resolveOrderIndex(b))
|
||||
: []
|
||||
}
|
||||
|
||||
const seen = new Map<string, any>()
|
||||
const result: any[] = []
|
||||
|
||||
const orderedFields = fields.slice().sort((a, b) => resolveOrderIndex(a) - resolveOrderIndex(b))
|
||||
|
||||
orderedFields.forEach((field) => {
|
||||
if (!field || typeof field !== 'object') return
|
||||
|
||||
const rawName = resolveFieldName(field)
|
||||
const normalizedName = typeof rawName === 'string' ? rawName.trim() : ''
|
||||
if (!normalizedName) return
|
||||
|
||||
field.name = normalizedName
|
||||
field.type = field.type || resolveFieldType(field)
|
||||
|
||||
const fieldId = resolveCustomFieldId(field)
|
||||
const nameKey = fieldKeyFromNameAndType(normalizedName, field.type)
|
||||
const key = fieldId || nameKey
|
||||
|
||||
if (!key) {
|
||||
field.orderIndex = resolveOrderIndex(field)
|
||||
result.push(field)
|
||||
return
|
||||
}
|
||||
|
||||
const existing = seen.get(key)
|
||||
if (!existing) {
|
||||
field.orderIndex = resolveOrderIndex(field)
|
||||
seen.set(key, field)
|
||||
result.push(field)
|
||||
return
|
||||
}
|
||||
|
||||
const existingHasValue = existing.value !== undefined && existing.value !== null && String(existing.value).trim().length > 0
|
||||
const incomingHasValue = field.value !== undefined && field.value !== null && String(field.value).trim().length > 0
|
||||
|
||||
if (!existingHasValue && incomingHasValue) {
|
||||
Object.assign(existing, field)
|
||||
existing.orderIndex = Math.min(resolveOrderIndex(existing), resolveOrderIndex(field))
|
||||
seen.set(key, existing)
|
||||
}
|
||||
})
|
||||
|
||||
return result.sort((a, b) => resolveOrderIndex(a) - resolveOrderIndex(b))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Definition sources builder
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export function buildDefinitionSources(entity: any, entityType: 'composant' | 'piece'): any[] {
|
||||
const definitions: any[] = []
|
||||
const pushFields = (collection: any) => {
|
||||
if (Array.isArray(collection)) definitions.push(...collection)
|
||||
}
|
||||
|
||||
if (entityType === 'composant') {
|
||||
const requirement = entity.typeMachineComponentRequirement || {}
|
||||
const type = requirement.typeComposant || entity.typeComposant || {}
|
||||
|
||||
pushFields(entity.customFields)
|
||||
pushFields(entity.definition?.customFields)
|
||||
pushFields(type.customFields)
|
||||
pushFields(requirement.customFields)
|
||||
pushFields(requirement.definition?.customFields)
|
||||
|
||||
;[
|
||||
entity.definition?.structure,
|
||||
type.structure,
|
||||
type.componentSkeleton,
|
||||
requirement.structure,
|
||||
requirement.componentSkeleton,
|
||||
].forEach((structure) => {
|
||||
const fields = extractStructureCustomFields(structure)
|
||||
if (fields.length) definitions.push(...fields)
|
||||
})
|
||||
} else {
|
||||
const requirement = entity.typeMachinePieceRequirement || {}
|
||||
const type = requirement.typePiece || entity.typePiece || {}
|
||||
|
||||
pushFields(entity.customFields)
|
||||
pushFields(entity.definition?.customFields)
|
||||
pushFields(entity.typePiece?.customFields)
|
||||
pushFields(type.customFields)
|
||||
pushFields(requirement.typePiece?.customFields)
|
||||
pushFields(requirement.customFields)
|
||||
pushFields(requirement.definition?.customFields)
|
||||
|
||||
;[
|
||||
entity.definition?.structure,
|
||||
entity.typePiece?.structure,
|
||||
type.structure,
|
||||
type.pieceSkeleton,
|
||||
entity.typePiece?.pieceSkeleton,
|
||||
requirement.structure,
|
||||
requirement.pieceSkeleton,
|
||||
].forEach((structure) => {
|
||||
const fields = extractStructureCustomFields(structure)
|
||||
if (fields.length) definitions.push(...fields)
|
||||
})
|
||||
}
|
||||
|
||||
return deduplicateFieldDefinitions(definitions)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Candidate fields builder
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export function buildCandidateCustomFields(entity: any, definitionSources: any[]): any[] {
|
||||
const map = new Map<string, any>()
|
||||
const register = (collection: any[]) => {
|
||||
if (!Array.isArray(collection)) return
|
||||
collection.forEach((item) => {
|
||||
if (!item || typeof item !== 'object') return
|
||||
const id = item.id || item.customFieldId
|
||||
const name = typeof item.name === 'string' ? item.name : null
|
||||
const key = id || (name ? `${name}::${item.type ?? ''}` : null)
|
||||
if (!key || map.has(key)) return
|
||||
map.set(key, item)
|
||||
})
|
||||
}
|
||||
|
||||
register((entity.customFieldValues || []).map((value: any) => value?.customField))
|
||||
register(definitionSources)
|
||||
|
||||
return Array.from(map.values())
|
||||
}
|
||||
Reference in New Issue
Block a user