From 9f7dd12b34dd881bf8a0c54231d5c48c4b7f86de Mon Sep 17 00:00:00 2001 From: Matthieu Date: Mon, 9 Feb 2026 15:58:36 +0100 Subject: [PATCH] perf(edit-pages) : reduce blocking API calls on edit pages - Remove redundant getCustomFieldValuesByEntity() calls (use entity response) - Remove redundant refreshDocuments() from onMounted (docs already in entity) - Make loadHistory() non-blocking (fire-and-forget) - Defer bulk catalog loads on component edit (pieces/products/composants) - Use pieceTypes cache instead of separate getModelType() call on piece edit - Try embedded typeProduct from entity response on product edit Co-Authored-By: Claude Opus 4.6 --- app/pages/component/[id]/edit.vue | 25 +++++++++--------- app/pages/pieces/[id]/edit.vue | 39 ++++++++++++++++----------- app/pages/product/[id]/edit.vue | 44 ++++++++++++++++++++----------- 3 files changed, 64 insertions(+), 44 deletions(-) diff --git a/app/pages/component/[id]/edit.vue b/app/pages/component/[id]/edit.vue index d073099..310530e 100644 --- a/app/pages/component/[id]/edit.vue +++ b/app/pages/component/[id]/edit.vue @@ -576,7 +576,7 @@ const { updateComposant, loadComposants, composants: componentCatalogRef } = use const { pieces, loadPieces } = usePieces() const { products, loadProducts } = useProducts() const { ensureConstructeurs } = useConstructeurs() -const { upsertCustomFieldValue, updateCustomFieldValue, getCustomFieldValuesByEntity } = useCustomFields() +const { upsertCustomFieldValue, updateCustomFieldValue } = useCustomFields() const toast = useToast() const { loadDocumentsByComponent, uploadDocuments, deleteDocument } = useDocuments() const { @@ -764,12 +764,10 @@ const fetchComponent = async () => { component.value = result.data componentDocuments.value = Array.isArray(result.data?.documents) ? result.data.documents : [] - const customValues = await getCustomFieldValuesByEntity('composant', result.data.id) - if (customValues.success && Array.isArray(customValues.data)) { - component.value.customFieldValues = customValues.data - refreshCustomFieldInputs(undefined, customValues.data) - } - await loadHistory(result.data.id) + const customValues = Array.isArray(result.data?.customFieldValues) ? result.data.customFieldValues : [] + refreshCustomFieldInputs(undefined, customValues) + + loadHistory(result.data.id).catch(() => {}) } else { component.value = null componentDocuments.value = [] @@ -1130,14 +1128,15 @@ onMounted(async () => { loadComponentTypes(), loadPieceTypes(), loadProductTypes(), - loadPieces({ itemsPerPage: 500 }), - loadProducts({ itemsPerPage: 500, force: true }), - loadComposants({ itemsPerPage: 500 }), fetchComponent(), ]) loading.value = false - if (component.value?.id) { - await refreshDocuments() - } + + // Defer bulk catalog loads — not needed for initial render + Promise.allSettled([ + loadPieces({ itemsPerPage: 500 }), + loadProducts({ itemsPerPage: 500 }), + loadComposants({ itemsPerPage: 500 }), + ]).catch(() => {}) }) diff --git a/app/pages/pieces/[id]/edit.vue b/app/pages/pieces/[id]/edit.vue index 5480b2d..b9a357d 100644 --- a/app/pages/pieces/[id]/edit.vue +++ b/app/pages/pieces/[id]/edit.vue @@ -516,7 +516,7 @@ const router = useRouter() const { get } = useApi() const { pieceTypes, loadPieceTypes } = usePieceTypes() const { updatePiece } = usePieces() -const { upsertCustomFieldValue, updateCustomFieldValue, getCustomFieldValuesByEntity } = useCustomFields() +const { upsertCustomFieldValue, updateCustomFieldValue } = useCustomFields() const toast = useToast() const { loadDocumentsByPiece, uploadDocuments, deleteDocument } = useDocuments() const { ensureConstructeurs } = useConstructeurs() @@ -750,20 +750,23 @@ const fetchPiece = async () => { if (result.success) { piece.value = result.data pieceDocuments.value = Array.isArray(result.data?.documents) ? result.data.documents : [] - const customValues = await getCustomFieldValuesByEntity('piece', result.data.id) - if (customValues.success && Array.isArray(customValues.data)) { - piece.value.customFieldValues = customValues.data - refreshCustomFieldInputs(undefined, customValues.data) - } - await loadPieceTypeDetails(result.data) - await loadHistory(result.data.id) + + // Use customFieldValues from entity response (enriched with customField definitions via serialization groups) + const customValues = Array.isArray(result.data?.customFieldValues) ? result.data.customFieldValues : [] + refreshCustomFieldInputs(undefined, customValues) + + // Use cached type from loadPieceTypes() instead of separate getModelType() call + loadPieceTypeDetailsFromCache(result.data) + + // History is non-blocking — template handles its own loading state + loadHistory(result.data.id).catch(() => {}) } else { piece.value = null pieceDocuments.value = [] } } -const loadPieceTypeDetails = async (currentPiece: any) => { +const loadPieceTypeDetailsFromCache = (currentPiece: any) => { const typeId = currentPiece?.typePieceId || extractRelationId(currentPiece?.typePiece) || '' @@ -771,15 +774,22 @@ const loadPieceTypeDetails = async (currentPiece: any) => { pieceTypeDetails.value = null return } - try { - const type = await getModelType(typeId) + // Look up in the already-loaded pieceTypes cache (from loadPieceTypes in onMounted) + const cachedType = (pieceTypes.value || []).find((t: any) => t.id === typeId) ?? null + if (cachedType) { + pieceTypeDetails.value = cachedType + refreshCustomFieldInputs((cachedType.structure as PieceModelStructure | null) ?? null, currentPiece?.customFieldValues ?? null) + return + } + // Fallback: fetch if not in cache (edge case) + getModelType(typeId).then((type) => { if (type && typeof type === 'object') { pieceTypeDetails.value = type refreshCustomFieldInputs((type.structure as PieceModelStructure | null) ?? null, currentPiece?.customFieldValues ?? null) } - } catch (_error) { + }).catch(() => { pieceTypeDetails.value = null - } + }) } let initialized = false @@ -920,8 +930,5 @@ const submitEdition = async () => { onMounted(async () => { await Promise.allSettled([loadPieceTypes(), fetchPiece()]) loading.value = false - if (piece.value?.id) { - await refreshDocuments() - } }) diff --git a/app/pages/product/[id]/edit.vue b/app/pages/product/[id]/edit.vue index bb8d89a..3ebac0b 100644 --- a/app/pages/product/[id]/edit.vue +++ b/app/pages/product/[id]/edit.vue @@ -428,7 +428,7 @@ const route = useRoute() const router = useRouter() const toast = useToast() const { getProduct, updateProduct } = useProducts() -const { upsertCustomFieldValue, updateCustomFieldValue, getCustomFieldValuesByEntity } = useCustomFields() +const { upsertCustomFieldValue, updateCustomFieldValue } = useCustomFields() const { loadDocumentsByProduct, uploadDocuments: uploadProductDocuments, @@ -520,15 +520,17 @@ const loadProduct = async () => { if (result.success && result.data) { product.value = result.data productDocuments.value = Array.isArray(result.data.documents) ? result.data.documents : [] + await loadProductType() - const customValues = await getCustomFieldValuesByEntity('product', result.data.id) - if (customValues.success && Array.isArray(customValues.data)) { - product.value.customFieldValues = customValues.data - refreshCustomFieldInputs(undefined, customValues.data) - } - await hydrateForm() - await refreshDocuments() - await loadHistory(result.data.id) + + // Use customFieldValues from entity response (enriched with customField definitions via serialization groups) + const customValues = Array.isArray(result.data?.customFieldValues) ? result.data.customFieldValues : [] + refreshCustomFieldInputs(undefined, customValues) + + hydrateForm() + + // History is non-blocking — template handles its own loading state + loadHistory(result.data.id).catch(() => {}) } else { product.value = null } @@ -587,9 +589,20 @@ const handleFilesAdded = async (files: File[]) => { } const loadProductType = async () => { + // Try using the expanded typeProduct from entity response first + const embedded = product.value?.typeProduct + if (embedded && typeof embedded === 'object' && embedded.id) { + const embeddedStructure = embedded.structure ?? embedded.productSkeleton ?? null + if (embeddedStructure) { + productType.value = embedded + structure.value = normalizeProductStructureForSave(embeddedStructure) + return + } + } + if (!product.value?.typeProductId) { - productType.value = product.value?.typeProduct ?? null - structure.value = normalizeProductStructureForSave(productType.value?.structure ?? null) + productType.value = embedded ?? null + structure.value = normalizeProductStructureForSave(embedded?.structure ?? null) return } try { @@ -598,12 +611,12 @@ const loadProductType = async () => { structure.value = normalizeProductStructureForSave(type?.structure ?? type?.productSkeleton ?? null) } catch (error) { console.error('Erreur lors du chargement du type de produit:', error) - productType.value = product.value?.typeProduct ?? null - structure.value = normalizeProductStructureForSave(productType.value?.structure ?? null) + productType.value = embedded ?? null + structure.value = normalizeProductStructureForSave(embedded?.structure ?? null) } } -const hydrateForm = async () => { +const hydrateForm = () => { if (!product.value) { return } @@ -618,7 +631,8 @@ const hydrateForm = async () => { : '' refreshCustomFieldInputs(structure.value, product.value.customFieldValues) if (editionForm.constructeurIds.length) { - await ensureConstructeurs(editionForm.constructeurIds) + // Smart-cached + deduped — fire-and-forget, ConstructeurSelect handles its own loading + ensureConstructeurs(editionForm.constructeurIds).catch(() => {}) } }