From 4132fb79a661aaed78560001415959c285bc4d3c Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Thu, 26 Aug 2021 21:16:40 +0100 Subject: [PATCH 1/2] Allow nodes to be raised/lowered in the workspace --- .../@node-red/editor-client/src/js/history.js | 17 +- .../@node-red/editor-client/src/js/nodes.js | 456 +++++++++++++----- .../src/js/ui/tab-info-outliner.js | 16 + .../editor-client/src/js/ui/view-tools.js | 42 ++ .../@node-red/editor-client/src/js/ui/view.js | 28 +- .../editor-client/src/js/ui/workspaces.js | 9 +- 6 files changed, 450 insertions(+), 118 deletions(-) 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 21823ae26..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 = { 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..df95c6392 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 = []; @@ -209,6 +208,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;n 0) { + var z = nodesToMove[0].z; + var existingOrder = RED.nodes.getNodeOrder(z); + var movedNodes; + if (dir === "forwards") { + movedNodes = RED.nodes.moveNodesForwards(nodesToMove); + } else if (dir === "backwards") { + movedNodes = RED.nodes.moveNodesBacks(nodesToMove); + } else if (dir === "front") { + movedNodes = RED.nodes.moveNodesToFront(nodesToMove); + } else if (dir === "back") { + movedNodes = RED.nodes.moveNodesToBack(nodesToMove); + } + if (movedNodes.length > 0) { + var newOrder = RED.nodes.getNodeOrder(z); + RED.history.push({t:"reorder",nodes:{z:z,from:existingOrder,to:newOrder},dirty:RED.nodes.dirty()}); + RED.nodes.dirty(true); + RED.view.redraw(true); + } + } + } + } return { init: function() { @@ -710,6 +745,12 @@ RED.view.tools = (function() { RED.actions.add("core:move-selection-down", function() { moveSelection(0,1);}); RED.actions.add("core:move-selection-left", function() { moveSelection(-1,0);}); + RED.actions.add("core:move-selection-forwards", function() { reorderSelection('forwards') }) + RED.actions.add("core:move-selection-backwards", function() { reorderSelection('backwards') }) + RED.actions.add("core:move-selection-to-front", function() { reorderSelection('front') }) + RED.actions.add("core:move-selection-to-back", function() { reorderSelection('back') }) + + RED.actions.add("core:step-selection-up", function() { moveSelection(0,-RED.view.gridSize());}); RED.actions.add("core:step-selection-right", function() { moveSelection(RED.view.gridSize(),0);}); RED.actions.add("core:step-selection-down", function() { moveSelection(0,RED.view.gridSize());}); @@ -743,6 +784,7 @@ RED.view.tools = (function() { RED.actions.add("core:distribute-selection-vertically", function() { distributeSelection('v') }) + // RED.actions.add("core:add-node", function() { addNode() }) }, /** diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/view.js b/packages/node_modules/@node-red/editor-client/src/js/ui/view.js index 51f4428f6..40d8af2e7 100755 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/view.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/view.js @@ -663,14 +663,17 @@ RED.view = (function() { var activeWorkspace = RED.workspaces.active(); activeNodes = RED.nodes.filterNodes({z:activeWorkspace}); - + activeNodes.forEach(function(n,i) { + n._index = i; + }) activeLinks = RED.nodes.filterLinks({ source:{z:activeWorkspace}, target:{z:activeWorkspace} }); activeGroups = RED.nodes.groups(activeWorkspace)||[]; - activeGroups.forEach(function(g) { + activeGroups.forEach(function(g,i) { + g._index = i; if (g.g) { g._root = g.g; g._depth = 1; @@ -703,7 +706,8 @@ RED.view = (function() { if (a._root === b._root) { return a._depth - b._depth; } else { - return a._root.localeCompare(b._root); + // return a._root.localeCompare(b._root); + return a._index - b._index; } }); @@ -712,7 +716,8 @@ RED.view = (function() { if (a._root === b._root) { return a._depth - b._depth; } else { - return a._root.localeCompare(b._root); + return a._index - b._index; + // return a._root.localeCompare(b._root); } }) } @@ -3816,7 +3821,6 @@ RED.view = (function() { .attr("class", "red-ui-flow-node red-ui-flow-node-group") .classed("red-ui-flow-subflow", activeSubflow != null); - nodeEnter.each(function(d,i) { this.__outputs__ = []; this.__inputs__ = []; @@ -3962,7 +3966,12 @@ RED.view = (function() { RED.hooks.trigger("viewAddNode",{node:d,el:this}) }); + var nodesReordered = false; node.each(function(d,i) { + if (d._reordered) { + nodesReordered = true; + delete d._reordered; + } if (d.dirty) { var self = this; var thisNode = d3.select(this); @@ -4270,6 +4279,13 @@ RED.view = (function() { RED.hooks.trigger("viewRedrawNode",{node:d,el:this}) }); + + if (nodesReordered) { + node.sort(function(a,b) { + return a._index - b._index; + }) + } + var link = linkLayer.selectAll(".red-ui-flow-link").data( activeLinks, function(d) { @@ -4508,7 +4524,7 @@ RED.view = (function() { if (a._root === b._root) { return a._depth - b._depth; } else { - return a._root.localeCompare(b._root); + return a._index - b._index; } }) } diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/workspaces.js b/packages/node_modules/@node-red/editor-client/src/js/ui/workspaces.js index a7a4e024f..336b243c5 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/workspaces.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/workspaces.js @@ -293,7 +293,14 @@ RED.workspaces = (function() { } }, onreorder: function(oldOrder, newOrder) { - RED.history.push({t:'reorder',order:oldOrder,dirty:RED.nodes.dirty()}); + RED.history.push({ + t:'reorder', + workspaces: { + from:oldOrder, + to:newOrder + }, + dirty:RED.nodes.dirty() + }); RED.nodes.dirty(true); setWorkspaceOrder(newOrder); }, From 9dc5ae21c4a8da55cfa77b565a6d7eb18c868280 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Tue, 7 Sep 2021 12:04:58 +0100 Subject: [PATCH 2/2] Add menu options for raise/lower actions --- .../editor-client/locales/en-US/editor.json | 6 +++++- .../@node-red/editor-client/src/js/red.js | 21 ++++++++++++------- .../editor-client/src/js/ui/view-tools.js | 2 +- .../@node-red/editor-client/src/js/ui/view.js | 14 +++++++++++++ 4 files changed, 33 insertions(+), 10 deletions(-) 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 adeae0c2d..63ea0efe9 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 @@ -125,7 +125,11 @@ "alignMiddle":"Align to middle", "alignBottom":"Align to bottom", "distributeHorizontally":"Distribute horizontally", - "distributeVertically":"Distribute vertically" + "distributeVertically":"Distribute vertically", + "moveToBack":"Move to back", + "moveToFront":"Move to front", + "moveBackwards":"Move backwards", + "moveForwards":"Move forwards" } }, "actions": { diff --git a/packages/node_modules/@node-red/editor-client/src/js/red.js b/packages/node_modules/@node-red/editor-client/src/js/red.js index 171e3124b..ad68ea612 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/red.js +++ b/packages/node_modules/@node-red/editor-client/src/js/red.js @@ -584,16 +584,21 @@ var RED = (function() { ]}); menuOptions.push({id:"menu-item-arrange-menu", label:RED._("menu.label.arrange"), options: [ - {id: "menu-item-view-tools-align-left", label:RED._("menu.label.alignLeft"), onselect: "core:align-selection-to-left"}, - {id: "menu-item-view-tools-align-center", label:RED._("menu.label.alignCenter"), onselect: "core:align-selection-to-center"}, - {id: "menu-item-view-tools-align-right", label:RED._("menu.label.alignRight"), onselect: "core:align-selection-to-right"}, + {id: "menu-item-view-tools-move-to-back", label:RED._("menu.label.moveToBack"), disabled: true, onselect: "core:move-selection-to-back"}, + {id: "menu-item-view-tools-move-to-front", label:RED._("menu.label.moveToFront"), disabled: true, onselect: "core:move-selection-to-front"}, + {id: "menu-item-view-tools-move-backwards", label:RED._("menu.label.moveBackwards"), disabled: true, onselect: "core:move-selection-backwards"}, + {id: "menu-item-view-tools-move-forwards", label:RED._("menu.label.moveForwards"), disabled: true, onselect: "core:move-selection-forwards"}, null, - {id: "menu-item-view-tools-align-top", label:RED._("menu.label.alignTop"), onselect: "core:align-selection-to-top"}, - {id: "menu-item-view-tools-align-middle", label:RED._("menu.label.alignMiddle"), onselect: "core:align-selection-to-middle"}, - {id: "menu-item-view-tools-align-bottom", label:RED._("menu.label.alignBottom"), onselect: "core:align-selection-to-bottom"}, + {id: "menu-item-view-tools-align-left", label:RED._("menu.label.alignLeft"), disabled: true, onselect: "core:align-selection-to-left"}, + {id: "menu-item-view-tools-align-center", label:RED._("menu.label.alignCenter"), disabled: true, onselect: "core:align-selection-to-center"}, + {id: "menu-item-view-tools-align-right", label:RED._("menu.label.alignRight"), disabled: true, onselect: "core:align-selection-to-right"}, null, - {id: "menu-item-view-tools-distribute-horizontally", label:RED._("menu.label.distributeHorizontally"), onselect: "core:distribute-selection-horizontally"}, - {id: "menu-item-view-tools-distribute-veritcally", label:RED._("menu.label.distributeVertically"), onselect: "core:distribute-selection-vertically"} + {id: "menu-item-view-tools-align-top", label:RED._("menu.label.alignTop"), disabled: true, onselect: "core:align-selection-to-top"}, + {id: "menu-item-view-tools-align-middle", label:RED._("menu.label.alignMiddle"), disabled: true, onselect: "core:align-selection-to-middle"}, + {id: "menu-item-view-tools-align-bottom", label:RED._("menu.label.alignBottom"), disabled: true, onselect: "core:align-selection-to-bottom"}, + null, + {id: "menu-item-view-tools-distribute-horizontally", label:RED._("menu.label.distributeHorizontally"), disabled: true, onselect: "core:distribute-selection-horizontally"}, + {id: "menu-item-view-tools-distribute-veritcally", label:RED._("menu.label.distributeVertically"), disabled: true, onselect: "core:distribute-selection-vertically"} ]}); menuOptions.push(null); diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/view-tools.js b/packages/node_modules/@node-red/editor-client/src/js/ui/view-tools.js index 2c59fc352..2fba37e88 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/view-tools.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/view-tools.js @@ -709,7 +709,7 @@ RED.view.tools = (function() { if (dir === "forwards") { movedNodes = RED.nodes.moveNodesForwards(nodesToMove); } else if (dir === "backwards") { - movedNodes = RED.nodes.moveNodesBacks(nodesToMove); + movedNodes = RED.nodes.moveNodesBackwards(nodesToMove); } else if (dir === "front") { movedNodes = RED.nodes.moveNodesToFront(nodesToMove); } else if (dir === "back") { diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/view.js b/packages/node_modules/@node-red/editor-client/src/js/ui/view.js index 40d8af2e7..d154fe865 100755 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/view.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/view.js @@ -504,9 +504,23 @@ RED.view = (function() { RED.events.on("view:selection-changed", function(selection) { var hasSelection = (selection.nodes && selection.nodes.length > 0); + var hasMultipleSelection = hasSelection && selection.nodes.length > 1; RED.menu.setDisabled("menu-item-edit-cut",!hasSelection); RED.menu.setDisabled("menu-item-edit-copy",!hasSelection); RED.menu.setDisabled("menu-item-edit-select-connected",!hasSelection); + RED.menu.setDisabled("menu-item-view-tools-move-to-back",!hasSelection); + RED.menu.setDisabled("menu-item-view-tools-move-to-front",!hasSelection); + RED.menu.setDisabled("menu-item-view-tools-move-backwards",!hasSelection); + RED.menu.setDisabled("menu-item-view-tools-move-forwards",!hasSelection); + + RED.menu.setDisabled("menu-item-view-tools-align-left",!hasMultipleSelection); + RED.menu.setDisabled("menu-item-view-tools-align-center",!hasMultipleSelection); + RED.menu.setDisabled("menu-item-view-tools-align-right",!hasMultipleSelection); + RED.menu.setDisabled("menu-item-view-tools-align-top",!hasMultipleSelection); + RED.menu.setDisabled("menu-item-view-tools-align-middle",!hasMultipleSelection); + RED.menu.setDisabled("menu-item-view-tools-align-bottom",!hasMultipleSelection); + RED.menu.setDisabled("menu-item-view-tools-distribute-horizontally",!hasMultipleSelection); + RED.menu.setDisabled("menu-item-view-tools-distribute-veritcally",!hasMultipleSelection); }) RED.actions.add("core:delete-selection",deleteSelection);