diff --git a/src/frontend/src/components/dashboard/DashboardLayout.tsx b/src/frontend/src/components/dashboard/DashboardLayout.tsx index 5edcf5cc26..ddccb3034c 100644 --- a/src/frontend/src/components/dashboard/DashboardLayout.tsx +++ b/src/frontend/src/components/dashboard/DashboardLayout.tsx @@ -97,11 +97,14 @@ export default function DashboardLayout() { 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 }; Object.keys(_layouts).forEach((key) => { - _layouts[key] = updateLayoutForWidget(_layouts[key], widgets, true); + _layouts[key] = updateLayoutForWidget(_layouts[key], widgets, false); }); setLayouts(_layouts); diff --git a/src/frontend/tests/pages/pui_dashboard.spec.ts b/src/frontend/tests/pages/pui_dashboard.spec.ts index acc3547e4b..dd9ef7ea05 100644 --- a/src/frontend/tests/pages/pui_dashboard.spec.ts +++ b/src/frontend/tests/pages/pui_dashboard.spec.ts @@ -1,4 +1,5 @@ import type { Page } from '@playwright/test'; +import { expect } from '@playwright/test'; import { test } from '../baseFixtures.js'; import { doCachedLogin } from '../login.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.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 = {}; + 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); + } +});