From 6e7b5f84e96582cf491ab4438176b8c4e7848ca1 Mon Sep 17 00:00:00 2001 From: iris Date: Sun, 25 Oct 2026 01:33:29 +0200 Subject: [PATCH] swarm-ui: give the new-flow draft a route no flow id can take, and drop the dead hit-count rule /notifications/new could be a saved flow's own page, so the draft now opens at /notifications/draft/new, which has a segment more than any flow's route. The placeholder id no longer needs reserving. The .notify-node-hits rule styled the removed tree rows; nothing renders it. --- docs/swarm/ui.md | 2 +- frontend/packages/swarm-ui/src/App.tsx | 4 ++++ .../swarm-ui/src/pages/notifications/FlowDraft.ts | 13 ++++++------- .../src/pages/notifications/FlowEditorPage.tsx | 10 ++++++---- .../src/pages/notifications/NotificationsPage.css | 9 +-------- .../src/pages/notifications/NotificationsPage.tsx | 6 +++--- frontend/packages/swarm-ui/test/flow-draft.test.mjs | 5 ----- 7 files changed, 21 insertions(+), 28 deletions(-) diff --git a/docs/swarm/ui.md b/docs/swarm/ui.md index 70ece9c5..c4a413a1 100644 --- a/docs/swarm/ui.md +++ b/docs/swarm/ui.md @@ -251,7 +251,7 @@ works. See [deliveries and dry runs](notify-router.md#deliveries-and-dry-runs). #### Editing a flow -`/notifications/` edits one custom flow, and `/notifications/new` makes +`/notifications/` edits one custom flow, and `/notifications/draft/new` makes one: it opens on an empty flow, or on a copy of a flow with `?from=`, as **create flow** and **duplicate** do. A new flow takes its id from its name when you save it, and **save** needs a name. The canvas draws the flow left diff --git a/frontend/packages/swarm-ui/src/App.tsx b/frontend/packages/swarm-ui/src/App.tsx index 2081e70f..af15f724 100644 --- a/frontend/packages/swarm-ui/src/App.tsx +++ b/frontend/packages/swarm-ui/src/App.tsx @@ -9,6 +9,7 @@ import { AlertsPage } from "./pages/AlertsPage.js"; import { BuildsPage } from "./pages/builds/BuildsPage.js"; import { BroadcastPage } from "./pages/broadcast/BroadcastPage.js"; import { MessagesPage } from "./pages/messages/MessagesPage.js"; +import { NEW_FLOW_PATH } from "./pages/notifications/FlowDraft.js"; import { FlowEditorPage } from "./pages/notifications/FlowEditorPage.js"; import { NotificationsPage } from "./pages/notifications/NotificationsPage.js"; import { ComponentsPage } from "./pages/ComponentsPage.js"; @@ -48,6 +49,9 @@ export function App() { + + + diff --git a/frontend/packages/swarm-ui/src/pages/notifications/FlowDraft.ts b/frontend/packages/swarm-ui/src/pages/notifications/FlowDraft.ts index 517da5ca..3d7a0f3f 100644 --- a/frontend/packages/swarm-ui/src/pages/notifications/FlowDraft.ts +++ b/frontend/packages/swarm-ui/src/pages/notifications/FlowDraft.ts @@ -356,16 +356,15 @@ export function duplicateFlow( }); } -// The id of a flow nobody has saved yet. It is the editor's route for one, so -// no saved flow may take it. +// The editor's route for a flow nobody has saved yet. It has a segment more +// than a flow's own route, so no flow's id can reach it. +export const NEW_FLOW_PATH = "/notifications/draft/new"; + +// The id a draft carries until it is saved (see `savedAs`). export const NEW_FLOW_ID = "new"; function savedIds(view: FlowsView): string[] { - return [ - NEW_FLOW_ID, - ...view.builtins.map((f) => f.id), - ...view.flows.map((f) => f.id), - ]; + return [...view.builtins.map((f) => f.id), ...view.flows.map((f) => f.id)]; } // The draft the editor opens for a flow not saved yet: an empty one, or a copy diff --git a/frontend/packages/swarm-ui/src/pages/notifications/FlowEditorPage.tsx b/frontend/packages/swarm-ui/src/pages/notifications/FlowEditorPage.tsx index 3190e2ca..9c01d4fc 100644 --- a/frontend/packages/swarm-ui/src/pages/notifications/FlowEditorPage.tsx +++ b/frontend/packages/swarm-ui/src/pages/notifications/FlowEditorPage.tsx @@ -3,7 +3,7 @@ // document back at the revision it was read. Edits stay in a draft until // then, so a half-built flow never reaches the router. // -// The same page makes a flow: `/notifications/new` is an empty draft, and +// The same page makes a flow: `NEW_FLOW_PATH` is an empty draft, and // `?from=` a copy of a flow to start from. Nothing exists until Save, which // gives the flow an id made of its name. // @@ -33,7 +33,6 @@ import { isDirty, limitsOf, moveAmongSiblings, - NEW_FLOW_ID, paletteKinds, removeNode, reparent, @@ -59,9 +58,12 @@ function rootOf(flow: Flow): string | null { export function FlowEditorPage({ params, -}: RouteComponentProps<{ flowId: string }>) { + isNew = false, +}: RouteComponentProps<{ flowId: string }> & { + // The page for a flow not saved yet, which has no flow id. + isNew?: boolean; +}) { const flowId = decodeURIComponent(params.flowId); - const isNew = flowId === NEW_FLOW_ID; const from = new URLSearchParams(useSearch()).get("from"); const [, navigate] = useLocation(); const [view, setView] = useState(null); diff --git a/frontend/packages/swarm-ui/src/pages/notifications/NotificationsPage.css b/frontend/packages/swarm-ui/src/pages/notifications/NotificationsPage.css index 168dd388..a2561504 100644 --- a/frontend/packages/swarm-ui/src/pages/notifications/NotificationsPage.css +++ b/frontend/packages/swarm-ui/src/pages/notifications/NotificationsPage.css @@ -272,14 +272,7 @@ flex: 0 1 24em; } -/* The dry run's counts beside each node, and the router's "not available". */ -.notify-node-hits { - margin-left: auto; - flex: none; - color: var(--muted); - font-variant-numeric: tabular-nums; -} - +/* The dry run's header line, and the router's "not available". */ .notify-dry-run { margin: 0; color: var(--muted); diff --git a/frontend/packages/swarm-ui/src/pages/notifications/NotificationsPage.tsx b/frontend/packages/swarm-ui/src/pages/notifications/NotificationsPage.tsx index 0758b81a..ef2c5026 100644 --- a/frontend/packages/swarm-ui/src/pages/notifications/NotificationsPage.tsx +++ b/frontend/packages/swarm-ui/src/pages/notifications/NotificationsPage.tsx @@ -20,7 +20,7 @@ import { } from "../../ui/refresh-interval/RefreshInterval.js"; import { fetchJson, usePolled } from "../../ui/polled/usePolled.js"; import { Deliveries } from "./Deliveries.js"; -import { NEW_FLOW_ID } from "./FlowDraft.js"; +import { NEW_FLOW_PATH } from "./FlowDraft.js"; import { FlowGraph } from "./FlowGraph.js"; import { withBuiltin, type FlowsView } from "./FlowTypes.js"; import { @@ -91,8 +91,8 @@ export function NotificationsPage() { const draftFrom = (from?: string) => navigate( from === undefined - ? `/notifications/${NEW_FLOW_ID}` - : `/notifications/${NEW_FLOW_ID}?from=${encodeURIComponent(from)}`, + ? NEW_FLOW_PATH + : `${NEW_FLOW_PATH}?from=${encodeURIComponent(from)}`, ); return ( diff --git a/frontend/packages/swarm-ui/test/flow-draft.test.mjs b/frontend/packages/swarm-ui/test/flow-draft.test.mjs index f274f553..27c28909 100644 --- a/frontend/packages/swarm-ui/test/flow-draft.test.mjs +++ b/frontend/packages/swarm-ui/test/flow-draft.test.mjs @@ -388,9 +388,4 @@ test("savedAs takes the id from the name, clear of every saved id", () => { assert.equal(savedAs(view, { ...draft, name: "Closed" }).id, "closed"); assert.equal(savedAs(view, { ...draft, name: "Assigned" }).id, "assigned-2"); assert.equal(savedAs(view, { ...draft, name: "mine" }).id, "mine-2"); - assert.equal( - savedAs(view, { ...draft, name: "New" }).id, - "new-2", - "no flow takes the id of the new-flow route", - ); });