@parallelworks/ui 0.37.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 (64) hide show
  1. package/README.md +1 -1
  2. package/dist/ai/components/ChatInput.js +8 -23
  3. package/dist/ai/components/ChatSidebar.js +7 -4
  4. package/dist/ai/components/chatDraft.d.ts +7 -0
  5. package/dist/ai/components/chatDraft.js +66 -0
  6. package/dist/ai/index.d.ts +1 -1
  7. package/dist/ai/index.js +2 -2
  8. package/dist/ai/ui/sidebar.d.ts +6 -0
  9. package/dist/ai/ui/sidebar.js +15 -1
  10. package/dist/editing.d.ts +2 -0
  11. package/dist/editor/Monaco.js +1 -1
  12. package/dist/editor/index.js +1 -1
  13. package/dist/editor/lintContext.js +15 -3
  14. package/dist/form/FieldWrapper.js +1 -0
  15. package/dist/form/Form.js +64 -23
  16. package/dist/form/Wizard/WizardContainer.js +66 -6
  17. package/dist/form/Wizard/WizardStepContent.js +23 -10
  18. package/dist/form/Wizard/wizardPages.d.ts +12 -0
  19. package/dist/form/Wizard/wizardPages.js +5 -0
  20. package/dist/form/formEditing.d.ts +25 -0
  21. package/dist/form/formEditing.js +20 -0
  22. package/dist/graph/DependencyGraph.d.ts +8 -1
  23. package/dist/graph/DependencyGraph.js +433 -204
  24. package/dist/graph/GraphEditor.d.ts +27 -0
  25. package/dist/graph/GraphEditor.js +1543 -0
  26. package/dist/graph/GraphEditorDialogs.d.ts +87 -0
  27. package/dist/graph/GraphEditorDialogs.js +1808 -0
  28. package/dist/graph/InputDialog.js +1568 -54
  29. package/dist/graph/InputsEditor.d.ts +12 -0
  30. package/dist/graph/InputsEditor.js +1594 -0
  31. package/dist/graph/JobSummary.d.ts +3 -0
  32. package/dist/graph/JobSummary.js +34 -21
  33. package/dist/graph/MatrixGroup.d.ts +2 -1
  34. package/dist/graph/MatrixGroup.js +10 -3
  35. package/dist/graph/ShortcutsButton.js +56 -0
  36. package/dist/graph/SuggestionInput.js +29 -0
  37. package/dist/graph/WorkflowSettingsDialog.d.ts +21 -0
  38. package/dist/graph/WorkflowSettingsDialog.js +670 -0
  39. package/dist/graph/editorApi.js +8 -0
  40. package/dist/graph/editorChrome.js +75 -0
  41. package/dist/graph/editorFields.js +535 -0
  42. package/dist/graph/editorPrimitives.js +95 -4
  43. package/dist/graph/editorRows.d.ts +58 -0
  44. package/dist/graph/editorRows.js +460 -0
  45. package/dist/graph/editorStrings.d.ts +1 -0
  46. package/dist/graph/editorStrings.js +10 -1
  47. package/dist/graph/expressionRefs.js +39 -0
  48. package/dist/graph/fieldProblems.js +111 -0
  49. package/dist/graph/graphEditorState.d.ts +191 -0
  50. package/dist/graph/graphEditorState.js +74 -0
  51. package/dist/graph/index.d.ts +4 -0
  52. package/dist/graph/index.js +6 -2
  53. package/dist/graph/inputLayout.js +337 -0
  54. package/dist/graph/inputPages.d.ts +33 -0
  55. package/dist/graph/inputPages.js +186 -0
  56. package/dist/graph/inputRefs.js +388 -0
  57. package/dist/graph/inputRows.d.ts +43 -0
  58. package/dist/graph/inputRows.js +65 -0
  59. package/dist/graph/refusalText.js +13 -0
  60. package/dist/graph/settingsViews.js +62 -0
  61. package/dist/graph/stepWith.d.ts +38 -0
  62. package/dist/graph/stepWith.js +536 -0
  63. package/dist/styles.css +1 -1
  64. package/package.json +1 -1
