feat(frontend): Export status-filter menu on the Products page (SD-0002 §5.2, PUC-9)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-06-11 22:08:34 -07:00
parent 6f213e1f02
commit 0a85c4fef8
4 changed files with 101 additions and 7 deletions
+27 -6
View File
@@ -3,6 +3,8 @@
import { useEffect, useState } from "react";
import {
dialectLabel,
EXPORT_STATUSES,
exportUrl,
getProductsSummary,
listRuns,
type ProductsSummary,
@@ -63,12 +65,31 @@ export default function ProductsPage() {
{!empty && <span className="products__count"> · {summary.product_count.toLocaleString()}</span>}
</h1>
<div className="products__actions">
<div className="products__export">
<button type="button" className="btn-secondary" disabled title="Export arrives in a coming release">
Export
</button>
<span className="note">Export arrives in a coming release</span>
</div>
{empty ? (
<div className="products__export">
<button type="button" className="btn-secondary" disabled>
Export
</button>
<span className="note">Export arrives when you have products</span>
</div>
) : (
<details className="products__export menu">
<summary className="btn-secondary" role="button">
Export
</summary>
<ul className="menu__list">
{EXPORT_STATUSES.map((s) => (
<li key={s.value}>
{/* A real download: the browser navigates to the streamed
endpoint and saves the attachment (PUC-9). */}
<a className="menu__item" href={exportUrl(s.value)} download>
{s.label}
</a>
</li>
))}
</ul>
</details>
)}
<a className="btn-primary" href="#/products/import">
Import products
</a>
@@ -0,0 +1,19 @@
import { describe, expect, it } from "vitest";
import { EXPORT_STATUSES, exportUrl } from "../../productsApi";
import { isExportEnabled } from "./exportMenu";
describe("export menu", () => {
it("disables export for an empty catalog and enables it once there are products", () => {
expect(isExportEnabled(0)).toBe(false);
expect(isExportEnabled(3)).toBe(true);
});
it("builds the four status download URLs, 'all' first", () => {
expect(EXPORT_STATUSES.map((s) => exportUrl(s.value))).toEqual([
"/api/products/export?status=all",
"/api/products/export?status=active",
"/api/products/export?status=draft",
"/api/products/export?status=archived",
]);
});
});
@@ -0,0 +1,11 @@
// Pure logic behind the Products page Export menu (SD-0002 §5.2, PUC-9). The
// status list + URL builder live in productsApi.ts (re-used by the component);
// this is the one decision the menu turns on — whether export is offered at all.
// The disclosure JSX is verified by the E2E suite, matching SLICE-2/3's pattern
// of pure-logic unit tests + E2E for screens.
// Export is offered only when the catalog has products (PUC-9: an empty catalog
// shows a disabled button + note instead).
export function isExportEnabled(count: number): boolean {
return count > 0;
}