flexdesk 0.16.1 → 0.17.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 (252) hide show
  1. package/CHANGELOG.md +93 -0
  2. package/LICENSE +0 -0
  3. package/README.md +2217 -42
  4. package/THIRD_PARTY_LICENSES.md +0 -0
  5. package/css/base.css +1834 -75
  6. package/css/flexdesk.css +3146 -164
  7. package/css/overrides.css +16 -0
  8. package/css/reset.css +0 -0
  9. package/css/tokens.css +0 -0
  10. package/dist/canvas.js +6 -239
  11. package/dist/canvas.js.map +3 -3
  12. package/dist/charts.js +709 -18
  13. package/dist/charts.js.map +4 -4
  14. package/dist/chunk-3PHCPZHT.js +0 -0
  15. package/dist/chunk-3PHCPZHT.js.map +0 -0
  16. package/dist/chunk-4A7SJC3T.js +503 -0
  17. package/dist/chunk-4A7SJC3T.js.map +7 -0
  18. package/dist/{chunk-6JSOVNID.js → chunk-CVMCZZX2.js} +635 -10
  19. package/dist/chunk-CVMCZZX2.js.map +7 -0
  20. package/dist/chunk-D3COLZZ2.js +1311 -0
  21. package/dist/chunk-D3COLZZ2.js.map +7 -0
  22. package/dist/chunk-FOOS3T5L.js +0 -0
  23. package/dist/chunk-FOOS3T5L.js.map +0 -0
  24. package/dist/{chunk-SN4LSOAR.js → chunk-GOQLR5WR.js} +452 -2
  25. package/dist/chunk-GOQLR5WR.js.map +7 -0
  26. package/dist/{chunk-FL5KFNQH.js → chunk-HNU6MM2B.js} +52 -17
  27. package/dist/chunk-HNU6MM2B.js.map +7 -0
  28. package/dist/chunk-HYJYYEPK.js +375 -0
  29. package/dist/chunk-HYJYYEPK.js.map +7 -0
  30. package/dist/chunk-JKZLNKG6.js +2041 -0
  31. package/dist/chunk-JKZLNKG6.js.map +7 -0
  32. package/dist/chunk-JYWURG5T.js +0 -0
  33. package/dist/chunk-JYWURG5T.js.map +0 -0
  34. package/dist/{chunk-RQM5Z2MU.js → chunk-MCIXFNVF.js} +633 -311
  35. package/dist/chunk-MCIXFNVF.js.map +7 -0
  36. package/dist/chunk-N7HC3ADV.js +30 -0
  37. package/dist/chunk-N7HC3ADV.js.map +7 -0
  38. package/dist/{chunk-FBGP5KN5.js → chunk-Q36EPT2R.js} +3268 -203
  39. package/dist/chunk-Q36EPT2R.js.map +7 -0
  40. package/dist/chunk-QWQJUDPD.js +610 -0
  41. package/dist/chunk-QWQJUDPD.js.map +7 -0
  42. package/dist/{chunk-NLTOY57M.js → chunk-UURZ37SD.js} +28 -48
  43. package/dist/chunk-UURZ37SD.js.map +7 -0
  44. package/dist/{chunk-B5Q53LAJ.js → chunk-UWO43BZU.js} +2 -2
  45. package/dist/chunk-UWO43BZU.js.map +7 -0
  46. package/dist/chunk-WVFGV5FT.js +0 -0
  47. package/dist/chunk-WVFGV5FT.js.map +0 -0
  48. package/dist/core.js +170 -118
  49. package/dist/core.js.map +2 -2
  50. package/dist/editor.js +156 -70
  51. package/dist/editor.js.map +2 -2
  52. package/dist/flexdesk.css +3146 -164
  53. package/dist/flow.js +2789 -306
  54. package/dist/flow.js.map +4 -4
  55. package/dist/host.js +109 -3
  56. package/dist/host.js.map +3 -3
  57. package/dist/reset.css +0 -0
  58. package/dist/tile_registry-6WZPTUZV.js +0 -0
  59. package/dist/tile_registry-6WZPTUZV.js.map +0 -0
  60. package/dist/tiles.js +657 -234
  61. package/dist/tiles.js.map +4 -4
  62. package/dist/tokens.css +0 -0
  63. package/dist/widgets.js +3252 -2124
  64. package/dist/widgets.js.map +4 -4
  65. package/dist/wm.js +1227 -347
  66. package/dist/wm.js.map +4 -4
  67. package/package.json +3 -1
  68. package/src/canvas/canvas_adapter.js +5 -0
  69. package/src/canvas/edge_router.js +0 -0
  70. package/src/canvas/graph/connector_router.js +0 -0
  71. package/src/canvas/graph/history_service.js +0 -0
  72. package/src/canvas/graph/node_platform.js +0 -0
  73. package/src/canvas/index.js +1 -0
  74. package/src/canvas/layered_layout.js +244 -0
  75. package/src/charts/chart_types.js +0 -0
  76. package/src/charts/data_series_plot_window.js +0 -0
  77. package/src/charts/downsample.js +2 -2
  78. package/src/charts/plot_config.js +53 -29
  79. package/src/charts/plot_config_editor.js +627 -0
  80. package/src/charts/plot_grid.js +449 -0
  81. package/src/charts/plot_popout_window.js +103 -317
  82. package/src/charts/plotly_wrapper.js +22 -2
  83. package/src/charts/series_transforms.js +206 -0
  84. package/src/core/event_bus.js +0 -0
  85. package/src/core/logging.js +1 -1
  86. package/src/core/settings.js +105 -19
  87. package/src/core/state_guard.js +93 -51
  88. package/src/core/state_guard_config.js +85 -74
  89. package/src/core/state_machine.js +0 -0
  90. package/src/editor/editor_pane.js +3 -3
  91. package/src/editor/monaco_editor_factory.js +124 -60
  92. package/src/editor/monaco_loader.js +103 -28
  93. package/src/editor/notebook_search.js +0 -0
  94. package/src/editor/notebook_tab_bar.js +38 -52
  95. package/src/editor/notebook_undo_manager.js +0 -0
  96. package/src/editor/split_pane_container.js +6 -3
  97. package/src/flow/canvas_editor/connections.js +90 -0
  98. package/src/flow/canvas_editor/editor.js +2351 -153
  99. package/src/flow/canvas_editor/index.js +2 -1
  100. package/src/flow/canvas_editor/layout.js +119 -12
  101. package/src/flow/canvas_editor/strings.js +80 -0
  102. package/src/flow/kit/always_before.js +0 -0
  103. package/src/flow/kit/catalogue.js +97 -3
  104. package/src/flow/kit/chip_input.js +0 -0
  105. package/src/flow/kit/dom.js +0 -0
  106. package/src/flow/kit/findings.js +0 -0
  107. package/src/flow/kit/history.js +0 -0
  108. package/src/flow/kit/index.js +3 -3
  109. package/src/flow/kit/keys.js +7 -1
  110. package/src/flow/kit/logic_graph.js +97 -15
  111. package/src/flow/kit/references.js +0 -0
  112. package/src/flow/kit/settings_panel.js +0 -0
  113. package/src/flow/kit/settings_schema.js +2 -1
  114. package/src/flow/kit/step_picker.js +0 -0
  115. package/src/flow/kit/strings.js +3 -1
  116. package/src/flow/kit/value_picker.js +0 -0
  117. package/src/flow/kit/widgets.js +86 -109
  118. package/src/flow/lanes/editor.js +136 -14
  119. package/src/flow/lanes/index.js +3 -1
  120. package/src/flow/lanes/layout.js +0 -0
  121. package/src/flow/lanes/pipeline.js +44 -2
  122. package/src/flow/lanes/preview.js +0 -0
  123. package/src/flow/lanes/strings.js +1 -0
  124. package/src/flow/outline/editor.js +0 -0
  125. package/src/flow/outline/index.js +0 -0
  126. package/src/flow/outline/mapping.js +0 -0
  127. package/src/flow/outline/menu.js +0 -0
  128. package/src/flow/outline/operations.js +0 -0
  129. package/src/flow/outline/recognise.js +0 -0
  130. package/src/flow/outline/strings.js +0 -0
  131. package/src/help/help_modal.js +168 -42
  132. package/src/help/help_registry.js +12 -1
  133. package/src/host/external_links.js +0 -0
  134. package/src/host/file_picker.js +117 -0
  135. package/src/host/host.js +10 -2
  136. package/src/host/photino_host.js +9 -0
  137. package/src/host/photino_host.md +21 -3
  138. package/src/host/pywebview_host.js +10 -0
  139. package/src/tiles/chart_tiles.js +53 -245
  140. package/src/tiles/config_schema.js +8 -0
  141. package/src/tiles/figure_tile.js +322 -0
  142. package/src/tiles/layout_persistence.js +3 -3
  143. package/src/tiles/list_tiles.js +7 -7
  144. package/src/tiles/scope_picker.js +0 -0
  145. package/src/tiles/tile_base.js +1 -1
  146. package/src/tiles/tile_config_form.js +442 -0
  147. package/src/tiles/tile_grid.js +2 -0
  148. package/src/tiles/tile_registry.js +0 -0
  149. package/src/tiling/bottom_panel_tabs.js +67 -99
  150. package/src/tiling/command_palette.js +64 -19
  151. package/src/tiling/content_registry.js +4 -1
  152. package/src/tiling/desktops.js +11 -1
  153. package/src/tiling/entity_sources.js +0 -0
  154. package/src/tiling/keymap.js +403 -128
  155. package/src/tiling/kind_taxonomy.js +78 -0
  156. package/src/tiling/landing_table.js +0 -0
  157. package/src/tiling/loading_overlay.js +0 -0
  158. package/src/tiling/nav_panel.js +59 -10
  159. package/src/tiling/page_factory.js +5 -5
  160. package/src/tiling/panel_availability.js +49 -0
  161. package/src/tiling/panel_keys.js +0 -0
  162. package/src/tiling/shell.js +114 -12
  163. package/src/tiling/tab_strip.js +0 -0
  164. package/src/tiling/tile_breadcrumb.js +0 -0
  165. package/src/tiling/tile_renderer.js +105 -9
  166. package/src/tiling/tile_tab_menu.js +0 -0
  167. package/src/tiling/tile_tree.js +0 -0
  168. package/src/tiling/wm.js +562 -30
  169. package/src/tiling/zoom.js +0 -0
  170. package/src/ui/base/component_base.js +0 -0
  171. package/src/ui/base/controller_base.js +0 -0
  172. package/src/ui/base/page_base.js +0 -0
  173. package/src/ui/components/about_dialog.js +0 -0
  174. package/src/ui/components/action_dropdown.js +30 -2
  175. package/src/ui/components/alert_log.js +0 -0
  176. package/src/ui/components/alerts_pane.js +0 -0
  177. package/src/ui/components/attribute_list_editor.js +0 -0
  178. package/src/ui/components/autocomplete_field.js +0 -0
  179. package/src/ui/components/cell_editor_host.js +211 -0
  180. package/src/ui/components/cell_grid.js +648 -0
  181. package/src/ui/components/cell_selection.js +220 -0
  182. package/src/ui/components/choice_controls.js +351 -0
  183. package/src/ui/components/color_field.js +211 -0
  184. package/src/ui/components/color_list_field.js +210 -0
  185. package/src/ui/components/column_fit.js +0 -0
  186. package/src/ui/components/column_groups.js +314 -0
  187. package/src/ui/components/column_order.js +142 -0
  188. package/src/ui/components/computing_status_window.js +98 -25
  189. package/src/ui/components/confirm_dialog.js +81 -38
  190. package/src/ui/components/context_menu.js +2 -2
  191. package/src/ui/components/conversation_view.js +440 -0
  192. package/src/ui/components/crosstab.js +289 -0
  193. package/src/ui/components/data_table.js +601 -62
  194. package/src/ui/components/detail_header.js +22 -11
  195. package/src/ui/components/document_outline.js +397 -0
  196. package/src/ui/components/drag_reorder.js +0 -0
  197. package/src/ui/components/fact_table.js +0 -0
  198. package/src/ui/components/fill_handle.js +322 -0
  199. package/src/ui/components/gallery_picker.js +19 -8
  200. package/src/ui/components/header_actions.js +55 -0
  201. package/src/ui/components/inline_renamer.js +0 -0
  202. package/src/ui/components/job_progress.js +51 -0
  203. package/src/ui/components/managed_window.js +27 -4
  204. package/src/ui/components/menu_bar.js +745 -0
  205. package/src/ui/components/modal.js +449 -75
  206. package/src/ui/components/notification_history.js +3 -3
  207. package/src/ui/components/progress.js +483 -0
  208. package/src/ui/components/record_list.js +0 -0
  209. package/src/ui/components/row_activation.js +0 -0
  210. package/src/ui/components/section_stack.js +298 -0
  211. package/src/ui/components/series.js +173 -0
  212. package/src/ui/components/settings_view.js +634 -0
  213. package/src/ui/components/slide_out_panel.js +43 -6
  214. package/src/ui/components/slider_field.js +275 -18
  215. package/src/ui/components/sortable_list.js +0 -0
  216. package/src/ui/components/staged_edits.js +343 -0
  217. package/src/ui/components/staged_history.js +172 -0
  218. package/src/ui/components/table_filter_chips.js +0 -0
  219. package/src/ui/components/table_state_store.js +0 -0
  220. package/src/ui/components/table_structure.js +182 -0
  221. package/src/ui/components/table_values.js +0 -0
  222. package/src/ui/components/tabs.js +497 -0
  223. package/src/ui/components/tag_input.js +215 -0
  224. package/src/ui/components/toast.js +25 -8
  225. package/src/ui/components/toggle_field.js +175 -0
  226. package/src/ui/components/tree_view.js +55 -34
  227. package/src/ui/components/window_taskbar.js +0 -0
  228. package/src/ui/components/wizard.js +563 -0
  229. package/src/ui/controllers/window_chrome_controller.js +2 -2
  230. package/src/ui/notification_center.js +375 -119
  231. package/src/ui/utils/auto_scrollbars.js +0 -0
  232. package/src/ui/utils/markdown.js +709 -0
  233. package/src/ui/utils/native_context_menu.js +0 -0
  234. package/src/ui/utils/overlay_scrollbar.js +16 -12
  235. package/src/ui/utils/raf_resize_observer.js +0 -0
  236. package/src/ui/utils/scroll_lock.js +0 -0
  237. package/src/ui/utils/strip_scroll.js +55 -0
  238. package/src/ui/utils/tooltip_service.js +0 -8
  239. package/dist/chunk-6JSOVNID.js.map +0 -7
  240. package/dist/chunk-B5Q53LAJ.js.map +0 -7
  241. package/dist/chunk-FBGP5KN5.js.map +0 -7
  242. package/dist/chunk-FL5KFNQH.js.map +0 -7
  243. package/dist/chunk-NLTOY57M.js.map +0 -7
  244. package/dist/chunk-NNFTETK3.js +0 -633
  245. package/dist/chunk-NNFTETK3.js.map +0 -7
  246. package/dist/chunk-O5OHMWBB.js +0 -284
  247. package/dist/chunk-O5OHMWBB.js.map +0 -7
  248. package/dist/chunk-RQM5Z2MU.js.map +0 -7
  249. package/dist/chunk-SN4LSOAR.js.map +0 -7
  250. package/dist/chunk-W6FBJLPW.js +0 -367
  251. package/dist/chunk-W6FBJLPW.js.map +0 -7
  252. package/src/ui/controllers/panel_state_machine.js +0 -634
