add sub componet in catego ske

This commit is contained in:
Matthieu
2025-10-16 16:48:36 +02:00
parent 761c5f559a
commit 42c788103a
11 changed files with 859 additions and 109 deletions

View File

@@ -8,6 +8,7 @@
:lock-type="lockRootType"
:locked-type-label="displayedRootTypeLabel"
:allow-subcomponents="allowSubcomponents"
:max-subcomponent-depth="maxSubcomponentDepth"
is-root
/>
</div>
@@ -48,6 +49,10 @@ const props = defineProps({
type: Boolean,
default: true,
},
maxSubcomponentDepth: {
type: Number,
default: Infinity,
},
})
const emit = defineEmits(['update:modelValue'])
@@ -61,6 +66,9 @@ const { componentTypes, loadComponentTypes } = useComponentTypes()
const availablePieceTypes = computed(() => pieceTypes.value ?? [])
const availableComponentTypes = computed(() => componentTypes.value ?? [])
const allowSubcomponents = computed(() => props.allowSubcomponents !== false)
const maxSubcomponentDepth = computed(() =>
typeof props.maxSubcomponentDepth === 'number' ? props.maxSubcomponentDepth : Infinity,
)
const fallbackRootTypeLabel = computed(() => {
if (!props.rootTypeId) {
@@ -72,6 +80,79 @@ const fallbackRootTypeLabel = computed(() => {
const displayedRootTypeLabel = computed(() => props.rootTypeLabel || fallbackRootTypeLabel.value)
const formatOptionsText = (field: Record<string, any>) => {
if (typeof field?.optionsText === 'string') {
return field.optionsText
}
if (Array.isArray(field?.options)) {
return field.options.join('\n')
}
return ''
}
const normalizeLineEndings = (text: string) =>
text.replace(/\r\n/g, '\n').replace(/\r/g, '\n')
const parseOptionsFromText = (text: string) => {
return text
.split('\n')
.map((option) => option.trim())
.filter((option) => option.length > 0)
}
const applyCustomFieldOptions = (node: Record<string, any> | null | undefined) => {
if (!node || typeof node !== 'object') {
return
}
if (Array.isArray(node.customFields)) {
node.customFields = node.customFields.map((field: any) => {
if (!field || typeof field !== 'object') {
return field
}
const next = { ...field }
if (next.type === 'select') {
const baseText = normalizeLineEndings(formatOptionsText(next))
if (next.optionsText !== baseText) {
next.optionsText = baseText
}
const parsedOptions = parseOptionsFromText(next.optionsText || '')
if (parsedOptions.length > 0) {
next.options = parsedOptions
} else {
delete next.options
}
} else {
if (next.options !== undefined) {
delete next.options
}
if (next.optionsText !== undefined && next.optionsText !== '') {
next.optionsText = ''
}
}
return next
})
}
if (Array.isArray(node.subcomponents)) {
node.subcomponents = node.subcomponents.map((sub: any) => {
if (!sub || typeof sub !== 'object') {
return sub
}
const copy = { ...sub }
applyCustomFieldOptions(copy)
return copy
})
}
}
const prepareStructureForEmit = (structure: any) => {
const clone = cloneStructure(structure)
applyCustomFieldOptions(clone as Record<string, any>)
return clone
}
const syncRootType = () => {
if (!props.lockRootType) {
previousLockedLabel.value = props.rootTypeLabel || ''
@@ -97,9 +178,14 @@ const syncRootType = () => {
previousLockedLabel.value = newLabel
}
let lastEmitted = JSON.stringify(cloneStructure(props.modelValue))
let lastEmitted = JSON.stringify(prepareStructureForEmit(props.modelValue))
const syncFromProps = (value: any) => {
const normalizedIncoming = prepareStructureForEmit(value)
const incomingSerialized = JSON.stringify(normalizedIncoming)
if (incomingSerialized === lastEmitted) {
return
}
const hydrated = hydrateStructureForEditor(value)
localStructure.customFields = hydrated.customFields
localStructure.pieces = hydrated.pieces
@@ -109,7 +195,7 @@ const syncFromProps = (value: any) => {
localStructure.modelId = hydrated.modelId
localStructure.familyCode = hydrated.familyCode
localStructure.alias = hydrated.alias
lastEmitted = JSON.stringify(cloneStructure(value))
lastEmitted = incomingSerialized
syncRootType()
}
@@ -139,7 +225,7 @@ watch(
watch(
localStructure,
(value) => {
const payload = cloneStructure(value)
const payload = prepareStructureForEmit(value)
const serialized = JSON.stringify(payload)
if (serialized !== lastEmitted) {
lastEmitted = serialized

View File

@@ -175,18 +175,23 @@
</div>
</section>
<section v-if="allowSubcomponents" class="space-y-3">
<section v-if="canManageSubcomponents || hasSubcomponents" class="space-y-3">
<div class="flex items-center justify-between gap-2">
<h4 :class="headingClass">Sous-composants</h4>
<button type="button" class="btn btn-outline btn-xs" @click="addSubComponent">
<button
v-if="canManageSubcomponents"
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="!isRoot" class="text-[11px] text-gray-500">
<p v-if="!isRoot && canManageSubcomponents" class="text-[11px] text-gray-500">
Sélectionnez uniquement la famille de ce sous-composant ; il sera configuré via son propre modèle.
</p>
<p v-if="!(node.subcomponents?.length)" class="text-xs text-gray-500">
<p v-if="!hasSubcomponents" class="text-xs text-gray-500">
Aucun sous-composant défini.
</p>
<div v-else class="space-y-3">
@@ -197,7 +202,8 @@
:depth="depth + 1"
:component-types="componentTypes"
:piece-types="pieceTypes"
:allow-subcomponents="allowSubcomponents"
:allow-subcomponents="childAllowSubcomponents"
:max-subcomponent-depth="maxSubcomponentDepth"
@remove="removeSubComponent(index)"
/>
</div>
@@ -235,6 +241,7 @@ const props = withDefaults(defineProps<{
lockType?: boolean
lockedTypeLabel?: string
allowSubcomponents?: boolean
maxSubcomponentDepth?: number
}>(), {
depth: 0,
componentTypes: () => [],
@@ -243,6 +250,7 @@ const props = withDefaults(defineProps<{
lockType: false,
lockedTypeLabel: '',
allowSubcomponents: true,
maxSubcomponentDepth: Infinity,
})
const emit = defineEmits(['remove'])
@@ -250,10 +258,23 @@ const emit = defineEmits(['remove'])
const componentTypes = computed(() => props.componentTypes ?? [])
const pieceTypes = computed(() => props.pieceTypes ?? [])
const allowSubcomponents = computed(() => props.allowSubcomponents !== false)
const maxSubcomponentDepth = computed(() =>
typeof props.maxSubcomponentDepth === 'number' ? props.maxSubcomponentDepth : Infinity,
)
const currentDepth = computed(() => Math.max(0, props.depth ?? 0))
const canManageSubcomponents = computed(
() => allowSubcomponents.value && currentDepth.value < maxSubcomponentDepth.value,
)
const childAllowSubcomponents = computed(
() => allowSubcomponents.value && currentDepth.value + 1 < maxSubcomponentDepth.value,
)
const hasSubcomponents = computed(
() => Array.isArray(props.node?.subcomponents) && props.node.subcomponents.length > 0,
)
const depthClasses = ['', 'ml-4', 'ml-8', 'ml-12', 'ml-16', 'ml-20']
const containerClass = computed(() => {
const level = Math.max(0, props.depth ?? 0)
const level = currentDepth.value
const index = Math.min(level, depthClasses.length - 1)
return level === 0 ? 'space-y-4' : `${depthClasses[index]} space-y-4`
})
@@ -279,6 +300,17 @@ const componentTypeMap = computed(() => {
return map
})
const componentTypeCodeMap = computed(() => {
const map = new Map<string, ModelTypeOption>()
componentTypes.value.forEach((type) => {
const code = typeof type?.code === 'string' ? type.code.trim() : ''
if (code) {
map.set(code, type)
}
})
return map
})
const pieceTypeMap = computed(() => {
const map = new Map<string, ModelTypeOption>()
pieceTypes.value.forEach((type) => {
@@ -346,6 +378,22 @@ const syncComponentType = (component: EditableStructureNode) => {
: ''
if (!id) {
const code =
typeof component.familyCode === 'string' && component.familyCode
? component.familyCode
: ''
if (code) {
const codeMatch = componentTypeCodeMap.value.get(code)
if (codeMatch?.id) {
component.typeComposantId = codeMatch.id
component.typeComposantLabel = formatModelTypeOption(codeMatch)
component.familyCode = codeMatch.code ?? component.familyCode
if (!component.alias || component.alias === '' || component.alias === lockedTypeDisplay.value) {
component.alias = codeMatch.name || component.typeComposantLabel
}
return
}
}
component.typeComposantLabel = ''
component.familyCode = ''
return
@@ -456,7 +504,7 @@ const removePiece = (index: number) => {
}
const addSubComponent = () => {
if (!allowSubcomponents.value) {
if (!canManageSubcomponents.value) {
return
}
ensureArray('subcomponents')
@@ -476,7 +524,7 @@ const removeSubComponent = (index: number) => {
}
watch(
allowSubcomponents,
canManageSubcomponents,
(allowed) => {
if (!allowed && Array.isArray(props.node.subcomponents) && props.node.subcomponents.length) {
props.node.subcomponents.splice(0, props.node.subcomponents.length)

View File

@@ -79,6 +79,7 @@
<ComponentModelStructureEditor
v-model="componentStructure"
:allow-subcomponents="allowComponentSubcomponents"
:max-subcomponent-depth="componentSubcomponentMaxDepth"
/>
</div>
@@ -119,6 +120,7 @@ import {
formatPieceStructurePreview,
formatStructurePreview,
normalizePieceStructureForSave,
normalizeStructureForEditor,
normalizeStructureForSave,
} from '~/shared/modelUtils'
import type { ModelCategory, ModelTypePayload } from '~/services/modelTypes'
@@ -131,12 +133,14 @@ const props = withDefaults(defineProps<{
lockCategory?: boolean
structureLoading?: boolean
allowComponentSubcomponents?: boolean
componentSubcomponentMaxDepth?: number
}>(), {
initialData: null,
saving: false,
lockCategory: false,
structureLoading: false,
allowComponentSubcomponents: true,
componentSubcomponentMaxDepth: 1,
})
const emit = defineEmits<{
@@ -148,6 +152,11 @@ const lockCategory = computed(() => props.lockCategory ?? false)
const structureLoading = computed(() => props.structureLoading ?? false)
const saving = computed(() => props.saving ?? false)
const allowComponentSubcomponents = computed(() => props.allowComponentSubcomponents !== false)
const componentSubcomponentMaxDepth = computed(() =>
typeof props.componentSubcomponentMaxDepth === 'number'
? props.componentSubcomponentMaxDepth
: 1,
)
const form = reactive<ModelTypePayload>({
name: '',
@@ -160,7 +169,7 @@ const form = reactive<ModelTypePayload>({
const errors = reactive<{ name?: string }>({})
const nameInput = ref<HTMLInputElement | null>(null)
const componentStructure = ref(normalizeStructureForSave(defaultStructure()))
const componentStructure = ref(normalizeStructureForEditor(defaultStructure()))
const pieceStructure = ref(normalizePieceStructureForSave(defaultPieceStructure()))
const generateCodeFromName = (name: string) => {
@@ -179,7 +188,7 @@ const generateCodeFromName = (name: string) => {
const resetStructures = (incomingStructure: ModelTypePayload['structure'], category: ModelCategory) => {
if (category === 'COMPONENT') {
componentStructure.value = normalizeStructureForSave(
componentStructure.value = normalizeStructureForEditor(
incomingStructure && props.initialData?.category === 'COMPONENT'
? incomingStructure
: defaultStructure(),
@@ -296,7 +305,7 @@ watch(
}
if (category === 'COMPONENT') {
componentStructure.value = normalizeStructureForSave(defaultStructure())
componentStructure.value = normalizeStructureForEditor(defaultStructure())
}
if (category === 'PIECE') {