findTopLevelGroups & InitialLayoutCompleted

Hi,

strange things happen…I need to reposition the principal group so I used node.move(go.Point.parse(loc)) with saved location string, positioning the function within the “InitialLayoutCompleted” event. Things went right until
I tried to get the principal group (only one in the canvas) with the diagram.findTopLevelGroups that extracted 2 groups(!!!)… I can confirm that there’s only a main group containing the others.One of the extracted group is within the main, why is considered toplevel? The main has no group property inside and the others have group set with the main’s key, all right, I guess…or not?
Here the canvas:


Thx
F

We’ll investigate if there’s a bug with Diagram.findTopLevelGroups.

in the meantime…Is there a way to detect which event drives to “LayoutComplete” event? For example a mouseDrop or more… Is possible from the exposed object in the function get those events? Thank you

Any number of changes might cause a layout to happen.
https://gojs.net/latest/intro/layouts.html#LayoutInvalidation

my need is only to detect if the layoutcomplete event’s been reached from a mouse drop, external or internal

There might be many reasons that cause a layout, and one cannot tell whether a mouse drop might have been one of those reasons.

Ok. I try to be more specific, because I don’t execute many actions, maybe there a solution: when I drag an external object into canvas “layoutcomplete” fires, but also when the json is loaded into model in the initial phase…I need only to divide drop action from each other. But from your words I guess that is impossible to detect this from exposed object, is it? I wish there was something like “stack” actions, maybe also the previous function could be useful…

I learned something else, maybe could be useful for somebody… Initially I used the “InitialLayoutCompleted” event for repositioning the group. That was a reasonable solution because this event is fired only at the beginning, but this code:

            	if (!e.diagram.nodes.all(function(n) { return n.location.isReal(); })) {
            		e.diagram.layoutDiagram(true);
            	}

before the repositioning instructions was changing the reloading of the position. So I put my code on the “LayoutComplete” event, but unfortunately this event is called very often, also when is not very useful… The solution? I put my code again in InitialLayoutCompleted, just before the series of instructions above… of course I didn’t know that this could work but…magic! Here’s my very basic solution.
I keep on wait your solution about findTopLevelGroups…
Thx & ciao
F

I’ve looked at our code and I’ve written a bunch of tests, and I still cannot find a bug that would cause Diagram.findTopLevelGroups to return the wrong collection.

Could you please help me by telling me how to reproduce the problem?

I did this:

  1. I’ve dropped two groups, and after I’ve dropped into two nodes for each group
  2. I’ve dropped a 3d group and after I’ve dropped into that the previous created groups
  3. saved all the created json into db
  4. reload
  5. in debug mode with firefox I put
    myDiagram.findTopLevelGroups().each(function(g) { console.log(g.data.text); });
    into the InitialLayoutCompleted function
    If you need the complete group code I can send you
    That’s all

Here’s my test app that lets one do what you did:

