From 5ad2c69e1e45902db10b2089637e64329df0e1fd Mon Sep 17 00:00:00 2001 From: dharsh03rs-cpu Date: Sat, 12 Sep 2026 09:00:01 +0530 Subject: [PATCH 1/2] add text wrapping toggle --- src/components/ResultsGrid.tsx | 18 +++++-- src/components/results-grid/StatsBar.tsx | 33 ++++++++++++- tests/components/ResultsGrid.test.tsx | 9 ++++ .../components/results-grid/StatsBar.test.tsx | 48 +++++++++++++++++-- 4 files changed, 99 insertions(+), 9 deletions(-) diff --git a/src/components/ResultsGrid.tsx b/src/components/ResultsGrid.tsx index b7c43c8d9..14f325712 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 437ef98a3..485d10580 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 5695175ea..28b16f106 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 7d8f0b5d5..7d1adf485 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); }); From 21e21bc41780a37909d72081234b1958560f3236 Mon Sep 17 00:00:00 2001 From: cevheri Date: Mon, 14 Sep 2026 17:06:49 +0300 Subject: [PATCH 2/2] fix(results-grid): let the wrap toggle wrap the value and grow the row With WRAP ON a long value was still cut to one line and every row stayed at its estimated height. Three things held it there: - each cell renderer puts the value in an inner div with `truncate`, which sets its own nowrap, so only the outer cell ever changed; - rows kept a fixed inline height and cells kept h-full, so measureElement could only read back the estimate; - the mobile table rows had no data-index, so a measured row grew on screen while the rows below it stayed put. Turning wrapping off again also left the grown rows tall, because both virtualizers cache what they measured; the toggle now drops that cache. The virtualizer's own default measureElement is used instead of passing it explicitly. --- src/components/ResultsGrid.tsx | 43 ++++++--- tests/components/ResultsGrid.test.tsx | 120 ++++++++++++++++++++++++-- 2 files changed, 144 insertions(+), 19 deletions(-) diff --git a/src/components/ResultsGrid.tsx b/src/components/ResultsGrid.tsx index 14f325712..e67e08e84 100644 --- a/src/components/ResultsGrid.tsx +++ b/src/components/ResultsGrid.tsx @@ -14,7 +14,7 @@ import { tableFeatures, useTable, } from "@tanstack/react-table"; -import { measureElement, useVirtualizer } from "@tanstack/react-virtual"; +import { useVirtualizer } from "@tanstack/react-virtual"; import { cn } from "@/lib/utils"; import { ArrowUpDown, ArrowUp, ArrowDown, Eye, Funnel, Lock } from "lucide-react"; import { @@ -219,6 +219,9 @@ export function ResultsGrid({ }, []); const columns = useMemo>[]>(() => { + // `truncate` carries its own `white-space: nowrap`, so wrapping has to replace it here, + // on the element holding the value, not only on the cell around it. + const valueFlow = wrapText ? "whitespace-pre-wrap break-words" : "truncate h-full"; return result.fields.map((field) => ({ // `id` + `accessorFn`, never `accessorKey`: TanStack reads a DOT in an // accessorKey as a path into the row, so `shipping.city` was fetched as @@ -372,7 +375,7 @@ export function ResultsGrid({ if (effectiveMaskingEnabled && sensitivePattern && val !== null && val !== undefined && !isRevealed) { const masked = maskValueByPattern(val, sensitivePattern); return ( -
+
{masked} {userCanReveal && (