@@ -5,8 +5,17 @@
5
5
  *
6
6
  * The shape is the consumer server's:
7
7
  *
8
- * { nodes: [{ id, type, label?, config, position: {x, y} }],
9
- * connections: [{ source, target, sourcePort, targetPort }] }
8
+ * { nodes: [{ id, type, label?, config, position: {x, y}, collapsed? }],
9
+ * connections: [{ source, target, sourcePort, targetPort }],
10
+ * groups?: [{ id, label?, tone?, nodes: [id, …] }],
11
+ * notes?: [{ id, text, position: {x, y}, size?: {w?, h?} }] }
12
+ *
13
+ * `groups` and `notes` (0.17.0) are there only when the graph has some: a
14
+ * graph without them is the same object, and the same text, it always was. A
15
+ * group is FLAT and a node is in one group at most — `normalise` keeps a node
16
+ * in the first group that names it, leaves out ids that are not nodes of the
17
+ * graph, and drops a group left with no member. `removeNode` takes the node
18
+ * out of its group, and the group with its last member.
10
19
  *
11
20
  * **The editors decide nothing a publish depends on.** Whether a graph is a
12
21
  * valid flow is the consumer's validator, drawn as findings. What `canConnect`
@@ -16,8 +25,10 @@
16
25
  * returned, never thrown into a console.
