@parallelworks/ui 0.38.0 → 0.39.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.
Files changed (40) hide show
  1. package/README.md +1 -1
  2. package/dist/ai/components/ChatInput.js +8 -23
  3. package/dist/ai/components/chatDraft.d.ts +7 -0
  4. package/dist/ai/components/chatDraft.js +66 -0
  5. package/dist/editing.d.ts +2 -0
  6. package/dist/editor/Monaco.js +1 -1
  7. package/dist/editor/index.js +1 -1
  8. package/dist/editor/lintContext.js +15 -3
  9. package/dist/graph/DependencyGraph.d.ts +8 -1
  10. package/dist/graph/DependencyGraph.js +433 -204
  11. package/dist/graph/GraphEditor.d.ts +27 -0
  12. package/dist/graph/GraphEditor.js +1543 -0
  13. package/dist/graph/GraphEditorDialogs.d.ts +87 -0
  14. package/dist/graph/GraphEditorDialogs.js +1808 -0
  15. package/dist/graph/InputDialog.js +4 -4
  16. package/dist/graph/InputsEditor.js +2 -2
  17. package/dist/graph/JobSummary.d.ts +3 -0
  18. package/dist/graph/JobSummary.js +34 -21
  19. package/dist/graph/MatrixGroup.d.ts +2 -1
  20. package/dist/graph/MatrixGroup.js +10 -3
  21. package/dist/graph/WorkflowSettingsDialog.d.ts +21 -0
  22. package/dist/graph/WorkflowSettingsDialog.js +670 -0
  23. package/dist/graph/editorFields.js +144 -3
  24. package/dist/graph/editorRows.d.ts +58 -0
  25. package/dist/graph/editorRows.js +460 -0
  26. package/dist/graph/editorStrings.d.ts +1 -0
  27. package/dist/graph/editorStrings.js +1 -0
  28. package/dist/graph/expressionRefs.js +39 -0
  29. package/dist/graph/fieldProblems.js +36 -2
  30. package/dist/graph/graphEditorState.d.ts +191 -0
  31. package/dist/graph/graphEditorState.js +74 -0
  32. package/dist/graph/index.d.ts +3 -0
  33. package/dist/graph/index.js +5 -2
  34. package/dist/graph/inputLayout.js +1 -1
  35. package/dist/graph/inputRefs.js +196 -4
  36. package/dist/graph/refusalText.js +13 -0
  37. package/dist/graph/stepWith.d.ts +38 -0
  38. package/dist/graph/stepWith.js +536 -0
  39. package/dist/styles.css +1 -1
  40. package/package.json +1 -1
