diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 07efaf18e..bdb978517 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -32,3 +32,4 @@ To help us understand the issue, please fill-in as much of the following informa - [ ] npm version: - [ ] Platform/OS: - [ ] Browser: +- [ ] running in Docker: diff --git a/.github/ISSUE_TEMPLATE/--bug_report.md b/.github/ISSUE_TEMPLATE/--bug_report.md index 63923455e..7cdc9caad 100644 --- a/.github/ISSUE_TEMPLATE/--bug_report.md +++ b/.github/ISSUE_TEMPLATE/--bug_report.md @@ -1,6 +1,6 @@ --- name: Bug report -about: Reproducable software issues in the core of Node-RED +about: Reproducible software issues in the core of Node-RED title: '' labels: '' assignees: '' diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 05fdacd51..92a0abb8a 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -29,6 +29,6 @@ the [forum](https://discourse.nodered.org) or - [ ] I have read the [contribution guidelines](https://github.com/node-red/node-red/blob/master/CONTRIBUTING.md) -- [ ] For non-bugfix PRs, I have discussed this change on the mailing list/slack team. +- [ ] For non-bugfix PRs, I have discussed this change on the forum/slack team. - [ ] I have run `grunt` to verify the unit tests pass - [ ] I have added suitable unit tests to cover the new/changed functionality diff --git a/.gitignore b/.gitignore index c3fa9624a..7b1f1f219 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,4 @@ packages/node_modules/@node-red/editor-client/public !test/**/node_modules docs !packages/node_modules/**/docs +.vscode \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index a101eecb0..5a8eebb8a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,6 +2,7 @@ sudo: false language: node_js matrix: include: + - node_js: "14" - node_js: "12" - node_js: "10" script: diff --git a/CHANGELOG.md b/CHANGELOG.md index b7b75d30f..9d24243fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,113 @@ +#### 1.0.6: Maintenance Release + +Runtime + + - Update to JSONata 1.8.3 + - #2536 Handle clone of null in utils + +Editor + + - Prevent button label wrapping in typedInput + - Handle error objects when reporting in palette manager + +Nodes + + - Inject: Revert to cron 1.7.2 + - UDP: when reusing input socket honour the broadcast mode. + +#### 1.0.5: Maintenance Release + +Runtime + + - #2500 Support for context stores using JSONata and evaluateNodeProperty() + - Add better handling of host-key-verify error with projects + - #2517 Handle false values in $env() properly + - #2514 Ensure complete node scope is remapped in subflows + - #2513 Flows/subflows must preinitialise their context objects + - Clear node.close timeout to avoid unnecessary work on restart + - #2532 Set flow.disabled when disabled property is false + - #2522 Ensure file context does not write 'undefined' to store + +Editor + + - #2489 Fix XPath in UI tests + - #2504 Fix paletteCategories order + - #2501 Add page objects for UI testing + - #2494 Check node props when deciding if pasted node can splice links + - #2521 Don't double-sanitize node name in debug sidebar + - #2519 German i18n updates + - #2523 Update nodeTabMap when replacing unknown nodes + - Update TypedInput to use flexbox and remove resizing code + - Handle nodes with no wires array + - Do not collapse whitespace in Debug string messages + +Nodes + + - File: Remove old legacy wording from file node info to stop confusing users. + - Join: Ensure join node handles missing buffer joiner when not in string mode + - Exec: make exec node logging consistent with itself. (only be verbose when in verbose mode) + - Trigger: reset default timeout value when switching away from wait for reset + - Join: Fix join to not crash on appending invalid types to buffer. + - MQTT out: Add warning if topic contains + or # + - #2502 WebSocket i18n update + - #2508 Add Japanese translation for join node + - TCP out: tidy up select of which rows to display + + +#### 1.0.4: Maintenance Release + +Runtime + + - Update all dependencies to latest fix versions + - Update JSONata to 1.8.1 + - #2473 Handle httpAdminRoot missing ending slash with login strategy Fixes + - #2470 Update https-proxy-agent + - #2461 Allow credentials to be provided as part of /flows api + - #2444 Move receive metric position to better reflect async changes Fixes + - #2406 Improve file store error when cache disabled and sync api used Closes + - #2399 cloneMessage should handle undefined without throwing err Fixes + - #2418 Fix the library api routes to prevent too broad matching of regex URLs + - #2417 Remove undefined loadFlowLibrary call + +Editor + + - #2465 Add better regex highlighting in jsonata edit mode Fixes + - Add regex awareness to jsonata formatter + - #2472 Avoid adding extra newlines when formating jsonata Fixes + - #2475 Add UI test case for error handling + - Avoid adding extra divs to edit form to avoid size miscalculation + - Upgrade to latest marked and dompurify libs + - Ensure catalog load errors are logged to the console + - #2460 Track context sidebar element paths to track formatting changes Fixes + - Battling Chrome Autocomplete, part 31: Wrap search input with form + - #2445 Trick chrome into autofilling dummy username/password inputs Fixes + - #2457 Fix garbled characters in library + - #2409 Filter palette using raw label not html formatted label Fixes + - #2400 Wrap long context values when displaying in sidebar Fixes + - Fix duplicating array item in visual json editor + - #2338 Modify history sidebar button positioning to handle long labels Fixes + - #2438 Add some auto-complete snippets to the nrjavascript mode Close + - #2430 Ignore disabled nodes when checking for invalid configs on deploy Closes + - #2442 #2458 #2453 Update zh-CN translations + - #2235 Add initial zh-TW translation + - Re-enable jshint on editor and fixup issues + - #2431 Remove unnecessary namespaces for i18n + - #2440 Support BrowserStack in UI testing + - #2358 Add path property to debug messages Fixes + - Fix false change detection when no config node selected + - Fix IME bug in text editor + - Make node highlighting a bit more obvious for busy flows + - #2392 Add icons and support i18n in typedInput of JSON editor + +Nodes + - #2462 MQTT: Ensure IPv6 broker names are wrapped in brackets Fixes + - Join node - check existance before clearing timeout + - Trigger: Complete 2nd msg when set to send latest + - TCP: clarify text regarding blank parameters. + - #2449 HTTP Request: Add HEAD as Method + - Make min-height for change, switch, batch and mqtt consistent + + #### 1.0.3: Maintenance Release Runtime @@ -44,7 +154,7 @@ Runtime Editor - Ensure node status is refreshed whenever node is edited - - #2315 #2316 Ensure z property included in full message debug payload + - #2316 Ensure z property included in full message debug payload #2315 - #2321 Fixed editor.json (JA nls) - #2313 Fix element to collapse items in visual JSON editor - #2314 Insert divider in menu by calling RED.menu.addItem('id', null); @@ -113,9 +223,9 @@ Editor - Move flow-status button to footer for consistency - Fix node hover effect to prevent jumping position - Filter quick-add properly when splicing a wire - - Mark workspace dirty when deleting link node link Fixes #2274 + - #2274 Mark workspace dirty when deleting link node link Fixes - Add red-ui-button class to strategy login button - - Fix padding of subflow locale select Closes #2276 + - #2276 Fix padding of subflow locale select Closes - Update info text of complete node & add JP text - Add class red-ui-button to cancel button - Add css class to login submit button (#2275) @@ -138,12 +248,12 @@ Runtime - [FEATURE] Add Node Done API - make message passing async - Ensure the subflow stop promise is waiting for before restarting - Limit the regex for the /nodes/ api end points - - Add error event handler to ssh-keygen child_process Fixes #2255 - - Fix default value handling on context array access Fixes #2252 + - #2255 Add error event handler to ssh-keygen child_process Fixes + - #2252 Fix default value handling on context array access Fixes - Remove all ui test dependencies from package.json - Add req back to audit log events and extend to Projects api - - Ensure 2nd arg to node.error is an object Fixes #2228 - - Use a more atomic process for writing context files Fixes #2271 + - #2228 Ensure 2nd arg to node.error is an object Fixes + - #2271 Use a more atomic process for writing context files Fixes Editor @@ -162,25 +272,25 @@ Editor - [FEATURE] add support for specifying subflow template color - [FEATURE] Use ctrl-click on wire to splice node in place - [FEATURE] Allow search results to show more than 25 results - - [FEATURE] Allow a node to change if it has an input port Closes #2268 - - Revealing node position needs to account for zoom level Fixes #2172 - - Fix typedInput option selection Fixes #2174 - - Fix palette node id handling so search works Fixes #2173 + - #2268 [FEATURE] Allow a node to change if it has an input port Closes + - #2172 Revealing node position needs to account for zoom level Fixes + - #2174 Fix typedInput option selection Fixes + - #2173 Fix palette node id handling so search works Fixes - Add popover tooltips to debug sidebar,function and template - Add popovers to context sidebar mini buttons - Ensure node status icon is shown when value set - Revert treeList children function signature change - Restore tray component css for compatibility. Mark as deprecated - fix function name & string compare function - - Handle empty list of example flows Fixes #2171 + - #2171 Handle empty list of example flows Fixes - Ensure library list has an item selected when opened - Ensure tooltip popover doesn't replace normal popover - Fix clipboard export download button - Ensure input box has focus on repeated quick add - Fix width calculation of typedInput - Remove some hardcoded css colors - - Fix display of node help when clicking in palette Fixes #2194 - - Ensure node help is loaded in the right language Fixes #2195 + - #2194 Fix display of node help when clicking in palette Fixes + - #2195 Ensure node help is loaded in the right language Fixes - Do not allow tab focus on clipboard hidden element - Fix undefined error on typedInput due to valueLabel used before being added - Fix undo of flow disable state change @@ -193,7 +303,7 @@ Editor - Update all node icons to SVG - Handle png/svg fallback for def.icon values. Remove old pngs - Ignore empty examples directories (don't add to import menu) - - better handle example file at any depth - #2222 + - #2222 better handle example file at any depth - - Properly escape node types in palette - Ensure session expiry timeout doesn't exceed limit - Use node/tab map to make filterNodes more efficient @@ -201,22 +311,22 @@ Editor - Handle undefined node.\_def in edit stack title. - fix converting selection to subflow - Fix inserting new subflow node to existing wire between nodes - - Support displaying falsey node status values Fixes #2246 + - #2246 Support displaying falsey node status values Fixes - Remove tab menu from node property UI for subflow and config nodes - - Mark workspace dirty when shift-click-drag detaches wires Fixes #2260 + - #2260 Mark workspace dirty when shift-click-drag detaches wires Fixes - Fix subflow category change on palette Nodes - Remove pi gpi, twitter, email and feedparser nodes from core - - Fix error handling in Websocket broadcast function Fixes #2182 + - #2182 Fix error handling in Websocket broadcast function Fixes - Handle websocket item being parseable but not an object better - stop join tripping up if last message of buffer is blank. - Add support for env var propety in switch node - Improve handling of file upload in request node - Add "has key" rule to switch node + tests - Optimise generation of switch node edit dialog - - Add keep-alive option to HTTP Request - #2261 + - #2261 Add keep-alive option to HTTP Request - #### 1.0.0-beta.2: Beta Release @@ -228,7 +338,7 @@ Runtime Runtime - Update runtime apis to support multiple libraries - Add Node 12 to travis (allow_failures) - - Bump all dependencies Fixes #2152 + - #2152 Bump all dependencies Fixes Editor - [BREAKING] complete overhaul of editor DOM/CSS structure @@ -240,17 +350,17 @@ Editor - Allow script tags with src to reference esm modules - Upgrade to jq 3.4.1 / jq-ui 1.12.1 - Allow editor language to be chosen in editor settings - - Only NLS status text that starts with a letter Fixes #2128 - - Fix display of link node list within subflow Fixes #2140 - - Blur the active element when closing edit dialog via action Fixes #2097 - - Trigger change evnt on typedInput when type changes and options present Fixes #2160 + - #2128 Only NLS status text that starts with a letter Fixes + - #2140 Fix display of link node list within subflow Fixes + - #2097 Blur the active element when closing edit dialog via action Fixes + - #2160 Trigger change evnt on typedInput when type changes and options present Fixes - Move library import/export to single dialog - Move type-library dialogs to new style dialog - Fix node drag and drop animation - let status be simple text if wanted - Add workspace statusBar - Complete refresh of German translations - - Fix memory leak in Debug sidebar #2163 + - #2163 Fix memory leak in Debug sidebar - Introduce toggleButton and move flow-disabled to use it - Allow RED.settings.get/set to use full property desc - Add auto-refresh toggle to context sidebar @@ -269,26 +379,26 @@ Nodes #### 0.20.8: Maintenance Release - Sanitize tab name in edit dialog - - Pass httpServer to runtime even when httpAdmin disabled Fixes #2272 + - #2272 Pass httpServer to runtime even when httpAdmin disabled Fixes #### 0.20.7: Maintenance Release - - Update jsonata to 1.6.5 which should fix #2183 + - #2183 Update jsonata to 1.6.5 which should fix - Ensure the subflow stop promise is waiting for before restarting - Properly escape node types in palette #### 0.20.6: Maintenance Release - - Revealing node position needs to account for zoom level Fixes #2172 + - #2172 Revealing node position needs to account for zoom level Fixes - stop join tripping up if last message of buffer is blank. - Improve handling of file upload in request node - - Handle subflow internal node wired to a non-existant node Fixes #2202 + - #2202 Handle subflow internal node wired to a non-existant node Fixes - Do not save subflow env vars with blank names - Don't allow a link node virtual wire to connect to normal port - - Clear HTTP Request node authType when auth disabled Fixes #2215 - - Fix parsing of content-type header Fixes #2216 + - #2215 Clear HTTP Request node authType when auth disabled Fixes + - #2216 Fix parsing of content-type header Fixes - Fix join node reset issue with merging objects - - Copy data-i18n attribute on TypedInput Fixes #2211 + - #2211 Copy data-i18n attribute on TypedInput Fixes #### 0.20.5: Maintenance Release @@ -325,27 +435,27 @@ Nodes #### 0.20.1: Maintenance Release - - Ensure all subflow instances are stopped when flow stopping Fixes #2095 - - modify name of korean locale forders #2091 + - #2095 Ensure all subflow instances are stopped when flow stopping Fixes + - #2091 modify name of korean locale forders - Ensure node names are sanitized before being presented - - Subflow status node must pass status to parent flow Fixes #2087 - - fix problem on displaying option label on Firefox #2090 + - #2087 Subflow status node must pass status to parent flow Fixes + - #2090 fix problem on displaying option label on Firefox #### 0.20.0: Milestone Release Runtime - Pass complete status to Status node and filter to editor - - Ensure flows wait for all nodes to close before restarting Fixes #2067 + - #2067 Ensure flows wait for all nodes to close before restarting Fixes - Fix git clone with password protected key - Allow a project to be located below the root of repo - Detect the cloning of an empty git repo properly - Fix use of custom auth strategy plugins - - Remove remnants of when library in git/index Fixes #2057 + - #2057 Remove remnants of when library in git/index Fixes - Clear subflow status on close - Add exportGlobalContextKeys to prevent exposing functionGlobalContext keys - Add --no-audit and --no-update-notifier flags to npm commands to reduce workload - Add envVarExcludes setting to block named env vars - - Update settings.js docs on userDir to match reality Fixes #2082 + - #2082 Update settings.js docs on userDir to match reality Fixes - Add Korean Language @@ -389,16 +499,16 @@ Editor - Add env type to subflow env var types - Display parent subflow properties in edit dialog - Fix direction value of subflow output - - Add Status Node to Subflow to allow subflow-specific status Closes #597 - - Better handling of multiple flow merges Fixes #2039 + - #597 Add Status Node to Subflow to allow subflow-specific status Closes + - #2039 Better handling of multiple flow merges Fixes Nodes - Various translation updates - - Catch: Add 'catch uncaught only' mode. Closes #1747 + - #1747 Catch: Add 'catch uncaught only' mode. Closes - Link: scroll to current flow in node list - HTTPRequest: add option to urlencode cookies - - HTTPRequest: option to use msg.payload as query params on GET. #1981 + - #1981 HTTPRequest: option to use msg.payload as query params on GET. - Debug: Add local time display option to numerics in debug window - MQTT: Add parsed JSON output option @@ -416,9 +526,9 @@ Editor - German translation - Change default dropdown appearance and sidebar tab menu handling - - Handle multiple-select box when nothing selected Fixes #2021 - - Handle i18n properly when key is a valid sub-identifier Fixes #2028 - - Avoid duplicate links when missing node type installed Fixes #2032 + - #2021 Handle multiple-select box when nothing selected Fixes + - #2028 Handle i18n properly when key is a valid sub-identifier Fixes + - #2032 Avoid duplicate links when missing node type installed Fixes - Add View Tools - Don't collapse version control header when clicking refresh - Add fast entry via keyboard for string of nodes @@ -438,15 +548,15 @@ Editor - Update palette manager view properly when module updated - Add TreeList common widget - - Fix visual jump when opening Comment editor on Safari Part of #2008 - - Fix vertical align of markdown editor in Safari Fixes #2008 - - Avoid marking node as changed if label state is default Fixes #2009 + - #2008 Fix visual jump when opening Comment editor on Safari Part of + - #2008 Fix vertical align of markdown editor in Safari Fixes + - #2009 Avoid marking node as changed if label state is default Fixes - Highlight port on node hover while joining - Support drag-wiring of link nodes - Allow TypeSearch to include a filter option - Improve diff colouring - Allow sections to toggle in 2-element stack - - Add support for ${} env var syntax when skipping validation Closes #1980 + - #1980 Add support for ${} env var syntax when skipping validation Closes - i18 support for markdown editor tooltip - Add RED.editor.registerTypeEditor for custom type editors - Tidy up markdown toolbar handling across all editors @@ -456,15 +566,15 @@ Editor Runtime - - Bump JSONata to 1.6.4 Fixes #2023 + - #2023 Bump JSONata to 1.6.4 Fixes - Add audit logging to admin api - - Fix failure of RED.require #2010 - - Allow oauth strategy callback method to be customised Closes #1998 - - Ensure fs context cache is flushed on close Fixes #2001 + - #2010 Fix failure of RED.require + - #1998 Allow oauth strategy callback method to be customised Closes + - #2001 Ensure fs context cache is flushed on close Fixes - Fix library Buffer( to Buffer.alloc( for node 10 - Catch file-not-found on startup when non-existant flow file specified - Actively expire login sesssions and notify user - - Add quotation marks for basic auth challenge #1976 + - #1976 Add quotation marks for basic auth challenge Nodes @@ -482,7 +592,7 @@ Nodes Editor - Allow the editor to use a custom admin api url root - - Improve performance of Flow Diff dialog - @TothiViseo #1989 + - #1989 Improve performance of Flow Diff dialog - @TothiViseo - Add 'open project' option to Projects Welcome dialog - Add 'type already registered' check in palette editor - Handle missing tab.disabled property @@ -500,11 +610,11 @@ Editor - Allow left-hand node button to act as toggle - Support dbl-click in tab bar to add new flow in position - Fix duplicate subflow detection on import - - Add import notification with info on what has been imported Closes #1862 + - #1862 Add import notification with info on what has been imported Closes - Show error details when trying to import invalid json - Show default icon when non-existent font-awesome icon was specified - Add configurable option for showing node label - - Avoid http redirects as Safari doesn't reuse Auth header Fixes #1903 + - #1903 Avoid http redirects as Safari doesn't reuse Auth header Fixes - Tidy up ace tooltip styling - Add event log to editor - Add tooltips to multiple editor elements @@ -532,30 +642,30 @@ Editor Runtime - Allow a project to be loaded from cmdline - - Handle lookup of undefined property in Global context Fixes #1978 + - #1978 Handle lookup of undefined property in Global context Fixes - Refuse to enable Manage Palette if npm too old - Remove restriction on upgrading non-local modules - - Remove deprecated Buffer constructor usage Fixes #1709 + - #1709 Remove deprecated Buffer constructor usage Fixes - Update httpServerOptions doc in settings.js - Exclude non-testable .js files from the unit tests - Add --safe mode flag to allow starting without flows running - - Add setting-defined accessToken for automated access to the adminAPI - #1789 + - #1789 Add setting-defined accessToken for automated access to the adminAPI - Nodes - - Move all core node EN help to their own locale files - #1990 + - #1990 Move all core node EN help to their own locale files - - CSV: better regex for number detection - Debug: hide button if not configured to send to sidebar - Delay: report queue activity when in by-topic mode - Delay: add msg.flush mode - Exec: Preserve existing properties on msg object - File: remove CR/LF from incoming filename - - Function: create custom ace javascript mode to handle ES6 Fixes #1911 + - #1911 Function: create custom ace javascript mode to handle ES6 Fixes - Function: add env.get - - HTTP Request: Add http-proxy config #1913 + - #1913 HTTP Request: Add http-proxy config - HTTP Request: add msg.redirectList to output - - HTTP Request: add msg.requestTimeout option for per-message setting - @natcl #1959 - - MQTT: add auto-detect and base64 output to mqtt node Fixes #1912 - @DurandA + - #1959 HTTP Request: add msg.requestTimeout option for per-message setting - @natcl + - #1912 - @DurandA MQTT: add auto-detect and base64 output to mqtt node Fixes - MQTT: only unsubscribe node that is being removed - Sentiment: move to node-red-node-sentiment - Switch: add missing edit dialog icon @@ -570,48 +680,48 @@ Nodes #### 0.19.6: Maintenance Release - - Fix encoding of file node from binary to utf8 - #2051 + - #2051 Fix encoding of file node from binary to utf8 - #### 0.19.5: Maintenance Release - Recognize pip installs of RPi.GPIO (#1934) - - Merge pull request #1941 from node-red-hitachi/master-batch - - Merge pull request #1931 from node-red-hitachi/master-typedinput + - #1941 from node-red-hitachi/master-batch Merge pull request + - #1931 from node-red-hitachi/master-typedinput Merge pull request - Set min value of properties and spinners for batch - Fix that unnecessary optionMenu remains - - Merge pull request #1894 from node-red-hitachi/fix-overlapping-file-node-execution - - Merge pull request #1924 from imZack/patch-1 + - #1894 from node-red-hitachi/fix-overlapping-file-node-execution Merge pull request + - #1924 from imZack/patch-1 Merge pull request - Add missing comma - - Do not disable context sidebar during node edit Fixes #1921 - - Don't allow virtual links to be spliced Fixes #1920 + - #1921 Do not disable context sidebar during node edit Fixes + - #1920 Don't allow virtual links to be spliced Fixes - Merge project package changes to avoid overwritten changes - - Handle manually added project deps that are unused Fixes #1908 + - #1908 Handle manually added project deps that are unused Fixes - update close & input handling of File node - make close handler argument only one - - Merge pull request #1907 from amilajack/patch-2 + - #1907 from amilajack/patch-2 Merge pull request - Change repo badge to point to master branch - invoke callbacks if async handler is specified - - Merge pull request #1891 from camlow325/resolve-example-path-for-windows-support - - Merge pull request #1900 from kazuhitoyokoi/master-addtestcases4settings.js + - #1891 from camlow325/resolve-example-path-for-windows-support Merge pull request + - #1900 from kazuhitoyokoi/master-addtestcases4settings.js Merge pull request - wait closing while pending messages exist - Add test cases for red/api/editor/settings.js - - Ensure all palette categories are opened properly Closes #1893 + - #1893 Ensure all palette categories are opened properly Closes - Resolve path when sending example file for Windows support - fix multiple input message processing of file node #### 0.19.4: Maintenance Release - - Fix race condition in non-cache lfs context Fixes #1888 + - #1888 Fix race condition in non-cache lfs context Fixes - LocalFileSystem Context: Remove extra flush code - Prevent race condition in caching mode of lfs context (#1889) - Allow context store name to be provided in the key - Switch node: only use promises when absolutely necessary - Fix dbl-click handling on webkit-based browsers - Ensure context.flow/global cannot be deleted or enumerated - - Handle context.get with multiple levels of unknown key Fixes #1883 + - #1883 Handle context.get with multiple levels of unknown key Fixes - Fix global.get("foo.bar") for functionGlobalContext set values - Fix node color bug (#1877) - - Merge pull request #1857 from cclauss/patch-1 + - #1857 from cclauss/patch-1 Merge pull request - Define raw_input() in Python 3 & fix time.sleep() #### 0.19.3: Maintenance Release @@ -626,14 +736,14 @@ Nodes - TCP-request node - only write payload - JSON schema: perform validation when obj -> obj or str -> str - JSON schema: add draft-06 support (via $schema keyword) - - Mqtt proxy configuration for websocket connection, #1651. + - #1651. Mqtt proxy configuration for websocket connection, - Allows MQTT Shared Subscriptions for MQTT-In core node - Fix use of HTML tag or CSS class specification as icon of typedInput #### 0.19.2: Maintenance Release - Ensure node default colour is used if palette.theme has no match - - fix lost messages / properties in TCPRequest Node; closes #1863 (#1864) + - #1863 (#1864) fix lost messages / properties in TCPRequest Node; closes - Fix typo in template.html - Improve error reporting from context plugin loading - Prevent no-op edit of node marking as changed due to icon @@ -653,8 +763,8 @@ Nodes Editor - Add editorTheme.palette.theme to allow overriding colours - - Index all node properties when searching Fixes #1446 - - Handle NaN and Infinity properly in debug sidebar Fixes #1778 #1779 + - #1446 Index all node properties when searching Fixes + - #1779 Handle NaN and Infinity properly in debug sidebar Fixes #1778 - Prevent horizontal scroll when palette name cannot wrap - Ignore middle-click on node/ports to enable panning - Better wire layout when looping back @@ -668,7 +778,7 @@ Editor - Only edit nodes on dbl click on primary button with no modifiers - Allow subflows to be put in any palette category - Add flow navigator widget - - Cache flow library result to improve response time Fixes #1753 + - #1753 Cache flow library result to improve response time Fixes - Add middle-button-drag to pan the workspace - allow multi-line category name in editor - Redesign sidebar tabs @@ -676,20 +786,20 @@ Editor Nodes - - Change: Ensure runtime errors in Change node can be caught Fixes #1769 + - #1769 Change: Ensure runtime errors in Change node can be caught Fixes - File: Add output to File Out node - Function: add expandable JavaScript editor pane - Function: allow id and name reference in function node code (#1731) - HTTP Request: Move to request module - - HTTP: Ensure apiMaxLength applies to HTTP Nodes Fixes #1278 + - #1278 HTTP: Ensure apiMaxLength applies to HTTP Nodes Fixes - Join: accumulate top level properties - Join: allow environment variable as reduce init value - JSON: add JSON schema validation via msg.schema - Pi: Let nrgpio code work with python 3 - Pi: let Pi nodes be visible/editable on all platforms - Switch: add isEmpty rule - - TCP: queue messages while connecting; closes #1414 - - TLS: Add servername option to TLS config node for SNI Fixes #1805 + - #1414 TCP: queue messages while connecting; closes + - #1805 TLS: Add servername option to TLS config node for SNI Fixes - UDP: Don't accidentally re-use udp port when set to not do so Persistent Context @@ -704,8 +814,8 @@ Persistent Context Runtime - Support flow.disabled and .info in /flow API - - Node errors should be Strings not Errors Fixes #1781 - - Add detection of connection timeout in git communication Fixes #1770 + - #1781 Node errors should be Strings not Errors Fixes + - #1770 Add detection of connection timeout in git communication Fixes - Handle loading empty nodesDir - Add 'private' property to userDir generated package.json - Add RED.require to allow nodes to access other modules @@ -715,7 +825,7 @@ Runtime Editor Fixes - - Do not trim wires if node declares outputs in defaults but misses value Fixes #1737 + - #1737 Do not trim wires if node declares outputs in defaults but misses value Fixes Node Fixes @@ -725,16 +835,16 @@ Node Fixes - typo fix in node help (#1735) Other Fixes - - Tidy up default grunt task and fixup test break due to reorder Fixes #1738 + - #1738 Tidy up default grunt task and fixup test break due to reorder Fixes - Bump jsonata version #### 0.18.6: Maintenance Release Editor Fixes - - Handle a node having wires in the editor on ports it no longer has Fixes #1724 + - #1724 Handle a node having wires in the editor on ports it no longer has Fixes - Add missing ACE snippet files - - Fix wireClippedNodes is not defined Fixes #1726 + - #1726 Fix wireClippedNodes is not defined Fixes - Split node html to isolate bad nodes when loading - Avoid unnecessary use of .html() where .text() will do @@ -761,32 +871,32 @@ New Features Editor Fixes - - Highlight subflow node when log msg comes from inside Fixes #1698 - - Ensure node wires array is not longer than outputs value Fixes #1678 - - Allow importing an unknown config node to be undone Fixes #1681 - - Ensure keyboard shortcuts get saved in runtime settings Fixes #1696 + - #1698 Highlight subflow node when log msg comes from inside Fixes + - #1678 Ensure node wires array is not longer than outputs value Fixes + - #1681 Allow importing an unknown config node to be undone Fixes + - #1696 Ensure keyboard shortcuts get saved in runtime settings Fixes - Don't mark a subflow changed when actually modified nothing (#1665) Node Fixes - bind to correct port when doing udp broadcast/multicast (#1686) - Provide full error stack in Function node log message (#1700) - - Fix http request doc type Fixes #1690 + - #1690 Fix http request doc type Fixes - Make debug slightly larger to pass WCAG AA rating - - Make core nodes labels more consistent, to close #1673 - - Allow template node to be updated more than once Fixes #1671 + - #1673 Make core nodes labels more consistent, to close + - #1671 Allow template node to be updated more than once Fixes - Fix the problem that output labels of switch node sometimes disappear (#1664) - Chinese translations for core nodes (#1607) Runtime Fixes - - Handle and display for invalid flow credentials when project is disabled #1689 (#1694) + - #1689 (#1694) Handle and display for invalid flow credentials when project is disabled - node-red-pi: fix behavior with old bash version (#1713) - Fix ENOENT error on first start when no user dir (#1711) - - Handle null error object in Flow.handleError Fixes #1721 + - #1721 Handle null error object in Flow.handleError Fixes - update settings comments to describe how to setup for ipv6 (#1675) - - Remove credential props after diffing flow to prevent future false positives Fixes #1359 - - Log error if settings unavailable when saving user settings Fixes #1645 + - #1359 Remove credential props after diffing flow to prevent future false positives Fixes + - #1645 Log error if settings unavailable when saving user settings Fixes - Keep backup of .config.json - Add warning if using \_credentialSecret from .config.json - Filter req.user in /settings to prevent potentially leaking info @@ -812,7 +922,7 @@ Editor Fixes - Fix merging a remote diff - Fixed the problems when using a node without defaults - Disable user defined icon for subflow - - getDefaultNodeIcon should handle subflow instance nodes Fixes #1635 + - #1635 getDefaultNodeIcon should handle subflow instance nodes Fixes - Add Japanese info text for core nodes - Fix message lookup for core nodes in case of i18 locales directory exists - Prevent the last tab from being deleted @@ -843,7 +953,7 @@ Editor Fixes - Fix offset calculation when dragging node from palette - Allow a library entry to use non-default node-input- prefixes - - Change remote-diff shortcut and add it to keymap Fixes #1628 + - #1628 Change remote-diff shortcut and add it to keymap Fixes #### 0.18.2: Maintenance Release @@ -882,7 +992,7 @@ Projects - Handle more repo clone error cases - Relax validation of git urls - Revalidate project name on return to project-details view - - Avoid unnecessary project refresh on branch-switch Fixes #1597 + - #1597 Avoid unnecessary project refresh on branch-switch Fixes - Add support for file:// git urls - Handle project first-run without existing flow file - Handle delete of last remote in project settings @@ -893,9 +1003,9 @@ Projects Node Fixes - Trigger node migration - ensure bytopic not blank - - Add HEAD to list of methods with no body in http req node #1598 - - Do not include payload in GET requests Fixes #1598 - - Update sort/batch docs Fixes #1601 + - #1598 Add HEAD to list of methods with no body in http req node + - #1598 Do not include payload in GET requests Fixes + - #1601 Update sort/batch docs Fixes - Don't assume node has defaults when exporting icon property @@ -908,11 +1018,11 @@ Runtime - Better error reporting when module provides duplicate type - Update jsonata to 1.5.0 - add express-session memorystore without leaks (#1435) - - Allow adminAuth.user to be a Function Fixes #1461 + - #1461 Allow adminAuth.user to be a Function Fixes - Ensure RED.server is set even if admin api disabled - - Ensure strategy login button uses relative URL Fixes #1481 + - #1481 Ensure strategy login button uses relative URL Fixes - ignore `_msgid` when merging full objects - - Move node install to spawn to allow for big stdout Fixes #1488 + - #1488 Move node install to spawn to allow for big stdout Fixes - SIGINT handler should wait for stop to complete before exit Editor @@ -920,12 +1030,12 @@ Editor - allow a node's icon to be set dynamically (#1490) - Batch messages sent over comms to increase throughput - Migrate deploy confirmations to notifications - - `oneditdelete` should be available to all node types Closes #1346 + - #1346 `oneditdelete` should be available to all node types Closes - Sort typeSearch results based on position of match - Update ACE to test and add python highlighter (#1373) - - Clear mouse state when typeSearch cancelled Fixes #1517 + - #1517 Clear mouse state when typeSearch cancelled Fixes - Handle scoped modules via palette editor - - TypedInput: handle user defined value/labels options Fixes #1549 + - #1549 TypedInput: handle user defined value/labels options Fixes Nodes @@ -940,7 +1050,7 @@ Nodes - Add support for rejectUnauthorized msg property - Add TLS options to WebSocket client - Added parsed YAML support for template node (#1443) - - Allow delay node in rate-limit mode to be reset Fixes #1360 + - #1360 Allow delay node in rate-limit mode to be reset Fixes - Allow setTimeout in Function node to be promisified in node 8 - Debug to status option (#1499) - enable template config via msg.template for stored or generated templates (#1503) @@ -959,13 +1069,13 @@ Nodes - MQTT node - if Server/URL config contains '//' use it as a complete url; enabled ws:// and wss:// - clone messages before delayed send (#1474) - Decrement connected client count rather than show disconnected - - Don't end mqtt client on first error Fixes #1566 - - File out - create dirs synchronously to ensure they exist Fixes #1489 + - #1566 Don't end mqtt client on first error Fixes + - #1489 File out - create dirs synchronously to ensure they exist Fixes - Fix debug message format for Buffer (#1444) - Fix global.keys() bug in function node (#1417) - Handle escape characters in template node which uses Mustache format and JSON output mode (#1377) - - Move all node.send to end of timer functions in trigger node (issue #1527) (#1539) - - Publish null/undefined to mqtt as blank not toString Fixes #1521 + - #1527) (#1539) Move all node.send to end of timer functions in trigger node (issue + - #1521 Publish null/undefined to mqtt as blank not toString Fixes - remove inject node at specific time spinner - restrict inject interval to less that 2^31 millisecs - tag UDP ports in use properly so they get closed correctly (#1508) @@ -974,10 +1084,10 @@ Nodes - Add express-session missing dependency for oauth - Fix improper type tests is core test cases - - File node: recreate write stream when file deleted Fixes #1351 + - #1351 File node: recreate write stream when file deleted Fixes - Add flow stopping trace messages - Fix userDir test case when .config.json exists (#1350) - - Do not try to send msg after http request error handled Fixes #1344 + - #1344 Do not try to send msg after http request error handled Fixes - Fix boundary problem in range node (#1338) - Modify messages in node properties to refer messages.json (#1339) - Fix settings.js replacing webSocketVerifyClient by webSocketNodeVerifyClient (#1343) @@ -988,16 +1098,16 @@ Nodes - Add request node test case for POSTing 0 - Allow false and 0 in payload for httprequest (#1334) - Add file extension into flow name of library automatically (#1331) - - Fix accessing global context from jsonata expressions Fixes #1335 - - Disable editor whilst a deploy is inflight Fixes #1332 + - #1335 Fix accessing global context from jsonata expressions Fixes + - #1332 Disable editor whilst a deploy is inflight Fixes - Replace Unknown nodes with their real versions when node loaded - Retry auto-install of modules that fail - Fix column name in link nodes to refer language file (#1330) - - Use namespaces with link node title attributes i18n name Fixes #1329 - - Tidy up GPIO pin table presentation Fixes #1328 + - #1329 Use namespaces with link node title attributes i18n name Fixes + - #1328 Tidy up GPIO pin table presentation Fixes - Join: count of 0 should not send on every msg - Handle importing only one end of a link node pair - - Make sending to Debug synchronous again Fixes #1323 + - #1323 Make sending to Debug synchronous again Fixes - Make send-error behaviour optional in file node - Restore File In node behaviour of sending msg on error - Expose context.keys within Function node @@ -1011,10 +1121,10 @@ Nodes - Fix missing icons for some nodes (#1321) - Add reformat button to JSONata test data editor - Update delay node status without spawning unnecessary intervals - - Avoid stringify ServerResponse and Socket in Debug node Fixes #1311 + - #1311 Avoid stringify ServerResponse and Socket in Debug node Fixes - Fix creating userDir other than system drive on Windows (#1317) - - Trigger node not handling a duration of 0 as block mode Fixes #1316 - - Unable to config GPIO Pin 13 Fixes #1314 + - #1316 Trigger node not handling a duration of 0 as block mode Fixes + - #1314 Unable to config GPIO Pin 13 Fixes #### 0.17.2: Maintenance Release @@ -1023,7 +1133,7 @@ Nodes #### 0.17.1: Maintenance Release - Fix PI gpio to use BCM - - Prevent event thread contention when sending to Debug node Closes #1311 + - #1311 Prevent event thread contention when sending to Debug node Closes - Fix Bug: Can not display node icon when npm package has scope (#1305) (#1309) - Clear moved flag when nodes are deployed @@ -1031,7 +1141,7 @@ Nodes Runtime - - Return flow rev on reload api when api v2 enabled Closes #1273 + - #1273 Return flow rev on reload api when api v2 enabled Closes - Provide single endpoint to load all node message catalogs - Add .trace and .debug to Node prototype - Rename oauth auth scheme to strategy as it works for openid @@ -1039,16 +1149,16 @@ Runtime - Add support for oauth adminAuth configs - Cache auth details to save needlessly recalculating hashes - Add context.keys function to list top-level keys - - Strip BOM character from JSON files if present Fixes #1239 + - #1239 Strip BOM character from JSON files if present Fixes - Version check no meta (#1243) - - Ensure all nodes have access to global context Fixes #1230 - - Don't process subscription for unauthenticated comms link Fixes #851 - - Clone credentials when passing to node Fixes #1198 + - #1230 Ensure all nodes have access to global context Fixes + - #851 Don't process subscription for unauthenticated comms link Fixes + - #1198 Clone credentials when passing to node Fixes - Resolve dir argument of getLocalNodeFiles function (#1216) - Add wait for writing a library entry into a file. (#1186) - Use correct Buffer.from method rather than constructor - update core nodes to use newer Buffer syntax - - Treat missing msg properties as undefined rather than throw error Fixes #1167 + - #1167 Treat missing msg properties as undefined rather than throw error Fixes - Allows flows to be enabled/disabled in the runtime - add off option to logging settings comment - Log error stack traces if verbose flag is set @@ -1081,8 +1191,8 @@ Nodes - Fix wrong number of double quotes in CSV parsing - let csv node handle ip addresses without trying to parse - Update debug node to register the settings it uses - - Handle IncomingMessage/ServerResponse object types in debug Fixes #1202 - - Toggling debug node enabled/disabled state should set state dirty Fixes #1203 + - #1202 Handle IncomingMessage/ServerResponse object types in debug Fixes + - #1203 Toggling debug node enabled/disabled state should set state dirty Fixes - redo delay node status messages to be interval based - Update delay node ui - Add new msg.delay option to delay node @@ -1118,16 +1228,16 @@ Nodes - First pass of new node-info style - MQTT new style info - Fix empty extra node help content issue - - Handle HTTP In url that is missing its leading / Fixes #1218 + - #1218 Handle HTTP In url that is missing its leading / Fixes - Add file upload support to HTTP In node - HTTP Request node: add info on how to do form encoding - - Prevent unmodified msg.headers from breaking HTTP Request flows Closed #1015 + - #1015 Prevent unmodified msg.headers from breaking HTTP Request flows Closed - Add cookie handling to HTTP Request node - Add guard against the http-request buffer fix being reverted - Multipart streaming - Add http-request node unit tests - http request node add transport validity check and warn. - - Update follow_redirects to fix http_proxy handling Fixes #1172 + - #1172 Update follow_redirects to fix http_proxy handling Fixes - Allow statusCode/headers to be set directly within HTTP Response node - let inject "between time" also fire at start - Plus new info - remove repeat symbol from inject if repeat is 0 @@ -1176,7 +1286,7 @@ Nodes - Move udp sock error listener to only be instantiated once. - Let watch node recurse into subdirectories - Misconfigured WebSocket nodes should not register msg handlers - - Add websocketVerifyClient option to enable custom websocket auth Fixes #1127 + - #1127 Add websocketVerifyClient option to enable custom websocket auth Fixes Editor @@ -1198,11 +1308,11 @@ Editor - Remove unused modified flag on debug messages - Add copy path/value buttons to debug messages - dont match only part of the node type (#1242) - - Add editorTheme.logout.redirect to allow redirect on logout Closes #1213 - - Handle logging out and already logged-out editor Fixes #1288 + - #1213 Add editorTheme.logout.redirect to allow redirect on logout Closes + - #1288 Handle logging out and already logged-out editor Fixes - Fix bug: Export Subflows (#1282) - destroy editor to ensure fully removed on close (function, template, comment) - - Don't try to nls status text starting with '.' Fixes #1258 + - #1258 Don't try to nls status text starting with '.' Fixes - Add note of removed flows in diffConfig (#1253) - Add description to flow same as subflow - Allow tabs to be enabled/disabled in the editor @@ -1274,7 +1384,7 @@ Editor - Allow RED.validators.number to allow blank values as valid - Support dropping json files into the editor - NLS Expression/JSON editor and fix their height calculation - - Update JSONata to 1.2.4 Closes #1275 + - #1275 Update JSONata to 1.2.4 Closes - Remember test expression data on a per-node basis - NLS jsonata test messages - Add JSONata expr tester and improved feedback @@ -1291,13 +1401,13 @@ Other #### 0.16.2: Maintenance Release - - Ensure custom mustache context parent set in Template node fixes #1126 + - #1126 Ensure custom mustache context parent set in Template node fixes - Display debug node name in debug panel if its known - Ensure auth-tokens are removed when no user is specified in settings - Ensure all a tags have blank target in info sidebar - Ensure links do not span tabs in the editor - Avoid creating multiple reconnect timers in websocket node - - Fix inner reference in install fail message catalog entry Fixes #1120 + - #1120 Fix inner reference in install fail message catalog entry Fixes - Display buffer data properly for truncated buffers under Object property #### 0.16.1: Maintenance Release @@ -1305,9 +1415,9 @@ Other - Add colour swatches to debug when hex colour matched - Nodes with hasUsers set to false should not appear unused - Change hard error to verbose warning if using old node.js level - - Don't filter debug properties starting with _ Fixes #1117 - - Node logged errors not displayed properly in debug pane Fixes #1116 - - Do not look for existing nodes when checking for wires on paste Fixes #1114 + - #1117 Don't filter debug properties starting with _ Fixes + - #1116 Node logged errors not displayed properly in debug pane Fixes + - #1114 Do not look for existing nodes when checking for wires on paste Fixes - -v option not enabling verbose mode properly - Add node.js version check on startup @@ -1319,10 +1429,10 @@ Runtime Nodes - - Add option to colourise debug console output Closes #1103 + - #1103 Add option to colourise debug console output Closes - Add property validation to nodes using typedInput - - Add common validator for typedInput fields Closes #1104 - - Update debug node console logging indicator icon Closes #1094 + - #1104 Add common validator for typedInput fields Closes + - #1094 Update debug node console logging indicator icon Closes - Let exec node (spawn) handle commands with spaces in path - Add symbol to debug node to indicate debugging also to console.log - Change file node to use node 4 syntax (drops support for 0.8) @@ -1334,9 +1444,9 @@ Nodes Editor - - Add install/remove dialog to increase friction Closes #1109 - - Report node catalogue load errors Closes #1009 - - Properly report module remove errors in palette editor Fixes #1043 + - #1109 Add install/remove dialog to increase friction Closes + - #1009 Report node catalogue load errors Closes + - #1043 Properly report module remove errors in palette editor Fixes - Update rather than hide install button after success install - Tweak search box styling - Display info tips slightly longer @@ -1357,36 +1467,36 @@ Editor - Focus tray body when edit dialog opened - Hit enter to edit first node in selection - Add node delete button to edit dialog - - Add notification when runtime stopped due to missing types Part of #832 + - #832 Add notification when runtime stopped due to missing types Part of Fixes - - Do not tie debug src loading to needsPermission Fixes #1111 - - Initialise nodeApp regardless of httpAdmin setting Closes #1096 #1095 + - #1111 Do not tie debug src loading to needsPermission Fixes + - #1095 Initialise nodeApp regardless of httpAdmin setting Closes #1096 - Speed up reveal of search dialogs - - Ensure flows exist before delegating status/error events Fixes #1069 + - #1069 Ensure flows exist before delegating status/error events Fixes - Update package dependencies - Update MQTT to latest 2.2.1 - Node status not being refreshed properly in the editor - - Try to prevent auto-fill of password fields in node edit tray Fixes #1081 + - #1081 Try to prevent auto-fill of password fields in node edit tray Fixes - Fix whitespace in localfilesystem - fix bug where savesettings did not honor local settings variables (#1073) - - Tidy up unused/duplicate editor messages Closes #922 + - #922 Tidy up unused/duplicate editor messages Closes - Property expressions must not be blank - Tidy up merge commit of validatePropertyExpression - add port if wires array > number of ports declared. - - Allow quoted property expressions Fixes #1101 + - #1101 Allow quoted property expressions Fixes - Index all node properties for node search - Remove node 0.10 from travis config - update welcome message to use logger so it can be turned off/on if required (#1083) - Fix dynamically loading multiple node-sets from palette editor - - Allow a node to reorder its outputs and maintain links Fixes #1031 + - #1031 Allow a node to reorder its outputs and maintain links Fixes #### 0.15.3: Maintenance Release - Tcpgetfix: Another small check (#1070) - TCPGet: Ensure done() is called only once (#1068) - - Allow $ and _ at start of property identifiers Fixes #1063 + - #1063 Allow $ and _ at start of property identifiers Fixes - TCPGet: Separated the node.connected property for each instance (#1062) - Corrected 'overide' typo in XML node help (#1061) - TCPGet: Last property check (hopefully) (#1059) @@ -1419,11 +1529,11 @@ Fixes #### 0.15.2: Maintenance Release - - Revert bidi changes to nodes and hide menu option until fixed Fixes #1024 + - #1024 Revert bidi changes to nodes and hide menu option until fixed Fixes - Let xml node set options both ways - Bump serialport to use version 4 - gpio node handle multiple bits of data returned in one go - - HTTP In should pass application/octet-stream as buffer not string Fixes #1023 + - #1023 HTTP In should pass application/octet-stream as buffer not string Fixes - Handle missing httpNodeRoot setting properly - Config sidebar not handling node definition error properly - Add minimum show time to deploy spinner to avoid flicker @@ -1431,24 +1541,24 @@ Fixes - Add log.removeHandler function - Add Crtl/Shift/p shortcut for manage palette - Add spinner to deploy button - - Status messages from nodes in subflows not delegated properly Fixes #1016 + - #1016 Status messages from nodes in subflows not delegated properly Fixes - fix spelling in join node info - Speed up tab scrolling - - Update delay burst test to be more tolerant of timing Fixes #1013 + - #1013 Update delay burst test to be more tolerant of timing Fixes #### 0.15.1: Maintenance Release - Update default palette catalogue to use https - Disable palette editor if npm not found - and fix for Windows - - Searching package catalogue should be case-insensitive Fixes #1010 - - contenteditable fields not handled in config nodes Fixes #1011 + - #1010 Searching package catalogue should be case-insensitive Fixes + - #1011 contenteditable fields not handled in config nodes Fixes - Change html link refs from `_new` to `_blank` to be standards compliant #### 0.15.0: Milestone Release Runtime - - Increase default apiMaxLength to 5mb and add to default settings Closes #1001 + - #1001 Increase default apiMaxLength to 5mb and add to default settings Closes - Add v2 /flows api and deploy-overwrite protection - Encrypt credentials by default - Ensure errors thrown by RED.events handlers don't percolate up @@ -1457,7 +1567,7 @@ Editor - Mark nodes as changed when they are moved - Added parent containment option for draggable. (#1006) - - Ignore bidi event handling on non-existent and non-Input elements Closes #999 + - #999 Ignore bidi event handling on non-existent and non-Input elements Closes - Remove list of flows from menu - Allow nodes to be imported with their credentials - Add workspace search option @@ -1469,7 +1579,7 @@ Editor - Add import-to-new-tab option - Add new options to export-nodes dialog - Stop nodes being added beyond the outer bounds of the workspace - - Default config nodes to global scope unless in a subflow Closes #972 + - #972 Default config nodes to global scope unless in a subflow Closes - Bidi support for Text Direction and Structured Text (#961) - Fix jQuery selector, selecting more than one help pane/popover and displaying incorrectly. (#970) - Fixes removeItem not passing row data to callback. (#965) @@ -1483,7 +1593,7 @@ Nodes - Clean up status on close for several core nodes. - Change node: re-parse JSON set value each time to avoid pass-by-ref - Better handle HTTP Request header capitalisation - - Enable ES6 parsing in Function editor by default Fixes #985 + - #985 Enable ES6 parsing in Function editor by default Fixes - Update debug sidebar to use RED.view.reveal to show debug nodes - Add full path tip to file node, And tidy up Pi node tips - Remove WebSocket node maxlistener warning @@ -1502,7 +1612,7 @@ Nodes Other - - Add npm build/test scripts Closes #946 #660 + - #660 Add npm build/test scripts Closes #946 - Move travis to node 6 and 7 - drop 5 and 0.12 @@ -1510,15 +1620,15 @@ Other Fixes - - Tell ace about Function node globals. Closes #927 - - Tidy up mqtt nodes - linting and done handling. Closes #935 + - #927 Tell ace about Function node globals. Closes + - #935 Tidy up mqtt nodes - linting and done handling. Closes - Fix invalid html in TCP and HTML node edit templates - Add proper help text to link nodes - Handle importing old mqtt-broker configs that lack properties - Update ace to 1.2.4 - Allow config nodes to provide a sort function for their select list - Add log warning if node module required version cannot be satisfied - - Handle empty credentials file. Closes #937 + - #937 Handle empty credentials file. Closes - Add RPi.GPIO lib test for ArchLinux #### 0.14.5: Maintenance Release @@ -1528,8 +1638,8 @@ Fixes - Cannot clear cookies with http nodes - let HTML parse node allow msg.select set select - Validate nodes on import after any references have been remapped - - Debug node handles objects without constructor property Fixes #933 - - Ensure 'false' property values are displayed in info panel Fixes #940 + - #933 Debug node handles objects without constructor property Fixes + - #940 Ensure 'false' property values are displayed in info panel Fixes - Fix node enable/disable over restart - load configs after settings init #### 0.14.4: Maintenance Release @@ -1538,20 +1648,20 @@ Nodes - Update trigger node ui to use typedInputs - Better handling of quotes in CSV node - - Clarify the MQTT node sends msg.payload - closes #929 - - Inject node should reuse the message it is triggered with Closes #914 + - #929 Clarify the MQTT node sends msg.payload - closes + - #914 Inject node should reuse the message it is triggered with Closes - Stop trigger node re-using old message - Allow node.status text to be 'falsey' values Fixes - - Handle DOMException when embedded in an iframe of different origin Fixes #932 + - #932 Handle DOMException when embedded in an iframe of different origin Fixes - Fix double firing of menu actions - - Fix select box handling in Safari - fixes #928 - - Clear context in node test helper Fixes #858 - - Allow node properties to be same as existing object functions Fixes #880 + - #928 Fix select box handling in Safari - fixes + - #858 Clear context in node test helper Fixes + - #880 Allow node properties to be same as existing object functions Fixes - Handle comms link closing whilst completing the initial connect - - Protect against node type names that clash with Object property names Fixes #917 + - #917 Protect against node type names that clash with Object property names Fixes - Clone default node properties to avoid reference leakage - Strip tab node definition when exporting - Check for null config properties in editor before over-writing them @@ -1561,16 +1671,16 @@ Editor - Add sql mode to ace editor - Keyboard shortcuts dialog update (#923) - - Ensure importing link nodes to a subflow doesn't add outbound links Fixes #921 + - #921 Ensure importing link nodes to a subflow doesn't add outbound links Fixes - Add updateConfigNodeUsers function to editor - Scroll to bottom when item added to editableList - - Form input widths behave more consistently when resizing Fixes #919 #920 + - #920 Form input widths behave more consistently when resizing Fixes #919 #### 0.14.3: Maintenance Release Fixes - - Create default setting.js in user-specified directory. Fixes #908 + - #908 Create default setting.js in user-specified directory. Fixes - MQTT In subscription qos not defaulting properly - Let exec node handle 0 as well as "0" @@ -1578,7 +1688,7 @@ Fixes Fixes - - Cannot add new twitter credentials. Fixes #913 + - #913 Cannot add new twitter credentials. Fixes - Support array references in Debug property field #### 0.14.1: Maintenance Release @@ -1586,8 +1696,8 @@ Fixes Fixes - Handle undefined property that led to missing wires in the editor - - Remove duplicate 'Delete' entry in keyboard shortcut window. Closes #911 - - Add 'exec' to node-red-pi launch script. Closes #910 + - #911 Remove duplicate 'Delete' entry in keyboard shortcut window. Closes + - #910 Add 'exec' to node-red-pi launch script. Closes #### 0.14.0: Milestone Release @@ -1609,9 +1719,9 @@ Editor Runtime - Always log node warnings on start without requiring -v - - Add support for loading scoped node modules. Closes #885 + - #885 Add support for loading scoped node modules. Closes - Add process.env.PORT to settings.js - - Clear node context on deploy. Closes #870 + - #870 Clear node context on deploy. Closes - Enable finer grained permissions in adminAuth Nodes @@ -1619,9 +1729,9 @@ Nodes - Enable config nodes to reference other config nodes - Add Split/Join nodes - Add Link nodes - - Add support to HTTP In node for PATCH requests. Closes #904 + - #904 Add support to HTTP In node for PATCH requests. Closes - Add cookie handling to HTTP In and HTTP Response nodes - - Add repeat indicator to inject node label. Closes #887 + - #887 Add repeat indicator to inject node label. Closes - Add javascript highlighter to template node - Add optional timeout to exec node - Add TLS node and update MQTT/HTTP nodes to use it @@ -1633,19 +1743,19 @@ Nodes - Update Serial node to support custom baud rates - Add support for array-syntax in typedInput msg properties - Add RED.util to Function node sandbox - - Capture error stack on node.error. Closes #879 + - #879 Capture error stack on node.error. Closes Fixes - Add error handling to all node definition api calls - Handle null return from Function node in array of messages - - Defer loading of token sessions until they are accessed. Fixes #895 + - #895 Defer loading of token sessions until they are accessed. Fixes - set pi gpio pin status correctly if set on start - - Prevent parent window scrolling when view is focused. Fixes #635 + - #635 Prevent parent window scrolling when view is focused. Fixes - Handle missing tab nodes in a loaded flow config - Ensure typedInput dropdown doesn't fall off the page - - Protect against node types with reserved names such as toString. Fixes #880 + - #880 Protect against node types with reserved names such as toString. Fixes - Do not rely on the HTML file to identify where nodes are registered from - Preserve node properties on import - Fix regression in delay node. topic based queue was emptying all the time instead of spreading out messages. @@ -1661,22 +1771,22 @@ Fixes #### 0.13.4: Maintenance Release - Add timed release mode to delay node - - Enable link splicing for when import_dragging nodes. Closes #811 + - #811 Enable link splicing for when import_dragging nodes. Closes - Fix uncaught exception on deploy whilst node sending messages - Deprecate old mqtt client and connection pool modules - - Change node: add bool/num types to change mode Closes #835 - - Validate fields that are `$(env-vars)` Closes #825 + - #835 Change node: add bool/num types to change mode Closes + - #825 Validate fields that are `$(env-vars)` Closes - Handle missing config nodes when validating node properties - Pi node - don't try to send data if closing - Load node message catalog when added dynamically - Split palette labels on spaces and hyphens when laying out - - Warn if editor routes are accessed but runtime not started Closes #816 - - Better handling of zero-length flow files Closes #819 + - #816 Warn if editor routes are accessed but runtime not started Closes + - #819 Better handling of zero-length flow files Closes - Allow runtime calls to RED._ to specify other namespace - Better right alignment of numerics in delay and trigger nodes - Allow node modules to include example flows - Create node_modules in userDir - - Ensure errors in node def functions don't break view rendering Fixes #815 + - #815 Ensure errors in node def functions don't break view rendering Fixes - Updated Inject node info with instructions for flow and global options @@ -1700,13 +1810,13 @@ Fixes - Make jquery spinner element css consistent with other inputs - tcp node add reply (to all) capability - Allow the template node to be treated as plain text - - Validate MQTT In topics Fixes #792 - - httpNodeAuth should not block http options requests Fixes #793 + - #792 Validate MQTT In topics Fixes + - #793 httpNodeAuth should not block http options requests Fixes - Disable perMessageDeflate on WS servers - fixes 'zlib binding closed' error - Clear trigger status icon on re-deploy - Don't default inject payload to blank string - Trigger node, add configurable reset - - Allow function properties in settings Fixes #790 - fixes use of httpNodeMiddleware + - #790 - fixes use of httpNodeMiddleware Allow function properties in settings Fixes - Fix order of config dialog calls to save/creds/validate - Add debounce to Pi GPIO node @@ -1722,7 +1832,7 @@ Fixes - Add 'previous value' option to Switch node - Allow existing nodes to splice into links on drag - - CORS not properly configured on multiple http routes Fixes #783 + - #783 CORS not properly configured on multiple http routes Fixes - Restore shift-drag to snap/unsnap to grid - Moving nodes with keyboard should flag workspace dirty - Notifications flagged as fixed should not be click-closable diff --git a/Gruntfile.js b/Gruntfile.js index 1e421f62d..32556f0a4 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -26,6 +26,10 @@ module.exports = function(grunt) { nodemonArgs.push(flowFile); } + var browserstack = grunt.option('browserstack'); + if (browserstack) { + process.env.BROWSERSTACK = true; + } var nonHeadless = grunt.option('non-headless'); if (nonHeadless) { process.env.NODE_RED_NON_HEADLESS = true; @@ -121,6 +125,7 @@ module.exports = function(grunt) { src: [ // Ensure editor source files are concatenated in // the right order + "packages/node_modules/@node-red/editor-client/src/js/polyfills.js", "packages/node_modules/@node-red/editor-client/src/js/jquery-addons.js", "packages/node_modules/@node-red/editor-client/src/js/red.js", "packages/node_modules/@node-red/editor-client/src/js/events.js", @@ -147,6 +152,7 @@ module.exports = function(grunt) { "packages/node_modules/@node-red/editor-client/src/js/ui/common/stack.js", "packages/node_modules/@node-red/editor-client/src/js/ui/common/typedInput.js", "packages/node_modules/@node-red/editor-client/src/js/ui/common/toggleButton.js", + "packages/node_modules/@node-red/editor-client/src/js/ui/common/colorPicker.js", "packages/node_modules/@node-red/editor-client/src/js/ui/actions.js", "packages/node_modules/@node-red/editor-client/src/js/ui/deploy.js", "packages/node_modules/@node-red/editor-client/src/js/ui/diff.js", @@ -159,6 +165,8 @@ module.exports = function(grunt) { "packages/node_modules/@node-red/editor-client/src/js/ui/sidebar.js", "packages/node_modules/@node-red/editor-client/src/js/ui/palette.js", "packages/node_modules/@node-red/editor-client/src/js/ui/tab-info.js", + "packages/node_modules/@node-red/editor-client/src/js/ui/tab-info-outliner.js", + "packages/node_modules/@node-red/editor-client/src/js/ui/tab-help.js", "packages/node_modules/@node-red/editor-client/src/js/ui/tab-config.js", "packages/node_modules/@node-red/editor-client/src/js/ui/tab-context.js", "packages/node_modules/@node-red/editor-client/src/js/ui/palette-editor.js", @@ -173,6 +181,7 @@ module.exports = function(grunt) { "packages/node_modules/@node-red/editor-client/src/js/ui/actionList.js", "packages/node_modules/@node-red/editor-client/src/js/ui/typeSearch.js", "packages/node_modules/@node-red/editor-client/src/js/ui/subflow.js", + "packages/node_modules/@node-red/editor-client/src/js/ui/group.js", "packages/node_modules/@node-red/editor-client/src/js/ui/userSettings.js", "packages/node_modules/@node-red/editor-client/src/js/ui/projects/projects.js", "packages/node_modules/@node-red/editor-client/src/js/ui/projects/projectSettings.js", @@ -185,11 +194,12 @@ module.exports = function(grunt) { vendor: { files: { "packages/node_modules/@node-red/editor-client/public/vendor/vendor.js": [ - "packages/node_modules/@node-red/editor-client/src/vendor/jquery/js/jquery-3.4.1.min.js", - "packages/node_modules/@node-red/editor-client/src/vendor/jquery/js/jquery-migrate-3.0.1.min.js", + "packages/node_modules/@node-red/editor-client/src/vendor/jquery/js/jquery-3.5.1.min.js", + "packages/node_modules/@node-red/editor-client/src/vendor/jquery/js/jquery-migrate-3.3.0.min.js", "packages/node_modules/@node-red/editor-client/src/vendor/jquery/js/jquery-ui.min.js", "packages/node_modules/@node-red/editor-client/src/vendor/jquery/js/jquery.ui.touch-punch.min.js", - "packages/node_modules/@node-red/editor-client/src/vendor/marked/marked.min.js", + "node_modules/marked/marked.min.js", + "node_modules/dompurify/dist/purify.min.js", "packages/node_modules/@node-red/editor-client/src/vendor/d3/d3.v3.min.js", "packages/node_modules/@node-red/editor-client/src/vendor/i18next/i18next.min.js", "node_modules/jsonata/jsonata-es5.min.js", diff --git a/package.json b/package.json index 9aaeb8f9e..c20eb7f2c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-red", - "version": "1.0.3", + "version": "1.1.0", "description": "Low-code programming for event-driven applications", "homepage": "http://nodered.org", "license": "Apache-2.0", @@ -24,7 +24,7 @@ } ], "dependencies": { - "ajv": "6.10.2", + "ajv": "6.12.0", "basic-auth": "2.0.1", "bcryptjs": "2.4.3", "body-parser": "1.19.0", @@ -32,7 +32,7 @@ "clone": "2.1.2", "content-type": "1.0.4", "cookie": "0.4.0", - "cookie-parser": "1.4.4", + "cookie-parser": "1.4.5", "cors": "2.8.5", "cron": "1.7.2", "denque": "1.4.1", @@ -41,40 +41,44 @@ "fs-extra": "8.1.0", "fs.notify": "0.0.4", "hash-sum": "2.0.0", - "https-proxy-agent": "2.2.4", + "https-proxy-agent": "5.0.0", "i18next": "15.1.2", - "iconv-lite": "0.5.0", + "iconv-lite": "0.5.1", "is-utf8": "0.2.1", "js-yaml": "3.13.1", "json-stringify-safe": "5.0.1", - "jsonata": "1.7.0", + "jsonata": "1.8.3", + "lodash.clonedeep": "^4.5.0", "media-typer": "1.1.0", - "memorystore": "1.6.1", + "memorystore": "1.6.2", "mime": "2.4.4", + "moment-timezone": "^0.5.31", "mqtt": "2.18.8", "multer": "1.4.2", - "mustache": "3.0.2", + "mustache": "4.0.1", "node-red-node-rbe": "^0.2.6", "node-red-node-sentiment": "^0.1.6", "node-red-node-tail": "^0.1.0", - "nopt": "4.0.1", + "nopt": "4.0.3", "oauth2orize": "1.11.0", "on-headers": "1.0.2", - "passport": "0.4.0", + "passport": "0.4.1", "passport-http-bearer": "1.0.1", "passport-oauth2-client-password": "0.1.2", "raw-body": "2.4.1", "request": "2.88.0", "semver": "6.3.0", - "uglify-js": "3.6.9", + "uglify-js": "3.8.1", "when": "3.7.8", "ws": "6.2.1", - "xml2js": "0.4.22" + "xml2js": "0.4.23" }, "optionalDependencies": { - "bcrypt": "3.0.6" + "bcrypt": "3.0.8" }, "devDependencies": { + "marked": "0.8.2", + "dompurify": "2.0.8", "grunt": "~1.0.4", "grunt-chmod": "~1.1.1", "grunt-cli": "~1.3.2", @@ -101,8 +105,8 @@ "minami": "1.2.3", "mocha": "^5.2.0", "mosca": "^2.8.3", - "node-red-node-test-helper": "^0.2.3", - "node-sass": "^4.13.0", + "node-red-node-test-helper": "^0.2.5", + "node-sass": "^4.13.1", "should": "^8.4.0", "sinon": "1.17.7", "stoppable": "^1.1.0", diff --git a/packages/node_modules/@node-red/editor-api/lib/admin/nodes.js b/packages/node_modules/@node-red/editor-api/lib/admin/nodes.js index 2787a5c36..b78de9d75 100644 --- a/packages/node_modules/@node-red/editor-api/lib/admin/nodes.js +++ b/packages/node_modules/@node-red/editor-api/lib/admin/nodes.js @@ -44,6 +44,7 @@ module.exports = { user: req.user, module: req.body.module, version: req.body.version, + url: req.body.url, req: apiUtils.getRequestLogObject(req) } runtimeAPI.nodes.addModule(opts).then(function(info) { diff --git a/packages/node_modules/@node-red/editor-api/lib/auth/index.js b/packages/node_modules/@node-red/editor-api/lib/auth/index.js index a7ee2e7f3..d4ec10f08 100644 --- a/packages/node_modules/@node-red/editor-api/lib/auth/index.js +++ b/packages/node_modules/@node-red/editor-api/lib/auth/index.js @@ -36,6 +36,7 @@ var log = require("@node-red/util").log; // TODO: separate module passport.use(strategies.bearerStrategy.BearerStrategy); passport.use(strategies.clientPasswordStrategy.ClientPasswordStrategy); passport.use(strategies.anonymousStrategy); +passport.use(strategies.tokensStrategy); var server = oauth2orize.createServer(); @@ -60,7 +61,7 @@ function init(_settings,storage) { function needsPermission(permission) { return function(req,res,next) { if (settings && settings.adminAuth) { - return passport.authenticate(['bearer','anon'],{ session: false })(req,res,function() { + return passport.authenticate(['bearer','tokens','anon'],{ session: false })(req,res,function() { if (!req.user) { return next(); } @@ -100,7 +101,10 @@ function login(req,res) { } } else if (mergedAdminAuth.type === "strategy") { - var urlPrefix = (settings.httpAdminRoot==='/')?"":settings.httpAdminRoot; + var urlPrefix = (settings.httpAdminRoot||"").replace(/\/$/,""); + if (urlPrefix.length > 0) { + urlPrefix += "/"; + } response = { "type":"strategy", "prompts":[{type:"button",label:mergedAdminAuth.strategy.label, url: urlPrefix + "auth/strategy"}] diff --git a/packages/node_modules/@node-red/editor-api/lib/auth/strategies.js b/packages/node_modules/@node-red/editor-api/lib/auth/strategies.js index b17bf1473..87023a487 100644 --- a/packages/node_modules/@node-red/editor-api/lib/auth/strategies.js +++ b/packages/node_modules/@node-red/editor-api/lib/auth/strategies.js @@ -123,9 +123,38 @@ AnonymousStrategy.prototype.authenticate = function(req) { }); } +function TokensStrategy() { + passport.Strategy.call(this); + this.name = 'tokens'; +} +util.inherits(TokensStrategy, passport.Strategy); +TokensStrategy.prototype.authenticate = function(req) { + var self = this; + var token = null; + if (Users.tokenHeader() === 'authorization') { + if (req.headers.authorization && req.headers.authorization.split(' ')[0] === 'Bearer') { + token = req.headers.authorization.split(' ')[1]; + } + } else { + token = req.headers[Users.tokenHeader()]; + } + if (token) { + Users.tokens(token).then(function(admin) { + if (admin) { + self.success(admin,{scope:admin.permissions}); + } else { + self.fail(401); + } + }); + } else { + self.fail(401); + } +} + module.exports = { bearerStrategy: bearerStrategy, clientPasswordStrategy: clientPasswordStrategy, passwordTokenExchange: passwordTokenExchange, - anonymousStrategy: new AnonymousStrategy() + anonymousStrategy: new AnonymousStrategy(), + tokensStrategy: new TokensStrategy() } diff --git a/packages/node_modules/@node-red/editor-api/lib/auth/users.js b/packages/node_modules/@node-red/editor-api/lib/auth/users.js index 24a762958..f032332db 100644 --- a/packages/node_modules/@node-red/editor-api/lib/auth/users.js +++ b/packages/node_modules/@node-red/editor-api/lib/auth/users.js @@ -59,7 +59,9 @@ function getDefaultUser() { var api = { get: get, authenticate: authenticate, - default: getDefaultUser + default: getDefaultUser, + tokens: getDefaultUser, + tokenHeader: "authorization" } function init(config) { @@ -105,6 +107,12 @@ function init(config) { } else { api.default = getDefaultUser; } + if (config.tokens && typeof config.tokens === "function") { + api.tokens = config.tokens; + if (config.tokenHeader && typeof config.tokenHeader === "string") { + api.tokenHeader = config.tokenHeader.toLowerCase(); + } + } } function cleanUser(user) { if (user && user.hasOwnProperty('password')) { @@ -118,5 +126,7 @@ module.exports = { init: init, get: function(username) { return api.get(username).then(cleanUser)}, authenticate: function() { return api.authenticate.apply(null, arguments) }, - default: function() { return api.default(); } + default: function() { return api.default(); }, + tokens: function(token) { return api.tokens(token); }, + tokenHeader: function() { return api.tokenHeader } }; diff --git a/packages/node_modules/@node-red/editor-api/lib/index.js b/packages/node_modules/@node-red/editor-api/lib/index.js index 0dc06ab71..534a77869 100644 --- a/packages/node_modules/@node-red/editor-api/lib/index.js +++ b/packages/node_modules/@node-red/editor-api/lib/index.js @@ -59,6 +59,12 @@ function init(settings,_server,storage,runtimeAPI) { }); adminApp.use(corsHandler); + if (settings.httpAdminMiddleware) { + if (typeof settings.httpAdminMiddleware === "function") { + adminApp.use(settings.httpAdminMiddleware) + } + } + auth.init(settings,storage); var maxApiRequestSize = settings.apiMaxLength || '5mb'; diff --git a/packages/node_modules/@node-red/editor-api/lib/util.js b/packages/node_modules/@node-red/editor-api/lib/util.js index 0cef96bbb..950e3d9cc 100644 --- a/packages/node_modules/@node-red/editor-api/lib/util.js +++ b/packages/node_modules/@node-red/editor-api/lib/util.js @@ -43,10 +43,16 @@ module.exports = { rejectHandler: function(req,res,err) { //TODO: why this when errorHandler also?! log.audit({event: "api.error",error:err.code||"unexpected_error",message:err.message||err.toString()},req); - res.status(err.status||400).json({ + var response = { code: err.code||"unexpected_error", message: err.message||err.toString() - }); + }; + // Handle auth failures on a specific remote + // TODO: don't hardcode this here - allow users of rejectHandler to identify extra props to send + if (err.remote) { + response.remote = err.remote; + } + res.status(err.status||400).json(response); }, getRequestLogObject: function(req) { return { diff --git a/packages/node_modules/@node-red/editor-api/package.json b/packages/node_modules/@node-red/editor-api/package.json index 824f4df5b..936fe3edc 100644 --- a/packages/node_modules/@node-red/editor-api/package.json +++ b/packages/node_modules/@node-red/editor-api/package.json @@ -1,6 +1,6 @@ { "name": "@node-red/editor-api", - "version": "1.0.3", + "version": "1.1.0", "license": "Apache-2.0", "main": "./lib/index.js", "repository": { @@ -16,21 +16,21 @@ } ], "dependencies": { - "@node-red/util": "1.0.3", - "@node-red/editor-client": "1.0.3", + "@node-red/util": "1.1.0", + "@node-red/editor-client": "1.1.0", "bcryptjs": "2.4.3", "body-parser": "1.19.0", "clone": "2.1.2", "cors": "2.8.5", "express-session": "1.17.0", "express": "4.17.1", - "memorystore": "1.6.1", + "memorystore": "1.6.2", "mime": "2.4.4", - "mustache": "3.0.2", + "mustache": "4.0.1", "oauth2orize": "1.11.0", "passport-http-bearer": "1.0.1", "passport-oauth2-client-password": "0.1.2", - "passport": "0.4.0", + "passport": "0.4.1", "when": "3.7.8", "ws": "6.2.1" }, diff --git a/packages/node_modules/@node-red/editor-client/locales/de/editor.json b/packages/node_modules/@node-red/editor-client/locales/de/editor.json index c7394c347..0c8dd42eb 100755 --- a/packages/node_modules/@node-red/editor-client/locales/de/editor.json +++ b/packages/node_modules/@node-red/editor-client/locales/de/editor.json @@ -34,11 +34,11 @@ "view" : "Ansicht", "grid" : "Gitter", "showGrid" : "Raster anzeigen", - "snapGrid" : "Einrasten am Raster", + "snapGrid" : "Am Raster ausrichten", "gridSize" : "Rastergröße", "textDir" : "Textrichtung", "defaultDir" : "Standard", - "ltr" : "Links-nach-rechts", + "ltr" : "Von links nach rechts", "rtl" : "Von rechts nach links", "auto" : "Kontextuell" }, @@ -53,15 +53,15 @@ "import" : "Import", "export" : "Exportieren", "search" : "Flows durchsuchen", - "searchInput" : "durchsuchen Sie Ihre Flows", + "searchInput" : "Flows durchsuchen", "subflows" : "Subflow", "createSubflow" : "Subflow erstellen", - "selectionToSubflow" : "Auswahl für Subflow", + "selectionToSubflow" : "Auswahl zu Subflow", "flows" : "Flows", "add" : "Hinzufügen", "rename" : "Umbenennen", "delete" : "Löschen", - "keyboardShortcuts" : "Tastaturkurzbefehle", + "keyboardShortcuts" : "Tastenkürzel", "login" : "Anmelden", "logout" : "Abmelden", "editPalette" : "Palette verwalten", @@ -217,7 +217,7 @@ "remote" : "Ferne Änderungen", "reviewChanges" : "Änderungen prüfen", "noBinaryFileShowed" : "Der Inhalt der Binärdatei kann nicht angezeigt", - "viewCommitDiff" : "Änderungen festschreiben", + "viewCommitDiff" : "Änderungen committen", "compareChanges" : "Änderungen vergleichen", "saveConflict" : "Konfliktlösung speichern", "conflictHeader" : " __resolved__ von __unresolved__ -Konflikten behoben", @@ -226,8 +226,8 @@ "newVersionError" : "Neue Version enthält keine gültige JSON-Datei:" }, "subflow" : { - "editSubflow" : "Flowschablone bearbeiten: __name__", - "edit" : "Flowsschablone bearbeiten", + "editSubflow" : "Subflow bearbeiten: __name__", + "edit" : "Subflow bearbeiten", "subflowInstances" : "Es ist __count__ Instanz dieser Subflow-Vorlage vorhanden.", "subflowInstances_plural" : "Es gibt __count__ Instanzen dieser Subflow-Vorlage.", "editSubflowProperties" : "Eigenschaften bearbeiten", @@ -266,7 +266,7 @@ } }, "keyboard" : { - "title" : "Tastaturkurzbefehle", + "title" : "Tastenkürzel", "keyboard" : "Tastatur", "filterActions" : "Filteraktionen", "shortcut" : "Direktaufruf", @@ -283,7 +283,7 @@ "exportNode" : "Node exportieren", "nudgeNode" : "Ausgewählte Nodes verschieben (1px)", "moveNode" : "Ausgewählte Nodes verschieben (20px)", - "toggleSidebar" : "Seitenleiste ein-/ausschalten", + "toggleSidebar" : "Seitenleiste ein-/ausblenden", "copyNode" : "Ausgewählte Nodes kopieren", "cutNode" : "Ausgewählte Nodes ausschneiden", "pasteNode" : "Node einfügen", @@ -308,7 +308,7 @@ }, "palette" : { "noInfo" : "Keine Informationen verfügbar", - "filter" : "Filter Nodes", + "filter" : "Nodes filtern", "search" : "Suchmodule", "addCategory" : "Neu hinzufügen ...", "label" : { @@ -366,11 +366,11 @@ "remove" : "entfernen", "update" : "Update auf __version__", "updated" : "aktualisiert", - "install" : "installieren", - "installed" : "installiert", + "install" : "Installieren", + "installed" : "Installiert", "loading" : "Kataloge werden geladen ...", "tab-nodes" : "Nodes", - "tab-install" : "installieren", + "tab-install" : "Installieren", "sort" : "Sortierung:", "sortAZ" : "a-z", "sortRecent" : "kürzlich", @@ -452,7 +452,7 @@ "name" : "Kontextdaten", "label" : "Kontext", "none" : "keine ausgewählt", - "refresh" : "Aktualisierung zum Laden", + "refresh" : "Zum Aktualisieren neu laden", "empty" : "leer", "node" : "Node", "flow" : "Flow", @@ -477,7 +477,7 @@ "none" : "Keine", "install" : "installieren", "removeFromProject" : "Aus Projekt entfernen", - "addToProject" : "zu Projekt hinzufügen", + "addToProject" : "Zu Projekt hinzufügen", "files" : "Dateien", "flow" : "Flow", "credentials" : "Berechtigungsnachweis", @@ -510,7 +510,7 @@ }, "userSettings" : { "committerDetail" : "Committer-Details", - "committerTip" : "Leer Wert für Systemstandardwert belassen", + "committerTip" : "Leer lassen für Systemstandard", "userName" : "Benutzername", "email" : "E-Mail", "sshKeys" : "SSH-Schlüssel", @@ -544,7 +544,7 @@ "revertChanges" : "Änderungen zurücksetzen", "localChanges" : "Lokale Änderungen", "none" : "Keine", - "conflictResolve" : "Alle Konflikte wurden aufgelöst. Festschreiben der Änderungen, um den Mischvorgang abzuschließen.", + "conflictResolve" : "Alle Konflikte wurden aufgelöst. Committe die Änderungen, um den Merge Request abzuschließen.", "localFiles" : "Lokale Dateien", "all" : "alle", "unmergedChanges" : "Nicht zusammengeführte Änderungen", diff --git a/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json b/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json index 72af1d38b..f71bd2577 100755 --- a/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json +++ b/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json @@ -14,7 +14,13 @@ "back": "Back", "next": "Next", "clone": "Clone project", - "cont": "Continue" + "cont": "Continue", + "style": "Style", + "line": "Outline", + "fill": "Fill", + "label": "Label", + "color": "Color", + "position": "Position" }, "type": { "string": "string", @@ -28,6 +34,13 @@ "null": "null" } }, + "event": { + "loadPalette": "Loading Palette", + "loadNodeCatalogs": "Loading Node catalogs", + "loadNodes": "Loading Nodes __count__", + "loadFlows": "Loading Flows", + "importFlows": "Adding Flows to workspace" + }, "workspace": { "defaultName": "Flow __number__", "editFlow": "Edit flow: __name__", @@ -91,7 +104,12 @@ "projects-new": "New", "projects-open": "Open", "projects-settings": "Project Settings", - "showNodeLabelDefault": "Show label of newly added nodes" + "showNodeLabelDefault": "Show label of newly added nodes", + "groups": "Groups", + "groupSelection": "Group selection", + "ungroupSelection": "Ungroup selection", + "groupMergeSelection": "Merge selection", + "groupRemoveSelection": "Remove from group" } }, "actions": { @@ -171,6 +189,8 @@ "node_plural": "__count__ nodes", "configNode": "__count__ configuration node", "configNode_plural": "__count__ configuration nodes", + "group": "__count__ group", + "group_plural": "__count__ groups", "flow": "__count__ flow", "flow_plural": "__count__ flows", "subflow": "__count__ subflow", @@ -186,6 +206,9 @@ "nodesImported": "Imported:", "nodeCopied": "__count__ node copied", "nodeCopied_plural": "__count__ nodes copied", + "groupCopied": "__count__ group copied", + "groupCopied_plural": "__count__ groups copied", + "groupStyleCopied": "Group style copied", "invalidFlow": "Invalid flow: __message__", "export": { "selected":"selected nodes", @@ -308,6 +331,13 @@ "multipleInputsToSelection": "Cannot create subflow: multiple inputs to selection" } }, + "group": { + "editGroup": "Edit group: __name__", + "errors": { + "cannotCreateDiffGroups": "Cannot create group using nodes from different groups", + "cannotAddSubflowPorts": "Cannot add subflow ports to a group" + } + }, "editor": { "configEdit": "Edit", "configAdd": "Add", @@ -351,7 +381,8 @@ "bool": "bool", "json": "JSON", "bin": "buffer", - "env": "env variable" + "env": "env variable", + "cred": "credential" }, "menu": { "input": "input", @@ -538,6 +569,7 @@ "label": "info", "node": "Node", "type": "Type", + "group": "Group", "module": "Module", "id": "ID", "status": "Status", @@ -560,7 +592,20 @@ "nodeHelp": "Node Help", "none":"None", "arrayItems": "__count__ items", - "showTips":"You can open the tips from the settings panel" + "showTips":"You can open the tips from the settings panel", + "outline": "Outline", + "empty": "empty", + "globalConfig": "Global Configuration Nodes" + }, + "help": { + "name": "Help", + "label": "help", + "search": "Search help", + "nodeHelp": "Node Help", + "showHelp": "Show help", + "showInOutline": "Show in outline", + "showTopics": "Show topics", + "noHelp": "No help topic selected" }, "config": { "name": "Configuration nodes", @@ -613,7 +658,6 @@ "removeFromProject": "remove from project", "addToProject": "add to project", "files": "Files", - "package": "Package", "flow": "Flow", "credentials": "Credentials", "package":"Package", @@ -757,7 +801,8 @@ "bin": "buffer", "date": "timestamp", "jsonata": "expression", - "env": "env variable" + "env": "env variable", + "cred": "credential" } }, "editableList": { @@ -804,9 +849,9 @@ "expandItems": "Expand items", "collapseItems": "Collapse items", "duplicate": "Duplicate", - "error": { - "invalidJSON": "Invalid JSON: " - } + "error": { + "invalidJSON": "Invalid JSON: " + } }, "markdownEditor": { "title": "Markdown editor", @@ -977,7 +1022,8 @@ "passphrase": "Passphrase", "retry": "Retry", "update-failed": "Failed to update auth", - "unhandled": "Unhandled error response" + "unhandled": "Unhandled error response", + "host-key-verify-failed": "

Host key verification failed.

The repository host key could not be verified. Please update your known_hosts file and try again.

" }, "create-branch-list": { "invalid": "Invalid branch", @@ -1008,6 +1054,7 @@ "en-US": "English", "ja": "Japanese", "ko": "Korean", - "zh-CN": "Chinese(Simplified)" + "zh-CN": "Chinese(Simplified)", + "zh-TW": "Chinese(Traditional)" } } diff --git a/packages/node_modules/@node-red/editor-client/locales/en-US/jsonata.json b/packages/node_modules/@node-red/editor-client/locales/en-US/jsonata.json index 8f516a289..d777d1919 100755 --- a/packages/node_modules/@node-red/editor-client/locales/en-US/jsonata.json +++ b/packages/node_modules/@node-red/editor-client/locales/en-US/jsonata.json @@ -262,5 +262,9 @@ "$distinct": { "args": "array", "desc": "Returns an array with duplicate values removed from `array`" + }, + "$type": { + "args": "value", + "desc": "Returns the type of `value` as a string. If `value` is undefined, this will return `undefined`" } } diff --git a/packages/node_modules/@node-red/editor-client/locales/ja/editor.json b/packages/node_modules/@node-red/editor-client/locales/ja/editor.json old mode 100755 new mode 100644 index 8290ca1c1..9a0c23cc5 --- a/packages/node_modules/@node-red/editor-client/locales/ja/editor.json +++ b/packages/node_modules/@node-red/editor-client/locales/ja/editor.json @@ -14,7 +14,13 @@ "back": "戻る", "next": "進む", "clone": "プロジェクトをクローン", - "cont": "続ける" + "cont": "続ける", + "style": "形式", + "line": "線", + "fill": "塗りつぶし", + "label": "ラベル", + "color": "色", + "position": "配置" }, "type": { "string": "文字列", @@ -28,6 +34,13 @@ "null": "null" } }, + "event": { + "loadPalette": "パレットを読み込み中", + "loadNodeCatalogs": "ノードカタログを読み込み中", + "loadNodes": "ノードを読み込み中 __count__", + "loadFlows": "フローを読み込み中", + "importFlows": "ワークスペースにフローを追加中" + }, "workspace": { "defaultName": "フロー __number__", "editFlow": "フローを編集: __name__", @@ -67,7 +80,7 @@ "settings": "設定", "userSettings": "ユーザ設定", "nodes": "ノード", - "displayStatus": "ノードの状態を表示", + "displayStatus": "ノードのステータスを表示", "displayConfig": "ノードの設定", "import": "読み込み", "export": "書き出し", @@ -91,7 +104,12 @@ "projects-new": "新規", "projects-open": "開く", "projects-settings": "設定", - "showNodeLabelDefault": "追加したノードのラベルを表示" + "showNodeLabelDefault": "追加したノードのラベルを表示", + "groups": "グループ", + "groupSelection": "選択部分をグループ化", + "ungroupSelection": "選択部分をグループ解除", + "groupMergeSelection": "選択部分をマージ", + "groupRemoveSelection": "グループから削除" } }, "actions": { @@ -171,6 +189,8 @@ "node_plural": "__count__ 個のノード", "configNode": "__count__ 個の設定ノード", "configNode_plural": "__count__ 個の設定ノード", + "group": "__count__ 個のグループ", + "group_plural": "__count__ 個のグループ", "flow": "__count__ 個のフロー", "flow_plural": "__count__ 個のフロー", "subflow": "__count__ 個のサブフロー", @@ -186,6 +206,9 @@ "nodesImported": "読み込みました:", "nodeCopied": "__count__ 個のノードをコピーしました", "nodeCopied_plural": "__count__ 個のノードをコピーしました", + "groupCopied": "__count__ 個のグループをコピーしました", + "groupCopied_plural": "__count__ 個のグループをコピーしました", + "groupStyleCopied": "グループの形式をコピーしました", "invalidFlow": "不正なフロー: __message__", "export": { "selected": "選択したフロー", @@ -308,6 +331,13 @@ "multipleInputsToSelection": "サブフローを作成できません: 複数の入力が選択されています" } }, + "group": { + "editGroup": "__name__ グループを編集", + "errors": { + "cannotCreateDiffGroups": "異なるグループのノードを使用してグループを作成することはできません", + "cannotAddSubflowPorts": "グループにサブフローの端子を追加できません" + } + }, "editor": { "configEdit": "編集", "configAdd": "追加", @@ -351,7 +381,8 @@ "bool": "真偽", "json": "JSON", "bin": "バッファ", - "env": "環境変数" + "env": "環境変数", + "cred": "認証情報" }, "menu": { "input": "入力", @@ -538,6 +569,7 @@ "label": "情報", "node": "ノード", "type": "型", + "group": "グループ", "module": "モジュール", "id": "ID", "status": "状態", @@ -560,7 +592,20 @@ "nodeHelp": "ノードのヘルプ", "none": "なし", "arrayItems": "__count__ 要素", - "showTips": "設定からヒントを表示できます" + "showTips": "設定からヒントを表示できます", + "outline": "アウトライン", + "empty": "空", + "globalConfig": "グローバル設定ノード" + }, + "help": { + "name": "ヘルプ", + "label": "ヘルプ", + "search": "ヘルプを検索", + "nodeHelp": "ノードヘルプ", + "showHelp": "ヘルプを表示", + "showInOutline": "アウトラインに表示", + "showTopics": "トピックを表示", + "noHelp": "ヘルプのトピックが未選択" }, "config": { "name": "ノードの設定を表示", @@ -613,9 +658,9 @@ "removeFromProject": "プロジェクトから削除", "addToProject": "プロジェクトへ追加", "files": "ファイル", - "package": "パッケージ", "flow": "フロー", "credentials": "認証情報", + "package": "パッケージ", "packageCreate": "変更が保存された時にファイルが作成されます", "fileNotExist": "ファイルが存在しません", "selectFile": "ファイルを選択", @@ -756,7 +801,8 @@ "bin": "バッファ", "date": "日時", "jsonata": "JSONata式", - "env": "環境変数" + "env": "環境変数", + "cred": "認証情報" } }, "editableList": { @@ -803,9 +849,9 @@ "expandItems": "要素を展開", "collapseItems": "要素を折り畳む", "duplicate": "複製", - "error": { - "invalidJSON": "不正なJSON: " - } + "error": { + "invalidJSON": "不正なJSON: " + } }, "markdownEditor": { "title": "マークダウンエディタ", @@ -976,7 +1022,8 @@ "passphrase": "パスフレーズ", "retry": "リトライ", "update-failed": "認証の更新に失敗しました", - "unhandled": "エラー応答が処理されませんでした" + "unhandled": "エラー応答が処理されませんでした", + "host-key-verify-failed": "

ホストキーの検証に失敗

リポジトリのホストキーを検証できませんでした。known_hostsファイルを更新して、もう一度試してください。

" }, "create-branch-list": { "invalid": "不正なブランチ", @@ -1007,6 +1054,7 @@ "en-US": "英語", "ja": "日本語", "ko": "韓国語", - "zh-CN": "中国語(簡体)" + "zh-CN": "中国語(簡体)", + "zh-TW": "中国語(繁体)" } } diff --git a/packages/node_modules/@node-red/editor-client/locales/ja/jsonata.json b/packages/node_modules/@node-red/editor-client/locales/ja/jsonata.json index ba873c448..659cf66df 100755 --- a/packages/node_modules/@node-red/editor-client/locales/ja/jsonata.json +++ b/packages/node_modules/@node-red/editor-client/locales/ja/jsonata.json @@ -245,11 +245,11 @@ }, "$encodeUrl": { "args": "str", - "desc": "Uniform Resource Locator (URL)を構成する文字を1、2、3、もしくは、4文字エスケープシーケンスのUTF-8文字エンコーディングで置換します。\n\n例: `$encodeUrlComponent(\"?x=test\")` => `\"%3Fx%3Dtest\"`" + "desc": "Uniform Resource Locator (URL)を構成する文字を1、2、3、もしくは、4文字エスケープシーケンスのUTF-8文字エンコーディングで置換します。\n\n例: `$encodeUrlComponent(\"?x=test\")` => `\"%3Fx%3Dtest\"`" }, "$encodeUrlComponent": { "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\"`" + "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": { "args": "str", @@ -262,5 +262,9 @@ "$distinct": { "args": "array", "desc": "配列`array`から重複要素を削除した配列を返します。" + }, + "$type": { + "args": "value", + "desc": "`value` の型を文字列として返します。もし `value` が未定義の場合、 `undefined` が返されます。" } } diff --git a/packages/node_modules/@node-red/editor-client/locales/zh-CN/editor.json b/packages/node_modules/@node-red/editor-client/locales/zh-CN/editor.json index a45f37f66..95d3fa5ee 100644 --- a/packages/node_modules/@node-red/editor-client/locales/zh-CN/editor.json +++ b/packages/node_modules/@node-red/editor-client/locales/zh-CN/editor.json @@ -10,7 +10,22 @@ "load": "读取", "save": "保存", "import": "导入", - "export": "导出" + "export": "导出", + "back": "后退", + "next": "下一个", + "clone": "克隆项目", + "cont": "继续" + }, + "type": { + "string": "字符串", + "number": "数字", + "boolean": "布尔值", + "array": "数组", + "buffer": "buffer", + "object": "对象", + "jsonString": "JSON字符串", + "undefined": "未定义", + "null": "空" } }, "workspace": { @@ -19,10 +34,13 @@ "confirmDelete": "确认删除", "delete": "你确定想删除 '__label__'?", "dropFlowHere": "把流程放到这里", + "addFlow": "添加流程", + "listFlows": "流程一览", "status": "状态", "enabled": "有效", "disabled": "无效", - "info": "详细描述" + "info": "详细描述", + "selectNodes": "点击节点来选择" }, "menu": { "label": { @@ -36,11 +54,16 @@ "defaultDir": "默认方向", "ltr": "从左到右", "rtl": "从右到左", - "auto": "上下文" + "auto": "上下文", + "language": "语言", + "browserDefault": "浏览器默认" }, "sidebar": { "show": "显示侧边栏" }, + "palette": { + "show": "显示控制板" + }, "settings": "设置", "userSettings": "用户设置", "nodes": "节点", @@ -60,11 +83,23 @@ "keyboardShortcuts": "键盘快捷方式", "login": "登陆", "logout": "退出", - "editPalette":"节点管理", + "editPalette": "节点管理", "other": "其他", - "showTips": "显示小提示" + "showTips": "显示小提示", + "help": "Node-RED网页", + "projects": "项目", + "projects-new": "新建", + "projects-open": "打开", + "projects-settings": "项目设定", + "showNodeLabelDefault": "显示新添加的节点的标签" } }, + "actions": { + "toggle-navigator": "切换导航器", + "zoom-out": "缩小", + "zoom-reset": "重设缩放", + "zoom-in": "放大" + }, "user": { "loggedInAs": "作为__name__登陆", "username": "账号", @@ -82,29 +117,73 @@ "warning": "警告: __message__", "warnings": { "undeployedChanges": "节点中存在未部署的更改", + "nodeActionDisabled": "节点操作已禁用", "nodeActionDisabledSubflow": "节点动作在子流程中被禁用", "missing-types": "流程由于缺少节点类型而停止。请检查日志的详细信息", - "restartRequired": "Node-RED必须重新启动,以启用升级的模块" + "safe-mode": "

流程以安全模式停止。

您可以修改流程并部署更改以重新启动。

", + "restartRequired": "Node-RED必须重新启动,以启用升级的模块", + "credentials_load_failed": "

由于无法解密凭据,因此流程停止。

流程凭据文件已加密,但是项目的加密密钥丢失或无效。

", + "credentials_load_failed_reset": "

凭据无法解密

流凭据文件已加密,但是项目的加密密钥丢失或无效。

流凭据文件将在下一次部署时重置。任何现有的流凭证将被清除。

", + "missing_flow_file": "

找不到项目流程文件。

该项目未配置流程文件。

", + "missing_package_file": "

找不到项目包文件。

项目缺少package.json文件。

", + "project_empty": "

该项目为空。

是否要创建一组默认的项目文件?
否则,您将必须在编辑器外部手动将文件添加到项目中。

", + "project_not_found": "

未找到项目'__project__'。

", + "git_merge_conflict": "

自动合并更改失败。

修复未合并的冲突,然后提交结果。

" }, - "error": "Error: __message__", + "error": "错误: __message__", "errors": { "lostConnection": "丢失与服务器的连接,重新连接...", "lostConnectionReconnect": "丢失与服务器的连接,__time__秒后重新连接", "lostConnectionTry": "现在尝试", "cannotAddSubflowToItself": "无法向其自身添加子流程", "cannotAddCircularReference": "无法添加子流程 - 循环引用", - "unsupportedVersion": "您正在使用不受支持的Node.js版本
请升级到最新版本的Node.js LTS" + "unsupportedVersion": "您正在使用不受支持的Node.js版本
请升级到最新版本的Node.js LTS", + "failedToAppendNode": "

'__module__'加载失败

__error__

" + }, + "project": { + "change-branch": "转到本地分支'__project__'", + "merge-abort": "Git合并中止", + "loaded": "项目'__project__'已加载", + "updated": "项目'__project__'已更新", + "pull": "项目'__project__'已重新加载", + "revert": "项目 '__project__'已还原", + "merge-complete": "Git合并完成", + "setupCredentials": "设定证书", + "setupProjectFiles": "设置项目文件", + "no": "不了,谢谢", + "createDefault": "创建默认项目文件", + "mergeConflict": "显示合并冲突" + }, + "label": { + "manage-project-dep": "管理项目依赖性", + "setup-cred": "设定证书", + "setup-project": "设置项目文件", + "create-default-package": "创建默认的包文件", + "no-thanks": "不了,谢谢", + "create-default-project": "创建默认项目文件", + "show-merge-conflicts": "显示合并冲突" } }, "clipboard": { "clipboard": "剪贴板", "nodes": "节点", + "node": "__count__节点", + "node_plural": "__count__节点", + "configNode": "__count__配置节点", + "configNode_plural": "__count__配置节点", + "flow": "__count__流程", + "flow_plural": "__count__流程", + "subflow": "__count__子流程", + "subflow_plural": "__count__子流程", "pasteNodes": "在这里粘贴节点", + "selectFile": "选择要导入的文件", "importNodes": "导入节点", "exportNodes": "导出节点至剪贴板", + "download": "下载", "importUnrecognised": "导入了无法识别的类型:", "importUnrecognised_plural": "导入了无法识别的类型:", "nodesExported": "节点导出到了剪贴板", + "nodesImported": "导入:", "nodeCopied": "已复制__count__个节点", "nodeCopied_plural": "已复制__count__个节点", "invalidFlow": "无效的流程: __message__", @@ -114,11 +193,21 @@ "all": "所有流程", "compact": "紧凑", "formatted": "已格式化", - "copy": "导出到剪贴板" + "copy": "导出到剪贴板", + "export": "到处到库", + "exportAs": "导出为", + "overwrite": "替换", + "exists": "

\"__file__\"已存在

是否要替换它?

" }, "import": { "import": "导入到", - "newFlow": "新流程" + "newFlow": "新流程", + "errors": { + "notArray": "输入的不是JSON数组", + "itemNotObject": "输入的流无效 - 项目__index__不是节点对象", + "missingId": "输入的流无效-项 __index__ 缺少'id'属性", + "missingType": "输入的流程无效-项__index__缺少'类型'属性" + } }, "copyMessagePath": "已复制路径", "copyMessageValue": "已复制数值", @@ -132,7 +221,10 @@ "modifiedFlowsDesc": "只部署包含已更改节点的流", "modifiedNodes": "已更改的节点", "modifiedNodesDesc": "只部署已经更改的节点", + "restartFlows": "重启流程", + "restartFlowsDesc": "重新启动当前部署的流程", "successfulDeploy": "部署成功", + "successfulRestart": "成功重启流程", "deployFailed": "部署失败: __message__", "unusedConfigNodes": "您有一些未使用的配置节点", "unusedConfigNodesLink": "点击此处查看它们", @@ -152,16 +244,24 @@ "improperlyConfigured": "工作区包含一些未正确配置的节点:", "unknown": "工作区包含一些未知的节点类型:", "confirm": "你确定要部署吗?", + "doNotWarn": "不要再对此发出警告", "conflict": "服务器正在运行较新的一组流程。", "backgroundUpdate": "服务器上的流程已更新。", "conflictChecking": "检查是否可以自动合并更改", "conflictAutoMerge": "此更改不包括冲突,可以自动合并", - "conflictManualMerge": "这些更改包括了在部署之前必须解决的冲突。" + "conflictManualMerge": "这些更改包括了在部署之前必须解决的冲突。", + "plusNMore": "+ __count__更多" } }, + "eventLog": { + "title": "事件记录日志", + "view": "查看日志" + }, "diff": { "unresolvedCount": "__count__个未解决的冲突", "unresolvedCount_plural": "__count__个未解决的冲突", + "globalNodes": "全局节点", + "flowProperties": "流程属性", "type": { "added": "已添加", "changed": "已更改", @@ -175,9 +275,19 @@ "nodeCount": "__count__个节点", "nodeCount_plural": "__count__个节点", "local": "本地", - "remote": "远程" + "remote": "远程", + "reviewChanges": "查看变更", + "noBinaryFileShowed": "无法显示二进制文件内容", + "viewCommitDiff": "查看提交更改", + "compareChanges": "比较变更", + "saveConflict": "保存冲突解决", + "conflictHeader": "已解决__unresolved__中的__resolved__个冲突", + "commonVersionError": "通用版本不包含有效的JSON:", + "oldVersionError": "旧版本不包含有效的JSON:", + "newVersionError": "新版本不包含有效的JSON:" }, "subflow": { + "editSubflowInstance": "编辑子流实例:__name__", "editSubflow": "编辑流程模板: __name__", "edit": "编辑流程模板", "subflowInstances": "这个子流程模板有__count__个实例", @@ -185,8 +295,14 @@ "editSubflowProperties": "编辑属性", "input": "输入:", "output": "输出:", + "status": "状态节点", "deleteSubflow": "删除子流程", "info": "详细描述", + "category": "类别", + "env": { + "restore": "恢复为默认子流", + "remove": "删除环境变量" + }, "errors": { "noNodesSelected": "无法创建子流程: 未选择节点", "multipleInputsToSelection": "无法创建子流程: 多个输入到了选择" @@ -204,18 +320,68 @@ "editConfig": "编辑__type__配置", "addNewType": "添加新的__type__节点", "nodeProperties": "节点属性", + "label": "标签", + "color": "颜色", "portLabels": "端口标签", "labelInputs": "输入", "labelOutputs": "输出", + "settingIcon": "图标", + "default": "默认", "noDefaultLabel": "无", "defaultLabel": "使用默认标签", + "searchIcons": "搜索图标", + "useDefault": "使用默认", + "description": "描述", + "show": "显示", + "hide": "隐藏", + "locale": "选择界面语言", + "icon": "图标", + "inputType": "输入类型", + "inputs": { + "input": "输入", + "select": "选择", + "checkbox": "复选框", + "spinner": "微调器", + "none": "空", + "hidden": "隐藏属性" + }, + "types": { + "str": "字符串", + "num": "数字", + "bool": "布尔", + "json": "JSON", + "bin": "buffer", + "env": "环境变量" + }, + "menu": { + "input": "输入", + "select": "选择", + "checkbox": "复选框", + "spinner": "微调器", + "hidden": "仅标签" + }, + "select": { + "label": "标签", + "value": "值" + }, + "spinner": { + "min": "最小值", + "max": "最大值" + }, "errors": { - "scopeChange": "更改范围将使其他流中的节点无法使用" + "scopeChange": "更改范围将使其他流中的节点无法使用", + "invalidProperties": "无效的属性:" } }, "keyboard": { "title": "键盘快捷键", + "keyboard": "键盘", + "filterActions": "筛选动作", + "shortcut": "快捷键", + "scope": "范围", "unassigned": "未分配", + "global": "全局", + "workspace": "工作组", "selectAll": "选择所有节点", "selectAllConnected": "选择所有连接的节点", "addRemoveNode": "从选择中添加/删除节点", @@ -226,12 +392,14 @@ "nudgeNode": "移动所选节点(1px)", "moveNode": "移动所选节点(20px)", "toggleSidebar": "切换侧边栏", + "togglePalette": "切换控制板", "copyNode": "复制所选节点", "cutNode": "剪切所选节点", "pasteNode": "粘贴节点", "undoChange": "撤消上次执行的更改", "searchBox": "打开搜索框", - "managePalette": "管理面板" + "managePalette": "管理面板", + "actionList": "动作列表" }, "library": { "library": "库", @@ -239,30 +407,42 @@ "saveToLibrary": "保存到库...", "typeLibrary": "__type__类型库", "unnamedType": "无名__type__", - "exportToLibrary": "将节点导出到库", + "exportedToLibrary": "节点导出到库", "dialogSaveOverwrite": "一个叫做__libraryName__的__libraryType__已经存在,您需要覆盖么?", "invalidFilename": "无效的文件名", "savedNodes": "保存的节点", "savedType": "已保存__type__", "saveFailed": "保存失败: __message__", + "newFolder": "新文件夹", "types": { + "local": "本地的", "examples": "例子" - } + }, + "exportToLibrary": "将节点导出到库" }, "palette": { "noInfo": "无可用信息", "filter": "过滤节点", "search": "搜索模块", + "addCategory": "添加新的...", "label": { "subflows": "子流程", + "network": "网络", + "common": "共通", "input": "输入", "output": "输出", "function": "功能", + "sequence": "序列", + "parser": "解析", "social": "社交", "storage": "存储", "analysis": "分析", "advanced": "高级" }, + "actions": { + "collapse-all": "收起所有类别", + "expand-all": "展开所有类别" + }, "event": { "nodeAdded": "添加到面板中的节点:", "nodeAdded_plural": "添加到面板中的多个节点", @@ -276,6 +456,7 @@ }, "editor": { "title": "面板管理", + "palette": "控制板", "times": { "seconds": "秒前", "minutes": "分前", @@ -309,6 +490,8 @@ "updated": "已更新", "install": "安装", "installed": "已安装", + "conflict": "冲突", + "conflictTip": "

无法安装此模块,因为它包含已安装的
节点类型

__module__冲突

", "loading": "加载目录...", "tab-nodes": "节点", "tab-install": "安装", @@ -356,6 +539,7 @@ "label": "信息", "node": "节点", "type": "类型", + "module": "模组", "id": "ID", "status": "状态", "enabled": "启用", @@ -364,17 +548,18 @@ "instances": "实例", "properties": "属性", "info": "信息", + "desc": "描述", "blank": "空白", "null": "空", "showMore": "展开", "showLess": "收起", "flow": "流程", - "selection":"选择", - "nodes":"__count__ 个节点", + "selection": "选择", + "nodes": "__count__ 个节点", "flowDesc": "流程描述", "subflowDesc": "子流程描述", "nodeHelp": "节点帮助", - "none":"无", + "none": "无", "arrayItems": "__count__个项目", "showTips": "您可以从设置面板启用提示信息" }, @@ -386,9 +571,25 @@ "subflows": "子流程", "flows": "流程", "filterAll": "所有", + "showAllConfigNodes": "显示所有配置节点", "filterUnused": "未使用", + "showAllUnusedConfigNodes": "显示所有未使用的配置节点", "filtered": "__count__ 个隐藏" }, + "context": { + "name": "上下文数据", + "label": "上下午", + "none": "未选择", + "refresh": "刷新以加载", + "empty": "空", + "node": "节点", + "flow": "流程", + "global": "全局", + "deleteConfirm": "你确定要删除这个项目吗?", + "autoRefresh": "刷新选择更改", + "refrsh": "刷新", + "delete": "删除" + }, "palette": { "name": "节点管理", "label": "节点" @@ -399,8 +600,151 @@ "description": "描述", "dependencies": "依赖", "settings": "设置", + "noSummaryAvailable": "无可用摘要", "editDescription": "编辑项目描述", - "editDependencies": "编辑项目依赖" + "editDependencies": "编辑项目依赖", + "noDescriptionAvailable": "没有可用的描述", + "editReadme": "编辑README.md", + "showProjectSettings": "显示项目设置", + "projectSettings": { + "title": "项目设置", + "edit": "编辑", + "none": "空", + "install": "安装", + "removeFromProject": "从项目中删除", + "addToProject": "添加到项目", + "files": "文件", + "package": "包", + "flow": "流程", + "credentials": "证书", + "packageCreate": "保存更改后将创建文件", + "fileNotExist": "文件不存在", + "selectFile": "选择文件", + "invalidEncryptionKey": "无效的加密密钥", + "encryptionEnabled": "启用加密", + "encryptionDisabled": "加密已禁用", + "setTheEncryptionKey": "设置加密密钥", + "resetTheEncryptionKey": "重置加密密钥", + "changeTheEncryptionKey": "更改加密密钥", + "currentKey": "当前密钥", + "newKey": "新密钥", + "credentialsAlert": "这将删除所有现有凭证", + "versionControl": "版本控制", + "branches": "分支", + "noBranches": "没有分支", + "deleteConfirm": "您确定要删除本地分支'__name__'吗? 这不能被撤消。", + "unmergedConfirm": "本地分支'__name__'具有未合并的更改,这些更改将丢失。你确定要删除吗?", + "deleteUnmergedBranch": "删除未合并的分支", + "gitRemotes": "Git远程仓库", + "addRemote": "添加远程仓库", + "addRemote2": "添加远程仓库", + "remoteName": "远程仓库名", + "nameRule": "只能包含A-Z 0-9 _ -", + "url": "URL", + "urlRule": "https://, ssh:// or file://", + "urlRule2": "网址中不能包含用户名/密码", + "noRemotes": "没有远程仓库", + "deleteRemoteConfrim": "您确定要删除远程仓库'__name__'吗?", + "deleteRemote": "删除远程仓库" + }, + "userSettings": { + "committerDetail": "提交者详细信息", + "committerTip": "保留空白以使用系统默认值", + "userName": "用户名", + "email": "电子邮件", + "sshKeys": "SSH密钥", + "sshKeysTip": "允许您创建到远程git存储库的安全连接。", + "add": "添加密钥", + "addSshKey": "添加SSH密钥", + "addSshKeyTip": "生成新的公钥/私钥对", + "name": "名字", + "nameRule": "只能包含A-Z 0-9 _ -", + "passphrase": "密码短语", + "passphraseShort": "密码短语过短", + "optional": "可选的", + "cancel": "取消", + "generate": "生成密钥", + "noSshKeys": "没有SSH密钥", + "copyPublicKey": "将公钥复制到剪贴板", + "delete": "删除密钥", + "gitConfig": "Git配置", + "deleteConfirm": "您确定要删除SSH密钥__name__吗?这不能被撤消。" + }, + "versionControl": { + "unstagedChanges": "未暂存的变更", + "stagedChanges": "暂存的变更", + "unstageChange": "取消变更的暂存", + "stageChange": "暂存变更", + "unstageAllChange": "取消所有变更的暂存", + "stageAllChange": "暂存所有变更", + "commitChanges": "提交变更", + "resolveConflicts": "解决冲突", + "head": "HEAD", + "staged": "暂存的", + "unstaged": "未暂存的", + "local": "本地的", + "remote": "远程的", + "revert": "您确定要将更改恢复为'__file__'吗?这不能被撤消。", + "revertChanges": "还原变更", + "localChanges": "本地变更", + "none": "None", + "conflictResolve": "解决所有冲突。提交更改以完成合并。", + "localFiles": "本地文件", + "all": "所有的", + "unmergedChanges": "未合并的更改", + "abortMerge": "中止合并", + "commit": "提交", + "changeToCommit": "提交变更", + "commitPlaceholder": "输入您的提交信息", + "cancelCapital": "取消", + "commitCapital": "提交", + "commitHistory": "提交历史", + "branch": "分支:", + "moreCommits": "更多提交", + "changeLocalBranch": "变更本地分支", + "createBranchPlaceholder": "查找或创建分支", + "upstream": "上游", + "localOverwrite": "切换分支会覆盖您现有的本地更改。您必须先提交或撤消那些更改。", + "manageRemoteBranch": "管理远程分支", + "unableToAccess": "无法访问远程存储库", + "retry": "重试", + "setUpstreamBranch": "设置为上游分支", + "createRemoteBranchPlaceholder": "查找或创建远程分支", + "trackedUpstreamBranch": "创建的分支将被设置为跟踪的上游分支。", + "selectUpstreamBranch": "分支将被创建。 在下面选择以将其设置为被跟踪的上游分支。", + "pushFailed": "推送失败,因为远程具有更多的最新提交。请先拉取并合并,然后再尝试推送。", + "push": "推送", + "pull": "拉取", + "unablePull": "

无法提取远程更改;您未暂存的本地更改将被覆盖。

请先提交更改,然后重试。

", + "showUnstagedChanges": "显示未暂存的更改", + "connectionFailed": "无法连接到远程存储库:", + "pullUnrelatedHistory": "

远程有无关的提交历史

您确定要将这些更改拉入本地仓库吗?

", + "pullChanges": "拉取更改", + "history": "历史", + "projectHistory": "项目历史", + "daysAgo": "__count__天前", + "daysAgo_plural": "__count__天前", + "hoursAgo": "__count__小时前", + "hoursAgo_plural": "__count__小时前", + "minsAgo": "__count__分钟前", + "minsAgo_plural": "__count__分钟前", + "secondsAgo": "秒前", + "notTracking": "您的本地分支当前未跟踪一个远程分支。", + "statusUnmergedChanged": "您的仓库中有未合并的更改。您需要解决冲突并提交结果。", + "repositoryUpToDate": "您的仓库是最新的。", + "commitsAhead": "您的存储库领先远程仓库__count__次提交。您现在可以推送这些提交。", + "commitsAhead_plural": "您的存储库领先远程仓库__count__次提交。您现在可以推送这些提交。", + "commitsBehind": "您的存储库落后远程仓库__count__次提交。您现在可以拉取这些提交。", + "commitsBehind_plural": "您的存储库落后远程仓库__count__次提交。您现在可以拉取这些提交。", + "commitsAheadAndBehind1": "您的存储库落后远程仓库__count__次提交", + "commitsAheadAndBehind1_plural": "您的存储库落后远程仓库__count__次提交", + "commitsAheadAndBehind2": "领先远程仓库__count__次提交。", + "commitsAheadAndBehind2_plural": "领先远程仓库__count__次提交。", + "commitsAheadAndBehind3": "您必须先拉取远程提交,然后才能进行推送。", + "commitsAheadAndBehind3_plural": "您必须先拉取远程提交,然后才能进行推送。", + "refreshCommitHistory": "刷新提交历史", + "refreshChanges": "刷新更改" + } } }, "typedInput": { @@ -408,10 +752,12 @@ "str": "文字列", "num": "数字", "re": "正则表达式", - "bool": "布尔", + "bool": "布尔值", "json": "JSON", "bin": "二进制流", - "date": "时间戳" + "date": "时间戳", + "jsonata": "表达式", + "env": "环境变量" } }, "editableList": { @@ -423,8 +769,10 @@ }, "expressionEditor": { "functions": "功能", + "functionReference": "功能reference", "insert": "插入", "title": "JSONata表达式编辑器", + "test": "测试", "data": "示例消息", "result": "结果", "format": "格式表达方法", @@ -438,14 +786,229 @@ "eval": "评估表达式错误:\n __message__" } }, + "jsEditor": { + "title": "JavaScript编辑器" + }, + "textEditor": { + "title": "文本编辑器" + }, "jsonEditor": { "title": "JSON编辑器", - "format": "格式化JSON" + "format": "格式化JSON", + "rawMode": "编辑 JSON", + "uiMode": "Visual编辑器", + "insertAbove": "在上方插入", + "insertBelow": "在下方插入", + "addItem": "添加项目", + "copyPath": "复制路径到项目", + "expandItems": "展开项目", + "collapseItems": "收合项目", + "duplicate": "重复", + "error": { + "invalidJSON": "无效的JSON: " + } + }, + "markdownEditor": { + "title": "Markdown编辑器", + "expand": "展开", + "format": "格式化为markdown", + "heading1": "标题 1", + "heading2": "标题 2", + "heading3": "标题 3", + "bold": "粗体", + "italic": "斜体", + "code": "代码", + "ordered-list": "排序的列表", + "unordered-list": "非排序的列表", + "quote": "引用", + "link": "链接", + "horizontal-rule": "水平线", + "toggle-preview": "切换预览" }, "bufferEditor": { "title": "缓冲区编辑器", "modeString": "作为UTF-8字符串处理", "modeArray": "作为JSON数组处理", "modeDesc": "

缓冲区编辑器

缓冲区类型被存储为字节值的JSON数组。编辑器将尝试将输入的数值解析为JSON数组。如果它不是有效的JSON,它将被视为UTF-8字符串,并被转换为单个字符代码点的数组。

例如,Hello World的值会被转换为JSON数组:

[72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100]

" + }, + "projects": { + "config-git": "配置Git客户端", + "welcome": { + "hello": "你好! 我们已经将“项目”引入了Node-RED。", + "desc0": "这是一种用于管理流程文件的新方法,并且包括对流程的版本控制。", + "desc1": "首先,您可以创建您的第一个项目或从git存储库克隆现有项目。", + "desc2": "如果不确定,可以暂时跳过此步骤。您仍然可以随时通过“项目”菜单创建第一个项目。", + "create": "建立专案", + "clone": "克隆仓库", + "openExistingProject": "打开现有项目", + "not-right-now": "不是现在" + }, + "git-config": { + "setup": "设置您的版本控制客户端", + "desc0": "Node-RED使用开源工具Git进行版本控制。它跟踪对项目文件的更改,并允许您将其推送到远程存储库。", + "desc1": "提交一组更改时,Git会使用用户名和电子邮件地址记录谁进行了更改。用户名可以是您想要的任何名称-不必是您的真实姓名。", + "desc2": "您的Git客户端已经配置了以下详细信息。", + "desc3": "您可以稍后在设置对话框的'Git config'标签下更改这些设置。", + "username": "用户名", + "email": "电子邮件" + }, + "project-details": { + "create": "创建你的项目", + "desc0": "项目被维护为Git仓库。与他人一起共享您的流程", + "desc1": "您可以创建多个项目,并通过编辑器在它们之间快速切换。", + "desc2": "首先,您的项目需要一个名称和一个可选的描述。", + "already-exists": "项目已存在", + "must-contain": "只能包含A-Z 0-9 _ -", + "project-name": "项目名", + "desc": "描述", + "opt": "可选的" + }, + "clone-project": { + "clone": "克隆一个项目", + "desc0": "如果您已经有一个包含项目的git仓库,则可以对其进行克隆以开始使用。", + "already-exists": "项目已存在", + "must-contain": "只能包含A-Z 0-9 _ -", + "project-name": "项目名", + "no-info-in-url": "网址中不要包含用户名/密码", + "git-url": "Git仓库的url", + "protocols": "https://, ssh:// or file://", + "auth-failed": "认证失败", + "username": "用户名", + "passwd": "秘密啊", + "ssh-key": "SSH密钥", + "passphrase": "密码短语", + "ssh-key-desc": "在通过ssh克隆仓库之前,必须添加SSH密钥才能访问它。", + "ssh-key-add": "添加一个ssh密钥", + "credential-key": "证书加密密钥", + "cant-get-ssh-key": "错误! 无法获取所选的SSH密钥路径。", + "already-exists2": "已存在", + "git-error": "git错误", + "connection-failed": "连接失败", + "not-git-repo": "不是一个git仓库", + "repo-not-found": "未发现仓库" + }, + "default-files": { + "create": "创建您的项目文件", + "desc0": "一个包含您的流程文件,Readme文件和package.json文件的项目。", + "desc1": "它可以包含您要在Git仓库中维护的任何其他文件。", + "desc2": "您现有的流程和凭证文件将被复制到项目中。", + "flow-file": "流程文件", + "credentials-file": "证书文件" + }, + "encryption-config": { + "setup": "设置证书文件的加密", + "desc0": "您的流程证书文件可以被加密以确保其内容安全。", + "desc1": "如果要将这些证书存储在公共Git存储库中,则必须通过提供密钥短语来对它们进行加密。", + "desc2": "您的流程证书文件当前未加密。", + "desc3": "这意味着任何有权访问该文件的人都可以读取其内容,例如密码和访问令牌。", + "desc4": "如果要将这些证书存储在公共Git仓库中,则必须通过提供密钥短语来对它们进行加密。", + "desc5": "当前,使用设置文件中的credentialSecret属性作为密钥来加密流程证书文件。", + "desc6": "您的流程证书文件当前使用系统生成的密钥加密。您应该为此项目提供一个新的密钥。", + "desc7": "密钥将与项目文件分开存储。您将需要提供在另一个Node-RED实例中使用该项目的密钥。", + "credentials": "证书", + "enable": "启用加密", + "disable": "禁用加密", + "disabled": "禁用的", + "copy": "复制现有密钥", + "use-custom": "使用自定义密钥", + "desc8": "证书文件不会被加密,其内容很容易阅读", + "create-project-files": "创建项目文件", + "create-project": "创建项目", + "already-exists": "已存在", + "git-error": "git错误", + "git-auth-error": "git认证错误" + }, + "create-success": { + "success": "您已经成功创建了第一个项目!", + "desc0": "现在,您可以像往常一样继续使用Node-RED。", + "desc1": "侧栏中的“信息”标签显示了您当前的活动项目。名称旁边的按钮可用于访问项目设置视图。", + "desc2": "侧栏中的“历史记录”标签可用于查看项目中已更改的文件并提交。它向您显示了提交的完整历史记录,并允许您将更改推送到远程存储库。" + }, + "create": { + "projects": "项目", + "already-exists": "项目已存在", + "must-contain": "只能包含A-Z 0-9 _ -", + "no-info-in-url": "网址中不要包含用户名/密码", + "open": "打开项目", + "create": "创建项目", + "clone": "克隆仓库", + "project-name": "项目名", + "desc": "描述", + "opt": "可选的", + "flow-file": "流程文件", + "credentials": "证书", + "enable-encryption": "启用加密", + "disable-encryption": "禁用加密", + "encryption-key": "加密密钥", + "desc0": "用来保护您的凭证的短语", + "desc1": "凭证文件不会被加密,其内容很容易阅读", + "git-url": "Git存储库URL", + "protocols": "https://, ssh:// or file://", + "auth-failed": "验证失败", + "username": "用户名", + "password": "密码", + "ssh-key": "SSH密钥", + "passphrase": "密码短语", + "desc2": "在通过ssh克隆存储库之前,必须添加SSH密钥才能访问它。", + "add-ssh-key": "添加一个ssh密钥", + "credentials-encryption-key": "证书加密密钥", + "already-exists-2": "已存在", + "git-error": "git错误", + "con-failed": "连接失败", + "not-git": "不是git仓库", + "no-resource": "找不到存储库", + "cant-get-ssh-key-path": "错误!无法获取所选的SSH密钥路径。", + "unexpected_error": "意外的错误" + }, + "delete": { + "confirm": "您确定要删除此项目吗?" + }, + "create-project-list": { + "search": "搜索您的项目", + "current": "当前的" + }, + "require-clean": { + "confirm": "

您有未部署的更改,这些更改将丢失。

您要继续吗?

" + }, + "send-req": { + "auth-req": "存储库需要认证", + "username": "用户名", + "password": "秘密", + "passphrase": "密码短语", + "retry": "重试", + "update-failed": "无法更新身份验证", + "unhandled": "未处理的错误响应" + }, + "create-branch-list": { + "invalid": "无效的分支", + "create": "创建分支", + "current": "当前的" + }, + "create-default-file-set": { + "no-active": "没有活动项目就无法创建默认文件集", + "no-empty": "无法在非空项目上创建默认文件集", + "git-error": "git错误" + }, + "errors": { + "no-username-email": "您的Git客户端未配置用户名/电子邮件。", + "unexpected": "发生了一个意料之外的问题", + "code": "代码" + } + }, + "editor-tab": { + "properties": "属性", + "envProperties": "环境变量", + "description": "描述", + "appearance": "外观", + "preview": "UI预览", + "defaultValue": "默认值" + }, + "languages": { + "de": "德语", + "en-US": "英文", + "ja": "日语", + "ko": "韩文", + "zh-CN": "简体中文", + "zh-TW": "繁体中文" } } diff --git a/packages/node_modules/@node-red/editor-client/locales/zh-CN/jsonata.json b/packages/node_modules/@node-red/editor-client/locales/zh-CN/jsonata.json index 669a7683a..f27ec1f51 100644 --- a/packages/node_modules/@node-red/editor-client/locales/zh-CN/jsonata.json +++ b/packages/node_modules/@node-red/editor-client/locales/zh-CN/jsonata.json @@ -214,5 +214,57 @@ "$toMillis": { "args": "timestamp", "desc": "将ISO 8601格式的字符串`timestamp`转换为从UNIX时间 (1970年1月1日 UTC/GMT的午夜)开始到现在的毫秒数。如果该字符串的格式不正确,则抛出错误。" + }, + "$env": { + "args": "arg", + "desc": "返回环境变量的值。\n\n这是Node-RED定义的函数。" + }, + "$eval": { + "args": "expr [, context]", + "desc": "使用当前上下文来作为评估依据,分析并评估字符串`expr`,其中包含文字JSON或JSONata表达式。" + }, + "$formatInteger": { + "args": "number, picture", + "desc": "将“数字”转换为字符串,并将其格式化为“图片”字符串指定的整数表示形式。图片字符串参数定义了数字的格式,并具有与XPath F&O 3.1 规范中的fn:format-integer相同的语法。" + }, + "$parseInteger": { + "args": "string, picture", + "desc": "使用“图片”字符串指定的格式将“字符串”参数的内容解析为整数(作为JSON数字)。图片字符串参数与$formatInteger格式相同。." + }, + "$error": { + "args": "[str]", + "desc": "引发错误并显示一条消息。 可选的`str`将替代$error()函数评估的默认消息。" + }, + "$assert": { + "args": "arg, str", + "desc": "如果`arg`为真,则该函数返回。 如果arg为假,则抛出带有str的异常作为异常消息。" + }, + "$single": { + "args": "array, function", + "desc": "返回满足参数function谓语的array参数中的唯一值 (比如:传递值时,函数返回布尔值“true”)。如果匹配值的数量不唯一时,则抛出异常。\n\n应在以下签名中提供函数:`function(value [,index [,array []]])`其中value是数组的每个输入,index是该值的位置,整个数组作为第三个参数传递。" + }, + "$encodeUrl": { + "args": "str", + "desc": "通过用表示字符的UTF-8编码的一个,两个,三个或四个转义序列替换某些字符的每个实例,对统一资源定位符(URL)组件进行编码。\n\n示例:`$encodeUrlComponent(\"?x=test\")` => `\"%3Fx%3Dtest\"`" + }, + "$encodeUrlComponent": { + "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": { + "args": "str", + "desc": "解码以前由encodeUrlComponent创建的统一资源定位器(URL)组件。 \n\n示例: `$decodeUrlComponent(\"%3Fx%3Dtest\")` => `\"?x=test\"`" + }, + "$decodeUrlComponent": { + "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=шеллы\"`" + }, + "$distinct": { + "args": "array", + "desc": "返回一个数组,其中重复的值已从`数组`中删除" + }, + "$type": { + "args": "value", + "desc": "以字符串形式返回`值`的类型。 如果该`值`未定义,则将返回`未定义`" } } diff --git a/packages/node_modules/@node-red/editor-client/locales/zh-TW/editor.json b/packages/node_modules/@node-red/editor-client/locales/zh-TW/editor.json new file mode 100644 index 000000000..897599bc7 --- /dev/null +++ b/packages/node_modules/@node-red/editor-client/locales/zh-TW/editor.json @@ -0,0 +1,1015 @@ +{ + "common": { + "label": { + "name": "名稱", + "ok": "確認", + "done": "完成", + "cancel": "取消", + "delete": "刪除", + "close": "關閉", + "load": "讀取", + "save": "保存", + "import": "匯入", + "export": "匯出", + "back": "返回", + "next": "下一步", + "clone": "複製專案", + "cont": "Continue" + }, + "type": { + "string": "字符串", + "number": "數值", + "boolean": "布林", + "array": "數組", + "buffer": "buffer", + "object": "對象", + "jsonString": "JSON字符串", + "undefined": "未定義", + "null": "空" + } + }, + "workspace": { + "defaultName": "流程__number__", + "editFlow": "編輯流程: __name__", + "confirmDelete": "確認刪除", + "delete": "確定想要刪除 '__label__'?", + "dropFlowHere": "把流程放到這裡", + "addFlow": "新增流程", + "listFlows": "流程列表", + "status": "狀態", + "enabled": "有效", + "disabled": "無效", + "info": "詳細描述", + "selectNodes": "點擊節點用於選擇" + }, + "menu": { + "label": { + "view": { + "view": "顯示", + "grid": "格線", + "showGrid": "顯示格線", + "snapGrid": "對齊格線", + "gridSize": "格線尺寸", + "textDir": "文本方向", + "defaultDir": "默認方向", + "ltr": "從左到右", + "rtl": "從右到左", + "auto": "上下文", + "language": "語言", + "browserDefault": "瀏覽器默認" + }, + "sidebar": { + "show": "顯示側邊欄" + }, + "palette": { + "show": "顯示控制板" + }, + "settings": "設置", + "userSettings": "使用者設置", + "nodes": "節點", + "displayStatus": "顯示節點狀態", + "displayConfig": "修改節點配置", + "import": "匯入", + "export": "匯出", + "search": "搜尋流程", + "searchInput": "搜尋流程", + "subflows": "子流程", + "createSubflow": "新建子流程", + "selectionToSubflow": "將選擇部分更改為子流程", + "flows": "流程", + "add": "增加", + "rename": "重新命名", + "delete": "刪除", + "keyboardShortcuts": "鍵盤快速鍵", + "login": "登入", + "logout": "退出", + "editPalette": "節點管理", + "other": "其他", + "showTips": "顯示小提示", + "help": "Node-RED website", + "projects": "專案", + "projects-new": "新專案", + "projects-open": "開啟專案", + "projects-settings": "專案設定", + "showNodeLabelDefault": "顯示新添加節點的標籤" + } + }, + "actions": { + "toggle-navigator": "切換導航器", + "zoom-out": "縮小", + "zoom-reset": "重置縮放", + "zoom-in": "放大" + }, + "user": { + "loggedInAs": "作為__name__登入", + "username": "帳號", + "password": "密碼", + "login": "登入", + "loginFailed": "登入失敗", + "notAuthorized": "未授權", + "errors": { + "settings": "設置資訊需要登入後才能訪問", + "deploy": "改動需要登入後才能部署", + "notAuthorized": "此操作需要登入後才能執行" + } + }, + "notification": { + "warning": "警告: __message__", + "warnings": { + "undeployedChanges": "節點中存在未部署的更改", + "nodeActionDisabled": "節點動作在子流程中被禁用", + "nodeActionDisabledSubflow": "子流程中禁用了節點操作", + "missing-types": "流程由於缺少節點類型而停止。請檢查日誌的詳細資訊", + "safe-mode": "

流程在安全模式下停止。

您可以修改流程並部署更改以重新啟動。

", + "restartRequired": "Node-RED必須重新啟動,以啟用升級的模組", + "credentials_load_failed": "

流程由於無法解密證書而停止。

流程證書文件已加密,但是項目的加密密鑰丟失或無效。

", + "credentials_load_failed_reset": "

證書無法解密

流程證書文件已加密,但是項目的加密密鑰丟失或無效。

流程證書文件將在下一次部署時重置。任何現有的流程證書將被清除。

", + "missing_flow_file": "

找不到項目流程文件。

該項目未配置流程文件。

", + "missing_package_file": "

找不到項目包文件。

項目缺少package.json文件。

", + "project_empty": "

該項目為空。

是否要創建一組默認的項目文件?
否則,您將不得不在編輯器外部手動將文件添加到項目中。

", + "project_not_found": "

找不到項目的'__project__'

", + "git_merge_conflict": "

自動合併更改失敗。

修復未合併的衝突,然後提交結果。

" + }, + "error": "Error: __message__", + "errors": { + "lostConnection": "丟失與伺服器的連接,重新連接...", + "lostConnectionReconnect": "丟失與伺服器的連接,__time__秒後重新連接", + "lostConnectionTry": "現在嘗試", + "cannotAddSubflowToItself": "無法向其自身添加子流程", + "cannotAddCircularReference": "無法添加子流程 - 迴圈引用", + "unsupportedVersion": "您正在使用不受支持的Node.js版本
請升級到最新版本的Node.js LTS", + "failedToAppendNode": "

加載'__module__'失敗

__error__

" + }, + "project": { + "change-branch": "轉到本地分支'__project__'", + "merge-abort": "Git合併中止", + "loaded": "已加載項目'__project__'", + "updated": "已更新項目'__project__'", + "pull": "已重新加載項目'__project__'", + "revert": "項目“__project__”已還原", + "merge-complete": "Git合併完成", + "setupCredentials": "設定證書", + "setupProjectFiles": "設置項目文件", + "no": "不了,謝謝", + "createDefault": "創建默認項目文件", + "mergeConflict": "顯示合併衝突" + }, + "label": { + "manage-project-dep": "管理項目依賴性", + "setup-cred": "設定憑證", + "setup-project": "設置項目文件", + "create-default-package": "創建默認的包文件", + "no-thanks": "不了,謝謝", + "create-default-project": "創建默認項目文件", + "show-merge-conflicts": "顯示合併衝突" + } + }, + "clipboard": { + "clipboard": "剪貼簿", + "nodes": "節點", + "node": "__count__ 節點", + "node_plural": "__count__ 多個節點", + "configNode": "__count__ 節點組態", + "configNode_plural": "__count__ 多節點組態", + "flow": "__count__ 流程", + "flow_plural": "__count__ 多流程", + "subflow": "__count__ 子流程", + "subflow_plural": "__count__ 多子流程", + "pasteNodes": "在這裡粘貼節點", + "selectFile": "匯入所選檔案", + "importNodes": "匯入節點", + "exportNodes": "匯出節點至剪貼簿", + "download": "下載", + "importUnrecognised": "匯入了無法識別的類型:", + "importUnrecognised_plural": "匯入了無法識別的類型:", + "nodesExported": "節點匯出到了剪貼簿", + "nodesImported": "已匯入:", + "nodeCopied": "已複製__count__個節點", + "nodeCopied_plural": "已複製__count__個節點", + "invalidFlow": "無效的流程: __message__", + "export": { + "selected": "已選擇的節點", + "current": "現在的節點", + "all": "所有流程", + "compact": "緊湊", + "formatted": "已格式化", + "copy": "匯出到剪貼簿", + "export": "匯出到庫", + "exportAs": "匯出為", + "overwrite": "取代", + "exists": "

\"__file__\" 已經存在.

是否要取代?

" + }, + "import": { + "import": "匯入到", + "newFlow": "新流程", + "errors": { + "notArray": "輸入的不是JSON數組", + "itemNotObject": "輸入的流程無效-項目__index__不是節點對象", + "missingId": "輸入的流程無效-項__index__缺少“ id”屬性", + "missingType": "輸入的流程無效-項__index__缺少“類型”屬性" + } + }, + "copyMessagePath": "已複製路徑", + "copyMessageValue": "已複製數值", + "copyMessageValue_truncated": "已複製捨棄的數值" + }, + "deploy": { + "deploy": "部署", + "full": "全面", + "fullDesc": "在工作區中部署所有內容", + "modifiedFlows": "已修改的流程", + "modifiedFlowsDesc": "只部署包含已更改節點的流程", + "modifiedNodes": "已更改的節點", + "modifiedNodesDesc": "只部署已經更改的節點", + "restartFlows": "重新啟動流程", + "restartFlowsDesc": "重新啟動當前部署的流程", + "successfulDeploy": "部署成功", + "successfulRestart": "成功重啟流程", + "deployFailed": "部署失敗: __message__", + "unusedConfigNodes": "您有一些未使用的配置節點", + "unusedConfigNodesLink": "點擊此處查看它們", + "errors": { + "noResponse": "伺服器沒有回應" + }, + "confirm": { + "button": { + "ignore": "忽略", + "confirm": "確認部署", + "review": "查看更改", + "cancel": "取消", + "merge": "合併", + "overwrite": "忽略 & 部署" + }, + "undeployedChanges": "您有未部署的更改。\n\n離開此頁面將丟失這些更改。", + "improperlyConfigured": "工作區包含一些未正確配置的節點:", + "unknown": "工作區包含一些未知的節點類型:", + "confirm": "確定要部署嗎?", + "doNotWarn": "不要再對此發出警告", + "conflict": "伺服器正在運行較新的一組流程。", + "backgroundUpdate": "伺服器上的流程已更新。", + "conflictChecking": "檢查是否可以自動合併更改", + "conflictAutoMerge": "此更改不包括衝突,可以自動合併", + "conflictManualMerge": "這些更改包括了在部署之前必須解決的衝突。", + "plusNMore": "+更多的__count__" + } + }, + "eventLog": { + "title": "事件日誌", + "view": "查看日誌" + }, + "diff": { + "unresolvedCount": "__count__個未解決的衝突", + "unresolvedCount_plural": "__count__個未解決的衝突", + "globalNodes": "全局節點", + "flowProperties": "流程屬性", + "type": { + "added": "已添加", + "changed": "已更改", + "unchanged": "未更改", + "deleted": "已刪除", + "flowDeleted": "已刪除流程", + "flowAdded": "已添加流程", + "movedTo": "移動至__id__", + "movedFrom": "從__id__移動" + }, + "nodeCount": "__count__個節點", + "nodeCount_plural": "__count__個節點", + "local": "本地", + "remote": "遠端", + "reviewChanges": "查看變更", + "noBinaryFileShowed": "無法顯示二進製文件內容", + "viewCommitDiff": "查看提交更改", + "compareChanges": "比較變更", + "saveConflict": "保存衝突解決", + "conflictHeader": "已解決__unresolved__中的__resolved__個衝突", + "commonVersionError": "通用版本不包含有效的JSON:", + "oldVersionError": "舊版本不包含有效的JSON:", + "newVersionError": "新版本不包含有效的JSON:" + }, + "subflow": { + "editSubflowInstance": "編輯子流程實例:__name__", + "editSubflow": "編輯流程範本: __name__", + "edit": "編輯流程範本", + "subflowInstances": "這個子流程範本有__count__個實例", + "subflowInstances_plural": "這個子流程範本有__count__個實例", + "editSubflowProperties": "編輯屬性", + "input": "輸入:", + "output": "輸出:", + "status": "狀態節點", + "deleteSubflow": "刪除子流程", + "info": "詳細描述", + "category": "類別", + "env": { + "restore": "恢復為默認子流程", + "remove": "類別刪除環境變量" + }, + "errors": { + "noNodesSelected": "無法創建子流程: 未選擇節點", + "multipleInputsToSelection": "無法創建子流程: 多個輸入到了選擇" + } + }, + "editor": { + "configEdit": "編輯", + "configAdd": "添加", + "configUpdate": "更新", + "configDelete": "刪除", + "nodesUse": "__count__個節點使用此配置", + "nodesUse_plural": "__count__個節點使用此配置", + "addNewConfig": "添加新的__type__配置", + "editNode": "編輯__type__節點", + "editConfig": "編輯__type__配置", + "addNewType": "添加新的__type__節點", + "nodeProperties": "節點屬性", + "label": "Label", + "color": "顏色", + "portLabels": "埠標籤", + "labelInputs": "輸入", + "labelOutputs": "輸出", + "settingIcon": "Icon", + "default": "默認", + "noDefaultLabel": "無", + "defaultLabel": "使用默認標籤", + "searchIcons": "搜尋圖標", + "useDefault": "使用默認", + "description": "描述", + "show": "顯示", + "hide": "隱藏", + "locale": "選擇界面語言", + "icon": "圖標", + "inputType": "輸入類型", + "inputs": { + "input": "輸入", + "select": "選擇", + "checkbox": "復選框", + "spinner": "微調器", + "none": "空", + "hidden": "隱藏屬性" + }, + "types": { + "str": "字符串", + "num": "數字", + "bool": "布爾", + "json": "JSON", + "bin": "buffer", + "env": "環境變量" + }, + "menu": { + "input": "輸入", + "select": "選擇", + "checkbox": "復選框", + "spinner": "微調器", + "hidden": "僅標簽" + }, + "select": { + "label": "標簽", + "value": "值" + }, + "spinner": { + "min": "最小值", + "max": "最大值" + }, + "errors": { + "scopeChange": "更改範圍將使其他流程中的節點無法使用", + "invalidProperties": "無效的屬性:" + } + }, + "keyboard": { + "title": "鍵盤快速鍵", + "keyboard": "鍵盤", + "filterActions": "篩選動作", + "shortcut": "快捷鍵", + "scope": "範圍", + "unassigned": "未分配", + "global": "全局", + "workspace": "工作區", + "selectAll": "選擇所有節點", + "selectAllConnected": "選擇所有連接的節點", + "addRemoveNode": "從選擇中添加/刪除節點", + "editSelected": "編輯選定節點", + "deleteSelected": "刪除選定節點或連結", + "importNode": "匯入節點", + "exportNode": "匯出節點", + "nudgeNode": "移動所選節點(1px)", + "moveNode": "移動所選節點(20px)", + "toggleSidebar": "切換側邊欄", + "togglePalette": "切換調色板", + "copyNode": "複製所選節點", + "cutNode": "剪切所選節點", + "pasteNode": "粘貼節點", + "undoChange": "撤銷上次執行的更改", + "searchBox": "打開搜尋框", + "managePalette": "管理面板", + "actionList": "動作列表" + }, + "library": { + "library": "庫", + "openLibrary": "打開庫...", + "saveToLibrary": "保存到庫...", + "typeLibrary": "__type__型別程式庫", + "unnamedType": "無名__type__", + "exportedToLibrary": "節點導出到庫", + "dialogSaveOverwrite": "一個叫做__libraryName__的__libraryType__已經存在,您需要覆蓋麼?", + "invalidFilename": "無效的檔案名", + "savedNodes": "保存的節點", + "savedType": "已保存__type__", + "saveFailed": "保存失敗: __message__", + "newFolder": "新文件夾", + "types": { + "local": "本地", + "examples": "例子" + }, + "exportToLibrary": "將節點匯出到庫" + }, + "palette": { + "noInfo": "無可用資訊", + "filter": "過濾節點", + "search": "搜尋模組", + "addCategory": "添加新的...", + "label": { + "subflows": "子流程", + "network": "網絡", + "common": "共通", + "input": "輸入", + "output": "輸出", + "function": "功能", + "sequence": "序列", + "parser": "解析", + "social": "社交", + "storage": "存儲", + "analysis": "分析", + "advanced": "高級" + }, + "actions": { + "collapse-all": "收起所有類別", + "expand-all": "展開所有類別" + }, + "event": { + "nodeAdded": "添加到面板中的節點:", + "nodeAdded_plural": "添加到面板中的多個節點", + "nodeRemoved": "從面板中刪除的節點:", + "nodeRemoved_plural": "從面板中刪除的多個節點:", + "nodeEnabled": "啟用節點:", + "nodeEnabled_plural": "啟用多個節點:", + "nodeDisabled": "禁用節點:", + "nodeDisabled_plural": "禁用多個節點:", + "nodeUpgraded": "節點模組__module__升級到__version__版本" + }, + "editor": { + "title": "面板管理", + "palette": "Palette", + "times": { + "seconds": "秒前", + "minutes": "分前", + "minutesV": "__count__分前", + "hoursV": "__count__小時前", + "hoursV_plural": "__count__小時前", + "daysV": "__count__天前", + "daysV_plural": "__count__天前", + "weeksV": "__count__周前", + "weeksV_plural": "__count__周前", + "monthsV": "__count__月前", + "monthsV_plural": "__count__月前", + "yearsV": "__count__年前", + "yearsV_plural": "__count__年前", + "yearMonthsV": "__y__年, __count__月前", + "yearMonthsV_plural": "__y__年, __count__月前", + "yearsMonthsV": "__y__年, __count__月前", + "yearsMonthsV_plural": "__y__年, __count__月前" + }, + "nodeCount": "__label__個節點", + "nodeCount_plural": "__label__個節點", + "moduleCount": "__count__個可用模組", + "moduleCount_plural": "__count__個可用模組", + "inuse": "使用中", + "enableall": "全部啟用", + "disableall": "全部禁用", + "enable": "啟用", + "disable": "禁用", + "remove": "移除", + "update": "更新至__version__版本", + "updated": "已更新", + "install": "安裝", + "installed": "已安裝", + "conflict": "conflict", + "conflictTip": "

無法安裝此模塊,因為它包含已安裝的
節點類型

__module__衝突

", + "loading": "載入目錄...", + "tab-nodes": "節點", + "tab-install": "安裝", + "sort": "排序:", + "sortAZ": "a-z順序", + "sortRecent": "日期順序", + "more": "增加__count__個", + "errors": { + "catalogLoadFailed": "無法載入節點目錄。
查看瀏覽器控制臺瞭解更多資訊", + "installFailed": "無法安裝: __module__
__message__
查看日誌瞭解更多資訊", + "removeFailed": "無法刪除: __module__
__message__
查看日誌瞭解更多資訊", + "updateFailed": "無法更新: __module__
__message__
查看日誌瞭解更多資訊", + "enableFailed": "無法啟用: __module__
__message__
查看日誌瞭解更多資訊", + "disableFailed": "無法禁用: __module__
__message__
查看日誌瞭解更多資訊" + }, + "confirm": { + "install": { + "body": "在安裝之前,請閱讀節點的文檔,某些節點的依賴關係不能自動解決,可能需要重新啟動Node-RED。", + "title": "安裝節點" + }, + "remove": { + "body": "刪除節點將從Node-RED卸載它。節點可能會繼續使用資源,直到重新啟動Node-RED。", + "title": "刪除節點" + }, + "update": { + "body": "更新節點將需要重新啟動Node-RED來完成更新,該過程必須由手動完成。", + "title": "更新節點" + }, + "cannotUpdate": { + "body": "此節點的更新可用,但不會安裝在面板管理器可以更新的位置。

請參閱有關如何更新此節點的文檔。" + }, + "button": { + "review": "打開節點資訊", + "install": "安裝", + "remove": "刪除", + "update": "更新" + } + } + } + }, + "sidebar": { + "info": { + "name": "節點信息", + "tabName": "名稱", + "label": "信息", + "node": "節點", + "type": "類型", + "module": "Module", + "id": "ID", + "status": "狀態", + "enabled": "啟用", + "disabled": "禁用", + "subflow": "子流程", + "instances": "實例", + "properties": "屬性", + "info": "信息", + "desc": "描述", + "blank": "空白", + "null": "空", + "showMore": "展開", + "showLess": "收起", + "flow": "流程", + "selection": "選擇", + "nodes": "__count__ 個節點", + "flowDesc": "流程描述", + "subflowDesc": "子流程描述", + "nodeHelp": "節點幫助", + "none": "無", + "arrayItems": "__count__個項目", + "showTips": "您可以從設置面板啟用提示資訊" + }, + "config": { + "name": "配置節點", + "label": "配置", + "global": "所有流程", + "none": "無", + "subflows": "子流程", + "flows": "流程", + "filterAll": "所有", + "showAllConfigNodes": "顯示所有配置節點", + "filterUnused": "未使用", + "showAllUnusedConfigNodes": "顯示所有未使用的配置節點", + "filtered": "__count__ 個隱藏" + }, + "context": { + "name": "上下文數據", + "label": "上下文", + "none": "未選擇", + "refresh": "刷新以加載", + "empty": "空", + "node": "節點", + "flow": "流程", + "global": "全局的", + "deleteConfirm": "你確定要刪除這個項目嗎?", + "autoRefresh": "自動刷新", + "refrsh": "刷新", + "delete": "刪除" + }, + "palette": { + "name": "節點管理", + "label": "節點" + }, + "project": { + "label": "項目", + "name": "名稱", + "description": "描述", + "dependencies": "依賴", + "settings": "設置", + "noSummaryAvailable": "無可用摘要", + "editDescription": "編輯專案描述", + "editDependencies": "編輯項目依賴", + "noDescriptionAvailable": "沒有可用的描述", + "editReadme": "Edit README.md", + "showProjectSettings": "顯示項目設置", + "projectSettings": { + "title": "項目設定", + "edit": "編輯", + "none": "None", + "install": "安裝", + "removeFromProject": "從項目中刪除", + "addToProject": "添加到項目", + "files": "文件", + "package": "包", + "flow": "流程", + "credentials": "證書", + "packageCreate": "保存更改後將創建文件", + "fileNotExist": "文件不存在", + "selectFile": "選擇文件", + "invalidEncryptionKey": "無效的加密密鑰", + "encryptionEnabled": "啟用加密", + "encryptionDisabled": "禁用加密", + "setTheEncryptionKey": "設置加密密鑰", + "resetTheEncryptionKey": "重置加密密鑰", + "changeTheEncryptionKey": "更改加密密鑰", + "currentKey": "當前密鑰", + "newKey": "新密鑰", + "credentialsAlert": "這將刪除所有現有證書", + "versionControl": "版本控制", + "branches": "分支", + "noBranches": "沒有分支", + "deleteConfirm": "您確定要刪除本地分支'__name__'嗎?這不能被撤消。", + "unmergedConfirm": "本地分支'__name__'具有未合併的更改,這些更改將丟失。你確定要刪除嗎?", + "deleteUnmergedBranch": "刪除未合併的分支", + "gitRemotes": "Git遠程倉庫", + "addRemote": "添加遠程倉庫", + "addRemote2": "添加遠程倉庫", + "remoteName": "遠程倉庫名", + "nameRule": "必須僅包含A-Z 0-9 _ -", + "url": "URL", + "urlRule": "https://, ssh:// or file://", + "urlRule2": "網址中不要包含用戶名/密碼", + "noRemotes": "沒有遠程倉庫", + "deleteRemoteConfrim": "確定要刪除遠程倉庫'__name__'嗎?", + "deleteRemote": "刪除遠程倉庫" + }, + "userSettings": { + "committerDetail": "提交者詳細信息", + "committerTip": "保留空白以使用系統默認值", + "userName": "用戶名", + "email": "電子郵件", + "sshKeys": "SSH密鑰", + "sshKeysTip": "允許您創建到遠程git存儲庫的安全連接。", + "add": "添加密鑰", + "addSshKey": "添加SSH密鑰", + "addSshKeyTip": "生成新的公鑰/私鑰對", + "name": "名字", + "nameRule": "必須僅包含A-Z 0-9 _ -", + "passphrase": "密碼短語", + "passphraseShort": "密碼短語太短", + "optional": "可選的", + "cancel": "取消", + "generate": "產生密鑰", + "noSshKeys": "沒有SSH密鑰", + "copyPublicKey": "將公鑰複製到剪貼板", + "delete": "刪除密鑰", + "gitConfig": "Git配置", + "deleteConfirm": "您確定要刪除SSH密鑰__name__嗎? 這不能被撤消。" + }, + "versionControl": { + "unstagedChanges": "未暫存的更改", + "stagedChanges": "已暫存的更改", + "unstageChange": "取消暫存更改", + "stageChange": "暫存更改", + "unstageAllChange": "取消暫存所有更改", + "stageAllChange": "暫存所有更改", + "commitChanges": "提交變更", + "resolveConflicts": "解決衝突", + "head": "HEAD", + "staged": "以暫存", + "unstaged": "未暫存", + "local": "本地的", + "remote": "遠程的", + "revert": "您確定要將更改恢復為'__file__'嗎? 這不能被撤消。", + "revertChanges": "還原變更", + "localChanges": "當地變化", + "none": "None", + "conflictResolve": "解決所有衝突。 提交更改以完成合併。", + "localFiles": "本地文件", + "all": "所有的", + "unmergedChanges": "未合併的更改", + "abortMerge": "合併中止", + "commit": "提交", + "changeToCommit": "提交變更", + "commitPlaceholder": "輸入您的提交信息", + "cancelCapital": "取消", + "commitCapital": "提交", + "commitHistory": "提交歷史", + "branch": "分支:", + "moreCommits": "更多提交", + "changeLocalBranch": "變更當地分支", + "createBranchPlaceholder": "查找或創建分支", + "upstream": "上遊的", + "localOverwrite": "您有可通過切換分支覆蓋的本地更改。您必須先提交或撤銷那些更改。", + "manageRemoteBranch": "管理遠程分支", + "unableToAccess": "無法訪問遠程存儲庫", + "retry": "重試", + "setUpstreamBranch": "設置為上遊分支", + "createRemoteBranchPlaceholder": "查找或創建遠程分支", + "trackedUpstreamBranch": "創建的分支將被設置為跟蹤的上遊分支。", + "selectUpstreamBranch": "分支將被創建。 在下面選擇以將其設置為被跟蹤的上遊分支。", + "pushFailed": "Push失敗,因為遠程具有更多的最新提交。請先進行pull與merge,然後再嘗試push。", + "push": "push", + "pull": "pull", + "unablePull": "

無法提取遠程更改;您未進行的暫存本地更改將被覆蓋。

提交更改,然後重試。

", + "showUnstagedChanges": "顯示未分階段的更改", + "connectionFailed": "無法連接到遠程存儲庫:", + "pullUnrelatedHistory": "

遠程服務器具有不相關的提交歷史記錄。

您確定要將更改保存到本地存儲庫中嗎?

", + "pullChanges": "Pull變更", + "history": "歷史", + "projectHistory": "項目歷史", + "daysAgo": "__count__天前", + "daysAgo_plural": "__count__天前", + "hoursAgo": "__count__小時前", + "hoursAgo_plural": "__count__小時前", + "minsAgo": "__count__分鐘前", + "minsAgo_plural": "__count__分鐘前", + "secondsAgo": "秒前", + "notTracking": "您的本地分支當前未跟蹤遠程分支。", + "statusUnmergedChanged": "您的存儲庫中有未合併的更改。您需要解決衝突並提交結果。", + "repositoryUpToDate": "您的存儲庫是最新的。", + "commitsAhead": "您的倉庫領先遠程倉庫__count__次提交。您現在可以push這些提交。", + "commitsAhead_plural": "您的倉庫領先遠程倉庫__count__次提交。您現在可以push這些提交。", + "commitsBehind": "您的倉庫落後遠程倉庫__count__次提交。您現在可以pull這些提交。", + "commitsBehind_plural": "您的倉庫落後遠程倉庫__count__次提交。您現在可以pull這些提交。", + "commitsAheadAndBehind1": "您的倉庫落後遠程倉庫__count__次提交", + "commitsAheadAndBehind1_plural": "您的倉庫落後遠程倉庫__count__次提交", + "commitsAheadAndBehind2": "領先遠程倉庫__count__次提交。", + "commitsAheadAndBehind2_plural": "領先遠程倉庫__count__次提交。", + "commitsAheadAndBehind3": "您必須先pull遠程提交,然後再進行push。", + "commitsAheadAndBehind3_plural": "您必須先pull遠程提交,然後再進行push。", + "refreshCommitHistory": "刷新提交歷史", + "refreshChanges": "刷新更改" + } + } + }, + "typedInput": { + "type": { + "str": "文字列", + "num": "數字", + "re": "規則運算式", + "bool": "布林", + "json": "JSON", + "bin": "二進位流", + "date": "時間戳記", + "jsonata": "expression", + "env": "env variable" + } + }, + "editableList": { + "add": "添加" + }, + "search": { + "empty": "找不到匹配", + "addNode": "添加一個節點..." + }, + "expressionEditor": { + "functions": "功能", + "functionReference": "Function reference", + "insert": "插入", + "title": "JSONata運算式編輯器", + "test": "Test", + "data": "示例消息", + "result": "結果", + "format": "格式表達方法", + "compatMode": "相容模式啟用", + "compatModeDesc": "

JSONata的相容模式

目前的運算式仍然參考msg,所以將以相容性模式進行評估。請更新運算式,使其不使用msg,因為此模式將在將來刪除。

當JSONata支持首次添加到Node-RED時,它需要運算式引用msg物件。例如msg.payload將用於訪問有效負載。

這樣便不再需要運算式直接針對消息進行評估。要訪問有效負載,運算式應該只是payload.

", + "noMatch": "無匹配結果", + "errors": { + "invalid-expr": "無效的JSONata運算式:\n __message__", + "invalid-msg": "無效的示例JSON消息:\n __message__", + "context-unsupported": "無法測試上下文函數\n $flowContext 或 $globalContext", + "eval": "評估運算式錯誤:\n __message__" + } + }, + "jsEditor": { + "title": "JavaScript 編輯器" + }, + "textEditor": { + "title": "Text 編輯器" + }, + "jsonEditor": { + "title": "JSON編輯器", + "format": "格式化JSON", + "rawMode": "編輯 JSON", + "uiMode": "Visual編輯器", + "insertAbove": "在上方插入", + "insertBelow": "在下方插入", + "addItem": "添加項目", + "copyPath": "復制路徑到項目", + "expandItems": "展開項目", + "collapseItems": "收合項目", + "duplicate": "重復", + "error": { + "invalidJSON": "無效的JSON: " + } + }, + "markdownEditor": { + "title": "Markdown 編輯器", + "expand": "展開", + "format": "F使用markdown格式化", + "heading1": "Heading 1", + "heading2": "Heading 2", + "heading3": "Heading 3", + "bold": "粗體", + "italic": "斜體", + "code": "程式碼", + "ordered-list": "編號清單", + "unordered-list": "符號清單", + "quote": "引用", + "link": "連結", + "horizontal-rule": "分隔線", + "toggle-preview": "預覽" + }, + "bufferEditor": { + "title": "緩衝區編輯器", + "modeString": "作為UTF-8字串處理", + "modeArray": "作為JSON陣列處理", + "modeDesc": "

緩衝區編輯器

緩衝區類型被存儲為位元組值的JSON陣列。編輯器將嘗試將輸入的數值解析為JSON陣列。如果它不是有效的JSON,它將被視為UTF-8字串,並被轉換為單個字元代碼點的陣列。

例如,Hello World的值會被轉換為JSON陣列:

[72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100]

" + }, + "projects": { + "config-git": "配置Git客戶端", + "welcome": { + "hello": "你好! 我們已經將“項目”引入了Node-RED。", + "desc0": "這是一種用於管理流程文件的新方法,並且包括對流程的版本控制。", + "desc1": "首先,您可以創建您的第一個項目或從git存儲庫克隆現有項目。", + "desc2": "如果不確定,可以暫時跳過此步驟。 您仍然可以隨時通過“項目”菜單創建第一個項目。", + "create": "創建項目", + "clone": "克隆存儲庫", + "openExistingProject": "打開現有項目", + "not-right-now": "不是現在" + }, + "git-config": { + "setup": "設置您的版本控制客戶端", + "desc0": "Node-RED使用開源工具Git進行版本控制。 它跟蹤對項目文件的更改,並允許您將其推送到遠程存儲庫。", + "desc1": "提交一組更改時,Git會使用用戶名和電子郵件地址記錄誰進行了更改。 用戶名可以是您想要的任何名稱-不必是您的真實姓名。", + "desc2": "您的Git客戶端已經配置了以下詳細信息。", + "desc3": "您可以稍後在設置對話框的“ Git config”標籤下更改這些設置。", + "username": "用戶名", + "email": "電子郵件" + }, + "project-details": { + "create": "創建您的項目", + "desc0": "項目被維護為Git存儲庫。 與他人共享您的流程並進行協作更容易。", + "desc1": "您可以創建多個項目,並通過編輯器在它們之間快速切換。", + "desc2": "首先,您的項目需要一個名稱和一個可選的描述。", + "already-exists": "項目已存在", + "must-contain": "必須僅包含A-Z 0-9 _ -", + "project-name": "項目名", + "desc": "描述", + "opt": "可選的" + }, + "clone-project": { + "clone": "克隆項目", + "desc0": "如果您已經有一個包含項目的git存儲庫,則可以對其進行克隆以開始使用。", + "already-exists": "項目已經存在", + "must-contain": "必須僅包含A-Z 0-9 _ -", + "project-name": "項目名", + "no-info-in-url": "網址中不要包含用戶名/密碼", + "git-url": "Git存儲庫URL", + "protocols": "https://, ssh:// or file://", + "auth-failed": "驗證失敗", + "username": "用戶名", + "passwd": "密碼", + "ssh-key": "SSH密鑰", + "passphrase": "密碼短語", + "ssh-key-desc": "在通過ssh克隆存儲庫之前,必須添加SSH密鑰才能訪問它。", + "ssh-key-add": "添加一個ssh密鑰", + "credential-key": "證書加密密鑰", + "cant-get-ssh-key": "錯誤! 無法獲取所選的SSH密鑰路徑。", + "already-exists2": "已存在", + "git-error": "git錯誤", + "connection-failed": "連接失敗", + "not-git-repo": "不是一個git倉庫", + "repo-not-found": "未發現倉庫" + }, + "default-files": { + "create": "創建您的項目文件", + "desc0": "一個項目包含您的流程文件,自述文件和package.json文件。", + "desc1": "它可以包含您要在Git存儲庫中維護的任何其他文件。", + "desc2": "您現有的流程和證書文件將被複製到項目中。", + "flow-file": "流文件", + "credentials-file": "證書文件" + }, + "encryption-config": { + "setup": "設置證書文件的加密", + "desc0": "您的流程證書文件可以被加密以確保其內容安全。", + "desc1": "如果要將這些證書存儲在公共Git存儲庫中,則必須通過提供密鑰短語來對它們進行加密。", + "desc2": "您的流程證書文件當前未加密。", + "desc3": "這意味著任何有權訪問該文件的人都可以讀取其內容,例如密碼和訪問令牌。", + "desc4": "如果要將這些證書存儲在公共Git存儲庫中,則必須通過提供密鑰短語來對它們進行加密。", + "desc5": "當前,使用設置文件中的credentialSecret屬性作為密鑰來加密流憑據文件。", + "desc6": "您的流程證書文件當前使用系統生成的密鑰加密。 您應該為此項目提供一個新的密鑰。", + "desc7": "密鑰將與項目文件分開存儲。 您將需要提供在另一個Node-RED實例中使用該項目的密鑰。", + "credentials": "證書", + "enable": "啟用加密", + "disable": "禁用加密", + "disabled": "禁用的", + "copy": "複製現有密鑰", + "use-custom": "使用自定義密鑰", + "desc8": "憑證文件不會被加密,其內容很容易閱讀", + "create-project-files": "創建項目文件", + "create-project": "創建項目", + "already-exists": "已存在", + "git-error": "git錯誤", + "git-auth-error": "git認證錯誤" + }, + "create-success": { + "success": "您已經成功創建了第一個項目!", + "desc0": "現在,您可以像往常一樣繼續使用Node-RED。", + "desc1": "側欄中的“信息”標籤顯示了您當前的活動項目。名稱旁邊的按鈕可用於訪問項目設置視圖。", + "desc2": "側欄中的“歷史記錄”標籤可用於查看項目中已更改的文件並提交。 它向您顯示了提交的完整歷史記錄,並允許您將更改推送到遠程存儲庫。" + }, + "create": { + "projects": "項目", + "already-exists": "項目已存在", + "must-contain": "必須僅包含A-Z 0-9 _ -", + "no-info-in-url": "網址中不要包含用戶名/密碼", + "open": "打開項目", + "create": "創建項目", + "clone": "克隆倉庫", + "project-name": "項目名", + "desc": "描述", + "opt": "可選的", + "flow-file": "流程文件", + "credentials": "證書", + "enable-encryption": "啟用加密", + "disable-encryption": "禁用加密", + "encryption-key": "加密的密鑰", + "desc0": "用來保護您的憑證的短語", + "desc1": "憑證文件不會被加密,其內容很容易閱讀", + "git-url": "Git倉庫的URL", + "protocols": "https://, ssh:// or file://", + "auth-failed": "驗證失敗", + "username": "用戶名", + "password": "密碼", + "ssh-key": "SSH密鑰", + "passphrase": "密碼短語", + "desc2": "在通過ssh克隆存儲庫之前,必須添加SSH密鑰才能訪問它。", + "add-ssh-key": "添加一個ssh密鑰", + "credentials-encryption-key": "憑證加密密鑰", + "already-exists-2": "已存在", + "git-error": "git錯誤", + "con-failed": "連接失敗", + "not-git": "不是git倉庫", + "no-resource": "找不到存儲庫", + "cant-get-ssh-key-path": "錯誤! 無法獲取所選的SSH密鑰路徑。", + "unexpected_error": "意外的錯誤" + }, + "delete": { + "confirm": "您確定要刪除此項目嗎?" + }, + "create-project-list": { + "search": "搜尋您的項目", + "current": "當前的" + }, + "require-clean": { + "confirm": "

您有未部署的更改,這些更改將丟失。

您要繼續嗎?

" + }, + "send-req": { + "auth-req": "存儲庫需要認證", + "username": "用戶名", + "password": "密碼", + "passphrase": "密碼短語", + "retry": "重試", + "update-failed": "無法更新身份驗證", + "unhandled": "未處理的錯誤響應" + }, + "create-branch-list": { + "invalid": "無效的分支", + "create": "創建分支", + "current": "當前的" + }, + "create-default-file-set": { + "no-active": "沒有活動項目就無法創建默認文件集", + "no-empty": "無法在非空項目上創建默認文件集", + "git-error": "git error" + }, + "errors": { + "no-username-email": "您的Git客戶端未配置用戶名/電子郵件。", + "unexpected": "發生了一個意料之外的問題", + "code": "代碼" + } + }, + "editor-tab": { + "properties": "屬性", + "envProperties": "環境變量", + "description": "描述", + "appearance": "外觀", + "preview": "UI預覽", + "defaultValue": "默認值", + "env": "環境變量" + }, + "languages": { + "de": "德語", + "en-US": "英語", + "ja": "日語", + "ko": "韓語", + "zh-CN": "簡體中文", + "zh-TW": "繁體中文" + } +} diff --git a/packages/node_modules/@node-red/editor-client/locales/zh-TW/infotips.json b/packages/node_modules/@node-red/editor-client/locales/zh-TW/infotips.json new file mode 100644 index 000000000..f783f2e8b --- /dev/null +++ b/packages/node_modules/@node-red/editor-client/locales/zh-TW/infotips.json @@ -0,0 +1,23 @@ +{ + "info": { + "tip0" : "您可以用 {{core:delete-selection}} 刪除選擇的節點或連結。", + "tip1" : "{{core:search}} 可以在流程內搜索節點。", + "tip2": "{{core:toggle-sidebar}} 可以顯示或隱藏邊側欄。", + "tip3": "您可以在 {{core:manage-palette}} 中管理節點的控制台。", + "tip4": "側邊欄中會列出流程中所有的配置節點。您可以通過功能表或者 {{core:show-config-tab}} 來訪問這些節點。", + "tip5": "您可以在設定中選擇顯示或隱藏這些提示。", + "tip6": "您可以用[left] [up] [down] [right]鍵來移動被選中的節點。按住[shift]可以更快地移動節點。", + "tip7": "把節點拖到連接上可以向連接中插入節點。", + "tip8": "您可以用 {{core:show-export-dialog}} 來匯出被選中的節點或標籤頁中的流程。", + "tip9": "您可以將流程的json文字檔拖入編輯方塊或 {{core:show-import-dialog}} 來導入流程。", + "tip10": "按住[shift]後按一下並拖動節點可以將該節點的多個連接一併移動到其他節點的埠。", + "tip11": "{{core:show-info-tab}} 可以顯示「資訊」標籤頁。 {{core:show-debug-tab}} 可以顯示「調試」標籤頁。", + "tip12": "按住[ctrl]的同時點擊工作介面可以在節點的對話欄中快速添加節點。", + "tip13": "按住[ctrl]的同時點擊節點的埠或後續節點可以快速連接多個節點。", + "tip14": "按住[shift]的同時點擊節點會選中所有被連接的節點。", + "tip15": "按住[ctrl]的同時點擊節點可以在選中或取消選中節點。", + "tip16": "{{core:show-previous-tab}} 和 {{core:show-next-tab}} 可以切換標籤頁。", + "tip17": "您可以在節點的屬性配置畫面中通過 {{core:confirm-edit-tray}} 來更改設置,或者用 {{core:cancel-edit-tray}} 來取消更改。", + "tip18": "您可以通過點擊 {{core:edit-selected-node}} 來顯示被選中節點的屬性設置畫面。" + } +} diff --git a/packages/node_modules/@node-red/editor-client/locales/zh-TW/jsonata.json b/packages/node_modules/@node-red/editor-client/locales/zh-TW/jsonata.json new file mode 100644 index 000000000..6d99ffc6a --- /dev/null +++ b/packages/node_modules/@node-red/editor-client/locales/zh-TW/jsonata.json @@ -0,0 +1,270 @@ +{ + "$string": { + "args": "arg", + "desc": "通過以下的類型轉換規則將參數*arg*轉換成字串:\n\n - 字串不轉換。\n -函數轉換成空的字串。\n - JSON的值無法用數字表示所以用無限大或者NaN(非數)表示。\n - 用’JSON.stringify’函數將其他值轉換成JSON字串。" + }, + "$length": { + "args": "str", + "desc": "輸出字串’str’的字數。如果’str’不是字串,拋出錯誤。" + }, + "$substring": { + "args": "str, start[, length]", + "desc": "輸出`start`位置後的的首次出現的包括`str`的子字串。 如果`length`被指定,那麼的字串中將只包括前`length`個文字。如果`start`是負數則輸出從`str`末尾開始的`length`個文字" + }, + "$substringBefore": { + "args": "str, chars", + "desc": "輸出’str’中首次出現的’chars’之前的子字串,如果’str’中不包括’chars’則輸出’str’。" + }, + "$substringAfter": { + "args": "str, chars", + "desc": "輸出’str’中首次出現的’chars’之後的子字串,如果’str’中不包括’chars’則輸出’str’。" + }, + "$uppercase": { + "args": "str", + "desc": "`將’str’中的所有字母變為大寫後輸出。" + }, + "$lowercase": { + "args": "str", + "desc": "將’str’中的所有字母變為小寫後輸出。" + }, + "$trim": { + "args": "str", + "desc": "將以下步驟應用於`str`來去除所有空白文字並實現標準化。\n\n – 將全部tab定位字元、Enter鍵、換行字元用空白代替。\n- 將連續的空白文字變成一個空白文字。\n- 消除開頭和末尾的空白文字。\n\n如果`str`沒有被指定(即在無輸入參數的情況下調用本函數),將上下文的值作為`str`來使用。 如果`str` 不是字串則拋出錯誤。" + }, + "$contains": { + "args": "str, pattern", + "desc": "字串`str` 和 `pattern`匹配的話輸出`true`,不匹配的情況下輸出 `false`。 不指定`str`的情況下(比如用一個參數調用本函數時)、將上下文的值作為`str`來使用。參數 `pattern`可以為字串或正則表達。" + }, + "$split": { + "args": "str[, separator][, limit]", + "desc": "將參數`str`分解成由子字串組成的陣列。 如果`str`不是字串拋出錯誤。可以省略的參數 `separator`中指定字串`str`的分隔符號。分隔符號可以是文字或規則運算式。在不指定`separator`的情況下、將分隔符號看作空的字串並把`str`拆分成由單個字母組成的陣列。如果`separator`不是字串則拋出錯誤。在可省略的參數`limit`中指定分割後的子字串的最大個數。超出個數的子字串將被捨棄。如果`limit`沒有被指定,`str` 將不考慮子字串的個數而將字串完全分隔。如果`limit`是負數則拋出錯誤。" + }, + "$join": { + "args": "array[, separator]", + "desc": "用可以省略的參數 `separator`來把多個字元串連接。如果`array`不是字串則拋出錯誤。 如果沒有指定`separator`,則用空字串來連接字元(即字串之間沒有`separator`)。 如果`separator`不是字元則拋出錯誤。" + }, + "$match": { + "args": "str, pattern [, limit]", + "desc": "對字串`str`使用規則運算式`pattern`並輸出與`str`相匹配的部分資訊。" + }, + "$replace": { + "args": "str, pattern, replacement [, limit]", + "desc": "在字串`str`中搜索`pattern`並用`replacement`來替換。\n\n可選參數`limit`用來指定替換次數的上限。" + }, + "$now": { + "args": "", + "desc": "生成ISO 8601互換格式的時刻,並作為字串輸出。" + }, + "$base64encode": { + "args": "string", + "desc": "將ASCII格式的字串轉換為Base 64格式。將字串中的文字視作二進位形式的資料處理。包含URI編碼在內的字串文字必須在0x00到0xFF的範圍內,否則不會被支持。" + }, + "$base64decode": { + "args": "string", + "desc": "用UTF-8內碼表將Base 64形式二進位值轉換為字串。" + }, + "$number": { + "args": "arg", + "desc": "用下述的規則將參數 `arg`轉換為數值。:\n\n – 數值不做轉換。\n – 將字串中合法的JSON數値表示轉換成數値。\n – 其他形式的值則拋出錯誤。" + }, + "$abs": { + "args": "number", + "desc": "輸出參數`number`的絕對值。" + }, + "$floor": { + "args": "number", + "desc": "輸出比`number`的值小的最大整數。" + }, + "$ceil": { + "args": "number", + "desc": "輸出比`number`的值大的最小整數。" + }, + "$round": { + "args": "number [, precision]", + "desc": "輸出四捨五入後的參數`number`。可省略的參數 `precision`指定四捨五入後小數點下的位數。" + }, + "$power": { + "args": "base, exponent", + "desc": "輸出底數`base`的`exponent`次冪。" + }, + "$sqrt": { + "args": "number", + "desc": "輸出參數 `number`的平方根。" + }, + "$random": { + "args": "", + "desc": "輸出比0大,比1小的偽亂數。" + }, + "$millis": { + "args": "", + "desc": "返回從UNIX時間 (1970年1月1日 UTC/GMT的午夜)開始到現在的毫秒數。在同一個運算式的測試中所有對`$millis()`的調用將會返回相同的值。" + }, + "$sum": { + "args": "array", + "desc": "輸出陣列`array`的總和。如果`array`不是數值則拋出錯誤。" + }, + "$max": { + "args": "array", + "desc": "輸出陣列`array`的最大值。如果`array`不是數值則拋出錯誤。" + }, + "$min": { + "args": "array", + "desc": "輸出陣列`array`的最小值。如果`array`不是數值則拋出錯誤。。" + }, + "$average": { + "args": "array", + "desc": "輸出陣列`array`的平均數。如果`array`不是數值則拋出錯誤。。" + }, + "$boolean": { + "args": "arg", + "desc": "用下述規則將資料轉換成布林值。:\n\n - 不轉換布林值`Boolean`。\n – 將空的字串`string`轉換為`false`\n – 將不為空的字串`string`轉換為`true`\n – 將為0的數位`number`轉換成`false`\n –將不為0的數位`number`轉換成`true`\n –將`null`轉換成`false`\n –將空的陣列`array`轉換成`false`\n –如果陣列`array`中含有可以轉換成`true`的要素則轉換成`true`\n –如果`array`中沒有可轉換成`true`的要素則轉換成`false`\n – 空的物件`object`轉換成`false`\n – 非空的物件`object`轉換成`true`\n –將函數`function`轉換成`false`" + }, + "$not": { + "args": "arg", + "desc": "輸出做反轉運算後的布林值。首先將`arg`轉換為布林值。" + }, + "$exists": { + "args": "arg", + "desc": "如果算式`arg`的值存在則輸出`true`。如果算式的值不存在(比如指向不存在區域的引用)則輸出`false`。" + }, + "$count": { + "args": "array", + "desc": "輸出陣列中的元素數。" + }, + "$append": { + "args": "array, array", + "desc": "將兩個陣列連接。" + }, + "$sort": { + "args": "array [, function]", + "desc": "輸出排序後的陣列`array`。\n\n如果使用了比較函數`function`,則下述兩個參數需要被指定。\n\n`function(left, right)`\n\n該比較函數是為了比較left和right兩個值而被排序演算法調用的。如果使用者希望left的值被置於right的值之後,那麼該函數必須輸出布林值`true`來表示位置交換。而在不需要位置交換時函數必須輸出`false`。" + }, + "$reverse": { + "args": "array", + "desc": "輸出倒序後的陣列`array`。" + }, + "$shuffle": { + "args": "array", + "desc": "輸出隨機排序後的陣列 `array`。" + }, + "$zip": { + "args": "array, ...", + "desc": "將陣列中的值按索引順序打包後輸出。" + }, + "$keys": { + "args": "object", + "desc": "輸出由物件內的鍵組成的陣列。如果參數是物件的陣列則輸出由所有物件中的鍵去重後組成的佇列。" + }, + "$lookup": { + "args": "object, key", + "desc": "輸出對象中與參數`key`對應的值。如果第一個參數`object`是陣列,那麼陣列中所有的物件都將被搜索並輸出這些物件中與參數`key`對應的值。" + }, + "$spread": { + "args": "object", + "desc": "將物件中的鍵值對分隔成每個要素中只含有一個鍵值對的陣列。如果參數`object`是陣列,那麼返回值的陣列中包含所有物件中的鍵值對。" + }, + "$merge": { + "args": "array<object>", + "desc": "將輸入陣列`objects`中所有的鍵值對合併到一個`object`中並返回。如果輸入陣列的要素中含有重複的鍵,則返回的`object`中將只包含陣列中最後出現要素的值。如果輸入陣列中包括物件以外的元素,則拋出錯誤。" + }, + "$sift": { + "args": "object, function", + "desc": "輸出參數`object`中符合`function`的鍵值對。\n\n`function`必須含有下述參數。\n\n`function(value [, key [, object]])`" + }, + "$each": { + "args": "object, function", + "desc": "將函數`function`應用於`object`中的所有鍵值對並輸出由所有返回值組成的陣列。" + }, + "$map": { + "args": "array, function", + "desc": "將函數`function`應用於陣列`array`中所有的值並輸出由返回值組成的陣列。\n\n`function`中必須含有下述參數。\n\n`function(value [, index [, array]])`" + }, + "$filter": { + "args": "array, function", + "desc": "輸出陣列`array`中符合函數`function`條件的值組成的陣列。\n\n`function`必須包括下述參數。\n\n`function(value [, index [, array]])`" + }, + "$reduce": { + "args": "array, function [, init]", + "desc": "將`function`依次應用於陣列中的各要素值。 其中,前一個要素值的計算結果將參與到下一次的函數運算中。。\n\n函數`function`接受兩個參數並作為中綴標記法中的操作符。\n\n可省略的參數`init`將作為運算的初始值。" + }, + "$flowContext": { + "args": "string", + "desc": "獲取流上下文(流等級的上下文,可以讓所有節點共用)的屬性。" + }, + "$globalContext": { + "args": "string", + "desc": "獲取全域上下文的屬性。" + }, + "$pad": { + "args": "string, width [, char]", + "desc": "根據需要,向字串`string`的副本中填充文字使該字串的字數達到`width`的絕對值並返回填充文字後的字串。\n\n如果`width`的值為正,則向字串`string`的右側填充文字,如果`width`為負,則向字串`string`的左側填充文字。\n\n可選參數`char`用來指定填充的文字。如果未指定該參數,則填充空白文字。" + }, + "$fromMillis": { + "args": "number", + "desc": "將表示從UNIX時間 (1970年1月1日 UTC/GMT的午夜)開始到現在的毫秒數的數值轉換成ISO 8601形式時間戳記的字串。" + }, + "$formatNumber": { + "args": "number, picture [, options]", + "desc": "將`number`轉換成具有`picture`所指定的數值格式的字串。\n\n此函數的功能與XPath F&O 3.1規格中定義的XPath/XQuery函數的fn:format-number功能相一致。參數`picture`用於指定數值的轉換格式,其語法與fn:format-number中的定義一致。\n\n可選的第三參數`options`用來覆蓋預設的局部環境格式,如小數點分隔符號。如果指定該參數,那麼該參數必須是包含name/value對的物件,並且name/value對必須符合XPath F&O 3.1規格中記述的數值格式。" + }, + "$formatBase": { + "args": "number [, radix]", + "desc": "將`number`變換為以參數`radix`的值為基數形式的字串。如果不指定`radix`的值,則默認基數為10。指定的`radix`值必須在2~36之間,否則拋出錯誤。" + }, + "$toMillis": { + "args": "timestamp", + "desc": "將ISO 8601格式的字串`timestamp`轉換為從UNIX時間 (1970年1月1日 UTC/GMT的午夜)開始到現在的毫秒數。如果該字串的格式不正確,則拋出錯誤。" + }, + "$env": { + "args": "arg", + "desc": "返回環境變量的值。\n\n這是Node-RED定義的函數。" + }, + "$eval": { + "args": "expr [, context]", + "desc": "使用當前上下文來作為評估依據,分析並評估字符串`expr`,其中包含文字JSON或JSONata表達式。" + }, + "$formatInteger": { + "args": "number, picture", + "desc": "將“數字”轉換為字符串,並將其格式化為“圖片”字符串指定的整數表示形式。圖片字符串參數定義了數字的格式,並具有與XPath F&O 3.1 規範中的fn:format-integer相同的語法。" + }, + "$parseInteger": { + "args": "string, picture", + "desc": "使用“圖片”字符串指定的格式將“字符串”參數的內容解析為整數(作為JSON數字)。圖片字符串參數與$formatInteger格式相同。." + }, + "$error": { + "args": "[str]", + "desc": "引發錯誤並顯示一條消息。 可選的`str`將替代$error()函數評估的默認消息。" + }, + "$assert": { + "args": "arg, str", + "desc": "如果`arg`為真,則該函數返回。 如果arg為假,則拋出帶有str的異常作為異常消息。" + }, + "$single": { + "args": "array, function", + "desc": "返回滿足參數function謂語的array參數中的唯一值 (比如:傳遞值時,函數返回布林值“true”)。如果匹配值的數量不唯一時,則拋出異常。\n\n應在以下簽名中提供函數:`function(value [,index [,array []]])`其中value是數組的每個輸入,index是該值的位置,整個數組作為第三個參數傳遞。" + }, + "$encodeUrl": { + "args": "str", + "desc": "通過用表示字符的UTF-8編碼的一個,兩個,三個或四個轉義序列替換某些字符的每個實例,對統一資源定位符(URL)組件進行編碼。\n\n示例:`$encodeUrlComponent(\"?x=test\")` => `\"%3Fx%3Dtest\"`" + }, + "$encodeUrlComponent": { + "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": { + "args": "str", + "desc": "解碼以前由encodeUrlComponent創建的統一資源定位器(URL)組件。 \n\n示例: `$decodeUrlComponent(\"%3Fx%3Dtest\")` => `\"?x=test\"`" + }, + "$decodeUrlComponent": { + "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=шеллы\"`" + }, + "$distinct": { + "args": "array", + "desc": "返回一個數組,其中重復的值已從`數組`中刪除" + }, + "$type": { + "args": "value", + "desc": "以字符串形式返回`值`的類型。 如果該`值`未定義,則將返回`未定義`" + } +} diff --git a/packages/node_modules/@node-red/editor-client/package.json b/packages/node_modules/@node-red/editor-client/package.json index 7f8061c6d..6774df246 100644 --- a/packages/node_modules/@node-red/editor-client/package.json +++ b/packages/node_modules/@node-red/editor-client/package.json @@ -1,6 +1,6 @@ { "name": "@node-red/editor-client", - "version": "1.0.3", + "version": "1.1.0", "license": "Apache-2.0", "repository": { "type": "git", diff --git a/packages/node_modules/@node-red/editor-client/src/ace/bin/snippets/nrjavascript.js b/packages/node_modules/@node-red/editor-client/src/ace/bin/snippets/nrjavascript.js index d11f3fc6a..c0f5c2e0d 100644 --- a/packages/node_modules/@node-red/editor-client/src/ace/bin/snippets/nrjavascript.js +++ b/packages/node_modules/@node-red/editor-client/src/ace/bin/snippets/nrjavascript.js @@ -1,4 +1,4 @@ -ace.define("ace/snippets/nrjavascript",[],function(e,t,n){"use strict";t.snippetText=undefined,t.scope="nrjavascript"}); +ace.define("ace/snippets/nrjavascript",[],function(e,t,n){"use strict";t.snippetText='# Prototype\nsnippet proto\n ${1:class_name}.prototype.${2:method_name} = function(${3:first_argument}) {\n ${4:// body...}\n };\n# Function\nsnippet fun\n function ${1?:function_name}(${2:argument}) {\n ${3:// body...}\n }\n# Anonymous Function\nregex /((=)\\s*|(:)\\s*|(\\()|\\b)/f/(\\))?/\nsnippet f\n function${M1?: ${1:functionName}}($2) {\n ${0:$TM_SELECTED_TEXT}\n }${M2?;}${M3?,}${M4?)}\n# Immediate function\ntrigger \\(?f\\(\nendTrigger \\)?\nsnippet f(\n (function(${1}) {\n ${0:${TM_SELECTED_TEXT:/* code */}}\n }(${1}));\n# if\nsnippet if\n if (${1:true}) {\n ${0}\n }\n# if ... else\nsnippet ife\n if (${1:true}) {\n ${2}\n } else {\n ${0}\n }\n# tertiary conditional\nsnippet ter\n ${1:/* condition */} ? ${2:a} : ${3:b}\n# switch\nsnippet switch\n switch (${1:expression}) {\n case \'${3:case}\':\n ${4:// code}\n break;\n ${5}\n default:\n ${2:// code}\n }\n# case\nsnippet case\n case \'${1:case}\':\n ${2:// code}\n break;\n ${3}\n\n# while (...) {...}\nsnippet wh\n while (${1:/* condition */}) {\n ${0:/* code */}\n }\n# try\nsnippet try\n try {\n ${0:/* code */}\n } catch (e) {}\n# do...while\nsnippet do\n do {\n ${2:/* code */}\n } while (${1:/* condition */});\n# Object Method\nsnippet :f\nregex /([,{[])|^\\s*/:f/\n ${1:method_name}: function(${2:attribute}) {\n ${0}\n }${3:,}\n# setTimeout function\nsnippet setTimeout\nregex /\\b/st|timeout|setTimeo?u?t?/\n setTimeout(function() {${3:$TM_SELECTED_TEXT}}, ${1:10});\n# console.log (Firebug)\nsnippet cl\n console.log(${1});\n# return\nsnippet ret\n return ${1:result}\n# for (property in object ) { ... }\nsnippet fori\n for (var ${1:prop} in ${2:Things}) {\n ${0:$2[$1]}\n }\n# hasOwnProperty\nsnippet has\n hasOwnProperty(${1})\n# docstring\nsnippet /**\n /**\n * ${1:description}\n *\n */\nsnippet @par\nregex /^\\s*\\*\\s*/@(para?m?)?/\n @param {${1:type}} ${2:name} ${3:description}\nsnippet @ret\n @return {${1:type}} ${2:description}\n# JSON.parse\nsnippet jsonp\n JSON.parse(${1:jstr});\n# JSON.stringify\nsnippet jsons\n JSON.stringify(${1:object});\n# self-defining function\nsnippet sdf\n var ${1:function_name} = function(${2:argument}) {\n ${3:// initial code ...}\n\n $1 = function($2) {\n ${4:// main code}\n };\n }\n# \nsnippet for-\n for (var ${1:i} = ${2:Things}.length; ${1:i}--; ) {\n ${0:${2:Things}[${1:i}];}\n }\n# for (...) {...}\nsnippet for\n for (var ${1:i} = 0; $1 < ${2:Things}.length; $1++) {\n ${3:$2[$1]}$0\n }\n# for (...) {...} (Improved Native For-Loop)\nsnippet forr\n for (var ${1:i} = ${2:Things}.length - 1; $1 >= 0; $1--) {\n ${3:$2[$1]}$0\n }\n# Node-RED Specific Funcs\nsnippet nodes\n node.send(${1:msg})\nsnippet clone\n RED.util.cloneMessage(${1:msg})\nsnippet nodel\n node.log($1)\nsnippet nodew\n node.warn($1)\nsnippet nodee\n node.error($1)\nsnippet noded\n node.debug($1)\nsnippet done\n node.done($1)\nsnippet flowg\n flow.get($1)\nsnippet flows\n flow.set($1, $2)\nsnippet globalg\n global.get($1)\nsnippet globals\n global.set($1, $2)\n',t.scope="nrjavascript"}); (function() { ace.require(["ace/snippets/nrjavascript"], function(m) { if (typeof module == "object" && typeof exports == "object" && module) { @@ -6,4 +6,3 @@ ace.define("ace/snippets/nrjavascript",[],function(e,t,n){"use strict";t.snippet } }); })(); - \ No newline at end of file diff --git a/packages/node_modules/@node-red/editor-client/src/js/events.js b/packages/node_modules/@node-red/editor-client/src/js/events.js index f79cc864f..41e669aba 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/events.js +++ b/packages/node_modules/@node-red/editor-client/src/js/events.js @@ -32,11 +32,16 @@ } } } - function emit(evt,arg) { + function emit() { + var evt = arguments[0] + var args = Array.prototype.slice.call(arguments,1); + if (RED.events.DEBUG) { + console.log(evt,args); + } if (handlers[evt]) { for (var i=0;idiv:not(.node-input-env-container-row)"); var height = size.height; @@ -413,14 +450,18 @@ RED.nodes = (function() { } }); sf._def = RED.nodes.getType("subflow:"+sf.id); + RED.events.emit("subflows:add",sf); } function getSubflow(id) { return subflows[id]; } function removeSubflow(sf) { - delete subflows[sf.id]; - delete nodeTabMap[sf.id]; - registry.removeNodeType("subflow:"+sf.id); + if (subflows[sf.id]) { + delete subflows[sf.id]; + delete nodeTabMap[sf.id]; + registry.removeNodeType("subflow:"+sf.id); + RED.events.emit("subflows:remove",sf); + } } function subflowContains(sfid,nodeid) { @@ -493,6 +534,9 @@ RED.nodes = (function() { if (n.d === true) { node.d = true; } + if (n.g) { + node.g = n.g; + } if (node.type == "unknown") { for (var p in n._orig) { if (n._orig.hasOwnProperty(p)) { @@ -505,19 +549,33 @@ RED.nodes = (function() { node[d] = n[d]; } } - if(exportCreds && n.credentials) { + if (exportCreds) { var credentialSet = {}; - node.credentials = {}; - for (var cred in n._def.credentials) { - if (n._def.credentials.hasOwnProperty(cred)) { - if (n._def.credentials[cred].type == 'password') { + if (/^subflow:/.test(node.type) && n.credentials) { + // A subflow instance node can have arbitrary creds + for (var sfCred in n.credentials) { + if (n.credentials.hasOwnProperty(sfCred)) { if (!n.credentials._ || - n.credentials["has_"+cred] != n.credentials._["has_"+cred] || - (n.credentials["has_"+cred] && n.credentials[cred])) { + n.credentials["has_"+sfCred] != n.credentials._["has_"+sfCred] || + (n.credentials["has_"+sfCred] && n.credentials[sfCred])) { + credentialSet[sfCred] = n.credentials[sfCred]; + } + } + } + } else if (n.credentials) { + node.credentials = {}; + // All other nodes have a well-defined list of possible credentials + for (var cred in n._def.credentials) { + if (n._def.credentials.hasOwnProperty(cred)) { + if (n._def.credentials[cred].type == 'password') { + if (!n.credentials._ || + n.credentials["has_"+cred] != n.credentials._["has_"+cred] || + (n.credentials["has_"+cred] && n.credentials[cred])) { + credentialSet[cred] = n.credentials[cred]; + } + } else if (n.credentials[cred] != null && (!n.credentials._ || n.credentials[cred] != n.credentials._[cred])) { credentialSet[cred] = n.credentials[cred]; } - } else if (n.credentials[cred] != null && (!n.credentials._ || n.credentials[cred] != n.credentials._[cred])) { - credentialSet[cred] = n.credentials[cred]; } } } @@ -526,6 +584,13 @@ RED.nodes = (function() { } } } + if (n.type === "group") { + node.x = n.x; + node.y = n.y; + node.w = n.w; + node.h = n.h; + node.nodes = node.nodes.map(function(n) { return n.id }); + } if (n._def.category != "config") { node.x = n.x; node.y = n.y; @@ -568,7 +633,7 @@ RED.nodes = (function() { return node; } - function convertSubflow(n) { + function convertSubflow(n, exportCreds) { var node = {}; node.id = n.id; node.type = n.type; @@ -578,6 +643,24 @@ RED.nodes = (function() { node.in = []; node.out = []; node.env = n.env; + + if (exportCreds) { + var credentialSet = {}; + // A subflow node can have arbitrary creds + for (var sfCred in n.credentials) { + if (n.credentials.hasOwnProperty(sfCred)) { + if (!n.credentials._ || + n.credentials["has_"+sfCred] != n.credentials._["has_"+sfCred] || + (n.credentials["has_"+sfCred] && n.credentials[sfCred])) { + credentialSet[sfCred] = n.credentials[sfCred]; + } + } + } + if (Object.keys(credentialSet).length > 0) { + node.credentials = credentialSet; + } + } + node.color = n.color; n.in.forEach(function(p) { @@ -633,8 +716,18 @@ RED.nodes = (function() { /** * Converts the current node selection to an exportable JSON Object **/ - function createExportableNodeSet(set, exportedSubflows, exportedConfigNodes) { + function createExportableNodeSet(set, exportedIds, exportedSubflows, exportedConfigNodes) { var nns = []; + + exportedIds = exportedIds || {}; + set = set.filter(function(n) { + if (exportedIds[n.id]) { + return false; + } + exportedIds[n.id] = true; + return true; + }) + exportedConfigNodes = exportedConfigNodes || {}; exportedSubflows = exportedSubflows || {}; for (var n=0;n 1) { + throw new Error("Node-RED's IE11 Array.from polyfill doesn't support multiple arguments"); + } + var arrayLike = arguments[0] + var result = []; + if (arrayLike.forEach) { + arrayLike.forEach(function(i) { + result.push(i); + }) + } else { + for (var i=0;i)/); + 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) { $("#red-ui-editor").i18n(); $("#red-ui-palette > .red-ui-palette-spinner").hide(); $(".red-ui-palette-scroll").removeClass("hide"); $("#red-ui-palette-search").removeClass("hide"); - loadFlows(function() { - if (RED.settings.theme("projects.enabled",false)) { - RED.projects.refresh(function(activeProject) { + if (RED.settings.theme("projects.enabled",false)) { + RED.projects.refresh(function(activeProject) { + loadFlows(function() { RED.sidebar.info.refresh() if (!activeProject) { // Projects enabled but no active project @@ -140,12 +147,14 @@ var RED = (function() { } completeLoad(); }); - } else { + }); + } else { + loadFlows(function() { // Projects disabled by the user RED.sidebar.info.refresh() completeLoad(); - } - }); + }); + } } else { var config = configs.shift(); appendNodeConfig(config,stepConfig); @@ -157,6 +166,7 @@ var RED = (function() { } function loadFlows(done) { + loader.reportProgress(RED._("event.loadFlows"),80 ) $.ajax({ headers: { "Accept":"application/json", @@ -167,6 +177,7 @@ var RED = (function() { if (nodes) { var currentHash = window.location.hash; RED.nodes.version(nodes.rev); + loader.reportProgress(RED._("event.importFlows"),90 ) RED.nodes.import(nodes.flows); RED.nodes.dirty(false); RED.view.redraw(true); @@ -193,6 +204,7 @@ var RED = (function() { return; } if (notificationId === "project-update") { + loader.start("Loading project",0) RED.nodes.clear(); RED.history.clear(); RED.view.redraw(true); @@ -208,6 +220,7 @@ var RED = (function() { "revert": RED._("notification.project.revert", {project: msg.project}), "merge-complete": RED._("notification.project.merge-complete") }[msg.action]; + loader.end() RED.notify("

"+message+"

"); RED.sidebar.info.refresh() }); @@ -423,6 +436,12 @@ var RED = (function() { var id = topic.substring(9); RED.eventLog.log(id,payload); }); + + $(".red-ui-header-toolbar").show(); + + setTimeout(function() { + loader.end(); + },100); } function showAbout() { @@ -431,8 +450,7 @@ var RED = (function() { ''+ ''; - RED.sidebar.info.set(aboutHeader+marked(data)); - RED.sidebar.info.show(); + RED.sidebar.help.set(aboutHeader+RED.utils.renderMarkdown(data)); }); } @@ -472,6 +490,14 @@ var RED = (function() { {id:"menu-item-subflow-create",label:RED._("menu.label.createSubflow"),onselect:"core:create-subflow"}, {id:"menu-item-subflow-convert",label:RED._("menu.label.selectionToSubflow"),disabled:true,onselect:"core:convert-to-subflow"}, ]}); + menuOptions.push({id:"menu-item-group",label:RED._("menu.label.groups"), options: [ + {id:"menu-item-group-group",label:RED._("menu.label.groupSelection"),disabled:true,onselect:"core:group-selection"}, + {id:"menu-item-group-ungroup",label:RED._("menu.label.ungroupSelection"),disabled:true,onselect:"core:ungroup-selection"}, + null, + {id:"menu-item-group-merge",label:RED._("menu.label.groupMergeSelection"),disabled:true,onselect:"core:merge-selection-to-group"}, + {id:"menu-item-group-remove",label:RED._("menu.label.groupRemoveSelection"),disabled:true,onselect:"core:remove-selection-from-group"} + ]}); + menuOptions.push(null); if (RED.settings.theme('palette.editable') !== false) { menuOptions.push({id:"menu-item-edit-palette",label:RED._("menu.label.editPalette"),onselect:"core:manage-palette"}); @@ -497,7 +523,6 @@ var RED = (function() { } function loadEditor() { - RED.workspaces.init(); RED.statusBar.init(); RED.view.init(); @@ -524,6 +549,7 @@ var RED = (function() { } RED.subflow.init(); + RED.group.init(); RED.clipboard.init(); RED.search.init(); RED.actionList.init(); @@ -539,13 +565,14 @@ var RED = (function() { RED.comms.connect(); $("#red-ui-main-container").show(); - $(".red-ui-header-toolbar").show(); + RED.actions.add("core:show-about", showAbout); loadNodeList(); } + function buildEditor(options) { var header = $('
').appendTo(options.target); var logo = $('').appendTo(header); @@ -560,6 +587,10 @@ var RED = (function() { '').appendTo(options.target); $('
').appendTo(options.target); $('
').appendTo(options.target); + + loader.init().appendTo("#red-ui-main-container"); + loader.start("...",0); + $.getJSON(options.apiRootUrl+"theme", function(theme) { if (theme.header) { if (theme.header.url) { @@ -592,12 +623,39 @@ var RED = (function() { options.target.addClass("red-ui-editor"); buildEditor(options); + RED.i18n.init(options, function() { RED.settings.init(options, loadEditor); }) } + var loader = { + init: function() { + var wrapper = $('
').hide(); + var container = $('
').appendTo(wrapper); + var label = $('
',{class:"red-ui-loading-bar-label"}).appendTo(container); + var bar = $('
',{class:"red-ui-loading-bar"}).appendTo(container); + var fill =$('').appendTo(bar); + return wrapper; + }, + start: function(text, prcnt) { + if (text) { + loader.reportProgress(text,prcnt) + } + $("#red-ui-loading-progress").show(); + }, + reportProgress: function(text, prcnt) { + $(".red-ui-loading-bar-label").text(text); + $(".red-ui-loading-bar span").width(prcnt+"%") + }, + end: function() { + $("#red-ui-loading-progress").hide(); + loader.reportProgress("",0); + } + } + return { - init: init + init: init, + loader: loader } })(); diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/clipboard.js b/packages/node_modules/@node-red/editor-client/src/js/ui/clipboard.js index cdff62919..33b306752 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/clipboard.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/clipboard.js @@ -184,7 +184,7 @@ RED.clipboard = (function() { '
'+ '
'+ '
'+ - '
'+ + '
'+ ''+ '
'+ '
'+ @@ -216,7 +216,7 @@ RED.clipboard = (function() { ' '+ ''+ '
'+ - '
'+ + '
'+ ''+ '
'+ '
'+ @@ -474,6 +474,12 @@ RED.clipboard = (function() { },100) } + var dialogHeight = 400; + var winHeight = $(window).height(); + if (winHeight < 600) { + dialogHeight = 400 - (600 - winHeight); + } + $(".red-ui-clipboard-dialog-box").height(dialogHeight); dialog.dialog("option","title",RED._("clipboard.importNodes")).dialog("open"); popover = RED.popover.create({ @@ -583,6 +589,7 @@ RED.clipboard = (function() { nodes = []; selection.forEach(function(n) { nodes.push(n); + nodes = nodes.concat(RED.nodes.groups(n.id)); nodes = nodes.concat(RED.nodes.filterNodes({z:n.id})); }); } else { @@ -592,7 +599,8 @@ RED.clipboard = (function() { nodes = RED.nodes.createExportableNodeSet(nodes.filter(function(n) { return n.type !== 'subflow'})); } else if (type === 'red-ui-clipboard-dialog-export-rng-flow') { var activeWorkspace = RED.workspaces.active(); - nodes = RED.nodes.filterNodes({z:activeWorkspace}); + 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); @@ -637,6 +645,14 @@ RED.clipboard = (function() { $("#red-ui-clipboard-dialog-export-fmt-mini").trigger("click"); } tabs.activateTab("red-ui-clipboard-dialog-export-tab-"+mode); + + var dialogHeight = 400; + var winHeight = $(window).height(); + if (winHeight < 600) { + dialogHeight = 400 - (600 - winHeight); + } + $(".red-ui-clipboard-dialog-box").height(dialogHeight); + dialog.dialog("option","title",RED._("clipboard.exportNodes")).dialog( "open" ); $("#red-ui-clipboard-dialog-export-text").trigger("focus"); @@ -738,6 +754,8 @@ RED.clipboard = (function() { RED.actions.add("core:show-library-export-dialog",function() { exportNodes('library') }); RED.actions.add("core:show-library-import-dialog",function() { importNodes('library') }); + RED.actions.add("core:show-examples-import-dialog",function() { importNodes('examples') }); + RED.events.on("editor:open",function() { disabled = true; }); RED.events.on("editor:close",function() { disabled = false; }); RED.events.on("search:open",function() { disabled = true; }); diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/common/colorPicker.js b/packages/node_modules/@node-red/editor-client/src/js/ui/common/colorPicker.js new file mode 100644 index 000000000..3739cd970 --- /dev/null +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/common/colorPicker.js @@ -0,0 +1,203 @@ +RED.colorPicker = (function() { + + function create(options) { + var color = options.value; + var id = options.id; + var colorPalette = options.palette || []; + var width = options.cellWidth || 30; + var height = options.cellHeight || 30; + var margin = options.cellMargin || 2; + var perRow = options.cellPerRow || 6; + + var container = $("
",{style:"display:inline-block"}); + var colorHiddenInput = $("", { id: id, type: "hidden", value: color }).appendTo(container); + var opacityHiddenInput = $("", { id: id+"-opacity", type: "hidden", value: options.hasOwnProperty('opacity')?options.opacity:"1" }).appendTo(container); + + var colorButton = $('').css({ + width:"20px" + }).appendTo(buttons).on("click", function(evt) { + evt.preventDefault(); + var currentType = that.input.attr("type"); + if (currentType === "text") { + that.input.attr("type","password"); + eyeCon.removeClass("fa-eye-slash").addClass("fa-eye"); + setTimeout(function() { + that.input.focus(); + },50); + } else { + that.input.attr("type","text"); + eyeCon.removeClass("fa-eye").addClass("fa-eye-slash"); + setTimeout(function() { + that.input.focus(); + },50); + } + }).hide(); + var eyeCon = $('').css("margin-left","-1px").appendTo(eyeButton); + + if (value === "__PWRD__") { + var innerContainer = $('
').css({ + padding:"6px 6px", + borderRadius:"4px" + }).addClass("red-ui-typedInput-value-label-inactive").appendTo(container); + var editButton = $('').appendTo(buttons).on("click", function(evt) { + evt.preventDefault(); + innerContainer.hide(); + container.css("background","none"); + container.css("pointer-events","none"); + that.input.val(""); + that.element.val(""); + that.elementDiv.show(); + editButton.hide(); + cancelButton.show(); + eyeButton.show(); + setTimeout(function() { + that.input.focus(); + },50); + }); + var cancelButton = $('').css("margin-left","3px").appendTo(buttons).on("click", function(evt) { + evt.preventDefault(); + innerContainer.show(); + container.css("background",""); + that.input.val("__PWRD__"); + that.element.val("__PWRD__"); + that.elementDiv.hide(); + editButton.show(); + cancelButton.hide(); + eyeButton.hide(); + that.input.attr("type","password"); + eyeCon.removeClass("fa-eye-slash").addClass("fa-eye"); + + }).hide(); + } else { + container.css("background","none"); + container.css("pointer-events","none"); + this.elementDiv.show(); + eyeButton.show(); + } + } } }; var nlsd = false; @@ -220,6 +298,8 @@ that.input.attr(d,m); }); + this.defaultInputType = this.input.attr('type'); + this.uiSelect.addClass("red-ui-typedInput-container"); this.element.attr('type','hidden'); @@ -488,56 +568,6 @@ done(labelWidth); } }, - _resize: function() { - var that = this; - if (this.uiWidth !== null) { - this.uiSelect.width(this.uiWidth); - } - var type = this.typeMap[this.propertyType]; - if (type && type.hasValue === false) { - this.selectTrigger.addClass("red-ui-typedInput-full-width"); - } else { - this.selectTrigger.removeClass("red-ui-typedInput-full-width"); - this._getLabelWidth(this.selectTrigger, function(labelWidth) { - that.elementDiv.css('left',labelWidth+"px"); - that.valueLabelContainer.css('left',labelWidth+"px"); - if (that.optionExpandButton.shown) { - that.elementDiv.css('right',"22px"); - that.valueLabelContainer.css('right',"22px"); - } else { - that.elementDiv.css('right','0'); - that.valueLabelContainer.css('right','0'); - that.input.css({ - 'border-top-right-radius': '4px', - 'border-bottom-right-radius': '4px' - }); - } - if (that.optionSelectTrigger) { - if (type && type.options && type.hasValue === true) { - that.optionSelectLabel.css({'left':'auto'}) - that._getLabelWidth(that.optionSelectLabel, function(lw) { - that.optionSelectTrigger.css({'width':(23+lw)+"px"}); - that.elementDiv.css('right',(23+lw)+"px"); - that.input.css({ - 'border-top-right-radius': 0, - 'border-bottom-right-radius': 0 - }); - }); - } else { - that.optionSelectLabel.css({'left':'0'}) - that.optionSelectTrigger.css({'width':'calc( 100% - '+labelWidth+'px )'}); - if (!that.optionExpandButton.shown) { - that.elementDiv.css({'right':0}); - that.input.css({ - 'border-top-right-radius': '4px', - 'border-bottom-right-radius': '4px' - }); - } - } - } - }); - } - }, _updateOptionSelectLabel: function(o) { var opt = this.typeMap[this.propertyType]; this.optionSelectLabel.empty(); @@ -565,7 +595,6 @@ } if (opt.hasValue) { this.optionValue = o.value; - this._resize(); this.input.trigger('change',this.propertyType,this.value()); } } else { @@ -605,11 +634,12 @@ this.propertyType = null; this.type(currentType); } - setTimeout(function() {that._resize();},0); }, width: function(desiredWidth) { this.uiWidth = desiredWidth; - this._resize(); + if (this.uiWidth !== null) { + this.uiSelect.width(this.uiWidth); + } }, value: function(value) { var that = this; @@ -680,19 +710,23 @@ } else if (opt.icon.indexOf("/") !== -1) { image = new Image(); - image.onload = function() { that._resize(); } - image.onerror = function() { that._resize(); } image.name = opt.icon; image.src = mapDeprecatedIcon(opt.icon); $('',{src:mapDeprecatedIcon(opt.icon),style:"margin-right: 4px;height: 18px;"}).prependTo(this.selectLabel); } else { - $('',{class:"red-ui-typedInput-icon "+opt.icon}).prependTo(this.selectLabel); + $('',{class:"red-ui-typedInput-icon "+opt.icon,style:"min-width: 13px; margin-right: 4px;"}).prependTo(this.selectLabel); } } if (opt.hasValue === false || (opt.showLabel !== false && !opt.icon)) { this.selectLabel.text(opt.label); } + if (opt.hasValue === false) { + this.selectTrigger.addClass("red-ui-typedInput-full-width"); + } else { + this.selectTrigger.removeClass("red-ui-typedInput-full-width"); + } + if (this.optionMenu) { this.optionMenu.remove(); this.optionMenu = null; @@ -703,11 +737,13 @@ this.optionExpandButton.shown = false; } if (this.optionSelectTrigger) { - this.optionSelectTrigger.show(); + this.optionSelectTrigger.css({"display":"inline-flex"}); if (!opt.hasValue) { + this.optionSelectTrigger.css({"flex-grow":1}) this.elementDiv.hide(); this.valueLabelContainer.hide(); } else { + this.optionSelectTrigger.css({"flex-grow":0}) this.elementDiv.show(); this.valueLabelContainer.hide(); } @@ -822,6 +858,11 @@ if (this.optionSelectTrigger) { this.optionSelectTrigger.hide(); } + if (opt.inputType) { + this.input.attr('type',opt.inputType) + } else { + this.input.attr('type',this.defaultInputType) + } if (opt.hasValue === false) { this.oldValue = this.input.val(); this.input.val(""); @@ -830,8 +871,8 @@ } else if (opt.valueLabel) { this.valueLabelContainer.show(); this.valueLabelContainer.empty(); - opt.valueLabel.call(this,this.valueLabelContainer,this.input.val()); this.elementDiv.hide(); + opt.valueLabel.call(this,this.valueLabelContainer,this.input.val()); } else { if (this.oldValue !== undefined) { this.input.val(this.oldValue); @@ -881,9 +922,6 @@ this._trigger("typechange",null,this.propertyType); this.input.trigger('change',this.propertyType,this.value()); } - if (!image) { - this._resize(); - } } } }, @@ -910,7 +948,6 @@ }, show: function() { this.uiSelect.show(); - this._resize(); }, hide: function() { this.uiSelect.hide(); diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/deploy.js b/packages/node_modules/@node-red/editor-client/src/js/ui/deploy.js index cdf84d02f..fdff99a69 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/deploy.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/deploy.js @@ -108,7 +108,7 @@ RED.deploy = (function() { - RED.events.on('nodes:change',function(state) { + RED.events.on('workspace:dirty',function(state) { if (state.dirty) { window.onbeforeunload = function() { return RED._("deploy.confirm.undeployedChanges"); @@ -334,8 +334,7 @@ RED.deploy = (function() { var invalidNodes = []; RED.nodes.eachNode(function(node) { - hasInvalid = hasInvalid || !node.valid; - if (!node.valid) { + if (!node.valid && !node.d) { invalidNodes.push(getNodeInfo(node)); } if (node.type === "unknown") { @@ -345,6 +344,7 @@ RED.deploy = (function() { } }); hasUnknown = unknownNodes.length > 0; + hasInvalid = invalidNodes.length > 0; var unusedConfigNodes = []; RED.nodes.eachConfig(function(node) { diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/editor.js b/packages/node_modules/@node-red/editor-client/src/js/ui/editor.js index 5f8379aa1..29e8ab06b 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/editor.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/editor.js @@ -490,8 +490,7 @@ RED.editor = (function() { done(); } } - - if (definition.credentials) { + if (definition.credentials || /^subflow:/.test(definition.type)) { if (node.credentials) { populateCredentialsInputs(node, definition.credentials, node.credentials, prefix); completePrepare(); @@ -499,7 +498,9 @@ RED.editor = (function() { $.getJSON(getCredentialsURL(node.type, node.id), function (data) { node.credentials = data; node.credentials._ = $.extend(true,{},data); - populateCredentialsInputs(node, definition.credentials, node.credentials, prefix); + if (!/^subflow:/.test(definition.type)) { + populateCredentialsInputs(node, definition.credentials, node.credentials, prefix); + } completePrepare(); }); } @@ -513,7 +514,9 @@ RED.editor = (function() { for (var i=editStack.length-1;i').prependTo(dialogForm); - $('').prependTo(dialogForm); + // - the elements cannot be hidden otherwise Chrome will ignore them. + // - the elements need to have id's that imply password/username + $('').prependTo(dialogForm); + $('').prependTo(dialogForm); + $('').prependTo(dialogForm); dialogForm.on("submit", function(e) { e.preventDefault();}); - dialogForm.find('input').attr("autocomplete","disable"); + dialogForm.find('input').attr("autocomplete","off"); return dialogForm; } @@ -783,6 +791,11 @@ RED.editor = (function() { nodeDiv.css({ 'backgroundColor': backgroundColor }); + var borderColor = RED.utils.getDarkerColor(backgroundColor); + if (borderColor !== backgroundColor) { + nodeDiv.css('border-color',borderColor) + } + } var iconContainer = $('
',{class:"red-ui-palette-icon-container"}).appendTo(nodeDiv); RED.utils.createIconElement(icon_url, iconContainer, true); @@ -818,99 +831,6 @@ RED.editor = (function() { searchInput.trigger("focus"); } - function createColorPicker(colorRow, color) { - - var colorButton = $('').appendTo(popOverContent) $('

',{style:"font-size: 0.8em"}).text(metaData).appendTo(popOverContent); } } catch(err) { @@ -178,7 +182,11 @@ RED.palette = (function() { function setIcon(element,sf) { var icon_url = RED.utils.getNodeIcon(sf._def); var iconContainer = element.find(".red-ui-palette-icon-container"); - RED.utils.createIconElement(icon_url, iconContainer, true); + var currentIcon = iconContainer.attr("data-palette-icon"); + if (currentIcon !== icon_url) { + iconContainer.attr("data-palette-icon", icon_url); + RED.utils.createIconElement(icon_url, iconContainer, true); + } } function getPaletteNode(type) { @@ -221,6 +229,7 @@ RED.palette = (function() { var iconContainer = $('

', { class: "red-ui-palette-icon-container"+(((!def.align && def.inputs !== 0 && def.outputs === 0) || "right" === def.align) ? " red-ui-palette-icon-container-right" : "") }).appendTo(d); + iconContainer.attr("data-palette-icon", icon_url); RED.utils.createIconElement(icon_url, iconContainer, true); } @@ -247,6 +256,7 @@ RED.palette = (function() { var popover = RED.popover.create({ target:d, trigger: "hover", + interactive: true, width: "300px", content: "hi", delay: { show: 750, hide: 50 } @@ -262,25 +272,28 @@ RED.palette = (function() { // html: true, // container:'body' // }); - d.on("click", function() { - RED.view.focus(); - var helpText; - if (nt.indexOf("subflow:") === 0) { - helpText = marked(RED.nodes.subflow(nt.substring(8)).info||"")||(''+RED._("sidebar.info.none")+''); - } else { - helpText = $("script[data-help-name='"+d.attr("data-palette-type")+"']").html()||(''+RED._("sidebar.info.none")+''); - } - // 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.info.set(helpText,RED._("sidebar.info.nodeHelp")); - }); + // 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||"")||(''+RED._("sidebar.info.none")+''); + // } else { + // helpText = $("script[data-help-name='"+d.attr("data-palette-type")+"']").html()||(''+RED._("sidebar.info.none")+''); + // } + // // 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; var mouseX; var mouseY; var spliceTimer; + var groupTimer; + var activeGroup; + var hoverGroup; var paletteWidth; var paletteTop; $(d).draggable({ @@ -292,16 +305,53 @@ RED.palette = (function() { start: function() { paletteWidth = $("#red-ui-palette").width(); paletteTop = $("#red-ui-palette").parent().position().top + $("#red-ui-palette-container").position().top; + hoverGroup = null; + activeGroup = RED.view.getActiveGroup(); + if (activeGroup) { + document.getElementById("group_select_"+activeGroup.id).classList.add("red-ui-flow-group-active-hovered"); + } RED.view.focus(); }, - stop: function() { d3.select('.red-ui-flow-link-splice').classed('red-ui-flow-link-splice',false); if (spliceTimer) { clearTimeout(spliceTimer); spliceTimer = null;}}, + stop: function() { + d3.select('.red-ui-flow-link-splice').classed('red-ui-flow-link-splice',false); + if (hoverGroup) { + document.getElementById("group_select_"+hoverGroup.id).classList.remove("red-ui-flow-group-hovered"); + } + if (activeGroup) { + document.getElementById("group_select_"+activeGroup.id).classList.remove("red-ui-flow-group-active-hovered"); + } + if (spliceTimer) { clearTimeout(spliceTimer); spliceTimer = null; } + if (groupTimer) { clearTimeout(groupTimer); groupTimer = null; } + }, drag: function(e,ui) { 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(); + if (!groupTimer) { + groupTimer = setTimeout(function() { + mouseX /= RED.view.scale(); + mouseY /= RED.view.scale(); + var group = RED.view.getGroupAtPoint(mouseX,mouseY); + if (group !== hoverGroup) { + if (hoverGroup) { + document.getElementById("group_select_"+hoverGroup.id).classList.remove("red-ui-flow-group-hovered"); + } + if (group) { + document.getElementById("group_select_"+group.id).classList.add("red-ui-flow-group-hovered"); + } + hoverGroup = group; + if (hoverGroup) { + $(ui.helper).data('group',hoverGroup); + } else { + $(ui.helper).removeData('group'); + } + } + groupTimer = null; + },200) + } if (def.inputs > 0 && def.outputs > 0) { - mouseX = ui.position.left - paletteWidth + (ui.helper.width()/2) + chart.scrollLeft(); - mouseY = ui.position.top - paletteTop + (ui.helper.height()/2) + chart.scrollTop(); if (!spliceTimer) { spliceTimer = setTimeout(function() { var nodes = []; @@ -367,7 +417,7 @@ RED.palette = (function() { RED.workspaces.show(nt.substring(8)); e.preventDefault(); }); - nodeInfo = marked(def.info||""); + nodeInfo = RED.utils.renderMarkdown(def.info||""); } setLabel(nt,d,label,nodeInfo); @@ -409,67 +459,73 @@ RED.palette = (function() { categoryNode.show(); paletteNode.show(); } - function refreshNodeTypes() { - RED.nodes.eachSubflow(function(sf) { - var paletteNode = getPaletteNode('subflow:'+sf.id); - var portInput = paletteNode.find(".red-ui-palette-port-input"); - var portOutput = paletteNode.find(".red-ui-palette-port-output"); + RED.nodes.eachSubflow(refreshSubflow) + } + function refreshSubflow(sf) { + var paletteNode = getPaletteNode('subflow:'+sf.id); + var portInput = paletteNode.find(".red-ui-palette-port-input"); + var portOutput = paletteNode.find(".red-ui-palette-port-output"); - var paletteLabel = paletteNode.find(".red-ui-palette-label"); - paletteLabel.attr("class","red-ui-palette-label" + (((!sf._def.align && sf.in.length !== 0 && sf.out.length === 0) || "right" === sf._def.align) ? " red-ui-palette-label-right" : "")); + var paletteLabel = paletteNode.find(".red-ui-palette-label"); + paletteLabel.attr("class","red-ui-palette-label" + (((!sf._def.align && sf.in.length !== 0 && sf.out.length === 0) || "right" === sf._def.align) ? " red-ui-palette-label-right" : "")); - var paletteIconContainer = paletteNode.find(".red-ui-palette-icon-container"); - paletteIconContainer.attr("class","red-ui-palette-icon-container" + (((!sf._def.align && sf.in.length !== 0 && sf.out.length === 0) || "right" === sf._def.align) ? " red-ui-palette-icon-container-right" : "")); + var paletteIconContainer = paletteNode.find(".red-ui-palette-icon-container"); + paletteIconContainer.attr("class","red-ui-palette-icon-container" + (((!sf._def.align && sf.in.length !== 0 && sf.out.length === 0) || "right" === sf._def.align) ? " red-ui-palette-icon-container-right" : "")); - if (portInput.length === 0 && sf.in.length > 0) { - var portIn = document.createElement("div"); - portIn.className = "red-ui-palette-port red-ui-palette-port-input"; - paletteNode.append(portIn); - } else if (portInput.length !== 0 && sf.in.length === 0) { - portInput.remove(); + if (portInput.length === 0 && sf.in.length > 0) { + var portIn = document.createElement("div"); + portIn.className = "red-ui-palette-port red-ui-palette-port-input"; + paletteNode.append(portIn); + } else if (portInput.length !== 0 && sf.in.length === 0) { + portInput.remove(); + } + + if (portOutput.length === 0 && sf.out.length > 0) { + var portOut = document.createElement("div"); + portOut.className = "red-ui-palette-port red-ui-palette-port-output"; + paletteNode.append(portOut); + } else if (portOutput.length !== 0 && sf.out.length === 0) { + portOutput.remove(); + } + var currentLabel = paletteNode.attr("data-palette-label"); + var currentInfo = paletteNode.attr("data-palette-info"); + + if (currentLabel !== sf.name || currentInfo !== sf.info) { + paletteNode.attr("data-palette-info",sf.info); + setLabel(sf.type+":"+sf.id,paletteNode,sf.name,RED.utils.renderMarkdown(sf.info||"")); + } + setIcon(paletteNode,sf); + + var currentCategory = paletteNode.data('category'); + var newCategory = (sf.category||"subflows"); + if (currentCategory !== newCategory) { + var category = escapeCategory(newCategory); + createCategory(newCategory,category,category,"node-red"); + + var currentCategoryNode = paletteNode.closest(".red-ui-palette-category"); + var newCategoryNode = $("#red-ui-palette-"+category); + newCategoryNode.append(paletteNode); + if (newCategoryNode.find(".red-ui-palette-node").length === 1) { + categoryContainers[category].open(); } - if (portOutput.length === 0 && sf.out.length > 0) { - var portOut = document.createElement("div"); - portOut.className = "red-ui-palette-port red-ui-palette-port-output"; - paletteNode.append(portOut); - } else if (portOutput.length !== 0 && sf.out.length === 0) { - portOutput.remove(); - } - setLabel(sf.type+":"+sf.id,paletteNode,sf.name,marked(sf.info||"")); - setIcon(paletteNode,sf); - - var currentCategory = paletteNode.data('category'); - var newCategory = (sf.category||"subflows"); - if (currentCategory !== newCategory) { - var category = escapeCategory(newCategory); - createCategory(newCategory,category,category,"node-red"); - - var currentCategoryNode = paletteNode.closest(".red-ui-palette-category"); - var newCategoryNode = $("#red-ui-palette-"+category); - newCategoryNode.append(paletteNode); - if (newCategoryNode.find(".red-ui-palette-node").length === 1) { - categoryContainers[category].open(); - } - - paletteNode.data('category',newCategory); - if (currentCategoryNode.find(".red-ui-palette-node").length === 0) { - if (currentCategoryNode.find("i").hasClass("expanded")) { - currentCategoryNode.find(".red-ui-palette-content").slideToggle(); - currentCategoryNode.find("i").toggleClass("expanded"); - } + paletteNode.data('category',newCategory); + if (currentCategoryNode.find(".red-ui-palette-node").length === 0) { + if (currentCategoryNode.find("i").hasClass("expanded")) { + currentCategoryNode.find(".red-ui-palette-content").slideToggle(); + currentCategoryNode.find("i").toggleClass("expanded"); } } + } - paletteNode.css("backgroundColor", sf.color); - }); + paletteNode.css("backgroundColor", sf.color); } function filterChange(val) { var re = new RegExp(val.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'),'i'); $("#red-ui-palette-container .red-ui-palette-node").each(function(i,el) { - var currentLabel = $(el).find(".red-ui-palette-label").text(); + var currentLabel = $(el).attr("data-palette-label"); var type = $(el).attr("data-palette-type"); if (val === "" || re.test(type) || re.test(currentLabel)) { $(this).show(); @@ -501,6 +557,8 @@ RED.palette = (function() { $('').appendTo("#red-ui-palette"); $('
').appendTo("#red-ui-palette"); + $("#red-ui-palette > .red-ui-palette-spinner").show(); + RED.events.on('registry:node-type-added', function(nodeType) { var def = RED.nodes.getType(nodeType); @@ -542,7 +600,8 @@ RED.palette = (function() { } }); - $("#red-ui-palette > .red-ui-palette-spinner").show(); + RED.events.on("subflows:change",refreshSubflow); + $("#red-ui-palette-search input").searchBox({ diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/projects/projectSettings.js b/packages/node_modules/@node-red/editor-client/src/js/ui/projects/projectSettings.js index 2d377ba54..f0944c879 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/projects/projectSettings.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/projects/projectSettings.js @@ -158,7 +158,7 @@ RED.projects.settings = (function() { container.empty(); var desc; if (activeProject.description) { - desc = marked(activeProject.description); + desc = RED.utils.renderMarkdown(activeProject.description); } else { desc = '' + RED._("sidebar.project.noDescriptionAvailable") + ''; } diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/projects/projectUserSettings.js b/packages/node_modules/@node-red/editor-client/src/js/ui/projects/projectUserSettings.js index 4219b189f..170aacea0 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/projects/projectUserSettings.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/projects/projectUserSettings.js @@ -30,13 +30,13 @@ RED.projects.userSettings = (function() { $('
').appendTo(gitconfigContainer).text(RED._("editor:sidebar.project.userSettings.committerTip")); var row = $('
').appendTo(gitconfigContainer); - $('').text(RED._("editor:sidebar.project.userSettings.userName")).appendTo(row); - gitUsernameInput = $('').appendTo(row); + $('').text(RED._("editor:sidebar.project.userSettings.userName")).appendTo(row); + gitUsernameInput = $('').appendTo(row); gitUsernameInput.val(currentGitSettings.user.name||""); row = $('
').appendTo(gitconfigContainer); - $('').text(RED._("editor:sidebar.project.userSettings.email")).appendTo(row); - gitEmailInput = $('').appendTo(row); + $('').text(RED._("editor:sidebar.project.userSettings.email")).appendTo(row); + gitEmailInput = $('').appendTo(row); gitEmailInput.val(currentGitSettings.user.email||""); } diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/projects/projects.js b/packages/node_modules/@node-red/editor-client/src/js/ui/projects/projects.js index 0c297c82a..462ad7575 100755 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/projects/projects.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/projects/projects.js @@ -685,6 +685,8 @@ RED.projects = (function() { } } },projectData).then(function() { + RED.menu.setDisabled('menu-item-projects-open',false); + RED.menu.setDisabled('menu-item-projects-settings',false); RED.events.emit("project:change", {name:name}); }).always(function() { setTimeout(function() { @@ -1495,7 +1497,6 @@ RED.projects = (function() { } } else if (projectType === 'open') { return switchProject(selectedProject.name,function(err,data) { - dialog.dialog( "close" ); if (err) { if (err.code !== 'credentials_load_failed') { console.log(RED._("projects.create.unexpected_error"),err) @@ -1604,6 +1605,7 @@ RED.projects = (function() { }, } },{active:true}).then(function() { + dialog.dialog( "close" ); RED.events.emit("project:change", {name:name}); }).always(function() { setTimeout(function() { @@ -1671,16 +1673,27 @@ RED.projects = (function() { if (typeof buttons === 'function') { buttons = buttons(options||{}); } + + + dialog.dialog('option','buttons',buttons); dialogBody.append(container); + + + var dialogHeight = 590; + var winHeight = $(window).height(); + if (winHeight < 750) { + dialogHeight = 590 - (750 - winHeight); + } + $(".red-ui-projects-dialog-box").height(dialogHeight); + $(".red-ui-projects-dialog-project-list-inner-container").height(Math.max(500,dialogHeight) - 180); dialog.dialog('option','title',screen.title||""); dialog.dialog("open"); - dialog.dialog({position: { 'my': 'center top', 'at': 'center top+20', 'of': window }}); } function createProjectList(options) { options = options||{}; - var height = options.height || "300px"; + var height = options.height || "200px"; var container = $('
',{class:"red-ui-projects-dialog-project-list-container" }); var filterTerm = ""; @@ -1939,100 +1952,121 @@ RED.projects = (function() { } }).fail(function(xhr,textStatus,err) { var responses; + if (options.responses && options.responses[xhr.status]) { responses = options.responses[xhr.status]; if (typeof responses === 'function') { resultCallback = responses; resultCallbackArgs = {error:responses.statusText}; return; - } else if (options.handleAuthFail !== false && xhr.responseJSON.code === 'git_auth_failed') { - var url = activeProject.git.remotes[xhr.responseJSON.remote||options.remote||'origin'].fetch; + } else if (options.handleAuthFail !== false && (xhr.responseJSON.code === 'git_auth_failed' || xhr.responseJSON.code === 'git_host_key_verification_failed')) { + if (xhr.responseJSON.code === 'git_auth_failed') { + var url = activeProject.git.remotes[xhr.responseJSON.remote||options.remote||'origin'].fetch; - var message = $('
'+ + var message = $('
'+ '
'+RED._("projects.send-req.auth-req")+':
'+ '
'+url+'
'+ '
'); - var isSSH = false; - if (/^https?:\/\//.test(url)) { - $('
'+ - '
').appendTo(message); - } else if (/^(?:ssh|[\d\w\.\-_]+@[\w\.]+):(?:\/\/)?/.test(url)) { - isSSH = true; - var row = $('
').appendTo(message); - $('').appendTo(row); - var projectRepoSSHKeySelect = $('
'+ + '
').appendTo(message); + } else if (/^(?:ssh|[\d\w\.\-_]+@[\w\.]+):(?:\/\/)?/.test(url)) { + isSSH = true; + var row = $('
').appendTo(message); + $('').appendTo(row); + var projectRepoSSHKeySelect = $('').appendTo(row); - } + row = $('
').appendTo(message); + $('').appendTo(row); + $('').appendTo(row); + } - var notification = RED.notify(message,{ - type:"error", - fixed: true, - modal: true, - buttons: [ - { - //id: "node-dialog-delete", - //class: 'leftButton', - text: RED._("common.label.cancel"), - click: function() { - notification.close(); - } - },{ - text: ' ' +RED._("projects.send-req.retry") +'', - click: function() { - body = body || {}; - var authBody = {}; - if (isSSH) { - authBody.keyFile = $('#projects-user-auth-key').val(); - authBody.passphrase = $('#projects-user-auth-passphrase').val(); - } else { - authBody.username = $('#projects-user-auth-username').val(); - authBody.password = $('#projects-user-auth-password').val(); + var notification = RED.notify(message,{ + type:"error", + fixed: true, + modal: true, + buttons: [ + { + //id: "node-dialog-delete", + //class: 'leftButton', + text: RED._("common.label.cancel"), + click: function() { + notification.close(); } - var done = function(err) { - if (err) { - console.log(RED._("projects.send-req.update-failed")); - console.log(err); + },{ + text: ' ' +RED._("projects.send-req.retry") +'', + click: function() { + body = body || {}; + var authBody = {}; + if (isSSH) { + authBody.keyFile = $('#projects-user-auth-key').val(); + authBody.passphrase = $('#projects-user-auth-passphrase').val(); } else { - sendRequest(options,body); - notification.close(); + authBody.username = $('#projects-user-auth-username').val(); + authBody.password = $('#projects-user-auth-password').val(); } + var done = function(err) { + if (err) { + console.log(RED._("projects.send-req.update-failed")); + console.log(err); + } else { + sendRequest(options,body); + notification.close(); + } - } - sendRequest({ - url: "projects/"+activeProject.name+"/remotes/"+(xhr.responseJSON.remote||options.remote||'origin'), - type: "PUT", - responses: { - 0: function(error) { - done(error,null); - }, - 200: function(data) { - done(null,data); - }, - 400: { - 'unexpected_error': function(error) { - done(error,null); - } - }, } - },{auth:authBody}); + sendRequest({ + url: "projects/"+activeProject.name+"/remotes/"+(xhr.responseJSON.remote||options.remote||'origin'), + type: "PUT", + responses: { + 0: function(error) { + done(error,null); + }, + 200: function(data) { + done(null,data); + }, + 400: { + 'unexpected_error': function(error) { + done(error,null); + } + }, + } + },{auth:authBody}); + } } - } - ] - }); - return; + ] + }); + return; + } else if (xhr.responseJSON.code === 'git_host_key_verification_failed') { + var message = $('
'+ + '
'+RED._("projects.send-req.host-key-verify-failed")+'
'+ + '
'); + var notification = RED.notify(message,{ + type:"error", + fixed: true, + modal: true, + buttons: [ + { + text: RED._("common.label.close"), + click: function() { + notification.close(); + } + } + ] + }); + return; + } } else if (responses[xhr.responseJSON.code]) { resultCallback = responses[xhr.responseJSON.code]; resultCallbackArgs = xhr.responseJSON; @@ -2066,6 +2100,8 @@ RED.projects = (function() { var branchFilterTerm = ""; var branchFilterCreateItem; var branches = []; + var branchNames = new Set(); + var remotes = []; var branchPrefix = ""; var container = $('
').appendTo(options.container); @@ -2073,18 +2109,30 @@ RED.projects = (function() { delay: 200, change: function() { branchFilterTerm = $(this).val(); - if (/(\.\.|\/\.|[?*[~^: \\]|\/\/|\/.$|\/$)/.test(branchFilterTerm)) { + // if there is a / then + // - check what preceeds it is a known remote + + var valid = false; + var hasRemote = false; + var m = /^([^/]+)\/[^/.~*?\[]/.exec(branchFilterTerm); + if (m && remotes.indexOf(m[1]) > -1) { + valid = true; + hasRemote = true; + } + + if (!valid && /(\.\.|\/\.|[?*[~^: \\]|\/\/|\/.$|\/$)/.test(branchFilterTerm)) { if (!branchFilterCreateItem.hasClass("input-error")) { branchFilterCreateItem.addClass("input-error"); branchFilterCreateItem.find("i").addClass("fa-warning").removeClass("fa-code-fork"); } - branchFilterCreateItem.find("span").text(RED._("projects.create-branch-list.invalid")+": "+branchPrefix+branchFilterTerm); + branchFilterCreateItem.find("span").text(RED._("projects.create-branch-list.invalid")+": "+(hasRemote?"":branchPrefix)+branchFilterTerm); } else { if (branchFilterCreateItem.hasClass("input-error")) { branchFilterCreateItem.removeClass("input-error"); branchFilterCreateItem.find("i").removeClass("fa-warning").addClass("fa-code-fork"); } - branchFilterCreateItem.find(".red-ui-sidebar-vc-branch-list-entry-create-name").text(branchPrefix+branchFilterTerm); + branchFilterCreateItem.find("span").text(RED._("projects.create-branch-list.create")+":"); + branchFilterCreateItem.find(".red-ui-sidebar-vc-branch-list-entry-create-name").text((hasRemote?"":branchPrefix)+branchFilterTerm); } branchList.editableList("filter"); } @@ -2117,8 +2165,12 @@ RED.projects = (function() { if (!entry.hasOwnProperty('commit')) { body.name = branchFilter.val(); body.create = true; - if (options.remote) { - body.name = options.remote()+"/"+body.name; + + if (options.remotes) { + var m = /^([^/]+)\/[^/.~*?\[]/.exec(body.name); + if (!m || remotes.indexOf(m[1]) === -1) { + body.name = remotes[0]+"/"+body.name; + } } } else { if ($(this).hasClass('selected')) { @@ -2133,11 +2185,17 @@ RED.projects = (function() { }, filter: function(data) { var isCreateEntry = (!data.hasOwnProperty('commit')); + var filterTerm = branchFilterTerm; + if (remotes.length > 0) { + var m = /^([^/]+)\/[^/.~*?\[]/.exec(filterTerm); + if (filterTerm !== "" && (!m || remotes.indexOf(m[1]) == -1)) { + filterTerm = remotes[0]+"/"+filterTerm; + } + } return ( isCreateEntry && ( - branchFilterTerm !== "" && - branches.indexOf(branchPrefix+branchFilterTerm) === -1 + filterTerm !== "" && !branchNames.has(filterTerm) ) ) || ( @@ -2152,12 +2210,14 @@ RED.projects = (function() { branchList.editableList('empty'); var start = Date.now(); var spinner = addSpinnerOverlay(container).addClass("red-ui-component-spinner-contain"); - if (options.remote) { - branchPrefix = options.remote()+"/"; + if (options.remotes) { + remotes = options.remotes(); + branchPrefix = remotes[0]+"/"; } else { branchPrefix = ""; + remotes = []; } - + branchNames = new Set(); sendRequest({ url: url, type: "GET", @@ -2169,6 +2229,7 @@ RED.projects = (function() { branches = result.branches; result.branches.forEach(function(b) { branchList.editableList('addItem',b); + branchNames.add(b.name); }); branchList.editableList('addItem',{}); setTimeout(function() { @@ -2204,7 +2265,7 @@ RED.projects = (function() { } function init() { - dialog = $('
') + dialog = $('
') .appendTo("#red-ui-editor") .dialog({ modal: true, @@ -2291,6 +2352,7 @@ RED.projects = (function() { if (data.active) { $.getJSON("projects/"+data.active, function(project) { activeProject = project; + RED.events.emit("projects:load",activeProject); RED.sidebar.versionControl.refresh(true); if (done) { done(activeProject); diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/projects/tab-versionControl.js b/packages/node_modules/@node-red/editor-client/src/js/ui/projects/tab-versionControl.js index 7069be6b7..7d20fe094 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/projects/tab-versionControl.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/projects/tab-versionControl.js @@ -336,7 +336,7 @@ RED.sidebar.versionControl = (function() { var unstagedContent = $('
').appendTo(localChanges.content); var header = $('
'+RED._("sidebar.project.versionControl.localFiles")+'
').appendTo(unstagedContent); - stageAllButton = $('') + stageAllButton = $('') .appendTo(header) .on("click", function(evt) { evt.preventDefault(); @@ -368,7 +368,7 @@ RED.sidebar.versionControl = (function() { unmergedContent = $('
').appendTo(localChanges.content); header = $('
'+RED._("sidebar.project.versionControl.unmergedChanges")+'
').appendTo(unmergedContent); - bg = $('
').appendTo(header); + bg = $('
').appendTo(header); var abortMergeButton = $('') .appendTo(bg) .on("click", function(evt) { @@ -433,7 +433,7 @@ RED.sidebar.versionControl = (function() { header = $('
'+RED._("sidebar.project.versionControl.changeToCommit")+'
').appendTo(stagedContent); - bg = $('
').appendTo(header); + bg = $('
').appendTo(header); var showCommitBox = function() { commitMessage.val(""); submitCommitButton.prop("disabled",true); @@ -830,10 +830,9 @@ RED.sidebar.versionControl = (function() { var remoteBranchList = utils.createBranchList({ placeholder: RED._("sidebar.project.versionControl.createRemoteBranchPlaceholder"), currentLabel: RED._("sidebar.project.versionControl.upstream"), - remote: function() { + remotes: function() { var project = RED.projects.getActiveProject(); - var remotes = Object.keys(project.git.remotes); - return remotes[0]; + return Object.keys(project.git.remotes); }, container: remoteBranchSubRow, onselect: function(body) { diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/search.js b/packages/node_modules/@node-red/editor-client/src/js/ui/search.js index 9bb4cefb0..7be689c4a 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/search.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/search.js @@ -23,8 +23,7 @@ RED.search = (function() { var visible = false; var index = {}; - var keys = []; - var results = []; + var currentResults = []; var previousActiveElement; @@ -66,23 +65,9 @@ RED.search = (function() { } } - function indexWorkspace() { - index = {}; - RED.nodes.eachWorkspace(indexNode); - RED.nodes.eachSubflow(indexNode); - RED.nodes.eachConfig(indexNode); - RED.nodes.eachNode(indexNode); - keys = Object.keys(index); - keys.sort(); - keys.forEach(function(key) { - index[key] = Object.keys(index[key]).map(function(id) { - return index[key][id]; - }) - }) - } - function search(val) { - searchResults.editableList('empty'); + var results = []; + var keys = Object.keys(index); var typeFilter; var m = /(?:^| )type:([^ ]+)/.exec(val); if (m) { @@ -92,8 +77,7 @@ RED.search = (function() { val = val.trim(); - selected = -1; - results = []; + if (val.length > 0 || typeFilter) { val = val.toLowerCase(); var i; @@ -104,10 +88,14 @@ RED.search = (function() { var key = keys[i]; var kpos = keys[i].indexOf(val); if (kpos > -1) { - for (j=0;j 0) { - for (i=0;i 25) { - searchResults.editableList('addItem', { - more: { - results: results, - start: 25 - } - }) - } - } else { - searchResults.editableList('addItem',{}); - } } + return results; } function ensureSelectedIsVisible() { @@ -161,13 +135,37 @@ RED.search = (function() { searchInput = $('').appendTo(searchDiv).searchBox({ delay: 200, change: function() { - search($(this).val()); + searchResults.editableList('empty'); + selected = -1; + currentResults = search($(this).val()); + if (currentResults.length > 0) { + for (i=0;i 25) { + searchResults.editableList('addItem', { + more: { + results: currentResults, + start: 25 + } + }) + } + } else { + searchResults.editableList('addItem',{}); + } + + } }); + var copySearchContainer = $('').appendTo(searchDiv).on('click', function(evt) { + evt.preventDefault(); + RED.sidebar.info.outliner.search(searchInput.val()) + hide(); + }); searchInput.on('keydown',function(evt) { var children; - if (results.length > 0) { + if (currentResults.length > 0) { if (evt.keyCode === 40) { // Down children = searchResults.children(); @@ -199,21 +197,21 @@ RED.search = (function() { var object = $(children[selected]).find(".red-ui-editableList-item-content").data('data'); if (object) { searchResults.editableList('removeItem',object); - for (i=object.more.start;i object.more.start+25) { + if (currentResults.length > object.more.start+25) { searchResults.editableList('addItem', { more: { - results: results, + results: currentResults, start: object.more.start+25 } }) } } } else { - if (results.length > 0) { - reveal(results[Math.max(0,selected)].node); + if (currentResults.length > 0) { + reveal(currentResults[Math.max(0,selected)].node); } } } @@ -234,13 +232,13 @@ RED.search = (function() { div.on("click", function(evt) { evt.preventDefault(); searchResults.editableList('removeItem',object); - for (i=object.more.start;i object.more.start+25) { + if (currentResults.length > object.more.start+25) { searchResults.editableList('addItem', { more: { - results: results, + results: currentResults, start: object.more.start+25 } }) @@ -253,17 +251,7 @@ RED.search = (function() { var def = node._def; div = $('',{href:'#',class:"red-ui-search-result"}).appendTo(container); - var nodeDiv = $('
',{class:"red-ui-search-result-node"}).appendTo(div); - var colour = RED.utils.getNodeColor(node.type,def); - var icon_url = RED.utils.getNodeIcon(def,node); - if (node.type === 'tab') { - colour = "#C0DEED"; - } - nodeDiv.css('backgroundColor',colour); - - var iconContainer = $('
',{class:"red-ui-palette-icon-container"}).appendTo(nodeDiv); - RED.utils.createIconElement(icon_url, iconContainer, true); - + RED.utils.createNodeIcon(node).appendTo(div); var contentDiv = $('
',{class:"red-ui-search-result-node-description"}).appendTo(div); if (node.z) { var workspace = RED.nodes.workspace(node.z); @@ -308,7 +296,7 @@ RED.search = (function() { $("#red-ui-palette-shade").show(); $("#red-ui-sidebar-shade").show(); $("#red-ui-sidebar-separator").hide(); - indexWorkspace(); + if (dialog === null) { createDialog(); } @@ -342,6 +330,28 @@ RED.search = (function() { } } + function clearIndex() { + index = {}; + } + + function addItemToIndex(item) { + indexNode(item); + } + function removeItemFromIndex(item) { + var keys = Object.keys(index); + for (var i=0,l=keys.length;i 0) { + n = tmplist.shift(); + if (n.type === "group") { + includedGroups.add(n.id); + tmplist = tmplist.concat(n.nodes); + } + nodeList.add(n); + } + + nodeList = Array.from(nodeList); + + var containingGroup = nodeList[0].g; + var nodesMovedFromGroup = []; + + for (i=0; i
'):undefined, addItem: function(container, i, opt) { + // If this is an instance node, these are properties unique to + // this instance - ie opt.parent will not be defined. + if (isTemplateNode) { container.addClass("red-ui-editor-subflow-env-editable") } @@ -859,52 +927,64 @@ RED.subflow = (function() { var nameField = null; var valueField = null; - // if (opt.parent) { - // buildEnvUIRow(envRow,opt,opt.parent.ui||{}) - // } else { - nameField = $('', { - class: "node-input-env-name", - type: "text", - placeholder: RED._("common.label.name") - }).attr("autocomplete","disable").appendTo(envRow).val(opt.name); - valueField = $('',{ - style: "width:100%", - class: "node-input-env-value", - type: "text", - }).attr("autocomplete","disable").appendTo(envRow) - valueField.typedInput({default:'str',types:['str','num','bool','json','bin','env']}); - valueField.typedInput('type', opt.parent?(opt.type||opt.parent.type):opt.type); - valueField.typedInput('value', opt.parent?((opt.value !== undefined)?opt.value:opt.parent.value):opt.value); - // } + nameField = $('', { + class: "node-input-env-name", + type: "text", + placeholder: RED._("common.label.name") + }).attr("autocomplete","disable").appendTo(envRow).val(opt.name); + valueField = $('',{ + style: "width:100%", + class: "node-input-env-value", + type: "text", + }).attr("autocomplete","disable").appendTo(envRow) + valueField.typedInput({default:'str',types:isTemplateNode?DEFAULT_ENV_TYPE_LIST:DEFAULT_ENV_TYPE_LIST_INC_CRED}); + valueField.typedInput('type', opt.type); + if (opt.type === "cred") { + if (opt.value) { + valueField.typedInput('value', opt.value); + } else if (node.credentials && node.credentials[opt.name]) { + valueField.typedInput('value', node.credentials[opt.name]); + } else if (node.credentials && node.credentials['has_'+opt.name]) { + valueField.typedInput('value', "__PWRD__"); + } else { + valueField.typedInput('value', ""); + } + } else { + valueField.typedInput('value', opt.value); + } opt.nameField = nameField; opt.valueField = valueField; - if (!opt.parent) { - var actionButton = $('
',{href:"#",class:"red-ui-editableList-item-remove red-ui-button red-ui-button-small"}).appendTo(envRow); - $('',{class:"fa "+(opt.parent?"fa-reply":"fa-remove")}).appendTo(actionButton); - var removeTip = RED.popover.tooltip(actionButton,RED._("subflow.env.remove")); - actionButton.on("click", function(evt) { - evt.preventDefault(); - removeTip.close(); - container.parent().addClass("red-ui-editableList-item-deleting") - container.fadeOut(300, function() { - envContainer.editableList('removeItem',opt); - }); + var actionButton = $('',{href:"#",class:"red-ui-editableList-item-remove red-ui-button red-ui-button-small"}).appendTo(envRow); + $('',{class:"fa "+(opt.parent?"fa-reply":"fa-remove")}).appendTo(actionButton); + var removeTip = RED.popover.tooltip(actionButton,RED._("subflow.env.remove")); + actionButton.on("click", function(evt) { + evt.preventDefault(); + removeTip.close(); + container.parent().addClass("red-ui-editableList-item-deleting") + container.fadeOut(300, function() { + envContainer.editableList('removeItem',opt); }); - } + }); if (isTemplateNode) { // Add the UI customisation row // if `opt.ui` does not exist, then apply defaults. If these // defaults do not change then they will get stripped off // before saving. - opt.ui = opt.ui || { - icon: "", - label: {}, - type: "input", - opts: {types:['str','num','bool','json','bin','env']} + if (opt.type === 'cred') { + opt.ui = opt.ui || { + icon: "", + type: "cred" + } + } else { + opt.ui = opt.ui || { + icon: "", + type: "input", + opts: {types:DEFAULT_ENV_TYPE_LIST} + } } opt.ui.label = opt.ui.label || {}; opt.ui.type = opt.ui.type || "input"; @@ -995,11 +1075,11 @@ RED.subflow = (function() { var row = $('
').appendTo(container); $('
').appendTo(row); - var typeOptions = { - 'input': {types:['str','num','bool','json','bin','env']}, + 'input': {types:DEFAULT_ENV_TYPE_LIST}, 'select': {opts:[]}, - 'spinner': {} + 'spinner': {}, + 'cred': {} }; if (ui.opts) { typeOptions[ui.type] = ui.opts; @@ -1054,7 +1134,7 @@ RED.subflow = (function() { } langs.forEach(function(l) { var row = $('
').appendTo(content); - $('').css({display:"inline-block",width:"120px"}).text(RED._("languages."+l)+(l===currentLocale?"*":"")).appendTo(row); + $('').css({display:"inline-block",width:"120px"}).text(RED._("languages."+l)+(l===currentLocale?"*":"")).appendTo(row); $('').text(ui.label[l]||"").appendTo(row); }); return content; @@ -1062,314 +1142,341 @@ RED.subflow = (function() { nameField.on('change',function(evt) { labelInput.attr("placeholder",$(this).val()) - }); + }); - var inputCell = $('
').appendTo(row); - var inputCellInput = $('').css("width","100%").appendTo(inputCell); - if (ui.type === "input") { - inputCellInput.val(ui.opts.types.join(",")); - } - var checkbox; - var selectBox; + var inputCell = $('
').appendTo(row); + var inputCellInput = $('').css("width","100%").appendTo(inputCell); + if (ui.type === "input") { + inputCellInput.val(ui.opts.types.join(",")); + } + var checkbox; + var selectBox; - inputCellInput.typedInput({ - types: [ - { - value:"input", - label:RED._("editor.inputs.input"), icon:"fa fa-i-cursor",showLabel:false,multiple:true,options:[ - {value:"str",label:RED._("editor.types.str"),icon:"red/images/typedInput/az.svg"}, - {value:"num",label:RED._("editor.types.num"),icon:"red/images/typedInput/09.svg"}, - {value:"bool",label:RED._("editor.types.bool"),icon:"red/images/typedInput/bool.svg"}, - {value:"json",label:RED._("editor.types.json"),icon:"red/images/typedInput/json.svg"}, - {value: "bin",label: RED._("editor.types.bin"),icon: "red/images/typedInput/bin.svg"}, - {value: "env",label: RED._("editor.types.env"),icon: "red/images/typedInput/env.svg"} - ], - default: ['str','num','bool','json','bin','env'], - valueLabel: function(container,value) { - container.css("padding",0); - var innerContainer = $('
').css({ - "background":"white", - "height":"100%", - "box-sizing": "border-box" - }).appendTo(container); + inputCellInput.typedInput({ + types: [ + { + value:"input", + label:RED._("editor.inputs.input"), icon:"fa fa-i-cursor",showLabel:false,multiple:true,options:[ + {value:"str",label:RED._("editor.types.str"),icon:"red/images/typedInput/az.svg"}, + {value:"num",label:RED._("editor.types.num"),icon:"red/images/typedInput/09.svg"}, + {value:"bool",label:RED._("editor.types.bool"),icon:"red/images/typedInput/bool.svg"}, + {value:"json",label:RED._("editor.types.json"),icon:"red/images/typedInput/json.svg"}, + {value: "bin",label: RED._("editor.types.bin"),icon: "red/images/typedInput/bin.svg"}, + {value: "env",label: RED._("editor.types.env"),icon: "red/images/typedInput/env.svg"}, + {value: "cred",label: RED._("editor.types.cred"),icon: "fa fa-lock"} + ], + default: DEFAULT_ENV_TYPE_LIST, + valueLabel: function(container,value) { + container.css("padding",0); + var innerContainer = $('
').css({ + "background":"white", + "height":"100%", + "box-sizing": "border-box" + }).appendTo(container); - var input = $('
').appendTo(innerContainer); - $('').appendTo(input); - if (value.length) { - value.forEach(function(v) { - $('',{src:v.icon,style:"max-width:14px; padding: 0 3px; margin-top:-4px; margin-left: 3px"}).appendTo(input); - }) - } else { - $("").css({ - "color":"#aaa", - "padding-left": "4px" - }).text("select types...").appendTo(input); - } - } - }, - { - value:"select", - label:RED._("editor.inputs.select"), icon:"fa fa-tasks",showLabel:false, - valueLabel: function(container,value) { - container.css("padding","0"); + var input = $('
').appendTo(innerContainer); + $('').appendTo(input); + if (value.length) { + value.forEach(function(v) { + if (!/^fa /.test(v.icon)) { + $('',{src:v.icon,style:"max-width:14px; padding: 0 3px; margin-top:-4px; margin-left: 1px"}).appendTo(input); + } else { + var s = $('',{style:"max-width:14px; padding: 0 3px; margin-top:-4px; margin-left: 1px"}).appendTo(input); + $("",{class: v.icon}).appendTo(s); + } + }) + } else { + $("").css({ + "color":"#aaa", + "padding-left": "4px" + }).text("select types...").appendTo(input); + } + } + }, + { + value: "cred", + label: RED._("typedInput.type.cred"), icon:"fa fa-lock", showLabel: false, + valueLabel: function(container,value) { + container.css("padding",0); + var innerContainer = $('
').css({ + "background":"white", + "height":"100%", + "box-sizing": "border-box", + "border-top-right-radius": "4px", + "border-bottom-right-radius": "4px" + }).appendTo(container); + $('
').html("••••••••").appendTo(innerContainer); + } + }, + { + value:"select", + label:RED._("editor.inputs.select"), icon:"fa fa-tasks",showLabel:false, + valueLabel: function(container,value) { + container.css("padding","0"); - selectBox = $('').appendTo(container); - if (ui.opts && Array.isArray(ui.opts.opts)) { - ui.opts.opts.forEach(function(o) { - var label = lookupLabel(o.l, o.l["en-US"]||o.v, currentLocale); - // $('
').appendTo(container); - var optList = $('
    ').appendTo(content).editableList({ - header:$("
    "+RED._("editor.select.label")+"
    "+RED._("editor.select.value")+"
    "), - addItem: function(row,index,itemData) { - var labelDiv = $('
    ').appendTo(row); - var label = lookupLabel(itemData.l, "", currentLocale); - itemData.label = $('').val(label).appendTo(labelDiv); - itemData.label.on('keydown', function(evt) { - if (evt.keyCode === 13) { - itemData.input.focus(); - evt.preventDefault(); - } - }); - var labelIcon = $('').appendTo(labelDiv); - RED.popover.tooltip(labelIcon,function() { - return currentLocale; - }) - itemData.input = $('').val(itemData.v).appendTo(row); + selectBox = $('').appendTo(container); + if (ui.opts && Array.isArray(ui.opts.opts)) { + ui.opts.opts.forEach(function(o) { + var label = lookupLabel(o.l, o.l["en-US"]||o.v, currentLocale); + // $('
    ').appendTo(container); + var optList = $('
      ').appendTo(content).editableList({ + header:$("
      "+RED._("editor.select.label")+"
      "+RED._("editor.select.value")+"
      "), + addItem: function(row,index,itemData) { + var labelDiv = $('
      ').appendTo(row); + var label = lookupLabel(itemData.l, "", currentLocale); + itemData.label = $('').val(label).appendTo(labelDiv); + itemData.label.on('keydown', function(evt) { + if (evt.keyCode === 13) { + itemData.input.focus(); + evt.preventDefault(); + } + }); + var labelIcon = $('').appendTo(labelDiv); + RED.popover.tooltip(labelIcon,function() { + return currentLocale; + }) + itemData.input = $('').val(itemData.v).appendTo(row); - // Problem using a TI here: - // - this is in a popout panel - // - clicking the expand button in the TI will close the parent edit tray - // and open the type editor. - // - but it leaves the popout panel over the top. - // - there is no way to get back to the popout panel after closing the type editor - //.typedInput({default:itemData.t||'str', types:['str','num','bool','json','bin','env']}); - itemData.input.on('keydown', function(evt) { - if (evt.keyCode === 13) { - // Enter or Tab - var index = optList.editableList('indexOf',itemData); - var length = optList.editableList('length'); - if (index + 1 === length) { - var newItem = {}; - optList.editableList('addItem',newItem); - setTimeout(function() { - if (newItem.label) { - newItem.label.focus(); - } - },100) - } else { - var nextItem = optList.editableList('getItemAt',index+1); - if (nextItem.label) { - nextItem.label.focus() - } - } - evt.preventDefault(); - } - }); - }, - sortable: true, - removable: true, - height: 160 - }) - if (ui.opts.opts.length > 0) { - ui.opts.opts.forEach(function(o) { - optList.editableList('addItem',$.extend(true,{},o)) - }) - } else { - optList.editableList('addItem',{}) - } - return { - onclose: function() { - var items = optList.editableList('items'); - var vals = []; - items.each(function (i,el) { - var data = el.data('data'); - var l = data.label.val().trim(); - var v = data.input.val(); - // var t = data.input.typedInput('type'); - // var v = data.input.typedInput('value'); - if (l.length > 0) { - data.l = data.l || {}; - data.l[currentLocale] = l; - } - data.v = v; + // Problem using a TI here: + // - this is in a popout panel + // - clicking the expand button in the TI will close the parent edit tray + // and open the type editor. + // - but it leaves the popout panel over the top. + // - there is no way to get back to the popout panel after closing the type editor + //.typedInput({default:itemData.t||'str', types:DEFAULT_ENV_TYPE_LIST}); + itemData.input.on('keydown', function(evt) { + if (evt.keyCode === 13) { + // Enter or Tab + var index = optList.editableList('indexOf',itemData); + var length = optList.editableList('length'); + if (index + 1 === length) { + var newItem = {}; + optList.editableList('addItem',newItem); + setTimeout(function() { + if (newItem.label) { + newItem.label.focus(); + } + },100) + } else { + var nextItem = optList.editableList('getItemAt',index+1); + if (nextItem.label) { + nextItem.label.focus() + } + } + evt.preventDefault(); + } + }); + }, + sortable: true, + removable: true, + height: 160 + }) + if (ui.opts.opts.length > 0) { + ui.opts.opts.forEach(function(o) { + optList.editableList('addItem',$.extend(true,{},o)) + }) + } else { + optList.editableList('addItem',{}) + } + return { + onclose: function() { + var items = optList.editableList('items'); + var vals = []; + items.each(function (i,el) { + var data = el.data('data'); + var l = data.label.val().trim(); + var v = data.input.val(); + // var t = data.input.typedInput('type'); + // var v = data.input.typedInput('value'); + if (l.length > 0) { + data.l = data.l || {}; + data.l[currentLocale] = l; + } + data.v = v; - if (l.length > 0 || v.length > 0) { - var val = {l:data.l,v:data.v}; - // if (t !== 'str') { - // val.t = t; - // } - vals.push(val); - } - }); - ui.opts.opts = vals; - inputCellInput.typedInput('value',Date.now()) - } - } - } - } - }, - { - value:"checkbox", - label:RED._("editor.inputs.checkbox"), icon:"fa fa-check-square-o",showLabel:false, - valueLabel: function(container,value) { - container.css("padding",0); - checkbox = $('').appendTo(container); - checkbox.on('change', function(evt) { - valueField.typedInput('value',$(this).prop('checked')?"true":"false"); - }) - checkbox.prop('checked',valueField.typedInput('value')==="true"); - } - }, - { - value:"spinner", - label:RED._("editor.inputs.spinner"), icon:"fa fa-sort-numeric-asc", showLabel:false, - valueLabel: function(container,value) { - container.css("padding",0); - var innerContainer = $('
      ').css({ - "background":"white", - "height":"100%", - "box-sizing": "border-box" - }).appendTo(container); + if (l.length > 0 || v.length > 0) { + var val = {l:data.l,v:data.v}; + // if (t !== 'str') { + // val.t = t; + // } + vals.push(val); + } + }); + ui.opts.opts = vals; + inputCellInput.typedInput('value',Date.now()) + } + } + } + } + }, + { + value:"checkbox", + label:RED._("editor.inputs.checkbox"), icon:"fa fa-check-square-o",showLabel:false, + valueLabel: function(container,value) { + container.css("padding",0); + checkbox = $('').appendTo(container); + checkbox.on('change', function(evt) { + valueField.typedInput('value',$(this).prop('checked')?"true":"false"); + }) + checkbox.prop('checked',valueField.typedInput('value')==="true"); + } + }, + { + value:"spinner", + label:RED._("editor.inputs.spinner"), icon:"fa fa-sort-numeric-asc", showLabel:false, + valueLabel: function(container,value) { + container.css("padding",0); + var innerContainer = $('
      ').css({ + "background":"white", + "height":"100%", + "box-sizing": "border-box" + }).appendTo(container); - var input = $('
      ').appendTo(innerContainer); - $('').appendTo(input); + var input = $('
      ').appendTo(innerContainer); + $('').appendTo(input); - var min = ui.opts && ui.opts.min; - var max = ui.opts && ui.opts.max; - var label = ""; - if (min !== undefined && max !== undefined) { - label = Math.min(min,max)+" - "+Math.max(min,max); - } else if (min !== undefined) { - label = "> "+min; - } else if (max !== undefined) { - label = "< "+max; - } - $('').css("margin-left","15px").text(label).appendTo(input); - }, - expand: { - icon: "fa-caret-down", - content: function(container) { - var content = $('
      ').appendTo(container); - content.css("padding","8px 5px") - var min = ui.opts.min; - var max = ui.opts.max; - var minInput = $(''); - minInput.val(min); - var maxInput = $(''); - maxInput.val(max); - $('
      ').append(minInput).appendTo(content); - $('
      ').append(maxInput).appendTo(content); - return { - onclose: function() { - var min = minInput.val().trim(); - var max = maxInput.val().trim(); - if (min !== "") { - ui.opts.min = parseInt(min); - } else { - delete ui.opts.min; - } - if (max !== "") { - ui.opts.max = parseInt(max); - } else { - delete ui.opts.max; - } - inputCellInput.typedInput('value',Date.now()) - } - } - } - } - }, - { - value:"none", - label:RED._("editor.inputs.none"), icon:"fa fa-times",hasValue:false - }, - { - value:"hide", - label:RED._("editor.inputs.hidden"), icon:"fa fa-ban",hasValue:false - } - ], - default: 'none' - }).on("typedinputtypechange", function(evt,type) { - ui.type = $(this).typedInput("type"); - ui.opts = typeOptions[ui.type]; - if (ui.type === 'input') { - // In the case of 'input' type, the typedInput uses the multiple-option - // mode. Its value needs to be set to a comma-separately list of the - // selected options. - inputCellInput.typedInput('value',ui.opts.types.join(",")) - } else { - // No other type cares about `value`, but doing this will - // force a refresh of the label now that `ui.opts` has - // been updated. - inputCellInput.typedInput('value',Date.now()) - } + var min = ui.opts && ui.opts.min; + var max = ui.opts && ui.opts.max; + var label = ""; + if (min !== undefined && max !== undefined) { + label = Math.min(min,max)+" - "+Math.max(min,max); + } else if (min !== undefined) { + label = "> "+min; + } else if (max !== undefined) { + label = "< "+max; + } + $('').css("margin-left","15px").text(label).appendTo(input); + }, + expand: { + icon: "fa-caret-down", + content: function(container) { + var content = $('
      ').appendTo(container); + content.css("padding","8px 5px") + var min = ui.opts.min; + var max = ui.opts.max; + var minInput = $(''); + minInput.val(min); + var maxInput = $(''); + maxInput.val(max); + $('
      ').append(minInput).appendTo(content); + $('
      ').append(maxInput).appendTo(content); + return { + onclose: function() { + var min = minInput.val().trim(); + var max = maxInput.val().trim(); + if (min !== "") { + ui.opts.min = parseInt(min); + } else { + delete ui.opts.min; + } + if (max !== "") { + ui.opts.max = parseInt(max); + } else { + delete ui.opts.max; + } + inputCellInput.typedInput('value',Date.now()) + } + } + } + } + }, + { + value:"none", + label:RED._("editor.inputs.none"), icon:"fa fa-times",hasValue:false + }, + { + value:"hide", + label:RED._("editor.inputs.hidden"), icon:"fa fa-ban",hasValue:false + } + ], + default: 'none' + }).on("typedinputtypechange", function(evt,type) { + ui.type = $(this).typedInput("type"); + ui.opts = typeOptions[ui.type]; + if (ui.type === 'input') { + // In the case of 'input' type, the typedInput uses the multiple-option + // mode. Its value needs to be set to a comma-separately list of the + // selected options. + inputCellInput.typedInput('value',ui.opts.types.join(",")) + } else { + // No other type cares about `value`, but doing this will + // force a refresh of the label now that `ui.opts` has + // been updated. + inputCellInput.typedInput('value',Date.now()) + } - switch (ui.type) { - case 'input': + switch (ui.type) { + case 'input': valueField.typedInput('types',ui.opts.types); break; case 'select': - valueField.typedInput('types',['str']); - break; - case 'checkbox': + valueField.typedInput('types',['str']); + break; + case 'checkbox': valueField.typedInput('types',['bool']); break; case 'spinner': - valueField.typedInput('types',['num']) + valueField.typedInput('types',['num']); + break; + case 'cred': + valueField.typedInput('types',['cred']); break; default: - valueField.typedInput('types',['str','num','bool','json','bin','env']) - } - if (ui.type === 'checkbox') { - valueField.typedInput('type','bool'); - } else if (ui.type === 'spinner') { - valueField.typedInput('type','num'); - } - if (ui.type !== 'checkbox') { - checkbox = null; - } + valueField.typedInput('types',DEFAULT_ENV_TYPE_LIST) + } + if (ui.type === 'checkbox') { + valueField.typedInput('type','bool'); + } else if (ui.type === 'spinner') { + valueField.typedInput('type','num'); + } + if (ui.type !== 'checkbox') { + checkbox = null; + } - }).on("change", function(evt,type) { - if (ui.type === 'input') { - ui.opts.types = inputCellInput.typedInput('value').split(","); - valueField.typedInput('types',ui.opts.types); - } - }); - valueField.on("change", function(evt) { - if (checkbox) { - checkbox.prop('checked',$(this).typedInput('value')==="true") - } - }) - // Set the input to the right type. This will trigger the 'typedinputtypechange' - // event handler (just above ^^) to update the value if needed - inputCellInput.typedInput('type',ui.type) + }).on("change", function(evt,type) { + if (ui.type === 'input') { + ui.opts.types = inputCellInput.typedInput('value').split(","); + valueField.typedInput('types',ui.opts.types); + } + }); + valueField.on("change", function(evt) { + if (checkbox) { + checkbox.prop('checked',$(this).typedInput('value')==="true") + } + }) + // Set the input to the right type. This will trigger the 'typedinputtypechange' + // event handler (just above ^^) to update the value if needed + inputCellInput.typedInput('type',ui.type) } - function buildEnvUIRow(row, tenv, ui) { + function buildEnvUIRow(row, tenv, ui, node) { ui.label = ui.label||{}; - if (!ui.type) { + if ((tenv.type === "cred" || (tenv.parent && tenv.parent.type === "cred")) && !ui.type) { + ui.type = "cred"; + ui.opts = {}; + } else if (!ui.type) { ui.type = "input"; - ui.opts = {types:['str','num','bool','json','bin','env']} + ui.opts = {types:DEFAULT_ENV_TYPE_LIST} } else { if (!ui.opts) { ui.opts = (ui.type === "select") ? {opts:[]} : {}; @@ -1467,6 +1574,24 @@ RED.subflow = (function() { input.spinner(spinnerOpts).parent().width('70%'); input.val(val.value); break; + case "cred": + input = $('').css('width','70%').appendTo(row); + if (node.credentials) { + if (node.credentials[tenv.name]) { + input.val(node.credentials[tenv.name]); + } else if (node.credentials['has_'+tenv.name]) { + input.val("__PWRD__") + } else { + input.val(""); + } + } else { + input.val(""); + } + input.typedInput({ + types: ['cred'], + default: 'cred' + }) + break; } if (input) { input.attr('id',getSubflowEnvPropertyName(tenv.name)) @@ -1478,7 +1603,7 @@ RED.subflow = (function() { * @param uiContainer - container for UI * @param envList - env var definitions of template */ - function buildEnvUI(uiContainer, envList) { + function buildEnvUI(uiContainer, envList,node) { uiContainer.empty(); var elementID = 0; for (var i = 0; i < envList.length; i++) { @@ -1487,7 +1612,7 @@ RED.subflow = (function() { continue; } var row = $("
      ", { class: "form-row" }).appendTo(uiContainer); - buildEnvUIRow(row,tenv, tenv.ui || {}); + buildEnvUIRow(row,tenv, tenv.ui || {}, node); // console.log(ui); } @@ -1525,7 +1650,7 @@ RED.subflow = (function() { // icon: "", // label: {}, // type: "input", - // opts: {types:['str','num','bool','json','bin','env']} + // opts: {types:DEFAULT_ENV_TYPE_LIST} // } if (!ui.icon) { delete ui.icon; @@ -1535,13 +1660,19 @@ RED.subflow = (function() { } switch (ui.type) { case "input": - if (JSON.stringify(ui.opts) === JSON.stringify({types:['str','num','bool','json','bin','env']})) { + if (JSON.stringify(ui.opts) === JSON.stringify({types:DEFAULT_ENV_TYPE_LIST})) { // This is the default input config. Delete it as it will // be applied automatically delete ui.type; delete ui.opts; } break; + case "cred": + if (envItem.type === "cred") { + delete ui.type; + } + delete ui.opts; + break; case "select": if (ui.opts && $.isEmptyObject(ui.opts.opts)) { // This is the default select config. @@ -1585,7 +1716,7 @@ RED.subflow = (function() { type: env.type, value: env.value }, - ui: env.ui + ui: $.extend(true,{},env.ui) } envList.push(item); parentEnv[env.name] = item; @@ -1621,13 +1752,17 @@ RED.subflow = (function() { var item; var ui = data.ui || {}; if (!ui.type) { - ui.type = "input"; - ui.opts = {types:['str','num','bool','json','bin','env']} + if (data.parent && data.parent.type === "cred") { + ui.type = "cred"; + } else { + ui.type = "input"; + ui.opts = {types:DEFAULT_ENV_TYPE_LIST} + } } else { ui.opts = ui.opts || {}; } var input = $("#"+getSubflowEnvPropertyName(data.name)); - if (input.length) { + if (input.length || ui.type === "cred") { item = { name: data.name }; switch(ui.type) { case "input": @@ -1639,6 +1774,10 @@ RED.subflow = (function() { item.type = 'str'; } break; + case "cred": + item.value = input.val(); + item.type = 'cred'; + break; case "spinner": item.value = input.val(); item.type = 'num'; @@ -1652,7 +1791,7 @@ RED.subflow = (function() { item.value = ""+input.prop("checked"); break; } - if (item.type !== data.parent.type || item.value !== data.parent.value) { + if (ui.type === "cred" || item.type !== data.parent.type || item.value !== data.parent.value) { env.push(item); } } @@ -1702,14 +1841,17 @@ RED.subflow = (function() { return defaultLabel; } - function buildEditForm(container,type,node) { + function buildEditForm(type,node) { if (type === "subflow-template") { buildPropertiesList($('#node-input-env-container'), node); } else if (type === "subflow") { - buildEnvUI($("#subflow-input-ui"), getSubflowInstanceParentEnv(node)); + // This gets called by the subflow type `oneditprepare` function + // registered in nodes.js#addSubflow() + buildEnvUI($("#subflow-input-ui"), getSubflowInstanceParentEnv(node), node); } } - function buildPropertiesForm(container, node) { + function buildPropertiesForm(node) { + var container = $('#editor-subflow-envProperties-content'); var form = $('
      ').appendTo(container); var listContainer = $('
      ').appendTo(form); var list = $('
        ').appendTo(listContainer); diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/tab-context.js b/packages/node_modules/@node-red/editor-client/src/js/ui/tab-context.js index 3bc41b982..9994d5000 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/tab-context.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/tab-context.js @@ -231,7 +231,8 @@ RED.sidebar.context = (function() { RED.utils.createObjectElement(RED.utils.decodeObject(payload,format), { typeHint: data.format, sourceId: id+"."+k, - tools: tools + tools: tools, + path: "" }).appendTo(propRow.children()[1]); } }) @@ -275,7 +276,8 @@ RED.sidebar.context = (function() { RED.utils.createObjectElement(RED.utils.decodeObject(payload,format), { typeHint: data.format, sourceId: id+"."+k, - tools: tools + tools: tools, + path: "" }).appendTo(propRow.children()[1]); } }); @@ -295,7 +297,8 @@ RED.sidebar.context = (function() { RED.utils.createObjectElement(RED.utils.decodeObject(payload,format), { typeHint: v.format, sourceId: id+"."+k, - tools: tools + tools: tools, + path: "" }).appendTo(propRow.children()[1]); if (contextStores.length > 1) { $("",{class:"red-ui-sidebar-context-property-storename"}).text(v.store).appendTo($(propRow.children()[0])) diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/tab-help.js b/packages/node_modules/@node-red/editor-client/src/js/ui/tab-help.js new file mode 100644 index 000000000..4c469425b --- /dev/null +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/tab-help.js @@ -0,0 +1,332 @@ +/** + * Copyright JS Foundation and other contributors, http://js.foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + **/ +RED.sidebar.help = (function() { + + var content; + var toolbar; + var helpSection; + var panels; + var panelRatio; + var helpTopics = []; + var treeList; + var tocPanel; + var helpIndex = {}; + + + function resizeStack() { + var h = $(content).parent().height() - toolbar.outerHeight(); + panels.resize(h) + } + + function init() { + + content = document.createElement("div"); + content.className = "red-ui-sidebar-info" + + toolbar = $("
        ", {class:"red-ui-sidebar-header red-ui-info-toolbar"}).appendTo(content); + $('').appendTo(toolbar) + var showTOCButton = toolbar.find('#red-ui-sidebar-help-show-toc') + RED.popover.tooltip(showTOCButton,RED._("sidebar.help.showTopics")); + showTOCButton.on("click",function(e) { + e.preventDefault(); + if ($(this).hasClass('selected')) { + hideTOC(); + } else { + showTOC(); + } + }); + + var stackContainer = $("
        ",{class:"red-ui-sidebar-help-stack"}).appendTo(content); + + tocPanel = $("
        ", {class: "red-ui-sidebar-help-toc"}).appendTo(stackContainer); + var helpPanel = $("
        ").css({ + "overflow-y": "scroll" + }).appendTo(stackContainer); + + panels = RED.panels.create({ + container: stackContainer + }) + panels.ratio(0.3); + + helpSearch = $('').appendTo(toolbar).searchBox({ + delay: 100, + change: function() { + var val = $(this).val().toLowerCase(); + if (val) { + showTOC(); + var c = treeList.treeList('filter',function(item) { + if (item.depth === 0) { + return true; + } + return (item.nodeType && item.nodeType.indexOf(val) > -1) || + (item.subflowLabel && item.subflowLabel.indexOf(val) > -1) + },true) + } else { + treeList.treeList('filter',null); + var selected = treeList.treeList('selected'); + if (selected.id) { + treeList.treeList('show',selected.id); + } + + } + } + }) + + helpSection = $("
        ",{class:"red-ui-help"}).css({ + "padding":"6px", + }).appendTo(helpPanel) + + $(''+RED._("sidebar.help.noHelp")+'').appendTo(helpSection); + + treeList = $("
        ").css({width: "100%"}).appendTo(tocPanel).treeList({data: []}) + treeList.on('treelistselect', function(e,item) { + if (item.nodeType) { + showHelp(item.nodeType); + } + }) + + RED.sidebar.addTab({ + id: "help", + label: RED._("sidebar.help.label"), + name: RED._("sidebar.help.name"), + iconClass: "fa fa-book", + action:"core:show-help-tab", + content: content, + pinned: true, + enableOnEdit: true, + onchange: function() { + resizeStack() + } + }); + + $(window).on("resize", resizeStack); + $(window).on("focus", resizeStack); + + RED.events.on('registry:node-type-added', queueRefresh); + RED.events.on('registry:node-type-removed', queueRefresh); + RED.events.on('subflows:change', refreshSubflow); + + RED.actions.add("core:show-help-tab",show); + + } + + var refreshTimer; + function queueRefresh() { + if (!refreshTimer) { + refreshTimer = setTimeout(function() { + refreshTimer = null; + refreshHelpIndex(); + },500); + } + } + + function refreshSubflow(sf) { + var item = treeList.treeList('get',"node-type:subflow:"+sf.id); + item.subflowLabel = sf._def.label().toLowerCase(); + item.treeList.replaceElement(getNodeLabel({_def:sf._def,type:sf._def.label()})); + } + + function hideTOC() { + var tocButton = $('#red-ui-sidebar-help-show-toc') + if (tocButton.hasClass('selected')) { + tocButton.removeClass('selected'); + panelRatio = panels.ratio(); + tocPanel.css({"transition":"height 0.2s"}) + panels.ratio(0) + setTimeout(function() { + tocPanel.css({"transition":""}) + },250); + } + } + function showTOC() { + var tocButton = $('#red-ui-sidebar-help-show-toc') + if (!tocButton.hasClass('selected')) { + tocButton.addClass('selected'); + tocPanel.css({"transition":"height 0.2s"}) + panels.ratio(Math.max(0.3,Math.min(panelRatio,0.7))); + setTimeout(function() { + tocPanel.css({"transition":""}) + var selected = treeList.treeList('selected'); + if (selected.id) { + treeList.treeList('show',selected); + } + },250); + } + } + + function refreshHelpIndex() { + helpTopics = []; + var modules = RED.nodes.registry.getModuleList(); + var moduleNames = Object.keys(modules); + moduleNames.sort(); + + var helpData = [{ + label: RED._("sidebar.help.nodeHelp"), + children: [], + expanded: true + }] + + var subflows = RED.nodes.registry.getNodeTypes().filter(function(t) {return /subflow/.test(t)}); + if (subflows.length > 0) { + helpData[0].children.push({ + label: RED._("menu.label.subflows"), + children: [] + }) + subflows.forEach(function(nodeType) { + var sf = RED.nodes.getType(nodeType); + helpData[0].children[0].children.push({ + id:"node-type:"+nodeType, + nodeType: nodeType, + subflowLabel: sf.label().toLowerCase(), + element: getNodeLabel({_def:sf,type:sf.label()}) + }) + }) + } + + + moduleNames.forEach(function(moduleName) { + var module = modules[moduleName]; + var nodeTypes = []; + + var setNames = Object.keys(module.sets); + setNames.forEach(function(setName) { + module.sets[setName].types.forEach(function(nodeType) { + if ($("script[data-help-name='"+nodeType+"']").length) { + nodeTypes.push({ + id: "node-type:"+nodeType, + nodeType: nodeType, + element:getNodeLabel({_def:RED.nodes.getType(nodeType),type:nodeType}) + }) + } + }) + }) + if (nodeTypes.length > 0) { + nodeTypes.sort(function(A,B) { + return A.nodeType.localeCompare(B.nodeType) + }) + helpData[0].children.push({ + id: moduleName, + icon: "fa fa-cube", + label: moduleName, + children: nodeTypes + }) + } + }); + treeList.treeList("data",helpData); + } + + function getNodeLabel(n) { + var div = $('
        ',{class:"red-ui-info-outline-item"}); + RED.utils.createNodeIcon(n).appendTo(div); + var contentDiv = $('
        ',{class:"red-ui-search-result-description"}).appendTo(div); + $('
        ',{class:"red-ui-search-result-node-label red-ui-info-outline-item-label"}).text(n.name||n.type).appendTo(contentDiv); + return div; + } + + function showHelp(nodeType) { + helpSection.empty(); + var helpText; + var title; + var m = /^subflow(:(.+))?$/.exec(nodeType); + if (m && m[2]) { + var subflowNode = RED.nodes.subflow(m[2]); + helpText = (RED.utils.renderMarkdown(subflowNode.info||"")||(''+RED._("sidebar.info.none")+'')); + title = subflowNode.name || nodeType; + } else { + helpText = $("script[data-help-name='"+nodeType+"']").html()||(''+RED._("sidebar.info.none")+''); + title = nodeType; + } + setInfoText(title, helpText, helpSection); + + var ratio = panels.ratio(); + if (ratio > 0.7) { + panels.ratio(0.7) + } + treeList.treeList("show","node-type:"+nodeType) + treeList.treeList("select","node-type:"+nodeType, false); + + } + + function show(type) { + RED.sidebar.show("help"); + if (type) { + hideTOC(); + showHelp(type); + } + } + + // TODO: DRY - projects.js + function addTargetToExternalLinks(el) { + $(el).find("a").each(function(el) { + var href = $(this).attr('href'); + if (/^https?:/.test(href)) { + $(this).attr('target','_blank'); + } + }); + return el; + } + + function setInfoText(title, infoText,target) { + if (title) { + $("

        ",{class:"red-ui-help-title"}).text(title).appendTo(target); + } + var info = addTargetToExternalLinks($('
        '+infoText+'
        ')).appendTo(target); + info.find(".red-ui-text-bidi-aware").contents().filter(function() { return this.nodeType === 3 && this.textContent.trim() !== "" }).wrap( "" ); + var foldingHeader = "H3"; + info.find(foldingHeader).wrapInner('') + .find("a").prepend('').on("click", function(e) { + e.preventDefault(); + var isExpanded = $(this).hasClass('expanded'); + var el = $(this).parent().next(); + while(el.length === 1 && el[0].nodeName !== foldingHeader) { + el.toggle(!isExpanded); + el = el.next(); + } + $(this).toggleClass('expanded',!isExpanded); + }) + target.parent().scrollTop(0); + } + + function set(html,title) { + $(helpSection).empty(); + setInfoText(title,html,helpSection); + hideTOC(); + show(); + } + + function refreshSelection(selection) { + if (selection === undefined) { + selection = RED.view.selection(); + } + if (selection.nodes) { + if (selection.nodes.length == 1) { + var node = selection.nodes[0]; + if (node.type === "subflow" && node.direction) { + // ignore subflow virtual ports + } else if (node.type !== 'group'){ + showHelp(node.type); + } + } + } + } + RED.events.on("view:selection-changed",refreshSelection); + + return { + init: init, + show: show, + set: set + } +})(); diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/tab-info-outliner.js b/packages/node_modules/@node-red/editor-client/src/js/ui/tab-info-outliner.js new file mode 100644 index 000000000..ccecfd043 --- /dev/null +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/tab-info-outliner.js @@ -0,0 +1,475 @@ +RED.sidebar.info.outliner = (function() { + + var treeList; + var searchInput; + var projectInfo; + var projectInfoLabel; + var flowList; + var subflowList; + var globalConfigNodes; + + var objects = {}; + var missingParents = {}; + + function getFlowData() { + var flowData = [ + { + label: RED._("menu.label.flows"), + expanded: true, + children: [] + }, + { + label: RED._("menu.label.subflows"), + children: [] + }, + { + id: "__global__", + label: RED._("sidebar.info.globalConfig"), + children: [] + } + ] + flowList = flowData[0]; + subflowList = flowData[1]; + globalConfigNodes = flowData[2]; + + return flowData; + } + + function getProjectLabel(p) { + var div = $('
        ',{class:"red-ui-info-outline-item red-ui-info-outline-item-flow"}); + div.css("width", "calc(100% - 40px)"); + var contentDiv = $('
        ',{class:"red-ui-search-result-description red-ui-info-outline-item-label"}).appendTo(div); + contentDiv.text(p.name); + var controls = $('
        ',{class:"red-ui-info-outline-item-controls"}).appendTo(div); + var editProjectButton = $('') + .appendTo(controls) + .on("click", function(evt) { + evt.preventDefault(); + RED.projects.editProject(); + }); + RED.popover.tooltip(editProjectButton,RED._('sidebar.project.showProjectSettings')); + return div; + } + + var empties = {}; + function getEmptyItem(id) { + var item = { + empty: true, + element: $('
        ').text(RED._("sidebar.info.empty")), + } + empties[id] = item; + 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: "+type+".label",err); + } + } + var newlineIndex = label.indexOf("\\n"); + if (newlineIndex > -1) { + label = label.substring(0,newlineIndex)+"..."; + } + return label; + } + + function getNodeLabel(n) { + var div = $('
        ',{class:"red-ui-info-outline-item"}); + RED.utils.createNodeIcon(n).appendTo(div); + var contentDiv = $('
        ',{class:"red-ui-search-result-description"}).appendTo(div); + var labelText = getNodeLabelText(n); + var label = $('
        ',{class:"red-ui-search-result-node-label red-ui-info-outline-item-label"}).appendTo(contentDiv); + if (labelText) { + label.text(labelText) + } else { + label.html(" ") + } + + addControls(n, div); + + return div; + } + + function getFlowLabel(n) { + var div = $('
        ',{class:"red-ui-info-outline-item red-ui-info-outline-item-flow"}); + var contentDiv = $('
        ',{class:"red-ui-search-result-description red-ui-info-outline-item-label"}).appendTo(div); + var label = (typeof n === "string")? n : n.label; + var newlineIndex = label.indexOf("\\n"); + if (newlineIndex > -1) { + label = label.substring(0,newlineIndex)+"..."; + } + contentDiv.text(label); + addControls(n, div); + return div; + } + + function getSubflowLabel(n) { + + var div = $('
        ',{class:"red-ui-info-outline-item"}); + RED.utils.createNodeIcon(n).appendTo(div); + var contentDiv = $('
        ',{class:"red-ui-search-result-description"}).appendTo(div); + var labelText = getNodeLabelText(n); + var label = $('
        ',{class:"red-ui-search-result-node-label red-ui-info-outline-item-label"}).appendTo(contentDiv); + if (labelText) { + label.text(labelText) + } else { + label.html(" ") + } + + addControls(n, div); + + return div; + + + // var div = $('
        ',{class:"red-ui-info-outline-item red-ui-info-outline-item-flow"}); + // var contentDiv = $('
        ',{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 = $('
        ',{class:"red-ui-info-outline-item-controls red-ui-info-outline-item-hover-controls"}).appendTo(div); + if (n._def.button) { + $('').appendTo(controls).on("click",function(evt) { + evt.preventDefault(); + evt.stopPropagation(); + RED.view.clickNodeButton(n); + }) + } + // $('').appendTo(controls).on("click",function(evt) { + // evt.preventDefault(); + // evt.stopPropagation(); + // RED.view.reveal(n.id); + // }) + if (n.type !== 'group' && n.type !== 'subflow') { + $('').appendTo(controls).on("click",function(evt) { + evt.preventDefault(); + evt.stopPropagation(); + if (n.type === 'tab') { + if (n.disabled) { + RED.workspaces.enable(n.id) + } else { + RED.workspaces.disable(n.id) + } + } else { + // TODO: this ought to be a utility function in RED.nodes + var historyEvent = { + t: "edit", + node: n, + changed: n.changed, + changes: { + d: n.d + }, + dirty:RED.nodes.dirty() + } + if (n.d) { + delete n.d; + } else { + n.d = true; + } + n.dirty = true; + n.changed = true; + RED.events.emit("nodes:change",n); + RED.nodes.dirty(true) + RED.view.redraw(); + } + }); + } else { + $('
        ').appendTo(controls) + } + controls.find("button").on("dblclick", function(evt) { + evt.preventDefault(); + evt.stopPropagation(); + }) + } + + function onProjectLoad(activeProject) { + objects = {}; + var newFlowData = getFlowData(); + projectInfoLabel.empty(); + getProjectLabel(activeProject).appendTo(projectInfoLabel); + projectInfo.show(); + treeList.treeList('data',newFlowData); + } + + function build() { + var container = $("
        ", {class:"red-ui-info-outline"}).css({'height': '100%'}); + var toolbar = $("
        ", {class:"red-ui-sidebar-header red-ui-info-toolbar"}).appendTo(container); + + searchInput = $('').appendTo(toolbar).searchBox({ + delay: 300, + change: function() { + var val = $(this).val(); + var searchResults = RED.search.search(val); + if (val) { + var resultMap = {}; + for (var i=0,l=searchResults.length;i
        ').hide().appendTo(container) + projectInfoLabel = $('').appendTo(projectInfo); + + //
        Space Monkey
        ').appendTo(container) + + treeList = $("
        ").css({width: "100%"}).appendTo(container).treeList({ + data:getFlowData() + }) + treeList.on('treelistselect', function(e,item) { + var node = RED.nodes.node(item.id) || RED.nodes.group(item.id); + if (node) { + if (node.type === 'group' || node._def.category !== "config") { + RED.view.select({nodes:[node]}) + } else { + RED.view.select({nodes:[]}) + } + } + }) + treeList.on('treelistconfirm', function(e,item) { + var node = RED.nodes.node(item.id); + if (node) { + if (node._def.category === "config") { + RED.editor.editConfig("", node.type, node.id); + } else { + RED.editor.edit(node); + } + } + }) + + RED.events.on("projects:load", onProjectLoad) + + RED.events.on("flows:add", onFlowAdd) + RED.events.on("flows:remove", onObjectRemove) + RED.events.on("flows:change", onFlowChange) + RED.events.on("flows:reorder", onFlowsReorder) + + RED.events.on("subflows:add", onSubflowAdd) + RED.events.on("subflows:remove", onObjectRemove) + RED.events.on("subflows:change", onSubflowChange) + + RED.events.on("nodes:add",onNodeAdd); + RED.events.on("nodes:remove",onObjectRemove); + RED.events.on("nodes:change",onNodeChange); + + RED.events.on("groups:add",onNodeAdd); + RED.events.on("groups:remove",onObjectRemove); + RED.events.on("groups:change",onNodeChange); + + RED.events.on("view:selection-changed", onSelectionChanged); + + RED.events.on("workspace:clear", onWorkspaceClear) + + return container; + } + function onWorkspaceClear() { + treeList.treeList('data',getFlowData()); + } + function onFlowAdd(ws) { + objects[ws.id] = { + id: ws.id, + element: getFlowLabel(ws), + children:[], + deferBuild: true, + icon: "red-ui-icons red-ui-icons-flow", + gutter: getGutter(ws) + } + if (missingParents[ws.id]) { + objects[ws.id].children = missingParents[ws.id]; + delete missingParents[ws.id] + } else { + objects[ws.id].children.push(getEmptyItem(ws.id)); + } + flowList.treeList.addChild(objects[ws.id]) + objects[ws.id].element.toggleClass("red-ui-info-outline-item-disabled", !!ws.disabled) + objects[ws.id].treeList.container.toggleClass("red-ui-info-outline-item-disabled", !!ws.disabled) + + + } + function onFlowChange(n) { + var existingObject = objects[n.id]; + + var label = n.label || n.id; + var newlineIndex = label.indexOf("\\n"); + if (newlineIndex > -1) { + label = label.substring(0,newlineIndex)+"..."; + } + existingObject.element.find(".red-ui-info-outline-item-label").text(label); + existingObject.element.toggleClass("red-ui-info-outline-item-disabled", !!n.disabled) + existingObject.treeList.container.toggleClass("red-ui-info-outline-item-disabled", !!n.disabled) + } + function onFlowsReorder(order) { + var indexMap = {}; + order.forEach(function(id,index) { + indexMap[id] = index; + }) + + flowList.treeList.sortChildren(function(A,B) { + if (A.id === "__global__") { return -1 } + if (B.id === "__global__") { return 1 } + return indexMap[A.id] - indexMap[B.id] + }) + } + function onSubflowAdd(sf) { + objects[sf.id] = { + id: sf.id, + element: getNodeLabel(sf), + children:[], + deferBuild: true, + gutter: getGutter(sf) + } + if (missingParents[sf.id]) { + objects[sf.id].children = missingParents[sf.id]; + delete missingParents[sf.id] + } else { + objects[sf.id].children.push(getEmptyItem(sf.id)); + } + subflowList.treeList.addChild(objects[sf.id]) + } + function onSubflowChange(sf) { + var existingObject = objects[sf.id]; + existingObject.treeList.replaceElement(getNodeLabel(sf)); + // existingObject.element.find(".red-ui-info-outline-item-label").text(n.name || n.id); + RED.nodes.eachNode(function(n) { + if (n.type == "subflow:"+sf.id) { + var sfInstance = objects[n.id]; + sfInstance.treeList.replaceElement(getNodeLabel(n)); + } + }); + } + + function onNodeChange(n) { + var existingObject = objects[n.id]; + var parent = n.g||n.z; + + var nodeLabelText = getNodeLabelText(n); + if (nodeLabelText) { + existingObject.element.find(".red-ui-info-outline-item-label").text(nodeLabelText); + } else { + existingObject.element.find(".red-ui-info-outline-item-label").html(" "); + } + + if (parent !== existingObject.parent.id) { + existingObject.treeList.remove(); + if (!parent) { + globalConfigNodes.treeList.addChild(existingObject); + } else { + if (empties[parent]) { + empties[parent].treeList.remove(); + delete empties[parent]; + } + objects[parent].treeList.addChild(existingObject) + } + } + existingObject.element.toggleClass("red-ui-info-outline-item-disabled", !!n.d) + } + function onObjectRemove(n) { + var existingObject = objects[n.id]; + existingObject.treeList.remove(); + delete objects[n.id] + var parent = existingObject.parent; + if (parent.children.length === 0) { + parent.treeList.addChild(getEmptyItem(parent.id)); + } + if (existingObject.children && (existingObject.children.length > 0)) { + existingObject.children.forEach(function (nc) { + if (!nc.empty) { + var childObject = objects[nc.id]; + nc.parent = parent; + objects[parent.id].treeList.addChild(childObject); + } + }); + } + } + function getGutter(n) { + var span = $("",{class:"red-ui-info-outline-gutter"}); + $('').appendTo(span).on("click",function(evt) { + evt.preventDefault(); + evt.stopPropagation(); + RED.view.reveal(n.id); + }) + return span; + } + function onNodeAdd(n) { + objects[n.id] = { + id: n.id, + element: getNodeLabel(n), + gutter: getGutter(n) + } + if (n.type === "group") { + objects[n.id].children = []; + objects[n.id].deferBuild = true; + if (missingParents[n.id]) { + objects[n.id].children = missingParents[n.id]; + delete missingParents[n.id] + } + } + var parent = n.g||n.z; + if (parent) { + if (objects[parent]) { + if (empties[parent]) { + empties[parent].treeList.remove(); + delete empties[parent]; + } + if (objects[parent].treeList) { + objects[parent].treeList.addChild(objects[n.id]); + } else { + objects[parent].children.push(objects[n.id]) + } + } else { + missingParents[parent] = missingParents[parent]||[]; + missingParents[parent].push(objects[n.id]) + } + } else { + // No parent - add to Global flow list + globalConfigNodes.treeList.addChild(objects[n.id]) + } + objects[n.id].element.toggleClass("red-ui-info-outline-item-disabled", !!n.d) + } + + function onSelectionChanged(selection) { + // treeList.treeList('clearSelection'); + } + + return { + build: build, + search: function(val) { + searchInput.searchBox('value',val) + }, + select: function(node) { + if (node) { + if (Array.isArray(node)) { + treeList.treeList('select', node.map(function(n) { return objects[n.id] }), false) + } else { + treeList.treeList('select', objects[node.id], false) + + } + } else { + treeList.treeList('clearSelection') + } + }, + reveal: function(node) { + treeList.treeList('show', objects[node.id]) + } + } +})(); diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/tab-info.js b/packages/node_modules/@node-red/editor-client/src/js/ui/tab-info.js index b8e3b150b..418939a72 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/tab-info.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/tab-info.js @@ -15,28 +15,33 @@ **/ RED.sidebar.info = (function() { - marked.setOptions({ - renderer: new marked.Renderer(), - gfm: true, - tables: true, - breaks: false, - pedantic: false, - sanitize: true, - smartLists: true, - smartypants: false - }); - var content; - var sections; - var propertiesSection; + var panels; var infoSection; - var helpSection; + + var propertiesPanelContent; + var propertiesPanelHeader; + var propertiesPanelHeaderIcon; + var propertiesPanelHeaderLabel; + var propertiesPanelHeaderReveal; + var propertiesPanelHeaderHelp; + + var selectedObject; + + var tipContainer; var tipBox; + // TODO: remove this var expandedSections = { "property": false }; + function resizeStack() { + if (panels) { + var h = $(content).parent().height() - tipContainer.outerHeight(); + panels.resize(h) + } + } function init() { content = document.createElement("div"); @@ -46,31 +51,79 @@ RED.sidebar.info = (function() { var stackContainer = $("
        ",{class:"red-ui-sidebar-info-stack"}).appendTo(content); - sections = RED.stack.create({ - container: stackContainer - }).hide(); + var outlinerPanel = $("
        ").css({ + "overflow": "hidden", + "height": "calc(70%)" + }).appendTo(stackContainer); + var propertiesPanel = $("
        ").css({ + "overflow":"hidden", + "height":"100%", + "display": "flex", + "flex-direction": "column" + }).appendTo(stackContainer); + propertiesPanelHeader = $("
        ", {class:"red-ui-palette-header red-ui-info-header"}).css({ + "flex":"0 0 auto" + }).appendTo(propertiesPanel); - propertiesSection = sections.add({ - title: RED._("sidebar.info.info"), - collapsible: true + propertiesPanelHeaderIcon = $("").appendTo(propertiesPanelHeader); + propertiesPanelHeaderLabel = $("").appendTo(propertiesPanelHeader); + propertiesPanelHeaderHelp = $('').css({ + position: 'absolute', + top: '12px', + right: '32px' + }).on("click", function(evt) { + evt.preventDefault(); + evt.stopPropagation(); + if (selectedObject) { + RED.sidebar.help.show(selectedObject.type); + } + }).appendTo(propertiesPanelHeader); + RED.popover.tooltip(propertiesPanelHeaderHelp,RED._("sidebar.help.showHelp")); + + + propertiesPanelHeaderReveal = $('').css({ + position: 'absolute', + top: '12px', + right: '8px' + }).on("click", function(evt) { + evt.preventDefault(); + evt.stopPropagation(); + if (selectedObject) { + RED.sidebar.info.outliner.reveal(selectedObject); + RED.view.reveal(selectedObject.id); + } + }).appendTo(propertiesPanelHeader); + RED.popover.tooltip(propertiesPanelHeaderReveal,RED._("sidebar.help.showInOutline")); + + + propertiesPanelContent = $("
        ").css({ + "flex":"1 1 auto", + "overflow-y":"scroll", + }).appendTo(propertiesPanel); + + + panels = RED.panels.create({container: stackContainer}) + panels.ratio(0.6); + RED.sidebar.info.outliner.build().appendTo(outlinerPanel); + + + RED.sidebar.addTab({ + id: "info", + label: RED._("sidebar.info.label"), + name: RED._("sidebar.info.name"), + iconClass: "fa fa-info", + action:"core:show-info-tab", + content: content, + pinned: true, + enableOnEdit: true }); - propertiesSection.expand(); - infoSection = sections.add({ - title: RED._("sidebar.info.desc"), - collapsible: true - }); - infoSection.expand(); - infoSection.content.css("padding","6px"); + $(window).on("resize", resizeStack); + $(window).on("focus", resizeStack); - helpSection = sections.add({ - title: RED._("sidebar.info.nodeHelp"), - collapsible: true - }); - helpSection.expand(); - helpSection.content.css("padding","6px"); - var tipContainer = $('
        ').appendTo(content); + // Tip Box + tipContainer = $('
        ').appendTo(content); tipBox = $('
        ').appendTo(tipContainer); var tipButtons = $('
        ').appendTo(tipContainer); @@ -86,17 +139,6 @@ RED.sidebar.info = (function() { RED.actions.invoke("core:toggle-show-tips"); RED.notify(RED._("sidebar.info.showTips")); }); - - RED.sidebar.addTab({ - id: "info", - label: RED._("sidebar.info.label"), - name: RED._("sidebar.info.name"), - iconClass: "fa fa-info", - action:"core:show-info-tab", - content: content, - pinned: true, - enableOnEdit: true - }); if (tips.enabled()) { tips.start(); } else { @@ -124,46 +166,36 @@ RED.sidebar.info = (function() { refreshSelection(); return; } - sections.show(); - $(propertiesSection.content).empty(); - $(infoSection.content).empty(); - $(helpSection.content).empty(); - infoSection.title.text(RED._("sidebar.info.desc")); + $(propertiesPanelContent).empty(); var propRow; - var table = $('
        ').appendTo(propertiesSection.content); + var table = $('
        ').appendTo(propertiesPanelContent); var tableBody = $('').appendTo(table); var subflowNode; var subflowUserCount; - var activeProject = RED.projects.getActiveProject(); - if (activeProject) { - propRow = $(''+ RED._("sidebar.project.name") + '').appendTo(tableBody); - $(propRow.children()[1]).text(activeProject.name||""); - $('').appendTo(tableBody); - var editProjectButton = $('') - .appendTo(propRow.children()[1]) - .on("click", function(evt) { - evt.preventDefault(); - RED.projects.editProject(); - }); - RED.popover.tooltip(editProjectButton,RED._('sidebar.project.showProjectSettings')); - } - propertiesSection.container.show(); - infoSection.container.show(); - helpSection.container.show(); if (node === null) { + RED.sidebar.info.outliner.select(null); return; } else if (Array.isArray(node)) { // Multiple things selected // - hide help and info sections + RED.sidebar.info.outliner.select(node); + + propertiesPanelHeaderIcon.empty(); + RED.utils.createNodeIcon({type:"_selection_"}).appendTo(propertiesPanelHeaderIcon); + propertiesPanelHeaderLabel.text("Selection"); + propertiesPanelHeaderReveal.hide(); + propertiesPanelHeaderHelp.hide(); + selectedObject = null; var types = { nodes:0, flows:0, - subflows:0 + subflows:0, + groups: 0 } node.forEach(function(n) { if (n.type === 'tab') { @@ -171,12 +203,13 @@ RED.sidebar.info = (function() { types.nodes += RED.nodes.filterNodes({z:n.id}).length; } else if (n.type === 'subflow') { types.subflows++; + } else if (n.type === 'group') { + types.groups++; } else { types.nodes++; } }); - helpSection.container.hide(); - infoSection.container.hide(); + // infoSection.container.hide(); // - show the count of selected nodes propRow = $(''+RED._("sidebar.info.selection")+"").appendTo(tableBody); @@ -190,14 +223,19 @@ RED.sidebar.info = (function() { if (types.nodes > 0) { $('
        ').text(RED._("clipboard.node",{count:types.nodes})).appendTo(counts); } + if (types.groups > 0) { + $('
        ').text(RED._("clipboard.group",{count:types.groups})).appendTo(counts); + } } else { // A single 'thing' selected. + RED.sidebar.info.outliner.select(node); + // Check to see if this is a subflow or subflow instance - var m = /^subflow(:(.+))?$/.exec(node.type); - if (m) { - if (m[2]) { - subflowNode = RED.nodes.subflow(m[2]); + var subflowRegex = /^subflow(:(.+))?$/.exec(node.type); + if (subflowRegex) { + if (subflowRegex[2]) { + subflowNode = RED.nodes.subflow(subflowRegex[2]); } else { subflowNode = node; } @@ -210,33 +248,76 @@ RED.sidebar.info = (function() { } }); } + + propertiesPanelHeaderIcon.empty(); + RED.utils.createNodeIcon(node).appendTo(propertiesPanelHeaderIcon); + var objectLabel = RED.utils.getNodeLabel(node, node.type+": "+node.id) + var newlineIndex = objectLabel.indexOf("\\n"); + if (newlineIndex > -1) { + objectLabel = objectLabel.substring(0,newlineIndex)+"..."; + } + propertiesPanelHeaderLabel.text(objectLabel); + propertiesPanelHeaderReveal.show(); + selectedObject = node; + + propRow = $('').appendTo(tableBody); + var objectType = "node"; + if (node.type === "subflow" || subflowRegex) { + objectType = "subflow"; + } else if (node.type === "tab") { + objectType = "flow"; + }else if (node.type === "group") { + objectType = "group"; + } + $(propRow.children()[0]).text(RED._("sidebar.info."+objectType)) + RED.utils.createObjectElement(node.id).appendTo(propRow.children()[1]); + if (node.type === "tab" || node.type === "subflow") { // If nothing is selected, but we're on a flow or subflow tab. - propRow = $(''+RED._("sidebar.info."+(node.type==='tab'?'flow':'subflow'))+'').appendTo(tableBody); - RED.utils.createObjectElement(node.id).appendTo(propRow.children()[1]); - propRow = $(''+RED._("sidebar.info.tabName")+"").appendTo(tableBody); - $(propRow.children()[1]).text(node.label||node.name||""); - if (node.type === "tab") { - propRow = $(''+RED._("sidebar.info.status")+'').appendTo(tableBody); - $(propRow.children()[1]).text((!!!node.disabled)?RED._("sidebar.info.enabled"):RED._("sidebar.info.disabled")) + propertiesPanelHeaderHelp.hide(); + + } else if (node.type === "group") { + propertiesPanelHeaderHelp.hide(); + + propRow = $(' ').appendTo(tableBody); + + var typeCounts = { + nodes:0, + groups: 0 } + var allNodes = RED.group.getNodes(node,true); + allNodes.forEach(function(n) { + if (n.type === "group") { + typeCounts.groups++; + } else { + typeCounts.nodes++ + } + }); + var counts = $('
        ').appendTo($(propRow.children()[1])); + if (typeCounts.nodes > 0) { + $('
        ').text(RED._("clipboard.node",{count:typeCounts.nodes})).appendTo(counts); + } + if (typeCounts.groups > 0) { + $('
        ').text(RED._("clipboard.group",{count:typeCounts.groups})).appendTo(counts); + } + + } else { - // An actual node is selected in the editor - build up its properties table - propRow = $(''+RED._("sidebar.info.node")+"").appendTo(tableBody); - RED.utils.createObjectElement(node.id).appendTo(propRow.children()[1]); - if (node.type !== "subflow" && node.type !== "unknown" && node.name) { - propRow = $(''+RED._("common.label.name")+'').appendTo(tableBody); - $('').text(node.name).appendTo(propRow.children()[1]); - } - if (!m) { - propRow = $(''+RED._("sidebar.info.type")+"").appendTo(tableBody); + propertiesPanelHeaderHelp.show(); + + if (!subflowRegex) { + propRow = $(''+RED._("sidebar.info.type")+'').appendTo(tableBody); $(propRow.children()[1]).text((node.type === "unknown")?node._orig.type:node.type); if (node.type === "unknown") { $('').prependTo($(propRow.children()[1])) } } + var count = 0; - if (!m && node.type != "subflow") { + if (!subflowRegex && node.type != "subflow" && node.type != "group") { + + var blankRow = $('').appendTo(tableBody); + var defaults; if (node.type === 'unknown') { defaults = {}; @@ -251,7 +332,6 @@ RED.sidebar.info = (function() { $(propRow.children()[1]).text(RED.nodes.getType(node.type).set.module); count++; } - $('').appendTo(tableBody); if (defaults) { for (var n in defaults) { @@ -259,7 +339,8 @@ RED.sidebar.info = (function() { var val = node[n]; var type = typeof val; count++; - propRow = $(''+n+"").appendTo(tableBody); + propRow = $('').appendTo(tableBody); + $(propRow.children()[0]).text(n); if (defaults[n].type) { var configNode = RED.nodes.node(val); if (!configNode) { @@ -289,49 +370,66 @@ RED.sidebar.info = (function() { } } if (count > 0) { - $(''+RED._("sidebar.info.showMore")+''+RED._("sidebar.info.showLess")+' ').appendTo(tableBody); + $(''+RED._("sidebar.info.showMore")+''+RED._("sidebar.info.showLess")+' ').appendTo(blankRow.children()[0]); } } if (node.type !== 'tab') { - if (m) { + if (subflowRegex) { $(''+RED._("sidebar.info.subflow")+'').appendTo(tableBody); $(''+RED._("common.label.name")+''+RED.utils.sanitize(subflowNode.name)+'').appendTo(tableBody); } } } - if (m) { + if (subflowRegex) { propRow = $(''+RED._("subflow.category")+'').appendTo(tableBody); var category = subflowNode.category||"subflows"; $(propRow.children()[1]).text(RED._("palette.label."+category,{defaultValue:category})) $(''+RED._("sidebar.info.instances")+""+subflowUserCount+'').appendTo(tableBody); } - var helpText = ""; - if (node.type === "tab" || node.type === "subflow") { - $(helpSection.container).hide(); - } else { - $(helpSection.container).show(); - if (subflowNode && node.type !== "subflow") { - // Selected a subflow instance node. - // - The subflow template info goes into help - helpText = (marked(subflowNode.info||"")||(''+RED._("sidebar.info.none")+'')); - } else { - helpText = $("script[data-help-name='"+node.type+"']").html()||(''+RED._("sidebar.info.none")+''); - } - setInfoText(helpText, helpSection.content); - } + // 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||"")||(''+RED._("sidebar.info.none")+'')); + // } else { + // helpText = $("script[data-help-name='"+node.type+"']").html()||(''+RED._("sidebar.info.none")+''); + // } + // setInfoText(helpText, helpSection.content); + // } var infoText = ""; if (node._def && node._def.info) { var info = node._def.info; var textInfo = (typeof info === "function" ? info.call(node) : info); - infoText = infoText + marked(textInfo); + infoText = infoText + RED.utils.renderMarkdown(textInfo); } if (node.info) { - infoText = infoText + marked(node.info || "") + infoText = infoText + RED.utils.renderMarkdown(node.info || "") } - setInfoText(infoText, infoSection.content); + var infoSectionContainer = $("
        ").css("padding","0 6px 6px").appendTo(propertiesPanelContent) + + // var editInfo = $('').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); $(".node-info-property-header").on("click", function(e) { @@ -347,7 +445,7 @@ RED.sidebar.info = (function() { // propRow = $('Actions').appendTo(tableBody); // var actionBar = $(propRow.children()[1]); // - // // var actionBar = $('
        ',{style:"background: #fefefe; padding: 3px;"}).appendTo(propertiesSection.content); + // // var actionBar = $('
        ',{style:"background: #fefefe; padding: 3px;"}).appendTo(propertiesPanel); // $('').appendTo(actionBar); // $('').appendTo(actionBar); // $('').appendTo(actionBar); @@ -422,6 +520,7 @@ RED.sidebar.info = (function() { } function startTips() { $(".red-ui-sidebar-info").addClass('show-tips'); + resizeStack(); if (enabled) { if (!startTimeout && !refreshTimeout) { if (tipCount === -1) { @@ -435,6 +534,7 @@ RED.sidebar.info = (function() { } function stopTips() { $(".red-ui-sidebar-info").removeClass('show-tips'); + resizeStack(); clearInterval(refreshTimeout); clearTimeout(startTimeout); refreshTimeout = null; @@ -459,15 +559,8 @@ RED.sidebar.info = (function() { } function set(html,title) { - // tips.stop(); - // sections.show(); - refresh(null); - propertiesSection.container.hide(); - helpSection.container.hide(); - infoSection.container.show(); - infoSection.title.text(title||RED._("sidebar.info.desc")); - setInfoText(html,infoSection.content); - $(".red-ui-sidebar-info-stack").scrollTop(0); + console.warn("Deprecated use of RED.sidebar.info.set - use RED.sidebar.help.set instead") + RED.sidebar.help.set(html,title); } function refreshSelection(selection) { diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/utils.js b/packages/node_modules/@node-red/editor-client/src/js/ui/utils.js index d35d1f1ba..e8306bd0f 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/utils.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/utils.js @@ -16,6 +16,28 @@ RED.utils = (function() { + window._marked = window.marked; + window.marked = function(txt) { + console.warn("Use of 'marked()' is deprecated. Use RED.utils.renderMarkdown() instead"); + return renderMarkdown(txt); + } + + _marked.setOptions({ + renderer: new _marked.Renderer(), + gfm: true, + tables: true, + breaks: false, + pedantic: false, + smartLists: true, + smartypants: false + }); + + function renderMarkdown(txt) { + var rendered = _marked(txt); + var cleaned = DOMPurify.sanitize(rendered, {SAFE_FOR_JQUERY: true}) + return cleaned; + } + function formatString(str) { return str.replace(/\r?\n/g,"↵").replace(/\t/g,"→"); } @@ -784,9 +806,9 @@ RED.utils = (function() { function separateIconPath(icon) { var result = {module: "", file: ""}; if (icon) { - var index = icon.indexOf('icons/'); - if (index !== -1) { - icon = icon.substring(index+6); + var index = icon.indexOf(RED.settings.apiRootUrl+'icons/'); + if (index === 0) { + icon = icon.substring((RED.settings.apiRootUrl+'icons/').length); } index = icon.indexOf('/'); if (index !== -1) { @@ -837,10 +859,15 @@ RED.utils = (function() { } function getNodeIcon(def,node) { - if (def.category === 'config') { + if (node && node.type === '_selection_') { + return "font-awesome/fa-object-ungroup"; + } else if (node && node.type === 'group') { + return "font-awesome/fa-object-group" + } else if (def.category === 'config') { return RED.settings.apiRootUrl+"icons/node-red/cog.svg" } else if (node && node.type === 'tab') { - return RED.settings.apiRootUrl+"icons/node-red/subflow.svg" + return "red-ui-icons/red-ui-icons-flow" + // return RED.settings.apiRootUrl+"images/subflow_tab.svg" } else if (node && node.type === 'unknown') { return RED.settings.apiRootUrl+"icons/node-red/alert.svg" } else if (node && node.icon) { @@ -899,6 +926,8 @@ RED.utils = (function() { var l; if (node.type === 'tab') { l = node.label || defaultLabel + } else if (node.type === 'group') { + l = node.name || defaultLabel } else { l = node._def.label; try { @@ -1032,11 +1061,63 @@ RED.utils = (function() { } // If the specified name is not defined in font-awesome, show arrow-in icon. iconUrl = RED.settings.apiRootUrl+"icons/node-red/arrow-in.svg" + } else if (iconPath.module === "red-ui-icons") { + var redIconElement = $('').appendTo(iconContainer); + redIconElement.addClass("red-ui-palette-icon red-ui-icons " + iconPath.file); + return; } var imageIconElement = $('
        ',{class:"red-ui-palette-icon"}).appendTo(iconContainer); imageIconElement.css("backgroundImage", "url("+iconUrl+")"); } + function createNodeIcon(node) { + var def = node._def; + var nodeDiv = $('
        ',{class:"red-ui-search-result-node"}) + if (node.type === "_selection_") { + nodeDiv.addClass("red-ui-palette-icon-selection"); + } else if (node.type === "group") { + nodeDiv.addClass("red-ui-palette-icon-group"); + } else if (node.type === 'tab') { + nodeDiv.addClass("red-ui-palette-icon-flow"); + } else { + var colour = RED.utils.getNodeColor(node.type,def); + // if (node.type === 'tab') { + // colour = "#C0DEED"; + // } + nodeDiv.css('backgroundColor',colour); + var borderColor = getDarkerColor(colour); + if (borderColor !== colour) { + nodeDiv.css('border-color',borderColor) + } + } + + var icon_url = RED.utils.getNodeIcon(def,node); + var iconContainer = $('
        ',{class:"red-ui-palette-icon-container"}).appendTo(nodeDiv); + RED.utils.createIconElement(icon_url, iconContainer, true); + return nodeDiv; + } + + function getDarkerColor(c) { + var r,g,b; + if (/^#[a-f0-9]{6}$/i.test(c)) { + r = parseInt(c.substring(1, 3), 16); + g = parseInt(c.substring(3, 5), 16); + b = parseInt(c.substring(5, 7), 16); + } else if (/^#[a-f0-9]{3}$/i.test(c)) { + r = parseInt(c.substring(1, 2)+c.substring(1, 2), 16); + g = parseInt(c.substring(2, 3)+c.substring(2, 3), 16); + b = parseInt(c.substring(3, 4)+c.substring(3, 4), 16); + } else { + return c; + } + var l = 0.3 * r/255 + 0.59 * g/255 + 0.11 * b/255 ; + r = Math.max(0,r-50); + g = Math.max(0,g-50); + b = Math.max(0,b-50); + var s = ((r<<16) + (g<<8) + b).toString(16); + return '#'+'000000'.slice(0, 6-s.length)+s; + } + return { createObjectElement: buildMessageElement, getMessageProperty: getMessageProperty, @@ -1053,6 +1134,9 @@ RED.utils = (function() { decodeObject: decodeObject, parseContextKey: parseContextKey, createIconElement: createIconElement, - sanitize: sanitize + sanitize: sanitize, + renderMarkdown: renderMarkdown, + createNodeIcon: createNodeIcon, + getDarkerColor: getDarkerColor } })(); diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/view-tools.js b/packages/node_modules/@node-red/editor-client/src/js/ui/view-tools.js index 0814892be..631e7ec1e 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/view-tools.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/view-tools.js @@ -67,9 +67,16 @@ RED.view.tools = (function() { function moveSelection(dx,dy) { if (moving_set === null) { + moving_set = []; var selection = RED.view.selection(); if (selection.nodes) { - moving_set = selection.nodes.map(function(n) { return {n:n}}); + while (selection.nodes.length > 0) { + var n = selection.nodes.shift(); + moving_set.push({n:n}); + if (n.type === "group") { + selection.nodes = selection.nodes.concat(n.nodes); + } + } } } if (moving_set && moving_set.length > 0) { @@ -93,6 +100,9 @@ RED.view.tools = (function() { node.n.x += dx; node.n.y += dy; node.n.dirty = true; + if (node.n.type === "group") { + RED.group.markDirty(node.n); + } minX = Math.min(node.n.x-node.n.w/2-5,minX); minY = Math.min(node.n.y-node.n.h/2-5,minY); } @@ -105,13 +115,86 @@ RED.view.tools = (function() { } } RED.view.redraw(); + } else { + RED.view.scroll(dx*10,dy*10); } } + function setSelectedNodeLabelState(labelShown) { + var selection = RED.view.selection(); + var historyEvents = []; + var nodes = []; + if (selection.nodes) { + selection.nodes.forEach(function(n) { + if (n.type !== 'subflow' && n.type !== 'group') { + nodes.push(n); + } else if (n.type === 'group') { + nodes = nodes.concat( RED.group.getNodes(n,true)); + } + }); + } + nodes.forEach(function(n) { + var modified = false; + var oldValue = n.l === undefined?true:n.l; + var isLink = /^link (in|out)$/.test(n._def.type); + + if (labelShown) { + if (n.l === false || (isLink && !n.hasOwnProperty('l'))) { + n.l = true; + modified = true; + } + } else { + if ((!isLink && (!n.hasOwnProperty('l') || n.l === true)) || (isLink && n.l === true) ) { + n.l = false; + modified = true; + } + } + if (modified) { + historyEvents.push({ + t: "edit", + node: n, + changed: n.changed, + changes: { + l: oldValue + } + }) + n.changed = true; + n.dirty = true; + n.resize = true; + } + }) + + if (historyEvents.length > 0) { + RED.history.push({ + t: "multi", + events: historyEvents, + dirty: RED.nodes.dirty() + }) + RED.nodes.dirty(true); + } + + RED.view.redraw(); + + + } + return { init: function() { + RED.actions.add("core:show-selected-node-labels", function() { setSelectedNodeLabelState(true); }) + RED.actions.add("core:hide-selected-node-labels", function() { setSelectedNodeLabelState(false); }) + RED.actions.add("core:align-selection-to-grid", alignToGrid); + RED.actions.add("core:scroll-view-up", function() { RED.view.scroll(0,-RED.view.gridSize());}); + RED.actions.add("core:scroll-view-right", function() { RED.view.scroll(RED.view.gridSize(),0);}); + RED.actions.add("core:scroll-view-down", function() { RED.view.scroll(0,RED.view.gridSize());}); + RED.actions.add("core:scroll-view-left", function() { RED.view.scroll(-RED.view.gridSize(),0);}); + + RED.actions.add("core:step-view-up", function() { RED.view.scroll(0,-5*RED.view.gridSize());}); + RED.actions.add("core:step-view-right", function() { RED.view.scroll(5*RED.view.gridSize(),0);}); + RED.actions.add("core:step-view-down", function() { RED.view.scroll(0,5*RED.view.gridSize());}); + RED.actions.add("core:step-view-left", function() { RED.view.scroll(-5*RED.view.gridSize(),0);}); + RED.actions.add("core:move-selection-up", function() { moveSelection(0,-1);}); RED.actions.add("core:move-selection-right", function() { moveSelection(1,0);}); RED.actions.add("core:move-selection-down", function() { moveSelection(0,1);}); diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/view.js b/packages/node_modules/@node-red/editor-client/src/js/ui/view.js index 7a561d9cd..7f541204e 100755 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/view.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/view.js @@ -21,12 +21,15 @@ * \- .red-ui-workspace-chart-event-layer "eventLayer" * |- .red-ui-workspace-chart-background * |- .red-ui-workspace-chart-grid "gridLayer" + * |- "groupLayer" + * |- "groupSelectLayer" * |- "linkLayer" * |- "dragGroupLayer" - * \- "nodeLayer" + * |- "nodeLayer" */ RED.view = (function() { + var DEBUG_EVENTS = false; var space_width = 5000, space_height = 5000, lineCurveScale = 0.75, @@ -48,34 +51,42 @@ RED.view = (function() { var activeSpliceLink; var spliceActive = false; var spliceTimer; + var groupHoverTimer; var activeSubflow = null; var activeNodes = []; var activeLinks = []; var activeFlowLinks = []; var activeLinkNodes = {}; + var activeGroup = null; + var activeHoverGroup = null; + var activeGroups = []; + var dirtyGroups = {}; - var selected_link = null, - mousedown_link = null, - mousedown_node = null, - mousedown_port_type = null, - mousedown_port_index = 0, - mouseup_node = null, - mouse_offset = [0,0], - mouse_position = null, - mouse_mode = 0, - moving_set = [], - lasso = null, - ghostNode = null, - showStatus = false, - lastClickNode = null, - dblClickPrimed = null, - clickTime = 0, - clickElapsed = 0, - scroll_position = [], - quickAddActive = false, - quickAddLink = null, - showAllLinkPorts = -1; + var selected_link = null; + var mousedown_link = null; + var mousedown_node = null; + var mousedown_group = null; + var mousedown_port_type = null; + var mousedown_port_index = 0; + var mouseup_node = null; + var mouse_offset = [0,0]; + var mouse_position = null; + var mouse_mode = 0; + var mousedown_group_handle = null; + var moving_set = []; + var lasso = null; + var ghostNode = null; + var showStatus = false; + var lastClickNode = null; + var dblClickPrimed = null; + var clickTime = 0; + var clickElapsed = 0; + var scroll_position = []; + var quickAddActive = false; + var quickAddLink = null; + var showAllLinkPorts = -1; + var groupNodeSelectPrimed = false; var selectNodesOptions; @@ -100,7 +111,9 @@ RED.view = (function() { var gridLayer; var linkLayer; var dragGroupLayer; + var groupSelectLayer; var nodeLayer; + var groupLayer; var drag_lines; function init() { @@ -253,9 +266,12 @@ RED.view = (function() { gridLayer = eventLayer.append("g").attr("class","red-ui-workspace-chart-grid"); updateGrid(); + groupLayer = eventLayer.append("g"); + groupSelectLayer = eventLayer.append("g"); linkLayer = eventLayer.append("g"); dragGroupLayer = eventLayer.append("g"); nodeLayer = eventLayer.append("g"); + drag_lines = []; RED.events.on("workspace:change",function(event) { @@ -377,14 +393,36 @@ RED.view = (function() { historyEvent.removedLinks = [spliceLink]; } - RED.history.push(historyEvent); RED.nodes.add(nn); + + var group = $(ui.helper).data("group"); + if (group) { + RED.group.addToGroup(group, nn); + historyEvent = { + t: 'multi', + events: [historyEvent], + + } + historyEvent.events.push({ + t: "addToGroup", + group: group, + nodes: nn + }) + } + + RED.history.push(historyEvent); RED.editor.validateNode(nn); RED.nodes.dirty(true); // auto select dropped node - so info shows (if visible) + exitActiveGroup(); clearSelection(); nn.selected = true; moving_set.push({n:nn}); + if (group) { + selectGroup(group,false); + enterActiveGroup(group); + activeGroup = group; + } updateActiveNodes(); updateSelection(); redraw(); @@ -517,6 +555,53 @@ RED.view = (function() { source:{z:activeWorkspace}, target:{z:activeWorkspace} }); + + activeGroups = RED.nodes.groups(activeWorkspace)||[]; + activeGroups.forEach(function(g) { + if (g.g) { + g._root = g.g; + g._depth = 1; + } else { + g._root = g.id; + g._depth = 0; + } + }); + var changed = false; + do { + changed = false; + activeGroups.forEach(function(g) { + if (g.g) { + var parentGroup = RED.nodes.group(g.g); + if (parentGroup) { + var parentDepth = parentGroup._depth; + if (g._depth !== parentDepth + 1) { + g._depth = parentDepth + 1; + changed = true; + } + if (g._root !== parentGroup._root) { + g._root = parentGroup._root; + changed = true; + } + } + } + }); + } while (changed) + activeGroups.sort(function(a,b) { + if (a._root === b._root) { + return a._depth - b._depth; + } else { + return a._root.localeCompare(b._root); + } + }); + + var group = groupLayer.selectAll(".red-ui-flow-group").data(activeGroups,function(d) { return d.id }); + group.sort(function(a,b) { + if (a._root === b._root) { + return a._depth - b._depth; + } else { + return a._root.localeCompare(b._root); + } + }) } function generateLinkPath(origX,origY, destX, destY, sc) { @@ -671,6 +756,7 @@ RED.view = (function() { } function canvasMouseDown() { +if (DEBUG_EVENTS) { console.warn("canvasMouseDown", mouse_mode); } var point; if (mouse_mode === RED.state.SELECTING_NODE) { d3.event.stopPropagation(); @@ -684,7 +770,7 @@ RED.view = (function() { scroll_position = [chart.scrollLeft(),chart.scrollTop()]; return; } - if (!mousedown_node && !mousedown_link) { + if (!mousedown_node && !mousedown_link && !mousedown_group) { selected_link = null; updateSelection(); } @@ -697,7 +783,13 @@ RED.view = (function() { if (mouse_mode === 0 || mouse_mode === RED.state.QUICK_JOINING) { if (d3.event.metaKey || d3.event.ctrlKey) { d3.event.stopPropagation(); - showQuickAddDialog(d3.mouse(this)); + clearSelection(); + point = d3.mouse(this); + var clickedGroup = getGroupAt(point[0],point[1]); + if (drag_lines.length > 0) { + clickedGroup = clickedGroup || RED.nodes.group(drag_lines[0].node.g) + } + showQuickAddDialog(point, null, clickedGroup); } } if (mouse_mode === 0 && !(d3.event.metaKey || d3.event.ctrlKey)) { @@ -718,7 +810,13 @@ RED.view = (function() { } } - function showQuickAddDialog(point,spliceLink) { + function showQuickAddDialog(point, spliceLink, targetGroup) { + if (targetGroup && !targetGroup.active) { + selectGroup(targetGroup,false); + enterActiveGroup(targetGroup); + RED.view.redraw(); + } + var ox = point[0]; var oy = point[1]; @@ -946,6 +1044,22 @@ RED.view = (function() { } } } + if (targetGroup) { + RED.group.addToGroup(targetGroup, nn); + if (historyEvent.t !== "multi") { + historyEvent = { + t:'multi', + events: [historyEvent] + } + } + historyEvent.events.push({ + t: "addToGroup", + group: targetGroup, + nodes: nn + }) + + } + if (spliceLink) { resetMouseVars(); // TODO: DRY - droppable/nodeMouseDown/canvasMouseUp/showQuickAddDialog @@ -972,6 +1086,10 @@ RED.view = (function() { // auto select dropped node - so info shows (if visible) clearSelection(); nn.selected = true; + if (targetGroup) { + selectGroup(targetGroup,false); + enterActiveGroup(targetGroup); + } moving_set.push({n:nn}); updateActiveNodes(); updateSelection(); @@ -1076,11 +1194,23 @@ RED.view = (function() { return; } - if (mouse_mode != RED.state.QUICK_JOINING && mouse_mode != RED.state.IMPORT_DRAGGING && !mousedown_node && selected_link == null) { + if (mouse_mode != RED.state.QUICK_JOINING && mouse_mode != RED.state.IMPORT_DRAGGING && !mousedown_node && !mousedown_group && selected_link == null) { return; } var mousePos; + // if (mouse_mode === RED.state.GROUP_RESIZE) { + // mousePos = mouse_position; + // var nx = mousePos[0] + mousedown_group.dx; + // var ny = mousePos[1] + mousedown_group.dy; + // switch(mousedown_group.activeHandle) { + // case 0: mousedown_group.pos.x0 = nx; mousedown_group.pos.y0 = ny; break; + // case 1: mousedown_group.pos.x1 = nx; mousedown_group.pos.y0 = ny; break; + // case 2: mousedown_group.pos.x1 = nx; mousedown_group.pos.y1 = ny; break; + // case 3: mousedown_group.pos.x0 = nx; mousedown_group.pos.y1 = ny; break; + // } + // mousedown_group.dirty = true; + // } if (mouse_mode == RED.state.JOINING || mouse_mode === RED.state.QUICK_JOINING) { // update drag line if (drag_lines.length === 0 && mousedown_port_type !== null) { @@ -1182,10 +1312,18 @@ RED.view = (function() { node.n.x = mousePos[0]+node.dx; node.n.y = mousePos[1]+node.dy; node.n.dirty = true; - minX = Math.min(node.n.x-node.n.w/2-5,minX); - minY = Math.min(node.n.y-node.n.h/2-5,minY); - maxX = Math.max(node.n.x+node.n.w/2+5,maxX); - maxY = Math.max(node.n.y+node.n.h/2+5,maxY); + if (node.n.type === "group") { + RED.group.markDirty(node.n); + minX = Math.min(node.n.x-5,minX); + minY = Math.min(node.n.y-5,minY); + maxX = Math.max(node.n.x+node.n.w+5,maxX); + maxY = Math.max(node.n.y+node.n.h+5,maxY); + } else { + minX = Math.min(node.n.x-node.n.w/2-5,minX); + minY = Math.min(node.n.y-node.n.h/2-5,minY); + maxX = Math.max(node.n.x+node.n.w/2+5,maxX); + maxY = Math.max(node.n.y+node.n.h/2+5,maxY); + } } if (minX !== 0 || minY !== 0) { for (i = 0; i 0) { - var gridOffset = [0,0]; - node = moving_set[0]; - gridOffset[0] = node.n.x-(gridSize*Math.floor((node.n.x-node.n.w/2)/gridSize)+node.n.w/2); - gridOffset[1] = node.n.y-(gridSize*Math.floor(node.n.y/gridSize)); + var i = 0; + + // Prefer to snap nodes to the grid if there is one in the selection + do { + node = moving_set[i++]; + } while(i x && n.x < x2 && n.y > y && n.y < y2); - if (n.selected) { - n.dirty = true; - moving_set.push({n:n}); + activeGroups.forEach(function(g) { + if (!g.selected) { + if (g.x > x && g.x+g.w < x2 && g.y > y && g.y+g.h < y2) { + while (g.g) { + g = RED.nodes.group(g.g); + } + if (!g.selected) { + selectGroup(g,true); + } + } + } + }) + + activeNodes.forEach(function(n) { + if (!n.selected) { + if (n.x > x && n.x < x2 && n.y > y && n.y < y2) { + if (n.g) { + var group = RED.nodes.group(n.g); + while (group.g) { + group = RED.nodes.group(group.g); + } + if (!group.selected) { + selectGroup(group,true); + } + } else { + n.selected = true; + n.dirty = true; + moving_set.push({n:n}); + } } } }); + + + + // var selectionChanged = false; + // do { + // selectionChanged = false; + // selectedGroups.forEach(function(g) { + // if (g.g && g.selected && RED.nodes.group(g.g).selected) { + // g.selected = false; + // selectionChanged = true; + // } + // }) + // } while(selectionChanged); + if (activeSubflow) { activeSubflow.in.forEach(function(n) { n.selected = (n.x > x && n.x < x2 && n.y > y && n.y < y2); @@ -1355,6 +1568,21 @@ RED.view = (function() { } if (mouse_mode == RED.state.MOVING_ACTIVE) { if (moving_set.length > 0) { + var addedToGroup = null; + if (activeHoverGroup) { + for (var j=0;j 0) { historyEvent = {t:"move",nodes:ns,dirty:RED.nodes.dirty()}; if (activeSpliceLink) { @@ -1386,12 +1615,31 @@ RED.view = (function() { historyEvent.removedLinks = [spliceLink]; updateActiveNodes(); } + if (addedToGroup) { + historyEvent.addToGroup = addedToGroup; + } RED.nodes.dirty(true); RED.history.push(historyEvent); } } } + // if (mouse_mode === RED.state.MOVING && mousedown_node && mousedown_node.g) { + // if (mousedown_node.gSelected) { + // delete mousedown_node.gSelected + // } else { + // if (!d3.event.ctrlKey && !d3.event.metaKey) { + // clearSelection(); + // } + // RED.nodes.group(mousedown_node.g).selected = true; + // mousedown_node.selected = true; + // mousedown_node.dirty = true; + // moving_set.push({n:mousedown_node}); + // } + // } if (mouse_mode == RED.state.MOVING || mouse_mode == RED.state.MOVING_ACTIVE) { + // if (mousedown_node) { + // delete mousedown_node.gSelected; + // } for (i=0;i 0) { - selection.nodes = moving_set.map(function(n) { return n.n;}); - } - if (selected_link != null) { - selection.link = selected_link; - } + selection = getSelection(); activeLinks = RED.nodes.filterLinks({ source:{z:activeWorkspace}, target:{z:activeWorkspace} @@ -1606,6 +1872,8 @@ RED.view = (function() { var node = moving_set[0].n; if (node.type === "subflow") { RED.editor.editSubflow(activeSubflow); + } else if (node.type === "group") { + RED.editor.editGroup(node); } else { RED.editor.edit(node); } @@ -1632,6 +1900,7 @@ RED.view = (function() { dirty: RED.nodes.dirty(), nodes: [], links: [], + groups: [], workspaces: [], subflows: [] } @@ -1651,6 +1920,7 @@ RED.view = (function() { } historyEvent.nodes = historyEvent.nodes.concat(subEvent.nodes); historyEvent.links = historyEvent.links.concat(subEvent.links); + historyEvent.groups = historyEvent.groups.concat(subEvent.groups); } RED.history.push(historyEvent); RED.nodes.dirty(true); @@ -1661,6 +1931,7 @@ RED.view = (function() { var result; var removedNodes = []; var removedLinks = []; + var removedGroups = []; var removedSubflowOutputs = []; var removedSubflowInputs = []; var removedSubflowStatus; @@ -1668,11 +1939,14 @@ RED.view = (function() { var startDirty = RED.nodes.dirty(); var startChanged = false; + var selectedGroups = []; if (moving_set.length > 0) { for (var i=0;i 0) { + var g = selectedGroups.shift(); + if (removedGroups.indexOf(g) === -1) { + removedGroups.push(g); + g.nodes.forEach(function(n) { + if (n.type === "group") { + selectedGroups.push(n); + } + }) + RED.nodes.removeGroup(g); + } + } if (removedSubflowOutputs.length > 0) { result = RED.subflow.removeOutput(removedSubflowOutputs); if (result) { @@ -1716,7 +2009,7 @@ RED.view = (function() { subflowInstances = instances.instances; } moving_set = []; - if (removedNodes.length > 0 || removedSubflowOutputs.length > 0 || removedSubflowInputs.length > 0 || removedSubflowStatus) { + if (removedNodes.length > 0 || removedSubflowOutputs.length > 0 || removedSubflowInputs.length > 0 || removedSubflowStatus || removedGroups.length > 0) { RED.nodes.dirty(true); } } @@ -1769,6 +2062,7 @@ RED.view = (function() { t:"delete", nodes:removedNodes, links:removedLinks, + groups: removedGroups, subflowOutputs:removedSubflowOutputs, subflowInputs:removedSubflowInputs, subflow: { @@ -1801,20 +2095,36 @@ RED.view = (function() { selection.forEach(function(n) { if (n.type === 'tab') { nodes.push(n); + nodes = nodes.concat(RED.nodes.groups(n.id)); nodes = nodes.concat(RED.nodes.filterNodes({z:n.id})); } }); - } else if (moving_set.length > 0) { - nodes = moving_set.map(function(n) { return n.n }); + } else { + selection = RED.view.selection(); + if (selection.nodes) { + selection.nodes.forEach(function(n) { + nodes.push(n); + if (n.type === 'group') { + nodes = nodes.concat(RED.group.getNodes(n,true)); + } + }) + } } if (nodes.length > 0) { var nns = []; + var nodeCount = 0; + var groupCount = 0; for (var n=0;n 0) { + RED.notify(RED._("clipboard.nodeCopied",{count:nodeCount}),{id:"clipboard"}); + } else if (groupCount > 0) { + RED.notify(RED._("clipboard.groupCopied",{count:groupCount}),{id:"clipboard"}); + } } } function calculateTextWidth(str, className, offset) { - return calculateTextDimensions(str,className,offset,0)[0]; + var result=convertLineBreakCharacter(str); + var width = 0; + for (var i=0;i 0 ? 1: 0) : (d.direction == "in" ? 0: 1) var wasJoining = false; if (mouse_mode === RED.state.JOINING || mouse_mode === RED.state.QUICK_JOINING) { @@ -2276,7 +2666,24 @@ RED.view = (function() { } } + function prepareDrag(mouse) { + mouse_mode = RED.state.MOVING; + // Called when moving_set should be prepared to be dragged + for (i=0;i 0 && clickElapsed < 750) { + mouse_mode = RED.state.DEFAULT; + RED.editor.editGroup(g); + d3.event.stopPropagation(); + return; + } + + } + + function groupMouseDown(g) { + var mouse = d3.touches(this.parentNode)[0]||d3.mouse(this.parentNode); + // if (! (mouse[0] < g.x+10 || mouse[0] > g.x+g.w-10 || mouse[1] < g.y+10 || mouse[1] > g.y+g.h-10) ) { + // return + // } + + focusView(); + 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) { + d3.event.stopPropagation(); + return; + } else if (mouse_mode === RED.state.SELECTING_NODE) { + d3.event.stopPropagation(); + return; + } + + mousedown_group = g; + + var now = Date.now(); + clickElapsed = now-clickTime; + clickTime = now; + + dblClickPrimed = ( + lastClickNode == g && + d3.event.button === 0 && + !d3.event.shiftKey && !d3.event.metaKey && !d3.event.altKey && !d3.event.ctrlKey + ); + lastClickNode = g; + + if (g.selected && (d3.event.ctrlKey||d3.event.metaKey)) { + if (g === activeGroup) { + exitActiveGroup(); + } + deselectGroup(g); + d3.event.stopPropagation(); + } else { + if (!g.selected) { + if (!d3.event.ctrlKey && !d3.event.metaKey) { + clearSelection(); + } + if (activeGroup) { + if (!RED.group.contains(activeGroup,g)) { + // Clicked on a group that is outside the activeGroup + exitActiveGroup(); + } else { + } + } + selectGroup(g,true);//!wasSelected); + } else { + exitActiveGroup(); + } + + + if (d3.event.button != 2) { + var d = g.nodes[0]; + prepareDrag(mouse); + mousedown_group.dx = mousedown_group.x - mouse[0]; + mousedown_group.dy = mousedown_group.y - mouse[1]; + } + } + + updateSelection(); + redraw(); + d3.event.stopPropagation(); + } + + function selectGroup(g, includeNodes) { + if (!g.selected) { + g.selected = true; + g.dirty = true; + } + moving_set.push({n:g}); + if (includeNodes) { + var currentSet = new Set(moving_set.map(function(n) { return n.n })); + var allNodes = RED.group.getNodes(g,true); + allNodes.forEach(function(n) { + if (!currentSet.has(n)) { + moving_set.push({n:n}) + // n.selected = true; + } + n.dirty = true; + }) + } + } + function enterActiveGroup(group) { + if (activeGroup) { + exitActiveGroup(); + } + group.active = true; + group.dirty = true; + activeGroup = group; + for (var i = moving_set.length-1; i >= 0; i -= 1) { + if (moving_set[i].n === group) { + moving_set.splice(i,1); + break; + } + } + } + function exitActiveGroup() { + if (activeGroup) { + activeGroup.active = false; + activeGroup.dirty = true; + deselectGroup(activeGroup); + selectGroup(activeGroup,true); + activeGroup = null; + } + } + function deselectGroup(g) { + if (g.selected) { + g.selected = false; + g.dirty = true; + } + var nodeSet = new Set(g.nodes); + nodeSet.add(g); + for (var i = moving_set.length-1; i >= 0; i -= 1) { + if (nodeSet.has(moving_set[i].n) || moving_set[i].n === g) { + moving_set[i].n.selected = false; + moving_set[i].n.dirty = true; + moving_set.splice(i,1); + } + } + } + function getGroupAt(x,y) { + var candidateGroups = {}; + for (var i=0;i= g.x && x <= g.x + g.w && y >= g.y && y <= g.y + g.h) { + candidateGroups[g.id] = g; + } + } + var ids = Object.keys(candidateGroups); + if (ids.length > 1) { + ids.forEach(function(id) { + if (candidateGroups[id] && candidateGroups[id].g) { + delete candidateGroups[candidateGroups[id].g] + } + }) + ids = Object.keys(candidateGroups); + } + if (ids.length === 0) { + return null; + } else { + return candidateGroups[ids[ids.length-1]] + } + } + function isButtonEnabled(d) { var buttonEnabled = true; var ws = RED.nodes.workspace(RED.workspaces.active()); @@ -2424,7 +3066,9 @@ RED.view = (function() { function nodeButtonClicked(d) { if (mouse_mode === RED.state.SELECTING_NODE) { - d3.event.stopPropagation(); + if (d3.event) { + d3.event.stopPropagation(); + } return; } var activeWorkspace = RED.workspaces.active(); @@ -2451,7 +3095,9 @@ RED.view = (function() { RED.notify(RED._("notification.warning", {message:RED._("notification.warnings.nodeActionDisabled")}),"warning"); } } - d3.event.preventDefault(); + if (d3.event) { + d3.event.preventDefault(); + } } function showTouchMenu(obj,pos) { @@ -2523,6 +3169,10 @@ RED.view = (function() { } function redraw() { + requestAnimationFrame(_redraw); + } + + function _redraw() { eventLayer.attr("transform","scale("+scaleFactor+")"); outer.attr("width", space_width*scaleFactor).attr("height", space_height*scaleFactor); @@ -2702,23 +3352,26 @@ RED.view = (function() { var nodeEnter = node.enter().insert("svg:g") .attr("class", "red-ui-flow-node red-ui-flow-node-group") - .classed("red-ui-flow-subflow",function(d) { return activeSubflow != null; }); + .classed("red-ui-flow-subflow", activeSubflow != null); nodeEnter.each(function(d,i) { var node = d3.select(this); var isLink = (d.type === "link in" || d.type === "link out") var hideLabel = d.hasOwnProperty('l')?!d.l : isLink; node.attr("id",d.id); - var l = RED.utils.getNodeLabel(d); + var labelWidth = calculateTextWidth(RED.utils.getNodeLabel(d), "red-ui-flow-node-label", 50); if (d.resize || d.w === undefined) { if (hideLabel) { d.w = node_height; } else { - d.w = Math.max(node_width,20*(Math.ceil((calculateTextWidth(l, "red-ui-flow-node-label", 50)+(d._def.inputs>0?7:0))/20)) ); + d.w = Math.max(node_width,20*(Math.ceil((labelWidth+(d._def.inputs>0?7:0))/20)) ); } } - d.h = Math.max(node_height,(d.outputs||0) * 15); - + if (hideLabel) { + d.h = Math.max(node_height,(d.outputs || 0) * 15); + } else { + d.h = Math.max(6+24*separateTextByLineBreak.length, (d.outputs || 0) * 15, 30); + } // if (d._def.badge) { // var badge = node.append("svg:g").attr("class","node_badge_group"); // var badgeRect = badge.append("rect").attr("class","node_badge").attr("rx",5).attr("ry",5).attr("width",40).attr("height",15); @@ -2765,10 +3418,10 @@ RED.view = (function() { var mainRect = node.append("rect") .attr("class", "red-ui-flow-node") - .classed("red-ui-flow-node-unknown",function(d) { return d.type == "unknown"; }) + .classed("red-ui-flow-node-unknown", d.type == "unknown") .attr("rx", 5) .attr("ry", 5) - .attr("fill",function(d) { return RED.utils.getNodeColor(d.type,d._def); /*d._def.color;*/}) + .attr("fill", RED.utils.getNodeColor(d.type,d._def)) .on("mouseup",nodeMouseUp) .on("mousedown",nodeMouseDown) .on("touchstart",function(d) { @@ -2876,17 +3529,17 @@ RED.view = (function() { .attr("x",0).attr("y",0) .attr("class","red-ui-flow-node-icon-shade") .attr("width","30") - .attr("height",function(d){return Math.min(50,d.h-4);}); + .attr("height", Math.min(50,d.h-4)); createIconAttributes(icon_url, icon_group, d); var icon_shade_border = icon_group.append("path") - .attr("d",function(d) { return "M 30 1 l 0 "+(d.h-2)}) + .attr("d","M 30 1 l 0 "+(d.h-2)) .attr("class","red-ui-flow-node-icon-shade-border"); if ("right" == d._def.align) { icon_group.attr("class","red-ui-flow-node-icon-group red-ui-flow-node-icon-group-"+d._def.align); - icon_shade_border.attr("d",function(d) { return "M 0 1 l 0 "+(d.h-2)}) + icon_shade_border.attr("d", "M 0 1 l 0 "+(d.h-2)) //icon.attr("class","red-ui-flow-node-icon red-ui-flow-node-icon-"+d._def.align); //icon.attr("class","red-ui-flow-node-icon-shade red-ui-flow-node-icon-shade-"+d._def.align); //icon.attr("class","red-ui-flow-node-icon-shade-border red-ui-flow-node-icon-shade-border-"+d._def.align); @@ -2904,17 +3557,22 @@ RED.view = (function() { //icon.style("pointer-events","none"); icon_group.style("pointer-events","none"); } - var text = node.append("svg:text") + var labelLineNumber = (separateTextByLineBreak.length == 0)? 1:separateTextByLineBreak.length; + var labelId = d.id.replace(".","-"); + for(var i=0;i0?7:0))/20)) ); + d.w = Math.max(node_width,20*(Math.ceil((labelWidth+(d._def.inputs>0?7:0))/20)) ); } // d.w = Math.max(node_width,20*(Math.ceil((calculateTextWidth(l, "red-ui-flow-node-label", 50)+(d._def.inputs>0?7:0))/20)) ); - d.h = Math.max(node_height,(d.outputs||0) * 15); d.x += (d.w-ow)/2; d.resize = false; } + if (hideLabel) { + d.h = Math.max(node_height,(d.outputs || 0) * 15); + } else { + d.h = Math.max(6+24*separateTextByLineBreak.length,(d.outputs || 0) * 15, 30); + } + var thisNode = d3.select(this); - thisNode.classed("red-ui-flow-node-disabled", function(d) { return d.d === true}); - thisNode.classed("red-ui-flow-subflow",function(d) { return activeSubflow != null; }) + thisNode.classed("red-ui-flow-node-disabled", d.d === true); + thisNode.classed("red-ui-flow-subflow", activeSubflow != null) //thisNode.selectAll(".centerDot").attr({"cx":function(d) { return d.w/2;},"cy":function(d){return d.h/2}}); - thisNode.attr("transform", function(d) { return "translate(" + (d.x-d.w/2) + "," + (d.y-d.h/2) + ")"; }); + thisNode.attr("transform", "translate(" + (d.x-d.w/2) + "," + (d.y-d.h/2) + ")"); if (mouse_mode != RED.state.MOVING_ACTIVE) { - thisNode.classed("red-ui-flow-node-selected",function(d) { return d.selected }) + thisNode.classed("red-ui-flow-node-selected", d.selected ) thisNode.selectAll(".red-ui-flow-node") - .attr("width",function(d){return d.w}) - .attr("height",function(d){return d.h}) - .classed("red-ui-flow-node-highlighted",function(d) { return d.highlighted; }) + .attr("width", d.w) + .attr("height", d.h) + .classed("red-ui-flow-node-highlighted",d.highlighted ) ; + var l = ""; + if (d._def.label) { + l = d._def.label; + try { + l = (typeof l === "function" ? l.call(d) : l)||""; + l = RED.text.bidi.enforceTextDirectionWithUCC(l); + } catch(err) { + console.log("Definition error: "+d.type+".label",err); + l = d.type; + } + } + var sa = convertLineBreakCharacter(l); + var sn = sa.length; + var st = ""; + var yp = d.h / 2 - (sn / 2) * 24 + 16 + var labelId = d.id.replace(".","-"); + if(labelLineNumber0?5:0);}); //thisNode.selectAll(".red-ui-flow-node-icon-shade-right").attr("x",function(d){return d.w-30;}); //thisNode.selectAll(".red-ui-flow-node-icon-shade-border-right").attr("d",function(d){return "M "+(d.w-30)+" 1 l 0 "+(d.h-2)}); @@ -3067,35 +3790,6 @@ RED.view = (function() { port.attr("transform", function(d) { return "translate("+x+","+((y+13*i)-5)+")";}); }); } - thisNode.selectAll("text.red-ui-flow-node-label").text(function(d,i){ - var l = ""; - if (d._def.label) { - l = d._def.label; - try { - l = (typeof l === "function" ? l.call(d) : l)||""; - l = RED.text.bidi.enforceTextDirectionWithUCC(l); - } catch(err) { - console.log("Definition error: "+d.type+".label",err); - l = d.type; - } - } - return l; - }) - .attr("y", function(d){return (d.h/2)-1;}) - .attr("class",function(d){ - var s = ""; - if (d._def.labelStyle) { - s = d._def.labelStyle; - try { - s = (typeof s === "function" ? s.call(d) : s)||""; - } catch(err) { - console.log("Definition error: "+d.type+".labelStyle",err); - s = ""; - } - s = " "+s; - } - return "red-ui-flow-node-label"+(d._def.align?" red-ui-flow-node-label-"+d._def.align:"")+s; - }).classed("hide",hideLabel); if (d._def.icon) { var icon = thisNode.select(".red-ui-flow-node-icon"); var faIcon = thisNode.select(".fa-lg"); @@ -3118,12 +3812,12 @@ RED.view = (function() { } thisNode.selectAll(".red-ui-flow-node-changed") - .attr("transform",function(d){return "translate("+(d.w-10)+", -2)"}) - .classed("hide",function(d) { return !(d.changed||d.moved); }); + .attr("transform", "translate("+(d.w-10)+", -2)") + .classed("hide", !(d.changed||d.moved)); thisNode.selectAll(".red-ui-flow-node-error") - .attr("transform",function(d){ return "translate("+(d.w-10-((d.changed||d.moved)?14:0))+", -2)"}) - .classed("hide",function(d) { return d.valid; }); + .attr("transform", "translate("+(d.w-10-((d.changed||d.moved)?14:0))+", -2)") + .classed("hide", d.valid); thisNode.selectAll(".red-ui-flow-port-input").each(function(d,i) { var port = d3.select(this); @@ -3131,18 +3825,14 @@ RED.view = (function() { }); thisNode.selectAll(".red-ui-flow-node-icon").attr("y",function(d){return (d.h-d3.select(this).attr("height"))/2;}); - thisNode.selectAll(".red-ui-flow-node-icon-shade").attr("height",function(d){return d.h;}); - thisNode.selectAll(".red-ui-flow-node-icon-shade-border").attr("d", function (d) { - return "M " + (((!d._def.align && d.inputs !== 0 && d.outputs === 0) || "right" === d._def.align) ? 0 : 30) + " 1 l 0 " + (d.h - 2); - }); - thisNode.selectAll(".fa-lg").attr("y",function(d){return (d.h+13)/2;}); + thisNode.selectAll(".red-ui-flow-node-icon-shade").attr("height", d.h ); + thisNode.selectAll(".red-ui-flow-node-icon-shade-border").attr("d", + "M " + (((!d._def.align && d.inputs !== 0 && d.outputs === 0) || "right" === d._def.align) ? 0 : 30) + " 1 l 0 " + (d.h - 2) + ); + thisNode.selectAll(".fa-lg").attr("y",(d.h+13)/2); - thisNode.selectAll(".red-ui-flow-node-button").attr("opacity",function(d) { - return (!isButtonEnabled(d))?0.4:1 - }); - thisNode.selectAll(".red-ui-flow-node-button-button").attr("cursor",function(d) { - return (!isButtonEnabled(d))?"":"pointer"; - }); + thisNode.selectAll(".red-ui-flow-node-button").attr("opacity", function(d2) { return !isButtonEnabled(d2)?0.4:1 }); + thisNode.selectAll(".red-ui-flow-node-button-button").attr("cursor",function(d2) { return isButtonEnabled(d2)?"":"pointer"}); thisNode.selectAll(".red-ui-flow-node-button").attr("transform",function(d){ var x = d._def.align == "right"?d.w-6:-25; if (d._def.button.toggle && !d[d._def.button.toggle]) { @@ -3208,6 +3898,15 @@ RED.view = (function() { } d.dirty = false; + if (d.g) { + if (!dirtyGroups[d.g]) { + var gg = d.g; + while (gg && !dirtyGroups[gg]) { + dirtyGroups[gg] = RED.nodes.group(gg); + gg = dirtyGroups[gg].g; + } + } + } } }); @@ -3238,7 +3937,9 @@ RED.view = (function() { d3.event.stopPropagation(); if (d3.event.metaKey || d3.event.ctrlKey) { l.classed("red-ui-flow-link-splice",true); - showQuickAddDialog(d3.mouse(this), selected_link); + var point = d3.mouse(this); + var clickedGroup = getGroupAt(point[0],point[1]); + showQuickAddDialog(point, selected_link, clickedGroup); } }) .on("touchstart",function(d) { @@ -3423,6 +4124,194 @@ RED.view = (function() { }) + + var group = groupLayer.selectAll(".red-ui-flow-group").data(activeGroups,function(d) { return d.id }); + group.exit().each(function(d,i) { + document.getElementById("group_select_"+d.id).remove() + }).remove(); + var groupEnter = group.enter().insert("svg:g").attr("class", "red-ui-flow-group") + + var addedGroups = false; + groupEnter.each(function(d,i) { + addedGroups = true; + var g = d3.select(this); + g.attr("id",d.id); + + var groupBorderRadius = 4; + + var selectGroup = groupSelectLayer.append('g').attr("class", "red-ui-flow-group").attr("id","group_select_"+d.id); + selectGroup.append('rect').classed("red-ui-flow-group-outline-select",true) + .attr('rx',groupBorderRadius).attr('ry',groupBorderRadius) + .attr("x",-4) + .attr("y",-4) + .style("stroke","rgba(255,255,255,0.8)") + .style("stroke-width",6) + + selectGroup.append('rect').classed("red-ui-flow-group-outline-select",true) + .attr('rx',groupBorderRadius).attr('ry',groupBorderRadius) + .attr("x",-4) + .attr("y",-4) + selectGroup.on("mousedown", function() {groupMouseDown.call(g[0][0],d)}); + selectGroup.on("mouseup", function() {groupMouseUp.call(g[0][0],d)}); + + g.append('rect').classed("red-ui-flow-group-outline",true).attr('rx',0.5).attr('ry',0.5); + + g.append('rect').classed("red-ui-flow-group-body",true) + .attr('rx',groupBorderRadius).attr('ry',groupBorderRadius).style({ + "fill":d.fill||"none", + "stroke": d.stroke||"none", + }) + g.on("mousedown",groupMouseDown).on("mouseup",groupMouseUp) + g.append('svg:text').attr("class","red-ui-flow-group-label"); + d.dirty = true; + }); + if (addedGroups) { + group.sort(function(a,b) { + if (a._root === b._root) { + return a._depth - b._depth; + } else { + return a._root.localeCompare(b._root); + } + }) + } + group[0].reverse(); + + group.each(function(d,i) { + if (d.resize) { + d.minWidth = 0; + delete d.resize; + } + if (d.dirty || dirtyGroups[d.id]) { + var g = d3.select(this); + if (d.nodes.length > 0) { + var minX = Number.POSITIVE_INFINITY; + var minY = Number.POSITIVE_INFINITY; + var maxX = 0; + var maxY = 0; + d.nodes.forEach(function(n) { + if (n.type !== "group") { + minX = Math.min(minX,n.x-n.w/2-25-((n._def.button && n._def.align!=="right")?20:0)); + minY = Math.min(minY,n.y-n.h/2-25); + maxX = Math.max(maxX,n.x+n.w/2+25+((n._def.button && n._def.align=="right")?20:0)); + maxY = Math.max(maxY,n.y+n.h/2+25); + } else { + minX = Math.min(minX,n.x-25) + minY = Math.min(minY,n.y-25) + maxX = Math.max(maxX,n.x+n.w+25) + maxY = Math.max(maxY,n.y+n.h+25) + } + }); + + d.x = minX; + d.y = minY; + d.w = maxX - minX; + d.h = maxY - minY; + } else { + d.w = 40; + d.h = 40; + } + if (!d.minWidth) { + if (d.style.label && d.name) { + d.minWidth = calculateTextWidth(d.name||"","red-ui-flow-group-label",8); + d.labels = separateTextByLineBreak; + } else { + d.minWidth = 40; + } + } + d.w = Math.max(d.minWidth,d.w); + if (d.style.label && d.labels) { + var h = (d.labels.length -1) * 16; + var labelPos = d.style["label-position"] || "nw"; + d.h += h; + if (labelPos[0] === "n") { + d.y -= h; + } + } + + g.attr("transform","translate("+d.x+","+d.y+")") + g.selectAll(".red-ui-flow-group-outline") + .attr("width",d.w) + .attr("height",d.h) + + + var selectGroup = document.getElementById("group_select_"+d.id); + selectGroup.setAttribute("transform","translate("+d.x+","+d.y+")"); + if (d.hovered) { + selectGroup.classList.add("red-ui-flow-group-hovered") + } else { + selectGroup.classList.remove("red-ui-flow-group-hovered") + } + var selectGroupRect = selectGroup.children[0]; + selectGroupRect.setAttribute("width",d.w+8) + selectGroupRect.setAttribute("height",d.h+8) + selectGroupRect.style.strokeOpacity = (d.selected || d.highlighted)?0.8:0; + selectGroupRect.style.strokeDasharray = (d.active)?"10 4":""; + selectGroupRect = selectGroup.children[1]; + selectGroupRect.setAttribute("width",d.w+8) + selectGroupRect.setAttribute("height",d.h+8) + selectGroupRect.style.strokeOpacity = (d.selected || d.highlighted)?0.8:0; + selectGroupRect.style.strokeDasharray = (d.active)?"10 4":""; + + if (d.highlighted) { + selectGroup.classList.add("red-ui-flow-node-highlighted"); + } else { + selectGroup.classList.remove("red-ui-flow-node-highlighted"); + } + + + g.selectAll(".red-ui-flow-group-body") + .attr("width",d.w) + .attr("height",d.h) + .style("stroke", d.style.stroke || "") + .style("stroke-opacity", d.style.hasOwnProperty('stroke-opacity') ? d.style['stroke-opacity'] : "") + .style("fill", d.style.fill || "") + .style("fill-opacity", d.style.hasOwnProperty('fill-opacity') ? d.style['fill-opacity'] : "") + + var label = g.selectAll(".red-ui-flow-group-label"); + label.classed("hide",!!!d.style.label) + if (d.style.label) { + var labelPos = d.style["label-position"] || "nw"; + var labelX = 0; + var labelY = 0; + + if (labelPos[0] === 'n') { + labelY = 0+15; // Allow for font-height + } else { + labelY = d.h - 5 -(d.labels.length -1) * 16; + } + if (labelPos[1] === 'w') { + labelX = 5; + labelAnchor = "start" + } else if (labelPos[1] === 'e') { + labelX = d.w-5; + labelAnchor = "end" + } else { + labelX = d.w/2; + labelAnchor = "middle" + } + label + .style("fill", d.style.hasOwnProperty('color')?d.style.color:"#999") + .attr("transform","translate("+labelX+","+labelY+")") + .attr("text-anchor",labelAnchor); + if (d.labels) { + var ypos = 0; + g.selectAll(".red-ui-flow-group-label-text").remove(); + d.labels.forEach(function (name) { + label.append("tspan") + .classed("red-ui-flow-group-label-text", true) + .text(name) + .attr("x", 0) + .attr("y", ypos); + ypos += 16; + }); + } + } + + delete dirtyGroups[d.id]; + delete d.dirty; + } + }) + } else { // JOINING - unselect any selected links linkLayer.selectAll(".red-ui-flow-link-selected").data( @@ -3436,7 +4325,6 @@ RED.view = (function() { if (d3.event) { d3.event.preventDefault(); } - } function focusView() { @@ -3474,29 +4362,36 @@ RED.view = (function() { if (result) { var new_nodes = result[0]; var new_links = result[1]; - var new_workspaces = result[2]; - var new_subflows = result[3]; - var new_default_workspace = result[4]; + var new_groups = result[2]; + var new_workspaces = result[3]; + var new_subflows = result[4]; + var new_default_workspace = result[5]; if (addNewFlow && new_default_workspace) { RED.workspaces.show(new_default_workspace.id); } var new_ms = new_nodes.filter(function(n) { return n.hasOwnProperty("x") && n.hasOwnProperty("y") && n.z == RED.workspaces.active() }).map(function(n) { return {n:n};}); + new_ms = new_ms.concat(new_groups.filter(function(g) { return g.z === RED.workspaces.active()}).map(function(g) { return {n:g}})) var new_node_ids = new_nodes.map(function(n){ n.changed = true; return n.id; }); - // TODO: pick a more sensible root node if (new_ms.length > 0) { - var root_node = new_ms[0].n; - var dx = root_node.x; - var dy = root_node.y; + if (mouse_position == null) { mouse_position = [0,0]; } + var dx = mouse_position[0]; + var dy = mouse_position[1]; + if (new_ms.length > 0) { + var root_node = new_ms[0].n; + dx = root_node.x; + dy = root_node.y; + } + var minX = 0; var minY = 0; var i; - var node; + var node,group; for (i=0;i 0 && - node.n._def.outputs > 0; + ((node.n.hasOwnProperty("inputs") && node.n.inputs > 0) || (!node.n.hasOwnProperty("inputs") && node.n._def.inputs > 0)) && + ((node.n.hasOwnProperty("outputs") && node.n.outputs > 0) || (!node.n.hasOwnProperty("outputs") && node.n._def.outputs > 0)) + + } } RED.keyboard.add("*","escape",function(){ @@ -3549,6 +4454,7 @@ RED.view = (function() { t:"add", nodes:new_node_ids, links:new_links, + groups:new_groups, workspaces:new_workspaces, subflows:new_subflows, dirty:RED.nodes.dirty() @@ -3581,12 +4487,16 @@ RED.view = (function() { newConfigNodeCount++; } }) + var newGroupCount = new_groups.length; if (new_workspaces.length > 0) { counts.push(RED._("clipboard.flow",{count:new_workspaces.length})); } if (newNodeCount > 0) { counts.push(RED._("clipboard.node",{count:newNodeCount})); } + if (newGroupCount > 0) { + counts.push(RED._("clipboard.group",{count:newGroupCount})); + } if (newConfigNodeCount > 0) { counts.push(RED._("clipboard.configNode",{count:newNodeCount})); } @@ -3638,22 +4548,25 @@ RED.view = (function() { var historyEvents = []; for (var i=0;i 0) { @@ -3668,7 +4581,40 @@ RED.view = (function() { RED.view.redraw(); } + function getSelection() { + var selection = {}; + var allNodes = new Set(); + + if (moving_set.length > 0) { + moving_set.forEach(function(n) { + if (n.n.type !== 'group') { + allNodes.add(n.n); + } + }); + } + var selectedGroups = activeGroups.filter(function(g) { return g.selected && !g.active }); + if (selectedGroups.length > 0) { + if (selectedGroups.length === 1 && selectedGroups[0].active) { + // Let nodes be nodes + } else { + selectedGroups.forEach(function(g) { + var groupNodes = RED.group.getNodes(g,true); + groupNodes.forEach(function(n) { + allNodes.delete(n); + }); + allNodes.add(g); + }); + } + } + if (allNodes.size > 0) { + selection.nodes = Array.from(allNodes); + } + if (selected_link != null) { + selection.link = selected_link; + } + return selection; + } return { init: init, state:function(state) { @@ -3679,12 +4625,17 @@ RED.view = (function() { } }, - redraw: function(updateActive) { + updateActive: updateActiveNodes, + redraw: function(updateActive, syncRedraw) { if (updateActive) { updateActiveNodes(); updateSelection(); } - redraw(); + if (syncRedraw) { + _redraw(); + } else { + redraw(); + } }, focus: focusView, importNodes: importNodes, @@ -3699,21 +4650,31 @@ RED.view = (function() { selectedNode.dirty = true; moving_set = [{n:selectedNode}]; } + } else if (selection) { + if (selection.nodes) { + updateActiveNodes(); + moving_set = []; + // TODO: this selection group span groups + // - if all in one group -> activate the group + // - if in multiple groups (or group/no-group) + // -> select the first 'set' of things in the same group/no-group + selection.nodes.forEach(function(n) { + if (n.type !== "group") { + n.selected = true; + n.dirty = true; + moving_set.push({n:n}); + } else { + selectGroup(n,true); + } + }) + } } } updateSelection(); - redraw(); - }, - selection: function() { - var selection = {}; - if (moving_set.length > 0) { - selection.nodes = moving_set.map(function(n) { return n.n;}); - } - if (selected_link != null) { - selection.link = selected_link; - } - return selection; + redraw(true); }, + selection: getSelection, + scale: function() { return scaleFactor; }, @@ -3728,47 +4689,57 @@ RED.view = (function() { } return result; }, - reveal: function(id) { + getGroupAtPoint: getGroupAt, + getActiveGroup: function() { return activeGroup }, + reveal: function(id,triggerHighlight) { if (RED.nodes.workspace(id) || RED.nodes.subflow(id)) { RED.workspaces.show(id); } else { - var node = RED.nodes.node(id); - if (node._def.category !== 'config' && node.z) { - node.highlighted = true; - node.dirty = true; - RED.workspaces.show(node.z); + var node = RED.nodes.node(id) || RED.nodes.group(id); + if (node) { + if (node.z && (node.type === "group" || node._def.category !== 'config')) { + node.dirty = true; + RED.workspaces.show(node.z); - var screenSize = [chart.width()/scaleFactor,chart.height()/scaleFactor]; - var scrollPos = [chart.scrollLeft()/scaleFactor,chart.scrollTop()/scaleFactor]; - - if (node.x < scrollPos[0] || node.y < scrollPos[1] || node.x > screenSize[0]+scrollPos[0] || node.y > screenSize[1]+scrollPos[1]) { - var deltaX = '-='+(((scrollPos[0] - node.x) + screenSize[0]/2)*scaleFactor); - var deltaY = '-='+(((scrollPos[1] - node.y) + screenSize[1]/2)*scaleFactor); - chart.animate({ - scrollLeft: deltaX, - scrollTop: deltaY - },200); - } - - if (!node._flashing) { - node._flashing = true; - var flash = 22; - var flashFunc = function() { - flash--; - node.dirty = true; - if (flash >= 0) { - node.highlighted = !node.highlighted; - setTimeout(flashFunc,100); - } else { - node.highlighted = false; - delete node._flashing; - } - RED.view.redraw(); + var screenSize = [chart.width()/scaleFactor,chart.height()/scaleFactor]; + var scrollPos = [chart.scrollLeft()/scaleFactor,chart.scrollTop()/scaleFactor]; + var cx = node.x; + var cy = node.y; + if (node.type === "group") { + cx += node.w/2; + cy += node.h/2; } - flashFunc(); + if (cx < scrollPos[0] || cy < scrollPos[1] || cx > screenSize[0]+scrollPos[0] || cy > screenSize[1]+scrollPos[1]) { + var deltaX = '-='+(((scrollPos[0] - cx) + screenSize[0]/2)*scaleFactor); + var deltaY = '-='+(((scrollPos[1] - cy) + screenSize[1]/2)*scaleFactor); + chart.animate({ + scrollLeft: deltaX, + scrollTop: deltaY + },200); + } + if (triggerHighlight !== false) { + node.highlighted = true; + if (!node._flashing) { + node._flashing = true; + var flash = 22; + var flashFunc = function() { + flash--; + node.dirty = true; + if (flash >= 0) { + node.highlighted = !node.highlighted; + setTimeout(flashFunc,100); + } else { + node.highlighted = false; + delete node._flashing; + } + RED.view.redraw(); + } + flashFunc(); + } + } + } else if (node._def.category === 'config') { + RED.sidebar.config.show(id); } - } else if (node._def.category === 'config') { - RED.sidebar.config.show(id); } } }, @@ -3793,7 +4764,6 @@ RED.view = (function() { mouse_mode = RED.state.SELECTING_NODE; clearSelection(); if (options.selected) { - console.log(options.selected); options.selected.forEach(function(id) { var n = RED.nodes.node(id); if (n) { @@ -3846,6 +4816,15 @@ RED.view = (function() { type: "compact", buttons: buttons }) + }, + scroll: function(x,y) { + chart.scrollLeft(chart.scrollLeft()+x); + chart.scrollTop(chart.scrollTop()+y) + }, + clickNodeButton: function(n) { + if (n._def.button) { + nodeButtonClicked(n); + } } }; })(); diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/workspaces.js b/packages/node_modules/@node-red/editor-client/src/js/ui/workspaces.js index 1f361d0f7..290083ea6 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/workspaces.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/workspaces.js @@ -128,10 +128,6 @@ RED.workspaces = (function() { RED.history.push(historyEvent); RED.nodes.dirty(true); RED.sidebar.config.refresh(); - var selection = RED.view.selection(); - if (!selection.nodes && !selection.links) { - RED.sidebar.info.refresh(workspace); - } if (changes.hasOwnProperty('disabled')) { RED.nodes.eachNode(function(n) { if (n.z === workspace.id) { @@ -140,6 +136,7 @@ RED.workspaces = (function() { }); RED.view.redraw(); } + RED.events.emit("flows:change",workspace); } RED.tray.close(); } @@ -219,7 +216,10 @@ RED.workspaces = (function() { if (RED.view.state() != RED.state.IMPORT_DRAGGING) { RED.view.state(RED.state.DEFAULT); } - RED.sidebar.info.refresh(workspace); + var selection = RED.view.selection(); + if (!selection.nodes && !selection.links && workspace.id === activeWorkspace) { + RED.sidebar.info.refresh(workspace); + } tabflowEditor.destroy(); } } @@ -371,7 +371,9 @@ RED.workspaces = (function() { var changes = { disabled: workspace.disabled }; workspace.disabled = disabled; $("#red-ui-tab-"+(workspace.id.replace(".","-"))).toggleClass('red-ui-workspace-disabled',!!workspace.disabled); - $("#red-ui-workspace").toggleClass("red-ui-workspace-disabled",!!workspace.disabled); + if (id === activeWorkspace) { + $("#red-ui-workspace").toggleClass("red-ui-workspace-disabled",!!workspace.disabled); + } var historyEvent = { t: "edit", changes:changes, @@ -380,10 +382,11 @@ RED.workspaces = (function() { } workspace.changed = true; RED.history.push(historyEvent); + RED.events.emit("flows:change",workspace); RED.nodes.dirty(true); RED.sidebar.config.refresh(); var selection = RED.view.selection(); - if (!selection.nodes && !selection.links) { + if (!selection.nodes && !selection.links && workspace.id === activeWorkspace) { RED.sidebar.info.refresh(workspace); } if (changes.hasOwnProperty('disabled')) { @@ -412,9 +415,14 @@ RED.workspaces = (function() { } function setWorkspaceOrder(order) { - RED.nodes.setWorkspaceOrder(order.filter(function(id) { + var newOrder = order.filter(function(id) { return RED.nodes.workspace(id) !== undefined; - })); + }) + var currentOrder = RED.nodes.getWorkspaceOrder(); + if (JSON.stringify(newOrder) !== JSON.stringify(currentOrder)) { + RED.nodes.setWorkspaceOrder(newOrder); + RED.events.emit("flows:reorder",newOrder); + } workspace_tabs.order(order); } diff --git a/packages/node_modules/@node-red/editor-client/src/sass/ace.scss b/packages/node_modules/@node-red/editor-client/src/sass/ace.scss index fb6eaf8eb..cd3739800 100644 --- a/packages/node_modules/@node-red/editor-client/src/sass/ace.scss +++ b/packages/node_modules/@node-red/editor-client/src/sass/ace.scss @@ -9,19 +9,15 @@ color: transparent !important; } } - - .ace_gutter { + background: $text-editor-gutter-background; border-top-left-radius: 4px; border-bottom-left-radius: 4px; } .ace_scroller { + background: $text-editor-background; border-top-right-radius: 4px; border-bottom-right-radius: 4px; - } - - .ace_scroller { - background: $text-editor-background; color: $text-editor-color; } .ace_marker-layer .ace_active-line { @@ -37,9 +33,6 @@ .ace_gutter-active-line { background: $text-editor-gutter-active-line-background; } - .ace_gutter { - background: $text-editor-gutter-background; - } .ace_tooltip { font-family: $primary-font; line-height: 1.4em; @@ -52,6 +45,9 @@ @include component-shadow; border-color: $popover-background; } + .ace_content { + line-height: 1; + } textarea.ace_text-input { overflow: hidden; padding: 0px 1px !important; diff --git a/packages/node_modules/@node-red/editor-client/src/sass/colors.scss b/packages/node_modules/@node-red/editor-client/src/sass/colors.scss index 8411343c5..96be7b5ab 100644 --- a/packages/node_modules/@node-red/editor-client/src/sass/colors.scss +++ b/packages/node_modules/@node-red/editor-client/src/sass/colors.scss @@ -23,7 +23,7 @@ $primary-background: #f3f3f3;//#0ff; $secondary-background: #fff;//#ff0; $secondary-background-selected: #efefef;//#e9e900; $secondary-background-inactive: #f0f0f0;//#f0f000; -$secondary-background-hover: #ddd;//#dd0; +$secondary-background-hover: #e6e6e6;//#dd0; $secondary-background-disabled: #f9f9f9;//#fafa0; $tertiary-background: #f7f7f7;//#f0f; @@ -94,7 +94,7 @@ $list-item-secondary-color: $secondary-text-color; $list-item-background: $secondary-background; $list-item-background-disabled: $secondary-background-inactive; $list-item-background-hover: $secondary-background-hover; -$list-item-background-selected: $secondary-background-selected; +$list-item-background-selected: #ffebc7; // #fff1e5; $list-item-border-selected: $secondary-text-color-selected; $tab-text-color-active: $header-text-color; @@ -284,3 +284,8 @@ $debug-message-border: #eee; $debug-message-border-hover: #999; $debug-message-border-warning: #ffdf9d; $debug-message-border-error: #f99; + +$group-default-fill: none; +$group-default-fill-opacity: 1; +$group-default-stroke: #999; +$group-default-stroke-opacity: 1; diff --git a/packages/node_modules/@node-red/editor-client/src/sass/debug.scss b/packages/node_modules/@node-red/editor-client/src/sass/debug.scss index d767aaed3..1e77e46e1 100644 --- a/packages/node_modules/@node-red/editor-client/src/sass/debug.scss +++ b/packages/node_modules/@node-red/editor-client/src/sass/debug.scss @@ -160,6 +160,7 @@ .red-ui-debug-msg-element { color: $debug-message-text-color; line-height: 1.3em; + overflow-wrap: break-word; } .red-ui-debug-msg-object-key { color: $debug-message-text-color-object-key; @@ -216,6 +217,10 @@ .red-ui-debug-msg-type-number { color: $debug-message-text-color-msg-type-number; }; .red-ui-debug-msg-type-number-toggle { cursor: pointer;} +.red-ui-debug-msg-type-string { + white-space: pre-wrap; +} + .red-ui-debug-msg-row { display: block; padding: 4px 2px 2px; diff --git a/packages/node_modules/@node-red/editor-client/src/sass/editor.scss b/packages/node_modules/@node-red/editor-client/src/sass/editor.scss index b13941f22..d8ef3e89c 100644 --- a/packages/node_modules/@node-red/editor-client/src/sass/editor.scss +++ b/packages/node_modules/@node-red/editor-client/src/sass/editor.scss @@ -304,9 +304,6 @@ button.red-ui-button-small &:first-child { padding: 20px 20px 0; } - &:last-child { - padding-bottom: 20px; - } } } .red-ui-editor-type-expression-tab-content { @@ -411,6 +408,133 @@ button.red-ui-button.red-ui-editor-node-appearance-button { } } +.red-ui-group-layout-picker { + padding: 5px; + background: $primary-background; +} +.red-ui-group-layout-picker-cell-text { + position: absolute; + width: 14px; + height: 2px; + border-top: 2px solid $secondary-text-color; + border-bottom: 2px solid $secondary-text-color; + margin: 2px; + + &.red-ui-group-layout-text-pos-nw { top: 0; left: 0; } + &.red-ui-group-layout-text-pos-n { top: 0; left: calc(50% - 9px); } + &.red-ui-group-layout-text-pos-ne { top: 0; right: 0; } + &.red-ui-group-layout-text-pos-sw { bottom: 0; left: 0; } + &.red-ui-group-layout-text-pos-s { bottom: 0; left: calc(50% - 9px); } + &.red-ui-group-layout-text-pos-se { bottom: 0; right: 0; } + &.red-ui-group-layout-text-pos- { + width: 100%; + height: 100%; + border-radius: 5px; + margin: 0; + background-color: #FFF; + background-size: 100% 100%; + background-position: 0 0, 50% 50%; + background-image: linear-gradient(45deg, transparent 45%, $secondary-border-color 45%, $secondary-border-color 55%, transparent 55%, transparent),linear-gradient(-45deg, transparent 45%, $secondary-border-color 45%, $secondary-border-color 55%, transparent 55%, transparent); + border: none; + } +} + +.red-ui-group-layout-picker button.red-ui-search-result-node { + float: none; + position: relative; + padding: 0; + margin: 5px; + width: 32px; + height: 27px; +} + +button.red-ui-group-layout-picker-none { + width: 100%; +} + +.red-ui-color-picker { + input[type="text"] { + border-radius:0; + width: 100%; + margin-bottom: 0; + border: none; + border-bottom: 1px solid $form-input-border-color; + } + small { + color: $secondary-text-color; + margin-left: 5px; + margin-right: 4px; + display: inline-block; + min-width: 35px; + text-align: right; + } + background: $primary-background; +} +.red-ui-editor-node-appearance-button { + .red-ui-search-result-node { + overflow: hidden + } +} +.red-ui-color-picker-cell { + padding: 0; + border-style: solid; + border-width: 1px; + border-color: $secondary-border-color; +} +.red-ui-color-picker-swatch { + position: absolute; + top:-1px;right:-1px;left:-1px;bottom:-1px; + border-radius: 4px; +} + +.red-ui-color-picker-cell-none { + height: 100%; + background-color: #FFF; + background-size: 100% 100%; + background-position: 0 0, 50% 50%; + background-image: linear-gradient(45deg, transparent 45%, $secondary-border-color 45%, $secondary-border-color 55%, transparent 55%, transparent),linear-gradient(-45deg, transparent 45%, $secondary-border-color 45%, $secondary-border-color 55%, transparent 55%, transparent) +} +.red-ui-search-result-node .red-ui-color-picker-cell-none { + border-radius: 4px; + background-size: 50% 50%; + background-image: linear-gradient(45deg, #eee 25%, transparent 25%, transparent 75%, #eee 75%, #eee), linear-gradient(45deg, #eee 25%, transparent 25%, transparent 75%, #eee 75%, #eee); +} + +.red-ui-color-picker-opacity-slider { + position:relative; + vertical-align: middle; + display: inline-block; + width: calc(100% - 50px); + height: 14px; + margin: 6px 3px 8px; + box-sizing: border-box; + background-color: white; + background-image: + linear-gradient(45deg, #eee 25%, transparent 25%, transparent 75%, #eee 25%), + linear-gradient(-45deg, #eee 25%, transparent 25%, transparent 75%, #eee 25%); + background-size: 6px 6px; +} +.red-ui-color-picker-opacity-slider-overlay { + position: absolute; + top:0;right:0;left:0;bottom:0; + background-image:linear-gradient(90deg, transparent 0%, #f00 100%); + background-size: 100% 100%; + border: 1px solid $primary-border-color; +} + +div.red-ui-button-small.red-ui-color-picker-opacity-slider-handle { + z-Index: 10; + top: -4px; + cursor: pointer; + min-width: 0; + width: 10px; + height: 22px; + padding: 0; + border: 1px solid $primary-border-color; + border-radius: 1px; + background: $secondary-background; + box-sizing: border-box; +} .red-ui-icon-picker { select { box-sizing: border-box; @@ -633,7 +757,7 @@ button.red-ui-toggleButton.toggle { .red-ui-typedInput-value-label,.red-ui-typedInput-option-label { select,.placeholder-input { margin: 3px; - height: 26px; + height: 24px; width: calc(100% - 10px); padding-left: 3px; } diff --git a/packages/node_modules/@node-red/editor-client/src/sass/flow.scss b/packages/node_modules/@node-red/editor-client/src/sass/flow.scss index d92d1e964..60de209cc 100644 --- a/packages/node_modules/@node-red/editor-client/src/sass/flow.scss +++ b/packages/node_modules/@node-red/editor-client/src/sass/flow.scss @@ -71,6 +71,48 @@ } } +.red-ui-flow-group { + &.red-ui-flow-group-hovered { + .red-ui-flow-group-outline-select { + stroke-opacity: 0.8 !important; + stroke-dasharray: 10 4 !important; + } + } + &.red-ui-flow-group-active-hovered:not(.red-ui-flow-group-hovered) { + .red-ui-flow-group-outline-select { + stroke: $link-link-color; + } + } +} + +.red-ui-flow-group-outline { + fill: none; + stroke: $node-selected-color; + stroke-opacity: 0; + stroke-width: 12; + pointer-events: stroke; +} +.red-ui-flow-group-outline-select { + fill: none; + stroke: $node-selected-color; + pointer-events: stroke; + stroke-opacity: 0; + stroke-width: 3; +} +.red-ui-flow-group-body { + pointer-events: none; + fill: $group-default-fill; + fill-opacity: $group-default-fill-opacity; + stroke-width: 2; + stroke: $group-default-stroke; + stroke-opacity: $group-default-stroke-opacity; +} +.red-ui-flow-group-label { + @include disable-selection; +} + + + .red-ui-flow-node-unknown { stroke-dasharray:10,4; stroke: $node-border-unknown; @@ -166,6 +208,9 @@ g.red-ui-flow-node-selected { fill-opacity: 1; stroke-dasharray: none; } + .red-ui-flow-group, .red-ui-flow-group-body { + stroke-dasharray: 8, 3; + } } .red-ui-flow-node-disabled { &.red-ui-flow-node, .red-ui-flow-node { @@ -248,6 +293,7 @@ g.red-ui-flow-node-selected { .red-ui-flow-link-outline { stroke: $view-background; + stroke-opacity: 0.4; stroke-width: 5; cursor: crosshair; fill: none; diff --git a/packages/node_modules/@node-red/editor-client/src/sass/library.scss b/packages/node_modules/@node-red/editor-client/src/sass/library.scss index 60014b2e6..5cff53f44 100644 --- a/packages/node_modules/@node-red/editor-client/src/sass/library.scss +++ b/packages/node_modules/@node-red/editor-client/src/sass/library.scss @@ -37,7 +37,7 @@ border-radius: 4px; font-family: $monospace-font !important; font-size: 13px !important; - height: 300px; + height: 100%; line-height: 1.3em; padding: 6px 10px; background: $clipboard-textarea-background; @@ -62,6 +62,7 @@ background: $form-input-background; &>div { height: 100%; + box-sizing: border-box; } } .red-ui-clipboard-dialog-box { diff --git a/packages/node_modules/@node-red/editor-client/src/sass/palette.scss b/packages/node_modules/@node-red/editor-client/src/sass/palette.scss index ea4b06c52..922a31e33 100644 --- a/packages/node_modules/@node-red/editor-client/src/sass/palette.scss +++ b/packages/node_modules/@node-red/editor-client/src/sass/palette.scss @@ -186,6 +186,21 @@ background-size: contain; background-repeat: no-repeat; } +.red-ui-search-result-node { + &.red-ui-palette-icon-flow, + &.red-ui-palette-icon-group, + &.red-ui-palette-icon-selection { + background: none; + border-color: transparent; + .red-ui-palette-icon-container { + background: none; + } + .red-ui-palette-icon-fa { + color: $secondary-text-color; + font-size: 18px; + } + } +} .red-ui-palette-icon-fa { color: white; position: absolute; diff --git a/packages/node_modules/@node-red/editor-client/src/sass/panels.scss b/packages/node_modules/@node-red/editor-client/src/sass/panels.scss index 9f99db5d4..455aab891 100644 --- a/packages/node_modules/@node-red/editor-client/src/sass/panels.scss +++ b/packages/node_modules/@node-red/editor-client/src/sass/panels.scss @@ -22,9 +22,19 @@ // border: 1px solid red; box-sizing: border-box; } + display: flex; + flex-direction: column; + + >.red-ui-panel:first-child { + flex: 0 0 auto; + } + >.red-ui-panel:last-child { + flex: 1 1 auto; + } } .red-ui-panels-separator { + flex: 0 0 auto; border-top: 1px solid $secondary-border-color; border-bottom: 1px solid $secondary-border-color; height: 7px; @@ -37,10 +47,13 @@ .red-ui-panel { overflow: auto; height: calc(50% - 4px); + position: relative; } .red-ui-panels.red-ui-panels-horizontal { height: 100%; + flex-direction: row; + &>.red-ui-panel { vertical-align: top; display: inline-block; diff --git a/packages/node_modules/@node-red/editor-client/src/sass/popover.scss b/packages/node_modules/@node-red/editor-client/src/sass/popover.scss index 95097a30e..872f32024 100644 --- a/packages/node_modules/@node-red/editor-client/src/sass/popover.scss +++ b/packages/node_modules/@node-red/editor-client/src/sass/popover.scss @@ -150,6 +150,16 @@ .red-ui-popover a.red-ui-button, .red-ui-popover button.red-ui-button { + &:not(.primary) { + border-color: $popover-button-border-color; + background: $popover-background; + color: $popover-color !important; + } + &:not(.primary):not(.disabled):not(.ui-button-disabled):hover { + border-color: $popover-button-border-color-hover; + } + + &.primary { border-color: $popover-button-border-color; } diff --git a/packages/node_modules/@node-red/editor-client/src/sass/projects.scss b/packages/node_modules/@node-red/editor-client/src/sass/projects.scss index 9b1005f22..e08551297 100644 --- a/packages/node_modules/@node-red/editor-client/src/sass/projects.scss +++ b/packages/node_modules/@node-red/editor-client/src/sass/projects.scss @@ -18,7 +18,12 @@ .red-ui-editableList-container { padding: 0px; } - + padding: 0; + .red-ui-projects-dialog-box { + box-sizing: border-box; + overflow-y: auto; + padding: 25px 25px 10px 25px; + } } #red-ui-project-settings-tab-settings { overflow-y: scroll; @@ -99,6 +104,7 @@ .red-ui-projects-dialog-screen-create { min-height: 500px; button.red-ui-projects-dialog-screen-create-type { + position: relative; height: auto; padding: 10px; } @@ -169,9 +175,14 @@ .red-ui-projects-dialog-project-list-container { border: 1px solid $secondary-border-color; border-radius: 2px; + display: flex; + flex-direction: column; + .red-ui-search-container { + flex-grow: 0; + } } .red-ui-projects-dialog-project-list-inner-container { - height: 300px; + flex-grow: 1 ; overflow-y: scroll; position:relative; .red-ui-editableList-border { @@ -254,6 +265,9 @@ } } } +.red-ui-projects-dialog-screen-create-type { + position: relative; +} .red-ui-projects-dialog-screen-create-type.red-ui-button.toggle.selected:not(.disabled):not(:disabled) { color: $secondary-text-color-active !important; } diff --git a/packages/node_modules/@node-red/editor-client/src/sass/search.scss b/packages/node_modules/@node-red/editor-client/src/sass/search.scss index 0ec8b6525..27ebb1a04 100644 --- a/packages/node_modules/@node-red/editor-client/src/sass/search.scss +++ b/packages/node_modules/@node-red/editor-client/src/sass/search.scss @@ -24,6 +24,13 @@ top: 0px; border: 1px solid $primary-border-color; box-shadow: 0 0 10px $shadow; + background: $secondary-background; + + .red-ui-searchBox-container { + display: inline-block; + margin-right: 6px; + width: calc(100% - 30px); + } } .red-ui-type-search { @@ -87,6 +94,8 @@ } .red-ui-palette-icon { width: 15px; + position:relative; + left: -1px; } .red-ui-search-result-description { margin-left:28px; @@ -153,7 +162,7 @@ width: 30px; float:left; height: 25px; - border-radius: 5px; + border-radius: 3px; border: 1px solid $node-border; background-position: 5% 50%; background-repeat: no-repeat; diff --git a/packages/node_modules/@node-red/editor-client/src/sass/style.scss b/packages/node_modules/@node-red/editor-client/src/sass/style.scss index 088e5c1b8..ca572ea46 100644 --- a/packages/node_modules/@node-red/editor-client/src/sass/style.scss +++ b/packages/node_modules/@node-red/editor-client/src/sass/style.scss @@ -42,6 +42,7 @@ @import "tab-config"; @import "tab-context"; @import "tab-info"; +@import "tab-help"; @import "popover"; @import "flow"; @import "palette-editor"; diff --git a/packages/node_modules/@node-red/editor-client/src/sass/tab-context.scss b/packages/node_modules/@node-red/editor-client/src/sass/tab-context.scss index 46b09de43..4be9761f7 100644 --- a/packages/node_modules/@node-red/editor-client/src/sass/tab-context.scss +++ b/packages/node_modules/@node-red/editor-client/src/sass/tab-context.scss @@ -27,9 +27,22 @@ display: none; } } + + .red-ui-info-table { + table-layout: fixed; + } + + table.red-ui-info-table tr:not(.blank) td:first-child { + width: 30%; + } + table.red-ui-info-table tr:not(.blank) td:last-child { + vertical-align: top; + } + } .red-ui-sidebar-context-property { + overflow-wrap: break-word; position: relative; .red-ui-debug-msg-tools { right: 0px; diff --git a/packages/node_modules/@node-red/editor-client/src/sass/tab-help.scss b/packages/node_modules/@node-red/editor-client/src/sass/tab-help.scss new file mode 100644 index 000000000..fe4f9fb84 --- /dev/null +++ b/packages/node_modules/@node-red/editor-client/src/sass/tab-help.scss @@ -0,0 +1,27 @@ +.red-ui-sidebar-help-stack { + // height: calc(100% - 39px); +} +.red-ui-help-search { + border-bottom: 1px solid $secondary-border-color; +} + +.red-ui-sidebar-help-toc { + .red-ui-treeList-label { + font-size: 13px; + padding: 2px 0; + overflow: hidden; + white-space: nowrap; + } + +} +#red-ui-sidebar-help-show-toc { + i.fa-angle-right { + transition: all 0.2s ease-in-out; + } + &.selected { + i.fa-angle-right { + transform: rotate(90deg); + } + } + +} diff --git a/packages/node_modules/@node-red/editor-client/src/sass/tab-info.scss b/packages/node_modules/@node-red/editor-client/src/sass/tab-info.scss index bc72f7532..c98bdb6ad 100644 --- a/packages/node_modules/@node-red/editor-client/src/sass/tab-info.scss +++ b/packages/node_modules/@node-red/editor-client/src/sass/tab-info.scss @@ -14,9 +14,25 @@ * limitations under the License. **/ +.red-ui-sidebar-info { + height: 100%; +} .red-ui-sidebar-info hr { margin: 10px 0; } +.red-ui-info-header { + padding-left: 9px; + line-height: 21px; + cursor: default; + > * { + vertical-align: middle + } + > span { + display: inline-block; + margin-left: 5px; + } + border-bottom: 1px solid $secondary-border-color; +} table.red-ui-info-table { font-size: 14px; margin: 0 0 10px; @@ -125,6 +141,9 @@ div.red-ui-info-table { font-size: 1.296em; line-height: 1.3em; margin: 8px auto; + &.red-ui-help-title { + border-bottom: 1px solid $tertiary-border-color; + } } h2 { font-weight: 500; @@ -214,12 +233,13 @@ div.red-ui-info-table { } .red-ui-sidebar-info-stack { - position: absolute; - top: 0; - bottom: 0; - left: 0; - right: 0; - overflow-y: scroll; + height: 100%; + // position: absolute; + // top: 0; + // bottom: 0; + // left: 0; + // right: 0; + // overflow-y: scroll; } .red-ui-help-tips { display: none; @@ -227,20 +247,23 @@ div.red-ui-info-table { left:0; right:0; bottom: 0; - height: 150px; + height: 0; + transition: height 0.2s, padding 0.2s; box-sizing: border-box; border-top: 1px solid $secondary-border-color; background-color: $secondary-background; - padding: 20px; + padding: 0; box-shadow: 0 5px 20px 0px $shadow; overflow-y: auto; } .red-ui-sidebar-info.show-tips { .red-ui-sidebar-info-stack { - bottom: 150px; + height: calc(100% - 150px); } .red-ui-help-tips { display: block; + height: 150px; + padding: 20px; } } @@ -279,3 +302,220 @@ div.red-ui-info-table { border-radius: 4px; padding: 2px 4px 2px; } + +.red-ui-info-outline,.red-ui-sidebar-help-toc { + display: flex; + flex-direction: column; + + .red-ui-treeList { + flex-grow: 1; + position: relative; + } + .red-ui-treeList-container { + position: absolute; + top: 0; + bottom: 0; + } + + .red-ui-treeList-container,.red-ui-editableList-border { + border: none; + border-radius: 0; + } + .red-ui-treeList-label { + font-size: 13px; + padding: 2px 0; + overflow: hidden; + } + .red-ui-info-outline-project { + border-bottom: 1px solid $secondary-border-color; + } + + .red-ui-info-outline-item { + display: inline-block; + padding: 0; + font-size: 13px; + border: none; + .red-ui-palette-icon-fa { + position: relative; + top: 1px; + left: 0px; + } + &:hover { + background: inherit + } + + &.red-ui-info-outline-item-flow { + .red-ui-search-result-description { + margin-left: 4px; + } + } + &.red-ui-info-outline-item-group .red-ui-search-result-node { + background: none; + border-color: transparent; + .red-ui-palette-icon-container { + background: none; + } + .red-ui-palette-icon-fa { + color: $secondary-text-color; + font-size: 18px; + } + } + &.red-ui-info-outline-item-empty { + font-style: italic; + color: $form-placeholder-color; + } + } + + .red-ui-search-result-node { + width: 24px; + height: 20px; + margin-top: 1px; + } + + .red-ui-palette-icon-container { + width: 24px; + } + .red-ui-palette-icon { + width: 20px; + } + .red-ui-search-result-description { + margin-left: 32px; + line-height: 22px; + white-space: nowrap; + } + .red-ui-search-result-node-label { + color: $secondary-text-color; + } +} +.red-ui-info-outline-item-control-spacer { + display: inline-block; + width: 23px; +} +.red-ui-info-outline-gutter { + display:none; + button { + position: absolute; + top: 1px; + left: 2px; + } + .red-ui-treeList-label:hover & { + display: inline; + } +} +.red-ui-info-outline-item-controls { + position: absolute; + top:0; + bottom: 0; + right: 0px; + padding: 2px 3px 0 1px; + text-align: right; + background: $list-item-background; + + .red-ui-treeList-label:hover & { + background: $list-item-background-hover; + } + .red-ui-treeList-label.selected & { + background: $list-item-background-selected; + } + + + &.red-ui-info-outline-item-hover-controls button { + min-width: 23px; + } + + .red-ui-treeList-label:not(:hover) &.red-ui-info-outline-item-hover-controls { + button { + border: none; + background: none; + } + } + .red-ui-info-outline-item-control-reveal, + .red-ui-info-outline-item-control-action { + display: none; + } + .red-ui-treeList-label:hover & { + .red-ui-info-outline-item-control-reveal, + .red-ui-info-outline-item-control-action { + display: inline-block; + } + } + + .fa-ban { + display: none; + } + .red-ui-info-outline-item.red-ui-info-outline-item-disabled & { + .fa-ban { + display: inline-block; + } + .fa-circle-thin { + display: none; + } + } + button { + margin-right: 3px + } +} +.red-ui-info-outline-item-disabled { + .red-ui-search-result-node { + opacity: 0.4; + } + .red-ui-info-outline-item-label { + font-style: italic; + color: $secondary-text-color-disabled; + } + .red-ui-icons-flow { + opacity: 0.4; + } +} + + + +.red-ui-icons { + display: inline-block; + width: 18px; + &:before { + white-space: pre; + content: ' ' + } + +} + +.red-ui-icons-flow { + background-image: url('images/subflow_tab.svg'); + background-repeat: no-repeat; + background-size: contain; + filter: brightness(2.5); +} + +.red-ui-info-toolbar { + min-height: 39px; + height: 39px; + box-sizing: border-box; + text-align: left; + // padding-left: 9px; + // box-sizing: border-box; + // background: $palette-header-background; + // border-bottom: 1px solid $secondary-border-color; + + .red-ui-searchBox-container { + position: absolute; + top: 6px; + right: 8px; + width: calc(100% - 150px); + max-width: 250px; + background: $palette-header-background; + input.red-ui-searchBox-input { + border: 1px solid $secondary-border-color; + border-radius: 3px; + font-size: 12px; + height: 26px; + } + input:focus.red-ui-searchBox-input { + border: 1px solid $secondary-border-color; + } + i.fa-search, i.fa-times { + top: 7px; + } + } + +} diff --git a/packages/node_modules/@node-red/editor-client/src/sass/ui/common/searchBox.scss b/packages/node_modules/@node-red/editor-client/src/sass/ui/common/searchBox.scss index 2f0740d61..d0cd631fc 100644 --- a/packages/node_modules/@node-red/editor-client/src/sass/ui/common/searchBox.scss +++ b/packages/node_modules/@node-red/editor-client/src/sass/ui/common/searchBox.scss @@ -32,6 +32,9 @@ right: 5px; top: 9px; } + form.red-ui-searchBox-form { + margin: 0; + } input.red-ui-searchBox-input { border-radius: 0; border: none; diff --git a/packages/node_modules/@node-red/editor-client/src/sass/ui/common/typedInput.scss b/packages/node_modules/@node-red/editor-client/src/sass/ui/common/typedInput.scss index be2d50674..ec865b116 100644 --- a/packages/node_modules/@node-red/editor-client/src/sass/ui/common/typedInput.scss +++ b/packages/node_modules/@node-red/editor-client/src/sass/ui/common/typedInput.scss @@ -18,7 +18,7 @@ border: 1px solid $form-input-border-color; border-radius: 4px; height: 34px; - display: inline-block; + display: inline-flex; padding: 0; margin: 0; vertical-align: middle; @@ -26,12 +26,7 @@ overflow:visible; position: relative; .red-ui-typedInput-input-wrap { - position: absolute; - left:0; - right:0; - top:0; - bottom:0; - outline: red; + flex-grow: 1; } input.red-ui-typedInput-input { width: 100%; @@ -49,14 +44,17 @@ border-color: $form-input-focus-color !important; } .red-ui-typedInput-value-label { - position: absolute; + flex-grow: 1; display: inline-block; height: 32px; box-sizing: border-box; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; - + .red-ui-typedInput-value-label-inactive { + background: $secondary-background-disabled; + color: $secondary-text-color-disabled; + } } } .red-ui-typedInput-options { @@ -104,18 +102,17 @@ button.red-ui-typedInput-option-trigger { text-align: left; border: none; - position: absolute; + flex-basis: auto; box-sizing: border-box; border-top-left-radius: 4px; border-bottom-left-radius: 4px; padding: 0 1px 0 5px; - display:inline-block; background: $form-button-background; height: 32px; line-height: 30px; - min-width: 23px; vertical-align: middle; color: $form-text-color; + white-space: nowrap; i.red-ui-typedInput-icon { margin-left: 1px; margin-right: 2px; @@ -123,7 +120,7 @@ button.red-ui-typedInput-option-trigger } &.disabled { cursor: default; - i.red-ui-typedInput-icon { + > i.red-ui-typedInput-icon { color: $secondary-text-color-disabled; } } @@ -151,7 +148,7 @@ button.red-ui-typedInput-option-trigger text-decoration: none; } &.red-ui-typedInput-full-width { - width: 100%; + flex-grow: 1; border-top-right-radius: 4px; border-bottom-right-radius: 4px; } @@ -167,7 +164,6 @@ button.red-ui-typedInput-option-expand { border-bottom-right-radius: 4px; border-top-left-radius: 0; border-bottom-left-radius: 0; - right: 0; } button.red-ui-typedInput-option-trigger { @@ -176,27 +172,23 @@ button.red-ui-typedInput-option-trigger { border-top-right-radius: 4px; border-bottom-right-radius: 4px; padding: 0 0 0 0; - position:absolute; - right: 0; + position:relative; + flex-grow: 1; + line-height: 32px; + display: inline-flex; .red-ui-typedInput-option-label { background:$form-button-background; color: $form-text-color; - position:absolute; - left:0; - right:23px; - top: 0; - padding: 0 5px 0 8px; - i.red-ui-typedInput-icon { - margin-right: 4px; - } + flex-grow: 1; + padding: 0 0 0 8px; + display:inline-block; } .red-ui-typedInput-option-caret { - top: 0; - position: absolute; - right: 0; - bottom: 0; - width: 17px; - padding-left: 5px; + flex-grow: 0; + display:inline-block; + width: 23px; + text-align: center; + height: 100%; &:before { content:''; display: inline-block; diff --git a/packages/node_modules/@node-red/editor-client/src/sass/workspace.scss b/packages/node_modules/@node-red/editor-client/src/sass/workspace.scss index f6255eacf..2adfb89ba 100644 --- a/packages/node_modules/@node-red/editor-client/src/sass/workspace.scss +++ b/packages/node_modules/@node-red/editor-client/src/sass/workspace.scss @@ -112,7 +112,7 @@ position: absolute; bottom: 0; right:0; - zIndex: 101; + z-index: 101; border-left: 1px solid $primary-border-color; border-top: 1px solid $primary-border-color; background: $view-navigator-background; @@ -122,7 +122,7 @@ stroke-dasharray: 5,5; pointer-events: none; stroke: $secondary-border-color; - strokeWidth: 1; + stroke-width: 1; fill: $view-background; } @@ -172,3 +172,44 @@ button.red-ui-footer-button-toggle { margin-right: 0; } } + + +#red-ui-loading-progress { + position: absolute; + background: $primary-background; + top: 0; + bottom: 0; + right: 0; + left: 0; + z-index: 200; + & > div { + position: absolute; + top: 30%; + left: 50%; + transform: translate(-50%, -50%); + width: 300px; + height:80px; + text-align: center; + color: $secondary-text-color; + + } +} +.red-ui-loading-bar { + box-sizing: border-box; + width: 300px; + height: 30px; + border: 2px solid $primary-border-color; + border-radius: 4px; + + > span { + display: block; + height: 100%; + background: $secondary-border-color; + transition: width 0.2s; + width: 10%; + } +} +.red-ui-loading-bar-label { + font-size: 13px; + margin-bottom: 2px; +} diff --git a/packages/node_modules/@node-red/editor-client/src/vendor/jquery/js/jquery-3.4.1.min.js b/packages/node_modules/@node-red/editor-client/src/vendor/jquery/js/jquery-3.4.1.min.js deleted file mode 100644 index a1c07fd80..000000000 --- a/packages/node_modules/@node-red/editor-client/src/vendor/jquery/js/jquery-3.4.1.min.js +++ /dev/null @@ -1,2 +0,0 @@ -/*! jQuery v3.4.1 | (c) JS Foundation and other contributors | jquery.org/license */ -!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(C,e){"use strict";var t=[],E=C.document,r=Object.getPrototypeOf,s=t.slice,g=t.concat,u=t.push,i=t.indexOf,n={},o=n.toString,v=n.hasOwnProperty,a=v.toString,l=a.call(Object),y={},m=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType},x=function(e){return null!=e&&e===e.window},c={type:!0,src:!0,nonce:!0,noModule:!0};function b(e,t,n){var r,i,o=(n=n||E).createElement("script");if(o.text=e,t)for(r in c)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function w(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?n[o.call(e)]||"object":typeof e}var f="3.4.1",k=function(e,t){return new k.fn.init(e,t)},p=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g;function d(e){var t=!!e&&"length"in e&&e.length,n=w(e);return!m(e)&&!x(e)&&("array"===n||0===t||"number"==typeof t&&0+~]|"+M+")"+M+"*"),U=new RegExp(M+"|>"),X=new RegExp($),V=new RegExp("^"+I+"$"),G={ID:new RegExp("^#("+I+")"),CLASS:new RegExp("^\\.("+I+")"),TAG:new RegExp("^("+I+"|[*])"),ATTR:new RegExp("^"+W),PSEUDO:new RegExp("^"+$),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+R+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/HTML$/i,Q=/^(?:input|select|textarea|button)$/i,J=/^h\d$/i,K=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ee=/[+~]/,te=new RegExp("\\\\([\\da-f]{1,6}"+M+"?|("+M+")|.)","ig"),ne=function(e,t,n){var r="0x"+t-65536;return r!=r||n?t:r<0?String.fromCharCode(r+65536):String.fromCharCode(r>>10|55296,1023&r|56320)},re=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ie=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},oe=function(){T()},ae=be(function(e){return!0===e.disabled&&"fieldset"===e.nodeName.toLowerCase()},{dir:"parentNode",next:"legend"});try{H.apply(t=O.call(m.childNodes),m.childNodes),t[m.childNodes.length].nodeType}catch(e){H={apply:t.length?function(e,t){L.apply(e,O.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function se(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,p=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==p&&9!==p&&11!==p)return n;if(!r&&((e?e.ownerDocument||e:m)!==C&&T(e),e=e||C,E)){if(11!==p&&(u=Z.exec(t)))if(i=u[1]){if(9===p){if(!(a=e.getElementById(i)))return n;if(a.id===i)return n.push(a),n}else if(f&&(a=f.getElementById(i))&&y(e,a)&&a.id===i)return n.push(a),n}else{if(u[2])return H.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&d.getElementsByClassName&&e.getElementsByClassName)return H.apply(n,e.getElementsByClassName(i)),n}if(d.qsa&&!A[t+" "]&&(!v||!v.test(t))&&(1!==p||"object"!==e.nodeName.toLowerCase())){if(c=t,f=e,1===p&&U.test(t)){(s=e.getAttribute("id"))?s=s.replace(re,ie):e.setAttribute("id",s=k),o=(l=h(t)).length;while(o--)l[o]="#"+s+" "+xe(l[o]);c=l.join(","),f=ee.test(t)&&ye(e.parentNode)||e}try{return H.apply(n,f.querySelectorAll(c)),n}catch(e){A(t,!0)}finally{s===k&&e.removeAttribute("id")}}}return g(t.replace(B,"$1"),e,n,r)}function ue(){var r=[];return function e(t,n){return r.push(t+" ")>b.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function le(e){return e[k]=!0,e}function ce(e){var t=C.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function fe(e,t){var n=e.split("|"),r=n.length;while(r--)b.attrHandle[n[r]]=t}function pe(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function de(t){return function(e){return"input"===e.nodeName.toLowerCase()&&e.type===t}}function he(n){return function(e){var t=e.nodeName.toLowerCase();return("input"===t||"button"===t)&&e.type===n}}function ge(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&ae(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function ve(a){return le(function(o){return o=+o,le(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function ye(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}for(e in d=se.support={},i=se.isXML=function(e){var t=e.namespaceURI,n=(e.ownerDocument||e).documentElement;return!Y.test(t||n&&n.nodeName||"HTML")},T=se.setDocument=function(e){var t,n,r=e?e.ownerDocument||e:m;return r!==C&&9===r.nodeType&&r.documentElement&&(a=(C=r).documentElement,E=!i(C),m!==C&&(n=C.defaultView)&&n.top!==n&&(n.addEventListener?n.addEventListener("unload",oe,!1):n.attachEvent&&n.attachEvent("onunload",oe)),d.attributes=ce(function(e){return e.className="i",!e.getAttribute("className")}),d.getElementsByTagName=ce(function(e){return e.appendChild(C.createComment("")),!e.getElementsByTagName("*").length}),d.getElementsByClassName=K.test(C.getElementsByClassName),d.getById=ce(function(e){return a.appendChild(e).id=k,!C.getElementsByName||!C.getElementsByName(k).length}),d.getById?(b.filter.ID=function(e){var t=e.replace(te,ne);return function(e){return e.getAttribute("id")===t}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n=t.getElementById(e);return n?[n]:[]}}):(b.filter.ID=function(e){var n=e.replace(te,ne);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),b.find.TAG=d.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):d.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},b.find.CLASS=d.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&E)return t.getElementsByClassName(e)},s=[],v=[],(d.qsa=K.test(C.querySelectorAll))&&(ce(function(e){a.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&v.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||v.push("\\["+M+"*(?:value|"+R+")"),e.querySelectorAll("[id~="+k+"-]").length||v.push("~="),e.querySelectorAll(":checked").length||v.push(":checked"),e.querySelectorAll("a#"+k+"+*").length||v.push(".#.+[+~]")}),ce(function(e){e.innerHTML="";var t=C.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&v.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&v.push(":enabled",":disabled"),a.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&v.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),v.push(",.*:")})),(d.matchesSelector=K.test(c=a.matches||a.webkitMatchesSelector||a.mozMatchesSelector||a.oMatchesSelector||a.msMatchesSelector))&&ce(function(e){d.disconnectedMatch=c.call(e,"*"),c.call(e,"[s!='']:x"),s.push("!=",$)}),v=v.length&&new RegExp(v.join("|")),s=s.length&&new RegExp(s.join("|")),t=K.test(a.compareDocumentPosition),y=t||K.test(a.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},D=t?function(e,t){if(e===t)return l=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)===(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!d.sortDetached&&t.compareDocumentPosition(e)===n?e===C||e.ownerDocument===m&&y(m,e)?-1:t===C||t.ownerDocument===m&&y(m,t)?1:u?P(u,e)-P(u,t):0:4&n?-1:1)}:function(e,t){if(e===t)return l=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e===C?-1:t===C?1:i?-1:o?1:u?P(u,e)-P(u,t):0;if(i===o)return pe(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?pe(a[r],s[r]):a[r]===m?-1:s[r]===m?1:0}),C},se.matches=function(e,t){return se(e,null,null,t)},se.matchesSelector=function(e,t){if((e.ownerDocument||e)!==C&&T(e),d.matchesSelector&&E&&!A[t+" "]&&(!s||!s.test(t))&&(!v||!v.test(t)))try{var n=c.call(e,t);if(n||d.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){A(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(te,ne),e[3]=(e[3]||e[4]||e[5]||"").replace(te,ne),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||se.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&se.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return G.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=h(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(te,ne).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=p[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&p(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=se.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function j(e,n,r){return m(n)?k.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?k.grep(e,function(e){return e===n!==r}):"string"!=typeof n?k.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(k.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||q,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:L.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof k?t[0]:t,k.merge(this,k.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:E,!0)),D.test(r[1])&&k.isPlainObject(t))for(r in t)m(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=E.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):m(e)?void 0!==n.ready?n.ready(e):e(k):k.makeArray(e,this)}).prototype=k.fn,q=k(E);var H=/^(?:parents|prev(?:Until|All))/,O={children:!0,contents:!0,next:!0,prev:!0};function P(e,t){while((e=e[t])&&1!==e.nodeType);return e}k.fn.extend({has:function(e){var t=k(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,he=/^$|^module$|\/(?:java|ecma)script/i,ge={option:[1,""],thead:[1,"","
        "],col:[2,"","
        "],tr:[2,"","
        "],td:[3,"","
        "],_default:[0,"",""]};function ve(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&A(e,t)?k.merge([e],n):n}function ye(e,t){for(var n=0,r=e.length;nx",y.noCloneChecked=!!me.cloneNode(!0).lastChild.defaultValue;var Te=/^key/,Ce=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,Ee=/^([^.]*)(?:\.(.+)|)/;function ke(){return!0}function Se(){return!1}function Ne(e,t){return e===function(){try{return E.activeElement}catch(e){}}()==("focus"===t)}function Ae(e,t,n,r,i,o){var a,s;if("object"==typeof t){for(s in"string"!=typeof n&&(r=r||n,n=void 0),t)Ae(e,s,n,r,t[s],o);return e}if(null==r&&null==i?(i=n,r=n=void 0):null==i&&("string"==typeof n?(i=r,r=void 0):(i=r,r=n,n=void 0)),!1===i)i=Se;else if(!i)return e;return 1===o&&(a=i,(i=function(e){return k().off(e),a.apply(this,arguments)}).guid=a.guid||(a.guid=k.guid++)),e.each(function(){k.event.add(this,t,i,r,n)})}function De(e,i,o){o?(Q.set(e,i,!1),k.event.add(e,i,{namespace:!1,handler:function(e){var t,n,r=Q.get(this,i);if(1&e.isTrigger&&this[i]){if(r.length)(k.event.special[i]||{}).delegateType&&e.stopPropagation();else if(r=s.call(arguments),Q.set(this,i,r),t=o(this,i),this[i](),r!==(n=Q.get(this,i))||t?Q.set(this,i,!1):n={},r!==n)return e.stopImmediatePropagation(),e.preventDefault(),n.value}else r.length&&(Q.set(this,i,{value:k.event.trigger(k.extend(r[0],k.Event.prototype),r.slice(1),this)}),e.stopImmediatePropagation())}})):void 0===Q.get(e,i)&&k.event.add(e,i,ke)}k.event={global:{},add:function(t,e,n,r,i){var o,a,s,u,l,c,f,p,d,h,g,v=Q.get(t);if(v){n.handler&&(n=(o=n).handler,i=o.selector),i&&k.find.matchesSelector(ie,i),n.guid||(n.guid=k.guid++),(u=v.events)||(u=v.events={}),(a=v.handle)||(a=v.handle=function(e){return"undefined"!=typeof k&&k.event.triggered!==e.type?k.event.dispatch.apply(t,arguments):void 0}),l=(e=(e||"").match(R)||[""]).length;while(l--)d=g=(s=Ee.exec(e[l])||[])[1],h=(s[2]||"").split(".").sort(),d&&(f=k.event.special[d]||{},d=(i?f.delegateType:f.bindType)||d,f=k.event.special[d]||{},c=k.extend({type:d,origType:g,data:r,handler:n,guid:n.guid,selector:i,needsContext:i&&k.expr.match.needsContext.test(i),namespace:h.join(".")},o),(p=u[d])||((p=u[d]=[]).delegateCount=0,f.setup&&!1!==f.setup.call(t,r,h,a)||t.addEventListener&&t.addEventListener(d,a)),f.add&&(f.add.call(t,c),c.handler.guid||(c.handler.guid=n.guid)),i?p.splice(p.delegateCount++,0,c):p.push(c),k.event.global[d]=!0)}},remove:function(e,t,n,r,i){var o,a,s,u,l,c,f,p,d,h,g,v=Q.hasData(e)&&Q.get(e);if(v&&(u=v.events)){l=(t=(t||"").match(R)||[""]).length;while(l--)if(d=g=(s=Ee.exec(t[l])||[])[1],h=(s[2]||"").split(".").sort(),d){f=k.event.special[d]||{},p=u[d=(r?f.delegateType:f.bindType)||d]||[],s=s[2]&&new RegExp("(^|\\.)"+h.join("\\.(?:.*\\.|)")+"(\\.|$)"),a=o=p.length;while(o--)c=p[o],!i&&g!==c.origType||n&&n.guid!==c.guid||s&&!s.test(c.namespace)||r&&r!==c.selector&&("**"!==r||!c.selector)||(p.splice(o,1),c.selector&&p.delegateCount--,f.remove&&f.remove.call(e,c));a&&!p.length&&(f.teardown&&!1!==f.teardown.call(e,h,v.handle)||k.removeEvent(e,d,v.handle),delete u[d])}else for(d in u)k.event.remove(e,d+t[l],n,r,!0);k.isEmptyObject(u)&&Q.remove(e,"handle events")}},dispatch:function(e){var t,n,r,i,o,a,s=k.event.fix(e),u=new Array(arguments.length),l=(Q.get(this,"events")||{})[s.type]||[],c=k.event.special[s.type]||{};for(u[0]=s,t=1;t\x20\t\r\n\f]*)[^>]*)\/>/gi,qe=/\s*$/g;function Oe(e,t){return A(e,"table")&&A(11!==t.nodeType?t:t.firstChild,"tr")&&k(e).children("tbody")[0]||e}function Pe(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function Re(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Me(e,t){var n,r,i,o,a,s,u,l;if(1===t.nodeType){if(Q.hasData(e)&&(o=Q.access(e),a=Q.set(t,o),l=o.events))for(i in delete a.handle,a.events={},l)for(n=0,r=l[i].length;n")},clone:function(e,t,n){var r,i,o,a,s,u,l,c=e.cloneNode(!0),f=oe(e);if(!(y.noCloneChecked||1!==e.nodeType&&11!==e.nodeType||k.isXMLDoc(e)))for(a=ve(c),r=0,i=(o=ve(e)).length;r").attr(n.scriptAttrs||{}).prop({charset:n.scriptCharset,src:n.url}).on("load error",i=function(e){r.remove(),i=null,e&&t("error"===e.type?404:200,e.type)}),E.head.appendChild(r[0])},abort:function(){i&&i()}}});var Vt,Gt=[],Yt=/(=)\?(?=&|$)|\?\?/;k.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=Gt.pop()||k.expando+"_"+kt++;return this[e]=!0,e}}),k.ajaxPrefilter("json jsonp",function(e,t,n){var r,i,o,a=!1!==e.jsonp&&(Yt.test(e.url)?"url":"string"==typeof e.data&&0===(e.contentType||"").indexOf("application/x-www-form-urlencoded")&&Yt.test(e.data)&&"data");if(a||"jsonp"===e.dataTypes[0])return r=e.jsonpCallback=m(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(Yt,"$1"+r):!1!==e.jsonp&&(e.url+=(St.test(e.url)?"&":"?")+e.jsonp+"="+r),e.converters["script json"]=function(){return o||k.error(r+" was not called"),o[0]},e.dataTypes[0]="json",i=C[r],C[r]=function(){o=arguments},n.always(function(){void 0===i?k(C).removeProp(r):C[r]=i,e[r]&&(e.jsonpCallback=t.jsonpCallback,Gt.push(r)),o&&m(i)&&i(o[0]),o=i=void 0}),"script"}),y.createHTMLDocument=((Vt=E.implementation.createHTMLDocument("").body).innerHTML="
        ",2===Vt.childNodes.length),k.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(y.createHTMLDocument?((r=(t=E.implementation.createHTMLDocument("")).createElement("base")).href=E.location.href,t.head.appendChild(r)):t=E),o=!n&&[],(i=D.exec(e))?[t.createElement(i[1])]:(i=we([e],t,o),o&&o.length&&k(o).remove(),k.merge([],i.childNodes)));var r,i,o},k.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(" ");return-1").append(k.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},k.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){k.fn[t]=function(e){return this.on(t,e)}}),k.expr.pseudos.animated=function(t){return k.grep(k.timers,function(e){return t===e.elem}).length},k.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=k.css(e,"position"),c=k(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=k.css(e,"top"),u=k.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),m(t)&&(t=t.call(e,n,k.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):c.css(f)}},k.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){k.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===k.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===k.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=k(e).offset()).top+=k.css(e,"borderTopWidth",!0),i.left+=k.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-k.css(r,"marginTop",!0),left:t.left-i.left-k.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===k.css(e,"position"))e=e.offsetParent;return e||ie})}}),k.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;k.fn[t]=function(e){return _(this,function(e,t,n){var r;if(x(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),k.each(["top","left"],function(e,n){k.cssHooks[n]=ze(y.pixelPosition,function(e,t){if(t)return t=_e(e,n),$e.test(t)?k(e).position()[n]+"px":t})}),k.each({Height:"height",Width:"width"},function(a,s){k.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){k.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return _(this,function(e,t,n){var r;return x(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?k.css(e,t,i):k.style(e,t,n,i)},s,n?e:void 0,n)}})}),k.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){k.fn[n]=function(e,t){return 0+~]|"+M+")"+M+"*"),U=new RegExp(M+"|>"),X=new RegExp(F),V=new RegExp("^"+I+"$"),G={ID:new RegExp("^#("+I+")"),CLASS:new RegExp("^\\.("+I+")"),TAG:new RegExp("^("+I+"|[*])"),ATTR:new RegExp("^"+W),PSEUDO:new RegExp("^"+F),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+R+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/HTML$/i,Q=/^(?:input|select|textarea|button)$/i,J=/^h\d$/i,K=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ee=/[+~]/,te=new RegExp("\\\\[\\da-fA-F]{1,6}"+M+"?|\\\\([^\\r\\n\\f])","g"),ne=function(e,t){var n="0x"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},re=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ie=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},oe=function(){T()},ae=be(function(e){return!0===e.disabled&&"fieldset"===e.nodeName.toLowerCase()},{dir:"parentNode",next:"legend"});try{H.apply(t=O.call(p.childNodes),p.childNodes),t[p.childNodes.length].nodeType}catch(e){H={apply:t.length?function(e,t){L.apply(e,O.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function se(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,p=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==p&&9!==p&&11!==p)return n;if(!r&&(T(e),e=e||C,E)){if(11!==p&&(u=Z.exec(t)))if(i=u[1]){if(9===p){if(!(a=e.getElementById(i)))return n;if(a.id===i)return n.push(a),n}else if(f&&(a=f.getElementById(i))&&y(e,a)&&a.id===i)return n.push(a),n}else{if(u[2])return H.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&d.getElementsByClassName&&e.getElementsByClassName)return H.apply(n,e.getElementsByClassName(i)),n}if(d.qsa&&!N[t+" "]&&(!v||!v.test(t))&&(1!==p||"object"!==e.nodeName.toLowerCase())){if(c=t,f=e,1===p&&(U.test(t)||z.test(t))){(f=ee.test(t)&&ye(e.parentNode)||e)===e&&d.scope||((s=e.getAttribute("id"))?s=s.replace(re,ie):e.setAttribute("id",s=S)),o=(l=h(t)).length;while(o--)l[o]=(s?"#"+s:":scope")+" "+xe(l[o]);c=l.join(",")}try{return H.apply(n,f.querySelectorAll(c)),n}catch(e){N(t,!0)}finally{s===S&&e.removeAttribute("id")}}}return g(t.replace($,"$1"),e,n,r)}function ue(){var r=[];return function e(t,n){return r.push(t+" ")>b.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function le(e){return e[S]=!0,e}function ce(e){var t=C.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function fe(e,t){var n=e.split("|"),r=n.length;while(r--)b.attrHandle[n[r]]=t}function pe(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function de(t){return function(e){return"input"===e.nodeName.toLowerCase()&&e.type===t}}function he(n){return function(e){var t=e.nodeName.toLowerCase();return("input"===t||"button"===t)&&e.type===n}}function ge(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&ae(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function ve(a){return le(function(o){return o=+o,le(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function ye(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}for(e in d=se.support={},i=se.isXML=function(e){var t=e.namespaceURI,n=(e.ownerDocument||e).documentElement;return!Y.test(t||n&&n.nodeName||"HTML")},T=se.setDocument=function(e){var t,n,r=e?e.ownerDocument||e:p;return r!=C&&9===r.nodeType&&r.documentElement&&(a=(C=r).documentElement,E=!i(C),p!=C&&(n=C.defaultView)&&n.top!==n&&(n.addEventListener?n.addEventListener("unload",oe,!1):n.attachEvent&&n.attachEvent("onunload",oe)),d.scope=ce(function(e){return a.appendChild(e).appendChild(C.createElement("div")),"undefined"!=typeof e.querySelectorAll&&!e.querySelectorAll(":scope fieldset div").length}),d.attributes=ce(function(e){return e.className="i",!e.getAttribute("className")}),d.getElementsByTagName=ce(function(e){return e.appendChild(C.createComment("")),!e.getElementsByTagName("*").length}),d.getElementsByClassName=K.test(C.getElementsByClassName),d.getById=ce(function(e){return a.appendChild(e).id=S,!C.getElementsByName||!C.getElementsByName(S).length}),d.getById?(b.filter.ID=function(e){var t=e.replace(te,ne);return function(e){return e.getAttribute("id")===t}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n=t.getElementById(e);return n?[n]:[]}}):(b.filter.ID=function(e){var n=e.replace(te,ne);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),b.find.TAG=d.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):d.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},b.find.CLASS=d.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&E)return t.getElementsByClassName(e)},s=[],v=[],(d.qsa=K.test(C.querySelectorAll))&&(ce(function(e){var t;a.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&v.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||v.push("\\["+M+"*(?:value|"+R+")"),e.querySelectorAll("[id~="+S+"-]").length||v.push("~="),(t=C.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||v.push("\\["+M+"*name"+M+"*="+M+"*(?:''|\"\")"),e.querySelectorAll(":checked").length||v.push(":checked"),e.querySelectorAll("a#"+S+"+*").length||v.push(".#.+[+~]"),e.querySelectorAll("\\\f"),v.push("[\\r\\n\\f]")}),ce(function(e){e.innerHTML="
        ";var t=C.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&v.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&v.push(":enabled",":disabled"),a.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&v.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),v.push(",.*:")})),(d.matchesSelector=K.test(c=a.matches||a.webkitMatchesSelector||a.mozMatchesSelector||a.oMatchesSelector||a.msMatchesSelector))&&ce(function(e){d.disconnectedMatch=c.call(e,"*"),c.call(e,"[s!='']:x"),s.push("!=",F)}),v=v.length&&new RegExp(v.join("|")),s=s.length&&new RegExp(s.join("|")),t=K.test(a.compareDocumentPosition),y=t||K.test(a.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},D=t?function(e,t){if(e===t)return l=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!d.sortDetached&&t.compareDocumentPosition(e)===n?e==C||e.ownerDocument==p&&y(p,e)?-1:t==C||t.ownerDocument==p&&y(p,t)?1:u?P(u,e)-P(u,t):0:4&n?-1:1)}:function(e,t){if(e===t)return l=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e==C?-1:t==C?1:i?-1:o?1:u?P(u,e)-P(u,t):0;if(i===o)return pe(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?pe(a[r],s[r]):a[r]==p?-1:s[r]==p?1:0}),C},se.matches=function(e,t){return se(e,null,null,t)},se.matchesSelector=function(e,t){if(T(e),d.matchesSelector&&E&&!N[t+" "]&&(!s||!s.test(t))&&(!v||!v.test(t)))try{var n=c.call(e,t);if(n||d.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){N(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(te,ne),e[3]=(e[3]||e[4]||e[5]||"").replace(te,ne),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||se.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&se.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return G.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=h(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(te,ne).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=m[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&m(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=se.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function D(e,n,r){return m(n)?S.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?S.grep(e,function(e){return e===n!==r}):"string"!=typeof n?S.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(S.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||j,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:q.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof S?t[0]:t,S.merge(this,S.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:E,!0)),N.test(r[1])&&S.isPlainObject(t))for(r in t)m(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=E.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):m(e)?void 0!==n.ready?n.ready(e):e(S):S.makeArray(e,this)}).prototype=S.fn,j=S(E);var L=/^(?:parents|prev(?:Until|All))/,H={children:!0,contents:!0,next:!0,prev:!0};function O(e,t){while((e=e[t])&&1!==e.nodeType);return e}S.fn.extend({has:function(e){var t=S(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,he=/^$|^module$|\/(?:java|ecma)script/i;ce=E.createDocumentFragment().appendChild(E.createElement("div")),(fe=E.createElement("input")).setAttribute("type","radio"),fe.setAttribute("checked","checked"),fe.setAttribute("name","t"),ce.appendChild(fe),y.checkClone=ce.cloneNode(!0).cloneNode(!0).lastChild.checked,ce.innerHTML="",y.noCloneChecked=!!ce.cloneNode(!0).lastChild.defaultValue,ce.innerHTML="",y.option=!!ce.lastChild;var ge={thead:[1,"","
        "],col:[2,"","
        "],tr:[2,"","
        "],td:[3,"","
        "],_default:[0,"",""]};function ve(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&A(e,t)?S.merge([e],n):n}function ye(e,t){for(var n=0,r=e.length;n",""]);var me=/<|&#?\w+;/;function xe(e,t,n,r,i){for(var o,a,s,u,l,c,f=t.createDocumentFragment(),p=[],d=0,h=e.length;d\s*$/g;function qe(e,t){return A(e,"table")&&A(11!==t.nodeType?t:t.firstChild,"tr")&&S(e).children("tbody")[0]||e}function Le(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function He(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Oe(e,t){var n,r,i,o,a,s;if(1===t.nodeType){if(Y.hasData(e)&&(s=Y.get(e).events))for(i in Y.remove(t,"handle events"),s)for(n=0,r=s[i].length;n").attr(n.scriptAttrs||{}).prop({charset:n.scriptCharset,src:n.url}).on("load error",i=function(e){r.remove(),i=null,e&&t("error"===e.type?404:200,e.type)}),E.head.appendChild(r[0])},abort:function(){i&&i()}}});var Ut,Xt=[],Vt=/(=)\?(?=&|$)|\?\?/;S.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=Xt.pop()||S.expando+"_"+Ct.guid++;return this[e]=!0,e}}),S.ajaxPrefilter("json jsonp",function(e,t,n){var r,i,o,a=!1!==e.jsonp&&(Vt.test(e.url)?"url":"string"==typeof e.data&&0===(e.contentType||"").indexOf("application/x-www-form-urlencoded")&&Vt.test(e.data)&&"data");if(a||"jsonp"===e.dataTypes[0])return r=e.jsonpCallback=m(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(Vt,"$1"+r):!1!==e.jsonp&&(e.url+=(Et.test(e.url)?"&":"?")+e.jsonp+"="+r),e.converters["script json"]=function(){return o||S.error(r+" was not called"),o[0]},e.dataTypes[0]="json",i=C[r],C[r]=function(){o=arguments},n.always(function(){void 0===i?S(C).removeProp(r):C[r]=i,e[r]&&(e.jsonpCallback=t.jsonpCallback,Xt.push(r)),o&&m(i)&&i(o[0]),o=i=void 0}),"script"}),y.createHTMLDocument=((Ut=E.implementation.createHTMLDocument("").body).innerHTML="
        ",2===Ut.childNodes.length),S.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(y.createHTMLDocument?((r=(t=E.implementation.createHTMLDocument("")).createElement("base")).href=E.location.href,t.head.appendChild(r)):t=E),o=!n&&[],(i=N.exec(e))?[t.createElement(i[1])]:(i=xe([e],t,o),o&&o.length&&S(o).remove(),S.merge([],i.childNodes)));var r,i,o},S.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(" ");return-1").append(S.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},S.expr.pseudos.animated=function(t){return S.grep(S.timers,function(e){return t===e.elem}).length},S.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=S.css(e,"position"),c=S(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=S.css(e,"top"),u=S.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),m(t)&&(t=t.call(e,n,S.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):("number"==typeof f.top&&(f.top+="px"),"number"==typeof f.left&&(f.left+="px"),c.css(f))}},S.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){S.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===S.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===S.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=S(e).offset()).top+=S.css(e,"borderTopWidth",!0),i.left+=S.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-S.css(r,"marginTop",!0),left:t.left-i.left-S.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===S.css(e,"position"))e=e.offsetParent;return e||re})}}),S.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;S.fn[t]=function(e){return $(this,function(e,t,n){var r;if(x(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),S.each(["top","left"],function(e,n){S.cssHooks[n]=$e(y.pixelPosition,function(e,t){if(t)return t=Be(e,n),Me.test(t)?S(e).position()[n]+"px":t})}),S.each({Height:"height",Width:"width"},function(a,s){S.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){S.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return $(this,function(e,t,n){var r;return x(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?S.css(e,t,i):S.style(e,t,n,i)},s,n?e:void 0,n)}})}),S.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){S.fn[t]=function(e){return this.on(t,e)}}),S.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)},hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}}),S.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){S.fn[n]=function(e,t){return 0= 0; - } - - var newValue = oldIsNumeric( val ), - oldValue = isNumeric2( val ); - - if ( newValue !== oldValue ) { - migrateWarn( "jQuery.isNumeric() should not be called on constructed objects" ); - } - - return oldValue; -}; - -migrateWarnFunc( jQuery, "holdReady", jQuery.holdReady, - "jQuery.holdReady is deprecated" ); - -migrateWarnFunc( jQuery, "unique", jQuery.uniqueSort, - "jQuery.unique is deprecated; use jQuery.uniqueSort" ); - -// Now jQuery.expr.pseudos is the standard incantation -migrateWarnProp( jQuery.expr, "filters", jQuery.expr.pseudos, - "jQuery.expr.filters is deprecated; use jQuery.expr.pseudos" ); -migrateWarnProp( jQuery.expr, ":", jQuery.expr.pseudos, - "jQuery.expr[':'] is deprecated; use jQuery.expr.pseudos" ); - - -var oldAjax = jQuery.ajax; - -jQuery.ajax = function( ) { - var jQXHR = oldAjax.apply( this, arguments ); - - // Be sure we got a jQXHR (e.g., not sync) - if ( jQXHR.promise ) { - migrateWarnFunc( jQXHR, "success", jQXHR.done, - "jQXHR.success is deprecated and removed" ); - migrateWarnFunc( jQXHR, "error", jQXHR.fail, - "jQXHR.error is deprecated and removed" ); - migrateWarnFunc( jQXHR, "complete", jQXHR.always, - "jQXHR.complete is deprecated and removed" ); - } - - return jQXHR; -}; - - -var oldRemoveAttr = jQuery.fn.removeAttr, - oldToggleClass = jQuery.fn.toggleClass, - rmatchNonSpace = /\S+/g; - -jQuery.fn.removeAttr = function( name ) { - var self = this; - - jQuery.each( name.match( rmatchNonSpace ), function( i, attr ) { - if ( jQuery.expr.match.bool.test( attr ) ) { - migrateWarn( "jQuery.fn.removeAttr no longer sets boolean properties: " + attr ); - self.prop( attr, false ); - } - } ); - - return oldRemoveAttr.apply( this, arguments ); -}; - -jQuery.fn.toggleClass = function( state ) { - - // Only deprecating no-args or single boolean arg - if ( state !== undefined && typeof state !== "boolean" ) { - return oldToggleClass.apply( this, arguments ); - } - - migrateWarn( "jQuery.fn.toggleClass( boolean ) is deprecated" ); - - // Toggle entire class name of each element - return this.each( function() { - var className = this.getAttribute && this.getAttribute( "class" ) || ""; - - if ( className ) { - jQuery.data( this, "__className__", className ); - } - - // If the element has a class name or if we're passed `false`, - // then remove the whole classname (if there was one, the above saved it). - // Otherwise bring back whatever was previously saved (if anything), - // falling back to the empty string if nothing was stored. - if ( this.setAttribute ) { - this.setAttribute( "class", - className || state === false ? - "" : - jQuery.data( this, "__className__" ) || "" - ); - } - } ); -}; - - -var internalSwapCall = false; - -// If this version of jQuery has .swap(), don't false-alarm on internal uses -if ( jQuery.swap ) { - jQuery.each( [ "height", "width", "reliableMarginRight" ], function( _, name ) { - var oldHook = jQuery.cssHooks[ name ] && jQuery.cssHooks[ name ].get; - - if ( oldHook ) { - jQuery.cssHooks[ name ].get = function() { - var ret; - - internalSwapCall = true; - ret = oldHook.apply( this, arguments ); - internalSwapCall = false; - return ret; - }; - } - } ); -} - -jQuery.swap = function( elem, options, callback, args ) { - var ret, name, - old = {}; - - if ( !internalSwapCall ) { - migrateWarn( "jQuery.swap() is undocumented and deprecated" ); - } - - // Remember the old values, and insert the new ones - for ( name in options ) { - old[ name ] = elem.style[ name ]; - elem.style[ name ] = options[ name ]; - } - - ret = callback.apply( elem, args || [] ); - - // Revert the old values - for ( name in options ) { - elem.style[ name ] = old[ name ]; - } - - return ret; -}; - -var oldData = jQuery.data; - -jQuery.data = function( elem, name, value ) { - var curData; - - // Name can be an object, and each entry in the object is meant to be set as data - if ( name && typeof name === "object" && arguments.length === 2 ) { - curData = jQuery.hasData( elem ) && oldData.call( this, elem ); - var sameKeys = {}; - for ( var key in name ) { - if ( key !== jQuery.camelCase( key ) ) { - migrateWarn( "jQuery.data() always sets/gets camelCased names: " + key ); - curData[ key ] = name[ key ]; - } else { - sameKeys[ key ] = name[ key ]; - } - } - - oldData.call( this, elem, sameKeys ); - - return name; - } - - // If the name is transformed, look for the un-transformed name in the data object - if ( name && typeof name === "string" && name !== jQuery.camelCase( name ) ) { - curData = jQuery.hasData( elem ) && oldData.call( this, elem ); - if ( curData && name in curData ) { - migrateWarn( "jQuery.data() always sets/gets camelCased names: " + name ); - if ( arguments.length > 2 ) { - curData[ name ] = value; - } - return curData[ name ]; - } - } - - return oldData.apply( this, arguments ); -}; - -var oldTweenRun = jQuery.Tween.prototype.run; -var linearEasing = function( pct ) { - return pct; - }; - -jQuery.Tween.prototype.run = function( ) { - if ( jQuery.easing[ this.easing ].length > 1 ) { - migrateWarn( - "'jQuery.easing." + this.easing.toString() + "' should use only one argument" - ); - - jQuery.easing[ this.easing ] = linearEasing; - } - - oldTweenRun.apply( this, arguments ); -}; - -jQuery.fx.interval = jQuery.fx.interval || 13; - -// Support: IE9, Android <=4.4 -// Avoid false positives on browsers that lack rAF -if ( window.requestAnimationFrame ) { - migrateWarnProp( jQuery.fx, "interval", jQuery.fx.interval, - "jQuery.fx.interval is deprecated" ); -} - -var oldLoad = jQuery.fn.load, - oldEventAdd = jQuery.event.add, - originalFix = jQuery.event.fix; - -jQuery.event.props = []; -jQuery.event.fixHooks = {}; - -migrateWarnProp( jQuery.event.props, "concat", jQuery.event.props.concat, - "jQuery.event.props.concat() is deprecated and removed" ); - -jQuery.event.fix = function( originalEvent ) { - var event, - type = originalEvent.type, - fixHook = this.fixHooks[ type ], - props = jQuery.event.props; - - if ( props.length ) { - migrateWarn( "jQuery.event.props are deprecated and removed: " + props.join() ); - while ( props.length ) { - jQuery.event.addProp( props.pop() ); - } - } - - if ( fixHook && !fixHook._migrated_ ) { - fixHook._migrated_ = true; - migrateWarn( "jQuery.event.fixHooks are deprecated and removed: " + type ); - if ( ( props = fixHook.props ) && props.length ) { - while ( props.length ) { - jQuery.event.addProp( props.pop() ); - } - } - } - - event = originalFix.call( this, originalEvent ); - - return fixHook && fixHook.filter ? fixHook.filter( event, originalEvent ) : event; -}; - -jQuery.event.add = function( elem, types ) { - - // This misses the multiple-types case but that seems awfully rare - if ( elem === window && types === "load" && window.document.readyState === "complete" ) { - migrateWarn( "jQuery(window).on('load'...) called after load event occurred" ); - } - return oldEventAdd.apply( this, arguments ); -}; - -jQuery.each( [ "load", "unload", "error" ], function( _, name ) { - - jQuery.fn[ name ] = function() { - var args = Array.prototype.slice.call( arguments, 0 ); - - // If this is an ajax load() the first arg should be the string URL; - // technically this could also be the "Anything" arg of the event .load() - // which just goes to show why this dumb signature has been deprecated! - // jQuery custom builds that exclude the Ajax module justifiably die here. - if ( name === "load" && typeof args[ 0 ] === "string" ) { - return oldLoad.apply( this, args ); - } - - migrateWarn( "jQuery.fn." + name + "() is deprecated" ); - - args.splice( 0, 0, name ); - if ( arguments.length ) { - return this.on.apply( this, args ); - } - - // Use .triggerHandler here because: - // - load and unload events don't need to bubble, only applied to window or image - // - error event should not bubble to window, although it does pre-1.7 - // See http://bugs.jquery.com/ticket/11820 - this.triggerHandler.apply( this, args ); - return this; - }; - -} ); - -jQuery.each( ( "blur focus focusin focusout resize scroll click dblclick " + - "mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave " + - "change select submit keydown keypress keyup contextmenu" ).split( " " ), - function( i, name ) { - - // Handle event binding - jQuery.fn[ name ] = function( data, fn ) { - migrateWarn( "jQuery.fn." + name + "() event shorthand is deprecated" ); - return arguments.length > 0 ? - this.on( name, null, data, fn ) : - this.trigger( name ); - }; -} ); - -// Trigger "ready" event only once, on document ready -jQuery( function() { - jQuery( window.document ).triggerHandler( "ready" ); -} ); - -jQuery.event.special.ready = { - setup: function() { - if ( this === window.document ) { - migrateWarn( "'ready' event is deprecated" ); - } - } -}; - -jQuery.fn.extend( { - - bind: function( types, data, fn ) { - migrateWarn( "jQuery.fn.bind() is deprecated" ); - return this.on( types, null, data, fn ); - }, - unbind: function( types, fn ) { - migrateWarn( "jQuery.fn.unbind() is deprecated" ); - return this.off( types, null, fn ); - }, - delegate: function( selector, types, data, fn ) { - migrateWarn( "jQuery.fn.delegate() is deprecated" ); - return this.on( types, selector, data, fn ); - }, - undelegate: function( selector, types, fn ) { - migrateWarn( "jQuery.fn.undelegate() is deprecated" ); - return arguments.length === 1 ? - this.off( selector, "**" ) : - this.off( types, selector || "**", fn ); - }, - hover: function( fnOver, fnOut ) { - migrateWarn( "jQuery.fn.hover() is deprecated" ); - return this.on( "mouseenter", fnOver ).on( "mouseleave", fnOut || fnOver ); - } -} ); - - -var oldOffset = jQuery.fn.offset; - -jQuery.fn.offset = function() { - var docElem, - elem = this[ 0 ], - origin = { top: 0, left: 0 }; - - if ( !elem || !elem.nodeType ) { - migrateWarn( "jQuery.fn.offset() requires a valid DOM element" ); - return origin; - } - - docElem = ( elem.ownerDocument || window.document ).documentElement; - if ( !jQuery.contains( docElem, elem ) ) { - migrateWarn( "jQuery.fn.offset() requires an element connected to a document" ); - return origin; - } - - return oldOffset.apply( this, arguments ); -}; - - -var oldParam = jQuery.param; - -jQuery.param = function( data, traditional ) { - var ajaxTraditional = jQuery.ajaxSettings && jQuery.ajaxSettings.traditional; - - if ( traditional === undefined && ajaxTraditional ) { - - migrateWarn( "jQuery.param() no longer uses jQuery.ajaxSettings.traditional" ); - traditional = ajaxTraditional; - } - - return oldParam.call( this, data, traditional ); -}; - -var oldSelf = jQuery.fn.andSelf || jQuery.fn.addBack; - -jQuery.fn.andSelf = function() { - migrateWarn( "jQuery.fn.andSelf() is deprecated and removed, use jQuery.fn.addBack()" ); - return oldSelf.apply( this, arguments ); -}; - - -var oldDeferred = jQuery.Deferred, - tuples = [ - - // Action, add listener, callbacks, .then handlers, final state - [ "resolve", "done", jQuery.Callbacks( "once memory" ), - jQuery.Callbacks( "once memory" ), "resolved" ], - [ "reject", "fail", jQuery.Callbacks( "once memory" ), - jQuery.Callbacks( "once memory" ), "rejected" ], - [ "notify", "progress", jQuery.Callbacks( "memory" ), - jQuery.Callbacks( "memory" ) ] - ]; - -jQuery.Deferred = function( func ) { - var deferred = oldDeferred(), - promise = deferred.promise(); - - deferred.pipe = promise.pipe = function( /* fnDone, fnFail, fnProgress */ ) { - var fns = arguments; - - migrateWarn( "deferred.pipe() is deprecated" ); - - return jQuery.Deferred( function( newDefer ) { - jQuery.each( tuples, function( i, tuple ) { - var fn = jQuery.isFunction( fns[ i ] ) && fns[ i ]; - - // Deferred.done(function() { bind to newDefer or newDefer.resolve }) - // deferred.fail(function() { bind to newDefer or newDefer.reject }) - // deferred.progress(function() { bind to newDefer or newDefer.notify }) - deferred[ tuple[ 1 ] ]( function() { - var returned = fn && fn.apply( this, arguments ); - if ( returned && jQuery.isFunction( returned.promise ) ) { - returned.promise() - .done( newDefer.resolve ) - .fail( newDefer.reject ) - .progress( newDefer.notify ); - } else { - newDefer[ tuple[ 0 ] + "With" ]( - this === promise ? newDefer.promise() : this, - fn ? [ returned ] : arguments - ); - } - } ); - } ); - fns = null; - } ).promise(); - - }; - - if ( func ) { - func.call( deferred, deferred ); - } - - return deferred; -}; - -// Preserve handler of uncaught exceptions in promise chains -jQuery.Deferred.exceptionHook = oldDeferred.exceptionHook; - -return jQuery; -} ); diff --git a/packages/node_modules/@node-red/editor-client/src/vendor/jquery/js/jquery-migrate-3.0.1.min.js b/packages/node_modules/@node-red/editor-client/src/vendor/jquery/js/jquery-migrate-3.0.1.min.js deleted file mode 100644 index cd6d6c853..000000000 --- a/packages/node_modules/@node-red/editor-client/src/vendor/jquery/js/jquery-migrate-3.0.1.min.js +++ /dev/null @@ -1,215 +0,0 @@ -/*! jQuery Migrate v3.0.1 | (c) jQuery Foundation and other contributors | jquery.org/license */ - -void 0 === jQuery.migrateMute && (jQuery.migrateMute = !0), function(e) { - "function" == typeof define && define.amd ? define([ "jquery" ], window, e) : "object" == typeof module && module.exports ? module.exports = e(require("jquery"), window) : e(jQuery, window); -}(function(e, t) { - "use strict"; - function r(r) { - var n = t.console; - o[r] || (o[r] = !0, e.migrateWarnings.push(r), n && n.warn && !e.migrateMute && (n.warn("JQMIGRATE: " + r), - e.migrateTrace && n.trace && n.trace())); - } - function n(e, t, n, a) { - Object.defineProperty(e, t, { - configurable: !0, - enumerable: !0, - get: function() { - return r(a), n; - }, - set: function(e) { - r(a), n = e; - } - }); - } - function a(e, t, n, a) { - e[t] = function() { - return r(a), n.apply(this, arguments); - }; - } - e.migrateVersion = "3.0.1", function() { - var r = /^[12]\./; - t.console && t.console.log && (e && !r.test(e.fn.jquery) || t.console.log("JQMIGRATE: jQuery 3.0.0+ REQUIRED"), - e.migrateWarnings && t.console.log("JQMIGRATE: Migrate plugin loaded multiple times"), - t.console.log("JQMIGRATE: Migrate is installed" + (e.migrateMute ? "" : " with logging active") + ", version " + e.migrateVersion)); - }(); - var o = {}; - e.migrateWarnings = [], void 0 === e.migrateTrace && (e.migrateTrace = !0), e.migrateReset = function() { - o = {}, e.migrateWarnings.length = 0; - }, "BackCompat" === t.document.compatMode && r("jQuery is not compatible with Quirks Mode"); - var i = e.fn.init, s = e.isNumeric, u = e.find, c = /\[(\s*[-\w]+\s*)([~|^$*]?=)\s*([-\w#]*?#[-\w#]*)\s*\]/, l = /\[(\s*[-\w]+\s*)([~|^$*]?=)\s*([-\w#]*?#[-\w#]*)\s*\]/g; - e.fn.init = function(e) { - var t = Array.prototype.slice.call(arguments); - return "string" == typeof e && "#" === e && (r("jQuery( '#' ) is not a valid selector"), - t[0] = []), i.apply(this, t); - }, e.fn.init.prototype = e.fn, e.find = function(e) { - var n = Array.prototype.slice.call(arguments); - if ("string" == typeof e && c.test(e)) try { - t.document.querySelector(e); - } catch (a) { - e = e.replace(l, function(e, t, r, n) { - return "[" + t + r + '"' + n + '"]'; - }); - try { - t.document.querySelector(e), r("Attribute selector with '#' must be quoted: " + n[0]), - n[0] = e; - } catch (e) { - r("Attribute selector with '#' was not fixed: " + n[0]); - } - } - return u.apply(this, n); - }; - var d; - for (d in u) Object.prototype.hasOwnProperty.call(u, d) && (e.find[d] = u[d]); - e.fn.size = function() { - return r("jQuery.fn.size() is deprecated and removed; use the .length property"), - this.length; - }, e.parseJSON = function() { - return r("jQuery.parseJSON is deprecated; use JSON.parse"), JSON.parse.apply(null, arguments); - }, e.isNumeric = function(t) { - var n = s(t), a = function(t) { - var r = t && t.toString(); - return !e.isArray(t) && r - parseFloat(r) + 1 >= 0; - }(t); - return n !== a && r("jQuery.isNumeric() should not be called on constructed objects"), - a; - }, a(e, "holdReady", e.holdReady, "jQuery.holdReady is deprecated"), a(e, "unique", e.uniqueSort, "jQuery.unique is deprecated; use jQuery.uniqueSort"), - n(e.expr, "filters", e.expr.pseudos, "jQuery.expr.filters is deprecated; use jQuery.expr.pseudos"), - n(e.expr, ":", e.expr.pseudos, "jQuery.expr[':'] is deprecated; use jQuery.expr.pseudos"); - var p = e.ajax; - e.ajax = function() { - var e = p.apply(this, arguments); - return e.promise && (a(e, "success", e.done, "jQXHR.success is deprecated and removed"), - a(e, "error", e.fail, "jQXHR.error is deprecated and removed"), a(e, "complete", e.always, "jQXHR.complete is deprecated and removed")), - e; - }; - var f = e.fn.removeAttr, y = e.fn.toggleClass, m = /\S+/g; - e.fn.removeAttr = function(t) { - var n = this; - return e.each(t.match(m), function(t, a) { - e.expr.match.bool.test(a) && (r("jQuery.fn.removeAttr no longer sets boolean properties: " + a), - n.prop(a, !1)); - }), f.apply(this, arguments); - }, e.fn.toggleClass = function(t) { - return void 0 !== t && "boolean" != typeof t ? y.apply(this, arguments) : (r("jQuery.fn.toggleClass( boolean ) is deprecated"), - this.each(function() { - var r = this.getAttribute && this.getAttribute("class") || ""; - r && e.data(this, "__className__", r), this.setAttribute && this.setAttribute("class", r || !1 === t ? "" : e.data(this, "__className__") || ""); - })); - }; - var h = !1; - e.swap && e.each([ "height", "width", "reliableMarginRight" ], function(t, r) { - var n = e.cssHooks[r] && e.cssHooks[r].get; - n && (e.cssHooks[r].get = function() { - var e; - return h = !0, e = n.apply(this, arguments), h = !1, e; - }); - }), e.swap = function(e, t, n, a) { - var o, i, s = {}; - h || r("jQuery.swap() is undocumented and deprecated"); - for (i in t) s[i] = e.style[i], e.style[i] = t[i]; - o = n.apply(e, a || []); - for (i in t) e.style[i] = s[i]; - return o; - }; - var g = e.data; - e.data = function(t, n, a) { - var o; - if (n && "object" == typeof n && 2 === arguments.length) { - o = e.hasData(t) && g.call(this, t); - var i = {}; - for (var s in n) s !== e.camelCase(s) ? (r("jQuery.data() always sets/gets camelCased names: " + s), - o[s] = n[s]) : i[s] = n[s]; - return g.call(this, t, i), n; - } - return n && "string" == typeof n && n !== e.camelCase(n) && (o = e.hasData(t) && g.call(this, t)) && n in o ? (r("jQuery.data() always sets/gets camelCased names: " + n), - arguments.length > 2 && (o[n] = a), o[n]) : g.apply(this, arguments); - }; - var v = e.Tween.prototype.run, j = function(e) { - return e; - }; - e.Tween.prototype.run = function() { - e.easing[this.easing].length > 1 && (r("'jQuery.easing." + this.easing.toString() + "' should use only one argument"), - e.easing[this.easing] = j), v.apply(this, arguments); - }, e.fx.interval = e.fx.interval || 13, t.requestAnimationFrame && n(e.fx, "interval", e.fx.interval, "jQuery.fx.interval is deprecated"); - var Q = e.fn.load, b = e.event.add, w = e.event.fix; - e.event.props = [], e.event.fixHooks = {}, n(e.event.props, "concat", e.event.props.concat, "jQuery.event.props.concat() is deprecated and removed"), - e.event.fix = function(t) { - var n, a = t.type, o = this.fixHooks[a], i = e.event.props; - if (i.length) for (r("jQuery.event.props are deprecated and removed: " + i.join()); i.length; ) e.event.addProp(i.pop()); - if (o && !o._migrated_ && (o._migrated_ = !0, r("jQuery.event.fixHooks are deprecated and removed: " + a), - (i = o.props) && i.length)) for (;i.length; ) e.event.addProp(i.pop()); - return n = w.call(this, t), o && o.filter ? o.filter(n, t) : n; - }, e.event.add = function(e, n) { - return e === t && "load" === n && "complete" === t.document.readyState && r("jQuery(window).on('load'...) called after load event occurred"), - b.apply(this, arguments); - }, e.each([ "load", "unload", "error" ], function(t, n) { - e.fn[n] = function() { - var e = Array.prototype.slice.call(arguments, 0); - return "load" === n && "string" == typeof e[0] ? Q.apply(this, e) : (r("jQuery.fn." + n + "() is deprecated"), - e.splice(0, 0, n), arguments.length ? this.on.apply(this, e) : (this.triggerHandler.apply(this, e), - this)); - }; - }), e.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "), function(t, n) { - e.fn[n] = function(e, t) { - return r("jQuery.fn." + n + "() event shorthand is deprecated"), arguments.length > 0 ? this.on(n, null, e, t) : this.trigger(n); - }; - }), e(function() { - e(t.document).triggerHandler("ready"); - }), e.event.special.ready = { - setup: function() { - this === t.document && r("'ready' event is deprecated"); - } - }, e.fn.extend({ - bind: function(e, t, n) { - return r("jQuery.fn.bind() is deprecated"), this.on(e, null, t, n); - }, - unbind: function(e, t) { - return r("jQuery.fn.unbind() is deprecated"), this.off(e, null, t); - }, - delegate: function(e, t, n, a) { - return r("jQuery.fn.delegate() is deprecated"), this.on(t, e, n, a); - }, - undelegate: function(e, t, n) { - return r("jQuery.fn.undelegate() is deprecated"), 1 === arguments.length ? this.off(e, "**") : this.off(t, e || "**", n); - }, - hover: function(e, t) { - return r("jQuery.fn.hover() is deprecated"), this.on("mouseenter", e).on("mouseleave", t || e); - } - }); - var x = e.fn.offset; - e.fn.offset = function() { - var n, a = this[0], o = { - top: 0, - left: 0 - }; - return a && a.nodeType ? (n = (a.ownerDocument || t.document).documentElement, e.contains(n, a) ? x.apply(this, arguments) : (r("jQuery.fn.offset() requires an element connected to a document"), - o)) : (r("jQuery.fn.offset() requires a valid DOM element"), o); - }; - var k = e.param; - e.param = function(t, n) { - var a = e.ajaxSettings && e.ajaxSettings.traditional; - return void 0 === n && a && (r("jQuery.param() no longer uses jQuery.ajaxSettings.traditional"), - n = a), k.call(this, t, n); - }; - var A = e.fn.andSelf || e.fn.addBack; - e.fn.andSelf = function() { - return r("jQuery.fn.andSelf() is deprecated and removed, use jQuery.fn.addBack()"), - A.apply(this, arguments); - }; - var S = e.Deferred, q = [ [ "resolve", "done", e.Callbacks("once memory"), e.Callbacks("once memory"), "resolved" ], [ "reject", "fail", e.Callbacks("once memory"), e.Callbacks("once memory"), "rejected" ], [ "notify", "progress", e.Callbacks("memory"), e.Callbacks("memory") ] ]; - return e.Deferred = function(t) { - var n = S(), a = n.promise(); - return n.pipe = a.pipe = function() { - var t = arguments; - return r("deferred.pipe() is deprecated"), e.Deferred(function(r) { - e.each(q, function(o, i) { - var s = e.isFunction(t[o]) && t[o]; - n[i[1]](function() { - var t = s && s.apply(this, arguments); - t && e.isFunction(t.promise) ? t.promise().done(r.resolve).fail(r.reject).progress(r.notify) : r[i[0] + "With"](this === a ? r.promise() : this, s ? [ t ] : arguments); - }); - }), t = null; - }).promise(); - }, t && t.call(n, n), n; - }, e.Deferred.exceptionHook = S.exceptionHook, e; -}); \ No newline at end of file diff --git a/packages/node_modules/@node-red/editor-client/src/vendor/jquery/js/jquery-migrate-3.3.0.min.js b/packages/node_modules/@node-red/editor-client/src/vendor/jquery/js/jquery-migrate-3.3.0.min.js new file mode 100644 index 000000000..411afbd8a --- /dev/null +++ b/packages/node_modules/@node-red/editor-client/src/vendor/jquery/js/jquery-migrate-3.3.0.min.js @@ -0,0 +1,2 @@ +/*! jQuery Migrate v3.3.0 | (c) OpenJS Foundation and other contributors | jquery.org/license */ +"undefined"==typeof jQuery.migrateMute&&(jQuery.migrateMute=!0),function(t){"use strict";"function"==typeof define&&define.amd?define(["jquery"],function(e){return t(e,window)}):"object"==typeof module&&module.exports?module.exports=t(require("jquery"),window):t(jQuery,window)}(function(s,n){"use strict";function e(e){return 0<=function(e,t){var r,n=/^(\d+)\.(\d+)\.(\d+)/,i=n.exec(e)||[],o=n.exec(t)||[];for(r=1;r<=3;r++){if(+i[r]>+o[r])return 1;if(+i[r]<+o[r])return-1}return 0}(s.fn.jquery,e)}s.migrateVersion="3.3.0",n.console&&n.console.log&&(s&&e("3.0.0")||n.console.log("JQMIGRATE: jQuery 3.0.0+ REQUIRED"),s.migrateWarnings&&n.console.log("JQMIGRATE: Migrate plugin loaded multiple times"),n.console.log("JQMIGRATE: Migrate is installed"+(s.migrateMute?"":" with logging active")+", version "+s.migrateVersion));var r={};function u(e){var t=n.console;s.migrateDeduplicateWarnings&&r[e]||(r[e]=!0,s.migrateWarnings.push(e),t&&t.warn&&!s.migrateMute&&(t.warn("JQMIGRATE: "+e),s.migrateTrace&&t.trace&&t.trace()))}function t(e,t,r,n){Object.defineProperty(e,t,{configurable:!0,enumerable:!0,get:function(){return u(n),r},set:function(e){u(n),r=e}})}function i(e,t,r,n){e[t]=function(){return u(n),r.apply(this,arguments)}}s.migrateDeduplicateWarnings=!0,s.migrateWarnings=[],void 0===s.migrateTrace&&(s.migrateTrace=!0),s.migrateReset=function(){r={},s.migrateWarnings.length=0},"BackCompat"===n.document.compatMode&&u("jQuery is not compatible with Quirks Mode");var o,a={},c=s.fn.init,d=s.find,l=/\[(\s*[-\w]+\s*)([~|^$*]?=)\s*([-\w#]*?#[-\w#]*)\s*\]/,p=/\[(\s*[-\w]+\s*)([~|^$*]?=)\s*([-\w#]*?#[-\w#]*)\s*\]/g,f=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g;for(o in s.fn.init=function(e){var t=Array.prototype.slice.call(arguments);return"string"==typeof e&&"#"===e&&(u("jQuery( '#' ) is not a valid selector"),t[0]=[]),c.apply(this,t)},s.fn.init.prototype=s.fn,s.find=function(t){var r=Array.prototype.slice.call(arguments);if("string"==typeof t&&l.test(t))try{n.document.querySelector(t)}catch(e){t=t.replace(p,function(e,t,r,n){return"["+t+r+'"'+n+'"]'});try{n.document.querySelector(t),u("Attribute selector with '#' must be quoted: "+r[0]),r[0]=t}catch(e){u("Attribute selector with '#' was not fixed: "+r[0])}}return d.apply(this,r)},d)Object.prototype.hasOwnProperty.call(d,o)&&(s.find[o]=d[o]);if(i(s.fn,"size",function(){return this.length},"jQuery.fn.size() is deprecated and removed; use the .length property"),i(s,"parseJSON",function(){return JSON.parse.apply(null,arguments)},"jQuery.parseJSON is deprecated; use JSON.parse"),i(s,"holdReady",s.holdReady,"jQuery.holdReady is deprecated"),i(s,"unique",s.uniqueSort,"jQuery.unique is deprecated; use jQuery.uniqueSort"),t(s.expr,"filters",s.expr.pseudos,"jQuery.expr.filters is deprecated; use jQuery.expr.pseudos"),t(s.expr,":",s.expr.pseudos,"jQuery.expr[':'] is deprecated; use jQuery.expr.pseudos"),e("3.1.1")&&i(s,"trim",function(e){return null==e?"":(e+"").replace(f,"")},"jQuery.trim is deprecated; use String.prototype.trim"),e("3.2.0")&&i(s,"nodeName",function(e,t){return e.nodeName&&e.nodeName.toLowerCase()===t.toLowerCase()},"jQuery.nodeName is deprecated"),e("3.3.0")&&(i(s,"isNumeric",function(e){var t=typeof e;return("number"==t||"string"==t)&&!isNaN(e-parseFloat(e))},"jQuery.isNumeric() is deprecated"),s.each("Boolean Number String Function Array Date RegExp Object Error Symbol".split(" "),function(e,t){a["[object "+t+"]"]=t.toLowerCase()}),i(s,"type",function(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?a[Object.prototype.toString.call(e)]||"object":typeof e},"jQuery.type is deprecated"),i(s,"isFunction",function(e){return"function"==typeof e},"jQuery.isFunction() is deprecated"),i(s,"isWindow",function(e){return null!=e&&e===e.window},"jQuery.isWindow() is deprecated"),i(s,"isArray",Array.isArray,"jQuery.isArray is deprecated; use Array.isArray")),s.ajax){var y=s.ajax;s.ajax=function(){var e=y.apply(this,arguments);return e.promise&&(i(e,"success",e.done,"jQXHR.success is deprecated and removed"),i(e,"error",e.fail,"jQXHR.error is deprecated and removed"),i(e,"complete",e.always,"jQXHR.complete is deprecated and removed")),e}}var m=s.fn.removeAttr,g=s.fn.toggleClass,h=/\S+/g;function v(e){return e.replace(/-([a-z])/g,function(e,t){return t.toUpperCase()})}s.fn.removeAttr=function(e){var r=this;return s.each(e.match(h),function(e,t){s.expr.match.bool.test(t)&&(u("jQuery.fn.removeAttr no longer sets boolean properties: "+t),r.prop(t,!1))}),m.apply(this,arguments)};var j,Q=!(s.fn.toggleClass=function(t){return void 0!==t&&"boolean"!=typeof t?g.apply(this,arguments):(u("jQuery.fn.toggleClass( boolean ) is deprecated"),this.each(function(){var e=this.getAttribute&&this.getAttribute("class")||"";e&&s.data(this,"__className__",e),this.setAttribute&&this.setAttribute("class",e||!1===t?"":s.data(this,"__className__")||"")}))}),b=/^[a-z]/,w=/^(?:Border(?:Top|Right|Bottom|Left)?(?:Width|)|(?:Margin|Padding)?(?:Top|Right|Bottom|Left)?|(?:Min|Max)?(?:Width|Height))$/;s.swap&&s.each(["height","width","reliableMarginRight"],function(e,t){var r=s.cssHooks[t]&&s.cssHooks[t].get;r&&(s.cssHooks[t].get=function(){var e;return Q=!0,e=r.apply(this,arguments),Q=!1,e})}),s.swap=function(e,t,r,n){var i,o,a={};for(o in Q||u("jQuery.swap() is undocumented and deprecated"),t)a[o]=e.style[o],e.style[o]=t[o];for(o in i=r.apply(e,n||[]),t)e.style[o]=a[o];return i},e("3.4.0")&&"undefined"!=typeof Proxy&&(s.cssProps=new Proxy(s.cssProps||{},{set:function(){return u("JQMIGRATE: jQuery.cssProps is deprecated"),Reflect.set.apply(this,arguments)}})),s.cssNumber||(s.cssNumber={}),j=s.fn.css,s.fn.css=function(e,t){var r=this;return"string"!=typeof e&&s.each(e,function(e,t){s.fn.css.call(r,e,t)}),"number"!=typeof t||function(e){return b.test(e)&&w.test(e[0].toUpperCase()+e.slice(1))}(v(e))||u("Use of number-typed values is deprecated in jQuery.fn.css"),j.apply(this,arguments)};var x=s.data;if(s.data=function(e,t,r){var n,i,o;if(t&&"object"==typeof t&&2===arguments.length){for(o in n=s.hasData(e)&&x.call(this,e),i={},t)o!==v(o)?(u("jQuery.data() always sets/gets camelCased names: "+o),n[o]=t[o]):i[o]=t[o];return x.call(this,e,i),t}return t&&"string"==typeof t&&t!==v(t)&&(n=s.hasData(e)&&x.call(this,e))&&t in n?(u("jQuery.data() always sets/gets camelCased names: "+t),2");t!==e&&C(e)!==C(t)&&u("HTML tags must be properly nested and closed: "+e)}var P=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([a-z][^\/\0>\x20\t\r\n\f]*)[^>]*)\/>/gi,q=s.htmlPrefilter;s.UNSAFE_restoreLegacyHtmlPrefilter=function(){s.htmlPrefilter=function(e){return T(e),e.replace(P,"<$1>")}},s.htmlPrefilter=function(e){return T(e),q(e)};var D=s.fn.offset;if(s.fn.offset=function(){var e,t=this[0];if(t&&t.nodeType)return e=(t.ownerDocument||n.document).documentElement,s.contains(e,t)?D.apply(this,arguments):(u("jQuery.fn.offset() requires an element connected to a document"),{top:0,left:0});u("jQuery.fn.offset() requires a valid DOM element")},s.ajax){var E=s.param;s.param=function(e,t){var r=s.ajaxSettings&&s.ajaxSettings.traditional;return void 0===t&&r&&(u("jQuery.param() no longer uses jQuery.ajaxSettings.traditional"),t=r),E.call(this,e,t)}}var _=s.fn.andSelf||s.fn.addBack;if(s.fn.andSelf=function(){return u("jQuery.fn.andSelf() is deprecated and removed, use jQuery.fn.addBack()"),_.apply(this,arguments)},s.Deferred){var F=s.Deferred,O=[["resolve","done",s.Callbacks("once memory"),s.Callbacks("once memory"),"resolved"],["reject","fail",s.Callbacks("once memory"),s.Callbacks("once memory"),"rejected"],["notify","progress",s.Callbacks("memory"),s.Callbacks("memory")]];s.Deferred=function(e){var o=F(),a=o.promise();return o.pipe=a.pipe=function(){var i=arguments;return u("deferred.pipe() is deprecated"),s.Deferred(function(n){s.each(O,function(e,t){var r="function"==typeof i[e]&&i[e];o[t[1]](function(){var e=r&&r.apply(this,arguments);e&&"function"==typeof e.promise?e.promise().done(n.resolve).fail(n.reject).progress(n.notify):n[t[0]+"With"](this===a?n.promise():this,r?[e]:arguments)})}),i=null}).promise()},e&&e.call(o,o),o},s.Deferred.exceptionHook=F.exceptionHook}return s}); diff --git a/packages/node_modules/@node-red/editor-client/src/vendor/jsonata/formatter.js b/packages/node_modules/@node-red/editor-client/src/vendor/jsonata/formatter.js index 0cc69715a..49a02c206 100644 --- a/packages/node_modules/@node-red/editor-client/src/vendor/jsonata/formatter.js +++ b/packages/node_modules/@node-red/editor-client/src/vendor/jsonata/formatter.js @@ -11,6 +11,7 @@ var length = str.length; var start = 0; var inString = false; + var inRegex = false; var inBox = false; var quoteChar; var list = []; @@ -24,8 +25,13 @@ } for (var i=0;i 70) { + post = result.substring(offset+f.pos+1); + if (!/^\n/.test(post)) { + indented = indentLine(post,indent); + hasNewline = /\n$/.test(pre); + result = pre+(hasNewline?"":"\n")+indented; + offset += indented.length-post.length+(hasNewline?0:1); + } + } + } else if (f.type === "open-block") { - if (f.width > 30) { + if (f.width > 40) { longStack.push(true); indent += 4; pre = result.substring(0,offset+f.pos+1); post = result.substring(offset+f.pos+1); + hasNewline = /\n$/.test(pre); indented = indentLine(post,indent); - result = pre+"\n"+indented; - offset += indented.length-post.length+1; + result = pre+(hasNewline?"":"\n")+indented; + offset += indented.length-post.length+(hasNewline?0:1); } else { longStack.push(false); } } else if (f.type === "close-block") { - if (f.width > 30) { + if (f.width > 40) { indent -= 4; pre = result.substring(0,offset+f.pos); post = result.substring(offset+f.pos); indented = indentLine(post,indent); - result = pre+"\n"+indented; - offset += indented.length-post.length+1; + hasNewline = /\n *$/.test(pre); + if (hasNewline) { + result = pre + post; + } else { + result = pre+"\n"+indented; + offset += indented.length-post.length+1; + } } longStack.pop(); } @@ -171,6 +216,7 @@ '$sum':{ args:[ 'array' ]}, '$toMillis':{args:['timestamp']}, // <------------- '$trim':{ args:[ 'str' ]}, + '$type':{ args:['value']}, '$uppercase':{ args:[ 'str' ]}, '$zip':{ args:[ 'array1' ]} } diff --git a/packages/node_modules/@node-red/editor-client/src/vendor/jsonata/mode-jsonata.js b/packages/node_modules/@node-red/editor-client/src/vendor/jsonata/mode-jsonata.js index 2a90e4ce7..48358517e 100644 --- a/packages/node_modules/@node-red/editor-client/src/vendor/jsonata/mode-jsonata.js +++ b/packages/node_modules/@node-red/editor-client/src/vendor/jsonata/mode-jsonata.js @@ -28,6 +28,11 @@ ace.define("ace/mode/jsonata",["require","exports","module","ace/lib/oop","ace/m }, "identifier"); this.$rules = { "start" : [ + { + token: "string.regexp", + regex: "\\/", + next: "regex" + }, { token : "string", regex : "'(?=.)", @@ -46,34 +51,35 @@ ace.define("ace/mode/jsonata",["require","exports","module","ace/lib/oop","ace/m token : "constant.numeric", // float regex : /[+-]?\d[\d_]*(?:(?:\.\d*)?(?:[eE][+-]?\d+)?)?\b/ }, - { token: "keyword", - regex: /λ/ - }, - { - token: "keyword", - regex: jsonataFunctions - }, - { - token : keywordMapper, - regex : "[a-zA-Z\\$_\u00a1-\uffff][a-zA-Z\\d\\$_\u00a1-\uffff]*" - }, - { - token : "punctuation.operator", - regex : /[.](?![.])/ - }, - { - token : "keyword.operator", - regex : /\|\||<=|>=|\.\.|\*\*|!=|:=|[=<>`!$%&*+\-~\/^]/, - next : "start" - }, - { - token : "punctuation.operator", - regex : /[?:,;.]/, - next : "start" - }, - { - token : "paren.lparen", - regex : /[\[({]/, + { + token: "keyword", + regex: /λ/ + }, + { + token: "keyword", + regex: jsonataFunctions + }, + { + token : keywordMapper, + regex : "[a-zA-Z\\$_\u00a1-\uffff][a-zA-Z\\d\\$_\u00a1-\uffff]*" + }, + { + token : "punctuation.operator", + regex : /[.](?![.])/ + }, + { + token : "keyword.operator", + regex : /\|\||<=|>=|\.\.|\*\*|!=|:=|[=<>`!$%&*+\-~\/^]/, + next : "start" + }, + { + token : "punctuation.operator", + regex : /[?:,;.]/, + next : "start" + }, + { + token : "paren.lparen", + regex : /[\[({]/, next : "start" }, { @@ -86,7 +92,8 @@ ace.define("ace/mode/jsonata",["require","exports","module","ace/lib/oop","ace/m token : "string", regex : '"|$', next : "start" - }, { + }, + { defaultToken: "string" } ], @@ -95,9 +102,24 @@ ace.define("ace/mode/jsonata",["require","exports","module","ace/lib/oop","ace/m token : "string", regex : "'|$", next : "start" - }, { + }, + { defaultToken: "string" } + ], + "regex" : [ + { + token: "string.regexp", + regex: "\\\\/" + }, + { + token: "string.regexp", + regex: "/[sxngimy]*", + next: "start" + }, + { + defaultToken: "string.regexp" + } ] }; }; diff --git a/packages/node_modules/@node-red/editor-client/src/vendor/marked/marked.min.js b/packages/node_modules/@node-red/editor-client/src/vendor/marked/marked.min.js deleted file mode 100644 index 555c1dc1d..000000000 --- a/packages/node_modules/@node-red/editor-client/src/vendor/marked/marked.min.js +++ /dev/null @@ -1,6 +0,0 @@ -/** - * marked - a markdown parser - * Copyright (c) 2011-2014, Christopher Jeffrey. (MIT Licensed) - * https://github.com/chjj/marked - */ -(function(){var block={newline:/^\n+/,code:/^( {4}[^\n]+\n*)+/,fences:noop,hr:/^( *[-*_]){3,} *(?:\n+|$)/,heading:/^ *(#{1,6}) *([^\n]+?) *#* *(?:\n+|$)/,nptable:noop,lheading:/^([^\n]+)\n *(=|-){2,} *(?:\n+|$)/,blockquote:/^( *>[^\n]+(\n(?!def)[^\n]+)*\n*)+/,list:/^( *)(bull) [\s\S]+?(?:hr|def|\n{2,}(?! )(?!\1bull )\n*|\s*$)/,html:/^ *(?:comment *(?:\n|\s*$)|closed *(?:\n{2,}|\s*$)|closing *(?:\n{2,}|\s*$))/,def:/^ *\[([^\]]+)\]: *]+)>?(?: +["(]([^\n]+)[")])? *(?:\n+|$)/,table:noop,paragraph:/^((?:[^\n]+\n?(?!hr|heading|lheading|blockquote|tag|def))+)\n*/,text:/^[^\n]+/};block.bullet=/(?:[*+-]|\d+\.)/;block.item=/^( *)(bull) [^\n]*(?:\n(?!\1bull )[^\n]*)*/;block.item=replace(block.item,"gm")(/bull/g,block.bullet)();block.list=replace(block.list)(/bull/g,block.bullet)("hr","\\n+(?=\\1?(?:[-*_] *){3,}(?:\\n+|$))")("def","\\n+(?="+block.def.source+")")();block.blockquote=replace(block.blockquote)("def",block.def)();block._tag="(?!(?:"+"a|em|strong|small|s|cite|q|dfn|abbr|data|time|code"+"|var|samp|kbd|sub|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo"+"|span|br|wbr|ins|del|img)\\b)\\w+(?!:/|[^\\w\\s@]*@)\\b";block.html=replace(block.html)("comment",//)("closed",/<(tag)[\s\S]+?<\/\1>/)("closing",/])*?>/)(/tag/g,block._tag)();block.paragraph=replace(block.paragraph)("hr",block.hr)("heading",block.heading)("lheading",block.lheading)("blockquote",block.blockquote)("tag","<"+block._tag)("def",block.def)();block.normal=merge({},block);block.gfm=merge({},block.normal,{fences:/^ *(`{3,}|~{3,})[ \.]*(\S+)? *\n([\s\S]*?)\s*\1 *(?:\n+|$)/,paragraph:/^/,heading:/^ *(#{1,6}) +([^\n]+?) *#* *(?:\n+|$)/});block.gfm.paragraph=replace(block.paragraph)("(?!","(?!"+block.gfm.fences.source.replace("\\1","\\2")+"|"+block.list.source.replace("\\1","\\3")+"|")();block.tables=merge({},block.gfm,{nptable:/^ *(\S.*\|.*)\n *([-:]+ *\|[-| :]*)\n((?:.*\|.*(?:\n|$))*)\n*/,table:/^ *\|(.+)\n *\|( *[-:]+[-| :]*)\n((?: *\|.*(?:\n|$))*)\n*/});function Lexer(options){this.tokens=[];this.tokens.links={};this.options=options||marked.defaults;this.rules=block.normal;if(this.options.gfm){if(this.options.tables){this.rules=block.tables}else{this.rules=block.gfm}}}Lexer.rules=block;Lexer.lex=function(src,options){var lexer=new Lexer(options);return lexer.lex(src)};Lexer.prototype.lex=function(src){src=src.replace(/\r\n|\r/g,"\n").replace(/\t/g," ").replace(/\u00a0/g," ").replace(/\u2424/g,"\n");return this.token(src,true)};Lexer.prototype.token=function(src,top,bq){var src=src.replace(/^ +$/gm,""),next,loose,cap,bull,b,item,space,i,l;while(src){if(cap=this.rules.newline.exec(src)){src=src.substring(cap[0].length);if(cap[0].length>1){this.tokens.push({type:"space"})}}if(cap=this.rules.code.exec(src)){src=src.substring(cap[0].length);cap=cap[0].replace(/^ {4}/gm,"");this.tokens.push({type:"code",text:!this.options.pedantic?cap.replace(/\n+$/,""):cap});continue}if(cap=this.rules.fences.exec(src)){src=src.substring(cap[0].length);this.tokens.push({type:"code",lang:cap[2],text:cap[3]||""});continue}if(cap=this.rules.heading.exec(src)){src=src.substring(cap[0].length);this.tokens.push({type:"heading",depth:cap[1].length,text:cap[2]});continue}if(top&&(cap=this.rules.nptable.exec(src))){src=src.substring(cap[0].length);item={type:"table",header:cap[1].replace(/^ *| *\| *$/g,"").split(/ *\| */),align:cap[2].replace(/^ *|\| *$/g,"").split(/ *\| */),cells:cap[3].replace(/\n$/,"").split("\n")};for(i=0;i ?/gm,"");this.token(cap,top,true);this.tokens.push({type:"blockquote_end"});continue}if(cap=this.rules.list.exec(src)){src=src.substring(cap[0].length);bull=cap[2];this.tokens.push({type:"list_start",ordered:bull.length>1});cap=cap[0].match(this.rules.item);next=false;l=cap.length;i=0;for(;i1&&b.length>1)){src=cap.slice(i+1).join("\n")+src;i=l-1}}loose=next||/\n\n(?!\s*$)/.test(item);if(i!==l-1){next=item.charAt(item.length-1)==="\n";if(!loose)loose=next}this.tokens.push({type:loose?"loose_item_start":"list_item_start"});this.token(item,false,bq);this.tokens.push({type:"list_item_end"})}this.tokens.push({type:"list_end"});continue}if(cap=this.rules.html.exec(src)){src=src.substring(cap[0].length);this.tokens.push({type:this.options.sanitize?"paragraph":"html",pre:!this.options.sanitizer&&(cap[1]==="pre"||cap[1]==="script"||cap[1]==="style"),text:cap[0]});continue}if(!bq&&top&&(cap=this.rules.def.exec(src))){src=src.substring(cap[0].length);this.tokens.links[cap[1].toLowerCase()]={href:cap[2],title:cap[3]};continue}if(top&&(cap=this.rules.table.exec(src))){src=src.substring(cap[0].length);item={type:"table",header:cap[1].replace(/^ *| *\| *$/g,"").split(/ *\| */),align:cap[2].replace(/^ *|\| *$/g,"").split(/ *\| */),cells:cap[3].replace(/(?: *\| *)?\n$/,"").split("\n")};for(i=0;i])/,autolink:/^<([^ >]+(@|:\/)[^ >]+)>/,url:noop,tag:/^|^<\/?\w+(?:"[^"]*"|'[^']*'|[^'">])*?>/,link:/^!?\[(inside)\]\(href\)/,reflink:/^!?\[(inside)\]\s*\[([^\]]*)\]/,nolink:/^!?\[((?:\[[^\]]*\]|[^\[\]])*)\]/,strong:/^__([\s\S]+?)__(?!_)|^\*\*([\s\S]+?)\*\*(?!\*)/,em:/^\b_((?:[^_]|__)+?)_\b|^\*((?:\*\*|[\s\S])+?)\*(?!\*)/,code:/^(`+)\s*([\s\S]*?[^`])\s*\1(?!`)/,br:/^ {2,}\n(?!\s*$)/,del:noop,text:/^[\s\S]+?(?=[\\?(?:\s+['"]([\s\S]*?)['"])?\s*/;inline.link=replace(inline.link)("inside",inline._inside)("href",inline._href)();inline.reflink=replace(inline.reflink)("inside",inline._inside)();inline.normal=merge({},inline);inline.pedantic=merge({},inline.normal,{strong:/^__(?=\S)([\s\S]*?\S)__(?!_)|^\*\*(?=\S)([\s\S]*?\S)\*\*(?!\*)/,em:/^_(?=\S)([\s\S]*?\S)_(?!_)|^\*(?=\S)([\s\S]*?\S)\*(?!\*)/});inline.gfm=merge({},inline.normal,{escape:replace(inline.escape)("])","~|])")(),url:/^(https?:\/\/[^\s<]+[^<.,:;"')\]\s])/,del:/^~~(?=\S)([\s\S]*?\S)~~/,text:replace(inline.text)("]|","~]|")("|","|https?://|")()});inline.breaks=merge({},inline.gfm,{br:replace(inline.br)("{2,}","*")(),text:replace(inline.gfm.text)("{2,}","*")()});function InlineLexer(links,options){this.options=options||marked.defaults;this.links=links;this.rules=inline.normal;this.renderer=this.options.renderer||new Renderer;this.renderer.options=this.options;if(!this.links){throw new Error("Tokens array requires a `links` property.")}if(this.options.gfm){if(this.options.breaks){this.rules=inline.breaks}else{this.rules=inline.gfm}}else if(this.options.pedantic){this.rules=inline.pedantic}}InlineLexer.rules=inline;InlineLexer.output=function(src,links,options){var inline=new InlineLexer(links,options);return inline.output(src)};InlineLexer.prototype.output=function(src){var out="",link,text,href,cap;while(src){if(cap=this.rules.escape.exec(src)){src=src.substring(cap[0].length);out+=cap[1];continue}if(cap=this.rules.autolink.exec(src)){src=src.substring(cap[0].length);if(cap[2]==="@"){text=cap[1].charAt(6)===":"?this.mangle(cap[1].substring(7)):this.mangle(cap[1]);href=this.mangle("mailto:")+text}else{text=escape(cap[1]);href=text}out+=this.renderer.link(href,null,text);continue}if(!this.inLink&&(cap=this.rules.url.exec(src))){src=src.substring(cap[0].length);text=escape(cap[1]);href=text;out+=this.renderer.link(href,null,text);continue}if(cap=this.rules.tag.exec(src)){if(!this.inLink&&/^/i.test(cap[0])){this.inLink=false}src=src.substring(cap[0].length);out+=this.options.sanitize?this.options.sanitizer?this.options.sanitizer(cap[0]):escape(cap[0]):cap[0];continue}if(cap=this.rules.link.exec(src)){src=src.substring(cap[0].length);this.inLink=true;out+=this.outputLink(cap,{href:cap[2],title:cap[3]});this.inLink=false;continue}if((cap=this.rules.reflink.exec(src))||(cap=this.rules.nolink.exec(src))){src=src.substring(cap[0].length);link=(cap[2]||cap[1]).replace(/\s+/g," ");link=this.links[link.toLowerCase()];if(!link||!link.href){out+=cap[0].charAt(0);src=cap[0].substring(1)+src;continue}this.inLink=true;out+=this.outputLink(cap,link);this.inLink=false;continue}if(cap=this.rules.strong.exec(src)){src=src.substring(cap[0].length);out+=this.renderer.strong(this.output(cap[2]||cap[1]));continue}if(cap=this.rules.em.exec(src)){src=src.substring(cap[0].length);out+=this.renderer.em(this.output(cap[2]||cap[1]));continue}if(cap=this.rules.code.exec(src)){src=src.substring(cap[0].length);out+=this.renderer.codespan(escape(cap[2],true));continue}if(cap=this.rules.br.exec(src)){src=src.substring(cap[0].length);out+=this.renderer.br();continue}if(cap=this.rules.del.exec(src)){src=src.substring(cap[0].length);out+=this.renderer.del(this.output(cap[1]));continue}if(cap=this.rules.text.exec(src)){src=src.substring(cap[0].length);out+=this.renderer.text(escape(this.smartypants(cap[0])));continue}if(src){throw new Error("Infinite loop on byte: "+src.charCodeAt(0))}}return out};InlineLexer.prototype.outputLink=function(cap,link){var href=escape(link.href),title=link.title?escape(link.title):null;return cap[0].charAt(0)!=="!"?this.renderer.link(href,title,this.output(cap[1])):this.renderer.image(href,title,escape(cap[1]))};InlineLexer.prototype.smartypants=function(text){if(!this.options.smartypants)return text;return text.replace(/---/g,"—").replace(/--/g,"–").replace(/(^|[-\u2014/(\[{"\s])'/g,"$1‘").replace(/'/g,"’").replace(/(^|[-\u2014/(\[{\u2018\s])"/g,"$1“").replace(/"/g,"”").replace(/\.{3}/g,"…")};InlineLexer.prototype.mangle=function(text){if(!this.options.mangle)return text;var out="",l=text.length,i=0,ch;for(;i.5){ch="x"+ch.toString(16)}out+="&#"+ch+";"}return out};function Renderer(options){this.options=options||{}}Renderer.prototype.code=function(code,lang,escaped){if(this.options.highlight){var out=this.options.highlight(code,lang);if(out!=null&&out!==code){escaped=true;code=out}}if(!lang){return"
        "+(escaped?code:escape(code,true))+"\n
        "}return'
        '+(escaped?code:escape(code,true))+"\n
        \n"};Renderer.prototype.blockquote=function(quote){return"
        \n"+quote+"
        \n"};Renderer.prototype.html=function(html){return html};Renderer.prototype.heading=function(text,level,raw){return"'+text+"\n"};Renderer.prototype.hr=function(){return this.options.xhtml?"
        \n":"
        \n"};Renderer.prototype.list=function(body,ordered){var type=ordered?"ol":"ul";return"<"+type+">\n"+body+"\n"};Renderer.prototype.listitem=function(text){return"
      1. "+text+"
      2. \n"};Renderer.prototype.paragraph=function(text){return"

        "+text+"

        \n"};Renderer.prototype.table=function(header,body){return"\n"+"\n"+header+"\n"+"\n"+body+"\n"+"
        \n"};Renderer.prototype.tablerow=function(content){return"\n"+content+"\n"};Renderer.prototype.tablecell=function(content,flags){var type=flags.header?"th":"td";var tag=flags.align?"<"+type+' style="text-align:'+flags.align+'">':"<"+type+">";return tag+content+"\n"};Renderer.prototype.strong=function(text){return""+text+""};Renderer.prototype.em=function(text){return""+text+""};Renderer.prototype.codespan=function(text){return""+text+""};Renderer.prototype.br=function(){return this.options.xhtml?"
        ":"
        "};Renderer.prototype.del=function(text){return""+text+""};Renderer.prototype.link=function(href,title,text){if(this.options.sanitize){try{var prot=decodeURIComponent(unescape(href)).replace(/[^\w:]/g,"").toLowerCase()}catch(e){return""}if(prot.indexOf("javascript:")===0||prot.indexOf("vbscript:")===0){return""}}var out='
        ";return out};Renderer.prototype.image=function(href,title,text){var out=''+text+'":">";return out};Renderer.prototype.text=function(text){return text};function Parser(options){this.tokens=[];this.token=null;this.options=options||marked.defaults;this.options.renderer=this.options.renderer||new Renderer;this.renderer=this.options.renderer;this.renderer.options=this.options}Parser.parse=function(src,options,renderer){var parser=new Parser(options,renderer);return parser.parse(src)};Parser.prototype.parse=function(src){this.inline=new InlineLexer(src.links,this.options,this.renderer);this.tokens=src.reverse();var out="";while(this.next()){out+=this.tok()}return out};Parser.prototype.next=function(){return this.token=this.tokens.pop()};Parser.prototype.peek=function(){return this.tokens[this.tokens.length-1]||0};Parser.prototype.parseText=function(){var body=this.token.text;while(this.peek().type==="text"){body+="\n"+this.next().text}return this.inline.output(body)};Parser.prototype.tok=function(){switch(this.token.type){case"space":{return""}case"hr":{return this.renderer.hr()}case"heading":{return this.renderer.heading(this.inline.output(this.token.text),this.token.depth,this.token.text)}case"code":{return this.renderer.code(this.token.text,this.token.lang,this.token.escaped)}case"table":{var header="",body="",i,row,cell,flags,j;cell="";for(i=0;i/g,">").replace(/"/g,""").replace(/'/g,"'")}function unescape(html){return html.replace(/&([#\w]+);/g,function(_,n){n=n.toLowerCase();if(n==="colon")return":";if(n.charAt(0)==="#"){return n.charAt(1)==="x"?String.fromCharCode(parseInt(n.substring(2),16)):String.fromCharCode(+n.substring(1))}return""})}function replace(regex,opt){regex=regex.source;opt=opt||"";return function self(name,val){if(!name)return new RegExp(regex,opt);val=val.source||val;val=val.replace(/(^|[^\[])\^/g,"$1");regex=regex.replace(name,val);return self}}function noop(){}noop.exec=noop;function merge(obj){var i=1,target,key;for(;iAn error occured:

        "+escape(e.message+"",true)+"
        "}throw e}}marked.options=marked.setOptions=function(opt){merge(marked.defaults,opt);return marked};marked.defaults={gfm:true,tables:true,breaks:false,pedantic:false,sanitize:false,sanitizer:null,mangle:true,smartLists:false,silent:false,highlight:null,langPrefix:"lang-",smartypants:false,headerPrefix:"",renderer:new Renderer,xhtml:false};marked.Parser=Parser;marked.parser=Parser.parse;marked.Renderer=Renderer;marked.Lexer=Lexer;marked.lexer=Lexer.lex;marked.InlineLexer=InlineLexer;marked.inlineLexer=InlineLexer.output;marked.parse=marked;if(typeof module!=="undefined"&&typeof exports==="object"){module.exports=marked}else if(typeof define==="function"&&define.amd){define(function(){return marked})}else{this.marked=marked}}).call(function(){return this||(typeof window!=="undefined"?window:global)}()); \ No newline at end of file diff --git a/packages/node_modules/@node-red/nodes/core/common/20-inject.html b/packages/node_modules/@node-red/nodes/core/common/20-inject.html index 87ec3f86b..8aa3dc7b2 100644 --- a/packages/node_modules/@node-red/nodes/core/common/20-inject.html +++ b/packages/node_modules/@node-red/nodes/core/common/20-inject.html @@ -14,16 +14,14 @@ limitations under the License. --> - diff --git a/packages/node_modules/@node-red/nodes/core/common/20-inject.js b/packages/node_modules/@node-red/nodes/core/common/20-inject.js index c0d9e0c2f..54715e131 100644 --- a/packages/node_modules/@node-red/nodes/core/common/20-inject.js +++ b/packages/node_modules/@node-red/nodes/core/common/20-inject.js @@ -20,9 +20,32 @@ module.exports = function(RED) { function InjectNode(n) { RED.nodes.createNode(this,n); - this.topic = n.topic; - this.payload = n.payload; - this.payloadType = n.payloadType; + + /* Handle legacy */ + if(!Array.isArray(n.props)){ + n.props = []; + n.props.push({ + p:'payload', + v:n.payload, + vt:n.payloadType + }); + n.props.push({ + p:'topic', + v:n.topic, + vt:'str' + }); + } else { + for (var i=0,l=n.props.length; i 2147483) { node.error(RED._("inject.errors.toolong", this)); delete node.repeat; } node.repeaterSetup = function () { - if (this.repeat && !isNaN(this.repeat) && this.repeat > 0) { - this.repeat = this.repeat * 1000; - if (RED.settings.verbose) { - this.log(RED._("inject.repeat", this)); + if (this.repeat && !isNaN(this.repeat) && this.repeat > 0) { + this.repeat = this.repeat * 1000; + if (RED.settings.verbose) { + this.log(RED._("inject.repeat", this)); + } + this.interval_id = setInterval(function() { + node.emit("input", {}); + }, this.repeat); + } else if (this.crontab) { + if (RED.settings.verbose) { + this.log(RED._("inject.crontab", this)); + } + this.cronjob = new cron.CronJob(this.crontab, function() { node.emit("input", {}); }, null, true); } - this.interval_id = setInterval(function() { - node.emit("input", {}); - }, this.repeat); - } else if (this.crontab) { - if (RED.settings.verbose) { - this.log(RED._("inject.crontab", this)); - } - this.cronjob = new cron.CronJob(this.crontab, function() { node.emit("input", {}); }, null, true); - } }; if (this.once) { this.onceTimeout = setTimeout( function() { - node.emit("input",{}); - node.repeaterSetup(); + node.emit("input",{}); + node.repeaterSetup(); }, this.onceDelay); } else { - node.repeaterSetup(); + node.repeaterSetup(); } - this.on("input",function(msg) { - msg.topic = this.topic; - if (this.payloadType !== 'flow' && this.payloadType !== 'global') { - try { - if ( (this.payloadType == null && this.payload === "") || this.payloadType === "date") { - msg.payload = Date.now(); - } else if (this.payloadType == null) { - msg.payload = this.payload; - } else if (this.payloadType === 'none') { - msg.payload = ""; - } else { - msg.payload = RED.util.evaluateNodeProperty(this.payload,this.payloadType,this,msg); - } - this.send(msg); - msg = null; - } catch(err) { - this.error(err,msg); - } - } else { - RED.util.evaluateNodeProperty(this.payload,this.payloadType,this,msg, function(err,res) { - if (err) { - node.error(err,msg); - } else { - msg.payload = res; - node.send(msg); - } + this.on("input", function(msg) { + var errors = []; - }); + this.props.forEach(p => { + var property = p.p; + var value = p.v ? p.v : ''; + var valueType = p.vt ? p.vt : 'str'; + + if (!property) return; + + if (valueType === "jsonata") { + if (p.exp) { + try { + var val = RED.util.evaluateJSONataExpression(p.exp, msg); + RED.util.setMessageProperty(msg, property, val, true); + } + catch (err) { + errors.push(err.message); + } + } + return; + } + try { + RED.util.setMessageProperty(msg,property,RED.util.evaluateNodeProperty(value, valueType, this, msg),true); + } catch (err) { + errors.push(err.toString()); + } + }); + + if (errors.length) { + node.error(errors.join('; '), msg); + } else { + node.send(msg); } }); } diff --git a/packages/node_modules/@node-red/nodes/core/common/21-debug.html b/packages/node_modules/@node-red/nodes/core/common/21-debug.html index 5e2f3ba49..d12279c74 100644 --- a/packages/node_modules/@node-red/nodes/core/common/21-debug.html +++ b/packages/node_modules/@node-red/nodes/core/common/21-debug.html @@ -1,30 +1,35 @@ - - diff --git a/packages/node_modules/@node-red/nodes/core/common/lib/debug/debug-utils.js b/packages/node_modules/@node-red/nodes/core/common/lib/debug/debug-utils.js index ddab5bbe2..64068ea86 100644 --- a/packages/node_modules/@node-red/nodes/core/common/lib/debug/debug-utils.js +++ b/packages/node_modules/@node-red/nodes/core/common/lib/debug/debug-utils.js @@ -457,7 +457,7 @@ RED.debug = (function() { var metaRow = $('
        ').appendTo(msg); $(''+ getTimestamp()+'').appendTo(metaRow); if (sourceNode) { - $('
        ',{href:"#",class:"red-ui-debug-msg-name"}).text('node: '+sanitize(o.name||sourceNode.name||sourceNode.id)) + $('',{href:"#",class:"red-ui-debug-msg-name"}).text('node: '+(o.name||sourceNode.name||sourceNode.id)) .appendTo(metaRow) .on("click", function(evt) { evt.preventDefault(); diff --git a/packages/node_modules/@node-red/nodes/core/function/10-function.html b/packages/node_modules/@node-red/nodes/core/function/10-function.html index 9202d96ba..78e7f2d63 100644 --- a/packages/node_modules/@node-red/nodes/core/function/10-function.html +++ b/packages/node_modules/@node-red/nodes/core/function/10-function.html @@ -1,22 +1,57 @@ - diff --git a/packages/node_modules/@node-red/nodes/core/function/10-function.js b/packages/node_modules/@node-red/nodes/core/function/10-function.js index 65a1b4a61..a6573a787 100644 --- a/packages/node_modules/@node-red/nodes/core/function/10-function.js +++ b/packages/node_modules/@node-red/nodes/core/function/10-function.js @@ -57,22 +57,55 @@ module.exports = function(RED) { } } + function createVMOpt(node, kind) { + var opt = { + filename: 'Function node'+kind+':'+node.id+(node.name?' ['+node.name+']':''), // filename for stack traces + displayErrors: true + // Using the following options causes node 4/6 to not include the line number + // in the stack output. So don't use them. + // lineOffset: -11, // line number offset to be used for stack traces + // columnOffset: 0, // column number offset to be used for stack traces + }; + return opt; + } + + function updateErrorInfo(err) { + if (err.stack) { + var stack = err.stack.toString(); + var m = /^([^:]+):([^:]+):(\d+).*/.exec(stack); + if (m) { + var line = parseInt(m[3]) -1; + var kind = "body:"; + if (/setup/.exec(m[1])) { + kind = "setup:"; + } + if (/cleanup/.exec(m[1])) { + kind = "cleanup:"; + } + err.message += " ("+kind+"line "+line+")"; + } + } + } + function FunctionNode(n) { RED.nodes.createNode(this,n); var node = this; - this.name = n.name; - this.func = n.func; + node.name = n.name; + node.func = n.func; + node.ini = n.initialize ? n.initialize : ""; + node.fin = n.finalize ? n.finalize : ""; var handleNodeDoneCall = true; + // Check to see if the Function appears to call `node.done()`. If so, // we will assume it is well written and does actually call node.done(). // Otherwise, we will call node.done() after the function returns regardless. - if (/node\.done\s*\(\s*\)/.test(this.func)) { + if (/node\.done\s*\(\s*\)/.test(node.func)) { handleNodeDoneCall = false; } var functionText = "var results = null;"+ - "results = (function(msg,__send__,__done__){ "+ + "results = (async function(msg,__send__,__done__){ "+ "var __msgid__ = msg._msgid;"+ "var node = {"+ "id:__node__.id,"+ @@ -87,11 +120,13 @@ module.exports = function(RED) { "send:function(msgs,cloneMsg){ __node__.send(__send__,__msgid__,msgs,cloneMsg);},"+ "done:__done__"+ "};\n"+ - this.func+"\n"+ + node.func+"\n"+ "})(msg,send,done);"; - this.topic = n.topic; - this.outstandingTimers = []; - this.outstandingIntervals = []; + var finScript = null; + var finOpt = null; + node.topic = n.topic; + node.outstandingTimers = []; + node.outstandingIntervals = []; var sandbox = { console:console, util:util, @@ -182,12 +217,12 @@ module.exports = function(RED) { arguments[0] = function() { sandbox.clearTimeout(timerId); try { - func.apply(this,arguments); + func.apply(node,arguments); } catch(err) { node.error(err,{}); } }; - timerId = setTimeout.apply(this,arguments); + timerId = setTimeout.apply(node,arguments); node.outstandingTimers.push(timerId); return timerId; }, @@ -203,12 +238,12 @@ module.exports = function(RED) { var timerId; arguments[0] = function() { try { - func.apply(this,arguments); + func.apply(node,arguments); } catch(err) { node.error(err,{}); } }; - timerId = setInterval.apply(this,arguments); + timerId = setInterval.apply(node,arguments); node.outstandingIntervals.push(timerId); return timerId; }, @@ -226,37 +261,48 @@ module.exports = function(RED) { sandbox.setTimeout(function(){ resolve(value); }, after); }); }; + sandbox.promisify = util.promisify; } var context = vm.createContext(sandbox); try { - this.script = vm.createScript(functionText, { - filename: 'Function node:'+this.id+(this.name?' ['+this.name+']':''), // filename for stack traces - displayErrors: true - // Using the following options causes node 4/6 to not include the line number - // in the stack output. So don't use them. - // lineOffset: -11, // line number offset to be used for stack traces - // columnOffset: 0, // column number offset to be used for stack traces - }); - this.on("input", function(msg,send,done) { - try { - var start = process.hrtime(); - context.msg = msg; - context.send = send; - context.done = done; + var iniScript = null; + var iniOpt = null; + if (node.ini && (node.ini !== "")) { + var iniText = "(async function () {\n"+node.ini +"\n})();"; + 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})();"; + finOpt = createVMOpt(node, " cleanup"); + finScript = new vm.Script(finText, finOpt); + } + var promise = Promise.resolve(); + if (iniScript) { + promise = iniScript.runInContext(context, iniOpt); + } - this.script.runInContext(context); - sendResults(this,send,msg._msgid,context.results,false); + function processMessage(msg, send, done) { + var start = process.hrtime(); + context.msg = msg; + context.send = send; + context.done = done; + + node.script.runInContext(context); + context.results.then(function(results) { + sendResults(node,send,msg._msgid,results,false); if (handleNodeDoneCall) { done(); } var duration = process.hrtime(start); var converted = Math.floor((duration[0] * 1e9 + duration[1])/10000)/100; - this.metric("duration", msg, converted); + node.metric("duration", msg, converted); if (process.env.NODE_RED_FUNCTION_TIME) { - this.status({fill:"yellow",shape:"dot",text:""+converted}); + node.status({fill:"yellow",shape:"dot",text:""+converted}); } - } catch(err) { + }).catch(err => { if ((typeof err === "object") && err.hasOwnProperty("stack")) { //remove unwanted part var index = err.stack.search(/\n\s*at ContextifyScript.Script.runInContext/); @@ -294,23 +340,67 @@ module.exports = function(RED) { else { done(JSON.stringify(err)); } + }); + } + + 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); } }); - this.on("close", function() { + node.on("close", function() { + if (finScript) { + try { + finScript.runInContext(context, finOpt); + } + catch (err) { + node.error(err); + } + } while (node.outstandingTimers.length > 0) { clearTimeout(node.outstandingTimers.pop()); } while (node.outstandingIntervals.length > 0) { clearInterval(node.outstandingIntervals.pop()); } - this.status({}); + node.status({}); }); - } catch(err) { + + promise.then(function (v) { + var msgs = messages; + messages = []; + while (msgs.length > 0) { + msgs.forEach(function (s) { + processMessage(s.msg, s.send, s.done); + }); + msgs = messages; + messages = []; + } + state = RESOLVED; + }).catch((error) => { + messages = []; + state = ERROR; + node.error(error); + }); + + } + catch(err) { // eg SyntaxError - which v8 doesn't include line number information // so we can't do better than this - this.error(err); + updateErrorInfo(err); + node.error(err); } } RED.nodes.registerType("function",FunctionNode); RED.library.register("functions"); }; + diff --git a/packages/node_modules/@node-red/nodes/core/function/15-change.html b/packages/node_modules/@node-red/nodes/core/function/15-change.html index fc9fa8d84..59d4337c5 100644 --- a/packages/node_modules/@node-red/nodes/core/function/15-change.html +++ b/packages/node_modules/@node-red/nodes/core/function/15-change.html @@ -1,5 +1,10 @@ diff --git a/packages/node_modules/@node-red/nodes/core/function/89-trigger.js b/packages/node_modules/@node-red/nodes/core/function/89-trigger.js index 48a595ccc..cda1afadf 100644 --- a/packages/node_modules/@node-red/nodes/core/function/89-trigger.js +++ b/packages/node_modules/@node-red/nodes/core/function/89-trigger.js @@ -24,6 +24,8 @@ module.exports = function(RED) { this.op2 = n.op2 || "0"; this.op1type = n.op1type || "str"; this.op2type = n.op2type || "str"; + this.second = (n.outputs == 2) ? true : false; + this.topic = n.topic || "topic"; if (this.op1type === 'val') { if (this.op1 === 'true' || this.op1 === 'false') { @@ -76,6 +78,7 @@ module.exports = function(RED) { var node = this; node.topics = {}; + var npay = {}; var pendingMessages = []; var activeMessagePromise = null; var processMessageQueue = function(msg) { @@ -110,8 +113,15 @@ module.exports = function(RED) { processMessageQueue(msg); }); + var stat = function() { + var l = Object.keys(node.topics).length; + if (l === 0) { return {} } + else if (l === 1) { return {fill:"blue",shape:"dot"} } + else return {fill:"blue",shape:"dot",text:l}; + } + var processMessage = function(msg) { - var topic = msg.topic || "_none"; + var topic = RED.util.getMessageProperty(msg,node.topic) || "_none"; var promise; if (node.bytopic === "all") { topic = "_none"; } node.topics[topic] = node.topics[topic] || {}; @@ -119,12 +129,13 @@ module.exports = function(RED) { if (node.loop === true) { clearInterval(node.topics[topic].tout); } else { clearTimeout(node.topics[topic].tout); } delete node.topics[topic]; - node.status({}); + node.status(stat()); } else { + if (node.op2type === "payl") { npay[topic] = RED.util.cloneMessage(msg); } if (((!node.topics[topic].tout) && (node.topics[topic].tout !== 0)) || (node.loop === true)) { promise = Promise.resolve(); - if (node.op2type === "pay" || node.op2type === "payl") { node.topics[topic].m2 = RED.util.cloneMessage(msg.payload); } + if (node.op2type === "pay") { node.topics[topic].m2 = RED.util.cloneMessage(msg.payload); } else if (node.op2Templated) { node.topics[topic].m2 = mustache.render(node.op2,msg); } else if (node.op2type !== "nul") { promise = new Promise((resolve,reject) => { @@ -186,22 +197,30 @@ module.exports = function(RED) { }); } promise.then(() => { - msg2.payload = node.topics[topic].m2; + if (node.op2type === "payl") { + if (node.second === true) { node.send([null,npay[topic]]); } + else { node.send(npay[topic]); } + delete npay[topic]; + } + else { + msg2.payload = node.topics[topic].m2; + if (node.second === true) { node.send([null,msg2]); } + else { node.send(msg2); } + } delete node.topics[topic]; - node.send(msg2); - node.status({}); + node.status(stat()); }).catch(err => { node.error(err); }); } else { delete node.topics[topic]; - node.status({}); + node.status(stat()); } }, node.duration); } } - node.status({fill:"blue",shape:"dot",text:" "}); + node.status(stat()); if (node.op1type !== "nul") { node.send(RED.util.cloneMessage(msg)); } }); }); @@ -237,16 +256,17 @@ module.exports = function(RED) { } } delete node.topics[topic]; - node.status({}); - node.send(msg2); + node.status(stat()); + if (node.second === true) { node.send([null,msg2]); } + else { node.send(msg2); } }).catch(err => { node.error(err); }); }, node.duration); } - else { - if (node.op2type === "payl") { node.topics[topic].m2 = RED.util.cloneMessage(msg.payload); } - } + // else { + // if (node.op2type === "payl") {node.topics[topic].m2 = RED.util.cloneMessage(msg.payload); } + // } } return Promise.resolve(); } @@ -259,7 +279,7 @@ module.exports = function(RED) { delete node.topics[t]; } } - node.status({}); + node.status(stat()); }); } RED.nodes.registerType("trigger",TriggerNode); diff --git a/packages/node_modules/@node-red/nodes/core/function/90-exec.html b/packages/node_modules/@node-red/nodes/core/function/90-exec.html index c6102d397..4a4e8942c 100644 --- a/packages/node_modules/@node-red/nodes/core/function/90-exec.html +++ b/packages/node_modules/@node-red/nodes/core/function/90-exec.html @@ -14,7 +14,7 @@ limitations under the License. --> - - - - - - - - - diff --git a/packages/node_modules/@node-red/nodes/core/sequence/19-batch.html b/packages/node_modules/@node-red/nodes/core/sequence/19-batch.html index f3cb47a54..418ac605b 100644 --- a/packages/node_modules/@node-red/nodes/core/sequence/19-batch.html +++ b/packages/node_modules/@node-red/nodes/core/sequence/19-batch.html @@ -60,8 +60,8 @@
        - - + +
        diff --git a/packages/node_modules/@node-red/nodes/core/sequence/19-batch.js b/packages/node_modules/@node-red/nodes/core/sequence/19-batch.js index 495862af8..77574f222 100644 --- a/packages/node_modules/@node-red/nodes/core/sequence/19-batch.js +++ b/packages/node_modules/@node-red/nodes/core/sequence/19-batch.js @@ -179,6 +179,11 @@ module.exports = function(RED) { } node.pending = []; this.on("input", function(msg) { + if (msg.hasOwnProperty("reset")) { + node.pending = []; + node.pending_count = 0; + return; + } var queue = node.pending; queue.push(msg); node.pending_count++; @@ -204,11 +209,26 @@ module.exports = function(RED) { var interval = Number(n.interval || "0") *1000; var allow_empty_seq = n.allowEmptySequence; node.pending = [] - var timer = setInterval(function() { + function msgHandler() { send_interval(node, allow_empty_seq); node.pending_count = 0; - }, interval); + } + var timer = undefined; + if (interval > 0) { + timer = setInterval(msgHandler, interval); + } this.on("input", function(msg) { + if (msg.hasOwnProperty("reset")) { + if (timer !== undefined) { + clearInterval(timer); + } + node.pending = []; + node.pending_count = 0; + if (interval > 0) { + timer = setInterval(msgHandler, interval); + } + return; + } node.pending.push(msg); node.pending_count++; var max_msgs = max_kept_msgs_count(node); @@ -219,7 +239,9 @@ module.exports = function(RED) { } }); this.on("close", function() { - clearInterval(timer); + if (timer !== undefined) { + clearInterval(timer); + } node.pending = []; node.pending_count = 0; }); @@ -230,6 +252,11 @@ module.exports = function(RED) { }); node.pending = {}; this.on("input", function(msg) { + if (msg.hasOwnProperty("reset")) { + node.pending = {}; + node.pending_count = 0; + return; + } concat_msg(node, msg); }); this.on("close", function() { diff --git a/packages/node_modules/@node-red/nodes/core/storage/10-file.html b/packages/node_modules/@node-red/nodes/core/storage/10-file.html index d20ad75c5..5487920b8 100755 --- a/packages/node_modules/@node-red/nodes/core/storage/10-file.html +++ b/packages/node_modules/@node-red/nodes/core/storage/10-file.html @@ -1,5 +1,5 @@ - - - diff --git a/packages/node_modules/@node-red/nodes/locales/de/network/06-httpproxy.html b/packages/node_modules/@node-red/nodes/locales/de/network/06-httpproxy.html index 228da487b..1ece7efe0 100755 --- a/packages/node_modules/@node-red/nodes/locales/de/network/06-httpproxy.html +++ b/packages/node_modules/@node-red/nodes/locales/de/network/06-httpproxy.html @@ -14,7 +14,7 @@ limitations under the License. --> - - - - - - - - - - diff --git a/packages/node_modules/@node-red/nodes/locales/de/network/31-tcpin.html b/packages/node_modules/@node-red/nodes/locales/de/network/31-tcpin.html index 2b22418dc..51ec15c6f 100755 --- a/packages/node_modules/@node-red/nodes/locales/de/network/31-tcpin.html +++ b/packages/node_modules/@node-red/nodes/locales/de/network/31-tcpin.html @@ -14,13 +14,13 @@ limitations under the License. --> - - - - - - - diff --git a/packages/node_modules/@node-red/nodes/locales/en-US/function/90-exec.html b/packages/node_modules/@node-red/nodes/locales/en-US/function/90-exec.html index 69447bc60..ff515e781 100644 --- a/packages/node_modules/@node-red/nodes/locales/en-US/function/90-exec.html +++ b/packages/node_modules/@node-red/nodes/locales/en-US/function/90-exec.html @@ -14,7 +14,7 @@ limitations under the License. --> - diff --git a/packages/node_modules/@node-red/nodes/locales/en-US/network/06-httpproxy.html b/packages/node_modules/@node-red/nodes/locales/en-US/network/06-httpproxy.html index b17012e72..12bb3684c 100644 --- a/packages/node_modules/@node-red/nodes/locales/en-US/network/06-httpproxy.html +++ b/packages/node_modules/@node-red/nodes/locales/en-US/network/06-httpproxy.html @@ -14,7 +14,7 @@ limitations under the License. --> - - - - - - - diff --git a/packages/node_modules/@node-red/nodes/locales/en-US/network/31-tcpin.html b/packages/node_modules/@node-red/nodes/locales/en-US/network/31-tcpin.html index e8cb29ffc..173f003f7 100644 --- a/packages/node_modules/@node-red/nodes/locales/en-US/network/31-tcpin.html +++ b/packages/node_modules/@node-red/nodes/locales/en-US/network/31-tcpin.html @@ -14,14 +14,14 @@ limitations under the License. --> - - - diff --git a/packages/node_modules/@node-red/nodes/locales/en-US/network/32-udp.html b/packages/node_modules/@node-red/nodes/locales/en-US/network/32-udp.html index 09e6aef34..666889be6 100644 --- a/packages/node_modules/@node-red/nodes/locales/en-US/network/32-udp.html +++ b/packages/node_modules/@node-red/nodes/locales/en-US/network/32-udp.html @@ -14,7 +14,7 @@ limitations under the License. --> - - diff --git a/packages/node_modules/@node-red/nodes/locales/en-US/parsers/70-HTML.html b/packages/node_modules/@node-red/nodes/locales/en-US/parsers/70-HTML.html index b9231663d..7b081976c 100644 --- a/packages/node_modules/@node-red/nodes/locales/en-US/parsers/70-HTML.html +++ b/packages/node_modules/@node-red/nodes/locales/en-US/parsers/70-HTML.html @@ -14,7 +14,7 @@ limitations under the License. --> - - diff --git a/packages/node_modules/@node-red/nodes/locales/en-US/storage/10-file.html b/packages/node_modules/@node-red/nodes/locales/en-US/storage/10-file.html index 26701d706..fe2bbc324 100644 --- a/packages/node_modules/@node-red/nodes/locales/en-US/storage/10-file.html +++ b/packages/node_modules/@node-red/nodes/locales/en-US/storage/10-file.html @@ -14,7 +14,7 @@ limitations under the License. --> - - diff --git a/packages/node_modules/@node-red/nodes/locales/en-US/storage/23-watch.html b/packages/node_modules/@node-red/nodes/locales/en-US/storage/23-watch.html index 4a28cfcab..bc6e4ad31 100644 --- a/packages/node_modules/@node-red/nodes/locales/en-US/storage/23-watch.html +++ b/packages/node_modules/@node-red/nodes/locales/en-US/storage/23-watch.html @@ -14,7 +14,7 @@ limitations under the License. --> - - diff --git a/packages/node_modules/@node-red/nodes/locales/ja/function/90-exec.html b/packages/node_modules/@node-red/nodes/locales/ja/function/90-exec.html index 7eb75b10a..d034f71e1 100644 --- a/packages/node_modules/@node-red/nodes/locales/ja/function/90-exec.html +++ b/packages/node_modules/@node-red/nodes/locales/ja/function/90-exec.html @@ -14,7 +14,7 @@ limitations under the License. --> - diff --git a/packages/node_modules/@node-red/nodes/locales/ja/network/06-httpproxy.html b/packages/node_modules/@node-red/nodes/locales/ja/network/06-httpproxy.html index 03beddb8d..24b15aa29 100644 --- a/packages/node_modules/@node-red/nodes/locales/ja/network/06-httpproxy.html +++ b/packages/node_modules/@node-red/nodes/locales/ja/network/06-httpproxy.html @@ -14,7 +14,7 @@ limitations under the License. --> - - - - - - - diff --git a/packages/node_modules/@node-red/nodes/locales/ja/network/31-tcpin.html b/packages/node_modules/@node-red/nodes/locales/ja/network/31-tcpin.html index efe82cbb1..e63c93ae5 100644 --- a/packages/node_modules/@node-red/nodes/locales/ja/network/31-tcpin.html +++ b/packages/node_modules/@node-red/nodes/locales/ja/network/31-tcpin.html @@ -14,12 +14,12 @@ limitations under the License. --> - - - diff --git a/packages/node_modules/@node-red/nodes/locales/ja/network/32-udp.html b/packages/node_modules/@node-red/nodes/locales/ja/network/32-udp.html index ec07a865e..fffcbcec5 100644 --- a/packages/node_modules/@node-red/nodes/locales/ja/network/32-udp.html +++ b/packages/node_modules/@node-red/nodes/locales/ja/network/32-udp.html @@ -14,13 +14,13 @@ limitations under the License. --> - - diff --git a/packages/node_modules/@node-red/nodes/locales/ja/parsers/70-HTML.html b/packages/node_modules/@node-red/nodes/locales/ja/parsers/70-HTML.html index eb7b00dff..19be36aed 100644 --- a/packages/node_modules/@node-red/nodes/locales/ja/parsers/70-HTML.html +++ b/packages/node_modules/@node-red/nodes/locales/ja/parsers/70-HTML.html @@ -14,7 +14,7 @@ limitations under the License. --> - - diff --git a/packages/node_modules/@node-red/nodes/locales/ja/storage/10-file.html b/packages/node_modules/@node-red/nodes/locales/ja/storage/10-file.html index 2df174104..21d4af9be 100644 --- a/packages/node_modules/@node-red/nodes/locales/ja/storage/10-file.html +++ b/packages/node_modules/@node-red/nodes/locales/ja/storage/10-file.html @@ -14,7 +14,7 @@ limitations under the License. --> - - diff --git a/packages/node_modules/@node-red/nodes/locales/ja/storage/23-watch.html b/packages/node_modules/@node-red/nodes/locales/ja/storage/23-watch.html index 45f658c05..3a4594ccc 100644 --- a/packages/node_modules/@node-red/nodes/locales/ja/storage/23-watch.html +++ b/packages/node_modules/@node-red/nodes/locales/ja/storage/23-watch.html @@ -14,7 +14,7 @@ limitations under the License. --> - diff --git a/packages/node_modules/@node-red/nodes/locales/ko/network/06-httpproxy.html b/packages/node_modules/@node-red/nodes/locales/ko/network/06-httpproxy.html index 4357c7f2b..cdfccd799 100644 --- a/packages/node_modules/@node-red/nodes/locales/ko/network/06-httpproxy.html +++ b/packages/node_modules/@node-red/nodes/locales/ko/network/06-httpproxy.html @@ -14,7 +14,7 @@ limitations under the License. --> - - - - - - - diff --git a/packages/node_modules/@node-red/nodes/locales/ko/network/31-tcpin.html b/packages/node_modules/@node-red/nodes/locales/ko/network/31-tcpin.html index 1c665bb8d..53c61e679 100644 --- a/packages/node_modules/@node-red/nodes/locales/ko/network/31-tcpin.html +++ b/packages/node_modules/@node-red/nodes/locales/ko/network/31-tcpin.html @@ -14,12 +14,12 @@ limitations under the License. --> - - - - - - diff --git a/packages/node_modules/@node-red/nodes/locales/zh-CN/common/21-debug.html b/packages/node_modules/@node-red/nodes/locales/zh-CN/common/21-debug.html new file mode 100644 index 000000000..e3137c50d --- /dev/null +++ b/packages/node_modules/@node-red/nodes/locales/zh-CN/common/21-debug.html @@ -0,0 +1,25 @@ + + + diff --git a/packages/node_modules/@node-red/nodes/locales/zh-CN/common/24-complete.html b/packages/node_modules/@node-red/nodes/locales/zh-CN/common/24-complete.html new file mode 100644 index 000000000..e69ebc6a0 --- /dev/null +++ b/packages/node_modules/@node-red/nodes/locales/zh-CN/common/24-complete.html @@ -0,0 +1,24 @@ + + + diff --git a/packages/node_modules/@node-red/nodes/locales/zh-CN/common/25-catch.html b/packages/node_modules/@node-red/nodes/locales/zh-CN/common/25-catch.html new file mode 100644 index 000000000..5b2b4c3b2 --- /dev/null +++ b/packages/node_modules/@node-red/nodes/locales/zh-CN/common/25-catch.html @@ -0,0 +1,36 @@ + + + diff --git a/packages/node_modules/@node-red/nodes/locales/zh-CN/common/25-status.html b/packages/node_modules/@node-red/nodes/locales/zh-CN/common/25-status.html new file mode 100644 index 000000000..7d9504c9f --- /dev/null +++ b/packages/node_modules/@node-red/nodes/locales/zh-CN/common/25-status.html @@ -0,0 +1,33 @@ + + + diff --git a/packages/node_modules/@node-red/nodes/locales/zh-CN/common/60-link.html b/packages/node_modules/@node-red/nodes/locales/zh-CN/common/60-link.html new file mode 100644 index 000000000..6f2bc5f99 --- /dev/null +++ b/packages/node_modules/@node-red/nodes/locales/zh-CN/common/60-link.html @@ -0,0 +1,31 @@ + + + + + diff --git a/packages/node_modules/@node-red/nodes/locales/zh-CN/common/90-comment.html b/packages/node_modules/@node-red/nodes/locales/zh-CN/common/90-comment.html new file mode 100644 index 000000000..f98577ff4 --- /dev/null +++ b/packages/node_modules/@node-red/nodes/locales/zh-CN/common/90-comment.html @@ -0,0 +1,21 @@ + + + diff --git a/packages/node_modules/@node-red/nodes/locales/zh-CN/common/98-unknown.html b/packages/node_modules/@node-red/nodes/locales/zh-CN/common/98-unknown.html new file mode 100644 index 000000000..108e19228 --- /dev/null +++ b/packages/node_modules/@node-red/nodes/locales/zh-CN/common/98-unknown.html @@ -0,0 +1,24 @@ + + + diff --git a/packages/node_modules/@node-red/nodes/locales/zh-CN/function/10-function.html b/packages/node_modules/@node-red/nodes/locales/zh-CN/function/10-function.html new file mode 100644 index 000000000..035ccc81f --- /dev/null +++ b/packages/node_modules/@node-red/nodes/locales/zh-CN/function/10-function.html @@ -0,0 +1,51 @@ + + + diff --git a/packages/node_modules/@node-red/nodes/locales/zh-CN/function/10-switch.html b/packages/node_modules/@node-red/nodes/locales/zh-CN/function/10-switch.html new file mode 100644 index 000000000..7ad25c24f --- /dev/null +++ b/packages/node_modules/@node-red/nodes/locales/zh-CN/function/10-switch.html @@ -0,0 +1,37 @@ + + + diff --git a/packages/node_modules/@node-red/nodes/locales/zh-CN/function/15-change.html b/packages/node_modules/@node-red/nodes/locales/zh-CN/function/15-change.html new file mode 100644 index 000000000..fcba3fed4 --- /dev/null +++ b/packages/node_modules/@node-red/nodes/locales/zh-CN/function/15-change.html @@ -0,0 +1,33 @@ + + + diff --git a/packages/node_modules/@node-red/nodes/locales/zh-CN/function/16-range.html b/packages/node_modules/@node-red/nodes/locales/zh-CN/function/16-range.html new file mode 100644 index 000000000..b5d2d033f --- /dev/null +++ b/packages/node_modules/@node-red/nodes/locales/zh-CN/function/16-range.html @@ -0,0 +1,40 @@ + + + diff --git a/packages/node_modules/@node-red/nodes/locales/zh-CN/function/80-template.html b/packages/node_modules/@node-red/nodes/locales/zh-CN/function/80-template.html new file mode 100644 index 000000000..938a77818 --- /dev/null +++ b/packages/node_modules/@node-red/nodes/locales/zh-CN/function/80-template.html @@ -0,0 +1,46 @@ + + + diff --git a/packages/node_modules/@node-red/nodes/locales/zh-CN/function/89-delay.html b/packages/node_modules/@node-red/nodes/locales/zh-CN/function/89-delay.html new file mode 100644 index 000000000..690bddaea --- /dev/null +++ b/packages/node_modules/@node-red/nodes/locales/zh-CN/function/89-delay.html @@ -0,0 +1,32 @@ + + + diff --git a/packages/node_modules/@node-red/nodes/locales/zh-CN/function/89-trigger.html b/packages/node_modules/@node-red/nodes/locales/zh-CN/function/89-trigger.html new file mode 100644 index 000000000..5f27a5002 --- /dev/null +++ b/packages/node_modules/@node-red/nodes/locales/zh-CN/function/89-trigger.html @@ -0,0 +1,33 @@ + + + diff --git a/packages/node_modules/@node-red/nodes/locales/zh-CN/function/90-exec.html b/packages/node_modules/@node-red/nodes/locales/zh-CN/function/90-exec.html new file mode 100644 index 000000000..27c421160 --- /dev/null +++ b/packages/node_modules/@node-red/nodes/locales/zh-CN/function/90-exec.html @@ -0,0 +1,74 @@ + + + diff --git a/packages/node_modules/@node-red/nodes/locales/zh-CN/messages.json b/packages/node_modules/@node-red/nodes/locales/zh-CN/messages.json index 9202bb3a7..934cc734d 100644 --- a/packages/node_modules/@node-red/nodes/locales/zh-CN/messages.json +++ b/packages/node_modules/@node-red/nodes/locales/zh-CN/messages.json @@ -6,7 +6,9 @@ "name": "名称", "username": "用户名", "password": "密码", - "property": "属性" + "property": "属性", + "selectNodes": "选择节点...", + "expand": "扩展" }, "status": { "connected": "已连接", @@ -35,7 +37,22 @@ "stopped": "停止", "failed": "注入失败: __error__", "label": { - "repeat": "重复" + "repeat": "重复", + "flow": "流上下午", + "global": "全局上下文", + "str": "字符串", + "num": "数值", + "bool": "布尔值", + "json": "JSON对象", + "bin": "buffer", + "date": "时间戳", + "env": "环境变量", + "object": "对象", + "string": "字符串", + "boolean": "布尔值", + "number": "数值", + "Array": "数组", + "invalid": "无效的JSON对象" }, "timestamp": "时间戳", "none": "无", @@ -70,15 +87,13 @@ } }, "catch": { - "catch": "监测所有节点", - "catchNodes": "监测__number__个节点", + "catch": "捕获:所有节点", + "catchNodes": "捕获:__number__个节点", + "catchUncaught": "捕获:未捕获", "label": { - "source": "监测范围", - "node": "节点", - "type": "类型", + "source": "捕获范围", "selectAll": "全选", - "sortByLabel": "按名称排序", - "sortByType": "按类型排序" + "uncaught": "忽略其他捕获节点处理的错误" }, "scope": { "all": "所有节点", @@ -90,10 +105,6 @@ "statusNodes": "报告__number__个节点状态", "label": { "source": "报告状态范围", - "node": "节点", - "type": "类型", - "selectAll": "全选", - "sortByLabel": "按名称排序", "sortByType": "按类型排序" }, "scope": { @@ -101,8 +112,13 @@ "selected": "指定节点" } }, + "complete": { + "completeNodes": "完成: __number__个节点" + }, "debug": { "output": "输出", + "none": "None", + "invalid-exp": "无效的JSONata表达式: __error__", "msgprop": "信息属性", "msgobj": "完整信息", "to": "目标", @@ -124,7 +140,11 @@ "filterCurrent": "当前流程", "debugNodes": "调试节点", "clearLog": "清空日志", - "openWindow": "在新窗口打开" + "filterLog": "过滤日志", + "openWindow": "在新窗口打开", + "copyPath": "复制路径", + "copyPayload": "复制值", + "pinPath": "固定展开" }, "messageMenu": { "collapseAll": "折叠所有路径", @@ -146,26 +166,33 @@ "key": "私钥", "passphrase": "密码", "ca": "CA证书", - "verify-server-cert":"验证服务器证书" + "verify-server-cert":"验证服务器证书", + "servername": "服务器名" }, "placeholder": { "cert":"证书路径 (PEM 格式)", "key":"私钥路径 (PEM 格式)", "ca":"CA证书路径 (PEM 格式)", - "passphrase":"私钥密码 (可选)" + "passphrase":"私钥密码 (可选)", + "servername":"用于SNI" }, "error": { "missing-file": "未提供证书/密钥文件" } }, "exec": { + "exec": "exec", + "spawn": "spawn", "label": { "command": "命令", "append": "追加", "timeout": "超时", "timeoutplace": "可选填", "return": "输出", - "seconds": "秒" + "seconds": "秒", + "stdout": "标准输出", + "stderr": "标准错误输出", + "retcode": "返回码" }, "placeholder": { "extraparams": "额外的输入参数" @@ -177,6 +204,7 @@ "oldrc": "使用旧式输出 (兼容模式)" }, "function": { + "function": "函数", "label": { "function": "函数", "outputs": "输出" @@ -187,6 +215,7 @@ } }, "template": { + "template": "模板", "label": { "template": "模版", "property": "属性", @@ -199,7 +228,7 @@ "yaml": "YAML", "none": "无" }, - "templatevalue": "This is the payload: {{payload}} !" + "templatevalue": "这是有效载荷: {{payload}} !" }, "delay": { "action": "行为设置", @@ -272,6 +301,9 @@ "wait-reset": "等待被重置", "wait-for": "等待", "wait-loop": "周期性重发", + "for": "处理", + "bytopics": "每个msg.topic", + "alltopics": "所有消息", "duration": { "ms": "毫秒", "s": "秒", @@ -290,6 +322,7 @@ } }, "comment": { + "comment": "注释" }, "unknown": { "label": { @@ -300,9 +333,10 @@ "mqtt": { "label": { "broker": "服务端", - "example": "e.g. localhost", + "example": "比如:本地主机", "output": "输出", "qos": "QoS", + "retain": "保持", "clientid": "客户端ID", "port": "端口", "keepalive": "Keepalive计时(秒)", @@ -312,17 +346,22 @@ "verify-server-cert":"验证服务器证书", "compatmode": "使用旧式MQTT 3.1支持" }, + "sections-label":{ + "birth-message": "连接时发送的消息(出生消息)", + "will-message":"意外断开连接时的发送消息(Will消息)", + "close-message":"断开连接前发送的消息(关闭消息)" + }, "tabs-label": { "connection": "连接", "security": "安全", - "will": "Will信息", - "birth": "Birth信息" + "messages": "消息" }, "placeholder": { "clientid": "留白则自动生成", "clientid-nonclean":"如非新会话,必须设置客户端ID", "will-topic": "留白将禁止Will信息", - "birth-topic": "留白将禁止Birth信息" + "birth-topic": "留白将禁止Birth信息", + "close-topic": "留白以禁用关闭消息" }, "state": { "connected": "已连接到服务端: __broker__", @@ -333,7 +372,9 @@ "output": { "buffer": "Buffer", "string": "字符串", - "base64": "Base64编码字符串" + "base64": "Base64编码字符串", + "auto": "自动检测 (字符串或buffer)", + "json": "解析的JSON对象" }, "true": "是", "false": "否", @@ -342,7 +383,9 @@ "not-defined": "主题未设置", "missing-config": "未设置服务端", "invalid-topic": "主题无效", - "nonclean-missingclientid": "客户端ID未设定,使用新会话" + "nonclean-missingclientid": "客户端ID未设定,使用新会话", + "invalid-json-string": "无效的JSON字符串", + "invalid-json-parse": "无法解析JSON字符串" } }, "httpin": { @@ -353,13 +396,27 @@ "return": "返回", "upload": "接受文件上传?", "status": "状态码", - "headers": "Header", - "other": "其他" + "headers": "头", + "other": "其他", + "paytoqs" : "将msg.payload附加为查询字符串参数", + "utf8String": "UTF8格式的字符串", + "binaryBuffer": "二进制buffer", + "jsonObject": "解析的JSON对象", + "authType": "类型", + "bearerToken": "Token" }, "setby": "- 用 msg.method 设定 -", "basicauth": "基本认证", "use-tls": "使用安全连接 (SSL/TLS) ", "tls-config":"TLS 设置", + "basic": "基本认证", + "digest": "摘要认证", + "bearer": "bearer认证", + "use-proxy": "使用代理服务器", + "persist": "对连接启用keep-alive", + "proxy-config": "代理服务器设置", + "use-proxyauth": "使用代理身份验证", + "noproxy-hosts": "代理例外", "utf8": "UTF-8 字符串", "binary": "二进制数据", "json": "JSON对象", @@ -376,7 +433,10 @@ "json-error": "JSON 解析错误", "no-url": "未设定 URL", "deprecated-call":"__method__方法已弃用", - "invalid-transport":"非HTTP传输请求" + "invalid-transport":"非HTTP传输请求", + "timeout-isnan": "超时值不是有效数字,忽略", + "timeout-isnegative": "超时值为负,忽略", + "invalid-payload": "无效的有效载荷" }, "status": { "requesting": "请求中" @@ -395,17 +455,23 @@ "message": "完整信息", "tip": { "path1": "默认情况下,payload将包含要发送或从Websocket接收的数据。侦听器可以配置为以JSON格式的字符串发送或接收整个消息对象.", - "path2": "这条路径将相对于 ", + "path2": "这条路径将相对于 __path__.", "url1": "URL 应该使用ws://或者wss://方案并指向现有的websocket侦听器.", "url2": "默认情况下,payload 将包含要发送或从Websocket接收的数据。可以将客户端配置为以JSON格式的字符串发送或接收整个消息对象." }, + "status": { + "connected": "连接数 __count__", + "connected_plural": "连接数 __count__" + }, "errors": { "connect-error": "ws连接发生了错误: ", "send-error": "发送时发生了错误: ", - "missing-conf": "未设置服务器" + "missing-conf": "未设置服务器", + "duplicate-path": "同一路径上不能有两个WebSocket侦听器: __path__" } }, "watch": { + "watch": "watch", "label": { "files": "文件", "recursive": "递归所有子文件夹" @@ -421,7 +487,7 @@ "output": "输出", "port": "端口", "host": "主机地址", - "payload": "的有效载荷", + "payload": "有效载荷", "delimited": "分隔符号", "close-connection": "是否在成功发送每条信息后断开连接?", "decode-base64": "用 Base64 解码信息?", @@ -480,7 +546,6 @@ "output": "输出", "group": "组", "interface": "本地IP", - "interfaceprompt": "(可选)本地 IP 绑定到", "send": "发送一个", "toport": "到端口", "address": "地址", @@ -488,6 +553,7 @@ }, "placeholder": { "interface": "(可选)eth0的IP地址", + "interfaceprompt": "(可选) 要绑定的本地接口或地址", "address": "目标IP地址" }, "udpmsgs": "udp信息", @@ -529,15 +595,18 @@ "ip-notset": "udp: IP地址未设定", "port-notset": "udp: 端口未设定", "port-invalid": "udp: 无效端口号码", - "alreadyused": "udp: 端口已被占用" + "alreadyused": "udp: 端口已被占用", + "ifnotfound": "udp: 接口 __iface__ 未发现" } }, "switch": { + "switch": "switch", "label": { "property": "属性", "rule": "规则", "repair" : "重建信息队列" }, + "previous": "先前值", "and": "与", "checkall": "全选所有规则", "stopfirst": "接受第一条匹配信息后停止", @@ -550,11 +619,15 @@ "false":"为假", "null":"为空", "nnull":"非空", - "head":"head", - "tail":"tail", - "index":"index between", + "istype": "类型是", + "empty": "为空", + "nempty": "非空", + "head":"头", + "tail":"尾", + "index":"索引在..中间", "exp":"JSONata表达式", - "else":"除此以外" + "else":"除此以外", + "hask": "拥有键" }, "errors": { "invalid-expr": "无效的JSONata表达式: __error__", @@ -588,6 +661,7 @@ } }, "range": { + "range": "range", "label": { "action": "操作", "inputrange": "映射输入数据", @@ -623,7 +697,8 @@ "firstrow": "第一行包含列名", "output": "输出", "includerow": "包含列名行", - "newline": "换行符" + "newline": "换行符", + "usestrings": "解析数值" }, "placeholder": { "columns": "用逗号分割列名" @@ -654,7 +729,8 @@ "html": { "label": { "select": "选取项", - "output": "输出" + "output": "输出", + "in": "in" }, "output": { "html": "选定元素的html内容", @@ -670,7 +746,9 @@ "errors": { "dropped-object": "忽略非对象格式的有效负载", "dropped": "忽略不支持格式的有效负载类型", - "dropped-error": "转换有效负载失败" + "dropped-error": "转换有效负载失败", + "schema-error": "JSON架构错误", + "schema-error-compile": "JSON架构错误: 未能编译架构" }, "label": { "o2j": "对象至JSON", @@ -713,7 +791,10 @@ "breaklines": "分拆成行", "filelabel": "文件", "sendError": "发生错误时发送消息(传统模式)", - "deletelabel": "删除 __file__" + "deletelabel": "删除 __file__", + "encoding": "编码", + "utf8String": "UTF8字符串", + "binaryBuffer": "二进制buffer" }, "action": { "append": "追加至文件", @@ -731,6 +812,21 @@ "deletedfile": "删除文件: __file__", "appendedfile": "追加至文件: __file__" }, + "encoding": { + "none": "默认", + "native": "Native", + "unicode": "Unicode", + "japanese": "日本", + "chinese": "中国", + "korean": "韩国", + "taiwan": "台湾/香港", + "windows": "Windows代码页", + "iso": "ISO代码页", + "ibm": "IBM代码页", + "mac": "Mac代码页", + "koi8": "KOI8代码页", + "misc": "其它" + }, "errors": { "nofilename": "未指定文件名", "invaliddelete": "警告:无效删除。请在配置对话框中使用特定的删除选项", @@ -742,6 +838,7 @@ "tip": "提示: 文件名应该是绝对路径,否则它将相对于Node-RED进程的工作目录。" }, "split": { + "split": "split", "intro":"基于以下类型拆分msg.payload:", "object":"对象", "objectSend":"每个键值对作为单个消息发送", @@ -753,6 +850,7 @@ "addname":" 复制键到 " }, "join":{ + "join": "join", "mode":{ "mode":"模式", "auto":"自动", @@ -761,6 +859,7 @@ "custom":"手动" }, "combine":"合并每个", + "completeMessage": "完整的消息", "create":"输出为", "type":{ "string":"字符串", @@ -799,6 +898,7 @@ } }, "sort" : { + "sort": "排序", "target" : "排序属性", "seq" : "信息队列", "key" : "键值", @@ -807,11 +907,12 @@ "ascending" : "升序", "descending" : "降序", "as-number" : "作为数值", - "invalid-exp" : "sort节点中存在无效的JSONata表达式", - "too-many" : "sort节点中有太多待定信息", - "clear" : "清空sort节点中的待定信息" + "invalid-exp" : "排序节点中存在无效的JSONata表达式", + "too-many" : "排序节点中有太多待定信息", + "clear" : "清空排序节点中的待定信息" }, "batch" : { + "batch": "batch", "mode": { "label" : "模式", "num-msgs" : "按指定数量分组", diff --git a/packages/node_modules/@node-red/nodes/locales/zh-CN/network/05-tls.html b/packages/node_modules/@node-red/nodes/locales/zh-CN/network/05-tls.html new file mode 100644 index 000000000..5a9603946 --- /dev/null +++ b/packages/node_modules/@node-red/nodes/locales/zh-CN/network/05-tls.html @@ -0,0 +1,19 @@ + + + diff --git a/packages/node_modules/@node-red/nodes/locales/zh-CN/network/06-httpproxy.html b/packages/node_modules/@node-red/nodes/locales/zh-CN/network/06-httpproxy.html new file mode 100644 index 000000000..e84971973 --- /dev/null +++ b/packages/node_modules/@node-red/nodes/locales/zh-CN/network/06-httpproxy.html @@ -0,0 +1,22 @@ + + + diff --git a/packages/node_modules/@node-red/nodes/locales/zh-CN/network/10-mqtt.html b/packages/node_modules/@node-red/nodes/locales/zh-CN/network/10-mqtt.html new file mode 100644 index 000000000..520d7f4ef --- /dev/null +++ b/packages/node_modules/@node-red/nodes/locales/zh-CN/network/10-mqtt.html @@ -0,0 +1,71 @@ + + + + + + + diff --git a/packages/node_modules/@node-red/nodes/locales/zh-CN/network/21-httpin.html b/packages/node_modules/@node-red/nodes/locales/zh-CN/network/21-httpin.html new file mode 100644 index 000000000..0fb52efd1 --- /dev/null +++ b/packages/node_modules/@node-red/nodes/locales/zh-CN/network/21-httpin.html @@ -0,0 +1,81 @@ + + + + + diff --git a/packages/node_modules/@node-red/nodes/locales/zh-CN/network/21-httprequest.html b/packages/node_modules/@node-red/nodes/locales/zh-CN/network/21-httprequest.html new file mode 100644 index 000000000..3c7b163e2 --- /dev/null +++ b/packages/node_modules/@node-red/nodes/locales/zh-CN/network/21-httprequest.html @@ -0,0 +1,78 @@ + + + diff --git a/packages/node_modules/@node-red/nodes/locales/zh-CN/network/22-websocket.html b/packages/node_modules/@node-red/nodes/locales/zh-CN/network/22-websocket.html new file mode 100644 index 000000000..d2ee29dfa --- /dev/null +++ b/packages/node_modules/@node-red/nodes/locales/zh-CN/network/22-websocket.html @@ -0,0 +1,35 @@ + + + + + + + + + diff --git a/packages/node_modules/@node-red/nodes/locales/zh-CN/network/31-tcpin.html b/packages/node_modules/@node-red/nodes/locales/zh-CN/network/31-tcpin.html new file mode 100644 index 000000000..2f00ed5f5 --- /dev/null +++ b/packages/node_modules/@node-red/nodes/locales/zh-CN/network/31-tcpin.html @@ -0,0 +1,35 @@ + + + + + + + diff --git a/packages/node_modules/@node-red/nodes/locales/zh-CN/network/32-udp.html b/packages/node_modules/@node-red/nodes/locales/zh-CN/network/32-udp.html new file mode 100644 index 000000000..1e01aa0a3 --- /dev/null +++ b/packages/node_modules/@node-red/nodes/locales/zh-CN/network/32-udp.html @@ -0,0 +1,28 @@ + + + + + diff --git a/packages/node_modules/@node-red/nodes/locales/zh-CN/parsers/70-CSV.html b/packages/node_modules/@node-red/nodes/locales/zh-CN/parsers/70-CSV.html new file mode 100644 index 000000000..5657f4cd3 --- /dev/null +++ b/packages/node_modules/@node-red/nodes/locales/zh-CN/parsers/70-CSV.html @@ -0,0 +1,43 @@ + + + diff --git a/packages/node_modules/@node-red/nodes/locales/zh-CN/parsers/70-HTML.html b/packages/node_modules/@node-red/nodes/locales/zh-CN/parsers/70-HTML.html new file mode 100644 index 000000000..73b365600 --- /dev/null +++ b/packages/node_modules/@node-red/nodes/locales/zh-CN/parsers/70-HTML.html @@ -0,0 +1,33 @@ + + + diff --git a/packages/node_modules/@node-red/nodes/locales/zh-CN/parsers/70-JSON.html b/packages/node_modules/@node-red/nodes/locales/zh-CN/parsers/70-JSON.html new file mode 100644 index 000000000..2e574a0bf --- /dev/null +++ b/packages/node_modules/@node-red/nodes/locales/zh-CN/parsers/70-JSON.html @@ -0,0 +1,43 @@ + + + diff --git a/packages/node_modules/@node-red/nodes/locales/zh-CN/parsers/70-XML.html b/packages/node_modules/@node-red/nodes/locales/zh-CN/parsers/70-XML.html new file mode 100644 index 000000000..969f410a0 --- /dev/null +++ b/packages/node_modules/@node-red/nodes/locales/zh-CN/parsers/70-XML.html @@ -0,0 +1,48 @@ + + + diff --git a/packages/node_modules/@node-red/nodes/locales/zh-CN/parsers/70-YAML.html b/packages/node_modules/@node-red/nodes/locales/zh-CN/parsers/70-YAML.html new file mode 100644 index 000000000..e65d1b87d --- /dev/null +++ b/packages/node_modules/@node-red/nodes/locales/zh-CN/parsers/70-YAML.html @@ -0,0 +1,34 @@ + + + diff --git a/packages/node_modules/@node-red/nodes/locales/zh-CN/sequence/17-split.html b/packages/node_modules/@node-red/nodes/locales/zh-CN/sequence/17-split.html new file mode 100644 index 000000000..22f01832a --- /dev/null +++ b/packages/node_modules/@node-red/nodes/locales/zh-CN/sequence/17-split.html @@ -0,0 +1,133 @@ + + + + + diff --git a/packages/node_modules/@node-red/nodes/locales/zh-CN/sequence/18-sort.html b/packages/node_modules/@node-red/nodes/locales/zh-CN/sequence/18-sort.html new file mode 100644 index 000000000..226355a8c --- /dev/null +++ b/packages/node_modules/@node-red/nodes/locales/zh-CN/sequence/18-sort.html @@ -0,0 +1,41 @@ + + + diff --git a/packages/node_modules/@node-red/nodes/locales/zh-CN/sequence/19-batch.html b/packages/node_modules/@node-red/nodes/locales/zh-CN/sequence/19-batch.html new file mode 100644 index 000000000..012f20816 --- /dev/null +++ b/packages/node_modules/@node-red/nodes/locales/zh-CN/sequence/19-batch.html @@ -0,0 +1,34 @@ + + + diff --git a/packages/node_modules/@node-red/nodes/locales/zh-CN/storage/10-file.html b/packages/node_modules/@node-red/nodes/locales/zh-CN/storage/10-file.html new file mode 100644 index 000000000..eb38a5235 --- /dev/null +++ b/packages/node_modules/@node-red/nodes/locales/zh-CN/storage/10-file.html @@ -0,0 +1,55 @@ + + + + + diff --git a/packages/node_modules/@node-red/nodes/locales/zh-CN/storage/23-watch.html b/packages/node_modules/@node-red/nodes/locales/zh-CN/storage/23-watch.html new file mode 100644 index 000000000..eec611429 --- /dev/null +++ b/packages/node_modules/@node-red/nodes/locales/zh-CN/storage/23-watch.html @@ -0,0 +1,25 @@ + + + diff --git a/packages/node_modules/@node-red/nodes/locales/zh-TW/common/20-inject.html b/packages/node_modules/@node-red/nodes/locales/zh-TW/common/20-inject.html new file mode 100644 index 000000000..046eddd7e --- /dev/null +++ b/packages/node_modules/@node-red/nodes/locales/zh-TW/common/20-inject.html @@ -0,0 +1,35 @@ + + + + \ No newline at end of file diff --git a/packages/node_modules/@node-red/nodes/locales/zh-TW/common/21-debug.html b/packages/node_modules/@node-red/nodes/locales/zh-TW/common/21-debug.html new file mode 100644 index 000000000..31f78e907 --- /dev/null +++ b/packages/node_modules/@node-red/nodes/locales/zh-TW/common/21-debug.html @@ -0,0 +1,25 @@ + + + diff --git a/packages/node_modules/@node-red/nodes/locales/zh-TW/common/24-complete.html b/packages/node_modules/@node-red/nodes/locales/zh-TW/common/24-complete.html new file mode 100644 index 000000000..862745310 --- /dev/null +++ b/packages/node_modules/@node-red/nodes/locales/zh-TW/common/24-complete.html @@ -0,0 +1,24 @@ + + + diff --git a/packages/node_modules/@node-red/nodes/locales/zh-TW/common/25-catch.html b/packages/node_modules/@node-red/nodes/locales/zh-TW/common/25-catch.html new file mode 100644 index 000000000..4e3db015d --- /dev/null +++ b/packages/node_modules/@node-red/nodes/locales/zh-TW/common/25-catch.html @@ -0,0 +1,36 @@ + + + diff --git a/packages/node_modules/@node-red/nodes/locales/zh-TW/common/25-status.html b/packages/node_modules/@node-red/nodes/locales/zh-TW/common/25-status.html new file mode 100644 index 000000000..d961c8e52 --- /dev/null +++ b/packages/node_modules/@node-red/nodes/locales/zh-TW/common/25-status.html @@ -0,0 +1,33 @@ + + + diff --git a/packages/node_modules/@node-red/nodes/locales/zh-TW/common/60-link.html b/packages/node_modules/@node-red/nodes/locales/zh-TW/common/60-link.html new file mode 100644 index 000000000..e7723c499 --- /dev/null +++ b/packages/node_modules/@node-red/nodes/locales/zh-TW/common/60-link.html @@ -0,0 +1,31 @@ + + + + + diff --git a/packages/node_modules/@node-red/nodes/locales/zh-TW/common/90-comment.html b/packages/node_modules/@node-red/nodes/locales/zh-TW/common/90-comment.html new file mode 100644 index 000000000..d044f28db --- /dev/null +++ b/packages/node_modules/@node-red/nodes/locales/zh-TW/common/90-comment.html @@ -0,0 +1,21 @@ + + + diff --git a/packages/node_modules/@node-red/nodes/locales/zh-TW/common/98-unknown.html b/packages/node_modules/@node-red/nodes/locales/zh-TW/common/98-unknown.html new file mode 100644 index 000000000..c3588def1 --- /dev/null +++ b/packages/node_modules/@node-red/nodes/locales/zh-TW/common/98-unknown.html @@ -0,0 +1,24 @@ + + + diff --git a/packages/node_modules/@node-red/nodes/locales/zh-TW/function/10-function.html b/packages/node_modules/@node-red/nodes/locales/zh-TW/function/10-function.html new file mode 100644 index 000000000..9f8ddb43f --- /dev/null +++ b/packages/node_modules/@node-red/nodes/locales/zh-TW/function/10-function.html @@ -0,0 +1,51 @@ + + + diff --git a/packages/node_modules/@node-red/nodes/locales/zh-TW/function/10-switch.html b/packages/node_modules/@node-red/nodes/locales/zh-TW/function/10-switch.html new file mode 100644 index 000000000..5a65eff93 --- /dev/null +++ b/packages/node_modules/@node-red/nodes/locales/zh-TW/function/10-switch.html @@ -0,0 +1,37 @@ + + + diff --git a/packages/node_modules/@node-red/nodes/locales/zh-TW/function/15-change.html b/packages/node_modules/@node-red/nodes/locales/zh-TW/function/15-change.html new file mode 100644 index 000000000..91a320945 --- /dev/null +++ b/packages/node_modules/@node-red/nodes/locales/zh-TW/function/15-change.html @@ -0,0 +1,33 @@ + + + diff --git a/packages/node_modules/@node-red/nodes/locales/zh-TW/function/16-range.html b/packages/node_modules/@node-red/nodes/locales/zh-TW/function/16-range.html new file mode 100644 index 000000000..62eb63d0b --- /dev/null +++ b/packages/node_modules/@node-red/nodes/locales/zh-TW/function/16-range.html @@ -0,0 +1,40 @@ + + + diff --git a/packages/node_modules/@node-red/nodes/locales/zh-TW/function/80-template.html b/packages/node_modules/@node-red/nodes/locales/zh-TW/function/80-template.html new file mode 100644 index 000000000..874ae3801 --- /dev/null +++ b/packages/node_modules/@node-red/nodes/locales/zh-TW/function/80-template.html @@ -0,0 +1,46 @@ + + + diff --git a/packages/node_modules/@node-red/nodes/locales/zh-TW/function/89-delay.html b/packages/node_modules/@node-red/nodes/locales/zh-TW/function/89-delay.html new file mode 100644 index 000000000..28c291de8 --- /dev/null +++ b/packages/node_modules/@node-red/nodes/locales/zh-TW/function/89-delay.html @@ -0,0 +1,32 @@ + + + diff --git a/packages/node_modules/@node-red/nodes/locales/zh-TW/function/89-trigger.html b/packages/node_modules/@node-red/nodes/locales/zh-TW/function/89-trigger.html new file mode 100644 index 000000000..6bbe72f4d --- /dev/null +++ b/packages/node_modules/@node-red/nodes/locales/zh-TW/function/89-trigger.html @@ -0,0 +1,33 @@ + + + diff --git a/packages/node_modules/@node-red/nodes/locales/zh-TW/function/90-exec.html b/packages/node_modules/@node-red/nodes/locales/zh-TW/function/90-exec.html new file mode 100644 index 000000000..27be34e00 --- /dev/null +++ b/packages/node_modules/@node-red/nodes/locales/zh-TW/function/90-exec.html @@ -0,0 +1,74 @@ + + + diff --git a/packages/node_modules/@node-red/nodes/locales/zh-TW/messages.json b/packages/node_modules/@node-red/nodes/locales/zh-TW/messages.json new file mode 100644 index 000000000..5368bb993 --- /dev/null +++ b/packages/node_modules/@node-red/nodes/locales/zh-TW/messages.json @@ -0,0 +1,940 @@ +{ + "common": { + "label": { + "payload": "內容", + "topic": "主題", + "name": "名稱", + "username": "使用者名稱", + "password": "密碼", + "property": "屬性", + "selectNodes": "選擇節點...", + "expand": "擴展" + }, + "status": { + "connected": "已連接", + "not-connected": "未連接", + "disconnected": "已斷開", + "connecting": "連接中", + "error": "錯誤", + "ok": "確定" + }, + "notification": { + "error": "錯誤: __message__", + "errors": { + "not-deployed": "節點未部署", + "no-response": "伺服器無反應", + "unexpected": "發生意外錯誤 (__status__) __message__" + } + }, + "errors": { + "nooverride": "警告: 資訊的屬性已經不可以改寫節點的屬性. 詳情參考 bit.ly/nr-override-msg-props" + } + }, + "inject": { + "inject": "注入", + "repeat": "重複 = __repeat__", + "crontab": "crontab = __crontab__", + "stopped": "停止", + "failed": "注入失敗: __error__", + "label": { + "repeat": "重複", + "flow": "流上下午", + "global": "全局上下文", + "str": "字符串", + "num": "數值", + "bool": "布爾值", + "json": "JSON對象", + "bin": "buffer", + "date": "時間戳", + "env": "環境變量", + "object": "對象", + "string": "字符串", + "boolean": "布爾值", + "number": "數值", + "Array": "數組", + "invalid": "無效的JSON對象" + }, + "timestamp": "時間戳記", + "none": "無", + "interval": "週期性執行", + "interval-time": "指定時間段並週期性執行", + "time": "指定時間", + "seconds": "秒", + "minutes": "分鐘", + "hours": "小時", + "between": "介於", + "previous": "之前數值", + "at": "在", + "and": "至", + "every": "每隔", + "days": [ + "星期一", + "星期二", + "星期三", + "星期四", + "星期五", + "星期六", + "星期天" + ], + "on": "在", + "onstart": "立刻執行於", + "onceDelay": "秒後, 此後", + "tip": "注意: \"指定時間段並週期性執行\" 和 \"指定時間\" 會使用cron系統.
        詳情查看信息頁.", + "success": "成功注入: __label__", + "errors": { + "failed": "注入失敗, 請查看日誌", + "toolong": "週期過長" + } + }, + "catch": { + "catch": "監測所有節點", + "catchNodes": "監測__number__個節點", + "catchUncaught": "捕獲:未捕獲", + "label": { + "source": "監測範圍", + "selectAll": "全選", + "uncaught": "忽略其他捕獲節點處理的錯誤" + }, + "scope": { + "all": "所有節點", + "selected": "指定節點" + } + }, + "status": { + "status": "報告所有節點狀態", + "statusNodes": "報告__number__個節點狀態", + "label": { + "source": "報告狀態範圍", + "sortByType": "按類型排序" + }, + "scope": { + "all": "所有節點", + "selected": "指定節點" + } + }, + "complete": { + "completeNodes": "完成: __number__個節點" + }, + "debug": { + "output": "輸出", + "none": "None", + "invalid-exp": "無效的JSONata表達式: __error__", + "msgprop": "資訊屬性", + "msgobj": "完整資訊", + "to": "目標", + "debtab": "除錯窗口", + "tabcon": "除錯窗口及Console", + "toSidebar": "除錯窗口", + "toConsole": "Console", + "toStatus": "節點狀態 (32位元字元)", + "severity": "級別", + "notification": { + "activated": "成功啟動: __label__", + "deactivated": "成功取消: __label__" + }, + "sidebar": { + "label": "除錯窗口", + "name": "名稱", + "filterAll": "所有節點", + "filterSelected": "已選節點", + "filterCurrent": "當前流程", + "debugNodes": "除錯節點", + "clearLog": "清空日誌", + "filterLog": "過濾日誌", + "openWindow": "在新視窗打開", + "copyPath": "復制路徑", + "copyPayload": "復制值", + "pinPath": "固定展開" + }, + "messageMenu": { + "collapseAll": "折疊所有路徑", + "clearPinned": "清空已固定路徑", + "filterNode": "過濾此節點", + "clearFilter": "清空過濾條件" + } + }, + "link": { + "linkIn": "輸入", + "linkOut": "輸出" + }, + "tls": { + "tls": "TLS設置", + "label": { + "use-local-files": "使用本地密鑰及證書檔", + "upload": "上傳", + "cert": "證書", + "key": "私密金鑰", + "passphrase": "密碼", + "ca": "CA證書", + "verify-server-cert": "驗證伺服器憑證", + "servername": "服務器名" + }, + "placeholder": { + "cert": "憑證路徑 (PEM 格式)", + "key": "私密金鑰路徑 (PEM 格式)", + "ca": "CA憑證路徑 (PEM 格式)", + "passphrase": "私密金鑰密碼 (可選)", + "servername": "用於SNI" + }, + "error": { + "missing-file": "未提供證書/金鑰檔案" + } + }, + "exec": { + "exec": "exec", + "spawn": "spawn", + "label": { + "command": "命令", + "append": "追加", + "timeout": "超時", + "timeoutplace": "可選填", + "return": "輸出", + "seconds": "秒", + "stdout": "標準輸出", + "stderr": "標準錯誤輸出", + "retcode": "返回碼" + }, + "placeholder": { + "extraparams": "額外的輸入參數" + }, + "opt": { + "exec": "當命令完成時 - exec模式", + "spawn": "當命令進行時 - spawn模式" + }, + "oldrc": "使用舊式輸出 (相容模式)" + }, + "function": { + "function": "函數", + "label": { + "function": "函數", + "outputs": "輸出" + }, + "error": { + "inputListener": "無法在函數中監聽對'注入'事件", + "non-message-returned": "函數節點嘗試返回類型為 __type__ 的資訊" + } + }, + "template": { + "template": "模板", + "label": { + "template": "模版", + "property": "屬性", + "format": "語法高亮", + "syntax": "格式", + "output": "輸出為", + "mustache": "Mustache 模版", + "plain": "純文字", + "json": "JSON", + "yaml": "YAML", + "none": "無" + }, + "templatevalue": "This is the payload: {{payload}} !" + }, + "delay": { + "action": "行為設置", + "for": "時長", + "delaymsg": "延遲每一條資訊", + "delayfixed": "固定延遲時間", + "delayvarmsg": "允許msg.delay複寫延遲時長", + "randomdelay": "隨機延遲", + "limitrate": "限制資訊頻率", + "limitall": "所有資訊", + "limittopic": "每一個msg.topic", + "fairqueue": "依次發送每一個topic", + "timedqueue": "發所有topic", + "milisecs": "毫秒", + "secs": "秒", + "sec": "秒", + "mins": "分", + "min": "分", + "hours": "小時", + "hour": "小時", + "days": "天", + "day": "天", + "between": "介於", + "and": "至", + "rate": "速度", + "msgper": "信息 每", + "dropmsg": "不傳輸中間資訊", + "label": { + "delay": "延遲", + "variable": "變數", + "limit": "限制", + "limitTopic": "限制主題", + "random": "隨機", + "units": { + "second": { + "plural": "秒", + "singular": "秒" + }, + "minute": { + "plural": "分鐘", + "singular": "分鐘" + }, + "hour": { + "plural": "小時", + "singular": "小時" + }, + "day": { + "plural": "天", + "singular": "天" + } + } + }, + "error": { + "buffer": "緩衝了超過 1000 條資訊", + "buffer1": "緩衝了超過 10000 條資訊" + } + }, + "trigger": { + "send": "發送", + "then": "然後", + "then-send": "然後發送", + "output": { + "string": "字串", + "number": "數字", + "existing": "現有資訊物件", + "original": "原本資訊物件", + "latest": "最新資訊物件", + "nothing": "無" + }, + "wait-reset": "等待被重置", + "wait-for": "等待", + "wait-loop": "週期性重發", + "for": "處理", + "bytopics": "每個msg.topic", + "alltopics": "所有消息", + "duration": { + "ms": "毫秒", + "s": "秒", + "m": "分鐘", + "h": "小時" + }, + "extend": " 如有新資訊,延長延遲", + "label": { + "trigger": "觸發", + "trigger-block": "觸發並阻止", + "trigger-loop": "週期性重發", + "reset": "重置觸發節點條件 如果:", + "resetMessage": "msg.reset已設置", + "resetPayload": "msg.payload等於", + "resetprompt": "可選填" + } + }, + "comment": { + "comment": "注釋" + }, + "unknown": { + "label": { + "unknown": "未知" + }, + "tip": "

        此節點是您安裝,但Node-RED所不知道的類型。

        如果在此狀態下部署節點,那麼它的配置將被保留,但是流程將不會啟動,直到安裝缺少的節點。

        有關更多説明,請參閱資訊側欄

        " + }, + "mqtt": { + "label": { + "broker": "服務端", + "example": "e.g. localhost", + "output": "輸出", + "qos": "QoS", + "retain": "保持", + "clientid": "使用者端ID", + "port": "埠", + "keepalive": "Keepalive計時(秒)", + "cleansession": "使用新的會話", + "use-tls": "使用安全連接 (SSL/TLS)", + "tls-config": "TLS 設置", + "verify-server-cert": "驗證伺服器憑證", + "compatmode": "使用舊式MQTT 3.1支援" + }, + "sections-label": { + "birth-message": "連接時發送的消息(出生消息)", + "will-message": "意外斷開連接時的發送消息(Will消息)", + "close-message": "斷開連接前發送的消息(關閉消息)" + }, + "tabs-label": { + "connection": "連接", + "security": "安全", + "messages": "消息" + }, + "placeholder": { + "clientid": "留白則自動隨機生成", + "clientid-nonclean": "如非新會話,必須設置使用者端ID", + "will-topic": "留白將禁止Will資訊", + "birth-topic": "留白將禁止Birth資訊", + "close-topic": "留白以禁用關閉消息" + }, + "state": { + "connected": "已連接到服務端: __broker__", + "disconnected": "已斷開與服務端 __broker__ 的連結", + "connect-failed": "與服務端 __broker__ 的連接失敗" + }, + "retain": "保留", + "output": { + "buffer": "Buffer", + "string": "字串", + "base64": "Base64編碼字串", + "auto": "自動檢測 (字符串或buffer)", + "json": "解析的JSON對象" + }, + "true": "是", + "false": "否", + "tip": "提示: 若希望通過msg屬性對topic(資訊), qos及retain(保留)進行設置, 則將上述項留白", + "errors": { + "not-defined": "主題未設置", + "missing-config": "未設置服務端", + "invalid-topic": "主題無效", + "nonclean-missingclientid": "使用者端ID未設定,使用新會話", + "invalid-json-string": "無效的JSON字符串", + "invalid-json-parse": "無法解析JSON字符串" + } + }, + "httpin": { + "label": { + "method": "請求方式", + "url": "URL", + "doc": "文字檔", + "return": "返回", + "upload": "接受檔案上傳?", + "status": "狀態碼", + "headers": "Header", + "other": "其他", + "paytoqs": "將msg.payload附加為查詢字符串參數", + "utf8String": "UTF8格式的字符串", + "binaryBuffer": "二進制buffer", + "jsonObject": "解析的JSON對象", + "authType": "類型", + "bearerToken": "Token" + }, + "setby": "- 用 msg.method 設定 -", + "basicauth": "基本認證", + "use-tls": "使用安全連接 (SSL/TLS) ", + "tls-config": "TLS 設置", + "basic": "基本認證", + "digest": "摘要認證", + "bearer": "bearer認證", + "use-proxy": "使用代理服務器", + "persist": "對連接啟用keep-alive", + "proxy-config": "代理服務器設置", + "use-proxyauth": "使用代理身份驗證", + "noproxy-hosts": "代理例外", + "utf8": "UTF-8 字串", + "binary": "二進位資料", + "json": "JSON對象", + "tip": { + "in": "相對URL", + "res": "發送到此節點的消息必須來自http input節點", + "req": "提示:如果JSON解析失敗,則獲取的字串將按原樣返回." + }, + "httpreq": "http 請求", + "errors": { + "not-created": "當httpNodeRoot為否時,無法創建http-in節點", + "missing-path": "無路徑", + "no-response": "無響應物件", + "json-error": "JSON 解析錯誤", + "no-url": "未設定 URL", + "deprecated-call": "__method__方法已棄用", + "invalid-transport": "非HTTP傳輸請求", + "timeout-isnan": "超時值不是有效數字,忽略", + "timeout-isnegative": "超時值為負,忽略", + "invalid-payload": "無效的有效載荷" + }, + "status": { + "requesting": "請求中" + } + }, + "websocket": { + "label": { + "type": "類型", + "path": "路徑", + "url": "URL" + }, + "listenon": "監聽", + "connectto": "連接", + "sendrec": "發送/接受", + "payload": "有效載荷", + "message": "完整資訊", + "tip": { + "path1": "預設情況下,payload將包含要發送或從Websocket接收的資料。偵聽器可以配置為以JSON格式的字串發送或接收整個消息物件.", + "path2": "這條路徑將相對於 __path__.", + "url1": "URL 應該使用ws://或者wss://方案並指向現有的websocket監聽器.", + "url2": "預設情況下,payload 將包含要發送或從Websocket接收的資料。可以將使用者端配置為以JSON格式的字串發送或接收整個消息物件." + }, + "status": { + "connected": "連接數 __count__", + "connected_plural": "連接數 __count__" + }, + "errors": { + "connect-error": "ws連接發生了錯誤: ", + "send-error": "發送時發生了錯誤: ", + "missing-conf": "未設置伺服器", + "duplicate-path": "同一路徑上不能有兩個WebSocket偵聽器: __path__" + } + }, + "watch": { + "watch": "watch", + "label": { + "files": "文件", + "recursive": "遞迴所有子資料夾" + }, + "placeholder": { + "files": "逗號分開文件或資料夾" + }, + "tip": "在Windows上,請務必使用雙斜杠 \\\\ 來隔開資料夾名字" + }, + "tcpin": { + "label": { + "type": "類型", + "output": "輸出", + "port": "埠", + "host": "主機位址", + "payload": "的有效載荷", + "delimited": "分隔符號號", + "close-connection": "是否在成功發送每條資訊後斷開連接?", + "decode-base64": "用 Base64 解碼信息?", + "server": "伺服器", + "return": "返回", + "ms": "毫秒", + "chars": "字元" + }, + "type": { + "listen": "監聽", + "connect": "連接", + "reply": "回應 TCP" + }, + "output": { + "stream": "字串流", + "single": "單一", + "buffer": "Buffer", + "string": "字串", + "base64": "Base64 字串" + }, + "return": { + "timeout": "指定時間後", + "character": "當收到某個字元為", + "number": "指定字元數", + "never": "永不 - 保持連接", + "immed": "馬上 - 不需要等待回復" + }, + "status": { + "connecting": "正在連接到 __host__:__port__", + "connected": "已經連接到 __host__:__port__", + "listening-port": "監聽埠 __port__", + "stopped-listening": "已停止監聽埠", + "connection-from": "連接來自 __host__:__port__", + "connection-closed": "連接已關閉 __host__:__port__", + "connections": "__count__ 個連接", + "connections_plural": "__count__ 個連接" + }, + "errors": { + "connection-lost": "連接中斷 __host__:__port__", + "timeout": "超時關閉通訊端連接,埠 __port__", + "cannot-listen": "無法監聽埠 __port__, 錯誤: __error__", + "error": "錯誤: __error__", + "socket-error": "通訊端連接錯誤來自 __host__:__port__", + "no-host": "主機位址或埠未設定", + "connect-timeout": "連接逾時", + "connect-fail": "連接失敗" + } + }, + "udp": { + "label": { + "listen": "監聽", + "onport": "埠", + "using": "使用", + "output": "輸出", + "group": "組", + "interface": "本地IP", + "send": "發送一個", + "toport": "到埠", + "address": "地址", + "decode-base64": "是否解碼Base64編碼的資訊?", + "interfaceprompt": "(可選)本地 IP 綁定到" + }, + "placeholder": { + "interface": "(可選)eth0的IP地址", + "interfaceprompt": "(可選) 要綁定的本地接口或地址", + "address": "目標IP位址" + }, + "udpmsgs": "udp信息", + "mcmsgs": "群播信息", + "udpmsg": "udp信息", + "bcmsg": "廣播資訊", + "mcmsg": "群播信息", + "output": { + "buffer": "Buffer", + "string": "字串", + "base64": "Base64編碼字串" + }, + "bind": { + "random": "綁定到任意本地埠", + "local": "綁定到本地埠", + "target": "綁定到目標埠" + }, + "tip": { + "in": "提示:確保您的防火牆將允許資料進入", + "out": "提示:如果要使用msg.ipmsg.port設置,請將位址和埠留空", + "port": "正在使用埠: " + }, + "status": { + "listener-at": "udp 監聽器正在監聽 __host__:__port__", + "mc-group": "udp 群播到 __group__", + "listener-stopped": "udp 監聽器已停止", + "output-stopped": "udp 輸出已停止", + "mc-ready": "udp 群播已準備好: __outport__ -> __host__:__port__", + "bc-ready": "udp 廣播已準備好: __outport__ -> __host__:__port__", + "ready": "udp 已準備好: __outport__ -> __host__:__port__", + "ready-nolocal": "udp 已準備好: __host__:__port__", + "re-use": "udp 重用通訊端: __outport__ -> __host__:__port__" + }, + "errors": { + "access-error": "UDP 訪問錯誤, 你可能需要root許可權才能接入1024以下的埠", + "error": "錯誤: __error__", + "bad-mcaddress": "無效的群播地址", + "interface": "必須是指定介面的IP位址", + "ip-notset": "udp: IP地址未設定", + "port-notset": "udp: 埠未設定", + "port-invalid": "udp: 無效埠號碼", + "alreadyused": "udp: 埠已被佔用", + "ifnotfound": "udp: 接口 __iface__ 未發現" + } + }, + "switch": { + "switch": "switch", + "label": { + "property": "屬性", + "rule": "規則", + "repair": "重建資訊佇列" + }, + "previous": "先前值", + "and": "與", + "checkall": "全選所有規則", + "stopfirst": "接受第一條匹配資訊後停止", + "ignorecase": "忽略大小寫", + "rules": { + "btwn": "在之間", + "cont": "包含", + "regex": "匹配規則運算式", + "true": "為真", + "false": "為假", + "null": "為空", + "nnull": "非空", + "istype": "類型是", + "empty": "為空", + "nempty": "非空", + "head": "head", + "tail": "tail", + "index": "index between", + "exp": "JSONata運算式", + "else": "除此以外", + "hask": "擁有鍵" + }, + "errors": { + "invalid-expr": "無效的JSONata運算式: __error__", + "too-many": "Switch節點中有太多待定信息" + } + }, + "change": { + "label": { + "rules": "規則", + "rule": "規則", + "set": "設定 __property__", + "change": "修改 __property__", + "delete": "刪除 __property__", + "move": "移動 __property__", + "changeCount": "修改: __count__條規矩", + "regex": "使用規則運算式" + }, + "action": { + "set": "設定", + "change": "修改", + "delete": "刪除", + "move": "轉移", + "to": "到", + "search": "搜索", + "replace": "替代為" + }, + "errors": { + "invalid-from": "無效的'from'屬性: __error__", + "invalid-json": "無效的'to'JSON 屬性", + "invalid-expr": "無效的JSONata運算式: __error__" + } + }, + "range": { + "range": "range", + "label": { + "action": "操作", + "inputrange": "映射輸入資料", + "resultrange": "至目標範圍", + "from": "從", + "to": "到", + "roundresult": "取最接近整數?" + }, + "placeholder": { + "min": "e.g. 0", + "maxin": "e.g. 99", + "maxout": "e.g. 255" + }, + "scale": { + "payload": "按比例msg.payload", + "limit": "按比例並設定界限至目標範圍", + "wrap": "按比例並包含在目標範圍內" + }, + "tip": "提示: 此節點僅對數字有效", + "errors": { + "notnumber": "不是一個數字" + } + }, + "csv": { + "label": { + "columns": "列", + "separator": "分隔符號", + "c2o": "CSV至對象", + "o2c": "對象至CSV", + "input": "輸入", + "skip-s": "忽略前", + "skip-e": "行", + "firstrow": "第一行包含列名", + "output": "輸出", + "includerow": "包含列名行", + "newline": "分行符號", + "usestrings": "解析數值" + }, + "placeholder": { + "columns": "用逗號分割列名" + }, + "separator": { + "comma": "逗號", + "tab": "Tab", + "space": "空格", + "semicolon": "分號", + "colon": "冒號", + "hashtag": "井號", + "other": "其他..." + }, + "output": { + "row": "每行一條信息", + "array": "僅一條資訊 [陣列]" + }, + "newline": { + "linux": "Linux (\\n)", + "mac": "Mac (\\r)", + "windows": "Windows (\\r\\n)" + }, + "errors": { + "csv_js": "此節點僅處理CSV字串或JS物件", + "obj_csv": "對象->CSV轉換未設定列模版" + } + }, + "html": { + "label": { + "select": "選取項", + "output": "輸出", + "in": "in" + }, + "output": { + "html": "選定元素的html內容", + "text": "選定元素的純文字內容", + "attr": "包含選定元素的所有屬性的物件" + }, + "format": { + "single": "一條資訊 [陣列]", + "multi": "多條資訊,每條一個元素" + } + }, + "json": { + "errors": { + "dropped-object": "忽略非物件格式的有效負載", + "dropped": "忽略不支援格式的有效負載類型", + "dropped-error": "轉換有效負載失敗", + "schema-error": "JSON架構錯誤", + "schema-error-compile": "JSON架構錯誤: 未能編譯架構" + }, + "label": { + "o2j": "對象至JSON", + "pretty": "格式化JSON字串", + "action": "操作", + "property": "屬性", + "actions": { + "toggle": "JSON字串與物件互轉", + "str": "總是轉為JSON字串", + "obj": "總是轉為JS對象" + } + } + }, + "yaml": { + "errors": { + "dropped-object": "忽略非物件格式的有效負載", + "dropped": "忽略不支援格式的有效負載類型", + "dropped-error": "轉換有效負載失敗" + } + }, + "xml": { + "label": { + "represent": "XML標籤屬性的屬性名稱", + "prefix": "標籤文本內容的屬性名稱", + "advanced": "高級選項", + "x2o": "XML到物件選項" + }, + "errors": { + "xml_js": "此節點僅處理XML字串或JS物件." + } + }, + "file": { + "label": { + "filename": "檔案名", + "action": "行為", + "addnewline": "向每個有效載荷添加分行符號(\\n)?", + "createdir": "創建目錄(如果不存在)?", + "outputas": "輸出", + "breakchunks": "分拆成塊", + "breaklines": "分拆成行", + "filelabel": "文件", + "sendError": "發生錯誤時發送消息(傳統模式)", + "deletelabel": "刪除 __file__", + "encoding": "編碼", + "utf8String": "UTF8字符串", + "binaryBuffer": "二進制buffer" + }, + "action": { + "append": "追加至文件", + "overwrite": "複寫文件", + "delete": "刪除檔" + }, + "output": { + "utf8": "一個utf8字串", + "buffer": "一個Buffer物件", + "lines": "每行一條信息", + "stream": "一個Buffer流" + }, + "status": { + "wrotefile": "寫入至文件: __file__", + "deletedfile": "刪除檔: __file__", + "appendedfile": "追加至文件: __file__" + }, + "encoding": { + "none": "默認", + "native": "Native", + "unicode": "Unicode", + "japanese": "日本", + "chinese": "中國", + "korean": "韓國", + "taiwan": "臺灣/香港", + "windows": "Windows代碼頁", + "iso": "ISO代碼頁", + "ibm": "IBM代碼頁", + "mac": "Mac代碼頁", + "koi8": "KOI8代碼頁", + "misc": "其它" + }, + "errors": { + "nofilename": "未指定檔案名", + "invaliddelete": "警告:無效刪除。請在配置對話方塊中使用特定的刪除選項", + "deletefail": "無法刪除檔: __error__", + "writefail": "無法寫入文件: __error__", + "appendfail": "無法追加到文件: __error__", + "createfail": "檔創建失敗: __error__" + }, + "tip": "提示: 檔案名應該是絕對路徑,否則它將相對於Node-RED進程的工作目錄。" + }, + "split": { + "split": "split", + "intro": "基於以下類型拆分msg.payload:", + "object": "對象", + "objectSend": "每個鍵值對作為單個消息發送", + "strBuff": "字串 / Buffer", + "array": "陣列", + "splitUsing": "拆分使用", + "splitLength": "固定長度", + "stream": "作為消息流處理", + "addname": " 複製鍵到 " + }, + "join": { + "join": "join", + "mode": { + "mode": "模式", + "auto": "自動", + "merge": "合併序列", + "reduce": "縮減序列", + "custom": "手動" + }, + "combine": "合併每個", + "completeMessage": "完整的消息", + "create": "輸出為", + "type": { + "string": "字串", + "array": "陣列", + "buffer": "Buffer", + "object": "鍵值對對象", + "merged": "合併對象" + }, + "using": "使用此值", + "key": "作為鍵", + "joinedUsing": "合併符號", + "send": "發送資訊:", + "afterCount": "達到一定數量的資訊時", + "count": "數量", + "subsequent": "和每個後續的消息", + "afterTimeout": "第一條消息的若幹時間後", + "seconds": "秒", + "complete": "在收到存在msg.complete的消息後", + "tip": "此模式假定此節點與split相連, 或者接收到的消息有正確配置的msg.parts屬性.", + "too-many": "join節點中有太多待定信息", + "merge": { + "topics-label": "合併主題", + "topics": "主題", + "topic": "主題", + "on-change": "當收到一個新主題時發送已合併資訊" + }, + "reduce": { + "exp": "Reduce運算式", + "exp-value": "exp", + "init": "初始值", + "right": "反向求值(從後往前)", + "fixup": "Fix-up exp" + }, + "errors": { + "invalid-expr": "無效的JSONata運算式: __error__" + } + }, + "sort": { + "sort": "排序", + "target": "排序屬性", + "seq": "資訊佇列", + "key": "鍵值", + "elem": "元素值", + "order": "順序", + "ascending": "昇冪", + "descending": "降冪", + "as-number": "作為數值", + "invalid-exp": "排序節點中存在無效的JSONata運算式", + "too-many": "排序節點中有太多待定信息", + "clear": "清空排序節點中的待定資訊" + }, + "batch": { + "batch": "batch", + "mode": { + "label": "模式", + "num-msgs": "按指定數量分組", + "interval": "按時間間隔分組", + "concat": "按主題分組" + }, + "count": { + "label": "分組數量", + "overlap": "隊末隊首重疊數量", + "count": "數量", + "invalid": "無效的分組數量或重疊數量" + }, + "interval": { + "label": "時間間隔", + "seconds": "秒", + "empty": "無數據到達時發送空資訊" + }, + "concat": { + "topics-label": "主題", + "topic": "主題" + }, + "too-many": "batch節點中有太多待定信息", + "unexpected": "未知模式", + "no-parts": "資訊中沒有parts屬性" + } +} diff --git a/packages/node_modules/@node-red/nodes/locales/zh-TW/network/05-tls.html b/packages/node_modules/@node-red/nodes/locales/zh-TW/network/05-tls.html new file mode 100644 index 000000000..ee4b3bd95 --- /dev/null +++ b/packages/node_modules/@node-red/nodes/locales/zh-TW/network/05-tls.html @@ -0,0 +1,19 @@ + + + diff --git a/packages/node_modules/@node-red/nodes/locales/zh-TW/network/06-httpproxy.html b/packages/node_modules/@node-red/nodes/locales/zh-TW/network/06-httpproxy.html new file mode 100644 index 000000000..23f899627 --- /dev/null +++ b/packages/node_modules/@node-red/nodes/locales/zh-TW/network/06-httpproxy.html @@ -0,0 +1,22 @@ + + + diff --git a/packages/node_modules/@node-red/nodes/locales/zh-TW/network/10-mqtt.html b/packages/node_modules/@node-red/nodes/locales/zh-TW/network/10-mqtt.html new file mode 100644 index 000000000..825bf218e --- /dev/null +++ b/packages/node_modules/@node-red/nodes/locales/zh-TW/network/10-mqtt.html @@ -0,0 +1,70 @@ + + + + + + + diff --git a/packages/node_modules/@node-red/nodes/locales/zh-TW/network/21-httpin.html b/packages/node_modules/@node-red/nodes/locales/zh-TW/network/21-httpin.html new file mode 100644 index 000000000..0d44ce3b1 --- /dev/null +++ b/packages/node_modules/@node-red/nodes/locales/zh-TW/network/21-httpin.html @@ -0,0 +1,81 @@ + + + + + diff --git a/packages/node_modules/@node-red/nodes/locales/zh-TW/network/21-httprequest.html b/packages/node_modules/@node-red/nodes/locales/zh-TW/network/21-httprequest.html new file mode 100644 index 000000000..71ed96087 --- /dev/null +++ b/packages/node_modules/@node-red/nodes/locales/zh-TW/network/21-httprequest.html @@ -0,0 +1,78 @@ + + + diff --git a/packages/node_modules/@node-red/nodes/locales/zh-TW/network/22-websocket.html b/packages/node_modules/@node-red/nodes/locales/zh-TW/network/22-websocket.html new file mode 100644 index 000000000..4bb2c7f4d --- /dev/null +++ b/packages/node_modules/@node-red/nodes/locales/zh-TW/network/22-websocket.html @@ -0,0 +1,35 @@ + + + + + + + + + diff --git a/packages/node_modules/@node-red/nodes/locales/zh-TW/network/31-tcpin.html b/packages/node_modules/@node-red/nodes/locales/zh-TW/network/31-tcpin.html new file mode 100644 index 000000000..2898ca718 --- /dev/null +++ b/packages/node_modules/@node-red/nodes/locales/zh-TW/network/31-tcpin.html @@ -0,0 +1,35 @@ + + + + + + + diff --git a/packages/node_modules/@node-red/nodes/locales/zh-TW/network/32-udp.html b/packages/node_modules/@node-red/nodes/locales/zh-TW/network/32-udp.html new file mode 100644 index 000000000..401af48e3 --- /dev/null +++ b/packages/node_modules/@node-red/nodes/locales/zh-TW/network/32-udp.html @@ -0,0 +1,28 @@ + + + + + diff --git a/packages/node_modules/@node-red/nodes/locales/zh-TW/parsers/70-CSV.html b/packages/node_modules/@node-red/nodes/locales/zh-TW/parsers/70-CSV.html new file mode 100644 index 000000000..9a8638614 --- /dev/null +++ b/packages/node_modules/@node-red/nodes/locales/zh-TW/parsers/70-CSV.html @@ -0,0 +1,43 @@ + + + diff --git a/packages/node_modules/@node-red/nodes/locales/zh-TW/parsers/70-HTML.html b/packages/node_modules/@node-red/nodes/locales/zh-TW/parsers/70-HTML.html new file mode 100644 index 000000000..b1559455f --- /dev/null +++ b/packages/node_modules/@node-red/nodes/locales/zh-TW/parsers/70-HTML.html @@ -0,0 +1,33 @@ + + + diff --git a/packages/node_modules/@node-red/nodes/locales/zh-TW/parsers/70-JSON.html b/packages/node_modules/@node-red/nodes/locales/zh-TW/parsers/70-JSON.html new file mode 100644 index 000000000..1a46c3690 --- /dev/null +++ b/packages/node_modules/@node-red/nodes/locales/zh-TW/parsers/70-JSON.html @@ -0,0 +1,43 @@ + + + diff --git a/packages/node_modules/@node-red/nodes/locales/zh-TW/parsers/70-XML.html b/packages/node_modules/@node-red/nodes/locales/zh-TW/parsers/70-XML.html new file mode 100644 index 000000000..e4e433fdb --- /dev/null +++ b/packages/node_modules/@node-red/nodes/locales/zh-TW/parsers/70-XML.html @@ -0,0 +1,48 @@ + + + diff --git a/packages/node_modules/@node-red/nodes/locales/zh-TW/parsers/70-YAML.html b/packages/node_modules/@node-red/nodes/locales/zh-TW/parsers/70-YAML.html new file mode 100644 index 000000000..8ea4f87f8 --- /dev/null +++ b/packages/node_modules/@node-red/nodes/locales/zh-TW/parsers/70-YAML.html @@ -0,0 +1,34 @@ + + + diff --git a/packages/node_modules/@node-red/nodes/locales/zh-TW/sequence/17-split.html b/packages/node_modules/@node-red/nodes/locales/zh-TW/sequence/17-split.html new file mode 100644 index 000000000..fd97b497a --- /dev/null +++ b/packages/node_modules/@node-red/nodes/locales/zh-TW/sequence/17-split.html @@ -0,0 +1,133 @@ + + + + + diff --git a/packages/node_modules/@node-red/nodes/locales/zh-TW/sequence/18-sort.html b/packages/node_modules/@node-red/nodes/locales/zh-TW/sequence/18-sort.html new file mode 100644 index 000000000..cb8e7fa21 --- /dev/null +++ b/packages/node_modules/@node-red/nodes/locales/zh-TW/sequence/18-sort.html @@ -0,0 +1,41 @@ + + + diff --git a/packages/node_modules/@node-red/nodes/locales/zh-TW/sequence/19-batch.html b/packages/node_modules/@node-red/nodes/locales/zh-TW/sequence/19-batch.html new file mode 100644 index 000000000..79879076e --- /dev/null +++ b/packages/node_modules/@node-red/nodes/locales/zh-TW/sequence/19-batch.html @@ -0,0 +1,34 @@ + + + diff --git a/packages/node_modules/@node-red/nodes/locales/zh-TW/storage/10-file.html b/packages/node_modules/@node-red/nodes/locales/zh-TW/storage/10-file.html new file mode 100644 index 000000000..ce2531137 --- /dev/null +++ b/packages/node_modules/@node-red/nodes/locales/zh-TW/storage/10-file.html @@ -0,0 +1,55 @@ + + + + + diff --git a/packages/node_modules/@node-red/nodes/locales/zh-TW/storage/23-watch.html b/packages/node_modules/@node-red/nodes/locales/zh-TW/storage/23-watch.html new file mode 100644 index 000000000..d8e1b5807 --- /dev/null +++ b/packages/node_modules/@node-red/nodes/locales/zh-TW/storage/23-watch.html @@ -0,0 +1,25 @@ + + + diff --git a/packages/node_modules/@node-red/nodes/package.json b/packages/node_modules/@node-red/nodes/package.json index d2b3060ee..96e829eaf 100644 --- a/packages/node_modules/@node-red/nodes/package.json +++ b/packages/node_modules/@node-red/nodes/package.json @@ -1,6 +1,6 @@ { "name": "@node-red/nodes", - "version": "1.0.3", + "version": "1.1.0", "license": "Apache-2.0", "repository": { "type": "git", @@ -15,11 +15,11 @@ } ], "dependencies": { - "ajv": "6.10.2", + "ajv": "6.12.0", "body-parser": "1.19.0", "cheerio": "0.22.0", "content-type": "1.0.4", - "cookie-parser": "1.4.4", + "cookie-parser": "1.4.5", "cookie": "0.4.0", "cors": "2.8.5", "cron": "1.7.2", @@ -27,18 +27,18 @@ "fs-extra": "8.1.0", "fs.notify": "0.0.4", "hash-sum": "2.0.0", - "https-proxy-agent": "2.2.4", + "https-proxy-agent": "5.0.0", "is-utf8": "0.2.1", "js-yaml": "3.13.1", "media-typer": "1.1.0", "mqtt": "2.18.8", "multer": "1.4.2", - "mustache": "3.0.2", + "mustache": "4.0.1", "on-headers": "1.0.2", "raw-body": "2.4.1", "request": "2.88.0", "ws": "6.2.1", - "xml2js": "0.4.22", - "iconv-lite": "0.5.0" + "xml2js": "0.4.23", + "iconv-lite": "0.5.1" } } diff --git a/packages/node_modules/@node-red/registry/lib/installer.js b/packages/node_modules/@node-red/registry/lib/installer.js index 3562dfb47..dac100613 100644 --- a/packages/node_modules/@node-red/registry/lib/installer.js +++ b/packages/node_modules/@node-red/registry/lib/installer.js @@ -32,6 +32,7 @@ var paletteEditorEnabled = false; var settings; var moduleRe = /^(@[^/]+?[/])?[^/]+?$/; var slashRe = process.platform === "win32" ? /\\|[/]/ : /[/]/; +var pkgurlRe = /^(https?|git(|\+https?|\+ssh|\+file)):\/\//; function init(runtime) { events = runtime.events; @@ -76,14 +77,17 @@ function checkExistingModule(module,version) { } return false; } -function installModule(module,version) { +function installModule(module,version,url) { activePromise = activePromise.then(() => { //TODO: ensure module is 'safe' return new Promise((resolve,reject) => { var installName = module; var isUpgrade = false; try { - if (moduleRe.test(module)) { + if (url && pkgurlRe.test(url)) { + // Git remote url or Tarball url - check the valid package url + installName = url; + } else if (moduleRe.test(module)) { // Simple module name - assume it can be npm installed if (version) { installName += "@"+version; @@ -104,7 +108,7 @@ function installModule(module,version) { } var installDir = settings.userDir || process.env.NODE_RED_HOME || "."; - var args = ['install','--no-audit','--no-update-notifier','--save','--save-prefix="~"','--production',installName]; + var args = ['install','--no-audit','--no-update-notifier','--no-fund','--save','--save-prefix="~"','--production',installName]; log.trace(npmCommand + JSON.stringify(args)); exec.run(npmCommand,args,{ cwd: installDir @@ -197,7 +201,7 @@ function uninstallModule(module) { var list = registry.removeModule(module); log.info(log._("server.install.uninstalling",{name:module})); - var args = ['remove','--no-audit','--no-update-notifier','--save',module]; + var args = ['remove','--no-audit','--no-update-notifier','--no-fund','--save',module]; log.trace(npmCommand + JSON.stringify(args)); exec.run(npmCommand,args,{ diff --git a/packages/node_modules/@node-red/registry/lib/localfilesystem.js b/packages/node_modules/@node-red/registry/lib/localfilesystem.js index 947972031..c66d7d080 100644 --- a/packages/node_modules/@node-red/registry/lib/localfilesystem.js +++ b/packages/node_modules/@node-red/registry/lib/localfilesystem.js @@ -286,6 +286,10 @@ function getNodeFiles(disableNodePathScan) { nodeFiles.forEach(function(node) { nodeList["node-red"].nodes[node.name] = node; }); + if (settings.coreNodesDir) { + var examplesDir = path.join(settings.coreNodesDir,"examples"); + nodeList["node-red"].examples = {path: examplesDir}; + } if (!disableNodePathScan) { var moduleFiles = scanTreeForNodesModules(); diff --git a/packages/node_modules/@node-red/registry/package.json b/packages/node_modules/@node-red/registry/package.json index fefb24360..1ac6ce528 100644 --- a/packages/node_modules/@node-red/registry/package.json +++ b/packages/node_modules/@node-red/registry/package.json @@ -1,6 +1,6 @@ { "name": "@node-red/registry", - "version": "1.0.3", + "version": "1.1.0", "license": "Apache-2.0", "main": "./lib/index.js", "repository": { @@ -16,9 +16,9 @@ } ], "dependencies": { - "@node-red/util": "1.0.3", + "@node-red/util": "1.1.0", "semver": "6.3.0", - "uglify-js": "3.6.9", + "uglify-js": "3.8.1", "when": "3.7.8" } } diff --git a/packages/node_modules/@node-red/runtime/lib/api/flows.js b/packages/node_modules/@node-red/runtime/lib/api/flows.js index 1e6e72dc7..277099ecc 100644 --- a/packages/node_modules/@node-red/runtime/lib/api/flows.js +++ b/packages/node_modules/@node-red/runtime/lib/api/flows.js @@ -57,6 +57,8 @@ var api = module.exports = { * Sets the current flow configuration * @param {Object} opts * @param {User} opts.user - the user calling the api + * @param {Object} opts.flows - the flow configuration: `{flows: [..], credentials: {}}` + * @param {Object} opts.deploymentType - the type of deployment - "full", "nodes", "flows", "reload" * @param {Object} opts.req - the request to log (optional) * @return {Promise} - the active flow configuration * @memberof @node-red/runtime_flows @@ -83,7 +85,7 @@ var api = module.exports = { return reject(err); } } - apiPromise = runtime.nodes.setFlows(flows.flows,deploymentType); + apiPromise = runtime.nodes.setFlows(flows.flows,flows.credentials,deploymentType); } apiPromise.then(function(flowId) { return resolve({rev:flowId}); @@ -238,17 +240,25 @@ var api = module.exports = { if (!credentials) { return resolve({}); } - var definition = runtime.nodes.getCredentialDefinition(opts.type) || {}; - var sendCredentials = {}; - for (var cred in definition) { - if (definition.hasOwnProperty(cred)) { - if (definition[cred].type == "password") { - var key = 'has_' + cred; - sendCredentials[key] = credentials[cred] != null && credentials[cred] !== ''; - continue; + var cred; + if (/^subflow(:|$)/.test(opts.type)) { + for (cred in credentials) { + if (credentials.hasOwnProperty(cred)) { + sendCredentials['has_'+cred] = credentials[cred] != null && credentials[cred] !== ''; + } + } + } else { + var definition = runtime.nodes.getCredentialDefinition(opts.type) || {}; + for (cred in definition) { + if (definition.hasOwnProperty(cred)) { + if (definition[cred].type == "password") { + var key = 'has_' + cred; + sendCredentials[key] = credentials[cred] != null && credentials[cred] !== ''; + continue; + } + sendCredentials[cred] = credentials[cred] || ''; } - sendCredentials[cred] = credentials[cred] || ''; } } resolve(sendCredentials); diff --git a/packages/node_modules/@node-red/runtime/lib/api/nodes.js b/packages/node_modules/@node-red/runtime/lib/api/nodes.js index ee4d3bc1a..fb16cc631 100644 --- a/packages/node_modules/@node-red/runtime/lib/api/nodes.js +++ b/packages/node_modules/@node-red/runtime/lib/api/nodes.js @@ -159,6 +159,7 @@ var api = module.exports = { * @param {User} opts.user - the user calling the api * @param {String} opts.module - the id of the module to install * @param {String} opts.version - (optional) the version of the module to install + * @param {String} opts.url - (optional) url to install * @param {Object} opts.req - the request to log (optional) * @return {Promise} - the node module info * @memberof @node-red/runtime_nodes @@ -183,20 +184,20 @@ var api = module.exports = { return reject(err); } } - runtime.nodes.installModule(opts.module,opts.version).then(function(info) { - runtime.log.audit({event: "nodes.install",module:opts.module,version:opts.version}, opts.req); + runtime.nodes.installModule(opts.module,opts.version,opts.url).then(function(info) { + runtime.log.audit({event: "nodes.install",module:opts.module,version:opts.version,url:opts.url}, opts.req); return resolve(info); }).catch(function(err) { if (err.code === 404) { - runtime.log.audit({event: "nodes.install",module:opts.module,version:opts.version,error:"not_found"}, opts.req); + runtime.log.audit({event: "nodes.install",module:opts.module,version:opts.version,url:opts.url,error:"not_found"}, opts.req); // TODO: code/status err.status = 404; } else if (err.code) { err.status = 400; - runtime.log.audit({event: "nodes.install",module:opts.module,version:opts.version,error:err.code}, opts.req); + runtime.log.audit({event: "nodes.install",module:opts.module,version:opts.version,url:opts.url,error:err.code}, opts.req); } else { err.status = 400; - runtime.log.audit({event: "nodes.install",module:opts.module,version:opts.version,error:err.code||"unexpected_error",message:err.toString()}, opts.req); + runtime.log.audit({event: "nodes.install",module:opts.module,version:opts.version,url:opts.url,error:err.code||"unexpected_error",message:err.toString()}, opts.req); } return reject(err); }) diff --git a/packages/node_modules/@node-red/runtime/lib/nodes/Node.js b/packages/node_modules/@node-red/runtime/lib/nodes/Node.js index c62895f7f..a834c8c34 100644 --- a/packages/node_modules/@node-red/runtime/lib/nodes/Node.js +++ b/packages/node_modules/@node-red/runtime/lib/nodes/Node.js @@ -193,6 +193,7 @@ Node.prototype.emit = function(event, ...args) { */ Node.prototype._emitInput = function(arg) { var node = this; + this.metric("receive", arg); if (node._inputCallback) { // Just one callback registered. try { @@ -448,7 +449,6 @@ Node.prototype.receive = function(msg) { if (!msg._msgid) { msg._msgid = redUtil.generateId(); } - this.metric("receive",msg); this.emit("input",msg); }; diff --git a/packages/node_modules/@node-red/runtime/lib/nodes/context/index.js b/packages/node_modules/@node-red/runtime/lib/nodes/context/index.js index 322fa2868..c395c0615 100644 --- a/packages/node_modules/@node-red/runtime/lib/nodes/context/index.js +++ b/packages/node_modules/@node-red/runtime/lib/nodes/context/index.js @@ -420,15 +420,39 @@ function createRootContext() { Object.defineProperties(obj, { get: { value: function(key, storage, callback) { + if (!callback && typeof storage === 'function') { + callback = storage; + storage = undefined; + } + if (callback) { + callback() + return; + } return undefined; } }, set: { value: function(key, value, storage, callback) { + if (!callback && typeof storage === 'function') { + callback = storage; + storage = undefined; + } + if (callback) { + callback() + return + } } }, keys: { value: function(storage, callback) { + if (!callback && typeof storage === 'function') { + callback = storage; + storage = undefined; + } + if (callback) { + callback(); + return; + } return undefined; } } @@ -436,25 +460,48 @@ function createRootContext() { return obj; } -function getContext(localId,flowId,parent) { - var contextId = localId; +/** + * Get a flow-level context object. + * @param {string} flowId [description] + * @param {string} parentFlowId the id of the parent flow. undefined + * @return {object}} the context object + */ +function getFlowContext(flowId,parentFlowId) { + if (contexts.hasOwnProperty(flowId)) { + return contexts[flowId]; + } + var parentContext = contexts[parentFlowId]; + if (!parentContext) { + parentContext = createRootContext(); + contexts[parentFlowId] = parentContext; + // throw new Error("Flow "+flowId+" is missing parent context "+parentFlowId); + } + var newContext = createContext(flowId,undefined,parentContext); + contexts[flowId] = newContext; + return newContext; + +} + +function getContext(nodeId, flowId) { + var contextId = nodeId; if (flowId) { - contextId = localId+":"+flowId; + contextId = nodeId+":"+flowId; } if (contexts.hasOwnProperty(contextId)) { return contexts[contextId]; } - var newContext = createContext(contextId,undefined,parent); + var newContext = createContext(contextId); + if (flowId) { - var node = flows.get(flowId); - var parent = undefined; - if (node && node.type.startsWith("subflow:")) { - parent = node.context().flow; + var flowContext = contexts[flowId]; + if (!flowContext) { + // This is most likely due to a unit test for a node which doesn't + // initialise the flow properly. + // To keep things working, initialise the missing context. + // This *does not happen* in normal node-red operation + flowContext = createContext(flowId,undefined,createRootContext()); + contexts[flowId] = flowContext; } - else { - parent = createRootContext(); - } - var flowContext = getContext(flowId,undefined,parent); Object.defineProperty(newContext, 'flow', { value: flowContext }); @@ -466,6 +513,39 @@ function getContext(localId,flowId,parent) { return newContext; } +// +// function getContext(localId,flowId,parent) { +// var contextId = localId; +// if (flowId) { +// contextId = localId+":"+flowId; +// } +// console.log("getContext",localId,flowId,"known?",contexts.hasOwnProperty(contextId)); +// if (contexts.hasOwnProperty(contextId)) { +// return contexts[contextId]; +// } +// var newContext = createContext(contextId,undefined,parent); +// if (flowId) { +// var node = flows.get(flowId); +// console.log("flows,get",flowId,node&&node.type) +// var parent = undefined; +// if (node && node.type.startsWith("subflow:")) { +// parent = node.context().flow; +// } +// else { +// parent = createRootContext(); +// } +// var flowContext = getContext(flowId,undefined,parent); +// Object.defineProperty(newContext, 'flow', { +// value: flowContext +// }); +// } +// Object.defineProperty(newContext, 'global', { +// value: contexts['global'] +// }) +// contexts[contextId] = newContext; +// return newContext; +// } + function deleteContext(id,flowId) { if(!hasConfiguredStore){ // only delete context if there's no configured storage. @@ -517,6 +597,7 @@ module.exports = { load: load, listStores: listStores, get: getContext, + getFlowContext:getFlowContext, delete: deleteContext, clean: clean, close: close diff --git a/packages/node_modules/@node-red/runtime/lib/nodes/context/localfilesystem.js b/packages/node_modules/@node-red/runtime/lib/nodes/context/localfilesystem.js index 0f83573e8..9755681d8 100644 --- a/packages/node_modules/@node-red/runtime/lib/nodes/context/localfilesystem.js +++ b/packages/node_modules/@node-red/runtime/lib/nodes/context/localfilesystem.js @@ -203,10 +203,10 @@ LocalFileSystem.prototype.open = function(){ var newContext = self.cache._export(); scopes.forEach(function(scope) { var storagePath = getStoragePath(self.storageBaseDir,scope); - var context = newContext[scope]; + var context = newContext[scope] || {}; var stringifiedContext = stringify(context); if (stringifiedContext.circular && !self.knownCircularRefs[scope]) { - log.warn(log._("error-circular",{scope:scope})); + log.warn(log._("context.localfilesystem.error-circular",{scope:scope})); self.knownCircularRefs[scope] = true; } else { delete self.knownCircularRefs[scope]; @@ -245,7 +245,7 @@ LocalFileSystem.prototype.get = function(scope, key, callback) { return this.cache.get(scope,key,callback); } if(typeof callback !== "function"){ - throw new Error("Callback must be a function"); + throw new Error("File Store cache disabled - only asynchronous access supported"); } var storagePath = getStoragePath(this.storageBaseDir ,scope); loadFile(storagePath + ".json").then(function(data){ @@ -304,7 +304,7 @@ LocalFileSystem.prototype.set = function(scope, key, value, callback) { }, this.flushInterval); } } else if (callback && typeof callback !== 'function') { - throw new Error("Callback must be a function"); + throw new Error("File Store cache disabled - only asynchronous access supported"); } else { self.writePromise = self.writePromise.then(function() { return loadFile(storagePath + ".json") }).then(function(data){ var obj = data ? JSON.parse(data) : {} @@ -324,7 +324,7 @@ LocalFileSystem.prototype.set = function(scope, key, value, callback) { } var stringifiedContext = stringify(obj); if (stringifiedContext.circular && !self.knownCircularRefs[scope]) { - log.warn(log._("error-circular",{scope:scope})); + log.warn(log._("context.localfilesystem.error-circular",{scope:scope})); self.knownCircularRefs[scope] = true; } else { delete self.knownCircularRefs[scope]; diff --git a/packages/node_modules/@node-red/runtime/lib/nodes/credentials.js b/packages/node_modules/@node-red/runtime/lib/nodes/credentials.js index 355bd9bc5..153ef4b06 100644 --- a/packages/node_modules/@node-red/runtime/lib/nodes/credentials.js +++ b/packages/node_modules/@node-red/runtime/lib/nodes/credentials.js @@ -341,33 +341,62 @@ var api = module.exports = { extract: function(node) { var nodeID = node.id; var nodeType = node.type; + var cred; var newCreds = node.credentials; if (newCreds) { delete node.credentials; var savedCredentials = credentialCache[nodeID] || {}; - var dashedType = nodeType.replace(/\s+/g, '-'); - var definition = credentialsDef[dashedType]; - if (!definition) { - log.warn(log._("nodes.credentials.not-registered",{type:nodeType})); - return; - } + if (/^subflow(:|$)/.test(nodeType)) { + for (cred in newCreds) { + if (newCreds.hasOwnProperty(cred)) { + if (newCreds[cred] === "__PWRD__") { + continue; + } + if (0 === newCreds[cred].length || /^\s*$/.test(newCreds[cred])) { + delete savedCredentials[cred]; + dirty = true; + continue; + } + if (!savedCredentials.hasOwnProperty(cred) || JSON.stringify(savedCredentials[cred]) !== JSON.stringify(newCreds[cred])) { + savedCredentials[cred] = newCreds[cred]; + dirty = true; + } - for (var cred in definition) { - if (definition.hasOwnProperty(cred)) { - if (newCreds[cred] === undefined) { - continue; } - if (definition[cred].type == "password" && newCreds[cred] == '__PWRD__') { - continue; + } + for (cred in savedCredentials) { + if (savedCredentials.hasOwnProperty(cred)) { + if (!newCreds.hasOwnProperty(cred)) { + delete savedCredentials[cred]; + dirty = true; + } } - if (0 === newCreds[cred].length || /^\s*$/.test(newCreds[cred])) { - delete savedCredentials[cred]; - dirty = true; - continue; - } - if (!savedCredentials.hasOwnProperty(cred) || JSON.stringify(savedCredentials[cred]) !== JSON.stringify(newCreds[cred])) { - savedCredentials[cred] = newCreds[cred]; - dirty = true; + } + } else { + var dashedType = nodeType.replace(/\s+/g, '-'); + var definition = credentialsDef[dashedType]; + if (!definition) { + log.warn(log._("nodes.credentials.not-registered",{type:nodeType})); + return; + } + + for (cred in definition) { + if (definition.hasOwnProperty(cred)) { + if (newCreds[cred] === undefined) { + continue; + } + if (definition[cred].type == "password" && newCreds[cred] == '__PWRD__') { + continue; + } + if (0 === newCreds[cred].length || /^\s*$/.test(newCreds[cred])) { + delete savedCredentials[cred]; + dirty = true; + continue; + } + if (!savedCredentials.hasOwnProperty(cred) || JSON.stringify(savedCredentials[cred]) !== JSON.stringify(newCreds[cred])) { + savedCredentials[cred] = newCreds[cred]; + dirty = true; + } } } } diff --git a/packages/node_modules/@node-red/runtime/lib/nodes/flows/Flow.js b/packages/node_modules/@node-red/runtime/lib/nodes/flows/Flow.js index db15e8eb5..df4e810fa 100644 --- a/packages/node_modules/@node-red/runtime/lib/nodes/flows/Flow.js +++ b/packages/node_modules/@node-red/runtime/lib/nodes/flows/Flow.js @@ -18,6 +18,7 @@ var clone = require("clone"); var redUtil = require("@node-red/util").util; var flowUtil = require("./util"); var events = require("../../events"); +const context = require('../context'); var Subflow; var Log; @@ -54,6 +55,8 @@ class Flow { this.catchNodes = []; this.statusNodes = []; this.path = this.id; + // Ensure a context exists for this flow + this.context = context.getFlowContext(this.id,this.parent.id); } /** @@ -568,15 +571,18 @@ function stopNode(node,removed) { Log.trace("Stopping node "+node.type+":"+node.id+(removed?" removed":"")); const start = Date.now(); const closePromise = node.close(removed); + let closeTimer = null; const closeTimeout = new Promise((resolve,reject) => { - setTimeout(() => { + closeTimer = setTimeout(() => { reject("Close timed out"); }, nodeCloseTimeout); }); return Promise.race([closePromise,closeTimeout]).then(() => { + clearTimeout(closeTimer); var delta = Date.now() - start; Log.trace("Stopped node "+node.type+":"+node.id+" ("+delta+"ms)" ); }).catch(err => { + clearTimeout(closeTimer); node.error(Log._("nodes.flows.stopping-error",{message:err})); Log.debug(err.stack); }) diff --git a/packages/node_modules/@node-red/runtime/lib/nodes/flows/Subflow.js b/packages/node_modules/@node-red/runtime/lib/nodes/flows/Subflow.js index a7bec1234..cca230d79 100644 --- a/packages/node_modules/@node-red/runtime/lib/nodes/flows/Subflow.js +++ b/packages/node_modules/@node-red/runtime/lib/nodes/flows/Subflow.js @@ -16,12 +16,15 @@ const clone = require("clone"); const Flow = require('./Flow').Flow; - +const context = require('../context'); const util = require("util"); const redUtil = require("@node-red/util").util; const flowUtil = require("./util"); + +const credentials = require("../credentials"); + var Log; /** @@ -38,41 +41,12 @@ function evaluateInputValue(value, type, node) { if (type === "bool") { return (value === "true") || (value === true); } + if (type === "cred") { + return value; + } return redUtil.evaluateNodeProperty(value, type, node, null, null); } -/** - * Compose information object for env var - */ -function composeInfo(info, val) { - var result = { - name: info.name, - type: info.type, - value: val, - }; - if (info.ui) { - var ui = info.ui; - result.ui = { - hasUI: ui.hasUI, - icon: ui.icon, - labels: ui.labels, - type: ui.type - }; - var retUI = result.ui; - if (ui.type === "input") { - retUI.inputTypes = ui.inputTypes; - } - if (ui.type === "select") { - retUI.menu = ui.menu; - } - if (ui.type === "spinner") { - retUI.spinner = ui.spinner; - } - } - return result; -} - - /** * This class represents a subflow - which is handled as a special type of Flow */ @@ -142,10 +116,16 @@ class Subflow extends Flow { this.node_map = node_map; this.path = parent.path+"/"+(subflowInstance._alias||subflowInstance.id); + this.templateCredentials = credentials.get(subflowDef.id); + this.instanceCredentials = credentials.get(this.id); + var env = []; if (this.subflowDef.env) { this.subflowDef.env.forEach(e => { env[e.name] = e; + if (e.type === "cred") { + e.value = this.templateCredentials[e.name]; + } }); } if (this.subflowInstance.env) { @@ -154,7 +134,14 @@ class Subflow extends Flow { var ui = old ? old.ui : null; env[e.name] = e; if (ui) { - env[e.name].ui = ui; + env[e.name].ui = ui; + } + if (e.type === "cred") { + if (!old || this.instanceCredentials.hasOwnProperty(e.name) ) { + e.value = this.instanceCredentials[e.name]; + } else if (old) { + e.value = this.templateCredentials[e.name]; + } } }); } @@ -227,6 +214,10 @@ class Subflow extends Flow { this.node.on("input", function(msg) { this.send(msg);}); this.node.on("close", function() { this.status({}); }) this.node.status = status => this.parent.handleStatus(this.node,status); + // Create a context instance + // console.log("Node.context",this.type,"id:",this._alias||this.id,"z:",this.z) + this._context = context.get(this._alias||this.id,this.z); + this.node._updateWires = this.node.updateWires; @@ -324,19 +315,10 @@ class Subflow extends Flow { * @return {Object} val value of env var */ getSetting(name) { - this.trace("getSetting:"+name); if (!/^\$parent\./.test(name)) { var env = this.env; - var is_info = name.endsWith("_info"); - var is_type = name.endsWith("_type"); - var ename = (is_info || is_type) ? name.substring(0, name.length -5) : name; // 5 = length of "_info"/"_type" - - if (env && env.hasOwnProperty(ename)) { - var val = env[ename]; - - if (is_type) { - return val ? val.type : undefined; - } + if (env && env.hasOwnProperty(name)) { + var val = env[name]; // If this is an env type property we need to be careful not // to get into lookup loops. // 1. if the value to lookup is the same as this one, go straight to parent @@ -350,11 +332,7 @@ class Subflow extends Flow { value = value.replace(new RegExp("\\${"+name+"}","g"),"${$parent."+name+"}"); } try { - var ret = evaluateInputValue(value, type, this.node); - if (is_info) { - return composeInfo(val, ret); - } - return ret; + return evaluateInputValue(value, type, this.node); } catch (e) { this.error(e); @@ -466,7 +444,7 @@ function createNodeInSubflow(subflowInstanceId, def) { * properties in the nodes object to reference the new node ids. * This handles: * - node.wires, - * - node.scope of Catch and Status nodes, + * - node.scope of Complete, Catch and Status nodes, * - node.XYZ for any property where XYZ is recognised as an old property * @param {[type]} nodes [description] * @param {[type]} nodeMap [description] @@ -489,7 +467,7 @@ function remapSubflowNodes(nodes,nodeMap) { } } } - if ((node.type === 'catch' || node.type === 'status') && node.scope) { + if ((node.type === 'complete' || node.type === 'catch' || node.type === 'status') && node.scope) { node.scope = node.scope.map(function(id) { return nodeMap[id]?nodeMap[id].id:"" }) diff --git a/packages/node_modules/@node-red/runtime/lib/nodes/flows/index.js b/packages/node_modules/@node-red/runtime/lib/nodes/flows/index.js index 981ed7173..1a350ce7c 100644 --- a/packages/node_modules/@node-red/runtime/lib/nodes/flows/index.js +++ b/packages/node_modules/@node-red/runtime/lib/nodes/flows/index.js @@ -106,15 +106,20 @@ function load(forceStart) { // This is a force reload from the API - disable safeMode delete settings.safeMode; } - return setFlows(null,"load",false,forceStart); + return setFlows(null,null,"load",false,forceStart); } /* * _config - new node array configuration + * _credentials - new credentials configuration (optional) * type - full/nodes/flows/load (default full) * muteLog - don't emit the standard log messages (used for individual flow api) */ -function setFlows(_config,type,muteLog,forceStart) { +function setFlows(_config,_credentials,type,muteLog,forceStart) { + if (typeof _credentials === "string") { + type = _credentials; + _credentials = null; + } type = type||"full"; if (settings.safeMode) { if (type !== "load") { @@ -155,16 +160,27 @@ function setFlows(_config,type,muteLog,forceStart) { delete newFlowConfig.allNodes[id].credentials; } } + var credsDirty; - // Allow the credential store to remove anything no longer needed - credentials.clean(config); + if (_credentials) { + // A full set of credentials have been provided. Use those instead + configSavePromise = credentials.load(_credentials); + credsDirty = true; + } else { + // Allow the credential store to remove anything no longer needed + credentials.clean(config); - // Remember whether credentials need saving or not - var credsDirty = credentials.dirty(); + // Remember whether credentials need saving or not + var credsDirty = credentials.dirty(); + + configSavePromise = Promise.resolve(); + } // Get the latest credentials and ask storage to save them (if needed) // as well as the new flow configuration. - configSavePromise = credentials.export().then(function(creds) { + configSavePromise = configSavePromise.then(function() { + return credentials.export() + }).then(function(creds) { var saveConfig = { flows: config, credentialsDirty:credsDirty, @@ -515,7 +531,7 @@ function addFlow(flow) { var newConfig = clone(activeConfig.flows); newConfig = newConfig.concat(nodes); - return setFlows(newConfig,'flows',true).then(function() { + return setFlows(newConfig,null,'flows',true).then(function() { log.info(log._("nodes.flows.added-flow",{label:(flow.label?flow.label+" ":"")+"["+flow.id+"]"})); return flow.id; }); @@ -537,7 +553,7 @@ function getFlow(id) { if (flow.label) { result.label = flow.label; } - if (flow.disabled) { + if (flow.hasOwnProperty('disabled')) { result.disabled = flow.disabled; } if (flow.hasOwnProperty('info')) { @@ -646,7 +662,7 @@ function updateFlow(id,newFlow) { } newConfig = newConfig.concat(nodes); - return setFlows(newConfig,'flows',true).then(function() { + return setFlows(newConfig,null,'flows',true).then(function() { log.info(log._("nodes.flows.updated-flow",{label:(label?label+" ":"")+"["+id+"]"})); }) } @@ -668,7 +684,7 @@ function removeFlow(id) { return node.z !== id && node.id !== id; }); - return setFlows(newConfig,'flows',true).then(function() { + return setFlows(newConfig,null,'flows',true).then(function() { log.info(log._("nodes.flows.removed-flow",{label:(flow.label?flow.label+" ":"")+"["+flow.id+"]"})); }); } diff --git a/packages/node_modules/@node-red/runtime/lib/nodes/flows/util.js b/packages/node_modules/@node-red/runtime/lib/nodes/flows/util.js index b6074d792..e06cc5ede 100644 --- a/packages/node_modules/@node-red/runtime/lib/nodes/flows/util.js +++ b/packages/node_modules/@node-red/runtime/lib/nodes/flows/util.js @@ -85,6 +85,7 @@ module.exports = { flow.subflows = {}; flow.configs = {}; flow.flows = {}; + flow.groups = {}; flow.missingTypes = []; config.forEach(function(n) { @@ -95,8 +96,12 @@ module.exports = { flow.flows[n.id].configs = {}; flow.flows[n.id].nodes = {}; } + if (n.type === 'group') { + flow.groups[n.id] = n; + } }); + // TODO: why a separate forEach? this can be merged with above config.forEach(function(n) { if (n.type === 'subflow') { flow.subflows[n.id] = n; @@ -108,7 +113,7 @@ module.exports = { var linkWires = {}; var linkOutNodes = []; config.forEach(function(n) { - if (n.type !== 'subflow' && n.type !== 'tab') { + if (n.type !== 'subflow' && n.type !== 'tab' && n.type !== 'group') { var subflowDetails = subflowInstanceRE.exec(n.type); if ( (subflowDetails && !flow.subflows[subflowDetails[1]]) || (!subflowDetails && !typeRegistry.get(n.type)) ) { @@ -163,7 +168,7 @@ module.exports = { var addedTabs = {}; config.forEach(function(n) { - if (n.type !== 'subflow' && n.type !== 'tab') { + if (n.type !== 'subflow' && n.type !== 'tab' && n.type !== 'group') { for (var prop in n) { if (n.hasOwnProperty(prop) && prop !== 'id' && prop !== 'wires' && prop !== 'type' && prop !== '_users' && flow.configs.hasOwnProperty(n[prop])) { // This property references a global config node diff --git a/packages/node_modules/@node-red/runtime/lib/nodes/index.js b/packages/node_modules/@node-red/runtime/lib/nodes/index.js index fcb153fe1..09aebe6eb 100644 --- a/packages/node_modules/@node-red/runtime/lib/nodes/index.js +++ b/packages/node_modules/@node-red/runtime/lib/nodes/index.js @@ -150,10 +150,10 @@ function reportNodeStateChange(info,enabled) { } } -function installModule(module,version) { +function installModule(module,version,url) { var existingModule = registry.getModuleInfo(module); var isUpgrade = !!existingModule; - return registry.installModule(module,version).then(function(info) { + return registry.installModule(module,version,url).then(function(info) { if (isUpgrade) { events.emit("runtime-event",{id:"node/upgraded",retain:false,payload:{module:module,version:version}}); } else { diff --git a/packages/node_modules/@node-red/runtime/lib/settings.js b/packages/node_modules/@node-red/runtime/lib/settings.js index 35417a497..e22a90686 100644 --- a/packages/node_modules/@node-red/runtime/lib/settings.js +++ b/packages/node_modules/@node-red/runtime/lib/settings.js @@ -87,7 +87,7 @@ var persistentSettings = { throw new Error(log._("settings.not-available")); } var current = globalSettings[prop]; - globalSettings[prop] = value; + globalSettings[prop] = clone(value); try { assert.deepEqual(current,value); return when.resolve(); diff --git a/packages/node_modules/@node-red/runtime/lib/storage/localfilesystem/library.js b/packages/node_modules/@node-red/runtime/lib/storage/localfilesystem/library.js index da0e1f94f..04701321f 100644 --- a/packages/node_modules/@node-red/runtime/lib/storage/localfilesystem/library.js +++ b/packages/node_modules/@node-red/runtime/lib/storage/localfilesystem/library.js @@ -25,73 +25,66 @@ var settings; var libDir; var libFlowsDir; +function toSingleLine(text) { + var result = text.replace(/\\/g, "\\\\").replace(/\n/g, "\\n"); + return result; +} -function getFileMeta(root,path) { - var fn = fspath.join(root,path); - var fd = fs.openSync(fn,"r"); +function fromSingleLine(text) { + var result = text.replace(/\\[\\n]/g, function(s) { + return ((s === "\\\\") ? "\\" : "\n"); + }); + return result; +} + +function getFileMeta(root, path) { + var fn = fspath.join(root, path); + var fd = fs.openSync(fn, 'r'); var size = fs.fstatSync(fd).size; var meta = {}; var read = 0; var length = 10; - var remaining = ""; + var remaining = Buffer.alloc(0); var buffer = Buffer.alloc(length); - while(read < size) { - read+=fs.readSync(fd,buffer,0,length); - var data = remaining+buffer.toString(); - var parts = data.split("\n"); - remaining = parts.splice(-1); - for (var i=0;i 0?"\n":"")+parts[i]; - } - } - if (!scanning) { - body += remaining; - } - } else { - read += thisRead; - body += buffer.slice(0,thisRead).toString(); + for (var i = 0; i < parts.length; i++) { + if (! /^\/\/ \w+: /.test(parts[i]) || !scanning) { + body += (body.length > 0 ? '\n' : '') + parts[i]; + scanning = false; } } - fs.closeSync(fd); return body; } + function getLibraryEntry(type,path) { var root = fspath.join(libDir,type); var rootPath = fspath.join(libDir,type,path); @@ -172,7 +165,7 @@ module.exports = { var headers = ""; for (var i in meta) { if (meta.hasOwnProperty(i)) { - headers += "// "+i+": "+meta[i]+"\n"; + headers += "// "+i+": "+toSingleLine(meta[i])+"\n"; } } if (type === "flows" && settings.flowFilePretty) { diff --git a/packages/node_modules/@node-red/runtime/lib/storage/localfilesystem/projects/Project.js b/packages/node_modules/@node-red/runtime/lib/storage/localfilesystem/projects/Project.js index 7ba3bd2f3..ad3c0bf26 100644 --- a/packages/node_modules/@node-red/runtime/lib/storage/localfilesystem/projects/Project.js +++ b/packages/node_modules/@node-red/runtime/lib/storage/localfilesystem/projects/Project.js @@ -706,7 +706,9 @@ Project.prototype.fetch = function(user,remoteName) { promise = promise.then(function() { return gitTools.fetch(project.path,remote,authCache.get(project.name,project.remotes[remote].fetch,username)) }).catch(function(err) { - err.remote = remote; + if (!err.remote) { + err.remote = remote; + } throw err; }) }); diff --git a/packages/node_modules/@node-red/runtime/lib/storage/localfilesystem/projects/git/index.js b/packages/node_modules/@node-red/runtime/lib/storage/localfilesystem/projects/git/index.js index b9f114698..d812c05f2 100644 --- a/packages/node_modules/@node-red/runtime/lib/storage/localfilesystem/projects/git/index.js +++ b/packages/node_modules/@node-red/runtime/lib/storage/localfilesystem/projects/git/index.js @@ -41,6 +41,9 @@ function runGitCommand(args,cwd,env,emit) { err.code = "git_connection_failed"; } else if (/Connection timed out/i.test(stderr)) { err.code = "git_connection_failed"; + } else if(/Host key verification failed/i.test(stderr)) { + // TODO: handle host key verification errors separately + err.code = "git_host_key_verification_failed"; } else if (/fatal: could not read/i.test(stderr)) { // Username/Password err.code = "git_auth_failed"; @@ -48,9 +51,6 @@ function runGitCommand(args,cwd,env,emit) { err.code = "git_auth_failed"; } else if(/Permission denied \(publickey\)/i.test(stderr)) { err.code = "git_auth_failed"; - } else if(/Host key verification failed/i.test(stderr)) { - // TODO: handle host key verification errors separately - err.code = "git_auth_failed"; } else if (/commit your changes or stash/i.test(stderr)) { err.code = "git_local_overwrite"; } else if (/CONFLICT/.test(err.stdout)) { diff --git a/packages/node_modules/@node-red/runtime/lib/storage/localfilesystem/projects/git/node-red-ask-pass.sh b/packages/node_modules/@node-red/runtime/lib/storage/localfilesystem/projects/git/node-red-ask-pass.sh index 695b2a19e..b6fbf80f4 100755 --- a/packages/node_modules/@node-red/runtime/lib/storage/localfilesystem/projects/git/node-red-ask-pass.sh +++ b/packages/node_modules/@node-red/runtime/lib/storage/localfilesystem/projects/git/node-red-ask-pass.sh @@ -1 +1,2 @@ +#!/bin/sh "$NODE_RED_GIT_NODE_PATH" "$NODE_RED_GIT_ASKPASS_PATH" "$NODE_RED_GIT_SOCK_PATH" $@ diff --git a/packages/node_modules/@node-red/runtime/lib/storage/localfilesystem/projects/git/node-red-ssh.sh b/packages/node_modules/@node-red/runtime/lib/storage/localfilesystem/projects/git/node-red-ssh.sh index 26a232f83..b879b7384 100755 --- a/packages/node_modules/@node-red/runtime/lib/storage/localfilesystem/projects/git/node-red-ssh.sh +++ b/packages/node_modules/@node-red/runtime/lib/storage/localfilesystem/projects/git/node-red-ssh.sh @@ -1 +1,2 @@ +#!/bin/sh ssh -i "$NODE_RED_KEY_FILE" -F /dev/null $@ diff --git a/packages/node_modules/@node-red/runtime/locales/en-US/runtime.json b/packages/node_modules/@node-red/runtime/locales/en-US/runtime.json index d56031d2f..fea4d5591 100644 --- a/packages/node_modules/@node-red/runtime/locales/en-US/runtime.json +++ b/packages/node_modules/@node-red/runtime/locales/en-US/runtime.json @@ -47,7 +47,14 @@ "now-running": "Server now running at __listenpath__", "failed-to-start": "Failed to start server:", "headless-mode": "Running in headless mode", - "httpadminauth-deprecated": "use of httpAdminAuth is deprecated. Use adminAuth instead" + "httpadminauth-deprecated": "use of httpAdminAuth is deprecated. Use adminAuth instead", + "https": { + "refresh-interval": "Refreshing https settings every __interval__ hours", + "settings-refreshed": "Server https settings have been refreshed", + "refresh-failed": "Failed to refresh https settings: __message__", + "nodejs-version": "httpsRefreshInterval requires Node.js 11 or later", + "function-required": "httpsRefreshInterval requires https property to be a function" + } }, "api": { diff --git a/packages/node_modules/@node-red/runtime/locales/zh-CN/runtime.json b/packages/node_modules/@node-red/runtime/locales/zh-CN/runtime.json new file mode 100644 index 000000000..d0813f476 --- /dev/null +++ b/packages/node_modules/@node-red/runtime/locales/zh-CN/runtime.json @@ -0,0 +1,174 @@ +{ + "runtime": { + "welcome": "欢迎使用Node-RED", + "version": "__component__ 版本: __version__", + "unsupported_version": "__component__的不受支持的版本。要求: __requires__ 找到: __version__", + "paths": { + "settings": "设置文件 : __path__", + "httpStatic": "HTTP Static : __path__" + } + }, + + "server": { + "loading": "加载控制板节点", + "palette-editor": { + "disabled": "控制板编辑器已禁用:用户设置", + "npm-not-found": "控制板编辑器已禁用:找不到npm命令", + "npm-too-old": "控制板编辑器已禁用: npm版本太旧。需要版本npm >= 3.x" + }, + "errors": "无法注册__count__节点类型", + "errors_plural": "无法注册__count__个节点类型", + "errors-help": "使用-v运行以获取详细信息", + "missing-modules": "缺少节点模块:", + "node-version-mismatch": "无法在此版本上加载节点模块。要求:__ version__", + "type-already-registered": "'__type__'已由模块__module__注册", + "removing-modules": "从配置中删除模块", + "added-types": "添加的节点类型:", + "removed-types": "删除的节点类型:", + "install": { + "invalid": "无效的模块名称", + "installing": "安装模块:__ name__,版本:__ version__", + "installed": "已安装的模块:__ name__", + "install-failed": "安装失败", + "install-failed-long": "模块__name__安装失败:", + "install-failed-not-found": "$t(server.install.install-failed-long) 模块未发现", + "upgrading": "更新模块: __name__ 至版本: __version__", + "upgraded": "更新模块: __name__。 重新启动Node-RED以使用新版本", + "upgrade-failed-not-found": "$t(server.install.install-failed-long) 未发现版本", + "uninstalling": "卸载模块: __name__", + "uninstall-failed": "卸载失败", + "uninstall-failed-long": "卸载模块__name__失败:", + "uninstalled": "卸载模块: __name__" + }, + "unable-to-listen": "无法在__listenpath__上收听", + "port-in-use": "错误: 端口正在使用中", + "uncaught-exception": "未捕获的异常:", + "admin-ui-disabled": "管理员界面已禁用", + "now-running": "服务器现在在__listenpath__上运行", + "failed-to-start": "无法启动服务器:", + "headless-mode": "在headless模式下运行", + "httpadminauth-deprecated": "不建议使用httpAdminAuth。请改用adminAuth" + }, + + "api": { + "flows": { + "error-save": "保存流程错误: __message__", + "error-reload": "重载流程错误: __message__" + }, + "library": { + "error-load-entry": "加载库条目'__path__'时出错:__message__", + "error-save-entry": "保存库条目'__path__'时出错:__ message__", + "error-load-flow": "加载流程'__path__'时出错:__ message__", + "error-save-flow": "保存流'__path__'时出错:__ message__" + }, + "nodes": { + "enabled": "启用的节点类型:", + "disabled": "禁用的节点类型:", + "error-enable": "无法启用节点:" + } + }, + + "comms": { + "error": "通讯渠道错误:__ message__", + "error-server": "通信服务器错误:__ message__", + "error-send": "通讯发送错误:__ message__" + }, + + "settings": { + "user-not-available": "无法保存用户设置:__ message__", + "not-available": "设置不可用", + "property-read-only": "属性“ __prop__”是只读的" + }, + + "nodes": { + "credentials": { + "error":"加载证书时出错:__ message__", + "error-saving":"保存证书时出错:__ message__", + "not-registered": "证书类型'__type__'未注册", + "system-key-warning": "\n\n---------------------------------------------------------------------\n您的流程证书文件是使用系统生成的密钥加密的。\n\n如果系统生成的密钥由于任何原因丢失,则您的证书文件将无法恢复,您将必须删除它并重新输入您的证书。\n\n您应该使用您的设置文件中的'credentialSecret'选项设置自己的密钥。然后,下次部署更改时,Node-RED将使用选择的密钥重新加密您的证书文件。\n---------------------------------------------------------------------\n" + }, + "flows": { + "safe-mode": "流程在安全模式下停止。部署开始。", + "registered-missing": "缺少注册的类型:__ type__", + "error": "错误加载流程:__ message__", + "starting-modified-nodes": "启动修改的节点", + "starting-modified-flows": "启动修改的流程", + "starting-flows": "启动流程", + "started-modified-nodes": "修改的节点已启动", + "started-modified-flows": "修改的流程已启动", + "started-flows": "流程已启动", + "stopping-modified-nodes": "停止修改的节点", + "stopping-modified-flows": "停止修改的流程", + "stopping-flows": "停止流程", + "stopped-modified-nodes": "修改的节点已停止", + "stopped-modified-flows": "修改的流程已停止", + "stopped-flows": "流程已停止", + "stopped": "已停止", + "stopping-error": "错误停止节点:__ message__", + "added-flow": "流程已添加: __label__", + "updated-flow": "流程已更新: __label__", + "removed-flow": "流程已移除: __label__", + "missing-types": "等待缺少的类型被注册:", + "missing-type-provided": " - __type__ (由npm模块__module__提供)", + "missing-type-install-1": "要安装所有缺少的模块,请运行:", + "missing-type-install-2": "在目录中:" + }, + "flow": { + "unknown-type": "未知类型: __type__", + "missing-types": "缺少类型", + "error-loop": "邮件已超过最大捕获数" + }, + "index": { + "unrecognised-id": "无法识别的ID: __id__", + "type-in-use": "使用中的类型: __msg__", + "unrecognised-module": "无法识别的模块: __module__" + }, + "registry": { + "localfilesystem": { + "module-not-found": "找不到模块:'__module__'" + } + } + }, + + "storage": { + "index": { + "forbidden-flow-name": "禁止流程名称" + }, + "localfilesystem": { + "user-dir": "用户目录: __path__", + "flows-file": "流程文件: __path__", + "create": "创建新__type__文件", + "empty": "现有__type__文件为空", + "invalid": "现有__type__文件为无效json", + "restore": "恢复__type__文件备份:__path__", + "restore-fail": "恢复__type__文件备份失败:__ message__", + "fsync-fail": "将文件__path__刷新到磁盘失败:__message__", + "projects": { + "changing-project": "设置活动项目:__ project__", + "active-project": "活动项目:__ project__", + "project-not-found": "找不到项目:__ project__", + "no-active-project": "没有活动的项目:使用默认流文件", + "disabled": "项目已禁用:editorTheme.projects.enabled = false", + "disabledNoFlag": "项目已禁用:设置editorTheme.projects.enabled = true来启用", + "git-not-found": "项目已禁用:找不到git命令", + "git-version-old": "项目已禁用:不支持的git __version__。 需要的git版本为2.x", + "summary": "一个Node-RED项目", + "readme": "### 关于\n\n这是您项目的README.md文件。它可以帮助用户了解您的项目,如何使用它以及他们可能需要知道的其他任何信息。" + } + } + }, + + "context": { + "log-store-init": "上下文储存: '__name__' [__info__]", + "error-loading-module": "加载上下文存储时出错: __message__", + "error-loading-module2": "加载上下文存储时出错 '__module__': __message__", + "error-module-not-defined": "上下文存储库'__storage__'缺少“模块”选项", + "error-invalid-module-name": "无效的上下文存储名称:'__ name__'", + "error-invalid-default-module": "无效的默认的上下文存储库: '__storage__'", + "unknown-store": "指定了未知的上下文存储库'__name__'。 使用默认存储库。", + "localfilesystem": { + "error-circular": "上下文__scope__包含无法保留的循环引用", + "error-write": "编写上下文时出错:__ message__" + } + } +} diff --git a/packages/node_modules/@node-red/runtime/locales/zh-TW/runtime.json b/packages/node_modules/@node-red/runtime/locales/zh-TW/runtime.json new file mode 100644 index 000000000..b96d87a6e --- /dev/null +++ b/packages/node_modules/@node-red/runtime/locales/zh-TW/runtime.json @@ -0,0 +1,174 @@ +{ + "runtime": { + "welcome": "歡迎使用Node-RED", + "version": "__component__ 版本: __version__", + "unsupported_version": "__component__的不受支持的版本。要求: __requires__ 找到: __version__", + "paths": { + "settings": "設置文件 : __path__", + "httpStatic": "HTTP Static : __path__" + } + }, + + "server": { + "loading": "加載控制板節點", + "palette-editor": { + "disabled": "控制板編輯器已禁用:用戶設置", + "npm-not-found": "控制板編輯器已禁用:找不到npm命令", + "npm-too-old": "控制板編輯器已禁用: npm版本太舊。需要版本npm >= 3.x" + }, + "errors": "無法註冊__count__節點類型", + "errors_plural": "無法註冊__count__個節點類型", + "errors-help": "使用-v運行以獲取詳細信息", + "missing-modules": "缺少節點模組:", + "node-version-mismatch": "無法在此版本上加載節點模組。要求:__ version__", + "type-already-registered": "'__type__'已由模組__module__註冊", + "removing-modules": "從配置中刪除模組", + "added-types": "添加的節點類型:", + "removed-types": "刪除的節點類型:", + "install": { + "invalid": "無效的模組名稱", + "installing": "安裝模組:__ name__,版本:__ version__", + "installed": "已安裝的模組:__ name__", + "install-failed": "安裝失敗", + "install-failed-long": "模組__name__安裝失敗:", + "install-failed-not-found": "$t(server.install.install-failed-long) 模組未發現", + "upgrading": "更新模組: __name__ 至版本: __version__", + "upgraded": "更新模組: __name__。 重新啟動Node-RED以使用新版本", + "upgrade-failed-not-found": "$t(server.install.install-failed-long) 未發現版本", + "uninstalling": "卸載模組: __name__", + "uninstall-failed": "卸載失敗", + "uninstall-failed-long": "卸載模組__name__失敗:", + "uninstalled": "卸載模組: __name__" + }, + "unable-to-listen": "無法在__listenpath__上收聽", + "port-in-use": "錯誤: 端口正在使用中", + "uncaught-exception": "未捕獲的異常:", + "admin-ui-disabled": "管理員界面已禁用", + "now-running": "服務器現在在__listenpath__上運行", + "failed-to-start": "無法啟動服務器:", + "headless-mode": "在headless模式下運行", + "httpadminauth-deprecated": "不建議使用httpAdminAuth。請改用adminAuth" + }, + + "api": { + "flows": { + "error-save": "保存流程錯誤: __message__", + "error-reload": "重載流程錯誤: __message__" + }, + "library": { + "error-load-entry": "加載庫條目'__path__'時出錯:__message__", + "error-save-entry": "保存庫條目'__path__'時出錯:__ message__", + "error-load-flow": "加載流程'__path__'時出錯:__ message__", + "error-save-flow": "保存流'__path__'時出錯:__ message__" + }, + "nodes": { + "enabled": "啟用的節點類型:", + "disabled": "禁用的節點類型:", + "error-enable": "無法啟用節點:" + } + }, + + "comms": { + "error": "通訊渠道錯誤:__ message__", + "error-server": "通信服務器錯誤:__ message__", + "error-send": "通訊發送錯誤:__ message__" + }, + + "settings": { + "user-not-available": "無法保存用戶設置:__ message__", + "not-available": "設置不可用", + "property-read-only": "屬性“ __prop__”是只讀的" + }, + + "nodes": { + "credentials": { + "error":"加載證書時出錯:__ message__", + "error-saving":"保存證書時出錯:__ message__", + "not-registered": "證書類型'__type__'未註冊", + "system-key-warning": "\n\n---------------------------------------------------------------------\n您的流程證書文件是使用系統生成的密鑰加密的。\n\n如果系統生成的密鑰由於任何原因丟失,則您的證書文件將無法恢復,您將必須刪除它並重新輸入您的證書。\n\n您應該使用您的設置文件中的'credentialSecret'選項設置自己的密鑰。然後,下次部署更改時,Node-RED將使用選擇的密鑰重新加密您的證書文件。\n---------------------------------------------------------------------\n" + }, + "flows": { + "safe-mode": "流程在安全模式下停止。部署開始。", + "registered-missing": "缺少註冊的類型:__ type__", + "error": "錯誤加載流程:__ message__", + "starting-modified-nodes": "啟動修改的節點", + "starting-modified-flows": "啟動修改的流程", + "starting-flows": "啟動流程", + "started-modified-nodes": "修改的節點已啟動", + "started-modified-flows": "修改的流程已啟動", + "started-flows": "流程已啟動", + "stopping-modified-nodes": "停止修改的節點", + "stopping-modified-flows": "停止修改的流程", + "stopping-flows": "停止流程", + "stopped-modified-nodes": "修改的節點已停止", + "stopped-modified-flows": "修改的流程已停止", + "stopped-flows": "流程已停止", + "stopped": "已停止", + "stopping-error": "錯誤停止節點:__ message__", + "added-flow": "流程已添加: __label__", + "updated-flow": "流程已更新: __label__", + "removed-flow": "流程已移除: __label__", + "missing-types": "等待缺少的類型被註冊:", + "missing-type-provided": " - __type__ (由npm模組__module__提供)", + "missing-type-install-1": "要安裝所有缺少的模組,請運行:", + "missing-type-install-2": "在目錄中:" + }, + "flow": { + "unknown-type": "未知類型: __type__", + "missing-types": "缺少類型", + "error-loop": "郵件已超過最大捕獲數" + }, + "index": { + "unrecognised-id": "無法識別的ID: __id__", + "type-in-use": "使用中的類型: __msg__", + "unrecognised-module": "無法識別的模組: __module__" + }, + "registry": { + "localfilesystem": { + "module-not-found": "找不到模組:'__module__'" + } + } + }, + + "storage": { + "index": { + "forbidden-flow-name": "禁止流程名稱" + }, + "localfilesystem": { + "user-dir": "用戶目錄: __path__", + "flows-file": "流程文件: __path__", + "create": "創建新__type__文件", + "empty": "現有__type__文件為空", + "invalid": "現有__type__文件為無效json", + "restore": "恢復__type__文件備份:__path__", + "restore-fail": "恢復__type__文件備份失敗:__ message__", + "fsync-fail": "將文件__path__刷新到磁盤失敗:__message__", + "projects": { + "changing-project": "設置活動項目:__ project__", + "active-project": "活動項目:__ project__", + "project-not-found": "找不到項目:__ project__", + "no-active-project": "沒有活動的項目:使用默認流文件", + "disabled": "項目已禁用:editorTheme.projects.enabled = false", + "disabledNoFlag": "項目已禁用:設置editorTheme.projects.enabled = true來啟用", + "git-not-found": "項目已禁用:找不到git命令", + "git-version-old": "項目已禁用:不支持的git __version__。 需要的git版本為2.x", + "summary": "一個Node-RED項目", + "readme": "### 關於\n\n這是您項目的README.md文件。它可以幫助用戶了解您的項目,如何使用它以及他們可能需要知道的其他任何信息。" + } + } + }, + + "context": { + "log-store-init": "上下文儲存: '__name__' [__info__]", + "error-loading-module": "加載上下文存儲時出錯: __message__", + "error-loading-module2": "加載上下文存儲時出錯 '__module__': __message__", + "error-module-not-defined": "上下文存儲庫'__storage__'缺少“模組”選項", + "error-invalid-module-name": "無效的上下文存儲名稱:'__ name__'", + "error-invalid-default-module": "無效的默認的上下文存儲庫: '__storage__'", + "unknown-store": "指定了未知的上下文存儲庫'__name__'。 使用默認存儲庫。", + "localfilesystem": { + "error-circular": "上下文__scope__包含無法保留的循環引用", + "error-write": "編寫上下文時出錯:__ message__" + } + } +} diff --git a/packages/node_modules/@node-red/runtime/package.json b/packages/node_modules/@node-red/runtime/package.json index 0d03b355b..93dc305f9 100644 --- a/packages/node_modules/@node-red/runtime/package.json +++ b/packages/node_modules/@node-red/runtime/package.json @@ -1,6 +1,6 @@ { "name": "@node-red/runtime", - "version": "1.0.3", + "version": "1.1.0", "license": "Apache-2.0", "main": "./lib/index.js", "repository": { @@ -16,8 +16,8 @@ } ], "dependencies": { - "@node-red/registry": "1.0.3", - "@node-red/util": "1.0.3", + "@node-red/registry": "1.1.0", + "@node-red/util": "1.1.0", "clone": "2.1.2", "express": "4.17.1", "fs-extra": "8.1.0", diff --git a/packages/node_modules/@node-red/util/lib/util.js b/packages/node_modules/@node-red/util/lib/util.js index 9cdf2e0bb..e5e270723 100644 --- a/packages/node_modules/@node-red/util/lib/util.js +++ b/packages/node_modules/@node-red/util/lib/util.js @@ -19,9 +19,9 @@ * @mixin @node-red/util_util */ - -const clone = require("clone"); +const clonedeep = require("lodash.clonedeep"); const jsonata = require("jsonata"); +const moment = require("moment-timezone"); const safeJSONStringify = require("json-stringify-safe"); const util = require("util"); @@ -81,22 +81,25 @@ function ensureBuffer(o) { * @memberof @node-red/util_util */ function cloneMessage(msg) { - // Temporary fix for #97 - // TODO: remove this http-node-specific fix somehow - var req = msg.req; - var res = msg.res; - delete msg.req; - delete msg.res; - var m = clone(msg); - if (req) { - m.req = req; - msg.req = req; + if (typeof msg !== "undefined" && msg !== null) { + // Temporary fix for #97 + // TODO: remove this http-node-specific fix somehow + var req = msg.req; + var res = msg.res; + delete msg.req; + delete msg.res; + var m = clonedeep(msg); + if (req) { + m.req = req; + msg.req = req; + } + if (res) { + m.res = res; + msg.res = res; + } + return m; } - if (res) { - m.res = res; - msg.res = res; - } - return m; + return msg; } /** @@ -566,16 +569,23 @@ function evaluateNodeProperty(value, type, node, msg, callback) { */ function prepareJSONataExpression(value,node) { var expr = jsonata(value); - expr.assign('flowContext',function(val) { - return node.context().flow.get(val); + expr.assign('flowContext', function(val, store) { + return node.context().flow.get(val, store); }); - expr.assign('globalContext',function(val) { - return node.context().global.get(val); + expr.assign('globalContext', function(val, store) { + return node.context().global.get(val, store); }); expr.assign('env', function(name) { var val = getSetting(node, name); - return (val ? val : ""); - }) + if (typeof val !== 'undefined') { + return val; + } else { + return ""; + } + }); + expr.assign('moment', function(arg1, arg2, arg3, arg4) { + return moment(arg1, arg2, arg3, arg4); + }); expr.registerFunction('clone', cloneMessage, '<(oa)-:o>'); expr._legacyMode = /(^|[^a-zA-Z0-9_'"])msg([^a-zA-Z0-9_'"]|$)/.test(value); expr._node = node; diff --git a/packages/node_modules/@node-red/util/package.json b/packages/node_modules/@node-red/util/package.json index 174db5bfd..4ae8b5014 100644 --- a/packages/node_modules/@node-red/util/package.json +++ b/packages/node_modules/@node-red/util/package.json @@ -1,6 +1,6 @@ { "name": "@node-red/util", - "version": "1.0.3", + "version": "1.1.0", "license": "Apache-2.0", "repository": { "type": "git", @@ -18,7 +18,9 @@ "clone": "2.1.2", "i18next": "15.1.2", "json-stringify-safe": "5.0.1", - "jsonata": "1.7.0", + "jsonata": "1.8.3", + "lodash.clonedeep": "^4.5.0", + "moment-timezone": "^0.5.31", "when": "3.7.8" } } diff --git a/packages/node_modules/node-red/package.json b/packages/node_modules/node-red/package.json index 618480400..5c7192a20 100644 --- a/packages/node_modules/node-red/package.json +++ b/packages/node_modules/node-red/package.json @@ -1,6 +1,6 @@ { "name": "node-red", - "version": "1.0.3", + "version": "1.1.0", "description": "Low-code programming for event-driven applications", "homepage": "http://nodered.org", "license": "Apache-2.0", @@ -31,17 +31,17 @@ "flow" ], "dependencies": { - "@node-red/editor-api": "1.0.3", - "@node-red/runtime": "1.0.3", - "@node-red/util": "1.0.3", - "@node-red/nodes": "1.0.3", + "@node-red/editor-api": "1.1.0", + "@node-red/runtime": "1.1.0", + "@node-red/util": "1.1.0", + "@node-red/nodes": "1.1.0", "basic-auth": "2.0.1", "bcryptjs": "2.4.3", "express": "4.17.1", "fs-extra": "8.1.0", "node-red-node-rbe": "^0.2.6", "node-red-node-tail": "^0.1.0", - "nopt": "4.0.1", + "nopt": "4.0.3", "semver": "6.3.0" }, "optionalDependencies": { diff --git a/packages/node_modules/node-red/red.js b/packages/node_modules/node-red/red.js index 26b6d1fc8..05dc1a0bf 100755 --- a/packages/node_modules/node-red/red.js +++ b/packages/node_modules/node-red/red.js @@ -39,7 +39,8 @@ var knownOpts = { "title": String, "userDir": [path], "verbose": Boolean, - "safe": Boolean + "safe": Boolean, + "define": [String, Array] }; var shortHands = { "?":["--help"], @@ -49,7 +50,8 @@ var shortHands = { // doesn't get treated as --title "t":["--help"], "u":["--userDir"], - "v":["--verbose"] + "v":["--verbose"], + "D":["--define"] }; nopt.invalidHandler = function(k,v,t) { // TODO: console.log(k,v,t); @@ -69,6 +71,7 @@ if (parsedArgs.help) { console.log(" -u, --userDir DIR use specified user directory"); console.log(" -v, --verbose enable verbose output"); console.log(" --safe enable safe mode"); + console.log(" -D, --define X=Y overwrite value in settings file"); console.log(" -?, --help show this help"); console.log(""); console.log("Documentation can be found at http://nodered.org"); @@ -130,6 +133,36 @@ try { process.exit(); } +if (parsedArgs.define) { + var defs = parsedArgs.define; + try { + while (defs.length > 0) { + var def = defs.shift(); + var match = /^(([^=]+)=(.+)|@(.*))$/.exec(def); + if (match) { + if (!match[4]) { + var val = JSON.parse(match[3]); + RED.util.setObjectProperty(settings, match[2], val, true); + } else { + var obj = fs.readJsonSync(match[4]); + for (var k in obj) { + if (obj.hasOwnProperty(k)) { + RED.util.setObjectProperty(settings, k, obj[k], true) + } + } + } + } + else { + throw new Error("Invalid syntax: '"+def+"'"); + } + + } + } catch (e) { + console.log("Error processing -D option: "+e.message); + process.exit(); + } +} + if (parsedArgs.verbose) { settings.verbose = true; } @@ -142,208 +175,265 @@ if (process.env.NODE_RED_ENABLE_PROJECTS) { settings.editorTheme.projects.enabled = !/^false$/i.test(process.env.NODE_RED_ENABLE_PROJECTS); } -if (settings.https) { - server = https.createServer(settings.https,function(req,res) {app(req,res);}); -} else { - server = http.createServer(function(req,res) {app(req,res);}); -} -server.setMaxListeners(0); +// Delay logging of (translated) messages until the RED object has been initialized +var delayedLogItems = []; -function formatRoot(root) { - if (root[0] != "/") { - root = "/" + root; - } - if (root.slice(-1) != "/") { - root = root + "/"; - } - return root; +var startupHttps = settings.https; +if (typeof startupHttps === "function") { + // Get the result of the function, because createServer doesn't accept functions as input + startupHttps = startupHttps(); } +var httpsPromise = Promise.resolve(startupHttps); -if (settings.httpRoot === false) { - settings.httpAdminRoot = false; - settings.httpNodeRoot = false; -} else { - settings.httpRoot = settings.httpRoot||"/"; - settings.disableEditor = settings.disableEditor||false; -} +httpsPromise.then(function(startupHttps) { + if (startupHttps) { + server = https.createServer(startupHttps,function(req,res) {app(req,res);}); -if (settings.httpAdminRoot !== false) { - settings.httpAdminRoot = formatRoot(settings.httpAdminRoot || settings.httpRoot || "/"); - settings.httpAdminAuth = settings.httpAdminAuth || settings.httpAuth; -} else { - settings.disableEditor = true; -} - -if (settings.httpNodeRoot !== false) { - settings.httpNodeRoot = formatRoot(settings.httpNodeRoot || settings.httpRoot || "/"); - settings.httpNodeAuth = settings.httpNodeAuth || settings.httpAuth; -} - -// if we got a port from command line, use it (even if 0) -// replicate (settings.uiPort = parsedArgs.port||settings.uiPort||1880;) but allow zero -if (parsedArgs.port !== undefined){ - settings.uiPort = parsedArgs.port; -} else { - if (settings.uiPort === undefined){ - settings.uiPort = 1880; - } -} - -settings.uiHost = settings.uiHost||"0.0.0.0"; - -if (flowFile) { - settings.flowFile = flowFile; -} -if (parsedArgs.userDir) { - settings.userDir = parsedArgs.userDir; -} - -try { - RED.init(server,settings); -} catch(err) { - if (err.code == "unsupported_version") { - console.log("Unsupported version of Node.js:",process.version); - console.log("Node-RED requires Node.js v8.9.0 or later"); - } else { - console.log("Failed to start server:"); - if (err.stack) { - console.log(err.stack); - } else { - console.log(err); - } - } - process.exit(1); -} - -function basicAuthMiddleware(user,pass) { - var basicAuth = require('basic-auth'); - var checkPassword; - var localCachedPassword; - if (pass.length == "32") { - // Assume its a legacy md5 password - checkPassword = function(p) { - return crypto.createHash('md5').update(p,'utf8').digest('hex') === pass; - } - } else { - checkPassword = function(p) { - return bcrypt.compareSync(p,pass); - } - } - - var checkPasswordAndCache = function(p) { - // For BasicAuth routes we know the password cannot change without - // a restart of Node-RED. This means we can cache the provided crypted - // version to save recalculating each time. - if (localCachedPassword === p) { - return true; - } - var result = checkPassword(p); - if (result) { - localCachedPassword = p; - } - return result; - } - - return function(req,res,next) { - if (req.method === 'OPTIONS') { - return next(); - } - var requestUser = basicAuth(req); - if (!requestUser || requestUser.name !== user || !checkPasswordAndCache(requestUser.pass)) { - res.set('WWW-Authenticate', 'Basic realm="Authorization Required"'); - return res.sendStatus(401); - } - next(); - } -} - -if (settings.httpAdminRoot !== false && settings.httpAdminAuth) { - RED.log.warn(RED.log._("server.httpadminauth-deprecated")); - app.use(settings.httpAdminRoot, basicAuthMiddleware(settings.httpAdminAuth.user,settings.httpAdminAuth.pass)); -} - -if (settings.httpAdminRoot !== false) { - app.use(settings.httpAdminRoot,RED.httpAdmin); -} -if (settings.httpNodeRoot !== false && settings.httpNodeAuth) { - app.use(settings.httpNodeRoot,basicAuthMiddleware(settings.httpNodeAuth.user,settings.httpNodeAuth.pass)); -} -if (settings.httpNodeRoot !== false) { - app.use(settings.httpNodeRoot,RED.httpNode); -} -if (settings.httpStatic) { - settings.httpStaticAuth = settings.httpStaticAuth || settings.httpAuth; - if (settings.httpStaticAuth) { - app.use("/",basicAuthMiddleware(settings.httpStaticAuth.user,settings.httpStaticAuth.pass)); - } - app.use("/",express.static(settings.httpStatic)); -} - -function getListenPath() { - var port = settings.serverPort; - if (port === undefined){ - port = settings.uiPort; - } - - var listenPath = 'http'+(settings.https?'s':'')+'://'+ - (settings.uiHost == '::'?'localhost':(settings.uiHost == '0.0.0.0'?'127.0.0.1':settings.uiHost))+ - ':'+port; - if (settings.httpAdminRoot !== false) { - listenPath += settings.httpAdminRoot; - } else if (settings.httpStatic) { - listenPath += "/"; - } - return listenPath; -} - -RED.start().then(function() { - if (settings.httpAdminRoot !== false || settings.httpNodeRoot !== false || settings.httpStatic) { - server.on('error', function(err) { - if (err.errno === "EADDRINUSE") { - RED.log.error(RED.log._("server.unable-to-listen", {listenpath:getListenPath()})); - RED.log.error(RED.log._("server.port-in-use")); - } else { - RED.log.error(RED.log._("server.uncaught-exception")); - if (err.stack) { - RED.log.error(err.stack); + if (settings.httpsRefreshInterval) { + var httpsRefreshInterval = parseFloat(settings.httpsRefreshInterval)||12; + if (httpsRefreshInterval > 596) { + // Max value based on (2^31-1)ms - the max that setInterval can accept + httpsRefreshInterval = 596; + } + // Check whether setSecureContext is available (Node.js 11+) + if (server.setSecureContext) { + // Check whether `http` is a callable function + if (typeof settings.https === "function") { + delayedLogItems.push({type:"info", id:"server.https.refresh-interval", params:{interval:httpsRefreshInterval}}); + setInterval(function () { + try { + // Get the result of the function, because createServer doesn't accept functions as input + Promise.resolve(settings.https()).then(function(refreshedHttps) { + if (refreshedHttps) { + // Only update the credentials in the server when key or cert has changed + if(!server.key || !server.cert || !server.key.equals(refreshedHttps.key) || !server.cert.equals(refreshedHttps.cert)) { + server.setSecureContext(refreshedHttps); + RED.log.info(RED.log._("server.https.settings-refreshed")); + } + } + }).catch(function(err) { + RED.log.error(RED.log._("server.https.refresh-failed",{message:err})); + }); + } catch(err) { + RED.log.error(RED.log._("server.https.refresh-failed",{message:err})); + } + }, httpsRefreshInterval*60*60*1000); } else { - RED.log.error(err); + delayedLogItems.push({type:"warn", id:"server.https.function-required"}); } + } else { + delayedLogItems.push({type:"warn", id:"server.https.nodejs-version"}); } - process.exit(1); - }); - server.listen(settings.uiPort,settings.uiHost,function() { - if (settings.httpAdminRoot === false) { - RED.log.info(RED.log._("server.admin-ui-disabled")); + } + } else { + server = http.createServer(function(req,res) {app(req,res);}); + } + server.setMaxListeners(0); + + function formatRoot(root) { + if (root[0] != "/") { + root = "/" + root; + } + if (root.slice(-1) != "/") { + root = root + "/"; + } + return root; + } + + if (settings.httpRoot === false) { + settings.httpAdminRoot = false; + settings.httpNodeRoot = false; + } else { + settings.httpRoot = settings.httpRoot||"/"; + settings.disableEditor = settings.disableEditor||false; + } + + if (settings.httpAdminRoot !== false) { + settings.httpAdminRoot = formatRoot(settings.httpAdminRoot || settings.httpRoot || "/"); + settings.httpAdminAuth = settings.httpAdminAuth || settings.httpAuth; + } else { + settings.disableEditor = true; + } + + if (settings.httpNodeRoot !== false) { + settings.httpNodeRoot = formatRoot(settings.httpNodeRoot || settings.httpRoot || "/"); + settings.httpNodeAuth = settings.httpNodeAuth || settings.httpAuth; + } + + // if we got a port from command line, use it (even if 0) + // replicate (settings.uiPort = parsedArgs.port||settings.uiPort||1880;) but allow zero + if (parsedArgs.port !== undefined){ + settings.uiPort = parsedArgs.port; + } else { + if (settings.uiPort === undefined){ + settings.uiPort = 1880; + } + } + + settings.uiHost = settings.uiHost||"0.0.0.0"; + + if (flowFile) { + settings.flowFile = flowFile; + } + if (parsedArgs.userDir) { + settings.userDir = parsedArgs.userDir; + } + + try { + RED.init(server,settings); + } catch(err) { + if (err.code == "unsupported_version") { + console.log("Unsupported version of Node.js:",process.version); + console.log("Node-RED requires Node.js v8.9.0 or later"); + } else { + console.log("Failed to start server:"); + if (err.stack) { + console.log(err.stack); + } else { + console.log(err); } - settings.serverPort = server.address().port; - process.title = parsedArgs.title || 'node-red'; - RED.log.info(RED.log._("server.now-running", {listenpath:getListenPath()})); - }); - } else { - RED.log.info(RED.log._("server.headless-mode")); + } + process.exit(1); } -}).otherwise(function(err) { - RED.log.error(RED.log._("server.failed-to-start")); - if (err.stack) { - RED.log.error(err.stack); - } else { - RED.log.error(err); - } -}); -process.on('uncaughtException',function(err) { - util.log('[red] Uncaught Exception:'); - if (err.stack) { - util.log(err.stack); - } else { - util.log(err); - } - process.exit(1); -}); + function basicAuthMiddleware(user,pass) { + var basicAuth = require('basic-auth'); + var checkPassword; + var localCachedPassword; + if (pass.length == "32") { + // Assume its a legacy md5 password + checkPassword = function(p) { + return crypto.createHash('md5').update(p,'utf8').digest('hex') === pass; + } + } else { + checkPassword = function(p) { + return bcrypt.compareSync(p,pass); + } + } -process.on('SIGINT', function () { - RED.stop().then(function() { - process.exit(); + var checkPasswordAndCache = function(p) { + // For BasicAuth routes we know the password cannot change without + // a restart of Node-RED. This means we can cache the provided crypted + // version to save recalculating each time. + if (localCachedPassword === p) { + return true; + } + var result = checkPassword(p); + if (result) { + localCachedPassword = p; + } + return result; + } + + return function(req,res,next) { + if (req.method === 'OPTIONS') { + return next(); + } + var requestUser = basicAuth(req); + if (!requestUser || requestUser.name !== user || !checkPasswordAndCache(requestUser.pass)) { + res.set('WWW-Authenticate', 'Basic realm="Authorization Required"'); + return res.sendStatus(401); + } + next(); + } + } + + if (settings.httpAdminRoot !== false && settings.httpAdminAuth) { + RED.log.warn(RED.log._("server.httpadminauth-deprecated")); + app.use(settings.httpAdminRoot, basicAuthMiddleware(settings.httpAdminAuth.user,settings.httpAdminAuth.pass)); + } + + if (settings.httpAdminRoot !== false) { + app.use(settings.httpAdminRoot,RED.httpAdmin); + } + if (settings.httpNodeRoot !== false && settings.httpNodeAuth) { + app.use(settings.httpNodeRoot,basicAuthMiddleware(settings.httpNodeAuth.user,settings.httpNodeAuth.pass)); + } + if (settings.httpNodeRoot !== false) { + app.use(settings.httpNodeRoot,RED.httpNode); + } + if (settings.httpStatic) { + settings.httpStaticAuth = settings.httpStaticAuth || settings.httpAuth; + if (settings.httpStaticAuth) { + app.use("/",basicAuthMiddleware(settings.httpStaticAuth.user,settings.httpStaticAuth.pass)); + } + app.use("/",express.static(settings.httpStatic)); + } + + function getListenPath() { + var port = settings.serverPort; + if (port === undefined){ + port = settings.uiPort; + } + + var listenPath = 'http'+(settings.https?'s':'')+'://'+ + (settings.uiHost == '::'?'localhost':(settings.uiHost == '0.0.0.0'?'127.0.0.1':settings.uiHost))+ + ':'+port; + if (settings.httpAdminRoot !== false) { + listenPath += settings.httpAdminRoot; + } else if (settings.httpStatic) { + listenPath += "/"; + } + return listenPath; + } + + RED.start().then(function() { + if (settings.httpAdminRoot !== false || settings.httpNodeRoot !== false || settings.httpStatic) { + server.on('error', function(err) { + if (err.errno === "EADDRINUSE") { + RED.log.error(RED.log._("server.unable-to-listen", {listenpath:getListenPath()})); + RED.log.error(RED.log._("server.port-in-use")); + } else { + RED.log.error(RED.log._("server.uncaught-exception")); + if (err.stack) { + RED.log.error(err.stack); + } else { + RED.log.error(err); + } + } + process.exit(1); + }); + + // Log all the delayed messages, since they can be translated at this point + delayedLogItems.forEach(function (delayedLogItem, index) { + RED.log[delayedLogItem.type](RED.log._(delayedLogItem.id, delayedLogItem.params||{})); + }); + + server.listen(settings.uiPort,settings.uiHost,function() { + if (settings.httpAdminRoot === false) { + RED.log.info(RED.log._("server.admin-ui-disabled")); + } + settings.serverPort = server.address().port; + process.title = parsedArgs.title || 'node-red'; + RED.log.info(RED.log._("server.now-running", {listenpath:getListenPath()})); + }); + } else { + RED.log.info(RED.log._("server.headless-mode")); + } + }).otherwise(function(err) { + RED.log.error(RED.log._("server.failed-to-start")); + if (err.stack) { + RED.log.error(err.stack); + } else { + RED.log.error(err); + } }); + + process.on('uncaughtException',function(err) { + util.log('[red] Uncaught Exception:'); + if (err.stack) { + util.log(err.stack); + } else { + util.log(err); + } + process.exit(1); + }); + + process.on('SIGINT', function () { + RED.stop().then(function() { + process.exit(); + }); + }); +}).catch(function(err) { + console.log("Failed to get https settings: " + err); }); diff --git a/packages/node_modules/node-red/settings.js b/packages/node_modules/node-red/settings.js index 960cb6d17..1941b8fdc 100644 --- a/packages/node_modules/node-red/settings.js +++ b/packages/node_modules/node-red/settings.js @@ -139,13 +139,36 @@ module.exports = { // The following property can be used to enable HTTPS // See http://nodejs.org/api/https.html#https_https_createserver_options_requestlistener // for details on its contents. - // See the comment at the top of this file on how to load the `fs` module used by - // this setting. - // + // See the comment at the top of this file on how to load the `fs` module used by this setting. + // This property can be either an object, containing both a (private) key and a (public) certificate, + // or a function that returns such an object: + //// https object: //https: { - // key: fs.readFileSync('privatekey.pem'), - // cert: fs.readFileSync('certificate.pem') + // key: fs.readFileSync('privkey.pem'), + // cert: fs.readFileSync('cert.pem') //}, + ////https synchronous function: + //https: function() { + // return { + // key: fs.readFileSync('privkey.pem'), + // cert: fs.readFileSync('cert.pem') + // } + //}, + //// https asynchronous function: + //https: function() { + // return Promise.resolve({ + // key: fs.readFileSync('privkey.pem'), + // cert: fs.readFileSync('cert.pem') + // }); + //}, + + // The following property can be used to refresh the https settings at a + // regular time interval in hours. + // This requires: + // - the `https` setting to be a function that can be called to get + // the refreshed settings. + // - Node.js 11 or later. + //httpsRefreshInterval : 12, // The following property can be used to cause insecure HTTP connections to // be redirected to HTTPS. @@ -182,6 +205,17 @@ module.exports = { // next(); //}, + + // The following property can be used to add a custom middleware function + // in front of all admin http routes. For example, to set custom http + // headers + // httpAdminMiddleware: function(req,res,next) { + // // Set the X-Frame-Options header to limit where the editor + // // can be embedded + // //res.set('X-Frame-Options', 'sameorigin'); + // next(); + // }, + // The following property can be used to pass custom options to the Express.js // server used by Node-RED. For a full list of available options, refer // to http://expressjs.com/en/api.html#app.settings.table @@ -243,7 +277,7 @@ module.exports = { // palette. If a node's category is not in the list, the category will get // added to the end of the palette. // If not set, the following default order is used: - //paletteCategories: ['subflows','flow','input','output','function','parser','social','mobile','storage','analysis','advanced'], + //paletteCategories: ['subflows', 'common', 'function', 'network', 'sequence', 'parser', 'storage'], // Configure the logging output logging: { diff --git a/scripts/install-ui-test-dependencies.sh b/scripts/install-ui-test-dependencies.sh index 0e13f0b4f..155d2b275 100755 --- a/scripts/install-ui-test-dependencies.sh +++ b/scripts/install-ui-test-dependencies.sh @@ -3,5 +3,7 @@ npm install --no-save \ wdio-chromedriver-service@^0.1.5 \ wdio-mocha-framework@^0.6.4 \ wdio-spec-reporter@^0.1.5 \ - webdriverio@^4.14.1 \ - chromedriver@^78.0.1 + webdriverio@^4.14.4 \ + chromedriver@^79.0.0 \ + wdio-browserstack-service@^0.1.19 \ + browserstack-local@^1.4.4 diff --git a/test/editor/pageobjects/editor/palette_page.js b/test/editor/pageobjects/editor/palette_page.js index a35859c5f..3b484a58c 100644 --- a/test/editor/pageobjects/editor/palette_page.js +++ b/test/editor/pageobjects/editor/palette_page.js @@ -15,22 +15,40 @@ **/ var idMap = { - // input + // common "inject": ".red-ui-palette-node[data-palette-type='inject']", - "httpIn": ".red-ui-palette-node[data-palette-type='http in']", - "mqttIn": ".red-ui-palette-node[data-palette-type='mqtt in']", - // output "debug": ".red-ui-palette-node[data-palette-type='debug']", - "httpResponse": ".red-ui-palette-node[data-palette-type='http response']", - "mqttOut": ".red-ui-palette-node[data-palette-type='mqtt out']", + "complete": ".red-ui-palette-node[data-palette-type='complete']", + "catch": ".red-ui-palette-node[data-palette-type='catch']", + "status": ".red-ui-palette-node[data-palette-type='status']", + "comment": ".red-ui-palette-node[data-palette-type='comment']", // function "function": ".red-ui-palette-node[data-palette-type='function']", - "template": ".red-ui-palette-node[data-palette-type='template']", + "switch": ".red-ui-palette-node[data-palette-type='switch']", "change": ".red-ui-palette-node[data-palette-type='change']", "range": ".red-ui-palette-node[data-palette-type='range']", + "template": ".red-ui-palette-node[data-palette-type='template']", + "delay": ".red-ui-palette-node[data-palette-type='delay']", + "trigger": ".red-ui-palette-node[data-palette-type='trigger']", + "exec": ".red-ui-palette-node[data-palette-type='exec']", + // network + "mqttIn": ".red-ui-palette-node[data-palette-type='mqtt in']", + "mqttOut": ".red-ui-palette-node[data-palette-type='mqtt out']", + "httpIn": ".red-ui-palette-node[data-palette-type='http in']", + "httpResponse": ".red-ui-palette-node[data-palette-type='http response']", "httpRequest": ".red-ui-palette-node[data-palette-type='http request']", + "websocketIn": ".red-ui-palette-node[data-palette-type='websocket in']", + "websocketOut": ".red-ui-palette-node[data-palette-type='websocket out']", + // sequence + "split": ".red-ui-palette-node[data-palette-type='split']", + "join": ".red-ui-palette-node[data-palette-type='join']", + "batch": ".red-ui-palette-node[data-palette-type='batch']", + // parser + "csv": ".red-ui-palette-node[data-palette-type='csv']", "html": ".red-ui-palette-node[data-palette-type='html']", "json": ".red-ui-palette-node[data-palette-type='json']", + "xml": ".red-ui-palette-node[data-palette-type='xml']", + "yaml": ".red-ui-palette-node[data-palette-type='yaml']", // storage "fileIn": ".red-ui-palette-node[data-palette-type='file in']", }; diff --git a/test/editor/pageobjects/editor/workspace_page.js b/test/editor/pageobjects/editor/workspace_page.js index ddf0a1c60..92a725dac 100644 --- a/test/editor/pageobjects/editor/workspace_page.js +++ b/test/editor/pageobjects/editor/workspace_page.js @@ -42,7 +42,7 @@ function addNode(type, x, y) { } } browser.waitForVisible('#red-ui-palette-search'); - browser.setValue('//*[@id="red-ui-palette-search"]/div/input', type.replace(/([A-Z])/g,' $1').toLowerCase()); + browser.setValue('//*[@id="red-ui-palette-search"]/div/form/input', type.replace(/([A-Z])/g, ' $1').toLowerCase()); browser.pause(300); browser.waitForVisible(palette.getId(type)); browser.moveToObject(palette.getId(type)); @@ -66,8 +66,8 @@ function deleteAllNodes() { function deploy() { browser.call(function () { - return when.promise(function(resolve, reject) { - events.on("runtime-event", function(event) { + return when.promise(function (resolve, reject) { + events.on("runtime-event", function (event) { if (event.id === 'runtime-deploy') { events.removeListener("runtime-event", arguments.callee); resolve(); diff --git a/test/editor/pageobjects/nodes/core/common/21-debug_page.js b/test/editor/pageobjects/nodes/core/common/21-debug_page.js index 602d4d542..990ed6cd9 100644 --- a/test/editor/pageobjects/nodes/core/common/21-debug_page.js +++ b/test/editor/pageobjects/nodes/core/common/21-debug_page.js @@ -18,8 +18,6 @@ var util = require("util"); var nodePage = require("../../node_page"); -var keyPage = require("../../../util/key_page"); - function debugNode(id) { nodePage.call(this, id); } @@ -33,6 +31,8 @@ debugNode.prototype.setOutput = function (complete) { // Select the "msg" type. browser.clickWithWait('//div[contains(@class, "red-ui-typedInput-options")][1]/a[1]'); // Input the path in msg. + browser.clickWithWait('//*[contains(@class, "red-ui-typedInput-input")]/input'); + browser.keys(Array('payload'.length).fill('Backspace')); browser.setValue('//*[contains(@class, "red-ui-typedInput-input")]/input', complete); } else { // Select the "complete msg object" type. diff --git a/test/editor/pageobjects/nodes/core/common/24-complete_page.js b/test/editor/pageobjects/nodes/core/common/24-complete_page.js new file mode 100644 index 000000000..558ee709a --- /dev/null +++ b/test/editor/pageobjects/nodes/core/common/24-complete_page.js @@ -0,0 +1,47 @@ +/** + * Copyright JS Foundation and other contributors, http://js.foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + **/ + +var util = require('util'); + +var nodePage = require('../../node_page'); + +function completeNode(id) { + nodePage.call(this, id); +} + +util.inherits(completeNode, nodePage); + +completeNode.prototype.setScope = function (scope) { + if (scope) { + browser.clickWithWait('//*[@id="node-input-complete-target-select"]'); + browser.pause(1000); + if (Array.isArray(scope)) { + for (var i = 0; i < scope.length; i++) { + browser.moveToObject(scope[i].id); + browser.buttonDown(); + browser.buttonUp(); + } + } else { + browser.moveToObject(scope.id); + browser.buttonDown(); + browser.buttonUp(); + } + browser.clickWithWait('//*[contains(@class, "red-ui-notification")]/div/button[2]'); + browser.pause(1000); + } +} + +module.exports = completeNode; diff --git a/test/editor/pageobjects/nodes/core/common/25-catch_page.js b/test/editor/pageobjects/nodes/core/common/25-catch_page.js new file mode 100644 index 000000000..89bcb7f1e --- /dev/null +++ b/test/editor/pageobjects/nodes/core/common/25-catch_page.js @@ -0,0 +1,48 @@ +/** + * Copyright JS Foundation and other contributors, http://js.foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + **/ + +var util = require('util'); + +var nodePage = require('../../node_page'); + +function catchNode(id) { + nodePage.call(this, id); +} + +util.inherits(catchNode, nodePage); + +catchNode.prototype.setScope = function (scope) { + if (scope) { + browser.selectWithWait('#node-input-scope-select', 'target'); + browser.clickWithWait('//*[@id="node-input-catch-target-select"]'); + browser.pause(1000); + if (Array.isArray(scope)) { + for (var i = 0; i < scope.length; i++) { + browser.moveToObject(scope[i].id); + browser.buttonDown(); + browser.buttonUp(); + } + } else { + browser.moveToObject(scope.id); + browser.buttonDown(); + browser.buttonUp(); + } + browser.clickWithWait('//*[contains(@class, "red-ui-notification")]/div/button[2]'); + browser.pause(1000); + } +} + +module.exports = catchNode; diff --git a/test/editor/pageobjects/nodes/core/common/25-status_page.js b/test/editor/pageobjects/nodes/core/common/25-status_page.js new file mode 100644 index 000000000..6de0fcde7 --- /dev/null +++ b/test/editor/pageobjects/nodes/core/common/25-status_page.js @@ -0,0 +1,48 @@ +/** + * Copyright JS Foundation and other contributors, http://js.foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + **/ + +var util = require('util'); + +var nodePage = require('../../node_page'); + +function statusNode(id) { + nodePage.call(this, id); +} + +util.inherits(statusNode, nodePage); + +statusNode.prototype.setScope = function (scope) { + if (scope) { + browser.selectWithWait('#node-input-scope-select', 'target'); + browser.clickWithWait('//*[@id="node-input-status-target-select"]'); + browser.pause(1000); + if (Array.isArray(scope)) { + for (var i = 0; i < scope.length; i++) { + browser.moveToObject(scope[i].id); + browser.buttonDown(); + browser.buttonUp(); + } + } else { + browser.moveToObject(scope.id); + browser.buttonDown(); + browser.buttonUp(); + } + browser.clickWithWait('//*[contains(@class, "red-ui-notification")]/div/button[2]'); + browser.pause(1000); + } +} + +module.exports = statusNode; diff --git a/test/editor/pageobjects/nodes/core/common/90-comment_page.js b/test/editor/pageobjects/nodes/core/common/90-comment_page.js new file mode 100644 index 000000000..2687dacfe --- /dev/null +++ b/test/editor/pageobjects/nodes/core/common/90-comment_page.js @@ -0,0 +1,27 @@ +/** + * Copyright JS Foundation and other contributors, http://js.foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + **/ + +var util = require('util'); + +var nodePage = require('../../node_page'); + +function commentNode(id) { + nodePage.call(this, id); +} + +util.inherits(commentNode, nodePage); + +module.exports = commentNode; diff --git a/test/editor/pageobjects/nodes/core/function/10-switch_page.js b/test/editor/pageobjects/nodes/core/function/10-switch_page.js new file mode 100644 index 000000000..a04014063 --- /dev/null +++ b/test/editor/pageobjects/nodes/core/function/10-switch_page.js @@ -0,0 +1,234 @@ +/** + * Copyright JS Foundation and other contributors, http://js.foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + **/ + +var util = require('util'); + +var nodePage = require('../../node_page'); + +function switchNode(id) { + nodePage.call(this, id); +} + +util.inherits(switchNode, nodePage); + +var vtType = { + "msg": 1, + "flow": 2, + "global": 3, + "str": 4, + "num": 5, + "jsonata": 6, + "env": 7, + "prev": 8 +}; + +function setT(t, index) { + browser.selectWithWait('//*[@id="node-input-rule-container"]/li[' + index + ']/div/div[1]/select', t); +} + +function setV(v, vt, index) { + if (vt) { + browser.clickWithWait('//*[@id="node-input-rule-container"]/li[' + index + ']/div/div[1]/div/button[1]'); + browser.clickWithWait('//div[contains(@class, "red-ui-typedInput-options") and not(contains(@style, "display: none"))]/a[' + vtType[vt] + ']'); + } + if (v) { + browser.setValue('//*[@id="node-input-rule-container"]/li[' + index + ']/div/div[1]/div/div/input', v); + } +} + +function setBetweenV(v, vt, v2, v2t, index) { + if (vt) { + browser.clickWithWait('//*[@id="node-input-rule-container"]/li[' + index + ']/div/div[1]/div[2]/button[1]'); + browser.clickWithWait('//div[contains(@class, "red-ui-typedInput-options") and not(contains(@style, "display: none"))]/a[' + vtType[vt] + ']'); + } + if (v) { + browser.setValue('//*[@id="node-input-rule-container"]/li[' + index + ']/div/div[1]/div[2]/div[1]/input', v); + } + if (v2t) { + browser.clickWithWait('//*[@id="node-input-rule-container"]/li[' + index + ']/div/div[3]/div/button[1]'); + browser.clickWithWait('//div[contains(@class, "red-ui-typedInput-options") and not(contains(@style, "display: none"))]/a[' + vtType[v2t] + ']'); + } + if (v2) { + browser.setValue('//*[@id="node-input-rule-container"]/li[' + index + ']/div/div[3]/div/div[1]/input', v2); + } +} + +function setSequenceV(v, vt, index) { + var sType = { + "flow": 1, + "global": 2, + "num": 3, + "jsonata": 4, + "env": 5, + }; + + if (vt) { + browser.clickWithWait('//*[@id="node-input-rule-container"]/li[' + index + ']/div/div[1]/div[2]/button[1]'); + browser.clickWithWait('//div[contains(@class, "red-ui-typedInput-options") and not(contains(@style, "display: none"))]/a[' + sType[vt] + ']'); + } + if (v) { + browser.setValue('//*[@id="node-input-rule-container"]/li[' + index + ']/div/div[1]/div[2]/div[1]/input', v); + } +} + +switchNode.prototype.ruleEqual = function (v, vt, index) { + index = index || 1; + setT('eq', index); + setV(v, vt, index); +} + +switchNode.prototype.ruleNotEqual = function (v, vt, index) { + index = index || 1; + setT('neq', index); + setV(v, vt, index); +} + +switchNode.prototype.ruleLessThan = function (v, vt, index) { + index = index || 1; + setT('lt', index); + setV(v, vt, index); +} + +switchNode.prototype.ruleLessThanOrEqual = function (v, vt, index) { + index = index || 1; + setT('lte', index); + setV(v, vt, index); +} + +switchNode.prototype.ruleGreaterThan = function (v, vt, index) { + index = index || 1; + setT('gt', index); + setV(v, vt, index); +} + +switchNode.prototype.ruleGreaterThanOrEqual = function (v, vt, index) { + index = index || 1; + setT('gte', index); + setV(v, vt, index); +} + +switchNode.prototype.ruleHasKey = function (v, vt, index) { + index = index || 1; + setT('hask', index); + setV(v, vt, index); +} + +switchNode.prototype.ruleIsBetween = function (v, vt, v2, v2t, index) { + index = index || 1; + setT('btwn', index); + setBetweenV(v, vt, v2, v2t, index); +} + +switchNode.prototype.ruleContains = function (v, vt, index) { + index = index || 1; + setT('cont', index); + setV(v, vt, index); +} + +switchNode.prototype.ruleMatchesRegex = function (v, vt, index) { + index = index || 1; + setT('regex', index); + setV(v, vt, index); +} + +switchNode.prototype.ruleIsTrue = function (index) { + index = index || 1; + setT('true', index); +} + +switchNode.prototype.ruleIsFalse = function (index) { + index = index || 1; + setT('false', index); +} + +switchNode.prototype.ruleIsNull = function (index) { + index = index || 1; + setT('null', index); +} + +switchNode.prototype.ruleIsNotNull = function (index) { + index = index || 1; + setT('nnull', index); +} + +switchNode.prototype.ruleIsOfType = function (t, index) { + index = index || 1; + setT('istype', index); + + var tType = { + "str": 1, + "num": 2, + "boolean": 3, + "array": 4, + "buffer": 5, + "object": 6, + "json": 7, + "undefined": 8, + "null": 9 + }; + + if (t) { + browser.clickWithWait('//*[@id="node-input-rule-container"]/li[' + index + ']/div/div[1]/div[2]/button[1]'); + browser.clickWithWait('//div[contains(@class, "red-ui-typedInput-options") and not(contains(@style, "display: none"))]/a[' + tType[t] + ']'); + } +} + +switchNode.prototype.ruleIsEmpty = function (index) { + index = index || 1; + setT('empty', index); +} + +switchNode.prototype.ruleIsNotEmpty = function (index) { + index = index || 1; + setT('nempty', index); +} + +switchNode.prototype.ruleHead = function (v, vt, index) { + index = index || 1; + setT('head', index); + setSequenceV(v, vt, index); +} + +switchNode.prototype.ruleIndexBetween = function (v, vt, v2, v2t, index) { + index = index || 1; + setT('index', index); + setBetweenV(v, vt, v2, v2t, index); +} + +switchNode.prototype.ruleTail = function (v, vt, index) { + index = index || 1; + setT('tail', index); + setSequenceV(v, vt, index); +} + +switchNode.prototype.ruleJSONataExp = function (v, index) { + index = index || 1; + setT('jsonata_exp', index); + if (v) { + browser.setValue('//*[@id="node-input-rule-container"]/li[' + index + ']/div/div[1]/div[2]/div[1]/input', v); + } +} + +switchNode.prototype.ruleOtherwise = function (index) { + index = index || 1; + setT('else', index); +} + +switchNode.prototype.addRule = function () { + browser.clickWithWait('//div[contains(@class, "red-ui-editableList")]/a'); +} + +module.exports = switchNode; diff --git a/test/editor/pageobjects/nodes/core/function/15-change_page.js b/test/editor/pageobjects/nodes/core/function/15-change_page.js index 8e72afe38..eb26f48aa 100644 --- a/test/editor/pageobjects/nodes/core/function/15-change_page.js +++ b/test/editor/pageobjects/nodes/core/function/15-change_page.js @@ -14,9 +14,9 @@ * limitations under the License. **/ -var util = require("util"); +var util = require('util'); -var nodePage = require("../../node_page"); +var nodePage = require('../../node_page'); function changeNode(id) { nodePage.call(this, id); @@ -51,41 +51,82 @@ function setT(t, index) { // It is better to create a function whose input value is the object type in the future, changeNode.prototype.ruleSet = function (p, pt, to, tot, index) { index = index || 1; - setT("set", index); + setT('set', index); if (pt) { browser.clickWithWait('//*[@id="node-input-rule-container"]/li[' + index + ']/div/div[1]/div/button[1]'); - var num = 5 * (index - 1) + 1; - var ptXPath = '//div[contains(@class, "red-ui-typedInput-options")][' + num + ']/a[' + ptType[pt] + ']'; - browser.clickWithWait(ptXPath); + browser.clickWithWait('//div[contains(@class, "red-ui-typedInput-options") and not(contains(@style, "display: none"))]/a[' + ptType[pt] + ']'); } if (p) { browser.setValue('//*[@id="node-input-rule-container"]/li[' + index + ']/div/div[1]/div/div/input', p); } if (tot) { browser.clickWithWait('//*[@id="node-input-rule-container"]/li[' + index + ']/div/div[2]/div[2]/button[1]'); - var num = 5 * (index - 1) + 2; - var totXPath = '//div[contains(@class, "red-ui-typedInput-options")][' + num + ']/a[' + totType[tot] + ']'; - browser.clickWithWait(totXPath); + browser.clickWithWait('//div[contains(@class, "red-ui-typedInput-options") and not(contains(@style, "display: none"))]/a[' + totType[tot] + ']'); } if (to) { browser.setValue('//*[@id="node-input-rule-container"]/li[' + index + ']/div/div[2]/div[2]/div/input', to); } } -changeNode.prototype.ruleDelete = function (index) { +changeNode.prototype.ruleChange = function (p, pt, from, fromt, to, tot, index) { index = index || 1; - setT("delete", index); + setT('change', index); + if (pt) { + browser.clickWithWait('//*[@id="node-input-rule-container"]/li[' + index + ']/div/div[1]/div/button[1]'); + browser.clickWithWait('//div[contains(@class, "red-ui-typedInput-options") and not(contains(@style, "display: none"))]/a[' + ptType[pt] + ']'); + } + if (p) { + browser.setValue('//*[@id="node-input-rule-container"]/li[' + index + ']/div/div[1]/div/div/input', p); + } + if (fromt) { + browser.clickWithWait('//*[@id="node-input-rule-container"]/li[' + index + ']/div/div[3]/div[1]/div[2]/button[1]'); + browser.clickWithWait('//div[contains(@class, "red-ui-typedInput-options") and not(contains(@style, "display: none"))]/a[' + totType[pt] + ']'); + } + if (from) { + browser.setValue('//*[@id="node-input-rule-container"]/li[' + index + ']/div/div[3]/div[1]/div[2]/div[1]/input', from); + } + if (tot) { + browser.clickWithWait('//*[@id="node-input-rule-container"]/li[' + index + ']/div/div[3]/div[2]/div[2]/button[1]'); + browser.clickWithWait('//div[contains(@class, "red-ui-typedInput-options") and not(contains(@style, "display: none"))]/a[' + totType[pt] + ']'); + } + if (to) { + browser.setValue('//*[@id="node-input-rule-container"]/li[' + index + ']/div/div[3]/div[2]/div[2]/div[1]/input', to); + } } -changeNode.prototype.ruleMove = function (p, to, index) { +changeNode.prototype.ruleDelete = function (p, pt, index) { index = index || 1; - setT("move", index); - browser.setValue('//*[@id="node-input-rule-container"]/li[' + index + ']/div/div[1]/div/div/input', p); - browser.setValue('//*[@id="node-input-rule-container"]/li[' + index + ']/div/div[4]/div[2]/div/input', to); + setT('delete', index); + if (pt) { + browser.clickWithWait('//*[@id="node-input-rule-container"]/li[' + index + ']/div/div[1]/div/button[1]'); + browser.clickWithWait('//div[contains(@class, "red-ui-typedInput-options") and not(contains(@style, "display: none"))]/a[' + ptType[pt] + ']'); + } + if (p) { + browser.setValue('//*[@id="node-input-rule-container"]/li[' + index + ']/div/div[1]/div/div/input', p); + } +} + +changeNode.prototype.ruleMove = function (p, pt, to, tot, index) { + index = index || 1; + setT('move', index); + if (pt) { + browser.clickWithWait('//*[@id="node-input-rule-container"]/li[' + index + ']/div/div[1]/div/button[1]'); + browser.clickWithWait('//div[contains(@class, "red-ui-typedInput-options") and not(contains(@style, "display: none"))]/a[' + ptType[pt] + ']'); + } + if (p) { + browser.setValue('//*[@id="node-input-rule-container"]/li[' + index + ']/div/div[1]/div/div/input', p); + } + if (tot) { + browser.clickWithWait('//*[@id="node-input-rule-container"]/li[' + index + ']/div/div[4]/div[2]/button[1]'); + browser.clickWithWait('//div[contains(@class, "red-ui-typedInput-options") and not(contains(@style, "display: none"))]/a[' + totType[pt] + ']'); + } + if (to) { + browser.setValue('//*[@id="node-input-rule-container"]/li[' + index + ']/div/div[4]/div[2]/div/input', to); + } } changeNode.prototype.addRule = function () { - browser.clickWithWait('//*[@id="dialog-form"]/div[3]/div/a'); + browser.clickWithWait('//div[contains(@class, "red-ui-editableList")]/a'); } module.exports = changeNode; diff --git a/test/editor/pageobjects/nodes/core/network/10-mqttin_page.js b/test/editor/pageobjects/nodes/core/function/89-delay_page.js similarity index 71% rename from test/editor/pageobjects/nodes/core/network/10-mqttin_page.js rename to test/editor/pageobjects/nodes/core/function/89-delay_page.js index 31b909116..3604beb67 100644 --- a/test/editor/pageobjects/nodes/core/network/10-mqttin_page.js +++ b/test/editor/pageobjects/nodes/core/function/89-delay_page.js @@ -18,18 +18,14 @@ var util = require("util"); var nodePage = require("../../node_page"); -function mqttInNode(id) { +function delayNode(id) { nodePage.call(this, id); } -util.inherits(mqttInNode, nodePage); +util.inherits(delayNode, nodePage); -mqttInNode.prototype.setTopic = function (topic) { - browser.setValue('#node-input-topic', topic); +delayNode.prototype.setTimeout = function (timeout) { + browser.setValue('//*[@id="node-input-timeout"]', timeout); } -mqttInNode.prototype.setQoS = function (qos) { - browser.selectWithWait('#node-input-qos', qos); -} - -module.exports = mqttInNode; +module.exports = delayNode; diff --git a/test/editor/pageobjects/nodes/core/function/89-trigger_page.js b/test/editor/pageobjects/nodes/core/function/89-trigger_page.js new file mode 100644 index 000000000..5d24de380 --- /dev/null +++ b/test/editor/pageobjects/nodes/core/function/89-trigger_page.js @@ -0,0 +1,83 @@ +/** + * Copyright JS Foundation and other contributors, http://js.foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + **/ + +var util = require("util"); + +var nodePage = require("../../node_page"); + +function triggerNode(id) { + nodePage.call(this, id); +} + +util.inherits(triggerNode, nodePage); + +triggerNode.prototype.setSend = function (send, sendt) { + var sendType = { + "flow": 1, + "global": 2, + "str": 3, + "num": 4, + "bool": 5, + "json": 6, + "bin": 7, + "date": 8, + "env": 9, + "pay": 10, + "nul": 11 + }; + + if (sendt) { + browser.clickWithWait('//*[@id="dialog-form"]/div[1]/div/button[1]'); + browser.clickWithWait('//div[contains(@class, "red-ui-typedInput-options") and not(contains(@style, "display: none"))]/a[' + sendType[sendt] + ']'); + } + if (send) { + browser.setValue('//*[@id="dialog-form"]/div[1]/div/div[1]/input', send); + } +} + +triggerNode.prototype.setDuration = function (duration, units) { + browser.setValue('//*[@id="node-input-duration"]', duration); + if (units) { + browser.selectWithWait('//*[@id="node-input-units"]', units); + } +} + +triggerNode.prototype.setThenSend = function (thenSend, thenSendt) { + var thenSendType = { + "flow": 1, + "global": 2, + "str": 3, + "num": 4, + "bool": 5, + "json": 6, + "bin": 7, + "date": 8, + "env": 9, + "pay": 10, + "payl": 11, + "nul": 12 + }; + + if (thenSendt) { + browser.clickWithWait('//*[@id="dialog-form"]/div[5]/div/button[1]'); + browser.clickWithWait('//div[contains(@class, "red-ui-typedInput-options") and not(contains(@style, "display: none"))]/a[' + thenSendType[thenSendt] + ']'); + } + if (thenSend) { + browser.setValue('//*[@id="dialog-form"]/div[5]/div/div[1]/input', thenSend); + } +} + +module.exports = triggerNode; diff --git a/test/editor/pageobjects/nodes/core/network/10-mqttout_page.js b/test/editor/pageobjects/nodes/core/function/90-exec_page.js similarity index 66% rename from test/editor/pageobjects/nodes/core/network/10-mqttout_page.js rename to test/editor/pageobjects/nodes/core/function/90-exec_page.js index 783d87b55..69b8b6c9a 100644 --- a/test/editor/pageobjects/nodes/core/network/10-mqttout_page.js +++ b/test/editor/pageobjects/nodes/core/function/90-exec_page.js @@ -18,18 +18,20 @@ var util = require("util"); var nodePage = require("../../node_page"); -function mqttOutNode(id) { +function execNode(id) { nodePage.call(this, id); } -util.inherits(mqttOutNode, nodePage); +util.inherits(execNode, nodePage); -mqttOutNode.prototype.setTopic = function(topic) { - browser.setValue('#node-input-topic', topic); +execNode.prototype.setCommand = function (command) { + browser.setValue('//*[@id="node-input-command"]', command); } -mqttOutNode.prototype.setRetain = function (retain) { - browser.selectWithWait('#node-input-retain', retain); +execNode.prototype.setAppend = function (checkbox) { + if (browser.isSelected('#node-input-addpay') !== checkbox) { + browser.click('#node-input-addpay'); + } } -module.exports = mqttOutNode; \ No newline at end of file +module.exports = execNode; diff --git a/test/editor/pageobjects/nodes/core/network/10-mqttconfig_page.js b/test/editor/pageobjects/nodes/core/network/10-mqtt_page.js similarity index 52% rename from test/editor/pageobjects/nodes/core/network/10-mqttconfig_page.js rename to test/editor/pageobjects/nodes/core/network/10-mqtt_page.js index c7cdc90c5..4bdd92336 100644 --- a/test/editor/pageobjects/nodes/core/network/10-mqttconfig_page.js +++ b/test/editor/pageobjects/nodes/core/network/10-mqtt_page.js @@ -14,27 +14,61 @@ * limitations under the License. **/ -function setServer(broker, port) { +var util = require("util"); + +var nodePage = require("../../node_page"); + +var mqttBrokerNode = {}; + +mqttBrokerNode.setServer = function (broker, port) { browser.setValue('#node-config-input-broker', broker); port = port ? port : 1883; browser.setValue('#node-config-input-port', port); -} +}; -function clickOk() { +mqttBrokerNode.clickOk = function () { browser.clickWithWait('#node-config-dialog-ok'); // Wait until an config dialog closes. browser.waitForVisible('#node-config-dialog-ok', 10000, true); -} +}; -function edit() { +mqttBrokerNode.edit = function () { browser.waitForVisible('#node-input-lookup-broker'); browser.click('#node-input-lookup-broker'); // Wait until a config dialog opens. browser.waitForVisible('#node-config-dialog-ok', 10000); +}; + +function mqttInNode(id) { + nodePage.call(this, id); } -module.exports = { - setServer: setServer, - clickOk: clickOk, - edit: edit +util.inherits(mqttInNode, nodePage); + +mqttInNode.prototype.setTopic = function (topic) { + browser.setValue('#node-input-topic', topic); }; + +mqttInNode.prototype.setQoS = function (qos) { + browser.selectWithWait('#node-input-qos', qos); +}; + +mqttInNode.prototype.mqttBrokerNode = mqttBrokerNode; +module.exports.mqttInNode = mqttInNode; + +function mqttOutNode(id) { + nodePage.call(this, id); +} + +util.inherits(mqttOutNode, nodePage); + +mqttOutNode.prototype.setTopic = function (topic) { + browser.setValue('#node-input-topic', topic); +}; + +mqttOutNode.prototype.setRetain = function (retain) { + browser.selectWithWait('#node-input-retain', retain); +}; + +mqttOutNode.prototype.mqttBrokerNode = mqttBrokerNode; +module.exports.mqttOutNode = mqttOutNode; diff --git a/test/editor/pageobjects/nodes/core/network/22-websocket_page.js b/test/editor/pageobjects/nodes/core/network/22-websocket_page.js new file mode 100644 index 000000000..8f7dc261e --- /dev/null +++ b/test/editor/pageobjects/nodes/core/network/22-websocket_page.js @@ -0,0 +1,93 @@ +/** + * Copyright JS Foundation and other contributors, http://js.foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + **/ + +var util = require("util"); + +var nodePage = require("../../node_page"); + +var websocketListenerNode = {}; + +websocketListenerNode.setPath = function (path) { + browser.setValue('#node-config-input-path', path); +}; + +websocketListenerNode.setSendReceive = function (wholemsg) { + browser.selectWithWait('#node-config-input-wholemsg', wholemsg); +}; + +websocketListenerNode.clickOk = function () { + browser.clickWithWait('#node-config-dialog-ok'); + // Wait until an config dialog closes. + browser.waitForVisible('#node-config-dialog-ok', 10000, true); +}; + +websocketListenerNode.edit = function () { + browser.waitForVisible('#node-input-lookup-server'); + browser.click('#node-input-lookup-server'); + // Wait until a config dialog opens. + browser.waitForVisible('#node-config-dialog-ok', 10000); +}; + +var websocketClientNode = {}; + +websocketClientNode.setPath = function (path) { + browser.setValue('#node-config-input-path', path); +}; + +websocketClientNode.setSendReceive = function (wholemsg) { + browser.selectWithWait('#node-config-input-wholemsg', wholemsg); +}; + +websocketClientNode.clickOk = function () { + browser.clickWithWait('#node-config-dialog-ok'); + // Wait until an config dialog closes. + browser.waitForVisible('#node-config-dialog-ok', 10000, true); +}; + +websocketClientNode.edit = function () { + browser.waitForVisible('#node-input-lookup-client'); + browser.click('#node-input-lookup-client'); + // Wait until a config dialog opens. + browser.waitForVisible('#node-config-dialog-ok', 10000); +}; + +function websocketInNode(id) { + nodePage.call(this, id); +} + +util.inherits(websocketInNode, nodePage); + +websocketInNode.prototype.setType = function (type) { + browser.selectWithWait('#node-input-mode', type); +}; + +websocketInNode.prototype.websocketListenerNode = websocketListenerNode; +websocketInNode.prototype.websocketClientNode = websocketClientNode; +module.exports.websocketInNode = websocketInNode; + +function websocketOutNode(id) { + nodePage.call(this, id); +} + +util.inherits(websocketOutNode, nodePage); + +websocketOutNode.prototype.setType = function (type) { + browser.selectWithWait('#node-input-mode', type); +}; + +websocketOutNode.prototype.websocketListenerNode = websocketListenerNode; +websocketOutNode.prototype.websocketClientNode = websocketClientNode; +module.exports.websocketOutNode = websocketOutNode; diff --git a/test/editor/pageobjects/nodes/core/parsers/70-CSV_page.js b/test/editor/pageobjects/nodes/core/parsers/70-CSV_page.js new file mode 100644 index 000000000..e4bc9502c --- /dev/null +++ b/test/editor/pageobjects/nodes/core/parsers/70-CSV_page.js @@ -0,0 +1,51 @@ +/** + * Copyright JS Foundation and other contributors, http://js.foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + **/ + +var util = require('util'); + +var nodePage = require('../../node_page'); + +function csvNode(id) { + nodePage.call(this, id); +} + +util.inherits(csvNode, nodePage); + +csvNode.prototype.setColumns = function (columns) { + browser.setValue('#node-input-temp', columns); +} + +csvNode.prototype.setSkipLines = function (skip) { + browser.setValue('#node-input-skip', skip); +} + +csvNode.prototype.setFirstRow4Names = function (checkbox) { + if (browser.isSelected('#node-input-hdrin') !== checkbox) { + browser.click('#node-input-hdrin'); + } +} + +csvNode.prototype.setOutput = function (output) { + browser.selectWithWait('#node-input-multi', output); +} + +csvNode.prototype.setIncludeRow = function (checkbox) { + if (browser.isSelected('#node-input-hdrout') !== checkbox) { + browser.click('#node-input-hdrout'); + } +} + +module.exports = csvNode; diff --git a/test/editor/pageobjects/nodes/core/parsers/70-HTML_page.js b/test/editor/pageobjects/nodes/core/parsers/70-HTML_page.js index 9c89bc8e2..243e4c905 100644 --- a/test/editor/pageobjects/nodes/core/parsers/70-HTML_page.js +++ b/test/editor/pageobjects/nodes/core/parsers/70-HTML_page.js @@ -14,9 +14,9 @@ * limitations under the License. **/ -var util = require("util"); +var util = require('util'); -var nodePage = require("../../node_page"); +var nodePage = require('../../node_page'); function htmlNode(id) { nodePage.call(this, id); @@ -24,7 +24,7 @@ function htmlNode(id) { util.inherits(htmlNode, nodePage); -htmlNode.prototype.setSelector = function(tag) { +htmlNode.prototype.setSelector = function (tag) { browser.setValue('#node-input-tag', tag); } diff --git a/test/editor/pageobjects/nodes/core/parsers/70-JSON_page.js b/test/editor/pageobjects/nodes/core/parsers/70-JSON_page.js index 10a7e648f..e0b31dd36 100644 --- a/test/editor/pageobjects/nodes/core/parsers/70-JSON_page.js +++ b/test/editor/pageobjects/nodes/core/parsers/70-JSON_page.js @@ -14,9 +14,9 @@ * limitations under the License. **/ -var util = require("util"); +var util = require('util'); -var nodePage = require("../../node_page"); +var nodePage = require('../../node_page'); function jsonNode(id) { nodePage.call(this, id); @@ -28,8 +28,8 @@ jsonNode.prototype.setAction = function (action) { browser.setValue('node-input-action', action); } -jsonNode.prototype.setProperty = function(property) { - browser.setValue('//*[@id="dialog-form"]/div[2]/div/div/input', property); +jsonNode.prototype.setProperty = function (property) { + browser.setValue('//*[contains(@class, "red-ui-typedInput-container")]/div[1]/input', property); } module.exports = jsonNode; diff --git a/test/editor/pageobjects/nodes/core/parsers/70-XML_page.js b/test/editor/pageobjects/nodes/core/parsers/70-XML_page.js new file mode 100644 index 000000000..696ec59cb --- /dev/null +++ b/test/editor/pageobjects/nodes/core/parsers/70-XML_page.js @@ -0,0 +1,35 @@ +/** + * Copyright JS Foundation and other contributors, http://js.foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + **/ + +var util = require('util'); + +var nodePage = require('../../node_page'); + +function xmlNode(id) { + nodePage.call(this, id); +} + +util.inherits(xmlNode, nodePage); + +xmlNode.prototype.setAction = function (action) { + browser.setValue('node-input-action', action); +} + +xmlNode.prototype.setProperty = function (property) { + browser.setValue('//*[contains(@class, "red-ui-typedInput-container")]/div[1]/input', property); +} + +module.exports = xmlNode; diff --git a/test/editor/pageobjects/nodes/core/parsers/70-YAML_page.js b/test/editor/pageobjects/nodes/core/parsers/70-YAML_page.js new file mode 100644 index 000000000..1002f3eb4 --- /dev/null +++ b/test/editor/pageobjects/nodes/core/parsers/70-YAML_page.js @@ -0,0 +1,35 @@ +/** + * Copyright JS Foundation and other contributors, http://js.foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + **/ + +var util = require('util'); + +var nodePage = require('../../node_page'); + +function yamlNode(id) { + nodePage.call(this, id); +} + +util.inherits(yamlNode, nodePage); + +yamlNode.prototype.setAction = function (action) { + browser.setValue('node-input-action', action); +} + +yamlNode.prototype.setProperty = function (property) { + browser.setValue('//*[contains(@class, "red-ui-typedInput-container")]/div[1]/input', property); +} + +module.exports = yamlNode; diff --git a/test/editor/pageobjects/nodes/core/sequence/17-split_page.js b/test/editor/pageobjects/nodes/core/sequence/17-split_page.js new file mode 100644 index 000000000..8fc32ab1e --- /dev/null +++ b/test/editor/pageobjects/nodes/core/sequence/17-split_page.js @@ -0,0 +1,47 @@ +/** + * Copyright JS Foundation and other contributors, http://js.foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + **/ + +var util = require('util'); + +var nodePage = require('../../node_page'); + +function splitNode(id) { + nodePage.call(this, id); +} + +util.inherits(splitNode, nodePage); + +splitNode.prototype.setSplitUsing = function (splt, spltt) { + var spltType = { + "str": 1, + "bin": 2, + "len": 3 + }; + + browser.clickWithWait('//*[@id="dialog-form"]/div[3]/div/button[1]'); + browser.clickWithWait('//div[contains(@class, "red-ui-typedInput-options") and not(contains(@style, "display: none"))]/a[' + spltType[spltt] + ']'); + browser.setValue('//*[@id="dialog-form"]/div[3]/div/div[1]/input', splt); +}; + +module.exports.splitNode = splitNode; + +function joinNode(id) { + nodePage.call(this, id); +} + +util.inherits(joinNode, nodePage); + +module.exports.joinNode = joinNode; diff --git a/test/editor/pageobjects/nodes/core/sequence/19-batch_page.js b/test/editor/pageobjects/nodes/core/sequence/19-batch_page.js new file mode 100644 index 000000000..0d7b13028 --- /dev/null +++ b/test/editor/pageobjects/nodes/core/sequence/19-batch_page.js @@ -0,0 +1,39 @@ +/** + * Copyright JS Foundation and other contributors, http://js.foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + **/ + +var util = require('util'); + +var nodePage = require('../../node_page'); + +function batchNode(id) { + nodePage.call(this, id); +} + +batchNode.prototype.setMode = function (mode) { + browser.selectWithWait('#node-input-mode', mode); +} + +batchNode.prototype.setCount = function (count) { + browser.setValue('#node-input-count', count); +} + +batchNode.prototype.setOverlap = function (overlap) { + browser.setValue('#node-input-overlap', overlap); +} + +util.inherits(batchNode, nodePage); + +module.exports = batchNode; diff --git a/test/editor/pageobjects/nodes/node_page.js b/test/editor/pageobjects/nodes/node_page.js index 5250250e7..03e734cab 100644 --- a/test/editor/pageobjects/nodes/node_page.js +++ b/test/editor/pageobjects/nodes/node_page.js @@ -35,10 +35,11 @@ Node.prototype.clickOk = function () { browser.pause(50); } -Node.prototype.connect = function (targetNode) { - var outputPort = this.id + '/*[@class="red-ui-flow-port-output"]'; +Node.prototype.connect = function (targetNode, port) { + port = port || 1; + var outputPort = this.id + '/*[@class="red-ui-flow-port-output"][' + port + ']'; var inputPort = targetNode.id + '/*[@class="red-ui-flow-port-input"]'; - browser.dragAndDrop(outputPort, inputPort) + browser.dragAndDrop(outputPort, inputPort); } Node.prototype.clickLeftButton = function () { diff --git a/test/editor/pageobjects/nodes/nodefactory_page.js b/test/editor/pageobjects/nodes/nodefactory_page.js index 744c1570d..008ecc625 100644 --- a/test/editor/pageobjects/nodes/nodefactory_page.js +++ b/test/editor/pageobjects/nodes/nodefactory_page.js @@ -16,37 +16,70 @@ var injectNode = require('./core/common/20-inject_page'); var debugNode = require('./core/common/21-debug_page'); +var completeNode = require('./core/common/24-complete_page'); +var catchNode = require('./core/common/25-catch_page'); +var statusNode = require('./core/common/25-status_page'); +var commentNode = require('./core/common/90-comment_page'); var functionNode = require('./core/function/10-function_page'); +var switchNode = require('./core/function/10-switch_page'); var changeNode = require('./core/function/15-change_page'); var rangeNode = require('./core/function/16-range_page'); var templateNode = require('./core/function/80-template_page'); -var mqttInNode = require('./core/network/10-mqttin_page'); -var mqttOutNode = require('./core/network/10-mqttout_page'); +var delayNode = require('./core/function/89-delay_page'); +var triggerNode = require('./core/function/89-trigger_page'); +var execNode = require('./core/function/90-exec_page'); +var mqttInNode = require('./core/network/10-mqtt_page').mqttInNode; +var mqttOutNode = require('./core/network/10-mqtt_page').mqttOutNode; var httpInNode = require('./core/network/21-httpin_page'); var httpResponseNode = require('./core/network/21-httpresponse_page'); var httpRequestNode = require('./core/network/21-httprequest_page'); +var websocketInNode = require('./core/network/22-websocket_page').websocketInNode; +var websocketOutNode = require('./core/network/22-websocket_page').websocketOutNode; +var splitNode = require('./core/sequence/17-split_page').splitNode; +var joinNode = require('./core/sequence/17-split_page').joinNode; +var batchNode = require('./core/sequence/19-batch_page'); +var csvNode = require('./core/parsers/70-CSV_page'); var htmlNode = require('./core/parsers/70-HTML_page'); var jsonNode = require('./core/parsers/70-JSON_page'); +var xmlNode = require('./core/parsers/70-XML_page'); +var yamlNode = require('./core/parsers/70-YAML_page'); var fileInNode = require('./core/storage/10-filein_page'); var nodeCatalog = { // common "inject": injectNode, "debug": debugNode, + "complete": completeNode, + "catch": catchNode, + "status": statusNode, + "comment": commentNode, // function "function": functionNode, + "switch": switchNode, "change": changeNode, "range": rangeNode, "template": templateNode, + "delay": delayNode, + "trigger": triggerNode, + "exec": execNode, // network "mqttIn": mqttInNode, "mqttOut": mqttOutNode, "httpIn": httpInNode, "httpResponse": httpResponseNode, "httpRequest": httpRequestNode, + "websocketIn": websocketInNode, + "websocketOut": websocketOutNode, + // sequence + "split": splitNode, + "join": joinNode, + "batch": batchNode, // parser + "csv": csvNode, "html": htmlNode, "json": jsonNode, + "xml": xmlNode, + "yaml": yamlNode, // storage "fileIn": fileInNode }; diff --git a/test/editor/pageobjects/util/key_page.js b/test/editor/pageobjects/util/key_page.js index 509af9e22..497a8a141 100644 --- a/test/editor/pageobjects/util/key_page.js +++ b/test/editor/pageobjects/util/key_page.js @@ -27,6 +27,12 @@ var shortCutKeyMapForMac = { }; function getShortCutKey(type) { + if (process.env.BROWSERSTACK) { + if (browser.desiredCapabilities.os === 'OS X') { + return shortCutKeyMapForMac[type]; + } + return shortCutKeyMap[type]; + } if (os.type() === 'Darwin') { return shortCutKeyMapForMac[type]; } diff --git a/test/editor/pageobjects/util/util_page.js b/test/editor/pageobjects/util/util_page.js index 02508c831..3a764eb93 100644 --- a/test/editor/pageobjects/util/util_page.js +++ b/test/editor/pageobjects/util/util_page.js @@ -70,7 +70,7 @@ function init() { var ret = repeatUntilSuccess(function(args) { return browser.selectByValue(args[0], args[1]); - }, [selector, value]); + }, [selector, value.toString()]); return ret; } catch (e) { console.trace(); diff --git a/test/editor/specs/scenario/cookbook_dataformats_uispec.js b/test/editor/specs/scenario/cookbook_dataformats_uispec.js new file mode 100644 index 000000000..052481e91 --- /dev/null +++ b/test/editor/specs/scenario/cookbook_dataformats_uispec.js @@ -0,0 +1,364 @@ +/** + * Copyright JS Foundation and other contributors, http://js.foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + **/ + +var when = require('when'); +var should = require('should'); +var fs = require('fs-extra'); + +var helper = require('../../editor_helper'); +var debugTab = require('../../pageobjects/editor/debugTab_page'); +var workspace = require('../../pageobjects/editor/workspace_page'); +var specUtil = require('../../pageobjects/util/spec_util_page'); + +var httpNodeRoot = '/api'; + +// https://cookbook.nodered.org/ +describe('cookbook', function () { + beforeEach(function () { + workspace.init(); + }); + + before(function () { + helper.startServer(); + }); + + after(function () { + helper.stopServer(); + }); + + describe('working with data formats', function () { + it('convert to/from JSON', function () { + var injectNode1 = workspace.addNode('inject'); + var jsonNode1 = workspace.addNode('json'); + var debugNode1 = workspace.addNode('debug'); + + injectNode1.edit(); + injectNode1.setPayload('str', '{"a":1}'); + injectNode1.clickOk(); + + jsonNode1.edit(); + jsonNode1.setProperty('payload'); + jsonNode1.clickOk(); + + injectNode1.connect(jsonNode1); + jsonNode1.connect(debugNode1); + + var injectNode2 = workspace.addNode('inject'); + var jsonNode2 = workspace.addNode('json'); + var debugNode2 = workspace.addNode('debug'); + + injectNode2.edit(); + injectNode2.setPayload('json', '{"a":1}'); + injectNode2.clickOk(); + + jsonNode2.edit(); + jsonNode2.setProperty('payload'); + jsonNode2.clickOk(); + + injectNode2.connect(jsonNode2); + jsonNode2.connect(debugNode2); + + workspace.deploy(); + + debugTab.open(); + injectNode1.clickLeftButton(); + debugTab.getMessage().should.eql('1'); + debugTab.clearMessage(); + injectNode2.clickLeftButton(); + debugTab.getMessage().should.eql('"{"a":1}"'); + }); + + it('convert to/from XML', function () { + var injectNode1 = workspace.addNode('inject', 0); + var templateNode1 = workspace.addNode('template', 200); + var xmlNode1 = workspace.addNode('xml', 400); + var debugNode1 = workspace.addNode('debug', 600); + + injectNode1.edit(); + injectNode1.setPayload('str', '{"a":1}'); + injectNode1.clickOk(); + + templateNode1.edit(); + templateNode1.setFormat('text'); + templateNode1.setSyntax('plain'); + templateNode1.setTemplate('' + + ' Nick' + + ' Dave' + + ' Reminder' + + ' Update the website' + + ''); + templateNode1.clickOk(); + + xmlNode1.edit(); + xmlNode1.setProperty('payload'); + xmlNode1.clickOk(); + + injectNode1.connect(templateNode1); + templateNode1.connect(xmlNode1); + xmlNode1.connect(debugNode1); + + var injectNode2 = workspace.addNode('inject'); + var xmlNode2 = workspace.addNode('xml'); + var debugNode2 = workspace.addNode('debug'); + + injectNode2.edit(); + injectNode2.setPayload('json', '{' + + ' "note": {' + + ' "$": { "priority": "high" },' + + ' "to": [ "Nick" ],' + + ' "from": [ "Dave" ],' + + ' "heading": [ "Reminder" ],' + + ' "body": [ "Update the website" ]' + + ' }' + + '}'); + injectNode2.clickOk(); + + xmlNode2.edit(); + xmlNode2.setProperty('payload'); + xmlNode2.clickOk(); + + injectNode2.connect(xmlNode2); + xmlNode2.connect(debugNode2); + + workspace.deploy(); + + debugTab.open(); + injectNode1.clickLeftButton(); + debugTab.getMessage().should.eql('object'); + debugTab.clearMessage(); + injectNode2.clickLeftButton(); + debugTab.getMessage().should.eql('"' + + '' + + 'Nick' + + 'Dave' + + 'Reminder' + + 'Update the website' + + '"'); + }); + + it('convert to/from YAML', function () { + var injectNode1 = workspace.addNode('inject', 0); + var templateNode1 = workspace.addNode('template', 200); + var yamlNode1 = workspace.addNode('yaml', 400); + var debugNode1 = workspace.addNode('debug', 600); + + injectNode1.edit(); + injectNode1.setPayload('str', '{"a":1}'); + injectNode1.clickOk(); + + templateNode1.edit(); + templateNode1.setFormat('yaml'); + templateNode1.setSyntax('plain'); + templateNode1.setTemplate('a: 1\n' + + 'b:\n' + + ' - 1\n' + + '- 2\n' + + '- 3'); + templateNode1.clickOk(); + + yamlNode1.edit(); + yamlNode1.setProperty('payload'); + yamlNode1.clickOk(); + + injectNode1.connect(templateNode1); + templateNode1.connect(yamlNode1); + yamlNode1.connect(debugNode1); + + var injectNode2 = workspace.addNode('inject'); + var yamlNode2 = workspace.addNode('yaml'); + var debugNode2 = workspace.addNode('debug'); + + injectNode2.edit(); + injectNode2.setPayload('json', '{"a":1, "b":[1,2,3]}'); + injectNode2.clickOk(); + + yamlNode2.edit(); + yamlNode2.setProperty('payload'); + yamlNode2.clickOk(); + + injectNode2.connect(yamlNode2); + yamlNode2.connect(debugNode2); + + workspace.deploy(); + + debugTab.open(); + injectNode1.clickLeftButton(); + debugTab.getMessage().should.eql([ '1', 'array[3]' ]); + debugTab.clearMessage(); + injectNode2.clickLeftButton(); + debugTab.getMessage().should.eql('"a: 1↵b:↵ - 1↵ - 2↵ - 3↵"'); + }); + + it('generate CSV output', function () { + var injectNode1 = workspace.addNode('inject', 0); + var changeNode1 = workspace.addNode('change', 200); + var csvNode1 = workspace.addNode('csv', 400); + var debugNode1 = workspace.addNode('debug', 600); + + changeNode1.edit(); + changeNode1.ruleSet('payload', 'msg', '{' + + ' "a": $floor(100*$random()),' + + ' "b": $floor(100*$random()),' + + ' "c": $floor(100*$random())' + + '}', 'jsonata'); + changeNode1.clickOk(); + + csvNode1.edit(); + csvNode1.setColumns('a,b,c'); + csvNode1.clickOk(); + + injectNode1.connect(changeNode1); + changeNode1.connect(csvNode1); + csvNode1.connect(debugNode1); + + var injectNode2 = workspace.addNode('inject', 0, 80); + var changeNode2 = workspace.addNode('change', 200, 80); + var csvNode2 = workspace.addNode('csv', 400, 80); + var debugNode2 = workspace.addNode('debug', 600, 80); + + changeNode2.edit(); + changeNode2.ruleSet('payload', 'msg', '[' + + ' {' + + ' "a": $floor(100*$random()),' + + ' "b": $floor(100*$random()),' + + ' "c": $floor(100*$random())' + + ' }, {' + + ' "a": $floor(100*$random()),' + + ' "b": $floor(100*$random()),' + + ' "c": $floor(100*$random())' + + ' }, {' + + ' "a": $floor(100*$random()),' + + ' "b": $floor(100*$random()),' + + ' "c": $floor(100*$random())' + + ' }, {' + + ' "a": $floor(100*$random()),' + + ' "b": $floor(100*$random()),' + + ' "c": $floor(100*$random())' + + ' }' + + ']', 'jsonata'); + changeNode2.clickOk(); + + csvNode2.edit(); + csvNode2.setColumns('a,b,c'); + csvNode2.setIncludeRow(true); + csvNode2.clickOk(); + + injectNode2.connect(changeNode2); + changeNode2.connect(csvNode2); + csvNode2.connect(debugNode2); + + workspace.deploy(); + + debugTab.open(); + injectNode1.clickLeftButton(); + debugTab.getMessage().should.match(/^"([1-9]?[0-9],){2}[1-9]?[0-9]↵"$/); + debugTab.clearMessage(); + injectNode2.clickLeftButton(); + debugTab.getMessage().should.match(/^"a,b,c↵(([1-9]?[0-9],){2}[1-9]?[0-9]↵){4}"$/); + }); + + it('parse CSV input', function () { + var injectNode = workspace.addNode('inject'); + var templateNode = workspace.addNode('template'); + var csvNode = workspace.addNode('csv'); + var debugNode = workspace.addNode('debug'); + + templateNode.edit(); + templateNode.setFormat('handlebars'); + templateNode.setSyntax('mustache'); + templateNode.setTemplate('# This is some random data\n' + + 'a,b,c\n' + + '80,18,2\n' + + '52,36,10\n' + + '91,18,61\n' + + '32,47,65'); + templateNode.clickOk(); + + csvNode.edit(); + csvNode.setSkipLines(1); + csvNode.setFirstRow4Names(true); + csvNode.setOutput('mult'); + csvNode.clickOk(); + + injectNode.connect(templateNode); + templateNode.connect(csvNode); + csvNode.connect(debugNode); + + workspace.deploy(); + + debugTab.open(); + injectNode.clickLeftButton(); + debugTab.getMessage().should.eql([ 'object', 'object', 'object', 'object' ]); + }); + + it('simple GET request', function () { + var injectNode = workspace.addNode('inject'); + var httpRequestNode = workspace.addNode('httpRequest'); + var htmlNode = workspace.addNode('html'); + var debugNode = workspace.addNode('debug'); + + httpRequestNode.edit(); + httpRequestNode.setMethod('GET'); + httpRequestNode.setUrl('https://nodered.org'); + httpRequestNode.clickOk(); + + htmlNode.edit(); + htmlNode.setSelector('.node-red-latest-version'); + htmlNode.clickOk(); + + injectNode.connect(httpRequestNode); + httpRequestNode.connect(htmlNode); + htmlNode.connect(debugNode); + + workspace.deploy(); + + debugTab.open(); + injectNode.clickLeftButton(); + debugTab.getMessage().should.match(/^"v[0-9]+\.[0-9]+\.[0-9]"$/); + }); + + it('split text into one message per line', function () { + var injectNode = workspace.addNode('inject'); + var templateNode = workspace.addNode('template'); + var splitNode = workspace.addNode('split'); + var changeNode = workspace.addNode('change'); + var joinNode = workspace.addNode('join'); + var debugNode = workspace.addNode('debug'); + + templateNode.edit(); + templateNode.setFormat('handlebars'); + templateNode.setSyntax('mustache'); + templateNode.setTemplate('one\ntwo\nthree\nfour\nfive'); + templateNode.clickOk(); + + changeNode.edit(); + changeNode.ruleSet('payload', 'msg', '(parts.index+1) & ": " & payload', 'jsonata'); + changeNode.clickOk(); + + injectNode.connect(templateNode); + templateNode.connect(splitNode); + splitNode.connect(changeNode); + changeNode.connect(joinNode); + joinNode.connect(debugNode); + + workspace.deploy(); + + debugTab.open(); + injectNode.clickLeftButton(); + debugTab.getMessage().should.eql('"1: one↵2: two↵3: three↵4: four↵5: five"'); + }); + }); +}); diff --git a/test/editor/specs/scenario/cookbook_errorhandling_uispec.js b/test/editor/specs/scenario/cookbook_errorhandling_uispec.js new file mode 100644 index 000000000..5285c5c0f --- /dev/null +++ b/test/editor/specs/scenario/cookbook_errorhandling_uispec.js @@ -0,0 +1,74 @@ +/** + * Copyright JS Foundation and other contributors, http://js.foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + **/ + +var when = require('when'); +var should = require('should'); +var fs = require('fs-extra'); + +var helper = require('../../editor_helper'); +var debugTab = require('../../pageobjects/editor/debugTab_page'); +var workspace = require('../../pageobjects/editor/workspace_page'); +var specUtil = require('../../pageobjects/util/spec_util_page'); + +var httpNodeRoot = '/api'; + +// https://cookbook.nodered.org/ +describe('cookbook', function () { + beforeEach(function () { + workspace.init(); + }); + + before(function () { + helper.startServer(); + }); + + after(function () { + helper.stopServer(); + }); + + describe('messages', function () { + it('trigger a flow when a node throws an error', function () { + var injectNode = workspace.addNode('inject'); + var functionNode = workspace.addNode('function'); + var catchNode = workspace.addNode('catch', 0 , 80); + var debugNode = workspace.addNode('debug'); + + functionNode.edit(); + functionNode.setFunction('node.error("an example error", msg);'); + functionNode.clickOk(); + + catchNode.edit(); + catchNode.setScope(functionNode); + catchNode.clickOk(); + + debugNode.edit(); + debugNode.setOutput('error'); + debugNode.clickOk(); + + injectNode.connect(functionNode); + catchNode.connect(debugNode); + + workspace.deploy(); + + debugTab.open(); + injectNode.clickLeftButton(); + debugTab.getMessage().should.eql(['"an example error"', 'function']); + }); + + // skip this case since the flow outputs random results. + it.skip('automatically retry an action after an error'); + }); +}); diff --git a/test/editor/specs/scenario/cookbook_flowcontrol_uispec.js b/test/editor/specs/scenario/cookbook_flowcontrol_uispec.js new file mode 100644 index 000000000..724d1c56f --- /dev/null +++ b/test/editor/specs/scenario/cookbook_flowcontrol_uispec.js @@ -0,0 +1,81 @@ +/** + * Copyright JS Foundation and other contributors, http://js.foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + **/ + +var when = require('when'); +var should = require('should'); +var fs = require('fs-extra'); + +var helper = require('../../editor_helper'); +var debugTab = require('../../pageobjects/editor/debugTab_page'); +var workspace = require('../../pageobjects/editor/workspace_page'); +var specUtil = require('../../pageobjects/util/spec_util_page'); + +var httpNodeRoot = '/api'; + +// https://cookbook.nodered.org/ +describe('cookbook', function () { + beforeEach(function () { + workspace.init(); + }); + + before(function () { + helper.startServer(); + }); + + after(function () { + helper.stopServer(); + }); + + describe('flow control', function () { + it('trigger a flow whenever Node-RED starts', function () { + var injectNode = workspace.addNode('inject'); + var debugNode = workspace.addNode('debug'); + + injectNode.edit(); + injectNode.setPayload('str', 'Started!'); + injectNode.setOnce(true); + injectNode.clickOk(); + injectNode.connect(debugNode); + + debugTab.open(); + workspace.deploy(); + debugTab.getMessage().should.eql('"Started!"'); + }); + + it('trigger a flow at regular intervals', function () { + var injectNode = workspace.addNode('inject'); + var debugNode = workspace.addNode('debug'); + + injectNode.edit(); + injectNode.setRepeat('interval'); + injectNode.setRepeatInterval(1); + injectNode.clickOk(); + injectNode.connect(debugNode); + + workspace.deploy(); + + debugTab.open(); + specUtil.pause(1000); + var t1 = Number(debugTab.getMessage(1)); + t1.should.within(1500000000000, 3000000000000); + specUtil.pause(1000); + debugTab.getMessage(2).should.within(t1 + 900, 3000000000000); + }); + + // skip this case since it needs up to one minite. + it.skip('trigger a flow at a specific time'); + }); +}); diff --git a/test/editor/specs/scenario/cookbook_endpoint_uispec.js b/test/editor/specs/scenario/cookbook_httpendpoints_uispec.js similarity index 98% rename from test/editor/specs/scenario/cookbook_endpoint_uispec.js rename to test/editor/specs/scenario/cookbook_httpendpoints_uispec.js index 9a2ed0710..134498370 100644 --- a/test/editor/specs/scenario/cookbook_endpoint_uispec.js +++ b/test/editor/specs/scenario/cookbook_httpendpoints_uispec.js @@ -23,16 +23,16 @@ var workspace = require('../../pageobjects/editor/workspace_page'); var httpNodeRoot = "/api"; // https://cookbook.nodered.org/ -describe('cookbook', function() { - beforeEach(function() { +describe('cookbook', function () { + beforeEach(function () { workspace.init(); }); - before(function() { + before(function () { helper.startServer(); }); - after(function() { + after(function () { helper.stopServer(); }); @@ -359,7 +359,7 @@ describe('cookbook', function() { debugTab.getMessage().indexOf('Text file').should.not.eql(-1); }); - it('post raw data to a flow', function() { + it('post raw data to a flow', function () { var httpInNode = workspace.addNode("httpIn"); var templateNode = workspace.addNode("template"); var httpResponseNode = workspace.addNode("httpResponse"); @@ -383,7 +383,7 @@ describe('cookbook', function() { var httpRequestNode = workspace.addNode("httpRequest"); var debugNode = workspace.addNode("debug"); - injectNode.edit() + injectNode.edit(); injectNode.setPayload("str", "Nick"); injectNode.clickOk(); @@ -427,7 +427,7 @@ describe('cookbook', function() { var httpRequestNode = workspace.addNode("httpRequest"); var debugNode = workspace.addNode("debug"); - injectNode.edit() + injectNode.edit(); injectNode.setPayload("str", "name=Nick"); injectNode.clickOk(); @@ -451,7 +451,7 @@ describe('cookbook', function() { debugTab.getMessage().indexOf('Hello Nick!').should.not.eql(-1); }); - it('post JSON data to a flow', function() { + it('post JSON data to a flow', function () { var httpInNode = workspace.addNode("httpIn"); var templateNode = workspace.addNode("template"); var httpResponseNode = workspace.addNode("httpResponse"); @@ -476,7 +476,7 @@ describe('cookbook', function() { var httpRequestNode = workspace.addNode("httpRequest"); var debugNode = workspace.addNode("debug"); - injectNode.edit() + injectNode.edit(); injectNode.setPayload("json", '{"name":"Nick"}'); injectNode.clickOk(); diff --git a/test/editor/specs/scenario/cookbook_httprequests_uispec.js b/test/editor/specs/scenario/cookbook_httprequests_uispec.js new file mode 100644 index 000000000..797b06041 --- /dev/null +++ b/test/editor/specs/scenario/cookbook_httprequests_uispec.js @@ -0,0 +1,300 @@ +/** + * Copyright JS Foundation and other contributors, http://js.foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + **/ + +var when = require('when'); +var should = require('should'); +var fs = require('fs-extra'); + +var helper = require('../../editor_helper'); +var debugTab = require('../../pageobjects/editor/debugTab_page'); +var workspace = require('../../pageobjects/editor/workspace_page'); +var specUtil = require('../../pageobjects/util/spec_util_page'); + +var httpNodeRoot = '/api'; + +// https://cookbook.nodered.org/ +describe('cookbook', function () { + beforeEach(function () { + workspace.init(); + }); + + before(function () { + helper.startServer(); + }); + + after(function () { + helper.stopServer(); + }); + + describe('HTTP requests', function () { + it('simple get request', function () { + var injectNode = workspace.addNode('inject'); + var httpRequestNode = workspace.addNode('httpRequest'); + var htmlNode = workspace.addNode('html'); + var debugNode = workspace.addNode('debug'); + + httpRequestNode.edit(); + httpRequestNode.setMethod('GET'); + httpRequestNode.setUrl(helper.url()); + httpRequestNode.clickOk(); + + htmlNode.edit(); + htmlNode.setSelector('title'); + htmlNode.clickOk(); + + injectNode.connect(httpRequestNode); + httpRequestNode.connect(htmlNode); + htmlNode.connect(debugNode); + + workspace.deploy(); + + debugTab.open(); + injectNode.clickLeftButton(); + debugTab.getMessage().should.eql('"Node-RED"'); + }); + + it('set the URL of a request', function () { + var injectNode = workspace.addNode('inject'); + var changeNode = workspace.addNode('change'); + var httpRequestNode = workspace.addNode('httpRequest'); + var debugNode = workspace.addNode('debug'); + + injectNode.edit(); + injectNode.setPayload('str', helper.url()); + injectNode.clickOk(); + + changeNode.edit(); + changeNode.ruleSet('url', 'msg', 'payload', 'msg'); + changeNode.clickOk(); + + injectNode.connect(changeNode); + changeNode.connect(httpRequestNode); + httpRequestNode.connect(debugNode); + + workspace.deploy(); + + debugTab.open(); + injectNode.clickLeftButton(); + debugTab.getMessage().should.containEql('Node-RED'); + }); + + it('set the URL of a request using a template', function () { + var injectNode = workspace.addNode('inject'); + var changeNode = workspace.addNode('change'); + var httpRequestNode = workspace.addNode('httpRequest'); + var debugNode = workspace.addNode('debug'); + + injectNode.edit(); + injectNode.setPayload('str', 'settings'); + injectNode.clickOk(); + + changeNode.edit(); + changeNode.ruleSet('query', 'msg', 'payload', 'msg'); + changeNode.clickOk(); + + httpRequestNode.edit(); + httpRequestNode.setUrl(helper.url() + '/{{{query}}}'); + httpRequestNode.clickOk(); + + injectNode.connect(changeNode); + changeNode.connect(httpRequestNode); + httpRequestNode.connect(debugNode); + + workspace.deploy(); + + debugTab.open(); + injectNode.clickLeftButton(); + debugTab.getMessage().should.containEql('httpNodeRoot'); + }); + + it('set the query string parameters', function () { + var injectNode = workspace.addNode('inject'); + var changeNode = workspace.addNode('change'); + var httpRequestNode = workspace.addNode('httpRequest'); + var debugNode = workspace.addNode('debug'); + + injectNode.edit(); + injectNode.setPayload('str', 'Nick'); + injectNode.clickOk(); + + changeNode.edit(); + changeNode.ruleSet('query', 'msg', 'payload', 'msg'); + changeNode.clickOk(); + + httpRequestNode.edit(); + httpRequestNode.setUrl(helper.url() + httpNodeRoot + '/set-query?q={{{query}}}'); + httpRequestNode.clickOk(); + + injectNode.connect(changeNode); + changeNode.connect(httpRequestNode); + httpRequestNode.connect(debugNode); + + // The code for confirmation starts from here. + var httpInNode = workspace.addNode('httpIn', 0, 200); + var templateNode = workspace.addNode('template'); + var httpResponseNode = workspace.addNode('httpResponse'); + + httpInNode.edit(); + httpInNode.setMethod('get'); + httpInNode.setUrl('/set-query'); + httpInNode.clickOk(); + + templateNode.edit(); + templateNode.setSyntax('mustache'); + templateNode.setFormat('handlebars'); + templateNode.setTemplate('Hello {{req.query.q}}'); + templateNode.clickOk(); + + httpInNode.connect(templateNode); + templateNode.connect(httpResponseNode); + // The code for confirmation ends here. + + workspace.deploy(); + debugTab.open(); + injectNode.clickLeftButton(); + debugTab.getMessage().should.eql('"Hello Nick"'); + }); + + it('get a parsed JSON response', function () { + var injectNode = workspace.addNode('inject'); + var changeNodeSetPost = workspace.addNode('change'); + var httpRequestNode = workspace.addNode('httpRequest'); + var debugNode = workspace.addNode('debug'); + + injectNode.edit(); + injectNode.setPayload('str', 'json-response'); + injectNode.clickOk(); + + changeNodeSetPost.edit(); + changeNodeSetPost.ruleSet('post', 'msg', 'payload', 'msg'); + changeNodeSetPost.clickOk(); + + httpRequestNode.edit(); + httpRequestNode.setMethod('GET'); + var url = helper.url() + httpNodeRoot + '/{{post}}'; + httpRequestNode.setUrl(url); + httpRequestNode.setReturn('obj'); + httpRequestNode.clickOk(); + + debugNode.edit(); + debugNode.setOutput('payload.title'); + debugNode.clickOk(); + + injectNode.connect(changeNodeSetPost); + changeNodeSetPost.connect(httpRequestNode); + httpRequestNode.connect(debugNode); + + // The code for confirmation starts from here. + var httpInNode = workspace.addNode('httpIn', 0, 200); + var templateNode = workspace.addNode('template'); + var changeNodeSetHeader = workspace.addNode('change'); + var httpResponseNode = workspace.addNode('httpResponse'); + + httpInNode.edit(); + httpInNode.setMethod('get'); + httpInNode.setUrl('/json-response'); + httpInNode.clickOk(); + + templateNode.edit(); + templateNode.setSyntax('mustache'); + templateNode.setFormat('handlebars'); + templateNode.setTemplate('{"title": "Hello"}'); + templateNode.clickOk(); + + changeNodeSetHeader.edit(); + changeNodeSetHeader.ruleSet('headers', 'msg', '{"content-type":"application/json"}', 'json'); + changeNodeSetHeader.clickOk(); + + httpInNode.connect(templateNode); + templateNode.connect(changeNodeSetHeader); + changeNodeSetHeader.connect(httpResponseNode); + // The code for confirmation ends here. + + workspace.deploy(); + debugTab.open(); + injectNode.clickLeftButton(); + debugTab.getMessage().should.eql('"Hello"'); + }); + + it('get a binary response', function () { + var injectNode = workspace.addNode('inject'); + var httpRequestNode = workspace.addNode('httpRequest'); + var debugNode = workspace.addNode('debug'); + + httpRequestNode.edit(); + httpRequestNode.setMethod('GET'); + httpRequestNode.setUrl(helper.url() + '/settings'); + httpRequestNode.setReturn('bin'); + httpRequestNode.clickOk(); + + injectNode.connect(httpRequestNode); + httpRequestNode.connect(debugNode); + + workspace.deploy(); + + debugTab.open(); + injectNode.clickLeftButton(); + + debugTab.getMessage().should.eql(['123', '34', '104', '116', '116', '112', '78', '111', '100', '101']); + }); + + it('set a request header', function () { + var injectNode = workspace.addNode('inject'); + var functionNode = workspace.addNode('function'); + var httpRequestNode = workspace.addNode('httpRequest'); + var debugNode = workspace.addNode('debug'); + + functionNode.edit(); + functionNode.setFunction('msg.payload = "data to post";\nreturn msg;'); + functionNode.clickOk(); + + httpRequestNode.edit(); + httpRequestNode.setMethod('POST'); + var url = helper.url() + httpNodeRoot + '/set-header'; + httpRequestNode.setUrl(url); + httpRequestNode.clickOk(); + + injectNode.connect(functionNode); + functionNode.connect(httpRequestNode); + httpRequestNode.connect(debugNode); + + // The code for confirmation starts from here. + var httpInNode = workspace.addNode('httpIn', 0, 200); + var templateNode = workspace.addNode('template'); + var httpResponseNode = workspace.addNode('httpResponse'); + + httpInNode.edit(); + httpInNode.setMethod('post'); + httpInNode.setUrl('/set-header'); + httpInNode.clickOk(); + + templateNode.edit(); + templateNode.setSyntax('mustache'); + templateNode.setFormat('handlebars'); + templateNode.setTemplate('{{ payload }}'); + templateNode.clickOk(); + + httpInNode.connect(templateNode); + templateNode.connect(httpResponseNode); + // The code for confirmation ends here. + + workspace.deploy(); + debugTab.open(); + injectNode.clickLeftButton(); + debugTab.getMessage().should.eql('"data to post"'); + }); + }); +}); diff --git a/test/editor/specs/scenario/cookbook_messages_uispec.js b/test/editor/specs/scenario/cookbook_messages_uispec.js new file mode 100644 index 000000000..78facbcda --- /dev/null +++ b/test/editor/specs/scenario/cookbook_messages_uispec.js @@ -0,0 +1,142 @@ +/** + * Copyright JS Foundation and other contributors, http://js.foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + **/ + +var when = require('when'); +var should = require('should'); +var fs = require('fs-extra'); + +var helper = require('../../editor_helper'); +var debugTab = require('../../pageobjects/editor/debugTab_page'); +var workspace = require('../../pageobjects/editor/workspace_page'); +var specUtil = require('../../pageobjects/util/spec_util_page'); + +var httpNodeRoot = '/api'; + +// https://cookbook.nodered.org/ +describe('cookbook', function () { + beforeEach(function () { + workspace.init(); + }); + + before(function () { + helper.startServer(); + }); + + after(function () { + helper.stopServer(); + }); + + describe('messages', function () { + it('set a message property to a fixed value', function () { + var injectNode = workspace.addNode('inject'); + var changeNode = workspace.addNode('change'); + var debugNode = workspace.addNode('debug'); + + changeNode.edit(); + changeNode.ruleSet('payload', 'msg', 'Hello World!'); + changeNode.clickOk(); + + injectNode.connect(changeNode); + changeNode.connect(debugNode); + + workspace.deploy(); + + debugTab.open(); + injectNode.clickLeftButton(); + debugTab.getMessage().should.eql('"Hello World!"'); + }); + + it('delete a message property', function () { + var injectNode = workspace.addNode('inject'); + var changeNode = workspace.addNode('change'); + var debugNode = workspace.addNode('debug'); + + changeNode.edit(); + changeNode.ruleDelete(); + changeNode.clickOk(); + + injectNode.connect(changeNode); + changeNode.connect(debugNode); + + workspace.deploy(); + + debugTab.open(); + injectNode.clickLeftButton(); + debugTab.getMessage().should.eql('undefined'); + }); + + it('move a message property', function () { + var injectNode = workspace.addNode('inject'); + var changeNode = workspace.addNode('change'); + var debugNode = workspace.addNode('debug'); + + injectNode.edit(); + injectNode.setTopic('Hello'); + injectNode.clickOk(); + + changeNode.edit(); + changeNode.ruleMove('topic', 'msg', 'payload', 'msg'); + changeNode.clickOk(); + + injectNode.connect(changeNode); + changeNode.connect(debugNode); + + workspace.deploy(); + + debugTab.open(); + injectNode.clickLeftButton(); + debugTab.getMessage().should.eql('"Hello"'); + }); + + it('map a property between different numeric ranges', function () { + var injectNode1 = workspace.addNode('inject'); + var injectNode2 = workspace.addNode('inject', 0, 100); + var injectNode3 = workspace.addNode('inject', 0, 200); + var rangeNode = workspace.addNode('range', 200, 100); + var debugNode = workspace.addNode('debug', 400); + + injectNode1.edit(); + injectNode1.setPayload('num', 0); + injectNode1.clickOk(); + injectNode2.edit(); + injectNode2.setPayload('num', 512); + injectNode2.clickOk(); + injectNode3.edit(); + injectNode3.setPayload('num', 1023); + injectNode3.clickOk(); + + rangeNode.edit(); + rangeNode.setAction('clamp'); + rangeNode.setRange(0, 1023, 0, 5); + rangeNode.clickOk(); + + injectNode1.connect(rangeNode); + injectNode2.connect(rangeNode); + injectNode3.connect(rangeNode); + rangeNode.connect(debugNode); + + workspace.deploy(); + + debugTab.open(); + injectNode1.clickLeftButton(); + debugTab.getMessage(1).should.eql('0'); + injectNode2.clickLeftButton(); + debugTab.getMessage(2).should.eql('2.5024437927663734'); + injectNode3.clickLeftButton(); + debugTab.getMessage(3).should.eql('5'); + }); + }); +}); diff --git a/test/editor/specs/scenario/cookbook_mqtt_uispec.js b/test/editor/specs/scenario/cookbook_mqtt_uispec.js index 655074750..b68170eb5 100644 --- a/test/editor/specs/scenario/cookbook_mqtt_uispec.js +++ b/test/editor/specs/scenario/cookbook_mqtt_uispec.js @@ -22,7 +22,6 @@ var helper = require("../../editor_helper"); var debugTab = require('../../pageobjects/editor/debugTab_page'); var workspace = require('../../pageobjects/editor/workspace_page'); var specUtil = require('../../pageobjects/util/spec_util_page'); -var mqttConfig = require('../../pageobjects/nodes/core/network/10-mqttconfig_page.js'); var httpNodeRoot = "/api"; @@ -73,9 +72,9 @@ describe('cookbook', function () { var mqttOutNode = workspace.addNode("mqttOut"); mqttOutNode.edit(); - mqttConfig.edit(); - mqttConfig.setServer("localhost", moscaSettings.port); - mqttConfig.clickOk(); + mqttOutNode.mqttBrokerNode.edit(); + mqttOutNode.mqttBrokerNode.setServer("localhost", moscaSettings.port); + mqttOutNode.mqttBrokerNode.clickOk(); mqttOutNode.clickOk(); workspace.deploy(); diff --git a/test/editor/specs/scenario/cookbook_uispec.js b/test/editor/specs/scenario/cookbook_uispec.js deleted file mode 100644 index 6e16ca8a7..000000000 --- a/test/editor/specs/scenario/cookbook_uispec.js +++ /dev/null @@ -1,441 +0,0 @@ -/** - * Copyright JS Foundation and other contributors, http://js.foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - **/ - -var when = require('when'); -var should = require("should"); -var fs = require('fs-extra'); - -var helper = require("../../editor_helper"); -var debugTab = require('../../pageobjects/editor/debugTab_page'); -var workspace = require('../../pageobjects/editor/workspace_page'); -var specUtil = require('../../pageobjects/util/spec_util_page'); - -var httpNodeRoot = "/api"; - -// https://cookbook.nodered.org/ -describe('cookbook', function() { - beforeEach(function() { - workspace.init(); - }); - - before(function() { - helper.startServer(); - }); - - after(function() { - helper.stopServer(); - }); - - describe('messages', function() { - it('set a message property to a fixed value', function() { - var injectNode = workspace.addNode("inject"); - var changeNode = workspace.addNode("change"); - var debugNode = workspace.addNode("debug"); - - changeNode.edit(); - changeNode.ruleSet("payload", "msg", "Hello World!"); - changeNode.clickOk(); - - injectNode.connect(changeNode); - changeNode.connect(debugNode); - - workspace.deploy(); - - debugTab.open(); - injectNode.clickLeftButton(); - debugTab.getMessage().should.eql('"Hello World!"'); - }); - - it('delete a message property', function() { - var injectNode = workspace.addNode("inject"); - var changeNode = workspace.addNode("change"); - var debugNode = workspace.addNode("debug"); - - changeNode.edit(); - changeNode.ruleDelete(); - changeNode.clickOk(); - - injectNode.connect(changeNode); - changeNode.connect(debugNode); - - workspace.deploy(); - - debugTab.open(); - injectNode.clickLeftButton(); - debugTab.getMessage().should.eql("undefined"); - }); - - it('move a message property', function() { - var injectNode = workspace.addNode("inject"); - var changeNode = workspace.addNode("change"); - var debugNode = workspace.addNode("debug"); - - injectNode.edit(); - injectNode.setTopic("Hello"); - injectNode.clickOk(); - - changeNode.edit(); - changeNode.ruleMove("topic", "payload"); - changeNode.clickOk(); - - injectNode.connect(changeNode); - changeNode.connect(debugNode); - - workspace.deploy(); - - debugTab.open(); - injectNode.clickLeftButton(); - debugTab.getMessage().should.eql('"Hello"'); - }); - - it('map a property between different numeric ranges', function() { - var injectNode1 = workspace.addNode("inject"); - var injectNode2 = workspace.addNode("inject", 0, 100); - var injectNode3 = workspace.addNode("inject", 0, 200); - var rangeNode = workspace.addNode("range", 200, 100); - var debugNode = workspace.addNode("debug", 400); - - injectNode1.edit(); - injectNode1.setPayload("num", 0); - injectNode1.clickOk(); - injectNode2.edit(); - injectNode2.setPayload("num", 512); - injectNode2.clickOk(); - injectNode3.edit(); - injectNode3.setPayload("num", 1023); - injectNode3.clickOk(); - - rangeNode.edit(); - rangeNode.setAction("clamp"); - rangeNode.setRange(0, 1023, 0, 5); - rangeNode.clickOk(); - - injectNode1.connect(rangeNode); - injectNode2.connect(rangeNode); - injectNode3.connect(rangeNode); - rangeNode.connect(debugNode); - - workspace.deploy(); - - debugTab.open(); - injectNode1.clickLeftButton(); - debugTab.getMessage(1).should.eql('0'); - injectNode2.clickLeftButton(); - debugTab.getMessage(2).should.eql('2.5024437927663734'); - injectNode3.clickLeftButton(); - debugTab.getMessage(3).should.eql('5'); - }); - }); - - describe('flow control', function() { - it('trigger a flow whenever Node-RED starts', function() { - var injectNode = workspace.addNode("inject"); - var debugNode = workspace.addNode("debug"); - - injectNode.edit(); - injectNode.setPayload("str", "Started!") - injectNode.setOnce(true); - injectNode.clickOk(); - injectNode.connect(debugNode); - - debugTab.open(); - workspace.deploy(); - debugTab.getMessage().should.eql('"Started!"'); - }); - - it('trigger a flow at regular intervals', function() { - var injectNode = workspace.addNode("inject"); - var debugNode = workspace.addNode("debug"); - - injectNode.edit(); - injectNode.setRepeat("interval"); - injectNode.setRepeatInterval(1); - injectNode.clickOk(); - injectNode.connect(debugNode); - - workspace.deploy(); - - debugTab.open(); - specUtil.pause(1000); - var t1 = Number(debugTab.getMessage(1)); - t1.should.within(1500000000000, 3000000000000); - specUtil.pause(1000); - debugTab.getMessage(2).should.within(t1 + 900, 3000000000000); - }); - - // skip this case since it needs up to one minite. - it.skip('trigger a flow at a specific time'); - }); - - describe('HTTP requests', function() { - it('simple get request', function() { - var injectNode = workspace.addNode("inject"); - var httpRequetNode = workspace.addNode("httpRequest"); - var htmlNode = workspace.addNode("html"); - var debugNode = workspace.addNode("debug"); - - httpRequetNode.edit(); - httpRequetNode.setMethod("GET"); - httpRequetNode.setUrl(helper.url()); - httpRequetNode.clickOk(); - - htmlNode.edit(); - htmlNode.setSelector("title"); - htmlNode.clickOk(); - - injectNode.connect(httpRequetNode); - httpRequetNode.connect(htmlNode); - htmlNode.connect(debugNode); - - workspace.deploy(); - - debugTab.open(); - injectNode.clickLeftButton(); - debugTab.getMessage().should.eql('"Node-RED"'); - }); - - it('set the URL of a request', function() { - var injectNode = workspace.addNode("inject"); - var changeNode = workspace.addNode("change"); - var httpRequetNode = workspace.addNode("httpRequest"); - var debugNode = workspace.addNode("debug"); - - injectNode.edit(); - injectNode.setPayload("str", helper.url()); - injectNode.clickOk(); - - changeNode.edit(); - changeNode.ruleSet("url", "msg", "payload", "msg"); - changeNode.clickOk(); - - injectNode.connect(changeNode); - changeNode.connect(httpRequetNode); - httpRequetNode.connect(debugNode); - - workspace.deploy(); - - debugTab.open(); - injectNode.clickLeftButton(); - debugTab.getMessage().should.containEql('Node-RED'); - }); - - it('set the URL of a request using a template', function() { - var injectNode = workspace.addNode("inject"); - var changeNode = workspace.addNode("change"); - var httpRequetNode = workspace.addNode("httpRequest"); - var debugNode = workspace.addNode("debug"); - - injectNode.edit(); - injectNode.setPayload("str", 'settings'); - injectNode.clickOk(); - - changeNode.edit(); - changeNode.ruleSet("query", "msg", "payload", "msg"); - changeNode.clickOk(); - - httpRequetNode.edit(); - httpRequetNode.setUrl(helper.url() + "/{{{query}}}"); - httpRequetNode.clickOk(); - - injectNode.connect(changeNode); - changeNode.connect(httpRequetNode); - httpRequetNode.connect(debugNode); - - workspace.deploy(); - - debugTab.open(); - injectNode.clickLeftButton(); - debugTab.getMessage().should.containEql('httpNodeRoot'); - }); - - it('set the query string parameters', function() { - var injectNode = workspace.addNode("inject"); - var changeNode = workspace.addNode("change"); - var httpRequetNode = workspace.addNode("httpRequest"); - var debugNode = workspace.addNode("debug"); - - injectNode.edit(); - injectNode.setPayload("str", 'Nick'); - injectNode.clickOk(); - - changeNode.edit(); - changeNode.ruleSet("query", "msg", "payload", "msg"); - changeNode.clickOk(); - - httpRequetNode.edit(); - httpRequetNode.setUrl(helper.url() + httpNodeRoot + '/set-query?q={{{query}}}'); - httpRequetNode.clickOk(); - - injectNode.connect(changeNode); - changeNode.connect(httpRequetNode); - httpRequetNode.connect(debugNode); - - // The code for confirmation starts from here. - var httpInNode = workspace.addNode("httpIn", 0, 200); - var templateNode = workspace.addNode("template"); - var httpResponseNode = workspace.addNode("httpResponse"); - - httpInNode.edit(); - httpInNode.setMethod("get"); - httpInNode.setUrl("/set-query"); - httpInNode.clickOk(); - - templateNode.edit(); - templateNode.setSyntax("mustache"); - templateNode.setFormat("handlebars"); - templateNode.setTemplate("Hello {{req.query.q}}"); - templateNode.clickOk(); - - httpInNode.connect(templateNode); - templateNode.connect(httpResponseNode); - // The code for confirmation ends here. - - workspace.deploy(); - debugTab.open(); - injectNode.clickLeftButton(); - debugTab.getMessage().should.eql('"Hello Nick"'); - }); - - it('get a parsed JSON response', function() { - var injectNode = workspace.addNode("inject"); - var changeNodeSetPost = workspace.addNode("change"); - var httpRequetNode = workspace.addNode("httpRequest"); - var debugNode = workspace.addNode("debug"); - - injectNode.edit(); - injectNode.setPayload("str", "json-response"); - injectNode.clickOk(); - - changeNodeSetPost.edit(); - changeNodeSetPost.ruleSet("post", "msg", "payload", "msg"); - changeNodeSetPost.clickOk(); - - httpRequetNode.edit(); - httpRequetNode.setMethod("GET"); - var url = helper.url() + httpNodeRoot + "/{{post}}"; - httpRequetNode.setUrl(url); - httpRequetNode.setReturn("obj"); - httpRequetNode.clickOk(); - - debugNode.edit(); - debugNode.setOutput(".title"); - debugNode.clickOk(); - - injectNode.connect(changeNodeSetPost); - changeNodeSetPost.connect(httpRequetNode); - httpRequetNode.connect(debugNode); - - // The code for confirmation starts from here. - var httpInNode = workspace.addNode("httpIn", 0, 200); - var templateNode = workspace.addNode("template"); - var changeNodeSetHeader = workspace.addNode("change"); - var httpResponseNode = workspace.addNode("httpResponse"); - - httpInNode.edit(); - httpInNode.setMethod("get"); - httpInNode.setUrl("/json-response"); - httpInNode.clickOk(); - - templateNode.edit(); - templateNode.setSyntax("mustache"); - templateNode.setFormat("handlebars"); - templateNode.setTemplate('{"title": "Hello"}'); - templateNode.clickOk(); - - changeNodeSetHeader.edit(); - changeNodeSetHeader.ruleSet("headers", "msg", '{"content-type":"application/json"}', "json"); - changeNodeSetHeader.clickOk(); - - httpInNode.connect(templateNode); - templateNode.connect(changeNodeSetHeader); - changeNodeSetHeader.connect(httpResponseNode); - // The code for confirmation ends here. - - workspace.deploy(); - debugTab.open(); - injectNode.clickLeftButton(); - debugTab.getMessage().should.eql('"Hello"'); - }); - - it('get a binary response', function() { - var injectNode = workspace.addNode("inject"); - var httpRequetNode = workspace.addNode("httpRequest"); - var debugNode = workspace.addNode("debug"); - - httpRequetNode.edit(); - httpRequetNode.setMethod("GET"); - httpRequetNode.setUrl(helper.url() + "/settings"); - httpRequetNode.setReturn("bin"); - httpRequetNode.clickOk(); - - injectNode.connect(httpRequetNode); - httpRequetNode.connect(debugNode); - - workspace.deploy(); - - debugTab.open(); - injectNode.clickLeftButton(); - - debugTab.getMessage().should.eql(['123', '34', '104', '116', '116', '112', '78', '111', '100', '101']); - }); - - it('set a request header', function() { - var injectNode = workspace.addNode("inject"); - var functionNode = workspace.addNode("function"); - var httpRequetNode = workspace.addNode("httpRequest"); - var debugNode = workspace.addNode("debug"); - - functionNode.edit(); - functionNode.setFunction('msg.payload = "data to post";\nreturn msg;'); - functionNode.clickOk(); - - httpRequetNode.edit(); - httpRequetNode.setMethod("POST"); - var url = helper.url() + httpNodeRoot + "/set-header"; - httpRequetNode.setUrl(url); - httpRequetNode.clickOk(); - - injectNode.connect(functionNode); - functionNode.connect(httpRequetNode); - httpRequetNode.connect(debugNode); - - // The code for confirmation starts from here. - var httpInNode = workspace.addNode("httpIn", 0, 200); - var templateNode = workspace.addNode("template"); - var httpResponseNode = workspace.addNode("httpResponse"); - - httpInNode.edit(); - httpInNode.setMethod("post"); - httpInNode.setUrl("/set-header"); - httpInNode.clickOk(); - - templateNode.edit(); - templateNode.setSyntax("mustache"); - templateNode.setFormat("handlebars"); - templateNode.setTemplate("{{ payload }}"); - templateNode.clickOk(); - - httpInNode.connect(templateNode); - templateNode.connect(httpResponseNode); - // The code for confirmation ends here. - - workspace.deploy(); - debugTab.open(); - injectNode.clickLeftButton(); - debugTab.getMessage().should.eql('"data to post"'); - }); - }); -}); diff --git a/test/editor/wdio.conf.js b/test/editor/wdio.conf.js index 7bbfcbe24..4e5a602e0 100644 --- a/test/editor/wdio.conf.js +++ b/test/editor/wdio.conf.js @@ -14,6 +14,7 @@ * limitations under the License. **/ +var browserstack = require('browserstack-local'); exports.config = { // @@ -48,27 +49,20 @@ exports.config = { // and 30 processes will get spawned. The property handles how many capabilities // from the same test should run tests. // - maxInstances: 10, + // maxInstances: 10, // // If you have trouble getting all important capabilities together, check out the // Sauce Labs platform configurator - a great tool to configure your capabilities: // https://docs.saucelabs.com/reference/platforms-configurator // - capabilities: [{ + // capabilities: [{ // maxInstances can get overwritten per capability. So if you have an in-house Selenium // grid with only 5 firefox instances available you can make sure that not more than // 5 instances get started at a time. - maxInstances: 2, + // maxInstances: 5, // - browserName: 'chrome', - 'goog:chromeOptions': { - args: process.env.NODE_RED_NON_HEADLESS - // Runs tests with opening a browser. - ? ['--disable-gpu', '--no-sandbox'] - // Runs tests without opening a browser. - : ['--headless', '--disable-gpu', 'window-size=1920,1080', '--no-sandbox'] - }, - }], + // browserName: 'firefox' + // }], // // =================== // Test Configurations @@ -103,7 +97,7 @@ exports.config = { baseUrl: 'http://localhost', // // Default timeout for all waitFor* commands. - waitforTimeout: 10000, + waitforTimeout: 20000, // // Default timeout in milliseconds for request // if Selenium Grid doesn't send response @@ -134,9 +128,7 @@ exports.config = { // Services take over a specific job you don't want to take care of. They enhance // your test setup with almost no effort. Unlike plugins, they don't add new // commands. Instead, they hook themselves up into the test process. - port: 9515, - path: '/', - services: ['chromedriver'], + //services: ['chromedriver'], // // Framework you want to run your specs with. // The following are supported: Mocha, Jasmine, and Cucumber @@ -155,7 +147,7 @@ exports.config = { // Options to be passed to Mocha. // See the full list at http://mochajs.org/ mochaOpts: { - timeout: 100000, + timeout: 1000000, ui: 'bdd' }, // @@ -171,8 +163,44 @@ exports.config = { * @param {Object} config wdio configuration object * @param {Array.} capabilities list of capabilities details */ - // onPrepare: function (config, capabilities) { - // }, + onPrepare: function (config, capabilities) { + if (process.env.BROWSERSTACK) { + return new Promise(function (resolve, reject) { + var options = { key: exports.config.key }; + var proxy = process.env.http_proxy || process.env.HTTP_PROXY; + if (proxy) { + var proxyConfigs = proxy.match(/^(https?):\/\/(([^:@\/]+):([^:@\/]+)@)?([^:@\/]+)(:([^:@\/]+))?\/?$/); + if (proxyConfigs) { + var protocol = proxyConfigs[1]; + var user = proxyConfigs[3]; + var pass = proxyConfigs[4]; + var host = proxyConfigs[5]; + var port = proxyConfigs[7]; + if (!port) { + if (protocol === 'http') { + port = 80; + } else if (protocol === 'https') { + port = 443; + } + } + if (host) { options.proxyHost = host; } + if (port) { options.proxyPort = port; } + if (user) { options.proxyUser = user; } + if (pass) { options.proxyPass = pass; } + } else { + reject('error in parsing the environment variable, http_proxy'); + } + } + exports.bs_local = new browserstack.Local(); + exports.bs_local.start(options, function (error) { + if (error) { + return reject(error); + } + resolve(); + }); + }); + } + }, /** * Gets executed just before initialising the webdriver session and test framework. It allows you * to manipulate configurations depending on the capability or spec. @@ -267,6 +295,44 @@ exports.config = { * @param {Object} config wdio configuration object * @param {Array.} capabilities list of capabilities details */ - // onComplete: function(exitCode, config, capabilities) { - // } + onComplete: function(exitCode, config, capabilities) { + if (process.env.BROWSERSTACK) { + exports.bs_local.stop(function () {}); + } + } +}; + +if (process.env.BROWSERSTACK) { + exports.config.maxInstances = 1; + if (process.env.BROWSERSTACK_USERNAME && process.env.BROWSERSTACK_ACCESS_KEY) { + exports.config.user = process.env.BROWSERSTACK_USERNAME; + exports.config.key = process.env.BROWSERSTACK_ACCESS_KEY; + } else { + console.log('You need to set the following environment variables.'); + console.log('BROWSERSTACK_USERNAME='); + console.log('BROWSERSTACK_ACCESS_KEY='); + } + exports.config.services = ['browserstack']; + var capabilities = []; + capabilities.push({ os: 'Windows', os_version: '10', browser: 'Chrome', resolution: '1920x1080', 'browserstack.local': true }); + capabilities.push({ os: 'Windows', os_version: '10', browser: 'Firefox', resolution: '1920x1080', 'browserstack.local': true }); + capabilities.push({ os: 'OS X', os_version: 'Catalina', browser: 'Chrome', resolution: '1920x1080', 'browserstack.local': true }); + capabilities.push({ os: 'OS X', os_version: 'Catalina', browser: 'Firefox', resolution: '1920x1080', 'browserstack.local': true }); + exports.config.capabilities = capabilities; +} else { + exports.config.maxInstances = 10; + exports.config.port = 9515; + exports.config.path = '/'; + exports.config.services = ['chromedriver']; + exports.config.capabilities = [{ + maxInstances: 2, + browserName: 'chrome', + 'goog:chromeOptions': { + args: process.env.NODE_RED_NON_HEADLESS + // Runs tests with opening a browser. + ? ['--disable-gpu', '--no-sandbox'] + // Runs tests without opening a browser. + : ['--headless', '--disable-gpu', 'window-size=1920,1080', '--no-sandbox'] + } + }]; } diff --git a/test/nodes/core/common/20-inject_spec.js b/test/nodes/core/common/20-inject_spec.js index ea78d0f53..ff5eb7f73 100644 --- a/test/nodes/core/common/20-inject_spec.js +++ b/test/nodes/core/common/20-inject_spec.js @@ -488,6 +488,77 @@ describe('inject node', function() { }); }); + + it('should inject multiple properties ', function (done) { + var flow = [{id: "n1", type: "inject", props: [{p:"topic", v:"t1", vt:"str"}, {p:"payload", v:"foo", vt:"str"}, {p:"x", v: 10, "vt":"num"}, {p:"y", v: "x+2", "vt":"jsonata"}], wires: [["n2"]], z: "flow"}, + {id: "n2", type: "helper"}]; + helper.load(injectNode, flow, function () { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + n2.on("input", function (msg) { + try { + msg.should.have.property("topic", "t1"); + msg.should.have.property("payload", "foo"); + msg.should.have.property("x", 10); + msg.should.have.property("y", 12); + done(); + } catch (err) { + done(err); + } + }); + n1.receive({}); + }); + }); + + it('should inject multiple properties using legacy props if needed', function (done) { + var flow = [{id: "n1", type: "inject", payload:"123", payloadType:"num", topic:"foo", props: [{p:"topic", vt:"str"}, {p:"payload"}], wires: [["n2"]], z: "flow"}, + {id: "n2", type: "helper"}]; + helper.load(injectNode, flow, function () { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + n2.on("input", function (msg) { + try { + msg.should.have.property("topic", "foo"); + msg.should.have.property("payload", 123); + done(); + } catch (err) { + done(err); + } + }); + n1.receive({}); + }); + }); + + + it('should report invalid JSONata expression', function (done) { + var flow = [{id: "n1", type: "inject", props: [{p:"topic", v:"t1", vt:"str"}, {p:"payload", v:"@", vt:"jsonata"}], wires: [["n2"]], z: "flow"}, + {id: "n2", type: "helper"}]; + helper.load(injectNode, flow, function () { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + var count = 0; + n2.on("input", function (msg) { + try { + msg.should.have.property("topic", "t1"); + msg.should.not.have.property("payload"); + count++; + if (count == 2) { + done(); + } + } catch (err) { + done(err); + } + }); + n1.on("call:error", function(err) { + count++; + if (count == 2) { + done(); + } + }); + n1.receive({}); + }); + }); + describe('post', function() { it('should inject message', function(done) { helper.load(injectNode, diff --git a/test/nodes/core/common/21-debug_spec.js b/test/nodes/core/common/21-debug_spec.js index bdff7976c..990797ef3 100644 --- a/test/nodes/core/common/21-debug_spec.js +++ b/test/nodes/core/common/21-debug_spec.js @@ -603,6 +603,30 @@ describe('debug node', function() { .post('/debug/n99/enable') .expect(404).end(done); }); + + it('should return 400 for invalid bulk disable', function(done) { + var flow = [{id:"n1", type:"debug", active: true }]; + helper.load(debugNode, flow, function() { + helper.request() + .post('/debug/disable') + .send({}) + .set('Content-type', 'application/json') + .expect(400).end(done); + }); + + }) + + it('should return success for bulk disable', function(done) { + var flow = [{id:"n1", type:"debug", active: true }]; + helper.load(debugNode, flow, function() { + helper.request() + .post('/debug/disable') + .send({nodes:['n1']}) + .set('Content-type', 'application/json') + .expect(201).end(done); + }); + + }) }); describe('get', function() { diff --git a/test/nodes/core/function/10-function_spec.js b/test/nodes/core/function/10-function_spec.js index c13dd1972..ab7e4b486 100644 --- a/test/nodes/core/function/10-function_spec.js +++ b/test/nodes/core/function/10-function_spec.js @@ -53,7 +53,6 @@ describe('function node', function() { }); }); - it('should be loaded', function(done) { var flow = [{id:"n1", type:"function", name: "function" }]; helper.load(functionNode, flow, function() { @@ -1336,6 +1335,46 @@ describe('function node', function() { }); }); + it('should execute initialization', function(done) { + var flow = [{id:"n1",type:"function",wires:[["n2"]],func:"msg.payload = global.get('X'); return msg;",initialize:"global.set('X','bar');"}, + {id:"n2", type:"helper"}]; + helper.load(functionNode, flow, function() { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + n2.on("input", function(msg) { + msg.should.have.property("payload", "bar"); + done(); + }); + n1.receive({payload: "foo"}); + }); + }); + + it('should wait completion of initialization', function(done) { + var flow = [{id:"n1",type:"function",wires:[["n2"]],func:"msg.payload = global.get('X'); return msg;",initialize:"global.set('X', '-'); return new Promise((resolve, reject) => setTimeout(() => { global.set('X','bar'); resolve(); }, 500));"}, + {id:"n2", type:"helper"}]; + helper.load(functionNode, flow, function() { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + n2.on("input", function(msg) { + msg.should.have.property("payload", "bar"); + done(); + }); + n1.receive({payload: "foo"}); + }); + }); + + it('should execute finalization', function(done) { + var flow = [{id:"n1",type:"function",wires:[],func:"return msg;",finalize:"global.set('X','bar');"}]; + helper.load(functionNode, flow, function() { + var n1 = helper.getNode("n1"); + var ctx = n1.context().global; + helper.unload().then(function () { + ctx.get('X').should.equal("bar"); + done(); + }); + }); + }); + describe('Logger', function () { it('should log an Info Message', function (done) { var flow = [{id: "n1", type: "function", wires: [["n2"]], func: "node.log('test');"}]; diff --git a/test/nodes/core/function/89-trigger_spec.js b/test/nodes/core/function/89-trigger_spec.js index 5f7bfc2d1..3377ee1f7 100644 --- a/test/nodes/core/function/89-trigger_spec.js +++ b/test/nodes/core/function/89-trigger_spec.js @@ -102,20 +102,20 @@ describe('trigger node', function() { function basicTest(type, val, rval) { it('should output 1st value when triggered ('+type+')', function(done) { var flow = [{"id":"n1", "type":"trigger", "name":"triggerNode", op1:val, op1type:type, op2:"", op2type:"null", duration:"20", wires:[["n2"]] }, - {id:"n2", type:"helper"} ]; + {id:"n2", type:"helper"} ]; process.env[val] = rval; helper.load(triggerNode, flow, function() { var n1 = helper.getNode("n1"); var n2 = helper.getNode("n2"); n2.on("input", function(msg) { try { - if (rval) { - msg.should.have.property("payload"); - should.deepEqual(msg.payload, rval); - } - else { - msg.should.have.property("payload", val); - } + if (rval) { + msg.should.have.property("payload"); + should.deepEqual(msg.payload, rval); + } + else { + msg.should.have.property("payload", val); + } delete process.env[val]; done(); } @@ -127,7 +127,7 @@ describe('trigger node', function() { it('should output 2st value when triggered ('+type+')', function(done) { var flow = [{"id":"n1", "type":"trigger", "name":"triggerNode", op1:"foo", op1type:"str", op2:val, op2type:type, duration:"20", wires:[["n2"]] }, - {id:"n2", type:"helper"} ]; + {id:"n2", type:"helper"} ]; process.env[val] = rval; helper.load(triggerNode, flow, function() { var n1 = helper.getNode("n1"); @@ -136,17 +136,17 @@ describe('trigger node', function() { n2.on("input", function(msg) { try { if (c === 0) { - msg.should.have.property("payload", "foo"); + msg.should.have.property("payload", "foo"); c++; } else { - if (rval) { - msg.should.have.property("payload"); - should.deepEqual(msg.payload, rval); - } - else { - msg.should.have.property("payload", val); - } + if (rval) { + msg.should.have.property("payload"); + should.deepEqual(msg.payload, rval); + } + else { + msg.should.have.property("payload", val); + } delete process.env[val]; done(); } @@ -378,6 +378,51 @@ describe('trigger node', function() { }); }); + it('should handle multiple other properties individually if asked to do so', function(done) { + var flow = [{"id":"n1", "type":"trigger", "name":"triggerNode", bytopic:"topic", topic:"foo", op1:"1", op2:"0", op1type:"num", op2type:"num", duration:"30", wires:[["n2"]] }, + {id:"n2", type:"helper"} ]; + helper.load(triggerNode, flow, function() { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + var c = 0; + n2.on("input", function(msg) { + try { + c += 1; + if (c === 1) { + msg.should.have.a.property("payload", 1); + msg.should.have.a.property("foo", "A"); + } + else if (c === 2) { + msg.should.have.a.property("payload", 1); + msg.should.have.a.property("foo", "B"); + } + else if (c === 3) { + msg.should.have.a.property("payload", 1); + msg.should.have.a.property("foo", "C"); + } + else if (c === 4) { + msg.should.have.a.property("payload", 0); + msg.should.have.a.property("foo", "A"); + } + else if (c === 5) { + msg.should.have.a.property("payload", 0); + msg.should.have.a.property("foo", "B"); + } + else if (c === 6) { + msg.should.have.a.property("payload", 0); + msg.should.have.a.property("foo", "C"); + done(); + } + } catch(err) { + done(err); + } + }); + n1.emit("input", {payload:1,foo:"A"}); + n1.emit("input", {payload:2,foo:"B"}); + n1.emit("input", {payload:3,foo:"C"}); + }); + }); + it('should be able to return things from flow and global context variables', function(done) { var spy = sinon.stub(RED.util, 'evaluateNodeProperty', function(arg1, arg2, arg3, arg4, arg5) { if (arg5) { arg5(null, arg1) } else { return arg1; } } @@ -408,8 +453,8 @@ describe('trigger node', function() { it('should be able to return things from persistable flow and global context variables', function (done) { var flow = [{"id": "n1", "type": "trigger", "name": "triggerNode", "op1": "#:(memory1)::foo", "op1type": "flow", - "op2": "#:(memory1)::bar", "op2type": "global", "duration": "20", "wires": [["n2"]], "z": "flow" }, - {"id": "n2", "type": "helper"}]; + "op2": "#:(memory1)::bar", "op2type": "global", "duration": "20", "wires": [["n2"]], "z": "flow" }, + {"id": "n2", "type": "helper"}]; helper.load(triggerNode, flow, function () { initContext(function () { var n1 = helper.getNode("n1"); @@ -442,11 +487,11 @@ describe('trigger node', function() { it('should be able to return things from multiple persistable global context variables', function (done) { var flow = [{"id": "n1", "z": "flow", "type": "trigger", - "duration": "20", "wires": [["n2"]], - "op1": "#:(memory1)::val", "op1type": "global", - "op2": "#:(memory2)::val", "op2type": "global" - }, - {"id": "n2", "type": "helper"}]; + "duration": "20", "wires": [["n2"]], + "op1": "#:(memory1)::val", "op1type": "global", + "op2": "#:(memory2)::val", "op2type": "global" + }, + {"id": "n2", "type": "helper"}]; helper.load(triggerNode, flow, function () { initContext(function () { var n1 = helper.getNode("n1"); @@ -481,11 +526,11 @@ describe('trigger node', function() { it('should be able to return things from multiple persistable flow context variables', function (done) { var flow = [{"id": "n1", "z": "flow", "type": "trigger", - "duration": "20", "wires": [["n2"]], - "op1": "#:(memory1)::val", "op1type": "flow", - "op2": "#:(memory2)::val", "op2type": "flow" - }, - {"id": "n2", "type": "helper"}]; + "duration": "20", "wires": [["n2"]], + "op1": "#:(memory1)::val", "op1type": "flow", + "op2": "#:(memory2)::val", "op2type": "flow" + }, + {"id": "n2", "type": "helper"}]; helper.load(triggerNode, flow, function () { initContext(function () { var n1 = helper.getNode("n1"); @@ -520,11 +565,11 @@ describe('trigger node', function() { it('should be able to return things from multiple persistable flow & global context variables', function (done) { var flow = [{"id": "n1", "z": "flow", "type": "trigger", - "duration": "20", "wires": [["n2"]], - "op1": "#:(memory1)::val", "op1type": "flow", - "op2": "#:(memory2)::val", "op2type": "global" - }, - {"id": "n2", "type": "helper"}]; + "duration": "20", "wires": [["n2"]], + "op1": "#:(memory1)::val", "op1type": "flow", + "op2": "#:(memory2)::val", "op2type": "global" + }, + {"id": "n2", "type": "helper"}]; helper.load(triggerNode, flow, function () { initContext(function () { var n1 = helper.getNode("n1"); @@ -736,10 +781,12 @@ describe('trigger node', function() { try { if (c === 0) { msg.should.have.a.property("payload", "Goodbye"); + msg.should.have.a.property("topic", "test2"); c += 1; } else { msg.should.have.a.property("payload", "World"); + msg.should.have.a.property("topic", "test3"); (Date.now() - ss).should.be.greaterThan(70); done(); } @@ -747,16 +794,51 @@ describe('trigger node', function() { catch(err) { done(err); } }); var ss = Date.now(); - n1.emit("input", {payload:"Hello"}); + n1.emit("input", {payload:"Hello", topic:"test1"}); setTimeout( function() { - n1.emit("input", {payload:"Goodbye"}); + n1.emit("input", {payload:"Goodbye", topic:"test2"}); },20); setTimeout( function() { - n1.emit("input", {payload:"World"}); + n1.emit("input", {payload:"World", topic:"test3"}); },80); }); }); + it('should be able output the 2nd payload and handle multiple topics', function(done) { + var flow = [{"id":"n1", "type":"trigger", "name":"triggerNode", extend:"false", op1type:"nul", op2type:"payl", op1:"false", op2:"true", duration:"80", bytopic:"topic", wires:[["n2"]] }, + {id:"n2", type:"helper"} ]; + helper.load(triggerNode, flow, function() { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + var c = 0; + n2.on("input", function(msg) { + try { + if (c === 0) { + msg.should.have.a.property("payload", "Goodbye1"); + msg.should.have.a.property("topic", "test1"); + c += 1; + } + else { + msg.should.have.a.property("payload", "Goodbye2"); + msg.should.have.a.property("topic", "test2"); + done(); + } + } + catch(err) { done(err); } + }); + n1.emit("input", {payload:"Hello1", topic:"test1"}); + setTimeout( function() { + n1.emit("input", {payload:"Hello2", topic:"test2"}); + },20); + setTimeout( function() { + n1.emit("input", {payload:"Goodbye2", topic:"test2"}); + },20); + setTimeout( function() { + n1.emit("input", {payload:"Goodbye1", topic:"test1"}); + },20); + }); + }); + it('should be able to apply mustache templates to payloads', function(done) { var flow = [{"id":"n1", "type":"trigger", "name":"triggerNode", op1type:"val", op2type:"val", op1:"{{payload}}", op2:"{{topic}}", duration:"50", wires:[["n2"]] }, {id:"n2", type:"helper"} ]; @@ -781,6 +863,40 @@ describe('trigger node', function() { }); }); + it('should be able to send 2nd message to a 2nd output', function(done) { + var flow = [{"id":"n1", "type":"trigger", "name":"triggerNode", op1type:"val", op2type:"val", op1:"hello", op2:"world", duration:"50", outputs:2, wires:[["n2"],["n3"]] }, + {id:"n2", type:"helper"}, {id:"n3", type:"helper"} ]; + helper.load(triggerNode, flow, function() { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + var n3 = helper.getNode("n3"); + var c = 0; + n2.on("input", function(msg) { + try { + if (c === 0) { + msg.should.have.a.property("payload", "hello"); + msg.should.have.a.property("topic", "test"); + c+=1; + } + else { done(err); } + } + catch(err) { done(err); } + }); + n3.on("input", function(msg) { + try { + if (c === 1) { + msg.should.have.a.property("payload", "world"); + msg.should.have.a.property("topic", "test"); + done(); + } + else { done(err); } + } + catch(err) { done(err); } + }); + n1.emit("input", {payload:"go",topic:"test"}); + }); + }); + it('should handle string null as null', function(done) { var flow = [{"id":"n1", "type":"trigger", "name":"triggerNode", op1type:"val", op2type:"pay", op1:"null", op2:"null", duration:"40", wires:[["n2"]] }, {id:"n2", type:"helper"} ]; diff --git a/test/nodes/core/parsers/70-CSV_spec.js b/test/nodes/core/parsers/70-CSV_spec.js index f46afc24a..10e167b6d 100644 --- a/test/nodes/core/parsers/70-CSV_spec.js +++ b/test/nodes/core/parsers/70-CSV_spec.js @@ -1,3 +1,4 @@ +/* eslint-disable no-undef */ /** * Copyright JS Foundation and other contributors, http://js.foundation * @@ -70,12 +71,13 @@ describe('CSV node', function() { it('should convert a simple csv string to a javascript object', function(done) { var flow = [ { id:"n1", type:"csv", temp:"a,b,c,d", wires:[["n2"]] }, - {id:"n2", type:"helper"} ]; + {id:"n2", type:"helper"} ]; helper.load(csvNode, flow, function() { var n1 = helper.getNode("n1"); var n2 = helper.getNode("n2"); n2.on("input", function(msg) { msg.should.have.property('payload', { a: 1, b: 2, c: 3, d: 4 }); + msg.should.have.property('columns', "a,b,c,d"); check_parts(msg, 0, 1); done(); }); @@ -86,7 +88,7 @@ describe('CSV node', function() { it('should remove quotes and whitespace from template', function(done) { var flow = [ { id:"n1", type:"csv", temp:'"a", "b" , " c "," d " ', wires:[["n2"]] }, - {id:"n2", type:"helper"} ]; + {id:"n2", type:"helper"} ]; helper.load(csvNode, flow, function() { var n1 = helper.getNode("n1"); var n2 = helper.getNode("n2"); @@ -102,12 +104,13 @@ describe('CSV node', function() { it('should create column names if no template provided', function(done) { var flow = [ { id:"n1", type:"csv", temp:'', wires:[["n2"]] }, - {id:"n2", type:"helper"} ]; + {id:"n2", type:"helper"} ]; helper.load(csvNode, flow, function() { var n1 = helper.getNode("n1"); var n2 = helper.getNode("n2"); n2.on("input", function(msg) { msg.should.have.property('payload', { col1: 1, col2: 2, col3: 3, col4: 4 }); + msg.should.have.property('columns', "col1,col2,col3,col4"); check_parts(msg, 0, 1); done(); }); @@ -118,12 +121,13 @@ describe('CSV node', function() { it('should allow dropping of fields from the template', function(done) { var flow = [ { id:"n1", type:"csv", temp:"a,,,d", wires:[["n2"]] }, - {id:"n2", type:"helper"} ]; + {id:"n2", type:"helper"} ]; helper.load(csvNode, flow, function() { var n1 = helper.getNode("n1"); var n2 = helper.getNode("n2"); n2.on("input", function(msg) { msg.should.have.property('payload', { a: 1, d: 4 }); + msg.should.have.property('columns', 'a,d'); check_parts(msg, 0, 1); done(); }); @@ -134,7 +138,7 @@ describe('CSV node', function() { it('should leave numbers starting with 0, e and + as strings (except 0.)', function(done) { var flow = [ { id:"n1", type:"csv", temp:"a,b,c,d,e,f,g", wires:[["n2"]] }, - {id:"n2", type:"helper"} ]; + {id:"n2", type:"helper"} ]; helper.load(csvNode, flow, function() { var n1 = helper.getNode("n1"); var n2 = helper.getNode("n2"); @@ -150,7 +154,7 @@ describe('CSV node', function() { it('should not parse numbers when told not to do so', function(done) { var flow = [ { id:"n1", type:"csv", temp:"a,b,c,d,e,f,g", strings:false, wires:[["n2"]] }, - {id:"n2", type:"helper"} ]; + {id:"n2", type:"helper"} ]; helper.load(csvNode, flow, function() { var n1 = helper.getNode("n1"); var n2 = helper.getNode("n2"); @@ -166,7 +170,7 @@ describe('CSV node', function() { it('should leave handle strings with scientific notation as numbers', function(done) { var flow = [ { id:"n1", type:"csv", temp:"a,b,c,d,e,f,g", wires:[["n2"]] }, - {id:"n2", type:"helper"} ]; + {id:"n2", type:"helper"} ]; helper.load(csvNode, flow, function() { var n1 = helper.getNode("n1"); var n2 = helper.getNode("n2"); @@ -181,44 +185,128 @@ describe('CSV node', function() { }); - it('should allow quotes in the input', function(done) { - var flow = [ { id:"n1", type:"csv", temp:"a,b,c,d,e,f,g", wires:[["n2"]] }, - {id:"n2", type:"helper"} ]; + it('should allow quotes in the input (but drop blank strings)', function(done) { + var flow = [ { id:"n1", type:"csv", temp:"a,b,c,d,e,f,g,h", wires:[["n2"]] }, + {id:"n2", type:"helper"} ]; helper.load(csvNode, flow, function() { var n1 = helper.getNode("n1"); var n2 = helper.getNode("n2"); n2.on("input", function(msg) { //console.log(msg); - msg.should.have.property('payload', { a: 1, b: -2, c: '+3', d: '04', e: '-05', f: 'ab"cd', g: 'with,a,comma' }); + msg.should.have.property('payload', { a:1, b:-2, c:'+3', d:'04', f:'-05', g:'ab"cd', h:'with,a,comma' }); check_parts(msg, 0, 1); done(); }); - var testString = '"1","-2","+3","04","-05","ab""cd","with,a,comma"'+String.fromCharCode(10); + var testString = '"1","-2","+3","04","","-05","ab""cd","with,a,comma"'+String.fromCharCode(10); + n1.emit("input", {payload:testString}); + }); + }); + + it('should allow blank strings in the input if selected', function(done) { + var flow = [ { id:"n1", type:"csv", temp:"a,b,c,d,e,f,g", include_empty_strings:true, wires:[["n2"]] }, + {id:"n2", type:"helper"} ]; + helper.load(csvNode, flow, function() { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + n2.on("input", function(msg) { + //console.log(msg); + msg.should.have.property('payload', { a: 1, b: '', c: '', d: '', e: '-05', f: 'ab"cd', g: 'with,a,comma' }); + //check_parts(msg, 0, 1); + done(); + }); + var testString = '"1","","","","-05","ab""cd","with,a,comma"'+String.fromCharCode(10); + n1.emit("input", {payload:testString}); + }); + }); + + it('should allow missing columns (nulls) in the input if selected', function(done) { + var flow = [ { id:"n1", type:"csv", temp:"a,b,c,d,e,f,g", include_null_values:true, wires:[["n2"]] }, + {id:"n2", type:"helper"} ]; + helper.load(csvNode, flow, function() { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + n2.on("input", function(msg) { + //console.log(msg); + msg.should.have.property('payload', { a: 1, b: null, c: '+3', d: null, e: '-05', f: 'ab"cd', g: 'with,a,comma' }); + //check_parts(msg, 0, 1); + done(); + }); + var testString = '"1",,"+3",,"-05","ab""cd","with,a,comma"'+String.fromCharCode(10); + n1.emit("input", {payload:testString}); + }); + }); + + it('should handle cr and lf in the input', function(done) { + var flow = [ { id:"n1", type:"csv", temp:"a,b,c,d,e,f,g", wires:[["n2"]] }, + {id:"n2", type:"helper"} ]; + helper.load(csvNode, flow, function() { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + n2.on("input", function(msg) { + //console.log(msg); + msg.should.have.property('payload', { a: "with a\nnew line", b: "and a\rcarriage return", c: "and why\r\nnot both"}); + check_parts(msg, 0, 1); + done(); + }); + var testString = '"with a'+String.fromCharCode(10)+'new line","and a'+String.fromCharCode(13)+'carriage return","and why\r\nnot both"'+String.fromCharCode(10); n1.emit("input", {payload:testString}); }); }); it('should recover from an odd number of quotes in the input', function(done) { var flow = [ { id:"n1", type:"csv", temp:"a,b,c,d,e,f,g", wires:[["n2"]] }, - {id:"n2", type:"helper"} ]; + {id:"n2", type:"helper"} ]; helper.load(csvNode, flow, function() { var n1 = helper.getNode("n1"); var n2 = helper.getNode("n2"); + var c = 0; n2.on("input", function(msg) { - //console.log(msg); - msg.should.have.property('payload', { a: "with,an", b: "odd,number", c: "ofquotes" }); - //msg.should.have.property('payload', { a: 1, b: -2, c: '+3', d: 4, e: -5, f: 'ab"cd', g: 'with,a,comma' }); - check_parts(msg, 0, 1); - done(); + if (c == 0) { + c = 1; + msg.should.have.property('payload', { a: "with,an", b: "odd,number", c: "ofquotes\n" }); + check_parts(msg, 0, 1); + } + else { + msg.should.have.property('payload', { a: "this is", b: "a normal", c: "line" }); + check_parts(msg, 0, 1); + done(); + } }); var testString = '"with,a"n,odd","num"ber","of"qu"ot"es"'+String.fromCharCode(10); n1.emit("input", {payload:testString}); + n1.emit("input", {payload:'"this is","a normal","line"'}); + }); + }); + + it('should recover from an odd number of quotes in the input (2)', function(done) { + var flow = [ { id:"n1", type:"csv", temp:"a,b,c,d,e,f,g", wires:[["n2"]] }, + {id:"n2", type:"helper"} ]; + helper.load(csvNode, flow, function() { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + var c = 0; + n2.on("input", function(msg) { + //console.log(msg) + if (c == 0) { + c = 1; + msg.should.have.property('payload', { a: "with,an", b: "odd,number", c: "ofquotes\nthis is,a normal,line" }); + check_parts(msg, 0, 1); + } + else { + msg.should.have.property('payload', { a: "this is", b: "another", c: "line" }); + check_parts(msg, 0, 1); + done(); + } + }); + var testString = '"with,a"n,odd","num"ber","of"qu"ot"es"'+String.fromCharCode(10)+'"this is","a normal","line"'+String.fromCharCode(10); + n1.emit("input", {payload:testString}); + n1.emit("input", {payload:'"this is","another","line"'}); }); }); it('should be able to use the first line as a template', function(done) { var flow = [ { id:"n1", type:"csv", temp:"a,b,c,d", hdrin:true, wires:[["n2"]] }, - {id:"n2", type:"helper"} ]; + {id:"n2", type:"helper"} ]; helper.load(csvNode, flow, function() { var n1 = helper.getNode("n1"); var n2 = helper.getNode("n2"); @@ -243,12 +331,13 @@ describe('CSV node', function() { it('should be able to output multiple lines as one array', function(done) { var flow = [ { id:"n1", type:"csv", temp:"a,b,c,d", multi:"yes", wires:[["n2"]] }, - {id:"n2", type:"helper"} ]; + {id:"n2", type:"helper"} ]; helper.load(csvNode, flow, function() { var n1 = helper.getNode("n1"); var n2 = helper.getNode("n2"); n2.on("input", function(msg) { msg.should.have.property('payload', [ { a: 1, b: 2, c: 3, d: 4 },{ a: 5, b: -6, c: '07', d: '+8' },{ a: 9, b: 0, c: 'a', d: 'b' },{ a: 'c', b: 'd', c: 'e', d: 'f' } ]); + msg.should.have.property('columns','a,b,c,d'); msg.should.not.have.property('parts'); done(); }); @@ -259,7 +348,7 @@ describe('CSV node', function() { it('should handle numbers in strings but not IP addresses', function(done) { var flow = [ { id:"n1", type:"csv", temp:"a,b,c,d,e", wires:[["n2"]] }, - {id:"n2", type:"helper"} ]; + {id:"n2", type:"helper"} ]; helper.load(csvNode, flow, function() { var n1 = helper.getNode("n1"); var n2 = helper.getNode("n2"); @@ -275,7 +364,7 @@ describe('CSV node', function() { it('should preserve parts property', function(done) { var flow = [ { id:"n1", type:"csv", temp:"a,b,c,d", wires:[["n2"]] }, - {id:"n2", type:"helper"} ]; + {id:"n2", type:"helper"} ]; helper.load(csvNode, flow, function() { var n1 = helper.getNode("n1"); var n2 = helper.getNode("n2"); @@ -319,7 +408,7 @@ describe('CSV node', function() { it('should skip several lines from start if requested', function(done) { var flow = [ { id:"n1", type:"csv", temp:"a,b,c,d", skip: 2, wires:[["n2"]] }, - {id:"n2", type:"helper"} ]; + {id:"n2", type:"helper"} ]; helper.load(csvNode, flow, function() { var n1 = helper.getNode("n1"); var n2 = helper.getNode("n2"); @@ -333,9 +422,9 @@ describe('CSV node', function() { }); }); - it('should skip several lines from start then use next line as a tempate', function(done) { + it('should skip several lines from start then use next line as a template', function(done) { var flow = [ { id:"n1", type:"csv", temp:"a,b,c,d", hdrin:true, skip: 2, wires:[["n2"]] }, - {id:"n2", type:"helper"} ]; + {id:"n2", type:"helper"} ]; helper.load(csvNode, flow, function() { var n1 = helper.getNode("n1"); var n2 = helper.getNode("n2"); @@ -351,7 +440,7 @@ describe('CSV node', function() { it('should skip several lines from start and correct parts', function(done) { var flow = [ { id:"n1", type:"csv", temp:"a,b,c,d", skip: 2, wires:[["n2"]] }, - {id:"n2", type:"helper"} ]; + {id:"n2", type:"helper"} ]; helper.load(csvNode, flow, function() { var n1 = helper.getNode("n1"); var n2 = helper.getNode("n2"); @@ -383,11 +472,13 @@ describe('CSV node', function() { n2.on("input", function(msg) { if (c === 0) { msg.should.have.property('payload', { w: 1, x: 2, y: 3, z: 4 }); + msg.should.have.property('columns', 'w,x,y,z'); check_parts(msg, 0, 2); c += 1; } else { msg.should.have.property('payload', { w: 5, x: 6, y: 7, z: 8 }); + msg.should.have.property('columns', 'w,x,y,z'); check_parts(msg, 1, 2); done(); } @@ -411,7 +502,7 @@ describe('CSV node', function() { it('should convert a simple object back to a csv', function(done) { var flow = [ { id:"n1", type:"csv", temp:"a,b,c,,e", wires:[["n2"]] }, - {id:"n2", type:"helper"} ]; + {id:"n2", type:"helper"} ]; helper.load(csvNode, flow, function() { var n1 = helper.getNode("n1"); var n2 = helper.getNode("n2"); @@ -429,7 +520,7 @@ describe('CSV node', function() { it('should convert a simple object back to a csv with no template', function(done) { var flow = [ { id:"n1", type:"csv", temp:" ", wires:[["n2"]] }, - {id:"n2", type:"helper"} ]; + {id:"n2", type:"helper"} ]; helper.load(csvNode, flow, function() { var n1 = helper.getNode("n1"); var n2 = helper.getNode("n2"); @@ -447,7 +538,7 @@ describe('CSV node', function() { it('should handle a template with spaces in the property names', function(done) { var flow = [ { id:"n1", type:"csv", temp:"a,b o,c p,,e", wires:[["n2"]] }, - {id:"n2", type:"helper"} ]; + {id:"n2", type:"helper"} ]; helper.load(csvNode, flow, function() { var n1 = helper.getNode("n1"); var n2 = helper.getNode("n2"); @@ -465,7 +556,7 @@ describe('CSV node', function() { it('should convert an array of objects to a multi-line csv', function(done) { var flow = [ { id:"n1", type:"csv", temp:"a,b,c,d", wires:[["n2"]] }, - {id:"n2", type:"helper"} ]; + {id:"n2", type:"helper"} ]; helper.load(csvNode, flow, function() { var n1 = helper.getNode("n1"); var n2 = helper.getNode("n2"); @@ -483,7 +574,7 @@ describe('CSV node', function() { it('should convert a simple array back to a csv', function(done) { var flow = [ { id:"n1", type:"csv", temp:"a,b,c,d", wires:[["n2"]] }, - {id:"n2", type:"helper"} ]; + {id:"n2", type:"helper"} ]; helper.load(csvNode, flow, function() { var n1 = helper.getNode("n1"); var n2 = helper.getNode("n2"); @@ -501,7 +592,7 @@ describe('CSV node', function() { it('should convert an array of arrays back to a multi-line csv', function(done) { var flow = [ { id:"n1", type:"csv", temp:"a,b,c,d", wires:[["n2"]] }, - {id:"n2", type:"helper"} ]; + {id:"n2", type:"helper"} ]; helper.load(csvNode, flow, function() { var n1 = helper.getNode("n1"); var n2 = helper.getNode("n2"); @@ -519,7 +610,7 @@ describe('CSV node', function() { it('should be able to include column names as first row', function(done) { var flow = [ { id:"n1", type:"csv", temp:"a,b,c,d", hdrout:true, ret:"\r\n", wires:[["n2"]] }, - {id:"n2", type:"helper"} ]; + {id:"n2", type:"helper"} ]; helper.load(csvNode, flow, function() { var n1 = helper.getNode("n1"); var n2 = helper.getNode("n2"); @@ -535,9 +626,36 @@ describe('CSV node', function() { }); }); + it('should be able to pass in column names', function(done) { + var flow = [ { id:"n1", type:"csv", temp:"", hdrout:"once", ret:"\r\n", wires:[["n2"]] }, + {id:"n2", type:"helper"} ]; + helper.load(csvNode, flow, function() { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + var count = 0; + n2.on("input", function(msg) { + count += 1; + try { + if (count === 1) { + msg.should.have.property('payload', 'a,,b,a\r\n4,,3,4\r\n'); + } + if (count === 3) { + msg.should.have.property('payload', '4,,3,4\r\n'); + done() + } + } + catch(e) { done(e); } + }); + var testJson = [{ d: 1, b: 3, c: 2, a: 4 }]; + n1.emit("input", {payload:testJson, columns:"a,,b,a"}); + n1.emit("input", {payload:testJson}); + n1.emit("input", {payload:testJson}); + }); + }); + it('should handle quotes and sub-properties', function(done) { var flow = [ { id:"n1", type:"csv", temp:"a,b,c,d", wires:[["n2"]] }, - {id:"n2", type:"helper"} ]; + {id:"n2", type:"helper"} ]; helper.load(csvNode, flow, function() { var n1 = helper.getNode("n1"); var n2 = helper.getNode("n2"); @@ -557,7 +675,7 @@ describe('CSV node', function() { it('should just pass through if no payload provided', function(done) { var flow = [ { id:"n1", type:"csv", temp:"a,b,c,d", wires:[["n2"]] }, - {id:"n2", type:"helper"} ]; + {id:"n2", type:"helper"} ]; helper.load(csvNode, flow, function() { var n1 = helper.getNode("n1"); var n2 = helper.getNode("n2"); @@ -577,7 +695,7 @@ describe('CSV node', function() { it('should warn if provided a number or boolean', function(done) { var flow = [ { id:"n1", type:"csv", temp:"a,b,c,d", wires:[["n2"]] }, - {id:"n2", type:"helper"} ]; + {id:"n2", type:"helper"} ]; helper.load(csvNode, flow, function() { var n1 = helper.getNode("n1"); var n2 = helper.getNode("n2"); diff --git a/test/nodes/core/sequence/17-split_spec.js b/test/nodes/core/sequence/17-split_spec.js index 3d38a9ac8..2c45f0e02 100644 --- a/test/nodes/core/sequence/17-split_spec.js +++ b/test/nodes/core/sequence/17-split_spec.js @@ -517,6 +517,49 @@ describe('JOIN node', function() { }); }); + it('should join things into an array after a count with a buffer join set', function(done) { + var flow = [{id:"n1", type:"join", wires:[["n2"]], count:3, joinerType:"bin", joiner:"" ,mode:"custom"}, + {id:"n2", type:"helper"}]; + helper.load(joinNode, flow, function() { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + n2.on("input", function(msg) { + try { + msg.should.have.property("payload"); + msg.payload.should.be.an.Array(); + msg.payload[0].should.equal(1); + msg.payload[1].should.equal(true); + //msg.payload[2].a.should.equal(1); + done(); + } + catch(e) {done(e);} + }); + n1.receive({payload:1}); + n1.receive({payload:true}); + n1.receive({payload:{a:1}}); + }); + }); + + it('should join strings into a buffer after a count', function(done) { + var flow = [{id:"n1", type:"join", wires:[["n2"]], count:2, build:"buffer", joinerType:"bin", joiner:"", mode:"custom"}, + {id:"n2", type:"helper"}]; + helper.load(joinNode, flow, function() { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + n2.on("input", function(msg) { + try { + msg.should.have.property("payload"); + msg.payload.length.should.equal(10); + msg.payload.toString().should.equal("helloworld"); + done(); + } + catch(e) {done(e);} + }); + n1.receive({payload:"hello"}); + n1.receive({payload:"world"}); + }); + }); + it('should join things into an object after a count', function(done) { var flow = [{id:"n1", type:"join", wires:[["n2"]], count:5, build:"object", mode:"custom"}, {id:"n2", type:"helper"}]; diff --git a/test/nodes/core/sequence/19-batch_spec.js b/test/nodes/core/sequence/19-batch_spec.js index 3a40ebfb5..b2e1e6de2 100644 --- a/test/nodes/core/sequence/19-batch_spec.js +++ b/test/nodes/core/sequence/19-batch_spec.js @@ -107,13 +107,18 @@ describe('BATCH node', function() { } } - function delayed_send(receiver, index, count, delay) { + function delayed_send(receiver, index, count, delay, done) { if (index < count) { setTimeout(function() { receiver.receive({payload: index}); - delayed_send(receiver, index+1, count, delay); + delayed_send(receiver, index+1, count, delay, done); }, delay); } + else if(index === count) { + if (done) { + done(); + } + } } function check_interval(flow, results, delay, done) { @@ -198,10 +203,28 @@ describe('BATCH node', function() { }); }); + it('should handle reset', function(done) { + var flow = [{id:"n1", type:"batch", name: "BatchNode", mode: "count", count: 2, overlap: 0, interval: 0, allowEmptySequence: false, topics: [], wires:[["n2"]]}, + {id:"n2", type:"helper"}]; + helper.load(batchNode, flow, function() { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + var results = [ + [0, 1], + [4, 5] + ]; + check_data(n1, n2, results, done); + n1.receive({payload:0}); + n1.receive({payload:1}); + n1.receive({payload:2}); + n1.receive({payload:3, reset: true}); + n1.receive({payload:4}); + n1.receive({payload:5}); + }); + }); }); describe('mode: interval', function() { - it('should create seq. with interval', function(done) { var flow = [{id:"n1", type:"batch", name: "BatchNode", mode: "interval", count: 0, overlap: 0, interval: 1, allowEmptySequence: false, topics: [], wires:[["n2"]]}, {id:"n2", type:"helper"}]; @@ -265,10 +288,29 @@ describe('BATCH node', function() { }); }); + it('should handle reset', function(done) { + var flow = [{id:"n1", type:"batch", name: "BatchNode", mode: "interval", count: 0, overlap: 0, interval: 1, allowEmptySequence: false, topics: [], wires:[["n2"]]}, + {id:"n2", type:"helper"}]; + helper.load(batchNode, flow, function() { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + var results = [ + [0, 1], + [4, 5] + ]; + check_data(n1, n2, results, done); + delayed_send(n1, 0, 3, 400, function () { + setTimeout(function () { + n1.receive({payload: "3", reset: true}); + delayed_send(n1, 4, 7, 400); + }, 10); + }); + }); + }); + }); describe('mode: concat', function() { - it('should concat two seq. (series)', function(done) { var flow = [{id:"n1", type:"batch", name: "BatchNode", mode: "concat", count: 0, overlap: 0, interval: 1, allowEmptySequence: false, topics: [{topic: "TA"}, {topic: "TB"}], wires:[["n2"]]}, {id:"n2", type:"helper"}]; @@ -355,6 +397,58 @@ describe('BATCH node', function() { }); }); + it('should handle reset', function(done) { + var flow = [{id:"n1", type:"batch", name: "BatchNode", mode: "concat", count: 0, overlap: 0, interval: 1, allowEmptySequence: false, topics: [{topic: "TA"}, {topic: "TB"}], wires:[["n2"]]}, + {id:"n2", type:"helper"}]; + try { + helper.load(batchNode, flow, function() { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + var results = [ + [2, 3, 0, 1] + ]; + check_data(n1, n2, results, done); + var inputs0 = [ + ["TB", 0, 0, 2], + ["TA", 1, 0, 2], + ]; + for(var data of inputs0) { + var msg = { + topic: data[0], + payload: data[1], + parts: { + id: data[0], + index: data[2], + count: data[3] + } + }; + n1.receive(msg); + } + n1.receive({payload: undefined, reset: true}); + var inputs1 = [ + ["TB", 0, 0, 2], + ["TB", 1, 1, 2], + ["TA", 2, 0, 2], + ["TA", 3, 1, 2] + ]; + for(var data of inputs1) { + var msg = { + topic: data[0], + payload: data[1], + parts: { + id: data[0], + index: data[2], + count: data[3] + } + }; + n1.receive(msg); + } + }); + } + catch (e) { + done(e); + } + }); }); }); diff --git a/test/nodes/subflow/subflow_spec.js b/test/nodes/subflow/subflow_spec.js index 39f5f6b8a..b60328296 100644 --- a/test/nodes/subflow/subflow_spec.js +++ b/test/nodes/subflow/subflow_spec.js @@ -447,117 +447,4 @@ describe('subflow', function() { }); }); - it('should access env var type of subflow instance', function(done) { - var flow = [ - {id:"t0", type:"tab", label:"", disabled:false, info:""}, - {id:"n1", x:10, y:10, z:"t0", type:"subflow:s1", - env: [ - {name: "K", type: "str", value: "V"} - ], - wires:[["n2"]]}, - {id:"n2", x:10, y:10, z:"t0", type:"helper", wires:[]}, - // Subflow - {id:"s1", type:"subflow", name:"Subflow", info:"", - in:[{ - x:10, y:10, - wires:[ {id:"s1-n1"} ] - }], - out:[{ - x:10, y:10, - wires:[ {id:"s1-n1", port:0} ] - }] - }, - {id:"s1-n1", x:10, y:10, z:"s1", type:"function", - func:"msg.V = env.get('K_type'); return msg;", - wires:[]} - ]; - helper.load(functionNode, flow, function() { - var n1 = helper.getNode("n1"); - var n2 = helper.getNode("n2"); - n2.on("input", function(msg) { - try { - msg.should.have.property("V", "str"); - done(); - } - catch (e) { - console.log(e); - done(e); - } - }); - n1.receive({payload:"foo"}); - }); - }); - - it('should access env var info of subflow instance', function(done) { - var flow = [ - {id:"t0", type:"tab", label:"", disabled:false, info:""}, - {id:"n1", x:10, y:10, z:"t0", type:"subflow:s1", - env: [ - {name: "K", type: "str", value: "V"} - ], - wires:[["n2"]]}, - {id:"n2", x:10, y:10, z:"t0", type:"helper", wires:[]}, - // Subflow - {id:"s1", type:"subflow", name:"Subflow", info:"", - in:[{ - x:10, y:10, - wires:[ {id:"s1-n1"} ] - }], - out:[{ - x:10, y:10, - wires:[ {id:"s1-n1", port:0} ] - }], - env:[ - { - name: "K", type: "str", value: "", - ui: { - hasUI: true, - icon: "icon", - labels: { - "en-US": "label" - }, - type: "input", - inputTypes: { - str: true - } - } - } - ] - }, - {id:"s1-n1", x:10, y:10, z:"s1", type:"function", - func:"msg.V = env.get('K_info'); return msg;", - wires:[]} - ]; - helper.load(functionNode, flow, function() { - var n1 = helper.getNode("n1"); - var n2 = helper.getNode("n2"); - n2.on("input", function(msg) { - try { - msg.should.have.property("V"); - var v = msg.V; - v.should.have.property("name", "K"); - v.should.have.property("value", "V"); - v.should.have.property("type", "str"); - v.should.have.property("ui"); - var ui = v.ui; - ui.should.have.property("hasUI", true); - ui.should.have.property("icon", "icon"); - ui.should.have.property("type", "input"); - ui.should.have.property("labels"); - var labels = ui.labels; - labels.should.have.property("en-US", "label"); - ui.should.have.property("inputTypes"); - var types = ui.inputTypes; - types.should.have.property("str", true); - done(); - } - catch (e) { - console.log(e); - done(e); - } - }); - n1.receive({payload:"foo"}); - }); - }); - }); diff --git a/test/unit/@node-red/editor-api/lib/admin/nodes_spec.js b/test/unit/@node-red/editor-api/lib/admin/nodes_spec.js index ef98fb677..0d373b8d0 100644 --- a/test/unit/@node-red/editor-api/lib/admin/nodes_spec.js +++ b/test/unit/@node-red/editor-api/lib/admin/nodes_spec.js @@ -227,7 +227,7 @@ describe("api/admin/nodes", function() { }); request(app) .post('/nodes') - .send({module: 'foo',version:"1.2.3"}) + .send({module: 'foo',version:"1.2.3",url:"https://example/foo-1.2.3.tgz"}) .expect(200) .end(function(err,res) { if (err) { @@ -238,6 +238,7 @@ describe("api/admin/nodes", function() { res.body.nodes[0].should.have.property("id","123"); opts.should.have.property("module","foo"); opts.should.have.property("version","1.2.3"); + opts.should.have.property("url","https://example/foo-1.2.3.tgz"); done(); }); }); @@ -256,7 +257,7 @@ describe("api/admin/nodes", function() { }); request(app) .post('/nodes') - .send({module: 'foo',version:"1.2.3"}) + .send({module: 'foo',version:"1.2.3",url:"https://example/foo-1.2.3.tgz"}) .expect(400) .end(function(err,res) { if (err) { diff --git a/test/unit/@node-red/editor-api/lib/auth/strategies_spec.js b/test/unit/@node-red/editor-api/lib/auth/strategies_spec.js index d30f6198a..848aaf99d 100644 --- a/test/unit/@node-red/editor-api/lib/auth/strategies_spec.js +++ b/test/unit/@node-red/editor-api/lib/auth/strategies_spec.js @@ -129,6 +129,61 @@ describe("api/auth/strategies", function() { }) }); + describe("Tokens Strategy", function() { + it('Succeeds if tokens user enabled custom header',function(done) { + var userTokens = sinon.stub(Users,"tokens",function(token) { + return when.resolve("tokens-"+token); + }); + var userTokenHeader = sinon.stub(Users,"tokenHeader",function(token) { + return "x-test-token"; + }); + strategies.tokensStrategy._success = strategies.tokensStrategy.success; + strategies.tokensStrategy.success = function(user) { + user.should.equal("tokens-1234"); + strategies.tokensStrategy.success = strategies.tokensStrategy._success; + delete strategies.tokensStrategy._success; + done(); + }; + strategies.tokensStrategy.authenticate({headers:{"x-test-token":"1234"}}); + }); + it('Succeeds if tokens user enabled default header',function(done) { + var userTokens = sinon.stub(Users,"tokens",function(token) { + return when.resolve("tokens-"+token); + }); + var userTokenHeader = sinon.stub(Users,"tokenHeader",function(token) { + return "authorization"; + }); + strategies.tokensStrategy._success = strategies.tokensStrategy.success; + strategies.tokensStrategy.success = function(user) { + user.should.equal("tokens-1234"); + strategies.tokensStrategy.success = strategies.tokensStrategy._success; + delete strategies.tokensStrategy._success; + done(); + }; + strategies.tokensStrategy.authenticate({headers:{"authorization":"Bearer 1234"}}); + }); + it('Fails if tokens user not enabled',function(done) { + var userTokens = sinon.stub(Users,"tokens",function() { + return when.resolve(null); + }); + var userTokenHeader = sinon.stub(Users,"tokenHeader",function(token) { + return "authorization"; + }); + strategies.tokensStrategy._fail = strategies.tokensStrategy.fail; + strategies.tokensStrategy.fail = function(err) { + err.should.equal(401); + strategies.tokensStrategy.fail = strategies.tokensStrategy._fail; + delete strategies.tokensStrategy._fail; + done(); + }; + strategies.tokensStrategy.authenticate({headers:{"authorization":"Bearer 1234"}}); + }); + afterEach(function() { + Users.tokens.restore(); + Users.tokenHeader.restore(); + }) + }); + describe("Bearer Strategy", function() { it('Rejects invalid token',function(done) { var getToken = sinon.stub(Tokens,"get",function(token) { diff --git a/test/unit/@node-red/editor-api/lib/auth/users_spec.js b/test/unit/@node-red/editor-api/lib/auth/users_spec.js index 515d23034..228163684 100644 --- a/test/unit/@node-red/editor-api/lib/auth/users_spec.js +++ b/test/unit/@node-red/editor-api/lib/auth/users_spec.js @@ -227,4 +227,47 @@ describe("api/auth/users", function() { }); }); }); + + describe('Initialised with tokens set as function',function() { + before(function() { + Users.init({ + type:"strategy", + tokens: function(token) { return("Done-"+token); } + }); + }); + after(function() { + Users.init({}); + }); + describe('#tokens',function() { + it('handles api.tokens being a function',function(done) { + Users.should.have.property('tokens').which.is.a.Function(); + (Users.tokens("1234")).should.equal("Done-1234"); + (Users.tokenHeader()).should.equal("authorization"); + done(); + }); + }); + }); + + describe('Initialised with tokens set as function and tokenHeader set as token header name',function() { + before(function() { + Users.init({ + type:"strategy", + tokens: function(token) { return("Done-"+token); }, + tokenHeader: "X-TEST-TOKEN" + }); + }); + after(function() { + Users.init({}); + }); + describe('#tokens',function() { + it('handles api.tokens being a function and api.tokenHeader being a header name',function(done) { + Users.should.have.property('tokens').which.is.a.Function(); + (Users.tokens("1234")).should.equal("Done-1234"); + Users.should.have.property('tokenHeader').which.is.a.Function(); + (Users.tokenHeader()).should.equal("x-test-token"); + done(); + }); + }); + }); + }); diff --git a/test/unit/@node-red/registry/lib/installer_spec.js b/test/unit/@node-red/registry/lib/installer_spec.js index f3bae8b3a..045a81e0d 100644 --- a/test/unit/@node-red/registry/lib/installer_spec.js +++ b/test/unit/@node-red/registry/lib/installer_spec.js @@ -121,6 +121,17 @@ describe('nodes/registry/installer', function() { done(); }); }); + it("rejects when update requested to existing version and url", function(done) { + sinon.stub(typeRegistry,"getModuleInfo", function() { + return { + version: "0.1.1" + } + }); + installer.installModule("this_wont_exist","0.1.1","https://example/foo-0.1.1.tgz").catch(function(err) { + err.code.should.be.eql('module_already_loaded'); + done(); + }); + }); it("rejects with generic error", function(done) { var res = { code: 1, @@ -201,6 +212,29 @@ describe('nodes/registry/installer', function() { done(err); }); }); + it("succeeds when url is valid node-red module", function(done) { + var nodeInfo = {nodes:{module:"foo",types:["a"]}}; + + var res = { + code: 0, + stdout:"", + stderr:"" + } + var p = Promise.resolve(res); + p.catch((err)=>{}); + initInstaller(p) + + var addModule = sinon.stub(registry,"addModule",function(md) { + return when.resolve(nodeInfo); + }); + + installer.installModule("this_wont_exist",null,"https://example/foo-0.1.1.tgz").then(function(info) { + info.should.eql(nodeInfo); + done(); + }).catch(function(err) { + done(err); + }); + }); }); describe("uninstalls module", function() { diff --git a/test/unit/@node-red/runtime/lib/api/flows_spec.js b/test/unit/@node-red/runtime/lib/api/flows_spec.js index a7c85efa2..dafbbc69b 100644 --- a/test/unit/@node-red/runtime/lib/api/flows_spec.js +++ b/test/unit/@node-red/runtime/lib/api/flows_spec.js @@ -53,7 +53,7 @@ describe("runtime-api/flows", function() { var loadFlows; var reloadError = false; beforeEach(function() { - setFlows = sinon.spy(function(flows,type) { + setFlows = sinon.spy(function(flows,credentials,type) { if (flows[0] === "error") { var err = new Error("error"); err.code = "error"; @@ -91,7 +91,19 @@ describe("runtime-api/flows", function() { result.should.eql({rev:"newRev"}); setFlows.called.should.be.true(); setFlows.lastCall.args[0].should.eql([4,5,6]); - setFlows.lastCall.args[1].should.eql("full"); + setFlows.lastCall.args[2].should.eql("full"); + done(); + }).catch(done); + }); + it("includes credentials when part of the request", function(done) { + flows.setFlows({ + flows: {flows:[4,5,6], credentials: {$:"creds"}}, + }).then(function(result) { + result.should.eql({rev:"newRev"}); + setFlows.called.should.be.true(); + setFlows.lastCall.args[0].should.eql([4,5,6]); + setFlows.lastCall.args[1].should.eql({$:"creds"}); + setFlows.lastCall.args[2].should.eql("full"); done(); }).catch(done); }); @@ -103,7 +115,7 @@ describe("runtime-api/flows", function() { result.should.eql({rev:"newRev"}); setFlows.called.should.be.true(); setFlows.lastCall.args[0].should.eql([4,5,6]); - setFlows.lastCall.args[1].should.eql("nodes"); + setFlows.lastCall.args[2].should.eql("nodes"); done(); }).catch(done); }); @@ -125,7 +137,7 @@ describe("runtime-api/flows", function() { result.should.eql({rev:"newRev"}); setFlows.called.should.be.true(); setFlows.lastCall.args[0].should.eql([4,5,6]); - setFlows.lastCall.args[1].should.eql("nodes"); + setFlows.lastCall.args[2].should.eql("nodes"); done(); }).catch(done); }); diff --git a/test/unit/@node-red/runtime/lib/nodes/context/index_spec.js b/test/unit/@node-red/runtime/lib/nodes/context/index_spec.js index 3c9030b03..6fd76a421 100644 --- a/test/unit/@node-red/runtime/lib/nodes/context/index_spec.js +++ b/test/unit/@node-red/runtime/lib/nodes/context/index_spec.js @@ -32,17 +32,20 @@ describe('context', function() { return Context.close(); }); it('stores local property',function() { + var flowContext = Context.getFlowContext("flowA") var context1 = Context.get("1","flowA"); should.not.exist(context1.get("foo")); context1.set("foo","test"); context1.get("foo").should.equal("test"); }); it('stores local property - creates parent properties',function() { + var flowContext = Context.getFlowContext("flowA") var context1 = Context.get("1","flowA"); context1.set("foo.bar","test"); context1.get("foo").should.eql({bar:"test"}); }); it('deletes local property',function() { + var flowContext = Context.getFlowContext("flowA") var context1 = Context.get("1","flowA"); context1.set("foo.abc.bar1","test1"); context1.set("foo.abc.bar2","test2"); @@ -55,12 +58,14 @@ describe('context', function() { should.not.exist(context1.get("foo")); }); it('stores flow property',function() { + var flowContext = Context.getFlowContext("flowA") var context1 = Context.get("1","flowA"); should.not.exist(context1.flow.get("foo")); context1.flow.set("foo","test"); context1.flow.get("foo").should.equal("test"); }); it('stores global property',function() { + var flowContext = Context.getFlowContext("flowA") var context1 = Context.get("1","flowA"); should.not.exist(context1.global.get("foo")); context1.global.set("foo","test"); @@ -68,6 +73,7 @@ describe('context', function() { }); it('keeps local context local', function() { + var flowContext = Context.getFlowContext("flowA") var context1 = Context.get("1","flowA"); var context2 = Context.get("2","flowA"); @@ -79,6 +85,7 @@ describe('context', function() { should.not.exist(context2.get("foo")); }); it('flow context accessible to all flow nodes', function() { + var flowContext = Context.getFlowContext("flowA") var context1 = Context.get("1","flowA"); var context2 = Context.get("2","flowA"); @@ -91,6 +98,8 @@ describe('context', function() { }); it('flow context not shared to nodes on other flows', function() { + var flowContextA = Context.getFlowContext("flowA") + var flowContextB = Context.getFlowContext("flowB") var context1 = Context.get("1","flowA"); var context2 = Context.get("2","flowB"); @@ -103,6 +112,9 @@ describe('context', function() { }); it('global context shared to all nodes', function() { + var flowContextA = Context.getFlowContext("flowA") + var flowContextB = Context.getFlowContext("flowB") + var context1 = Context.get("1","flowA"); var context2 = Context.get("2","flowB"); @@ -115,6 +127,7 @@ describe('context', function() { }); it('context.flow/global are not enumerable', function() { + var flowContextA = Context.getFlowContext("flowA") var context1 = Context.get("1","flowA"); Object.keys(context1).length.should.equal(0); Object.keys(context1.flow).length.should.equal(0); @@ -122,6 +135,7 @@ describe('context', function() { }) it('context.flow/global cannot be deleted', function() { + var flowContextA = Context.getFlowContext("flowA") var context1 = Context.get("1","flowA"); delete context1.flow; should.exist(context1.flow); @@ -130,6 +144,7 @@ describe('context', function() { }) it('deletes context',function() { + var flowContextA = Context.getFlowContext("flowA") var context = Context.get("1","flowA"); should.not.exist(context.get("foo")); context.set("foo","abc"); @@ -142,6 +157,7 @@ describe('context', function() { }); it('enumerates context keys - sync', function() { + var flowContextA = Context.getFlowContext("flowA") var context = Context.get("1","flowA"); var keys = context.keys(); @@ -160,6 +176,7 @@ describe('context', function() { }); it('enumerates context keys - async', function(done) { + var flowContextA = Context.getFlowContext("flowA") var context = Context.get("1","flowA"); var keys = context.keys(function(err,keys) { @@ -183,6 +200,7 @@ describe('context', function() { it('should enumerate only context keys when GlobalContext was given - sync', function() { Context.init({functionGlobalContext: {foo:"bar"}}); Context.load().then(function(){ + var flowContextA = Context.getFlowContext("flowA") var context = Context.get("1","flowA"); context.global.set("foo2","bar2"); var keys = context.global.keys(); @@ -195,6 +213,7 @@ describe('context', function() { it('should enumerate only context keys when GlobalContext was given - async', function(done) { Context.init({functionGlobalContext: {foo:"bar"}}); Context.load().then(function(){ + var flowContextA = Context.getFlowContext("flowA") var context = Context.get("1","flowA"); context.global.set("foo2","bar2"); context.global.keys(function(err,keys) { @@ -210,6 +229,7 @@ describe('context', function() { it('returns functionGlobalContext value if store value undefined', function() { Context.init({functionGlobalContext: {foo:"bar"}}); return Context.load().then(function(){ + var flowContextA = Context.getFlowContext("flowA") var context = Context.get("1","flowA"); var v = context.global.get('foo'); v.should.equal('bar'); @@ -219,6 +239,7 @@ describe('context', function() { it('returns functionGlobalContext sub-value if store value undefined', function() { Context.init({functionGlobalContext: {foo:{bar:123}}}); return Context.load().then(function(){ + var flowContextA = Context.getFlowContext("flowA") var context = Context.get("1","flowA"); var v = context.global.get('foo.bar'); should.equal(v,123); @@ -227,40 +248,67 @@ describe('context', function() { describe("$parent", function() { it('should get undefined for $parent without key', function() { + var flowContextA = Context.getFlowContext("flowA") + var flowContextB = Context.getFlowContext("flowB","flowA") var context0 = Context.get("0","flowA"); - var context1 = Context.get("1","flowB", context0); + var context1 = Context.get("1","flowB"); var parent = context1.get("$parent"); should.equal(parent, undefined); }); it('should get undefined for $parent of root', function() { + var flowContextA = Context.getFlowContext("flowA") + var flowContextB = Context.getFlowContext("flowB","flowA") var context0 = Context.get("0","flowA"); - var context1 = Context.get("1","flowB", context0); - var parent = context1.get("$parent.$parent.K"); + var context1 = Context.get("1","flowB"); + var parent = context1.flow.get("$parent.$parent.K"); should.equal(parent, undefined); }); - it('should get value in $parent', function() { + it('should get undefined for $parent of root - callback', function(done) { + var flowContextA = Context.getFlowContext("flowA") + var flowContextB = Context.getFlowContext("flowB","flowA") var context0 = Context.get("0","flowA"); - var context1 = Context.get("1","flowB", context0); - context0.set("K", "v"); - var v = context1.get("$parent.K"); + var context1 = Context.get("1","flowB"); + context1.flow.get("$parent.$parent.K", function(err, result) { + try { + should.equal(err, undefined); + should.equal(result, undefined); + done(); + } catch(err) { + done(err); + } + }); + + }); + + it('should get value in $parent', function() { + var flowContextA = Context.getFlowContext("flowA") + var flowContextB = Context.getFlowContext("flowB","flowA") + var context0 = Context.get("0","flowA"); + var context1 = Context.get("1","flowB"); + flowContextA.set("K", "v"); + var v = context1.flow.get("$parent.K"); should.equal(v, "v"); }); it('should set value in $parent', function() { + var flowContextA = Context.getFlowContext("flowA") + var flowContextB = Context.getFlowContext("flowB","flowA") var context0 = Context.get("0","flowA"); - var context1 = Context.get("1","flowB", context0); - context1.set("$parent.K", "v"); - var v = context0.get("K"); + var context1 = Context.get("1","flowB"); + context1.flow.set("$parent.K", "v"); + var v = flowContextA.get("K"); should.equal(v, "v"); }); it('should not contain $parent in keys', function() { + var flowContextA = Context.getFlowContext("flowA") + var flowContextB = Context.getFlowContext("flowB","flowA") var context0 = Context.get("0","flowA"); - var context1 = Context.get("1","flowB", context0); + var context1 = Context.get("1","flowB"); var parent = context1.get("$parent"); - context0.set("K0", "v0"); + flowContextA.set("K0", "v0"); context1.set("K1", "v1"); var keys = context1.keys(); keys.should.have.length(1); @@ -366,6 +414,7 @@ describe('context', function() { it('should ignore reserved storage name `_`', function(done) { Context.init({contextStorage:{_:{module:testPlugin}}}); Context.load().then(function(){ + var flowContext = Context.getFlowContext("flow") var context = Context.get("1","flow"); var cb = function(){} context.set("foo","bar","_",cb); @@ -452,6 +501,7 @@ describe('context', function() { Context.init({contextStorage:contextStorage}); var cb = function(){done("An error occurred")} Context.load().then(function(){ + var flowContext = Context.getFlowContext("flow"); var context = Context.get("1","flow"); context.set("foo","bar","test",cb); context.get("foo","test",cb); @@ -465,6 +515,7 @@ describe('context', function() { it('should store flow property to external context storage',function(done) { Context.init({contextStorage:contextStorage}); Context.load().then(function(){ + var flowContext = Context.getFlowContext("flow"); var context = Context.get("1","flow"); var cb = function(){done("An error occurred")} context.flow.set("foo","bar","test",cb); @@ -479,6 +530,7 @@ describe('context', function() { it('should store global property to external context storage',function(done) { Context.init({contextStorage:contextStorage}); Context.load().then(function(){ + var flowContext = Context.getFlowContext("flow"); var context = Context.get("1","flow"); var cb = function(){done("An error occurred")} context.global.set("foo","bar","test",cb); @@ -493,6 +545,7 @@ describe('context', function() { it('should store data to the default context when non-existent context storage was specified', function(done) { Context.init({contextStorage:contextDefaultStorage}); Context.load().then(function(){ + var flowContext = Context.getFlowContext("flow"); var context = Context.get("1","flow"); var cb = function(){done("An error occurred")} context.set("foo","bar","nonexist",cb); @@ -510,6 +563,7 @@ describe('context', function() { it('should use the default context', function(done) { Context.init({contextStorage:contextDefaultStorage}); Context.load().then(function(){ + var flowContext = Context.getFlowContext("flow"); var context = Context.get("1","flow"); var cb = function(){done("An error occurred")} context.set("foo","bar","default",cb); @@ -527,6 +581,7 @@ describe('context', function() { it('should use the alias of default context', function(done) { Context.init({contextStorage:contextDefaultStorage}); Context.load().then(function(){ + var flowContext = Context.getFlowContext("flow"); var context = Context.get("1","flow"); var cb = function(){done("An error occurred")} context.set("foo","alias",cb); @@ -541,10 +596,11 @@ describe('context', function() { done(); }).catch(done); }); - + it('should allow the store name to be provide in the key', function(done) { Context.init({contextStorage:contextDefaultStorage}); Context.load().then(function(){ + var flowContext = Context.getFlowContext("flow"); var context = Context.get("1","flow"); var cb = function(){done("An error occurred")} context.set("#:(test)::foo","bar"); @@ -561,6 +617,7 @@ describe('context', function() { it('should use default as the alias of other context', function(done) { Context.init({contextStorage:contextAlias}); Context.load().then(function(){ + var flowContext = Context.getFlowContext("flow"); var context = Context.get("1","flow"); var cb = function(){done("An error occurred")} context.set("foo","alias",cb); @@ -575,6 +632,7 @@ describe('context', function() { it('should not throw an error using undefined storage for local context', function(done) { Context.init({contextStorage:contextStorage}); Context.load().then(function(){ + var flowContext = Context.getFlowContext("flow"); var context = Context.get("1","flow"); var cb = function(){done("An error occurred")} context.get("local","nonexist",cb); @@ -584,6 +642,7 @@ describe('context', function() { it('should throw an error using undefined storage for flow context', function(done) { Context.init({contextStorage:contextStorage}); Context.load().then(function(){ + var flowContext = Context.getFlowContext("flow"); var context = Context.get("1","flow"); var cb = function(){done("An error occurred")} context.flow.get("flow","nonexist",cb); @@ -595,6 +654,7 @@ describe('context', function() { var fGC = { "foo": 456 }; Context.init({contextStorage:memoryStorage, functionGlobalContext:fGC }); Context.load().then(function() { + var flowContext = Context.getFlowContext("flow"); var context = Context.get("1","flow"); // Get foo - should be value from fGC var v = context.global.get("foo"); @@ -615,6 +675,7 @@ describe('context', function() { var fGC = { "foo": 456 }; Context.init({contextStorage:memoryStorage, functionGlobalContext:fGC }); Context.load().then(function() { + var flowContext = Context.getFlowContext("flow"); var context = Context.get("1","flow"); // Get foo - should be value from fGC context.global.get("foo", function(err, v) { @@ -647,6 +708,7 @@ describe('context', function() { it('should return multiple values if key is an array', function(done) { Context.init({contextStorage:memoryStorage}); Context.load().then(function(){ + var flowContext = Context.getFlowContext("flow"); var context = Context.get("1","flow"); context.set("foo1","bar1","memory"); context.set("foo2","bar2","memory"); @@ -667,6 +729,7 @@ describe('context', function() { var fGC = { "foo1": 456, "foo2": {"bar":789} }; Context.init({contextStorage:memoryStorage, functionGlobalContext:fGC }); Context.load().then(function(){ + var flowContext = Context.getFlowContext("flow"); var context = Context.get("1","flow"); context.global.get(["foo1","foo2.bar","foo3"], "memory", function(err,foo1,foo2,foo3){ if (err) { @@ -685,6 +748,7 @@ describe('context', function() { Context.init({contextStorage:contextStorage}); stubGet.onFirstCall().callsArgWith(2, "error2", "bar1"); Context.load().then(function(){ + var flowContext = Context.getFlowContext("flow") var context = Context.get("1","flow"); context.global.get(["foo1","foo2","foo3"], "memory", function(err,foo1,foo2,foo3){ if (err === "error2") { @@ -702,6 +766,7 @@ describe('context', function() { stubGet.onSecondCall().callsArgWith(2, null, "bar2"); stubGet.onThirdCall().callsArgWith(2, "error3"); Context.load().then(function(){ + var flowContext = Context.getFlowContext("flow"); var context = Context.get("1","flow"); context.get(["foo1","foo2","foo3"], "memory", function(err,foo1,foo2,foo3){ if (err === "error1") { @@ -716,6 +781,7 @@ describe('context', function() { it('should store multiple properties if key and value are arrays', function(done) { Context.init({contextStorage:memoryStorage}); Context.load().then(function(){ + var flowContext = Context.getFlowContext("flow"); var context = Context.get("1","flow"); context.set(["foo1","foo2","foo3"], ["bar1","bar2","bar3"], "memory", function(err){ if (err) { @@ -739,6 +805,7 @@ describe('context', function() { it('should deletes multiple properties', function(done) { Context.init({contextStorage:memoryStorage}); Context.load().then(function(){ + var flowContext = Context.getFlowContext("flow"); var context = Context.get("1","flow"); context.set(["foo1","foo2","foo3"], ["bar1","bar2","bar3"], "memory", function(err){ if (err) { @@ -777,6 +844,7 @@ describe('context', function() { it('should use null for missing values if the value array is shorter than the key array', function(done) { Context.init({contextStorage:memoryStorage}); Context.load().then(function(){ + var flowContext = Context.getFlowContext("flow"); var context = Context.get("1","flow"); context.set(["foo1","foo2","foo3"], ["bar1","bar2"], "memory", function(err){ if (err) { @@ -804,6 +872,7 @@ describe('context', function() { it('should use null for missing values if the value is not array', function(done) { Context.init({contextStorage:memoryStorage}); Context.load().then(function(){ + var flowContext = Context.getFlowContext("flow"); var context = Context.get("1","flow"); context.set(["foo1","foo2","foo3"], "bar1", "memory", function(err){ if (err) { @@ -831,6 +900,7 @@ describe('context', function() { it('should ignore the extra values if the value array is longer than the key array', function(done) { Context.init({contextStorage:memoryStorage}); Context.load().then(function(){ + var flowContext = Context.getFlowContext("flow"); var context = Context.get("1","flow"); context.set(["foo1","foo2","foo3"], ["bar1","bar2","bar3","ignored"], "memory", function(err){ if (err) { @@ -859,6 +929,7 @@ describe('context', function() { Context.init({contextStorage:contextStorage}); stubSet.onFirstCall().callsArgWith(3, "error2"); Context.load().then(function(){ + var flowContext = Context.getFlowContext("flow"); var context = Context.get("1","flow"); context.set(["foo1","foo2","foo3"], ["bar1","bar2","bar3"], "memory", function(err){ if (err === "error2") { @@ -873,6 +944,7 @@ describe('context', function() { it('should throw an error if callback of context.get is not a function', function (done) { Context.init({ contextStorage: memoryStorage }); Context.load().then(function () { + var flowContext = Context.getFlowContext("flow"); var context = Context.get("1", "flow"); context.get("foo", "memory", "callback"); done("should throw an error."); @@ -884,6 +956,7 @@ describe('context', function() { it('should not throw an error if callback of context.get is not specified', function (done) { Context.init({ contextStorage: memoryStorage }); Context.load().then(function () { + var flowContext = Context.getFlowContext("flow"); var context = Context.get("1", "flow"); context.get("foo", "memory"); done(); @@ -893,6 +966,7 @@ describe('context', function() { it('should throw an error if callback of context.set is not a function', function (done) { Context.init({ contextStorage: memoryStorage }); Context.load().then(function () { + var flowContext = Context.getFlowContext("flow"); var context = Context.get("1", "flow"); context.set("foo", "bar", "memory", "callback"); done("should throw an error."); @@ -904,6 +978,7 @@ describe('context', function() { it('should not throw an error if callback of context.set is not specified', function (done) { Context.init({ contextStorage: memoryStorage }); Context.load().then(function () { + var flowContext = Context.getFlowContext("flow"); var context = Context.get("1", "flow"); context.set("foo", "bar", "memory"); done(); @@ -913,6 +988,7 @@ describe('context', function() { it('should throw an error if callback of context.keys is not a function', function (done) { Context.init({ contextStorage: memoryStorage }); Context.load().then(function () { + var flowContext = Context.getFlowContext("flow"); var context = Context.get("1", "flow"); context.keys("memory", "callback"); done("should throw an error."); @@ -924,6 +1000,7 @@ describe('context', function() { it('should not throw an error if callback of context.keys is not specified', function (done) { Context.init({ contextStorage: memoryStorage }); Context.load().then(function () { + var flowContext = Context.getFlowContext("flow"); var context = Context.get("1", "flow"); context.keys("memory"); done(); @@ -953,7 +1030,6 @@ describe('context', function() { }).catch(done); }); }); - describe('delete context',function(){ it('should not call delete() when external context storage is used', function(done) { Context.init({contextStorage:contextDefaultStorage}); diff --git a/test/unit/@node-red/runtime/lib/nodes/flows/index_spec.js b/test/unit/@node-red/runtime/lib/nodes/flows/index_spec.js index a37d88f78..8865c5d3f 100644 --- a/test/unit/@node-red/runtime/lib/nodes/flows/index_spec.js +++ b/test/unit/@node-red/runtime/lib/nodes/flows/index_spec.js @@ -67,7 +67,10 @@ describe('flows/index', function() { }); return when.resolve(); }); - credentialsLoad = sinon.stub(credentials,"load",function() { + credentialsLoad = sinon.stub(credentials,"load",function(creds) { + if (creds && creds.hasOwnProperty("$") && creds['$'] === "fail") { + return when.reject("creds error"); + } return when.resolve(); }); flowCreate = sinon.stub(Flow,"create",function(parent, global, flow) { @@ -177,6 +180,23 @@ describe('flows/index', function() { }); }); + it('sets the full flow including credentials', function(done) { + var originalConfig = [ + {id:"t1-1",x:10,y:10,z:"t1",type:"test",wires:[]}, + {id:"t1",type:"tab"} + ]; + var credentials = {"t1-1":{"a":1}}; + + flows.init({log:mockLog, settings:{},storage:storage}); + flows.setFlows(originalConfig,credentials).then(function() { + credentialsClean.called.should.be.false(); + credentialsLoad.called.should.be.true(); + credentialsLoad.lastCall.args[0].should.eql(credentials); + flows.getFlows().flows.should.eql(originalConfig); + done(); + }); + }); + it('updates existing flows with partial deployment - nodes', function(done) { var originalConfig = [ {id:"t1-1",x:10,y:10,z:"t1",type:"test",wires:[]}, @@ -235,6 +255,20 @@ describe('flows/index', function() { }); }); + it('returns error if it cannot decrypt credentials', function(done) { + var originalConfig = [ + {id:"t1-1",x:10,y:10,z:"t1",type:"test",wires:[]}, + {id:"t1",type:"tab"} + ]; + var credentials = {"$":"fail"}; + + flows.init({log:mockLog, settings:{},storage:storage}); + flows.setFlows(originalConfig,credentials).then(function() { + done("Unexpected success when credentials couldn't be decrypted") + }).catch(function(err) { + done(); + }); + }); }); describe('#load', function() { diff --git a/test/unit/@node-red/runtime/lib/nodes/flows/util_spec.js b/test/unit/@node-red/runtime/lib/nodes/flows/util_spec.js index ac8efef79..c20d222e2 100644 --- a/test/unit/@node-red/runtime/lib/nodes/flows/util_spec.js +++ b/test/unit/@node-red/runtime/lib/nodes/flows/util_spec.js @@ -150,7 +150,7 @@ describe('flows/util', function() { {id:"t1",type:"tab"} ]; var parsedConfig = flowUtil.parseConfig(originalConfig); - var expectedConfig = {"allNodes":{"t1-1":{"id":"t1-1","x":10,"y":10,"z":"t1","type":"test","wires":[]},"t1":{"id":"t1","type":"tab"}},"subflows":{},"configs":{},"flows":{"t1":{"id":"t1","type":"tab","subflows":{},"configs":{},"nodes":{"t1-1":{"id":"t1-1","x":10,"y":10,"z":"t1","type":"test","wires":[]}}}},"missingTypes":[]}; + var expectedConfig = {"allNodes":{"t1-1":{"id":"t1-1","x":10,"y":10,"z":"t1","type":"test","wires":[]},"t1":{"id":"t1","type":"tab"}},"subflows":{},"configs":{},"flows":{"t1":{"id":"t1","type":"tab","subflows":{},"configs":{},"nodes":{"t1-1":{"id":"t1-1","x":10,"y":10,"z":"t1","type":"test","wires":[]}}}},"groups":{},"missingTypes":[]}; parsedConfig.should.eql(expectedConfig); }); @@ -161,7 +161,7 @@ describe('flows/util', function() { {id:"t1",type:"tab"} ]; var parsedConfig = flowUtil.parseConfig(originalConfig); - var expectedConfig = {"allNodes":{"t1-1":{"id":"t1-1","x":10,"y":10,"z":"t1","type":"test","foo":"cn","wires":[]},"cn":{"id":"cn","type":"test"},"t1":{"id":"t1","type":"tab"}},"subflows":{},"configs":{"cn":{"id":"cn","type":"test","_users":["t1-1"]}},"flows":{"t1":{"id":"t1","type":"tab","subflows":{},"configs":{},"nodes":{"t1-1":{"id":"t1-1","x":10,"y":10,"z":"t1","type":"test","foo":"cn","wires":[]}}}},"missingTypes":[]}; + var expectedConfig = {"allNodes":{"t1-1":{"id":"t1-1","x":10,"y":10,"z":"t1","type":"test","foo":"cn","wires":[]},"cn":{"id":"cn","type":"test"},"t1":{"id":"t1","type":"tab"}},"subflows":{},"configs":{"cn":{"id":"cn","type":"test","_users":["t1-1"]}},"flows":{"t1":{"id":"t1","type":"tab","subflows":{},"configs":{},"nodes":{"t1-1":{"id":"t1-1","x":10,"y":10,"z":"t1","type":"test","foo":"cn","wires":[]}}}},"groups":{},"missingTypes":[]}; parsedConfig.should.eql(expectedConfig); }); @@ -173,7 +173,7 @@ describe('flows/util', function() { {id:"t2-1",x:10,y:10,z:"t2",type:"test",wires:[]} ]; var parsedConfig = flowUtil.parseConfig(originalConfig); - var expectedConfig = {"allNodes":{"t1":{"id":"t1","type":"tab"},"t1-1":{"id":"t1-1","x":10,"y":10,"z":"t1","type":"test","wires":[]},"t2":{"id":"t2","type":"tab"},"t2-1":{"id":"t2-1","x":10,"y":10,"z":"t2","type":"test","wires":[]}},"subflows":{},"configs":{},"flows":{"t1":{"id":"t1","type":"tab","subflows":{},"configs":{},"nodes":{"t1-1":{"id":"t1-1","x":10,"y":10,"z":"t1","type":"test","wires":[]}}},"t2":{"id":"t2","type":"tab","subflows":{},"configs":{},"nodes":{"t2-1":{"id":"t2-1","x":10,"y":10,"z":"t2","type":"test","wires":[]}}}},"missingTypes":[]}; + var expectedConfig = {"allNodes":{"t1":{"id":"t1","type":"tab"},"t1-1":{"id":"t1-1","x":10,"y":10,"z":"t1","type":"test","wires":[]},"t2":{"id":"t2","type":"tab"},"t2-1":{"id":"t2-1","x":10,"y":10,"z":"t2","type":"test","wires":[]}},"subflows":{},"configs":{},"flows":{"t1":{"id":"t1","type":"tab","subflows":{},"configs":{},"nodes":{"t1-1":{"id":"t1-1","x":10,"y":10,"z":"t1","type":"test","wires":[]}}},"t2":{"id":"t2","type":"tab","subflows":{},"configs":{},"nodes":{"t2-1":{"id":"t2-1","x":10,"y":10,"z":"t2","type":"test","wires":[]}}}},"groups":{},"missingTypes":[]}; parsedConfig.should.eql(expectedConfig); }); @@ -185,7 +185,7 @@ describe('flows/util', function() { {id:"sf1-1",x:10,y:10,z:"sf1",type:"test",wires:[]} ]; var parsedConfig = flowUtil.parseConfig(originalConfig); - var expectedConfig = {"allNodes":{"t1":{"id":"t1","type":"tab"},"t1-1":{"id":"t1-1","x":10,"y":10,"z":"t1","type":"subflow:sf1","wires":[]},"sf1":{"id":"sf1","type":"subflow"},"sf1-1":{"id":"sf1-1","x":10,"y":10,"z":"sf1","type":"test","wires":[]}},"subflows":{"sf1":{"id":"sf1","type":"subflow","configs":{},"nodes":{"sf1-1":{"id":"sf1-1","x":10,"y":10,"z":"sf1","type":"test","wires":[]}},"instances":[{"id":"t1-1","x":10,"y":10,"z":"t1","type":"subflow:sf1","wires":[],"subflow":"sf1"}]}},"configs":{},"flows":{"t1":{"id":"t1","type":"tab","subflows":{},"configs":{},"nodes":{"t1-1":{"id":"t1-1","x":10,"y":10,"z":"t1","type":"subflow:sf1","wires":[],"subflow":"sf1"}}}},"missingTypes":[]}; + var expectedConfig = {"allNodes":{"t1":{"id":"t1","type":"tab"},"t1-1":{"id":"t1-1","x":10,"y":10,"z":"t1","type":"subflow:sf1","wires":[]},"sf1":{"id":"sf1","type":"subflow"},"sf1-1":{"id":"sf1-1","x":10,"y":10,"z":"sf1","type":"test","wires":[]}},"subflows":{"sf1":{"id":"sf1","type":"subflow","configs":{},"nodes":{"sf1-1":{"id":"sf1-1","x":10,"y":10,"z":"sf1","type":"test","wires":[]}},"instances":[{"id":"t1-1","x":10,"y":10,"z":"t1","type":"subflow:sf1","wires":[],"subflow":"sf1"}]}},"configs":{},"flows":{"t1":{"id":"t1","type":"tab","subflows":{},"configs":{},"nodes":{"t1-1":{"id":"t1-1","x":10,"y":10,"z":"t1","type":"subflow:sf1","wires":[],"subflow":"sf1"}}}},"groups":{},"missingTypes":[]}; parsedConfig.should.eql(expectedConfig); }); @@ -197,7 +197,7 @@ describe('flows/util', function() { ]; var parsedConfig = flowUtil.parseConfig(originalConfig); parsedConfig.missingTypes.should.eql(['missing']); - var expectedConfig = {"allNodes":{"t1":{"id":"t1","type":"tab"},"t1-1":{"id":"t1-1","x":10,"y":10,"z":"t1","type":"sf1","wires":[]},"t1-2":{"id":"t1-2","x":10,"y":10,"z":"t1","type":"missing","wires":[]}},"subflows":{},"configs":{},"flows":{"t1":{"id":"t1","type":"tab","subflows":{},"configs":{},"nodes":{"t1-1":{"id":"t1-1","x":10,"y":10,"z":"t1","type":"sf1","wires":[]},'t1-2': { id: 't1-2', x: 10, y: 10, z: 't1', type: 'missing', wires: [] }}}},"missingTypes":["missing"]}; + var expectedConfig = {"allNodes":{"t1":{"id":"t1","type":"tab"},"t1-1":{"id":"t1-1","x":10,"y":10,"z":"t1","type":"sf1","wires":[]},"t1-2":{"id":"t1-2","x":10,"y":10,"z":"t1","type":"missing","wires":[]}},"subflows":{},"configs":{},"flows":{"t1":{"id":"t1","type":"tab","subflows":{},"configs":{},"nodes":{"t1-1":{"id":"t1-1","x":10,"y":10,"z":"t1","type":"sf1","wires":[]},'t1-2': { id: 't1-2', x: 10, y: 10, z: 't1', type: 'missing', wires: [] }}}},"groups":{},"missingTypes":["missing"]}; redUtil.compareObjects(parsedConfig,expectedConfig).should.be.true(); }); @@ -207,10 +207,20 @@ describe('flows/util', function() { {id:"cn",type:"test"}, ]; var parsedConfig = flowUtil.parseConfig(originalConfig); - var expectedConfig = {"allNodes":{"t1-1":{"id":"t1-1","x":10,"y":10,"z":"t1","type":"test","foo":"cn","wires":[]},"cn":{"id":"cn","type":"test"}},"subflows":{},"configs":{"cn":{"id":"cn","type":"test","_users":["t1-1"]}},"flows":{"t1":{"id":"t1","type":"tab","subflows":{},"configs":{},"nodes":{"t1-1":{"id":"t1-1","x":10,"y":10,"z":"t1","type":"test","foo":"cn","wires":[]}}}},"missingTypes":[]}; + var expectedConfig = {"allNodes":{"t1-1":{"id":"t1-1","x":10,"y":10,"z":"t1","type":"test","foo":"cn","wires":[]},"cn":{"id":"cn","type":"test"}},"subflows":{},"configs":{"cn":{"id":"cn","type":"test","_users":["t1-1"]}},"flows":{"t1":{"id":"t1","type":"tab","subflows":{},"configs":{},"nodes":{"t1-1":{"id":"t1-1","x":10,"y":10,"z":"t1","type":"test","foo":"cn","wires":[]}}}},"groups":{},"missingTypes":[]}; parsedConfig.should.eql(expectedConfig); }); + it('parses a flow including a group', function() { + var originalConfig = [ + {id:"t1",type:"tab"}, + {id:"t1-1",x:10,y:10,z:"t1",type:"test",wires:[]}, + {id:"g1",type:"group",z:"t1"} + ]; + var parsedConfig = flowUtil.parseConfig(originalConfig); + var expectedConfig = {"allNodes":{"t1":{"id":"t1","type":"tab"},"t1-1":{"id":"t1-1","x":10,"y":10,"z":"t1","type":"test","wires":[]},"g1":{"id":"g1","type":"group","z":"t1"}},"subflows":{},"configs":{},"flows":{"t1":{"id":"t1","type":"tab","subflows":{},"configs":{},"nodes":{"t1-1":{"id":"t1-1","x":10,"y":10,"z":"t1","type":"test","wires":[]}}}},"groups":{"g1":{"id":"g1","type":"group","z":"t1"}},"missingTypes":[]} + parsedConfig.should.eql(expectedConfig); + }); }); diff --git a/test/unit/@node-red/runtime/lib/storage/localfilesystem/library_spec.js b/test/unit/@node-red/runtime/lib/storage/localfilesystem/library_spec.js index 70152ea1c..69b6e3da6 100644 --- a/test/unit/@node-red/runtime/lib/storage/localfilesystem/library_spec.js +++ b/test/unit/@node-red/runtime/lib/storage/localfilesystem/library_spec.js @@ -72,45 +72,52 @@ describe('storage/localfilesystem/library', function() { }); function createObjectLibrary(type) { - type = type ||"object"; - var objLib = path.join(userDir,"lib",type); + type = type || "object"; + var objLib = path.join(userDir, "lib", type); try { fs.mkdirSync(objLib); - } catch(err) { + } catch (err) { } - fs.mkdirSync(path.join(objLib,"A")); - fs.mkdirSync(path.join(objLib,"B")); - fs.mkdirSync(path.join(objLib,"B","C")); + fs.mkdirSync(path.join(objLib, "A")); + fs.mkdirSync(path.join(objLib, "B")); + fs.mkdirSync(path.join(objLib, "B", "C")); + fs.mkdirSync(path.join(objLib, "D")); if (type === "functions" || type === "object") { - fs.writeFileSync(path.join(objLib,"file1.js"),"// abc: def\n// not a metaline \n\n Hi",'utf8'); - fs.writeFileSync(path.join(objLib,"B","file2.js"),"// ghi: jkl\n// not a metaline \n\n Hi",'utf8'); + fs.writeFileSync(path.join(objLib, "file1.js"), "// abc: def\n// not a metaline \n\n Hi", 'utf8'); + fs.writeFileSync(path.join(objLib, "B", "file2.js"), "// ghi: jkl\n// not a metaline \n\n Hi", 'utf8'); + fs.writeFileSync(path.join(objLib, "D", "file3.js"), "// mno: 日本語テスト\n\nこんにちわ", 'utf8'); } if (type === "flows" || type === "object") { - fs.writeFileSync(path.join(objLib,"B","flow.json"),"Hi",'utf8'); + fs.writeFileSync(path.join(objLib, "B", "flow.json"), "Hi", 'utf8'); } } - it('should return a directory listing of library objects',function(done) { - localfilesystemLibrary.init({userDir:userDir}).then(function() { + it('should return a directory listing of library objects', function (done) { + localfilesystemLibrary.init({userDir: userDir}).then(function () { createObjectLibrary(); - localfilesystemLibrary.getLibraryEntry('object','').then(function(flows) { - flows.should.eql([ 'A', 'B', { abc: 'def', fn: 'file1.js' } ]); - localfilesystemLibrary.getLibraryEntry('object','B').then(function(flows) { - flows.should.eql([ 'C', { ghi: 'jkl', fn: 'file2.js' }, { fn: 'flow.json' } ]); - localfilesystemLibrary.getLibraryEntry('object','B/C').then(function(flows) { + localfilesystemLibrary.getLibraryEntry('object', '').then(function (flows) { + flows.should.eql([ 'A', 'B', 'D', { abc: 'def', fn: 'file1.js' }]); + localfilesystemLibrary.getLibraryEntry('object', 'B').then(function (flows) { + flows.should.eql([ 'C', { ghi: 'jkl', fn: 'file2.js' }, { fn: 'flow.json' }]); + localfilesystemLibrary.getLibraryEntry('object', 'B/C').then(function (flows) { flows.should.eql([]); - done(); - }).catch(function(err) { + localfilesystemLibrary.getLibraryEntry('object', 'D').then(function (flows) { + flows.should.eql([{ mno: '日本語テスト', fn: 'file3.js' }]); + done(); + }).catch(function (err) { + done(err); + }); + }).catch(function (err) { done(err); }); - }).catch(function(err) { + }).catch(function (err) { done(err); }); - }).catch(function(err) { + }).catch(function (err) { done(err); }); - }).catch(function(err) { + }).catch(function (err) { done(err); }); }); @@ -203,4 +210,35 @@ describe('storage/localfilesystem/library', function() { done(err); }); }); + + it('should return a newly saved library flow (multi-byte character)',function(done) { + localfilesystemLibrary.init({userDir:userDir}).then(function() { + createObjectLibrary("flows"); + localfilesystemLibrary.getLibraryEntry('flows','B').then(function(flows) { + flows.should.eql([ 'C', {fn:'flow.json'} ]); + var ft = path.join("B","D","file4"); + localfilesystemLibrary.saveLibraryEntry('flows',ft,{mno:'pqr'},"こんにちわこんにちわこんにちわ").then(function() { + setTimeout(function() { + localfilesystemLibrary.getLibraryEntry('flows',path.join("B","D")).then(function(flows) { + flows.should.eql([ { mno: 'pqr', fn: 'file4.json' } ]); + localfilesystemLibrary.getLibraryEntry('flows',ft+".json").then(function(body) { + body.should.eql("こんにちわこんにちわこんにちわ"); + done(); + }).catch(function(err) { + done(err); + }); + }).catch(function(err) { + done(err); + }) + }, 50); + }).catch(function(err) { + done(err); + }); + }).catch(function(err) { + done(err); + }); + }).catch(function(err) { + done(err); + }); + }); }); diff --git a/test/unit/@node-red/util/lib/util_spec.js b/test/unit/@node-red/util/lib/util_spec.js index cf59df55e..f8c77d68a 100644 --- a/test/unit/@node-red/util/lib/util_spec.js +++ b/test/unit/@node-red/util/lib/util_spec.js @@ -143,6 +143,10 @@ describe("@node-red/util/util", function() { cloned.req.should.equal(msg.req); cloned.res.should.equal(msg.res); }); + it('handles undefined values without throwing an error', function() { + var result = util.cloneMessage(undefined); + should.not.exist(result); + }) }); describe('getObjectProperty', function() { it('gets a property beginning with "msg."', function() { @@ -495,12 +499,27 @@ describe("@node-red/util/util", function() { var result = util.evaluateJSONataExpression(expr,{payload:"hello"}); result.should.eql("bar"); }); + it('accesses undefined environment variable from an expression', function() { + var expr = util.prepareJSONataExpression('$env("UTIL_ENV")',{}); + var result = util.evaluateJSONataExpression(expr,{}); + result.should.eql(''); + }); it('accesses environment variable from an expression', function() { process.env.UTIL_ENV = 'foo'; var expr = util.prepareJSONataExpression('$env("UTIL_ENV")',{}); var result = util.evaluateJSONataExpression(expr,{}); result.should.eql('foo'); }); + it('accesses moment from an expression', function() { + var expr = util.prepareJSONataExpression('$moment("2020-05-27", "YYYY-MM-DD").add("days", 7).add("months", 1).format("YYYY-MM-DD")',{}); + var result = util.evaluateJSONataExpression(expr,{}); + result.should.eql('2020-07-03'); + }); + it('accesses moment-timezone from an expression', function() { + var expr = util.prepareJSONataExpression('$moment("2013-11-18 11:55Z").tz("Asia/Taipei").format()',{}); + var result = util.evaluateJSONataExpression(expr,{}); + result.should.eql('2013-11-18T19:55:00+08:00'); + }); it('handles non-existant flow context variable', function() { var expr = util.prepareJSONataExpression('$flowContext("nonExistant")',{context:function() { return {flow:{get: function(key) { return {'foo':'bar'}[key]}}}}}); var result = util.evaluateJSONataExpression(expr,{payload:"hello"}); @@ -840,11 +859,11 @@ describe("@node-red/util/util", function() { }, } }; - + for (var i = 0; i < 1000; i++) { msg.msg.obj.big += 'some more string '; } - + var result = util.encodeObject(msg); result.format.should.eql("error"); var resultJson = JSON.parse(result.msg); @@ -862,7 +881,7 @@ describe("@node-red/util/util", function() { throw new Error('Exception in toString - should have been caught'); } msg.msg.constructor = { name: "strangeobj" }; - + var result = util.encodeObject(msg); var success = (result.msg.indexOf('[Type not printable]') >= 0); success.should.eql(true); @@ -872,11 +891,11 @@ describe("@node-red/util/util", function() { var msg = { msg: { mystrangeobj:"hello", - constructor: { + constructor: { get name(){ throw new Error('Exception in constructor name'); } - } + } }, }; var result = util.encodeObject(msg);