Overviews: icon toolbar, dashboard drafts, publish all

Both overviews carried the same toolbar twice, left-aligned, with a search
field permanently taking a row of width. One `OverviewToolbar` now serves
them: the search folds into an icon and expands again on click (Escape puts
it away and hands focus back), create is a `+`, and everything sits right of
the page. Each page keeps its own create dialog — the toolbar only renders
the trigger — so the testids the runtime spec and the capture script drive
stayed where they were.

Dashboards get the flow store's draft/publish split. The editor autosaves
`dashboard.draft.json` beside `dashboard.json`; `/view/{name}`, `bindings_for`
and `history_requirements` keep reading the published file, so a wall panel
sees an edit only once someone publishes it. `POST /dashboards/{name}/publish`
and `/discard` mirror the flow routes down to the version precondition and the
409, `GET /dashboards/{name}?draft=true` is what the editor asks for, and the
dock grows the same Publish button — which flushes a queued save first, so an
autosave in flight is not published around. Creating a dashboard still writes
the published file directly: an empty document on a panel is harmless, and it
keeps the store free of a never-published case.

"Publish all" is a checkmark in the toolbar, live only when something actually
has `has_draft`. A summary carries no version and publish needs the one it is
based on, so each document's detail is read immediately before its publish —
honest against a stale list, and no version-less backend path to maintain.
Failures are counted rather than swallowed: three of five fails says so and
names the three.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XC2jX6Hdj7pxGGKzBTrbqB
This commit is contained in:
2026-08-17 11:57:11 +02:00
co-authored by Claude Opus 5
parent 683c25b26d
commit 9351a86eec
14 changed files with 713 additions and 144 deletions
+34 -6
View File
@@ -8,7 +8,9 @@ import {
export const dashboardKeys = {
all: ["dashboards"] as const,
detail: (name: string) => ["dashboards", name] as const,
/** The working copy and the published document are two different reads. */
detail: (name: string, draft = false) =>
["dashboards", name, draft ? "draft" : "published"] as const,
messages: ["messages"] as const,
history: (message: string) => ["messages", message, "history"] as const,
}
@@ -18,9 +20,15 @@ export const dashboardsQueryOptions = () => ({
queryFn: () => DashboardsService.readDashboards(),
})
export const dashboardQueryOptions = (name: string) => ({
queryKey: dashboardKeys.detail(name),
queryFn: () => DashboardsService.readDashboard({ name }),
/**
* A dashboard as a panel shows it, or with `draft` the copy being edited.
*
* A wall panel asks for the published one, which is the whole point of the
* split: nothing half-arranged reaches the wall until someone publishes.
*/
export const dashboardQueryOptions = (name: string, draft = false) => ({
queryKey: dashboardKeys.detail(name, draft),
queryFn: () => DashboardsService.readDashboard({ name, draft }),
})
/** Every message any flow declares — what a widget can be pointed at. */
@@ -34,14 +42,34 @@ export const messageHistoryQueryOptions = (message: string) => ({
queryFn: () => MessagesService.readMessageHistory({ name: message }),
})
/** Saving a dashboard, carrying the version it was based on. */
/** Saving a dashboard, carrying the version it was based on.
*
* This writes the draft; panels keep showing the published document.
*/
export function useSaveDashboard(name: string) {
const queryClient = useQueryClient()
return useMutation({
mutationFn: (body: DashboardDef_Input) =>
DashboardsService.saveDashboard({ name, requestBody: body }),
onSuccess: (saved) => {
queryClient.setQueryData(dashboardKeys.detail(name), saved)
queryClient.setQueryData(dashboardKeys.detail(name, true), saved)
queryClient.invalidateQueries({ queryKey: dashboardKeys.all })
},
})
}
/** Publish the unpublished changes, which is what puts them on the panels. */
export function usePublishDashboard(name: string) {
const queryClient = useQueryClient()
return useMutation({
mutationFn: (version: number) =>
DashboardsService.publishDashboard({
name,
requestBody: { version },
}),
onSuccess: (published) => {
queryClient.setQueryData(dashboardKeys.detail(name, true), published)
queryClient.setQueryData(dashboardKeys.detail(name), published)
queryClient.invalidateQueries({ queryKey: dashboardKeys.all })
},
})