@@ -0,0 +1,5 @@
|
||||
import AdminSavedViewsPage from './AdminSavedViewsPage.vue';
|
||||
|
||||
export default { title: 'Pages/AdminSavedViewsPage', component: AdminSavedViewsPage };
|
||||
|
||||
export const SavedViews = {};
|
||||
@@ -0,0 +1,86 @@
|
||||
<script setup>
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import { createAdminSavedView, deleteAdminSavedView, listAdminSavedViews } from '@/services/admin-saved-view-service.js';
|
||||
|
||||
const views = ref([]);
|
||||
const name = ref('');
|
||||
const filters = ref('{}');
|
||||
const isLoading = ref(true);
|
||||
const isSaving = ref(false);
|
||||
const error = ref('');
|
||||
const message = ref('');
|
||||
|
||||
function csrfToken() {
|
||||
return document.cookie.split('; ').find((value) => value.startsWith('pokemon_inventory_csrf='))?.split('=')[1] ?? '';
|
||||
}
|
||||
|
||||
async function load() {
|
||||
try {
|
||||
views.value = await listAdminSavedViews();
|
||||
} catch (requestError) {
|
||||
error.value = requestError.message;
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function createView() {
|
||||
isSaving.value = true;
|
||||
error.value = '';
|
||||
message.value = '';
|
||||
try {
|
||||
const view = await createAdminSavedView({ name: name.value, filters: JSON.parse(filters.value) }, csrfToken());
|
||||
views.value.push(view);
|
||||
name.value = '';
|
||||
filters.value = '{}';
|
||||
message.value = 'Saved view created.';
|
||||
} catch (requestError) {
|
||||
error.value = requestError instanceof SyntaxError ? 'Filters must be valid JSON.' : requestError.message;
|
||||
} finally {
|
||||
isSaving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function removeView(id) {
|
||||
isSaving.value = true;
|
||||
try {
|
||||
await deleteAdminSavedView(id, csrfToken());
|
||||
views.value = views.value.filter((view) => view.id !== id);
|
||||
} catch (requestError) {
|
||||
error.value = requestError.message;
|
||||
} finally {
|
||||
isSaving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(load);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="admin-page">
|
||||
<p class="catalog-kicker">Inventory management</p>
|
||||
<h1>Saved views</h1>
|
||||
<p v-if="isLoading" class="state state--loading" role="status">Loading saved views...</p>
|
||||
<p v-if="message" class="state" role="status">{{ message }}</p>
|
||||
<p v-if="error" class="state state--error" role="alert">{{ error }}</p>
|
||||
<form v-if="!isLoading" class="showcase-panel" @submit.prevent="createView">
|
||||
<h2>New saved view</h2>
|
||||
<label class="fixture-label" for="saved-view-name">Name</label>
|
||||
<input id="saved-view-name" v-model="name" class="fixture-input" required maxlength="100" />
|
||||
<label class="fixture-label" for="saved-view-filters">Filters (JSON)</label>
|
||||
<textarea id="saved-view-filters" v-model="filters" class="fixture-input" rows="4" required />
|
||||
<button class="button button--primary" type="submit" :disabled="isSaving">Save view</button>
|
||||
</form>
|
||||
<section class="admin-list" aria-labelledby="saved-views-title">
|
||||
<h2 id="saved-views-title">Your saved views</h2>
|
||||
<p v-if="!views.length && !isLoading" class="state state--empty">No saved views yet.</p>
|
||||
<ul v-else>
|
||||
<li v-for="view in views" :key="view.id">
|
||||
<strong>{{ view.name }}</strong> · {{ JSON.stringify(view.filters) }}
|
||||
<button class="button button--secondary" type="button" :disabled="isSaving" @click="removeView(view.id)">Delete</button>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
</section>
|
||||
</template>
|
||||
@@ -20,6 +20,7 @@ async function logout() {
|
||||
<nav aria-label="Admin navigation">
|
||||
<a href="/admin">Overview</a>
|
||||
<RouterLink :to="{ name: 'admin-settings' }">Settings</RouterLink>
|
||||
<RouterLink :to="{ name: 'admin-saved-views' }">Saved views</RouterLink>
|
||||
</nav>
|
||||
<div class="admin-sidebar__footer">
|
||||
<span>{{ principal?.email }}</span>
|
||||
|
||||
@@ -9,6 +9,7 @@ import LoginPage from '@/pages/LoginPage.vue';
|
||||
import { useAuth } from '@/composables/useAuth.js';
|
||||
import AdminImagePage from '@/pages/AdminImagePage.vue';
|
||||
import AdminSettingsPage from '@/pages/AdminSettingsPage.vue';
|
||||
import AdminSavedViewsPage from '@/pages/AdminSavedViewsPage.vue';
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
@@ -24,6 +25,7 @@ const router = createRouter({
|
||||
{ path: '', name: 'admin-home', component: AdminHomePage },
|
||||
{ path: 'inventory/:id/images', name: 'admin-image-manager', component: AdminImagePage },
|
||||
{ path: 'settings', name: 'admin-settings', component: AdminSettingsPage },
|
||||
{ path: 'saved-views', name: 'admin-saved-views', component: AdminSavedViewsPage },
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
const apiBaseUrl = import.meta.env?.VITE_API_BASE_URL ?? '/api/v1';
|
||||
|
||||
async function request(path, options = {}) {
|
||||
const response = await fetch(`${apiBaseUrl}${path}`, {
|
||||
credentials: 'include',
|
||||
...options,
|
||||
headers: { 'content-type': 'application/json', ...options.headers },
|
||||
});
|
||||
const payload = await response.json().catch(() => null);
|
||||
if (!response.ok) throw new Error(payload?.error?.message ?? 'Saved view request failed.');
|
||||
return payload?.data;
|
||||
}
|
||||
|
||||
export function listAdminSavedViews() { return request('/admin/saved-views'); }
|
||||
export function createAdminSavedView(view, csrfToken) {
|
||||
return request('/admin/saved-views', { method: 'POST', headers: { 'x-csrf-token': csrfToken }, body: JSON.stringify(view) });
|
||||
}
|
||||
export function deleteAdminSavedView(id, csrfToken) {
|
||||
return request(`/admin/saved-views/${id}`, { method: 'DELETE', headers: { 'x-csrf-token': csrfToken } });
|
||||
}
|
||||
Reference in New Issue
Block a user