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
`/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
**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

View file

@ -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() {
<Route path="/broadcast" component={BroadcastPage} />
<Route path="/messages" component={MessagesPage} />
<Route path="/notifications" component={NotificationsPage} />
<Route path={NEW_FLOW_PATH}>
<FlowEditorPage params={{ flowId: "" }} isNew />
</Route>
<Route path="/notifications/:flowId" component={FlowEditorPage} />
<Route path="/issues" component={IssueReportPage} />
<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
// 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

View file

@ -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=<id>` 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<FlowsView | null>(null);

View file

@ -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);

View file

@ -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 (

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: "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",
);
});