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:
@@ -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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user