diff --git a/frontend/src/widgets/SchemaColumnsDialog.test.tsx b/frontend/src/widgets/SchemaColumnsDialog.test.tsx index e913434e..f58c570c 100644 --- a/frontend/src/widgets/SchemaColumnsDialog.test.tsx +++ b/frontend/src/widgets/SchemaColumnsDialog.test.tsx @@ -46,3 +46,62 @@ test("readOnly renders no checkboxes and no bold", () => { expect(screen.getByText("cod_paz")).toBeInTheDocument(); expect(screen.getByText("cod_paz").className).not.toContain("font-bold"); }); + +const wide: SchemaTable = { + id: "t", + name: "t", + kind: "promote", + columns: [ + { name: "aaa", description: "alfa", suggested: false }, + { name: "zzz_key", description: "chiave", suggested: true }, + { name: "bbb", description: "Codice beta", suggested: false }, + ], +}; + +test("suggested columns are listed first, then catalog order (stable)", () => { + render( + {}} + onClose={() => {}} + /> + ); + const order = screen.getAllByRole("checkbox").map((cb) => cb.getAttribute("aria-label")); + expect(order).toEqual(["zzz_key", "aaa", "bbb"]); +}); + +test("filter narrows visible columns by name", async () => { + render( + {}} + onClose={() => {}} + /> + ); + await userEvent.type(screen.getByRole("textbox", { name: /filter/i }), "zzz"); + const order = screen.getAllByRole("checkbox").map((cb) => cb.getAttribute("aria-label")); + expect(order).toEqual(["zzz_key"]); + expect(screen.queryByText("aaa")).toBeNull(); +}); + +test("filter matches the description too", async () => { + render( + {}} + onClose={() => {}} + /> + ); + await userEvent.type(screen.getByRole("textbox", { name: /filter/i }), "codice"); + const order = screen.getAllByRole("checkbox").map((cb) => cb.getAttribute("aria-label")); + expect(order).toEqual(["bbb"]); +}); diff --git a/frontend/src/widgets/SchemaColumnsDialog.tsx b/frontend/src/widgets/SchemaColumnsDialog.tsx index 997123bd..0084ae00 100644 --- a/frontend/src/widgets/SchemaColumnsDialog.tsx +++ b/frontend/src/widgets/SchemaColumnsDialog.tsx @@ -1,4 +1,4 @@ -import type { ReactElement } from "react"; +import { useMemo, useState, type ReactElement } from "react"; import type { SchemaTable } from "../api/types"; import { Dialog, DialogContent, DialogTitle } from "../components/ui/dialog"; @@ -17,13 +17,47 @@ export function SchemaColumnsDialog({ onToggle: (column: string) => void; onClose: () => void; }): ReactElement { + const [query, setQuery] = useState(""); + + // Suggested-first, then catalog order. Sorting on the descriptor's `suggested` + // flag (not the live selection) keeps the list stable: toggling a checkbox never + // reorders rows. For excluded tables (all suggested=false) this leaves catalog order. + const ordered = useMemo( + () => + table.columns + .map((c, i) => ({ c, i })) + .sort((a, b) => Number(!!b.c.suggested) - Number(!!a.c.suggested) || a.i - b.i) + .map((x) => x.c), + [table.columns], + ); + + const q = query.trim().toLowerCase(); + const displayed = q + ? ordered.filter( + (c) => c.name.toLowerCase().includes(q) || (c.description ?? "").toLowerCase().includes(q), + ) + : ordered; + return ( { if (!o) onClose(); }}> - + {table.name} +
+ setQuery(e.target.value)} + aria-label="Filter columns" + placeholder="Filter columns…" + className="min-w-0 flex-1 rounded-md border border-input bg-card px-2.5 py-1.5 text-sm shadow-xs outline-none transition-[border-color,box-shadow] focus:border-primary/50 focus:ring-2 focus:ring-ring/20" + /> + + {displayed.length}/{table.columns.length} + +
    - {table.columns.map((col) => { + {displayed.map((col) => { const on = selected.has(col.name); const strong = on && !readOnly; return ( @@ -50,6 +84,9 @@ export function SchemaColumnsDialog({ ); })} + {displayed.length === 0 && ( +
  • No columns match the filter.
  • + )}