Watch
0
0
Fork
You've already forked hyperhive
0

swarm-ui: draw notification flows as graphs in the list and make a new or copied flow a draft until saved

The flows list draws each flow as the editor's graph, read-only and with the
dry run's hit counts in the boxes, in place of the indented tree. A box's line
of text is cut by the browser at the box's edge, so it no longer runs out of it.

create flow (a primary button beside the refresh interval) and duplicate open
the editor on an unsaved draft at /notifications/new, with ?from= naming the
flow to copy; the flow is made by save, under an id taken from its name. Edit
and duplicate are buttons in the flow's header row. The editor's head and
inspector are laid out as sections.
This commit is contained in:
iris 2026-10-25 01:27:50 +02:00 • committed by mara
commit d0a55502a8
12 changed files with 406 additions and 262 deletions

View file

@ -12,14 +12,17 @@ import {
isLeafKind,
limitsOf,
moveAmongSiblings,
NEW_FLOW_ID,
newFlow,
newNodeId,
paletteKinds,
removeNode,
reparent,
replaceNode,
savedAs,
saveOutcome,
saveRequest,
seedFlow,
siblingPosition,
slugId,
subtreeIds,
@ -351,3 +354,43 @@ test("moveAmongSiblings swaps two branches and leaves other edges alone", () =>
assert.equal(moveAmongSiblings(f, "c", 1).ok, false, "already last");
assert.equal(moveAmongSiblings(f, "source", 1).ok, false, "the source");
});
test("seedFlow opens an empty or copied draft under the placeholder id", () => {
const view = {
revision: 1,
disabled_builtins: [],
builtins: [{ ...flow(), id: "assigned", name: "Assigned" }],
flows: [{ ...flow(), id: "mine", name: "Mine" }],
};
const empty = seedFlow(view, null);
assert.equal(empty.id, NEW_FLOW_ID);
assert.equal(empty.name, "");
assert.deepEqual(
empty.nodes.map((n) => n.kind),
["source"],
);
const copy = seedFlow(view, "assigned");
assert.equal(copy.id, NEW_FLOW_ID);
assert.equal(copy.name, "Assigned (copy)");
assert.equal(copy.nodes.length, flow().nodes.length);
assert.equal(seedFlow(view, "gone"), null);
assert.equal(view.flows.length, 1, "nothing is added to the view");
});
test("savedAs takes the id from the name, clear of every saved id", () => {
const view = {
revision: 1,
disabled_builtins: [],
builtins: [{ ...flow(), id: "assigned" }],
flows: [{ ...flow(), id: "mine" }],
};
const draft = { ...flow(), id: NEW_FLOW_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",
);
});

View file

@ -3,10 +3,8 @@ import assert from "node:assert/strict";
import { test } from "node:test";
import {
describeNode,
ellipsize,
kindValues,
ordinal,
treeRows,
withBuiltin,
} from "../src/pages/notifications/FlowTypes.ts";
@ -45,25 +43,6 @@ const flow = {
],
};
test("treeRows: each subtree follows its parent, with its depth", () => {
assert.deepEqual(
treeRows(flow).map((r) => [r.node.id, r.depth]),
[
["s", 0],
["m", 1],
["t", 2],
["m2", 1],
["u", 2],
],
);
});
test("treeRows: a cycle ends the walk and a flow with no source is empty", () => {
const cyclic = { ...flow, edges: [...flow.edges, { from: "t", to: "s" }] };
assert.equal(treeRows(cyclic).length, 5);
assert.deepEqual(treeRows({ ...flow, nodes: [] }), []);
});
test("describeNode: says what each kind does", () => {
assert.equal(describeNode(flow.nodes[0]), "forge issues, issue_assign");
assert.equal(describeNode(flow.nodes[1]), "action = closed");
@ -90,13 +69,6 @@ test("withBuiltin: switching off adds once, switching on removes", () => {
assert.deepEqual(withBuiltin(["a", "b"], "a", true), ["b"]);
});
test("ellipsize cuts long text and leaves short text alone", () => {
assert.equal(ellipsize("short", 10), "short");
assert.equal(ellipsize("exactly10!", 10), "exactly10!");
assert.equal(ellipsize("a rather long line", 10), "a rather …");
assert.equal(ellipsize("a rather long line", 10).length, 10);
});
test("kindValues lists the values a schema allows for a field", () => {
const schema = {
node_kinds: [