From 154a4e23ddeb0827c994843cb498a659f5cac556 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Tue, 11 Jan 2022 14:11:55 +0000 Subject: [PATCH] Add delete-selection-and-reconnect action --- .../editor-client/src/js/keymap.json | 2 + .../@node-red/editor-client/src/js/nodes.js | 144 +++++++++++++++++- .../editor-client/src/js/ui/group.js | 8 +- .../@node-red/editor-client/src/js/ui/view.js | 136 +++++------------ 4 files changed, 191 insertions(+), 99 deletions(-) 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 766f6bd9f..f1f3f46e7 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 @@ -38,7 +38,9 @@ }, "red-ui-workspace": { "backspace": "core:delete-selection", + "ctrl-backspace": "core:delete-selection-and-reconnect", "delete": "core:delete-selection", + "ctrl-delete": "core:delete-selection-and-reconnect", "enter": "core:edit-selected-node", "ctrl-enter": "core:go-to-selection", "ctrl-c": "core:copy-selection-to-internal-clipboard", 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 8feb741ac..78b9deae6 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 @@ -15,6 +15,9 @@ **/ RED.nodes = (function() { + var PORT_TYPE_INPUT = 1; + var PORT_TYPE_OUTPUT = 0; + var node_defs = {}; var linkTabMap = {}; @@ -2458,6 +2461,144 @@ RED.nodes = (function() { return helpContent; } + function getNodeIslands(nodes) { + var selectedNodes = new Set(nodes); + // Maps node => island index + var nodeToIslandIndex = new Map(); + // Maps island index => [nodes in island] + var islandIndexToNodes = new Map(); + var internalLinks = new Set(); + nodes.forEach((node, index) => { + nodeToIslandIndex.set(node,index); + islandIndexToNodes.set(index, [node]); + var inboundLinks = RED.nodes.getNodeLinks(node, PORT_TYPE_INPUT); + var outboundLinks = RED.nodes.getNodeLinks(node, PORT_TYPE_OUTPUT); + inboundLinks.forEach(l => { + if (selectedNodes.has(l.source)) { + internalLinks.add(l) + } + }) + outboundLinks.forEach(l => { + if (selectedNodes.has(l.target)) { + internalLinks.add(l) + } + }) + }) + + internalLinks.forEach(l => { + let source = l.source; + let target = l.target; + if (nodeToIslandIndex.get(source) !== nodeToIslandIndex.get(target)) { + let sourceIsland = nodeToIslandIndex.get(source); + let islandToMove = nodeToIslandIndex.get(target); + let nodesToMove = islandIndexToNodes.get(islandToMove); + nodesToMove.forEach(n => { + nodeToIslandIndex.set(n,sourceIsland); + islandIndexToNodes.get(sourceIsland).push(n); + }) + islandIndexToNodes.delete(islandToMove); + } + }) + const result = []; + islandIndexToNodes.forEach((nodes,index) => { + result.push(nodes); + }) + return result; + } + + function detachNodes(nodes) { + let allSelectedNodes = []; + nodes.forEach(node => { + if (node.type === 'group') { + let groupNodes = RED.group.getNodes(node,true,true); + allSelectedNodes = allSelectedNodes.concat(groupNodes); + } else { + allSelectedNodes.push(node); + } + }) + if (allSelectedNodes.length > 0 ) { + const nodeIslands = RED.nodes.getNodeIslands(allSelectedNodes); + let removedLinks = []; + let newLinks = []; + let createdLinkIds = new Set(); + + nodeIslands.forEach(nodes => { + let selectedNodes = new Set(nodes); + let allInboundLinks = []; + let allOutboundLinks = []; + // Identify links that enter or exit this island of nodes + nodes.forEach(node => { + var inboundLinks = RED.nodes.getNodeLinks(node, PORT_TYPE_INPUT); + var outboundLinks = RED.nodes.getNodeLinks(node, PORT_TYPE_OUTPUT); + inboundLinks.forEach(l => { + if (!selectedNodes.has(l.source)) { + allInboundLinks.push(l) + } + }) + outboundLinks.forEach(l => { + if (!selectedNodes.has(l.target)) { + allOutboundLinks.push(l) + } + }) + }); + + + // Identify the links to restore + allInboundLinks.forEach(inLink => { + // For Each inbound link, + // - get source node. + // - trace through to all outbound links + let sourceNode = inLink.source; + let targetNodes = new Set(); + let visited = new Set(); + let stack = [inLink.target]; + while (stack.length > 0) { + let node = stack.pop(stack); + visited.add(node) + let links = RED.nodes.getNodeLinks(node, PORT_TYPE_OUTPUT); + links.forEach(l => { + if (visited.has(l.target)) { + return + } + visited.add(l.target); + if (selectedNodes.has(l.target)) { + // internal link + stack.push(l.target) + } else { + targetNodes.add(l.target) + } + }) + } + targetNodes.forEach(target => { + let linkId = `${sourceNode.id}[${inLink.sourcePort}] -> ${target.id}` + if (!createdLinkIds.has(linkId)) { + createdLinkIds.add(linkId); + let link = { + source: sourceNode, + sourcePort: inLink.sourcePort, + target: target + } + let existingLinks = RED.nodes.filterLinks(link) + if (existingLinks.length === 0) { + newLinks.push(link); + } + } + }) + }) + + // 2. delete all those links + allInboundLinks.forEach(l => { RED.nodes.removeLink(l); removedLinks.push(l)}) + allOutboundLinks.forEach(l => { RED.nodes.removeLink(l); removedLinks.push(l)}) + }) + + newLinks.forEach(l => RED.nodes.addLink(l)); + return { + newLinks, + removedLinks + } + } + } + return { init: function() { RED.events.on("registry:node-type-added",function(type) { @@ -2539,7 +2680,7 @@ RED.nodes = (function() { add: addNode, remove: removeNode, clear: clear, - + detachNodes: detachNodes, moveNodesForwards: moveNodesForwards, moveNodesBackwards: moveNodesBackwards, moveNodesToFront: moveNodesToFront, @@ -2638,6 +2779,7 @@ RED.nodes = (function() { getAllFlowNodes: getAllFlowNodes, getAllUpstreamNodes: getAllUpstreamNodes, getAllDownstreamNodes: getAllDownstreamNodes, + getNodeIslands: getNodeIslands, createExportableNodeSet: createExportableNodeSet, createCompleteNodeSet: createCompleteNodeSet, updateConfigNodeUsers: updateConfigNodeUsers, diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/group.js b/packages/node_modules/@node-red/editor-client/src/js/ui/group.js index a6e2492fd..15c48cc81 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/group.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/group.js @@ -590,12 +590,14 @@ RED.group = (function() { markDirty(group); } - function getNodes(group,recursive) { + function getNodes(group,recursive,excludeGroup) { var nodes = []; group.nodes.forEach(function(n) { - nodes.push(n); + if (n.type !== 'group' || !excludeGroup) { + nodes.push(n); + } if (recursive && n.type === 'group') { - nodes = nodes.concat(getNodes(n,recursive)) + nodes = nodes.concat(getNodes(n,recursive,excludeGroup)) } }) return nodes; 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 22197b330..c64e9af84 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 @@ -562,6 +562,7 @@ RED.view = (function() { }) RED.actions.add("core:delete-selection",deleteSelection); + RED.actions.add("core:delete-selection-and-reconnect",function() { deleteSelection(true) }); RED.actions.add("core:edit-selected-node",editSelection); RED.actions.add("core:go-to-selection",function() { if (movingSet.length() > 0) { @@ -1916,9 +1917,11 @@ RED.view = (function() { return; } if (mouse_mode === RED.state.DETACHED_DRAGGING) { - var node = movingSet.get(0); - node.n.x = node.ox; - node.n.y = node.oy; + for (var j=0;j 0) { + historyEvents.push({ t:'add', links: addedLinks }) + } + removedLinks = removedLinks.concat(reconnectResult.removedLinks) + } var startDirty = RED.nodes.dirty(); var startChanged = false; @@ -2310,7 +2323,6 @@ RED.view = (function() { } } - var linkRemoveHistoryEvents = []; if (selectedLinks.length() > 0) { selectedLinks.forEach(function(link) { if (link.link) { @@ -2318,31 +2330,22 @@ RED.view = (function() { var targetId = link.target.id; var sourceIdIndex = link.target.links.indexOf(sourceId); var targetIdIndex = link.source.links.indexOf(targetId); - - linkRemoveHistoryEvents.push({ - t:"multi", - events: [ - { - t: "edit", - node: link.source, - changed: link.source.changed, - changes: { - links: $.extend(true,{},{v:link.source.links}).v - } - }, - { - t: "edit", - node: link.target, - changed: link.target.changed, - changes: { - links: $.extend(true,{},{v:link.target.links}).v - } - } - - ], - dirty:startDirty + historyEvents.push({ + t: "edit", + node: link.source, + changed: link.source.changed, + changes: { + links: $.extend(true,{},{v:link.source.links}).v + } + }) + historyEvents.push({ + t: "edit", + node: link.target, + changed: link.target.changed, + changes: { + links: $.extend(true,{},{v:link.target.links}).v + } }) - link.source.changed = true; link.target.changed = true; link.target.links.splice(sourceIdIndex,1); @@ -2373,11 +2376,11 @@ RED.view = (function() { if (removedSubflowStatus) { historyEvent.subflow.status = removedSubflowStatus; } - if (linkRemoveHistoryEvents.length > 0) { - linkRemoveHistoryEvents.unshift(historyEvent); + if (historyEvents.length > 0) { + historyEvents.unshift(historyEvent); RED.history.push({ t:"multi", - events: linkRemoveHistoryEvents + events: historyEvents }) } else { RED.history.push(historyEvent); @@ -2460,80 +2463,23 @@ RED.view = (function() { } } + function detachSelectedNodes() { var selection = RED.view.selection(); - if (selection.nodes && selection.nodes.length === 1) { - // var selectedNodes = new Set(selection.nodes); - // - // var allInboundLinks = []; - // var allOutboundLinks = []; - // - // selection.nodes.forEach(node => { - // var inboundLinks = RED.nodes.getNodeLinks(node, PORT_TYPE_INPUT); - // var outboundLinks = RED.nodes.getNodeLinks(node, PORT_TYPE_OUTPUT); - // inboundLinks.forEach(l => { - // if (!selectedNodes.has(l.source)) { - // allInboundLinks.push(l); - // } - // }) - // outboundLinks.forEach(l => { - // if (!selectedNodes.has(l.target)) { - // allOutboundLinks.push(l); - // } - // }) - // }) - - - var node = selection.nodes[0]; - // 1. find all the links attached to this node - var inboundLinks = RED.nodes.getNodeLinks(node, PORT_TYPE_INPUT); - var outboundLinks = RED.nodes.getNodeLinks(node, PORT_TYPE_OUTPUT); - - - // 2. delete all those links - inboundLinks.forEach(l => RED.nodes.removeLink(l)) - outboundLinks.forEach(l => RED.nodes.removeLink(l)) - - // 3. restore links from all source nodes to all target nodes - - var newLinks = []; - var createdLinkIds = new Set(); - - inboundLinks.forEach(inLink => { - outboundLinks.forEach(outLink => { - var linkId = inLink.source.id+":"+inLink.sourcePort+":"+outLink.target.id - if (!createdLinkIds.has(linkId)) { - createdLinkIds.add(linkId); - var link = { - source: inLink.source, - sourcePort: inLink.sourcePort, - target: outLink.target - }; - var existingLinks = RED.nodes.filterLinks(link) - if (existingLinks.length === 0) { - newLinks.push(link); - RED.nodes.addLink(link); - } - } - }) - }) - - var oldLinks = inboundLinks.concat(outboundLinks); - - if (oldLinks.length) { + if (selection.nodes) { + const {newLinks, removedLinks} = RED.nodes.detachNodes(selection.nodes); + if (removedLinks.length || newLinks.length) { RED.history.push({ t: "multi", events: [ - { t:'delete', links: oldLinks }, + { t:'delete', links: removedLinks }, { t:'add', links: newLinks } ], dirty: RED.nodes.dirty() }) RED.nodes.dirty(true) } - - - prepareDrag([node.x,node.y]); + prepareDrag([selection.nodes[0].x,selection.nodes[0].y]); mouse_mode = RED.state.DETACHED_DRAGGING; RED.view.redraw(true); }