21 KiB
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
<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:
<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
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 objectdescription— optional description textpreviewBadge— the badge text (e.g., fromformatStructurePreview)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
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):
sanitizeCustomFieldssanitizePiecessanitizeProductssanitizeSubcomponents(make it exported)- Helper:
extractFieldValueObject,toStringArray
~275 lines → new file
Step 2: Create componentStructureHydrate.ts
Move these functions (lines 364-495, 654-739):
hydrateCustomFieldshydratePieceshydrateProductshydrateSubcomponentsmapComponentCustomFieldsmapComponentPiecesmapComponentProductsmapSubcomponents
~250 lines → new file
Step 3: Update componentStructure.ts
Keep only:
isPlainObject,ModelStructurePreview,defaultStructure,ensureStructureShape,cloneStructurenormalizeStructureForEditor,normalizeStructureForSavehydrateStructureForEditor,extractStructureFromComponentcomputeStructureStats,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
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
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:
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
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,refreshCustomFieldInputscollectStructureSelectionsfunction (lines 802-879 — 77 lines alone)submitEditionfunction- 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
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) submitCreationfunction- State management and watchers
Step 2: Update the page
Step 3: Run lint + typecheck
Step 4: Commit
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
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
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
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
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,toEditorProductbuildPayload,serializeStructure,emitUpdatenormalizeProductEntry, product type metadata updates- Drag state and drag functions
Step 2: Update the component
Step 3: Run lint + typecheck
Step 4: Commit
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
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
git commit -m "refactor(frontend) : extract shared piece product selection utils"
Execution order (dependencies)
- Task 1 (DocumentListInline) — no deps, reduces 5 files
- Task 2 (StructureSkeletonPreview) — no deps, reduces 4 files
- Task 9 (CustomFieldDisplay) — no deps, reduces PieceItem + ComponentItem
- Task 3 (componentStructure.ts split) — no deps
- Task 4 (useMachineDetailData split) — no deps
- Task 5 (StructureNodeEditor) — no deps
- Task 10 (ComponentStructureAssignmentNode) — no deps
- Task 11 (index.vue modals) — no deps
- Task 12 (PieceModelStructureEditor) — no deps
- Task 13 (ManagementView) — no deps
- Task 16 (pieces/create.vue) — no deps
- Task 6 (component/edit) — after Tasks 1, 2
- Task 7 (component/create) — after Task 2
- Task 8 (pieces/edit) — after Tasks 1, 2
- Task 15 (product/edit) — after Task 1
- 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).