@@ -0,0 +1,1543 @@
1
+ import { useWorkflowEditing } from "../components/Provider.js";
2
+ import { BiSolidTrashAlt } from "../icons/bi/BiSolidTrashAlt.js";
3
+ import { BsGrid } from "../icons/bs/BsGrid.js";
4
+ import { FaEdit } from "../icons/fa/FaEdit.js";
5
+ import { FaLink } from "../icons/fa/FaLink.js";
6
+ import { FaPlus } from "../icons/fa/FaPlus.js";
7
+ import { HiDuplicate } from "../icons/hi/HiDuplicate.js";
8
+ import { IoMdClose } from "../icons/io/IoMdClose.js";
9
+ import { MdRefresh } from "../icons/md/MdRefresh.js";
10
+ import { IconButton } from "../components/IconButton.js";
11
+ import { useRowMenu } from "../list/RowContextMenu.js";
12
+ import { createStore, reclaimFocus, returningFocus, trackDrag, useFocusAfterDialog, usePressOutside } from "./editorPrimitives.js";
13
+ import { jobLabel } from "./util.js";
14
+ import { EDITOR_HANDLE_CLASS, NO_EDGES, NO_PROBLEMS, NO_SELECTION, NO_STEPS, contentBox, labelsOf, sameEdge, sameStep, selectionBounds, toContent, useUi } from "./graphEditorState.js";
15
+ import { useGraphEditorStrings } from "./editorStrings.js";
16
+ import { asRecord } from "./records.js";
17
+ import { openOnAddOf } from "./editorFields.js";
18
+ import { refusalText } from "./refusalText.js";
19
+ import { JobDialog, StepDialog } from "./GraphEditorDialogs.js";
20
+ import { overlaps, typing } from "./editorApi.js";
21
+ import { MOD_KEY } from "./ShortcutsButton.js";
22
+ import { AddChip, BarDivider, DragLabel, EditorBar } from "./editorChrome.js";
23
+ import cx from "classnames";
24
+ import { useEffect, useMemo, useRef, useState } from "react";
25
+ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
26
+ //#region src/graph/GraphEditor.tsx
27
+ var EDGE_ZONE = 48;
28
+ var INDICATOR = 6;
29
+ var copied = null;
30
+ var createUiStore = () => createStore({
31
+ drag: null,
32
+ edges: NO_EDGES,
33
+ dialog: null,
34
+ selection: NO_SELECTION,
35
+ steps: NO_STEPS,
36
+ problems: NO_PROBLEMS,
37
+ marquee: null
38
+ });
39
+ var ROW_GAP = 96;
40
+ function forgetSteps(api) {
41
+ if (api.store.get().steps.length > 0) api.store.set({ steps: NO_STEPS });
42
+ }
43
+ function withoutRefs(needs, refs) {
44
+ if (refs.length === 0) return needs;
45
+ const out = Object.create(null);
46
+ for (const [job, list] of Object.entries(needs)) out[job] = list.filter((need) => !refs.some((ref) => ref.job === job && ref.need === need));
47
+ return out;
48
+ }
49
+ function restores(edits, refs, needTarget) {
50
+ const pairs = edits.flatMap((e) => e.type === "connect" ? [e] : []);
51
+ return pairs.length === refs.length && pairs.every((pair) => refs.some((ref) => ref.job === pair.to && needTarget(ref.need) === pair.from));
52
+ }
53
+ function isMatrix(job) {
54
+ const strategy = job?.strategy;
55
+ return strategy?.matrix !== void 0 && strategy.matrix !== null;
56
+ }
57
+ function stepsOf(job) {
58
+ const steps = job?.steps;
59
+ return Array.isArray(steps) ? steps : [];
60
+ }
61
+ function boxOf(cols, job) {
62
+ for (const col of cols) for (const box of col) if (box.includes(job)) return box;
63
+ return [job];
64
+ }
65
+ function slotAt(grid, client, moving) {
66
+ const { wrapper, cols } = grid;
67
+ const p = toContent(wrapper, client);
68
+ const columns = cols.map((col, c) => {
69
+ const boxes = col.flatMap((box) => {
70
+ const el = document.getElementById(`node_${box[0]}`);
71
+ return el ? [contentBox(wrapper, el)] : [];
72
+ });
73
+ return {
74
+ boxes,
75
+ left: Math.min(...boxes.map((b) => b.left)),
76
+ right: Math.max(...boxes.map((b) => b.right)),
77
+ number: grid.columns[c] ?? c
78
+ };
79
+ });
80
+ const drawn = columns.flatMap((c) => c.boxes);
81
+ if (drawn.length === 0) return null;
82
+ const top = Math.min(...drawn.map((b) => b.top));
83
+ const bottom = Math.max(...drawn.map((b) => b.bottom));
84
+ let origin = null;
85
+ for (const [c, col] of cols.entries()) for (const [index, box] of col.entries()) if (moving && box.includes(moving.anchor)) {
86
+ const whole = box.every((job) => moving.jobs.includes(job));
87
+ const only = whole && moving.jobs.length === box.length;
88
+ origin = {
89
+ col: c,
90
+ box: index,
91
+ row: grid.rows[c]?.[index] ?? index,
92
+ whole,
93
+ alone: only && col.length === 1
94
+ };
95
+ }
96
+ const line = (left, right, y) => ({
97
+ left,
98
+ right,
99
+ top: y - INDICATOR / 2,
100
+ bottom: y + INDICATOR / 2
101
+ });
102
+ const pointerRow = Math.floor(p.y / 176);
103
+ const insertAt = (c, x) => {
104
+ if (origin?.alone && (c === origin.col || c === origin.col + 1)) return null;
105
+ const last = columns[columns.length - 1]?.number ?? -1;
106
+ return {
107
+ kind: "slot",
108
+ slot: {
109
+ column: columns[c]?.number ?? last + 1,
110
+ row: 0,
111
+ insertColumn: true
112
+ },
113
+ indicator: {
114
+ left: x - INDICATOR / 2,
115
+ right: x + INDICATOR / 2,
116
+ top,
117
+ bottom
118
+ }
119
+ };
120
+ };
121
+ const emptyColumn = (number, left) => ({
122
+ kind: "slot",
123
+ slot: {
124
+ column: number,
125
+ row: pointerRow
126
+ },
127
+ indicator: line(left + ROW_GAP, left + 381 - ROW_GAP, ROW_GAP / 2 + pointerRow * 176)
128
+ });
129
+ const rowSlot = (c) => {
130
+ const col = columns[c];
131
+ if (!col) return null;
132
+ const rows = grid.rows[c] ?? [];
133
+ const own = origin?.col === c ? origin : null;
134
+ const at = (row, y) => own?.whole && (row === own.row || row === own.row + 1 && rows.includes(row)) ? null : {
135
+ kind: "slot",
136
+ slot: {
137
+ column: col.number,
138
+ row
139
+ },
140
+ indicator: line(col.left, col.right, y)
141
+ };
142
+ if (p.y < 0) return at(pointerRow, ROW_GAP / 2 + pointerRow * 176);
143
+ let above = -1;
144
+ let edge = 0;
145
+ for (const [i, box] of col.boxes.entries()) {
146
+ const row = rows[i] ?? i;
147
+ if (p.y < box.top) {
148
+ const empty = row - above - 1;
149
+ if (empty > 0) {
150
+ const band = Math.min(empty - 1, Math.max(0, Math.floor((p.y - edge) / 176)));
151
+ return at(above + 1 + band, edge + ROW_GAP / 2 + band * 176);
152
+ }
153
+ return at(row, i === 0 ? box.top - ROW_GAP / 2 : (edge + box.top) / 2);
154
+ }
155
+ if (p.y <= box.bottom) {
156
+ if (own?.whole && own.box === i) return null;
157
+ return p.y < (box.top + box.bottom) / 2 ? at(row, box.top - ROW_GAP / 2) : at(row + 1, box.bottom + ROW_GAP / 2);
158
+ }
159
+ above = row;
160
+ edge = box.bottom;
161
+ }
162
+ const band = Math.max(0, Math.floor((p.y - edge) / 176));
163
+ return at(above + 1 + band, edge + ROW_GAP / 2 + band * 176);
164
+ };
165
+ for (let c = 0; c < columns.length; c++) {
166
+ const col = columns[c];
167
+ if (!col || col.boxes.length === 0) continue;
168
+ if (p.x < col.left) {
169
+ const prev = columns[c - 1];
170
+ if (!prev) {
171
+ if (p.x >= col.left - EDGE_ZONE) return rowSlot(c);
172
+ const away = Math.ceil((col.left - EDGE_ZONE - p.x) / 381);
173
+ return emptyColumn(col.number - away, col.left - ROW_GAP - away * 381);
174
+ }
175
+ const empty = col.number - prev.number - 1;
176
+ if (empty > 0) {
177
+ const start = prev.right + ROW_GAP;
178
+ if (p.x < start) return rowSlot(c - 1);
179
+ if (p.x >= col.left - ROW_GAP) return rowSlot(c);
180
+ const band = Math.min(empty - 1, Math.floor((p.x - start) / 381));
181
+ return emptyColumn(prev.number + 1 + band, start + band * 381);
182
+ }
183
+ const third = (col.left - prev.right) / 3;
184
+ if (p.x < prev.right + third) return rowSlot(c - 1);
185
+ if (p.x > col.left - third) return rowSlot(c);
186
+ return insertAt(c, (prev.right + col.left) / 2);
187
+ }
188
+ if (p.x <= col.right) return rowSlot(c);
189
+ }
190
+ const last = columns[columns.length - 1];
191
+ if (!last || p.x <= last.right + EDGE_ZONE) return rowSlot(columns.length - 1);
192
+ const away = Math.ceil((p.x - last.right - EDGE_ZONE) / 381);
193
+ return emptyColumn(last.number + away, last.right + ROW_GAP + (away - 1) * 381);
194
+ }
195
+ function copyOf(stepRow, wrapper) {
196
+ const selector = `[data-dag-step-job="${CSS.escape(stepRow.dataset["dagStepJob"] ?? "")}"][data-dag-step="${CSS.escape(stepRow.dataset["dagStep"] ?? "")}"]`;
197
+ let copy = stepRow;
198
+ for (let el = stepRow.parentElement; el && el !== wrapper; el = el.parentElement) {
199
+ if (el.querySelectorAll(selector).length > 1) break;
200
+ copy = el;
201
+ }
202
+ return copy;
203
+ }
204
+ function stepDropAt(grid, client, hit, stepCount) {
205
+ const { wrapper } = grid;
206
+ const stepRow = hit?.closest("[data-dag-step-job]");
207
+ const jobRow = stepRow ? null : hit?.closest("[data-dag-job]");
208
+ const on = stepRow || jobRow ? null : nodeAt(hit, grid);
209
+ const box = on ? boxOf(grid.cols, on.head) : [];
210
+ const job = stepRow?.dataset["dagStepJob"] ?? jobRow?.dataset["dagJob"] ?? (box.length === 1 ? box[0] : void 0);
211
+ if (!job) return null;
212
+ const rows = [...(stepRow ? copyOf(stepRow, wrapper) : wrapper).querySelectorAll(`[data-dag-step-job="${CSS.escape(job)}"]`)].map((row) => ({
213
+ index: Number(row.dataset["dagStep"]),
214
+ box: contentBox(wrapper, row)
215
+ })).filter((row) => row.box.bottom > row.box.top).sort((a, b) => a.index - b.index);
216
+ if (rows.length === 0) {
217
+ const around = wrapper.querySelector(`[data-dag-job="${CSS.escape(job)}"]`) ?? on?.el;
218
+ return around ? {
219
+ job,
220
+ index: stepCount(job),
221
+ indicator: contentBox(wrapper, around),
222
+ around: true
223
+ } : null;
224
+ }
225
+ const p = toContent(wrapper, client);
226
+ const below = stepRow ? rows.find((r) => (r.box.top + r.box.bottom) / 2 >= p.y) : void 0;
227
+ const index = below ? below.index : stepRow ? (rows[rows.length - 1]?.index ?? -1) + 1 : stepCount(job);
228
+ const before = [...rows].reverse().find((r) => r.index < index);
229
+ const after = rows.find((r) => r.index >= index);
230
+ const y = !before ? (after?.box.top ?? 0) - 2 : !after ? before.box.bottom + 2 : (before.box.bottom + after.box.top) / 2;
231
+ return {
232
+ job,
233
+ index,
234
+ indicator: {
235
+ left: Math.min(...rows.map((r) => r.box.left)),
236
+ right: Math.max(...rows.map((r) => r.box.right)),
237
+ top: y - 2,
238
+ bottom: y + 2
239
+ },
240
+ around: false
241
+ };
242
+ }
243
+ var NOT_EMPTY = `.${EDITOR_HANDLE_CLASS}, button, a, input, textarea, select, label, [role="menu"], [role="dialog"]`;
244
+ var ARROWS = {
245
+ ArrowUp: "up",
246
+ ArrowDown: "down",
247
+ ArrowLeft: "left",
248
+ ArrowRight: "right"
249
+ };
250
+ function segmentInBox(a, b, box) {
251
+ let low = 0;
252
+ let high = 1;
253
+ const clip = (p, q) => {
254
+ if (p === 0) return q >= 0;
255
+ const t = q / p;
256
+ if (p < 0) low = Math.max(low, t);
257
+ else high = Math.min(high, t);
258
+ return low <= high;
259
+ };
260
+ const dx = b.x - a.x;
261
+ const dy = b.y - a.y;
262
+ return clip(-dx, a.x - box.left) && clip(dx, box.right - a.x) && clip(-dy, a.y - box.top) && clip(dy, box.bottom - a.y);
263
+ }
264
+ var CONNECTOR_SAMPLE = 6;
265
+ function connectorLines(container) {
266
+ return [...container.querySelectorAll("path[data-dag-edge-from]")].map((path) => {
267
+ const edge = {
268
+ from: path.getAttribute("data-dag-edge-from") ?? "",
269
+ to: path.getAttribute("data-dag-edge-to") ?? ""
270
+ };
271
+ const matrix = path.getScreenCTM();
272
+ const length = path.getTotalLength();
273
+ if (!matrix || !(length > 0)) return {
274
+ edge,
275
+ points: []
276
+ };
277
+ const count = Math.ceil(length / CONNECTOR_SAMPLE);
278
+ return {
279
+ edge,
280
+ points: Array.from({ length: count + 1 }, (_, i) => {
281
+ const p = path.getPointAtLength(length * i / count);
282
+ return {
283
+ x: matrix.a * p.x + matrix.c * p.y + matrix.e,
284
+ y: matrix.b * p.x + matrix.d * p.y + matrix.f
285
+ };
286
+ })
287
+ };
288
+ });
289
+ }
290
+ var lineInBox = (points, box) => points.some((point, i) => segmentInBox(points[i - 1] ?? point, point, box));
291
+ function boxSelection(base, touched) {
292
+ const kind = base.selection.length > 0 && "jobs" || base.steps.length > 0 && "steps" || base.edges.length > 0 && "edges" || touched.jobs.length > 0 && "jobs" || touched.steps.length > 0 && "steps" || touched.edges.length > 0 && "edges";
293
+ const added = (kept, more, same) => [...kept, ...more.filter((item) => !kept.some((other) => same(other, item)))];
294
+ return {
295
+ selection: kind === "jobs" ? added(base.selection, touched.jobs, (a, b) => a === b) : NO_SELECTION,
296
+ steps: kind === "steps" ? added(base.steps, touched.steps, sameStep) : NO_STEPS,
297
+ edges: kind === "edges" ? added(base.edges, touched.edges, sameEdge) : NO_EDGES
298
+ };
299
+ }
300
+ function nodeAt(hit, grid) {
301
+ const heads = new Set(grid.cols.flat().map((box) => box[0]));
302
+ for (let el = hit?.closest("[id^=\"node_\"]"); el; el = el.parentElement?.closest("[id^=\"node_\"]")) {
303
+ const head = el.id.slice(5);
304
+ if (heads.has(head)) return {
305
+ head,
306
+ el
307
+ };
308
+ }
309
+ return null;
310
+ }
311
+ /**
312
+ * Editor state, handlers and overlays for a DependencyGraph given `editor`.
313
+ * `yamlJobs` are the jobs as written, before previews expand matrices.
314
+ */
315
+ function useGraphEditorState({ editor, jobs, yamlJobs, inputs, workflow, layout, container }) {
316
+ const t = useGraphEditorStrings();
317
+ const [store] = useState(createUiStore);
318
+ const gridRef = useRef(null);
319
+ const viewRef = useRef(null);
320
+ const trashRef = useRef(null);
321
+ const source = yamlJobs ?? jobs;
322
+ const latest = useRef({
323
+ editor,
324
+ source,
325
+ layout,
326
+ container
327
+ });
328
+ latest.current = {
329
+ editor,
330
+ source,
331
+ layout,
332
+ container
333
+ };
334
+ const rowMenu = useRowMenu();
335
+ const openMenu = useMemo(() => returningFocus(rowMenu.openMenu, () => latest.current.container), [rowMenu.openMenu]);
336
+ const contextMenu = rowMenu.contextMenu;
337
+ useFocusAfterDialog(store, () => latest.current.container);
338
+ const editing = useWorkflowEditing();
339
+ const api = useMemo(() => {
340
+ const { applyGraphEdit, batchOf, dependsOn, jobNeeds, jobsYaml, layoutFromCols, layoutPosition, moveJobs, needInUse, needTarget, snippetKind, stepMoveProblem, stepsYaml } = editing;
341
+ const edit = (graphEdit) => {
342
+ const result = latest.current.editor?.onEdit(graphEdit);
343
+ reclaimFocus(latest.current.container);
344
+ if (store.get().steps.length > 0 && graphEdit.type !== "moveSteps" && graphEdit.type !== "pasteSteps") store.set({ steps: NO_STEPS });
345
+ return result;
346
+ };
347
+ const materialize = () => layoutFromCols(gridRef.current?.cols ?? [], latest.current.layout, gridRef.current?.rows, gridRef.current?.columns);
348
+ const needs = () => jobNeeds(latest.current.source);
349
+ const selected = () => store.get().selection.filter((job) => Object.hasOwn(latest.current.source, job));
350
+ const stepCount = (job) => stepsOf(latest.current.source[job]).length;
351
+ const selectedSteps = () => store.get().steps.filter((ref) => ref.index < stepCount(ref.job));
352
+ const problems = /* @__PURE__ */ new Map();
353
+ const stepProblem = (refs, job) => {
354
+ if (!problems.has(job)) {
355
+ const yml = latest.current.editor?.readSource?.();
356
+ let problem;
357
+ try {
358
+ const reason = yml === void 0 ? void 0 : stepMoveProblem(yml, refs, job);
359
+ problem = reason && refusalText(t, editing, reason);
360
+ } catch {
361
+ problem = void 0;
362
+ }
363
+ problems.set(job, problem);
364
+ }
365
+ return problems.get(job);
366
+ };
367
+ const editJob = (job) => store.set({ dialog: {
368
+ kind: "job",
369
+ job
370
+ } });
371
+ const editStep = (job, index) => store.set({ dialog: {
372
+ kind: "step",
373
+ job,
374
+ index
375
+ } });
376
+ const preview = (graphEdit) => {
377
+ const yml = latest.current.editor?.readSource?.();
378
+ if (yml === void 0) return null;
379
+ try {
380
+ return applyGraphEdit({
381
+ yml,
382
+ layout: latest.current.layout
383
+ }, graphEdit);
384
+ } catch {
385
+ return null;
386
+ }
387
+ };
388
+ const addJob = (graphEdit, into) => {
389
+ const join = (created) => joinInto([created], into ?? "") ?? {
390
+ type: "groupJobs",
391
+ jobs: [created],
392
+ into: into ?? "",
393
+ layout: materialize()
394
+ };
395
+ const created = preview(graphEdit)?.created;
396
+ const addition = created ? into ? {
397
+ type: "batch",
398
+ edits: [graphEdit, join(created)]
399
+ } : graphEdit : void 0;
400
+ const next = addition ? preview(addition) : null;
401
+ if (addition && latest.current.editor?.openOnAdd === false) {
402
+ edit(addition);
403
+ return;
404
+ }
405
+ if (created && addition && next) {
406
+ store.set({ dialog: {
407
+ kind: "job",
408
+ job: created,
409
+ addition: {
410
+ edit: addition,
411
+ yml: next.yml
412
+ }
413
+ } });
414
+ return;
415
+ }
416
+ const name = into ? editing.newJobName(Object.keys(latest.current.source)) : void 0;
417
+ const result = edit(name ? {
418
+ type: "batch",
419
+ edits: [graphEdit, join(name)]
420
+ } : graphEdit);
421
+ const added = name ?? result?.created;
422
+ if (result && added && latest.current.editor?.openOnAdd !== false) editJob(added);
423
+ };
424
+ const addStep = (job) => {
425
+ const graphEdit = {
426
+ type: "addStep",
427
+ job
428
+ };
429
+ const index = stepsOf(latest.current.source[job]).length;
430
+ if (latest.current.editor?.openOnAdd === false) {
431
+ edit(graphEdit);
432
+ return;
433
+ }
434
+ const next = preview(graphEdit);
435
+ if (next) store.set({ dialog: {
436
+ kind: "step",
437
+ job,
438
+ index,
439
+ addition: {
440
+ edit: graphEdit,
441
+ yml: next.yml
442
+ }
443
+ } });
444
+ else if (edit(graphEdit)) editStep(job, index);
445
+ };
446
+ const joinEdits = (jobs, box) => {
447
+ const source = latest.current.source;
448
+ const all = needs();
449
+ const head = box[0] ?? "";
450
+ const listed = (job) => {
451
+ const raw = source[job]?.needs;
452
+ return raw === void 0 || raw === null || Array.isArray(raw);
453
+ };
454
+ if (isMatrix(source[head]) || !box.every(listed) || jobs.some((job) => isMatrix(source[job]) || !listed(job))) return null;
455
+ const node = [...box, ...jobs];
456
+ const own = (job) => all[job] ?? [];
457
+ const dropped = box.flatMap((member) => own(member).filter((need) => jobs.includes(needTarget(need))).map((need) => ({
458
+ job: member,
459
+ need
460
+ })));
461
+ const drawn = [...new Set(box.flatMap(own).map(needTarget))].filter((dep) => !node.includes(dep));
462
+ const replaced = jobs.flatMap((job) => own(job).filter((need) => !drawn.includes(needTarget(need))).map((need) => ({
463
+ job,
464
+ need
465
+ })));
466
+ const before = withoutRefs(all, [...dropped, ...replaced]);
467
+ const targets = (job) => (before[job] ?? []).map(needTarget);
468
+ const dependents = Object.keys(before).filter((job) => !node.includes(job) && targets(job).some((dep) => node.includes(dep)));
469
+ const after = { ...before };
470
+ for (const job of node) after[job] = drawn;
471
+ for (const job of dependents) after[job] = [.../* @__PURE__ */ new Set([...targets(job), ...node])];
472
+ if (node.some((job) => drawn.some((dep) => dependsOn(before, dep, job))) || node.some((a) => node.some((b) => a !== b && dependsOn(after, a, b))) || [...node, ...dependents].some((job) => dependsOn(after, job, job)) || needInUse(source, [...dropped, ...replaced])) return null;
473
+ const has = (job) => (box.includes(job) && job !== head ? before[job] : before[head])?.map(needTarget) ?? [];
474
+ return [
475
+ ...dropped.length > 0 ? [{
476
+ type: "disconnect",
477
+ needs: dropped
478
+ }] : [],
479
+ {
480
+ type: "groupJobs",
481
+ jobs,
482
+ into: head,
483
+ layout: materialize()
484
+ },
485
+ ...node.flatMap((job) => drawn.filter((dep) => !has(job).includes(dep)).map((dep) => ({
486
+ type: "connect",
487
+ from: dep,
488
+ to: job
489
+ }))),
490
+ ...dependents.flatMap((job) => node.filter((member) => !targets(job).includes(member)).map((member) => ({
491
+ type: "connect",
492
+ from: member,
493
+ to: job
494
+ })))
495
+ ];
496
+ };
497
+ const joinInto = (jobs, into) => {
498
+ const edits = joinEdits(jobs, boxOf(gridRef.current?.cols ?? [], into));
499
+ return edits && batchOf(edits);
500
+ };
501
+ const connectEdits = (payload, jobs) => {
502
+ const [deps, dependents] = payload.side === "out" ? [payload.from, jobs] : [jobs, payload.from];
503
+ const all = withoutRefs(needs(), payload.replaces ?? []);
504
+ const edits = [];
505
+ for (const to of dependents) {
506
+ const raw = latest.current.source[to]?.needs;
507
+ for (const from of deps) {
508
+ if ((all[to] ?? []).some((need) => needTarget(need) === from)) continue;
509
+ if (from === to || !(raw === void 0 || raw === null || Array.isArray(raw)) || dependsOn(all, from, to)) return null;
510
+ edits.push({
511
+ type: "connect",
512
+ from,
513
+ to
514
+ });
515
+ }
516
+ }
517
+ return edits.length > 0 ? edits : null;
518
+ };
519
+ const connectWith = (edits, acting, moved = []) => {
520
+ const ends = (end) => [...new Set(edits.flatMap((e) => e.type === "connect" ? [e[end]] : []))];
521
+ const dependents = ends("to");
522
+ const left = acting?.side === "out" && moved.length === 0;
523
+ const place = left ? {
524
+ type: "placeBeforeDependents",
525
+ jobs: acting.from,
526
+ dependents,
527
+ layout: materialize()
528
+ } : {
529
+ type: "placeAfterNeeds",
530
+ jobs: dependents,
531
+ layout: materialize()
532
+ };
533
+ const still = (left ? dependents : ends("from"))[0];
534
+ if (still !== void 0) viewRef.current?.hold(still);
535
+ return edit({
536
+ type: "batch",
537
+ edits: [
538
+ ...moved.length > 0 ? [{
539
+ type: "disconnect",
540
+ needs: moved
541
+ }] : [],
542
+ ...edits,
543
+ place
544
+ ]
545
+ });
546
+ };
547
+ const findTarget = (payload, client) => {
548
+ const grid = gridRef.current;
549
+ const trash = trashRef.current?.getBoundingClientRect();
550
+ if (payload.kind === "jobs" && trash && client.x >= trash.left && client.x <= trash.right && client.y >= trash.top && client.y <= trash.bottom) return { kind: "trash" };
551
+ if (!grid) return null;
552
+ const hit = document.elementFromPoint(client.x, client.y);
553
+ const view = grid.wrapper.closest(".react-transform-wrapper");
554
+ if (view && hit && !view.contains(hit)) return null;
555
+ if (payload.kind === "connect") {
556
+ const connecting = (jobs, box) => {
557
+ const edits = connectEdits(payload, jobs);
558
+ return payload.replaces && edits && restores(edits, payload.replaces, needTarget) ? null : {
559
+ kind: "connect",
560
+ edits,
561
+ box
562
+ };
563
+ };
564
+ const port = hit?.closest("[data-dag-port]");
565
+ const bounds = port?.dataset["dagSelection"] !== void 0 ? selectionBounds(grid.wrapper, selected()) : null;
566
+ if (bounds) return connecting(selected(), bounds);
567
+ const head = port?.dataset["dagNode"];
568
+ const node = head ? document.getElementById(`node_${head}`) : null;
569
+ if (head && node) return connecting(boxOf(grid.cols, head), contentBox(grid.wrapper, node));
570
+ const row = hit?.closest("[data-dag-job]");
571
+ const job = row?.dataset["dagJob"];
572
+ if (row && job) return connecting([job], contentBox(grid.wrapper, row));
573
+ const on = nodeAt(hit, grid);
574
+ return on ? connecting(boxOf(grid.cols, on.head), contentBox(grid.wrapper, on.el)) : null;
575
+ }
576
+ if (payload.kind === "steps") {
577
+ const at = stepDropAt(grid, client, hit, stepCount);
578
+ if (!at) return null;
579
+ const indices = payload.steps.map((ref) => ref.index).sort((a, b) => a - b);
580
+ const first = indices[0] ?? 0;
581
+ const last = indices[indices.length - 1] ?? 0;
582
+ if (payload.steps.every((ref) => ref.job === at.job) && last - first === indices.length - 1 && at.index >= first && at.index <= last + 1) return null;
583
+ const problem = stepProblem(payload.steps, at.job);
584
+ return {
585
+ kind: "steps",
586
+ ...at,
587
+ ...problem ? { problem } : {}
588
+ };
589
+ }
590
+ const moving = payload.kind === "jobs" ? {
591
+ jobs: payload.jobs,
592
+ anchor: payload.anchor ?? payload.jobs[0] ?? ""
593
+ } : null;
594
+ const on = nodeAt(hit, grid);
595
+ const box = on ? boxOf(grid.cols, on.head) : [];
596
+ if (on && !moving?.jobs.some((job) => box.includes(job))) {
597
+ const jobs = moving?.jobs ?? [];
598
+ return {
599
+ kind: "merge",
600
+ into: on.head,
601
+ jobs,
602
+ valid: (payload.kind !== "new" || !payload.matrix) && joinEdits(jobs, box) !== null,
603
+ box: contentBox(grid.wrapper, on.el)
604
+ };
605
+ }
606
+ const slot = slotAt(grid, client, moving);
607
+ if (slot?.kind === "slot" && moving) {
608
+ const { cuts } = moveJobs(materialize(), needs(), moving.jobs, slot.slot, moving.anchor);
609
+ if (needInUse(latest.current.source, cuts)) return {
610
+ ...slot,
611
+ blocked: true
612
+ };
613
+ }
614
+ return slot;
615
+ };
616
+ const drop = (payload, target) => {
617
+ switch (payload.kind) {
618
+ case "jobs":
619
+ if (target?.kind === "trash") {
620
+ edit({
621
+ type: "deleteJob",
622
+ jobs: payload.jobs
623
+ });
624
+ store.set({ selection: store.get().selection.filter((job) => !payload.jobs.includes(job)) });
625
+ } else if (target?.kind === "merge") {
626
+ const joined = target.valid ? joinInto(target.jobs, target.into) : null;
627
+ if (joined) edit(joined);
628
+ } else if (target?.kind === "slot" && !target.blocked) edit({
629
+ type: "move",
630
+ jobs: payload.jobs,
631
+ to: target.slot,
632
+ layout: materialize(),
633
+ ...payload.anchor ? { anchor: payload.anchor } : {}
634
+ });
635
+ return;
636
+ case "new":
637
+ if (target?.kind === "merge") {
638
+ if (target.valid) addJob({
639
+ type: "addJob",
640
+ ...payload.matrix ? { matrix: true } : {}
641
+ }, target.into);
642
+ } else if (target?.kind === "slot") addJob({
643
+ type: "addJob",
644
+ ...payload.matrix ? { matrix: true } : {},
645
+ to: target.slot,
646
+ layout: materialize()
647
+ });
648
+ else if ((gridRef.current?.cols.length ?? 0) === 0) addJob({
649
+ type: "addJob",
650
+ ...payload.matrix ? { matrix: true } : {}
651
+ });
652
+ return;
653
+ case "connect":
654
+ if (target?.kind === "connect" && target.edits) {
655
+ const moved = payload.replaces ?? [];
656
+ if (connectWith(target.edits, payload, moved) && moved.length > 0) store.set({ edges: NO_EDGES });
657
+ }
658
+ return;
659
+ case "steps": if (target?.kind === "steps" && !target.problem) {
660
+ const kept = payload.steps.length > 1 || selectedSteps().some((ref) => payload.steps.some((step) => sameStep(ref, step)));
661
+ const result = edit({
662
+ type: "moveSteps",
663
+ steps: payload.steps,
664
+ job: target.job,
665
+ index: target.index
666
+ });
667
+ store.set({ steps: kept && result?.steps ? result.steps : NO_STEPS });
668
+ }
669
+ }
670
+ };
671
+ const startDrag = (e, payload) => {
672
+ if (e.button !== 0) return;
673
+ e.stopPropagation();
674
+ problems.clear();
675
+ const grid = gridRef.current;
676
+ let origin = null;
677
+ if (payload.kind === "connect" && payload.origin) origin = payload.origin;
678
+ else if (payload.kind === "connect" && grid) {
679
+ const r = e.currentTarget.getBoundingClientRect();
680
+ origin = toContent(grid.wrapper, {
681
+ x: (r.left + r.right) / 2,
682
+ y: (r.top + r.bottom) / 2
683
+ });
684
+ }
685
+ trackDrag({
686
+ x: e.clientX,
687
+ y: e.clientY
688
+ }, {
689
+ escape: true,
690
+ holdText: "drag",
691
+ swallowClick: "drag",
692
+ onMove: (client) => store.set({ drag: {
693
+ payload,
694
+ client,
695
+ content: gridRef.current ? toContent(gridRef.current.wrapper, client) : null,
696
+ origin,
697
+ target: findTarget(payload, client)
698
+ } }),
699
+ onEnd: (client, dragged) => {
700
+ if (dragged) {
701
+ const target = findTarget(payload, client);
702
+ store.set({ drag: null });
703
+ drop(payload, target);
704
+ }
705
+ },
706
+ onCancel: () => store.set({ drag: null })
707
+ });
708
+ };
709
+ const connectMenu = (job, side, label) => {
710
+ const items = Object.keys(latest.current.source).flatMap((other) => {
711
+ const edits = other === job ? null : connectEdits({
712
+ kind: "connect",
713
+ from: [job],
714
+ side,
715
+ label: job,
716
+ port: ""
717
+ }, [other]);
718
+ return edits ? [{
719
+ kind: "action",
720
+ label: jobLabel(other),
721
+ onSelect: () => connectWith(edits, {
722
+ from: [job],
723
+ side
724
+ })
725
+ }] : [];
726
+ });
727
+ return items.length > 0 ? [{
728
+ kind: "submenu",
729
+ label,
730
+ icon: /* @__PURE__ */ jsx(FaLink, {}),
731
+ items
732
+ }] : [];
733
+ };
734
+ const openJobMenu = (x, y, job) => {
735
+ const matrix = isMatrix(latest.current.source[job]);
736
+ const items = [
737
+ {
738
+ kind: "action",
739
+ label: matrix ? t.editMatrix : t.editJob,
740
+ icon: matrix ? /* @__PURE__ */ jsx(BsGrid, {}) : /* @__PURE__ */ jsx(FaEdit, {}),
741
+ onSelect: () => editJob(job)
742
+ },
743
+ {
744
+ kind: "action",
745
+ label: t.duplicate,
746
+ icon: /* @__PURE__ */ jsx(HiDuplicate, {}),
747
+ onSelect: () => edit({
748
+ type: "duplicateJob",
749
+ job,
750
+ layout: materialize()
751
+ })
752
+ },
753
+ {
754
+ kind: "action",
755
+ label: t.addStep,
756
+ icon: /* @__PURE__ */ jsx(FaPlus, {}),
757
+ onSelect: () => addStep(job)
758
+ },
759
+ matrix ? {
760
+ kind: "action",
761
+ label: t.removeMatrix,
762
+ icon: /* @__PURE__ */ jsx(IoMdClose, {}),
763
+ onSelect: () => edit({
764
+ type: "setMatrix",
765
+ job,
766
+ variables: null
767
+ })
768
+ } : {
769
+ kind: "action",
770
+ label: t.makeMatrix,
771
+ icon: /* @__PURE__ */ jsx(BsGrid, {}),
772
+ onSelect: () => edit({
773
+ type: "setMatrix",
774
+ job,
775
+ variables: [["value", [1, 2]]]
776
+ })
777
+ },
778
+ ...connectMenu(job, "in", t.menuDependsOn),
779
+ ...connectMenu(job, "out", t.menuNeededBy),
780
+ { kind: "divider" },
781
+ {
782
+ kind: "action",
783
+ label: t.deleteJob,
784
+ icon: /* @__PURE__ */ jsx(BiSolidTrashAlt, {}),
785
+ destructive: true,
786
+ onSelect: () => edit({
787
+ type: "deleteJob",
788
+ jobs: [job]
789
+ })
790
+ }
791
+ ];
792
+ openMenu(x, y, items);
793
+ };
794
+ const openStepMenu = (x, y, job, index) => {
795
+ openMenu(x, y, [{
796
+ kind: "action",
797
+ label: t.editStep,
798
+ icon: /* @__PURE__ */ jsx(FaEdit, {}),
799
+ onSelect: () => editStep(job, index)
800
+ }, {
801
+ kind: "action",
802
+ label: t.deleteStep,
803
+ icon: /* @__PURE__ */ jsx(BiSolidTrashAlt, {}),
804
+ destructive: true,
805
+ onSelect: () => edit({
806
+ type: "deleteStep",
807
+ job,
808
+ index
809
+ })
810
+ }]);
811
+ };
812
+ const edgeRefs = (edge) => {
813
+ const cols = gridRef.current?.cols ?? [];
814
+ const deps = boxOf(cols, edge.from);
815
+ const all = needs();
816
+ return boxOf(cols, edge.to).flatMap((job) => (all[job] ?? []).filter((need) => deps.includes(needTarget(need))).map((need) => ({
817
+ job,
818
+ need
819
+ })));
820
+ };
821
+ const refsInUse = (refs) => !!needInUse(latest.current.source, refs);
822
+ const selectEdge = (edge, add) => {
823
+ const { edges, selection, steps } = store.get();
824
+ if (!add || steps.length > 0 || selection.length > 0) store.set({
825
+ edges: [edge],
826
+ selection: NO_SELECTION,
827
+ steps: NO_STEPS
828
+ });
829
+ else store.set({ edges: edges.some((other) => sameEdge(other, edge)) ? edges.filter((other) => !sameEdge(other, edge)) : [...edges, edge] });
830
+ };
831
+ const startEdgeEnd = (e, edge, end, fixed) => {
832
+ const refs = edgeRefs(edge);
833
+ if (refs.length === 0 || refsInUse(refs)) return;
834
+ const jobs = boxOf(gridRef.current?.cols ?? [], end === "to" ? edge.from : edge.to);
835
+ startDrag(e, {
836
+ kind: "connect",
837
+ from: jobs,
838
+ side: end === "to" ? "out" : "in",
839
+ label: labelsOf(jobs),
840
+ port: `edge:${edge.from}:${edge.to}`,
841
+ replaces: refs,
842
+ origin: fixed
843
+ });
844
+ };
845
+ const editSelection = () => {
846
+ const { selection, steps } = store.get();
847
+ const [step] = steps;
848
+ if (steps.length === 1 && step && selection.length === 0) {
849
+ editStep(step.job, step.index);
850
+ return true;
851
+ }
852
+ const [job] = selection;
853
+ if (selection.length === 1 && job && steps.length === 0) {
854
+ editJob(job);
855
+ return true;
856
+ }
857
+ return false;
858
+ };
859
+ const deleteSelection = () => {
860
+ const jobs = selected();
861
+ const gone = (job) => jobs.includes(job);
862
+ const refs = store.get().edges.flatMap(edgeRefs).filter((ref) => !gone(ref.job) && !gone(needTarget(ref.need)));
863
+ const steps = selectedSteps().filter((ref) => !gone(ref.job)).sort((a, b) => b.index - a.index);
864
+ const edits = [
865
+ ...refs.length > 0 ? [{
866
+ type: "disconnect",
867
+ needs: refs
868
+ }] : [],
869
+ ...steps.map((ref) => ({
870
+ type: "deleteStep",
871
+ job: ref.job,
872
+ index: ref.index
873
+ })),
874
+ ...jobs.length > 0 ? [{
875
+ type: "deleteJob",
876
+ jobs
877
+ }] : []
878
+ ];
879
+ const only = edits.length === 1 ? edits[0] : void 0;
880
+ if (edits.length === 0) return false;
881
+ if (edit(only ?? {
882
+ type: "batch",
883
+ edits
884
+ })) store.set({
885
+ selection: NO_SELECTION,
886
+ steps: NO_STEPS,
887
+ edges: NO_EDGES
888
+ });
889
+ return true;
890
+ };
891
+ const openEdgeMenu = (x, y, edge) => {
892
+ const cols = gridRef.current?.cols ?? [];
893
+ const deps = boxOf(cols, edge.from);
894
+ const targets = boxOf(cols, edge.to);
895
+ const refs = edgeRefs(edge);
896
+ const inUse = refsInUse(refs);
897
+ const items = [{
898
+ kind: "action",
899
+ label: t.removeDependency,
900
+ icon: /* @__PURE__ */ jsx(BiSolidTrashAlt, {}),
901
+ destructive: true,
902
+ disabled: inUse,
903
+ tooltip: inUse ? t.dependencyInUse : void 0,
904
+ onSelect: () => edit({
905
+ type: "disconnect",
906
+ needs: refs
907
+ })
908
+ }];
909
+ if (deps.length === 1 && isMatrix(latest.current.source[edge.from])) {
910
+ const any = refs.length > 0 && refs.every((r) => r.need.endsWith(":any"));
911
+ items.unshift({
912
+ kind: "action",
913
+ label: t.waitForAny,
914
+ icon: /* @__PURE__ */ jsx(BsGrid, {}),
915
+ selected: any,
916
+ onSelect: () => edit({
917
+ type: "setNeedAny",
918
+ jobs: targets,
919
+ dep: edge.from,
920
+ any: !any
921
+ })
922
+ });
923
+ }
924
+ if (!store.get().edges.some((other) => sameEdge(other, edge))) selectEdge(edge, false);
925
+ openMenu(x, y, items);
926
+ };
927
+ const copySelection = () => {
928
+ const yml = latest.current.editor?.readSource?.();
929
+ if (yml === void 0) return null;
930
+ const steps = selectedSteps();
931
+ if (steps.length > 0) {
932
+ const text = stepsYaml(yml, steps);
933
+ copied = {
934
+ text,
935
+ steps
936
+ };
937
+ return text;
938
+ }
939
+ const layout = materialize();
940
+ const at = (job) => layoutPosition(layout, job) ?? {
941
+ column: 0,
942
+ row: 0
943
+ };
944
+ const jobs = selected().sort((a, b) => at(a).column - at(b).column || at(a).row - at(b).row);
945
+ const origin = jobs[0] ? at(jobs[0]) : void 0;
946
+ if (!origin) return null;
947
+ const text = jobsYaml(yml, jobs);
948
+ copied = {
949
+ text,
950
+ jobs,
951
+ offsets: Object.fromEntries(jobs.map((job) => [job, {
952
+ column: at(job).column - origin.column,
953
+ row: at(job).row - origin.row
954
+ }]))
955
+ };
956
+ return text;
957
+ };
958
+ const paste = (text, pointer) => {
959
+ const kind = snippetKind(text);
960
+ const grid = gridRef.current;
961
+ const memo = copied?.text === text ? copied : null;
962
+ if (kind === "jobs") {
963
+ const layout = materialize();
964
+ let to;
965
+ const slot = pointer && grid ? slotAt(grid, pointer, null) : null;
966
+ if (slot?.kind === "slot") to = slot.slot;
967
+ else if (memo?.jobs?.every((job) => layoutPosition(layout, job))) {
968
+ const first = layoutPosition(layout, memo.jobs[0] ?? "");
969
+ const bottom = Math.max(...memo.jobs.map((job) => layoutPosition(layout, job)?.row ?? 0));
970
+ to = first ? {
971
+ column: first.column,
972
+ row: bottom + 1
973
+ } : void 0;
974
+ }
975
+ const result = edit({
976
+ type: "pasteJobs",
977
+ yaml: text,
978
+ ...to ? {
979
+ to,
980
+ layout
981
+ } : {},
982
+ ...memo?.offsets ? { offsets: memo.offsets } : {}
983
+ });
984
+ if (result?.createdJobs) store.set({
985
+ selection: result.createdJobs,
986
+ steps: NO_STEPS
987
+ });
988
+ return true;
989
+ }
990
+ if (kind === "steps") {
991
+ const hit = pointer && grid ? document.elementFromPoint(pointer.x, pointer.y) : null;
992
+ const at = pointer && grid ? stepDropAt(grid, pointer, hit, stepCount) : null;
993
+ const last = memo?.steps?.[memo.steps.length - 1];
994
+ const target = at ?? (last && last.index < stepCount(last.job) ? {
995
+ job: last.job,
996
+ index: last.index + 1
997
+ } : null);
998
+ if (target) {
999
+ const result = edit({
1000
+ type: "pasteSteps",
1001
+ yaml: text,
1002
+ job: target.job,
1003
+ index: target.index
1004
+ });
1005
+ if (result?.steps) store.set({
1006
+ steps: result.steps,
1007
+ selection: NO_SELECTION
1008
+ });
1009
+ }
1010
+ return true;
1011
+ }
1012
+ return false;
1013
+ };
1014
+ const nudge = (direction) => {
1015
+ const jobs = selected();
1016
+ if (jobs.length === 0) return false;
1017
+ edit({
1018
+ type: "nudge",
1019
+ jobs,
1020
+ direction,
1021
+ layout: materialize()
1022
+ });
1023
+ return true;
1024
+ };
1025
+ return {
1026
+ store,
1027
+ t,
1028
+ isMatrixJob: (job) => isMatrix(latest.current.source[job]),
1029
+ startDrag,
1030
+ openJobMenu,
1031
+ openStepMenu,
1032
+ openEdgeMenu,
1033
+ selectEdge,
1034
+ edgeInUse: (edge) => refsInUse(edgeRefs(edge)),
1035
+ startEdgeEnd,
1036
+ deleteSelection,
1037
+ editSelection,
1038
+ addJob: (matrix) => addJob({
1039
+ type: "addJob",
1040
+ ...matrix ? { matrix: true } : {}
1041
+ }),
1042
+ addStep,
1043
+ editJob,
1044
+ editStep,
1045
+ registerGrid: (grid) => {
1046
+ gridRef.current = grid;
1047
+ },
1048
+ grid: () => gridRef.current,
1049
+ registerView: (view) => {
1050
+ viewRef.current = view;
1051
+ },
1052
+ selected,
1053
+ toggleSelected: (job) => {
1054
+ const { selection, steps, edges } = store.get();
1055
+ if (steps.length > 0 || edges.length > 0) {
1056
+ store.set({
1057
+ selection: [job],
1058
+ steps: NO_STEPS,
1059
+ edges: NO_EDGES
1060
+ });
1061
+ return;
1062
+ }
1063
+ store.set({ selection: selection.includes(job) ? selection.filter((other) => other !== job) : [...selection, job] });
1064
+ },
1065
+ selectedSteps,
1066
+ toggleStep: (step) => {
1067
+ const { selection, steps, edges } = store.get();
1068
+ if (selection.length > 0 || edges.length > 0) {
1069
+ store.set({
1070
+ selection: NO_SELECTION,
1071
+ steps: [step],
1072
+ edges: NO_EDGES
1073
+ });
1074
+ return;
1075
+ }
1076
+ store.set({ steps: steps.some((ref) => sameStep(ref, step)) ? steps.filter((ref) => !sameStep(ref, step)) : [...steps, step] });
1077
+ },
1078
+ copySelection,
1079
+ paste,
1080
+ nudge,
1081
+ reveal: (target) => latest.current.editor?.onReveal?.(target),
1082
+ focus: ({ job, step }) => store.set(step === void 0 ? {
1083
+ selection: [job],
1084
+ steps: NO_STEPS,
1085
+ edges: NO_EDGES
1086
+ } : {
1087
+ selection: NO_SELECTION,
1088
+ steps: [{
1089
+ job,
1090
+ index: step
1091
+ }],
1092
+ edges: NO_EDGES
1093
+ })
1094
+ };
1095
+ }, [
1096
+ store,
1097
+ openMenu,
1098
+ t,
1099
+ editing
1100
+ ]);
1101
+ const problems = editor?.problems ?? NO_PROBLEMS;
1102
+ useEffect(() => {
1103
+ store.set({ problems });
1104
+ }, [store, problems]);
1105
+ usePressOutside(() => latest.current.container, () => {
1106
+ const { selection, steps, edges } = store.get();
1107
+ if (selection.length > 0 || steps.length > 0 || edges.length > 0) store.set({
1108
+ selection: NO_SELECTION,
1109
+ steps: NO_STEPS,
1110
+ edges: NO_EDGES
1111
+ });
1112
+ });
1113
+ const active = editor !== void 0;
1114
+ useEffect(() => {
1115
+ if (!container || !active) return;
1116
+ const onDown = (e) => {
1117
+ if (e.button !== 0 || !(e.target instanceof Element) || e.target.closest(NOT_EMPTY)) return;
1118
+ const start = {
1119
+ x: e.clientX,
1120
+ y: e.clientY
1121
+ };
1122
+ if (!e.shiftKey) {
1123
+ trackDrag(start, { onEnd: (_, dragged) => {
1124
+ if (!dragged) store.set({
1125
+ selection: NO_SELECTION,
1126
+ steps: NO_STEPS,
1127
+ edges: NO_EDGES
1128
+ });
1129
+ } });
1130
+ return;
1131
+ }
1132
+ e.preventDefault();
1133
+ const base = store.get();
1134
+ let lines = null;
1135
+ const move = (client) => {
1136
+ const rect = {
1137
+ left: Math.min(start.x, client.x),
1138
+ right: Math.max(start.x, client.x),
1139
+ top: Math.min(start.y, client.y),
1140
+ bottom: Math.max(start.y, client.y)
1141
+ };
1142
+ const touched = (selector) => [...container.querySelectorAll(selector)].filter((row) => overlaps(row.getBoundingClientRect(), rect));
1143
+ const wrapper = gridRef.current?.wrapper;
1144
+ const from = wrapper && toContent(wrapper, {
1145
+ x: rect.left,
1146
+ y: rect.top
1147
+ });
1148
+ const to = wrapper && toContent(wrapper, {
1149
+ x: rect.right,
1150
+ y: rect.bottom
1151
+ });
1152
+ lines ??= connectorLines(container);
1153
+ store.set({
1154
+ ...boxSelection(base, {
1155
+ jobs: touched("[data-dag-job]").map((row) => row.dataset["dagJob"] ?? ""),
1156
+ steps: touched("[data-dag-step-job]").map((row) => ({
1157
+ job: row.dataset["dagStepJob"] ?? "",
1158
+ index: Number(row.dataset["dagStep"])
1159
+ })),
1160
+ edges: lines.filter(({ points }) => lineInBox(points, rect)).map(({ edge }) => edge)
1161
+ }),
1162
+ marquee: from && to ? {
1163
+ left: from.x,
1164
+ top: from.y,
1165
+ right: to.x,
1166
+ bottom: to.y
1167
+ } : null
1168
+ });
1169
+ };
1170
+ const done = (dragged) => {
1171
+ if (dragged) store.set({ marquee: null });
1172
+ };
1173
+ trackDrag(start, {
1174
+ holdText: "press",
1175
+ onMove: move,
1176
+ onEnd: (_, dragged) => done(dragged),
1177
+ onCancel: done
1178
+ });
1179
+ };
1180
+ const onEscape = (e) => {
1181
+ if (e.key === "Escape" && !store.get().drag && !typing(e.target)) store.set({
1182
+ selection: NO_SELECTION,
1183
+ steps: NO_STEPS,
1184
+ edges: NO_EDGES
1185
+ });
1186
+ };
1187
+ container.addEventListener("pointerdown", onDown);
1188
+ container.addEventListener("keydown", onEscape);
1189
+ return () => {
1190
+ container.removeEventListener("pointerdown", onDown);
1191
+ container.removeEventListener("keydown", onEscape);
1192
+ };
1193
+ }, [
1194
+ container,
1195
+ active,
1196
+ store
1197
+ ]);
1198
+ const editable = editor !== void 0;
1199
+ useEffect(() => {
1200
+ if (!container || !editable) return;
1201
+ const mac = /Mac|iPhone|iPad/.test(navigator.platform);
1202
+ let pointer = null;
1203
+ let pending;
1204
+ const pasteCopied = () => {
1205
+ if (copied) api.paste(copied.text, pointer);
1206
+ };
1207
+ const onKey = (e) => {
1208
+ const busy = typing(e.target) || e.target instanceof Element && !!e.target.closest("[role=\"dialog\"], [role=\"menu\"]");
1209
+ const direction = ARROWS[e.key];
1210
+ if (direction && !busy && !e.metaKey && !e.ctrlKey && !e.altKey) {
1211
+ if (api.nudge(direction)) e.preventDefault();
1212
+ return;
1213
+ }
1214
+ if (e.key.toLowerCase() === "e" && !busy && !e.metaKey && !e.ctrlKey && !e.altKey && api.editSelection()) {
1215
+ e.preventDefault();
1216
+ return;
1217
+ }
1218
+ if ((e.key === "Delete" || e.key === "Backspace") && !busy && !e.metaKey && !e.ctrlKey && !e.altKey) {
1219
+ if (api.deleteSelection()) {
1220
+ e.preventDefault();
1221
+ requestAnimationFrame(() => {
1222
+ if (!container.contains(document.activeElement)) container.focus({ preventScroll: true });
1223
+ });
1224
+ }
1225
+ return;
1226
+ }
1227
+ if (!(e.metaKey || e.ctrlKey)) return;
1228
+ const key = e.key.toLowerCase();
1229
+ const native = mac ? e.metaKey : e.ctrlKey;
1230
+ if (key === "z" && !e.shiftKey) {
1231
+ e.preventDefault();
1232
+ forgetSteps(api);
1233
+ latest.current.editor?.onUndo();
1234
+ } else if (key === "z" && e.shiftKey || key === "y") {
1235
+ e.preventDefault();
1236
+ forgetSteps(api);
1237
+ latest.current.editor?.onRedo();
1238
+ } else if (busy) return;
1239
+ else if (key === "c" && !native) {
1240
+ const text = api.copySelection();
1241
+ if (text) {
1242
+ e.preventDefault();
1243
+ navigator.clipboard?.writeText(text).catch(() => {});
1244
+ }
1245
+ } else if (key === "v" && !native) {
1246
+ e.preventDefault();
1247
+ pasteCopied();
1248
+ } else if (key === "v") {
1249
+ window.clearTimeout(pending);
1250
+ pending = window.setTimeout(pasteCopied, 100);
1251
+ }
1252
+ };
1253
+ const ours = (e) => container.contains(document.activeElement) && !typing(e.target) && !typing(document.activeElement);
1254
+ const onCopy = (e) => {
1255
+ const text = ours(e) ? api.copySelection() : null;
1256
+ if (text) {
1257
+ e.preventDefault();
1258
+ e.clipboardData?.setData("text/plain", text);
1259
+ }
1260
+ };
1261
+ const onPaste = (e) => {
1262
+ window.clearTimeout(pending);
1263
+ if (!ours(e)) return;
1264
+ const text = e.clipboardData ? e.clipboardData.getData("text/plain") : copied?.text;
1265
+ if (text && api.paste(text, pointer)) e.preventDefault();
1266
+ };
1267
+ const onMove = (e) => {
1268
+ pointer = {
1269
+ x: e.clientX,
1270
+ y: e.clientY
1271
+ };
1272
+ };
1273
+ const onLeave = () => {
1274
+ pointer = null;
1275
+ };
1276
+ const onDown = (e) => {
1277
+ if (!typing(e.target)) container.focus({ preventScroll: true });
1278
+ };
1279
+ container.addEventListener("keydown", onKey);
1280
+ document.addEventListener("copy", onCopy);
1281
+ document.addEventListener("paste", onPaste);
1282
+ container.addEventListener("pointermove", onMove);
1283
+ container.addEventListener("pointerleave", onLeave);
1284
+ container.addEventListener("pointerdown", onDown);
1285
+ return () => {
1286
+ window.clearTimeout(pending);
1287
+ container.removeEventListener("keydown", onKey);
1288
+ document.removeEventListener("copy", onCopy);
1289
+ document.removeEventListener("paste", onPaste);
1290
+ container.removeEventListener("pointermove", onMove);
1291
+ container.removeEventListener("pointerleave", onLeave);
1292
+ container.removeEventListener("pointerdown", onDown);
1293
+ };
1294
+ }, [
1295
+ container,
1296
+ editable,
1297
+ api
1298
+ ]);
1299
+ return {
1300
+ api,
1301
+ overlays: /* @__PURE__ */ jsxs(Fragment$1, { children: [
1302
+ /* @__PURE__ */ jsx(EditorToolbar, {
1303
+ api,
1304
+ editor,
1305
+ layout
1306
+ }),
1307
+ /* @__PURE__ */ jsx(DragOverlays, {
1308
+ api,
1309
+ container,
1310
+ trashRef
1311
+ }),
1312
+ /* @__PURE__ */ jsx(EditorDialogs, {
1313
+ api,
1314
+ editor,
1315
+ jobs: source,
1316
+ inputs,
1317
+ workflow
1318
+ }),
1319
+ contextMenu
1320
+ ] })
1321
+ };
1322
+ }
1323
+ function graphShortcuts(t) {
1324
+ const key = t.shortcutKeys;
1325
+ const does = t.shortcutDoes;
1326
+ return [
1327
+ {
1328
+ title: t.shortcutGroups.select,
1329
+ shortcuts: [
1330
+ {
1331
+ combos: [[key.shift, key.click]],
1332
+ does: does.select
1333
+ },
1334
+ {
1335
+ combos: [[key.shift, key.drag]],
1336
+ does: does.box
1337
+ },
1338
+ {
1339
+ combos: [[key.click]],
1340
+ does: does.dependency
1341
+ },
1342
+ {
1343
+ combos: [["Tab"]],
1344
+ does: does.focus
1345
+ },
1346
+ {
1347
+ combos: [[key.shift, key.enter]],
1348
+ does: does.pickJob
1349
+ },
1350
+ {
1351
+ combos: [[key.enter]],
1352
+ does: does.pickDependency
1353
+ },
1354
+ {
1355
+ combos: [["Esc"]],
1356
+ does: does.escape
1357
+ }
1358
+ ]
1359
+ },
1360
+ {
1361
+ title: t.shortcutGroups.change,
1362
+ shortcuts: [
1363
+ {
1364
+ combos: [[key.dragJob]],
1365
+ does: does.move
1366
+ },
1367
+ {
1368
+ combos: [[key.dragCircle]],
1369
+ does: does.connect
1370
+ },
1371
+ {
1372
+ combos: [[key.dragButton]],
1373
+ does: does.addJob
1374
+ },
1375
+ {
1376
+ combos: [["E"]],
1377
+ does: does.edit
1378
+ },
1379
+ {
1380
+ combos: [[
1381
+ "←",
1382
+ "↑",
1383
+ "↓",
1384
+ "→"
1385
+ ]],
1386
+ does: does.nudge
1387
+ },
1388
+ {
1389
+ combos: [[key.delete]],
1390
+ does: does.remove
1391
+ },
1392
+ {
1393
+ combos: [[key.rightClick]],
1394
+ does: does.menu
1395
+ },
1396
+ {
1397
+ combos: [[MOD_KEY, "C"], [MOD_KEY, "V"]],
1398
+ does: does.copy
1399
+ },
1400
+ {
1401
+ combos: [[MOD_KEY, "Z"], [
1402
+ key.shift,
1403
+ MOD_KEY,
1404
+ "Z"
1405
+ ]],
1406
+ does: does.undo
1407
+ }
1408
+ ]
1409
+ },
1410
+ {
1411
+ title: t.shortcutGroups.view,
1412
+ shortcuts: [{
1413
+ combos: [[key.dragSpace]],
1414
+ does: does.pan
1415
+ }, {
1416
+ combos: [[key.scroll]],
1417
+ does: does.zoom
1418
+ }]
1419
+ }
1420
+ ];
1421
+ }
1422
+ function EditorToolbar({ api, editor, layout }) {
1423
+ const t = useGraphEditorStrings();
1424
+ const editing = useWorkflowEditing();
1425
+ const undoable = {
1426
+ ...editor,
1427
+ onUndo: () => {
1428
+ forgetSteps(api);
1429
+ editor.onUndo();
1430
+ },
1431
+ onRedo: () => {
1432
+ forgetSteps(api);
1433
+ editor.onRedo();
1434
+ }
1435
+ };
1436
+ const chip = (matrix) => {
1437
+ const label = matrix ? t.addMatrixJob : t.addJob;
1438
+ return /* @__PURE__ */ jsx(AddChip, {
1439
+ icon: matrix ? /* @__PURE__ */ jsx(BsGrid, { className: "h-3 w-3" }) : /* @__PURE__ */ jsx(FaPlus, { className: "h-3 w-3" }),
1440
+ label,
1441
+ hint: matrix ? t.addMatrixJobHint : t.addJobHint,
1442
+ className: EDITOR_HANDLE_CLASS,
1443
+ onPointerDown: (e) => api.startDrag(e, {
1444
+ kind: "new",
1445
+ matrix,
1446
+ label
1447
+ }),
1448
+ onClick: () => api.addJob(matrix)
1449
+ });
1450
+ };
1451
+ return /* @__PURE__ */ jsxs(EditorBar, {
1452
+ editor: undoable,
1453
+ groups: graphShortcuts(t),
1454
+ className: cx(EDITOR_HANDLE_CLASS, "absolute bottom-2 right-2 z-10 max-w-[calc(100%-1rem)]"),
1455
+ children: [
1456
+ chip(false),
1457
+ chip(true),
1458
+ /* @__PURE__ */ jsx(BarDivider, {}),
1459
+ /* @__PURE__ */ jsx(IconButton, {
1460
+ icon: /* @__PURE__ */ jsx(MdRefresh, { className: "h-4 w-4" }),
1461
+ label: t.resetLayout,
1462
+ size: "sm",
1463
+ variant: "ghost",
1464
+ disabled: !editing.hasLayout(layout),
1465
+ onClick: () => editor.onEdit({ type: "resetLayout" })
1466
+ })
1467
+ ]
1468
+ });
1469
+ }
1470
+ function DragOverlays({ api, container, trashRef }) {
1471
+ const t = useGraphEditorStrings();
1472
+ const drag = useUi(api, (state) => state.drag, null);
1473
+ if (!drag || !container) return null;
1474
+ const rect = container.getBoundingClientRect();
1475
+ const overTrash = drag.target?.kind === "trash";
1476
+ const problem = drag.target?.kind === "steps" ? drag.target.problem : void 0;
1477
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [drag.payload.kind === "jobs" && /* @__PURE__ */ jsxs("div", {
1478
+ ref: trashRef,
1479
+ className: cx("absolute bottom-2 left-1/2 z-20 flex -translate-x-1/2 items-center gap-2 rounded-lg border-2 border-dashed px-4 py-2 text-sm", overTrash ? "border-(--theme-error) bg-(--theme-error-muted) text-(--theme-error)" : "theme-border bg-(--theme-panel-bg) theme-muted-text"),
1480
+ children: [/* @__PURE__ */ jsx(BiSolidTrashAlt, { className: "h-4 w-4" }), t.dropToDelete]
1481
+ }), drag.payload.kind !== "connect" && /* @__PURE__ */ jsx(DragLabel, {
1482
+ at: {
1483
+ x: drag.client.x - rect.left,
1484
+ y: drag.client.y - rect.top
1485
+ },
1486
+ text: problem ?? drag.payload.label,
1487
+ refused: problem !== void 0
1488
+ })] });
1489
+ }
1490
+ function EditorDialogs({ api, editor, jobs, inputs, workflow }) {
1491
+ const editing = useWorkflowEditing();
1492
+ const dialog = useUi(api, (state) => state.dialog, null);
1493
+ const close = () => api.store.set({ dialog: null });
1494
+ const onEdit = (edit) => {
1495
+ forgetSteps(api);
1496
+ editor.onEdit(edit);
1497
+ };
1498
+ const openOnAdd = openOnAddOf(editor);
1499
+ if (!dialog) return null;
1500
+ const copy = dialog.addition ? asRecord(editing.loadYaml(dialog.addition.yml)) : null;
1501
+ const here = copy ? asRecord(copy["jobs"]) : jobs;
1502
+ const source = dialog.addition?.yml ?? editor.readSource?.();
1503
+ if (dialog.kind === "job") return Object.hasOwn(here, dialog.job) ? /* @__PURE__ */ jsx(JobDialog, {
1504
+ job: dialog.job,
1505
+ jobs: here,
1506
+ inputs,
1507
+ workflow: copy ?? workflow,
1508
+ source,
1509
+ addition: dialog.addition?.edit,
1510
+ view: editor.settingsView,
1511
+ onViewChange: editor.onSettingsViewChange,
1512
+ openOnAdd,
1513
+ onEdit,
1514
+ onClose: close
1515
+ }, dialog.job) : null;
1516
+ const steps = stepsOf(here[dialog.job]);
1517
+ return dialog.index < steps.length ? /* @__PURE__ */ jsx(StepDialog, {
1518
+ job: dialog.job,
1519
+ index: dialog.index,
1520
+ steps,
1521
+ inputs,
1522
+ workflow: copy ?? workflow,
1523
+ source,
1524
+ addition: dialog.addition?.edit,
1525
+ view: editor.settingsView,
1526
+ onViewChange: editor.onSettingsViewChange,
1527
+ openOnAdd,
1528
+ usesSuggestions: editor.usesSuggestions ?? [],
1529
+ onEdit,
1530
+ onClose: close
1531
+ }, `${dialog.job}:${dialog.index}`) : null;
1532
+ }
1533
+ /** The editor's state and overlays for a DependencyGraph, loaded only where a graph is edited. */
1534
+ function GraphEditorOverlays({ onApi, ...inputs }) {
1535
+ const { api, overlays } = useGraphEditorState(inputs);
1536
+ useEffect(() => {
1537
+ onApi(api);
1538
+ return () => onApi(null);
1539
+ }, [api, onApi]);
1540
+ return overlays;
1541
+ }
1542
+ //#endregion
1543
+ export { GraphEditorOverlays as default };