diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index a20b8ae14..70d36deb1 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -12,12 +12,11 @@ permissions: jobs: build: permissions: - checks: write # for coverallsapp/github-action to create new checks contents: read # for actions/checkout to fetch code runs-on: ubuntu-latest strategy: matrix: - node-version: [16, 18, 20] + node-version: [18, 20, 22] steps: - uses: actions/checkout@v4 - name: Use Node.js ${{ matrix.node-version }} @@ -29,8 +28,3 @@ jobs: - name: Run tests run: | npm run test - # - name: Publish to coveralls.io - # if: ${{ matrix.node-version == 16 }} - # uses: coverallsapp/github-action@v1.1.2 - # with: - # github-token: ${{ github.token }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 00daf9639..062fbc944 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,14 +1,27 @@ -#### 3.1.11: Maintenance Release +#### 4.0.0: Milestone Release - - Add/Update German Translations for delay node (#4762) @dceejay - - Update ws dependency +This marks the next major release of Node-RED. The following changes represent +those added since the last beta. Check the beta release details below for the complete +list. -#### 3.1.10: Maintenance Release +Breaking Changes + + - Node-RED now requires Node 18.x or later. At the time of release, we recommend + using Node 20. + +Editor + + - Add `httpStaticCors` (#4761) @knolleary + - Update dependencies (#4763) @knolleary + - Sync master to dev (#4756) @knolleary + - Add tooltip and message validation to `typedInput` (#4747) @GogoVega + - Replace bcrypt with @node-rs/bcrypt (#4744) @knolleary + - Export Nodes dialog refinement (#4746) @Steve-Mcl + +#### 4.0.0-beta.4: Beta Release + +Editor - - Include rewired nodes when calculating Modified Flows stop list (#4754) @knolleary - - Fix clone of group env var properties (#4753) @knolleary - - Fix losing links when importing a copy of links into a subflow (#4750) @GogoVega - - Ensure all CSS variables are in the output file (#3743) @bonanitech - Fix the Sidebar Config is not refreshed after a deploy (#4734) @GogoVega - Fix checkboxes are not updated when calling `typedInput("value", "")` (#4729) @GogoVega - Fix panning with middle mouse button on windows 10/11 (#4716) @corentin-sodebo-voile @@ -19,669 +32,109 @@ - Change the Config Node cursor to `pointer` (#4711) @GogoVega - Add missing tooltips to Sidebar (#4713) @GogoVega - Allow nodes to return additional history entries in onEditSave (#4710) @knolleary - - Pass full error object in Function node and copy over cause property (#4685) @knolleary - - Replacing vm.createScript in favour of vm.Script (#4534) @patlux - - Avoid login loops when autoLogin enabled but login fails (#4684) @knolleary + - Update to Monaco 0.49.0 (#4725) @Steve-Mcl + - Add Japanese translations for 4.0.0-beta.3 (#4726) @kazuhitoyokoi + - Show lock on deploy if user is read-only (#4706) @knolleary + +Runtime + + - Ensure all CSS variables are in the output file (#3743) @bonanitech + - Add httpAdminCookieOptions (#4718) @knolleary + - chore: migrate deprecated `util.isArray` (#4724) @Rotzbua + - Add --version cli args (#4707) @knolleary + - feat(grunt): fail if files are missing (#4739) @Rotzbua + - fix(node-red-pi): node-red not started by path (#4736) @Rotzbua + - fix(editor): remove trailing slash (#4735) @Rotzbua + - fix: remove deprecated mqtt.js (#4733) @Rotzbua + +Nodes + + - Perform Proxy logic more like cURL (#4616) @Steve-Mcl + +#### 4.0.0-beta.3: Beta Release + +Editor + + - Improve background-deploy notification handling (#4692) @knolleary + - Hide workspace tab on middle mouse click (#4657) @Steve-Mcl + - multiplayer: Add user presence indicators (#4666) @knolleary + - Enable updating dependency node of package.json in project feature (#4676) @kazuhitoyokoi + - Add French translations for 4.0.0-beta.2 (#4681) @GogoVega + - Add Japanese translations for 4.0.0-beta.2 (#4674) @kazuhitoyokoi + - Fix saving of conf-type properties in module packaged subflows (#4658) @knolleary + - Add npm install timeout notification (#4662) @hardillb - Fix undo of subflow env property edits (#4667) @knolleary - Fix three error typos in monaco.js (#4660) @JoshuaCWebDeveloper - docs: Add closing paragraph tag (#4664) @ZJvandeWeg - -#### 3.1.9: Maintenance Release - - - Prevent subflow being added to itself (#4654) @knolleary - - Fix use of spawn on windows with cmd files (#4652) @knolleary - - Guard refresh of unknown subflow (#4640) @knolleary - - Fix subflow module sending messages to debug sidebar (#4642) @knolleary - -#### 3.1.8: Maintenance Release - - - Add validation and error handling on subflow instance properties (#4632) @knolleary - - Hide import/export context menu if disabled in theme (#4633) @knolleary - - Show change indicator on subflow tabs (#4631) @knolleary - - Bump dependencies (#4630) @knolleary - - Reset workspace index when clearing nodes (#4619) @knolleary - - Remove typo in global config (#4613) @kazuhitoyokoi - -#### 3.1.7: Maintenance Release - - - Add Japanese translation for v3.1.6 (#4603) @kazuhitoyokoi - - Update jsonata version (#4593) @hardillb - -#### 3.1.6: Maintenance Release - -Editor - - - Do not flag env var in num typedInput as error (#4582) @knolleary - - Fix example flow name in import dialog (#4578) @kazuhitoyokoi - - Fix missing node icons in workspace (#4570) @knolleary + - Avoid login loops when autoLogin enabled but login fails (#4684) @knolleary Runtime - - Handle undefined env vars (#4581) @knolleary - - fix: Removed offending MD5 crypto hash and replaced with SHA1 and SHA256 … (#4568) @JaysonHurst - - chore: remove never use import code (#4580) @giscafer + - Allow blank strings to be used for env var property substitutions (#4672) @knolleary + - Use rfdc for cloning pure JSON values (#4679) @knolleary + - fix: remove outdated Node 11+ check (#4314) @Rotzbua + - feat(ci): add new nodejs v22 (#4694) @Rotzbua + - fix(node): increase required node >=18.5 (#4690) @Rotzbua + - fix(dns): remove outdated node check (#4689) @Rotzbua + - fix(polyfill): remove import module polyfill (#4688) @Rotzbua + - Fix typo (#4686) @Rotzbua Nodes - - fix: template node zh-CN translation (#4575) @giscafer + - Pass full error object in Function node and copy over cause property (#4685) @knolleary + - Replacing vm.createScript in favour of vm.Script (#4534) @patlux -#### 3.1.5: Maintenance Release - -Runtime - - - Fix require of dns module (#4562) @knolleary - - Ensure global creds object is initialised when adding first cred (#4561) @knolleary - -#### 3.1.4: Maintenance Release +#### 4.0.0-beta.2: Beta Release Editor - - Highlight errors in config node sidebar (#4529) @knolleary - - Improve feedback in import dialog to show conflicted nodes (#4550) @knolleary - - Modify node users info in config editor footer (#4528) @knolleary - - Handle modified-nodes deploy after replacing unknown config node (#4556) @knolleary - - Handle undefined default export when importing module (#4539) @knolleary - - Fix icon scaling for non .svg icons (#4491) @ralphwetzel - - (convertNode) Do not create the credentials object if there is nothing to export (#4544) @GogoVega - - Ensure subflow instance node has g property set (#4538) @knolleary - - Handle importing flow with existing subflow and instance node (#4546) @knolleary - - Update index.mst (#4483) @gorenje - - Include top level property name when copying path from context (#4527) @knolleary - - Add handling to disable items on context menu (#4500) @kazuhitoyokoi - - Focus Quick Add dialog from context menu (#4516) @kazuhitoyokoi - - Fix subflow ports in Quick Add dialog (#4518) @kazuhitoyokoi - - Fix location of subflow ports in palette (#4502) @kazuhitoyokoi - - Client/Editor Events: fix off-in-on pattern emulating once (#4484) @gorenje - - Restore caching busting functionality without using explict version number (#4512) @knolleary - - Do not translate the list of available languages (#4531) @GogoVega - - Add French translation of v3.1.3 changes (#4477) @GogoVega - - i18n(es-ES) Spanish Spain translation (#4495) @joebordes - - Add missing validation messages (#4487) @GogoVega - - Add Japanese translations for v3.1.3 (#4498) @kazuhitoyokoi - - Replace `rename` by `edit` for the menu flow label (#4506) @GogoVega - - Update editor.json fix typo in German translation (#4552) @guidoffm + - Introduce multiplayer feature (#4629) @knolleary + - Separate the "add new config-node" option into a new (+) button (#4627) @GogoVega + - Retain Palette categories collapsed and filter to localStorage (#4634) @knolleary + - Ensure palette filter reapplies and clear up unknown categories (#4637) @knolleary + - Add support for plugin (only) modules to the palette manager (#4620) @knolleary + - Update monaco to latest and node types to 18 LTS (#4615) @Steve-Mcl Runtime - - Bump the github-actions group with 1 update (#4554) @app/dependabot - - Clone objects types when getting env values (#4519) @knolleary - - Ensure global-config credential env vars are merged on deploy (#4526) @knolleary + - Fix handling of subflow config-node select type in sf module (#4643) @knolleary + - Comms API updates (#4628) @knolleary + - Add French translations for 4.0.0-beta.1 (#4621) @GogoVega + - Add Japanese translations for 4.0.0-beta.1 (#4612) @kazuhitoyokoi + +Nodes + - Fix change node handling of replacing with boolean (#4639) @knolleary + +#### 4.0.0-beta.1: Beta Release + +Editor + + - Click on id in debug panel highlights node or flow (#4439) @ralphwetzel + - Support config selection in a subflow env var (#4587) @Steve-Mcl + - Add timestamp formatting options to TypedInput (#4468) @knolleary + - Allow RED.view.select to select links (#4553) @lgrkvst + - Add auto-complete to flow/global/env typedInput types (#4480) @knolleary + - Improve the appearance of the Node-RED primary header (#4598) @joepavitt + +Runtime + + - let settings.httpNodeAuth accept single middleware or array of middlewares (#4572) @kevinGodell + - Upgrade to JSONata 2.x (#4590) @knolleary + - Bump minimum version to node 18 (#4571) @knolleary + - npm: Remove production flag on npm invocation (#4347) @ZJvandeWeg + - Timer testing fix (#4367) @hlovdal + - Bump to 4.0.0-dev (#4322) @knolleary Nodes - - 21-httprequest.js remove unused code, because of broken use of toLowercase (#4522) @gorenje - -#### 3.1.3: Maintenance Release - -Editor - - - Add missing en-us messages (#4475) @knolleary - -#### 3.1.2: Maintenance Release - -Editor - - - Relax some node validators to allow undefined value (#4471) @knolleary - - Fix switch validation of typeof field (#4465) @knolleary - - Use move cursor when hovering on group border (#4467) @knolleary - - Added action list Chinese (Simplified and Traditional) translation + v3.1.1 changes (#4470) @wangyiyi2056 - - Add French translation of `action-list` + v3.1.1 changes (#4466) @GogoVega - - Runtime - - - Ensure nested groups inside subflows have their g props remapped (#4472) @knolleary - -#### 3.1.1: Maintenance Release - -Editor - - - Fix debug filter (#4461) @knolleary - - Fix various issues with debug pop-out window (#4459) @knolleary - - Ensure subflow instances keep track of their groups (#4457) @knolleary - - Fix `validateNodeProperty` without validator provided (#4455) @GogoVega - - Debounce node-removed notifications (#4453) @knolleary - - Don't try to load the parents of the first commit (#4448) @bonanitech - - Allow a theme to specifiy which theme mermaid should use (#4441) @knolleary - - Update browser title with flow name if set (#4427) @knolleary - - Ensure typeSearch handles undefined node definitions (#4423) @knolleary - - Ensure group w/h are imported if present (#4426) @knolleary - - Hide node status background when there is no status to show (#4425) @knolleary - - Add a close button to the restart-required notification (#4407) @knolleary - - Extend typedInput "num" type validity check to NaN, binary, octal & hex (#4371) @ralphwetzel - - Fix unintended new line in node name (#4399) @kazuhitoyokoi - - Ctrl-Enter does not close tray (Monaco) #4377 (#4382) @hazymat - - fix buffer viewer to handle 0b style binary (#4393) @dceejay - - Rework mermaid integration to support off-DOM rendering (#4364) @knolleary - - Add missing nls labels to context menu (#4365) @knolleary - -Runtime - - - Bump the github-actions group with 2 updates (#4404) @app/dependabot - - Handle unknown node reference inside subflow module (#4460) @knolleary - - Add modules.install audit event when external module installed (#4452) @knolleary - - Allow import of modules with subpath in specifier (#4451) @knolleary - - Update node-red-admin version (#4438) @knolleary - - Handle false-like env vars properly (#4411) @knolleary - - Only save settings once during node load process (#4409) @knolleary - - Ensure global-config nodes lookup cred values properly (#4405) @knolleary - - Handle credential env var evaluation when no value set (#4362) @knolleary - - Don't commit package-lock.json (#4354) @bonanitech - - Fix env evaluation when one env references another in the same object (#4361) @knolleary - - Add dependabot for Github Actions (#4312) @Rotzbua - - Update outdated Github Actions (#4311) @Rotzbua - - github: Request `npm run test` in PR template (#4348) @ZJvandeWeg - - Add French translation of v3.1.0-beta.4 changes + slight improvements (#4329) @GogoVega - - Handle nodes with multiple input handlers properly (#4332) @knolleary - - Soften the language around unrequited PRs (#4351) @knolleary - -Nodes - - - CSV: make CSV export way faster by not re-allocating and handling huge string (#4349) @Fadoli - - Delay: Fix regression in delay node to not pass on msg.reset (#4350) @dceejay - - Link Call: Handle undefined linkType value for existing link-call nodes (#4331) @knolleary - - MQTT: Guard against node.broker being undefined (#4454) @knolleary - - MQTT: check topic length > 0 before publish (#4416) @dceejay - - Switch/Change: Improve validation of switch/change node rules (#4368) @knolleary - - Template: Fix height of description editor in template node (#4346) @kazuhitoyokoi - - Various: Add validators to any fields using msg-typed Input (#4440) @knolleary - -#### 3.1.0: Milestone Release - -Editor - - - Default filter to All Catalogues and show nodes for small lists (#4318) @knolleary - - Better distinguish between ctrl and meta keys on mac (#4310) @knolleary - - Ensure junction appears when filtering quick-add list (#4297) @knolleary - - Update message catalogs for JSONata Expression editor (#4287) @kazuhitoyokoi - - Add tooltip to relevance sort button in user settings UI (#4288) @kazuhitoyokoi - - Capture workspace dirty state when quick-adding junction (#4283) @knolleary - - Add docs for $clone function (#4284) @knolleary - -Runtime - - - Dependency updates (#4317) @knolleary - - Ensure storage/util.writeFile handles concurrent write attempts (#4316) @knolleary - - Migrate http -> https for nodered.org (#4313) @Rotzbua - - Add Node 20 to GH Action test matrix (#4305) @Rotzbua - - Handle group-scoped nodes inside subflow (#4301) @knolleary - - Handle non-url-safe chars in context api (#4298) @knolleary - - Fix git pull operation in project feature (#4290) @kazuhitoyokoi - - Change linefeed codes in Korean message catalogs (#4286) @kazuhitoyokoi - - Fix file permissions of message catalogs (#4285) @kazuhitoyokoi - - Update tour (#4278) @knolleary - -Nodes - - - File: Fix handling in file nodes when number is specified as file name (#4267) @kazuhitoyokoi - - Function: Adding function timeout to settings file (#4265) (#4309) @knolleary - - Function: Fix function setup tab layout (#4299) @knolleary - - HTTP Request: Handle 204 in httprequest JSON (#4262) @sammachin - - JSON: Fix test cases of JSON node (#4275) @kazuhitoyokoi - - MQTT: Remove unnecessary check for clientid if autoUnsub set (#4302) @knolleary - -##### 3.1.0-beta.4: Beta Release - - Editor - - - Add Japanese translation for 3.1.0 (#4252) @kazuhitoyokoi - - Improve Catalogue visibility (#4248) @Steve-Mcl - - Add support for wiring and moving junctions on touch device (#4244) @Steve-Mcl - - Show errors and statuses of config nodes in the sidebar when no catch node is available (#4231) @bvmensvoort - - Improve wiring for horizontally aligned nodes (#4232) @knolleary - - French translation of Welcome Tours (#4200) @GogoVega - - French translation of v3.1.0-beta.3 changes (#4199) @GogoVega - - add Japanese message for 3.1.0 beta 3 (#4209) @HiroyasuNishiyama - - Dont clone the group nodes `node` array when saving edits (#4208) @Steve-Mcl - - Runtime - - - Add NR_SUBFLOW_NAME/ID/PATH env vars (#4250) @knolleary - - Evaluate all env vars as part of async flow start (#4230) @knolleary - - Add support for httpStatic middleware (#4229) @knolleary - - Nodes - - - Fix JSONata in file nodes (#4246) @kazuhitoyokoi - - Fix timeout icon in function and link call nodes (#4253) @kazuhitoyokoi - - Fix connection keep-alive in http request node (#4228) @knolleary - - adding timeout attribute to function node (#4177) @k1ln - - Fix manual mode join when multiple sequences being handled (#4143) @BitCaesar - - Fix delay node flush issue (#4203) @dceejay - - Update status and catch node labels in group mode (#4207) @Steve-Mcl - -##### 3.1.0-beta.3: Beta Release - -Editor - - - Select the item that is specified in a deep link URL (#4113) @Steve-Mcl - - Update to Monaco 0.38.0 (#4189) @Steve-Mcl - - Place subflow outputs/inputs relative to current view (#4183) @knolleary - - Enable RED.view.select to select group by id (#4184) @knolleary - - Combine existing env vars when merging groups (#4182) @knolleary - - Avoid creating empty global-config node if not needed (#4153) @knolleary - - Fix group selection when using lasso (#4108) @knolleary - - Use editor path in generating localStorage keys (#4151) @mw75 - - Ensure no node credentials are included when exporting to clipboard (#4112) @knolleary - - Fix jsonata expression test ui (#4097) @knolleary - - Fix search button in palette popover (#4096) @knolleary - -Runtime - - - Allow options object on each httpStatic configuration (#4109) @kevinGodell - - Ensure non-zero exit codes for errors (#4181) @knolleary - - Ensure external modules are installed synchronously (#4180) @knolleary - - Update dependecies include got (#4155) @knolleary - - Add Japanese translations for v3.1 beta.2 (#4158) @kazuhitoyokoi - - Ensure express server options are applied consistently (#4178) @knolleary - - Remove version info from theme endpoint (#4179) @knolleary - - Add Japanese translations for welcome tour of 3.1.0 beta.2 (#4145) @kazuhitoyokoi - - Added SHA-256 and SHA-512-256 digest authentication (#4100) @sroebert - - Add "timers" types to known types (#4103) @Steve-Mcl - -Nodes - - - Allow Catch/Status nodes to be scoped to their group (#4185) @NetHans - - MQTT: Option to disable MQTT topic unsubscribe on disconnect (#4078) @flying7eleven - - -##### 3.1.0-beta.2: Beta Release - -Editor - - - NEW: Add change icon to tabs (#4068) @knolleary - - NEW: Complete overhaul of Group UX (#4079) @knolleary - - NEW: Add link to node help in node edit dialog footer (#4065) @knolleary - - NEW: Added editor feature for connecting multiple nodes to single node (#4051) @sonntam - - NEW: Increase workspace size to 8000x8000 (#4094) @knolleary - - Ensure node buttons are redrawn when flow lock state is changed (#4091) @knolleary - - Prevent loops being created with junction nodes (#4087) @knolleary - - Prevent opening locked node's edit dialog (#4069) @knolleary - - Reverse direction of tab scroll to expected direction (#4064) @knolleary - - Add cancel operation to editableList (#4077) @HiroyasuNishiyama - - Apply Mermaid diagram for project settings UI (#4054) @kazuhitoyokoi - - Add tooltip for show/hide button on info sidebar (#4050) @kazuhitoyokoi - - Fix align nodes on locked tab (#4072) @HiroyasuNishiyama - - Fix importing connected link nodes into a subflow (#4082) @knolleary - - Fix to add empty marker to empty group (#4060) @HiroyasuNishiyama - - Fix image URLs for v3.0 tour (#4053) @kazuhitoyokoi - - Show scrollbar in notification dialog only when needed (#4048) @kazuhitoyokoi - - Update-monaco-and-typings (#4089) @Steve-Mcl - - Update jquery UI (#4088) @knolleary - - Support i18n of lock/unlock buttons in flow property UI (#4049) @kazuhitoyokoi - - Translation kr (#3895) @hae-iotplatform - - Translation zhcn (!!请懂中文的帮忙review) (#3952) @cliyr - - Add French translation of nodes (#3964) @GogoVega - - Add French translation (#3962) @GogoVega - - Portuguese Brazilian (pt-BR) translation (#3804) @FabsMuller - - -Runtime - - - NEW: Generate stable ids for subflow instance internal nodes (#4093) @knolleary - - NEW: Change default file name to flows.json in project feature (#4073) @kazuhitoyokoi - - NEW: Deprecate synchronous access to jsonata (#4090) @knolleary - - Add Node 18 to test matrix (#4084) @knolleary - - Bump minimum nodejs version supported to match documented value (#4086) @knolleary - - Update monaco docs link in settings.js (#4075) @Steve-Mcl - - Remove duplicated messages in the message catalog (#4066) @kazuhitoyokoi - - Ensure errors in preDeliver callback are handled (#3911) @knolleary - - Fix "EADDRINUSE" error (#4046) @bggbr - -Nodes - - - Link Call: Clear link-call timeouts when node is closed (#4085) @knolleary - - Join: ensure inflight status is cleared when in auto mode (#4083) @knolleary - - File Out: Fix extra newline append for multipart file write (#3915) @dceejay - - Add validators for complete and link call nodes (#4056) @kazuhitoyokoi - -##### 3.1.0-beta.1: Beta Release - -Editor - - - NEW: Locking Flows (#3938) @knolleary - - NEW: Improve UX around hiding flows via context menu (#3930) @knolleary - - NEW: Add support for inline image in markdown editor by drag and drop of an image file (#4006) @HiroyasuNishiyama - - NEW: Add support for mermaid diagram to markdown editor (#4007) @HiroyasuNishiyama - - NEW: Support uri fragments for nodes and groups including edit support (#3870) @knolleary - - NEW: Add global environment variable feature (#3941) @HiroyasuNishiyama - - - Remember compact/pretty flow export user choice (#3974) @Steve-Mcl - - fix .red-ui-notification class (#4035) @xiaobinqt - - Fix border radius on Modules list header (#4038) @bonanitech - - fix workspace reference error in case of empty tabs (#4029) @HiroyasuNishiyama - - Disable delete tab menu when single tab exists (#4030) @HiroyasuNishiyama - - Disable hide all menu if all tabs hidden (#4031) @HiroyasuNishiyama - - fix hide subflow tooltip (#4033) @HiroyasuNishiyama - - Fix disabled menu items in project feature (#4027) @kazuhitoyokoi - - Let themes change radialMenu text colors (#3995) @bonanitech - - Add Japanese translations for v3.0.3 (#4012) @kazuhitoyokoi - - Add Japanese translation for v3.1.0-beta.0 (#3997) @kazuhitoyokoi - - Add Japanese translation for v3.1.0-beta.0 (#3916) @kazuhitoyokoi - - Hide subflow category after deleting subflow (#3980) @kazuhitoyokoi - - Prevent dbl-click opening node edit dialog with text selected (#3970) @knolleary - - Handle replacing unknown node inside group or subflow (#3921) @knolleary - - Fix #3939, red border red-ui-typedInput-container (#3949) @Steveorevo - - i18n item URL copy notification & add Japanese message (#3946) @HiroyasuNishiyama - - add Japanese message for item url copy actions (#3947) @HiroyasuNishiyama - - Fix autocomplete entry for responseUrl (#3884) @knolleary - - Fix Japanese translation for JSONata editor (#3872) @HiroyasuNishiyama - - Fix search type with spaces (#3841) @Steve-Mcl - - Fix error hanndling of JSONata expression editor for extended functions (#3871) @HiroyasuNishiyama - - Add button type to the adding SSH key button (#3866) @kazuhitoyokoi - - Check radio button as default in project dialog (#3879) @kazuhitoyokoi - - Add $clone as supported function (#3874) @HiroyasuNishiyama - - Env var jsonata (#3807) @HiroyasuNishiyama - - Add Japanese translation for v3.0.2 (#3852) @kazuhitoyokoi - -Runtime - - - Force IPv4 name resolution to have priority (#4019) @dceejay - - Fix async loading of modules containing both nodes and plugins (#3999) @knolleary - - Use main branch as default in project feature (#4036) @kazuhitoyokoi - - Rename package var to avoid strict mode error (#4020) @knolleary - - Fix typos in settings.js (#4013) @ypid - - Ensure credentials object is removed before returning node in getFlow request (#3971) @knolleary - - Ignore commit error in project feature (#3987) @kazuhitoyokoi - - Update dependencies (#3969) @knolleary - - Add check that node sends object rather than primitive type (#3909) @knolleary - - Ensure key_path is quoted in GIT_SSH_COMMAND in case of spaces in pathname (#3912) @knolleary - - Fix nodesDir scan when node package has js/html in sub dir to package.json (#3867) @Steve-Mcl - - Fix file permissions (#3917) @kazuhitoyokoi - - ci: add minimum GitHub token permissions for workflows (#3907) @boahc077 - -Nodes - - - Catch: fix typo in catch.html (#3965) @we11adam - - Change: Fix change node overwriting msg with itself (#3899) @dceejay - - Comment node: Clarify where the text will appear (#4004) @dirkjanfaber - - CSV: change replace to replaceAll (#3990) @dceejay - - CSV node: check header properties for ' and " (#3920) @dceejay - - CSV: Fix for CSV undefined property (#3906) @dceejay - - Delay: let delay node handle both flush then reset (#3898) @dceejay - - Function: Limit number of ports in function node (#3886) @kazuhitoyokoi - - Function: Remove dot from variable name for external module in function node (#3880) @kazuhitoyokoi - - Function: add function node monaco types util and promisify (#3868) @Steve-Mcl - - HTTP In: Ensure msg.req.headers is enumerable (#3908) @knolleary - - HTTP Request: Support form-data arrays (#3991) @hardillb - - HTTP Request: Fix httprequest tests to be more lenient on error message (#3922) @knolleary - - HTTP Request: Add missing property to node object HTTPRequest (#3842) @hardillb - - HTTP Request/Response: Support sortable list on property UI of http request and http response nodes (#3857) @kazuhitoyokoi - - HTTP Response: Ensure statusCode is a number (#3894) @hardillb - - Inject: Allow Inject node to work with async context stores (#4021) @knolleary - - Join/Batch: Add count to join and batch node labels (#4028) @dceejay - - MQTT: Fix birth topic handling in MQTT node (#3905) @Steve-Mcl - - MQTT: Fix pull-down menus of MQTT configuration node (#3890) @kazuhitoyokoi - - MQTT: Prevent invalid mqtt birth topic crashing node-red (#3869) @Steve-Mcl - - MQTT: ensure sessionExpiry(Interval) is applied (#3840) @Steve-Mcl - - MQTT: Fix mqtt nodes not reconnecting on modified-flows deploy (#3992) @knolleary - - MQTT: fix single subscription mqtt node status (#3966) @Steve-Mcl - - Range: Add drop mode to range node (#3935) @dceejay - - Remove done from describe (#3873) @HiroyasuNishiyama - - Split node: avoid duplicate done call for buffer split (#4000) @knolleary - - Status: Fix typo in 25-status.html (#3981) @kazuhitoyokoi - - TCP Node: ensure newline substitution applies to whole message (#4009) @dceejay - - Template: Add information about environment variable to template node (#3882) @kazuhitoyokoi - - Trigger: Hide trigger node repeat send option if sending nothing (#4023) @dceejay - - Watch: fix watch node test on MacOS/ARM (#3942) @HiroyasuNishiyama - -#### 3.0.2: Maintenance Release - -Editor - - - Fix workspace chart bottom property (#3812) @bonanitech - - Update german translation (#3802) @Dennis14e - - Support color reset to the default in subflow and group (#3801) @kazuhitoyokoi - - Allow generateNodeNames to handle names containing regex control chars (#3817) @knolleary - - Hide scrollbars until they're needed (#3808) @bonanitech - - Include junctions/groups when exporting subflows plus related fixes (#3816) @knolleary - - remove console.log (#3820) @Steve-Mcl - -Runtime - - - Register subflow module instance node with parent flow (#3818) @knolleary - -Nodes - - - HTTP Request: Allow HTTP Headers not in spec (#3776) @hardillb - -#### 3.0.1: Maintenance Release - -Editor - - - Allow codeEditor theme to be set even if `codeEditor` is not set in settings.js (#3794) @Steve-Mcl - - Sys info (diagnostics report) amendments (#3793) @Steve-Mcl - - Allow `mode` and `title` to be omitted in `options` argument for `createEditor` (#3791) @Steve-Mcl - - Fix focus issues (#3789) @Steve-Mcl - - Ensure all typedInput buttons have button type set (#3788) @knolleary - - Do not flag hasUsers=false nodes as unused in search (#3787) @knolleary - - Properly position quick-add dialog in all cases (#3786) @knolleary - - Ensure quick-add dialog does not obscure ghost node when shifted (#3785) @knolleary - - Remove use of Object.hasOwn (#3784) @knolleary - -#### 3.0.0: Milestone Release - -Editor - - - Use theme page and header values if settings.js values are not present (#3767) @Steve-Mcl - - Focus editor for undo after some actions in menu (#3759) @kazuhitoyokoi - - Ensure node icon shade has properly rounded corners (#3763) @knolleary - - Fix storing subflow credential type when input has multiple types (#3762) @knolleary - - Ensure global-config and flow-config have info in the hierarchy popover (#3752) @Steve-Mcl - - Include dirty state in history event (#3748) @Steve-Mcl - - Fix display direction of context sub-menu (#3746) @knolleary - - Fix clear pinned paths of debug sidebar menu (#3745) @HiroyasuNishiyama - - prevent exception generating tooltip for deleted nodes (#3742) @Steve-Mcl - - Fix context menu issues ready for v3 beta.5 (#3741) @Steve-Mcl - - Do not generate new node-ids when pasting a cut flow (#3729) @knolleary - - Fix to prevent node from moving out of workspace (#3731) @HiroyasuNishiyama - - Don't let themes change disabled config node background color (#3736) @bonanitech - - Move colors left behind in #3692 to CSS variables (#3737) @bonanitech - - Fix handling of global debug message (#3733) @HiroyasuNishiyama - - Fix label overflow @ config-node palette (#3730) @ralphwetzel - - Fix defaulting to monaco if settings does not contain codeEditor (#3732) @knolleary - - Disable keyboard shortcut mapping when showing Edit[..]Dialog (#3700) @ralphwetzel - - Update add-junction menu to work in more cases (#3727) @knolleary - - Ensure importMap is not null when using import UI (#3723) @Steve-Mcl - - Add Japanese translations for v3.0-beta.4 (#3724) @kazuhitoyokoi - - Fix "split with" on virtual links (#3766) @Steve-Mcl - -Runtime - - - Do not remove unknown credentials of Subflow Modules (#3728) @knolleary - - Add missing entries from beta.4 changelog (#3721) @knolleary - -Nodes - - - Change: Fix change node, not handling from field properly when using context (#3754) @Fadoli - - Link Call: Fix linkcall registry bugs (#3751) @Steve-Mcl - - WebSocket: Fix close timeout of websocket node (#3734) @HiroyasuNishiyama - -#### 3.0.0-beta.4: Beta Release - -Editor - - - Move all colours to CSS variables (#3692) @bonanitech - - Fix clicking on node in workspace to hide context menu (#3696) @knolleary - - Fix credential type input item of subflow template (#3703) @HiroyasuNishiyama - - Add option flag `reimport` to `importNodes` (#3718) @Steve-Mcl - - Update german translation (#3691) @Dennis14e - - List welcome tours in help sidebar (#3717) @knolleary - - Ensure 'hidden flow' count doesn't include subflows (#3715) @knolleary - - Fix Chinese translate (#3706) @hotlong - - Fix use default button for node icon (#3714) @kazuhitoyokoi - - Fix select boxes vertical alignment (#3698) @bonanitech - - Ensure workspace clean after undoing dropped node (#3708) @Steve-Mcl - - Use solid colour as config node icon background to hide text overflow (#3710) @Steve-Mcl - - Increase quick-add height to reveal 2 most recent entries (#3711) @Steve-Mcl - - Set default editor to monaco in absence of user preference (#3702) @knolleary - - Add Japanese translations for v3.0-beta.3 (#3688) @kazuhitoyokoi - - Fix handling of spacebar inside JSON visual editor (#3687) @knolleary - - Fix menu padding to handle both icons and submenus (#3686) @knolleary - - Include scroll offset when positioning quick-add dialog (#3685) @knolleary - -Runtime - - - Allow flows to be stopped and started manually (#3719) @knolleary - - Import default export if node is a transpiled es module (#3669) @dschmidt - - Leave Monaco theme commented out by default (#3704) @bonanitech - -Nodes - - - CSV: Fix CSV node to handle when outputting text fields (#3716) @dceejay - - Delay: Fix delay rate limit last timing when empty (#3709) @dceejay - - Link: Ensure link-call cache is updated when link-in is modified (#3695) @Steve-Mcl - - Join: Join node in reduce mode doesn't keep existing msg properties (#3670) @dceejay - - Template: Add support for evalulating {{env.}} within a template node (#3690) @cow0w - -#### 3.0.0-beta.3: Beta Release - -Editor - - - Add Right-Click content menu (#3678) @knolleary - - Fix disable junction (#3671) @HiroyasuNishiyama - - Add Japanese translations for v2.2.3 (#3672) @kazuhitoyokoi - - Reset mouse state when switching tabs (#3643) @knolleary - - Fix uncorrect fix of junction to subflow conversion (#3666) @HiroyasuNishiyama - - Fix undoing junction to subflow (#3653) @HiroyasuNishiyama - - Fix conversion of junction to subflow (#3652) @HiroyasuNishiyama - - Fix to include junction to exported nodes (#3650) @HiroyasuNishiyama - - Fix z-index value for shade to cover nodes in palette (#3649) @kazuhitoyokoi - - Fix to extend escaped subflow category characters (#3647) @HiroyasuNishiyama - - Fix to sanitize tab name (#3646) @HiroyasuNishiyama - - Fix selector placement (#3644) @bonanitech - - Add Japanese translations for v3.0-beta.2 (#3622) @kazuhitoyokoi - - Fix new folder menu of save to library dialog (#3633) @HiroyasuNishiyama - - Fix layer of palette node (#3638) @HiroyasuNishiyama - - Fix to place a node dragged from palette within the workspace (#3637) @HiroyasuNishiyama - - Fix typo in CSS (#3628) @bonanitech - - Use the correct variable for the gutter text color (#3615) @bonanitech - - -Runtime - - - Support loading node modules from `nodesdir` (#3676) @Steve-Mcl - - fix buffer parse error message of evaluateNodeProperty (#3624) @HiroyasuNishiyama - -Nodes - - - File: Further simplify file node filename entry UX (v3) (#3677) @Steve-Mcl - - Function: Fix initial cursor position of init/finalize tab of function node (#3674) @HiroyasuNishiyama - - Function: Fix ESM module loading in Function node (#3645) @knolleary - - Inject: Fix JSONata evaluation of inject button (#3632) @HiroyasuNishiyama - - TCP: Dont delete TCP socket twice (#3630) @Steve-Mcl - - MQTT Node: define noproxy variable (#3626) @Steve-Mcl - - Debug: i18n debug sidebar node label (#3623) @HiroyasuNishiyama - -#### 3.0.0-beta.2: Beta Release - -**Migration from 2.x** - - - The 'slice wires' action has changed from Ctrl-RightMouseButton to Alt-LeftMouseButton - -Editor - - - Rework Junctions to be more node like in their event handling (#3607) @knolleary - - Change slicing / slice-junction operations over to mouse button 0 (Left Mouse Button) (#3609) @Steve-Mcl - - Do not slice-junction link node wires (#3608) @knolleary - - Handle many-to-one slicing of wires (#3604) @knolleary - - Ensure ACE worker options are set (#3611) @Steve-Mcl - - Remove duplicate history add of ungroup event (#3605) @knolleary - - use text width instead of number of characters for deciding select fi… (#3603) @HiroyasuNishiyama - - Update Japanese info of link call node reflecting update of English info (#3600) @HiroyasuNishiyama - - Fix typedInput label not visible on themes (#3580) @bonanitech - - Fix project switching when junctions are present (#3595) @Steve-Mcl - - Fix junction: when wiring from a regular nodes INPUT, backwards to a junction (#3591) @Steve-Mcl - - Fix error initialising flow tab editor (#3585) @Steve-Mcl - - Add Japanese translations for v3.0-beta.1 (#3576) @kazuhitoyokoi - - Fix image paths where `red/image/typedInput/XXXX.png` should be `red/image/typedInput/XXXX.svg` (#3592) @kazuhitoyokoi - - Fix browser console error Uncaught TypeError when searching certain terms (#3584) @Steve-Mcl - -Runtime - - - fix error on system-info action (#3589) @HiroyasuNishiyama - -Nodes - - - I18n switch rule selector (#3602) @HiroyasuNishiyama - - Handle removal of event handlers to allow mqtt client.end() to work (#3594) @PhilDay-CT - - update link-call node info according to current behavior (#3597) @HiroyasuNishiyama - - -#### 3.0.0-beta.1: Beta Release - -**Migration from 2.x** - - - Node-RED now requires Node.js 14.x or later. - - New installs of Node-RED will default to the monaco editor. - - -Editor - - - Add Junctions (#3462) @knolleary - - Allow node name to be auto-generated when added (#3478, #3538) @knolleary - - Set monaco as default code editor as of v3.x (#3543) @Steve-Mcl - - Update Monaco to V0.33.0 (#3522) @Steve-Mcl - - Auto-complete Improvements (#3521) @Steve-Mcl - - Add a tooltip to debug sidebar messages to reveal full path to node (#3503) @knolleary - - Fix down arrow triggering menu in search box (#3507) @Steve-Mcl - - Add Japanese translations for v3.0 (#3512) @kazuhitoyokoi - - Add feature: Continuous search tools (search previous, search next) (#3405) @Steve-Mcl - - Add feature: split-wire-to-links (#3399, #3476) @Steve-Mcl - - Add copy button to node properties tables (#3390) @knolleary - - Add info-tab search options dropdown to the regular search (#3395) @Steve-Mcl - - New Feature: Add ability to find modified nodes/flows. (#3392) @Steve-Mcl - - Code editor ux improvements around remembering state of each code editor in a flow (#3553) @Steve-Mcl - - Make it easier to apply themes on SVG icons (#3515) @bonanitech - - Add support of property validation message (#3438) @HiroyasuNishiyama - - Ensure node validation tooltip is closed when field becomes valid (#3570) @knolleary - - Add "search for" buttons to notifications (#3567) @Steve-Mcl - - Don't let themes change node config colors (#3564) @bonanitech - - Fix gap between typedInput containers borders (#3560) @bonanitech - - Fix recording removed links in edit history (#3547) @knolleary - - Remove unused SASS vars (#3536) @bonanitech - - Add custom style for jQuery widgets borders (#3537) @bonanitech - - fix out of scope reference of hasUnusedConfig variable (#3535) @HiroyasuNishiyama - - correct "non string" check parenthesis (#3524) @Steve-Mcl - - Ensure i18n of scoped package name (#3516) @Steve-Mcl - - Prevent shortcut deploy when deploy button shaded (#3517) @Steve-Mcl - - Fix: Sidebar "Configuration" filter button tooltip (#3500) @ralphwetzel - - Add the ability to customize diff colors even more (#3499) @bonanitech - - Do JSON comparison of old value/new value in editor (#3481) @Steve-Mcl - - Fix nodes losing their wires when in an iframe (#3484) @zettca - - Improve scroll into view (#3468) @Steve-Mcl - - Do not show 1st tab if hidden when loading (#3464) @Steve-Mcl - -Runtime - - - Fix importing external module from node-red module (#3541) @knolleary - - Add support for multiple static paths with optional static root (#3542) @Steve-Mcl - - Store external token when authenticating if provided (#3460) @ArFe - - Support OAuth/OpenID logout (#3388) @mw75 - - Allow adminAuth to auto-login users when using passport strategy (#3519) @knolleary - - Add runtime diagnostics admin endpoint (#3511) @Steve-Mcl - - Don't start if user has no home directory (#3540) @hardillb - - Error on invalid encrypted credentials (#3498) @sammachin - -Nodes - - - Debug: Add message count option to Debug status (#3544 #3551) @rafaelmuynarsk @knolleary - - File: Change basic Filename field to a typedInput (#3533) @Steve-Mcl - - HTTP Request: Add UI for Http Request node headers (#3488) @Steve-Mcl - - Inject: let inject optionally fire at start in only at time mode. (#3385) @dceejay - - Link Call: Dynamic link call (#3463) @Steve-Mcl - - Link Call: Display link targets of nodes in a regular flow, for Link Call nodes inside a subflow (#3528) @Steve-Mcl - - MQTT: MQTT payload auto parsing improvements (#3530) @Steve-Mcl - - MQTT: Add client and Runtime MQTT topic validation (#3563) @Steve-Mcl [dev] - - MQTT: save and restore v5 config user props (#3562) @Steve-Mcl - - MQTT: Fix incorrect MQTT status (#3552) @Steve-Mcl - - MQTT: fix reference error of msg.status in debug node (#3526) @HiroyasuNishiyama - - MQTT: Add unit tests for MQTT nodes (#3497) @Steve-Mcl - - MQTT: fix typo of will properties (#3502) @Steve-Mcl - - MQTT: ensure mqtt v5 props can be set false (#3472) @Steve-Mcl - - Switch: add check for NaN in is of type number to be false (#3409) @dceejay - - TCP: TCP node better split (#3465) @dceejay - - Watch: Update Watch node to use node-watch module (#3559 #3569) @knolleary - - WebSocket: call done after ws disconnects (#3531) @Steve-Mcl + - TCP node - when resetting, if no payload, stay disconnected @dceejay + - HTML node: add option for collecting attributes and content (#4513) @gorenje + - let split node specify property to split on, and join auto join correctly (#4386) @dceejay + - Add RFC4180 compliant mode to CSV node (#4540) @Steve-Mcl + - Fix change node to return boolean if asked (#4525) @dceejay + - Let msg.reset reset Tcp request node connection when in stay connected mode (#4406) @dceejay + - Let debug node status msg length be settable via settings (#4402) @dceejay + - Feat: Add ability to set headers for WebSocket client (#4436) @marcus-j-davies #### Older Releases diff --git a/Gruntfile.js b/Gruntfile.js index 09b057837..05be6a58a 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -143,6 +143,7 @@ module.exports = function(grunt) { "packages/node_modules/@node-red/editor-client/src/js/user.js", "packages/node_modules/@node-red/editor-client/src/js/comms.js", "packages/node_modules/@node-red/editor-client/src/js/runtime.js", + "packages/node_modules/@node-red/editor-client/src/js/multiplayer.js", "packages/node_modules/@node-red/editor-client/src/js/text/bidi.js", "packages/node_modules/@node-red/editor-client/src/js/text/format.js", "packages/node_modules/@node-red/editor-client/src/js/ui/state.js", @@ -207,38 +208,52 @@ module.exports = function(grunt) { "packages/node_modules/@node-red/editor-client/src/js/ui/touch/radialMenu.js", "packages/node_modules/@node-red/editor-client/src/js/ui/tour/*.js" ], + nonull: true, dest: "packages/node_modules/@node-red/editor-client/public/red/red.js" }, 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.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", - "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", - "node_modules/i18next/i18next.min.js", - "node_modules/i18next-http-backend/i18nextHttpBackend.min.js", - "node_modules/jquery-i18next/jquery-i18next.min.js", - "node_modules/jsonata/jsonata-es5.min.js", - "packages/node_modules/@node-red/editor-client/src/vendor/jsonata/formatter.js", - "packages/node_modules/@node-red/editor-client/src/vendor/ace/ace.js", - "packages/node_modules/@node-red/editor-client/src/vendor/ace/ext-language_tools.js" - ], - // "packages/node_modules/@node-red/editor-client/public/vendor/vendor.css": [ - // // TODO: resolve relative resource paths in - // // bootstrap/FA/jquery - // ], - "packages/node_modules/@node-red/editor-client/public/vendor/ace/worker-jsonata.js": [ - "node_modules/jsonata/jsonata-es5.min.js", - "packages/node_modules/@node-red/editor-client/src/vendor/jsonata/worker-jsonata.js" - ], - "packages/node_modules/@node-red/editor-client/public/vendor/mermaid/mermaid.min.js": [ - "node_modules/mermaid/dist/mermaid.min.js" - ] - } + files: [ + { + src: [ + "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", + "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", + "node_modules/i18next/i18next.min.js", + "node_modules/i18next-http-backend/i18nextHttpBackend.min.js", + "node_modules/jquery-i18next/jquery-i18next.min.js", + "node_modules/jsonata/jsonata-es5.min.js", + "packages/node_modules/@node-red/editor-client/src/vendor/jsonata/formatter.js", + "packages/node_modules/@node-red/editor-client/src/vendor/ace/ace.js", + "packages/node_modules/@node-red/editor-client/src/vendor/ace/ext-language_tools.js" + ], + nonull: true, + dest: "packages/node_modules/@node-red/editor-client/public/vendor/vendor.js" + }, + // { + // src: [ + // // TODO: resolve relative resource paths in + // // bootstrap/FA/jquery + // ], + // dest: "packages/node_modules/@node-red/editor-client/public/vendor/vendor.css" + // }, + { + src: [ + "node_modules/jsonata/jsonata-es5.min.js", + "packages/node_modules/@node-red/editor-client/src/vendor/jsonata/worker-jsonata.js" + ], + nonull: true, + dest: "packages/node_modules/@node-red/editor-client/public/vendor/ace/worker-jsonata.js", + }, + { + src: "node_modules/mermaid/dist/mermaid.min.js", + nonull: true, + dest: "packages/node_modules/@node-red/editor-client/public/vendor/mermaid/mermaid.min.js", + }, + ] } }, uglify: { diff --git a/package.json b/package.json index 97bf6428a..5783ef911 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-red", - "version": "3.1.11", + "version": "4.0.0", "description": "Low-code programming for event-driven applications", "homepage": "https://nodered.org", "license": "Apache-2.0", @@ -26,25 +26,25 @@ } ], "dependencies": { - "acorn": "8.8.2", - "acorn-walk": "8.2.0", - "ajv": "8.12.0", - "async-mutex": "0.4.0", + "acorn": "8.11.3", + "acorn-walk": "8.3.2", + "ajv": "8.14.0", + "async-mutex": "0.5.0", "basic-auth": "2.0.1", "bcryptjs": "2.4.3", "body-parser": "1.20.2", "cheerio": "1.0.0-rc.10", "clone": "2.1.2", "content-type": "1.0.5", - "cookie": "0.5.0", + "cookie": "0.6.0", "cookie-parser": "1.4.6", "cors": "2.8.5", "cronosjs": "1.7.1", "denque": "2.1.0", "express": "4.19.2", - "express-session": "1.17.3", + "express-session": "1.18.0", "form-data": "4.0.0", - "fs-extra": "11.1.1", + "fs-extra": "11.2.0", "got": "12.6.0", "hash-sum": "2.0.0", "hpagent": "1.2.0", @@ -54,35 +54,36 @@ "is-utf8": "0.2.1", "js-yaml": "4.1.0", "json-stringify-safe": "5.0.1", - "jsonata": "1.8.7", + "jsonata": "2.0.5", "lodash.clonedeep": "^4.5.0", "media-typer": "1.1.0", "memorystore": "1.6.7", "mime": "3.0.0", - "moment": "2.29.4", - "moment-timezone": "0.5.43", - "mqtt": "4.3.7", + "moment": "2.30.1", + "moment-timezone": "0.5.45", + "mqtt": "5.7.0", "multer": "1.4.5-lts.1", "mustache": "4.2.0", - "node-red-admin": "^3.1.3", + "node-red-admin": "^4.0.0", "node-watch": "0.7.4", "nopt": "5.0.0", - "oauth2orize": "1.11.1", + "oauth2orize": "1.12.0", "on-headers": "1.0.2", - "passport": "0.6.0", + "passport": "0.7.0", "passport-http-bearer": "1.0.1", "passport-oauth2-client-password": "0.1.2", "raw-body": "2.5.2", + "rfdc": "^1.3.1", "semver": "7.5.4", - "tar": "6.2.1", - "tough-cookie": "4.1.3", + "tar": "7.2.0", + "tough-cookie": "4.1.4", "uglify-js": "3.17.4", - "uuid": "9.0.0", + "uuid": "9.0.1", "ws": "7.5.10", "xml2js": "0.6.2" }, "optionalDependencies": { - "bcrypt": "5.1.1" + "@node-rs/bcrypt": "1.10.4" }, "devDependencies": { "dompurify": "2.4.1", @@ -122,6 +123,6 @@ "supertest": "6.3.3" }, "engines": { - "node": ">=14" + "node": ">=18.5" } } diff --git a/packages/node_modules/@node-red/editor-api/lib/admin/context.js b/packages/node_modules/@node-red/editor-api/lib/admin/context.js index 54bfd9f85..4124b812d 100644 --- a/packages/node_modules/@node-red/editor-api/lib/admin/context.js +++ b/packages/node_modules/@node-red/editor-api/lib/admin/context.js @@ -33,6 +33,9 @@ module.exports = { store: req.query['store'], req: apiUtils.getRequestLogObject(req) } + if (req.query['keysOnly'] !== undefined) { + opts.keysOnly = true + } runtimeAPI.context.getValue(opts).then(function(result) { res.json(result); }).catch(function(err) { diff --git a/packages/node_modules/@node-red/editor-api/lib/admin/index.js b/packages/node_modules/@node-red/editor-api/lib/admin/index.js index 26eabe65b..fa910a399 100644 --- a/packages/node_modules/@node-red/editor-api/lib/admin/index.js +++ b/packages/node_modules/@node-red/editor-api/lib/admin/index.js @@ -91,6 +91,7 @@ module.exports = { // Plugins adminApp.get("/plugins", needsPermission("plugins.read"), plugins.getAll, apiUtil.errorHandler); adminApp.get("/plugins/messages", needsPermission("plugins.read"), plugins.getCatalogs, apiUtil.errorHandler); + adminApp.get(/^\/plugins\/((@[^\/]+\/)?[^\/]+)\/([^\/]+)$/,needsPermission("plugins.read"),plugins.getConfig,apiUtil.errorHandler); adminApp.get("/diagnostics", needsPermission("diagnostics.read"), diagnostics.getReport, apiUtil.errorHandler); diff --git a/packages/node_modules/@node-red/editor-api/lib/admin/plugins.js b/packages/node_modules/@node-red/editor-api/lib/admin/plugins.js index ac6c6f701..304aed90e 100644 --- a/packages/node_modules/@node-red/editor-api/lib/admin/plugins.js +++ b/packages/node_modules/@node-red/editor-api/lib/admin/plugins.js @@ -40,5 +40,31 @@ module.exports = { console.log(err.stack); apiUtils.rejectHandler(req,res,err); }) + }, + getConfig: function(req, res) { + + let opts = { + user: req.user, + module: req.params[0], + req: apiUtils.getRequestLogObject(req) + } + + if (req.get("accept") === "application/json") { + runtimeAPI.nodes.getNodeInfo(opts.module).then(function(result) { + res.send(result); + }).catch(function(err) { + apiUtils.rejectHandler(req,res,err); + }) + } else { + opts.lang = apiUtils.determineLangFromHeaders(req.acceptsLanguages()); + if (/[^0-9a-z=\-\*]/i.test(opts.lang)) { + opts.lang = "en-US"; + } + runtimeAPI.plugins.getPluginConfig(opts).then(function(result) { + return res.send(result); + }).catch(function(err) { + apiUtils.rejectHandler(req,res,err); + }) + } } }; 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 e39e972db..c5e1d93c7 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 @@ -160,20 +160,30 @@ function completeVerify(profile,done) { function genericStrategy(adminApp,strategy) { - var crypto = require("crypto") - var session = require('express-session') - var MemoryStore = require('memorystore')(session) + const crypto = require("crypto") + const session = require('express-session') + const MemoryStore = require('memorystore')(session) - adminApp.use(session({ - // As the session is only used across the life-span of an auth - // hand-shake, we can use a instance specific random string - secret: crypto.randomBytes(20).toString('hex'), - resave: false, - saveUninitialized: false, - store: new MemoryStore({ - checkPeriod: 86400000 // prune expired entries every 24h - }) - })); + const sessionOptions = { + // As the session is only used across the life-span of an auth + // hand-shake, we can use a instance specific random string + secret: crypto.randomBytes(20).toString('hex'), + resave: false, + saveUninitialized: false, + store: new MemoryStore({ + checkPeriod: 86400000 // prune expired entries every 24h + }) + } + if (settings.httpAdminCookieOptions) { + sessionOptions.cookie = { + path: '/', + httpOnly: true, + secure: false, + maxAge: null, + ...settings.httpAdminCookieOptions + } + } + adminApp.use(session(sessionOptions)); //TODO: all passport references ought to be in ./auth adminApp.use(passport.initialize()); adminApp.use(passport.session()); diff --git a/packages/node_modules/@node-red/editor-api/lib/auth/permissions.js b/packages/node_modules/@node-red/editor-api/lib/auth/permissions.js index ba5005ba8..77545a73c 100644 --- a/packages/node_modules/@node-red/editor-api/lib/auth/permissions.js +++ b/packages/node_modules/@node-red/editor-api/lib/auth/permissions.js @@ -25,7 +25,7 @@ function hasPermission(userScope,permission) { } var i; - if (util.isArray(permission)) { + if (Array.isArray(permission)) { // Multiple permissions requested - check each one for (i=0;i { + return res ? cleanUser(user) : null + }).catch(err => { + return null + }) } else { // Try to extract common profile information if (args[0].hasOwnProperty('photos') && args[0].photos.length > 0) { @@ -74,7 +74,7 @@ function init(config) { } else { var us = config.users; /* istanbul ignore else */ - if (!util.isArray(us)) { + if (!Array.isArray(us)) { us = [us]; } for (var i=0;iFailed to load node catalogue.