@@ -0,0 +1,460 @@
1
+ import { BsThreeDots } from "../icons/bs/BsThreeDots.js";
2
+ import { FaPlus } from "../icons/fa/FaPlus.js";
3
+ import { MdDragHandle } from "../icons/md/MdDragHandle.js";
4
+ import { TbAlertTriangle } from "../icons/tb/TbAlertTriangle.js";
5
+ import { TOOLTIP_ID } from "../components/Tooltip.js";
6
+ import { MarqueeBox, place } from "./editorPrimitives.js";
7
+ import { EDITOR_HANDLE_CLASS, NO_SELECTION, labelsOf, problemsIn, sameEdge, sameStep, selectionBounds, useGraphEditor, useSelectedEdges, useUi } from "./graphEditorState.js";
8
+ import cx from "classnames";
9
+ import { useEffect, useRef } from "react";
10
+ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
11
+ //#region src/graph/editorRows.tsx
12
+ /** Drop indicators, the connection line and the selection, drawn in the graph's own coordinates. */
13
+ function GraphEditorCanvas() {
14
+ const api = useGraphEditor();
15
+ const drag = useUi(api, (state) => state.drag, null);
16
+ const selection = useUi(api, (state) => state.selection, NO_SELECTION);
17
+ const marquee = useUi(api, (state) => state.marquee, null);
18
+ if (!api || !drag && !marquee && selection.length === 0) return null;
19
+ const target = drag?.target ?? null;
20
+ const indicator = target?.kind === "slot" || target?.kind === "steps" && !target.around ? target.indicator : null;
21
+ const outline = target?.kind === "steps" && target.around ? target.indicator : null;
22
+ const refused = target?.kind === "slot" && target.blocked || target?.kind === "steps" && !!target.problem;
23
+ return /* @__PURE__ */ jsxs("div", {
24
+ className: "pointer-events-none absolute inset-0",
25
+ style: { zIndex: 60 },
26
+ children: [
27
+ marquee && /* @__PURE__ */ jsx(MarqueeBox, { box: marquee }),
28
+ !marquee && drag?.payload.kind !== "jobs" && /* @__PURE__ */ jsx(SelectionBox, { api }),
29
+ indicator && /* @__PURE__ */ jsx("div", {
30
+ className: cx("absolute rounded-full", refused ? "bg-(--theme-error)" : "bg-(--theme-element)"),
31
+ style: place(indicator)
32
+ }),
33
+ outline && /* @__PURE__ */ jsx("div", {
34
+ className: cx("absolute rounded-md border-4", refused ? "border-(--theme-error)" : "border-(--theme-element)"),
35
+ style: {
36
+ left: outline.left - 8,
37
+ top: outline.top - 4,
38
+ width: outline.right - outline.left + 16,
39
+ height: outline.bottom - outline.top + 8
40
+ }
41
+ }),
42
+ (target?.kind === "connect" || target?.kind === "merge") && /* @__PURE__ */ jsx("div", {
43
+ className: cx("absolute rounded-md border-4", (target.kind === "connect" ? target.edits : target.valid) ? "border-(--theme-element)" : "border-(--theme-error)"),
44
+ style: {
45
+ left: target.box.left - 8,
46
+ top: target.box.top - 4,
47
+ width: target.box.right - target.box.left + 16,
48
+ height: target.box.bottom - target.box.top + 8
49
+ }
50
+ }),
51
+ drag?.payload.kind === "connect" && drag.origin && drag.content && /* @__PURE__ */ jsx("svg", {
52
+ "aria-hidden": "true",
53
+ overflow: "visible",
54
+ className: "absolute left-0 top-0",
55
+ children: /* @__PURE__ */ jsx("line", {
56
+ x1: drag.origin.x,
57
+ y1: drag.origin.y,
58
+ x2: drag.content.x,
59
+ y2: drag.content.y,
60
+ stroke: target?.kind === "connect" && !target.edits ? "var(--theme-error)" : "var(--theme-element)",
61
+ strokeWidth: 6,
62
+ strokeDasharray: "16 10"
63
+ })
64
+ })
65
+ ]
66
+ });
67
+ }
68
+ /** One outline around the selected jobs, with a grip to move them all and circles to connect them. */
69
+ function SelectionBox({ api }) {
70
+ const t = api.t;
71
+ const grid = api.grid();
72
+ const jobs = api.selected();
73
+ const bounds = grid ? selectionBounds(grid.wrapper, jobs) : null;
74
+ if (!grid || !bounds) return null;
75
+ const anchor = grid.cols.flat(2).find((job) => jobs.includes(job));
76
+ const y = bounds.connectorY;
77
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [
78
+ /* @__PURE__ */ jsx("div", {
79
+ className: "absolute rounded-xl border-2 border-dashed border-(--theme-element)",
80
+ style: place(bounds)
81
+ }),
82
+ /* @__PURE__ */ jsx(PortCircle, {
83
+ side: "in",
84
+ at: {
85
+ x: bounds.left,
86
+ y
87
+ },
88
+ draw: true,
89
+ jobs,
90
+ port: "selection",
91
+ hint: t.selectionInHint
92
+ }),
93
+ /* @__PURE__ */ jsx(PortCircle, {
94
+ side: "out",
95
+ at: {
96
+ x: bounds.right,
97
+ y
98
+ },
99
+ draw: true,
100
+ jobs,
101
+ port: "selection",
102
+ hint: t.selectionOutHint
103
+ }),
104
+ /* @__PURE__ */ jsx("button", {
105
+ type: "button",
106
+ "aria-label": t.moveSelectionHint,
107
+ "data-tooltip-id": TOOLTIP_ID,
108
+ "data-tooltip-content": t.moveSelectionHint,
109
+ className: cx(EDITOR_HANDLE_CLASS, "pointer-events-auto absolute flex h-8 w-8 cursor-grab items-center justify-center rounded-full border-4 border-(--theme-element) bg-(--theme-panel-bg)"),
110
+ style: {
111
+ left: bounds.left - 16,
112
+ top: bounds.top - 16
113
+ },
114
+ onPointerDown: (e) => api.startDrag(e, {
115
+ kind: "jobs",
116
+ jobs,
117
+ label: labelsOf(jobs),
118
+ ...anchor ? { anchor } : {}
119
+ }),
120
+ children: /* @__PURE__ */ jsx(MdDragHandle, { className: "h-5 w-5" })
121
+ })
122
+ ] });
123
+ }
124
+ /** A multi-job box's handle for moving all its jobs at once. */
125
+ function BoxGrip({ jobs }) {
126
+ const api = useGraphEditor();
127
+ if (!api) return null;
128
+ const t = api.t;
129
+ return /* @__PURE__ */ jsx("button", {
130
+ type: "button",
131
+ "aria-label": t.moveBoxHint,
132
+ "data-tooltip-id": TOOLTIP_ID,
133
+ "data-tooltip-content": t.moveBoxHint,
134
+ className: cx(EDITOR_HANDLE_CLASS, "absolute -left-[18px] -top-[18px] flex h-8 w-8 cursor-grab items-center justify-center rounded-full border-4 theme-border bg-(--theme-panel-bg)"),
135
+ onPointerDown: (e) => api.startDrag(e, {
136
+ kind: "jobs",
137
+ jobs,
138
+ label: labelsOf(jobs)
139
+ }),
140
+ children: /* @__PURE__ */ jsx(MdDragHandle, { className: "h-5 w-5" })
141
+ });
142
+ }
143
+ var onBadge = (target) => target instanceof Element && !!target.closest("[data-problem-badge]");
144
+ /** A step's problems, or a job's with its steps', listed on hover; a click shows the first one's line. */
145
+ function ProblemBadge({ api, job, step }) {
146
+ const t = api.t;
147
+ const mine = (problems) => step === void 0 ? problems.filter((problem) => problem.job === job) : problemsIn(problems, job, step);
148
+ const messages = useUi(api, (state) => mine(state.problems).map((problem) => problem.message).join("\n"), "");
149
+ const line = useUi(api, (state) => mine(state.problems)[0]?.line ?? 0, 0);
150
+ if (!messages) return null;
151
+ return /* @__PURE__ */ jsx("button", {
152
+ type: "button",
153
+ "data-problem-badge": "",
154
+ "aria-label": t.problemCount(messages.split("\n").length),
155
+ "data-tooltip-id": TOOLTIP_ID,
156
+ "data-tooltip-content": messages,
157
+ className: "cursor-pointer rounded p-0.5 text-(--theme-error) hover:bg-(--theme-muted-panel-bg)",
158
+ onPointerDown: (e) => e.stopPropagation(),
159
+ onClick: (e) => {
160
+ e.stopPropagation();
161
+ api.reveal({ line });
162
+ },
163
+ children: /* @__PURE__ */ jsx(TbAlertTriangle, { className: "h-[0.9em] w-[0.9em]" })
164
+ });
165
+ }
166
+ /** What a job or step row does in the editor: drag, shift-click to select, a menu, its problems. */
167
+ function EditableRow({ api, selected, flagged, data, className, drag, toggle, reveal, openMenu, menuLabel, menuClassName, iconClassName, badges, children }) {
168
+ return /* @__PURE__ */ jsxs("div", {
169
+ role: "none",
170
+ ...data,
171
+ ...selected ? { "data-selected": "" } : {},
172
+ className: cx(EDITOR_HANDLE_CLASS, "-mx-0.5 px-0.5", className, selected && "bg-(--theme-element)/15", flagged ? "ring-2 ring-(--theme-error)" : selected && "ring-2 ring-(--theme-element)"),
173
+ onPointerDown: (e) => {
174
+ if (e.shiftKey) e.preventDefault();
175
+ api.startDrag(e, drag());
176
+ },
177
+ onClickCapture: (e) => {
178
+ if (onBadge(e.target)) return;
179
+ if (e.shiftKey) {
180
+ e.preventDefault();
181
+ e.stopPropagation();
182
+ if (!toggle()) return;
183
+ }
184
+ reveal();
185
+ },
186
+ onContextMenu: (e) => {
187
+ e.preventDefault();
188
+ e.stopPropagation();
189
+ openMenu(e.clientX, e.clientY);
190
+ },
191
+ children: [
192
+ children,
193
+ badges,
194
+ /* @__PURE__ */ jsx("button", {
195
+ type: "button",
196
+ "aria-label": menuLabel,
197
+ className: cx("cursor-pointer rounded theme-muted-text opacity-0 transition-opacity hover:bg-(--theme-muted-panel-bg) focus-visible:opacity-100", menuClassName),
198
+ onPointerDown: (e) => e.stopPropagation(),
199
+ onClick: (e) => {
200
+ e.stopPropagation();
201
+ const r = e.currentTarget.getBoundingClientRect();
202
+ openMenu(r.right, r.bottom);
203
+ },
204
+ children: /* @__PURE__ */ jsx(BsThreeDots, { className: iconClassName })
205
+ })
206
+ ]
207
+ });
208
+ }
209
+ /** A job row made draggable, with its menu and matrix badge; shift-click selects it. */
210
+ function EditableJobRow({ job, label, badge = true, children }) {
211
+ const api = useGraphEditor();
212
+ const selected = useUi(api, (state) => state.selection.includes(job), false);
213
+ const flagged = useUi(api, (state) => problemsIn(state.problems, job).length > 0, false);
214
+ if (!api) return children;
215
+ const t = api.t;
216
+ return /* @__PURE__ */ jsx(EditableRow, {
217
+ api,
218
+ selected,
219
+ flagged,
220
+ data: { "data-dag-job": job },
221
+ className: "group/row relative flex cursor-grab items-center gap-x-2 rounded-md [&>button:first-child]:w-auto [&>button:first-child]:flex-1",
222
+ drag: () => {
223
+ const selection = api.selected();
224
+ return selection.length > 1 && selection.includes(job) ? {
225
+ kind: "jobs",
226
+ jobs: selection,
227
+ anchor: job,
228
+ label: labelsOf(selection)
229
+ } : {
230
+ kind: "jobs",
231
+ jobs: [job],
232
+ label
233
+ };
234
+ },
235
+ toggle: () => {
236
+ api.toggleSelected(job);
237
+ return api.store.get().selection.includes(job);
238
+ },
239
+ reveal: () => api.reveal({ job }),
240
+ openMenu: (x, y) => api.openJobMenu(x, y, job),
241
+ menuLabel: t.jobActions,
242
+ menuClassName: "p-1 group-hover/row:opacity-100",
243
+ iconClassName: "h-[1em] w-[1em]",
244
+ badges: /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(ProblemBadge, {
245
+ api,
246
+ job
247
+ }), badge && api.isMatrixJob(job) && /* @__PURE__ */ jsx("span", {
248
+ className: "rounded-full border-2 theme-border px-2 text-[1rem] theme-muted-text",
249
+ children: t.matrixBadge
250
+ })] }),
251
+ children
252
+ });
253
+ }
254
+ /** One connector circle: dragging it connects `jobs`, dropping on it connects to them. */
255
+ function PortCircle({ side, at, draw, jobs, port, hint, zIndex }) {
256
+ const api = useGraphEditor();
257
+ const drag = useUi(api, (state) => state.drag, null);
258
+ if (!api) return null;
259
+ const dragging = drag?.payload.kind === "connect" && drag.payload.port === port && drag.payload.side === side;
260
+ return /* @__PURE__ */ jsxs("span", {
261
+ role: "none",
262
+ "data-dag-port": side,
263
+ ...port === "selection" ? { "data-dag-selection": "" } : { "data-dag-node": jobs[0] },
264
+ ...drag ? {} : { "data-tooltip-id": TOOLTIP_ID },
265
+ "data-tooltip-content": hint,
266
+ className: cx(EDITOR_HANDLE_CLASS, "group/port pointer-events-auto absolute flex h-8 w-8 cursor-crosshair items-center justify-center"),
267
+ style: {
268
+ left: at.x - 16,
269
+ top: at.y - 16,
270
+ ...zIndex === void 0 ? {} : { zIndex }
271
+ },
272
+ onPointerDown: (e) => api.startDrag(e, {
273
+ kind: "connect",
274
+ from: jobs,
275
+ side,
276
+ label: labelsOf(jobs),
277
+ port
278
+ }),
279
+ children: [draw && /* @__PURE__ */ jsx("span", { className: "absolute inset-0 bg-(--theme-panel-bg)" }), /* @__PURE__ */ jsx("span", { className: cx("relative h-4 w-4 rounded-full group-hover/port:bg-(--theme-element)", dragging ? "bg-(--theme-element)" : draw && "bg-(--theme-border)") })]
280
+ });
281
+ }
282
+ /**
283
+ * A node's connector circles, where the graph draws its connector dots. Drag from the
284
+ * right one to add jobs that depend on the node, from the left one to add what it needs.
285
+ */
286
+ function NodePorts({ jobs, left, right, drawLeft, drawRight, zIndex }) {
287
+ const api = useGraphEditor();
288
+ if (!api) return null;
289
+ const t = api.t;
290
+ const port = `node:${jobs[0]}`;
291
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(PortCircle, {
292
+ side: "in",
293
+ at: left,
294
+ draw: drawLeft,
295
+ jobs,
296
+ port,
297
+ hint: t.connectInHint,
298
+ zIndex
299
+ }), /* @__PURE__ */ jsx(PortCircle, {
300
+ side: "out",
301
+ at: right,
302
+ draw: drawRight,
303
+ jobs,
304
+ port,
305
+ hint: t.connectOutHint,
306
+ zIndex
307
+ })] });
308
+ }
309
+ /** A step row made draggable within its job, with its menu. */
310
+ function EditableStepRow({ job, index, label, children }) {
311
+ const api = useGraphEditor();
312
+ const selected = useUi(api, (state) => state.steps.some((ref) => ref.job === job && ref.index === index), false);
313
+ const flagged = useUi(api, (state) => problemsIn(state.problems, job, index).length > 0, false);
314
+ if (!api) return children;
315
+ const t = api.t;
316
+ return /* @__PURE__ */ jsx(EditableRow, {
317
+ api,
318
+ selected,
319
+ flagged,
320
+ data: {
321
+ "data-dag-step-job": job,
322
+ "data-dag-step": index
323
+ },
324
+ className: "group/step flex cursor-grab items-center gap-x-1 rounded",
325
+ drag: () => {
326
+ const steps = api.selectedSteps();
327
+ return selected && steps.length > 1 ? {
328
+ kind: "steps",
329
+ steps,
330
+ label: t.stepCount(steps.length)
331
+ } : {
332
+ kind: "steps",
333
+ steps: [{
334
+ job,
335
+ index
336
+ }],
337
+ label
338
+ };
339
+ },
340
+ toggle: () => {
341
+ api.toggleStep({
342
+ job,
343
+ index
344
+ });
345
+ return api.store.get().steps.some((ref) => sameStep(ref, {
346
+ job,
347
+ index
348
+ }));
349
+ },
350
+ reveal: () => api.reveal({
351
+ job,
352
+ step: index
353
+ }),
354
+ openMenu: (x, y) => api.openStepMenu(x, y, job, index),
355
+ menuLabel: t.stepActions,
356
+ menuClassName: "p-0.5 group-hover/step:opacity-100",
357
+ iconClassName: "h-[0.9em] w-[0.9em]",
358
+ badges: /* @__PURE__ */ jsx(ProblemBadge, {
359
+ api,
360
+ job,
361
+ step: index
362
+ }),
363
+ children
364
+ });
365
+ }
366
+ function AddStepButton({ job }) {
367
+ const api = useGraphEditor();
368
+ if (!api) return null;
369
+ const t = api.t;
370
+ return /* @__PURE__ */ jsxs("button", {
371
+ type: "button",
372
+ className: "ml-1.5 flex cursor-pointer items-center gap-x-1.5 text-left theme-muted-text hover:text-(--theme-link)",
373
+ onPointerDown: (e) => e.stopPropagation(),
374
+ onClick: (e) => {
375
+ e.stopPropagation();
376
+ api.addStep(job);
377
+ },
378
+ children: [/* @__PURE__ */ jsx(FaPlus, { className: "h-[0.8em] w-[0.8em]" }), t.addStep]
379
+ });
380
+ }
381
+ /** Connector hit area: a click selects the dependency, shift adds it, a right-click opens its menu. */
382
+ function EdgeHitPath({ d, edge }) {
383
+ const api = useGraphEditor();
384
+ const selected = useSelectedEdges(api).some((other) => sameEdge(other, edge));
385
+ if (!api) return null;
386
+ const t = api.t;
387
+ return /* @__PURE__ */ jsx("path", {
388
+ role: "button",
389
+ tabIndex: 0,
390
+ "aria-label": t.dependencyOf(edge.from, edge.to),
391
+ "aria-pressed": selected,
392
+ "data-dag-edge-from": edge.from,
393
+ "data-dag-edge-to": edge.to,
394
+ d,
395
+ stroke: "transparent",
396
+ strokeWidth: 28,
397
+ fill: "none",
398
+ className: cx(EDITOR_HANDLE_CLASS, "outline-none focus-visible:[stroke:color-mix(in_srgb,var(--theme-element)_35%,transparent)]"),
399
+ style: {
400
+ pointerEvents: "stroke",
401
+ cursor: "pointer"
402
+ },
403
+ onClick: (e) => api.selectEdge(edge, e.shiftKey),
404
+ onKeyDown: (e) => {
405
+ if (e.key === "Enter" || e.key === " ") {
406
+ e.preventDefault();
407
+ api.selectEdge(edge, e.shiftKey);
408
+ }
409
+ },
410
+ onContextMenu: (e) => {
411
+ e.preventDefault();
412
+ api.openEdgeMenu(e.clientX, e.clientY, edge);
413
+ }
414
+ });
415
+ }
416
+ /** A selected connector's two ends; dragging one onto another job or node moves that end there. */
417
+ function EdgeEnds({ edge, start, end, zIndex }) {
418
+ const api = useGraphEditor();
419
+ const drag = useUi(api, (state) => state.drag, null);
420
+ if (!api) return null;
421
+ const t = api.t;
422
+ const inUse = api.edgeInUse(edge);
423
+ const handle = (side, at, fixed) => /* @__PURE__ */ jsx("span", {
424
+ role: "none",
425
+ "data-dag-edge-end": side,
426
+ ...drag ? {} : { "data-tooltip-id": TOOLTIP_ID },
427
+ "data-tooltip-content": inUse ? t.dependencyInUse : t.moveDependencyEnd,
428
+ className: cx(EDITOR_HANDLE_CLASS, "pointer-events-auto absolute flex h-8 w-8 items-center justify-center", inUse ? "cursor-not-allowed" : "cursor-grab"),
429
+ style: {
430
+ left: at.x - 16,
431
+ top: at.y - 16,
432
+ zIndex
433
+ },
434
+ onPointerDown: (e) => {
435
+ if (!inUse) api.startEdgeEnd(e, edge, side, fixed);
436
+ },
437
+ children: /* @__PURE__ */ jsx("span", { className: "h-5 w-5 rounded-full border-4 border-(--theme-element) bg-(--theme-panel-bg)" })
438
+ }, side);
439
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [handle("from", start, end), handle("to", end, start)] });
440
+ }
441
+ /** Shown in place of the graph when the workflow has no jobs yet. */
442
+ /** The canvas of a graph with no jobs, which takes the graph's keys, so undo and paste still work. */
443
+ function EmptyGraphEditor({ containerRef, overlays, height, removeBorder }) {
444
+ const own = useRef(null);
445
+ useEffect(() => {
446
+ if (document.activeElement === document.body) own.current?.focus({ preventScroll: true });
447
+ }, []);
448
+ return /* @__PURE__ */ jsx("div", {
449
+ ref: (el) => {
450
+ own.current = el;
451
+ containerRef(el);
452
+ },
453
+ tabIndex: -1,
454
+ className: cx("panel relative w-full overflow-hidden outline-none", removeBorder ? "border-none" : "border"),
455
+ style: { height },
456
+ children: overlays
457
+ });
458
+ }
459
+ //#endregion
460
+ export { AddStepButton, BoxGrip, EdgeEnds, EdgeHitPath, EditableJobRow, EditableStepRow, EmptyGraphEditor, GraphEditorCanvas, NodePorts };
@@ -241,6 +241,7 @@ export declare const GRAPH_EDITOR_STRINGS: {
241
241
  otherInputs: string;
242
242
  viewForm: string;
243
243
  viewYaml: string;
244
+ fixFieldsFirst: string;
244
245
  yamlSyntax: (message: string) => string;
245
246
  yamlNotMap: string;
246
247
  fields: {
@@ -243,6 +243,7 @@ var GRAPH_EDITOR_STRINGS = {
243
243
  otherInputs: "Other inputs",
244
244
  viewForm: "Form",
245
245
  viewYaml: "YAML",
246
+ fixFieldsFirst: "Fix the fields with errors first, so the YAML shows everything you entered.",
246
247
  yamlSyntax: (message) => `The YAML doesn’t parse: ${message}`,
247
248
  yamlNotMap: "Write the settings as keys and values, such as run: make.",
248
249
  fields: {
@@ -0,0 +1,39 @@
1
+ import { asRecord } from "./records.js";
2
+ import { inputRefs, refExpression } from "./inputRefs.js";
3
+ //#region src/graph/expressionRefs.ts
4
+ var ref = (group, label) => ({
5
+ group,
6
+ label,
7
+ expression: `\${{ ${label} }}`
8
+ });
9
+ function names(value) {
10
+ if (Array.isArray(value)) return value.filter((item) => typeof item === "string");
11
+ return Object.keys(asRecord(value));
12
+ }
13
+ /** Everything an expression in `job` can read; without a job, only what the whole workflow has. */
14
+ function expressionRefs(editing, workflow, job) {
15
+ const root = asRecord(workflow);
16
+ const jobs = asRecord(root["jobs"]);
17
+ const own = job === void 0 ? {} : asRecord(jobs[job]);
18
+ const out = inputRefs(editing, editing.workflowInputsSchema(root)).map((input) => ({
19
+ group: "inputs",
20
+ label: `inputs.${input.path.join(".")}`,
21
+ expression: refExpression(input.path)
22
+ }));
23
+ const needs = Array.isArray(own["needs"]) ? own["needs"] : [];
24
+ for (const need of needs) {
25
+ if (typeof need !== "string") continue;
26
+ const target = editing.needTarget(need);
27
+ for (const output of Object.keys(asRecord(asRecord(jobs[target])["outputs"]))) out.push(ref("outputs", `needs.${target}.outputs.${output}`));
28
+ }
29
+ for (const variable of editing.matrixNames(own) ?? []) out.push(ref("matrix", `matrix.${variable}`));
30
+ const declared = asRecord(root["needs"]);
31
+ for (const name of names(declared["organizationVariables"])) out.push(ref("variables", `org.${name}`));
32
+ for (const name of names(declared["userVariables"])) out.push(ref("variables", `var.${name}`));
33
+ for (const name of Object.keys(asRecord(root["sessions"]))) out.push(ref("sessions", `sessions.${name}`));
34
+ const env = /* @__PURE__ */ new Set([...Object.keys(asRecord(root["env"])), ...Object.keys(asRecord(own["env"]))]);
35
+ for (const key of env) out.push(ref("env", `env.${key}`));
36
+ return out;
37
+ }
38
+ //#endregion
39
+ export { expressionRefs };
@@ -2,13 +2,47 @@ import { useWorkflowEditing } from "../components/Provider.js";
2
2
  import { TbAlertTriangle } from "../icons/tb/TbAlertTriangle.js";
3
3
  import { keyedByContent } from "../components/keys.js";
4
4
  import { useGraphEditorStrings } from "./editorStrings.js";
5
- import { createContext, useContext, useEffect } from "react";
5
+ import { createContext, useCallback, useContext, useEffect, useMemo, useState } from "react";
6
6
  import { jsx, jsxs } from "react/jsx-runtime";
7
7
  //#region src/graph/fieldProblems.tsx
8
8
  var FieldProblemsContext = createContext(null);
9
9
  var keyOf = (at) => at.join(".");
10
10
  var within = (problem, key) => keyOf(problem.at) === key || keyOf(problem.at).startsWith(`${key}.`);
11
+ /** Hands a form's fields the problems at the keys they set. */
12
+ function FieldProblems({ problems, children }) {
13
+ const [fields, setFields] = useState(/* @__PURE__ */ new Map());
14
+ const register = useCallback((key, id) => {
15
+ setFields((current) => current.get(key) === id ? current : new Map(current).set(key, id));
16
+ return () => setFields((current) => {
17
+ if (current.get(key) !== id) return current;
18
+ const next = new Map(current);
19
+ next.delete(key);
20
+ return next;
21
+ });
22
+ }, []);
23
+ const value = useMemo(() => ({
24
+ problems,
25
+ fields,
26
+ register
27
+ }), [
28
+ problems,
29
+ fields,
30
+ register
31
+ ]);
32
+ return /* @__PURE__ */ jsx(FieldProblemsContext.Provider, {
33
+ value,
34
+ children
35
+ });
36
+ }
11
37
  var LintScopeContext = createContext("");
38
+ /** Where the fields inside sit, such as `ssh` for a remote host's fields. */
39
+ function LintScope({ at, children }) {
40
+ const outer = useContext(LintScopeContext);
41
+ return /* @__PURE__ */ jsx(LintScopeContext.Provider, {
42
+ value: outer ? `${outer}.${at}` : at,
43
+ children
44
+ });
45
+ }
12
46
  /** The problems at the field with id `id`, which sets `key`; the dialog's list then leads to it. */
13
47
  function useFieldProblems(key, id) {
14
48
  const context = useContext(FieldProblemsContext);
@@ -74,4 +108,4 @@ function ProblemList({ problems }) {
74
108
  });
75
109
  }
76
110
  //#endregion
77
- export { FixButtons, ProblemList, useFieldLint, useFieldProblems };
111
+ export { FieldProblems, FixButtons, LintScope, ProblemList, useFieldLint, useFieldProblems };