<!DOCTYPE html>
<html>
<head>
  <title>Minimal GoJS Editor</title>
  <!-- Copyright 1998-2021 by Northwoods Software Corporation. -->
  <script src="go.js"></script>
  <script id="code">
    function init() {
      var $ = go.GraphObject.make;

      // initialize main Diagram
      myDiagram =
        $(go.Diagram, "myDiagramDiv",
          {
            "commandHandler.archetypeGroupData": { isGroup: true, text: "Group", color: "green" },
            "undoManager.isEnabled": true,
            "InitialLayoutCompleted": function(e) {
              myDiagram.findTopLevelGroups().each(function(g) { console.log(g.data.text); });
            }
          });

      myDiagram.nodeTemplate =
        $(go.Node, "Auto",
          { locationSpot: go.Spot.Center },
          new go.Binding("location", "location", go.Point.parse).makeTwoWay(go.Point.stringify),
          $(go.Shape,
            {
              fill: "white", stroke: "gray", strokeWidth: 2,
              portId: "", fromLinkable: true, toLinkable: true,
              fromLinkableDuplicates: true, toLinkableDuplicates: true,
              fromLinkableSelfNode: true, toLinkableSelfNode: true
            },
            new go.Binding("stroke", "color")),
          $(go.TextBlock,
            {
              margin: new go.Margin(5, 5, 3, 5), font: "10pt sans-serif",
              minSize: new go.Size(16, 16), maxSize: new go.Size(120, NaN),
              editable: true
            },
            new go.Binding("text").makeTwoWay())
        );

      myDiagram.groupTemplate =
        $(go.Group, "Auto",
          {
            ungroupable: true,
            handlesDragDropForMembers: true,
            mouseDrop: function(e, grp) { grp.addMembers(e.diagram.selection); }
          },
          new go.Binding("location", "loc", go.Point.parse).makeTwoWay(go.Point.stringify),
          $(go.Shape, { fill: "#AAAAAA20", strokeWidth: 0 }),
          $(go.Panel, "Table",
            $(go.Shape,
              { fill: "orange", strokeWidth: 0, width: 20, stretch: go.GraphObject.Vertical },
              new go.Binding("fill", "color")),
            $(go.Panel, "Vertical",
              { column: 1 },
              $(go.TextBlock,
                { margin: 5, minSize: new go.Size(200, NaN), editable: true },
                new go.Binding("text").makeTwoWay()),
              $(go.Placeholder, { padding: 5, minSize: new go.Size(200, NaN) }),
              // A textual button for toggling Group.isSubGraphExpanded
              $(go.TextBlock,
                {
                  alignment: go.Spot.Left,
                  margin: 5,
                  isUnderline: true,
                  stroke: "royalblue",
                  click: function(e, tb) {
                    var group = tb.part;
                    if (group.isSubGraphExpanded) {
                      group.diagram.commandHandler.collapseSubGraph(group);
                    } else {
                      group.diagram.commandHandler.expandSubGraph(group);
                    }
                  }
                },
                new go.Binding("text", "isSubGraphExpanded",
                  function(exp) { return exp ? "Hide" : "Show"; }).ofObject()
              )
              // Alternatively:
              // $("SubGraphExpanderButton", { alignment: go.Spot.Left, margin: 5 })
            )
          )
        )

      // initialize Palette
      myPalette =
        $(go.Palette, "myPaletteDiv",
          {
            nodeTemplateMap: myDiagram.nodeTemplateMap,
            groupTemplateMap: myDiagram.groupTemplateMap,
            model: new go.GraphLinksModel([
              { text: "red node", color: "red" },
              { text: "green node", color: "green" },
              { text: "blue node", color: "blue" },
              { text: "orange node", color: "orange" },
              { text: "Group 1", color: "purple", isGroup: true }
            ])
          });

      // initialize Overview
      myOverview =
        $(go.Overview, "myOverviewDiv",
          {
            observed: myDiagram,
            contentAlignment: go.Spot.Center
          });

      load();
    }

    // save a model to and load a model from Json text, displayed below the Diagram
    function save() {
      var str = myDiagram.model.toJson();
      document.getElementById("mySavedModel").value = str;
    }
    function load() {
      var str = document.getElementById("mySavedModel").value;
      myDiagram.model = go.Model.fromJson(str);
    }
    </script>
</head>
<body onload="init()">
  <div style="width: 100%; display: flex; justify-content: space-between">
    <div style="display: flex; flex-direction: column; margin: 0 2px 0 0">
      <div id="myPaletteDiv" style="flex-grow: 1; width: 100px; background-color: floralwhite; border: solid 1px black"></div>
      <div id="myOverviewDiv" style="margin: 2px 0 0 0; width: 100px; height: 100px; background-color: whitesmoke; border: solid 1px black"></div>
    </div>
    <div id="myDiagramDiv" style="flex-grow: 1; height: 400px; border: solid 1px black"></div>
  </div>
  <div id="buttons">
    <button id="loadModel" onclick="load()">Load</button>
    <button id="saveModel" onclick="save()">Save</button>
  </div>
  <textarea id="mySavedModel" style="width:100%;height:200px">
  { "class": "GraphLinksModel",
  "linkFromPortIdProperty": "fromPort",
  "linkToPortIdProperty": "toPort",
  "nodeDataArray": [ 
{"isGroup":true, "text":"Somma", "loc":"128 -31.5", "horiz":"#FFAA33", "rightArray":[ {"portId":"Ambito", "checked":true},{"portId":"Scalo", "checked":true},{"portId":"Profit", "checked":true} ], "source":"/FILL_EM/img/plus.png", "key":-1},
{"isGroup":true, "text":"Prodotto", "loc":"144 14", "horiz":"#33D3E5", "rightArray":[ {"portId":"Ambito", "checked":true},{"portId":"Scalo", "checked":true},{"portId":"Profit", "checked":true} ], "source":"/FILL_EM/img/mult.png", "key":-2, "group":-1},
{"isGroup":true, "text":"Prodotto", "loc":"744 14", "horiz":"#33D3E5", "rightArray":[ {"portId":"Profit", "checked":true},{"portId":"Scalo", "checked":true},{"portId":"Ambito", "checked":true} ], "source":"/FILL_EM/img/mult.png", "key":-3, "group":-1},
{"key":"PE700", "text":"PE700", "category":"Input", "group":-2, "name":"Parametro test dimensioni su base anagrafica 1", "fields":[ {"id":"PE700_3", "name":"Ambito"},{"id":"PE700_1", "name":"Scalo"} ], "loc":"159 29.00000000000007"},
{"key":"PE703", "text":"PE703", "category":"TOTOUT", "group":-2, "name":"Parametro test calcolato prodotto", "fields":[ {"id":"PE703_1", "name":"Scalo"},{"id":"PE703_3", "name":"Ambito"},{"id":"PE703_2", "name":"Profit"} ], "loc":"399.00000000000017 29.00000000000007"},
{"key":"PE701", "text":"PE701", "category":"Input", "group":-3, "name":"Parametro test dimensioni su base anagrafica 2", "fields":[ {"id":"PE701_2", "name":"Profit"},{"id":"PE701_1", "name":"Scalo"},{"id":"PE701_3", "name":"Ambito"} ], "loc":"758.9999999999998 29.00000000000007"},
{"key":"PE704", "text":"PE704", "category":"Input", "group":-3, "name":"Parametro test dimensioni configurabili", "fields":[ {"id":"PE704_1", "name":"Scalo"},{"id":"PE704_3", "name":"Ambito"} ], "loc":"999.0000000000002 29.00000000000007"}
  ],
  "linkDataArray": []}
  </textarea>