Check the browser console for more information

", "installFailed": "

Failed to install: __module__

__message__

Check the log for more information

", + "installTimeout": "

Install continuing the background.

Nodes will appear in palette when complete. Check the log for more information.

", "removeFailed": "

Failed to remove: __module__

__message__

Check the log for more information

", "updateFailed": "

Failed to update: __module__

__message__

Check the log for more information

", "enableFailed": "

Failed to enable: __module__

__message__

Check the log for more information

", @@ -655,6 +660,9 @@ "body": "

Removing '__module__'

Removing the node will uninstall it from Node-RED. The node may continue to use resources until Node-RED is restarted.

", "title": "Remove nodes" }, + "removePlugin": { + "body": "

Removed plugin __module__. Please reload the editor to clear left-overs.

" + }, "update": { "body": "

Updating '__module__'

Updating the node will require a restart of Node-RED to complete the update. This must be done manually.

", "title": "Update nodes" @@ -666,7 +674,8 @@ "review": "Open node information", "install": "Install", "remove": "Remove", - "update": "Update" + "update": "Update", + "understood": "Understood" } } } @@ -927,7 +936,14 @@ "date": "timestamp", "jsonata": "expression", "env": "env variable", - "cred": "credential" + "cred": "credential", + "conf-types": "config node" + }, + "date": { + "format": { + "timestamp": "milliseconds since epoch", + "object": "JavaScript Date Object" + } } }, "editableList": { diff --git a/packages/node_modules/@node-red/editor-client/locales/fr/editor.json b/packages/node_modules/@node-red/editor-client/locales/fr/editor.json index f0c801455..bacd5b70f 100644 --- a/packages/node_modules/@node-red/editor-client/locales/fr/editor.json +++ b/packages/node_modules/@node-red/editor-client/locales/fr/editor.json @@ -614,6 +614,8 @@ }, "nodeCount": "__label__ noeud", "nodeCount_plural": "__label__ noeuds", + "pluginCount": "__count__ plugin", + "pluginCount_plural": "__count__ plugins", "moduleCount": "__count__ module disponible", "moduleCount_plural": "__count__ modules disponibles", "inuse": "En cours d'utilisation", @@ -641,6 +643,7 @@ "errors": { "catalogLoadFailed": "

