@parallelworks/ui 0.35.0 → 0.37.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 (81) hide show
  1. package/README.md +16 -0
  2. package/dist/ai/agent/permissions.d.ts +2 -2
  3. package/dist/ai/agent/permissions.js +3 -1
  4. package/dist/ai/components/agent/AgentMessageParts.js +8 -2
  5. package/dist/ai/components/agent/PermissionPicker.js +1 -0
  6. package/dist/ai/strings.d.ts +8 -0
  7. package/dist/ai/strings.js +8 -0
  8. package/dist/ai/types.d.ts +2 -0
  9. package/dist/components/Dropdown.d.ts +2 -0
  10. package/dist/components/Dropdown.js +7 -2
  11. package/dist/components/Input.d.ts +11 -6
  12. package/dist/components/Input.js +93 -74
  13. package/dist/components/Provider.d.ts +31 -0
  14. package/dist/components/Provider.js +50 -2
  15. package/dist/editing.d.ts +404 -0
  16. package/dist/editor/EditorField.js +1 -1
  17. package/dist/editor/Monaco.d.ts +15 -1
  18. package/dist/editor/Monaco.js +117 -13
  19. package/dist/editor/editorModel.d.ts +5 -0
  20. package/dist/editor/editorModel.js +23 -0
  21. package/dist/editor/index.d.ts +4 -0
  22. package/dist/editor/index.js +5 -1
  23. package/dist/editor/lintContext.d.ts +21 -0
  24. package/dist/editor/lintContext.js +136 -0
  25. package/dist/editor/lintOwner.d.ts +2 -0
  26. package/dist/editor/lintOwner.js +5 -0
  27. package/dist/editor/nestedText.d.ts +13 -0
  28. package/dist/editor/nestedText.js +15 -0
  29. package/dist/editor/reveal.d.ts +4 -0
  30. package/dist/editor/reveal.js +15 -0
  31. package/dist/editor/settingsYaml.d.ts +12 -0
  32. package/dist/editor/settingsYaml.js +41 -0
  33. package/dist/form/Fields/DropdownField.js +1 -1
  34. package/dist/form/Fields/DurationField.js +1 -1
  35. package/dist/form/Form.js +1 -1
  36. package/dist/form/fieldRegistry.js +1 -1
  37. package/dist/form/index.js +5 -5
  38. package/dist/graph/InputDialog.d.ts +80 -0
  39. package/dist/graph/InputDialog.js +596 -0
  40. package/dist/graph/ProblemsButton.d.ts +9 -0
  41. package/dist/graph/ProblemsButton.js +61 -0
  42. package/dist/graph/ShortcutsButton.d.ts +15 -0
  43. package/dist/graph/SuggestionInput.d.ts +18 -0
  44. package/dist/graph/ToolbarPopover.d.ts +8 -0
  45. package/dist/graph/ToolbarPopover.js +71 -0
  46. package/dist/graph/editorApi.d.ts +51 -0
  47. package/dist/graph/editorChrome.d.ts +27 -0
  48. package/dist/graph/editorFields.d.ts +180 -0
  49. package/dist/graph/editorPrimitives.d.ts +60 -0
  50. package/dist/graph/editorPrimitives.js +59 -0
  51. package/dist/graph/editorStrings.d.ts +695 -0
  52. package/dist/graph/editorStrings.js +699 -0
  53. package/dist/graph/expressionRefs.d.ts +12 -0
  54. package/dist/graph/fieldProblems.d.ts +32 -0
  55. package/dist/graph/index.d.ts +6 -0
  56. package/dist/graph/index.js +5 -1
  57. package/dist/graph/inputLayout.d.ts +25 -0
  58. package/dist/graph/inputRefs.d.ts +103 -0
  59. package/dist/graph/records.d.ts +3 -0
  60. package/dist/graph/records.js +7 -0
  61. package/dist/graph/refusalText.d.ts +6 -0
  62. package/dist/graph/settingsViews.d.ts +26 -0
  63. package/dist/graph/usesInputs.d.ts +17 -0
  64. package/dist/graph/usesInputs.js +40 -0
  65. package/dist/icons/bi/BiRedo.js +16 -0
  66. package/dist/icons/bi/BiUndo.js +16 -0
  67. package/dist/icons/lu/LuBraces.js +27 -0
  68. package/dist/icons/lu/LuCode.js +27 -0
  69. package/dist/icons/lu/LuKeyboard.js +71 -0
  70. package/dist/icons/lu/LuNetwork.js +63 -0
  71. package/dist/icons/lu/LuTextCursorInput.js +45 -0
  72. package/dist/icons.d.ts +2 -2
  73. package/dist/icons.js +8 -1
  74. package/dist/index.d.ts +2 -1
  75. package/dist/index.js +2 -2
  76. package/dist/list/ActionToolbar.d.ts +2 -0
  77. package/dist/list/ActionToolbar.js +18 -5
  78. package/dist/list/RowContextMenu.d.ts +10 -1
  79. package/dist/list/RowContextMenu.js +185 -30
  80. package/dist/styles.css +1 -1
  81. package/package.json +1 -1
