add sub componet in catego ske
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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') {
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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 : []
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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') {
|
||||||
|
|||||||
@@ -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') {
|
||||||
|
|||||||
@@ -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')
|
||||||
|
: '',
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user