@anchrd/intel-ui 0.35.0 → 0.36.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/flows/flows.tsx +69 -9
- package/src/resource-menu/resource-menu.tsx +11 -19
package/package.json
CHANGED
package/src/flows/flows.tsx
CHANGED
|
@@ -45,7 +45,7 @@ import { SaveButton, UnsavedChangesGuard } from "@/save-button/save-button.tsx";
|
|
|
45
45
|
import { useResolvedTheme } from "@/theme/theme-context.tsx";
|
|
46
46
|
import { TitleRow, TitleRowFrame, TitleRowScrollArea } from "@/title-row/title-row.tsx";
|
|
47
47
|
|
|
48
|
-
type CanvasNode = ReactFlowNode<{ node: FlowNode }, "intel">;
|
|
48
|
+
type CanvasNode = ReactFlowNode<{ node: FlowNode; branches?: readonly string[] }, "intel">;
|
|
49
49
|
type CanvasEdge = Edge;
|
|
50
50
|
|
|
51
51
|
// One handle id at both ends of a context edge. React Flow reports the handles a connection was
|
|
@@ -53,6 +53,38 @@ type CanvasEdge = Edge;
|
|
|
53
53
|
// stores the meaning itself, not which dot it was dragged from.
|
|
54
54
|
const ContextHandle = "context";
|
|
55
55
|
|
|
56
|
+
// What a condition drawn on this canvas calls its branches. A DEFAULT for a new one, not a
|
|
57
|
+
// vocabulary: `sourceHandle` is any non-empty string in the contract, and `compileFlow` asks only
|
|
58
|
+
// that a condition's branches are unique and not null. A graph arrives over MCP as readily as from
|
|
59
|
+
// here, and one whose branches are called something else is as valid as this one.
|
|
60
|
+
const DefaultBranches = ["yes", "no"] as const;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The branch names of one condition, as the loaded graph spells them.
|
|
64
|
+
*
|
|
65
|
+
* ⚠️ Read from the edges, never imposed on them. Rendering a fixed `yes`/`no` pair left every edge
|
|
66
|
+
* saved with other names unattached to any handle — the author redrew it, the redrawn edge took
|
|
67
|
+
* this canvas's name, and the old edge stayed in the state until its target had two incoming edges
|
|
68
|
+
* and `compileFlow` refused the save (#627). The names are taken once, when the version is loaded,
|
|
69
|
+
* so deleting an edge does not take its branch off the card: the handle stays, and drawing the edge
|
|
70
|
+
* again lands on the same name it had.
|
|
71
|
+
*
|
|
72
|
+
* Filled up to two from the defaults only where the graph is short of them — a condition needs two
|
|
73
|
+
* branches to be worth drawing, and one with none at all would offer nowhere to start.
|
|
74
|
+
*/
|
|
75
|
+
function branchesOf(graph: FlowGraph, nodeId: string): string[] {
|
|
76
|
+
const named = graph.edges
|
|
77
|
+
.filter((edge) => edge.kind === "flow" && edge.source === nodeId)
|
|
78
|
+
.map((edge) => edge.sourceHandle)
|
|
79
|
+
.filter((handle): handle is string => handle !== null);
|
|
80
|
+
const branches = [...new Set(named)];
|
|
81
|
+
for (const fallback of DefaultBranches) {
|
|
82
|
+
if (branches.length >= DefaultBranches.length) break;
|
|
83
|
+
if (!branches.includes(fallback)) branches.push(fallback);
|
|
84
|
+
}
|
|
85
|
+
return branches;
|
|
86
|
+
}
|
|
87
|
+
|
|
56
88
|
// The order the bar offers them in: roughly the order a flow is built, from the start to the result.
|
|
57
89
|
// ⚠️ The start is in the bar even though every graph opens with one, because it can be deleted —
|
|
58
90
|
// without an entry there would be no way back to a flow that has a beginning.
|
|
@@ -114,6 +146,7 @@ function FlowCard({ data, selected }: NodeProps<CanvasNode>) {
|
|
|
114
146
|
const broken = isLinkNode(contract) && invalidLinks.has(contract.configuration.resourceId);
|
|
115
147
|
const Icon = nodeIcon[contract.kind];
|
|
116
148
|
const branching = contract.kind === "condition";
|
|
149
|
+
const branches = data.branches ?? DefaultBranches;
|
|
117
150
|
// ⚠️ The start is told apart by silhouette first (#39): a pill among rectangles, in the primary
|
|
118
151
|
// token. Shape survives zooming out past the point where the label is legible, and it is the only
|
|
119
152
|
// one of the three that also carries into the overview map, where nothing is written at all.
|
|
@@ -155,12 +188,18 @@ function FlowCard({ data, selected }: NodeProps<CanvasNode>) {
|
|
|
155
188
|
{contract.kind !== "output" && !branching && (
|
|
156
189
|
<Handle type="source" position={Position.Right} />
|
|
157
190
|
)}
|
|
158
|
-
{branching &&
|
|
159
|
-
|
|
160
|
-
<Handle
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
191
|
+
{branching &&
|
|
192
|
+
branches.map((branch, index) => (
|
|
193
|
+
<Handle
|
|
194
|
+
key={branch}
|
|
195
|
+
id={branch}
|
|
196
|
+
type="source"
|
|
197
|
+
position={Position.Right}
|
|
198
|
+
// Spread over the right edge rather than pinned: a condition may carry more than the two
|
|
199
|
+
// this canvas offers, and three branches stacked on two fixed points would hide one.
|
|
200
|
+
style={{ top: `${(100 * (index + 1)) / (branches.length + 1)}%` }}
|
|
201
|
+
/>
|
|
202
|
+
))}
|
|
164
203
|
{/* The context point (#37). Sideways is the order of work, downwards is what a step works
|
|
165
204
|
with — including at the output, where the same edge reads as where the result goes. Both
|
|
166
205
|
ends sit on the vertical axis so an attachment hangs under its step and the two meanings
|
|
@@ -207,13 +246,34 @@ function defaultGraph(): FlowGraph {
|
|
|
207
246
|
};
|
|
208
247
|
}
|
|
209
248
|
|
|
249
|
+
/**
|
|
250
|
+
* The edges left over once `connection` is drawn.
|
|
251
|
+
*
|
|
252
|
+
* ⚠️ A point in the order of work leads to ONE place, and drawing from it again means "there
|
|
253
|
+
* instead". `compileFlow` says so twice — a step has exactly one outgoing edge, and a condition's
|
|
254
|
+
* branches are unique — so a second line from the same point can only ever be a graph that will not
|
|
255
|
+
* save. Dropping the old one here is what #627 cost when nothing did: the author redrew a branch,
|
|
256
|
+
* both lines stayed, and the refusal that followed named a node they had not touched.
|
|
257
|
+
*
|
|
258
|
+
* The context point is the exception it has always been: a step works with as much material as it
|
|
259
|
+
* needs, and those edges leave from the same handle by design.
|
|
260
|
+
*/
|
|
261
|
+
export function clearedFor(edges: CanvasEdge[], connection: Connection): CanvasEdge[] {
|
|
262
|
+
if (connection.sourceHandle === ContextHandle) return edges;
|
|
263
|
+
return edges.filter(
|
|
264
|
+
(edge) =>
|
|
265
|
+
edge.source !== connection.source ||
|
|
266
|
+
(edge.sourceHandle ?? null) !== (connection.sourceHandle ?? null),
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
|
|
210
270
|
function canvas(graph: FlowGraph) {
|
|
211
271
|
return {
|
|
212
272
|
nodes: graph.nodes.map((node) => ({
|
|
213
273
|
id: node.id,
|
|
214
274
|
type: "intel" as const,
|
|
215
275
|
position: node.position,
|
|
216
|
-
data: { node },
|
|
276
|
+
data: node.kind === "condition" ? { node, branches: branchesOf(graph, node.id) } : { node },
|
|
217
277
|
})),
|
|
218
278
|
edges: graph.edges.map((edge) => {
|
|
219
279
|
const context = edge.kind === "context";
|
|
@@ -734,7 +794,7 @@ function FlowsEditor() {
|
|
|
734
794
|
id: crypto.randomUUID(),
|
|
735
795
|
...contextEdgeStyle(connection.sourceHandle === ContextHandle),
|
|
736
796
|
},
|
|
737
|
-
current,
|
|
797
|
+
clearedFor(current, connection),
|
|
738
798
|
),
|
|
739
799
|
);
|
|
740
800
|
}}
|
|
@@ -526,31 +526,23 @@ function VersionHistory({ target, close }: { target: ResourceTarget; close(): vo
|
|
|
526
526
|
}));
|
|
527
527
|
},
|
|
528
528
|
});
|
|
529
|
+
// ⚠️ A Modal, like every other panel in this menu — NOT an `absolute` drawer. `absolute` measures
|
|
530
|
+
// against the nearest positioned ancestor, and this menu has no say in what that is: on the title
|
|
531
|
+
// line it is the title row (`title-row.tsx`, `relative z-10`, around 100px tall), which clipped
|
|
532
|
+
// the list to its first entry on every viewport; in the folder listing it is the table container
|
|
533
|
+
// (`table.tsx`, `relative … overflow-x-auto`), which clips it differently again (#628). The list
|
|
534
|
+
// scrolls INSIDE the dialog instead, the way `NodeLinksPanel` further down does it.
|
|
529
535
|
return (
|
|
530
|
-
<
|
|
531
|
-
aria-label={i18n.t("node.versions")}
|
|
532
|
-
className="absolute bottom-0 right-0 top-0 z-10 w-80 overflow-y-auto border-l bg-card p-5 shadow-xl"
|
|
533
|
-
>
|
|
534
|
-
<div className="flex items-start justify-between gap-4">
|
|
535
|
-
<h3 className="font-semibold">{i18n.t("node.versions")}</h3>
|
|
536
|
-
<button
|
|
537
|
-
type="button"
|
|
538
|
-
onClick={close}
|
|
539
|
-
aria-label={i18n.t("common.close")}
|
|
540
|
-
className="rounded-md px-2 py-1 text-muted-foreground outline-none hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring"
|
|
541
|
-
>
|
|
542
|
-
×
|
|
543
|
-
</button>
|
|
544
|
-
</div>
|
|
536
|
+
<Modal title={i18n.t("node.versions")} close={close}>
|
|
545
537
|
{versions.isPending ? (
|
|
546
|
-
<p className="
|
|
538
|
+
<p className="text-sm text-muted-foreground">{i18n.t("common.loading")}</p>
|
|
547
539
|
) : null}
|
|
548
540
|
{versions.isError ? (
|
|
549
|
-
<p role="alert" className="
|
|
541
|
+
<p role="alert" className="text-sm text-destructive">
|
|
550
542
|
{i18n.t(target.type === "flow" ? "flows.operationFailed" : "node.operationFailed")}
|
|
551
543
|
</p>
|
|
552
544
|
) : null}
|
|
553
|
-
<ol className="
|
|
545
|
+
<ol className="max-h-72 space-y-3 overflow-y-auto">
|
|
554
546
|
{versions.data?.map((version) => (
|
|
555
547
|
<li key={version.id} className="rounded-md border p-3 text-sm">
|
|
556
548
|
<span className="font-medium">
|
|
@@ -563,7 +555,7 @@ function VersionHistory({ target, close }: { target: ResourceTarget; close(): vo
|
|
|
563
555
|
</li>
|
|
564
556
|
))}
|
|
565
557
|
</ol>
|
|
566
|
-
</
|
|
558
|
+
</Modal>
|
|
567
559
|
);
|
|
568
560
|
}
|
|
569
561
|
|