package/dist/editing.d.ts CHANGED
@@ -5,20 +5,386 @@ export interface GraphPosition {
5
5
  }
6
6
  /** Job name to grid slot, as stored on a workflow. */
7
7
  export type GraphLayout = Record<string, GraphPosition>;
8
+ /** Where a dragged job lands; `insertColumn` opens a new column at `column`. */
9
+ export interface GraphSlot {
10
+ column: number;
11
+ row: number;
12
+ insertColumn?: boolean;
13
+ }
14
+ /** One entry of a job's `needs` list. */
15
+ export interface NeedRef {
16
+ job: string;
17
+ need: string;
18
+ }
19
+ export type NudgeDirection = 'up' | 'down' | 'left' | 'right';
8
20
  /** One column's boxes, top to bottom, the row each box sits in, and the column's number. */
9
21
  export interface LaidOutColumn {
10
22
  boxes: string[][];
11
23
  rows: number[];
12
24
  column: number;
13
25
  }
26
+ /** A step by its job and its index in that job's `steps`. */
27
+ export interface StepRef {
28
+ job: string;
29
+ index: number;
30
+ }
31
+ /** A matrix variable: a list of values, or a `${{ }}` expression string. */
32
+ export type MatrixVariable = [name: string, values: unknown[] | string];
33
+ /** Keys to write (a null value writes YAML null) and keys to remove. */
34
+ export interface FieldPatch {
35
+ set?: Record<string, unknown>;
36
+ unset?: string[];
37
+ }
38
+ /** Keys from the workflow's inputs down to an input, through groups, lists and steps. */
39
+ export type InputPath = string[];
40
+ export type InputEdit = {
41
+ type: 'addInput';
42
+ parent: InputPath;
43
+ /** Position among the container's inputs; appends when omitted. */
44
+ index?: number;
45
+ /** Generated when omitted. */
46
+ name?: string;
47
+ definition: Record<string, unknown>;
48
+ } | {
49
+ type: 'deleteInput';
50
+ path: InputPath;
51
+ }
52
+ /** Inputs, kept in form order, moved together to one place. */
53
+ | {
54
+ type: 'moveInputs';
55
+ paths: InputPath[];
56
+ parent: InputPath;
57
+ index: number;
58
+ } | {
59
+ type: 'duplicateInput';
60
+ path: InputPath;
61
+ } | ({
62
+ type: 'updateInput';
63
+ path: InputPath;
64
+ name?: string;
65
+ } & FieldPatch)
66
+ /** Replaces an input's settings with YAML text, as its YAML editor writes them. */
67
+ | {
68
+ type: 'setInputYaml';
69
+ path: InputPath;
70
+ yaml: string;
71
+ } | ({
72
+ type: 'updateWorkflow';
73
+ /** Changes to the input form's `$meta`, in the same undo step. */
74
+ inputsMeta?: FieldPatch;
75
+ } & FieldPatch);
76
+ export type GraphEdit =
77
+ /** `anchor` lands in `to`; the other jobs keep their offsets from it. */
78
+ {
79
+ type: 'move';
80
+ jobs: string[];
81
+ to: GraphSlot;
82
+ layout: GraphLayout;
83
+ anchor?: string;
84
+ } | {
85
+ type: 'addJob';
86
+ matrix?: boolean;
87
+ to?: GraphSlot;
88
+ layout?: GraphLayout;
89
+ } | {
90
+ type: 'deleteJob';
91
+ jobs: string[];
92
+ } | {
93
+ type: 'renameJob';
94
+ from: string;
95
+ to: string;
96
+ } | {
97
+ type: 'duplicateJob';
98
+ job: string;
99
+ layout?: GraphLayout;
100
+ } | {
101
+ type: 'connect';
102
+ from: string;
103
+ to: string;
104
+ }
105
+ /** `jobs` and what depends on them move right of their needs; `layout` is the grid as drawn. */
106
+ | {
107
+ type: 'placeAfterNeeds';
108
+ jobs: string[];
109
+ layout?: GraphLayout;
110
+ }
111
+ /** Moves `jobs` left of what needs them; `dependents` gained a need and may leave their node. */
112
+ | {
113
+ type: 'placeBeforeDependents';
114
+ jobs: string[];
115
+ dependents: string[];
116
+ layout?: GraphLayout;
117
+ }
118
+ /** Jobs join `into`'s node: they take its slot and a copy of its needs. */
119
+ | {
120
+ type: 'groupJobs';
121
+ jobs: string[];
122
+ into: string;
123
+ layout: GraphLayout;
124
+ } | {
125
+ type: 'disconnect';
126
+ needs: NeedRef[];
127
+ } | {
128
+ type: 'setNeedAny';
129
+ jobs: string[];
130
+ dep: string;
131
+ any: boolean;
132
+ } | {
133
+ type: 'setMatrix';
134
+ job: string;
135
+ variables: MatrixVariable[] | null;
136
+ } | {
137
+ type: 'addStep';
138
+ job: string;
139
+ } | {
140
+ type: 'deleteStep';
141
+ job: string;
142
+ index: number;
143
+ } | {
144
+ type: 'renameStep';
145
+ job: string;
146
+ index: number;
147
+ name: string;
148
+ } | {
149
+ type: 'moveStep';
150
+ job: string;
151
+ from: number;
152
+ to: number;
153
+ }
154
+ /** Steps, in order, to insertion point `index` of `job` (an index into its steps before the move). */
155
+ | {
156
+ type: 'moveSteps';
157
+ steps: StepRef[];
158
+ job: string;
159
+ index: number;
160
+ }
161
+ /** The steps in `yaml`, a list as stepsYaml writes it, inserted at `index` of `job`. */
162
+ | {
163
+ type: 'pasteSteps';
164
+ yaml: string;
165
+ job: string;
166
+ index: number;
167
+ }
168
+ /** The jobs in `yaml`, as jobsYaml writes it, added as new jobs; each lands at `to` plus its offset. */
169
+ | {
170
+ type: 'pasteJobs';
171
+ yaml: string;
172
+ to?: GraphSlot;
173
+ offsets?: Record<string, GraphPosition>;
174
+ layout?: GraphLayout;
175
+ }
176
+ /** Jobs one slot over, as the arrow keys move them. */
177
+ | {
178
+ type: 'nudge';
179
+ jobs: string[];
180
+ direction: NudgeDirection;
181
+ layout: GraphLayout;
182
+ } | ({
183
+ type: 'updateJob';
184
+ job: string;
185
+ name?: string;
186
+ } & FieldPatch) | ({
187
+ type: 'updateStep';
188
+ job: string;
189
+ index: number;
190
+ } & FieldPatch)
191
+ /** Replaces a job's or step's settings with YAML text, as its YAML editor writes them. */
192
+ | {
193
+ type: 'setJobYaml';
194
+ job: string;
195
+ yaml: string;
196
+ } | {
197
+ type: 'setStepYaml';
198
+ job: string;
199
+ index: number;
200
+ yaml: string;
201
+ }
202
+ /** Replaces the workflow's own settings, everything but its jobs and trigger, with YAML text. */
203
+ | {
204
+ type: 'setSettingsYaml';
205
+ yaml: string;
206
+ } | {
207
+ type: 'resetLayout';
208
+ } | InputEdit
209
+ /** Several edits as one undo step, such as a new input and the field that reads it. */
210
+ | {
211
+ type: 'batch';
212
+ edits: GraphEdit[];
213
+ };
214
+ export interface GraphEditState {
215
+ yml: string;
216
+ layout: GraphLayout | undefined;
217
+ }
218
+ export interface GraphEditResult extends GraphEditState {
219
+ /** Name of the job or input the edit created, if any. */
220
+ created?: string;
221
+ /** The jobs a paste added, in order. */
222
+ createdJobs?: string[];
223
+ /** Where moved or pasted steps now sit. */
224
+ steps?: StepRef[];
225
+ }
226
+ /** A step as a refusal names it: by its name, or by where it sits when it has none. */
227
+ export type RefusedStep = {
228
+ name: string;
229
+ } | {
230
+ job: string;
231
+ index: number;
232
+ };
233
+ /** Why the workflow can't take an edit, by code, so the editor can say it in the reader's language. */
234
+ export type EditRefusalReason = {
235
+ code: 'cycle';
236
+ job: string;
237
+ need: string;
238
+ } | {
239
+ code: 'needRead';
240
+ job: string;
241
+ need: string;
242
+ } | {
243
+ code: 'columnRead';
244
+ job: string;
245
+ need: string;
246
+ } | {
247
+ code: 'stepReadsNeed';
248
+ step: RefusedStep;
249
+ need: string;
250
+ job: string;
251
+ } | {
252
+ code: 'stepReadsStep';
253
+ step: RefusedStep;
254
+ id: string;
255
+ job: string;
256
+ } | {
257
+ code: 'stepReadsMatrix';
258
+ step: RefusedStep;
259
+ variable: string;
260
+ job: string;
261
+ } | {
262
+ code: 'stepLeftBehind';
263
+ job: string;
264
+ id: string;
265
+ target: string;
266
+ } | {
267
+ code: 'jobNameInvalid';
268
+ } | {
269
+ code: 'jobNameTaken';
270
+ name: string;
271
+ } | {
272
+ code: 'inputNameInvalid';
273
+ } | {
274
+ code: 'inputNameTaken';
275
+ name: string;
276
+ } | {
277
+ code: 'inputIntoItself';
278
+ } | {
279
+ code: 'needsExpression';
280
+ } | {
281
+ code: 'matrixExpression';
282
+ } | {
283
+ code: 'inputsExpression';
284
+ name: string;
285
+ } | {
286
+ code: 'notAGroup';
287
+ name: string;
288
+ } | {
289
+ code: 'noStepsToPaste';
290
+ } | {
291
+ code: 'noJobsToPaste';
292
+ } | {
293
+ code: 'notASetting';
294
+ key: string;
295
+ };
296
+ /** A "did you mean": `replace` for each reference to `find` in the value, or, with `key`, the key's new name. */
297
+ export interface LintFix {
298
+ find: string;
299
+ replace: string;
300
+ key: boolean;
301
+ }
302
+ /** One problem a workflow's schema can't catch, such as a need read but never listed. */
303
+ export interface WorkflowLintProblem {
304
+ path: string;
305
+ message: string;
306
+ line: number;
307
+ fix?: LintFix;
308
+ }
309
+ /** What the checks can't work out from the document: `uses` targets' inputs, and secret variable names. */
310
+ export interface WorkflowLintContext {
311
+ usesInputs?: Record<string, unknown>;
312
+ secretVars?: string[];
313
+ }
314
+ export type SettingsYamlProblem = {
315
+ kind: 'syntax';
316
+ message: string;
317
+ } | {
318
+ kind: 'notMap';
319
+ };
320
+ /** A key or list index on the way down to a YAML node. */
321
+ export type YamlSegment = string | number;
322
+ type UnknownRecord = Record<string, unknown>;
323
+ /** One input a built-in action takes in its step's `with`, labelled in the reader's language. */
324
+ export interface WorkflowActionInput {
325
+ key: string;
326
+ kind: 'text' | 'textarea' | 'number' | 'any' | 'list' | 'bool' | 'choice' | 'flags' | 'target';
327
+ label: string;
328
+ description: string;
329
+ required?: boolean | undefined;
330
+ choices?: string[] | undefined;
331
+ /** A `${{ }}` expression also stands in for a choice. */
332
+ expression?: boolean | undefined;
333
+ /** Inputs of these types, or their `suffix` property, can fill this one in. */
334
+ refTypes?: string[] | undefined;
335
+ suffix?: string | undefined;
336
+ /** What a `bool` left unset means. */
337
+ fallback?: boolean | undefined;
338
+ /** A `target` input's own keys. */
339
+ fields?: {
340
+ key: string;
341
+ label: string;
342
+ description: string;
343
+ }[] | undefined;
344
+ }
345
+ /** A built-in action a step can use, keyed by its `uses` value in `UIData['workflowActions']`. */
346
+ export interface WorkflowAction {
347
+ /** What it does, shown beside it where it's picked. */
348
+ description: string;
349
+ inputs: WorkflowActionInput[];
350
+ }
14
351
  /**
15
352
  * The workflow editing the visual editor defers to: reading and rewriting the workflow's YAML,
16
353
  * its graph layout and its checks. A host built on a workflow package can pass that package's
17
354
  * functions of the same names.
18
355
  */
