feat(catalog) : M7 — écran Ajouter un stockage /admin/storages/new (ERP-217)
- Formulaire de création à plat (pas d'onglets, HP-M7-06), gate catalog.storages.manage - Champs Site, Type de stockage, Numéro, État (multi ≥1) en composants Malio, validation inline 422 par champ via useFormErrors - 409 doublon (site, type, numéro) RG-7.01 → erreur inline sous Numéro + toast explicite - Composable useStorageForm (POST /storages, payload relations en IRI), libellés i18n - Référentiel des types PLAT : pas de cascade Site→Type (RG-7.03 non portée côté back, StorageType sans relation Site — à reclarifier spec) - Tests Vitest de useStorageForm (référentiel plat, submit, 409/422)
This commit is contained in:
@@ -1117,9 +1117,20 @@
|
|||||||
"apply": "Voir les résultats",
|
"apply": "Voir les résultats",
|
||||||
"reset": "Réinitialiser"
|
"reset": "Réinitialiser"
|
||||||
},
|
},
|
||||||
|
"form": {
|
||||||
|
"title": "Ajouter un stockage",
|
||||||
|
"back": "Retour à la liste",
|
||||||
|
"submit": "Valider",
|
||||||
|
"site": "Site",
|
||||||
|
"storageType": "Type de stockage",
|
||||||
|
"numero": "Numéro",
|
||||||
|
"states": "État du type de stockage",
|
||||||
|
"duplicateNumero": "Un stockage avec ce site, ce type et ce numéro existe déjà."
|
||||||
|
},
|
||||||
"toast": {
|
"toast": {
|
||||||
"error": "Une erreur est survenue. Réessayez.",
|
"error": "Une erreur est survenue. Réessayez.",
|
||||||
"exportError": "L'export du répertoire stockage a échoué. Réessayez."
|
"exportError": "L'export du répertoire stockage a échoué. Réessayez.",
|
||||||
|
"createSuccess": "Stockage créé avec succès"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,189 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||||
|
import { useFormErrors } from '~/shared/composables/useFormErrors'
|
||||||
|
import { useStorageForm } from '../useStorageForm'
|
||||||
|
|
||||||
|
// Stubs des auto-imports Nuxt consommes par le composable + ses dependances.
|
||||||
|
const mockGet = vi.hoisted(() => vi.fn())
|
||||||
|
const mockPost = vi.hoisted(() => vi.fn())
|
||||||
|
const mockToastSuccess = vi.hoisted(() => vi.fn())
|
||||||
|
const mockToastError = vi.hoisted(() => vi.fn())
|
||||||
|
|
||||||
|
vi.stubGlobal('useApi', () => ({
|
||||||
|
get: mockGet,
|
||||||
|
post: mockPost,
|
||||||
|
put: vi.fn(),
|
||||||
|
patch: vi.fn(),
|
||||||
|
delete: vi.fn(),
|
||||||
|
}))
|
||||||
|
vi.stubGlobal('useToast', () => ({
|
||||||
|
success: mockToastSuccess,
|
||||||
|
error: mockToastError,
|
||||||
|
}))
|
||||||
|
// useFormErrors est un auto-import Nuxt : on expose l'implementation reelle
|
||||||
|
// (elle consomme useToast/useI18n deja stubbes) pour tester l'integration 422/409.
|
||||||
|
vi.stubGlobal('useFormErrors', useFormErrors)
|
||||||
|
vi.stubGlobal('useI18n', () => ({
|
||||||
|
t: (key: string, params?: Record<string, unknown>) =>
|
||||||
|
params ? `${key}::${JSON.stringify(params)}` : key,
|
||||||
|
}))
|
||||||
|
|
||||||
|
/** Referentiel PLAT des types de stockage (renvoye tel quel, sans filtre site). */
|
||||||
|
const STORAGE_TYPES = {
|
||||||
|
member: [
|
||||||
|
{ '@id': '/api/storage_types/9', label: 'Cellule' },
|
||||||
|
{ '@id': '/api/storage_types/5', label: 'Tas' },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('useStorageForm', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
mockGet.mockReset()
|
||||||
|
mockPost.mockReset()
|
||||||
|
mockToastSuccess.mockReset()
|
||||||
|
mockToastError.mockReset()
|
||||||
|
|
||||||
|
// Routage des GET par url (referentiels). Le type de stockage est un
|
||||||
|
// referentiel plat : meme reponse quelle que soit la requete.
|
||||||
|
mockGet.mockImplementation((url: string) => {
|
||||||
|
if (url === '/sites') {
|
||||||
|
return Promise.resolve({ member: [{ '@id': '/api/sites/1', name: 'Chatellerault' }] })
|
||||||
|
}
|
||||||
|
if (url === '/storage_types') {
|
||||||
|
return Promise.resolve(STORAGE_TYPES)
|
||||||
|
}
|
||||||
|
return Promise.resolve({ member: [] })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('referentiel plat — pas de cascade Site->Type (RG-7.03 non portee back)', () => {
|
||||||
|
it('loadReferentials charge les sites et TOUS les types, sans filtre site', async () => {
|
||||||
|
const { siteOptions, storageTypeOptions, loadReferentials } = useStorageForm()
|
||||||
|
await loadReferentials()
|
||||||
|
|
||||||
|
const storageCall = mockGet.mock.calls.find(c => c[0] === '/storage_types')
|
||||||
|
expect(storageCall).toBeDefined()
|
||||||
|
// Aucun filtre siteId envoye (referentiel plat).
|
||||||
|
expect(storageCall?.[1]).not.toHaveProperty('siteId[]')
|
||||||
|
|
||||||
|
expect(siteOptions.value.map(o => o.value)).toEqual(['/api/sites/1'])
|
||||||
|
expect(storageTypeOptions.value.map(o => o.value)).toEqual([
|
||||||
|
'/api/storage_types/9',
|
||||||
|
'/api/storage_types/5',
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('changer de site ne recharge pas les types ni ne purge la selection', async () => {
|
||||||
|
const { form, setSite, setStorageType, loadReferentials } = useStorageForm()
|
||||||
|
await loadReferentials()
|
||||||
|
const callsBefore = mockGet.mock.calls.filter(c => c[0] === '/storage_types').length
|
||||||
|
|
||||||
|
setStorageType('/api/storage_types/9')
|
||||||
|
setSite('/api/sites/1')
|
||||||
|
|
||||||
|
expect(form.siteIri).toBe('/api/sites/1')
|
||||||
|
// Selection conservee : pas de cascade ni de purge par site.
|
||||||
|
expect(form.storageTypeIri).toBe('/api/storage_types/9')
|
||||||
|
const callsAfter = mockGet.mock.calls.filter(c => c[0] === '/storage_types').length
|
||||||
|
expect(callsAfter).toBe(callsBefore)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('submit — POST /storages', () => {
|
||||||
|
function fillValidForm(form: ReturnType<typeof useStorageForm>['form']): void {
|
||||||
|
form.siteIri = '/api/sites/1'
|
||||||
|
form.storageTypeIri = '/api/storage_types/9'
|
||||||
|
form.numero = '12'
|
||||||
|
form.states = ['RECEPTION', 'PRODUCTION']
|
||||||
|
}
|
||||||
|
|
||||||
|
it('poste le payload (relations en IRI) et retourne true au succes', async () => {
|
||||||
|
mockPost.mockResolvedValueOnce({ id: 42 })
|
||||||
|
const { form, submit } = useStorageForm()
|
||||||
|
fillValidForm(form)
|
||||||
|
|
||||||
|
const ok = await submit()
|
||||||
|
|
||||||
|
expect(ok).toBe(true)
|
||||||
|
expect(mockPost).toHaveBeenCalledWith(
|
||||||
|
'/storages',
|
||||||
|
{
|
||||||
|
numero: '12',
|
||||||
|
states: ['RECEPTION', 'PRODUCTION'],
|
||||||
|
site: '/api/sites/1',
|
||||||
|
storageType: '/api/storage_types/9',
|
||||||
|
},
|
||||||
|
expect.objectContaining({ toast: false }),
|
||||||
|
)
|
||||||
|
expect(mockToastSuccess).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('omet `site` / `storageType` du payload quand la relation n\'est pas choisie', async () => {
|
||||||
|
// Envoyer null casserait la denormalisation back (IRI attendu) et
|
||||||
|
// court-circuiterait les autres violations -> on omet la cle.
|
||||||
|
mockPost.mockResolvedValueOnce({ id: 43 })
|
||||||
|
const { form, submit } = useStorageForm()
|
||||||
|
fillValidForm(form)
|
||||||
|
form.siteIri = null
|
||||||
|
form.storageTypeIri = null
|
||||||
|
|
||||||
|
await submit()
|
||||||
|
|
||||||
|
const payload = mockPost.mock.calls[0][1]
|
||||||
|
expect(payload).not.toHaveProperty('site')
|
||||||
|
expect(payload).not.toHaveProperty('storageType')
|
||||||
|
// numero envoye en chaine vide si non saisi (NotBlank cote back).
|
||||||
|
expect(payload).toHaveProperty('numero')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('mappe un 409 doublon (site, type, numero) sur errors.numero + toast explicite', async () => {
|
||||||
|
mockPost.mockRejectedValueOnce({ response: { status: 409, _data: {} } })
|
||||||
|
const { form, errors, submit } = useStorageForm()
|
||||||
|
fillValidForm(form)
|
||||||
|
|
||||||
|
const ok = await submit()
|
||||||
|
|
||||||
|
expect(ok).toBe(false)
|
||||||
|
expect(errors.numero).toBe('admin.storages.form.duplicateNumero')
|
||||||
|
expect(mockToastError).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('mappe une 422 inline par champ (errors.numero) sans toast', async () => {
|
||||||
|
mockPost.mockRejectedValueOnce({
|
||||||
|
response: {
|
||||||
|
status: 422,
|
||||||
|
_data: { violations: [{ propertyPath: 'numero', message: 'Le numéro du stockage est obligatoire.' }] },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const { form, errors, submit } = useStorageForm()
|
||||||
|
fillValidForm(form)
|
||||||
|
form.numero = null
|
||||||
|
|
||||||
|
const ok = await submit()
|
||||||
|
|
||||||
|
expect(ok).toBe(false)
|
||||||
|
expect(errors.numero).toBe('Le numéro du stockage est obligatoire.')
|
||||||
|
expect(mockToastError).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('mappe une 422 sur site / storageType / states (NotNull / Count)', async () => {
|
||||||
|
mockPost.mockRejectedValueOnce({
|
||||||
|
response: {
|
||||||
|
status: 422,
|
||||||
|
_data: { violations: [
|
||||||
|
{ propertyPath: 'site', message: 'Le site est obligatoire.' },
|
||||||
|
{ propertyPath: 'storageType', message: 'Le type de stockage est obligatoire.' },
|
||||||
|
{ propertyPath: 'states', message: 'Sélectionnez au moins un état.' },
|
||||||
|
] },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const { form, errors, submit } = useStorageForm()
|
||||||
|
fillValidForm(form)
|
||||||
|
|
||||||
|
await submit()
|
||||||
|
|
||||||
|
expect(errors.site).toBe('Le site est obligatoire.')
|
||||||
|
expect(errors.storageType).toBe('Le type de stockage est obligatoire.')
|
||||||
|
expect(errors.states).toBe('Sélectionnez au moins un état.')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
/**
|
||||||
|
* Composable du formulaire de creation d'un stockage (M7 — ERP-217).
|
||||||
|
*
|
||||||
|
* Porte l'etat du formulaire principal (a plat, PAS d'onglets — HP-M7-06), les
|
||||||
|
* referentiels des selects et la soumission `POST /api/storages` avec mapping des
|
||||||
|
* erreurs 422/409 inline (useFormErrors, ERP-101). Reference : ecran « Ajouter un
|
||||||
|
* produit » (M6) / ecran Client.
|
||||||
|
*
|
||||||
|
* Referentiel des types de stockage : PLAT (RG-6.06 / decision back). Le concept
|
||||||
|
* type<->site a ete retire en M6 (jointure storage_type_site droppee, migration
|
||||||
|
* Version20260626100000) et `StorageType` n'a plus de relation Site ; le provider
|
||||||
|
* ignore tout filtre `?siteId[]`. La cascade Site->Type de RG-7.03 n'est donc PAS
|
||||||
|
* portee (decision produit du 30/06 : referentiel plat, fidele au back ; RG-7.03 a
|
||||||
|
* reclarifier cote spec). On charge donc TOUS les types une fois, Site et Type
|
||||||
|
* independants.
|
||||||
|
*
|
||||||
|
* Etat 100 % local a l'instance.
|
||||||
|
*/
|
||||||
|
import { reactive, ref } from 'vue'
|
||||||
|
import {
|
||||||
|
useSiteOptions,
|
||||||
|
useStorageTypeOptions,
|
||||||
|
} from '~/modules/catalog/composables/useProductOptions'
|
||||||
|
|
||||||
|
/** Etats d'un stockage (miroir de l'enum back Storage::STATE_*, RG-7.04). */
|
||||||
|
export const STORAGE_STATES = ['RECEPTION', 'PRODUCTION', 'TRIAGE'] as const
|
||||||
|
|
||||||
|
export function useStorageForm() {
|
||||||
|
const api = useApi()
|
||||||
|
const { t } = useI18n()
|
||||||
|
const toast = useToast()
|
||||||
|
const formErrors = useFormErrors()
|
||||||
|
|
||||||
|
const sites = useSiteOptions()
|
||||||
|
const storageTypes = useStorageTypeOptions()
|
||||||
|
|
||||||
|
// ── Etat du formulaire ───────────────────────────────────────────────────
|
||||||
|
// Les relations (site, storageType) sont stockees en IRI (envoyees telles
|
||||||
|
// quelles au POST) ; `states` porte les codes enum.
|
||||||
|
const form = reactive({
|
||||||
|
siteIri: null as string | null,
|
||||||
|
storageTypeIri: null as string | null,
|
||||||
|
numero: null as string | null,
|
||||||
|
states: [] as string[],
|
||||||
|
})
|
||||||
|
|
||||||
|
const submitting = ref(false)
|
||||||
|
|
||||||
|
/** Met a jour le site (select simple, RG-7.02). */
|
||||||
|
function setSite(iri: string | null): void {
|
||||||
|
form.siteIri = iri
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Met a jour le type de stockage (select simple, referentiel plat). */
|
||||||
|
function setStorageType(iri: string | null): void {
|
||||||
|
form.storageTypeIri = iri
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Met a jour les etats (multi-select, >= 1, RG-7.04). */
|
||||||
|
function setStates(states: string[]): void {
|
||||||
|
form.states = states
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Charge les referentiels (sites + TOUS les types de stockage). Resilient :
|
||||||
|
* un referentiel en echec reste vide sans casser l'autre. Pas de cascade par
|
||||||
|
* site (referentiel plat, cf. docblock).
|
||||||
|
*/
|
||||||
|
async function loadReferentials(): Promise<void> {
|
||||||
|
await Promise.allSettled([sites.load(), storageTypes.load()])
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Soumet la creation. Retourne true au succes (la page redirige), false sinon.
|
||||||
|
* 422 → mapping inline par champ (useFormErrors, `{ toast: false }`) ; 409
|
||||||
|
* doublon du triplet (site, type, numero, RG-7.01) → erreur inline sur `numero`
|
||||||
|
* (propertyPath exploitable cote back) + toast explicite.
|
||||||
|
*/
|
||||||
|
async function submit(): Promise<boolean> {
|
||||||
|
if (submitting.value) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
submitting.value = true
|
||||||
|
formErrors.clearErrors()
|
||||||
|
try {
|
||||||
|
const payload: Record<string, unknown> = {
|
||||||
|
// Chaine vide (jamais null) : le setter back setNumero attend un
|
||||||
|
// `string` non-nullable -> envoyer null leverait une erreur de type
|
||||||
|
// (denormalisation) qui court-circuiterait les autres violations.
|
||||||
|
// Avec '', la contrainte NotBlank renvoie un message propre par champ.
|
||||||
|
numero: form.numero ?? '',
|
||||||
|
states: form.states,
|
||||||
|
}
|
||||||
|
// `site` / `storageType` attendent un IRI (string) : envoyer null
|
||||||
|
// declencherait une erreur de denormalisation API Platform qui
|
||||||
|
// court-circuiterait TOUTES les autres violations. On omet la cle quand
|
||||||
|
// la relation n'est pas choisie -> la contrainte NotNull renvoie un
|
||||||
|
// message propre, et les autres champs sont valides dans la meme 422.
|
||||||
|
if (form.siteIri) {
|
||||||
|
payload.site = form.siteIri
|
||||||
|
}
|
||||||
|
if (form.storageTypeIri) {
|
||||||
|
payload.storageType = form.storageTypeIri
|
||||||
|
}
|
||||||
|
|
||||||
|
const options = { headers: { Accept: 'application/ld+json' }, toast: false }
|
||||||
|
await api.post('/storages', payload, options)
|
||||||
|
toast.success({ title: t('admin.storages.toast.createSuccess') })
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
const status = (error as { response?: { status?: number } })?.response?.status
|
||||||
|
if (status === 409) {
|
||||||
|
// Doublon (site, type, numero) RG-7.01 : inline sur `numero` + toast.
|
||||||
|
const message = t('admin.storages.form.duplicateNumero')
|
||||||
|
formErrors.setError('numero', message)
|
||||||
|
toast.error({ title: t('admin.storages.toast.error'), message })
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
formErrors.handleApiError(error, { fallbackMessage: t('admin.storages.toast.error') })
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
submitting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
form,
|
||||||
|
errors: formErrors.errors,
|
||||||
|
submitting,
|
||||||
|
siteOptions: sites.options,
|
||||||
|
storageTypeOptions: storageTypes.options,
|
||||||
|
setSite,
|
||||||
|
setStorageType,
|
||||||
|
setStates,
|
||||||
|
loadReferentials,
|
||||||
|
submit,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<!-- En-tete : retour vers la liste + titre. -->
|
||||||
|
<div class="flex items-center gap-3 pt-11">
|
||||||
|
<MalioButtonIcon
|
||||||
|
icon="mdi:arrow-left-bold"
|
||||||
|
icon-size="24"
|
||||||
|
variant="ghost"
|
||||||
|
:title="t('admin.storages.form.back')"
|
||||||
|
v-bind="{ ariaLabel: t('admin.storages.form.back') }"
|
||||||
|
@click="goBack"
|
||||||
|
/>
|
||||||
|
<h1 class="text-[30px] font-semibold text-m-primary">{{ t('admin.storages.form.title') }}</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ── Formulaire principal de creation (a plat, PAS d'onglets — HP-M7-06)
|
||||||
|
Bouton « Valider » TOUJOURS actif (ERP-101) : la validation
|
||||||
|
autoritaire est serveur, les erreurs 422 reviennent inline. -->
|
||||||
|
<div class="mt-[48px] grid grid-cols-3 xl:grid-cols-4 gap-x-[44px] gap-y-4">
|
||||||
|
<!-- Site : select simple obligatoire (RG-7.02). -->
|
||||||
|
<MalioSelect
|
||||||
|
:model-value="form.siteIri"
|
||||||
|
:options="siteOptions"
|
||||||
|
:label="t('admin.storages.form.site')"
|
||||||
|
empty-option-label=""
|
||||||
|
:required="true"
|
||||||
|
:error="errors.site"
|
||||||
|
@update:model-value="(v: string | number | null) => setSite(v === null || v === '' ? null : String(v))"
|
||||||
|
/>
|
||||||
|
<!-- Type de stockage : select simple obligatoire. Referentiel plat :
|
||||||
|
tous les types (pas de cascade par site, RG-7.03 non portee back). -->
|
||||||
|
<MalioSelect
|
||||||
|
:model-value="form.storageTypeIri"
|
||||||
|
:options="storageTypeOptions"
|
||||||
|
:label="t('admin.storages.form.storageType')"
|
||||||
|
empty-option-label=""
|
||||||
|
:required="true"
|
||||||
|
:error="errors.storageType"
|
||||||
|
@update:model-value="(v: string | number | null) => setStorageType(v === null || v === '' ? null : String(v))"
|
||||||
|
/>
|
||||||
|
<!-- Numero : texte libre obligatoire (RG-7.01, normalise trim cote serveur). -->
|
||||||
|
<MalioInputText
|
||||||
|
v-model="form.numero"
|
||||||
|
:mask="FREE_TEXT_MASK"
|
||||||
|
:label="t('admin.storages.form.numero')"
|
||||||
|
:required="true"
|
||||||
|
:error="errors.numero"
|
||||||
|
/>
|
||||||
|
<!-- Etat du type de stockage : multi-select obligatoire (>= 1, RG-7.04). -->
|
||||||
|
<MalioSelectCheckbox
|
||||||
|
:model-value="form.states"
|
||||||
|
:options="stateOptions"
|
||||||
|
:max-tags="3"
|
||||||
|
:label="t('admin.storages.form.states')"
|
||||||
|
:display-tag="true"
|
||||||
|
:required="true"
|
||||||
|
:error="errors.states"
|
||||||
|
@update:model-value="(v: (string | number)[]) => setStates(v.map(String))"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-12 flex justify-center">
|
||||||
|
<MalioButton
|
||||||
|
variant="primary"
|
||||||
|
:label="t('admin.storages.form.submit')"
|
||||||
|
:disabled="submitting"
|
||||||
|
@click="onSubmit"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Onglets de la maquette (Clients / Règles / Etiquette / Comptabilité) :
|
||||||
|
HORS perimetre HP-M7-06 — aucune barre d'onglets a l'ajout. -->
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, onMounted } from 'vue'
|
||||||
|
import { useStorageForm, STORAGE_STATES } from '~/modules/catalog/composables/useStorageForm'
|
||||||
|
import { FREE_TEXT_MASK } from '~/shared/utils/textSanitize'
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
const router = useRouter()
|
||||||
|
const { can } = usePermissions()
|
||||||
|
|
||||||
|
useHead({ title: t('admin.storages.form.title') })
|
||||||
|
|
||||||
|
// Gating de la route : la creation est reservee a `manage` (repertoire admin-only).
|
||||||
|
if (!can('catalog.storages.manage')) {
|
||||||
|
await navigateTo('/admin/storages')
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
form,
|
||||||
|
errors,
|
||||||
|
submitting,
|
||||||
|
siteOptions,
|
||||||
|
storageTypeOptions,
|
||||||
|
setSite,
|
||||||
|
setStorageType,
|
||||||
|
setStates,
|
||||||
|
loadReferentials,
|
||||||
|
submit,
|
||||||
|
} = useStorageForm()
|
||||||
|
|
||||||
|
// Options de l'etat : libelles i18n (la valeur d'option = code enum).
|
||||||
|
const stateOptions = computed(() =>
|
||||||
|
STORAGE_STATES.map(code => ({ value: code, label: t(`admin.storages.state.${code}`) })),
|
||||||
|
)
|
||||||
|
|
||||||
|
/** Retour vers la liste des stockages (fleche d'en-tete). */
|
||||||
|
function goBack(): void {
|
||||||
|
router.push('/admin/storages')
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Soumet la creation ; au succes, retour a la liste. */
|
||||||
|
async function onSubmit(): Promise<void> {
|
||||||
|
const ok = await submit()
|
||||||
|
if (ok) {
|
||||||
|
router.push('/admin/storages')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
// Echec du chargement des referentiels non bloquant : les selects restent vides.
|
||||||
|
loadReferentials().catch(() => {})
|
||||||
|
})
|
||||||
|
</script>
|
||||||
Reference in New Issue
Block a user