feat(spa): fixedvalues.vue per-type list page
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
import { mount, flushPromises } from "@vue/test-utils";
|
||||
import { setActivePinia, createPinia } from "pinia";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { createMemoryHistory, createRouter } from "vue-router";
|
||||
import PrimeVue from "primevue/config";
|
||||
import ToastService from "primevue/toastservice";
|
||||
import ConfirmationService from "primevue/confirmationservice";
|
||||
|
||||
vi.mock("../api/client", () => ({
|
||||
api: {
|
||||
modules: {
|
||||
list: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
duplicate: vi.fn(),
|
||||
favorite: vi.fn(),
|
||||
},
|
||||
categories: { list: vi.fn().mockResolvedValue({ items: [] }) },
|
||||
},
|
||||
ApiError: class extends Error {
|
||||
constructor(public status: number, message: string) { super(message); }
|
||||
},
|
||||
}));
|
||||
|
||||
import { api } from "../api/client";
|
||||
import FixedValues from "../views/FixedValues.vue";
|
||||
|
||||
const apiMod = api.modules as unknown as Record<string, ReturnType<typeof vi.fn>>;
|
||||
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia());
|
||||
Object.values(apiMod).forEach((fn) => fn.mockReset());
|
||||
});
|
||||
afterEach(() => vi.clearAllMocks());
|
||||
|
||||
function makeRouter() {
|
||||
return createRouter({
|
||||
history: createMemoryHistory(),
|
||||
routes: [
|
||||
{ path: "/", component: { template: "<div/>" } },
|
||||
{ path: "/fixed-values/new", component: { template: "<div/>" } },
|
||||
{ path: "/fixed-values/:id/edit", name: "fixed-values-edit", component: { template: "<div/>" } },
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
function mountView() {
|
||||
return mount(FixedValues, {
|
||||
global: { plugins: [makeRouter(), PrimeVue, ToastService, ConfirmationService] },
|
||||
});
|
||||
}
|
||||
|
||||
describe("FixedValues.vue", () => {
|
||||
it("filters store by type=fixed_values on mount", async () => {
|
||||
apiMod.list.mockResolvedValue({ items: [], total: 0 });
|
||||
mountView();
|
||||
await flushPromises();
|
||||
expect(apiMod.list).toHaveBeenCalledWith(expect.objectContaining({ type: "fixed_values" }));
|
||||
});
|
||||
|
||||
it("renders rows", async () => {
|
||||
apiMod.list.mockResolvedValue({
|
||||
items: [{
|
||||
id: "fv_a", type: "fixed_values", name: "lens", category_id: null,
|
||||
tags: [], is_favorite: false, payload: { values: [] }, version: 1,
|
||||
description: "", created_at: "", updated_at: "",
|
||||
}],
|
||||
total: 1,
|
||||
});
|
||||
const wrap = mountView();
|
||||
await flushPromises();
|
||||
expect(wrap.text()).toContain("lens");
|
||||
});
|
||||
|
||||
it("empty state shows CTA", async () => {
|
||||
apiMod.list.mockResolvedValue({ items: [], total: 0 });
|
||||
const wrap = mountView();
|
||||
await flushPromises();
|
||||
expect(wrap.text()).toContain("No fixed values yet");
|
||||
});
|
||||
});
|
||||
@@ -1,9 +1,133 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { useToast } from "primevue/usetoast";
|
||||
import Column from "primevue/column";
|
||||
import Button from "primevue/button";
|
||||
import Badge from "primevue/badge";
|
||||
import Checkbox from "primevue/checkbox";
|
||||
import EntityListView from "../components/EntityListView.vue";
|
||||
import RelativeDate from "../components/RelativeDate.vue";
|
||||
import { useModuleStore } from "../stores/moduleStore";
|
||||
import type { ModuleRow } from "../api/types";
|
||||
|
||||
const router = useRouter();
|
||||
const store = useModuleStore();
|
||||
const toast = useToast();
|
||||
|
||||
onMounted(async () => {
|
||||
store.filter.type = "fixed_values";
|
||||
await fetch();
|
||||
});
|
||||
|
||||
async function fetch() {
|
||||
store.filter.type = "fixed_values";
|
||||
try {
|
||||
await store.fetchAll();
|
||||
} catch (e) {
|
||||
toast.add({ severity: "error", summary: "Load failed", detail: String(e), life: 4000 });
|
||||
}
|
||||
}
|
||||
|
||||
function edit(row: ModuleRow) {
|
||||
router.push({ name: "fixed-values-edit", params: { id: row.id } });
|
||||
}
|
||||
|
||||
async function dup(row: ModuleRow) {
|
||||
try {
|
||||
await store.duplicate(row.id);
|
||||
toast.add({ severity: "success", summary: "Duplicated", detail: row.name, life: 2000 });
|
||||
} catch (e) {
|
||||
toast.add({ severity: "error", summary: "Duplicate failed", detail: String(e), life: 4000 });
|
||||
}
|
||||
}
|
||||
|
||||
async function fav(row: ModuleRow) {
|
||||
try { await store.toggleFavorite(row.id); }
|
||||
catch (e) { toast.add({ severity: "error", summary: "Favorite failed", detail: String(e), life: 4000 }); }
|
||||
}
|
||||
|
||||
async function del(row: ModuleRow) {
|
||||
try {
|
||||
await store.remove(row.id);
|
||||
toast.add({ severity: "success", summary: "Deleted", detail: row.name, life: 2000 });
|
||||
} catch (e) {
|
||||
toast.add({ severity: "error", summary: "Delete failed", detail: String(e), life: 4000 });
|
||||
}
|
||||
}
|
||||
|
||||
async function bulkDel(items: ModuleRow[]) {
|
||||
for (const item of items) await del(item);
|
||||
}
|
||||
|
||||
function valueCount(row: ModuleRow): number {
|
||||
return ((row.payload as { values?: unknown[] }).values ?? []).length;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="p-6 text-wp-text">
|
||||
<h1 class="text-xl font-semibold m-0">Fixed Values</h1>
|
||||
<p class="text-sm text-wp-text2 m-0 mt-2">Stub — wired up in Task 7.</p>
|
||||
</div>
|
||||
<EntityListView
|
||||
title="Fixed Values"
|
||||
subtitle="Fixed-value modules emit one or more named string variables, set explicitly."
|
||||
new-label="New Fixed Values"
|
||||
new-route="/fixed-values/new"
|
||||
:items="store.items"
|
||||
:loading="store.loading"
|
||||
:filter="store.filter"
|
||||
empty-message="No fixed values yet. Use these to lock variables to specific strings."
|
||||
@fetch="fetch"
|
||||
@delete="del"
|
||||
@bulk-delete="bulkDel"
|
||||
>
|
||||
<template #filter-panel="{ filter, emitFetch }">
|
||||
<div class="flex items-center gap-4">
|
||||
<label class="flex items-center gap-2 text-sm cursor-pointer">
|
||||
<Checkbox v-model="filter.favorites" :binary="true" @change="emitFetch" />
|
||||
Favorites only
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #columns>
|
||||
<Column selection-mode="multiple" header-style="width:3rem" />
|
||||
<Column header-style="width:3rem">
|
||||
<template #body="{ data }">
|
||||
<Button
|
||||
:icon="data.is_favorite ? 'pi pi-star-fill' : 'pi pi-star'"
|
||||
text rounded size="small"
|
||||
:severity="data.is_favorite ? 'warning' : 'secondary'"
|
||||
aria-label="Toggle favorite"
|
||||
@click.stop="fav(data)"
|
||||
/>
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="name" header="Name" sortable>
|
||||
<template #body="{ data }">
|
||||
<div class="flex flex-col">
|
||||
<span class="cursor-pointer font-medium" @click="edit(data)">{{ data.name }}</span>
|
||||
<span class="text-xs text-wp-text3 font-mono">{{ data.id }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</Column>
|
||||
<Column header="Values" header-style="width:7rem">
|
||||
<template #body="{ data }">
|
||||
<Badge :value="String(valueCount(data))" severity="secondary" />
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="updated_at" header="Updated" sortable header-style="width:10rem">
|
||||
<template #body="{ data }">
|
||||
<RelativeDate :value="data.updated_at" />
|
||||
</template>
|
||||
</Column>
|
||||
<Column header="Actions" header-style="width:11rem">
|
||||
<template #body="{ data }">
|
||||
<div class="flex gap-1" @click.stop>
|
||||
<Button icon="pi pi-pencil" text rounded size="small" aria-label="Edit" @click="edit(data)" />
|
||||
<Button icon="pi pi-copy" text rounded size="small" aria-label="Duplicate" @click="dup(data)" />
|
||||
<Button icon="pi pi-trash" text rounded size="small" severity="danger" aria-label="Delete" @click="del(data)" />
|
||||
</div>
|
||||
</template>
|
||||
</Column>
|
||||
</template>
|
||||
</EntityListView>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user