</body>
</html>

After doing what you list you did, I am unable to reproduce any problem.

myDiagram.findTopLevelGroups().each(function(g) { console.log(g.data.text); });
printed two groups, as one would expect with the third one as a member of one of the other two groups.

EDIT: I have replaced the earlier serialized model in the page with the one I just built by hand, in case what I built isn’t what you were building.

Here’s the json:

{ "class": "GraphLinksModel",
  "linkFromPortIdProperty": "fromPort",
  "linkToPortIdProperty": "toPort",
  "nodeDataArray": [ 
{"isGroup":true, "text":"Somma", "loc":"128 -31.5", "horiz":"#FFAA33", "rightArray":[ {"portId":"Ambito", "checked":true},{"portId":"Scalo", "checked":true},{"portId":"Profit", "checked":true} ], "source":"/FILL_EM/img/plus.png", "key":-1},
{"isGroup":true, "text":"Prodotto", "loc":"144 14", "horiz":"#33D3E5", "rightArray":[ {"portId":"Ambito", "checked":true},{"portId":"Scalo", "checked":true},{"portId":"Profit", "checked":true} ], "source":"/FILL_EM/img/mult.png", "key":-2, "group":-1},
{"isGroup":true, "text":"Prodotto", "loc":"744 14", "horiz":"#33D3E5", "rightArray":[ {"portId":"Profit", "checked":true},{"portId":"Scalo", "checked":true},{"portId":"Ambito", "checked":true} ], "source":"/FILL_EM/img/mult.png", "key":-3, "group":-1},
{"key":"PE700", "text":"PE700", "category":"Input", "group":-2, "name":"Parametro test dimensioni su base anagrafica 1", "fields":[ {"id":"PE700_3", "name":"Ambito"},{"id":"PE700_1", "name":"Scalo"} ], "loc":"159 29.00000000000007"},
{"key":"PE703", "text":"PE703", "category":"TOTOUT", "group":-2, "name":"Parametro test calcolato prodotto", "fields":[ {"id":"PE703_1", "name":"Scalo"},{"id":"PE703_3", "name":"Ambito"},{"id":"PE703_2", "name":"Profit"} ], "loc":"399.00000000000017 29.00000000000007"},
{"key":"PE701", "text":"PE701", "category":"Input", "group":-3, "name":"Parametro test dimensioni su base anagrafica 2", "fields":[ {"id":"PE701_2", "name":"Profit"},{"id":"PE701_1", "name":"Scalo"},{"id":"PE701_3", "name":"Ambito"} ], "loc":"758.9999999999998 29.00000000000007"},
{"key":"PE704", "text":"PE704", "category":"Input", "group":-3, "name":"Parametro test dimensioni configurabili", "fields":[ {"id":"PE704_1", "name":"Scalo"},{"id":"PE704_3", "name":"Ambito"} ], "loc":"999.0000000000002 29.00000000000007"}
 ],
  "linkDataArray": []}