Échec du chargement du catalogue de noeuds.

Vérifier la console du navigateur pour plus d'informations

", "installFailed": "

Échec lors de l'installation : __module__

__message__

Consulter le journal pour plus d'informations

", + "installTimeout": "

L'installation continue en arrière-plan.

Les noeuds apparaîtront dans la palette une fois l'installation terminée. Consulter le journal pour plus d'informations.

", "removeFailed": "

Échec lors de la suppression : __module__

__message__

Consulter le journal pour plus d'informations

", "updateFailed": "

Échec lors de la mise à jour : __module__

__message__

Consulter le journal pour plus d'informations

", "enableFailed": "

Échec lors de l'activation : __module__

__message__

Consulter le journal pour plus d'informations

", @@ -652,9 +655,12 @@ "title": "Installer les noeuds" }, "remove": { - "body": "

Suppression de '__module__'

La suppression du noeud le désinstallera de Node-RED. Le noeud peut continuer à utiliser des ressources jusqu'au redémarrage de Node-RED.

", + "body": "

Suppression de '__module__'

La suppression du noeud le désinstallera de Node-RED. Le noeud peut continuer à utiliser ses ressources jusqu'au redémarrage de Node-RED.

", "title": "Supprimer les noeuds" }, + "removePlugin": { + "body": "

