set up new view for skeleton hiearchi
This commit is contained in:
242
app/components/ComponentModelStructureEditor.vue
Normal file
242
app/components/ComponentModelStructureEditor.vue
Normal file
@@ -0,0 +1,242 @@
|
||||
<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="grid grid-cols-1 md:grid-cols-2 gap-2">
|
||||
<label class="flex items-center gap-2 text-xs">
|
||||
<input v-model="field.required" type="checkbox" class="checkbox checkbox-xs" />
|
||||
Obligatoire
|
||||
</label>
|
||||
<input
|
||||
v-model="field.defaultValue"
|
||||
type="text"
|
||||
class="input input-bordered input-xs"
|
||||
placeholder="Valeur par défaut"
|
||||
/>
|
||||
</div>
|
||||
<textarea
|
||||
v-if="field.type === 'select'"
|
||||
v-model="field.optionsText"
|
||||
class="textarea textarea-bordered textarea-xs h-20"
|
||||
placeholder="Option 1 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"
|
||||
>
|
||||
<div class="flex items-start justify-between gap-2">
|
||||
<div class="flex-1 grid grid-cols-1 md:grid-cols-3 gap-2">
|
||||
<input
|
||||
v-model="piece.name"
|
||||
type="text"
|
||||
class="input input-bordered input-xs"
|
||||
placeholder="Nom de la pièce"
|
||||
/>
|
||||
<input
|
||||
v-model="piece.reference"
|
||||
type="text"
|
||||
class="input input-bordered input-xs"
|
||||
placeholder="Référence"
|
||||
/>
|
||||
<input
|
||||
v-model.number="piece.quantity"
|
||||
type="number"
|
||||
min="0"
|
||||
step="1"
|
||||
class="input input-bordered input-xs"
|
||||
placeholder="Quantité"
|
||||
/>
|
||||
</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"
|
||||
@remove="removeSubComponent(index)"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, watch } from 'vue'
|
||||
import IconLucidePlus from '~icons/lucide/plus'
|
||||
import IconLucideTrash from '~icons/lucide/trash'
|
||||
import StructureSubComponentEditor from '~/components/StructureSubComponentEditor.vue'
|
||||
import {
|
||||
defaultStructure,
|
||||
hydrateStructureForEditor,
|
||||
normalizeStructureForSave,
|
||||
} from '~/shared/modelUtils'
|
||||
|
||||
defineOptions({ name: 'ComponentModelStructureEditor' })
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Object,
|
||||
default: () => defaultStructure(),
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const localStructure = reactive(hydrateStructureForEditor(props.modelValue))
|
||||
|
||||
const syncFromProps = (value: any) => {
|
||||
const hydrated = hydrateStructureForEditor(value)
|
||||
localStructure.customFields = hydrated.customFields
|
||||
localStructure.pieces = hydrated.pieces
|
||||
localStructure.subComponents = hydrated.subComponents
|
||||
lastEmitted = JSON.stringify(normalizeStructureForSave(value))
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(value) => {
|
||||
syncFromProps(value)
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
let lastEmitted = JSON.stringify(normalizeStructureForSave(props.modelValue))
|
||||
|
||||
watch(
|
||||
localStructure,
|
||||
(value) => {
|
||||
const normalized = normalizeStructureForSave(value)
|
||||
const serialized = JSON.stringify(normalized)
|
||||
if (serialized !== lastEmitted) {
|
||||
lastEmitted = serialized
|
||||
emit('update:modelValue', normalized)
|
||||
}
|
||||
},
|
||||
{ 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,
|
||||
defaultValue: '',
|
||||
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: '',
|
||||
reference: '',
|
||||
quantity: undefined,
|
||||
})
|
||||
}
|
||||
|
||||
const removePiece = (index: number) => {
|
||||
if (!Array.isArray(localStructure.pieces)) return
|
||||
localStructure.pieces.splice(index, 1)
|
||||
}
|
||||
|
||||
const addSubComponent = () => {
|
||||
ensureArray('subComponents')
|
||||
localStructure.subComponents.push({
|
||||
name: '',
|
||||
description: '',
|
||||
quantity: undefined,
|
||||
customFields: [],
|
||||
pieces: [],
|
||||
subComponents: [],
|
||||
})
|
||||
}
|
||||
|
||||
const removeSubComponent = (index: number) => {
|
||||
if (!Array.isArray(localStructure.subComponents)) return
|
||||
localStructure.subComponents.splice(index, 1)
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user