@anchrd/intel-ui 0.35.0 → 0.37.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/app/view-toggle/view-toggle.tsx +7 -2
- package/src/flows/flows.tsx +80 -10
- package/src/i18n/de.json +0 -1
- package/src/i18n/en.json +0 -1
- package/src/i18n/es.json +0 -1
- package/src/resource-menu/resource-menu.tsx +23 -57
- package/src/title-row/title-row.tsx +12 -1
package/package.json
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import { useNavigate, useRouterState } from "@tanstack/react-router";
|
|
2
|
-
import {
|
|
2
|
+
import { ListChecks, Network, PencilRuler } from "lucide-react";
|
|
3
3
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
|
4
4
|
import { useI18n } from "@/i18n/i18n-context.tsx";
|
|
5
5
|
import { type IntelView, viewFrom } from "@/router/selection-search.ts";
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
// ⚠️ `runs` is NOT `History`, and that is the point of this line rather than a taste in icons.
|
|
8
|
+
// `History` is what the resource menu draws beside "Version history" (`resource-menu.tsx`), and both
|
|
9
|
+
// are visible on the same flow header — one as this switch, one in the menu right below it. The same
|
|
10
|
+
// clock for two different things was read as one: the switch was taken for the version history and
|
|
11
|
+
// asked about as such (#635). Whatever stands here has to stay out of that menu.
|
|
12
|
+
const icons = { editor: PencilRuler, graph: Network, runs: ListChecks } as const;
|
|
8
13
|
const labels = {
|
|
9
14
|
editor: "view.showEditor",
|
|
10
15
|
graph: "view.showGraph",
|
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";
|
|
@@ -615,6 +675,7 @@ function FlowsEditor() {
|
|
|
615
675
|
saving={save.isPending}
|
|
616
676
|
canMutate={canMutate}
|
|
617
677
|
incomplete={stepsMissingServer.map((node) => ({ label: node.data.node.label }))}
|
|
678
|
+
scrollingView={selection.view === "runs"}
|
|
618
679
|
onSave={() => save.mutate()}
|
|
619
680
|
onPublish={() => setPublishing(true)}
|
|
620
681
|
/>
|
|
@@ -734,7 +795,7 @@ function FlowsEditor() {
|
|
|
734
795
|
id: crypto.randomUUID(),
|
|
735
796
|
...contextEdgeStyle(connection.sourceHandle === ContextHandle),
|
|
736
797
|
},
|
|
737
|
-
current,
|
|
798
|
+
clearedFor(current, connection),
|
|
738
799
|
),
|
|
739
800
|
);
|
|
740
801
|
}}
|
|
@@ -883,6 +944,7 @@ function FlowTitle({
|
|
|
883
944
|
saving,
|
|
884
945
|
canMutate,
|
|
885
946
|
incomplete,
|
|
947
|
+
scrollingView,
|
|
886
948
|
onSave,
|
|
887
949
|
onPublish,
|
|
888
950
|
}: {
|
|
@@ -890,6 +952,9 @@ function FlowTitle({
|
|
|
890
952
|
dirty: boolean;
|
|
891
953
|
saving: boolean;
|
|
892
954
|
canMutate: boolean;
|
|
955
|
+
// Whether what stands below this line can scroll under it. The runs list can and earns its edge
|
|
956
|
+
// the usual way; the canvas and the graph pan instead, and would never get one (#634).
|
|
957
|
+
scrollingView: boolean;
|
|
893
958
|
// Tool steps that name no server yet. They cannot be saved, so the refusal is shown here rather
|
|
894
959
|
// than fetched from the server as a message that names no step.
|
|
895
960
|
incomplete: Array<{ label: string }>;
|
|
@@ -914,7 +979,12 @@ function FlowTitle({
|
|
|
914
979
|
// loudest thing in this line and used to hold the edge; the menu takes it, because a menu one can
|
|
915
980
|
// hit without looking is worth more than the primary button being outermost. `TitleRow` is what
|
|
916
981
|
// enforces that — nothing passed in here can get past the menu.
|
|
917
|
-
<TitleRow
|
|
982
|
+
<TitleRow
|
|
983
|
+
title={flow.title}
|
|
984
|
+
description={flow.description}
|
|
985
|
+
target={{ type: "flow", flow }}
|
|
986
|
+
separated={!scrollingView}
|
|
987
|
+
>
|
|
918
988
|
{/* ⚠️ #454: the toggle used to stand in the global header, beside the search — where what
|
|
919
989
|
applies EVERYWHERE stands. But it says how THIS flow is shown, and so belongs in the line
|
|
920
990
|
that names this flow. A flow is the only level with runs, and therefore the only one with
|
package/src/i18n/de.json
CHANGED
|
@@ -75,7 +75,6 @@
|
|
|
75
75
|
"resource.archive": "Archivieren",
|
|
76
76
|
"resource.columns": "Spalten",
|
|
77
77
|
"resource.export": "Export",
|
|
78
|
-
"resource.exportExcludes": "Nimmt den aktuellen Stand mit. Frühere Versionen, Freigaben und Flow-Läufe bleiben zurück. Von einem veröffentlichten Flow reist der veröffentlichte Graph — spätere Änderungen am Entwurf bleiben zurück.",
|
|
79
78
|
"resource.import": "Import",
|
|
80
79
|
"resource.validate": "Prüfen",
|
|
81
80
|
"resource.links": "Verweise",
|
package/src/i18n/en.json
CHANGED
|
@@ -75,7 +75,6 @@
|
|
|
75
75
|
"resource.archive": "Archive",
|
|
76
76
|
"resource.columns": "Columns",
|
|
77
77
|
"resource.export": "Export",
|
|
78
|
-
"resource.exportExcludes": "Carries the current state. Earlier versions, shares and flow runs stay behind. A published flow travels as its published graph — changes drafted since stay behind.",
|
|
79
78
|
"resource.import": "Import",
|
|
80
79
|
"resource.validate": "Check",
|
|
81
80
|
"resource.links": "Links",
|
package/src/i18n/es.json
CHANGED
|
@@ -75,7 +75,6 @@
|
|
|
75
75
|
"resource.archive": "Archivar",
|
|
76
76
|
"resource.columns": "Columnas",
|
|
77
77
|
"resource.export": "Exportar",
|
|
78
|
-
"resource.exportExcludes": "Se lleva el estado actual. Las versiones anteriores, los permisos y las ejecuciones de flujo se quedan. Un flujo publicado viaja con su grafo publicado; los cambios redactados después se quedan.",
|
|
79
78
|
"resource.import": "Importar",
|
|
80
79
|
"resource.validate": "Comprobar",
|
|
81
80
|
"resource.links": "Enlaces",
|
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
ShieldCheck,
|
|
19
19
|
} from "lucide-react";
|
|
20
20
|
import type * as React from "react";
|
|
21
|
-
import {
|
|
21
|
+
import { useState } from "react";
|
|
22
22
|
import { AccessSummary } from "@/access-summary/access-summary.tsx";
|
|
23
23
|
import { allTreeLevelsKey, moveErrorKey, useTreeMove } from "@/app/tree-move/tree-move.tsx";
|
|
24
24
|
import {
|
|
@@ -154,11 +154,6 @@ export function ResourceMenu({
|
|
|
154
154
|
// nowhere else: a person had strictly less reach on their own table than a model did.
|
|
155
155
|
const isTable = node !== null && node.kind === "table";
|
|
156
156
|
const bundleImport = useBundleImport({ where: title });
|
|
157
|
-
// ⚠️ Per instance, not a constant: the folder table renders one menu PER ROW, so a fixed id would
|
|
158
|
-
// put the same `id` in the document twenty times and every entry's name would resolve to the
|
|
159
|
-
// first one's.
|
|
160
|
-
const exportLabelId = useId();
|
|
161
|
-
const exportHintId = useId();
|
|
162
157
|
|
|
163
158
|
// Everything a change here can make stale. The row sits in one level of the tree, the open screen
|
|
164
159
|
// reads the record, and the flat collections behind the search, the link picker and the relation
|
|
@@ -323,38 +318,17 @@ export function ResourceMenu({
|
|
|
323
318
|
(#534). That is the export's gap on every kind it can hit, not something this entry
|
|
324
319
|
was covering — which is why it is fixed there and not by keeping a second entry.
|
|
325
320
|
|
|
326
|
-
⚠️ The
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
behind" would expect the opposite of what happens. `Excluded` in `bundle.ts` names it
|
|
338
|
-
with a word of its own for the same reason.
|
|
339
|
-
|
|
340
|
-
⚠️ It is the entry's DESCRIPTION, not part of its name: `aria-labelledby` keeps the
|
|
341
|
-
name at the one word every other entry here uses, and `aria-describedby` is what a
|
|
342
|
-
screen reader reads after it. Folding a whole sentence into the name would make this
|
|
343
|
-
the only entry in the menu somebody has to listen through to know what it does — and
|
|
344
|
-
the sentence is a condition of the action, which is what a description is for. */}
|
|
345
|
-
<DropdownMenuItem
|
|
346
|
-
className="items-start"
|
|
347
|
-
aria-labelledby={exportLabelId}
|
|
348
|
-
aria-describedby={exportHintId}
|
|
349
|
-
onSelect={() => exportBundle.mutate()}
|
|
350
|
-
>
|
|
351
|
-
<FolderDown aria-hidden="true" className="mt-0.5" />
|
|
352
|
-
<span className="flex flex-col gap-0.5">
|
|
353
|
-
<span id={exportLabelId}>{i18n.t("resource.export")}</span>
|
|
354
|
-
<span id={exportHintId} className="text-xs text-muted-foreground">
|
|
355
|
-
{i18n.t("resource.exportExcludes")}
|
|
356
|
-
</span>
|
|
357
|
-
</span>
|
|
321
|
+
⚠️ The entry carried a three-clause description under its name until 2026-08-19, put
|
|
322
|
+
there by #433/ADR-0006 so that an irreversible loss was readable BEFORE the gesture: a
|
|
323
|
+
bundle takes one version per node, shares and runs stay behind, and a published flow
|
|
324
|
+
travels as its published graph (#585). Jack removed it — it was the only entry running
|
|
325
|
+
over two lines and it pushed the menu apart. What the reader now gets instead is
|
|
326
|
+
`excluded` in the zip's `manifest.json` and `BundleImportResult.excluded` after an
|
|
327
|
+
import, both of which are read only once the zip exists. That is a deliberate trade,
|
|
328
|
+
recorded in ADR-0006 and in #636, and NOT a leftover to be restored. */}
|
|
329
|
+
<DropdownMenuItem onSelect={() => exportBundle.mutate()}>
|
|
330
|
+
<FolderDown aria-hidden="true" />
|
|
331
|
+
{i18n.t("resource.export")}
|
|
358
332
|
</DropdownMenuItem>
|
|
359
333
|
{/* The other half of the same round trip, next to it rather than in the tree's plus
|
|
360
334
|
(#346): one word in the menu, both sources under it. */}
|
|
@@ -526,31 +500,23 @@ function VersionHistory({ target, close }: { target: ResourceTarget; close(): vo
|
|
|
526
500
|
}));
|
|
527
501
|
},
|
|
528
502
|
});
|
|
503
|
+
// ⚠️ A Modal, like every other panel in this menu — NOT an `absolute` drawer. `absolute` measures
|
|
504
|
+
// against the nearest positioned ancestor, and this menu has no say in what that is: on the title
|
|
505
|
+
// line it is the title row (`title-row.tsx`, `relative z-10`, around 100px tall), which clipped
|
|
506
|
+
// the list to its first entry on every viewport; in the folder listing it is the table container
|
|
507
|
+
// (`table.tsx`, `relative … overflow-x-auto`), which clips it differently again (#628). The list
|
|
508
|
+
// scrolls INSIDE the dialog instead, the way `NodeLinksPanel` further down does it.
|
|
529
509
|
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>
|
|
510
|
+
<Modal title={i18n.t("node.versions")} close={close}>
|
|
545
511
|
{versions.isPending ? (
|
|
546
|
-
<p className="
|
|
512
|
+
<p className="text-sm text-muted-foreground">{i18n.t("common.loading")}</p>
|
|
547
513
|
) : null}
|
|
548
514
|
{versions.isError ? (
|
|
549
|
-
<p role="alert" className="
|
|
515
|
+
<p role="alert" className="text-sm text-destructive">
|
|
550
516
|
{i18n.t(target.type === "flow" ? "flows.operationFailed" : "node.operationFailed")}
|
|
551
517
|
</p>
|
|
552
518
|
) : null}
|
|
553
|
-
<ol className="
|
|
519
|
+
<ol className="max-h-72 space-y-3 overflow-y-auto">
|
|
554
520
|
{versions.data?.map((version) => (
|
|
555
521
|
<li key={version.id} className="rounded-md border p-3 text-sm">
|
|
556
522
|
<span className="font-medium">
|
|
@@ -563,7 +529,7 @@ function VersionHistory({ target, close }: { target: ResourceTarget; close(): vo
|
|
|
563
529
|
</li>
|
|
564
530
|
))}
|
|
565
531
|
</ol>
|
|
566
|
-
</
|
|
532
|
+
</Modal>
|
|
567
533
|
);
|
|
568
534
|
}
|
|
569
535
|
|
|
@@ -69,11 +69,22 @@ export function TitleRow({
|
|
|
69
69
|
title,
|
|
70
70
|
description,
|
|
71
71
|
target,
|
|
72
|
+
separated,
|
|
72
73
|
children,
|
|
73
74
|
}: {
|
|
74
75
|
title: string;
|
|
75
76
|
description?: string | null;
|
|
76
77
|
target: ResourceTarget;
|
|
78
|
+
/**
|
|
79
|
+
* A separator that does NOT wait for a scroll.
|
|
80
|
+
*
|
|
81
|
+
* ⚠️ For a surface where scrolling cannot happen, and that is the only case it is for (#634). The
|
|
82
|
+
* edge below this row is normally `scrolled`, set by `TitleRowScrollArea` — which is right where
|
|
83
|
+
* content moves under the row. The flow canvas does not scroll, it pans: nothing ever sets
|
|
84
|
+
* `scrolled`, so the row sat on the dark canvas with no edge at all. Do not pass this where the
|
|
85
|
+
* content does scroll; a permanent edge there would replace a signal with decoration.
|
|
86
|
+
*/
|
|
87
|
+
separated?: boolean;
|
|
77
88
|
children?: ReactNode;
|
|
78
89
|
}) {
|
|
79
90
|
const scroll = useContext(TitleRowScrollContext);
|
|
@@ -88,7 +99,7 @@ export function TitleRow({
|
|
|
88
99
|
data-scrolled={scroll?.scrolled || undefined}
|
|
89
100
|
className={cn(
|
|
90
101
|
"relative z-10 flex shrink-0 items-start justify-between gap-5 px-6 py-4 transition-shadow",
|
|
91
|
-
scroll?.scrolled && "shadow-[inset_0_-1px_0_var(--border)]",
|
|
102
|
+
(separated || scroll?.scrolled) && "shadow-[inset_0_-1px_0_var(--border)]",
|
|
92
103
|
)}
|
|
93
104
|
>
|
|
94
105
|
<div className="flex min-w-0 items-center gap-3">
|