1
0
mirror of https://github.com/node-red/node-red.git synced 2023-10-10 13:36:53 +02:00

Merge pull request #4079 from node-red/group-rework

Complete overhaul of Group UX
This commit is contained in:
Nick O'Leary 2023-03-02 15:27:17 +00:00 committed by GitHub
commit e5054d306e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 644 additions and 863 deletions

View File

@ -378,7 +378,8 @@ RED.history = (function() {
if (ev.addToGroup) { if (ev.addToGroup) {
RED.group.removeFromGroup(ev.addToGroup,ev.nodes.map(function(n) { return n.n }),false); RED.group.removeFromGroup(ev.addToGroup,ev.nodes.map(function(n) { return n.n }),false);
inverseEv.removeFromGroup = ev.addToGroup; inverseEv.removeFromGroup = ev.addToGroup;
} else if (ev.removeFromGroup) { }
if (ev.removeFromGroup) {
RED.group.addToGroup(ev.removeFromGroup,ev.nodes.map(function(n) { return n.n })); RED.group.addToGroup(ev.removeFromGroup,ev.nodes.map(function(n) { return n.n }));
inverseEv.addToGroup = ev.removeFromGroup; inverseEv.addToGroup = ev.removeFromGroup;
} }
@ -649,6 +650,12 @@ RED.history = (function() {
ev.groups[i].nodes = []; ev.groups[i].nodes = [];
RED.nodes.addGroup(ev.groups[i]); RED.nodes.addGroup(ev.groups[i]);
RED.group.addToGroup(ev.groups[i],nodes); RED.group.addToGroup(ev.groups[i],nodes);
if (ev.groups[i].g) {
const parentGroup = RED.nodes.group(ev.groups[i].g)
if (parentGroup) {
RED.group.addToGroup(parentGroup, ev.groups[i])
}
}
} }
} }
} else if (ev.t == "addToGroup") { } else if (ev.t == "addToGroup") {

View File

@ -71,7 +71,6 @@ RED.nodes = (function() {
} }
}; };
var exports = { var exports = {
setModulePendingUpdated: function(module,version) { setModulePendingUpdated: function(module,version) {
moduleList[module].pending_version = version; moduleList[module].pending_version = version;
@ -252,6 +251,42 @@ RED.nodes = (function() {
// Set of object ids of things added to a tab after initial import // Set of object ids of things added to a tab after initial import
var addedDirtyObjects = new Set() var addedDirtyObjects = new Set()
function changeCollectionDepth(tabNodes, toMove, direction, singleStep) {
const result = []
const moved = new Set();
const startIndex = direction ? tabNodes.length - 1 : 0
const endIndex = direction ? -1 : tabNodes.length
const step = direction ? -1 : 1
let target = startIndex // Only used for all-the-way moves
for (let i = startIndex; i != endIndex; i += step) {
if (toMove.size === 0) {
break;
}
const n = tabNodes[i]
if (toMove.has(n)) {
if (singleStep) {
if (i !== startIndex && !moved.has(tabNodes[i - step])) {
tabNodes.splice(i, 1)
tabNodes.splice(i - step, 0, n)
n._reordered = true
result.push(n)
}
} else {
if (i !== target) {
tabNodes.splice(i, 1)
tabNodes.splice(target, 0, n)
n._reordered = true
result.push(n)
}
target += step
}
toMove.delete(n);
moved.add(n);
}
}
return result
}
var api = { var api = {
addTab: function(id) { addTab: function(id) {
tabMap[id] = []; tabMap[id] = [];
@ -326,152 +361,54 @@ RED.nodes = (function() {
n.z = newZ; n.z = newZ;
api.addNode(n) api.addNode(n)
}, },
moveNodesForwards: function(nodes) { /**
var result = []; * @param {array} nodes
* @param {boolean} direction true:forwards false:back
* @param {boolean} singleStep true:single-step false:all-the-way
*/
changeDepth: function(nodes, direction, singleStep) {
if (!Array.isArray(nodes)) { if (!Array.isArray(nodes)) {
nodes = [nodes] nodes = [nodes]
} }
// Can only do this for nodes on the same tab. let result = []
// Use nodes[0] to get the z const tabNodes = tabMap[nodes[0].z];
var tabNodes = tabMap[nodes[0].z]; const toMove = new Set(nodes.filter(function(n) { return n.type !== "group" && n.type !== "subflow" }));
var toMove = new Set(nodes.filter(function(n) { return n.type !== "group" && n.type !== "subflow" })); if (toMove.size > 0) {
var moved = new Set(); result = result.concat(changeCollectionDepth(tabNodes, toMove, direction, singleStep))
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) { if (result.length > 0) {
RED.events.emit('nodes:reorder',{ RED.events.emit('nodes:reorder',{
z: nodes[0].z, z: nodes[0].z,
nodes: result nodes: result
}); });
} }
return result; }
const groupNodes = groupsByZ[nodes[0].z] || []
const groupsToMove = new Set(nodes.filter(function(n) { return n.type === 'group'}))
if (groupsToMove.size > 0) {
const groupResult = changeCollectionDepth(groupNodes, groupsToMove, direction, singleStep)
if (groupResult.length > 0) {
result = result.concat(groupResult)
RED.events.emit('groups:reorder',{
z: nodes[0].z,
nodes: groupResult
});
}
}
RED.view.redraw(true)
return result
},
moveNodesForwards: function(nodes) {
return api.changeDepth(nodes, true, true)
}, },
moveNodesBackwards: function(nodes) { moveNodesBackwards: function(nodes) {
var result = []; return api.changeDepth(nodes, false, true)
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) { moveNodesToFront: function(nodes) {
var result = []; return api.changeDepth(nodes, true, false)
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) { moveNodesToBack: function(nodes) {
var result = []; return api.changeDepth(nodes, false, false)
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) { getNodes: function(z) {
return tabMap[z]; return tabMap[z];
@ -571,7 +508,7 @@ RED.nodes = (function() {
return result; return result;
}, },
getNodeOrder: function(z) { getNodeOrder: function(z) {
return tabMap[z].map(function(n) { return n.id }) return (groupsByZ[z] || []).concat(tabMap[z]).map(n => n.id)
}, },
setNodeOrder: function(z, order) { setNodeOrder: function(z, order) {
var orderMap = {}; var orderMap = {};
@ -583,6 +520,11 @@ RED.nodes = (function() {
B._reordered = true; B._reordered = true;
return orderMap[A.id] - orderMap[B.id]; return orderMap[A.id] - orderMap[B.id];
}) })
if (groupsByZ[z]) {
groupsByZ[z].sort(function(A,B) {
return orderMap[A.id] - orderMap[B.id];
})
}
}, },
/** /**
* Update our records if an object is dirty or not * Update our records if an object is dirty or not
@ -2738,6 +2680,10 @@ RED.nodes = (function() {
delete groups[group.id]; delete groups[group.id];
RED.events.emit("groups:remove",group); RED.events.emit("groups:remove",group);
} }
function getGroupOrder(z) {
const groups = groupsByZ[z]
return groups.map(g => g.id)
}
function addJunction(junction) { function addJunction(junction) {
if (!junction.__isProxy__) { if (!junction.__isProxy__) {

View File

@ -128,17 +128,24 @@ RED.contextMenu = (function () {
options: [ options: [
{ onselect: 'core:group-selection' }, { onselect: 'core:group-selection' },
{ onselect: 'core:ungroup-selection', disabled: !hasGroup }, { onselect: 'core:ungroup-selection', disabled: !hasGroup },
null,
{ onselect: 'core:copy-group-style', disabled: !hasGroup },
{ onselect: 'core:paste-group-style', disabled: !hasGroup}
] ]
}) })
if (hasGroup) {
menuItems[menuItems.length - 1].options.push(
{ onselect: 'core:merge-selection-to-group', label: RED._("menu.label.groupMergeSelection") }
)
}
if (canRemoveFromGroup) { if (canRemoveFromGroup) {
menuItems[menuItems.length - 1].options.push( menuItems[menuItems.length - 1].options.push(
null,
{ onselect: 'core:remove-selection-from-group', label: RED._("menu.label.groupRemoveSelection") } { onselect: 'core:remove-selection-from-group', label: RED._("menu.label.groupRemoveSelection") }
) )
} }
menuItems[menuItems.length - 1].options.push(
null,
{ onselect: 'core:copy-group-style', disabled: !hasGroup },
{ onselect: 'core:paste-group-style', disabled: !hasGroup}
)
} }
if (canEdit && hasMultipleSelection) { if (canEdit && hasMultipleSelection) {
menuItems.push({ menuItems.push({

View File

@ -325,7 +325,7 @@ RED.group = (function() {
var selection = RED.view.selection(); var selection = RED.view.selection();
if (selection.nodes) { if (selection.nodes) {
var newSelection = []; var newSelection = [];
groups = selection.nodes.filter(function(n) { return n.type === "group" }); let groups = selection.nodes.filter(function(n) { return n.type === "group" });
var historyEvent = { var historyEvent = {
t:"ungroup", t:"ungroup",
@ -473,10 +473,18 @@ RED.group = (function() {
if (nodes.length === 0) { if (nodes.length === 0) {
return; return;
} }
if (nodes.filter(function(n) { return n.type === "subflow" }).length > 0) { const existingGroup = nodes[0].g
for (let i = 0; i < nodes.length; i++) {
const n = nodes[i]
if (n.type === 'subflow') {
RED.notify(RED._("group.errors.cannotAddSubflowPorts"),"error"); RED.notify(RED._("group.errors.cannotAddSubflowPorts"),"error");
return; return;
} }
if (n.g !== existingGroup) {
console.warn("Cannot add nooes with different z properties")
return
}
}
// nodes is an array // nodes is an array
// each node must be on the same tab (z) // each node must be on the same tab (z)
var group = { var group = {
@ -495,6 +503,10 @@ RED.group = (function() {
group.z = nodes[0].z; group.z = nodes[0].z;
group = RED.nodes.addGroup(group); group = RED.nodes.addGroup(group);
if (existingGroup) {
addToGroup(RED.nodes.group(existingGroup), group)
}
try { try {
addToGroup(group,nodes); addToGroup(group,nodes);
} catch(err) { } catch(err) {
@ -518,7 +530,7 @@ RED.group = (function() {
if (!z) { if (!z) {
z = n.z; z = n.z;
} else if (z !== n.z) { } else if (z !== n.z) {
throw new Error("Cannot add nooes with different z properties") throw new Error("Cannot add nodes with different z properties")
} }
if (n.g) { if (n.g) {
// This is already in a group. // This is already in a group.
@ -535,14 +547,10 @@ RED.group = (function() {
throw new Error(RED._("group.errors.cannotCreateDiffGroups")) throw new Error(RED._("group.errors.cannotCreateDiffGroups"))
} }
} }
// The nodes are already in a group. The assumption is they should be // The nodes are already in a group - so we need to remove them first
// wrapped in the newly provided group, and that group added to in their
// place to the existing containing group.
if (g) { if (g) {
g = RED.nodes.group(g); g = RED.nodes.group(g);
g.nodes.push(group);
g.dirty = true; g.dirty = true;
group.g = g.id;
} }
// Second pass - add them to the group // Second pass - add them to the group
for (i=0;i<nodes.length;i++) { for (i=0;i<nodes.length;i++) {
@ -594,7 +602,7 @@ RED.group = (function() {
n.dirty = true; n.dirty = true;
var index = group.nodes.indexOf(n); var index = group.nodes.indexOf(n);
group.nodes.splice(index,1); group.nodes.splice(index,1);
if (reparent && group.g) { if (reparent && parentGroup) {
n.g = group.g n.g = group.g
parentGroup.nodes.push(n); parentGroup.nodes.push(n);
} else { } else {

View File

@ -721,9 +721,8 @@ RED.view.tools = (function() {
var nodesToMove = []; var nodesToMove = [];
selection.nodes.forEach(function(n) { selection.nodes.forEach(function(n) {
if (n.type === "group") { if (n.type === "group") {
nodesToMove = nodesToMove.concat(RED.group.getNodes(n, true).filter(function(n) { nodesToMove.push(n)
return n.type !== "group"; nodesToMove = nodesToMove.concat(RED.group.getNodes(n, true))
}))
} else if (n.type !== "subflow"){ } else if (n.type !== "subflow"){
nodesToMove.push(n); nodesToMove.push(n);
} }

File diff suppressed because it is too large Load Diff

View File

@ -91,10 +91,13 @@
.red-ui-flow-group { .red-ui-flow-group {
&.red-ui-flow-group-hovered { &.red-ui-flow-group-hovered {
.red-ui-flow-group-outline-select { .red-ui-flow-group-outline-select-line {
stroke-opacity: 0.8 !important; stroke-opacity: 0.8 !important;
stroke-dasharray: 10 4 !important; stroke-dasharray: 10 4 !important;
} }
.red-ui-flow-group-outline-select-outline {
stroke-opacity: 0.8 !important;
}
} }
&.red-ui-flow-group-active-hovered:not(.red-ui-flow-group-hovered) { &.red-ui-flow-group-active-hovered:not(.red-ui-flow-group-hovered) {
.red-ui-flow-group-outline-select { .red-ui-flow-group-outline-select {
@ -113,15 +116,35 @@
.red-ui-flow-group-outline-select { .red-ui-flow-group-outline-select {
fill: none; fill: none;
stroke: var(--red-ui-node-selected-color); stroke: var(--red-ui-node-selected-color);
pointer-events: stroke; pointer-events: none;
stroke-opacity: 0; stroke-opacity: 0;
stroke-width: 3; stroke-width: 2;
&.red-ui-flow-group-outline-select-background { &.red-ui-flow-group-outline-select-outline {
stroke: var(--red-ui-view-background); stroke: var(--red-ui-view-background);
stroke-width: 6; stroke-width: 4;
}
&.red-ui-flow-group-outline-select-background {
fill: white;
fill-opacity: 0;
pointer-events: stroke;
stroke-width: 16;
} }
} }
svg:not(.red-ui-workspace-lasso-active) {
.red-ui-flow-group:not(.red-ui-flow-group-selected) {
.red-ui-flow-group-outline-select.red-ui-flow-group-outline-select-background:hover {
~ .red-ui-flow-group-outline-select {
stroke-opacity: 0.4 !important;
}
~ .red-ui-flow-group-outline-select-line {
stroke-dasharray: 10 4 !important;
}
}
}
}
.red-ui-flow-group-body { .red-ui-flow-group-body {
pointer-events: none; pointer-events: none;
fill: var(--red-ui-group-default-fill); fill: var(--red-ui-group-default-fill);