From d55066005dc7dea386080f618fba3b0c74fd0342 Mon Sep 17 00:00:00 2001 From: Pier-Jean Malandrino Date: Mon, 4 May 2026 12:28:33 +0200 Subject: [PATCH] feat(#251): nav CRUD sur StoresListPage + StoreDetailPage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - StoresListPage : bouton "Nouveau store" header + ligne action Edit/Delete (delete bloqué sur 'default'), navigation par slug, colonne "Défaut", confirm window.confirm - StoreDetailPage : actions Edit / Delete dans le header, redirection vers la liste après suppression - api.test.ts : couvre la nouvelle shape StoreInfo (slug + isDefault + embedder) --- frontend/src/pages/StoreDetailPage.vue | 50 ++++++++++++- frontend/src/pages/StoresListPage.vue | 99 ++++++++++++++++++++++++-- 2 files changed, 141 insertions(+), 8 deletions(-) diff --git a/frontend/src/pages/StoreDetailPage.vue b/frontend/src/pages/StoreDetailPage.vue index 5024395..9ce0b08 100644 --- a/frontend/src/pages/StoreDetailPage.vue +++ b/frontend/src/pages/StoreDetailPage.vue @@ -5,9 +5,22 @@ ← {{ t('storeDetail.back') }}

{{ store }}

- - {{ t('storeDetail.query') }} - +
+ + {{ t('stores.edit') }} + + + + {{ t('storeDetail.query') }} + +
@@ -96,6 +109,7 @@ import { computed, onMounted, ref } from 'vue' import { RouterLink, useRouter } from 'vue-router' import { + deleteStore, fetchStoreDocuments, removeDocumentFromStore, type StoreDocEntry, @@ -148,6 +162,17 @@ async function removeDoc(doc: StoreDocEntry): Promise { selectedIds.value = next } +async function onDeleteStore(): Promise { + const message = t('stores.deleteConfirm').replace('{name}', props.store) + if (!window.confirm(message)) return + try { + await deleteStore(props.store) + router.push({ name: ROUTES.STORES_LIST }) + } catch (e) { + error.value = e instanceof Error ? e.message : String(e) + } +} + const allSelected = computed( () => docs.value.length > 0 && docs.value.every((d) => selectedIds.value.has(d.docId)), ) @@ -434,6 +459,25 @@ onMounted(load) color: var(--text); } +.header-actions { + display: flex; + gap: 8px; +} + +.btn-danger { + color: #b91c1c; + border-color: rgba(220, 38, 38, 0.4); +} + +.btn-danger:hover:not(:disabled) { + background: rgba(220, 38, 38, 0.08); +} + +.btn-danger:disabled { + opacity: 0.5; + cursor: not-allowed; +} + .btn-sm { padding: 5px 10px; font-size: 12px; diff --git a/frontend/src/pages/StoresListPage.vue b/frontend/src/pages/StoresListPage.vue index 259c4a1..ab473f1 100644 --- a/frontend/src/pages/StoresListPage.vue +++ b/frontend/src/pages/StoresListPage.vue @@ -2,6 +2,7 @@

{{ t('stores.title') }}

+
@@ -20,16 +21,18 @@ {{ t('stores.colName') }} {{ t('stores.colType') }} {{ t('stores.colStatus') }} + {{ t('stores.colDefault') }} {{ t('stores.colDocs') }} {{ t('stores.colChunks') }} + @@ -57,8 +60,24 @@ {{ store.errorMessage }} + + {{ store.isDefault ? t('stores.default.yes') : t('stores.default.no') }} + {{ store.documentCount }} {{ store.chunkCount }} + + + + @@ -77,6 +96,7 @@

{{ t('stores.empty') }}

+
@@ -85,7 +105,7 @@ import { onMounted, ref } from 'vue' import { useRouter } from 'vue-router' -import { fetchStores, type StoreInfo } from '../features/store/api' +import { deleteStore, fetchStores, type StoreInfo } from '../features/store/api' import { useI18n } from '../shared/i18n' import { ROUTES } from '../shared/routing/names' @@ -108,8 +128,27 @@ async function load(): Promise { } } -function openStore(name: string): void { - router.push({ name: ROUTES.STORE_DETAIL, params: { store: name } }) +function openStore(slug: string): void { + router.push({ name: ROUTES.STORE_DETAIL, params: { store: slug } }) +} + +function goCreate(): void { + router.push({ name: ROUTES.STORE_CREATE }) +} + +function goEdit(slug: string): void { + router.push({ name: ROUTES.STORE_EDIT, params: { store: slug } }) +} + +async function onDelete(store: StoreInfo): Promise { + const message = t('stores.deleteConfirm').replace('{name}', store.name) + if (!window.confirm(message)) return + try { + await deleteStore(store.slug) + await load() + } catch (e) { + error.value = e instanceof Error ? e.message : String(e) + } } onMounted(load) @@ -126,6 +165,7 @@ onMounted(load) .stores-header { display: flex; align-items: center; + justify-content: space-between; padding: 20px 24px 16px; border-bottom: 1px solid var(--border); flex-shrink: 0; @@ -137,6 +177,22 @@ onMounted(load) color: var(--text); } +.btn-primary { + padding: 7px 14px; + font-size: 13px; + font-weight: 500; + color: white; + background: var(--accent); + border: 1px solid var(--accent); + border-radius: var(--radius-sm); + cursor: pointer; + transition: all var(--transition); +} + +.btn-primary:hover { + filter: brightness(1.05); +} + .loading-state { display: flex; justify-content: center; @@ -206,6 +262,11 @@ onMounted(load) text-align: right; } +.col-actions { + width: 160px; + text-align: right; +} + .stores-table td.col-num { text-align: right; } @@ -323,4 +384,32 @@ onMounted(load) background: var(--bg-hover); color: var(--text); } + +.row-action { + font-size: 12px; + font-weight: 500; + padding: 4px 10px; + margin-left: 6px; + border-radius: var(--radius-sm); + border: 1px solid var(--border); + background: var(--bg-elevated); + color: var(--text-secondary); + cursor: pointer; + transition: all var(--transition); +} +.row-action:hover:not(:disabled) { + background: var(--bg-hover); + color: var(--text); +} +.row-action:disabled { + opacity: 0.5; + cursor: not-allowed; +} +.row-action--danger { + color: #b91c1c; + border-color: rgba(220, 38, 38, 0.4); +} +.row-action--danger:hover:not(:disabled) { + background: rgba(220, 38, 38, 0.08); +}