flexdesk 0.16.1 → 0.17.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +112 -0
- package/LICENSE +0 -0
- package/README.md +2435 -42
- package/THIRD_PARTY_LICENSES.md +0 -0
- package/canvas.js +18 -0
- package/charts.js +21 -0
- package/core.js +14 -0
- package/css/base.css +1834 -75
- package/css/flexdesk.css +3165 -159
- package/css/overrides.css +16 -0
- package/css/reset.css +0 -0
- package/css/tokens.css +0 -0
- package/dist/canvas.js +6 -239
- package/dist/canvas.js.map +3 -3
- package/dist/charts.js +707 -18
- package/dist/charts.js.map +4 -4
- package/dist/chunk-3PHCPZHT.js +0 -0
- package/dist/chunk-3PHCPZHT.js.map +0 -0
- package/dist/chunk-4A7SJC3T.js +503 -0
- package/dist/chunk-4A7SJC3T.js.map +7 -0
- package/dist/{chunk-6JSOVNID.js → chunk-CVMCZZX2.js} +635 -10
- package/dist/chunk-CVMCZZX2.js.map +7 -0
- package/dist/chunk-D3COLZZ2.js +1311 -0
- package/dist/chunk-D3COLZZ2.js.map +7 -0
- package/dist/chunk-FOOS3T5L.js +0 -0
- package/dist/chunk-FOOS3T5L.js.map +0 -0
- package/dist/{chunk-SN4LSOAR.js → chunk-GOQLR5WR.js} +452 -2
- package/dist/chunk-GOQLR5WR.js.map +7 -0
- package/dist/{chunk-FL5KFNQH.js → chunk-HNU6MM2B.js} +52 -17
- package/dist/chunk-HNU6MM2B.js.map +7 -0
- package/dist/chunk-HYJYYEPK.js +375 -0
- package/dist/chunk-HYJYYEPK.js.map +7 -0
- package/dist/chunk-JKZLNKG6.js +2041 -0
- package/dist/chunk-JKZLNKG6.js.map +7 -0
- package/dist/chunk-JYWURG5T.js +0 -0
- package/dist/chunk-JYWURG5T.js.map +0 -0
- package/dist/{chunk-FBGP5KN5.js → chunk-LWV23YVE.js} +5073 -532
- package/dist/chunk-LWV23YVE.js.map +7 -0
- package/dist/chunk-N7HC3ADV.js +30 -0
- package/dist/chunk-N7HC3ADV.js.map +7 -0
- package/dist/{chunk-RQM5Z2MU.js → chunk-RMTRWWEO.js} +633 -311
- package/dist/chunk-RMTRWWEO.js.map +7 -0
- package/dist/{chunk-NLTOY57M.js → chunk-UURZ37SD.js} +28 -48
- package/dist/chunk-UURZ37SD.js.map +7 -0
- package/dist/{chunk-B5Q53LAJ.js → chunk-UWO43BZU.js} +2 -2
- package/dist/chunk-UWO43BZU.js.map +7 -0
- package/dist/chunk-WVFGV5FT.js +0 -0
- package/dist/chunk-WVFGV5FT.js.map +0 -0
- package/dist/core.js +170 -118
- package/dist/core.js.map +2 -2
- package/dist/editor.js +156 -70
- package/dist/editor.js.map +2 -2
- package/dist/flexdesk.css +3165 -159
- package/dist/flow.js +2789 -306
- package/dist/flow.js.map +4 -4
- package/dist/host.js +109 -3
- package/dist/host.js.map +3 -3
- package/dist/reset.css +0 -0
- package/dist/tile_registry-6WZPTUZV.js +0 -0
- package/dist/tile_registry-6WZPTUZV.js.map +0 -0
- package/dist/tiles.js +657 -234
- package/dist/tiles.js.map +4 -4
- package/dist/tokens.css +0 -0
- package/dist/widgets.js +3290 -2124
- package/dist/widgets.js.map +4 -4
- package/dist/wm.js +1227 -347
- package/dist/wm.js.map +4 -4
- package/editor.js +20 -0
- package/flow.js +21 -0
- package/host.js +14 -0
- package/package.json +12 -1
- package/src/canvas/canvas_adapter.js +5 -0
- package/src/canvas/edge_router.js +0 -0
- package/src/canvas/graph/connector_router.js +0 -0
- package/src/canvas/graph/history_service.js +0 -0
- package/src/canvas/graph/node_platform.js +0 -0
- package/src/canvas/index.js +1 -0
- package/src/canvas/layered_layout.js +244 -0
- package/src/charts/chart_types.js +0 -0
- package/src/charts/data_series_plot_window.js +0 -0
- package/src/charts/downsample.js +2 -2
- package/src/charts/plot_config.js +53 -29
- package/src/charts/plot_config_editor.js +627 -0
- package/src/charts/plot_grid.js +449 -0
- package/src/charts/plot_popout_window.js +103 -317
- package/src/charts/plotly_wrapper.js +22 -2
- package/src/charts/series_transforms.js +206 -0
- package/src/core/event_bus.js +0 -0
- package/src/core/logging.js +1 -1
- package/src/core/settings.js +105 -19
- package/src/core/state_guard.js +93 -51
- package/src/core/state_guard_config.js +85 -74
- package/src/core/state_machine.js +0 -0
- package/src/editor/editor_pane.js +3 -3
- package/src/editor/monaco_editor_factory.js +124 -60
- package/src/editor/monaco_loader.js +103 -28
- package/src/editor/notebook_search.js +0 -0
- package/src/editor/notebook_tab_bar.js +38 -52
- package/src/editor/notebook_undo_manager.js +0 -0
- package/src/editor/split_pane_container.js +6 -3
- package/src/flow/canvas_editor/connections.js +90 -0
- package/src/flow/canvas_editor/editor.js +2351 -153
- package/src/flow/canvas_editor/index.js +2 -1
- package/src/flow/canvas_editor/layout.js +119 -12
- package/src/flow/canvas_editor/strings.js +80 -0
- package/src/flow/kit/always_before.js +0 -0
- package/src/flow/kit/catalogue.js +97 -3
- package/src/flow/kit/chip_input.js +0 -0
- package/src/flow/kit/dom.js +0 -0
- package/src/flow/kit/findings.js +0 -0
- package/src/flow/kit/history.js +0 -0
- package/src/flow/kit/index.js +3 -3
- package/src/flow/kit/keys.js +7 -1
- package/src/flow/kit/logic_graph.js +97 -15
- package/src/flow/kit/references.js +0 -0
- package/src/flow/kit/settings_panel.js +0 -0
- package/src/flow/kit/settings_schema.js +2 -1
- package/src/flow/kit/step_picker.js +0 -0
- package/src/flow/kit/strings.js +3 -1
- package/src/flow/kit/value_picker.js +0 -0
- package/src/flow/kit/widgets.js +86 -109
- package/src/flow/lanes/editor.js +136 -14
- package/src/flow/lanes/index.js +3 -1
- package/src/flow/lanes/layout.js +0 -0
- package/src/flow/lanes/pipeline.js +44 -2
- package/src/flow/lanes/preview.js +0 -0
- package/src/flow/lanes/strings.js +1 -0
- package/src/flow/outline/editor.js +0 -0
- package/src/flow/outline/index.js +0 -0
- package/src/flow/outline/mapping.js +0 -0
- package/src/flow/outline/menu.js +0 -0
- package/src/flow/outline/operations.js +0 -0
- package/src/flow/outline/recognise.js +0 -0
- package/src/flow/outline/strings.js +0 -0
- package/src/help/help_modal.js +168 -42
- package/src/help/help_registry.js +12 -1
- package/src/host/external_links.js +0 -0
- package/src/host/file_picker.js +117 -0
- package/src/host/host.js +10 -2
- package/src/host/photino_host.js +9 -0
- package/src/host/photino_host.md +21 -3
- package/src/host/pywebview_host.js +10 -0
- package/src/tiles/chart_tiles.js +53 -245
- package/src/tiles/config_schema.js +8 -0
- package/src/tiles/figure_tile.js +322 -0
- package/src/tiles/layout_persistence.js +3 -3
- package/src/tiles/list_tiles.js +7 -7
- package/src/tiles/scope_picker.js +0 -0
- package/src/tiles/tile_base.js +1 -1
- package/src/tiles/tile_config_form.js +442 -0
- package/src/tiles/tile_grid.js +2 -0
- package/src/tiles/tile_registry.js +0 -0
- package/src/tiling/bottom_panel_tabs.js +67 -99
- package/src/tiling/command_palette.js +64 -19
- package/src/tiling/content_registry.js +4 -1
- package/src/tiling/desktops.js +11 -1
- package/src/tiling/entity_sources.js +0 -0
- package/src/tiling/keymap.js +403 -128
- package/src/tiling/kind_taxonomy.js +78 -0
- package/src/tiling/landing_table.js +0 -0
- package/src/tiling/loading_overlay.js +0 -0
- package/src/tiling/nav_panel.js +59 -10
- package/src/tiling/page_factory.js +5 -5
- package/src/tiling/panel_availability.js +49 -0
- package/src/tiling/panel_keys.js +0 -0
- package/src/tiling/shell.js +114 -12
- package/src/tiling/tab_strip.js +0 -0
- package/src/tiling/tile_breadcrumb.js +0 -0
- package/src/tiling/tile_renderer.js +105 -9
- package/src/tiling/tile_tab_menu.js +0 -0
- package/src/tiling/tile_tree.js +0 -0
- package/src/tiling/wm.js +562 -30
- package/src/tiling/zoom.js +0 -0
- package/src/ui/base/component_base.js +0 -0
- package/src/ui/base/controller_base.js +0 -0
- package/src/ui/base/page_base.js +0 -0
- package/src/ui/components/about_dialog.js +0 -0
- package/src/ui/components/action_dropdown.js +30 -2
- package/src/ui/components/alert_log.js +0 -0
- package/src/ui/components/alerts_pane.js +0 -0
- package/src/ui/components/attribute_list_editor.js +0 -0
- package/src/ui/components/autocomplete_field.js +0 -0
- package/src/ui/components/cell_editor_host.js +293 -0
- package/src/ui/components/cell_editors.js +480 -0
- package/src/ui/components/cell_grid.js +761 -0
- package/src/ui/components/cell_paste.js +243 -0
- package/src/ui/components/cell_selection.js +220 -0
- package/src/ui/components/choice_controls.js +351 -0
- package/src/ui/components/color_field.js +211 -0
- package/src/ui/components/color_list_field.js +210 -0
- package/src/ui/components/column_fit.js +0 -0
- package/src/ui/components/column_groups.js +314 -0
- package/src/ui/components/column_order.js +142 -0
- package/src/ui/components/computing_status_window.js +98 -25
- package/src/ui/components/confirm_dialog.js +81 -38
- package/src/ui/components/context_menu.js +2 -2
- package/src/ui/components/conversation_view.js +440 -0
- package/src/ui/components/crosstab.js +289 -0
- package/src/ui/components/data_table.js +696 -71
- package/src/ui/components/detail_header.js +22 -11
- package/src/ui/components/document_outline.js +397 -0
- package/src/ui/components/drag_reorder.js +0 -0
- package/src/ui/components/fact_table.js +0 -0
- package/src/ui/components/fill_handle.js +322 -0
- package/src/ui/components/gallery_picker.js +19 -8
- package/src/ui/components/header_actions.js +55 -0
- package/src/ui/components/inline_renamer.js +0 -0
- package/src/ui/components/job_progress.js +51 -0
- package/src/ui/components/managed_window.js +27 -4
- package/src/ui/components/menu_bar.js +745 -0
- package/src/ui/components/modal.js +449 -75
- package/src/ui/components/notification_history.js +3 -3
- package/src/ui/components/progress.js +483 -0
- package/src/ui/components/record_list.js +0 -0
- package/src/ui/components/row_activation.js +0 -0
- package/src/ui/components/section_stack.js +298 -0
- package/src/ui/components/series.js +173 -0
- package/src/ui/components/settings_view.js +634 -0
- package/src/ui/components/slide_out_panel.js +43 -6
- package/src/ui/components/slider_field.js +275 -18
- package/src/ui/components/sortable_list.js +0 -0
- package/src/ui/components/staged_edits.js +361 -0
- package/src/ui/components/staged_history.js +172 -0
- package/src/ui/components/table_filter_chips.js +0 -0
- package/src/ui/components/table_state_store.js +0 -0
- package/src/ui/components/table_structure.js +182 -0
- package/src/ui/components/table_values.js +0 -0
- package/src/ui/components/tabs.js +497 -0
- package/src/ui/components/tag_input.js +215 -0
- package/src/ui/components/toast.js +25 -8
- package/src/ui/components/toggle_field.js +175 -0
- package/src/ui/components/tree_view.js +55 -34
- package/src/ui/components/tsv.js +74 -0
- package/src/ui/components/window_taskbar.js +0 -0
- package/src/ui/components/wizard.js +563 -0
- package/src/ui/controllers/window_chrome_controller.js +2 -2
- package/src/ui/notification_center.js +375 -119
- package/src/ui/utils/auto_scrollbars.js +0 -0
- package/src/ui/utils/markdown.js +709 -0
- package/src/ui/utils/native_context_menu.js +0 -0
- package/src/ui/utils/overlay_scrollbar.js +16 -12
- package/src/ui/utils/raf_resize_observer.js +0 -0
- package/src/ui/utils/scroll_lock.js +0 -0
- package/src/ui/utils/strip_scroll.js +55 -0
- package/src/ui/utils/tooltip_service.js +0 -8
- package/tiles.js +27 -0
- package/widgets.js +69 -0
- package/wm.js +28 -0
- package/dist/chunk-6JSOVNID.js.map +0 -7
- package/dist/chunk-B5Q53LAJ.js.map +0 -7
- package/dist/chunk-FBGP5KN5.js.map +0 -7
- package/dist/chunk-FL5KFNQH.js.map +0 -7
- package/dist/chunk-NLTOY57M.js.map +0 -7
- package/dist/chunk-NNFTETK3.js +0 -633
- package/dist/chunk-NNFTETK3.js.map +0 -7
- package/dist/chunk-O5OHMWBB.js +0 -284
- package/dist/chunk-O5OHMWBB.js.map +0 -7
- package/dist/chunk-RQM5Z2MU.js.map +0 -7
- package/dist/chunk-SN4LSOAR.js.map +0 -7
- package/dist/chunk-W6FBJLPW.js +0 -367
- package/dist/chunk-W6FBJLPW.js.map +0 -7
- 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
|
|
20
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
159
|
-
|
|
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-
|
|
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
|
package/src/flow/kit/strings.js
CHANGED
|
@@ -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: '
|
|
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
|
package/src/flow/kit/widgets.js
CHANGED
|
@@ -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
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
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
|
|
446
|
-
|
|
447
|
-
|
|
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
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
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
|
|