Compare commits

..

352 Commits
1.2.8 ... 1.3.5

Author SHA1 Message Date
Nick O'Leary
4f77bbeb2b Update for 1.3.5 2021-05-18 11:52:49 +01:00
Nick O'Leary
be9521f659 Revert some of #2967 to fix treeList gutter width calculation 2021-05-14 21:17:47 +01:00
Nick O'Leary
90761fd840 Fix 'SyntaxError' in Function node when last line of on-stop is a comment 2021-05-13 17:23:25 +01:00
Nick O'Leary
d7dc7c4eda Fix error handling in runtime/lib/api/nodes 2021-05-13 15:46:56 +01:00
Nick O'Leary
2bbdc85a29 Prevent unknown node from breaking editor
If a node provides a .js file that registers a type
but its .html is empty, then the editor will know about
the type, but there will be no node definition.

This fix handles that in some of the utility functions
for generating node appearance.

This wasn't an exhaustive check for these things - just
some obvious candidates that I hit in testing 'bad' nodes
2021-05-13 14:28:01 +01:00
Nick O'Leary
15aa249f64 Stop module with missing types from preventing editor load 2021-05-13 14:06:43 +01:00
Nick O'Leary
866f305686 Open subflow tab next to active tab rather than at the end 2021-05-13 13:57:29 +01:00
Nick O'Leary
c8653f19bf Merge pull request #2985 from node-red-hitachi/update-function-node-info-text
Update Japanese info text of function node
2021-05-12 09:41:09 +01:00
Hiroyasu Nishiyama
b01100d818 Update Japanese info text of function node 2021-05-12 08:54:32 +09:00
Nick O'Leary
d4096a9026 Merge pull request #2984 from node-red/migrate-sass
Migrate to sass module from node-sass
2021-05-11 18:56:06 +01:00
Nick O'Leary
b9e780cdcd Add Node 16 with sass fixed 2021-05-11 18:14:05 +01:00
Nick O'Leary
b77cd56a01 Migrate from node-sass to sass
node-sass is deprecated and doesn't work on Node 16.
sass is actively maintained and considered the canonical sass
implementation.
2021-05-11 18:13:21 +01:00
Nick O'Leary
9cdec156dc Merge pull request #2974 from aheissenberger/fix-bundle-error
Fixed esbuild bundle error "installRetry" was declared a constant and changed
2021-05-11 17:45:07 +01:00
Nick O'Leary
6aa5968863 Fix Function tab label names in the node help text
Closes #2978
2021-05-11 17:15:27 +01:00
Nick O'Leary
8f7686cd7b Handle sidebar tab that no longer exists when setting first active 2021-05-11 16:42:32 +01:00
Nick O'Leary
d8d384a979 Fix plugin loading when browser sends unrecognised lang 2021-05-11 16:42:00 +01:00
Nick O'Leary
ade318bb78 Support mousewheel scroll in tab bar 2021-05-11 15:58:01 +01:00
Nick O'Leary
ed3aa8189f Shrink default notification box
Also reduces Inject/Debug notification display time as 5 seconds is a
long time for a message telling you it worked
2021-05-11 14:45:53 +01:00
Nick O'Leary
3e43597617 Prevent error whilst drag/drop importing from leaving dropTarget visible
Fixes #2982
2021-05-11 14:10:40 +01:00
Nick O'Leary
e641b0a965 Fix scaling issues when dragging nodes into scaled workspace 2021-05-10 21:03:27 +01:00
Hiroyasu Nishiyama
eddddc6c9b fix duplicate csv node example (#2980) 2021-05-10 15:39:23 +01:00
Nick O'Leary
f249d6306f Merge pull request #2976 from kazuhitoyokoi/master-fixinfotips
Fix incorrect shortcut keys in info tips
2021-05-10 09:42:59 +01:00
Kazuhito Yokoi
5c31bd54e4 Fix incorrect shortcut keys in info tips 2021-05-06 20:09:35 +09:00
Nick O'Leary
db0ff74857 Reduce code duplication around node/label generation 2021-05-04 11:12:55 +01:00
Andreas Heissenberger
54c9d27fd8 fix 2021-05-03 17:35:50 +02:00
Andreas Heissenberger
01888ff078 fix "installRetry" was declared a constant and changed 2021-05-03 17:23:27 +02:00
Nick O'Leary
1af21735a9 Fix theme handling when no editorTheme.page setting 2021-04-29 15:32:26 +01:00
Nick O'Leary
9886af3cec Fix jshint error in treeList 2021-04-29 14:09:21 +01:00
Nick O'Leary
1eb8f9ad97 Update changelog 2021-04-29 14:01:40 +01:00
Nick O'Leary
08e73d9d7d Merge pull request #2967 from hanc2006/master
Fixed remove item when depth=0 and wrong gutter calc treeList widget
2021-04-29 14:00:02 +01:00
Nick O'Leary
b0e349b215 Update for 1.3.4 2021-04-29 11:22:22 +01:00
Dave Conway-Jones
9ee8c1c791 Give delay node random mina nd max more space so you can see complete value 2021-04-29 10:36:13 +01:00
Nick O'Leary
cd3aba2b89 Allow nodes to access resolved theme files
Fixes #2968
2021-04-29 10:17:07 +01:00
Daniele
a150d8e289 Update packages/node_modules/@node-red/editor-client/src/js/ui/common/treeList.js
Thanks for the tip, I'll remember next time.

Co-authored-by: Nick O'Leary <nick.oleary@gmail.com>
2021-04-28 23:37:26 +02:00
Nick O'Leary
6da8e92f20 Fix inject node output tooltip extra property count 2021-04-28 22:01:39 +01:00
Nick O'Leary
7df1a03b4b Handle subflow modules that contain subflows 2021-04-28 21:50:00 +01:00
Nick O'Leary
ad316ffd37 Merge pull request #2964 from node-red-hitachi/fix-grunt-error-on-exec-node
fix grunt fail on exec node test
2021-04-28 21:02:23 +01:00
Nick O'Leary
91f5542a57 Fix importing node to currently flow rather than match its old z value
If you import a node whose z value is a known existing tab, it is getting
imported to that tab, rather than the expected behaviour of being imported
to the current tab.

This commit fixes that by checked if the node is being imported to a tab
that was included in the import, rather than pre-existing.
2021-04-28 20:54:31 +01:00
GitHub
d47a8aa562 Fix remove item when depth=0 and wrong gutter calc 2021-04-28 17:25:26 +02:00
Hiroyasu Nishiyama
70433f3d05 fix grunt fail on exec node test 2021-04-28 21:40:17 +09:00
Dave Conway-Jones
0e02d03d9a Merge branch 'master' of https://github.com/node-red/node-red 2021-04-27 11:12:10 +01:00
Nick O'Leary
8e7efd98b2 Don't let 'escape' whilst moving nodes interrupt things
Fixes #2960
2021-04-26 16:48:21 +01:00
Nick O'Leary
f5a1c8bc49 Merge pull request #2957 from node-red-hitachi/fix-error-on-git-auto-commit
fix error on auto commit for no flow change
2021-04-26 11:54:05 +01:00
Nick O'Leary
4cb8e99430 Timeout http upgrade requests that are not otherwise handled
Fixes #2956
2021-04-26 11:45:28 +01:00
Nick O'Leary
bbac49ff38 Ensure function expand button is above vertical scrollbar
Fixes #2955
2021-04-26 10:18:25 +01:00
Nick O'Leary
1d12017f11 Sort context stores in TypedInput and ensure default first
Fixes #2954
2021-04-26 10:13:57 +01:00
Hiroyasu Nishiyama
a480919ec3 fix error on auto commit for no flow change 2021-04-26 09:05:53 +09:00
Dave Conway-Jones
f8abf9fce1 add another test to csv 2021-04-25 08:53:18 +01:00
Dave Conway-Jones
9f1deb0c36 CSV Add couple more special character tests just to make sure 2021-04-23 11:19:23 +01:00
Dave Conway-Jones
4cebbf8d22 Fix CSV handling of special chars as separators
(ie escape regex special chars)
and add tests
to close #2950
2021-04-23 10:47:23 +01:00
Nick O'Leary
e57ebdb583 Merge pull request #2947 from kazuhitoyokoi/master
Fix margin between nodes on palette
2021-04-21 09:42:43 +01:00
Kazuhito Yokoi
372122037f Fix margin between nodes on palette 2021-04-21 13:14:46 +09:00
Nick O'Leary
23a5cb1917 Ensure typedInput option is selected in dropdown menu
Part of #2945
2021-04-20 23:39:21 +01:00
Nick O'Leary
f8d5fef3c4 Ensure typedInput without value has focus class removed
Closes #2945
2021-04-20 23:25:56 +01:00
Nick O'Leary
0e06da6c63 Update for 1.3.3 2021-04-20 11:06:23 +01:00
Nick O'Leary
9eb668ab30 Prevent TypedInput label overflowing element
Also adds title attribute to the button for the label so it gets a tooltip
2021-04-19 15:28:17 +01:00
Nick O'Leary
233a74c146 Remove TypedInput from tab focus when only one type available 2021-04-19 15:27:47 +01:00
Nick O'Leary
ff00afb5d7 Fix project credential secret reset handling
Part of #2868
2021-04-19 11:32:26 +01:00
Nick O'Leary
3f43dc1855 Fix jshint error 2021-04-19 10:43:01 +01:00
Nick O'Leary
4a4e7fc7cb Make typedInput.disable more consistent in behaviour
Fixes #2942
2021-04-19 10:39:58 +01:00
Dave Conway-Jones
0253dc9623 ensure CSV node can send false as string 2021-04-19 10:03:11 +01:00
Nick O'Leary
374ef3902c Merge pull request #2938 from hardillb/dev
Fix for #2935
2021-04-16 13:27:22 +01:00
Ben Hardill
235690064f Fix for #2935 2021-04-16 13:26:11 +01:00
Nick O'Leary
0167c25e08 Export package version in Grunt file so docs template can access 2021-04-16 11:56:23 +01:00
Nick O'Leary
04a3c4bb22 Ensure mqtt-close message is published when closing mqtt nodes
The change in 1.3 where we ensure config nodes are closed last broke this behaviour. Previously, the config node would get closed triggering the close message. With the new 1.3 behaviour, the flow nodes are stopped and as soon as the last flow node deregisters itself, the broker node would disconnect without sending the close message.

The fix is to send the close message as part of the deregister flow as that will handle all cases properly
2021-04-14 22:28:25 +01:00
Nick O'Leary
b5fda5642f Fix package semver comparison to allow >1 version increment 2021-04-14 18:06:59 +01:00
Nick O'Leary
b0955705be Update to 1.3.2 2021-04-13 13:34:16 +01:00
Nick O'Leary
a4a624d537 Update changelog 2021-04-13 13:33:41 +01:00
Nick O'Leary
6a8cf1b768 Fix variable reference error in editableList
Fixes #2933
2021-04-13 13:24:54 +01:00
Kazuhito Yokoi
39274b0c5d Add Japanese translations for Node-RED v1.3.1 (#2930) 2021-04-12 14:15:07 +01:00
Dave Conway-Jones
55c2430671 Merge branch 'master' of https://github.com/node-red/node-red 2021-04-12 12:16:32 +01:00
Dave Conway-Jones
023486e175 File out node - fix timing of msg.send to be after close., and...
allow msg.encoding to set encoding if desired.
To close #2921
2021-04-12 12:16:23 +01:00
Nick O'Leary
8227643741 Merge branch 'pr_2920' 2021-04-12 12:08:32 +01:00
Nick O'Leary
e44131f97a Update function node help reference to node properties 2021-04-12 12:08:07 +01:00
Nick O'Leary
5028377d45 Fix MQTT Broker TLS config row layout
Fixes #2927
2021-04-12 11:48:10 +01:00
Nick O'Leary
51aaf1b150 Handle package.json without dependencies section 2021-04-12 10:34:43 +01:00
Nick O'Leary
13406e76de Ensure theme login image is passed through to api response
Fixes #2929
2021-04-12 10:06:35 +01:00
Dave Conway-Jones
4672d98e8a split node - add comment to info re $N being number of messages arriving 2021-04-12 09:47:18 +01:00
Dave Conway-Jones
858b3d640a fix CSV parsing with other than , separator
(and joining as well...
and add tests
to close #2925
2021-04-10 22:17:31 +01:00
Nick O'Leary
6087002562 Fix handling of user-provided keymap
Fixes #2926
2021-04-10 21:34:26 +01:00
Kristian Heljas
ad788fbed1 Function node: describe node.outputCount in help text 2021-04-08 21:09:44 +03:00
Nick O'Leary
749533b0b4 Bump for 1.3.1 2021-04-08 16:23:22 +01:00
Nick O'Leary
142a5f7ca1 Fix change node form validation 2021-04-08 16:21:03 +01:00
Nick O'Leary
28bfa8e418 Update changelog 2021-04-08 14:57:38 +01:00
Kristian Heljas
7b8ed487e9 Function node: add node.outputCount property to sandbox (#2918)
* Function node: add `node.outputs` property to sandbox

https://discourse.nodered.org/t/expose-configured-output-count-to-function-node-i-can-pr/43848

* style: indetation for function node sanbox code

I guess this was unintentionally unindented in d51aefa156 (diff-24cd715c3b7405ea194bfdc0dc2a350ceb2f5d18696b8163c3e40105b981a666)

* Function node: tests for accessing node properties

consistently tests that `node.id`, `node.name` and `node.outputs`
are available in `init()`, `func()` and `finalize()` methods.

* Function node: rename `node.outputs` to `node.outputCount`

https://discourse.nodered.org/t/expose-configured-output-count-to-function-node-i-can-pr/43848/9?u=kristian
2021-04-08 14:52:02 +01:00
Nick O'Leary
0059f9475e Merge pull request #2919 from kazuhitoyokoi/master-addjpn
Add Japanese translations for Node-RED v1.3
2021-04-08 14:50:22 +01:00
Kazuhito Yokoi
9429ea7c64 Fix typo 2021-04-08 20:32:01 +09:00
Kazuhito Yokoi
a157580b22 Add Japanese translations for Node-RED v1.3 2021-04-08 20:06:35 +09:00
Nick O'Leary
41a0147938 Update changelog 2021-04-07 14:37:13 +01:00
Nick O'Leary
16e021e94f Request node: set followAllRedirects to work with POSTs
Fixes #2017
2021-04-07 14:35:29 +01:00
Nick O'Leary
449d76a6c7 Update version for 1.3.0 2021-04-06 18:31:49 +01:00
Nick O'Leary
70172db693 Update changelog 2021-04-06 18:31:16 +01:00
Nick O'Leary
ff93a38354 Update dependencies 2021-04-06 18:31:02 +01:00
Nick O'Leary
e3b70b10d1 Add property validate to Change node rule set
Closes #2911
2021-04-06 17:26:06 +01:00
Nick O'Leary
400141b093 Merge pull request #2913 from heikokue/i18n-de/fixes1
small fixes/improvements of DE translations
2021-04-06 17:03:32 +01:00
Nick O'Leary
ca5e45a46d Flag validation errors in Inject node props config
Fixes #2914
2021-04-06 16:45:21 +01:00
Nick O'Leary
74b547b93c Remove Node 8 from travis due to node-sass breakage 2021-03-31 23:54:25 +01:00
Nick O'Leary
a688305572 Fix jshint errors 2021-03-31 23:53:53 +01:00
Nick O'Leary
f0f2eefb59 Merge branch 'pr_2908' into dev 2021-03-31 23:50:30 +01:00
Nick O'Leary
bdb548ffdc Exec node: remove addpayValue and reuse addpay to track appending property 2021-03-31 23:50:00 +01:00
Nick O'Leary
fe5d4abec1 Add externalModules config to settings.js 2021-03-31 23:14:26 +01:00
Nick O'Leary
70632706f9 Allow Flow.getNode to return subflowInstance nodes
Related to #2898
2021-03-31 23:14:26 +01:00
Nick O'Leary
8f424c063e Merge pull request #2892 from node-red/view-stack
Add actions to make tab navigation easier
2021-03-31 20:56:15 +01:00
Heiko Kuester
9955c3dd5d small fixes/improvements of DE translations 2021-03-31 17:51:40 +02:00
Nick O'Leary
d555fcf7bd Merge pull request #2903 from node-red/plugin-resources
Allow module to provide resources and automatically expose them
2021-03-30 22:50:36 +01:00
Nick O'Leary
8da00c0872 Fix Switch node handling of hasKey rule when property is undefined 2021-03-30 21:37:39 +01:00
Nick O'Leary
393290df2c Prevent accidental text selection of subflow toolbar text 2021-03-25 22:40:25 +00:00
Nick O'Leary
f8a7835341 Fix credential lookup for nested subflows
Fixes #2910
2021-03-25 22:27:49 +00:00
Nick O'Leary
082bac8c3a Handle invalid regex set dynamically in Switch node
Fixes #2905
2021-03-22 21:06:59 +00:00
Nick O'Leary
4cafe42cf4 Update node-sass to 5.x
Fixes #2907
2021-03-22 20:18:23 +00:00
Kazuhito Yokoi
89485971fa Use RED.util.getMessageProperty() to check message property 2021-03-22 20:48:01 +09:00
Kazuhito Yokoi
cb72d5100e Remove type for typedInput in exec node 2021-03-22 20:24:12 +09:00
Kazuhito Yokoi
f103533852 Support typedInput in msg.payload field of exec node 2021-03-22 16:19:55 +09:00
Nick O'Leary
55f1e7ece1 Merge pull request #2904 from node-red-hitachi/fix-exporting-config-node
fix exporting config node
2021-03-18 10:30:03 +00:00
Hiroyasu Nishiyama
c0a765c998 fix exporting config node 2021-03-18 00:11:35 +09:00
heikokue
ed44fb461c updated DE translation for 1.3.0 (MQTT5, modules, function, ...) and other small fixes (#2901) 2021-03-16 09:52:16 +00:00
Nick O'Leary
8543613563 Allow module to provide resources and automatically expose them 2021-03-15 21:06:10 +00:00
Kazuhito Yokoi
734adc6445 Add Japanese translations for Node-RED v1.3.0 (#2900) 2021-03-15 08:23:30 +00:00
heikokue
827f8d4d51 rework of DE translation (#2806)
* started rework of translation to DE, added translation rules and dictionary

* reworks DE translation of JSONata /editor-client/locales/de/jsonata.json

* rework DE translation of editor-client

* moved /editor-client/locales/de/README.md to Wiki https://github.com/node-red/node-red/wiki/Design:-i18n-de

* Update README.md

* Update README.md

* Create README.md

* Create README.md

* fixed #2: "Sie müssen ..., um ... zu können"

* fixed #3

* fixed #4 and removed unnecessary spaces

* fixed #5

* fixed #6, added missing dots, removed unnecessary spaces

* fixed #7, #8, #9

* fixed #10, #11, #12, #13, #14, #15

* fixed #17, #18, 19

* fixed #19

* moved /editor-client/locales/de/dictionary.csv to https://github.com/heikokue/node-red-designs/blob/i18n-de/designs/i18n-de/dictionary.csv

* reworked DE translation of runtime

* fine-tuned DE translation of editor-client

* reworked DE translation of common nodes, fine-tuned editor-client

* reworked DE translation of all nodes, fine-tuned editor-client, intotips, jsonata & runtime

* small i18n fixes
2021-03-12 13:07:12 +00:00
Nick O'Leary
5bbd3d6273 Merge pull request #2894 from node-red-hitachi/fix-error-report-on-node-load
Fix error report on  node load
2021-03-11 16:01:06 +00:00
Dave Conway-Jones
df90e3414d CSV better handling of messages with incoming parts - to create array output
and add tests (apologies for the massive reformat of test file) - but honestly there are two new tests
2021-03-11 12:47:54 +00:00
Dave Conway-Jones
16b9abbe92 redo CSV fix for commas in header template 2021-03-11 09:34:30 +00:00
Nick O'Leary
2de43b719e Merge branch 'pr_2895' into dev 2021-03-10 21:31:27 +00:00
Nick O'Leary
3b84f27f36 Remove arrow-funcs from editor code 2021-03-10 21:31:05 +00:00
Nick O'Leary
f7a6a333e1 Show context store name on TypedInput flow/global types
Fixes #2793
2021-03-10 17:51:20 +00:00
Nick O'Leary
c37ea90206 Remember TypedInput selected sub option when switching types
Fixes #2896
2021-03-10 17:50:46 +00:00
Nick O'Leary
0b39ef68d9 Use cursor keys to change selection in workspace 2021-03-10 14:04:47 +00:00
Hiroyasu Nishiyama
40ea759e2c fix vanishing link in subflow 2021-03-10 09:03:52 +09:00
Hiroyasu Nishiyama
3671a70e3b fix error report on node load 2021-03-09 10:57:29 +09:00
Nick O'Leary
2fa50e458f Fix select up/down stream when zoomed in or out 2021-03-09 00:37:01 +00:00
Nick O'Leary
9c7db1381c Add core:go-to-selected-subflow action 2021-03-06 23:28:20 +00:00
Nick O'Leary
2d4f5b8603 Ctrl-dbclick on subflow node opens subflow tab 2021-03-06 23:21:16 +00:00
Nick O'Leary
5181890433 Add go-to-previous/next-location actions 2021-03-06 23:20:53 +00:00
Nick O'Leary
99a9e3a91b Fix handling encrypted creds on /flows api 2021-03-06 20:27:51 +00:00
Nick O'Leary
101378c625 Properly handle credentials passed to /flows api 2021-03-06 20:09:03 +00:00
Nick O'Leary
aa5e47b462 Fix copy-to-clipboard action in FireFox 2021-03-04 10:45:30 +00:00
Nick O'Leary
15715a2968 Ensure select-up/down-stream action follows branches in flows 2021-03-03 14:20:55 +00:00
Nick O'Leary
b5751e5746 Merge pull request #2890 from node-red-hitachi/update-node-i18n-jp
Update i18n and Japanese message catalogue for function node and mqtt node
2021-03-03 13:48:56 +00:00
Hiroyasu Nishiyama
7e40cb5331 update i18n and Japanese message for nodes 2021-03-03 10:07:33 +09:00
Nick O'Leary
4b1a86ee02 Update changelog 2021-03-02 20:23:33 +00:00
Nick O'Leary
6c66ca8acf Merge pull request #2888 from kazuhitoyokoi/dev-fixtraviserr
Fix regular expression for Node.js v8
2021-03-02 14:52:18 +00:00
Kazuhito Yokoi
d58a091bb7 Fix regular expression for Node.js v8 2021-03-02 13:13:23 +00:00
Nick O'Leary
0566a2d9b1 Fix function node tests use of RED.settings 2021-03-02 00:12:41 +00:00
Nick O'Leary
3d23d1de4f Merge pull request #2873 from node-red/function-modules
Function node external modules
2021-03-01 21:35:31 +00:00
Nick O'Leary
c9c5f7f088 Fix functionExternalModules tests 2021-03-01 21:34:37 +00:00
Nick O'Leary
8e65408b1c Prevent duplicate keyboard shortcut from being assigned 2021-03-01 20:50:34 +00:00
Nick O'Leary
c3adc956d7 Add functionExternalModules to settings and default to false 2021-03-01 18:24:16 +00:00
Nick O'Leary
f69d6b4eb1 Merge pull request #2886 from Steve-Mcl/dev
mqtt v5 fixes
2021-03-01 11:56:00 +00:00
Nick O'Leary
916d377aaa Fix handling of + in shortcuts 2021-02-28 10:29:54 +00:00
Nick O'Leary
39532a9d65 Fix keymap entries with multiple keys for same action 2021-02-27 21:58:22 +00:00
Nick O'Leary
3dc696b2a9 Fix semver comparison in palette editor 2021-02-27 21:58:05 +00:00
Nick O'Leary
7be7dec19a Fix removing links when deleting node 2021-02-27 21:57:27 +00:00
Dave Conway-Jones
fc709ba266 revert CSV node to commas in headers fix level 2021-02-26 14:34:38 +00:00
Steve-Mcl
080e2f2589 mqtt v5 fixes
- copy/paste issues with willMsg
- ensure helper func is ran for lwt messages
2021-02-25 19:58:59 +00:00
Nick O'Leary
0dc4440a99 Merge branch 'master' into dev 2021-02-25 17:56:01 +00:00
Nick O'Leary
f770786b89 Merge pull request #2846 from node-red/show-first-sidebar
Ensure the first sidebar tab is shown when editor loads
2021-02-25 16:06:24 +00:00
Nick O'Leary
46bc331428 Merge pull request #2843 from node-red/theme-keymap
Allow default keymap to be overridden in settings file
2021-02-25 16:05:51 +00:00
Nick O'Leary
3af77b6a31 Merge pull request #2785 from node-red/library-plugins
Library plugins
2021-02-25 16:05:23 +00:00
Nick O'Leary
8c4461c4f8 Merge pull request #2855 from node-red-hitachi/network-examples
Network node examples
2021-02-25 16:01:31 +00:00
Nick O'Leary
bccfd21cf4 Merge pull request #2854 from node-red/fix-csv-template-reset
fix csv node template reset when array complete
2021-02-25 16:00:30 +00:00
Nick O'Leary
e6f1394a74 Merge pull request #2869 from node-red/Fix-join-node-array-index-and-reset
Fix join node array index and reset
2021-02-25 15:59:54 +00:00
Nick O'Leary
de24831fb9 Merge pull request #2877 from node-red/select-tools
Add new select-* actions to editor
2021-02-25 15:58:56 +00:00
Nick O'Leary
e5716162ad Merge pull request #2880 from node-red/stop-order
Stop config nodes after flow nodes
2021-02-25 15:58:39 +00:00
Nick O'Leary
5809a3af0d Merge pull request #2881 from bartbutenaers/editableList-buttons
EditableList custom buttons
2021-02-25 15:58:23 +00:00
Nick O'Leary
11a385550a Merge branch 'pr_2778' into dev 2021-02-25 15:50:25 +00:00
Nick O'Leary
255b8f2005 Update mqtt nodes for v5 2021-02-25 15:49:56 +00:00
Nick O'Leary
3d398cfd53 Tidy up typedInput syntax 2021-02-25 13:39:59 +00:00
Nick O'Leary
5f8804c25c Ensure TypedInput Change event is passed type/value properties
Fixes #2883
2021-02-25 13:03:31 +00:00
bartbutenaers
02d1369d5b Escape all user input 2021-02-23 00:00:23 +01:00
bartbutenaers
0fef2ab509 Avoid innerHtml 2021-02-22 21:45:12 +01:00
Stephen McLaughlin
16088b8a08 improve description of mqtt options
Co-authored-by: Nick O'Leary <nick.oleary@gmail.com>
2021-02-22 12:45:32 +00:00
Nick O'Leary
69befe8f0e Merge pull request #2879 from HaKr/SemVerFullCompareNewAttempt
New attempt for PR #2821
2021-02-22 11:01:39 +00:00
bartbutenaers
ae7a3981c0 Pass evt to button handler 2021-02-21 23:24:40 +01:00
bartbutenaers
8b4aa3f5af Custom buttons comment 2021-02-21 22:57:34 +01:00
bartbutenaers
60c8a2c598 Custom buttons 2021-02-21 22:55:19 +01:00
bartbutenaers
fbb7dd4c3f EditableList custom buttons 2021-02-21 22:52:41 +01:00
Steve-Mcl
833ecfb1af MQTT V5 - prep for 1.3.0 beta...
* MQTT IN node tidy up
  * remove userProperties
  * remove subscriptionIdentifier
* MQTT OUT node tidy up
  * remove topicAlias
  * remove payloadFormatIndicator
  * remove subscriptionIdentifier
* MQTT BROKER node tidy up
  * remove topicAliasMaximum
  * remove maximumPacketSize
  * remove receiveMaximum
  * remove userProperties
2021-02-20 19:58:13 +00:00
Steve-Mcl
c20bab2436 more specific class selectors for show/hide items 2021-02-20 15:52:14 +00:00
Nick O'Leary
afb17af571 Fix global leak in lib/flows/index.js 2021-02-19 21:47:02 +00:00
Nick O'Leary
5012568464 Stop config nodes after flow nodes
Fixes #2876
2021-02-19 20:44:01 +00:00
Harry de Kroon
02dd141095 New attempt for PR #2821
PR #2821 could not be completed due to different email addresses used for it's commits.
This new branch is meant to be used as a new PR to replace the failed one
2021-02-19 16:55:04 +01:00
Nick O'Leary
0be82d964e Merge pull request #2870 from node-red-hitachi/fix-debug-filter
Allow filtering of debug node output within subflow
2021-02-19 15:45:57 +00:00
Nick O'Leary
b41c7962c2 Add tests for pluggable library 2021-02-19 15:24:56 +00:00
Nick O'Leary
6f9e06e78d Add validation of library plugin id and better error reporting 2021-02-19 14:39:42 +00:00
Nick O'Leary
c2347076f4 Handle errors when initialising library plugin 2021-02-19 14:01:33 +00:00
Nick O'Leary
c744af161d Add support for settings object in plugin definition 2021-02-19 11:59:49 +00:00
Hiroyasu Nishiyama
74ea382cf2 update subflow finding algorithm 2021-02-19 14:35:59 +09:00
Steve-Mcl
ab4a9e72d4 Merge branch 'dev' into mqtt5 2021-02-18 18:50:26 +00:00
Nick O'Leary
3f9a29730f Add partial implementation of adding library sources via editor
This adds lots of commented out code that provides a settings panel
to add new library sources. It is incomplete as it doesn't actually
add/update the library sources on the runtime.

For 1.3, I'm focussing on allowing additional sources get added
via the settings file only. I've done enough work on the editor
side to convince myself more work is needed than I can justify
at this time on what is otherwise not going to be a widely
used feature.
2021-02-18 11:58:23 +00:00
Nick O'Leary
8a076c01ab Support for library source plugins 2021-02-18 11:58:22 +00:00
Nick O'Leary
ca75efcbaf Adds shift-click support for selecting up/down stream nodes 2021-02-17 17:32:55 +00:00
Nick O'Leary
f96ce2fd83 Get node-red core nodes back to the top of the list 2021-02-17 15:20:16 +00:00
Nick O'Leary
d5f4f987f2 Add 'node' object to Function close scope 2021-02-17 14:41:50 +00:00
Nick O'Leary
11475b0c38 Move function expand buttons to overlap editor and save space 2021-02-17 14:41:25 +00:00
Nick O'Leary
137fa98903 Move name field above tab bar in Function node 2021-02-17 12:04:38 +00:00
Nick O'Leary
ea62c1806e Give edit dialog a little bit more vertical space 2021-02-17 12:04:20 +00:00
Nick O'Leary
45afd06047 Prevent rogue mouseup on tab from triggering tab change 2021-02-17 12:03:13 +00:00
Dave Conway-Jones
e6ec59092c Update settings.js 2021-02-17 12:02:27 +00:00
Nick O'Leary
35f788693d Add select-connected action 2021-02-16 21:16:21 +00:00
Nick O'Leary
d5314d2a85 Add select-up/downstream-nodes action to editor 2021-02-16 20:46:41 +00:00
Nick O'Leary
efd8c3d6d2 Merge pull request #2849 from node-red-hitachi/fix-copy-to-clipboard
fix line break of exporting nodes to clipboard
2021-02-16 15:50:24 +00:00
Nick O'Leary
7d04353843 Merge pull request #2859 from node-red/fix-numeric-status
fix numeric status not displaying by ensuring it's a string
2021-02-16 14:32:14 +00:00
Nick O'Leary
644da0b77b Merge pull request #2872 from node-red-hitachi/fix-node-deploy-with-group-change
Fix deploy of node in group
2021-02-16 14:18:08 +00:00
Nick O'Leary
785c349adc Prevent function module overwriting built-in sandbox properties 2021-02-16 13:58:59 +00:00
Nick O'Leary
1608196cd2 Merge pull request #2874 from kazuhitoyokoi/dev-jpn
Add Japanese translations for Node-RED v1.3.0
2021-02-16 10:07:40 +00:00
Nick O'Leary
9d34abf603 Function node: test modules identified in libs are added to sandbox 2021-02-15 20:59:37 +00:00
Nick O'Leary
05beb6ca79 Add unit tests for externalModules 2021-02-15 17:28:14 +00:00
Hiroyasu Nishiyama
12c7238c72 revert diffConfigs args 2021-02-15 22:05:42 +09:00
Kazuhito Yokoi
ed359ca10c Add Japanese translations for Node-RED v1.3.0 2021-02-15 13:02:58 +09:00
Hiroyasu Nishiyama
b66468c4ea restart node only if node's group changes 2021-02-14 10:06:46 +09:00
Nick O'Leary
d2c9ccbfdd Detect externalModule dependencies inside subflow modules
Not sure this is 100% the right approach. If a subflow module has a dependency
it should be in the subflow's package.json and therefore installed next to the
subflow module in ~/.node-red/node_modules.

By treating it as a 'normal' external module, it will be dynamically installed
in ~/.node-red/externalModules. That then exposes the module to the user
who won't know why its there and may remove it.

It would be better to allow nodes inside a subflow module to require
from ~/.node-red/node_modules and not limit it to the externalModules
dir. The hard part is knowing when to do that.
2021-02-14 00:02:08 +00:00
Nick O'Leary
85e05b787f Hide projects dialog when opening proj with invalid encrypt key 2021-02-13 19:53:10 +00:00
Nick O'Leary
e5471b44e0 Merge pull request #2871 from node-red-hitachi/fix-IE11-flow-downlod
add IE11 polyfill to support URI download scheme
2021-02-13 00:27:42 +00:00
Nick O'Leary
e0f0a76ae4 Update marked dependency 2021-02-13 00:25:40 +00:00
Nick O'Leary
6336ab121e Merge branch 'dev' into function-modules 2021-02-13 00:21:27 +00:00
Nick O'Leary
e899d2d5b8 Fix tests for externalModules component 2021-02-13 00:18:04 +00:00
Nick O'Leary
a94c19a6cf Fix up loading of freshly installed modules in Function node 2021-02-12 22:40:30 +00:00
Nick O'Leary
9c09ee3b71 Rework Function node module integration 2021-02-12 18:14:13 +00:00
Dave Conway-Jones
d8e68a75b9 Update CSV template words for clarity. 2021-02-12 17:24:33 +00:00
Dave Conway-Jones
302c5cfe09 CSV node - handle commas in msg.columns if quoted.
and add more tests
To close #2860
2021-02-12 16:55:41 +00:00
Hiroyasu Nishiyama
1be337fbc5 make nodes with only group change not deployed by nodes deploy mode 2021-02-13 00:23:30 +09:00
Hiroyasu Nishiyama
3ec37e2c66 make flow download code separate utility instead of polyfill 2021-02-11 23:10:33 +09:00
Hiroyasu Nishiyama
3740c21bee add IE11 polyfill to support URI download scheme 2021-02-11 17:56:08 +09:00
Hiroyasu Nishiyama
5a6568e7c2 allow filtering of debug node output within subflow 2021-02-10 17:32:27 +09:00
Dave Conway-Jones
4cd9b7b050 fix join node in array mode with repeated messages, and rallow reset all
to close #2866
2021-02-09 17:27:58 +00:00
Nick O'Leary
dd780945e1 Sanitize Debug node name when display enable/disable message 2021-02-09 15:04:00 +00:00
Dave Conway-Jones
e86f6a841a fix numeric status not displaying by ensuring it's a string 2021-02-05 11:36:26 +00:00
Nick O'Leary
fad8dcd304 Bump for 1.2.9 2021-02-03 18:04:37 +00:00
Nick O'Leary
1633a2ff70 Sanitize node type names when displaying in notifications 2021-02-03 15:50:05 +00:00
Nick O'Leary
a2878fa066 Sanitize branch name before displaying in notification message 2021-02-03 15:46:57 +00:00
Nick O'Leary
735de2908a Handle more valid language codes when validating lang params
Fixes #2856
2021-02-03 15:43:26 +00:00
Hiroyasu Nishiyama
818021e0b7 add examples for websocket in/out node 2021-02-03 17:13:56 +09:00
Hiroyasu Nishiyama
3cc6c4433f add examples for UDP in/out node 2021-02-03 17:12:35 +09:00
Hiroyasu Nishiyama
8306ddd40f add examples for TCP in/out node 2021-02-03 17:01:29 +09:00
Hiroyasu Nishiyama
024e71cdf5 add examples for HTTP in/response node 2021-02-03 16:34:17 +09:00
Dave Conway-Jones
4313cbaa5c fix csv node template reset when array complete
and add tests
to close #2853
2021-02-02 14:20:46 +00:00
Nick O'Leary
f5da2eb633 Merge branch 'master' into dev 2021-02-02 13:45:43 +00:00
Nick O'Leary
ac68e9c6b5 Merge pull request #2850 from node-red-hitachi/fix-grant-jshint-failure
fix jshint failure
2021-01-30 08:46:18 +00:00
Hiroyasu Nishiyama
32692dce07 fix jshint failure 2021-01-30 09:43:01 +09:00
Hiroyasu Nishiyama
64d3b8e104 fix line break of exporting nodes to clipboard 2021-01-30 01:20:51 +09:00
Nick O'Leary
a03edf3d58 Merge pull request #2845 from node-red/subflow-delete-prompt
Add confirm dialog when deleting subflow with instances in use
2021-01-29 10:57:11 +00:00
Nick O'Leary
e5b7ccb612 Add subflow edit button to palette tooltip 2021-01-29 10:42:04 +00:00
Nick O'Leary
9c1ce5543d Merge pull request #2844 from node-red/disable-groups
Add enable/disable toggle button for groups in info-outliner
2021-01-29 10:20:26 +00:00
Nick O'Leary
ba387a8e4e Merge pull request #2841 from node-red/credential-timeout
Handle timeouts when trying to load node credentials in editor
2021-01-29 10:20:04 +00:00
Nick O'Leary
7fa25c1ff4 Merge pull request #2839 from node-red/subflow-search
Add easier ways to find subflow instances
2021-01-29 10:19:36 +00:00
Nick O'Leary
3bd1bfc769 Fix check for existing config nodes in subflow export set 2021-01-29 10:16:40 +00:00
Kazuhiro Ito
dad47ade38 Add config node to refer to when exporting subflow 2021-01-29 11:05:15 +09:00
Nick O'Leary
fa6e0c8964 Ensure the first sidebar tab is shown when editor loads 2021-01-28 23:42:32 +00:00
Nick O'Leary
bb4b252401 Add confirm dialog when deleting subflow with instances in use 2021-01-28 23:08:47 +00:00
Nick O'Leary
a50404b141 Add enable/disable toggle button for groups in info-outliner 2021-01-28 22:07:19 +00:00
Nick O'Leary
61690ecf4a Allow default keymap to be overridden in settings file 2021-01-28 16:42:16 +00:00
Nick O'Leary
f4c87af5c1 Increase credential load timeout to 30secs 2021-01-28 11:30:10 +00:00
Nick O'Leary
83d12f7d39 Handle timeouts when trying to load node credentials in editor
Fixes #2840
2021-01-28 11:15:15 +00:00
Nick O'Leary
2e73b229d7 Add easier ways to find subflow instances 2021-01-28 00:41:19 +00:00
Nick O'Leary
3a0074d96e Merge branch 'master' into dev 2021-01-27 23:28:19 +00:00
Hiroyasu Nishiyama
7d08de9c99 Storage examples (#2784)
* add examples of file node

* add file-in node examples

* add watch node examples
2021-01-27 20:50:13 +00:00
Nick O'Leary
575d07e41a Merge pull request #2836 from node-red/theme-plugins
Add support for Theme Plugins
2021-01-27 20:45:59 +00:00
Nick O'Leary
24da3608c4 Merge pull request #2779 from node-red/plugins
Plugins
2021-01-27 20:37:35 +00:00
heikokue
9eb7fad621 fixed #2790 swapped description of encodeUrl/encodeUrlComponent and d… (#2791)
* fixed #2790 swapped description of encodeUrl/encodeUrlComponent and decodeUrl/decodeUrlComponent

* fixed #2790 swapped description of encodeUrl/encodeUrlComponent and decodeUrl/decodeUrlComponent also in ja, ru, zh-CN and zh-TW
2021-01-27 20:33:54 +00:00
Nick O'Leary
438d51d26e Allow nested msg properties in msg/flow/global expressions (#2822)
* Allow nested msg properties in msg/flow/global expressions

* Remove typo in RED.utils

Co-authored-by: Nick O'Leary <knolleary@users.noreply.github.com>
2021-01-27 20:32:52 +00:00
Hiroyasu Nishiyama
34ef055d7b Fix line break of subflow label on palette (#2828)
* fix line break of subflow label on palette

* handle line break on palette
2021-01-27 20:32:15 +00:00
Hiroyasu Nishiyama
4a1d66f210 update UI, Runtime API, metadata handling, and others 2021-01-27 22:27:54 +09:00
Nick O'Leary
1f6328bf4e Add initial support for ThemePlugins 2021-01-26 13:49:47 +00:00
Nick O'Leary
8e7a230dbc Fix plugin test to expect user flag 2021-01-26 13:49:13 +00:00
Nick O'Leary
6e718ca772 Fix merge of dev 2021-01-26 13:44:38 +00:00
Nick O'Leary
9e179170ee Add i18n function to editor plugins when they are registered
Adds a `_` function to the plugin definition object that will automatically
prepend the plugin's module namespace to any call. This saves the plugin
from having to prepend its namespace all of the time.
2021-01-20 15:35:44 +00:00
Nick O'Leary
9f71dbb006 Fixup merge 2021-01-18 16:25:41 +00:00
Nick O'Leary
7531314e3f Add RED.plugins module to editor 2021-01-18 16:25:40 +00:00
Nick O'Leary
a006b52052 Initial plugin runtime api implementation 2021-01-18 16:25:40 +00:00
Nick O'Leary
bebebaa3dd Merge pull request #2820 from node-red/export-preview
Add preview to Export Dialog
2021-01-18 16:25:16 +00:00
Tiago Ferreira
55ff035fc9 Ability to add projects path to the settings file (#2816)
* add the ability to set the projects path

* Update packages/node_modules/@node-red/runtime/locales/en-US/runtime.json

use directory to keep consistency with the project

Co-authored-by: Nick O'Leary <nick.oleary@gmail.com>

* Update packages/node_modules/@node-red/runtime/lib/storage/localfilesystem/projects/index.js

only show the projects directory is projects are enabled

Co-authored-by: Nick O'Leary <nick.oleary@gmail.com>

* use "directory" instead of "folder" to keep consistency with the Node-RED project

Co-authored-by: Nick O'Leary <nick.oleary@gmail.com>
2021-01-18 13:18:07 +00:00
Hiroyasu Nishiyama
d8c8d7bc57 hide unused input field (#2823) 2021-01-16 16:58:13 +00:00
Nathanaël Lécaudé
2b28ae3402 Add settings.execMaxBufferSize to control buffer size of exec node (#2819)
Co-authored-by: Dave Conway-Jones <dceejay@users.noreply.github.com>
closes #2817
2021-01-14 14:38:39 +00:00
Dave Conway-Jones
aa47bae2ad Exec node - don't append msg.payload to command by default (#2818)
* exec change default to not append payload
2021-01-13 10:12:19 +00:00
Nick O'Leary
ccfde84769 Merge pull request #2812 from node-red/property-types
Support node property typing
2021-01-12 23:56:01 +00:00
Nick O'Leary
b1df6d5149 Add preview of exported nodes to Export dialog 2021-01-12 18:23:15 +00:00
Hiroyasu Nishiyama
d51aefa156 initial support for npm module installation 2021-01-11 19:32:16 +09:00
Nick O'Leary
c40412d7c6 Merge pull request #2690 from node-red/sf-module
[sf-modules] Support npm subflow modules
2021-01-08 21:16:05 +00:00
Nick O'Leary
b0bc7ecacb Merge pull request #2763 from node-red/simple-git-setting
Allow project workflow to be configured via settings file
2021-01-08 15:20:11 +00:00
Nick O'Leary
5489bd37c9 Fix handling of default workflow mode when projects not active 2021-01-08 15:05:34 +00:00
Nick O'Leary
ea2e3f25d8 Implement node property typing
See https://github.com/node-red/designs/pull/37
2021-01-08 14:19:12 +00:00
Nick O'Leary
09b37cf538 Merge pull request #2796 from fellinga/feature/lang-select
add optional lang select
2021-01-07 17:15:36 +00:00
Nick O'Leary
e30a01310e Merge pull request #2788 from kevinGodell/dev
middleware integration
2021-01-07 17:11:48 +00:00
Nick O'Leary
d5cc5b2574 Use subflow.info for help text and meta.type for node type 2021-01-07 15:34:27 +00:00
Nick O'Leary
160ca6add4 remove console.log 2021-01-07 11:26:21 +00:00
Nick O'Leary
da96c85d32 Handle subflow modules with their own npm dependencies 2021-01-07 11:26:20 +00:00
Nick O'Leary
de15a1c36f Add subflow meta data edit pane 2021-01-07 11:10:58 +00:00
Nick O'Leary
814fc8bc69 Add SubflowModule class for running subflow modules 2021-01-07 11:10:58 +00:00
Nick O'Leary
0c9fd25d3e Nodes log via parent flow to allow flow-info to be added 2021-01-07 11:10:34 +00:00
Nick O'Leary
9a660f3fe9 Support npm subflow modules 2021-01-07 11:10:33 +00:00
Nick O'Leary
6e1466e411 Tidy some subflow env props css 2021-01-07 10:42:35 +00:00
Nick O'Leary
7913b3cbc2 Merge branch 'master' into dev 2021-01-07 10:17:50 +00:00
Nick O'Leary
87c9ed6356 Merge pull request #2797 from node-red/externalModules
Add support for settings.externalModules
2021-01-07 10:12:33 +00:00
Nick O'Leary
65b4ef6c3d Remove ES6 from editor code 2021-01-07 10:06:08 +00:00
Nick O'Leary
0284ef401e Fix loading individual module catalog 2021-01-06 20:20:32 +00:00
Nick O'Leary
8a87f93741 Use npm info to check pending install version 2021-01-06 20:03:22 +00:00
Nick O'Leary
af19536222 Better logging when deprecated editorTheme.palette.* settings used 2021-01-06 17:36:59 +00:00
Nick O'Leary
5743a5f91d Filter palette manager nodes based on allow/deny list 2020-12-27 21:34:21 +00:00
Nick O'Leary
9d2d060dec Fix unit tests for externalModules 2020-12-27 20:59:31 +00:00
fellinga
b36e7e172e add settings to init 2020-12-27 14:44:32 +01:00
Nick O'Leary
aacb92a7ae Implement allow/denyList when loading/installing modules 2020-12-27 12:49:17 +00:00
fellinga
4943bde3d4 add optional lang select 2020-12-27 12:59:12 +01:00
Nick O'Leary
fc459be531 Deprecate editorTheme.palette.editable for externalModules.palette.allowInstall
Also deprecates editorTheme.palette.editable for externalModules.palette.allowUpload
2020-12-23 23:29:07 +00:00
Nick O'Leary
3151502a3f Deprecate autoInstallModules for externalModules.autoInstall 2020-12-23 22:05:58 +00:00
Kevin Godell
79b10ed18a allow for adding an array of middleware functions 2020-12-22 16:30:38 -06:00
Kevin Godell
34b27f2e68 allow for adding an array of middleware functions 2020-12-22 16:28:33 -06:00
Nick O'Leary
9b1c114c3f Merge pull request #2772 from node-red/api-tidy
Fully remove when.js dependency
2020-12-07 14:05:15 +00:00
Nick O'Leary
4df27d4b0b Fix typos in parser examples 2020-12-07 14:04:42 +00:00
Nick O'Leary
e3445dae46 Merge pull request #2749 from node-red-hitachi/parser-example
add examples for parser category nodes
2020-12-07 14:01:14 +00:00
Nick O'Leary
f5fcf23678 Merge branch 'pr_2751' into dev 2020-12-07 13:58:53 +00:00
Nick O'Leary
0a6c08e2c3 Merge pull request #2750 from node-red-hitachi/split-join-node-mapi
Messaging API support in Split/Join nodes
2020-12-07 13:54:47 +00:00
Nick O'Leary
b80a7459cf Merge pull request #2733 from node-red-hitachi/delay-node-mapi
Messaging API support in Delay node
2020-12-07 13:53:51 +00:00
Nick O'Leary
f6480e6e0c Merge pull request #2744 from node-red-hitachi/sort-node-mapi
Messaging API support in Sort node
2020-12-07 13:52:34 +00:00
Nick O'Leary
41d12c433e Merge pull request #2734 from node-red-hitachi/csv-node-mapi
Messaging API support in CSV node
2020-12-07 13:50:09 +00:00
Nick O'Leary
169a2484f2 Merge pull request #2738 from node-red-hitachi/batch-node-mapi
Messaging API support in Batch node
2020-12-07 13:33:04 +00:00
Steve-Mcl
be52ec1390 mark more TODO areas (mostly debugging) 2020-12-07 12:48:33 +00:00
Steve-Mcl
00db43198d design/TODO comments 2020-12-07 12:26:27 +00:00
Steve-Mcl
6bac207611 better handling of server properties 2020-12-07 12:25:51 +00:00
Steve-Mcl
6150ae787d userProperties input type correction 2020-12-07 11:50:33 +00:00
Nick O'Leary
fc7967d455 Fix missing promise on setUserSettings 2020-12-07 11:49:02 +00:00
Nick O'Leary
fca21ac126 Rename paletteEditorEnabled to installerEnabled 2020-12-07 11:49:01 +00:00
Nick O'Leary
6fb96fa3c1 Move exec and events components to util module
The exec and events components are common components that
are used by both runtime and registry. It makes sense to
move them into the util package.

This also adds some docs to the registry module
2020-12-07 11:49:01 +00:00
Nick O'Leary
a1f565f756 Use more async funcs in runtime/lib/api to reduce Promise creation 2020-12-07 11:49:01 +00:00
Nick O'Leary
5992ed1fab Fully remove when.js dependency 2020-12-07 11:49:01 +00:00
Nick O'Leary
beccdac717 Merge branch 'master' into dev 2020-12-07 11:48:38 +00:00
Steve-Mcl
b72ea63100 config node userProperties should only permit json 2020-12-07 11:45:02 +00:00
Steve-Mcl
27550f2d4b automatically use resposeTopic if topic is empty 2020-12-07 09:43:52 +00:00
Steve-Mcl
6917919f35 show name as @x if alias is set but topic is not
- e.g. @1 denotes alias 1 is set but topic is empty
2020-12-07 09:41:43 +00:00
Steve-Mcl
48d6fe5918 Merge branch 'dev' into mqtt5 2020-12-07 08:21:59 +00:00
Nick O'Leary
bbe3ee701f Merge pull request #2768 from natcl/librarySymlinks
Library: properly handle symlinked folders
2020-11-25 21:07:50 +00:00
Nathanaël Lécaudé
b1c0d6b452 Library: properly handle symlinked folders 2020-11-24 18:29:39 -05:00
Steve-Mcl
3033d2086e Merge remote-tracking branch 'upstream/dev' into mqtt5 2020-11-19 13:45:56 +00:00
Nick O'Leary
0f7d185a61 Ensure runtime side picks up default project workflow mode 2020-11-18 16:37:39 +00:00
Nick O'Leary
81f200641b Allow default project workflow to be set via settings 2020-11-18 16:37:39 +00:00
Nick O'Leary
c6129b44a1 Merge branch 'master' into dev 2020-11-18 16:36:56 +00:00
Nick O'Leary
2a8290a4b7 Bump version to 1.3.0-beta.1 2020-11-13 18:23:35 +00:00
Kunihiko Toumura
f038069fe2 Messaging API support in Trigger node 2020-11-05 15:09:41 +09:00
Kunihiko Toumura
407cb3e7d5 Messaging API support in Split/Join nodes 2020-11-04 21:43:20 +09:00
Hiroyasu Nishiyama
b8bc62ee11 add example for YAML node 2020-11-03 20:54:04 +09:00
Hiroyasu Nishiyama
ccbd179f23 add example for XML node 2020-11-03 20:06:27 +09:00
Hiroyasu Nishiyama
dac7830bd4 add example for JSON node 2020-11-03 20:06:18 +09:00
Hiroyasu Nishiyama
bc82ca2106 add examples for HTML node 2020-11-03 17:32:53 +09:00
Hiroyasu Nishiyama
725c962236 add examples for CSV node 2020-11-03 14:34:34 +09:00
Kunihiko Toumura
d7dfeaf0c1 Messaging API support in Sort node 2020-11-02 13:31:27 +09:00
Kunihiko Toumura
dbfbd54e1f Messaging API support in Batch node 2020-10-29 16:16:03 +09:00
Kunihiko Toumura
8007bea7db Messaging API support in CSV node 2020-10-26 20:25:52 +09:00
Kunihiko Toumura
dc1ab7e331 Add support for Messaging API to delay node 2020-10-26 16:52:18 +09:00
Steve-Mcl
a0d197d0c0 Merge branch 'dev' into mqtt5 2020-10-25 09:52:33 +00:00
Steve-Mcl
a776ba248e correction to retain handling def value 2020-10-25 09:50:57 +00:00
Steve-Mcl
195aeb5caf mqttv5 progress 2020-10-14 23:30:03 +01:00
Steve-Mcl
38649de85f debugging 2020-10-10 10:56:10 +01:00
Steve-Mcl
33bb86cbcf mqtt5 1st draft 2020-10-08 20:24:35 +01:00
324 changed files with 20495 additions and 6181 deletions

3
.gitignore vendored
View File

@@ -7,7 +7,9 @@
.sessions.json
.settings
.tern-project
.i18n-editor-metadata
*.backup
*.bak
*_cred*
coverage
credentials.json
@@ -24,3 +26,4 @@ docs
!packages/node_modules/**/docs
.vscode
.nyc_output
sync.ffs_db

View File

@@ -4,6 +4,9 @@ addons:
language: node_js
matrix:
include:
- node_js: "16"
script:
- ./node_modules/.bin/grunt no-coverage
- node_js: "14"
script:
- ./node_modules/.bin/grunt && ( cat coverage/lcov.info | $(npm get prefix)/bin/coveralls || true ) && rm -rf coverage
@@ -16,6 +19,6 @@ matrix:
- node_js: "10"
script:
- ./node_modules/.bin/grunt no-coverage
- node_js: "8"
script:
- ./node_modules/.bin/grunt no-coverage
#- node_js: "8"
# script:
# - ./node_modules/.bin/grunt no-coverage

2
API.md
View File

@@ -10,6 +10,6 @@ Module | Description
[@node-red/editor-api](@node-red_editor-api.html) | an Express application that serves the Node-RED editor and provides the Admin HTTP API
[@node-red/runtime](@node-red_runtime.html) | the core runtime of Node-RED
[@node-red/util](@node-red_util.html) | common utilities for the Node-RED runtime and editor modules
@node-red/registry | the internal node registry
[@node-red/registry](@node-red_registry.html) | the internal node registry
@node-red/nodes | the default set of core nodes
@node-red/editor-client | the client-side resources of the Node-RED editor application

View File

@@ -1,3 +1,260 @@
### 1.3.5 Maintenance Release
Editor
- Open subflow tab next to active tab rather than at the end
- Shrink default notification box
- Support mousewheel scroll in tab bar
- Revert some of #2967 to fix treeList gutter width calculation
- Prevent unknown node from breaking editor
- Stop module with missing types from preventing editor load
- Handle sidebar tab that no longer exists when setting first active
- Fix plugin loading when browser sends unrecognised lang
- Prevent error whilst drag/drop importing from leaving dropTarget visible Fixes #2982
- Fix scaling issues when dragging nodes into scaled workspace
- Fix incorrect shortcut keys in info tips (#2980) @kazuhitoyokoi
- Reduce code duplication around node/label generation
- Fix theme handling when no editorTheme.page setting
- Fix jshint error in treeList
Runtime
- Fix error handling in runtime/lib/api/nodes
- Add Node 16 with sass fixed
- Migrate from node-sass to sass (#2984)
- Fix "installRetry" was declared a constant and changed (#2974) @aheissenberger
Nodes
- Function: Fix 'SyntaxError' in Function node when last line of on-stop is a comment
- Function: Fix Function tab label names in the node help text Closes #2978
- Function: Update Japanese info text of function node (#2985) @HiroyasuNishiyama
### 1.3.4 Maintenance Release
Editor
- Allow nodes to access resolved theme files Fixes #2968
- Fix importing node to currently flow rather than match its old z value
- Don't let 'escape' whilst moving nodes interrupt things Fixes #2960
- Sort context stores in TypedInput and ensure default first Fixes #2954
- Fix margin between nodes on palette (#2947) @kazuhitoyokoi
- Ensure typedInput option is selected in dropdown menu Part of #2945
- Ensure typedInput without value has focus class removed Closes #2945
- TreeList: Fix remove item when depth=0 and wrong gutter calc (#2967) @hanc2006
Runtime
- Handle subflow modules that contain subflows
- Timeout http upgrade requests that are not otherwise handled Fixes #2956
- Fix error on auto commit for no flow change (#2957) @HiroyasuNishiyama
Nodes
- CSV: Fix CSV handling of special chars as separators
- Delay: Give delay node random mina nd max more space so you can see complete value
- Exec: fix grunt fail on exec node test (#2964) @HiroyasuNishiyama
- Function: Ensure function expand button is above vertical scrollbar Fixes #2955
- Inject: Fix inject node output tooltip extra property count
### 1.3.3: Maintenance Release
Editor
- Fix package semver comparison to allow >1 version increment
- Prevent TypedInput label overflowing element Fixes #2941
- Remove TypedInput from tab focus when only one type available
- Make typedInput.disable more consistent in behaviour Fixes #2942
- Fix project credential secret reset handling Part of #2868
Runtime
- Export package version in Grunt file so docs template can access
Nodes
- CSV: ensure CSV node can send false as string
- HTTPIn: handle application/x-protobuf as Buffer type (#2935 #2938) @hardillb
- MQTT: Ensure mqtt-close message is published when closing mqtt nodes
### 1.3.2: Maintenance Release
Runtime
- Handle package.json without dependencies section
Editor
- Fix variable reference error in editableList Fixes #2933
- Fix handling of user-provided keymap Fixes #2926
- Ensure theme login image is passed through to api response Fixes #2929
- Add Japanese translations for Node-RED v1.3.1 (#2930) @kazuhitoyokoi
Nodes
- CSV: Fix CSV parsing with other than , separator
- File out: Fix timing of msg.send to be after close
- Function: describe `node.outputCount` in help text
- MQTT: Fix MQTT Broker TLS config row layout Fixes #2927
- Split: add comment to info re $N being number of messages arriving
### 1.3.1: Maintenance Release
Nodes
- Fix change node form validation
### 1.3.0: Milestone Release
Editor
- Remember TypedInput selected sub option when switching types Fixes #2896
- Show context store name on TypedInput flow/global types Fixes #2793
- Add core:go-to-selected-subflow action
- Ctrl-dbclick on subflow node opens subflow tab
- Add go-to-previous/next-location actions
- Fix copy-to-clipboard action in FireFox
- Fix select up/down stream when zoomed in or out
- Use cursor keys to change selection in workspace
- Prevent accidental text selection of subflow toolbar text
- Update node-sass to 5.x Fixes #2907
- Allow module to provide resources and automatically expose them (#2903) @knolleary
Runtime
- DE language updates (#2806 #2901 #2913) @heikokue
- Remove Node 8 from travis due to node-sass breakage
- Allow Flow.getNode to return subflowInstance nodes Related to #2898
- Fix credential lookup for nested subflows Fixes #2910
- Add externalModules config to settings.js
- Add Japanese translations for Node-RED v1.3.0 (#2900)
- Fix handling encrypted creds on /flows api
- Properly handle credentials passed to /flows api
- Fix line-number reporting in errors on node load (#2894) @HiroyasuNishiyama
Nodes
- Change: Add property validation to Change node rule set Closes #2911
- Exec: Allow any property to be appended to command (#2908) @kazuhitoyokoi
- HTTP Request: set followAllRedirects to work with POSTs Fixes #2017
- Inject: Flag validation errors in Inject node props config Fixes #2914
- Function: add node.outputCount to sandbox (#2918) @kristianheljas
- Switch: Fix Switch node handling of hasKey rule when property is undefined
- Switch: Handle invalid regex set dynamically in Switch node Fixes #2905
### 1.3.0-beta.1: Beta Release
Editor
- Add config node to refer to when exporting subflow
- Add confirm dialog when deleting subflow with instances in use (#2845) @knolleary
- Add easier ways to find subflow instances
- Add enable/disable toggle button for groups in info-outliner (#2844) @knolleary
- Add IE11 polyfill to support URI download scheme (#2871) @HiroyasuNishiyama
- Add Japanese translations for Node-RED v1.3.0 (#2874) @kazuhitoyokoi
- Add preview of exported nodes to Export dialog (#2820) @knolleary
- Add RED.plugins module to editor
- Add select-connected action (#2877) @knolleary
- Add select-up/downstream-nodes action to editor (#2877) @knolleary
- Add subflow edit button to palette tooltip
- Add subflow meta data edit pane
- Add support for library source plugins (#2785) @knolleary
- Adds shift-click support for selecting up/down stream nodes
- Allow default keymap to be overridden in settings file (#2843) @knolleary
- Allow EditableList to have custom buttons (#2881) @bartbutenaers
- Allow filtering of debug node output within subflow (#2870) @HiroyasuNishiyama
- Ensure the first sidebar tab is shown when editor loads (#2846) @knolleary
- Ensure TypedInput Change event is passed type/value properties Fixes #2883
- Escape all user input
- Filter palette manager nodes based on allow/deny list
- Fix check for existing config nodes in subflow export set
- Fix handling of + in shortcuts
- fix jshint failure (#2850) @HiroyasuNishiyama
- Fix keymap entries with multiple keys for same action
- fix line break of exporting nodes to clipboard (#2849) @HiroyasuNishiyama
- Fix line break of subflow label on palette (#2828)
- Fix loading individual module catalog
- Fix removing links when deleting node
- Fix semver comparison for IE11 (#2888) @knolleary
- fixed #2790 swapped description of encodeUrl/encodeUrlComponent and d… (#2791)
- Handle timeouts when trying to load node credentials in editor Fixes #2840 (#2841) @knolleary
- Hide projects dialog when opening proj with invalid encrypt key
- hide unused input field (#2823)
- Implement node property typing (#2812) @knolleary
- Improve SemVer comparison in Palette Manager (#2821 #2879) @HaKr
- Library: properly handle symlinked folders (#2768) @natcl
- make flow download code separate utility instead of polyfill
- Prevent duplicate keyboard shortcut from being assigned
- Prevent rogue mouseup on tab from triggering tab change
- Rename paletteEditorEnabled to installerEnabled
- Tidy some subflow env props css
- Tidy up typedInput syntax
- Use subflow.info for help text and meta.type for node type
Runtime
- Deprecate autoInstallModules for externalModules.autoInstall
- Deprecate editorTheme.palette.editable for externalModules.palette.allowInstall
- Initial plugin runtime api implementation (#2779) @knolleary
- Add initial support for ThemePlugins (#2836) @knolleary
- Support npm subflow modules (#2690) @knolleary
- Ability to add projects path to the settings file (#2816) @tfmf
- Add i18n function to editor plugins when they are registered
- Add optional 'lang' to settings file (#2796) @fellinga
- Add SubflowModule class for running subflow modules
- Add support for settings.externalModules (#2797) @knolleary
- Allow default project workflow to be set via settings (#2763) @knolleary
- Allow for adding an array of middleware functions (#2788) @kevinGodell
- Better logging when deprecated editorTheme.palette.* settings used
- Detect externalModule dependencies inside subflow modules
- Fix global leak in lib/flows/index.js
- Fix numeric status not displaying by ensuring it's a string (#2859) @knolleary
- Fully remove when.js dependency (#2772) @knolleary
- make nodes with only group change not deployed by nodes deploy mode
- Move exec and events components to util module
- Nodes log via parent flow to allow flow-info to be added
- Restart node only if node's group changes (#2872) @HiroyasuNishiyama
- Stop config nodes after flow nodes Fixes #2876 (#2880) @knolleary
- Update marked dependency
- Use more async funcs in runtime/lib/api to reduce Promise creation
- Use npm info to check pending install version
Nodes
- Allow nested msg properties in msg/flow/global expressions (#2822)
- Batch: Messaging API support in Batch node (#2738) @k-toumura
- CSV: Handle commas in msg.columns if quoted.
- CSV: Fix csv node template reset when array complete (#2854) @dceejay
- CSV: Messaging API support in CSV node (#2734) @k-toumura
- Debug: Sanitize Debug node name when display enable/disable message
- Delay: Add support for Messaging API to delay node (#2733)
- Exec: Add settings.execMaxBufferSize to control buffer size of exec node (#2819)
- Exec: Don't append msg.payload to command by default (#2818)
- Function: Add 'node' object to close scope
- Function: allow to load external modules (#2873) @knolleary
- Function: Add functionExternalModules to settings and default to false
- Join: Fix join node in array mode with repeated messages, and allow reset all (#2869) @dceejay
- MQTT: Add MQTT v5 support (#2778 #2886) @Steve-Mcl
- Sort: Messaging API support in Sort node (#2744) @k-toumura
- Split/Join: Messaging API support in Split/Join nodes (#2750) @k-toumura
- Trigger: Messaging API support in Trigger node (#2751) @k-toumura
- Add example flows for storage nodes (#2784) @HiroyasuNishiyama
- Add example flows for network nodes (#2855) @HiroyasuNishiyama
- Add example flows for parser nodes (#2749) @HiroyasuNishiyama
### 1.2.9: Maintenance Release
Editor
- Sanitize node type names when displaying in notifications
- Sanitize branch name before displaying in notification message
Runtime
- Handle more valid language codes when validating lang params Fixes #2856
### 1.2.8: Maintenance Release
Editor

View File

@@ -16,7 +16,7 @@
var path = require("path");
var fs = require("fs-extra");
var sass = require("node-sass");
var sass = require("sass");
module.exports = function(grunt) {
@@ -40,8 +40,11 @@ module.exports = function(grunt) {
if (nonHeadless) {
process.env.NODE_RED_NON_HEADLESS = true;
}
let packageFile = grunt.file.readJSON('package.json')
process.env.NODE_RED_PACKAGE_VERSION = packageFile.version;
grunt.initConfig({
pkg: grunt.file.readJSON('package.json'),
pkg: packageFile,
paths: {
dist: ".dist"
},
@@ -142,6 +145,7 @@ module.exports = function(grunt) {
"packages/node_modules/@node-red/editor-client/src/js/text/bidi.js",
"packages/node_modules/@node-red/editor-client/src/js/text/format.js",
"packages/node_modules/@node-red/editor-client/src/js/ui/state.js",
"packages/node_modules/@node-red/editor-client/src/js/plugins.js",
"packages/node_modules/@node-red/editor-client/src/js/nodes.js",
"packages/node_modules/@node-red/editor-client/src/js/font-awesome.js",
"packages/node_modules/@node-red/editor-client/src/js/history.js",
@@ -461,11 +465,13 @@ module.exports = function(grunt) {
'packages/node_modules/@node-red/runtime/lib/hooks.js',
'packages/node_modules/@node-red/util/**/*.js',
'packages/node_modules/@node-red/editor-api/lib/index.js',
'packages/node_modules/@node-red/editor-api/lib/auth/index.js'
'packages/node_modules/@node-red/editor-api/lib/auth/index.js',
'packages/node_modules/@node-red/registry/lib/index.js'
],
options: {
destination: 'docs',
configure: './jsdoc.json'
configure: './jsdoc.json',
fred: "hi there"
}
},
_editor: {

View File

@@ -1,6 +1,6 @@
{
"name": "node-red",
"version": "1.2.8",
"version": "1.3.5",
"description": "Low-code programming for event-driven applications",
"homepage": "http://nodered.org",
"license": "Apache-2.0",
@@ -27,7 +27,7 @@
],
"dependencies": {
"ajv": "6.12.6",
"async-mutex": "0.2.6",
"async-mutex": "0.3.1",
"basic-auth": "2.0.1",
"bcryptjs": "2.4.3",
"body-parser": "1.19.0",
@@ -53,16 +53,16 @@
"jsonata": "1.8.4",
"lodash.clonedeep": "^4.5.0",
"media-typer": "1.1.0",
"memorystore": "1.6.4",
"mime": "2.4.7",
"moment-timezone": "0.5.32",
"memorystore": "1.6.6",
"mime": "2.5.2",
"moment-timezone": "0.5.33",
"mqtt": "4.2.6",
"multer": "1.4.2",
"mustache": "4.1.0",
"mustache": "4.2.0",
"node-red-admin": "^0.2.6",
"node-red-node-rbe": "^0.2.9",
"node-red-node-rbe": "^0.5.0",
"node-red-node-sentiment": "^0.1.6",
"node-red-node-tail": "^0.1.0",
"node-red-node-tail": "^0.3.0",
"nopt": "5.0.0",
"oauth2orize": "1.11.0",
"on-headers": "1.0.2",
@@ -72,9 +72,8 @@
"raw-body": "2.4.1",
"request": "2.88.0",
"semver": "6.3.0",
"tar": "6.0.5",
"uglify-js": "3.12.4",
"when": "3.7.8",
"tar": "6.1.0",
"uglify-js": "3.13.3",
"ws": "6.2.1",
"xml2js": "0.4.23"
},
@@ -82,10 +81,10 @@
"bcrypt": "3.0.8"
},
"devDependencies": {
"dompurify": "2.2.6",
"dompurify": "2.2.7",
"grunt": "1.3.0",
"grunt-chmod": "~1.1.1",
"grunt-cli": "~1.3.2",
"grunt-cli": "~1.4.2",
"grunt-concurrent": "3.0.0",
"grunt-contrib-clean": "~2.0.0",
"grunt-contrib-compress": "1.6.0",
@@ -104,12 +103,12 @@
"grunt-simple-nyc": "^3.0.1",
"http-proxy": "1.18.1",
"jsdoc-nr-template": "github:node-red/jsdoc-nr-template",
"marked": "1.2.7",
"marked": "2.0.1",
"minami": "1.2.3",
"mocha": "^5.2.0",
"node-red-node-test-helper": "^0.2.6",
"node-sass": "^4.14.1",
"nodemon": "2.0.6",
"node-red-node-test-helper": "^0.2.7",
"nodemon": "2.0.7",
"sass": "1.32.12",
"should": "13.2.3",
"sinon": "1.17.7",
"stoppable": "^1.1.0",

View File

@@ -22,6 +22,7 @@ var flow = require("./flow");
var context = require("./context");
var auth = require("../auth");
var info = require("./settings");
var plugins = require("./plugins");
var apiUtil = require("../util");
@@ -32,6 +33,7 @@ module.exports = {
nodes.init(runtimeAPI);
context.init(runtimeAPI);
info.init(settings,runtimeAPI);
plugins.init(runtimeAPI);
var needsPermission = auth.needsPermission;
@@ -50,12 +52,14 @@ module.exports = {
// Nodes
adminApp.get("/nodes",needsPermission("nodes.read"),nodes.getAll,apiUtil.errorHandler);
if (!settings.editorTheme || !settings.editorTheme.palette || settings.editorTheme.palette.upload !== false) {
const multer = require('multer');
const upload = multer({ storage: multer.memoryStorage() });
adminApp.post("/nodes",needsPermission("nodes.write"),upload.single("tarball"),nodes.post,apiUtil.errorHandler);
} else {
adminApp.post("/nodes",needsPermission("nodes.write"),nodes.post,apiUtil.errorHandler);
if (!settings.externalModules || !settings.externalModules.palette || settings.externalModules.palette.allowInstall !== false) {
if (!settings.externalModules || !settings.externalModules.palette || settings.externalModules.palette.allowUpload !== false) {
const multer = require('multer');
const upload = multer({ storage: multer.memoryStorage() });
adminApp.post("/nodes",needsPermission("nodes.write"),upload.single("tarball"),nodes.post,apiUtil.errorHandler);
} else {
adminApp.post("/nodes",needsPermission("nodes.write"),nodes.post,apiUtil.errorHandler);
}
}
adminApp.get(/^\/nodes\/messages/,needsPermission("nodes.read"),nodes.getModuleCatalogs,apiUtil.errorHandler);
adminApp.get(/^\/nodes\/((@[^\/]+\/)?[^\/]+\/[^\/]+)\/messages/,needsPermission("nodes.read"),nodes.getModuleCatalog,apiUtil.errorHandler);
@@ -78,6 +82,10 @@ module.exports = {
adminApp.get("/settings",needsPermission("settings.read"),info.runtimeSettings,apiUtil.errorHandler);
// Plugins
adminApp.get("/plugins", needsPermission("plugins.read"), plugins.getAll, apiUtil.errorHandler);
adminApp.get("/plugins/messages", needsPermission("plugins.read"), plugins.getCatalogs, apiUtil.errorHandler);
return adminApp;
}
}

View File

@@ -33,6 +33,9 @@ module.exports = {
})
} else {
opts.lang = apiUtils.determineLangFromHeaders(req.acceptsLanguages());
if (/[^0-9a-z=\-\*]/i.test(opts.lang)) {
opts.lang = "en-US";
}
runtimeAPI.nodes.getNodeConfigs(opts).then(function(configs) {
res.send(configs);
})
@@ -60,6 +63,7 @@ module.exports = {
runtimeAPI.nodes.addModule(opts).then(function(info) {
res.json(info);
}).catch(function(err) {
console.log(err.stack);
apiUtils.rejectHandler(req,res,err);
})
},
@@ -91,6 +95,9 @@ module.exports = {
})
} else {
opts.lang = apiUtils.determineLangFromHeaders(req.acceptsLanguages());
if (/[^0-9a-z=\-\*]/i.test(opts.lang)) {
opts.lang = "en-US";
}
runtimeAPI.nodes.getNodeConfig(opts).then(function(result) {
return res.send(result);
}).catch(function(err) {
@@ -160,6 +167,9 @@ module.exports = {
lang: req.query.lng,
req: apiUtils.getRequestLogObject(req)
}
if (/[^0-9a-z=\-\*]/i.test(opts.lang)) {
opts.lang = "en-US";
}
runtimeAPI.nodes.getModuleCatalog(opts).then(function(result) {
res.json(result);
}).catch(function(err) {
@@ -174,6 +184,9 @@ module.exports = {
lang: req.query.lng,
req: apiUtils.getRequestLogObject(req)
}
if (/[^0-9a-z=\-\*]/i.test(opts.lang)) {
opts.lang = "en-US";
}
runtimeAPI.nodes.getModuleCatalogs(opts).then(function(result) {
res.json(result);
}).catch(function(err) {

View File

@@ -0,0 +1,44 @@
var apiUtils = require("../util");
var runtimeAPI;
module.exports = {
init: function(_runtimeAPI) {
runtimeAPI = _runtimeAPI;
},
getAll: function(req,res) {
var opts = {
user: req.user,
req: apiUtils.getRequestLogObject(req)
}
if (req.get("accept") == "application/json") {
runtimeAPI.plugins.getPluginList(opts).then(function(list) {
res.json(list);
})
} else {
opts.lang = apiUtils.determineLangFromHeaders(req.acceptsLanguages());
if (/[^0-9a-z=\-\*]/i.test(opts.lang)) {
opts.lang = "en-US";
}
runtimeAPI.plugins.getPluginConfigs(opts).then(function(configs) {
res.send(configs);
})
}
},
getCatalogs: function(req,res) {
var opts = {
user: req.user,
lang: req.query.lng,
req: apiUtils.getRequestLogObject(req)
}
if (/[^0-9a-z=\-\*]/i.test(opts.lang)) {
opts.lang = "en-US";
}
runtimeAPI.plugins.getPluginCatalogs(opts).then(function(result) {
res.json(result);
}).catch(function(err) {
console.log(err.stack);
apiUtils.rejectHandler(req,res,err);
})
}
};

View File

@@ -90,7 +90,7 @@ function getToken(req,res,next) {
return server.token()(req,res,next);
}
function login(req,res) {
async function login(req,res) {
var response = {};
if (settings.adminAuth) {
var mergedAdminAuth = Object.assign({}, settings.adminAuth, settings.adminAuth.module);
@@ -116,8 +116,9 @@ function login(req,res) {
response.prompts[0].image = theme.serveFile('/login/',mergedAdminAuth.strategy.image);
}
}
if (theme.context().login && theme.context().login.image) {
response.image = theme.context().login.image;
let themeContext = await theme.context();
if (themeContext.login && themeContext.login.image) {
response.image = themeContext.login.image;
}
}
res.json(response);

View File

@@ -75,8 +75,10 @@ module.exports = {
editorApp.get("/icons/:module/:icon",ui.icon);
editorApp.get("/icons/:scope/:module/:icon",ui.icon);
editorApp.get(/^\/resources\/((?:@[^\/]+\/)?[^\/]+)\/(.+)$/,ui.moduleResource);
var theme = require("./theme");
theme.init(settings);
theme.init(settings, runtimeAPI);
editorApp.use("/theme",theme.app());
editorApp.use("/",ui.editorResources);
@@ -93,6 +95,7 @@ module.exports = {
// Library
var library = require("./library");
library.init(runtimeAPI);
// editorApp.get("/library/:id",needsPermission("library.read"),library.getLibraryConfig);
editorApp.get(/^\/library\/([^\/]+)\/([^\/]+)(?:$|\/(.*))/,needsPermission("library.read"),library.getEntry);
editorApp.post(/^\/library\/([^\/]+)\/([^\/]+)\/(.*)/,needsPermission("library.write"),library.saveEntry);

View File

@@ -17,7 +17,6 @@
var apiUtils = require("../util");
var fs = require('fs');
var fspath = require('path');
var when = require('when');
var runtimeAPI;
@@ -25,6 +24,17 @@ module.exports = {
init: function(_runtimeAPI) {
runtimeAPI = _runtimeAPI;
},
// getLibraryConfig: function(req,res) {
// var opts = {
// user: req.user,
// library: req.params.id
// }
// runtimeAPI.library.getConfig(opts).then(function(result) {
// res.json(result);
// }).catch(function(err) {
// apiUtils.rejectHandler(req,res,err);
// });
// },
getEntry: function(req,res) {
var opts = {
user: req.user,

View File

@@ -41,7 +41,7 @@ module.exports = {
var namespace = req.params[0];
namespace = namespace.replace(/\.json$/,"");
var lang = req.query.lng || i18n.defaultLang; //apiUtil.determineLangFromHeaders(req.acceptsLanguages() || []);
if (/[^a-z\-\*]/i.test(lang)) {
if (/[^0-9a-z=\-\*]/i.test(lang)) {
res.json({});
return;
}

View File

@@ -41,6 +41,10 @@ var theme = null;
var themeContext = clone(defaultContext);
var themeSettings = null;
var activeTheme = null;
var activeThemeInitialised = false;
var runtimeAPI;
var themeApp;
function serveFile(app,baseUrl,file) {
@@ -58,7 +62,7 @@ function serveFile(app,baseUrl,file) {
}
}
function serveFilesFromTheme(themeValue, themeApp, directory) {
function serveFilesFromTheme(themeValue, themeApp, directory, baseDirectory) {
var result = [];
if (themeValue) {
var array = themeValue;
@@ -67,7 +71,14 @@ function serveFilesFromTheme(themeValue, themeApp, directory) {
}
for (var i=0;i<array.length;i++) {
var url = serveFile(themeApp,directory,array[i]);
let fullPath = array[i];
if (baseDirectory) {
fullPath = path.resolve(baseDirectory,array[i]);
if (fullPath.indexOf(baseDirectory) !== 0) {
continue;
}
}
var url = serveFile(themeApp,directory,fullPath);
if (url) {
result.push(url);
}
@@ -77,10 +88,12 @@ function serveFilesFromTheme(themeValue, themeApp, directory) {
}
module.exports = {
init: function(settings) {
init: function(settings, _runtimeAPI) {
runtimeAPI = _runtimeAPI;
themeContext = clone(defaultContext);
themeSettings = null;
theme = settings.editorTheme || {};
activeTheme = theme.theme;
},
app: function() {
@@ -116,6 +129,14 @@ module.exports = {
}
themeContext.page.title = theme.page.title || themeContext.page.title;
// Store the resolved urls to these resources so nodes (such as Debug)
// can access them
theme.page._ = {
css: themeContext.page.css,
scripts: themeContext.page.scripts,
favicon: themeContext.page.favicon
}
}
if (theme.header) {
@@ -169,7 +190,9 @@ module.exports = {
}
}
}
themeApp.get("/", function(req,res) {
themeApp.get("/", async function(req,res) {
const themePluginList = await runtimeAPI.plugins.getPluginsByType({type:"node-red-theme"});
themeContext.themes = themePluginList.map(theme => theme.id);
res.json(themeContext);
})
@@ -185,10 +208,46 @@ module.exports = {
themeSettings.projects = theme.projects;
}
if (theme.hasOwnProperty("keymap")) {
themeSettings.keymap = theme.keymap;
}
if (theme.theme) {
themeSettings.theme = theme.theme;
}
return themeApp;
},
context: function() {
context: async function() {
if (activeTheme && !activeThemeInitialised) {
const themePlugin = await runtimeAPI.plugins.getPlugin({
id:activeTheme
});
if (themePlugin) {
if (themePlugin.css) {
const cssFiles = serveFilesFromTheme(
themePlugin.css,
themeApp,
"/css/",
themePlugin.path
);
themeContext.page.css = cssFiles.concat(themeContext.page.css || [])
theme.page = theme.page || {_:{}}
theme.page._.css = cssFiles.concat(theme.page._.css || [])
}
if (themePlugin.scripts) {
const scriptFiles = serveFilesFromTheme(
themePlugin.scripts,
themeApp,
"/scripts/",
themePlugin.path
)
themeContext.page.scripts = scriptFiles.concat(themeContext.page.scripts || [])
theme.page = theme.page || {_:{}}
theme.page._.scripts = cssFiles.concat(theme.page._.scripts || [])
}
}
activeThemeInitialised = true;
}
return themeContext;
},
settings: function() {

View File

@@ -68,8 +68,30 @@ module.exports = {
apiUtils.rejectHandler(req,res,err);
})
},
editor: function(req,res) {
res.send(Mustache.render(editorTemplate,theme.context()));
moduleResource: function(req, res) {
let resourcePath = req.params[1];
let opts = {
user: req.user,
module: req.params[0],
path: resourcePath
}
runtimeAPI.nodes.getModuleResource(opts).then(function(data) {
if (data) {
var contentType = mime.getType(resourcePath);
res.set("Content-Type", contentType);
res.send(data);
} else {
res.status(404).end()
}
}).catch(function(err) {
console.log(err.stack);
apiUtils.rejectHandler(req,res,err);
})
},
editor: async function(req,res) {
res.send(Mustache.render(editorTemplate,await theme.context()));
},
editorResources: express.static(path.join(editorClientDir,'public'))
};

View File

@@ -28,7 +28,6 @@ var express = require("express");
var bodyParser = require("body-parser");
var util = require('util');
var passport = require('passport');
var when = require('when');
var cors = require('cors');
var auth = require("./auth");
@@ -60,8 +59,8 @@ function init(settings,_server,storage,runtimeAPI) {
adminApp.use(corsHandler);
if (settings.httpAdminMiddleware) {
if (typeof settings.httpAdminMiddleware === "function") {
adminApp.use(settings.httpAdminMiddleware)
if (typeof settings.httpAdminMiddleware === "function" || Array.isArray(settings.httpAdminMiddleware)) {
adminApp.use(settings.httpAdminMiddleware);
}
}
@@ -111,11 +110,9 @@ function init(settings,_server,storage,runtimeAPI) {
* @return {Promise} resolves when the application is ready to handle requests
* @memberof @node-red/editor-api
*/
function start() {
async function start() {
if (editor) {
return editor.start();
} else {
return when.resolve();
}
}
@@ -124,11 +121,10 @@ function start() {
* @return {Promise} resolves when the application is stopped
* @memberof @node-red/editor-api
*/
function stop() {
async function stop() {
if (editor) {
editor.stop();
}
return when.resolve();
}
module.exports = {
init: init,

View File

@@ -1,6 +1,6 @@
{
"name": "@node-red/editor-api",
"version": "1.2.8",
"version": "1.3.5",
"license": "Apache-2.0",
"main": "./lib/index.js",
"repository": {
@@ -16,23 +16,22 @@
}
],
"dependencies": {
"@node-red/util": "1.2.8",
"@node-red/editor-client": "1.2.8",
"@node-red/util": "1.3.5",
"@node-red/editor-client": "1.3.5",
"bcryptjs": "2.4.3",
"body-parser": "1.19.0",
"clone": "2.1.2",
"cors": "2.8.5",
"express-session": "1.17.1",
"express": "4.17.1",
"memorystore": "1.6.4",
"mime": "2.4.7",
"memorystore": "1.6.6",
"mime": "2.5.2",
"multer": "1.4.2",
"mustache": "4.1.0",
"mustache": "4.2.0",
"oauth2orize": "1.11.0",
"passport-http-bearer": "1.0.1",
"passport-oauth2-client-password": "0.1.2",
"passport": "0.4.1",
"when": "3.7.8",
"ws": "6.2.1"
},
"optionalDependencies": {

File diff suppressed because it is too large Load Diff

View File

@@ -1,23 +1,23 @@
{
"info" : {
"tip0" : "Sie können die ausgewählten Nodes oder Verbindungen mit {{ core:delete-selection }} entfernen.",
"tip1" : "Suche nach Nodes mit {{ core:search }}",
"tip2" : "{{ core:toggle-sidebar }} schaltet die Ansicht dieser Seitenleiste ein.",
"tip3" : "Sie können Ihre Palette von Nodes mit {{ core:manage-palette }} verwalten.",
"tip4" : "Ihre Flow-Konfigurations-Nodes werden in der Seitenleiste angezeigt. Es kann über das Menü oder mit {{ core:show-config-tab }} aufgerufen werden.",
"tip5" : "Aktiviert oder inaktiviert diese Tipps von der Option in den Einstellungen",
"tip6" : "Verschieben Sie die ausgewählten Nodes mit Hilfe der [left] [up] [down] und [right] Tasten. Halten Sie [Shift] gedrückt, um das Fenster weiter zu schieben",
"tip7" : "Wenn Sie einen Node auf eine Verbindung ziehen, wird er in die Verbindung eingefügt.",
"tip8" : "Die ausgewählten Nodes exportieren oder die aktuelle Registerkarte mit {{ core:show-export-dialog }}",
"tip9" : "Importieren Sie einen Flow, indem Sie sein JSON in den Editor ziehen oder mit {{ core:show-import-dialog }}.",
"tip10" : "[Umschalt] [Klicken] und ziehen Sie auf einen Node-Anschluss, um alle angeschlossenen Verbindungen oder nur die ausgewählte zu verschieben.",
"tip11" : "Die Registerkarte \"Info\" mit {{ core:show-info-tab }} oder der Registerkarte \"Debug\" mit {{ core:show-debug-tab }} anzeigen",
"tip12" : "[ctrl] [Klicken] in den Arbeitsbereich, um den Schnellhinzufügedialog zu öffnen.",
"tip13" : "Halten Sie [ctrl] gedrückt, wenn Sie auf einem Node-Anschluss klicken, um eine Schnellverbindung zu aktivieren.",
"tip14" : "Halten Sie [Umschalt] gedrückt, wenn Sie auf einen Node klicken, um auch alle verbundenen Nodes auszuwählen.",
"tip15" : "Halten Sie [ctrl] gedrückt, wenn Sie auf einen Node klicken, um ihn aus der aktuellen Auswahl hinzuzufügen oder zu entfernen.",
"tip16" : "Indexzungen wechseln mit {{ core:show-previous-tab }} und {{ core:show-next-tab }}",
"tip17" : "Sie können die Änderungen im Editierrahmen des Nodes mit {{ core:confirm-edit-tray }} bestätigen oder sie mit {{ core:cancel-edit-tray }} abbrechen.",
"tip18" : "Durch Drücken von {{ core:edit-selected-node }} wird der erste Node in der aktuellen Auswahl bearbeitet."
}
}
"info": {
"tip0": "Sie können die ausgewählten Nodes oder Verbindungen mit {{ core:delete-selection }} entfernen",
"tip1": "Sie können nach Nodes mit {{ core:search }} suchen",
"tip2": "{{ core:toggle-sidebar }} blendet die Seitenleiste ein/aus",
"tip3": "Sie können Ihre Node-Palette mit {{ core:manage-palette }} verwalten",
"tip4": "Ihre Flow-Konfigurationsnodes werden in der Seitenleiste angezeigt, die über das Menü oder mit {{ core:show-config-tab }} angezeigt werden kann",
"tip5": "Aktiviere oder deaktiviere diese Tipps in den Einstellungen im Tab 'Ansicht'",
"tip6": "Sie können die ausgewählten Nodes mit den [left]/[up]/[down]/[right]-Tasten verschieben. Wenn Sie dabei [Shift] gedrückt halten, können Sie den Fensterausschnitt verschieben.",
"tip7": "Wenn Sie ein Node auf eine Verbindung ziehen, wird es in die Verbindung eingefügt",
"tip8": "Sie können die ausgewählten Nodes oder den aktuellen Flow-Tab mit {{ core:show-export-dialog }} exportieren",
"tip9": "Sie können einen Flow importieren, indem Sie sein JSON in den Editor ziehen oder mittels {{ core:show-import-dialog }}",
"tip10": "Halten Sie [Shift] beim [Klicken] auf ein Node gedrückt, um auch alle verbundenen Nodes mit zu verschieben",
"tip11": "Sie können den Tab 'Info' mit {{ core:show-info-tab }} oder den Tab 'Debug' mit {{ core:show-debug-tab }} anzeigen lassen",
"tip12": "Halten Sie [Strg] beim [Klicken] in den Arbeitsbereich gedrückt, um den Schnellhinzufügedialog öffnen",
"tip13": "Halten Sie [Strg] beim [Klicken] auf einen Node-Anschluss gedrückt, um eine Verbindung nur durch kurzes [Klicken] (ohne Halten) zu verlegen",
"tip14": "Halten Sie [Shift] beim [Klicken] auf ein Node gedrückt, um auch alle verbundenen Nodes mit auszuwählen",
"tip15": "Halten Sie [Strg] beim [Klicken] auf ein Node gedrückt, um es zu der aktuellen Auswahl hinzuzufügen oder aus ihr zu entfernen",
"tip16": "Sie können die Flow-Tabs mit {{ core:show-previous-tab }} und {{ core:show-next-tab }} wechseln",
"tip17": "Sie können die Änderungen im Node-Editor mit {{ core:confirm-edit-tray }} bestätigen oder sie mit {{ core:cancel-edit-tray }} verwerfen",
"tip18": "Sie können mit {{ core:edit-selected-node }} den ersten Node in der aktuellen Auswahl bearbeiten"
}
}

View File

@@ -1,222 +1,274 @@
{
"$string" : {
"args" : "arg",
"desc" : "Transformiert den Parameter *arg* in eine Zeichenfolge mit den folgenden Transformationsregeln:\n\n -Zeichenfolgen bleiben unverändert\n -Funktionen werden in eine leere Zeichenfolge konvertiert\n -Numerische Unendlichkeit und NaN lösen einen Fehler aus, da sie nicht als JSON-Nummer dargestellt werden können.\n -Alle anderen Werte werden mit Hilfe der Funktion 'JSON.stringify' in eine JSON-Zeichenfolge konvertiert."
},
"$length" : {
"args" : "str",
"desc" : "Gibt die Anzahl der Zeichen in der Zeichenfolge `str` zurück. Es wird ein Fehler ausgelöst, wenn `str` keine Zeichenfolge ist."
},
"$substring" : {
"args" : "str, start [, länge]",
"desc" : "Gibt eine Zeichenfolge zurück, die die Zeichen im ersten Parameter `str` beginnend bei Position `start` (Null-Offset) enthält. Wenn \"length\" angegeben ist, enthält die Unterzeichenfolge maximal \"Länge\" Zeichen. Wenn `start` negativ ist, gibt es die Anzahl der Zeichen am Ende von `str` an."
},
"$substringBefore" : {
"args" : "str, chars",
"desc" : "Gibt die Unterzeichenfolge vor dem ersten Auftreten der Zeichenfolge `chars` in `str` zurück. Falls `str` nicht `chars` enthält, gibt es `str` zurück."
},
"$substringAfter" : {
"args" : "str, chars",
"desc" : "Gibt die Unterzeichenfolge nach dem ersten Auftreten der Zeichenfolge `chars` in `str` zurück. Falls `str` nicht `chars` enthält, gibt es `str` zurück."
},
"$uppercase" : {
"args" : "str",
"desc" : "Gibt eine Zeichenfolge mit allen Zeichen von `str` zurück, die in Großbuchstaben konvertiert werden."
},
"$lowercase" : {
"args" : "str",
"desc" : "Gibt eine Zeichenfolge mit allen Zeichen von `str` in Kleinbuchstaben zurück."
},
"$trim" : {
"args" : "str",
"desc" : "Normalisiert und trimmt alle Leerzeichen in `str` durch Anwenden der folgenden Schritte:\n\n -Alle Tabulatorstopps, Wagenrückläufe und Zeilenvorschübe werden durch Leerzeichen ersetzt.\n-Zusammenhängende Folgen von Räumen werden auf einen einzigen Raum reduziert.\n-Trailing und führende Plätze werden entfernt.\n\n Wenn 'str' nicht angegeben ist (d. h. Diese Funktion wird ohne Argumente aufgerufen), dann wird der Kontextwert als Wert von `str` verwendet. Es wird ein Fehler ausgelöst, wenn `str` keine Zeichenfolge ist."
},
"$contains" : {
"args" : "str, Muster",
"desc" : "Gibt `true` zurück, wenn `str` durch `Muster` abgeglichen wird, sonst gibt es `false` zurück. Wenn 'str' nicht angegeben ist (d. h. Diese Funktion wird mit einem Argument aufgerufen), dann wird der Kontextwert als Wert von `str` verwendet. Der Parameter 'Muster' kann entweder eine Zeichenfolge oder ein regulärer Ausdruck sein."
},
"$split" : {
"args" : "str [, Trennzeichen] [, Grenzwert]",
"desc" : "Teilt den Parameter 'str' in einem Array mit Unterzeichenfolgen. Es ist ein Fehler, wenn `str` keine Zeichenfolge ist. Der optionale Parameter 'Trennzeichen' gibt die Zeichen in der `str` an, um die es entweder als Zeichenfolge oder als regulärer Ausdruck geteilt werden soll. Wenn 'Trennzeichen' nicht angegeben wird, wird die leere Zeichenfolge angenommen, und `str` wird in ein Array aus einzelnen Zeichen aufgeteilt. Es handelt sich um einen Fehler, wenn `Trennzeichen' keine Zeichenfolge ist. Der optionale Parameter 'Grenzwert' ist eine Zahl, die die maximale Anzahl von Unterzeichenfolgen angibt, die in das resultierende Array eingeschlossen werden sollen. Alle zusätzlichen Unterzeichenfolgen werden gelöscht. Wenn 'Grenzwert' nicht angegeben wird, wird ' str ` vollständig geteilt, wobei die Größe des resultierenden Arrays nicht begrenzt ist. Es handelt sich um einen Fehler, wenn `Grenzwert' keine nicht negative Zahl ist."
},
"$join" : {
"args" : "array [, Trennzeichen]",
"desc" : "Verkettet ein Array von Komponentenzeichenfolgen in eine einzelne verkettete Zeichenfolge mit jeder Komponentenzeichenfolge, die durch den optionalen Parameter 'separator' getrennt ist. Es ist ein Fehler, wenn die Eingabe `Array` ein Element enthält, das keine Zeichenfolge ist. Wenn 'Trennzeichen' nicht angegeben wird, wird davon ausgegangen, dass es sich um eine leere Zeichenfolge handelt, d. h. Zwischen den Komponentenzeichenfolgen ist kein Trennzeichen vorhanden. Es handelt sich um einen Fehler, wenn `Trennzeichen' keine Zeichenfolge ist."
},
"$match" : {
"args" : "str, Muster [, Grenzwert]",
"desc" : "Wendet die Zeichenfolge `str` an den regulären Ausdruck `Muster` an und gibt ein Array von Objekten zurück, wobei jedes Objekt Informationen zu jedem Vorkommen einer Übereinstimmung in `str` enthält."
},
"$replace" : {
"args" : "str, Muster, Ersatz [, Grenzwert]",
"desc" : "Findet Vorkommen von `Muster` in `str` und ersetzt sie durch `Ersatz`.\n\nDer optionale Parameter 'Grenzwert' ist die maximale Anzahl an Ersetzungen."
},
"$now" : {
"args" : "",
"desc" : "Generiert einen Zeitstempel im ISO-8601-kompatiblen Format und gibt sie als Zeichenfolge zurück."
},
"$base64encode" : {
"args" : "Zeichenfolge",
"desc" : "Konvertiert eine ASCII-Zeichenfolge in eine Basis-64-Darstellung. Jedes Zeichen in der Zeichenfolge wird als Byte mit binären Daten behandelt. Dies setzt voraus, dass alle Zeichen in der Zeichenfolge im Bereich von 0x00 bis 0xFF liegen, der alle Zeichen in URI-codierten Zeichenfolgen enthält. Unicode-Zeichen außerhalb dieses Bereichs werden nicht unterstützt."
},
"$base64decode" : {
"args" : "Zeichenfolge",
"desc" : "Konvertiert die Basis-64-codierten Byte in eine Zeichenfolge unter Verwendung einer UTF-8-Unicode-Codepage."
},
"$number" : {
"args" : "arg",
"desc" : "Der Parameter 'arg' wird unter Verwendung der folgenden Regeln für das Casting in eine Zahl verwendet:\n\n -Zahlen bleiben unverändert\n -Zeichenfolgen, die eine Folge von Zeichen enthalten, die eine rechtliche JSON-Nummer darstellen, werden in diese Zahl konvertiert.\n -Alle anderen Werte bewirken, dass ein Fehler ausgelöst wird."
},
"$abs" : {
"args" : "Anzahl",
"desc" : "Gibt den absoluten Wert des Parameters 'Zahl' zurück."
},
"$floor" : {
"args" : "Anzahl",
"desc" : "Gibt den Wert von 'Zahl' auf die nächste ganze Zahl zurück, die kleiner oder gleich 'Zahl' ist."
},
"$ceil" : {
"args" : "Anzahl",
"desc" : "Gibt den Wert von 'Zahl' auf die nächste ganze Zahl zurück, die größer oder gleich 'Zahl' ist."
},
"$round" : {
"args" : "Zahl [, Genauigkeit]",
"desc" : "Gibt den Wert des Parameters `Zahl` zurück, der auf die Anzahl der Dezimalstellen gerundet wird, die durch den optionalen Parameter 'Genauigkeit' angegeben wird."
},
"$power" : {
"args" : "Basis, Exponent",
"desc" : "Gibt den Wert von `Basis` potenziert mit `Exponent` zurück."
},
"$sqrt" : {
"args" : "Zahl",
"desc" : "Gibt die Quadratwurzel des Werts des Parameters 'Zahl' zurück."
},
"$random" : {
"args" : "",
"desc" : "Gibt eine Pseudozufallszahl größer-gleich null und kleiner als eins zurück."
},
"$millis" : {
"args" : "",
"desc" : "Gibt die Anzahl der Millisekunden seit der Unix-Epoche (1. Januar 1970 (UTC)) als Zahl zurück. Alle Invocationen von `$millis ()` innerhalb einer Auswertung eines Ausdrucks geben alle denselben Wert zurück."
},
"$sum" : {
"args" : "Array",
"desc" : "Gibt die arithmetische Summe eines `Array` von Zahlen zurück. Es ist ein Fehler, wenn die Eingabe `Array` ein Element enthält, das keine Zahl ist."
},
"$max" : {
"args" : "Array",
"desc" : "Gibt die maximale Anzahl in einem `Array` von Zahlen zurück. Es ist ein Fehler, wenn die Eingabe `Array` ein Element enthält, das keine Zahl ist."
},
"$min" : {
"args" : "Array",
"desc" : "Gibt die minimale Zahl in einem `Array` von Zahlen zurück. Es ist ein Fehler, wenn die Eingabe `Array` ein Element enthält, das keine Zahl ist."
},
"$average" : {
"args" : "Array",
"desc" : "Gibt den Mittelwert eines `Array` von Zahlen zurück. Es ist ein Fehler, wenn die Eingabe `Array` ein Element enthält, das keine Zahl ist."
},
"$boolean" : {
"args" : "arg",
"desc" : "Castet das Argument mit den folgenden Regeln in einen Booleschen Wert:\n\n -` Boolean ': nicht geändert\n -` string `: leer: `false`\n -` string `: nicht leer: `true`\n -` Zahl `: ` 0 `: ` falsch `\n -` Zahl `: Nicht-Null: `true`\n -` null `: `false`\n -` array `: leer: `false`\n -` array `: enthält ein Mitglied, das auf `true` setzt: `true`\n -` array `: alle Member werden in `false` umgesetzt: `false`\n -` object `: empty: `false`\n -` object `: non-empty: `true`\n -` Funktion `: ` falsch `"
},
"$not" : {
"args" : "arg",
"desc" : "Gibt den Booleschen Wert NOT für das Argument zurück. `arg` wird zuerst in einen Booleschen Wert umgesetzt."
},
"$exists" : {
"args" : "arg",
"desc" : "Gibt den Booleschen Wert 'true' zurück, wenn der Ausdruck `arg` als Wert ausgewertet wird, oder 'false', wenn der Ausdruck nicht mit einem anderen Ausdruck übereinstimmt (z. B. ein Pfad zu einer nicht vorhandenen Feldreferenz)."
},
"$count" : {
"args" : "Array",
"desc" : "Gibt die Anzahl der Elemente in dem Array zurück."
},
"$append" : {
"args" : "Array, Array",
"desc" : "Hängen Sie zwei Arrays an."
},
"$sort" : {
"args" : "array [, Funktion]",
"desc" : "Gibt ein Array zurück, das alle Werte im Parameter 'array' enthält, aber in der Reihenfolge sortiert wird.\n\nWenn ein Vergleichsoperator 'function' angegeben wird, muss es sich um eine Funktion handeln, die zwei Parameter benötigt:\n\n` Funktion (links, rechts) `\n\nDiese Funktion wird durch den Sortieralgorithmus aufgerufen, um zwei Werte links und rechts zu vergleichen. Wenn der Wert von links nach dem Wert von rechts in der gewünschten Sortierreihenfolge platziert werden soll, muss die Funktion den Booleschen Wert 'true' zurückgeben, um einen Auslagerungsspeicher anzuzeigen. Andernfalls muss 'false' zurückgegeben werden."
},
"$reverse" : {
"args" : "Array",
"desc" : "Gibt ein Array zurück, das alle Werte aus dem Parameter 'array' enthält, aber in umgekehrter Reihenfolge."
},
"$shuffle" : {
"args" : "Array",
"desc" : "Gibt ein Array zurück, das alle Werte aus dem Parameter ` array ` enthält, aber in zufälliger Reihenfolge geschattiert ist."
},
"$zip" : {
"args" : "Array, ...",
"desc" : "Gibt ein konvolviertes (gezipptes) Array zurück, das gruppierte Arrays von Werten aus den Argumenten ` array1 ` ... ` arrayN ' aus Index 0, 1, 2 ... enthält."
},
"$keys" : {
"args" : "Objekt",
"desc" : "Gibt ein Array zurück, das die Schlüssel in dem Objekt enthält. Wenn es sich bei dem Argument um ein Array von Objekten handelt, enthält das zurückgegebene Array eine deduplizierte Liste aller Schlüssel in allen Objekten."
},
"$lookup" : {
"args" : "Objekt, Schlüssel",
"desc" : "Gibt den Wert zurück, der dem Schlüssel im Objekt zugeordnet ist. Wenn es sich bei dem ersten Argument um ein Array von Objekten handelt, werden alle Objekte im Array durchsucht, und die Werte, die mit allen Vorkommen des Schlüssels verknüpft sind, werden zurückgegeben."
},
"$spread" : {
"args" : "Objekt",
"desc" : "Teilt ein Objekt, das Schlüssel/Wert-Paare enthält, in ein Array von Objekten, von denen jedes ein einzelnes Schlüssel/Wert-Paar aus dem Eingabeobjekt hat. Wenn es sich bei dem Parameter um ein Array von Objekten handelt, enthält die resultierende Feldgruppe ein Objekt für jedes Schlüssel/Wert-Paar in jedem Objekt in der angegebenen Feldgruppe."
},
"$merge" : {
"args" : "array &lt;object&gt;",
"desc" : "Mischt ein Array von ` Objekten ` in ein einzelnes ` Objekt `, das alle Schlüssel/Wert-Paare aus jedem der Objekte in dem Eingabe-Array enthält. Wenn eines der Eingabeobjekte denselben Schlüssel enthält, enthält das zurückgegebene Objekt den Wert des letzten Objekts in der Feldgruppe. Es handelt sich um einen Fehler, wenn das Eingabe-Array ein Element enthält, das kein Objekt ist."
},
"$sift" : {
"args" : "Objekt, Funktion",
"desc" : "Gibt ein Objekt zurück, das nur die Schlüssel/Wert-Paare aus dem Parameter 'object' enthält, die die Prädikat ` funktion ' erfüllen, die als zweiter Parameter übergeben wird.\n\nDie Funktion ` function `, die als zweiter Parameter angegeben wird, muss die folgende Signatur aufweisen:\n\n` function (value [, key [, object]]) `"
},
"$each" : {
"args" : "Objekt, Funktion",
"desc" : "Gibt ein Array zurück, das die Werte enthält, die von der Funktion ` function ` zurückgegeben werden, wenn sie auf jedes Schlüssel/Wert-Paar im ` object ` angewendet werden."
},
"$map" : {
"args" : "Array, Funktion",
"desc" : "Gibt ein Array zurück, das die Ergebnisse der Anwendung des Parameters ` function ` auf jeden Wert im Parameter 'array' enthält.\n\nDie Funktion ` function `, die als zweiter Parameter angegeben wird, muss die folgende Signatur aufweisen:\n\n` function (value [, index [, array]]) `"
},
"$filter" : {
"args" : "Array, Funktion",
"desc" : "Gibt ein Array zurück, das nur die Werte im Parameter 'array' enthält, die das Prädikat ` funktion ` erfüllen.\n\nDie Funktion ` function `, die als zweiter Parameter angegeben wird, muss die folgende Signatur aufweisen:\n\n` function (value [, index [, array]]) `"
},
"$reduce" : {
"args" : "array, function [, init]",
"desc" : "Gibt einen aggregierten Wert zurück, der aus der Anwendung des Parameters ` function 'nacheinander auf jeden Wert in' array ` in Kombination mit dem Ergebnis der vorherigen Anwendung der Funktion angewendet wurde.\n\nDie Funktion muss zwei Argumente akzeptieren und verhält sich wie ein Infix-Operator zwischen jedem Wert innerhalb des ` Array `.\n\nDer optionale Parameter 'init' wird als Anfangswert in der Aggregation verwendet."
},
"$flowContext" : {
"args" : "Zeichenfolge [, Zeichenfolge]",
"desc" : "Ruft eine Flusskontexteigenschaft ab.\n\nDies ist eine definierte Funktion vom Typ \"Node-RED\"."
},
"$globalContext" : {
"args" : "Zeichenfolge [, Zeichenfolge]",
"desc" : "Ruft eine globale Kontexteigenschaft ab.\n\nDies ist eine definierte Funktion vom Typ \"Node-RED\"."
},
"$pad" : {
"args" : "string, width [, char]",
"desc" : "Gibt eine Kopie der ` Zeichenfolge ` mit zusätzlichen Aufenthalten zurück, falls erforderlich, so dass die Gesamtzahl der Zeichen mindestens der absolute Wert des Parameters 'width' ist.\n\nWenn ` width ` eine positive Zahl ist, wird die Zeichenfolge nach rechts aufgefüllt. Wenn sie negativ ist, wird sie nach links geplisften.\n\nDas optionale Argument 'char' gibt die Padding-Zeichen an, die verwendet werden sollen. Wenn keine Angabe gemacht wird, wird standardmäßig der Wert für das Leerzeichen angenommen."
},
"$fromMillis" : {
"args" : "Anzahl",
"desc" : "Konvertieren Sie eine Zahl, die Millisekunden seit der Unix-Epoche (1. Januar 1970 (UTC)) enthält in eine Zeitangabe im ISO 8601-Format."
},
"$formatNumber" : {
"args" : "Zahl, Bild [, Optionen]",
"desc" : "Transformiere die `Zahl` an eine Zeichenfolge und formatiert sie in eine dezimale Darstellung, wie in der 'Bild' -Zeichenfolge angegeben.\n\n Das Verhalten dieser Funktion ist mit der XPath/XQuery-Funktion fn:formatnummer konsistent, wie sie in der XPath F&O 3.1-Spezifikation definiert ist. Der Parameter für die Bildzeichenfolge definiert, wie die Zahl formatiert ist und hat die gleiche Syntax wie fn:format-number.\n\nDas optionale dritte Argument ` Optionen ` wird verwendet, um die standardmäßigen länderspezifischen Formatierungszeichen, wie z. B. das Dezimaltrennzeichen, zu überschreiben. Wenn dieses Argument angegeben wird, muss es sich um ein Objekt handeln, das Name/Wert-Paare enthält, die im Abschnitt mit dem Dezimalformat der XPath F&O 3.1-Spezifikation angegeben sind."
},
"$formatBase" : {
"args" : "Zahl [, Radix]",
"desc" : "Transformiere die `Zahl` in eine Zeichenfolge und formatiert sie in eine ganze Zahl, die in der durch das `radix` -Argument angegebenen Zahlenbasis dargestellt wird. Wenn 'radix' nicht angegeben wird, wird standardmäßig die Basis 10 verwendet. 'radix` kann zwischen 2 und 36 liegen, andernfalls wird ein Fehler ausgelöst."
},
"$toMillis" : {
"args" : "timestamp",
"desc" : "Konvertieren Sie eine Zeitangabe im ISO 8601-Format in die Anzahl der Millisekunden seit der Unix-Epoche (1. Januar 1970 (UTC)) als Zahl. Es wird ein Fehler ausgelöst, wenn die Zeichenfolge nicht das richtige Format hat."
},
"$env" : {
"args" : "arg",
"desc" : "Gibt den Wert einer Umgebungsvariablen zurück.\n\nDies ist eine definierte Funktion vom Typ \"Node-RED\"."
}
}
"$string": {
"args": "arg[, prettify]",
"desc": "Wandelt `arg` in eine Zeichenfolge um gemäß der folgenden Regeln:\n\n- Zeichenfolgen (string) bleiben unverändert\n- Funktionen werden in eine leere Zeichenfolge konvertiert\n- Numerische Unendlichkeit und NaN lösen einen Fehler aus, da sie nicht als JSON-Zahlenwert dargestellt werden können.\n- Alle anderen Werte werden mit Hilfe der Funktion `JSON.stringify` in eine JSON-Zeichenfolge konvertiert. Wenn `prettify` `true` ist, wird \"prettified\" JSON erzeugt. Z.B. Eine Zeile pro Feld und Zeilen werden eingeschoben basierend auf der Feldtiefe."
},
"$length": {
"args": "str",
"desc": "Gibt die Zeichenanzahl von `str` zurück. Es wird ein Fehler ausgelöst, wenn `str` keine Zeichenfolge ist."
},
"$substring": {
"args": "str, start [, length]",
"desc": "Gibt eine Teilzeichenfolge zurück, die die Zeichen in `str` beginnend bei Position `start` (Null-Offset) enthält. Wenn `length` vorgegeben ist, enthält die rückgegebene Zeichenfolge maximal die in `length` vorgegebene Zeichenanzahl. Wenn `start` negativ ist, werden die Zeichen vom Ende aus gezählt von `str` zurückgegeben."
},
"$substringBefore": {
"args": "str, chars",
"desc": "Gibt die Teilzeichenfolge vor dem ersten Auftreten der Zeichenfolge `chars` in `str` zurück. Falls `str` nicht `chars` enthält, wird `str` zurückgegeben."
},
"$substringAfter": {
"args": "str, chars",
"desc": "Gibt die Teilzeichenfolge nach dem ersten Auftreten der Zeichenfolge `chars` in `str` zurück. Falls `str` nicht `chars` enthält, wird `str` zurückgegeben."
},
"$uppercase": {
"args": "str",
"desc": "Gibt veränderten `str` zurück, bei dem allen Zeichen in Großbuchstaben umgewandelt wurden."
},
"$lowercase": {
"args": "str",
"desc": "Gibt veränderten `str` zurück, bei dem allen Zeichen in Kleinbuchstaben umgewandelt wurden."
},
"$trim": {
"args": "[str]",
"desc": "Normalisiert und trimmt alle Leerzeichen in `str` durch Anwenden der folgenden Schritte:\n\n- Alle Tabulatoren, Wagenrückläufe (returns) und Zeilenvorschübe (line feeds) werden durch Leerzeichen ersetzt.\n- Zusammenhängende Folgen von Leerzeichen werden auf ein einzelnes Leerzeichen reduziert.\n- Leerzeichen am Anfang und am Ende werden entfernt.\n\nWenn `str` nicht vorgegeben ist (d.h. diese Funktion wird ohne Parameter aufgerufen), dann wird der Kontextwert als Wert von `str` verwendet. Es wird ein Fehler ausgelöst, wenn `str` keine Zeichenfolge ist."
},
"$contains": {
"args": "str, pattern",
"desc": "Gibt `false` zurück, wenn `pattern` als Teilzeichenfolge in `str` enthalten ist, sonst gibt sie `false` zurück. Wenn `str` nicht vorgegeben ist (d. h. Diese Funktion wird mit einem Parameter aufgerufen), dann wird der Kontextwert als Wert von `str` verwendet. `pattern` kann entweder eine Zeichenfolge oder ein regulärer Ausdruck sein."
},
"$split": {
"args": "str [, separator] [, limit]",
"desc": "Teilt `str` in einem Array mit Teilzeichenfolgen. Es ergibt einen Fehler, wenn `str` keine Zeichenfolge ist.\n\nDer optionale Parameter `separator` gibt die Zeichen in der `str` an, anhand dem, vorgegeben entweder als Zeichenfolge oder als regulärer Ausdruck, `str` geteilt werden soll. Wenn `separator` nicht vorgegeben wird, wird ein leerer String als `separator` angenommen und `str` wird in ein Array aus einzelnen Zeichen aufgeteilt. Es handelt sich um einen Fehler, wenn `separator` leer ist.\n\nDer optionale Parameter `limit` ist eine Zahl, die die maximale Anzahl von Teilzeichenfolgen angibt, die in dem rückzugebenen Array enthalten sein sollen. Alle zusätzlichen Teilzeichenfolgen werden verworfen. Wenn `limit` nicht vorgegeben wird, wird `str` vollständig geteilt, wobei die Größe des resultierenden Arrays nicht begrenzt ist. Es handelt sich um einen Fehler, wenn `limit` eine negative Zahl ist."
},
"$join": {
"args": "array [, separator]",
"desc": "Verkettet ein Array von Zeichenfolgen zu einer einzigen Zeichenfolge, wobei die einzelnen Zeichenfolgen durch den optionalen Trennzeichen-Parameter `separator` getrennt sind. Es ergibt einen Fehler, wenn das `array` ein Element enthält, das keine Zeichenfolge ist. Wenn `separator` nicht vorgegeben wird, wird davon ausgegangen, dass es sich um eine leere Zeichenfolge handelt, d.h. zwischen den einzelnen Zeichenfolgen wird kein Trennzeichen eingefügt. Es handelt sich um einen Fehler, wenn `separator` keine Zeichenfolge ist."
},
"$match": {
"args": "str, pattern [, limit]",
"desc": "Wendet den regulären Ausdruck `pattern` auf die Zeichenfolge `str` an und gibt ein Array von Objekten zurück, die Informationen zu jedem Vorkommen von `pattern` in `str` enthält."
},
"$replace": {
"args": "str, pattern, replacement [, limit]",
"desc": "Findet Vorkommen von `pattern` in `str` und ersetzt sie durch `replacement`.\n\nDer optionale Parameter `limit` ist die maximale Anzahl an Ersetzungen."
},
"$now": {
"args": "",
"desc": "Generiert einen Zeitstempel im ISO-8601-kompatiblen Format und gibt sie als Zeichenfolge zurück."
},
"$base64encode": {
"args": "str",
"desc": "Konvertiert eine ASCII-Zeichenfolge `str` in eine Basis-64-Darstellung. Jedes Zeichen in `str` wird als Byte mit binären Daten behandelt. Dies setzt voraus, dass alle Zeichen in der Zeichenfolge im Bereich von 0x00 bis 0xFF liegen, der alle Zeichen in URI-codierten Zeichenfolgen enthält. Unicode-Zeichen außerhalb dieses Bereichs werden nicht unterstützt."
},
"$base64decode": {
"args": "str",
"desc": "Konvertiert den Basis-64-codierten `str` in eine Zeichenfolge unter Verwendung einer UTF-8-Unicode-Codepage."
},
"$number": {
"args": "arg",
"desc": "Wandelt `arg` unter Verwendung der folgenden Regeln in eine Zahl um:\n\n- Zahlen bleiben unverändert\n- Zeichenfolgen, die eine Folge von Zeichen enthalten, die einen echten JSON-Zahlenwert darstellen, werden in die entsprechende Zahl konvertiert.\n- Alle anderen Werte bewirken, dass ein Fehler ausgelöst wird."
},
"$abs": {
"args": "number",
"desc": "Gibt den absoluten Wert von `number` zurück."
},
"$floor": {
"args": "number",
"desc": "Gibt `number` abgerundet auf die nächste ganze Zahl zurück, die kleiner oder gleich `number` ist."
},
"$ceil": {
"args": "number",
"desc": "Gibt `number` aufgerundet auf die nächste ganze Zahl zurück, die größer oder gleich `number` ist."
},
"$round": {
"args": "number [, precision]",
"desc": "Gibt `number` gerundet auf die Anzahl der Nachkommastellen zurück, welche durch den optionalen Parameter `precision` vorgegeben ist."
},
"$power": {
"args": "base, exponent",
"desc": "Gibt `base` potenziert mit `exponent` zurück."
},
"$sqrt": {
"args": "number",
"desc": "Gibt die Quadratwurzel von `number` zurück."
},
"$random": {
"args": "",
"desc": "Gibt eine Pseudozufallszahl größer-gleich null und kleiner als eins zurück."
},
"$millis": {
"args": "",
"desc": "Gibt die aktuelle Anzahl der Millisekunden seit Beginn der Unix-Zeitrechnung (1. Januar 1970 UTC) als Zahl zurück. Alle Aufrufe von `$millis()` innerhalb der Auswertung eines Ausdrucks geben alle denselben Wert zurück."
},
"$sum": {
"args": "array",
"desc": "Gibt die arithmetische Summe eines `array` von Zahlen zurück. Es ergibt einen Fehler, wenn `array` ein Element enthält, das keine Zahl ist."
},
"$max": {
"args": "array",
"desc": "Gibt die größte Zahl von einem `array` von Zahlen zurück. Es ergibt einen Fehler, wenn `array` ein Element enthält, das keine Zahl ist."
},
"$min": {
"args": "array",
"desc": "Gibt die kleinste Zahl von einem `array` von Zahlen zurück. Es ergibt einen Fehler, wenn `array` ein Element enthält, das keine Zahl ist."
},
"$average": {
"args": "array",
"desc": "Gibt den Mittelwert eines `array` von Zahlen zurück. Es ergibt einen Fehler, wenn `array` ein Element enthält, das keine Zahl ist."
},
"$boolean": {
"args": "arg",
"desc": "Wandelt `arg` gemäß folgender Regeln in einen booleschen Wert um:\n\n- `Boolean`: unverändert\n- `string`: leer `false`, nicht leer `true`\n- `Zahl`: `0` → `falsch`, Nicht-Null `true`\n- `null` → `false`\n- `array`: leer `false`, enthält mindestens ein Element, das `true` ist → `true`, alle Elemente sind `false` `false`\n- `object`: leer → `false`, nicht leer → `true`\n- `function`: `false`"
},
"$not": {
"args": "arg",
"desc": "Gibt den invertierten booleschen Wert von `arg` zurück. `arg` wird zuerst in einen booleschen Wert umgesetzt."
},
"$exists": {
"args": "arg",
"desc": "Gibt den booleschen Wert `true` zurück, wenn der Ausdruck `arg` zu einem Wert ausgewertet wird, oder `false`, wenn der Ausdruck nicht mit einem anderen Ausdruck übereinstimmt (z.B. ein Pfad zu einer nicht vorhandenen Feldreferenz)."
},
"$count": {
"args": "array",
"desc": "Gibt die Anzahl der Elemente in dem Array `array` zurück."
},
"$append": {
"args": "array, array",
"desc": "Verkettet zwei Arrays miteinander."
},
"$sort": {
"args": "array [, function]",
"desc": "Gibt ein Array zurück, das alle Elemente vom `array` in sortierter Reihenfolge enthält.\n\nWenn ein Vergleichsoperator `function` vorgegeben wird, muss es sich um eine Funktion handeln, die zwei Parameter benötigt:\n\n`function(left, right)`\n\nDiese Funktion wird durch den Sortieralgorithmus aufgerufen, um zwei Elemente links und rechts zu vergleichen. Wenn das linke Element nach dem rechten in der gewünschten Sortierreihenfolge platziert werden soll, muss die Funktion den booleschen Wert `true` zurückgeben, um eine Vertauschung anzuzeigen. Andernfalls muss `false` zurückgegeben werden."
},
"$reverse": {
"args": "array",
"desc": "Gibt ein Array zurück, das alle Elemente vom `array` in umgekehrter Reihenfolge enthält."
},
"$shuffle": {
"args": "array",
"desc": "Gibt ein Array zurück, das alle Elemente vom `array` in zufälliger Reihenfolge enthält."
},
"$zip": {
"args": "array, ...",
"desc": "Gibt ein gepacktes (geziptes) Array zurück, das gruppierte Arrays der Elemente von `array1` ... `arrayN` aus Index 0, 1, 2 ... enthält."
},
"$keys": {
"args": "object",
"desc": "Gibt ein Array zurück, das die Schlüssel in dem Objekt `object` enthält. Wenn es sich bei dem Parameter um ein Array von Objekten handelt, enthält das zurückgegebene Array eine deduplizierte Liste aller Schlüssel in allen Objekten."
},
"$lookup": {
"args": "object, key",
"desc": "Gibt den Wert zurück, der dem Schlüssel `key` im Objekt `object` zugeordnet ist. Wenn es sich bei dem ersten Parameter um ein Array von Objekten handelt, werden alle Objekte im Array durchsucht, und die Werte, die mit allen Vorkommen des Schlüssels verknüpft sind, werden zurückgegeben."
},
"$spread": {
"args": "object",
"desc": "Teilt ein Objekt `object`, das Schlüssel/Wert-Paare enthält, in ein Array von Objekten, von denen jedes ein einzelnes Schlüssel/Wert-Paar aus dem Eingabeobjekt hat. Wenn es sich bei dem Parameter um ein Array von Objekten handelt, enthält die resultierende Feldgruppe ein Objekt für jedes Schlüssel/Wert-Paar in jedem Objekt in der vorgegebenen Feldgruppe."
},
"$merge": {
"args": "array&lt;object&gt;",
"desc": "Fügt ein Array von Objekt-Elementen `object` in ein einzelnes Objekt `object` zusammen, das alle Schlüssel/Wert-Paare aus jedem der Objekte in dem Ausgangs-Array enthält. Wenn eines der Ausgangs-Objekte denselben Schlüssel enthält, enthält das zurückgegebene Objekt den Wert des letzten Objekts des Arrays. Es handelt sich um einen Fehler, wenn das Ausgangs-Array ein Element enthält, das kein Objekt ist."
},
"$sift": {
"args": "object, function",
"desc": "Gibt ein Objekt zurück, das nur die Schlüssel/Wert-Paare aus dem Parameter `object` enthält, die die Prädikat `function` erfüllen, die als zweiter Parameter übergeben wird.\n\nDie Funktion `function`, die als zweiter Parameter vorgegeben wird, muss die folgende Signatur aufweisen:\n\n`function(value [, key [, object]])`"
},
"$each": {
"args": "object, function",
"desc": "Gibt ein Array zurück, das die Werte enthält, die von der Funktion `function` zurückgegeben werden, wenn sie auf jedes Schlüssel/Wert-Paar im `object` angewendet werden."
},
"$map": {
"args": "array, function",
"desc": "Gibt ein Array zurück, das die Ergebnisse von `function`, angewendet auf jedes Element von `array`, enthält.\n\nDie Funktion `function`, die als zweiter Parameter vorgegeben wird, muss die folgende Signatur aufweisen:\n\n`function(value [, index [, array]])`"
},
"$filter": {
"args": "array, function",
"desc": "Gibt ein Array zurück, das nur die Elemente von `array` enthält, die das Eigenschaft `function` erfüllen.\n\nDie Funktion `function`, die als zweiter Parameter vorgegeben wird, muss die folgende Signatur aufweisen:\n\n`function(value [, index [, array]])`"
},
"$reduce": {
"args": "array, function [, init]",
"desc": "Gibt einen aggregierten Wert zurück, der aus der Anwendung des Parameters `function` nacheinander auf jedes Element in `array` in Kombination mit dem Ergebnis der vorherigen Anwendung der Funktion angewendet wurde.\n\nDie Funktion muss zwei Parameter akzeptieren und verhält sich wie ein Infix-Operator zwischen jedem Element innerhalb des `array`.\n\nDer optionale Parameter `init` wird als Anfangswert in der Aggregation verwendet."
},
"$flowContext": {
"args": "str [, str]",
"desc": "Ruft eine Flow-Kontexteigenschaft ab.\n\nDies ist eine definierte Funktion vom Typ `Node-RED`."
},
"$globalContext": {
"args": "str [, str]",
"desc": "Ruft eine globale Kontexteigenschaft ab.\n\nDies ist eine definierte Funktion vom Typ `Node-RED`."
},
"$pad": {
"args": "str, width [, char]",
"desc": "Gibt eine aufgefüllte Kopie von `str` zurück, so dass (falls erforderlich) die Gesamtzahl der Zeichen mindestens dem absoluten Wert von `width` entspricht.\n\nWenn `width` eine positive Zahl ist, wird die Zeichenfolge nach rechts aufgefüllt. Wenn sie negativ ist, wird sie nach links aufgefüllt.\n\nDer optionale Parameter `char` gibt die Auffüll-Zeichen an, die verwendet werden sollen. Wenn keine Angabe gemacht wird, wird standardmäßig mit Leerzeichen aufgefüllt."
},
"$fromMillis": {
"args": "number",
"desc": "Konvertiert `number`, die die Millisekunden seit Beginn der Unix-Zeitrechnung (1. Januar 1970 UTC) enthält, in eine Zeitangabe im ISO 8601-Format."
},
"$formatNumber": {
"args": "number, picture [, options]",
"desc": "Wandelt `number` in eine Zeichenfolge um und formatiert sie in eine dezimale Darstellung, wie im `picture`-String-Parameter vorgegeben.\n\nDas Verhalten dieser Funktion ist mit der XPath/XQuery-Funktion fn:formatnummer konsistent, wie sie in der XPath F&O 3.1-Spezifikation definiert ist. Der `picture`-String-Parameter definiert, wie die Zahl formatiert ist und hat die gleiche Syntax wie fn:format-number.\n\nDer optionale dritte Parameter `options` wird verwendet, um die standardmäßigen länderspezifischen Formatierungszeichen, wie z.B. das Dezimaltrennzeichen, zu überschreiben. Wenn dieser Parameter vorgegeben wird, muss es sich um ein Objekt handeln, das Name/Wert-Paare enthält, die im Abschnitt mit dem Dezimalformat der XPath F&O 3.1-Spezifikation vorgegeben sind."
},
"$formatBase": {
"args": "number [, radix]",
"desc": "Wandelt `number` in eine Zeichenfolge um und formatiert sie in eine ganze Zahl, die in der durch den `radix`-Parameter vorgegebenen Zahlenbasis dargestellt wird. Wenn `radix` nicht vorgegeben wird, wird standardmäßig die Basis 10 verwendet. `radix` kann zwischen 2 und 36 liegen, andernfalls wird ein Fehler ausgelöst."
},
"$toMillis": {
"args": "timestamp",
"desc": "Konvertiert eine Zeitangabe `timestamp` im ISO 8601-Format in die Anzahl der Millisekunden seit Beginn der Unix-Zeitrechnung (1. Januar 1970 UTC). Es wird ein Fehler ausgelöst, wenn die Zeichenfolge nicht das richtige Format hat."
},
"$env": {
"args": "arg",
"desc": "Gibt den Wert einer Umgebungsvariablen zurück.\n\nDies ist eine definierte Funktion vom Typ `Node-RED`."
},
"$eval": {
"args": "expr [, context]",
"desc": "Analysiert (parse) und evaluiert den String `expr`, welcher JSON or a JSONata Ausdrücke enthält, unter Benutzung des aktuellen Kontextes für die Evaluierung."
},
"$formatInteger": {
"args": "number, picture",
"desc": "Wandelt `number` in eine Zeichenfolge um und formatiert sie in einer Ganzzahl-Darstellung, spezifiziert durch den `picture`-String-Parameter. Der `picture`-String-Parameter definiert, wie die Zahl `number` formatiert werden soll und hat den selben Syntax wie `fn:format-integer` der XPath F&O 3.1 Spezifikation."
},
"$parseInteger": {
"args": "str, picture",
"desc": "Wandelt den Inhalt von `str` in eine Ganzzahl `integer` (als JSON Zahl), spezifiziert durch den `picture`-String-Parameter. Der `picture`-String-Parameter hat das selbe Format wie `$formatInteger`."
},
"$error": {
"args": "[str]",
"desc": "Erzeugt eine Fehlermeldung. Der optionale String `str` ersetzt die Standardmeldung `$error() function evaluated`."
},
"$assert": {
"args": "arg, str",
"desc": "Wenn `arg` gleich `true` ist, liefert die Function `undefined` zurück. Wenn `arg` gleich `false` ist, wird ein Ausnahmefehler gemeldet mit dem String_Parameter `str` als Meldetext."
},
"$single": {
"args": "array, function",
"desc": "Gibt ein einziges Element aus `array` zurück, welches die Bedingung `function` erfüllt (d.h. die Funktion `function` gibt den booleschen Wert `true` zurück, wenn das Element übergeben werden soll). Sie meldet einen Ausnahmefehler, wenn die Anzahl der Elemente mit erfüllter Bedingung (`function` ist `true`) nicht genau eins ist.\n\nDie Funktion `function` sollte in der folgenden Art vorgegeben werden: `function(value [, index [, array]])` wobei `value` für jedes Element des Arrays gilt, `index` ist die Position dieses Elements und das gesamte Array `array` wird als dritter Parameter übergeben."
},
"$encodeUrlComponent": {
"args": "str",
"desc": "Kodiert eine URL-Komponente (Uniform Resource Locator), indem jedes Vorkommen bestimmter Zeichen durch eine, zwei, drei oder vier Escape-Sequenzen ersetzt wird, die die UTF-8-Kodierung des Zeichens darstellen.\n\nBeispiel: `$encodeUrlComponent(\"?x=test\")` => `\"%3Fx%3Dtest\"`"
},
"$encodeUrl": {
"args": "str",
"desc": "Kodiert eine URL (Uniform Resource Locator), indem jedes Vorkommen bestimmter Zeichen durch eine, zwei, drei oder vier Escape-Sequenzen ersetzt wird, die die UTF-8-Kodierung des Zeichens darstellen.\n\nBeispiel: `$encodeUrl(\"https://mozilla.org/?x=шеллы\")` => `\"https://mozilla.org/?x=%D1%88%D0%B5%D0%BB%D0%BB%D1%8B\"`"
},
"$decodeUrlComponent": {
"args": "str",
"desc": "Dekodiert eine URL-Komponente (Uniform Resource Locator) zuvor erzeugt von encodeUrlComponent.\n\nBeispiel: `$decodeUrlComponent(\"%3Fx%3Dtest\")` => `\"?x=test\"`"
},
"$decodeUrl": {
"args": "str",
"desc": "Dekodiert eine URL (Uniform Resource Locator) zuvor erzeugt von encodeUrl.\n\nBeispiel: `$decodeUrl(\"https://mozilla.org/?x=%D1%88%D0%B5%D0%BB%D0%BB%D1%8B\")` => `\"https://mozilla.org/?x=шеллы\"`"
},
"$distinct": {
"args": "array",
"desc": "Liefert ein `array` zurück, bei dem doppelte Elemente entfernt wurden."
},
"$type": {
"args": "value",
"desc": "Liefert den Typ von `value` als String. When `value` undefiniert ist, wird `undefined` zurückgeliefert."
},
"$moment": {
"args": "[str]",
"desc": "Liefert ein `date` Objekt unter Benutzung der Moment Library."
}
}

View File

@@ -38,12 +38,14 @@
}
},
"event": {
"loadPlugins": "Loading Plugins",
"loadPalette": "Loading Palette",
"loadNodeCatalogs": "Loading Node catalogs",
"loadNodes": "Loading Nodes __count__",
"loadFlows": "Loading Flows",
"importFlows": "Adding Flows to workspace",
"importError": "<p>Error adding flows</p><p>__message__</p>"
"importError": "<p>Error adding flows</p><p>__message__</p>",
"loadingProject": "Loading project"
},
"workspace": {
"defaultName": "Flow __number__",
@@ -142,6 +144,7 @@
"nodeActionDisabled": "node actions disabled",
"nodeActionDisabledSubflow": "node actions disabled within subflow",
"missing-types": "<p>Flows stopped due to missing node types.</p>",
"missing-modules": "<p>Flows stopped due to missing modules.</p>",
"safe-mode":"<p>Flows stopped in safe mode.</p><p>You can modify your flows and deploy the changes to restart.</p>",
"restartRequired": "Node-RED must be restarted to enable upgraded modules",
"credentials_load_failed": "<p>Flows stopped as the credentials could not be decrypted.</p><p>The flow credential file is encrypted, but the project's encryption key is missing or invalid.</p>",
@@ -337,8 +340,21 @@
"output": "outputs:",
"status": "status node",
"deleteSubflow": "delete subflow",
"confirmDelete": "Are you sure you want to delete this subflow?",
"info": "Description",
"category": "Category",
"module": "Module",
"license": "License",
"licenseNone": "none",
"licenseOther": "Other",
"type": "Node Type",
"version": "Version",
"versionPlaceholder": "x.y.z",
"keys": "Keywords",
"keysPlaceholder": "Comma-separated keywords",
"author": "Author",
"authorPlaceholder": "Your Name <email@example.com>",
"desc": "Description",
"env": {
"restore": "Restore to subflow default",
"remove": "Remove environment variable"
@@ -385,6 +401,7 @@
"icon": "Icon",
"inputType": "Input type",
"selectType": "select types...",
"loadCredentials": "Loading node credentials",
"inputs" : {
"input": "input",
"select": "select",
@@ -419,7 +436,8 @@
},
"errors": {
"scopeChange": "Changing the scope will make it unavailable to nodes in other flows that use it",
"invalidProperties": "Invalid properties:"
"invalidProperties": "Invalid properties:",
"credentialLoadFailed": "Failed to load node credentials"
}
},
"keyboard": {
@@ -841,7 +859,8 @@
}
},
"editableList": {
"add": "add"
"add": "add",
"addTitle": "add an item"
},
"search": {
"empty": "No matches found",
@@ -1079,6 +1098,7 @@
"editor-tab": {
"properties": "Properties",
"envProperties": "Environment Variables",
"module": "Module Properties",
"description": "Description",
"appearance": "Appearance",
"preview": "UI Preview",

View File

@@ -243,19 +243,19 @@
"args": "array, function",
"desc": "Returns the one and only value in the `array` parameter that satisfies the `function` predicate (i.e. the `function` returns Boolean `true` when passed the value). Throws an exception if the number of matching values is not exactly one.\n\nThe function should be supplied in the following signature: `function(value [, index [, array]])` where value is each input of the array, index is the position of that value and the whole array is passed as the third argument"
},
"$encodeUrl": {
"$encodeUrlComponent": {
"args": "str",
"desc": "Encodes a Uniform Resource Locator (URL) component by replacing each instance of certain characters by one, two, three, or four escape sequences representing the UTF-8 encoding of the character.\n\nExample: `$encodeUrlComponent(\"?x=test\")` => `\"%3Fx%3Dtest\"`"
},
"$encodeUrlComponent": {
"$encodeUrl": {
"args": "str",
"desc": "Encodes a Uniform Resource Locator (URL) by replacing each instance of certain characters by one, two, three, or four escape sequences representing the UTF-8 encoding of the character. \n\nExample: `$encodeUrl(\"https://mozilla.org/?x=шеллы\")` => `\"https://mozilla.org/?x=%D1%88%D0%B5%D0%BB%D0%BB%D1%8B\"`"
},
"$decodeUrl": {
"$decodeUrlComponent": {
"args": "str",
"desc": "Decodes a Uniform Resource Locator (URL) component previously created by encodeUrlComponent. \n\nExample: `$decodeUrlComponent(\"%3Fx%3Dtest\")` => `\"?x=test\"`"
},
"$decodeUrlComponent": {
"$decodeUrl": {
"args": "str",
"desc": "Decodes a Uniform Resource Locator (URL) previously created by encodeUrl. \n\nExample: `$decodeUrl(\"https://mozilla.org/?x=%D1%88%D0%B5%D0%BB%D0%BB%D1%8B\")` => `\"https://mozilla.org/?x=шеллы\"`"
},

View File

@@ -38,12 +38,14 @@
}
},
"event": {
"loadPlugins": "プラグインを読み込み中",
"loadPalette": "パレットを読み込み中",
"loadNodeCatalogs": "ノードカタログを読み込み中",
"loadNodes": "ノードを読み込み中 __count__",
"loadFlows": "フローを読み込み中",
"importFlows": "ワークスペースにフローを追加中",
"importError": "<p>フロー追加エラー</p><p>__message__</p>"
"importError": "<p>フロー追加エラー</p><p>__message__</p>",
"loadingProject": "プロジェクトを読み込み中"
},
"workspace": {
"defaultName": "フロー __number__",
@@ -85,7 +87,7 @@
"userSettings": "ユーザ設定",
"nodes": "ノード",
"displayStatus": "ノードのステータスを表示",
"displayConfig": "ノードの設定",
"displayConfig": "設定ノード",
"import": "読み込み",
"export": "書き出し",
"search": "ノードを検索",
@@ -142,6 +144,7 @@
"nodeActionDisabled": "ノードのアクションは無効になっています",
"nodeActionDisabledSubflow": "ノードのアクションは、サブフロー内で無効になっています",
"missing-types": "<p>不明なノードが存在するため、フローを停止しました。</p>",
"missing-modules": "<p>不明なモジュールが存在するため、フローを停止しました。</p>",
"safe-mode": "<p>セーフモードでフローを停止しました</p><p>フローを変更し、再起動するために変更をデプロイできます</p>",
"restartRequired": "更新されたモジュールを有効化するため、Node-REDを再起動する必要があります",
"credentials_load_failed": "<p>認証情報を復号できないため、フローを停止しました</p><p>フローの認証情報ファイルは暗号化されています。しかし、プロジェクトの暗号鍵が存在しない、または不正です</p>",
@@ -203,8 +206,8 @@
"replacedNodes_plural": "置換された __count__ 個のノード",
"pasteNodes": "JSON形式のフローデータを貼り付け",
"selectFile": "読み込むファイルを選択",
"importNodes": "フローをクリップボードから読み込み",
"exportNodes": "フローをクリップボードへ書き出し",
"importNodes": "フローを読み込み",
"exportNodes": "フローを書き出し",
"download": "ダウンロード",
"importUnrecognised": "認識できない型が読み込まれました:",
"importUnrecognised_plural": "認識できない型が読み込まれました:",
@@ -266,7 +269,7 @@
"successfulDeploy": "デプロイが成功しました",
"successfulRestart": "フローの再起動が成功しました",
"deployFailed": "デプロイが失敗しました: __message__",
"unusedConfigNodes": "使われていないノードの設定」があります。",
"unusedConfigNodes": "使われていない設定ノードがあります。",
"unusedConfigNodesLink": "設定を参照する",
"errors": {
"noResponse": "サーバの応答がありません"
@@ -337,8 +340,21 @@
"output": "出力:",
"status": "ステータスノード",
"deleteSubflow": "サブフローを削除",
"confirmDelete": "このサブフローを削除しても良いですか?",
"info": "詳細",
"category": "カテゴリ",
"module": "モジュール",
"license": "ライセンス",
"licenseNone": "なし",
"licenseOther": "その他",
"type": "ノードの型",
"version": "バージョン",
"versionPlaceholder": "x.y.z",
"keys": "キーワード",
"keysPlaceholder": "カンマ区切りのキーワード",
"author": "作者",
"authorPlaceholder": "名前 <email@example.com>",
"desc": "説明",
"env": {
"restore": "デフォルト値に戻す",
"remove": "環境変数を削除"
@@ -362,9 +378,9 @@
"configDelete": "削除",
"nodesUse": "__count__ 個のノードが、この設定を使用しています",
"nodesUse_plural": "__count__ 個のノードが、この設定を使用しています",
"addNewConfig": "新規に __type__ ノードの設定を追加",
"addNewConfig": "新規に __type__ 設定ノードを追加",
"editNode": "__type__ ノードを編集",
"editConfig": "__type__ ノードの設定を編集",
"editConfig": "__type__ 設定ノードを編集",
"addNewType": "新規に __type__ を追加...",
"nodeProperties": "プロパティ",
"label": "ラベル",
@@ -385,6 +401,7 @@
"icon": "記号",
"inputType": "入力形式",
"selectType": "形式選択...",
"loadCredentials": "ノードの認証情報を読み込み中",
"inputs": {
"input": "入力",
"select": "メニュー",
@@ -419,7 +436,8 @@
},
"errors": {
"scopeChange": "スコープの変更は、他のフローで使われているノードを無効にします",
"invalidProperties": "プロパティが不正です:"
"invalidProperties": "プロパティが不正です:",
"credentialLoadFailed": "ノードの認証情報の読み込みに失敗"
}
},
"keyboard": {
@@ -506,8 +524,8 @@
"title": "パレットの管理",
"palette": "パレット",
"times": {
"seconds": "秒前",
"minutes": "分前",
"seconds": "秒前",
"minutes": "分前",
"minutesV": "__count__ 分前",
"hoursV": "__count__ 時間前",
"hoursV_plural": "__count__ 時間前",
@@ -637,8 +655,8 @@
"noHelp": "ヘルプのトピックが未選択"
},
"config": {
"name": "ノードの設定を表示",
"label": "ノードの設定",
"name": "設定ノードを表示",
"label": "設定ノード",
"global": "全てのフロー上",
"none": "なし",
"subflows": "サブフロー",
@@ -841,7 +859,8 @@
}
},
"editableList": {
"add": "追加"
"add": "追加",
"addTitle": "要素を追加"
},
"search": {
"empty": "一致したものが見つかりませんでした",
@@ -1079,6 +1098,7 @@
"editor-tab": {
"properties": "プロパティ",
"envProperties": "環境変数",
"module": "モジュールプロパティ",
"description": "説明",
"appearance": "外観",
"preview": "UIプレビュー",

View File

@@ -243,19 +243,19 @@
"args": "array, function",
"desc": "`array`の要素のうち、条件判定関数`function`を満たす(`function`に与えた場合に真偽値`true`を返す)要素が1つのみである場合、それを返します。マッチする要素が1つのみでない場合、例外を送出します。\n\n指定する関数は`function(value [, index [, array]])`というシグネチャでなければなりません。ここで、`value`は`array`の要素値、`index`は要素の添字、第三引数には配列全体を渡します。"
},
"$encodeUrl": {
"$encodeUrlComponent": {
"args": "str",
"desc": "Uniform Resource Locator (URL)を構成する文字を1、2、3、もしくは、4文字エスケープシーケンスのUTF-8文字エンコーディングで置換します。\n\n例: `$encodeUrlComponent(\"?x=test\")` => `\"%3Fx%3Dtest\"`"
},
"$encodeUrlComponent": {
"$encodeUrl": {
"args": "str",
"desc": "Uniform Resource Locator (URL)要素を構成する文字を1、2、3、もしくは、4文字エスケープシーケンスのUTF-8文字エンコーディングで置換します。\n\n例: `$encodeUrl(\"https://mozilla.org/?x=шеллы\")` => `\"https://mozilla.org/?x=%D1%88%D0%B5%D0%BB%D0%BB%D1%8B\"`"
},
"$decodeUrl": {
"$decodeUrlComponent": {
"args": "str",
"desc": "encodeUrlComponentで置換したUniform Resource Locator (URL)をデコードします。\n\n例: `$decodeUrlComponent(\"%3Fx%3Dtest\")` => `\"?x=test\"`"
},
"$decodeUrlComponent": {
"$decodeUrl": {
"args": "str",
"desc": "encodeUrlで置換したUniform Resource Locator (URL)要素をデコードします。 \n\n例: `$decodeUrl(\"https://mozilla.org/?x=%D1%88%D0%B5%D0%BB%D0%BB%D1%8B\")` => `\"https://mozilla.org/?x=шеллы\"`"
},

View File

@@ -243,19 +243,19 @@
"args": "array, function",
"desc": "Возвращает одно-единственное значение из массива `array`, которое удовлетворяет предикату `function` (то есть когда `function` возвращает логическое `true` при передаче значения). Выдает исключение, если число подходящих значений не одно.\n\nФункция должна соответствовать следующей сигнатуре: `function(value [, index [, array]])` где value - элемент массива, index - позиция этого значения, а весь массив передается в качестве третьего аргумента"
},
"$encodeUrl": {
"$encodeUrlComponent": {
"args": "str",
"desc": "Кодирует компонент Uniform Resource Locator (URL), заменяя каждый экземпляр определенных символов одной, двумя, тремя или четырьмя escape-последовательностями, представляющими кодировку UTF-8 символа.\n\nПример: `$encodeUrlComponent(\"?x=test\")` => `\"%3Fx%3Dtest\"`"
},
"$encodeUrlComponent": {
"$encodeUrl": {
"args": "str",
"desc": "Кодирует Uniform Resource Locator (URL), заменяя каждый экземпляр определенных символов одной, двумя, тремя или четырьмя escape-последовательностями, представляющими кодировку UTF-8 символа.\n\nПример: `$encodeUrl(\"https://mozilla.org/?x=шеллы\")` => `\"https://mozilla.org/?x=%D1%88%D0%B5%D0%BB%D0%BB%D1%8B\"`"
},
"$decodeUrl": {
"$decodeUrlComponent": {
"args": "str",
"desc": "Декодирует компонент Uniform Resource Locator (URL), ранее созданный с помощью encodeUrlComponent.\n\nПример: `$decodeUrlComponent(\"%3Fx%3Dtest\")` => `\"?x=test\"`"
},
"$decodeUrlComponent": {
"$decodeUrl": {
"args": "str",
"desc": "Декодирует компонент Uniform Resource Locator (URL), ранее созданный с помощью encodeUrl. \n\nПример: `$decodeUrl(\"https://mozilla.org/?x=%D1%88%D0%B5%D0%BB%D0%BB%D1%8B\")` => `\"https://mozilla.org/?x=шеллы\"`"
},

View File

@@ -243,19 +243,19 @@
"args": "array, function",
"desc": "返回满足参数function谓语的array参数中的唯一值 (比如传递值时函数返回布尔值“true”)。如果匹配值的数量不唯一时,则抛出异常。\n\n应在以下签名中提供函数 `functionvalue [index [array []]]` 其中value是数组的每个输入index是该值的位置整个数组作为第三个参数传递。"
},
"$encodeUrl": {
"$encodeUrlComponent": {
"args": "str",
"desc": "通过用表示字符的UTF-8编码的一个两个三个或四个转义序列替换某些字符的每个实例对统一资源定位符URL组件进行编码。\n\n示例 `$encodeUrlComponent(\"?x=test\")` => `\"%3Fx%3Dtest\"`"
},
"$encodeUrlComponent": {
"$encodeUrl": {
"args": "str",
"desc": "通过用表示字符的UTF-8编码的一个两个三个或四个转义序列替换某些字符的每个实例对统一资源定位符URL进行编码。\n\n示例 `$encodeUrl(\"https://mozilla.org/?x=шеллы\")` => `\"https://mozilla.org/?x=%D1%88%D0%B5%D0%BB%D0%BB%D1%8B\"`"
},
"$decodeUrl": {
"$decodeUrlComponent": {
"args": "str",
"desc": "解码以前由encodeUrlComponent创建的统一资源定位器URL组件。 \n\n示例 `$decodeUrlComponent(\"%3Fx%3Dtest\")` => `\"?x=test\"`"
},
"$decodeUrlComponent": {
"$decodeUrl": {
"args": "str",
"desc": "解码先前由encodeUrl创建的统一资源定位符URL。 \n\n示例 `$decodeUrl(\"https://mozilla.org/?x=%D1%88%D0%B5%D0%BB%D0%BB%D1%8B\")` => `\"https://mozilla.org/?x=шеллы\"`"
},

View File

@@ -243,19 +243,19 @@
"args": "array, function",
"desc": "返回滿足參數function謂語的array參數中的唯一值 (比如傳遞值時函數返回布林值“true”)。如果匹配值的數量不唯一時,則拋出異常。\n\n應在以下簽名中提供函數`functionvalue [index [array []]]`其中value是數組的每個輸入index是該值的位置整個數組作為第三個參數傳遞。"
},
"$encodeUrl": {
"$encodeUrlComponent": {
"args": "str",
"desc": "通過用表示字符的UTF-8編碼的一個兩個三個或四個轉義序列替換某些字符的每個實例對統一資源定位符URL組件進行編碼。\n\n示例`$encodeUrlComponent(\"?x=test\")` => `\"%3Fx%3Dtest\"`"
},
"$encodeUrlComponent": {
"$encodeUrl": {
"args": "str",
"desc": "通過用表示字符的UTF-8編碼的一個兩個三個或四個轉義序列替換某些字符的每個實例對統一資源定位符URL進行編碼。\n\n示例 `$encodeUrl(\"https://mozilla.org/?x=шеллы\")` => `\"https://mozilla.org/?x=%D1%88%D0%B5%D0%BB%D0%BB%D1%8B\"`"
},
"$decodeUrl": {
"$decodeUrlComponent": {
"args": "str",
"desc": "解碼以前由encodeUrlComponent創建的統一資源定位器URL組件。 \n\n示例 `$decodeUrlComponent(\"%3Fx%3Dtest\")` => `\"?x=test\"`"
},
"$decodeUrlComponent": {
"$decodeUrl": {
"args": "str",
"desc": "解碼先前由encodeUrl創建的統一資源定位符URL。 \n\n示例 `$decodeUrl(\"https://mozilla.org/?x=%D1%88%D0%B5%D0%BB%D0%BB%D1%8B\")` => `\"https://mozilla.org/?x=шеллы\"`"
},

View File

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

View File

@@ -343,17 +343,29 @@ RED.history = (function() {
if (ev.changes.hasOwnProperty(i)) {
inverseEv.changes[i] = ev.node[i];
if (ev.node._def.defaults && ev.node._def.defaults[i] && ev.node._def.defaults[i].type) {
// This is a config node property
var currentConfigNode = RED.nodes.node(ev.node[i]);
if (currentConfigNode) {
currentConfigNode.users.splice(currentConfigNode.users.indexOf(ev.node),1);
RED.events.emit("nodes:change",currentConfigNode);
// This property is a reference to another node or nodes.
var nodeList = ev.node[i];
if (!Array.isArray(nodeList)) {
nodeList = [nodeList];
}
var newConfigNode = RED.nodes.node(ev.changes[i]);
if (newConfigNode) {
newConfigNode.users.push(ev.node);
RED.events.emit("nodes:change",newConfigNode);
nodeList.forEach(function(id) {
var currentConfigNode = RED.nodes.node(id);
if (currentConfigNode && currentConfigNode._def.category === "config") {
currentConfigNode.users.splice(currentConfigNode.users.indexOf(ev.node),1);
RED.events.emit("nodes:change",currentConfigNode);
}
});
nodeList = ev.changes[i];
if (!Array.isArray(nodeList)) {
nodeList = [nodeList];
}
nodeList.forEach(function(id) {
var newConfigNode = RED.nodes.node(id);
if (newConfigNode && newConfigNode._def.category === "config") {
newConfigNode.users.push(ev.node);
RED.events.emit("nodes:change",newConfigNode);
}
});
}
ev.node[i] = ev.changes[i];
}

View File

@@ -108,6 +108,31 @@ RED.i18n = (function() {
}
});
})
},
loadPluginCatalogs: function(done) {
var languageList = i18n.functions.toLanguages(localStorage.getItem("editor-language")||i18n.detectLanguage());
var toLoad = languageList.length;
languageList.forEach(function(lang) {
$.ajax({
headers: {
"Accept":"application/json"
},
cache: false,
url: apiRootUrl+'plugins/messages?lng='+lang,
success: function(data) {
var namespaces = Object.keys(data);
namespaces.forEach(function(ns) {
i18n.addResourceBundle(lang,ns,data[ns]);
});
toLoad--;
if (toLoad === 0) {
done();
}
}
});
})
}
}
})();

View File

@@ -3,7 +3,7 @@
"alt-shift-p":"core:manage-palette",
"ctrl-f": "core:search",
"ctrl-shift-f": "core:list-flows",
"ctrl-=": "core:zoom-in",
"ctrl-+": "core:zoom-in",
"ctrl--": "core:zoom-out",
"ctrl-0": "core:zoom-reset",
"ctrl-enter": "core:confirm-edit-tray",
@@ -38,12 +38,17 @@
"backspace": "core:delete-selection",
"delete": "core:delete-selection",
"enter": "core:edit-selected-node",
"ctrl-enter": "core:go-to-selection",
"ctrl-c": "core:copy-selection-to-internal-clipboard",
"ctrl-x": "core:cut-selection-to-internal-clipboard",
"ctrl-v": "core:paste-from-internal-clipboard",
"ctrl-z": "core:undo",
"ctrl-y": "core:redo",
"ctrl-a": "core:select-all-nodes",
"escape": "core:select-none",
"alt-s u": "core:select-upstream-nodes",
"alt-s d": "core:select-downstream-nodes",
"alt-s c": "core:select-connected-nodes",
"shift-?": "core:show-help",
"w": "core:scroll-view-up",
"d": "core:scroll-view-right",
@@ -53,19 +58,25 @@
"shift-d": "core:step-view-right",
"shift-s": "core:step-view-down",
"shift-a": "core:step-view-left",
"up": "core:move-selection-up",
"right": "core:move-selection-right",
"down": "core:move-selection-down",
"left": "core:move-selection-left",
"ctrl-up": "core:move-selection-up",
"ctrl-right": "core:move-selection-right",
"ctrl-down": "core:move-selection-down",
"ctrl-left": "core:move-selection-left",
"shift-up": "core:step-selection-up",
"shift-right": "core:step-selection-right",
"shift-down": "core:step-selection-down",
"shift-left": "core:step-selection-left",
"ctrl-shift-j": "core:show-previous-tab",
"ctrl-shift-k": "core:show-next-tab",
"ctrl-[": "core:show-previous-tab",
"ctrl-]": "core:show-next-tab",
"ctrl-shift-left": "core:go-to-previous-location",
"ctrl-shift-right": "core:go-to-next-location",
"ctrl-shift-g": "core:group-selection",
"ctrl-shift-u": "core:ungroup-selection",
"ctrl-shift-c": "core:copy-group-style",
"ctrl-shift-v": "core:paste-group-style"
"ctrl-shift-v": "core:paste-group-style",
"right": "core:go-to-nearest-node-on-right",
"left": "core:go-to-nearest-node-on-left",
"up": "core:go-to-nearest-node-above",
"down": "core:go-to-nearest-node-below"
}
}

View File

@@ -18,9 +18,11 @@ RED.nodes = (function() {
var node_defs = {};
var nodes = {};
var nodeTabMap = {};
var linkTabMap = {};
var configNodes = {};
var links = [];
var nodeLinks = {};
var defaultWorkspace;
var workspaces = {};
var workspacesOrder =[];
@@ -84,6 +86,10 @@ RED.nodes = (function() {
}
},
addNodeSet: function(ns) {
if (!ns.types) {
// A node has been loaded without any types. Ignore it.
return;
}
ns.added = false;
nodeSets[ns.id] = ns;
for (var j=0;j<ns.types.length;j++) {
@@ -164,6 +170,21 @@ RED.nodes = (function() {
// TODO: too tightly coupled into palette UI
}
if (def.defaults) {
for (var d in def.defaults) {
if (def.defaults.hasOwnProperty(d)) {
if (def.defaults[d].type) {
try {
def.defaults[d]._type = parseNodePropertyTypeString(def.defaults[d].type)
} catch(err) {
console.warn(err);
}
}
}
}
}
RED.events.emit("registry:node-type-added",nt);
},
removeNodeType: function(nt) {
@@ -193,6 +214,59 @@ RED.nodes = (function() {
return (1+Math.random()*4294967295).toString(16);
}
function parseNodePropertyTypeString(typeString) {
typeString = typeString.trim();
var c;
var pos = 0;
var isArray = /\[\]$/.test(typeString);
if (isArray) {
typeString = typeString.substring(0,typeString.length-2);
}
var l = typeString.length;
var inBrackets = false;
var inToken = false;
var currentToken = "";
var types = [];
while (pos < l) {
c = typeString[pos];
if (inToken) {
if (c === "|") {
types.push(currentToken.trim())
currentToken = "";
inToken = false;
} else if (c === ")") {
types.push(currentToken.trim())
currentToken = "";
inBrackets = false;
inToken = false;
} else {
currentToken += c;
}
} else {
if (c === "(") {
if (inBrackets) {
throw new Error("Invalid character '"+c+"' at position "+pos)
}
inBrackets = true;
} else if (c !== " ") {
inToken = true;
currentToken = c;
}
}
pos++;
}
currentToken = currentToken.trim();
if (currentToken.length > 0) {
types.push(currentToken)
}
return {
types: types,
array: isArray
}
}
function addNode(n) {
if (n.type.indexOf("subflow") !== 0) {
n["_"] = n._def._;
@@ -218,6 +292,9 @@ RED.nodes = (function() {
n.i = nextId+1;
}
nodes[n.id] = n;
if (!nodeLinks[n.id]) {
nodeLinks[n.id] = {in:[],out:[]};
}
if (nodeTabMap[n.z]) {
nodeTabMap[n.z][n.id] = n;
} else {
@@ -228,6 +305,22 @@ RED.nodes = (function() {
}
function addLink(l) {
links.push(l);
if (l.source) {
// Possible the node hasn't been added yet
if (!nodeLinks[l.source.id]) {
nodeLinks[l.source.id] = {in:[],out:[]};
}
nodeLinks[l.source.id].out.push(l);
}
if (l.target) {
if (!nodeLinks[l.target.id]) {
nodeLinks[l.target.id] = {in:[],out:[]};
}
nodeLinks[l.target.id].in.push(l);
}
if (l.source.z === l.target.z && linkTabMap[l.source.z]) {
linkTabMap[l.source.z].push(l);
}
RED.events.emit("links:add",l);
}
@@ -252,6 +345,7 @@ RED.nodes = (function() {
} else if (id in nodes) {
node = nodes[id];
delete nodes[id]
delete nodeLinks[id];
if (nodeTabMap[node.z]) {
delete nodeTabMap[node.z][node.id];
}
@@ -323,6 +417,27 @@ RED.nodes = (function() {
nodeTabMap[z] = {};
}
nodeTabMap[z][node.id] = node;
var nl = nodeLinks[node.id];
if (nl) {
nl.in.forEach(function(l) {
var idx = linkTabMap[node.z].indexOf(l);
if (idx != -1) {
linkTabMap[node.z].splice(idx, 1);
}
if ((l.source.z === z) && linkTabMap[z]) {
linkTabMap[z].push(l);
}
});
nl.out.forEach(function(l) {
var idx = linkTabMap[node.z].indexOf(l);
if (idx != -1) {
linkTabMap[node.z].splice(idx, 1);
}
if ((l.target.z === z) && linkTabMap[z]) {
linkTabMap[z].push(l);
}
});
}
node.z = z;
RED.events.emit("nodes:change",node);
}
@@ -339,6 +454,24 @@ RED.nodes = (function() {
var index = links.indexOf(l);
if (index != -1) {
links.splice(index,1);
if (l.source && nodeLinks[l.source.id]) {
var sIndex = nodeLinks[l.source.id].out.indexOf(l)
if (sIndex !== -1) {
nodeLinks[l.source.id].out.splice(sIndex,1)
}
}
if (l.target && nodeLinks[l.target.id]) {
var tIndex = nodeLinks[l.target.id].in.indexOf(l)
if (tIndex !== -1) {
nodeLinks[l.target.id].in.splice(tIndex,1)
}
}
if (l.source.z === l.target.z && linkTabMap[l.source.z]) {
index = linkTabMap[l.source.z].indexOf(l);
if (index !== -1) {
linkTabMap[l.source.z].splice(index,1)
}
}
}
RED.events.emit("links:remove",l);
}
@@ -346,6 +479,7 @@ RED.nodes = (function() {
function addWorkspace(ws,targetIndex) {
workspaces[ws.id] = ws;
nodeTabMap[ws.id] = {};
linkTabMap[ws.id] = [];
ws._def = RED.nodes.getType('tab');
if (targetIndex === undefined) {
@@ -369,6 +503,7 @@ RED.nodes = (function() {
if (ws) {
delete workspaces[id];
delete nodeTabMap[id];
delete linkTabMap[id];
workspacesOrder.splice(workspacesOrder.indexOf(id),1);
var i;
var node;
@@ -434,6 +569,7 @@ RED.nodes = (function() {
}
subflows[sf.id] = sf;
nodeTabMap[sf.id] = {};
linkTabMap[sf.id] = [];
RED.nodes.registerType("subflow:"+sf.id, {
defaults:{
@@ -506,29 +642,40 @@ RED.nodes = (function() {
return false;
}
function getAllFlowNodes(node) {
var visited = {};
visited[node.id] = true;
var nns = [node];
var stack = [node];
while(stack.length !== 0) {
var n = stack.shift();
var childLinks = links.filter(function(d) { return (d.source === n) || (d.target === n);});
for (var i=0;i<childLinks.length;i++) {
var child = (childLinks[i].source === n)?childLinks[i].target:childLinks[i].source;
var id = child.id;
if (!id) {
id = child.direction+":"+child.i;
}
if (!visited[id]) {
visited[id] = true;
nns.push(child);
stack.push(child);
}
}
}
return nns;
function getAllDownstreamNodes(node) {
return getAllFlowNodes(node,'down').filter(function(n) { return n !== node });
}
function getAllUpstreamNodes(node) {
return getAllFlowNodes(node,'up').filter(function(n) { return n !== node });
}
function getAllFlowNodes(node, direction) {
var selection = RED.view.selection();
var visited = new Set();
var nodes = [node];
var initialNode = true;
while(nodes.length > 0) {
var n = nodes.shift();
visited.add(n);
var links = [];
if (!initialNode || !direction || (initialNode && direction === 'up')) {
links = links.concat(nodeLinks[n.id].in);
}
if (!initialNode || !direction || (initialNode && direction === 'down')) {
links = links.concat(nodeLinks[n.id].out);
}
initialNode = false;
links.forEach(function(l) {
if (!visited.has(l.source)) {
nodes.push(l.source);
}
if (!visited.has(l.target)) {
nodes.push(l.target);
}
})
}
return Array.from(visited);
}
function convertWorkspace(n) {
var node = {};
@@ -670,6 +817,7 @@ RED.nodes = (function() {
node.in = [];
node.out = [];
node.env = n.env;
node.meta = n.meta;
if (exportCreds) {
var credentialSet = {};
@@ -780,6 +928,12 @@ RED.nodes = (function() {
subflowSet.push(n);
}
});
RED.nodes.eachConfig(function(n) {
if (n.z == subflowId) {
subflowSet.push(n);
exportedConfigNodes[n.id] = true;
}
});
var exportableSubflow = createExportableNodeSet(subflowSet, exportedIds, exportedSubflows, exportedConfigNodes);
nns = exportableSubflow.concat(nns);
}
@@ -787,16 +941,29 @@ RED.nodes = (function() {
if (node.type !== "subflow") {
var convertedNode = RED.nodes.convertNode(node);
for (var d in node._def.defaults) {
if (node._def.defaults[d].type && node[d] in configNodes) {
var confNode = configNodes[node[d]];
var exportable = registry.getNodeType(node._def.defaults[d].type).exportable;
if ((exportable == null || exportable)) {
if (!(node[d] in exportedConfigNodes)) {
exportedConfigNodes[node[d]] = true;
set.push(confNode);
if (node._def.defaults[d].type) {
var nodeList = node[d];
if (!Array.isArray(nodeList)) {
nodeList = [nodeList];
}
nodeList = nodeList.filter(function(id) {
if (id in configNodes) {
var confNode = configNodes[id];
if (confNode._def.exportable !== false) {
if (!(id in exportedConfigNodes)) {
exportedConfigNodes[id] = true;
set.push(confNode);
}
return true;
}
return false;
}
return true;
})
if (nodeList.length === 0) {
convertedNode[d] = Array.isArray(node[d])?[]:""
} else {
convertedNode[d] = "";
convertedNode[d] = Array.isArray(node[d])?nodeList:nodeList[0]
}
}
}
@@ -1261,6 +1428,8 @@ RED.nodes = (function() {
nid = getID();
workspace_map[n.id] = nid;
n.id = nid;
} else {
workspace_map[n.id] = n.id;
}
addWorkspace(n);
RED.workspaces.add(n);
@@ -1360,7 +1529,7 @@ RED.nodes = (function() {
}
}
} else {
if (n.z && !workspaces[n.z] && !subflow_map[n.z]) {
if (n.z && !workspace_map[n.z] && !subflow_map[n.z]) {
n.z = activeWorkspace;
}
}
@@ -1458,7 +1627,7 @@ RED.nodes = (function() {
node.id = getID();
} else {
node.id = n.id;
if (node.z == null || (!workspaces[node.z] && !subflow_map[node.z])) {
if (node.z == null || (!workspace_map[node.z] && !subflow_map[node.z])) {
if (createMissingWorkspace) {
if (missingWorkspace === null) {
missingWorkspace = RED.workspaces.add(null,true);
@@ -1587,15 +1756,6 @@ RED.nodes = (function() {
}
}
}
// TODO: make this a part of the node definition so it doesn't have to
// be hardcoded here
var nodeTypeArrayReferences = {
"catch":"scope",
"status":"scope",
"complete": "scope",
"link in":"links",
"link out":"links"
}
// Remap all wires and config node references
for (i=0;i<new_nodes.length;i++) {
@@ -1624,19 +1784,24 @@ RED.nodes = (function() {
}
for (var d3 in n._def.defaults) {
if (n._def.defaults.hasOwnProperty(d3)) {
if (n._def.defaults[d3].type && node_map[n[d3]]) {
configNode = node_map[n[d3]];
n[d3] = configNode.id;
if (configNode.users.indexOf(n) === -1) {
configNode.users.push(n);
if (n._def.defaults[d3].type) {
var nodeList = n[d3];
if (!Array.isArray(nodeList)) {
nodeList = [nodeList];
}
} else if (nodeTypeArrayReferences.hasOwnProperty(n.type) && nodeTypeArrayReferences[n.type] === d3 && n[d3] !== undefined && n[d3] !== null) {
for (var j = 0;j<n[d3].length;j++) {
if (node_map[n[d3][j]]) {
n[d3][j] = node_map[n[d3][j]].id;
nodeList = nodeList.map(function(id) {
var node = node_map[id];
if (node) {
if (node._def.category === 'config') {
if (node.users.indexOf(n) === -1) {
node.users.push(n);
}
}
return node.id;
}
}
return id;
})
n[d3] = Array.isArray(n[d3])?nodeList:nodeList[0];
}
}
}
@@ -1800,9 +1965,37 @@ RED.nodes = (function() {
}
function filterLinks(filter) {
var result = [];
for (var n=0;n<links.length;n++) {
var link = links[n];
var candidateLinks = [];
var hasCandidates = false;
var filterSZ = filter.source && filter.source.z;
var filterTZ = filter.target && filter.target.z;
var filterZ;
if (filterSZ || filterTZ) {
if (filterSZ === filterTZ) {
filterZ = filterSZ;
} else {
filterZ = (filterSZ === undefined)?filterTZ:filterSZ
}
}
if (filterZ) {
candidateLinks = linkTabMap[filterZ] || [];
hasCandidates = true;
} else if (filter.source && filter.source.hasOwnProperty("id")) {
if (nodeLinks[filter.source.id]) {
hasCandidates = true;
candidateLinks = candidateLinks.concat(nodeLinks[filter.source.id].out)
}
} else if (filter.target && filter.target.hasOwnProperty("id")) {
if (nodeLinks[filter.target.id]) {
hasCandidates = true;
candidateLinks = candidateLinks.concat(nodeLinks[filter.target.id].in)
}
}
if (!hasCandidates) {
candidateLinks = links;
}
for (var n=0;n<candidateLinks.length;n++) {
var link = candidateLinks[n];
if (filter.source) {
if (filter.source.hasOwnProperty("id") && link.source.id !== filter.source.id) {
continue;
@@ -1859,6 +2052,8 @@ RED.nodes = (function() {
nodes = {};
links = [];
nodeTabMap = {};
linkTabMap = {};
nodeLinks = {};
configNodes = {};
workspacesOrder = [];
groups = {};
@@ -1884,16 +2079,6 @@ RED.nodes = (function() {
RED.sidebar.info.refresh();
RED.events.emit("workspace:clear");
// var node_defs = {};
// var nodes = {};
// var configNodes = {};
// var links = [];
// var defaultWorkspace;
// var workspaces = {};
// var workspacesOrder =[];
// var subflows = {};
// var loadedFlowVersion = null;
}
function addGroup(group) {
@@ -1920,6 +2105,18 @@ RED.nodes = (function() {
RED.events.emit("groups:remove",group);
}
function getNodeHelp(type) {
var helpContent = "";
var helpElement = $("script[data-help-name='"+type+"']");
if (helpElement) {
helpContent = helpElement.html();
var helpType = helpElement.attr("type");
if (helpType === "text/markdown") {
helpContent = RED.utils.renderMarkdown(helpContent);
}
}
return helpContent;
}
return {
init: function() {
@@ -1999,6 +2196,7 @@ RED.nodes = (function() {
registerType: registry.registerNodeType,
getType: registry.getNodeType,
getNodeHelp: getNodeHelp,
convertNode: convertNode,
add: addNode,
@@ -2087,6 +2285,8 @@ RED.nodes = (function() {
identifyImportConflicts: identifyImportConflicts,
getAllFlowNodes: getAllFlowNodes,
getAllUpstreamNodes: getAllUpstreamNodes,
getAllDownstreamNodes: getAllDownstreamNodes,
createExportableNodeSet: createExportableNodeSet,
createCompleteNodeSet: createCompleteNodeSet,
updateConfigNodeUsers: updateConfigNodeUsers,

View File

@@ -0,0 +1,46 @@
RED.plugins = (function() {
var plugins = {};
var pluginsByType = {};
function registerPlugin(id,definition) {
plugins[id] = definition;
if (definition.type) {
pluginsByType[definition.type] = pluginsByType[definition.type] || [];
pluginsByType[definition.type].push(definition);
}
if (RED._loadingModule) {
definition.module = RED._loadingModule;
definition["_"] = function() {
var args = Array.prototype.slice.call(arguments);
var originalKey = args[0];
if (!/:/.test(args[0])) {
args[0] = definition.module+":"+args[0];
}
var result = RED._.apply(null,args);
if (result === args[0]) {
return originalKey;
}
return result;
}
} else {
definition["_"] = RED["_"]
}
if (definition.onadd && typeof definition.onadd === 'function') {
definition.onadd();
}
RED.events.emit("registry:plugin-added",id);
}
function getPlugin(id) {
return plugins[id]
}
function getPluginsByType(type) {
return pluginsByType[type] || [];
}
return {
registerPlugin: registerPlugin,
getPlugin: getPlugin,
getPluginsByType: getPluginsByType
}
})();

View File

@@ -52,6 +52,5 @@
Set.prototype = _Set.prototype;
Set.prototype.constructor = Set;
}
}
})();

View File

@@ -15,19 +15,65 @@
**/
var RED = (function() {
function appendNodeConfig(nodeConfig,done) {
function loadPluginList() {
loader.reportProgress(RED._("event.loadPlugins"), 10)
$.ajax({
headers: {
"Accept":"application/json"
},
cache: false,
url: 'plugins',
success: function(data) {
loader.reportProgress(RED._("event.loadPlugins"), 13)
RED.i18n.loadPluginCatalogs(function() {
loadPlugins(function() {
loadNodeList();
});
});
}
});
}
function loadPlugins(done) {
loader.reportProgress(RED._("event.loadPlugins",{count:""}), 17)
var lang = localStorage.getItem("editor-language")||i18n.detectLanguage();
$.ajax({
headers: {
"Accept":"text/html",
"Accept-Language": lang
},
cache: false,
url: 'plugins',
success: function(data) {
var configs = data.trim().split(/(?=<!-- --- \[red-plugin:\S+\] --- -->)/);
var totalCount = configs.length;
var stepConfig = function() {
// loader.reportProgress(RED._("event.loadNodes",{count:(totalCount-configs.length)+"/"+totalCount}), 30 + ((totalCount-configs.length)/totalCount)*40 )
if (configs.length === 0) {
done();
} else {
var config = configs.shift();
appendPluginConfig(config,stepConfig);
}
}
stepConfig();
}
});
}
function appendConfig(config, moduleIdMatch, targetContainer, done) {
done = done || function(){};
var m = /<!-- --- \[red-module:(\S+)\] --- -->/.exec(nodeConfig.trim());
var moduleId;
if (m) {
moduleId = m[1];
if (moduleIdMatch) {
moduleId = moduleIdMatch[1];
RED._loadingModule = moduleId;
} else {
moduleId = "unknown";
}
try {
var hasDeferred = false;
var nodeConfigEls = $("<div>"+nodeConfig+"</div>");
var nodeConfigEls = $("<div>"+config+"</div>");
var scripts = nodeConfigEls.find("script");
var scriptCount = scripts.length;
scripts.each(function(i,el) {
@@ -38,14 +84,15 @@ var RED = (function() {
newScript.onload = function() {
scriptCount--;
if (scriptCount === 0) {
$("#red-ui-editor-node-configs").append(nodeConfigEls);
$(targetContainer).append(nodeConfigEls);
delete RED._loadingModule;
done()
}
}
if ($(el).attr('type') === "module") {
newScript.type = "module";
}
$("#red-ui-editor-node-configs").append(newScript);
$(targetContainer).append(newScript);
newScript.src = RED.settings.apiRootUrl+srcUrl;
hasDeferred = true;
} else {
@@ -61,7 +108,8 @@ var RED = (function() {
}
})
if (!hasDeferred) {
$("#red-ui-editor-node-configs").append(nodeConfigEls);
$(targetContainer).append(nodeConfigEls);
delete RED._loadingModule;
done();
}
} catch(err) {
@@ -70,9 +118,27 @@ var RED = (function() {
timeout: 10000
});
console.log("["+moduleId+"] "+err.toString());
delete RED._loadingModule;
done();
}
}
function appendPluginConfig(pluginConfig,done) {
appendConfig(
pluginConfig,
/<!-- --- \[red-plugin:(\S+)\] --- -->/.exec(pluginConfig.trim()),
"#red-ui-editor-plugin-configs",
done
);
}
function appendNodeConfig(nodeConfig,done) {
appendConfig(
nodeConfig,
/<!-- --- \[red-module:(\S+)\] --- -->/.exec(nodeConfig.trim()),
"#red-ui-editor-node-configs",
done
);
}
function loadNodeList() {
loader.reportProgress(RED._("event.loadPalette"), 20)
@@ -183,9 +249,10 @@ var RED = (function() {
RED.nodes.dirty(false);
RED.view.redraw(true);
if (/^#flow\/.+$/.test(currentHash)) {
RED.workspaces.show(currentHash.substring(6));
RED.workspaces.show(currentHash.substring(6),true);
}
} catch(err) {
console.warn(err);
RED.notify(
RED._("event.importError", {message: err.message}),
{
@@ -214,7 +281,7 @@ var RED = (function() {
return;
}
if (notificationId === "project-update") {
loader.start("Loading project",0)
loader.start(RED._("event.loadingProject"), 0);
RED.nodes.clear();
RED.history.clear();
RED.view.redraw(true);
@@ -231,7 +298,7 @@ var RED = (function() {
"merge-complete": RED._("notification.project.merge-complete")
}[msg.action];
loader.end()
RED.notify("<p>"+message+"</p>");
RED.notify($("<p>").text(message));
RED.sidebar.info.refresh()
});
});
@@ -248,6 +315,7 @@ var RED = (function() {
id: notificationId
}
if (notificationId === "runtime-state") {
RED.events.emit("runtime-state",msg);
if (msg.error === "safe-mode") {
options.buttons = [
{
@@ -269,7 +337,7 @@ var RED = (function() {
}
}
]
// } else if (RED.settings.theme('palette.editable') !== false) {
// } else if (RED.settings.get('externalModules.palette.allowInstall', true) !== false) {
} else {
options.buttons = [
{
@@ -280,6 +348,16 @@ var RED = (function() {
}
]
}
} else if (msg.error === "missing-modules") {
text+="<ul><li>"+msg.modules.map(function(m) { return RED.utils.sanitize(m.module)+(m.error?(" - <small>"+RED.utils.sanitize(""+m.error)+"</small>"):"")}).join("</li><li>")+"</li></ul>";
options.buttons = [
{
text: RED._("common.label.close"),
click: function() {
persistentNotifications[notificationId].hideNotification();
}
}
]
} else if (msg.error === "credentials_load_failed") {
if (RED.settings.theme("projects.enabled",false)) {
// projects enabled
@@ -370,6 +448,9 @@ var RED = (function() {
} else if (persistentNotifications.hasOwnProperty(notificationId)) {
persistentNotifications[notificationId].close();
delete persistentNotifications[notificationId];
if (notificationId === 'runtime-state') {
RED.events.emit("runtime-state",msg);
}
}
});
RED.comms.subscribe("status/#",function(topic,msg) {
@@ -402,7 +483,7 @@ var RED = (function() {
});
});
if (addedTypes.length) {
typeList = "<ul><li>"+addedTypes.join("</li><li>")+"</li></ul>";
typeList = "<ul><li>"+addedTypes.map(RED.utils.sanitize).join("</li><li>")+"</li></ul>";
RED.notify(RED._("palette.event.nodeAdded", {count:addedTypes.length})+typeList,"success");
}
loadIconList();
@@ -411,7 +492,7 @@ var RED = (function() {
m = msg[i];
info = RED.nodes.removeNodeSet(m.id);
if (info.added) {
typeList = "<ul><li>"+m.types.join("</li><li>")+"</li></ul>";
typeList = "<ul><li>"+m.types.map(RED.utils.sanitize).join("</li><li>")+"</li></ul>";
RED.notify(RED._("palette.event.nodeRemoved", {count:m.types.length})+typeList,"success");
}
}
@@ -421,12 +502,12 @@ var RED = (function() {
info = RED.nodes.getNodeSet(msg.id);
if (info.added) {
RED.nodes.enableNodeSet(msg.id);
typeList = "<ul><li>"+msg.types.join("</li><li>")+"</li></ul>";
typeList = "<ul><li>"+msg.types.map(RED.utils.sanitize).join("</li><li>")+"</li></ul>";
RED.notify(RED._("palette.event.nodeEnabled", {count:msg.types.length})+typeList,"success");
} else {
$.get('nodes/'+msg.id, function(data) {
appendNodeConfig(data);
typeList = "<ul><li>"+msg.types.join("</li><li>")+"</li></ul>";
typeList = "<ul><li>"+msg.types.map(RED.utils.sanitize).join("</li><li>")+"</li></ul>";
RED.notify(RED._("palette.event.nodeAdded", {count:msg.types.length})+typeList,"success");
});
}
@@ -434,7 +515,7 @@ var RED = (function() {
} else if (topic == "notification/node/disabled") {
if (msg.types) {
RED.nodes.disableNodeSet(msg.id);
typeList = "<ul><li>"+msg.types.join("</li><li>")+"</li></ul>";
typeList = "<ul><li>"+msg.types.map(RED.utils.sanitize).join("</li><li>")+"</li></ul>";
RED.notify(RED._("palette.event.nodeDisabled", {count:msg.types.length})+typeList,"success");
}
} else if (topic == "notification/node/upgraded") {
@@ -449,6 +530,9 @@ var RED = (function() {
$(".red-ui-header-toolbar").show();
RED.sidebar.show(":first");
setTimeout(function() {
loader.end();
},100);
@@ -509,7 +593,7 @@ var RED = (function() {
]});
menuOptions.push(null);
if (RED.settings.theme('palette.editable') !== false) {
if (RED.settings.get('externalModules.palette.allowInstall', true) !== false) {
menuOptions.push({id:"menu-item-edit-palette",label:RED._("menu.label.editPalette"),onselect:"core:manage-palette"});
menuOptions.push(null);
}
@@ -544,7 +628,7 @@ var RED = (function() {
RED.palette.init();
RED.eventLog.init();
if (RED.settings.theme('palette.editable') !== false) {
if (RED.settings.get('externalModules.palette.allowInstall', true) !== false) {
RED.palette.editor.init();
} else {
console.log("Palette editor disabled");
@@ -579,7 +663,7 @@ var RED = (function() {
RED.actions.add("core:show-about", showAbout);
loadNodeList();
loadPluginList();
}
@@ -595,6 +679,7 @@ var RED = (function() {
'<div id="red-ui-sidebar"></div>'+
'<div id="red-ui-sidebar-separator"></div>'+
'</div>').appendTo(options.target);
$('<div id="red-ui-editor-plugin-configs"></div>').appendTo(options.target);
$('<div id="red-ui-editor-node-configs"></div>').appendTo(options.target);
$('<div id="red-ui-full-shade" class="hide"></div>').appendTo(options.target);
@@ -613,9 +698,12 @@ var RED = (function() {
$('<span>').html(theme.header.title).appendTo(logo);
}
}
if (theme.themes) {
knownThemes = theme.themes;
}
});
}
var knownThemes = null;
var initialised = false;
function init(options) {
@@ -635,7 +723,13 @@ var RED = (function() {
buildEditor(options);
RED.i18n.init(options, function() {
RED.settings.init(options, loadEditor);
RED.settings.init(options, function() {
if (knownThemes) {
RED.settings.editorTheme = RED.settings.editorTheme || {};
RED.settings.editorTheme.themes = knownThemes;
}
loadEditor();
});
})
}

View File

@@ -57,12 +57,11 @@ RED.settings = (function () {
return JSON.parse(localStorage.getItem(key));
} else {
var v;
try {
v = RED.utils.getMessageProperty(userSettings,key);
if (v === undefined) {
v = defaultIfUndefined;
}
} catch(err) {
try { v = RED.utils.getMessageProperty(userSettings,key); } catch(err) {}
if (v === undefined) {
try { v = RED.utils.getMessageProperty(RED.settings,key); } catch(err) {}
}
if (v === undefined) {
v = defaultIfUndefined;
}
return v;

View File

@@ -151,7 +151,6 @@ RED.actionList = (function() {
}
if (!visible) {
previousActiveElement = document.activeElement;
RED.keyboard.add("*","escape",function(){hide()});
$("#red-ui-header-shade").show();
$("#red-ui-editor-shade").show();
$("#red-ui-palette-shade").show();
@@ -185,7 +184,6 @@ RED.actionList = (function() {
function hide() {
if (visible) {
RED.keyboard.remove("escape");
visible = false;
$("#red-ui-header-shade").hide();
$("#red-ui-editor-shade").hide();
@@ -215,6 +213,9 @@ RED.actionList = (function() {
RED.events.on("type-search:open",function() { disabled = true; });
RED.events.on("type-search:close",function() { disabled = false; });
RED.keyboard.add("red-ui-actionList","escape",function(){hide()});
$("#red-ui-header-shade").on('mousedown',hide);
$("#red-ui-editor-shade").on('mousedown',hide);
$("#red-ui-palette-shade").on('mousedown',hide);

View File

@@ -26,10 +26,32 @@ RED.clipboard = (function() {
var currentPopoverError;
var activeTab;
var libraryBrowser;
var examplesBrowser;
var activeLibraries = {};
var pendingImportConfig;
function downloadData(file, data) {
if (window.navigator.msSaveBlob) {
// IE11 workaround
// IE does not support data uri scheme for downloading data
var blob = new Blob([data], {
type: "data:text/plain;charset=utf-8"
});
navigator.msSaveBlob(blob, file);
}
else {
var element = document.createElement('a');
element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(data));
element.setAttribute('download', file);
element.style.display = 'none';
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
}
}
function setupDialogs() {
dialog = $('<div id="red-ui-clipboard-dialog" class="hide"><form class="dialog-form form-horizontal"></form></div>')
.appendTo("#red-ui-editor")
@@ -56,13 +78,8 @@ RED.clipboard = (function() {
class: "primary",
text: RED._("clipboard.download"),
click: function() {
var element = document.createElement('a');
element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent($("#red-ui-clipboard-dialog-export-text").val()));
element.setAttribute('download', "flows.json");
element.style.display = 'none';
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
var data = $("#red-ui-clipboard-dialog-export-text").val();
downloadData("flows.json", data);
$( this ).dialog( "close" );
}
},
@@ -72,14 +89,15 @@ RED.clipboard = (function() {
text: RED._("clipboard.export.copy"),
click: function() {
if (activeTab === "red-ui-clipboard-dialog-export-tab-clipboard") {
$("#red-ui-clipboard-dialog-export-text").select();
document.execCommand("copy");
document.getSelection().removeAllRanges();
RED.notify(RED._("clipboard.nodesExported"),{id:"clipboard"});
var flowData = $("#red-ui-clipboard-dialog-export-text").val();
// Close the dialog first otherwise FireFox won't focus the hidden
// clipboard element in copyText
$( this ).dialog( "close" );
copyText(flowData);
RED.notify(RED._("clipboard.nodesExported"),{id:"clipboard"});
} else {
var flowToExport = $("#red-ui-clipboard-dialog-export-text").val();
var selectedPath = libraryBrowser.getSelected();
var selectedPath = activeLibraries[activeTab].getSelected();
if (!selectedPath.children) {
selectedPath = selectedPath.parent;
}
@@ -145,12 +163,7 @@ RED.clipboard = (function() {
if (activeTab === "red-ui-clipboard-dialog-import-tab-clipboard") {
importNodes($("#red-ui-clipboard-dialog-import-text").val(),addNewFlow);
} else {
var selectedPath;
if (activeTab === "red-ui-clipboard-dialog-import-tab-library") {
selectedPath = libraryBrowser.getSelected();
} else {
selectedPath = examplesBrowser.getSelected();
}
var selectedPath = activeLibraries[activeTab].getSelected();
if (selectedPath.path) {
$.get('library/'+selectedPath.library+'/'+selectedPath.type+'/'+selectedPath.path, function(data) {
importNodes(data,addNewFlow);
@@ -222,21 +235,26 @@ RED.clipboard = (function() {
'</div>'+
'<div id="red-ui-clipboard-dialog-export-tabs-content" class="red-ui-clipboard-dialog-tabs-content">'+
'<div id="red-ui-clipboard-dialog-export-tab-clipboard" class="red-ui-clipboard-dialog-tab-clipboard">'+
'<div class="form-row" style="height:calc(100% - 30px)">'+
'<textarea readonly id="red-ui-clipboard-dialog-export-text"></textarea>'+
'<div id="red-ui-clipboard-dialog-export-tab-clipboard-tab-bar">'+
'<ul id="red-ui-clipboard-dialog-export-tab-clipboard-tabs"></ul>'+
'</div>'+
'<div class="form-row" style="text-align: right;">'+
'<span id="red-ui-clipboard-dialog-export-fmt-group" class="button-group">'+
'<a id="red-ui-clipboard-dialog-export-fmt-mini" class="red-ui-button red-ui-button-small toggle" href="#" data-i18n="clipboard.export.compact"></a>'+
'<a id="red-ui-clipboard-dialog-export-fmt-full" class="red-ui-button red-ui-button-small toggle" href="#" data-i18n="clipboard.export.formatted"></a>'+
'</span>'+
'<div class="red-ui-clipboard-dialog-export-tab-clipboard-tab" id="red-ui-clipboard-dialog-export-tab-clipboard-preview">'+
'<div id="red-ui-clipboard-dialog-export-tab-clipboard-preview-list"></div>'+
'</div>'+
'<div class="red-ui-clipboard-dialog-export-tab-clipboard-tab" id="red-ui-clipboard-dialog-export-tab-clipboard-json">'+
'<div class="form-row" style="height:calc(100% - 40px)">'+
'<textarea readonly id="red-ui-clipboard-dialog-export-text"></textarea>'+
'</div>'+
'<div class="form-row" style="text-align: right;">'+
'<span id="red-ui-clipboard-dialog-export-fmt-group" class="button-group">'+
'<a id="red-ui-clipboard-dialog-export-fmt-mini" class="red-ui-button red-ui-button-small toggle" href="#" data-i18n="clipboard.export.compact"></a>'+
'<a id="red-ui-clipboard-dialog-export-fmt-full" class="red-ui-button red-ui-button-small toggle" href="#" data-i18n="clipboard.export.formatted"></a>'+
'</span>'+
'</div>'+
'</div>'+
'</div>'+
'<div id="red-ui-clipboard-dialog-export-tab-library" class="red-ui-clipboard-dialog-tab-library">'+
'<div id="red-ui-clipboard-dialog-export-tab-library-browser"></div>'+
'<div class="form-row">'+
'<label data-i18n="clipboard.export.exportAs"></label><input id="red-ui-clipboard-dialog-tab-library-name" type="text">'+
'</div>'+
'<div class="form-row" id="red-ui-clipboard-dialog-export-tab-library-filename">'+
'<label data-i18n="clipboard.export.exportAs"></label><input id="red-ui-clipboard-dialog-tab-library-name" type="text">'+
'</div>'+
'</div>'+
'</div>'
@@ -258,8 +276,6 @@ RED.clipboard = (function() {
'<textarea id="red-ui-clipboard-dialog-import-text"></textarea>'+
'</div>'+
'</div>'+
'<div id="red-ui-clipboard-dialog-import-tab-library" class="red-ui-clipboard-dialog-tab-library"></div>'+
'<div id="red-ui-clipboard-dialog-import-tab-examples" class="red-ui-clipboard-dialog-tab-library"></div>'+
'</div>'+
'</div>'+
'<div class="form-row">'+
@@ -392,7 +408,7 @@ RED.clipboard = (function() {
}
},100);
} else {
var file = libraryBrowser.getSelected();
var file = activeLibraries[activeTab].getSelected();
if (file && file.label && !file.children) {
$("#red-ui-clipboard-dialog-ok").button("enable");
} else {
@@ -424,7 +440,7 @@ RED.clipboard = (function() {
if (tab.id === "red-ui-clipboard-dialog-import-tab-clipboard") {
$("#red-ui-clipboard-dialog-import-text").trigger("focus");
} else {
libraryBrowser.focus();
activeLibraries[tab.id].focus();
}
validateImport();
}
@@ -433,54 +449,43 @@ RED.clipboard = (function() {
id: "red-ui-clipboard-dialog-import-tab-clipboard",
label: RED._("clipboard.clipboard")
});
tabs.addTab({
id: "red-ui-clipboard-dialog-import-tab-library",
label: RED._("library.library")
});
tabs.addTab({
id: "red-ui-clipboard-dialog-import-tab-examples",
label: RED._("library.types.examples")
});
var libraries = RED.settings.libraries || [];
libraries.forEach(function(lib) {
var tabId = "red-ui-clipboard-dialog-import-tab-library-"+lib.id
tabs.addTab({
id: tabId,
label: RED._(lib.label||lib.id)
})
var content = $('<div id="red-ui-clipboard-dialog-import-tab-library" class="red-ui-clipboard-dialog-tab-library"></div>')
.attr("id",tabId)
.hide()
.appendTo("#red-ui-clipboard-dialog-import-tabs-content");
var browser = RED.library.createBrowser({
container: content,
onselect: function(file) {
if (file && file.label && !file.children) {
$("#red-ui-clipboard-dialog-ok").button("enable");
} else {
$("#red-ui-clipboard-dialog-ok").button("disable");
}
},
onconfirm: function(item) {
if (item && item.label && !item.children) {
$("#red-ui-clipboard-dialog-ok").trigger("click");
}
}
})
loadFlowLibrary(browser,lib);
activeLibraries[tabId] = browser;
})
$("#red-ui-clipboard-dialog-tab-library-name").on("keyup", validateExportFilename);
$("#red-ui-clipboard-dialog-tab-library-name").on('paste',function() { setTimeout(validateExportFilename,10)});
$("#red-ui-clipboard-dialog-export").button("enable");
libraryBrowser = RED.library.createBrowser({
container: $("#red-ui-clipboard-dialog-import-tab-library"),
onselect: function(file) {
if (file && file.label && !file.children) {
$("#red-ui-clipboard-dialog-ok").button("enable");
} else {
$("#red-ui-clipboard-dialog-ok").button("disable");
}
},
onconfirm: function(item) {
if (item && item.label && !item.children) {
$("#red-ui-clipboard-dialog-ok").trigger("click");
}
}
})
loadFlowLibrary(libraryBrowser,"local",RED._("library.types.local"));
examplesBrowser = RED.library.createBrowser({
container: $("#red-ui-clipboard-dialog-import-tab-examples"),
onselect: function(file) {
if (file && file.label && !file.children) {
$("#red-ui-clipboard-dialog-ok").button("enable");
} else {
$("#red-ui-clipboard-dialog-ok").button("disable");
}
},
onconfirm: function(item) {
if (item && item.label && !item.children) {
$("#red-ui-clipboard-dialog-ok").trigger("click");
}
}
})
loadFlowLibrary(examplesBrowser,"_examples_",RED._("library.types.examples"));
dialogContainer.i18n();
$("#red-ui-clipboard-dialog-ok").show();
@@ -560,10 +565,12 @@ RED.clipboard = (function() {
if (tab.id === "red-ui-clipboard-dialog-export-tab-clipboard") {
$("#red-ui-clipboard-dialog-export").button("option","label", RED._("clipboard.export.copy"))
$("#red-ui-clipboard-dialog-download").show();
$("#red-ui-clipboard-dialog-export-tab-library-filename").hide();
} else {
$("#red-ui-clipboard-dialog-export").button("option","label", RED._("clipboard.export.export"))
$("#red-ui-clipboard-dialog-download").hide();
libraryBrowser.focus();
$("#red-ui-clipboard-dialog-export-tab-library-filename").show();
activeLibraries[activeTab].focus();
}
}
@@ -572,25 +579,68 @@ RED.clipboard = (function() {
id: "red-ui-clipboard-dialog-export-tab-clipboard",
label: RED._("clipboard.clipboard")
});
tabs.addTab({
id: "red-ui-clipboard-dialog-export-tab-library",
label: RED._("library.library")
});
var libraries = RED.settings.libraries || [];
libraries.forEach(function(lib) {
if (lib.readOnly) {
return
}
var tabId = "red-ui-clipboard-dialog-export-tab-library-"+lib.id
tabs.addTab({
id: tabId,
label: RED._(lib.label||lib.id)
})
var content = $('<div class="red-ui-clipboard-dialog-export-tab-library-browser red-ui-clipboard-dialog-tab-library"></div>')
.attr("id",tabId)
.hide()
.insertBefore("#red-ui-clipboard-dialog-export-tab-library-filename");
var browser = RED.library.createBrowser({
container: content,
folderTools: true,
onselect: function(file) {
if (file && file.label && !file.children) {
$("#red-ui-clipboard-dialog-tab-library-name").val(file.label);
}
},
})
loadFlowLibrary(browser,lib);
activeLibraries[tabId] = browser;
})
$("#red-ui-clipboard-dialog-tab-library-name").on("keyup", validateExportFilename);
$("#red-ui-clipboard-dialog-tab-library-name").on('paste',function() { setTimeout(validateExportFilename,10)});
$("#red-ui-clipboard-dialog-export").button("enable");
libraryBrowser = RED.library.createBrowser({
container: $("#red-ui-clipboard-dialog-export-tab-library-browser"),
folderTools: true,
onselect: function(file) {
if (file && file.label && !file.children) {
$("#red-ui-clipboard-dialog-tab-library-name").val(file.label);
}
var clipboardTabs = RED.tabs.create({
id: "red-ui-clipboard-dialog-export-tab-clipboard-tabs",
onchange: function(tab) {
$(".red-ui-clipboard-dialog-export-tab-clipboard-tab").hide();
$("#" + tab.id).show();
}
});
clipboardTabs.addTab({
id: "red-ui-clipboard-dialog-export-tab-clipboard-preview",
label: RED._("clipboard.exportNodes")
});
clipboardTabs.addTab({
id: "red-ui-clipboard-dialog-export-tab-clipboard-json",
label: RED._("editor.types.json")
});
var previewList = $("#red-ui-clipboard-dialog-export-tab-clipboard-preview-list").css({position:"absolute",top:0,right:0,bottom:0,left:0}).treeList({
data: []
})
loadFlowLibrary(libraryBrowser,"local",RED._("library.types.local"));
refreshExportPreview();
$("#red-ui-clipboard-dialog-tab-library-name").val("flows.json").select();
@@ -630,10 +680,10 @@ RED.clipboard = (function() {
}
$(this).parent().children().removeClass('selected');
$(this).addClass('selected');
var type = $(this).attr('id');
var type = $(this).attr('id').substring("red-ui-clipboard-dialog-export-rng-".length);
var flow = "";
var nodes = null;
if (type === 'red-ui-clipboard-dialog-export-rng-selected') {
if (type === 'selected') {
var selection = RED.workspaces.selection();
if (selection.length > 0) {
nodes = [];
@@ -647,14 +697,14 @@ RED.clipboard = (function() {
}
// Don't include the subflow meta-port nodes in the exported selection
nodes = RED.nodes.createExportableNodeSet(nodes.filter(function(n) { return n.type !== 'subflow'}));
} else if (type === 'red-ui-clipboard-dialog-export-rng-flow') {
} else if (type === 'flow') {
var activeWorkspace = RED.workspaces.active();
nodes = RED.nodes.groups(activeWorkspace);
nodes = nodes.concat(RED.nodes.filterNodes({z:activeWorkspace}));
var parentNode = RED.nodes.workspace(activeWorkspace)||RED.nodes.subflow(activeWorkspace);
nodes.unshift(parentNode);
nodes = RED.nodes.createExportableNodeSet(nodes);
} else if (type === 'red-ui-clipboard-dialog-export-rng-full') {
} else if (type === 'full') {
nodes = RED.nodes.createCompleteNodeSet(false);
}
if (nodes !== null) {
@@ -670,8 +720,10 @@ RED.clipboard = (function() {
$("#red-ui-clipboard-dialog-export").addClass('disabled');
}
$("#red-ui-clipboard-dialog-export-text").val(flow);
setTimeout(function() { $("#red-ui-clipboard-dialog-export-text").scrollTop(0); },50);
$("#red-ui-clipboard-dialog-export-text").trigger("focus");
setTimeout(function() {
$("#red-ui-clipboard-dialog-export-text").scrollTop(0);
refreshExportPreview(type);
},50);
})
$("#red-ui-clipboard-dialog-ok").hide();
@@ -717,45 +769,130 @@ RED.clipboard = (function() {
}
function loadFlowLibrary(browser,library,label) {
// if (includeExamples) {
// listing.push({
// library: "_examples_",
// type: "flows",
// icon: 'fa fa-hdd-o',
// label: RED._("library.types.examples"),
// path: "",
// children: function(done,item) {
// RED.library.loadLibraryFolder("_examples_","flows","",function(children) {
// item.children = children;
// done(children);
// })
// }
// })
// }
function refreshExportPreview(type) {
var flowData = $("#red-ui-clipboard-dialog-export-text").val() || "[]";
var flow = JSON.parse(flowData);
var flows = {};
var subflows = {};
var nodes = [];
var nodesByZ = {};
var treeFlows = [];
var treeSubflows = [];
flow.forEach(function(node) {
if (node.type === "tab") {
flows[node.id] = {
element: getFlowLabel(node,false),
deferBuild: type !== "flow",
expanded: type === "flow",
children: []
};
treeFlows.push(flows[node.id])
} else if (node.type === "subflow") {
subflows[node.id] = {
element: getNodeLabel(node,false),
deferBuild: true,
children: []
};
treeSubflows.push(subflows[node.id])
} else {
nodes.push(node);
}
});
var globalNodes = [];
var parentlessNodes = [];
nodes.forEach(function(node) {
var treeNode = {
element: getNodeLabel(node, false, false)
};
if (node.z) {
if (!flows[node.z] && !subflows[node.z]) {
parentlessNodes.push(treeNode)
} else if (flows[node.z]) {
flows[node.z].children.push(treeNode)
} else if (subflows[node.z]) {
subflows[node.z].children.push(treeNode)
}
} else {
globalNodes.push(treeNode);
}
});
var treeData = [];
if (parentlessNodes.length > 0) {
treeData = treeData.concat(parentlessNodes);
}
if (type === "flow") {
treeData = treeData.concat(treeFlows);
} else if (treeFlows.length > 0) {
treeData.push({
label: RED._("menu.label.flows"),
deferBuild: treeFlows.length > 20,
expanded: treeFlows.length <= 20,
children: treeFlows
})
}
if (treeSubflows.length > 0) {
treeData.push({
label: RED._("menu.label.subflows"),
deferBuild: treeSubflows.length > 10,
expanded: treeSubflows.length <= 10,
children: treeSubflows
})
}
if (globalNodes.length > 0) {
treeData.push({
label: RED._("sidebar.info.globalConfig"),
deferBuild: globalNodes.length > 10,
expanded: globalNodes.length <= 10,
children: globalNodes
})
}
$("#red-ui-clipboard-dialog-export-tab-clipboard-preview-list").treeList('data',treeData);
}
function loadFlowLibrary(browser,library) {
var icon = 'fa fa-hdd-o';
if (library.icon) {
var fullIcon = RED.utils.separateIconPath(library.icon);
icon = (fullIcon.module==="font-awesome"?"fa ":"")+fullIcon.file;
}
browser.data([{
library: library,
library: library.id,
type: "flows",
icon: 'fa fa-hdd-o',
label: label,
icon: icon,
label: RED._(library.label||library.id),
path: "",
expanded: true,
children: function(done, item) {
RED.library.loadLibraryFolder(library,"flows","",function(children) {
item.children = children;
done(children);
})
}
children: [{
library: library.id,
type: "flows",
icon: 'fa fa-cube',
label: "flows",
path: "",
expanded: true,
children: function(done, item) {
RED.library.loadLibraryFolder(library.id,"flows","",function(children) {
item.children = children;
done(children);
})
}
}]
}], true);
}
function hideDropTarget() {
$("#red-ui-drop-target").hide();
RED.keyboard.remove("escape");
}
function copyText(value,element,msg) {
var truncated = false;
var currentFocus = document.activeElement;
if (typeof value !== "string" ) {
value = JSON.stringify(value, function(key,value) {
if (value !== null && typeof value === 'object') {
@@ -787,7 +924,7 @@ RED.clipboard = (function() {
if (truncated) {
msg += "_truncated";
}
$("#red-ui-clipboard-hidden").val(value).select();
$("#red-ui-clipboard-hidden").val(value).focus().select();
var result = document.execCommand("copy");
if (result && element) {
var popover = RED.popover.create({
@@ -801,6 +938,10 @@ RED.clipboard = (function() {
},1000);
popover.open();
}
$("#red-ui-clipboard-hidden").val("");
if (currentFocus) {
$(currentFocus).focus();
}
return result;
}
@@ -1045,22 +1186,6 @@ RED.clipboard = (function() {
}
}
function getNodeLabelText(n) {
var label = n.name || n.type+": "+n.id;
if (n._def.label) {
try {
label = (typeof n._def.label === "function" ? n._def.label.call(n) : n._def.label)||"";
} catch(err) {
console.log("Definition error: "+n.type+".label",err);
}
}
var newlineIndex = label.indexOf("\\n");
if (newlineIndex > -1) {
label = label.substring(0,newlineIndex)+"...";
}
return label;
}
function getFlowLabel(n) {
n = JSON.parse(JSON.stringify(n));
n._def = RED.nodes.getType(n.type) || {};
@@ -1086,16 +1211,8 @@ RED.clipboard = (function() {
if (n._def) {
n._ = n._def._;
}
var div = $('<div>',{class:"red-ui-info-outline-item"});
RED.utils.createNodeIcon(n).appendTo(div);
var contentDiv = $('<div>',{class:"red-ui-search-result-description"}).appendTo(div);
var labelText = getNodeLabelText(n);
var label = $('<div>',{class:"red-ui-search-result-node-label red-ui-info-outline-item-label"}).appendTo(contentDiv);
if (labelText) {
label.text(labelText)
} else {
label.html(n.type)
}
var div = $('<div>',{class:"red-ui-node-list-item"});
RED.utils.createNodeIcon(n,true).appendTo(div);
return div;
}
@@ -1103,7 +1220,7 @@ RED.clipboard = (function() {
init: function() {
setupDialogs();
$('<input type="text" id="red-ui-clipboard-hidden" tabIndex="-1">').appendTo("#red-ui-editor");
$('<textarea type="text" id="red-ui-clipboard-hidden" tabIndex="-1">').appendTo("#red-ui-editor");
RED.actions.add("core:show-export-dialog",showExportNodes);
RED.actions.add("core:show-import-dialog",showImportNodes);
@@ -1124,11 +1241,12 @@ RED.clipboard = (function() {
$('<div id="red-ui-drop-target"><div data-i18n="[append]workspace.dropFlowHere"><i class="fa fa-download"></i><br></div></div>').appendTo('#red-ui-editor');
RED.keyboard.add("#red-ui-drop-target", "escape" ,hideDropTarget);
$('#red-ui-workspace-chart').on("dragenter",function(event) {
if ($.inArray("text/plain",event.originalEvent.dataTransfer.types) != -1 ||
$.inArray("Files",event.originalEvent.dataTransfer.types) != -1) {
$("#red-ui-drop-target").css({display:'table'});
RED.keyboard.add("*", "escape" ,hideDropTarget);
$("#red-ui-drop-target").css({display:'table'}).focus();
}
});
@@ -1142,22 +1260,27 @@ RED.clipboard = (function() {
hideDropTarget();
})
.on("drop",function(event) {
if ($.inArray("text/plain",event.originalEvent.dataTransfer.types) != -1) {
var data = event.originalEvent.dataTransfer.getData("text/plain");
data = data.substring(data.indexOf('['),data.lastIndexOf(']')+1);
importNodes(data);
} else if ($.inArray("Files",event.originalEvent.dataTransfer.types) != -1) {
var files = event.originalEvent.dataTransfer.files;
if (files.length === 1) {
var file = files[0];
var reader = new FileReader();
reader.onload = (function(theFile) {
return function(e) {
importNodes(e.target.result);
};
})(file);
reader.readAsText(file);
try {
if ($.inArray("text/plain",event.originalEvent.dataTransfer.types) != -1) {
var data = event.originalEvent.dataTransfer.getData("text/plain");
data = data.substring(data.indexOf('['),data.lastIndexOf(']')+1);
importNodes(data);
} else if ($.inArray("Files",event.originalEvent.dataTransfer.types) != -1) {
var files = event.originalEvent.dataTransfer.files;
if (files.length === 1) {
var file = files[0];
var reader = new FileReader();
reader.onload = (function(theFile) {
return function(e) {
importNodes(e.target.result);
};
})(file);
reader.readAsText(file);
}
}
} catch(err) {
// Ensure any errors throw above doesn't stop the drop target from
// being hidden.
}
hideDropTarget();
event.preventDefault();

View File

@@ -18,6 +18,7 @@
/**
* options:
* - addButton : boolean|string - text for add label, default 'add'
* - buttons : array - list of custom buttons (objects with fields 'label', 'icon', 'title', 'click')
* - height : number|'auto'
* - resize : function - called when list as a whole is resized
* - resizeItem : function(item) - called to resize individual item
@@ -67,24 +68,52 @@
this.topContainer.addClass(this.options.class);
}
var buttons = this.options.buttons || [];
if (this.options.addButton !== false) {
var addLabel;
var addLabel, addTitle;
if (typeof this.options.addButton === 'string') {
addLabel = this.options.addButton
} else {
if (RED && RED._) {
addLabel = RED._("editableList.add");
addTitle = RED._("editableList.addTitle");
} else {
addLabel = 'add';
addTitle = 'add new item';
}
}
$('<a href="#" class="red-ui-button red-ui-button-small red-ui-editableList-addButton" style="margin-top: 4px;"><i class="fa fa-plus"></i> '+addLabel+'</a>')
.appendTo(this.topContainer)
buttons.unshift({
label: addLabel,
icon: "fa fa-plus",
click: function(evt) {
that.addItem({});
},
title: addTitle
});
}
buttons.forEach(function(button) {
var element = $('<a href="#" class="red-ui-button red-ui-button-small red-ui-editableList-addButton" style="margin-top: 4px; margin-right: 5px;"></a>')
.appendTo(that.topContainer)
.on("click", function(evt) {
evt.preventDefault();
that.addItem({});
if (button.click !== undefined) {
button.click(evt);
}
});
}
if (button.title) {
element.attr("title", button.title);
}
if (button.icon) {
element.append($("<i></i>").attr("class", button.icon));
}
if (button.label) {
element.append($("<span></span>").text(" " + button.label));
}
});
if (this.element.css("position") === "absolute") {
["top","left","bottom","right"].forEach(function(s) {
var v = that.element.css(s);

View File

@@ -29,6 +29,7 @@ RED.tabs = (function() {
var currentTabWidth;
var currentActiveTabWidth = 0;
var collapsibleMenu;
var mousedownTab;
var preferredOrder = options.order;
var ul = options.element || $("#"+options.id);
var wrapper = ul.wrap( "<div>" ).parent();
@@ -99,7 +100,22 @@ RED.tabs = (function() {
if (options.scrollable) {
wrapper.addClass("red-ui-tabs-scrollable");
scrollContainer.addClass("red-ui-tabs-scroll-container");
scrollContainer.on("scroll",updateScroll);
scrollContainer.on("scroll",function(evt) {
// Generated by trackpads - not mousewheel
updateScroll(evt);
});
scrollContainer.on("wheel", function(evt) {
if (evt.originalEvent.deltaX === 0) {
// Prevent the scroll event from firing
evt.preventDefault();
// Assume this is wheel event which might not trigger
// the scroll event, so do things manually
var sl = scrollContainer.scrollLeft();
sl -= evt.originalEvent.deltaY;
scrollContainer.scrollLeft(sl);
}
})
scrollLeft = $('<div class="red-ui-tab-button red-ui-tab-scroll red-ui-tab-scroll-left"><a href="#" style="display:none;"><i class="fa fa-caret-left"></i></a></div>').appendTo(wrapper).find("a");
scrollLeft.on('mousedown',function(evt) { scrollEventHandler(evt,'-=150') }).on('click',function(evt){ evt.preventDefault();});
scrollRight = $('<div class="red-ui-tab-button red-ui-tab-scroll red-ui-tab-scroll-right"><a href="#" style="display:none;"><i class="fa fa-caret-right"></i></a></div>').appendTo(wrapper).find("a");
@@ -207,6 +223,11 @@ RED.tabs = (function() {
if (dragActive) {
return
}
if (evt.currentTarget !== mousedownTab) {
mousedownTab = null;
return;
}
mousedownTab = null;
if (dblClickTime && Date.now()-dblClickTime < 400) {
dblClickTime = 0;
dblClickArmed = true;
@@ -445,6 +466,7 @@ RED.tabs = (function() {
}
ul.find("li.red-ui-tab a")
.on("mousedown", function(evt) { mousedownTab = evt.currentTarget })
.on("mouseup",onTabClick)
.on("click", function(evt) {evt.preventDefault(); })
.on("dblclick", function(evt) {evt.stopPropagation(); evt.preventDefault(); })
@@ -509,8 +531,8 @@ RED.tabs = (function() {
li.attr('id',"red-ui-tab-"+(tab.id.replace(".","-")));
li.data("tabId",tab.id);
if (options.maximumTabWidth) {
li.css("maxWidth",options.maximumTabWidth+"px");
if (options.maximumTabWidth || tab.maximumTabWidth) {
li.css("maxWidth",(options.maximumTabWidth || tab.maximumTabWidth) +"px");
}
var link = $("<a/>",{href:"#"+tab.id, class:"red-ui-tab-label"}).appendTo(li);
if (tab.icon) {
@@ -636,6 +658,7 @@ RED.tabs = (function() {
}
}
link.on("mousedown", function(evt) { mousedownTab = evt.currentTarget })
link.on("mouseup",onTabClick);
link.on("click", function(evt) { evt.preventDefault(); })
link.on("dblclick", function(evt) { evt.stopPropagation(); evt.preventDefault(); })
@@ -762,6 +785,9 @@ RED.tabs = (function() {
count: function() {
return ul.find("li.red-ui-tab").length;
},
activeIndex: function() {
return ul.find("li.active").index()
},
contains: function(id) {
return ul.find("a[href='#"+id+"']").length > 0;
},

View File

@@ -312,6 +312,7 @@
}
if (child.depth !== parent.depth+1) {
child.depth = parent.depth+1;
// var labelPaddingWidth = ((child.gutter ? child.gutter[0].offsetWidth + 2 : 0) + (child.depth * 20));
var labelPaddingWidth = ((child.gutter?child.gutter.width()+2:0)+(child.depth*20));
child.treeList.labelPadding.width(labelPaddingWidth+'px');
if (child.element) {
@@ -348,6 +349,18 @@
that._selected.delete(item);
delete item.treeList;
delete that._items[item.id];
if(item.depth === 0) {
for(var key in that._items) {
if (that._items.hasOwnProperty(key)) {
var child = that._items[key];
if(child.parent && child.parent.id === item.id) {
delete that._items[key].treeList;
delete that._items[key];
}
}
}
that._data = that._data.filter(function(data) { return data.id !== item.id})
}
}
item.treeList.insertChildAt = function(newItem,position,select) {
newItem.parent = item;
@@ -480,7 +493,10 @@
if (item.treeList.container) {
$(item.element).remove();
$(element).appendTo(item.treeList.label);
var labelPaddingWidth = (item.gutter?item.gutter.width()+2:0)+(item.depth*20);
// using the JQuery Object, the gutter width will
// be wrong when the element is reattached the second time
var labelPaddingWidth = (item.gutter ? item.gutter[0].offsetWidth + 2 : 0) + (item.depth * 20);
$(element).css({
width: "calc(100% - "+(labelPaddingWidth+20+(item.icon?20:0))+"px)"
})
@@ -517,6 +533,7 @@
}
var labelPaddingWidth = (item.gutter?item.gutter.width()+2:0)+(depth*20);
// var labelPaddingWidth = (item.gutter ? item.gutter[0].offsetWidth + 2 : 0) + (depth * 20)
item.treeList.labelPadding = $('<span>').css({
display: "inline-block",
width: labelPaddingWidth+'px'

View File

@@ -14,8 +14,8 @@
* limitations under the License.
**/
(function($) {
var contextParse = function(v) {
var parts = RED.utils.parseContextKey(v);
var contextParse = function(v,defaultStore) {
var parts = RED.utils.parseContextKey(v, defaultStore&&defaultStore.value);
return {
option: parts.store,
value: parts.key
@@ -32,6 +32,21 @@
return v;
}
}
var contextLabel = function(container,value) {
var that = this;
container.css("pointer-events","none");
container.css("flex-grow",0);
container.css("position",'relative');
container.css("overflow",'visible');
$('<div></div>').text(value).css({
position: "absolute",
bottom:"-2px",
right: "5px",
"font-size": "0.7em",
opacity: 0.3
}).appendTo(container);
this.elementDiv.show();
}
var mapDeprecatedIcon = function(icon) {
if (/^red\/images\/typedInput\/.+\.png$/.test(icon)) {
icon = icon.replace(/.png$/,".svg");
@@ -44,13 +59,15 @@
options:[],
validate:RED.utils.validatePropertyExpression,
parse: contextParse,
export: contextExport
export: contextExport,
valueLabel: contextLabel
},
global: {value:"global",label:"global.",hasValue:true,
options:[],
validate:RED.utils.validatePropertyExpression,
parse: contextParse,
export: contextExport
export: contextExport,
valueLabel: contextLabel
},
str: {value:"str",label:"string",icon:"red/images/typedInput/az.svg"},
num: {value:"num",label:"number",icon:"red/images/typedInput/09.svg",validate:/^[+-]?[0-9]*\.?[0-9]*([eE][-+]?[0-9]+)?$/},
@@ -262,6 +279,14 @@
var contextStores = RED.settings.context.stores;
var contextOptions = contextStores.map(function(store) {
return {value:store,label: store, icon:'<i class="red-ui-typedInput-icon fa fa-database"></i>'}
}).sort(function(A,B) {
if (A.value === RED.settings.context.default) {
return -1;
} else if (B.value === RED.settings.context.default) {
return 1;
} else {
return A.value.localeCompare(B.value);
}
})
if (contextOptions.length < 2) {
allOptions.flow.options = [];
@@ -342,7 +367,17 @@
this.input.on('change', function() {
that.validate();
that.element.val(that.value());
that.element.trigger('change',that.propertyType,that.value());
that.element.trigger('change',[that.propertyType,that.value()]);
});
this.input.on('keyup', function(evt) {
that.validate();
that.element.val(that.value());
that.element.trigger('keyup',evt);
});
this.input.on('paste', function(evt) {
that.validate();
that.element.val(that.value());
that.element.trigger('paste',evt);
});
this.input.on('keydown', function(evt) {
if (evt.keyCode >= 37 && evt.keyCode <= 40) {
@@ -362,6 +397,11 @@
evt.stopPropagation();
}).on('focus', function() {
that.uiSelect.addClass('red-ui-typedInput-focus');
}).on('blur', function() {
var opt = that.typeMap[that.propertyType];
if (opt.hasValue === false) {
that.uiSelect.removeClass('red-ui-typedInput-focus');
}
})
// explicitly set optionSelectTrigger display to inline-block otherwise jQ sets it to 'inline'
@@ -411,7 +451,11 @@
});
this._showMenu(this.optionMenu,this.optionSelectTrigger);
var selectedOption = this.optionMenu.find("[value='"+this.optionValue+"']");
var targetValue = this.optionValue;
if (this.optionValue === null || this.optionValue === undefined) {
targetValue = this.value();
}
var selectedOption = this.optionMenu.find("[value='"+targetValue+"']");
if (selectedOption.length === 0) {
selectedOption = this.optionMenu.children(":first");
}
@@ -582,34 +626,43 @@
_updateOptionSelectLabel: function(o) {
var opt = this.typeMap[this.propertyType];
this.optionSelectLabel.empty();
if (opt.hasValue) {
this.valueLabelContainer.empty();
this.valueLabelContainer.show();
} else {
this.valueLabelContainer.hide();
}
if (this.typeMap[this.propertyType].valueLabel) {
if (opt.multiple) {
this.typeMap[this.propertyType].valueLabel.call(this,this.optionSelectLabel,o);
this.typeMap[this.propertyType].valueLabel.call(this,opt.hasValue?this.valueLabelContainer:this.optionSelectLabel,o);
} else {
this.typeMap[this.propertyType].valueLabel.call(this,this.optionSelectLabel,o.value);
this.typeMap[this.propertyType].valueLabel.call(this,opt.hasValue?this.valueLabelContainer:this.optionSelectLabel,o.value);
}
} else if (!opt.multiple) {
if (o.icon) {
if (o.icon.indexOf("<") === 0) {
$(o.icon).prependTo(this.optionSelectLabel);
} else if (o.icon.indexOf("/") !== -1) {
// url
$('<img>',{src:mapDeprecatedIcon(o.icon),style:"height: 18px;"}).prependTo(this.optionSelectLabel);
}
if (!this.typeMap[this.propertyType].valueLabel || opt.hasValue) {
if (!opt.multiple) {
if (o.icon) {
if (o.icon.indexOf("<") === 0) {
$(o.icon).prependTo(this.optionSelectLabel);
} else if (o.icon.indexOf("/") !== -1) {
// url
$('<img>',{src:mapDeprecatedIcon(o.icon),style:"height: 18px;"}).prependTo(this.optionSelectLabel);
} else {
// icon class
$('<i>',{class:"red-ui-typedInput-icon "+o.icon}).prependTo(this.optionSelectLabel);
}
} else if (o.label) {
this.optionSelectLabel.text(o.label);
} else {
// icon class
$('<i>',{class:"red-ui-typedInput-icon "+o.icon}).prependTo(this.optionSelectLabel);
this.optionSelectLabel.text(o.value);
}
if (opt.hasValue) {
this.optionValue = o.value;
this.input.trigger('change',[this.propertyType,this.value()]);
}
} else if (o.label) {
this.optionSelectLabel.text(o.label);
} else {
this.optionSelectLabel.text(o.value);
this.optionSelectLabel.text(o.length+" selected");
}
if (opt.hasValue) {
this.optionValue = o.value;
this.input.trigger('change',this.propertyType,this.value());
}
} else {
this.optionSelectLabel.text(o.length+" selected");
}
},
_destroy: function() {
@@ -633,6 +686,11 @@
that.typeMap[result.value] = result;
return result;
});
if (this.typeList.length < 2) {
this.selectTrigger.attr("tabindex", -1)
} else {
this.selectTrigger.attr("tabindex", 0)
}
this.selectTrigger.toggleClass("disabled", this.typeList.length === 1);
this.selectTrigger.find(".fa-caret-down").toggle(this.typeList.length > 1)
if (this.menu) {
@@ -699,7 +757,7 @@
opt.valueLabel.call(this,this.valueLabelContainer,value);
}
}
this.input.trigger('change',this.type(),value);
this.input.trigger('change',[this.type(),value]);
}
},
type: function(type) {
@@ -732,6 +790,11 @@
if (opt.hasValue === false || (opt.showLabel !== false && !opt.icon)) {
this.selectLabel.text(opt.label);
}
if (opt.label) {
this.selectTrigger.attr("title",opt.label);
} else {
this.selectTrigger.attr("title","");
}
if (opt.hasValue === false) {
this.selectTrigger.addClass("red-ui-typedInput-full-width");
} else {
@@ -819,7 +882,7 @@
} else {
var selectedOption = this.optionValue||opt.options[0];
if (opt.parse) {
var parts = opt.parse(this.input.val());
var parts = opt.parse(this.input.val(),selectedOption);
if (parts.option) {
selectedOption = parts.option;
if (!this.activeOptions.hasOwnProperty(selectedOption)) {
@@ -864,7 +927,7 @@
});
}
this._trigger("typechange",null,this.propertyType);
this.input.trigger('change',this.propertyType,this.value());
this.input.trigger('change',[this.propertyType,this.value()]);
} else {
if (this.optionSelectTrigger) {
this.optionSelectTrigger.hide();
@@ -883,6 +946,7 @@
// Reset any CSS the custom label may have set
this.valueLabelContainer.css("pointer-events","");
this.valueLabelContainer.css("flex-grow",1);
this.valueLabelContainer.css("overflow","hidden");
this.valueLabelContainer.show();
this.valueLabelContainer.empty();
this.elementDiv.hide();
@@ -934,7 +998,7 @@
}
}
this._trigger("typechange",null,this.propertyType);
this.input.trigger('change',this.propertyType,this.value());
this.input.trigger('change',[this.propertyType,this.value()]);
}
}
}
@@ -967,16 +1031,17 @@
this.uiSelect.hide();
},
disable: function(val) {
if(val === true) {
if(val === undefined || !!val ) {
this.uiSelect.attr("disabled", "disabled");
} else if (val === false) {
this.uiSelect.attr("disabled", null); //remove attr
} else {
this.uiSelect.attr("disabled", val); //user value
this.uiSelect.attr("disabled", null); //remove attr
}
},
enable: function() {
this.uiSelect.attr("disabled", null); //remove attr
},
disabled: function() {
return this.uiSelect.attr("disabled");
return this.uiSelect.attr("disabled") === "disabled";
}
});
})(jQuery);

View File

@@ -356,14 +356,14 @@ RED.editor = (function() {
function attachPropertyChangeHandler(node,definition,property,prefix) {
var input = $("#"+prefix+"-"+property);
if (definition !== undefined && "format" in definition[property] && definition[property].format !== "" && input[0].nodeName === "DIV") {
$("#"+prefix+"-"+property).on('change keyup', function(event,skipValidation) {
if (!skipValidation) {
$("#"+prefix+"-"+property).on('change keyup', function(event) {
if (!$(this).attr("skipValidation")) {
validateNodeEditor(node,prefix);
}
});
} else {
$("#"+prefix+"-"+property).on("change", function(event,skipValidation) {
if (!skipValidation) {
$("#"+prefix+"-"+property).on("change", function(event) {
if (!$(this).attr("skipValidation")) {
validateNodeEditor(node,prefix);
}
});
@@ -414,18 +414,20 @@ RED.editor = (function() {
for (var cred in credDefinition) {
if (credDefinition.hasOwnProperty(cred)) {
var input = $("#" + prefix + '-' + cred);
var value = input.val();
if (credDefinition[cred].type == 'password') {
node.credentials['has_' + cred] = (value !== "");
if (value == '__PWRD__') {
continue;
}
changed = true;
if (input.length > 0) {
var value = input.val();
if (credDefinition[cred].type == 'password') {
node.credentials['has_' + cred] = (value !== "");
if (value == '__PWRD__') {
continue;
}
changed = true;
}
node.credentials[cred] = value;
if (value != node.credentials._[cred]) {
changed = true;
}
node.credentials[cred] = value;
if (value != node.credentials._[cred]) {
changed = true;
}
}
}
}
@@ -442,16 +444,18 @@ RED.editor = (function() {
for (var d in definition.defaults) {
if (definition.defaults.hasOwnProperty(d)) {
if (definition.defaults[d].type) {
var configTypeDef = RED.nodes.getType(definition.defaults[d].type);
if (configTypeDef) {
if (configTypeDef.exclusive) {
prepareConfigNodeButton(node,d,definition.defaults[d].type,prefix);
if (!definition.defaults[d]._type.array) {
var configTypeDef = RED.nodes.getType(definition.defaults[d].type);
if (configTypeDef && configTypeDef.category === 'config') {
if (configTypeDef.exclusive) {
prepareConfigNodeButton(node,d,definition.defaults[d].type,prefix);
} else {
prepareConfigNodeSelect(node,d,definition.defaults[d].type,prefix);
}
} else {
prepareConfigNodeSelect(node,d,definition.defaults[d].type,prefix);
console.log("Unknown type:", definition.defaults[d].type);
preparePropertyEditor(node,d,prefix,definition.defaults);
}
} else {
console.log("Unknown type:", definition.defaults[d].type);
preparePropertyEditor(node,d,prefix,definition.defaults);
}
} else {
preparePropertyEditor(node,d,prefix,definition.defaults);
@@ -465,19 +469,34 @@ RED.editor = (function() {
definition.oneditprepare.call(node);
} catch(err) {
console.log("oneditprepare",node.id,node.type,err.toString());
console.log(err.stack);
}
}
// Now invoke any change handlers added to the fields - passing true
// to prevent full node validation from being triggered each time
for (var d in definition.defaults) {
if (definition.defaults.hasOwnProperty(d)) {
$("#"+prefix+"-"+d).trigger("change",[true]);
var el = $("#"+prefix+"-"+d);
el.attr("skipValidation", true);
if (el.data("noderedTypedInput") !== undefined) {
el.trigger("change",[el.typedInput('type'),el.typedInput('value')]);
} else {
el.trigger("change");
}
el.removeAttr("skipValidation");
}
}
if (definition.credentials) {
for (d in definition.credentials) {
if (definition.credentials.hasOwnProperty(d)) {
$("#"+prefix+"-"+d).trigger("change",[true]);
var el = $("#"+prefix+"-"+d);
el.attr("skipValidation", true);
if (el.data("noderedTypedInput") !== undefined) {
el.trigger("change",[el.typedInput('type'),el.typedInput('value')]);
} else {
el.trigger("change");
}
el.removeAttr("skipValidation");
}
}
}
@@ -491,11 +510,13 @@ RED.editor = (function() {
populateCredentialsInputs(node, definition.credentials, node.credentials, prefix);
completePrepare();
} else {
$.getJSON(getCredentialsURL(node.type, node.id), function (data) {
node.credentials = data;
node.credentials._ = $.extend(true,{},data);
if (!/^subflow:/.test(definition.type)) {
populateCredentialsInputs(node, definition.credentials, node.credentials, prefix);
getNodeCredentials(node.type, node.id, function(data) {
if (data) {
node.credentials = data;
node.credentials._ = $.extend(true,{},data);
if (!/^subflow:/.test(definition.type)) {
populateCredentialsInputs(node, definition.credentials, node.credentials, prefix);
}
}
completePrepare();
});
@@ -1083,8 +1104,11 @@ RED.editor = (function() {
node.infoEditor = nodeInfoEditor;
return nodeInfoEditor;
}
var buildingEditDialog = false;
function showEditDialog(node, defaultTab) {
if (buildingEditDialog) { return }
buildingEditDialog = true;
var editing_node = node;
var isDefaultIcon;
var defaultIcon;
@@ -1192,7 +1216,7 @@ RED.editor = (function() {
changed = true;
}
} catch(err) {
console.log("oneditsave",editing_node.id,editing_node.type,err.toString());
console.warn("oneditsave",editing_node.id,editing_node.type,err.toString());
}
for (d in editing_node._def.defaults) {
@@ -1609,6 +1633,7 @@ RED.editor = (function() {
if (defaultTab) {
editorTabs.activateTab(defaultTab);
}
buildingEditDialog = false;
done();
});
},
@@ -1660,6 +1685,8 @@ RED.editor = (function() {
* prefix - the input prefix of the parent property
*/
function showEditConfigNodeDialog(name,type,id,prefix) {
if (buildingEditDialog) { return }
buildingEditDialog = true;
var adding = (id == "_ADD_");
var node_def = RED.nodes.getType(type);
var editing_config_node = RED.nodes.node(id);
@@ -1823,6 +1850,7 @@ RED.editor = (function() {
trayBody.i18n();
trayFooter.i18n();
finishedBuilding = true;
buildingEditDialog = false;
done();
});
},
@@ -1890,7 +1918,7 @@ RED.editor = (function() {
try {
configTypeDef.oneditsave.call(editing_config_node);
} catch(err) {
console.log("oneditsave",editing_config_node.id,editing_config_node.type,err.toString());
console.warn("oneditsave",editing_config_node.id,editing_config_node.type,err.toString());
}
}
@@ -2146,6 +2174,8 @@ RED.editor = (function() {
}
function showEditSubflowDialog(subflow) {
if (buildingEditDialog) { return }
buildingEditDialog = true;
var editing_node = subflow;
editStack.push(subflow);
RED.view.state(RED.state.EDITING);
@@ -2250,6 +2280,14 @@ RED.editor = (function() {
changed = true;
}
var newMeta = RED.subflow.exportSubflowModuleProperties(editing_node);
if (!isSameObj(editing_node.meta,newMeta)) {
changes.meta = editing_node.meta;
editing_node.meta = newMeta;
changed = true;
}
if (changed) {
var wasChanged = editing_node.changed;
editing_node.changed = true;
@@ -2356,6 +2394,16 @@ RED.editor = (function() {
};
editorTabs.addTab(nodePropertiesTab);
var moduleTab = {
id: "editor-tab-module",
label: RED._("editor-tab.module"),
name: RED._("editor-tab.module"),
content: $('<div>', {class:"red-ui-tray-content"}).appendTo(editorContent).hide(),
iconClass: "fa fa-cube",
};
editorTabs.addTab(moduleTab);
RED.subflow.buildModuleForm(moduleTab.content, editing_node);
var descriptionTab = {
id: "editor-tab-description",
label: RED._("editor-tab.description"),
@@ -2384,15 +2432,17 @@ RED.editor = (function() {
buildEditForm(nodePropertiesTab.content,"dialog-form","subflow-template", undefined, editing_node);
trayBody.i18n();
$.getJSON(getCredentialsURL("subflow", subflow.id), function (data) {
subflow.credentials = data;
subflow.credentials._ = $.extend(true,{},data);
getNodeCredentials("subflow", subflow.id, function(data) {
if (data) {
subflow.credentials = data;
subflow.credentials._ = $.extend(true,{},data);
}
$("#subflow-input-name").val(subflow.name);
RED.text.bidi.prepareInput($("#subflow-input-name"));
finishedBuilding = true;
buildingEditDialog = false;
done();
});
},
@@ -2413,7 +2463,39 @@ RED.editor = (function() {
RED.tray.show(trayOptions);
}
function getNodeCredentials(type, id, done) {
var timeoutNotification;
var intialTimeout = setTimeout(function() {
timeoutNotification = RED.notify($('<p data-i18n="[prepend]editor.loadCredentials"> <img src="red/images/spin.svg"/></p>').i18n(),{fixed: true})
},800);
$.ajax({
url: getCredentialsURL(type,id),
dataType: 'json',
success: function(data) {
if (timeoutNotification) {
timeoutNotification.close();
timeoutNotification = null;
}
clearTimeout(intialTimeout);
done(data);
},
error: function(jqXHR,status,error) {
if (timeoutNotification) {
timeoutNotification.close();
timeoutNotification = null;
}
clearTimeout(intialTimeout);
RED.notify(RED._("editor.errors.credentialLoadFailed"),"error")
done(null);
},
timeout: 30000,
});
}
function showEditGroupDialog(group) {
if (buildingEditDialog) { return }
buildingEditDialog = true;
var editing_node = group;
editStack.push(group);
RED.view.state(RED.state.EDITING);
@@ -2457,7 +2539,7 @@ RED.editor = (function() {
changed = true;
}
} catch(err) {
console.log("oneditsave",editing_node.id,editing_node.type,err.toString());
console.warn("oneditsave",editing_node.id,editing_node.type,err.toString());
}
for (d in editing_node._def.defaults) {
@@ -2637,6 +2719,7 @@ RED.editor = (function() {
prepareEditDialog(group,group._def,"node-input", function() {
trayBody.i18n();
finishedBuilding = true;
buildingEditDialog = false;
done();
});
},

View File

@@ -20,6 +20,9 @@ RED.keyboard = (function() {
var handlersActive = true;
var handlers = {};
var knownShortcuts;
var partialState;
var keyMap = {
@@ -34,19 +37,24 @@ RED.keyboard = (function() {
"space": 32,
";":186,
"=":187,
"+":187, // <- QWERTY specific
",":188,
"-":189,
".":190,
"/":191,
"\\":220,
"'":222,
"?":191 // <- QWERTY specific
"?":191, // <- QWERTY specific
"[": 219,
"]": 221,
"{": 219,// <- QWERTY specific
"}": 221 // <- QWERTY specific
}
var metaKeyCodes = {
16:true,
17:true,
16: true,
17: true,
18: true,
91:true,
91: true,
93: true
}
var actionToKeyMap = {}
@@ -60,46 +68,90 @@ RED.keyboard = (function() {
}
function migrateOldKeymap() {
// pre-0.18
if ('localStorage' in window && window['localStorage'] !== null) {
var oldKeyMap = localStorage.getItem("keymap");
if (oldKeyMap !== null) {
localStorage.removeItem("keymap");
var currentEditorSettings = RED.settings.get('editor') || {};
currentEditorSettings.keymap = JSON.parse(oldKeyMap);
RED.settings.set('editor',currentEditorSettings);
RED.settings.set('editor.keymap',JSON.parse(oldKeyMap));
}
}
}
function getUserKey(action) {
var currentEditorSettings = RED.settings.get('editor') || {};
var userKeymap = currentEditorSettings.keymap || {};
return userKeymap[action];
return RED.settings.get('editor.keymap',{})[action]
}
function init() {
// Migrate from pre-0.18
migrateOldKeymap();
var currentEditorSettings = RED.settings.get('editor') || {};
var userKeymap = currentEditorSettings.keymap || {};
function mergeKeymaps(defaultKeymap, themeKeymap) {
// defaultKeymap has format: { scope: { key: action , key: action }}
// themeKeymap has format: {action: {scope,key}, action: {scope:key}}
$.getJSON("red/keymap.json",function(data) {
for (var scope in data) {
if (data.hasOwnProperty(scope)) {
var keys = data[scope];
for (var key in keys) {
if (keys.hasOwnProperty(key)) {
if (!userKeymap.hasOwnProperty(keys[key])) {
addHandler(scope,key,keys[key],false);
}
defaultKeyMap[keys[key]] = {
var mergedKeymap = {};
for (var scope in defaultKeymap) {
if (defaultKeymap.hasOwnProperty(scope)) {
var keys = defaultKeymap[scope];
for (var key in keys) {
if (keys.hasOwnProperty(key)) {
if (!mergedKeymap[keys[key]]) {
mergedKeymap[keys[key]] = [{
scope:scope,
key:key,
user:false
};
}];
} else {
mergedKeymap[keys[key]].push({
scope:scope,
key:key,
user:false
})
}
}
}
}
}
for (var action in themeKeymap) {
if (themeKeymap.hasOwnProperty(action)) {
if (!themeKeymap[action].key) {
// No key for this action - default is no keybinding
delete mergedKeymap[action]
} else {
mergedKeymap[action] = [{
scope: themeKeymap[action].scope || "*",
key: themeKeymap[action].key,
user: false
}]
if (mergedKeymap[action][0].scope === "workspace") {
mergedKeymap[action][0].scope = "red-ui-workspace";
}
}
}
}
return mergedKeymap;
}
function init() {
// Migrate from pre-0.18
migrateOldKeymap();
var userKeymap = RED.settings.get('editor.keymap', {});
$.getJSON("red/keymap.json",function(defaultKeymap) {
var keymap = mergeKeymaps(defaultKeymap, RED.settings.theme('keymap',{}));
// keymap has the format: {action: [{scope,key},{scope,key}], action: [{scope:key}]}
var action;
for (action in keymap) {
if (keymap.hasOwnProperty(action)) {
if (!userKeymap.hasOwnProperty(action)) {
keymap[action].forEach(function(km) {
addHandler(km.scope,km.key,action,false);
});
}
defaultKeyMap[action] = keymap[action][0];
}
}
for (var action in userKeymap) {
if (userKeymap.hasOwnProperty(action) && userKeymap[action]) {
var obj = userKeymap[action];
@@ -408,14 +460,21 @@ RED.keyboard = (function() {
container.addClass('keyboard-shortcut-entry-expanded');
var keyInput = $('<input type="text">').attr('placeholder',RED._('keyboard.unassigned')).val(object.key||"").appendTo(key);
keyInput.on("keyup",function(e) {
if (e.keyCode === 13) {
keyInput.on("change paste keyup",function(e) {
if (e.keyCode === 13 && !$(this).hasClass("input-error")) {
return endEditShortcut();
}
if (e.keyCode === 27) {
return endEditShortcut(true);
}
var currentVal = $(this).val();
currentVal = currentVal.trim();
var valid = (currentVal === "" || RED.keyboard.validateKey(currentVal));
if (valid && currentVal !== "") {
valid = !knownShortcuts.has(scopeSelect.val()+":"+currentVal.toLowerCase());
}
$(this).toggleClass("input-error",!valid);
okButton.attr("disabled",!valid);
})
var scopeSelect = $('<select><option value="*" data-i18n="keyboard.global"></option><option value="red-ui-workspace" data-i18n="keyboard.workspace"></option></select>').appendTo(scope);
@@ -424,6 +483,9 @@ RED.keyboard = (function() {
object.scope = "red-ui-workspace";
}
scopeSelect.val(object.scope||'*');
scopeSelect.on("change", function() {
keyInput.trigger("change");
})
var div = $('<div class="keyboard-shortcut-edit button-group-vertical"></div>').appendTo(scope);
var okButton = $('<button class="red-ui-button red-ui-button-small"><i class="fa fa-check"></i></button>').appendTo(div);
@@ -437,13 +499,10 @@ RED.keyboard = (function() {
e.stopPropagation();
container.empty();
container.removeClass('keyboard-shortcut-entry-expanded');
// var userKeymap = RED.settings.get('keymap') || {};
var currentEditorSettings = RED.settings.get('editor') || {};
var userKeymap = currentEditorSettings.keymap || {};
var userKeymap = RED.settings.get('editor.keymap', {});
userKeymap[object.id] = null;
currentEditorSettings.keymap = userKeymap;
RED.settings.set('editor',currentEditorSettings);
RED.settings.set('editor.keymap',userKeymap);
RED.keyboard.revertToDefault(object.id);
@@ -479,6 +538,7 @@ RED.keyboard = (function() {
keyDiv.empty();
scopeDiv.empty();
if (object.key) {
knownShortcuts.delete(object.scope+":"+object.key);
RED.keyboard.remove(object.key,true);
}
container.find(".keyboard-shortcut-entry-text i").css("opacity",1);
@@ -493,14 +553,17 @@ RED.keyboard = (function() {
$("<span>").text(scope).appendTo(scopeDiv);
object.key = key;
object.scope = scope;
knownShortcuts.add(object.scope+":"+object.key);
RED.keyboard.add(object.scope,object.key,object.id,true);
}
var currentEditorSettings = RED.settings.get('editor') || {};
var userKeymap = currentEditorSettings.keymap || {};
userKeymap[object.id] = RED.keyboard.getShortcut(object.id);
currentEditorSettings.keymap = userKeymap;
RED.settings.set('editor',currentEditorSettings);
var userKeymap = RED.settings.get('editor.keymap', {});
var shortcut = RED.keyboard.getShortcut(object.id);
userKeymap[object.id] = {
scope:shortcut.scope,
key:shortcut.key
}
RED.settings.set('editor.keymap',userKeymap);
}
}
}
@@ -588,7 +651,11 @@ RED.keyboard = (function() {
var Bid = B.id.replace(/^.*:/,"").replace(/[ -]/g,"").toLowerCase();
return Aid.localeCompare(Bid);
});
knownShortcuts = new Set();
shortcuts.forEach(function(s) {
if (s.key) {
knownShortcuts.add(s.scope+":"+s.key);
}
shortcutList.editableList('addItem',s);
});
return pane;

View File

@@ -216,31 +216,7 @@ RED.library = (function() {
{ id:'node-input-'+options.type+'-menu-open-library',
label: RED._("library.openLibrary"),
onselect: function() {
activeLibrary = options;
loadLibraryFolder("local",options.url, "", function(items) {
var listing = [{
library: "local",
type: options.url,
icon: 'fa fa-hdd-o',
label: RED._("library.types.local"),
path: "",
expanded: true,
writable: false,
children: [{
library: "local",
type: options.url,
icon: 'fa fa-cube',
label: options.type,
path: "",
expanded: true,
children: items
}]
}]
loadLibraryBrowser.data(listing);
setTimeout(function() {
loadLibraryBrowser.select(listing[0].children[0]);
},200);
});
libraryEditor = ace.edit('red-ui-library-dialog-load-preview-text',{
useWorker: false
});
@@ -256,6 +232,43 @@ RED.library = (function() {
libraryEditor.renderer.$cursorLayer.element.style.opacity=0;
libraryEditor.$blockScrolling = Infinity;
activeLibrary = options;
var listing = [];
var libraries = RED.settings.libraries || [];
libraries.forEach(function(lib) {
if (lib.types && lib.types.indexOf(options.url) === -1) {
return;
}
listing.push({
library: lib.id,
type: options.url,
icon: lib.icon || 'fa fa-hdd-o',
label: RED._(lib.label||lib.id),
path: "",
expanded: true,
writable: false,
children: [{
library: lib.id,
type: options.url,
icon: 'fa fa-cube',
label: options.type,
path: "",
expanded: false,
children: function(done, item) {
loadLibraryFolder(lib.id, options.url, "", function(children) {
item.children = children;
done(children);
})
}
}]
})
});
loadLibraryBrowser.data(listing);
setTimeout(function() {
loadLibraryBrowser.select(listing[0].children[0]);
},200);
var dialogHeight = 400;
var winHeight = $(window).height();
if (winHeight < 570) {
@@ -278,30 +291,40 @@ RED.library = (function() {
}
$("#red-ui-library-dialog-save-filename").attr("value",filename+"."+(options.ext||"txt"));
loadLibraryFolder("local",options.url, "", function(items) {
var listing = [{
library: "local",
var listing = [];
var libraries = RED.settings.libraries || [];
libraries.forEach(function(lib) {
if (lib.types && lib.types.indexOf(options.url) === -1) {
return;
}
listing.push({
library: lib.id,
type: options.url,
icon: 'fa fa-hdd-o',
label: RED._("library.types.local"),
icon: lib.icon || 'fa fa-hdd-o',
label: RED._(lib.label||lib.id),
path: "",
expanded: true,
writable: false,
children: [{
library: "local",
library: lib.id,
type: options.url,
icon: 'fa fa-cube',
label: options.type,
path: "",
expanded: true,
children: items
expanded: false,
children: function(done, item) {
loadLibraryFolder(lib.id, options.url, "", function(children) {
item.children = children;
done(children);
})
}
}]
}]
saveLibraryBrowser.data(listing);
setTimeout(function() {
saveLibraryBrowser.select(listing[0].children[0]);
},200);
})
});
saveLibraryBrowser.data(listing);
setTimeout(function() {
saveLibraryBrowser.select(listing[0].children[0]);
},200);
var dialogHeight = 400;
var winHeight = $(window).height();
@@ -460,9 +483,235 @@ RED.library = (function() {
}
}
// var libraryPlugins = {};
//
// function showLibraryDetailsDialog(container, lib, done) {
// var dialog = $('<div>').addClass("red-ui-projects-dialog-list-dialog").hide().appendTo(container);
// $('<div>').addClass("red-ui-projects-dialog-list-dialog-header").text(lib?"Edit library source":"Add library source").appendTo(dialog);
// var formRow = $('<div class="red-ui-settings-row"></div>').appendTo(dialog);
// $('<label>').text("Type").appendTo(formRow);
// var typeSelect = $('<select>').appendTo(formRow);
// for (var type in libraryPlugins) {
// if (libraryPlugins.hasOwnProperty(type)) {
// $('<option>').attr('value',type).attr('selected',(lib && lib.type === type)?true:null).text(libraryPlugins[type].name).appendTo(typeSelect);
// }
// }
// var dialogBody = $("<div>").addClass("red-ui-settings-section").appendTo(dialog);
// var libraryFields = {};
// var fieldsModified = {};
// function validateFields() {
// var validForm = true;
// for (var p in libraryFields) {
// if (libraryFields.hasOwnProperty(p)) {
// var v = libraryFields[p].input.val().trim();
// if (v === "") {
// validForm = false;
// if (libraryFields[p].modified) {
// libraryFields[p].input.addClass("input-error");
// }
// } else {
// libraryFields[p].input.removeClass("input-error");
// }
// }
// }
// okayButton.attr("disabled",validForm?null:"disabled");
// }
// typeSelect.on("change", function(evt) {
// dialogBody.empty();
// libraryFields = {};
// fieldsModified = {};
// var libDef = libraryPlugins[$(this).val()];
// var defaultIcon = lib?lib.icon:(libDef.icon || "font-awesome/fa-image");
// formRow = $('<div class="red-ui-settings-row"></div>').appendTo(dialogBody);
// $('<label>').text(RED._("editor.settingIcon")).appendTo(formRow);
// libraryFields['icon'] = {input: $('<input type="hidden">').val(defaultIcon) };
// var iconButton = $('<button type="button" class="red-ui-button"></button>').appendTo(formRow);
// iconButton.on("click", function(evt) {
// evt.preventDefault();
// var icon = libraryFields['icon'].input.val() || "";
// var iconPath = (icon ? RED.utils.separateIconPath(icon) : {});
// RED.editor.showIconPicker(iconButton, null, iconPath, true, function (newIcon) {
// iconButton.empty();
// var path = newIcon || "";
// var newPath = RED.utils.separateIconPath(path);
// if (newPath) {
// $('<i class="fa"></i>').addClass(newPath.file).appendTo(iconButton);
// }
// libraryFields['icon'].input.val(path);
// });
// })
// var newPath = RED.utils.separateIconPath(defaultIcon);
// $('<i class="fa '+newPath.file+'"></i>').appendTo(iconButton);
//
// var libProps = libDef.defaults;
// var libPropKeys = Object.keys(libProps).map(function(p) { return {id: p, def: libProps[p]}});
// libPropKeys.unshift({id: "label", def: {value:""}})
//
// libPropKeys.forEach(function(prop) {
// var p = prop.id;
// var def = prop.def;
// formRow = $('<div class="red-ui-settings-row"></div>').appendTo(dialogBody);
// var label = libDef._(def.label || "label."+p,{defaultValue: p});
// if (label === p) {
// label = libDef._("editor:common.label."+p,{defaultValue:p});
// }
// $('<label>').text(label).appendTo(formRow);
// libraryFields[p] = {
// input: $('<input type="text">').val(lib?(lib[p]||lib.config[p]):def.value).appendTo(formRow),
// modified: false
// }
// if (def.type === "password") {
// libraryFields[p].input.attr("type","password").typedInput({type:"cred"})
// }
//
// libraryFields[p].input.on("change paste keyup", function(evt) {
// if (!evt.key || evt.key.length === 1) {
// libraryFields[p].modified = true;
// }
// validateFields();
// })
// var desc = libDef._("desc."+p, {defaultValue: ""});
// if (desc) {
// $('<label class="red-ui-projects-edit-form-sublabel"></label>').append($('<small>').text(desc)).appendTo(formRow);
// }
// });
// validateFields();
// })
//
// var dialogButtons = $('<span class="button-row" style="position: relative; float: right; margin: 10px;"></span>').appendTo(dialog);
// var cancelButton = $('<button class="red-ui-button"></button>').text(RED._("common.label.cancel")).appendTo(dialogButtons).on("click", function(evt) {
// evt.preventDefault();
// done(false);
// })
// var okayButton = $('<button class="red-ui-button"></button>').text(lib?"Update library":"Add library").appendTo(dialogButtons).on("click", function(evt) {
// evt.preventDefault();
// var item;
// if (!lib) {
// item = {
// id: libraryFields['label'].input.val().trim().toLowerCase().replace(/( |[^a-z0-9])/g,"-"),
// user: true,
// type: typeSelect.val(),
// config: {}
// }
// } else {
// item = lib;
// }
//
// item.label = libraryFields['label'].input.val().trim();
// item.icon = libraryFields['icon'].input.val();
//
// for (var p in libraryFields) {
// if (libraryFields.hasOwnProperty(p) && p !== 'label') {
// item.config[p] = libraryFields[p].input.val().trim();
// }
// }
// done(item);
// });
//
// typeSelect.trigger("change");
// if (lib) {
// typeSelect.attr('disabled',true);
// }
//
// dialog.slideDown(200);
// }
//
// function createSettingsPane() {
// var pane = $('<div id="red-ui-settings-tab-library-manager"></div>');
// var toolbar = $('<div>').css("text-align","right").appendTo(pane);
// var addButton = $('<button class="red-ui-button"><i class="fa fa-plus"></i> Add library</button>').appendTo(toolbar);
//
// var addingLibrary = false;
//
// var libraryList = $("<ol>").css({
// position: "absolute",
// left: "10px",
// right: "10px",
// top: "50px",
// bottom: "10px"
// }).appendTo(pane).editableList({
// addButton: false,
// addItem: function(row,index,itemData) {
// if (itemData.id) {
// row.addClass("red-ui-settings-tab-library-entry");
// var iconCell = $("<span>").appendTo(row);
// if (itemData.icon) {
// var iconPath = RED.utils.separateIconPath(itemData.icon);
// if (iconPath) {
// $("<i>").addClass("fa "+iconPath.file).appendTo(iconCell);
// }
// }
// $("<span>").text(RED._(itemData.label)).appendTo(row);
// $("<span>").text(RED._(itemData.type)).appendTo(row);
// $('<button class="red-ui-button red-ui-button-small"></button>').text(RED._("sidebar.project.projectSettings.edit")).appendTo(
// $('<span>').appendTo(row)
// ).on("click", function(evt) {
// if (addingLibrary) {
// return;
// }
// evt.preventDefault();
// addingLibrary = true;
// row.empty();
// row.removeClass("red-ui-settings-tab-library-entry");
// showLibraryDetailsDialog(row,itemData,function(newItem) {
// var itemIndex = libraryList.editableList("indexOf", itemData);
// libraryList.editableList("removeItem", itemData);
// if (newItem) {
// libraryList.editableList("insertItemAt", newItem, itemIndex);
// } else {
// libraryList.editableList("insertItemAt", itemData,itemIndex);
// }
// addingLibrary = false;
//
// })
// })
//
// } else {
// showLibraryDetailsDialog(row,null,function(newItem) {
// libraryList.editableList("removeItem", itemData);
// if (newItem) {
// libraryList.editableList("addItem", newItem);
// }
// addingLibrary = false;
// })
//
// }
// }
// });
//
// addButton.on('click', function(evt) {
// evt.preventDefault();
// if (!addingLibrary) {
// addingLibrary = true;
// libraryList.editableList("addItem",{user:true});
// }
// })
// var libraries = RED.settings.libraries || [];
// libraries.forEach(function(library) {
// if (library.user) {
// libraryList.editableList("addItem",library)
// }
// })
//
// return pane;
// }
//
//
return {
init: function() {
// RED.events.on("registry:plugin-added", function(id) {
// var plugin = RED.plugins.getPlugin(id);
// if (plugin.type === "node-red-library-source") {
// libraryPlugins[id] = plugin;
// }
// });
//
// RED.userSettings.add({
// id:'library-manager',
// title: "NLS: Libraries",
// get: createSettingsPane,
// close: function() {}
// });
$(_librarySave).appendTo("#red-ui-editor").i18n();
$(_libraryLookup).appendTo("#red-ui-editor").i18n();

View File

@@ -31,15 +31,53 @@ RED.palette.editor = (function() {
var eventTimers = {};
var activeFilter = "";
function semVerCompare(A,B) {
var aParts = A.split(".").map(function(m) { return parseInt(m);});
var bParts = B.split(".").map(function(m) { return parseInt(m);});
for (var i=0;i<3;i++) {
var j = aParts[i]-bParts[i];
if (j<0) { return -1 }
if (j>0) { return 1 }
var semverre = /^(\d+)(\.(\d+))?(\.(\d+))?(-([0-9A-Za-z-]+))?(\.([0-9A-Za-z-.]+))?$/;
var NUMBERS_ONLY = /^\d+$/;
function SemVerPart(part) {
this.number = 0;
this.text = part;
if ( NUMBERS_ONLY.test(part)){
this.number = parseInt(part);
this.type = "N";
} else {
this.type = part == undefined || part.length < 1 ? "E" : "T";
}
return 0;
}
SemVerPart.prototype.compare = function(other) {
var types = this.type + other.type;
switch ( types ) {
case "EE": return 0;
case "NT":
case "TE":
case "EN": return -1;
case "NN": return this.number - other.number;
case "TT": return this.text.localeCompare( other.text );
case "ET":
case "TN":
case "NE": return 1;
}
};
function SemVer(ver) {
var groups = ver.match( semverre );
this.parts = [ new SemVerPart( groups[1] ), new SemVerPart( groups[3] ), new SemVerPart( groups[5] ), new SemVerPart( groups[7] ), new SemVerPart( groups[9] ) ];
}
SemVer.prototype.compare = function(other) {
var result = 0;
for ( var i = 0, n = this.parts.length; result == 0 && i < n; i++ ) {
result = this.parts[ i ].compare( other.parts[ i ] );
}
return result;
};
function semVerCompare(ver1, ver2) {
var semver1 = new SemVer(ver1);
var semver2 = new SemVer(ver2);
var result = semver1.compare(semver2);
return result;
}
function delayCallback(start,callback) {
@@ -293,7 +331,7 @@ RED.palette.editor = (function() {
nodeEntry.versionSpan.html(moduleInfo.version+' <i class="fa fa-long-arrow-right"></i> '+moduleInfo.pending_version).appendTo(nodeEntry.metaRow)
nodeEntry.updateButton.text(RED._('palette.editor.updated')).addClass('disabled').css('display', 'inline-block');
} else if (loadedIndex.hasOwnProperty(module)) {
if (semVerCompare(loadedIndex[module].version,moduleInfo.version) === 1) {
if (semVerCompare(loadedIndex[module].version,moduleInfo.version) > 0) {
nodeEntry.updateButton.show();
nodeEntry.updateButton.text(RED._('palette.editor.update',{version:loadedIndex[module].version}));
} else {
@@ -329,21 +367,26 @@ RED.palette.editor = (function() {
catalogueLoadStatus.push(err||v);
if (!err) {
if (v.modules) {
v.modules.forEach(function(m) {
loadedIndex[m.id] = m;
m.index = [m.id];
if (m.keywords) {
m.index = m.index.concat(m.keywords);
var a = false;
v.modules = v.modules.filter(function(m) {
if (RED.utils.checkModuleAllowed(m.id,m.version,installAllowList,installDenyList)) {
loadedIndex[m.id] = m;
m.index = [m.id];
if (m.keywords) {
m.index = m.index.concat(m.keywords);
}
if (m.types) {
m.index = m.index.concat(m.types);
}
if (m.updated_at) {
m.timestamp = new Date(m.updated_at).getTime();
} else {
m.timestamp = 0;
}
m.index = m.index.join(",").toLowerCase();
return true;
}
if (m.types) {
m.index = m.index.concat(m.types);
}
if (m.updated_at) {
m.timestamp = new Date(m.updated_at).getTime();
} else {
m.timestamp = 0;
}
m.index = m.index.join(",").toLowerCase();
return false;
})
loadedList = loadedList.concat(v.modules);
}
@@ -437,11 +480,22 @@ RED.palette.editor = (function() {
return -1 * (A.info.timestamp-B.info.timestamp);
}
var installAllowList = ['*'];
var installDenyList = [];
function init() {
if (RED.settings.theme('palette.editable') === false) {
if (RED.settings.get('externalModules.palette.allowInstall', true) === false) {
return;
}
var settingsAllowList = RED.settings.get("externalModules.palette.allowList")
var settingsDenyList = RED.settings.get("externalModules.palette.denyList")
if (settingsAllowList || settingsDenyList) {
installAllowList = settingsAllowList;
installDenyList = settingsDenyList
}
installAllowList = RED.utils.parseModuleList(installAllowList);
installDenyList = RED.utils.parseModuleList(installDenyList);
createSettingsPane();
RED.userSettings.add({
@@ -880,7 +934,7 @@ RED.palette.editor = (function() {
}
});
if (RED.settings.theme('palette.upload') !== false) {
if (RED.settings.get('externalModules.palette.allowUpload', true) !== false) {
var uploadSpan = $('<span class="button-group">').prependTo(toolBar);
var uploadButton = $('<button type="button" class="red-ui-sidebar-header-button red-ui-palette-editor-upload-button"><label><i class="fa fa-upload"></i><form id="red-ui-palette-editor-upload-form" enctype="multipart/form-data"><input name="tarball" type="file" accept=".tgz"></label></button>').appendTo(uploadSpan);
@@ -962,7 +1016,7 @@ RED.palette.editor = (function() {
}
function update(entry,version,url,container,done) {
if (RED.settings.theme('palette.editable') === false) {
if (RED.settings.get('externalModules.palette.allowInstall', true) === false) {
done(new Error('Palette not editable'));
return;
}
@@ -1021,7 +1075,7 @@ RED.palette.editor = (function() {
})
}
function remove(entry,container,done) {
if (RED.settings.theme('palette.editable') === false) {
if (RED.settings.get('externalModules.palette.allowInstall', true) === false) {
done(new Error('Palette not editable'));
return;
}
@@ -1078,7 +1132,7 @@ RED.palette.editor = (function() {
})
}
function install(entry,container,done) {
if (RED.settings.theme('palette.editable') === false) {
if (RED.settings.get('externalModules.palette.allowInstall', true) === false) {
done(new Error('Palette not editable'));
return;
}

View File

@@ -97,13 +97,18 @@ RED.palette = (function() {
label = RED.utils.sanitize(label);
var words = label.split(/[ -]/);
var words = label.split(/([ -]|\\n )/);
var displayLines = [];
var currentLine = "";
for (var i=0;i<words.length;i++) {
var word = words[i];
if (word === "\\n ") {
displayLines.push(currentLine);
currentLine = "";
continue;
}
var sep = (i == 0) ? "" : " ";
var newWidth = RED.view.calculateTextWidth(currentLine+sep+word, "red-ui-palette-label");
if (newWidth < nodeWidth) {
@@ -147,7 +152,7 @@ RED.palette = (function() {
var popOverContent;
try {
var l = "<p><b>"+RED.text.bidi.enforceTextDirectionWithUCC(label)+"</b></p>";
popOverContent = $('<div></div>').append($(l+(info?info:$("script[data-help-name='"+type+"']").html()||"<p>"+RED._("palette.noInfo")+"</p>").trim())
popOverContent = $('<div></div>').append($(l+(info?info:RED.nodes.getNodeHelp(type)||"<p>"+RED._("palette.noInfo")+"</p>").trim())
.filter(function(n) {
return (this.nodeType == 1 && this.nodeName == "P") || (this.nodeType == 3 && this.textContent.trim().length > 0)
}).slice(0,2));
@@ -165,7 +170,16 @@ RED.palette = (function() {
metaData = typeInfo.set.module+" : ";
}
metaData += type;
$('<button type="button" onclick="RED.sidebar.help.show(\''+type+'\'); return false;" class="red-ui-button red-ui-button-small" style="float: right"><i class="fa fa-book"></i></button>').appendTo(popOverContent)
if (/^subflow:/.test(type)) {
$('<button type="button" onclick="RED.workspaces.show(\''+type.substring(8).replace(/'/g,"\\'")+'\'); return false;" class="red-ui-button red-ui-button-small" style="float: right; margin-left: 5px;"><i class="fa fa-pencil"></i></button>').appendTo(popOverContent)
}
var safeType = type.replace(/'/g,"\\'");
$('<button type="button" onclick="RED.search.show(\'type:'+safeType+'\'); return false;" class="red-ui-button red-ui-button-small" style="float: right; margin-left: 5px;"><i class="fa fa-search"></i></button>').appendTo(popOverContent)
$('<button type="button" onclick="RED.sidebar.help.show(\''+safeType+'\'); return false;" class="red-ui-button red-ui-button-small" style="float: right; margin-left: 5px;"><i class="fa fa-book"></i></button>').appendTo(popOverContent)
$('<p>',{style:"font-size: 0.8em"}).text(metaData).appendTo(popOverContent);
}
} catch(err) {
@@ -264,27 +278,6 @@ RED.palette = (function() {
d.data('popover',popover);
// $(d).popover({
// title:d.type,
// placement:"right",
// trigger: "hover",
// delay: { show: 750, hide: 50 },
// html: true,
// container:'body'
// });
// d.on("click", function() {
// RED.view.focus();
// var helpText;
// if (nt.indexOf("subflow:") === 0) {
// helpText = RED.utils.renderMarkdown(RED.nodes.subflow(nt.substring(8)).info||"")||('<span class="red-ui-help-info-none">'+RED._("sidebar.info.none")+'</span>');
// } else {
// helpText = $("script[data-help-name='"+d.attr("data-palette-type")+"']").html()||('<span class="red-ui-help-info-none">'+RED._("sidebar.info.none")+'</span>');
// }
// // Don't look too closely. RED.sidebar.info.set will set the 'Description'
// // section of the sidebar. Pass in the title of the Help section so it looks
// // right.
// RED.sidebar.type.show(helpText,RED._("sidebar.info.nodeHelp"));
// });
var chart = $("#red-ui-workspace-chart");
var chartSVG = $("#red-ui-workspace-chart>svg").get(0);
var activeSpliceLink;
@@ -327,12 +320,12 @@ RED.palette = (function() {
var paletteNode = getPaletteNode(nt);
ui.originalPosition.left = paletteNode.offset().left;
mouseX = ui.position.left - paletteWidth + (ui.helper.width()/2) + chart.scrollLeft();
mouseY = ui.position.top - paletteTop + (ui.helper.height()/2) + chart.scrollTop();
mouseY = ui.position.top - paletteTop + (ui.helper.height()/2) + chart.scrollTop() + 10;
if (!groupTimer) {
groupTimer = setTimeout(function() {
mouseX /= RED.view.scale();
mouseY /= RED.view.scale();
var group = RED.view.getGroupAtPoint(mouseX,mouseY);
var mx = mouseX / RED.view.scale();
var my = mouseY / RED.view.scale();
var group = RED.view.getGroupAtPoint(mx,my);
if (group !== hoverGroup) {
if (hoverGroup) {
document.getElementById("group_select_"+hoverGroup.id).classList.remove("red-ui-flow-group-hovered");
@@ -364,23 +357,20 @@ RED.palette = (function() {
svgRect.width = 1;
svgRect.height = 1;
nodes = chartSVG.getIntersectionList(svgRect,chartSVG);
mouseX /= RED.view.scale();
mouseY /= RED.view.scale();
} else {
// Firefox doesn't do getIntersectionList and that
// makes us sad
mouseX /= RED.view.scale();
mouseY /= RED.view.scale();
nodes = RED.view.getLinksAtPoint(mouseX,mouseY);
}
var mx = mouseX / RED.view.scale();
var my = mouseY / RED.view.scale();
for (var i=0;i<nodes.length;i++) {
var node = d3.select(nodes[i]);
if (node.classed('red-ui-flow-link-background') && !node.classed('red-ui-flow-link-link')) {
var length = nodes[i].getTotalLength();
for (var j=0;j<length;j+=10) {
var p = nodes[i].getPointAtLength(j);
var d2 = ((p.x-mouseX)*(p.x-mouseX))+((p.y-mouseY)*(p.y-mouseY));
var d2 = ((p.x-mx)*(p.x-mx))+((p.y-my)*(p.y-my));
if (d2 < 200 && d2 < bestDistance) {
bestDistance = d2;
bestLink = nodes[i];

View File

@@ -465,7 +465,7 @@ RED.projects.settings = (function() {
metaRow = $('<div class="red-ui-palette-module-meta"></div>').appendTo(headerRow);
var buttons = $('<div class="red-ui-palette-module-button-group"></div>').appendTo(metaRow);
if (RED.user.hasPermission("projects.write")) {
if (!entry.installed && RED.settings.theme('palette.editable') !== false) {
if (!entry.installed && RED.settings.get('externalModules.palette.allowInstall', true) !== false) {
$('<a href="#" class="red-ui-button red-ui-button-small">' + RED._("sidebar.project.projectSettings.install") + '</a>').appendTo(buttons)
.on("click", function(evt) {
evt.preventDefault();
@@ -928,11 +928,11 @@ RED.projects.settings = (function() {
saveDisabled = isFlowInvalid || credFileLabelText.text()==="";
if (credentialSecretExistingInput.is(":visible")) {
if (credentialSecretExistingRow.is(":visible")) {
credentialSecretExistingInput.toggleClass("input-error", credentialSecretExistingInput.val() === "");
saveDisabled = saveDisabled || credentialSecretExistingInput.val() === "";
}
if (credentialSecretNewInput.is(":visible")) {
if (credentialSecretNewRow.is(":visible")) {
credentialSecretNewInput.toggleClass("input-error", credentialSecretNewInput.val() === "");
saveDisabled = saveDisabled || credentialSecretNewInput.val() === "";
}
@@ -1130,7 +1130,7 @@ RED.projects.settings = (function() {
}
if (credentialSecretResetButton.hasClass('selected') || credentialSecretEditButton.hasClass('selected')) {
payload.credentialSecret = credentialSecretNewInput.val();
if (credentialSecretExistingInput.is(":visible")) {
if (credentialSecretExistingRow.is(":visible")) {
payload.currentCredentialSecret = credentialSecretExistingInput.val();
}
}

View File

@@ -43,9 +43,11 @@ RED.projects.userSettings = (function() {
function createWorkflowSection(pane) {
var defaultWorkflowMode = RED.settings.theme("projects.workflow.mode","manual");
var currentGitSettings = RED.settings.get('git') || {};
currentGitSettings.workflow = currentGitSettings.workflow || {};
currentGitSettings.workflow.mode = currentGitSettings.workflow.mode || "manual";
currentGitSettings.workflow.mode = currentGitSettings.workflow.mode || defaultWorkflowMode;
var title = $('<h3></h3>').text(RED._("editor:sidebar.project.userSettings.workflow")).appendTo(pane);

View File

@@ -1606,6 +1606,11 @@ RED.projects = (function() {
done(null,data);
},
400: {
'credentials_load_failed': function(data) {
dialog.dialog( "close" );
RED.events.emit("project:change", {name:name});
done(null,data);
},
'*': done
},
}

View File

@@ -294,7 +294,10 @@ RED.sidebar.versionControl = (function() {
// TODO: this is a full refresh of the files - should be able to
// just do an incremental refresh
var workflowMode = ((RED.settings.get('git') || {}).workflow || {}).mode || "manual";
// Get the default workflow mode from theme settings
var defaultWorkflowMode = RED.settings.theme("projects.workflow.mode","manual");
// Check for the user-defined choice of mode
var workflowMode = ((RED.settings.get('git') || {}).workflow || {}).mode || defaultWorkflowMode;
if (workflowMode === 'auto') {
refresh(true);
} else {

View File

@@ -357,7 +357,6 @@ RED.search = (function() {
}
if (!visible) {
previousActiveElement = document.activeElement;
RED.keyboard.add("*","escape",function(){hide()});
$("#red-ui-header-shade").show();
$("#red-ui-editor-shade").show();
$("#red-ui-palette-shade").show();
@@ -377,7 +376,6 @@ RED.search = (function() {
function hide() {
if (visible) {
RED.keyboard.remove("escape");
visible = false;
$("#red-ui-header-shade").hide();
$("#red-ui-editor-shade").hide();
@@ -429,7 +427,7 @@ RED.search = (function() {
RED.events.on("actionList:open",function() { disabled = true; });
RED.events.on("actionList:close",function() { disabled = false; });
RED.keyboard.add("red-ui-search","escape",hide);
$("#red-ui-header-shade").on('mousedown',hide);
$("#red-ui-editor-shade").on('mousedown',hide);

View File

@@ -195,8 +195,11 @@ RED.sidebar = (function() {
}
function showSidebar(id) {
if (id === ":first") {
id = RED.settings.get("editor.sidebar.order",["info", "help", "version-control", "debug"])[0]
}
if (id) {
if (!containsTab(id)) {
if (!containsTab(id) && knownTabs[id]) {
sidebar_tabs.addTab(knownTabs[id]);
}
sidebar_tabs.activateTab(id);

View File

@@ -47,6 +47,37 @@ RED.subflow = (function() {
'</div>'+
'</script>';
var _subflowModulePaneTemplate = '<form class="dialog-form form-horizontal" autocomplete="off">'+
'<div class="form-row">'+
'<label for="subflow-input-module-module" data-i18n="[append]editor:subflow.module"><i class="fa fa-cube"></i> </label>'+
'<input style="width: calc(100% - 110px)" type="text" id="subflow-input-module-module" data-i18n="[placeholder]common.label.name">'+
'</div>'+
'<div class="form-row">'+
'<label for="subflow-input-module-type" data-i18n="[append]editor:subflow.type"> </label>'+
'<input style="width: calc(100% - 110px)" type="text" id="subflow-input-module-type">'+
'</div>'+
'<div class="form-row">'+
'<label for="subflow-input-module-version" data-i18n="[append]editor:subflow.version"></label>'+
'<input style="width: calc(100% - 110px)" type="text" id="subflow-input-module-version" data-i18n="[placeholder]editor:subflow.versionPlaceholder">'+
'</div>'+
'<div class="form-row">'+
'<label for="subflow-input-module-desc" data-i18n="[append]editor:subflow.desc"></label>'+
'<input style="width: calc(100% - 110px)" type="text" id="subflow-input-module-desc">'+
'</div>'+
'<div class="form-row">'+
'<label for="subflow-input-module-license" data-i18n="[append]editor:subflow.license"></label>'+
'<input style="width: calc(100% - 110px)" type="text" id="subflow-input-module-license">'+
'</div>'+
'<div class="form-row">'+
'<label for="subflow-input-module-author" data-i18n="[append]editor:subflow.author"></label>'+
'<input style="width: calc(100% - 110px)" type="text" id="subflow-input-module-author" data-i18n="[placeholder]editor:subflow.authorPlaceholder">'+
'</div>'+
'<div class="form-row">'+
'<label for="subflow-input-module-keywords" data-i18n="[append]editor:subflow.keys"></label>'+
'<input style="width: calc(100% - 110px)" type="text" id="subflow-input-module-keywords" data-i18n="[placeholder]editor:subflow.keysPlaceholder">'+
'</div>'+
'</form>';
function findAvailableSubflowIOPosition(subflow,isInput) {
var pos = {x:50,y:30};
if (!isInput) {
@@ -433,12 +464,43 @@ RED.subflow = (function() {
$("#red-ui-subflow-delete").on("click", function(event) {
event.preventDefault();
var startDirty = RED.nodes.dirty();
var historyEvent = removeSubflow(RED.workspaces.active());
historyEvent.t = 'delete';
historyEvent.dirty = startDirty;
var subflow = RED.nodes.subflow(RED.workspaces.active());
if (subflow.instances.length > 0) {
var msg = $('<div>')
$('<p>').text(RED._("subflow.subflowInstances",{count: subflow.instances.length})).appendTo(msg);
$('<p>').text(RED._("subflow.confirmDelete")).appendTo(msg);
var confirmDeleteNotification = RED.notify(msg, {
modal: true,
fixed: true,
buttons: [
{
text: RED._('common.label.cancel'),
click: function() {
confirmDeleteNotification.close();
}
},
{
text: RED._('workspace.confirmDelete'),
class: "primary",
click: function() {
confirmDeleteNotification.close();
completeDelete();
}
}
]
});
RED.history.push(historyEvent);
return;
} else {
completeDelete();
}
function completeDelete() {
var startDirty = RED.nodes.dirty();
var historyEvent = removeSubflow(RED.workspaces.active());
historyEvent.t = 'delete';
historyEvent.dirty = startDirty;
RED.history.push(historyEvent);
}
});
@@ -993,6 +1055,7 @@ RED.subflow = (function() {
icon: "",
type: "cred"
}
opt.ui.type = "cred";
} else {
opt.ui = opt.ui || {
icon: "",
@@ -1488,6 +1551,7 @@ RED.subflow = (function() {
var locale = RED.i18n.lang();
var labelText = lookupLabel(labels, labels["en-US"]||tenv.name, locale);
var label = $('<label>').appendTo(row);
$('<span>&nbsp;</span>').appendTo(row);
var labelContainer = $('<span></span>').appendTo(label);
if (ui.icon) {
var newPath = RED.utils.separateIconPath(ui.icon);
@@ -1723,22 +1787,54 @@ RED.subflow = (function() {
parentEnv[env.name] = item;
})
}
}
if (node.env) {
for (var i = 0; i < node.env.length; i++) {
var env = node.env[i];
if (parentEnv.hasOwnProperty(env.name)) {
parentEnv[env.name].type = env.type;
parentEnv[env.name].value = env.value;
} else {
// envList.push({
// name: env.name,
// type: env.type,
// value: env.value,
// });
if (node.env) {
for (var i = 0; i < node.env.length; i++) {
var env = node.env[i];
if (parentEnv.hasOwnProperty(env.name)) {
parentEnv[env.name].type = env.type;
parentEnv[env.name].value = env.value;
} else {
// envList.push({
// name: env.name,
// type: env.type,
// value: env.value,
// });
}
}
}
} else if (node._def.subflowModule) {
var keys = Object.keys(node._def.defaults);
keys.forEach(function(name) {
if (name !== 'name') {
var prop = node._def.defaults[name];
var nodeProp = node[name];
var nodePropType;
var nodePropValue = nodeProp;
if (prop.ui && prop.ui.type === "cred") {
nodePropType = "cred";
} else {
switch(typeof nodeProp) {
case "string": nodePropType = "str"; break;
case "number": nodePropType = "num"; break;
case "boolean": nodePropType = "bool"; nodePropValue = nodeProp?"true":"false"; break;
default:
nodePropType = nodeProp.type;
nodePropValue = nodeProp.value;
}
}
var item = {
name: name,
type: nodePropType,
value: nodePropValue,
parent: {
type: prop.type,
value: prop.value
},
ui: $.extend(true,{},prop.ui)
}
envList.push(item);
}
})
}
return envList;
}
@@ -1859,6 +1955,126 @@ RED.subflow = (function() {
buildPropertiesList(list, node);
}
function setupInputValidation(input,validator) {
var errorTip;
var validateTimeout;
var validateFunction = function() {
if (validateTimeout) {
return;
}
validateTimeout = setTimeout(function() {
var error = validator(input.val());
// if (!error && errorTip) {
// errorTip.close();
// errorTip = null;
// } else if (error && !errorTip) {
// errorTip = RED.popover.create({
// tooltip: true,
// target:input,
// size: "small",
// direction: "bottom",
// content: error,
// }).open();
// }
input.toggleClass("input-error",!!error);
validateTimeout = null;
})
}
input.on("change keyup paste", validateFunction);
}
function buildModuleForm(container, node) {
$(_subflowModulePaneTemplate).appendTo(container);
var moduleProps = node.meta || {};
[
'module',
'type',
'version',
'author',
'desc',
'keywords',
'license'
].forEach(function(property) {
$("#subflow-input-module-"+property).val(moduleProps[property]||"")
})
$("#subflow-input-module-type").attr("placeholder",node.id);
setupInputValidation($("#subflow-input-module-module"), function(newValue) {
newValue = newValue.trim();
var isValid = newValue.length < 215;
isValid = isValid && !/^[._]/.test(newValue);
isValid = isValid && !/[A-Z]/.test(newValue);
if (newValue !== encodeURIComponent(newValue)) {
var m = /^@([^\/]+)\/([^\/]+)$/.exec(newValue);
if (m) {
isValid = isValid && (m[1] === encodeURIComponent(m[1]) && m[2] === encodeURIComponent(m[2]))
} else {
isValid = false;
}
}
return isValid?"":"Invalid module name"
})
setupInputValidation($("#subflow-input-module-version"), function(newValue) {
newValue = newValue.trim();
var isValid = newValue === "" ||
/^(\d|[1-9]\d*)\.(\d|[1-9]\d*)\.(\d|[1-9]\d*)(-(0|[1-9A-Za-z-][0-9A-Za-z-]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*)(\.(0|[1-9A-Za-z-][0-9A-Za-z-]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*))*)?(\+[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$/.test(newValue);
return isValid?"":"Invalid version number"
})
var licenses = ["none", "Apache-2.0", "BSD-3-Clause", "BSD-2-Clause", "GPL-2.0", "GPL-3.0", "MIT", "MPL-2.0", "CDDL-1.0", "EPL-2.0"];
var typedLicenses = {
types: licenses.map(function(l) {
return {
value: l,
label: l === "none" ? RED._("editor:subflow.licenseNone") : l,
hasValue: false
};
})
}
typedLicenses.types.push({
value:"_custom_", label:RED._("editor:subflow.licenseOther"), icon:"red/images/typedInput/az.svg"
})
if (!moduleProps.license) {
typedLicenses.default = "none";
} else if (licenses.indexOf(moduleProps.license) > -1) {
typedLicenses.default = moduleProps.license;
} else {
typedLicenses.default = "_custom_";
}
$("#subflow-input-module-license").typedInput(typedLicenses)
}
function exportSubflowModuleProperties(node) {
var value;
var moduleProps = {};
[
'module',
'type',
'version',
'author',
'desc',
'keywords'
].forEach(function(property) {
value = $("#subflow-input-module-"+property).val().trim();
if (value) {
moduleProps[property] = value;
}
})
var selectedLicenseType = $("#subflow-input-module-license").typedInput("type");
if (selectedLicenseType === '_custom_') {
value = $("#subflow-input-module-license").val();
if (value) {
moduleProps.license = value;
}
} else if (selectedLicenseType !== "none") {
moduleProps.license = selectedLicenseType;
}
return moduleProps;
}
return {
init: init,
createSubflow: createSubflow,
@@ -1872,9 +2088,11 @@ RED.subflow = (function() {
buildEditForm: buildEditForm,
buildPropertiesForm: buildPropertiesForm,
buildModuleForm: buildModuleForm,
exportSubflowTemplateEnv: exportEnvList,
exportSubflowInstanceEnv: exportSubflowInstanceEnv
exportSubflowInstanceEnv: exportSubflowInstanceEnv,
exportSubflowModuleProperties: exportSubflowModuleProperties
}
})();

View File

@@ -230,10 +230,9 @@ RED.sidebar.help = (function() {
}
function getNodeLabel(n) {
var div = $('<div>',{class:"red-ui-info-outline-item"});
var div = $('<div>',{class:"red-ui-node-list-item"});
RED.utils.createNodeIcon(n).appendTo(div);
var contentDiv = $('<div>',{class:"red-ui-search-result-description"}).appendTo(div);
$('<div>',{class:"red-ui-search-result-node-label red-ui-info-outline-item-label"}).text(n.name||n.type).appendTo(contentDiv);
$('<div>',{class:"red-ui-node-label"}).text(n.name||n.type).appendTo(div);
return div;
}
@@ -247,7 +246,7 @@ RED.sidebar.help = (function() {
helpText = (RED.utils.renderMarkdown(subflowNode.info||"")||('<span class="red-ui-help-info-none">'+RED._("sidebar.info.none")+'</span>'));
title = subflowNode.name || nodeType;
} else {
helpText = $("script[data-help-name='"+nodeType+"']").html()||('<span class="red-ui-help-info-none">'+RED._("sidebar.info.none")+'</span>');
helpText = RED.nodes.getNodeHelp(nodeType)||('<span class="red-ui-help-info-none">'+RED._("sidebar.info.none")+'</span>');
title = nodeType;
}
setInfoText(title, helpText, helpSection);

View File

@@ -73,36 +73,11 @@ RED.sidebar.info.outliner = (function() {
return item;
}
function getNodeLabelText(n) {
var label = n.name || n.type+": "+n.id;
if (n._def.label) {
try {
label = (typeof n._def.label === "function" ? n._def.label.call(n) : n._def.label)||"";
} catch(err) {
console.log("Definition error: "+n.type+".label",err);
}
}
var newlineIndex = label.indexOf("\\n");
if (newlineIndex > -1) {
label = label.substring(0,newlineIndex)+"...";
}
return label;
}
function getNodeLabel(n) {
var div = $('<div>',{class:"red-ui-info-outline-item"});
RED.utils.createNodeIcon(n).appendTo(div);
var contentDiv = $('<div>',{class:"red-ui-search-result-description"}).appendTo(div);
var labelText = getNodeLabelText(n);
var label = $('<div>',{class:"red-ui-search-result-node-label red-ui-info-outline-item-label"}).appendTo(contentDiv);
if (labelText) {
label.text(labelText)
} else {
label.html("&nbsp;")
}
var div = $('<div>',{class:"red-ui-node-list-item red-ui-info-outline-item"});
RED.utils.createNodeIcon(n, true).appendTo(div);
div.find(".red-ui-node-label").addClass("red-ui-info-outline-item-label")
addControls(n, div);
return div;
}
@@ -119,34 +94,17 @@ RED.sidebar.info.outliner = (function() {
return div;
}
function getSubflowLabel(n) {
var div = $('<div>',{class:"red-ui-info-outline-item"});
RED.utils.createNodeIcon(n).appendTo(div);
var contentDiv = $('<div>',{class:"red-ui-search-result-description"}).appendTo(div);
var labelText = getNodeLabelText(n);
var label = $('<div>',{class:"red-ui-search-result-node-label red-ui-info-outline-item-label"}).appendTo(contentDiv);
if (labelText) {
label.text(labelText)
} else {
label.html("&nbsp;")
}
addControls(n, div);
return div;
// var div = $('<div>',{class:"red-ui-info-outline-item red-ui-info-outline-item-flow"});
// var contentDiv = $('<div>',{class:"red-ui-search-result-description red-ui-info-outline-item-label"}).appendTo(div);
// contentDiv.text(n.name || n.id);
// addControls(n, div);
// return div;
}
function addControls(n,div) {
var controls = $('<div>',{class:"red-ui-info-outline-item-controls red-ui-info-outline-item-hover-controls"}).appendTo(div);
if (n.type === "subflow") {
var subflowInstanceBadge = $('<button type="button" class="red-ui-info-outline-item-control-users red-ui-button red-ui-button-small"><i class="fa fa-toggle-right"></i></button>').text(n.instances.length).appendTo(controls).on("click",function(evt) {
evt.preventDefault();
evt.stopPropagation();
RED.search.show("type:subflow:"+n.id);
})
// RED.popover.tooltip(userCountBadge,function() { return RED._('editor.nodesUse',{count:n.users.length})});
}
if (n._def.category === "config" && n.type !== "group") {
var userCountBadge = $('<button type="button" class="red-ui-info-outline-item-control-users red-ui-button red-ui-button-small"><i class="fa fa-toggle-right"></i></button>').text(n.users.length).appendTo(controls).on("click",function(evt) {
evt.preventDefault();
@@ -169,7 +127,7 @@ RED.sidebar.info.outliner = (function() {
// evt.stopPropagation();
// RED.view.reveal(n.id);
// })
if (n.type !== 'group' && n.type !== 'subflow') {
if (n.type !== 'subflow') {
var toggleButton = $('<button type="button" class="red-ui-info-outline-item-control-disable red-ui-button red-ui-button-small"><i class="fa fa-circle-thin"></i><i class="fa fa-ban"></i></button>').appendTo(controls).on("click",function(evt) {
evt.preventDefault();
evt.stopPropagation();
@@ -179,6 +137,46 @@ RED.sidebar.info.outliner = (function() {
} else {
RED.workspaces.disable(n.id)
}
} else if (n.type === 'group') {
var groupNodes = RED.group.getNodes(n,true);
var groupHistoryEvent = {
t:'multi',
events:[],
dirty: RED.nodes.dirty()
}
var targetState;
groupNodes.forEach(function(n) {
if (n.type !== 'group') {
if (targetState === undefined) {
targetState = !n.d;
}
var state = !!n.d;
if (state !== targetState) {
var historyEvent = {
t: "edit",
node: n,
changed: n.changed,
changes: {
d: n.d
}
}
if (n.d) {
delete n.d;
} else {
n.d = true;
}
n.dirty = true;
n.changed = true;
RED.events.emit("nodes:change",n);
groupHistoryEvent.events.push(historyEvent);
}
}
if (groupHistoryEvent.events.length > 0) {
RED.history.push(groupHistoryEvent);
RED.nodes.dirty(true)
RED.view.redraw();
}
})
} else {
// TODO: this ought to be a utility function in RED.nodes
var historyEvent = {
@@ -198,11 +196,15 @@ RED.sidebar.info.outliner = (function() {
n.dirty = true;
n.changed = true;
RED.events.emit("nodes:change",n);
RED.history.push(historyEvent);
RED.nodes.dirty(true)
RED.view.redraw();
}
});
RED.popover.tooltip(toggleButton,function() {
if (n.type === "group") {
return RED._("common.label.enable")+" / "+RED._("common.label.disable")
}
return RED._("common.label."+(((n.type==='tab' && n.disabled) || (n.type!=='tab' && n.d))?"enable":"disable"));
});
} else {
@@ -403,7 +405,7 @@ RED.sidebar.info.outliner = (function() {
var existingObject = objects[n.id];
var parent = n.g||n.z||"__global__";
var nodeLabelText = getNodeLabelText(n);
var nodeLabelText = RED.utils.getNodeLabel(n,n.name || (n.type+": "+n.id));
if (nodeLabelText) {
existingObject.element.find(".red-ui-info-outline-item-label").text(nodeLabelText);
} else {
@@ -486,6 +488,13 @@ RED.sidebar.info.outliner = (function() {
existingObject.treeList.remove();
delete objects[n.id]
if (/^subflow:/.test(n.type)) {
var sfType = n.type.substring(8);
if (objects[sfType]) {
objects[sfType].element.find(".red-ui-info-outline-item-control-users").text(RED.nodes.subflow(sfType).instances.length);
}
}
// If this is a group being removed, it may have an empty item
if (empties[n.id]) {
delete empties[n.id];
@@ -587,6 +596,12 @@ RED.sidebar.info.outliner = (function() {
configNodeTypes[parent].types[n.type].treeList.addChild(objects[n.id]);
}
objects[n.id].element.toggleClass("red-ui-info-outline-item-disabled", !!n.d)
if (/^subflow:/.test(n.type)) {
var sfType = n.type.substring(8);
if (objects[sfType]) {
objects[sfType].element.find(".red-ui-info-outline-item-control-users").text(RED.nodes.subflow(sfType).instances.length);
}
}
updateSearch();
}

View File

@@ -338,7 +338,7 @@ RED.sidebar.info = (function() {
count++;
propRow = $('<tr class="red-ui-help-info-property-row'+(expandedSections.property?"":" hide")+'"><td></td><td></td></tr>').appendTo(tableBody);
$(propRow.children()[0]).text(n);
if (defaults[n].type) {
if (defaults[n].type && !defaults[n]._type.array) {
var configNode = RED.nodes.node(val);
if (!configNode) {
RED.utils.createObjectElement(undefined).appendTo(propRow.children()[1]);
@@ -382,21 +382,14 @@ RED.sidebar.info = (function() {
var category = subflowNode.category||"subflows";
$(propRow.children()[1]).text(RED._("palette.label."+category,{defaultValue:category}))
$('<tr class="node-info-subflow-row"><td>'+RED._("sidebar.info.instances")+"</td><td>"+subflowUserCount+'</td></tr>').appendTo(tableBody);
if (subflowNode.meta) {
propRow = $('<tr class="red-ui-help-info-row"><td>'+RED._("subflow.module")+'</td><td></td></tr>').appendTo(tableBody);
$(propRow.children()[1]).text(subflowNode.meta.module||"")
propRow = $('<tr class="red-ui-help-info-row"><td>'+RED._("subflow.version")+'</td><td></td></tr>').appendTo(tableBody);
$(propRow.children()[1]).text(subflowNode.meta.version||"")
}
}
// var helpText = "";
// if (node.type === "tab" || node.type === "subflow") {
// } else {
// if (subflowNode && node.type !== "subflow") {
// // Selected a subflow instance node.
// // - The subflow template info goes into help
// helpText = (RED.utils.renderMarkdown(subflowNode.info||"")||('<span class="red-ui-help-info-none">'+RED._("sidebar.info.none")+'</span>'));
// } else {
// helpText = $("script[data-help-name='"+node.type+"']").html()||('<span class="red-ui-help-info-none">'+RED._("sidebar.info.none")+'</span>');
// }
// setInfoText(helpText, helpSection.content);
// }
var infoText = "";
if (node._def && node._def.info) {
@@ -409,23 +402,6 @@ RED.sidebar.info = (function() {
}
var infoSectionContainer = $("<div>").css("padding","0 6px 6px").appendTo(propertiesPanelContent)
// var editInfo = $('<button class="red-ui-button red-ui-button-small" style="float: right"><i class="fa fa-file-text-o"></button>').appendTo(infoSectionContainer).on("click", function(evt) {
// //.text(RED._("sidebar.info.editDescription"))
// evt.preventDefault();
// evt.stopPropagation();
// if (node.type === 'tab') {
//
// } else if (node.type === 'subflow') {
//
// } else if (node.type === 'group') {
//
// } else if (node._def.category !== 'config') {
// RED.editor.edit(node,"editor-tab-description");
// } else {
//
// }
// })
setInfoText(infoText, infoSectionContainer);
$(".red-ui-sidebar-info-stack").scrollTop(0);
@@ -501,7 +477,7 @@ RED.sidebar.info = (function() {
return;
}
}
while ((m=/(\[(.*?)\])/.exec(tip))) {
while ((m=/(\[([a-z]*?)\])/.exec(tip))) {
tip = tip.replace(m[1],RED.keyboard.formatKey(m[2]));
}
tipBox.html(tip).fadeIn(200);

View File

@@ -224,14 +224,14 @@ RED.typeSearch = (function() {
}
function show(opts) {
if (!visible) {
RED.keyboard.add("*","escape",function(){
hide();
if (cancelCallback) {
cancelCallback();
}
});
if (dialog === null) {
createDialog();
RED.keyboard.add("red-ui-type-search","escape",function(){
hide();
if (cancelCallback) {
cancelCallback();
}
});
}
visible = true;
} else {
@@ -266,11 +266,10 @@ RED.typeSearch = (function() {
if (!opts.disableFocus) {
searchInput.trigger("focus");
}
},100);
},200);
}
function hide(fast) {
if (visible) {
RED.keyboard.remove("escape");
visible = false;
if (dialog !== null) {
searchResultsDiv.slideUp(fast?50:200,function() {

View File

@@ -109,13 +109,19 @@ RED.userSettings = (function() {
function compText(a, b) {
return a.text.localeCompare(b.text);
}
var viewSettings = [
{
options: [
{setting:"editor-language",local: true, label:"menu.label.view.language",options:function(done){ done([{val:'',text:RED._('menu.label.view.browserDefault')}].concat(RED.settings.theme("languages").map(localeToName).sort(compText))) }},
]
},{
},
// {
// options: [
// {setting:"theme", label:"Theme",options:function(done){ done([{val:'',text:'default'}].concat(RED.settings.theme("themes"))) }},
// ]
// },
{
title: "menu.label.view.grid",
options: [
{setting:"view-show-grid",oldSetting:"menu-menu-item-view-show-grid",label:"menu.label.view.showGrid", default: true, toggle:true,onchange:"core:toggle-show-grid"},

View File

@@ -615,18 +615,25 @@ RED.utils = (function() {
return element;
}
function normalisePropertyExpression(str) {
function createError(code, message) {
var e = new Error(message);
e.code = code;
return e;
}
function normalisePropertyExpression(str,msg) {
// This must be kept in sync with validatePropertyExpression
// in editor/js/ui/utils.js
var length = str.length;
if (length === 0) {
throw new Error("Invalid property expression: zero-length");
throw createError("INVALID_EXPR","Invalid property expression: zero-length");
}
var parts = [];
var start = 0;
var inString = false;
var inBox = false;
var boxExpression = false;
var quoteChar;
var v;
for (var i=0;i<length;i++) {
@@ -634,14 +641,14 @@ RED.utils = (function() {
if (!inString) {
if (c === "'" || c === '"') {
if (i != start) {
throw new Error("Invalid property expression: unexpected "+c+" at position "+i);
throw createError("INVALID_EXPR","Invalid property expression: unexpected "+c+" at position "+i);
}
inString = true;
quoteChar = c;
start = i+1;
} else if (c === '.') {
if (i===0) {
throw new Error("Invalid property expression: unexpected . at position 0");
throw createError("INVALID_EXPR","Invalid property expression: unexpected . at position 0");
}
if (start != i) {
v = str.substring(start,i);
@@ -652,57 +659,99 @@ RED.utils = (function() {
}
}
if (i===length-1) {
throw new Error("Invalid property expression: unterminated expression");
throw createError("INVALID_EXPR","Invalid property expression: unterminated expression");
}
// Next char is first char of an identifier: a-z 0-9 $ _
if (!/[a-z0-9\$\_]/i.test(str[i+1])) {
throw new Error("Invalid property expression: unexpected "+str[i+1]+" at position "+(i+1));
throw createError("INVALID_EXPR","Invalid property expression: unexpected "+str[i+1]+" at position "+(i+1));
}
start = i+1;
} else if (c === '[') {
if (i === 0) {
throw new Error("Invalid property expression: unexpected "+c+" at position "+i);
throw createError("INVALID_EXPR","Invalid property expression: unexpected "+c+" at position "+i);
}
if (start != i) {
parts.push(str.substring(start,i));
}
if (i===length-1) {
throw new Error("Invalid property expression: unterminated expression");
throw createError("INVALID_EXPR","Invalid property expression: unterminated expression");
}
// Next char is either a quote or a number
if (!/["'\d]/.test(str[i+1])) {
throw new Error("Invalid property expression: unexpected "+str[i+1]+" at position "+(i+1));
// Start of a new expression. If it starts with msg it is a nested expression
// Need to scan ahead to find the closing bracket
if (/^msg[.\[]/.test(str.substring(i+1))) {
var depth = 1;
var inLocalString = false;
var localStringQuote;
for (var j=i+1;j<length;j++) {
if (/["']/.test(str[j])) {
if (inLocalString) {
if (str[j] === localStringQuote) {
inLocalString = false
}
} else {
inLocalString = true;
localStringQuote = str[j]
}
}
if (str[j] === '[') {
depth++;
} else if (str[j] === ']') {
depth--;
}
if (depth === 0) {
try {
if (msg) {
parts.push(getMessageProperty(msg, str.substring(i+1,j)))
} else {
parts.push(normalisePropertyExpression(str.substring(i+1,j), msg));
}
inBox = false;
i = j;
start = j+1;
break;
} catch(err) {
throw createError("INVALID_EXPR","Invalid expression started at position "+(i+1))
}
}
}
if (depth > 0) {
throw createError("INVALID_EXPR","Invalid property expression: unmatched '[' at position "+i);
}
continue;
} else if (!/["'\d]/.test(str[i+1])) {
// Next char is either a quote or a number
throw createError("INVALID_EXPR","Invalid property expression: unexpected "+str[i+1]+" at position "+(i+1));
}
start = i+1;
inBox = true;
} else if (c === ']') {
if (!inBox) {
throw new Error("Invalid property expression: unexpected "+c+" at position "+i);
throw createError("INVALID_EXPR","Invalid property expression: unexpected "+c+" at position "+i);
}
if (start != i) {
v = str.substring(start,i);
if (/^\d+$/.test(v)) {
parts.push(parseInt(v));
} else {
throw new Error("Invalid property expression: unexpected array expression at position "+start);
throw createError("INVALID_EXPR","Invalid property expression: unexpected array expression at position "+start);
}
}
start = i+1;
inBox = false;
} else if (c === ' ') {
throw new Error("Invalid property expression: unexpected ' ' at position "+i);
throw createError("INVALID_EXPR","Invalid property expression: unexpected ' ' at position "+i);
}
} else {
if (c === quoteChar) {
if (i-start === 0) {
throw new Error("Invalid property expression: zero-length string at position "+start);
throw createError("INVALID_EXPR","Invalid property expression: zero-length string at position "+start);
}
parts.push(str.substring(start,i));
// If inBox, next char must be a ]. Otherwise it may be [ or .
if (inBox && !/\]/.test(str[i+1])) {
throw new Error("Invalid property expression: unexpected array expression at position "+start);
throw createError("INVALID_EXPR","Invalid property expression: unexpected array expression at position "+start);
} else if (!inBox && i+1!==length && !/[\[\.]/.test(str[i+1])) {
throw new Error("Invalid property expression: unexpected "+str[i+1]+" expression at position "+(i+1));
throw createError("INVALID_EXPR","Invalid property expression: unexpected "+str[i+1]+" expression at position "+(i+1));
}
start = i+1;
inString = false;
@@ -711,7 +760,7 @@ RED.utils = (function() {
}
if (inBox || inString) {
throw new Error("Invalid property expression: unterminated expression");
throw new createError("INVALID_EXPR","Invalid property expression: unterminated expression");
}
if (start < length) {
parts.push(str.substring(start));
@@ -826,6 +875,7 @@ RED.utils = (function() {
}
function getDefaultNodeIcon(def,node) {
def = def || {};
var icon_url;
if (node && node.type === "subflow") {
icon_url = "node-red/subflow.svg";
@@ -863,6 +913,7 @@ RED.utils = (function() {
}
function getNodeIcon(def,node) {
def = def || {};
if (node && node.type === '_selection_') {
return "font-awesome/fa-object-ungroup";
} else if (node && node.type === 'group') {
@@ -950,6 +1001,7 @@ RED.utils = (function() {
}
function getNodeColor(type, def) {
def = def || {};
var result = def.color;
var paletteTheme = RED.settings.theme('palette.theme') || [];
if (paletteTheme.length > 0) {
@@ -1021,7 +1073,7 @@ RED.utils = (function() {
return payload;
}
function parseContextKey(key) {
function parseContextKey(key, defaultStore) {
var parts = {};
var m = /^#:\((\S+?)\)::(.*)$/.exec(key);
if (m) {
@@ -1029,7 +1081,9 @@ RED.utils = (function() {
parts.key = m[2];
} else {
parts.key = key;
if (RED.settings.context) {
if (defaultStore) {
parts.store = defaultStore;
} else if (RED.settings.context) {
parts.store = RED.settings.context.default;
}
}
@@ -1074,9 +1128,9 @@ RED.utils = (function() {
imageIconElement.css("backgroundImage", "url("+iconUrl+")");
}
function createNodeIcon(node) {
function createNodeIcon(node, includeLabel) {
var def = node._def;
var nodeDiv = $('<div>',{class:"red-ui-search-result-node"})
var nodeDiv = $('<div>',{class:"red-ui-node-icon"})
if (node.type === "_selection_") {
nodeDiv.addClass("red-ui-palette-icon-selection");
} else if (node.type === "group") {
@@ -1096,8 +1150,20 @@ RED.utils = (function() {
}
var icon_url = RED.utils.getNodeIcon(def,node);
var iconContainer = $('<div/>',{class:"red-ui-palette-icon-container"}).appendTo(nodeDiv);
RED.utils.createIconElement(icon_url, iconContainer, true);
RED.utils.createIconElement(icon_url, nodeDiv, true);
if (includeLabel) {
var container = $('<span>');
nodeDiv.appendTo(container);
var labelText = RED.utils.getNodeLabel(node,node.name || (node.type+": "+node.id));
var label = $('<div>',{class:"red-ui-node-label"}).appendTo(container);
if (labelText) {
label.text(labelText)
} else {
label.html("&nbsp;")
}
return container;
}
return nodeDiv;
}
@@ -1122,6 +1188,67 @@ RED.utils = (function() {
return '#'+'000000'.slice(0, 6-s.length)+s;
}
function parseModuleList(list) {
list = list || ["*"];
return list.map(function(rule) {
var m = /^(.+?)(?:@(.*))?$/.exec(rule);
var wildcardPos = m[1].indexOf("*");
wildcardPos = wildcardPos===-1?Infinity:wildcardPos;
return {
module: new RegExp("^"+m[1].replace(/\*/g,".*")+"$"),
version: m[2],
wildcardPos: wildcardPos
}
})
}
function checkAgainstList(module,version,list) {
for (var i=0;i<list.length;i++) {
var rule = list[i];
if (rule.module.test(module)) {
// Without a full semver library in the editor,
// we skip the version check.
// Not ideal - but will get caught in the runtime
// if the user tries to install.
return rule;
}
}
}
function checkModuleAllowed(module,version,allowList,denyList) {
if (!allowList && !denyList) {
// Default to allow
return true;
}
if (allowList.length === 0 && denyList.length === 0) {
return true;
}
var allowedRule = checkAgainstList(module,version,allowList);
var deniedRule = checkAgainstList(module,version,denyList);
// console.log("A",allowedRule)
// console.log("D",deniedRule)
if (allowedRule && !deniedRule) {
return true;
}
if (!allowedRule && deniedRule) {
return false;
}
if (!allowedRule && !deniedRule) {
return true;
}
if (allowedRule.wildcardPos !== deniedRule.wildcardPos) {
return allowedRule.wildcardPos > deniedRule.wildcardPos
} else {
// First wildcard in same position.
// Go with the longer matching rule. This isn't going to be 100%
// right, but we are deep into edge cases at this point.
return allowedRule.module.toString().length > deniedRule.module.toString().length
}
return false;
}
return {
createObjectElement: buildMessageElement,
getMessageProperty: getMessageProperty,
@@ -1141,6 +1268,8 @@ RED.utils = (function() {
sanitize: sanitize,
renderMarkdown: renderMarkdown,
createNodeIcon: createNodeIcon,
getDarkerColor: getDarkerColor
getDarkerColor: getDarkerColor,
parseModuleList: parseModuleList,
checkModuleAllowed: checkModuleAllowed
}
})();

View File

@@ -16,6 +16,27 @@
RED.view.tools = (function() {
function selectConnected(type) {
var selection = RED.view.selection();
var visited = new Set();
if (selection.nodes && selection.nodes.length > 0) {
selection.nodes.forEach(function(n) {
if (!visited.has(n)) {
var connected;
if (type === 'all') {
connected = RED.nodes.getAllFlowNodes(n);
} else if (type === 'up') {
connected = [n].concat(RED.nodes.getAllUpstreamNodes(n));
} else if (type === 'down') {
connected = [n].concat(RED.nodes.getAllDownstreamNodes(n));
}
connected.forEach(function(nn) { visited.add(nn) })
}
});
RED.view.select({nodes:Array.from(visited)});
}
}
function alignToGrid() {
var selection = RED.view.selection();
@@ -180,6 +201,237 @@ RED.view.tools = (function() {
}
function selectFirstNode() {
var canidates;
var origin = {x:0, y:0};
var activeGroup = RED.view.getActiveGroup();
if (!activeGroup) {
candidates = RED.view.getActiveNodes();
} else {
candidates = RED.group.getNodes(activeGroup,false);
origin = activeGroup;
}
var distances = [];
candidates.forEach(function(node) {
var deltaX = node.x - origin.x;
var deltaY = node.y - origin.x;
var delta = deltaY*deltaY + deltaX*deltaX;
distances.push({node: node, delta: delta})
});
if (distances.length > 0) {
distances.sort(function(A,B) {
return A.delta - B.delta
})
var newNode = distances[0].node;
if (newNode) {
RED.view.select({nodes:[newNode]});
RED.view.reveal(newNode.id,false);
}
}
}
function gotoNextNode() {
var selection = RED.view.selection();
if (selection.nodes && selection.nodes.length === 1) {
var origin = selection.nodes[0];
var links = RED.nodes.filterLinks({source:origin});
if (links.length > 0) {
links.sort(function(A,B) {
return Math.abs(A.target.y - origin.y) - Math.abs(B.target.y - origin.y)
})
var newNode = links[0].target;
if (newNode) {
RED.view.select({nodes:[newNode]});
RED.view.reveal(newNode.id,false);
}
}
} else if (RED.workspaces.selection().length === 0) {
selectFirstNode();
}
}
function gotoPreviousNode() {
var selection = RED.view.selection();
if (selection.nodes && selection.nodes.length === 1) {
var origin = selection.nodes[0];
var links = RED.nodes.filterLinks({target:origin});
if (links.length > 0) {
links.sort(function(A,B) {
return Math.abs(A.source.y - origin.y) - Math.abs(B.source.y - origin.y)
})
var newNode = links[0].source;
if (newNode) {
RED.view.select({nodes:[newNode]});
RED.view.reveal(newNode.id,false);
}
}
} else if (RED.workspaces.selection().length === 0) {
selectFirstNode();
}
}
function getChildren(node) {
return RED.nodes.filterLinks({source:node}).map(function(l) { return l.target})
}
function getParents(node) {
return RED.nodes.filterLinks({target:node}).map(function(l) { return l.source})
}
function getSiblings(node) {
var siblings = new Set();
var parents = getParents(node);
parents.forEach(function(p) {
getChildren(p).forEach(function(c) { siblings.add(c) })
});
var children = getChildren(node);
children.forEach(function(p) {
getParents(p).forEach(function(c) { siblings.add(c) })
});
siblings.delete(node);
return Array.from(siblings);
}
function gotoNextSibling() {
// 'next' defined as nearest on the y-axis below this node
var selection = RED.view.selection();
if (selection.nodes && selection.nodes.length === 1) {
var origin = selection.nodes[0];
var siblings = getSiblings(origin);
if (siblings.length > 0) {
siblings = siblings.filter(function(n) { return n.y > origin. y})
siblings.sort(function(A,B) {
return Math.abs(A.y - origin.y) - Math.abs(B.y - origin.y)
})
var newNode = siblings[0];
if (newNode) {
RED.view.select({nodes:[newNode]});
RED.view.reveal(newNode.id,false);
}
}
} else if (RED.workspaces.selection().length === 0) {
selectFirstNode();
}
}
function gotoPreviousSibling() {
// 'next' defined as nearest on the y-axis above this node
var selection = RED.view.selection();
if (selection.nodes && selection.nodes.length === 1) {
var origin = selection.nodes[0];
var siblings = getSiblings(origin);
if (siblings.length > 0) {
siblings = siblings.filter(function(n) { return n.y < origin. y})
siblings.sort(function(A,B) {
return Math.abs(A.y - origin.y) - Math.abs(B.y - origin.y)
})
var newNode = siblings[0];
if (newNode) {
RED.view.select({nodes:[newNode]});
RED.view.reveal(newNode.id,false);
}
}
} else if (RED.workspaces.selection().length === 0) {
selectFirstNode();
}
}
function addNode() {
var selection = RED.view.selection();
if (selection.nodes && selection.nodes.length === 1 && selection.nodes[0].outputs > 0) {
var selectedNode = selection.nodes[0];
RED.view.showQuickAddDialog([
selectedNode.x + selectedNode.w + 50,selectedNode.y
])
} else {
RED.view.showQuickAddDialog();
}
}
function gotoNearestNode(direction) {
var selection = RED.view.selection();
if (selection.nodes && selection.nodes.length === 1) {
var origin = selection.nodes[0];
var candidates = RED.nodes.filterNodes({z:origin.z});
candidates = candidates.concat(RED.view.getSubflowPorts());
var distances = [];
candidates.forEach(function(node) {
if (node === origin) {
return;
}
var deltaX = node.x - origin.x;
var deltaY = node.y - origin.y;
var delta = deltaY*deltaY + deltaX*deltaX;
var angle = (180/Math.PI)*Math.atan2(deltaY,deltaX);
if (angle < 0) { angle += 360 }
if (angle > 360) { angle -= 360 }
var weight;
// 0 - right
// 270 - above
// 90 - below
// 180 - left
switch(direction) {
case 'up': if (angle < 210 || angle > 330) { return }
weight = Math.max(Math.abs(270 - angle)/60, 0.2);
break;
case 'down': if (angle < 30 || angle > 150) { return }
weight = Math.max(Math.abs(90 - angle)/60, 0.2);
break;
case 'left': if (angle < 140 || angle > 220) { return }
weight = Math.max(Math.abs(180 - angle)/40, 0.1 );
break;
case 'right': if (angle > 40 && angle < 320) { return }
weight = Math.max(Math.abs(angle)/40, 0.1);
break;
}
weight = Math.max(weight,0.1);
distances.push({
node: node,
d: delta,
w: weight,
delta: delta*weight
})
})
if (distances.length > 0) {
distances.sort(function(A,B) {
return A.delta - B.delta
})
var newNode = distances[0].node;
if (newNode) {
RED.view.select({nodes:[newNode]});
RED.view.reveal(newNode.id,false);
}
}
} else if (RED.workspaces.selection().length === 0) {
var candidates = RED.view.getActiveNodes();
var distances = [];
candidates.forEach(function(node) {
var deltaX = node.x;
var deltaY = node.y;
var delta = deltaY*deltaY + deltaX*deltaX;
distances.push({node: node, delta: delta})
});
if (distances.length > 0) {
distances.sort(function(A,B) {
return A.delta - B.delta
})
var newNode = distances[0].node;
if (newNode) {
RED.view.select({nodes:[newNode]});
RED.view.reveal(newNode.id,false);
}
}
}
}
return {
init: function() {
RED.actions.add("core:show-selected-node-labels", function() { setSelectedNodeLabelState(true); })
@@ -206,6 +458,23 @@ RED.view.tools = (function() {
RED.actions.add("core:step-selection-right", function() { moveSelection(RED.view.gridSize(),0);});
RED.actions.add("core:step-selection-down", function() { moveSelection(0,RED.view.gridSize());});
RED.actions.add("core:step-selection-left", function() { moveSelection(-RED.view.gridSize(),0);});
RED.actions.add("core:select-connected-nodes", function() { selectConnected("all") });
RED.actions.add("core:select-downstream-nodes", function() { selectConnected("down") });
RED.actions.add("core:select-upstream-nodes", function() { selectConnected("up") });
RED.actions.add("core:go-to-next-node", function() { gotoNextNode() })
RED.actions.add("core:go-to-previous-node", function() { gotoPreviousNode() })
RED.actions.add("core:go-to-next-sibling", function() { gotoNextSibling() })
RED.actions.add("core:go-to-previous-sibling", function() { gotoPreviousSibling() })
RED.actions.add("core:go-to-nearest-node-on-left", function() { gotoNearestNode('left')})
RED.actions.add("core:go-to-nearest-node-on-right", function() { gotoNearestNode('right')})
RED.actions.add("core:go-to-nearest-node-above", function() { gotoNearestNode('up') })
RED.actions.add("core:go-to-nearest-node-below", function() { gotoNearestNode('down') })
// RED.actions.add("core:add-node", function() { addNode() })
},
/**
* Aligns all selected nodes to the current grid

View File

@@ -86,7 +86,7 @@ RED.view = (function() {
var quickAddLink = null;
var showAllLinkPorts = -1;
var groupNodeSelectPrimed = false;
var lastClickPosition = [];
var selectNodesOptions;
var clipboard = "";
@@ -503,9 +503,21 @@ RED.view = (function() {
RED.actions.add("core:paste-from-internal-clipboard",function(){importNodes(clipboard,{generateIds: true});});
RED.actions.add("core:delete-selection",deleteSelection);
RED.actions.add("core:edit-selected-node",editSelection);
RED.actions.add("core:go-to-selection",function() {
if (movingSet.length() > 0) {
var node = movingSet.get(0).n;
if (/^subflow:/.test(node.type)) {
RED.workspaces.show(node.type.substring(8))
} else if (node.type === 'group') {
enterActiveGroup(node);
redraw();
}
}
});
RED.actions.add("core:undo",RED.history.pop);
RED.actions.add("core:redo",RED.history.redo);
RED.actions.add("core:select-all-nodes",selectAll);
RED.actions.add("core:select-none", selectNone);
RED.actions.add("core:zoom-in",zoomIn);
RED.actions.add("core:zoom-out",zoomOut);
RED.actions.add("core:zoom-reset",zoomZero);
@@ -848,7 +860,7 @@ RED.view = (function() {
if (drag_lines.length > 0) {
clickedGroup = clickedGroup || RED.nodes.group(drag_lines[0].node.g)
}
showQuickAddDialog(point, null, clickedGroup);
showQuickAddDialog({position:point, group:clickedGroup});
}
}
if (mouse_mode === 0 && !(d3.event.metaKey || d3.event.ctrlKey)) {
@@ -869,7 +881,13 @@ RED.view = (function() {
}
}
function showQuickAddDialog(point, spliceLink, targetGroup, touchTrigger) {
function showQuickAddDialog(options) {
options = options || {};
var point = options.position || lastClickPosition;
var spliceLink = options.splice;
var targetGroup = options.group;
var touchTrigger = options.touchTrigger;
if (targetGroup && !targetGroup.active) {
selectGroup(targetGroup,false);
enterActiveGroup(targetGroup);
@@ -1458,15 +1476,15 @@ RED.view = (function() {
var mouseY = node.n.y;
if (outer[0][0].getIntersectionList) {
var svgRect = outer[0][0].createSVGRect();
svgRect.x = mouseX;
svgRect.y = mouseY;
svgRect.x = mouseX*scaleFactor;
svgRect.y = mouseY*scaleFactor;
svgRect.width = 1;
svgRect.height = 1;
nodes = outer[0][0].getIntersectionList(svgRect, outer[0][0]);
} else {
// Firefox doesn"t do getIntersectionList and that
// makes us sad
nodes = RED.view.getLinksAtPoint(mouseX,mouseY);
nodes = RED.view.getLinksAtPoint(mouseX*scaleFactor,mouseY*scaleFactor);
}
for (var i=0;i<nodes.length;i++) {
if (d3.select(nodes[i]).classed("red-ui-flow-link-background")) {
@@ -1522,6 +1540,7 @@ RED.view = (function() {
}
function canvasMouseUp() {
lastClickPosition = [d3.event.offsetX/scaleFactor,d3.event.offsetY/scaleFactor];
if (RED.view.DEBUG) { console.warn("canvasMouseUp", mouse_mode); }
var i;
var historyEvent;
@@ -1591,7 +1610,6 @@ RED.view = (function() {
if (n.x > x && n.x < x2 && n.y > y && n.y < y2) {
if (!activeGroup || RED.group.contains(activeGroup,n)) {
if (n.g && (!activeGroup || n.g !== activeGroup.id)) {
console.log("HERE")
var group = RED.nodes.group(n.g);
while (group.g && (!activeGroup || group.g !== activeGroup.id)) {
group = RED.nodes.group(group.g);
@@ -1733,7 +1751,6 @@ RED.view = (function() {
}
}
if (mouse_mode == RED.state.IMPORT_DRAGGING) {
RED.keyboard.remove("escape");
updateActiveNodes();
RED.nodes.dirty(true);
}
@@ -1767,7 +1784,21 @@ RED.view = (function() {
redraw();
}
function selectNone() {
if (mouse_mode === RED.state.MOVING || mouse_mode === RED.state.MOVING_ACTIVE) {
return;
}
if (mouse_mode === RED.state.IMPORT_DRAGGING) {
clearSelection();
RED.history.pop();
mouse_mode = 0;
} else if (activeGroup) {
exitActiveGroup()
} else {
clearSelection();
}
redraw();
}
function selectAll() {
if (mouse_mode === RED.state.SELECTING_NODE && selectNodesOptions.single) {
return;
@@ -2276,7 +2307,7 @@ RED.view = (function() {
}
function calculateTextWidth(str, className) {
var result=convertLineBreakCharacter(str);
var result = convertLineBreakCharacter(str);
var width = 0;
for (var i=0;i<result.length;i++) {
var calculateTextW=calculateTextDimensions(result[i],className)[0];
@@ -2809,7 +2840,11 @@ RED.view = (function() {
if (dblClickPrimed && mousedown_node == d && clickElapsed > 0 && clickElapsed < dblClickInterval) {
mouse_mode = RED.state.DEFAULT;
if (d.type != "subflow") {
RED.editor.edit(d);
if (/^subflow:/.test(d.type) && (d3.event.ctrlKey || d3.event.metaKey)) {
RED.workspaces.show(d.type.substring(8));
} else {
RED.editor.edit(d);
}
} else {
RED.editor.editSubflow(activeSubflow);
}
@@ -2874,7 +2909,6 @@ RED.view = (function() {
//var pos = [touch0.pageX,touch0.pageY];
//RED.touch.radialMenu.show(d3.select(this),pos);
if (mouse_mode == RED.state.IMPORT_DRAGGING) {
RED.keyboard.remove("escape");
var historyEvent = RED.history.peek();
if (activeSpliceLink) {
// TODO: DRY - droppable/nodeMouseDown/canvasMouseUp
@@ -2952,7 +2986,7 @@ RED.view = (function() {
clickTime = now;
dblClickPrimed = (lastClickNode == mousedown_node &&
(d3.event.touches || d3.event.button === 0) &&
!d3.event.shiftKey && !d3.event.metaKey && !d3.event.altKey && !d3.event.ctrlKey &&
!d3.event.shiftKey && !d3.event.altKey &&
clickElapsed < dblClickInterval
)
lastClickNode = mousedown_node;
@@ -2992,6 +3026,7 @@ RED.view = (function() {
enterActiveGroup(ag);
activeGroup.selected = true;
}
console.log(d3.event);
var cnodes = RED.nodes.getAllFlowNodes(mousedown_node);
for (var n=0;n<cnodes.length;n++) {
if (!cnodes[n].selected) {
@@ -3080,7 +3115,19 @@ RED.view = (function() {
// } else
if (d3.event.shiftKey) {
clearSelection();
var cnodes = RED.nodes.getAllFlowNodes(mousedown_node);
var clickPosition = (d3.event.offsetX/scaleFactor - mousedown_node.x)
var edgeDelta = (mousedown_node.w/2) - Math.abs(clickPosition);
var cnodes;
var targetEdgeDelta = mousedown_node.w > 30 ? 25 : 8;
if (edgeDelta < targetEdgeDelta) {
if (clickPosition < 0) {
cnodes = [mousedown_node].concat(RED.nodes.getAllUpstreamNodes(mousedown_node));
} else {
cnodes = [mousedown_node].concat(RED.nodes.getAllDownstreamNodes(mousedown_node));
}
} else {
cnodes = RED.nodes.getAllFlowNodes(mousedown_node);
}
for (var n=0;n<cnodes.length;n++) {
cnodes[n].selected = true;
cnodes[n].dirty = true;
@@ -3232,7 +3279,7 @@ RED.view = (function() {
d3.select(this).classed("red-ui-flow-link-splice",true);
var point = d3.mouse(this);
var clickedGroup = getGroupAt(point[0],point[1]);
showQuickAddDialog(point, selected_link, clickedGroup);
showQuickAddDialog({position:point, splice:selected_link, group:clickedGroup});
}
}
function linkTouchStart(d) {
@@ -3278,9 +3325,8 @@ RED.view = (function() {
if (d3.event.button === 1) {
return;
}
if (mouse_mode == RED.state.IMPORT_DRAGGING) {
RED.keyboard.remove("escape");
} else if (mouse_mode == RED.state.QUICK_JOINING) {
if (mouse_mode == RED.state.QUICK_JOINING) {
d3.event.stopPropagation();
return;
} else if (mouse_mode === RED.state.SELECTING_NODE) {
@@ -3399,6 +3445,7 @@ RED.view = (function() {
}
}
function getGroupAt(x,y) {
// x,y expected to be in node-co-ordinate space
var candidateGroups = {};
for (var i=0;i<activeGroups.length;i++) {
var g = activeGroups[i];
@@ -3487,7 +3534,10 @@ RED.view = (function() {
options.push({name:"undo",disabled:(RED.history.depth() === 0),onselect:function() {RED.history.pop();}});
options.push({name:"add",onselect:function() {
chartPos = chart.offset();
showQuickAddDialog([pos[0]-chartPos.left+chart.scrollLeft(),pos[1]-chartPos.top+chart.scrollTop()],undefined,undefined,true)
showQuickAddDialog({
position:[pos[0]-chartPos.left+chart.scrollLeft(),pos[1]-chartPos.top+chart.scrollTop()],
touchTrigger:true
})
}});
RED.touch.radialMenu.show(obj,pos,options);
@@ -4779,12 +4829,7 @@ RED.view = (function() {
}
}
RED.keyboard.add("*","escape",function(){
RED.keyboard.remove("escape");
clearSelection();
RED.history.pop();
mouse_mode = 0;
});
}
var historyEvent = {
@@ -5037,6 +5082,9 @@ RED.view = (function() {
return scaleFactor;
},
getLinksAtPoint: function(x,y) {
// x,y must be in SVG co-ordinate space
// if they come from a node.x/y, they will need to be scaled using
// scaleFactor first.
var result = [];
var links = outer.selectAll(".red-ui-flow-link-background")[0];
for (var i=0;i<links.length;i++) {
@@ -5112,6 +5160,18 @@ RED.view = (function() {
getActiveNodes: function() {
return activeNodes;
},
getSubflowPorts: function() {
var result = [];
if (activeSubflow) {
var subflowOutputs = nodeLayer.selectAll(".red-ui-flow-subflow-port-output").data(activeSubflow.out,function(d,i){ return d.id;});
subflowOutputs.each(function(d,i) { result.push(d) })
var subflowInputs = nodeLayer.selectAll(".red-ui-flow-subflow-port-input").data(activeSubflow.in,function(d,i){ return d.id;});
subflowInputs.each(function(d,i) { result.push(d) })
var subflowStatus = nodeLayer.selectAll(".red-ui-flow-subflow-port-status").data(activeSubflow.status?[activeSubflow.status]:[],function(d,i){ return d.id;});
subflowStatus.each(function(d,i) { result.push(d) })
}
return result;
},
selectNodes: function(options) {
$("#red-ui-workspace-tabs-shade").show();
$("#red-ui-palette-shade").show();
@@ -5187,6 +5247,7 @@ RED.view = (function() {
clipboard: function() {
return clipboard
},
redrawStatus: redrawStatus
redrawStatus: redrawStatus,
showQuickAddDialog:showQuickAddDialog
};
})();

View File

@@ -20,6 +20,19 @@ RED.workspaces = (function() {
var activeWorkspace = 0;
var workspaceIndex = 0;
var viewStack = [];
var viewStackPos = 0;
function addToViewStack(id) {
if (viewStackPos !== viewStack.length) {
viewStack.splice(viewStackPos);
}
viewStack.push(id);
viewStackPos = viewStack.length;
// console.warn("addToViewStack",id,viewStack);
}
function addWorkspace(ws,skipHistoryEntry,targetIndex) {
if (ws) {
workspace_tabs.addTab(ws,targetIndex);
@@ -245,6 +258,9 @@ RED.workspaces = (function() {
RED.view.focus();
},
onclick: function(tab) {
if (tab.id !== activeWorkspace) {
addToViewStack(activeWorkspace);
}
RED.view.focus();
},
ondblclick: function(tab) {
@@ -328,8 +344,20 @@ RED.workspaces = (function() {
createWorkspaceTabs();
RED.events.on("sidebar:resize",workspace_tabs.resize);
RED.actions.add("core:show-next-tab",workspace_tabs.nextTab);
RED.actions.add("core:show-previous-tab",workspace_tabs.previousTab);
RED.actions.add("core:show-next-tab",function() {
var oldActive = activeWorkspace;
workspace_tabs.nextTab();
if (oldActive !== activeWorkspace) {
addToViewStack(oldActive)
}
});
RED.actions.add("core:show-previous-tab",function() {
var oldActive = activeWorkspace;
workspace_tabs.previousTab();
if (oldActive !== activeWorkspace) {
addToViewStack(oldActive)
}
});
RED.menu.setAction('menu-item-workspace-delete',function() {
deleteWorkspace(RED.nodes.workspace(activeWorkspace));
@@ -349,6 +377,23 @@ RED.workspaces = (function() {
RED.actions.invoke("core:search","type:tab ");
})
RED.actions.add("core:go-to-previous-location", function() {
if (viewStackPos > 0) {
if (viewStackPos === viewStack.length) {
// We're at the end of the stack. Remember the activeWorkspace
// so we can come back to it.
viewStack.push(activeWorkspace);
}
RED.workspaces.show(viewStack[--viewStackPos],true);
}
})
RED.actions.add("core:go-to-next-location", function() {
if (viewStackPos < viewStack.length - 1) {
RED.workspaces.show(viewStack[++viewStackPos],true);
}
})
hideWorkspace();
}
@@ -444,15 +489,22 @@ RED.workspaces = (function() {
selection: function() {
return workspace_tabs.selection();
},
show: function(id) {
show: function(id,skipStack) {
if (!workspace_tabs.contains(id)) {
var sf = RED.nodes.subflow(id);
if (sf) {
addWorkspace({type:"subflow",id:id,icon:"red/images/subflow_tab.svg",label:sf.name, closeable: true});
addWorkspace(
{type:"subflow",id:id,icon:"red/images/subflow_tab.svg",label:sf.name, closeable: true},
null,
workspace_tabs.activeIndex()+1
);
} else {
return;
}
}
if (!skipStack && activeWorkspace !== id) {
addToViewStack(activeWorkspace)
}
workspace_tabs.activateTab(id);
},
refresh: function() {

View File

@@ -146,6 +146,13 @@ body {
background-size: contain
}
.red-ui-font-code {
font-family: $monospace-font;
font-size: $primary-font-size;
color: $info-text-code-color;
white-space: nowrap;
}
code {
font-family: $monospace-font;
font-size: $primary-font-size;

View File

@@ -174,8 +174,8 @@ button.red-ui-tray-resize-button {
.red-ui-editor .red-ui-tray {
.dialog-form, #dialog-form, #node-config-dialog-edit-form {
margin: 20px;
height: calc(100% - 40px);
margin: 10px 20px;
height: calc(100% - 20px);
}
}
@@ -765,6 +765,10 @@ button.red-ui-toggleButton.toggle {
width: calc(100% - 10px);
padding-left: 3px;
}
select {
padding: 0 3px;
font-size: 11px;
}
.placeholder-input {
span:first-child {
display:inline-block;

View File

@@ -139,6 +139,9 @@
stroke-width: 2;
}
.red-ui-flow-node-icon-group {
text {
@include disable-selection;
}
.fa-lg {
@include disable-selection;
stroke: none;
@@ -252,11 +255,11 @@ g.red-ui-flow-node-selected {
}
}
@each $current-color in red green yellow blue grey gray {
.red-ui-flow-node-status-dot-#{$current-color} {
.red-ui-flow-node-status-dot-#{""+$current-color} {
fill: map-get($node-status-colors,$current-color);
stroke: map-get($node-status-colors,$current-color);
}
.red-ui-flow-node-status-ring-#{$current-color} {
.red-ui-flow-node-status-ring-#{""+$current-color} {
fill: $view-background;
stroke: map-get($node-status-colors,$current-color);
}

View File

@@ -29,8 +29,30 @@
}
}
}
.red-ui-clipboard-dialog-tab-clipboard {
#red-ui-clipboard-dialog-export-tab-clipboard-preview {
.red-ui-treeList-container,.red-ui-editableList-border {
border: none;
border-radius: 0;
}
}
#red-ui-clipboard-dialog-export-tab-clipboard-json {
padding: 10px 10px 0;
}
#red-ui-clipboard-dialog-import-tab-clipboard {
padding: 10px;
}
.red-ui-clipboard-dialog-export-tab-clipboard-tab {
position: absolute;
top: 40px;
right: 0;
left: 0;
bottom: 0;
}
.red-ui-clipboard-dialog-tab-clipboard {
textarea {
resize: none;
width: 100%;
@@ -71,10 +93,9 @@
border:1px solid $primary-border-color;
}
.red-ui-clipboard-dialog-tab-library {
.form-row {
margin-left: 10px;
}
#red-ui-clipboard-dialog-export-tab-library-filename {
height: auto !important;
margin-left: 10px;
}
#red-ui-clipboard-dialog {
@@ -88,7 +109,7 @@
#red-ui-clipboard-dialog-tab-library-name {
width: calc(100% - 120px);
}
#red-ui-clipboard-dialog-export-tab-library-browser {
.red-ui-clipboard-dialog-tabs-content>div.red-ui-clipboard-dialog-export-tab-library-browser {
height: calc(100% - 60px);
margin-bottom: 13px;
border-bottom: 1px solid $primary-border-color;
@@ -210,4 +231,42 @@
padding: 0;
margin: 0;
}
}
#red-ui-settings-tab-library-manager {
box-sizing: border-box;
padding: 10px;
position: relative;
height: 100%;
li {
padding: 0;
}
}
.red-ui-settings-tab-library-entry {
display: flex;
flex-direction: row;
span:not(:nth-child(2)) {
@include disable-selection;
}
span {
padding: 8px 0;
}
span:first-child {
display: inline-block;
width: 20px;
padding-right: 8px;
text-align: center;
flex-grow: 0;
}
span:nth-child(2) {
flex-grow: 1;
}
span:nth-child(3), span:nth-child(4) {
flex-grow: 0;
padding-right: 5px;
color: $secondary-text-color;
font-size: 0.9em;
}
}

View File

@@ -25,7 +25,7 @@
.red-ui-notification {
box-sizing: border-box;
position: relative;
padding: 14px 18px;
padding: 8px 18px 0px;
margin-bottom: 4px;
box-shadow: 0 1px 1px 1px $shadow;
background-color: $secondary-background;
@@ -35,6 +35,7 @@
overflow: hidden;
.ui-dialog-buttonset {
margin-top: 20px;
margin-bottom: 10px;
}
}
.red-ui-notification p:first-child {

View File

@@ -131,10 +131,10 @@
width: 120px;
background-size: contain;
position: relative;
&:not(.red-ui-palette-node-config):first-child {
&:not(.red-ui-palette-node-config):not(.red-ui-palette-node-small):first-child {
margin-top: 15px;
}
&:not(.red-ui-palette-node-config):last-child {
&:not(.red-ui-palette-node-config):not(.red-ui-palette-node-small):last-child {
margin-bottom: 15px;
}
}
@@ -229,3 +229,47 @@
left: 1px;
}
}
////////////////
.red-ui-node-list-item {
display: inline-block;
padding: 0;
font-size: 13px;
border: none;
}
.red-ui-node-icon {
display: inline-block;
float:left;
width: 24px;
height: 20px;
margin-top: 1px;
// width: 30px;
// height: 25px;
border-radius: 3px;
border: 1px solid $node-border;
background-position: 5% 50%;
background-repeat: no-repeat;
background-size: contain;
position: relative;
background-color: $node-icon-background-color;
text-align: center;
.red-ui-palette-icon {
width: 20px;
}
.red-ui-palette-icon-fa {
font-size: 14px;
position: relative;
top: -1px;
left: 0px;
}
}
.red-ui-node-label {
margin-left: 32px;
line-height: 23px;
white-space: nowrap;
color: $secondary-text-color;
}

View File

@@ -807,6 +807,7 @@ div.red-ui-projects-dialog-ssh-public-key {
border: 1px solid $secondary-border-color;
.red-ui-projects-edit-form-sublabel {
margin-top: -8px !important;
margin-right: 50px;
display: block !important;
width: auto !important;
}

View File

@@ -326,13 +326,17 @@ div.red-ui-info-table {
border-bottom: 1px solid $secondary-border-color;
}
}
.red-ui-info-outline,.red-ui-sidebar-help-toc, #red-ui-clipboard-dialog-import-conflicts-list {
.red-ui-info-outline,
// TODO: remove these classes for 2.0. Keeping in 1.x for backwards compatibility
// of theme generators.
.red-ui-sidebar-help-toc, #red-ui-clipboard-dialog-import-conflicts-list, #red-ui-clipboard-dialog-export-tab-clipboard-preview
{
.red-ui-info-outline-item {
display: inline-block;
padding: 0;
font-size: 13px;
border: none;
.red-ui-palette-icon-fa {
&:not(.red-ui-node-list-item) .red-ui-palette-icon-fa {
position: relative;
top: 1px;
left: 0px;

View File

@@ -24,7 +24,7 @@
margin: 0;
vertical-align: middle;
box-sizing: border-box;
overflow:visible;
overflow: hidden;
position: relative;
&[disabled] {
input, button {
@@ -33,7 +33,7 @@
cursor: not-allowed;
}
}
.red-ui-typedInput-input-wrap {
flex-grow: 1;
}

View File

@@ -32,6 +32,7 @@
white-space: nowrap;
transition: right 0.2s ease;
overflow: hidden;
@include disable-selection;
label {
padding: 1px 8px;
@@ -47,7 +48,6 @@
margin-right: 10px;
padding: 2px 8px;
}
.button-group {
@include disable-selection;

View File

@@ -163,12 +163,27 @@
$("#node-input-property-container").editableList('height',height);
}
RED.nodes.registerType('inject',{
RED.nodes.registerType('inject',{  
category: 'common',
color:"#a6bbcf",
defaults: {
name: {value:""},
props:{value:[{p:"payload"},{p:"topic",vt:"str"}]},
props:{value:[{p:"payload"},{p:"topic",vt:"str"}], validate:function(v) {
if (!v || v.length === 0) { return true }
for (var i=0;i<v.length;i++) {
if (/msg|flow|global/.test(v[i].vt)) {
if (!RED.utils.validatePropertyExpression(v[i].v)) {
return false;
}
} else if (v[i].vt === "jsonata") {
try{jsonata(v[i].v);}catch(e){return false;}
} else if ([i].vt === "json") {
try{JSON.parse(v[i].v);}catch(e){return false;}
}
}
return true;
}
},
repeat: {value:"", validate:function(v) { return ((v === "") || (RED.validators.number(v) && (v >= 0) && (v <= 2147483))) }},
crontab: {value:""},
once: {value:false},
@@ -199,7 +214,7 @@
for (var i=0,l=props.length; i<l; i++) {
if (i > 0) lab += "\n";
if (i === 5) {
lab += " + "+(props.length-4);
lab += "... +"+(props.length-5);
break;
}
lab += props[i].p+": ";
@@ -621,7 +636,7 @@
url: "inject/"+this.id,
type:"POST",
success: function(resp) {
RED.notify(node._("inject.success",{label:label}),{type:"success",id:"inject"});
RED.notify(node._("inject.success",{label:label}),{type:"success",id:"inject", timeout: 2000});
},
error: function(jqXHR,textStatus,errorThrown) {
if (jqXHR.status == 404) {

View File

@@ -108,7 +108,7 @@
toggle: "active",
visible: function() { return this.tosidebar; },
onclick: function() {
var label = this.name||"debug";
var label = RED.utils.sanitize(this.name||"debug");
var node = this;
activateAjaxCall(node, node.active, function(resp, textStatus, xhr) {
var historyEvent = {
@@ -129,9 +129,9 @@
RED.history.push(historyEvent);
RED.view.redraw();
if (xhr.status == 200) {
RED.notify(node._("debug.notification.activated",{label:label}),"success");
RED.notify(node._("debug.notification.activated",{label:label}),{type: "success", timeout: 2000});
} else if (xhr.status == 201) {
RED.notify(node._("debug.notification.deactivated",{label:label}),"success");
RED.notify(node._("debug.notification.deactivated",{label:label}),{type: "success", timeout: 2000});
}
});
}

View File

@@ -2,7 +2,8 @@ module.exports = function(RED) {
"use strict";
var util = require("util");
var events = require("events");
//var path = require("path");
const fs = require("fs-extra");
const path = require("path");
var debuglength = RED.settings.debugMaxLength || 1000;
var useColors = RED.settings.debugUseColors || false;
util.inspect.styles.boolean = "red";
@@ -249,11 +250,34 @@ module.exports = function(RED) {
}
});
let cachedDebugView;
RED.httpAdmin.get("/debug/view/view.html", function(req,res) {
if (!cachedDebugView) {
fs.readFile(path.join(__dirname,"lib","debug","view.html")).then(data => {
let customStyles = "";
try {
let customStyleList = RED.settings.editorTheme.page._.css || [];
customStyleList.forEach(style => {
customStyles += `<link rel="stylesheet" href="../../${style}">\n`
})
} catch(err) {}
cachedDebugView = data.toString().replace("<!-- INSERT-THEME-CSS -->",customStyles)
res.set('Content-Type', 'text/html');
res.send(cachedDebugView).end();
}).catch(err => {
res.sendStatus(404);
})
} else {
res.send(cachedDebugView).end();
}
});
// As debug/view/debug-utils.js is loaded via <script> tag, it won't get
// the auth header attached. So do not use RED.auth.needsPermission here.
RED.httpAdmin.get("/debug/view/*",function(req,res) {
var options = {
root: __dirname + '/lib/debug/',
root: path.join(__dirname,"lib","debug"),
dotfiles: 'deny'
};
res.sendFile(req.params[0], options);

View File

@@ -18,7 +18,7 @@
color:"#c0edc0",
defaults: {
name: {value:""},
scope: {value:[]},
scope: {value:[], type:"*[]"},
uncaught: {value:false}
},
inputs:0,

View File

@@ -30,7 +30,7 @@
color:"#e49191",
defaults: {
name: {value:""},
scope: {value:null},
scope: {value:null, type:"*[]"},
uncaught: {value:false}
},
inputs:0,

View File

@@ -26,7 +26,7 @@
color:"#94c1d0",
defaults: {
name: {value:""},
scope: {value:null}
scope: {value:null, type:"*[]"}
},
inputs:0,
outputs:1,

View File

@@ -187,7 +187,7 @@
color:"#ddd",//"#87D8CF",
defaults: {
name: {value:""},
links: { value: [] }
links: { value: [], type:"link out[]" }
},
inputs:0,
outputs:1,
@@ -216,7 +216,7 @@
color:"#ddd",//"#87D8CF",
defaults: {
name: {value:""},
links: { value: []}
links: { value: [], type:"link in[]"}
},
align:"right",
inputs:1,

View File

@@ -120,7 +120,7 @@ RED.debug = (function() {
filteredNodes[node.id] = !$(this).prop('checked');
$(".red-ui-debug-msg-node-"+node.id.replace(/\./g,"_")).toggleClass('hide',filteredNodes[node.id]);
});
if (!node.active || RED.nodes.workspace(node.z).disabled) {
if ((node.hasOwnProperty("active") && !node.active) || RED.nodes.workspace(node.z).disabled) {
container.addClass('disabled');
muteControl.checkboxSet('disable');
}
@@ -224,26 +224,91 @@ RED.debug = (function() {
});
RED.popover.tooltip(toolbar.find("#red-ui-sidebar-debug-clear"),RED._('node-red:debug.sidebar.clearLog'),"core:clear-debug-messages");
return {
content: content,
footer: footerToolbar
}
};
}
function containsDebug(sid, map) {
var item = map[sid];
if (item) {
if (item.debug === undefined) {
var sfs = Object.keys(item.subflows);
var contain = false;
for (var i = 0; i < sfs.length; i++) {
var sf = sfs[i];
if (containsDebug(sf, map)) {
contain = true;
break;
}
}
item.debug = contain;
}
return item.debug;
}
return false;
}
function refreshDebugNodeList() {
debugNodeList.editableList('empty');
var candidateNodes = RED.nodes.filterNodes({type:'debug'});
var workspaceOrder = RED.nodes.getWorkspaceOrder();
var workspaceOrderMap = {};
workspaceOrder.forEach(function(ws,i) {
workspaceOrderMap[ws] = i;
});
candidateNodes = candidateNodes.filter(function(node) {
return workspaceOrderMap.hasOwnProperty(node.z);
})
var candidateNodes = [];
var candidateSFs = [];
var subflows = {};
RED.nodes.eachNode(function (n) {
var nt = n.type;
if (nt === "debug") {
if (n.z in workspaceOrderMap) {
candidateNodes.push(n);
}
else {
var sf = RED.nodes.subflow(n.z);
if (sf) {
subflows[sf.id] = {
debug: true,
subflows: {}
};
}
}
}
else if(nt.substring(0, 8) === "subflow:") {
if (n.z in workspaceOrderMap) {
candidateSFs.push(n);
}
else {
var psf = RED.nodes.subflow(n.z);
if (psf) {
var sid = nt.substring(8);
var item = subflows[psf.id];
if (!item) {
item = {
debug: undefined,
subflows: {}
};
subflows[psf.id] = item;
}
item.subflows[sid] = true;
}
}
}
});
candidateSFs.forEach(function (sf) {
var sid = sf.type.substring(8);
if (containsDebug(sid, subflows)) {
candidateNodes.push(sf);
}
});
candidateNodes.sort(function(A,B) {
var wsA = workspaceOrderMap[A.z];
var wsB = workspaceOrderMap[B.z];
@@ -253,7 +318,7 @@ RED.debug = (function() {
var labelA = RED.utils.getNodeLabel(A,A.id);
var labelB = RED.utils.getNodeLabel(B,B.id);
return labelA.localeCompare(labelB);
})
});
var currentWs = null;
var nodeList = [];
candidateNodes.forEach(function(node) {
@@ -262,10 +327,8 @@ RED.debug = (function() {
nodeList.push(RED.nodes.workspace(node.z));
}
nodeList.push(node);
})
debugNodeList.editableList('addItems',nodeList)
});
debugNodeList.editableList('addItems',nodeList);
}
function getTimestamp() {

View File

@@ -2,6 +2,7 @@
<head>
<link rel="stylesheet" href="../../red/style.min.css">
<link rel="stylesheet" href="../../vendor/font-awesome/css/font-awesome.min.css">
<!-- INSERT-THEME-CSS -->
<title>Node-RED Debug Tools</title>
</head>
<body class="red-ui-editor red-ui-debug-window">

View File

@@ -1,60 +1,318 @@
<script type="text/html" data-template-name="function">
<style>
.func-tabs-row {
margin-bottom: 0;
}
#node-input-libs-container-row .red-ui-editableList-container {
padding: 0px;
}
#node-input-libs-container-row .red-ui-editableList-container li {
padding:5px;
}
#node-input-libs-container-row .red-ui-editableList-item-remove {
right: 5px;
}
.node-libs-entry {
display: flex;
}
.node-libs-entry .node-input-libs-var, .node-libs-entry .red-ui-typedInput-container {
flex-grow: 1;
}
.node-libs-entry > code,.node-libs-entry > span {
line-height: 30px;
}
.node-libs-entry > input[type=text] {
border-radius: 0;
border-left: none;
border-top: none;
border-right: none;
padding-top: 2px;
padding-bottom: 2px;
margin-top: 4px;
margin-bottom: 2px;
height: 26px;
}
.node-libs-entry > span > i {
display: none;
}
.node-libs-entry > span.input-error > i {
display: inline;
}
</style>
<input type="hidden" id="node-input-func">
<input type="hidden" id="node-input-noerr">
<input type="hidden" id="node-input-finalize">
<input type="hidden" id="node-input-initialize">
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="common.label.name"></span></label>
<div style="display: inline-block; width: calc(100% - 105px)"><input type="text" id="node-input-name" data-i18n="[placeholder]common.label.name"></div>
</div>
<div class="form-row">
<div class="form-row func-tabs-row">
<ul style="min-width: 600px; margin-bottom: 20px;" id="func-tabs"></ul>
</div>
<div id="func-tabs-content" style="min-height: calc(100% - 95px);">
<div id="func-tab-init" style="display:none">
<div class="form-row" style="margin-bottom: 0px;">
<input type="hidden" id="node-input-initialize" autofocus="autofocus">
</div>
<div class="form-row node-text-editor-row" style="position:relative">
<div style="position: absolute; right:0; bottom: calc(100% + 3px);"><button id="node-init-expand-js" class="red-ui-button red-ui-button-small"><i class="fa fa-expand"></i></button></div>
<div style="height: 250px; min-height:150px; margin-top: 30px;" class="node-text-editor" id="node-input-init-editor" ></div>
</div>
</div>
<div id="func-tab-body" style="display:none">
<div class="form-row" style="margin-bottom: 0px;">
<input type="hidden" id="node-input-func" autofocus="autofocus">
<input type="hidden" id="node-input-noerr">
</div>
<div class="form-row node-text-editor-row" style="position:relative">
<div style="position: absolute; right:0; bottom: calc(100% + 3px);"><button id="node-function-expand-js" class="red-ui-button red-ui-button-small"><i class="fa fa-expand"></i></button></div>
<div style="height: 220px; min-height:120px; margin-top: 30px;" class="node-text-editor" id="node-input-func-editor" ></div>
</div>
<div class="form-row" style="margin-bottom: 0px">
<div id="func-tab-config" style="display:none">
<div class="form-row">
<label for="node-input-outputs"><i class="fa fa-random"></i> <span data-i18n="function.label.outputs"></span></label>
<input id="node-input-outputs" style="width: 60px;" value="1">
</div>
<div class="form-row node-input-libs-row hide" style="margin-bottom: 0px;">
<label><i class="fa fa-cubes"></i> <span data-i18n="function.label.modules"></span></label>
</div>
<div class="form-row node-input-libs-row hide" id="node-input-libs-container-row">
<ol id="node-input-libs-container"></ol>
</div>
</div>
<div id="func-tab-init" style="display:none">
<div class="form-row node-text-editor-row" style="position:relative">
<div style="height: 250px; min-height:150px;" class="node-text-editor" id="node-input-init-editor" ></div>
<div style="position: absolute; right:0; bottom: calc(100% - 20px); z-Index: 10;"><button id="node-init-expand-js" class="red-ui-button red-ui-button-small"><i class="fa fa-expand"></i></button></div>
</div>
</div>
<div id="func-tab-body" style="display:none">
<div class="form-row node-text-editor-row" style="position:relative">
<div style="height: 220px; min-height:150px;" class="node-text-editor" id="node-input-func-editor" ></div>
<div style="position: absolute; right:0; bottom: calc(100% - 20px); z-Index: 10;"><button id="node-function-expand-js" class="red-ui-button red-ui-button-small"><i class="fa fa-expand"></i></button></div>
</div>
</div>
<div id="func-tab-finalize" style="display:none">
<div class="form-row" style="margin-bottom: 0px;">
<input type="hidden" id="node-input-finalize" autofocus="autofocus">
</div>
<div class="form-row node-text-editor-row" style="position:relative">
<div style="position: absolute; right:0; bottom: calc(100% + 3px);"><button id="node-finalize-expand-js" class="red-ui-button red-ui-button-small"><i class="fa fa-expand"></i></button></div>
<div style="height: 250px; min-height:150px; margin-top: 30px;" class="node-text-editor" id="node-input-finalize-editor" ></div>
<div style="height: 250px; min-height:150px;" class="node-text-editor" id="node-input-finalize-editor" ></div>
<div style="position: absolute; right:0; bottom: calc(100% - 20px); z-Index: 10;"><button id="node-finalize-expand-js" class="red-ui-button red-ui-button-small"><i class="fa fa-expand"></i></button></div>
</div>
</div>
</div>
</script>
<script type="text/javascript">
(function() {
var invalidModuleVNames = [
'console',
'util',
'Buffer',
'Date',
'RED',
'node',
'__node__',
'context',
'flow',
'global',
'env',
'setTimeout',
'clearTimeout',
'setInterval',
'clearInterval',
'promisify'
]
var knownFunctionNodes = {};
RED.events.on("nodes:add", function(n) {
if (n.type === "function") {
knownFunctionNodes[n.id] = n;
}
})
RED.events.on("nodes:remove", function(n) {
if (n.type === "function") {
delete knownFunctionNodes[n.id];
}
})
var missingModules = [];
var missingModuleReasons = {};
RED.events.on("runtime-state", function(event) {
if (event.error === "missing-modules") {
missingModules = event.modules.map(function(m) { missingModuleReasons[m.module] = m.error; return m.module });
for (var id in knownFunctionNodes) {
if (knownFunctionNodes.hasOwnProperty(id) && knownFunctionNodes[id].libs && knownFunctionNodes[id].libs.length > 0) {
RED.editor.validateNode(knownFunctionNodes[id])
}
}
} else if (!event.text) {
missingModuleReasons = {};
missingModules = [];
for (var id in knownFunctionNodes) {
if (knownFunctionNodes.hasOwnProperty(id) && knownFunctionNodes[id].libs && knownFunctionNodes[id].libs.length > 0) {
RED.editor.validateNode(knownFunctionNodes[id])
}
}
}
RED.view.redraw();
});
var installAllowList = ['*'];
var installDenyList = [];
var modulesEnabled = true;
if (RED.settings.get('externalModules.modules.allowInstall', true) === false) {
modulesEnabled = false;
}
var settingsAllowList = RED.settings.get("externalModules.modules.allowList")
var settingsDenyList = RED.settings.get("externalModules.modules.denyList")
if (settingsAllowList || settingsDenyList) {
installAllowList = settingsAllowList;
installDenyList = settingsDenyList
}
installAllowList = RED.utils.parseModuleList(installAllowList);
installDenyList = RED.utils.parseModuleList(installDenyList);
// object that maps from library name to its descriptor
var allLibs = [];
function moduleName(module) {
var match = /^([^@]+)@(.+)/.exec(module);
if (match) {
return [match[1], match[2]];
}
return [module, undefined];
}
function getAllUsedModules() {
var moduleSet = new Set();
for (var id in knownFunctionNodes) {
if (knownFunctionNodes.hasOwnProperty(id)) {
if (knownFunctionNodes[id].libs) {
for (var i=0, l=knownFunctionNodes[id].libs.length; i<l; i++) {
if (RED.utils.checkModuleAllowed(knownFunctionNodes[id].libs[i].module,null,installAllowList,installDenyList)) {
moduleSet.add(knownFunctionNodes[id].libs[i].module);
}
}
}
}
}
var modules = Array.from(moduleSet);
modules.sort();
return modules;
}
function prepareLibraryConfig(node) {
$(".node-input-libs-row").show();
var usedModules = getAllUsedModules();
var typedModules = usedModules.map(function(l) {
return {icon:"fa fa-cube", value:l,label:l,hasValue:false}
})
typedModules.push({
value:"_custom_", label:RED._("editor:subflow.licenseOther"), icon:"red/images/typedInput/az.svg"
})
var libList = $("#node-input-libs-container").css('min-height','100px').css('min-width','450px').editableList({
addItem: function(container,i,opt) {
var parent = container.parent();
var row0 = $("<div/>").addClass("node-libs-entry").appendTo(container);
var fieldWidth = "260px";
$('<code>const </code>').appendTo(row0);
var fvar = $("<input/>", {
class: "node-input-libs-var red-ui-font-code",
placeholder: RED._("node-red:function.require.var"),
type: "text"
}).css({
width: "120px",
"margin-left": "5px"
}).appendTo(row0).val(opt.var);
var vnameWarning = $('<span style="display:inline-block; width: 16px;"><i class="fa fa-warning"></i></span>').appendTo(row0);
RED.popover.tooltip(vnameWarning.find("i"),function() {
var val = fvar.val();
if (invalidModuleVNames.indexOf(val) !== -1) {
return RED._("node-red:function.error.moduleNameReserved",{name:val})
} else {
return RED._("node-red:function.error.moduleNameError",{name:val})
}
})
$('<code> = require(</code>').appendTo(row0);
var fmodule = $("<input/>", {
class: "node-input-libs-val",
placeholder: RED._("node-red:function.require.module"),
type: "text"
}).css({
width: "180px",
}).appendTo(row0).typedInput({
types: typedModules,
default: usedModules.indexOf(opt.module) > -1 ? opt.module : "_custom_"
});
if (usedModules.indexOf(opt.module) === -1) {
fmodule.typedInput('value', opt.module);
}
$('<code>)</code>').appendTo(row0);
var moduleWarning = $('<span style="display:inline-block; width: 16px;"><i class="fa fa-warning"></i></span>').appendTo(row0);
RED.popover.tooltip(moduleWarning.find("i"),function() {
var val = fmodule.typedInput("type");
if (val === "_custom_") {
val = fmodule.val();
}
var errors = [];
if (!RED.utils.checkModuleAllowed(val,null,installAllowList,installDenyList)) {
return RED._("node-red:function.error.moduleNotAllowed",{module:val});
} else {
return RED._("node-red:function.error.moduleLoadError",{module:val,error:missingModuleReasons[val]});
}
})
fvar.on("change keyup paste", function (e) {
var v = $(this).val().trim();
if (v === "" || / /.test(v) || invalidModuleVNames.indexOf(v) !== -1) {
fvar.addClass("input-error");
vnameWarning.addClass("input-error");
} else {
fvar.removeClass("input-error");
vnameWarning.removeClass("input-error");
}
});
fmodule.on("change keyup paste", function (e) {
var val = $(this).typedInput("type");
if (val === "_custom_") {
val = $(this).val();
}
var varName = val.trim().replace(/^@/,"").replace(/@.*$/,"").replace(/[-_/]./g, function(v) { return v[1].toUpperCase() });
fvar.val(varName);
fvar.trigger("change");
if (RED.utils.checkModuleAllowed(val,null,installAllowList,installDenyList) && (missingModules.indexOf(val) === -1)) {
fmodule.removeClass("input-error");
moduleWarning.removeClass("input-error");
} else {
fmodule.addClass("input-error");
moduleWarning.addClass("input-error");
}
});
if (RED.utils.checkModuleAllowed(opt.module,null,installAllowList,installDenyList) && (missingModules.indexOf(opt.module) === -1)) {
fmodule.removeClass("input-error");
moduleWarning.removeClass("input-error");
} else {
fmodule.addClass("input-error");
moduleWarning.addClass("input-error");
}
if (opt.var) {
fvar.trigger("change");
}
},
removable: true
});
var libs = node.libs || [];
for (var i=0,l=libs.length;i<l; i++) {
libList.editableList('addItem',libs[i])
}
}
RED.nodes.registerType('function',{
color:"#fdd0a2",
category: 'function',
@@ -64,7 +322,26 @@
outputs: {value:1},
noerr: {value:0,required:true,validate:function(v) { return !v; }},
initialize: {value:""},
finalize: {value:""}
finalize: {value:""},
libs: {value: [], validate: function(v) {
if (!v) { return true; }
for (var i=0,l=v.length;i<l;i++) {
var m = v[i];
if (!RED.utils.checkModuleAllowed(m.module,null,installAllowList,installDenyList)) {
return false
}
if (m.var === "" || / /.test(m.var)) {
return false;
}
if (missingModules.indexOf(m.module) > -1) {
return false;
}
if (invalidModuleVNames.indexOf(m.var) !== -1){
return false;
}
}
return true;
}}
},
inputs:1,
outputs:1,
@@ -85,6 +362,12 @@
$("#" + tab.id).show();
}
});
tabs.addTab({
id: "func-tab-config",
iconClass: "fa fa-cog",
label: that._("function.label.setup")
});
tabs.addTab({
id: "func-tab-init",
label: that._("function.label.initialize")
@@ -97,6 +380,7 @@
id: "func-tab-finalize",
label: that._("function.label.finalize")
});
tabs.activateTab("func-tab-body");
$( "#node-input-outputs" ).spinner({
@@ -205,7 +489,9 @@
RED.popover.tooltip($("#node-function-expand-js"), RED._("node-red:common.label.expand"));
RED.popover.tooltip($("#node-finalize-expand-js"), RED._("node-red:common.label.expand"));
if (RED.settings.functionExternalModules !== false) {
prepareLibraryConfig(that);
}
},
oneditsave: function() {
var node = this;
@@ -237,7 +523,28 @@
$("#node-input-noerr").val(noerr);
this.noerr = noerr;
if (RED.settings.functionExternalModules === true) {
var libs = $("#node-input-libs-container").editableList("items");
node.libs = [];
libs.each(function(i) {
var item = $(this);
var v = item.find(".node-input-libs-var").val();
var n = item.find(".node-input-libs-val").typedInput("type");
if (n === "_custom_") {
n = item.find(".node-input-libs-val").val();
}
if ((!v || (v === "")) ||
(!n || (n === ""))) {
return;
}
node.libs.push({
var: v,
module: n
});
});
} else {
node.libs = [];
}
},
oneditcancel: function() {
var node = this;
@@ -259,18 +566,19 @@
}
var editorRow = $("#dialog-form>div.node-text-editor-row");
height -= (parseInt(editorRow.css("marginTop"))+parseInt(editorRow.css("marginBottom")));
$(".node-text-editor").css("height",height+"px");
this.editor.resize();
$("#dialog-form .node-text-editor").css("height",height+"px");
var height = size.height;
$("#node-input-init-editor").css("height", (height -105)+"px");
$("#node-input-func-editor").css("height", (height -145)+"px");
$("#node-input-finalize-editor").css("height", (height -105)+"px");
$("#node-input-init-editor").css("height", (height -45-48)+"px");
$("#node-input-func-editor").css("height", (height -45-48)+"px");
$("#node-input-finalize-editor").css("height", (height -45-48)+"px");
this.initEditor.resize();
this.editor.resize();
this.finalizeEditor.resize();
$("#node-input-libs-container").css("height", (height - 185)+"px");
}
});
})();
</script>

View File

@@ -16,6 +16,7 @@
module.exports = function(RED) {
"use strict";
var util = require("util");
var vm = require("vm");
@@ -92,8 +93,14 @@ module.exports = function(RED) {
var node = this;
node.name = n.name;
node.func = n.func;
node.outputs = n.outputs;
node.ini = n.initialize ? n.initialize.trim() : "";
node.fin = n.finalize ? n.finalize.trim() : "";
node.libs = n.libs || [];
if (RED.settings.functionExternalModules !== true && node.libs.length > 0) {
throw new Error(RED._("function.error.externalModuleNotAllowed"));
}
var handleNodeDoneCall = true;
@@ -105,23 +112,24 @@ module.exports = function(RED) {
}
var functionText = "var results = null;"+
"results = (async function(msg,__send__,__done__){ "+
"var __msgid__ = msg._msgid;"+
"var node = {"+
"id:__node__.id,"+
"name:__node__.name,"+
"log:__node__.log,"+
"error:__node__.error,"+
"warn:__node__.warn,"+
"debug:__node__.debug,"+
"trace:__node__.trace,"+
"on:__node__.on,"+
"status:__node__.status,"+
"send:function(msgs,cloneMsg){ __node__.send(__send__,__msgid__,msgs,cloneMsg);},"+
"done:__done__"+
"};\n"+
node.func+"\n"+
"})(msg,__send__,__done__);";
"results = (async function(msg,__send__,__done__){ "+
"var __msgid__ = msg._msgid;"+
"var node = {"+
"id:__node__.id,"+
"name:__node__.name,"+
"outputCount:__node__.outputCount,"+
"log:__node__.log,"+
"error:__node__.error,"+
"warn:__node__.warn,"+
"debug:__node__.debug,"+
"trace:__node__.trace,"+
"on:__node__.on,"+
"status:__node__.status,"+
"send:function(msgs,cloneMsg){ __node__.send(__send__,__msgid__,msgs,cloneMsg);},"+
"done:__done__"+
"};\n"+
node.func+"\n"+
"})(msg,__send__,__done__);";
var finScript = null;
var finOpt = null;
node.topic = n.topic;
@@ -140,6 +148,7 @@ module.exports = function(RED) {
__node__: {
id: node.id,
name: node.name,
outputCount: node.outputs,
log: function() {
node.log.apply(node, arguments);
},
@@ -266,34 +275,99 @@ module.exports = function(RED) {
};
sandbox.promisify = util.promisify;
}
if (node.hasOwnProperty("libs")) {
let moduleErrors = false;
var modules = node.libs;
modules.forEach(module => {
var vname = module.hasOwnProperty("var") ? module.var : null;
if (vname && (vname !== "")) {
if (sandbox.hasOwnProperty(vname) || vname === 'node') {
node.error(RED._("function.error.moduleNameError",{name:vname}))
moduleErrors = true;
return;
}
sandbox[vname] = null;
try {
var spec = module.module;
if (spec && (spec !== "")) {
var lib = RED.require(module.module);
sandbox[vname] = lib;
}
} catch (e) {
//TODO: NLS error message
node.error(RED._("function.error.moduleLoadError",{module:module.spec, error:e.toString()}))
moduleErrors = true;
}
}
});
if (moduleErrors) {
throw new Error(RED._("function.error.externalModuleLoadError"));
}
}
const RESOLVING = 0;
const RESOLVED = 1;
const ERROR = 2;
var state = RESOLVING;
var messages = [];
var processMessage = (() => {});
node.on("input", function(msg,send,done) {
if(state === RESOLVING) {
messages.push({msg:msg, send:send, done:done});
}
else if(state === RESOLVED) {
processMessage(msg, send, done);
}
});
var context = vm.createContext(sandbox);
try {
var iniScript = null;
var iniOpt = null;
if (node.ini && (node.ini !== "")) {
var iniText = `
(async function(__send__) {
var node = {
id:__node__.id,
name:__node__.name,
log:__node__.log,
error:__node__.error,
warn:__node__.warn,
debug:__node__.debug,
trace:__node__.trace,
status:__node__.status,
send: function(msgs, cloneMsg) {
__node__.send(__send__, RED.util.generateId(), msgs, cloneMsg);
}
};
`+ node.ini +`
})(__initSend__);`;
(async function(__send__) {
var node = {
id:__node__.id,
name:__node__.name,
outputCount:__node__.outputCount,
log:__node__.log,
error:__node__.error,
warn:__node__.warn,
debug:__node__.debug,
trace:__node__.trace,
status:__node__.status,
send: function(msgs, cloneMsg) {
__node__.send(__send__, RED.util.generateId(), msgs, cloneMsg);
}
};
`+ node.ini +`
})(__initSend__);`;
iniOpt = createVMOpt(node, " setup");
iniScript = new vm.Script(iniText, iniOpt);
}
node.script = vm.createScript(functionText, createVMOpt(node, ""));
if (node.fin && (node.fin !== "")) {
var finText = "(function () {\n"+node.fin +"\n})();";
var finText = `(function () {
var node = {
id:__node__.id,
name:__node__.name,
outputCount:__node__.outputCount,
log:__node__.log,
error:__node__.error,
warn:__node__.warn,
debug:__node__.debug,
trace:__node__.trace,
status:__node__.status,
send: function(msgs, cloneMsg) {
__node__.error("Cannot send from close function");
}
};
`+node.fin +`
})();`;
finOpt = createVMOpt(node, " cleanup");
finScript = new vm.Script(finText, finOpt);
}
@@ -303,7 +377,7 @@ module.exports = function(RED) {
promise = iniScript.runInContext(context, iniOpt);
}
function processMessage(msg, send, done) {
processMessage = function (msg, send, done) {
var start = process.hrtime();
context.msg = msg;
context.__send__ = send;
@@ -363,20 +437,6 @@ module.exports = function(RED) {
});
}
const RESOLVING = 0;
const RESOLVED = 1;
const ERROR = 2;
var state = RESOLVING;
var messages = [];
node.on("input", function(msg,send,done) {
if(state === RESOLVING) {
messages.push({msg:msg, send:send, done:done});
}
else if(state === RESOLVED) {
processMessage(msg, send, done);
}
});
node.on("close", function() {
if (finScript) {
try {
@@ -422,7 +482,11 @@ module.exports = function(RED) {
node.error(err);
}
}
RED.nodes.registerType("function",FunctionNode);
RED.nodes.registerType("function",FunctionNode, {
dynamicModuleList: "libs",
settings: {
functionExternalModules: { value: false, exportable: true }
}
});
RED.library.register("functions");
};

View File

@@ -72,7 +72,7 @@ module.exports = function(RED) {
return ((min <= index) && (index <= max));
},
'hask': function(a, b) {
return (typeof b !== "object" ) && a.hasOwnProperty(b+"");
return a !== undefined && a !== null && (typeof b !== "object" ) && a.hasOwnProperty(b+"");
},
'jsonata_exp': function(a, b) { return (b === true); },
'else': function(a) { return a === true; }
@@ -177,11 +177,17 @@ module.exports = function(RED) {
getV1(node,msg,rule,state.hasParts, (err,value) => {
if (err) {
// This only happens if v1 is an invalid JSONata expr
// But that will have already been logged and the node marked
// invalid as part of the constructor
return done(err);
}
v1 = value;
getV2(node,msg,rule, (err,value) => {
if (err) {
// This only happens if v1 is an invalid JSONata expr
// But that will have already been logged and the node marked
// invalid as part of the constructor
return done(err);
}
v2 = value;
@@ -189,16 +195,22 @@ module.exports = function(RED) {
property = state.elseflag;
state.elseflag = true;
}
if (operators[rule.t](property,v1,v2,rule.case,msg.parts)) {
state.onward.push(msg);
state.elseflag = false;
if (node.checkall == "false") {
return done(undefined,false);
try {
if (operators[rule.t](property,v1,v2,rule.case,msg.parts)) {
state.onward.push(msg);
state.elseflag = false;
if (node.checkall == "false") {
return done(undefined,false);
}
} else {
state.onward.push(null);
}
} else {
state.onward.push(null);
done(undefined, state.currentRule < node.rules.length - 1);
} catch(err) {
// An error occurred evaluating the rule - for example, an
// invalid RegExp value.
done(err);
}
done(undefined, state.currentRule < node.rules.length - 1);
});
});
}
@@ -437,7 +449,7 @@ module.exports = function(RED) {
} else {
applyRules(node,msg,property,undefined,(err,onward) => {
if (err) {
node.warn(err);
node.error(err, msg);
} else {
if (!repair || !hasParts) {
node.send(onward);

View File

@@ -18,12 +18,44 @@
</script>
<script type="text/javascript">
(function() {
function validateProperty(v,vt) {
if (/msg|flow|global/.test(vt)) {
if (!RED.utils.validatePropertyExpression(v)) {
return false;
}
} else if (vt === "jsonata") {
try{jsonata(v);}catch(e){return false;}
} else if (vt === "json") {
try{JSON.parse(v);}catch(e){return false;}
}
return true;
}
RED.nodes.registerType('change', {
color: "#E2D96E",
category: 'function',
defaults: {
name: {value:""},
rules:{value:[{t:"set",p:"payload",pt:"msg",to:"",tot:"str"}]},
rules:{value:[{t:"set",p:"payload",pt:"msg",to:"",tot:"str"}],validate: function(rules) {
if (!rules || rules.length === 0) { return true }
for (var i=0;i<rules.length;i++) {
var r = rules[i];
if (r.t === 'set') {
if (!validateProperty(r.p,r.pt) || !validateProperty(r.to,r.tot)) {
return false;
}
} else if (r.t === 'change') {
if (!validateProperty(r.p,r.pt) || !validateProperty(r.from,r.fromt) || !validateProperty(r.to,r.tot)) {
return false;
}
} else if (r.t === 'move') {
if (!validateProperty(r.p,r.pt)) {
return false;
}
}
}
return true;
}},
// legacy
action: {value:""},
property: {value:""},
@@ -323,4 +355,5 @@
$("#node-input-rule-container").editableList('height',height);
}
});
})();
</script>

View File

@@ -168,6 +168,10 @@ module.exports = function(RED) {
return getFromValueType(RED.util.getMessageProperty(msg,rule.from),done);
} else if (rule.fromt === 'flow' || rule.fromt === 'global') {
var contextKey = RED.util.parseContextStore(rule.from);
if (/\[msg\./.test(context.key)) {
// The key has a nest msg. reference to evaluate first
context.key = RED.util.normalisePropertyExpression(contextKey.key,msg,true);
}
node.context()[rule.fromt].get(contextKey.key, contextKey.store, (err,fromValue) => {
if (err) {
done(err)
@@ -243,6 +247,10 @@ module.exports = function(RED) {
return done(undefined,msg);
} else if (rule.pt === 'flow' || rule.pt === 'global') {
var contextKey = RED.util.parseContextStore(property);
if (/\[msg/.test(contextKey.key)) {
// The key has a nest msg. reference to evaluate first
contextKey.key = RED.util.normalisePropertyExpression(contextKey.key, msg, true)
}
var target = node.context()[rule.pt];
var callback = err => {
if (err) {

View File

@@ -45,9 +45,9 @@
</div>
<div id="random-details" class="form-row">
<label for="node-input-randomFirst"><i class="fa fa-clock-o"></i> <span data-i18n="delay.between"></span></label>
<input type="text" id="node-input-randomFirst" placeholder="" style="text-align:end; width:30px !important">
<span data-i18n="delay.and"></span>
<input type="text" id="node-input-randomLast" placeholder="" style="text-align:end; width:30px !important">
<input type="text" id="node-input-randomFirst" placeholder="" style="text-align:end; width:50px !important">
&nbsp;<span data-i18n="delay.and"></span>&nbsp;
<input type="text" id="node-input-randomLast" placeholder="" style="text-align:end; width:50px !important">
<select id="node-input-randomUnits" style="width:140px !important">
<option value="milliseconds" data-i18n="delay.milisecs"></option>
<option value="seconds" data-i18n="delay.secs"></option>

View File

@@ -80,10 +80,10 @@ module.exports = function(RED) {
this.drop = n.drop;
var node = this;
function ourTimeout(handler, delay) {
function ourTimeout(handler, delay, clearHandler) {
var toutID = setTimeout(handler, delay);
return {
clear: function() { clearTimeout(toutID); },
clear: function() { clearTimeout(toutID); clearHandler(); },
trigger: function() { clearTimeout(toutID); return handler(); }
};
}
@@ -113,14 +113,15 @@ module.exports = function(RED) {
}
if (node.pauseType === "delay") {
node.on("input", function(msg) {
if (msg.hasOwnProperty("flush")) { flushDelayList(); }
node.on("input", function(msg, send, done) {
if (msg.hasOwnProperty("flush")) { flushDelayList(); done(); }
else {
var id = ourTimeout(function() {
node.idList.splice(node.idList.indexOf(id),1);
if (node.idList.length === 0) { node.status({}); }
node.send(msg);
}, node.timeout);
send(msg);
done();
}, node.timeout, () => done());
node.idList.push(id);
if ((node.timeout > 1000) && (node.idList.length !== 0)) {
node.status({fill:"blue",shape:"dot",text:" "});
@@ -131,7 +132,7 @@ module.exports = function(RED) {
node.on("close", function() { clearDelayList(); });
}
else if (node.pauseType === "delayv") {
node.on("input", function(msg) {
node.on("input", function(msg, send, done) {
var delayvar = Number(node.timeout);
if (msg.hasOwnProperty("delay") && !isNaN(parseFloat(msg.delay))) {
delayvar = parseFloat(msg.delay);
@@ -140,8 +141,9 @@ module.exports = function(RED) {
var id = ourTimeout(function() {
node.idList.splice(node.idList.indexOf(id),1);
if (node.idList.length === 0) { node.status({}); }
node.send(msg);
}, delayvar);
send(msg);
done();
}, delayvar, () => done());
node.idList.push(id);
if ((delayvar >= 0) && (node.idList.length !== 0)) {
node.status({fill:"blue",shape:"dot",text:delayvar/1000+"s"});
@@ -152,7 +154,7 @@ module.exports = function(RED) {
node.on("close", function() { clearDelayList(); });
}
else if (node.pauseType === "rate") {
node.on("input", function(msg) {
node.on("input", function(msg, send, done) {
if (msg.hasOwnProperty("reset")) {
if (node.intervalID !== -1 ) {
clearInterval(node.intervalID);
@@ -161,17 +163,18 @@ module.exports = function(RED) {
delete node.lastSent;
node.buffer = [];
node.status({text:"reset"});
done();
return;
}
if (!node.drop) {
var m = RED.util.cloneMessage(msg);
delete m.flush;
if (node.intervalID !== -1) {
node.buffer.push(m);
node.buffer.push({msg: m, send: send, done: done});
node.reportDepth();
}
else {
node.send(m);
send(m);
node.reportDepth();
node.intervalID = setInterval(function() {
if (node.buffer.length === 0) {
@@ -179,16 +182,22 @@ module.exports = function(RED) {
node.intervalID = -1;
}
if (node.buffer.length > 0) {
node.send(node.buffer.shift());
const msgInfo = node.buffer.shift();
msgInfo.send(msgInfo.msg);
msgInfo.done();
}
node.reportDepth();
}, node.rate);
done();
}
if (msg.hasOwnProperty("flush")) {
while (node.buffer.length > 0) {
node.send(node.buffer.shift());
const msgInfo = node.buffer.shift();
msgInfo.send(msgInfo.msg);
msgInfo.done();
}
node.status({});
done();
}
}
else {
@@ -198,17 +207,19 @@ module.exports = function(RED) {
}
if (!node.lastSent) { // ensuring that we always send the first message
node.lastSent = process.hrtime();
node.send(msg);
send(msg);
}
else if ( ( (timeSinceLast[0] * SECONDS_TO_NANOS) + timeSinceLast[1] ) > (node.rate * MILLIS_TO_NANOS) ) {
node.lastSent = process.hrtime();
node.send(msg);
send(msg);
}
done();
}
});
node.on("close", function() {
clearInterval(node.intervalID);
clearTimeout(node.busy);
node.buffer.forEach((msgInfo) => msgInfo.done());
node.buffer = [];
node.status({});
});
@@ -217,57 +228,75 @@ module.exports = function(RED) {
node.intervalID = setInterval(function() {
if (node.pauseType === "queue") {
if (node.buffer.length > 0) {
node.send(node.buffer.shift()); // send the first on the queue
const msgInfo = node.buffer.shift();
msgInfo.send(msgInfo.msg); // send the first on the queue
msgInfo.done();
}
}
else {
while (node.buffer.length > 0) { // send the whole queue
node.send(node.buffer.shift());
const msgInfo = node.buffer.shift();
msgInfo.send(msgInfo.msg);
msgInfo.done();
}
}
node.reportDepth();
},node.rate);
var hit;
node.on("input", function(msg) {
node.on("input", function(msg, send, done) {
if (!msg.hasOwnProperty("topic")) { msg.topic = "_none_"; }
hit = false;
for (var b in node.buffer) { // check if already in queue
if (msg.topic === node.buffer[b].topic) {
node.buffer[b] = msg; // if so - replace existing entry
if (msg.topic === node.buffer[b].msg.topic) {
node.buffer[b].done();
node.buffer[b] = {msg, send, done}; // if so - replace existing entry
hit = true;
break;
}
}
if (!hit) {
node.buffer.push(msg); // if not add to end of queue
node.buffer.push({msg, send, done}); // if not add to end of queue
node.reportDepth();
}
if (msg.hasOwnProperty("reset")) {
while (node.buffer.length > 0) {
const msgInfo = node.buffer.shift();
msgInfo.done();
}
node.buffer = [];
node.status({text:"reset"});
done();
}
if (msg.hasOwnProperty("flush")) {
while (node.buffer.length > 0) {
node.send(node.buffer.shift());
const msgInfo = node.buffer.shift();
msgInfo.send(msgInfo.msg);
msgInfo.done();
}
node.status({});
done();
}
});
node.on("close", function() {
clearInterval(node.intervalID);
while (node.buffer.length > 0) {
const msgInfo = node.buffer.shift();
msgInfo.done();
}
node.buffer = [];
node.status({});
});
}
else if (node.pauseType === "random") {
node.on("input", function(msg) {
node.on("input", function(msg, send, done) {
var wait = node.randomFirst + (node.diff * Math.random());
var id = ourTimeout(function() {
node.idList.splice(node.idList.indexOf(id),1);
node.send(msg);
send(msg);
node.status({});
}, wait);
done();
}, wait, () => done());
node.idList.push(id);
if ((node.timeout >= 1000) && (node.idList.length !== 0)) {
node.status({fill:"blue",shape:"dot",text:parseInt(wait/10)/100+"s"});

View File

@@ -82,10 +82,10 @@ module.exports = function(RED) {
var npay = {};
var pendingMessages = [];
var activeMessagePromise = null;
var processMessageQueue = function(msg) {
if (msg) {
var processMessageQueue = function(msgInfo) {
if (msgInfo) {
// A new message has arrived - add it to the message queue
pendingMessages.push(msg);
pendingMessages.push(msgInfo);
if (activeMessagePromise !== null) {
// The node is currently processing a message, so do nothing
// more with this message
@@ -101,17 +101,17 @@ module.exports = function(RED) {
// There are more messages to process. Get the next message and
// start processing it. Recurse back in to check for any more
var nextMsg = pendingMessages.shift();
activeMessagePromise = processMessage(nextMsg)
var nextMsgInfo = pendingMessages.shift();
activeMessagePromise = processMessage(nextMsgInfo)
.then(processMessageQueue)
.catch((err) => {
node.error(err,nextMsg);
nextMsgInfo.done(err);
return processMessageQueue();
});
}
this.on('input', function(msg) {
processMessageQueue(msg);
this.on('input', function(msg, send, done) {
processMessageQueue({msg, send, done});
});
var stat = function() {
@@ -121,7 +121,8 @@ module.exports = function(RED) {
else return {fill:"blue",shape:"dot",text:l};
}
var processMessage = function(msg) {
var processMessage = function(msgInfo) {
let msg = msgInfo.msg;
var topic = RED.util.getMessageProperty(msg,node.topic) || "_none";
var promise;
var delayDuration = node.duration;
@@ -181,7 +182,7 @@ module.exports = function(RED) {
var msg2 = RED.util.cloneMessage(msg);
node.topics[topic].tout = setInterval(function() {
if (node.op1type === "date") { msg2.payload = Date.now(); }
node.send(RED.util.cloneMessage(msg2));
msgInfo.send(RED.util.cloneMessage(msg2));
}, delayDuration);
}
}
@@ -206,15 +207,15 @@ module.exports = function(RED) {
}
promise.then(() => {
if (node.op2type === "payl") {
if (node.second === true) { node.send([null,npay[topic]]); }
else { node.send(npay[topic]); }
if (node.second === true) { msgInfo.send([null,npay[topic]]); }
else { msgInfo.send(npay[topic]); }
delete npay[topic];
}
else {
msg2.payload = node.topics[topic].m2;
if (node.op2type === "date") { msg2.payload = Date.now(); }
if (node.second === true) { node.send([null,msg2]); }
else { node.send(msg2); }
if (node.second === true) { msgInfo.send([null,msg2]); }
else { msgInfo.send(msg2); }
}
delete node.topics[topic];
node.status(stat());
@@ -229,8 +230,9 @@ module.exports = function(RED) {
}, delayDuration);
}
}
msgInfo.done();
node.status(stat());
if (node.op1type !== "nul") { node.send(RED.util.cloneMessage(msg)); }
if (node.op1type !== "nul") { msgInfo.send(RED.util.cloneMessage(msg)); }
});
});
}
@@ -266,8 +268,8 @@ module.exports = function(RED) {
}
delete node.topics[topic];
node.status(stat());
if (node.second === true) { node.send([null,msg2]); }
else { node.send(msg2); }
if (node.second === true) { msgInfo.send([null,msg2]); }
else { msgInfo.send(msg2); }
}).catch(err => {
node.error(err);
});
@@ -277,6 +279,7 @@ module.exports = function(RED) {
// if (node.op2type === "payl") {node.topics[topic].m2 = RED.util.cloneMessage(msg.payload); }
// }
}
msgInfo.done();
return Promise.resolve();
}
this.on("close", function() {

View File

@@ -21,8 +21,8 @@
</div>
<div class="form-row">
<label><i class="fa fa-plus"></i> <span data-i18n="exec.label.append"></span></label>
<input type="checkbox" id="node-input-addpay" style="display:inline-block; width:auto; vertical-align:top;">
&nbsp;msg.payload
<input type="checkbox" id="node-input-addpay-cb" style="display:inline-block; width:auto;">
<input type="text" id="node-input-addpay" style="margin-left: 5px; width:160px;">
</div>
<div class="form-row">
<label for="node-input-append"> </label>
@@ -52,7 +52,7 @@
color:"darksalmon",
defaults: {
command: {value:""},
addpay: {value:true},
addpay: {value:""},
append: {value:""},
useSpawn: {value:"false"},
timer: {value:""},
@@ -79,6 +79,24 @@
if ($("#node-input-useSpawn").val() === null) {
$("#node-input-useSpawn").val(this.useSpawn.toString());
}
$("#node-input-addpay-cb").prop("checked", this.addpay === true || (this.addpay !== false && this.addpay !== ""))
var addpayValue = (this.addpay === true)?"payload":((this.addpay === false || this.addpay === "")?"payload":this.addpay);
$("#node-input-addpay-cb").on("change", function(evt) {
$("#node-input-addpay").typedInput("disable",!$("#node-input-addpay-cb").prop("checked"));
});
$("#node-input-addpay").val(addpayValue);
$("#node-input-addpay").typedInput({
default: "msg",
types: ["msg"]
});
$("#node-input-addpay-cb").trigger("change")
},
oneditsave: function() {
if (!$("#node-input-addpay-cb").prop("checked")) {
$("#node-input-addpay").val("");
}
}
});
</script>

View File

@@ -26,17 +26,20 @@ module.exports = function(RED) {
this.cmd = (n.command || "").trim();
if (n.addpay === undefined) { n.addpay = true; }
this.addpay = n.addpay;
if (this.addpay === true) {
this.addpay = "payload";
}
this.append = (n.append || "").trim();
this.useSpawn = (n.useSpawn == "true");
this.timer = Number(n.timer || 0)*1000;
this.activeProcesses = {};
this.oldrc = (n.oldrc || false).toString();
this.execOpt = {encoding:'binary', maxBuffer:10000000};
this.execOpt = {encoding:'binary', maxBuffer:RED.settings.execMaxBufferSize||10000000};
var node = this;
if (process.platform === 'linux' && fs.existsSync('/bin/bash')) { node.execOpt.shell = '/bin/bash'; }
var cleanup = function(p) {
if (process.platform === 'linux' && fs.existsSync('/bin/bash')) { node.execOpt.shell = '/bin/bash'; }
var cleanup = function(p) {
node.activeProcesses[p].kill();
//node.status({fill:"red",shape:"dot",text:"timeout"});
//node.error("Exec node timeout");
@@ -61,12 +64,17 @@ module.exports = function(RED) {
}
else {
var child;
// make the extra args into an array
// then prepend with the msg.payload
var arg = node.cmd;
if (node.addpay) {
var value = RED.util.getMessageProperty(msg, node.addpay);
if (value !== undefined) {
arg += " " + value;
}
}
if (node.append.trim() !== "") { arg += " " + node.append; }
if (this.useSpawn === true) {
// make the extra args into an array
// then prepend with the msg.payload
var arg = node.cmd;
if ((node.addpay === true) && msg.hasOwnProperty("payload")) { arg += " "+msg.payload; }
if (node.append.trim() !== "") { arg += " "+node.append; }
// slice whole line by spaces and removes any quotes since spawn can't handle them
arg = arg.match(/(?:[^\s"]+|"[^"]*")+/g).map((a) => {
if (/^".*"$/.test(a)) {
@@ -126,12 +134,9 @@ module.exports = function(RED) {
});
}
else {
var cl = node.cmd;
if ((node.addpay === true) && msg.hasOwnProperty("payload")) { cl += " "+msg.payload; }
if (node.append.trim() !== "") { cl += " "+node.append; }
/* istanbul ignore else */
if (RED.settings.verbose) { node.log(cl); }
child = exec(cl, node.execOpt, function (error, stdout, stderr) {
if (RED.settings.verbose) { node.log(arg); }
child = exec(arg, node.execOpt, function (error, stdout, stderr) {
var msg2, msg3;
delete msg.payload;
if (stderr) {

View File

@@ -10,6 +10,51 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
<style>
.mqtt-form-row-cols2 > input.mqtt-form-row-col1 {
width: calc(35% - 75px);
}
.mqtt-form-row-cols2 > select.mqtt-form-row-col1 {
width: calc(35% - 75px);
}
.mqtt-form-row-cols2 > label.mqtt-form-row-col2 {
width: 100px;
margin-left: 42px;
display: inline-block;
}
.mqtt-form-row-cols2 > input.mqtt-form-row-col2 {
width: calc(35% - 75px);
display: inline-block;
}
.mqtt-form-row-cols2 > select.mqtt-form-row-col2 {
width: calc(35% - 75px);
display: inline-block;
}
.form-row.mqtt5-out > label {
width: 130px;
}
.form-row.mqtt-flags-row > label {
vertical-align: top;
}
.form-row.mqtt-flags-row > .mqtt-flags {
display: inline-block;
width: 70%
}
.form-row.mqtt-flags-row > .mqtt-flags > .mqtt-flag > label {
display: block;
width: 100%;
}
.form-row.mqtt-flags-row > .mqtt-flags > .mqtt-flag > label > input {
position: relative;
vertical-align: bottom;
top: -2px;
width: 15px;
height: 15px;
}
</style>
<script type="text/html" data-template-name="mqtt in">
<div class="form-row">
@@ -38,43 +83,37 @@
<option value="base64" data-i18n="mqtt.output.base64"></option>
</select>
</div>
<div class="form-row mqtt-flags-row mqtt5">
<label for="node-input-nl" ><i class="fa fa-flag"></i> <span data-i18n="mqtt.label.flags">Flags</span></label>
<div class="mqtt-flags">
<div class="mqtt-flag">
<label for="node-input-nl">
<input type="checkbox" id="node-input-nl">
<span data-i18n="mqtt.label.nl"></span>
</label>
</div>
<div class="mqtt-flag">
<label for="node-input-rap">
<input type="checkbox" id="node-input-rap">
<span data-i18n="mqtt.label.rap"></span>
</label>
</div>
</div>
</div>
<div class="form-row mqtt5">
<label for="node-input-rh" style="width:100%"><i class="fa fa-tag"></i> <span data-i18n="mqtt.label.rh"></span></label>
<select id="node-input-rh" style="margin-left: 104px; width: 70%">
<option value="0" data-i18n="mqtt.label.rh0"></option>
<option value="1" data-i18n="mqtt.label.rh1"></option>
<option value="2" data-i18n="mqtt.label.rh2"></option>
</select>
</div>
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="common.label.name"></span></label>
<input type="text" id="node-input-name" data-i18n="[placeholder]common.label.name">
</div>
</script>
<script type="text/javascript">
RED.nodes.registerType('mqtt in',{
category: 'network',
defaults: {
name: {value:""},
topic: {value:"",required:true,validate: RED.validators.regex(/^(#$|(\+|[^+#]*)(\/(\+|[^+#]*))*(\/(\+|#|[^+#]*))?$)/)},
qos: {value: "2"},
datatype: {value:"auto",required:true},
broker: {type:"mqtt-broker", required:true}
},
color:"#d8bfd8",
inputs:0,
outputs:1,
icon: "bridge.svg",
label: function() {
return this.name||this.topic||"mqtt";
},
labelStyle: function() {
return this.name?"node_label_italic":"";
},
oneditprepare: function() {
if (this.qos === undefined) {
$("#node-input-qos").val("2");
}
if (this.datatype === undefined) {
$("#node-input-datatype").val("auto");
}
}
});
</script>
<script type="text/html" data-template-name="mqtt out">
<div class="form-row">
<label for="node-input-broker"><i class="fa fa-globe"></i> <span data-i18n="mqtt.label.broker"></span></label>
@@ -84,20 +123,45 @@
<label for="node-input-topic"><i class="fa fa-tasks"></i> <span data-i18n="common.label.topic"></span></label>
<input type="text" id="node-input-topic" data-i18n="[placeholder]common.label.topic">
</div>
<div class="form-row">
<label for="node-input-qos"><i class="fa fa-empire"></i> <span data-i18n="mqtt.label.qos"></span></label>
<select id="node-input-qos" style="width:125px !important">
<div class="form-row mqtt-form-row-cols2">
<label for="node-input-qos" class="mqtt-form-row-col1"><i class="fa fa-empire"></i> <span data-i18n="mqtt.label.qos"></span></label>
<select id="node-input-qos" class="mqtt-form-row-col1">
<option value=""></option>
<option value="0">0</option>
<option value="1">1</option>
<option value="2">2</option>
</select>
&nbsp;&nbsp;<i class="fa fa-history"></i>&nbsp;<span data-i18n="mqtt.retain"></span> &nbsp;<select id="node-input-retain" style="width:125px !important">
<label for="node-input-retain" class="mqtt-form-row-col2"><i class="fa fa-history"></i> <span data-i18n="mqtt.retain"></span></label>
<select id="node-input-retain" class="mqtt-form-row-col2" >
<option value=""></option>
<option value="false" data-i18n="mqtt.false"></option>
<option value="true" data-i18n="mqtt.true"></option>
</select>
</div>
<div class="form-row mqtt5 mqtt5-out">
<label for="node-input-userProps"><span data-i18n="mqtt.label.userProperties"></span></label>
<input type="text" id="node-input-userProps" style="width: calc(100% - 166px);">
</div>
<div class="form-row mqtt5 mqtt5-out">
<label for="node-input-respTopic"><span data-i18n="mqtt.label.responseTopic"></span></label>
<input type="text" id="node-input-respTopic" style="width: calc(100% - 166px);">
</div>
<div class="form-row mqtt5 mqtt5-out">
<label for="node-input-correl"><span data-i18n="mqtt.label.correlationData"></span></label>
<input type="text" id="node-input-correl" style="width: calc(100% - 166px);">
</div>
<div class="form-row mqtt5 mqtt5-out">
<label for="node-input-contentType"><span data-i18n="mqtt.label.contentType"></span></label>
<input type="text" id="node-input-contentType" style="width: calc(100% - 166px);">
</div>
<div class="form-row mqtt-form-row-cols2 mqtt5 mqtt5-out">
<label for="node-input-expiry" class="mqtt-form-row-col1"><span data-i18n="mqtt.label.expiry"></span></label>
<input id="node-input-expiry" style="width: calc(100% - 166px);" class="mqtt-form-row-col1" >
</div>
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="common.label.name"></span></label>
<input type="text" id="node-input-name" data-i18n="[placeholder]common.label.name">
@@ -105,30 +169,6 @@
<div class="form-tips"><span data-i18n="mqtt.tip"></span></div>
</script>
<script type="text/javascript">
RED.nodes.registerType('mqtt out',{
category: 'network',
defaults: {
name: {value:""},
topic: {value:""},
qos: {value:""},
retain: {value:""},
broker: {type:"mqtt-broker", required:true}
},
color:"#d8bfd8",
inputs:1,
outputs:0,
icon: "bridge.svg",
align: "right",
label: function() {
return this.name||this.topic||"mqtt";
},
labelStyle: function() {
return this.name?"node_label_italic":"";
}
});
</script>
<script type="text/html" data-template-name="mqtt-broker">
<div class="form-row">
<label for="node-config-input-name"><i class="fa fa-tag"></i> <span data-i18n="common.label.name"></span></label>
@@ -141,31 +181,51 @@
<div id="mqtt-broker-tab-connection" style="display:none">
<div class="form-row node-input-broker">
<label for="node-config-input-broker"><i class="fa fa-globe"></i> <span data-i18n="mqtt.label.broker"></span></label>
<input type="text" id="node-config-input-broker" style="width:40%;" data-i18n="[placeholder]mqtt.label.example">
<input type="text" id="node-config-input-broker" style="width: calc(100% - 300px);" data-i18n="[placeholder]mqtt.label.example">
<label for="node-config-input-port" style="margin-left:20px; width:43px; "> <span data-i18n="mqtt.label.port"></span></label>
<input type="text" id="node-config-input-port" data-i18n="[placeholder]mqtt.label.port" style="width:55px">
</div>
<div class="form-row" style="height: 34px;">
<input type="checkbox" id="node-config-input-usetls" style="height: 34px; margin: 0 5px 0 104px; display: inline-block; width: auto; vertical-align: top;">
<label for="node-config-input-usetls" style="width: 100px; line-height: 34px;"><span data-i18n="mqtt.label.use-tls"></span></label>
<span id="node-config-row-tls" class="hide"><input style="width: 320px;" type="text" id="node-config-input-tls"></span>
</div>
<div class="form-row">
<input type="checkbox" id="node-config-input-usetls" style="display: inline-block; width: auto; vertical-align: top;">
<label for="node-config-input-usetls" style="width: auto" data-i18n="mqtt.label.use-tls"></label>
<div id="node-config-row-tls" class="hide">
<label style="width: auto; margin-left: 20px; margin-right: 10px;" for="node-config-input-tls"><span data-i18n="mqtt.label.tls-config"></span></label><input style="width: 300px;" type="text" id="node-config-input-tls">
</div>
<label for="node-config-input-protocolVersion"><i class="fa fa-cog"></i> <span data-i18n="mqtt.label.protocolVersion"></span></label>
<select id="node-config-input-protocolVersion" style="width:70%;">
<option value="3" data-i18n="mqtt.label.protocolVersion3"></option>
<option value="4" data-i18n="mqtt.label.protocolVersion4"></option>
<option value="5" data-i18n="mqtt.label.protocolVersion5"></option>
</select>
</div>
<div class="form-row">
<label for="node-config-input-clientid"><i class="fa fa-tag"></i> <span data-i18n="mqtt.label.clientid"></span></label>
<input type="text" id="node-config-input-clientid" data-i18n="[placeholder]mqtt.placeholder.clientid">
</div>
<div class="form-row">
<label for="node-config-input-keepalive" style="width: auto"><i class="fa fa-clock-o"></i> <span data-i18n="mqtt.label.keepalive"></span></label>
<input type="text" id="node-config-input-keepalive" style="width: 50px">
<input type="checkbox" id="node-config-input-cleansession" style="margin-left: 30px; height: 1em;display: inline-block; width: auto; vertical-align: middle;">
<label for="node-config-input-cleansession" style="width: auto;" data-i18n="mqtt.label.cleansession"></label>
<label for="node-config-input-keepalive"><i class="fa fa-heartbeat"></i> <span data-i18n="mqtt.label.keepalive"></span></label>
<input type="number" min="0" id="node-config-input-keepalive" style="width: 100px">
</div>
<div class="form-row">
<input type="checkbox" id="node-config-input-compatmode" style="display: inline-block; width: auto; vertical-align: top;">
<label for="node-config-input-compatmode" style="width: auto;" data-i18n="mqtt.label.compatmode"></label>
<div class="form-row" style="margin-bottom:0">
<label style="vertical-align:top;"><i class="fa fa-info"></i> <span data-i18n="mqtt.label.session"></span></label>
<div style="display: inline-block; width:calc(100% - 110px)">
<div class="form-row">
<label for="node-config-input-cleansession" style="width: auto;">
<input type="checkbox" id="node-config-input-cleansession" style="position: relative;vertical-align: bottom; top: -2px; width: 15px;height: 15px;">
<span id="node-config-input-cleansession-label" data-i18n="mqtt.label.cleansession"></span>
</label>
</div>
<div class="form-row mqtt5">
<label style="width:auto" for="node-config-input-sessionExpiry"><span data-i18n="mqtt.label.sessionExpiry"></span></label>
<input type="number" min="0" id="node-config-input-sessionExpiry" style="width: 100px" >
</div>
</div>
</div>
<div class="form-row mqtt5">
<label style="width: 125px;" for="node-config-input-userProps"><span data-i18n="mqtt.label.userProperties"></span></label>
<input type="text" id="node-config-input-userProps" style="width: calc(100% - 166px);">
</div>
<br>
</div>
<div id="mqtt-broker-tab-security" style="display:none">
<div class="form-row">
@@ -202,6 +262,26 @@
<option value="2">2</option>
</select>
</div>
<div class="form-row mqtt5 mqtt5-out">
<label for="node-config-input-birth-contentType" data-i18n="mqtt.label.contentType"></label>
<input type="text" style="width:calc(100% - 200px);" id="node-config-input-birth-contentType">
</div>
<div class="form-row mqtt5 mqtt5-out">
<label for="node-config-input-birth-props" data-i18n="mqtt.label.userProperties"></label>
<input type="text" style="width:calc(100% - 200px);" id="node-config-input-birth-props">
</div>
<div class="form-row mqtt5 mqtt5-out">
<label for="node-config-input-birth-respTopic"><span data-i18n="mqtt.label.responseTopic"></span></label>
<input type="text" id="node-config-input-birth-respTopic" style="width: calc(100% - 200px);">
</div>
<div class="form-row mqtt5 mqtt5-out">
<label for="node-config-input-birth-correl"><span data-i18n="mqtt.label.correlationData"></span></label>
<input type="text" id="node-config-input-birth-correl" style="width: calc(100% - 200px);">
</div>
<div class="form-row mqtt5 mqtt5-out">
<label for="node-config-input-birth-expiry"><span data-i18n="mqtt.label.expiry"></span></label>
<input id="node-config-input-birth-expiry" style="width: calc(100% - 200px);">
</div>
</div>
</div>
<div id="mqtt-broker-section-close">
@@ -228,6 +308,26 @@
<option value="2">2</option>
</select>
</div>
<div class="form-row mqtt5 mqtt5-out">
<label for="node-config-input-close-contentType" data-i18n="mqtt.label.contentType"></label>
<input type="text" style="width:calc(100% - 200px);" id="node-config-input-close-contentType">
</div>
<div class="form-row mqtt5 mqtt5-out">
<label for="node-config-input-close-props" data-i18n="mqtt.label.userProperties"></label>
<input type="text" style="width:calc(100% - 200px);" id="node-config-input-close-props">
</div>
<div class="form-row mqtt5 mqtt5-out">
<label for="node-config-input-close-respTopic"><span data-i18n="mqtt.label.responseTopic"></span></label>
<input type="text" id="node-config-input-close-respTopic" style="width: calc(100% - 200px);">
</div>
<div class="form-row mqtt5 mqtt5-out">
<label for="node-config-input-close-correl"><span data-i18n="mqtt.label.correlationData"></span></label>
<input type="text" id="node-config-input-close-correl" style="width: calc(100% - 200px);">
</div>
<div class="form-row mqtt5 mqtt5-out">
<label for="node-config-input-close-expiry"><span data-i18n="mqtt.label.expiry"></span></label>
<input id="node-config-input-close-expiry" style="width: calc(100% - 200px);">
</div>
</div>
</div>
<div id="mqtt-broker-section-will">
@@ -254,6 +354,30 @@
<option value="2">2</option>
</select>
</div>
<div class="form-row mqtt5">
<label><span data-i18n="mqtt.label.delay"></span></label>
<input type="number" min="0" id="node-config-input-will-delay" style="width: 100px" >
</div>
<div class="form-row mqtt5 mqtt5-out">
<label for="node-config-input-will-contentType" data-i18n="mqtt.label.contentType"></label>
<input type="text" style="width:calc(100% - 200px);" id="node-config-input-will-contentType">
</div>
<div class="form-row mqtt5 mqtt5-out">
<label for="node-config-input-will-props" data-i18n="mqtt.label.userProperties"></label>
<input type="text" style="width:calc(100% - 200px);" id="node-config-input-will-props">
</div>
<div class="form-row mqtt5 mqtt5-out">
<label for="node-config-input-will-respTopic"><span data-i18n="mqtt.label.responseTopic"></span></label>
<input type="text" id="node-config-input-will-respTopic" style="width: calc(100% - 200px);">
</div>
<div class="form-row mqtt5 mqtt5-out">
<label for="node-config-input-will-correl"><span data-i18n="mqtt.label.correlationData"></span></label>
<input type="text" id="node-config-input-will-correl" style="width: calc(100% - 200px);">
</div>
<div class="form-row mqtt5 mqtt5-out">
<label for="node-config-input-will-expiry"><span data-i18n="mqtt.label.expiry"></span></label>
<input id="node-config-input-will-expiry" style="width: calc(100% - 200px);">
</div>
</div>
</div>
</div>
@@ -261,6 +385,40 @@
</script>
<script type="text/javascript">
(function() {
var typedInputNoneOpt = { value: 'none', label: '', hasValue: false };
var makeTypedInputOpt = function(value){
return {
value: value,
label:value,
hasValue: false
}
}
var contentTypeOpts = [
typedInputNoneOpt,
makeTypedInputOpt("application/json"),
makeTypedInputOpt("application/octet-stream"),
makeTypedInputOpt("text/csv"),
makeTypedInputOpt("text/html"),
makeTypedInputOpt("text/plain"),
{value:"other", label:""}
];
function getDefaultContentType(value) {
var defaultContentType;
var matchedContentType = contentTypeOpts.filter(function(v) {
return v.value === value;
})
if (matchedContentType.length > 0) {
defaultContentType = matchedContentType[0].value;
}
if (value && !defaultContentType) {
defaultContentType = 'other';
}
return defaultContentType || 'none'
}
RED.nodes.registerType('mqtt-broker',{
category: 'config',
defaults: {
@@ -279,20 +437,25 @@
usetls: {value: false},
verifyservercert: { value: false},
compatmode: { value: false},
protocolVersion: { value: 4},
keepalive: {value:60,validate:RED.validators.number()},
cleansession: {value: true},
birthTopic: {value:""},
birthQos: {value:"0"},
birthRetain: {value:false},
birthPayload: {value:""},
birthMsg: { value: {}},
closeTopic: {value:""},
closeQos: {value:"0"},
closeRetain: {value:false},
closePayload: {value:""},
closeMsg: { value: {}},
willTopic: {value:""},
willQos: {value:"0"},
willRetain: {value:false},
willPayload: {value:""}
willPayload: {value:""},
willMsg: { value: {}},
sessionExpiry: {value:0}
},
credentials: {
user: {type:"text"},
@@ -334,8 +497,8 @@
label: this._("mqtt.tabs-label.messages")
});
function setUpSection(sectionId, isExpanded) {
var birthMessageSection = $(sectionId);
function setUpSection(sectionId, v5Opts, isExpanded) {
var birthMessageSection = $("#mqtt-broker-section-"+sectionId);
var paletteHeader = birthMessageSection.find('.red-ui-palette-header');
var twistie = paletteHeader.find('i');
var sectionContent = birthMessageSection.find('.section-content');
@@ -350,6 +513,27 @@
toggleSection(!isExpanded);
});
toggleSection(isExpanded);
$("#node-config-input-"+sectionId+"-contentType").val(v5Opts?v5Opts.contentType:"").typedInput({
default: getDefaultContentType(v5Opts?v5Opts.contentType:""),
types: contentTypeOpts,
});
$("#node-config-input-"+sectionId+"-props").val(v5Opts?v5Opts.userProps:"").typedInput({
default: !(v5Opts?v5Opts.userProps:null)? 'none':'json',
types: [typedInputNoneOpt, 'json'],
});
$("#node-config-input-"+sectionId+"-respTopic").val(v5Opts?v5Opts.respTopic:"").typedInput({
default: !(v5Opts?v5Opts.respTopic:null)? 'none':'str',
types: [typedInputNoneOpt, 'str'],
});
$("#node-config-input-"+sectionId+"-correl").val(v5Opts?v5Opts.correl:"").typedInput({
default: !(v5Opts?v5Opts.correl:null)? 'none':'str',
types: [typedInputNoneOpt, 'str'],
});
$("#node-config-input-"+sectionId+"-expiry").val(v5Opts?v5Opts.expiry:"").typedInput({
default: !(v5Opts?v5Opts.expiry:null)? 'none':'num',
types: [typedInputNoneOpt, 'num'],
});
}
// show first section if none are set so the user gets the idea
@@ -357,9 +541,13 @@
|| this.willTopic === ""
&& this.birthTopic === ""
&& this.closeTopic == "";
setUpSection('#mqtt-broker-section-birth', showBirthSection);
setUpSection('#mqtt-broker-section-close', this.closeTopic !== "");
setUpSection('#mqtt-broker-section-will', this.willTopic !== "");
setUpSection('birth', this.birthMsg, showBirthSection);
setUpSection('close', this.closeMsg, this.closeTopic !== "");
setUpSection('will', this.willMsg, this.willTopic !== "");
if (this.willMsg) {
$("#node-config-input-will-delay").val(this.willMsg.delay);
}
setTimeout(function() { tabs.resize(); },0);
if (typeof this.cleansession === 'undefined') {
@@ -370,10 +558,28 @@
this.usetls = false;
$("#node-config-input-usetls").prop("checked",false);
}
if (typeof this.compatmode === 'undefined') {
this.compatmode = false;
$("#node-config-input-compatmode").prop('checked', false);
if (this.compatmode === 'true' || this.compatmode === true) {
delete this.compatmode;
this.protocolVersion = 4;
}
if (typeof this.protocolVersion === 'undefined') {
this.protocolVersion = 4;
}
$("#node-config-input-protocolVersion").on("change", function() {
var v5 = $("#node-config-input-protocolVersion").val() == "5";
if(v5) {
$("#node-config-input-cleansession-label").text(RED._("node-red:mqtt.label.cleanstart"))
$("div.form-row.mqtt5").show();
} else {
$("#node-config-input-cleansession-label").text(RED._("node-red:mqtt.label.cleansession"))
$("div.form-row.mqtt5").hide();
}
});
$("#node-config-input-protocolVersion").val(this.protocolVersion);
$("#node-config-input-userProps").typedInput({
default: !this.userProps ? 'none':'json',
types: [typedInputNoneOpt, 'json']
});
if (typeof this.keepalive === 'undefined') {
this.keepalive = 15;
$("#node-config-input-keepalive").val(this.keepalive);
@@ -391,6 +597,7 @@
$("#node-config-input-willQos").val("0");
}
function updateTLSOptions() {
if ($("#node-config-input-usetls").is(':checked')) {
$("#node-config-row-tls").show();
@@ -409,7 +616,7 @@
} else {
$("#node-config-input-clientid").attr("placeholder",node._("mqtt.placeholder.clientid-nonclean"));
}
$("#node-config-input-clientid").change();
$("#node-config-input-clientid").trigger("change");
}
setTimeout(updateClientId,0);
$("#node-config-input-cleansession").on("click",function() {
@@ -436,12 +643,198 @@
updatePortEntry();
});
setTimeout(updatePortEntry,50);
setTimeout(function() {
$("#node-config-input-protocolVersion").trigger("change");
},50);
},
oneditsave: function() {
if (!$("#node-config-input-usetls").is(':checked')) {
$("#node-config-input-tls").val("");
}
var v5 = $("#node-config-input-protocolVersion").val() == "5";
function saveV5Message(section) {
var msg = {};
if ($("#node-config-input-"+section+"Topic").val().trim().length > 0) {
var contentType = $("#node-config-input-"+section+"-contentType").val().trim();
if (contentType === '') {
contentType = $("#node-config-input-"+section+"-contentType").typedInput('type');
if (contentType === 'none' || contentType === 'other') {
contentType = "";
}
}
if (contentType) {
msg.contentType = contentType;
}
var props = $("#node-config-input-"+section+"-props").val().trim();
if (props) {
msg.userProps = props;
}
var resp = $("#node-config-input-"+section+"-respTopic").val().trim();
if (props) {
msg.respTopic = resp;
}
var correl = $("#node-config-input-"+section+"-correl").val().trim();
if (correl) {
msg.correl = correl;
}
var expiry = $("#node-config-input-"+section+"-expiry").val().trim();
if (expiry) {
msg.expiry = expiry;
}
}
return msg;
}
if (v5) {
this.birthMsg = saveV5Message("birth");
this.closeMsg = saveV5Message("close");
this.willMsg = saveV5Message("will");
var willDelay = $("#node-config-input-will-delay").val();
if (willDelay) {
this.willMsg.delay = willDelay;
}
} else {
this.willMsg = {};
this.birthMsg = {};
this.closeMsg = {};
}
}
});
RED.nodes.registerType('mqtt in',{
category: 'network',
defaults: {
name: {value:""},
topic: {value:"",required:true,validate: RED.validators.regex(/^(#$|(\+|[^+#]*)(\/(\+|[^+#]*))*(\/(\+|#|[^+#]*))?$)/)},
qos: {value: "2"},
datatype: {value:"auto",required:true},
broker: {type:"mqtt-broker", required:true},
// subscriptionIdentifier: {value:0},
nl: {value:false},
rap: {value:true},
rh: {value:0},
},
color:"#d8bfd8",
inputs:0,
outputs:1,
icon: "bridge.svg",
label: function() {
return this.name||this.topic||"mqtt";
},
labelStyle: function() {
return this.name?"node_label_italic":"";
},
oneditprepare: function() {
$("#node-input-broker").on("change",function(d){
var confNode = RED.nodes.node($("#node-input-broker").val());
var v5 = confNode && confNode.protocolVersion == "5";
if(v5) {
$("div.form-row.mqtt5").show();
} else {
$("div.form-row.mqtt5").hide();
}
});
if (this.qos === undefined) {
$("#node-input-qos").val("2");
}
if (this.datatype === undefined) {
$("#node-input-datatype").val("auto");
}
}
});
RED.nodes.registerType('mqtt out',{
category: 'network',
defaults: {
name: {value:""},
topic: {value:""},
qos: {value:""},
retain: {value:""},
respTopic: {value:""},
contentType: {value:""},
userProps: {value:''},
correl: {value:''},
expiry: {value:''},
broker: {type:"mqtt-broker", required:true}
},
color:"#d8bfd8",
inputs:1,
outputs:0,
icon: "bridge.svg",
align: "right",
label: function() {
return this.name||this.topic||"mqtt";
},
oneditprepare: function() {
var that = this;
function showHideDynamicFields() {
var confNode = RED.nodes.node($("#node-input-broker").val());
var v5 = confNode && confNode.protocolVersion == "5";
if(v5) {
$("div.form-row.mqtt5").show();
var t = $("#node-input-respTopic").typedInput("type");
if (t == 'none') {
$("#node-input-correl").parent().hide();
} else {
$("#node-input-correl").parent().show();
}
} else {
$("div.form-row.mqtt5").hide();
}
}
$("#node-input-broker").on("change",function(d){
showHideDynamicFields();
});
var respTopicTI = $("#node-input-respTopic").typedInput({
default: !this.respTopic ? 'none':'str',
types: [typedInputNoneOpt, 'str'],
});
var correlTI = $("#node-input-correl").typedInput({
default: !this.correl ? 'none':'str',
types: [typedInputNoneOpt, 'str']
});
//show / hide correlation data depending on respTopic
respTopicTI.on("change", showHideDynamicFields);
respTopicTI.triggerHandler("change");
$("#node-input-userProps").typedInput({
default: !this.userProps ? 'none':'json',
types: [typedInputNoneOpt, 'json'],
});
$("#node-input-expiry").typedInput({
default: !this.expiry ? 'none':'num',
types: [typedInputNoneOpt, 'num']
});
$("#node-input-contentType").on('change', function (event, type, value, urg) {
console.log(event);
console.log("ct change",type,value, urg);
}).typedInput({
default: getDefaultContentType(this.contentType),
types: contentTypeOpts
})
},
oneditsave: function() {
var contentType = $("#node-input-contentType").val().trim();
if (contentType === '') {
contentType = $("#node-input-contentType").typedInput('type');
if (contentType === 'none' || contentType === 'other') {
contentType = "";
}
}
$("#node-input-contentType").val(contentType)
},
labelStyle: function() {
return this.name?"node_label_italic":"";
}
});
})();
</script>

View File

@@ -43,6 +43,109 @@ module.exports = function(RED) {
return re.test(t);
}
/**
* Helper function for setting integer property values in the MQTT V5 properties object
* @param {object} src Source object containing properties
* @param {object} dst Destination object to set/add properties
* @param {string} propName The property name to set in the Destination object
* @param {integer} [minVal] The minimum value. If the src value is less than minVal, it will NOT be set in the destination
* @param {integer} [maxVal] The maximum value. If the src value is greater than maxVal, it will NOT be set in the destination
* @param {integer} [def] An optional default to set in the destination object if prop is NOT present in the soruce object
*/
function setIntProp(src, dst, propName, minVal, maxVal, def) {
if (src.hasOwnProperty(propName)) {
var v = parseInt(src[propName]);
if(isNaN(v)) return;
if(minVal != null) {
if(v < minVal) return;
}
if(maxVal != null) {
if(v > maxVal) return;
}
dst[propName] = v;
} else {
if(def != undefined) dst[propName] = def;
}
}
/**
* Helper function for setting string property values in the MQTT V5 properties object
* @param {object} src Source object containing properties
* @param {object} dst Destination object to set/add properties
* @param {string} propName The property name to set in the Destination object
* @param {string} [def] An optional default to set in the destination object if prop is NOT present in the soruce object
*/
function setStrProp(src, dst, propName, def) {
if (src[propName] && typeof src[propName] == "string") {
dst[propName] = src[propName];
} else {
if(def != undefined) dst[propName] = def;
}
}
/**
* Helper function for setting boolean property values in the MQTT V5 properties object
* @param {object} src Source object containing properties
* @param {object} dst Destination object to set/add properties
* @param {string} propName The property name to set in the Destination object
* @param {boolean} [def] An optional default to set in the destination object if prop is NOT present in the soruce object
*/
function setBoolProp(src, dst, propName, def) {
if (src[propName] != null) {
if(src[propName] === "true" || src[propName] === true) {
dst[propName] = true;
} else if(src[propName] === "false" || src[propName] === false) {
dst[propName] = true;
}
} else {
if(def != undefined) dst[propName] = def;
}
}
/**
* Helper function for copying the MQTT v5 srcUserProperties object (parameter1) to the properties object (parameter2).
* Any property in srcUserProperties that is NOT a key/string pair will be silently discarded.
* NOTE: if no sutable properties are present, the userProperties object will NOT be added to the properties object
* @param {object} srcUserProperties An object with key/value string pairs
* @param {object} properties A properties object in which userProperties will be copied to
*/
function setUserProperties(srcUserProperties, properties) {
if (srcUserProperties && typeof srcUserProperties == "object") {
let _clone = {};
let count = 0;
let keys = Object.keys(srcUserProperties);
if(!keys || !keys.length) return null;
keys.forEach(key => {
let val = srcUserProperties[key];
if(typeof val == "string") {
count++;
_clone[key] = val;
}
});
if(count) properties.userProperties = _clone;
}
}
/**
* Helper function for copying the MQTT v5 buffer type properties
* NOTE: if src[propName] is not a buffer, dst[propName] will NOT be assigned a value (unless def is set)
* @param {object} src Source object containing properties
* @param {object} dst Destination object to set/add properties
* @param {string} propName The property name to set in the Destination object
* @param {boolean} [def] An optional default to set in the destination object if prop is NOT present in the Source object
*/
function setBufferProp(src, dst, propName, def) {
if(!dst) return;
if (src && dst) {
var buf = src[propName];
if (buf && typeof Buffer.isBuffer(buf)) {
dst[propName] = Buffer.from(buf);
}
} else {
if(def != undefined) dst[propName] = def;
}
}
function MQTTBrokerNode(n) {
RED.nodes.createNode(this,n);
@@ -54,8 +157,15 @@ module.exports = function(RED) {
this.usews = n.usews;
this.verifyservercert = n.verifyservercert;
this.compatmode = n.compatmode;
this.protocolVersion = n.protocolVersion;
this.keepalive = n.keepalive;
this.cleansession = n.cleansession;
this.sessionExpiryInterval = n.sessionExpiry;
this.topicAliasMaximum = n.topicAliasMaximum;
this.maximumPacketSize = n.maximumPacketSize;
this.receiveMaximum = n.receiveMaximum;
this.userProperties = n.userProperties;//https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901116
this.userPropertiesType = n.userPropertiesType;
// Config node state
this.brokerurl = "";
@@ -71,8 +181,23 @@ module.exports = function(RED) {
topic: n.birthTopic,
payload: n.birthPayload || "",
qos: Number(n.birthQos||0),
retain: n.birthRetain=="true"|| n.birthRetain===true
retain: n.birthRetain=="true"|| n.birthRetain===true,
//TODO: add payloadFormatIndicator, messageExpiryInterval, contentType, responseTopic, correlationData, userProperties
};
if (n.birthMsg) {
setStrProp(n.birthMsg, this.birthMessage, "contentType");
if(n.birthMsg.userProps && /^ *{/.test(n.birthMsg.userProps)) {
try {
setUserProperties(JSON.parse(n.birthMsg.userProps), this.birthMessage);
} catch(err) {}
}
n.birthMsg.responseTopic = n.birthMsg.respTopic;
setStrProp(n.birthMsg, this.birthMessage, "responseTopic");
n.birthMsg.correlationData = n.birthMsg.correl;
setBufferProp(n.birthMsg, this.birthMessage, "correlationData");
n.birthMsg.messageExpiryInterval = n.birthMsg.expiry
setIntProp(n.birthMsg,this.birthMessage, "messageExpiryInterval")
}
}
if (n.closeTopic) {
@@ -80,8 +205,23 @@ module.exports = function(RED) {
topic: n.closeTopic,
payload: n.closePayload || "",
qos: Number(n.closeQos||0),
retain: n.closeRetain=="true"|| n.closeRetain===true
retain: n.closeRetain=="true"|| n.closeRetain===true,
//TODO: add payloadFormatIndicator, messageExpiryInterval, contentType, responseTopic, correlationData, userProperties
};
if (n.closeMsg) {
setStrProp(n.closeMsg, this.closeMessage, "contentType");
if(n.closeMsg.userProps && /^ *{/.test(n.closeMsg.userProps)) {
try {
setUserProperties(JSON.parse(n.closeMsg.userProps), this.closeMessage);
} catch(err) {}
}
n.closeMsg.responseTopic = n.closeMsg.respTopic;
setStrProp(n.closeMsg, this.closeMessage, "responseTopic");
n.closeMsg.correlationData = n.closeMsg.correl;
setBufferProp(n.closeMsg, this.closeMessage, "correlationData");
n.closeMsg.messageExpiryInterval = n.closeMsg.expiry
setIntProp(n.birthMsg,this.closeMessage, "messageExpiryInterval")
}
}
if (this.credentials) {
@@ -97,9 +237,6 @@ module.exports = function(RED) {
if (typeof this.usews === 'undefined') {
this.usews = false;
}
if (typeof this.compatmode === 'undefined') {
this.compatmode = false;
}
if (typeof this.verifyservercert === 'undefined') {
this.verifyservercert = false;
}
@@ -183,9 +320,22 @@ module.exports = function(RED) {
this.options.keepalive = this.keepalive;
this.options.clean = this.cleansession;
this.options.reconnectPeriod = RED.settings.mqttReconnectTime||5000;
if (this.compatmode == "true" || this.compatmode === true) {
if (this.compatmode == "true" || this.compatmode === true || this.protocolVersion == 3) {
this.options.protocolId = 'MQIsdp';
this.options.protocolVersion = 3;
} else if ( this.protocolVersion == 5 ) {
this.options.protocolVersion = 5;
this.options.properties = {};
this.options.properties.requestResponseInformation = true;
this.options.properties.requestProblemInformation = true;
if(this.userProperties && /^ *{/.test(this.userProperties)) {
try {
setUserProperties(JSON.parse(this.userProperties), this.options.properties);
} catch(err) {}
}
if (this.sessionExpiryInterval && this.sessionExpiryInterval !== "0") {
setIntProp(this,this.options.properties,"sessionExpiryInterval");
}
}
if (this.usetls && n.tls) {
var tlsNode = RED.nodes.getNode(n.tls);
@@ -193,7 +343,6 @@ module.exports = function(RED) {
tlsNode.addTLSOptions(this.options);
}
}
// console.log(this.brokerurl,this.options);
// If there's no rejectUnauthorized already, then this could be an
// old config where this option was provided on the broker node and
@@ -207,10 +356,32 @@ module.exports = function(RED) {
topic: n.willTopic,
payload: n.willPayload || "",
qos: Number(n.willQos||0),
retain: n.willRetain=="true"|| n.willRetain===true
retain: n.willRetain=="true"|| n.willRetain===true,
//TODO: add willDelayInterval, payloadFormatIndicator, messageExpiryInterval, contentType, responseTopic, correlationData, userProperties
};
if (n.willMsg) {
this.options.will.properties = {};
setStrProp(n.willMsg, this.options.will.properties, "contentType");
if(n.willMsg.userProps && /^ *{/.test(n.willMsg.userProps)) {
try {
setUserProperties(JSON.parse(n.willMsg.userProps), this.options.will.properties);
} catch(err) {}
}
n.willMsg.responseTopic = n.willMsg.respTopic;
setStrProp(n.willMsg, this.options.will.properties, "responseTopic");
n.willMsg.correlationData = n.willMsg.correl;
setBufferProp(n.willMsg, this.options.will.properties, "correlationData");
n.willMsg.willDelayInterval = n.willMsg.delay
setIntProp(n.willMsg,this.options.will.properties, "willDelayInterval")
n.willMsg.messageExpiryInterval = n.willMsg.expiry
setIntProp(n.willMsg,this.options.will.properties, "messageExpiryInterval")
this.options.will.payloadFormatIndicator = true;
}
}
// console.log(this.brokerurl,this.options);
// Define functions called by MQTT in and out nodes
var node = this;
this.users = {};
@@ -229,7 +400,15 @@ module.exports = function(RED) {
}
if (Object.keys(node.users).length === 0) {
if (node.client && node.client.connected) {
return node.client.end(done);
// Send close message
if (node.closeMessage) {
node.publish(node.closeMessage,function(err) {
node.client.end(done);
});
} else {
node.client.end(done);
}
return;
} else {
node.client.end();
return done();
@@ -242,13 +421,36 @@ module.exports = function(RED) {
if (!node.connected && !node.connecting) {
node.connecting = true;
try {
node.serverProperties = {};
node.client = mqtt.connect(node.brokerurl ,node.options);
node.client.setMaxListeners(0);
// Register successful connect or reconnect handler
node.client.on('connect', function () {
node.client.on('connect', function (connack) {
node.connecting = false;
node.connected = true;
node.topicAliases = {};
node.log(RED._("mqtt.state.connected",{broker:(node.clientid?node.clientid+"@":"")+node.brokerurl}));
if(node.options.protocolVersion == 5 && connack && connack.hasOwnProperty("properties")) {
if(typeof connack.properties == "object") {
//clean & assign all props sent from server.
setIntProp(connack.properties, node.serverProperties, "topicAliasMaximum", 0);
setIntProp(connack.properties, node.serverProperties, "receiveMaximum", 0);
setIntProp(connack.properties, node.serverProperties, "sessionExpiryInterval", 0, 0xFFFFFFFF);
setIntProp(connack.properties, node.serverProperties, "maximumQoS", 0, 2);
setBoolProp(connack.properties, node.serverProperties, "retainAvailable",true);
setBoolProp(connack.properties, node.serverProperties, "wildcardSubscriptionAvailable", true);
setBoolProp(connack.properties, node.serverProperties, "subscriptionIdentifiersAvailable", true);
setBoolProp(connack.properties, node.serverProperties, "sharedSubscriptionAvailable");
setIntProp(connack.properties, node.serverProperties, "maximumPacketSize", 0);
setIntProp(connack.properties, node.serverProperties, "serverKeepAlive");
setStrProp(connack.properties, node.serverProperties, "responseInformation");
setStrProp(connack.properties, node.serverProperties, "serverReference");
setStrProp(connack.properties, node.serverProperties, "assignedClientIdentifier");
setStrProp(connack.properties, node.serverProperties, "reasonString");
setUserProperties(connack.properties, node.serverProperties);
// node.debug("CONNECTED. node.serverProperties ==> "+JSON.stringify(node.serverProperties));//TODO: remove
}
}
for (var id in node.users) {
if (node.users.hasOwnProperty(id)) {
node.users[id].status({fill:"green",shape:"dot",text:"node-red:common.status.connected"});
@@ -260,16 +462,18 @@ module.exports = function(RED) {
// Re-subscribe to stored topics
for (var s in node.subscriptions) {
if (node.subscriptions.hasOwnProperty(s)) {
var topic = s;
var qos = 0;
let topic = s;
let qos = 0;
let _options = {};
for (var r in node.subscriptions[s]) {
if (node.subscriptions[s].hasOwnProperty(r)) {
qos = Math.max(qos,node.subscriptions[s][r].qos);
_options = node.subscriptions[s][r].options;
node.client.on('message',node.subscriptions[s][r].handler);
}
}
var options = {qos: qos};
node.client.subscribe(topic, options);
_options.qos = _options.qos || qos;
node.client.subscribe(topic, _options);
}
}
@@ -284,7 +488,14 @@ module.exports = function(RED) {
node.users[id].status({fill:"yellow",shape:"ring",text:"node-red:common.status.connecting"});
}
}
})
});
//TODO: what to do with this event? Anything? Necessary?
node.client.on("disconnect", function(packet) {
//Emitted after receiving disconnect packet from broker. MQTT 5.0 feature.
var rc = packet && packet.properties && packet.properties.reasonString;
var rc = packet && packet.properties && packet.reasonCode;
//TODO: If keeping this event, do we use these? log these?
});
// Register disconnect handlers
node.client.on('close', function () {
if (node.connected) {
@@ -302,31 +513,54 @@ module.exports = function(RED) {
// Register connect error handler
// The client's own reconnect logic will take care of errors
node.client.on('error', function (error) {});
node.client.on('error', function (error) {
});
}catch(err) {
console.log(err);
}
}
};
this.subscribe = function (topic,qos,callback,ref) {
this.subscriptionIds = {};
this.subid = 1;
this.subscribe = function (topic,options,callback,ref) {
ref = ref||0;
var qos;
if(typeof options == "object") {
qos = options.qos;
} else {
qos = options;
options = {};
}
options.qos = qos;
if (!node.subscriptionIds[topic]) {
node.subscriptionIds[topic] = node.subid++;
}
options.properties = options.properties || {};
options.properties.subscriptionIdentifier = node.subscriptionIds[topic];
node.subscriptions[topic] = node.subscriptions[topic]||{};
var sub = {
topic:topic,
qos:qos,
options:options,
handler:function(mtopic,mpayload, mpacket) {
if (matchTopic(topic,mtopic)) {
if(mpacket.properties && options.properties && mpacket.properties.subscriptionIdentifier && options.properties.subscriptionIdentifier && (mpacket.properties.subscriptionIdentifier !== options.properties.subscriptionIdentifier) ) {
//do nothing as subscriptionIdentifier does not match
// node.debug(`> no match - this nodes subID (${options.properties.subscriptionIdentifier}) !== packet subID (${mpacket.properties.subscriptionIdentifier})`); //TODO: remove
} else if (matchTopic(topic,mtopic)) {
// node.debug(`> MATCHED '${topic}' to '${mtopic}' - performing callback`); //TODO: remove
callback(mtopic,mpayload, mpacket);
} else {
// node.debug(`> no match / no callback`); //TODO: remove
}
},
ref: ref
};
node.subscriptions[topic][ref] = sub;
if (node.connected) {
// node.debug(`this.subscribe - registering handler ref ${ref} for ${topic} and subscribing `+JSON.stringify(options)); //TODO: remove
node.client.on('message',sub.handler);
var options = {};
options.qos = qos;
node.client.subscribe(topic, options);
}
};
@@ -334,21 +568,35 @@ module.exports = function(RED) {
this.unsubscribe = function (topic, ref, removed) {
ref = ref||0;
var sub = node.subscriptions[topic];
// var _debug = `unsubscribe for topic ${topic} called... ` ; //TODO: remove
if (sub) {
// _debug += "sub found. " //TODO: remove
if (sub[ref]) {
// debug(`this.unsubscribe - removing handler ref ${ref} for ${topic} `); //TODO: remove
// _debug += `removing handler ref ${ref} for ${topic}. `
node.client.removeListener('message',sub[ref].handler);
delete sub[ref];
}
if (removed) {
//TODO: Review. The `if(removed)` was commented out to always delete and remove subscriptions.
// if we dont then property changes dont get applied and old subs still trigger
//if (removed) {
if (Object.keys(sub).length === 0) {
delete node.subscriptions[topic];
delete node.subscriptionIds[topic];
if (node.connected) {
// _debug += `calling client.unsubscribe to remove topic ${topic}` //TODO: remove
node.client.unsubscribe(topic);
}
}
}
//}
} else {
// _debug += "sub not found! "; //TODO: remove
}
// node.debug(_debug); //TODO: remove
};
this.topicAliases = {};
this.publish = function (msg,done) {
if (node.connected) {
@@ -361,13 +609,36 @@ module.exports = function(RED) {
msg.payload = "" + msg.payload;
}
}
var options = {
qos: msg.qos || 0,
retain: msg.retain || false
};
//https://github.com/mqttjs/MQTT.js/blob/master/README.md#mqttclientpublishtopic-message-options-callback
if(node.options.protocolVersion == 5) {
options.properties = options.properties || {};
setStrProp(msg, options.properties, "responseTopic");
setBufferProp(msg, options.properties, "correlationData");
setStrProp(msg, options.properties, "contentType");
setIntProp(msg, options.properties, "messageExpiryInterval", 0);
setUserProperties(msg.userProperties, options.properties);
setIntProp(msg, options.properties, "topicAlias", 1, node.serverProperties.topicAliasMaximum || 0);
setBoolProp(msg, options.properties, "payloadFormatIndicator");
//FUTURE setIntProp(msg, options.properties, "subscriptionIdentifier", 1, 268435455);
if (options.properties.topicAlias) {
if (!node.topicAliases.hasOwnProperty(options.properties.topicAlias) && msg.topic == "") {
done("Invalid topicAlias");
return
}
if (node.topicAliases[options.properties.topicAlias] === msg.topic) {
msg.topic = ""
} else {
node.topicAliases[options.properties.topicAlias] = msg.topic
}
}
}
node.client.publish(msg.topic, msg.payload, options, function(err) {
done && done();
done && done(err);
return
});
}
@@ -376,10 +647,6 @@ module.exports = function(RED) {
this.on('close', function(done) {
this.closing = true;
if (this.connected) {
// Send close message
if (node.closeMessage) {
node.publish(node.closeMessage);
}
this.client.once('close', function() {
done();
});
@@ -405,6 +672,12 @@ module.exports = function(RED) {
RED.nodes.createNode(this,n);
this.topic = n.topic;
this.qos = parseInt(n.qos);
this.subscriptionIdentifier = n.subscriptionIdentifier;//https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901117
this.nl = n.nl;
this.rap = n.rap;
this.rh = n.rh;
if (isNaN(this.qos) || this.qos < 0 || this.qos > 2) {
this.qos = 2;
}
@@ -416,10 +689,27 @@ module.exports = function(RED) {
this.datatype = n.datatype || "utf8";
var node = this;
if (this.brokerConn) {
let v5 = this.brokerConn.options && this.brokerConn.options.protocolVersion == 5;
this.status({fill:"red",shape:"ring",text:"node-red:common.status.disconnected"});
if (this.topic) {
node.brokerConn.register(this);
this.brokerConn.subscribe(this.topic,this.qos,function(topic,payload,packet) {
let options = { qos: this.qos };
if(v5) {
// options.properties = {};
// if(node.userProperties) {
// let userProperties = RED.util.evaluateNodeProperty(node.userProperties, node.userPropertiesType, node, {});
// setUserProperties(userProperties, options.properties);
// }
// setIntProp(node,options.properties,"subscriptionIdentifier", 1);
setIntProp(node, options, "rh");
if(node.nl === "true" || node.nl === true) options.nl = true;
else if(node.nl === "false" || node.nl === false) options.nl = false;
if(node.rap === "true" || node.rap === true) options.rap = true;
else if(node.rap === "false" || node.rap === false) options.rap = false;
}
this.brokerConn.subscribe(this.topic,options,function(topic,payload,packet) {
// node.debug(`Sent ${topic}, datatype ${node.datatype} `+JSON.stringify(packet));//TODO: remove
if (node.datatype === "buffer") {
// payload = payload;
} else if (node.datatype === "base64") {
@@ -437,6 +727,18 @@ module.exports = function(RED) {
if (isUtf8(payload)) { payload = payload.toString(); }
}
var msg = {topic:topic, payload:payload, qos:packet.qos, retain:packet.retain};
if(v5 && packet.properties) {
//msg.properties = packet.properties;
setStrProp(packet.properties, msg, "responseTopic");
setBufferProp(packet.properties, msg, "correlationData");
setStrProp(packet.properties, msg, "contentType");
// setIntProp(packet.properties, msg, "topicAlias", 1, node.brokerConn.serverProperties.topicAliasMaximum || 0);
// setIntProp(packet.properties, msg, "subscriptionIdentifier", 1, 268435455);
setIntProp(packet.properties, msg, "messageExpiryInterval", 0);
setBoolProp(packet.properties, msg, "payloadFormatIndicator");
setStrProp(packet.properties, msg, "reasonString");
setUserProperties(packet.properties.userProperties, msg);
}
if ((node.brokerConn.broker === "localhost")||(node.brokerConn.broker === "127.0.0.1")) {
msg._topic = topic;
}
@@ -467,11 +769,26 @@ module.exports = function(RED) {
this.qos = n.qos || null;
this.retain = n.retain;
this.broker = n.broker;
this.responseTopic = n.respTopic;//https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901114
this.correlationData = n.correl;//https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901115
this.contentType = n.contentType;//https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901118
this.messageExpiryInterval = n.expiry; //https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901112
try {
if (/^ *{/.test(n.userProps)) {
//setup this.userProperties
setUserProperties(JSON.parse(n.userProps), this);//https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901116
}
} catch(err) {}
// this.topicAlias = n.topicAlias; //https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901113
// this.payloadFormatIndicator = n.payloadFormatIndicator; //https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901111
// this.subscriptionIdentifier = n.subscriptionIdentifier;//https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901117
this.brokerConn = RED.nodes.getNode(this.broker);
var node = this;
var chk = /[\+#]/;
if (this.brokerConn) {
let v5 = this.brokerConn.options && this.brokerConn.options.protocolVersion == 5;
this.status({fill:"red",shape:"ring",text:"node-red:common.status.disconnected"});
this.on("input",function(msg,send,done) {
if (msg.qos) {
@@ -483,13 +800,63 @@ module.exports = function(RED) {
msg.qos = Number(node.qos || msg.qos || 0);
msg.retain = node.retain || msg.retain || false;
msg.retain = ((msg.retain === true) || (msg.retain === "true")) || false;
if (node.topic) {
msg.topic = node.topic;
/** If node property exists, override/set that to property in msg */
let msgPropOverride = function(propName) { if(node[propName]) { msg[propName] = node[propName]; } }
msgPropOverride("topic");
if(v5) {
if(node.userProperties) {
msg.userProperties = node.userProperties;
}
if(node.responseTopic) {
msg.responseTopic = node.responseTopic;
}
if(node.correlationData) {
msg.correlationData = node.correlationData;
}
if(node.contentType) {
msg.contentType = node.contentType;
}
if(node.messageExpiryInterval) {
msg.messageExpiryInterval = node.messageExpiryInterval;
}
//Next, update/override the msg.xxxx properties from node config
//TODO: Should we be expecting msg.properties.xxxx instead of msg.xxxx?
// setStrProp(node,msg,"responseTopic");
// setBufferProp(node,msg,"correlationData");
// setStrProp(node,msg,"contentType");
// setIntProp(node,msg,"messageExpiryInterval");
//FUTURE setStrProp(node,msg,"topicAlias");
//FUTURE setBoolProp(node,msg,"payloadFormatIndicator");
//FUTURE setIntProp(node,msg,"subscriptionIdentifier");
}
if (msg.userProperties && typeof msg.userProperties !== "object") {
delete msg.userProperties;
}
if (msg.hasOwnProperty("topicAlias") && !isNaN(msg.topicAlias) && (msg.topicAlias === 0 || node.brokerConn.serverProperties.topicAliasMaximum === 0 || msg.topicAlias > node.brokerConn.serverProperties.topicAliasMaximum)) {
delete msg.topicAlias;
}
if ( msg.hasOwnProperty("payload")) {
if (msg.hasOwnProperty("topic") && (typeof msg.topic === "string") && (msg.topic !== "")) { // topic must exist
if (chk.test(msg.topic)) { node.warn(RED._("mqtt.errors.invalid-topic")); }
this.brokerConn.publish(msg, done); // send the message
let topicOK = msg.hasOwnProperty("topic") && (typeof msg.topic === "string") && (msg.topic !== "");
if (!topicOK && v5) {
//NOTE: A value of 0 (in server props topicAliasMaximum) indicates that the Server does not accept any Topic Aliases on this connection
if (msg.hasOwnProperty("topicAlias") && !isNaN(msg.topicAlias) && msg.topicAlias >= 0 && node.brokerConn.serverProperties.topicAliasMaximum && node.brokerConn.serverProperties.topicAliasMaximum >= msg.topicAlias) {
topicOK = true;
msg.topic = ""; //must be empty string
} else if (msg.hasOwnProperty("responseTopic") && (typeof msg.responseTopic === "string") && (msg.responseTopic !== "")) {
//TODO: if topic is empty but responseTopic has a string value, use that instead. Is this desirable?
topicOK = true;
msg.topic = msg.responseTopic;
//TODO: delete msg.responseTopic - to prevent it being resent?
}
}
if (topicOK) { // topic must exist
// node.debug(`sending msg to ${msg.topic} `+JSON.stringify(msg));//TODO: remove
this.brokerConn.publish(msg, function(err) {
let args = arguments;
let l = args.length;
done(err);
}); // send the message
} else {
node.warn(RED._("mqtt.errors.invalid-topic"));
done();

View File

@@ -46,7 +46,9 @@ module.exports = function(RED) {
isText = true;
} else if (parsedType.type !== "application") {
isText = false;
} else if ((parsedType.subtype !== "octet-stream") && (parsedType.subtype !== "cbor")) {
} else if ((parsedType.subtype !== "octet-stream")
&& (parsedType.subtype !== "cbor")
&& (parsedType.subtype !== "x-protobuf")) {
checkUTF = true;
} else {
// application/octet-stream or application/cbor
@@ -210,7 +212,7 @@ module.exports = function(RED) {
var httpMiddleware = function(req,res,next) { next(); }
if (RED.settings.httpNodeMiddleware) {
if (typeof RED.settings.httpNodeMiddleware === "function") {
if (typeof RED.settings.httpNodeMiddleware === "function" || Array.isArray(RED.settings.httpNodeMiddleware)) {
httpMiddleware = RED.settings.httpNodeMiddleware;
}
}

View File

@@ -146,6 +146,7 @@ module.exports = function(RED) {
}
if (msg.hasOwnProperty('followRedirects')) {
opts.followRedirect = msg.followRedirects;
opts.followAllRedirects = !!opts.followRedirect;
}
var redirectList = [];
if (!opts.hasOwnProperty('followRedirect') || opts.followRedirect) {
@@ -282,7 +283,7 @@ module.exports = function(RED) {
node.error(RED._("httpin.errors.invalid-payload"),msg);
nodeDone();
return;
}
}
} else if ( method == "GET" && typeof msg.payload !== "undefined" && paytobody) {
if (typeof msg.payload === "object") {
opts.body = JSON.stringify(msg.payload);
@@ -302,7 +303,7 @@ module.exports = function(RED) {
opts.headers[clSet] = opts.headers['content-length'];
delete opts.headers['content-length'];
}
var noproxy;
if (noprox) {
for (var i = 0; i < noprox.length; i += 1) {

View File

@@ -235,6 +235,7 @@
oneditprepare: function() {
var previous = null;
$("#node-input-out").on('focus', function () { previous = this.value; }).on("change", function() {
$("#node-input-splitc").show();
if (previous === null) { previous = $("#node-input-out").val(); }
if ($("#node-input-out").val() == "char") {
if (previous != "char") { $("#node-input-splitc").val("\\n"); }
@@ -247,6 +248,7 @@
else if ($("#node-input-out").val() == "immed") {
if (previous != "immed") { $("#node-input-splitc").val(" "); }
$("#node-units").text("");
$("#node-input-splitc").hide();
}
else if ($("#node-input-out").val() == "count") {
if (previous != "count") { $("#node-input-splitc").val("12"); }
@@ -255,6 +257,7 @@
else {
if (previous != "sit") { $("#node-input-splitc").val(" "); }
$("#node-units").text("");
$("#node-input-splitc").hide();
}
});
}

View File

@@ -18,7 +18,7 @@ module.exports = function(RED) {
"use strict";
function CSVNode(n) {
RED.nodes.createNode(this,n);
this.template = (n.temp || "").split(",");
this.template = (n.temp || "");
this.sep = (n.sep || ',').replace("\\t","\t").replace("\\n","\n").replace("\\r","\r");
this.quo = '"';
this.ret = (n.ret || "\n").replace("\\n","\n").replace("\\r","\r");
@@ -38,49 +38,50 @@ module.exports = function(RED) {
if (this.hdrout === true) { this.hdrout = "all"; }
var tmpwarn = true;
var node = this;
var re = new RegExp(node.sep.replace(/[-[\]{}()*+!<=:?.\/\\^$|#\s,]/g,'\\$&') + '(?=(?:(?:[^"]*"){2})*[^"]*$)','g');
// pass in an array of column names to be trimed, de-quoted and retrimed
var clean = function(col) {
for (var t = 0; t < col.length; t++) {
col[t] = col[t].trim(); // remove leading and trailing whitespace
if (col[t].charAt(0) === '"' && col[t].charAt(col[t].length -1) === '"') {
// remove leading and trailing quotes (if they exist) - and remove whitepace again.
col[t] = col[t].substr(1,col[t].length -2).trim();
}
}
// pass in an array of column names to be trimmed, de-quoted and retrimmed
var clean = function(col,sep) {
if (sep) { re = new RegExp(sep.replace(/[-[\]{}()*+!<=:?.\/\\^$|#\s,]/g,'\\$&') +'(?=(?:(?:[^"]*"){2})*[^"]*$)','g'); }
col = col.trim().split(re) || [""];
col = col.map(x => x.replace(/"/g,'').trim());
if ((col.length === 1) && (col[0] === "")) { node.goodtmpl = false; }
else { node.goodtmpl = true; }
return col;
}
node.template = clean(node.template);
var template = clean(node.template,',');
var notemplate = template.length === 1 && template[0] === '';
node.hdrSent = false;
this.on("input", function(msg) {
this.on("input", function(msg, send, done) {
if (msg.hasOwnProperty("reset")) {
node.hdrSent = false;
}
if (msg.hasOwnProperty("payload")) {
if (typeof msg.payload == "object") { // convert object to CSV string
try {
if (!(notemplate && (msg.hasOwnProperty("parts") && msg.parts.hasOwnProperty("index") && msg.parts.index > 0))) {
template = clean(node.template);
}
var ou = "";
if (!Array.isArray(msg.payload)) { msg.payload = [ msg.payload ]; }
if (node.hdrout !== "none" && node.hdrSent === false) {
if ((node.template.length === 1) && (node.template[0] === '')) {
if ((template.length === 1) && (template[0] === '')) {
if (msg.hasOwnProperty("columns")) {
node.template = clean((msg.columns || "").split(","));
template = clean(msg.columns || "",",");
}
else {
node.template = Object.keys(msg.payload[0]);
template = Object.keys(msg.payload[0]);
}
}
ou += node.template.join(node.sep) + node.ret;
ou += template.map(v => v.indexOf(node.sep)!==-1 ? '"'+v+'"' : v).join(node.sep) + node.ret;
if (node.hdrout === "once") { node.hdrSent = true; }
}
for (var s = 0; s < msg.payload.length; s++) {
if ((Array.isArray(msg.payload[s])) || (typeof msg.payload[s] !== "object")) {
if (typeof msg.payload[s] !== "object") { msg.payload = [ msg.payload ]; }
for (var t = 0; t < msg.payload[s].length; t++) {
if (!msg.payload[s][t] && (msg.payload[s][t] !== 0)) { msg.payload[s][t] = ""; }
if (msg.payload[s][t] === undefined) { msg.payload[s][t] = ""; }
if (msg.payload[s][t].toString().indexOf(node.quo) !== -1) { // add double quotes if any quotes
msg.payload[s][t] = msg.payload[s][t].toString().replace(/"/g, '""');
msg.payload[s][t] = node.quo + msg.payload[s][t].toString() + node.quo;
@@ -92,10 +93,10 @@ module.exports = function(RED) {
ou += msg.payload[s].join(node.sep) + node.ret;
}
else {
if ((node.template.length === 1) && (node.template[0] === '') && (msg.hasOwnProperty("columns"))) {
node.template = clean((msg.columns || "").split(","));
if ((template.length === 1) && (template[0] === '') && (msg.hasOwnProperty("columns"))) {
template = clean(msg.columns || "",",");
}
if ((node.template.length === 1) && (node.template[0] === '')) {
if ((template.length === 1) && (template[0] === '')) {
/* istanbul ignore else */
if (tmpwarn === true) { // just warn about missing template once
node.warn(RED._("csv.errors.obj_csv"));
@@ -121,12 +122,12 @@ module.exports = function(RED) {
ou = ou.slice(0,-1) + node.ret;
}
else {
for (var t=0; t < node.template.length; t++) {
if (node.template[t] === '') {
for (var t=0; t < template.length; t++) {
if (template[t] === '') {
ou += node.sep;
}
else {
var p = RED.util.ensureString(RED.util.getMessageProperty(msg,"payload["+s+"]['"+node.template[t]+"']"));
var p = RED.util.ensureString(RED.util.getMessageProperty(msg,"payload["+s+"]['"+template[t]+"']"));
/* istanbul ignore else */
if (p === "undefined") { p = ""; }
if (p.indexOf(node.quo) !== -1) { // add double quotes if any quotes
@@ -144,10 +145,11 @@ module.exports = function(RED) {
}
}
msg.payload = ou;
msg.columns = node.template.join(',');
if (msg.payload !== '') { node.send(msg); }
msg.columns = template.map(v => v.indexOf(',')!==-1 ? '"'+v+'"' : v).join(',');
if (msg.payload !== '') { send(msg); }
done();
}
catch(e) { node.error(e,msg); }
catch(e) { done(e); }
}
else if (typeof msg.payload == "string") { // convert CSV string to object
try {
@@ -157,19 +159,24 @@ module.exports = function(RED) {
var o = {}; // output object to build up
var a = []; // output array is needed for multiline option
var first = true; // is this the first line
var last = false;
var line = msg.payload;
var linecount = 0;
var tmp = "";
var has_parts = msg.hasOwnProperty("parts");
var reg = /^[-]?(?!E)(?!0\d)\d*\.?\d*(E-?\+?)?\d+$/i;
if (msg.hasOwnProperty("parts")) {
linecount = msg.parts.index;
if (msg.parts.index > node.skip) { first = false; }
if (msg.parts.hasOwnProperty("count") && (msg.parts.index+1 >= msg.parts.count)) { last = true; }
}
// For now we are just going to assume that any \r or \n means an end of line...
// got to be a weird csv that has singleton \r \n in it for another reason...
// Now process the whole file/line
var nocr = (line.match(/[\r\n]/g)||[]).length;
if (has_parts && node.multi === "mult" && nocr > 1) { tmp = ""; first = true; }
for (var i = 0; i < line.length; i++) {
if (first && (linecount < node.skip)) {
if (line[i] === "\n") { linecount += 1; }
@@ -178,7 +185,7 @@ module.exports = function(RED) {
if ((node.hdrin === true) && first) { // if the template is in the first line
if ((line[i] === "\n")||(line[i] === "\r")||(line.length - i === 1)) { // look for first line break
if (line.length - i === 1) { tmp += line[i]; }
node.template = clean(tmp.split(node.sep));
template = clean(tmp,node.sep);
first = false;
}
else { tmp += line[i]; }
@@ -192,14 +199,14 @@ module.exports = function(RED) {
//if ((line[i-1] !== node.sep) && (line[i+1] !== node.sep)) { k[j] += line[i]; }
}
else if ((line[i] === node.sep) && f) { // if it is the end of the line then finish
if (!node.goodtmpl) { node.template[j] = "col"+(j+1); }
if ( node.template[j] && (node.template[j] !== "") ) {
if (!node.goodtmpl) { template[j] = "col"+(j+1); }
if ( template[j] && (template[j] !== "") ) {
// if no value between separators ('1,,"3"...') or if the line beings with separator (',1,"2"...') treat value as null
if (line[i-1] === node.sep || line[i-1].includes('\n','\r')) k[j] = null;
if ( (k[j] !== null && node.parsestrings === true) && reg.test(k[j]) ) { k[j] = parseFloat(k[j]); }
if (node.include_null_values && k[j] === null) o[node.template[j]] = k[j];
if (node.include_empty_strings && k[j] === "") o[node.template[j]] = k[j];
if (k[j] !== null && k[j] !== "") o[node.template[j]] = k[j];
if (node.include_null_values && k[j] === null) o[template[j]] = k[j];
if (node.include_empty_strings && k[j] === "") o[template[j]] = k[j];
if (k[j] !== null && k[j] !== "") o[template[j]] = k[j];
}
j += 1;
// if separator is last char in processing string line (without end of line), add null value at the end - example: '1,2,3\n3,"3",'
@@ -207,15 +214,15 @@ module.exports = function(RED) {
}
else if (((line[i] === "\n") || (line[i] === "\r")) && f) { // handle multiple lines
//console.log(j,k,o,k[j]);
if (!node.goodtmpl) { node.template[j] = "col"+(j+1); }
if ( node.template[j] && (node.template[j] !== "") ) {
if (!node.goodtmpl) { template[j] = "col"+(j+1); }
if ( template[j] && (template[j] !== "") ) {
// if separator before end of line, set null value ie. '1,2,"3"\n1,2,\n1,2,3'
if (line[i-1] === node.sep) k[j] = null;
if ( (k[j] !== null && node.parsestrings === true) && reg.test(k[j]) ) { k[j] = parseFloat(k[j]); }
else { if (k[j] !== null) k[j].replace(/\r$/,''); }
if (node.include_null_values && k[j] === null) o[node.template[j]] = k[j];
if (node.include_empty_strings && k[j] === "") o[node.template[j]] = k[j];
if (k[j] !== null && k[j] !== "") o[node.template[j]] = k[j];
if (node.include_null_values && k[j] === null) o[template[j]] = k[j];
if (node.include_empty_strings && k[j] === "") o[template[j]] = k[j];
if (k[j] !== null && k[j] !== "") o[template[j]] = k[j];
}
if (JSON.stringify(o) !== "{}") { // don't send empty objects
a.push(o); // add to the array
@@ -232,44 +239,44 @@ module.exports = function(RED) {
}
// Finished so finalize and send anything left
if (f === false) { node.warn(RED._("csv.errors.bad_csv")); }
if (!node.goodtmpl) { node.template[j] = "col"+(j+1); }
if (!node.goodtmpl) { template[j] = "col"+(j+1); }
if ( node.template[j] && (node.template[j] !== "") ) {
if ( template[j] && (template[j] !== "") ) {
if ( (k[j] !== null && node.parsestrings === true) && reg.test(k[j]) ) { k[j] = parseFloat(k[j]); }
else { if (k[j] !== null) k[j].replace(/\r$/,''); }
if (node.include_null_values && k[j] === null) o[node.template[j]] = k[j];
if (node.include_empty_strings && k[j] === "") o[node.template[j]] = k[j];
if (k[j] !== null && k[j] !== "") o[node.template[j]] = k[j];
if (node.include_null_values && k[j] === null) o[template[j]] = k[j];
if (node.include_empty_strings && k[j] === "") o[template[j]] = k[j];
if (k[j] !== null && k[j] !== "") o[template[j]] = k[j];
}
if (JSON.stringify(o) !== "{}") { // don't send empty objects
a.push(o); // add to the array
}
var has_parts = msg.hasOwnProperty("parts");
if (node.multi !== "one") {
msg.payload = a;
if (has_parts) {
if (has_parts && nocr <= 1) {
if (JSON.stringify(o) !== "{}") {
node.store.push(o);
}
if (msg.parts.index + 1 === msg.parts.count) {
msg.payload = node.store;
msg.columns = node.template.filter(val => val).join(',');
msg.columns = template.map(v => v.indexOf(',')!==-1 ? '"'+v+'"' : v).filter(v => v).join(',');
delete msg.parts;
node.send(msg);
send(msg);
node.store = [];
}
}
else {
msg.columns = node.template.filter(val => val).join(',');
node.send(msg); // finally send the array
msg.columns = template.map(v => v.indexOf(',')!==-1 ? '"'+v+'"' : v).filter(v => v).join(',');
send(msg); // finally send the array
}
}
else {
var len = a.length;
for (var i = 0; i < len; i++) {
var newMessage = RED.util.cloneMessage(msg);
newMessage.columns = node.template.filter(val => val).join(',');
newMessage.columns = template.map(v => v.indexOf(',')!==-1 ? '"'+v+'"' : v).filter(v => v).join(',');
newMessage.payload = a[i];
if (!has_parts) {
newMessage.parts = {
@@ -286,19 +293,25 @@ module.exports = function(RED) {
newMessage.parts.count -= 1;
}
}
node.send(newMessage);
if (last) { newMessage.complete = true; }
send(newMessage);
}
if (has_parts && last && len === 0) {
send({complete:true});
}
}
node.linecount = 0;
done();
}
catch(e) { node.error(e,msg); }
catch(e) { done(e); }
}
else { node.warn(RED._("csv.errors.csv_js")); }
else { node.warn(RED._("csv.errors.csv_js")); done(); }
}
else {
if (!msg.hasOwnProperty("reset")) {
node.send(msg); // If no payload and not reset - just pass it on.
}
done();
}
});
}

View File

@@ -17,18 +17,18 @@
module.exports = function(RED) {
"use strict";
function sendArray(node,msg,array) {
function sendArray(node,msg,array,send) {
for (var i = 0; i < array.length-1; i++) {
msg.payload = array[i];
msg.parts.index = node.c++;
if (node.stream !== true) { msg.parts.count = array.length; }
node.send(RED.util.cloneMessage(msg));
send(RED.util.cloneMessage(msg));
}
if (node.stream !== true) {
msg.payload = array[i];
msg.parts.index = node.c++;
msg.parts.count = array.length;
node.send(RED.util.cloneMessage(msg));
send(RED.util.cloneMessage(msg));
node.c = 0;
}
else { node.remainder = array[i]; }
@@ -67,7 +67,8 @@ module.exports = function(RED) {
}
node.c = 0;
node.buffer = Buffer.from([]);
this.on("input", function(msg) {
node.pendingDones = [];
this.on("input", function(msg, send, done) {
if (msg.hasOwnProperty("payload")) {
if (msg.hasOwnProperty("parts")) { msg.parts = { parts:msg.parts }; } // push existing parts to a stack
else { msg.parts = {}; }
@@ -93,14 +94,23 @@ module.exports = function(RED) {
msg.payload = data.substring(pos,pos+node.splt);
msg.parts.index = node.c++;
pos += node.splt;
node.send(RED.util.cloneMessage(msg));
send(RED.util.cloneMessage(msg));
}
if (count > 1) {
node.pendingDones.forEach(d => d());
node.pendingDones = [];
}
node.remainder = data.substring(pos);
if ((node.stream !== true) || (node.remainder.length === node.splt)) {
msg.payload = node.remainder;
msg.parts.index = node.c++;
node.send(RED.util.cloneMessage(msg));
send(RED.util.cloneMessage(msg));
node.pendingDones.forEach(d => d());
node.pendingDones = [];
done();
node.remainder = "";
} else {
node.pendingDones.push(done);
}
}
else {
@@ -115,7 +125,8 @@ module.exports = function(RED) {
a = msg.payload.split(node.splt);
msg.parts.ch = node.splt; // pass the split char to other end for rejoin
}
sendArray(node,msg,a);
sendArray(node,msg,a,send);
done();
}
}
else if (Array.isArray(msg.payload)) { // then split array into messages
@@ -135,8 +146,9 @@ module.exports = function(RED) {
}
msg.parts.index = i;
pos += node.arraySplt;
node.send(RED.util.cloneMessage(msg));
send(RED.util.cloneMessage(msg));
}
done();
}
else if ((typeof msg.payload === "object") && !Buffer.isBuffer(msg.payload)) {
var j = 0;
@@ -152,10 +164,11 @@ module.exports = function(RED) {
msg.parts.key = p;
msg.parts.index = j;
msg.parts.count = l;
node.send(RED.util.cloneMessage(msg));
send(RED.util.cloneMessage(msg));
j += 1;
}
}
done();
}
else if (Buffer.isBuffer(msg.payload)) {
var len = node.buffer.length + msg.payload.length;
@@ -176,14 +189,23 @@ module.exports = function(RED) {
msg.payload = buff.slice(pos,pos+node.splt);
msg.parts.index = node.c++;
pos += node.splt;
node.send(RED.util.cloneMessage(msg));
send(RED.util.cloneMessage(msg));
}
if (count > 1) {
node.pendingDones.forEach(d => d());
node.pendingDones = [];
}
node.buffer = buff.slice(pos);
if ((node.stream !== true) || (node.buffer.length === node.splt)) {
msg.payload = node.buffer;
msg.parts.index = node.c++;
node.send(RED.util.cloneMessage(msg));
send(RED.util.cloneMessage(msg));
node.pendingDones.forEach(d => d());
node.pendingDones = [];
done();
node.buffer = Buffer.from([]);
} else {
node.pendingDones.push(done);
}
}
else {
@@ -210,23 +232,34 @@ module.exports = function(RED) {
while (pos > -1) {
msg.payload = buff.slice(p,pos);
msg.parts.index = node.c++;
node.send(RED.util.cloneMessage(msg));
send(RED.util.cloneMessage(msg));
i++;
p = pos+node.splt.length;
pos = buff.indexOf(node.splt,p);
}
if (count > 1) {
node.pendingDones.forEach(d => d());
node.pendingDones = [];
}
if ((node.stream !== true) && (p < buff.length)) {
msg.payload = buff.slice(p,buff.length);
msg.parts.index = node.c++;
msg.parts.count = node.c++;
node.send(RED.util.cloneMessage(msg));
send(RED.util.cloneMessage(msg));
node.pendingDones.forEach(d => d());
node.pendingDones = [];
}
else {
node.buffer = buff.slice(p,buff.length);
node.pendingDones.push(done);
}
if (node.buffer.length == 0) {
done();
}
}
} else { // otherwise drop the message.
done();
}
//else { } // otherwise drop the message.
}
});
}
@@ -264,16 +297,16 @@ module.exports = function(RED) {
}
function reduceMessageGroup(node,msgs,exp,fixup,count,accumulator,done) {
var msg = msgs.shift();
exp.assign("I", msg.parts.index);
function reduceMessageGroup(node,msgInfos,exp,fixup,count,accumulator,done) {
var msgInfo = msgInfos.shift();
exp.assign("I", msgInfo.msg.parts.index);
exp.assign("N", count);
exp.assign("A", accumulator);
RED.util.evaluateJSONataExpression(exp, msg, (err,result) => {
RED.util.evaluateJSONataExpression(exp, msgInfo.msg, (err,result) => {
if (err) {
return done(err);
}
if (msgs.length === 0) {
if (msgInfos.length === 0) {
if (fixup) {
fixup.assign("N", count);
fixup.assign("A", result);
@@ -281,39 +314,43 @@ module.exports = function(RED) {
if (err) {
return done(err);
}
node.send({payload: result});
msgInfo.send({payload: result});
done();
});
} else {
node.send({payload: result});
msgInfo.send({payload: result});
done();
}
} else {
reduceMessageGroup(node,msgs,exp,fixup,count,result,done);
reduceMessageGroup(node,msgInfos,exp,fixup,count,result,done);
}
});
}
function reduceAndSendGroup(node, group, done) {
var is_right = node.reduce_right;
var flag = is_right ? -1 : 1;
var msgs = group.msgs;
var msgInfos = group.msgs;
const preservedMsgInfos = [...msgInfos];
try {
RED.util.evaluateNodeProperty(node.exp_init, node.exp_init_type, node, {}, (err,accum) => {
var reduceExpression = node.reduceExpression;
var fixupExpression = node.fixupExpression;
var count = group.count;
msgs.sort(function(x,y) {
var ix = x.parts.index;
var iy = y.parts.index;
msgInfos.sort(function(x,y) {
var ix = x.msg.parts.index;
var iy = y.msg.parts.index;
if (ix < iy) {return -flag;}
if (ix > iy) {return flag;}
return 0;
});
reduceMessageGroup(node, msgs,reduceExpression,fixupExpression,count,accum,(err,result) => {
reduceMessageGroup(node, msgInfos,reduceExpression,fixupExpression,count,accum,(err,result) => {
if (err) {
preservedMsgInfos.pop(); // omit last message to emit error message
preservedMsgInfos.forEach(mInfo => mInfo.done());
done(err);
return;
} else {
preservedMsgInfos.forEach(mInfo => mInfo.done());
done();
}
})
@@ -323,7 +360,8 @@ module.exports = function(RED) {
}
}
function reduceMessage(node, msg, done) {
function reduceMessage(node, msgInfo, done) {
let msg = msgInfo.msg;
if (msg.hasOwnProperty('parts')) {
var parts = msg.parts;
var pending = node.pending;
@@ -344,7 +382,7 @@ module.exports = function(RED) {
if (parts.hasOwnProperty('count') && (group.count === undefined)) {
group.count = parts.count;
}
msgs.push(msg);
msgs.push(msgInfo);
pending_count++;
var completeProcess = function(err) {
if (err) {
@@ -353,6 +391,13 @@ module.exports = function(RED) {
node.pending_count = pending_count;
var max_msgs = maxKeptMsgsCount(node);
if ((max_msgs > 0) && (pending_count > max_msgs)) {
Object.values(node.pending).forEach(group => {
group.msgs.forEach(mInfo => {
if (mInfo.msg._msgid !== msgInfo.msg._msgid) {
mInfo.done();
}
});
});
node.pending = {};
node.pending_count = 0;
done(RED._("join.too-many"));
@@ -368,7 +413,8 @@ module.exports = function(RED) {
completeProcess();
}
} else {
node.send(msg);
msgInfo.send(msg);
msgInfo.done();
done();
}
}
@@ -480,7 +526,9 @@ module.exports = function(RED) {
delete group.msg.parts;
}
delete group.msg.complete;
node.send(RED.util.cloneMessage(group.msg));
group.send(RED.util.cloneMessage(group.msg));
group.dones.forEach(f => f());
group.dones = [];
}
var pendingMessages = [];
@@ -489,10 +537,10 @@ module.exports = function(RED) {
// groups may overlap and cause unexpected results. The use of JSONata
// means some async processing *might* occur if flow/global context is
// accessed.
var processReduceMessageQueue = function(msg) {
if (msg) {
var processReduceMessageQueue = function(msgInfo) {
if (msgInfo) {
// A new message has arrived - add it to the message queue
pendingMessages.push(msg);
pendingMessages.push(msgInfo);
if (activeMessage !== null) {
// The node is currently processing a message, so do nothing
// more with this message
@@ -508,25 +556,21 @@ module.exports = function(RED) {
// There are more messages to process. Get the next message and
// start processing it. Recurse back in to check for any more
var nextMsg = pendingMessages.shift();
var nextMsgInfo = pendingMessages.shift();
activeMessage = true;
reduceMessage(node, nextMsg, err => {
reduceMessage(node, nextMsgInfo, err => {
if (err) {
node.error(err,nextMsg);
nextMsgInfo.done(err);//.error(err,nextMsg);
}
activeMessage = null;
processReduceMessageQueue();
})
}
this.on("input", function(msg) {
this.on("input", function(msg, send, done) {
try {
var property;
if (node.mode === 'auto' && (!msg.hasOwnProperty("parts")||!msg.parts.hasOwnProperty("id"))) {
node.warn("Message missing msg.parts property - cannot join in 'auto' mode")
return;
}
var partId = "_";
if (node.propertyType == "full") {
property = msg;
}
@@ -535,11 +579,26 @@ module.exports = function(RED) {
property = RED.util.getMessageProperty(msg,node.property);
} catch(err) {
node.warn("Message property "+node.property+" not found");
done();
return;
}
}
var partId;
if (node.mode === 'auto' && (!msg.hasOwnProperty("parts")||!msg.parts.hasOwnProperty("id"))) {
// if a blank reset messag erest it all.
if (msg.hasOwnProperty("reset")) {
if (inflight && inflight.hasOwnProperty("partId") && inflight[partId].timeout) {
clearTimeout(inflight[partId].timeout);
}
inflight = {};
}
else {
node.warn("Message missing msg.parts property - cannot join in 'auto' mode")
}
done();
return;
}
var payloadType;
var propertyKey;
var targetCount;
@@ -557,11 +616,10 @@ module.exports = function(RED) {
propertyIndex = msg.parts.index;
}
else if (node.mode === 'reduce') {
return processReduceMessageQueue(msg);
return processReduceMessageQueue({msg, send, done});
}
else {
// Use the node configuration to identify all of the group information
partId = "_";
payloadType = node.build;
targetCount = node.count;
joinChar = node.joiner;
@@ -571,6 +629,9 @@ module.exports = function(RED) {
if (node.build === 'object') {
propertyKey = RED.util.getMessageProperty(msg,node.key);
}
if (msg.hasOwnProperty("parts")) {
propertyIndex = msg.parts.index;
}
}
if (msg.hasOwnProperty("reset")) {
@@ -578,9 +639,11 @@ module.exports = function(RED) {
if (inflight[partId].timeout) {
clearTimeout(inflight[partId].timeout);
}
inflight[partId].dones.forEach(f => f());
delete inflight[partId]
}
return
done();
return;
}
if ((payloadType === 'object') && (propertyKey === null || propertyKey === undefined || propertyKey === "")) {
@@ -591,6 +654,7 @@ module.exports = function(RED) {
if (msg.hasOwnProperty('complete')) {
if (inflight[partId]) {
inflight[partId].msg.complete = msg.complete;
inflight[partId].send = send;
completeSend(partId);
}
}
@@ -598,6 +662,7 @@ module.exports = function(RED) {
node.warn("Message missing key property 'msg."+node.key+"' - cannot add to object")
}
}
done();
return;
}
@@ -608,7 +673,9 @@ module.exports = function(RED) {
payload:{},
targetCount:targetCount,
type:"object",
msg:RED.util.cloneMessage(msg)
msg:RED.util.cloneMessage(msg),
send: send,
dones: []
};
}
else {
@@ -617,7 +684,9 @@ module.exports = function(RED) {
payload:[],
targetCount:targetCount,
type:payloadType,
msg:RED.util.cloneMessage(msg)
msg:RED.util.cloneMessage(msg),
send: send,
dones: []
};
if (payloadType === 'string') {
inflight[partId].joinChar = joinChar;
@@ -634,6 +703,7 @@ module.exports = function(RED) {
}, node.timer)
}
}
inflight[partId].dones.push(done);
var group = inflight[partId];
if (payloadType === 'buffer') {
@@ -642,7 +712,7 @@ module.exports = function(RED) {
inflight[partId].bufferLen += property.length;
}
else {
node.error(RED._("join.errors.invalid-type",{error:(typeof property)}),msg);
done(RED._("join.errors.invalid-type",{error:(typeof property)}));
return;
}
}
@@ -666,8 +736,8 @@ module.exports = function(RED) {
}
} else {
if (!isNaN(propertyIndex)) {
if (group.payload[propertyIndex] == undefined) { group.currentCount++; }
group.payload[propertyIndex] = property;
group.currentCount++;
} else {
if (property !== undefined) {
group.payload.push(property);
@@ -676,6 +746,7 @@ module.exports = function(RED) {
}
}
group.msg = Object.assign(group.msg, msg);
group.send = send;
var tcnt = group.targetCount;
if (msg.hasOwnProperty("parts")) {
tcnt = group.targetCount || msg.parts.count;
@@ -686,6 +757,7 @@ module.exports = function(RED) {
}
}
catch(err) {
done(err);
console.log(err.stack);
}
});
@@ -694,10 +766,10 @@ module.exports = function(RED) {
for (var i in inflight) {
if (inflight.hasOwnProperty(i)) {
clearTimeout(inflight[i].timeout);
inflight[i].dones.forEach(d => d());
}
}
});
}
RED.nodes.registerType("join",JoinNode);
}

View File

@@ -81,16 +81,16 @@ module.exports = function(RED) {
function sortMessageGroup(group) {
var promise;
var msgs = group.msgs;
var msgInfos = group.msgInfos;
if (key_is_exp) {
var evaluatedDataPromises = msgs.map(msg => {
var evaluatedDataPromises = msgInfos.map(mInfo => {
return new Promise((resolve,reject) => {
RED.util.evaluateJSONataExpression(key_exp, msg, (err, result) => {
RED.util.evaluateJSONataExpression(key_exp, mInfo.msg, (err, result) => {
if (err) {
reject(RED._("sort.invalid-exp",{message:err.toString()}));
} else {
resolve({
item: msg,
item: mInfo,
sortValue: result
})
}
@@ -106,20 +106,21 @@ module.exports = function(RED) {
var key = function(msg) {
return ;
}
var comp = generateComparisonFunction(msg => RED.util.getMessageProperty(msg, key_prop));
var comp = generateComparisonFunction(mInfo => RED.util.getMessageProperty(mInfo.msg, key_prop));
try {
msgs.sort(comp);
msgInfos.sort(comp);
}
catch (e) {
return; // not send when error
}
promise = Promise.resolve(msgs);
promise = Promise.resolve(msgInfos);
}
return promise.then(msgs => {
for (var i = 0; i < msgs.length; i++) {
var msg = msgs[i];
return promise.then(msgInfos => {
for (let i = 0; i < msgInfos.length; i++) {
const msg = msgInfos[i].msg;
msg.parts.index = i;
node.send(msg);
msgInfos[i].send(msg);
msgInfos[i].done();
}
});
}
@@ -181,65 +182,79 @@ module.exports = function(RED) {
}
}
if(oldest !== undefined) {
oldest.msgInfos[oldest.msgInfos.length - 1].done(RED._("sort.too-many"));
for (let i = 0; i < oldest.msgInfos.length - 1; i++) {
oldest.msgInfos[i].done();
}
delete pending[oldest_key];
return oldest.msgs.length;
return oldest.msgInfos.length;
}
return 0;
}
function processMessage(msg) {
function processMessage(msgInfo) {
const msg = msgInfo.msg;
if (target_is_prop) {
sortMessageProperty(msg).then(send => {
if (send) {
node.send(msg);
msgInfo.send(msg);
}
msgInfo.done();
}).catch(err => {
node.error(err,msg);
msgInfo.done(err);
});
return;
}
var parts = msg.parts;
if (!parts || !parts.hasOwnProperty("id") || !parts.hasOwnProperty("index")) {
msgInfo.done();
return;
}
var gid = parts.id;
if (!pending.hasOwnProperty(gid)) {
pending[gid] = {
count: undefined,
msgs: [],
msgInfos: [],
seq_no: pending_id++
};
}
var group = pending[gid];
var msgs = group.msgs;
msgs.push(msg);
var msgInfos = group.msgInfos;
msgInfos.push(msgInfo);
if (parts.hasOwnProperty("count")) {
group.count = parts.count;
}
pending_count++;
if (group.count === msgs.length) {
if (group.count === msgInfos.length) {
delete pending[gid]
sortMessageGroup(group).catch(err => {
node.error(err,msg);
// throw an error for last message, and just call done() for remaining messages
msgInfos[msgInfos.length-1].done(err);
for (let i = 0; i < msgInfos.length - 1; i++) {
msgInfos[i].done()
};
});
pending_count -= msgs.length;
pending_count -= msgInfos.length;
} else {
var max_msgs = max_kept_msgs_count(node);
if ((max_msgs > 0) && (pending_count > max_msgs)) {
pending_count -= removeOldestPending();
node.error(RED._("sort.too-many"), msg);
}
}
}
this.on("input", function(msg) {
processMessage(msg);
this.on("input", function(msg, send, done) {
processMessage({msg, send, done});
});
this.on("close", function() {
for(var key in pending) {
if (pending.hasOwnProperty(key)) {
node.log(RED._("sort.clear"), pending[key].msgs[0]);
node.log(RED._("sort.clear"), pending[key].msgInfos[0]);
const group = pending[key];
group.msgInfos.forEach(mInfo => {
mInfo.done();
});
delete pending[key];
}
}

Some files were not shown because too many files have changed in this diff Show More