From 64f8e20816d2a201aa3bf82743e449321099ad52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Bramer=20Schmidt?= Date: Fri, 17 Jul 2026 21:25:21 +0700 Subject: [PATCH] Fix grid column widths resetting when pin state changes Resizing a column and then pinning or unpinning any column wiped all user column widths (and custom column order) back to defaults. The "reset on column identity change" effect in DataGrid depends on defaultColumnPinning, whose identity also changes whenever the URL-backed pinnedColumnIds prop round-trips after a pin update, so the effect re-ran and cleared columnSizing/columnOrder state. Guard the reset with the column definition identity key so it only fires when the set of columns actually changes (issue #1371). Co-Authored-By: Claude Fable 5 --- .../grid-column-sizing-survives-pinning.md | 5 + ui/studio/grid/DataGrid.pinning.test.tsx | 132 ++++++++++++++++++ ui/studio/grid/DataGrid.tsx | 12 ++ 3 files changed, 149 insertions(+) create mode 100644 .changeset/grid-column-sizing-survives-pinning.md diff --git a/.changeset/grid-column-sizing-survives-pinning.md b/.changeset/grid-column-sizing-survives-pinning.md new file mode 100644 index 00000000..39a02f48 --- /dev/null +++ b/.changeset/grid-column-sizing-survives-pinning.md @@ -0,0 +1,5 @@ +--- +"@prisma/studio-core": patch +--- + +Fix grid column widths being reset when columns are pinned or unpinned. Resizing a column and then changing any column's pin state (which round-trips through the URL-backed `pinnedColumnIds` prop) wiped all user column widths and custom column ordering back to defaults. The reset now only happens when the set of columns itself changes. diff --git a/ui/studio/grid/DataGrid.pinning.test.tsx b/ui/studio/grid/DataGrid.pinning.test.tsx index fcbd7878..51693b7c 100644 --- a/ui/studio/grid/DataGrid.pinning.test.tsx +++ b/ui/studio/grid/DataGrid.pinning.test.tsx @@ -602,4 +602,136 @@ describe("DataGrid pinning", () => { } } }); + + // Regression test for https://github.com/prisma/studio/issues/1371: + // pinning or unpinning a column round-trips through the pinnedColumnIds + // prop (URL state), which used to retrigger the "reset on column identity + // change" effect and wipe user column widths. + it("keeps user column sizing when pinned columns change", () => { + createSelection({ isCollapsed: true }); + + const columnDefs = [ + { + accessorKey: "id", + id: "id", + header({ header, table }) { + return (props: Omit) => ( + + + + + ); + }, + cell({ cell }) { + return (props: Omit) => ( + {String(cell.getValue() ?? "")} + ); + }, + }, + { + accessorKey: "name", + id: "name", + header({ header }) { + return (props: Omit) => ( + {header.id} + ); + }, + cell({ cell }) { + return (props: Omit) => ( + {String(cell.getValue() ?? "")} + ); + }, + }, + ] satisfies GridColumnDef[]; + + const container = document.createElement("div"); + document.body.appendChild(container); + const root = createRoot(container); + + function ControlledPinningHarness() { + const [rowSelectionState, setRowSelectionState] = + useState({}); + const [pinnedColumnIds, setPinnedColumnIds] = useState([]); + + return ( + >[] + } + isFetching={false} + isProcessing={false} + onPaginationChange={vi.fn()} + onPinnedColumnIdsChange={(columnIds) => { + // Mimic URL-backed pinning state: every update produces a new + // array identity, like `useColumnPinning` does. + setPinnedColumnIds([...columnIds]); + }} + onRowSelectionChange={(updater) => { + setRowSelectionState((previous) => + typeof updater === "function" ? updater(previous) : updater, + ); + }} + pageCount={1} + paginationState={{ pageIndex: 0, pageSize: 20 }} + pinnedColumnIds={pinnedColumnIds} + rows={[{ __ps_rowid: "row_1", id: "org_acme", name: "Acme Labs" }]} + rowSelectionState={rowSelectionState} + /> + ); + } + + act(() => { + root.render(); + }); + + function getHeader(columnId: string): HTMLTableCellElement { + const header = container.querySelector( + `th[data-grid-header-column-id="${columnId}"]`, + ); + + if (!(header instanceof HTMLTableCellElement)) { + throw new Error(`Could not find header: ${columnId}`); + } + + return header; + } + + function clickTestButton(testId: string) { + const button = container.querySelector(`[data-testid="${testId}"]`); + + if (!(button instanceof HTMLButtonElement)) { + throw new Error(`Could not find button: ${testId}`); + } + + act(() => { + button.click(); + }); + } + + expect(getHeader("name").style.width).toBe("200px"); + + clickTestButton("resize-name-to-320"); + expect(getHeader("name").style.width).toBe("320px"); + + clickTestButton("pin-id-controlled"); + expect(getHeader("id").className).toContain("sticky"); + expect(getHeader("name").style.width).toBe("320px"); + + act(() => { + root.unmount(); + }); + container.remove(); + }); }); diff --git a/ui/studio/grid/DataGrid.tsx b/ui/studio/grid/DataGrid.tsx index 6a611298..8ecf1be5 100644 --- a/ui/studio/grid/DataGrid.tsx +++ b/ui/studio/grid/DataGrid.tsx @@ -725,7 +725,19 @@ export function DataGrid(props: DataGridProps) { // Reset column order/pinning/sizing only when column identities change. // This avoids expensive state churn when parent components re-render with // new columnDef object references but equivalent column ids. + // + // The dependency list includes `defaultColumnPinning`, whose identity also + // changes when the pinned-column props change (e.g. after pinning or + // unpinning a column). Without the identity-key guard below, any pinning + // update would wipe user column widths and ordering (issue #1371). + const lastResetColumnIdentityKeyRef = useRef(null); + useEffect(() => { + if (lastResetColumnIdentityKeyRef.current === columnDefinitionIdentityKey) { + return; + } + + lastResetColumnIdentityKeyRef.current = columnDefinitionIdentityKey; setColumnOrder(initialColumnOrder); setColumnPinning(defaultColumnPinning); setColumnSizing(DEFAULT_COLUMN_SIZING);