Shared module-level loading ref in useComposants caused structureDataLoading to toggle during submission, unmounting the skeleton assignment UI. On remount, watchers cleared selections not found in the limited local catalog. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
381 lines
11 KiB
TypeScript
381 lines
11 KiB
TypeScript
import { computed, ref, watch } from 'vue'
|
|
import { useApi } from '~/composables/useApi'
|
|
import { extractCollection } from '~/shared/utils/apiHelpers'
|
|
import {
|
|
componentOptionDescription,
|
|
componentOptionLabel,
|
|
describePieceRequirement as _describePieceRequirement,
|
|
describeProductRequirement as _describeProductRequirement,
|
|
pieceOptionDescription,
|
|
pieceOptionLabel,
|
|
productOptionDescription,
|
|
productOptionLabel,
|
|
} from '~/shared/utils/structureAssignmentLabels'
|
|
import type {
|
|
ComponentOption,
|
|
PieceOption,
|
|
ProductOption,
|
|
StructureAssignmentNode,
|
|
StructurePieceAssignment,
|
|
StructureProductAssignment,
|
|
} from '~/shared/utils/structureAssignmentLabels'
|
|
|
|
export type {
|
|
ComponentOption,
|
|
PieceOption,
|
|
ProductOption,
|
|
StructureAssignmentNode,
|
|
StructurePieceAssignment,
|
|
StructureProductAssignment,
|
|
} from '~/shared/utils/structureAssignmentLabels'
|
|
|
|
export interface StructureAssignmentFetchDeps {
|
|
assignment: StructureAssignmentNode
|
|
pieces: PieceOption[] | null
|
|
products: ProductOption[] | null
|
|
components: ComponentOption[] | null
|
|
isRoot: () => boolean
|
|
pieceTypeLabelMap: Record<string, string>
|
|
productTypeLabelMap: Record<string, string>
|
|
componentTypeLabelMap: Record<string, string>
|
|
}
|
|
|
|
export function useStructureAssignmentFetch(deps: StructureAssignmentFetchDeps) {
|
|
const { get } = useApi()
|
|
|
|
const pieceOptionsByPath = ref<Record<string, PieceOption[]>>({})
|
|
const productOptionsByPath = ref<Record<string, ProductOption[]>>({})
|
|
const componentOptionsByPath = ref<Record<string, ComponentOption[]>>({})
|
|
const pieceLoadingByPath = ref<Record<string, boolean>>({})
|
|
const productLoadingByPath = ref<Record<string, boolean>>({})
|
|
const componentLoadingByPath = ref<Record<string, boolean>>({})
|
|
|
|
const setLoading = (target: Record<string, boolean>, key: string, value: boolean) => {
|
|
target[key] = value
|
|
}
|
|
|
|
const typeIri = (id: string) => `/api/model_types/${id}`
|
|
const primedPiecePaths = new Set<string>()
|
|
const primedProductPaths = new Set<string>()
|
|
const primedComponentPaths = new Set<string>()
|
|
|
|
// --- Component options ---
|
|
|
|
const componentOptions = computed(() => {
|
|
if (deps.isRoot()) {
|
|
return []
|
|
}
|
|
const cached = componentOptionsByPath.value[deps.assignment.path]
|
|
if (cached) {
|
|
return cached
|
|
}
|
|
const definition = deps.assignment.definition || {}
|
|
const requiredTypeId =
|
|
definition.typeComposantId || definition.modelId || null
|
|
const requiredFamilyCode = definition.familyCode || null
|
|
|
|
return (deps.components || []).filter((component) => {
|
|
if (!component || typeof component !== 'object') {
|
|
return false
|
|
}
|
|
if (requiredTypeId) {
|
|
return component.typeComposantId === requiredTypeId
|
|
}
|
|
if (requiredFamilyCode) {
|
|
return (
|
|
component.typeComposant?.code === requiredFamilyCode
|
|
|| component.typeComposantId === requiredFamilyCode
|
|
)
|
|
}
|
|
return true
|
|
})
|
|
})
|
|
|
|
const fetchComponentOptions = async (term = '') => {
|
|
if (deps.isRoot()) {
|
|
return
|
|
}
|
|
const key = deps.assignment.path
|
|
if (componentLoadingByPath.value[key]) {
|
|
return
|
|
}
|
|
|
|
const definition = deps.assignment.definition || {}
|
|
const requiredTypeId =
|
|
definition.typeComposantId || definition.modelId || definition.typeComposant?.id || null
|
|
|
|
const params = new URLSearchParams()
|
|
params.set('itemsPerPage', '200')
|
|
if (term.trim()) {
|
|
params.set('search', term.trim())
|
|
}
|
|
if (requiredTypeId) {
|
|
params.set('typeComposant', typeIri(requiredTypeId))
|
|
}
|
|
|
|
setLoading(componentLoadingByPath.value, key, true)
|
|
try {
|
|
const result = await get(`/composants?${params.toString()}`)
|
|
if (result.success) {
|
|
componentOptionsByPath.value[key] = extractCollection(result.data)
|
|
}
|
|
}
|
|
finally {
|
|
setLoading(componentLoadingByPath.value, key, false)
|
|
}
|
|
}
|
|
|
|
// --- Piece options ---
|
|
|
|
const getPieceOptions = (assignment: StructurePieceAssignment) => {
|
|
const cached = pieceOptionsByPath.value[assignment.path]
|
|
if (cached) {
|
|
return cached
|
|
}
|
|
const definition = assignment.definition
|
|
const requiredTypeId =
|
|
definition.typePieceId
|
|
|| definition.typePiece?.id
|
|
|| definition.familyCode
|
|
|| null
|
|
|
|
return (deps.pieces || []).filter((piece) => {
|
|
if (!piece || typeof piece !== 'object') {
|
|
return false
|
|
}
|
|
if (!requiredTypeId) {
|
|
return true
|
|
}
|
|
if (definition.typePieceId || definition.typePiece?.id) {
|
|
return (
|
|
piece.typePieceId === requiredTypeId
|
|
|| piece.typePiece?.id === requiredTypeId
|
|
)
|
|
}
|
|
if (definition.familyCode) {
|
|
return (
|
|
piece.typePiece?.code === requiredTypeId
|
|
|| piece.typePieceId === requiredTypeId
|
|
)
|
|
}
|
|
return false
|
|
})
|
|
}
|
|
|
|
const fetchPieceOptions = async (assignment: StructurePieceAssignment, term = '') => {
|
|
const key = assignment.path
|
|
if (pieceLoadingByPath.value[key]) {
|
|
return
|
|
}
|
|
|
|
const definition = assignment.definition || {}
|
|
const requiredTypeId =
|
|
definition.typePieceId || definition.typePiece?.id || null
|
|
|
|
const params = new URLSearchParams()
|
|
params.set('itemsPerPage', '200')
|
|
if (term.trim()) {
|
|
params.set('search', term.trim())
|
|
}
|
|
if (requiredTypeId) {
|
|
params.set('typePiece', typeIri(requiredTypeId))
|
|
}
|
|
|
|
setLoading(pieceLoadingByPath.value, key, true)
|
|
try {
|
|
const result = await get(`/pieces?${params.toString()}`)
|
|
if (result.success) {
|
|
pieceOptionsByPath.value[key] = extractCollection(result.data)
|
|
}
|
|
}
|
|
finally {
|
|
setLoading(pieceLoadingByPath.value, key, false)
|
|
}
|
|
}
|
|
|
|
const describePieceRequirement = (assignment: StructurePieceAssignment) => {
|
|
const options = getPieceOptions(assignment)
|
|
return _describePieceRequirement(assignment, options, deps.pieceTypeLabelMap)
|
|
}
|
|
|
|
// --- Product options ---
|
|
|
|
const getProductOptions = (assignment: StructureProductAssignment) => {
|
|
const cached = productOptionsByPath.value[assignment.path]
|
|
if (cached) {
|
|
return cached
|
|
}
|
|
const definition = assignment.definition
|
|
const requiredTypeId =
|
|
definition.typeProductId
|
|
|| definition.typeProduct?.id
|
|
|| definition.familyCode
|
|
|| null
|
|
|
|
return (deps.products || []).filter((product) => {
|
|
if (!product || typeof product !== 'object') {
|
|
return false
|
|
}
|
|
if (!requiredTypeId) {
|
|
return true
|
|
}
|
|
if (definition.typeProductId || definition.typeProduct?.id) {
|
|
return (
|
|
product.typeProductId === requiredTypeId
|
|
|| product.typeProduct?.id === requiredTypeId
|
|
)
|
|
}
|
|
if (definition.familyCode) {
|
|
return (
|
|
product.typeProduct?.code === requiredTypeId
|
|
|| product.typeProductId === requiredTypeId
|
|
)
|
|
}
|
|
return false
|
|
})
|
|
}
|
|
|
|
const fetchProductOptions = async (assignment: StructureProductAssignment, term = '') => {
|
|
const key = assignment.path
|
|
if (productLoadingByPath.value[key]) {
|
|
return
|
|
}
|
|
|
|
const definition = assignment.definition || {}
|
|
const requiredTypeId =
|
|
definition.typeProductId || definition.typeProduct?.id || null
|
|
|
|
const params = new URLSearchParams()
|
|
params.set('itemsPerPage', '200')
|
|
if (term.trim()) {
|
|
params.set('search', term.trim())
|
|
}
|
|
if (requiredTypeId) {
|
|
params.set('typeProduct', typeIri(requiredTypeId))
|
|
}
|
|
|
|
setLoading(productLoadingByPath.value, key, true)
|
|
try {
|
|
const result = await get(`/products?${params.toString()}`)
|
|
if (result.success) {
|
|
productOptionsByPath.value[key] = extractCollection(result.data)
|
|
}
|
|
}
|
|
finally {
|
|
setLoading(productLoadingByPath.value, key, false)
|
|
}
|
|
}
|
|
|
|
const describeProductRequirement = (assignment: StructureProductAssignment) => {
|
|
const options = getProductOptions(assignment)
|
|
return _describeProductRequirement(assignment, options, deps.productTypeLabelMap)
|
|
}
|
|
|
|
// --- Watchers ---
|
|
|
|
watch(
|
|
componentOptions,
|
|
(options) => {
|
|
if (deps.isRoot()) {
|
|
return
|
|
}
|
|
// Only clear if we have loaded options (cache or catalog); skip when options are empty
|
|
// because the fetch may not have completed yet.
|
|
if (!options.length) {
|
|
return
|
|
}
|
|
const hasMatch = options.some(
|
|
(component) => component.id === deps.assignment.selectedComponentId,
|
|
)
|
|
if (!hasMatch) {
|
|
deps.assignment.selectedComponentId = ''
|
|
}
|
|
},
|
|
{ immediate: true },
|
|
)
|
|
|
|
watch(
|
|
() => [deps.pieces, deps.assignment.pieces],
|
|
() => {
|
|
for (const pieceAssignment of deps.assignment.pieces) {
|
|
const hasCachedOptions = !!pieceOptionsByPath.value[pieceAssignment.path]
|
|
// Only clear selections when we have loaded options (cached or from catalog).
|
|
// When no cache exists, a fetch is about to fire — clearing now would lose
|
|
// user input before the real option list arrives.
|
|
if (hasCachedOptions) {
|
|
const options = getPieceOptions(pieceAssignment)
|
|
if (
|
|
pieceAssignment.selectedPieceId
|
|
&& !options.some((piece) => piece.id === pieceAssignment.selectedPieceId)
|
|
) {
|
|
pieceAssignment.selectedPieceId = ''
|
|
}
|
|
}
|
|
if (!primedPiecePaths.has(pieceAssignment.path) && !pieceOptionsByPath.value[pieceAssignment.path]) {
|
|
primedPiecePaths.add(pieceAssignment.path)
|
|
fetchPieceOptions(pieceAssignment).catch(() => {})
|
|
}
|
|
}
|
|
},
|
|
{ deep: true, immediate: true },
|
|
)
|
|
|
|
watch(
|
|
() => [deps.products, deps.assignment.products],
|
|
() => {
|
|
for (const productAssignment of deps.assignment.products) {
|
|
const hasCachedOptions = !!productOptionsByPath.value[productAssignment.path]
|
|
if (hasCachedOptions) {
|
|
const options = getProductOptions(productAssignment)
|
|
if (
|
|
productAssignment.selectedProductId
|
|
&& !options.some((product) => product.id === productAssignment.selectedProductId)
|
|
) {
|
|
productAssignment.selectedProductId = ''
|
|
}
|
|
}
|
|
if (!primedProductPaths.has(productAssignment.path) && !productOptionsByPath.value[productAssignment.path]) {
|
|
primedProductPaths.add(productAssignment.path)
|
|
fetchProductOptions(productAssignment).catch(() => {})
|
|
}
|
|
}
|
|
},
|
|
{ deep: true, immediate: true },
|
|
)
|
|
|
|
watch(
|
|
() => deps.assignment.definition,
|
|
() => {
|
|
if (deps.isRoot()) {
|
|
return
|
|
}
|
|
const key = deps.assignment.path
|
|
if (!primedComponentPaths.has(key) && !componentOptionsByPath.value[key]) {
|
|
primedComponentPaths.add(key)
|
|
fetchComponentOptions().catch(() => {})
|
|
}
|
|
},
|
|
{ immediate: true },
|
|
)
|
|
|
|
return {
|
|
pieceLoadingByPath,
|
|
productLoadingByPath,
|
|
componentLoadingByPath,
|
|
componentOptions,
|
|
componentOptionLabel,
|
|
componentOptionDescription,
|
|
fetchComponentOptions,
|
|
getPieceOptions,
|
|
pieceOptionLabel,
|
|
pieceOptionDescription,
|
|
fetchPieceOptions,
|
|
describePieceRequirement,
|
|
getProductOptions,
|
|
productOptionLabel,
|
|
productOptionDescription,
|
|
fetchProductOptions,
|
|
describeProductRequirement,
|
|
}
|
|
}
|