From 1feaa1f6cb675268e6f1270032e5c30689aec41a Mon Sep 17 00:00:00 2001 From: stroblme Date: Fri, 28 Aug 2026 19:27:55 +0200 Subject: [PATCH] Keep the lock off the stacked editor while a dashboard is arranged MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The phone path draws through DashboardView in both modes, so a locked dashboard came out with dead controls while it was being arranged — the canvas path hands editing to its own grid and never mounts the lock. DashboardView takes `editing` and skips LockedProvider for it; reading a locked dashboard stacked still locks. Guarded in mobile.spec.ts. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01K1moruzue2kTJd3uVisgNk --- .../components/Dashboard/DashboardEditor.tsx | 1 + .../components/Dashboard/DashboardView.tsx | 34 ++++++++----- frontend/tests/mobile.spec.ts | 49 +++++++++++++++++++ 3 files changed, 72 insertions(+), 12 deletions(-) diff --git a/frontend/src/components/Dashboard/DashboardEditor.tsx b/frontend/src/components/Dashboard/DashboardEditor.tsx index 97687cb..72904c7 100644 --- a/frontend/src/components/Dashboard/DashboardEditor.tsx +++ b/frontend/src/components/Dashboard/DashboardEditor.tsx @@ -461,6 +461,7 @@ export function DashboardEditor({ diff --git a/frontend/src/components/Dashboard/DashboardView.tsx b/frontend/src/components/Dashboard/DashboardView.tsx index c147230..0990784 100644 --- a/frontend/src/components/Dashboard/DashboardView.tsx +++ b/frontend/src/components/Dashboard/DashboardView.tsx @@ -306,6 +306,7 @@ export function DashboardView({ renderWidget, stacked, rail, + editing, }: { dashboard: Dashboard renderWidget?: (widget: WidgetDef) => React.ReactNode @@ -313,6 +314,9 @@ export function DashboardView({ stacked?: boolean /** Whether the panel carries a rail, which takes a column of the canvas. */ rail?: boolean + /** Being arranged rather than used, so the lock is not mounted — the + * stacked editor draws through here, where the canvas draws its own grid. */ + editing?: boolean }) { const all = widgetsOf(dashboard) if (all.length === 0) { @@ -337,20 +341,26 @@ export function DashboardView({ : all const columns = columnsOf(dashboard) + const grid = ( + + + + ) + return ( - - - - - + {editing ? ( + grid + ) : ( + {grid} + )} ) } diff --git a/frontend/tests/mobile.spec.ts b/frontend/tests/mobile.spec.ts index 78c0011..90921a9 100644 --- a/frontend/tests/mobile.spec.ts +++ b/frontend/tests/mobile.spec.ts @@ -15,6 +15,8 @@ const dashboardName = `${flowName}_panel` /** What the bar and the forecast read. A flow of its own, so the graph the * editor lays out above stays the shape those assertions were written for. */ const feedName = `${flowName}_feed` +/** A read-only dashboard, on its own so the width checks above keep a live one. */ +const lockedName = `${flowName}_locked` test.describe.configure({ mode: "serial" }) @@ -129,6 +131,7 @@ test.beforeAll(async ({ browser }) => { // this whole name in the chart's legend. { name: "climate_series_reading", dtype: "float" }, { name: "mode", dtype: "str" }, + { name: "lamp", dtype: "bool" }, ], }, ], @@ -269,12 +272,37 @@ test.beforeAll(async ({ browser }) => { method: "POST", data: { version: draft.version }, }) + + const locked = await ( + await api(page, `/dashboards/${lockedName}`, { method: "POST" }) + ).json() + locked.settings = { ...(locked.settings ?? {}), locked: { value: true } } + locked.widgets = [ + { + id: "lamp", + type: "switch", + title: "Lamp", + layout: { lg: { x: 0, y: 0, w: 3, h: 2 } }, + config: { target: `${feedName}.lamp`, dtype: "bool", style: "button" }, + }, + ] + const lockedDraft = await ( + await api(page, `/dashboards/${lockedName}`, { + method: "PUT", + data: locked, + }) + ).json() + await api(page, `/dashboards/${lockedName}/publish`, { + method: "POST", + data: { version: lockedDraft.version }, + }) await page.close() }) test.afterAll(async ({ browser }) => { await deleteAll(browser, [ `/dashboards/${dashboardName}`, + `/dashboards/${lockedName}`, `/flows/${flowName}`, `/flows/${feedName}`, ]) @@ -360,6 +388,27 @@ test("a dashboard stacks instead of shrinking", async ({ page }) => { expect(second!.y).toBeGreaterThanOrEqual(first!.y + first!.height - 1) }) +/** + * A read-only dashboard, read and arranged at a phone's width. + * + * Stacked, both modes draw through `DashboardView` — where the canvas hands + * editing to its own grid instead — so the lock has to follow the mode rather + * than the page. Arranging a dashboard is not using it. + */ +test("a locked dashboard locks when read, not while arranged", async ({ + page, +}) => { + const lamp = page.getByRole("button", { name: "Lamp" }) + + await page.goto(`/dashboards/${lockedName}`) + await lamp.waitFor({ timeout: 15000 }) + await expect(lamp, "a locked dashboard being read").toBeDisabled() + + await page.goto(`/dashboards/${lockedName}?edit=true`) + await lamp.waitFor({ timeout: 15000 }) + await expect(lamp, "a locked dashboard being arranged").toBeEnabled() +}) + test("the panel view fits the viewport", async ({ page }) => { await page.goto(`/view/${dashboardName}`) await page.waitForSelector("[data-testid=widget-frame]", { timeout: 15000 })