Suppression du plugin '__module__'. Veuillez recharger l'éditeur afin d'appliquer les changements.

" + }, "update": { "body": "

Mise à jour de '__module__'

La mise à jour du noeud nécessitera un redémarrage de Node-RED pour terminer la mise à jour. Cela doit être fait manuellement.

", "title": "Mettre à jour les noeuds" @@ -666,7 +672,8 @@ "review": "Ouvrir la documentation", "install": "Installer", "remove": "Supprimer", - "update": "Mettre à jour" + "update": "Mettre à jour", + "understood": "Compris" } } } @@ -927,7 +934,14 @@ "date": "horodatage", "jsonata": "expression", "env": "variable d'environnement", - "cred": "identifiant" + "cred": "identifiant", + "conf-types": "noeud de configuration" + }, + "date": { + "format": { + "timestamp": "millisecondes depuis l'époque", + "object": "Objet de date JavaScript" + } } }, "editableList": { 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 index b9a835ab9..a2ee81767 100644 --- a/packages/node_modules/@node-red/editor-client/locales/ja/editor.json +++ b/packages/node_modules/@node-red/editor-client/locales/ja/editor.json @@ -372,6 +372,7 @@ "deleted": "削除", "flowDeleted": "削除されたフロー", "flowAdded": "追加されたフロー", + "moved": "移動", "movedTo": "__id__ へ移動", "movedFrom": "__id__ から移動" }, @@ -614,6 +615,8 @@ }, "nodeCount": "__label__ 個のノード", "nodeCount_plural": "__label__ 個のノード", + "pluginCount": "__count__ 個のプラグイン", + "pluginCount_plural": "__count__ 個のプラグイン", "moduleCount": "__count__ 個のモジュール", "moduleCount_plural": "__count__ 個のモジュール", "inuse": "使用中", @@ -641,6 +644,7 @@ "errors": { "catalogLoadFailed": "

ノードのカタログの読み込みに失敗しました。

詳細はブラウザのコンソールを確認してください。

", "installFailed": "

追加処理が失敗しました: __module__

__message__

詳細はログを確認してください。

", + "installTimeout": "

バックグラウンドでインストールが継続されます。

完了した時にノードが表示されます。詳細はログを確認してください。

", "removeFailed": "

削除処理が失敗しました: __module__

__message__

詳細はログを確認してください。

", "updateFailed": "

更新処理が失敗しました: __module__

__message__

詳細はログを確認してください。

", "enableFailed": "

有効化処理が失敗しました: __module__

__message__

詳細はログを確認してください。

", @@ -655,6 +659,9 @@ "body": "

__module__ を削除します。

Node-REDからノードを削除します。ノードはNode-REDが再起動されるまで、リソースを使い続ける可能性があります。

", "title": "ノードを削除" }, + "removePlugin": { + "body": "

プラグイン __module__ を削除しました。ブラウザを再読み込みして残った表示を消してください。

" + }, "update": { "body": "

__module__ を更新します。

更新を完了するには手動でNode-REDを再起動する必要があります。

