feat: add API optimizations, cache invalidation and comprehensive test suite
- Add abort controllers and request deduplication to composables - Add entity type cache invalidation on create/update/delete flows - Add 179 new tests (utilities, services, composables, components) - Fix Vue runtime warnings in structure editors Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -80,9 +80,9 @@ export function useCategoryEditGuard (config: GuardConfig) {
|
||||
return ''
|
||||
}
|
||||
if (linkedCount.value === 1) {
|
||||
return `Mode restreint : 1 ${config.labels.singular} est déjà lié à cette catégorie. Vous pouvez ajouter de nouveaux champs personnalisés, mais pas modifier ou supprimer les existants.`
|
||||
return `Mode restreint : 1 ${config.labels.singular} est déjà lié à cette catégorie. Vous pouvez ajouter de nouveaux champs personnalisés et renommer les existants, mais pas modifier leur type ou les supprimer.`
|
||||
}
|
||||
return `Mode restreint : ${linkedCount.value} ${config.labels.plural} sont déjà liés à cette catégorie. Vous pouvez ajouter de nouveaux champs personnalisés, mais pas modifier ou supprimer les existants.`
|
||||
return `Mode restreint : ${linkedCount.value} ${config.labels.plural} sont déjà liés à cette catégorie. Vous pouvez ajouter de nouveaux champs personnalisés et renommer les existants, mais pas modifier leur type ou les supprimer.`
|
||||
})
|
||||
|
||||
const submitBlockMessage = computed(() => {
|
||||
|
||||
@@ -18,6 +18,7 @@ interface ConstructeurResult {
|
||||
|
||||
const constructeurs = ref<Constructeur[]>([])
|
||||
const loading = ref(false)
|
||||
const loaded = ref(false)
|
||||
|
||||
const uniqueConstructeurs = (items: Constructeur[] = []): Constructeur[] => {
|
||||
const map = new Map<string, Constructeur>()
|
||||
@@ -59,7 +60,10 @@ export function useConstructeurs() {
|
||||
const { get, post, patch, delete: del } = useApi()
|
||||
const { showSuccess, showError } = useToast()
|
||||
|
||||
const loadConstructeurs = async (search = ''): Promise<ConstructeurResult> => {
|
||||
const loadConstructeurs = async (search = '', options: { force?: boolean } = {}): Promise<ConstructeurResult> => {
|
||||
if (!search && !options.force && loaded.value) {
|
||||
return { success: true, data: constructeurs.value }
|
||||
}
|
||||
loading.value = true
|
||||
try {
|
||||
const query = search ? `?search=${encodeURIComponent(search)}` : ''
|
||||
@@ -67,6 +71,7 @@ export function useConstructeurs() {
|
||||
if (result.success) {
|
||||
const items = extractCollection(result.data)
|
||||
constructeurs.value = uniqueConstructeurs(items)
|
||||
if (!search) loaded.value = true
|
||||
}
|
||||
return result as ConstructeurResult
|
||||
} catch (error) {
|
||||
|
||||
@@ -50,18 +50,31 @@ const generateCodeFromName = (name: string): string => {
|
||||
}
|
||||
|
||||
// Shared state per category (module-level singletons)
|
||||
const stateByCategory: Record<string, { types: Ref<EntityType[]>; loading: Ref<boolean> }> = {}
|
||||
const stateByCategory: Record<string, { types: Ref<EntityType[]>; loading: Ref<boolean>; loaded: Ref<boolean> }> = {}
|
||||
|
||||
function getOrCreateState(category: ModelCategory) {
|
||||
if (!stateByCategory[category]) {
|
||||
stateByCategory[category] = {
|
||||
types: ref<EntityType[]>([]),
|
||||
loading: ref(false),
|
||||
loaded: ref(false),
|
||||
}
|
||||
}
|
||||
return stateByCategory[category]
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks the cache for a given category as stale.
|
||||
* Next call to `loadTypes()` (without `force`) will refetch from the API.
|
||||
* Safe to call from event handlers (no setup context required).
|
||||
*/
|
||||
export function invalidateEntityTypeCache(category: ModelCategory) {
|
||||
const state = stateByCategory[category]
|
||||
if (state) {
|
||||
state.loaded.value = false
|
||||
}
|
||||
}
|
||||
|
||||
export function useEntityTypes(config: EntityTypeConfig) {
|
||||
const { category, label } = config
|
||||
const { showSuccess, showError } = useToast()
|
||||
@@ -72,7 +85,10 @@ export function useEntityTypes(config: EntityTypeConfig) {
|
||||
description: item.description ?? item.notes ?? null,
|
||||
})
|
||||
|
||||
const loadTypes = async (): Promise<EntityTypeResult> => {
|
||||
const loadTypes = async (options: { force?: boolean } = {}): Promise<EntityTypeResult> => {
|
||||
if (!options.force && state.loaded.value) {
|
||||
return { success: true, data: state.types.value }
|
||||
}
|
||||
state.loading.value = true
|
||||
try {
|
||||
const data = await listModelTypes({
|
||||
@@ -82,6 +98,7 @@ export function useEntityTypes(config: EntityTypeConfig) {
|
||||
limit: 200,
|
||||
})
|
||||
state.types.value = data.items.map(normalizeItem)
|
||||
state.loaded.value = true
|
||||
return { success: true, data: state.types.value }
|
||||
} catch (error) {
|
||||
const err = error as Error & { message?: string }
|
||||
|
||||
@@ -1198,6 +1198,11 @@ export function useMachineDetailData(machineId: string) {
|
||||
syncMachineCustomFields()
|
||||
initMachineFields()
|
||||
|
||||
// Start document loading early (independent of products/links)
|
||||
const documentPromise = !machineDocumentsLoaded.value
|
||||
? refreshMachineDocuments()
|
||||
: Promise.resolve()
|
||||
|
||||
if (!(productInventory.value as AnyRecord[]).length) {
|
||||
try {
|
||||
await loadProducts()
|
||||
@@ -1228,9 +1233,8 @@ export function useMachineDetailData(machineId: string) {
|
||||
|
||||
collapseAllComponents()
|
||||
|
||||
if (!machineDocumentsLoaded.value) {
|
||||
await refreshMachineDocuments()
|
||||
}
|
||||
// Wait for documents if still loading
|
||||
await documentPromise
|
||||
} catch (error) {
|
||||
console.error('Erreur lors du chargement des données:', error)
|
||||
} finally {
|
||||
@@ -1238,10 +1242,12 @@ export function useMachineDetailData(machineId: string) {
|
||||
}
|
||||
}
|
||||
|
||||
const loadInitialData = () => {
|
||||
if (!constructeurs.value.length) loadConstructeurs()
|
||||
if (!componentTypes.value.length) loadComponentTypes()
|
||||
if (!pieceTypes.value.length) loadPieceTypes()
|
||||
const loadInitialData = (): Promise<unknown[]> => {
|
||||
return Promise.all([
|
||||
loadConstructeurs(),
|
||||
loadComponentTypes(),
|
||||
loadPieceTypes(),
|
||||
])
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -24,6 +24,7 @@ export interface MachineType {
|
||||
|
||||
const machineTypes = ref<MachineType[]>([])
|
||||
const loading = ref(false)
|
||||
const loaded = ref(false)
|
||||
|
||||
const normalizeRequirementList = (value: unknown, relationKey: string): MachineTypeRequirement[] => {
|
||||
if (!Array.isArray(value)) {
|
||||
@@ -59,10 +60,11 @@ const normalizeMachineType = (type: Record<string, unknown>): MachineType | null
|
||||
}
|
||||
|
||||
export function useMachineTypesApi() {
|
||||
const { showSuccess, showInfo } = useToast()
|
||||
const { showSuccess } = useToast()
|
||||
const { get, post, put, delete: del } = useApi()
|
||||
|
||||
const loadMachineTypes = async (): Promise<void> => {
|
||||
const loadMachineTypes = async (options: { force?: boolean } = {}): Promise<void> => {
|
||||
if (!options.force && loaded.value) return
|
||||
loading.value = true
|
||||
try {
|
||||
const result = await get('/type_machines')
|
||||
@@ -71,7 +73,7 @@ export function useMachineTypesApi() {
|
||||
machineTypes.value = items
|
||||
.map((item) => normalizeMachineType(item as Record<string, unknown>))
|
||||
.filter((item): item is MachineType => item !== null)
|
||||
showInfo(`Chargement de ${machineTypes.value.length} type(s) de machine réussi`)
|
||||
loaded.value = true
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Erreur lors du chargement des types de machines:', error)
|
||||
|
||||
@@ -17,6 +17,7 @@ export interface Machine {
|
||||
|
||||
const machines = ref<Machine[]>([])
|
||||
const loading = ref(false)
|
||||
const loaded = ref(false)
|
||||
|
||||
const resolveLinkCollection = (source: Record<string, unknown>, keys: string[]): unknown[] | undefined => {
|
||||
if (!source || typeof source !== 'object') {
|
||||
@@ -73,10 +74,11 @@ const normalizeMachineResponse = (payload: unknown): Machine | null => {
|
||||
}
|
||||
|
||||
export function useMachines() {
|
||||
const { showSuccess, showError, showInfo } = useToast()
|
||||
const { showSuccess, showError } = useToast()
|
||||
const { get, post, patch, delete: del } = useApi()
|
||||
|
||||
const loadMachines = async (): Promise<void> => {
|
||||
const loadMachines = async (options: { force?: boolean } = {}): Promise<void> => {
|
||||
if (!options.force && loaded.value) return
|
||||
loading.value = true
|
||||
try {
|
||||
const result = await get('/machines')
|
||||
@@ -86,7 +88,7 @@ export function useMachines() {
|
||||
.map((item) => normalizeMachineResponse(item))
|
||||
.filter((item): item is Machine => item !== null)
|
||||
machines.value = normalized
|
||||
showInfo(`Chargement de ${normalized.length} machine(s) réussi`)
|
||||
loaded.value = true
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Erreur lors du chargement des machines:', error)
|
||||
|
||||
@@ -115,8 +115,16 @@ export function useProducts() {
|
||||
itemsPerPage = 30,
|
||||
orderBy = 'name',
|
||||
orderDir = 'asc',
|
||||
force = false,
|
||||
} = options
|
||||
|
||||
if (!force && loaded.value && !search && page === 1) {
|
||||
return {
|
||||
success: true,
|
||||
data: { items: products.value, total: total.value, page, itemsPerPage },
|
||||
}
|
||||
}
|
||||
|
||||
if (loading.value) {
|
||||
return {
|
||||
success: true,
|
||||
|
||||
@@ -24,19 +24,21 @@ interface SiteResult {
|
||||
|
||||
const sites = ref<Site[]>([])
|
||||
const loading = ref(false)
|
||||
const loaded = ref(false)
|
||||
|
||||
export function useSites() {
|
||||
const { showSuccess, showInfo } = useToast()
|
||||
const { showSuccess } = useToast()
|
||||
const { get, post, patch, delete: del } = useApi()
|
||||
|
||||
const loadSites = async (): Promise<void> => {
|
||||
const loadSites = async (options: { force?: boolean } = {}): Promise<void> => {
|
||||
if (!options.force && loaded.value) return
|
||||
loading.value = true
|
||||
try {
|
||||
const result = await get('/sites')
|
||||
if (result.success) {
|
||||
const collection = extractCollection(result.data)
|
||||
sites.value = collection
|
||||
showInfo(`Chargement de ${collection.length} site(s) réussi`)
|
||||
loaded.value = true
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Erreur lors du chargement des sites:', error)
|
||||
|
||||
Reference in New Issue
Block a user