19
356
  export interface WorkflowEditing {
357
+ loadYaml: (text: string) => unknown;
358
+ dumpYaml: (data: unknown) => string;
359
+ /** Applies one edit to the workflow's text and layout; throws `EditRefusal` when it can't. */
360
+ applyGraphEdit: (state: GraphEditState, edit: GraphEdit) => GraphEditResult;
361
+ /** `edits` as one undo step: the edit itself when there is one, nothing when there are none. */
362
+ batchOf: (edits: GraphEdit[]) => GraphEdit | null;
363
+ /** What an edit throws when the workflow can't take it, so the reason can be told. */
364
+ EditRefusal: abstract new (reason: EditRefusalReason) => Error & {
365
+ readonly reason: EditRefusalReason;
366
+ };
367
+ /** The English wording of a refusal. */
368
+ refusalMessage: (reason: EditRefusalReason) => string;
369
+ /** Each job's `needs` entries; expression-valued needs count as none. */
370
+ jobNeeds: (jobs: unknown) => Record<string, string[]>;
371
+ /** Whether `from` already depends on `to`, directly or through other jobs. */
372
+ dependsOn: (needs: Record<string, string[]>, from: string, to: string) => boolean;
373
+ /** The first of `refs` an expression in its job reads, so cutting it would break the read. */
374
+ needInUse: (jobs: Record<string, unknown>, refs: NeedRef[]) => NeedRef | undefined;
20
375
  /** The job a `needs` entry points at, without a matrix's wait-for-any suffix. */
21
376
  needTarget: (need: string) => string;
377
+ isValidJobName: (name: string) => boolean;
378
+ jobYaml: (text: string, job: string) => string;
379
+ jobsYaml: (text: string, jobs: string[]) => string;
380
+ stepYaml: (text: string, job: string, index: number) => string;
381
+ stepsYaml: (text: string, refs: StepRef[]) => string;
382
+ /** What a pasted snippet holds: jobs, steps, or neither. */
383
+ snippetKind: (snippet: string) => 'jobs' | 'steps' | undefined;
384
+ /** Why moving `refs` into `job` would leave an expression reading something that isn't there. */
385
+ stepMoveProblem: (text: string, refs: StepRef[], job: string) => EditRefusalReason | undefined;
386
+ /** A job's matrix variables; null when the matrix is an expression, which may hold any. */
387
+ matrixNames: (job: unknown) => Set<string> | null;
22
388
  /** Jobs whose matrix runs more than once as `<job>-<n>` members, the way a run lists them. */
23
389
  expandMatrixJobs: <T>(jobs: Record<string, T>) => Record<string, T>;
24
390
  hasLayout: (layout: GraphLayout | undefined) => boolean;
@@ -29,4 +395,42 @@ export interface WorkflowEditing {
29
395
  together: (a: string, b: string) => boolean;
30
396
  alone: (job: string) => boolean;
31
397
  }) => LaidOutColumn[];
