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:
commit
e5054d306e
@ -378,7 +378,8 @@ RED.history = (function() {
|
||||
if (ev.addToGroup) {
|
||||
RED.group.removeFromGroup(ev.addToGroup,ev.nodes.map(function(n) { return n.n }),false);
|
||||
inverseEv.removeFromGroup = ev.addToGroup;
|
||||
} else if (ev.removeFromGroup) {
|
||||
}
|
||||
if (ev.removeFromGroup) {
|
||||
RED.group.addToGroup(ev.removeFromGroup,ev.nodes.map(function(n) { return n.n }));
|
||||
inverseEv.addToGroup = ev.removeFromGroup;
|
||||
}
|
||||
@ -649,6 +650,12 @@ RED.history = (function() {
|
||||
ev.groups[i].nodes = [];
|
||||
RED.nodes.addGroup(ev.groups[i]);
|
||||
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") {
|
||||
|
@ -71,7 +71,6 @@ RED.nodes = (function() {
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
var exports = {
|
||||
setModulePendingUpdated: function(module,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
|
||||
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 = {
|
||||
addTab: function(id) {
|
||||
tabMap[id] = [];
|
||||
@ -326,152 +361,54 @@ RED.nodes = (function() {
|
||||
n.z = newZ;
|
||||
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)) {
|
||||
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);
|
||||
let result = []
|
||||
const tabNodes = tabMap[nodes[0].z];
|
||||
const toMove = new Set(nodes.filter(function(n) { return n.type !== "group" && n.type !== "subflow" }));
|
||||
if (toMove.size > 0) {
|
||||
result = result.concat(changeCollectionDepth(tabNodes, toMove, direction, singleStep))
|
||||
if (result.length > 0) {
|
||||
RED.events.emit('nodes:reorder',{
|
||||
z: nodes[0].z,
|
||||
nodes: result
|
||||
});
|
||||
}
|
||||
}
|
||||
if (result.length > 0) {
|
||||
RED.events.emit('nodes:reorder',{
|
||||
z: nodes[0].z,
|
||||
nodes: 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
|
||||
});
|
||||
}
|
||||
}
|
||||
return result;
|
||||
RED.view.redraw(true)
|
||||
return result
|
||||
},
|
||||
moveNodesForwards: function(nodes) {
|
||||
return api.changeDepth(nodes, true, true)
|
||||
},
|
||||
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;
|
||||
return api.changeDepth(nodes, false, true)
|
||||
},
|
||||
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;
|
||||
return api.changeDepth(nodes, true, false)
|
||||
},
|
||||
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;
|
||||
return api.changeDepth(nodes, false, false)
|
||||
},
|
||||
getNodes: function(z) {
|
||||
return tabMap[z];
|
||||
@ -571,7 +508,7 @@ RED.nodes = (function() {
|
||||
return result;
|
||||
},
|
||||
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) {
|
||||
var orderMap = {};
|
||||
@ -583,6 +520,11 @@ RED.nodes = (function() {
|
||||
B._reordered = true;
|
||||
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
|
||||
@ -2738,6 +2680,10 @@ RED.nodes = (function() {
|
||||
delete groups[group.id];
|
||||
RED.events.emit("groups:remove",group);
|
||||
}
|
||||
function getGroupOrder(z) {
|
||||
const groups = groupsByZ[z]
|
||||
return groups.map(g => g.id)
|
||||
}
|
||||
|
||||
function addJunction(junction) {
|
||||
if (!junction.__isProxy__) {
|
||||
|
@ -128,17 +128,24 @@ RED.contextMenu = (function () {
|
||||
options: [
|
||||
{ onselect: 'core:group-selection' },
|
||||
{ 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) {
|
||||
menuItems[menuItems.length - 1].options.push(
|
||||
null,
|
||||
{ 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) {
|
||||
menuItems.push({
|
||||
|
@ -325,7 +325,7 @@ RED.group = (function() {
|
||||
var selection = RED.view.selection();
|
||||
if (selection.nodes) {
|
||||
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 = {
|
||||
t:"ungroup",
|
||||
@ -473,9 +473,17 @@ RED.group = (function() {
|
||||
if (nodes.length === 0) {
|
||||
return;
|
||||
}
|
||||
if (nodes.filter(function(n) { return n.type === "subflow" }).length > 0) {
|
||||
RED.notify(RED._("group.errors.cannotAddSubflowPorts"),"error");
|
||||
return;
|
||||
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");
|
||||
return;
|
||||
}
|
||||
if (n.g !== existingGroup) {
|
||||
console.warn("Cannot add nooes with different z properties")
|
||||
return
|
||||
}
|
||||
}
|
||||
// nodes is an array
|
||||
// each node must be on the same tab (z)
|
||||
@ -495,6 +503,10 @@ RED.group = (function() {
|
||||
group.z = nodes[0].z;
|
||||
group = RED.nodes.addGroup(group);
|
||||
|
||||
if (existingGroup) {
|
||||
addToGroup(RED.nodes.group(existingGroup), group)
|
||||
}
|
||||
|
||||
try {
|
||||
addToGroup(group,nodes);
|
||||
} catch(err) {
|
||||
@ -518,7 +530,7 @@ RED.group = (function() {
|
||||
if (!z) {
|
||||
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) {
|
||||
// This is already in a group.
|
||||
@ -535,14 +547,10 @@ RED.group = (function() {
|
||||
throw new Error(RED._("group.errors.cannotCreateDiffGroups"))
|
||||
}
|
||||
}
|
||||
// The nodes are already in a group. The assumption is they should be
|
||||
// wrapped in the newly provided group, and that group added to in their
|
||||
// place to the existing containing group.
|
||||
// The nodes are already in a group - so we need to remove them first
|
||||
if (g) {
|
||||
g = RED.nodes.group(g);
|
||||
g.nodes.push(group);
|
||||
g.dirty = true;
|
||||
group.g = g.id;
|
||||
}
|
||||
// Second pass - add them to the group
|
||||
for (i=0;i<nodes.length;i++) {
|
||||
@ -594,7 +602,7 @@ RED.group = (function() {
|
||||
n.dirty = true;
|
||||
var index = group.nodes.indexOf(n);
|
||||
group.nodes.splice(index,1);
|
||||
if (reparent && group.g) {
|
||||
if (reparent && parentGroup) {
|
||||
n.g = group.g
|
||||
parentGroup.nodes.push(n);
|
||||
} else {
|
||||
|
@ -721,9 +721,8 @@ RED.view.tools = (function() {
|
||||
var nodesToMove = [];
|
||||
selection.nodes.forEach(function(n) {
|
||||
if (n.type === "group") {
|
||||
nodesToMove = nodesToMove.concat(RED.group.getNodes(n, true).filter(function(n) {
|
||||
return n.type !== "group";
|
||||
}))
|
||||
nodesToMove.push(n)
|
||||
nodesToMove = nodesToMove.concat(RED.group.getNodes(n, true))
|
||||
} else if (n.type !== "subflow"){
|
||||
nodesToMove.push(n);
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -91,10 +91,13 @@
|
||||
|
||||
.red-ui-flow-group {
|
||||
&.red-ui-flow-group-hovered {
|
||||
.red-ui-flow-group-outline-select {
|
||||
.red-ui-flow-group-outline-select-line {
|
||||
stroke-opacity: 0.8 !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-outline-select {
|
||||
@ -113,15 +116,35 @@
|
||||
.red-ui-flow-group-outline-select {
|
||||
fill: none;
|
||||
stroke: var(--red-ui-node-selected-color);
|
||||
pointer-events: stroke;
|
||||
pointer-events: none;
|
||||
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-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 {
|
||||
pointer-events: none;
|
||||
fill: var(--red-ui-group-default-fill);
|
||||
|
Loading…
Reference in New Issue
Block a user