refactor(frontend) : extract assignment fetch logic into composable

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-08 15:07:40 +01:00
parent 9f9ad80c61
commit e911f169ce
4 changed files with 1322 additions and 513 deletions

View File

@@ -0,0 +1,647 @@
# Reduce Frontend Files Under 500 Lines — Implementation Plan
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** Reduce all 14 frontend files currently over 500 lines to under 500 lines each, without changing any functionality.
**Architecture:** Extract shared UI sections into reusable components, split large composables/utilities into focused modules, and extract page-level script logic into dedicated composables. Each extraction is a pure refactor — no behavior changes.
**Tech Stack:** Vue 3 Composition API, TypeScript, Nuxt 4 (auto-imports for composables and components)
---
## Inventory of files to reduce
| # | File | Lines | Target strategy |
|---|------|------:|-----------------|
| 1 | `composables/useMachineDetailData.ts` | 1353 | Split into 4 focused composables |
| 2 | `components/StructureNodeEditor.vue` | 926 | Extract type-map + sync logic into composable |
| 3 | `pages/component/[id]/edit.vue` | 911 | Extract shared component + composable |
| 4 | `pages/component/create.vue` | 852 | Extract structure assignment helpers |
| 5 | `pages/pieces/[id]/edit.vue` | 821 | Extract page composable |
| 6 | `shared/model/componentStructure.ts` | 794 | Split into 3 focused modules |
| 7 | `components/PieceItem.vue` | 757 | Extract document list + custom fields template |
| 8 | `components/ComponentStructureAssignmentNode.vue` | 722 | Extract fetch/options logic |
| 9 | `pages/index.vue` | 584 | Extract modal components |
| 10 | `components/PieceModelStructureEditor.vue` | 578 | Extract drag-reorder + field logic |
| 11 | `components/model-types/ManagementView.vue` | 577 | Extract related-items modal |
| 12 | `components/ComponentItem.vue` | 573 | Extract document list template |
| 13 | `pages/product/[id]/edit.vue` | 570 | Extract page composable |
| 14 | `pages/pieces/create.vue` | 540 | Extract product-selection logic |
## Shared extractions (do these FIRST — they reduce multiple files)
### Task 1: Extract `DocumentListInline.vue` shared component
**Rationale:** The document list display (thumbnail + name + mimeType + size + Consulter/Télécharger/Supprimer buttons) is duplicated identically in:
- `PieceItem.vue` (lines 401-477)
- `ComponentItem.vue` (lines 312-379)
- `pages/component/[id]/edit.vue` (lines 307-375)
- `pages/pieces/[id]/edit.vue` (lines 254-325)
- `pages/product/[id]/edit.vue` (lines 165-232)
**Files:**
- Create: `app/components/common/DocumentListInline.vue`
- Modify: all 5 files above
**Step 1: Create `DocumentListInline.vue`**
```vue
<template>
<div v-if="documents.length" class="space-y-2">
<div
v-for="document in documents"
:key="document.id || document.path || document.name"
class="flex items-center justify-between rounded border border-base-200 bg-base-100 px-3 py-2"
>
<div class="flex items-center gap-3 text-sm">
<div
class="flex-shrink-0 overflow-hidden rounded-md border border-base-200 bg-base-200/70 flex items-center justify-center"
:class="documentThumbnailClass(document)"
>
<img
v-if="isImageDocument(document) && (document.fileUrl || document.path)"
:src="document.fileUrl || document.path"
class="h-full w-full object-cover"
:alt="`Aperçu de ${document.name}`"
>
<iframe
v-else-if="shouldInlinePdf(document)"
:src="documentPreviewSrc(document)"
class="h-full w-full border-0 bg-white"
title="Aperçu PDF"
/>
<component
v-else
:is="documentIcon(document).component"
class="h-6 w-6"
:class="documentIcon(document).colorClass"
aria-hidden="true"
/>
</div>
<div>
<div class="font-medium">{{ document.name }}</div>
<div class="text-xs text-base-content/70">
{{ document.mimeType || 'Inconnu' }} {{ formatSize(document.size) }}
</div>
</div>
</div>
<div class="flex items-center gap-2">
<button
type="button"
class="btn btn-ghost btn-xs"
:disabled="!canPreviewDocument(document)"
:title="canPreviewDocument(document) ? 'Consulter le document' : 'Aucun aperçu disponible pour ce type'"
@click="$emit('preview', document)"
>
Consulter
</button>
<button type="button" class="btn btn-ghost btn-xs" @click="downloadDocument(document)">
Télécharger
</button>
<button
v-if="canDelete"
type="button"
class="btn btn-error btn-xs"
:disabled="deleteDisabled"
@click="$emit('delete', document.id)"
>
Supprimer
</button>
</div>
</div>
</div>
<p v-else class="text-xs text-base-content/70">
{{ emptyText }}
</p>
</template>
<script setup lang="ts">
import { canPreviewDocument, isImageDocument } from '~/utils/documentPreview'
import {
documentIcon,
formatSize,
shouldInlinePdf,
documentPreviewSrc,
documentThumbnailClass,
downloadDocument,
} from '~/shared/utils/documentDisplayUtils'
withDefaults(defineProps<{
documents: any[]
canDelete?: boolean
deleteDisabled?: boolean
emptyText?: string
}>(), {
canDelete: false,
deleteDisabled: false,
emptyText: 'Aucun document.',
})
defineEmits<{
(e: 'preview', document: any): void
(e: 'delete', documentId: string): void
}>()
</script>
```
**Step 2: Run lint**
Run: `cd Inventory_frontend && npm run lint:fix`
**Step 3: Replace document list in each of the 5 files**
In each file, replace the `v-for` document list block with:
```vue
<DocumentListInline
:documents="xxxDocuments"
:can-delete="canEdit || isEditMode"
:delete-disabled="uploadingDocuments"
:empty-text="'Aucun document lié à cet élément.'"
@preview="openPreview"
@delete="removeDocument"
/>
```
Remove the now-unused imports (`documentIcon`, `formatSize`, `shouldInlinePdf`, etc.) from each file.
**Step 4: Run lint + typecheck**
Run: `cd Inventory_frontend && npm run lint:fix && npx nuxi typecheck`
**Step 5: Commit**
```bash
git add app/components/common/DocumentListInline.vue app/components/PieceItem.vue app/components/ComponentItem.vue app/pages/component/\[id\]/edit.vue app/pages/pieces/\[id\]/edit.vue app/pages/product/\[id\]/edit.vue
git commit -m "refactor(frontend) : extract DocumentListInline shared component"
```
**Expected savings:** ~60 lines per file × 5 files = ~300 lines total
---
### Task 2: Extract `StructureSkeletonPreview.vue` shared component
**Rationale:** The "Squelette sélectionné" details section (collapsible, shows custom fields / pieces / products / subcomponents) is duplicated in:
- `pages/component/[id]/edit.vue` (lines 141-225)
- `pages/component/create.vue` (lines 112-189)
- `pages/pieces/[id]/edit.vue` (lines 185-216)
- `pages/pieces/create.vue` (lines 156-187)
**Files:**
- Create: `app/components/common/StructureSkeletonPreview.vue`
- Modify: all 4 pages above
**Step 1: Create the component**
Extract the common `<details>` collapse + custom fields list + pieces list + products list + subcomponents list into a single component with props:
- `structure` — the normalized structure object
- `description` — optional description text
- `previewBadge` — the badge text (e.g., from `formatStructurePreview`)
- `pieceTypeLabelMap`, `productTypeLabelMap` — for label resolution (component pages only)
- `variant``'component'` or `'piece'` to control which sections display
**Step 2: Replace in each page**
**Step 3: Run lint + typecheck**
**Step 4: Commit**
```bash
git commit -m "refactor(frontend) : extract StructureSkeletonPreview shared component"
```
**Expected savings:** ~50 lines per file × 4 files = ~200 lines total
---
### Task 3: Split `shared/model/componentStructure.ts` (794 lines → 3 files)
**Files:**
- Create: `app/shared/model/componentStructureSanitize.ts`
- Create: `app/shared/model/componentStructureHydrate.ts`
- Modify: `app/shared/model/componentStructure.ts` (keep only normalize + format + extract)
**Step 1: Create `componentStructureSanitize.ts`**
Move these functions (lines 88-362):
- `sanitizeCustomFields`
- `sanitizePieces`
- `sanitizeProducts`
- `sanitizeSubcomponents` (make it exported)
- Helper: `extractFieldValueObject`, `toStringArray`
~275 lines → new file
**Step 2: Create `componentStructureHydrate.ts`**
Move these functions (lines 364-495, 654-739):
- `hydrateCustomFields`
- `hydratePieces`
- `hydrateProducts`
- `hydrateSubcomponents`
- `mapComponentCustomFields`
- `mapComponentPieces`
- `mapComponentProducts`
- `mapSubcomponents`
~250 lines → new file
**Step 3: Update `componentStructure.ts`**
Keep only:
- `isPlainObject`, `ModelStructurePreview`, `defaultStructure`, `ensureStructureShape`, `cloneStructure`
- `normalizeStructureForEditor`, `normalizeStructureForSave`
- `hydrateStructureForEditor`, `extractStructureFromComponent`
- `computeStructureStats`, `formatStructurePreview`
Import sanitize/hydrate functions from the new files. File should end up ~270 lines.
**Step 4: Verify all imports across the codebase still work**
Run: `cd Inventory_frontend && npx nuxi typecheck`
**Step 5: Commit**
```bash
git commit -m "refactor(frontend) : split componentStructure.ts into focused modules"
```
---
### Task 4: Split `composables/useMachineDetailData.ts` (1353 lines → 4 composables)
**Files:**
- Create: `app/composables/useMachineDetailDocuments.ts` (~200 lines)
- Create: `app/composables/useMachineDetailCustomFields.ts` (~150 lines)
- Create: `app/composables/useMachineDetailHierarchy.ts` (~200 lines)
- Create: `app/composables/useMachineDetailProducts.ts` (~150 lines)
- Modify: `app/composables/useMachineDetailData.ts` (should end up ~400 lines)
**Step 1: Identify extraction boundaries**
Read the full file and map which functions/refs belong to which domain:
- **Documents:** document loading, upload, delete, preview state
- **Custom fields:** custom field value management, display logic
- **Hierarchy:** machine hierarchy building, component/piece tree resolution
- **Products:** product display, resolution, supplier info
**Step 2: Extract `useMachineDetailDocuments.ts`**
Move all document-related refs, functions, and watchers. The composable accepts `machineId` and returns `{ documents, loadDocuments, uploadDocuments, ... }`.
**Step 3: Extract `useMachineDetailCustomFields.ts`**
Move custom field resolution, display filtering, and update logic.
**Step 4: Extract `useMachineDetailHierarchy.ts`**
Move `buildMachineHierarchyFromLinks` usage, component/piece tree construction.
**Step 5: Extract `useMachineDetailProducts.ts`**
Move product display resolution, supplier info formatting.
**Step 6: Update `useMachineDetailData.ts`**
Import and compose the 4 sub-composables. Keep only the orchestration logic (data loading sequence, top-level state).
**Step 7: Run lint + typecheck**
**Step 8: Commit**
```bash
git commit -m "refactor(frontend) : split useMachineDetailData into focused composables"
```
---
### Task 5: Extract composable from `StructureNodeEditor.vue` (926 → <500)
**Files:**
- Create: `app/composables/useStructureNodeLogic.ts`
- Modify: `app/components/StructureNodeEditor.vue`
**Step 1: Create `useStructureNodeLogic.ts`**
Extract from the `<script>` section (lines 358-926):
- Type maps (`componentTypeMap`, `pieceTypeMap`, `productTypeMap`) and label getters
- Sync functions (`syncComponentType`, `updatePieceTypeLabel`, `updateProductTypeLabel`, `syncPieceLabels`, `syncProductLabels`)
- Handler functions (`handleComponentTypeSelect`, `handlePieceTypeSelect`, `handleProductTypeSelect`)
- CRUD functions (`addCustomField`, `removeCustomField`, `addPiece`, `removePiece`, `addProduct`, `removeProduct`, `addSubComponent`, `removeSubComponent`)
- Lock state (`initialCustomFieldIndices`, etc., `isCustomFieldLocked`, etc.)
- All watchers
The composable signature:
```ts
export function useStructureNodeLogic(props: { node: ..., componentTypes: ..., ... }) {
// ... all the extracted logic
return { ... }
}
```
**Step 2: Update `StructureNodeEditor.vue`**
Keep only the `<template>` (356 lines — already under 500 on its own) + thin `<script>` that calls the composable.
**Step 3: Run lint + typecheck**
**Step 4: Commit**
```bash
git commit -m "refactor(frontend) : extract StructureNodeEditor logic into composable"
```
---
### Task 6: Extract composable from `pages/component/[id]/edit.vue` (911 → <500)
After Task 1 (DocumentListInline) and Task 2 (StructureSkeletonPreview), this file will be ~750 lines. Still needs ~250 lines extracted.
**Files:**
- Create: `app/composables/useComponentEdit.ts`
- Modify: `app/pages/component/[id]/edit.vue`
**Step 1: Create `useComponentEdit.ts`**
Extract:
- All state declarations (refs, reactive)
- `fetchComponent`, `refreshDocuments`, `refreshCustomFieldInputs`
- `collectStructureSelections` function (lines 802-879 — 77 lines alone)
- `submitEdition` function
- All watchers
- Type label maps and catalog maps
**Step 2: Update the page to use the composable**
**Step 3: Run lint + typecheck**
**Step 4: Commit**
```bash
git commit -m "refactor(frontend) : extract component edit page logic into composable"
```
---
### Task 7: Extract composable from `pages/component/create.vue` (852 → <500)
After Task 2 (StructureSkeletonPreview), ~800 lines remain.
**Files:**
- Create: `app/composables/useComponentCreate.ts`
- Modify: `app/pages/component/create.vue`
**Step 1: Create `useComponentCreate.ts`**
Extract:
- Structure assignment building functions (`extractSubcomponents`, `extractPiecesFromNode`, `extractProductsFromNode`, `buildAssignmentNode`, `initializeStructureAssignments`, `hasAssignments`, `isAssignmentNodeComplete`)
- Serialization functions (`stripNullish`, `sanitizeStructureDefinition`, `sanitizePieceDefinition`, `sanitizeProductDefinition`, `serializeStructureAssignments`)
- `submitCreation` function
- State management and watchers
**Step 2: Update the page**
**Step 3: Run lint + typecheck**
**Step 4: Commit**
```bash
git commit -m "refactor(frontend) : extract component create page logic into composable"
```
---
### Task 8: Extract composable from `pages/pieces/[id]/edit.vue` (821 → <500)
After Task 1 and Task 2, ~650 lines remain.
**Files:**
- Create: `app/composables/usePieceEdit.ts`
- Modify: `app/pages/pieces/[id]/edit.vue`
**Step 1: Create `usePieceEdit.ts`**
Extract:
- Product selection logic (`describeProductRequirement`, `productRequirementEntries`, `productSelectionsFilled`, `ensureProductSelections`, `setProductSelection`)
- `fetchPiece`, `loadPieceTypeDetailsFromCache`, `submitEdition`
- State refs and watchers
**Step 2: Update the page**
**Step 3: Run lint + typecheck**
**Step 4: Commit**
```bash
git commit -m "refactor(frontend) : extract piece edit page logic into composable"
```
---
### Task 9: Reduce `PieceItem.vue` (757 → <500)
After Task 1 (DocumentListInline), ~680 lines remain. The custom field rendering template (lines 236-373) is ~140 lines.
**Files:**
- Create: `app/components/common/CustomFieldDisplay.vue` (~140 lines)
- Modify: `app/components/PieceItem.vue`
**Step 1: Create `CustomFieldDisplay.vue`**
Extract the custom field edit/display template section. Props: `fields`, `isEditMode`, plus events for `update` and `blur`.
**Step 2: Replace in PieceItem.vue and ComponentItem.vue**
Both components have this same custom field display pattern.
**Step 3: Run lint + typecheck**
**Step 4: Commit**
```bash
git commit -m "refactor(frontend) : extract CustomFieldDisplay shared component"
```
**Expected savings:** ~130 lines from PieceItem, ~70 lines from ComponentItem
---
### Task 10: Extract composable from `ComponentStructureAssignmentNode.vue` (722 → <500)
**Files:**
- Create: `app/composables/useStructureAssignmentFetch.ts`
- Modify: `app/components/ComponentStructureAssignmentNode.vue`
**Step 1: Create `useStructureAssignmentFetch.ts`**
Extract:
- All fetch functions (`fetchComponentOptions`, `fetchPieceOptions`, `fetchProductOptions`)
- Option getters (`getPieceOptions`, `getProductOptions`, `componentOptions`)
- Loading state maps (`pieceLoadingByPath`, `productLoadingByPath`, `componentLoadingByPath`)
- Options-by-path state maps
- Label/description helper functions (`describePieceRequirement`, `describeProductRequirement`, etc.)
- All watchers
**Step 2: Update the component**
**Step 3: Run lint + typecheck**
**Step 4: Commit**
```bash
git commit -m "refactor(frontend) : extract assignment fetch logic into composable"
```
---
### Task 11: Extract modals from `pages/index.vue` (584 → <500)
**Files:**
- Create: `app/components/home/AddSiteModal.vue` (~50 lines)
- Create: `app/components/home/AddMachineModal.vue` (~70 lines)
- Modify: `app/pages/index.vue`
**Step 1: Extract `AddSiteModal.vue`**
Move lines 261-297 (site modal template + form).
Props: `open`, `disabled`. Events: `close`, `create`.
**Step 2: Extract `AddMachineModal.vue`**
Move lines 300-368 (machine modal template + form).
Props: `open`, `sites`, `disabled`. Events: `close`, `create`.
**Step 3: Update `index.vue` to use the components**
Move `newSite` and `newMachine` reactive objects into the modals.
**Step 4: Run lint + typecheck**
**Step 5: Commit**
```bash
git commit -m "refactor(frontend) : extract home page modals into components"
```
---
### Task 12: Reduce `PieceModelStructureEditor.vue` (578 → <500)
After Task 9 (if `useDragReorder` composable is already available), the drag logic is already minimal. The remaining bulk is field/product CRUD.
**Files:**
- Create: `app/composables/usePieceStructureEditorLogic.ts`
- Modify: `app/components/PieceModelStructureEditor.vue`
**Step 1: Create `usePieceStructureEditorLogic.ts`**
Extract:
- `hydrateFields`, `hydrateProducts`, `toEditorField`, `toEditorProduct`
- `buildPayload`, `serializeStructure`, `emitUpdate`
- `normalizeProductEntry`, product type metadata updates
- Drag state and drag functions
**Step 2: Update the component**
**Step 3: Run lint + typecheck**
**Step 4: Commit**
```bash
git commit -m "refactor(frontend) : extract PieceModelStructureEditor logic into composable"
```
---
### Task 13: Extract related modal from `ManagementView.vue` (577 → <500)
**Files:**
- Create: `app/components/model-types/RelatedItemsModal.vue` (~100 lines)
- Modify: `app/components/model-types/ManagementView.vue`
**Step 1: Create `RelatedItemsModal.vue`**
Move the related items modal template (lines 109-161) and its logic (`relatedModalOpen`, `relatedItems`, `relatedLoading`, `relatedError`, `loadRelatedItems`, etc.).
Props: `open`, `modelType`. Events: `close`, `open-edit`.
**Step 2: Update `ManagementView.vue`**
**Step 3: Run lint + typecheck**
**Step 4: Commit**
```bash
git commit -m "refactor(frontend) : extract RelatedItemsModal from ManagementView"
```
---
### Task 14: Reduce `ComponentItem.vue` (573 → <500)
After Task 1 (DocumentListInline ~60 lines) and Task 9 (CustomFieldDisplay ~70 lines), the file drops to ~443 lines. **Already under 500 — no further action needed.**
---
### Task 15: Reduce `pages/product/[id]/edit.vue` (570 → <500)
After Task 1 (DocumentListInline ~60 lines), drops to ~510. Need ~10 more lines extracted.
**Files:**
- Modify: `app/pages/product/[id]/edit.vue`
**Step 1: Extract `loadProductType` and `hydrateForm` into a small composable or inline**
Move `loadProductType` (lines 462-488) and `hydrateForm` (lines 490-508) into a shared `useProductEdit.ts` if beneficial, or just use Task 1 savings which may be enough.
**Step 2: Run lint + typecheck**
**Step 3: Commit (if changes needed)**
---
### Task 16: Reduce `pages/pieces/create.vue` (540 → <500)
After Task 2 (StructureSkeletonPreview ~30 lines), drops to ~510. Need ~10 more lines.
**Files:**
- Modify: `app/pages/pieces/create.vue`
**Step 1: Extract product selection logic**
The `describeProductRequirement`, `productRequirementDescriptions`, `productRequirementEntries`, `productSelectionsFilled`, `ensureProductSelections`, `setProductSelection` block (lines 343-398, ~55 lines) is duplicated with `pages/pieces/[id]/edit.vue`.
Extract into `app/shared/utils/pieceProductSelectionUtils.ts`.
**Step 2: Update both piece pages to import from shared utils**
**Step 3: Run lint + typecheck**
**Step 4: Commit**
```bash
git commit -m "refactor(frontend) : extract shared piece product selection utils"
```
---
## Execution order (dependencies)
1. **Task 1** (DocumentListInline) — no deps, reduces 5 files
2. **Task 2** (StructureSkeletonPreview) — no deps, reduces 4 files
3. **Task 9** (CustomFieldDisplay) — no deps, reduces PieceItem + ComponentItem
4. **Task 3** (componentStructure.ts split) — no deps
5. **Task 4** (useMachineDetailData split) — no deps
6. **Task 5** (StructureNodeEditor) — no deps
7. **Task 10** (ComponentStructureAssignmentNode) — no deps
8. **Task 11** (index.vue modals) — no deps
9. **Task 12** (PieceModelStructureEditor) — no deps
10. **Task 13** (ManagementView) — no deps
11. **Task 16** (pieces/create.vue) — no deps
12. **Task 6** (component/edit) — after Tasks 1, 2
13. **Task 7** (component/create) — after Task 2
14. **Task 8** (pieces/edit) — after Tasks 1, 2
15. **Task 15** (product/edit) — after Task 1
16. **Task 14** (ComponentItem) — verify after Tasks 1, 9
Tasks 1-11 are independent and can be parallelized via subagents (pairs that don't touch the same files).