17
26
  *
18
27
  * `serialise` is byte-stable: node keys in the order `id, type, label?, config,
19
- * position`, config keys sorted, nodes and connections in the order they were
20
- * drawn — so a graph opened and saved with no edit is the identical text.
28
+ * position, collapsed?` (`collapsed` only when it is `true`, so a graph that
29
+ * never folded a node is the text it always was), config keys sorted, nodes and connections in the order they were
30
+ * drawn (and then the groups and the notes, in theirs) — so a graph opened and
31
+ * saved with no edit is the identical text.
21
32
  */
22
33
 
23
34
  import { createStrings } from './strings.js';
@@ -33,6 +44,7 @@ export const LOGIC_GRAPH_STRINGS = Object.freeze({
33
44
  connectNotFlow: (how) => (how
34
45
  ? `Only flow ports connect; a data port is read as ${how}.`
35
46
  : 'Only flow ports connect; a data port is read, not connected.'),
47
+ connectKinds: 'A flow port connects to a flow port, and a data port to a data port.',
36
48
  connectTwice: 'These two are already connected.',
37
49
  connectOneLine: (port) => `The ${port} port takes one connection; `
38
50
  + 'use a step that runs several branches.',
@@ -42,16 +54,62 @@ export function emptyGraph() {
42
54
  return { nodes: [], connections: [] };
43
55
  }
44
56
 
57
+ const round = (v) => Math.round(Number(v) || 0);
58
+
59
+ /** The groups worth keeping: flat, each node in the first group that names it. */
60
+ function groupsOf(list, nodeIds) {
61
+ const out = [];
62
+ const ids = new Set();
63
+ const placed = new Set();
64
+ for (const gr of Array.isArray(list) ? list : []) {
65
+ if (!gr || typeof gr !== 'object' || gr.id === undefined || gr.id === null || gr.id === '') continue;
66
+ const id = String(gr.id);
67
+ if (ids.has(id)) continue;
68
+ const nodes = [];
69
+ for (const raw of Array.isArray(gr.nodes) ? gr.nodes : []) {
70
+ const n = String(raw);
71
+ if (!nodeIds.has(n) || placed.has(n)) continue;
72
+ placed.add(n);
73
+ nodes.push(n);
74
+ }
75
+ if (!nodes.length) continue;
76
+ ids.add(id);
77
+ out.push({ id, ...(gr.label ? { label: String(gr.label) } : {}), ...(gr.tone ? { tone: String(gr.tone) } : {}),
78
+ nodes });
79
+ }
80
+ return out;
81
+ }
82
+
83
+ /** The notes worth keeping: an id each, their text, where they are and the size they were given. */
84
+ function notesOf(list) {
85
+ const out = [];
86
+ const ids = new Set();
87
+ for (const note of Array.isArray(list) ? list : []) {
88
+ if (!note || typeof note !== 'object' || note.id === undefined || note.id === null || note.id === '') continue;
89
+ const id = String(note.id);
90
+ if (ids.has(id)) continue;
91
+ ids.add(id);
92
+ const w = round(note.size?.w);
93
+ const h = round(note.size?.h);
94
+ const size = { ...(w > 0 ? { w } : {}), ...(h > 0 ? { h } : {}) };
95
+ out.push({ id, text: note.text === undefined || note.text === null ? '' : String(note.text),
96
+ position: { x: round(note.position?.x), y: round(note.position?.y) },
97
+ ...(Object.keys(size).length ? { size } : {}) });
98
+ }
99
+ return out;
100
+ }
101
+
45
102
  /** A deep copy with every field the graph carries, and nothing else. */
46
103
  export function normalise(graph) {
47
104
  const g = graph && typeof graph === 'object' ? graph : emptyGraph();
48
- return {
105
+ const out = {
49
106
  nodes: (Array.isArray(g.nodes) ? g.nodes : []).map((n) => ({
50
107
  id: String(n.id),
51
108
  type: String(n.type),
52
109
  ...(n.label ? { label: String(n.label) } : {}),
53
110
  config: n.config && typeof n.config === 'object' ? structuredClone(n.config) : {},
54
111
  position: { x: Math.round(Number(n.position?.x) || 0), y: Math.round(Number(n.position?.y) || 0) },
112
+ ...(n.collapsed === true ? { collapsed: true } : {}),
55
113
  })),
56
114
  connections: (Array.isArray(g.connections) ? g.connections : []).map((c) => ({
57
115
  source: String(c.source),
@@ -60,6 +118,11 @@ export function normalise(graph) {
60
118
  targetPort: String(c.targetPort || 'in'),
61
119
  })),
62
120
  };
121
+ const groups = groupsOf(g.groups, new Set(out.nodes.map((n) => n.id)));
122
+ if (groups.length) out.groups = groups;
123
+ const notes = notesOf(g.notes);
124
+ if (notes.length) out.notes = notes;
125
+ return out;
63
126
  }
64
127
 
65
128
  function sortedKeys(value) {
@@ -76,11 +139,13 @@ export function serialise(graph) {
76
139
  return JSON.stringify({
77
140
  nodes: g.nodes.map((n) => ({
78
141
  id: n.id, type: n.type, ...(n.label ? { label: n.label } : {}),
79
- config: sortedKeys(n.config), position: n.position,
142
+ config: sortedKeys(n.config), position: n.position, ...(n.collapsed ? { collapsed: true } : {}),
80
143
  })),
81
144
  connections: g.connections.map((c) => ({
82
145
  source: c.source, target: c.target, sourcePort: c.sourcePort, targetPort: c.targetPort,
83
146
  })),
147
+ ...(g.groups ? { groups: g.groups } : {}),
148
+ ...(g.notes ? { notes: g.notes } : {}),
84
149
  });
85
150
  }
86
151
 
@@ -118,9 +183,15 @@ export function addNode(graph, typeId, position = null, { id = null, label = nul
118
183
  return node;
119
184
  }
120
185
 
186
+ /** Take a step away, with its lines; its group shrinks, and goes with its last member. */
121
187
  export function removeNode(graph, nodeId) {
122
188
  graph.nodes = graph.nodes.filter((n) => n.id !== nodeId);
123
189
  graph.connections = graph.connections.filter((c) => c.source !== nodeId && c.target !== nodeId);
190
+ if (Array.isArray(graph.groups)) {
191
+ for (const gr of graph.groups) gr.nodes = gr.nodes.filter((id) => id !== nodeId);
192
+ graph.groups = graph.groups.filter((gr) => gr.nodes.length);
193
+ if (!graph.groups.length) delete graph.groups;
194
+ }
124
195
  }
125
196
 
126
197
  export function moveNode(graph, nodeId, position) {
@@ -143,9 +214,13 @@ function portOf(catalogue, node, name, direction) {
143
214
  * `{ok: false, reason}` — the editor shows the reason where the gesture failed.
144
215
  * `dataRead` is the consumer's words for how a data port is read instead
145
216
  * (`syntax.format(...)`), put in the refusal.
217
+ *
218
+ * With `dataPorts: true` a data port connects to a data port (never to a flow
219
+ * port) and a data output carries any number of lines; whatever else limits a
220
+ * data connection is the caller's (the canvas editor asks a `ConnectorRouter`).
146
221
  */
147
222
  export function canConnect(graph, catalogue, { source, sourcePort, target, targetPort },
148
- { strings = null, dataRead = null } = {}) {
223
+ { strings = null, dataRead = null, dataPorts = false } = {}) {
149
224
  const S = createStrings(LOGIC_GRAPH_STRINGS, strings);
150
225
  if (source === target) return { ok: false, reason: S.connectSelf };
151
226
  const from = graph.nodes.find((n) => n.id === source);
@@ -155,14 +230,15 @@ export function canConnect(graph, catalogue, { source, sourcePort, target, targe
155
230
  const inp = portOf(catalogue, to, targetPort, 'input');
156
231
  if (!out) return { ok: false, reason: S.connectNoOutput(sourcePort) };
157
232
  if (!inp) return { ok: false, reason: S.connectNoInput(targetPort) };
158
- if ((out.port_type || 'FLOW') !== 'FLOW' || (inp.port_type || 'FLOW') !== 'FLOW') {
159
- return { ok: false, reason: S.connectNotFlow(dataRead) };
160
- }
233
+ const flowOut = (out.port_type || 'FLOW') === 'FLOW';
234
+ const flowIn = (inp.port_type || 'FLOW') === 'FLOW';
235
+ if (!dataPorts && (!flowOut || !flowIn)) return { ok: false, reason: S.connectNotFlow(dataRead) };
236
+ if (flowOut !== flowIn) return { ok: false, reason: S.connectKinds };
161
237
  if (graph.connections.some((c) => c.source === source && c.sourcePort === sourcePort
162
238
  && c.target === target && c.targetPort === targetPort)) {
163
239
  return { ok: false, reason: S.connectTwice };
164
240
  }
165
- if (!out.multiple && graph.connections.some((c) => c.source === source && c.sourcePort === sourcePort)) {
241
+ if (flowOut && !out.multiple && graph.connections.some((c) => c.source === source && c.sourcePort === sourcePort)) {
166
242
  return { ok: false, reason: S.connectOneLine(out.label || sourcePort) };
167
243
  }
168
244
  return { ok: true };
@@ -171,13 +247,19 @@ export function canConnect(graph, catalogue, { source, sourcePort, target, targe
171
247
  /** Connect when `canConnect` allows it; returns its verdict either way. */
172
248
  export function connect(graph, catalogue, edge, options = {}) {
173
249
  const verdict = canConnect(graph, catalogue, edge, options);
174
- if (verdict.ok) {
175
- graph.connections.push({ source: edge.source, target: edge.target,
176
- sourcePort: edge.sourcePort, targetPort: edge.targetPort });
177
- }
250
+ if (verdict.ok) addConnection(graph, edge);
178
251
  return verdict;
179
252
  }
180
253
 
254
+ /**
255
+ * Append the line `edge` describes, asking nothing — for a caller whose own
256
+ * rules already allowed it (the canvas editor's `connections` option). The one
257
+ * place a connection row is built.
258
+ */
259
+ export function addConnection(graph, { source, target, sourcePort, targetPort }) {
260
+ graph.connections.push({ source, target, sourcePort, targetPort });
261
+ }
262
+
181
263
  export function disconnect(graph, edge) {
182
264
  graph.connections = graph.connections.filter((c) => !sameEdge(c, edge));
183
265
  }
File without changes
File without changes
@@ -49,7 +49,8 @@ export const SETTINGS_SCHEMA_TYPES = Object.freeze(['object', 'array', 'string',
49
49
 
50
50
  /** The `x-ui-` hints the kit reads (36 §3.2); the server ignores every `x-` key. */
51
51
  export const UI_HINTS = Object.freeze(['x-ui-widget', 'x-ui-references', 'x-ui-when', 'x-ui-placeholder',
52
- 'x-ui-enum-labels', 'x-ui-multiline', 'x-ui-fold']);
52
+ 'x-ui-enum-labels', 'x-ui-enum-descriptions', 'x-ui-multiline',
53
+ 'x-ui-fold', 'x-ui-add-label', 'x-ui-formula-references']);
53
54
 
54
55
  const isPlainObject = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);
55
56
 
File without changes
@@ -60,7 +60,9 @@ export const FLOW_STRINGS = Object.freeze({
60
60
  value: 'Value',
61
61
  formula: 'Formula',
62
62
  textOrValue: 'Text, or insert a value',
63
- noColumns: 'There are no columns to choose from yet: connect a step before this one.',
63
+ noColumns: 'Columns not known yet: type the name.',
64
+ columnName: 'Column name',
65
+ columnNames: 'Column name, then Enter',
64
66
  noColumnsChosen: 'None chosen',
65
67
  fieldFinding: 'Fix this before publishing',
66
68
 
File without changes
@@ -29,6 +29,7 @@
29
29
  */
30
30
 
31
31
  import { button, el, uid } from './dom.js';
32
+ import { choiceList, selectBox, shownValue, toggleChips } from '../../ui/components/choice_controls.js';
32
33
  import { createChipInput } from './chip_input.js';
33
34
  import { resolveSyntaxes } from './references.js';
34
35
  import { admittedTypes, enumLabel, fieldsFromSchema, primaryType, titleOf, visibleFields, whenKeys }
@@ -37,9 +38,6 @@ import { createStrings, say } from './strings.js';
37
38
 
38
39
  // ── values that keep their type ──────────────────────────────────────────
39
40
 
40
- /** A stored value as a one-line box shows it: a string as itself, anything else as JSON. */
41
- export const shownValue = (v) => (v === undefined ? '' : typeof v === 'string' ? v : JSON.stringify(v));
42
-
43
41
  /**
44
42
  * The text typed into a box, as a value OF THE TYPE IT WAS:
45
43
  * - an untouched box writes back what was stored, exactly;
@@ -71,34 +69,6 @@ function textBox(value, { type = 'text', placeholder = '', label = '' } = {}) {
71
69
  return input;
72
70
  }
73
71
 
74
- /** A `<select>` over `options` (`{value, label, disabled?}`), keeping a stored value it lacks. */
75
- function selectBox(options, value, { empty = null, label = '', notFound = (v) => `${v} (not found)` } = {}) {
76
- const box = el('select', 'twm-flow-input twm-flow-input--select');
77
- if (label) box.setAttribute('aria-label', label);
78
- const values = [];
79
- if (empty !== null) {
80
- box.appendChild(new Option(empty, ''));
81
- values.push(undefined);
82
- }
83
- options.forEach((opt) => {
84
- const o = new Option(opt.label, String(values.length));
85
- if (opt.disabled) o.disabled = true;
86
- box.appendChild(o);
87
- values.push(opt.value);
88
- });
89
- const at = values.findIndex((v, i) => i >= (empty !== null ? 1 : 0) && v === value);
90
- if (value !== undefined && value !== null && value !== '' && at < 0) {
91
- // A stored value the list no longer offers is KEPT as its own option:
92
- // it must not silently become the first entry on the next save.
93
- box.appendChild(new Option(notFound(shownValue(value)), String(values.length)));
94
- values.push(value);
95
- box.value = String(values.length - 1);
96
- } else {
97
- box.value = at >= 0 ? String(at) : (empty !== null ? '' : '0');
98
- }
99
- return { box, valueOf: () => (box.value === '' ? undefined : values[Number(box.value)]) };
100
- }
101
-
102
72
  /**
103
73
  * A textarea as tall as its text — wrapped lines included — up to `maxLines`,
104
74
  * then it scrolls. Measured from `scrollHeight` once laid out (a
@@ -345,49 +315,6 @@ function stringListWidget(spec, value, ctx) {
345
315
  return { el: rows.el, destroy: rows.destroy };
346
316
  }
347
317
 
348
- /** Toggle chips over a fixed list; the stored list follows the list's order,
349
- * and a stored entry the list lacks is kept, marked, until it is toggled off. */
350
- function toggleChips({ options, value, label, readOnly, onChange, notFound }) {
351
- const root = el('div', 'twm-flow-togglechips');
352
- root.setAttribute('role', 'group');
353
- if (label) root.setAttribute('aria-label', label);
354
- const known = options.map((o) => o.value);
355
- const chosen = new Set((Array.isArray(value) ? value : []).filter((v) => known.includes(v)));
356
- const strays = (Array.isArray(value) ? value : []).filter((v) => !known.includes(v));
357
- const emit = () => {
358
- const out = [...known.filter((v) => chosen.has(v)), ...strays];
359
- onChange(out.length ? out : undefined);
360
- };
361
- const chipFor = (text, pressed, toggle, stray = false) => {
362
- const b = el('button', `twm-flow-togglechip${stray ? ' twm-flow-togglechip--missing' : ''}`, text);
363
- b.type = 'button';
364
- b.setAttribute('aria-pressed', pressed ? 'true' : 'false');
365
- b.disabled = Boolean(readOnly);
366
- b.addEventListener('click', () => {
367
- const on = toggle();
368
- b.setAttribute('aria-pressed', on ? 'true' : 'false');
369
- emit();
370
- });
371
- return b;
372
- };
373
- for (const o of options) {
374
- root.appendChild(chipFor(o.label, chosen.has(o.value), () => {
375
- if (chosen.has(o.value)) chosen.delete(o.value); else chosen.add(o.value);
376
- return chosen.has(o.value);
377
- }));
378
- }
379
- for (const v of [...strays]) {
380
- const b = chipFor(notFound(shownValue(v)), true, () => {
381
- const i = strays.indexOf(v);
382
- if (i >= 0) { strays.splice(i, 1); return false; }
383
- strays.push(v);
384
- return true;
385
- }, true);
386
- root.appendChild(b);
387
- }
388
- return root;
389
- }
390
-
391
318
  /** A multi-select from `items.enum`, drawn as chips. */
392
319
  function enumChipsWidget(spec, value, ctx) {
393
320
  const S = ctx.strings;
@@ -398,7 +325,7 @@ function enumChipsWidget(spec, value, ctx) {
398
325
  el: toggleChips({
399
326
  options: values.map((v) => ({ value: v, label: words(v) })), value, label: ctx.label,
400
327
  readOnly: ctx.readOnly, onChange: (v) => ctx.set(v), notFound: (v) => say(S, 'notFound', v),
401
- }),
328
+ }).el,
402
329
  };
403
330
  }
404
331
 
@@ -408,43 +335,41 @@ function choiceCardsWidget(spec, value, ctx) {
408
335
  const values = Array.isArray(spec.enum) ? spec.enum : [];
409
336
  const descriptions = spec['x-ui-enum-descriptions'] && typeof spec['x-ui-enum-descriptions'] === 'object'
410
337
  ? spec['x-ui-enum-descriptions'] : {};
411
- const root = el('div', 'twm-flow-choices');
412
- root.setAttribute('role', 'radiogroup');
413
- if (ctx.label) root.setAttribute('aria-label', ctx.label);
414
- const name = uid('choice');
415
- const current = value !== undefined ? value : spec.default;
416
- const radios = [];
417
- for (const v of values) {
418
- const row = el('label', 'twm-flow-choice');
419
- const radio = el('input', 'twm-flow-choice__radio');
420
- radio.type = 'radio';
421
- radio.name = name;
422
- radio.checked = v === current;
423
- radio.disabled = Boolean(ctx.readOnly);
424
- radio.addEventListener('change', () => { if (radio.checked) ctx.set(v); });
425
- radios.push(radio);
426
- const words = el('span', 'twm-flow-choice__words');
427
- words.appendChild(el('span', 'twm-flow-choice__label', enumLabel(spec, v)));
428
- if (descriptions[v] !== undefined) words.appendChild(el('span', 'twm-flow-choice__sub', descriptions[v]));
429
- row.append(radio, words);
430
- root.appendChild(row);
431
- }
432
- if (current !== undefined && !values.includes(current)) {
433
- root.appendChild(el('p', 'twm-flow-field__note', say(ctx.strings, 'notFound', shownValue(current))));
434
- }
435
- return { el: root, focus: () => (radios.find((r) => r.checked) || radios[0])?.focus({ preventScroll: true }) };
338
+ const list = choiceList({
339
+ options: values.map((v) => ({ value: v, label: enumLabel(spec, v), description: descriptions[v] })),
340
+ value: value !== undefined ? value : spec.default,
341
+ name: uid('choice'), label: ctx.label, readOnly: ctx.readOnly,
342
+ onChange: (v) => ctx.set(v), notFound: (v) => say(ctx.strings, 'notFound', v),
343
+ });
344
+ return { el: list.el, focus: list.focus };
436
345
  }
437
346
 
438
347
  const columnWords = (c) => (c.type ? `${c.name} · ${c.type}` : String(c.name));
439
348
 
349
+ /**
350
+ * The columns are NOT KNOWN YET (`ctx.columns()` is null: nothing described or
351
+ * previewed the step's input so far): a column is typed by name, and the field
352
+ * says why it is not a choice. The name is stored as typed; once the columns
353
+ * arrive the panel is drawn again and the same setting is a choice among them,
354
+ * a name they lack kept and marked not found.
355
+ */
356
+ function columnsNotKnownNote(S) {
357
+ return el('p', 'twm-flow-field__note', say(S, 'noColumns'));
358
+ }
359
+
440
360
  /** One column of the step's input, by NAME. */
441
361
  function upstreamColumnWidget(spec, value, ctx) {
442
362
  const S = ctx.strings;
443
363
  const columns = ctx.columns?.();
444
364
  if (!Array.isArray(columns)) {
445
- const note = el('p', 'twm-flow-field__note', say(S, 'noColumns'));
446
- if (value !== undefined) note.appendChild(el('span', 'twm-flow-field__kept', ` ${shownValue(value)}`));
447
- return { el: note };
365
+ const root = el('div', 'twm-flow-field__typed');
366
+ const box = textBox(shownValue(value), { label: ctx.label,
367
+ placeholder: spec['x-ui-placeholder'] ?? say(S, 'columnName') });
368
+ box.spellcheck = false;
369
+ box.readOnly = Boolean(ctx.readOnly);
370
+ box.addEventListener('input', () => ctx.set(clean(typedValue(box.value, value))));
371
+ root.append(box, columnsNotKnownNote(S));
372
+ return { el: root, focus: () => box.focus({ preventScroll: true }) };
448
373
  }
449
374
  const { box, valueOf } = selectBox(columns.map((c) => ({ value: String(c.name), label: columnWords(c) })),
450
375
  value, { empty: say(S, 'choose'), label: ctx.label,
@@ -454,23 +379,75 @@ function upstreamColumnWidget(spec, value, ctx) {
454
379
  return { el: box };
455
380
  }
456
381
 
382
+ /**
383
+ * Names typed into a chip list: Enter (or a comma, or leaving the box) makes
384
+ * the text a chip, pressing a chip removes it. The list keeps the order the
385
+ * names were given in, a name given twice once, and a stored entry as it is.
386
+ */
387
+ function typedChips({ value, label, placeholder, readOnly, onChange, strings }) {
388
+ const root = el('div', 'twm-flow-togglechips twm-flow-typedchips');
389
+ root.setAttribute('role', 'group');
390
+ if (label) root.setAttribute('aria-label', label);
391
+ const list = Array.isArray(value) ? [...value] : [];
392
+ const input = textBox('', { label, placeholder });
393
+ input.classList.add('twm-flow-typedchips__input');
394
+ input.spellcheck = false;
395
+ input.readOnly = Boolean(readOnly);
396
+ const emit = () => onChange(list.length ? [...list] : undefined);
397
+ const chipFor = (v) => {
398
+ const text = shownValue(v);
399
+ const b = el('button', 'twm-flow-togglechip', text);
400
+ b.type = 'button';
401
+ b.setAttribute('aria-pressed', 'true');
402
+ b.setAttribute('aria-label', say(strings, 'removeNamed', text));
403
+ b.title = say(strings, 'removeNamed', text);
404
+ b.disabled = Boolean(readOnly);
405
+ b.addEventListener('click', () => {
406
+ const i = list.indexOf(v);
407
+ if (i >= 0) list.splice(i, 1);
408
+ b.remove();
409
+ emit();
410
+ input.focus({ preventScroll: true });
411
+ });
412
+ return b;
413
+ };
414
+ const take = () => {
415
+ const name = input.value.replace(/,+$/, '').trim();
416
+ input.value = '';
417
+ if (!name || list.includes(name)) return;
418
+ list.push(name);
419
+ root.insertBefore(chipFor(name), input);
420
+ emit();
421
+ };
422
+ for (const v of list) root.appendChild(chipFor(v));
423
+ root.appendChild(input);
424
+ input.addEventListener('keydown', (ev) => {
425
+ if (ev.isComposing || (ev.key !== 'Enter' && ev.key !== ',')) return;
426
+ ev.preventDefault();
427
+ take();
428
+ });
429
+ input.addEventListener('blur', () => { if (input.value.trim()) take(); });
430
+ return { el: root, input };
431
+ }
432
+
457
433
  /** Several columns of the step's input, by name, as chips in the input's order. */
458
434
  function upstreamColumnsWidget(spec, value, ctx) {
459
435
  const S = ctx.strings;
460
436
  const columns = ctx.columns?.();
461
437
  if (!Array.isArray(columns)) {
462
- const note = el('p', 'twm-flow-field__note', say(S, 'noColumns'));
463
- if (Array.isArray(value) && value.length) {
464
- note.appendChild(el('span', 'twm-flow-field__kept', ` ${value.map(shownValue).join(', ')}`));
465
- }
466
- return { el: note };
438
+ const root = el('div', 'twm-flow-field__typed');
439
+ const chips = typedChips({ value, label: ctx.label, readOnly: ctx.readOnly, strings: S,
440
+ placeholder: spec['x-ui-placeholder'] ?? say(S, 'columnNames'),
441
+ onChange: (v) => ctx.set(v) });
442
+ root.append(chips.el, columnsNotKnownNote(S));
443
+ return { el: root, focus: () => chips.input.focus({ preventScroll: true }) };
467
444
  }
468
445
  return {
469
446
  el: toggleChips({
470
447
  options: columns.map((c) => ({ value: String(c.name), label: String(c.name) })), value,
471
448
  label: ctx.label, readOnly: ctx.readOnly, onChange: (v) => ctx.set(v),
472
449
  notFound: (v) => say(S, 'notFound', v),
473
- }),
450
+ }).el,
474
451
  };
475
452
  }
476
453