- Remove TypeEdit*, TypeInfoDisplay, MachineSkeletonSummary, MachineCreatePreview components - Remove machine-skeleton pages and type pages - Remove useMachineTypesApi, useMachineSkeletonEditor, useMachineCreateSelections composables - Add AddEntityToMachineModal for direct entity linking - Update machine detail/create pages for direct custom fields - Fix SearchSelect, category display, and ipartial search filters Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
336 lines
12 KiB
TypeScript
336 lines
12 KiB
TypeScript
/**
|
|
* 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 seenIds = new Set<string>()
|
|
const seenNames = 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)
|
|
// Deduplicate by name+type (primary) AND by id — a field with the same
|
|
// name+type is the same field even when stored with different IDs.
|
|
if (nameKey && seenNames.has(nameKey)) return
|
|
if (id && seenIds.has(id)) return
|
|
if (id) seenIds.add(id)
|
|
if (nameKey) seenNames.add(nameKey)
|
|
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 seenById = new Map<string, any>()
|
|
const seenByName = 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)
|
|
|
|
// Check duplicates by name+type first (same field can have different IDs)
|
|
const existing = (nameKey ? seenByName.get(nameKey) : undefined) ?? (fieldId ? seenById.get(fieldId) : undefined)
|
|
|
|
if (!existing) {
|
|
field.orderIndex = resolveOrderIndex(field)
|
|
if (fieldId) seenById.set(fieldId, field)
|
|
if (nameKey) seenByName.set(nameKey, 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))
|
|
if (fieldId) seenById.set(fieldId, existing)
|
|
if (nameKey) seenByName.set(nameKey, 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 type = entity.typeComposant || {}
|
|
|
|
pushFields(entity.customFields)
|
|
pushFields(entity.definition?.customFields)
|
|
pushFields(type.customFields)
|
|
|
|
;[
|
|
entity.definition?.structure,
|
|
type.structure,
|
|
].forEach((structure) => {
|
|
const fields = extractStructureCustomFields(structure)
|
|
if (fields.length) definitions.push(...fields)
|
|
})
|
|
} else {
|
|
const type = entity.typePiece || {}
|
|
|
|
pushFields(entity.customFields)
|
|
pushFields(entity.definition?.customFields)
|
|
pushFields(type.customFields)
|
|
|
|
;[
|
|
entity.definition?.structure,
|
|
type.structure,
|
|
].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())
|
|
}
|