mirror of
				https://github.com/node-red/node-red.git
				synced 2025-03-01 10:36:34 +00:00 
			
		
		
		
	Merge pull request #4079 from node-red/group-rework
Complete overhaul of Group UX
This commit is contained in:
		| @@ -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); | ||||
|   | ||||
		Reference in New Issue
	
	Block a user