Extend ComponentModelPiece/Product with optional typePiece/typeProduct nested objects. Replace 12 'as any' casts in assignment node, convert Promise<any> to Promise<unknown>, use Record<string, unknown> at API boundaries. ~15 casts eliminated. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
275 lines
7.4 KiB
Vue
275 lines
7.4 KiB
Vue
<template>
|
|
<div class="space-y-6">
|
|
<StructureNodeEditor
|
|
:node="localStructure"
|
|
:depth="0"
|
|
:component-types="availableComponentTypes"
|
|
:piece-types="availablePieceTypes"
|
|
:product-types="availableProductTypes"
|
|
:lock-type="lockRootType"
|
|
:locked-type-label="displayedRootTypeLabel"
|
|
:allow-subcomponents="allowSubcomponents"
|
|
:max-subcomponent-depth="maxSubcomponentDepth"
|
|
:restricted-mode="restrictedMode"
|
|
is-root
|
|
/>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { reactive, watch, computed, onMounted, ref } from 'vue'
|
|
import StructureNodeEditor from '~/components/StructureNodeEditor.vue'
|
|
import {
|
|
defaultStructure,
|
|
hydrateStructureForEditor,
|
|
cloneStructure,
|
|
} from '~/shared/modelUtils'
|
|
import { usePieceTypes } from '~/composables/usePieceTypes'
|
|
import { useComponentTypes } from '~/composables/useComponentTypes'
|
|
import { useProductTypes } from '~/composables/useProductTypes'
|
|
import type { ComponentModelStructure } from '~/shared/types/inventory'
|
|
|
|
defineOptions({ name: 'ComponentModelStructureEditor' })
|
|
|
|
const props = defineProps({
|
|
modelValue: {
|
|
type: Object,
|
|
default: () => defaultStructure(),
|
|
},
|
|
rootTypeId: {
|
|
type: String,
|
|
default: '',
|
|
},
|
|
rootTypeLabel: {
|
|
type: String,
|
|
default: '',
|
|
},
|
|
lockRootType: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
allowSubcomponents: {
|
|
type: Boolean,
|
|
default: true,
|
|
},
|
|
maxSubcomponentDepth: {
|
|
type: Number,
|
|
default: Infinity,
|
|
},
|
|
restrictedMode: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
})
|
|
|
|
const emit = defineEmits(['update:modelValue'])
|
|
|
|
const localStructure = reactive<ComponentModelStructure>(hydrateStructureForEditor(props.modelValue))
|
|
const previousLockedLabel = ref(props.rootTypeLabel || '')
|
|
|
|
const { pieceTypes, loadPieceTypes } = usePieceTypes()
|
|
const { componentTypes, loadComponentTypes } = useComponentTypes()
|
|
const { productTypes, loadProductTypes } = useProductTypes()
|
|
|
|
const availablePieceTypes = computed(() => pieceTypes.value ?? [])
|
|
const availableComponentTypes = computed(() => componentTypes.value ?? [])
|
|
const availableProductTypes = computed(() => productTypes.value ?? [])
|
|
const allowSubcomponents = computed(() => props.allowSubcomponents !== false)
|
|
const maxSubcomponentDepth = computed(() =>
|
|
typeof props.maxSubcomponentDepth === 'number' ? props.maxSubcomponentDepth : Infinity,
|
|
)
|
|
|
|
const fallbackRootTypeLabel = computed(() => {
|
|
if (!props.rootTypeId) {
|
|
return ''
|
|
}
|
|
const match = availableComponentTypes.value.find((type) => type?.id === props.rootTypeId)
|
|
return match?.name || ''
|
|
})
|
|
|
|
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: Record<string, 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: Record<string, 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 || ''
|
|
return
|
|
}
|
|
|
|
const newTypeId = props.rootTypeId || ''
|
|
const newLabel = displayedRootTypeLabel.value
|
|
|
|
localStructure.typeComposantId = newTypeId
|
|
localStructure.typeComposantLabel = newLabel
|
|
|
|
const match = availableComponentTypes.value.find((type) => type?.id === newTypeId)
|
|
if (match?.code) {
|
|
localStructure.familyCode = match.code
|
|
}
|
|
|
|
const previousLabel = previousLockedLabel.value
|
|
if (!localStructure.alias || localStructure.alias === previousLabel || localStructure.alias === '') {
|
|
localStructure.alias = newLabel || localStructure.alias
|
|
}
|
|
|
|
previousLockedLabel.value = newLabel
|
|
}
|
|
|
|
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
|
|
localStructure.products = hydrated.products
|
|
localStructure.subcomponents = hydrated.subcomponents
|
|
localStructure.typeComposantId = hydrated.typeComposantId
|
|
localStructure.typeComposantLabel = hydrated.typeComposantLabel
|
|
localStructure.modelId = hydrated.modelId
|
|
localStructure.familyCode = hydrated.familyCode
|
|
localStructure.alias = hydrated.alias
|
|
lastEmitted = incomingSerialized
|
|
syncRootType()
|
|
}
|
|
|
|
watch(
|
|
() => props.modelValue,
|
|
(value) => {
|
|
syncFromProps(value)
|
|
},
|
|
{ deep: true }
|
|
)
|
|
|
|
watch(
|
|
() => [props.rootTypeId, props.rootTypeLabel, props.lockRootType],
|
|
() => {
|
|
syncRootType()
|
|
},
|
|
{ immediate: true }
|
|
)
|
|
|
|
watch(
|
|
availableComponentTypes,
|
|
() => {
|
|
syncRootType()
|
|
}
|
|
)
|
|
|
|
watch(
|
|
localStructure,
|
|
(value) => {
|
|
const payload = prepareStructureForEmit(value)
|
|
const serialized = JSON.stringify(payload)
|
|
if (serialized !== lastEmitted) {
|
|
lastEmitted = serialized
|
|
emit('update:modelValue', payload)
|
|
}
|
|
},
|
|
{ deep: true }
|
|
)
|
|
|
|
onMounted(async () => {
|
|
const loaders: Promise<unknown>[] = []
|
|
if (!availablePieceTypes.value.length) {
|
|
loaders.push(loadPieceTypes())
|
|
}
|
|
if (!availableComponentTypes.value.length) {
|
|
loaders.push(loadComponentTypes())
|
|
}
|
|
if (!availableProductTypes.value.length) {
|
|
loaders.push(loadProductTypes())
|
|
}
|
|
if (loaders.length) {
|
|
await Promise.allSettled(loaders)
|
|
}
|
|
syncRootType()
|
|
})
|
|
|
|
watch(
|
|
allowSubcomponents,
|
|
(allowed) => {
|
|
if (!allowed && Array.isArray(localStructure.subcomponents) && localStructure.subcomponents.length) {
|
|
localStructure.subcomponents = []
|
|
}
|
|
},
|
|
{ immediate: true }
|
|
)
|
|
</script>
|