Merge branch 'dev' into make-split/join-more-flexible

This commit is contained in:
Nick O'Leary 2024-03-07 15:40:56 +00:00 committed by GitHub
commit 742aa2fa0d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
171 changed files with 9151 additions and 1463 deletions

View File

@ -30,5 +30,5 @@ the [forum](https://discourse.nodered.org) or
- [ ] I have read the [contribution guidelines](https://github.com/node-red/node-red/blob/master/CONTRIBUTING.md) - [ ] I have read the [contribution guidelines](https://github.com/node-red/node-red/blob/master/CONTRIBUTING.md)
- [ ] For non-bugfix PRs, I have discussed this change on the forum/slack team. - [ ] For non-bugfix PRs, I have discussed this change on the forum/slack team.
- [ ] I have run `grunt` to verify the unit tests pass - [ ] I have run `npm run test` to verify the unit tests pass
- [ ] I have added suitable unit tests to cover the new/changed functionality - [ ] I have added suitable unit tests to cover the new/changed functionality

15
.github/dependabot.yml vendored Normal file
View File

@ -0,0 +1,15 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
updates:
- package-ecosystem: "github-actions" # See documentation for possible values
directory: "/" # Location of package manifests
schedule:
interval: "monthly"
groups:
github-actions:
patterns:
- "*"

View File

@ -14,25 +14,25 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Check out node-red repository - name: Check out node-red repository
uses: actions/checkout@v2 uses: actions/checkout@v4
with: with:
path: 'node-red' path: 'node-red'
- name: Check out node-red-docker repository - name: Check out node-red-docker repository
uses: actions/checkout@v2 uses: actions/checkout@v4
with: with:
repository: 'node-red/node-red-docker' repository: 'node-red/node-red-docker'
path: 'node-red-docker' path: 'node-red-docker'
- name: Check out node-red.github.io repository - name: Check out node-red.github.io repository
uses: actions/checkout@v2 uses: actions/checkout@v4
with: with:
repository: 'node-red/node-red.github.io' repository: 'node-red/node-red.github.io'
path: 'node-red.github.io' path: 'node-red.github.io'
- uses: actions/setup-node@v1 - uses: actions/setup-node@v4
with: with:
node-version: '16' node-version: '16'
- run: node ./node-red/.github/scripts/update-node-red-docker.js - run: node ./node-red/.github/scripts/update-node-red-docker.js
- name: Create Docker Pull Request - name: Create Docker Pull Request
uses: peter-evans/create-pull-request@v2 uses: peter-evans/create-pull-request@v6
with: with:
token: ${{ secrets.NR_REPO_TOKEN }} token: ${{ secrets.NR_REPO_TOKEN }}
committer: GitHub <noreply@github.com> committer: GitHub <noreply@github.com>
@ -48,7 +48,7 @@ jobs:
This PR was auto-generated by a GitHub Action. Any questions, speak to @knolleary This PR was auto-generated by a GitHub Action. Any questions, speak to @knolleary
- run: node ./node-red/.github/scripts/update-node-red-website.js - run: node ./node-red/.github/scripts/update-node-red-website.js
- name: Create Website Pull Request - name: Create Website Pull Request
uses: peter-evans/create-pull-request@v2 uses: peter-evans/create-pull-request@v6
with: with:
token: ${{ secrets.NR_REPO_TOKEN }} token: ${{ secrets.NR_REPO_TOKEN }}
committer: GitHub <noreply@github.com> committer: GitHub <noreply@github.com>

View File

@ -12,16 +12,15 @@ permissions:
jobs: jobs:
build: build:
permissions: permissions:
checks: write # for coverallsapp/github-action to create new checks
contents: read # for actions/checkout to fetch code contents: read # for actions/checkout to fetch code
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
node-version: [16, 18, 20] node-version: [18, 20]
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- name: Use Node.js ${{ matrix.node-version }} - name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3 uses: actions/setup-node@v4
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
- name: Install Dependencies - name: Install Dependencies
@ -29,8 +28,3 @@ jobs:
- name: Run tests - name: Run tests
run: | run: |
npm run test 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 }}

1
.gitignore vendored
View File

@ -27,3 +27,4 @@ docs
.vscode .vscode
.nyc_output .nyc_output
sync.ffs_db sync.ffs_db
package-lock.json

View File

@ -1,3 +1,123 @@
#### 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
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
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
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 #### 3.1.0: Milestone Release
Editor Editor

View File

@ -16,6 +16,9 @@ behavior to the project's core team at team@nodered.org.
Please raise any bug reports on the relevant project's issue tracker. Be sure to Please raise any bug reports on the relevant project's issue tracker. Be sure to
search the list to see if your issue has already been raised. search the list to see if your issue has already been raised.
If your issue is more of a question on how to do something with Node-RED, please
consider using the [community forum](https://discourse.nodered.org/).
A good bug report is one that make it easy for us to understand what you were A good bug report is one that make it easy for us to understand what you were
trying to do and what went wrong. trying to do and what went wrong.
@ -35,14 +38,18 @@ For feature requests, please raise them on the [forum](https://discourse.nodered
## Pull-Requests ## Pull-Requests
If you want to raise a pull-request with a new feature, or a refactoring If you want to raise a pull-request with a new feature, or a refactoring
of existing code, it may well get rejected if you haven't discussed it on of existing code, please come and discuss it with us first. We prefer to
the [forum](https://discourse.nodered.org) first. do it that way to make sure your time and effort is well spent on something
that fits with our goals.
If you've got a bug-fix or similar for us, then you are most welcome to
get it raised - just make sure you link back to the issue it's fixing and
try to include some tests!
All contributors need to sign the OpenJS Foundation's Contributor License Agreement. All contributors need to sign the OpenJS Foundation's Contributor License Agreement.
It is an online process and quick to do. If you raise a pull-request without It is an online process and quick to do. If you raise a pull-request without
having signed the CLA, you will be prompted to do so automatically. having signed the CLA, you will be prompted to do so automatically.
### Code Branches ### Code Branches
When raising a PR for a fix or a new feature, it is important to target the right branch. When raising a PR for a fix or a new feature, it is important to target the right branch.

View File

@ -151,7 +151,6 @@ module.exports = function(grunt) {
"packages/node_modules/@node-red/editor-client/src/js/font-awesome.js", "packages/node_modules/@node-red/editor-client/src/js/font-awesome.js",
"packages/node_modules/@node-red/editor-client/src/js/history.js", "packages/node_modules/@node-red/editor-client/src/js/history.js",
"packages/node_modules/@node-red/editor-client/src/js/validators.js", "packages/node_modules/@node-red/editor-client/src/js/validators.js",
"packages/node_modules/@node-red/editor-client/src/js/ui/mermaid.js",
"packages/node_modules/@node-red/editor-client/src/js/ui/utils.js", "packages/node_modules/@node-red/editor-client/src/js/ui/utils.js",
"packages/node_modules/@node-red/editor-client/src/js/ui/common/editableList.js", "packages/node_modules/@node-red/editor-client/src/js/ui/common/editableList.js",
"packages/node_modules/@node-red/editor-client/src/js/ui/common/treeList.js", "packages/node_modules/@node-red/editor-client/src/js/ui/common/treeList.js",

View File

@ -54,7 +54,7 @@
"is-utf8": "0.2.1", "is-utf8": "0.2.1",
"js-yaml": "4.1.0", "js-yaml": "4.1.0",
"json-stringify-safe": "5.0.1", "json-stringify-safe": "5.0.1",
"jsonata": "1.8.6", "jsonata": "2.0.4",
"lodash.clonedeep": "^4.5.0", "lodash.clonedeep": "^4.5.0",
"media-typer": "1.1.0", "media-typer": "1.1.0",
"memorystore": "1.6.7", "memorystore": "1.6.7",
@ -64,7 +64,7 @@
"mqtt": "4.3.7", "mqtt": "4.3.7",
"multer": "1.4.5-lts.1", "multer": "1.4.5-lts.1",
"mustache": "4.2.0", "mustache": "4.2.0",
"node-red-admin": "^3.1.0", "node-red-admin": "^3.1.2",
"node-watch": "0.7.4", "node-watch": "0.7.4",
"nopt": "5.0.0", "nopt": "5.0.0",
"oauth2orize": "1.11.1", "oauth2orize": "1.11.1",
@ -109,7 +109,7 @@
"jquery-i18next": "1.2.1", "jquery-i18next": "1.2.1",
"jsdoc-nr-template": "github:node-red/jsdoc-nr-template", "jsdoc-nr-template": "github:node-red/jsdoc-nr-template",
"marked": "4.3.0", "marked": "4.3.0",
"mermaid": "^9.4.3", "mermaid": "^10.4.0",
"minami": "1.2.3", "minami": "1.2.3",
"mocha": "9.2.2", "mocha": "9.2.2",
"node-red-node-test-helper": "^0.3.2", "node-red-node-test-helper": "^0.3.2",
@ -122,6 +122,6 @@
"supertest": "6.3.3" "supertest": "6.3.3"
}, },
"engines": { "engines": {
"node": ">=14" "node": ">=18"
} }
} }

View File

@ -33,6 +33,9 @@ module.exports = {
store: req.query['store'], store: req.query['store'],
req: apiUtils.getRequestLogObject(req) req: apiUtils.getRequestLogObject(req)
} }
if (req.query['keysOnly'] !== undefined) {
opts.keysOnly = true
}
runtimeAPI.context.getValue(opts).then(function(result) { runtimeAPI.context.getValue(opts).then(function(result) {
res.json(result); res.json(result);
}).catch(function(err) { }).catch(function(err) {

View File

@ -51,7 +51,7 @@ module.exports = {
var ui = require("./ui"); var ui = require("./ui");
ui.init(runtimeAPI); ui.init(settings, runtimeAPI);
const editorApp = apiUtil.createExpressApp(settings) const editorApp = apiUtil.createExpressApp(settings)

View File

@ -339,6 +339,8 @@ module.exports = {
} }
theme.codeEditor = theme.codeEditor || {} theme.codeEditor = theme.codeEditor || {}
theme.codeEditor.options = Object.assign({}, themePlugin.monacoOptions, theme.codeEditor.options); theme.codeEditor.options = Object.assign({}, themePlugin.monacoOptions, theme.codeEditor.options);
theme.mermaid = Object.assign({}, themePlugin.mermaid, theme.mermaid)
} }
activeThemeInitialised = true; activeThemeInitialised = true;
} }

View File

