From 0a85c4fef8fb6d98a8aa22dcfb3177d67809672d Mon Sep 17 00:00:00 2001 From: Ben Stull Date: Thu, 11 Jun 2026 22:08:34 -0700 Subject: [PATCH] =?UTF-8?q?feat(frontend):=20Export=20status-filter=20menu?= =?UTF-8?q?=20on=20the=20Products=20page=20(SD-0002=20=C2=A75.2,=20PUC-9)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 --- .../src/screens/products/ProductsPage.tsx | 33 +++++++++++--- .../src/screens/products/exportMenu.test.ts | 19 ++++++++ frontend/src/screens/products/exportMenu.ts | 11 +++++ frontend/src/styles/products.css | 45 ++++++++++++++++++- 4 files changed, 101 insertions(+), 7 deletions(-) create mode 100644 frontend/src/screens/products/exportMenu.test.ts create mode 100644 frontend/src/screens/products/exportMenu.ts diff --git a/frontend/src/screens/products/ProductsPage.tsx b/frontend/src/screens/products/ProductsPage.tsx index b8de3f6..ace39e4 100644 --- a/frontend/src/screens/products/ProductsPage.tsx +++ b/frontend/src/screens/products/ProductsPage.tsx @@ -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 && · {summary.product_count.toLocaleString()}}
-
- - Export arrives in a coming release -
+ {empty ? ( +
+ + Export arrives when you have products +
+ ) : ( +
+ + Export + +
    + {EXPORT_STATUSES.map((s) => ( +
  • + {/* A real download: the browser navigates to the streamed + endpoint and saves the attachment (PUC-9). */} + + {s.label} + +
  • + ))} +
+
+ )} Import products diff --git a/frontend/src/screens/products/exportMenu.test.ts b/frontend/src/screens/products/exportMenu.test.ts new file mode 100644 index 0000000..63e4e79 --- /dev/null +++ b/frontend/src/screens/products/exportMenu.test.ts @@ -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", + ]); + }); +}); diff --git a/frontend/src/screens/products/exportMenu.ts b/frontend/src/screens/products/exportMenu.ts new file mode 100644 index 0000000..6640c8a --- /dev/null +++ b/frontend/src/screens/products/exportMenu.ts @@ -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; +} diff --git a/frontend/src/styles/products.css b/frontend/src/styles/products.css index d59d556..57f976f 100644 --- a/frontend/src/styles/products.css +++ b/frontend/src/styles/products.css @@ -53,9 +53,52 @@ } .products__count { color: var(--text-on-dark-mute); font-weight: var(--weight-medium); } .products__actions { display: flex; gap: 12px; align-items: center; } -/* Disabled Export + its visible "coming release" caption, stacked. */ +/* Disabled Export + its visible "no products yet" caption, stacked (empty catalog). */ .products__export { display: flex; flex-direction: column; gap: 4px; align-items: center; } .products__export .note { font-size: 11.5px; } + +/* Export status menu (SD-0002 §5.2 — PUC-9). A native
disclosure so + it's keyboard-accessible with no extra JS (§6.6). Tokens align with the design + bundle (--surface-raised / --border-card / --radius-panel); the fallbacks keep + it working regardless. */ +.products__export.menu { + position: relative; + display: inline-block; +} +.products__export.menu > summary { + list-style: none; + cursor: pointer; +} +.products__export.menu > summary::-webkit-details-marker { + display: none; +} +.menu__list { + position: absolute; + right: 0; + z-index: 10; + margin: 0.25rem 0 0; + padding: 0.25rem; + list-style: none; + background: var(--surface-raised, #fff); + border: 1px solid var(--border-card, #d8d3c8); + border-radius: var(--radius-panel, 8px); + box-shadow: var(--shadow-soft, 0 6px 20px rgba(0, 0, 0, 0.12)); + min-width: 10rem; +} +.menu__item { + display: block; + padding: 0.5rem 0.75rem; + border-radius: var(--radius-sm, 6px); + text-decoration: none; + color: var(--text-on-dark-soft, inherit); + font-family: var(--wv-font-display); + font-size: 14px; +} +.menu__item:hover, +.menu__item:focus { + background: var(--surface-raised-hi, #f3efe7); + color: var(--wv-starlight); +} .products .btn-primary { width: auto; text-decoration: none; } .products .empty { margin: 24px auto 0; }