add sub componet in catego ske
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -25,7 +25,6 @@
|
||||
initial-category="COMPONENT"
|
||||
:initial-data="initialData"
|
||||
:lock-category="true"
|
||||
:allow-component-subcomponents="false"
|
||||
:saving="saving"
|
||||
@submit="handleSubmit"
|
||||
@cancel="handleCancel"
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
mode="create"
|
||||
initial-category="COMPONENT"
|
||||
:lock-category="true"
|
||||
:allow-component-subcomponents="false"
|
||||
:saving="saving"
|
||||
@submit="handleSubmit"
|
||||
@cancel="handleCancel"
|
||||
|
||||
@@ -131,32 +131,43 @@
|
||||
{{ selectedType.description || 'Ce squelette définit la structure et les contraintes du composant.' }}
|
||||
</p>
|
||||
</div>
|
||||
<span class="badge badge-outline">{{ formatStructurePreview(selectedType.structure) }}</span>
|
||||
<span class="badge badge-outline">{{ formatStructurePreview(selectedTypeStructure) }}</span>
|
||||
</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">
|
||||
Consulter le détail du squelette
|
||||
</summary>
|
||||
<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>
|
||||
<ul class="list-disc list-inside space-y-1">
|
||||
<ul class="space-y-2">
|
||||
<li
|
||||
v-for="field in getStructureCustomFields(selectedType.structure)"
|
||||
:key="field.key || field.name"
|
||||
v-for="field in getStructureCustomFields(selectedTypeStructure)"
|
||||
: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>
|
||||
<span v-if="field.value !== undefined && field.value !== null"> : {{ field.value }}</span>
|
||||
<p class="font-medium text-sm text-base-content">
|
||||
{{ 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>
|
||||
</ul>
|
||||
</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>
|
||||
<ul class="list-disc list-inside space-y-1">
|
||||
<li
|
||||
v-for="(piece, index) in getStructurePieces(selectedType.structure)"
|
||||
v-for="(piece, index) in getStructurePieces(selectedTypeStructure)"
|
||||
:key="piece.role || piece.typePieceId || piece.familyCode || index"
|
||||
>
|
||||
{{ resolvePieceLabel(piece) }}
|
||||
@@ -164,11 +175,11 @@
|
||||
</ul>
|
||||
</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>
|
||||
<ul class="list-disc list-inside space-y-1">
|
||||
<li
|
||||
v-for="(subcomponent, index) in getStructureSubcomponents(selectedType.structure)"
|
||||
v-for="(subcomponent, index) in getStructureSubcomponents(selectedTypeStructure)"
|
||||
:key="subcomponent.alias || subcomponent.typeComposantId || subcomponent.familyCode || index"
|
||||
>
|
||||
{{ resolveSubcomponentLabel(subcomponent) }}
|
||||
@@ -177,7 +188,7 @@
|
||||
</div>
|
||||
|
||||
<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"
|
||||
>
|
||||
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 { useToast } from '~/composables/useToast'
|
||||
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 { ModelType } from '~/services/modelTypes'
|
||||
import { getFileIcon } from '~/utils/fileIcons'
|
||||
@@ -530,6 +541,11 @@ const selectedType = computed(() => {
|
||||
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(() =>
|
||||
customFieldInputs.value.every((field) => {
|
||||
if (!field.required) {
|
||||
@@ -582,8 +598,8 @@ const fetchComponent = async () => {
|
||||
let initialized = false
|
||||
|
||||
watch(
|
||||
[component, selectedType],
|
||||
([currentComponent, currentType]) => {
|
||||
[component, selectedTypeStructure],
|
||||
([currentComponent, currentStructure]) => {
|
||||
if (!currentComponent || initialized) {
|
||||
return
|
||||
}
|
||||
@@ -596,7 +612,7 @@ watch(
|
||||
editionForm.prix = currentComponent.prix !== null && currentComponent.prix !== undefined ? String(currentComponent.prix) : ''
|
||||
|
||||
customFieldInputs.value = buildCustomFieldInputs(
|
||||
currentType?.structure ?? null,
|
||||
currentStructure,
|
||||
currentComponent.customFieldValues,
|
||||
)
|
||||
|
||||
@@ -605,12 +621,12 @@ watch(
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
watch(selectedType, (currentType) => {
|
||||
if (!component.value || !currentType) {
|
||||
watch(selectedTypeStructure, (currentStructure) => {
|
||||
if (!component.value) {
|
||||
return
|
||||
}
|
||||
customFieldInputs.value = buildCustomFieldInputs(
|
||||
currentType.structure,
|
||||
currentStructure,
|
||||
component.value.customFieldValues,
|
||||
)
|
||||
})
|
||||
@@ -662,7 +678,8 @@ const buildCustomFieldInputs = (
|
||||
structure: ComponentModelStructure | null,
|
||||
values: any[] | null,
|
||||
): CustomFieldInput[] => {
|
||||
const definitions = normalizeCustomFieldInputs(structure)
|
||||
const normalizedStructure = structure ? normalizeStructureForEditor(structure) : null
|
||||
const definitions = normalizeCustomFieldInputs(normalizedStructure)
|
||||
const valueList = Array.isArray(values) ? values : []
|
||||
|
||||
const mapById = new Map<string, any>()
|
||||
@@ -694,7 +711,7 @@ const buildCustomFieldInputs = (
|
||||
}
|
||||
}
|
||||
|
||||
const resolvedValue = matched.value ?? ''
|
||||
const resolvedValue = extractStoredCustomFieldValue(matched)
|
||||
return {
|
||||
...definition,
|
||||
customFieldId: matched.customField?.id || definition.customFieldId || definition.id,
|
||||
@@ -728,11 +745,10 @@ const normalizeCustomField = (rawField: any): CustomFieldInput | null => {
|
||||
return null
|
||||
}
|
||||
const type = resolveFieldType(rawField)
|
||||
const required = !!rawField.required
|
||||
const options = Array.isArray(rawField.options)
|
||||
? rawField.options.filter((option: unknown): option is string => typeof option === 'string')
|
||||
: []
|
||||
const value = formatDefaultValue(type, rawField.value)
|
||||
const required = resolveRequiredFlag(rawField)
|
||||
const options = resolveOptions(rawField)
|
||||
const defaultSource = resolveDefaultValue(rawField)
|
||||
const value = formatDefaultValue(type, defaultSource)
|
||||
const id = typeof rawField.id === 'string' ? rawField.id : null
|
||||
const customFieldId = typeof rawField.customFieldId === 'string' ? rawField.customFieldId : id
|
||||
const customFieldValueId = typeof rawField.customFieldValueId === 'string'
|
||||
@@ -756,14 +772,56 @@ const resolveFieldName = (field: any): string => {
|
||||
|
||||
const resolveFieldType = (field: any): string => {
|
||||
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'
|
||||
}
|
||||
|
||||
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 => {
|
||||
if (defaultValue === null || defaultValue === undefined) {
|
||||
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') {
|
||||
const normalized = String(defaultValue).toLowerCase()
|
||||
if (normalized === 'true' || normalized === '1') {
|
||||
@@ -777,6 +835,90 @@ const formatDefaultValue = (type: string, defaultValue: any): string => {
|
||||
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) => {
|
||||
return Array.isArray(structure?.customFields) ? structure.customFields : []
|
||||
}
|
||||
|
||||
@@ -104,32 +104,43 @@
|
||||
{{ selectedType.description || 'Ce squelette définit la structure et les contraintes du composant.' }}
|
||||
</p>
|
||||
</div>
|
||||
<span class="badge badge-outline">{{ formatStructurePreview(selectedType.structure) }}</span>
|
||||
<span class="badge badge-outline">{{ formatStructurePreview(selectedTypeStructure) }}</span>
|
||||
</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">
|
||||
Consulter le détail du squelette
|
||||
</summary>
|
||||
<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>
|
||||
<ul class="list-disc list-inside space-y-1">
|
||||
<ul class="space-y-2">
|
||||
<li
|
||||
v-for="field in getStructureCustomFields(selectedType.structure)"
|
||||
:key="field.key || field.name"
|
||||
v-for="field in getStructureCustomFields(selectedTypeStructure)"
|
||||
: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>
|
||||
<span v-if="field.value !== undefined && field.value !== null"> : {{ field.value }}</span>
|
||||
<p class="font-medium text-sm text-base-content">
|
||||
{{ 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>
|
||||
</ul>
|
||||
</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>
|
||||
<ul class="list-disc list-inside space-y-1">
|
||||
<li
|
||||
v-for="(piece, index) in getStructurePieces(selectedType.structure)"
|
||||
v-for="(piece, index) in getStructurePieces(selectedTypeStructure)"
|
||||
:key="piece.role || piece.typePieceId || piece.familyCode || index"
|
||||
>
|
||||
{{ resolvePieceLabel(piece) }}
|
||||
@@ -137,11 +148,11 @@
|
||||
</ul>
|
||||
</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>
|
||||
<ul class="list-disc list-inside space-y-1">
|
||||
<li
|
||||
v-for="(subcomponent, index) in getStructureSubcomponents(selectedType.structure)"
|
||||
v-for="(subcomponent, index) in getStructureSubcomponents(selectedTypeStructure)"
|
||||
:key="subcomponent.alias || subcomponent.typeComposantId || subcomponent.familyCode || index"
|
||||
>
|
||||
{{ resolveSubcomponentLabel(subcomponent) }}
|
||||
@@ -327,7 +338,7 @@ import { usePieces } from '~/composables/usePieces'
|
||||
import { useToast } from '~/composables/useToast'
|
||||
import { useCustomFields } from '~/composables/useCustomFields'
|
||||
import { useDocuments } from '~/composables/useDocuments'
|
||||
import { formatStructurePreview } from '~/shared/modelUtils'
|
||||
import { formatStructurePreview, normalizeStructureForEditor } from '~/shared/modelUtils'
|
||||
import type {
|
||||
ComponentModelPiece,
|
||||
ComponentModelStructure,
|
||||
@@ -422,6 +433,11 @@ const selectedType = computed(() => {
|
||||
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) => {
|
||||
if (!type) {
|
||||
clearCreationForm()
|
||||
@@ -433,8 +449,8 @@ watch(selectedType, (type) => {
|
||||
creationForm.name = type.name
|
||||
}
|
||||
lastSuggestedName.value = creationForm.name
|
||||
customFieldInputs.value = normalizeCustomFieldInputs(type.structure)
|
||||
structureAssignments.value = initializeStructureAssignments(type.structure)
|
||||
customFieldInputs.value = normalizeCustomFieldInputs(selectedTypeStructure.value)
|
||||
structureAssignments.value = initializeStructureAssignments(selectedTypeStructure.value)
|
||||
})
|
||||
|
||||
const extractSubcomponents = (
|
||||
@@ -845,11 +861,10 @@ const normalizeCustomField = (rawField: any): CustomFieldInput | null => {
|
||||
return null
|
||||
}
|
||||
const type = resolveFieldType(rawField)
|
||||
const required = !!rawField.required
|
||||
const options = Array.isArray(rawField.options)
|
||||
? rawField.options.filter((option: unknown): option is string => typeof option === 'string')
|
||||
: []
|
||||
const value = formatDefaultValue(type, rawField.value)
|
||||
const required = resolveRequiredFlag(rawField)
|
||||
const options = resolveOptions(rawField)
|
||||
const defaultSource = resolveDefaultValue(rawField)
|
||||
const value = formatDefaultValue(type, defaultSource)
|
||||
const id = typeof rawField.id === 'string' ? rawField.id : null
|
||||
const customFieldId = typeof rawField.customFieldId === 'string' ? rawField.customFieldId : id
|
||||
const customFieldValueId = typeof rawField.customFieldValueId === 'string'
|
||||
@@ -873,14 +888,56 @@ const resolveFieldName = (field: any): string => {
|
||||
|
||||
const resolveFieldType = (field: any): string => {
|
||||
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'
|
||||
}
|
||||
|
||||
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 => {
|
||||
if (defaultValue === null || defaultValue === undefined) {
|
||||
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') {
|
||||
const normalized = String(defaultValue).toLowerCase()
|
||||
if (normalized === 'true' || normalized === '1') {
|
||||
@@ -894,6 +951,55 @@ const formatDefaultValue = (type: string, defaultValue: any): string => {
|
||||
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) => ({
|
||||
customFieldName: field.name,
|
||||
customFieldType: field.type,
|
||||
|
||||
@@ -699,7 +699,8 @@ const normalizeCustomField = (rawField: any): CustomFieldInput | null => {
|
||||
const options = Array.isArray(rawField.options)
|
||||
? 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 customFieldId = typeof rawField.customFieldId === 'string' ? rawField.customFieldId : id
|
||||
const customFieldValueId = typeof rawField.customFieldValueId === 'string'
|
||||
@@ -728,10 +729,46 @@ const resolveFieldType = (field: any): string => {
|
||||
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 => {
|
||||
if (defaultValue === null || defaultValue === undefined) {
|
||||
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') {
|
||||
const normalized = String(defaultValue).toLowerCase()
|
||||
if (normalized === 'true' || normalized === '1') {
|
||||
|
||||
@@ -494,7 +494,8 @@ const normalizeCustomField = (rawField: any): CustomFieldInput | null => {
|
||||
const options = Array.isArray(rawField.options)
|
||||
? 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 customFieldId = typeof rawField.customFieldId === 'string' ? rawField.customFieldId : id
|
||||
const customFieldValueId = typeof rawField.customFieldValueId === 'string'
|
||||
@@ -523,10 +524,46 @@ const resolveFieldType = (field: any): string => {
|
||||
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 => {
|
||||
if (defaultValue === null || defaultValue === undefined) {
|
||||
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') {
|
||||
const normalized = String(defaultValue).toLowerCase()
|
||||
if (normalized === 'true' || normalized === '1') {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
createEmptyComponentModelStructure,
|
||||
type ComponentModelCustomFieldType,
|
||||
type ComponentModelCustomField,
|
||||
type ComponentModelPiece,
|
||||
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[] => {
|
||||
if (!Array.isArray(fields)) {
|
||||
return []
|
||||
@@ -68,32 +94,84 @@ const sanitizeCustomFields = (fields: any[]): ComponentModelCustomField[] => {
|
||||
|
||||
return fields
|
||||
.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) {
|
||||
return null
|
||||
}
|
||||
|
||||
const type = typeof field?.type === 'string' && field.type ? field.type : 'text'
|
||||
const required = !!field?.required
|
||||
const valueObject = extractFieldValueObject(field)
|
||||
|
||||
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
|
||||
if (type === 'select') {
|
||||
const rawOptions = typeof field?.optionsText === 'string'
|
||||
? field.optionsText
|
||||
: Array.isArray(field?.options)
|
||||
? field.options.join('\n')
|
||||
: ''
|
||||
const parsed = rawOptions
|
||||
.split(/\r?\n/)
|
||||
.map((option) => option.trim())
|
||||
.filter((option) => option.length > 0)
|
||||
options = parsed.length > 0 ? parsed : undefined
|
||||
options =
|
||||
toStringArray(valueObject?.options) ||
|
||||
toStringArray((valueObject as any)?.choices) ||
|
||||
toStringArray(field?.options)
|
||||
|
||||
if (!options && typeof field?.optionsText === 'string') {
|
||||
const parsedFromText = field.optionsText
|
||||
.split(/\r?\n/)
|
||||
.map((option) => option.trim())
|
||||
.filter((option) => option.length > 0)
|
||||
options = parsedFromText.length ? parsedFromText : undefined
|
||||
}
|
||||
}
|
||||
|
||||
const result: ComponentModelCustomField = { name, type, required }
|
||||
if (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
|
||||
})
|
||||
.filter((field): field is ComponentModelCustomField => !!field)
|
||||
@@ -207,13 +285,142 @@ const sanitizeSubcomponents = (components: any[]): ComponentModelStructureNode[]
|
||||
.filter((component): component is ComponentModelStructureNode => !!component)
|
||||
}
|
||||
|
||||
export const normalizeStructureForSave = (input: any): ComponentModelStructure => {
|
||||
export const normalizeStructureForEditor = (input: any): ComponentModelStructure => {
|
||||
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 = {
|
||||
customFields: sanitizeCustomFields(source.customFields),
|
||||
customFields: customFields as ComponentModelCustomField[],
|
||||
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
|
||||
@@ -224,13 +431,83 @@ const hydrateCustomFields = (fields: any[]): any[] => {
|
||||
return []
|
||||
}
|
||||
|
||||
return fields.map((field) => ({
|
||||
name: field?.name ?? '',
|
||||
type: field?.type ?? 'text',
|
||||
required: !!field?.required,
|
||||
options: Array.isArray(field?.options) ? field.options : [],
|
||||
optionsText: Array.isArray(field?.options) ? field.options.join('\n') : (field?.optionsText ?? ''),
|
||||
}))
|
||||
return fields.map((field) => {
|
||||
const valueObject = extractFieldValueObject(field)
|
||||
const name = typeof field?.name === 'string'
|
||||
? field.name
|
||||
: typeof field?.key === 'string'
|
||||
? 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[] => {
|
||||
@@ -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[]) => {
|
||||
if (!Array.isArray(fields)) {
|
||||
return []
|
||||
}
|
||||
return fields.map((field) => ({
|
||||
name: field?.name ?? '',
|
||||
type: field?.type ?? 'text',
|
||||
required: !!field?.required,
|
||||
options: Array.isArray(field?.options) ? field.options : [],
|
||||
optionsText: toOptionsText(field),
|
||||
}))
|
||||
return hydrateCustomFields(fields).map((field) => {
|
||||
const defaultValue =
|
||||
field?.defaultValue !== undefined && field?.defaultValue !== null && field?.defaultValue !== ''
|
||||
? field.defaultValue
|
||||
: null
|
||||
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[] => {
|
||||
@@ -352,7 +631,7 @@ export const extractStructureFromComponent = (component: any) => {
|
||||
alias: component?.alias ?? component?.name ?? '',
|
||||
}
|
||||
|
||||
return normalizeStructureForSave(raw)
|
||||
return normalizeStructureForEditor(raw)
|
||||
}
|
||||
|
||||
export const computeStructureStats = (structure: any): ModelStructurePreview => {
|
||||
@@ -518,7 +797,11 @@ const hydratePieceCustomFields = (fields: any[]): PieceModelStructureEditorField
|
||||
type: field?.type ?? 'text',
|
||||
required: !!field?.required,
|
||||
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')
|
||||
: '',
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,10 @@ export interface ComponentModelCustomField {
|
||||
type: ComponentModelCustomFieldType
|
||||
required: boolean
|
||||
options?: string[]
|
||||
defaultValue?: string | null
|
||||
optionsText?: string
|
||||
id?: string
|
||||
customFieldId?: string
|
||||
}
|
||||
|
||||
export interface ComponentModelPiece {
|
||||
|
||||
Reference in New Issue
Block a user