@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.
- package/README.md +1 -1
- package/dist/ai/components/ChatInput.js +8 -23
- package/dist/ai/components/ChatSidebar.js +7 -4
- package/dist/ai/components/chatDraft.d.ts +7 -0
- package/dist/ai/components/chatDraft.js +66 -0
- package/dist/ai/index.d.ts +1 -1
- package/dist/ai/index.js +2 -2
- package/dist/ai/ui/sidebar.d.ts +6 -0
- package/dist/ai/ui/sidebar.js +15 -1
- package/dist/editing.d.ts +2 -0
- package/dist/editor/Monaco.js +1 -1
- package/dist/editor/index.js +1 -1
- package/dist/editor/lintContext.js +15 -3
- package/dist/form/FieldWrapper.js +1 -0
- package/dist/form/Form.js +64 -23
- package/dist/form/Wizard/WizardContainer.js +66 -6
- package/dist/form/Wizard/WizardStepContent.js +23 -10
- package/dist/form/Wizard/wizardPages.d.ts +12 -0
- package/dist/form/Wizard/wizardPages.js +5 -0
- package/dist/form/formEditing.d.ts +25 -0
- package/dist/form/formEditing.js +20 -0
- package/dist/graph/DependencyGraph.d.ts +8 -1
- package/dist/graph/DependencyGraph.js +433 -204
- package/dist/graph/GraphEditor.d.ts +27 -0
- package/dist/graph/GraphEditor.js +1543 -0
- package/dist/graph/GraphEditorDialogs.d.ts +87 -0
- package/dist/graph/GraphEditorDialogs.js +1808 -0
- package/dist/graph/InputDialog.js +1568 -54
- package/dist/graph/InputsEditor.d.ts +12 -0
- package/dist/graph/InputsEditor.js +1594 -0
- package/dist/graph/JobSummary.d.ts +3 -0
- package/dist/graph/JobSummary.js +34 -21
- package/dist/graph/MatrixGroup.d.ts +2 -1
- package/dist/graph/MatrixGroup.js +10 -3
- package/dist/graph/ShortcutsButton.js +56 -0
- package/dist/graph/SuggestionInput.js +29 -0
- package/dist/graph/WorkflowSettingsDialog.d.ts +21 -0
- package/dist/graph/WorkflowSettingsDialog.js +670 -0
- package/dist/graph/editorApi.js +8 -0
- package/dist/graph/editorChrome.js +75 -0
- package/dist/graph/editorFields.js +535 -0
- package/dist/graph/editorPrimitives.js +95 -4
- package/dist/graph/editorRows.d.ts +58 -0
- package/dist/graph/editorRows.js +460 -0
- package/dist/graph/editorStrings.d.ts +1 -0
- package/dist/graph/editorStrings.js +10 -1
- package/dist/graph/expressionRefs.js +39 -0
- package/dist/graph/fieldProblems.js +111 -0
- package/dist/graph/graphEditorState.d.ts +191 -0
- package/dist/graph/graphEditorState.js +74 -0
- package/dist/graph/index.d.ts +4 -0
- package/dist/graph/index.js +6 -2
- package/dist/graph/inputLayout.js +337 -0
- package/dist/graph/inputPages.d.ts +33 -0
- package/dist/graph/inputPages.js +186 -0
- package/dist/graph/inputRefs.js +388 -0
- package/dist/graph/inputRows.d.ts +43 -0
- package/dist/graph/inputRows.js +65 -0
- package/dist/graph/refusalText.js +13 -0
- package/dist/graph/settingsViews.js +62 -0
- package/dist/graph/stepWith.d.ts +38 -0
- package/dist/graph/stepWith.js +536 -0
- package/dist/styles.css +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
import { FaPlus } from "../icons/fa/FaPlus.js";
|
|
2
|
+
import { IconButton } from "../components/IconButton.js";
|
|
3
|
+
import { Textarea, fieldBoxClasses } from "../components/Input.js";
|
|
4
|
+
import { Toggle } from "../components/Toggle.js";
|
|
5
|
+
import { useGraphEditorStrings, useInputsEditorStrings } from "./editorStrings.js";
|
|
6
|
+
import { asRecord } from "./records.js";
|
|
7
|
+
import { useFieldLint } from "./fieldProblems.js";
|
|
8
|
+
import { SuggestionInput } from "./SuggestionInput.js";
|
|
9
|
+
import { ExpressionToggle, FieldError, FieldLabel, GROWING_AREA, LabelledField, flagError, flagValue, text, useGrowingArea } from "./editorFields.js";
|
|
10
|
+
import { useId, useRef, useState } from "react";
|
|
11
|
+
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
12
|
+
//#region src/graph/inputRefs.tsx
|
|
13
|
+
/** Every input with a value, where a run reads it: groups nest unless flattened, lists hide their rows. */
|
|
14
|
+
function inputRefs(editing, inputs) {
|
|
15
|
+
const root = asRecord(inputs);
|
|
16
|
+
const flatSteps = editing.wizardFlattens(root);
|
|
17
|
+
const out = [];
|
|
18
|
+
const walk = (map, prefix) => {
|
|
19
|
+
for (const [name, raw] of Object.entries(map)) {
|
|
20
|
+
if (name === "$meta") continue;
|
|
21
|
+
const definition = asRecord(raw);
|
|
22
|
+
const type = text(definition["type"]);
|
|
23
|
+
const childKey = editing.inputChildrenKey(type);
|
|
24
|
+
if (type === "group" || type === "step") {
|
|
25
|
+
const flat = type === "step" ? flatSteps : definition["flatten"] === true;
|
|
26
|
+
walk(asRecord(childKey ? definition[childKey] : void 0), flat ? prefix : [...prefix, name]);
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
if (type === "header") continue;
|
|
30
|
+
out.push({
|
|
31
|
+
path: [...prefix, name],
|
|
32
|
+
type,
|
|
33
|
+
label: text(definition["label"]) || name,
|
|
34
|
+
definition
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
walk(root, []);
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
41
|
+
var REF = /^\$\{\{\s*(!?)\s*inputs\.([A-Za-z0-9_.-]+?)\s*\}\}$/;
|
|
42
|
+
function refExpression(path, suffix) {
|
|
43
|
+
return `\${{ inputs.${[...path, ...suffix ? [suffix] : []].join(".")} }}`;
|
|
44
|
+
}
|
|
45
|
+
/** The input `value` reads when it is exactly `${{ inputs.PATH[.suffix] }}`. */
|
|
46
|
+
function readRef(value, suffix) {
|
|
47
|
+
const match = typeof value === "string" ? REF.exec(value.trim()) : null;
|
|
48
|
+
if (!match || match[1]) return null;
|
|
49
|
+
const parts = (match[2] ?? "").split(".");
|
|
50
|
+
if (suffix) return parts.at(-1) === suffix ? parts.slice(0, -1) : null;
|
|
51
|
+
return parts;
|
|
52
|
+
}
|
|
53
|
+
function flagExpression(path, negated) {
|
|
54
|
+
return `\${{ ${negated ? "!" : ""}inputs.${path.join(".")} }}`;
|
|
55
|
+
}
|
|
56
|
+
function equalsExpression(path, value, negated = false) {
|
|
57
|
+
const operator = negated ? "!=" : "==";
|
|
58
|
+
return `\${{ inputs.${path.join(".")} ${operator} '${value.replace(/'/g, "''")}' }}`;
|
|
59
|
+
}
|
|
60
|
+
var pathKey = (path) => path.join(".");
|
|
61
|
+
/** The option values a dropdown input offers, for conditions on it. */
|
|
62
|
+
function optionValues(definition) {
|
|
63
|
+
const options = definition["options"];
|
|
64
|
+
return Array.isArray(options) ? options.map((option) => typeof option === "object" && option !== null ? text(option["value"]) : text(option)) : [];
|
|
65
|
+
}
|
|
66
|
+
/** Picks an existing input of the given types, or asks for a new one. */
|
|
67
|
+
function InputRefSelect({ refs, types, value, onChange, onNew, newLabel, label }) {
|
|
68
|
+
const t = useGraphEditorStrings();
|
|
69
|
+
const id = useId();
|
|
70
|
+
const matching = refs.filter((ref) => types.includes(ref.type));
|
|
71
|
+
const current = value ? pathKey(value) : "";
|
|
72
|
+
const known = matching.some((ref) => pathKey(ref.path) === current);
|
|
73
|
+
return /* @__PURE__ */ jsxs("select", {
|
|
74
|
+
id,
|
|
75
|
+
"aria-label": label,
|
|
76
|
+
value: known || !current ? current : `?${current}`,
|
|
77
|
+
onChange: (e) => {
|
|
78
|
+
const next = e.target.value;
|
|
79
|
+
if (next === "+") onNew?.();
|
|
80
|
+
else if (next && !next.startsWith("?")) onChange(next.split("."));
|
|
81
|
+
},
|
|
82
|
+
className: fieldBoxClasses,
|
|
83
|
+
children: [
|
|
84
|
+
/* @__PURE__ */ jsx("option", {
|
|
85
|
+
value: "",
|
|
86
|
+
disabled: true,
|
|
87
|
+
children: matching.length > 0 ? t.chooseInput : t.noMatchingInputs
|
|
88
|
+
}),
|
|
89
|
+
!known && current && /* @__PURE__ */ jsxs("option", {
|
|
90
|
+
value: `?${current}`,
|
|
91
|
+
children: ["inputs.", current]
|
|
92
|
+
}),
|
|
93
|
+
matching.map((ref) => /* @__PURE__ */ jsx("option", {
|
|
94
|
+
value: pathKey(ref.path),
|
|
95
|
+
children: ref.label === ref.path.at(-1) ? `inputs.${pathKey(ref.path)}` : `${ref.label} (inputs.${pathKey(ref.path)})`
|
|
96
|
+
}, pathKey(ref.path))),
|
|
97
|
+
onNew && /* @__PURE__ */ jsx("option", {
|
|
98
|
+
value: "+",
|
|
99
|
+
children: newLabel ?? t.newInput
|
|
100
|
+
})
|
|
101
|
+
]
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
/** Makes a new input of `type` for the field, when the host can; `onCreated` gets where it went. */
|
|
105
|
+
function NewInputButton({ type, source, onCreated }) {
|
|
106
|
+
const t = useGraphEditorStrings();
|
|
107
|
+
const inputsEditor = useInputsEditorStrings();
|
|
108
|
+
const create = source?.create;
|
|
109
|
+
if (!create) return null;
|
|
110
|
+
const types = inputsEditor.types;
|
|
111
|
+
return /* @__PURE__ */ jsx(IconButton, {
|
|
112
|
+
icon: /* @__PURE__ */ jsx(FaPlus, { className: "h-4 w-4" }),
|
|
113
|
+
label: t.newInputOf(types[type] ?? type),
|
|
114
|
+
size: "sm",
|
|
115
|
+
variant: "ghost",
|
|
116
|
+
onClick: () => create(type, onCreated)
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
function insertAt(element, value, reference) {
|
|
120
|
+
const start = element?.selectionStart ?? value.length;
|
|
121
|
+
const end = element?.selectionEnd ?? value.length;
|
|
122
|
+
return value.slice(0, start) + reference + value.slice(end);
|
|
123
|
+
}
|
|
124
|
+
var ONE_OF = ["dropdown", "radio"];
|
|
125
|
+
/** A text field that takes any value, expressions included, with suggestions to pick from. */
|
|
126
|
+
function ExpressionInput({ id, label, value, onChange, suggestions = [], placeholder, error, describedBy, required }) {
|
|
127
|
+
return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(SuggestionInput, {
|
|
128
|
+
id,
|
|
129
|
+
ariaLabel: label,
|
|
130
|
+
"aria-describedby": describedBy,
|
|
131
|
+
required,
|
|
132
|
+
value,
|
|
133
|
+
placeholder,
|
|
134
|
+
suggestions,
|
|
135
|
+
onChange
|
|
136
|
+
}), /* @__PURE__ */ jsx(FieldError, { message: error })] });
|
|
137
|
+
}
|
|
138
|
+
/** Conditions on the form's switches and choices, for fields that run on one. */
|
|
139
|
+
function conditionSuggestions(source, t) {
|
|
140
|
+
return (source?.refs ?? []).flatMap((ref) => {
|
|
141
|
+
if (ref.type === "boolean") return [{
|
|
142
|
+
value: flagExpression(ref.path, false),
|
|
143
|
+
label: t.whenOn(ref.label)
|
|
144
|
+
}, {
|
|
145
|
+
value: flagExpression(ref.path, true),
|
|
146
|
+
label: t.whenOff(ref.label)
|
|
147
|
+
}];
|
|
148
|
+
return ONE_OF.includes(ref.type) ? optionValues(ref.definition).map((option) => ({
|
|
149
|
+
value: equalsExpression(ref.path, option),
|
|
150
|
+
label: t.whenIs(ref.label, option)
|
|
151
|
+
})) : [];
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
/** A switch, or an expression typed in its place, such as one that follows another input. */
|
|
155
|
+
function FlagField({ label, yamlKey, description, value, original, fallback = false, onChange, source }) {
|
|
156
|
+
const t = useGraphEditorStrings();
|
|
157
|
+
const id = useId();
|
|
158
|
+
const [typed, setTyped] = useState(typeof value === "string");
|
|
159
|
+
const checked = typeof value === "boolean" ? value : fallback;
|
|
160
|
+
const setTypedValue = (next) => onChange(next.trim() ? next : void 0);
|
|
161
|
+
const lint = useFieldLint(yamlKey, id, typeof value === "string" ? value : "", setTypedValue);
|
|
162
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
163
|
+
className: "flex flex-col gap-1.5",
|
|
164
|
+
children: [
|
|
165
|
+
/* @__PURE__ */ jsxs("div", {
|
|
166
|
+
className: "flex items-center justify-between gap-3",
|
|
167
|
+
children: [/* @__PURE__ */ jsx(FieldLabel, {
|
|
168
|
+
label,
|
|
169
|
+
yamlKey,
|
|
170
|
+
description
|
|
171
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
172
|
+
className: "flex shrink-0 items-center gap-1",
|
|
173
|
+
children: [!typed && /* @__PURE__ */ jsx(Toggle, {
|
|
174
|
+
checked,
|
|
175
|
+
onChange: (on) => onChange(flagValue(on, fallback, original)),
|
|
176
|
+
label
|
|
177
|
+
}), /* @__PURE__ */ jsx(ExpressionToggle, {
|
|
178
|
+
active: typed,
|
|
179
|
+
onChange: (on) => {
|
|
180
|
+
setTyped(on);
|
|
181
|
+
onChange(void 0);
|
|
182
|
+
}
|
|
183
|
+
})]
|
|
184
|
+
})]
|
|
185
|
+
}),
|
|
186
|
+
typed && /* @__PURE__ */ jsxs("div", {
|
|
187
|
+
className: "flex items-center gap-2",
|
|
188
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
189
|
+
className: "flex-1",
|
|
190
|
+
children: /* @__PURE__ */ jsx(ExpressionInput, {
|
|
191
|
+
id,
|
|
192
|
+
label,
|
|
193
|
+
value: typeof value === "string" ? value : "",
|
|
194
|
+
suggestions: conditionSuggestions(source, t),
|
|
195
|
+
error: flagError(value, t) ?? lint.message,
|
|
196
|
+
onChange: setTypedValue
|
|
197
|
+
})
|
|
198
|
+
}), /* @__PURE__ */ jsx(NewInputButton, {
|
|
199
|
+
type: "boolean",
|
|
200
|
+
source,
|
|
201
|
+
onCreated: (path) => onChange(flagExpression(path, false))
|
|
202
|
+
})]
|
|
203
|
+
}),
|
|
204
|
+
typed && lint.fixes
|
|
205
|
+
]
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
var CONDITIONS = [
|
|
209
|
+
["always", "always"],
|
|
210
|
+
["failure()", "failure"],
|
|
211
|
+
["cancelled()", "cancelled"],
|
|
212
|
+
["success()", "success"]
|
|
213
|
+
];
|
|
214
|
+
/** When a job or step runs: any condition, typed, with the executor's status checks and the form's switches offered. */
|
|
215
|
+
function ConditionField({ label, yamlKey, description, value, onChange, source, scope }) {
|
|
216
|
+
const t = useGraphEditorStrings();
|
|
217
|
+
const id = useId();
|
|
218
|
+
const texts = scope === "job" ? t.jobConditions : t.stepConditions;
|
|
219
|
+
const lint = useFieldLint(yamlKey, id, value, onChange);
|
|
220
|
+
const insert = (reference) => {
|
|
221
|
+
const element = document.getElementById(id);
|
|
222
|
+
onChange(insertAt(element instanceof HTMLInputElement && element.value === value ? element : null, value, reference));
|
|
223
|
+
};
|
|
224
|
+
return /* @__PURE__ */ jsxs(LabelledField, {
|
|
225
|
+
label,
|
|
226
|
+
yamlKey,
|
|
227
|
+
description,
|
|
228
|
+
htmlFor: id,
|
|
229
|
+
descriptionId: `${id}-help`,
|
|
230
|
+
actions: /* @__PURE__ */ jsxs(Fragment$1, { children: [allRefs(source).length > 0 && /* @__PURE__ */ jsx(InsertRefSelect, {
|
|
231
|
+
source,
|
|
232
|
+
label: t.insertValue,
|
|
233
|
+
onInsert: insert
|
|
234
|
+
}), /* @__PURE__ */ jsx(NewInputButton, {
|
|
235
|
+
type: "boolean",
|
|
236
|
+
source,
|
|
237
|
+
onCreated: (path) => onChange(flagExpression(path, false))
|
|
238
|
+
})] }),
|
|
239
|
+
children: [/* @__PURE__ */ jsx(ExpressionInput, {
|
|
240
|
+
id,
|
|
241
|
+
label,
|
|
242
|
+
describedBy: `${id}-help`,
|
|
243
|
+
value,
|
|
244
|
+
error: lint.message,
|
|
245
|
+
placeholder: texts.default,
|
|
246
|
+
suggestions: [
|
|
247
|
+
...CONDITIONS.map(([condition, key]) => ({
|
|
248
|
+
value: condition,
|
|
249
|
+
label: texts[key]
|
|
250
|
+
})),
|
|
251
|
+
{
|
|
252
|
+
value: "false",
|
|
253
|
+
label: t.conditionNever
|
|
254
|
+
},
|
|
255
|
+
...conditionSuggestions(source, t)
|
|
256
|
+
],
|
|
257
|
+
onChange
|
|
258
|
+
}), lint.fixes]
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
/** A value typed in, with the form's inputs of the given types offered, or a new one made for it. */
|
|
262
|
+
function ValueOrInputField({ label, yamlKey, description, value, onChange, source, types, suffix, literal = true, required = false, error, suggestions = [], placeholder }) {
|
|
263
|
+
const t = useGraphEditorStrings();
|
|
264
|
+
const id = useId();
|
|
265
|
+
const lint = useFieldLint(yamlKey, id, value, onChange);
|
|
266
|
+
const firstType = types[0] ?? "string";
|
|
267
|
+
const offered = [...source.refs.filter((ref) => types.includes(ref.type)).map((ref) => ({
|
|
268
|
+
value: refExpression(ref.path, suffix),
|
|
269
|
+
label: ref.label
|
|
270
|
+
})), ...literal ? suggestions.map((suggestion) => ({ value: suggestion })) : []];
|
|
271
|
+
return /* @__PURE__ */ jsxs(LabelledField, {
|
|
272
|
+
label: required ? `${label} *` : label,
|
|
273
|
+
yamlKey,
|
|
274
|
+
description,
|
|
275
|
+
descriptionId: `${id}-help`,
|
|
276
|
+
children: [
|
|
277
|
+
/* @__PURE__ */ jsxs("div", {
|
|
278
|
+
className: "flex items-center gap-2",
|
|
279
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
280
|
+
className: "flex-1",
|
|
281
|
+
children: /* @__PURE__ */ jsx(ExpressionInput, {
|
|
282
|
+
id,
|
|
283
|
+
label,
|
|
284
|
+
describedBy: `${id}-help`,
|
|
285
|
+
required,
|
|
286
|
+
value,
|
|
287
|
+
placeholder: placeholder ?? (literal ? void 0 : t.chooseInput),
|
|
288
|
+
suggestions: offered,
|
|
289
|
+
onChange
|
|
290
|
+
})
|
|
291
|
+
}), /* @__PURE__ */ jsx(NewInputButton, {
|
|
292
|
+
type: firstType,
|
|
293
|
+
source,
|
|
294
|
+
onCreated: (path) => onChange(refExpression(path, suffix))
|
|
295
|
+
})]
|
|
296
|
+
}),
|
|
297
|
+
/* @__PURE__ */ jsx(FieldError, { message: error ?? lint.message }),
|
|
298
|
+
lint.fixes
|
|
299
|
+
]
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
/** A script field with a menu that drops an input's reference at the cursor. */
|
|
303
|
+
function ScriptField({ label, yamlKey, description, value, onChange, source, rows, error, placeholder }) {
|
|
304
|
+
const t = useGraphEditorStrings();
|
|
305
|
+
const id = useId();
|
|
306
|
+
const growing = useGrowingArea(value);
|
|
307
|
+
const area = rows === void 0 ? growing : null;
|
|
308
|
+
const fixed = useRef(null);
|
|
309
|
+
const lint = useFieldLint(yamlKey, id, value, onChange);
|
|
310
|
+
const shownError = error ?? lint.message;
|
|
311
|
+
const insert = (reference) => onChange(insertAt((area ?? fixed).current, value, reference));
|
|
312
|
+
return /* @__PURE__ */ jsxs(LabelledField, {
|
|
313
|
+
label,
|
|
314
|
+
yamlKey,
|
|
315
|
+
description,
|
|
316
|
+
htmlFor: id,
|
|
317
|
+
descriptionId: `${id}-help`,
|
|
318
|
+
actions: allRefs(source).length > 0 && /* @__PURE__ */ jsx(InsertRefSelect, {
|
|
319
|
+
source,
|
|
320
|
+
label: t.insertValue,
|
|
321
|
+
onInsert: insert
|
|
322
|
+
}),
|
|
323
|
+
children: [/* @__PURE__ */ jsx(Textarea, {
|
|
324
|
+
ref: area ?? fixed,
|
|
325
|
+
id,
|
|
326
|
+
mono: true,
|
|
327
|
+
"aria-describedby": `${id}-help`,
|
|
328
|
+
rows: rows ?? 1,
|
|
329
|
+
value,
|
|
330
|
+
className: rows === void 0 ? GROWING_AREA : "",
|
|
331
|
+
...shownError ? { error: shownError } : {},
|
|
332
|
+
...placeholder ? { placeholder } : {},
|
|
333
|
+
onChange: (e) => onChange(e.target.value)
|
|
334
|
+
}), lint.fixes]
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
var GROUP_ORDER = [
|
|
338
|
+
"inputs",
|
|
339
|
+
"outputs",
|
|
340
|
+
"matrix",
|
|
341
|
+
"variables",
|
|
342
|
+
"sessions",
|
|
343
|
+
"env"
|
|
344
|
+
];
|
|
345
|
+
function InsertRefSelect({ source, label, onInsert }) {
|
|
346
|
+
const t = useGraphEditorStrings();
|
|
347
|
+
const all = allRefs(source);
|
|
348
|
+
return /* @__PURE__ */ jsxs("span", {
|
|
349
|
+
className: "relative inline-flex shrink-0 items-center",
|
|
350
|
+
children: [/* @__PURE__ */ jsx(FaPlus, {
|
|
351
|
+
"aria-hidden": "true",
|
|
352
|
+
className: "pointer-events-none absolute left-2 h-3 w-3 theme-muted-text"
|
|
353
|
+
}), /* @__PURE__ */ jsxs("select", {
|
|
354
|
+
"aria-label": label,
|
|
355
|
+
value: "",
|
|
356
|
+
onChange: (e) => {
|
|
357
|
+
if (e.target.value) onInsert(e.target.value);
|
|
358
|
+
},
|
|
359
|
+
className: "cursor-pointer appearance-none rounded-md border border-(--theme-border) bg-(--theme-panel-bg) py-1 pr-2 pl-6 text-xs text-(--theme-app) hover:bg-(--theme-muted-panel-bg)",
|
|
360
|
+
children: [/* @__PURE__ */ jsx("option", {
|
|
361
|
+
value: "",
|
|
362
|
+
children: label
|
|
363
|
+
}), GROUP_ORDER.map((group) => {
|
|
364
|
+
const refs = all.filter((item) => item.group === group);
|
|
365
|
+
return refs.length > 0 ? /* @__PURE__ */ jsx("optgroup", {
|
|
366
|
+
label: t.refGroups[group],
|
|
367
|
+
children: refs.map((item) => /* @__PURE__ */ jsx("option", {
|
|
368
|
+
value: item.expression,
|
|
369
|
+
children: item.label
|
|
370
|
+
}, item.label))
|
|
371
|
+
}, group) : null;
|
|
372
|
+
})]
|
|
373
|
+
})]
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
function allRefs(source) {
|
|
377
|
+
return [...source.refs.map((input) => ({
|
|
378
|
+
group: "inputs",
|
|
379
|
+
label: `inputs.${pathKey(input.path)}`,
|
|
380
|
+
expression: refExpression(input.path)
|
|
381
|
+
})), ...(source.extras ?? []).filter((extra) => extra.group !== "inputs")];
|
|
382
|
+
}
|
|
383
|
+
/** References offered as suggestions on free-text values. */
|
|
384
|
+
function refSuggestions(source) {
|
|
385
|
+
return allRefs(source).map((item) => item.expression);
|
|
386
|
+
}
|
|
387
|
+
//#endregion
|
|
388
|
+
export { ConditionField, ExpressionInput, FlagField, InputRefSelect, ScriptField, ValueOrInputField, equalsExpression, flagExpression, inputRefs, readRef, refExpression, refSuggestions };
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { InputPath } from '../editing.js';
|
|
2
|
+
/** One input's row as drawn, with its place among its list's inputs. */
|
|
3
|
+
export interface DrawnRow {
|
|
4
|
+
path: InputPath;
|
|
5
|
+
/** Which copy of the input this is, when a list draws it in each of its rows. */
|
|
6
|
+
instance: string;
|
|
7
|
+
index: number;
|
|
8
|
+
rect: DOMRect;
|
|
9
|
+
el: HTMLElement;
|
|
10
|
+
hidden: boolean;
|
|
11
|
+
/** A hidden input a list with widths draws after the inputs the form shows. */
|
|
12
|
+
trailing: boolean;
|
|
13
|
+
}
|
|
14
|
+
/** Inputs drawn one under another: an input and those `anchor-below` it, top to bottom. */
|
|
15
|
+
export interface Column {
|
|
16
|
+
rows: DrawnRow[];
|
|
17
|
+
/** The cell a list with widths draws the column in. */
|
|
18
|
+
cell: HTMLElement | null;
|
|
19
|
+
}
|
|
20
|
+
/** The columns of one list drawn side by side on one line of the form, left to right. */
|
|
21
|
+
export interface Line {
|
|
22
|
+
parent: InputPath;
|
|
23
|
+
columns: Column[];
|
|
24
|
+
}
|
|
25
|
+
/** Where dragged inputs land: before `index` of `parent`, as a column beside one, or in one. */
|
|
26
|
+
export interface Drop {
|
|
27
|
+
parent: InputPath;
|
|
28
|
+
index: number;
|
|
29
|
+
beside?: {
|
|
30
|
+
path: InputPath;
|
|
31
|
+
side: 'left' | 'right';
|
|
32
|
+
};
|
|
33
|
+
stack?: {
|
|
34
|
+
path: InputPath;
|
|
35
|
+
side: 'above' | 'below';
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
export declare const rowsOf: (line: Line) => DrawnRow[];
|
|
39
|
+
export declare function inputCell(el: HTMLElement): HTMLElement | null;
|
|
40
|
+
/** The form's lines: a column is the rows one cell draws, and columns at one height share a line. */
|
|
41
|
+
export declare function linesIn(root: HTMLElement): Line[];
|
|
42
|
+
/** Neighbours on one line, split at `pointer` (a percent of their list's width), in 5% steps. */
|
|
43
|
+
export declare function snapSplit(total: number, pointer: number): [left: number, right: number];
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
//#region src/graph/inputRows.ts
|
|
2
|
+
var key = (path) => JSON.stringify(path);
|
|
3
|
+
var samePath = (a, b) => a.length === b.length && a.every((name, i) => b[i] === name);
|
|
4
|
+
var rowsOf = (line) => line.columns.flatMap((column) => column.rows);
|
|
5
|
+
var topOf = (column) => Math.min(...column.rows.map((row) => row.rect.top));
|
|
6
|
+
var leftOf = (column) => Math.min(...column.rows.map((row) => row.rect.left));
|
|
7
|
+
function inputCell(el) {
|
|
8
|
+
let child = el;
|
|
9
|
+
for (let parent = el.parentElement; parent; parent = parent.parentElement) {
|
|
10
|
+
if (parent.hasAttribute("data-input-path")) return null;
|
|
11
|
+
if (parent.hasAttribute("data-input-cell")) return parent;
|
|
12
|
+
if (parent.parentElement?.hasAttribute("data-layout-grid")) return child;
|
|
13
|
+
if (parent.hasAttribute("data-layout-boundary")) return null;
|
|
14
|
+
child = parent;
|
|
15
|
+
}
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
/** The form's lines: a column is the rows one cell draws, and columns at one height share a line. */
|
|
19
|
+
function linesIn(root) {
|
|
20
|
+
const columns = /* @__PURE__ */ new Map();
|
|
21
|
+
for (const el of root.querySelectorAll("[data-input-path]")) {
|
|
22
|
+
const path = JSON.parse(el.dataset["inputPath"] ?? "[]");
|
|
23
|
+
const cell = inputCell(el);
|
|
24
|
+
const owner = cell ?? el;
|
|
25
|
+
const entry = columns.get(owner) ?? {
|
|
26
|
+
parent: path.slice(0, -1),
|
|
27
|
+
column: {
|
|
28
|
+
rows: [],
|
|
29
|
+
cell
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
columns.set(owner, entry);
|
|
33
|
+
entry.column.rows.push({
|
|
34
|
+
path,
|
|
35
|
+
instance: el.dataset["inputInstance"] ?? key(path),
|
|
36
|
+
index: Number(el.dataset["inputIndex"] ?? 0),
|
|
37
|
+
rect: el.getBoundingClientRect(),
|
|
38
|
+
el,
|
|
39
|
+
hidden: el.dataset["inputHidden"] !== void 0,
|
|
40
|
+
trailing: false
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
const lines = [];
|
|
44
|
+
for (const { parent, column } of columns.values()) {
|
|
45
|
+
column.rows.sort((a, b) => a.rect.top - b.rect.top);
|
|
46
|
+
if (column.cell && column.rows.every((row) => row.hidden)) for (const row of column.rows) row.trailing = true;
|
|
47
|
+
const top = topOf(column);
|
|
48
|
+
const line = lines.find((other) => samePath(other.parent, parent) && Math.abs(topOf(other.columns[0]) - top) < 1);
|
|
49
|
+
if (line) line.columns.push(column);
|
|
50
|
+
else lines.push({
|
|
51
|
+
parent,
|
|
52
|
+
columns: [column]
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
for (const line of lines) line.columns.sort((a, b) => leftOf(a) - leftOf(b));
|
|
56
|
+
return lines;
|
|
57
|
+
}
|
|
58
|
+
/** Neighbours on one line, split at `pointer` (a percent of their list's width), in 5% steps. */
|
|
59
|
+
function snapSplit(total, pointer) {
|
|
60
|
+
const floor = Math.min(10, total / 2);
|
|
61
|
+
const left = Math.min(Math.max(Math.round(pointer / 5) * 5, floor), total - floor);
|
|
62
|
+
return [left, total - left];
|
|
63
|
+
}
|
|
64
|
+
//#endregion
|
|
65
|
+
export { inputCell, linesIn, rowsOf, snapSplit };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { GRAPH_EDITOR_STRINGS } from "./editorStrings.js";
|
|
2
|
+
//#region src/graph/refusalText.ts
|
|
3
|
+
/** A refusal in the host's words, or the engine's English when the host gives none. */
|
|
4
|
+
function refusalText(t, editing, reason) {
|
|
5
|
+
return t.refusal === GRAPH_EDITOR_STRINGS.refusal ? editing.refusalMessage(reason) : t.refusal(reason);
|
|
6
|
+
}
|
|
7
|
+
/** What to tell the person when an edit throws: why it was refused, or the error itself. */
|
|
8
|
+
function editErrorText(t, editing, error) {
|
|
9
|
+
if (error instanceof editing.EditRefusal) return refusalText(t, editing, error.reason);
|
|
10
|
+
return error instanceof Error ? error.message : String(error);
|
|
11
|
+
}
|
|
12
|
+
//#endregion
|
|
13
|
+
export { editErrorText, refusalText };
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { TOOLTIP_ID } from "../components/Tooltip.js";
|
|
2
|
+
import Loader from "../components/Loader.js";
|
|
3
|
+
import { useGraphEditorStrings } from "./editorStrings.js";
|
|
4
|
+
import { ProblemList } from "./fieldProblems.js";
|
|
5
|
+
import { ChoiceButtons, FieldError } from "./editorFields.js";
|
|
6
|
+
import { Suspense, lazy } from "react";
|
|
7
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
8
|
+
//#region src/graph/settingsViews.tsx
|
|
9
|
+
var MonacoEditor = lazy(() => import("../editor/Monaco.js"));
|
|
10
|
+
function yamlProblem(yaml, t, editing) {
|
|
11
|
+
const problem = editing.settingsYamlProblem(yaml);
|
|
12
|
+
return !problem ? void 0 : problem.kind === "syntax" ? t.yamlSyntax(problem.message) : t.yamlNotMap;
|
|
13
|
+
}
|
|
14
|
+
function ViewSwitch({ yaml, onForm, onYaml }) {
|
|
15
|
+
const t = useGraphEditorStrings();
|
|
16
|
+
return /* @__PURE__ */ jsx("div", {
|
|
17
|
+
"data-tooltip-id": TOOLTIP_ID,
|
|
18
|
+
"data-tooltip-content": t.help.view,
|
|
19
|
+
children: /* @__PURE__ */ jsx(ChoiceButtons, {
|
|
20
|
+
options: [{
|
|
21
|
+
value: "yaml",
|
|
22
|
+
label: t.viewYaml
|
|
23
|
+
}, {
|
|
24
|
+
value: "form",
|
|
25
|
+
label: t.viewForm
|
|
26
|
+
}],
|
|
27
|
+
value: yaml ? "yaml" : "form",
|
|
28
|
+
onChange: (view) => view === "yaml" ? onYaml() : onForm()
|
|
29
|
+
})
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
var NO_SCOPED = {
|
|
33
|
+
markers: [],
|
|
34
|
+
problems: []
|
|
35
|
+
};
|
|
36
|
+
function YamlPane({ path, value, onChange, problem, scoped, nested }) {
|
|
37
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
38
|
+
className: "flex flex-col gap-1.5",
|
|
39
|
+
children: [
|
|
40
|
+
/* @__PURE__ */ jsx("div", {
|
|
41
|
+
className: "h-[50vh] overflow-hidden rounded-md border theme-border",
|
|
42
|
+
children: /* @__PURE__ */ jsx(Suspense, {
|
|
43
|
+
fallback: /* @__PURE__ */ jsx(Loader, {}),
|
|
44
|
+
children: /* @__PURE__ */ jsx(MonacoEditor, {
|
|
45
|
+
path,
|
|
46
|
+
language: "yaml",
|
|
47
|
+
value,
|
|
48
|
+
height: "100%",
|
|
49
|
+
scrollBeyondLastLine: false,
|
|
50
|
+
onChange,
|
|
51
|
+
markers: scoped.markers,
|
|
52
|
+
...nested ? { nested } : {}
|
|
53
|
+
})
|
|
54
|
+
})
|
|
55
|
+
}),
|
|
56
|
+
/* @__PURE__ */ jsx(FieldError, { message: problem }),
|
|
57
|
+
/* @__PURE__ */ jsx(ProblemList, { problems: scoped.problems })
|
|
58
|
+
]
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
//#endregion
|
|
62
|
+
export { NO_SCOPED, ViewSwitch, YamlPane, yamlProblem };
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { WorkflowAction } from '../editing.js';
|
|
2
|
+
import { type Row, type Strings } from './editorFields.js';
|
|
3
|
+
import { type InputSource } from './inputRefs.js';
|
|
4
|
+
/** The built-in actions a step can use, by `uses` value, as the host lists them. */
|
|
5
|
+
export type WorkflowActions = Record<string, WorkflowAction>;
|
|
6
|
+
/** Every `with` key the step dialog writes for `actions`, for a schema coverage test. */
|
|
7
|
+
export declare function withFields(actions: WorkflowActions): string[];
|
|
8
|
+
type UsesKind = 'action' | 'github' | 'gitlab' | 'subworkflow' | 'other';
|
|
9
|
+
export declare function usesKind(uses: string, actions: WorkflowActions): UsesKind;
|
|
10
|
+
export interface WithDraft {
|
|
11
|
+
values: Record<string, unknown>;
|
|
12
|
+
rows: Row[];
|
|
13
|
+
removed: string[];
|
|
14
|
+
}
|
|
15
|
+
export declare function withDraftFrom(value: unknown, actions: WorkflowActions): WithDraft;
|
|
16
|
+
/** The step's `with` for `uses`, or its original when nothing changed. */
|
|
17
|
+
export declare function withValue(uses: string, actions: WorkflowActions, draft: WithDraft, initial: WithDraft, originalUses: string, original: unknown): unknown;
|
|
18
|
+
export declare function withError(uses: string, actions: WorkflowActions, draft: WithDraft, original: unknown, t: Strings): string | undefined;
|
|
19
|
+
/** What a step uses: a built-in action, a workflow of yours, one from the marketplace, or one in a repository. */
|
|
20
|
+
export declare function UsesPicker({ uses, onUses, choices, draft, onDraft, error, }: {
|
|
21
|
+
uses: string;
|
|
22
|
+
onUses: (uses: string) => void;
|
|
23
|
+
/** `workflow/NAME` and `marketplace/SLUG` values to offer. */
|
|
24
|
+
choices: string[];
|
|
25
|
+
draft: WithDraft;
|
|
26
|
+
onDraft: (draft: WithDraft) => void;
|
|
27
|
+
error: string | undefined;
|
|
28
|
+
}): import("react").JSX.Element;
|
|
29
|
+
/** The step's `with`: an action's own inputs, or a subworkflow's by name. */
|
|
30
|
+
export declare function WithEditor({ uses, draft, original, onChange, error, source, }: {
|
|
31
|
+
uses: string;
|
|
32
|
+
draft: WithDraft;
|
|
33
|
+
original: unknown;
|
|
34
|
+
onChange: (draft: WithDraft) => void;
|
|
35
|
+
error: string | undefined;
|
|
36
|
+
source: InputSource;
|
|
37
|
+
}): import("react").JSX.Element;
|
|
38
|
+
export {};
|