Watch
0
0
Fork
You've already forked hyperhive
0

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.
This commit is contained in:
iris 2026-10-25 01:33:29 +02:00 • committed by mara
commit 6e7b5f84e9
7 changed files with 21 additions and 28 deletions

View file

@ -251,7 +251,7 @@ works. See [deliveries and dry runs](notify-router.md#deliveries-and-dry-runs).
#### Editing a flow #### Editing a flow
`/notifications/<flow id>` edits one custom flow, and `/notifications/new` makes `/notifications/<flow id>` 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=<flow id>`, as one: it opens on an empty flow, or on a copy of a flow with `?from=<flow id>`, as
**create flow** and **duplicate** do. A new flow takes its id from its name when **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 you save it, and **save** needs a name. The canvas draws the flow left

View file

@ -9,6 +9,7 @@ import { AlertsPage } from "./pages/AlertsPage.js";
import { BuildsPage } from "./pages/builds/BuildsPage.js"; import { BuildsPage } from "./pages/builds/BuildsPage.js";
import { BroadcastPage } from "./pages/broadcast/BroadcastPage.js"; import { BroadcastPage } from "./pages/broadcast/BroadcastPage.js";
import { MessagesPage } from "./pages/messages/MessagesPage.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 { FlowEditorPage } from "./pages/notifications/FlowEditorPage.js";
import { NotificationsPage } from "./pages/notifications/NotificationsPage.js"; import { NotificationsPage } from "./pages/notifications/NotificationsPage.js";
import { ComponentsPage } from "./pages/ComponentsPage.js"; import { ComponentsPage } from "./pages/ComponentsPage.js";
@ -48,6 +49,9 @@ export function App() {
<Route path="/broadcast" component={BroadcastPage} /> <Route path="/broadcast" component={BroadcastPage} />
<Route path="/messages" component={MessagesPage} /> <Route path="/messages" component={MessagesPage} />
<Route path="/notifications" component={NotificationsPage} /> <Route path="/notifications" component={NotificationsPage} />
<Route path={NEW_FLOW_PATH}>
<FlowEditorPage params={{ flowId: "" }} isNew />
</Route>
<Route path="/notifications/:flowId" component={FlowEditorPage} /> <Route path="/notifications/:flowId" component={FlowEditorPage} />
<Route path="/issues" component={IssueReportPage} /> <Route path="/issues" component={IssueReportPage} />
<Route path="/review" component={ReviewPage} /> <Route path="/review" component={ReviewPage} />

View file

@ -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 // The editor's route for a flow nobody has saved yet. It has a segment more
// no saved flow may take it. // 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"; export const NEW_FLOW_ID = "new";
function savedIds(view: FlowsView): string[] { function savedIds(view: FlowsView): string[] {
return [ return [...view.builtins.map((f) => f.id), ...view.flows.map((f) => f.id)];
NEW_FLOW_ID,
...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 // The draft the editor opens for a flow not saved yet: an empty one, or a copy

View file

@ -3,7 +3,7 @@
// document back at the revision it was read. Edits stay in a draft until // document back at the revision it was read. Edits stay in a draft until
// then, so a half-built flow never reaches the router. // 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=<id>` a copy of a flow to start from. Nothing exists until Save, which // `?from=<id>` a copy of a flow to start from. Nothing exists until Save, which
// gives the flow an id made of its name. // gives the flow an id made of its name.
// //
@ -33,7 +33,6 @@ import {
isDirty, isDirty,
limitsOf, limitsOf,
moveAmongSiblings, moveAmongSiblings,
NEW_FLOW_ID,
paletteKinds, paletteKinds,
removeNode, removeNode,
reparent, reparent,
@ -59,9 +58,12 @@ function rootOf(flow: Flow): string | null {
export function FlowEditorPage({ export function FlowEditorPage({
params, 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 flowId = decodeURIComponent(params.flowId);
const isNew = flowId === NEW_FLOW_ID;
const from = new URLSearchParams(useSearch()).get("from"); const from = new URLSearchParams(useSearch()).get("from");
const [, navigate] = useLocation(); const [, navigate] = useLocation();
const [view, setView] = useState<FlowsView | null>(null); const [view, setView] = useState<FlowsView | null>(null);

View file

@ -272,14 +272,7 @@
flex: 0 1 24em; flex: 0 1 24em;
} }
/* The dry run's counts beside each node, and the router's "not available". */ /* The dry run's header line, and the router's "not available". */
.notify-node-hits {
margin-left: auto;
flex: none;
color: var(--muted);
font-variant-numeric: tabular-nums;
}
.notify-dry-run { .notify-dry-run {
margin: 0; margin: 0;
color: var(--muted); color: var(--muted);

View file

@ -20,7 +20,7 @@ import {
} from "../../ui/refresh-interval/RefreshInterval.js"; } from "../../ui/refresh-interval/RefreshInterval.js";
import { fetchJson, usePolled } from "../../ui/polled/usePolled.js"; import { fetchJson, usePolled } from "../../ui/polled/usePolled.js";
import { Deliveries } from "./Deliveries.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 { FlowGraph } from "./FlowGraph.js";
import { withBuiltin, type FlowsView } from "./FlowTypes.js"; import { withBuiltin, type FlowsView } from "./FlowTypes.js";
import { import {
@ -91,8 +91,8 @@ export function NotificationsPage() {
const draftFrom = (from?: string) => const draftFrom = (from?: string) =>
navigate( navigate(
from === undefined from === undefined
? `/notifications/${NEW_FLOW_ID}` ? NEW_FLOW_PATH
: `/notifications/${NEW_FLOW_ID}?from=${encodeURIComponent(from)}`, : `${NEW_FLOW_PATH}?from=${encodeURIComponent(from)}`,
); );
return ( return (

View file

@ -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: "Closed" }).id, "closed");
assert.equal(savedAs(view, { ...draft, name: "Assigned" }).id, "assigned-2"); 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: "mine" }).id, "mine-2");
assert.equal(
savedAs(view, { ...draft, name: "New" }).id,
"new-2",
"no flow takes the id of the new-flow route",
);
}); });