@flowfuse/nr-assistant 0.14.0 → 0.14.1-11df41b-202605121045.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -19
- package/package.json +1 -1
- package/resources/expertAutomations.js +471 -33
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# CHANGELOG
|
|
2
|
+
|
|
1
3
|
## [0.14.0](https://github.com/FlowFuse/nr-assistant/compare/v0.13.1...v0.14.0) (2026-05-07)
|
|
2
4
|
|
|
3
5
|
- feat(actions): add automation/open-palette-manager action (#314) @andypalmi
|
|
@@ -10,7 +12,7 @@
|
|
|
10
12
|
- chore(deps): bump hono from 4.12.14 to 4.12.18 (#308) @app/dependabot
|
|
11
13
|
- build(deps): bump protobufjs from 7.5.3 to 7.5.5 (#272) @app/dependabot
|
|
12
14
|
|
|
13
|
-
## 0.13.0
|
|
15
|
+
## [0.13.0](https://github.com/FlowFuse/nr-assistant/compare/v0.12.0...v0.13.0) (2026-05-06)
|
|
14
16
|
|
|
15
17
|
- ci: Use new project-automation workflow (#303)
|
|
16
18
|
- ci: Replace PAT with GitHub Application token in `Projects automations` workflow (#300)
|
|
@@ -53,7 +55,7 @@
|
|
|
53
55
|
- Update lint scripts to include all subdirectories (#255) @Steve-Mcl
|
|
54
56
|
- Use git ref_name for version instead of npm info (#252) @allthedoll
|
|
55
57
|
|
|
56
|
-
## 0.12.0
|
|
58
|
+
## [0.12.0](https://github.com/FlowFuse/nr-assistant/compare/v0.11.0...v0.12.0) (2026-04-08)
|
|
57
59
|
|
|
58
60
|
- Bump actions/create-github-app-token from 2.2.1 to 3.0.0 (#191)
|
|
59
61
|
- Bump actions/setup-node from 6.2.0 to 6.3.0 (#177)
|
|
@@ -85,7 +87,7 @@
|
|
|
85
87
|
- Bump hono from 4.12.3 to 4.12.5 (#166) @app/dependabot
|
|
86
88
|
- Bump minimatch from 3.1.2 to 3.1.5 (#161) @app/dependabot
|
|
87
89
|
|
|
88
|
-
## 0.11.0
|
|
90
|
+
## [0.11.0](https://github.com/FlowFuse/nr-assistant/compare/v0.10.2...v0.11.0) (2026-02-26)
|
|
89
91
|
|
|
90
92
|
- Bump flowfuse/github-actions-workflows/.github/workflows/publish_node_package.yml (#149)
|
|
91
93
|
- Bump flowfuse/github-actions-workflows/.github/workflows/build_node_package.yml (#148)
|
|
@@ -98,7 +100,7 @@
|
|
|
98
100
|
- Bump hono from 4.11.7 to 4.12.0 (#152) @app/dependabot
|
|
99
101
|
- Bump qs from 6.14.1 to 6.14.2 (#146) @app/dependabot
|
|
100
102
|
|
|
101
|
-
## 0.10.2
|
|
103
|
+
## [0.10.2](https://github.com/FlowFuse/nr-assistant/compare/v0.10.1...v0.10.2) (2026-02-12)
|
|
102
104
|
|
|
103
105
|
- Bump JS-DevTools/npm-publish from 4.1.4 to 4.1.5 (#136)
|
|
104
106
|
- Bump flowfuse/github-actions-workflows/.github/workflows/publish_node_package.yml (#141)
|
|
@@ -107,13 +109,13 @@
|
|
|
107
109
|
- Bump @modelcontextprotocol/sdk from 1.25.3 to 1.26.0 (#138) @app/dependabot
|
|
108
110
|
- Update public catalouge on release (#144) @hardillb
|
|
109
111
|
|
|
110
|
-
## 0.10.1
|
|
112
|
+
## [0.10.1](https://github.com/FlowFuse/nr-assistant/compare/v0.10.0...v0.10.1) (2026-01-30)
|
|
111
113
|
|
|
112
114
|
- Bump hono from 4.11.5 to 4.11.7 (#129) @app/dependabot
|
|
113
115
|
- Improve discoverability of supported features (#131) @Steve-Mcl
|
|
114
116
|
- Replace hard coded event mapping with dynamic registrations (#132) @Steve-Mcl
|
|
115
117
|
|
|
116
|
-
## 0.10.0
|
|
118
|
+
## [0.10.0](https://github.com/FlowFuse/nr-assistant/compare/v0.9.0...v0.10.0) (2026-01-27)
|
|
117
119
|
|
|
118
120
|
- Bump JS-DevTools/npm-publish from 4.1.3 to 4.1.4 (#110)
|
|
119
121
|
- Bump actions/checkout from 6.0.1 to 6.0.2 (#124)
|
|
@@ -122,7 +124,7 @@
|
|
|
122
124
|
- Bump flowfuse/github-actions-workflows/.github/workflows/publish_node_package.yml (#121)
|
|
123
125
|
- Add selection handling: `view:selection-changed` notifier and `get-selection` handler (#125) @cstns
|
|
124
126
|
|
|
125
|
-
## 0.9.0
|
|
127
|
+
## [0.9.0](https://github.com/FlowFuse/nr-assistant/compare/v0.8.0...v0.9.0) (2026-01-24)
|
|
126
128
|
|
|
127
129
|
- Update dependencies (#119) @Steve-Mcl
|
|
128
130
|
- Bump actions/setup-node from 6.1.0 to 6.2.0 (#113)
|
|
@@ -131,7 +133,7 @@
|
|
|
131
133
|
- Expose installed packages to flowfuse expert (#114) @cstns
|
|
132
134
|
- ci: Enable SAST (#109) @ppawlowski
|
|
133
135
|
|
|
134
|
-
## 0.8.0
|
|
136
|
+
## [0.8.0](https://github.com/FlowFuse/nr-assistant/compare/v0.7.0...v0.8.0) (2026-01-14)
|
|
135
137
|
|
|
136
138
|
- Bump JS-DevTools/npm-publish from 4.1.1 to 4.1.3 (#105)
|
|
137
139
|
- Bump flowfuse/github-actions-workflows/.github/workflows/publish_node_package.yml (#96)
|
|
@@ -143,7 +145,7 @@
|
|
|
143
145
|
- Bump flowfuse/github-actions-workflows/.github/workflows/publish_node_package.yml from 0.44.0 to 0.45.0 (#98) @app/dependabot
|
|
144
146
|
- Bump flowfuse/github-actions-workflows/.github/workflows/build_node_package.yml from 0.44.0 to 0.45.0 (#97) @app/dependabot
|
|
145
147
|
|
|
146
|
-
## 0.7.0
|
|
148
|
+
## [0.7.0](https://github.com/FlowFuse/nr-assistant/compare/v0.6.0...v0.7.0) (2025-12-09)
|
|
147
149
|
|
|
148
150
|
- Allow the assistant to be installed in standalone Node-RED instances (#89) @knolleary
|
|
149
151
|
|
|
@@ -161,33 +163,33 @@
|
|
|
161
163
|
- Bump body-parser from 2.2.0 to 2.2.1 (#88) @app/dependabot
|
|
162
164
|
- Bump js-yaml from 4.1.0 to 4.1.1 (#86) @app/dependabot
|
|
163
165
|
|
|
164
|
-
## 0.6.0
|
|
166
|
+
## [0.6.0](https://github.com/FlowFuse/nr-assistant/compare/v0.5.0...v0.6.0) (2025-09-05)
|
|
165
167
|
- Fix relative script source path (#77) @Steve-Mcl
|
|
166
168
|
- Add inline completions feature (#75) @Steve-Mcl
|
|
167
169
|
|
|
168
|
-
## 0.5.0
|
|
170
|
+
## [0.5.0](https://github.com/FlowFuse/nr-assistant/compare/v0.4.0...v0.5.0) (2025-08-28)
|
|
169
171
|
- Bump actions/checkout from 4.2.2 to 5.0.0 (#69)
|
|
170
172
|
- Bump flowfuse/github-actions-workflows from 0.40.0 to 0.42.0 (#68)
|
|
171
173
|
- Add tables codelens feature (#72) @Steve-Mcl
|
|
172
174
|
|
|
173
|
-
## 0.4.0
|
|
175
|
+
## [0.4.0](https://github.com/FlowFuse/nr-assistant/compare/v0.3.0...v0.4.0) (2025-07-30)
|
|
174
176
|
- update package for 0.4.0 release
|
|
175
177
|
- Bump flowfuse/github-actions-workflows from 0.39.0 to 0.40.0 (#60)
|
|
176
178
|
- Update imports (#64) @Steve-Mcl
|
|
177
179
|
- Implement node suggestions (#62) @Steve-Mcl
|
|
178
180
|
- Add copy to clipboard and generate comment node to explain dialog (#61) @Steve-Mcl
|
|
179
181
|
|
|
180
|
-
## 0.3.0
|
|
182
|
+
## [0.3.0](https://github.com/FlowFuse/nr-assistant/compare/v0.2.1...v0.3.0) (2025-06-27)
|
|
181
183
|
- Change assistant button to menu for exposing new Flows Explainer by @Steve-Mcl in #53
|
|
182
184
|
- Add menu shortcuts for menu items by @Steve-Mcl in #54
|
|
183
185
|
- Show flow explanation in dialog by @Steve-Mcl in #52
|
|
184
186
|
- Add codelens for CSS and DB2 ui-template by @Steve-Mcl in #56
|
|
185
187
|
|
|
186
|
-
## 0.2.1
|
|
188
|
+
## [0.2.1](https://github.com/FlowFuse/nr-assistant/compare/v0.2.0...v0.2.1) (2025-06-12)
|
|
187
189
|
- Improve README with visuals of what it does by @Steve-Mcl in #49
|
|
188
190
|
- V0.2.1 by @Steve-Mcl in #50
|
|
189
191
|
|
|
190
|
-
## 0.2.0
|
|
192
|
+
## [0.2.0](https://github.com/FlowFuse/nr-assistant/compare/v0.1.3...v0.2.0) (2025-06-11)
|
|
191
193
|
|
|
192
194
|
- Bump flowfuse/github-actions-workflows from 0.19.0 to 0.28.0 by @dependabot in #32
|
|
193
195
|
- Bump flowfuse/github-actions-workflows from 0.28.0 to 0.29.0 by @dependabot in #33
|
|
@@ -202,17 +204,17 @@
|
|
|
202
204
|
- Add initial MCP support by @Steve-Mcl in #44
|
|
203
205
|
- V0.2.0 by @Steve-Mcl in #47
|
|
204
206
|
|
|
205
|
-
## 0.1.3
|
|
207
|
+
## [0.1.3](https://github.com/FlowFuse/nr-assistant/compare/v0.1.2...v0.1.3) (2024-07-17)
|
|
206
208
|
|
|
207
209
|
- Fix icon on device agent by @Steve-Mcl in #29
|
|
208
210
|
- bump for 0.1.3 by @Steve-Mcl in #30
|
|
209
211
|
|
|
210
|
-
## 0.1.2
|
|
212
|
+
## [0.1.2](https://github.com/FlowFuse/nr-assistant/compare/v0.1.1...v0.1.2) (2024-07-16)
|
|
211
213
|
|
|
212
214
|
- Fix height of new icon on NR3.x by @Steve-Mcl in #26
|
|
213
215
|
- bump for 0.1.2 by @Steve-Mcl in #27
|
|
214
216
|
|
|
215
|
-
## 0.1.1
|
|
217
|
+
## [0.1.1](https://github.com/FlowFuse/nr-assistant/compare/v0.1.0...v0.1.1) (2024-07-16)
|
|
216
218
|
|
|
217
219
|
- ci: Add build and publish nightly package workflow by @ppawlowski in #7
|
|
218
220
|
- Bump tibdex/github-app-token from 1 to 2 by @dependabot in #11
|
|
@@ -223,7 +225,7 @@
|
|
|
223
225
|
- Add comma to settings.js by @kazuhitoyokoi in #22
|
|
224
226
|
- Improved messaging for error responses by @Steve-Mcl in #24
|
|
225
227
|
|
|
226
|
-
## 0.1.0
|
|
228
|
+
## 0.1.0 (2024-07-03)
|
|
227
229
|
|
|
228
230
|
- add automations by @Steve-Mcl in #1
|
|
229
231
|
- Bump JS-DevTools/npm-publish from 2 to 3 by @dependabot in #5
|
package/package.json
CHANGED
|
@@ -25,6 +25,14 @@ const GET_NODE_TYPES = 'automation/get-node-types'
|
|
|
25
25
|
const GET_PALETTE = 'automation/get-palette'
|
|
26
26
|
const LIST_CONFIG_NODES = 'automation/list-config-nodes'
|
|
27
27
|
const OPEN_PALETTE_MANAGER = 'automation/open-palette-manager'
|
|
28
|
+
const MANAGE_GROUPS = 'automation/manage-groups'
|
|
29
|
+
|
|
30
|
+
const ERROR_CODES = Object.freeze({
|
|
31
|
+
GROUP_OPERATION_REQUIRED: 'GROUP_OPERATION_REQUIRED',
|
|
32
|
+
FORBIDDEN_PROPERTY: 'FORBIDDEN_PROPERTY'
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
const LINK_NODE_TYPES = ['link in', 'link out', 'link call']
|
|
28
36
|
|
|
29
37
|
/**
|
|
30
38
|
* @typedef {SELECT_NODES
|
|
@@ -50,7 +58,8 @@ const OPEN_PALETTE_MANAGER = 'automation/open-palette-manager'
|
|
|
50
58
|
* |GET_NODE_TYPES
|
|
51
59
|
* |GET_PALETTE
|
|
52
60
|
* |LIST_CONFIG_NODES
|
|
53
|
-
* |OPEN_PALETTE_MANAGER
|
|
61
|
+
* |OPEN_PALETTE_MANAGER
|
|
62
|
+
* |MANAGE_GROUPS} ExpertAutomationsActionsEnum
|
|
54
63
|
*/
|
|
55
64
|
|
|
56
65
|
export class ExpertAutomations extends ExpertActionsInterface {
|
|
@@ -187,6 +196,10 @@ export class ExpertAutomations extends ExpertActionsInterface {
|
|
|
187
196
|
tabId: {
|
|
188
197
|
type: 'string',
|
|
189
198
|
description: 'Optional parameter to only get nodes on a specific workspace. Exclude this parameter to get node from all workspaces.'
|
|
199
|
+
},
|
|
200
|
+
full: {
|
|
201
|
+
type: 'boolean',
|
|
202
|
+
description: 'When true, returns the raw node objects instead of condensed summaries.'
|
|
190
203
|
}
|
|
191
204
|
}
|
|
192
205
|
}
|
|
@@ -268,6 +281,10 @@ export class ExpertAutomations extends ExpertActionsInterface {
|
|
|
268
281
|
type: 'array',
|
|
269
282
|
items: { type: 'string' },
|
|
270
283
|
description: 'IDs of nodes to remove from the canvas'
|
|
284
|
+
},
|
|
285
|
+
reconnectWires: {
|
|
286
|
+
type: 'boolean',
|
|
287
|
+
description: 'If true, reconnects wires around removed nodes (pass-through). Default: false'
|
|
271
288
|
}
|
|
272
289
|
},
|
|
273
290
|
required: ['ids']
|
|
@@ -377,6 +394,61 @@ export class ExpertAutomations extends ExpertActionsInterface {
|
|
|
377
394
|
}
|
|
378
395
|
}
|
|
379
396
|
}
|
|
397
|
+
},
|
|
398
|
+
[MANAGE_GROUPS]: {
|
|
399
|
+
params: {
|
|
400
|
+
type: 'object',
|
|
401
|
+
properties: {
|
|
402
|
+
operations: {
|
|
403
|
+
type: 'array',
|
|
404
|
+
items: {
|
|
405
|
+
type: 'object',
|
|
406
|
+
properties: {
|
|
407
|
+
op: {
|
|
408
|
+
type: 'string',
|
|
409
|
+
enum: ['create', 'update', 'manage-members', 'delete'],
|
|
410
|
+
description: 'Operation to perform'
|
|
411
|
+
},
|
|
412
|
+
id: {
|
|
413
|
+
type: 'string',
|
|
414
|
+
description: 'Group ID (required for update, manage-members, and delete)'
|
|
415
|
+
},
|
|
416
|
+
nodeIds: {
|
|
417
|
+
type: 'array',
|
|
418
|
+
items: { type: 'string' },
|
|
419
|
+
description: 'Node IDs: required for create and manage-members'
|
|
420
|
+
},
|
|
421
|
+
name: {
|
|
422
|
+
type: 'string',
|
|
423
|
+
description: 'Group display name (optional for create and update)'
|
|
424
|
+
},
|
|
425
|
+
mode: {
|
|
426
|
+
type: 'string',
|
|
427
|
+
enum: ['add', 'remove'],
|
|
428
|
+
description: 'manage-members only — "add" moves nodes into the group, "remove" takes them out'
|
|
429
|
+
},
|
|
430
|
+
style: {
|
|
431
|
+
type: 'object',
|
|
432
|
+
description: 'Visual style: stroke/border-color, fill, color, stroke-opacity, fill-opacity, label, label-position',
|
|
433
|
+
properties: {
|
|
434
|
+
stroke: { type: 'string' },
|
|
435
|
+
'border-color': { type: 'string' },
|
|
436
|
+
fill: { type: 'string' },
|
|
437
|
+
color: { type: 'string' },
|
|
438
|
+
'stroke-opacity': { type: 'number' },
|
|
439
|
+
'fill-opacity': { type: 'number' },
|
|
440
|
+
label: { type: 'boolean' },
|
|
441
|
+
'label-position': { type: 'string', enum: ['nw', 'n', 'ne', 'sw', 's', 'se'] }
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
},
|
|
445
|
+
required: ['op']
|
|
446
|
+
},
|
|
447
|
+
description: 'Array of group operations to execute sequentially'
|
|
448
|
+
}
|
|
449
|
+
},
|
|
450
|
+
required: ['operations']
|
|
451
|
+
}
|
|
380
452
|
}
|
|
381
453
|
})
|
|
382
454
|
|
|
@@ -821,7 +893,7 @@ export class ExpertAutomations extends ExpertActionsInterface {
|
|
|
821
893
|
* @param {string} id - workspace ID to show
|
|
822
894
|
*/
|
|
823
895
|
showWorkspace (id) {
|
|
824
|
-
|
|
896
|
+
this._assertWorkspaceExists(id)
|
|
825
897
|
this.RED.workspaces.show(id)
|
|
826
898
|
}
|
|
827
899
|
|
|
@@ -831,9 +903,24 @@ export class ExpertAutomations extends ExpertActionsInterface {
|
|
|
831
903
|
* @returns {boolean} true if the workspace exists, false otherwise
|
|
832
904
|
*/
|
|
833
905
|
hasWorkspace (id) {
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
906
|
+
return !!this.RED.nodes.workspace(id)
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
/**
|
|
910
|
+
* Throw if the workspace does not exist.
|
|
911
|
+
* @param {string} id - workspace ID
|
|
912
|
+
*/
|
|
913
|
+
_assertWorkspaceExists (id) {
|
|
914
|
+
if (!this.hasWorkspace(id)) throw new Error(`Workspace ${id} not found`)
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
/**
|
|
918
|
+
* Throw if the workspace tab is locked. No-op when tabId is falsy (e.g. config nodes).
|
|
919
|
+
* @param {string|null|undefined} tabId - workspace tab ID
|
|
920
|
+
*/
|
|
921
|
+
_assertWorkspaceNotLocked (tabId) {
|
|
922
|
+
if (!tabId) return
|
|
923
|
+
if (this.RED.workspaces.isLocked(tabId)) throw new Error(`Workspace ${tabId} is locked`)
|
|
837
924
|
}
|
|
838
925
|
|
|
839
926
|
closeSearch () { this.RED.search.hide() }
|
|
@@ -918,9 +1005,8 @@ export class ExpertAutomations extends ExpertActionsInterface {
|
|
|
918
1005
|
// Validate all target tabs exist and are not locked
|
|
919
1006
|
const uniqueZs = [...new Set(prepared.map(n => n.z).filter(Boolean))]
|
|
920
1007
|
for (const z of uniqueZs) {
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
if (ws.locked) throw new Error(`Workspace tab ${z} is locked`)
|
|
1008
|
+
this._assertWorkspaceExists(z)
|
|
1009
|
+
this._assertWorkspaceNotLocked(z)
|
|
924
1010
|
}
|
|
925
1011
|
// Pre-import: reject if any node ID already exists on the canvas
|
|
926
1012
|
if (!generateIds) {
|
|
@@ -953,9 +1039,13 @@ export class ExpertAutomations extends ExpertActionsInterface {
|
|
|
953
1039
|
|
|
954
1040
|
/**
|
|
955
1041
|
* Remove one or more nodes from the live NR4 canvas by ID.
|
|
1042
|
+
* Delegates to Node-RED's core:delete-selection action to ensure all internal data
|
|
1043
|
+
* structures (including group membership arrays) are properly cleaned up.
|
|
956
1044
|
* @param {string[]} ids - node IDs to remove
|
|
1045
|
+
* @param {object} [options]
|
|
1046
|
+
* @param {boolean} [options.reconnectWires=false] - reconnect wires around removed nodes
|
|
957
1047
|
*/
|
|
958
|
-
removeNodes (ids) {
|
|
1048
|
+
removeNodes (ids, { reconnectWires = false } = {}) {
|
|
959
1049
|
// Resolve all nodes once and check for missing
|
|
960
1050
|
const nodes = ids.map(id => {
|
|
961
1051
|
const node = this.RED.nodes.node(id)
|
|
@@ -964,19 +1054,15 @@ export class ExpertAutomations extends ExpertActionsInterface {
|
|
|
964
1054
|
})
|
|
965
1055
|
// Check if any node's workspace is locked
|
|
966
1056
|
for (const node of nodes) {
|
|
967
|
-
|
|
968
|
-
throw new Error(`Cannot remove node ${node.id} — workspace ${node.z} is locked`)
|
|
969
|
-
}
|
|
1057
|
+
this._assertWorkspaceNotLocked(node.z)
|
|
970
1058
|
}
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
1059
|
+
this.RED.view.select({ nodes })
|
|
1060
|
+
this._verifySelection(nodes)
|
|
1061
|
+
if (reconnectWires) {
|
|
1062
|
+
this.RED.actions.invoke('core:delete-selection-and-reconnect')
|
|
1063
|
+
} else {
|
|
1064
|
+
this.RED.actions.invoke('core:delete-selection')
|
|
975
1065
|
}
|
|
976
|
-
this.RED.history.push({ t: 'delete', nodes, links: allRemovedLinks, dirty: this.RED.nodes.dirty() })
|
|
977
|
-
this.RED.nodes.dirty(true)
|
|
978
|
-
this.RED.view.updateActive()
|
|
979
|
-
this.RED.view.redraw()
|
|
980
1066
|
}
|
|
981
1067
|
|
|
982
1068
|
/**
|
|
@@ -998,9 +1084,7 @@ export class ExpertAutomations extends ExpertActionsInterface {
|
|
|
998
1084
|
throw new Error('Source and target nodes must be on the same tab')
|
|
999
1085
|
}
|
|
1000
1086
|
// Check workspace is not locked
|
|
1001
|
-
|
|
1002
|
-
throw new Error(`Cannot modify wires — workspace ${sourceNode.z} is locked`)
|
|
1003
|
-
}
|
|
1087
|
+
this._assertWorkspaceNotLocked(sourceNode.z)
|
|
1004
1088
|
// Validate output port exists on source
|
|
1005
1089
|
const port = output ?? 0
|
|
1006
1090
|
if (port >= (sourceNode.outputs || 0)) {
|
|
@@ -1069,12 +1153,8 @@ export class ExpertAutomations extends ExpertActionsInterface {
|
|
|
1069
1153
|
throw new Error(`Source node ${source} is a link call in dynamic mode and cannot have static links`)
|
|
1070
1154
|
}
|
|
1071
1155
|
// Check workspace locks for both nodes
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
}
|
|
1075
|
-
if (targetNode.z && targetNode.z !== sourceNode.z && this.RED.workspaces.isLocked(targetNode.z)) {
|
|
1076
|
-
throw new Error(`Cannot modify links — workspace ${targetNode.z} is locked`)
|
|
1077
|
-
}
|
|
1156
|
+
this._assertWorkspaceNotLocked(sourceNode.z)
|
|
1157
|
+
if (targetNode.z !== sourceNode.z) this._assertWorkspaceNotLocked(targetNode.z)
|
|
1078
1158
|
const isBidirectional = sourceNode.type === 'link out'
|
|
1079
1159
|
const sourceLinks = sourceNode.links || []
|
|
1080
1160
|
const targetLinks = targetNode.links || []
|
|
@@ -1197,6 +1277,30 @@ export class ExpertAutomations extends ExpertActionsInterface {
|
|
|
1197
1277
|
break
|
|
1198
1278
|
|
|
1199
1279
|
case UPDATE_NODE: {
|
|
1280
|
+
if (this.RED.nodes.group(params.id)) {
|
|
1281
|
+
result.error = `Node ${params.id} is a group — group nodes cannot be updated via this action`
|
|
1282
|
+
result.errorCode = ERROR_CODES.GROUP_OPERATION_REQUIRED
|
|
1283
|
+
result.success = false
|
|
1284
|
+
break
|
|
1285
|
+
}
|
|
1286
|
+
if (params.properties && 'wires' in params.properties) {
|
|
1287
|
+
result.error = `Node ${params.id}: "wires" cannot be set directly — wire connections must be managed via a dedicated action`
|
|
1288
|
+
result.errorCode = ERROR_CODES.FORBIDDEN_PROPERTY
|
|
1289
|
+
result.success = false
|
|
1290
|
+
break
|
|
1291
|
+
}
|
|
1292
|
+
if (params.properties && 'links' in params.properties && LINK_NODE_TYPES.includes(this.RED.nodes.node(params.id)?.type)) {
|
|
1293
|
+
result.error = `Node ${params.id}: "links" cannot be set directly — link connections must be managed via a dedicated action`
|
|
1294
|
+
result.errorCode = ERROR_CODES.FORBIDDEN_PROPERTY
|
|
1295
|
+
result.success = false
|
|
1296
|
+
break
|
|
1297
|
+
}
|
|
1298
|
+
if (params.properties && 'g' in params.properties) {
|
|
1299
|
+
result.error = `Node ${params.id}: "g" cannot be set directly — group membership must be managed via a dedicated action`
|
|
1300
|
+
result.errorCode = ERROR_CODES.FORBIDDEN_PROPERTY
|
|
1301
|
+
result.success = false
|
|
1302
|
+
break
|
|
1303
|
+
}
|
|
1200
1304
|
await this.updateNode(params.id, params.properties, params.patches)
|
|
1201
1305
|
const updatedNode = this.RED.nodes.node(params.id)
|
|
1202
1306
|
result.data = this._summarizeNode(updatedNode)
|
|
@@ -1216,15 +1320,18 @@ export class ExpertAutomations extends ExpertActionsInterface {
|
|
|
1216
1320
|
case GET_FLOW:
|
|
1217
1321
|
result.flows = this.getFlow()
|
|
1218
1322
|
if (result.flows && Array.isArray(result.flows) && result.flows.length > 0) {
|
|
1219
|
-
if (params.type) {
|
|
1323
|
+
if (params && params.type) {
|
|
1220
1324
|
// filter by type if specified (e.g. "tab", "subflow", or any node type)
|
|
1221
1325
|
result.flows = result.flows.filter(f => f.type === params.type)
|
|
1222
1326
|
}
|
|
1223
|
-
if (params.tabId) {
|
|
1327
|
+
if (params && params.tabId) {
|
|
1224
1328
|
// filter by parent tab ID if specified (for nodes/config nodes)
|
|
1225
1329
|
result.flows = result.flows.filter(f => f.z === params.tabId)
|
|
1226
1330
|
}
|
|
1227
1331
|
}
|
|
1332
|
+
if (!params || !params.full) {
|
|
1333
|
+
result.flows = (result.flows || []).map(f => this._summarizeFlowItem(f))
|
|
1334
|
+
}
|
|
1228
1335
|
result.success = true
|
|
1229
1336
|
break
|
|
1230
1337
|
|
|
@@ -1265,6 +1372,34 @@ export class ExpertAutomations extends ExpertActionsInterface {
|
|
|
1265
1372
|
break
|
|
1266
1373
|
|
|
1267
1374
|
case ADD_NODES: {
|
|
1375
|
+
const groupNodes = (params.nodes || []).filter(n => n.type === 'group')
|
|
1376
|
+
if (groupNodes.length > 0) {
|
|
1377
|
+
result.error = `Nodes [${groupNodes.map(n => n.id).join(', ')}] are group nodes — group nodes cannot be added via this action`
|
|
1378
|
+
result.errorCode = ERROR_CODES.GROUP_OPERATION_REQUIRED
|
|
1379
|
+
result.success = false
|
|
1380
|
+
break
|
|
1381
|
+
}
|
|
1382
|
+
const wiresNode = (params.nodes || []).find(n => n.wires !== undefined)
|
|
1383
|
+
if (wiresNode) {
|
|
1384
|
+
result.error = `Node ${wiresNode.id}: "wires" cannot be set directly — wire connections must be managed via a dedicated action`
|
|
1385
|
+
result.errorCode = ERROR_CODES.FORBIDDEN_PROPERTY
|
|
1386
|
+
result.success = false
|
|
1387
|
+
break
|
|
1388
|
+
}
|
|
1389
|
+
const linksNode = (params.nodes || []).find(n => n.links !== undefined && LINK_NODE_TYPES.includes(n.type))
|
|
1390
|
+
if (linksNode) {
|
|
1391
|
+
result.error = `Node ${linksNode.id}: "links" cannot be set directly — link connections must be managed via a dedicated action`
|
|
1392
|
+
result.errorCode = ERROR_CODES.FORBIDDEN_PROPERTY
|
|
1393
|
+
result.success = false
|
|
1394
|
+
break
|
|
1395
|
+
}
|
|
1396
|
+
const gNode = (params.nodes || []).find(n => n.g !== undefined)
|
|
1397
|
+
if (gNode) {
|
|
1398
|
+
result.error = `Node ${gNode.id}: "g" cannot be set directly — group membership must be managed via a dedicated action`
|
|
1399
|
+
result.errorCode = ERROR_CODES.FORBIDDEN_PROPERTY
|
|
1400
|
+
result.success = false
|
|
1401
|
+
break
|
|
1402
|
+
}
|
|
1268
1403
|
this.addNodes(params.nodes, { generateIds: params.generateIds ?? false })
|
|
1269
1404
|
const addedNodes = params.nodes.map(n => this.RED.nodes.node(n.id)).filter(Boolean)
|
|
1270
1405
|
if (this.RED.editor?.validateNode) {
|
|
@@ -1276,10 +1411,25 @@ export class ExpertAutomations extends ExpertActionsInterface {
|
|
|
1276
1411
|
}
|
|
1277
1412
|
break
|
|
1278
1413
|
|
|
1279
|
-
case REMOVE_NODES:
|
|
1280
|
-
|
|
1414
|
+
case REMOVE_NODES: {
|
|
1415
|
+
const groupIds = (params.ids || []).filter(id => !!this.RED.nodes.group(id))
|
|
1416
|
+
if (groupIds.length > 0) {
|
|
1417
|
+
result.error = `IDs [${groupIds.join(', ')}] are group nodes — group nodes cannot be removed via this action`
|
|
1418
|
+
result.errorCode = ERROR_CODES.GROUP_OPERATION_REQUIRED
|
|
1419
|
+
result.success = false
|
|
1420
|
+
break
|
|
1421
|
+
}
|
|
1422
|
+
const groupedNodes = (params.ids || []).map(id => this.RED.nodes.node(id)).filter(n => n && n.g)
|
|
1423
|
+
if (groupedNodes.length > 0) {
|
|
1424
|
+
result.error = `Node ${groupedNodes[0].id}: cannot be removed while it is a member of a group — group membership must be managed via a dedicated action first`
|
|
1425
|
+
result.errorCode = ERROR_CODES.FORBIDDEN_PROPERTY
|
|
1426
|
+
result.success = false
|
|
1427
|
+
break
|
|
1428
|
+
}
|
|
1429
|
+
this.removeNodes(params.ids, { reconnectWires: params.reconnectWires ?? false })
|
|
1281
1430
|
result.data = { removed: params.ids }
|
|
1282
1431
|
result.success = true
|
|
1432
|
+
}
|
|
1283
1433
|
break
|
|
1284
1434
|
|
|
1285
1435
|
case SET_WIRES:
|
|
@@ -1362,6 +1512,57 @@ export class ExpertAutomations extends ExpertActionsInterface {
|
|
|
1362
1512
|
})
|
|
1363
1513
|
result.success = true
|
|
1364
1514
|
break
|
|
1515
|
+
case MANAGE_GROUPS: {
|
|
1516
|
+
const operations = params.operations
|
|
1517
|
+
if (!Array.isArray(operations) || operations.length === 0) {
|
|
1518
|
+
throw new Error('operations array is required and must not be empty')
|
|
1519
|
+
}
|
|
1520
|
+
const results = []
|
|
1521
|
+
for (const entry of operations) {
|
|
1522
|
+
const op = entry.op
|
|
1523
|
+
if (!op) throw new Error('op is required for each manage-groups operation')
|
|
1524
|
+
switch (op) {
|
|
1525
|
+
case 'create': {
|
|
1526
|
+
const group = this.createGroup(entry.nodeIds, entry.name, { style: entry.style, env: entry.env, id: entry.id })
|
|
1527
|
+
results.push({ op: 'create', ...this._summarizeGroup(group) })
|
|
1528
|
+
break
|
|
1529
|
+
}
|
|
1530
|
+
case 'update': {
|
|
1531
|
+
if (!entry.id) throw new Error('id is required for update')
|
|
1532
|
+
const group = this.updateGroup(entry.id, {
|
|
1533
|
+
name: entry.name,
|
|
1534
|
+
style: entry.style,
|
|
1535
|
+
env: entry.env
|
|
1536
|
+
})
|
|
1537
|
+
results.push({ op: 'update', ...this._summarizeGroup(group) })
|
|
1538
|
+
break
|
|
1539
|
+
}
|
|
1540
|
+
case 'manage-members': {
|
|
1541
|
+
if (!entry.id) throw new Error('id is required for manage-members')
|
|
1542
|
+
if (!entry.nodeIds || entry.nodeIds.length === 0) throw new Error('nodeIds is required for manage-members')
|
|
1543
|
+
const mode = entry.mode
|
|
1544
|
+
if (mode !== 'add' && mode !== 'remove') throw new Error('mode must be "add" or "remove"')
|
|
1545
|
+
const group = this.updateGroup(entry.id, {
|
|
1546
|
+
nodeIds: entry.nodeIds,
|
|
1547
|
+
remove: mode === 'remove'
|
|
1548
|
+
})
|
|
1549
|
+
results.push({ op: 'manage-members', mode, ...this._summarizeGroup(group) })
|
|
1550
|
+
break
|
|
1551
|
+
}
|
|
1552
|
+
case 'delete': {
|
|
1553
|
+
if (!entry.id) throw new Error('id is required for delete')
|
|
1554
|
+
this.deleteGroup(entry.id)
|
|
1555
|
+
results.push({ op: 'delete', deleted: entry.id })
|
|
1556
|
+
break
|
|
1557
|
+
}
|
|
1558
|
+
default:
|
|
1559
|
+
throw new Error(`Unknown manage-groups op: ${op}`)
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
result.data = results
|
|
1563
|
+
result.success = true
|
|
1564
|
+
break
|
|
1565
|
+
}
|
|
1365
1566
|
default:
|
|
1366
1567
|
result.handled = false
|
|
1367
1568
|
result.success = false
|
|
@@ -1411,6 +1612,185 @@ export class ExpertAutomations extends ExpertActionsInterface {
|
|
|
1411
1612
|
}
|
|
1412
1613
|
}
|
|
1413
1614
|
|
|
1615
|
+
/**
|
|
1616
|
+
* Create a group containing the specified nodes.
|
|
1617
|
+
* Uses Node-RED's core:group-selection action after selecting the target nodes.
|
|
1618
|
+
* @param {string[]} nodeIds - IDs of nodes to group
|
|
1619
|
+
* @param {string} [name] - optional group display name
|
|
1620
|
+
* @param {object} [opts]
|
|
1621
|
+
* @param {object} [opts.style] - visual style overrides
|
|
1622
|
+
* @param {Array} [opts.env] - environment variables for nodes inside the group
|
|
1623
|
+
* @param {string} [opts.id] - explicit group ID; auto-generated if omitted
|
|
1624
|
+
* @returns {object} the created group node
|
|
1625
|
+
*/
|
|
1626
|
+
createGroup (nodeIds, name, { style, env, id } = {}) {
|
|
1627
|
+
if (!nodeIds || nodeIds.length === 0) {
|
|
1628
|
+
throw new Error('nodeIds is required and must not be empty')
|
|
1629
|
+
}
|
|
1630
|
+
const nodes = nodeIds.map(id => {
|
|
1631
|
+
const n = this.RED.nodes.node(id)
|
|
1632
|
+
if (!n) throw new Error(`Node ${id} not found`)
|
|
1633
|
+
return n
|
|
1634
|
+
})
|
|
1635
|
+
const tabs = new Set(nodes.map(n => n.z).filter(Boolean))
|
|
1636
|
+
if (tabs.size > 1) {
|
|
1637
|
+
throw new Error('All nodes must be on the same tab to form a group')
|
|
1638
|
+
}
|
|
1639
|
+
const tabId = tabs.size === 1 ? [...tabs][0] : null
|
|
1640
|
+
this._assertWorkspaceNotLocked(tabId)
|
|
1641
|
+
|
|
1642
|
+
// Snapshot existing group IDs so we can identify the newly created one
|
|
1643
|
+
const existingGroupIds = new Set()
|
|
1644
|
+
this.RED.nodes.eachGroup(g => existingGroupIds.add(g.id))
|
|
1645
|
+
|
|
1646
|
+
this.RED.view.select({ nodes })
|
|
1647
|
+
this._verifySelection(nodes)
|
|
1648
|
+
this.RED.actions.invoke('core:group-selection')
|
|
1649
|
+
|
|
1650
|
+
const newGroups = []
|
|
1651
|
+
this.RED.nodes.eachGroup(g => {
|
|
1652
|
+
if (!existingGroupIds.has(g.id)) newGroups.push(g)
|
|
1653
|
+
})
|
|
1654
|
+
|
|
1655
|
+
if (newGroups.length === 0) {
|
|
1656
|
+
throw new Error('Group creation failed: core:group-selection did not create a new group')
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
const newGroup = newGroups[0]
|
|
1660
|
+
const missingIds = nodeIds.filter(nid => !newGroup.nodes?.some(n => (n.id || n) === nid))
|
|
1661
|
+
if (missingIds.length > 0) {
|
|
1662
|
+
throw new Error(`Group created but missing nodes: ${missingIds.join(', ')}`)
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
// Rename to the requested ID if one was provided and differs from the auto-generated one
|
|
1666
|
+
if (id !== undefined && id !== newGroup.id) {
|
|
1667
|
+
if (this.RED.nodes.node(id) || this.RED.nodes.group(id)) {
|
|
1668
|
+
throw new Error(`Group ID ${id} is already in use`)
|
|
1669
|
+
}
|
|
1670
|
+
const oldId = newGroup.id
|
|
1671
|
+
this.RED.nodes.remove(oldId)
|
|
1672
|
+
newGroup.id = id
|
|
1673
|
+
;(newGroup.nodes || []).forEach(n => {
|
|
1674
|
+
const nodeRef = typeof n === 'object' ? n : this.RED.nodes.node(n)
|
|
1675
|
+
if (nodeRef && nodeRef.g === oldId) nodeRef.g = id
|
|
1676
|
+
})
|
|
1677
|
+
this.RED.nodes.add(newGroup)
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
const changes = {}
|
|
1681
|
+
if (name) {
|
|
1682
|
+
changes.name = newGroup.name
|
|
1683
|
+
newGroup.name = name
|
|
1684
|
+
}
|
|
1685
|
+
if (style) {
|
|
1686
|
+
changes.style = { ...newGroup.style }
|
|
1687
|
+
newGroup.style = Object.assign({}, newGroup.style || {}, style)
|
|
1688
|
+
}
|
|
1689
|
+
if (env !== undefined) {
|
|
1690
|
+
changes.env = newGroup.env
|
|
1691
|
+
newGroup.env = env
|
|
1692
|
+
}
|
|
1693
|
+
if (Object.keys(changes).length > 0) {
|
|
1694
|
+
newGroup.changed = true
|
|
1695
|
+
newGroup.dirty = true
|
|
1696
|
+
this.RED.history.push({ t: 'edit', node: newGroup, changes, changed: false, dirty: this.RED.nodes.dirty() })
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
this.RED.nodes.dirty(true)
|
|
1700
|
+
this.RED.view.redraw()
|
|
1701
|
+
return newGroup
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1704
|
+
/**
|
|
1705
|
+
* Update an existing group: rename, add/remove nodes, change style.
|
|
1706
|
+
* @param {string} id - group ID
|
|
1707
|
+
* @param {object} updates - { name?, addNodeIds?, removeNodeIds?, style? }
|
|
1708
|
+
* @returns {object} the updated group node
|
|
1709
|
+
*/
|
|
1710
|
+
updateGroup (id, { name, nodeIds, remove, style, env } = {}) {
|
|
1711
|
+
let group = this.RED.nodes.group(id)
|
|
1712
|
+
if (!group) throw new Error(`Group ${id} not found`)
|
|
1713
|
+
this._assertWorkspaceNotLocked(group.z)
|
|
1714
|
+
|
|
1715
|
+
if (nodeIds && nodeIds.length > 0) {
|
|
1716
|
+
if (remove) {
|
|
1717
|
+
const nodes = nodeIds.map(nodeId => {
|
|
1718
|
+
const node = this.RED.nodes.node(nodeId)
|
|
1719
|
+
if (!node) throw new Error(`Node ${nodeId} not found`)
|
|
1720
|
+
if (node.g !== group.id) throw new Error(`Node ${nodeId} is not in group ${id}`)
|
|
1721
|
+
return node
|
|
1722
|
+
})
|
|
1723
|
+
this.RED.view.select({ nodes })
|
|
1724
|
+
this._verifySelection(nodes)
|
|
1725
|
+
this.RED.actions.invoke('core:remove-selection-from-group')
|
|
1726
|
+
} else {
|
|
1727
|
+
const existingGroupIds = new Set()
|
|
1728
|
+
this.RED.nodes.eachGroup(g => existingGroupIds.add(g.id))
|
|
1729
|
+
|
|
1730
|
+
const newNodes = nodeIds.map(nodeId => {
|
|
1731
|
+
const node = this.RED.nodes.node(nodeId)
|
|
1732
|
+
if (!node) throw new Error(`Node ${nodeId} not found`)
|
|
1733
|
+
if (node.z !== group.z) throw new Error(`Node ${nodeId} is on tab ${node.z} but group is on tab ${group.z}`)
|
|
1734
|
+
return node
|
|
1735
|
+
})
|
|
1736
|
+
this.RED.view.select({ nodes: [group, ...newNodes] })
|
|
1737
|
+
this._verifySelection([group, ...newNodes])
|
|
1738
|
+
this.RED.actions.invoke('core:merge-selection-to-group')
|
|
1739
|
+
|
|
1740
|
+
const newGroups = []
|
|
1741
|
+
this.RED.nodes.eachGroup(g => {
|
|
1742
|
+
if (!existingGroupIds.has(g.id)) newGroups.push(g)
|
|
1743
|
+
})
|
|
1744
|
+
if (newGroups.length === 0) {
|
|
1745
|
+
throw new Error('Add nodes failed: core:merge-selection-to-group did not create a new group')
|
|
1746
|
+
}
|
|
1747
|
+
group = newGroups[0]
|
|
1748
|
+
}
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
const changes = {}
|
|
1752
|
+
|
|
1753
|
+
if (name !== undefined) {
|
|
1754
|
+
changes.name = group.name
|
|
1755
|
+
group.name = name
|
|
1756
|
+
}
|
|
1757
|
+
|
|
1758
|
+
if (style) {
|
|
1759
|
+
changes.style = { ...group.style }
|
|
1760
|
+
group.style = Object.assign({}, group.style || {}, style)
|
|
1761
|
+
}
|
|
1762
|
+
|
|
1763
|
+
if (env !== undefined) {
|
|
1764
|
+
changes.env = group.env
|
|
1765
|
+
group.env = env
|
|
1766
|
+
}
|
|
1767
|
+
|
|
1768
|
+
if (Object.keys(changes).length > 0) {
|
|
1769
|
+
group.changed = true
|
|
1770
|
+
group.dirty = true
|
|
1771
|
+
this.RED.history.push({ t: 'edit', node: group, changes, changed: false, dirty: this.RED.nodes.dirty() })
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1774
|
+
this.RED.nodes.dirty(true)
|
|
1775
|
+
this.RED.view.redraw()
|
|
1776
|
+
return group
|
|
1777
|
+
}
|
|
1778
|
+
|
|
1779
|
+
/**
|
|
1780
|
+
* Delete a group (ungroup its nodes).
|
|
1781
|
+
* @param {string} id - group ID
|
|
1782
|
+
*/
|
|
1783
|
+
deleteGroup (id) {
|
|
1784
|
+
const group = this.RED.nodes.group(id)
|
|
1785
|
+
if (!group) throw new Error(`Group ${id} not found`)
|
|
1786
|
+
this._assertWorkspaceNotLocked(group.z)
|
|
1787
|
+
this.RED.view.select({ nodes: [group] })
|
|
1788
|
+
this._verifySelection([group])
|
|
1789
|
+
this.RED.actions.invoke('core:ungroup-selection')
|
|
1790
|
+
this.RED.nodes.dirty(true)
|
|
1791
|
+
this.RED.view.redraw()
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1414
1794
|
_summarizeNode (node) {
|
|
1415
1795
|
if (!node) return null
|
|
1416
1796
|
const s = { id: node.id }
|
|
@@ -1420,7 +1800,65 @@ export class ExpertAutomations extends ExpertActionsInterface {
|
|
|
1420
1800
|
if (node.x !== undefined) s.x = node.x
|
|
1421
1801
|
if (node.y !== undefined) s.y = node.y
|
|
1422
1802
|
if (node.z !== undefined) s.z = node.z
|
|
1803
|
+
if (node.wires !== undefined) s.wires = node.wires
|
|
1804
|
+
if (node.links !== undefined) s.links = node.links
|
|
1423
1805
|
if (node.valid !== undefined) s.valid = node.valid
|
|
1424
1806
|
return s
|
|
1425
1807
|
}
|
|
1808
|
+
|
|
1809
|
+
/**
|
|
1810
|
+
* Defensive check: verify the view selection contains exactly the expected nodes.
|
|
1811
|
+
* Guards against editor states where RED.view.select() is silently ignored,
|
|
1812
|
+
* which would cause core actions to act on unintended nodes.
|
|
1813
|
+
* @param {object[]} expectedNodes
|
|
1814
|
+
*/
|
|
1815
|
+
_verifySelection (expectedNodes) {
|
|
1816
|
+
const selection = this.RED.view.selection()
|
|
1817
|
+
const selectedNodes = selection?.nodes || []
|
|
1818
|
+
const expectedIds = expectedNodes.map(n => n.id)
|
|
1819
|
+
const selectedIds = selectedNodes.map(n => n.id)
|
|
1820
|
+
const expectedSet = new Set(expectedIds)
|
|
1821
|
+
const selectedSet = new Set(selectedIds)
|
|
1822
|
+
if (selectedSet.size !== expectedSet.size || ![...expectedSet].every(id => selectedSet.has(id))) {
|
|
1823
|
+
throw new Error(
|
|
1824
|
+
`Selection mismatch: expected [${expectedIds.join(', ')}] but selection is [${selectedIds.join(', ')}] — editor may be in an unexpected state`
|
|
1825
|
+
)
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1828
|
+
|
|
1829
|
+
_summarizeSubflow (subflow) {
|
|
1830
|
+
if (!subflow) return null
|
|
1831
|
+
return {
|
|
1832
|
+
id: subflow.id,
|
|
1833
|
+
type: subflow.type,
|
|
1834
|
+
name: subflow.name,
|
|
1835
|
+
info: subflow.info,
|
|
1836
|
+
inputs: Array.isArray(subflow.in) ? subflow.in.length : 0,
|
|
1837
|
+
outputs: Array.isArray(subflow.out) ? subflow.out.length : 0
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
|
|
1841
|
+
_summarizeFlowItem (item) {
|
|
1842
|
+
if (!item) return null
|
|
1843
|
+
if (item.type === 'tab') {
|
|
1844
|
+
const ws = this.RED.nodes.workspace(item.id)
|
|
1845
|
+
return this._summarizeWorkspace(ws)
|
|
1846
|
+
}
|
|
1847
|
+
if (item.type === 'subflow') return this._summarizeSubflow(item)
|
|
1848
|
+
if (item.type === 'group') return this._summarizeGroup(item)
|
|
1849
|
+
return this._summarizeNode(item)
|
|
1850
|
+
}
|
|
1851
|
+
|
|
1852
|
+
_summarizeGroup (group) {
|
|
1853
|
+
if (!group) return null
|
|
1854
|
+
const s = { id: group.id, type: 'group' }
|
|
1855
|
+
if (group.name !== undefined) s.name = group.name
|
|
1856
|
+
if (group.z !== undefined) s.z = group.z
|
|
1857
|
+
if (Array.isArray(group.nodes)) {
|
|
1858
|
+
s.nodes = group.nodes.filter(Boolean).map(n => (typeof n === 'string' ? n : n.id))
|
|
1859
|
+
}
|
|
1860
|
+
if (group.style !== undefined) s.style = group.style
|
|
1861
|
+
if (Array.isArray(group.env) && group.env.length > 0) s.env = group.env
|
|
1862
|
+
return s
|
|
1863
|
+
}
|
|
1426
1864
|
}
|