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:
@@ -275,78 +275,7 @@
|
||||
Mettez à jour les valeurs propres à ce composant.
|
||||
</p>
|
||||
</header>
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<div
|
||||
v-for="(field, index) in customFieldInputs"
|
||||
:key="fieldKey(field, index)"
|
||||
class="form-control"
|
||||
>
|
||||
<label class="label">
|
||||
<span class="label-text">{{ field.name }}</span>
|
||||
<span v-if="field.required" class="label-text-alt text-error">*</span>
|
||||
</label>
|
||||
<input
|
||||
v-if="field.type === 'text'"
|
||||
v-model="field.value"
|
||||
type="text"
|
||||
class="input input-bordered input-sm md:input-md"
|
||||
:required="field.required"
|
||||
:disabled="!canEdit || saving"
|
||||
>
|
||||
<input
|
||||
v-else-if="field.type === 'number'"
|
||||
v-model="field.value"
|
||||
type="number"
|
||||
step="0.01"
|
||||
class="input input-bordered input-sm md:input-md"
|
||||
:required="field.required"
|
||||
:disabled="!canEdit || saving"
|
||||
>
|
||||
<select
|
||||
v-else-if="field.type === 'select'"
|
||||
v-model="field.value"
|
||||
class="select select-bordered select-sm md:select-md"
|
||||
:required="field.required"
|
||||
:disabled="!canEdit || saving"
|
||||
>
|
||||
<option value="">Sélectionner...</option>
|
||||
<option
|
||||
v-for="option in field.options"
|
||||
:key="option"
|
||||
:value="option"
|
||||
>
|
||||
{{ option }}
|
||||
</option>
|
||||
</select>
|
||||
<label v-else-if="field.type === 'boolean'" class="flex items-center gap-3 cursor-pointer">
|
||||
<input
|
||||
v-model="field.value"
|
||||
type="checkbox"
|
||||
class="toggle toggle-primary toggle-sm md:toggle-md"
|
||||
true-value="true"
|
||||
false-value="false"
|
||||
:disabled="!canEdit || saving"
|
||||
>
|
||||
<span class="text-sm" :class="field.value === 'true' ? 'text-success font-medium' : 'text-base-content/60'">{{ field.value === 'true' ? 'Oui' : 'Non' }}</span>
|
||||
</label>
|
||||
<input
|
||||
v-else-if="field.type === 'date'"
|
||||
v-model="field.value"
|
||||
type="date"
|
||||
class="input input-bordered input-sm md:input-md"
|
||||
:required="field.required"
|
||||
:disabled="!canEdit || saving"
|
||||
>
|
||||
<input
|
||||
v-else
|
||||
v-model="field.value"
|
||||
type="text"
|
||||
class="input input-bordered input-sm md:input-md"
|
||||
:required="field.required"
|
||||
:disabled="!canEdit || saving"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<CustomFieldInputGrid :fields="customFieldInputs" :disabled="!canEdit || saving" />
|
||||
</div>
|
||||
|
||||
<div class="space-y-4 rounded-lg border border-base-200 bg-base-200/40 p-4">
|
||||
@@ -449,73 +378,12 @@
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<section class="space-y-3 rounded-lg border border-base-200 bg-base-200/40 p-4">
|
||||
<header class="flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<h2 class="font-semibold text-base-content">Historique</h2>
|
||||
<p class="text-xs text-base-content/70">
|
||||
Qui a changé quoi, et quand.
|
||||
</p>
|
||||
</div>
|
||||
<span v-if="historyEntries.length" class="badge badge-outline">
|
||||
{{ historyEntries.length }} entrée{{ historyEntries.length > 1 ? 's' : '' }}
|
||||
</span>
|
||||
</header>
|
||||
|
||||
<div v-if="historyLoading" class="flex items-center gap-2 text-sm text-base-content/70">
|
||||
<span class="loading loading-spinner loading-sm" aria-hidden="true" />
|
||||
Chargement de l’historique…
|
||||
</div>
|
||||
|
||||
<div v-else-if="historyError" class="alert alert-warning">
|
||||
<span>{{ historyError }}</span>
|
||||
</div>
|
||||
|
||||
<p v-else-if="historyEntries.length === 0" class="text-xs text-base-content/70">
|
||||
Aucun changement enregistré pour le moment.
|
||||
</p>
|
||||
|
||||
<ul v-else class="max-h-80 space-y-2 overflow-y-auto pr-1">
|
||||
<li
|
||||
v-for="entry in historyEntries"
|
||||
:key="entry.id"
|
||||
class="rounded-md border border-base-200 bg-base-100 px-3 py-2"
|
||||
>
|
||||
<div class="flex flex-wrap items-center justify-between gap-2 text-xs text-base-content/70">
|
||||
<span class="font-medium text-base-content">
|
||||
{{ historyActionLabel(entry.action) }}
|
||||
</span>
|
||||
<span>{{ formatHistoryDate(entry.createdAt) }}</span>
|
||||
</div>
|
||||
<p class="mt-1 text-xs text-base-content/60">
|
||||
Par {{ entry.actor?.label || 'Inconnu' }}
|
||||
</p>
|
||||
|
||||
<ul
|
||||
v-if="historyDiffEntries(entry).length"
|
||||
class="mt-2 space-y-1 text-xs"
|
||||
>
|
||||
<li
|
||||
v-for="diffEntry in historyDiffEntries(entry)"
|
||||
:key="`${entry.id}-${diffEntry.field}`"
|
||||
class="flex flex-col gap-0.5"
|
||||
>
|
||||
<span class="font-medium text-base-content/80">{{ diffEntry.label }}</span>
|
||||
<span class="text-base-content/60">
|
||||
{{ diffEntry.fromLabel }} → {{ diffEntry.toLabel }}
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p
|
||||
v-else-if="entry.snapshot?.name"
|
||||
class="mt-2 text-xs text-base-content/70"
|
||||
>
|
||||
{{ entry.snapshot.name }}
|
||||
</p>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
<EntityHistorySection
|
||||
:entries="history"
|
||||
:loading="historyLoading"
|
||||
:error="historyError"
|
||||
:field-labels="historyFieldLabels"
|
||||
/>
|
||||
|
||||
<div class="flex flex-col gap-3 md:flex-row md:justify-end">
|
||||
<NuxtLink to="/component-catalog" class="btn btn-ghost" :class="{ 'btn-disabled': saving }">
|
||||
@@ -559,7 +427,7 @@ import { useToast } from '~/composables/useToast'
|
||||
import { extractRelationId } from '~/shared/apiRelations'
|
||||
import { useDocuments } from '~/composables/useDocuments'
|
||||
import { useConstructeurs } from '~/composables/useConstructeurs'
|
||||
import { useComponentHistory, type ComponentHistoryEntry } from '~/composables/useComponentHistory'
|
||||
import { useComponentHistory } from '~/composables/useComponentHistory'
|
||||
import { formatStructurePreview, normalizeStructureForEditor } from '~/shared/modelUtils'
|
||||
import { uniqueConstructeurIds } from '~/shared/constructeurUtils'
|
||||
import type { ComponentModelStructure } from '~/shared/types/inventory'
|
||||
@@ -567,7 +435,6 @@ import type { ModelType } from '~/services/modelTypes'
|
||||
import { canPreviewDocument } from '~/utils/documentPreview'
|
||||
import {
|
||||
type CustomFieldInput,
|
||||
fieldKey,
|
||||
buildCustomFieldInputs,
|
||||
requiredCustomFieldsFilled as _requiredCustomFieldsFilled,
|
||||
saveCustomFieldValues as _saveCustomFieldValues,
|
||||
@@ -580,11 +447,6 @@ import {
|
||||
documentThumbnailClass,
|
||||
downloadDocument,
|
||||
} from '~/shared/utils/documentDisplayUtils'
|
||||
import {
|
||||
historyActionLabel,
|
||||
formatHistoryDate,
|
||||
historyDiffEntries as _historyDiffEntries,
|
||||
} from '~/shared/utils/historyDisplayUtils'
|
||||
|
||||
interface ComponentCatalogType extends ModelType {
|
||||
structure: ComponentModelStructure | null
|
||||
@@ -622,8 +484,6 @@ const componentDocuments = ref<any[]>([])
|
||||
const previewDocument = ref<any | null>(null)
|
||||
const previewVisible = ref(false)
|
||||
|
||||
const historyEntries = computed<ComponentHistoryEntry[]>(() => history.value)
|
||||
|
||||
const historyFieldLabels: Record<string, string> = {
|
||||
name: 'Nom',
|
||||
reference: 'Référence',
|
||||
@@ -634,8 +494,6 @@ const historyFieldLabels: Record<string, string> = {
|
||||
constructeurIds: 'Fournisseurs',
|
||||
}
|
||||
|
||||
const historyDiffEntries = (entry: ComponentHistoryEntry) =>
|
||||
_historyDiffEntries(entry, historyFieldLabels)
|
||||
const selectedTypeId = ref<string>('')
|
||||
const editionForm = reactive({
|
||||
name: '' as string,
|
||||
|
||||
@@ -241,78 +241,7 @@
|
||||
Renseignez les valeurs propres à ce composant selon le squelette choisi.
|
||||
</p>
|
||||
</header>
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<div
|
||||
v-for="(field, index) in customFieldInputs"
|
||||
:key="fieldKey(field, index)"
|
||||
class="form-control"
|
||||
>
|
||||
<label class="label">
|
||||
<span class="label-text">{{ field.name }}</span>
|
||||
<span v-if="field.required" class="label-text-alt text-error">*</span>
|
||||
</label>
|
||||
<input
|
||||
v-if="field.type === 'text'"
|
||||
v-model="field.value"
|
||||
type="text"
|
||||
class="input input-bordered input-sm md:input-md"
|
||||
:required="field.required"
|
||||
:disabled="!canEdit || submitting"
|
||||
>
|
||||
<input
|
||||
v-else-if="field.type === 'number'"
|
||||
v-model="field.value"
|
||||
type="number"
|
||||
step="0.01"
|
||||
class="input input-bordered input-sm md:input-md"
|
||||
:required="field.required"
|
||||
:disabled="!canEdit || submitting"
|
||||
>
|
||||
<select
|
||||
v-else-if="field.type === 'select'"
|
||||
v-model="field.value"
|
||||
class="select select-bordered select-sm md:select-md"
|
||||
:required="field.required"
|
||||
:disabled="!canEdit || submitting"
|
||||
>
|
||||
<option value="">Sélectionner...</option>
|
||||
<option
|
||||
v-for="option in field.options"
|
||||
:key="option"
|
||||
:value="option"
|
||||
>
|
||||
{{ option }}
|
||||
</option>
|
||||
</select>
|
||||
<label v-else-if="field.type === 'boolean'" class="flex items-center gap-3 cursor-pointer">
|
||||
<input
|
||||
v-model="field.value"
|
||||
type="checkbox"
|
||||
class="toggle toggle-primary toggle-sm md:toggle-md"
|
||||
true-value="true"
|
||||
false-value="false"
|
||||
:disabled="!canEdit || submitting"
|
||||
>
|
||||
<span class="text-sm" :class="field.value === 'true' ? 'text-success font-medium' : 'text-base-content/60'">{{ field.value === 'true' ? 'Oui' : 'Non' }}</span>
|
||||
</label>
|
||||
<input
|
||||
v-else-if="field.type === 'date'"
|
||||
v-model="field.value"
|
||||
type="date"
|
||||
class="input input-bordered input-sm md:input-md"
|
||||
:required="field.required"
|
||||
:disabled="!canEdit || submitting"
|
||||
>
|
||||
<input
|
||||
v-else
|
||||
v-model="field.value"
|
||||
type="text"
|
||||
class="input input-bordered input-sm md:input-md"
|
||||
:required="field.required"
|
||||
:disabled="!canEdit || submitting"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<CustomFieldInputGrid :fields="customFieldInputs" :disabled="!canEdit || submitting" />
|
||||
</div>
|
||||
|
||||
<div class="space-y-4 rounded-lg border border-base-200 bg-base-200/40 p-4">
|
||||
@@ -396,7 +325,7 @@ const route = useRoute()
|
||||
const router = useRouter()
|
||||
const { get } = useApi()
|
||||
|
||||
const { componentTypes, loadComponentTypes, loadingComponentTypes } = useComponentTypes()
|
||||
const { componentTypes, loadComponentTypes, loadingComponentTypes: loadingTypes } = useComponentTypes()
|
||||
const { pieceTypes, loadPieceTypes } = usePieceTypes()
|
||||
const { productTypes, loadProductTypes } = useProductTypes()
|
||||
const {
|
||||
@@ -487,7 +416,6 @@ watch(selectedTypeId, (id) => {
|
||||
router.replace({ path: route.path, query: nextQuery }).catch(() => {})
|
||||
})
|
||||
|
||||
const loadingTypes = computed(() => loadingComponentTypes.value)
|
||||
const componentTypeList = computed<ComponentCatalogType[]>(() =>
|
||||
(componentTypes.value || [])
|
||||
.filter((item: any) => item?.category === 'COMPONENT') as ComponentCatalogType[],
|
||||
@@ -1026,8 +954,6 @@ interface CustomFieldInput {
|
||||
orderIndex: number
|
||||
}
|
||||
|
||||
const fieldKey = (field: CustomFieldInput, index: number) =>
|
||||
field.customFieldValueId || field.id || `${field.name}-${index}`
|
||||
|
||||
const normalizeCustomFieldInputs = (structure: ComponentModelStructure | null): CustomFieldInput[] => {
|
||||
if (!structure || typeof structure !== 'object') {
|
||||
|
||||
Reference in New Issue
Block a user