feat(frontend): suggested-first ordering + filter box in the schema-linking columns modal
Wide tables (e.g. 116 columns, 3 suggested) made curation a scroll hunt. Now the modal lists suggested columns first (stable sort on the descriptor flag, so toggling never reorders rows) and adds a filter box matching name + description, with a "shown/total" count. Selection stays keyed by column name, so the response column order is unchanged (catalog order). Applies to read-only (excluded) tables too. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
committed by
Marco Pancotti
co-authored by
Claude Opus 4.8
parent
d942635085
commit
9fe1c9321b
@@ -46,3 +46,62 @@ test("readOnly renders no checkboxes and no bold", () => {
|
|||||||
expect(screen.getByText("cod_paz")).toBeInTheDocument();
|
expect(screen.getByText("cod_paz")).toBeInTheDocument();
|
||||||
expect(screen.getByText("cod_paz").className).not.toContain("font-bold");
|
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(
|
||||||
|
<SchemaColumnsDialog
|
||||||
|
table={wide}
|
||||||
|
open
|
||||||
|
readOnly={false}
|
||||||
|
selected={new Set()}
|
||||||
|
onToggle={() => {}}
|
||||||
|
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(
|
||||||
|
<SchemaColumnsDialog
|
||||||
|
table={wide}
|
||||||
|
open
|
||||||
|
readOnly={false}
|
||||||
|
selected={new Set()}
|
||||||
|
onToggle={() => {}}
|
||||||
|
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(
|
||||||
|
<SchemaColumnsDialog
|
||||||
|
table={wide}
|
||||||
|
open
|
||||||
|
readOnly={false}
|
||||||
|
selected={new Set()}
|
||||||
|
onToggle={() => {}}
|
||||||
|
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"]);
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { ReactElement } from "react";
|
import { useMemo, useState, type ReactElement } from "react";
|
||||||
import type { SchemaTable } from "../api/types";
|
import type { SchemaTable } from "../api/types";
|
||||||
import { Dialog, DialogContent, DialogTitle } from "../components/ui/dialog";
|
import { Dialog, DialogContent, DialogTitle } from "../components/ui/dialog";
|
||||||
|
|
||||||
@@ -17,13 +17,47 @@ export function SchemaColumnsDialog({
|
|||||||
onToggle: (column: string) => void;
|
onToggle: (column: string) => void;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}): ReactElement {
|
}): 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 (
|
return (
|
||||||
<Dialog open={open} onOpenChange={(o) => { if (!o) onClose(); }}>
|
<Dialog open={open} onOpenChange={(o) => { if (!o) onClose(); }}>
|
||||||
<DialogContent className="grid max-h-[70vh] w-[70vw] max-w-[46rem] grid-rows-[auto_1fr] gap-3 sm:max-w-[46rem]">
|
<DialogContent className="grid max-h-[70vh] w-[70vw] max-w-[46rem] grid-rows-[auto_auto_1fr] gap-3 sm:max-w-[46rem]">
|
||||||
<DialogTitle className="font-mono text-sm">{table.name}</DialogTitle>
|
<DialogTitle className="font-mono text-sm">{table.name}</DialogTitle>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={query}
|
||||||
|
onChange={(e) => 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"
|
||||||
|
/>
|
||||||
|
<span className="shrink-0 text-xs tabular-nums text-muted-foreground">
|
||||||
|
{displayed.length}/{table.columns.length}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
<div className="min-h-0 overflow-auto">
|
<div className="min-h-0 overflow-auto">
|
||||||
<ul className="flex flex-col divide-y divide-border/50">
|
<ul className="flex flex-col divide-y divide-border/50">
|
||||||
{table.columns.map((col) => {
|
{displayed.map((col) => {
|
||||||
const on = selected.has(col.name);
|
const on = selected.has(col.name);
|
||||||
const strong = on && !readOnly;
|
const strong = on && !readOnly;
|
||||||
return (
|
return (
|
||||||
@@ -50,6 +84,9 @@ export function SchemaColumnsDialog({
|
|||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
{displayed.length === 0 && (
|
||||||
|
<li className="py-3 text-sm text-muted-foreground">No columns match the filter.</li>
|
||||||
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|||||||
Reference in New Issue
Block a user