Skip to content

Commit 3f4d84c

Browse files
sorenbsclaude
andauthored
Fix grid column widths resetting when pin state changes (#1552)
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 <noreply@anthropic.com>
1 parent 6c9e625 commit 3f4d84c

3 files changed

Lines changed: 149 additions & 0 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@prisma/studio-core": patch
3+
---
4+
5+
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.

ui/studio/grid/DataGrid.pinning.test.tsx

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -602,4 +602,136 @@ describe("DataGrid pinning", () => {
602602
}
603603
}
604604
});
605+
606+
// Regression test for https://github.com/prisma/studio/issues/1371:
607+
// pinning or unpinning a column round-trips through the pinnedColumnIds
608+
// prop (URL state), which used to retrigger the "reset on column identity
609+
// change" effect and wipe user column widths.
610+
it("keeps user column sizing when pinned columns change", () => {
611+
createSelection({ isCollapsed: true });
612+
613+
const columnDefs = [
614+
{
615+
accessorKey: "id",
616+
id: "id",
617+
header({ header, table }) {
618+
return (props: Omit<CellProps, "children" | "ref">) => (
619+
<TableHead {...props}>
620+
<button
621+
data-testid="resize-name-to-320"
622+
type="button"
623+
onClick={() => table.setColumnSizing({ name: 320 })}
624+
>
625+
Resize name
626+
</button>
627+
<button
628+
data-testid="pin-id-controlled"
629+
type="button"
630+
onClick={() => header.column.pin("left")}
631+
>
632+
Pin id
633+
</button>
634+
</TableHead>
635+
);
636+
},
637+
cell({ cell }) {
638+
return (props: Omit<CellProps, "children" | "ref">) => (
639+
<Cell {...props}>{String(cell.getValue() ?? "")}</Cell>
640+
);
641+
},
642+
},
643+
{
644+
accessorKey: "name",
645+
id: "name",
646+
header({ header }) {
647+
return (props: Omit<CellProps, "children" | "ref">) => (
648+
<TableHead {...props}>{header.id}</TableHead>
649+
);
650+
},
651+
cell({ cell }) {
652+
return (props: Omit<CellProps, "children" | "ref">) => (
653+
<Cell {...props}>{String(cell.getValue() ?? "")}</Cell>
654+
);
655+
},
656+
},
657+
] satisfies GridColumnDef[];
658+
659+
const container = document.createElement("div");
660+
document.body.appendChild(container);
661+
const root = createRoot(container);
662+
663+
function ControlledPinningHarness() {
664+
const [rowSelectionState, setRowSelectionState] =
665+
useState<RowSelectionState>({});
666+
const [pinnedColumnIds, setPinnedColumnIds] = useState<string[]>([]);
667+
668+
return (
669+
<DataGrid
670+
columnDefs={
671+
columnDefs as AccessorKeyColumnDefBase<Record<string, unknown>>[]
672+
}
673+
isFetching={false}
674+
isProcessing={false}
675+
onPaginationChange={vi.fn()}
676+
onPinnedColumnIdsChange={(columnIds) => {
677+
// Mimic URL-backed pinning state: every update produces a new
678+
// array identity, like `useColumnPinning` does.
679+
setPinnedColumnIds([...columnIds]);
680+
}}
681+
onRowSelectionChange={(updater) => {
682+
setRowSelectionState((previous) =>
683+
typeof updater === "function" ? updater(previous) : updater,
684+
);
685+
}}
686+
pageCount={1}
687+
paginationState={{ pageIndex: 0, pageSize: 20 }}
688+
pinnedColumnIds={pinnedColumnIds}
689+
rows={[{ __ps_rowid: "row_1", id: "org_acme", name: "Acme Labs" }]}
690+
rowSelectionState={rowSelectionState}
691+
/>
692+
);
693+
}
694+
695+
act(() => {
696+
root.render(<ControlledPinningHarness />);
697+
});
698+
699+
function getHeader(columnId: string): HTMLTableCellElement {
700+
const header = container.querySelector(
701+
`th[data-grid-header-column-id="${columnId}"]`,
702+
);
703+
704+
if (!(header instanceof HTMLTableCellElement)) {
705+
throw new Error(`Could not find header: ${columnId}`);
706+
}
707+
708+
return header;
709+
}
710+
711+
function clickTestButton(testId: string) {
712+
const button = container.querySelector(`[data-testid="${testId}"]`);
713+
714+
if (!(button instanceof HTMLButtonElement)) {
715+
throw new Error(`Could not find button: ${testId}`);
716+
}
717+
718+
act(() => {
719+
button.click();
720+
});
721+
}
722+
723+
expect(getHeader("name").style.width).toBe("200px");
724+
725+
clickTestButton("resize-name-to-320");
726+
expect(getHeader("name").style.width).toBe("320px");
727+
728+
clickTestButton("pin-id-controlled");
729+
expect(getHeader("id").className).toContain("sticky");
730+
expect(getHeader("name").style.width).toBe("320px");
731+
732+
act(() => {
733+
root.unmount();
734+
});
735+
container.remove();
736+
});
605737
});

ui/studio/grid/DataGrid.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -733,7 +733,19 @@ export function DataGrid(props: DataGridProps) {
733733
// Reset column order/pinning/sizing only when column identities change.
734734
// This avoids expensive state churn when parent components re-render with
735735
// new columnDef object references but equivalent column ids.
736+
//
737+
// The dependency list includes `defaultColumnPinning`, whose identity also
738+
// changes when the pinned-column props change (e.g. after pinning or
739+
// unpinning a column). Without the identity-key guard below, any pinning
740+
// update would wipe user column widths and ordering (issue #1371).
741+
const lastResetColumnIdentityKeyRef = useRef<string | null>(null);
742+
736743
useEffect(() => {
744+
if (lastResetColumnIdentityKeyRef.current === columnDefinitionIdentityKey) {
745+
return;
746+
}
747+
748+
lastResetColumnIdentityKeyRef.current = columnDefinitionIdentityKey;
737749
setColumnOrder(initialColumnOrder);
738750
setColumnPinning(defaultColumnPinning);
739751
setColumnSizing(DEFAULT_COLUMN_SIZING);

0 commit comments

Comments
 (0)