flexdesk 0.17.3 → 0.17.4

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 (47) hide show
  1. package/CHANGELOG.md +86 -65
  2. package/README.md +27 -4544
  3. package/css/base.css +18 -1
  4. package/css/flexdesk.css +18 -1
  5. package/dist/{chunk-4A7SJC3T.js → chunk-RKNFBX5K.js} +15 -4
  6. package/dist/{chunk-4A7SJC3T.js.map → chunk-RKNFBX5K.js.map} +2 -2
  7. package/dist/flexdesk.css +18 -1
  8. package/dist/tiles.js +1 -1
  9. package/dist/widgets.js +14 -2
  10. package/dist/widgets.js.map +2 -2
  11. package/docs/README.md +97 -0
  12. package/docs/best-practices.md +45 -0
  13. package/docs/components/charts.md +211 -0
  14. package/docs/components/command-palette.md +102 -0
  15. package/docs/components/conversation.md +72 -0
  16. package/docs/components/data-table-grid.md +328 -0
  17. package/docs/components/data-table.md +596 -0
  18. package/docs/components/editor.md +22 -0
  19. package/docs/components/flow-canvas-editor.md +482 -0
  20. package/docs/components/flow-editors.md +176 -0
  21. package/docs/components/flow-lane-editor.md +165 -0
  22. package/docs/components/flow-outline-editor.md +176 -0
  23. package/docs/components/forms-and-dialogs.md +294 -0
  24. package/docs/components/input-fields.md +72 -0
  25. package/docs/components/keymap.md +143 -0
  26. package/docs/components/layout-widgets.md +163 -0
  27. package/docs/components/markdown-and-outline.md +106 -0
  28. package/docs/components/menus.md +86 -0
  29. package/docs/components/notifications.md +194 -0
  30. package/docs/components/panels-and-desktops.md +127 -0
  31. package/docs/components/record-list-and-fact-table.md +128 -0
  32. package/docs/components/settings-view.md +112 -0
  33. package/docs/components/shell.md +189 -0
  34. package/docs/components/slide-out-panel.md +68 -0
  35. package/docs/components/tiles.md +210 -0
  36. package/docs/components/wizard.md +54 -0
  37. package/docs/concepts/building-and-packaging.md +33 -0
  38. package/docs/concepts/domain-neutrality.md +110 -0
  39. package/docs/concepts/host-port.md +137 -0
  40. package/docs/concepts/packages-and-entry-points.md +32 -0
  41. package/docs/concepts/peer-dependencies.md +42 -0
  42. package/docs/concepts/state-guard.md +58 -0
  43. package/docs/concepts/styling.md +47 -0
  44. package/docs/quickstart.md +179 -0
  45. package/package.json +2 -1
  46. package/src/ui/components/slide_out_panel.js +14 -3
  47. package/src/ui/utils/auto_scrollbars.js +16 -2