398
+ /** The grid as drawn, as a layout to store. */
399
+ layoutFromCols: (dependencyCols: string[][][], previous?: GraphLayout, rows?: number[][], columns?: number[]) => GraphLayout;
400
+ /** Where moved jobs land, and the needs the move would cut. */
401
+ moveJobs: (layout: GraphLayout, needs: Record<string, string[]>, jobs: string[], slot: GraphSlot, anchor?: string | undefined) => {
402
+ layout: GraphLayout;
403
+ cuts: NeedRef[];
404
+ };
405
+ /** Where an input of this type keeps the inputs it holds, if it holds any. */
406
+ inputChildrenKey: (type: unknown) => string | undefined;
407
+ isValidInputName: (name: string) => boolean;
408
+ newInputName: (existing: Iterable<string>) => string;
409
+ /** `base`, or `base_2`, `base_3`… when it's taken. */
410
+ nextName: (taken: Iterable<string>, base: string) => string;
411
+ /** The first of `candidate(1)`, `candidate(2)`… that `taken` doesn't hold. */
412
+ freeName: (taken: Iterable<string>, candidate: (n: number) => string) => string;
413
+ inputYaml: (text: string, path: InputPath) => string;
414
+ /** Whether a wizard's steps put their fields at the top level of the inputs. */
415
+ wizardFlattens: (schema: UnknownRecord) => boolean;
416
+ /** The input definitions of a parsed workflow. */
417
+ workflowInputsSchema: (workflow: UnknownRecord | undefined | null) => UnknownRecord | undefined;
418
+ /** The workflow's own settings as YAML: everything but its jobs and trigger. */
419
+ settingsYaml: (text: string) => string;
420
+ /** What's wrong with YAML text written as a job's, step's or input's settings, if anything. */
421
+ settingsYamlProblem: (snippet: string) => SettingsYamlProblem | undefined;
422
+ /** The first and last 1-based line of the node at `path`, its key included. */
423
+ linesAt: (text: string, path: YamlSegment[]) => {
424
+ start: number;
425
+ end: number;
426
+ } | undefined;
427
+ /** `value` with each whole reference to `find` replaced by `replace`. */
428
+ replaceReference: (value: string, find: string, replace: string) => string;
429
+ /** The workflow's problems its schema can't catch; none until the checks have loaded. */
430
+ lintWorkflow: (yaml: string, context?: WorkflowLintContext) => WorkflowLintProblem[];
431
+ /** Form definitions for picking a workflow file from a repository. */
432
+ WORKFLOW_FORMS: {
433
+ github: UnknownRecord;
434
+ };
32
435
  }
