2
0
mirror of https://github.com/inventree/InvenTree.git synced 2026-05-06 09:43:38 +00:00

Fix: preserve existing widget sizes when adding a dashboard widget (#11834)

* Fix: preserve existing widget sizes when adding a dashboard widget

When adding a new widget from the drawer, addWidget called
updateLayoutForWidget with overrideSize=true on every existing layout
key. That flag forces w/h on every layout entry to the widget's
minWidth/minHeight, which silently reset all user-customised widget
sizes back to their minimums.

Pass overrideSize=false instead. Existing widgets retain their layout
entries (and therefore their user-set sizes); the newly added widget
has no entry yet, so react-grid-layout auto-places it at default size.

Adds a Playwright regression test that resizes a widget, adds a second
widget, and asserts the first widget's height is preserved.

* Make widget-resize regression test reliable

Replace the previous drag-based test with a deterministic check that
manipulates the persisted layout directly. The resize handles only
exist when isResizable={editing} is true, so the previous test's
attempt to grab .react-resizable-handle returned null in CI.

The new test inflates the first widget's layout entry in
localStorage, reloads to rehydrate zustand, then adds a second widget
via the menu and asserts the first widget retained its enlarged
dimensions. This directly exercises the addWidget code path without
needing edit mode or mouse simulation.

* Apply biome formatting

* Patch backend profile to inflate widget; fix unused non-null assertion

Updating localStorage was wiped out by observeProfile() on reload, so
inflate the widget on the backend user profile instead.

---------

Co-authored-by: tigger2000ttfn <tigger2000ttfn@users.noreply.github.com>
This commit is contained in:
tigger2000ttfn
2026-04-30 23:58:54 -04:00
committed by GitHub
parent 2b6952eabd
commit d603358205
2 changed files with 82 additions and 2 deletions
@@ -97,11 +97,14 @@ export default function DashboardLayout() {
setWidgets([...widgets, newWidget]); setWidgets([...widgets, newWidget]);
} }
// Update the layouts to include the new widget (and enforce initial size) // Update the layouts to include the new widget.
// Pass overrideSize=false so existing widgets keep their user-set
// dimensions; only the newly added widget will receive default sizing
// via react-grid-layout's auto-placement.
const _layouts: any = { ...layouts }; const _layouts: any = { ...layouts };
Object.keys(_layouts).forEach((key) => { Object.keys(_layouts).forEach((key) => {
_layouts[key] = updateLayoutForWidget(_layouts[key], widgets, true); _layouts[key] = updateLayoutForWidget(_layouts[key], widgets, false);
}); });
setLayouts(_layouts); setLayouts(_layouts);
@@ -1,4 +1,5 @@
import type { Page } from '@playwright/test'; import type { Page } from '@playwright/test';
import { expect } from '@playwright/test';
import { test } from '../baseFixtures.js'; import { test } from '../baseFixtures.js';
import { doCachedLogin } from '../login.js'; import { doCachedLogin } from '../login.js';
import { setPluginState } from '../settings.js'; import { setPluginState } from '../settings.js';
@@ -94,3 +95,79 @@ test('Dashboard - Plugins', async ({ browser }) => {
await page.getByRole('heading', { name: 'Sample Dashboard Item' }).waitFor(); await page.getByRole('heading', { name: 'Sample Dashboard Item' }).waitFor();
await page.getByText('Hello world! This is a sample').waitFor(); await page.getByText('Hello world! This is a sample').waitFor();
}); });
test('Dashboard - Preserve widget sizes when adding new widget', async ({
browser
}) => {
// Regression: addWidget previously snapped every existing widget back to
// its minW/minH. Fix is in DashboardLayout.tsx::addWidget (overrideSize=false).
const TARGET_W = 10;
const TARGET_H = 6;
const readLayouts = (page: Page) =>
page.evaluate(() => {
const raw = localStorage.getItem('session-settings');
return raw ? (JSON.parse(raw)?.state?.layouts ?? {}) : {};
});
const page = await doCachedLogin(browser);
await resetDashboard(page);
// Add widget A; this also persists to the backend user profile.
await page.getByLabel('dashboard-menu').click();
await page.getByRole('menuitem', { name: 'Add Widget' }).click();
await page.getByLabel('dashboard-widgets-filter-input').fill('overdue order');
await page.getByLabel('add-widget-ovr-so').click();
await page.getByRole('banner').getByRole('button').click();
await page.getByText('Overdue Sales Orders').waitFor();
await page.waitForTimeout(500);
// Inflate widget A on the backend profile and reload. The auth flow on
// page load rehydrates layouts from the profile, not localStorage, so a
// localStorage-only edit would be wiped out on reload.
const current = await readLayouts(page);
const inflated: Record<string, any[]> = {};
for (const bp of Object.keys(current)) {
inflated[bp] = current[bp].map((it: any) =>
it?.i === 'ovr-so' ? { ...it, w: TARGET_W, h: TARGET_H } : it
);
}
await page.evaluate(async (layouts) => {
const csrf = document.cookie.match(/csrftoken=([^;]+)/)?.[1] ?? '';
await fetch('/api/user/profile/', {
method: 'PATCH',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrf
},
body: JSON.stringify({ widgets: { widgets: ['ovr-so'], layouts } })
});
}, inflated);
await page.reload();
await page.getByText('Overdue Sales Orders').waitFor();
await page.waitForTimeout(500);
// Sanity: profile rehydration produced the inflated values.
for (const [bp, items] of Object.entries(await readLayouts(page))) {
const entry = (items as any[]).find((i) => i?.i === 'ovr-so');
expect(entry?.w, `${bp}: ovr-so missing or wrong w`).toBe(TARGET_W);
expect(entry?.h, `${bp}: ovr-so missing or wrong h`).toBe(TARGET_H);
}
// Add widget B. With the bug, this clobbered widget A back to minW/minH.
await page.getByLabel('dashboard-menu').click();
await page.getByRole('menuitem', { name: 'Add Widget' }).click();
await page.getByLabel('dashboard-widgets-filter-input').fill('overdue order');
await page.getByLabel('add-widget-ovr-po').click();
await page.getByRole('banner').getByRole('button').click();
await page.getByText('Overdue Purchase Orders').waitFor();
await page.waitForTimeout(800);
for (const [bp, items] of Object.entries(await readLayouts(page))) {
const entry = (items as any[]).find((i) => i?.i === 'ovr-so');
expect(entry?.w, `${bp}: ovr-so width was reset`).toBe(TARGET_W);
expect(entry?.h, `${bp}: ovr-so height was reset`).toBe(TARGET_H);
}
});