Files
ThothII/frontend/src/widgets/SchemaLinkingGateWidget.tsx
T

128 lines
4.7 KiB
TypeScript

import { useState } from "react";
import type { WidgetProps } from "./types";
import type { SchemaTable } from "../api/types";
import { SchemaColumnsDialog } from "./SchemaColumnsDialog";
import { ReservedControls } from "./ReservedControls";
export function SchemaLinkingGateWidget({ descriptor, onRespond }: WidgetProps) {
const tables: SchemaTable[] = descriptor.tables ?? [];
const [enacted, setEnacted] = useState<Set<string>>(
() => new Set(tables.filter((t) => t.recommended).map((t) => t.id))
);
const [selected, setSelected] = useState<Map<string, Set<string>>>(() => {
const m = new Map<string, Set<string>>();
for (const t of tables) {
m.set(
t.id,
new Set(t.kind === "promote" ? t.columns.filter((c) => c.suggested).map((c) => c.name) : [])
);
}
return m;
});
const [openId, setOpenId] = useState<string | null>(null);
function toggleEnact(id: string) {
setEnacted((prev) => {
const next = new Set(prev);
if (next.has(id)) next.delete(id);
else next.add(id);
return next;
});
}
function toggleColumn(tableId: string, column: string) {
setSelected((prev) => {
const next = new Map(prev);
const set = new Set(next.get(tableId) ?? []);
if (set.has(column)) set.delete(column);
else set.add(column);
next.set(tableId, set);
return next;
});
}
function confirm() {
const payload = tables.map((t) => {
const on = enacted.has(t.id);
if (on && t.kind === "promote") {
const sel = selected.get(t.id) ?? new Set<string>();
return { id: t.id, enacted: true, columns: t.columns.filter((c) => sel.has(c.name)).map((c) => c.name) };
}
return { id: t.id, enacted: on };
});
onRespond({ id: descriptor.id, kind: "schema-linking", tables: payload });
}
const openTable = tables.find((t) => t.id === openId) ?? null;
return (
<div className="space-y-4 rounded-xl border border-border/70 bg-card p-4 shadow-sm">
{descriptor.title && (
<p className="text-[0.95rem] font-semibold leading-snug text-foreground">{descriptor.title}</p>
)}
<div className="flex flex-col gap-2">
{tables.map((t) => {
const on = enacted.has(t.id);
const count = selected.get(t.id)?.size ?? 0;
return (
<div
key={t.id}
className={`rounded-lg border px-3 py-2.5 transition-colors ${on ? "border-border/70 bg-accent/40" : "border-transparent bg-muted/30"}`}
>
<div className="flex items-start gap-3">
<input
type="checkbox"
className="mt-1 size-4 shrink-0 accent-[oklch(var(--primary))]"
checked={on}
onChange={() => toggleEnact(t.id)}
aria-label={t.name}
/>
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
<span className="font-mono text-sm font-semibold text-foreground">{t.name}</span>
<span className="thot-label">{t.kind === "promote" ? "Promuovi" : "Escludi"}</span>
</div>
{t.description && <p className="mt-0.5 text-sm text-muted-foreground">{t.description}</p>}
{t.rationale && <p className="mt-0.5 text-sm text-foreground/80">{t.rationale}</p>}
</div>
<button
className="shrink-0 self-center rounded-md border border-border/60 px-2.5 py-1 text-xs text-muted-foreground shadow-xs transition-colors hover:bg-muted hover:text-foreground"
onClick={() => setOpenId(t.id)}
>
{t.kind === "promote" ? `Colonne ${count}/${t.columns.length}` : `Colonne ${t.columns.length}`}
</button>
</div>
</div>
);
})}
</div>
{openTable && (
<SchemaColumnsDialog
table={openTable}
open
readOnly={openTable.kind === "exclude"}
selected={selected.get(openTable.id) ?? new Set()}
onToggle={(c) => toggleColumn(openTable.id, c)}
onClose={() => setOpenId(null)}
/>
)}
<div>
<button
className="rounded-md bg-primary px-4 py-2 text-sm font-semibold text-primary-foreground shadow-xs transition-colors hover:bg-[oklch(var(--primary-hover))]"
onClick={confirm}
>
Confirm
</button>
</div>
<ReservedControls
reserved={descriptor.reserved}
onControl={(c, t) => onRespond({ id: descriptor.id, control: c, ...(t !== undefined ? { text: t } : {}) })}
/>
</div>
);
}