@@ -0,0 +1,176 @@
1
+ # Flow editors and the flow kit
2
+
3
+ > **Packages:** `@flexdesk/flow`, `@flexdesk/canvas` · **Editors:** [canvas](flow-canvas-editor.md), [outline](flow-outline-editor.md), [lanes](flow-lane-editor.md) · [All docs](../README.md)
4
+
5
+ This page introduces the flow editors and the kit they share. Each editor has its own page: the [canvas editor](flow-canvas-editor.md) (which stands on the `@flexdesk/canvas` kernel), the [outline editor](flow-outline-editor.md) and the [lane editor](flow-lane-editor.md).
6
+
7
+ ## Flow editors
8
+
9
+ *New in 0.6.0.* Both entries first ship in FlexDesk 0.6.0.
10
+
11
+ `@flexdesk/flow` holds three editors for flows — a graph of steps, each with
12
+ settings — and the kit they share: a **canvas** (nodes and lines), an
13
+ **outline** (the graph drawn as a list of blocks) and **lanes** (data flows,
14
+ left to right). All three are domain-neutral. An editor never fetches, never
15
+ saves and never starts a timer that writes: the step types, the reference
16
+ grammar, every finding and every byte of I/O are the consumer's, handed in as
17
+ options and handed back through callbacks. `@flexdesk/canvas` is the kernel
18
+ under the canvas editor (and under any other diagram), on its own so that a
19
+ diagram does not download the editors.
20
+
21
+ Nothing here changes an existing entry. Both are new entry points, and their
22
+ CSS is four new sections at the end of `flexdesk.css`, read only by the classes
23
+ these modules set.
24
+
25
+ ```js
26
+ import {
27
+ createStepCatalogue, createWidgetRegistry, createSettingsPanel,
28
+ TEMPLATE_REFERENCES, FlowHistory, bindFlowKeys,
29
+ } from '@flexdesk/flow';
30
+ ```
31
+
32
+ ## The kit
33
+
34
+ The parts every editor stands on, exported for a consumer that builds its own.
35
+ A test (`tests/flow_kit_vocabulary.test.mjs`) fails when a file under
36
+ `src/flow/` or `src/canvas/` names a consumer's vocabulary, calls `fetch` or
37
+ touches browser storage — and is shown to fail on a planted one.
38
+
39
+ | Part | What it does |
40
+ |---|---|
41
+ | `createStepCatalogue(types, {categories, idBase})` | The step types the consumer passes in, read as they arrive (`type_id`, `label`, `category`, `role`, `description`, `icon`, `ports`, `config_schema`, `unavailable`; any other key is kept). `get`, `list`, `byCategory`, `role`, `tone`, `inputs(typeId, {flow})`, `outputs`, `port`, `idBase`, and `problems` — a broken type is listed there, never thrown. A type may also carry `render`, `update`, `size: {w, h}` and `accent` (a tone), which only the canvas editor reads ([Node bodies](flow-canvas-editor.md#node-bodies-0170)); `size(typeId)`, `accent(typeId)` and `body(typeId)` answer them, `null` when a type has none, and a `size`, `accent` or hook of the wrong kind is a line in `problems` and is not used (0.17.0). `inline: ['key', …]` names settings of its `config_schema` the canvas editor draws on the node ([Edited on the node](flow-canvas-editor.md#edited-on-the-node-0170)); `inline(typeId)` answers the names the schema has, `[]` when none, and a name it lacks (or an `inline` that is not a list) is a line in `problems` and is not drawn (0.17.0). `rank: 'source' \| 'sink'` is the hint the canvas editor's `layout: 'layered'` Arrange gives the type's steps (the first or the last column); `rank(typeId)` answers it, `null` when a type has none, and any other value is a line in `problems` and is not used (0.17.0). **Ports keep their declared order**: a lane reads the first input as its own and an outline draws the other outputs as arms in order, so nothing sorts them |
42
+ | `checkSettingsSchema(schema)` | The JSON-Schema subset a step's settings may use: `type`, `enum`, `const`, `required`, `properties`, `additionalProperties` (a boolean), `items`, `minItems`/`maxItems`, `minimum`/`maximum`, `minLength`/`maxLength`, `format: "uuid"`, the annotations and any `x-` hint. Returns the first refusal as the server's own sentence, or `null`. `oneOf`, `anyOf`, `$ref` and `pattern` are refused. `tests/fixtures/flow_kit_settings_schema.json` holds 30 schemas and their verdicts. Tables' own `check_schema` gave the same 30 verdicts when the file was written, and is meant to read this file in a test of its own |
43
+ | `createWidgetRegistry()` | The controls, by name. The ten generic widgets are registered already; `register(name, widget)` throws for a name that exists, and replacing one takes `{replace: true}`. A widget is `(spec, value, ctx) => {el, destroy?, focus?, caret?, restoreCaret?}` and reports with `ctx.set(value)`; `undefined` removes the setting |
44
+ | `createSettingsPanel(options)` | One step's settings, or the flow's own, drawn from the schema (below) |
45
+ | `createReferenceSyntax({name, pattern, format, describe, stepScope})` | What a chip is. Three are ready: `TEMPLATE_REFERENCES` (`${a.b[0]}`), `FORMULA_REFERENCES` (`[a.b]`), `PARAMETER_REFERENCES` (`{{name}}`). `find`, `format`, `rename(text, map)` and the consumer's `describe(match, ctx) → {label, tone, known}` |
46
+ | `createChipInput({value, syntaxes, multiline, readOnly, onInput})` | Text with references drawn as chips. **The value is always the plain text**; a chip is a view of it |
47
+ | `openStepPicker({anchor, entries, categories, where, paste})` | What "+" opens: grouped, searchable, ↑ ↓ and Enter; a refused entry is shown greyed with its reason. Resolves `{entry}`, `{paste: true}` or `null` |
48
+ | `openValuePicker({anchor, groups, note, onPick})` | *Insert a value*: the consumer's groups of values, a level down for a value with children (`body ›`), searchable across levels |
49
+ | `alwaysBefore(graph, catalogue, stepId, {loopPorts, waitsForAll})` | The steps that run before this one on every path, nearest first: the only ones *Insert a value* should offer. A parallel whose merge waits for every branch counts each branch's steps; a loop's body never counts for a step after the loop. `enclosingLoops` lists the loops around a step |
50
+ | `groupFindings(list)`, `createFindingsStrip({onGoTo, nameOf})` | The consumer's findings (`{code, message, severity, node_id?, field?}`) by step, and the strip: *"N things to fix before this can be published."*, a line each, *Go to it* |
51
+ | `new FlowHistory({actions, restore, onState, limit, mergeMs})` | Undo and redo over whole snapshots. The editor passes its own list of actions and `commit` throws on any other, so an edit nobody named fails its first test. Loading and publishing are `baseline`s, never entries. Keyed commits within `mergeMs` merge (typing a name is one entry), never across an undo. `keep()` and `adopt(kept, state)` carry the stacks to a successor that loaded the same bytes |
52
+ | `bindFlowKeys(root, handlers)`, `ownsUndo(target)` | An editor's keys, bound on its root, never on `window` (below) |
53
+ | `emptyGraph`, `normalise`, `serialise`, `nextId`, `addNode`, `removeNode`, `connect`, `addConnection`, `canConnect`, `disconnect`, `inputNamesOf` | A logic flow's graph — `{nodes: [{id, type, label?, config, position}], connections: [{source, target, sourcePort, targetPort}]}`, the shape the canvas and the outline both save. `serialise` is byte-stable: a graph opened and saved with no edit is the same text. `canConnect(graph, catalogue, edge, {strings, dataRead, dataPorts})` refuses only what an editor cannot draw; with `dataPorts: true` a data port connects to a data port, any number of lines out of one. `connect` adds the line when `canConnect` allows it; `addConnection(graph, edge)` adds it asking nothing, for a caller whose own rules already allowed it. It may also carry `groups: [{id, label?, tone?, nodes: [id, …]}]` and `notes: [{id, text, position, size?: {w?, h?}}]` (0.17.0), and only when it has some, so a graph without them is the same text as before. Groups are flat: `normalise` keeps a node in the first group that names it, leaves out ids that are not nodes, and drops a group with no member; `removeNode` takes the node out of its group, and the group with its last member |
54
+ | `createStrings(...layers)`, `FLOW_STRINGS` | Every word drawn, English and domain-free by default; an editor lays its own words over these, and the consumer lays its words over both |
55
+
56
+ **The settings panel.**
57
+
58
+ ```js
59
+ const panel = createSettingsPanel({
60
+ widgets, references: { template: TEMPLATE_REFERENCES },
61
+ onChange: (stepId, key, value) => apply(stepId, key, value), // you write it into your graph
62
+ onRename: (stepId, label) => rename(stepId, label),
63
+ onFocusLost: () => surface.focus(),
64
+ values: async ({ stepId, field }) => groupsFor(stepId), // Insert a value; absent, no "{ }"
65
+ columns: (step) => inputColumnsOf(step), // upstream-column(s); null in a logic flow
66
+ host,
67
+ });
68
+ panel.show({ step, type, title, typeLabel, where, description, idLine, icon, tone,
69
+ rename: true, extra: [...], actions: [...], slots: { before, after } });
70
+ panel.setFindings(findings); // under each field, the rest at the top; rebuilds nothing
71
+ panel.focusField('retry.max_attempts');
72
+ panel.repaintSlots(); // your areas only; no field is rebuilt
73
+ ```
74
+
75
+ It never writes into the step: a change is reported to `onChange`, and the
76
+ consumer writes it. A redraw puts focus back on the rebuilt control of the same
77
+ field, with the caret where it was, or hands it to `onFocusLost`, never to
78
+ `<body>`. Findings and slot redraws rebuild no field, so a run overlay that
79
+ refreshes every second leaves a name being typed alone. A control that cannot
80
+ be used is disabled and its reason is shown next to it, not only in a tooltip.
81
+ `extra` holds the editor's own fields (a port shown as a setting) and notes,
82
+ placed with `after: '<key>'` or `at: 'start'`.
83
+
84
+ **The widgets.** A schema type with no `x-ui-widget` draws its plain control: an
85
+ `enum` a select (writing the enum's own value, so a number stays a number), a
86
+ string a text box, a number a number box, a boolean a checkbox, an object with
87
+ properties a group of its own fields. A control never writes a value the reader
88
+ did not give it. An untouched setting stays absent, and emptying one removes it.
89
+ A list or map entry keeps its stored type when another entry is edited. A widget
90
+ name nobody registered is refused on screen, by name, and its value is kept.
91
+
92
+ **A widget is built before it is in the document.** The panel calls the widget,
93
+ then attaches what it returns, and a field inside a folded (`x-ui-fold`) group
94
+ is built inside a closed `<details>`. A widget that measures itself — a Monaco
95
+ editor registered as `expression` — must create that editor once its element is
96
+ connected and has a size (a `ResizeObserver` on `el` will tell it), never inside
97
+ the widget call: an editor built into a box with no size measures a zero
98
+ character width and paints a viewport of no lines, and nothing repaints it.
99
+
100
+ | Generic widget | Draws |
101
+ |---|---|
102
+ | `template` | Text with chips and *Insert a value*. A setting that may also be a list or an object is shown as JSON and written back as one when the text parses |
103
+ | `expression` | A formula: monospace, with chips. Replace it with your own editor (`{replace: true}`) |
104
+ | `json-body` | JSON when the text parses, text otherwise |
105
+ | `key-value-map` | Names to values |
106
+ | `key-value-list` | Ordered assignments: `{variable, value}` or `{variable, expression}` |
107
+ | `string-list` | Short strings |
108
+ | `enum-chips` | A multi-select of `items.enum`, as chips, in the enum's order |
109
+ | `choice-cards` | One of N as radio rows, each with its words |
110
+ | `upstream-column`, `upstream-columns` | One or several columns of the step's input, by name, from the panel's `columns`. While `columns` answers `null` (the columns are not known yet) the name is typed instead — a text box, or a chip list that takes a name on Enter or a comma — under the hint *Columns not known yet*; once columns arrive the same setting is a select (or toggle chips) with the stored name chosen, and a name the columns lack is kept and marked *(not found)* (0.17.0) |
111
+
112
+ | Schema hint | Meaning |
113
+ |---|---|
114
+ | `x-ui-widget` | The widget that draws the field |
115
+ | `x-ui-references` | The syntaxes its chips are recognised in, by name. Without it, `template`, `json-body` and the two key-value widgets use `template`, and `expression` uses `formula` |
116
+ | `x-ui-when` | `{field, in}`: drawn only while that sibling holds one of those values (its stored value, else its default) |
117
+ | `x-ui-placeholder`, `x-ui-multiline` | A placeholder; a string as a growing textarea |
118
+ | `x-ui-enum-labels`, `x-ui-enum-descriptions` | An enum's words, and a sentence under each choice card |
119
+ | `x-ui-fold` | An object drawn folded, with a one-line summary of what is set in it |
120
+ | `x-ui-add-label` | The words on a list's *Add* button |
121
+ | `x-ui-formula-references` | `key-value-list`: the syntaxes of a formula row (default `formula`) |
122
+
123
+ **References and chips.** A syntax recognises references in order to draw them.
124
+ It is not a grammar. A reference your server would refuse is still drawn as a chip
125
+ (`describe` can mark it `known: false`) and kept as text. Each syntax has a
126
+ default tone for its chips (`PARAMETER_REFERENCES` is violet, the others blue),
127
+ so two syntaxes in one field read as two kinds of chip. In the chip input the
128
+ browser does the typing, so an IME works. After every input the text is read
129
+ back and re-tokenised. Everything else the control does on the text itself:
130
+ ← and → step over a chip, one Backspace removes a whole chip, a paste is plain
131
+ text, a copy puts the exact text on the clipboard, and Enter adds a line only in
132
+ a multi-line field. It keeps its own undo, because a script that redraws a
133
+ `contenteditable` breaks the browser's. `rename(text, map)` rewrites only the
134
+ path segment that names a step, and only once the syntax is told which segment
135
+ that is (`stepScope: 'steps'`, or a `stepSegment` function). A syntax nobody
136
+ configured renames nothing.
137
+
138
+ **Keys.** `bindFlowKeys(root, handlers)` binds Ctrl/⌘+Z (`undo`), Ctrl/⌘+Y and
139
+ Ctrl/⌘+Shift+Z (`redo`), Delete (`remove`), Ctrl/⌘+D, C, X and V (`duplicate`,
140
+ `copy`, `cut`, `paste`), Ctrl/⌘+A (`selectAll`, 0.17.0), Ctrl/⌘+G and Ctrl/⌘+Shift+G (`group`, `ungroup`, 0.17.0), F2 (`rename`), Alt+↑, Alt+↓ and Alt+← (`moveUp`,
141
+ `moveDown`, `moveOut`), the arrows (`up`, `down`, `left`, `right`), Enter
142
+ (`open`) and Escape (`escape`), and returns an unbind. A key whose handler is
143
+ absent, or returns `false`, is not taken. An editor's own row or card marks
144
+ itself `data-twm-flow-item`, so Enter on it opens it. Inside a text field Ctrl+Z is the field's own, but it is
145
+ still stopped at the editor's root, so a tile beside the editor never hears it.
146
+ In a field every other key belongs to the field, and Enter on a button belongs
147
+ to the button. **Backspace is never an editor key.** It is not handled and not
148
+ stopped, because a host may use it to go back a level.
149
+
150
+ **Tones and strings.** A category names a tone (`violet`, `teal`, `amber`,
151
+ `grey`, `blue`, `indigo`), and `tokens.css` maps each tone to colours:
152
+ `--twm-flow-<tone>-bg`, `-fg`, `-line`, `-icon-bg`, `-text-bg` and `-dot`.
153
+
154
+ **What it does not do.** It does not validate a value against its schema, decide
155
+ whether a reference is valid, save, fetch, or keep anything between page loads.
156
+ Those belong to the consumer.
157
+
158
+ **Where each claim was checked.** jsdom computes no layout and does no editing,
159
+ so the suites (`tests/flow_kit_*.test.mjs`) assert the DOM, the events and the
160
+ text. The rest was checked in headless Edge with real input over the DevTools
161
+ protocol: `demo/flow_kit_probe.mjs` serves the repository, opens
162
+ `demo/flow_kit.html` and drives it (`demo/flow_cdp.mjs` is the small driver it
163
+ uses, for the editors' pages too). The demo pages load the source barrels;
164
+ `FLOW_DIST=1 node demo/flow_<kit|canvas|outline|lanes>_probe.mjs` redirects
165
+ `flow.js`, `canvas.js` and the three sheets to `dist/`, so the same checks run
166
+ against the built bundle. All four probes passed both ways on the integration
167
+ build. In the chip input, typing on either side of
168
+ a chip, ← and → over it, one Backspace removing it, a real paste of `${a.b}`
169
+ becoming a chip, a real copy giving the exact text, an IME composition leaving
170
+ the text alone until it committed, and the field's own Ctrl+Z all passed. The
171
+ same Backspace in a plain `contenteditable` whose chips are not
172
+ `contenteditable="false"` split its chip. A pointer drag across a chip started
173
+ no native drag. `elementFromPoint` at the centre of Undo, "+", the panel's
174
+ first field, its title, a field's "{ }", an action, a choice and the strip's
175
+ *Go to it* returned each one. Both pickers opened inside the viewport, their
176
+ search box focused, and gave the focus back to "+" on Escape.
@@ -0,0 +1,165 @@
1
+ # Flow lane editor
2
+
3
+ > **Package:** `@flexdesk/flow` · **Shared kit:** [Flow editors and the flow kit](flow-editors.md) · [All docs](../README.md)
4
+
5
+ ## The lane editor
6
+
7
+ `createLaneEditor(host, options)` draws a data flow as lanes, left to right.
8
+ Every source starts a lane, and each step follows the one before it. A join
9
+ brings another lane in from below, and its line is laid out for you. Cards sit
10
+ where the layout puts them: **nothing is dragged and no line is drawn by
11
+ hand.** A step is added with the "+" on a card's right tip, which splices it in
12
+ between the card and what the card led to (`A → new → B`). *Add a source* starts
13
+ a new lane. A join's other lanes are settings (*Joined with*), not lines. Under
14
+ the lanes are a tab per step and a dock: the kit's settings panel on the left,
15
+ and on the right what the step gives — **Preview**, **Columns** and, for a sink,
16
+ **Would be rejected**. A toolbar toggle, **Steps show: The preview | The last
17
+ run**, picks what each card's status line says.
18
+
19
+ ```js
20
+ import { createLaneEditor, createStepCatalogue, createWidgetRegistry, PARAMETER_REFERENCES } from '@flexdesk/flow';
21
+
22
+ const editor = createLaneEditor(host, {
23
+ catalogue: createStepCatalogue(types, { categories }), // roles: source | transform | operation | sink
24
+ widgets: createWidgetRegistry(),
25
+ references: { parameter: PARAMETER_REFERENCES },
26
+ summarise: (step, type) => '…', // a card's second line
27
+ preview: async ({ pipeline, text, parameters, signal }) => ({ nodes: { [id]: {
28
+ status, rows, total, columns: [{ name, type }], head: [[…]], caption, rejects, error } } }),
29
+ describe: async ({ pipeline, text, parameters, signal }) => ({ nodes: { [id]: {
30
+ columns: [{ name, type, origin, change }] } } }),
31
+ lastRun: async ({ pipeline, signal }) => runOverlay, // asked when Steps show turns to The last run
32
+ actions: [{ id: 'publish', label: 'Publish', primary: true, run }],
33
+ onChange: ({ pipeline, text, action, key }) => save(text), // after every edit, undo and redo
34
+ });
35
+ editor.load({ graph: pipeline });
36
+ ```
37
+
38
+ It edits the data flow's JSON — `{nodes: [{id, type, label?, config, position?}],
39
+ connections: [{id, sourceId, sourcePort, targetId, targetPort}], parameters:
40
+ {name: {type, default?, description?}}}` — with two rules added. A step's name
41
+ is a top-level `label`, never a key inside `config`. Connection ids are
42
+ deterministic: `c1`, `c2`, … the lowest free number. `serialisePipeline` is
43
+ byte-stable, so a flow opened and saved with no edit is the same text. Keys the
44
+ editor does not know are kept, and a node's `position` is kept and never used.
45
+ **A step's ports keep their declared order**: the first FLOW input is the lane
46
+ input, and every further input takes another lane. The first output is the
47
+ continuation.
48
+
49
+ **The layout** is `layoutLanes(pipeline, catalogue)` and `placeLanes(layout,
50
+ size)`. Both are pure, and the wires are computed from the grid, never measured
51
+ from the page. A step belongs to the lane that feeds its lane input, not to
52
+ whichever lane reaches it first. An output that feeds two steps' lane inputs
53
+ forks a new lane below. A feeder lane is moved right until its last card sits
54
+ one column before the join it feeds, and never so far that the join moves.
55
+ Under each lane come its feeders, then its forks. A wire that would run through a card that is not one of its ends (a feeder that goes on past its feed, a fork drawn under a feeder) goes round by the gutters, the gaps between columns and between lanes, which hold no card. A step two lines feed on one
56
+ input (`lanes_two_inputs`), a cycle (`lanes_cycle`) or two steps with one id
57
+ (`lanes_duplicate_step`) cannot be drawn honestly as lanes. Such a flow opens
58
+ read only with the sentence, is drawn as well as it can be, and is never
59
+ repaired. The sizes are `LANE_GEOMETRY`: `regular` is 200 × 62 (the editor),
60
+ `small` is 160 × 50 (two lines a card, for a narrower host) and `strip` is
61
+ 92 × 26 (`compact`). The pure edits — splice, add a source, remove, duplicate,
62
+ set an input, the input candidates, parameters — are `lanePipeline`.
63
+ `lanePipeline.duplicateStep(pipeline, catalogue, id)` (0.17.0) puts a copy right
64
+ after the step: a fresh id, a label derived from its title (`Title (copy)`, then
65
+ `(copy 2)`, …, words in `LANE_PIPELINE_STRINGS`), a deep copy of its config and
66
+ the same lines in, none out. It answers `{ok: true, node}`, or `{ok: false,
67
+ reason}` and changes nothing when there is no such step or its type is
68
+ `unavailable`.
69
+
70
+ | Option | Meaning |
71
+ |---|---|
72
+ | `catalogue` | The kit's step catalogue (or a list of types and `categories`) |
73
+ | `widgets`, `references`, `services`, `strings` | As for the kit's panel. `references.parameter` decides how the parameters strip spells a reference |
74
+ | `values` | *Insert a value*: `({pipeline, stepId, field, key, upstream, parameters}) => groups`. `upstream` is every step whose output reaches this one, nearest first |
75
+ | `summarise(step, type)`, `summariseReads(step, type)` | A card's second line; the panel's id line |
76
+ | `preview`, `describe` | The providers (above). Absent, the dock has no Preview, or no Columns |
77
+ | `previewDelayMs` | The debounce, 800 by default |
78
+ | `lastRun` | Asked once, when *Steps show* first turns to *The last run* and no overlay is set |
79
+ | `flowSettings` | `{schema, value}`: the flow's own settings, under a first tab, *The flow* |
80
+ | `readOnly` | `false`, or `{reason}`, drawn above the editor |
81
+ | `actions` | Toolbar verbs after Undo and Redo: `[{id, label, icon?, primary?, run}]` |
82
+ | `slots` | `toolbarStart(el)`, `toolbarEnd(el)`, `flowPanel(el)`, `stepPanel(el, step)` |
83
+ | `size` | `'regular'` or `'small'` |
84
+ | `stepTabs`, `parameters` | `false` leaves out the step tabs or the parameters strip |
85
+ | `height` | `'fill'` (the default: the editor fills its host, which must have a height) or `'auto'` (the editor flows, and the dock is `dockHeight`, 360 by default) |
86
+ | `compact` | `true` (or `{size: 'small'}`): the lanes alone, read only, nothing to press. This is what a step that runs a data flow shows as *What it does, left to right* |
87
+ | `parameterTypes` | The parameter types a parameter may be: `text`, `number`, `integer`, `boolean`, `date` by default |
88
+ | `select` | The step chosen on load (the first step, left to right, by default) |
89
+ | `history` | `{limit, mergeMs}` |
90
+ | `menuItems(step)` | Your own items on a card's menu, after the built-in ones: `[{id, label, icon?, danger?, disabled?, reason?, run}]`. `run({step, pipeline, editor})` gets a DRAFT of the flow to change in place (it may answer later, with a promise); what it changed is one undoable edit, `flow:step:menu`. Answering `false` takes nothing, and neither does a draft that answers after another edit. `disabled` greys the item with its `reason`; a read-only flow greys every one with the read-only reason (0.17.0) |
91
+ | `onChange`, `onSelect({kind, id})`, `onStepsShow(mode)` | Callbacks |
92
+
93
+ | Method | Does |
94
+ |---|---|
95
+ | `load({graph, flowSettings}, {baseline = true})` | Replaces the content. A baseline clears both undo stacks, and a preview is asked for at once |
96
+ | `getGraph()`, `serialise()`, `getFlowSettings()` | A copy of the flow; its byte-stable text |
97
+ | `setFindings(list)` | On the card (its status line becomes the message), under the field, and in the strip with *Go to it* |
98
+ | `setRunOverlay({steps: {[id]: {state, line, tone}}, banner})` | The last run, shown when *Steps show* is *The last run*. `line` is drawn as given |
99
+ | `setStepsShow('preview' \| 'run')`, `stepsShow` | — |
100
+ | `setReadOnly(false \| {reason})`, `readOnly` | — |
101
+ | `setActionState(id, {disabled, reason, busy})`, `setStatus(text)` | A refused verb shows its reason beside it |
102
+ | `select(id \| 'flow' \| null)`, `selected`, `focus()` | — |
103
+ | `undo()`, `redo()`, `history` | Over whole snapshots. The actions are `LANE_ACTIONS` (`flow:step:add`, `:remove`, `:label`, `:config`, `:duplicate`, `:menu`, `flow:join:set`, `flow:source:add`, `flow:parameter:add`, `:change`, `:remove`, `flow:settings`) |
104
+ | `refreshPreview()` | Asks the providers now, without the debounce |
105
+ | `inputColumns(stepId, port?)` | The columns of the frame a step reads, from the last description, else the last preview. The `upstream-column(s)` widgets read the same |
106
+ | `layout`, `placed` | The current layout and its coordinates |
107
+ | `destroy()` | Removes every listener, popover and table, and aborts the preview in flight |
108
+
109
+ **Columns that arrive while you are in the settings** are drawn when the focus
110
+ leaves them, so a column typed by name before the columns were known becomes a
111
+ choice then, and nothing is rebuilt under the reader's caret.
112
+
113
+ **The preview** is one request for the whole flow to each provider. It is sent
114
+ 800 ms after the last edit and once after a load. A rename does not send one,
115
+ because it changes no row. Every request carries a sequence number and an
116
+ `AbortSignal`. A newer edit aborts the request in flight, and an answer that is
117
+ not the newest is dropped even when it arrives. A failure is shown as the
118
+ provider's own sentence in the dock, and nothing else changes. The rows are a
119
+ FlexDesk `DataTable` in a box with a definite height. How many rows a preview
120
+ reads, as whom it reads them and that it writes nothing are all the provider's
121
+ business; the editor knows only what it is handed.
122
+
123
+ **Keys.** These are the kit's, on the editor's root. ← → ↑ ↓ move between cards
124
+ and between the step tabs. Enter opens a step's settings on their first field,
125
+ and Escape goes back to the card. Delete removes the step, Ctrl/⌘+D duplicates
126
+ it (the copy is chosen and focused) and F2 renames it.
127
+ Ctrl/⌘+Z and Ctrl/⌘+Y undo and redo, and are stopped at the editor even inside
128
+ a field. The context-menu key or a right press opens a card's menu: *Open its
129
+ settings*, *Add a step after it*, *Duplicate*, *Remove step*, then your
130
+ `menuItems`. An item that cannot be chosen — *Duplicate* on a read-only flow, or
131
+ on a step whose type is not available — is greyed with its reason in its own
132
+ line, and a refused Ctrl/⌘+D says the reason beside the status. Backspace is never the
133
+ editor's. The cards are one Tab stop, the chosen one, and its "+" comes next in
134
+ the Tab order. The "+" shows on hover, on focus and on the chosen card, never
135
+ on hover alone.
136
+
137
+ **What it does not do.** It does not fetch, save, validate, propagate a schema,
138
+ coerce a value or run anything. Steps are never dragged and lines are never
139
+ drawn by hand. The editor does not draw a page trail, a breadcrumb or a "you
140
+ are editing the shared flow" banner either: a host draws those around it.
141
+
142
+ **Where each claim was checked.** The layout, the JSON, the edits and the
143
+ preview runner are pure, and `tests/flow_lanes_layout.test.mjs`,
144
+ `flow_lanes_pipeline.test.mjs` and `flow_lanes_preview.test.mjs` test them under
145
+ plain node. The layout suite reproduces the mock and shows that the earlier
146
+ layout (EcoSim's `buildLanes`, copied verbatim) puts the join in the feeder's
147
+ lane when the feeder is listed first. The preview suite has a provider that
148
+ answers out of order, and the stale answer is shown to be dropped.
149
+ `flow_lanes_editor.test.mjs` mounts the editor in jsdom: choosing a card
150
+ rebuilds no node, every edit is one undo entry, and a flow that cannot be drawn
151
+ is never written. `flow_lanes_css.test.mjs` holds the classes and the rules to
152
+ each other and the sheet's card sizes to `LANE_GEOMETRY`. The rest was checked
153
+ in headless Edge with real input (`node demo/flow_lanes_probe.mjs`, which opens
154
+ `demo/flow_lanes.html` at all three sizes). In a laid-out page every wire
155
+ starts at its card's tip and ends in the next card's notch, and the join's line
156
+ and its port dot meet the join's bottom centre. `elementFromPoint` at the centre
157
+ of Undo, a card, a "+", *Add a source*, a step tab, *Steps show*, a parameter,
158
+ the panel's first field, its name, *Remove step*, a dock tab and a preview cell
159
+ returns each one, and the settings are covered by nothing. The preview's
160
+ `DataTable` draws its rows with a height, and every card's words stay inside
161
+ the card. A real press on "+" opens the picker inside the viewport with its
162
+ search focused, and real keys filter it and add the step. A real drag across a
163
+ card starts no native drag (a `dragstart` counter at 0) and moves nothing. Real
164
+ arrows, Delete, Ctrl+Z and Backspace do what is said above, and three real
165
+ keystrokes ask the preview once.
@@ -0,0 +1,176 @@
1
+ # Flow outline editor
2
+
3
+ > **Package:** `@flexdesk/flow` · **Shared kit:** [Flow editors and the flow kit](flow-editors.md) · [All docs](../README.md)
4
+
5
+ ## The outline editor
6
+
7
+ `createOutlineEditor` draws a logic flow as a list of blocks: *If … otherwise*,
8
+ *For each row* with its *Next row* foot, *At the same time* with its *Then wait
9
+ for every branch* foot, and a step's other ports (an error port, an empty-result
10
+ port) as arms under it. **The outline is the graph.** It reads the same
11
+ `{nodes, connections}` JSON the canvas edits, holds no second model, and every
12
+ edit is an operation on that graph. A graph opened and saved with no edit is the
13
+ same text, byte for byte, and node positions are kept and never used. A graph
14
+ that is not block-shaped is refused, never repaired: it opens read-only, as a
15
+ flat list in the order it runs, with the reason above it.
16
+
17
+ ```js
18
+ import { createOutlineEditor, createStepCatalogue, createWidgetRegistry, TEMPLATE_REFERENCES } from '@flexdesk/flow';
19
+
20
+ const editor = createOutlineEditor(host, {
21
+ catalogue, widgets: createWidgetRegistry(), references: { template: TEMPLATE_REFERENCES },
22
+ blocks, // which roles are blocks, and their words (below)
23
+ values: async ({ stepId, field, before, loops }) => groupsFor(before, loops), // Insert a value
24
+ valuesNote: 'Only steps that always run before this one are listed.',
25
+ summarise: (node, type) => oneLine(node), // a row's line; {text, mono: true} for code
26
+ summariseReads: (node, type) => howLaterStepsRead(node), // the panel's id line
27
+ flowSettings: { schema, value, title, typeLabel, where, description, idLine },
28
+ actions: [{ id: 'publish', label: 'Publish', icon: 'publish', primary: true, run: () => publish() }],
29
+ slots: { toolbarEnd: (el) => {}, flowPanel: (el) => {}, stepPanel: (el, node) => {} },
30
+ initialFolds: saved, onFoldChange: (ids) => keep(ids),
31
+ onChange: ({ graph, flowSettings, text, action, key }) => autosave(text),
32
+ onSelect: ({ kind, id }) => {}, // kind: 'flow' or 'step'
33
+ readOnly: false, // or {reason}
34
+ history: { limit: 100, mergeMs: 1000 },
35
+ });
36
+ editor.load({ graph, flowSettings }); // a baseline: nothing to undo
37
+ ```
38
+
39
+ | Method | Does |
40
+ |---|---|
41
+ | `load({graph, flowSettings}, {baseline = true})` | Replaces the content; a baseline clears both undo stacks and emits nothing |
42
+ | `getGraph()`, `serialise()`, `getFlowSettings()` | A copy of the graph; its byte-stable text; the flow's own settings |
43
+ | `setFindings(list)` | A finding with a `node_id` replaces that row's line with its message, in the error or warning tone, marks the row, and is drawn under its field; the others go to the strip, with *Go to it* |
44
+ | `setRunOverlay({steps, ports, banner} \| null)` | A run drawn on the outline (below) |
45
+ | `setReadOnly(false \| {reason})`, `setActionState(id, {disabled, reason, busy})`, `setStatus(text)` | A refused verb is disabled with its reason beside it |
46
+ | `select(id \| 'flow' \| null)`, `focus()`, `undo()`, `redo()`, `history`, `folds`, `setFolds(ids)` | `select` unfolds the blocks around the step |
47
+ | `describe()`, `refusal` | The outline as text (`describeOutline`), or why the graph could not be drawn as one |
48
+ | `destroy()` | Removes every listener, popover and ghost. It flushes nothing, because it holds nothing |
49
+
50
+ **The block mapping.** The consumer says which catalogue roles are blocks and
51
+ what their parts are called. Nothing here guesses a domain:
52
+
53
+ ```js
54
+ const blocks = {
55
+ start: { role: 'start', label: 'When it runs' }, // the flow row
56
+ end: { role: 'end', entry: { label: 'End the run', sub: 'End' } },
57
+ step: { input: 'in', continue: 'out' }, // an ordinary step's two ports
58
+ branch: { role: 'branch', arms: { true: 'Then', false: 'Otherwise' },
59
+ entry: { label: 'If … otherwise', sub: 'Condition' }, note: 'Either arm may be empty.' },
60
+ fanout: { role: 'fanout', join: { role: 'join', type: 'merge', field: 'join',
61
+ foot: { all: 'Then wait for every branch', any: 'Then go on when the first finishes' } },
62
+ arm: 'Branch {n}', addArm: 'Add a branch', entry: { label: 'At the same time', sub: 'Parallel + Merge' } },
63
+ loop: { role: 'loop', ports: { entry: 'in', next: 'next', body: 'body', done: 'done' },
64
+ foot: 'Next row', skip: 'Go on with the next row', addInside: 'Add a step to the loop',
65
+ entry: { label: 'For each row', sub: 'Loop over rows' } },
66
+ arms: { error: { label: (step) => `If ${step.label} fails`, unconnected: 'fail',
67
+ setting: { label: 'If it fails', unconnected: 'Fail the run', after: 'timeout_seconds',
68
+ connected: (arm) => `Run the steps under “${arm}”` } } },
69
+ types: { 'http-request': { arms: { error: { label: 'If the request fails' } } } },
70
+ };
71
+ ```
72
+
73
+ `unconnected` is what your runtime does with a port nothing is connected to,
74
+ `'fail'` or `'stop'`, and only you can say it. An ordinary step's other flow
75
+ outputs are its arms, in the order the type declares them. **Ports become
76
+ settings** in the panel. An arm is a choice between "Fail the run" (or "Stop
77
+ here") and "Run the steps under …", which connects or disconnects the port. An
78
+ arm inside a loop also chooses between carrying on with the next step and going
79
+ on with the next row. A parallel step carries its merge's *When the branches
80
+ finish*, written to the merge step's config.
81
+
82
+ **What "block-shaped" means.** From the start step, every step is placed exactly
83
+ once: as a step, a branch, a parallel closed by one merge, a loop whose body
84
+ ends at its Next, or an end. An arm ends by rejoining what follows its block,
85
+ by going on with the loop's next row, by ending, or by leaving its port
86
+ unconnected. Anything else is refused with a code and the step it names:
87
+ `outline_no_start`, `outline_two_starts`, `outline_unknown_type`,
88
+ `outline_unknown_port`, `outline_broken_line`, `outline_unreachable`,
89
+ `outline_shared_step`, `outline_jump_out`, `outline_two_continuations`,
90
+ `outline_parallel_unjoined`, `outline_merge_unpaired`, `outline_loop_shape`,
91
+ `outline_after_end`, `outline_cycle`. Where a branch's arms meet nowhere
92
+ (every arm but one ends), the arm that goes on holds the rest of its list. The
93
+ lines cannot say more than that, and the outline does not pretend they do.
94
+
95
+ **Edits.** "+" between rows (shown on hover and on focus) and the add rows open
96
+ the kit's step picker. It says where the step will land (*"inside For each new
97
+ application, after Check eligibility"*) and offers the blocks as blocks: *At
98
+ the same time* is one entry and two steps. A step's menu (its "…", a
99
+ right-click, or the ContextMenu key) moves it up, down or out, wraps it in a
100
+ block, duplicates, copies, cuts, pastes after, renames and removes it. An entry
101
+ or a verb that would leave a graph the outline cannot draw is shown greyed with
102
+ its reason: *"Move down — already last in For each new application"*. A step is
103
+ dragged by its grip, with pointer events and a ghost after 4 px, never
104
+ `draggable` and never `setPointerCapture`. Its drop line names the place
105
+ (*"Into Branch 2 · after Write a summary"*) or says why the place is refused.
106
+ Each edit is one history entry from a closed list: `flow:step:add`, `remove`,
107
+ `move`, `wrap`, `duplicate`, `paste`, `label` and `config`, `flow:arm`,
108
+ `flow:branch:add`, `flow:branch:remove` and `flow:settings`.
109
+
110
+ **Keys.** ↑ and ↓ move the selection, ← and → fold and unfold, and Enter opens
111
+ the settings at their first field. Alt+↑, Alt+↓ and Alt+← move a step, Ctrl+D
112
+ duplicates, Ctrl+C, Ctrl+X and Ctrl+V copy, cut and paste after the selection,
113
+ F2 renames, Delete removes, and Ctrl+Z and Ctrl+Y undo and redo. All of them are
114
+ bound on the editor's root. Backspace is never the editor's.
115
+
116
+ ***Insert a value*.** A field's "{ }" calls your `values` provider with
117
+ `before`, the steps that always run before this one, nearest first
118
+ (`alwaysBefore`), and `loops`, the loops the step is inside, innermost first. A
119
+ step on the other arm of a condition is never in `before`, and neither is a
120
+ loop's body for a step after the loop.
121
+
122
+ **A run drawn on the outline.** `setRunOverlay({steps: {[id]: {state, line,
123
+ tone}}, ports: {[id]: {[port]: {taken, count}}}, banner})` puts a pill beside
124
+ each step. The pill shows `line` as you give it, toned by `state`
125
+ (`completed`, `failed`, `skipped`, `running`, `cancelled`, `not-reached`). Each
126
+ arm says *taken*, *not taken* or a count, an untaken arm and its steps are
127
+ dimmed, a loop's *Next row* shows its count (`ports[loop].next.count`), and
128
+ your banner is shown above the list. It draws only what you pass, and it
129
+ rebuilds no field in the panel. A redraw you did not ask for (findings, a run
130
+ refresh, a read-only switch) waits while a press is held in the editor, because
131
+ a browser fires a click only when the press and the release land on the same
132
+ element.
133
+
134
+ **The pure parts** are exported for a consumer that reads or checks an outline
135
+ without drawing one. `outlineFromGraph(graph, catalogue, blocks)` returns
136
+ `{ok: true, tree}` or `{ok: false, code, message, node_id}`, and
137
+ `graphFromOutline(tree)` is its inverse (`outlineGraphsEqual(a, b)` compares
138
+ two graphs as sets). `describeOutline(tree)` gives the tree as text.
139
+ `createOutlineOperations({catalogue, blocks, references})` gives every edit as
140
+ a function from a graph to `{ok, graph, select}` or `{ok: false, reason}`. Also
141
+ exported: `createOutlineBlocks`, `outlineRunOrder`, `outlineFlatOrder`,
142
+ `indexOutline`, `openOutlineStepMenu`, `OUTLINE_STRINGS`, `OUTLINE_ACTIONS` and
143
+ `OUTLINE_INDENT` (22 px a level, which the stylesheet also uses).
144
+
145
+ **What it does not do.** It does not fetch, save, validate or start a timer
146
+ that writes. Your validator's findings are drawn, never made. The folds are
147
+ yours to keep (`initialFolds`, `onFoldChange`).
148
+
149
+ **Where each claim was checked.** The round trip is tested over a corpus
150
+ (`tests/fixtures/flow_outline_corpus.json`). Every block-shaped graph is
151
+ recognised as the tree the corpus states and written back equal as sets (I1).
152
+ Opened and saved with no edit, it is the same text (I2). Random sequences of
153
+ operations keep it block-shaped (I3). Every refusal names a code and a step and
154
+ leaves the graph untouched (I4). `tests/flow_outline_editor.test.mjs` mounts
155
+ the editor in jsdom, and `tests/flow_outline_css.test.mjs` checks that every
156
+ class the editor sets has a rule and every rule names a class it sets. jsdom
157
+ computes no layout and starts no drag, so `demo/flow_outline_probe.mjs` drives
158
+ `demo/flow_outline.html` in headless Edge with real input. `elementFromPoint`
159
+ at the centre of Undo, an action, Fold all, the find box, the flow row, a row,
160
+ its menu, its "+" (with its row hovered), an add row, the panel's first field,
161
+ a port drawn as a setting and the strip's *Go to it* returned each one. A "+"
162
+ whose row was not hovered caught no press, and a level measured 22 px. The step
163
+ picker and the step menu opened inside the viewport on a real press, real keys
164
+ filtered and chose, and Escape gave the focus back. Real Alt+↑, Ctrl+Z, Ctrl+D
165
+ and Delete edited the flow, and Backspace reached the window unprevented. A real
166
+ grip drag drew its ghost and a drop line naming the place, moved the step as
167
+ one edit and started no native drag (a `dragstart` counter at 0). A refused
168
+ place said why and moved nothing, and Escape cancelled the drag.
169
+
170
+ **In a narrow editor** (under 1000px the list, the settings and the aside are
171
+ stacked and the body scrolls as one), a step picked with the mouse, with Enter,
172
+ with *Go to it* or with `select()` scrolls its settings on screen (`'nearest'`)
173
+ when they lie outside the body's visible box. ↑ and ↓ leave the list where it
174
+ is, and side by side nothing scrolls. `demo/flow_outline_narrow_probe.mjs`
175
+ pressed a step at a 900px viewport and found the panel's first field with
176
+ `elementFromPoint`. At 1440px the same press moved no scroll position.