Compare commits

...

161 Commits

Author SHA1 Message Date
Nick O'Leary
2feb290ae3
Merge pull request #5060 from gorenje/patch-3
Add .editorconfig to .gitignore
2025-02-25 09:57:04 +00:00
Nick O'Leary
45bcb74dae
Merge pull request #5062 from GogoVega/tab-config-changed-badge
Add the `changed` badge to the config node
2025-02-25 09:55:45 +00:00
GogoVega
6da890bf88
Manage badge locations with pure CSS + cleanup 2025-02-24 21:09:44 +01:00
GogoVega
b8eeef182c
Add changed badge for category header + fix cropped badge 2025-02-24 21:09:11 +01:00
GogoVega
54e0de64a7
Add the changed badge for the config node 2025-02-24 21:08:43 +01:00
Gerrit Riessen
3bd1b58217
Add .editorconfig to .gitignore
I use [EditorConfig](https://editorconfig.org/) to adjust my editor settings for indentation.

My settings should probably be best ignored, unless there is a desire to have a project wide .editorconfig
2025-02-24 19:01:17 +01:00
Nick O'Leary
71f8de94b0
Merge pull request #5056 from gorenje/master
Support text drag & drop into markdown editor
2025-02-24 16:26:19 +00:00
Nick O'Leary
4723378f2f
Merge pull request #5054 from GogoVega/deployment-ignore-disabled
Ignore state of disabled nodes/flows during deployment
2025-02-24 16:11:55 +00:00
Nick O'Leary
8837597ff5
Merge pull request #5053 from matiseni51/fix-include-label-themeSettings-config
fix: set label in themeSettings.deployButton despite type attribute
2025-02-24 16:08:47 +00:00
Gerrit Riessen
473b93f497
Support text drag & drop into markdown editor
Along with image drop, this change adds text drop into the editor. 

This is useful for highlighting text and then drag that text into the description of a node. Similar creating a mermaid diagram using a third-party tool and then drag the text into the description.

Also drop text files is also supported for longer texts.
2025-02-20 09:30:50 +01:00
GogoVega
d9c5144fe2
Ignore state of disabled nodes/flows during deployment 2025-02-17 14:05:51 +01:00
matiseni51
cbc91a9ac8
Merge branch 'master' into fix-include-label-themeSettings-config 2025-02-16 14:03:24 +01:00
matiseni51
88c946d401 fix: set label in themeSettings.deployButton despite type attribute 2025-02-16 13:44:22 +01:00
Nick O'Leary
ff565bacb4
Merge pull request #5052 from node-red/rel409
Update for 4.0.9
2025-02-14 17:07:27 +00:00
Nick O'Leary
e55301c073
Update for 4.0.9 2025-02-14 17:03:13 +00:00
Nick O'Leary
dc69226944
Merge pull request #5050 from aikitori/mqtt-update-translation
Add details for the dynamic subscription to match the English docs
2025-02-14 16:27:12 +00:00
Nick O'Leary
bf6b18b8a6
Merge pull request #5051 from GogoVega/typedInput-tooltip
Fix tooltip snapping based on `typedInput` type
2025-02-14 16:26:52 +00:00
GogoVega
e1b591d761
Fix tooltip snapping based on input type 2025-02-13 13:59:06 +01:00
aikitori
27463197cd
Add details for the dynamic subscription to match the English documentation 2025-02-13 08:55:10 +01:00
Nick O'Leary
854460db56
Merge pull request #5049 from node-red/Steve-Mcl-patch-1
Prevent symbol usage warning in monaco
2025-02-12 17:02:49 +00:00
Stephen McLaughlin
4984af48f1
Update monaco.js for symbol error 2025-02-12 09:42:55 +00:00
Nick O'Leary
e9dab46de8
Merge pull request #5025 from node-red/browse-subflow-flow-context
Show subflow flow context under node section of sidebar
2025-02-10 16:51:55 +00:00
Nick O'Leary
4605f01c5d
Merge pull request #5030 from matiseni51/add-editorTheme-custom-label-default-deploy-button
feat: Add custom label for default deploy button in settings.editorTheme
2025-02-10 16:51:45 +00:00
Nick O'Leary
a0ddf96e03
Merge pull request #5042 from node-red/5028-improve-auto-complete-long-keys
Handle long auto-complete suggests
2025-02-10 16:51:31 +00:00
Nick O'Leary
16d25b9d41
Merge pull request #5043 from node-red/5036-fix-undefined-username
Handle undefined username when generating user icon
2025-02-10 16:51:20 +00:00
Nick O'Leary
77c4ccf8fb
Handle undefined username when generating user icon
Fixes #5036
2025-02-10 16:41:18 +00:00
Nick O'Leary
7d9e09f5a7
Handle long auto-complete suggests
Fixes #5028
2025-02-10 16:23:13 +00:00
matiseni51
f4c184af4d
Merge branch 'master' into add-editorTheme-custom-label-default-deploy-button 2025-02-03 20:52:23 +01:00
Nick O'Leary
9694c8bdfa
Merge pull request #5027 from node-red/5026-fix-splice-group-add
Handle dragging node into group and splicing link at same time
2025-02-03 16:04:33 +00:00
matiseni51
ca61efc986 feat: Add custom label for default deploy button in settings.editorTheme 2025-02-01 11:44:57 +01:00
Nick O'Leary
ffdbd94927
Handle dragging node into group and splicing link at same time
Fixes #5026
2025-01-29 14:22:44 +00:00
Nick O'Leary
43df2318d4
Show subflow flow context under node section of sidebar 2025-01-27 16:36:57 +00:00
Nick O'Leary
21612a5215
Merge pull request #5021 from node-red/5008-remember-context-tree-state
Remember context sidebar tree state when refreshing
2025-01-27 15:58:43 +00:00
Nick O'Leary
756485e308
Merge pull request #5023 from node-red/5010-update-instance-envvars
Update sf instance env vars when removed from template
2025-01-27 15:58:30 +00:00
Nick O'Leary
efbe38f509
Merge pull request #5022 from node-red/5020-quick-add-group-select
Do not select group when triggering quick-add within it
2025-01-27 15:58:17 +00:00
Nick O'Leary
daa76e6e5f
Update sf instance env vars when removed from template 2025-01-22 10:25:24 +00:00
Nick O'Leary
48d2d269a5
Do not select group when triggering quick-add within it 2025-01-21 16:15:13 +00:00
Nick O'Leary
13cac1b5ef
Remember context sidebar tree state when refreshing
Closes #5008
2025-01-21 13:56:44 +00:00
Nick O'Leary
479b7e756d
Merge pull request #5016 from node-red/5009-fix-env-context-access
Allow env var access to context
2025-01-20 11:32:55 +00:00
Nick O'Leary
503ef62cf5
Merge pull request #5018 from node-red/fix-debug-status-if-null
fix debug status reporting if null
2025-01-20 11:32:43 +00:00
Nick O'Leary
b355a37378
Merge pull request #5017 from node-red/5004-icon-fix-2
Fix library icon handling within library browser component
2025-01-20 11:32:05 +00:00
Dave Conway-Jones
1acc16c9ef
fix debug status reporting if null 2025-01-19 10:57:58 +00:00
Nick O'Leary
4cbf672b26
Fix library icon handling within library browser component
Closes #5004
2025-01-17 17:07:58 +00:00
Nick O'Leary
272355a48e
Merge pull request #5014 from node-red/5013-switch-call-stack-fix
Avoid exceeding call stack when draining message group in Switch
2025-01-17 16:48:17 +00:00
Nick O'Leary
e2981f2970
Allow env var access to context 2025-01-17 16:45:44 +00:00
Nick O'Leary
804551000a
Merge pull request #5015 from node-red/fix-nodemon
Fix grunt dev via better ndoemon ignore rules
2025-01-15 14:43:08 +00:00
Nick O'Leary
254b6a1e23
Fix grunt dev via better ndoemon ignore rules 2025-01-15 10:32:10 +00:00
Nick O'Leary
3838e4e605
Merge pull request #5007 from natcl/patch-7
Fix typo in CHANGELOG (4.0.7-->4.0.8)
2025-01-13 16:44:29 +00:00
Nick O'Leary
953b7584a3
Avoid exceeding call stack when draining message group in Switch
Fixes #5013
2025-01-13 16:37:35 +00:00
Nathanaël Lécaudé
3da22882e9
Fix typo in CHANGELOG (4.0.7-->4.0.8) 2025-01-07 11:30:35 -05:00
Nick O'Leary
1e8f840993
Merge pull request #5001 from node-red/rel408
Bump for 4.0.8
2024-12-20 11:12:47 +00:00
Nick O'Leary
4845a1f7eb
Bump for 4.0.8 2024-12-20 11:06:08 +00:00
Nick O'Leary
a0952d9a07
Merge pull request #5000 from node-red/fix-config-node-import
Fix config node sort order when importing
2024-12-20 11:04:58 +00:00
Nick O'Leary
7fa4e60c82
Fix config node sort order when importing 2024-12-20 09:54:13 +00:00
Nick O'Leary
30eead76e6
Merge pull request #4998 from node-red/rel407
Bump for 4.0.7
2024-12-18 10:27:56 +00:00
Nick O'Leary
100e5244c8
Bump for 4.0.7 2024-12-17 17:08:56 +00:00
Nick O'Leary
ed0399b855
Merge pull request #4997 from GogoVega/fix-def-import
Fix def can be undefined if the type is missing
2024-12-17 17:04:52 +00:00
Nick O'Leary
27e9c18a4e
Merge pull request #4995 from GogoVega/fix-4994
Fix the user list of nested config node
2024-12-17 17:01:41 +00:00
GogoVega
c6895713ed
Fix def can be undefined if the type is missing 2024-12-17 17:57:01 +01:00
Nick O'Leary
965ca97ad1
Merge pull request #4993 from node-red/login-prompt
Support custom login message and button
2024-12-17 16:40:03 +00:00
GogoVega
7785ce0dc0
Fix the user list of nested config node 2024-12-17 15:36:51 +01:00
Nick O'Leary
1a47e2fc76
Fix login image auth setting 2024-12-16 17:09:31 +00:00
Nick O'Leary
b7e96ce6bc
Support custom login message and button 2024-12-16 16:58:13 +00:00
Nick O'Leary
7a3741165b
Merge pull request #4990 from node-red/rel406
Update for 4.0.6
2024-12-16 13:56:46 +00:00
Nick O'Leary
e9d5d20e2d
Update for 4.0.6 2024-12-16 11:35:14 +00:00
Nick O'Leary
867a6ad2da
Merge pull request #4975 from node-red/gg-changes
Roll up various fixes on config node change history
2024-12-16 11:29:11 +00:00
Nick O'Leary
03507c2a1f
Merge pull request #4949 from AGhorab-upland/master
Add qoutes when installing local tgz to fix spacing in the file path
2024-12-16 09:44:28 +00:00
Nick O'Leary
aa79aa5479
Merge pull request #4982 from node-red/4977-fix-csv-sep
Ensure node.sep is honoured when generating CSV
2024-12-16 09:16:29 +00:00
Steve-Mcl
16005a462d update tests to check msg.columns is strictly RFC compliant 2024-12-12 16:44:16 +00:00
Steve-Mcl
82c756b091 add test for correct CSV output when separator in non-comma 2024-12-12 16:43:38 +00:00
Steve-Mcl
b139eb4a18 update CSV to adhere to strict rfc compliance on msg.columns 2024-12-12 16:42:11 +00:00
Steve-Mcl
6af3c8c2a9 revert changes to csv parser 2024-12-12 16:41:26 +00:00
Steve-Mcl
2c3fbb1467 revert changes to legacy mode 2024-12-12 16:40:43 +00:00
Stephen McLaughlin
01716119e6
Merge branch 'master' into 4977-fix-csv-sep 2024-12-12 10:26:24 +00:00
Nick O'Leary
a50a37ac26
Merge pull request #4983 from node-red/gg-changes-2
Rework saving of credentials to undo history
2024-12-10 15:43:08 +00:00
Nick O'Leary
11c4277466
Update packages/node_modules/@node-red/editor-client/src/js/ui/editor.js
Co-authored-by: Gauthier Dandele <92022724+GogoVega@users.noreply.github.com>
2024-12-10 15:42:59 +00:00
Nick O'Leary
7d284ce157
Update packages/node_modules/@node-red/editor-client/src/js/ui/editor.js
Co-authored-by: Gauthier Dandele <92022724+GogoVega@users.noreply.github.com>
2024-12-10 15:42:42 +00:00
Nick O'Leary
aa1d5ad06b
Merge pull request #4987 from node-red/update-deps-3
Update dependencies
2024-12-10 15:34:14 +00:00
Nick O'Leary
00a3010933
Update dependencies 2024-12-10 15:25:23 +00:00
Dave Conway-Jones
56a4530ec6
Fix delay node not dropping when nodeMessageBufferMaxLength is set (#4973)
* Fix delay node not dropping when nodeMessageBufferMaxLength is set

to close #4966

* Rmove redundant codes

* Tidy up code removal

---------

Co-authored-by: Nick O'Leary <nick.oleary@gmail.com>
2024-12-06 22:22:37 +00:00
Nick O'Leary
89e40a0b8f
Rework saving of credentials to undo history 2024-12-06 16:15:37 +00:00
Nick O'Leary
66bd1feb47
Apply suggestions from code review
Co-authored-by: Gauthier Dandele <92022724+GogoVega@users.noreply.github.com>
2024-12-06 13:45:48 +00:00
Nick O'Leary
b419e2e303
Merge branch 'master' into master 2024-12-05 16:32:19 +00:00
Nick O'Leary
dae4ba8044
Merge pull request #4964 from node-red/4962-import-warning-tidy
Validate json dropped into editor to avoid unhelpful error messages
2024-12-05 16:31:33 +00:00
Nick O'Leary
fe22afea6a
Merge pull request #4974 from node-red/4969-fix-junction-insert-location
Fix junction insert position via context menu
2024-12-05 16:23:51 +00:00
Nick O'Leary
69753a9940
Merge pull request #4981 from node-red/4978-zoom-annotation-fix
Apply zoom scale when calculating annotation positions
2024-12-05 16:23:36 +00:00
Nick O'Leary
f6e565ba04
Merge pull request #4980 from braincube-io/feat/fadoli/MakeFileReadFast
Performance : make reading single buffer / string file faster by not re-allocating and handling huge buffers
2024-12-05 16:17:52 +00:00
Nick O'Leary
e4fdf24545
Ensure node.sep is honoured when generating CSV 2024-12-05 16:00:54 +00:00
Nick O'Leary
43a9a3c3b1
Apply zoom scale when calculating annotation positions
Fixes #4978
2024-12-05 15:34:24 +00:00
Franck
bfd98aaf22 PERF : make single buffer / string file reading faster 2024-12-05 12:24:22 +01:00
Nick O'Leary
4e61c54be5
Update packages/node_modules/@node-red/editor-client/src/js/ui/editor.js
Co-authored-by: Gauthier Dandele <92022724+GogoVega@users.noreply.github.com>
2024-12-04 15:49:39 +00:00
Nick O'Leary
39a85c721d
Update packages/node_modules/@node-red/editor-client/src/js/history.js 2024-12-04 13:10:48 +00:00
Nick O'Leary
f9877f8d0b
Update packages/node_modules/@node-red/editor-client/src/js/ui/subflow.js
Co-authored-by: Gauthier Dandele <92022724+GogoVega@users.noreply.github.com>
2024-12-04 10:41:27 +00:00
Nick O'Leary
92dff4bacd
Apply suggestions from code review
Co-authored-by: Gauthier Dandele <92022724+GogoVega@users.noreply.github.com>
2024-12-04 10:41:10 +00:00
Nick O'Leary
338ddf17de
Update packages/node_modules/@node-red/editor-client/src/js/nodes.js
Co-authored-by: Gauthier Dandele <92022724+GogoVega@users.noreply.github.com>
2024-12-04 09:55:09 +00:00
Nick O'Leary
4e6c8ea367
Merge pull request #4947 from GogoVega/fix-conf-type-env
Handle subflow node as user of config nodes
2024-12-03 17:21:57 +00:00
Nick O'Leary
5f92bc83fd
Merge branch 'gg-changes' into fix-conf-type-env 2024-12-03 17:21:24 +00:00
Nick O'Leary
5e429f3be0
Merge pull request #4819 from GogoVega/validate-users-in-history
Validate user nodes into history when editing a config node
2024-12-03 17:14:32 +00:00
Nick O'Leary
2a71175cd4
Merge pull request #4807 from GogoVega/fix-user-count
Fix the config node users count
2024-12-03 17:14:20 +00:00
Nick O'Leary
aee531bf16
Update packages/node_modules/@node-red/editor-client/src/js/ui/subflow.js
Co-authored-by: Gauthier Dandele <92022724+GogoVega@users.noreply.github.com>
2024-12-03 17:14:04 +00:00
Nick O'Leary
2c99909353
Merge pull request #4808 from GogoVega/fix-invalid-outputs
Fix a node with an invalid number of outputs
2024-12-03 14:56:46 +00:00
Nick O'Leary
50e821d5d7
Merge pull request #4793 from GogoVega/fix-creds-props-pane
Fix save and history of credentials for panes
2024-12-03 14:56:02 +00:00
Nick O'Leary
06f3f3c0be
Merge branch 'gg-changes' into validate-users-in-history 2024-12-03 14:54:50 +00:00
Nick O'Leary
0b09cf5fa9
Update packages/node_modules/@node-red/editor-client/src/js/nodes.js 2024-12-03 14:51:32 +00:00
Nick O'Leary
93102837dd
Merge pull request #4797 from GogoVega/config-node-history
Add config node to `RED.history` and handle `changed` prop
2024-12-03 14:48:23 +00:00
Nick O'Leary
e8d81d814c
Apply scaleFactor when calculating junction slice positions 2024-12-03 10:15:59 +00:00
Nick O'Leary
f6cf051282
Fix junction insert position via context menu 2024-12-02 17:03:14 +00:00
AGhorab-upland
328390c2a9
Merge branch 'master' into master 2024-11-26 21:11:33 +02:00
GogoVega
6194285b6e
Add a guard to check if wires exist 2024-11-25 21:07:42 +01:00
Nick O'Leary
84a2fbed2e
Merge pull request #4811 from GogoVega/handle-bad-subflow
Handle the import of an incomplete Subflow
2024-11-25 16:49:32 +00:00
Nick O'Leary
5ce3cdb845
Merge branch 'master' into handle-bad-subflow 2024-11-25 16:27:41 +00:00
Nick O'Leary
3e0b5f2fe8
Merge pull request #4809 from GogoVega/fix-subflow-name
Fix updating the Subflow name during a copy
2024-11-25 16:22:31 +00:00
Nick O'Leary
94e3fdd7a9
Validate json dropped into editor to avoid unhelpful error messages
Fixes #4962
2024-11-18 17:12:28 +00:00
Nick O'Leary
69b413040f
Merge pull request #4963 from node-red/rename-var
Rename variable to avoid confusion in view.js
2024-11-18 16:39:52 +00:00
Nick O'Leary
ffecf86281
Merge pull request #4960 from GogoVega/fix-4942
Get the env config node from the parent subflow
2024-11-18 16:39:37 +00:00
Nick O'Leary
4cb3ccc984
Rename variable to avoid confusion in view.js 2024-11-18 16:20:58 +00:00
Nick O'Leary
47e1389548
Merge pull request #4959 from hungtcs/master
Change groups.length to groups.size
2024-11-18 16:18:27 +00:00
GogoVega
6d6e6fa416
Get the env config node from the parent subflow 2024-11-15 14:30:47 +01:00
鸿则
ad615a76c8
Change groups.length to groups.size
Fix wrong length attribute of Set
2024-11-14 16:31:36 +08:00
Nick O'Leary
e48607c743
Merge pull request #4940 from node-red/Delay-node-not-send-on-reset-if-queue-empty
Make delay node rate limit reset consistent - not send on reset.
2024-11-11 13:49:41 +00:00
Ahmed Ghorab
046d56d692 Add qoutes when installing local tgz to fix spacing in the file path 2024-11-10 13:30:41 +02:00
Nick O'Leary
604c70ec04
Merge pull request #4946 from GogoVega/fix-quickAddDialog
Remove disabled node types from QuickAddDialog list
2024-11-08 13:46:19 +00:00
Gauthier Dandele
59a133cc13
Need to guard against subflows that doesn't have a set property
Co-authored-by: Nick O'Leary <nick.oleary@gmail.com>
2024-11-08 12:28:40 +01:00
Nick O'Leary
0590d81e80
Merge pull request #4939 from GogoVega/fix-4831
Fix `setModulePendingUpdated` with plugins
2024-11-08 11:26:52 +00:00
GogoVega
c8a02d53e8
Ensure the node added to config node users is the proxy object 2024-11-07 22:06:45 +01:00
GogoVega
deccfdf654
Handle users of env config nodes when undo subflow node changes 2024-11-07 22:00:36 +01:00
GogoVega
f2d72b1050
Handle users of env config nodes when saving subflow node config 2024-11-07 21:59:14 +01:00
GogoVega
3d9bc265dd
Handle users of env config nodes when adding/removing subflow node 2024-11-07 21:58:11 +01:00
GogoVega
abe0b60bf7
Remove disabled node types from QuickAddDialog list 2024-11-06 17:28:51 +01:00
Nick O'Leary
54bf3f4402
Merge pull request #4934 from ersinpw/patch-1
Missing getSubscriptions in the docs while its implemented
2024-11-01 09:06:36 +00:00
Dave Conway-Jones
d3219f0600
do add to queue in case it needs to also be flushed 2024-10-31 17:21:53 +00:00
Nick O'Leary
348ec34446
Merge pull request #4925 from GogoVega/fix-4923
Apply `envVarExcludes` setting to `util.getSetting` into the function node
2024-10-31 17:18:32 +00:00
Dave Conway-Jones
33a5b2527c
Make delay node rate limit reset consistent - not send on reset.
to fix #4830
2024-10-31 17:06:13 +00:00
GogoVega
443492d6eb
Fix setModulePendingUpdated with plugins 2024-10-31 17:12:13 +01:00
Ersin
a7ee31307e
missing getSubscriptions in the docs while its implemented
See:
https://github.com/node-red/node-red/blob/master/packages/node_modules/%40node-red/nodes/core/network/10-mqtt.js#L1288
2024-10-29 13:38:32 +01:00
Gauthier Dandele
f67268b89a
Revert and force getSetting to use the local node
Co-authored-by: Nick O'Leary <nick.oleary@gmail.com>
2024-10-28 18:01:42 +01:00
Nick O'Leary
42382e1a03
Merge pull request #4932 from GogoVega/fix-envVarList-sortable
Fix `envVar` editable list should be sortable
2024-10-28 16:35:56 +00:00
Nick O'Leary
419dfbf08b
Merge pull request #4915 from node-red/Fix-for-trigger-node-date-handling
Fix trigger node date handling for latest time type input
2024-10-28 16:17:38 +00:00
GogoVega
70aed23ef0
Fix envVarList sortable value 2024-10-28 16:31:36 +01:00
GogoVega
966064328f
Add oneditsave credentials changes to history 2024-10-25 23:33:19 +02:00
GogoVega
83696abf9d
Fixes and improvements with comments 2024-10-25 17:34:51 +02:00
GogoVega
2fd7aee4da
Move envVarExcludes to the top scope 2024-10-24 13:44:40 +02:00
GogoVega
7555e0644f
Apply envVarExcludes setting to RED.util.getSetting into the function node 2024-10-23 12:15:24 +02:00
Nick O'Leary
c363f375b6
Merge pull request #4912 from GogoVega/improve-node-name
Improve the node name auto-generated with the first available number
2024-10-21 16:42:04 +01:00
Nick O'Leary
2220956007
Ensure trigger second output is revaluated for date types 2024-10-21 16:35:11 +01:00
Nick O'Leary
b3aff3a3e6
Ensure trigger node properties work with evaluateNodeProperty 2024-10-21 16:26:03 +01:00
Nick O'Leary
d0ad62a82b
Revert trigger node fix 2024-10-21 16:24:21 +01:00
Dave Conway-Jones
892933ff75
Update 89-trigger_spec.js 2024-10-12 17:22:24 +01:00
Dave Conway-Jones
61fd01b871
And add some tests 2024-10-12 17:09:18 +01:00
Dave Conway-Jones
2eba754801
Fix trigger node date handling for latest time type input
to fix #4914
2024-10-12 16:49:09 +01:00
GogoVega
a7b1ce0cf8
Improve the node name auto-generated with the first available nb 2024-10-10 15:47:42 +02:00
GogoVega
8aec038c24
Remove duplicate type definition 2024-07-01 17:59:28 +02:00
Gauthier Dandele
bda9f249bc
Update packages/node_modules/@node-red/editor-client/src/js/nodes.js
Co-authored-by: Nick O'Leary <nick.oleary@gmail.com>
2024-07-01 17:43:39 +02:00
GogoVega
10ac7fc369
Validate user nodes into history when editing a config node 2024-06-29 16:08:04 +02:00
GogoVega
bea08706cc
Handle the import of an incomplete Subflow 2024-06-26 22:51:08 +02:00
GogoVega
7950ee1241
Fix updating the subflow name during a copy 2024-06-26 21:16:59 +02:00
GogoVega
a743764345
Fix a node with an invalid number of outputs 2024-06-26 19:42:26 +02:00
GogoVega
cc1c87387b
Fix the config node users count 2024-06-26 19:10:55 +02:00
GogoVega
ed4b98b598
Fix adding users to history if multiple props modified 2024-06-26 09:22:01 +02:00
GogoVega
53e092e484
Add config node to history + handling changed prop 2024-06-25 15:06:58 +02:00
GogoVega
eab512ef22
Fix save and history of credentials for panes 2024-06-24 17:55:23 +02:00
59 changed files with 1332 additions and 468 deletions

1
.gitignore vendored
View File

@ -28,3 +28,4 @@ docs
.nyc_output .nyc_output
sync.ffs_db sync.ffs_db
package-lock.json package-lock.json
.editorconfig

View File

@ -1,4 +0,0 @@
/Gruntfile.js
/.git/*
*.backup
/public/*

View File

@ -1,3 +1,76 @@
#### 4.0.9: Maintenance Release
Editor
- Add details for the dynamic subscription to match the English docs (#5050) @aikitori
- Fix tooltip snapping based on `typedInput` type (#5051) @GogoVega
- Prevent symbol usage warning in monaco (#5049) @Steve-Mcl
- Show subflow flow context under node section of sidebar (#5025) @knolleary
- feat: Add custom label for default deploy button in settings.editorTheme (#5030) @matiseni51
- Handle long auto-complete suggests (#5042) @knolleary
- Handle undefined username when generating user icon (#5043) @knolleary
- Handle dragging node into group and splicing link at same time (#5027) @knolleary
- Remember context sidebar tree state when refreshing (#5021) @knolleary
- Update sf instance env vars when removed from template (#5023) @knolleary
- Do not select group when triggering quick-add within it (#5022) @knolleary
- Fix library icon handling within library browser component (#5017) @knolleary
Runtime
- Allow env var access to context (#5016) @knolleary
- fix debug status reporting if null (#5018) @dceejay
- Fix grunt dev via better ndoemon ignore rules (#5015) @knolleary
- Fix typo in CHANGELOG (4.0.7-->4.0.8) (#5007) @natcl
Nodes
- Switch: Avoid exceeding call stack when draining message group in Switch (#5014) @knolleary
#### 4.0.8: Maintenance Release
Editor
- Fix config node sort order when importing (#5000) @knolleary
#### 4.0.7: Maintenance Release
Editor
- Fix def can be undefined if the type is missing (#4997) @GogoVega
- Fix the user list of nested config node (#4995) @GogoVega
- Support custom login message and button (#4993) @knolleary
#### 4.0.6: Maintenance Release
Editor
- Roll up various fixes on config node change history (#4975) @knolleary
- Add quotes when installing local tgz to fix spacing in the file path (#4949) @AGhorab-upland
- Validate json dropped into editor to avoid unhelpful error messages (#4964) @knolleary
- Fix junction insert position via context menu (#4974) @knolleary
- Apply zoom scale when calculating annotation positions (#4981) @knolleary
- Handle the import of an incomplete Subflow (#4811) @GogoVega
- Fix updating the Subflow name during a copy (#4809) @GogoVega
- Rename variable to avoid confusion in view.js (#4963) @knolleary
- Change groups.length to groups.size (#4959) @hungtcs
- Remove disabled node types from QuickAddDialog list (#4946) @GogoVega
- Fix `setModulePendingUpdated` with plugins (#4939) @GogoVega
- Missing getSubscriptions in the docs while its implemented (#4934) @ersinpw
- Apply `envVarExcludes` setting to `util.getSetting` into the function node (#4925) @GogoVega
- Fix `envVar` editable list should be sortable (#4932) @GogoVega
- Improve the node name auto-generated with the first available number (#4912) @GogoVega
Runtime
- Get the env config node from the parent subflow (#4960) @GogoVega
- Update dependencies (#4987) @knolleary
Nodes
- Performance : make reading single buffer / string file faster by not re-allocating and handling huge buffers (#4980) @Fadoli
- Make delay node rate limit reset consistent - not send on reset. (#4940) @dceejay
- Fix trigger node date handling for latest time type input (#4915) @dceejay
- Fix delay node not dropping when nodeMessageBufferMaxLength is set (#4973)
- Ensure node.sep is honoured when generating CSV (#4982) @knolleary
#### 4.0.5: Maintenance Release #### 4.0.5: Maintenance Release
Editor Editor

16
nodemon.json Normal file
View File

@ -0,0 +1,16 @@
{
"ignoreRoot": [
".git",
".nyc_output",
".sass-cache",
"bower-components",
"coverage"
],
"ignore": [
"/Gruntfile.js",
"/.git/*",
"*.backup",
"/public/*"
]
}

View File

@ -1,6 +1,6 @@
{ {
"name": "node-red", "name": "node-red",
"version": "4.0.5", "version": "4.0.9",
"description": "Low-code programming for event-driven applications", "description": "Low-code programming for event-driven applications",
"homepage": "https://nodered.org", "homepage": "https://nodered.org",
"license": "Apache-2.0", "license": "Apache-2.0",
@ -41,7 +41,7 @@
"cors": "2.8.5", "cors": "2.8.5",
"cronosjs": "1.7.1", "cronosjs": "1.7.1",
"denque": "2.1.0", "denque": "2.1.0",
"express": "4.21.1", "express": "4.21.2",
"express-session": "1.18.1", "express-session": "1.18.1",
"form-data": "4.0.0", "form-data": "4.0.0",
"fs-extra": "11.2.0", "fs-extra": "11.2.0",

View File

@ -126,6 +126,14 @@ async function login(req,res) {
if (themeContext.login && themeContext.login.image) { if (themeContext.login && themeContext.login.image) {
response.image = themeContext.login.image; response.image = themeContext.login.image;
} }
if (themeContext.login?.message) {
response.loginMessage = themeContext.login?.message
}
if (themeContext.login?.button) {
response.prompts = [
{ type: "button", ...themeContext.login.button }
]
}
} }
res.json(response); res.json(response);
} }

View File

@ -185,13 +185,12 @@ module.exports = {
} }
if (theme.deployButton) { if (theme.deployButton) {
if (theme.deployButton.type == "simple") { themeSettings.deployButton = {};
themeSettings.deployButton = {
type: "simple"
}
if (theme.deployButton.label) { if (theme.deployButton.label) {
themeSettings.deployButton.label = theme.deployButton.label; themeSettings.deployButton.label = theme.deployButton.label;
} }
if (theme.deployButton.type == "simple") {
themeSettings.deployButton.type = theme.deployButton.type;
if (theme.deployButton.icon) { if (theme.deployButton.icon) {
url = serveFile(themeApp,"/deploy/",theme.deployButton.icon); url = serveFile(themeApp,"/deploy/",theme.deployButton.icon);
if (url) { if (url) {
@ -206,13 +205,25 @@ module.exports = {
} }
if (theme.login) { if (theme.login) {
let themeContextLogin = {}
let hasLoginTheme = false
if (theme.login.image) { if (theme.login.image) {
url = serveFile(themeApp,"/login/",theme.login.image); url = serveFile(themeApp,"/login/",theme.login.image);
if (url) { if (url) {
themeContext.login = { themeContextLogin.image = url
image: url hasLoginTheme = true
} }
} }
if (theme.login.message) {
themeContextLogin.message = theme.login.message
hasLoginTheme = true
}
if (theme.login.button) {
themeContextLogin.button = theme.login.button
hasLoginTheme = true
}
if (hasLoginTheme) {
themeContext.login = themeContextLogin
} }
} }
themeApp.get("/", async function(req,res) { themeApp.get("/", async function(req,res) {

View File

@ -1,6 +1,6 @@
{ {
"name": "@node-red/editor-api", "name": "@node-red/editor-api",
"version": "4.0.5", "version": "4.0.9",
"license": "Apache-2.0", "license": "Apache-2.0",
"main": "./lib/index.js", "main": "./lib/index.js",
"repository": { "repository": {
@ -16,14 +16,14 @@
} }
], ],
"dependencies": { "dependencies": {
"@node-red/util": "4.0.5", "@node-red/util": "4.0.9",
"@node-red/editor-client": "4.0.5", "@node-red/editor-client": "4.0.9",
"bcryptjs": "2.4.3", "bcryptjs": "2.4.3",
"body-parser": "1.20.3", "body-parser": "1.20.3",
"clone": "2.1.2", "clone": "2.1.2",
"cors": "2.8.5", "cors": "2.8.5",
"express-session": "1.18.1", "express-session": "1.18.1",
"express": "4.21.1", "express": "4.21.2",
"memorystore": "1.6.7", "memorystore": "1.6.7",
"mime": "3.0.0", "mime": "3.0.0",
"multer": "1.4.5-lts.1", "multer": "1.4.5-lts.1",

View File

@ -1,6 +1,6 @@
{ {
"name": "@node-red/editor-client", "name": "@node-red/editor-client",
"version": "4.0.5", "version": "4.0.9",
"license": "Apache-2.0", "license": "Apache-2.0",
"repository": { "repository": {
"type": "git", "type": "git",

View File

@ -453,10 +453,61 @@ RED.history = (function() {
RED.events.emit("nodes:change",newConfigNode); RED.events.emit("nodes:change",newConfigNode);
} }
}); });
} else if (i === "env" && ev.node.type.indexOf("subflow:") === 0) {
// Subflow can have config node in node.env
let nodeList = ev.node.env || [];
nodeList = nodeList.reduce((list, prop) => {
if (prop.type === "conf-type" && prop.value) {
list.push(prop.value);
} }
return list;
}, []);
nodeList.forEach(function(id) {
const configNode = RED.nodes.node(id);
if (configNode) {
if (configNode.users.indexOf(ev.node) !== -1) {
configNode.users.splice(configNode.users.indexOf(ev.node), 1);
RED.events.emit("nodes:change", configNode);
}
}
});
nodeList = ev.changes.env || [];
nodeList = nodeList.reduce((list, prop) => {
if (prop.type === "conf-type" && prop.value) {
list.push(prop.value);
}
return list;
}, []);
nodeList.forEach(function(id) {
const configNode = RED.nodes.node(id);
if (configNode) {
if (configNode.users.indexOf(ev.node) === -1) {
configNode.users.push(ev.node);
RED.events.emit("nodes:change", configNode);
}
}
});
}
if (i === "credentials" && ev.changes[i]) {
// Reset - Only want to keep the changes
inverseEv.changes[i] = {};
for (const [key, value] of Object.entries(ev.changes[i])) {
// Edge case: node.credentials is cleared after a deploy, so we can't
// capture values for the inverse event when undoing past a deploy
if (ev.node.credentials) {
inverseEv.changes[i][key] = ev.node.credentials[key];
}
ev.node.credentials[key] = value;
}
} else {
ev.node[i] = ev.changes[i]; ev.node[i] = ev.changes[i];
} }
} }
}
ev.node.dirty = true; ev.node.dirty = true;
ev.node.changed = ev.changed; ev.node.changed = ev.changed;
@ -536,6 +587,24 @@ RED.history = (function() {
RED.editor.updateNodeProperties(ev.node,outputMap); RED.editor.updateNodeProperties(ev.node,outputMap);
RED.editor.validateNode(ev.node); RED.editor.validateNode(ev.node);
} }
// If it's a Config Node, validate user nodes too.
// NOTE: The Config Node must be validated before validating users.
if (ev.node.users) {
const validatedNodes = new Set();
const userStack = ev.node.users.slice();
validatedNodes.add(ev.node.id);
while (userStack.length) {
const node = userStack.pop();
if (!validatedNodes.has(node.id)) {
validatedNodes.add(node.id);
if (node.users) {
userStack.push(...node.users);
}
RED.editor.validateNode(node);
}
}
}
if (ev.links) { if (ev.links) {
inverseEv.createdLinks = []; inverseEv.createdLinks = [];
for (i=0;i<ev.links.length;i++) { for (i=0;i<ev.links.length;i++) {

View File

@ -398,14 +398,13 @@ RED.multiplayer = (function () {
anonIconBody.setAttribute("d",`M ${radius/2} ${radius/2 + 5} h -2.5 c -2 1 -2 -5 0.5 -4.5 c 2 1 2 1 4 0 c 2.5 -0.5 2.5 5.5 0 4.5 z`); anonIconBody.setAttribute("d",`M ${radius/2} ${radius/2 + 5} h -2.5 c -2 1 -2 -5 0.5 -4.5 c 2 1 2 1 4 0 c 2.5 -0.5 2.5 5.5 0 4.5 z`);
group.appendChild(anonIconBody) group.appendChild(anonIconBody)
} else { } else {
const labelText = user.username ? user.username.substring(0,2) : user
const label = document.createElementNS("http://www.w3.org/2000/svg","text"); const label = document.createElementNS("http://www.w3.org/2000/svg","text");
if (user.username) { if (user.username || user.email) {
label.setAttribute("class","red-ui-multiplayer-annotation-label"); label.setAttribute("class","red-ui-multiplayer-annotation-label");
label.textContent = user.username.substring(0,2) label.textContent = (user.username || user.email).substring(0,2)
} else { } else {
label.setAttribute("class","red-ui-multiplayer-annotation-label red-ui-multiplayer-user-count") label.setAttribute("class","red-ui-multiplayer-annotation-label red-ui-multiplayer-user-count")
label.textContent = user label.textContent = 'nr'
} }
label.setAttribute("text-anchor", "middle") label.setAttribute("text-anchor", "middle")
label.setAttribute("x",radius/2); label.setAttribute("x",radius/2);

View File

@ -73,7 +73,13 @@ RED.nodes = (function() {
var exports = { var exports = {
setModulePendingUpdated: function(module,version) { setModulePendingUpdated: function(module,version) {
if (!!RED.plugins.getModule(module)) {
// The module updated is a plugin
RED.plugins.getModule(module).pending_version = version;
} else {
moduleList[module].pending_version = version; moduleList[module].pending_version = version;
}
RED.events.emit("registry:module-updated",{module:module,version:version}); RED.events.emit("registry:module-updated",{module:module,version:version});
}, },
getModule: function(module) { getModule: function(module) {
@ -701,12 +707,15 @@ RED.nodes = (function() {
} }
n["_"] = RED._; n["_"] = RED._;
} }
// Both node and config node can use a config node
updateConfigNodeUsers(newNode, { action: "add" });
if (n._def.category == "config") { if (n._def.category == "config") {
configNodes[n.id] = n; configNodes[n.id] = newNode;
} else { } else {
if (n.wires && (n.wires.length > n.outputs)) { n.outputs = n.wires.length; } if (n.wires && (n.wires.length > n.outputs)) { n.outputs = n.wires.length; }
n.dirty = true; n.dirty = true;
updateConfigNodeUsers(n);
if (n._def.category == "subflows" && typeof n.i === "undefined") { if (n._def.category == "subflows" && typeof n.i === "undefined") {
var nextId = 0; var nextId = 0;
RED.nodes.eachNode(function(node) { RED.nodes.eachNode(function(node) {
@ -768,9 +777,11 @@ RED.nodes = (function() {
var removedLinks = []; var removedLinks = [];
var removedNodes = []; var removedNodes = [];
var node; var node;
if (id in configNodes) { if (id in configNodes) {
node = configNodes[id]; node = configNodes[id];
delete configNodes[id]; delete configNodes[id];
updateConfigNodeUsers(node, { action: "remove" });
RED.events.emit('nodes:remove',node); RED.events.emit('nodes:remove',node);
RED.workspaces.refresh(); RED.workspaces.refresh();
} else if (allNodes.hasNode(id)) { } else if (allNodes.hasNode(id)) {
@ -779,6 +790,9 @@ RED.nodes = (function() {
delete nodeLinks[id]; delete nodeLinks[id];
removedLinks = links.filter(function(l) { return (l.source === node) || (l.target === node); }); removedLinks = links.filter(function(l) { return (l.source === node) || (l.target === node); });
removedLinks.forEach(removeLink); removedLinks.forEach(removeLink);
updateConfigNodeUsers(node, { action: "remove" });
// TODO: Legacy code for exclusive config node
var updatedConfigNode = false; var updatedConfigNode = false;
for (var d in node._def.defaults) { for (var d in node._def.defaults) {
if (node._def.defaults.hasOwnProperty(d)) { if (node._def.defaults.hasOwnProperty(d)) {
@ -792,10 +806,6 @@ RED.nodes = (function() {
if (configNode._def.exclusive) { if (configNode._def.exclusive) {
removeNode(node[d]); removeNode(node[d]);
removedNodes.push(configNode); removedNodes.push(configNode);
} else {
var users = configNode.users;
users.splice(users.indexOf(node),1);
RED.events.emit('nodes:change',configNode)
} }
} }
} }
@ -1032,23 +1042,34 @@ RED.nodes = (function() {
return {nodes:removedNodes,links:removedLinks, groups: removedGroups, junctions: removedJunctions}; return {nodes:removedNodes,links:removedLinks, groups: removedGroups, junctions: removedJunctions};
} }
/**
* Add a Subflow to the Workspace
*
* @param {object} sf The Subflow to add.
* @param {boolean|undefined} createNewIds Whether to update the name.
*/
function addSubflow(sf, createNewIds) { function addSubflow(sf, createNewIds) {
if (createNewIds) { if (createNewIds) {
var subflowNames = Object.keys(subflows).map(function(sfid) { // Update the Subflow name to highlight that this is a copy
return subflows[sfid].name; const subflowNames = Object.keys(subflows).map(function (sfid) {
}); return subflows[sfid].name || "";
})
subflowNames.sort()
subflowNames.sort(); let copyNumber = 1;
var copyNumber = 1; let subflowName = sf.name;
var subflowName = sf.name;
subflowNames.forEach(function(name) { subflowNames.forEach(function(name) {
if (subflowName == name) { if (subflowName == name) {
subflowName = sf.name + " (" + copyNumber + ")";
copyNumber++; copyNumber++;
subflowName = sf.name+" ("+copyNumber+")";
} }
}); });
sf.name = subflowName; sf.name = subflowName;
} }
sf.instances = [];
subflows[sf.id] = sf; subflows[sf.id] = sf;
allNodes.addTab(sf.id); allNodes.addTab(sf.id);
linkTabMap[sf.id] = []; linkTabMap[sf.id] = [];
@ -1101,7 +1122,7 @@ RED.nodes = (function() {
module: "node-red" module: "node-red"
} }
}); });
sf.instances = [];
sf._def = RED.nodes.getType("subflow:"+sf.id); sf._def = RED.nodes.getType("subflow:"+sf.id);
RED.events.emit("subflows:add",sf); RED.events.emit("subflows:add",sf);
} }
@ -1743,7 +1764,8 @@ RED.nodes = (function() {
// Remove the old subflow definition - but leave the instances in place // Remove the old subflow definition - but leave the instances in place
var removalResult = RED.subflow.removeSubflow(n.id, true); var removalResult = RED.subflow.removeSubflow(n.id, true);
// Create the list of nodes for the new subflow def // Create the list of nodes for the new subflow def
var subflowNodes = [n].concat(zMap[n.id]); // Need to sort the list in order to remove missing nodes
var subflowNodes = [n].concat(zMap[n.id]).filter((s) => !!s);
// Import the new subflow - no clashes should occur as we've removed // Import the new subflow - no clashes should occur as we've removed
// the old version // the old version
var result = importNodes(subflowNodes); var result = importNodes(subflowNodes);
@ -1780,9 +1802,20 @@ RED.nodes = (function() {
// Replace config nodes // Replace config nodes
// //
configNodeIds.forEach(function(id) { configNodeIds.forEach(function(id) {
removedNodes = removedNodes.concat(convertNode(getNode(id))); const configNode = getNode(id);
const currentUserCount = configNode.users;
// Add a snapshot of the Config Node
removedNodes = removedNodes.concat(convertNode(configNode));
// Remove the Config Node instance
removeNode(id); removeNode(id);
importNodes([newConfigNodes[id]])
// Import the new one
importNodes([newConfigNodes[id]]);
// Re-attributes the user count
getNode(id).users = currentUserCount;
}); });
return { return {
@ -2023,6 +2056,8 @@ RED.nodes = (function() {
if (matchingSubflow) { if (matchingSubflow) {
subflow_denylist[n.id] = matchingSubflow; subflow_denylist[n.id] = matchingSubflow;
} else { } else {
const oldId = n.id;
subflow_map[n.id] = n; subflow_map[n.id] = n;
if (createNewIds || options.importMap[n.id] === "copy") { if (createNewIds || options.importMap[n.id] === "copy") {
nid = getID(); nid = getID();
@ -2050,7 +2085,7 @@ RED.nodes = (function() {
n.status.id = getID(); n.status.id = getID();
} }
new_subflows.push(n); new_subflows.push(n);
addSubflow(n,createNewIds || options.importMap[n.id] === "copy"); addSubflow(n,createNewIds || options.importMap[oldId] === "copy");
} }
} }
} }
@ -2064,6 +2099,8 @@ RED.nodes = (function() {
activeWorkspace = RED.workspaces.active(); activeWorkspace = RED.workspaces.active();
} }
const pendingConfigNodes = []
const pendingConfigNodeIds = new Set()
// Find all config nodes and add them // Find all config nodes and add them
for (i=0;i<newNodes.length;i++) { for (i=0;i<newNodes.length;i++) {
n = newNodes[i]; n = newNodes[i];
@ -2123,7 +2160,8 @@ RED.nodes = (function() {
type:n.type, type:n.type,
info: n.info, info: n.info,
users:[], users:[],
_config:{} _config:{},
_configNodeReferences: new Set()
}; };
if (!n.z) { if (!n.z) {
delete configNode.z; delete configNode.z;
@ -2138,6 +2176,9 @@ RED.nodes = (function() {
if (def.defaults.hasOwnProperty(d)) { if (def.defaults.hasOwnProperty(d)) {
configNode[d] = n[d]; configNode[d] = n[d];
configNode._config[d] = JSON.stringify(n[d]); configNode._config[d] = JSON.stringify(n[d]);
if (def.defaults[d].type) {
configNode._configNodeReferences.add(n[d])
}
} }
} }
if (def.hasOwnProperty('credentials') && n.hasOwnProperty('credentials')) { if (def.hasOwnProperty('credentials') && n.hasOwnProperty('credentials')) {
@ -2154,11 +2195,55 @@ RED.nodes = (function() {
configNode.id = getID(); configNode.id = getID();
} }
node_map[n.id] = configNode; node_map[n.id] = configNode;
new_nodes.push(configNode); pendingConfigNodes.push(configNode);
pendingConfigNodeIds.add(configNode.id)
} }
} }
} }
// We need to sort new_nodes (which only contains config nodes at this point)
// to ensure they get added in the right order. If NodeA depends on NodeB, then
// NodeB must be added first.
// Limit us to 5 full iterations of the list - this should be more than
// enough to process the list as config->config node relationships are
// not very common
let iterationLimit = pendingConfigNodes.length * 5
const handledConfigNodes = new Set()
while (pendingConfigNodes.length > 0 && iterationLimit > 0) {
const node = pendingConfigNodes.shift()
let hasPending = false
// Loop through the nodes referenced by this node to see if anything
// is pending
node._configNodeReferences.forEach(id => {
if (pendingConfigNodeIds.has(id) && !handledConfigNodes.has(id)) {
// This reference is for a node we know is in this import, but
// it isn't added yet - flag as pending
hasPending = true
}
})
if (!hasPending) {
// This node has no pending config node references - safe to add
delete node._configNodeReferences
new_nodes.push(node)
handledConfigNodes.add(node.id)
} else {
// This node has pending config node references
// Put to the back of the queue
pendingConfigNodes.push(node)
}
iterationLimit--
}
if (pendingConfigNodes.length > 0) {
// We exceeded the iteration count. Could be due to reference loops
// between the config nodes. At this point, just add the remaining
// nodes as-is
pendingConfigNodes.forEach(node => {
delete node._configNodeReferences
new_nodes.push(node)
})
}
// Find regular flow nodes and subflow instances // Find regular flow nodes and subflow instances
for (i=0;i<newNodes.length;i++) { for (i=0;i<newNodes.length;i++) {
n = newNodes[i]; n = newNodes[i];
@ -2170,7 +2255,7 @@ RED.nodes = (function() {
x:parseFloat(n.x || 0), x:parseFloat(n.x || 0),
y:parseFloat(n.y || 0), y:parseFloat(n.y || 0),
z:n.z, z:n.z,
type:0, type: n.type,
info: n.info, info: n.info,
changed:false, changed:false,
_config:{} _config:{}
@ -2231,7 +2316,6 @@ RED.nodes = (function() {
} }
} }
} }
node.type = n.type;
node._def = def; node._def = def;
if (node.type === "group") { if (node.type === "group") {
node._def = RED.group.def; node._def = RED.group.def;
@ -2261,6 +2345,15 @@ RED.nodes = (function() {
outputs: n.outputs|| (n.wires && n.wires.length) || 0, outputs: n.outputs|| (n.wires && n.wires.length) || 0,
set: registry.getNodeSet("node-red/unknown") set: registry.getNodeSet("node-red/unknown")
} }
var orig = {};
for (var p in n) {
if (n.hasOwnProperty(p) && p!="x" && p!="y" && p!="z" && p!="id" && p!="wires") {
orig[p] = n[p];
}
}
node._orig = orig;
node.name = n.type;
node.type = "unknown";
} else { } else {
if (subflow_denylist[parentId] || createNewIds || options.importMap[n.id] === "copy") { if (subflow_denylist[parentId] || createNewIds || options.importMap[n.id] === "copy") {
parentId = subflow.id; parentId = subflow.id;
@ -2321,29 +2414,31 @@ RED.nodes = (function() {
node.type = "unknown"; node.type = "unknown";
} }
if (node._def.category != "config") { if (node._def.category != "config") {
if (n.hasOwnProperty('inputs')) { if (n.hasOwnProperty('inputs') && node._def.defaults.hasOwnProperty("inputs")) {
node.inputs = n.inputs; node.inputs = parseInt(n.inputs, 10);
node._config.inputs = JSON.stringify(n.inputs); node._config.inputs = JSON.stringify(n.inputs);
} else { } else {
node.inputs = node._def.inputs; node.inputs = node._def.inputs;
} }
if (n.hasOwnProperty('outputs')) { if (n.hasOwnProperty('outputs') && node._def.defaults.hasOwnProperty("outputs")) {
node.outputs = n.outputs; node.outputs = parseInt(n.outputs, 10);
node._config.outputs = JSON.stringify(n.outputs); node._config.outputs = JSON.stringify(n.outputs);
} else { } else {
node.outputs = node._def.outputs; node.outputs = node._def.outputs;
} }
if (node.hasOwnProperty('wires') && node.wires.length > node.outputs) {
if (!node._def.defaults.hasOwnProperty("outputs") || !isNaN(parseInt(n.outputs))) {
// If 'wires' is longer than outputs, clip wires
console.log("Warning: node.wires longer than node.outputs - trimming wires:",node.id," wires:",node.wires.length," outputs:",node.outputs);
node.wires = node.wires.slice(0,node.outputs);
} else {
// The node declares outputs in its defaults, but has not got a valid value // The node declares outputs in its defaults, but has not got a valid value
// Defer to the length of the wires array // Defer to the length of the wires array
if (node.hasOwnProperty('wires')) {
if (isNaN(node.outputs)) {
node.outputs = node.wires.length; node.outputs = node.wires.length;
} else if (node.wires.length > node.outputs) {
// If 'wires' is longer than outputs, clip wires
console.log("Warning: node.wires longer than node.outputs - trimming wires:", node.id, " wires:", node.wires.length, " outputs:", node.outputs);
node.wires = node.wires.slice(0, node.outputs);
} }
} }
for (d in node._def.defaults) { for (d in node._def.defaults) {
if (node._def.defaults.hasOwnProperty(d) && d !== 'inputs' && d !== 'outputs') { if (node._def.defaults.hasOwnProperty(d) && d !== 'inputs' && d !== 'outputs') {
node[d] = n[d]; node[d] = n[d];
@ -2440,11 +2535,6 @@ RED.nodes = (function() {
nodeList = nodeList.map(function(id) { nodeList = nodeList.map(function(id) {
var node = node_map[id]; var node = node_map[id];
if (node) { if (node) {
if (node._def.category === 'config') {
if (node.users.indexOf(n) === -1) {
node.users.push(n);
}
}
return node.id; return node.id;
} }
return id; return id;
@ -2458,9 +2548,11 @@ RED.nodes = (function() {
n = new_subflows[i]; n = new_subflows[i];
n.in.forEach(function(input) { n.in.forEach(function(input) {
input.wires.forEach(function(wire) { input.wires.forEach(function(wire) {
if (node_map.hasOwnProperty(wire.id)) {
var link = {source:input, sourcePort:0, target:node_map[wire.id]}; var link = {source:input, sourcePort:0, target:node_map[wire.id]};
addLink(link); addLink(link);
new_links.push(link); new_links.push(link);
}
}); });
delete input.wires; delete input.wires;
}); });
@ -2469,11 +2561,13 @@ RED.nodes = (function() {
var link; var link;
if (subflow_map[wire.id] && subflow_map[wire.id].id == n.id) { if (subflow_map[wire.id] && subflow_map[wire.id].id == n.id) {
link = {source:n.in[wire.port], sourcePort:wire.port,target:output}; link = {source:n.in[wire.port], sourcePort:wire.port,target:output};
} else { } else if (node_map.hasOwnProperty(wire.id) || subflow_map.hasOwnProperty(wire.id)) {
link = {source:node_map[wire.id]||subflow_map[wire.id], sourcePort:wire.port,target:output}; link = {source:node_map[wire.id]||subflow_map[wire.id], sourcePort:wire.port,target:output};
} }
if (link) {
addLink(link); addLink(link);
new_links.push(link); new_links.push(link);
}
}); });
delete output.wires; delete output.wires;
}); });
@ -2482,11 +2576,13 @@ RED.nodes = (function() {
var link; var link;
if (subflow_map[wire.id] && subflow_map[wire.id].id == n.id) { if (subflow_map[wire.id] && subflow_map[wire.id].id == n.id) {
link = {source:n.in[wire.port], sourcePort:wire.port,target:n.status}; link = {source:n.in[wire.port], sourcePort:wire.port,target:n.status};
} else { } else if (node_map.hasOwnProperty(wire.id) || subflow_map.hasOwnProperty(wire.id)) {
link = {source:node_map[wire.id]||subflow_map[wire.id], sourcePort:wire.port,target:n.status}; link = {source:node_map[wire.id]||subflow_map[wire.id], sourcePort:wire.port,target:n.status};
} }
if (link) {
addLink(link); addLink(link);
new_links.push(link); new_links.push(link);
}
}); });
delete n.status.wires; delete n.status.wires;
} }
@ -2665,19 +2761,43 @@ RED.nodes = (function() {
return result; return result;
} }
// Update any config nodes referenced by the provided node to ensure their 'users' list is correct /**
function updateConfigNodeUsers(n) { * Update any config nodes referenced by the provided node to ensure
for (var d in n._def.defaults) { * their 'users' list is correct.
if (n._def.defaults.hasOwnProperty(d)) { *
var property = n._def.defaults[d]; * @param {object} node The node in which to check if it contains references
* @param {object} options Options to apply.
* @param {"add" | "remove"} [options.action] Add or remove the node from
* the Config Node users list. Default `add`.
* @param {boolean} [options.emitEvent] Emit the `nodes:changes` event.
* Default true.
*/
function updateConfigNodeUsers(node, options) {
const defaultOptions = { action: "add", emitEvent: true };
options = Object.assign({}, defaultOptions, options);
for (var d in node._def.defaults) {
if (node._def.defaults.hasOwnProperty(d)) {
var property = node._def.defaults[d];
if (property.type) { if (property.type) {
var type = registry.getNodeType(property.type); var type = registry.getNodeType(property.type);
// Need to ensure the type is a config node to not treat links nodes
if (type && type.category == "config") { if (type && type.category == "config") {
var configNode = configNodes[n[d]]; var configNode = configNodes[node[d]];
if (configNode) { if (configNode) {
if (configNode.users.indexOf(n) === -1) { if (options.action === "add") {
configNode.users.push(n); if (configNode.users.indexOf(node) === -1) {
RED.events.emit('nodes:change',configNode) configNode.users.push(node);
if (options.emitEvent) {
RED.events.emit('nodes:change', configNode);
}
}
} else if (options.action === "remove") {
if (configNode.users.indexOf(node) !== -1) {
const users = configNode.users;
users.splice(users.indexOf(node), 1);
if (options.emitEvent) {
RED.events.emit('nodes:change', configNode);
} }
} }
} }
@ -2685,6 +2805,36 @@ RED.nodes = (function() {
} }
} }
} }
}
// Subflows can have config node env
if (node.type.indexOf("subflow:") === 0) {
node.env?.forEach((prop) => {
if (prop.type === "conf-type" && prop.value) {
// Add the node to the config node users
const configNode = getNode(prop.value);
if (configNode) {
if (options.action === "add") {
if (configNode.users.indexOf(node) === -1) {
configNode.users.push(node);
if (options.emitEvent) {
RED.events.emit('nodes:change', configNode);
}
}
} else if (options.action === "remove") {
if (configNode.users.indexOf(node) !== -1) {
const users = configNode.users;
users.splice(users.indexOf(node), 1);
if (options.emitEvent) {
RED.events.emit('nodes:change', configNode);
}
}
}
}
}
});
}
}
function flowVersion(version) { function flowVersion(version) {
if (version !== undefined) { if (version !== undefined) {

View File

@ -334,6 +334,30 @@ RED.clipboard = (function() {
},100); },100);
} }
/**
* Validates if the provided string looks like valid flow json
* @param {string} flowString the string to validate
* @returns If valid, returns the node array
*/
function validateFlowString(flowString) {
const res = JSON.parse(flowString)
if (!Array.isArray(res)) {
throw new Error(RED._("clipboard.import.errors.notArray"));
}
for (let i = 0; i < res.length; i++) {
if (typeof res[i] !== "object") {
throw new Error(RED._("clipboard.import.errors.itemNotObject",{index:i}));
}
if (!Object.hasOwn(res[i], 'id')) {
throw new Error(RED._("clipboard.import.errors.missingId",{index:i}));
}
if (!Object.hasOwn(res[i], 'type')) {
throw new Error(RED._("clipboard.import.errors.missingType",{index:i}));
}
}
return res
}
var validateImportTimeout; var validateImportTimeout;
function validateImport() { function validateImport() {
if (activeTab === "red-ui-clipboard-dialog-import-tab-clipboard") { if (activeTab === "red-ui-clipboard-dialog-import-tab-clipboard") {
@ -351,21 +375,7 @@ RED.clipboard = (function() {
return; return;
} }
try { try {
if (!/^\[[\s\S]*\]$/m.test(v)) { validateFlowString(v)
throw new Error(RED._("clipboard.import.errors.notArray"));
}
var res = JSON.parse(v);
for (var i=0;i<res.length;i++) {
if (typeof res[i] !== "object") {
throw new Error(RED._("clipboard.import.errors.itemNotObject",{index:i}));
}
if (!res[i].hasOwnProperty('id')) {
throw new Error(RED._("clipboard.import.errors.missingId",{index:i}));
}
if (!res[i].hasOwnProperty('type')) {
throw new Error(RED._("clipboard.import.errors.missingType",{index:i}));
}
}
currentPopoverError = null; currentPopoverError = null;
popover.close(true); popover.close(true);
importInput.removeClass("input-error"); importInput.removeClass("input-error");
@ -998,16 +1008,16 @@ RED.clipboard = (function() {
} }
function importNodes(nodesStr,addFlow) { function importNodes(nodesStr,addFlow) {
var newNodes = nodesStr; let newNodes = nodesStr;
if (typeof nodesStr === 'string') { if (typeof nodesStr === 'string') {
try { try {
nodesStr = nodesStr.trim(); nodesStr = nodesStr.trim();
if (nodesStr.length === 0) { if (nodesStr.length === 0) {
return; return;
} }
newNodes = JSON.parse(nodesStr); newNodes = validateFlowString(nodesStr)
} catch(err) { } catch(err) {
var e = new Error(RED._("clipboard.invalidFlow",{message:err.message})); const e = new Error(RED._("clipboard.invalidFlow",{message:err.message}));
e.code = "NODE_RED"; e.code = "NODE_RED";
throw e; throw e;
} }
@ -1342,6 +1352,7 @@ RED.clipboard = (function() {
} }
} }
} catch(err) { } catch(err) {
console.warn('Import failed: ', err)
// Ensure any errors throw above doesn't stop the drop target from // Ensure any errors throw above doesn't stop the drop target from
// being hidden. // being hidden.
} }

View File

@ -61,7 +61,7 @@
} }
this.menu = RED.popover.menu({ this.menu = RED.popover.menu({
tabSelect: true, tabSelect: true,
width: 300, width: Math.max(300, this.element.width()),
maxHeight: 200, maxHeight: 200,
class: "red-ui-autoComplete-container", class: "red-ui-autoComplete-container",
options: completions, options: completions,

View File

@ -63,6 +63,7 @@
pre: value.substring(0,idx), pre: value.substring(0,idx),
match: value.substring(idx,idx+len), match: value.substring(idx,idx+len),
post: value.substring(idx+len), post: value.substring(idx+len),
exact: idx === 0 && value.length === searchValue.length
} }
} }
function generateSpans(match) { function generateSpans(match) {
@ -83,7 +84,7 @@
const srcMatch = getMatch(optSrc, val); const srcMatch = getMatch(optSrc, val);
if (valMatch.found || srcMatch.found) { if (valMatch.found || srcMatch.found) {
const element = $('<div>',{style: "display: flex"}); const element = $('<div>',{style: "display: flex"});
const valEl = $('<div/>',{style:"font-family: var(--red-ui-monospace-font); white-space:nowrap; overflow: hidden; flex-grow:1"}); const valEl = $('<div/>',{ class: "red-ui-autoComplete-completion" });
valEl.append(generateSpans(valMatch)); valEl.append(generateSpans(valMatch));
valEl.appendTo(element); valEl.appendTo(element);
if (optSrc) { if (optSrc) {
@ -159,7 +160,7 @@
if (valMatch.found) { if (valMatch.found) {
const optSrc = envVarsMap[v] const optSrc = envVarsMap[v]
const element = $('<div>',{style: "display: flex"}); const element = $('<div>',{style: "display: flex"});
const valEl = $('<div/>',{style:"font-family: var(--red-ui-monospace-font); white-space:nowrap; overflow: hidden; flex-grow:1"}); const valEl = $('<div/>',{ class: "red-ui-autoComplete-completion" });
valEl.append(generateSpans(valMatch)) valEl.append(generateSpans(valMatch))
valEl.appendTo(element) valEl.appendTo(element)
@ -201,7 +202,7 @@
const that = this const that = this
const getContextKeysFromRuntime = function(scope, store, searchKey, done) { const getContextKeysFromRuntime = function(scope, store, searchKey, done) {
contextKnownKeys[scope] = contextKnownKeys[scope] || {} contextKnownKeys[scope] = contextKnownKeys[scope] || {}
contextKnownKeys[scope][store] = contextKnownKeys[scope][store] || new Set() contextKnownKeys[scope][store] = contextKnownKeys[scope][store] || new Map()
if (searchKey.length > 0) { if (searchKey.length > 0) {
try { try {
RED.utils.normalisePropertyExpression(searchKey) RED.utils.normalisePropertyExpression(searchKey)
@ -223,11 +224,12 @@
const result = data[store] || {} const result = data[store] || {}
const keys = result.keys || [] const keys = result.keys || []
const keyPrefix = searchKey + (searchKey.length > 0 ? '.' : '') const keyPrefix = searchKey + (searchKey.length > 0 ? '.' : '')
keys.forEach(key => { keys.forEach(keyInfo => {
const key = keyInfo.key
if (/^[a-zA-Z_$][0-9a-zA-Z_$]*$/.test(key)) { if (/^[a-zA-Z_$][0-9a-zA-Z_$]*$/.test(key)) {
contextKnownKeys[scope][store].add(keyPrefix + key) contextKnownKeys[scope][store].set(keyPrefix + key, keyInfo)
} else { } else {
contextKnownKeys[scope][store].add(searchKey + "[\""+key.replace(/"/,"\\\"")+"\"]") contextKnownKeys[scope][store].set(searchKey + "[\""+key.replace(/"/,"\\\"")+"\"]", keyInfo)
} }
}) })
done() done()
@ -242,14 +244,14 @@
// Get the flow id of the node we're editing // Get the flow id of the node we're editing
const editStack = RED.editor.getEditStack() const editStack = RED.editor.getEditStack()
if (editStack.length === 0) { if (editStack.length === 0) {
done([]) done(new Map())
return return
} }
const editingNode = editStack.pop() const editingNode = editStack.pop()
if (editingNode.z) { if (editingNode.z) {
scope = `${scope}/${editingNode.z}` scope = `${scope}/${editingNode.z}`
} else { } else {
done([]) done(new Map())
return return
} }
} }
@ -269,17 +271,29 @@
return function(val, done) { return function(val, done) {
getContextKeys(val, function (keys) { getContextKeys(val, function (keys) {
const matches = [] const matches = []
keys.forEach(v => { keys.forEach((keyInfo, v) => {
let optVal = v let optVal = v
let valMatch = getMatch(optVal, val); let valMatch = getMatch(optVal, val);
if (!valMatch.found && val.length > 0 && val.endsWith('.')) { if (!valMatch.found && val.length > 0) {
if (val.endsWith('.')) {
// Search key ends in '.' - but doesn't match. Check again // Search key ends in '.' - but doesn't match. Check again
// with [" at the end instead so we match bracket notation // with [" at the end instead so we match bracket notation
valMatch = getMatch(optVal, val.substring(0, val.length - 1) + '["') valMatch = getMatch(optVal, val.substring(0, val.length - 1) + '["')
// } else if (val.endsWith('[') && /^array/.test(keyInfo.format)) {
// console.log('this case')
}
} }
if (valMatch.found) { if (valMatch.found) {
const element = $('<div>',{style: "display: flex"}); const element = $('<div>',{style: "display: flex"});
const valEl = $('<div/>',{style:"font-family: var(--red-ui-monospace-font); white-space:nowrap; overflow: hidden; flex-grow:1"}); const valEl = $('<div/>',{ class: "red-ui-autoComplete-completion" });
// if (keyInfo.format) {
// valMatch.post += ' ' + keyInfo.format
// }
if (valMatch.exact && /^array/.test(keyInfo.format)) {
valMatch.post += `[0-${keyInfo.length}]`
optVal += '['
}
valEl.append(generateSpans(valMatch)) valEl.append(generateSpans(valMatch))
valEl.appendTo(element) valEl.appendTo(element)
matches.push({ matches.push({
@ -1567,7 +1581,8 @@
if (tooltip) { if (tooltip) {
tooltip.setContent(valid); tooltip.setContent(valid);
} else { } else {
tooltip = RED.popover.tooltip(this.elementDiv, valid); const target = this.typeMap[type]?.options ? this.optionSelectLabel : this.elementDiv;
tooltip = RED.popover.tooltip(target, valid);
this.element.data("tooltip", tooltip); this.element.data("tooltip", tooltip);
} }
} }

View File

@ -54,15 +54,15 @@ RED.contextMenu = (function () {
} }
} }
const scale = RED.view.scale()
const offset = $("#red-ui-workspace-chart").offset() const offset = $("#red-ui-workspace-chart").offset()
let addX = (options.x - offset.left + $("#red-ui-workspace-chart").scrollLeft()) / scale
let addX = options.x - offset.left + $("#red-ui-workspace-chart").scrollLeft() let addY = (options.y - offset.top + $("#red-ui-workspace-chart").scrollTop()) / scale
let addY = options.y - offset.top + $("#red-ui-workspace-chart").scrollTop()
if (RED.view.snapGrid) { if (RED.view.snapGrid) {
const gridSize = RED.view.gridSize() const gridSize = RED.view.gridSize()
addX = gridSize * Math.floor(addX / gridSize) addX = gridSize * Math.round(addX / gridSize)
addY = gridSize * Math.floor(addY / gridSize) addY = gridSize * Math.round(addY / gridSize)
} }
if (RED.settings.theme("menu.menu-item-action-list", true)) { if (RED.settings.theme("menu.menu-item-action-list", true)) {
@ -87,7 +87,9 @@ RED.contextMenu = (function () {
}, },
(hasLinks) ? { // has least 1 wire selected (hasLinks) ? { // has least 1 wire selected
label: RED._("contextMenu.junction"), label: RED._("contextMenu.junction"),
onselect: 'core:split-wires-with-junctions', onselect: function () {
RED.actions.invoke('core:split-wires-with-junctions', { x: addX, y: addY })
},
disabled: !canEdit || !hasLinks disabled: !canEdit || !hasLinks
} : { } : {
label: RED._("contextMenu.junction"), label: RED._("contextMenu.junction"),

View File

@ -44,6 +44,7 @@ RED.deploy = (function() {
/** /**
* options: * options:
* type: "default" - Button with drop-down options - no further customisation available * type: "default" - Button with drop-down options - no further customisation available
* label: the text to display - default: "Deploy"
* type: "simple" - Button without dropdown. Customisations: * type: "simple" - Button without dropdown. Customisations:
* label: the text to display - default: "Deploy" * label: the text to display - default: "Deploy"
* icon : the icon to use. Null removes the icon. default: "red/images/deploy-full-o.svg" * icon : the icon to use. Null removes the icon. default: "red/images/deploy-full-o.svg"
@ -51,13 +52,14 @@ RED.deploy = (function() {
function init(options) { function init(options) {
options = options || {}; options = options || {};
var type = options.type || "default"; var type = options.type || "default";
var label = options.label || RED._("deploy.deploy");
if (type == "default") { if (type == "default") {
$('<li><span class="red-ui-deploy-button-group button-group">'+ $('<li><span class="red-ui-deploy-button-group button-group">'+
'<a id="red-ui-header-button-deploy" class="red-ui-deploy-button disabled" href="#">'+ '<a id="red-ui-header-button-deploy" class="red-ui-deploy-button disabled" href="#">'+
'<span class="red-ui-deploy-button-content">'+ '<span class="red-ui-deploy-button-content">'+
'<img id="red-ui-header-button-deploy-icon" src="red/images/deploy-full-o.svg"> '+ '<img id="red-ui-header-button-deploy-icon" src="red/images/deploy-full-o.svg"> '+
'<span>'+RED._("deploy.deploy")+'</span>'+ '<span>'+label+'</span>'+
'</span>'+ '</span>'+
'<span class="red-ui-deploy-button-spinner hide">'+ '<span class="red-ui-deploy-button-spinner hide">'+
'<img src="red/images/spin.svg"/>'+ '<img src="red/images/spin.svg"/>'+
@ -78,7 +80,6 @@ RED.deploy = (function() {
mainMenuItems.push({id:"deploymenu-item-reload", icon:"red/images/deploy-reload.svg",label:RED._("deploy.restartFlows"),sublabel:RED._("deploy.restartFlowsDesc"),onselect:"core:restart-flows"}) mainMenuItems.push({id:"deploymenu-item-reload", icon:"red/images/deploy-reload.svg",label:RED._("deploy.restartFlows"),sublabel:RED._("deploy.restartFlowsDesc"),onselect:"core:restart-flows"})
RED.menu.init({id:"red-ui-header-button-deploy-options", options: mainMenuItems }); RED.menu.init({id:"red-ui-header-button-deploy-options", options: mainMenuItems });
} else if (type == "simple") { } else if (type == "simple") {
var label = options.label || RED._("deploy.deploy");
var icon = 'red/images/deploy-full-o.svg'; var icon = 'red/images/deploy-full-o.svg';
if (options.hasOwnProperty('icon')) { if (options.hasOwnProperty('icon')) {
icon = options.icon; icon = options.icon;
@ -424,11 +425,15 @@ RED.deploy = (function() {
const unknownNodes = []; const unknownNodes = [];
const invalidNodes = []; const invalidNodes = [];
const isDisabled = function (node) {
return (node.d || RED.nodes.workspace(node.z)?.disabled);
};
RED.nodes.eachConfig(function (node) { RED.nodes.eachConfig(function (node) {
if (node.valid === undefined) { if (node.valid === undefined) {
RED.editor.validateNode(node); RED.editor.validateNode(node);
} }
if (!node.valid && !node.d) { if (!node.valid && !isDisabled(node)) {
invalidNodes.push(getNodeInfo(node)); invalidNodes.push(getNodeInfo(node));
} }
if (node.type === "unknown") { if (node.type === "unknown") {
@ -438,7 +443,7 @@ RED.deploy = (function() {
} }
}); });
RED.nodes.eachNode(function (node) { RED.nodes.eachNode(function (node) {
if (!node.valid && !node.d) { if (!node.valid && !isDisabled(node)) {
invalidNodes.push(getNodeInfo(node)); invalidNodes.push(getNodeInfo(node));
} }
if (node.type === "unknown") { if (node.type === "unknown") {
@ -452,7 +457,7 @@ RED.deploy = (function() {
const unusedConfigNodes = []; const unusedConfigNodes = [];
RED.nodes.eachConfig(function (node) { RED.nodes.eachConfig(function (node) {
if ((node._def.hasUsers !== false) && (node.users.length === 0)) { if ((node._def.hasUsers !== false) && (node.users.length === 0) && !isDisabled(node)) {
unusedConfigNodes.push(getNodeInfo(node)); unusedConfigNodes.push(getNodeInfo(node));
hasUnusedConfig = true; hasUnusedConfig = true;
} }

View File

@ -808,6 +808,20 @@ RED.editor = (function() {
} }
} }
const oldCreds = {};
if (editing_node._def.credentials) {
for (const prop in editing_node._def.credentials) {
if (Object.prototype.hasOwnProperty.call(editing_node._def.credentials, prop)) {
if (editing_node._def.credentials[prop].type === 'password') {
oldCreds['has_' + prop] = editing_node.credentials['has_' + prop];
}
if (prop in editing_node.credentials) {
oldCreds[prop] = editing_node.credentials[prop];
}
}
}
}
try { try {
const rc = editing_node._def.oneditsave.call(editing_node); const rc = editing_node._def.oneditsave.call(editing_node);
if (rc === true) { if (rc === true) {
@ -839,6 +853,25 @@ RED.editor = (function() {
} }
} }
} }
if (editing_node._def.credentials) {
for (const prop in editing_node._def.credentials) {
if (Object.prototype.hasOwnProperty.call(editing_node._def.credentials, prop)) {
if (oldCreds[prop] !== editing_node.credentials[prop]) {
if (editing_node.credentials[prop] === '__PWRD__') {
// The password may not exist in oldCreds
// The value '__PWRD__' means the password exists,
// so ignore this change
continue;
}
editState.changes.credentials = editState.changes.credentials || {};
editState.changes.credentials['has_' + prop] = oldCreds['has_' + prop];
editState.changes.credentials[prop] = oldCreds[prop];
editState.changed = true;
}
}
}
}
} }
} }
@ -1481,134 +1514,181 @@ RED.editor = (function() {
}, },
{ {
id: "node-config-dialog-ok", id: "node-config-dialog-ok",
text: adding?RED._("editor.configAdd"):RED._("editor.configUpdate"), text: adding ? RED._("editor.configAdd") : RED._("editor.configUpdate"),
class: "primary", class: "primary",
click: function() { click: function() {
var editState = { // TODO: Already defined
const configProperty = name;
const configType = type;
const configTypeDef = RED.nodes.getType(configType);
const wasChanged = editing_config_node.changed;
const editState = {
changes: {}, changes: {},
changed: false, changed: false,
outputMap: null outputMap: null
}; };
var configProperty = name;
var configId = editing_config_node.id;
var configType = type;
var configAdding = adding;
var configTypeDef = RED.nodes.getType(configType);
var d;
var input;
if (configTypeDef.oneditsave) { // Call `oneditsave` and search for changes
try { handleEditSave(editing_config_node, editState);
configTypeDef.oneditsave.call(editing_config_node);
} catch(err) {
console.warn("oneditsave",editing_config_node.id,editing_config_node.type,err.toString());
}
}
for (d in configTypeDef.defaults) { // Search for changes in the edit box (panes)
if (configTypeDef.defaults.hasOwnProperty(d)) { activeEditPanes.forEach(function (pane) {
var newValue;
input = $("#node-config-input-"+d);
if (input.attr('type') === "checkbox") {
newValue = input.prop('checked');
} else if ("format" in configTypeDef.defaults[d] && configTypeDef.defaults[d].format !== "" && input[0].nodeName === "DIV") {
newValue = input.text();
} else {
newValue = input.val();
}
if (newValue != null && newValue !== editing_config_node[d]) {
if (editing_config_node._def.defaults[d].type) {
if (newValue == "_ADD_") {
newValue = "";
}
// Change to a related config node
var configNode = RED.nodes.node(editing_config_node[d]);
if (configNode) {
var users = configNode.users;
users.splice(users.indexOf(editing_config_node),1);
RED.events.emit("nodes:change",configNode);
}
configNode = RED.nodes.node(newValue);
if (configNode) {
configNode.users.push(editing_config_node);
RED.events.emit("nodes:change",configNode);
}
}
editing_config_node[d] = newValue;
}
}
}
activeEditPanes.forEach(function(pane) {
if (pane.apply) { if (pane.apply) {
pane.apply.call(pane, editState); pane.apply.call(pane, editState);
} }
}) });
editing_config_node.label = configTypeDef.label; // TODO: Why?
editing_config_node.label = configTypeDef.label
var scope = $("#red-ui-editor-config-scope").val();
editing_config_node.z = scope;
// Check if disabled has changed
if ($("#node-config-input-node-disabled").prop('checked')) { if ($("#node-config-input-node-disabled").prop('checked')) {
if (editing_config_node.d !== true) { if (editing_config_node.d !== true) {
editState.changes.d = editing_config_node.d;
editState.changed = true;
editing_config_node.d = true; editing_config_node.d = true;
} }
} else { } else {
if (editing_config_node.d === true) { if (editing_config_node.d === true) {
editState.changes.d = editing_config_node.d;
editState.changed = true;
delete editing_config_node.d; delete editing_config_node.d;
} }
} }
if (scope) { // NOTE: must be undefined if no scope used
// Search for nodes that use this one that are no longer const scope = $("#red-ui-editor-config-scope").val() || undefined;
// Check if the scope has changed
if (editing_config_node.z !== scope) {
editState.changes.z = editing_config_node.z;
editState.changed = true;
editing_config_node.z = scope;
}
// Search for nodes that use this config node that are no longer
// in scope, so must be removed // in scope, so must be removed
editing_config_node.users = editing_config_node.users.filter(function(n) { const historyEvents = [];
var keep = true; if (scope) {
for (var d in n._def.defaults) { const newUsers = editing_config_node.users.filter(function (node) {
if (n._def.defaults.hasOwnProperty(d)) { let keepNode = false;
if (n._def.defaults[d].type === editing_config_node.type && let nodeModified = null;
n[d] === editing_config_node.id &&
n.z !== scope) { for (const d in node._def.defaults) {
keep = false; if (node._def.defaults.hasOwnProperty(d)) {
// Remove the reference to this node if (node._def.defaults[d].type === editing_config_node.type) {
// and revalidate if (node[d] === editing_config_node.id) {
n[d] = null; if (node.z === editing_config_node.z) {
n.dirty = true; // The node is kept only if at least one property uses
n.changed = true; // this config node in the correct scope.
validateNode(n); keepNode = true;
} else {
if (!nodeModified) {
nodeModified = {
t: "edit",
node: node,
changes: { [d]: node[d] },
changed: node.changed,
dirty: node.dirty
};
} else {
nodeModified.changes[d] = node[d];
}
// Remove the reference to the config node
node[d] = "";
} }
} }
} }
return keep; }
}
// Add the node modified to the history
if (nodeModified) {
historyEvents.push(nodeModified);
}
// Mark as changed and revalidate this node
if (!keepNode) {
node.changed = true;
node.dirty = true;
validateNode(node);
RED.events.emit("nodes:change", node);
}
return keepNode;
}); });
// Check if users are changed
if (editing_config_node.users.length !== newUsers.length) {
editState.changes.users = editing_config_node.users;
editState.changed = true;
editing_config_node.users = newUsers;
}
} }
if (configAdding) { if (editState.changed) {
RED.nodes.add(editing_config_node); // Set the congig node as changed
editing_config_node.changed = true;
} }
// Now, validate the config node
validateNode(editing_config_node); validateNode(editing_config_node);
var validatedNodes = {};
validatedNodes[editing_config_node.id] = true;
var userStack = editing_config_node.users.slice(); // And validate nodes using this config node too
while(userStack.length > 0) { const validatedNodes = new Set();
var user = userStack.pop(); const userStack = editing_config_node.users.slice();
if (!validatedNodes[user.id]) {
validatedNodes[user.id] = true; validatedNodes.add(editing_config_node.id);
if (user.users) { while (userStack.length) {
userStack = userStack.concat(user.users); const node = userStack.pop();
if (!validatedNodes.has(node.id)) {
validatedNodes.add(node.id);
if (node.users) {
userStack.push(...node.users);
} }
validateNode(user); validateNode(node);
} }
} }
let historyEvent = {
t: "edit",
node: editing_config_node,
changes: editState.changes,
changed: wasChanged,
dirty: RED.nodes.dirty()
};
if (historyEvents.length) {
// Need a multi events
historyEvent = {
t: "multi",
events: [historyEvent].concat(historyEvents),
dirty: historyEvent.dirty
};
}
if (!adding) {
// This event is triggered when the edit box is saved,
// regardless of whether there are any modifications.
RED.events.emit("editor:save", editing_config_node);
}
if (editState.changed) {
if (adding) {
RED.history.push({ t: "add", nodes: [editing_config_node.id], dirty: RED.nodes.dirty() });
// Add the new config node and trigger the `nodes:add` event
RED.nodes.add(editing_config_node);
} else {
RED.history.push(historyEvent);
RED.events.emit("nodes:change", editing_config_node);
}
RED.nodes.dirty(true); RED.nodes.dirty(true);
RED.view.redraw(true); RED.view.redraw(true);
if (!configAdding) {
RED.events.emit("editor:save",editing_config_node);
RED.events.emit("nodes:change",editing_config_node);
} }
RED.tray.close(function() { RED.tray.close(function() {
var filter = null; var filter = null;
// when editing a config via subflow edit panel, the `configProperty` will not // when editing a config via subflow edit panel, the `configProperty` will not
@ -1756,8 +1836,18 @@ RED.editor = (function() {
} }
}); });
} }
let envToRemove = new Set()
if (!isSameObj(old_env, new_env)) { if (!isSameObj(old_env, new_env)) {
// Get a list of env properties that have been removed
// by comparing old_env and new_env
if (old_env) {
old_env.forEach(env => { envToRemove.add(env.name) })
}
if (new_env) {
new_env.forEach(env => {
envToRemove.delete(env.name)
})
}
editState.changes.env = editing_node.env; editState.changes.env = editing_node.env;
editing_node.env = new_env; editing_node.env = new_env;
editState.changed = true; editState.changed = true;
@ -1766,10 +1856,11 @@ RED.editor = (function() {
if (editState.changed) { if (editState.changed) {
var wasChanged = editing_node.changed; let wasChanged = editing_node.changed;
editing_node.changed = true; editing_node.changed = true;
validateNode(editing_node); validateNode(editing_node);
var subflowInstances = []; let subflowInstances = [];
let instanceHistoryEvents = []
RED.nodes.eachNode(function(n) { RED.nodes.eachNode(function(n) {
if (n.type == "subflow:"+editing_node.id) { if (n.type == "subflow:"+editing_node.id) {
subflowInstances.push({ subflowInstances.push({
@ -1779,13 +1870,35 @@ RED.editor = (function() {
n._def.color = editing_node.color; n._def.color = editing_node.color;
n.changed = true; n.changed = true;
n.dirty = true; n.dirty = true;
if (n.env) {
const oldEnv = n.env
const newEnv = []
let envChanged = false
n.env.forEach((env, index) => {
if (envToRemove.has(env.name)) {
envChanged = true
} else {
newEnv.push(env)
}
})
if (envChanged) {
instanceHistoryEvents.push({
t: 'edit',
node: n,
changes: { env: oldEnv },
dirty: n.dirty,
changed: n.changed
})
n.env = newEnv
}
}
updateNodeProperties(n); updateNodeProperties(n);
validateNode(n); validateNode(n);
} }
}); });
RED.events.emit("subflows:change",editing_node); RED.events.emit("subflows:change",editing_node);
RED.nodes.dirty(true); RED.nodes.dirty(true);
var historyEvent = { let historyEvent = {
t:'edit', t:'edit',
node:editing_node, node:editing_node,
changes:editState.changes, changes:editState.changes,
@ -1795,7 +1908,13 @@ RED.editor = (function() {
instances:subflowInstances instances:subflowInstances
} }
}; };
if (instanceHistoryEvents.length > 0) {
historyEvent = {
t: 'multi',
events: [ historyEvent, ...instanceHistoryEvents ],
dirty: wasDirty
}
}
RED.history.push(historyEvent); RED.history.push(historyEvent);
} }
editing_node.dirty = true; editing_node.dirty = true;

View File

@ -691,6 +691,7 @@ RED.editor.codeEditor.monaco = (function() {
2322, //Type 'unknown' is not assignable to type 'string' 2322, //Type 'unknown' is not assignable to type 'string'
2339, //property does not exist on 2339, //property does not exist on
2345, //Argument of type xxx is not assignable to parameter of type 'DateTimeFormatOptions' 2345, //Argument of type xxx is not assignable to parameter of type 'DateTimeFormatOptions'
2538, //Ignore symbols as index property error.
7043, //i forget what this one is, 7043, //i forget what this one is,
80001, //Convert to ES6 module 80001, //Convert to ES6 module
80004, //JSDoc types may be moved to TypeScript types. 80004, //JSDoc types may be moved to TypeScript types.

View File

@ -131,7 +131,7 @@ RED.editor.envVarList = (function() {
nameField.trigger('change'); nameField.trigger('change');
} }
}, },
sortable: ".red-ui-editableList-item-handle", sortable: true,
removable: false removable: false
}); });
var parentEnv = {}; var parentEnv = {};

View File

@ -27,6 +27,12 @@
reader.readAsDataURL(file); reader.readAsDataURL(file);
} }
function file2Text(file,cb) {
file.arrayBuffer().then(d => {
cb( new TextDecoder().decode(d) )
}).catch(ex => { cb(`error: ${ex}`) })
}
var initialized = false; var initialized = false;
var currentEditor = null; var currentEditor = null;
/** /**
@ -52,6 +58,7 @@
if (files.length === 1) { if (files.length === 1) {
var file = files[0]; var file = files[0];
var name = file.name.toLowerCase(); var name = file.name.toLowerCase();
var fileType = file.type.toLowerCase();
if (name.match(/\.(apng|avif|gif|jpeg|png|svg|webp)$/)) { if (name.match(/\.(apng|avif|gif|jpeg|png|svg|webp)$/)) {
file2base64Image(file, function (image) { file2base64Image(file, function (image) {
@ -63,6 +70,29 @@
}); });
return; return;
} }
if ( fileType.startsWith("text/") ) {
file2Text(file, function (txt) {
var session = currentEditor.getSession();
var pos = session.getCursorPosition();
session.insert(pos, txt);
$("#red-ui-image-drop-target").hide();
});
return;
}
}
} else if ($.inArray("text/plain", ev.originalEvent.dataTransfer.types) != -1) {
let item = Object.values(ev.originalEvent.dataTransfer.items).filter(d => d.type == "text/plain")[0]
if (item) {
item.getAsString(txt => {
var session = currentEditor.getSession();
var pos = session.getCursorPosition();
session.insert(pos, txt);
$("#red-ui-image-drop-target").hide();
})
return
} }
} }
$("#red-ui-image-drop-target").hide(); $("#red-ui-image-drop-target").hide();

View File

@ -20,10 +20,31 @@
apply: function(editState) { apply: function(editState) {
var old_env = node.env; var old_env = node.env;
var new_env = []; var new_env = [];
if (/^subflow:/.test(node.type)) { if (/^subflow:/.test(node.type)) {
// Get the list of environment variables from the node properties
new_env = RED.subflow.exportSubflowInstanceEnv(node); new_env = RED.subflow.exportSubflowInstanceEnv(node);
} }
if (old_env && old_env.length) {
old_env.forEach(function (prop) {
if (prop.type === "conf-type" && prop.value) {
const stillInUse = new_env?.some((p) => p.type === "conf-type" && p.name === prop.name && p.value === prop.value);
if (!stillInUse) {
// Remove the node from the config node users
// Only for empty value or modified
const configNode = RED.nodes.node(prop.value);
if (configNode) {
if (configNode.users.indexOf(node) !== -1) {
configNode.users.splice(configNode.users.indexOf(node), 1);
RED.events.emit('nodes:change', configNode)
}
}
}
}
});
}
// Get the values from the Properties table tab // Get the values from the Properties table tab
var items = this.list.editableList('items'); var items = this.list.editableList('items');
items.each(function (i,el) { items.each(function (i,el) {
@ -41,7 +62,6 @@
} }
}); });
if (new_env && new_env.length > 0) { if (new_env && new_env.length > 0) {
new_env.forEach(function(prop) { new_env.forEach(function(prop) {
if (prop.type === "cred") { if (prop.type === "cred") {
@ -52,6 +72,15 @@
editState.changed = true; editState.changed = true;
} }
delete prop.value; delete prop.value;
} else if (prop.type === "conf-type" && prop.value) {
const configNode = RED.nodes.node(prop.value);
if (configNode) {
if (configNode.users.indexOf(node) === -1) {
// Add the node to the config node users
configNode.users.push(node);
RED.events.emit('nodes:change', configNode);
}
}
} }
}); });
} }

View File

@ -44,6 +44,7 @@
apply: function(editState) { apply: function(editState) {
var newValue; var newValue;
var d; var d;
// If the node is a subflow, the node's properties (exepts name) are saved by `envProperties`
if (node._def.defaults) { if (node._def.defaults) {
for (d in node._def.defaults) { for (d in node._def.defaults) {
if (node._def.defaults.hasOwnProperty(d)) { if (node._def.defaults.hasOwnProperty(d)) {
@ -131,9 +132,16 @@
} }
} }
if (node._def.credentials) { if (node._def.credentials) {
var credDefinition = node._def.credentials; const credDefinition = node._def.credentials;
var credsChanged = updateNodeCredentials(node,credDefinition,this.inputClass); const credChanges = updateNodeCredentials(node, credDefinition, this.inputClass);
editState.changed = editState.changed || credsChanged;
if (Object.keys(credChanges).length) {
editState.changed = true;
editState.changes.credentials = {
...(editState.changes.credentials || {}),
...credChanges
};
}
} }
} }
} }
@ -161,10 +169,11 @@
* @param node - the node containing the credentials * @param node - the node containing the credentials
* @param credDefinition - definition of the credentials * @param credDefinition - definition of the credentials
* @param prefix - prefix of the input fields * @param prefix - prefix of the input fields
* @return {boolean} whether anything has changed * @return {object} an object containing the modified properties
*/ */
function updateNodeCredentials(node, credDefinition, prefix) { function updateNodeCredentials(node, credDefinition, prefix) {
var changed = false; const changes = {};
if (!node.credentials) { if (!node.credentials) {
node.credentials = {_:{}}; node.credentials = {_:{}};
} else if (!node.credentials._) { } else if (!node.credentials._) {
@ -177,22 +186,33 @@
if (input.length > 0) { if (input.length > 0) {
var value = input.val(); var value = input.val();
if (credDefinition[cred].type == 'password') { if (credDefinition[cred].type == 'password') {
node.credentials['has_' + cred] = (value !== ""); if (value === '__PWRD__') {
if (value == '__PWRD__') { // A cred value exists - no changes
continue; } else if (value === '' && node.credentials['has_' + cred] === false) {
} // Empty cred value exists - no changes
changed = true; } else if (value === node.credentials[cred]) {
// A cred value exists locally in the editor - no changes
} // Like the user sets a value, saves the config,
// reopens the config and save the config again
} else {
changes['has_' + cred] = node.credentials['has_' + cred];
changes[cred] = node.credentials[cred];
node.credentials[cred] = value; node.credentials[cred] = value;
if (value != node.credentials._[cred]) {
changed = true;
}
}
}
}
return changed;
} }
node.credentials['has_' + cred] = (value !== '');
} else {
// Since these creds are loaded by the editor,
// values can be directly compared
if (value !== node.credentials[cred]) {
changes[cred] = node.credentials[cred];
node.credentials[cred] = value;
}
}
}
}
}
return changes;
}
})(); })();

View File

@ -245,10 +245,15 @@ RED.library = (function() {
if (lib.types && lib.types.indexOf(options.url) === -1) { if (lib.types && lib.types.indexOf(options.url) === -1) {
return; return;
} }
let icon = 'fa fa-hdd-o';
if (lib.icon) {
const fullIcon = RED.utils.separateIconPath(lib.icon);
icon = (fullIcon.module==="font-awesome"?"fa ":"")+fullIcon.file;
}
listing.push({ listing.push({
library: lib.id, library: lib.id,
type: options.url, type: options.url,
icon: lib.icon || 'fa fa-hdd-o', icon,
label: RED._(lib.label||lib.id), label: RED._(lib.label||lib.id),
path: "", path: "",
expanded: true, expanded: true,
@ -303,10 +308,15 @@ RED.library = (function() {
if (lib.types && lib.types.indexOf(options.url) === -1) { if (lib.types && lib.types.indexOf(options.url) === -1) {
return; return;
} }
let icon = 'fa fa-hdd-o';
if (lib.icon) {
const fullIcon = RED.utils.separateIconPath(lib.icon);
icon = (fullIcon.module==="font-awesome"?"fa ":"")+fullIcon.file;
}
listing.push({ listing.push({
library: lib.id, library: lib.id,
type: options.url, type: options.url,
icon: lib.icon || 'fa fa-hdd-o', icon,
label: RED._(lib.label||lib.id), label: RED._(lib.label||lib.id),
path: "", path: "",
expanded: true, expanded: true,

View File

@ -1362,7 +1362,7 @@ RED.subflow = (function() {
item.value = ""+input.prop("checked"); item.value = ""+input.prop("checked");
break; break;
case "conf-types": case "conf-types":
item.value = input.val() item.value = input.val() === "_ADD_" ? "" : input.val();
item.type = "conf-type" item.type = "conf-type"
} }
if (ui.type === "cred" || item.type !== data.parent.type || item.value !== data.parent.value) { if (ui.type === "cred" || item.type !== data.parent.type || item.value !== data.parent.value) {

View File

@ -56,7 +56,16 @@ RED.sidebar.config = (function() {
} else { } else {
$('<span class="red-ui-palette-node-config-label" data-i18n="sidebar.config.'+name+'">').appendTo(header); $('<span class="red-ui-palette-node-config-label" data-i18n="sidebar.config.'+name+'">').appendTo(header);
} }
$('<span class="red-ui-sidebar-node-config-filter-info"></span>').appendTo(header); $('<span class="red-ui-sidebar-node-config-filter-info"></span>').appendTo(header);
const changeBadgeContainer = $('<svg class="red-ui-sidebar-config-category-changed red-ui-flow-node-changed" width="10" height="10" viewBox="-1 -1 12 12"></svg>').appendTo(header);
const changeBadge = document.createElementNS("http://www.w3.org/2000/svg", "circle");
changeBadge.setAttribute("cx", "5");
changeBadge.setAttribute("cy", "5");
changeBadge.setAttribute("r", "5");
changeBadgeContainer.append(changeBadge);
category = $('<ul class="red-ui-palette-content red-ui-sidebar-node-config-list"></ul>').appendTo(container); category = $('<ul class="red-ui-palette-content red-ui-sidebar-node-config-list"></ul>').appendTo(container);
category.on("click", function(e) { category.on("click", function(e) {
$(content).find(".red-ui-palette-node").removeClass("selected"); $(content).find(".red-ui-palette-node").removeClass("selected");
@ -150,9 +159,6 @@ RED.sidebar.config = (function() {
$('<li class="red-ui-palette-node-config-type">'+node.type+'</li>').appendTo(list); $('<li class="red-ui-palette-node-config-type">'+node.type+'</li>').appendTo(list);
currentType = node.type; currentType = node.type;
} }
if (node.changed) {
labelText += "!!"
}
var entry = $('<li class="red-ui-palette-node_id_'+node.id.replace(/\./g,"-")+'"></li>').appendTo(list); var entry = $('<li class="red-ui-palette-node_id_'+node.id.replace(/\./g,"-")+'"></li>').appendTo(list);
var nodeDiv = $('<div class="red-ui-palette-node-config red-ui-palette-node"></div>').appendTo(entry); var nodeDiv = $('<div class="red-ui-palette-node-config red-ui-palette-node"></div>').appendTo(entry);
entry.data('node',node.id); entry.data('node',node.id);
@ -181,15 +187,29 @@ RED.sidebar.config = (function() {
} }
} }
if (node.changed) {
const nodeDivAnnotations = $('<svg class="red-ui-palette-node-annotations red-ui-flow-node-changed" width="10" height="10" viewBox="-1 -1 12 12"></svg>').appendTo(nodeDiv);
const changeBadge = document.createElementNS("http://www.w3.org/2000/svg", "circle");
changeBadge.setAttribute("cx", "5");
changeBadge.setAttribute("cy", "5");
changeBadge.setAttribute("r", "5");
nodeDivAnnotations.append($(changeBadge));
const categoryHeader = list.parent().find(".red-ui-sidebar-config-tray-header.red-ui-palette-header");
categoryHeader.addClass("red-ui-sidebar-config-changed");
nodeDiv.addClass("red-ui-palette-node-config-changed");
}
if (!node.valid) { if (!node.valid) {
nodeDiv.addClass("red-ui-palette-node-config-invalid") const nodeDivAnnotations = $('<svg class="red-ui-palette-node-annotations red-ui-flow-node-error" width="10" height="10"></svg>').appendTo(nodeDiv);
const nodeDivAnnotations = $('<svg class="red-ui-palette-node-annotations red-ui-flow-node-error" width="10" height="10"></svg>').appendTo(nodeDiv) const errorBadge = document.createElementNS("http://www.w3.org/2000/svg", "path");
const errorBadge = document.createElementNS("http://www.w3.org/2000/svg","path"); errorBadge.setAttribute("d", "M 0,9 l 10,0 -5,-8 z");
errorBadge.setAttribute("d","M 0,9 l 10,0 -5,-8 z"); nodeDivAnnotations.append($(errorBadge));
nodeDivAnnotations.append($(errorBadge))
nodeDiv.addClass("red-ui-palette-node-config-invalid");
RED.popover.tooltip(nodeDivAnnotations, function () { RED.popover.tooltip(nodeDivAnnotations, function () {
if (node.validationErrors && node.validationErrors.length > 0) { if (node.validationErrors && node.validationErrors.length > 0) {
return RED._("editor.errors.invalidProperties")+"<br> - "+node.validationErrors.join("<br> - ") return RED._("editor.errors.invalidProperties") + "<br> - " + node.validationErrors.join("<br> - ");
} }
}) })
} }
@ -252,6 +272,10 @@ RED.sidebar.config = (function() {
$(this).remove(); $(this).remove();
delete categories[id]; delete categories[id];
} }
// Remove the `changed` badge from the category header
const categoryHeader = $(this).find(".red-ui-sidebar-config-tray-header.red-ui-palette-header");
categoryHeader.removeClass("red-ui-sidebar-config-changed");
}) })
var globalConfigNodes = []; var globalConfigNodes = [];
var configList = {}; var configList = {};

View File

@ -18,8 +18,6 @@ RED.sidebar.context = (function() {
var content; var content;
var sections; var sections;
var localCache = {};
var flowAutoRefresh; var flowAutoRefresh;
var nodeAutoRefresh; var nodeAutoRefresh;
var nodeSection; var nodeSection;
@ -27,6 +25,8 @@ RED.sidebar.context = (function() {
var flowSection; var flowSection;
var globalSection; var globalSection;
const expandedPaths = {}
var currentNode; var currentNode;
var currentFlow; var currentFlow;
@ -212,14 +212,41 @@ RED.sidebar.context = (function() {
var l = keys.length; var l = keys.length;
for (var i = 0; i < l; i++) { for (var i = 0; i < l; i++) {
sortedData[keys[i]].forEach(function(v) { sortedData[keys[i]].forEach(function(v) {
var k = keys[i]; const k = keys[i];
var l2 = sortedData[k].length; let payload = v.msg;
var propRow = $('<tr class="red-ui-help-info-row"><td class="red-ui-sidebar-context-property"></td><td></td></tr>').appendTo(container); let format = v.format;
var obj = $(propRow.children()[0]); const tools = $('<span class="button-group"></span>');
expandedPaths[id + "." + k] = expandedPaths[id + "." + k] || new Set()
const objectElementOptions = {
typeHint: format,
sourceId: id + "." + k,
tools,
path: k,
rootPath: k,
exposeApi: true,
ontoggle: function(path,state) {
path = path.substring(k.length+1)
if (state) {
expandedPaths[id+"."+k].add(path)
} else {
// if 'a' has been collapsed, we want to remove 'a.b' and 'a[0]...' from the set
// of collapsed paths
for (let expandedPath of expandedPaths[id+"."+k]) {
if (expandedPath.startsWith(path+".") || expandedPath.startsWith(path+"[")) {
expandedPaths[id+"."+k].delete(expandedPath)
}
}
expandedPaths[id+"."+k].delete(path)
}
},
expandPaths: [ ...expandedPaths[id+"."+k] ].sort(),
expandLeafNodes: true
}
const propRow = $('<tr class="red-ui-help-info-row"><td class="red-ui-sidebar-context-property"></td><td></td></tr>').appendTo(container);
const obj = $(propRow.children()[0]);
obj.text(k); obj.text(k);
var tools = $('<span class="button-group"></span>');
const urlSafeK = encodeURIComponent(k) const urlSafeK = encodeURIComponent(k)
var refreshItem = $('<button class="red-ui-button red-ui-button-small"><i class="fa fa-refresh"></i></button>').appendTo(tools).on("click", function(e) { const refreshItem = $('<button class="red-ui-button red-ui-button-small"><i class="fa fa-refresh"></i></button>').appendTo(tools).on("click", function(e) {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
$.getJSON(baseUrl+"/"+urlSafeK+"?store="+v.store, function(data) { $.getJSON(baseUrl+"/"+urlSafeK+"?store="+v.store, function(data) {
@ -229,16 +256,14 @@ RED.sidebar.context = (function() {
tools.detach(); tools.detach();
$(propRow.children()[1]).empty(); $(propRow.children()[1]).empty();
RED.utils.createObjectElement(RED.utils.decodeObject(payload,format), { RED.utils.createObjectElement(RED.utils.decodeObject(payload,format), {
...objectElementOptions,
typeHint: data.format, typeHint: data.format,
sourceId: id+"."+k,
tools: tools,
path: k
}).appendTo(propRow.children()[1]); }).appendTo(propRow.children()[1]);
} }
}) })
}); });
RED.popover.tooltip(refreshItem,RED._("sidebar.context.refrsh")); RED.popover.tooltip(refreshItem,RED._("sidebar.context.refrsh"));
var deleteItem = $('<button class="red-ui-button red-ui-button-small"><i class="fa fa-trash"></i></button>').appendTo(tools).on("click", function(e) { const deleteItem = $('<button class="red-ui-button red-ui-button-small"><i class="fa fa-trash"></i></button>').appendTo(tools).on("click", function(e) {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
var popover = RED.popover.create({ var popover = RED.popover.create({
@ -246,7 +271,7 @@ RED.sidebar.context = (function() {
target: propRow, target: propRow,
direction: "left", direction: "left",
content: function() { content: function() {
var content = $('<div>'); const content = $('<div>');
$('<p data-i18n="sidebar.context.deleteConfirm"></p>').appendTo(content); $('<p data-i18n="sidebar.context.deleteConfirm"></p>').appendTo(content);
var row = $('<p>').appendTo(content); var row = $('<p>').appendTo(content);
var bg = $('<span class="button-group"></span>').appendTo(row); var bg = $('<span class="button-group"></span>').appendTo(row);
@ -269,16 +294,15 @@ RED.sidebar.context = (function() {
if (container.children().length === 0) { if (container.children().length === 0) {
$('<tr class="red-ui-help-info-row red-ui-search-empty blank" colspan="2"><td data-i18n="sidebar.context.empty"></td></tr>').appendTo(container).i18n(); $('<tr class="red-ui-help-info-row red-ui-search-empty blank" colspan="2"><td data-i18n="sidebar.context.empty"></td></tr>').appendTo(container).i18n();
} }
delete expandedPaths[id + "." + k]
} else { } else {
payload = data.msg; payload = data.msg;
format = data.format; format = data.format;
tools.detach(); tools.detach();
$(propRow.children()[1]).empty(); $(propRow.children()[1]).empty();
RED.utils.createObjectElement(RED.utils.decodeObject(payload,format), { RED.utils.createObjectElement(RED.utils.decodeObject(payload,format), {
typeHint: data.format, ...objectElementOptions,
sourceId: id+"."+k, typeHint: data.format
tools: tools,
path: k
}).appendTo(propRow.children()[1]); }).appendTo(propRow.children()[1]);
} }
}); });
@ -293,14 +317,7 @@ RED.sidebar.context = (function() {
}); });
RED.popover.tooltip(deleteItem,RED._("sidebar.context.delete")); RED.popover.tooltip(deleteItem,RED._("sidebar.context.delete"));
var payload = v.msg; RED.utils.createObjectElement(RED.utils.decodeObject(payload,format), objectElementOptions).appendTo(propRow.children()[1]);
var format = v.format;
RED.utils.createObjectElement(RED.utils.decodeObject(payload,format), {
typeHint: v.format,
sourceId: id+"."+k,
tools: tools,
path: k
}).appendTo(propRow.children()[1]);
if (contextStores.length > 1) { if (contextStores.length > 1) {
$("<span>",{class:"red-ui-sidebar-context-property-storename"}).text(v.store).appendTo($(propRow.children()[0])) $("<span>",{class:"red-ui-sidebar-context-property-storename"}).text(v.store).appendTo($(propRow.children()[0]))
} }

View File

@ -382,7 +382,7 @@ RED.typeSearch = (function() {
var items = []; var items = [];
RED.nodes.registry.getNodeTypes().forEach(function(t) { RED.nodes.registry.getNodeTypes().forEach(function(t) {
var def = RED.nodes.getType(t); var def = RED.nodes.getType(t);
if (def.category !== 'config' && t !== 'unknown' && t !== 'tab') { if (def.set?.enabled !== false && def.category !== 'config' && t !== 'unknown' && t !== 'tab') {
items.push({type:t,def: def, label:getTypeLabel(t,def)}); items.push({type:t,def: def, label:getTypeLabel(t,def)});
} }
}); });

View File

@ -230,7 +230,7 @@ RED.utils = (function() {
var pinnedPaths = {}; var pinnedPaths = {};
var formattedPaths = {}; var formattedPaths = {};
function addMessageControls(obj,sourceId,key,msg,rootPath,strippedKey,extraTools) { function addMessageControls(obj,sourceId,key,msg,rootPath,strippedKey,extraTools,enablePinning) {
if (!pinnedPaths.hasOwnProperty(sourceId)) { if (!pinnedPaths.hasOwnProperty(sourceId)) {
pinnedPaths[sourceId] = {} pinnedPaths[sourceId] = {}
} }
@ -250,7 +250,7 @@ RED.utils = (function() {
RED.clipboard.copyText(msg,copyPayload,"clipboard.copyMessageValue"); RED.clipboard.copyText(msg,copyPayload,"clipboard.copyMessageValue");
}) })
RED.popover.tooltip(copyPayload,RED._("node-red:debug.sidebar.copyPayload")); RED.popover.tooltip(copyPayload,RED._("node-red:debug.sidebar.copyPayload"));
if (strippedKey !== undefined && strippedKey !== '') { if (enablePinning && strippedKey !== undefined && strippedKey !== '') {
var isPinned = pinnedPaths[sourceId].hasOwnProperty(strippedKey); var isPinned = pinnedPaths[sourceId].hasOwnProperty(strippedKey);
var pinPath = $('<button class="red-ui-button red-ui-button-small red-ui-debug-msg-tools-pin"><i class="fa fa-map-pin"></i></button>').appendTo(tools).on("click", function(e) { var pinPath = $('<button class="red-ui-button red-ui-button-small red-ui-debug-msg-tools-pin"><i class="fa fa-map-pin"></i></button>').appendTo(tools).on("click", function(e) {
@ -281,13 +281,16 @@ RED.utils = (function() {
} }
} }
} }
function checkExpanded(strippedKey,expandPaths,minRange,maxRange) { function checkExpanded(strippedKey, expandPaths, { minRange, maxRange, expandLeafNodes }) {
if (expandPaths && expandPaths.length > 0) { if (expandPaths && expandPaths.length > 0) {
if (strippedKey === '' && minRange === undefined) { if (strippedKey === '' && minRange === undefined) {
return true; return true;
} }
for (var i=0;i<expandPaths.length;i++) { for (var i=0;i<expandPaths.length;i++) {
var p = expandPaths[i]; var p = expandPaths[i];
if (expandLeafNodes && p === strippedKey) {
return true
}
if (p.indexOf(strippedKey) === 0 && (p[strippedKey.length] === "." || p[strippedKey.length] === "[") ) { if (p.indexOf(strippedKey) === 0 && (p[strippedKey.length] === "." || p[strippedKey.length] === "[") ) {
if (minRange !== undefined && p[strippedKey.length] === "[") { if (minRange !== undefined && p[strippedKey.length] === "[") {
@ -394,6 +397,8 @@ RED.utils = (function() {
var sourceId = options.sourceId; var sourceId = options.sourceId;
var rootPath = options.rootPath; var rootPath = options.rootPath;
var expandPaths = options.expandPaths; var expandPaths = options.expandPaths;
const enablePinning = options.enablePinning
const expandLeafNodes = options.expandLeafNodes;
var ontoggle = options.ontoggle; var ontoggle = options.ontoggle;
var exposeApi = options.exposeApi; var exposeApi = options.exposeApi;
var tools = options.tools; var tools = options.tools;
@ -416,11 +421,11 @@ RED.utils = (function() {
} }
header = $('<span class="red-ui-debug-msg-row"></span>').appendTo(element); header = $('<span class="red-ui-debug-msg-row"></span>').appendTo(element);
if (sourceId) { if (sourceId) {
addMessageControls(header,sourceId,path,obj,rootPath,strippedKey,tools); addMessageControls(header,sourceId,path,obj,rootPath,strippedKey,tools, enablePinning);
} }
if (!key) { if (!key) {
element.addClass("red-ui-debug-msg-top-level"); element.addClass("red-ui-debug-msg-top-level");
if (sourceId) { if (sourceId && !expandPaths) {
var pinned = pinnedPaths[sourceId]; var pinned = pinnedPaths[sourceId];
expandPaths = []; expandPaths = [];
if (pinned) { if (pinned) {
@ -476,7 +481,7 @@ RED.utils = (function() {
$('<span class="red-ui-debug-msg-type-meta red-ui-debug-msg-object-type-header"></span>').text(typeHint||'string').appendTo(header); $('<span class="red-ui-debug-msg-type-meta red-ui-debug-msg-object-type-header"></span>').text(typeHint||'string').appendTo(header);
var row = $('<div class="red-ui-debug-msg-object-entry collapsed"></div>').appendTo(element); var row = $('<div class="red-ui-debug-msg-object-entry collapsed"></div>').appendTo(element);
$('<pre class="red-ui-debug-msg-type-string"></pre>').text(obj).appendTo(row); $('<pre class="red-ui-debug-msg-type-string"></pre>').text(obj).appendTo(row);
},function(state) {if (ontoggle) { ontoggle(path,state);}}, checkExpanded(strippedKey,expandPaths)); },function(state) {if (ontoggle) { ontoggle(path,state);}}, checkExpanded(strippedKey, expandPaths, { expandLeafNodes }));
} }
e = $('<span class="red-ui-debug-msg-type-string red-ui-debug-msg-object-header"></span>').html('"'+formatString(sanitize(obj))+'"').appendTo(entryObj); e = $('<span class="red-ui-debug-msg-type-string red-ui-debug-msg-object-header"></span>').html('"'+formatString(sanitize(obj))+'"').appendTo(entryObj);
if (/^#[0-9a-f]{6}$/i.test(obj)) { if (/^#[0-9a-f]{6}$/i.test(obj)) {
@ -592,14 +597,16 @@ RED.utils = (function() {
typeHint: type==='buffer'?'hex':false, typeHint: type==='buffer'?'hex':false,
hideKey: false, hideKey: false,
path: path+"["+i+"]", path: path+"["+i+"]",
sourceId: sourceId, sourceId,
rootPath: rootPath, rootPath,
expandPaths: expandPaths, expandPaths,
ontoggle: ontoggle, expandLeafNodes,
exposeApi: exposeApi, ontoggle,
exposeApi,
// tools: tools // Do not pass tools down as we // tools: tools // Do not pass tools down as we
// keep them attached to the top-level header // keep them attached to the top-level header
nodeSelector: options.nodeSelector, nodeSelector: options.nodeSelector,
enablePinning
} }
).appendTo(row); ).appendTo(row);
} }
@ -623,21 +630,23 @@ RED.utils = (function() {
typeHint: type==='buffer'?'hex':false, typeHint: type==='buffer'?'hex':false,
hideKey: false, hideKey: false,
path: path+"["+i+"]", path: path+"["+i+"]",
sourceId: sourceId, sourceId,
rootPath: rootPath, rootPath,
expandPaths: expandPaths, expandPaths,
ontoggle: ontoggle, expandLeafNodes,
exposeApi: exposeApi, ontoggle,
exposeApi,
// tools: tools // Do not pass tools down as we // tools: tools // Do not pass tools down as we
// keep them attached to the top-level header // keep them attached to the top-level header
nodeSelector: options.nodeSelector, nodeSelector: options.nodeSelector,
enablePinning
} }
).appendTo(row); ).appendTo(row);
} }
} }
})(), })(),
(function() { var path = path+"["+i+"]"; return function(state) {if (ontoggle) { ontoggle(path,state);}}})(), (function() { var path = path+"["+i+"]"; return function(state) {if (ontoggle) { ontoggle(path,state);}}})(),
checkExpanded(strippedKey,expandPaths,minRange,Math.min(fullLength-1,(minRange+9)))); checkExpanded(strippedKey,expandPaths,{ minRange, maxRange: Math.min(fullLength-1,(minRange+9)), expandLeafNodes}));
$('<span class="red-ui-debug-msg-object-key"></span>').html("["+minRange+" &hellip; "+Math.min(fullLength-1,(minRange+9))+"]").appendTo(header); $('<span class="red-ui-debug-msg-object-key"></span>').html("["+minRange+" &hellip; "+Math.min(fullLength-1,(minRange+9))+"]").appendTo(header);
} }
if (fullLength < originalLength) { if (fullLength < originalLength) {
@ -646,7 +655,7 @@ RED.utils = (function() {
} }
}, },
function(state) {if (ontoggle) { ontoggle(path,state);}}, function(state) {if (ontoggle) { ontoggle(path,state);}},
checkExpanded(strippedKey,expandPaths)); checkExpanded(strippedKey, expandPaths, { expandLeafNodes }));
} }
} else if (typeof obj === 'object') { } else if (typeof obj === 'object') {
element.addClass('collapsed'); element.addClass('collapsed');
@ -680,14 +689,16 @@ RED.utils = (function() {
typeHint: false, typeHint: false,
hideKey: false, hideKey: false,
path: newPath, path: newPath,
sourceId: sourceId, sourceId,
rootPath: rootPath, rootPath,
expandPaths: expandPaths, expandPaths,
ontoggle: ontoggle, expandLeafNodes,
exposeApi: exposeApi, ontoggle,
exposeApi,
// tools: tools // Do not pass tools down as we // tools: tools // Do not pass tools down as we
// keep them attached to the top-level header // keep them attached to the top-level header
nodeSelector: options.nodeSelector, nodeSelector: options.nodeSelector,
enablePinning
} }
).appendTo(row); ).appendTo(row);
} }
@ -696,7 +707,7 @@ RED.utils = (function() {
} }
}, },
function(state) {if (ontoggle) { ontoggle(path,state);}}, function(state) {if (ontoggle) { ontoggle(path,state);}},
checkExpanded(strippedKey,expandPaths)); checkExpanded(strippedKey, expandPaths, { expandLeafNodes }));
} }
if (key) { if (key) {
$('<span class="red-ui-debug-msg-type-meta"></span>').text(type).appendTo(entryObj); $('<span class="red-ui-debug-msg-type-meta"></span>').text(type).appendTo(entryObj);

View File

@ -11,7 +11,7 @@ RED.view.annotations = (function() {
} }
let badgeRDX = 0; let badgeRDX = 0;
let badgeLDX = 0; let badgeLDX = 0;
const scale = RED.view.scale()
for (let i=0,l=evt.el.__annotations__.length;i<l;i++) { for (let i=0,l=evt.el.__annotations__.length;i<l;i++) {
const annotation = evt.el.__annotations__[i]; const annotation = evt.el.__annotations__[i];
if (annotations.hasOwnProperty(annotation.id)) { if (annotations.hasOwnProperty(annotation.id)) {
@ -42,15 +42,17 @@ RED.view.annotations = (function() {
} }
if (isBadge) { if (isBadge) {
if (showAnnotation) { if (showAnnotation) {
const rect = annotation.element.getBoundingClientRect(); // getBoundingClientRect is in real-world scale so needs to be adjusted according to
// the current scale factor
const rectWidth = annotation.element.getBoundingClientRect().width / scale;
let annotationX let annotationX
if (!opts.align || opts.align === 'right') { if (!opts.align || opts.align === 'right') {
annotationX = evt.node.w - 3 - badgeRDX - rect.width annotationX = evt.node.w - 3 - badgeRDX - rectWidth
badgeRDX += rect.width + 4; badgeRDX += rectWidth + 4;
} else if (opts.align === 'left') { } else if (opts.align === 'left') {
annotationX = 3 + badgeLDX annotationX = 3 + badgeLDX
badgeLDX += rect.width + 4; badgeLDX += rectWidth + 4;
} }
annotation.element.setAttribute("transform", "translate("+annotationX+", -8)"); annotation.element.setAttribute("transform", "translate("+annotationX+", -8)");
} }

View File

@ -1102,18 +1102,27 @@ RED.view.tools = (function() {
const paletteLabel = RED.utils.getPaletteLabel(n.type, nodeDef) const paletteLabel = RED.utils.getPaletteLabel(n.type, nodeDef)
const defaultNodeNameRE = new RegExp('^'+paletteLabel.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')+' (\\d+)$') const defaultNodeNameRE = new RegExp('^'+paletteLabel.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')+' (\\d+)$')
if (!typeIndex.hasOwnProperty(n.type)) { if (!typeIndex.hasOwnProperty(n.type)) {
const existingNodes = RED.nodes.filterNodes({type: n.type}) const existingNodes = RED.nodes.filterNodes({ type: n.type });
let maxNameNumber = 0; const existingIds = existingNodes.reduce((ids, node) => {
existingNodes.forEach(n => { let match = defaultNodeNameRE.exec(node.name);
let match = defaultNodeNameRE.exec(n.name)
if (match) { if (match) {
let nodeNumber = parseInt(match[1]) const nodeNumber = parseInt(match[1], 10);
if (nodeNumber > maxNameNumber) { if (!ids.includes(nodeNumber)) {
maxNameNumber = nodeNumber ids.push(nodeNumber);
} }
} }
}) return ids;
typeIndex[n.type] = maxNameNumber + 1 }, []).sort((a, b) => a - b);
let availableNameNumber = 1;
for (let i = 0; i < existingIds.length; i++) {
if (existingIds[i] !== availableNameNumber) {
break;
}
availableNameNumber++;
}
typeIndex[n.type] = availableNameNumber;
} }
if ((options.renameBlank && n.name === '') || (options.renameClash && defaultNodeNameRE.test(n.name))) { if ((options.renameBlank && n.name === '') || (options.renameClash && defaultNodeNameRE.test(n.name))) {
if (generateHistory) { if (generateHistory) {
@ -1145,11 +1154,11 @@ RED.view.tools = (function() {
} }
} }
function addJunctionsToWires(wires) { function addJunctionsToWires(options = {}) {
if (RED.workspaces.isLocked()) { if (RED.workspaces.isLocked()) {
return return
} }
let wiresToSplit = wires || (RED.view.selection().links && RED.view.selection().links.filter(e => !e.link)); let wiresToSplit = options.wires || (RED.view.selection().links && RED.view.selection().links.filter(e => !e.link));
if (!wiresToSplit) { if (!wiresToSplit) {
return return
} }
@ -1197,6 +1206,10 @@ RED.view.tools = (function() {
if (links.length === 0) { if (links.length === 0) {
return return
} }
if (addedJunctions.length === 0 && Object.hasOwn(options, 'x') && Object.hasOwn(options, 'y')) {
junction.x = options.x
junction.y = options.y
} else {
let pointCount = 0 let pointCount = 0
links.forEach(function(l) { links.forEach(function(l) {
if (l._sliceLocation) { if (l._sliceLocation) {
@ -1212,6 +1225,7 @@ RED.view.tools = (function() {
}) })
junction.x = Math.round(junction.x/pointCount) junction.x = Math.round(junction.x/pointCount)
junction.y = Math.round(junction.y/pointCount) junction.y = Math.round(junction.y/pointCount)
}
if (RED.view.snapGrid) { if (RED.view.snapGrid) {
let gridSize = RED.view.gridSize() let gridSize = RED.view.gridSize()
junction.x = (gridSize*Math.round(junction.x/gridSize)); junction.x = (gridSize*Math.round(junction.x/gridSize));
@ -1401,7 +1415,7 @@ RED.view.tools = (function() {
RED.actions.add("core:wire-multiple-to-node", function() { wireMultipleToNode() }) RED.actions.add("core:wire-multiple-to-node", function() { wireMultipleToNode() })
RED.actions.add("core:split-wire-with-link-nodes", function () { splitWiresWithLinkNodes() }); RED.actions.add("core:split-wire-with-link-nodes", function () { splitWiresWithLinkNodes() });
RED.actions.add("core:split-wires-with-junctions", function () { addJunctionsToWires() }); RED.actions.add("core:split-wires-with-junctions", function (options) { addJunctionsToWires(options) });
RED.actions.add("core:generate-node-names", generateNodeNames ) RED.actions.add("core:generate-node-names", generateNodeNames )

View File

@ -288,7 +288,7 @@ RED.view = (function() {
} }
selectedLinks.clearUnselected() selectedLinks.clearUnselected()
}, },
length: () => groups.length, length: () => groups.size,
forEach: (func) => { groups.forEach(func) }, forEach: (func) => { groups.forEach(func) },
toArray: () => [...groups], toArray: () => [...groups],
clear: function () { clear: function () {
@ -321,8 +321,8 @@ RED.view = (function() {
evt.stopPropagation() evt.stopPropagation()
RED.contextMenu.show({ RED.contextMenu.show({
type: 'workspace', type: 'workspace',
x:evt.clientX-5, x: evt.clientX,
y:evt.clientY-5 y: evt.clientY
}) })
return false return false
}) })
@ -1265,11 +1265,6 @@ RED.view = (function() {
var targetGroup = options.group; var targetGroup = options.group;
var touchTrigger = options.touchTrigger; var touchTrigger = options.touchTrigger;
if (targetGroup) {
selectedGroups.add(targetGroup,false);
RED.view.redraw();
}
// `point` is the place in the workspace the mouse has clicked. // `point` is the place in the workspace the mouse has clicked.
// This takes into account scrolling and scaling of the workspace. // This takes into account scrolling and scaling of the workspace.
var ox = point[0]; var ox = point[0];
@ -1591,9 +1586,6 @@ RED.view = (function() {
// auto select dropped node - so info shows (if visible) // auto select dropped node - so info shows (if visible)
clearSelection(); clearSelection();
nn.selected = true; nn.selected = true;
if (targetGroup) {
selectedGroups.add(targetGroup,false);
}
movingSet.add(nn); movingSet.add(nn);
updateActiveNodes(); updateActiveNodes();
updateSelection(); updateSelection();
@ -2178,17 +2170,22 @@ RED.view = (function() {
n.n.moved = true; n.n.moved = true;
} }
} }
// If a node has moved and ends up being spliced into a link, keep
// Check to see if we need to splice a link // track of which historyEvent to add the splice info to
let targetSpliceEvent = null
if (moveEvent.nodes.length > 0) { if (moveEvent.nodes.length > 0) {
historyEvent.events.push(moveEvent) historyEvent.events.push(moveEvent)
if (activeSpliceLink) { targetSpliceEvent = moveEvent
var linkToSplice = d3.select(activeSpliceLink).data()[0];
spliceLink(linkToSplice, movingSet.get(0).n, moveEvent)
}
} }
if (moveAndChangedGroupEvent.nodes.length > 0) { if (moveAndChangedGroupEvent.nodes.length > 0) {
historyEvent.events.push(moveAndChangedGroupEvent) historyEvent.events.push(moveAndChangedGroupEvent)
targetSpliceEvent = moveAndChangedGroupEvent
}
// activeSpliceLink will only be set if the movingSet has a single
// node that is able to splice.
if (targetSpliceEvent && activeSpliceLink) {
var linkToSplice = d3.select(activeSpliceLink).data()[0];
spliceLink(linkToSplice, movingSet.get(0).n, targetSpliceEvent)
} }
// Only continue if something has moved // Only continue if something has moved
@ -2689,22 +2686,21 @@ RED.view = (function() {
addToRemovedLinks(reconnectResult.removedLinks) addToRemovedLinks(reconnectResult.removedLinks)
} }
var startDirty = RED.nodes.dirty(); const startDirty = RED.nodes.dirty();
var startChanged = false; let movingSelectedGroups = [];
var selectedGroups = [];
if (movingSet.length() > 0) { if (movingSet.length() > 0) {
for (var i=0;i<movingSet.length();i++) { for (var i=0;i<movingSet.length();i++) {
node = movingSet.get(i).n; node = movingSet.get(i).n;
if (node.type === "group") { if (node.type === "group") {
selectedGroups.push(node); movingSelectedGroups.push(node);
} }
} }
// Make sure we have identified all groups about to be deleted // Make sure we have identified all groups about to be deleted
for (i=0;i<selectedGroups.length;i++) { for (i=0;i<movingSelectedGroups.length;i++) {
selectedGroups[i].nodes.forEach(function(n) { movingSelectedGroups[i].nodes.forEach(function(n) {
if (n.type === "group" && selectedGroups.indexOf(n) === -1) { if (n.type === "group" && movingSelectedGroups.indexOf(n) === -1) {
selectedGroups.push(n); movingSelectedGroups.push(n);
} }
}) })
} }
@ -2721,7 +2717,7 @@ RED.view = (function() {
addToRemovedLinks(removedEntities.links); addToRemovedLinks(removedEntities.links);
if (node.g) { if (node.g) {
var group = RED.nodes.group(node.g); var group = RED.nodes.group(node.g);
if (selectedGroups.indexOf(group) === -1) { if (movingSelectedGroups.indexOf(group) === -1) {
// Don't use RED.group.removeFromGroup as that emits // Don't use RED.group.removeFromGroup as that emits
// a change event on the node - but we're deleting it // a change event on the node - but we're deleting it
var index = group.nodes.indexOf(node); var index = group.nodes.indexOf(node);
@ -2735,7 +2731,7 @@ RED.view = (function() {
removedLinks = removedLinks.concat(result.links); removedLinks = removedLinks.concat(result.links);
if (node.g) { if (node.g) {
var group = RED.nodes.group(node.g); var group = RED.nodes.group(node.g);
if (selectedGroups.indexOf(group) === -1) { if (movingSelectedGroups.indexOf(group) === -1) {
// Don't use RED.group.removeFromGroup as that emits // Don't use RED.group.removeFromGroup as that emits
// a change event on the node - but we're deleting it // a change event on the node - but we're deleting it
var index = group.nodes.indexOf(node); var index = group.nodes.indexOf(node);
@ -2757,8 +2753,8 @@ RED.view = (function() {
// Groups must be removed in the right order - from inner-most // Groups must be removed in the right order - from inner-most
// to outermost. // to outermost.
for (i = selectedGroups.length-1; i>=0; i--) { for (i = movingSelectedGroups.length-1; i>=0; i--) {
var g = selectedGroups[i]; var g = movingSelectedGroups[i];
removedGroups.push(g); removedGroups.push(g);
RED.nodes.removeGroup(g); RED.nodes.removeGroup(g);
} }
@ -5175,8 +5171,8 @@ RED.view = (function() {
var delta = Infinity; var delta = Infinity;
for (var i = 0; i < lineLength; i++) { for (var i = 0; i < lineLength; i++) {
var linePos = pathLine.getPointAtLength(i); var linePos = pathLine.getPointAtLength(i);
var posDeltaX = Math.abs(linePos.x-d3.event.offsetX) var posDeltaX = Math.abs(linePos.x-(d3.event.offsetX / scaleFactor))
var posDeltaY = Math.abs(linePos.y-d3.event.offsetY) var posDeltaY = Math.abs(linePos.y-(d3.event.offsetY / scaleFactor))
var posDelta = posDeltaX*posDeltaX + posDeltaY*posDeltaY var posDelta = posDeltaX*posDeltaX + posDeltaY*posDeltaY
if (posDelta < delta) { if (posDelta < delta) {
pos = linePos pos = linePos

View File

@ -168,6 +168,37 @@ RED.user = (function() {
} }
} else {
if (data.prompts) {
if (data.loginMessage) {
const sessionMessages = $("<div/>",{class:"form-row",style:"text-align: center"}).appendTo("#node-dialog-login-fields");
$('<div>').text(data.loginMessage).appendTo(sessionMessages);
}
i = 0;
for (;i<data.prompts.length;i++) {
var field = data.prompts[i];
var row = $("<div/>",{class:"form-row",style:"text-align: center"}).appendTo("#node-dialog-login-fields");
var loginButton = $('<a href="#" class="red-ui-button"></a>',{style: "padding: 10px"}).appendTo(row).on("click", function() {
document.location = field.url;
});
if (field.image) {
$("<img>",{src:field.image}).appendTo(loginButton);
} else if (field.label) {
var label = $('<span></span>').text(field.label);
if (field.icon) {
$('<i></i>',{class: "fa fa-2x "+field.icon, style:"vertical-align: middle"}).appendTo(loginButton);
label.css({
"verticalAlign":"middle",
"marginLeft":"8px"
});
}
label.appendTo(loginButton);
}
loginButton.button();
}
}
} }
if (opts.cancelable) { if (opts.cancelable) {
$("#node-dialog-login-cancel").button().on("click", function( event ) { $("#node-dialog-login-cancel").button().on("click", function( event ) {
@ -320,10 +351,10 @@ RED.user = (function() {
userIcon.css({ userIcon.css({
backgroundImage: "url("+user.image+")", backgroundImage: "url("+user.image+")",
}) })
} else if (user.anonymous) { } else if (user.anonymous || (!user.username && !user.email)) {
$('<i class="fa fa-user"></i>').appendTo(userIcon); $('<i class="fa fa-user"></i>').appendTo(userIcon);
} else { } else {
$('<span>').text(user.username.substring(0,2)).appendTo(userIcon); $('<span>').text((user.username || user.email).substring(0,2)).appendTo(userIcon);
} }
if (user.profileColor !== undefined) { if (user.profileColor !== undefined) {
userIcon.addClass('red-ui-user-profile-color-' + user.profileColor) userIcon.addClass('red-ui-user-profile-color-' + user.profileColor)

View File

@ -84,6 +84,11 @@ ul.red-ui-sidebar-node-config-list {
background: var(--red-ui-node-config-background); background: var(--red-ui-node-config-background);
color: var(--red-ui-primary-text-color); color: var(--red-ui-primary-text-color);
cursor: pointer; cursor: pointer;
&.red-ui-palette-node-config-invalid.red-ui-palette-node-config-changed {
.red-ui-palette-node-annotations.red-ui-flow-node-error {
left: calc(100% - 28px);
}
}
} }
ul.red-ui-sidebar-node-config-list li.red-ui-palette-node-config-type { ul.red-ui-sidebar-node-config-list li.red-ui-palette-node-config-type {
color: var(--red-ui-secondary-text-color); color: var(--red-ui-secondary-text-color);
@ -115,6 +120,15 @@ ul.red-ui-sidebar-node-config-list li.red-ui-palette-node-config-type {
.red-ui-palette-node-config-invalid { .red-ui-palette-node-config-invalid {
border-color: var(--red-ui-form-input-border-error-color) border-color: var(--red-ui-form-input-border-error-color)
} }
.red-ui-sidebar-config-tray-header.red-ui-palette-header:not(.red-ui-sidebar-config-changed) .red-ui-flow-node-changed {
display: none;
}
.red-ui-sidebar-config-tray-header.red-ui-palette-header.red-ui-sidebar-config-changed .red-ui-flow-node-changed {
display: inline-block;
position: absolute;
top: 1px;
right: 1px;
}
.red-ui-palette-node-annotations { .red-ui-palette-node-annotations {
position: absolute; position: absolute;
left: calc(100% - 15px); left: calc(100% - 15px);

View File

@ -2,4 +2,15 @@
&.red-ui-popover-panel { &.red-ui-popover-panel {
border-top: none; border-top: none;
} }
}
.red-ui-autoComplete-completion {
font-family: var(--red-ui-monospace-font);
white-space: nowrap;
overflow: hidden;
flex-grow: 1;
text-overflow: ellipsis;
direction: rtl;
text-align: left;
} }

View File

@ -148,7 +148,7 @@ module.exports = function(RED) {
var st = (typeof output === 'string') ? output : util.inspect(output); var st = (typeof output === 'string') ? output : util.inspect(output);
var fill = "grey"; var fill = "grey";
var shape = "dot"; var shape = "dot";
if (typeof output === 'object' && hasOwnProperty.call(output, "fill") && hasOwnProperty.call(output, "shape") && hasOwnProperty.call(output, "text")) { if (typeof output === 'object' && output?.fill && output?.shape && output?.text) {
fill = output.fill; fill = output.fill;
shape = output.shape; shape = output.shape;
st = output.text; st = output.text;

View File

@ -511,9 +511,10 @@ RED.debug = (function() {
typeHint: format, typeHint: format,
hideKey: false, hideKey: false,
path: path, path: path,
sourceId: sourceNode&&sourceNode.id, sourceId: sourceNode && sourceNode.id,
rootPath: path, rootPath: path,
nodeSelector: config.messageSourceClick, nodeSelector: config.messageSourceClick,
enablePinning: true
}); });
// Do this in a separate step so the element functions aren't stripped // Do this in a separate step so the element functions aren't stripped
debugMessage.appendTo(el); debugMessage.appendTo(el);

View File

@ -111,8 +111,6 @@ module.exports = function(RED) {
throw new Error(RED._("function.error.externalModuleNotAllowed")); throw new Error(RED._("function.error.externalModuleNotAllowed"));
} }
var functionText = "var results = null;"+ var functionText = "var results = null;"+
"results = (async function(msg,__send__,__done__){ "+ "results = (async function(msg,__send__,__done__){ "+
"var __msgid__ = msg._msgid;"+ "var __msgid__ = msg._msgid;"+
@ -166,7 +164,13 @@ module.exports = function(RED) {
Buffer:Buffer, Buffer:Buffer,
Date: Date, Date: Date,
RED: { RED: {
util: RED.util util: {
...RED.util,
getSetting: function (_node, name, _flow) {
// Ensure `node` argument is the Function node and do not allow flow to be overridden.
return RED.util.getSetting(node, name);
}
}
}, },
__node__: { __node__: {
id: node.id, id: node.id,

View File

@ -352,7 +352,9 @@ module.exports = function(RED) {
if (msgs.length === 0) { if (msgs.length === 0) {
done() done()
} else { } else {
setImmediate(() => {
drainMessageGroup(msgs,count,done); drainMessageGroup(msgs,count,done);
})
} }
} }
}) })
@ -505,7 +507,9 @@ module.exports = function(RED) {
if (err) { if (err) {
node.error(err,nextMsg); node.error(err,nextMsg);
} }
setImmediate(() => {
processMessageQueue() processMessageQueue()
})
}); });
} }

View File

@ -253,7 +253,13 @@ module.exports = function(RED) {
if (node.allowrate && m.hasOwnProperty("rate") && !isNaN(parseFloat(m.rate))) { if (node.allowrate && m.hasOwnProperty("rate") && !isNaN(parseFloat(m.rate))) {
node.rate = m.rate; node.rate = m.rate;
} }
send(m); if (msg.hasOwnProperty("reset")) {
if (msg.hasOwnProperty("flush")) {
node.buffer.push({msg: m, send: send, done: done});
}
}
else { send(m); }
node.reportDepth(); node.reportDepth();
node.intervalID = setInterval(sendMsgFromBuffer, node.rate); node.intervalID = setInterval(sendMsgFromBuffer, node.rate);
done(); done();
@ -285,25 +291,6 @@ module.exports = function(RED) {
} }
} }
else if (!msg.hasOwnProperty("reset")) { else if (!msg.hasOwnProperty("reset")) {
if (maxKeptMsgsCount(node) > 0) {
if (node.intervalID === -1) {
node.send(msg);
node.intervalID = setInterval(sendMsgFromBuffer, node.rate);
} else {
if (node.allowrate && msg.hasOwnProperty("rate") && !isNaN(parseFloat(msg.rate)) && node.rate !== msg.rate) {
node.rate = msg.rate;
clearInterval(node.intervalID);
node.intervalID = setInterval(sendMsgFromBuffer, node.rate);
}
if (node.buffer.length < _maxKeptMsgsCount) {
var m = RED.util.cloneMessage(msg);
node.buffer.push({msg: m, send: send, done: done});
} else {
node.trace("dropped due to buffer overflow. msg._msgid = " + msg._msgid);
node.droppedMsgs++;
}
}
} else {
if (node.allowrate && msg.hasOwnProperty("rate") && !isNaN(parseFloat(msg.rate))) { if (node.allowrate && msg.hasOwnProperty("rate") && !isNaN(parseFloat(msg.rate))) {
node.rate = msg.rate; node.rate = msg.rate;
} }
@ -318,9 +305,9 @@ module.exports = function(RED) {
else if ( ( (timeSinceLast[0] * SECONDS_TO_NANOS) + timeSinceLast[1] ) > (node.rate * MILLIS_TO_NANOS) ) { else if ( ( (timeSinceLast[0] * SECONDS_TO_NANOS) + timeSinceLast[1] ) > (node.rate * MILLIS_TO_NANOS) ) {
node.lastSent = process.hrtime(); node.lastSent = process.hrtime();
send(msg); send(msg);
} else if (node.outputs === 2) {
send([null,msg])
} }
else if (node.outputs === 2) {
send([null,msg])
} }
done(); done();
} }

View File

@ -24,6 +24,14 @@ module.exports = function(RED) {
this.op2 = n.op2 || "0"; this.op2 = n.op2 || "0";
this.op1type = n.op1type || "str"; this.op1type = n.op1type || "str";
this.op2type = n.op2type || "str"; this.op2type = n.op2type || "str";
// If the op1/2type is 'date', then we need to leave op1/2 alone so that
// evaluateNodeProperty works as expected.
if (this.op1type === 'date' && this.op1 === '1') {
this.op1 = ''
}
if (this.op2type === 'date' && this.op2 === '0') {
this.op2 = ''
}
this.second = (n.outputs == 2) ? true : false; this.second = (n.outputs == 2) ? true : false;
this.topic = n.topic || "topic"; this.topic = n.topic || "topic";
@ -193,7 +201,7 @@ module.exports = function(RED) {
if (node.op2type !== "nul") { if (node.op2type !== "nul") {
var promise = Promise.resolve(); var promise = Promise.resolve();
msg2 = RED.util.cloneMessage(msg); msg2 = RED.util.cloneMessage(msg);
if (node.op2type === "flow" || node.op2type === "global") { if (node.op2type === "flow" || node.op2type === "global" || node.op2type === "date") {
promise = new Promise((resolve,reject) => { promise = new Promise((resolve,reject) => {
RED.util.evaluateNodeProperty(node.op2,node.op2type,node,msg,(err,value) => { RED.util.evaluateNodeProperty(node.op2,node.op2type,node,msg,(err,value) => {
if (err) { if (err) {
@ -213,7 +221,6 @@ module.exports = function(RED) {
} }
else { else {
msg2.payload = node.topics[topic].m2; msg2.payload = node.topics[topic].m2;
if (node.op2type === "date") { msg2.payload = Date.now(); }
if (node.second === true) { msgInfo.send([null,msg2]); } if (node.second === true) { msgInfo.send([null,msg2]); }
else { msgInfo.send(msg2); } else { msgInfo.send(msg2); }
} }

View File

@ -367,20 +367,21 @@ module.exports = function(RED) {
const sendHeadersAlways = node.hdrout === "all" const sendHeadersAlways = node.hdrout === "all"
const sendHeaders = !dontSendHeaders && (sendHeadersOnce || sendHeadersAlways) const sendHeaders = !dontSendHeaders && (sendHeadersOnce || sendHeadersAlways)
const quoteables = [node.sep, node.quo, "\n", "\r"] const quoteables = [node.sep, node.quo, "\n", "\r"]
const templateQuoteables = [',', '"', "\n", "\r"] const templateQuoteables = [node.sep, node.quo, "\n", "\r"]
const templateQuoteablesStrict = [',', '"', "\n", "\r"]
let badTemplateWarnOnce = true let badTemplateWarnOnce = true
const columnStringToTemplateArray = function (col, sep) { const columnStringToTemplateArray = function (col, sep) {
// NOTE: enforce strict column template parsing in RFC4180 mode // NOTE: enforce strict column template parsing in RFC4180 mode
const parsed = csv.parse(col, { separator: sep, quote: node.quo, outputStyle: 'array', strict: true }) const parsed = csv.parse(col, { separator: sep, quote: node.quo, outputStyle: 'array', strict: true })
if (parsed.headers.length > 0) { node.goodtmpl = true } else { node.goodtmpl = false } if (parsed.data?.length === 1) { node.goodtmpl = true } else { node.goodtmpl = false }
return parsed.headers.length ? parsed.headers : null return node.goodtmpl ? parsed.data[0] : null
} }
const templateArrayToColumnString = function (template, keepEmptyColumns) { const templateArrayToColumnString = function (template, keepEmptyColumns, separator = ',', quotables = templateQuoteablesStrict) {
// NOTE: enforce strict column template parsing in RFC4180 mode // NOTE: defaults to strict column template parsing (commas and double quotes)
const parsed = csv.parse('', {headers: template, headersOnly:true, separator: ',', quote: node.quo, outputStyle: 'array', strict: true }) const parsed = csv.parse('', {headers: template, headersOnly:true, separator, quote: node.quo, outputStyle: 'array', strict: true })
return keepEmptyColumns return keepEmptyColumns
? parsed.headers.map(e => addQuotes(e || '', { separator: ',', quoteables: templateQuoteables})) ? parsed.headers.map(e => addQuotes(e || '', { separator, quoteables: quotables })).join(separator)
: parsed.header // exclues empty columns : parsed.header // exclues empty columns
// TODO: resolve inconsistency between CSV->JSON and JSON->CSV // TODO: resolve inconsistency between CSV->JSON and JSON->CSV
// CSV->JSON: empty columns are excluded // CSV->JSON: empty columns are excluded
@ -447,7 +448,7 @@ module.exports = function(RED) {
template = Object.keys(inputData[0]) || [''] template = Object.keys(inputData[0]) || ['']
} }
} }
stringBuilder.push(templateArrayToColumnString(template, true)) stringBuilder.push(templateArrayToColumnString(template, true, node.sep, templateQuoteables)) // use user set separator for output data.
if (sendHeadersOnce) { node.hdrSent = true } if (sendHeadersOnce) { node.hdrSent = true }
} }
@ -483,6 +484,7 @@ module.exports = function(RED) {
node.warn(RED._("csv.errors.obj_csv")) node.warn(RED._("csv.errors.obj_csv"))
badTemplateWarnOnce = false badTemplateWarnOnce = false
} }
template = Object.keys(row) || ['']
const rowData = [] const rowData = []
for (let header in inputData[0]) { for (let header in inputData[0]) {
if (row.hasOwnProperty(header)) { if (row.hasOwnProperty(header)) {
@ -518,7 +520,7 @@ module.exports = function(RED) {
// join lines, don't forget to add the last new line // join lines, don't forget to add the last new line
msg.payload = stringBuilder.join(node.ret) + node.ret msg.payload = stringBuilder.join(node.ret) + node.ret
msg.columns = templateArrayToColumnString(template) msg.columns = templateArrayToColumnString(template) // always strict commas + double quotes for
if (msg.payload !== '') { send(msg) } if (msg.payload !== '') { send(msg) }
done() done()
} }
@ -615,16 +617,15 @@ module.exports = function(RED) {
} }
if (msg.parts.index + 1 === msg.parts.count) { if (msg.parts.index + 1 === msg.parts.count) {
msg.payload = node.store msg.payload = node.store
msg.columns = csvParseResult.header // msg.columns = csvParseResult.header
// msg._mode = 'RFC4180 mode' msg.columns = templateArrayToColumnString(csvParseResult.headers) // always strict commas + double quotes for msg.columns
delete msg.parts delete msg.parts
send(msg) send(msg)
node.store = [] node.store = []
} }
} }
else { else {
msg.columns = csvParseResult.header msg.columns = templateArrayToColumnString(csvParseResult.headers) // always strict commas + double quotes for msg.columns
// msg._mode = 'RFC4180 mode'
msg.payload = data msg.payload = data
send(msg); // finally send the array send(msg); // finally send the array
} }
@ -633,7 +634,8 @@ module.exports = function(RED) {
const len = data.length const len = data.length
for (let row = 0; row < len; row++) { for (let row = 0; row < len; row++) {
const newMessage = RED.util.cloneMessage(msg) const newMessage = RED.util.cloneMessage(msg)
newMessage.columns = csvParseResult.header // newMessage.columns = csvParseResult.header
newMessage.columns = templateArrayToColumnString(csvParseResult.headers) // always strict commas + double quotes for msg.columns
newMessage.payload = data[row] newMessage.payload = data[row]
if (!has_parts) { if (!has_parts) {
newMessage.parts = { newMessage.parts = {

View File

@ -339,7 +339,7 @@ module.exports = function(RED) {
} }
else { else {
msg.filename = filename; msg.filename = filename;
var lines = Buffer.from([]); const bufferArray = [];
var spare = ""; var spare = "";
var count = 0; var count = 0;
var type = "buffer"; var type = "buffer";
@ -397,7 +397,7 @@ module.exports = function(RED) {
} }
} }
else { else {
lines = Buffer.concat([lines,chunk]); bufferArray.push(chunk);
} }
} }
}) })
@ -413,10 +413,11 @@ module.exports = function(RED) {
}) })
.on('end', function() { .on('end', function() {
if (node.chunk === false) { if (node.chunk === false) {
const buffer = Buffer.concat(bufferArray);
if (node.format === "utf8") { if (node.format === "utf8") {
msg.payload = decode(lines, node.encoding); msg.payload = decode(buffer, node.encoding);
} }
else { msg.payload = lines; } else { msg.payload = buffer; }
nodeSend(msg); nodeSend(msg);
} }
else if (node.format === "lines") { else if (node.format === "lines") {

View File

@ -40,9 +40,35 @@
</dl> </dl>
<h3>Details</h3> <h3>Details</h3>
<p>Das abonnierte Topic darf MQTT-Platzhalterzeichen (wildcards) enthalten (+ für eine Ebene und # für mehrere Ebenen).</p> <p>Das abonnierte Topic darf MQTT-Platzhalterzeichen (wildcards) enthalten (+ für eine Ebene und # für mehrere Ebenen).</p>
<p>Dieser Node erfordert eine Verbindung zu einem MQTT-Broker, der über die Auswahlliste selektiert werden kann. <p>Diese Node erfordert eine Verbindung zu einem MQTT-Broker, der über die Auswahlliste selektiert werden kann. Eine neue Verbindung wird durch Klicken auf das Stiftsymbol erstellt.</p>
Eine neue Verbindung wird durch Klicken auf das Stiftsymbol erstellt.</p>
<p>Mehrere MQTT-Nodes (in oder out) können bei Bedarf dieselbe Broker-Verbindung nutzen.</p> <p>Mehrere MQTT-Nodes (in oder out) können bei Bedarf dieselbe Broker-Verbindung nutzen.</p>
<h4>Dynamische Steuerung</h4>
Die von der Node genutzte Verbindung kann dynamisch gesteuert werden, wenn die MQTT-Node eine der folgenden Nachrichten erhält. Die Payload dieser Nachrichten werden nicht veröffentlicht.
<h4>Eingangsdaten</h4>
<p>Nur Verfügbar, wenn die Node für dynamische Abonnements konfiguriert wurde.</p>
<dl class="message-properties">
<dt>action <span class="property-type">string</span></dt>
<dd>Der Name der Aktion, die die MQTT-Node ausführen soll. Verfügbare Aktionen sind: <code>"connect"</code>, <code>"disconnect"</code>, <code>"getSubscriptions"</code>, <code>"subscribe"</code> und <code>"unsubscribe"</code>.</dd>
<dt class="optional">topic <span class="property-type">string|object|array</span></dt>
<dd>Bei den Aktionen <code>"subscribe"</code> und <code>"unsubscribe"</code> gibt diese Eigenschaft die MQTT-Topic an. Dabei kann es sich um Folgendes handeln:
<ul>
<li>eine Zeichenfolge, die den Topic-Filter enthält</li>
<li>ein Objekt mit den Eigenschaften <code>topic</code> und <code>qos</code></li>
<li>ein Array aus Zeichenfolgen oder Objekten, um mehrere Topics gleichzeitig zu verwalten</li>
</ul>
</dd>
<dt class="optional">broker <span class="property-type">broker</span> </dt>
<dd>Für die Aktion <code>"connect"</code> kann diese Eigenschaft jede der einzelnen Broker-Konfigurationseinstellungen überschreiben, einschließlich: <ul>
<li><code>broker</code></li>
<li><code>port</code></li>
<li><code>url</code> - überschreibt Broker/Port, um eine vollständige Verbindungs-URL bereitzustellen</li>
<li><code>username</code></li>
<li><code>password</code></li>
</ul>
<p>Wenn diese Eigenschaft gesetzt ist und der Broker bereits verbunden ist, wird ein Fehler protokolliert, es sei denn, die Eigenschaft <code>force</code> gesetzt - in diesem Fall wird die Verbindung zum Broker getrennt, die neuen Einstellungen angewendet und erneut verbunden.</p>
</dd>
</dl>
</script> </script>
<script type="text/html" data-help-name="mqtt out"> <script type="text/html" data-help-name="mqtt out">

View File

@ -48,7 +48,8 @@
<dl class="message-properties"> <dl class="message-properties">
<dt>action <span class="property-type">string</span></dt> <dt>action <span class="property-type">string</span></dt>
<dd>the name of the action the node should perform. Available actions are: <code>"connect"</code>, <dd>the name of the action the node should perform. Available actions are: <code>"connect"</code>,
<code>"disconnect"</code>, <code>"subscribe"</code> and <code>"unsubscribe"</code>.</dd> <code>"disconnect"</code>, <code>"getSubscriptions"</code>, <code>"subscribe"</code> and
<code>"unsubscribe"</code>.</dd>
<dt class="optional">topic <span class="property-type">string|object|array</span></dt> <dt class="optional">topic <span class="property-type">string|object|array</span></dt>
<dd>For the <code>"subscribe"</code> and <code>"unsubscribe"</code> actions, this property <dd>For the <code>"subscribe"</code> and <code>"unsubscribe"</code> actions, this property
provides the topic. It can be set as either:<ul> provides the topic. It can be set as either:<ul>

View File

@ -1,6 +1,6 @@
{ {
"name": "@node-red/nodes", "name": "@node-red/nodes",
"version": "4.0.5", "version": "4.0.9",
"license": "Apache-2.0", "license": "Apache-2.0",
"repository": { "repository": {
"type": "git", "type": "git",

View File

@ -144,7 +144,7 @@ async function installModule(module,version,url) {
if (url) { if (url) {
if (pkgurlRe.test(url) || localtgzRe.test(url)) { if (pkgurlRe.test(url) || localtgzRe.test(url)) {
// Git remote url or Tarball url - check the valid package url // Git remote url or Tarball url - check the valid package url
installName = url; installName = localtgzRe.test(url) && slashRe.test(url) ? `"${url}"` : url;
isRegistryPackage = false; isRegistryPackage = false;
} else { } else {
log.warn(log._("server.install.install-failed-url",{name:module,url:url})); log.warn(log._("server.install.install-failed-url",{name:module,url:url}));

View File

@ -1,6 +1,6 @@
{ {
"name": "@node-red/registry", "name": "@node-red/registry",
"version": "4.0.5", "version": "4.0.9",
"license": "Apache-2.0", "license": "Apache-2.0",
"main": "./lib/index.js", "main": "./lib/index.js",
"repository": { "repository": {
@ -16,7 +16,7 @@
} }
], ],
"dependencies": { "dependencies": {
"@node-red/util": "4.0.5", "@node-red/util": "4.0.9",
"clone": "2.1.2", "clone": "2.1.2",
"fs-extra": "11.2.0", "fs-extra": "11.2.0",
"semver": "7.6.3", "semver": "7.6.3",

View File

@ -96,21 +96,37 @@ var api = module.exports = {
} else if (scope === 'node') { } else if (scope === 'node') {
var node = runtime.nodes.getNode(id); var node = runtime.nodes.getNode(id);
if (node) { if (node) {
if (/^subflow:/.test(node.type)) {
ctx = runtime.nodes.getContext(node.id);
} else {
ctx = node.context(); ctx = node.context();
} }
} }
}
if (ctx) { if (ctx) {
if (key) { if (key) {
store = store || availableStores.default; store = store || availableStores.default;
ctx.get(key,store,function(err, v) { ctx.get(key,store,function(err, v) {
if (opts.keysOnly) { if (opts.keysOnly) {
const result = {}
if (Array.isArray(v)) { if (Array.isArray(v)) {
resolve({ [store]: { format: `array[${v.length}]`}}) result.format = `array[${v.length}]`
} else if (typeof v === 'object') { } else if (typeof v === 'object') {
resolve({ [store]: { keys: Object.keys(v), format: 'Object' } }) result.keys = Object.keys(v).map(k => {
if (Array.isArray(v[k])) {
return { key: k, format: `array[${v[k].length}]`, length: v[k].length }
} else if (typeof v[k] === 'object') {
return { key: k, format: 'object' }
} else { } else {
resolve({ [store]: { keys: [] }}) return { key: k }
} }
})
result.format = 'object'
} else {
result.keys = []
}
resolve({ [store]: result })
return
} }
var encoded = util.encodeObject({msg:v}); var encoded = util.encodeObject({msg:v});
if (store !== availableStores.default) { if (store !== availableStores.default) {
@ -147,7 +163,7 @@ var api = module.exports = {
} }
return return
} }
result[store] = { keys } result[store] = { keys: keys.map(key => { return { key }}) }
c--; c--;
if (c === 0) { if (c === 0) {
if (!errorReported) { if (!errorReported) {

View File

@ -719,6 +719,14 @@ class Flow {
}); });
} }
getContext(scope) {
if (scope === 'flow') {
return this.context
} else if (scope === 'global') {
return context.get('global')
}
}
dump() { dump() {
console.log("==================") console.log("==================")
console.log(this.TYPE, this.id); console.log(this.TYPE, this.id);

View File

@ -49,6 +49,14 @@ class Group {
} }
return this.parent.getSetting(key); return this.parent.getSetting(key);
} }
error(msg) {
this.parent.error(msg);
}
getContext(scope) {
return this.parent.getContext(scope);
}
} }
module.exports = { module.exports = {

View File

@ -100,7 +100,24 @@ async function evaluateEnvProperties(flow, env, credentials) {
} }
} else if (type ==='jsonata') { } else if (type ==='jsonata') {
pendingEvaluations.push(new Promise((resolve, _) => { pendingEvaluations.push(new Promise((resolve, _) => {
redUtil.evaluateNodeProperty(value, 'jsonata', {_flow: flow}, null, (err, result) => { redUtil.evaluateNodeProperty(value, 'jsonata',{
// Fake a node object to provide access to _flow and context
_flow: flow,
context: () => {
return {
flow: {
get: (value, store, callback) => {
return flow.getContext('flow').get(value, store, callback)
}
},
global: {
get: (value, store, callback) => {
return flow.getContext('global').get(value, store, callback)
}
}
}
}
}, null, (err, result) => {
if (!err) { if (!err) {
if (typeof result === 'object') { if (typeof result === 'object') {
result = { value: result, __clone__: true} result = { value: result, __clone__: true}
@ -113,6 +130,10 @@ async function evaluateEnvProperties(flow, env, credentials) {
resolve() resolve()
}); });
})) }))
} else if (type === "conf-type" && /^\${[^}]+}$/.test(value)) {
// Get the config node from the parent subflow
const name = value.substring(2, value.length - 1);
value = flow.getSetting(name);
} else { } else {
try { try {
value = redUtil.evaluateNodeProperty(value, type, {_flow: flow}, null, null); value = redUtil.evaluateNodeProperty(value, type, {_flow: flow}, null, null);

View File

@ -1,6 +1,6 @@
{ {
"name": "@node-red/runtime", "name": "@node-red/runtime",
"version": "4.0.5", "version": "4.0.9",
"license": "Apache-2.0", "license": "Apache-2.0",
"main": "./lib/index.js", "main": "./lib/index.js",
"repository": { "repository": {
@ -16,11 +16,11 @@
} }
], ],
"dependencies": { "dependencies": {
"@node-red/registry": "4.0.5", "@node-red/registry": "4.0.9",
"@node-red/util": "4.0.5", "@node-red/util": "4.0.9",
"async-mutex": "0.5.0", "async-mutex": "0.5.0",
"clone": "2.1.2", "clone": "2.1.2",
"express": "4.21.1", "express": "4.21.2",
"fs-extra": "11.2.0", "fs-extra": "11.2.0",
"json-stringify-safe": "5.0.1", "json-stringify-safe": "5.0.1",
"rfdc": "^1.3.1" "rfdc": "^1.3.1"

View File

@ -1,6 +1,6 @@
{ {
"name": "@node-red/util", "name": "@node-red/util",
"version": "4.0.5", "version": "4.0.9",
"license": "Apache-2.0", "license": "Apache-2.0",
"repository": { "repository": {
"type": "git", "type": "git",

View File

@ -1,6 +1,6 @@
{ {
"name": "node-red", "name": "node-red",
"version": "4.0.5", "version": "4.0.9",
"description": "Low-code programming for event-driven applications", "description": "Low-code programming for event-driven applications",
"homepage": "https://nodered.org", "homepage": "https://nodered.org",
"license": "Apache-2.0", "license": "Apache-2.0",
@ -31,14 +31,14 @@
"flow" "flow"
], ],
"dependencies": { "dependencies": {
"@node-red/editor-api": "4.0.5", "@node-red/editor-api": "4.0.9",
"@node-red/runtime": "4.0.5", "@node-red/runtime": "4.0.9",
"@node-red/util": "4.0.5", "@node-red/util": "4.0.9",
"@node-red/nodes": "4.0.5", "@node-red/nodes": "4.0.9",
"basic-auth": "2.0.1", "basic-auth": "2.0.1",
"bcryptjs": "2.4.3", "bcryptjs": "2.4.3",
"cors": "2.8.5", "cors": "2.8.5",
"express": "4.21.1", "express": "4.21.2",
"fs-extra": "11.2.0", "fs-extra": "11.2.0",
"node-red-admin": "^4.0.1", "node-red-admin": "^4.0.1",
"nopt": "5.0.0", "nopt": "5.0.0",

View File

@ -1009,6 +1009,29 @@ describe('delay Node', function() {
}); });
}); });
it('sending a msg with reset to empty queue doesnt send anything', function(done) {
this.timeout(2000);
var flow = [{"id":"delayNode1","type":"delay","name":"delayNode","pauseType":"rate","timeout":1,"timeoutUnits":"seconds","rate":2,"rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"wires":[["helperNode1"]]},
{id:"helperNode1", type:"helper", wires:[]}];
helper.load(delayNode, flow, function() {
var delayNode1 = helper.getNode("delayNode1");
var helperNode1 = helper.getNode("helperNode1");
var t = Date.now();
var c = 0;
helperNode1.on("input", function(msg) {
console.log("Shold not get here")
done(e);
});
setTimeout( function() {
if (c === 0) { done(); }
}, 250);
// send test messages
delayNode1.receive({payload:1,topic:"foo",reset:true}); // send something with blank topic
});
});
/* Messaging API support */ /* Messaging API support */
function mapiDoneTestHelper(done, pauseType, drop, msgAndTimings) { function mapiDoneTestHelper(done, pauseType, drop, msgAndTimings) {
const completeNode = require("nr-test-utils").require("@node-red/nodes/core/common/24-complete.js"); const completeNode = require("nr-test-utils").require("@node-red/nodes/core/common/24-complete.js");

View File

@ -111,8 +111,16 @@ describe('trigger node', function() {
try { try {
if (rval) { if (rval) {
msg.should.have.property("payload"); msg.should.have.property("payload");
if (type == "date" && val == "1") {
should.deepEqual(Math.round(msg.payload/1000000), Math.round(Date.now()/1000000));
}
else if (type == "date" && val == "iso") {
should.deepEqual(msg.payload.substr(0,11), rval.substr(0,11));
}
else {
should.deepEqual(msg.payload, rval); should.deepEqual(msg.payload, rval);
} }
}
else { else {
msg.should.have.property("payload", val); msg.should.have.property("payload", val);
} }
@ -126,6 +134,7 @@ describe('trigger node', function() {
}); });
it('should output 2st value when triggered ('+type+')', function(done) { it('should output 2st value when triggered ('+type+')', function(done) {
if (type == "date" && val == "1") { val = "0"; }
var flow = [{"id":"n1", "type":"trigger", "name":"triggerNode", op1:"foo", op1type:"str", op2:val, op2type:type, duration:"20", wires:[["n2"]] }, var flow = [{"id":"n1", "type":"trigger", "name":"triggerNode", op1:"foo", op1type:"str", op2:val, op2type:type, duration:"20", wires:[["n2"]] },
{id:"n2", type:"helper"} ]; {id:"n2", type:"helper"} ];
process.env[val] = rval; process.env[val] = rval;
@ -142,8 +151,16 @@ describe('trigger node', function() {
else { else {
if (rval) { if (rval) {
msg.should.have.property("payload"); msg.should.have.property("payload");
if (type == "date" && val == "0") {
;(Math.round(msg.payload/1000000)).should.be.approximately(parseInt(Date.now()/1000000), 1);
}
else if (type == "date" && val == "iso") {
should.deepEqual(msg.payload.substr(0,11), rval.substr(0,11));
}
else {
should.deepEqual(msg.payload, rval); should.deepEqual(msg.payload, rval);
} }
}
else { else {
msg.should.have.property("payload", val); msg.should.have.property("payload", val);
} }
@ -166,6 +183,9 @@ describe('trigger node', function() {
var val_buf = "[1,2,3,4,5]"; var val_buf = "[1,2,3,4,5]";
basicTest("bin", val_buf, Buffer.from(JSON.parse(val_buf))); basicTest("bin", val_buf, Buffer.from(JSON.parse(val_buf)));
basicTest("env", "NR-TEST", "env-val"); basicTest("env", "NR-TEST", "env-val");
basicTest("date", "1", Date.now());
basicTest("date", "iso", (new Date()).toISOString());
// basicTest("date", "object", Date.now());
it('should output 1 then 0 when triggered (default)', function(done) { it('should output 1 then 0 when triggered (default)', function(done) {
var flow = [{"id":"n1", "type":"trigger", "name":"triggerNode", duration:"20", wires:[["n2"]] }, var flow = [{"id":"n1", "type":"trigger", "name":"triggerNode", duration:"20", wires:[["n2"]] },

View File

@ -2067,6 +2067,27 @@ describe('CSV node (RFC Mode)', function () {
n2.on("input", function (msg) { n2.on("input", function (msg) {
try { try {
msg.should.have.property('payload', '1\tfoo\t"ba""r"\tdi,ng\n'); msg.should.have.property('payload', '1\tfoo\t"ba""r"\tdi,ng\n');
msg.should.have.property('columns', 'd,b,c,a'); // Strict RFC columns
done();
} catch (e) {
done(e);
}
});
const testJson = { d: 1, b: "foo", c: "ba\"r", a: "di,ng" };
n1.emit("input", { payload: testJson });
});
});
it('should convert a simple object back to a tsv with headers using a tab as a separator', function (done) {
const flow = [{ id: "n1", type: "csv", spec: "rfc", temp: "", sep: "\t", ret: '\n', hdrout: "all", wires: [["n2"]] }, // RFC-vs-Legacy difference - use line separator \n to satisfy original test
{ id: "n2", type: "helper" }];
helper.load(csvNode, flow, function () {
const n1 = helper.getNode("n1");
const n2 = helper.getNode("n2");
n2.on("input", function (msg) {
try {
msg.should.have.property('payload', 'd\tb\tc\ta\n1\tfoo\t"ba""r"\tdi,ng\n');
msg.should.have.property('columns', 'd,b,c,a'); // Strict RFC columns
done(); done();
} catch (e) { } catch (e) {
done(e); done(e);
@ -2086,6 +2107,7 @@ describe('CSV node (RFC Mode)', function () {
n2.on("input", function (msg) { n2.on("input", function (msg) {
try { try {
msg.should.have.property('payload', '4,foo,true,,0\n'); msg.should.have.property('payload', '4,foo,true,,0\n');
msg.should.have.property('columns', 'a,b o,c p,e'); // Strict RFC columns
done(); done();
} catch (e) { } catch (e) {
done(e); done(e);
@ -2106,6 +2128,7 @@ describe('CSV node (RFC Mode)', function () {
try { try {
// 'payload', 'a"a,b\'b\nA1,B1\nA2,B2\n'); // Legacy // 'payload', 'a"a,b\'b\nA1,B1\nA2,B2\n'); // Legacy
msg.should.have.property('payload', '"a""a",b\'b\nA1,B1\nA2,B2\n'); // RFC-vs-Legacy difference - RFC4180 Section 2.6, 2.7 quote handling msg.should.have.property('payload', '"a""a",b\'b\nA1,B1\nA2,B2\n'); // RFC-vs-Legacy difference - RFC4180 Section 2.6, 2.7 quote handling
msg.should.have.property('columns', '"a""a",b\'b'); // RCF compliant column names
done(); done();
} catch (e) { } catch (e) {
done(e); done(e);
@ -2171,6 +2194,7 @@ describe('CSV node (RFC Mode)', function () {
n2.on("input", function (msg) { n2.on("input", function (msg) {
try { try {
msg.should.have.property('payload', '1,3,2,4\n4,2,3,1\n'); msg.should.have.property('payload', '1,3,2,4\n4,2,3,1\n');
msg.should.have.property('columns', 'd,b,c,a'); // Strict RFC columns
done(); done();
} }
catch (e) { done(e); } catch (e) { done(e); }
@ -2189,6 +2213,7 @@ describe('CSV node (RFC Mode)', function () {
n2.on("input", function (msg) { n2.on("input", function (msg) {
try { try {
msg.should.have.property('payload', 'd,b,c,a\n1,3,2,4\n4,"f\ng",3,1\n'); msg.should.have.property('payload', 'd,b,c,a\n1,3,2,4\n4,"f\ng",3,1\n');
msg.should.have.property('columns', 'd,b,c,a'); // Strict RFC columns
done(); done();
} }
catch (e) { done(e); } catch (e) { done(e); }
@ -2208,6 +2233,7 @@ describe('CSV node (RFC Mode)', function () {
try { try {
// 'payload', ',0,1,foo,"ba""r","di,ng","fa\nba"\n'); // 'payload', ',0,1,foo,"ba""r","di,ng","fa\nba"\n');
msg.should.have.property('payload', ',0,1,foo\n'); // RFC-vs-Legacy difference - respect that user has specified a template with 4 columns msg.should.have.property('payload', ',0,1,foo\n'); // RFC-vs-Legacy difference - respect that user has specified a template with 4 columns
msg.should.have.property('columns', 'a,b,c,d');
done(); done();
} }
catch (e) { done(e); } catch (e) { done(e); }
@ -2327,6 +2353,7 @@ describe('CSV node (RFC Mode)', function () {
n2.on("input", function (msg) { n2.on("input", function (msg) {
try { try {
msg.should.have.property('payload', '{},"text,with,commas","This ""is"" a banana","{""sub"":""object""}"\n'); msg.should.have.property('payload', '{},"text,with,commas","This ""is"" a banana","{""sub"":""object""}"\n');
msg.should.have.property('columns', 'a,b,c,d');
done(); done();
} }
catch (e) { done(e); } catch (e) { done(e); }

View File

@ -258,6 +258,29 @@ describe('nodes/registry/installer', function() {
}).catch(done); }).catch(done);
}); });
it("succeeds when file path is valid node-red module", function(done) {
var nodeInfo = {nodes:{module:"foo",types:["a"]}};
var res = {
code: 0,
stdout:"",
stderr:""
}
var p = Promise.resolve(res);
p.catch((err)=>{});
execResponse = p;
var addModule = sinon.stub(registry,"addModule").callsFake(function(md) {
return Promise.resolve(nodeInfo);
});
installer.installModule("foo",null,"/example path/foo-0.1.1.tgz").then(function(info) {
exec.run.lastCall.args[1].should.eql([ 'install', '--no-audit', '--no-update-notifier', '--no-fund', '--save', '--save-prefix=~', '--omit=dev', '--engine-strict', '"/example path/foo-0.1.1.tgz"' ]);
info.should.eql(nodeInfo);
done();
}).catch(done);
});
it("triggers preInstall and postInstall hooks", function(done) { it("triggers preInstall and postInstall hooks", function(done) {
let receivedPreEvent,receivedPostEvent; let receivedPreEvent,receivedPostEvent;
hooks.add("preInstall", function(event) { event.args = ["a"]; receivedPreEvent = event; }) hooks.add("preInstall", function(event) { event.args = ["a"]; receivedPreEvent = event; })