diff --git a/docs/superpowers/plans/2026-07-14-workflow-ui-regressions.md b/docs/superpowers/plans/2026-07-14-workflow-ui-regressions.md index 5dbc2b23..0ec4b0c2 100644 --- a/docs/superpowers/plans/2026-07-14-workflow-ui-regressions.md +++ b/docs/superpowers/plans/2026-07-14-workflow-ui-regressions.md @@ -169,30 +169,30 @@ Expected: PASS. - `SqlViewer({ blocks })` shows the layout toggle only when `blocks.length > 1`. - CTE plan data shape remains unchanged. -- [ ] **Step 1: Write failing semantic/layout tests** +- [x] **Step 1: Write failing semantic/layout tests** Assert long filter data is rendered in distinct elements labeled Column, Operator, and Value; assert card sections expose stable headings. Assert a one-block SQL viewer has no Horizontal or Vertical controls while a two-block viewer retains both. -- [ ] **Step 2: Run tests and verify RED** +- [x] **Step 2: Run tests and verify RED** Run: `cd frontend && npx vitest run src/viewers/CtePlanViewer.test.tsx src/viewers/SqlViewer.test.tsx src/viewers/CteResultViewer.test.tsx` Expected: structured filter labels are absent and the one-block toggle is present. -- [ ] **Step 3: Implement responsive CTE cards** +- [x] **Step 3: Implement responsive CTE cards** Use a bordered header grid, prose blocks with `leading-relaxed`, metadata rows with fixed labels, `break-words`/`font-mono` for physical identifiers, and a responsive filter grid such as `grid-cols-1 sm:grid-cols-[minmax(0,1fr)_auto_minmax(0,1fr)]`. Keep output columns wrapping. -- [ ] **Step 4: Hide the single-block toggle** +- [x] **Step 4: Hide the single-block toggle** Wrap the layout control in `{blocks.length > 1 && (...)}` without changing multi-block state or rendering. -- [ ] **Step 5: Run targeted tests and typecheck** +- [x] **Step 5: Run targeted tests and typecheck** Run: `cd frontend && npx vitest run src/viewers/CtePlanViewer.test.tsx src/viewers/SqlViewer.test.tsx src/viewers/CteResultViewer.test.tsx && npx tsc -b` diff --git a/frontend/src/viewers/CtePlanViewer.test.tsx b/frontend/src/viewers/CtePlanViewer.test.tsx index 78d38b3f..4d8d66a7 100644 --- a/frontend/src/viewers/CtePlanViewer.test.tsx +++ b/frontend/src/viewers/CtePlanViewer.test.tsx @@ -1,4 +1,4 @@ -import { render, screen } from "@testing-library/react"; +import { render, screen, within } from "@testing-library/react"; import { CtePlanViewer } from "./CtePlanViewer"; import type { CtePlanV2 } from "./artifactV2"; @@ -56,6 +56,18 @@ test("renders one card per CTE with index badge, purpose, and filter value", () expect(screen.getByText(/TRUE/)).toBeInTheDocument(); }); +test("renders each filter as aligned column, operator, and value fields", () => { + render(); + + const filter = screen.getByRole("group", { name: "Filter 1" }); + expect(within(filter).getByText("Column")).toBeInTheDocument(); + expect(within(filter).getByText("Operator")).toBeInTheDocument(); + expect(within(filter).getByText("Value")).toBeInTheDocument(); + expect(within(filter).getByText("pazienti.stato")).toBeInTheDocument(); + expect(within(filter).getByText("IS")).toBeInTheDocument(); + expect(within(filter).getByText("TRUE")).toBeInTheDocument(); +}); + test("shows 'no dependencies' for a CTE with an empty depends_on", () => { render(); expect(screen.getByText(/no dependencies/i)).toBeInTheDocument(); diff --git a/frontend/src/viewers/CtePlanViewer.tsx b/frontend/src/viewers/CtePlanViewer.tsx index 674d1372..9a6731e4 100644 --- a/frontend/src/viewers/CtePlanViewer.tsx +++ b/frontend/src/viewers/CtePlanViewer.tsx @@ -4,19 +4,55 @@ import { Badge } from "../components/ui/badge"; import type { CtePlanCte, CtePlanFilter, CtePlanV2 } from "./artifactV2"; const CHIP = "rounded-md bg-muted px-1.5 py-0.5 font-mono text-xs text-foreground/90"; +const FIELD_LABEL = "text-[0.65rem] font-semibold uppercase tracking-[0.08em] text-muted-foreground"; function Chip({ children }: { children: ReactNode }) { return {children}; } -function FilterChip({ filter }: { filter: CtePlanFilter }) { +function FilterCard({ filter, index }: { filter: CtePlanFilter; index: number }) { return ( -
- - {filter.column} {filter.op} {filter.value} - - {filter.description &&

{filter.description}

} - {filter.rationale &&

{filter.rationale}

} +
+
+
+

Column

+ + {filter.column} + +
+
+

Operator

+ + {filter.op} + +
+
+

Value

+ + {filter.value} + +
+
+ {(filter.description || filter.rationale) && ( +
+ {filter.description && ( + <> + Description +

{filter.description}

+ + )} + {filter.rationale && ( + <> + Rationale +

{filter.rationale}

+ + )} +
+ )}
); } @@ -25,84 +61,97 @@ function CteCard({ cte, total }: { cte: CtePlanCte; total: number }) { const dependsOn = cte.depends_on ?? []; return ( - - -
+ + +
CTE {cte.index}/{total} - {cte.name} + + {cte.name} +
- {cte.purpose &&

{cte.purpose}

} + {cte.purpose && ( +

{cte.purpose}

+ )}
- -
-

Depends on

- {dependsOn.length ? ( -
- {dependsOn.map((d) => ( - {d} - ))} -
- ) : ( -

no dependencies

+ +
+
+

Depends on

+ {dependsOn.length ? ( +
+ {dependsOn.map((dependency) => ( + {dependency} + ))} +
+ ) : ( +

no dependencies

+ )} +
+ + {cte.keys && cte.keys.length > 0 && ( +
+

Keys

+
+ {cte.keys.map((key) => ( + {key} + ))} +
+
)}
{cte.tables && cte.tables.length > 0 && ( -
-

Tables

-
- {cte.tables.map((t) => ( -
- {t.name} - {t.description && ( -

{t.description}

+
+

Tables

+
+ {cte.tables.map((table) => ( +
+ + {table.name} + + {table.description && ( +

+ {table.description} +

)}
))}
-
- )} - - {cte.keys && cte.keys.length > 0 && ( -
-

Keys

-
- {cte.keys.map((k) => ( - {k} - ))} -
-
+ )} {cte.filters && cte.filters.length > 0 && ( -
-

Filters

-
+
+

Filters

+
{cte.filters.map((f, i) => ( - + ))}
-
+ )} {cte.output_columns && cte.output_columns.length > 0 && ( -
-

Output columns

+
+

Output columns

- {cte.output_columns.map((c) => ( - {c} + {cte.output_columns.map((column) => ( + {column} ))}
-
+ )} {cte.rationale && ( -
-

Rationale

-

{cte.rationale}

-
+
+

Rationale

+

{cte.rationale}

+
)} @@ -112,8 +161,8 @@ function CteCard({ cte, total }: { cte: CtePlanCte; total: number }) { export function CtePlanViewer({ plan }: { plan: CtePlanV2 }) { return (
- {plan.question &&

{plan.question}

} - {plan.strategy &&

{plan.strategy}

} + {plan.question &&

{plan.question}

} + {plan.strategy &&

{plan.strategy}

}
{plan.ctes.map((c, i) => ( diff --git a/frontend/src/viewers/CteResultViewer.test.tsx b/frontend/src/viewers/CteResultViewer.test.tsx index 5b8fb6b9..812a0e7f 100644 --- a/frontend/src/viewers/CteResultViewer.test.tsx +++ b/frontend/src/viewers/CteResultViewer.test.tsx @@ -45,6 +45,7 @@ test("passes sql to SqlViewer with mapped test status", async () => { render(); await screen.findByTestId("hl"); expect(screen.getByText(/passed/i)).toBeInTheDocument(); + expect(screen.queryByRole("button", { name: /horizontal|vertical/i })).not.toBeInTheDocument(); }); test("error status maps to failed test status and destructive badge", async () => { diff --git a/frontend/src/viewers/SqlViewer.test.tsx b/frontend/src/viewers/SqlViewer.test.tsx index 8e58b60e..edf27bb4 100644 --- a/frontend/src/viewers/SqlViewer.test.tsx +++ b/frontend/src/viewers/SqlViewer.test.tsx @@ -84,3 +84,11 @@ test("(c) layout toggle flips its label between Orizzontale and Verticale", asyn screen.queryByRole("button", { name: /horizontal/i }) ).not.toBeInTheDocument(); }); + +test("a single SQL block hides the multi-block layout toggle", async () => { + render(); + await waitFor(() => expect(screen.getByTestId("hl")).toBeInTheDocument()); + + expect(screen.queryByRole("button", { name: /horizontal/i })).not.toBeInTheDocument(); + expect(screen.queryByRole("button", { name: /vertical/i })).not.toBeInTheDocument(); +}); diff --git a/frontend/src/viewers/SqlViewer.tsx b/frontend/src/viewers/SqlViewer.tsx index 8778b551..3dca5ad3 100644 --- a/frontend/src/viewers/SqlViewer.tsx +++ b/frontend/src/viewers/SqlViewer.tsx @@ -89,16 +89,18 @@ export function SqlViewer({ blocks }: { blocks: SqlBlock[] }) { return (
-
- -
+ {blocks.length > 1 && ( +
+ +
+ )}