fix champs personnalisé update

This commit is contained in:
Matthieu
2025-09-30 10:46:46 +02:00
parent c489f093ed
commit fd60cbbbfe
7 changed files with 2611 additions and 1196 deletions

View File

@@ -1,138 +1,18 @@
<template>
<div class="space-y-6">
<section class="space-y-3">
<div class="flex items-center justify-between">
<h3 class="text-sm font-semibold">Champs personnalisés du composant</h3>
<button type="button" class="btn btn-outline btn-xs" @click="addCustomField">
<IconLucidePlus class="w-3 h-3 mr-2" aria-hidden="true" />
Ajouter
</button>
</div>
<p v-if="!(localStructure.customFields?.length)" class="text-xs text-gray-500">
Aucun champ n'a encore été défini.
</p>
<div v-else class="space-y-2">
<div
v-for="(field, index) in localStructure.customFields"
:key="`root-field-${index}`"
class="border border-base-200 rounded-md p-3 space-y-2"
>
<div class="flex items-start justify-between gap-2">
<div class="flex-1 space-y-2">
<div class="grid grid-cols-1 md:grid-cols-2 gap-2">
<input
v-model="field.name"
type="text"
class="input input-bordered input-xs"
placeholder="Nom du champ"
/>
<select v-model="field.type" class="select select-bordered select-xs">
<option value="text">Texte</option>
<option value="number">Nombre</option>
<option value="select">Liste</option>
<option value="boolean">Oui/Non</option>
<option value="date">Date</option>
</select>
</div>
<div class="flex items-center gap-2 text-xs">
<input v-model="field.required" type="checkbox" class="checkbox checkbox-xs" />
Obligatoire
</div>
<textarea
v-if="field.type === 'select'"
v-model="field.optionsText"
class="textarea textarea-bordered textarea-xs h-20"
placeholder="Option 1&#10;Option 2"
></textarea>
</div>
<button type="button" class="btn btn-error btn-xs btn-square" @click="removeCustomField(index)">
<IconLucideTrash class="w-4 h-4" aria-hidden="true" />
</button>
</div>
</div>
</div>
</section>
<section class="space-y-3">
<div class="flex items-center justify-between">
<h3 class="text-sm font-semibold">Pièces incluses par défaut</h3>
<button type="button" class="btn btn-outline btn-xs" @click="addPiece">
<IconLucidePlus class="w-3 h-3 mr-2" aria-hidden="true" />
Ajouter
</button>
</div>
<p v-if="!(localStructure.pieces?.length)" class="text-xs text-gray-500">Aucune pièce définie.</p>
<div v-else class="space-y-2">
<div
v-for="(piece, index) in localStructure.pieces"
:key="`root-piece-${index}`"
class="border border-base-200 rounded-md p-3 space-y-3"
>
<div class="flex items-start justify-between gap-2">
<div class="flex-1 space-y-3">
<div class="form-control">
<label class="label"><span class="label-text">Famille de pièce</span></label>
<div>
<select
v-model="piece.typePieceId"
class="select select-bordered select-xs"
@change="handlePieceTypeSelect(piece)"
>
<option value="">
Sélectionner une famille
</option>
<option
v-for="type in availablePieceTypes"
:key="type.id"
:value="type.id"
>
{{ formatPieceTypeOption(type) }}
</option>
</select>
</div>
<p class="mt-1 text-[11px] text-gray-500">
{{ piece.typePieceId ? `Sélection : ${getPieceTypeLabel(piece.typePieceId) || 'Inconnue'}` : 'Aucune famille sélectionnée' }}
</p>
</div>
</div>
<button type="button" class="btn btn-error btn-xs btn-square" @click="removePiece(index)">
<IconLucideTrash class="w-4 h-4" aria-hidden="true" />
</button>
</div>
</div>
</div>
</section>
<section class="space-y-3">
<div class="flex items-center justify-between">
<h3 class="text-sm font-semibold">Sous-composants</h3>
<button type="button" class="btn btn-outline btn-xs" @click="addSubComponent">
<IconLucidePlus class="w-3 h-3 mr-2" aria-hidden="true" />
Ajouter
</button>
</div>
<p v-if="!(localStructure.subComponents?.length)" class="text-xs text-gray-500">
Aucun sous-composant défini.
</p>
<div v-else class="space-y-3">
<StructureSubComponentEditor
v-for="(subComponent, index) in localStructure.subComponents"
:key="`root-sub-${index}`"
:node="subComponent"
:depth="0"
:component-types="availableComponentTypes"
@remove="removeSubComponent(index)"
/>
</div>
</section>
<StructureNodeEditor
:node="localStructure"
:depth="0"
:component-types="availableComponentTypes"
:piece-types="availablePieceTypes"
is-root
/>
</div>
</template>
<script setup lang="ts">
import { reactive, watch, computed, onMounted } from 'vue'
import IconLucidePlus from '~icons/lucide/plus'
import IconLucideTrash from '~icons/lucide/trash'
import StructureSubComponentEditor from '~/components/StructureSubComponentEditor.vue'
import StructureNodeEditor from '~/components/StructureNodeEditor.vue'
import {
defaultStructure,
hydrateStructureForEditor,
@@ -185,201 +65,11 @@ watch(
{ deep: true }
)
type ModelTypeOption = {
id: string
name: string
code?: string | null
}
const formatModelTypeOption = (type: ModelTypeOption | undefined | null) => {
if (!type) return ''
return type.code ? `${type.name} (${type.code})` : type.name
}
const { pieceTypes, loadPieceTypes } = usePieceTypes()
const { componentTypes, loadComponentTypes } = useComponentTypes()
const availablePieceTypes = computed<ModelTypeOption[]>(() => pieceTypes.value ?? [])
const availableComponentTypes = computed<ModelTypeOption[]>(() => componentTypes.value ?? [])
const pieceTypeMap = computed(() => {
const map = new Map<string, ModelTypeOption>()
availablePieceTypes.value.forEach((type) => {
if (type && typeof type.id === 'string') {
map.set(type.id, type)
}
})
return map
})
const componentTypeMap = computed(() => {
const map = new Map<string, ModelTypeOption>()
availableComponentTypes.value.forEach((type) => {
if (type && typeof type.id === 'string') {
map.set(type.id, type)
}
})
return map
})
const formatPieceTypeOption = (type: ModelTypeOption | undefined | null) => formatModelTypeOption(type)
const formatComponentTypeOption = (type: ModelTypeOption | undefined | null) => formatModelTypeOption(type)
const resolvePieceType = (input: string) => {
const normalized = input.trim().toLowerCase()
if (!normalized) {
return null
}
return (
availablePieceTypes.value.find((type) => {
const formatted = formatPieceTypeOption(type).toLowerCase()
const name = (type?.name ?? '').toLowerCase()
const code = (type?.code ?? '').toLowerCase()
return (
formatted === normalized
|| name === normalized
|| (!!code && code === normalized)
)
}) ?? null
)
}
const resolveComponentType = (input: string) => {
const normalized = input.trim().toLowerCase()
if (!normalized) {
return null
}
return (
availableComponentTypes.value.find((type) => {
const formatted = formatComponentTypeOption(type).toLowerCase()
const name = (type?.name ?? '').toLowerCase()
const code = (type?.code ?? '').toLowerCase()
return (
formatted === normalized
|| name === normalized
|| (!!code && code === normalized)
)
}) ?? null
)
}
const getPieceTypeLabel = (id?: string) => {
if (!id) return ''
const option = pieceTypeMap.value.get(id)
return formatPieceTypeOption(option)
}
const updatePieceTypeLabel = (piece: any) => {
if (!piece) {
return
}
if (piece.typePieceId) {
const option = pieceTypeMap.value.get(piece.typePieceId)
if (option) {
piece.typePieceLabel = formatPieceTypeOption(option)
piece.name = option.name || formatPieceTypeOption(option)
} else if (!piece.typePieceLabel) {
piece.name = ''
}
} else if (piece.typePieceLabel) {
const match = resolvePieceType(piece.typePieceLabel)
if (match) {
piece.typePieceId = match.id
piece.typePieceLabel = formatPieceTypeOption(match)
piece.name = match.name || formatPieceTypeOption(match)
} else if (!piece.name) {
piece.name = piece.typePieceLabel
}
}
}
const handlePieceTypeSelect = (piece: any) => {
if (!piece) {
return
}
const id = typeof piece.typePieceId === 'string' ? piece.typePieceId : ''
if (!id) {
piece.typePieceId = ''
piece.typePieceLabel = ''
piece.name = ''
return
}
const option = pieceTypeMap.value.get(id)
if (!option) {
piece.typePieceId = ''
piece.typePieceLabel = ''
piece.name = ''
return
}
piece.typePieceLabel = formatPieceTypeOption(option)
piece.name = option.name || piece.typePieceLabel
}
const applyPieceLabels = (pieces?: any[]) => {
if (!Array.isArray(pieces)) {
return
}
pieces.forEach((piece) => {
if (piece?.typePieceId) {
updatePieceTypeLabel(piece)
} else if (piece?.typePieceLabel) {
const match = resolvePieceType(piece.typePieceLabel)
if (match) {
piece.typePieceId = match.id
piece.typePieceLabel = formatPieceTypeOption(match)
piece.name = match.name || formatPieceTypeOption(match)
} else if (!piece.name) {
piece.name = piece.typePieceLabel
}
} else if (!piece?.name) {
piece.name = ''
}
})
}
const applyComponentTypeLabel = (component: any) => {
if (!component) {
return
}
if (component.typeComposantId) {
const option = componentTypeMap.value.get(component.typeComposantId)
if (option) {
component.typeComposantLabel = formatComponentTypeOption(option)
component.name = option.name || formatComponentTypeOption(option)
} else if (!component.typeComposantLabel) {
component.name = ''
}
} else if (component.typeComposantLabel) {
const match = resolveComponentType(component.typeComposantLabel)
if (match) {
component.typeComposantId = match.id
component.typeComposantLabel = formatComponentTypeOption(match)
component.name = match.name || formatComponentTypeOption(match)
} else {
component.typeComposantLabel = ''
component.name = ''
}
}
}
const traverseSubComponents = (components?: any[]) => {
if (!Array.isArray(components)) {
return
}
components.forEach((component) => {
applyComponentTypeLabel(component)
applyPieceLabels(component?.pieces)
traverseSubComponents(component?.subComponents)
})
}
const syncAllTypeLabels = () => {
applyPieceLabels(localStructure.pieces)
traverseSubComponents(localStructure.subComponents)
}
const availablePieceTypes = computed(() => pieceTypes.value ?? [])
const availableComponentTypes = computed(() => componentTypes.value ?? [])
onMounted(async () => {
const loaders: Promise<any>[] = []
@@ -390,74 +80,7 @@ onMounted(async () => {
loaders.push(loadComponentTypes())
}
if (loaders.length) {
await Promise.all(loaders)
await Promise.allSettled(loaders)
}
syncAllTypeLabels()
})
watch(
() => availablePieceTypes.value,
() => {
syncAllTypeLabels()
},
{ deep: true }
)
watch(
() => availableComponentTypes.value,
() => {
syncAllTypeLabels()
},
{ deep: true }
)
const ensureArray = (key: 'customFields' | 'pieces' | 'subComponents') => {
if (!Array.isArray(localStructure[key])) {
localStructure[key] = []
}
}
const addCustomField = () => {
ensureArray('customFields')
localStructure.customFields.push({
name: '',
type: 'text',
required: false,
optionsText: '',
options: [],
})
}
const removeCustomField = (index: number) => {
if (!Array.isArray(localStructure.customFields)) return
localStructure.customFields.splice(index, 1)
}
const addPiece = () => {
ensureArray('pieces')
localStructure.pieces.push({
name: '',
typePieceId: '',
typePieceLabel: '',
})
}
const removePiece = (index: number) => {
if (!Array.isArray(localStructure.pieces)) return
localStructure.pieces.splice(index, 1)
}
const addSubComponent = () => {
ensureArray('subComponents')
localStructure.subComponents.push({
name: '',
typeComposantId: '',
typeComposantLabel: '',
})
}
const removeSubComponent = (index: number) => {
if (!Array.isArray(localStructure.subComponents)) return
localStructure.subComponents.splice(index, 1)
}
</script>