@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,1808 @@
1
+ import { useWorkflowActions, useWorkflowEditing } from "../components/Provider.js";
2
+ import { BiSolidTrashAlt } from "../icons/bi/BiSolidTrashAlt.js";
3
+ import { FaArrowDown } from "../icons/fa/FaArrowDown.js";
4
+ import { FaArrowUp } from "../icons/fa/FaArrowUp.js";
5
+ import { IoMdClose } from "../icons/io/IoMdClose.js";
6
+ import { withPositionKeys } from "../components/keys.js";
7
+ import { IconButton } from "../components/IconButton.js";
8
+ import { Input, fieldBoxClasses } from "../components/Input.js";
9
+ import { useGraphEditorStrings } from "./editorStrings.js";
10
+ import { asRecord } from "./records.js";
11
+ import { useLintContext, useLintEditing } from "../editor/lintContext.js";
12
+ import { JOB_YAML_PATH, STEP_YAML_PATH } from "../editor/settingsYaml.js";
13
+ import { FieldProblems, LintScope, ProblemList, useFieldProblems } from "./fieldProblems.js";
14
+ import { AddRowButton, ChoiceButtons, DialogShell, ENV_KEY, EXPRESSION, ExpressionToggle, FieldError, FieldLabel, KeyValueEditor, LabelledField, Section, SectionMemory, ToggleField, conditionText, countError, diffPatch, durationError, expressionError, flagError, flagOf, isEmptyPatch, isScalar, labelled, orUndefined, parseCondition, parseCount, parseJson, parseScalar, removeAt, rowsError, rowsFrom, rowsTo, sameValue, text, unchangedValue, updateAt, useSectionMemory } from "./editorFields.js";
15
+ import { ConditionField, FlagField, InputRefSelect, ScriptField, ValueOrInputField, readRef, refExpression, refSuggestions } from "./inputRefs.js";
16
+ import { expressionRefs } from "./expressionRefs.js";
17
+ import { NO_SCOPED, ViewSwitch, YamlPane, yamlProblem } from "./settingsViews.js";
18
+ import { useNewInputs } from "./InputDialog.js";
19
+ import { editErrorText } from "./refusalText.js";
20
+ import { UsesPicker, WithEditor, usesKind, withDraftFrom, withError, withValue } from "./stepWith.js";
21
+ import cx from "classnames";
22
+ import { useId, useMemo, useRef, useState } from "react";
23
+ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
24
+ //#region src/graph/GraphEditorDialogs.tsx
25
+ var SHELL = /^$|^(\/?([^/ ]+\/)*)?(bash|sh)( .*)?$/;
26
+ var OUTPUT_KEY = /^[a-zA-Z0-9_-]+$/;
27
+ var MATRIX_KEY = /^[a-zA-Z0-9_-]+$/;
28
+ var SSH_KEYS = [
29
+ "remoteHost",
30
+ "remoteUser",
31
+ "jumpNodeHost",
32
+ "jumpNodeUser",
33
+ "disconnect-timeout"
34
+ ];
35
+ /** What the job dialog writes; the schema coverage test holds these to the workflow schema. */
36
+ var JOB_FIELDS = [
37
+ "needs",
38
+ "if",
39
+ "ssh",
40
+ "runs-on",
41
+ "working-directory",
42
+ "timeout",
43
+ "env",
44
+ "outputs",
45
+ "strategy"
46
+ ];
47
+ var RUNS_ON_FIELDS = [
48
+ "environment",
49
+ "environment.cluster",
50
+ "environment.name",
51
+ "params",
52
+ "mode",
53
+ "targetId",
54
+ "environmentId",
55
+ "schedulingParams",
56
+ "newWorker",
57
+ "workerId"
58
+ ];
59
+ var STRATEGY_FIELDS = [
60
+ "matrix",
61
+ "matrix.include",
62
+ "matrix.exclude",
63
+ "fail-fast",
64
+ "max-parallel"
65
+ ];
66
+ /** What the step dialog writes. */
67
+ var STEP_FIELDS = [
68
+ "name",
69
+ "run",
70
+ "shell",
71
+ "uses",
72
+ "with",
73
+ "if",
74
+ "working-directory",
75
+ "id",
76
+ "timeout",
77
+ "ignore-errors",
78
+ "early-cancel",
79
+ "cleanup",
80
+ "retry",
81
+ "env",
82
+ "ssh"
83
+ ];
84
+ var RETRY_FIELDS = [
85
+ "max-retries",
86
+ "interval",
87
+ "timeout",
88
+ "clean-on-retry"
89
+ ];
90
+ var CLUSTER_TYPES = ["compute-clusters", "compute-resources"];
91
+ function sshFrom(value) {
92
+ const record = asRecord(value);
93
+ return Object.fromEntries(SSH_KEYS.map((key) => [key, text(record[key])]));
94
+ }
95
+ function sshTo(fields, original) {
96
+ const written = asRecord(original);
97
+ const entries = SSH_KEYS.flatMap((key) => {
98
+ const value = fields[key].trim();
99
+ return value ? [[key, unchangedValue(value, written[key]) ? written[key] : value]] : [];
100
+ });
101
+ return entries.length > 0 ? Object.fromEntries(entries) : void 0;
102
+ }
103
+ function sshError(fields, t) {
104
+ return SSH_KEYS.some((key) => fields[key].trim()) && !fields.remoteHost.trim() ? t.remoteHostRequired : void 0;
105
+ }
106
+ var TARGET_MODES = [
107
+ "login",
108
+ "environment",
109
+ "worker"
110
+ ];
111
+ var TARGET_KEYS = [
112
+ "mode",
113
+ "targetId",
114
+ "environmentId",
115
+ "schedulingParams",
116
+ "newWorker",
117
+ "workerId"
118
+ ];
119
+ function runsOnFrom(value) {
120
+ const record = asRecord(value);
121
+ const environment = asRecord(record["environment"]);
122
+ const newWorker = record["newWorker"];
123
+ return {
124
+ kind: TARGET_KEYS.some((key) => key in record) ? "target" : "name",
125
+ cluster: text(environment["cluster"]),
126
+ name: text(environment["name"]),
127
+ params: rowsFrom(record["params"], false),
128
+ mode: text(record["mode"]),
129
+ targetId: text(record["targetId"]),
130
+ environmentId: text(record["environmentId"]),
131
+ schedulingParams: rowsFrom(record["schedulingParams"], false),
132
+ newWorker: typeof newWorker === "boolean" ? newWorker : void 0,
133
+ workerId: text(record["workerId"])
134
+ };
135
+ }
136
+ function runsOnTo(fields) {
137
+ if (fields.kind === "target") {
138
+ const mode = fields.mode.trim();
139
+ const targetId = fields.targetId.trim();
140
+ const environmentId = fields.environmentId.trim();
141
+ const schedulingParams = rowsTo(fields.schedulingParams, parseScalar);
142
+ const workerId = fields.workerId.trim();
143
+ if (!mode && !targetId && !environmentId && !schedulingParams && fields.newWorker === void 0 && !workerId) return;
144
+ return {
145
+ mode,
146
+ ...targetId ? { targetId } : {},
147
+ ...environmentId ? { environmentId } : {},
148
+ ...schedulingParams ? { schedulingParams } : {},
149
+ ...fields.newWorker === void 0 ? {} : { newWorker: fields.newWorker },
150
+ ...workerId ? { workerId } : {}
151
+ };
152
+ }
153
+ const cluster = fields.cluster.trim();
154
+ const name = fields.name.trim();
155
+ const params = rowsTo(fields.params, parseScalar);
156
+ if (!cluster && !name && !params) return;
157
+ return {
158
+ environment: {
159
+ cluster,
160
+ name
161
+ },
162
+ ...params ? { params } : {}
163
+ };
164
+ }
165
+ var PARAM_KEY = /^[A-Za-z0-9_-]+$/;
166
+ function runsOnError(fields, sshUsed, t) {
167
+ if (!runsOnTo(fields)) return;
168
+ if (sshUsed) return t.runsOnWithSsh;
169
+ if (fields.kind === "target") {
170
+ if (!TARGET_MODES.includes(fields.mode.trim())) return t.runsOnNeedsMode;
171
+ return rowsError(fields.schedulingParams, PARAM_KEY, t.invalidKey, t);
172
+ }
173
+ if (!fields.cluster.trim() || !fields.name.trim()) return t.runsOnNeedsEnvironment;
174
+ return rowsError(fields.params, PARAM_KEY, t.invalidKey, t);
175
+ }
176
+ function sshHelp(key, t) {
177
+ return key === "disconnect-timeout" ? t.help.disconnectTimeout : t.help[key];
178
+ }
179
+ function sshLabel(key, t) {
180
+ return key === "disconnect-timeout" ? t.fields.disconnectTimeout : t.fields[key];
181
+ }
182
+ function SshEditor({ fields, onChange, error, source }) {
183
+ const t = useGraphEditorStrings();
184
+ const set = (key) => (value) => onChange({
185
+ ...fields,
186
+ [key]: value
187
+ });
188
+ return /* @__PURE__ */ jsxs(LintScope, {
189
+ at: "ssh",
190
+ children: [
191
+ /* @__PURE__ */ jsx(ValueOrInputField, {
192
+ label: t.fields.remoteHost,
193
+ yamlKey: "remoteHost",
194
+ description: t.help.remoteHost,
195
+ value: fields.remoteHost,
196
+ onChange: set("remoteHost"),
197
+ source,
198
+ types: CLUSTER_TYPES,
199
+ suffix: "ip"
200
+ }),
201
+ /* @__PURE__ */ jsx(ValueOrInputField, {
202
+ label: t.fields.remoteUser,
203
+ yamlKey: "remoteUser",
204
+ description: t.help.remoteUser,
205
+ value: fields.remoteUser,
206
+ onChange: set("remoteUser"),
207
+ source,
208
+ types: CLUSTER_TYPES,
209
+ suffix: "user"
210
+ }),
211
+ [
212
+ "jumpNodeHost",
213
+ "jumpNodeUser",
214
+ "disconnect-timeout"
215
+ ].map((key) => /* @__PURE__ */ jsx(Input, {
216
+ mono: true,
217
+ ...labelled(sshLabel(key, t), key),
218
+ description: sshHelp(key, t),
219
+ value: fields[key],
220
+ onChange: (e) => set(key)(e.target.value)
221
+ }, key)),
222
+ /* @__PURE__ */ jsx(FieldError, { message: error })
223
+ ]
224
+ });
225
+ }
226
+ /** A job's or step's env, each value checked against what it reads. */
227
+ function EnvSection({ description, rows, onChange, error, source }) {
228
+ const t = useGraphEditorStrings();
229
+ return /* @__PURE__ */ jsx(Section, {
230
+ title: t.sectionEnvironment,
231
+ description,
232
+ open: rows.length > 0,
233
+ alert: !!error,
234
+ children: /* @__PURE__ */ jsx(LintScope, {
235
+ at: "env",
236
+ children: /* @__PURE__ */ jsx(KeyValueEditor, {
237
+ lint: true,
238
+ rows,
239
+ onChange,
240
+ error,
241
+ valueSuggestions: refSuggestions(source)
242
+ })
243
+ })
244
+ });
245
+ }
246
+ var OUTPUT_REF = /^\$\{\{\s*needs\.([A-Za-z0-9_-]+)\.steps\.([A-Za-z0-9_-]+)\.outputs\.([A-Za-z0-9_-]+)\s*\}\}$/;
247
+ function outputRowsFrom(value, job) {
248
+ return Object.entries(asRecord(value)).map(([name, raw]) => {
249
+ const match = typeof raw === "string" ? OUTPUT_REF.exec(raw.trim()) : null;
250
+ return match && match[1] === job ? {
251
+ name,
252
+ step: match[2] ?? "",
253
+ output: match[3] ?? "",
254
+ original: String(raw)
255
+ } : {
256
+ name,
257
+ step: "",
258
+ output: "",
259
+ custom: text(raw)
260
+ };
261
+ });
262
+ }
263
+ function outputsTo(rows, job) {
264
+ if (rows.length === 0) return;
265
+ return Object.fromEntries(rows.map((row) => {
266
+ const written = `\${{ needs.${job}.steps.${row.step}.outputs.${row.output.trim()} }}`;
267
+ const same = row.original?.replace(/\s/g, "") === written.replace(/\s/g, "");
268
+ return [row.name.trim(), row.custom ?? (same ? row.original : written)];
269
+ }));
270
+ }
271
+ function outputsError(rows, t) {
272
+ const names = rows.map((row) => row.name.trim());
273
+ for (const [i, row] of rows.entries()) {
274
+ if (!OUTPUT_KEY.test(names[i] ?? "")) return t.invalidKey;
275
+ if (names.indexOf(names[i] ?? "") !== i) return t.duplicateKey;
276
+ if (row.custom !== void 0 && !row.custom.trim()) return t.required;
277
+ if (row.custom === void 0 && (!row.step || !OUTPUT_KEY.test(row.output.trim()))) return t.outputNeedsStep;
278
+ }
279
+ }
280
+ function OutputsEditor({ rows, onChange, stepIds, error }) {
281
+ const t = useGraphEditorStrings();
282
+ const update = (i, patch) => onChange(rows.map((row, j) => i === j ? {
283
+ ...row,
284
+ ...patch
285
+ } : row));
286
+ return /* @__PURE__ */ jsxs("div", {
287
+ className: "flex flex-col gap-2",
288
+ children: [
289
+ stepIds.length === 0 && /* @__PURE__ */ jsx("div", {
290
+ className: "text-xs theme-muted-text",
291
+ children: t.noStepIds
292
+ }),
293
+ withPositionKeys(rows).map(({ key, item: row }, i) => /* @__PURE__ */ jsxs("div", {
294
+ className: "flex items-center gap-2",
295
+ children: [
296
+ /* @__PURE__ */ jsx("div", {
297
+ className: "w-1/3",
298
+ children: /* @__PURE__ */ jsx(Input, {
299
+ mono: true,
300
+ "aria-label": t.outputName,
301
+ placeholder: t.outputName,
302
+ value: row.name,
303
+ onChange: (e) => update(i, { name: e.target.value })
304
+ })
305
+ }),
306
+ row.custom !== void 0 ? /* @__PURE__ */ jsx("div", {
307
+ className: "flex-1",
308
+ children: /* @__PURE__ */ jsx(Input, {
309
+ mono: true,
310
+ "aria-label": t.outputValue,
311
+ value: row.custom,
312
+ onChange: (e) => update(i, { custom: e.target.value })
313
+ })
314
+ }) : /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("div", {
315
+ className: "w-1/3",
316
+ children: /* @__PURE__ */ jsxs("select", {
317
+ "aria-label": t.outputStep,
318
+ value: row.step,
319
+ onChange: (e) => update(i, { step: e.target.value }),
320
+ className: fieldBoxClasses,
321
+ children: [/* @__PURE__ */ jsx("option", {
322
+ value: "",
323
+ disabled: true,
324
+ children: t.outputStep
325
+ }), stepIds.map((id) => /* @__PURE__ */ jsx("option", {
326
+ value: id,
327
+ children: id
328
+ }, id))]
329
+ })
330
+ }), /* @__PURE__ */ jsx("div", {
331
+ className: "flex-1",
332
+ children: /* @__PURE__ */ jsx(Input, {
333
+ mono: true,
334
+ "aria-label": t.outputKey,
335
+ placeholder: t.outputKey,
336
+ value: row.output,
337
+ onChange: (e) => update(i, { output: e.target.value })
338
+ })
339
+ })] }),
340
+ /* @__PURE__ */ jsx(ExpressionToggle, {
341
+ active: row.custom !== void 0,
342
+ onChange: (on) => onChange(rows.map((other, j) => j === i ? on ? {
343
+ name: other.name,
344
+ step: "",
345
+ output: "",
346
+ custom: ""
347
+ } : {
348
+ name: other.name,
349
+ step: stepIds[0] ?? "",
350
+ output: ""
351
+ } : other))
352
+ }),
353
+ /* @__PURE__ */ jsx(IconButton, {
354
+ icon: /* @__PURE__ */ jsx(IoMdClose, { className: "h-4 w-4" }),
355
+ label: t.removeRow,
356
+ variant: "ghost",
357
+ size: "sm",
358
+ onClick: () => onChange(removeAt(rows, i))
359
+ })
360
+ ]
361
+ }, key)),
362
+ /* @__PURE__ */ jsx(FieldError, { message: error }),
363
+ /* @__PURE__ */ jsx(AddRowButton, {
364
+ label: t.addRow,
365
+ onClick: () => onChange([...rows, {
366
+ name: "",
367
+ step: stepIds[0] ?? "",
368
+ output: ""
369
+ }])
370
+ })
371
+ ]
372
+ });
373
+ }
374
+ var matrixRowKeys = 0;
375
+ var matrixRow = (row) => ({
376
+ ...row,
377
+ key: ++matrixRowKeys
378
+ });
379
+ function matrixRowsFrom(matrix) {
380
+ return Object.entries(asRecord(matrix)).filter(([name]) => name !== "include" && name !== "exclude").map(([name, values]) => {
381
+ if (typeof values === "string") return matrixRow({
382
+ name,
383
+ values,
384
+ original: values,
385
+ initial: values
386
+ });
387
+ if (Array.isArray(values) && values.every((value) => isScalar(value) && !(typeof value === "string" && value.includes(",")))) {
388
+ const joined = values.join(", ");
389
+ return matrixRow({
390
+ name,
391
+ values: joined,
392
+ original: values,
393
+ initial: joined
394
+ });
395
+ }
396
+ const json = JSON.stringify(values);
397
+ return matrixRow({
398
+ name,
399
+ values: json,
400
+ nested: true,
401
+ original: values,
402
+ initial: json
403
+ });
404
+ });
405
+ }
406
+ function matrixValues(row) {
407
+ if (row.initial !== void 0 && row.values === row.initial) return row.original;
408
+ const trimmed = row.values.trim();
409
+ if (row.nested) return parseJson(trimmed).value;
410
+ if (EXPRESSION.test(trimmed)) return trimmed;
411
+ return trimmed.split(",").map((value) => value.trim()).filter(Boolean).map(parseScalar);
412
+ }
413
+ function matrixRowInvalid(row, names, i) {
414
+ const name = names[i] ?? "";
415
+ const values = matrixValues(row);
416
+ return !MATRIX_KEY.test(name) || name === "include" || name === "exclude" || names.indexOf(name) !== i || !(typeof values === "string" || Array.isArray(values)) || Array.isArray(values) && values.length === 0;
417
+ }
418
+ function entriesFrom(value) {
419
+ return Array.isArray(value) ? value.map((entry) => rowsFrom(entry)) : [];
420
+ }
421
+ function entriesTo(entries) {
422
+ return entries.length > 0 ? entries.map((rows) => rowsTo(rows, parseScalar) ?? {}) : void 0;
423
+ }
424
+ function entriesError(entries, t) {
425
+ for (const rows of entries) {
426
+ const error = rows.length === 0 ? t.invalidMatrixEntry : rowsError(rows, MATRIX_KEY, t.invalidKey, t);
427
+ if (error) return error;
428
+ }
429
+ }
430
+ /** Combinations to add or skip, as entries of keys and values or as one expression. */
431
+ function MatrixEntries({ label, yamlKey, description, entries, onChange, text, onText, error }) {
432
+ const t = useGraphEditorStrings();
433
+ return /* @__PURE__ */ jsxs("div", {
434
+ className: "flex flex-col gap-2",
435
+ children: [/* @__PURE__ */ jsx(FieldLabel, {
436
+ label,
437
+ yamlKey,
438
+ description,
439
+ actions: /* @__PURE__ */ jsx(ExpressionToggle, {
440
+ active: text !== void 0,
441
+ onChange: (on) => onText(on ? "" : void 0)
442
+ })
443
+ }), text !== void 0 ? /* @__PURE__ */ jsx(Input, {
444
+ mono: true,
445
+ "aria-label": label,
446
+ value: text,
447
+ error,
448
+ onChange: (e) => onText(e.target.value)
449
+ }) : /* @__PURE__ */ jsxs(Fragment$1, { children: [
450
+ withPositionKeys(entries).map(({ key, item: rows }, i) => /* @__PURE__ */ jsxs("div", {
451
+ className: "flex items-start gap-2",
452
+ children: [/* @__PURE__ */ jsx("div", {
453
+ className: "flex-1 rounded-md border theme-border p-2",
454
+ children: /* @__PURE__ */ jsx(KeyValueEditor, {
455
+ rows,
456
+ onChange: (next) => onChange(updateAt(entries, i, next)),
457
+ error: void 0
458
+ })
459
+ }), /* @__PURE__ */ jsx(IconButton, {
460
+ icon: /* @__PURE__ */ jsx(IoMdClose, { className: "h-4 w-4" }),
461
+ label: t.removeEntry,
462
+ variant: "ghost",
463
+ size: "sm",
464
+ onClick: () => onChange(removeAt(entries, i))
465
+ })]
466
+ }, key)),
467
+ /* @__PURE__ */ jsx(FieldError, { message: error }),
468
+ /* @__PURE__ */ jsx(AddRowButton, {
469
+ label: t.addEntry,
470
+ onClick: () => onChange([...entries, [{
471
+ key: "",
472
+ value: ""
473
+ }]])
474
+ })
475
+ ] })]
476
+ });
477
+ }
478
+ /** One matrix variable: values listed here, or the selections of a multi-select input. */
479
+ function MatrixVariable({ row, source, onChange, onRemove }) {
480
+ const t = useGraphEditorStrings();
481
+ const ref = readRef(row.values.trim());
482
+ const [fromInput, setFromInput] = useState(ref !== null);
483
+ const { create } = source;
484
+ return /* @__PURE__ */ jsxs("div", {
485
+ className: "flex items-start gap-2",
486
+ children: [/* @__PURE__ */ jsxs("div", {
487
+ className: "flex flex-1 flex-col gap-2 rounded-md border theme-border p-2",
488
+ children: [/* @__PURE__ */ jsxs("div", {
489
+ className: "flex items-center gap-2",
490
+ children: [/* @__PURE__ */ jsx("div", {
491
+ className: "w-1/3",
492
+ children: /* @__PURE__ */ jsx(Input, {
493
+ mono: true,
494
+ "aria-label": t.matrixVariable,
495
+ placeholder: t.matrixVariable,
496
+ value: row.name,
497
+ onChange: (e) => onChange({
498
+ ...row,
499
+ name: e.target.value
500
+ })
501
+ })
502
+ }), /* @__PURE__ */ jsx("div", {
503
+ className: "flex-1",
504
+ children: /* @__PURE__ */ jsx(ChoiceButtons, {
505
+ options: [{
506
+ value: "values",
507
+ label: t.matrixValuesListed
508
+ }, {
509
+ value: "input",
510
+ label: t.modeFromInput
511
+ }],
512
+ value: fromInput ? "input" : "values",
513
+ onChange: (mode) => {
514
+ setFromInput(mode === "input");
515
+ onChange({
516
+ ...row,
517
+ values: "",
518
+ nested: false
519
+ });
520
+ }
521
+ })
522
+ })]
523
+ }), fromInput ? /* @__PURE__ */ jsx(InputRefSelect, {
524
+ refs: source.refs,
525
+ types: ["multi-dropdown", "checkbox-group"],
526
+ value: ref,
527
+ label: t.matrixValues,
528
+ onChange: (path) => onChange({
529
+ ...row,
530
+ values: refExpression(path)
531
+ }),
532
+ ...create ? {
533
+ onNew: () => create("multi-dropdown", (path) => onChange({
534
+ ...row,
535
+ values: refExpression(path)
536
+ })),
537
+ newLabel: t.newInput
538
+ } : {}
539
+ }) : /* @__PURE__ */ jsx(Input, {
540
+ mono: true,
541
+ "aria-label": t.matrixValues,
542
+ placeholder: t.matrixValues,
543
+ value: row.values,
544
+ title: row.nested ? t.jsonValue : void 0,
545
+ onChange: (e) => onChange({
546
+ ...row,
547
+ values: e.target.value
548
+ })
549
+ })]
550
+ }), /* @__PURE__ */ jsx(IconButton, {
551
+ icon: /* @__PURE__ */ jsx(IoMdClose, { className: "h-4 w-4" }),
552
+ label: t.removeVariable,
553
+ variant: "ghost",
554
+ size: "sm",
555
+ onClick: onRemove
556
+ })]
557
+ });
558
+ }
559
+ /** Every field of a job, saved as one undoable edit. */
560
+ function JobForm({ job, jobs, inputs, workflow, onEdit, onClose, renderShell, onDraft, pending = false }) {
561
+ const t = useGraphEditorStrings();
562
+ const editing = useWorkflowEditing();
563
+ const { needTarget } = editing;
564
+ const newInputs = useNewInputs(inputs);
565
+ const source = {
566
+ ...newInputs.source,
567
+ extras: expressionRefs(editing, workflow ?? { jobs }, job)
568
+ };
569
+ const [original] = useState(() => asRecord(jobs[job]));
570
+ const rawNeeds = original["needs"];
571
+ const needsExpression = typeof rawNeeds === "string" ? rawNeeds : void 0;
572
+ const strategy = asRecord(original["strategy"]);
573
+ const rawMatrix = strategy["matrix"];
574
+ const matrixRecord = asRecord(rawMatrix);
575
+ const [name, setName] = useState(job);
576
+ const [needs, setNeeds] = useState(() => Array.isArray(rawNeeds) ? rawNeeds.filter((n) => typeof n === "string") : []);
577
+ const [needsText, setNeedsText] = useState(needsExpression ?? "");
578
+ const [needsTyped, setNeedsTyped] = useState(needsExpression !== void 0);
579
+ const [condition, setCondition] = useState(conditionText(original["if"]));
580
+ const [ssh, setSsh] = useState(() => sshFrom(original["ssh"]));
581
+ const rawRunsOn = original["runs-on"];
582
+ const [runsOn, setRunsOn] = useState(() => runsOnFrom(rawRunsOn));
583
+ const [runsOnTyped, setRunsOnTyped] = useState(typeof rawRunsOn === "string");
584
+ const [runsOnText, setRunsOnText] = useState(typeof rawRunsOn === "string" ? rawRunsOn : "");
585
+ const [workingDirectory, setWorkingDirectory] = useState(text(original["working-directory"]));
586
+ const [timeout, setTimeoutText] = useState(text(original["timeout"]));
587
+ const [env, setEnv] = useState(() => rowsFrom(original["env"], false));
588
+ const [outputs, setOutputs] = useState(() => outputRowsFrom(original["outputs"], job));
589
+ const stepIds = (Array.isArray(original["steps"]) ? original["steps"] : []).flatMap((step) => {
590
+ const id = asRecord(step)["id"];
591
+ return typeof id === "string" && id ? [id] : [];
592
+ });
593
+ const [matrixOn, setMatrixOn] = useState(rawMatrix !== void 0);
594
+ const [matrixMode, setMatrixMode] = useState(typeof rawMatrix === "string" ? "expression" : "variables");
595
+ const [matrixText, setMatrixText] = useState(typeof rawMatrix === "string" ? rawMatrix : "");
596
+ const [matrixRows, setMatrixRows] = useState(() => matrixRowsFrom(rawMatrix));
597
+ const [include, setInclude] = useState(() => entriesFrom(matrixRecord["include"]));
598
+ const [exclude, setExclude] = useState(() => entriesFrom(matrixRecord["exclude"]));
599
+ const [includeText, setIncludeText] = useState(() => typeof matrixRecord["include"] === "string" ? matrixRecord["include"] : void 0);
600
+ const [excludeText, setExcludeText] = useState(() => typeof matrixRecord["exclude"] === "string" ? matrixRecord["exclude"] : void 0);
601
+ const [failFast, setFailFast] = useState(() => flagOf(strategy["fail-fast"]));
602
+ const [maxParallel, setMaxParallel] = useState(text(strategy["max-parallel"]));
603
+ const needsId = useId();
604
+ const needsLint = useFieldProblems("needs", needsId).map((problem) => problem.message).join(" ");
605
+ const others = Object.keys(jobs).filter((other) => other !== job);
606
+ const missing = needs.map(needTarget).filter((target) => !Object.hasOwn(jobs, target));
607
+ const allNeeds = editing.jobNeeds(jobs);
608
+ const isMatrixJob = (other) => asRecord(asRecord(jobs[other])["strategy"])["matrix"] !== void 0;
609
+ const toggleNeed = (other, on) => setNeeds((current) => on ? [...current, other] : current.filter((need) => needTarget(need) !== other));
610
+ const toggleAny = (other, any) => setNeeds((current) => current.map((need) => needTarget(need) === other ? any ? `${other}:any` : other : need));
611
+ const trimmedName = name.trim();
612
+ const matrixNames = matrixRows.map((row) => row.name.trim());
613
+ const matrixError = () => {
614
+ if (!matrixOn) return;
615
+ if (matrixMode === "expression") return matrixText.trim() ? expressionError(matrixText, t) : t.invalidExpressionValue;
616
+ if (matrixRows.length === 0 && include.length === 0 && includeText === void 0) return t.matrixNeedsVariable;
617
+ return matrixRows.some((row, i) => matrixRowInvalid(row, matrixNames, i)) ? t.matrixInvalidVariable : void 0;
618
+ };
619
+ const variablesMode = matrixOn && matrixMode === "variables";
620
+ const errors = {
621
+ name: !editing.isValidJobName(trimmedName) ? t.invalidJobName : trimmedName !== job && Object.hasOwn(jobs, trimmedName) ? t.jobExists : void 0,
622
+ needs: needsTyped ? expressionError(needsText, t) : void 0,
623
+ ssh: sshError(ssh, t),
624
+ runsOn: runsOnTyped ? !runsOnText.trim() ? void 0 : sshTo(ssh, original["ssh"]) ? t.runsOnWithSsh : expressionError(runsOnText, t) : runsOnError(runsOn, !!sshTo(ssh, original["ssh"]), t),
625
+ timeout: durationError(timeout, t),
626
+ env: rowsError(env, ENV_KEY, t.invalidEnvKey, t),
627
+ outputs: outputsError(outputs, t),
628
+ matrix: matrixError(),
629
+ include: !variablesMode ? void 0 : includeText !== void 0 ? expressionError(includeText, t) : entriesError(include, t),
630
+ exclude: !variablesMode ? void 0 : excludeText !== void 0 ? expressionError(excludeText, t) : entriesError(exclude, t),
631
+ failFast: matrixOn ? flagError(failFast, t) : void 0,
632
+ maxParallel: matrixOn ? countError(maxParallel, 1, t.invalidMaxParallel) : void 0
633
+ };
634
+ const nextStrategy = () => {
635
+ if (!matrixOn) return;
636
+ const includeList = includeText !== void 0 ? orUndefined(includeText) : entriesTo(include);
637
+ const excludeList = excludeText !== void 0 ? orUndefined(excludeText) : entriesTo(exclude);
638
+ const next = { matrix: matrixMode === "expression" ? matrixText.trim() : {
639
+ ...Object.fromEntries(matrixRows.map((row) => [row.name.trim(), matrixValues(row)])),
640
+ ...includeList ? { include: includeList } : {},
641
+ ...excludeList ? { exclude: excludeList } : {}
642
+ } };
643
+ if (failFast !== void 0) next["fail-fast"] = failFast;
644
+ const parallel = parseCount(maxParallel);
645
+ if (parallel !== void 0) next["max-parallel"] = parallel;
646
+ return next;
647
+ };
648
+ const patch = diffPatch(original, {
649
+ needs: needsTyped ? orUndefined(needsText) : needs.length > 0 ? needs : void 0,
650
+ if: parseCondition(condition),
651
+ ssh: sshTo(ssh, original["ssh"]),
652
+ "runs-on": runsOnTyped ? orUndefined(runsOnText) : runsOnTo(runsOn),
653
+ "working-directory": orUndefined(workingDirectory),
654
+ timeout: orUndefined(timeout),
655
+ env: rowsTo(env, (value) => value),
656
+ outputs: outputsTo(outputs, trimmedName),
657
+ strategy: nextStrategy()
658
+ });
659
+ const renamed = trimmedName !== job;
660
+ const dirty = renamed || !isEmptyPatch(patch);
661
+ const invalid = Object.values(errors).some(Boolean);
662
+ const addVariableRow = () => setMatrixRows((rows) => rows.length === 0 ? [matrixRow({
663
+ name: "value",
664
+ values: "1, 2"
665
+ })] : rows);
666
+ const saveEdit = newInputs.save(dirty ? {
667
+ type: "updateJob",
668
+ job,
669
+ ...renamed ? { name: trimmedName } : {},
670
+ ...patch
671
+ } : null);
672
+ onDraft?.(saveEdit, invalid);
673
+ const children = /* @__PURE__ */ jsxs(Fragment$1, { children: [
674
+ newInputs.dialog,
675
+ /* @__PURE__ */ jsx(Input, {
676
+ autoFocus: true,
677
+ mono: true,
678
+ label: t.jobName,
679
+ description: t.help.jobName,
680
+ value: name,
681
+ error: errors.name,
682
+ onChange: (e) => setName(e.target.value)
683
+ }),
684
+ /* @__PURE__ */ jsxs(Section, {
685
+ title: t.sectionDependencies,
686
+ open: true,
687
+ children: [
688
+ /* @__PURE__ */ jsx(FieldLabel, {
689
+ label: t.fields.needs,
690
+ yamlKey: "needs",
691
+ description: needsTyped ? t.help.needsExpression : t.help.needs,
692
+ actions: /* @__PURE__ */ jsx(ExpressionToggle, {
693
+ active: needsTyped,
694
+ onChange: setNeedsTyped
695
+ })
696
+ }),
697
+ needsTyped ? /* @__PURE__ */ jsx(Input, {
698
+ id: needsId,
699
+ mono: true,
700
+ "aria-label": t.fields.needs,
701
+ value: needsText,
702
+ error: errors.needs ?? (needsLint || void 0),
703
+ onChange: (e) => setNeedsText(e.target.value)
704
+ }) : others.length === 0 ? /* @__PURE__ */ jsx("div", {
705
+ className: "text-xs theme-muted-text",
706
+ children: t.noOtherJobs
707
+ }) : /* @__PURE__ */ jsxs("div", {
708
+ id: needsId,
709
+ tabIndex: -1,
710
+ className: "flex flex-col gap-1.5",
711
+ children: [[...others, ...missing].map((other) => {
712
+ const entry = needs.find((need) => needTarget(need) === other);
713
+ const cycle = !entry && editing.dependsOn(allNeeds, other, job);
714
+ return /* @__PURE__ */ jsxs("div", {
715
+ className: "flex items-center gap-2 text-sm",
716
+ children: [/* @__PURE__ */ jsxs("label", {
717
+ className: cx("flex flex-1 items-center gap-2", cycle ? "cursor-not-allowed opacity-50" : "cursor-pointer"),
718
+ title: cycle ? t.dependsOnThisJob : void 0,
719
+ children: [
720
+ /* @__PURE__ */ jsx("input", {
721
+ type: "checkbox",
722
+ checked: !!entry,
723
+ disabled: cycle,
724
+ onChange: (e) => toggleNeed(other, e.target.checked)
725
+ }),
726
+ /* @__PURE__ */ jsx("span", {
727
+ className: "font-mono",
728
+ children: other
729
+ }),
730
+ !Object.hasOwn(jobs, other) && /* @__PURE__ */ jsx("span", {
731
+ className: "text-xs text-(--theme-error)",
732
+ children: t.missingJob
733
+ })
734
+ ]
735
+ }), entry && isMatrixJob(other) && /* @__PURE__ */ jsxs("label", {
736
+ className: "flex cursor-pointer items-center gap-1.5 text-xs theme-muted-text",
737
+ title: t.help.waitForAny,
738
+ children: [/* @__PURE__ */ jsx("input", {
739
+ type: "checkbox",
740
+ checked: entry.endsWith(":any"),
741
+ onChange: (e) => toggleAny(other, e.target.checked)
742
+ }), t.waitForAny]
743
+ })]
744
+ }, other);
745
+ }), /* @__PURE__ */ jsx(FieldError, { message: needsLint || void 0 })]
746
+ }),
747
+ /* @__PURE__ */ jsx(ConditionField, {
748
+ scope: "job",
749
+ label: t.fields.condition,
750
+ yamlKey: "if",
751
+ description: t.help.jobIf,
752
+ value: condition,
753
+ onChange: setCondition,
754
+ source
755
+ })
756
+ ]
757
+ }),
758
+ /* @__PURE__ */ jsx(Section, {
759
+ title: t.sectionRemoteHost,
760
+ description: t.runsInWorkspace,
761
+ open: !!sshTo(ssh, original["ssh"]),
762
+ alert: !!errors.ssh,
763
+ children: /* @__PURE__ */ jsx(SshEditor, {
764
+ fields: ssh,
765
+ onChange: setSsh,
766
+ error: errors.ssh,
767
+ source
768
+ })
769
+ }),
770
+ /* @__PURE__ */ jsx(Section, {
771
+ title: t.sectionComputeEnvironment,
772
+ description: t.runsOnHint,
773
+ open: runsOnTyped ? !!runsOnText.trim() : !!runsOnTo(runsOn),
774
+ alert: !!errors.runsOn,
775
+ children: /* @__PURE__ */ jsxs(LintScope, {
776
+ at: "runs-on",
777
+ children: [/* @__PURE__ */ jsx(FieldLabel, {
778
+ label: t.fields.runsOn,
779
+ yamlKey: "runs-on",
780
+ description: t.help.runsOn,
781
+ actions: /* @__PURE__ */ jsx(ExpressionToggle, {
782
+ active: runsOnTyped,
783
+ onChange: setRunsOnTyped
784
+ })
785
+ }), runsOnTyped ? /* @__PURE__ */ jsx(Input, {
786
+ mono: true,
787
+ "aria-label": t.fields.runsOn,
788
+ value: runsOnText,
789
+ error: errors.runsOn,
790
+ onChange: (e) => setRunsOnText(e.target.value)
791
+ }) : /* @__PURE__ */ jsxs(Fragment$1, { children: [
792
+ /* @__PURE__ */ jsx(ChoiceButtons, {
793
+ options: [{
794
+ value: "name",
795
+ label: t.runsOnByName
796
+ }, {
797
+ value: "target",
798
+ label: t.runsOnByTarget
799
+ }],
800
+ value: runsOn.kind,
801
+ onChange: (kind) => setRunsOn({
802
+ ...runsOn,
803
+ kind
804
+ })
805
+ }),
806
+ runsOn.kind === "name" ? /* @__PURE__ */ jsxs(Fragment$1, { children: [
807
+ /* @__PURE__ */ jsx(Input, {
808
+ mono: true,
809
+ ...labelled(t.fields.environmentCluster, "cluster"),
810
+ description: t.help.environmentCluster,
811
+ value: runsOn.cluster,
812
+ onChange: (e) => setRunsOn({
813
+ ...runsOn,
814
+ cluster: e.target.value
815
+ })
816
+ }),
817
+ /* @__PURE__ */ jsx(Input, {
818
+ mono: true,
819
+ ...labelled(t.fields.environmentName, "name"),
820
+ description: t.help.environmentName,
821
+ value: runsOn.name,
822
+ onChange: (e) => setRunsOn({
823
+ ...runsOn,
824
+ name: e.target.value
825
+ })
826
+ }),
827
+ /* @__PURE__ */ jsx(LabelledField, {
828
+ label: t.fields.schedulingParams,
829
+ yamlKey: "params",
830
+ description: t.help.schedulingParams,
831
+ children: /* @__PURE__ */ jsx(KeyValueEditor, {
832
+ rows: runsOn.params,
833
+ onChange: (params) => setRunsOn({
834
+ ...runsOn,
835
+ params
836
+ }),
837
+ error: void 0
838
+ })
839
+ })
840
+ ] }) : /* @__PURE__ */ jsxs(Fragment$1, { children: [
841
+ /* @__PURE__ */ jsx(LabelledField, {
842
+ label: t.fields.targetMode,
843
+ yamlKey: "mode",
844
+ description: t.help.targetMode,
845
+ children: /* @__PURE__ */ jsx(ChoiceButtons, {
846
+ options: TARGET_MODES.map((mode) => ({
847
+ value: mode,
848
+ label: t.targetModes[mode]
849
+ })),
850
+ value: runsOn.mode,
851
+ onChange: (mode) => setRunsOn({
852
+ ...runsOn,
853
+ mode
854
+ })
855
+ })
856
+ }),
857
+ /* @__PURE__ */ jsx(Input, {
858
+ mono: true,
859
+ ...labelled(t.fields.targetId, "targetId"),
860
+ description: t.help.targetId,
861
+ value: runsOn.targetId,
862
+ onChange: (e) => setRunsOn({
863
+ ...runsOn,
864
+ targetId: e.target.value
865
+ })
866
+ }),
867
+ /* @__PURE__ */ jsx(Input, {
868
+ mono: true,
869
+ ...labelled(t.fields.environmentId, "environmentId"),
870
+ description: t.help.environmentId,
871
+ value: runsOn.environmentId,
872
+ onChange: (e) => setRunsOn({
873
+ ...runsOn,
874
+ environmentId: e.target.value
875
+ })
876
+ }),
877
+ /* @__PURE__ */ jsx(LabelledField, {
878
+ label: t.fields.schedulingParams,
879
+ yamlKey: "schedulingParams",
880
+ description: t.help.schedulingParams,
881
+ children: /* @__PURE__ */ jsx(KeyValueEditor, {
882
+ rows: runsOn.schedulingParams,
883
+ onChange: (schedulingParams) => setRunsOn({
884
+ ...runsOn,
885
+ schedulingParams
886
+ }),
887
+ error: void 0
888
+ })
889
+ }),
890
+ /* @__PURE__ */ jsx(ToggleField, {
891
+ label: t.fields.newWorker,
892
+ yamlKey: "newWorker",
893
+ description: t.help.newWorker,
894
+ checked: runsOn.newWorker === true,
895
+ onChange: (newWorker) => setRunsOn({
896
+ ...runsOn,
897
+ newWorker
898
+ })
899
+ }),
900
+ /* @__PURE__ */ jsx(Input, {
901
+ mono: true,
902
+ ...labelled(t.fields.workerId, "workerId"),
903
+ description: t.help.workerId,
904
+ value: runsOn.workerId,
905
+ onChange: (e) => setRunsOn({
906
+ ...runsOn,
907
+ workerId: e.target.value
908
+ })
909
+ })
910
+ ] }),
911
+ /* @__PURE__ */ jsx(FieldError, { message: errors.runsOn })
912
+ ] })]
913
+ })
914
+ }),
915
+ /* @__PURE__ */ jsxs(Section, {
916
+ title: t.sectionMatrix,
917
+ open: matrixOn,
918
+ alert: !!(errors.matrix || errors.include || errors.exclude || errors.failFast || errors.maxParallel),
919
+ children: [/* @__PURE__ */ jsx(ToggleField, {
920
+ label: t.fields.matrix,
921
+ yamlKey: "strategy.matrix",
922
+ description: t.matrixOn,
923
+ checked: matrixOn,
924
+ onChange: (on) => {
925
+ setMatrixOn(on);
926
+ if (on && matrixMode === "variables") addVariableRow();
927
+ }
928
+ }), matrixOn && /* @__PURE__ */ jsxs(Fragment$1, { children: [
929
+ typeof rawMatrix === "string" && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(FieldLabel, {
930
+ label: t.fields.matrixSource,
931
+ description: t.help.matrixMode
932
+ }), /* @__PURE__ */ jsx(ChoiceButtons, {
933
+ options: [{
934
+ value: "variables",
935
+ label: t.matrixFromVariables
936
+ }, {
937
+ value: "expression",
938
+ label: t.matrixFromExpression
939
+ }],
940
+ value: matrixMode,
941
+ onChange: (mode) => {
942
+ setMatrixMode(mode);
943
+ if (mode === "variables") addVariableRow();
944
+ }
945
+ })] }),
946
+ matrixMode === "expression" ? /* @__PURE__ */ jsx(Input, {
947
+ mono: true,
948
+ ...labelled(t.fields.matrixExpression, "strategy.matrix"),
949
+ description: t.help.matrixExpression,
950
+ value: matrixText,
951
+ error: errors.matrix,
952
+ onChange: (e) => setMatrixText(e.target.value)
953
+ }) : /* @__PURE__ */ jsxs(Fragment$1, { children: [
954
+ /* @__PURE__ */ jsx("div", {
955
+ className: "text-xs theme-muted-text",
956
+ children: t.help.matrixVariables
957
+ }),
958
+ matrixRows.map((row, i) => /* @__PURE__ */ jsx(MatrixVariable, {
959
+ row,
960
+ source,
961
+ onChange: (next) => setMatrixRows((rows) => updateAt(rows, i, next)),
962
+ onRemove: () => setMatrixRows((rows) => removeAt(rows, i))
963
+ }, row.key)),
964
+ /* @__PURE__ */ jsx(FieldError, { message: errors.matrix }),
965
+ /* @__PURE__ */ jsx(AddRowButton, {
966
+ label: t.addVariable,
967
+ onClick: () => setMatrixRows((rows) => [...rows, matrixRow({
968
+ name: "",
969
+ values: ""
970
+ })])
971
+ }),
972
+ /* @__PURE__ */ jsx(MatrixEntries, {
973
+ label: t.fields.include,
974
+ yamlKey: "include",
975
+ description: t.help.include,
976
+ entries: include,
977
+ onChange: setInclude,
978
+ text: includeText,
979
+ onText: setIncludeText,
980
+ error: errors.include
981
+ }),
982
+ /* @__PURE__ */ jsx(MatrixEntries, {
983
+ label: t.fields.exclude,
984
+ yamlKey: "exclude",
985
+ description: t.help.exclude,
986
+ entries: exclude,
987
+ onChange: setExclude,
988
+ text: excludeText,
989
+ onText: setExcludeText,
990
+ error: errors.exclude
991
+ })
992
+ ] }),
993
+ /* @__PURE__ */ jsx(FlagField, {
994
+ label: t.fields.failFast,
995
+ yamlKey: "fail-fast",
996
+ description: t.help.failFast,
997
+ value: failFast,
998
+ original: strategy["fail-fast"],
999
+ fallback: true,
1000
+ onChange: setFailFast,
1001
+ source
1002
+ }),
1003
+ /* @__PURE__ */ jsx(Input, {
1004
+ mono: true,
1005
+ ...labelled(t.fields.maxParallel, "max-parallel"),
1006
+ description: t.help.maxParallel,
1007
+ value: maxParallel,
1008
+ error: errors.maxParallel,
1009
+ onChange: (e) => setMaxParallel(e.target.value)
1010
+ })
1011
+ ] })]
1012
+ }),
1013
+ /* @__PURE__ */ jsx(EnvSection, {
1014
+ description: t.help.jobEnv,
1015
+ rows: env,
1016
+ onChange: setEnv,
1017
+ error: errors.env,
1018
+ source
1019
+ }),
1020
+ /* @__PURE__ */ jsx(Section, {
1021
+ title: t.sectionOutputs,
1022
+ description: t.help.outputs,
1023
+ open: outputs.length > 0,
1024
+ alert: !!errors.outputs,
1025
+ children: /* @__PURE__ */ jsx(OutputsEditor, {
1026
+ rows: outputs,
1027
+ onChange: setOutputs,
1028
+ stepIds,
1029
+ error: errors.outputs
1030
+ })
1031
+ }),
1032
+ /* @__PURE__ */ jsxs(Section, {
1033
+ title: t.sectionAdvanced,
1034
+ open: !!workingDirectory || !!timeout,
1035
+ alert: !!errors.timeout,
1036
+ children: [/* @__PURE__ */ jsx(Input, {
1037
+ mono: true,
1038
+ ...labelled(t.fields.workingDirectory, "working-directory"),
1039
+ description: t.help.jobWorkingDirectory,
1040
+ value: workingDirectory,
1041
+ onChange: (e) => setWorkingDirectory(e.target.value)
1042
+ }), /* @__PURE__ */ jsx(Input, {
1043
+ mono: true,
1044
+ ...labelled(t.fields.timeout, "timeout"),
1045
+ description: t.help.jobTimeout,
1046
+ value: timeout,
1047
+ error: errors.timeout,
1048
+ onChange: (e) => setTimeoutText(e.target.value)
1049
+ })]
1050
+ })
1051
+ ] });
1052
+ const shellProps = {
1053
+ title: rawMatrix !== void 0 ? t.editMatrix : t.editJob,
1054
+ onClose,
1055
+ dirty: dirty || pending,
1056
+ saveDisabled: invalid,
1057
+ locked: newInputs.dialog !== null,
1058
+ onSubmit: () => {
1059
+ if (saveEdit) onEdit(saveEdit);
1060
+ onClose();
1061
+ },
1062
+ children
1063
+ };
1064
+ return renderShell ? renderShell(shellProps) : /* @__PURE__ */ jsx(DialogShell, { ...shellProps });
1065
+ }
1066
+ /** Every field of a step, saved as one undoable edit. */
1067
+ function StepForm({ job, index, steps, inputs, workflow, usesSuggestions, onEdit, onClose, renderShell, onDraft, pending = false, adding = false }) {
1068
+ const t = useGraphEditorStrings();
1069
+ const editing = useWorkflowEditing();
1070
+ const actions = useWorkflowActions();
1071
+ const newInputs = useNewInputs(inputs);
1072
+ const source = {
1073
+ ...newInputs.source,
1074
+ extras: expressionRefs(editing, workflow, job)
1075
+ };
1076
+ const [original] = useState(() => asRecord(steps[index]));
1077
+ const rawRetry = original["retry"];
1078
+ const retry = asRecord(rawRetry);
1079
+ const [name, setName] = useState(text(original["name"]));
1080
+ const [kind, setKind] = useState(original["uses"] !== void 0 ? "uses" : "run");
1081
+ const [run, setRun] = useState(text(original["run"]));
1082
+ const [shell, setShell] = useState(text(original["shell"]));
1083
+ const [uses, setUses] = useState(text(original["uses"]));
1084
+ const [initialWith] = useState(() => withDraftFrom(original["with"], actions));
1085
+ const [withDraft, setWithDraft] = useState(initialWith);
1086
+ const [condition, setCondition] = useState(conditionText(original["if"]));
1087
+ const [workingDirectory, setWorkingDirectory] = useState(text(original["working-directory"]));
1088
+ const [id, setId] = useState(text(original["id"]));
1089
+ const [timeout, setTimeoutText] = useState(text(original["timeout"]));
1090
+ const [ignoreErrors, setIgnoreErrors] = useState(() => flagOf(original["ignore-errors"]));
1091
+ const [earlyCancel, setEarlyCancel] = useState(original["early-cancel"] === "any-job-failed");
1092
+ const [cleanup, setCleanup] = useState(text(original["cleanup"]));
1093
+ const [retryOn, setRetryOn] = useState(rawRetry !== void 0);
1094
+ const [maxRetries, setMaxRetries] = useState(text(retry["max-retries"]));
1095
+ const [interval, setIntervalText] = useState(text(retry["interval"]));
1096
+ const [retryTimeout, setRetryTimeout] = useState(text(retry["timeout"]));
1097
+ const [cleanOnRetry, setCleanOnRetry] = useState(() => flagOf(retry["clean-on-retry"]));
1098
+ const [env, setEnv] = useState(() => rowsFrom(original["env"], false));
1099
+ const [sshMode, setSshMode] = useState(original["ssh"] === null ? "none" : original["ssh"] !== void 0 ? "custom" : "inherit");
1100
+ const [ssh, setSsh] = useState(() => sshFrom(original["ssh"]));
1101
+ const trimmedUses = uses.trim();
1102
+ const errors = {
1103
+ run: kind === "run" && !run.trim() ? t.required : void 0,
1104
+ shell: kind === "run" && !SHELL.test(shell.trim()) ? t.invalidShell : void 0,
1105
+ uses: kind !== "uses" ? void 0 : !trimmedUses ? t.required : usesKind(trimmedUses, actions) === "other" ? t.invalidUses : void 0,
1106
+ with: kind === "uses" ? withError(trimmedUses, actions, withDraft, original["with"], t) : void 0,
1107
+ id: id.trim() && !editing.isValidJobName(id.trim()) ? t.invalidJobName : void 0,
1108
+ timeout: durationError(timeout, t),
1109
+ ignoreErrors: flagError(ignoreErrors, t),
1110
+ maxRetries: retryOn ? countError(maxRetries, 0, t.invalidCount) : void 0,
1111
+ interval: retryOn ? durationError(interval, t) : void 0,
1112
+ retryTimeout: retryOn ? durationError(retryTimeout, t) : void 0,
1113
+ cleanOnRetry: retryOn ? flagError(cleanOnRetry, t) : void 0,
1114
+ env: rowsError(env, ENV_KEY, t.invalidEnvKey, t),
1115
+ ssh: sshMode === "custom" ? sshError(ssh, t) : void 0
1116
+ };
1117
+ const nextRetry = () => {
1118
+ if (!retryOn) return;
1119
+ const next = {};
1120
+ const retries = parseCount(maxRetries);
1121
+ if (retries !== void 0) next["max-retries"] = retries;
1122
+ if (interval.trim()) next["interval"] = interval.trim();
1123
+ if (retryTimeout.trim()) next["timeout"] = retryTimeout.trim();
1124
+ if (cleanOnRetry !== void 0) next["clean-on-retry"] = cleanOnRetry;
1125
+ return next;
1126
+ };
1127
+ const patch = diffPatch(original, {
1128
+ name: orUndefined(name),
1129
+ run: kind === "run" ? run : void 0,
1130
+ shell: kind === "run" ? orUndefined(shell) : void 0,
1131
+ uses: kind === "uses" ? trimmedUses : void 0,
1132
+ with: kind === "uses" ? withValue(trimmedUses, actions, withDraft, initialWith, text(original["uses"]), original["with"]) : void 0,
1133
+ if: parseCondition(condition),
1134
+ "working-directory": original["working-directory"] === null && !workingDirectory.trim() ? null : orUndefined(workingDirectory),
1135
+ id: orUndefined(id),
1136
+ timeout: orUndefined(timeout),
1137
+ "ignore-errors": ignoreErrors,
1138
+ "early-cancel": earlyCancel ? "any-job-failed" : void 0,
1139
+ cleanup: cleanup.trim() ? cleanup : void 0,
1140
+ retry: nextRetry(),
1141
+ env: rowsTo(env, (value) => value),
1142
+ ssh: sshMode === "inherit" ? void 0 : sshMode === "none" ? null : sshTo(ssh, original["ssh"])
1143
+ });
1144
+ const dirty = !isEmptyPatch(patch);
1145
+ const invalid = Object.values(errors).some(Boolean);
1146
+ const edit = (graphEdit) => {
1147
+ onEdit(graphEdit);
1148
+ onClose();
1149
+ };
1150
+ const fromId = text(original["id"]).trim();
1151
+ const references = fromId && id.trim() && id.trim() !== fromId ? stepIdEdits(workflow, steps, job, index, fromId, id.trim()) : [];
1152
+ const saveEdit = newInputs.save(dirty ? editing.batchOf([{
1153
+ type: "updateStep",
1154
+ job,
1155
+ index,
1156
+ ...patch
1157
+ }, ...references]) : null);
1158
+ onDraft?.(saveEdit, invalid);
1159
+ const children = /* @__PURE__ */ jsxs(Fragment$1, { children: [
1160
+ newInputs.dialog,
1161
+ /* @__PURE__ */ jsx(Input, {
1162
+ autoFocus: true,
1163
+ ...labelled(t.stepName, "name"),
1164
+ description: t.help.stepName,
1165
+ value: name,
1166
+ onChange: (e) => setName(e.target.value)
1167
+ }),
1168
+ /* @__PURE__ */ jsx("div", {
1169
+ className: "text-xs theme-muted-text",
1170
+ children: t.help.stepKind
1171
+ }),
1172
+ /* @__PURE__ */ jsx(ChoiceButtons, {
1173
+ options: [{
1174
+ value: "run",
1175
+ label: t.stepRun
1176
+ }, {
1177
+ value: "uses",
1178
+ label: t.stepUses
1179
+ }],
1180
+ value: kind,
1181
+ onChange: setKind
1182
+ }),
1183
+ kind === "run" ? /* @__PURE__ */ jsx(ScriptField, {
1184
+ label: t.fields.run,
1185
+ yamlKey: "run",
1186
+ description: t.help.run,
1187
+ rows: 8,
1188
+ value: run,
1189
+ error: errors.run,
1190
+ placeholder: "echo \"Hello World\"",
1191
+ onChange: setRun,
1192
+ source
1193
+ }) : /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(UsesPicker, {
1194
+ uses: trimmedUses,
1195
+ onUses: setUses,
1196
+ choices: usesSuggestions,
1197
+ draft: withDraft,
1198
+ onDraft: setWithDraft,
1199
+ error: errors.uses
1200
+ }), /* @__PURE__ */ jsx(WithEditor, {
1201
+ uses: trimmedUses,
1202
+ draft: withDraft,
1203
+ original: original["with"],
1204
+ onChange: setWithDraft,
1205
+ error: errors.with,
1206
+ source
1207
+ })] }),
1208
+ /* @__PURE__ */ jsx(Section, {
1209
+ title: t.sectionCleanup,
1210
+ open: !!cleanup.trim(),
1211
+ children: /* @__PURE__ */ jsx(ScriptField, {
1212
+ label: t.fields.cleanup,
1213
+ yamlKey: "cleanup",
1214
+ description: t.help.cleanup,
1215
+ value: cleanup,
1216
+ onChange: setCleanup,
1217
+ source
1218
+ })
1219
+ }),
1220
+ /* @__PURE__ */ jsxs(Section, {
1221
+ title: t.sectionBehavior,
1222
+ open: !!condition || !!timeout || ignoreErrors !== void 0 || earlyCancel,
1223
+ alert: !!(errors.timeout || errors.ignoreErrors),
1224
+ children: [
1225
+ /* @__PURE__ */ jsx(ConditionField, {
1226
+ scope: "step",
1227
+ label: t.fields.condition,
1228
+ yamlKey: "if",
1229
+ description: t.help.stepIf,
1230
+ value: condition,
1231
+ onChange: setCondition,
1232
+ source
1233
+ }),
1234
+ /* @__PURE__ */ jsx(Input, {
1235
+ mono: true,
1236
+ ...labelled(t.fields.timeout, "timeout"),
1237
+ description: t.help.stepTimeout,
1238
+ value: timeout,
1239
+ error: errors.timeout,
1240
+ onChange: (e) => setTimeoutText(e.target.value)
1241
+ }),
1242
+ /* @__PURE__ */ jsx(FlagField, {
1243
+ label: t.fields.ignoreErrors,
1244
+ yamlKey: "ignore-errors",
1245
+ description: t.help.ignoreErrors,
1246
+ value: ignoreErrors,
1247
+ original: original["ignore-errors"],
1248
+ onChange: setIgnoreErrors,
1249
+ source
1250
+ }),
1251
+ /* @__PURE__ */ jsx(ToggleField, {
1252
+ label: t.fields.earlyCancel,
1253
+ yamlKey: "early-cancel",
1254
+ description: t.help.earlyCancel,
1255
+ checked: earlyCancel,
1256
+ onChange: setEarlyCancel
1257
+ })
1258
+ ]
1259
+ }),
1260
+ /* @__PURE__ */ jsxs(Section, {
1261
+ title: t.sectionRetry,
1262
+ open: retryOn,
1263
+ alert: !!(errors.maxRetries || errors.interval || errors.retryTimeout || errors.cleanOnRetry),
1264
+ children: [/* @__PURE__ */ jsx(ToggleField, {
1265
+ label: t.fields.retry,
1266
+ yamlKey: "retry",
1267
+ description: t.help.retry,
1268
+ checked: retryOn,
1269
+ onChange: setRetryOn
1270
+ }), retryOn && /* @__PURE__ */ jsxs(Fragment$1, { children: [
1271
+ /* @__PURE__ */ jsx(Input, {
1272
+ mono: true,
1273
+ ...labelled(t.fields.maxRetries, "max-retries"),
1274
+ description: t.help.maxRetries,
1275
+ value: maxRetries,
1276
+ error: errors.maxRetries,
1277
+ placeholder: "10",
1278
+ onChange: (e) => setMaxRetries(e.target.value)
1279
+ }),
1280
+ /* @__PURE__ */ jsx(Input, {
1281
+ mono: true,
1282
+ ...labelled(t.fields.retryInterval, "interval"),
1283
+ description: t.help.retryInterval,
1284
+ value: interval,
1285
+ error: errors.interval,
1286
+ placeholder: "5s",
1287
+ onChange: (e) => setIntervalText(e.target.value)
1288
+ }),
1289
+ /* @__PURE__ */ jsx(Input, {
1290
+ mono: true,
1291
+ ...labelled(t.fields.retryTimeout, "timeout"),
1292
+ description: t.help.retryTimeout,
1293
+ value: retryTimeout,
1294
+ error: errors.retryTimeout,
1295
+ placeholder: "30s",
1296
+ onChange: (e) => setRetryTimeout(e.target.value)
1297
+ }),
1298
+ /* @__PURE__ */ jsx(FlagField, {
1299
+ label: t.fields.cleanOnRetry,
1300
+ yamlKey: "clean-on-retry",
1301
+ description: t.help.cleanOnRetry,
1302
+ value: cleanOnRetry,
1303
+ original: retry["clean-on-retry"],
1304
+ fallback: true,
1305
+ onChange: setCleanOnRetry,
1306
+ source
1307
+ })
1308
+ ] })]
1309
+ }),
1310
+ /* @__PURE__ */ jsxs(Section, {
1311
+ title: t.sectionRemoteHost,
1312
+ description: t.help.stepSsh,
1313
+ open: sshMode !== "inherit",
1314
+ alert: !!errors.ssh,
1315
+ children: [/* @__PURE__ */ jsx(ChoiceButtons, {
1316
+ options: [
1317
+ {
1318
+ value: "inherit",
1319
+ label: t.sshInherit
1320
+ },
1321
+ {
1322
+ value: "custom",
1323
+ label: t.sshCustom
1324
+ },
1325
+ {
1326
+ value: "none",
1327
+ label: t.sshNone
1328
+ }
1329
+ ],
1330
+ value: sshMode,
1331
+ onChange: setSshMode
1332
+ }), sshMode === "custom" && /* @__PURE__ */ jsx(SshEditor, {
1333
+ fields: ssh,
1334
+ onChange: setSsh,
1335
+ error: errors.ssh,
1336
+ source
1337
+ })]
1338
+ }),
1339
+ /* @__PURE__ */ jsx(EnvSection, {
1340
+ description: t.help.stepEnv,
1341
+ rows: env,
1342
+ onChange: setEnv,
1343
+ error: errors.env,
1344
+ source
1345
+ }),
1346
+ /* @__PURE__ */ jsxs(Section, {
1347
+ title: t.sectionAdvanced,
1348
+ open: !!workingDirectory || !!id || !!shell,
1349
+ alert: !!(errors.id || errors.shell),
1350
+ children: [
1351
+ /* @__PURE__ */ jsx(Input, {
1352
+ mono: true,
1353
+ ...labelled(t.fields.workingDirectory, "working-directory"),
1354
+ description: t.help.stepWorkingDirectory,
1355
+ value: workingDirectory,
1356
+ onChange: (e) => setWorkingDirectory(e.target.value)
1357
+ }),
1358
+ /* @__PURE__ */ jsx(Input, {
1359
+ mono: true,
1360
+ ...labelled(t.fields.stepId, "id"),
1361
+ description: t.help.stepId,
1362
+ value: id,
1363
+ error: errors.id,
1364
+ onChange: (e) => setId(e.target.value)
1365
+ }),
1366
+ kind === "run" && /* @__PURE__ */ jsx(Input, {
1367
+ mono: true,
1368
+ ...labelled(t.fields.shell, "shell"),
1369
+ description: t.help.shell,
1370
+ value: shell,
1371
+ error: errors.shell,
1372
+ placeholder: "bash -e {0}",
1373
+ onChange: (e) => setShell(e.target.value)
1374
+ })
1375
+ ]
1376
+ })
1377
+ ] });
1378
+ const shellProps = {
1379
+ title: t.editStep,
1380
+ onClose,
1381
+ dirty: dirty || pending,
1382
+ saveDisabled: invalid,
1383
+ locked: newInputs.dialog !== null,
1384
+ onSubmit: () => {
1385
+ if (saveEdit) onEdit(saveEdit);
1386
+ onClose();
1387
+ },
1388
+ footerStart: /* @__PURE__ */ jsxs(Fragment$1, { children: [
1389
+ /* @__PURE__ */ jsx(IconButton, {
1390
+ icon: /* @__PURE__ */ jsx(BiSolidTrashAlt, { className: "h-4 w-4" }),
1391
+ label: t.deleteStep,
1392
+ variant: "ghost",
1393
+ size: "sm",
1394
+ onClick: () => adding ? onClose() : edit({
1395
+ type: "deleteStep",
1396
+ job,
1397
+ index
1398
+ })
1399
+ }),
1400
+ /* @__PURE__ */ jsx(IconButton, {
1401
+ icon: /* @__PURE__ */ jsx(FaArrowUp, { className: "h-4 w-4" }),
1402
+ label: t.moveUp,
1403
+ variant: "ghost",
1404
+ size: "sm",
1405
+ disabled: dirty || pending || index === 0,
1406
+ onClick: () => edit({
1407
+ type: "moveStep",
1408
+ job,
1409
+ from: index,
1410
+ to: index - 1
1411
+ })
1412
+ }),
1413
+ /* @__PURE__ */ jsx(IconButton, {
1414
+ icon: /* @__PURE__ */ jsx(FaArrowDown, { className: "h-4 w-4" }),
1415
+ label: t.moveDown,
1416
+ variant: "ghost",
1417
+ size: "sm",
1418
+ disabled: dirty || pending || index >= steps.length - 1,
1419
+ onClick: () => edit({
1420
+ type: "moveStep",
1421
+ job,
1422
+ from: index,
1423
+ to: index + 2
1424
+ })
1425
+ })
1426
+ ] }),
1427
+ children
1428
+ };
1429
+ return renderShell ? renderShell(shellProps) : /* @__PURE__ */ jsx(DialogShell, { ...shellProps });
1430
+ }
1431
+ function openingYaml(view, source, read) {
1432
+ if (view === "form" || source === void 0) return null;
1433
+ try {
1434
+ return read(source);
1435
+ } catch {
1436
+ return null;
1437
+ }
1438
+ }
1439
+ function stepIdEdits(workflow, steps, job, index, from, to) {
1440
+ const id = from.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1441
+ const own = new RegExp(`(?<![A-Za-z0-9_.-])steps\\.${id}(?![A-Za-z0-9_-])`, "g");
1442
+ const needed = new RegExp(`(?<![A-Za-z0-9_.-])needs\\.${job}\\.steps\\.${id}(?![A-Za-z0-9_-])`, "g");
1443
+ const rewrite = (value, inJob) => {
1444
+ if (typeof value === "string") return value.replace(/\$\{\{[\s\S]*?\}\}/g, (expression) => {
1445
+ const outside = expression.replace(needed, `needs.${job}.steps.${to}`);
1446
+ return inJob ? outside.replace(own, `steps.${to}`) : outside;
1447
+ });
1448
+ if (Array.isArray(value)) return value.map((item) => rewrite(item, inJob));
1449
+ return value && typeof value === "object" ? Object.fromEntries(Object.entries(value).map(([key, item]) => [key, rewrite(item, inJob)])) : value;
1450
+ };
1451
+ const changed = (record, inJob) => {
1452
+ const set = {};
1453
+ for (const [key, value] of Object.entries(record)) {
1454
+ const next = rewrite(value, inJob);
1455
+ if (!sameValue(next, value)) set[key] = next;
1456
+ }
1457
+ return set;
1458
+ };
1459
+ const jobs = workflow ? asRecord(workflow["jobs"]) : { [job]: { steps } };
1460
+ return Object.entries(jobs).flatMap(([name, value]) => {
1461
+ const { steps: own = [], ...fields } = asRecord(value);
1462
+ const inJob = name === job;
1463
+ const jobSet = changed(fields, inJob);
1464
+ return [...Object.keys(jobSet).length > 0 ? [{
1465
+ type: "updateJob",
1466
+ job: name,
1467
+ set: jobSet
1468
+ }] : [], ...(Array.isArray(own) ? own : []).flatMap((step, i) => {
1469
+ const set = inJob && i === index ? {} : changed(asRecord(step), inJob);
1470
+ return Object.keys(set).length > 0 ? [{
1471
+ type: "updateStep",
1472
+ job: name,
1473
+ index: i,
1474
+ set
1475
+ }] : [];
1476
+ })];
1477
+ });
1478
+ }
1479
+ function applyAll(editing, text, edits) {
1480
+ return edits.reduce((yml, edit) => editing.applyGraphEdit({
1481
+ yml,
1482
+ layout: void 0
1483
+ }, edit).yml, text);
1484
+ }
1485
+ function renamedJob(edit) {
1486
+ if (!edit) return;
1487
+ if (edit.type === "batch") return edit.edits.map(renamedJob).findLast(Boolean);
1488
+ return edit.type === "updateJob" ? edit.name : void 0;
1489
+ }
1490
+ /**
1491
+ * The linter's problems inside one job or step of `whole`, the workflow as the dialog has it,
1492
+ * with lines in `fragment`, the YAML the dialog shows, when it shows YAML.
1493
+ */
1494
+ function useScopedProblems(whole, prefix, fragment, exclude = []) {
1495
+ const ready = useLintEditing();
1496
+ const context = useLintContext(whole);
1497
+ const contextKey = JSON.stringify(context);
1498
+ const scope = prefix.join(".");
1499
+ return useMemo(() => {
1500
+ if (!ready || whole === void 0) return NO_SCOPED;
1501
+ const problems = ready.lintWorkflow(whole, context).flatMap((problem) => {
1502
+ const path = problem.path.split(".");
1503
+ if (!prefix.every((segment, i) => path[i] === segment) || exclude.includes(path[prefix.length] ?? "")) return [];
1504
+ return [{
1505
+ at: path.slice(prefix.length).map((segment) => /^\d+$/.test(segment) ? Number(segment) : segment),
1506
+ message: problem.message,
1507
+ fix: problem.fix
1508
+ }];
1509
+ });
1510
+ return {
1511
+ problems,
1512
+ markers: fragment === null ? [] : problems.map((problem) => ({
1513
+ message: problem.message,
1514
+ line: (problem.at.length > 0 ? ready.linesAt(fragment, problem.at)?.start : void 0) ?? 1
1515
+ }))
1516
+ };
1517
+ }, [
1518
+ ready,
1519
+ whole,
1520
+ contextKey,
1521
+ scope,
1522
+ fragment,
1523
+ exclude.join()
1524
+ ]);
1525
+ }
1526
+ /**
1527
+ * The input groups a `with:` completion in the YAML view adds to the workflow, saved with the
1528
+ * dialog ahead of the text that reads them.
1529
+ */
1530
+ function useAddedGroups(start, committed) {
1531
+ const editing = useWorkflowEditing();
1532
+ const [added, setAdded] = useState([]);
1533
+ const nested = {
1534
+ inputs: () => {
1535
+ const text = editedWorkflow(editing, start, committed).yml;
1536
+ const on = asRecord(asRecord(text === void 0 ? {} : editing.loadYaml(text))["on"]);
1537
+ return {
1538
+ ...asRecord(asRecord(on["execute"])["inputs"]),
1539
+ ...Object.fromEntries(added.map((group) => [group.name, group.definition]))
1540
+ };
1541
+ },
1542
+ addInputs: (name, definition) => setAdded((current) => [...current.filter((group) => group.name !== name), {
1543
+ name,
1544
+ definition
1545
+ }])
1546
+ };
1547
+ const edits = (text) => added.filter(({ name }) => text.includes(`inputs.${name}.`)).map(({ name, definition }) => ({
1548
+ type: "addInput",
1549
+ parent: [],
1550
+ index: 0,
1551
+ name,
1552
+ definition
1553
+ }));
1554
+ return {
1555
+ nested,
1556
+ edits,
1557
+ clear: () => setAdded([])
1558
+ };
1559
+ }
1560
+ function editedWorkflow(editing, start, edits) {
1561
+ if (start === void 0) return {
1562
+ yml: void 0,
1563
+ error: void 0
1564
+ };
1565
+ try {
1566
+ return {
1567
+ yml: applyAll(editing, start, edits),
1568
+ error: void 0
1569
+ };
1570
+ } catch (error) {
1571
+ return {
1572
+ yml: void 0,
1573
+ error
1574
+ };
1575
+ }
1576
+ }
1577
+ /** A dialog's settings in a form or as YAML, with the edits carried across a switch. */
1578
+ function useSettingsViews(o) {
1579
+ const t = useGraphEditorStrings();
1580
+ const editing = useWorkflowEditing();
1581
+ const sections = useSectionMemory();
1582
+ const [start] = useState(o.source);
1583
+ const [opening] = useState(() => openingYaml(o.view, o.source, (text) => o.read(text, o.name)));
1584
+ const [committed, setCommitted] = useState([]);
1585
+ const [name, setName] = useState(o.name);
1586
+ const [yamlName, setYamlName] = useState(o.name);
1587
+ const [yaml, setYaml] = useState(opening);
1588
+ const [yamlAtStart, setYamlAtStart] = useState(opening ?? "");
1589
+ const [problem, setProblem] = useState();
1590
+ const [version, setVersion] = useState(0);
1591
+ const draft = useRef(null);
1592
+ const invalidDraft = useRef(false);
1593
+ const [, setDrafts] = useState(0);
1594
+ const lastDraft = useRef("");
1595
+ const trackDraft = (edit, invalid) => {
1596
+ draft.current = edit;
1597
+ invalidDraft.current = invalid;
1598
+ const seen = JSON.stringify([edit, invalid]);
1599
+ if (seen !== lastDraft.current) {
1600
+ lastDraft.current = seen;
1601
+ window.setTimeout(() => {
1602
+ setDrafts((n) => n + 1);
1603
+ if (!invalid) setProblem(void 0);
1604
+ }, 0);
1605
+ }
1606
+ };
1607
+ const groups = useAddedGroups(start, committed);
1608
+ const drafted = () => draft.current ? [...committed, draft.current] : committed;
1609
+ const withYaml = (text) => text === yamlAtStart ? committed : [
1610
+ ...committed,
1611
+ ...groups.edits(text),
1612
+ o.write(name, text)
1613
+ ];
1614
+ const problemOf = (text) => o.optional && !text.trim() ? void 0 : yamlProblem(text, t, editing);
1615
+ const nextName = yamlName.trim();
1616
+ const nameError = o.rename?.error(nextName, name);
1617
+ const withRename = (edits) => o.rename && nextName !== name ? [...edits, o.rename.edit(name, nextName)] : edits;
1618
+ const edits = yaml !== null ? withYaml(yaml) : drafted();
1619
+ const editsKey = JSON.stringify(edits);
1620
+ const applied = useMemo(() => editedWorkflow(editing, start, edits), [
1621
+ editing,
1622
+ start,
1623
+ editsKey
1624
+ ]);
1625
+ const whole = applied.yml;
1626
+ const refused = yaml !== null && applied.error !== void 0 ? editErrorText(t, editing, applied.error) : void 0;
1627
+ const scopeName = yaml !== null ? name : o.rename?.from(draft.current) ?? name;
1628
+ const scoped = useScopedProblems(whole, o.scope(scopeName), yaml, o.exclude);
1629
+ const current = useMemo(() => start !== void 0 && committed.length > 0 ? asRecord(editing.loadYaml(applyAll(editing, start, committed))) : null, [
1630
+ editing,
1631
+ start,
1632
+ committed
1633
+ ]);
1634
+ const save = (edits) => {
1635
+ const edit = editing.batchOf(o.addition ? [o.addition, ...edits] : edits);
1636
+ if (edit) o.onEdit(edit);
1637
+ o.onClose();
1638
+ };
1639
+ const toYaml = () => {
1640
+ if (start === void 0) return;
1641
+ if (invalidDraft.current) {
1642
+ setProblem(t.fixFieldsFirst);
1643
+ return;
1644
+ }
1645
+ const edits = drafted();
1646
+ const renamed = o.rename?.from(draft.current) ?? name;
1647
+ try {
1648
+ const text = o.read(applyAll(editing, start, edits), renamed);
1649
+ setCommitted(edits);
1650
+ setName(renamed);
1651
+ setYamlName(renamed);
1652
+ setYaml(text);
1653
+ setYamlAtStart(text);
1654
+ setProblem(void 0);
1655
+ o.onViewChange?.("yaml");
1656
+ } catch (error) {
1657
+ setProblem(editErrorText(t, editing, error));
1658
+ }
1659
+ };
1660
+ const toForm = () => {
1661
+ if (yaml === null) return;
1662
+ const issue = problemOf(yaml) ?? nameError ?? refused;
1663
+ if (issue) {
1664
+ setProblem(issue);
1665
+ return;
1666
+ }
1667
+ setCommitted(withRename(withYaml(yaml)));
1668
+ setName(nextName);
1669
+ groups.clear();
1670
+ setYaml(null);
1671
+ setVersion((current) => current + 1);
1672
+ o.onViewChange?.("form");
1673
+ };
1674
+ const switcher = /* @__PURE__ */ jsx(ViewSwitch, {
1675
+ yaml: yaml !== null,
1676
+ onForm: toForm,
1677
+ onYaml: toYaml
1678
+ });
1679
+ const yamlDialog = (title, path, text) => /* @__PURE__ */ jsxs(DialogShell, {
1680
+ title,
1681
+ onClose: o.onClose,
1682
+ dirty: committed.length > 0 || text !== yamlAtStart || nextName !== name,
1683
+ saveDisabled: problemOf(text) !== void 0 || nameError !== void 0 || refused !== void 0,
1684
+ onSubmit: () => save(withRename(withYaml(text))),
1685
+ headerEnd: switcher,
1686
+ openOnAdd: o.openOnAdd,
1687
+ children: [o.rename && /* @__PURE__ */ jsx(Input, {
1688
+ mono: true,
1689
+ label: o.rename.label,
1690
+ description: o.rename.description,
1691
+ value: yamlName,
1692
+ error: nameError,
1693
+ onChange: (e) => setYamlName(e.target.value)
1694
+ }), /* @__PURE__ */ jsx(YamlPane, {
1695
+ path,
1696
+ nested: groups.nested,
1697
+ value: text,
1698
+ onChange: (next) => {
1699
+ setYaml(next);
1700
+ setProblem(void 0);
1701
+ },
1702
+ problem: problem ?? problemOf(text) ?? refused,
1703
+ scoped
1704
+ })]
1705
+ });
1706
+ const formDialog = (render) => /* @__PURE__ */ jsx(SectionMemory.Provider, {
1707
+ value: sections,
1708
+ children: /* @__PURE__ */ jsx(FieldProblems, {
1709
+ problems: scoped.problems,
1710
+ children: render({
1711
+ pending: committed.length > 0 || o.addition !== void 0,
1712
+ onDraft: trackDraft,
1713
+ renderShell: (shellProps) => /* @__PURE__ */ jsxs(DialogShell, {
1714
+ ...shellProps,
1715
+ headerEnd: switcher,
1716
+ openOnAdd: o.openOnAdd,
1717
+ onSubmit: () => save(drafted()),
1718
+ children: [
1719
+ /* @__PURE__ */ jsx(FieldError, { message: problem }),
1720
+ /* @__PURE__ */ jsx(ProblemList, { problems: scoped.problems }),
1721
+ shellProps.children
1722
+ ]
1723
+ })
1724
+ })
1725
+ })
1726
+ });
1727
+ return {
1728
+ start,
1729
+ yaml,
1730
+ name,
1731
+ version,
1732
+ current,
1733
+ yamlDialog,
1734
+ formDialog
1735
+ };
1736
+ }
1737
+ /** A job's settings in a form, or as YAML; switching carries the edits across. */
1738
+ function JobDialog(props) {
1739
+ const t = useGraphEditorStrings();
1740
+ const editing = useWorkflowEditing();
1741
+ const views = useSettingsViews({
1742
+ ...props,
1743
+ name: props.job,
1744
+ read: editing.jobYaml,
1745
+ write: (job, yaml) => ({
1746
+ type: "setJobYaml",
1747
+ job,
1748
+ yaml
1749
+ }),
1750
+ scope: (job) => ["jobs", job],
1751
+ rename: {
1752
+ label: t.jobName,
1753
+ description: t.help.jobName,
1754
+ error: (next, current) => !editing.isValidJobName(next) ? t.invalidJobName : next !== current && next !== props.job && Object.hasOwn(props.jobs, next) ? t.jobExists : void 0,
1755
+ edit: (job, name) => ({
1756
+ type: "updateJob",
1757
+ job,
1758
+ name
1759
+ }),
1760
+ from: renamedJob
1761
+ }
1762
+ });
1763
+ if (views.start === void 0) return /* @__PURE__ */ jsx(JobForm, { ...props });
1764
+ if (views.yaml !== null) return views.yamlDialog(t.editJob, JOB_YAML_PATH, views.yaml);
1765
+ const { current } = views;
1766
+ return views.formDialog((host) => /* @__PURE__ */ jsx(JobForm, {
1767
+ ...props,
1768
+ ...host,
1769
+ job: views.name,
1770
+ jobs: current ? asRecord(current["jobs"]) : props.jobs,
1771
+ workflow: current ?? props.workflow
1772
+ }, views.version));
1773
+ }
1774
+ /** A step's settings in a form, or as YAML; switching carries the edits across. */
1775
+ function StepDialog(props) {
1776
+ const { job, index } = props;
1777
+ const t = useGraphEditorStrings();
1778
+ const editing = useWorkflowEditing();
1779
+ const views = useSettingsViews({
1780
+ ...props,
1781
+ name: job,
1782
+ read: (text) => editing.stepYaml(text, job, index),
1783
+ write: (_, yaml) => ({
1784
+ type: "setStepYaml",
1785
+ job,
1786
+ index,
1787
+ yaml
1788
+ }),
1789
+ scope: () => [
1790
+ "jobs",
1791
+ job,
1792
+ "steps",
1793
+ String(index)
1794
+ ]
1795
+ });
1796
+ if (views.start === void 0) return /* @__PURE__ */ jsx(StepForm, { ...props });
1797
+ if (views.yaml !== null) return views.yamlDialog(t.editStep, STEP_YAML_PATH, views.yaml);
1798
+ const steps = asRecord(asRecord(views.current?.["jobs"])[job])["steps"];
1799
+ return views.formDialog((host) => /* @__PURE__ */ jsx(StepForm, {
1800
+ ...props,
1801
+ ...host,
1802
+ adding: props.addition !== void 0,
1803
+ steps: Array.isArray(steps) ? steps : props.steps,
1804
+ workflow: views.current ?? props.workflow
1805
+ }, views.version));
1806
+ }
1807
+ //#endregion
1808
+ export { JOB_FIELDS, JobDialog, RETRY_FIELDS, RUNS_ON_FIELDS, SSH_KEYS, STEP_FIELDS, STRATEGY_FIELDS, StepDialog, useSettingsViews };