diff --git a/CHANGELOG.md b/CHANGELOG.md index 54e98ba74..d442405b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,17 @@ +#### 2.0.6: Maintenance Release + +Editor + + - Fix typo in ko editor.json Fixes #3119 + - Change fade color when hovering an inactive tab (#3106) @bonanitech + - Ensure treeList row has suitable min-height when no content Fixes #3109 + +Runtime + + - Update tar to latest (#3128) @aksswami + - Give passport verify callback the same arity as the original callback (#3117) @dschmidt + - Handle HTTPS Key and certificate as string or buffer (#3115) @bartbutenaers + #### 2.0.5: Maintenance Release Editor diff --git a/Gruntfile.js b/Gruntfile.js index df9939a8c..261f0c72b 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -162,7 +162,6 @@ module.exports = function(grunt) { "packages/node_modules/@node-red/editor-client/src/js/ui/common/stack.js", "packages/node_modules/@node-red/editor-client/src/js/ui/common/typedInput.js", "packages/node_modules/@node-red/editor-client/src/js/ui/common/toggleButton.js", - "packages/node_modules/@node-red/editor-client/src/js/ui/common/colorPicker.js", "packages/node_modules/@node-red/editor-client/src/js/ui/actions.js", "packages/node_modules/@node-red/editor-client/src/js/ui/deploy.js", "packages/node_modules/@node-red/editor-client/src/js/ui/diff.js", @@ -182,6 +181,7 @@ module.exports = function(grunt) { "packages/node_modules/@node-red/editor-client/src/js/ui/tab-context.js", "packages/node_modules/@node-red/editor-client/src/js/ui/palette-editor.js", "packages/node_modules/@node-red/editor-client/src/js/ui/editor.js", + "packages/node_modules/@node-red/editor-client/src/js/ui/editors/panes/*.js", "packages/node_modules/@node-red/editor-client/src/js/ui/editors/*.js", "packages/node_modules/@node-red/editor-client/src/js/ui/editors/code-editors/*.js", "packages/node_modules/@node-red/editor-client/src/js/ui/event-log.js", diff --git a/package.json b/package.json index 2296ad220..d52055bc4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-red", - "version": "2.0.5", + "version": "2.0.6", "description": "Low-code programming for event-driven applications", "homepage": "http://nodered.org", "license": "Apache-2.0", @@ -28,8 +28,8 @@ "dependencies": { "acorn": "8.4.1", "acorn-walk": "8.1.1", - "ajv": "8.6.0", - "async-mutex": "0.3.1", + "ajv": "8.6.2", + "async-mutex": "0.3.2", "basic-auth": "2.0.1", "bcryptjs": "2.4.3", "body-parser": "1.19.0", @@ -55,14 +55,14 @@ "is-utf8": "0.2.1", "js-yaml": "3.14.1", "json-stringify-safe": "5.0.1", - "jsonata": "1.8.4", + "jsonata": "1.8.5", "lodash.clonedeep": "^4.5.0", "media-typer": "1.1.0", "memorystore": "1.6.6", "mime": "2.5.2", "moment-timezone": "0.5.33", "mqtt": "4.2.8", - "multer": "1.4.2", + "multer": "1.4.3", "mustache": "4.2.0", "node-red-admin": "^2.2.0", "nopt": "5.0.0", @@ -73,9 +73,9 @@ "passport-oauth2-client-password": "0.1.2", "raw-body": "2.4.1", "semver": "7.3.5", - "tar": "6.1.2", + "tar": "6.1.11", "tough-cookie": "4.0.0", - "uglify-js": "3.13.10", + "uglify-js": "3.14.1", "uuid": "8.3.2", "ws": "7.5.1", "xml2js": "0.4.23" @@ -84,7 +84,7 @@ "bcrypt": "5.0.1" }, "devDependencies": { - "dompurify": "2.2.9", + "dompurify": "2.3.1", "grunt": "1.4.1", "grunt-chmod": "~1.1.1", "grunt-cli": "~1.4.3", @@ -109,15 +109,15 @@ "jsdoc-nr-template": "github:node-red/jsdoc-nr-template", "marked": "2.1.3", "minami": "1.2.3", - "mocha": "9.0.1", + "mocha": "9.1.1", "node-red-node-test-helper": "^0.2.7", - "nodemon": "2.0.8", + "nodemon": "2.0.12", "proxy": "^1.0.2", - "sass": "1.35.1", + "sass": "1.39.0", "should": "13.2.3", - "sinon": "11.1.1", + "sinon": "11.1.2", "stoppable": "^1.1.0", - "supertest": "6.1.3" + "supertest": "6.1.6" }, "engines": { "node": ">=12" diff --git a/packages/node_modules/@node-red/editor-api/lib/auth/index.js b/packages/node_modules/@node-red/editor-api/lib/auth/index.js index fb95ede7b..5552301d0 100644 --- a/packages/node_modules/@node-red/editor-api/lib/auth/index.js +++ b/packages/node_modules/@node-red/editor-api/lib/auth/index.js @@ -173,27 +173,30 @@ function genericStrategy(adminApp,strategy) { adminApp.use(passport.session()); var options = strategy.options; + var verify = function() { + var originalDone = arguments[arguments.length-1]; + if (options.verify) { + var args = Array.from(arguments); + args[args.length-1] = function(err,profile) { + if (err) { + return originalDone(err); + } else { + return completeVerify(profile,originalDone); + } + }; - passport.use(new strategy.strategy(options, - function() { - var originalDone = arguments[arguments.length-1]; - if (options.verify) { - var args = Array.from(arguments); - args[args.length-1] = function(err,profile) { - if (err) { - return originalDone(err); - } else { - return completeVerify(profile,originalDone); - } - }; - options.verify.apply(null,args); - } else { - var profile = arguments[arguments.length - 2]; - return completeVerify(profile,originalDone); - } - + options.verify.apply(null,args); + } else { + var profile = arguments[arguments.length - 2]; + return completeVerify(profile,originalDone); } - )); + }; + // Give our callback the same arity as the original one from options + if (options.verify) { + Object.defineProperty(verify, "length", { value: options.verify.length }) + } + + passport.use(new strategy.strategy(options, verify)); adminApp.get('/auth/strategy', passport.authenticate(strategy.name, {session:false, failureRedirect: settings.httpAdminRoot }), diff --git a/packages/node_modules/@node-red/editor-api/package.json b/packages/node_modules/@node-red/editor-api/package.json index d3c342cba..73f512538 100644 --- a/packages/node_modules/@node-red/editor-api/package.json +++ b/packages/node_modules/@node-red/editor-api/package.json @@ -1,6 +1,6 @@ { "name": "@node-red/editor-api", - "version": "2.0.5", + "version": "2.0.6", "license": "Apache-2.0", "main": "./lib/index.js", "repository": { @@ -16,8 +16,8 @@ } ], "dependencies": { - "@node-red/util": "2.0.5", - "@node-red/editor-client": "2.0.5", + "@node-red/util": "2.0.6", + "@node-red/editor-client": "2.0.6", "bcryptjs": "2.4.3", "body-parser": "1.19.0", "clone": "2.1.2", @@ -26,7 +26,7 @@ "express": "4.17.1", "memorystore": "1.6.6", "mime": "2.5.2", - "multer": "1.4.2", + "multer": "1.4.3", "mustache": "4.2.0", "oauth2orize": "1.11.0", "passport-http-bearer": "1.0.1", diff --git a/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json b/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json index 1f5c19799..2a1e16f1d 100755 --- a/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json +++ b/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json @@ -123,7 +123,20 @@ "groupSelection": "Group selection", "ungroupSelection": "Ungroup selection", "groupMergeSelection": "Merge selection", - "groupRemoveSelection": "Remove from group" + "groupRemoveSelection": "Remove from group", + "arrange":"Arrange", + "alignLeft":"Align to left", + "alignCenter":"Align to center", + "alignRight":"Align to right", + "alignTop":"Align to top", + "alignMiddle":"Align to middle", + "alignBottom":"Align to bottom", + "distributeHorizontally":"Distribute horizontally", + "distributeVertically":"Distribute vertically", + "moveToBack":"Move to back", + "moveToFront":"Move to front", + "moveBackwards":"Move backwards", + "moveForwards":"Move forwards" } }, "actions": { @@ -457,8 +470,9 @@ "unassigned": "Unassigned", "global": "global", "workspace": "workspace", - "selectAll": "Select all nodes", - "selectAllConnected": "Select all connected nodes", + "selectAll": "Select all", + "selectNone": "Select none", + "selectAllConnected": "Select connected", "addRemoveNode": "Add/remove node from selection", "editSelected": "Edit selected node", "deleteSelected": "Delete selected nodes or link", @@ -471,7 +485,10 @@ "copyNode": "Copy selected nodes", "cutNode": "Cut selected nodes", "pasteNode": "Paste nodes", - "undoChange": "Undo the last change performed", + "copyGroupStyle": "Copy group style", + "pasteGroupStyle": "Paste group style", + "undoChange": "Undo", + "redoChange": "Redo", "searchBox": "Open search box", "managePalette": "Manage palette", "actionList":"Action list" diff --git a/packages/node_modules/@node-red/editor-client/locales/ko/editor.json b/packages/node_modules/@node-red/editor-client/locales/ko/editor.json index 3c12e116a..9e0c74ea8 100755 --- a/packages/node_modules/@node-red/editor-client/locales/ko/editor.json +++ b/packages/node_modules/@node-red/editor-client/locales/ko/editor.json @@ -56,7 +56,7 @@ "displayConfig": "설정노드 보기", "import": "가져오기", "export": "내보내기", - "search": "플로우 겅색", + "search": "플로우 검색", "searchInput": "플로우 검색", "subflows": "보조 플로우", "createSubflow": "보조 플로우 생성", diff --git a/packages/node_modules/@node-red/editor-client/package.json b/packages/node_modules/@node-red/editor-client/package.json index ee3f1d8ad..fc315b61d 100644 --- a/packages/node_modules/@node-red/editor-client/package.json +++ b/packages/node_modules/@node-red/editor-client/package.json @@ -1,6 +1,6 @@ { "name": "@node-red/editor-client", - "version": "2.0.5", + "version": "2.0.6", "license": "Apache-2.0", "repository": { "type": "git", diff --git a/packages/node_modules/@node-red/editor-client/src/js/history.js b/packages/node_modules/@node-red/editor-client/src/js/history.js index 338d955e1..256500200 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/history.js +++ b/packages/node_modules/@node-red/editor-client/src/js/history.js @@ -558,11 +558,22 @@ RED.history = (function() { } else if (ev.t == "reorder") { inverseEv = { t: 'reorder', - order: RED.nodes.getWorkspaceOrder(), dirty: RED.nodes.dirty() }; - if (ev.order) { - RED.workspaces.order(ev.order); + if (ev.workspaces) { + inverseEv.workspaces = { + from: ev.workspaces.to, + to: ev.workspaces.from + } + RED.workspaces.order(ev.workspaces.from); + } + if (ev.nodes) { + inverseEv.nodes = { + z: ev.nodes.z, + from: ev.nodes.to, + to: ev.nodes.from + } + RED.nodes.setNodeOrder(ev.nodes.z,ev.nodes.from); } } else if (ev.t == "createGroup") { inverseEv = { @@ -658,6 +669,8 @@ RED.history = (function() { push: function(ev) { undoHistory.push(ev); redoHistory = []; + RED.menu.setDisabled("menu-item-edit-undo", false); + RED.menu.setDisabled("menu-item-edit-redo", true); }, pop: function() { var ev = undoHistory.pop(); @@ -665,6 +678,8 @@ RED.history = (function() { if (rev) { redoHistory.push(rev); } + RED.menu.setDisabled("menu-item-edit-undo", undoHistory.length === 0); + RED.menu.setDisabled("menu-item-edit-redo", redoHistory.length === 0); }, peek: function() { return undoHistory[undoHistory.length-1]; @@ -672,6 +687,8 @@ RED.history = (function() { clear: function() { undoHistory = []; redoHistory = []; + RED.menu.setDisabled("menu-item-edit-undo", true); + RED.menu.setDisabled("menu-item-edit-redo", true); }, redo: function() { var ev = redoHistory.pop(); @@ -681,6 +698,8 @@ RED.history = (function() { undoHistory.push(uev); } } + RED.menu.setDisabled("menu-item-edit-undo", undoHistory.length === 0); + RED.menu.setDisabled("menu-item-edit-redo", redoHistory.length === 0); } } diff --git a/packages/node_modules/@node-red/editor-client/src/js/keymap.json b/packages/node_modules/@node-red/editor-client/src/js/keymap.json index 7a3bab2c2..766f6bd9f 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/keymap.json +++ b/packages/node_modules/@node-red/editor-client/src/js/keymap.json @@ -79,6 +79,15 @@ "right": "core:go-to-nearest-node-on-right", "left": "core:go-to-nearest-node-on-left", "up": "core:go-to-nearest-node-above", - "down": "core:go-to-nearest-node-below" + "down": "core:go-to-nearest-node-below", + "alt-a g": "core:align-selection-to-grid", + "alt-a l": "core:align-selection-to-left", + "alt-a r": "core:align-selection-to-right", + "alt-a t": "core:align-selection-to-top", + "alt-a b": "core:align-selection-to-bottom", + "alt-a m": "core:align-selection-to-middle", + "alt-a c": "core:align-selection-to-center", + "alt-a h": "core:distribute-selection-horizontally", + "alt-a v": "core:distribute-selection-vertically" } } diff --git a/packages/node_modules/@node-red/editor-client/src/js/nodes.js b/packages/node_modules/@node-red/editor-client/src/js/nodes.js index ee3542eee..da3bdb729 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/nodes.js +++ b/packages/node_modules/@node-red/editor-client/src/js/nodes.js @@ -16,8 +16,6 @@ RED.nodes = (function() { var node_defs = {}; - var nodes = {}; - var nodeTabMap = {}; var linkTabMap = {}; var configNodes = {}; @@ -41,6 +39,7 @@ RED.nodes = (function() { RED.events.emit("workspace:dirty",{dirty:dirty}); } + // The registry holds information about all node types. var registry = (function() { var moduleList = {}; var nodeList = []; @@ -53,7 +52,8 @@ RED.nodes = (function() { defaults: { label: {value:""}, disabled: {value: false}, - info: {value: ""} + info: {value: ""}, + env: {value: []} } }; @@ -209,6 +209,279 @@ RED.nodes = (function() { return exports; })(); + // allNodes holds information about the Flow nodes. + var allNodes = (function() { + var nodes = {}; + var tabMap = {}; + var api = { + addTab: function(id) { + tabMap[id] = []; + }, + hasTab: function(z) { + return tabMap.hasOwnProperty(z) + }, + removeTab: function(id) { + delete tabMap[id]; + }, + addNode: function(n) { + nodes[n.id] = n; + if (tabMap.hasOwnProperty(n.z)) { + tabMap[n.z].push(n); + } else { + console.warn("Node added to unknown tab/subflow:",n); + tabMap["_"] = tabMap["_"] || []; + tabMap["_"].push(n); + } + }, + removeNode: function(n) { + delete nodes[n.id] + if (tabMap.hasOwnProperty(n.z)) { + var i = tabMap[n.z].indexOf(n); + if (i > -1) { + tabMap[n.z].splice(i,1); + } + } + }, + hasNode: function(id) { + return nodes.hasOwnProperty(id); + }, + getNode: function(id) { + return nodes[id] + }, + moveNode: function(n, newZ) { + api.removeNode(n); + tabMap[newZ] = tabMap[newZ] || []; + tabMap[newZ].push(n); + }, + moveNodesForwards: function(nodes) { + var result = []; + if (!Array.isArray(nodes)) { + nodes = [nodes] + } + // Can only do this for nodes on the same tab. + // Use nodes[0] to get the z + var tabNodes = tabMap[nodes[0].z]; + var toMove = new Set(nodes.filter(function(n) { return n.type !== "group" && n.type !== "subflow" })); + var moved = new Set(); + for (var i = tabNodes.length-1; i >= 0; i--) { + if (toMove.size === 0) { + break; + } + var n = tabNodes[i]; + if (toMove.has(n)) { + // This is a node to move. + if (i < tabNodes.length-1 && !moved.has(tabNodes[i+1])) { + // Remove from current position + tabNodes.splice(i,1); + // Add it back one position higher + tabNodes.splice(i+1,0,n); + n._reordered = true; + result.push(n); + } + toMove.delete(n); + moved.add(n); + } + } + if (result.length > 0) { + RED.events.emit('nodes:reorder',{ + z: nodes[0].z, + nodes: result + }); + } + return result; + }, + moveNodesBackwards: function(nodes) { + var result = []; + if (!Array.isArray(nodes)) { + nodes = [nodes] + } + // Can only do this for nodes on the same tab. + // Use nodes[0] to get the z + var tabNodes = tabMap[nodes[0].z]; + var toMove = new Set(nodes.filter(function(n) { return n.type !== "group" && n.type !== "subflow" })); + var moved = new Set(); + for (var i = 0; i < tabNodes.length; i++) { + if (toMove.size === 0) { + break; + } + var n = tabNodes[i]; + if (toMove.has(n)) { + // This is a node to move. + if (i > 0 && !moved.has(tabNodes[i-1])) { + // Remove from current position + tabNodes.splice(i,1); + // Add it back one position lower + tabNodes.splice(i-1,0,n); + n._reordered = true; + result.push(n); + } + toMove.delete(n); + moved.add(n); + } + } + if (result.length > 0) { + RED.events.emit('nodes:reorder',{ + z: nodes[0].z, + nodes: result + }); + } + return result; + }, + moveNodesToFront: function(nodes) { + var result = []; + if (!Array.isArray(nodes)) { + nodes = [nodes] + } + // Can only do this for nodes on the same tab. + // Use nodes[0] to get the z + var tabNodes = tabMap[nodes[0].z]; + var toMove = new Set(nodes.filter(function(n) { return n.type !== "group" && n.type !== "subflow" })); + var target = tabNodes.length-1; + for (var i = tabNodes.length-1; i >= 0; i--) { + if (toMove.size === 0) { + break; + } + var n = tabNodes[i]; + if (toMove.has(n)) { + // This is a node to move. + if (i < target) { + // Remove from current position + tabNodes.splice(i,1); + tabNodes.splice(target,0,n); + n._reordered = true; + result.push(n); + } + target--; + toMove.delete(n); + } + } + if (result.length > 0) { + RED.events.emit('nodes:reorder',{ + z: nodes[0].z, + nodes: result + }); + } + return result; + }, + moveNodesToBack: function(nodes) { + var result = []; + if (!Array.isArray(nodes)) { + nodes = [nodes] + } + // Can only do this for nodes on the same tab. + // Use nodes[0] to get the z + var tabNodes = tabMap[nodes[0].z]; + var toMove = new Set(nodes.filter(function(n) { return n.type !== "group" && n.type !== "subflow" })); + var target = 0; + for (var i = 0; i < tabNodes.length; i++) { + if (toMove.size === 0) { + break; + } + var n = tabNodes[i]; + if (toMove.has(n)) { + // This is a node to move. + if (i > target) { + // Remove from current position + tabNodes.splice(i,1); + // Add it back one position lower + tabNodes.splice(target,0,n); + n._reordered = true; + result.push(n); + } + target++; + toMove.delete(n); + } + } + if (result.length > 0) { + RED.events.emit('nodes:reorder',{ + z: nodes[0].z, + nodes: result + }); + } + return result; + }, + getNodes: function(z) { + return tabMap[z]; + }, + clear: function() { + nodes = {}; + tabMap = {}; + }, + eachNode: function(cb) { + var nodeList,i,j; + for (i in subflows) { + if (subflows.hasOwnProperty(i)) { + nodeList = tabMap[i]; + for (j = 0; j < nodeList.length; j++) { + if (cb(nodeList[j]) === false) { + return; + } + } + } + } + for (i = 0; i < workspacesOrder.length; i++) { + nodeList = tabMap[workspacesOrder[i]]; + for (j = 0; j < nodeList.length; j++) { + if (cb(nodeList[j]) === false) { + return; + } + } + } + // Flow nodes that do not have a valid tab/subflow + if (tabMap["_"]) { + nodeList = tabMap["_"]; + for (j = 0; j < nodeList.length; j++) { + if (cb(nodeList[j]) === false) { + return; + } + } + } + }, + filterNodes: function(filter) { + var result = []; + var searchSet = null; + var doZFilter = false; + if (filter.hasOwnProperty("z")) { + if (tabMap.hasOwnProperty(filter.z)) { + searchSet = tabMap[filter.z]; + } else { + doZFilter = true; + } + } + if (searchSet === null) { + searchSet = nodes; + } + + for (var n=0;ndiv:not(.node-input-env-container-row)"); - var height = size.height; - // for (var i=0; idiv.node-input-env-container-row"); - // height -= (parseInt(editorRow.css("marginTop"))+parseInt(editorRow.css("marginBottom"))); - $("ol.red-ui-editor-subflow-env-list").editableList('height',height); + if (this.type === 'subflow') { + $("#node-input-env-container").editableList('height',size.height - 80); + } }, set:{ module: "node-red" @@ -618,27 +890,24 @@ RED.nodes = (function() { function removeSubflow(sf) { if (subflows[sf.id]) { delete subflows[sf.id]; - delete nodeTabMap[sf.id]; + allNodes.removeTab(sf.id); registry.removeNodeType("subflow:"+sf.id); RED.events.emit("subflows:remove",sf); } } function subflowContains(sfid,nodeid) { - for (var i in nodes) { - if (nodes.hasOwnProperty(i)) { - var node = nodes[i]; - if (node.z === sfid) { - var m = /^subflow:(.+)$/.exec(node.type); - if (m) { - if (m[1] === nodeid) { - return true; - } else { - var result = subflowContains(m[1],nodeid); - if (result) { - return true; - } - } + var sfNodes = allNodes.getNodes(sfid); + for (var i = 0; i 0) { + node.credentials = credentialSet; + } + } + } return node; } /** @@ -710,7 +1002,7 @@ RED.nodes = (function() { } if (n.type === 'tab') { - return convertWorkspace(n); + return convertWorkspace(n, { credentials: exportCreds }); } var node = {}; node.id = n.id; @@ -739,8 +1031,10 @@ RED.nodes = (function() { } if (exportCreds) { var credentialSet = {}; - if (/^subflow:/.test(node.type) && n.credentials) { - // A subflow instance node can have arbitrary creds + if ((/^subflow:/.test(node.type) || + (node.type === "group")) && + n.credentials) { + // A subflow instance/group node can have arbitrary creds for (var sfCred in n.credentials) { if (n.credentials.hasOwnProperty(sfCred)) { if (!n.credentials._ || @@ -934,11 +1228,15 @@ RED.nodes = (function() { function createExportableSubflow(id) { var sf = getSubflow(id); - var nodeSet = [sf]; - var sfNodeIds = Object.keys(nodeTabMap[sf.id]||{}); - for (var i=0, l=sfNodeIds.length; i'+RED.keyboard.formatKey(shortcut.key, true)+'').appendTo(link); + } + } menuItems[opt.id] = opt; @@ -276,6 +283,22 @@ RED.menu = (function() { } } + function refreshShortcuts() { + for (var id in menuItems) { + if (menuItems.hasOwnProperty(id)) { + var opt = menuItems[id]; + if (typeof opt.onselect === "string" && opt.shortcutSpan) { + opt.shortcutSpan.remove(); + delete opt.shortcutSpan; + var shortcut = RED.keyboard.getShortcut(opt.onselect); + if (shortcut && shortcut.key) { + opt.shortcutSpan = $(''+RED.keyboard.formatKey(shortcut.key, true)+'').appendTo(opt.link); + } + } + } + } + } + return { init: createMenu, setSelected: setSelected, @@ -284,6 +307,7 @@ RED.menu = (function() { setDisabled: setDisabled, addItem: addItem, removeItem: removeItem, - setAction: setAction + setAction: setAction, + refreshShortcuts: refreshShortcuts } })(); diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/editor.js b/packages/node_modules/@node-red/editor-client/src/js/ui/editor.js index 9cac57872..27be8db63 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/editor.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/editor.js @@ -20,19 +20,16 @@ RED.editor = (function() { var editStack = []; + var buildingEditDialog = false; var editing_node = null; var editing_config_node = null; - var subflowEditor; var customEditTypes = {}; + var editPanes = {}; + var filteredEditPanes = {}; var editTrayWidthCache = {}; - function getCredentialsURL(nodeType, nodeID) { - var dashedType = nodeType.replace(/\s+/g, '-'); - return 'credentials/' + dashedType + "/" + nodeID; - } - /** * Validate a node * @param node - the node being validated @@ -160,7 +157,6 @@ RED.editor = (function() { return valid; } - function validateNodeEditor(node,prefix) { for (var prop in node._def.defaults) { if (node._def.defaults.hasOwnProperty(prop)) { @@ -398,74 +394,107 @@ RED.editor = (function() { } } - /** - * Update the node credentials from the edit form - * @param node - the node containing the credentials - * @param credDefinition - definition of the credentials - * @param prefix - prefix of the input fields - * @return {boolean} whether anything has changed - */ - function updateNodeCredentials(node, credDefinition, prefix) { - var changed = false; - if (!node.credentials) { - node.credentials = {_:{}}; - } else if (!node.credentials._) { - node.credentials._ = {}; - } - - for (var cred in credDefinition) { - if (credDefinition.hasOwnProperty(cred)) { - var input = $("#" + prefix + '-' + cred); - if (input.length > 0) { - var value = input.val(); - if (credDefinition[cred].type == 'password') { - node.credentials['has_' + cred] = (value !== ""); - if (value == '__PWRD__') { - continue; - } - changed = true; - - } - node.credentials[cred] = value; - if (value != node.credentials._[cred]) { - changed = true; - } - } - } - } - return changed; - } - /** * Prepare all of the editor dialog fields + * @param trayBody - the tray body to populate + * @param nodeEditPanes - array of edit pane ids to add to the dialog * @param node - the node being edited * @param definition - the node definition * @param prefix - the prefix to use in the input element ids (node-input|node-config-input) + * @param default - the id of the tab to show by default */ - function prepareEditDialog(node,definition,prefix,done) { - for (var d in definition.defaults) { - if (definition.defaults.hasOwnProperty(d)) { - if (definition.defaults[d].type) { - if (!definition.defaults[d]._type.array) { - var configTypeDef = RED.nodes.getType(definition.defaults[d].type); - if (configTypeDef && configTypeDef.category === 'config') { - if (configTypeDef.exclusive) { - prepareConfigNodeButton(node,d,definition.defaults[d].type,prefix); - } else { - prepareConfigNodeSelect(node,d,definition.defaults[d].type,prefix); - } - } else { - console.log("Unknown type:", definition.defaults[d].type); - preparePropertyEditor(node,d,prefix,definition.defaults); - } - } - } else { - preparePropertyEditor(node,d,prefix,definition.defaults); - } - attachPropertyChangeHandler(node,definition.defaults,d,prefix); - } - } + function prepareEditDialog(trayBody, nodeEditPanes, node, definition, prefix, defaultTab, done) { + var finishedBuilding = false; var completePrepare = function() { + + var editorTabEl = $('
    ').appendTo(trayBody); + var editorContent = $('
    ').appendTo(trayBody); + + var editorTabs = RED.tabs.create({ + element:editorTabEl, + onchange:function(tab) { + editorContent.children().hide(); + tab.content.show(); + if (tab.onchange) { + tab.onchange.call(tab); + } + if (finishedBuilding) { + RED.tray.resize(); + } + }, + collapsible: true, + menu: false + }); + + var activeEditPanes = []; + + nodeEditPanes = nodeEditPanes.slice(); + for (var i in filteredEditPanes) { + if (filteredEditPanes.hasOwnProperty(i)) { + if (filteredEditPanes[i](node)) { + nodeEditPanes.push(i); + } + } + } + + nodeEditPanes.forEach(function(id) { + try { + var editPaneDefinition = editPanes[id]; + if (editPaneDefinition) { + if (typeof editPaneDefinition === 'function') { + editPaneDefinition = editPaneDefinition.call(editPaneDefinition, node); + } + var editPaneContent = $('
    ', {class:"red-ui-tray-content"}).appendTo(editorContent).hide(); + editPaneDefinition.create.call(editPaneDefinition,editPaneContent); + var editTab = { + id: id, + label: editPaneDefinition.label, + name: editPaneDefinition.name, + iconClass: editPaneDefinition.iconClass, + content: editPaneContent, + onchange: function() { + if (editPaneDefinition.show) { + editPaneDefinition.show.call(editPaneDefinition) + } + } + } + editorTabs.addTab(editTab); + activeEditPanes.push(editPaneDefinition); + } else { + console.warn("Unregisted edit pane:",id) + } + } catch(err) { + console.log(id,err); + } + }); + + for (var d in definition.defaults) { + if (definition.defaults.hasOwnProperty(d)) { + if (definition.defaults[d].type) { + if (!definition.defaults[d]._type.array) { + var configTypeDef = RED.nodes.getType(definition.defaults[d].type); + if (configTypeDef && configTypeDef.category === 'config') { + if (configTypeDef.exclusive) { + prepareConfigNodeButton(node,d,definition.defaults[d].type,prefix); + } else { + prepareConfigNodeSelect(node,d,definition.defaults[d].type,prefix); + } + } else { + console.log("Unknown type:", definition.defaults[d].type); + preparePropertyEditor(node,d,prefix,definition.defaults); + } + } + } else { + preparePropertyEditor(node,d,prefix,definition.defaults); + } + attachPropertyChangeHandler(node,definition.defaults,d,prefix); + } + } + + if (!/^subflow:/.test(definition.type)) { + populateCredentialsInputs(node, definition.credentials, node.credentials, prefix); + } + if (definition.oneditprepare) { try { definition.oneditprepare.call(node); @@ -474,6 +503,7 @@ RED.editor = (function() { console.log(err.stack); } } + // Now invoke any change handlers added to the fields - passing true // to prevent full node validation from being triggered each time for (var d in definition.defaults) { @@ -503,22 +533,27 @@ RED.editor = (function() { } } validateNodeEditor(node,prefix); + finishedBuilding = true; + if (defaultTab) { + editorTabs.activateTab(defaultTab); + } if (done) { - done(); + done(activeEditPanes); } } - if (definition.credentials || /^subflow:/.test(definition.type)) { + if (definition.credentials || /^subflow:/.test(definition.type) || node.type === "group" || node.type === "tab") { if (node.credentials) { populateCredentialsInputs(node, definition.credentials, node.credentials, prefix); completePrepare(); } else { - getNodeCredentials(node.type, node.id, function(data) { + var nodeType = node.type; + if (/^subflow:/.test(nodeType)) { + nodeType = "subflow" + } + getNodeCredentials(nodeType, node.id, function(data) { if (data) { node.credentials = data; node.credentials._ = $.extend(true,{},data); - if (!/^subflow:/.test(definition.type)) { - populateCredentialsInputs(node, definition.credentials, node.credentials, prefix); - } } completePrepare(); }); @@ -598,13 +633,6 @@ RED.editor = (function() { $(this).attr("data-i18n",keys.join(";")); }); - if (type === "subflow-template") { - // This is the 'edit properties' dialog for a subflow template - // TODO: this needs to happen later in the dialog open sequence - // so that credentials can be loaded prior to building the form - RED.subflow.buildEditForm(type,node); - } - // Add dummy fields to prevent 'Enter' submitting the form in some // cases, and also prevent browser auto-fill of password // - the elements cannot be hidden otherwise Chrome will ignore them. @@ -617,506 +645,147 @@ RED.editor = (function() { return dialogForm; } - function refreshLabelForm(container,node) { - - var inputPlaceholder = node._def.inputLabels?RED._("editor.defaultLabel"):RED._("editor.noDefaultLabel"); - var outputPlaceholder = node._def.outputLabels?RED._("editor.defaultLabel"):RED._("editor.noDefaultLabel"); - - var inputsDiv = $("#red-ui-editor-node-label-form-inputs"); - var outputsDiv = $("#red-ui-editor-node-label-form-outputs"); - - var inputCount; - var formInputs = $("#node-input-inputs").val(); - if (formInputs === undefined) { - if (node.type === 'subflow') { - inputCount = node.in.length; - } else { - inputCount = node.inputs || node._def.inputs || 0; - } - } else { - inputCount = Math.min(1,Math.max(0,parseInt(formInputs))); - if (isNaN(inputCount)) { - inputCount = 0; - } - } - - var children = inputsDiv.children(); - var childCount = children.length; - if (childCount === 1 && $(children[0]).hasClass('red-ui-editor-node-label-form-none')) { - childCount--; - } - - if (childCount < inputCount) { - if (childCount === 0) { - // remove the 'none' placeholder - $(children[0]).remove(); - } - for (i = childCount;i inputCount) { - for (i=inputCount;i B.__label__) { + return 1; + } + return 0; + } + + function updateConfigNodeSelect(name,type,value,prefix) { + // if prefix is null, there is no config select to update + if (prefix) { + var button = $("#"+prefix+"-edit-"+name); + if (button.length) { + if (value) { + button.text(RED._("editor.configEdit")); } else { - row.detach(); + button.text(RED._("editor.configAdd")); } - if (outputMap[p] !== -1) { - outputCount++; - rows.push({i:parseInt(outputMap[p]),r:row}); - } - }); - rows.sort(function(A,B) { - return A.i-B.i; - }) - rows.forEach(function(r,i) { - r.r.find("label").text((i+1)+"."); - r.r.appendTo(outputsDiv); - }) - if (rows.length === 0) { - buildLabelRow("output",i,"").appendTo(outputsDiv); + $("#"+prefix+"-"+name).val(value); } else { - } - } else { - outputCount = Math.max(0,parseInt(formOutputs)); - } - children = outputsDiv.children(); - childCount = children.length; - if (childCount === 1 && $(children[0]).hasClass('red-ui-editor-node-label-form-none')) { - childCount--; - } - if (childCount < outputCount) { - if (childCount === 0) { - // remove the 'none' placeholder - $(children[0]).remove(); - } - for (i = childCount;i outputCount) { - for (i=outputCount;i',{class:"red-ui-editor-node-label-form-row"}); - if (type === undefined) { - $('').text(RED._("editor.noDefaultLabel")).appendTo(result); - result.addClass("red-ui-editor-node-label-form-none"); - } else { - result.addClass(""); - var id = "red-ui-editor-node-label-form-"+type+"-"+index; - $('