feat(frontend): schema-linking types + columns modal
This commit is contained in:
@@ -7,6 +7,24 @@ export interface WidgetOption {
|
|||||||
opens?: WidgetDescriptor;
|
opens?: WidgetDescriptor;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SchemaColumn {
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
type?: string;
|
||||||
|
pk?: boolean;
|
||||||
|
suggested?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SchemaTable {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
kind: "promote" | "exclude";
|
||||||
|
recommended?: boolean;
|
||||||
|
description?: string;
|
||||||
|
rationale?: string;
|
||||||
|
columns: SchemaColumn[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface WidgetDescriptor {
|
export interface WidgetDescriptor {
|
||||||
id: string;
|
id: string;
|
||||||
schema_version?: number;
|
schema_version?: number;
|
||||||
@@ -21,6 +39,7 @@ export interface WidgetDescriptor {
|
|||||||
artifact?: { kind: string; content?: string; [k: string]: unknown };
|
artifact?: { kind: string; content?: string; [k: string]: unknown };
|
||||||
level?: "info" | "warning" | "error";
|
level?: "info" | "warning" | "error";
|
||||||
text?: string;
|
text?: string;
|
||||||
|
tables?: SchemaTable[];
|
||||||
[k: string]: unknown;
|
[k: string]: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,6 +50,7 @@ export interface UiResponse {
|
|||||||
text?: string;
|
text?: string;
|
||||||
decision?: { type: string };
|
decision?: { type: string };
|
||||||
control?: string;
|
control?: string;
|
||||||
|
tables?: { id: string; enacted: boolean; columns?: string[] }[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export type StreamEvent =
|
export type StreamEvent =
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import { render, screen } from "@testing-library/react";
|
||||||
|
import userEvent from "@testing-library/user-event";
|
||||||
|
import { SchemaColumnsDialog } from "./SchemaColumnsDialog";
|
||||||
|
import type { SchemaTable } from "../api/types";
|
||||||
|
|
||||||
|
const table: SchemaTable = {
|
||||||
|
id: "t-pat",
|
||||||
|
name: "dim_patient",
|
||||||
|
kind: "promote",
|
||||||
|
columns: [
|
||||||
|
{ name: "cod_paz", description: "Codice paziente", suggested: true },
|
||||||
|
{ name: "nome", description: "Nome", suggested: false },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
test("selected column renders bold; unselected is normal; toggle fires", async () => {
|
||||||
|
const onToggle = vi.fn();
|
||||||
|
render(
|
||||||
|
<SchemaColumnsDialog
|
||||||
|
table={table}
|
||||||
|
open
|
||||||
|
readOnly={false}
|
||||||
|
selected={new Set(["cod_paz"])}
|
||||||
|
onToggle={onToggle}
|
||||||
|
onClose={() => {}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
expect(screen.getByText("cod_paz").className).toContain("font-bold");
|
||||||
|
expect(screen.getByText("nome").className).not.toContain("font-bold");
|
||||||
|
await userEvent.click(screen.getByRole("checkbox", { name: "nome" }));
|
||||||
|
expect(onToggle).toHaveBeenCalledWith("nome");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("readOnly renders no checkboxes and no bold", () => {
|
||||||
|
render(
|
||||||
|
<SchemaColumnsDialog
|
||||||
|
table={table}
|
||||||
|
open
|
||||||
|
readOnly
|
||||||
|
selected={new Set()}
|
||||||
|
onToggle={() => {}}
|
||||||
|
onClose={() => {}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
expect(screen.queryByRole("checkbox")).toBeNull();
|
||||||
|
expect(screen.getByText("cod_paz")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("cod_paz").className).not.toContain("font-bold");
|
||||||
|
});
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import type { ReactElement } from "react";
|
||||||
|
import type { SchemaTable } from "../api/types";
|
||||||
|
import { Dialog, DialogContent, DialogTitle } from "../components/ui/dialog";
|
||||||
|
|
||||||
|
export function SchemaColumnsDialog({
|
||||||
|
table,
|
||||||
|
open,
|
||||||
|
readOnly,
|
||||||
|
selected,
|
||||||
|
onToggle,
|
||||||
|
onClose,
|
||||||
|
}: {
|
||||||
|
table: SchemaTable;
|
||||||
|
open: boolean;
|
||||||
|
readOnly: boolean;
|
||||||
|
selected: Set<string>;
|
||||||
|
onToggle: (column: string) => void;
|
||||||
|
onClose: () => void;
|
||||||
|
}): ReactElement {
|
||||||
|
return (
|
||||||
|
<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]">
|
||||||
|
<DialogTitle className="font-mono text-sm">{table.name}</DialogTitle>
|
||||||
|
<div className="min-h-0 overflow-auto">
|
||||||
|
<ul className="flex flex-col divide-y divide-border/50">
|
||||||
|
{table.columns.map((col) => {
|
||||||
|
const on = selected.has(col.name);
|
||||||
|
const strong = on && !readOnly;
|
||||||
|
return (
|
||||||
|
<li key={col.name} className="flex items-start gap-3 py-2">
|
||||||
|
{!readOnly && (
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
className="mt-0.5 size-4 shrink-0 accent-[oklch(var(--primary))]"
|
||||||
|
checked={on}
|
||||||
|
onChange={() => onToggle(col.name)}
|
||||||
|
aria-label={col.name}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div className="min-w-0">
|
||||||
|
<div className={`font-mono text-sm ${strong ? "font-bold text-foreground" : "text-foreground/90"}`}>
|
||||||
|
{col.name}
|
||||||
|
</div>
|
||||||
|
{col.description ? (
|
||||||
|
<p className={`text-sm ${strong ? "font-semibold text-foreground" : "text-muted-foreground"}`}>
|
||||||
|
{col.description}
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user