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" :lock-type="lockRootType"
:locked-type-label="displayedRootTypeLabel" :locked-type-label="displayedRootTypeLabel"
:allow-subcomponents="allowSubcomponents" :allow-subcomponents="allowSubcomponents"
:max-subcomponent-depth="maxSubcomponentDepth"
is-root is-root
/> />
</div> </div>
@@ -48,6 +49,10 @@ const props = defineProps({
type: Boolean, type: Boolean,
default: true, default: true,
}, },
maxSubcomponentDepth: {
type: Number,
default: Infinity,
},
}) })
const emit = defineEmits(['update:modelValue']) const emit = defineEmits(['update:modelValue'])
@@ -61,6 +66,9 @@ const { componentTypes, loadComponentTypes } = useComponentTypes()
const availablePieceTypes = computed(() => pieceTypes.value ?? []) const availablePieceTypes = computed(() => pieceTypes.value ?? [])
const availableComponentTypes = computed(() => componentTypes.value ?? []) const availableComponentTypes = computed(() => componentTypes.value ?? [])
const allowSubcomponents = computed(() => props.allowSubcomponents !== false) const allowSubcomponents = computed(() => props.allowSubcomponents !== false)
const maxSubcomponentDepth = computed(() =>
typeof props.maxSubcomponentDepth === 'number' ? props.maxSubcomponentDepth : Infinity,
)
const fallbackRootTypeLabel = computed(() => { const fallbackRootTypeLabel = computed(() => {
if (!props.rootTypeId) { if (!props.rootTypeId) {
@@ -72,6 +80,79 @@ const fallbackRootTypeLabel = computed(() => {
const displayedRootTypeLabel = computed(() => props.rootTypeLabel || fallbackRootTypeLabel.value) 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 = () => { const syncRootType = () => {
if (!props.lockRootType) { if (!props.lockRootType) {
previousLockedLabel.value = props.rootTypeLabel || '' previousLockedLabel.value = props.rootTypeLabel || ''
@@ -97,9 +178,14 @@ const syncRootType = () => {
previousLockedLabel.value = newLabel previousLockedLabel.value = newLabel
} }
let lastEmitted = JSON.stringify(cloneStructure(props.modelValue)) let lastEmitted = JSON.stringify(prepareStructureForEmit(props.modelValue))
const syncFromProps = (value: any) => { const syncFromProps = (value: any) => {
const normalizedIncoming = prepareStructureForEmit(value)
const incomingSerialized = JSON.stringify(normalizedIncoming)
if (incomingSerialized === lastEmitted) {
return
}
const hydrated = hydrateStructureForEditor(value) const hydrated = hydrateStructureForEditor(value)
localStructure.customFields = hydrated.customFields localStructure.customFields = hydrated.customFields
localStructure.pieces = hydrated.pieces localStructure.pieces = hydrated.pieces
@@ -109,7 +195,7 @@ const syncFromProps = (value: any) => {
localStructure.modelId = hydrated.modelId localStructure.modelId = hydrated.modelId
localStructure.familyCode = hydrated.familyCode localStructure.familyCode = hydrated.familyCode
localStructure.alias = hydrated.alias localStructure.alias = hydrated.alias
lastEmitted = JSON.stringify(cloneStructure(value)) lastEmitted = incomingSerialized
syncRootType() syncRootType()
} }
@@ -139,7 +225,7 @@ watch(
watch( watch(
localStructure, localStructure,
(value) => { (value) => {
const payload = cloneStructure(value) const payload = prepareStructureForEmit(value)
const serialized = JSON.stringify(payload) const serialized = JSON.stringify(payload)
if (serialized !== lastEmitted) { if (serialized !== lastEmitted) {
lastEmitted = serialized lastEmitted = serialized

View File

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

View File

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

View File

@@ -25,7 +25,6 @@
initial-category="COMPONENT" initial-category="COMPONENT"
:initial-data="initialData" :initial-data="initialData"
:lock-category="true" :lock-category="true"
:allow-component-subcomponents="false"
:saving="saving" :saving="saving"
@submit="handleSubmit" @submit="handleSubmit"
@cancel="handleCancel" @cancel="handleCancel"

View File

@@ -19,7 +19,6 @@
mode="create" mode="create"
initial-category="COMPONENT" initial-category="COMPONENT"
:lock-category="true" :lock-category="true"
:allow-component-subcomponents="false"
:saving="saving" :saving="saving"
@submit="handleSubmit" @submit="handleSubmit"
@cancel="handleCancel" @cancel="handleCancel"

View File

@@ -131,32 +131,43 @@
{{ selectedType.description || 'Ce squelette définit la structure et les contraintes du composant.' }} {{ selectedType.description || 'Ce squelette définit la structure et les contraintes du composant.' }}
</p> </p>
</div> </div>
<span class="badge badge-outline">{{ formatStructurePreview(selectedType.structure) }}</span> <span class="badge badge-outline">{{ formatStructurePreview(selectedTypeStructure) }}</span>
</div> </div>
<details v-if="selectedType.structure" class="collapse collapse-arrow bg-base-100"> <details v-if="selectedTypeStructure" class="collapse collapse-arrow bg-base-100">
<summary class="collapse-title text-sm font-medium"> <summary class="collapse-title text-sm font-medium">
Consulter le détail du squelette Consulter le détail du squelette
</summary> </summary>
<div class="collapse-content space-y-4 text-sm text-base-content/80"> <div class="collapse-content space-y-4 text-sm text-base-content/80">
<div v-if="getStructureCustomFields(selectedType.structure).length" class="space-y-2"> <div v-if="getStructureCustomFields(selectedTypeStructure).length" class="space-y-2">
<h3 class="font-semibold text-sm text-base-content">Champs personnalisés</h3> <h3 class="font-semibold text-sm text-base-content">Champs personnalisés</h3>
<ul class="list-disc list-inside space-y-1"> <ul class="space-y-2">
<li <li
v-for="field in getStructureCustomFields(selectedType.structure)" v-for="field in getStructureCustomFields(selectedTypeStructure)"
:key="field.key || field.name" :key="field.customFieldId || field.id || field.name"
class="rounded bg-base-200/60 px-3 py-2"
> >
<span class="font-medium">{{ field.key || field.name }}</span> <p class="font-medium text-sm text-base-content">
<span v-if="field.value !== undefined && field.value !== null"> : {{ field.value }}</span> {{ field.name || field.key }}
</p>
<p class="text-xs text-base-content/70 mt-1">
Type : {{ field.type || 'text' }}<span v-if="field.required"> Obligatoire</span>
<span v-if="Array.isArray(field.options) && field.options.length">
Options : {{ field.options.join(', ') }}
</span>
<span v-if="field.defaultValue">
Défaut : {{ field.defaultValue }}
</span>
</p>
</li> </li>
</ul> </ul>
</div> </div>
<div v-if="getStructurePieces(selectedType.structure).length" class="space-y-2"> <div v-if="getStructurePieces(selectedTypeStructure).length" class="space-y-2">
<h3 class="font-semibold text-sm text-base-content">Pièces imposées</h3> <h3 class="font-semibold text-sm text-base-content">Pièces imposées</h3>
<ul class="list-disc list-inside space-y-1"> <ul class="list-disc list-inside space-y-1">
<li <li
v-for="(piece, index) in getStructurePieces(selectedType.structure)" v-for="(piece, index) in getStructurePieces(selectedTypeStructure)"
:key="piece.role || piece.typePieceId || piece.familyCode || index" :key="piece.role || piece.typePieceId || piece.familyCode || index"
> >
{{ resolvePieceLabel(piece) }} {{ resolvePieceLabel(piece) }}
@@ -164,11 +175,11 @@
</ul> </ul>
</div> </div>
<div v-if="getStructureSubcomponents(selectedType.structure).length" class="space-y-2"> <div v-if="getStructureSubcomponents(selectedTypeStructure).length" class="space-y-2">
<h3 class="font-semibold text-sm text-base-content">Sous-composants</h3> <h3 class="font-semibold text-sm text-base-content">Sous-composants</h3>
<ul class="list-disc list-inside space-y-1"> <ul class="list-disc list-inside space-y-1">
<li <li
v-for="(subcomponent, index) in getStructureSubcomponents(selectedType.structure)" v-for="(subcomponent, index) in getStructureSubcomponents(selectedTypeStructure)"
:key="subcomponent.alias || subcomponent.typeComposantId || subcomponent.familyCode || index" :key="subcomponent.alias || subcomponent.typeComposantId || subcomponent.familyCode || index"
> >
{{ resolveSubcomponentLabel(subcomponent) }} {{ resolveSubcomponentLabel(subcomponent) }}
@@ -177,7 +188,7 @@
</div> </div>
<p <p
v-if="!getStructureCustomFields(selectedType.structure).length && !getStructurePieces(selectedType.structure).length && !getStructureSubcomponents(selectedType.structure).length" v-if="!getStructureCustomFields(selectedTypeStructure).length && !getStructurePieces(selectedTypeStructure).length && !getStructureSubcomponents(selectedTypeStructure).length"
class="text-xs text-gray-500" class="text-xs text-gray-500"
> >
Ce squelette ne définit pas encore de pièces, sous-composants ou valeurs par défaut. Ce squelette ne définit pas encore de pièces, sous-composants ou valeurs par défaut.
@@ -383,7 +394,7 @@ import { useCustomFields } from '~/composables/useCustomFields'
import { useApi } from '~/composables/useApi' import { useApi } from '~/composables/useApi'
import { useToast } from '~/composables/useToast' import { useToast } from '~/composables/useToast'
import { useDocuments } from '~/composables/useDocuments' import { useDocuments } from '~/composables/useDocuments'
import { formatStructurePreview } from '~/shared/modelUtils' import { formatStructurePreview, normalizeStructureForEditor } from '~/shared/modelUtils'
import type { ComponentModelStructure } from '~/shared/types/inventory' import type { ComponentModelStructure } from '~/shared/types/inventory'
import type { ModelType } from '~/services/modelTypes' import type { ModelType } from '~/services/modelTypes'
import { getFileIcon } from '~/utils/fileIcons' import { getFileIcon } from '~/utils/fileIcons'
@@ -530,6 +541,11 @@ const selectedType = computed(() => {
return componentTypeList.value.find((type) => type.id === selectedTypeId.value) ?? null return componentTypeList.value.find((type) => type.id === selectedTypeId.value) ?? null
}) })
const selectedTypeStructure = computed<ComponentModelStructure | null>(() => {
const structure = selectedType.value?.structure ?? null
return structure ? normalizeStructureForEditor(structure) : null
})
const requiredCustomFieldsFilled = computed(() => const requiredCustomFieldsFilled = computed(() =>
customFieldInputs.value.every((field) => { customFieldInputs.value.every((field) => {
if (!field.required) { if (!field.required) {
@@ -582,8 +598,8 @@ const fetchComponent = async () => {
let initialized = false let initialized = false
watch( watch(
[component, selectedType], [component, selectedTypeStructure],
([currentComponent, currentType]) => { ([currentComponent, currentStructure]) => {
if (!currentComponent || initialized) { if (!currentComponent || initialized) {
return return
} }
@@ -596,7 +612,7 @@ watch(
editionForm.prix = currentComponent.prix !== null && currentComponent.prix !== undefined ? String(currentComponent.prix) : '' editionForm.prix = currentComponent.prix !== null && currentComponent.prix !== undefined ? String(currentComponent.prix) : ''
customFieldInputs.value = buildCustomFieldInputs( customFieldInputs.value = buildCustomFieldInputs(
currentType?.structure ?? null, currentStructure,
currentComponent.customFieldValues, currentComponent.customFieldValues,
) )
@@ -605,12 +621,12 @@ watch(
{ immediate: true }, { immediate: true },
) )
watch(selectedType, (currentType) => { watch(selectedTypeStructure, (currentStructure) => {
if (!component.value || !currentType) { if (!component.value) {
return return
} }
customFieldInputs.value = buildCustomFieldInputs( customFieldInputs.value = buildCustomFieldInputs(
currentType.structure, currentStructure,
component.value.customFieldValues, component.value.customFieldValues,
) )
}) })
@@ -662,7 +678,8 @@ const buildCustomFieldInputs = (
structure: ComponentModelStructure | null, structure: ComponentModelStructure | null,
values: any[] | null, values: any[] | null,
): CustomFieldInput[] => { ): CustomFieldInput[] => {
const definitions = normalizeCustomFieldInputs(structure) const normalizedStructure = structure ? normalizeStructureForEditor(structure) : null
const definitions = normalizeCustomFieldInputs(normalizedStructure)
const valueList = Array.isArray(values) ? values : [] const valueList = Array.isArray(values) ? values : []
const mapById = new Map<string, any>() const mapById = new Map<string, any>()
@@ -694,7 +711,7 @@ const buildCustomFieldInputs = (
} }
} }
const resolvedValue = matched.value ?? '' const resolvedValue = extractStoredCustomFieldValue(matched)
return { return {
...definition, ...definition,
customFieldId: matched.customField?.id || definition.customFieldId || definition.id, customFieldId: matched.customField?.id || definition.customFieldId || definition.id,
@@ -728,11 +745,10 @@ const normalizeCustomField = (rawField: any): CustomFieldInput | null => {
return null return null
} }
const type = resolveFieldType(rawField) const type = resolveFieldType(rawField)
const required = !!rawField.required const required = resolveRequiredFlag(rawField)
const options = Array.isArray(rawField.options) const options = resolveOptions(rawField)
? rawField.options.filter((option: unknown): option is string => typeof option === 'string') const defaultSource = resolveDefaultValue(rawField)
: [] const value = formatDefaultValue(type, defaultSource)
const value = formatDefaultValue(type, rawField.value)
const id = typeof rawField.id === 'string' ? rawField.id : null const id = typeof rawField.id === 'string' ? rawField.id : null
const customFieldId = typeof rawField.customFieldId === 'string' ? rawField.customFieldId : id const customFieldId = typeof rawField.customFieldId === 'string' ? rawField.customFieldId : id
const customFieldValueId = typeof rawField.customFieldValueId === 'string' const customFieldValueId = typeof rawField.customFieldValueId === 'string'
@@ -756,14 +772,56 @@ const resolveFieldName = (field: any): string => {
const resolveFieldType = (field: any): string => { const resolveFieldType = (field: any): string => {
const allowed = ['text', 'number', 'select', 'boolean', 'date'] const allowed = ['text', 'number', 'select', 'boolean', 'date']
const value = typeof field?.type === 'string' ? field.type.toLowerCase() : '' const rawType =
typeof field?.type === 'string'
? field.type
: typeof field?.value?.type === 'string'
? field.value.type
: ''
const value = rawType.toLowerCase()
return allowed.includes(value) ? value : 'text' return allowed.includes(value) ? value : 'text'
} }
const resolveDefaultValue = (field: any): any => {
if (!field || typeof field !== 'object') {
return null
}
if (field.defaultValue !== undefined && field.defaultValue !== null) {
return field.defaultValue
}
if (field.value !== undefined && field.value !== null && typeof field.value !== 'object') {
return field.value
}
if (field.default !== undefined && field.default !== null) {
return field.default
}
if (field.value && typeof field.value === 'object') {
if ((field.value as any).defaultValue !== undefined && (field.value as any).defaultValue !== null) {
return (field.value as any).defaultValue
}
if ((field.value as any).value !== undefined && (field.value as any).value !== null && typeof (field.value as any).value !== 'object') {
return (field.value as any).value
}
}
return null
}
const formatDefaultValue = (type: string, defaultValue: any): string => { const formatDefaultValue = (type: string, defaultValue: any): string => {
if (defaultValue === null || defaultValue === undefined) { if (defaultValue === null || defaultValue === undefined) {
return '' return ''
} }
if (typeof defaultValue === 'object') {
if (defaultValue === null) {
return ''
}
if ('defaultValue' in (defaultValue as Record<string, any>)) {
return formatDefaultValue(type, (defaultValue as Record<string, any>).defaultValue)
}
if ('value' in (defaultValue as Record<string, any>)) {
return formatDefaultValue(type, (defaultValue as Record<string, any>).value)
}
return ''
}
if (type === 'boolean') { if (type === 'boolean') {
const normalized = String(defaultValue).toLowerCase() const normalized = String(defaultValue).toLowerCase()
if (normalized === 'true' || normalized === '1') { if (normalized === 'true' || normalized === '1') {
@@ -777,6 +835,90 @@ const formatDefaultValue = (type: string, defaultValue: any): string => {
return String(defaultValue) return String(defaultValue)
} }
const resolveRequiredFlag = (field: any): boolean => {
if (typeof field?.required === 'boolean') {
return field.required
}
const nestedRequired = field?.value?.required
if (typeof nestedRequired === 'boolean') {
return nestedRequired
}
if (typeof nestedRequired === 'string') {
const normalized = nestedRequired.toLowerCase()
return normalized === 'true' || normalized === '1'
}
return false
}
const resolveOptions = (field: any): string[] => {
const sources = [field?.options, field?.value?.options, field?.value?.choices]
for (const source of sources) {
if (Array.isArray(source)) {
const mapped = source
.map((option: unknown) => {
if (option === null || option === undefined) {
return ''
}
if (typeof option === 'string') {
return option.trim()
}
if (typeof option === 'object') {
const record = option as Record<string, unknown>
const keys = ['value', 'label', 'name']
for (const key of keys) {
const candidate = record[key]
if (typeof candidate === 'string' && candidate.trim().length > 0) {
return candidate.trim()
}
}
}
const fallback = String(option).trim()
return fallback === '[object Object]' ? '' : fallback
})
.filter((option) => option.length > 0)
if (mapped.length) {
return mapped
}
}
}
return []
}
const extractStoredCustomFieldValue = (entry: any): any => {
if (entry === null || entry === undefined) {
return ''
}
if (typeof entry === 'string' || typeof entry === 'number' || typeof entry === 'boolean') {
return entry
}
if (typeof entry !== 'object') {
return String(entry)
}
const direct = entry.value
if (direct !== undefined && direct !== null) {
if (typeof direct === 'object') {
if (direct === null) {
return ''
}
if ('value' in direct && direct.value !== undefined && direct.value !== null) {
return direct.value
}
if ('defaultValue' in direct && direct.defaultValue !== undefined && direct.defaultValue !== null) {
return direct.defaultValue
}
return ''
}
return direct
}
if (entry.defaultValue !== undefined && entry.defaultValue !== null) {
return entry.defaultValue
}
if (entry.customFieldValue?.value !== undefined && entry.customFieldValue?.value !== null) {
return entry.customFieldValue.value
}
return ''
}
const getStructureCustomFields = (structure: ComponentModelStructure | null) => { const getStructureCustomFields = (structure: ComponentModelStructure | null) => {
return Array.isArray(structure?.customFields) ? structure.customFields : [] return Array.isArray(structure?.customFields) ? structure.customFields : []
} }

View File

@@ -104,32 +104,43 @@
{{ selectedType.description || 'Ce squelette définit la structure et les contraintes du composant.' }} {{ selectedType.description || 'Ce squelette définit la structure et les contraintes du composant.' }}
</p> </p>
</div> </div>
<span class="badge badge-outline">{{ formatStructurePreview(selectedType.structure) }}</span> <span class="badge badge-outline">{{ formatStructurePreview(selectedTypeStructure) }}</span>
</div> </div>
<details v-if="selectedType.structure" class="collapse collapse-arrow bg-base-100"> <details v-if="selectedTypeStructure" class="collapse collapse-arrow bg-base-100">
<summary class="collapse-title text-sm font-medium"> <summary class="collapse-title text-sm font-medium">
Consulter le détail du squelette Consulter le détail du squelette
</summary> </summary>
<div class="collapse-content space-y-4 text-sm text-base-content/80"> <div class="collapse-content space-y-4 text-sm text-base-content/80">
<div v-if="getStructureCustomFields(selectedType.structure).length" class="space-y-2"> <div v-if="getStructureCustomFields(selectedTypeStructure).length" class="space-y-2">
<h3 class="font-semibold text-sm text-base-content">Champs personnalisés</h3> <h3 class="font-semibold text-sm text-base-content">Champs personnalisés</h3>
<ul class="list-disc list-inside space-y-1"> <ul class="space-y-2">
<li <li
v-for="field in getStructureCustomFields(selectedType.structure)" v-for="field in getStructureCustomFields(selectedTypeStructure)"
:key="field.key || field.name" :key="field.customFieldId || field.id || field.name"
class="rounded bg-base-200/60 px-3 py-2"
> >
<span class="font-medium">{{ field.key || field.name }}</span> <p class="font-medium text-sm text-base-content">
<span v-if="field.value !== undefined && field.value !== null"> : {{ field.value }}</span> {{ field.name || field.key }}
</p>
<p class="text-xs text-base-content/70 mt-1">
Type : {{ field.type || 'text' }}<span v-if="field.required"> Obligatoire</span>
<span v-if="Array.isArray(field.options) && field.options.length">
Options : {{ field.options.join(', ') }}
</span>
<span v-if="field.defaultValue">
Défaut : {{ field.defaultValue }}
</span>
</p>
</li> </li>
</ul> </ul>
</div> </div>
<div v-if="getStructurePieces(selectedType.structure).length" class="space-y-2"> <div v-if="getStructurePieces(selectedTypeStructure).length" class="space-y-2">
<h3 class="font-semibold text-sm text-base-content">Pièces imposées</h3> <h3 class="font-semibold text-sm text-base-content">Pièces imposées</h3>
<ul class="list-disc list-inside space-y-1"> <ul class="list-disc list-inside space-y-1">
<li <li
v-for="(piece, index) in getStructurePieces(selectedType.structure)" v-for="(piece, index) in getStructurePieces(selectedTypeStructure)"
:key="piece.role || piece.typePieceId || piece.familyCode || index" :key="piece.role || piece.typePieceId || piece.familyCode || index"
> >
{{ resolvePieceLabel(piece) }} {{ resolvePieceLabel(piece) }}
@@ -137,11 +148,11 @@
</ul> </ul>
</div> </div>
<div v-if="getStructureSubcomponents(selectedType.structure).length" class="space-y-2"> <div v-if="getStructureSubcomponents(selectedTypeStructure).length" class="space-y-2">
<h3 class="font-semibold text-sm text-base-content">Sous-composants</h3> <h3 class="font-semibold text-sm text-base-content">Sous-composants</h3>
<ul class="list-disc list-inside space-y-1"> <ul class="list-disc list-inside space-y-1">
<li <li
v-for="(subcomponent, index) in getStructureSubcomponents(selectedType.structure)" v-for="(subcomponent, index) in getStructureSubcomponents(selectedTypeStructure)"
:key="subcomponent.alias || subcomponent.typeComposantId || subcomponent.familyCode || index" :key="subcomponent.alias || subcomponent.typeComposantId || subcomponent.familyCode || index"
> >
{{ resolveSubcomponentLabel(subcomponent) }} {{ resolveSubcomponentLabel(subcomponent) }}
@@ -327,7 +338,7 @@ import { usePieces } from '~/composables/usePieces'
import { useToast } from '~/composables/useToast' import { useToast } from '~/composables/useToast'
import { useCustomFields } from '~/composables/useCustomFields' import { useCustomFields } from '~/composables/useCustomFields'
import { useDocuments } from '~/composables/useDocuments' import { useDocuments } from '~/composables/useDocuments'
import { formatStructurePreview } from '~/shared/modelUtils' import { formatStructurePreview, normalizeStructureForEditor } from '~/shared/modelUtils'
import type { import type {
ComponentModelPiece, ComponentModelPiece,
ComponentModelStructure, ComponentModelStructure,
@@ -422,6 +433,11 @@ const selectedType = computed(() => {
return componentTypeList.value.find((type) => type.id === selectedTypeId.value) ?? null return componentTypeList.value.find((type) => type.id === selectedTypeId.value) ?? null
}) })
const selectedTypeStructure = computed<ComponentModelStructure | null>(() => {
const structure = selectedType.value?.structure ?? null
return structure ? normalizeStructureForEditor(structure) : null
})
watch(selectedType, (type) => { watch(selectedType, (type) => {
if (!type) { if (!type) {
clearCreationForm() clearCreationForm()
@@ -433,8 +449,8 @@ watch(selectedType, (type) => {
creationForm.name = type.name creationForm.name = type.name
} }
lastSuggestedName.value = creationForm.name lastSuggestedName.value = creationForm.name
customFieldInputs.value = normalizeCustomFieldInputs(type.structure) customFieldInputs.value = normalizeCustomFieldInputs(selectedTypeStructure.value)
structureAssignments.value = initializeStructureAssignments(type.structure) structureAssignments.value = initializeStructureAssignments(selectedTypeStructure.value)
}) })
const extractSubcomponents = ( const extractSubcomponents = (
@@ -845,11 +861,10 @@ const normalizeCustomField = (rawField: any): CustomFieldInput | null => {
return null return null
} }
const type = resolveFieldType(rawField) const type = resolveFieldType(rawField)
const required = !!rawField.required const required = resolveRequiredFlag(rawField)
const options = Array.isArray(rawField.options) const options = resolveOptions(rawField)
? rawField.options.filter((option: unknown): option is string => typeof option === 'string') const defaultSource = resolveDefaultValue(rawField)
: [] const value = formatDefaultValue(type, defaultSource)
const value = formatDefaultValue(type, rawField.value)
const id = typeof rawField.id === 'string' ? rawField.id : null const id = typeof rawField.id === 'string' ? rawField.id : null
const customFieldId = typeof rawField.customFieldId === 'string' ? rawField.customFieldId : id const customFieldId = typeof rawField.customFieldId === 'string' ? rawField.customFieldId : id
const customFieldValueId = typeof rawField.customFieldValueId === 'string' const customFieldValueId = typeof rawField.customFieldValueId === 'string'
@@ -873,14 +888,56 @@ const resolveFieldName = (field: any): string => {
const resolveFieldType = (field: any): string => { const resolveFieldType = (field: any): string => {
const allowed = ['text', 'number', 'select', 'boolean', 'date'] const allowed = ['text', 'number', 'select', 'boolean', 'date']
const value = typeof field?.type === 'string' ? field.type.toLowerCase() : '' const rawType =
typeof field?.type === 'string'
? field.type
: typeof field?.value?.type === 'string'
? field.value.type
: ''
const value = rawType.toLowerCase()
return allowed.includes(value) ? value : 'text' return allowed.includes(value) ? value : 'text'
} }
const resolveDefaultValue = (field: any): any => {
if (!field || typeof field !== 'object') {
return null
}
if (field.defaultValue !== undefined && field.defaultValue !== null) {
return field.defaultValue
}
if (field.value !== undefined && field.value !== null && typeof field.value !== 'object') {
return field.value
}
if (field.default !== undefined && field.default !== null) {
return field.default
}
if (field.value && typeof field.value === 'object') {
if ((field.value as any).defaultValue !== undefined && (field.value as any).defaultValue !== null) {
return (field.value as any).defaultValue
}
if ((field.value as any).value !== undefined && (field.value as any).value !== null && typeof (field.value as any).value !== 'object') {
return (field.value as any).value
}
}
return null
}
const formatDefaultValue = (type: string, defaultValue: any): string => { const formatDefaultValue = (type: string, defaultValue: any): string => {
if (defaultValue === null || defaultValue === undefined) { if (defaultValue === null || defaultValue === undefined) {
return '' return ''
} }
if (typeof defaultValue === 'object') {
if (defaultValue === null) {
return ''
}
if ('defaultValue' in (defaultValue as Record<string, any>)) {
return formatDefaultValue(type, (defaultValue as Record<string, any>).defaultValue)
}
if ('value' in (defaultValue as Record<string, any>)) {
return formatDefaultValue(type, (defaultValue as Record<string, any>).value)
}
return ''
}
if (type === 'boolean') { if (type === 'boolean') {
const normalized = String(defaultValue).toLowerCase() const normalized = String(defaultValue).toLowerCase()
if (normalized === 'true' || normalized === '1') { if (normalized === 'true' || normalized === '1') {
@@ -894,6 +951,55 @@ const formatDefaultValue = (type: string, defaultValue: any): string => {
return String(defaultValue) return String(defaultValue)
} }
const resolveRequiredFlag = (field: any): boolean => {
if (typeof field?.required === 'boolean') {
return field.required
}
const nestedRequired = field?.value?.required
if (typeof nestedRequired === 'boolean') {
return nestedRequired
}
if (typeof nestedRequired === 'string') {
const normalized = nestedRequired.toLowerCase()
return normalized === 'true' || normalized === '1'
}
return false
}
const resolveOptions = (field: any): string[] => {
const sources = [field?.options, field?.value?.options, field?.value?.choices]
for (const source of sources) {
if (Array.isArray(source)) {
const mapped = source
.map((option: unknown) => {
if (option === null || option === undefined) {
return ''
}
if (typeof option === 'string') {
return option.trim()
}
if (typeof option === 'object') {
const record = option as Record<string, unknown>
const keys = ['value', 'label', 'name']
for (const key of keys) {
const candidate = record[key]
if (typeof candidate === 'string' && candidate.trim().length > 0) {
return candidate.trim()
}
}
}
const fallback = String(option).trim()
return fallback === '[object Object]' ? '' : fallback
})
.filter((option) => option.length > 0)
if (mapped.length) {
return mapped
}
}
}
return []
}
const buildCustomFieldMetadata = (field: CustomFieldInput) => ({ const buildCustomFieldMetadata = (field: CustomFieldInput) => ({
customFieldName: field.name, customFieldName: field.name,
customFieldType: field.type, customFieldType: field.type,

View File

@@ -699,7 +699,8 @@ const normalizeCustomField = (rawField: any): CustomFieldInput | null => {
const options = Array.isArray(rawField.options) const options = Array.isArray(rawField.options)
? rawField.options.filter((option: unknown): option is string => typeof option === 'string') ? rawField.options.filter((option: unknown): option is string => typeof option === 'string')
: [] : []
const value = formatDefaultValue(type, rawField.value) const defaultSource = resolveDefaultValue(rawField)
const value = formatDefaultValue(type, defaultSource)
const id = typeof rawField.id === 'string' ? rawField.id : null const id = typeof rawField.id === 'string' ? rawField.id : null
const customFieldId = typeof rawField.customFieldId === 'string' ? rawField.customFieldId : id const customFieldId = typeof rawField.customFieldId === 'string' ? rawField.customFieldId : id
const customFieldValueId = typeof rawField.customFieldValueId === 'string' const customFieldValueId = typeof rawField.customFieldValueId === 'string'
@@ -728,10 +729,46 @@ const resolveFieldType = (field: any): string => {
return allowed.includes(value) ? value : 'text' return allowed.includes(value) ? value : 'text'
} }
const resolveDefaultValue = (field: any): any => {
if (!field || typeof field !== 'object') {
return null
}
if (field.defaultValue !== undefined && field.defaultValue !== null) {
return field.defaultValue
}
if (field.value !== undefined && field.value !== null && typeof field.value !== 'object') {
return field.value
}
if (field.default !== undefined && field.default !== null) {
return field.default
}
if (field.value && typeof field.value === 'object') {
if ((field.value as any).defaultValue !== undefined && (field.value as any).defaultValue !== null) {
return (field.value as any).defaultValue
}
if ((field.value as any).value !== undefined && (field.value as any).value !== null && typeof (field.value as any).value !== 'object') {
return (field.value as any).value
}
}
return null
}
const formatDefaultValue = (type: string, defaultValue: any): string => { const formatDefaultValue = (type: string, defaultValue: any): string => {
if (defaultValue === null || defaultValue === undefined) { if (defaultValue === null || defaultValue === undefined) {
return '' return ''
} }
if (typeof defaultValue === 'object') {
if (defaultValue === null) {
return ''
}
if ('defaultValue' in (defaultValue as Record<string, any>)) {
return formatDefaultValue(type, (defaultValue as Record<string, any>).defaultValue)
}
if ('value' in (defaultValue as Record<string, any>)) {
return formatDefaultValue(type, (defaultValue as Record<string, any>).value)
}
return ''
}
if (type === 'boolean') { if (type === 'boolean') {
const normalized = String(defaultValue).toLowerCase() const normalized = String(defaultValue).toLowerCase()
if (normalized === 'true' || normalized === '1') { if (normalized === 'true' || normalized === '1') {

View File

@@ -494,7 +494,8 @@ const normalizeCustomField = (rawField: any): CustomFieldInput | null => {
const options = Array.isArray(rawField.options) const options = Array.isArray(rawField.options)
? rawField.options.filter((option: unknown): option is string => typeof option === 'string') ? rawField.options.filter((option: unknown): option is string => typeof option === 'string')
: [] : []
const value = formatDefaultValue(type, rawField.value) const defaultSource = resolveDefaultValue(rawField)
const value = formatDefaultValue(type, defaultSource)
const id = typeof rawField.id === 'string' ? rawField.id : null const id = typeof rawField.id === 'string' ? rawField.id : null
const customFieldId = typeof rawField.customFieldId === 'string' ? rawField.customFieldId : id const customFieldId = typeof rawField.customFieldId === 'string' ? rawField.customFieldId : id
const customFieldValueId = typeof rawField.customFieldValueId === 'string' const customFieldValueId = typeof rawField.customFieldValueId === 'string'
@@ -523,10 +524,46 @@ const resolveFieldType = (field: any): string => {
return allowed.includes(value) ? value : 'text' return allowed.includes(value) ? value : 'text'
} }
const resolveDefaultValue = (field: any): any => {
if (!field || typeof field !== 'object') {
return null
}
if (field.defaultValue !== undefined && field.defaultValue !== null) {
return field.defaultValue
}
if (field.value !== undefined && field.value !== null && typeof field.value !== 'object') {
return field.value
}
if (field.default !== undefined && field.default !== null) {
return field.default
}
if (field.value && typeof field.value === 'object') {
if ((field.value as any).defaultValue !== undefined && (field.value as any).defaultValue !== null) {
return (field.value as any).defaultValue
}
if ((field.value as any).value !== undefined && (field.value as any).value !== null && typeof (field.value as any).value !== 'object') {
return (field.value as any).value
}
}
return null
}
const formatDefaultValue = (type: string, defaultValue: any): string => { const formatDefaultValue = (type: string, defaultValue: any): string => {
if (defaultValue === null || defaultValue === undefined) { if (defaultValue === null || defaultValue === undefined) {
return '' return ''
} }
if (typeof defaultValue === 'object') {
if (defaultValue === null) {
return ''
}
if ('defaultValue' in (defaultValue as Record<string, any>)) {
return formatDefaultValue(type, (defaultValue as Record<string, any>).defaultValue)
}
if ('value' in (defaultValue as Record<string, any>)) {
return formatDefaultValue(type, (defaultValue as Record<string, any>).value)
}
return ''
}
if (type === 'boolean') { if (type === 'boolean') {
const normalized = String(defaultValue).toLowerCase() const normalized = String(defaultValue).toLowerCase()
if (normalized === 'true' || normalized === '1') { if (normalized === 'true' || normalized === '1') {

View File

@@ -1,5 +1,6 @@
import { import {
createEmptyComponentModelStructure, createEmptyComponentModelStructure,
type ComponentModelCustomFieldType,
type ComponentModelCustomField, type ComponentModelCustomField,
type ComponentModelPiece, type ComponentModelPiece,
type ComponentModelStructure, type ComponentModelStructure,
@@ -61,6 +62,31 @@ export const cloneStructure = (input: any): ComponentModelStructure => {
} }
} }
const toStringArray = (input: unknown): string[] | undefined => {
if (!Array.isArray(input)) {
return undefined
}
const parsed = input
.map((value) => {
if (typeof value === 'string') {
return value.trim()
}
if (value === null || value === undefined) {
return ''
}
return String(value).trim()
})
.filter((value) => value.length > 0)
return parsed.length ? parsed : undefined
}
const extractFieldValueObject = (field: any): Record<string, any> => {
if (isPlainObject(field?.value)) {
return field.value as Record<string, any>
}
return {}
}
const sanitizeCustomFields = (fields: any[]): ComponentModelCustomField[] => { const sanitizeCustomFields = (fields: any[]): ComponentModelCustomField[] => {
if (!Array.isArray(fields)) { if (!Array.isArray(fields)) {
return [] return []
@@ -68,32 +94,84 @@ const sanitizeCustomFields = (fields: any[]): ComponentModelCustomField[] => {
return fields return fields
.map((field) => { .map((field) => {
const name = typeof field?.name === 'string' ? field.name.trim() : '' const rawName =
typeof field?.name === 'string'
? field.name
: typeof field?.key === 'string'
? field.key
: ''
const name = rawName.trim()
if (!name) { if (!name) {
return null return null
} }
const type = typeof field?.type === 'string' && field.type ? field.type : 'text' const valueObject = extractFieldValueObject(field)
const required = !!field?.required
const candidateType =
typeof field?.type === 'string' && field.type
? field.type
: typeof valueObject?.type === 'string'
? valueObject.type
: ''
const allowedTypes: ComponentModelCustomFieldType[] = ['text', 'number', 'select', 'boolean', 'date']
const type = allowedTypes.includes(candidateType as ComponentModelCustomFieldType)
? (candidateType as ComponentModelCustomFieldType)
: 'text'
const required =
typeof valueObject?.required === 'boolean' ? valueObject.required : !!field?.required
let options: string[] | undefined let options: string[] | undefined
if (type === 'select') { if (type === 'select') {
const rawOptions = typeof field?.optionsText === 'string' options =
? field.optionsText toStringArray(valueObject?.options) ||
: Array.isArray(field?.options) toStringArray((valueObject as any)?.choices) ||
? field.options.join('\n') toStringArray(field?.options)
: ''
const parsed = rawOptions if (!options && typeof field?.optionsText === 'string') {
.split(/\r?\n/) const parsedFromText = field.optionsText
.map((option) => option.trim()) .split(/\r?\n/)
.filter((option) => option.length > 0) .map((option) => option.trim())
options = parsed.length > 0 ? parsed : undefined .filter((option) => option.length > 0)
options = parsedFromText.length ? parsedFromText : undefined
}
} }
const result: ComponentModelCustomField = { name, type, required } const result: ComponentModelCustomField = { name, type, required }
if (options) { if (options) {
result.options = options result.options = options
} }
const defaultCandidate =
field?.defaultValue ?? valueObject?.defaultValue ?? field?.value ?? field?.default ?? null
const resolvedDefault = (() => {
if (defaultCandidate === undefined || defaultCandidate === null) {
return undefined
}
if (typeof defaultCandidate === 'object') {
if (defaultCandidate === null) {
return undefined
}
if ('defaultValue' in (defaultCandidate as Record<string, any>)) {
return (defaultCandidate as Record<string, any>).defaultValue
}
if ('value' in (defaultCandidate as Record<string, any>)) {
return (defaultCandidate as Record<string, any>).value
}
return undefined
}
return defaultCandidate
})()
if (resolvedDefault !== undefined && resolvedDefault !== null && resolvedDefault !== '') {
result.defaultValue = String(resolvedDefault)
}
const id = typeof field?.id === 'string' ? field.id : undefined
if (id) {
result.id = id
}
const customFieldId = typeof field?.customFieldId === 'string' ? field.customFieldId : undefined
if (customFieldId) {
result.customFieldId = customFieldId
}
return result return result
}) })
.filter((field): field is ComponentModelCustomField => !!field) .filter((field): field is ComponentModelCustomField => !!field)
@@ -207,13 +285,142 @@ const sanitizeSubcomponents = (components: any[]): ComponentModelStructureNode[]
.filter((component): component is ComponentModelStructureNode => !!component) .filter((component): component is ComponentModelStructureNode => !!component)
} }
export const normalizeStructureForSave = (input: any): ComponentModelStructure => { export const normalizeStructureForEditor = (input: any): ComponentModelStructure => {
const source = cloneStructure(input) const source = cloneStructure(input)
const sanitizedCustomFields = sanitizeCustomFields(source.customFields)
const customFields = sanitizedCustomFields.map((field) => {
const options = Array.isArray(field.options) ? [...field.options] : []
const optionsText = options.length ? options.join('\n') : ''
const defaultValue =
field.defaultValue !== undefined && field.defaultValue !== null && field.defaultValue !== ''
? String(field.defaultValue)
: null
const copy: ComponentModelCustomField = {
name: field.name,
type: field.type,
required: field.required,
options,
defaultValue,
optionsText,
id: field.id,
customFieldId: field.customFieldId,
}
return copy
})
const result: ComponentModelStructure = { const result: ComponentModelStructure = {
customFields: sanitizeCustomFields(source.customFields), customFields: customFields as ComponentModelCustomField[],
pieces: sanitizePieces(source.pieces), pieces: sanitizePieces(source.pieces),
subcomponents: sanitizeSubcomponents(source.subcomponents), subcomponents: hydrateSubcomponents(source.subcomponents),
}
if (typeof source.typeComposantId === 'string' && source.typeComposantId.length > 0) {
result.typeComposantId = source.typeComposantId
}
if (typeof source.typeComposantLabel === 'string' && source.typeComposantLabel.length > 0) {
result.typeComposantLabel = source.typeComposantLabel
}
if (typeof source.modelId === 'string' && source.modelId.length > 0) {
result.modelId = source.modelId
}
if (typeof source.familyCode === 'string' && source.familyCode.length > 0) {
result.familyCode = source.familyCode
}
if (typeof source.alias === 'string' && source.alias.length > 0) {
result.alias = source.alias
}
return result
}
export const normalizeStructureForSave = (input: any): any => {
const source = cloneStructure(input)
const sanitizedCustomFields = sanitizeCustomFields(source.customFields)
const backendCustomFields = sanitizedCustomFields.map((field) => {
const value: Record<string, any> = {
type: field.type,
required: !!field.required,
}
if (field.options && field.options.length) {
value.options = field.options
}
if (field.defaultValue !== undefined && field.defaultValue !== null && field.defaultValue !== '') {
value.defaultValue = field.defaultValue
}
const payload: Record<string, any> = {
key: field.name,
value,
}
if (field.id) {
payload.id = field.id
}
if (field.customFieldId) {
payload.customFieldId = field.customFieldId
}
return payload
}) as any
const backendPieces = sanitizePieces(source.pieces).map((piece) => {
const payload: Record<string, any> = {}
if ((piece as any).familyCode) {
payload.familyCode = (piece as any).familyCode
}
if (piece.typePieceId) {
payload.typePieceId = piece.typePieceId
}
if (piece.typePieceLabel) {
payload.typePieceLabel = piece.typePieceLabel
}
if (piece.reference) {
payload.reference = piece.reference
}
return payload
}) as any
const mapSubcomponentForSave = (subcomponent: ComponentModelStructureNode): any => {
const payload: Record<string, any> = {}
if (subcomponent.typeComposantId) {
payload.typeComposantId = subcomponent.typeComposantId
}
if (subcomponent.modelId) {
payload.modelId = subcomponent.modelId
}
if (subcomponent.familyCode) {
payload.familyCode = subcomponent.familyCode
}
if (subcomponent.alias) {
payload.alias = subcomponent.alias
}
if (Array.isArray(subcomponent.subcomponents) && subcomponent.subcomponents.length) {
payload.subcomponents = subcomponent.subcomponents.map(mapSubcomponentForSave)
}
return payload
}
const backendSubcomponents = sanitizeSubcomponents(source.subcomponents).map(mapSubcomponentForSave) as any
const result: ComponentModelStructure = {
customFields: backendCustomFields,
pieces: backendPieces,
subcomponents: backendSubcomponents,
}
if (typeof source.typeComposantId === 'string' && source.typeComposantId.length > 0) {
(result as any).typeComposantId = source.typeComposantId
}
if (typeof source.typeComposantLabel === 'string' && source.typeComposantLabel.length > 0) {
(result as any).typeComposantLabel = source.typeComposantLabel
}
if (typeof source.modelId === 'string' && source.modelId.length > 0) {
(result as any).modelId = source.modelId
}
if (typeof source.familyCode === 'string' && source.familyCode.length > 0) {
(result as any).familyCode = source.familyCode
}
if (typeof source.alias === 'string' && source.alias.length > 0) {
(result as any).alias = source.alias
} }
return result return result
@@ -224,13 +431,83 @@ const hydrateCustomFields = (fields: any[]): any[] => {
return [] return []
} }
return fields.map((field) => ({ return fields.map((field) => {
name: field?.name ?? '', const valueObject = extractFieldValueObject(field)
type: field?.type ?? 'text', const name = typeof field?.name === 'string'
required: !!field?.required, ? field.name
options: Array.isArray(field?.options) ? field.options : [], : typeof field?.key === 'string'
optionsText: Array.isArray(field?.options) ? field.options.join('\n') : (field?.optionsText ?? ''), ? field.key
})) : ''
const candidateType =
typeof field?.type === 'string' && field.type
? field.type
: typeof valueObject?.type === 'string'
? valueObject.type
: ''
const allowedTypes: ComponentModelCustomFieldType[] = ['text', 'number', 'select', 'boolean', 'date']
const type = allowedTypes.includes(candidateType as ComponentModelCustomFieldType)
? (candidateType as ComponentModelCustomFieldType)
: 'text'
const required =
typeof field?.required === 'boolean'
? field.required
: typeof valueObject?.required === 'boolean'
? valueObject.required
: false
const options =
toStringArray(field?.options) ||
toStringArray(valueObject?.options) ||
toStringArray((valueObject as any)?.choices) ||
[]
const optionsText = typeof field?.optionsText === 'string'
? field.optionsText
: options.length
? options.join('\n')
: ''
const defaultCandidate =
field?.defaultValue ?? valueObject?.defaultValue ?? field?.value ?? field?.default ?? null
const resolvedDefault = (() => {
if (defaultCandidate === undefined || defaultCandidate === null) {
return undefined
}
if (typeof defaultCandidate === 'object') {
if (defaultCandidate === null) {
return undefined
}
if ('defaultValue' in (defaultCandidate as Record<string, any>)) {
return (defaultCandidate as Record<string, any>).defaultValue
}
if ('value' in (defaultCandidate as Record<string, any>)) {
return (defaultCandidate as Record<string, any>).value
}
return undefined
}
return defaultCandidate
})()
const defaultValue =
resolvedDefault !== undefined && resolvedDefault !== null && resolvedDefault !== ''
? String(resolvedDefault)
: ''
const id = typeof field?.id === 'string' ? field.id : undefined
const customFieldId = typeof field?.customFieldId === 'string' ? field.customFieldId : undefined
return {
name,
type,
required,
options,
optionsText,
defaultValue,
id,
customFieldId,
}
})
} }
const hydratePieces = (pieces: any[]): ComponentModelPiece[] => { const hydratePieces = (pieces: any[]): ComponentModelPiece[] => {
@@ -280,27 +557,29 @@ export const hydrateStructureForEditor = (input: any): ComponentModelStructure =
} }
} }
const toOptionsText = (field: any) => {
if (typeof field?.optionsText === 'string') {
return field.optionsText
}
if (Array.isArray(field?.options)) {
return field.options.join('\n')
}
return ''
}
const mapComponentCustomFields = (fields: any[]) => { const mapComponentCustomFields = (fields: any[]) => {
if (!Array.isArray(fields)) { if (!Array.isArray(fields)) {
return [] return []
} }
return fields.map((field) => ({ return hydrateCustomFields(fields).map((field) => {
name: field?.name ?? '', const defaultValue =
type: field?.type ?? 'text', field?.defaultValue !== undefined && field?.defaultValue !== null && field?.defaultValue !== ''
required: !!field?.required, ? field.defaultValue
options: Array.isArray(field?.options) ? field.options : [], : null
optionsText: toOptionsText(field), return {
})) name: typeof field?.name === 'string' ? field.name : '',
type: field?.type ?? 'text',
required: !!field?.required,
options: Array.isArray(field?.options) ? field.options : [],
optionsText: typeof field?.optionsText === 'string' ? field.optionsText : '',
defaultValue,
id: typeof (field as any)?.id === 'string' ? (field as any).id : undefined,
customFieldId:
typeof (field as any)?.customFieldId === 'string'
? (field as any).customFieldId
: undefined,
}
})
} }
const mapComponentPieces = (pieces: any[]): ComponentModelPiece[] => { const mapComponentPieces = (pieces: any[]): ComponentModelPiece[] => {
@@ -352,7 +631,7 @@ export const extractStructureFromComponent = (component: any) => {
alias: component?.alias ?? component?.name ?? '', alias: component?.alias ?? component?.name ?? '',
} }
return normalizeStructureForSave(raw) return normalizeStructureForEditor(raw)
} }
export const computeStructureStats = (structure: any): ModelStructurePreview => { export const computeStructureStats = (structure: any): ModelStructurePreview => {
@@ -518,7 +797,11 @@ const hydratePieceCustomFields = (fields: any[]): PieceModelStructureEditorField
type: field?.type ?? 'text', type: field?.type ?? 'text',
required: !!field?.required, required: !!field?.required,
options: Array.isArray(field?.options) ? field.options : undefined, options: Array.isArray(field?.options) ? field.options : undefined,
optionsText: Array.isArray(field?.options) ? field.options.join('\n') : (field?.optionsText ?? ''), optionsText: typeof field?.optionsText === 'string'
? field.optionsText
: Array.isArray(field?.options)
? field.options.join('\n')
: '',
})) }))
} }

View File

@@ -5,6 +5,10 @@ export interface ComponentModelCustomField {
type: ComponentModelCustomFieldType type: ComponentModelCustomFieldType
required: boolean required: boolean
options?: string[] options?: string[]
defaultValue?: string | null
optionsText?: string
id?: string
customFieldId?: string
} }
export interface ComponentModelPiece { export interface ComponentModelPiece {