This is the InitialLayoutCompleted function:

            "InitialLayoutCompleted": function(e) {
    			myDiagram.findTopLevelGroups().each(function(g) 
    					{ 
    						console.log(g.data.text);
    					});
            	// if not all Nodes have real locations, force a layout to happen
            	repositionGroup(jQuery.parm_conf);
            	if (!e.diagram.nodes.all(function(n) { return n.location.isReal(); })) {
            		e.diagram.layoutDiagram(true);
            	}
            	CheckAllInfoTab();
            	//repositionGroup(jQuery.parm_conf);
            },

This is the result in the console log:
consolelog
And this is the image of the viewport you’ve already seen before:

As you can see in the log details the main group called “Somma” is at the same level of the “Prodotto” group inside (the second one)
Now or I miss some concepts or something is wrong…
I think it’s also a matter of timing, maybe I’m wrong
(for reason of privacy I can’t put all the code, that is anyway a tons…)

I have replaced the code I posted above with the latest, including your model, verbatim. Here’s the result:


And what it outputs in the console log:

Somma                                              minimalEditor copy.html:18:73

In other words, what I think we both agree is the correct output – just one Group.

So I’m wondering if you have some other code that is modifying either the model or the Parts in your Diagram.

Or you have something else printing “Prodotto”?

I really don’t understand what can be the reason having in mind that group keys are correct…so parent children relationship.
Here’s instruction for inserting nodes (not group):

						diag.startTransaction("addNode");
						diag.model.addNodeData({"key":parametro, "text":parametro, "category":type, "group":group, "name":dime2, "fields":dime1, loc:point});
						diag.commitTransaction("addNode");

and here’s instructions for groups:

			  if(containerkey==undefined){
				  myDiagram.model.addNodeData({"isGroup" : true, "text" : dsP, loc:pnt,"horiz":clr, "rightArray":[],"source":src}); 
			  }else{
				  myDiagram.model.addNodeData({"isGroup" : true, "group": containerkey, "text" : dsP, loc:pnt,"horiz":clr, "rightArray":[],"source":src}); 
			  }

