Move category editor to full pages and simplify root skeleton
This commit is contained in:
@@ -5,10 +5,15 @@
|
||||
<div class="flex-1 min-w-[220px] space-y-2">
|
||||
<label class="label">
|
||||
<span class="label-text text-xs font-semibold">
|
||||
{{ isRoot ? 'Famille de composant racine' : 'Famille de composant' }}
|
||||
{{ isRoot ? 'Composant racine de la catégorie' : 'Famille de composant' }}
|
||||
</span>
|
||||
</label>
|
||||
<template v-if="!lockType">
|
||||
<template v-if="isRoot">
|
||||
<p class="text-[11px] text-gray-500">
|
||||
Le composant racine correspond à la catégorie que vous éditez. Sélectionnez uniquement les familles pour les sous-composants.
|
||||
</p>
|
||||
</template>
|
||||
<template v-else-if="!lockType">
|
||||
<select
|
||||
v-model="node.typeComposantId"
|
||||
class="select select-bordered select-sm w-full"
|
||||
@@ -312,6 +317,15 @@ const syncComponentType = (component: EditableStructureNode) => {
|
||||
if (!component) {
|
||||
return
|
||||
}
|
||||
if (props.isRoot) {
|
||||
component.typeComposantId = ''
|
||||
component.typeComposantLabel = ''
|
||||
component.familyCode = ''
|
||||
if (component.alias) {
|
||||
component.alias = ''
|
||||
}
|
||||
return
|
||||
}
|
||||
if (props.lockType && props.isRoot) {
|
||||
if (props.lockedTypeLabel) {
|
||||
component.typeComposantLabel = props.lockedTypeLabel
|
||||
|
||||
@@ -1,360 +0,0 @@
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<div v-if="visible" class="modal modal-open" role="dialog" aria-modal="true" aria-labelledby="model-type-modal-title">
|
||||
<div class="modal-box max-w-2xl">
|
||||
<h2 id="model-type-modal-title" class="text-2xl font-semibold text-base-content">
|
||||
{{ modalTitle }}
|
||||
</h2>
|
||||
<p class="mt-1 text-sm text-base-content/70">
|
||||
Les champs marqués d'un astérisque sont obligatoires.
|
||||
</p>
|
||||
|
||||
<form class="mt-6 space-y-6" @submit.prevent="handleSubmit">
|
||||
<div>
|
||||
<label class="label" for="model-type-name">
|
||||
<span class="label-text">Nom *</span>
|
||||
</label>
|
||||
<input
|
||||
id="model-type-name"
|
||||
ref="nameInput"
|
||||
v-model.trim="form.name"
|
||||
type="text"
|
||||
class="input input-bordered w-full"
|
||||
name="name"
|
||||
minlength="2"
|
||||
maxlength="120"
|
||||
required
|
||||
/>
|
||||
<p v-if="errors.name" class="mt-1 text-sm text-error">{{ errors.name }}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="label" for="model-type-code">
|
||||
<span class="label-text">Code *</span>
|
||||
</label>
|
||||
<input
|
||||
id="model-type-code"
|
||||
v-model.trim="form.code"
|
||||
type="text"
|
||||
class="input input-bordered w-full"
|
||||
name="code"
|
||||
minlength="2"
|
||||
maxlength="60"
|
||||
required
|
||||
autocomplete="off"
|
||||
/>
|
||||
<p class="mt-1 text-xs text-base-content/70">Caractères autorisés : lettres, chiffres, -, _ et .</p>
|
||||
<p v-if="errors.code" class="mt-1 text-sm text-error">{{ errors.code }}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="label" for="model-type-category">
|
||||
<span class="label-text">Catégorie *</span>
|
||||
</label>
|
||||
<select
|
||||
id="model-type-category"
|
||||
v-model="form.category"
|
||||
class="select select-bordered w-full"
|
||||
name="category"
|
||||
required
|
||||
:disabled="lockCategory"
|
||||
>
|
||||
<option value="COMPONENT">Composants</option>
|
||||
<option value="PIECE">Pièces</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="label" for="model-type-notes">
|
||||
<span class="label-text">Notes</span>
|
||||
</label>
|
||||
<textarea
|
||||
id="model-type-notes"
|
||||
v-model.trim="form.notes"
|
||||
class="textarea textarea-bordered w-full"
|
||||
rows="4"
|
||||
name="notes"
|
||||
maxlength="2000"
|
||||
></textarea>
|
||||
<p class="mt-1 text-xs text-base-content/70">Saisissez des informations complémentaires (facultatif).</p>
|
||||
</div>
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
<section class="space-y-4">
|
||||
<header>
|
||||
<h3 class="text-lg font-semibold text-base-content">
|
||||
Structure du squelette
|
||||
</h3>
|
||||
<p class="mt-1 text-sm text-base-content/70">
|
||||
Définissez la structure canonique appliquée lors de la création des composants ou pièces de cette catégorie.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<div
|
||||
v-if="structureLoading"
|
||||
class="flex items-center justify-center rounded-lg border border-dashed border-base-300 py-12"
|
||||
>
|
||||
<span class="loading loading-spinner loading-lg" aria-hidden="true"></span>
|
||||
<span class="ml-3 text-sm text-base-content/70">Chargement du squelette…</span>
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<div
|
||||
v-if="form.category === 'COMPONENT'"
|
||||
class="space-y-3 rounded-lg border border-base-300 p-4"
|
||||
>
|
||||
<p class="text-sm text-base-content/70">
|
||||
Aperçu :
|
||||
<span class="font-medium text-base-content">{{ componentStructurePreview }}</span>
|
||||
</p>
|
||||
<ComponentModelStructureEditor v-model="componentStructure" />
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else
|
||||
class="space-y-3 rounded-lg border border-base-300 p-4"
|
||||
>
|
||||
<p class="text-sm text-base-content/70">
|
||||
Aperçu :
|
||||
<span class="font-medium text-base-content">{{ pieceStructurePreview }}</span>
|
||||
</p>
|
||||
<PieceModelStructureEditor v-model="pieceStructure" />
|
||||
</div>
|
||||
</template>
|
||||
</section>
|
||||
|
||||
<div class="modal-action">
|
||||
<button type="button" class="btn btn-ghost" @click="close">Annuler</button>
|
||||
<button type="submit" class="btn btn-primary" :disabled="isSubmitDisabled">
|
||||
<span
|
||||
v-if="saving"
|
||||
class="loading loading-spinner loading-sm"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
{{ submitLabel }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<button type="button" class="modal-backdrop" aria-label="Fermer la modale" @click="close"></button>
|
||||
</div>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, reactive, ref, watch, onBeforeUnmount } from 'vue';
|
||||
import ComponentModelStructureEditor from '~/components/ComponentModelStructureEditor.vue';
|
||||
import PieceModelStructureEditor from '~/components/PieceModelStructureEditor.vue';
|
||||
import {
|
||||
clonePieceStructure,
|
||||
cloneStructure,
|
||||
defaultPieceStructure,
|
||||
defaultStructure,
|
||||
formatPieceStructurePreview,
|
||||
formatStructurePreview,
|
||||
normalizePieceStructureForSave,
|
||||
normalizeStructureForSave,
|
||||
} from '~/shared/modelUtils';
|
||||
import type { ModelCategory, ModelTypePayload } from '~/services/modelTypes';
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean;
|
||||
mode: 'create' | 'edit';
|
||||
initialCategory: ModelCategory;
|
||||
initialData?: Partial<ModelTypePayload> | null;
|
||||
saving?: boolean;
|
||||
lockCategory?: boolean;
|
||||
structureLoading?: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'close'): void;
|
||||
(e: 'save', payload: ModelTypePayload): void;
|
||||
}>();
|
||||
|
||||
const lockCategory = computed(() => props.lockCategory ?? false);
|
||||
const structureLoading = computed(() => props.structureLoading ?? false);
|
||||
|
||||
const form = reactive<ModelTypePayload>({
|
||||
name: '',
|
||||
code: '',
|
||||
category: 'COMPONENT',
|
||||
notes: '',
|
||||
structure: undefined,
|
||||
});
|
||||
|
||||
const errors = reactive<{ name?: string; code?: string }>({});
|
||||
|
||||
const nameInput = ref<HTMLInputElement | null>(null);
|
||||
|
||||
const codePattern = /^[a-z0-9\-_.]+$/i;
|
||||
|
||||
const componentStructure = ref(normalizeStructureForSave(defaultStructure()));
|
||||
const pieceStructure = ref(normalizePieceStructureForSave(defaultPieceStructure()));
|
||||
|
||||
const resetForm = () => {
|
||||
form.name = props.initialData?.name ?? '';
|
||||
form.code = props.initialData?.code ?? '';
|
||||
form.category = props.initialData?.category ?? props.initialCategory;
|
||||
form.notes = props.initialData?.notes ?? '';
|
||||
errors.name = undefined;
|
||||
errors.code = undefined;
|
||||
|
||||
const incomingStructure = props.initialData?.structure;
|
||||
if (form.category === 'COMPONENT') {
|
||||
componentStructure.value = normalizeStructureForSave(
|
||||
incomingStructure && props.initialData?.category === 'COMPONENT'
|
||||
? incomingStructure
|
||||
: defaultStructure(),
|
||||
);
|
||||
} else {
|
||||
pieceStructure.value = normalizePieceStructureForSave(
|
||||
incomingStructure && props.initialData?.category === 'PIECE'
|
||||
? incomingStructure
|
||||
: defaultPieceStructure(),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const close = () => {
|
||||
emit('close');
|
||||
};
|
||||
|
||||
const modalTitle = computed(() =>
|
||||
props.mode === 'edit' ? 'Modifier le type de modèle' : 'Créer un type de modèle',
|
||||
);
|
||||
|
||||
const submitLabel = computed(() => (props.mode === 'edit' ? 'Enregistrer' : 'Créer'));
|
||||
|
||||
const saving = computed(() => props.saving ?? false);
|
||||
const isSubmitDisabled = computed(() => saving.value || structureLoading.value);
|
||||
|
||||
const onEscape = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape') {
|
||||
event.preventDefault();
|
||||
close();
|
||||
}
|
||||
};
|
||||
|
||||
const validate = () => {
|
||||
errors.name = undefined;
|
||||
errors.code = undefined;
|
||||
|
||||
if (form.name.trim().length < 2) {
|
||||
errors.name = 'Le nom doit contenir au moins 2 caractères.';
|
||||
}
|
||||
if (form.name.trim().length > 120) {
|
||||
errors.name = 'Le nom ne peut pas dépasser 120 caractères.';
|
||||
}
|
||||
|
||||
if (!codePattern.test(form.code.trim())) {
|
||||
errors.code = 'Le code doit respecter le format demandé.';
|
||||
} else if (form.code.trim().length < 2 || form.code.trim().length > 60) {
|
||||
errors.code = 'Le code doit contenir entre 2 et 60 caractères.';
|
||||
}
|
||||
|
||||
return !errors.name && !errors.code;
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (!validate()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const common = {
|
||||
name: form.name.trim(),
|
||||
code: form.code.trim(),
|
||||
notes: form.notes?.trim() ? form.notes.trim() : undefined,
|
||||
};
|
||||
|
||||
if (form.category === 'COMPONENT') {
|
||||
emit('save', {
|
||||
...common,
|
||||
category: 'COMPONENT',
|
||||
structure: normalizeStructureForSave(cloneStructure(componentStructure.value)),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
emit('save', {
|
||||
...common,
|
||||
category: 'PIECE',
|
||||
structure: normalizePieceStructureForSave(clonePieceStructure(pieceStructure.value)),
|
||||
});
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
(visible) => {
|
||||
if (visible) {
|
||||
resetForm();
|
||||
document.addEventListener('keydown', onEscape);
|
||||
nextTick(() => nameInput.value?.focus());
|
||||
} else {
|
||||
document.removeEventListener('keydown', onEscape);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
watch(
|
||||
() => form.category,
|
||||
(category, previous) => {
|
||||
if (category === previous) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (category === 'COMPONENT' && previous !== 'COMPONENT') {
|
||||
componentStructure.value = normalizeStructureForSave(defaultStructure());
|
||||
}
|
||||
if (category === 'PIECE' && previous !== 'PIECE') {
|
||||
pieceStructure.value = normalizePieceStructureForSave(defaultPieceStructure());
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.initialData?.structure,
|
||||
(value) => {
|
||||
if (!props.visible) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (form.category === 'COMPONENT') {
|
||||
componentStructure.value = normalizeStructureForSave(
|
||||
props.initialData?.category === 'COMPONENT' && value
|
||||
? value
|
||||
: componentStructure.value,
|
||||
);
|
||||
} else if (form.category === 'PIECE') {
|
||||
pieceStructure.value = normalizePieceStructureForSave(
|
||||
props.initialData?.category === 'PIECE' && value
|
||||
? value
|
||||
: pieceStructure.value,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const componentStructurePreview = computed(() =>
|
||||
formatStructurePreview(componentStructure.value),
|
||||
);
|
||||
|
||||
const pieceStructurePreview = computed(() =>
|
||||
formatPieceStructurePreview(pieceStructure.value),
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.initialData,
|
||||
() => {
|
||||
if (props.visible) {
|
||||
resetForm();
|
||||
}
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
document.removeEventListener('keydown', onEscape);
|
||||
});
|
||||
</script>
|
||||
@@ -14,13 +14,13 @@
|
||||
:search="searchInput"
|
||||
:sort="sort"
|
||||
:dir="dir"
|
||||
:loading="loading || saving"
|
||||
:loading="loading"
|
||||
:show-category-tabs="allowCategorySwitch"
|
||||
@update:category="onCategoryChange"
|
||||
@update:search="onSearchInput"
|
||||
@update:sort="onSortChange"
|
||||
@update:dir="onDirChange"
|
||||
@create="openCreateModal"
|
||||
@create="openCreatePage"
|
||||
/>
|
||||
|
||||
<ModelTypesTable
|
||||
@@ -29,41 +29,24 @@
|
||||
:total="total"
|
||||
:limit="limit"
|
||||
:offset="offset"
|
||||
@edit="openEditModal"
|
||||
@edit="openEditPage"
|
||||
@delete="confirmDelete"
|
||||
@update:offset="onOffsetChange"
|
||||
/>
|
||||
|
||||
<EditModal
|
||||
:visible="isModalOpen"
|
||||
:mode="modalMode"
|
||||
:initial-category="selectedCategory"
|
||||
:initial-data="modalInitialData"
|
||||
:saving="saving"
|
||||
:lock-category="!allowCategorySwitch"
|
||||
:structure-loading="structureLoading"
|
||||
@close="closeModal"
|
||||
@save="handleSave"
|
||||
/>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from "vue";
|
||||
import { useHead } from "#imports";
|
||||
import { useHead, useRouter } from "#imports";
|
||||
import ModelTypesToolbar from "~/components/model-types/Toolbar.vue";
|
||||
import ModelTypesTable from "~/components/model-types/Table.vue";
|
||||
import EditModal from "~/components/model-types/EditModal.vue";
|
||||
import {
|
||||
createModelType,
|
||||
deleteModelType,
|
||||
getModelType,
|
||||
listModelTypes,
|
||||
updateModelType,
|
||||
type ModelCategory,
|
||||
type ModelType,
|
||||
type ModelTypeListResponse,
|
||||
type ModelTypePayload,
|
||||
} from "~/services/modelTypes";
|
||||
import { useToast } from "~/composables/useToast";
|
||||
|
||||
@@ -93,17 +76,11 @@ const offset = ref(0);
|
||||
const items = ref<ModelType[]>([]);
|
||||
const total = ref(0);
|
||||
const loading = ref(false);
|
||||
const saving = ref(false);
|
||||
|
||||
const isModalOpen = ref(false);
|
||||
const modalMode = ref<"create" | "edit">("create");
|
||||
const modalInitialData = ref<Partial<ModelTypePayload> | null>(null);
|
||||
const editingId = ref<string | null>(null);
|
||||
const structureLoading = ref(false);
|
||||
|
||||
let debounceTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
let activeController: AbortController | null = null;
|
||||
|
||||
const router = useRouter();
|
||||
const { showError, showSuccess } = useToast();
|
||||
|
||||
const headingText = computed(() => props.heading);
|
||||
@@ -233,72 +210,22 @@ const onOffsetChange = (value: number) => {
|
||||
}
|
||||
};
|
||||
|
||||
const openCreateModal = () => {
|
||||
modalMode.value = "create";
|
||||
modalInitialData.value = null;
|
||||
editingId.value = null;
|
||||
structureLoading.value = false;
|
||||
isModalOpen.value = true;
|
||||
const resolveCategoryBasePath = (category: ModelCategory) =>
|
||||
category === "COMPONENT" ? "/component-category" : "/piece-category";
|
||||
|
||||
const openCreatePage = () => {
|
||||
const basePath = resolveCategoryBasePath(selectedCategory.value);
|
||||
router.push(`${basePath}/new`).catch(() => {
|
||||
showError("Navigation impossible vers la page de création.");
|
||||
});
|
||||
};
|
||||
|
||||
const openEditModal = async (item: ModelType) => {
|
||||
modalMode.value = "edit";
|
||||
editingId.value = item.id;
|
||||
modalInitialData.value = {
|
||||
name: item.name,
|
||||
code: item.code,
|
||||
category: item.category,
|
||||
notes: item.notes ?? item.description ?? "",
|
||||
structure: item.structure,
|
||||
};
|
||||
isModalOpen.value = true;
|
||||
|
||||
structureLoading.value = true;
|
||||
|
||||
try {
|
||||
const response = await getModelType(item.id);
|
||||
modalInitialData.value = {
|
||||
name: response.name,
|
||||
code: response.code,
|
||||
category: response.category,
|
||||
notes: response.notes ?? response.description ?? "",
|
||||
structure: response.structure,
|
||||
};
|
||||
} catch (error) {
|
||||
showError(extractErrorMessage(error));
|
||||
} finally {
|
||||
structureLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const closeModal = () => {
|
||||
isModalOpen.value = false;
|
||||
};
|
||||
|
||||
const handleSave = async (payload: ModelTypePayload) => {
|
||||
saving.value = true;
|
||||
|
||||
try {
|
||||
const enrichedPayload = {
|
||||
...payload,
|
||||
description: payload.notes ?? null,
|
||||
};
|
||||
|
||||
if (modalMode.value === "create") {
|
||||
await createModelType(enrichedPayload);
|
||||
showSuccess("Type de modèle créé avec succès.");
|
||||
} else if (modalMode.value === "edit" && editingId.value) {
|
||||
await updateModelType(editingId.value, enrichedPayload);
|
||||
showSuccess("Type de modèle mis à jour avec succès.");
|
||||
}
|
||||
|
||||
isModalOpen.value = false;
|
||||
await refresh();
|
||||
} catch (error) {
|
||||
showError(extractErrorMessage(error));
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
const openEditPage = (item: ModelType) => {
|
||||
const category = item.category ?? selectedCategory.value;
|
||||
const basePath = resolveCategoryBasePath(category);
|
||||
router.push(`${basePath}/${item.id}/edit`).catch(() => {
|
||||
showError("Navigation impossible vers la page d'édition.");
|
||||
});
|
||||
};
|
||||
|
||||
const confirmDelete = async (item: ModelType) => {
|
||||
|
||||
306
app/components/model-types/ModelTypeForm.vue
Normal file
306
app/components/model-types/ModelTypeForm.vue
Normal file
@@ -0,0 +1,306 @@
|
||||
<template>
|
||||
<form class="space-y-8" @submit.prevent="handleSubmit">
|
||||
<section class="space-y-4">
|
||||
<div>
|
||||
<label class="label" for="model-type-name">
|
||||
<span class="label-text">Nom *</span>
|
||||
</label>
|
||||
<input
|
||||
id="model-type-name"
|
||||
ref="nameInput"
|
||||
v-model.trim="form.name"
|
||||
type="text"
|
||||
class="input input-bordered w-full"
|
||||
name="name"
|
||||
minlength="2"
|
||||
maxlength="120"
|
||||
required
|
||||
/>
|
||||
<p v-if="errors.name" class="mt-1 text-sm text-error">{{ errors.name }}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="label" for="model-type-code">
|
||||
<span class="label-text">Code *</span>
|
||||
</label>
|
||||
<input
|
||||
id="model-type-code"
|
||||
v-model.trim="form.code"
|
||||
type="text"
|
||||
class="input input-bordered w-full"
|
||||
name="code"
|
||||
minlength="2"
|
||||
maxlength="60"
|
||||
required
|
||||
autocomplete="off"
|
||||
/>
|
||||
<p class="mt-1 text-xs text-base-content/70">Caractères autorisés : lettres, chiffres, -, _ et .</p>
|
||||
<p v-if="errors.code" class="mt-1 text-sm text-error">{{ errors.code }}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="label" for="model-type-category">
|
||||
<span class="label-text">Catégorie *</span>
|
||||
</label>
|
||||
<select
|
||||
id="model-type-category"
|
||||
v-model="form.category"
|
||||
class="select select-bordered w-full"
|
||||
name="category"
|
||||
required
|
||||
:disabled="lockCategory"
|
||||
>
|
||||
<option value="COMPONENT">Composants</option>
|
||||
<option value="PIECE">Pièces</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="label" for="model-type-notes">
|
||||
<span class="label-text">Notes</span>
|
||||
</label>
|
||||
<textarea
|
||||
id="model-type-notes"
|
||||
v-model.trim="form.notes"
|
||||
class="textarea textarea-bordered w-full"
|
||||
rows="4"
|
||||
name="notes"
|
||||
maxlength="2000"
|
||||
></textarea>
|
||||
<p class="mt-1 text-xs text-base-content/70">Saisissez des informations complémentaires (facultatif).</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="space-y-4">
|
||||
<header>
|
||||
<h3 class="text-lg font-semibold text-base-content">Structure du squelette</h3>
|
||||
<p class="mt-1 text-sm text-base-content/70">
|
||||
Définissez la structure canonique appliquée lors de la création des composants ou pièces de cette catégorie.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<div
|
||||
v-if="structureLoading"
|
||||
class="flex items-center justify-center rounded-lg border border-dashed border-base-300 py-12"
|
||||
>
|
||||
<span class="loading loading-spinner loading-lg" aria-hidden="true"></span>
|
||||
<span class="ml-3 text-sm text-base-content/70">Chargement du squelette…</span>
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<div
|
||||
v-if="form.category === 'COMPONENT'"
|
||||
class="space-y-3 rounded-lg border border-base-300 p-4"
|
||||
>
|
||||
<p class="text-sm text-base-content/70">
|
||||
Aperçu :
|
||||
<span class="font-medium text-base-content">{{ componentStructurePreview }}</span>
|
||||
</p>
|
||||
<ComponentModelStructureEditor v-model="componentStructure" />
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else
|
||||
class="space-y-3 rounded-lg border border-base-300 p-4"
|
||||
>
|
||||
<p class="text-sm text-base-content/70">
|
||||
Aperçu :
|
||||
<span class="font-medium text-base-content">{{ pieceStructurePreview }}</span>
|
||||
</p>
|
||||
<PieceModelStructureEditor v-model="pieceStructure" />
|
||||
</div>
|
||||
</template>
|
||||
</section>
|
||||
|
||||
<footer class="flex flex-col gap-3 border-t border-base-300 pt-4 sm:flex-row sm:justify-end">
|
||||
<button type="button" class="btn btn-ghost" :disabled="saving" @click="emit('cancel')">
|
||||
Annuler
|
||||
</button>
|
||||
<button type="submit" class="btn btn-primary" :disabled="isSubmitDisabled">
|
||||
<span v-if="saving" class="loading loading-spinner loading-sm" aria-hidden="true"></span>
|
||||
{{ submitLabel }}
|
||||
</button>
|
||||
</footer>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, onMounted, reactive, ref, watch } from 'vue'
|
||||
import ComponentModelStructureEditor from '~/components/ComponentModelStructureEditor.vue'
|
||||
import PieceModelStructureEditor from '~/components/PieceModelStructureEditor.vue'
|
||||
import {
|
||||
clonePieceStructure,
|
||||
cloneStructure,
|
||||
defaultPieceStructure,
|
||||
defaultStructure,
|
||||
formatPieceStructurePreview,
|
||||
formatStructurePreview,
|
||||
normalizePieceStructureForSave,
|
||||
normalizeStructureForSave,
|
||||
} from '~/shared/modelUtils'
|
||||
import type { ModelCategory, ModelTypePayload } from '~/services/modelTypes'
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
mode: 'create' | 'edit'
|
||||
initialCategory: ModelCategory
|
||||
initialData?: Partial<ModelTypePayload> | null
|
||||
saving?: boolean
|
||||
lockCategory?: boolean
|
||||
structureLoading?: boolean
|
||||
}>(), {
|
||||
initialData: null,
|
||||
saving: false,
|
||||
lockCategory: false,
|
||||
structureLoading: false,
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'submit', payload: ModelTypePayload): void
|
||||
(e: 'cancel'): void
|
||||
}>()
|
||||
|
||||
const lockCategory = computed(() => props.lockCategory ?? false)
|
||||
const structureLoading = computed(() => props.structureLoading ?? false)
|
||||
const saving = computed(() => props.saving ?? false)
|
||||
|
||||
const form = reactive<ModelTypePayload>({
|
||||
name: '',
|
||||
code: '',
|
||||
category: props.initialCategory,
|
||||
notes: '',
|
||||
structure: undefined,
|
||||
})
|
||||
|
||||
const errors = reactive<{ name?: string; code?: string }>({})
|
||||
const nameInput = ref<HTMLInputElement | null>(null)
|
||||
const codePattern = /^[a-z0-9\-_.]+$/i
|
||||
|
||||
const componentStructure = ref(normalizeStructureForSave(defaultStructure()))
|
||||
const pieceStructure = ref(normalizePieceStructureForSave(defaultPieceStructure()))
|
||||
|
||||
const resetStructures = (incomingStructure: ModelTypePayload['structure'], category: ModelCategory) => {
|
||||
if (category === 'COMPONENT') {
|
||||
componentStructure.value = normalizeStructureForSave(
|
||||
incomingStructure && props.initialData?.category === 'COMPONENT'
|
||||
? incomingStructure
|
||||
: defaultStructure(),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
pieceStructure.value = normalizePieceStructureForSave(
|
||||
incomingStructure && props.initialData?.category === 'PIECE'
|
||||
? incomingStructure
|
||||
: defaultPieceStructure(),
|
||||
)
|
||||
}
|
||||
|
||||
const resetForm = () => {
|
||||
const incoming = props.initialData ?? {}
|
||||
form.name = typeof incoming.name === 'string' ? incoming.name : ''
|
||||
form.code = typeof incoming.code === 'string' ? incoming.code : ''
|
||||
form.category = incoming.category ?? props.initialCategory
|
||||
form.notes = typeof incoming.notes === 'string'
|
||||
? incoming.notes
|
||||
: typeof (incoming as any).description === 'string'
|
||||
? (incoming as any).description
|
||||
: ''
|
||||
|
||||
errors.name = undefined
|
||||
errors.code = undefined
|
||||
|
||||
resetStructures(incoming.structure, form.category)
|
||||
}
|
||||
|
||||
const submitLabel = computed(() => (props.mode === 'edit' ? 'Enregistrer' : 'Créer'))
|
||||
const isSubmitDisabled = computed(() => saving.value || structureLoading.value)
|
||||
|
||||
const validate = () => {
|
||||
errors.name = undefined
|
||||
errors.code = undefined
|
||||
|
||||
if (form.name.trim().length < 2) {
|
||||
errors.name = 'Le nom doit contenir au moins 2 caractères.'
|
||||
}
|
||||
if (form.name.trim().length > 120) {
|
||||
errors.name = 'Le nom ne peut pas dépasser 120 caractères.'
|
||||
}
|
||||
|
||||
if (!codePattern.test(form.code.trim())) {
|
||||
errors.code = 'Le code doit respecter le format demandé.'
|
||||
} else if (form.code.trim().length < 2 || form.code.trim().length > 60) {
|
||||
errors.code = 'Le code doit contenir entre 2 et 60 caractères.'
|
||||
}
|
||||
|
||||
return !errors.name && !errors.code
|
||||
}
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (!validate()) {
|
||||
return
|
||||
}
|
||||
|
||||
const common = {
|
||||
name: form.name.trim(),
|
||||
code: form.code.trim(),
|
||||
notes: form.notes?.trim() ? form.notes.trim() : undefined,
|
||||
}
|
||||
|
||||
if (form.category === 'COMPONENT') {
|
||||
emit('submit', {
|
||||
...common,
|
||||
category: 'COMPONENT',
|
||||
structure: normalizeStructureForSave(cloneStructure(componentStructure.value)),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
emit('submit', {
|
||||
...common,
|
||||
category: 'PIECE',
|
||||
structure: normalizePieceStructureForSave(clonePieceStructure(pieceStructure.value)),
|
||||
})
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.initialData,
|
||||
() => {
|
||||
resetForm()
|
||||
},
|
||||
{ deep: true, immediate: true },
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.initialCategory,
|
||||
() => {
|
||||
if (!props.initialData) {
|
||||
form.category = props.initialCategory
|
||||
resetStructures(undefined, form.category)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
watch(
|
||||
() => form.category,
|
||||
(category, previous) => {
|
||||
if (category === previous) {
|
||||
return
|
||||
}
|
||||
|
||||
if (category === 'COMPONENT') {
|
||||
componentStructure.value = normalizeStructureForSave(defaultStructure())
|
||||
}
|
||||
|
||||
if (category === 'PIECE') {
|
||||
pieceStructure.value = normalizePieceStructureForSave(defaultPieceStructure())
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
const componentStructurePreview = computed(() => formatStructurePreview(componentStructure.value))
|
||||
const pieceStructurePreview = computed(() => formatPieceStructurePreview(pieceStructure.value))
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => nameInput.value?.focus())
|
||||
})
|
||||
</script>
|
||||
124
app/pages/component-category/[id]/edit.vue
Normal file
124
app/pages/component-category/[id]/edit.vue
Normal file
@@ -0,0 +1,124 @@
|
||||
<template>
|
||||
<main class="mx-auto flex w-full max-w-4xl flex-col gap-8 px-4 py-8 sm:px-6 lg:px-8">
|
||||
<header class="space-y-2">
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-base-content">{{ title }}</h1>
|
||||
<p class="text-base text-base-content/70">
|
||||
Ajustez le squelette et les métadonnées de cette catégorie de composant. Les modifications seront appliquées lors des prochaines créations de composants.
|
||||
</p>
|
||||
</div>
|
||||
<NuxtLink class="btn btn-ghost" to="/component-category">
|
||||
Retour au catalogue
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="rounded-xl border border-base-300 bg-base-100 p-6 shadow-sm">
|
||||
<div v-if="loading" class="flex items-center justify-center py-16">
|
||||
<span class="loading loading-spinner loading-lg" aria-hidden="true"></span>
|
||||
<span class="ml-3 text-sm text-base-content/70">Chargement de la catégorie…</span>
|
||||
</div>
|
||||
<ModelTypeForm
|
||||
v-else
|
||||
mode="edit"
|
||||
initial-category="COMPONENT"
|
||||
:initial-data="initialData"
|
||||
:lock-category="true"
|
||||
:saving="saving"
|
||||
@submit="handleSubmit"
|
||||
@cancel="handleCancel"
|
||||
/>
|
||||
</section>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useHead, useRoute, useRouter } from '#imports'
|
||||
import ModelTypeForm from '~/components/model-types/ModelTypeForm.vue'
|
||||
import { getModelType, updateModelType, type ModelTypePayload } from '~/services/modelTypes'
|
||||
import { useToast } from '~/composables/useToast'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const { showError, showSuccess } = useToast()
|
||||
|
||||
const loading = ref(true)
|
||||
const saving = ref(false)
|
||||
const initialData = ref<Partial<ModelTypePayload> | null>(null)
|
||||
|
||||
const title = computed(() =>
|
||||
initialData.value?.name
|
||||
? `Modifier « ${initialData.value.name} »`
|
||||
: 'Modifier une catégorie de composant',
|
||||
)
|
||||
|
||||
useHead(() => ({
|
||||
title: title.value,
|
||||
}))
|
||||
|
||||
const navigateBackToList = async () => {
|
||||
await router.push('/component-category').catch(() => {
|
||||
showError("Navigation impossible vers la liste des catégories.")
|
||||
})
|
||||
}
|
||||
|
||||
const normalizeError = (error: any) => {
|
||||
const message = error?.data?.message || error?.message || 'Une erreur est survenue.'
|
||||
return Array.isArray(message) ? message[0] : message
|
||||
}
|
||||
|
||||
const loadCategory = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const id = String(route.params.id)
|
||||
const response = await getModelType(id)
|
||||
|
||||
if (response.category !== 'COMPONENT') {
|
||||
showError("Cette catégorie n'est pas un composant." )
|
||||
await navigateBackToList()
|
||||
return
|
||||
}
|
||||
|
||||
initialData.value = {
|
||||
name: response.name,
|
||||
code: response.code,
|
||||
category: response.category,
|
||||
notes: response.notes ?? response.description ?? '',
|
||||
structure: response.structure ?? undefined,
|
||||
}
|
||||
} catch (error) {
|
||||
showError(normalizeError(error))
|
||||
await navigateBackToList()
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleCancel = () => {
|
||||
navigateBackToList()
|
||||
}
|
||||
|
||||
const handleSubmit = async (payload: Parameters<typeof updateModelType>[1]) => {
|
||||
const id = String(route.params.id)
|
||||
saving.value = true
|
||||
try {
|
||||
const enrichedPayload = {
|
||||
...payload,
|
||||
description: payload?.notes ?? null,
|
||||
}
|
||||
await updateModelType(id, enrichedPayload)
|
||||
showSuccess('Catégorie de composant mise à jour avec succès.')
|
||||
await navigateBackToList()
|
||||
} catch (error) {
|
||||
showError(normalizeError(error))
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadCategory()
|
||||
})
|
||||
</script>
|
||||
68
app/pages/component-category/new.vue
Normal file
68
app/pages/component-category/new.vue
Normal file
@@ -0,0 +1,68 @@
|
||||
<template>
|
||||
<main class="mx-auto flex w-full max-w-4xl flex-col gap-8 px-4 py-8 sm:px-6 lg:px-8">
|
||||
<header class="space-y-2">
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-base-content">Nouvelle catégorie de composant</h1>
|
||||
<p class="text-base text-base-content/70">
|
||||
Configurez le squelette canonique qui sera appliqué lors de la création des composants appartenant à cette catégorie.
|
||||
</p>
|
||||
</div>
|
||||
<NuxtLink class="btn btn-ghost" to="/component-category">
|
||||
Retour au catalogue
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="rounded-xl border border-base-300 bg-base-100 p-6 shadow-sm">
|
||||
<ModelTypeForm
|
||||
mode="create"
|
||||
initial-category="COMPONENT"
|
||||
:lock-category="true"
|
||||
:saving="saving"
|
||||
@submit="handleSubmit"
|
||||
@cancel="handleCancel"
|
||||
/>
|
||||
</section>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { useHead, useRouter } from '#imports'
|
||||
import ModelTypeForm from '~/components/model-types/ModelTypeForm.vue'
|
||||
import { createModelType } from '~/services/modelTypes'
|
||||
import { useToast } from '~/composables/useToast'
|
||||
|
||||
useHead(() => ({
|
||||
title: 'Nouvelle catégorie de composant',
|
||||
}))
|
||||
|
||||
const router = useRouter()
|
||||
const { showError, showSuccess } = useToast()
|
||||
const saving = ref(false)
|
||||
|
||||
const handleCancel = () => {
|
||||
router.push('/component-category').catch(() => {
|
||||
showError("Navigation impossible vers la liste des catégories.")
|
||||
})
|
||||
}
|
||||
|
||||
const handleSubmit = async (payload: Parameters<typeof createModelType>[0]) => {
|
||||
saving.value = true
|
||||
try {
|
||||
const enrichedPayload = {
|
||||
...payload,
|
||||
description: payload.notes ?? null,
|
||||
}
|
||||
await createModelType(enrichedPayload)
|
||||
showSuccess('Catégorie de composant créée avec succès.')
|
||||
await router.push('/component-category')
|
||||
} catch (error: any) {
|
||||
const message = error?.data?.message || error?.message || 'Une erreur est survenue lors de la création.'
|
||||
showError(Array.isArray(message) ? message[0] : message)
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
122
app/pages/piece-category/[id]/edit.vue
Normal file
122
app/pages/piece-category/[id]/edit.vue
Normal file
@@ -0,0 +1,122 @@
|
||||
<template>
|
||||
<main class="mx-auto flex w-full max-w-4xl flex-col gap-8 px-4 py-8 sm:px-6 lg:px-8">
|
||||
<header class="space-y-2">
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-base-content">{{ title }}</h1>
|
||||
<p class="text-base text-base-content/70">
|
||||
Mettez à jour la structure et les champs personnalisés de cette catégorie de pièce pour préparer les futures créations.
|
||||
</p>
|
||||
</div>
|
||||
<NuxtLink class="btn btn-ghost" to="/piece-category">
|
||||
Retour au catalogue
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="rounded-xl border border-base-300 bg-base-100 p-6 shadow-sm">
|
||||
<div v-if="loading" class="flex items-center justify-center py-16">
|
||||
<span class="loading loading-spinner loading-lg" aria-hidden="true"></span>
|
||||
<span class="ml-3 text-sm text-base-content/70">Chargement de la catégorie…</span>
|
||||
</div>
|
||||
<ModelTypeForm
|
||||
v-else
|
||||
mode="edit"
|
||||
initial-category="PIECE"
|
||||
:initial-data="initialData"
|
||||
:lock-category="true"
|
||||
:saving="saving"
|
||||
@submit="handleSubmit"
|
||||
@cancel="handleCancel"
|
||||
/>
|
||||
</section>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useHead, useRoute, useRouter } from '#imports'
|
||||
import ModelTypeForm from '~/components/model-types/ModelTypeForm.vue'
|
||||
import { getModelType, updateModelType, type ModelTypePayload } from '~/services/modelTypes'
|
||||
import { useToast } from '~/composables/useToast'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const { showError, showSuccess } = useToast()
|
||||
|
||||
const loading = ref(true)
|
||||
const saving = ref(false)
|
||||
const initialData = ref<Partial<ModelTypePayload> | null>(null)
|
||||
|
||||
const title = computed(() =>
|
||||
initialData.value?.name ? `Modifier « ${initialData.value.name} »` : 'Modifier une catégorie de pièce',
|
||||
)
|
||||
|
||||
useHead(() => ({
|
||||
title: title.value,
|
||||
}))
|
||||
|
||||
const navigateBackToList = async () => {
|
||||
await router.push('/piece-category').catch(() => {
|
||||
showError("Navigation impossible vers la liste des catégories.")
|
||||
})
|
||||
}
|
||||
|
||||
const normalizeError = (error: any) => {
|
||||
const message = error?.data?.message || error?.message || 'Une erreur est survenue.'
|
||||
return Array.isArray(message) ? message[0] : message
|
||||
}
|
||||
|
||||
const loadCategory = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const id = String(route.params.id)
|
||||
const response = await getModelType(id)
|
||||
|
||||
if (response.category !== 'PIECE') {
|
||||
showError("Cette catégorie n'est pas une pièce.")
|
||||
await navigateBackToList()
|
||||
return
|
||||
}
|
||||
|
||||
initialData.value = {
|
||||
name: response.name,
|
||||
code: response.code,
|
||||
category: response.category,
|
||||
notes: response.notes ?? response.description ?? '',
|
||||
structure: response.structure ?? undefined,
|
||||
}
|
||||
} catch (error) {
|
||||
showError(normalizeError(error))
|
||||
await navigateBackToList()
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleCancel = () => {
|
||||
navigateBackToList()
|
||||
}
|
||||
|
||||
const handleSubmit = async (payload: Parameters<typeof updateModelType>[1]) => {
|
||||
const id = String(route.params.id)
|
||||
saving.value = true
|
||||
try {
|
||||
const enrichedPayload = {
|
||||
...payload,
|
||||
description: payload?.notes ?? null,
|
||||
}
|
||||
await updateModelType(id, enrichedPayload)
|
||||
showSuccess('Catégorie de pièce mise à jour avec succès.')
|
||||
await navigateBackToList()
|
||||
} catch (error) {
|
||||
showError(normalizeError(error))
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadCategory()
|
||||
})
|
||||
</script>
|
||||
68
app/pages/piece-category/new.vue
Normal file
68
app/pages/piece-category/new.vue
Normal file
@@ -0,0 +1,68 @@
|
||||
<template>
|
||||
<main class="mx-auto flex w-full max-w-4xl flex-col gap-8 px-4 py-8 sm:px-6 lg:px-8">
|
||||
<header class="space-y-2">
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-base-content">Nouvelle catégorie de pièce</h1>
|
||||
<p class="text-base text-base-content/70">
|
||||
Définissez les champs personnalisés et le squelette appliqué lors de la création des pièces de cette catégorie.
|
||||
</p>
|
||||
</div>
|
||||
<NuxtLink class="btn btn-ghost" to="/piece-category">
|
||||
Retour au catalogue
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="rounded-xl border border-base-300 bg-base-100 p-6 shadow-sm">
|
||||
<ModelTypeForm
|
||||
mode="create"
|
||||
initial-category="PIECE"
|
||||
:lock-category="true"
|
||||
:saving="saving"
|
||||
@submit="handleSubmit"
|
||||
@cancel="handleCancel"
|
||||
/>
|
||||
</section>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { useHead, useRouter } from '#imports'
|
||||
import ModelTypeForm from '~/components/model-types/ModelTypeForm.vue'
|
||||
import { createModelType } from '~/services/modelTypes'
|
||||
import { useToast } from '~/composables/useToast'
|
||||
|
||||
useHead(() => ({
|
||||
title: 'Nouvelle catégorie de pièce',
|
||||
}))
|
||||
|
||||
const router = useRouter()
|
||||
const { showError, showSuccess } = useToast()
|
||||
const saving = ref(false)
|
||||
|
||||
const handleCancel = () => {
|
||||
router.push('/piece-category').catch(() => {
|
||||
showError("Navigation impossible vers la liste des catégories.")
|
||||
})
|
||||
}
|
||||
|
||||
const handleSubmit = async (payload: Parameters<typeof createModelType>[0]) => {
|
||||
saving.value = true
|
||||
try {
|
||||
const enrichedPayload = {
|
||||
...payload,
|
||||
description: payload.notes ?? null,
|
||||
}
|
||||
await createModelType(enrichedPayload)
|
||||
showSuccess('Catégorie de pièce créée avec succès.')
|
||||
await router.push('/piece-category')
|
||||
} catch (error: any) {
|
||||
const message = error?.data?.message || error?.message || 'Une erreur est survenue lors de la création.'
|
||||
showError(Array.isArray(message) ? message[0] : message)
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -210,16 +210,13 @@ const sanitizeSubcomponents = (components: any[]): ComponentModelStructureNode[]
|
||||
export const normalizeStructureForSave = (input: any): ComponentModelStructure => {
|
||||
const source = cloneStructure(input)
|
||||
|
||||
return {
|
||||
const result: ComponentModelStructure = {
|
||||
customFields: sanitizeCustomFields(source.customFields),
|
||||
pieces: sanitizePieces(source.pieces),
|
||||
subcomponents: sanitizeSubcomponents(source.subcomponents),
|
||||
typeComposantId: source.typeComposantId,
|
||||
typeComposantLabel: source.typeComposantLabel,
|
||||
modelId: source.modelId,
|
||||
familyCode: source.familyCode,
|
||||
alias: source.alias,
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
const hydrateCustomFields = (fields: any[]): any[] => {
|
||||
|
||||
Reference in New Issue
Block a user