From 4c714b3647c09ad1ac72ad8bea973672cdbadd62 Mon Sep 17 00:00:00 2001 From: Matthieu Date: Tue, 28 Oct 2025 18:08:14 +0100 Subject: [PATCH] =?UTF-8?q?feat:=20drag=20&=20drop=20des=20champs=20person?= =?UTF-8?q?nalis=C3=A9s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/components/ComponentItem.vue | 64 +++++++-- app/components/CustomFieldsDisplay.vue | 15 +- app/components/PieceItem.vue | 59 +++++++- app/components/PieceModelStructureEditor.vue | 17 ++- app/components/StructureNodeEditor.vue | 113 ++++++++++++++- .../TypeEditCustomFieldsSection.vue | 129 ++++++++++++++++-- app/components/TypeEditForm.vue | 25 +++- app/pages/component/[id]/edit.vue | 18 ++- app/pages/component/create.vue | 9 +- app/pages/machine-skeleton/new.vue | 7 +- app/pages/machine/[id].vue | 13 +- app/pages/pieces/[id]/edit.vue | 19 ++- app/pages/pieces/create.vue | 10 +- app/pages/type/edit/[id].vue | 7 +- app/shared/modelUtils.ts | 18 ++- app/shared/types/inventory.ts | 2 + 16 files changed, 458 insertions(+), 67 deletions(-) diff --git a/app/components/ComponentItem.vue b/app/components/ComponentItem.vue index 8624d41..905feab 100644 --- a/app/components/ComponentItem.vue +++ b/app/components/ComponentItem.vue @@ -461,16 +461,39 @@ const extractStructureCustomFields = (structure) => { } function fieldKeyFromNameAndType(name, type) { - const normalizedName = typeof name === 'string' ? name.trim() : '' - const normalizedType = typeof type === 'string' ? type : '' + const normalizedName = + typeof name === 'string' ? name.trim().toLowerCase() : '' + const normalizedType = + typeof type === 'string' ? type.trim().toLowerCase() : '' return normalizedName ? `${normalizedName}::${normalizedType}` : null } +function resolveOrderIndex(field) { + 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 +} + function deduplicateFieldDefinitions(definitions) { const result = [] const seen = new Set() - ;(Array.isArray(definitions) ? definitions : []).forEach((field) => { + const orderedDefinitions = (Array.isArray(definitions) + ? definitions.slice() + : [] + ).sort((a, b) => resolveOrderIndex(a) - resolveOrderIndex(b)) + + orderedDefinitions.forEach((field) => { if (!field || typeof field !== 'object') { return } @@ -490,6 +513,7 @@ function deduplicateFieldDefinitions(definitions) { if (key) { seen.add(key) } + field.orderIndex = resolveOrderIndex(field) result.push(field) }) @@ -530,10 +554,16 @@ function mergeFieldDefinitionsWithValues(definitions, values) { if (!matchedValue) { return { ...field, - value: field?.value ?? '' + value: field?.value ?? '', + orderIndex: resolveOrderIndex(field), } } + const resolvedOrder = Math.min( + resolveOrderIndex(field), + resolveOrderIndex(matchedValue.customField), + ) + return { ...field, customFieldValueId: matchedValue.id ?? field.customFieldValueId ?? null, @@ -543,7 +573,8 @@ function mergeFieldDefinitionsWithValues(definitions, values) { fieldId ?? null, customField: matchedValue.customField ?? field.customField ?? null, - value: matchedValue.value ?? field.value ?? '' + value: matchedValue.value ?? field.value ?? '', + orderIndex: resolvedOrder, } }) @@ -583,23 +614,30 @@ function mergeFieldDefinitionsWithValues(definitions, values) { required: entry.customField?.required ?? false, options: entry.customField?.options ?? [], value: entry.value ?? '', - customField: entry.customField ?? null + customField: entry.customField ?? null, + orderIndex: resolveOrderIndex(entry.customField), }) } }) - return merged + return merged.sort((a, b) => resolveOrderIndex(a) - resolveOrderIndex(b)) } function dedupeMergedFields(fields) { if (!Array.isArray(fields) || fields.length <= 1) { - return Array.isArray(fields) ? fields : [] + return Array.isArray(fields) + ? fields.slice().sort((a, b) => resolveOrderIndex(a) - resolveOrderIndex(b)) + : [] } const seen = new Map() const result = [] - fields.forEach((field) => { + const orderedFields = fields + .slice() + .sort((a, b) => resolveOrderIndex(a) - resolveOrderIndex(b)) + + orderedFields.forEach((field) => { if (!field || typeof field !== 'object') { return } @@ -617,12 +655,14 @@ function dedupeMergedFields(fields) { 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 @@ -640,11 +680,15 @@ function dedupeMergedFields(fields) { if (!existingHasValue && incomingHasValue) { Object.assign(existing, field) + existing.orderIndex = Math.min( + resolveOrderIndex(existing), + resolveOrderIndex(field), + ) seen.set(key, existing) } }) - return result + return result.sort((a, b) => resolveOrderIndex(a) - resolveOrderIndex(b)) } const componentDefinitionSources = computed(() => { diff --git a/app/components/CustomFieldsDisplay.vue b/app/components/CustomFieldsDisplay.vue index c75fb76..8f35667 100644 --- a/app/components/CustomFieldsDisplay.vue +++ b/app/components/CustomFieldsDisplay.vue @@ -5,7 +5,7 @@
@@ -81,7 +81,7 @@ diff --git a/app/components/TypeEditForm.vue b/app/components/TypeEditForm.vue index 9bab4c5..33f1ec0 100644 --- a/app/components/TypeEditForm.vue +++ b/app/components/TypeEditForm.vue @@ -54,14 +54,33 @@ const emit = defineEmits(['update:modelValue', 'submit']) const deepClone = value => JSON.parse(JSON.stringify(value)) +const createFieldKey = () => `cf-${Math.random().toString(16).slice(2)}-${Date.now()}` + +const withNormalizedOrder = (items = []) => { + if (!Array.isArray(items)) { return [] } + return items + .map((item, index) => { + const clone = deepClone(item) + const currentOrder = + typeof clone?.orderIndex === 'number' ? clone.orderIndex : index + clone.orderIndex = currentOrder + if (typeof clone?.__key !== 'string' || !clone.__key) { + clone.__key = createFieldKey() + } + return clone + }) + .sort((a, b) => (a.orderIndex ?? 0) - (b.orderIndex ?? 0)) + .map((item, index) => ({ ...item, orderIndex: index })) +} + const createDefaultForm = (source = {}) => ({ name: source.name || '', description: source.description || '', category: source.category || '', maintenanceFrequency: source.maintenanceFrequency || '', - customFields: deepClone(source.customFields || []), - componentRequirements: deepClone(source.componentRequirements || []), - pieceRequirements: deepClone(source.pieceRequirements || []) + customFields: withNormalizedOrder(source.customFields || []), + componentRequirements: withNormalizedOrder(source.componentRequirements || []), + pieceRequirements: withNormalizedOrder(source.pieceRequirements || []) }) const formData = reactive(createDefaultForm(props.modelValue)) diff --git a/app/pages/component/[id]/edit.vue b/app/pages/component/[id]/edit.vue index e57fa7c..d503b67 100644 --- a/app/pages/component/[id]/edit.vue +++ b/app/pages/component/[id]/edit.vue @@ -424,6 +424,7 @@ interface CustomFieldInput { value: string customFieldId: string | null customFieldValueId: string | null + orderIndex: number } const route = useRoute() @@ -756,6 +757,7 @@ const buildCustomFieldInputs = ( ...definition, customFieldId: definition.customFieldId || definition.id, customFieldValueId: null, + orderIndex: definition.orderIndex, } } @@ -765,8 +767,14 @@ const buildCustomFieldInputs = ( customFieldId: matched.customField?.id || definition.customFieldId || definition.id, customFieldValueId: matched.id ?? null, value: formatDefaultValue(definition.type, resolvedValue), + orderIndex: Math.min( + definition.orderIndex ?? 0, + typeof matched.customField?.orderIndex === 'number' + ? matched.customField.orderIndex + : definition.orderIndex ?? 0, + ), } - }) + }).sort((a, b) => (a.orderIndex ?? 0) - (b.orderIndex ?? 0)) return resolved } @@ -780,11 +788,12 @@ const normalizeCustomFieldInputs = (structure: ComponentModelStructure | null): } const fields = Array.isArray(structure.customFields) ? structure.customFields : [] return fields - .map((field) => normalizeCustomField(field)) + .map((field, index) => normalizeCustomField(field, index)) .filter((field): field is CustomFieldInput => field !== null) + .sort((a, b) => a.orderIndex - b.orderIndex) } -const normalizeCustomField = (rawField: any): CustomFieldInput | null => { +const normalizeCustomField = (rawField: any, fallbackIndex = 0): CustomFieldInput | null => { if (!rawField || typeof rawField !== 'object') { return null } @@ -802,7 +811,8 @@ const normalizeCustomField = (rawField: any): CustomFieldInput | null => { const customFieldValueId = typeof rawField.customFieldValueId === 'string' ? rawField.customFieldValueId : null - return { id, name, type, required, options, value, customFieldId, customFieldValueId } + const orderIndex = typeof rawField.orderIndex === 'number' ? rawField.orderIndex : fallbackIndex + return { id, name, type, required, options, value, customFieldId, customFieldValueId, orderIndex } } const resolveFieldName = (field: any): string => { diff --git a/app/pages/component/create.vue b/app/pages/component/create.vue index e381119..2d1bc6a 100644 --- a/app/pages/component/create.vue +++ b/app/pages/component/create.vue @@ -841,6 +841,7 @@ interface CustomFieldInput { value: string customFieldId: string | null customFieldValueId: string | null + orderIndex: number } const fieldKey = (field: CustomFieldInput, index: number) => @@ -852,11 +853,12 @@ const normalizeCustomFieldInputs = (structure: ComponentModelStructure | null): } const fields = Array.isArray(structure.customFields) ? structure.customFields : [] return fields - .map((field) => normalizeCustomField(field)) + .map((field, index) => normalizeCustomField(field, index)) .filter((field): field is CustomFieldInput => field !== null) + .sort((a, b) => a.orderIndex - b.orderIndex) } -const normalizeCustomField = (rawField: any): CustomFieldInput | null => { +const normalizeCustomField = (rawField: any, fallbackIndex = 0): CustomFieldInput | null => { if (!rawField || typeof rawField !== 'object') { return null } @@ -874,7 +876,8 @@ const normalizeCustomField = (rawField: any): CustomFieldInput | null => { const customFieldValueId = typeof rawField.customFieldValueId === 'string' ? rawField.customFieldValueId : null - return { id, name, type, required, options, value, customFieldId, customFieldValueId } + const orderIndex = typeof rawField.orderIndex === 'number' ? rawField.orderIndex : fallbackIndex + return { id, name, type, required, options, value, customFieldId, customFieldValueId, orderIndex } } const resolveFieldName = (field: any): string => { diff --git a/app/pages/machine-skeleton/new.vue b/app/pages/machine-skeleton/new.vue index 49dc61b..26112cd 100644 --- a/app/pages/machine-skeleton/new.vue +++ b/app/pages/machine-skeleton/new.vue @@ -139,12 +139,15 @@ const parseOptions = (field = {}) => { const normalizeCustomFields = (fields = []) => fields .filter(field => field?.name && field.name.trim() !== '') - .map(field => ({ + .map((field, index) => ({ name: field.name, type: field.type || '', required: !!field.required, - options: parseOptions(field) + options: parseOptions(field), + orderIndex: typeof field.orderIndex === 'number' ? field.orderIndex : index })) + .sort((a, b) => (a.orderIndex ?? 0) - (b.orderIndex ?? 0)) + .map((field, index) => ({ ...field, orderIndex: index })) const toIntegerOrNull = (value, fallback = null) => { if (value === '' || value === undefined || value === null) { diff --git a/app/pages/machine/[id].vue b/app/pages/machine/[id].vue index a5f0d54..4fdb4c8 100644 --- a/app/pages/machine/[id].vue +++ b/app/pages/machine/[id].vue @@ -1853,6 +1853,12 @@ const visibleMachineCustomFields = computed(() => { const summarizeCustomFields = (fields = []) => { const seen = new Set() return fields + .slice() + .sort((a, b) => { + const left = typeof a?.orderIndex === 'number' ? a.orderIndex : 0 + const right = typeof b?.orderIndex === 'number' ? b.orderIndex : 0 + return left - right + }) .filter(shouldDisplayCustomField) .filter((field) => { const key = field.customFieldId || field.id || field.name @@ -2013,11 +2019,12 @@ const normalizeExistingCustomFieldDefinitions = (fields) => { return [] } return fields - .map((field) => normalizeCustomFieldDefinitionEntry(field)) + .map((field, index) => normalizeCustomFieldDefinitionEntry(field, index)) .filter((definition) => definition !== null) + .sort((a, b) => (a.orderIndex ?? 0) - (b.orderIndex ?? 0)) } -const normalizeCustomFieldDefinitionEntry = (definition = {}) => { +const normalizeCustomFieldDefinitionEntry = (definition = {}, fallbackIndex = 0) => { const name = extractDefinitionName(definition) if (!name) { return null @@ -2028,6 +2035,7 @@ const normalizeCustomFieldDefinitionEntry = (definition = {}) => { const defaultValue = extractDefinitionDefaultValue(definition) const id = typeof definition?.id === 'string' ? definition.id : undefined const customFieldId = typeof definition?.customFieldId === 'string' ? definition.customFieldId : id + const orderIndex = typeof definition?.orderIndex === 'number' ? definition.orderIndex : fallbackIndex return { id, customFieldId, @@ -2037,6 +2045,7 @@ const normalizeCustomFieldDefinitionEntry = (definition = {}) => { options, defaultValue, readOnly: !!definition?.readOnly, + orderIndex, } } diff --git a/app/pages/pieces/[id]/edit.vue b/app/pages/pieces/[id]/edit.vue index 3e0fd9b..ce2e891 100644 --- a/app/pages/pieces/[id]/edit.vue +++ b/app/pages/pieces/[id]/edit.vue @@ -383,6 +383,7 @@ interface CustomFieldInput { value: string customFieldId: string | null customFieldValueId: string | null + orderIndex: number } const route = useRoute() @@ -706,6 +707,7 @@ const buildCustomFieldInputs = ( ...definition, customFieldId: definition.customFieldId || definition.id, customFieldValueId: null, + orderIndex: definition.orderIndex, } } @@ -715,8 +717,14 @@ const buildCustomFieldInputs = ( customFieldId: matched.customField?.id || definition.customFieldId || definition.id, customFieldValueId: matched.id ?? null, value: formatDefaultValue(definition.type, resolvedValue), + orderIndex: Math.min( + definition.orderIndex ?? 0, + typeof matched.customField?.orderIndex === 'number' + ? matched.customField.orderIndex + : definition.orderIndex ?? 0, + ), } - }) + }).sort((a, b) => (a.orderIndex ?? 0) - (b.orderIndex ?? 0)) return resolved } @@ -730,11 +738,12 @@ const normalizeCustomFieldInputs = (structure: PieceModelStructure | null): Cust } const fields = Array.isArray(structure.customFields) ? structure.customFields : [] return fields - .map((field) => normalizeCustomField(field)) + .map((field, index) => normalizeCustomField(field, index)) .filter((field): field is CustomFieldInput => field !== null) + .sort((a, b) => a.orderIndex - b.orderIndex) } -const normalizeCustomField = (rawField: any): CustomFieldInput | null => { +const normalizeCustomField = (rawField: any, fallbackIndex = 0): CustomFieldInput | null => { if (!rawField || typeof rawField !== 'object') { return null } @@ -755,7 +764,9 @@ const normalizeCustomField = (rawField: any): CustomFieldInput | null => { ? rawField.customFieldValueId : null - return { id, name, type, required, options, value, customFieldId, customFieldValueId } + const orderIndex = typeof rawField.orderIndex === 'number' ? rawField.orderIndex : fallbackIndex + + return { id, name, type, required, options, value, customFieldId, customFieldValueId, orderIndex } } const resolveFieldName = (field: any): string => { diff --git a/app/pages/pieces/create.vue b/app/pages/pieces/create.vue index ec2437b..7952368 100644 --- a/app/pages/pieces/create.vue +++ b/app/pages/pieces/create.vue @@ -467,6 +467,7 @@ interface CustomFieldInput { value: string customFieldId: string | null customFieldValueId: string | null + orderIndex: number } const fieldKey = (field: CustomFieldInput, index: number) => @@ -478,11 +479,12 @@ const normalizeCustomFieldInputs = (structure: PieceModelStructure | null): Cust } const fields = Array.isArray(structure.customFields) ? structure.customFields : [] return fields - .map((field) => normalizeCustomField(field)) + .map((field, index) => normalizeCustomField(field, index)) .filter((field): field is CustomFieldInput => field !== null) + .sort((a, b) => a.orderIndex - b.orderIndex) } -const normalizeCustomField = (rawField: any): CustomFieldInput | null => { +const normalizeCustomField = (rawField: any, fallbackIndex = 0): CustomFieldInput | null => { if (!rawField || typeof rawField !== 'object') { return null } @@ -503,7 +505,9 @@ const normalizeCustomField = (rawField: any): CustomFieldInput | null => { ? rawField.customFieldValueId : null - return { id, name, type, required, options, value, customFieldId, customFieldValueId } + const orderIndex = typeof rawField.orderIndex === 'number' ? rawField.orderIndex : fallbackIndex + + return { id, name, type, required, options, value, customFieldId, customFieldValueId, orderIndex } } const resolveFieldName = (field: any): string => { diff --git a/app/pages/type/edit/[id].vue b/app/pages/type/edit/[id].vue index 15cc7f4..dcaba98 100644 --- a/app/pages/type/edit/[id].vue +++ b/app/pages/type/edit/[id].vue @@ -92,12 +92,15 @@ const parseOptions = (field = {}) => { const normalizeCustomFields = (fields = []) => fields .filter(field => field?.name && field.name.trim() !== '') - .map(field => ({ + .map((field, index) => ({ name: field.name, type: field.type || '', required: !!field.required, - options: parseOptions(field) + options: parseOptions(field), + orderIndex: typeof field.orderIndex === 'number' ? field.orderIndex : index })) + .sort((a, b) => (a.orderIndex ?? 0) - (b.orderIndex ?? 0)) + .map((field, index) => ({ ...field, orderIndex: index })) const toIntegerOrNull = (value, fallback = null) => { if (value === '' || value === undefined || value === null) { diff --git a/app/shared/modelUtils.ts b/app/shared/modelUtils.ts index 68d6025..fbe1522 100644 --- a/app/shared/modelUtils.ts +++ b/app/shared/modelUtils.ts @@ -94,7 +94,7 @@ const sanitizeCustomFields = (fields: any[]): ComponentModelCustomField[] => { } return fields - .map((field) => { + .map((field, index) => { const rawName = typeof field?.name === 'string' ? field.name @@ -173,6 +173,8 @@ const sanitizeCustomFields = (fields: any[]): ComponentModelCustomField[] => { if (customFieldId) { result.customFieldId = customFieldId } + const orderIndex = typeof field?.orderIndex === 'number' ? field.orderIndex : index + result.orderIndex = orderIndex return result }) .filter((field): field is ComponentModelCustomField => !!field) @@ -448,7 +450,7 @@ const hydrateCustomFields = (fields: any[]): any[] => { return [] } - return fields.map((field) => { + return fields.map((field, index) => { const valueObject = extractFieldValueObject(field) const name = typeof field?.name === 'string' ? field.name @@ -513,6 +515,7 @@ const hydrateCustomFields = (fields: any[]): any[] => { const id = typeof field?.id === 'string' ? field.id : undefined const customFieldId = typeof field?.customFieldId === 'string' ? field.customFieldId : undefined + const orderIndex = typeof field?.orderIndex === 'number' ? field.orderIndex : index return { name, @@ -523,6 +526,7 @@ const hydrateCustomFields = (fields: any[]): any[] => { defaultValue, id, customFieldId, + orderIndex, } }) } @@ -580,7 +584,7 @@ const mapComponentCustomFields = (fields: any[]) => { if (!Array.isArray(fields)) { return [] } - return hydrateCustomFields(fields).map((field) => { + return hydrateCustomFields(fields).map((field, index) => { const defaultValue = field?.defaultValue !== undefined && field?.defaultValue !== null && field?.defaultValue !== '' ? field.defaultValue @@ -597,6 +601,7 @@ const mapComponentCustomFields = (fields: any[]) => { typeof (field as any)?.customFieldId === 'string' ? (field as any).customFieldId : undefined, + orderIndex: typeof field?.orderIndex === 'number' ? field.orderIndex : index, } }) } @@ -772,7 +777,7 @@ const sanitizePieceCustomFields = (fields: any[]): PieceModelCustomField[] => { } return fields - .map((field) => { + .map((field, index) => { const name = typeof field?.name === 'string' ? field.name.trim() : '' if (!name) { return null @@ -799,6 +804,8 @@ const sanitizePieceCustomFields = (fields: any[]): PieceModelCustomField[] => { if (options) { result.options = options } + const orderIndex = typeof field?.orderIndex === 'number' ? field.orderIndex : index + result.orderIndex = orderIndex return result }) .filter((field): field is PieceModelCustomField => !!field) @@ -819,7 +826,7 @@ const hydratePieceCustomFields = (fields: any[]): PieceModelStructureEditorField return [] } - return fields.map((field) => ({ + return fields.map((field, index) => ({ name: field?.name ?? '', type: field?.type ?? 'text', required: !!field?.required, @@ -829,6 +836,7 @@ const hydratePieceCustomFields = (fields: any[]): PieceModelStructureEditorField : Array.isArray(field?.options) ? field.options.join('\n') : '', + orderIndex: typeof field?.orderIndex === 'number' ? field.orderIndex : index, })) } diff --git a/app/shared/types/inventory.ts b/app/shared/types/inventory.ts index c09546e..d842b5b 100644 --- a/app/shared/types/inventory.ts +++ b/app/shared/types/inventory.ts @@ -9,6 +9,7 @@ export interface ComponentModelCustomField { optionsText?: string id?: string customFieldId?: string + orderIndex?: number } export interface ComponentModelPiece { @@ -40,6 +41,7 @@ export interface PieceModelCustomField { type: PieceModelCustomFieldType required: boolean options?: string[] + orderIndex?: number } export interface PieceModelStructure {