Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/grid-column-sizing-survives-pinning.md
Original file line number Diff line number Diff line change
@@ -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.
132 changes: 132 additions & 0 deletions ui/studio/grid/DataGrid.pinning.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<CellProps, "children" | "ref">) => (
<TableHead {...props}>
<button
data-testid="resize-name-to-320"
type="button"
onClick={() => table.setColumnSizing({ name: 320 })}
>
Resize name
</button>
<button
data-testid="pin-id-controlled"
type="button"
onClick={() => header.column.pin("left")}
>
Pin id
</button>
</TableHead>
);
},
cell({ cell }) {
return (props: Omit<CellProps, "children" | "ref">) => (
<Cell {...props}>{String(cell.getValue() ?? "")}</Cell>
);
},
},
{
accessorKey: "name",
id: "name",
header({ header }) {
return (props: Omit<CellProps, "children" | "ref">) => (
<TableHead {...props}>{header.id}</TableHead>
);
},
cell({ cell }) {
return (props: Omit<CellProps, "children" | "ref">) => (
<Cell {...props}>{String(cell.getValue() ?? "")}</Cell>
);
},
},
] satisfies GridColumnDef[];

const container = document.createElement("div");
document.body.appendChild(container);
const root = createRoot(container);

function ControlledPinningHarness() {
const [rowSelectionState, setRowSelectionState] =
useState<RowSelectionState>({});
const [pinnedColumnIds, setPinnedColumnIds] = useState<string[]>([]);

return (
<DataGrid
columnDefs={
columnDefs as AccessorKeyColumnDefBase<Record<string, unknown>>[]
}
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(<ControlledPinningHarness />);
});

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();
});
});
12 changes: 12 additions & 0 deletions ui/studio/grid/DataGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<string | null>(null);

useEffect(() => {
if (lastResetColumnIdentityKeyRef.current === columnDefinitionIdentityKey) {
return;
}

lastResetColumnIdentityKeyRef.current = columnDefinitionIdentityKey;
setColumnOrder(initialColumnOrder);
setColumnPinning(defaultColumnPinning);
setColumnSizing(DEFAULT_COLUMN_SIZING);
Expand Down