436
+ export {};
@@ -1,5 +1,5 @@
1
- import Editor from "./Editor.js";
2
1
  import { FieldWrapper } from "../form/FieldWrapper.js";
2
+ import Editor from "./Editor.js";
3
3
  import cx from "classnames";
4
4
  import { useMemo, useRef } from "react";
5
5
  import { jsx } from "react/jsx-runtime";
@@ -1,5 +1,11 @@
1
1
  import * as monaco from 'monaco-editor';
2
+ import { type NestedWorkflowText } from './nestedText.js';
2
3
  export type ISchemaError = monaco.editor.IMarker;
4
+ /** A problem to mark on one line of the editor. */
5
+ export interface EditorMarker {
6
+ line: number;
7
+ message: string;
8
+ }
3
9
  export interface IEditorProps extends monaco.editor.IStandaloneEditorConstructionOptions {
4
10
  className?: string;
5
11
  disabled?: boolean | undefined;
@@ -8,5 +14,13 @@ export interface IEditorProps extends monaco.editor.IStandaloneEditorConstructio
8
14
  width?: string | number;
9
15
  height?: string | number;
10
16
  path?: 'file:///workflow.yaml' | string;
17
+ /** Apply outside `value` changes as undoable edits instead of resetting the undo history. */
18
+ preserveUndo?: boolean;
19
+ /** Checks a whole workflow for what the schema can't, such as needs read but never listed. */
20
+ lint?: boolean;
21
+ /** Problems found elsewhere to mark in the text, such as a job's in the workflow around it. */
22
+ markers?: EditorMarker[];
23
+ /** Set when the text is one job or step, for completions that touch the rest of the workflow. */
24
+ nested?: NestedWorkflowText;
11
25
  }
