refactor(frontend) : extract shared components and reduce file sizes

- Extract CustomFieldInputGrid.vue from 6 duplicated template blocks (~70 lines each)
- Extract EntityHistorySection.vue from 3 identical history sections in edit pages
- Extract useDragReorder composable from 4 identical drag-and-drop implementations in StructureNodeEditor (~330 lines → ~30)
- Extract catalogDisplayUtils.ts (resolvePrimaryDocument, resolveSupplierNames, buildSuppliersDisplay)
- Remove redundant computed wrappers (historyEntries, loadingTypes, selectedFiles)
- Remove unused imports (fieldKey, historyActionLabel, formatHistoryDate, *HistoryEntry types)
- Move Intl.DateTimeFormat to module-level in date.ts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-08 02:28:26 +01:00
parent 7b3eb1c5fc
commit de7be1b9d0
18 changed files with 464 additions and 1114 deletions

View File

@@ -63,7 +63,7 @@
<template #cell-preview="{ row }">
<DocumentThumbnail
:document="resolvePrimaryDocument(row.product)"
:document="resolvePrimaryDocument(row.product, true)"
:alt="resolvePreviewAlt(row.product)"
/>
</template>
@@ -147,8 +147,8 @@ import { useProductTypes } from '~/composables/useProductTypes'
import { useToast } from '~/composables/useToast'
import { useDataTable } from '~/composables/useDataTable'
import DocumentThumbnail from '~/components/DocumentThumbnail.vue'
import { isImageDocument, isPdfDocument } from '~/utils/documentPreview'
import { resolveDeleteImpact, buildDeleteMessage } from '~/shared/utils/deleteImpactUtils'
import { resolvePrimaryDocument, resolvePreviewAlt, resolveSupplierNames, buildSuppliersDisplay } from '~/shared/utils/catalogDisplayUtils'
const { canEdit } = usePermissions()
@@ -197,7 +197,7 @@ const productRows = computed(() =>
normalizedProducts.value.map(product => ({
id: product.id,
product,
suppliers: buildSuppliersDisplay(product),
suppliers: buildProductSuppliersDisplay(product),
})),
)
@@ -225,72 +225,8 @@ const formatPrice = (value: any) => {
return Number.isNaN(number) ? '—' : priceFormatter.format(number)
}
const MAX_VISIBLE_SUPPLIERS = 3
const resolveProductSuppliers = (product: Record<string, any>) => {
const names: string[] = []
const seen = new Set<string>()
const pushName = (maybeName: unknown) => {
if (typeof maybeName !== 'string') return
const normalized = maybeName.trim().replace(/\s+/g, ' ')
if (!normalized.length) return
const key = normalized.toLowerCase()
if (seen.has(key)) return
seen.add(key)
names.push(normalized)
}
const collectConstructeurs = (value: unknown): void => {
if (!value) return
if (Array.isArray(value)) { value.forEach(collectConstructeurs); return }
if (typeof value === 'string') { pushName(value); return }
if (typeof value === 'object') {
const record = value as Record<string, any>
pushName(record?.name ?? record?.label ?? record?.companyName ?? record?.company ?? null)
if (record?.constructeur) collectConstructeurs(record.constructeur)
if (Array.isArray(record?.constructeurs)) collectConstructeurs(record.constructeurs)
}
}
const collectFromLabel = (value: unknown): void => {
if (typeof value !== 'string') return
value.split(/[,;\\/•·|]+/).map(part => part.trim()).filter(Boolean).forEach(pushName)
}
collectConstructeurs(product?.constructeurs)
collectConstructeurs(product?.constructeur)
collectFromLabel(product?.constructeursLabel)
collectFromLabel(product?.supplierLabel)
collectFromLabel(product?.suppliers)
return names
}
const buildSuppliersDisplay = (product: Record<string, any>) => {
const suppliers = resolveProductSuppliers(product)
const visible = suppliers.slice(0, MAX_VISIBLE_SUPPLIERS)
const overflow = Math.max(suppliers.length - visible.length, 0)
return { suppliers, visible, overflow, tooltip: suppliers.length ? suppliers.join(', ') : '' }
}
const resolvePrimaryDocument = (product: Record<string, any>) => {
const documents = Array.isArray(product?.documents) ? product.documents : []
if (!documents.length) return null
const normalized = documents.filter((doc: any) => doc && typeof doc === 'object')
const withPath = normalized.filter((doc: any) => doc?.fileUrl || doc?.path)
if (!withPath.length) return normalized[0] ?? null
const images = withPath.filter((doc: any) => isImageDocument(doc))
if (images.length) return images[0]
const pdf = withPath.find((doc: any) => isPdfDocument(doc))
if (pdf) return pdf
return withPath[0]
}
const resolvePreviewAlt = (product: Record<string, any>) => {
const parts = [product?.name, product?.reference].filter(Boolean)
return parts.length ? `Aperçu du document de ${parts.join(' ')}` : 'Aperçu du document'
}
const buildProductSuppliersDisplay = (product: Record<string, any>) =>
buildSuppliersDisplay(resolveSupplierNames(product))
const reload = () => fetchProducts()