Frontend store API client : - Types StoreInfo (avec slug + isDefault + embedder), StoreDetail, StoreCreatePayload, StoreUpdatePayload - Fonctions createStore (POST), updateStore (PATCH), deleteStore (DELETE), fetchStore (GET /:slug) - Tests Vitest étendus pour les 4 nouveaux endpoints Composants formulaire (features/store/ui/) : - StoreForm : champs communs (name/slug/kind/embedder/isDefault) avec validation slug locale, lock du slug pour 'default' - StoreConfigForm : dispatcher dynamique par kind (extensible) - OpenSearchConfigForm : champ indexName, validation requise Pages CRUD : - StoreCreatePage (/index/new) : formulaire + redirect liste - StoreEditPage (/index/:store/edit) : préremplit via fetchStore, redirige vers la fiche Routes + i18n : - Routes ROUTES.STORE_CREATE et ROUTES.STORE_EDIT ajoutées (names + routes.ts) - Clés i18n FR/EN sous stores.* et storeForm.*
118 lines
2.6 KiB
Vue
118 lines
2.6 KiB
Vue
<template>
|
|
<div class="store-edit-page">
|
|
<header class="header">
|
|
<h1 class="title">{{ t('storeForm.titleEdit') }}</h1>
|
|
</header>
|
|
|
|
<div v-if="loading" class="loading-state">
|
|
<div class="spinner" />
|
|
</div>
|
|
|
|
<div v-else-if="loadError" class="error-state">
|
|
<p class="error-text">{{ loadError }}</p>
|
|
</div>
|
|
|
|
<StoreForm
|
|
v-else-if="initial"
|
|
mode="edit"
|
|
:initial-value="initial"
|
|
:submitting="submitting"
|
|
:error-message="errorMessage"
|
|
:lock-slug="initial.slug === 'default'"
|
|
@submit="onSubmit"
|
|
@cancel="onCancel"
|
|
/>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { onMounted, ref } from 'vue'
|
|
import { useRouter } from 'vue-router'
|
|
import StoreForm from '../features/store/ui/StoreForm.vue'
|
|
import {
|
|
fetchStore,
|
|
updateStore,
|
|
type StoreCreatePayload,
|
|
type StoreDetail,
|
|
} from '../features/store/api'
|
|
import { ROUTES } from '../shared/routing/names'
|
|
import { useI18n } from '../shared/i18n'
|
|
|
|
const props = defineProps<{ store: string }>()
|
|
|
|
const { t } = useI18n()
|
|
const router = useRouter()
|
|
|
|
const initial = ref<StoreDetail | null>(null)
|
|
const loading = ref(true)
|
|
const loadError = ref<string | null>(null)
|
|
const submitting = ref(false)
|
|
const errorMessage = ref<string | null>(null)
|
|
|
|
onMounted(async () => {
|
|
try {
|
|
initial.value = await fetchStore(props.store)
|
|
} catch (err) {
|
|
loadError.value = err instanceof Error ? err.message : String(err)
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
})
|
|
|
|
async function onSubmit(payload: StoreCreatePayload) {
|
|
submitting.value = true
|
|
errorMessage.value = null
|
|
try {
|
|
const updated = await updateStore(props.store, payload)
|
|
if (updated.slug !== props.store) {
|
|
router.push({ name: ROUTES.STORE_DETAIL, params: { store: updated.slug } })
|
|
} else {
|
|
router.push({ name: ROUTES.STORE_DETAIL, params: { store: updated.slug } })
|
|
}
|
|
} catch (err) {
|
|
errorMessage.value = err instanceof Error ? err.message : String(err)
|
|
} finally {
|
|
submitting.value = false
|
|
}
|
|
}
|
|
|
|
function onCancel() {
|
|
router.push({ name: ROUTES.STORES_LIST })
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.store-edit-page {
|
|
padding: 1.5rem 2rem;
|
|
}
|
|
.header {
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
.title {
|
|
font-size: 1.5rem;
|
|
font-weight: 600;
|
|
margin: 0;
|
|
}
|
|
.loading-state,
|
|
.error-state {
|
|
padding: 2rem;
|
|
display: flex;
|
|
justify-content: center;
|
|
}
|
|
.error-text {
|
|
color: #dc2626;
|
|
}
|
|
.spinner {
|
|
width: 2rem;
|
|
height: 2rem;
|
|
border: 3px solid #e5e7eb;
|
|
border-top-color: #2563eb;
|
|
border-radius: 50%;
|
|
animation: spin 0.6s linear infinite;
|
|
}
|
|
@keyframes spin {
|
|
to {
|
|
transform: rotate(360deg);
|
|
}
|
|
}
|
|
</style>
|