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);