12
- export default function MonacoEditor({ className, disabled, onChange, onValidate, width, height, path, ...options }: IEditorProps): import("react").JSX.Element;
26
+ export default function MonacoEditor({ className, disabled, onChange, onValidate, width, height, path, preserveUndo, lint, markers, nested, ...options }: IEditorProps): import("react").JSX.Element;
@@ -1,6 +1,11 @@
1
1
  import { useCssIsDark } from "../components/useCssIsDark.js";
2
+ import { HOST_MARKER_OWNER, LINT_OWNER } from "./lintOwner.js";
3
+ import { publishNestedText } from "./nestedText.js";
4
+ import { registerRevealer } from "./reveal.js";
2
5
  import { defineEditorThemes, getThemeName } from "./themes.js";
3
6
  import { setupMonacoWorkers } from "./workers.js";
7
+ import { holdListeners, modelFor } from "./editorModel.js";
8
+ import { lintContext, useLintEditing, useLintSources } from "./lintContext.js";
4
9
  import { ensureYamlLanguage } from "./yamlLanguage.js";
5
10
  import cx from "classnames";
6
11
  import { useEffect, useLayoutEffect, useRef } from "react";
@@ -8,20 +13,68 @@ import { jsx } from "react/jsx-runtime";
8
13
  import * as monaco from "monaco-editor";
9
14
  //#region src/editor/Monaco.tsx
10
15
  setupMonacoWorkers();
16
+ var SCHEMA_UNREADABLE = "768";
17
+ function lintMarker(model, { line, message }) {
18
+ const at = Math.min(Math.max(line || 1, 1), model.getLineCount());
19
+ return {
20
+ severity: monaco.MarkerSeverity.Error,
21
+ message,
22
+ startLineNumber: at,
23
+ startColumn: model.getLineFirstNonWhitespaceColumn(at) || 1,
24
+ endLineNumber: at,
25
+ endColumn: model.getLineMaxColumn(at)
26
+ };
27
+ }
28
+ function sameMarkers(a, b) {
29
+ return a.length === b.length && a.every((marker, i) => marker.message === b[i]?.message && marker.startLineNumber === b[i]?.startLineNumber);
30
+ }
31
+ function setMarkers(model, owner, wanted) {
32
+ if (!sameMarkers(monaco.editor.getModelMarkers({
33
+ resource: model.uri,
34
+ owner
35
+ }), wanted)) monaco.editor.setModelMarkers(model, owner, wanted);
36
+ }
11
37
  function editorText(value) {
12
38
  return value.replace(/\r/g, "");
13
39
  }
14
40
  var noop = () => {};