Nodes can only be inserted in a group.
Here’s group template:

      myDiagram.groupTemplate =
    	  $gj(go.Group, "Spot",
    		  { 
    		  background: "transparent",
    		  ungroupable: true,
    		  // highlight when dragging into the Group
    		  mouseDragEnter: function(e, grp, prev) { highlightGroup(e, grp, true); },
    		  mouseDragLeave: function(e, grp, next) { highlightGroup(e, grp, false); },
    		  computesBoundsAfterDrag: true,
    		  // when the selection is dropped into a Group, add the selected Parts into that Group;
    		  // if it fails, cancel the tool, rolling back any changes
    		  mouseDrop: finishDrop,
    		  handlesDragDropForMembers: true,  // don't need to define handlers on member Nodes and Links
    		  // Groups containing Groups lay out their members horizontally
    		  layout: makeLayout(false),
    		  cursor: "move",
    		  dragComputation: stayInGroup,
              containingGroupChanged: function(prt, oldgroup, newgroup) {
			  			processParentGroupRec(prt);
			  			CheckAllInfoTab();
                	},    		  
    		  },
    			  new go.Binding("location", "loc", go.Point.parse).makeTwoWay(go.Point.stringify),
    			  new go.Binding("layout", "horiz", makeLayout),
    			  new go.Binding("background", "isHighlighted", function(h) {
    				  return h ? "rgba(0,255,0,0.6)" : "white";
    			  }).ofObject(),
    			  $gj(go.Panel, "Auto",
    					  $gj(go.Shape, "Rectangle",
    						  { 
    						  fill: null, 
    						  strokeWidth: 2 
    						  },
    						  new go.Binding("stroke", "horiz", defaultColor),
    						  new go.Binding("stroke", "color")),
    						  $gj(go.Panel, "Vertical",  // title above Placeholder
    								  $gj(go.Panel, "Horizontal",  // button next to TextBlock
    								  {
    									  name: "hover-screenshot-panel",
    									  stretch: go.GraphObject.Horizontal, 
    									  //background: defaultColor(false), 
    									  portId: "totale",
    									  // allows links to/from all sides
    									  fromSpot: go.Spot.Right,
    									  toSpot: go.Spot.Left,
    									  fromLinkable: true,
    									  toLinkable: true,
    									  cursor:"pointer"
    								  },
									  new go.Binding("background", "horiz", defaultColor),
									  new go.Binding("background", "color"),
									  //$gj("SubGraphExpanderButton",{ alignment: go.Spot.Right, margin: 5 }),
									  $gj(go.TextBlock,
									  {
										  alignment: go.Spot.Left,
										  editable: false,
										  margin: 5,
										  font: defaultFont(false),
										  opacity: 0.75,  // allow some color to show through
										  stroke: "#404040"
									  },
									  new go.Binding("font", "horiz", defaultFont),
									  new go.Binding("text", "text").makeTwoWay())
    								  ),	// end Horizontal Panel
    								  $gj(go.Panel, "Table",{},
	    								  $gj(go.Placeholder,
	    								  { 
	    									  padding: 15, 
	    									  alignment: go.Spot.TopLeft, 
	    						    		  minSize: new go.Size(250, 150)
	    								  }),
	    								  $gj(go.Picture,  //BASE_URL+"img/plus.png",
	    									{
	    									  width: 200, height: 200, scale: 1.0, 
	    								      alignment: go.Spot.LeftCenter,
	    								      opacity:0.3
	    								    },
	    								    new go.Binding("source", "source")
	    								  )
    				    			  )
    						  )  // end Vertical Panel
    			  ), // end of Panel Auto
    			  $gj(go.Panel, "Vertical",
    					  {
    				  alignment: new go.Spot(1,0.6,50,0),
    				  name: "VPANEL"
    					  },
    					  new go.Binding("itemArray", "rightArray"),
    					  {
    				  itemTemplate:
    					  $gj(go.Panel,
    							  {
    						  _side: "right",
    						  fromSpot: go.Spot.Right, toSpot: go.Spot.Right,
    						  fromLinkable: true, toLinkable: false, 
    						  cursor: "pointer",
    						  contextMenu: portMenu
    							  },
    							  new go.Binding("portId", "portId"),
    							  $gj(go.Shape, "Rectangle",
    									  {
    								  stroke: null, strokeWidth: 0,
    								  desiredSize: portSize,
    								  margin: new go.Margin(3, 0),
    								  fill: "blue"
    									  }
    							  ),
    							  $gj(go.TextBlock,
    									  { margin: new go.Margin(5, 0, 0, 25), 
    								  font: "bold 12px sans-serif",
    								  stroke:"white",
    								  alignment: go.Spot.Left, 
    								  fromLinkable: false, 
    								  toLinkable: false,
    								  shadowVisible: false
    									  },
   									  new go.Binding("text", "portId")
    							  ),
								  $gj("CheckBox", "checked",
									{ "ButtonIcon.stroke": "green",
									  "_buttonFillOver": "lightgreen", 
									  "_buttonStrokeOver": "green",
									  "Button.width": 20, "Button.height": 20,
									  margin:new go.Margin(3, 0),
					                  "_doClick": function(e, obj) {
					                	  if(obj.part.ports.count==2){
					                		  return;
					                	  }  
					                	  processParentGroupRec(obj.part);
					                	  CheckAllInfoTab();
					                  }
									}
								  )
    					  )  // end itemTemplate
    					  }
    			  )  // end Vertical Panel
    	  );

Some functions check only the tree adding ports and populating a html popup.
That’s really all…

What are the values of group when adding a node and of containerkey when adding a group?

adding a node, “group” property is set to the containerkey. A node can’t be inserted outside of a group. A group, instead, can be obviously inserted outside and inside a group. The value of containerkey is the same for every node and group that are added to group. Json values above show values inserted.

OK, I was just checking that the model in JSON format was actually what was generated.

Could you check in your “InitialLayoutCompleted” handler:

  "InitialLayoutCompleted": function(e) {
    console.log(e.diagram.findTopLevelGroups().count);
    e.diagram.findTopLevelGroups().each(function(g) { console.log(g.key, g.data.text); });
    . . .
  }

consolelog

Here’s the result for code posted.
Two groups, the keys are right. We must call Mulder & Scully …

And when I execute the code I posted earlier, with the modified “InitialLayoutCompleted” event listener, using the exactly JSON-formatted model you posted earlier:

1
-1 Somma

I don’t know what’s different between my app and your app. Maybe you have some other event listeners or event handlers that are affecting groups somehow? Search your code for any places you might be setting the “group” property on the node data objects. The two cases you have quoted above should be OK because you have set the property before adding the node data object to the model.

Here I go again. I did some tries in every direction but result is the same. I really don’t know why it keeps on extracting 2 toplevel groups. I’m sure that there’s nothing changing group’s properties, especially when loading. I’ve also seen that trying to reposition the main group in last position registered is quite impossible, especially when there’s more than one group nested. I’ll open another topic on this.
thanks Walter for your support.