", "title": "ノードの更新" @@ -666,7 +673,8 @@ "review": "ノードの情報を参照", "install": "追加", "remove": "削除", - "update": "更新" + "update": "更新", + "understood": "了解" } } } @@ -925,7 +933,14 @@ "date": "日時", "jsonata": "JSONata式", "env": "環境変数", - "cred": "認証情報" + "cred": "認証情報", + "conf-types": "設定ノード" + }, + "date": { + "format": { + "timestamp": "エポックからの経過ミリ秒", + "object": "JavaScript日付オブジェクト" + } } }, "editableList": { @@ -1231,7 +1246,7 @@ }, "env-var": { "environment": "環境変数", - "header": "大域環境変数", + "header": "グローバル環境変数", "revert": "破棄" }, "action-list": { @@ -1383,7 +1398,7 @@ "copy-item-edit-url": "要素の編集URLをコピー", "move-flow-to-start": "フローを先頭に移動", "move-flow-to-end": "フローを末尾に移動", - "show-global-env": "大域環境変数を表示", + "show-global-env": "グローバル環境変数を表示", "lock-flow": "フローを固定", "unlock-flow": "フローの固定を解除", "show-node-help": "ノードのヘルプを表示" diff --git a/packages/node_modules/@node-red/editor-client/package.json b/packages/node_modules/@node-red/editor-client/package.json index 5de7559d8..08e8227b1 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": "3.1.11", + "version": "4.0.0", "license": "Apache-2.0", "repository": { "type": "git", diff --git a/packages/node_modules/@node-red/editor-client/src/js/comms.js b/packages/node_modules/@node-red/editor-client/src/js/comms.js index 75a018a56..715572b9d 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/comms.js +++ b/packages/node_modules/@node-red/editor-client/src/js/comms.js @@ -26,6 +26,15 @@ RED.comms = (function() { var reconnectAttempts = 0; var active = false; + RED.events.on('login', function(username) { + // User has logged in + // Need to upgrade the connection to be authenticated + if (ws && ws.readyState == 1) { + const auth_tokens = RED.settings.get("auth-tokens"); + ws.send(JSON.stringify({auth:auth_tokens.access_token})) + } + }) + function connectWS() { active = true; var wspath; @@ -56,6 +65,7 @@ RED.comms = (function() { ws.send(JSON.stringify({subscribe:t})); } } + emit('connect') } ws = new WebSocket(wspath); @@ -180,9 +190,53 @@ RED.comms = (function() { } } + function send(topic, msg) { + if (ws && ws.readyState == 1) { + ws.send(JSON.stringify({ + topic, + data: msg + })) + } + } + + const eventHandlers = {}; + function on(evt,func) { + eventHandlers[evt] = eventHandlers[evt]||[]; + eventHandlers[evt].push(func); + } + function off(evt,func) { + const handler = eventHandlers[evt]; + if (handler) { + for (let i=0;i { + const node = RED.nodes.node(n.id) + if (node) { + inverseEv.changed[n.id] = node.changed + inverseEv.moved[n.id] = node.moved + } + }) RED.nodes.clear(); var imported = RED.nodes.import(ev.config); + // Clear all change flags from the import + RED.nodes.dirty(false); + + const flowsToLock = new Set() + imported.nodes.forEach(function(n) { if (ev.changed[n.id]) { + ensureUnlocked(n.z, flowsToLock) n.changed = true; - inverseEv.changed[n.id] = true; } + if (ev.moved[n.id]) { + ensureUnlocked(n.z, flowsToLock) + n.moved = true; + } + }) + flowsToLock.forEach(flow => { + flow.locked = true }) RED.nodes.version(ev.rev); + RED.view.redraw(true); + RED.palette.refresh(); + RED.workspaces.refresh(); + RED.workspaces.show(selectedTab, true); + RED.sidebar.config.refresh(); } else { var importMap = {}; ev.config.forEach(function(n) { diff --git a/packages/node_modules/@node-red/editor-client/src/js/multiplayer.js b/packages/node_modules/@node-red/editor-client/src/js/multiplayer.js new file mode 100644 index 000000000..ea836eaf4 --- /dev/null +++ b/packages/node_modules/@node-red/editor-client/src/js/multiplayer.js @@ -0,0 +1,490 @@ +RED.multiplayer = (function () { + + // activeSessionId - used to identify sessions across websocket reconnects + let activeSessionId + + let headerWidget + // Map of session id to { session:'', user:{}, location:{}} + let sessions = {} + // Map of username to { user:{}, sessions:[] } + let users = {} + + function addUserSession (session) { + if (sessions[session.session]) { + // This is an existing connection that has been authenticated + const existingSession = sessions[session.session] + if (existingSession.user.username !== session.user.username) { + removeUserHeaderButton(users[existingSession.user.username]) + } + } + sessions[session.session] = session + const user = users[session.user.username] = users[session.user.username] || { + user: session.user, + sessions: [] + } + if (session.user.profileColor === undefined) { + session.user.profileColor = (1 + Math.floor(Math.random() * 5)) + } + session.location = session.location || {} + user.sessions.push(session) + + if (session.session === activeSessionId) { + // This is the current user session - do not add a extra button for them + } else { + if (user.sessions.length === 1) { + if (user.button) { + clearTimeout(user.inactiveTimeout) + clearTimeout(user.removeTimeout) + user.button.removeClass('inactive') + } else { + addUserHeaderButton(user) + } + } + sessions[session.session].location = session.location + updateUserLocation(session.session) + } + } + + function removeUserSession (sessionId, isDisconnected) { + removeUserLocation(sessionId) + const session = sessions[sessionId] + delete sessions[sessionId] + const user = users[session.user.username] + const i = user.sessions.indexOf(session) + user.sessions.splice(i, 1) + if (isDisconnected) { + removeUserHeaderButton(user) + } else { + if (user.sessions.length === 0) { + // Give the user 5s to reconnect before marking inactive + user.inactiveTimeout = setTimeout(() => { + user.button.addClass('inactive') + // Give the user further 20 seconds to reconnect before removing them + // from the user toolbar entirely + user.removeTimeout = setTimeout(() => { + removeUserHeaderButton(user) + }, 20000) + }, 5000) + } + } + } + + function addUserHeaderButton (user) { + user.button = $('
  • ') + .attr('data-username', user.user.username) + .prependTo("#red-ui-multiplayer-user-list"); + var button = user.button.find("button") + RED.popover.tooltip(button, user.user.username) + button.on('click', function () { + const location = user.sessions[0].location + revealUser(location) + }) + + const userProfile = RED.user.generateUserIcon(user.user) + userProfile.appendTo(button) + } + + function removeUserHeaderButton (user) { + user.button.remove() + delete user.button + } + + function getLocation () { + const location = { + workspace: RED.workspaces.active() + } + const editStack = RED.editor.getEditStack() + for (let i = editStack.length - 1; i >= 0; i--) { + if (editStack[i].id) { + location.node = editStack[i].id + break + } + } + return location + } + function publishLocation () { + const location = getLocation() + if (location.workspace !== 0) { + log('send', 'multiplayer/location', location) + RED.comms.send('multiplayer/location', location) + } + } + + function revealUser(location, skipWorkspace) { + if (location.node) { + // Need to check if this is a known node, so we can fall back to revealing + // the workspace instead + const node = RED.nodes.node(location.node) + if (node) { + RED.view.reveal(location.node) + } else if (!skipWorkspace && location.workspace) { + RED.view.reveal(location.workspace) + } + } else if (!skipWorkspace && location.workspace) { + RED.view.reveal(location.workspace) + } + } + + const workspaceTrays = {} + function getWorkspaceTray(workspaceId) { + // console.log('get tray for',workspaceId) + if (!workspaceTrays[workspaceId]) { + const tray = $('
    ') + const users = [] + const userIcons = {} + + const userCountIcon = $(`
    `) + const userCountSpan = userCountIcon.find('span span') + userCountIcon.hide() + userCountSpan.text('') + userCountIcon.appendTo(tray) + const userCountTooltip = RED.popover.tooltip(userCountIcon, function () { + const content = $('
    ') + users.forEach(sessionId => { + $('
    ').append($('').text(sessions[sessionId].user.username).on('click', function (evt) { + evt.preventDefault() + revealUser(sessions[sessionId].location, true) + userCountTooltip.close() + })).appendTo(content) + }) + return content + }, + null, + true + ) + + const updateUserCount = function () { + const maxShown = 2 + const children = tray.children() + children.each(function (index, element) { + const i = users.length - index + if (i > maxShown) { + $(this).hide() + } else if (i >= 0) { + $(this).show() + } + }) + if (users.length < maxShown + 1) { + userCountIcon.hide() + } else { + userCountSpan.text('+'+(users.length - maxShown)) + userCountIcon.show() + } + } + workspaceTrays[workspaceId] = { + attached: false, + tray, + users, + userIcons, + addUser: function (sessionId) { + if (users.indexOf(sessionId) === -1) { + // console.log(`addUser ws:${workspaceId} session:${sessionId}`) + users.push(sessionId) + const userLocationId = `red-ui-multiplayer-user-location-${sessionId}` + const userLocationIcon = $(`
    `) + RED.user.generateUserIcon(sessions[sessionId].user).appendTo(userLocationIcon) + userLocationIcon.prependTo(tray) + RED.popover.tooltip(userLocationIcon, sessions[sessionId].user.username) + userIcons[sessionId] = userLocationIcon + updateUserCount() + } + }, + removeUser: function (sessionId) { + // console.log(`removeUser ws:${workspaceId} session:${sessionId}`) + const userLocationId = `red-ui-multiplayer-user-location-${sessionId}` + const index = users.indexOf(sessionId) + if (index > -1) { + users.splice(index, 1) + userIcons[sessionId].remove() + delete userIcons[sessionId] + } + updateUserCount() + }, + updateUserCount + } + } + const trayDef = workspaceTrays[workspaceId] + if (!trayDef.attached) { + const workspaceTab = $(`#red-ui-tab-${workspaceId}`) + if (workspaceTab.length > 0) { + trayDef.attached = true + trayDef.tray.appendTo(workspaceTab) + trayDef.users.forEach(sessionId => { + trayDef.userIcons[sessionId].on('click', function (evt) { + revealUser(sessions[sessionId].location, true) + }) + }) + } + } + return workspaceTrays[workspaceId] + } + function attachWorkspaceTrays () { + let viewTouched = false + for (let sessionId of Object.keys(sessions)) { + const location = sessions[sessionId].location + if (location) { + if (location.workspace) { + getWorkspaceTray(location.workspace).updateUserCount() + } + if (location.node) { + addUserToNode(sessionId, location.node) + viewTouched = true + } + } + } + if (viewTouched) { + RED.view.redraw() + } + } + + function addUserToNode(sessionId, nodeId) { + const node = RED.nodes.node(nodeId) + if (node) { + if (!node._multiplayer) { + node._multiplayer = { + users: [sessionId] + } + node._multiplayer_refresh = true + } else { + if (node._multiplayer.users.indexOf(sessionId) === -1) { + node._multiplayer.users.push(sessionId) + node._multiplayer_refresh = true + } + } + } + } + function removeUserFromNode(sessionId, nodeId) { + const node = RED.nodes.node(nodeId) + if (node && node._multiplayer) { + const i = node._multiplayer.users.indexOf(sessionId) + if (i > -1) { + node._multiplayer.users.splice(i, 1) + } + if (node._multiplayer.users.length === 0) { + delete node._multiplayer + } else { + node._multiplayer_refresh = true + } + } + + } + + function removeUserLocation (sessionId) { + updateUserLocation(sessionId, {}) + } + function updateUserLocation (sessionId, location) { + let viewTouched = false + const oldLocation = sessions[sessionId].location + if (location) { + if (oldLocation.workspace !== location.workspace) { + // console.log('removing', sessionId, oldLocation.workspace) + workspaceTrays[oldLocation.workspace]?.removeUser(sessionId) + } + if (oldLocation.node !== location.node) { + removeUserFromNode(sessionId, oldLocation.node) + viewTouched = true + } + sessions[sessionId].location = location + } else { + location = sessions[sessionId].location + } + // console.log(`updateUserLocation sessionId:${sessionId} oldWS:${oldLocation?.workspace} newWS:${location.workspace}`) + if (location.workspace) { + getWorkspaceTray(location.workspace).addUser(sessionId) + } + if (location.node) { + addUserToNode(sessionId, location.node) + viewTouched = true + } + if (viewTouched) { + RED.view.redraw() + } + } + + // function refreshUserLocations () { + // for (const session of Object.keys(sessions)) { + // if (session !== activeSessionId) { + // updateUserLocation(session) + // } + // } + // } + + return { + init: function () { + + function createAnnotationUser(user) { + + const group = document.createElementNS("http://www.w3.org/2000/svg","g"); + const badge = document.createElementNS("http://www.w3.org/2000/svg","circle"); + const radius = 20 + badge.setAttribute("cx",radius/2); + badge.setAttribute("cy",radius/2); + badge.setAttribute("r",radius/2); + badge.setAttribute("class", "red-ui-multiplayer-annotation-background") + group.appendChild(badge) + if (user && user.profileColor !== undefined) { + badge.setAttribute("class", "red-ui-multiplayer-annotation-background red-ui-user-profile-color-" + user.profileColor) + } + if (user && user.image) { + const image = document.createElementNS("http://www.w3.org/2000/svg","image"); + image.setAttribute("width", radius) + image.setAttribute("height", radius) + image.setAttribute("href", user.image) + image.setAttribute("clip-path", "circle("+Math.floor(radius/2)+")") + group.appendChild(image) + } else if (user && user.anonymous) { + const anonIconHead = document.createElementNS("http://www.w3.org/2000/svg","circle"); + anonIconHead.setAttribute("cx", radius/2) + anonIconHead.setAttribute("cy", radius/2 - 2) + anonIconHead.setAttribute("r", 2.4) + anonIconHead.setAttribute("class","red-ui-multiplayer-annotation-anon-label"); + group.appendChild(anonIconHead) + const anonIconBody = document.createElementNS("http://www.w3.org/2000/svg","path"); + anonIconBody.setAttribute("class","red-ui-multiplayer-annotation-anon-label"); + // anonIconBody.setAttribute("d",`M ${radius/2 - 4} ${radius/2 + 1} h 8 v4 h -8 z`); + anonIconBody.setAttribute("d",`M ${radius/2} ${radius/2 + 5} h -2.5 c -2 1 -2 -5 0.5 -4.5 c 2 1 2 1 4 0 c 2.5 -0.5 2.5 5.5 0 4.5 z`); + group.appendChild(anonIconBody) + } else { + const labelText = user.username ? user.username.substring(0,2) : user + const label = document.createElementNS("http://www.w3.org/2000/svg","text"); + if (user.username) { + label.setAttribute("class","red-ui-multiplayer-annotation-label"); + label.textContent = user.username.substring(0,2) + } else { + label.setAttribute("class","red-ui-multiplayer-annotation-label red-ui-multiplayer-user-count") + label.textContent = user + } + label.setAttribute("text-anchor", "middle") + label.setAttribute("x",radius/2); + label.setAttribute("y",radius/2 + 3); + group.appendChild(label) + } + const border = document.createElementNS("http://www.w3.org/2000/svg","circle"); + border.setAttribute("cx",radius/2); + border.setAttribute("cy",radius/2); + border.setAttribute("r",radius/2); + border.setAttribute("class", "red-ui-multiplayer-annotation-border") + group.appendChild(border) + + + + return group + } + + RED.view.annotations.register("red-ui-multiplayer",{ + type: 'badge', + align: 'left', + class: "red-ui-multiplayer-annotation", + show: "_multiplayer", + refresh: "_multiplayer_refresh", + element: function(node) { + const containerGroup = document.createElementNS("http://www.w3.org/2000/svg","g"); + containerGroup.setAttribute("transform","translate(0,-4)") + if (node._multiplayer) { + let y = 0 + for (let i = Math.min(1, node._multiplayer.users.length - 1); i >= 0; i--) { + const user = sessions[node._multiplayer.users[i]].user + const group = createAnnotationUser(user) + group.setAttribute("transform","translate("+y+",0)") + y += 15 + containerGroup.appendChild(group) + } + if (node._multiplayer.users.length > 2) { + const group = createAnnotationUser('+'+(node._multiplayer.users.length - 2)) + group.setAttribute("transform","translate("+y+",0)") + y += 12 + containerGroup.appendChild(group) + } + + } + return containerGroup; + }, + tooltip: node => { return node._multiplayer.users.map(u => sessions[u].user.username).join('\n') } + }); + + + // activeSessionId = RED.settings.getLocal('multiplayer:sessionId') + // if (!activeSessionId) { + activeSessionId = RED.nodes.id() + // RED.settings.setLocal('multiplayer:sessionId', activeSessionId) + // log('Session ID (new)', activeSessionId) + // } else { + log('Session ID', activeSessionId) + // } + + headerWidget = $('
    • ').prependTo('.red-ui-header-toolbar') + + RED.comms.on('connect', () => { + const location = getLocation() + const connectInfo = { + session: activeSessionId + } + if (location.workspace !== 0) { + connectInfo.location = location + } + RED.comms.send('multiplayer/connect', connectInfo) + }) + RED.comms.subscribe('multiplayer/#', (topic, msg) => { + log('recv', topic, msg) + if (topic === 'multiplayer/init') { + // We have just reconnected, runtime has sent state to + // initialise the world + sessions = {} + users = {} + $('#red-ui-multiplayer-user-list').empty() + + msg.sessions.forEach(session => { + addUserSession(session) + }) + } else if (topic === 'multiplayer/connection-added') { + addUserSession(msg) + } else if (topic === 'multiplayer/connection-removed') { + removeUserSession(msg.session, msg.disconnected) + } else if (topic === 'multiplayer/location') { + const session = msg.session + delete msg.session + updateUserLocation(session, msg) + } + }) + + RED.events.on('workspace:change', (event) => { + getWorkspaceTray(event.workspace) + publishLocation() + }) + RED.events.on('editor:open', () => { + publishLocation() + }) + RED.events.on('editor:close', () => { + publishLocation() + }) + RED.events.on('editor:change', () => { + publishLocation() + }) + RED.events.on('login', () => { + publishLocation() + }) + RED.events.on('flows:loaded', () => { + attachWorkspaceTrays() + }) + RED.events.on('workspace:close', (event) => { + // A subflow tab has been closed. Need to mark its tray as detached + if (workspaceTrays[event.workspace]) { + workspaceTrays[event.workspace].attached = false + } + }) + RED.events.on('logout', () => { + const disconnectInfo = { + session: activeSessionId + } + RED.comms.send('multiplayer/disconnect', disconnectInfo) + RED.settings.removeLocal('multiplayer:sessionId') + }) + } + } + + function log() { + if (RED.multiplayer.DEBUG) { + console.log('[multiplayer]', ...arguments) + } + } +})(); diff --git a/packages/node_modules/@node-red/editor-client/src/js/nodes.js b/packages/node_modules/@node-red/editor-client/src/js/nodes.js index 3783c804b..2a7b440f2 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/nodes.js +++ b/packages/node_modules/@node-red/editor-client/src/js/nodes.js @@ -91,6 +91,31 @@ RED.nodes = (function() { getNodeTypes: function() { return Object.keys(nodeDefinitions); }, + /** + * Get an array of node definitions + * @param {Object} options - options object + * @param {boolean} [options.configOnly] - if true, only return config nodes + * @param {function} [options.filter] - a filter function to apply to the list of nodes + * @returns array of node definitions + */ + getNodeDefinitions: function(options) { + const result = [] + const configOnly = (options && options.configOnly) + const filter = (options && options.filter) + const keys = Object.keys(nodeDefinitions) + for (const key of keys) { + const def = nodeDefinitions[key] + if(!def) { continue } + if (configOnly && def.category !== "config") { + continue + } + if (filter && !filter(nodeDefinitions[key])) { + continue + } + result.push(nodeDefinitions[key]) + } + return result + }, setNodeList: function(list) { nodeList = []; for(var i=0;i { + addedPlugins.push(p.id); + }) + + RED.i18n.loadNodeCatalog(id, function() { + var lang = localStorage.getItem("editor-language")||RED.i18n.detectLanguage(); + $.ajax({ + headers: { + "Accept":"text/html", + "Accept-Language": lang + }, + cache: false, + url: 'plugins/'+id, + success: function(data) { + appendPluginConfig(data); + } + }); + }); + }); + if (addedPlugins.length) { + let pluginList = "
      • "+addedPlugins.map(RED.utils.sanitize).join("
      • ")+"
      "; + // ToDo: Adapt notification (node -> plugin) + RED.notify(RED._("palette.event.nodeAdded", {count:addedPlugins.length})+pluginList,"success"); + } + }) + } + }); let pendingNodeRemovedNotifications = [] let pendingNodeRemovedTimeout @@ -803,6 +840,10 @@ var RED = (function() { RED.nodes.init(); RED.runtime.init() + + if (RED.settings.theme("multiplayer.enabled",false)) { + RED.multiplayer.init() + } RED.comms.connect(); $("#red-ui-main-container").show(); 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 690968338..4e16bd3f6 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 @@ -26,6 +26,7 @@ RED.clipboard = (function() { var currentPopoverError; var activeTab; var libraryBrowser; + var clipboardTabs; var activeLibraries = {}; @@ -215,6 +216,13 @@ RED.clipboard = (function() { open: function( event, ui ) { RED.keyboard.disable(); }, + beforeClose: function(e) { + if (clipboardTabs && activeTab === "red-ui-clipboard-dialog-export-tab-clipboard") { + const jsonTabIndex = clipboardTabs.getTabIndex('red-ui-clipboard-dialog-export-tab-clipboard-json') + const activeTabIndex = clipboardTabs.activeIndex() + RED.settings.set("editor.dialog.export.json-view", activeTabIndex === jsonTabIndex ) + } + }, close: function(e) { RED.keyboard.enable(); if (popover) { @@ -228,12 +236,23 @@ RED.clipboard = (function() { exportNodesDialog = '
      '+ - ''+ - ''+ - ''+ - ''+ - ''+ - ''+ + '
      '+ + '
      '+ + ''+ + ''+ + ''+ + ''+ + ''+ + ''+ + '
      '+ + '
      '+ + ''+ + ''+ + ''+ + ''+ + ''+ + '
      '+ + '
      '+ '
      '+ '
      '+ '
      '+ @@ -248,15 +267,9 @@ RED.clipboard = (function() { '
      '+ '
      '+ '
      '+ - '
      '+ + '
      '+ ''+ '
      '+ - '
      '+ - ''+ - ''+ - ''+ - ''+ - '
      '+ '
      '+ '
      '+ '
      '+ @@ -569,7 +582,7 @@ RED.clipboard = (function() { dialogContainer.empty(); dialogContainer.append($(exportNodesDialog)); - + clipboardTabs = null var tabs = RED.tabs.create({ id: "red-ui-clipboard-dialog-export-tabs", vertical: true, @@ -630,7 +643,7 @@ RED.clipboard = (function() { $("#red-ui-clipboard-dialog-tab-library-name").on('paste',function() { setTimeout(validateExportFilename,10)}); $("#red-ui-clipboard-dialog-export").button("enable"); - var clipboardTabs = RED.tabs.create({ + clipboardTabs = RED.tabs.create({ id: "red-ui-clipboard-dialog-export-tab-clipboard-tabs", onchange: function(tab) { $(".red-ui-clipboard-dialog-export-tab-clipboard-tab").hide(); @@ -647,6 +660,9 @@ RED.clipboard = (function() { id: "red-ui-clipboard-dialog-export-tab-clipboard-json", label: RED._("editor.types.json") }); + if (RED.settings.get("editor.dialog.export.json-view") === true) { + clipboardTabs.activateTab("red-ui-clipboard-dialog-export-tab-clipboard-json"); + } var previewList = $("#red-ui-clipboard-dialog-export-tab-clipboard-preview-list").css({position:"absolute",top:0,right:0,bottom:0,left:0}).treeList({ data: [] diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/common/editableList.js b/packages/node_modules/@node-red/editor-client/src/js/ui/common/editableList.js index 8ee1f0e29..ab9c8a837 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/common/editableList.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/common/editableList.js @@ -174,12 +174,24 @@ this.uiContainer.width(m[1]); } if (this.options.sortable) { + var isCanceled = false; // Flag to track if an item has been canceled from being dropped into a different list + var noDrop = false; // Flag to track if an item is being dragged into a different list var handle = (typeof this.options.sortable === 'string')? this.options.sortable : ".red-ui-editableList-item-handle"; var sortOptions = { axis: "y", update: function( event, ui ) { + // dont trigger update if the item is being canceled + const targetList = $(event.target); + const draggedItem = ui.item; + const draggedItemParent = draggedItem.parent(); + if (!targetList.is(draggedItemParent) && draggedItem.hasClass("red-ui-editableList-item-constrained")) { + noDrop = true; + } + if (isCanceled || noDrop) { + return; + } if (that.options.sortItems) { that.options.sortItems(that.items()); } @@ -189,8 +201,32 @@ tolerance: "pointer", forcePlaceholderSize:true, placeholder: "red-ui-editabelList-item-placeholder", - start: function(e, ui){ - ui.placeholder.height(ui.item.height()-4); + start: function (event, ui) { + isCanceled = false; + ui.placeholder.height(ui.item.height() - 4); + ui.item.css('cursor', 'grabbing'); // TODO: this doesn't seem to work, use a class instead? + }, + stop: function (event, ui) { + ui.item.css('cursor', 'auto'); + }, + receive: function (event, ui) { + if (ui.item.hasClass("red-ui-editableList-item-constrained")) { + isCanceled = true; + $(ui.sender).sortable('cancel'); + } + }, + over: function (event, ui) { + // if the dragged item is constrained, prevent it from being dropped into a different list + const targetList = $(event.target); + const draggedItem = ui.item; + const draggedItemParent = draggedItem.parent(); + if (!targetList.is(draggedItemParent) && draggedItem.hasClass("red-ui-editableList-item-constrained")) { + noDrop = true; + draggedItem.css('cursor', 'no-drop'); // TODO: this doesn't seem to work, use a class instead? + } else { + noDrop = false; + draggedItem.css('cursor', 'grabbing'); // TODO: this doesn't seem to work, use a class instead? + } } }; if (this.options.connectWith) { diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/common/popover.js b/packages/node_modules/@node-red/editor-client/src/js/ui/common/popover.js index 9ddd3d866..381bb9d3a 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/common/popover.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/common/popover.js @@ -211,7 +211,7 @@ RED.popover = (function() { closePopup(true); }); } - if (trigger === 'hover' && options.interactive) { + if (/*trigger === 'hover' && */options.interactive) { div.on('mouseenter', function(e) { clearTimeout(timer); active = true; @@ -445,9 +445,12 @@ RED.popover = (function() { return { create: createPopover, - tooltip: function(target,content, action) { + tooltip: function(target,content, action, interactive) { var label = function() { var label = content; + if (typeof content === 'function') { + label = content() + } if (action) { var shortcut = RED.keyboard.getShortcut(action); if (shortcut && shortcut.key) { @@ -463,6 +466,7 @@ RED.popover = (function() { size: "small", direction: "bottom", content: label, + interactive, delay: { show: 750, hide: 50 } }); popover.setContent = function(newContent) { diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/common/tabs.js b/packages/node_modules/@node-red/editor-client/src/js/ui/common/tabs.js index abb76e622..d9dc4b289 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/common/tabs.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/common/tabs.js @@ -365,7 +365,10 @@ RED.tabs = (function() { var thisTabA = thisTab.find("a"); if (options.onclick) { - options.onclick(tabs[thisTabA.attr('href').slice(1)]); + options.onclick(tabs[thisTabA.attr('href').slice(1)], evt); + if (evt.isDefaultPrevented() && evt.isPropagationStopped()) { + return false + } } activateTab(thisTabA); if (fireSelectionChanged) { @@ -548,6 +551,8 @@ RED.tabs = (function() { ul.find("li.red-ui-tab a") .on("mousedown", function(evt) { mousedownTab = evt.currentTarget }) .on("mouseup",onTabClick) + // prevent browser-default middle-click behaviour + .on("auxclick", function(evt) { evt.preventDefault() }) .on("click", function(evt) {evt.preventDefault(); }) .on("dblclick", function(evt) {evt.stopPropagation(); evt.preventDefault(); }) @@ -816,6 +821,8 @@ RED.tabs = (function() { } link.on("mousedown", function(evt) { mousedownTab = evt.currentTarget }) link.on("mouseup",onTabClick); + // prevent browser-default middle-click behaviour + link.on("auxclick", function(evt) { evt.preventDefault() }) link.on("click", function(evt) { evt.preventDefault(); }) link.on("dblclick", function(evt) { evt.stopPropagation(); evt.preventDefault(); }) diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/common/typedInput.js b/packages/node_modules/@node-red/editor-client/src/js/ui/common/typedInput.js index 3071e6f64..47355565f 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/common/typedInput.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/common/typedInput.js @@ -54,25 +54,26 @@ return icon; } - var autoComplete = function(options) { - function getMatch(value, searchValue) { - const idx = value.toLowerCase().indexOf(searchValue.toLowerCase()); - const len = idx > -1 ? searchValue.length : 0; - return { - index: idx, - found: idx > -1, - pre: value.substring(0,idx), - match: value.substring(idx,idx+len), - post: value.substring(idx+len), - } - } - function generateSpans(match) { - const els = []; - if(match.pre) { els.push($('').text(match.pre)); } - if(match.match) { els.push($('',{style:"font-weight: bold; color: var(--red-ui-text-color-link);"}).text(match.match)); } - if(match.post) { els.push($('').text(match.post)); } - return els; + function getMatch(value, searchValue) { + const idx = value.toLowerCase().indexOf(searchValue.toLowerCase()); + const len = idx > -1 ? searchValue.length : 0; + return { + index: idx, + found: idx > -1, + pre: value.substring(0,idx), + match: value.substring(idx,idx+len), + post: value.substring(idx+len), } + } + function generateSpans(match) { + const els = []; + if(match.pre) { els.push($('').text(match.pre)); } + if(match.match) { els.push($('',{style:"font-weight: bold; color: var(--red-ui-text-color-link);"}).text(match.match)); } + if(match.post) { els.push($('').text(match.post)); } + return els; + } + + const msgAutoComplete = function(options) { return function(val) { var matches = []; options.forEach(opt => { @@ -102,6 +103,197 @@ } } + function getEnvVars (obj, envVars = {}) { + contextKnownKeys.env = contextKnownKeys.env || {} + if (contextKnownKeys.env[obj.id]) { + return contextKnownKeys.env[obj.id] + } + let parent + if (obj.type === 'tab' || obj.type === 'subflow') { + RED.nodes.eachConfig(function (conf) { + if (conf.type === "global-config") { + parent = conf; + } + }) + } else if (obj.g) { + parent = RED.nodes.group(obj.g) + } else if (obj.z) { + parent = RED.nodes.workspace(obj.z) || RED.nodes.subflow(obj.z) + } + if (parent) { + getEnvVars(parent, envVars) + } + if (obj.env) { + obj.env.forEach(env => { + envVars[env.name] = obj + }) + } + contextKnownKeys.env[obj.id] = envVars + return envVars + } + + const envAutoComplete = function (val) { + const editStack = RED.editor.getEditStack() + if (editStack.length === 0) { + done([]) + return + } + const editingNode = editStack.pop() + if (!editingNode) { + return [] + } + const envVarsMap = getEnvVars(editingNode) + const envVars = Object.keys(envVarsMap) + const matches = [] + const i = val.lastIndexOf('${') + let searchKey = val + let isSubkey = false + if (i > -1) { + if (val.lastIndexOf('}') < i) { + searchKey = val.substring(i+2) + isSubkey = true + } + } + envVars.forEach(v => { + let valMatch = getMatch(v, searchKey); + if (valMatch.found) { + const optSrc = envVarsMap[v] + const element = $('
      ',{style: "display: flex"}); + const valEl = $('
      ',{style:"font-family: var(--red-ui-monospace-font); white-space:nowrap; overflow: hidden; flex-grow:1"}); + valEl.append(generateSpans(valMatch)) + valEl.appendTo(element) + + if (optSrc) { + const optEl = $('
      ').css({ "font-size": "0.8em" }); + let label + if (optSrc.type === 'global-config') { + label = RED._('sidebar.context.global') + } else if (optSrc.type === 'group') { + label = RED.utils.getNodeLabel(optSrc) || (RED._('sidebar.info.group') + ': '+optSrc.id) + } else { + label = RED.utils.getNodeLabel(optSrc) || optSrc.id + } + + optEl.append(generateSpans({ match: label })); + optEl.appendTo(element); + } + matches.push({ + value: isSubkey ? val + v + '}' : v, + label: element, + i: valMatch.index + }); + } + }) + matches.sort(function(A,B){return A.i-B.i}) + return matches + } + + let contextKnownKeys = {} + let contextCache = {} + if (RED.events) { + RED.events.on("editor:close", function () { + contextCache = {} + contextKnownKeys = {} + }); + } + + const contextAutoComplete = function() { + const that = this + const getContextKeysFromRuntime = function(scope, store, searchKey, done) { + contextKnownKeys[scope] = contextKnownKeys[scope] || {} + contextKnownKeys[scope][store] = contextKnownKeys[scope][store] || new Set() + if (searchKey.length > 0) { + try { + RED.utils.normalisePropertyExpression(searchKey) + } catch (err) { + // Not a valid context key, so don't try looking up + done() + return + } + } + const url = `context/${scope}/${encodeURIComponent(searchKey)}?store=${store}&keysOnly` + if (contextCache[url]) { + // console.log('CACHED', url) + done() + } else { + // console.log('GET', url) + $.getJSON(url, function(data) { + // console.log(data) + contextCache[url] = true + const result = data[store] || {} + const keys = result.keys || [] + const keyPrefix = searchKey + (searchKey.length > 0 ? '.' : '') + keys.forEach(key => { + if (/^[a-zA-Z_$][0-9a-zA-Z_$]*$/.test(key)) { + contextKnownKeys[scope][store].add(keyPrefix + key) + } else { + contextKnownKeys[scope][store].add(searchKey + "[\""+key.replace(/"/,"\\\"")+"\"]") + } + }) + done() + }) + } + } + const getContextKeys = function(key, done) { + const keyParts = key.split('.') + const partialKey = keyParts.pop() + let scope = that.propertyType + if (scope === 'flow') { + // Get the flow id of the node we're editing + const editStack = RED.editor.getEditStack() + if (editStack.length === 0) { + done([]) + return + } + const editingNode = editStack.pop() + if (editingNode.z) { + scope = `${scope}/${editingNode.z}` + } else { + done([]) + return + } + } + const store = (contextStoreOptions.length === 1) ? contextStoreOptions[0].value : that.optionValue + const searchKey = keyParts.join('.') + + getContextKeysFromRuntime(scope, store, searchKey, function() { + if (contextKnownKeys[scope][store].has(key) || key.endsWith(']')) { + getContextKeysFromRuntime(scope, store, key, function() { + done(contextKnownKeys[scope][store]) + }) + } + done(contextKnownKeys[scope][store]) + }) + } + + return function(val, done) { + getContextKeys(val, function (keys) { + const matches = [] + keys.forEach(v => { + let optVal = v + let valMatch = getMatch(optVal, val); + if (!valMatch.found && val.length > 0 && val.endsWith('.')) { + // Search key ends in '.' - but doesn't match. Check again + // with [" at the end instead so we match bracket notation + valMatch = getMatch(optVal, val.substring(0, val.length - 1) + '["') + } + if (valMatch.found) { + const element = $('
      ',{style: "display: flex"}); + const valEl = $('
      ',{style:"font-family: var(--red-ui-monospace-font); white-space:nowrap; overflow: hidden; flex-grow:1"}); + valEl.append(generateSpans(valMatch)) + valEl.appendTo(element) + matches.push({ + value: optVal, + label: element, + }); + } + }) + matches.sort(function(a, b) { return a.value.localeCompare(b.value) }); + done(matches); + }) + } + } + // This is a hand-generated list of completions for the core nodes (based on the node help html). var msgCompletions = [ { value: "payload" }, @@ -166,68 +358,93 @@ { value: "_session", source: ["websocket out","tcp out"] }, ] var allOptions = { - msg: {value:"msg",label:"msg.",validate:RED.utils.validatePropertyExpression, autoComplete: autoComplete(msgCompletions)}, - flow: {value:"flow",label:"flow.",hasValue:true, - options:[], - validate:RED.utils.validatePropertyExpression, + msg: { value: "msg", label: "msg.", validate: RED.utils.validatePropertyExpression, autoComplete: msgAutoComplete(msgCompletions) }, + flow: { value: "flow", label: "flow.", hasValue: true, + options: [], + validate: RED.utils.validatePropertyExpression, parse: contextParse, export: contextExport, - valueLabel: contextLabel + valueLabel: contextLabel, + autoComplete: contextAutoComplete }, - global: {value:"global",label:"global.",hasValue:true, - options:[], - validate:RED.utils.validatePropertyExpression, + global: { + value: "global", label: "global.", hasValue: true, + options: [], + validate: RED.utils.validatePropertyExpression, parse: contextParse, export: contextExport, - valueLabel: contextLabel + valueLabel: contextLabel, + autoComplete: contextAutoComplete }, - str: {value:"str",label:"string",icon:"red/images/typedInput/az.svg"}, - num: {value:"num",label:"number",icon:"red/images/typedInput/09.svg",validate: function(v) { - return (true === RED.utils.validateTypedProperty(v, "num")); + str: { value: "str", label: "string", icon: "red/images/typedInput/az.svg" }, + num: { value: "num", label: "number", icon: "red/images/typedInput/09.svg", validate: function (v, o) { + return RED.utils.validateTypedProperty(v, "num", o); } }, - bool: {value:"bool",label:"boolean",icon:"red/images/typedInput/bool.svg",options:["true","false"]}, + bool: { value: "bool", label: "boolean", icon: "red/images/typedInput/bool.svg", options: ["true", "false"] }, json: { - value:"json", - label:"JSON", - icon:"red/images/typedInput/json.svg", - validate: function(v) { try{JSON.parse(v);return true;}catch(e){return false;}}, - expand: function() { + value: "json", + label: "JSON", + icon: "red/images/typedInput/json.svg", + validate: function (v, o) { + return RED.utils.validateTypedProperty(v, "json", o); + }, + expand: function () { var that = this; var value = this.value(); try { - value = JSON.stringify(JSON.parse(value),null,4); - } catch(err) { + value = JSON.stringify(JSON.parse(value), null, 4); + } catch (err) { } RED.editor.editJSON({ value: value, stateId: RED.editor.generateViewStateId("typedInput", that, "json"), focus: true, - complete: function(v) { + complete: function (v) { var value = v; try { value = JSON.stringify(JSON.parse(v)); - } catch(err) { + } catch (err) { } that.value(value); } }) } }, - re: {value:"re",label:"regular expression",icon:"red/images/typedInput/re.svg"}, - date: {value:"date",label:"timestamp",icon:"fa fa-clock-o",hasValue:false}, + re: { value: "re", label: "regular expression", icon: "red/images/typedInput/re.svg" }, + date: { + value: "date", + label: "timestamp", + icon: "fa fa-clock-o", + options: [ + { + label: 'milliseconds since epoch', + value: '' + }, + { + label: 'YYYY-MM-DDTHH:mm:ss.sssZ', + value: 'iso' + }, + { + label: 'JavaScript Date Object', + value: 'object' + } + ] + }, jsonata: { value: "jsonata", label: "expression", icon: "red/images/typedInput/expr.svg", - validate: function(v) { try{jsonata(v);return true;}catch(e){return false;}}, - expand:function() { + validate: function (v, o) { + return RED.utils.validateTypedProperty(v, "jsonata", o); + }, + expand: function () { var that = this; RED.editor.editExpression({ - value: this.value().replace(/\t/g,"\n"), + value: this.value().replace(/\t/g, "\n"), stateId: RED.editor.generateViewStateId("typedInput", that, "jsonata"), focus: true, - complete: function(v) { - that.value(v.replace(/\n/g,"\t")); + complete: function (v) { + that.value(v.replace(/\n/g, "\t")); } }) } @@ -236,13 +453,13 @@ value: "bin", label: "buffer", icon: "red/images/typedInput/bin.svg", - expand: function() { + expand: function () { var that = this; RED.editor.editBuffer({ value: this.value(), stateId: RED.editor.generateViewStateId("typedInput", that, "bin"), focus: true, - complete: function(v) { + complete: function (v) { that.value(v); } }) @@ -251,15 +468,16 @@ env: { value: "env", label: "env variable", - icon: "red/images/typedInput/env.svg" + icon: "red/images/typedInput/env.svg", + autoComplete: envAutoComplete }, node: { value: "node", label: "node", icon: "red/images/typedInput/target.svg", - valueLabel: function(container,value) { + valueLabel: function (container, value) { var node = RED.nodes.node(value); - var nodeDiv = $('
      ',{class:"red-ui-search-result-node"}).css({ + var nodeDiv = $('
      ', { class: "red-ui-search-result-node" }).css({ "margin-top": "2px", "margin-left": "3px" }).appendTo(container); @@ -268,133 +486,190 @@ "margin-left": "6px" }).appendTo(container); if (node) { - var colour = RED.utils.getNodeColor(node.type,node._def); - var icon_url = RED.utils.getNodeIcon(node._def,node); + var colour = RED.utils.getNodeColor(node.type, node._def); + var icon_url = RED.utils.getNodeIcon(node._def, node); if (node.type === 'tab') { colour = "#C0DEED"; } - nodeDiv.css('backgroundColor',colour); - var iconContainer = $('
      ',{class:"red-ui-palette-icon-container"}).appendTo(nodeDiv); + nodeDiv.css('backgroundColor', colour); + var iconContainer = $('
      ', { class: "red-ui-palette-icon-container" }).appendTo(nodeDiv); RED.utils.createIconElement(icon_url, iconContainer, true); - var l = RED.utils.getNodeLabel(node,node.id); + var l = RED.utils.getNodeLabel(node, node.id); nodeLabel.text(l); } else { nodeDiv.css({ 'backgroundColor': '#eee', - 'border-style' : 'dashed' + 'border-style': 'dashed' }); } }, - expand: function() { + expand: function () { var that = this; RED.tray.hide(); RED.view.selectNodes({ single: true, selected: [that.value()], - onselect: function(selection) { + onselect: function (selection) { that.value(selection.id); RED.tray.show(); }, - oncancel: function() { + oncancel: function () { RED.tray.show(); } }) } }, - cred:{ - value:"cred", - label:"credential", - icon:"fa fa-lock", + cred: { + value: "cred", + label: "credential", + icon: "fa fa-lock", inputType: "password", - valueLabel: function(container,value) { + valueLabel: function (container, value) { var that = this; - container.css("pointer-events","none"); - container.css("flex-grow",0); + container.css("pointer-events", "none"); + container.css("flex-grow", 0); this.elementDiv.hide(); var buttons = $('
      ').css({ position: "absolute", - right:"6px", + right: "6px", top: "6px", - "pointer-events":"all" + "pointer-events": "all" }).appendTo(container); var eyeButton = $('').css({ - width:"20px" - }).appendTo(buttons).on("click", function(evt) { + width: "20px" + }).appendTo(buttons).on("click", function (evt) { evt.preventDefault(); var cursorPosition = that.input[0].selectionStart; var currentType = that.input.attr("type"); if (currentType === "text") { - that.input.attr("type","password"); + that.input.attr("type", "password"); eyeCon.removeClass("fa-eye-slash").addClass("fa-eye"); - setTimeout(function() { + setTimeout(function () { that.input.focus(); that.input[0].setSelectionRange(cursorPosition, cursorPosition); - },50); + }, 50); } else { - that.input.attr("type","text"); + that.input.attr("type", "text"); eyeCon.removeClass("fa-eye").addClass("fa-eye-slash"); - setTimeout(function() { + setTimeout(function () { that.input.focus(); that.input[0].setSelectionRange(cursorPosition, cursorPosition); - },50); + }, 50); } }).hide(); - var eyeCon = $('').css("margin-left","-2px").appendTo(eyeButton); + var eyeCon = $('').css("margin-left", "-2px").appendTo(eyeButton); if (value === "__PWRD__") { var innerContainer = $('
      ').css({ - padding:"6px 6px", - borderRadius:"4px" + padding: "6px 6px", + borderRadius: "4px" }).addClass("red-ui-typedInput-value-label-inactive").appendTo(container); - var editButton = $('').appendTo(buttons).on("click", function(evt) { + var editButton = $('').appendTo(buttons).on("click", function (evt) { evt.preventDefault(); innerContainer.hide(); - container.css("background","none"); - container.css("pointer-events","none"); + 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() { + setTimeout(function () { that.input.focus(); - },50); + }, 50); }); - var cancelButton = $('').css("margin-left","3px").appendTo(buttons).on("click", function(evt) { + var cancelButton = $('').css("margin-left", "3px").appendTo(buttons).on("click", function (evt) { evt.preventDefault(); innerContainer.show(); - container.css("background",""); + 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"); + that.input.attr("type", "password"); eyeCon.removeClass("fa-eye-slash").addClass("fa-eye"); }).hide(); } else { - container.css("background","none"); - container.css("pointer-events","none"); + container.css("background", "none"); + container.css("pointer-events", "none"); this.elementDiv.show(); eyeButton.show(); } } + }, + 'conf-types': { + value: "conf-types", + label: "config", + icon: "fa fa-cog", + // hasValue: false, + valueLabel: function (container, value) { + // get the selected option (for access to the "name" and "module" properties) + const _options = this._optionsCache || this.typeList.find(opt => opt.value === value)?.options || [] + const selectedOption = _options.find(opt => opt.value === value) || { + title: '', + name: '', + module: '' + } + container.attr("title", selectedOption.title) // set tooltip to the full path/id of the module/node + container.text(selectedOption.name) // apply the "name" of the selected option + // set "line-height" such as to make the "name" appear further up, giving room for the "module" to be displayed below the value + container.css("line-height", "1.4em") + // add the module name in smaller, lighter font below the value + $('
      ').text(selectedOption.module).css({ + // "font-family": "var(--red-ui-monospace-font)", + color: "var(--red-ui-tertiary-text-color)", + "font-size": "0.8em", + "line-height": "1em", + opacity: 0.8 + }).appendTo(container); + }, + // hasValue: false, + options: function () { + if (this._optionsCache) { + return this._optionsCache + } + const configNodes = RED.nodes.registry.getNodeDefinitions({configOnly: true, filter: (def) => def.type !== "global-config"}).map((def) => { + // create a container with with 2 rows (row 1 for the name, row 2 for the module name in smaller, lighter font) + const container = $('
      ') + const row1Name = $('
      ').text(def.type) + const row2Module = $('
      ').text(def.set.module) + container.append(row1Name, row2Module) + + return { + value: def.type, + name: def.type, + enabled: def.set.enabled ?? true, + local: def.set.local, + title: def.set.id, // tooltip e.g. "node-red-contrib-foo/bar" + module: def.set.module, + icon: container[0].outerHTML.trim(), // the typeInput will interpret this as html text and render it in the anchor + } + }) + this._optionsCache = configNodes + return configNodes + } } }; + // For a type with options, check value is a valid selection // If !opt.multiple, returns the valid option object // if opt.multiple, returns an array of valid option objects // If not valid, returns null; function isOptionValueValid(opt, currentVal) { + let _options = opt.options + if (typeof _options === "function") { + _options = _options.call(this) + } if (!opt.multiple) { - for (var i=0;i'} }).sort(function(A,B) { if (A.value === RED.settings.context.default) { @@ -449,13 +725,17 @@ return A.value.localeCompare(B.value); } }) - if (contextOptions.length < 2) { + if (contextStoreOptions.length < 2) { allOptions.flow.options = []; allOptions.global.options = []; } else { - allOptions.flow.options = contextOptions; - allOptions.global.options = contextOptions; + allOptions.flow.options = contextStoreOptions; + allOptions.global.options = contextStoreOptions; } + // Translate timestamp options + allOptions.date.options.forEach(opt => { + opt.label = RED._("typedInput.date.format." + (opt.value || 'timestamp'), {defaultValue: opt.label}) + }) } nlsd = true; var that = this; @@ -544,7 +824,7 @@ that.element.trigger('paste',evt); }); this.input.on('keydown', function(evt) { - if (that.typeMap[that.propertyType].autoComplete) { + if (that.typeMap[that.propertyType].autoComplete || that.input.hasClass('red-ui-autoComplete')) { return } if (evt.keyCode >= 37 && evt.keyCode <= 40) { @@ -838,7 +1118,9 @@ if (this.optionMenu) { this.optionMenu.remove(); } - this.menu.remove(); + if (this.menu) { + this.menu.remove(); + } this.uiSelect.remove(); }, types: function(types) { @@ -871,7 +1153,7 @@ this.menu = this._createMenu(this.typeList,{},function(v) { that.type(v) }); if (currentType && !this.typeMap.hasOwnProperty(currentType)) { if (!firstCall) { - this.type(this.typeList[0].value); + this.type(this.typeList[0]?.value || ""); // permit empty typeList } } else { this.propertyType = null; @@ -908,6 +1190,11 @@ var selectedOption = []; var valueToCheck = value; if (opt.options) { + let _options = opt.options + if (typeof opt.options === "function") { + _options = opt.options.call(this) + } + if (opt.hasValue && opt.parse) { var parts = opt.parse(value); if (this.options.debug) { console.log(this.identifier,"new parse",parts) } @@ -921,8 +1208,8 @@ checkValues = valueToCheck.split(","); } checkValues.forEach(function(valueToCheck) { - for (var i=0;i'+ ''+ ''+ - ''+ + ''+ '').prependTo(".red-ui-header-toolbar"); const mainMenuItems = [ {id:"deploymenu-item-full",toggle:"deploy-type",icon:"red/images/deploy-full.svg",label:RED._("deploy.full"),sublabel:RED._("deploy.fullDesc"),selected: true, onselect:function(s) { if(s){changeDeploymentType("full")}}}, @@ -112,53 +114,80 @@ RED.deploy = (function() { RED.actions.add("core:set-deploy-type-to-modified-nodes",function() { RED.menu.setSelected("deploymenu-item-node",true); }); } - + window.addEventListener('beforeunload', function (event) { + if (RED.nodes.dirty()) { + event.preventDefault(); + event.stopImmediatePropagation() + event.returnValue = RED._("deploy.confirm.undeployedChanges"); + return + } + }) RED.events.on('workspace:dirty',function(state) { + if (RED.settings.user?.permissions === 'read') { + return + } if (state.dirty) { - window.onbeforeunload = function() { - return RED._("deploy.confirm.undeployedChanges"); - } + // window.onbeforeunload = function() { + // return + // } $("#red-ui-header-button-deploy").removeClass("disabled"); } else { - window.onbeforeunload = null; + // window.onbeforeunload = null; $("#red-ui-header-button-deploy").addClass("disabled"); } }); - var activeNotifyMessage; RED.comms.subscribe("notification/runtime-deploy",function(topic,msg) { - if (!activeNotifyMessage) { - var currentRev = RED.nodes.version(); - if (currentRev === null || deployInflight || currentRev === msg.revision) { - return; - } - var message = $('

      ').text(RED._('deploy.confirm.backgroundUpdate')); - activeNotifyMessage = RED.notify(message,{ - modal: true, - fixed: true, - buttons: [ - { - text: RED._('deploy.confirm.button.ignore'), - click: function() { - activeNotifyMessage.close(); - activeNotifyMessage = null; - } - }, - { - text: RED._('deploy.confirm.button.review'), - class: "primary", - click: function() { - activeNotifyMessage.close(); - var nns = RED.nodes.createCompleteNodeSet(); - resolveConflict(nns,false); - activeNotifyMessage = null; - } + var currentRev = RED.nodes.version(); + if (currentRev === null || deployInflight || currentRev === msg.revision) { + return; + } + if (activeBackgroundDeployNotification?.hidden && !activeBackgroundDeployNotification?.closed) { + activeBackgroundDeployNotification.showNotification() + return + } + const message = $('

      ').text(RED._('deploy.confirm.backgroundUpdate')); + const options = { + id: 'background-update', + type: 'compact', + modal: false, + fixed: true, + timeout: 10000, + buttons: [ + { + text: RED._('deploy.confirm.button.review'), + class: "primary", + click: function() { + activeBackgroundDeployNotification.hideNotification(); + var nns = RED.nodes.createCompleteNodeSet(); + resolveConflict(nns,false); } - ] - }); + } + ] + } + if (!activeBackgroundDeployNotification || activeBackgroundDeployNotification.closed) { + activeBackgroundDeployNotification = RED.notify(message, options) + } else { + activeBackgroundDeployNotification.update(message, options) } }); + + + updateLockedState() + RED.events.on('login', updateLockedState) + } + + function updateLockedState() { + if (RED.settings.user?.permissions === 'read') { + $(".red-ui-deploy-button-group").addClass("readOnly"); + $("#red-ui-header-button-deploy").addClass("disabled"); + } else { + $(".red-ui-deploy-button-group").removeClass("readOnly"); + if (RED.nodes.dirty()) { + $("#red-ui-header-button-deploy").removeClass("disabled"); + } + } } function getNodeInfo(node) { @@ -213,7 +242,11 @@ RED.deploy = (function() { class: "primary disabled", click: function() { if (!$("#red-ui-deploy-dialog-confirm-deploy-review").hasClass('disabled')) { - RED.diff.showRemoteDiff(); + RED.diff.showRemoteDiff(null, { + onmerge: function () { + activeBackgroundDeployNotification.close() + } + }); conflictNotification.close(); } } @@ -226,6 +259,7 @@ RED.deploy = (function() { if (!$("#red-ui-deploy-dialog-confirm-deploy-merge").hasClass('disabled')) { RED.diff.mergeDiff(currentDiff); conflictNotification.close(); + activeBackgroundDeployNotification.close() } } } @@ -238,6 +272,7 @@ RED.deploy = (function() { click: function() { save(true,activeDeploy); conflictNotification.close(); + activeBackgroundDeployNotification.close() } }) } @@ -248,21 +283,17 @@ RED.deploy = (function() { buttons: buttons }); - var now = Date.now(); RED.diff.getRemoteDiff(function(diff) { - var ellapsed = Math.max(1000 - (Date.now()-now), 0); currentDiff = diff; - setTimeout(function() { - conflictCheck.hide(); - var d = Object.keys(diff.conflicts); - if (d.length === 0) { - conflictAutoMerge.show(); - $("#red-ui-deploy-dialog-confirm-deploy-merge").removeClass('disabled') - } else { - conflictManualMerge.show(); - } - $("#red-ui-deploy-dialog-confirm-deploy-review").removeClass('disabled') - },ellapsed); + conflictCheck.hide(); + var d = Object.keys(diff.conflicts); + if (d.length === 0) { + conflictAutoMerge.show(); + $("#red-ui-deploy-dialog-confirm-deploy-merge").removeClass('disabled') + } else { + conflictManualMerge.show(); + } + $("#red-ui-deploy-dialog-confirm-deploy-review").removeClass('disabled') }) } function cropList(list) { diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/diff.js b/packages/node_modules/@node-red/editor-client/src/js/ui/diff.js index 3f73e29aa..ebdf683e3 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/diff.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/diff.js @@ -1,5 +1,4 @@ RED.diff = (function() { - var currentDiff = {}; var diffVisible = false; var diffList; @@ -62,12 +61,14 @@ RED.diff = (function() { addedCount:0, deletedCount:0, changedCount:0, + movedCount:0, unchangedCount: 0 }, remote: { addedCount:0, deletedCount:0, changedCount:0, + movedCount:0, unchangedCount: 0 }, conflicts: 0 @@ -138,7 +139,7 @@ RED.diff = (function() { $(this).parent().toggleClass('collapsed'); }); - createNodePropertiesTable(def,tab,localTabNode,remoteTabNode,conflicts).appendTo(div); + createNodePropertiesTable(def,tab,localTabNode,remoteTabNode).appendTo(div); selectState = ""; if (conflicts[tab.id]) { flowStats.conflicts++; @@ -208,19 +209,26 @@ RED.diff = (function() { var localStats = $('',{class:"red-ui-diff-list-flow-stats"}).appendTo(localCell); $('').text(RED._('diff.nodeCount',{count:localNodeCount})).appendTo(localStats); - if (flowStats.conflicts + flowStats.local.addedCount + flowStats.local.changedCount + flowStats.local.deletedCount > 0) { + if (flowStats.conflicts + flowStats.local.addedCount + flowStats.local.changedCount + flowStats.local.movedCount + flowStats.local.deletedCount > 0) { $(' [ ').appendTo(localStats); if (flowStats.conflicts > 0) { $(' '+flowStats.conflicts+'').appendTo(localStats); } if (flowStats.local.addedCount > 0) { - $(' '+flowStats.local.addedCount+'').appendTo(localStats); + const cell = $(' '+flowStats.local.addedCount+'').appendTo(localStats); + RED.popover.tooltip(cell, RED._('diff.type.added')) } if (flowStats.local.changedCount > 0) { - $(' '+flowStats.local.changedCount+'').appendTo(localStats); + const cell = $(' '+flowStats.local.changedCount+'').appendTo(localStats); + RED.popover.tooltip(cell, RED._('diff.type.changed')) + } + if (flowStats.local.movedCount > 0) { + const cell = $(' '+flowStats.local.movedCount+'').appendTo(localStats); + RED.popover.tooltip(cell, RED._('diff.type.moved')) } if (flowStats.local.deletedCount > 0) { - $(' '+flowStats.local.deletedCount+'').appendTo(localStats); + const cell = $(' '+flowStats.local.deletedCount+'').appendTo(localStats); + RED.popover.tooltip(cell, RED._('diff.type.deleted')) } $(' ] ').appendTo(localStats); } @@ -246,19 +254,26 @@ RED.diff = (function() { } var remoteStats = $('',{class:"red-ui-diff-list-flow-stats"}).appendTo(remoteCell); $('').text(RED._('diff.nodeCount',{count:remoteNodeCount})).appendTo(remoteStats); - if (flowStats.conflicts + flowStats.remote.addedCount + flowStats.remote.changedCount + flowStats.remote.deletedCount > 0) { + if (flowStats.conflicts + flowStats.remote.addedCount + flowStats.remote.changedCount + flowStats.remote.movedCount + flowStats.remote.deletedCount > 0) { $(' [ ').appendTo(remoteStats); if (flowStats.conflicts > 0) { $(' '+flowStats.conflicts+'').appendTo(remoteStats); } if (flowStats.remote.addedCount > 0) { - $(' '+flowStats.remote.addedCount+'').appendTo(remoteStats); + const cell = $(' '+flowStats.remote.addedCount+'').appendTo(remoteStats); + RED.popover.tooltip(cell, RED._('diff.type.added')) } if (flowStats.remote.changedCount > 0) { - $(' '+flowStats.remote.changedCount+'').appendTo(remoteStats); + const cell = $(' '+flowStats.remote.changedCount+'').appendTo(remoteStats); + RED.popover.tooltip(cell, RED._('diff.type.changed')) + } + if (flowStats.remote.movedCount > 0) { + const cell = $(' '+flowStats.remote.movedCount+'').appendTo(remoteStats); + RED.popover.tooltip(cell, RED._('diff.type.moved')) } if (flowStats.remote.deletedCount > 0) { - $(' '+flowStats.remote.deletedCount+'').appendTo(remoteStats); + const cell = $(' '+flowStats.remote.deletedCount+'').appendTo(remoteStats); + RED.popover.tooltip(cell, RED._('diff.type.deleted')) } $(' ] ').appendTo(remoteStats); } @@ -293,7 +308,7 @@ RED.diff = (function() { if (options.mode === "merge") { diffPanel.addClass("red-ui-diff-panel-merge"); } - var diffList = createDiffTable(diffPanel, diff); + var diffList = createDiffTable(diffPanel, diff, options); var localDiff = diff.localDiff; var remoteDiff = diff.remoteDiff; @@ -516,7 +531,6 @@ RED.diff = (function() { var hasChanges = false; // exists in original and local/remote but with changes var unChanged = true; // existing in original,local,remote unchanged - var localChanged = false; if (localDiff.added[node.id]) { stats.local.addedCount++; @@ -535,12 +549,20 @@ RED.diff = (function() { unChanged = false; } if (localDiff.changed[node.id]) { - stats.local.changedCount++; + if (localDiff.positionChanged[node.id]) { + stats.local.movedCount++ + } else { + stats.local.changedCount++; + } hasChanges = true; unChanged = false; } if (remoteDiff && remoteDiff.changed[node.id]) { - stats.remote.changedCount++; + if (remoteDiff.positionChanged[node.id]) { + stats.remote.movedCount++ + } else { + stats.remote.changedCount++; + } hasChanges = true; unChanged = false; } @@ -605,27 +627,32 @@ RED.diff = (function() { localNodeDiv.addClass("red-ui-diff-status-moved"); var localMovedMessage = ""; if (node.z === localN.z) { - localMovedMessage = RED._("diff.type.movedFrom",{id:(localDiff.currentConfig.all[node.id].z||'global')}); + const movedFromNodeTab = localDiff.currentConfig.all[localDiff.currentConfig.all[node.id].z] + const movedFromLabel = `'${movedFromNodeTab ? (movedFromNodeTab.label || movedFromNodeTab.id) : 'global'}'` + localMovedMessage = RED._("diff.type.movedFrom",{id: movedFromLabel}); } else { - localMovedMessage = RED._("diff.type.movedTo",{id:(localN.z||'global')}); + const movedToNodeTab = localDiff.newConfig.all[localN.z] + const movedToLabel = `'${movedToNodeTab ? (movedToNodeTab.label || movedToNodeTab.id) : 'global'}'` + localMovedMessage = RED._("diff.type.movedTo",{id:movedToLabel}); } $(' '+localMovedMessage+'').appendTo(localNodeDiv); } - localChanged = true; } else if (localDiff.deleted[node.z]) { localNodeDiv.addClass("red-ui-diff-empty"); - localChanged = true; } else if (localDiff.deleted[node.id]) { localNodeDiv.addClass("red-ui-diff-status-deleted"); $(' ').appendTo(localNodeDiv); - localChanged = true; } else if (localDiff.changed[node.id]) { if (localDiff.newConfig.all[node.id].z !== node.z) { localNodeDiv.addClass("red-ui-diff-empty"); } else { - localNodeDiv.addClass("red-ui-diff-status-changed"); - $(' ').appendTo(localNodeDiv); - localChanged = true; + if (localDiff.positionChanged[node.id]) { + localNodeDiv.addClass("red-ui-diff-status-moved"); + $(' ').appendTo(localNodeDiv); + } else { + localNodeDiv.addClass("red-ui-diff-status-changed"); + $(' ').appendTo(localNodeDiv); + } } } else { if (localDiff.newConfig.all[node.id].z !== node.z) { @@ -646,9 +673,13 @@ RED.diff = (function() { remoteNodeDiv.addClass("red-ui-diff-status-moved"); var remoteMovedMessage = ""; if (node.z === remoteN.z) { - remoteMovedMessage = RED._("diff.type.movedFrom",{id:(remoteDiff.currentConfig.all[node.id].z||'global')}); + const movedFromNodeTab = remoteDiff.currentConfig.all[remoteDiff.currentConfig.all[node.id].z] + const movedFromLabel = `'${movedFromNodeTab ? (movedFromNodeTab.label || movedFromNodeTab.id) : 'global'}'` + remoteMovedMessage = RED._("diff.type.movedFrom",{id:movedFromLabel}); } else { - remoteMovedMessage = RED._("diff.type.movedTo",{id:(remoteN.z||'global')}); + const movedToNodeTab = remoteDiff.newConfig.all[remoteN.z] + const movedToLabel = `'${movedToNodeTab ? (movedToNodeTab.label || movedToNodeTab.id) : 'global'}'` + remoteMovedMessage = RED._("diff.type.movedTo",{id:movedToLabel}); } $(' '+remoteMovedMessage+'').appendTo(remoteNodeDiv); } @@ -661,8 +692,13 @@ RED.diff = (function() { if (remoteDiff.newConfig.all[node.id].z !== node.z) { remoteNodeDiv.addClass("red-ui-diff-empty"); } else { - remoteNodeDiv.addClass("red-ui-diff-status-changed"); - $(' ').appendTo(remoteNodeDiv); + if (remoteDiff.positionChanged[node.id]) { + remoteNodeDiv.addClass("red-ui-diff-status-moved"); + $(' ').appendTo(remoteNodeDiv); + } else { + remoteNodeDiv.addClass("red-ui-diff-status-changed"); + $(' ').appendTo(remoteNodeDiv); + } } } else { if (remoteDiff.newConfig.all[node.id].z !== node.z) { @@ -788,7 +824,7 @@ RED.diff = (function() { $("",{class:"red-ui-diff-list-cell-label"}).text("position").appendTo(row); localCell = $("",{class:"red-ui-diff-list-cell red-ui-diff-list-node-local"}).appendTo(row); if (localNode) { - localCell.addClass("red-ui-diff-status-"+(localChanged?"changed":"unchanged")); + localCell.addClass("red-ui-diff-status-"+(localChanged?"moved":"unchanged")); $(''+(localChanged?'':'')+'').appendTo(localCell); element = $('').appendTo(localCell); var localPosition = {x:localNode.x,y:localNode.y}; @@ -813,7 +849,7 @@ RED.diff = (function() { if (remoteNode !== undefined) { remoteCell = $("",{class:"red-ui-diff-list-cell red-ui-diff-list-node-remote"}).appendTo(row); - remoteCell.addClass("red-ui-diff-status-"+(remoteChanged?"changed":"unchanged")); + remoteCell.addClass("red-ui-diff-status-"+(remoteChanged?"moved":"unchanged")); if (remoteNode) { $(''+(remoteChanged?'':'')+'').appendTo(remoteCell); element = $('').appendTo(remoteCell); @@ -1099,11 +1135,11 @@ RED.diff = (function() { // var diff = generateDiff(originalFlow,nns); // showDiff(diff); // } - function showRemoteDiff(diff) { - if (diff === undefined) { - getRemoteDiff(showRemoteDiff); + function showRemoteDiff(diff, options = {}) { + if (!diff) { + getRemoteDiff((remoteDiff) => showRemoteDiff(remoteDiff, options)); } else { - showDiff(diff,{mode:'merge'}); + showDiff(diff,{...options, mode:'merge'}); } } function parseNodes(nodeList) { @@ -1144,23 +1180,53 @@ RED.diff = (function() { } } function generateDiff(currentNodes,newNodes) { - var currentConfig = parseNodes(currentNodes); - var newConfig = parseNodes(newNodes); - var added = {}; - var deleted = {}; - var changed = {}; - var moved = {}; + const currentConfig = parseNodes(currentNodes); + const newConfig = parseNodes(newNodes); + const added = {}; + const deleted = {}; + const changed = {}; + const positionChanged = {}; + const moved = {}; Object.keys(currentConfig.all).forEach(function(id) { - var node = RED.nodes.workspace(id)||RED.nodes.subflow(id)||RED.nodes.node(id); + const node = RED.nodes.workspace(id)||RED.nodes.subflow(id)||RED.nodes.node(id); if (!newConfig.all.hasOwnProperty(id)) { deleted[id] = true; - } else if (JSON.stringify(currentConfig.all[id]) !== JSON.stringify(newConfig.all[id])) { + return + } + const currentConfigJSON = JSON.stringify(currentConfig.all[id]) + const newConfigJSON = JSON.stringify(newConfig.all[id]) + + if (currentConfigJSON !== newConfigJSON) { changed[id] = true; - if (currentConfig.all[id].z !== newConfig.all[id].z) { moved[id] = true; + } else if ( + currentConfig.all[id].x !== newConfig.all[id].x || + currentConfig.all[id].y !== newConfig.all[id].y || + currentConfig.all[id].w !== newConfig.all[id].w || + currentConfig.all[id].h !== newConfig.all[id].h + ) { + // This node's position on its parent has changed. We want to + // check if this is the *only* change for this given node + const currentNodeClone = JSON.parse(currentConfigJSON) + const newNodeClone = JSON.parse(newConfigJSON) + + delete currentNodeClone.x + delete currentNodeClone.y + delete currentNodeClone.w + delete currentNodeClone.h + delete newNodeClone.x + delete newNodeClone.y + delete newNodeClone.w + delete newNodeClone.h + + if (JSON.stringify(currentNodeClone) === JSON.stringify(newNodeClone)) { + // Only the position has changed - everything else is the same + positionChanged[id] = true + } } + } }); Object.keys(newConfig.all).forEach(function(id) { @@ -1169,13 +1235,14 @@ RED.diff = (function() { } }); - var diff = { - currentConfig: currentConfig, - newConfig: newConfig, - added: added, - deleted: deleted, - changed: changed, - moved: moved + const diff = { + currentConfig, + newConfig, + added, + deleted, + changed, + positionChanged, + moved }; return diff; } @@ -1240,12 +1307,14 @@ RED.diff = (function() { return diff; } - function showDiff(diff,options) { + function showDiff(diff, options) { if (diffVisible) { return; } options = options || {}; var mode = options.mode || 'merge'; + + options.hidePositionChanges = true var localDiff = diff.localDiff; var remoteDiff = diff.remoteDiff; @@ -1315,6 +1384,9 @@ RED.diff = (function() { if (!$("#red-ui-diff-view-diff-merge").hasClass('disabled')) { refreshConflictHeader(diff); mergeDiff(diff); + if (options.onmerge) { + options.onmerge() + } RED.tray.close(); } } @@ -1345,6 +1417,7 @@ RED.diff = (function() { var newConfig = []; var node; var nodeChangedStates = {}; + var nodeMovedStates = {}; var localChangedStates = {}; for (id in localDiff.newConfig.all) { if (localDiff.newConfig.all.hasOwnProperty(id)) { @@ -1352,12 +1425,14 @@ RED.diff = (function() { if (resolutions[id] === 'local') { if (node) { nodeChangedStates[id] = node.changed; + nodeMovedStates[id] = node.moved; } newConfig.push(localDiff.newConfig.all[id]); } else if (resolutions[id] === 'remote') { if (!remoteDiff.deleted[id] && remoteDiff.newConfig.all.hasOwnProperty(id)) { if (node) { nodeChangedStates[id] = node.changed; + nodeMovedStates[id] = node.moved; } localChangedStates[id] = 1; newConfig.push(remoteDiff.newConfig.all[id]); @@ -1381,8 +1456,9 @@ RED.diff = (function() { } return { config: newConfig, - nodeChangedStates: nodeChangedStates, - localChangedStates: localChangedStates + nodeChangedStates, + nodeMovedStates, + localChangedStates } } @@ -1393,6 +1469,7 @@ RED.diff = (function() { var newConfig = appliedDiff.config; var nodeChangedStates = appliedDiff.nodeChangedStates; + var nodeMovedStates = appliedDiff.nodeMovedStates; var localChangedStates = appliedDiff.localChangedStates; var isDirty = RED.nodes.dirty(); @@ -1401,33 +1478,56 @@ RED.diff = (function() { t:"replace", config: RED.nodes.createCompleteNodeSet(), changed: nodeChangedStates, + moved: nodeMovedStates, + complete: true, dirty: isDirty, rev: RED.nodes.version() } RED.history.push(historyEvent); - var originalFlow = RED.nodes.originalFlow(); - // originalFlow is what the editor things it loaded - // - add any newly added nodes from remote diff as they are now part of the record - for (var id in diff.remoteDiff.added) { - if (diff.remoteDiff.added.hasOwnProperty(id)) { - if (diff.remoteDiff.newConfig.all.hasOwnProperty(id)) { - originalFlow.push(JSON.parse(JSON.stringify(diff.remoteDiff.newConfig.all[id]))); - } - } - } + // var originalFlow = RED.nodes.originalFlow(); + // // originalFlow is what the editor thinks it loaded + // // - add any newly added nodes from remote diff as they are now part of the record + // for (var id in diff.remoteDiff.added) { + // if (diff.remoteDiff.added.hasOwnProperty(id)) { + // if (diff.remoteDiff.newConfig.all.hasOwnProperty(id)) { + // originalFlow.push(JSON.parse(JSON.stringify(diff.remoteDiff.newConfig.all[id]))); + // } + // } + // } RED.nodes.clear(); var imported = RED.nodes.import(newConfig); - // Restore the original flow so subsequent merge resolutions can properly - // identify new-vs-old - RED.nodes.originalFlow(originalFlow); + // // Restore the original flow so subsequent merge resolutions can properly + // // identify new-vs-old + // RED.nodes.originalFlow(originalFlow); + + // Clear all change flags from the import + RED.nodes.dirty(false); + + const flowsToLock = new Set() + function ensureUnlocked(id) { + const flow = id && (RED.nodes.workspace(id) || RED.nodes.subflow(id) || null); + const isLocked = flow ? flow.locked : false; + if (flow && isLocked) { + flow.locked = false; + flowsToLock.add(flow) + } + } imported.nodes.forEach(function(n) { - if (nodeChangedStates[n.id] || localChangedStates[n.id]) { + if (nodeChangedStates[n.id]) { + ensureUnlocked(n.z) n.changed = true; } + if (nodeMovedStates[n.id]) { + ensureUnlocked(n.z) + n.moved = true; + } + }) + flowsToLock.forEach(flow => { + flow.locked = true }) RED.nodes.version(diff.remoteDiff.rev); 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 70de4d9ac..f11dd45d9 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 @@ -190,7 +190,10 @@ RED.editor = (function() { const input = $("#"+prefix+"-"+property); const isTypedInput = input.length > 0 && input.next(".red-ui-typedInput-container").length > 0; if (isTypedInput) { - valid = input.typedInput("validate"); + valid = input.typedInput("validate", { returnErrorMessage: true }); + if (typeof valid === "string") { + return label ? label + ": " + valid : valid; + } } } } @@ -328,48 +331,108 @@ RED.editor = (function() { /** * Create a config-node select box for this property - * @param node - the node being edited - * @param property - the name of the field - * @param type - the type of the config-node + * @param {Object} node - the node being edited + * @param {String} property - the name of the node property + * @param {String} type - the type of the config-node + * @param {"node-config-input"|"node-input"|"node-input-subflow-env"} prefix - the prefix to use in the input element ids + * @param {Function} [filter] - a function to filter the list of config nodes + * @param {Object} [env] - the environment variable object (only used for subflow env vars) */ - function prepareConfigNodeSelect(node,property,type,prefix,filter) { - var input = $("#"+prefix+"-"+property); - if (input.length === 0 ) { + function prepareConfigNodeSelect(node, property, type, prefix, filter, env) { + let nodeValue + if (prefix === 'node-input-subflow-env') { + nodeValue = env?.value + } else { + nodeValue = node[property] + } + + const addBtnId = `${prefix}-btn-${property}-add`; + const editBtnId = `${prefix}-btn-${property}-edit`; + const selectId = prefix + '-' + property; + const input = $(`#${selectId}`); + if (input.length === 0) { return; } - var newWidth = input.width(); - var attrStyle = input.attr('style'); - var m; + const attrStyle = input.attr('style'); + let newWidth; + let m; if ((m = /(^|\s|;)width\s*:\s*([^;]+)/i.exec(attrStyle)) !== null) { newWidth = m[2].trim(); } else { newWidth = "70%"; } - var outerWrap = $("

      ").css({ + const outerWrap = $("
      ").css({ width: newWidth, - display:'inline-flex' + display: 'inline-flex' }); - var select = $('').appendTo(outerWrap); + const select = $('').appendTo(outerWrap); input.replaceWith(outerWrap); // set the style attr directly - using width() on FF causes a value of 114%... select.css({ 'flex-grow': 1 }); - updateConfigNodeSelect(property,type,node[property],prefix,filter); - $('') - .css({"margin-left":"10px"}) + + updateConfigNodeSelect(property, type, nodeValue, prefix, filter); + + // create the edit button + const editButton = $('') + .css({ "margin-left": "10px" }) .appendTo(outerWrap); - $('#'+prefix+'-lookup-'+property).on("click", function(e) { - showEditConfigNodeDialog(property,type,select.find(":selected").val(),prefix,node); + + RED.popover.tooltip(editButton, RED._('editor.editConfig', { type })); + + // create the add button + const addButton = $('') + .css({ "margin-left": "10px" }) + .appendTo(outerWrap); + RED.popover.tooltip(addButton, RED._('editor.addNewConfig', { type })); + + const disableButton = function(button, disabled) { + $(button).prop("disabled", !!disabled) + $(button).toggleClass("disabled", !!disabled) + }; + + // add the click handler + addButton.on("click", function (e) { + if (addButton.prop("disabled")) { return } + showEditConfigNodeDialog(property, type, "_ADD_", prefix, node); e.preventDefault(); }); + editButton.on("click", function (e) { + const selectedOpt = select.find(":selected") + if (selectedOpt.data('env')) { return } // don't show the dialog for env vars items (MVP. Future enhancement: lookup the env, if present, show the associated edit dialog) + if (editButton.prop("disabled")) { return } + showEditConfigNodeDialog(property, type, selectedOpt.val(), prefix, node); + e.preventDefault(); + }); + + // dont permit the user to click the button if the selected option is an env var + select.on("change", function () { + const selectedOpt = select.find(":selected"); + const optionsLength = select.find("option").length; + if (selectedOpt?.data('env')) { + disableButton(addButton, true); + disableButton(editButton, true); + // disable the edit button if no options available + } else if (optionsLength === 1 && selectedOpt.val() === "_ADD_") { + disableButton(addButton, false); + disableButton(editButton, true); + } else if (selectedOpt.val() === "") { + disableButton(addButton, false); + disableButton(editButton, true); + } else { + disableButton(addButton, false); + disableButton(editButton, false); + } + }); + var label = ""; - var configNode = RED.nodes.node(node[property]); - var node_def = RED.nodes.getType(type); + var configNode = RED.nodes.node(nodeValue); if (configNode) { - label = RED.utils.getNodeLabel(configNode,configNode.id); + label = RED.utils.getNodeLabel(configNode, configNode.id); } + input.val(label); } @@ -777,12 +840,9 @@ RED.editor = (function() { } function defaultConfigNodeSort(A,B) { - if (A.__label__ < B.__label__) { - return -1; - } else if (A.__label__ > B.__label__) { - return 1; - } - return 0; + // sort case insensitive so that `[env] node-name` items are at the top and + // not mixed inbetween the the lower and upper case items + return (A.__label__ || '').localeCompare((B.__label__ || ''), undefined, {sensitivity: 'base'}) } function updateConfigNodeSelect(name,type,value,prefix,filter) { @@ -797,7 +857,7 @@ RED.editor = (function() { } $("#"+prefix+"-"+name).val(value); } else { - + let inclSubflowEnvvars = false var select = $("#"+prefix+"-"+name); var node_def = RED.nodes.getType(type); select.children().remove(); @@ -805,6 +865,7 @@ RED.editor = (function() { var activeWorkspace = RED.nodes.workspace(RED.workspaces.active()); if (!activeWorkspace) { activeWorkspace = RED.nodes.subflow(RED.workspaces.active()); + inclSubflowEnvvars = true } var configNodes = []; @@ -820,6 +881,31 @@ RED.editor = (function() { } } }); + + // as includeSubflowEnvvars is true, this is a subflow. + // include any 'conf-types' env vars as a list of avaiable configs + // in the config dropdown as `[env] node-name` + if (inclSubflowEnvvars && activeWorkspace.env) { + const parentEnv = activeWorkspace.env.filter(env => env.ui?.type === 'conf-types' && env.type === type) + if (parentEnv && parentEnv.length > 0) { + const locale = RED.i18n.lang() + for (let i = 0; i < parentEnv.length; i++) { + const tenv = parentEnv[i] + const ui = tenv.ui || {} + const labels = ui.label || {} + const labelText = RED.editor.envVarList.lookupLabel(labels, labels["en-US"] || tenv.name, locale) + const config = { + env: tenv, + id: '${' + parentEnv[0].name + '}', + type: type, + label: labelText, + __label__: `[env] ${labelText}` + } + configNodes.push(config) + } + } + } + var configSortFn = defaultConfigNodeSort; if (typeof node_def.sort == "function") { configSortFn = node_def.sort; @@ -831,7 +917,10 @@ RED.editor = (function() { } configNodes.forEach(function(cn) { - $('').text(RED.text.bidi.enforceTextDirectionWithUCC(cn.__label__)).appendTo(select); + const option = $('').text(RED.text.bidi.enforceTextDirectionWithUCC(cn.__label__)).appendTo(select); + if (cn.env) { + option.data('env', cn.env) // set a data attribute to indicate this is an env var (to inhibit the edit button) + } delete cn.__label__; }); @@ -844,7 +933,12 @@ RED.editor = (function() { } } - select.append(''); + if (!configNodes.length) { + select.append(''); + } else { + select.append(''); + } + window.setTimeout(function() { select.trigger("change");},50); } } @@ -1512,9 +1606,16 @@ RED.editor = (function() { } RED.tray.close(function() { var filter = null; - if (editContext && typeof editContext._def.defaults[configProperty].filter === 'function') { - filter = function(n) { - return editContext._def.defaults[configProperty].filter.call(editContext,n); + // when editing a config via subflow edit panel, the `configProperty` will not + // necessarily be a property of the editContext._def.defaults object + // Also, when editing via dashboard sidebar, editContext can be null + // so we need to guard both scenarios + if (editContext?._def) { + const isSubflow = (editContext._def.type === 'subflow' || /subflow:.*/.test(editContext._def.type)) + if (editContext && !isSubflow && typeof editContext._def.defaults?.[configProperty]?.filter === 'function') { + filter = function(n) { + return editContext._def.defaults[configProperty].filter.call(editContext,n); + } } } updateConfigNodeSelect(configProperty,configType,editing_config_node.id,prefix,filter); @@ -1575,7 +1676,7 @@ RED.editor = (function() { RED.history.push(historyEvent); RED.tray.close(function() { var filter = null; - if (editContext && typeof editContext._def.defaults[configProperty].filter === 'function') { + if (editContext && typeof editContext._def.defaults[configProperty]?.filter === 'function') { filter = function(n) { return editContext._def.defaults[configProperty].filter.call(editContext,n); } @@ -2116,6 +2217,7 @@ RED.editor = (function() { } }, editBuffer: function(options) { showTypeEditor("_buffer", options) }, + getEditStack: function () { return [...editStack] }, buildEditForm: buildEditForm, validateNode: validateNode, updateNodeProperties: updateNodeProperties, @@ -2160,6 +2262,7 @@ RED.editor = (function() { filteredEditPanes[type] = filter } editPanes[type] = definition; - } + }, + prepareConfigNodeSelect: prepareConfigNodeSelect, } })(); diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/editors/code-editors/monaco.js b/packages/node_modules/@node-red/editor-client/src/js/ui/editors/code-editors/monaco.js index 9f104faaf..cbeecd512 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/editors/code-editors/monaco.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/editors/code-editors/monaco.js @@ -585,7 +585,7 @@ RED.editor.codeEditor.monaco = (function() { createMonacoCompletionItem("set (flow context)", 'flow.set("${1:name}", ${1:value});','Set a value in flow context',range), createMonacoCompletionItem("get (global context)", 'global.get("${1:name}");','Get a value from global context',range), createMonacoCompletionItem("set (global context)", 'global.set("${1:name}", ${1:value});','Set a value in global context',range), - createMonacoCompletionItem("get (env)", 'env.get("${1|NR_NODE_ID,NR_NODE_NAME,NR_NODE_PATH,NR_GROUP_ID,NR_GROUP_NAME,NR_FLOW_ID,NR_FLOW_NAME|}");','Get env variable value',range), + createMonacoCompletionItem("get (env)", 'env.get("${1|NR_NODE_ID,NR_NODE_NAME,NR_NODE_PATH,NR_GROUP_ID,NR_GROUP_NAME,NR_FLOW_ID,NR_FLOW_NAME,NR_SUBFLOW_NAME,NR_SUBFLOW_ID,NR_SUBFLOW_PATH|}");','Get env variable value',range), createMonacoCompletionItem("cloneMessage (RED.util)", 'RED.util.cloneMessage(${1:msg});', ["```typescript", "RED.util.cloneMessage(msg: T): T", diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/editors/envVarList.js b/packages/node_modules/@node-red/editor-client/src/js/ui/editors/envVarList.js index ba71e651f..dda5d1660 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/editors/envVarList.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/editors/envVarList.js @@ -1,8 +1,9 @@ RED.editor.envVarList = (function() { var currentLocale = 'en-US'; - var DEFAULT_ENV_TYPE_LIST = ['str','num','bool','json','bin','env']; - var DEFAULT_ENV_TYPE_LIST_INC_CRED = ['str','num','bool','json','bin','env','cred','jsonata']; + const DEFAULT_ENV_TYPE_LIST = ['str','num','bool','json','bin','env']; + const DEFAULT_ENV_TYPE_LIST_INC_CONFTYPES = ['str','num','bool','json','bin','env','conf-types']; + const DEFAULT_ENV_TYPE_LIST_INC_CRED = ['str','num','bool','json','bin','env','cred','jsonata']; /** * Create env var edit interface @@ -10,8 +11,8 @@ RED.editor.envVarList = (function() { * @param node - subflow node */ function buildPropertiesList(envContainer, node) { - - var isTemplateNode = (node.type === "subflow"); + if(RED.editor.envVarList.debug) { console.log('envVarList: buildPropertiesList', envContainer, node) } + const isTemplateNode = (node.type === "subflow"); envContainer .css({ @@ -83,7 +84,14 @@ RED.editor.envVarList = (function() { // if `opt.ui` does not exist, then apply defaults. If these // defaults do not change then they will get stripped off // before saving. - if (opt.type === 'cred') { + if (opt.type === 'conf-types') { + opt.ui = opt.ui || { + icon: "fa fa-cog", + type: "conf-types", + opts: {opts:[]} + } + opt.ui.type = "conf-types"; + } else if (opt.type === 'cred') { opt.ui = opt.ui || { icon: "", type: "cred" @@ -119,7 +127,7 @@ RED.editor.envVarList = (function() { } }); - buildEnvEditRow(uiRow, opt.ui, nameField, valueField); + buildEnvEditRow(uiRow, opt, nameField, valueField); nameField.trigger('change'); } }, @@ -181,21 +189,23 @@ RED.editor.envVarList = (function() { * @param nameField - name field of env var * @param valueField - value field of env var */ - function buildEnvEditRow(container, ui, nameField, valueField) { + function buildEnvEditRow(container, opt, nameField, valueField) { + const ui = opt.ui + if(RED.editor.envVarList.debug) { console.log('envVarList: buildEnvEditRow', container, ui, nameField, valueField) } container.addClass("red-ui-editor-subflow-env-ui-row") var topRow = $('
      ').appendTo(container); $('
      ').appendTo(topRow); $('
      ').text(RED._("editor.icon")).appendTo(topRow); $('
      ').text(RED._("editor.label")).appendTo(topRow); - $('
      ').text(RED._("editor.inputType")).appendTo(topRow); + $('
      ').text(RED._("editor.inputType")).appendTo(topRow); var row = $('
      ').appendTo(container); $('
      ').appendTo(row); var typeOptions = { - 'input': {types:DEFAULT_ENV_TYPE_LIST}, - 'select': {opts:[]}, - 'spinner': {}, - 'cred': {} + 'input': {types:DEFAULT_ENV_TYPE_LIST_INC_CONFTYPES}, + 'select': {opts:[]}, + 'spinner': {}, + 'cred': {} }; if (ui.opts) { typeOptions[ui.type] = ui.opts; @@ -260,15 +270,16 @@ RED.editor.envVarList = (function() { labelInput.attr("placeholder",$(this).val()) }); - var inputCell = $('
      ').appendTo(row); - var inputCellInput = $('').css("width","100%").appendTo(inputCell); + var inputCell = $('
      ').appendTo(row); + var uiInputTypeInput = $('').css("width","100%").appendTo(inputCell); if (ui.type === "input") { - inputCellInput.val(ui.opts.types.join(",")); + uiInputTypeInput.val(ui.opts.types.join(",")); } var checkbox; var selectBox; - inputCellInput.typedInput({ + // the options presented in the UI section for an "input" type selection + uiInputTypeInput.typedInput({ types: [ { value:"input", @@ -429,7 +440,7 @@ RED.editor.envVarList = (function() { } }); ui.opts.opts = vals; - inputCellInput.typedInput('value',Date.now()) + uiInputTypeInput.typedInput('value',Date.now()) } } } @@ -496,12 +507,13 @@ RED.editor.envVarList = (function() { } else { delete ui.opts.max; } - inputCellInput.typedInput('value',Date.now()) + uiInputTypeInput.typedInput('value',Date.now()) } } } } }, + 'conf-types', { value:"none", label:RED._("editor.inputs.none"), icon:"fa fa-times",hasValue:false @@ -519,14 +531,20 @@ RED.editor.envVarList = (function() { // 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(",")) + uiInputTypeInput.typedInput('value',ui.opts.types.join(",")) + } else if (ui.type === 'conf-types') { + // In the case of 'conf-types' type, the typedInput will be populated + // with a list of all config nodes types installed. + // Restore the value to the last selected type + uiInputTypeInput.typedInput('value', opt.type) } 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()) + uiInputTypeInput.typedInput('value',Date.now()) } + if(RED.editor.envVarList.debug) { console.log('envVarList: inputCellInput on:typedinputtypechange. ui.type = ' + ui.type) } switch (ui.type) { case 'input': valueField.typedInput('types',ui.opts.types); @@ -544,7 +562,7 @@ RED.editor.envVarList = (function() { valueField.typedInput('types',['cred']); break; default: - valueField.typedInput('types',DEFAULT_ENV_TYPE_LIST) + valueField.typedInput('types', DEFAULT_ENV_TYPE_LIST); } if (ui.type === 'checkbox') { valueField.typedInput('type','bool'); @@ -556,8 +574,46 @@ RED.editor.envVarList = (function() { } }).on("change", function(evt,type) { - if (ui.type === 'input') { - var types = inputCellInput.typedInput('value'); + const selectedType = $(this).typedInput('type') // the UI typedInput type + if(RED.editor.envVarList.debug) { console.log('envVarList: inputCellInput on:change. selectedType = ' + selectedType) } + if (selectedType === 'conf-types') { + const selectedConfigType = $(this).typedInput('value') || opt.type + let activeWorkspace = RED.nodes.workspace(RED.workspaces.active()); + if (!activeWorkspace) { + activeWorkspace = RED.nodes.subflow(RED.workspaces.active()); + } + + // get a list of all config nodes matching the selectedValue + const configNodes = []; + RED.nodes.eachConfig(function(config) { + if (config.type == selectedConfigType && (!config.z || config.z === activeWorkspace.id)) { + const modulePath = config._def?.set?.id || '' + let label = RED.utils.getNodeLabel(config, config.id) || config.id; + label += config.d ? ' ['+RED._('workspace.disabled')+']' : ''; + const _config = { + _type: selectedConfigType, + value: config.id, + label: label, + title: modulePath ? modulePath + ' - ' + label : label, + enabled: config.d !== true, + disabled: config.d === true, + } + configNodes.push(_config); + } + }); + const tiTypes = { + value: selectedConfigType, + label: "config", + icon: "fa fa-cog", + options: configNodes, + } + valueField.typedInput('types', [tiTypes]); + valueField.typedInput('type', selectedConfigType); + valueField.typedInput('value', opt.value); + + + } else if (ui.type === 'input') { + var types = uiInputTypeInput.typedInput('value'); ui.opts.types = (types === "") ? ["str"] : types.split(","); valueField.typedInput('types',ui.opts.types); } @@ -569,7 +625,7 @@ RED.editor.envVarList = (function() { }) // 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) + uiInputTypeInput.typedInput('type',ui.type) } function setLocale(l, list) { diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/env-var.js b/packages/node_modules/@node-red/editor-client/src/js/ui/env-var.js index 79c626af4..55c800be1 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/env-var.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/env-var.js @@ -153,10 +153,6 @@ RED.envVar = (function() { } function init(done) { - if (!RED.user.hasPermission("settings.write")) { - RED.notify(RED._("user.errors.settings"),"error"); - return; - } RED.userSettings.add({ id:'envvar', title: RED._("env-var.environment"), diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/notifications.js b/packages/node_modules/@node-red/editor-client/src/js/ui/notifications.js index 30dcc4bd5..d68d03d3f 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/notifications.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/notifications.js @@ -221,12 +221,12 @@ RED.notifications = (function() { if (newType) { n.className = "red-ui-notification red-ui-notification-"+newType; } - + newTimeout = newOptions.hasOwnProperty('timeout')?newOptions.timeout:timeout if (!fixed || newOptions.fixed === false) { - newTimeout = (newOptions.hasOwnProperty('timeout')?newOptions.timeout:timeout)||5000; + newTimeout = newTimeout || 5000 } if (newOptions.buttons) { - var buttonSet = $('
      ').appendTo(nn) + var buttonSet = $('
      ').appendTo(nn) newOptions.buttons.forEach(function(buttonDef) { var b = $('