feat: drag & drop des champs personnalisés

This commit is contained in:
Matthieu
2025-10-28 18:08:14 +01:00
parent b752fba69a
commit 4c714b3647
16 changed files with 458 additions and 67 deletions

View File

@@ -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(() => {

View File

@@ -5,7 +5,7 @@
</h4>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div
v-for="field in customFields"
v-for="field in sortedCustomFields"
:key="field.id"
class="form-control"
>
@@ -81,7 +81,7 @@
</template>
<script setup>
import { ref, reactive, onMounted, watch } from 'vue'
import { ref, reactive, onMounted, watch, computed } from 'vue'
const props = defineProps({
customFields: {
@@ -101,6 +101,17 @@ const props = defineProps({
const emit = defineEmits(['update'])
const sortedCustomFields = computed(() => {
if (!Array.isArray(props.customFields)) {
return []
}
return [...props.customFields].sort((a, b) => {
const left = typeof a?.orderIndex === 'number' ? a.orderIndex : 0
const right = typeof b?.orderIndex === 'number' ? b.orderIndex : 0
return left - right
})
})
// Valeurs des champs personnalisés
const fieldValues = reactive({})

View File

@@ -456,16 +456,36 @@ const extractStructureCustomFields = (structure) => {
return Array.isArray(customFields) ? customFields : [];
};
function fieldKeyFromNameAndType(name, type) {
const normalizedName = typeof name === 'string' ? name : '';
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;
}
@@ -482,6 +502,7 @@ function deduplicateFieldDefinitions(definitions) {
if (key) {
seen.add(key);
}
field.orderIndex = resolveOrderIndex(field);
result.push(field);
});
@@ -529,9 +550,15 @@ function mergeFieldDefinitionsWithValues(definitions, values) {
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,
@@ -542,6 +569,7 @@ function mergeFieldDefinitionsWithValues(definitions, values) {
null,
customField: matchedValue.customField ?? field.customField ?? null,
value: matchedValue.value ?? field.value ?? '',
orderIndex: resolvedOrder,
};
});
@@ -588,22 +616,31 @@ function mergeFieldDefinitionsWithValues(definitions, values) {
options: entry.customField?.options ?? [],
value: entry.value ?? '',
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;
}
@@ -632,12 +669,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;
@@ -654,11 +693,17 @@ 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 pieceDefinitionSources = computed(() => {

View File

@@ -108,7 +108,7 @@ const extractRest = (structure = {}) => {
)
}
const toEditorField = (input = {}) => ({
const toEditorField = (input = {}, index = 0) => ({
name: typeof input.name === 'string' ? input.name : '',
type: typeof input.type === 'string' && input.type ? input.type : 'text',
required: Boolean(input.required),
@@ -116,10 +116,14 @@ const toEditorField = (input = {}) => ({
? input.options.join('\n')
: typeof input.optionsText === 'string'
? input.optionsText
: ''
: '',
orderIndex: typeof input.orderIndex === 'number' ? input.orderIndex : index,
})
const hydrateFields = (structure = {}) => ensureArray(structure.customFields).map(toEditorField)
const hydrateFields = (structure = {}) =>
ensureArray(structure.customFields)
.map((field, index) => toEditorField(field, index))
.sort((a, b) => (a.orderIndex ?? 0) - (b.orderIndex ?? 0))
const localState = reactive({
fields: hydrateFields(props.modelValue)
@@ -138,7 +142,7 @@ const localFields = computed({
const normalizeFields = (fields) => {
return ensureArray(fields)
.map((field) => {
.map((field, index) => {
const name = typeof field.name === 'string' ? field.name.trim() : ''
if (!name) {
return null
@@ -156,7 +160,7 @@ const normalizeFields = (fields) => {
options = parsed.length > 0 ? parsed : undefined
}
const normalized = { name, type, required }
const normalized = { name, type, required, orderIndex: index }
if (options) {
normalized.options = options
}
@@ -199,7 +203,8 @@ watch(
watch(localFields, emitUpdate, { deep: true })
const addField = () => {
localFields.value = [...localFields.value, toEditorField()]
const index = localFields.value.length
localFields.value = [...localFields.value, toEditorField({}, index)]
}
const removeField = (index) => {

View File

@@ -79,9 +79,27 @@
<div
v-for="(field, index) in node.customFields"
:key="`field-${index}`"
class="border border-base-200 rounded-md p-3 space-y-2"
class="border border-base-200 rounded-md p-3 space-y-2 transition-colors"
:class="customFieldReorderClass(index)"
draggable="true"
@dragstart="onCustomFieldDragStart(index, $event)"
@dragenter="onCustomFieldDragEnter(index)"
@dragover.prevent
@drop="onCustomFieldDrop(index)"
@dragend="onCustomFieldDragEnd"
>
<div class="flex items-start justify-between gap-2">
<div class="flex items-center gap-2">
<button
type="button"
class="btn btn-ghost btn-xs btn-square cursor-grab active:cursor-grabbing"
title="Réorganiser"
draggable="true"
@dragstart.stop="onCustomFieldDragStart(index, $event)"
>
<IconLucideGripVertical class="w-4 h-4" aria-hidden="true" />
</button>
</div>
<div class="flex-1 space-y-2">
<div class="grid grid-cols-1 md:grid-cols-2 gap-2">
<input
@@ -506,20 +524,97 @@ const handlePieceTypeSelect = (piece: ComponentModelPiece & Record<string, any>)
piece.typePieceLabel = formatPieceTypeOption(option)
}
const customFieldDragState = ref({
draggingIndex: null as number | null,
dropTargetIndex: null as number | null,
})
const reindexCustomFields = () => {
if (!Array.isArray(props.node.customFields)) {
return
}
props.node.customFields.forEach((field: any, index: number) => {
if (!field || typeof field !== 'object') {
return
}
field.orderIndex = index
})
}
const resetCustomFieldDragState = () => {
customFieldDragState.value.draggingIndex = null
customFieldDragState.value.dropTargetIndex = null
}
const onCustomFieldDragStart = (index: number, event: DragEvent) => {
customFieldDragState.value.draggingIndex = index
customFieldDragState.value.dropTargetIndex = index
if (event.dataTransfer) {
event.dataTransfer.effectAllowed = 'move'
}
}
const onCustomFieldDragEnter = (index: number) => {
if (customFieldDragState.value.draggingIndex === null) {
return
}
customFieldDragState.value.dropTargetIndex = index
}
const onCustomFieldDrop = (index: number) => {
if (!Array.isArray(props.node.customFields)) {
resetCustomFieldDragState()
return
}
const from = customFieldDragState.value.draggingIndex
const to = index
if (from === null || to === null) {
resetCustomFieldDragState()
return
}
moveItemInPlace(props.node.customFields, from, to)
reindexCustomFields()
resetCustomFieldDragState()
}
const onCustomFieldDragEnd = () => {
resetCustomFieldDragState()
}
const customFieldReorderClass = (index: number) => {
if (customFieldDragState.value.draggingIndex === index) {
return 'border-dashed border-primary'
}
if (
customFieldDragState.value.draggingIndex !== null &&
customFieldDragState.value.dropTargetIndex === index &&
customFieldDragState.value.draggingIndex !== index
) {
return 'border-primary border-dashed bg-primary/5'
}
return ''
}
const addCustomField = () => {
ensureArray('customFields')
const nextIndex = Array.isArray(props.node.customFields)
? props.node.customFields.length
: 0
props.node.customFields.push({
name: '',
type: 'text',
required: false,
optionsText: '',
options: [],
orderIndex: nextIndex,
})
reindexCustomFields()
}
const removeCustomField = (index: number) => {
if (!Array.isArray(props.node.customFields)) return
props.node.customFields.splice(index, 1)
reindexCustomFields()
}
const addPiece = () => {
@@ -723,6 +818,22 @@ watch(
{ deep: true }
)
watch(
() => props.node.customFields,
(value) => {
if (!Array.isArray(value)) {
return
}
value.sort((a: any, b: any) => {
const left = typeof a?.orderIndex === 'number' ? a.orderIndex : 0
const right = typeof b?.orderIndex === 'number' ? b.orderIndex : 0
return left - right
})
reindexCustomFields()
},
{ deep: true }
)
watch(
() => [props.lockedTypeLabel, props.lockType],
() => {

View File

@@ -24,11 +24,26 @@
<div v-if="expanded" class="space-y-4">
<div
v-for="(field, fieldIndex) in fields"
:key="fieldIndex"
class="border border-gray-200 rounded-lg p-4 bg-gray-50"
:key="field.id || field.customFieldId || field.__key || `field-${fieldIndex}`"
class="border border-gray-200 rounded-lg p-4 bg-gray-50 transition-colors"
:class="fieldReorderClass(fieldIndex)"
draggable="true"
@dragstart="onFieldDragStart(fieldIndex, $event)"
@dragenter="onFieldDragEnter(fieldIndex)"
@dragover.prevent
@drop="onFieldDrop(fieldIndex)"
@dragend="onFieldDragEnd"
>
<div class="flex items-center justify-between mb-3">
<div class="flex items-center gap-2">
<button
type="button"
class="btn btn-ghost btn-xs btn-square cursor-grab active:cursor-grabbing"
title="Réorganiser"
draggable="false"
>
<IconLucideGripVertical class="w-4 h-4" aria-hidden="true" />
</button>
<button
type="button"
class="btn btn-ghost btn-xs p-1"
@@ -160,6 +175,7 @@ import IconLucideChevronRight from '~icons/lucide/chevron-right'
import IconLucideListChecks from '~icons/lucide/list-checks'
import IconLucideX from '~icons/lucide/x'
import IconLucidePlus from '~icons/lucide/plus'
import IconLucideGripVertical from '~icons/lucide/grip-vertical'
const props = defineProps({
modelValue: {
@@ -183,8 +199,57 @@ const fields = computed({
set: value => emit('update:modelValue', value)
})
const createFieldKey = () => `cf-${Math.random().toString(16).slice(2)}-${Date.now()}`
const expanded = ref(false)
const expandedFields = ref([])
const draggingFieldIndex = ref(null)
const fieldDropTargetIndex = ref(null)
const applyOrderIndex = (list = []) => {
if (!Array.isArray(list)) { return [] }
list.forEach((field, index) => {
if (field && typeof field === 'object') {
field.orderIndex = index
if (typeof field.__key !== 'string' || !field.__key) {
field.__key = createFieldKey()
}
}
})
return list
}
const createEmptyField = () => ({
name: '',
type: '',
required: false,
optionsText: '',
orderIndex: fields.value.length,
__key: createFieldKey()
})
const resetDragState = () => {
draggingFieldIndex.value = null
fieldDropTargetIndex.value = null
}
const reorderFields = (from, to) => {
const list = Array.isArray(fields.value) ? fields.value.slice() : []
if (from === to || from < 0 || to < 0 || from >= list.length || to >= list.length) {
resetDragState()
return
}
const [moved] = list.splice(from, 1)
list.splice(to, 0, moved)
if (Array.isArray(expandedFields.value)) {
const expandedCopy = expandedFields.value.slice()
const [expandedState] = expandedCopy.splice(from, 1)
expandedCopy.splice(to, 0, expandedState)
expandedFields.value = expandedCopy
}
fields.value = applyOrderIndex(list)
resetDragState()
}
watch(
() => props.expandAllTrigger,
@@ -223,26 +288,25 @@ const toggleField = (index) => {
}
const addField = () => {
fields.value = [
...fields.value,
{
name: '',
type: '',
required: false,
optionsText: ''
}
]
const next = Array.isArray(fields.value) ? fields.value.slice() : []
next.push(createEmptyField())
fields.value = applyOrderIndex(next)
expandedFields.value.push(true)
expanded.value = true
}
const removeField = (index) => {
fields.value = fields.value.filter((_, i) => i !== index)
const next = Array.isArray(fields.value)
? fields.value.filter((_, i) => i !== index)
: []
fields.value = applyOrderIndex(next)
expandedFields.value.splice(index, 1)
}
const updateField = (index, patch) => {
fields.value = fields.value.map((field, i) => (i === index ? { ...field, ...patch } : field))
const next = Array.isArray(fields.value) ? fields.value.slice() : []
next[index] = { ...next[index], ...patch }
fields.value = applyOrderIndex(next)
}
const updateOptions = (index, value) => {
@@ -250,4 +314,43 @@ const updateOptions = (index, value) => {
optionsText: value.replace(/\r\n/g, '\n').replace(/\r/g, '\n')
})
}
const onFieldDragStart = (index, event) => {
draggingFieldIndex.value = index
fieldDropTargetIndex.value = index
if (event.dataTransfer) {
event.dataTransfer.effectAllowed = 'move'
}
}
const onFieldDragEnter = (index) => {
if (draggingFieldIndex.value === null) { return }
fieldDropTargetIndex.value = index
}
const onFieldDrop = (index) => {
if (draggingFieldIndex.value === null) {
resetDragState()
return
}
reorderFields(draggingFieldIndex.value, index)
}
const onFieldDragEnd = () => {
resetDragState()
}
const fieldReorderClass = (index) => {
if (draggingFieldIndex.value === index) {
return 'border-dashed border-primary'
}
if (
draggingFieldIndex.value !== null &&
fieldDropTargetIndex.value === index &&
draggingFieldIndex.value !== index
) {
return 'border-primary border-dashed bg-primary/5'
}
return ''
}
</script>

View File

@@ -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))

View File

@@ -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 => {

View File

@@ -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 => {

View File

@@ -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) {

View File

@@ -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,
}
}

View File

@@ -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 => {

View File

@@ -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 => {

View File

@@ -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) {

View File

@@ -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,
}))
}

View File

@@ -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 {