feat: drag & drop des champs personnalisés
This commit is contained in:
@@ -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(() => {
|
||||
|
||||
@@ -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({})
|
||||
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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],
|
||||
() => {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user