fix(client): stop the resize buttons growing a widget over its neighbor - #177
Conversation
The dashboard grid blocks overlaps — compactType null plus preventCollision — so react-grid-layout refuses a drag that would land a widget on top of another one. The resize buttons never went through that path. handleResize mutates the layout item directly, checking only the grid bounds (item.x + item.w < gridCols), and then calls saveLayoutsToApi unconditionally. Nothing asked whether the larger widget still fit. So a resize could put a widget on top of its neighbor, and the overlap was persisted. It survives reloads, because the stored layout is what the next load rebuilds from. The second half is what makes it look like a different bug entirely. An overlap consumes the free cells a drag needs, and on a full tab there are none left. A drag then has nowhere legal to land, react-grid-layout refuses the move and returns the layout unchanged, handleLayoutChange sees nothing changed and returns before saving. The widget snaps back and no request is sent. The dashboard reads as "layout changes don't save", while the actual cause is an overlap created earlier by a resize. It compounds: every resize that overlaps removes more free space, so more drags are refused. A tab degrades toward accepting no moves at all. Three separate paths make it silent. The refused drag reports nothing, the unchanged-layout check returns early, and saveLayoutsToApi ends in an empty catch. Nothing reaches the user or the console. canCommitResize applies react-grid-layout's own collision rule to the resize path. A resize that would overlap is abandoned and the layout is returned untouched, so nothing is saved. Shrinking is always allowed — it can only free cells, and refusing it would trap a widget on a dashboard that is already overlapping, which is the state this bug leaves behind. The helper is plain functions in client/src/utils/, with no React and no grid library, so the decision is unit-testable without a renderer.
jherforth#176 landed in WidgetContainer first, so both sides added imports at the same place. Nothing else collided: the resize guard sits inside handleResize, and jherforth#176 touched the rebuild effect, handleLayoutChange and the grid's layout prop. Both import lines are kept. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Reviewed critically and merging. I resolved the conflict, reproduced the bug on ConflictTrivial — #176 landed in Reproduced on main, fixed hereTwo adjacent widgets, Main grows the widget over its neighbour and writes it to the database. The PR refuses and sends nothing. Both escape hatches still workThese were the regression risks, so I drove them rather than reasoning about them: The second matters most, and you were right to call it out: it is the state the old bug leaves dashboards in, so refusing it would strand people. On the
|
The problem
The dashboard grid is configured to block overlaps
compactTypenull pluspreventCollisionso react-grid-layout refuses a drag that would land a widget on top of another one.The resize buttons never went through that path.
handleResizeinWidgetContainermutates the layout item directly, checking only the grid bounds, then callssaveLayoutsToApiunconditionally. Nothing asked whether the larger widget still fit.So a resize could put a widget on top of its neighbor, and the overlap was persisted. It survives reloads, because the stored layout is what the next load rebuilds from.
Why it looks like a different bug
An overlap consumes the free cells a drag needs. On a full tab there are none left, so a drag has nowhere legal to land: react-grid-layout refuses the move and returns the layout unchanged,
handleLayoutChangesees nothing changed and returns before saving. The widget snaps back and no request is sent.The dashboard reads as "layout changes don't save", while the actual cause is an overlap created earlier by a resize. It compounds every overlapping resize removes more free space, so more drags are refused, until a tab accepts no moves at all.
It is silent in three separate places: the refused drag reports nothing, the unchanged-layout check returns early, and
saveLayoutsToApiends in an empty catch. Nothing reaches the user or the console.Only dashboards packed tightly enough to have no free cells are affected, which is why it can sit unnoticed for a long time on a board with slack.
The fix
canCommitResizeapplies react-grid-layout's own collision rule to the resize path. A resize that would overlap is abandoned and the layout is returned untouched, so nothing is saved.Shrinking is always allowed. It can only free cells, and refusing it would trap a widget on a dashboard that is already overlapping which is the state this bug leaves behind, so that escape hatch has to keep working.
The helper is plain functions in
client/src/utils/, with no React and no grid library, so the decision is unit-testable without a renderer.Testing
9 new unit tests in
client/src/utils/resizeGuard.test.jscover growth into free space, growth blocked by a neighbor, growth left and up, shrinking while already overlapping, and the production layout that prompted this. Full client suite passes.Verified on a live install: before the change, a resize toward a neighbor overlaps it and persists; after, the resize is refused and no write is sent, while shrinking and drags into free space still work.