refactor(frontend) : extract StructureNodeEditor logic into composable
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -356,26 +356,13 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import IconLucidePlus from '~icons/lucide/plus'
|
||||
import IconLucideTrash from '~icons/lucide/trash'
|
||||
import IconLucideGripVertical from '~icons/lucide/grip-vertical'
|
||||
import type { ComponentModelPiece, ComponentModelProduct, ComponentModelStructureNode } from '~/shared/types/inventory'
|
||||
import type { EditableStructureNode, ModelTypeOption } from '~/composables/useStructureNodeLogic'
|
||||
|
||||
defineOptions({ name: 'StructureNodeEditor' })
|
||||
|
||||
type ModelTypeOption = {
|
||||
id: string
|
||||
name: string
|
||||
code?: string | null
|
||||
}
|
||||
|
||||
type EditableStructureNode = ComponentModelStructureNode & {
|
||||
customFields?: any[]
|
||||
pieces?: ComponentModelPiece[]
|
||||
products?: ComponentModelProduct[]
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
node: EditableStructureNode
|
||||
depth?: number
|
||||
@@ -405,522 +392,60 @@ const props = withDefaults(defineProps<{
|
||||
|
||||
const emit = defineEmits(['remove'])
|
||||
|
||||
const initialCustomFieldIndices = ref<Set<number>>(new Set())
|
||||
const initialPieceIndices = ref<Set<number>>(new Set())
|
||||
const initialProductIndices = ref<Set<number>>(new Set())
|
||||
const initialSubcomponentIndices = ref<Set<number>>(new Set())
|
||||
|
||||
const initializeLockedIndices = () => {
|
||||
if (props.restrictedMode) {
|
||||
const customFieldsLength = Array.isArray(props.node.customFields) ? props.node.customFields.length : 0
|
||||
const piecesLength = Array.isArray(props.node.pieces) ? props.node.pieces.length : 0
|
||||
const productsLength = Array.isArray(props.node.products) ? props.node.products.length : 0
|
||||
const subcomponentsLength = Array.isArray(props.node.subcomponents) ? props.node.subcomponents.length : 0
|
||||
|
||||
initialCustomFieldIndices.value = new Set(Array.from({ length: customFieldsLength }, (_, i) => i))
|
||||
initialPieceIndices.value = new Set(Array.from({ length: piecesLength }, (_, i) => i))
|
||||
initialProductIndices.value = new Set(Array.from({ length: productsLength }, (_, i) => i))
|
||||
initialSubcomponentIndices.value = new Set(Array.from({ length: subcomponentsLength }, (_, i) => i))
|
||||
}
|
||||
}
|
||||
|
||||
initializeLockedIndices()
|
||||
|
||||
const isCustomFieldLocked = (index: number): boolean => {
|
||||
return props.restrictedMode === true && initialCustomFieldIndices.value.has(index)
|
||||
}
|
||||
|
||||
const isPieceLocked = (index: number): boolean => {
|
||||
return props.restrictedMode === true && initialPieceIndices.value.has(index)
|
||||
}
|
||||
|
||||
const isProductLocked = (index: number): boolean => {
|
||||
return props.restrictedMode === true && initialProductIndices.value.has(index)
|
||||
}
|
||||
|
||||
const isSubcomponentLocked = (index: number): boolean => {
|
||||
return props.restrictedMode === true && initialSubcomponentIndices.value.has(index)
|
||||
}
|
||||
|
||||
const isLocked = computed(() => props.isLocked === true)
|
||||
const restrictedMode = computed(() => props.restrictedMode === true)
|
||||
|
||||
const componentTypes = computed(() => props.componentTypes ?? [])
|
||||
const pieceTypes = computed(() => props.pieceTypes ?? [])
|
||||
const productTypes = computed(() => props.productTypes ?? [])
|
||||
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 = currentDepth.value
|
||||
const index = Math.min(level, depthClasses.length - 1)
|
||||
return level === 0 ? 'space-y-4' : `${depthClasses[index]} space-y-4`
|
||||
})
|
||||
|
||||
const headingClass = computed(() => (props.isRoot ? 'text-sm font-semibold' : 'text-xs font-semibold'))
|
||||
const lockedTypeDisplay = computed(() => {
|
||||
if (props.lockedTypeLabel) {
|
||||
return props.lockedTypeLabel
|
||||
}
|
||||
return getComponentTypeLabel(props.node?.typeComposantId) || 'Famille non définie'
|
||||
})
|
||||
|
||||
const formatModelTypeOption = (type: ModelTypeOption | undefined | null) =>
|
||||
type?.name ?? ''
|
||||
|
||||
const componentTypeMap = computed(() => {
|
||||
const map = new Map<string, ModelTypeOption>()
|
||||
componentTypes.value.forEach((type) => {
|
||||
if (type && typeof type.id === 'string') {
|
||||
map.set(type.id, type)
|
||||
}
|
||||
})
|
||||
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) => {
|
||||
if (type && typeof type.id === 'string') {
|
||||
map.set(type.id, type)
|
||||
}
|
||||
})
|
||||
return map
|
||||
})
|
||||
|
||||
const productTypeMap = computed(() => {
|
||||
const map = new Map<string, ModelTypeOption>()
|
||||
productTypes.value.forEach((type) => {
|
||||
if (type && typeof type.id === 'string') {
|
||||
map.set(type.id, type)
|
||||
}
|
||||
})
|
||||
return map
|
||||
})
|
||||
|
||||
const getComponentTypeLabel = (id?: string) => {
|
||||
if (!id) return ''
|
||||
return formatModelTypeOption(componentTypeMap.value.get(id))
|
||||
}
|
||||
|
||||
const getPieceTypeLabel = (id?: string) => {
|
||||
if (!id) return ''
|
||||
return formatModelTypeOption(pieceTypeMap.value.get(id))
|
||||
}
|
||||
|
||||
const formatComponentTypeOption = (type: ModelTypeOption | undefined | null) =>
|
||||
formatModelTypeOption(type)
|
||||
|
||||
const formatPieceTypeOption = (type: ModelTypeOption | undefined | null) =>
|
||||
formatModelTypeOption(type)
|
||||
|
||||
const formatProductTypeOption = (type: ModelTypeOption | undefined | null) =>
|
||||
formatModelTypeOption(type)
|
||||
|
||||
const ensureArray = (key: 'customFields' | 'pieces' | 'products' | 'subcomponents') => {
|
||||
if (!Array.isArray((props.node as any)[key])) {
|
||||
if (key === 'subcomponents') {
|
||||
props.node.subcomponents = []
|
||||
} else if (key === 'products') {
|
||||
props.node.products = []
|
||||
} else {
|
||||
(props.node as any)[key] = []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const syncComponentType = (component: EditableStructureNode) => {
|
||||
if (!component) {
|
||||
return
|
||||
}
|
||||
if (props.isRoot) {
|
||||
component.typeComposantId = ''
|
||||
component.typeComposantLabel = ''
|
||||
component.familyCode = ''
|
||||
if (component.alias) {
|
||||
component.alias = ''
|
||||
}
|
||||
return
|
||||
}
|
||||
const id = typeof component.typeComposantId === 'string'
|
||||
? component.typeComposantId
|
||||
: ''
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
const option = componentTypeMap.value.get(id)
|
||||
if (!option) {
|
||||
component.typeComposantLabel = ''
|
||||
component.familyCode = ''
|
||||
return
|
||||
}
|
||||
|
||||
component.typeComposantLabel = formatModelTypeOption(option)
|
||||
component.familyCode = option.code ?? component.familyCode
|
||||
if (!component.alias || component.alias === '' || component.alias === lockedTypeDisplay.value) {
|
||||
component.alias = option.name || component.typeComposantLabel
|
||||
}
|
||||
}
|
||||
|
||||
const updatePieceTypeLabel = (piece: ComponentModelPiece & Record<string, any>) => {
|
||||
if (!piece) return
|
||||
|
||||
if (piece.typePieceId) {
|
||||
const option = pieceTypeMap.value.get(piece.typePieceId)
|
||||
if (option) {
|
||||
piece.typePieceLabel = formatPieceTypeOption(option)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (piece.typePieceLabel) {
|
||||
const normalized = piece.typePieceLabel.trim().toLowerCase()
|
||||
if (normalized) {
|
||||
const match = pieceTypes.value.find((type) => {
|
||||
const formatted = formatPieceTypeOption(type).toLowerCase()
|
||||
const name = (type?.name ?? '').toLowerCase()
|
||||
const code = (type?.code ?? '').toLowerCase()
|
||||
return formatted === normalized || name === normalized || (!!code && code === normalized)
|
||||
})
|
||||
if (match) {
|
||||
piece.typePieceId = match.id
|
||||
piece.typePieceLabel = formatPieceTypeOption(match)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const updateProductTypeLabel = (product: ComponentModelProduct & Record<string, any>) => {
|
||||
if (!product) return
|
||||
|
||||
if (product.typeProductId) {
|
||||
const option = productTypeMap.value.get(product.typeProductId)
|
||||
if (option) {
|
||||
product.typeProductLabel = formatProductTypeOption(option)
|
||||
product.familyCode = option.code ?? product.familyCode ?? ''
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (product.typeProductLabel) {
|
||||
const normalized = product.typeProductLabel.trim().toLowerCase()
|
||||
if (normalized) {
|
||||
const match = productTypes.value.find((type) => {
|
||||
const formatted = formatProductTypeOption(type).toLowerCase()
|
||||
const name = (type?.name ?? '').toLowerCase()
|
||||
const code = (type?.code ?? '').toLowerCase()
|
||||
return formatted === normalized || name === normalized || (!!code && code === normalized)
|
||||
})
|
||||
if (match) {
|
||||
product.typeProductId = match.id
|
||||
product.typeProductLabel = formatProductTypeOption(match)
|
||||
product.familyCode = match.code ?? product.familyCode ?? ''
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const syncPieceLabels = (pieces?: any[]) => {
|
||||
if (!Array.isArray(pieces)) {
|
||||
return
|
||||
}
|
||||
pieces.forEach((piece) => {
|
||||
updatePieceTypeLabel(piece)
|
||||
})
|
||||
}
|
||||
|
||||
const syncProductLabels = (products?: any[]) => {
|
||||
if (!Array.isArray(products)) {
|
||||
return
|
||||
}
|
||||
products.forEach((product) => {
|
||||
updateProductTypeLabel(product)
|
||||
})
|
||||
}
|
||||
|
||||
const handleComponentTypeSelect = (component: any) => {
|
||||
syncComponentType(component)
|
||||
}
|
||||
|
||||
const handlePieceTypeSelect = (piece: ComponentModelPiece & Record<string, any>) => {
|
||||
if (!piece) {
|
||||
return
|
||||
}
|
||||
const id = typeof piece.typePieceId === 'string' ? piece.typePieceId : ''
|
||||
if (!id) {
|
||||
piece.typePieceLabel = ''
|
||||
return
|
||||
}
|
||||
const option = pieceTypeMap.value.get(id)
|
||||
if (!option) {
|
||||
piece.typePieceId = ''
|
||||
piece.typePieceLabel = ''
|
||||
return
|
||||
}
|
||||
piece.typePieceLabel = formatPieceTypeOption(option)
|
||||
}
|
||||
|
||||
const handleProductTypeSelect = (product: ComponentModelProduct & Record<string, any>) => {
|
||||
if (!product) {
|
||||
return
|
||||
}
|
||||
const id = typeof product.typeProductId === 'string' ? product.typeProductId : ''
|
||||
if (!id) {
|
||||
product.typeProductLabel = ''
|
||||
return
|
||||
}
|
||||
const option = productTypeMap.value.get(id)
|
||||
if (!option) {
|
||||
product.typeProductId = ''
|
||||
product.typeProductLabel = ''
|
||||
return
|
||||
}
|
||||
product.typeProductLabel = formatProductTypeOption(option)
|
||||
product.familyCode = option.code ?? product.familyCode ?? ''
|
||||
}
|
||||
|
||||
const reindexCustomFields = () => {
|
||||
if (!Array.isArray(props.node.customFields)) {
|
||||
return
|
||||
}
|
||||
props.node.customFields.forEach((field: any, index: number) => {
|
||||
if (!field || typeof field !== 'object') {
|
||||
return
|
||||
}
|
||||
field.orderIndex = index
|
||||
})
|
||||
}
|
||||
|
||||
const customFieldDrag = useDragReorder(
|
||||
() => props.node.customFields,
|
||||
{ onReorder: reindexCustomFields },
|
||||
)
|
||||
const onCustomFieldDragStart = customFieldDrag.onDragStart
|
||||
const onCustomFieldDragEnter = customFieldDrag.onDragEnter
|
||||
const onCustomFieldDrop = customFieldDrag.onDrop
|
||||
const onCustomFieldDragEnd = customFieldDrag.onDragEnd
|
||||
const customFieldReorderClass = customFieldDrag.reorderClass
|
||||
|
||||
const addCustomField = () => {
|
||||
ensureArray('customFields')
|
||||
const fields = props.node.customFields!
|
||||
const nextIndex = fields.length
|
||||
fields.push({
|
||||
name: '',
|
||||
type: 'text',
|
||||
required: false,
|
||||
optionsText: '',
|
||||
options: [],
|
||||
orderIndex: nextIndex,
|
||||
})
|
||||
reindexCustomFields()
|
||||
}
|
||||
|
||||
const removeCustomField = (index: number) => {
|
||||
if (!Array.isArray(props.node.customFields)) return
|
||||
props.node.customFields.splice(index, 1)
|
||||
reindexCustomFields()
|
||||
}
|
||||
|
||||
const addPiece = () => {
|
||||
ensureArray('pieces')
|
||||
props.node.pieces!.push({
|
||||
typePieceId: '',
|
||||
typePieceLabel: '',
|
||||
reference: '',
|
||||
familyCode: '',
|
||||
role: '',
|
||||
})
|
||||
}
|
||||
|
||||
const removePiece = (index: number) => {
|
||||
if (!Array.isArray(props.node.pieces)) return
|
||||
props.node.pieces.splice(index, 1)
|
||||
}
|
||||
|
||||
const addProduct = () => {
|
||||
ensureArray('products')
|
||||
props.node.products!.push({
|
||||
typeProductId: '',
|
||||
typeProductLabel: '',
|
||||
familyCode: '',
|
||||
})
|
||||
}
|
||||
|
||||
const removeProduct = (index: number) => {
|
||||
if (!Array.isArray(props.node.products)) return
|
||||
props.node.products.splice(index, 1)
|
||||
}
|
||||
|
||||
const addSubComponent = () => {
|
||||
if (!canManageSubcomponents.value) {
|
||||
return
|
||||
}
|
||||
ensureArray('subcomponents')
|
||||
props.node.subcomponents.push({
|
||||
typeComposantId: '',
|
||||
typeComposantLabel: '',
|
||||
modelId: '',
|
||||
familyCode: '',
|
||||
alias: '',
|
||||
subcomponents: [],
|
||||
})
|
||||
}
|
||||
|
||||
const removeSubComponent = (index: number) => {
|
||||
if (!Array.isArray(props.node.subcomponents)) return
|
||||
props.node.subcomponents.splice(index, 1)
|
||||
}
|
||||
|
||||
const pieceDrag = useDragReorder(() => props.node.pieces)
|
||||
const onPieceDragStart = pieceDrag.onDragStart
|
||||
const onPieceDragEnter = pieceDrag.onDragEnter
|
||||
const onPieceDragOver = pieceDrag.onDragOver
|
||||
const onPieceDrop = pieceDrag.onDrop
|
||||
const onPieceDragEnd = pieceDrag.onDragEnd
|
||||
const pieceReorderClass = pieceDrag.reorderClass
|
||||
|
||||
const productDrag = useDragReorder(() => props.node.products)
|
||||
const onProductDragStart = productDrag.onDragStart
|
||||
const onProductDragEnter = productDrag.onDragEnter
|
||||
const onProductDragOver = productDrag.onDragOver
|
||||
const onProductDrop = productDrag.onDrop
|
||||
const onProductDragEnd = productDrag.onDragEnd
|
||||
const productReorderClass = productDrag.reorderClass
|
||||
|
||||
const subcomponentDrag = useDragReorder(
|
||||
() => props.node.subcomponents,
|
||||
{ draggingClass: 'ring-2 ring-primary', dropTargetClass: 'ring-2 ring-primary/70' },
|
||||
)
|
||||
const onSubcomponentDragStart = subcomponentDrag.onDragStart
|
||||
const onSubcomponentDragEnter = subcomponentDrag.onDragEnter
|
||||
const onSubcomponentDragOver = subcomponentDrag.onDragOver
|
||||
const onSubcomponentDrop = subcomponentDrag.onDrop
|
||||
const onSubcomponentDragEnd = subcomponentDrag.onDragEnd
|
||||
const subcomponentReorderClass = subcomponentDrag.reorderClass
|
||||
|
||||
watch(
|
||||
const {
|
||||
isCustomFieldLocked,
|
||||
isPieceLocked,
|
||||
isProductLocked,
|
||||
isSubcomponentLocked,
|
||||
isLocked,
|
||||
restrictedMode,
|
||||
componentTypes,
|
||||
pieceTypes,
|
||||
productTypes,
|
||||
canManageSubcomponents,
|
||||
(allowed) => {
|
||||
if (!allowed && Array.isArray(props.node.subcomponents) && props.node.subcomponents.length) {
|
||||
props.node.subcomponents.splice(0, props.node.subcomponents.length)
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
watch(componentTypes, () => {
|
||||
syncComponentType(props.node)
|
||||
}, { deep: true, immediate: true })
|
||||
|
||||
watch(
|
||||
() => props.node.typeComposantId,
|
||||
() => {
|
||||
syncComponentType(props.node)
|
||||
},
|
||||
)
|
||||
|
||||
watch(pieceTypes, () => {
|
||||
syncPieceLabels(props.node?.pieces)
|
||||
}, { deep: true, immediate: true })
|
||||
|
||||
watch(
|
||||
() => props.node.pieces,
|
||||
(value) => {
|
||||
syncPieceLabels(value)
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
watch(productTypes, () => {
|
||||
syncProductLabels(props.node?.products)
|
||||
}, { deep: true, immediate: true })
|
||||
|
||||
watch(
|
||||
() => props.node.products,
|
||||
(value) => {
|
||||
syncProductLabels(value)
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.node.customFields,
|
||||
(value) => {
|
||||
if (!Array.isArray(value)) {
|
||||
return
|
||||
}
|
||||
value.sort((a: any, b: any) => {
|
||||
const left = typeof a?.orderIndex === 'number' ? a.orderIndex : 0
|
||||
const right = typeof b?.orderIndex === 'number' ? b.orderIndex : 0
|
||||
return left - right
|
||||
})
|
||||
reindexCustomFields()
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
watch(
|
||||
() => [props.lockedTypeLabel, props.lockType],
|
||||
() => {
|
||||
if (props.lockType && props.isRoot) {
|
||||
const label = props.lockedTypeLabel || lockedTypeDisplay.value
|
||||
props.node.typeComposantLabel = label
|
||||
if (label && (!props.node.alias || props.node.alias === lockedTypeDisplay.value)) {
|
||||
props.node.alias = label
|
||||
}
|
||||
if (props.node.typeComposantId) {
|
||||
const option = componentTypeMap.value.get(props.node.typeComposantId)
|
||||
props.node.familyCode = option?.code ?? props.node.familyCode
|
||||
}
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
childAllowSubcomponents,
|
||||
hasSubcomponents,
|
||||
containerClass,
|
||||
headingClass,
|
||||
lockedTypeDisplay,
|
||||
getComponentTypeLabel,
|
||||
getPieceTypeLabel,
|
||||
formatComponentTypeOption,
|
||||
formatPieceTypeOption,
|
||||
formatProductTypeOption,
|
||||
handleComponentTypeSelect,
|
||||
handlePieceTypeSelect,
|
||||
handleProductTypeSelect,
|
||||
addCustomField,
|
||||
removeCustomField,
|
||||
addPiece,
|
||||
removePiece,
|
||||
addProduct,
|
||||
removeProduct,
|
||||
addSubComponent,
|
||||
removeSubComponent,
|
||||
onCustomFieldDragStart,
|
||||
onCustomFieldDragEnter,
|
||||
onCustomFieldDrop,
|
||||
onCustomFieldDragEnd,
|
||||
customFieldReorderClass,
|
||||
onPieceDragStart,
|
||||
onPieceDragEnter,
|
||||
onPieceDragOver,
|
||||
onPieceDrop,
|
||||
onPieceDragEnd,
|
||||
pieceReorderClass,
|
||||
onProductDragStart,
|
||||
onProductDragEnter,
|
||||
onProductDragOver,
|
||||
onProductDrop,
|
||||
onProductDragEnd,
|
||||
productReorderClass,
|
||||
onSubcomponentDragStart,
|
||||
onSubcomponentDragEnter,
|
||||
onSubcomponentDragOver,
|
||||
onSubcomponentDrop,
|
||||
onSubcomponentDragEnd,
|
||||
subcomponentReorderClass,
|
||||
} = useStructureNodeLogic(props)
|
||||
</script>
|
||||
|
||||
205
app/composables/useStructureNodeCrud.ts
Normal file
205
app/composables/useStructureNodeCrud.ts
Normal file
@@ -0,0 +1,205 @@
|
||||
import { ref } from 'vue'
|
||||
import type { EditableStructureNode } from '~/composables/useStructureNodeLogic'
|
||||
|
||||
export interface StructureNodeCrudDeps {
|
||||
node: EditableStructureNode
|
||||
restrictedMode: boolean
|
||||
canManageSubcomponents: () => boolean
|
||||
}
|
||||
|
||||
export function useStructureNodeCrud(props: StructureNodeCrudDeps) {
|
||||
// --- Lock state ---
|
||||
const initialCustomFieldIndices = ref<Set<number>>(new Set())
|
||||
const initialPieceIndices = ref<Set<number>>(new Set())
|
||||
const initialProductIndices = ref<Set<number>>(new Set())
|
||||
const initialSubcomponentIndices = ref<Set<number>>(new Set())
|
||||
|
||||
const initializeLockedIndices = () => {
|
||||
if (props.restrictedMode) {
|
||||
const customFieldsLength = Array.isArray(props.node.customFields) ? props.node.customFields.length : 0
|
||||
const piecesLength = Array.isArray(props.node.pieces) ? props.node.pieces.length : 0
|
||||
const productsLength = Array.isArray(props.node.products) ? props.node.products.length : 0
|
||||
const subcomponentsLength = Array.isArray(props.node.subcomponents) ? props.node.subcomponents.length : 0
|
||||
|
||||
initialCustomFieldIndices.value = new Set(Array.from({ length: customFieldsLength }, (_, i) => i))
|
||||
initialPieceIndices.value = new Set(Array.from({ length: piecesLength }, (_, i) => i))
|
||||
initialProductIndices.value = new Set(Array.from({ length: productsLength }, (_, i) => i))
|
||||
initialSubcomponentIndices.value = new Set(Array.from({ length: subcomponentsLength }, (_, i) => i))
|
||||
}
|
||||
}
|
||||
|
||||
initializeLockedIndices()
|
||||
|
||||
const isCustomFieldLocked = (index: number): boolean => {
|
||||
return props.restrictedMode === true && initialCustomFieldIndices.value.has(index)
|
||||
}
|
||||
|
||||
const isPieceLocked = (index: number): boolean => {
|
||||
return props.restrictedMode === true && initialPieceIndices.value.has(index)
|
||||
}
|
||||
|
||||
const isProductLocked = (index: number): boolean => {
|
||||
return props.restrictedMode === true && initialProductIndices.value.has(index)
|
||||
}
|
||||
|
||||
const isSubcomponentLocked = (index: number): boolean => {
|
||||
return props.restrictedMode === true && initialSubcomponentIndices.value.has(index)
|
||||
}
|
||||
|
||||
// --- Helpers ---
|
||||
const ensureArray = (key: 'customFields' | 'pieces' | 'products' | 'subcomponents') => {
|
||||
if (!Array.isArray((props.node as any)[key])) {
|
||||
if (key === 'subcomponents') {
|
||||
props.node.subcomponents = []
|
||||
} else if (key === 'products') {
|
||||
props.node.products = []
|
||||
} else {
|
||||
(props.node as any)[key] = []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Custom field reindex ---
|
||||
const reindexCustomFields = () => {
|
||||
if (!Array.isArray(props.node.customFields)) {
|
||||
return
|
||||
}
|
||||
props.node.customFields.forEach((field: any, index: number) => {
|
||||
if (!field || typeof field !== 'object') {
|
||||
return
|
||||
}
|
||||
field.orderIndex = index
|
||||
})
|
||||
}
|
||||
|
||||
// --- Drag reorder ---
|
||||
const customFieldDrag = useDragReorder(
|
||||
() => props.node.customFields,
|
||||
{ onReorder: reindexCustomFields },
|
||||
)
|
||||
|
||||
const pieceDrag = useDragReorder(() => props.node.pieces)
|
||||
const productDrag = useDragReorder(() => props.node.products)
|
||||
const subcomponentDrag = useDragReorder(
|
||||
() => props.node.subcomponents,
|
||||
{ draggingClass: 'ring-2 ring-primary', dropTargetClass: 'ring-2 ring-primary/70' },
|
||||
)
|
||||
|
||||
// --- CRUD functions ---
|
||||
const addCustomField = () => {
|
||||
ensureArray('customFields')
|
||||
const fields = props.node.customFields!
|
||||
const nextIndex = fields.length
|
||||
fields.push({
|
||||
name: '',
|
||||
type: 'text',
|
||||
required: false,
|
||||
optionsText: '',
|
||||
options: [],
|
||||
orderIndex: nextIndex,
|
||||
})
|
||||
reindexCustomFields()
|
||||
}
|
||||
|
||||
const removeCustomField = (index: number) => {
|
||||
if (!Array.isArray(props.node.customFields)) return
|
||||
props.node.customFields.splice(index, 1)
|
||||
reindexCustomFields()
|
||||
}
|
||||
|
||||
const addPiece = () => {
|
||||
ensureArray('pieces')
|
||||
props.node.pieces!.push({
|
||||
typePieceId: '',
|
||||
typePieceLabel: '',
|
||||
reference: '',
|
||||
familyCode: '',
|
||||
role: '',
|
||||
})
|
||||
}
|
||||
|
||||
const removePiece = (index: number) => {
|
||||
if (!Array.isArray(props.node.pieces)) return
|
||||
props.node.pieces.splice(index, 1)
|
||||
}
|
||||
|
||||
const addProduct = () => {
|
||||
ensureArray('products')
|
||||
props.node.products!.push({
|
||||
typeProductId: '',
|
||||
typeProductLabel: '',
|
||||
familyCode: '',
|
||||
})
|
||||
}
|
||||
|
||||
const removeProduct = (index: number) => {
|
||||
if (!Array.isArray(props.node.products)) return
|
||||
props.node.products.splice(index, 1)
|
||||
}
|
||||
|
||||
const addSubComponent = () => {
|
||||
if (!props.canManageSubcomponents()) {
|
||||
return
|
||||
}
|
||||
ensureArray('subcomponents')
|
||||
props.node.subcomponents.push({
|
||||
typeComposantId: '',
|
||||
typeComposantLabel: '',
|
||||
modelId: '',
|
||||
familyCode: '',
|
||||
alias: '',
|
||||
subcomponents: [],
|
||||
})
|
||||
}
|
||||
|
||||
const removeSubComponent = (index: number) => {
|
||||
if (!Array.isArray(props.node.subcomponents)) return
|
||||
props.node.subcomponents.splice(index, 1)
|
||||
}
|
||||
|
||||
return {
|
||||
// Lock checks
|
||||
isCustomFieldLocked,
|
||||
isPieceLocked,
|
||||
isProductLocked,
|
||||
isSubcomponentLocked,
|
||||
// Helpers exposed for watchers
|
||||
reindexCustomFields,
|
||||
// CRUD
|
||||
addCustomField,
|
||||
removeCustomField,
|
||||
addPiece,
|
||||
removePiece,
|
||||
addProduct,
|
||||
removeProduct,
|
||||
addSubComponent,
|
||||
removeSubComponent,
|
||||
// Drag reorder — custom fields
|
||||
onCustomFieldDragStart: customFieldDrag.onDragStart,
|
||||
onCustomFieldDragEnter: customFieldDrag.onDragEnter,
|
||||
onCustomFieldDrop: customFieldDrag.onDrop,
|
||||
onCustomFieldDragEnd: customFieldDrag.onDragEnd,
|
||||
customFieldReorderClass: customFieldDrag.reorderClass,
|
||||
// Drag reorder — pieces
|
||||
onPieceDragStart: pieceDrag.onDragStart,
|
||||
onPieceDragEnter: pieceDrag.onDragEnter,
|
||||
onPieceDragOver: pieceDrag.onDragOver,
|
||||
onPieceDrop: pieceDrag.onDrop,
|
||||
onPieceDragEnd: pieceDrag.onDragEnd,
|
||||
pieceReorderClass: pieceDrag.reorderClass,
|
||||
// Drag reorder — products
|
||||
onProductDragStart: productDrag.onDragStart,
|
||||
onProductDragEnter: productDrag.onDragEnter,
|
||||
onProductDragOver: productDrag.onDragOver,
|
||||
onProductDrop: productDrag.onDrop,
|
||||
onProductDragEnd: productDrag.onDragEnd,
|
||||
productReorderClass: productDrag.reorderClass,
|
||||
// Drag reorder — subcomponents
|
||||
onSubcomponentDragStart: subcomponentDrag.onDragStart,
|
||||
onSubcomponentDragEnter: subcomponentDrag.onDragEnter,
|
||||
onSubcomponentDragOver: subcomponentDrag.onDragOver,
|
||||
onSubcomponentDrop: subcomponentDrag.onDrop,
|
||||
onSubcomponentDragEnd: subcomponentDrag.onDragEnd,
|
||||
subcomponentReorderClass: subcomponentDrag.reorderClass,
|
||||
}
|
||||
}
|
||||
462
app/composables/useStructureNodeLogic.ts
Normal file
462
app/composables/useStructureNodeLogic.ts
Normal file
@@ -0,0 +1,462 @@
|
||||
import { computed, watch } from 'vue'
|
||||
import type { ComponentModelPiece, ComponentModelProduct, ComponentModelStructureNode } from '~/shared/types/inventory'
|
||||
import { useStructureNodeCrud } from '~/composables/useStructureNodeCrud'
|
||||
|
||||
export type ModelTypeOption = {
|
||||
id: string
|
||||
name: string
|
||||
code?: string | null
|
||||
}
|
||||
|
||||
export type EditableStructureNode = ComponentModelStructureNode & {
|
||||
customFields?: any[]
|
||||
pieces?: ComponentModelPiece[]
|
||||
products?: ComponentModelProduct[]
|
||||
}
|
||||
|
||||
export interface StructureNodeLogicDeps {
|
||||
node: EditableStructureNode
|
||||
depth: number
|
||||
componentTypes: ModelTypeOption[]
|
||||
pieceTypes: ModelTypeOption[]
|
||||
productTypes: ModelTypeOption[]
|
||||
isRoot: boolean
|
||||
lockType: boolean
|
||||
lockedTypeLabel: string
|
||||
allowSubcomponents: boolean
|
||||
maxSubcomponentDepth: number
|
||||
restrictedMode: boolean
|
||||
isLocked: boolean
|
||||
}
|
||||
|
||||
export function useStructureNodeLogic(props: StructureNodeLogicDeps) {
|
||||
// --- Computed props ---
|
||||
const isLocked = computed(() => props.isLocked === true)
|
||||
const restrictedMode = computed(() => props.restrictedMode === true)
|
||||
|
||||
const componentTypes = computed(() => props.componentTypes ?? [])
|
||||
const pieceTypes = computed(() => props.pieceTypes ?? [])
|
||||
const productTypes = computed(() => props.productTypes ?? [])
|
||||
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 = currentDepth.value
|
||||
const index = Math.min(level, depthClasses.length - 1)
|
||||
return level === 0 ? 'space-y-4' : `${depthClasses[index]} space-y-4`
|
||||
})
|
||||
|
||||
const headingClass = computed(() => (props.isRoot ? 'text-sm font-semibold' : 'text-xs font-semibold'))
|
||||
|
||||
// --- Type maps ---
|
||||
const formatModelTypeOption = (type: ModelTypeOption | undefined | null) =>
|
||||
type?.name ?? ''
|
||||
|
||||
const componentTypeMap = computed(() => {
|
||||
const map = new Map<string, ModelTypeOption>()
|
||||
componentTypes.value.forEach((type) => {
|
||||
if (type && typeof type.id === 'string') {
|
||||
map.set(type.id, type)
|
||||
}
|
||||
})
|
||||
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) => {
|
||||
if (type && typeof type.id === 'string') {
|
||||
map.set(type.id, type)
|
||||
}
|
||||
})
|
||||
return map
|
||||
})
|
||||
|
||||
const productTypeMap = computed(() => {
|
||||
const map = new Map<string, ModelTypeOption>()
|
||||
productTypes.value.forEach((type) => {
|
||||
if (type && typeof type.id === 'string') {
|
||||
map.set(type.id, type)
|
||||
}
|
||||
})
|
||||
return map
|
||||
})
|
||||
|
||||
// --- Label getters ---
|
||||
const getComponentTypeLabel = (id?: string) => {
|
||||
if (!id) return ''
|
||||
return formatModelTypeOption(componentTypeMap.value.get(id))
|
||||
}
|
||||
|
||||
const getPieceTypeLabel = (id?: string) => {
|
||||
if (!id) return ''
|
||||
return formatModelTypeOption(pieceTypeMap.value.get(id))
|
||||
}
|
||||
|
||||
const formatComponentTypeOption = (type: ModelTypeOption | undefined | null) =>
|
||||
formatModelTypeOption(type)
|
||||
|
||||
const formatPieceTypeOption = (type: ModelTypeOption | undefined | null) =>
|
||||
formatModelTypeOption(type)
|
||||
|
||||
const formatProductTypeOption = (type: ModelTypeOption | undefined | null) =>
|
||||
formatModelTypeOption(type)
|
||||
|
||||
const lockedTypeDisplay = computed(() => {
|
||||
if (props.lockedTypeLabel) {
|
||||
return props.lockedTypeLabel
|
||||
}
|
||||
return getComponentTypeLabel(props.node?.typeComposantId) || 'Famille non définie'
|
||||
})
|
||||
|
||||
// --- Sync functions ---
|
||||
const syncComponentType = (component: EditableStructureNode) => {
|
||||
if (!component) {
|
||||
return
|
||||
}
|
||||
if (props.isRoot) {
|
||||
component.typeComposantId = ''
|
||||
component.typeComposantLabel = ''
|
||||
component.familyCode = ''
|
||||
if (component.alias) {
|
||||
component.alias = ''
|
||||
}
|
||||
return
|
||||
}
|
||||
const id = typeof component.typeComposantId === 'string'
|
||||
? component.typeComposantId
|
||||
: ''
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
const option = componentTypeMap.value.get(id)
|
||||
if (!option) {
|
||||
component.typeComposantLabel = ''
|
||||
component.familyCode = ''
|
||||
return
|
||||
}
|
||||
|
||||
component.typeComposantLabel = formatModelTypeOption(option)
|
||||
component.familyCode = option.code ?? component.familyCode
|
||||
if (!component.alias || component.alias === '' || component.alias === lockedTypeDisplay.value) {
|
||||
component.alias = option.name || component.typeComposantLabel
|
||||
}
|
||||
}
|
||||
|
||||
const updatePieceTypeLabel = (piece: ComponentModelPiece & Record<string, any>) => {
|
||||
if (!piece) return
|
||||
|
||||
if (piece.typePieceId) {
|
||||
const option = pieceTypeMap.value.get(piece.typePieceId)
|
||||
if (option) {
|
||||
piece.typePieceLabel = formatPieceTypeOption(option)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (piece.typePieceLabel) {
|
||||
const normalized = piece.typePieceLabel.trim().toLowerCase()
|
||||
if (normalized) {
|
||||
const match = pieceTypes.value.find((type) => {
|
||||
const formatted = formatPieceTypeOption(type).toLowerCase()
|
||||
const name = (type?.name ?? '').toLowerCase()
|
||||
const code = (type?.code ?? '').toLowerCase()
|
||||
return formatted === normalized || name === normalized || (!!code && code === normalized)
|
||||
})
|
||||
if (match) {
|
||||
piece.typePieceId = match.id
|
||||
piece.typePieceLabel = formatPieceTypeOption(match)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const updateProductTypeLabel = (product: ComponentModelProduct & Record<string, any>) => {
|
||||
if (!product) return
|
||||
|
||||
if (product.typeProductId) {
|
||||
const option = productTypeMap.value.get(product.typeProductId)
|
||||
if (option) {
|
||||
product.typeProductLabel = formatProductTypeOption(option)
|
||||
product.familyCode = option.code ?? product.familyCode ?? ''
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (product.typeProductLabel) {
|
||||
const normalized = product.typeProductLabel.trim().toLowerCase()
|
||||
if (normalized) {
|
||||
const match = productTypes.value.find((type) => {
|
||||
const formatted = formatProductTypeOption(type).toLowerCase()
|
||||
const name = (type?.name ?? '').toLowerCase()
|
||||
const code = (type?.code ?? '').toLowerCase()
|
||||
return formatted === normalized || name === normalized || (!!code && code === normalized)
|
||||
})
|
||||
if (match) {
|
||||
product.typeProductId = match.id
|
||||
product.typeProductLabel = formatProductTypeOption(match)
|
||||
product.familyCode = match.code ?? product.familyCode ?? ''
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const syncPieceLabels = (pieces?: any[]) => {
|
||||
if (!Array.isArray(pieces)) {
|
||||
return
|
||||
}
|
||||
pieces.forEach((piece) => {
|
||||
updatePieceTypeLabel(piece)
|
||||
})
|
||||
}
|
||||
|
||||
const syncProductLabels = (products?: any[]) => {
|
||||
if (!Array.isArray(products)) {
|
||||
return
|
||||
}
|
||||
products.forEach((product) => {
|
||||
updateProductTypeLabel(product)
|
||||
})
|
||||
}
|
||||
|
||||
// --- Handler functions ---
|
||||
const handleComponentTypeSelect = (component: any) => {
|
||||
syncComponentType(component)
|
||||
}
|
||||
|
||||
const handlePieceTypeSelect = (piece: ComponentModelPiece & Record<string, any>) => {
|
||||
if (!piece) {
|
||||
return
|
||||
}
|
||||
const id = typeof piece.typePieceId === 'string' ? piece.typePieceId : ''
|
||||
if (!id) {
|
||||
piece.typePieceLabel = ''
|
||||
return
|
||||
}
|
||||
const option = pieceTypeMap.value.get(id)
|
||||
if (!option) {
|
||||
piece.typePieceId = ''
|
||||
piece.typePieceLabel = ''
|
||||
return
|
||||
}
|
||||
piece.typePieceLabel = formatPieceTypeOption(option)
|
||||
}
|
||||
|
||||
const handleProductTypeSelect = (product: ComponentModelProduct & Record<string, any>) => {
|
||||
if (!product) {
|
||||
return
|
||||
}
|
||||
const id = typeof product.typeProductId === 'string' ? product.typeProductId : ''
|
||||
if (!id) {
|
||||
product.typeProductLabel = ''
|
||||
return
|
||||
}
|
||||
const option = productTypeMap.value.get(id)
|
||||
if (!option) {
|
||||
product.typeProductId = ''
|
||||
product.typeProductLabel = ''
|
||||
return
|
||||
}
|
||||
product.typeProductLabel = formatProductTypeOption(option)
|
||||
product.familyCode = option.code ?? product.familyCode ?? ''
|
||||
}
|
||||
|
||||
// --- CRUD & Lock (delegated to useStructureNodeCrud) ---
|
||||
const crud = useStructureNodeCrud({
|
||||
node: props.node,
|
||||
restrictedMode: props.restrictedMode,
|
||||
canManageSubcomponents: () => canManageSubcomponents.value,
|
||||
})
|
||||
|
||||
// --- Watchers ---
|
||||
watch(
|
||||
canManageSubcomponents,
|
||||
(allowed) => {
|
||||
if (!allowed && Array.isArray(props.node.subcomponents) && props.node.subcomponents.length) {
|
||||
props.node.subcomponents.splice(0, props.node.subcomponents.length)
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
watch(componentTypes, () => {
|
||||
syncComponentType(props.node)
|
||||
}, { deep: true, immediate: true })
|
||||
|
||||
watch(
|
||||
() => props.node.typeComposantId,
|
||||
() => {
|
||||
syncComponentType(props.node)
|
||||
},
|
||||
)
|
||||
|
||||
watch(pieceTypes, () => {
|
||||
syncPieceLabels(props.node?.pieces)
|
||||
}, { deep: true, immediate: true })
|
||||
|
||||
watch(
|
||||
() => props.node.pieces,
|
||||
(value) => {
|
||||
syncPieceLabels(value)
|
||||
},
|
||||
{ deep: true },
|
||||
)
|
||||
|
||||
watch(productTypes, () => {
|
||||
syncProductLabels(props.node?.products)
|
||||
}, { deep: true, immediate: true })
|
||||
|
||||
watch(
|
||||
() => props.node.products,
|
||||
(value) => {
|
||||
syncProductLabels(value)
|
||||
},
|
||||
{ deep: true },
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.node.customFields,
|
||||
(value) => {
|
||||
if (!Array.isArray(value)) {
|
||||
return
|
||||
}
|
||||
value.sort((a: any, b: any) => {
|
||||
const left = typeof a?.orderIndex === 'number' ? a.orderIndex : 0
|
||||
const right = typeof b?.orderIndex === 'number' ? b.orderIndex : 0
|
||||
return left - right
|
||||
})
|
||||
crud.reindexCustomFields()
|
||||
},
|
||||
{ deep: true },
|
||||
)
|
||||
|
||||
watch(
|
||||
() => [props.lockedTypeLabel, props.lockType],
|
||||
() => {
|
||||
if (props.lockType && props.isRoot) {
|
||||
const label = props.lockedTypeLabel || lockedTypeDisplay.value
|
||||
props.node.typeComposantLabel = label
|
||||
if (label && (!props.node.alias || props.node.alias === lockedTypeDisplay.value)) {
|
||||
props.node.alias = label
|
||||
}
|
||||
if (props.node.typeComposantId) {
|
||||
const option = componentTypeMap.value.get(props.node.typeComposantId)
|
||||
props.node.familyCode = option?.code ?? props.node.familyCode
|
||||
}
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
return {
|
||||
// Lock checks
|
||||
isCustomFieldLocked: crud.isCustomFieldLocked,
|
||||
isPieceLocked: crud.isPieceLocked,
|
||||
isProductLocked: crud.isProductLocked,
|
||||
isSubcomponentLocked: crud.isSubcomponentLocked,
|
||||
// Computed state
|
||||
isLocked,
|
||||
restrictedMode,
|
||||
componentTypes,
|
||||
pieceTypes,
|
||||
productTypes,
|
||||
canManageSubcomponents,
|
||||
childAllowSubcomponents,
|
||||
hasSubcomponents,
|
||||
containerClass,
|
||||
headingClass,
|
||||
lockedTypeDisplay,
|
||||
// Label getters & formatters
|
||||
getComponentTypeLabel,
|
||||
getPieceTypeLabel,
|
||||
formatComponentTypeOption,
|
||||
formatPieceTypeOption,
|
||||
formatProductTypeOption,
|
||||
// Handlers
|
||||
handleComponentTypeSelect,
|
||||
handlePieceTypeSelect,
|
||||
handleProductTypeSelect,
|
||||
// CRUD
|
||||
addCustomField: crud.addCustomField,
|
||||
removeCustomField: crud.removeCustomField,
|
||||
addPiece: crud.addPiece,
|
||||
removePiece: crud.removePiece,
|
||||
addProduct: crud.addProduct,
|
||||
removeProduct: crud.removeProduct,
|
||||
addSubComponent: crud.addSubComponent,
|
||||
removeSubComponent: crud.removeSubComponent,
|
||||
// Drag reorder — custom fields
|
||||
onCustomFieldDragStart: crud.onCustomFieldDragStart,
|
||||
onCustomFieldDragEnter: crud.onCustomFieldDragEnter,
|
||||
onCustomFieldDrop: crud.onCustomFieldDrop,
|
||||
onCustomFieldDragEnd: crud.onCustomFieldDragEnd,
|
||||
customFieldReorderClass: crud.customFieldReorderClass,
|
||||
// Drag reorder — pieces
|
||||
onPieceDragStart: crud.onPieceDragStart,
|
||||
onPieceDragEnter: crud.onPieceDragEnter,
|
||||
onPieceDragOver: crud.onPieceDragOver,
|
||||
onPieceDrop: crud.onPieceDrop,
|
||||
onPieceDragEnd: crud.onPieceDragEnd,
|
||||
pieceReorderClass: crud.pieceReorderClass,
|
||||
// Drag reorder — products
|
||||
onProductDragStart: crud.onProductDragStart,
|
||||
onProductDragEnter: crud.onProductDragEnter,
|
||||
onProductDragOver: crud.onProductDragOver,
|
||||
onProductDrop: crud.onProductDrop,
|
||||
onProductDragEnd: crud.onProductDragEnd,
|
||||
productReorderClass: crud.productReorderClass,
|
||||
// Drag reorder — subcomponents
|
||||
onSubcomponentDragStart: crud.onSubcomponentDragStart,
|
||||
onSubcomponentDragEnter: crud.onSubcomponentDragEnter,
|
||||
onSubcomponentDragOver: crud.onSubcomponentDragOver,
|
||||
onSubcomponentDrop: crud.onSubcomponentDrop,
|
||||
onSubcomponentDragEnd: crud.onSubcomponentDragEnd,
|
||||
subcomponentReorderClass: crud.subcomponentReorderClass,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user