@parallelworks/ui 0.36.0 → 0.38.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 +16 -0
- package/dist/ai/agent/permissions.d.ts +2 -2
- package/dist/ai/agent/permissions.js +3 -1
- package/dist/ai/components/ChatSidebar.js +7 -4
- package/dist/ai/components/agent/PermissionPicker.js +1 -0
- package/dist/ai/index.d.ts +1 -1
- package/dist/ai/index.js +2 -2
- package/dist/ai/strings.d.ts +2 -0
- package/dist/ai/strings.js +2 -0
- package/dist/ai/ui/sidebar.d.ts +6 -0
- package/dist/ai/ui/sidebar.js +15 -1
- package/dist/components/Dropdown.d.ts +2 -0
- package/dist/components/Dropdown.js +7 -2
- package/dist/components/Input.d.ts +11 -6
- package/dist/components/Input.js +93 -74
- package/dist/components/Provider.d.ts +31 -0
- package/dist/components/Provider.js +50 -2
- package/dist/editing.d.ts +404 -0
- package/dist/editor/EditorField.js +1 -1
- package/dist/editor/Monaco.d.ts +15 -1
- package/dist/editor/Monaco.js +117 -13
- package/dist/editor/editorModel.d.ts +5 -0
- package/dist/editor/editorModel.js +23 -0
- package/dist/editor/index.d.ts +4 -0
- package/dist/editor/index.js +5 -1
- package/dist/editor/lintContext.d.ts +21 -0
- package/dist/editor/lintContext.js +136 -0
- package/dist/editor/lintOwner.d.ts +2 -0
- package/dist/editor/lintOwner.js +5 -0
- package/dist/editor/nestedText.d.ts +13 -0
- package/dist/editor/nestedText.js +15 -0
- package/dist/editor/reveal.d.ts +4 -0
- package/dist/editor/reveal.js +15 -0
- package/dist/editor/settingsYaml.d.ts +12 -0
- package/dist/editor/settingsYaml.js +41 -0
- package/dist/form/FieldWrapper.js +1 -0
- package/dist/form/Fields/DropdownField.js +1 -1
- package/dist/form/Fields/DurationField.js +1 -1
- package/dist/form/Form.js +65 -24
- 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/fieldRegistry.js +1 -1
- package/dist/form/formEditing.d.ts +25 -0
- package/dist/form/formEditing.js +20 -0
- package/dist/form/index.js +5 -5
- package/dist/graph/InputDialog.d.ts +80 -0
- package/dist/graph/InputDialog.js +2110 -0
- package/dist/graph/InputsEditor.d.ts +12 -0
- package/dist/graph/InputsEditor.js +1594 -0
- package/dist/graph/ProblemsButton.d.ts +9 -0
- package/dist/graph/ProblemsButton.js +61 -0
- package/dist/graph/ShortcutsButton.d.ts +15 -0
- package/dist/graph/ShortcutsButton.js +56 -0
- package/dist/graph/SuggestionInput.d.ts +18 -0
- package/dist/graph/SuggestionInput.js +29 -0
- package/dist/graph/ToolbarPopover.d.ts +8 -0
- package/dist/graph/ToolbarPopover.js +71 -0
- package/dist/graph/editorApi.d.ts +51 -0
- package/dist/graph/editorApi.js +8 -0
- package/dist/graph/editorChrome.d.ts +27 -0
- package/dist/graph/editorChrome.js +75 -0
- package/dist/graph/editorFields.d.ts +180 -0
- package/dist/graph/editorFields.js +394 -0
- package/dist/graph/editorPrimitives.d.ts +60 -0
- package/dist/graph/editorPrimitives.js +150 -0
- package/dist/graph/editorStrings.d.ts +695 -0
- package/dist/graph/editorStrings.js +707 -0
- package/dist/graph/expressionRefs.d.ts +12 -0
- package/dist/graph/fieldProblems.d.ts +32 -0
- package/dist/graph/fieldProblems.js +77 -0
- package/dist/graph/index.d.ts +7 -0
- package/dist/graph/index.js +6 -1
- package/dist/graph/inputLayout.d.ts +25 -0
- 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.d.ts +103 -0
- package/dist/graph/inputRefs.js +196 -0
- package/dist/graph/inputRows.d.ts +43 -0
- package/dist/graph/inputRows.js +65 -0
- package/dist/graph/records.d.ts +3 -0
- package/dist/graph/records.js +7 -0
- package/dist/graph/refusalText.d.ts +6 -0
- package/dist/graph/settingsViews.d.ts +26 -0
- package/dist/graph/settingsViews.js +62 -0
- package/dist/graph/usesInputs.d.ts +17 -0
- package/dist/graph/usesInputs.js +40 -0
- package/dist/icons/bi/BiRedo.js +16 -0
- package/dist/icons/bi/BiUndo.js +16 -0
- package/dist/icons/lu/LuBraces.js +27 -0
- package/dist/icons/lu/LuCode.js +27 -0
- package/dist/icons/lu/LuKeyboard.js +71 -0
- package/dist/icons/lu/LuNetwork.js +63 -0
- package/dist/icons/lu/LuTextCursorInput.js +45 -0
- package/dist/icons.d.ts +2 -2
- package/dist/icons.js +8 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -2
- package/dist/list/ActionToolbar.d.ts +2 -0
- package/dist/list/ActionToolbar.js +18 -5
- package/dist/list/RowContextMenu.d.ts +10 -1
- package/dist/list/RowContextMenu.js +185 -30
- package/dist/styles.css +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { inputLayout } from "./inputLayout.js";
|
|
2
|
+
//#region src/graph/inputPages.ts
|
|
3
|
+
var asRecord = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
4
|
+
var isStep = (definition) => asRecord(definition)["type"] === "step";
|
|
5
|
+
var inputNames = (list) => Object.keys(list).filter((name) => !name.startsWith("$"));
|
|
6
|
+
/**
|
|
7
|
+
* The wizard settings a list split into pages gets: what it had, and otherwise pages that can be
|
|
8
|
+
* jumped between and, for the form itself, a Submit button.
|
|
9
|
+
*/
|
|
10
|
+
function wizardSettings(list, submitLabel) {
|
|
11
|
+
const wizard = asRecord(asRecord(asRecord(list)["$meta"])["wizard"]);
|
|
12
|
+
return {
|
|
13
|
+
...wizard,
|
|
14
|
+
mode: "wizard",
|
|
15
|
+
navigation: {
|
|
16
|
+
allowJump: true,
|
|
17
|
+
...asRecord(wizard["navigation"])
|
|
18
|
+
},
|
|
19
|
+
...submitLabel && wizard["submitLabel"] === void 0 ? { submitLabel } : {}
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* What splitting the form into pages adds besides its `$meta`: a wizard draws only its pages, so the
|
|
24
|
+
* inputs outside one go into a first page, and a form with no pages gets one to show.
|
|
25
|
+
*/
|
|
26
|
+
function firstPageEdits(editing, inputs, title) {
|
|
27
|
+
const names = inputNames(asRecord(inputs));
|
|
28
|
+
const loose = names.filter((name) => !isStep(asRecord(inputs)[name]));
|
|
29
|
+
if (loose.length === 0 && names.length > 0) return [];
|
|
30
|
+
const page = editing.nextName(names, "step_1");
|
|
31
|
+
const layout = asRecord(asRecord(inputs)["$meta"])["layout"];
|
|
32
|
+
return [
|
|
33
|
+
{
|
|
34
|
+
type: "addInput",
|
|
35
|
+
parent: [],
|
|
36
|
+
index: 0,
|
|
37
|
+
name: page,
|
|
38
|
+
definition: newStep(title)
|
|
39
|
+
},
|
|
40
|
+
...loose.length > 0 ? [{
|
|
41
|
+
type: "moveInputs",
|
|
42
|
+
paths: loose.map((name) => [name]),
|
|
43
|
+
parent: [page],
|
|
44
|
+
index: 0
|
|
45
|
+
}] : [],
|
|
46
|
+
...layout !== void 0 ? [{
|
|
47
|
+
type: "updateInput",
|
|
48
|
+
path: [page],
|
|
49
|
+
set: { options: {
|
|
50
|
+
...Object.fromEntries(loose.map((name) => [name, asRecord(inputs)[name]])),
|
|
51
|
+
$meta: { layout }
|
|
52
|
+
} }
|
|
53
|
+
}] : []
|
|
54
|
+
];
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* The edits that split a list of inputs into pages: the form's (`parent` is `[]`), or those the group
|
|
58
|
+
* or list at `parent` holds under `childKey`. Only the form's `$meta` is edited on its own, so a group's
|
|
59
|
+
* or list's inputs are written whole.
|
|
60
|
+
*/
|
|
61
|
+
function splitIntoPagesEdits(editing, list, parent, childKey, title, submitLabel) {
|
|
62
|
+
if (parent.length === 0) return [{
|
|
63
|
+
type: "updateWorkflow",
|
|
64
|
+
inputsMeta: {
|
|
65
|
+
set: { wizard: wizardSettings(list, submitLabel) },
|
|
66
|
+
unset: ["layout"]
|
|
67
|
+
}
|
|
68
|
+
}, ...firstPageEdits(editing, list, title)];
|
|
69
|
+
const record = asRecord(list);
|
|
70
|
+
const names = inputNames(record);
|
|
71
|
+
const loose = names.filter((name) => !isStep(record[name]));
|
|
72
|
+
const pages = Object.fromEntries(names.filter((name) => isStep(record[name])).map((name) => [name, record[name]]));
|
|
73
|
+
const first = loose.length > 0 || names.length === 0 ? { [editing.nextName(names, "step_1")]: {
|
|
74
|
+
...newStep(title),
|
|
75
|
+
options: {
|
|
76
|
+
...Object.fromEntries(loose.map((name) => [name, record[name]])),
|
|
77
|
+
...asRecord(record["$meta"])["layout"] !== void 0 ? { $meta: { layout: asRecord(record["$meta"])["layout"] } } : {}
|
|
78
|
+
}
|
|
79
|
+
} } : {};
|
|
80
|
+
const { layout: _layout, ...previousMeta } = asRecord(record["$meta"]);
|
|
81
|
+
const meta = {
|
|
82
|
+
...previousMeta,
|
|
83
|
+
wizard: wizardSettings(record)
|
|
84
|
+
};
|
|
85
|
+
return [{
|
|
86
|
+
type: "updateInput",
|
|
87
|
+
path: parent,
|
|
88
|
+
set: { [childKey ?? "items"]: {
|
|
89
|
+
$meta: meta,
|
|
90
|
+
...first,
|
|
91
|
+
...pages
|
|
92
|
+
} }
|
|
93
|
+
}];
|
|
94
|
+
}
|
|
95
|
+
/** A page added after a wizard's last, titled by how many it then has. */
|
|
96
|
+
function addPageEdit(editing, list, parent, title) {
|
|
97
|
+
const names = inputNames(asRecord(list));
|
|
98
|
+
const count = names.filter((name) => isStep(asRecord(list)[name])).length + 1;
|
|
99
|
+
return {
|
|
100
|
+
type: "addInput",
|
|
101
|
+
parent,
|
|
102
|
+
name: editing.nextName(names, `step_${count}`),
|
|
103
|
+
definition: newStep(title(count))
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* What undoing pages adds besides dropping the wizard from `$meta`: each page's inputs go back where
|
|
108
|
+
* the page was, in order, and the pages go. Moving them one by one refuses a name two pages share.
|
|
109
|
+
*/
|
|
110
|
+
function pagesBackEdits(list, parent) {
|
|
111
|
+
const record = asRecord(list);
|
|
112
|
+
const order = inputNames(record);
|
|
113
|
+
const edits = [];
|
|
114
|
+
for (const page of order.filter((name) => isStep(record[name]))) {
|
|
115
|
+
const fields = inputNames(asRecord(asRecord(record[page])["options"]));
|
|
116
|
+
const at = order.indexOf(page);
|
|
117
|
+
if (fields.length > 0) edits.push({
|
|
118
|
+
type: "moveInputs",
|
|
119
|
+
paths: fields.map((name) => [
|
|
120
|
+
...parent,
|
|
121
|
+
page,
|
|
122
|
+
name
|
|
123
|
+
]),
|
|
124
|
+
parent,
|
|
125
|
+
index: at
|
|
126
|
+
});
|
|
127
|
+
order.splice(at, 1, ...fields);
|
|
128
|
+
edits.push({
|
|
129
|
+
type: "deleteInput",
|
|
130
|
+
path: [...parent, page]
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
return edits;
|
|
134
|
+
}
|
|
135
|
+
/** The edits that turn a wizard back into a list of inputs drawn all at once. */
|
|
136
|
+
function unsplitPagesEdits(list, parent, childKey) {
|
|
137
|
+
const record = asRecord(list);
|
|
138
|
+
const pageLayouts = inputNames(record).filter((name) => isStep(record[name])).map((name) => asRecord(asRecord(record[name])["options"]));
|
|
139
|
+
const layout = pageLayouts.some((options) => asRecord(options["$meta"])["layout"] !== void 0) ? {
|
|
140
|
+
type: "stack",
|
|
141
|
+
children: pageLayouts.map(inputLayout)
|
|
142
|
+
} : void 0;
|
|
143
|
+
if (parent.length === 0) return [...pagesBackEdits(list, []), {
|
|
144
|
+
type: "updateWorkflow",
|
|
145
|
+
inputsMeta: {
|
|
146
|
+
unset: ["wizard"],
|
|
147
|
+
...layout ? { set: { layout } } : {}
|
|
148
|
+
}
|
|
149
|
+
}];
|
|
150
|
+
const { wizard: _wizard, ...meta } = asRecord(record["$meta"]);
|
|
151
|
+
if (layout) meta["layout"] = layout;
|
|
152
|
+
const unpaged = Object.keys(meta).length > 0 ? { $meta: meta } : {};
|
|
153
|
+
for (const name of inputNames(record)) if (isStep(record[name])) {
|
|
154
|
+
const options = asRecord(asRecord(record[name])["options"]);
|
|
155
|
+
for (const field of inputNames(options)) unpaged[field] = options[field];
|
|
156
|
+
} else unpaged[name] = record[name];
|
|
157
|
+
return [...pagesBackEdits(list, parent), {
|
|
158
|
+
type: "updateInput",
|
|
159
|
+
path: parent,
|
|
160
|
+
set: { [childKey ?? "items"]: unpaged }
|
|
161
|
+
}];
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* A wizard step added where the inputs aren't a wizard comes as a wizard of its own: a group whose
|
|
165
|
+
* fields are read as the form's own, holding the step.
|
|
166
|
+
*/
|
|
167
|
+
function wizardGroupDefinition(label, title) {
|
|
168
|
+
return {
|
|
169
|
+
type: "group",
|
|
170
|
+
label,
|
|
171
|
+
flatten: true,
|
|
172
|
+
items: {
|
|
173
|
+
$meta: { wizard: wizardSettings(void 0) },
|
|
174
|
+
step_1: newStep(title)
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
function newStep(title) {
|
|
179
|
+
return {
|
|
180
|
+
type: "step",
|
|
181
|
+
title,
|
|
182
|
+
options: {}
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
//#endregion
|
|
186
|
+
export { addPageEdit, firstPageEdits, pagesBackEdits, splitIntoPagesEdits, unsplitPagesEdits, wizardGroupDefinition, wizardSettings };
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import type { WorkflowEditing } from '../editing.js';
|
|
2
|
+
import { type Flag, type Json } from './editorFields.js';
|
|
3
|
+
import type { ExpressionRef } from './expressionRefs.js';
|
|
4
|
+
import { type Suggestion } from './SuggestionInput.js';
|
|
5
|
+
/** An input a workflow expression can read, by its path under `inputs`. */
|
|
6
|
+
export interface InputRef {
|
|
7
|
+
path: string[];
|
|
8
|
+
type: string;
|
|
9
|
+
label: string;
|
|
10
|
+
definition: Json;
|
|
11
|
+
}
|
|
12
|
+
/** Every input with a value, where a run reads it: groups nest unless flattened, lists hide their rows. */
|
|
13
|
+
export declare function inputRefs(editing: WorkflowEditing, inputs: Json | undefined): InputRef[];
|
|
14
|
+
export declare function refExpression(path: string[], suffix?: string): string;
|
|
15
|
+
/** The input `value` reads when it is exactly `${{ inputs.PATH[.suffix] }}`. */
|
|
16
|
+
export declare function readRef(value: unknown, suffix?: string): string[] | null;
|
|
17
|
+
export declare function flagExpression(path: string[], negated: boolean): string;
|
|
18
|
+
export declare function equalsExpression(path: string[], value: string, negated?: boolean): string;
|
|
19
|
+
/** Picks an existing input of the given types, or asks for a new one. */
|
|
20
|
+
export declare function InputRefSelect({ refs, types, value, onChange, onNew, newLabel, label, }: {
|
|
21
|
+
refs: InputRef[];
|
|
22
|
+
types: string[];
|
|
23
|
+
value: string[] | null;
|
|
24
|
+
onChange: (path: string[]) => void;
|
|
25
|
+
onNew?: (() => void) | undefined;
|
|
26
|
+
newLabel?: string;
|
|
27
|
+
label: string;
|
|
28
|
+
}): import("react").JSX.Element;
|
|
29
|
+
/** Where fields find inputs to read, and how they ask for a new one. */
|
|
30
|
+
export interface InputSource {
|
|
31
|
+
refs: InputRef[];
|
|
32
|
+
/** Outputs, matrix values, variables, sessions and env an expression here can read. */
|
|
33
|
+
extras?: ExpressionRef[] | undefined;
|
|
34
|
+
/** Opens a dialog for a new input of `type`, then reports where it lives. */
|
|
35
|
+
create?: ((type: string, onCreated: (path: string[]) => void) => void) | undefined;
|
|
36
|
+
}
|
|
37
|
+
/** A text field that takes any value, expressions included, with suggestions to pick from. */
|
|
38
|
+
export declare function ExpressionInput({ id, label, value, onChange, suggestions, placeholder, error, describedBy, required, }: {
|
|
39
|
+
id?: string;
|
|
40
|
+
label: string;
|
|
41
|
+
value: string;
|
|
42
|
+
onChange: (value: string) => void;
|
|
43
|
+
suggestions?: Suggestion[];
|
|
44
|
+
placeholder?: string | undefined;
|
|
45
|
+
error?: string | undefined;
|
|
46
|
+
describedBy?: string;
|
|
47
|
+
required?: boolean;
|
|
48
|
+
}): import("react").JSX.Element;
|
|
49
|
+
/** A switch, or an expression typed in its place, such as one that follows another input. */
|
|
50
|
+
export declare function FlagField({ label, yamlKey, description, value, original, fallback, onChange, source, }: {
|
|
51
|
+
label: string;
|
|
52
|
+
yamlKey?: string;
|
|
53
|
+
description: string;
|
|
54
|
+
value: Flag;
|
|
55
|
+
original: unknown;
|
|
56
|
+
fallback?: boolean;
|
|
57
|
+
onChange: (value: Flag) => void;
|
|
58
|
+
source?: InputSource | undefined;
|
|
59
|
+
}): import("react").JSX.Element;
|
|
60
|
+
/** When a job or step runs: any condition, typed, with the executor's status checks and the form's switches offered. */
|
|
61
|
+
export declare function ConditionField({ label, yamlKey, description, value, onChange, source, scope, }: {
|
|
62
|
+
label: string;
|
|
63
|
+
yamlKey?: string;
|
|
64
|
+
description: string;
|
|
65
|
+
value: string;
|
|
66
|
+
onChange: (value: string) => void;
|
|
67
|
+
source: InputSource;
|
|
68
|
+
/** A job's status checks read the jobs it needs; a step's, the steps before it. */
|
|
69
|
+
scope: 'job' | 'step';
|
|
70
|
+
}): import("react").JSX.Element;
|
|
71
|
+
/** A value typed in, with the form's inputs of the given types offered, or a new one made for it. */
|
|
72
|
+
export declare function ValueOrInputField({ label, yamlKey, description, value, onChange, source, types, suffix, literal, required, error, suggestions, placeholder, }: {
|
|
73
|
+
label: string;
|
|
74
|
+
yamlKey?: string;
|
|
75
|
+
description: string;
|
|
76
|
+
value: string;
|
|
77
|
+
onChange: (value: string) => void;
|
|
78
|
+
source: InputSource;
|
|
79
|
+
types: string[];
|
|
80
|
+
/** A property of the input's value to read, such as a cluster's ip. */
|
|
81
|
+
suffix?: string;
|
|
82
|
+
/** False when the schema takes only an expression here. */
|
|
83
|
+
literal?: boolean;
|
|
84
|
+
required?: boolean;
|
|
85
|
+
error?: string | undefined;
|
|
86
|
+
suggestions?: string[];
|
|
87
|
+
placeholder?: string;
|
|
88
|
+
}): import("react").JSX.Element;
|
|
89
|
+
/** A script field with a menu that drops an input's reference at the cursor. */
|
|
90
|
+
export declare function ScriptField({ label, yamlKey, description, value, onChange, source, rows, error, placeholder, }: {
|
|
91
|
+
label: string;
|
|
92
|
+
yamlKey?: string;
|
|
93
|
+
description: string;
|
|
94
|
+
value: string;
|
|
95
|
+
onChange: (value: string) => void;
|
|
96
|
+
source: InputSource;
|
|
97
|
+
/** Lines to show; without it the field starts one line tall and grows. */
|
|
98
|
+
rows?: number;
|
|
99
|
+
error?: string | undefined;
|
|
100
|
+
placeholder?: string;
|
|
101
|
+
}): import("react").JSX.Element;
|
|
102
|
+
/** References offered as suggestions on free-text values. */
|
|
103
|
+
export declare function refSuggestions(source: InputSource): string[];
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { FaPlus } from "../icons/fa/FaPlus.js";
|
|
2
|
+
import { IconButton } from "../components/IconButton.js";
|
|
3
|
+
import { Toggle } from "../components/Toggle.js";
|
|
4
|
+
import { useGraphEditorStrings, useInputsEditorStrings } from "./editorStrings.js";
|
|
5
|
+
import { useFieldLint } from "./fieldProblems.js";
|
|
6
|
+
import { asRecord } from "./records.js";
|
|
7
|
+
import { SuggestionInput } from "./SuggestionInput.js";
|
|
8
|
+
import { ExpressionToggle, FieldError, FieldLabel, LabelledField, flagError, flagValue, text } from "./editorFields.js";
|
|
9
|
+
import { useId, useState } from "react";
|
|
10
|
+
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
11
|
+
//#region src/graph/inputRefs.tsx
|
|
12
|
+
/** Every input with a value, where a run reads it: groups nest unless flattened, lists hide their rows. */
|
|
13
|
+
function inputRefs(editing, inputs) {
|
|
14
|
+
const root = asRecord(inputs);
|
|
15
|
+
const flatSteps = editing.wizardFlattens(root);
|
|
16
|
+
const out = [];
|
|
17
|
+
const walk = (map, prefix) => {
|
|
18
|
+
for (const [name, raw] of Object.entries(map)) {
|
|
19
|
+
if (name === "$meta") continue;
|
|
20
|
+
const definition = asRecord(raw);
|
|
21
|
+
const type = text(definition["type"]);
|
|
22
|
+
const childKey = editing.inputChildrenKey(type);
|
|
23
|
+
if (type === "group" || type === "step") {
|
|
24
|
+
const flat = type === "step" ? flatSteps : definition["flatten"] === true;
|
|
25
|
+
walk(asRecord(childKey ? definition[childKey] : void 0), flat ? prefix : [...prefix, name]);
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
if (type === "header") continue;
|
|
29
|
+
out.push({
|
|
30
|
+
path: [...prefix, name],
|
|
31
|
+
type,
|
|
32
|
+
label: text(definition["label"]) || name,
|
|
33
|
+
definition
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
walk(root, []);
|
|
38
|
+
return out;
|
|
39
|
+
}
|
|
40
|
+
function refExpression(path, suffix) {
|
|
41
|
+
return `\${{ inputs.${[...path, ...suffix ? [suffix] : []].join(".")} }}`;
|
|
42
|
+
}
|
|
43
|
+
function flagExpression(path, negated) {
|
|
44
|
+
return `\${{ ${negated ? "!" : ""}inputs.${path.join(".")} }}`;
|
|
45
|
+
}
|
|
46
|
+
function equalsExpression(path, value, negated = false) {
|
|
47
|
+
const operator = negated ? "!=" : "==";
|
|
48
|
+
return `\${{ inputs.${path.join(".")} ${operator} '${value.replace(/'/g, "''")}' }}`;
|
|
49
|
+
}
|
|
50
|
+
/** The option values a dropdown input offers, for conditions on it. */
|
|
51
|
+
function optionValues(definition) {
|
|
52
|
+
const options = definition["options"];
|
|
53
|
+
return Array.isArray(options) ? options.map((option) => typeof option === "object" && option !== null ? text(option["value"]) : text(option)) : [];
|
|
54
|
+
}
|
|
55
|
+
/** Makes a new input of `type` for the field, when the host can; `onCreated` gets where it went. */
|
|
56
|
+
function NewInputButton({ type, source, onCreated }) {
|
|
57
|
+
const t = useGraphEditorStrings();
|
|
58
|
+
const inputsEditor = useInputsEditorStrings();
|
|
59
|
+
const create = source?.create;
|
|
60
|
+
if (!create) return null;
|
|
61
|
+
const types = inputsEditor.types;
|
|
62
|
+
return /* @__PURE__ */ jsx(IconButton, {
|
|
63
|
+
icon: /* @__PURE__ */ jsx(FaPlus, { className: "h-4 w-4" }),
|
|
64
|
+
label: t.newInputOf(types[type] ?? type),
|
|
65
|
+
size: "sm",
|
|
66
|
+
variant: "ghost",
|
|
67
|
+
onClick: () => create(type, onCreated)
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
var ONE_OF = ["dropdown", "radio"];
|
|
71
|
+
/** A text field that takes any value, expressions included, with suggestions to pick from. */
|
|
72
|
+
function ExpressionInput({ id, label, value, onChange, suggestions = [], placeholder, error, describedBy, required }) {
|
|
73
|
+
return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(SuggestionInput, {
|
|
74
|
+
id,
|
|
75
|
+
ariaLabel: label,
|
|
76
|
+
"aria-describedby": describedBy,
|
|
77
|
+
required,
|
|
78
|
+
value,
|
|
79
|
+
placeholder,
|
|
80
|
+
suggestions,
|
|
81
|
+
onChange
|
|
82
|
+
}), /* @__PURE__ */ jsx(FieldError, { message: error })] });
|
|
83
|
+
}
|
|
84
|
+
/** Conditions on the form's switches and choices, for fields that run on one. */
|
|
85
|
+
function conditionSuggestions(source, t) {
|
|
86
|
+
return (source?.refs ?? []).flatMap((ref) => {
|
|
87
|
+
if (ref.type === "boolean") return [{
|
|
88
|
+
value: flagExpression(ref.path, false),
|
|
89
|
+
label: t.whenOn(ref.label)
|
|
90
|
+
}, {
|
|
91
|
+
value: flagExpression(ref.path, true),
|
|
92
|
+
label: t.whenOff(ref.label)
|
|
93
|
+
}];
|
|
94
|
+
return ONE_OF.includes(ref.type) ? optionValues(ref.definition).map((option) => ({
|
|
95
|
+
value: equalsExpression(ref.path, option),
|
|
96
|
+
label: t.whenIs(ref.label, option)
|
|
97
|
+
})) : [];
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
/** A switch, or an expression typed in its place, such as one that follows another input. */
|
|
101
|
+
function FlagField({ label, yamlKey, description, value, original, fallback = false, onChange, source }) {
|
|
102
|
+
const t = useGraphEditorStrings();
|
|
103
|
+
const id = useId();
|
|
104
|
+
const [typed, setTyped] = useState(typeof value === "string");
|
|
105
|
+
const checked = typeof value === "boolean" ? value : fallback;
|
|
106
|
+
const setTypedValue = (next) => onChange(next.trim() ? next : void 0);
|
|
107
|
+
const lint = useFieldLint(yamlKey, id, typeof value === "string" ? value : "", setTypedValue);
|
|
108
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
109
|
+
className: "flex flex-col gap-1.5",
|
|
110
|
+
children: [
|
|
111
|
+
/* @__PURE__ */ jsxs("div", {
|
|
112
|
+
className: "flex items-center justify-between gap-3",
|
|
113
|
+
children: [/* @__PURE__ */ jsx(FieldLabel, {
|
|
114
|
+
label,
|
|
115
|
+
yamlKey,
|
|
116
|
+
description
|
|
117
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
118
|
+
className: "flex shrink-0 items-center gap-1",
|
|
119
|
+
children: [!typed && /* @__PURE__ */ jsx(Toggle, {
|
|
120
|
+
checked,
|
|
121
|
+
onChange: (on) => onChange(flagValue(on, fallback, original)),
|
|
122
|
+
label
|
|
123
|
+
}), /* @__PURE__ */ jsx(ExpressionToggle, {
|
|
124
|
+
active: typed,
|
|
125
|
+
onChange: (on) => {
|
|
126
|
+
setTyped(on);
|
|
127
|
+
onChange(void 0);
|
|
128
|
+
}
|
|
129
|
+
})]
|
|
130
|
+
})]
|
|
131
|
+
}),
|
|
132
|
+
typed && /* @__PURE__ */ jsxs("div", {
|
|
133
|
+
className: "flex items-center gap-2",
|
|
134
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
135
|
+
className: "flex-1",
|
|
136
|
+
children: /* @__PURE__ */ jsx(ExpressionInput, {
|
|
137
|
+
id,
|
|
138
|
+
label,
|
|
139
|
+
value: typeof value === "string" ? value : "",
|
|
140
|
+
suggestions: conditionSuggestions(source, t),
|
|
141
|
+
error: flagError(value, t) ?? lint.message,
|
|
142
|
+
onChange: setTypedValue
|
|
143
|
+
})
|
|
144
|
+
}), /* @__PURE__ */ jsx(NewInputButton, {
|
|
145
|
+
type: "boolean",
|
|
146
|
+
source,
|
|
147
|
+
onCreated: (path) => onChange(flagExpression(path, false))
|
|
148
|
+
})]
|
|
149
|
+
}),
|
|
150
|
+
typed && lint.fixes
|
|
151
|
+
]
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
/** A value typed in, with the form's inputs of the given types offered, or a new one made for it. */
|
|
155
|
+
function ValueOrInputField({ label, yamlKey, description, value, onChange, source, types, suffix, literal = true, required = false, error, suggestions = [], placeholder }) {
|
|
156
|
+
const t = useGraphEditorStrings();
|
|
157
|
+
const id = useId();
|
|
158
|
+
const lint = useFieldLint(yamlKey, id, value, onChange);
|
|
159
|
+
const firstType = types[0] ?? "string";
|
|
160
|
+
const offered = [...source.refs.filter((ref) => types.includes(ref.type)).map((ref) => ({
|
|
161
|
+
value: refExpression(ref.path, suffix),
|
|
162
|
+
label: ref.label
|
|
163
|
+
})), ...literal ? suggestions.map((suggestion) => ({ value: suggestion })) : []];
|
|
164
|
+
return /* @__PURE__ */ jsxs(LabelledField, {
|
|
165
|
+
label: required ? `${label} *` : label,
|
|
166
|
+
yamlKey,
|
|
167
|
+
description,
|
|
168
|
+
descriptionId: `${id}-help`,
|
|
169
|
+
children: [
|
|
170
|
+
/* @__PURE__ */ jsxs("div", {
|
|
171
|
+
className: "flex items-center gap-2",
|
|
172
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
173
|
+
className: "flex-1",
|
|
174
|
+
children: /* @__PURE__ */ jsx(ExpressionInput, {
|
|
175
|
+
id,
|
|
176
|
+
label,
|
|
177
|
+
describedBy: `${id}-help`,
|
|
178
|
+
required,
|
|
179
|
+
value,
|
|
180
|
+
placeholder: placeholder ?? (literal ? void 0 : t.chooseInput),
|
|
181
|
+
suggestions: offered,
|
|
182
|
+
onChange
|
|
183
|
+
})
|
|
184
|
+
}), /* @__PURE__ */ jsx(NewInputButton, {
|
|
185
|
+
type: firstType,
|
|
186
|
+
source,
|
|
187
|
+
onCreated: (path) => onChange(refExpression(path, suffix))
|
|
188
|
+
})]
|
|
189
|
+
}),
|
|
190
|
+
/* @__PURE__ */ jsx(FieldError, { message: error ?? lint.message }),
|
|
191
|
+
lint.fixes
|
|
192
|
+
]
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
//#endregion
|
|
196
|
+
export { ExpressionInput, FlagField, ValueOrInputField, equalsExpression, flagExpression, inputRefs, refExpression };
|
|
@@ -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,7 @@
|
|
|
1
|
+
//#region src/graph/records.ts
|
|
2
|
+
/** `value` when it's a plain object, else an empty one, so its keys read without checks. */
|
|
3
|
+
function asRecord(value) {
|
|
4
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
|
|
5
|
+
}
|
|
6
|
+
//#endregion
|
|
7
|
+
export { asRecord };
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { EditRefusalReason, WorkflowEditing } from '../editing.js';
|
|
2
|
+
import { type GraphEditorStrings } from './editorStrings.js';
|
|
3
|
+
/** A refusal in the host's words, or the engine's English when the host gives none. */
|
|
4
|
+
export declare function refusalText(t: GraphEditorStrings, editing: WorkflowEditing, reason: EditRefusalReason): string;
|
|
5
|
+
/** What to tell the person when an edit throws: why it was refused, or the error itself. */
|
|
6
|
+
export declare function editErrorText(t: GraphEditorStrings, editing: WorkflowEditing, error: unknown): string;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { WorkflowEditing } from '../editing.js';
|
|
2
|
+
import type { EditorMarker } from '../editor/Monaco.js';
|
|
3
|
+
import type { NestedWorkflowText } from '../editor/nestedText.js';
|
|
4
|
+
import { type Strings } from './editorFields.js';
|
|
5
|
+
import { type ScopedProblem } from './fieldProblems.js';
|
|
6
|
+
/** Which view the job, step, input and settings dialogs open in. */
|
|
7
|
+
export type SettingsView = 'form' | 'yaml';
|
|
8
|
+
export declare function yamlProblem(yaml: string, t: Strings, editing: WorkflowEditing): string | undefined;
|
|
9
|
+
export declare function ViewSwitch({ yaml, onForm, onYaml, }: {
|
|
10
|
+
yaml: boolean;
|
|
11
|
+
onForm: () => void;
|
|
12
|
+
onYaml: () => void;
|
|
13
|
+
}): import("react").JSX.Element;
|
|
14
|
+
export interface ScopedProblems {
|
|
15
|
+
markers: EditorMarker[];
|
|
16
|
+
problems: ScopedProblem[];
|
|
17
|
+
}
|
|
18
|
+
export declare const NO_SCOPED: ScopedProblems;
|
|
19
|
+
export declare function YamlPane({ path, value, onChange, problem, scoped, nested, }: {
|
|
20
|
+
path: string;
|
|
21
|
+
value: string;
|
|
22
|
+
onChange: (value: string) => void;
|
|
23
|
+
problem: string | undefined;
|
|
24
|
+
scoped: ScopedProblems;
|
|
25
|
+
nested?: NestedWorkflowText | undefined;
|
|
26
|
+
}): import("react").JSX.Element;
|