15
- function MonacoEditor({ className, disabled, onChange = noop, onValidate = noop, width = "100%", height = "100%", path = "file:///default", ...options }) {
41
+ function MonacoEditor({ className, disabled, onChange = noop, onValidate = noop, width = "100%", height = "100%", path = "file:///default", preserveUndo = false, lint = false, markers, nested, ...options }) {
16
42
  const isDark = useCssIsDark();
17
- const readOnly = disabled || options.readOnly;
43
+ const readOnly = Boolean(disabled || options.readOnly);
18
44
  const monacoRef = useRef(null);
19
45
  const isSyncingValueRef = useRef(false);
46
+ const lintSources = useLintSources();
47
+ const lintSourcesRef = useRef(lintSources);
48
+ lintSourcesRef.current = lintSources;
49
+ const runLintRef = useRef(null);
50
+ const unregisterRevealRef = useRef(null);
51
+ const listenersRef = useRef([]);
52
+ const nestedRef = useRef(nested);
53
+ nestedRef.current = nested;
54
+ const isNested = nested !== void 0;
55
+ const lintEditing = useLintEditing(lint);
56
+ const lintEditingRef = useRef(lintEditing);
57
+ lintEditingRef.current = lintEditing;
58
+ const lintRef = useRef(lint);
59
+ lintRef.current = lint;
60
+ useEffect(() => {
61
+ const model = monacoRef.current?.getModel();
62
+ if (!lint && model) setMarkers(model, LINT_OWNER, []);
63
+ else if (lint && lintEditing) runLintRef.current?.();
64
+ }, [lint, lintEditing]);
65
+ useEffect(() => {
66
+ const model = monacoRef.current?.getModel();
67
+ if (model) setMarkers(model, HOST_MARKER_OWNER, (markers ?? []).map((marker) => lintMarker(model, marker)));
68
+ }, [markers]);
69
+ useEffect(() => {
70
+ if (!isNested) return;
71
+ return publishNestedText(monaco.Uri.parse(path).toString(), nestedRef);
72
+ }, [path, isNested]);
20
73
  useEffect(() => {
21
74
  if (!monacoRef.current) return;
22
75
  defineEditorThemes(monaco.editor);
23
76
  monaco.editor.setTheme(getThemeName(isDark, disabled));
24
- monacoRef.current.updateOptions(readOnly === void 0 ? {} : { readOnly });
77
+ monacoRef.current.updateOptions({ readOnly });
25
78
  }, [
26
79
  disabled,
27
80
  isDark,
@@ -32,13 +85,23 @@ function MonacoEditor({ className, disabled, onChange = noop, onValidate = noop,
32
85
  if (!model || options.value === void 0 || editorText(model.getValue()) === editorText(options.value)) return;
33
86
  isSyncingValueRef.current = true;
34
87
  try {
35
- model.setValue(options.value);
88
+ if (preserveUndo) {
89
+ const editor = monacoRef.current;
90
+ editor?.pushUndoStop();
91
+ model.pushEditOperations([], [{
92
+ range: model.getFullModelRange(),
93
+ text: options.value
94
+ }], () => null);
95
+ editor?.pushUndoStop();
96
+ } else model.setValue(options.value);
36
97
  } finally {
37
98
  isSyncingValueRef.current = false;
38
99
  }
39
- }, [options.value]);
100
+ }, [options.value, preserveUndo]);
40
101
  useEffect(() => {
41
102
  return () => {
103
+ unregisterRevealRef.current?.();
104
+ for (const listener of listenersRef.current) listener.dispose();
42
105
  monacoRef.current?.dispose();
43
106
  monacoRef.current = null;
44
107
  };
@@ -56,15 +119,14 @@ function MonacoEditor({ className, disabled, onChange = noop, onValidate = noop,
56
119
  className: cx("relative border rounded", className),
57
120
  ref: (ref) => {
58
121
  if (ref && !monacoRef.current) {
59
- monaco.editor.getModel(monaco.Uri.parse(path))?.dispose();
60
122
  defineEditorThemes(monaco.editor);
61
123
  monaco.editor.setTheme(getThemeName(isDark, disabled));
62
- const model = monaco.editor.createModel(options.value ?? "", options.language, monaco.Uri.parse(path));
124
+ const model = modelFor(monaco.Uri.parse(path), options.value ?? "", options.language);
63
125
  const editor = monaco.editor.create(ref, {
64
126
  automaticLayout: true,
65
127
  model,
66
128
  ...options,
67
- ...readOnly === void 0 ? {} : { readOnly },
129
+ readOnly,
68
130
  fixedOverflowWidgets: true,
69
131
  suggest: {
70
132
  preview: true,
@@ -72,12 +134,54 @@ function MonacoEditor({ className, disabled, onChange = noop, onValidate = noop,
72
134
  showInlineDetails: true
73
135
  }
74
136
  });
75
- model.onDidChangeContent(() => {
76
- if (isSyncingValueRef.current) return;
77
- onChangeWrapper(editor.getValue());
78
- });
79
- model.onDidChangeDecorations(() => {
137
+ setMarkers(model, HOST_MARKER_OWNER, (markers ?? []).map((marker) => lintMarker(model, marker)));
138
+ setMarkers(model, LINT_OWNER, []);
139
+ const runLint = () => {
140
+ const editing = lintEditingRef.current;
141
+ if (!lintRef.current || !editing || model.isDisposed()) return;
142
+ const schema = monaco.editor.getModelMarkers({ resource: model.uri }).filter((marker) => marker.owner !== "workflowlint" && marker.owner !== "workflowmarkers" && String(marker.code ?? "") !== SCHEMA_UNREADABLE);
143
+ const source = model.getValue();
144
+ setMarkers(model, LINT_OWNER, schema.length > 0 ? [] : editing.lintWorkflow(source, lintContext(editing, source, lintSourcesRef.current, runLint)).map((problem) => lintMarker(model, problem)));
145
+ };
146
+ runLintRef.current = runLint;
147
+ let lintTimer;
148
+ const validate = () => {
149
+ runLint();
80
150
  onValidate(monaco.editor.getModelMarkers({ resource: model.uri }));
151
+ };
152
+ const modelListeners = [
153
+ model.onDidChangeContent(() => {
154
+ if (lintRef.current) {
155
+ window.clearTimeout(lintTimer);
156
+ lintTimer = window.setTimeout(runLint, 300);
157
+ }
158
+ if (isSyncingValueRef.current) return;
159
+ onChangeWrapper(editor.getValue());
160
+ }),
161
+ monaco.editor.onDidChangeMarkers((resources) => {
162
+ const uri = model.uri.toString();
163
+ if (resources.some((resource) => resource.toString() === uri)) validate();
164
+ }),
165
+ { dispose: () => window.clearTimeout(lintTimer) }
166
+ ];
167
+ holdListeners(model, modelListeners);
168
+ listenersRef.current = modelListeners;
169
+ validate();
170
+ unregisterRevealRef.current = registerRevealer(path, (start, end) => {
171
+ const last = Math.min(Math.max(start, end), model.getLineCount());
172
+ editor.revealLinesInCenterIfOutsideViewport(start, last);
173
+ editor.setPosition({
174
+ lineNumber: start,
175
+ column: model.getLineFirstNonWhitespaceColumn(start) || 1
176
+ });
177
+ const flash = editor.createDecorationsCollection([{
178
+ range: new monaco.Range(start, 1, last, 1),
179
+ options: {
180
+ isWholeLine: true,
181
+ className: "bg-(--theme-element)/20"
182
+ }
183
+ }]);
184
+ window.setTimeout(() => flash.clear(), 1500);
81
185
  });
82
186
  ensureYamlLanguage();
83
187
  monacoRef.current = editor;
@@ -0,0 +1,5 @@
1
+ import * as monaco from 'monaco-editor';
2
+ /** The model a new editor on `uri` edits, holding `value`. */
3
+ export declare function modelFor(uri: monaco.Uri, value: string, language: string | undefined): monaco.editor.ITextModel;
4
+ /** An editor's listeners on its model, dropped when the next editor takes the model over. */
5
+ export declare function holdListeners(model: monaco.editor.ITextModel, disposables: monaco.IDisposable[]): void;
@@ -0,0 +1,23 @@
1
+ import * as monaco from "monaco-editor";
2
+ //#region src/editor/editorModel.ts
3
+ var listeners = /* @__PURE__ */ new WeakMap();
4
+ /** The model a new editor on `uri` edits, holding `value`. */
5
+ function modelFor(uri, value, language) {
6
+ const existing = monaco.editor.getModel(uri);
7
+ if (existing && language === "yaml" && existing.getLanguageId() === "yaml") {
8
+ for (const listener of listeners.get(existing) ?? []) listener.dispose();
9
+ const owners = new Set(monaco.editor.getModelMarkers({ resource: uri }).map((marker) => marker.owner));
10
+ for (const owner of owners) monaco.editor.setModelMarkers(existing, owner, []);
11
+ existing.setValue(value);
12
+ existing.detectIndentation(true, 4);
13
+ return existing;
14
+ }
15
+ existing?.dispose();
16
+ return monaco.editor.createModel(value, language, uri);
17
+ }
18
+ /** An editor's listeners on its model, dropped when the next editor takes the model over. */
19
+ function holdListeners(model, disposables) {
20
+ listeners.set(model, disposables);
21
+ }
22
+ //#endregion
23
+ export { holdListeners, modelFor };
@@ -1,7 +1,11 @@
1
1
  export { default as Editor } from './Editor.js';
2
2
  export { default as EditorField, type IEditorField } from './EditorField.js';
3
3
  export { default as FormCodePanel } from './FormCodePanel.js';
4
+ export { HOST_MARKER_OWNER, LINT_OWNER } from './lintOwner.js';
4
5
  export type { IEditorProps, ISchemaError } from './Monaco.js';
6
+ export { type NestedWorkflowText, nestedWorkflowText } from './nestedText.js';
7
+ export { revealLines } from './reveal.js';
8
+ export { INPUT_YAML_PATH, JOB_YAML_PATH, type JsonSchema, SETTINGS_YAML_PATH, STEP_YAML_PATH, settingsSchemas, } from './settingsYaml.js';
5
9
  export { defineEditorThemes, getThemeName } from './themes.js';
6
10
  export { setupMonacoWorkers } from './workers.js';
7
11
  export { configureEditorYaml } from './yaml.js';