@parallelworks/ui 0.38.0 → 0.40.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/README.md +1 -1
- package/dist/ai/agent/permissions.d.ts +5 -4
- package/dist/ai/agent/permissions.js +15 -6
- package/dist/ai/components/ChatInput.js +8 -23
- package/dist/ai/components/agent/PermissionPicker.d.ts +3 -1
- package/dist/ai/components/agent/PermissionPicker.js +2 -2
- package/dist/ai/components/chatDraft.d.ts +7 -0
- package/dist/ai/components/chatDraft.js +66 -0
- package/dist/editing.d.ts +2 -0
- package/dist/editor/Monaco.js +1 -1
- package/dist/editor/index.js +1 -1
- package/dist/editor/lintContext.js +15 -3
- package/dist/graph/DependencyGraph.d.ts +8 -1
- package/dist/graph/DependencyGraph.js +433 -204
- package/dist/graph/GraphEditor.d.ts +27 -0
- package/dist/graph/GraphEditor.js +1543 -0
- package/dist/graph/GraphEditorDialogs.d.ts +87 -0
- package/dist/graph/GraphEditorDialogs.js +1808 -0
- package/dist/graph/InputDialog.js +4 -4
- package/dist/graph/InputsEditor.js +2 -2
- package/dist/graph/JobSummary.d.ts +3 -0
- package/dist/graph/JobSummary.js +34 -21
- package/dist/graph/MatrixGroup.d.ts +2 -1
- package/dist/graph/MatrixGroup.js +10 -3
- package/dist/graph/WorkflowSettingsDialog.d.ts +21 -0
- package/dist/graph/WorkflowSettingsDialog.js +670 -0
- package/dist/graph/editorFields.js +144 -3
- package/dist/graph/editorRows.d.ts +58 -0
- package/dist/graph/editorRows.js +460 -0
- package/dist/graph/editorStrings.d.ts +1 -0
- package/dist/graph/editorStrings.js +1 -0
- package/dist/graph/expressionRefs.js +39 -0
- package/dist/graph/fieldProblems.js +36 -2
- package/dist/graph/graphEditorState.d.ts +191 -0
- package/dist/graph/graphEditorState.js +74 -0
- package/dist/graph/index.d.ts +3 -0
- package/dist/graph/index.js +5 -2
- package/dist/graph/inputLayout.js +1 -1
- package/dist/graph/inputRefs.js +196 -4
- package/dist/graph/refusalText.js +13 -0
- package/dist/graph/stepWith.d.ts +38 -0
- package/dist/graph/stepWith.js +536 -0
- package/dist/styles.css +1 -1
- 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 };
|
|
@@ -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 };
|