@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
**/ **/
const crypto = require('crypto')
var express = require('express'); var express = require('express');
var fs = require("fs"); var fs = require("fs");
var path = require("path"); var path = require("path");
@ -24,13 +25,16 @@ var apiUtils = require("../util");
var theme = require("./theme"); var theme = require("./theme");
var runtimeAPI; var runtimeAPI;
let settings;
var editorClientDir = path.dirname(require.resolve("@node-red/editor-client")); var editorClientDir = path.dirname(require.resolve("@node-red/editor-client"));
var defaultNodeIcon = path.join(editorClientDir,"public","red","images","icons","arrow-in.svg"); var defaultNodeIcon = path.join(editorClientDir,"public","red","images","icons","arrow-in.svg");
var editorTemplatePath = path.join(editorClientDir,"templates","index.mst"); var editorTemplatePath = path.join(editorClientDir,"templates","index.mst");
var editorTemplate; var editorTemplate;
let cacheBuster
module.exports = { module.exports = {
init: function(_runtimeAPI) { init: function(_settings, _runtimeAPI) {
settings = _settings;
runtimeAPI = _runtimeAPI; runtimeAPI = _runtimeAPI;
editorTemplate = fs.readFileSync(editorTemplatePath,"utf8"); editorTemplate = fs.readFileSync(editorTemplatePath,"utf8");
Mustache.parse(editorTemplate); Mustache.parse(editorTemplate);
@ -91,6 +95,12 @@ module.exports = {
}, },
editor: async function(req,res) { editor: async function(req,res) {
if (!cacheBuster) {
// settings.instanceId is set asynchronously to the editor-api
// being initiaised. So we defer calculating the cacheBuster hash
// until the first load of the editor
cacheBuster = crypto.createHash('md5').update(`${settings.version || 'version'}-${settings.instanceId || 'instanceId'}`).digest("hex").substring(0,12)
}
let sessionMessages; let sessionMessages;
if (req.session && req.session.messages) { if (req.session && req.session.messages) {
@ -99,6 +109,7 @@ module.exports = {
} }
res.send(Mustache.render(editorTemplate,{ res.send(Mustache.render(editorTemplate,{
sessionMessages, sessionMessages,
cacheBuster,
...await theme.context() ...await theme.context()
})); }));
}, },

View File

@ -109,7 +109,6 @@
"selectionToSubflow": "Auswahl in Subflow umwandeln", "selectionToSubflow": "Auswahl in Subflow umwandeln",
"flows": "Flow", "flows": "Flow",
"add": "Hinzufügen", "add": "Hinzufügen",
"rename": "Umbenennen",
"delete": "Löschen", "delete": "Löschen",
"keyboardShortcuts": "Tastenkürzel", "keyboardShortcuts": "Tastenkürzel",
"login": "Anmelden", "login": "Anmelden",
@ -1076,7 +1075,7 @@
"git-auth-error": "Git-Authentifizierungsfehler" "git-auth-error": "Git-Authentifizierungsfehler"
}, },
"create-success": { "create-success": {
"success": "Sie haben Ihr erstes Projekt erfolgreich erstduellt!", "success": "Sie haben Ihr erstes Projekt erfolgreich erstellt!",
"desc0": "Sie können jetzt Node-RED wie bisher verwenden.", "desc0": "Sie können jetzt Node-RED wie bisher verwenden.",
"desc1": "Im Tab 'Info' in der Seitenleiste wird angezeigt, welches das aktuelle Projekt ist. Über die Schaltfläche rechts neben dem Projektnamen gelangt man zu 'Projekteinstellungen'.", "desc1": "Im Tab 'Info' in der Seitenleiste wird angezeigt, welches das aktuelle Projekt ist. Über die Schaltfläche rechts neben dem Projektnamen gelangt man zu 'Projekteinstellungen'.",
"desc2": "Im Tab 'Commit-Historie' in der Seitenleiste werden alle Dateien angezeigt, die sich in Ihrem Projekt geändert haben, und um sie ins lokale Repository zu übertragen (commit). Es zeigt Ihnen eine vollständige Historie Ihrer Commits an und ermöglicht es Ihnen, Ihre Commits in ein (remote) Server-Repository zu schieben (push)." "desc2": "Im Tab 'Commit-Historie' in der Seitenleiste werden alle Dateien angezeigt, die sich in Ihrem Projekt geändert haben, und um sie ins lokale Repository zu übertragen (commit). Es zeigt Ihnen eine vollständige Historie Ihrer Commits an und ermöglicht es Ihnen, Ihre Commits in ein (remote) Server-Repository zu schieben (push)."
@ -1172,17 +1171,6 @@
"diagnostics": { "diagnostics": {
"title": "System-Informationen" "title": "System-Informationen"
}, },
"languages": {
"de": "Deutsch",
"en-US": "Englisch",
"fr": "Französisch",
"ja": "Japanisch",
"ko": "Koreanisch",
"pt-BR":"Portugiesisch",
"ru": "Russisch",
"zh-CN": "Chinesisch (Vereinfacht)",
"zh-TW": "Chinesisch (Traditionell)"
},
"validator": { "validator": {
"errors": { "errors": {
"invalid-json": "Ungültige JSON-Daten: __error__", "invalid-json": "Ungültige JSON-Daten: __error__",

View File

@ -113,7 +113,7 @@
"displayStatus": "Show node status", "displayStatus": "Show node status",
"displayConfig": "Configuration nodes", "displayConfig": "Configuration nodes",
"import": "Import", "import": "Import",
"importExample": "Import Example Flow", "importExample": "Import example flow",
"export": "Export", "export": "Export",
"search": "Search flows", "search": "Search flows",
"searchInput": "search your flows", "searchInput": "search your flows",
@ -122,7 +122,6 @@
"selectionToSubflow": "Selection to Subflow", "selectionToSubflow": "Selection to Subflow",
"flows": "Flows", "flows": "Flows",
"add": "Add", "add": "Add",
"rename": "Rename",
"delete": "Delete", "delete": "Delete",
"keyboardShortcuts": "Keyboard shortcuts", "keyboardShortcuts": "Keyboard shortcuts",
"login": "Login", "login": "Login",
@ -130,6 +129,11 @@
"editPalette": "Manage palette", "editPalette": "Manage palette",
"other": "Other", "other": "Other",
"showTips": "Show tips", "showTips": "Show tips",
"showNodeHelp": "Show node help",
"enableSelectedNodes": "Enable selected nodes",
"disableSelectedNodes": "Disable selected nodes",
"showSelectedNodeLabels": "Show selected node labels",
"hideSelectedNodeLabels": "Hide selected node labels",
"showWelcomeTours": "Show guided tours for new versions", "showWelcomeTours": "Show guided tours for new versions",
"help": "Node-RED website", "help": "Node-RED website",
"projects": "Projects", "projects": "Projects",
@ -299,7 +303,8 @@
"missingType": "Input not a valid flow - item __index__ missing 'type' property" "missingType": "Input not a valid flow - item __index__ missing 'type' property"
}, },
"conflictNotification1": "Some of the nodes you are importing already exist in your workspace.", "conflictNotification1": "Some of the nodes you are importing already exist in your workspace.",
"conflictNotification2": "Select which nodes to import and whether to replace the existing nodes, or to import a copy of them." "conflictNotification2": "Select which nodes to import and whether to replace the existing nodes, or to import a copy of them.",
"alreadyExists": "This node already exists"
}, },
"copyMessagePath": "Path copied", "copyMessagePath": "Path copied",
"copyMessageValue": "Value copied", "copyMessageValue": "Value copied",
@ -511,8 +516,8 @@
"selectAllConnected": "Select connected", "selectAllConnected": "Select connected",
"addRemoveNode": "Add/remove node from selection", "addRemoveNode": "Add/remove node from selection",
"editSelected": "Edit selected node", "editSelected": "Edit selected node",
"deleteSelected": "Delete selected nodes or link", "deleteSelected": "Delete selection",
"deleteReconnect": "Delete and Reconnect", "deleteReconnect": "Delete and reconnect",
"importNode": "Import nodes", "importNode": "Import nodes",
"exportNode": "Export nodes", "exportNode": "Export nodes",
"nudgeNode": "Move selected nodes (1px)", "nudgeNode": "Move selected nodes (1px)",
@ -703,7 +708,7 @@
"triggerAction": "Trigger action", "triggerAction": "Trigger action",
"find": "Find in workspace", "find": "Find in workspace",
"copyItemUrl": "Copy item url", "copyItemUrl": "Copy item url",
"copyURL2Clipboard": "Copied url to clipboard", "copyURL2Clipboard": "Copied url to clipboard",
"showFlow": "Show", "showFlow": "Show",
"hideFlow": "Hide" "hideFlow": "Hide"
}, },
@ -920,6 +925,12 @@
"jsonata": "expression", "jsonata": "expression",
"env": "env variable", "env": "env variable",
"cred": "credential" "cred": "credential"
},
"date": {
"format": {
"timestamp": "milliseconds since epoch",
"object": "JavaScript Date Object"
}
} }
}, },
"editableList": { "editableList": {
@ -1202,22 +1213,22 @@
"title": "System Info" "title": "System Info"
}, },
"languages": { "languages": {
"de": "German", "de": "Deutsch",
"en-US": "English", "en-US": "English",
"fr": "French", "es-ES": "Español (España)",
"ja": "Japanese", "fr": "Français",
"ja": "日本語",
"ko": "Korean", "ko": "Korean",
"pt-BR":"Portuguese", "pt-BR": "Português (Brasil)",
"ru": "Russian", "ru": "Русский",
"zh-CN": "Chinese(Simplified)", "zh-CN": "简体中文",
"zh-TW": "Chinese(Traditional)" "zh-TW": "繁體中文"
}, },
"validator": { "validator": {
"errors": { "errors": {
"invalid-json": "Invalid JSON data: __error__", "invalid-json": "Invalid JSON data: __error__",
"invalid-json-prop": "__prop__: invalid JSON data: __error__", "invalid-expr": "Invalid JSONata expression: __error__",
"invalid-prop": "Invalid property expression", "invalid-prop": "Invalid property expression",
"invalid-prop-prop": "__prop__: invalid property expression",
"invalid-num": "Invalid number", "invalid-num": "Invalid number",
"invalid-num-prop": "__prop__: invalid number", "invalid-num-prop": "__prop__: invalid number",
"invalid-regexp": "Invalid input pattern", "invalid-regexp": "Invalid input pattern",
@ -1229,6 +1240,7 @@
} }
}, },
"contextMenu": { "contextMenu": {
"showActionList": "Show action list",
"insert": "Insert", "insert": "Insert",
"node": "Node", "node": "Node",
"junction": "Junction", "junction": "Junction",

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,26 @@
{
"info": {
"tip0": "Puedes eliminar los nodos o enlaces seleccionados con {{core:delete-selection}}",
"tip1": "Busca nodos con {{core:search}}",
"tip2": "{{core:toggle-sidebar}} alternará la vista de esta barra lateral",
"tip3": "Puedes gestionar tu paleta de nodos con {{core:manage-palette}}",
"tip4": "Tus nodos de configuración de flujo aparecen en el panel de la barra lateral. Se puede acceder desde el menú o con {{core:show-config-tab}}",
"tip5": "Activa o desactiva estos consejos desde la opción en la configuración",
"tip6": "Mueve los nodos seleccionados usando las teclas [izquierda] [arriba] [abajo] y [derecha]. Mantén pulsada [Mayús] para desplazarlos más",
"tip7": "Arrastrar un nodo a un cable lo insertará en el enlace",
"tip8": "Exporta los nodos seleccionados, o la pestaña actual con {{core:show-export-dialog}}",
"tip9": "Importa un flujo arrastrando su JSON al editor, o con {{core:show-import-dialog}}",
"tip10": "[shift][clic] y arrastrar en un puerto de nodo para mover todos los cables conectados o sólo el seleccionado",
"tip11": "Mostrar la pestaña Información con {{core:show-info-tab}} o la pestaña Depuración con {{core:show-debug-tab}}",
"tip12": "[ctrl] [clic] en el área de trabajo para abrir el diálogo de adición rápida",
"tip13": "Mantén pulsada [ctrl] cuando [haces clic] en un puerto de nodo para habilitar el enlazado rápido",
"tip14": "Mantén pulsada [shift] cuando [haces clic] en un nodo para seleccionar también todos sus nodos conectados",
"tip15": "Mantén pulsada [ctrl] cuando [haces clic] en un nodo para añadirlo o eliminarlo de la selección actual",
"tip16": "Cambia de pestaña de flujo con {{core:show-previous-tab}} y {{core:show-next-tab}}",
"tip17": "Puedes confirmar tus cambios en la bandeja de edición de nodos con {{core:confirm-edit-tray}} o cancelarlos con {{core:cancel-edit-tray}}",
"tip18": "Al pulsar {{core:edit-selected-node}} se editará el primer nodo de la selección actual"
}
}

View File

@ -0,0 +1,278 @@
{
"$string": {
"args": "arg[, prettify]",
"desc": "Convierte el parámetro `arg` a una cadena usando las siguientes reglas de conversión:\n\n - Las cadenas no cambian\n - Las funciones se convierten en una cadena vacía\n - El infinito numérico y NaN arrojan un error porque no se pueden representar como un número JSON\n: todos los demás valores se convierten a una cadena JSON usando la función `JSON.stringify`. Si `prettify` es verdadero, entonces se produce JSON \"prettified\". es decir, una línea por campo y las líneas se indentarán según la profundidad del campo."
},
"$length": {
"args": "str",
"desc": "Devuelve el número de caracteres de la cadena `str`. Se genera un error si `str` no es una cadena."
},
"$substring": {
"args": "str, start[, length]",
"desc": "Devuelve una cadena que contiene los caracteres del primer parámetro `str` comenzando en la posición `start` (desplazamiento cero). Si se especifica 'longitud', la subcadena contendrá el máximo de caracteres de 'longitud'. Si 'inicio' es negativo, indica el número de caracteres desde el final de 'cadena'."
},
"$substringBefore": {
"args": "str, chars",
"desc": "Devuelve la subcadena antes de la primera aparición de la secuencia de caracteres `chars` en `str`. Si `str` no contiene `caracteres`, entonces devuelve `str`."
},
"$substringAfter": {
"args": "str, chars",
"desc": "Devuelve la subcadena después de la primera aparición de la secuencia de caracteres `chars` en `str`. Si `str` no contiene `caracteres`, entonces devuelve `str`."
},
"$uppercase": {
"args": "str",
"desc": "Devuelve una cadena con todos los caracteres de `str` convertidos a mayúsculas."
},
"$lowercase": {
"args": "str",
"desc": "Devuelve una cadena con todos los caracteres de `str` convertidos a minúsculas."
},
"$trim": {
"args": "str",
"desc": "Normaliza y recorta todos los caracteres de espacio en blanco en `str` aplicando los siguientes pasos:\n\n - Todas las tabulaciones, retornos de carro y avances de línea se reemplazan con espacios.\n- Las secuencias contiguas de espacios se reducen a un solo espacio.\n- Se eliminan los espacios iniciales y finales.\n\n Si no se especifica `str` (es decir, esta función se invoca sin argumentos), entonces el valor de contexto se utiliza como el valor de `str`. Se genera un error si `str` no es una cadena."
},
"$contains": {
"args": "str, pattern",
"desc": "Devuelve 'verdadero' si 'cadena' coincide con 'patrón', de lo contrario, devuelve 'falso'. Si no se especifica `str` (es decir, esta función se invoca con un argumento), entonces el valor del contexto se utiliza como valor de `str`. El parámetro `patrón` puede ser una cadena o una expresión regular."
},
"$split": {
"args": "str[, separator][, limit]",
"desc": "Divide el parámetro `str` en una matriz de subcadenas. Es un error si `str` no es una cadena. El parámetro opcional `separador` especifica los caracteres dentro de la `cadena` sobre los cuales se debe dividir como una cadena o una expresión regular. Si no se especifica 'separador', se supone que la cadena está vacía y 'cadena' se dividirá en una matriz de caracteres individuales. Es un error si el 'separador' no es una cadena. El parámetro opcional 'límite' es un número que especifica el número máximo de subcadenas que se incluirán en la matriz resultante. Cualquier subcadena adicional se descarta. Si no se especifica `límite`, entonces `str` se divide completamente sin límite para el tamaño de la matriz resultante. Es un error si 'límite' no es un número positivo."
},
"$join": {
"args": "array[, separator]",
"desc": "Une una matriz de cadenas de componentes en una única cadena concatenada con cada cadena de componentes separada por el parámetro 'separador' opcional. Es un error si la 'matriz' de entrada contiene un elemento que no es una cadena. Si no se especifica 'separador', se supone que es una cadena vacía, es decir, que no hay 'separador' entre las cadenas componentes. Es un error si el 'separador' no es una cadena."
},
"$match": {
"args": "str, pattern [, limit]",
"desc": "Aplica la cadena `str` a la expresión regular `pattern` y devuelve una matriz de objetos, cada objeto contiene información sobre cada aparición de una coincidencia dentro de `str`."
},
"$replace": {
"args": "str, pattern, replacement [, limit]",
"desc": "Encuentra apariciones de `patrón` dentro de `str` y las reemplaza con `reemplazo`.\n\nEl parámetro opcional `límite` es el número máximo de reemplazos."
},
"$now": {
"args": "$[picture [, timezone]]",
"desc": "Genera una marca de tiempo en formato compatible con ISO 8601 y la devuelve como una cadena. Si se proporcionan los parámetros opcionales `picture` y `zona horaria`, entonces la marca de tiempo actual se formatea como se describe en la función `$fromMillis()`"
},
"$base64encode": {
"args": "string",
"desc": "Convierte una cadena ASCII a una representación base 64. Cada carácter de la cadena se trata como un byte de datos binarios. Esto requiere que todos los caracteres de la cadena estén en el rango de 0x00 a 0xFF, que incluye todos los caracteres de las cadenas codificadas con URI. No se admiten caracteres Unicode fuera de ese rango."
},
"$base64decode": {
"args": "string",
"desc": "Convierte bytes codificados en base 64 en una cadena, utilizando una página de códigos Unicode UTF-8."
},
"$number": {
"args": "arg",
"desc": "Convierte el parámetro `arg` a un número usando las siguientes reglas de conversión:\n\n - Los números no cambian\n - Las cadenas que contienen una secuencia de caracteres que representan un número JSON legal se convierten a ese número\n - Todos los demás valores provocar que se arroje un error."
},
"$abs": {
"args": "number",
"desc": "Devuelve el valor absoluto del parámetro 'número'."
},
"$floor": {
"args": "number",
"desc": "Devuelve el valor de 'número' redondeado hacia abajo al entero más cercano que sea menor o igual a 'número'."
},
"$ceil": {
"args": "number",
"desc": "Devuelve el valor de 'número' redondeado al número entero más cercano que sea mayor o igual a 'número'."
},
"$round": {
"args": "number [, precision]",
"desc": "Devuelve el valor del parámetro 'número' redondeado al número de decimales especificado por el parámetro opcional 'precisión'."
},
"$power": {
"args": "base, exponent",
"desc": "Devuelve el valor de 'base' elevado a la potencia de 'exponente'."
},
"$sqrt": {
"args": "number",
"desc": "Devuelve la raíz cuadrada del valor del parámetro 'número'."
},
"$random": {
"args": "",
"desc": "Devuelve un número pseudoaleatorio mayor o igual a cero y menor que uno."
},
"$millis": {
"args": "",
"desc": "Devuelve el número de milisegundos desde la época Unix (1 de enero de 1970 UTC) como un número. Todas las invocaciones de `$millis()` dentro de una evaluación de una expresión devolverán el mismo valor."
},
"$sum": {
"args": "array",
"desc": "Devuelve la suma aritmética de una 'matriz' de números. Es un error si la 'matriz' de entrada contiene un elemento que no es un número."
},
"$max": {
"args": "array",
"desc": "Devuelve el número máximo en una 'matriz' de números. Es un error si la 'matriz' de entrada contiene un elemento que no es un número."
},
"$min": {
"args": "array",
"desc": "Devuelve el número mínimo en una 'matriz' de números. Es un error si la 'matriz' de entrada contiene un elemento que no es un número."
},
"$average": {
"args": "array",
"desc": "Devuelve el valor medio de una 'matriz' de números. Es un error si la 'matriz' de entrada contiene un elemento que no es un número."
},
"$boolean": {
"args": "arg",
"desc": "Convierte el argumento a un booleano usando las siguientes reglas:\n\n - `Booleano`: sin cambios\n - `cadena`: vacía: `falso`\n - `cadena`: no vacía: `verdadero`\n - `número`: `0`: `falso`\n - `número`: distinto de cero: `verdadero`\n - `nulo`: `falso`\n - `matriz`: vacía: `falso`\n - `array`: contiene un miembro que se convierte en `true`: `true`\n - `array`: todos los miembros se convierten en `false`: `false`\n - `object`: vacío: `false`\n - `objeto`: no vacío: `verdadero`\n - `función`: `falso`"
},
"$not": {
"args": "arg",
"desc": "Devuelve booleano NEGADO del argumento. `arg` se convierte antes en un booleano"
},
"$exists": {
"args": "arg",
"desc": "Devuelve booleano 'verdadero' si la expresión 'arg' se evalúa como un valor, o 'falso' si la expresión no coincide con nada (por ejemplo, una ruta a una referencia de campo inexistente)."
},
"$count": {
"args": "array",
"desc": "Devuelve el número de elementos de la matriz."
},
"$append": {
"args": "array, array",
"desc": "Agrega dos matrices"
},
"$sort": {
"args": "array [, function]",
"desc": "Devuelve una matriz que contiene todos los valores en el parámetro `array`, pero ordenados.\n\nSi se proporciona una `función` de comparador, entonces debe ser una función que toma dos parámetros:\n\n`function(left , derecha)`\n\nEsta función es invocada por el algoritmo de clasificación para comparar dos valores `izquierda` y `derecha`. Si el valor de `izquierda` debe colocarse después del valor de `derecha` en el orden de clasificación deseado, entonces la función debe devolver un valor booleano 'verdadero' para indicar un intercambio. De lo contrario debe devolver 'falso'."
},
"$reverse": {
"args": "array",
"desc": "Devuelve una matriz que contiene todos los valores del parámetro `matriz`, pero en orden inverso."
},
"$shuffle": {
"args": "array",
"desc": "Devuelve una matriz que contiene todos los valores del parámetro `array`, pero mezclados en orden aleatorio."
},
"$zip": {
"args": "array, ...",
"desc": "Devuelve una matriz convolucionada (comprimida) que contiene matrices agrupadas de valores de los argumentos `matriz1`... `matrizN` del índice 0, 1, 2...."
},
"$keys": {
"args": "object",
"desc": "Devuelve una matriz que contiene las claves del objeto. Si el argumento es una matriz de objetos, entonces la matriz devuelta contiene una lista deduplicada de todas las claves de todos los objetos."
},
"$lookup": {
"args": "object, key",
"desc": "Devuelve el valor asociado con la clave en el objeto. Si el primer argumento es una matriz de objetos, entonces se buscan todos los objetos de la matriz y se devuelven los valores asociados con todas las apariciones de la clave."
},
"$spread": {
"args": "object",
"desc": "Divide un objeto que contiene pares clave/valor en una matriz de objetos, cada uno de los cuales tiene un único par clave/valor del objeto de entrada. Si el parámetro es una matriz de objetos, entonces la matriz resultante contiene un objeto para cada par clave/valor en cada objeto de la matriz proporcionada."
},
"$merge": {
"args": "array&lt;object&gt;",
"desc": "Fusiona una matriz de objetos en un único objeto que contiene todos los pares clave/valor de cada uno de los objetos en la matriz de entrada. Si alguno de los objetos de entrada contiene la misma clave, entonces el objeto devuelto contendrá el valor del último en la matriz. Es un error si la matriz de entrada contiene un elemento que no es un objeto."
},
"$sift": {
"args": "object, function",
"desc": "Devuelve un objeto que contiene solo los pares clave/valor del parámetro `objeto` que satisfacen el predicado `función` pasado como segundo parámetro.\n\nLa `función` que se proporciona como segundo parámetro debe tener la siguiente firma:\n\n`función(valor [, clave [, objeto]])`"
},
"$each": {
"args": "object, function",
"desc": "Devuelve una matriz que contiene los valores devueltos por la función cuando se aplica a cada par clave/valor en el objeto."
},
"$map": {
"args": "array, function",
"desc": "Devuelve una matriz que contiene los resultados de aplicar el parámetro `función` a cada valor en el parámetro `matriz`.\n\nLa `función` que se proporciona como segundo parámetro debe tener la siguiente firma:\n\n`función( valor [, índice [, matriz]])`"
},
"$filter": {
"args": "array, function",
"desc": "Devuelve una matriz que contiene solo los valores en el parámetro `matriz` que satisfacen el predicado `función`.\n\nLa `función` que se proporciona como segundo parámetro debe tener la siguiente firma:\n\n`función(valor [ , índice [, matriz]])`"
},
"$reduce": {
"args": "array, function [, init]",
"desc": "Devuelve un valor agregado derivado de aplicar el parámetro `función` sucesivamente a cada valor en `matriz` en combinación con el resultado de la aplicación anterior de la función.\n\nLa función debe aceptar dos argumentos y se comporta como un operador infijo entre cada valor dentro de la matriz. La firma de la `función` debe tener la forma: `myfunc($accumulator, $value[, $index[, $array]])`\n\nEl parámetro opcional `init` se utiliza como valor inicial en la agregación."
},
"$flowContext": {
"args": "string[, string]",
"desc": "Recupera una propiedad de contexto de flujo.\n\nEsta es una función definida por Node-RED."
},
"$globalContext": {
"args": "string[, string]",
"desc": "Recupera una propiedad de contexto global.\n\nEsta es una función definida por Node-RED."
},
"$pad": {
"args": "string, width [, char]",
"desc": "Devuelve una copia de la `cadena` con relleno adicional, si es necesario, de modo que su número total de caracteres sea al menos el valor absoluto del parámetro `ancho`.\n\nSi `ancho` es un número positivo, entonces la cadena está acolchado hacia la derecha; si es negativo, se rellena hacia la izquierda.\n\nEl argumento opcional `char` especifica los caracteres de relleno que se utilizarán. Si no se especifica, el valor predeterminado es el carácter de espacio."
},
"$fromMillis": {
"args": "number, [, picture [, timezone]]",
"desc": "Convierte el `número` que representa milisegundos desde la época Unix (1 de enero de 1970 UTC) en una representación de cadena formateada según la plantilla en picture.\n\nSi se omite el parámetro opcional `picture`, entonces la marca de tiempo es formateado en el formato ISO 8601.\n\nSi se proporciona la cadena opcional `picture`, entonces la marca de tiempo se formatea de acuerdo con la representación especificada en esa cadena. El comportamiento de esta función es consistente con la versión de dos argumentos de la función XPath/XQuery `format-dateTime` tal como se define en la especificación XPath F&O 3.1. El parámetro de plantilla define cómo se formatea la marca de tiempo y tiene la misma sintaxis que `format-dateTime`.\n\nSi se proporciona la cadena opcional `timezone`, entonces la marca de tiempo formateada estará en esa zona horaria. La cadena `timezone` debe tener el formato '±HHMM', donde ± es el signo más o menos y HHMM es el desplazamiento en horas y minutos desde UTC. Desplazamiento positivo para zonas horarias al este de UTC, desplazamiento negativo para zonas horarias al oeste de UTC."
},
"$formatNumber": {
"args": "number, picture [, options]",
"desc": "Convierte el `número` en una cadena y lo formatea en una representación decimal según lo especificado en la cadena `picture`.\n\n El comportamiento de esta función es coherente con la función XPath/XQuery `fn:format-number` tal como se define en la especificación XPath F&O 3.1. El parámetro de cadena `picture` define cómo se formatea el número y tiene la misma sintaxis que `fn:formato-número`.\n\nEl tercer argumento opcional `opciones` se utiliza para anular los caracteres de formato específicos de la configuración regional predeterminada, como el decimal. separador. Si se proporciona, este argumento debe ser un objeto que contenga pares de nombre/valor especificados en la sección de formato decimal de la especificación XPath F&O 3.1."
},
"$formatBase": {
"args": "number [, radix]",
"desc": "Convierte el número en una cadena y lo formatea como un número entero representado en la base numérica especificada por el argumento `radix`. Si no se especifica `radix`, el valor predeterminado es la base 10. `radix` puede estar entre 2 y 36; de lo contrario, se genera un error."
},
"$toMillis": {
"args": "timestamp",
"desc": "Convierte una cadena de `marca de tiempo` en el formato ISO 8601 al número de milisegundos desde la época Unix (1 de enero de 1970 UTC) como un número. Se genera un error si la cadena no tiene el formato correcto."
},
"$env": {
"args": "arg",
"desc": "Devuelve el valor de una variable de entorno.\n\nEsta es una función definida por Node-RED."
},
"$eval": {
"args": "expr [, context]",
"desc": "Analiza y evalúa la cadena `expr` que contiene JSON literal o una expresión JSONata utilizando el contexto actual como contexto para la evaluación."
},
"$formatInteger": {
"args": "number, picture",
"desc": "Convierte el número en una cadena y lo formatea en una representación entera como lo especifica la cadena `picture`. El parámetro de define cómo se formatea el número y tiene la misma sintaxis que `fn:format-integer` de la especificación XPath F&O 3.1."
},
"$parseInteger": {
"args": "string, picture",
"desc": "Analiza el contenido del parámetro cadena en un número entero (como un número JSON) utilizando el formato especificado por la cadena `picture`. El parámetro tiene el mismo formato que `$formatInteger`."
},
"$error": {
"args": "[str]",
"desc": "Lanza un error con un mensaje. El parámetro `str` opcional reemplazará el mensaje predeterminado de `$error() función evaluada`"
},
"$assert": {
"args": "arg, str",
"desc": "Si `arg` es `verdadero`, la función devuelve indefinido. Si `arg` es `falso`, se lanza una excepción con `str` como mensaje de excepción."
},
"$single": {
"args": "array, function",
"desc": "Devuelve el único valor en el parámetro `array` que satisface el predicado de `función` (es decir, la `función` devuelve booleano `verdadero` cuando se pasa el valor). Lanza una excepción si el número de valores coincidentes no es exactamente uno.\n\nLa función debe proporcionarse con la siguiente firma: `función(valor [, índice [, matriz]])` donde el valor es cada entrada de la matriz. El índice es la posición de ese valor y toda la matriz se pasa como tercer argumento."
},
"$encodeUrlComponent": {
"args": "str",
"desc": "Codifica un componente de URL reemplazando cada instancia de ciertos caracteres por una, dos, tres o cuatro secuencias de escape que representan la codificación UTF-8 del carácter.\n\nEjemplo: `$encodeUrlComponent(\"?x=prueba\")` => `\"%3Fx%3Dprueba\"`"
},
"$encodeUrl": {
"args": "str",
"desc": "Codifica una URL reemplazando cada instancia de ciertos caracteres por una, dos, tres o cuatro secuencias de escape que representan la codificación UTF-8 del carácter.\n\nEjemplo: `$encodeUrl(\"https://mozilla.org/?x=шеллы\")` => `\"https://mozilla.org/?x=%D1%88%D0%B5%D0%BB%D0%BB%D1%8B\"`"
},
"$decodeUrlComponent": {
"args": "str",
"desc": "Decodifica un componente de URL creado previamente por encodeUrlComponent.\n\nEjemplo: `$decodeUrlComponent(\"%3Fx%3Dtest\")` => `\"?x=test\"`"
},
"$decodeUrl": {
"args": "str",
"desc": "Decodifica una URL creado previamente por encodeUrl.\n\nEjemplo: `$decodeUrl(\"https://mozilla.org/?x=%D1%88%D0%B5%D0%BB%D0%BB%D1%8B\")` => `\"https://mozilla.org/?x=шеллы\"`"
},
"$distinct": {
"args": "array",
"desc": "Devuelve una matriz con valores duplicados eliminados de `matriz`"
},
"$type": {
"args": "value",
"desc": "Devuelve el tipo de `valor` como una cadena. Si `valor` no está definido, esto devolverá indefinido."
},
"$moment": {
"args": "[str]",
"desc": "Obtiene un objeto de fecha usando la biblioteca Moment."
},
"$clone": {
"args": "value",
"desc": "Clona un objeto de forma segura."
}
}

View File

@ -119,10 +119,9 @@
"searchInput": "Rechercher vos flux", "searchInput": "Rechercher vos flux",
"subflows": "Sous-flux", "subflows": "Sous-flux",
"createSubflow": "Créer un sous-flux", "createSubflow": "Créer un sous-flux",
"selectionToSubflow": "Selection d'un sous-flux", "selectionToSubflow": "Convertir en sous-flux",
"flows": "Flux", "flows": "Flux",
"add": "Ajouter", "add": "Ajouter",
"rename": "Renommer",
"delete": "Supprimer", "delete": "Supprimer",
"keyboardShortcuts": "Raccourcis clavier", "keyboardShortcuts": "Raccourcis clavier",
"login": "Se connecter", "login": "Se connecter",
@ -130,6 +129,11 @@
"editPalette": "Gérer la palette", "editPalette": "Gérer la palette",
"other": "Autre", "other": "Autre",
"showTips": "Afficher les astuces", "showTips": "Afficher les astuces",
"showNodeHelp": "Afficher l'aide du noeud",
"enableSelectedNodes": "Activer les noeuds sélectionnés",
"disableSelectedNodes": "Désactiver les noeuds sélectionnés",
"showSelectedNodeLabels": "Afficher les étiquettes des noeuds sélectionnés",
"hideSelectedNodeLabels": "Masquer les étiquettes des noeuds sélectionnés",
"showWelcomeTours": "Afficher les visites guidées pour les nouvelles versions", "showWelcomeTours": "Afficher les visites guidées pour les nouvelles versions",
"help": "Site web de Node-RED", "help": "Site web de Node-RED",
"projects": "Projets", "projects": "Projets",
@ -274,23 +278,23 @@
"recoveredNodesInfo": "Les noeuds importés sur ce flux contiennent un mauvais identifiant de flux. Ces noeuds ont été ajoutés à ce flux afin que vous puissiez les restaurer ou les supprimer.", "recoveredNodesInfo": "Les noeuds importés sur ce flux contiennent un mauvais identifiant de flux. Ces noeuds ont été ajoutés à ce flux afin que vous puissiez les restaurer ou les supprimer.",
"recoveredNodesNotification": "<p>Noeuds importés sans identifiant de flux valide</p><p>Ils ont été ajoutés à un nouveau flux appelé '__flowName__'.</p>", "recoveredNodesNotification": "<p>Noeuds importés sans identifiant de flux valide</p><p>Ils ont été ajoutés à un nouveau flux appelé '__flowName__'.</p>",
"export": { "export": {
"selected": "noeuds sélectionnés", "selected": "les noeuds sélectionnés",
"current": "flux actuel", "current": "le flux actuel",
"all": "tous les flux", "all": "tous les flux",
"compact": "condensé", "compact": "Condensé",
"formatted": "formaté", "formatted": "Formaté",
"copy": "Copier dans le presse-papier", "copy": "Copier dans le presse-papier",
"export": "Exporter vers la bibliothèque", "export": "Exporter vers la bibliothèque",
"exportAs": "Exporter en tant que", "exportAs": "Exporter comme",
"overwrite": "Remplacer", "overwrite": "Remplacer",
"exists": "<p><b>\"__file__\"</b> existe déjà.</p><p>Voulez-vous le remplacer ?</p>" "exists": "<p><b>\"__file__\"</b> existe déjà.</p><p>Voulez-vous le remplacer ?</p>"
}, },
"import": { "import": {
"import": "Importer vers", "import": "Importer vers",
"importSelected": "Importation sélectionnée", "importSelected": "Importer la sélection",
"importCopy": "Importer une copie", "importCopy": "Importer une copie",
"viewNodes": "Afficher les noeuds...", "viewNodes": "Vérifier ces noeuds",
"newFlow": "Nouveau flux", "newFlow": "un nouveau flux",
"replace": "Remplacer", "replace": "Remplacer",
"errors": { "errors": {
"notArray": "L'entrée n'est pas un tableau JSON", "notArray": "L'entrée n'est pas un tableau JSON",
@ -299,7 +303,8 @@
"missingType": "L'entrée n'est pas un flux valide - l'élément '__index__' n'a pas de propriété 'type'" "missingType": "L'entrée n'est pas un flux valide - l'élément '__index__' n'a pas de propriété 'type'"
}, },
"conflictNotification1": "Certains des noeuds que vous avez importés existent déjà dans votre espace de travail.", "conflictNotification1": "Certains des noeuds que vous avez importés existent déjà dans votre espace de travail.",
"conflictNotification2": "Sélectionner les noeuds à importer et choisir s'il faut remplacer les noeuds existants ou en importer une copie." "conflictNotification2": "Sélectionnez les noeuds à importer et choisissez s'il faut remplacer les noeuds existants ou en importer une copie.",
"alreadyExists": "Ce noeud existe déjà"
}, },
"copyMessagePath": "Chemin copié", "copyMessagePath": "Chemin copié",
"copyMessageValue": "Valeur copiée", "copyMessageValue": "Valeur copiée",
@ -391,10 +396,10 @@
"subflowInstances": "Il existe __count__ instance de ce modèle de sous-flux", "subflowInstances": "Il existe __count__ instance de ce modèle de sous-flux",
"subflowInstances_plural": "Il existe __count__ instances de ce modèle de sous-flux", "subflowInstances_plural": "Il existe __count__ instances de ce modèle de sous-flux",
"editSubflowProperties": "modifier les propriétés", "editSubflowProperties": "modifier les propriétés",
"input": "entrées:", "input": "Entrées:",
"output": "sorties:", "output": "Sorties:",
"status": "statut du noeud", "status": "Statut du noeud",
"deleteSubflow": "supprimer le sous-flux", "deleteSubflow": "Supprimer le sous-flux",
"confirmDelete": "Voulez-vous vraiment supprimer ce sous-flux ?", "confirmDelete": "Voulez-vous vraiment supprimer ce sous-flux ?",
"info": "Description", "info": "Description",
"category": "Catégorie", "category": "Catégorie",
@ -416,6 +421,7 @@
}, },
"errors": { "errors": {
"noNodesSelected": "<strong>Impossible de créer un sous-flux</strong> : aucun noeud sélectionné", "noNodesSelected": "<strong>Impossible de créer un sous-flux</strong> : aucun noeud sélectionné",
"acrossMultipleGroups": "Impossible de créer un sous-flux sur plusieurs groupes",
"multipleInputsToSelection": "<strong>Impossible de créer un sous-flux</strong> : plusieurs entrées pour la sélection" "multipleInputsToSelection": "<strong>Impossible de créer un sous-flux</strong> : plusieurs entrées pour la sélection"
} }
}, },
@ -447,8 +453,8 @@
"default": "Par défaut", "default": "Par défaut",
"noDefaultLabel": "Aucune", "noDefaultLabel": "Aucune",
"defaultLabel": "Utiliser l'étiquette par défaut", "defaultLabel": "Utiliser l'étiquette par défaut",
"searchIcons": "Icônes de recherche", "searchIcons": "Rechercher une icône",
"useDefault": "Utilisation par défaut", "useDefault": "Icône par défaut",
"description": "Description", "description": "Description",
"show": "Afficher", "show": "Afficher",
"hide": "Masquer", "hide": "Masquer",
@ -498,19 +504,19 @@
"keyboard": { "keyboard": {
"title": "Raccourcis clavier", "title": "Raccourcis clavier",
"keyboard": "Clavier", "keyboard": "Clavier",
"filterActions": "Actions de filtrage", "filterActions": "Rechercher l'action",
"shortcut": "raccourci", "shortcut": "Raccourci",
"scope": "portée", "scope": "Portée",
"unassigned": "Non attribué", "unassigned": "Non attribué",
"global": "global", "global": "Global",
"workspace": "espace de travail", "workspace": "Espace de travail",
"editor": "boîte de dialogue d'édition", "editor": "Boîte de dialogue d'édition",
"selectAll": "Tout sélectionner", "selectAll": "Tout sélectionner",
"selectNone": "Ne rien sélectionner", "selectNone": "Ne rien sélectionner",
"selectAllConnected": "Sélectionner tous les éléments connectés", "selectAllConnected": "Sélectionner tous les éléments connectés",
"addRemoveNode": "Ajouter/supprimer un noeud de la sélection", "addRemoveNode": "Ajouter/supprimer un noeud de la sélection",
"editSelected": "Modifier le noeud sélectionné", "editSelected": "Modifier le noeud sélectionné",
"deleteSelected": "Supprimer les noeuds ou le lien sélectionné(s)", "deleteSelected": "Supprimer la sélection",
"deleteReconnect": "Supprimer et reconnecter", "deleteReconnect": "Supprimer et reconnecter",
"importNode": "Importer les noeuds", "importNode": "Importer les noeuds",
"exportNode": "Exporter les noeuds", "exportNode": "Exporter les noeuds",
@ -550,22 +556,22 @@
}, },
"palette": { "palette": {
"noInfo": "Pas d'information disponible", "noInfo": "Pas d'information disponible",
"filter": "Filtrer les noeuds", "filter": "Rechercher le noeud",
"search": "Rechercher les modules", "search": "Rechercher les modules",
"addCategory": "Ajouter un nouveau...", "addCategory": "Ajouter un nouveau...",
"label": { "label": {
"subflows": "sous-flux", "subflows": "Sous-flux",
"network": "réseau", "network": "Réseau",
"common": "commun", "common": "Commun",
"input": "entrée", "input": "Entrée",
"output": "sortie", "output": "Sortie",
"function": "fonction", "function": "Fonction",
"sequence": "séquence", "sequence": "Séquence",
"parser": "analyseur", "parser": "Analyseur",
"social": "social", "social": "Social",
"storage": "stockage", "storage": "Stockage",
"analysis": "analyse", "analysis": "Analyse",
"advanced": "avancé" "advanced": "Avancé"
}, },
"actions": { "actions": {
"collapse-all": "Réduire toutes les catégories", "collapse-all": "Réduire toutes les catégories",
@ -586,6 +592,7 @@
"editor": { "editor": {
"title": "Gérer la palette", "title": "Gérer la palette",
"palette": "Palette", "palette": "Palette",
"allCatalogs": "Tous les catalogues",
"times": { "times": {
"seconds": "il y a quelques secondes", "seconds": "il y a quelques secondes",
"minutes": "il y a quelques minutes", "minutes": "il y a quelques minutes",
@ -609,24 +616,25 @@
"nodeCount_plural": "__label__ noeuds", "nodeCount_plural": "__label__ noeuds",
"moduleCount": "__count__ module disponible", "moduleCount": "__count__ module disponible",
"moduleCount_plural": "__count__ modules disponibles", "moduleCount_plural": "__count__ modules disponibles",
"inuse": "en cours d'utilisation", "inuse": "En cours d'utilisation",
"enableall": "activer tout", "enableall": "Activer tout",
"disableall": "désactiver tout", "disableall": "Désactiver tout",
"enable": "activer", "enable": "Activer",
"disable": "désactiver", "disable": "Désactiver",
"remove": "supprimer", "remove": "Supprimer",
"update": "mettre à jour vers __version__", "update": "Mettre à jour vers __version__",
"updated": "mis à jour", "updated": "Mis à jour",
"install": "installer", "install": "Installer",
"installed": "installé", "installed": "Installé",
"conflict": "conflit", "conflict": "Conflit",
"conflictTip": "<p>Ce module ne peut pas être installé car il inclut un<br/>type de noeud qui a déjà été installé</p><p>Conflits avec <code>__module__</code></p>", "conflictTip": "<p>Ce module ne peut pas être installé car il inclut un<br/>type de noeud qui a déjà été installé</p><p>Conflits avec <code>__module__</code></p>",
"loading": "Chargement des catalogues...", "loading": "Chargement des catalogues...",
"tab-nodes": "Noeuds", "tab-nodes": "Noeuds",
"tab-install": "Installer", "tab-install": "Installer",
"sort": "trier:", "sort": "Trier:",
"sortAZ": "a-z", "sortRelevance": "Pertinence",
"sortRecent": "récent", "sortAZ": "A-Z",
"sortRecent": "Récent",
"more": "+ __count__ en plus", "more": "+ __count__ en plus",
"upload": "Charger le fichier tgz du module", "upload": "Charger le fichier tgz du module",
"refresh": "Actualiser la liste des modules", "refresh": "Actualiser la liste des modules",
@ -667,7 +675,7 @@
"info": { "info": {
"name": "Information", "name": "Information",
"tabName": "Nom", "tabName": "Nom",
"label": "info", "label": "Info",
"node": "Noeud", "node": "Noeud",
"type": "Type", "type": "Type",
"group": "Groupe", "group": "Groupe",
@ -681,10 +689,10 @@
"properties": "Propriétés", "properties": "Propriétés",
"info": "Information", "info": "Information",
"desc": "Description", "desc": "Description",
"blank": "vide", "blank": "Vide",
"null": "nul", "null": "Nul",
"showMore": "afficher en plus", "showMore": "Afficher en plus",
"showLess": "afficher en moins", "showLess": "Afficher en moins",
"flow": "Flux", "flow": "Flux",
"selection": "Sélection", "selection": "Sélection",
"nodes": "__count__ noeuds", "nodes": "__count__ noeuds",
@ -695,7 +703,7 @@
"arrayItems": "__count__ éléments", "arrayItems": "__count__ éléments",
"showTips": "Vous pouvez ouvrir les astuces à partir du panneau des paramètres", "showTips": "Vous pouvez ouvrir les astuces à partir du panneau des paramètres",
"outline": "Plan", "outline": "Plan",
"empty": "vide", "empty": "Vide",
"globalConfig": "Noeuds de configuration globale", "globalConfig": "Noeuds de configuration globale",
"triggerAction": "Déclencher une action", "triggerAction": "Déclencher une action",
"find": "Rechercher dans l'espace de travail", "find": "Rechercher dans l'espace de travail",
@ -706,7 +714,7 @@
}, },
"help": { "help": {
"name": "Aide", "name": "Aide",
"label": "aide", "label": "Aide",
"search": "Aide à la recherche", "search": "Aide à la recherche",
"nodeHelp": "Aide sur les noeuds", "nodeHelp": "Aide sur les noeuds",
"showHelp": "Afficher l'aide", "showHelp": "Afficher l'aide",
@ -717,23 +725,23 @@
}, },
"config": { "config": {
"name": "Noeuds de configuration", "name": "Noeuds de configuration",
"label": "configuration", "label": "Configuration",
"global": "Tous les flux", "global": "Tous les flux",
"none": "aucun", "none": "Aucun",
"subflows": "sous-flux", "subflows": "Sous-flux",
"flows": "flux", "flows": "Flux",
"filterAll": "tout", "filterAll": "Tout",
"showAllConfigNodes": "Afficher tous les noeuds de configuration", "showAllConfigNodes": "Afficher tous les noeuds de configuration",
"filterUnused": "inutilisé", "filterUnused": "Inutilisé",
"showAllUnusedConfigNodes": "Afficher tous les noeuds de configuration inutilisés", "showAllUnusedConfigNodes": "Afficher tous les noeuds de configuration inutilisés",
"filtered": "__count__ caché(s)" "filtered": "__count__ caché(s)"
}, },
"context": { "context": {
"name": "Données contextuelles", "name": "Données contextuelles",
"label": "contexte", "label": "Contexte",
"none": "aucune sélection", "none": "Aucune sélection",
"refresh": "actualiser pour charger", "refresh": "Actualiser pour charger",
"empty": "vide", "empty": "Vide",
"node": "Noeud", "node": "Noeud",
"flow": "Flux", "flow": "Flux",
"global": "Global", "global": "Global",
@ -744,10 +752,10 @@
}, },
"palette": { "palette": {
"name": "Gestion des palettes", "name": "Gestion des palettes",
"label": "palette" "label": "Palette"
}, },
"project": { "project": {
"label": "projet", "label": "Projet",
"name": "Projet", "name": "Projet",
"description": "Description", "description": "Description",
"dependencies": "Dépendances", "dependencies": "Dépendances",
@ -760,11 +768,11 @@
"showProjectSettings": "Afficher les paramètres du projet", "showProjectSettings": "Afficher les paramètres du projet",
"projectSettings": { "projectSettings": {
"title": "Paramètres du projet", "title": "Paramètres du projet",
"edit": "modifier", "edit": "Modifier",
"none": "Vide", "none": "Vide",
"install": "installer", "install": "Installer",
"removeFromProject": "supprimer du projet", "removeFromProject": "Supprimer du projet",
"addToProject": "ajouter au projet", "addToProject": "Ajouter au projet",
"files": "Fichiers", "files": "Fichiers",
"flow": "Flux", "flow": "Flux",
"credentials": "Identifiants", "credentials": "Identifiants",
@ -812,7 +820,7 @@
"workflowAutoTip": "Les modifications sont validées automatiquement à chaque déploiement", "workflowAutoTip": "Les modifications sont validées automatiquement à chaque déploiement",
"sshKeys": "Clés SSH", "sshKeys": "Clés SSH",
"sshKeysTip": "Vous permet de créer des connexions sécurisées aux référentiels Git distants.", "sshKeysTip": "Vous permet de créer des connexions sécurisées aux référentiels Git distants.",
"add": "ajouter une clé", "add": "Ajouter une clé",
"addSshKey": "Ajouter une clé SSH", "addSshKey": "Ajouter une clé SSH",
"addSshKeyTip": "Générer une nouvelle paire de clés publique/privée", "addSshKeyTip": "Générer une nouvelle paire de clés publique/privée",
"name": "Nom", "name": "Nom",
@ -826,7 +834,7 @@
"copyPublicKey": "Copier la clé publique dans le presse-papiers", "copyPublicKey": "Copier la clé publique dans le presse-papiers",
"delete": "Supprimer une clé", "delete": "Supprimer une clé",
"gitConfig": "Configuration Git", "gitConfig": "Configuration Git",
"deleteConfirm": "Êtes-vous sûr de vouloir supprimer la clé SSH __nom__ ? Ça ne peut pas être annulé." "deleteConfirm": "Êtes-vous sûr de vouloir supprimer la clé SSH __name__ ? Ça ne peut pas être annulé."
}, },
"versionControl": { "versionControl": {
"unstagedChanges": "Abandon des changements", "unstagedChanges": "Abandon des changements",
@ -848,7 +856,7 @@
"none": "Vide", "none": "Vide",
"conflictResolve": "Tous les conflits ont été résolus. Valider les modifications pour terminer la fusion.", "conflictResolve": "Tous les conflits ont été résolus. Valider les modifications pour terminer la fusion.",
"localFiles": "Fichiers locaux", "localFiles": "Fichiers locaux",
"all": "tout", "all": "Tout",
"unmergedChanges": "Modifications non fusionnées", "unmergedChanges": "Modifications non fusionnées",
"abortMerge": "Abandonner la fusion", "abortMerge": "Abandonner la fusion",
"commit": "Valider", "commit": "Valider",
@ -1097,9 +1105,9 @@
"desc8": "Le fichier contenant les identifiants ne sera pas crypté et son contenu sera facilement lisible", "desc8": "Le fichier contenant les identifiants ne sera pas crypté et son contenu sera facilement lisible",
"create-project-files": "Créer des fichiers de projet", "create-project-files": "Créer des fichiers de projet",
"create-project": "Créer un projet", "create-project": "Créer un projet",
"already-exists": "existe déjà", "already-exists": "Existe déjà",
"git-error": "Erreur Git", "git-error": "Erreur Git",
"git-auth-error": "erreur d'authentification Git" "git-auth-error": "Erreur d'authentification Git"
}, },
"create-success": { "create-success": {
"success": "Vous avez créé avec succès votre premier projet !", "success": "Vous avez créé avec succès votre premier projet !",
@ -1135,8 +1143,8 @@
"desc2": "Avant de pouvoir cloner un référentiel sur ssh, vous devez ajouter une clé SSH pour y accéder.", "desc2": "Avant de pouvoir cloner un référentiel sur ssh, vous devez ajouter une clé SSH pour y accéder.",
"add-ssh-key": "Ajouter une clé ssh", "add-ssh-key": "Ajouter une clé ssh",
"credentials-encryption-key": "Clé de chiffrement des identifiants", "credentials-encryption-key": "Clé de chiffrement des identifiants",
"already-exists-2": "existe déjà", "already-exists-2": "Existe déjà",
"git-error": "erreur git", "git-error": "Erreur git",
"con-failed": "La connexion a échoué", "con-failed": "La connexion a échoué",
"not-git": "Ce n'est pas un dépôt git", "not-git": "Ce n'est pas un dépôt git",
"no-resource": "Référentiel introuvable", "no-resource": "Référentiel introuvable",
@ -1148,8 +1156,8 @@
"confirm": "Voulez-vous vraiment supprimer ce projet ?" "confirm": "Voulez-vous vraiment supprimer ce projet ?"
}, },
"create-project-list": { "create-project-list": {
"search": "rechercher vos projets", "search": "Rechercher vos projets",
"current": "actuel" "current": "Actuel"
}, },
"require-clean": { "require-clean": {
"confirm": "<p>Vous avez des modifications non déployées qui seront perdues.</p><p>Voulez-vous continuer ?</p>" "confirm": "<p>Vous avez des modifications non déployées qui seront perdues.</p><p>Voulez-vous continuer ?</p>"
@ -1198,23 +1206,11 @@
"diagnostics": { "diagnostics": {
"title": "Information système" "title": "Information système"
}, },
"languages": {
"de": "Allemand",
"en-US": "Anglais",
"fr": "Français",
"ja": "Japonais",
"ko": "Coréen",
"pt-BR": "Portugais brésilien",
"ru": "Russe",
"zh-CN": "Chinois (Simplifié)",
"zh-TW": "Chinois (Traditionnel)"
},
"validator": { "validator": {
"errors": { "errors": {
"invalid-json": "Données JSON invalides : __error__", "invalid-json": "Données JSON invalides : __error__",
"invalid-json-prop": "__prop__: données JSON invalides : __error__", "invalid-expr": "Expression JSONata invalide : __error__",
"invalid-prop": "Expression de propriété non valide", "invalid-prop": "Expression de propriété invalide",
"invalid-prop-prop": "__prop__: expression de propriété invalide",
"invalid-num": "Numéro invalide", "invalid-num": "Numéro invalide",
"invalid-num-prop": "__prop__: numéro invalide", "invalid-num-prop": "__prop__: numéro invalide",
"invalid-regexp": "Modèle d'entrée non valide", "invalid-regexp": "Modèle d'entrée non valide",
@ -1226,6 +1222,7 @@
} }
}, },
"contextMenu": { "contextMenu": {
"showActionList": "Afficher la liste des actions",
"insert": "Insérer", "insert": "Insérer",
"node": "Noeud", "node": "Noeud",
"junction": "Jonction", "junction": "Jonction",
@ -1235,5 +1232,159 @@
"environment": "Environment", "environment": "Environment",
"header": "Variables d'environnement globales", "header": "Variables d'environnement globales",
"revert": "Rétablir" "revert": "Rétablir"
},
"action-list": {
"toggle-show-tips": "Basculer l'affichage des astuces",
"show-about": "Afficher la description de Node-RED",
"show-welcome-tour": "Afficher la visite de bienvenue",
"show-next-tab": "Afficher l'onglet suivant",
"show-previous-tab": "Afficher l'onglet précédent",
"add-flow": "Ajouter un flux",
"add-flow-to-right": "Ajouter un flux à droite",
"edit-flow": "Modifier le flux",
"remove-flow": "Supprimer le flux",
"enable-flow": "Activer le flux",
"disable-flow": "Désactiver le flux",
"hide-flow": "Masquer le flux",
"hide-other-flows": "Masquer les autres flux",
"hide-all-flows": "Masquer tous les flux",
"show-all-flows": "Afficher tous les flux",
"show-last-hidden-flow": "Afficher le dernier flux masqué",
"list-modified-nodes": "Afficher les flux modifiés",
"list-hidden-flows": "Afficher les flux cachés",
"list-flows": "Lister les flux",
"list-subflows": "Liste les sous-flux",
"go-to-previous-location": "Aller à l'emplacement précédent",
"go-to-next-location": "Aller à l'emplacement suivant",
"copy-selection-to-internal-clipboard": "Copier la sélection dans le presse-papiers",
"cut-selection-to-internal-clipboard": "Couper la sélection dans le presse-papiers",
"paste-from-internal-clipboard": "Coller depuis le presse-papiers",
"detach-selected-nodes": "Détacher les noeuds sélectionnés",
"delete-selection": "Supprimer la sélection",
"delete-selection-and-reconnect": "Supprimer la sélection et reconnecter",
"edit-selected-node": "Modifier le noeud sélectionné",
"go-to-selection": "Aller à la sélection",
"undo": "Annuler les modifications",
"redo": "Rétablir les modifications",
"select-all-nodes": "Sélectionner tous les noeuds",
"select-none": "Sélectionner un noeud",
"enable-selected-nodes": "Activer les noeuds sélectionnés",
"disable-selected-nodes": "Désactiver les noeuds sélectionnés",
"toggle-show-grid": "Basculer l'affichage de la grille",
"toggle-snap-grid": "Basculer l'aide au placement des noeuds",
"toggle-status": "Commuter l'état",
"show-selected-node-labels": "Afficher les étiquettes des noeuds sélectionnés",
"hide-selected-node-labels": "Masquer les étiquettes des noeuds sélectionnés",
"scroll-view-up": "Faire défiler vers le haut",
"scroll-view-right": "Faire défiler vers la droite",
"scroll-view-down": "Faire défiler vers le bas",
"scroll-view-left": "Faire défiler vers la gauche",
"step-view-up": "Faire défiler d'une unité vers le haut",
"step-view-right": "Faire défiler d'une unité vers la droite",
"step-view-down": "Faire défiler d'une unité vers le bas",
"step-view-left": "Faire défiler d'une unité vers la gauche",
"move-selection-up": "Déplacer la sélection vers le haut",
"move-selection-right": "Déplacer la sélection vers la droite",
"move-selection-down": "Déplacer la sélection vers le bas",
"move-selection-left": "Déplacer la sélection vers la gauche",
"move-selection-forwards": "Avancer la sélection",
"move-selection-backwards": "Reculer la sélection",
"move-selection-to-front": "Déplacer la sélection vers l'avant",
"move-selection-to-back": "Déplacer la sélection vers l'arrière",
"step-selection-up": "Déplacer la sélection d'une unité vers le haut",
"step-selection-right": "Déplacer la sélection d'une unité vers la droite",
"step-selection-down": "Déplacer la sélection d'une unité vers le bas",
"step-selection-left": "Déplacer la sélection d'une unité vers la gauche",
"select-connected-nodes": "Sélectionner les noeuds connectés",
"select-downstream-nodes": "Sélectionner les noeuds connectés en aval",
"select-upstream-nodes": "Sélectionner les noeuds connectés en amont",
"go-to-next-node": "Aller au noeud suivant",
"go-to-previous-node": "Aller au noeud précédent",
"go-to-next-sibling": "Aller au noeud frère suivant",
"go-to-previous-sibling": "Aller au noeud frère précédent",
"go-to-nearest-node-on-left": "Aller au noeud gauche le plus proche",
"go-to-nearest-node-on-right": "Aller au noeud droit le plus proche",
"go-to-nearest-node-above": "Aller au noeud supérieur le plus proche",
"go-to-nearest-node-below": "Aller au noeud le plus proche ci-dessous",
"align-selection-to-grid": "Aligner la sélection",
"align-selection-to-left": "Aligner la sélection à gauche",
"align-selection-to-right": "Aligner la sélection à droite",
"align-selection-to-top": "Aligner la sélection en haut",
"align-selection-to-bottom": "Aligner la sélection vers le bas",
"align-selection-to-middle": "Aligner la sélection au centre verticalement",
"align-selection-to-center": "Aligner la sélection au centre horizontalement",
"distribute-selection-horizontally": "Distribuer la sélection horizontalement",
"distribute-selection-vertical": "Distribuer la sélection verticalement",
"wire-series-of-nodes": "Connecter les noeuds en série",
"wire-node-to-multiple": "Connecter les noeuds à plusieurs",
"wire-multiple-to-node": "Connecter plusieurs au noeud",
"split-wire-with-link-nodes": "Diviser le fil avec des noeuds de liaison",
"generate-node-names": "Générer les noms de noeuds",
"show-user-settings": "Afficher les paramètres utilisateur",
"show-help": "Afficher l'aide",
"toggle-palette": "Basculer l'affichage de la palette",
"show-event-log": "Afficher le journal des événements",
"manage-palette": "Gérer la palette",
"toggle-sidebar": "Basculer l'affichage de la barre latérale",
"show-info-tab": "Afficher l'onglet d'informations sur le noeud",
"show-help-tab": "Afficher l'onglet d'aide du noeud",
"show-config-tab": "Afficher l'onglet du noeud de configuration",
"select-all-config-nodes": "Sélectionner tous les noeuds de configuration",
"delete-config-selection": "Supprimer le noeud de configuration sélectionné",
"show-context-tab": "Afficher l'onglet des données contextuelles",
"create-subflow": "Créer un sous-flux",
"convert-to-subflow": "Convertir la sélection en sous-flux",
"group-selection": "Grouper la sélection",
"ungroup-selection": "Dissocier la sélection",
"merge-selection-to-group": "Fusionner la sélection dans le groupe",
"remove-selection-from-group": "Supprimer la sélection du groupe",
"copy-group-style": "Copier le style du groupe",
"paste-group-style": "Coller le style du groupe",
"show-export-dialog": "Afficher la boîte de dialogue d'exportation",
"show-import-dialog": "Afficher la boîte de dialogue d'importation",
"show-library-export-dialog": "Afficher la boîte de dialogue d'exportation de la bibliothèque",
"show-library-import-dialog": "Afficher la boîte de dialogue d'importation de bibliothèque",
"show-examples-import-dialog": "Afficher la boîte de dialogue d'importation d'exemples",
"search": "Rechercher",
"search-previous": "Recherche précédente",
"search-next": "Recherche suivante",
"show-action-list": "Afficher la liste d'actions",
"confirm-edit-tray": "Confirmer la modification",
"cancel-edit-tray": "Annuler la modification",
"show-remote-diff": "Afficher les différences avec les modifications distantes",
"deploy-flows": "Déployer des flux",
"restart-flows": "Redémarrer les flux",
"set-deploy-type-to-full": "Définir le déploiement sur 'tout'",
"set-deploy-type-to-modified-flows": "Définir le déploiement sur 'flux modifiés'",
"set-deploy-type-to-modified-nodes": "Définir le déploiement sur 'noeuds modifiés'",
"show-debug-tab": "Afficher l'onglet de débogage",
"clear-debug-messages": "Supprimer les messages de débogage",
"clear-filtered-debug-messages": "Supprimer les messages de débogage filtrés",
"activate-selected-debug-nodes": "Activer les noeuds de débogage sélectionnés",
"activate-all-debug-nodes": "Activer tous les noeuds de débogage",
"activate-all-flow-debug-nodes": "Activer tous les noeuds de débogage dans un flux",
"deactivate-selected-debug-nodes": "Désactiver les noeuds de débogage sélectionnés",
"deactivate-all-debug-nodes": "Désactiver tous les noeuds de débogage",
"deactivate-all-flow-debug-nodes": "Désactiver tous les noeuds de débogage dans un flux",
"zoom-in": "Zoomer",
"zoom-out": "Dézoomer",
"zoom-reset": "Réinitialiser le zoom",
"toggle-navigator": "Basculer l'affichage du navigateur",
"show-system-info": "Afficher les informations système",
"split-wires-with-junctions": "Diviser les fils avec des jonctions",
"new-project": "Nouveau projet",
"open-project": "Ouvrir le projet",
"show-project-settings": "Afficher les paramètres du projet",
"show-version-control-tab": "Afficher l'onglet de contrôle de version",
"start-flows": "Démarrer les flux",
"stop-flows": "Arrêter les flux",
"copy-item-url": "Copier l'URL de l'élément",
"copy-item-edit-url": "Copier l'URL de modification de l'élément",
"move-flow-to-start": "Déplacer le flux jusqu'au début",
"move-flow-to-end": "Déplacer le flux jusqu'à la fin",
"show-global-env": "Afficher les variables d'environnement globales",
"lock-flow": "Verrouiller le flux",
"unlock-flow": "Déverrouiller le flux",
"show-node-help": "Afficher l'aide du noeud"
} }
} }

View File

@ -270,5 +270,9 @@
"$moment": { "$moment": {
"args": "[str]", "args": "[str]",
"desc": "Obtient un objet de date à l'aide de la bibliothèque Moment." "desc": "Obtient un objet de date à l'aide de la bibliothèque Moment."
},
"$clone": {
"args": "valeur",
"desc": "Cloner un objet en toute sécurité."
} }
} }

View File

@ -122,7 +122,6 @@
"selectionToSubflow": "選択部分をサブフロー化", "selectionToSubflow": "選択部分をサブフロー化",
"flows": "フロー", "flows": "フロー",
"add": "フローを新規追加", "add": "フローを新規追加",
"rename": "フロー名を変更",
"delete": "フローを削除", "delete": "フローを削除",
"keyboardShortcuts": "ショートカットキーの説明", "keyboardShortcuts": "ショートカットキーの説明",
"login": "ログイン", "login": "ログイン",
@ -130,6 +129,11 @@
"editPalette": "パレットの管理", "editPalette": "パレットの管理",
"other": "その他", "other": "その他",
"showTips": "ヒントを表示", "showTips": "ヒントを表示",
"showNodeHelp": "ノードのヘルプを表示",
"enableSelectedNodes": "選択したノードを有効化",
"disableSelectedNodes": "選択したノードを無効化",
"showSelectedNodeLabels": "選択したノードのラベル表示",
"hideSelectedNodeLabels": "選択したノードのラベル非表示",
"showWelcomeTours": "新バージョンのガイドツアーを表示", "showWelcomeTours": "新バージョンのガイドツアーを表示",
"help": "Node-REDウェブサイト", "help": "Node-REDウェブサイト",
"projects": "プロジェクト", "projects": "プロジェクト",
@ -511,7 +515,7 @@
"selectAllConnected": "接続されたノードを選択", "selectAllConnected": "接続されたノードを選択",
"addRemoveNode": "ノードの選択、選択解除", "addRemoveNode": "ノードの選択、選択解除",
"editSelected": "選択したノードを編集", "editSelected": "選択したノードを編集",
"deleteSelected": "選択したノードや接続を削除", "deleteSelected": "選択部分を削除",
"deleteReconnect": "削除と再接続", "deleteReconnect": "削除と再接続",
"importNode": "フローの読み込み", "importNode": "フローの読み込み",
"exportNode": "フローの書き出し", "exportNode": "フローの書き出し",
@ -1201,23 +1205,11 @@
"diagnostics": { "diagnostics": {
"title": "システム情報" "title": "システム情報"
}, },
"languages": {
"de": "ドイツ語",
"en-US": "英語",
"fr": "フランス語",
"ja": "日本語",
"ko": "韓国語",
"pt-BR": "ポルトガル語",
"ru": "ロシア語",
"zh-CN": "中国語(簡体)",
"zh-TW": "中国語(繁体)"
},
"validator": { "validator": {
"errors": { "errors": {
"invalid-json": "JSONデータが不正: __error__", "invalid-json": "JSONデータが不正: __error__",
"invalid-json-prop": "__prop__: JSONデータが不正: __error__", "invalid-expr": "不正なJSONata式: __error__",
"invalid-prop": "プロパティ式が不正", "invalid-prop": "プロパティ式が不正",
"invalid-prop-prop": "__prop__: プロパティ式が不正",
"invalid-num": "数値が不正", "invalid-num": "数値が不正",
"invalid-num-prop": "__prop__: 数値が不正", "invalid-num-prop": "__prop__: 数値が不正",
"invalid-regexp": "入力パターンが不正", "invalid-regexp": "入力パターンが不正",
@ -1229,6 +1221,7 @@
} }
}, },
"contextMenu": { "contextMenu": {
"showActionList": "動作一覧を表示",
"insert": "挿入", "insert": "挿入",
"node": "ノード", "node": "ノード",
"junction": "分岐点", "junction": "分岐点",

View File

@ -79,7 +79,6 @@
"selectionToSubflow": "서브 플로우 선택", "selectionToSubflow": "서브 플로우 선택",
"flows": "플로우", "flows": "플로우",
"add": "추가", "add": "추가",
"rename": "이름변경",
"delete": "삭제", "delete": "삭제",
"keyboardShortcuts": "단축키", "keyboardShortcuts": "단축키",
"login": "로그인", "login": "로그인",

View File

@ -109,7 +109,6 @@
"selectionToSubflow": "Seleção para subfluxo", "selectionToSubflow": "Seleção para subfluxo",
"flows": "Fluxos", "flows": "Fluxos",
"add": "Adicionar", "add": "Adicionar",
"rename": "Renomear",
"delete": "Apagar", "delete": "Apagar",
"keyboardShortcuts": "Atalhos do teclado", "keyboardShortcuts": "Atalhos do teclado",
"login": "Ingressar", "login": "Ingressar",
@ -1173,22 +1172,10 @@
"diagnostics": { "diagnostics": {
"title": "informações do Sistema" "title": "informações do Sistema"
}, },
"languages": {
"de": "Alemão",
"en-US": "Inglês",
"ja": "Japonês",
"ko": "Coreano",
"pt-BR": "Português(Brasil)",
"ru": "Russo",
"zh-CN": "Chinês(Simplificado)",
"zh-TW": "Chinês(Tradicional)"
},
"validator": { "validator": {
"errors": { "errors": {
"invalid-json": "Dados JSON inválidos: __error__", "invalid-json": "Dados JSON inválidos: __error__",
"invalid-json-prop": "__prop__: dados JSON inválidos: __error__",
"invalid-prop": "Expressão de propriedade inválida", "invalid-prop": "Expressão de propriedade inválida",
"invalid-prop-prop": "__prop__: expressão de propriedade inválida",
"invalid-num": "Número inválido", "invalid-num": "Número inválido",
"invalid-num-prop": "__prop__: número inválido", "invalid-num-prop": "__prop__: número inválido",
"invalid-regexp": "Padrão de entrada inválido", "invalid-regexp": "Padrão de entrada inválido",

View File

@ -95,7 +95,6 @@
"selectionToSubflow": "Выделение в подпоток", "selectionToSubflow": "Выделение в подпоток",
"flows": "Потоки", "flows": "Потоки",
"add": "Добавить", "add": "Добавить",
"rename": "Переименовать",
"delete": "Удалить", "delete": "Удалить",
"keyboardShortcuts": "Сочетания клавиш", "keyboardShortcuts": "Сочетания клавиш",
"login": "Войти", "login": "Войти",
@ -1129,16 +1128,5 @@
"appearance": "Внешний вид", "appearance": "Внешний вид",
"preview": "Предпросмотр редактора", "preview": "Предпросмотр редактора",
"defaultValue": "Значение по умолчанию" "defaultValue": "Значение по умолчанию"
},
"languages" : {
"de": "Немецкий",
"en-US": "Английский",
"fr": "Французский",
"ja": "Японский",
"ko": "Корейский",
"pt-BR":"португальский",
"ru": "Русский",
"zh-CN": "Китайский (упрощенный)",
"zh-TW": "Китайский (традиционный)"
} }
} }

View File

@ -23,7 +23,11 @@
"position": "位置", "position": "位置",
"enable": "启用", "enable": "启用",
"disable": "禁用", "disable": "禁用",
"upload": "上传" "upload": "上传",
"lock": "锁定",
"unlock": "解锁",
"locked": "锁定",
"unlocked": "解锁"
}, },
"type": { "type": {
"string": "字符串", "string": "字符串",
@ -68,7 +72,13 @@
"enabled": "有效", "enabled": "有效",
"disabled": "无效", "disabled": "无效",
"info": "详细描述", "info": "详细描述",
"selectNodes": "点击节点来选择" "selectNodes": "点击节点来选择",
"enableFlow": "启用流程",
"disableFlow": "禁用流程",
"lockFlow": "锁定流程",
"unlockFlow": "解除锁定",
"moveToStart": "移动到起始",
"moveToEnd": "移动到末尾"
}, },
"menu": { "menu": {
"label": { "label": {
@ -101,6 +111,7 @@
"displayStatus": "显示节点状态", "displayStatus": "显示节点状态",
"displayConfig": "修改节点配置", "displayConfig": "修改节点配置",
"import": "导入", "import": "导入",
"importExample": "导入示例流程",
"export": "导出", "export": "导出",
"search": "查找流程", "search": "查找流程",
"searchInput": "查找流程", "searchInput": "查找流程",
@ -109,7 +120,6 @@
"selectionToSubflow": "将选择部分更改为子流程", "selectionToSubflow": "将选择部分更改为子流程",
"flows": "流程", "flows": "流程",
"add": "增加", "add": "增加",
"rename": "重命名",
"delete": "删除", "delete": "删除",
"keyboardShortcuts": "键盘快捷方式", "keyboardShortcuts": "键盘快捷方式",
"login": "登录", "login": "登录",
@ -142,7 +152,12 @@
"moveToBack": "置于底层", "moveToBack": "置于底层",
"moveToFront": "置于顶层", "moveToFront": "置于顶层",
"moveBackwards": "向后移动", "moveBackwards": "向后移动",
"moveForwards": "向前移动" "moveForwards": "向前移动",
"showNodeHelp":"显示节点帮助",
"enableSelectedNodes":"启用当前选中节点",
"disableSelectedNodes":"禁用当前选中节点",
"showSelectedNodeLabels":"显示选中的节点标签",
"hideSelectedNodeLabels":"隐藏选中的节点标签"
} }
}, },
"actions": { "actions": {
@ -403,6 +418,7 @@
}, },
"errors": { "errors": {
"noNodesSelected": "<strong>无法创建子流程</strong>: 未选择节点", "noNodesSelected": "<strong>无法创建子流程</strong>: 未选择节点",
"acrossMultipleGroups": "无法跨多个组创建子流",
"multipleInputsToSelection": "<strong>无法创建子流程</strong>: 多个输入到了选择" "multipleInputsToSelection": "<strong>无法创建子流程</strong>: 多个输入到了选择"
} }
}, },
@ -491,12 +507,14 @@
"unassigned": "未分配", "unassigned": "未分配",
"global": "全局", "global": "全局",
"workspace": "工作区", "workspace": "工作区",
"editor": "编辑对话框",
"selectAll": "选择所有节点", "selectAll": "选择所有节点",
"selectNone": "取消所有选择", "selectNone": "取消所有选择",
"selectAllConnected": "选择所有连接的节点", "selectAllConnected": "选择所有连接的节点",
"addRemoveNode": "从选择中添加/删除节点", "addRemoveNode": "从选择中添加/删除节点",
"editSelected": "编辑选定节点", "editSelected": "编辑选定节点",
"deleteSelected": "删除选定节点或链接", "deleteSelected": "删除选定节点或链接",
"deleteReconnect": "删除并重新连接",
"importNode": "导入节点", "importNode": "导入节点",
"exportNode": "导出节点", "exportNode": "导出节点",
"nudgeNode": "移动所选节点(1px)", "nudgeNode": "移动所选节点(1px)",
@ -571,6 +589,7 @@
"editor": { "editor": {
"title": "面板管理", "title": "面板管理",
"palette": "控制板", "palette": "控制板",
"allCatalogs": "所有目录",
"times": { "times": {
"seconds": "秒前", "seconds": "秒前",
"minutes": "分前", "minutes": "分前",
@ -610,6 +629,7 @@
"tab-nodes": "节点", "tab-nodes": "节点",
"tab-install": "安装", "tab-install": "安装",
"sort": "排序:", "sort": "排序:",
"sortRelevance": "关联",
"sortAZ": "a-z顺序", "sortAZ": "a-z顺序",
"sortRecent": "日期顺序", "sortRecent": "日期顺序",
"more": "增加 __count__ 个", "more": "增加 __count__ 个",
@ -683,7 +703,11 @@
"empty": "空的", "empty": "空的",
"globalConfig": "全局配置节点", "globalConfig": "全局配置节点",
"triggerAction": "触发动作", "triggerAction": "触发动作",
"find": "在工作区中查找" "find": "在工作区中查找",
"copyItemUrl": "复制地址",
"copyURL2Clipboard": "复制地址到剪贴板",
"showFlow": "显示流程",
"hideFlow": "隐藏流程"
}, },
"help": { "help": {
"name": "帮助", "name": "帮助",
@ -984,7 +1008,10 @@
"quote": "引用", "quote": "引用",
"link": "链接", "link": "链接",
"horizontal-rule": "水平线", "horizontal-rule": "水平线",
"toggle-preview": "切换预览" "toggle-preview": "切换预览",
"mermaid": {
"summary": "美人鱼图"
}
}, },
"bufferEditor": { "bufferEditor": {
"title": "Buffer 编辑器", "title": "Buffer 编辑器",
@ -1147,17 +1174,6 @@
"create": "创建分支", "create": "创建分支",
"current": "当前的" "current": "当前的"
}, },
"languages": {
"de": "德语",
"en-US": "英文",
"fr": "法语",
"ja": "日语",
"ko": "韩文",
"pt-BR":"葡萄牙语",
"ru":"俄語",
"zh-CN": "简体中文",
"zh-TW": "繁体中文"
},
"create-default-file-set": { "create-default-file-set": {
"no-active": "没有活动项目就无法创建默认文件集", "no-active": "没有活动项目就无法创建默认文件集",
"no-empty": "无法在非空项目上创建默认文件集", "no-empty": "无法在非空项目上创建默认文件集",
@ -1187,21 +1203,11 @@
"diagnostics": { "diagnostics": {
"title": "系统信息" "title": "系统信息"
}, },
"languages": {
"de": "德语-Deutsch",
"en-US": "英文-English",
"ja": "日语-日本",
"ko": "韩文-한국인",
"ru": "俄语-Русский",
"zh-CN": "简体中文",
"zh-TW": "繁體中文"
},
"validator": { "validator": {
"errors": { "errors": {
"invalid-json": "无效的 JSON 数据: __error__", "invalid-json": "无效的 JSON 数据: __error__",
"invalid-json-prop": "__prop__: 无效的 JSON 数据: __error__", "invalid-expr": "无效的 JSONata 表达式: __error__",
"invalid-prop": "无效的属性表达式", "invalid-prop": "无效的属性表达式",
"invalid-prop-prop": "__prop__: 无效的属性表达式",
"invalid-num": "无效的数字", "invalid-num": "无效的数字",
"invalid-num-prop": "__prop__: 无效的数字", "invalid-num-prop": "__prop__: 无效的数字",
"invalid-regexp": "输入格式无效", "invalid-regexp": "输入格式无效",
@ -1213,9 +1219,15 @@
} }
}, },
"contextMenu": { "contextMenu": {
"showActionList":"显示动作列表",
"insert": "插入", "insert": "插入",
"node": "节点", "node": "节点",
"junction": "连接点", "junction": "连接点",
"linkNodes": "链接节点" "linkNodes": "链接节点"
},
"env-var": {
"environment": "环境配置",
"header": "全局环境变量",
"revert": "重置"
} }
} }

View File

@ -270,5 +270,9 @@
"$moment": { "$moment": {
"args": "[str]", "args": "[str]",
"desc": "使用Moment库获取日期对象。" "desc": "使用Moment库获取日期对象。"
},
"$clone": {
"args": "value",
"desc": "安全克隆对象."
} }
} }

View File

@ -23,7 +23,11 @@
"position": "位置", "position": "位置",
"enable": "啟用", "enable": "啟用",
"disable": "禁用", "disable": "禁用",
"upload": "上傳" "upload": "上傳",
"lock": "鎖定",
"unlock": "解鎖",
"locked": "鎖定",
"unlocked": "解鎖"
}, },
"type": { "type": {
"string": "字符串", "string": "字符串",
@ -38,11 +42,14 @@
} }
}, },
"event": { "event": {
"loadPlugins": "加載插件",
"loadPalette": "加載控制板", "loadPalette": "加載控制板",
"loadNodeCatalogs": "加載節點目錄", "loadNodeCatalogs": "加載節點目錄",
"loadNodes": "加載 __count__ 個節點", "loadNodes": "加載 __count__ 個節點",
"loadFlows": "加載流程", "loadFlows": "加載流程",
"importFlows": "往工作區中加載流程" "importFlows": "往工作區中加載流程",
"importError": "<p>加載流程錯誤</p><p>__message__</p>",
"loadingProject": "加載項目"
}, },
"workspace": { "workspace": {
"defaultName": "流程__number__", "defaultName": "流程__number__",
@ -51,18 +58,35 @@
"delete": "確定想要刪除 '__label__'?", "delete": "確定想要刪除 '__label__'?",
"dropFlowHere": "把流程放到這裡", "dropFlowHere": "把流程放到這裡",
"addFlow": "新增流程", "addFlow": "新增流程",
"listFlows": "流程列表", "addFlowToRight": "在右側新增流程",
"hideFlow": "隱藏流程",
"hideOtherFlows": "隱藏其它流程",
"showAllFlows": "顯示所有流程",
"hideAllFlows": "隱藏所有流程",
"hiddenFlows": "列出 __count__ 個隱藏流程",
"hiddenFlows_plural": "列出 __count__ 個隱藏流程",
"showLastHiddenFlow": "顯示最後一個隱藏流程",
" ": "流程列表",
"listSubflows": "列出子流程",
"status": "狀態", "status": "狀態",
"enabled": "有效", "enabled": "有效",
"disabled": "無效", "disabled": "無效",
"info": "詳細描述", "info": "詳細描述",
"selectNodes": "點擊節點用於選擇" "selectNodes": "點擊節點用於選擇",
"enableFlow": "啟用流程",
"disableFlow": "禁用流程",
"lockFlow": "鎖定流程",
"unlockFlow": "解除鎖定",
"moveToStart": "移動到起始",
"moveToEnd": "移動到末尾"
}, },
"menu": { "menu": {
"label": { "label": {
"view": { "view": {
"view": "顯示", "view": "顯示",
"grid": "格線", "grid": "格線",
"storeZoom": "加載時還原縮放尺寸",
"storePosition": "加載時還原滾動位置",
"showGrid": "顯示格線", "showGrid": "顯示格線",
"snapGrid": "對齊格線", "snapGrid": "對齊格線",
"gridSize": "格線尺寸", "gridSize": "格線尺寸",
@ -80,12 +104,14 @@
"palette": { "palette": {
"show": "顯示控制板" "show": "顯示控制板"
}, },
"edit": "編輯",
"settings": "設置", "settings": "設置",
"userSettings": "使用者設置", "userSettings": "使用者設置",
"nodes": "節點", "nodes": "節點",
"displayStatus": "顯示節點狀態", "displayStatus": "顯示節點狀態",
"displayConfig": "修改節點配置", "displayConfig": "修改節點配置",
"import": "匯入", "import": "匯入",
"importExample": "導入示例流程",
"export": "匯出", "export": "匯出",
"search": "搜尋流程", "search": "搜尋流程",
"searchInput": "搜尋流程", "searchInput": "搜尋流程",
@ -94,7 +120,6 @@
"selectionToSubflow": "將選擇部分更改為子流程", "selectionToSubflow": "將選擇部分更改為子流程",
"flows": "流程", "flows": "流程",
"add": "增加", "add": "增加",
"rename": "重新命名",
"delete": "刪除", "delete": "刪除",
"keyboardShortcuts": "鍵盤快速鍵", "keyboardShortcuts": "鍵盤快速鍵",
"login": "登入", "login": "登入",
@ -102,24 +127,48 @@
"editPalette": "節點管理", "editPalette": "節點管理",
"other": "其他", "other": "其他",
"showTips": "顯示小提示", "showTips": "顯示小提示",
"help": "Node-RED website", "showWelcomeTours": "顯示新版本向導",
"help": "Node-RED 文檔主頁",
"projects": "專案", "projects": "專案",
"projects-new": "新專案", "projects-new": "新專案",
"projects-open": "開啟專案", "projects-open": "開啟專案",
"projects-settings": "專案設定", "projects-settings": "專案設定",
"showNodeLabelDefault": "顯示新添加節點的標籤", "showNodeLabelDefault": "顯示新添加節點的標籤",
"codeEditor": "代碼編輯器",
"groups": "組", "groups": "組",
"groupSelection": "選擇組", "groupSelection": "選擇組",
"ungroupSelection": "取消選擇組", "ungroupSelection": "取消選擇組",
"groupMergeSelection": "合并選擇", "groupMergeSelection": "合并選擇",
"groupRemoveSelection": "從組中移除" "groupRemoveSelection": "從組中移除",
"arrange": "布局",
"alignLeft": "左對齊",
"alignCenter": "居中對齊",
"alignRight": "右對齊",
"alignTop": "頂部對齊",
"alignMiddle": "垂直居中對齊",
"alignBottom": "底部對齊",
"distributeHorizontally": "横向分布",
"distributeVertically": "垂直分布",
"moveToBack": "置於底層",
"moveToFront": "置於頂層",
"moveBackwards": "向後移動",
"moveForwards": "向前移動",
"showNodeHelp":"顯示節點幫助",
"enableSelectedNodes":"啟用當前選中節點",
"disableSelectedNodes":"禁用當前選中節點",
"showSelectedNodeLabels":"顯示選中的節點標簽",
"hideSelectedNodeLabels":"隱藏選中的節點標簽"
} }
}, },
"actions": { "actions": {
"toggle-navigator": "切換導航器", "toggle-navigator": "切換導航器",
"zoom-out": "縮小", "zoom-out": "縮小",
"zoom-reset": "重置縮放", "zoom-reset": "重置縮放",
"zoom-in": "放大" "zoom-in": "放大",
"search-flows": "搜索流程",
"search-prev": "上一個",
"search-next": "下一個",
"search-counter": "\"__term__\" __result__ of __count__"
}, },
"user": { "user": {
"loggedInAs": "作為 __name__ 登入", "loggedInAs": "作為 __name__ 登入",
@ -135,12 +184,17 @@
} }
}, },
"notification": { "notification": {
"state": {
"flowsStopped": "流程已停止",
"flowsStarted": "流程已啟動"
},
"warning": "<strong>警告</strong>: __message__", "warning": "<strong>警告</strong>: __message__",
"warnings": { "warnings": {
"undeployedChanges": "節點中存在未部署的更改", "undeployedChanges": "節點中存在未部署的更改",
"nodeActionDisabled": "節點動作在子流程中被禁用", "nodeActionDisabled": "節點動作在子流程中被禁用",
"nodeActionDisabledSubflow": "子流程中禁用了節點操作", "nodeActionDisabledSubflow": "子流程中禁用了節點操作",
"missing-types": "流程由於缺少節點類型而停止。請檢查日誌的詳細資訊", "missing-types": "流程由於缺少節點類型而停止。請檢查日誌的詳細資訊",
"missing-modules": "<p>流程因缺少模塊而停止。</p>",
"safe-mode": "<p>流程在安全模式下停止。</p><p>您可以修改流程並部署更改以重新啟動。</p>", "safe-mode": "<p>流程在安全模式下停止。</p><p>您可以修改流程並部署更改以重新啟動。</p>",
"restartRequired": "Node-RED必須重新啟動以啟用升級的模組", "restartRequired": "Node-RED必須重新啟動以啟用升級的模組",
"credentials_load_failed": "<p>流程由於無法解密證書而停止。</p> <p>流程證書文件已加密,但是項目的加密密鑰丟失或無效。</p>", "credentials_load_failed": "<p>流程由於無法解密證書而停止。</p> <p>流程證書文件已加密,但是項目的加密密鑰丟失或無效。</p>",
@ -151,7 +205,7 @@
"project_not_found": "<p>找不到項目的'__project__'</p>", "project_not_found": "<p>找不到項目的'__project__'</p>",
"git_merge_conflict": "<p>自動合併更改失敗。</p><p>修復未合併的衝突,然後提交結果。</p>" "git_merge_conflict": "<p>自動合併更改失敗。</p><p>修復未合併的衝突,然後提交結果。</p>"
}, },
"error": "<strong>Error</strong>: __message__", "error": "<strong>錯誤</strong>: __message__",
"errors": { "errors": {
"lostConnection": "丟失與伺服器的連接,重新連接...", "lostConnection": "丟失與伺服器的連接,重新連接...",
"lostConnectionReconnect": "丟失與伺服器的連接__time__ 秒後重新連接", "lostConnectionReconnect": "丟失與伺服器的連接__time__ 秒後重新連接",
@ -208,6 +262,8 @@
"download": "下載", "download": "下載",
"importUnrecognised": "匯入了無法識別的類型:", "importUnrecognised": "匯入了無法識別的類型:",
"importUnrecognised_plural": "匯入了無法識別的類型:", "importUnrecognised_plural": "匯入了無法識別的類型:",
"importDuplicate": "導入了重復節點:",
"importDuplicate_plural": "導入了重復節點:",
"nodesExported": "節點匯出到了剪貼簿", "nodesExported": "節點匯出到了剪貼簿",
"nodesImported": "已匯入:", "nodesImported": "已匯入:",
"nodeCopied": "已複製 __count__ 個節點", "nodeCopied": "已複製 __count__ 個節點",
@ -259,6 +315,10 @@
"modifiedFlowsDesc": "只部署包含已更改節點的流程", "modifiedFlowsDesc": "只部署包含已更改節點的流程",
"modifiedNodes": "已更改的節點", "modifiedNodes": "已更改的節點",
"modifiedNodesDesc": "只部署已經更改的節點", "modifiedNodesDesc": "只部署已經更改的節點",
"startFlows": "啟動",
"startFlowsDesc": "啟動流程",
"stopFlows": "停止",
"stopFlowsDesc": "停止流程",
"restartFlows": "重新啟動流程", "restartFlows": "重新啟動流程",
"restartFlowsDesc": "重新啟動當前部署的流程", "restartFlowsDesc": "重新啟動當前部署的流程",
"successfulDeploy": "部署成功", "successfulDeploy": "部署成功",
@ -337,14 +397,28 @@
"output": "輸出:", "output": "輸出:",
"status": "狀態節點", "status": "狀態節點",
"deleteSubflow": "刪除子流程", "deleteSubflow": "刪除子流程",
"confirmDelete": "您確定要刪除此子流程?",
"info": "詳細描述", "info": "詳細描述",
"category": "類別", "category": "類別",
"module": "模塊",
"license": "許可",
"licenseNone": "無",
"licenseOther": "其它",
"type": "節點類型",
"version": "版本",
"versionPlaceholder": "x.y.z",
"keys": "關鍵字",
"keysPlaceholder": "使用英文逗號分隔關鍵字",
"author": "作者",
"authorPlaceholder": "名字 <email@example.com>",
"desc": "描述",
"env": { "env": {
"restore": "恢復為默認子流程", "restore": "恢復為默認子流程",
"remove": "類別刪除環境變量" "remove": "類別刪除環境變量"
}, },
"errors": { "errors": {
"noNodesSelected": "<strong>無法創建子流程</strong>: 未選擇節點", "noNodesSelected": "<strong>無法創建子流程</strong>: 未選擇節點",
"acrossMultipleGroups": "無法跨多個組創建子流",
"multipleInputsToSelection": "<strong>無法創建子流程</strong>: 多個輸入到了選擇" "multipleInputsToSelection": "<strong>無法創建子流程</strong>: 多個輸入到了選擇"
} }
}, },
@ -367,12 +441,12 @@
"editConfig": "編輯 __type__ 配置", "editConfig": "編輯 __type__ 配置",
"addNewType": "添加新的 __type__ 節點", "addNewType": "添加新的 __type__ 節點",
"nodeProperties": "節點屬性", "nodeProperties": "節點屬性",
"label": "Label", "label": "標簽",
"color": "顏色", "color": "顏色",
"portLabels": "埠標籤", "portLabels": "埠標籤",
"labelInputs": "輸入", "labelInputs": "輸入",
"labelOutputs": "輸出", "labelOutputs": "輸出",
"settingIcon": "Icon", "settingIcon": "圖標",
"default": "默認", "default": "默認",
"noDefaultLabel": "無", "noDefaultLabel": "無",
"defaultLabel": "使用默認標籤", "defaultLabel": "使用默認標籤",
@ -385,6 +459,7 @@
"icon": "圖標", "icon": "圖標",
"inputType": "輸入類型", "inputType": "輸入類型",
"selectType": "選擇類型...", "selectType": "選擇類型...",
"loadCredentials": "加載節點憑證",
"inputs": { "inputs": {
"input": "輸入", "input": "輸入",
"select": "選擇", "select": "選擇",
@ -419,7 +494,8 @@
}, },
"errors": { "errors": {
"scopeChange": "更改範圍將使其他流程中的節點無法使用", "scopeChange": "更改範圍將使其他流程中的節點無法使用",
"invalidProperties": "無效的屬性:" "invalidProperties": "無效的屬性:",
"credentialLoadFailed": "無法加載節點憑據"
} }
}, },
"keyboard": { "keyboard": {
@ -431,11 +507,14 @@
"unassigned": "未分配", "unassigned": "未分配",
"global": "全局", "global": "全局",
"workspace": "工作區", "workspace": "工作區",
"editor": "編輯對話框",
"selectAll": "選擇所有節點", "selectAll": "選擇所有節點",
"selectNone": "取消所有選擇",
"selectAllConnected": "選擇所有連接的節點", "selectAllConnected": "選擇所有連接的節點",
"addRemoveNode": "從選擇中添加/刪除節點", "addRemoveNode": "從選擇中添加/刪除節點",
"editSelected": "編輯選定節點", "editSelected": "編輯選定節點",
"deleteSelected": "刪除選定節點或連結", "deleteSelected": "刪除選定節點或連結",
"deleteReconnect": "刪除並重新連接",
"importNode": "匯入節點", "importNode": "匯入節點",
"exportNode": "匯出節點", "exportNode": "匯出節點",
"nudgeNode": "移動所選節點(1px)", "nudgeNode": "移動所選節點(1px)",
@ -445,10 +524,14 @@
"copyNode": "複製所選節點", "copyNode": "複製所選節點",
"cutNode": "剪切所選節點", "cutNode": "剪切所選節點",
"pasteNode": "粘貼節點", "pasteNode": "粘貼節點",
"copyGroupStyle": "復製組樣式",
"pasteGroupStyle": "粘貼組樣式",
"undoChange": "撤銷上次執行的更改", "undoChange": "撤銷上次執行的更改",
"redoChange": "重做",
"searchBox": "打開搜尋框", "searchBox": "打開搜尋框",
"managePalette": "管理面板", "managePalette": "管理面板",
"actionList": "動作列表" "actionList": "動作列表",
"splitWireWithLinks": "使用Link節點拆分已選項"
}, },
"library": { "library": {
"library": "庫", "library": "庫",
@ -466,12 +549,11 @@
"types": { "types": {
"local": "本地", "local": "本地",
"examples": "例子" "examples": "例子"
}, }
"exportToLibrary": "將節點匯出到庫"
}, },
"palette": { "palette": {
"noInfo": "無可用資訊", "noInfo": "無可用資訊",
"filter": "過濾節點", "filter": "過濾已安裝模組",
"search": "搜尋模組", "search": "搜尋模組",
"addCategory": "添加新的...", "addCategory": "添加新的...",
"label": { "label": {
@ -501,11 +583,13 @@
"nodeEnabled_plural": "啟用多個節點:", "nodeEnabled_plural": "啟用多個節點:",
"nodeDisabled": "禁用節點:", "nodeDisabled": "禁用節點:",
"nodeDisabled_plural": "禁用多個節點:", "nodeDisabled_plural": "禁用多個節點:",
"nodeUpgraded": "節點模組__module__升級到__version__版本" "nodeUpgraded": "節點模組__module__升級到__version__版本",
"unknownNodeRegistered": "加載節點錯誤: <ul><li>__type__<br>__error__</li></ul>"
}, },
"editor": { "editor": {
"title": "面板管理", "title": "面板管理",
"palette": "Palette", "palette": "控製板",
"allCatalogs": "所有目錄",
"times": { "times": {
"seconds": "秒前", "seconds": "秒前",
"minutes": "分前", "minutes": "分前",
@ -545,10 +629,12 @@
"tab-nodes": "節點", "tab-nodes": "節點",
"tab-install": "安裝", "tab-install": "安裝",
"sort": "排序:", "sort": "排序:",
"sortRelevance": "關聯",
"sortAZ": "a-z順序", "sortAZ": "a-z順序",
"sortRecent": "日期順序", "sortRecent": "日期順序",
"more": "增加 __count__ 個", "more": "增加 __count__ 個",
"upload": "上傳模塊tgz文件", "upload": "上傳模塊tgz文件",
"refresh": "更新模塊列表",
"errors": { "errors": {
"catalogLoadFailed": "無法載入節點目錄。<br>查看瀏覽器控制臺瞭解更多資訊", "catalogLoadFailed": "無法載入節點目錄。<br>查看瀏覽器控制臺瞭解更多資訊",
"installFailed": "無法安裝: __module__<br>__message__<br>查看日誌瞭解更多資訊", "installFailed": "無法安裝: __module__<br>__message__<br>查看日誌瞭解更多資訊",
@ -617,7 +703,11 @@
"empty": "空的", "empty": "空的",
"globalConfig": "全局配置節點", "globalConfig": "全局配置節點",
"triggerAction": "觸發動作", "triggerAction": "觸發動作",
"find": "在工作區中查找" "find": "在工作區中查找",
"copyItemUrl": "復製地址",
"copyURL2Clipboard": "復製地址到剪貼板",
"showFlow": "顯示流程",
"hideFlow": "隱藏流程"
}, },
"help": { "help": {
"name": "幫助", "name": "幫助",
@ -627,7 +717,8 @@
"showHelp": "顯示幫助", "showHelp": "顯示幫助",
"showInOutline": "在大綱中顯示", "showInOutline": "在大綱中顯示",
"showTopics": "顯示主題", "showTopics": "顯示主題",
"noHelp": "未選擇幫助主題" "noHelp": "未選擇幫助主題",
"changeLog": "更新日誌"
}, },
"config": { "config": {
"name": "配置節點", "name": "配置節點",
@ -828,31 +919,37 @@
"json": "JSON", "json": "JSON",
"bin": "二進位流", "bin": "二進位流",
"date": "時間戳記", "date": "時間戳記",
"jsonata": "expression", "jsonata": "表達式",
"env": "env variable", "env": "環境變量",
"cred": "證書" "cred": "證書"
} }
}, },
"editableList": { "editableList": {
"add": "添加" "add": "添加",
"addTitle": "添加項"
}, },
"search": { "search": {
"empty": "找不到匹配", "history": "搜索歷史",
"clear": "清除所有",
"empty": "找不到匹配項",
"addNode": "添加一個節點...", "addNode": "添加一個節點...",
"options": { "options": {
"configNodes": "配置節點", "configNodes": "配置節點",
"unusedConfigNodes": "未使用的配置節點", "unusedConfigNodes": "未使用的配置節點",
"invalidNodes": "無效的節點", "invalidNodes": "無效的節點",
"uknownNodes": "未知的節點", "uknownNodes": "未知的節點",
"unusedSubflows": "未使用的子流程" "unusedSubflows": "未使用的子流程",
"hiddenFlows": "隱藏的流程",
"modifiedNodes": "已修改的節點或流程",
"thisFlow": "當前流程"
} }
}, },
"expressionEditor": { "expressionEditor": {
"functions": "功能", "functions": "功能",
"functionReference": "Function reference", "functionReference": "功能參考",
"insert": "插入", "insert": "插入",
"title": "JSONata運算式編輯器", "title": "JSONata運算式編輯器",
"test": "Test", "test": "測試",
"data": "示例消息", "data": "示例消息",
"result": "結果", "result": "結果",
"format": "格式表達方法", "format": "格式表達方法",
@ -863,20 +960,28 @@
"invalid-expr": "無效的JSONata運算式:\n __message__", "invalid-expr": "無效的JSONata運算式:\n __message__",
"invalid-msg": "無效的示例JSON消息:\n __message__", "invalid-msg": "無效的示例JSON消息:\n __message__",
"context-unsupported": "無法測試上下文函數\n $flowContext 或 $globalContext", "context-unsupported": "無法測試上下文函數\n $flowContext 或 $globalContext",
"env-unsupported": "無法測試 $env 函數",
"moment-unsupported": "無法測試 $moment 函數",
"clone-unsupported": "無法測試 $clone 函數",
"eval": "評估運算式錯誤:\n __message__" "eval": "評估運算式錯誤:\n __message__"
} }
}, },
"monaco": {
"setTheme": "設置主題"
},
"jsEditor": { "jsEditor": {
"title": "JavaScript 編輯器" "title": "JavaScript 編輯器"
}, },
"textEditor": { "textEditor": {
"title": "Text 編輯器" "title": "文本編輯器"
}, },
"jsonEditor": { "jsonEditor": {
"title": "JSON編輯器", "title": "JSON編輯器",
"format": "格式化JSON", "format": "格式化JSON",
"rawMode": "編輯 JSON", "rawMode": "編輯 JSON",
"uiMode": "Visual編輯器", "uiMode": "可視化編輯器",
"rawMode-readonly": "原始JSON",
"uiMode-readonly": "可視化",
"insertAbove": "在上方插入", "insertAbove": "在上方插入",
"insertBelow": "在下方插入", "insertBelow": "在下方插入",
"addItem": "添加項目", "addItem": "添加項目",
@ -892,9 +997,9 @@
"title": "Markdown 編輯器", "title": "Markdown 編輯器",
"expand": "展開", "expand": "展開",
"format": "F使用markdown格式化", "format": "F使用markdown格式化",
"heading1": "Heading 1", "heading1": "標題 1",
"heading2": "Heading 2", "heading2": "標題 2",
"heading3": "Heading 3", "heading3": "標題 3",
"bold": "粗體", "bold": "粗體",
"italic": "斜體", "italic": "斜體",
"code": "程式碼", "code": "程式碼",
@ -903,7 +1008,10 @@
"quote": "引用", "quote": "引用",
"link": "連結", "link": "連結",
"horizontal-rule": "分隔線", "horizontal-rule": "分隔線",
"toggle-preview": "預覽" "toggle-preview": "切換預覽",
"mermaid": {
"summary": "美人魚圖"
}
}, },
"bufferEditor": { "bufferEditor": {
"title": "緩衝區編輯器", "title": "緩衝區編輯器",
@ -1038,7 +1146,8 @@
"not-git": "不是git倉庫", "not-git": "不是git倉庫",
"no-resource": "找不到存儲庫", "no-resource": "找不到存儲庫",
"cant-get-ssh-key-path": "錯誤! 無法獲取所選的SSH密鑰路徑。", "cant-get-ssh-key-path": "錯誤! 無法獲取所選的SSH密鑰路徑。",
"unexpected_error": "意外的錯誤" "unexpected_error": "意外的錯誤",
"clearContext": "更改項目時清除上下文"
}, },
"delete": { "delete": {
"confirm": "您確定要刪除此項目嗎?" "confirm": "您確定要刪除此項目嗎?"
@ -1068,7 +1177,7 @@
"create-default-file-set": { "create-default-file-set": {
"no-active": "沒有活動項目就無法創建默認文件集", "no-active": "沒有活動項目就無法創建默認文件集",
"no-empty": "無法在非空項目上創建默認文件集", "no-empty": "無法在非空項目上創建默認文件集",
"git-error": "git error" "git-error": "git錯誤"
}, },
"errors": { "errors": {
"no-username-email": "您的Git客戶端未配置用戶名/電子郵件。", "no-username-email": "您的Git客戶端未配置用戶名/電子郵件。",
@ -1079,21 +1188,45 @@
"editor-tab": { "editor-tab": {
"properties": "屬性", "properties": "屬性",
"envProperties": "環境變量", "envProperties": "環境變量",
"module": "模塊屬性",
"description": "描述", "description": "描述",
"appearance": "外觀", "appearance": "外觀",
"preview": "UI預覽", "preview": "UI預覽",
"defaultValue": "默認值", "defaultValue": "默認值"
"env": "環境變量"
}, },
"languages": { "tourGuide": {
"de": "德語", "takeATour": "查看更新內容",
"en-US": "英語", "start": "開始",
"fr": "法語", "next": "下一個",
"ja": "日語", "welcomeTours": "歡迎使用 Node-RED"
"ko": "韓語", },
"pt-BR":"葡萄牙语", "diagnostics": {
"ru":"俄語", "title": "系统信息"
"zh-CN": "簡體中文", },
"zh-TW": "繁體中文" "validator": {
"errors": {
"invalid-json": "無效的 JSON 數據: __error__",
"invalid-expr": "無效的 JSONata 表達式: __error__",
"invalid-prop": "無效的屬性表達式",
"invalid-num": "無效的數字",
"invalid-regexp": "輸入格式無效",
"invalid-regex-prop": "__prop__: 輸入格式無效",
"missing-required-prop": "__prop__: 缺少屬性值",
"invalid-config": "__prop__: 無效的配置節點",
"missing-config": "__prop__: 缺少配置節點",
"validation-error": "__prop__: 驗證錯誤: __node__, __id__: __error__"
}
},
"contextMenu": {
"showActionList":"顯示動作列表",
"insert": "插入",
"node": "節點",
"junction": "連接點",
"linkNodes": "鏈接節點"
},
"env-var": {
"environment": "環境配置",
"header": "全局環境變量",
"revert": "重置"
} }
} }

View File

@ -270,5 +270,9 @@
"$moment": { "$moment": {
"args": "[str]", "args": "[str]",
"desc": "使用Moment庫獲取日期對象。" "desc": "使用Moment庫獲取日期對象。"
},
"$clone": {
"args": "value",
"desc": "安全克隆對象."
} }
} }

View File

@ -39,15 +39,16 @@
console.warn(evt,args); console.warn(evt,args);
} }
if (handlers[evt]) { if (handlers[evt]) {
for (var i=0;i<handlers[evt].length;i++) { let cpyHandlers = [...handlers[evt]];
for (var i=0;i<cpyHandlers.length;i++) {
try { try {
handlers[evt][i].apply(null, args); cpyHandlers[i].apply(null, args);
} catch(err) { } catch(err) {
console.warn("RED.events.emit error: ["+evt+"] "+(err.toString())); console.warn("RED.events.emit error: ["+evt+"] "+(err.toString()));
console.warn(err); console.warn(err);
} }
} }
} }
} }
return { return {

View File

@ -797,8 +797,8 @@ RED.nodes = (function() {
if (node && node._def.onremove) { if (node && node._def.onremove) {
// Deprecated: never documented but used by some early nodes // Deprecated: never documented but used by some early nodes
console.log("Deprecated API warning: node type ",node.type," has an onremove function - should be oneditremove - please report"); console.log("Deprecated API warning: node type ",node.type," has an onremove function - should be oneditdelete - please report");
node._def.onremove.call(n); node._def.onremove.call(node);
} }
return {links:removedLinks,nodes:removedNodes}; return {links:removedLinks,nodes:removedNodes};
} }
@ -1228,7 +1228,6 @@ RED.nodes = (function() {
} }
} }
} else if (n.credentials) { } else if (n.credentials) {
node.credentials = {};
// All other nodes have a well-defined list of possible credentials // All other nodes have a well-defined list of possible credentials
for (var cred in n._def.credentials) { for (var cred in n._def.credentials) {
if (n._def.credentials.hasOwnProperty(cred)) { if (n._def.credentials.hasOwnProperty(cred)) {
@ -2198,6 +2197,12 @@ RED.nodes = (function() {
} }
node._config.x = node.x; node._config.x = node.x;
node._config.y = node.y; node._config.y = node.y;
if (n.hasOwnProperty('w')) {
node.w = n.w
}
if (n.hasOwnProperty('h')) {
node.h = n.h
}
} else if (n.type.substring(0,7) === "subflow") { } else if (n.type.substring(0,7) === "subflow") {
var parentId = n.type.split(":")[1]; var parentId = n.type.split(":")[1];
var subflow = subflow_denylist[parentId]||subflow_map[parentId]||getSubflow(parentId); var subflow = subflow_denylist[parentId]||subflow_map[parentId]||getSubflow(parentId);
@ -2211,7 +2216,7 @@ RED.nodes = (function() {
set: registry.getNodeSet("node-red/unknown") set: registry.getNodeSet("node-red/unknown")
} }
} else { } else {
if (createNewIds || options.importMap[n.id] === "copy") { if (subflow_denylist[parentId] || createNewIds || options.importMap[n.id] === "copy") {
parentId = subflow.id; parentId = subflow.id;
node.type = "subflow:"+parentId; node.type = "subflow:"+parentId;
node._def = registry.getNodeType(node.type); node._def = registry.getNodeType(node.type);

View File

@ -498,6 +498,15 @@ var RED = (function() {
] ]
} }
} }
} else if (notificationId === 'restart-required') {
options.buttons = [
{
text: RED._("common.label.close"),
click: function() {
persistentNotifications[notificationId].hideNotification();
}
}
]
} }
if (!persistentNotifications.hasOwnProperty(notificationId)) { if (!persistentNotifications.hasOwnProperty(notificationId)) {
persistentNotifications[notificationId] = RED.notify(text,options); persistentNotifications[notificationId] = RED.notify(text,options);
@ -525,6 +534,10 @@ var RED = (function() {
RED.view.redrawStatus(node); RED.view.redrawStatus(node);
} }
}); });
let pendingNodeRemovedNotifications = []
let pendingNodeRemovedTimeout
RED.comms.subscribe("notification/node/#",function(topic,msg) { RED.comms.subscribe("notification/node/#",function(topic,msg) {
var i,m; var i,m;
var typeList; var typeList;
@ -562,8 +575,15 @@ var RED = (function() {
m = msg[i]; m = msg[i];
info = RED.nodes.removeNodeSet(m.id); info = RED.nodes.removeNodeSet(m.id);
if (info.added) { if (info.added) {
typeList = "<ul><li>"+m.types.map(RED.utils.sanitize).join("</li><li>")+"</li></ul>"; pendingNodeRemovedNotifications = pendingNodeRemovedNotifications.concat(m.types.map(RED.utils.sanitize))
RED.notify(RED._("palette.event.nodeRemoved", {count:m.types.length})+typeList,"success"); if (pendingNodeRemovedTimeout) {
clearTimeout(pendingNodeRemovedTimeout)
}
pendingNodeRemovedTimeout = setTimeout(function () {
typeList = "<ul><li>"+pendingNodeRemovedNotifications.join("</li><li>")+"</li></ul>";
RED.notify(RED._("palette.event.nodeRemoved", {count:pendingNodeRemovedNotifications.length})+typeList,"success");
pendingNodeRemovedNotifications = []
}, 200)
} }
} }
loadIconList(); loadIconList();
@ -702,7 +722,7 @@ var RED = (function() {
menuOptions.push({id:"menu-item-config-nodes",label:RED._("menu.label.displayConfig"),onselect:"core:show-config-tab"}); menuOptions.push({id:"menu-item-config-nodes",label:RED._("menu.label.displayConfig"),onselect:"core:show-config-tab"});
menuOptions.push({id:"menu-item-workspace",label:RED._("menu.label.flows"),options:[ menuOptions.push({id:"menu-item-workspace",label:RED._("menu.label.flows"),options:[
{id:"menu-item-workspace-add",label:RED._("menu.label.add"),onselect:"core:add-flow"}, {id:"menu-item-workspace-add",label:RED._("menu.label.add"),onselect:"core:add-flow"},
{id:"menu-item-workspace-edit",label:RED._("menu.label.rename"),onselect:"core:edit-flow"}, {id:"menu-item-workspace-edit",label:RED._("menu.label.edit"),onselect:"core:edit-flow"},
{id:"menu-item-workspace-delete",label:RED._("menu.label.delete"),onselect:"core:remove-flow"} {id:"menu-item-workspace-delete",label:RED._("menu.label.delete"),onselect:"core:remove-flow"}
]}); ]});
menuOptions.push({id:"menu-item-subflow",label:RED._("menu.label.subflows"), options: [ menuOptions.push({id:"menu-item-subflow",label:RED._("menu.label.subflows"), options: [

View File

@ -819,7 +819,7 @@ RED.clipboard = (function() {
flow.forEach(function(node) { flow.forEach(function(node) {
if (node.type === "tab") { if (node.type === "tab") {
flows[node.id] = { flows[node.id] = {
element: getFlowLabel(node,false), element: getFlowLabel(node),
deferBuild: type !== "flow", deferBuild: type !== "flow",
expanded: type === "flow", expanded: type === "flow",
children: [] children: []
@ -1000,7 +1000,6 @@ RED.clipboard = (function() {
try { try {
RED.view.importNodes(newNodes, importOptions); RED.view.importNodes(newNodes, importOptions);
} catch(error) { } catch(error) {
console.log(error.importConfig)
// Thrown for import_conflict // Thrown for import_conflict
confirmImport(error.importConfig, newNodes, importOptions); confirmImport(error.importConfig, newNodes, importOptions);
} }
@ -1170,9 +1169,9 @@ RED.clipboard = (function() {
function getNodeElement(n, isConflicted, isSelected, parent) { function getNodeElement(n, isConflicted, isSelected, parent) {
var element; var element;
if (n.type === "tab") { if (n.type === "tab") {
element = getFlowLabel(n, isSelected); element = getFlowLabel(n, isConflicted);
} else { } else {
element = getNodeLabel(n, isConflicted, isSelected); element = getNodeLabel(n, isConflicted, isSelected, parent);
} }
var controls = $('<div>',{class:"red-ui-clipboard-dialog-import-conflicts-controls"}).appendTo(element); var controls = $('<div>',{class:"red-ui-clipboard-dialog-import-conflicts-controls"}).appendTo(element);
controls.on("click", function(evt) { evt.stopPropagation(); }); controls.on("click", function(evt) { evt.stopPropagation(); });
@ -1222,14 +1221,14 @@ RED.clipboard = (function() {
} }
} }
function getFlowLabel(n) { function getFlowLabel(n, isConflicted) {
n = JSON.parse(JSON.stringify(n)); n = JSON.parse(JSON.stringify(n));
n._def = RED.nodes.getType(n.type) || {}; n._def = RED.nodes.getType(n.type) || {};
if (n._def) { if (n._def) {
n._ = n._def._; n._ = n._def._;
} }
var div = $('<div>',{class:"red-ui-info-outline-item red-ui-info-outline-item-flow"}); var div = $('<div>',{class:"red-ui-info-outline-item red-ui-info-outline-item-flow red-ui-node-list-item"});
var contentDiv = $('<div>',{class:"red-ui-search-result-description red-ui-info-outline-item-label"}).appendTo(div); var contentDiv = $('<div>',{class:"red-ui-search-result-description red-ui-info-outline-item-label"}).appendTo(div);
var label = (typeof n === "string")? n : n.label; var label = (typeof n === "string")? n : n.label;
var newlineIndex = label.indexOf("\\n"); var newlineIndex = label.indexOf("\\n");
@ -1237,11 +1236,17 @@ RED.clipboard = (function() {
label = label.substring(0,newlineIndex)+"..."; label = label.substring(0,newlineIndex)+"...";
} }
contentDiv.text(label); contentDiv.text(label);
if (!!isConflicted) {
const conflictIcon = $('<span style="padding: 0 10px;"><i class="fa fa-exclamation-circle"></span>').appendTo(div)
RED.popover.tooltip(conflictIcon, RED._('clipboard.import.alreadyExists'))
}
// A conflicted flow should not be imported by default. // A conflicted flow should not be imported by default.
return div; return div;
} }
function getNodeLabel(n, isConflicted) { function getNodeLabel(n, isConflicted, isSelected, parent) {
n = JSON.parse(JSON.stringify(n)); n = JSON.parse(JSON.stringify(n));
n._def = RED.nodes.getType(n.type) || {}; n._def = RED.nodes.getType(n.type) || {};
if (n._def) { if (n._def) {
@ -1249,6 +1254,11 @@ RED.clipboard = (function() {
} }
var div = $('<div>',{class:"red-ui-node-list-item"}); var div = $('<div>',{class:"red-ui-node-list-item"});
RED.utils.createNodeIcon(n,true).appendTo(div); RED.utils.createNodeIcon(n,true).appendTo(div);
if (!parent && !!isConflicted) {
const conflictIcon = $('<span style="padding: 0 10px;"><i class="fa fa-exclamation-circle"></span>').appendTo(div)
RED.popover.tooltip(conflictIcon, RED._('clipboard.import.alreadyExists'))
}
return div; return div;
} }

View File

@ -54,25 +54,26 @@
return icon; return icon;
} }
var autoComplete = function(options) { function getMatch(value, searchValue) {
function getMatch(value, searchValue) { const idx = value.toLowerCase().indexOf(searchValue.toLowerCase());
const idx = value.toLowerCase().indexOf(searchValue.toLowerCase()); const len = idx > -1 ? searchValue.length : 0;
const len = idx > -1 ? searchValue.length : 0; return {
return { index: idx,
index: idx, found: idx > -1,
found: idx > -1, pre: value.substring(0,idx),
pre: value.substring(0,idx), match: value.substring(idx,idx+len),
match: value.substring(idx,idx+len), post: value.substring(idx+len),
post: value.substring(idx+len),
}
}
function generateSpans(match) {
const els = [];
if(match.pre) { els.push($('<span/>').text(match.pre)); }
if(match.match) { els.push($('<span/>',{style:"font-weight: bold; color: var(--red-ui-text-color-link);"}).text(match.match)); }
if(match.post) { els.push($('<span/>').text(match.post)); }
return els;
} }
}
function generateSpans(match) {
const els = [];
if(match.pre) { els.push($('<span/>').text(match.pre)); }
if(match.match) { els.push($('<span/>',{style:"font-weight: bold; color: var(--red-ui-text-color-link);"}).text(match.match)); }
if(match.post) { els.push($('<span/>').text(match.post)); }
return els;
}
const msgAutoComplete = function(options) {
return function(val) { return function(val) {
var matches = []; var matches = [];
options.forEach(opt => { 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 = $('<div>',{style: "display: flex"});
const valEl = $('<div/>',{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 = $('<div>').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 = $('<div>',{style: "display: flex"});
const valEl = $('<div/>',{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). // This is a hand-generated list of completions for the core nodes (based on the node help html).
var msgCompletions = [ var msgCompletions = [
{ value: "payload" }, { value: "payload" },
@ -166,23 +358,27 @@
{ value: "_session", source: ["websocket out","tcp out"] }, { value: "_session", source: ["websocket out","tcp out"] },
] ]
var allOptions = { var allOptions = {
msg: {value:"msg",label:"msg.",validate:RED.utils.validatePropertyExpression, autoComplete: autoComplete(msgCompletions)}, msg: {value:"msg",label:"msg.",validate:RED.utils.validatePropertyExpression, autoComplete: msgAutoComplete(msgCompletions)},
flow: {value:"flow",label:"flow.",hasValue:true, flow: {value:"flow",label:"flow.",hasValue:true,
options:[], options:[],
validate:RED.utils.validatePropertyExpression, validate:RED.utils.validatePropertyExpression,
parse: contextParse, parse: contextParse,
export: contextExport, export: contextExport,
valueLabel: contextLabel valueLabel: contextLabel,
autoComplete: contextAutoComplete
}, },
global: {value:"global",label:"global.",hasValue:true, global: {value:"global",label:"global.",hasValue:true,
options:[], options:[],
validate:RED.utils.validatePropertyExpression, validate:RED.utils.validatePropertyExpression,
parse: contextParse, parse: contextParse,
export: contextExport, export: contextExport,
valueLabel: contextLabel valueLabel: contextLabel,
autoComplete: contextAutoComplete
}, },
str: {value:"str",label:"string",icon:"red/images/typedInput/az.svg"}, str: {value:"str",label:"string",icon:"red/images/typedInput/az.svg"},
num: {value:"num",label:"number",icon:"red/images/typedInput/09.svg",validate:/^[+-]?[0-9]*\.?[0-9]*([eE][-+]?[0-9]+)?$/}, num: {value:"num",label:"number",icon:"red/images/typedInput/09.svg",validate: function(v) {
return (true === RED.utils.validateTypedProperty(v, "num"));
} },
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: { json: {
value:"json", value:"json",
@ -212,7 +408,25 @@
} }
}, },
re: {value:"re",label:"regular expression",icon:"red/images/typedInput/re.svg"}, re: {value:"re",label:"regular expression",icon:"red/images/typedInput/re.svg"},
date: {value:"date",label:"timestamp",icon:"fa fa-clock-o",hasValue:false}, 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: { jsonata: {
value: "jsonata", value: "jsonata",
label: "expression", label: "expression",
@ -249,7 +463,8 @@
env: { env: {
value: "env", value: "env",
label: "env variable", label: "env variable",
icon: "red/images/typedInput/env.svg" icon: "red/images/typedInput/env.svg",
autoComplete: envAutoComplete
}, },
node: { node: {
value: "node", value: "node",
@ -425,6 +640,7 @@
} }
var nlsd = false; var nlsd = false;
let contextStoreOptions;
$.widget( "nodered.typedInput", { $.widget( "nodered.typedInput", {
_create: function() { _create: function() {
@ -436,7 +652,7 @@
} }
} }
var contextStores = RED.settings.context.stores; var contextStores = RED.settings.context.stores;
var contextOptions = contextStores.map(function(store) { contextStoreOptions = contextStores.map(function(store) {
return {value:store,label: store, icon:'<i class="red-ui-typedInput-icon fa fa-database"></i>'} return {value:store,label: store, icon:'<i class="red-ui-typedInput-icon fa fa-database"></i>'}
}).sort(function(A,B) { }).sort(function(A,B) {
if (A.value === RED.settings.context.default) { if (A.value === RED.settings.context.default) {
@ -447,13 +663,17 @@
return A.value.localeCompare(B.value); return A.value.localeCompare(B.value);
} }
}) })
if (contextOptions.length < 2) { if (contextStoreOptions.length < 2) {
allOptions.flow.options = []; allOptions.flow.options = [];
allOptions.global.options = []; allOptions.global.options = [];
} else { } else {
allOptions.flow.options = contextOptions; allOptions.flow.options = contextStoreOptions;
allOptions.global.options = contextOptions; 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; nlsd = true;
var that = this; var that = this;
@ -542,7 +762,7 @@
that.element.trigger('paste',evt); that.element.trigger('paste',evt);
}); });
this.input.on('keydown', function(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 return
} }
if (evt.keyCode >= 37 && evt.keyCode <= 40) { if (evt.keyCode >= 37 && evt.keyCode <= 40) {
@ -965,6 +1185,9 @@
// If previousType is !null, then this is a change of the type, rather than the initialisation // If previousType is !null, then this is a change of the type, rather than the initialisation
var previousType = this.typeMap[this.propertyType]; var previousType = this.typeMap[this.propertyType];
previousValue = this.input.val(); previousValue = this.input.val();
if (this.input.hasClass('red-ui-autoComplete')) {
this.input.autoComplete("destroy");
}
if (previousType && this.typeChanged) { if (previousType && this.typeChanged) {
if (this.options.debug) { console.log(this.identifier,"typeChanged",{previousType,previousValue}) } if (this.options.debug) { console.log(this.identifier,"typeChanged",{previousType,previousValue}) }
@ -1011,7 +1234,9 @@
this.input.val(this.oldValues.hasOwnProperty("_")?this.oldValues["_"]:(opt.default||"")) this.input.val(this.oldValues.hasOwnProperty("_")?this.oldValues["_"]:(opt.default||""))
} }
if (previousType.autoComplete) { if (previousType.autoComplete) {
this.input.autoComplete("destroy"); if (this.input.hasClass('red-ui-autoComplete')) {
this.input.autoComplete("destroy");
}
} }
} }
this.propertyType = type; this.propertyType = type;
@ -1139,6 +1364,16 @@
} else { } else {
this.optionSelectTrigger.hide(); this.optionSelectTrigger.hide();
} }
if (opt.autoComplete) {
let searchFunction = opt.autoComplete
if (searchFunction.length === 0) {
searchFunction = opt.autoComplete.call(this)
}
this.input.autoComplete({
search: searchFunction,
minLength: 0
})
}
} }
this.optionMenu = this._createMenu(opt.options,opt,function(v){ this.optionMenu = this._createMenu(opt.options,opt,function(v){
if (!opt.multiple) { if (!opt.multiple) {
@ -1181,8 +1416,12 @@
this.valueLabelContainer.hide(); this.valueLabelContainer.hide();
this.elementDiv.show(); this.elementDiv.show();
if (opt.autoComplete) { if (opt.autoComplete) {
let searchFunction = opt.autoComplete
if (searchFunction.length === 0) {
searchFunction = opt.autoComplete.call(this)
}
this.input.autoComplete({ this.input.autoComplete({
search: opt.autoComplete, search: searchFunction,
minLength: 0 minLength: 0
}) })
} }

View File

@ -30,8 +30,26 @@ RED.contextMenu = (function () {
const isGroup = hasSelection && selection.nodes.length === 1 && selection.nodes[0].type === 'group' const isGroup = hasSelection && selection.nodes.length === 1 && selection.nodes[0].type === 'group'
const canEdit = !RED.workspaces.isLocked() const canEdit = !RED.workspaces.isLocked()
const canRemoveFromGroup = hasSelection && !!selection.nodes[0].g const canRemoveFromGroup = hasSelection && !!selection.nodes[0].g
const isAllGroups = hasSelection && selection.nodes.filter(n => n.type !== 'group').length === 0 let hasGroup, isAllGroups = true, hasDisabledNode, hasEnabledNode, hasLabeledNode, hasUnlabeledNode;
const hasGroup = hasSelection && selection.nodes.filter(n => n.type === 'group' ).length > 0 if (hasSelection) {
selection.nodes.forEach(n => {
if (n.type === 'group') {
hasGroup = true;
} else {
isAllGroups = false;
}
if (n.d) {
hasDisabledNode = true;
} else {
hasEnabledNode = true;
}
if (n.l === undefined || n.l) {
hasLabeledNode = true;
} else {
hasUnlabeledNode = true;
}
});
}
const offset = $("#red-ui-workspace-chart").offset() const offset = $("#red-ui-workspace-chart").offset()
let addX = options.x - offset.left + $("#red-ui-workspace-chart").scrollLeft() let addX = options.x - offset.left + $("#red-ui-workspace-chart").scrollLeft()
@ -44,7 +62,7 @@ RED.contextMenu = (function () {
} }
menuItems.push( menuItems.push(
{ onselect: 'core:show-action-list', onpostselect: function () { } } { onselect: 'core:show-action-list', label: RED._("contextMenu.showActionList"), onpostselect: function () { } }
) )
const insertOptions = [] const insertOptions = []
@ -55,7 +73,7 @@ RED.contextMenu = (function () {
onselect: function () { onselect: function () {
RED.view.showQuickAddDialog({ RED.view.showQuickAddDialog({
position: [addX, addY], position: [addX, addY],
touchTrigger: true, touchTrigger: 'ontouchstart' in window,
splice: isSingleLink ? selection.links[0] : undefined, splice: isSingleLink ? selection.links[0] : undefined,
// spliceMultiple: isMultipleLinks // spliceMultiple: isMultipleLinks
}) })
@ -108,16 +126,16 @@ RED.contextMenu = (function () {
const nodeOptions = [] const nodeOptions = []
if (!hasMultipleSelection && !isGroup) { if (!hasMultipleSelection && !isGroup) {
nodeOptions.push( nodeOptions.push(
{ onselect: 'core:show-node-help' }, { onselect: 'core:show-node-help', label: RED._('menu.label.showNodeHelp') },
null null
) )
} }
nodeOptions.push( nodeOptions.push(
{ onselect: 'core:enable-selected-nodes' }, { onselect: 'core:enable-selected-nodes', label: RED._('menu.label.enableSelectedNodes'), disabled: !hasDisabledNode },
{ onselect: 'core:disable-selected-nodes' }, { onselect: 'core:disable-selected-nodes', label: RED._('menu.label.disableSelectedNodes'), disabled: !hasEnabledNode },
null, null,
{ onselect: 'core:show-selected-node-labels' }, { onselect: 'core:show-selected-node-labels', label: RED._('menu.label.showSelectedNodeLabels'), disabled: !hasUnlabeledNode },
{ onselect: 'core:hide-selected-node-labels' } { onselect: 'core:hide-selected-node-labels', label: RED._('menu.label.hideSelectedNodeLabels'), disabled: !hasLabeledNode }
) )
menuItems.push({ menuItems.push({
label: RED._('sidebar.info.node'), label: RED._('sidebar.info.node'),
@ -126,8 +144,8 @@ RED.contextMenu = (function () {
menuItems.push({ menuItems.push({
label: RED._('sidebar.info.group'), label: RED._('sidebar.info.group'),
options: [ options: [
{ onselect: 'core:group-selection' }, { onselect: 'core:group-selection', label: RED._("menu.label.groupSelection") },
{ onselect: 'core:ungroup-selection', disabled: !hasGroup }, { onselect: 'core:ungroup-selection', label: RED._("menu.label.ungroupSelection"), disabled: !hasGroup },
] ]
}) })
if (hasGroup) { if (hasGroup) {
@ -143,8 +161,8 @@ RED.contextMenu = (function () {
} }
menuItems[menuItems.length - 1].options.push( menuItems[menuItems.length - 1].options.push(
null, null,
{ onselect: 'core:copy-group-style', disabled: !hasGroup }, { onselect: 'core:copy-group-style', label: RED._("keyboard.copyGroupStyle"), disabled: !hasGroup },
{ onselect: 'core:paste-group-style', disabled: !hasGroup} { onselect: 'core:paste-group-style', label: RED._("keyboard.pasteGroupStyle"), disabled: !hasGroup}
) )
} }
if (canEdit && hasMultipleSelection) { if (canEdit && hasMultipleSelection) {
@ -168,16 +186,16 @@ RED.contextMenu = (function () {
menuItems.push( menuItems.push(
null, null,
{ onselect: 'core:undo', disabled: RED.history.list().length === 0 }, { onselect: 'core:undo', label: RED._("keyboard.undoChange"), disabled: RED.history.list().length === 0 },
{ onselect: 'core:redo', disabled: RED.history.listRedo().length === 0 }, { onselect: 'core:redo', label: RED._("keyboard.redoChange"), disabled: RED.history.listRedo().length === 0 },
null, null,
{ onselect: 'core:cut-selection-to-internal-clipboard', label: RED._("keyboard.cutNode"), disabled: !canEdit || !hasSelection }, { onselect: 'core:cut-selection-to-internal-clipboard', label: RED._("keyboard.cutNode"), disabled: !canEdit || !hasSelection },
{ onselect: 'core:copy-selection-to-internal-clipboard', label: RED._("keyboard.copyNode"), disabled: !hasSelection }, { onselect: 'core:copy-selection-to-internal-clipboard', label: RED._("keyboard.copyNode"), disabled: !hasSelection },
{ onselect: 'core:paste-from-internal-clipboard', label: RED._("keyboard.pasteNode"), disabled: !canEdit || !RED.view.clipboard() }, { onselect: 'core:paste-from-internal-clipboard', label: RED._("keyboard.pasteNode"), disabled: !canEdit || !RED.view.clipboard() },
{ onselect: 'core:delete-selection', disabled: !canEdit || !canDelete }, { onselect: 'core:delete-selection', label: RED._('keyboard.deleteSelected'), disabled: !canEdit || !canDelete },
{ onselect: 'core:delete-selection-and-reconnect', label: RED._('keyboard.deleteReconnect'), disabled: !canEdit || !canDelete }, { onselect: 'core:delete-selection-and-reconnect', label: RED._('keyboard.deleteReconnect'), disabled: !canEdit || !canDelete },
{ onselect: 'core:show-export-dialog', label: RED._("menu.label.export") }, { onselect: 'core:show-export-dialog', label: RED._("menu.label.export") },
{ onselect: 'core:select-all-nodes' }, { onselect: 'core:select-all-nodes', label: RED._("keyboard.selectAll") },
) )
} }

View File

@ -989,9 +989,10 @@ RED.diff = (function() {
} }
if (localNode && remoteNode && typeof localNode[d] === "string") { if (localNode && remoteNode && typeof localNode[d] === "string") {
if (/\n/.test(localNode[d]) || /\n/.test(remoteNode[d])) { if (/\n/.test(localNode[d]) || /\n/.test(remoteNode[d])) {
$('<button class="red-ui-button red-ui-button-small red-ui-diff-text-diff-button"><i class="fa fa-file-o"> <i class="fa fa-caret-left"></i> <i class="fa fa-caret-right"></i> <i class="fa fa-file-o"></i></button>').on("click", function() { var textDiff = $('<button class="red-ui-button red-ui-button-small red-ui-diff-text-diff-button"><i class="fa fa-file-o"> <i class="fa fa-caret-left"></i> <i class="fa fa-caret-right"></i> <i class="fa fa-file-o"></i></button>').on("click", function() {
showTextDiff(localNode[d],remoteNode[d]); showTextDiff(localNode[d],remoteNode[d]);
}).appendTo(propertyNameCell); }).appendTo(propertyNameCell);
RED.popover.tooltip(textDiff, RED._("diff.compareChanges"));
} }
} }

View File

@ -115,8 +115,9 @@ RED.editor = (function() {
var valid = validateNodeProperty(node, definition, prop, properties[prop]); var valid = validateNodeProperty(node, definition, prop, properties[prop]);
if ((typeof valid) === "string") { if ((typeof valid) === "string") {
result.push(valid); result.push(valid);
} } else if (Array.isArray(valid)) {
else if(!valid) { result = result.concat(valid)
} else if(!valid) {
result.push(prop); result.push(prop);
} }
} }
@ -165,7 +166,7 @@ RED.editor = (function() {
// If the validator takes two arguments, it is a 3.x validator that // If the validator takes two arguments, it is a 3.x validator that
// can return a String to mean 'invalid' and provide a reason // can return a String to mean 'invalid' and provide a reason
if ((definition[property].validate.length === 2) && if ((definition[property].validate.length === 2) &&
((typeof valid) === "string")) { ((typeof valid) === "string") || Array.isArray(valid)) {
return valid; return valid;
} else { } else {
// Otherwise, a 2.x returns a truth-like/false-like value that // Otherwise, a 2.x returns a truth-like/false-like value that
@ -181,6 +182,17 @@ RED.editor = (function() {
error: err.message error: err.message
}); });
} }
} else if (valid) {
// If the validator is not provided in node property => Check if the input has a validator
if ("category" in node._def) {
const isConfig = node._def.category === "config";
const prefix = isConfig ? "node-config-input" : "node-input";
const input = $("#"+prefix+"-"+property);
const isTypedInput = input.length > 0 && input.next(".red-ui-typedInput-container").length > 0;
if (isTypedInput) {
valid = input.typedInput("validate");
}
}
} }
if (valid && definition[property].type && RED.nodes.getType(definition[property].type) && !("validate" in definition[property])) { if (valid && definition[property].type && RED.nodes.getType(definition[property].type) && !("validate" in definition[property])) {
if (!value || value == "_ADD_") { if (!value || value == "_ADD_") {
@ -1219,7 +1231,11 @@ RED.editor = (function() {
}) })
if (node_def.hasUsers !== false) { if (node_def.hasUsers !== false) {
$('<span><i class="fa fa-info-circle"></i> <span id="red-ui-editor-config-user-count"></span></span>').css("margin-left", "10px").appendTo(trayFooterLeft); // $('<span><i class="fa fa-info-circle"></i> <span id="red-ui-editor-config-user-count"></span></span>').css("margin-left", "10px").appendTo(trayFooterLeft);
$('<button type="button" class="red-ui-button"><i class="fa fa-user"></i><span id="red-ui-editor-config-user-count"></span></button>').on('click', function() {
RED.sidebar.info.outliner.search('uses:'+editing_config_node.id)
RED.sidebar.info.show()
}).appendTo(trayFooterLeft);
} }
trayFooter.append('<span class="red-ui-tray-footer-right"><span id="red-ui-editor-config-scope-warning" data-i18n="[title]editor.errors.scopeChange"><i class="fa fa-warning"></i></span><select id="red-ui-editor-config-scope"></select></span>'); trayFooter.append('<span class="red-ui-tray-footer-right"><span id="red-ui-editor-config-scope-warning" data-i18n="[title]editor.errors.scopeChange"><i class="fa fa-warning"></i></span><select id="red-ui-editor-config-scope"></select></span>');
@ -1277,7 +1293,8 @@ RED.editor = (function() {
}); });
} }
if (node_def.hasUsers !== false) { if (node_def.hasUsers !== false) {
$("#red-ui-editor-config-user-count").text(RED._("editor.nodesUse", {count:editing_config_node.users.length})).parent().show(); $("#red-ui-editor-config-user-count").text(editing_config_node.users.length).parent().show();
RED.popover.tooltip($("#red-ui-editor-config-user-count").parent(), function() { return RED._('editor.nodesUse',{count:editing_config_node.users.length})});
} }
trayBody.i18n(); trayBody.i18n();
trayFooter.i18n(); trayFooter.i18n();
@ -2070,6 +2087,7 @@ RED.editor = (function() {
} }
}, },
editBuffer: function(options) { showTypeEditor("_buffer", options) }, editBuffer: function(options) { showTypeEditor("_buffer", options) },
getEditStack: function () { return [...editStack] },
buildEditForm: buildEditForm, buildEditForm: buildEditForm,
validateNode: validateNode, validateNode: validateNode,
updateNodeProperties: updateNodeProperties, updateNodeProperties: updateNodeProperties,

View File

@ -121,7 +121,7 @@
var i=0,l=bufferBinValue.length; var i=0,l=bufferBinValue.length;
var c = 0; var c = 0;
for(i=0;i<l;i++) { for(i=0;i<l;i++) {
var d = parseInt(bufferBinValue[i]); var d = parseInt(Number(bufferBinValue[i]));
if (!isString && (isNaN(d) || d < 0 || d > 255)) { if (!isString && (isNaN(d) || d < 0 || d > 255)) {
valid = false; valid = false;
break; break;

View File

@ -966,12 +966,10 @@ RED.editor.codeEditor.monaco = (function() {
//Unbind ctrl-Enter (default action is to insert a newline in editor) This permits the shortcut to close the tray. //Unbind ctrl-Enter (default action is to insert a newline in editor) This permits the shortcut to close the tray.
try { try {
ed._standaloneKeybindingService.addDynamicKeybinding( monaco.editor.addKeybindingRule({keybinding: 0, command: "-editor.action.insertLineAfter"});
'-editor.action.insertLineAfter', // command ID prefixed by '-' } catch (error) {
null, // keybinding console.warn(error)
() => {} // need to pass an empty handler }
);
} catch (error) { }
ed.nodered = { ed.nodered = {
refreshModuleLibs: refreshModuleLibs //expose this for function node externalModules refresh refreshModuleLibs: refreshModuleLibs //expose this for function node externalModules refresh

View File

@ -169,7 +169,7 @@
var currentScrollTop = $(".red-ui-editor-type-markdown-panel-preview").scrollTop(); var currentScrollTop = $(".red-ui-editor-type-markdown-panel-preview").scrollTop();
$(".red-ui-editor-type-markdown-panel-preview").html(RED.utils.renderMarkdown(expressionEditor.getValue())); $(".red-ui-editor-type-markdown-panel-preview").html(RED.utils.renderMarkdown(expressionEditor.getValue()));
$(".red-ui-editor-type-markdown-panel-preview").scrollTop(currentScrollTop); $(".red-ui-editor-type-markdown-panel-preview").scrollTop(currentScrollTop);
mermaid.init(); RED.editor.mermaid.render()
},200); },200);
}) })
if (options.header) { if (options.header) {
@ -178,7 +178,7 @@
if (value) { if (value) {
$(".red-ui-editor-type-markdown-panel-preview").html(RED.utils.renderMarkdown(expressionEditor.getValue())); $(".red-ui-editor-type-markdown-panel-preview").html(RED.utils.renderMarkdown(expressionEditor.getValue()));
mermaid.init(); RED.editor.mermaid.render()
} }
panels = RED.panels.create({ panels = RED.panels.create({
id:"red-ui-editor-type-markdown-panels", id:"red-ui-editor-type-markdown-panels",

View File

@ -0,0 +1,54 @@
RED.editor.mermaid = (function () {
let initializing = false
let loaded = false
let pendingEvals = []
let diagramIds = 0
function render(selector = '.mermaid') {
// $(selector).hide()
if (!loaded) {
pendingEvals.push(selector)
if (!initializing) {
initializing = true
$.getScript(
'vendor/mermaid/mermaid.min.js',
function (data, stat, jqxhr) {
mermaid.initialize({
startOnLoad: false,
theme: RED.settings.get('mermaid', {}).theme
})
loaded = true
while(pendingEvals.length > 0) {
const pending = pendingEvals.shift()
render(pending)
}
}
)
}
} else {
const nodes = document.querySelectorAll(selector)
nodes.forEach(async node => {
if (!node.getAttribute('mermaid-processed')) {
const mermaidContent = node.innerText
node.setAttribute('mermaid-processed', true)
try {
const { svg } = await mermaid.render('mermaid-render-'+Date.now()+'-'+(diagramIds++), mermaidContent);
node.innerHTML = svg
} catch (err) {
$('<div>').css({
fontSize: '0.8em',
border: '1px solid var(--red-ui-border-color-error)',
padding: '5px',
marginBottom: '10px',
}).text(err.toString()).prependTo(node)
}
}
})
}
}
return {
render: render,
};
})();

View File

@ -196,7 +196,7 @@
} }
$('<div class="form-row">'+ $('<div class="form-row">'+
'<label for="node-input-show-label-btn" data-i18n="editor.label"></label>'+ '<label for="node-input-show-label" data-i18n="editor.label"></label>'+
'<span style="margin-right: 2px;"/>'+ '<span style="margin-right: 2px;"/>'+
'<input type="checkbox" id="node-input-show-label"/>'+ '<input type="checkbox" id="node-input-show-label"/>'+
'</div>').appendTo(dialogForm); '</div>').appendTo(dialogForm);

View File

@ -71,7 +71,7 @@ RED.envVar = (function() {
}; };
if (item.name.trim() !== "") { if (item.name.trim() !== "") {
new_env.push(item); new_env.push(item);
if ((item.type === "cred") && (item.value !== "__PWRD__")) { if (item.type === "cred") {
credentials.map[item.name] = item.value; credentials.map[item.name] = item.value;
credentials.map["has_"+item.name] = (item.value !== ""); credentials.map["has_"+item.name] = (item.value !== "");
item.value = "__PWRD__"; item.value = "__PWRD__";

View File

@ -1,46 +0,0 @@
// Mermaid diagram stub library for on-demand dynamic loading
// Will be overwritten after script loading by $.getScript
var mermaid = (function () {
var enabled /* = undefined */;
var initializing = false;
var initCalled = false;
function initialize(opt) {
if (enabled === undefined) {
if (RED.settings.markdownEditor &&
RED.settings.markdownEditor.mermaid) {
enabled = RED.settings.markdownEditor.mermaid.enabled;
}
else {
enabled = true;
}
}
if (enabled) {
initializing = true;
$.getScript("vendor/mermaid/mermaid.min.js",
function (data, stat, jqxhr) {
$(".mermaid").show();
// invoke loaded mermaid API
initializing = false;
mermaid.initialize(opt);
if (initCalled) {
mermaid.init();
initCalled = false;
}
});
}
}
function init() {
if (initializing) {
$(".mermaid").hide();
initCalled = true;
}
}
return {
initialize: initialize,
init: init,
};
})();

View File

@ -484,7 +484,7 @@ RED.palette = (function() {
var currentLabel = paletteNode.attr("data-palette-label"); var currentLabel = paletteNode.attr("data-palette-label");
var currentInfo = paletteNode.attr("data-palette-info"); var currentInfo = paletteNode.attr("data-palette-info");
if (currentLabel !== sf.name || currentInfo !== sf.info) { if (currentLabel !== sf.name || currentInfo !== sf.info || sf.in.length > 0 || sf.out.length > 0) {
paletteNode.attr("data-palette-info",sf.info); paletteNode.attr("data-palette-info",sf.info);
setLabel(sf.type+":"+sf.id,paletteNode,sf.name,RED.utils.renderMarkdown(sf.info||"")); setLabel(sf.type+":"+sf.id,paletteNode,sf.name,RED.utils.renderMarkdown(sf.info||""));
} }

View File

@ -166,7 +166,7 @@ RED.projects.settings = (function() {
var description = addTargetToExternalLinks($('<span class="red-ui-text-bidi-aware" dir=\"'+RED.text.bidi.resolveBaseTextDir(desc)+'">'+desc+'</span>')).appendTo(container); var description = addTargetToExternalLinks($('<span class="red-ui-text-bidi-aware" dir=\"'+RED.text.bidi.resolveBaseTextDir(desc)+'">'+desc+'</span>')).appendTo(container);
description.find(".red-ui-text-bidi-aware").contents().filter(function() { return this.nodeType === 3 && this.textContent.trim() !== "" }).wrap( "<span></span>" ); description.find(".red-ui-text-bidi-aware").contents().filter(function() { return this.nodeType === 3 && this.textContent.trim() !== "" }).wrap( "<span></span>" );
setTimeout(function () { setTimeout(function () {
mermaid.init(); RED.editor.mermaid.render()
}, 200); }, 200);
} }

View File

@ -647,9 +647,9 @@ RED.sidebar.versionControl = (function() {
$.getJSON("projects/"+activeProject.name+"/commits/"+entry.sha,function(result) { $.getJSON("projects/"+activeProject.name+"/commits/"+entry.sha,function(result) {
result.project = activeProject; result.project = activeProject;
result.parents = entry.parents; result.parents = entry.parents;
result.oldRev = entry.sha+"~1"; result.oldRev = entry.parents[0].length !== 0 ? entry.sha+"~1" : entry.sha;
result.newRev = entry.sha; result.newRev = entry.sha;
result.oldRevTitle = RED._("sidebar.project.versionControl.commitCapital")+" "+entry.sha.substring(0,7)+"~1"; result.oldRevTitle = entry.parents[0].length !== 0 ? RED._("sidebar.project.versionControl.commitCapital")+" "+entry.sha.substring(0,7)+"~1" : " ";
result.newRevTitle = RED._("sidebar.project.versionControl.commitCapital")+" "+entry.sha.substring(0,7); result.newRevTitle = RED._("sidebar.project.versionControl.commitCapital")+" "+entry.sha.substring(0,7);
result.date = humanizeSinceDate(parseInt(entry.date)); result.date = humanizeSinceDate(parseInt(entry.date));
RED.diff.showCommitDiff(result); RED.diff.showCommitDiff(result);

View File

@ -158,6 +158,7 @@ RED.sidebar.config = (function() {
entry.data('node',node.id); entry.data('node',node.id);
nodeDiv.data('node',node.id); nodeDiv.data('node',node.id);
var label = $('<div class="red-ui-palette-label"></div>').text(labelText).appendTo(nodeDiv); var label = $('<div class="red-ui-palette-label"></div>').text(labelText).appendTo(nodeDiv);
if (node.d) { if (node.d) {
nodeDiv.addClass("red-ui-palette-node-config-disabled"); nodeDiv.addClass("red-ui-palette-node-config-disabled");
$('<i class="fa fa-ban"></i>').prependTo(label); $('<i class="fa fa-ban"></i>').prependTo(label);
@ -179,6 +180,20 @@ RED.sidebar.config = (function() {
nodeDiv.addClass("red-ui-palette-node-config-unused"); nodeDiv.addClass("red-ui-palette-node-config-unused");
} }
} }
if (!node.valid) {
nodeDiv.addClass("red-ui-palette-node-config-invalid")
const nodeDivAnnotations = $('<svg class="red-ui-palette-node-annotations red-ui-flow-node-error" width="10" height="10"></svg>').appendTo(nodeDiv)
const errorBadge = document.createElementNS("http://www.w3.org/2000/svg","path");
errorBadge.setAttribute("d","M 0,9 l 10,0 -5,-8 z");
nodeDivAnnotations.append($(errorBadge))
RED.popover.tooltip(nodeDivAnnotations, function () {
if (node.validationErrors && node.validationErrors.length > 0) {
return RED._("editor.errors.invalidProperties")+"<br> - "+node.validationErrors.join("<br> - ")
}
})
}
nodeDiv.on('click',function(e) { nodeDiv.on('click',function(e) {
e.stopPropagation(); e.stopPropagation();
RED.view.select(false); RED.view.select(false);

View File

@ -232,7 +232,7 @@ RED.sidebar.context = (function() {
typeHint: data.format, typeHint: data.format,
sourceId: id+"."+k, sourceId: id+"."+k,
tools: tools, tools: tools,
path: "" path: k
}).appendTo(propRow.children()[1]); }).appendTo(propRow.children()[1]);
} }
}) })
@ -278,7 +278,7 @@ RED.sidebar.context = (function() {
typeHint: data.format, typeHint: data.format,
sourceId: id+"."+k, sourceId: id+"."+k,
tools: tools, tools: tools,
path: "" path: k
}).appendTo(propRow.children()[1]); }).appendTo(propRow.children()[1]);
} }
}); });
@ -299,7 +299,7 @@ RED.sidebar.context = (function() {
typeHint: v.format, typeHint: v.format,
sourceId: id+"."+k, sourceId: id+"."+k,
tools: tools, tools: tools,
path: "" path: k
}).appendTo(propRow.children()[1]); }).appendTo(propRow.children()[1]);
if (contextStores.length > 1) { if (contextStores.length > 1) {
$("<span>",{class:"red-ui-sidebar-context-property-storename"}).text(v.store).appendTo($(propRow.children()[0])) $("<span>",{class:"red-ui-sidebar-context-property-storename"}).text(v.store).appendTo($(propRow.children()[0]))

View File

@ -383,6 +383,7 @@ RED.sidebar.help = (function() {
$(this).toggleClass('expanded',!isExpanded); $(this).toggleClass('expanded',!isExpanded);
}) })
helpSection.parent().scrollTop(0); helpSection.parent().scrollTop(0);
RED.editor.mermaid.render()
} }
function set(html,title) { function set(html,title) {

View File

@ -464,7 +464,7 @@ RED.sidebar.info = (function() {
} }
$(this).toggleClass('expanded',!isExpanded); $(this).toggleClass('expanded',!isExpanded);
}); });
mermaid.init(); RED.editor.mermaid.render()
} }
var tips = (function() { var tips = (function() {

View File

@ -186,8 +186,15 @@ RED.typeSearch = (function() {
var iconContainer = $('<div/>',{class:"red-ui-palette-icon-container"}).appendTo(nodeDiv); var iconContainer = $('<div/>',{class:"red-ui-palette-icon-container"}).appendTo(nodeDiv);
RED.utils.createIconElement(icon_url, iconContainer, false); RED.utils.createIconElement(icon_url, iconContainer, false);
if (/^subflow:/.test(object.type)) {
if (!/^_action_:/.test(object.type) && object.type !== "junction") { var sf = RED.nodes.subflow(object.type.substring(8));
if (sf.in.length > 0) {
$('<div/>',{class:"red-ui-search-result-node-port"}).appendTo(nodeDiv);
}
if (sf.out.length > 0) {
$('<div/>',{class:"red-ui-search-result-node-port red-ui-search-result-node-output"}).appendTo(nodeDiv);
}
} else if (!/^_action_:/.test(object.type) && object.type !== "junction") {
if (def.inputs > 0) { if (def.inputs > 0) {
$('<div/>',{class:"red-ui-search-result-node-port"}).appendTo(nodeDiv); $('<div/>',{class:"red-ui-search-result-node-port"}).appendTo(nodeDiv);
} }
@ -323,7 +330,7 @@ RED.typeSearch = (function() {
} }
} }
function applyFilter(filter,type,def) { function applyFilter(filter,type,def) {
return !filter || return !def || !filter ||
( (
(!filter.spliceMultiple) && (!filter.spliceMultiple) &&
(!filter.type || type === filter.type) && (!filter.type || type === filter.type) &&

View File

@ -101,28 +101,8 @@ RED.utils = (function() {
renderer.code = function (code, lang) { renderer.code = function (code, lang) {
if(lang === "mermaid") { if(lang === "mermaid") {
// mermaid diagram rendering return `<pre class='mermaid'>${code}</pre>`;
if (mermaidIsEnabled === undefined) { } else {
if (RED.settings.markdownEditor &&
RED.settings.markdownEditor.mermaid) {
mermaidIsEnabled = RED.settings.markdownEditor.mermaid.enabled;
}
else {
mermaidIsEnabled = true;
}
}
if (mermaidIsEnabled) {
if (!mermaidIsInitialized) {
mermaidIsInitialized = true;
mermaid.initialize({startOnLoad:false});
}
return `<pre class='mermaid'>${code}</pre>`;
}
else {
return `<details><summary>${RED._("markdownEditor.mermaid.summary")}</summary><pre><code>${code}</code></pre></details>`;
}
}
else {
return "<pre><code>" +code +"</code></pre>"; return "<pre><code>" +code +"</code></pre>";
} }
}; };
@ -917,6 +897,51 @@ RED.utils = (function() {
} }
} }
/**
* Checks a typed property is valid according to the type.
* Returns true if valid.
* Return String error message if invalid
* @param {*} propertyType
* @param {*} propertyValue
* @returns true if valid, String if invalid
*/
function validateTypedProperty(propertyValue, propertyType, opt) {
let error
if (propertyType === 'json') {
try {
JSON.parse(propertyValue);
} catch(err) {
error = RED._("validator.errors.invalid-json", {
error: err.message
})
}
} else if (propertyType === 'msg' || propertyType === 'flow' || propertyType === 'global' ) {
if (!RED.utils.validatePropertyExpression(propertyValue)) {
error = RED._("validator.errors.invalid-prop")
}
} else if (propertyType === 'num') {
if (!/^NaN$|^[+-]?[0-9]*\.?[0-9]*([eE][-+]?[0-9]+)?$|^[+-]?(0b|0B)[01]+$|^[+-]?(0o|0O)[0-7]+$|^[+-]?(0x|0X)[0-9a-fA-F]+$/.test(propertyValue)) {
error = RED._("validator.errors.invalid-num")
}
} else if (propertyType === 'jsonata') {
try {
jsonata(propertyValue)
} catch(err) {
error = RED._("validator.errors.invalid-expr", {
error: err.message
})
}
}
if (error) {
if (opt && opt.label) {
return opt.label+': '+error
}
return error
}
return true
}
function getMessageProperty(msg,expr) { function getMessageProperty(msg,expr) {
var result = null; var result = null;
var msgPropParts; var msgPropParts;
@ -1451,6 +1476,7 @@ RED.utils = (function() {
getDarkerColor: getDarkerColor, getDarkerColor: getDarkerColor,
parseModuleList: parseModuleList, parseModuleList: parseModuleList,
checkModuleAllowed: checkModuleAllowed, checkModuleAllowed: checkModuleAllowed,
getBrowserInfo: getBrowserInfo getBrowserInfo: getBrowserInfo,
validateTypedProperty: validateTypedProperty
} }
})(); })();

View File

@ -4155,10 +4155,15 @@ RED.view = (function() {
scaleFactor = 30/largestEdge; scaleFactor = 30/largestEdge;
} }
var width = img.width * scaleFactor; var width = img.width * scaleFactor;
if (width > 20) {
scalefactor *= 20/width;
width = 20;
}
var height = img.height * scaleFactor; var height = img.height * scaleFactor;
icon.attr("width",width); icon.attr("width",width);
icon.attr("height",height); icon.attr("height",height);
icon.attr("x",15-width/2); icon.attr("x",15-width/2);
icon.attr("y",(30-height)/2);
} }
icon.attr("xlink:href",iconUrl); icon.attr("xlink:href",iconUrl);
icon.style("display",null); icon.style("display",null);
@ -4187,7 +4192,7 @@ RED.view = (function() {
nodeEl.__statusGroup__.style.display = "none"; nodeEl.__statusGroup__.style.display = "none";
} else { } else {
nodeEl.__statusGroup__.style.display = "inline"; nodeEl.__statusGroup__.style.display = "inline";
let backgroundWidth = 12 let backgroundWidth = 15
var fill = status_colours[d.status.fill]; // Only allow our colours for now var fill = status_colours[d.status.fill]; // Only allow our colours for now
if (d.status.shape == null && fill == null) { if (d.status.shape == null && fill == null) {
backgroundWidth = 0 backgroundWidth = 0
@ -4207,7 +4212,11 @@ RED.view = (function() {
nodeEl.__statusLabel__.textContent = ""; nodeEl.__statusLabel__.textContent = "";
} }
const textSize = nodeEl.__statusLabel__.getBBox() const textSize = nodeEl.__statusLabel__.getBBox()
nodeEl.__statusBackground__.setAttribute('width', backgroundWidth + textSize.width + 6) backgroundWidth += textSize.width
if (backgroundWidth > 0 && textSize.width > 0) {
backgroundWidth += 6
}
nodeEl.__statusBackground__.setAttribute('width', backgroundWidth)
} }
delete d.dirtyStatus; delete d.dirtyStatus;
} }
@ -4619,8 +4628,8 @@ RED.view = (function() {
statusBackground.setAttribute("y",-1); statusBackground.setAttribute("y",-1);
statusBackground.setAttribute("width",200); statusBackground.setAttribute("width",200);
statusBackground.setAttribute("height",13); statusBackground.setAttribute("height",13);
statusBackground.setAttribute("rx",1); statusBackground.setAttribute("rx",2);
statusBackground.setAttribute("ry",1); statusBackground.setAttribute("ry",2);
statusEl.appendChild(statusBackground); statusEl.appendChild(statusBackground);
node[0][0].__statusBackground__ = statusBackground; node[0][0].__statusBackground__ = statusBackground;
@ -6243,6 +6252,10 @@ RED.view = (function() {
} }
}) })
} }
if (selection.links) {
selectedLinks.clear();
selection.links.forEach(selectedLinks.add);
}
} }
} }
updateSelection(); updateSelection();

View File

@ -17,6 +17,8 @@
RED.workspaces = (function() { RED.workspaces = (function() {
const documentTitle = document.title;
var activeWorkspace = 0; var activeWorkspace = 0;
var workspaceIndex = 0; var workspaceIndex = 0;
@ -339,12 +341,18 @@ RED.workspaces = (function() {
$("#red-ui-workspace-chart").show(); $("#red-ui-workspace-chart").show();
activeWorkspace = tab.id; activeWorkspace = tab.id;
window.location.hash = 'flow/'+tab.id; window.location.hash = 'flow/'+tab.id;
if (tab.label) {
document.title = `${documentTitle} : ${tab.label}`
} else {
document.title = documentTitle
}
$("#red-ui-workspace").toggleClass("red-ui-workspace-disabled", !!tab.disabled); $("#red-ui-workspace").toggleClass("red-ui-workspace-disabled", !!tab.disabled);
$("#red-ui-workspace").toggleClass("red-ui-workspace-locked", !!tab.locked); $("#red-ui-workspace").toggleClass("red-ui-workspace-locked", !!tab.locked);
} else { } else {
$("#red-ui-workspace-chart").hide(); $("#red-ui-workspace-chart").hide();
activeWorkspace = 0; activeWorkspace = 0;
window.location.hash = ''; window.location.hash = '';
document.title = documentTitle
} }
event.workspace = activeWorkspace; event.workspace = activeWorkspace;
RED.events.emit("workspace:change",event); RED.events.emit("workspace:change",event);

View File

@ -40,46 +40,32 @@ RED.validators = {
return opt ? RED._("validator.errors.invalid-regexp") : false; return opt ? RED._("validator.errors.invalid-regexp") : false;
}; };
}, },
typedInput: function(ptypeName,isConfig,mopt) { typedInput: function(ptypeName, isConfig, mopt) {
let options = ptypeName
if (typeof ptypeName === 'string' ) {
options = {}
options.typeField = ptypeName
options.isConfig = isConfig
options.allowBlank = false
}
return function(v, opt) { return function(v, opt) {
var ptype = $("#node-"+(isConfig?"config-":"")+"input-"+ptypeName).val() || this[ptypeName]; let ptype = options.type
if (ptype === 'json') { if (!ptype && options.typeField) {
try { ptype = $("#node-"+(options.isConfig?"config-":"")+"input-"+options.typeField).val() || this[options.typeField];
JSON.parse(v);
return true;
} catch(err) {
if (opt && opt.label) {
return RED._("validator.errors.invalid-json-prop", {
error: err.message,
prop: opt.label,
});
}
return opt ? RED._("validator.errors.invalid-json", {
error: err.message
}) : false;
}
} else if (ptype === 'msg' || ptype === 'flow' || ptype === 'global' ) {
if (RED.utils.validatePropertyExpression(v)) {
return true;
}
if (opt && opt.label) {
return RED._("validator.errors.invalid-prop-prop", {
prop: opt.label
});
}
return opt ? RED._("validator.errors.invalid-prop") : false;
} else if (ptype === 'num') {
if (/^[+-]?[0-9]*\.?[0-9]*([eE][-+]?[0-9]+)?$/.test(v)) {
return true;
}
if (opt && opt.label) {
return RED._("validator.errors.invalid-num-prop", {
prop: opt.label
});
}
return opt ? RED._("validator.errors.invalid-num") : false;
} }
return true; if (options.allowBlank && v === '') {
}; return true
}
if (options.allowUndefined && v === undefined) {
return true
}
const result = RED.utils.validateTypedProperty(v, ptype, opt)
if (result === true || opt) {
// Valid, or opt provided - return result as-is
return result
}
// No opt - need to return false for backwards compatibilty
return false
}
} }
}; };

View File

@ -114,6 +114,7 @@
pointer-events: stroke; pointer-events: stroke;
} }
.red-ui-flow-group-outline-select { .red-ui-flow-group-outline-select {
cursor: move;
fill: none; fill: none;
stroke: var(--red-ui-node-selected-color); stroke: var(--red-ui-node-selected-color);
pointer-events: none; pointer-events: none;

View File

@ -194,10 +194,6 @@
} }
} }
.red-ui-clipboard-dialog-import-conflicts-controls { .red-ui-clipboard-dialog-import-conflicts-controls {
position: absolute;
top:0;
bottom: 0;
right: 0px;
text-align: center; text-align: center;
color: var(--red-ui-form-text-color); color: var(--red-ui-form-text-color);
.form-row & label { .form-row & label {
@ -218,9 +214,21 @@
margin: 0; margin: 0;
} }
} }
#red-ui-clipboard-dialog-import-conflicts-list .disabled .red-ui-info-outline-item { #red-ui-clipboard-dialog-import-conflicts-list .disabled {
opacity: 0.4; .red-ui-info-outline-item,
.red-ui-node-list-item {
opacity: 0.4;
}
} }
#red-ui-clipboard-dialog-import-conflicts-list .red-ui-node-list-item {
display: flex;
align-items: center;
& > :first-child {
flex-grow: 1
}
}
.form-row label.red-ui-clipboard-dialog-import-conflicts-gutter { .form-row label.red-ui-clipboard-dialog-import-conflicts-gutter {
box-sizing: border-box; box-sizing: border-box;
width: 22px; width: 22px;

View File

@ -825,6 +825,7 @@ div.red-ui-projects-dialog-ssh-public-key {
margin-top: 0 !important; margin-top: 0 !important;
padding: 5px 10px; padding: 5px 10px;
margin-bottom: 10px; margin-bottom: 10px;
border-radius: 3px 3px 0px 0px;
} }
} }

View File

@ -36,7 +36,7 @@ ul.red-ui-sidebar-node-config-list {
text-align: center; text-align: center;
} }
.red-ui-palette-node { .red-ui-palette-node {
overflow: hidden; // overflow: hidden;
cursor: default; cursor: default;
&.selected { &.selected {
border-color: transparent; border-color: transparent;
@ -113,6 +113,15 @@ ul.red-ui-sidebar-node-config-list li.red-ui-palette-node-config-type {
margin-right: 5px; margin-right: 5px;
} }
} }
.red-ui-palette-node-config-invalid {
border-color: var(--red-ui-form-input-border-error-color)
}
.red-ui-palette-node-annotations {
position: absolute;
left: calc(100% - 15px);
top: -8px;
display: block;
}
.red-ui-sidebar-node-config-filter-info { .red-ui-sidebar-node-config-filter-info {
position: absolute; position: absolute;
top: 0; top: 0;

View File

@ -22,26 +22,26 @@
limitations under the License. limitations under the License.
--> -->
<title>{{ page.title }}</title> <title>{{ page.title }}</title>
<link rel="icon" type="image/png" href="{{ page.favicon }}"> <link rel="icon" type="image/png" href="{{{ page.favicon }}}">
<link rel="mask-icon" href="{{ page.tabicon.icon }}" color="{{ page.tabicon.colour }}"> <link rel="mask-icon" href="{{{ page.tabicon.icon }}}" color="{{ page.tabicon.colour }}">
<link rel="stylesheet" href="vendor/jquery/css/base/jquery-ui.min.css?v={{ page.version }}"> <link rel="stylesheet" href="vendor/jquery/css/base/jquery-ui.min.css?v={{ cacheBuster }}">
<link rel="stylesheet" href="vendor/font-awesome/css/font-awesome.min.css?v={{ page.version }}"> <link rel="stylesheet" href="vendor/font-awesome/css/font-awesome.min.css?v={{ cacheBuster }}">
<link rel="stylesheet" href="red/style.min.css?v={{ page.version }}"> <link rel="stylesheet" href="red/style.min.css?v={{ cacheBuster }}">
{{#page.css}} {{#page.css}}
<link rel="stylesheet" href="{{.}}"> <link rel="stylesheet" href="{{.}}">
{{/page.css}} {{/page.css}}
{{#asset.vendorMonaco}} {{#asset.vendorMonaco}}
<link rel="stylesheet" href="vendor/monaco/style.css?v={{ page.version }}"> <link rel="stylesheet" href="vendor/monaco/style.css?v={{ cacheBuster }}">
{{/asset.vendorMonaco}} {{/asset.vendorMonaco}}
</head> </head>
<body spellcheck="false"> <body spellcheck="false">
<div id="red-ui-editor"></div> <div id="red-ui-editor"></div>
<script src="vendor/vendor.js?v={{ page.version }}"></script> <script src="vendor/vendor.js?v={{ cacheBuster }}"></script>
{{#asset.vendorMonaco}} {{#asset.vendorMonaco}}
<script src="{{ asset.vendorMonaco }}?v={{ page.version }}"></script> <script src="{{{ asset.vendorMonaco }}}?v={{ cacheBuster }}"></script>
{{/asset.vendorMonaco}} {{/asset.vendorMonaco}}
<script src="{{ asset.red }}?v={{ page.version }}"></script> <script src="{{{ asset.red }}}?v={{ cacheBuster }}"></script>
<script src="{{ asset.main }}?v={{ page.version }}"></script> <script src="{{{ asset.main }}}?v={{ cacheBuster }}"></script>
{{# page.scripts }} {{# page.scripts }}
<script src="{{.}}"></script> <script src="{{.}}"></script>
{{/ page.scripts }} {{/ page.scripts }}

View File

@ -320,7 +320,7 @@
} }
// but replace with repeat one if set to repeat // but replace with repeat one if set to repeat
if ((this.repeat && this.repeat != 0) || this.crontab) { if ((this.repeat && this.repeat != 0) || this.crontab) {
suffix = " ↻"; suffix = "\t↻";
} }
if (this.name) { if (this.name) {
return this.name+suffix; return this.name+suffix;

View File

@ -109,9 +109,8 @@ module.exports = function(RED) {
} }
const p = props.shift() const p = props.shift()
const property = p.p; const property = p.p;
const value = p.v ? p.v : ''; const value = p.v !== undefined ? p.v : '';
const valueType = p.vt ? p.vt : 'str'; const valueType = p.vt !== undefined ? p.vt : 'str';
if (property) { if (property) {
if (valueType === "jsonata") { if (valueType === "jsonata") {
if (p.v) { if (p.v) {

View File

@ -86,7 +86,7 @@
}, },
label: function() { label: function() {
var suffix = ""; var suffix = "";
if (this.console === true || this.console === "true") { suffix = " ⇲"; } if (this.console === true || this.console === "true") { suffix = "\t⇲"; }
if (this.targetType === "jsonata") { if (this.targetType === "jsonata") {
return (this.name || "JSONata") + suffix; return (this.name || "JSONata") + suffix;
} }
@ -195,6 +195,119 @@
node.dirty = true; node.dirty = true;
}); });
RED.view.redraw(); RED.view.redraw();
},
requestDebugNodeList: function(filteredNodes) {
var workspaceOrder = RED.nodes.getWorkspaceOrder();
var workspaceOrderMap = {};
workspaceOrder.forEach(function(ws,i) {
workspaceOrderMap[ws] = i;
});
var candidateNodes = [];
var candidateSFs = [];
var subflows = {};
RED.nodes.eachNode(function (n) {
var nt = n.type;
if (nt === "debug") {
if (n.z in workspaceOrderMap) {
candidateNodes.push(n);
}
else {
var sf = RED.nodes.subflow(n.z);
if (sf) {
subflows[sf.id] = {
debug: true,
subflows: {}
};
}
}
}
else if(nt.substring(0, 8) === "subflow:") {
if (n.z in workspaceOrderMap) {
candidateSFs.push(n);
}
else {
var psf = RED.nodes.subflow(n.z);
if (psf) {
var sid = nt.substring(8);
var item = subflows[psf.id];
if (!item) {
item = {
debug: undefined,
subflows: {}
};
subflows[psf.id] = item;
}
item.subflows[sid] = true;
}
}
}
});
candidateSFs.forEach(function (sf) {
var sid = sf.type.substring(8);
if (containsDebug(sid, subflows)) {
candidateNodes.push(sf);
}
});
candidateNodes.sort(function(A,B) {
var wsA = workspaceOrderMap[A.z];
var wsB = workspaceOrderMap[B.z];
if (wsA !== wsB) {
return wsA-wsB;
}
var labelA = RED.utils.getNodeLabel(A,A.id);
var labelB = RED.utils.getNodeLabel(B,B.id);
return labelA.localeCompare(labelB);
});
var currentWs = null;
var data = [];
var currentFlow;
var currentSelectedCount = 0;
candidateNodes.forEach(function(node) {
if (currentWs !== node.z) {
if (currentFlow && currentFlow.checkbox) {
currentFlow.selected = currentSelectedCount === currentFlow.children.length
}
currentSelectedCount = 0;
currentWs = node.z;
var parent = RED.nodes.workspace(currentWs) || RED.nodes.subflow(currentWs);
currentFlow = {
label: RED.utils.getNodeLabel(parent, currentWs),
}
if (!parent.disabled) {
currentFlow.children = [];
currentFlow.checkbox = true;
} else {
currentFlow.class = "disabled"
}
data.push(currentFlow);
}
if (currentFlow.children) {
if (!filteredNodes[node.id]) {
currentSelectedCount++;
}
currentFlow.children.push({
label: RED.utils.getNodeLabel(node,node.id),
node: {
id: node.id
},
checkbox: true,
selected: !filteredNodes[node.id]
});
}
});
if (currentFlow && currentFlow.checkbox) {
currentFlow.selected = currentSelectedCount === currentFlow.children.length
}
if (subWindow) {
try {
subWindow.postMessage({event:"refreshDebugNodeList", nodes:data},"*");
} catch(err) {
console.log(err);
}
}
RED.debug.refreshDebugNodeList(data)
} }
}; };
@ -396,6 +509,26 @@
} }
} }
function containsDebug(sid, map) {
var item = map[sid];
if (item) {
if (item.debug === undefined) {
var sfs = Object.keys(item.subflows);
var contain = false;
for (var i = 0; i < sfs.length; i++) {
var sf = sfs[i];
if (containsDebug(sf, map)) {
contain = true;
break;
}
}
item.debug = contain;
}
return item.debug;
}
return false;
}
$("#red-ui-sidebar-debug-open").on("click", function(e) { $("#red-ui-sidebar-debug-open").on("click", function(e) {
e.preventDefault(); e.preventDefault();
subWindow = window.open(document.location.toString().replace(/[?#].*$/,"")+"debug/view/view.html"+document.location.search,"nodeREDDebugView","menubar=no,location=no,toolbar=no,chrome,height=500,width=600"); subWindow = window.open(document.location.toString().replace(/[?#].*$/,"")+"debug/view/view.html"+document.location.search,"nodeREDDebugView","menubar=no,location=no,toolbar=no,chrome,height=500,width=600");
@ -427,6 +560,8 @@
options.messageSourceClick(msg.id,msg._alias,msg.path); options.messageSourceClick(msg.id,msg._alias,msg.path);
} else if (msg.event === "clear") { } else if (msg.event === "clear") {
options.clear(); options.clear();
} else if (msg.event === "requestDebugNodeList") {
options.requestDebugNodeList(msg.filteredNodes)
} }
}; };
window.addEventListener('message',this.handleWindowMessage); window.addEventListener('message',this.handleWindowMessage);

View File

@ -5,6 +5,7 @@ module.exports = function(RED) {
const fs = require("fs-extra"); const fs = require("fs-extra");
const path = require("path"); const path = require("path");
var debuglength = RED.settings.debugMaxLength || 1000; var debuglength = RED.settings.debugMaxLength || 1000;
var statuslength = RED.settings.debugStatusLength || 32;
var useColors = RED.settings.debugUseColors || false; var useColors = RED.settings.debugUseColors || false;
util.inspect.styles.boolean = "red"; util.inspect.styles.boolean = "red";
const { hasOwnProperty } = Object.prototype; const { hasOwnProperty } = Object.prototype;
@ -164,7 +165,7 @@ module.exports = function(RED) {
} }
} }
if (st.length > 32) { st = st.substr(0,32) + "..."; } if (st.length > statuslength) { st = st.substr(0,statuslength) + "..."; }
var newStatus = {fill:fill, shape:shape, text:st}; var newStatus = {fill:fill, shape:shape, text:st};
if (JSON.stringify(newStatus) !== node.oldState) { // only send if we have to if (JSON.stringify(newStatus) !== node.oldState) { // only send if we have to

View File

@ -275,7 +275,7 @@
value: [], value: [],
type: "link in[]", type: "link in[]",
validate: function (v, opt) { validate: function (v, opt) {
if ((this.linkType === "static" && v.length > 0) if (((this.linkType || "static") === "static" && v.length > 0)
|| this.linkType === "dynamic") { || this.linkType === "dynamic") {
return true; return true;
} }

View File

@ -167,19 +167,13 @@ RED.debug = (function() {
var menu = RED.popover.menu({ var menu = RED.popover.menu({
options: options, options: options,
onselect: function(item) { onselect: function(item) {
if (item.value !== filterType) { setFilterType(item.value)
filterType = item.value;
$('#red-ui-sidebar-debug-filter span').text(RED._('node-red:debug.sidebar.'+filterType));
refreshMessageList();
RED.settings.set("debug.filter",filterType)
}
if (filterType === 'filterSelected') { if (filterType === 'filterSelected') {
refreshDebugNodeList(); config.requestDebugNodeList(filteredNodes);
filterDialog.slideDown(200); filterDialog.slideDown(200);
filterDialogShown = true; filterDialogShown = true;
debugNodeTreeList.focus(); debugNodeTreeList.focus();
} }
} }
}); });
menu.show({ menu.show({
@ -254,131 +248,7 @@ RED.debug = (function() {
} }
function refreshDebugNodeList(data) {
function containsDebug(sid, map) {
var item = map[sid];
if (item) {
if (item.debug === undefined) {
var sfs = Object.keys(item.subflows);
var contain = false;
for (var i = 0; i < sfs.length; i++) {
var sf = sfs[i];
if (containsDebug(sf, map)) {
contain = true;
break;
}
}
item.debug = contain;
}
return item.debug;
}
return false;
}
function refreshDebugNodeList() {
var workspaceOrder = RED.nodes.getWorkspaceOrder();
var workspaceOrderMap = {};
workspaceOrder.forEach(function(ws,i) {
workspaceOrderMap[ws] = i;
});
var candidateNodes = [];
var candidateSFs = [];
var subflows = {};
RED.nodes.eachNode(function (n) {
var nt = n.type;
if (nt === "debug") {
if (n.z in workspaceOrderMap) {
candidateNodes.push(n);
}
else {
var sf = RED.nodes.subflow(n.z);
if (sf) {
subflows[sf.id] = {
debug: true,
subflows: {}
};
}
}
}
else if(nt.substring(0, 8) === "subflow:") {
if (n.z in workspaceOrderMap) {
candidateSFs.push(n);
}
else {
var psf = RED.nodes.subflow(n.z);
if (psf) {
var sid = nt.substring(8);
var item = subflows[psf.id];
if (!item) {
item = {
debug: undefined,
subflows: {}
};
subflows[psf.id] = item;
}
item.subflows[sid] = true;
}
}
}
});
candidateSFs.forEach(function (sf) {
var sid = sf.type.substring(8);
if (containsDebug(sid, subflows)) {
candidateNodes.push(sf);
}
});
candidateNodes.sort(function(A,B) {
var wsA = workspaceOrderMap[A.z];
var wsB = workspaceOrderMap[B.z];
if (wsA !== wsB) {
return wsA-wsB;
}
var labelA = RED.utils.getNodeLabel(A,A.id);
var labelB = RED.utils.getNodeLabel(B,B.id);
return labelA.localeCompare(labelB);
});
var currentWs = null;
var data = [];
var currentFlow;
var currentSelectedCount = 0;
candidateNodes.forEach(function(node) {
if (currentWs !== node.z) {
if (currentFlow && currentFlow.checkbox) {
currentFlow.selected = currentSelectedCount === currentFlow.children.length
}
currentSelectedCount = 0;
currentWs = node.z;
var parent = RED.nodes.workspace(currentWs) || RED.nodes.subflow(currentWs);
currentFlow = {
label: RED.utils.getNodeLabel(parent, currentWs),
}
if (!parent.disabled) {
currentFlow.children = [];
currentFlow.checkbox = true;
} else {
currentFlow.class = "disabled"
}
data.push(currentFlow);
}
if (currentFlow.children) {
if (!filteredNodes[node.id]) {
currentSelectedCount++;
}
currentFlow.children.push({
label: RED.utils.getNodeLabel(node,node.id),
node: node,
checkbox: true,
selected: !filteredNodes[node.id]
});
}
});
if (currentFlow && currentFlow.checkbox) {
currentFlow.selected = currentSelectedCount === currentFlow.children.length
}
debugNodeTreeList.treeList("data", data); debugNodeTreeList.treeList("data", data);
} }
@ -401,7 +271,7 @@ RED.debug = (function() {
},200); },200);
} }
function _refreshMessageList(_activeWorkspace) { function _refreshMessageList(_activeWorkspace) {
if (_activeWorkspace) { if (typeof _activeWorkspace === 'string') {
activeWorkspace = _activeWorkspace.replace(/\./g,"_"); activeWorkspace = _activeWorkspace.replace(/\./g,"_");
} }
if (filterType === "filterAll") { if (filterType === "filterAll") {
@ -479,12 +349,12 @@ RED.debug = (function() {
filteredNodes[n.id] = true; filteredNodes[n.id] = true;
}); });
delete filteredNodes[sourceId]; delete filteredNodes[sourceId];
$("#red-ui-sidebar-debug-filterSelected").trigger("click");
RED.settings.set('debug.filteredNodes',Object.keys(filteredNodes)) RED.settings.set('debug.filteredNodes',Object.keys(filteredNodes))
setFilterType('filterSelected')
refreshMessageList(); refreshMessageList();
}}, }},
{id:"red-ui-debug-msg-menu-item-clear-filter",label:RED._("node-red:debug.messageMenu.clearFilter"),onselect:function(){ {id:"red-ui-debug-msg-menu-item-clear-filter",label:RED._("node-red:debug.messageMenu.clearFilter"),onselect:function(){
$("#red-ui-sidebar-debug-filterAll").trigger("click"); clearFilterSettings()
refreshMessageList(); refreshMessageList();
}} }}
); );
@ -713,9 +583,17 @@ RED.debug = (function() {
if (!!clearFilter) { if (!!clearFilter) {
clearFilterSettings(); clearFilterSettings();
} }
refreshDebugNodeList(); config.requestDebugNodeList(filteredNodes);
} }
function setFilterType(type) {
if (type !== filterType) {
filterType = type;
$('#red-ui-sidebar-debug-filter span').text(RED._('node-red:debug.sidebar.'+filterType));
refreshMessageList();
RED.settings.set("debug.filter",filterType)
}
}
function clearFilterSettings() { function clearFilterSettings() {
filteredNodes = {}; filteredNodes = {};
filterType = 'filterAll'; filterType = 'filterAll';
@ -728,6 +606,7 @@ RED.debug = (function() {
init: init, init: init,
refreshMessageList:refreshMessageList, refreshMessageList:refreshMessageList,
handleDebugMessage: handleDebugMessage, handleDebugMessage: handleDebugMessage,
clearMessageList: clearMessageList clearMessageList: clearMessageList,
refreshDebugNodeList: refreshDebugNodeList
} }
})(); })();

View File

@ -12,6 +12,9 @@ $(function() {
}, },
clear: function() { clear: function() {
window.opener.postMessage({event:"clear"},'*'); window.opener.postMessage({event:"clear"},'*');
},
requestDebugNodeList: function(filteredNodes) {
window.opener.postMessage({event: 'requestDebugNodeList', filteredNodes},'*')
} }
} }
@ -26,6 +29,8 @@ $(function() {
RED.debug.refreshMessageList(evt.data.activeWorkspace); RED.debug.refreshMessageList(evt.data.activeWorkspace);
} else if (evt.data.event === "projectChange") { } else if (evt.data.event === "projectChange") {
RED.debug.clearMessageList(true); RED.debug.clearMessageList(true);
} else if (evt.data.event === "refreshDebugNodeList") {
RED.debug.refreshDebugNodeList(evt.data.nodes)
} }
},false); },false);
} catch(err) { } catch(err) {

View File

@ -315,7 +315,7 @@ module.exports = function(RED) {
var spec = module.module; var spec = module.module;
if (spec && (spec !== "")) { if (spec && (spec !== "")) {
moduleLoadPromises.push(RED.import(module.module).then(lib => { moduleLoadPromises.push(RED.import(module.module).then(lib => {
sandbox[vname] = lib.default; sandbox[vname] = lib.default || lib;
}).catch(err => { }).catch(err => {
node.error(RED._("function.error.moduleLoadError",{module:module.spec, error:err.toString()})) node.error(RED._("function.error.moduleLoadError",{module:module.spec, error:err.toString()}))
throw err; throw err;

View File

@ -103,7 +103,6 @@
} else if (type === "istype") { } else if (type === "istype") {
r.v = rule.find(".node-input-rule-type-value").typedInput('type'); r.v = rule.find(".node-input-rule-type-value").typedInput('type');
r.vt = rule.find(".node-input-rule-type-value").typedInput('type'); r.vt = rule.find(".node-input-rule-type-value").typedInput('type');
r.vt = (r.vt === "number") ? "num" : "str";
} else if (type === "jsonata_exp") { } else if (type === "jsonata_exp") {
r.v = rule.find(".node-input-rule-exp-value").typedInput('value'); r.v = rule.find(".node-input-rule-exp-value").typedInput('value');
r.vt = rule.find(".node-input-rule-exp-value").typedInput('type'); r.vt = rule.find(".node-input-rule-exp-value").typedInput('type');
@ -168,7 +167,35 @@
label:RED._("node-red:common.label.payload"), label:RED._("node-red:common.label.payload"),
validate: RED.validators.typedInput("propertyType", false)}, validate: RED.validators.typedInput("propertyType", false)},
propertyType: { value:"msg" }, propertyType: { value:"msg" },
rules: {value:[{t:"eq", v:"", vt:"str"}]}, rules: {
value:[{t:"eq", v:"", vt:"str"}],
validate: function (rules, opt) {
let msg;
const errors = []
if (!rules || rules.length === 0) { return true }
for (var i=0;i<rules.length;i++) {
const opt = { label: RED._('node-red:switch.label.rule')+' '+(i+1) }
const r = rules[i];
if (r.t !== 'istype') {
if (r.hasOwnProperty('v')) {
if ((msg = RED.utils.validateTypedProperty(r.v,r.vt,opt)) !== true) {
errors.push(msg)
}
}
if (r.hasOwnProperty('v2')) {
if ((msg = RED.utils.validateTypedProperty(r.v2,r.v2t,opt)) !== true) {
errors.push(msg)
}
}
}
}
if (errors.length) {
console.log(errors)
return errors
}
return true;
}
},
checkall: {value:"true", required:true}, checkall: {value:"true", required:true},
repair: {value:false}, repair: {value:false},
outputs: {value:1} outputs: {value:1}
@ -218,7 +245,11 @@
if (i > 0) { if (i > 0) {
var lastRule = $("#node-input-rule-container").editableList('getItemAt',i-1); var lastRule = $("#node-input-rule-container").editableList('getItemAt',i-1);
var exportedRule = exportRule(lastRule.element); var exportedRule = exportRule(lastRule.element);
opt.r.vt = exportedRule.vt; if (exportedRule.t === "istype") {
opt.r.vt = (exportedRule.vt === "number") ? "num" : "str";
} else {
opt.r.vt = exportedRule.vt;
}
opt.r.v = ""; opt.r.v = "";
// We could copy the value over as well and preselect it (see the 'activeElement' code below) // We could copy the value over as well and preselect it (see the 'activeElement' code below)
// But not sure that feels right. Is copying over the last value 'expected' behaviour? // But not sure that feels right. Is copying over the last value 'expected' behaviour?

View File

@ -19,71 +19,42 @@
<script type="text/javascript"> <script type="text/javascript">
(function() { (function() {
function isInvalidProperty(v,vt) {
if (/msg|flow|global/.test(vt)) {
if (!RED.utils.validatePropertyExpression(v)) {
return RED._("node-red:change.errors.invalid-prop", {
property: v
});
}
} else if (vt === "jsonata") {
try{ jsonata(v); } catch(e) {
return RED._("node-red:change.errors.invalid-expr", {
error: e.message
});
}
} else if (vt === "json") {
try{ JSON.parse(v); } catch(e) {
return RED._("node-red:change.errors.invalid-json-data", {
error: e.message
});
}
}
return false;
}
RED.nodes.registerType('change', { RED.nodes.registerType('change', {
color: "#E2D96E", color: "#E2D96E",
category: 'function', category: 'function',
defaults: { defaults: {
name: {value:""}, name: {value:""},
rules:{value:[{t:"set",p:"payload",pt:"msg",to:"",tot:"str"}],validate: function(rules, opt) { rules:{
var msg; value:[{t:"set",p:"payload",pt:"msg",to:"",tot:"str"}],
if (!rules || rules.length === 0) { return true } validate: function(rules, opt) {
for (var i=0;i<rules.length;i++) { let msg;
var r = rules[i]; const errors = []
if (r.t === 'set') { if (!rules || rules.length === 0) { return true }
if (msg = isInvalidProperty(r.p,r.pt)) { for (var i=0;i<rules.length;i++) {
return msg; const opt = { label: RED._('node-red:change.label.rule')+' '+(i+1) }
const r = rules[i];
if (r.t === 'set' || r.t === 'change' || r.t === 'delete' || r.t === 'move') {
if ((msg = RED.utils.validateTypedProperty(r.p,r.pt,opt)) !== true) {
errors.push(msg)
}
} }
if (msg = isInvalidProperty(r.to,r.tot)) { if (r.t === 'set' || r.t === 'change' || r.t === 'move') {
return msg; if ((msg = RED.utils.validateTypedProperty(r.to,r.tot,opt)) !== true) {
errors.push(msg)
}
} }
} else if (r.t === 'change') { if (r.t === 'change') {
if (msg = isInvalidProperty(r.p,r.pt)) { if ((msg = RED.utils.validateTypedProperty(r.from,r.fromt,opt)) !== true) {
return msg; errors.push(msg)
} }
if(msg = isInvalidProperty(r.from,r.fromt)) {
return msg;
}
if(msg = isInvalidProperty(r.to,r.tot)) {
return msg;
}
} else if (r.t === 'delete') {
if (msg = isInvalidProperty(r.p,r.pt)) {
return msg;
}
} else if (r.t === 'move') {
if (msg = isInvalidProperty(r.p,r.pt)) {
return msg;
}
if (msg = isInvalidProperty(r.to,r.tot)) {
return msg;
} }
} }
if (errors.length) {
return errors
}
return true;
} }
return true; },
}},
// legacy // legacy
action: {value:""}, action: {value:""},
property: {value:""}, property: {value:""},

View File

@ -117,7 +117,7 @@ module.exports = function(RED) {
}); });
return return
} else if (rule.tot === 'date') { } else if (rule.tot === 'date') {
value = Date.now(); value = RED.util.evaluateNodeProperty(rule.to, rule.tot, node)
} else if (rule.tot === 'jsonata') { } else if (rule.tot === 'jsonata') {
RED.util.evaluateJSONataExpression(rule.to,msg, (err, value) => { RED.util.evaluateJSONataExpression(rule.to,msg, (err, value) => {
if (err) { if (err) {
@ -233,7 +233,9 @@ module.exports = function(RED) {
// only replace if they match exactly // only replace if they match exactly
RED.util.setMessageProperty(msg,property,value); RED.util.setMessageProperty(msg,property,value);
} else { } else {
current = current.replace(fromRE,value); // if target is boolean then just replace it
if (rule.tot === "bool") { current = value; }
else { current = current.replace(fromRE,value); }
RED.util.setMessageProperty(msg,property,current); RED.util.setMessageProperty(msg,property,current);
} }
} else if ((typeof current === 'number' || current instanceof Number) && fromType === 'num') { } else if ((typeof current === 'number' || current instanceof Number) && fromType === 'num') {

View File

@ -57,7 +57,9 @@
action: {value:"scale"}, action: {value:"scale"},
round: {value:false}, round: {value:false},
property: {value:"payload",required:true, property: {value:"payload",required:true,
label:RED._("node-red:common.label.property")}, label:RED._("node-red:common.label.property"),
validate: RED.validators.typedInput({ type: 'msg', allowBlank: true })
},
name: {value:""} name: {value:""}
}, },
inputs: 1, inputs: 1,

View File

@ -153,7 +153,7 @@
} }
var editorRow = $("#dialog-form>div.node-text-editor-row"); var editorRow = $("#dialog-form>div.node-text-editor-row");
height -= (parseInt(editorRow.css("marginTop"))+parseInt(editorRow.css("marginBottom"))); height -= (parseInt(editorRow.css("marginTop"))+parseInt(editorRow.css("marginBottom")));
$(".node-text-editor").css("height",height+"px"); $("#dialog-form .node-text-editor").css("height",height+"px");
this.editor.resize(); this.editor.resize();
} }
}); });

View File

@ -284,7 +284,7 @@ module.exports = function(RED) {
done(); done();
} }
} }
else { else if (!msg.hasOwnProperty("reset")) {
if (maxKeptMsgsCount(node) > 0) { if (maxKeptMsgsCount(node) > 0) {
if (node.intervalID === -1) { if (node.intervalID === -1) {
node.send(msg); node.send(msg);

View File

@ -56,7 +56,7 @@
color:"darksalmon", color:"darksalmon",
defaults: { defaults: {
command: {value:""}, command: {value:""},
addpay: {value:""}, addpay: {value:"", validate: RED.validators.typedInput({ type: 'msg', allowBlank: true })},
append: {value:""}, append: {value:""},
useSpawn: {value:"false"}, useSpawn: {value:"false"},
timer: {value:""}, timer: {value:""},

View File

@ -56,9 +56,11 @@
inout: {value:"out"}, inout: {value:"out"},
septopics: {value:true}, septopics: {value:true},
property: {value:"payload", required:true, property: {value:"payload", required:true,
label:RED._("node-red:rbe.label.property")}, label:RED._("node-red:rbe.label.property"),
validate: RED.validators.typedInput({ type: 'msg', allowUndefined: true })},
topi: {value:"topic", required:true, topi: {value:"topic", required:true,
label:RED._("node-red:rbe.label.topic")} label:RED._("node-red:rbe.label.topic"),
validate: RED.validators.typedInput({ type: 'msg', allowUndefined: true })}
}, },
inputs:1, inputs:1,
outputs:1, outputs:1,

View File

@ -104,6 +104,7 @@ module.exports = function(RED) {
* @returns `true` if it is a valid topic * @returns `true` if it is a valid topic
*/ */
function isValidPublishTopic(topic) { function isValidPublishTopic(topic) {
if (topic.length === 0) return false;
return !/[\+#\b\f\n\r\t\v\0]/.test(topic); return !/[\+#\b\f\n\r\t\v\0]/.test(topic);
} }
@ -219,8 +220,8 @@ module.exports = function(RED) {
*/ */
function subscriptionHandler(node, datatype ,topic, payload, packet) { function subscriptionHandler(node, datatype ,topic, payload, packet) {
const msg = {topic:topic, payload:null, qos:packet.qos, retain:packet.retain}; const msg = {topic:topic, payload:null, qos:packet.qos, retain:packet.retain};
const v5 = (node && node.brokerConn) const v5 = (node && node.brokerConn)
? node.brokerConn.v5() ? node.brokerConn.v5()
: Object.prototype.hasOwnProperty.call(packet, "properties"); : Object.prototype.hasOwnProperty.call(packet, "properties");
if(v5 && packet.properties) { if(v5 && packet.properties) {
setStrProp(packet.properties, msg, "responseTopic"); setStrProp(packet.properties, msg, "responseTopic");
@ -451,7 +452,7 @@ module.exports = function(RED) {
/** /**
* Perform the disconnect action * Perform the disconnect action
* @param {MQTTInNode|MQTTOutNode} node * @param {MQTTInNode|MQTTOutNode} node
* @param {Function} done * @param {Function} done
*/ */
function handleDisconnectAction(node, done) { function handleDisconnectAction(node, done) {
@ -611,7 +612,7 @@ module.exports = function(RED) {
node.brokerurl = node.url; node.brokerurl = node.url;
} else { } else {
// if the broker is ws:// or wss:// or tcp:// // if the broker is ws:// or wss:// or tcp://
if (node.broker.indexOf("://") > -1) { if ((typeof node.broker === 'string') && node.broker.indexOf("://") > -1) {
node.brokerurl = node.broker; node.brokerurl = node.broker;
// Only for ws or wss, check if proxy env var for additional configuration // Only for ws or wss, check if proxy env var for additional configuration
if (node.brokerurl.indexOf("wss://") > -1 || node.brokerurl.indexOf("ws://") > -1) { if (node.brokerurl.indexOf("wss://") > -1 || node.brokerurl.indexOf("ws://") > -1) {
@ -865,7 +866,7 @@ module.exports = function(RED) {
* Call end and wait for the client to end (or timeout) * Call end and wait for the client to end (or timeout)
* @param {mqtt.MqttClient} client The broker client * @param {mqtt.MqttClient} client The broker client
* @param {number} ms The time to wait for the client to end * @param {number} ms The time to wait for the client to end
* @returns * @returns
*/ */
let waitEnd = (client, ms) => { let waitEnd = (client, ms) => {
return new Promise( (resolve, reject) => { return new Promise( (resolve, reject) => {
@ -905,7 +906,7 @@ module.exports = function(RED) {
node.subid = 1; node.subid = 1;
//typedef for subscription object: //typedef for subscription object:
/** /**
* @typedef {Object} Subscription * @typedef {Object} Subscription
* @property {String} topic - topic to subscribe to * @property {String} topic - topic to subscribe to
* @property {Object} [options] - options object * @property {Object} [options] - options object
@ -933,7 +934,7 @@ module.exports = function(RED) {
const ref = _ref || 0; const ref = _ref || 0;
let options let options
let qos = 1 // default to QoS 1 (AWS and several other brokers don't support QoS 2) let qos = 1 // default to QoS 1 (AWS and several other brokers don't support QoS 2)
// if options is an object, then clone it // if options is an object, then clone it
if (typeof _options == "object") { if (typeof _options == "object") {
options = RED.util.cloneMessage(_options || {}) options = RED.util.cloneMessage(_options || {})
@ -947,7 +948,7 @@ module.exports = function(RED) {
if (typeof qos === "number" && qos >= 0 && qos <= 2) { if (typeof qos === "number" && qos >= 0 && qos <= 2) {
options.qos = qos; options.qos = qos;
} }
subscription.topic = _topic; subscription.topic = _topic;
subscription.qos = qos; subscription.qos = qos;
subscription.options = RED.util.cloneMessage(options); subscription.options = RED.util.cloneMessage(options);
@ -957,16 +958,16 @@ module.exports = function(RED) {
} }
/** /**
* If topic is a subscription object, then use that, otherwise look up the topic in * If topic is a subscription object, then use that, otherwise look up the topic in
* the subscriptions object. If the topic is not found, then create a new subscription * the subscriptions object. If the topic is not found, then create a new subscription
* object and add it to the subscriptions object. * object and add it to the subscriptions object.
* @param {Subscription|String} topic * @param {Subscription|String} topic
* @param {*} options * @param {*} options
* @param {*} callback * @param {*} callback
* @param {*} ref * @param {*} ref
*/ */
node.subscribe = function (topic, options, callback, ref) { node.subscribe = function (topic, options, callback, ref) {
/** @type {Subscription} */ /** @type {Subscription} */
let subscription let subscription
let doCompare = false let doCompare = false
let changesFound = false let changesFound = false
@ -1004,7 +1005,7 @@ module.exports = function(RED) {
_brokerConn.unsubscribe(sub.topic, sub.ref, true) _brokerConn.unsubscribe(sub.topic, sub.ref, true)
} }
}) })
// if subscription is found (or sent in as a parameter), then check for changes. // if subscription is found (or sent in as a parameter), then check for changes.
// if there are any changes requested, tidy up the old subscription // if there are any changes requested, tidy up the old subscription
if (subscription) { if (subscription) {
@ -1091,7 +1092,7 @@ module.exports = function(RED) {
delete sub[ref] delete sub[ref]
} }
} }
// if instructed to remove the actual MQTT client subscription // if instructed to remove the actual MQTT client subscription
if (unsub) { if (unsub) {
// if there are no more subscriptions for the topic, then remove the topic // if there are no more subscriptions for the topic, then remove the topic
if (Object.keys(sub).length === 0) { if (Object.keys(sub).length === 0) {

View File

@ -141,15 +141,7 @@ in your Node-RED user directory (${RED.settings.userDir}).
}); });
} }
} }
/**
* @param {Object} headersObject
* @param {string} name
* @return {any} value
*/
const getHeaderValue = (headersObject, name) => {
const asLowercase = name.toLowercase();
return headersObject[Object.keys(headersObject).find(k => k.toLowerCase() === asLowercase)];
}
this.on("input",function(msg,nodeSend,nodeDone) { this.on("input",function(msg,nodeSend,nodeDone) {
checkNodeAgentPatch(); checkNodeAgentPatch();
//reset redirectList on each request //reset redirectList on each request
@ -300,7 +292,7 @@ in your Node-RED user directory (${RED.settings.userDir}).
} }
opts.headers = {}; opts.headers = {};
//add msg.headers //add msg.headers
//NOTE: ui headers will take precidence over msg.headers //NOTE: ui headers will take precidence over msg.headers
if (msg.headers) { if (msg.headers) {
if (msg.headers.hasOwnProperty('x-node-red-request-node')) { if (msg.headers.hasOwnProperty('x-node-red-request-node')) {
@ -452,10 +444,6 @@ in your Node-RED user directory (${RED.settings.userDir}).
formData.append(opt, val); formData.append(opt, val);
} else if (typeof val === 'object' && val.hasOwnProperty('value')) { } else if (typeof val === 'object' && val.hasOwnProperty('value')) {
formData.append(opt,val.value,val.options || {}); formData.append(opt,val.value,val.options || {});
} else if (Array.isArray(val)) {
for (var i=0; i<val.length; i++) {
formData.append(opt, val[i])
}
} else { } else {
formData.append(opt,JSON.stringify(val)); formData.append(opt,JSON.stringify(val));
} }
@ -637,7 +625,7 @@ in your Node-RED user directory (${RED.settings.userDir}).
msg.payload = msg.payload.toString('utf8'); // txt msg.payload = msg.payload.toString('utf8'); // txt
if (node.ret === "obj") { if (node.ret === "obj") {
if (msg.statusCode == 204){msg.payload= "{}"}; if (msg.statusCode == 204){msg.payload= "{}"};
try { msg.payload = JSON.parse(msg.payload); } // obj try { msg.payload = JSON.parse(msg.payload); } // obj
catch(e) { node.warn(RED._("httpin.errors.json-error")); } catch(e) { node.warn(RED._("httpin.errors.json-error")); }
} }
@ -744,7 +732,7 @@ in your Node-RED user directory (${RED.settings.userDir}).
* *
* If the algorithm directive's value ends with "-sess", then HA1 is * If the algorithm directive's value ends with "-sess", then HA1 is
* HA1=digestCompute(digestCompute(username:realm:password):nonce:cnonce) * HA1=digestCompute(digestCompute(username:realm:password):nonce:cnonce)
* *
* If the algorithm directive's value does not end with "-sess", then HA1 is * If the algorithm directive's value does not end with "-sess", then HA1 is
* HA1=digestCompute(username:realm:password) * HA1=digestCompute(username:realm:password)
*/ */

View File

@ -411,23 +411,33 @@ module.exports = function(RED) {
if (msg._session && msg._session.type == "tcp") { if (msg._session && msg._session.type == "tcp") {
var client = connectionPool[msg._session.id]; var client = connectionPool[msg._session.id];
if (client) { if (client) {
if (Buffer.isBuffer(msg.payload)) { if (msg?.reset === true) {
client.write(msg.payload); client.destroy();
} else if (typeof msg.payload === "string" && node.base64) { }
client.write(Buffer.from(msg.payload,'base64')); else {
} else { if (Buffer.isBuffer(msg.payload)) {
client.write(Buffer.from(""+msg.payload)); client.write(msg.payload);
} else if (typeof msg.payload === "string" && node.base64) {
client.write(Buffer.from(msg.payload,'base64'));
} else {
client.write(Buffer.from(""+msg.payload));
}
} }
} }
} }
else { else {
for (var i in connectionPool) { for (var i in connectionPool) {
if (Buffer.isBuffer(msg.payload)) { if (msg?.reset === true) {
connectionPool[i].write(msg.payload); connectionPool[i].destroy();
} else if (typeof msg.payload === "string" && node.base64) { }
connectionPool[i].write(Buffer.from(msg.payload,'base64')); else {
} else { if (Buffer.isBuffer(msg.payload)) {
connectionPool[i].write(Buffer.from(""+msg.payload)); connectionPool[i].write(msg.payload);
} else if (typeof msg.payload === "string" && node.base64) {
connectionPool[i].write(Buffer.from(msg.payload,'base64'));
} else {
connectionPool[i].write(Buffer.from(""+msg.payload));
}
} }
} }
} }
@ -547,13 +557,33 @@ module.exports = function(RED) {
this.on("input", function(msg, nodeSend, nodeDone) { this.on("input", function(msg, nodeSend, nodeDone) {
var i = 0; var i = 0;
if ((!Buffer.isBuffer(msg.payload)) && (typeof msg.payload !== "string")) { if (msg.payload !== undefined && (!Buffer.isBuffer(msg.payload)) && (typeof msg.payload !== "string")) {
msg.payload = msg.payload.toString(); msg.payload = msg.payload.toString();
} }
var host = node.server || msg.host; var host = node.server || msg.host;
var port = node.port || msg.port; var port = node.port || msg.port;
if (node.out === "sit" && msg?.reset) {
if (msg.reset === true) { // kill all connections
for (var cl in clients) {
if (clients[cl].hasOwnProperty("client")) {
clients[cl].client.destroy();
delete clients[cl];
}
}
}
if (typeof(msg.reset) === "string" && msg.reset.includes(":")) { // just kill connection host:port
if (clients.hasOwnProperty(msg.reset) && clients[msg.reset].hasOwnProperty("client")) {
clients[msg.reset].client.destroy();
delete clients[msg.reset];
}
}
const cc = Object.keys(clients).length;
node.status({fill:"green",shape:cc===0?"ring":"dot",text:RED._("tcpin.status.connections",{count:cc})});
if ((host === undefined || port === undefined) && !msg.hasOwnProperty("payload")) { return; }
}
// Store client information independently // Store client information independently
// the clients object will have: // the clients object will have:
// clients[id].client, clients[id].msg, clients[id].timeout // clients[id].client, clients[id].msg, clients[id].timeout
@ -621,13 +651,16 @@ module.exports = function(RED) {
clients[connection_id].connecting = true; clients[connection_id].connecting = true;
clients[connection_id].client.connect(connOpts, function() { clients[connection_id].client.connect(connOpts, function() {
//node.log(RED._("tcpin.errors.client-connected")); //node.log(RED._("tcpin.errors.client-connected"));
node.status({fill:"green",shape:"dot",text:"common.status.connected"}); // node.status({fill:"green",shape:"dot",text:"common.status.connected"});
node.status({fill:"green",shape:"dot",text:RED._("tcpin.status.connections",{count:Object.keys(clients).length})});
if (clients[connection_id] && clients[connection_id].client) { if (clients[connection_id] && clients[connection_id].client) {
clients[connection_id].connected = true; clients[connection_id].connected = true;
clients[connection_id].connecting = false; clients[connection_id].connecting = false;
let event; let event;
while (event = dequeue(clients[connection_id].msgQueue)) { while (event = dequeue(clients[connection_id].msgQueue)) {
clients[connection_id].client.write(event.msg.payload); if (event.msg.payload !== undefined) {
clients[connection_id].client.write(event.msg.payload);
}
event.nodeDone(); event.nodeDone();
} }
if (node.out === "time" && node.splitc < 0) { if (node.out === "time" && node.splitc < 0) {
@ -823,7 +856,9 @@ module.exports = function(RED) {
else if (!clients[connection_id].connecting && clients[connection_id].connected) { else if (!clients[connection_id].connecting && clients[connection_id].connected) {
if (clients[connection_id] && clients[connection_id].client) { if (clients[connection_id] && clients[connection_id].client) {
let event = dequeue(clients[connection_id].msgQueue) let event = dequeue(clients[connection_id].msgQueue)
clients[connection_id].client.write(event.msg.payload); if (event.msg.payload !== undefined ) {
clients[connection_id].client.write(event.msg.payload);
}
event.nodeDone(); event.nodeDone();
} }
} }

View File

@ -17,7 +17,20 @@
</select> </select>
<input style="width:40px;" type="text" id="node-input-sep" pattern="."> <input style="width:40px;" type="text" id="node-input-sep" pattern=".">
</div> </div>
<div class="form-row">
<label><i class="fa fa-code"></i> <span data-i18n="csv.label.spec"></span></label>
<div style="display: inline-grid;width: 70%;">
<select style="width:100%" id="csv-option-spec">
<option value="rfc" data-i18n="csv.spec.rfc"></option>
<option value="" data-i18n="csv.spec.legacy"></option>
</select>
<div>
<div class="form-tips csv-lecacy-warning" data-i18n="node-red:csv.spec.legacy_warning"
style="width: calc(100% - 18px); margin-top: 4px; max-width: unset;">
</div>
</div>
</div>
</div>
<div class="form-row"> <div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="common.label.name"></span></label> <label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="common.label.name"></span></label>
<input type="text" id="node-input-name" data-i18n="[placeholder]common.label.name"> <input type="text" id="node-input-name" data-i18n="[placeholder]common.label.name">
@ -60,10 +73,10 @@
<div class="form-row" style="padding-left:20px;"> <div class="form-row" style="padding-left:20px;">
<label></label> <label></label>
<label style="width:auto; margin-right:10px;" for="node-input-ret"><span data-i18n="csv.label.newline"></span></label> <label style="width:auto; margin-right:10px;" for="node-input-ret"><span data-i18n="csv.label.newline"></span></label>
<select style="width:150px;" id="node-input-ret"> <select style="width:calc(70% - 108px);" id="node-input-ret">
<option value='\r\n' data-i18n="csv.newline.windows"></option>
<option value='\n' data-i18n="csv.newline.linux"></option> <option value='\n' data-i18n="csv.newline.linux"></option>
<option value='\r' data-i18n="csv.newline.mac"></option> <option value='\r' data-i18n="csv.newline.mac"></option>
<option value='\r\n' data-i18n="csv.newline.windows"></option>
</select> </select>
</div> </div>
</script> </script>
@ -75,6 +88,7 @@
color:"#DEBD5C", color:"#DEBD5C",
defaults: { defaults: {
name: {value:""}, name: {value:""},
spec: {value:"rfc"},
sep: { sep: {
value:',', required:true, value:',', required:true,
label:RED._("node-red:csv.label.separator"), label:RED._("node-red:csv.label.separator"),
@ -83,7 +97,7 @@
hdrin: {value:""}, hdrin: {value:""},
hdrout: {value:"none"}, hdrout: {value:"none"},
multi: {value:"one",required:true}, multi: {value:"one",required:true},
ret: {value:'\\n'}, ret: {value:'\\r\\n'}, // default to CRLF (RFC4180 Sec 2.1: "Each record is located on a separate line, delimited by a line break (CRLF)")
temp: {value:""}, temp: {value:""},
skip: {value:"0"}, skip: {value:"0"},
strings: {value:true}, strings: {value:true},
@ -123,6 +137,27 @@
$("#node-input-sep").hide(); $("#node-input-sep").hide();
} }
}); });
$("#csv-option-spec").on("change", function() {
if ($("#csv-option-spec").val() == "rfc") {
$(".form-tips.csv-lecacy-warning").hide();
} else {
$(".form-tips.csv-lecacy-warning").show();
}
});
// new nodes will have `spec` set to "rfc" (default), but existing nodes will either not have
// a spec value or it will be empty - we need to maintain the legacy behaviour for existing
// flows but default to rfc for new nodes
let spec = !this.spec ? "" : "rfc"
$("#csv-option-spec").val(spec).trigger("change")
},
oneditsave: function() {
const specFormVal = $("#csv-option-spec").val() || '' // empty === legacy
const spectNodeVal = this.spec || '' // empty === legacy, null/undefined means in-place node upgrade (keep as is)
if (specFormVal !== spectNodeVal) {
// only update the flow value if changed (avoid marking the node dirty unnecessarily)
this.spec = specFormVal
}
} }
}); });
</script> </script>

View File

@ -15,319 +15,674 @@
**/ **/
module.exports = function(RED) { module.exports = function(RED) {
const csv = require('./lib/csv')
"use strict"; "use strict";
function CSVNode(n) { function CSVNode(n) {
RED.nodes.createNode(this,n); RED.nodes.createNode(this,n)
this.template = (n.temp || ""); const node = this
this.sep = (n.sep || ',').replace(/\\t/g,"\t").replace(/\\n/g,"\n").replace(/\\r/g,"\r"); const RFC4180Mode = n.spec === 'rfc'
this.quo = '"'; const legacyMode = !RFC4180Mode
this.ret = (n.ret || "\n").replace(/\\n/g,"\n").replace(/\\r/g,"\r");
this.winflag = (this.ret === "\r\n");
this.lineend = "\n";
this.multi = n.multi || "one";
this.hdrin = n.hdrin || false;
this.hdrout = n.hdrout || "none";
this.goodtmpl = true;
this.skip = parseInt(n.skip || 0);
this.store = [];
this.parsestrings = n.strings;
this.include_empty_strings = n.include_empty_strings || false;
this.include_null_values = n.include_null_values || false;
if (this.parsestrings === undefined) { this.parsestrings = true; }
if (this.hdrout === false) { this.hdrout = "none"; }
if (this.hdrout === true) { this.hdrout = "all"; }
var tmpwarn = true;
var node = this;
var re = new RegExp(node.sep.replace(/[-[\]{}()*+!<=:?.\/\\^$|#\s,]/g,'\\$&') + '(?=(?:(?:[^"]*"){2})*[^"]*$)','g');
// pass in an array of column names to be trimmed, de-quoted and retrimmed node.status({}) // clear status
var clean = function(col,sep) {
if (sep) { re = new RegExp(sep.replace(/[-[\]{}()*+!<=:?.\/\\^$|#\s,]/g,'\\$&') +'(?=(?:(?:[^"]*"){2})*[^"]*$)','g'); }
col = col.trim().split(re) || [""];
col = col.map(x => x.replace(/"/g,'').trim());
if ((col.length === 1) && (col[0] === "")) { node.goodtmpl = false; }
else { node.goodtmpl = true; }
return col;
}
var template = clean(node.template,',');
var notemplate = template.length === 1 && template[0] === '';
node.hdrSent = false;
this.on("input", function(msg, send, done) { if (legacyMode) {
if (msg.hasOwnProperty("reset")) { this.template = (n.temp || "");
node.hdrSent = false; this.sep = (n.sep || ',').replace(/\\t/g,"\t").replace(/\\n/g,"\n").replace(/\\r/g,"\r");
this.quo = '"';
this.ret = (n.ret || "\n").replace(/\\n/g,"\n").replace(/\\r/g,"\r");
this.winflag = (this.ret === "\r\n");
this.lineend = "\n";
this.multi = n.multi || "one";
this.hdrin = n.hdrin || false;
this.hdrout = n.hdrout || "none";
this.goodtmpl = true;
this.skip = parseInt(n.skip || 0);
this.store = [];
this.parsestrings = n.strings;
this.include_empty_strings = n.include_empty_strings || false;
this.include_null_values = n.include_null_values || false;
if (this.parsestrings === undefined) { this.parsestrings = true; }
if (this.hdrout === false) { this.hdrout = "none"; }
if (this.hdrout === true) { this.hdrout = "all"; }
var tmpwarn = true;
// var node = this;
var re = new RegExp(node.sep.replace(/[-[\]{}()*+!<=:?.\/\\^$|#\s,]/g,'\\$&') + '(?=(?:(?:[^"]*"){2})*[^"]*$)','g');
// pass in an array of column names to be trimmed, de-quoted and retrimmed
var clean = function(col,sep) {
if (sep) { re = new RegExp(sep.replace(/[-[\]{}()*+!<=:?.\/\\^$|#\s,]/g,'\\$&') +'(?=(?:(?:[^"]*"){2})*[^"]*$)','g'); }
col = col.trim().split(re) || [""];
col = col.map(x => x.replace(/"/g,'').trim());
if ((col.length === 1) && (col[0] === "")) { node.goodtmpl = false; }
else { node.goodtmpl = true; }
return col;
} }
if (msg.hasOwnProperty("payload")) { var template = clean(node.template,',');
if (typeof msg.payload == "object") { // convert object to CSV string var notemplate = template.length === 1 && template[0] === '';
try { node.hdrSent = false;
if (!(notemplate && (msg.hasOwnProperty("parts") && msg.parts.hasOwnProperty("index") && msg.parts.index > 0))) {
template = clean(node.template); this.on("input", function(msg, send, done) {
} if (msg.hasOwnProperty("reset")) {
var ou = ""; node.hdrSent = false;
if (!Array.isArray(msg.payload)) { msg.payload = [ msg.payload ]; } }
if (node.hdrout !== "none" && node.hdrSent === false) { if (msg.hasOwnProperty("payload")) {
if ((template.length === 1) && (template[0] === '')) { if (typeof msg.payload == "object") { // convert object to CSV string
if (msg.hasOwnProperty("columns")) { try {
template = clean(msg.columns || "",","); if (!(notemplate && (msg.hasOwnProperty("parts") && msg.parts.hasOwnProperty("index") && msg.parts.index > 0))) {
} template = clean(node.template);
else {
template = Object.keys(msg.payload[0]);
}
} }
ou += template.map(v => v.indexOf(node.sep)!==-1 ? '"'+v+'"' : v).join(node.sep) + node.ret; const ou = [];
if (node.hdrout === "once") { node.hdrSent = true; } if (!Array.isArray(msg.payload)) { msg.payload = [ msg.payload ]; }
} if (node.hdrout !== "none" && node.hdrSent === false) {
for (var s = 0; s < msg.payload.length; s++) {
if ((Array.isArray(msg.payload[s])) || (typeof msg.payload[s] !== "object")) {
if (typeof msg.payload[s] !== "object") { msg.payload = [ msg.payload ]; }
for (var t = 0; t < msg.payload[s].length; t++) {
if (msg.payload[s][t] === undefined) { msg.payload[s][t] = ""; }
if (msg.payload[s][t].toString().indexOf(node.quo) !== -1) { // add double quotes if any quotes
msg.payload[s][t] = msg.payload[s][t].toString().replace(/"/g, '""');
msg.payload[s][t] = node.quo + msg.payload[s][t].toString() + node.quo;
}
else if (msg.payload[s][t].toString().indexOf(node.sep) !== -1) { // add quotes if any "commas"
msg.payload[s][t] = node.quo + msg.payload[s][t].toString() + node.quo;
}
else if (msg.payload[s][t].toString().indexOf("\n") !== -1) { // add quotes if any "\n"
msg.payload[s][t] = node.quo + msg.payload[s][t].toString() + node.quo;
}
}
ou += msg.payload[s].join(node.sep) + node.ret;
}
else {
if ((template.length === 1) && (template[0] === '') && (msg.hasOwnProperty("columns"))) {
template = clean(msg.columns || "",",");
}
if ((template.length === 1) && (template[0] === '')) { if ((template.length === 1) && (template[0] === '')) {
/* istanbul ignore else */ if (msg.hasOwnProperty("columns")) {
if (tmpwarn === true) { // just warn about missing template once template = clean(msg.columns || "",",");
node.warn(RED._("csv.errors.obj_csv"));
tmpwarn = false;
} }
for (var p in msg.payload[0]) { else {
template = Object.keys(msg.payload[0]);
}
}
ou.push(template.map(v => v.indexOf(node.sep)!==-1 ? '"'+v+'"' : v).join(node.sep));
if (node.hdrout === "once") { node.hdrSent = true; }
}
for (var s = 0; s < msg.payload.length; s++) {
if ((Array.isArray(msg.payload[s])) || (typeof msg.payload[s] !== "object")) {
if (typeof msg.payload[s] !== "object") { msg.payload = [ msg.payload ]; }
for (var t = 0; t < msg.payload[s].length; t++) {
if (msg.payload[s][t] === undefined) { msg.payload[s][t] = ""; }
if (msg.payload[s][t].toString().indexOf(node.quo) !== -1) { // add double quotes if any quotes
msg.payload[s][t] = msg.payload[s][t].toString().replace(/"/g, '""');
msg.payload[s][t] = node.quo + msg.payload[s][t].toString() + node.quo;
}
else if (msg.payload[s][t].toString().indexOf(node.sep) !== -1) { // add quotes if any "commas"
msg.payload[s][t] = node.quo + msg.payload[s][t].toString() + node.quo;
}
else if (msg.payload[s][t].toString().indexOf("\n") !== -1) { // add quotes if any "\n"
msg.payload[s][t] = node.quo + msg.payload[s][t].toString() + node.quo;
}
}
ou.push(msg.payload[s].join(node.sep));
}
else {
if ((template.length === 1) && (template[0] === '') && (msg.hasOwnProperty("columns"))) {
template = clean(msg.columns || "",",");
}
if ((template.length === 1) && (template[0] === '')) {
/* istanbul ignore else */ /* istanbul ignore else */
if (msg.payload[s].hasOwnProperty(p)) { if (tmpwarn === true) { // just warn about missing template once
node.warn(RED._("csv.errors.obj_csv"));
tmpwarn = false;
}
const row = [];
for (var p in msg.payload[0]) {
/* istanbul ignore else */ /* istanbul ignore else */
if (typeof msg.payload[s][p] !== "object") { if (msg.payload[s].hasOwnProperty(p)) {
// Fix to honour include null values flag /* istanbul ignore else */
//if (typeof msg.payload[s][p] !== "object" || (node.include_null_values === true && msg.payload[s][p] === null)) { if (typeof msg.payload[s][p] !== "object") {
var q = ""; // Fix to honour include null values flag
if (msg.payload[s][p] !== undefined) { //if (typeof msg.payload[s][p] !== "object" || (node.include_null_values === true && msg.payload[s][p] === null)) {
q += msg.payload[s][p]; var q = "";
if (msg.payload[s][p] !== undefined) {
q += msg.payload[s][p];
}
if (q.indexOf(node.quo) !== -1) { // add double quotes if any quotes
q = q.replace(/"/g, '""');
row.push(node.quo + q + node.quo);
}
else if (q.indexOf(node.sep) !== -1 || p.indexOf("\n") !== -1) { // add quotes if any "commas" or "\n"
row.push(node.quo + q + node.quo);
}
else { row.push(q); } // otherwise just add
} }
if (q.indexOf(node.quo) !== -1) { // add double quotes if any quotes
q = q.replace(/"/g, '""');
ou += node.quo + q + node.quo + node.sep;
}
else if (q.indexOf(node.sep) !== -1 || p.indexOf("\n") !== -1) { // add quotes if any "commas" or "\n"
ou += node.quo + q + node.quo + node.sep;
}
else { ou += q + node.sep; } // otherwise just add
} }
} }
ou.push(row.join(node.sep)); // add separator
} }
ou = ou.slice(0,-1) + node.ret; else {
const row = [];
for (var t=0; t < template.length; t++) {
if (template[t] === '') {
row.push('');
}
else {
var tt = template[t];
if (template[t].indexOf('"') >=0 ) { tt = "'"+tt+"'"; }
else { tt = '"'+tt+'"'; }
var p = RED.util.getMessageProperty(msg,'payload["'+s+'"]['+tt+']');
/* istanbul ignore else */
if (p === undefined) { p = ""; }
// fix to honour include null values flag
//if (p === null && node.include_null_values !== true) { p = "";}
p = RED.util.ensureString(p);
if (p.indexOf(node.quo) !== -1) { // add double quotes if any quotes
p = p.replace(/"/g, '""');
row.push(node.quo + p + node.quo);
}
else if (p.indexOf(node.sep) !== -1 || p.indexOf("\n") !== -1) { // add quotes if any "commas" or "\n"
row.push(node.quo + p + node.quo);
}
else { row.push(p); } // otherwise just add
}
}
ou.push(row.join(node.sep)); // add separator
}
}
}
// join lines, don't forget to add the last new line
msg.payload = ou.join(node.ret) + node.ret;
msg.columns = template.map(v => v.indexOf(',')!==-1 ? '"'+v+'"' : v).join(',');
if (msg.payload !== '') {
send(msg);
}
done();
}
catch(e) { done(e); }
}
else if (typeof msg.payload == "string") { // convert CSV string to object
try {
var f = true; // flag to indicate if inside or outside a pair of quotes true = outside.
var j = 0; // pointer into array of template items
var k = [""]; // array of data for each of the template items
var o = {}; // output object to build up
var a = []; // output array is needed for multiline option
var first = true; // is this the first line
var last = false;
var line = msg.payload;
var linecount = 0;
var tmp = "";
var has_parts = msg.hasOwnProperty("parts");
var reg = /^[-]?(?!E)(?!0\d)\d*\.?\d*(E-?\+?)?\d+$/i;
if (msg.hasOwnProperty("parts")) {
linecount = msg.parts.index;
if (msg.parts.index > node.skip) { first = false; }
if (msg.parts.hasOwnProperty("count") && (msg.parts.index+1 >= msg.parts.count)) { last = true; }
}
// For now we are just going to assume that any \r or \n means an end of line...
// got to be a weird csv that has singleton \r \n in it for another reason...
// Now process the whole file/line
var nocr = (line.match(/[\r\n]/g)||[]).length;
if (has_parts && node.multi === "mult" && nocr > 1) { tmp = ""; first = true; }
for (var i = 0; i < line.length; i++) {
if (first && (linecount < node.skip)) {
if (line[i] === "\n") { linecount += 1; }
continue;
}
if ((node.hdrin === true) && first) { // if the template is in the first line
if ((line[i] === "\n")||(line[i] === "\r")||(line.length - i === 1)) { // look for first line break
if (line.length - i === 1) { tmp += line[i]; }
template = clean(tmp,node.sep);
first = false;
}
else { tmp += line[i]; }
} }
else { else {
for (var t=0; t < template.length; t++) { if (line[i] === node.quo) { // if it's a quote toggle inside or outside
if (template[t] === '') { f = !f;
ou += node.sep; if (line[i-1] === node.quo) {
if (f === false) { k[j] += '\"'; }
} // if it's a quotequote then it's actually a quote
//if ((line[i-1] !== node.sep) && (line[i+1] !== node.sep)) { k[j] += line[i]; }
}
else if ((line[i] === node.sep) && f) { // if it is the end of the line then finish
if (!node.goodtmpl) { template[j] = "col"+(j+1); }
if ( template[j] && (template[j] !== "") ) {
// if no value between separators ('1,,"3"...') or if the line beings with separator (',1,"2"...') treat value as null
if (line[i-1] === node.sep || line[i-1].includes('\n','\r')) k[j] = null;
if ( (k[j] !== null && node.parsestrings === true) && reg.test(k[j].trim()) ) { k[j] = parseFloat(k[j].trim()); }
if (node.include_null_values && k[j] === null) o[template[j]] = k[j];
if (node.include_empty_strings && k[j] === "") o[template[j]] = k[j];
if (k[j] !== null && k[j] !== "") o[template[j]] = k[j];
} }
else { j += 1;
var tt = template[t]; // if separator is last char in processing string line (without end of line), add null value at the end - example: '1,2,3\n3,"3",'
if (template[t].indexOf('"') >=0 ) { tt = "'"+tt+"'"; } k[j] = line.length - 1 === i ? null : "";
else { tt = '"'+tt+'"'; } }
var p = RED.util.getMessageProperty(msg,'payload["'+s+'"]['+tt+']'); else if (((line[i] === "\n") || (line[i] === "\r")) && f) { // handle multiple lines
/* istanbul ignore else */ //console.log(j,k,o,k[j]);
if (p === undefined) { p = ""; } if (!node.goodtmpl) { template[j] = "col"+(j+1); }
// fix to honour include null values flag if ( template[j] && (template[j] !== "") ) {
//if (p === null && node.include_null_values !== true) { p = "";} // if separator before end of line, set null value ie. '1,2,"3"\n1,2,\n1,2,3'
p = RED.util.ensureString(p); if (line[i-1] === node.sep) k[j] = null;
if (p.indexOf(node.quo) !== -1) { // add double quotes if any quotes if ( (k[j] !== null && node.parsestrings === true) && reg.test(k[j].trim()) ) { k[j] = parseFloat(k[j].trim()); }
p = p.replace(/"/g, '""'); else { if (k[j] !== null) k[j].replace(/\r$/,''); }
ou += node.quo + p + node.quo + node.sep; if (node.include_null_values && k[j] === null) o[template[j]] = k[j];
} if (node.include_empty_strings && k[j] === "") o[template[j]] = k[j];
else if (p.indexOf(node.sep) !== -1 || p.indexOf("\n") !== -1) { // add quotes if any "commas" or "\n" if (k[j] !== null && k[j] !== "") o[template[j]] = k[j];
ou += node.quo + p + node.quo + node.sep;
}
else { ou += p + node.sep; } // otherwise just add
} }
if (JSON.stringify(o) !== "{}") { // don't send empty objects
a.push(o); // add to the array
}
j = 0;
k = [""];
o = {};
f = true; // reset in/out flag ready for next line.
}
else { // just add to the part of the message
k[j] += line[i];
} }
ou = ou.slice(0,-1) + node.ret; // remove final "comma" and add "newline"
} }
} }
} // Finished so finalize and send anything left
msg.payload = ou; if (f === false) { node.warn(RED._("csv.errors.bad_csv")); }
msg.columns = template.map(v => v.indexOf(',')!==-1 ? '"'+v+'"' : v).join(','); if (!node.goodtmpl) { template[j] = "col"+(j+1); }
if (msg.payload !== '') { send(msg); }
done();
}
catch(e) { done(e); }
}
else if (typeof msg.payload == "string") { // convert CSV string to object
try {
var f = true; // flag to indicate if inside or outside a pair of quotes true = outside.
var j = 0; // pointer into array of template items
var k = [""]; // array of data for each of the template items
var o = {}; // output object to build up
var a = []; // output array is needed for multiline option
var first = true; // is this the first line
var last = false;
var line = msg.payload;
var linecount = 0;
var tmp = "";
var has_parts = msg.hasOwnProperty("parts");
var reg = /^[-]?(?!E)(?!0\d)\d*\.?\d*(E-?\+?)?\d+$/i;
if (msg.hasOwnProperty("parts")) {
linecount = msg.parts.index;
if (msg.parts.index > node.skip) { first = false; }
if (msg.parts.hasOwnProperty("count") && (msg.parts.index+1 >= msg.parts.count)) { last = true; }
}
// For now we are just going to assume that any \r or \n means an end of line... if ( template[j] && (template[j] !== "") ) {
// got to be a weird csv that has singleton \r \n in it for another reason... if ( (k[j] !== null && node.parsestrings === true) && reg.test(k[j].trim()) ) { k[j] = parseFloat(k[j].trim()); }
else { if (k[j] !== null) k[j].replace(/\r$/,''); }
// Now process the whole file/line if (node.include_null_values && k[j] === null) o[template[j]] = k[j];
var nocr = (line.match(/[\r\n]/g)||[]).length; if (node.include_empty_strings && k[j] === "") o[template[j]] = k[j];
if (has_parts && node.multi === "mult" && nocr > 1) { tmp = ""; first = true; } if (k[j] !== null && k[j] !== "") o[template[j]] = k[j];
for (var i = 0; i < line.length; i++) {
if (first && (linecount < node.skip)) {
if (line[i] === "\n") { linecount += 1; }
continue;
} }
if ((node.hdrin === true) && first) { // if the template is in the first line
if ((line[i] === "\n")||(line[i] === "\r")||(line.length - i === 1)) { // look for first line break if (JSON.stringify(o) !== "{}") { // don't send empty objects
if (line.length - i === 1) { tmp += line[i]; } a.push(o); // add to the array
template = clean(tmp,node.sep);
first = false;
}
else { tmp += line[i]; }
} }
else {
if (line[i] === node.quo) { // if it's a quote toggle inside or outside
f = !f;
if (line[i-1] === node.quo) {
if (f === false) { k[j] += '\"'; }
} // if it's a quotequote then it's actually a quote
//if ((line[i-1] !== node.sep) && (line[i+1] !== node.sep)) { k[j] += line[i]; }
}
else if ((line[i] === node.sep) && f) { // if it is the end of the line then finish
if (!node.goodtmpl) { template[j] = "col"+(j+1); }
if ( template[j] && (template[j] !== "") ) {
// if no value between separators ('1,,"3"...') or if the line beings with separator (',1,"2"...') treat value as null
if (line[i-1] === node.sep || line[i-1].includes('\n','\r')) k[j] = null;
if ( (k[j] !== null && node.parsestrings === true) && reg.test(k[j].trim()) ) { k[j] = parseFloat(k[j].trim()); }
if (node.include_null_values && k[j] === null) o[template[j]] = k[j];
if (node.include_empty_strings && k[j] === "") o[template[j]] = k[j];
if (k[j] !== null && k[j] !== "") o[template[j]] = k[j];
}
j += 1;
// if separator is last char in processing string line (without end of line), add null value at the end - example: '1,2,3\n3,"3",'
k[j] = line.length - 1 === i ? null : "";
}
else if (((line[i] === "\n") || (line[i] === "\r")) && f) { // handle multiple lines
//console.log(j,k,o,k[j]);
if (!node.goodtmpl) { template[j] = "col"+(j+1); }
if ( template[j] && (template[j] !== "") ) {
// if separator before end of line, set null value ie. '1,2,"3"\n1,2,\n1,2,3'
if (line[i-1] === node.sep) k[j] = null;
if ( (k[j] !== null && node.parsestrings === true) && reg.test(k[j].trim()) ) { k[j] = parseFloat(k[j].trim()); }
else { if (k[j] !== null) k[j].replace(/\r$/,''); }
if (node.include_null_values && k[j] === null) o[template[j]] = k[j];
if (node.include_empty_strings && k[j] === "") o[template[j]] = k[j];
if (k[j] !== null && k[j] !== "") o[template[j]] = k[j];
}
if (JSON.stringify(o) !== "{}") { // don't send empty objects
a.push(o); // add to the array
}
j = 0;
k = [""];
o = {};
f = true; // reset in/out flag ready for next line.
}
else { // just add to the part of the message
k[j] += line[i];
}
}
}
// Finished so finalize and send anything left
if (f === false) { node.warn(RED._("csv.errors.bad_csv")); }
if (!node.goodtmpl) { template[j] = "col"+(j+1); }
if ( template[j] && (template[j] !== "") ) { if (node.multi !== "one") {
if ( (k[j] !== null && node.parsestrings === true) && reg.test(k[j].trim()) ) { k[j] = parseFloat(k[j].trim()); } msg.payload = a;
else { if (k[j] !== null) k[j].replace(/\r$/,''); } if (has_parts && nocr <= 1) {
if (node.include_null_values && k[j] === null) o[template[j]] = k[j]; if (JSON.stringify(o) !== "{}") {
if (node.include_empty_strings && k[j] === "") o[template[j]] = k[j]; node.store.push(o);
if (k[j] !== null && k[j] !== "") o[template[j]] = k[j]; }
} if (msg.parts.index + 1 === msg.parts.count) {
msg.payload = node.store;
if (JSON.stringify(o) !== "{}") { // don't send empty objects msg.columns = template.map(v => v.indexOf(',')!==-1 ? '"'+v+'"' : v).filter(v => v).join(',');
a.push(o); // add to the array delete msg.parts;
} send(msg);
node.store = [];
if (node.multi !== "one") { }
msg.payload = a;
if (has_parts && nocr <= 1) {
if (JSON.stringify(o) !== "{}") {
node.store.push(o);
} }
if (msg.parts.index + 1 === msg.parts.count) { else {
msg.payload = node.store;
msg.columns = template.map(v => v.indexOf(',')!==-1 ? '"'+v+'"' : v).filter(v => v).join(','); msg.columns = template.map(v => v.indexOf(',')!==-1 ? '"'+v+'"' : v).filter(v => v).join(',');
delete msg.parts; send(msg); // finally send the array
send(msg);
node.store = [];
} }
} }
else { else {
msg.columns = template.map(v => v.indexOf(',')!==-1 ? '"'+v+'"' : v).filter(v => v).join(','); var len = a.length;
send(msg); // finally send the array for (var i = 0; i < len; i++) {
} var newMessage = RED.util.cloneMessage(msg);
} newMessage.columns = template.map(v => v.indexOf(',')!==-1 ? '"'+v+'"' : v).filter(v => v).join(',');
else { newMessage.payload = a[i];
var len = a.length; if (!has_parts) {
for (var i = 0; i < len; i++) { newMessage.parts = {
var newMessage = RED.util.cloneMessage(msg); id: msg._msgid,
newMessage.columns = template.map(v => v.indexOf(',')!==-1 ? '"'+v+'"' : v).filter(v => v).join(','); index: i,
newMessage.payload = a[i]; count: len
if (!has_parts) { };
newMessage.parts = { }
id: msg._msgid, else {
index: i, newMessage.parts.index -= node.skip;
count: len newMessage.parts.count -= node.skip;
}; if (node.hdrin) { // if we removed the header line then shift the counts by 1
newMessage.parts.index -= 1;
newMessage.parts.count -= 1;
}
}
if (last) { newMessage.complete = true; }
send(newMessage);
} }
else { if (has_parts && last && len === 0) {
newMessage.parts.index -= node.skip; send({complete:true});
newMessage.parts.count -= node.skip; }
if (node.hdrin) { // if we removed the header line then shift the counts by 1 }
newMessage.parts.index -= 1; node.linecount = 0;
newMessage.parts.count -= 1; done();
}
catch(e) { done(e); }
}
else { node.warn(RED._("csv.errors.csv_js")); done(); }
}
else {
if (!msg.hasOwnProperty("reset")) {
node.send(msg); // If no payload and not reset - just pass it on.
}
done();
}
});
}
if(RFC4180Mode) {
node.template = (n.temp || "")
node.sep = (n.sep || ',').replace(/\\t/g, "\t").replace(/\\n/g, "\n").replace(/\\r/g, "\r")
node.quo = '"'
// default to CRLF (RFC4180 Sec 2.1: "Each record is located on a separate line, delimited by a line break (CRLF)")
node.ret = (n.ret || "\r\n").replace(/\\n/g, "\n").replace(/\\r/g, "\r")
node.multi = n.multi || "one"
node.hdrin = n.hdrin || false
node.hdrout = n.hdrout || "none"
node.goodtmpl = true
node.skip = parseInt(n.skip || 0)
node.store = []
node.parsestrings = n.strings
node.include_empty_strings = n.include_empty_strings || false
node.include_null_values = n.include_null_values || false
if (node.parsestrings === undefined) { node.parsestrings = true }
if (node.hdrout === false) { node.hdrout = "none" }
if (node.hdrout === true) { node.hdrout = "all" }
const dontSendHeaders = node.hdrout === "none"
const sendHeadersOnce = node.hdrout === "once"
const sendHeadersAlways = node.hdrout === "all"
const sendHeaders = !dontSendHeaders && (sendHeadersOnce || sendHeadersAlways)
const quoteables = [node.sep, node.quo, "\n", "\r"]
const templateQuoteables = [',', '"', "\n", "\r"]
let badTemplateWarnOnce = true
const columnStringToTemplateArray = function (col, sep) {
// NOTE: enforce strict column template parsing in RFC4180 mode
const parsed = csv.parse(col, { separator: sep, quote: node.quo, outputStyle: 'array', strict: true })
if (parsed.headers.length > 0) { node.goodtmpl = true } else { node.goodtmpl = false }
return parsed.headers.length ? parsed.headers : null
}
const templateArrayToColumnString = function (template, keepEmptyColumns) {
// NOTE: enforce strict column template parsing in RFC4180 mode
const parsed = csv.parse('', {headers: template, headersOnly:true, separator: ',', quote: node.quo, outputStyle: 'array', strict: true })
return keepEmptyColumns
? parsed.headers.map(e => addQuotes(e || '', { separator: ',', quoteables: templateQuoteables}))
: parsed.header // exclues empty columns
// TODO: resolve inconsistency between CSV->JSON and JSON->CSV
// CSV->JSON: empty columns are excluded
// JSON->CSV: empty columns are kept in some cases
}
function addQuotes(cell, options) {
options = options || {}
return csv.quoteCell(cell, {
quote: options.quote || node.quo || '"',
separator: options.separator || node.sep || ',',
quoteables: options.quoteables || quoteables
})
}
const hasTemplate = (t) => t?.length > 0 && !(t.length === 1 && t[0] === '')
let template
try {
template = columnStringToTemplateArray(node.template, ',') || ['']
} catch (e) {
node.warn(RED._("csv.errors.bad_template")) // is warning really necessary now we have status?
node.status({ fill: "red", shape: "dot", text: RED._("csv.errors.bad_template") })
return // dont hook up the node
}
const noTemplate = hasTemplate(template) === false
node.hdrSent = false
node.on("input", function (msg, send, done) {
node.status({}) // clear status
if (msg.hasOwnProperty("reset")) {
node.hdrSent = false
}
if (msg.hasOwnProperty("payload")) {
let inputData = msg.payload
if (typeof inputData == "object") { // convert object to CSV string
try {
// first determine the payload kind. Array or objects? Array of primitives? Array of arrays? Just an object?
// then, if necessary, convert to an array of objects/arrays
let isObject = !Array.isArray(inputData) && typeof inputData === 'object'
let isArrayOfObjects = Array.isArray(inputData) && inputData.length > 0 && typeof inputData[0] === 'object'
let isArrayOfArrays = Array.isArray(inputData) && inputData.length > 0 && Array.isArray(inputData[0])
let isArrayOfPrimitives = Array.isArray(inputData) && inputData.length > 0 && typeof inputData[0] !== 'object'
if (isObject) {
inputData = [inputData]
isArrayOfObjects = true
isObject = false
} else if (isArrayOfPrimitives) {
inputData = [inputData]
isArrayOfArrays = true
isArrayOfPrimitives = false
}
const stringBuilder = []
if (!(noTemplate && (msg.hasOwnProperty("parts") && msg.parts.hasOwnProperty("index") && msg.parts.index > 0))) {
template = columnStringToTemplateArray(node.template) || ['']
}
// build header line
if (sendHeaders && node.hdrSent === false) {
if (hasTemplate(template) === false) {
if (msg.hasOwnProperty("columns")) {
template = columnStringToTemplateArray(msg.columns || "", ",") || ['']
}
else {
template = Object.keys(inputData[0]) || ['']
} }
} }
if (last) { newMessage.complete = true; } stringBuilder.push(templateArrayToColumnString(template, true))
send(newMessage); if (sendHeadersOnce) { node.hdrSent = true }
} }
if (has_parts && last && len === 0) {
send({complete:true}); // build csv lines
for (let s = 0; s < inputData.length; s++) {
let row = inputData[s]
if (isArrayOfArrays) {
/*** row is an array of arrays ***/
const _hasTemplate = hasTemplate(template)
const len = _hasTemplate ? template.length : row.length
const result = []
for (let t = 0; t < len; t++) {
let cell = row[t]
if (cell === undefined) { cell = "" }
if(_hasTemplate) {
const header = template[t]
if (header) {
result[t] = addQuotes(RED.util.ensureString(cell))
}
} else {
result[t] = addQuotes(RED.util.ensureString(cell))
}
}
stringBuilder.push(result.join(node.sep))
} else {
/*** row is an object ***/
if (hasTemplate(template) === false && (msg.hasOwnProperty("columns"))) {
template = columnStringToTemplateArray(msg.columns || "", ",")
}
if (hasTemplate(template) === false) {
/*** row is an object but we still don't have a template ***/
if (badTemplateWarnOnce === true) {
node.warn(RED._("csv.errors.obj_csv"))
badTemplateWarnOnce = false
}
const rowData = []
for (let header in inputData[0]) {
if (row.hasOwnProperty(header)) {
const cell = row[header]
if (typeof cell !== "object") {
let cellValue = ""
if (cell !== undefined) {
cellValue += cell
}
rowData.push(addQuotes(cellValue))
}
}
}
stringBuilder.push(rowData.join(node.sep))
} else {
/*** row is an object and we have a template ***/
const rowData = []
for (let t = 0; t < template.length; t++) {
if (!template[t]) {
rowData.push('')
}
else {
let cellValue = inputData[s][template[t]]
if (cellValue === undefined) { cellValue = "" }
cellValue = RED.util.ensureString(cellValue)
rowData.push(addQuotes(cellValue))
}
}
stringBuilder.push(rowData.join(node.sep)); // add separator
}
}
} }
// join lines, don't forget to add the last new line
msg.payload = stringBuilder.join(node.ret) + node.ret
msg.columns = templateArrayToColumnString(template)
if (msg.payload !== '') { send(msg) }
done()
}
catch (e) {
done(e)
} }
node.linecount = 0;
done();
} }
catch(e) { done(e); } else if (typeof inputData == "string") { // convert CSV string to object
try {
let firstLine = true; // is this the first line
let last = false
let linecount = 0
const has_parts = msg.hasOwnProperty("parts")
// determine if this is a multi part message and if so what part we are processing
if (msg.hasOwnProperty("parts")) {
linecount = msg.parts.index
if (msg.parts.index > node.skip) { firstLine = false }
if (msg.parts.hasOwnProperty("count") && (msg.parts.index + 1 >= msg.parts.count)) { last = true }
}
// If skip is set, compute the cursor position to start parsing from
let _cursor = 0
if (node.skip > 0 && linecount < node.skip) {
for (; _cursor < inputData.length; _cursor++) {
if (firstLine && (linecount < node.skip)) {
if (inputData[_cursor] === "\r" || inputData[_cursor] === "\n") {
linecount += 1
}
continue
}
break
}
if (_cursor >= inputData.length) {
return // skip this line
}
}
// count the number of line breaks in the string
const noofCR = ((_cursor ? inputData.slice(_cursor) : inputData).match(/[\r\n]/g) || []).length
// if we have `parts` and we are outputting multiple objects and we have more than one line
// then we need to set firstLine to true so that we process the header line
if (has_parts && node.multi === "mult" && noofCR > 1) {
firstLine = true
}
// if we are processing the first line and the node has been set to extract the header line
// update the template with the header line
if (firstLine && node.hdrin === true) {
/** @type {import('./lib/csv/index.js').CSVParseOptions} */
const csvOptionsForHeaderRow = {
cursor: _cursor,
separator: node.sep,
quote: node.quo,
dataHasHeaderRow: true,
headersOnly: true,
outputStyle: 'array',
strict: true // enforce strict parsing of the header row
}
try {
const csvHeader = csv.parse(inputData, csvOptionsForHeaderRow)
template = csvHeader.headers
_cursor = csvHeader.cursor
} catch (e) {
// node.warn(RED._("csv.errors.bad_template")) // add warning?
node.status({ fill: "red", shape: "dot", text: RED._("csv.errors.bad_template") })
throw e
}
}
// now we process the data lines
/** @type {import('./lib/csv/index.js').CSVParseOptions} */
const csvOptions = {
cursor: _cursor,
separator: node.sep,
quote: node.quo,
dataHasHeaderRow: false,
headers: hasTemplate(template) ? template : null,
outputStyle: 'object',
includeNullValues: node.include_null_values,
includeEmptyStrings: node.include_empty_strings,
parseNumeric: node.parsestrings,
strict: false // relax the strictness of the parser for data rows
}
const csvParseResult = csv.parse(inputData, csvOptions)
const data = csvParseResult.data
// output results
if (node.multi !== "one") {
if (has_parts && noofCR <= 1) {
if (data.length > 0) {
node.store.push(...data)
}
if (msg.parts.index + 1 === msg.parts.count) {
msg.payload = node.store
msg.columns = csvParseResult.header
// msg._mode = 'RFC4180 mode'
delete msg.parts
send(msg)
node.store = []
}
}
else {
msg.columns = csvParseResult.header
// msg._mode = 'RFC4180 mode'
msg.payload = data
send(msg); // finally send the array
}
}
else {
const len = data.length
for (let row = 0; row < len; row++) {
const newMessage = RED.util.cloneMessage(msg)
newMessage.columns = csvParseResult.header
newMessage.payload = data[row]
if (!has_parts) {
newMessage.parts = {
id: msg._msgid,
index: row,
count: len
}
}
else {
newMessage.parts.index -= node.skip
newMessage.parts.count -= node.skip
if (node.hdrin) { // if we removed the header line then shift the counts by 1
newMessage.parts.index -= 1
newMessage.parts.count -= 1
}
}
if (last) { newMessage.complete = true }
// newMessage._mode = 'RFC4180 mode'
send(newMessage)
}
if (has_parts && last && len === 0) {
// send({complete:true, _mode: 'RFC4180 mode'})
send({ complete: true })
}
}
node.linecount = 0
done()
}
catch (e) {
done(e)
}
}
else {
// RFC-vs-legacy mode difference: In RFC mode, we throw catchable errors and provide a status message
const err = new Error(RED._("csv.errors.csv_js"))
node.status({ fill: "red", shape: "dot", text: err.message })
done(err)
}
} }
else { node.warn(RED._("csv.errors.csv_js")); done(); } else {
} if (!msg.hasOwnProperty("reset")) {
else { node.send(msg); // If no payload and not reset - just pass it on.
if (!msg.hasOwnProperty("reset")) { }
node.send(msg); // If no payload and not reset - just pass it on. done()
} }
done(); })
} }
});
} }
RED.nodes.registerType("csv",CSVNode);
RED.nodes.registerType("csv",CSVNode)
} }

View File

@ -41,8 +41,8 @@
color:"#DEBD5C", color:"#DEBD5C",
defaults: { defaults: {
name: {value:""}, name: {value:""},
property: {value:"payload"}, property: {value:"payload", validate: RED.validators.typedInput({ type: 'msg', allowUndefined: true }) },
outproperty: {value:"payload"}, outproperty: {value:"payload", validate: RED.validators.typedInput({ type: 'msg', allowUndefined: true }) },
tag: {value:""}, tag: {value:""},
ret: {value:"html"}, ret: {value:"html"},
as: {value:"single"} as: {value:"single"}

View File

@ -32,6 +32,7 @@
defaults: { defaults: {
name: {value:""}, name: {value:""},
property: {value:"payload",required:true, property: {value:"payload",required:true,
validate: RED.validators.typedInput({ type: 'msg', allowUndefined: true}),
label:RED._("node-red:json.label.property")}, label:RED._("node-red:json.label.property")},
action: {value:""}, action: {value:""},
pretty: {value:false} pretty: {value:false}

View File

@ -27,7 +27,8 @@
defaults: { defaults: {
name: {value:""}, name: {value:""},
property: {value:"payload",required:true, property: {value:"payload",required:true,
label:RED._("node-red:common.label.property")}, label:RED._("node-red:common.label.property"),
validate: RED.validators.typedInput({ type: 'msg', allowUndefined: true })},
attr: {value:""}, attr: {value:""},
chr: {value:""} chr: {value:""}
}, },

View File

@ -16,6 +16,7 @@
color:"#DEBD5C", color:"#DEBD5C",
defaults: { defaults: {
property: {value:"payload",required:true, property: {value:"payload",required:true,
validate: RED.validators.typedInput({ type: 'msg', allowUndefined: true }),
label:RED._("node-red:common.label.property")}, label:RED._("node-red:common.label.property")},
name: {value:""} name: {value:""}
}, },

View File

@ -0,0 +1,324 @@
/**
* @typedef {Object} CSVParseOptions
* @property {number} [cursor=0] - an index into the CSV to start parsing from
* @property {string} [separator=','] - the separator character
* @property {string} [quote='"'] - the quote character
* @property {boolean} [headersOnly=false] - only parse the headers and return them
* @property {string[]} [headers=[]] - an array of headers to use instead of the first row of the CSV data
* @property {boolean} [dataHasHeaderRow=true] - whether the CSV data to parse has a header row
* @property {boolean} [outputHeader=true] - whether the output data should include a header row (only applies to array output)
* @property {boolean} [parseNumeric=false] - parse numeric values into numbers
* @property {boolean} [includeNullValues=false] - include null values in the output
* @property {boolean} [includeEmptyStrings=true] - include empty strings in the output
* @property {string} [outputStyle='object'] - output an array of arrays or an array of objects
* @property {boolean} [strict=false] - throw an error if the CSV is malformed
*/
/**
* Parses a CSV string into an array of arrays or an array of objects.
*
* NOTES:
* * Deviations from the RFC4180 spec (for the sake of user fiendliness, system implementations and flexibility), this parser will:
* * accept any separator character, not just `,`
* * accept any quote character, not just `"`
* * parse `\r`, `\n` or `\r\n` as line endings (RRFC4180 2.1 states lines are separated by CRLF)
* * Only single character `quote` is supported
* * `quote` is `"` by default
* * Any cell that contains a `quote` or `separator` will be quoted
* * Any `quote` characters inside a cell will be escaped as per RFC 4180 2.6
* * Only single character `separator` is supported
* * Only `array` and `object` output styles are supported
* * `array` output style is an array of arrays [[],[],[]]
* * `object` output style is an array of objects [{},{},{}]
* * Only `headers` or `dataHasHeaderRow` are supported, not both
* @param {string} csvIn - the CSV string to parse
* @param {CSVParseOptions} parseOptions - options
* @throws {Error}
*/
function parse(csvIn, parseOptions) {
/* Normalise options */
parseOptions = parseOptions || {};
const separator = parseOptions.separator ?? ',';
const quote = parseOptions.quote ?? '"';
const headersOnly = parseOptions.headersOnly ?? false;
const headers = Array.isArray(parseOptions.headers) ? parseOptions.headers : []
const dataHasHeaderRow = parseOptions.dataHasHeaderRow ?? true;
const outputHeader = parseOptions.outputHeader ?? true;
const parseNumeric = parseOptions.parseNumeric ?? false;
const includeNullValues = parseOptions.includeNullValues ?? false;
const includeEmptyStrings = parseOptions.includeEmptyStrings ?? true;
const outputStyle = ['array', 'object'].includes(parseOptions.outputStyle) ? parseOptions.outputStyle : 'object'; // 'array [[],[],[]]' or 'object [{},{},{}]
const strict = parseOptions.strict ?? false
/* Local variables */
const cursorMax = csvIn.length;
const ouputArrays = outputStyle === 'array';
const headersSupplied = headers.length > 0
// The original regex was an "is-a-number" positive logic test. /^ *[-]?(?!E)(?!0\d)\d*\.?\d*(E-?\+?)?\d+ *$/i;
// Below, is less strict and inverted logic but coupled with +cast it is 13%+ faster than original regex+parsefloat
// and has the benefit of understanding hexadecimals, binary and octal numbers.
const skipNumberConversion = /^ *(\+|-0\d|0\d)/
const cellBuilder = []
let rowBuilder = []
let cursor = typeof parseOptions.cursor === 'number' ? parseOptions.cursor : 0;
let newCell = true, inQuote = false, closed = false, output = [];
/* inline helper functions */
const finaliseCell = () => {
let cell = cellBuilder.join('')
cellBuilder.length = 0
// push the cell:
// NOTE: if cell is empty but newCell==true, then this cell had zero chars - push `null`
// otherwise push empty string
return rowBuilder.push(cell || (newCell ? null : ''))
}
const finaliseRow = () => {
if (cellBuilder.length) {
finaliseCell()
}
if (rowBuilder.length) {
output.push(rowBuilder)
rowBuilder = []
}
}
/* Main parsing loop */
while (cursor < cursorMax) {
const char = csvIn[cursor]
if (inQuote) {
if (char === quote && csvIn[cursor + 1] === quote) {
cellBuilder.push(quote)
cursor += 2;
newCell = false;
closed = false;
} else if (char === quote) {
inQuote = false;
cursor += 1;
newCell = false;
closed = true;
} else {
cellBuilder.push(char)
newCell = false;
closed = false;
cursor++;
}
} else {
if (char === separator) {
finaliseCell()
cursor += 1;
newCell = true;
closed = false;
} else if (char === quote) {
if (newCell) {
inQuote = true;
cursor += 1;
newCell = false;
closed = false;
}
else if (strict) {
throw new UnquotedQuoteError(cursor)
} else {
// not strict, keep 1 quote if the next char is not a cell/record separator
cursor++
if (csvIn[cursor] && csvIn[cursor] !== '\n' && csvIn[cursor] !== '\r' && csvIn[cursor] !== separator) {
cellBuilder.push(char)
if (csvIn[cursor] === quote) {
cursor++ // skip the next quote
}
}
}
} else {
if (char === '\n' || char === '\r') {
finaliseRow()
if (csvIn[cursor + 1] === '\n') {
cursor += 2;
} else {
cursor++
}
newCell = true;
closed = false;
if (headersOnly) {
break
}
} else {
if (closed) {
if (strict) {
throw new DataAfterCloseError(cursor)
} else {
cursor--; // move back to grab the previously discarded char
closed = false
}
} else {
cellBuilder.push(char)
newCell = false;
cursor++;
}
}
}
}
}
if (strict && inQuote) {
throw new ParseError(`Missing quote, unclosed cell`, cursor)
}
// finalise the last cell/row
finaliseRow()
let firstRowIsHeader = false
// if no headers supplied, generate them
if (output.length >= 1) {
if (headersSupplied) {
// headers already supplied
} else if (dataHasHeaderRow) {
// take the first row as the headers
headers.push(...output[0])
firstRowIsHeader = true
} else {
// generate headers col1, col2, col3, etc
for (let i = 0; i < output[0].length; i++) {
headers.push("col" + (i + 1))
}
}
}
const finalResult = {
/** @type {String[]} headers as an array of string */
headers: headers,
/** @type {String} headers as a comma-separated string */
header: null,
/** @type {Any[]} Result Data (may include header row: check `firstRowIsHeader` flag) */
data: [],
/** @type {Boolean|undefined} flag to indicate if the first row is a header row (only applies when `outputStyle` is 'array') */
firstRowIsHeader: undefined,
/** @type {'array'|'object'} flag to indicate the output style */
outputStyle: outputStyle,
/** @type {Number} The current cursor position */
cursor: cursor,
}
const quotedHeaders = []
for (let i = 0; i < headers.length; i++) {
if (!headers[i]) {
continue
}
quotedHeaders.push(quoteCell(headers[i], { quote, separator: ',' }))
}
finalResult.header = quotedHeaders.join(',') // always quote headers and join with comma
// output is an array of arrays [[],[],[]]
if (ouputArrays || headersOnly) {
if (!firstRowIsHeader && !headersOnly && outputHeader && headers.length > 0) {
if (output.length > 0) {
output.unshift(headers)
} else {
output = [headers]
}
firstRowIsHeader = true
}
if (headersOnly) {
delete finalResult.firstRowIsHeader
return finalResult
}
finalResult.firstRowIsHeader = firstRowIsHeader
finalResult.data = (firstRowIsHeader && !outputHeader) ? output.slice(1) : output
return finalResult
}
// output is an array of objects [{},{},{}]
const outputObjects = []
let i = firstRowIsHeader ? 1 : 0
for (; i < output.length; i++) {
const rowObject = {}
let isEmpty = true
for (let j = 0; j < headers.length; j++) {
if (!headers[j]) {
continue
}
let v = output[i][j] === undefined ? null : output[i][j]
if (v === null && !includeNullValues) {
continue
} else if (v === "" && !includeEmptyStrings) {
continue
} else if (parseNumeric === true && v && !skipNumberConversion.test(v)) {
const vTemp = +v
const isNumber = !isNaN(vTemp)
if(isNumber) {
v = vTemp
}
}
rowObject[headers[j]] = v
isEmpty = false
}
// determine if this row is empty
if (!isEmpty) {
outputObjects.push(rowObject)
}
}
finalResult.data = outputObjects
delete finalResult.firstRowIsHeader
return finalResult
}
/**
* Quotes a cell in a CSV string if necessary. Addiionally, any double quotes inside the cell will be escaped as per RFC 4180 2.6 (https://datatracker.ietf.org/doc/html/rfc4180#section-2).
* @param {string} cell - the string to quote
* @param {*} options - options
* @param {string} [options.quote='"'] - the quote character
* @param {string} [options.separator=','] - the separator character
* @param {string[]} [options.quoteables] - an array of characters that, when encountered, will trigger the application of outer quotes
* @returns
*/
function quoteCell(cell, { quote = '"', separator = ",", quoteables } = {
quote: '"',
separator: ",",
quoteables: [quote, separator, '\r', '\n']
}) {
quoteables = quoteables || [quote, separator, '\r', '\n'];
let doubleUp = false;
if (cell.indexOf(quote) !== -1) { // add double quotes if any quotes
doubleUp = true;
}
const quoteChar = quoteables.some(q => cell.includes(q)) ? quote : '';
return quoteChar + (doubleUp ? cell.replace(/"/g, '""') : cell) + quoteChar;
}
// #region Custom Error Classes
class ParseError extends Error {
/**
* @param {string} message - the error message
* @param {number} cursor - the cursor index where the error occurred
*/
constructor(message, cursor) {
super(message)
this.name = 'ParseError'
this.cursor = cursor
}
}
class UnquotedQuoteError extends ParseError {
/**
* @param {number} cursor - the cursor index where the error occurred
*/
constructor(cursor) {
super('Quote found in the middle of an unquoted field', cursor)
this.name = 'UnquotedQuoteError'
}
}
class DataAfterCloseError extends ParseError {
/**
* @param {number} cursor - the cursor index where the error occurred
*/
constructor(cursor) {
super('Data found after closing quote', cursor)
this.name = 'DataAfterCloseError'
}
}
// #endregion
exports.parse = parse
exports.quoteCell = quoteCell
exports.ParseError = ParseError
exports.UnquotedQuoteError = UnquotedQuoteError
exports.DataAfterCloseError = DataAfterCloseError

View File

@ -60,7 +60,7 @@
arraySplt: {value:1}, arraySplt: {value:1},
arraySpltType: {value:"len"}, arraySpltType: {value:"len"},
stream: {value:false}, stream: {value:false},
addname: {value:""}, addname: {value:"", validate: RED.validators.typedInput({ type: 'msg', allowBlank: true })},
property: {value:"payload",required:true} property: {value:"payload",required:true}
}, },
inputs:1, inputs:1,
@ -216,7 +216,22 @@
validate:RED.validators.typedInput("propertyType", false) validate:RED.validators.typedInput("propertyType", false)
}, },
propertyType: { value:"msg"}, propertyType: { value:"msg"},
key: {value:"topic"}, key: {value:"topic", validate: (function () {
const typeValidator = RED.validators.typedInput({ type: 'msg' })
return function(v, opt) {
const joinMode = $("#node-input-mode").val() || this.mode
if (joinMode !== 'custom') {
return true
}
const buildType = $("#node-input-build").val() || this.build
if (buildType !== 'object') {
return true
} else {
return typeValidator(v, opt)
}
}
})()
},
joiner: { value:"\\n"}, joiner: { value:"\\n"},
joinerType: { value:"str"}, joinerType: { value:"str"},
accumulate: { value:"false" }, accumulate: { value:"false" },

View File

@ -198,7 +198,7 @@
category: 'storage', category: 'storage',
defaults: { defaults: {
name: {value:""}, name: {value:""},
filename: {value:""}, filename: {value:"", validate: RED.validators.typedInput({ typeField: 'filenameType' })},
filenameType: {value:"str"}, filenameType: {value:"str"},
appendNewline: {value:true}, appendNewline: {value:true},
createDir: {value:false}, createDir: {value:false},
@ -297,7 +297,7 @@
category: 'storage', category: 'storage',
defaults: { defaults: {
name: {value:""}, name: {value:""},
filename: {value:""}, filename: {value:"", validate: RED.validators.typedInput({ typeField: 'filenameType' }) },
filenameType: {value:"str"}, filenameType: {value:"str"},
format: {value:"utf8"}, format: {value:"utf8"},
chunk: {value:false}, chunk: {value:false},

Binary file not shown.

Before

Width:  |  Height:  |  Size: 603 B

View File

@ -0,0 +1 @@
<svg width="40" height="60" viewBox="0, 0, 40, 60" xmlns="http://www.w3.org/2000/svg"><g fill="none" stroke="#fff"><path d="m9.7884 22.379c-5.2427-0.41732-9.6475 5.7885-7.4975 10.585 2.0949 5.2041 9.9782 6.6154 13.727 2.4477 3.633-3.5613 5.0332-9.0411 9.4821-11.853 4.5205-3.0872 11.797-0.172 12.68 5.3144 0.86 5.2537-4.8017 10.364-9.9231 8.8205-3.7873-0.85449-6.5051-4.0905-8.0487-7.4975-1.9019-3.2526-4.3882-6.7257-8.2693-7.6077-0.6891-0.15656-1.4003-0.21831-2.1059-0.21721z" stroke-width="3.3"/><path d="m6.7012 29.821h6.6154" stroke-width="1.4"/><path d="m26.988 29.821h5.5128m-2.8115-2.7564v5.5128" stroke-width="1.8"/></g></svg>

After

Width:  |  Height:  |  Size: 635 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -0,0 +1 @@
<svg width="40" height="60" viewBox="0, 0, 40, 60" xmlns="http://www.w3.org/2000/svg"><path d="m8.3474 17.75 22.298 22.444-10.747 13.013v-46.497l10.747 12.428-22.298 21.859" fill="none" stroke="#fff" stroke-width="4"/></svg>

After

Width:  |  Height:  |  Size: 225 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -0,0 +1 @@
<svg width="40" height="60" viewBox="0, 0, 40, 60" xmlns="http://www.w3.org/2000/svg"><path d="m2.7078 12.986c0 7.7994-0.36386 21.569 0 32.545s35.118 9.8751 34.848 0c-0.26959-9.8751 0-24.82 0-32.545 0-7.7243-34.848-7.7995-34.848 0z" fill="none" stroke="#fff"/><g fill="#fff"><path d="m3.8741 13.406v8.955c0.021834 3.5781 19.543 5.0789 25.575 3.2543 0 0 0.02229-2.6683 0.02998-2.6673l5.5325 0.7238c0.64508 0.0844 1.1345-0.74597 1.134-1.3284v-8.573l-0.99896 0.93349-15.217-2.2765c4.5883 2.1798 9.808 4.1312 9.808 4.1312-9.3667 3.1562-25.846-0.31965-25.864-3.1525z"/><path d="m3.886 26.607v8.1052c3.2188 6.1087 29.901 5.8574 32.272 0v-8.1052c-3.3598 4.6685-29.204 5.1534-32.272 0z"/><path d="m4.0032 39.082v7.1522c2.556 7.4622 28.918 7.6072 32.272 0v-7.1522c-3.2345 4.9471-29.087 5.359-32.272 0z"/></g></svg>

After

Width:  |  Height:  |  Size: 806 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 414 B

View File

@ -0,0 +1 @@
<svg width="40" height="60" viewBox="0, 0, 40, 60" xmlns="http://www.w3.org/2000/svg"><path d="m23.515 13.831c-4.7594-5.8789-2.6084-5.7751-7.3474 0-8.0368 10.477-8.3322 24.431 2.5476 32.935 0.13181 2.0418 0.46056 4.9803 0.46056 4.9803h1.315s0.32875-2.9219 0.46017-4.9803c2.8458-2.2339 16.799-14.619 2.5641-32.935z" fill="#fff"/></svg>

After

Width:  |  Height:  |  Size: 335 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 671 B

View File

@ -0,0 +1 @@
<svg width="40" height="60" viewBox="0, 0, 40, 60" xmlns="http://www.w3.org/2000/svg"><g fill="none" stroke="#fff" stroke-width="3"><path d="m6 30c6 5 24 4 29-0.07"/><path d="m21 33 0.1-19c0.02-4 4-3 4-6s-4-2-4-5"/><path d="m6 22c0-11 29-10 29 0v21c0 18-29 19-29 0s4e-7 -11 0-21z"/></g></svg>

After

Width:  |  Height:  |  Size: 293 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 252 B

View File

@ -0,0 +1 @@
<svg width="40" height="60" viewBox="0, 0, 40, 60" xmlns="http://www.w3.org/2000/svg"><path d="m29 12s0.1 30 0.05 31-3 5-7 5-19 0.04-19 0.04c6-4 9-5 17-5 0 0 4-0.1 4-2 0-2 8e-3 -29 8e-3 -29z" fill="#fff"/><path d="m12 47s-0.1-30-0.05-31 3-5 7-5 19-0.04 19-0.04c-6 4-9 5-17 5 0 0-4 0.1-4 2 0 2-8e-3 29-8e-3 29z" fill="#fff"/></svg>

After

Width:  |  Height:  |  Size: 331 B

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