diff --git a/src/components/ResultsGrid.tsx b/src/components/ResultsGrid.tsx index b7c43c8d..14f32571 100644 --- a/src/components/ResultsGrid.tsx +++ b/src/components/ResultsGrid.tsx @@ -14,7 +14,7 @@ import { tableFeatures, useTable, } from "@tanstack/react-table"; -import { useVirtualizer } from "@tanstack/react-virtual"; +import { measureElement, useVirtualizer } from "@tanstack/react-virtual"; import { cn } from "@/lib/utils"; import { ArrowUpDown, ArrowUp, ArrowDown, Eye, Funnel, Lock } from "lucide-react"; import { @@ -138,6 +138,7 @@ export function ResultsGrid({ const [editingCell, setEditingCell] = useState<{ rowIndex: number; columnId: string } | null>(null); const [editValue, setEditValue] = useState(""); const [viewMode, setViewMode] = useState<"card" | "table">("card"); + const [wrapText, setWrapText] = useState(false); const [selectedRow, setSelectedRow] = useState<{ row: Record; index: number } | null>(null); const [columnFilters, setColumnFilters] = useState>(new Map()); const [activeFilterCol, setActiveFilterCol] = useState(null); @@ -476,6 +477,7 @@ export function ResultsGrid({ count: rows.length, getScrollElement: () => tableContainerRef.current, estimateSize: () => 36, + measureElement: wrapText ? measureElement : undefined, overscan: 10, }); @@ -490,6 +492,7 @@ export function ResultsGrid({ count: result.rows.length, getScrollElement: () => mobileTableContainerRef.current, estimateSize: () => 48, + measureElement: wrapText ? measureElement : undefined, overscan: 5, }); @@ -528,6 +531,8 @@ export function ResultsGrid({ onClearFilters={handleClearFilters} viewMode={viewMode} onSetViewMode={setViewMode} + wrapText={wrapText} + onToggleWrapText={() => setWrapText((value) => !value)} hasSensitive={hasSensitive} effectiveMaskingEnabled={effectiveMaskingEnabled} userCanToggle={userCanToggle} @@ -622,6 +627,7 @@ export function ResultsGrid({ height: `${virtualRow.size}px`, transform: `translateY(${virtualRow.start}px)`, }} + ref={wrapText ? mobileTableVirtualizer.measureElement : undefined} className="flex hover:bg-brand-tint/[0.03] transition-colors border-b border-hairline cursor-pointer text-left" onClick={() => setSelectedRow({ row, index: virtualRow.index })} > @@ -638,9 +644,9 @@ export function ResultsGrid({
{displayValue} @@ -687,6 +693,7 @@ export function ResultsGrid({
{flexRender(cell.column.columnDef.cell, cell.getContext())}
diff --git a/src/components/results-grid/StatsBar.tsx b/src/components/results-grid/StatsBar.tsx index 437ef98a..485d1058 100644 --- a/src/components/results-grid/StatsBar.tsx +++ b/src/components/results-grid/StatsBar.tsx @@ -3,7 +3,19 @@ import React from "react"; import { QueryResult } from "@/lib/types"; import { cn } from "@/lib/utils"; -import { ChevronDown, LayoutGrid, Table2, LoaderCircle, EyeOff, Eye, Save, X, Funnel, Lock } from "lucide-react"; +import { + ChevronDown, + LayoutGrid, + Table2, + LoaderCircle, + EyeOff, + Eye, + Save, + X, + Funnel, + Lock, + WrapText, +} from "lucide-react"; import { Button } from "@/components/ui/button"; import type { CellChange } from "@/components/ResultsGrid"; import { describeWarning } from "@/components/results-grid/utils"; @@ -19,6 +31,8 @@ export interface StatsBarProps { onClearFilters: () => void; viewMode: "card" | "table"; onSetViewMode: (mode: "card" | "table") => void; + wrapText: boolean; + onToggleWrapText: () => void; // Masking props hasSensitive: boolean; effectiveMaskingEnabled: boolean; @@ -41,6 +55,8 @@ export function StatsBar({ onClearFilters, viewMode, onSetViewMode, + wrapText, + onToggleWrapText, hasSensitive, effectiveMaskingEnabled, userCanToggle, @@ -106,6 +122,19 @@ export function StatsBar({ {MASKED_LABEL} ) : null)} + {editingEnabled && pendingChanges && pendingChanges.length > 0 && (
@@ -116,6 +145,7 @@ export function StatsBar({ variant="ghost" size="sm" className="h-6 px-1.5 text-xs text-success hover:bg-success-tint/10" + aria-label="Apply changes" onClick={onApplyChanges} > @@ -124,6 +154,7 @@ export function StatsBar({ variant="ghost" size="sm" className="h-6 px-1.5 text-xs text-danger hover:bg-danger-tint/10" + aria-label="Discard changes" onClick={onDiscardChanges} > diff --git a/tests/components/ResultsGrid.test.tsx b/tests/components/ResultsGrid.test.tsx index 5695175e..28b16f10 100644 --- a/tests/components/ResultsGrid.test.tsx +++ b/tests/components/ResultsGrid.test.tsx @@ -85,6 +85,13 @@ mock.module("@/components/results-grid/StatsBar", () => ({ `${(props.pendingChanges as unknown[]).length} changes`, ) : null, + props.onToggleWrapText + ? React.createElement( + "button", + { "data-testid": "wrap-toggle", onClick: props.onToggleWrapText as () => void }, + "WRAP", + ) + : null, (props.activeFilterCount as number) > 0 ? React.createElement( "button", @@ -109,6 +116,7 @@ mock.module("@/components/results-grid/StatsBar", () => ({ // ── Mock @tanstack/react-virtual ──────────────────────────────────────────── mock.module("@tanstack/react-virtual", () => ({ + measureElement: () => 36, useVirtualizer: (opts: { count: number }) => ({ getVirtualItems: () => Array.from({ length: opts.count }, (_, i) => ({ @@ -118,6 +126,7 @@ mock.module("@tanstack/react-virtual", () => ({ key: i, })), getTotalSize: () => opts.count * 36, + measureElement: () => {}, }), })); diff --git a/tests/components/results-grid/StatsBar.test.tsx b/tests/components/results-grid/StatsBar.test.tsx index 7d8f0b5d..7d1adf48 100644 --- a/tests/components/results-grid/StatsBar.test.tsx +++ b/tests/components/results-grid/StatsBar.test.tsx @@ -43,6 +43,8 @@ describe("results-grid/StatsBar", () => { onClearFilters={onClearFilters} viewMode="card" onSetViewMode={mock(() => {})} + wrapText={false} + onToggleWrapText={mock(() => {})} hasSensitive={false} effectiveMaskingEnabled={false} userCanToggle={false} @@ -71,6 +73,8 @@ describe("results-grid/StatsBar", () => { onClearFilters={mock(() => {})} viewMode="table" onSetViewMode={onSetViewMode} + wrapText={false} + onToggleWrapText={mock(() => {})} hasSensitive effectiveMaskingEnabled={false} userCanToggle @@ -88,6 +92,29 @@ describe("results-grid/StatsBar", () => { expect(onSetViewMode).toHaveBeenCalledTimes(2); }); + test("supports text wrapping toggle", () => { + const onToggleWrapText = mock(() => {}); + const { queryByText } = render( + {})} + viewMode="table" + onSetViewMode={mock(() => {})} + wrapText={false} + onToggleWrapText={onToggleWrapText} + hasSensitive={false} + effectiveMaskingEnabled={false} + userCanToggle={false} + />, + ); + + expect(queryByText("WRAP")).not.toBeNull(); + fireEvent.click(queryByText("WRAP")!); + expect(onToggleWrapText).toHaveBeenCalledTimes(1); + }); + test("shows locked masked label when user cannot toggle", () => { const { queryByText } = render( { onClearFilters={mock(() => {})} viewMode="card" onSetViewMode={mock(() => {})} + wrapText={false} + onToggleWrapText={mock(() => {})} hasSensitive effectiveMaskingEnabled userCanToggle={false} @@ -114,6 +143,8 @@ describe("results-grid/StatsBar", () => { onClearFilters={mock(() => {})} viewMode="card" onSetViewMode={mock(() => {})} + wrapText={false} + onToggleWrapText={mock(() => {})} hasSensitive={false} effectiveMaskingEnabled={false} userCanToggle={false} @@ -130,6 +161,8 @@ describe("results-grid/StatsBar", () => { onClearFilters={mock(() => {})} viewMode="card" onSetViewMode={mock(() => {})} + wrapText={false} + onToggleWrapText={mock(() => {})} hasSensitive={false} effectiveMaskingEnabled={false} userCanToggle={false} @@ -150,6 +183,8 @@ describe("results-grid/StatsBar", () => { onClearFilters={mock(() => {})} viewMode="card" onSetViewMode={mock(() => {})} + wrapText={false} + onToggleWrapText={mock(() => {})} hasSensitive={false} effectiveMaskingEnabled={false} userCanToggle={false} @@ -173,6 +208,8 @@ describe("results-grid/StatsBar", () => { onClearFilters={mock(() => {})} viewMode="card" onSetViewMode={mock(() => {})} + wrapText={false} + onToggleWrapText={mock(() => {})} hasSensitive={false} effectiveMaskingEnabled={false} userCanToggle={false} @@ -196,6 +233,8 @@ describe("results-grid/StatsBar", () => { onClearFilters={mock(() => {})} viewMode="card" onSetViewMode={mock(() => {})} + wrapText={false} + onToggleWrapText={mock(() => {})} hasSensitive={false} effectiveMaskingEnabled={false} userCanToggle={false} @@ -213,7 +252,7 @@ describe("results-grid/StatsBar", () => { const pendingChanges: CellChange[] = [ { rowIndex: 0, columnId: "name", originalValue: "Alice", newValue: "Alicia" }, ]; - const { container, queryByText } = render( + const { queryByText, getByLabelText } = render( { onClearFilters={mock(() => {})} viewMode="card" onSetViewMode={mock(() => {})} + wrapText={false} + onToggleWrapText={mock(() => {})} hasSensitive={false} effectiveMaskingEnabled={false} userCanToggle={false} @@ -232,9 +273,8 @@ describe("results-grid/StatsBar", () => { ); expect(queryByText("1 change")).not.toBeNull(); - const buttons = container.querySelectorAll("button"); - fireEvent.click(buttons[0]!); - fireEvent.click(buttons[1]!); + fireEvent.click(getByLabelText("Apply changes")); + fireEvent.click(getByLabelText("Discard changes")); expect(onApplyChanges).toHaveBeenCalledTimes(1); expect(onDiscardChanges).toHaveBeenCalledTimes(1); });