Keep the lock off the stacked editor while a dashboard is arranged

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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K1moruzue2kTJd3uVisgNk
This commit is contained in:
2026-08-28 19:27:55 +02:00
co-authored by Claude Opus 5
parent 9d11e01a48
commit 1feaa1f6cb
3 changed files with 72 additions and 12 deletions
@@ -461,6 +461,7 @@ export function DashboardEditor({
<DashboardView
dashboard={draft}
stacked
editing={edit}
renderWidget={edit ? pickable : undefined}
/>
</div>
@@ -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,9 +341,7 @@ export function DashboardView({
: all
const columns = columnsOf(dashboard)
return (
<LookProvider dashboard={dashboard}>
<LockedProvider dashboard={dashboard}>
const grid = (
<PaletteProvider dashboard={dashboard}>
<WidgetGrid
dashboard={dashboard}
@@ -350,7 +352,15 @@ export function DashboardView({
renderWidget={renderWidget}
/>
</PaletteProvider>
</LockedProvider>
)
return (
<LookProvider dashboard={dashboard}>
{editing ? (
grid
) : (
<LockedProvider dashboard={dashboard}>{grid}</LockedProvider>
)}
</LookProvider>
)
}
+49
View File
@@ -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 })