Ordering of links at node

Hi @walter I have another question, please help me solve the following situation. I have a tree map where a destination node can receive multiple source links from different groups. Currently, I have a fixed order set to 1, but it should automatically determine the order of the links that reach the destination node. How can I also allow the user to change this automatic configuration to their preference, i.e., if they want the link they created to be number 1 or 2, and vice versa?

onMounted(() => {
  getDestination();
  getOrigins();
  getMappingLinks();

  diagram = new Diagram('treeMapperDiv', {
    'commandHandler.copiesTree': true,
    'commandHandler.deletesTree': true,
    'linkingTool.archetypeLinkData': { category: 'Mapping' },
    'linkingTool.linkValidation': checkLink,
    'relinkingTool.linkValidation': checkLink,
    'undoManager.isEnabled': true,
    TreeCollapsed: handleTreeCollapseExpand,
    TreeExpanded: handleTreeCollapseExpand,
    LinkDrawn: async (e: DiagramEvent) => {
      const link = e.subject.part;
      if (link && link.data.category === 'Mapping') {
        const fromNode = link.fromNode;
        const toNode = link.toNode;
        const parentNode = fromNode.findTreeParentNode();
        const toGroup = toNode.containingGroup;
        const mappingLink: MappingData = {
          origin: parentNode ? parentNode.data.key : null,
          from: link.data.from,
          group: 0,
          package_id: packageName.value,
          destination: toGroup ? toGroup.data.key : null,
          to: link.data.to,
          order: 1,
          config: configId
        };
        mappingLinks.push(mappingLink);
      }
    }
  });

  let previousLinkDataArray = [...(diagram.model as go.GraphLinksModel).linkDataArray];

  // diagram.addModelChangedListener((e) => {
  //   if (e.modelChange === "linkDataArray") {
  //     const model = e.model as go.GraphLinksModel;
  //     if (model && Array.isArray(model.linkDataArray)) {
  //       console.log("model.linkDataArray", model.linkDataArray);

  //       // Comprobar el estado anterior y actual
  //       console.log("Previous link data:", previousLinkDataArray);
  //       console.log("Current link data:", model.linkDataArray);

  //       mappingLinks = model.linkDataArray
  //         .filter((link: any) => link.category === 'Mapping')
  //         .map((link: any) => ({
  //           origin: link.origin || '',
  //           from: link.from,
  //           to: link.to,
  //           group: link.group || -1,
  //           package_id: link.package_id || '',
  //         })) as MappingData[];

  //       console.log("Model changed - Updated mappingLinks:", mappingLinks.length);
  //     }
  //   }
  // });

  // diagram.addModelChangedListener((e) => {
  //   if (e.modelChange === "linkDataArray") {
  //     const model = e.model as go.GraphLinksModel;
  //     if (model && Array.isArray(model.linkDataArray)) {
  //       // Actualiza previousLinkDataArray con el estado actual
  //       previousLinkDataArray = [...model.linkDataArray];
  //       console.log("Previous link data:", previousLinkDataArray);
  //     }
  //   }
  // });

  diagram.addModelChangedListener(evt => {
    // ignore unimportant Transaction events
    if (!evt.isTransactionFinished) return;
    const txn = evt.object;  // a Transaction
    if (txn === null) return;

    // iterate over all of the actual ChangedEvents of the Transaction
    txn.changes.each(e => {
      // ignore any kind of change other than adding/removing a node
      if (e.modelChange !== "nodeDataArray") return;

      // record node insertions and removals
      if (e.change === go.ChangeType.Insert) {
        console.log(evt.propertyName + " added node with key: " + e.newValue.key);
      } else if (e.change === go.ChangeType.Remove) {
        console.log(evt.propertyName + " removed node with key: " + e.oldValue.key);
      }
    });
  });
  const selectionAdornmentTemplate = $(
    go.Adornment, 'Auto',
    $(go.Shape, 'RoundedRectangle', {
      fill: null,
      stroke: '#55C6C0',
      strokeWidth: 2
    }),
    $(go.Placeholder)
  );

  // const cxElement = document.getElementById('contextMenu');
  // const myContextMenu = new go.HTMLInfo({
  //   show: showContextMenu,
  //   hide: hideContextMenu
  // });

  // cxElement!.addEventListener('contextmenu', (e) => {
  //   e.preventDefault();
  //   return false;
  // }, false);

  // function hideCX() {
  //   if (diagram.currentTool instanceof go.ContextMenuTool) {
  //     diagram.currentTool.doCancel();
  //   }
  // }

  // function showContextMenu(obj, diagram, tool) {
  //   var cmd = diagram.commandHandler;
  //   var hasMenuItem = false;
  //   function maybeShowItem(elt, pred) {
  //     if (pred) {
  //       elt.style.display = 'block';
  //       hasMenuItem = true;
  //     } else {
  //       elt.style.display = 'none';
  //     }
  //   }
  //   maybeShowItem(document.getElementById('cut'), cmd.canCutSelection());
  //   maybeShowItem(document.getElementById('copy'), cmd.canCopySelection());
  //   maybeShowItem(document.getElementById('delete'), cmd.canDeleteSelection());
  //   maybeShowItem(document.getElementById('color'), obj !== null);

  //   if (hasMenuItem) {
  //     cxElement!.classList.add('show-menu');
  //     var mousePt = diagram.lastInput.viewPoint;
  //     cxElement!.style.left = mousePt.x + 5 + 'px';
  //     cxElement!.style.top = mousePt.y + 'px';
  //   }

  //   window.addEventListener('pointerdown', hideCX, true);
  // }

  // function hideContextMenu() {
  //   cxElement!.classList.remove('show-menu');
  //   window.removeEventListener('pointerdown', hideCX, true);
  // }

  // function cxcommand(event, val) {
  //   if (val === undefined) val = event.currentTarget.id;
  //   var myDiagram = diagram;
  //   switch (val) {
  //     case 'cut':
  //     myDiagram.commandHandler.cutSelection();
  //       break;
  //     case 'copy':
  //     myDiagram.commandHandler.copySelection();
  //       break;
  //     case 'delete':
  //     myDiagram.commandHandler.deleteSelection();
  //       break;
  //   }
  //   diagram.currentTool.stopTool();
  // }

  diagram.nodeTemplate = $(
    TreeNode,
    {
      isTreeExpanded: false,
      movable: false,
      copyable: false,
      deletable: false,
      selectionAdorned: true,
      // contextMenu: myContextMenu,
      selectionAdornmentTemplate: selectionAdornmentTemplate,
      background: 'white'
    },
    new Binding('fromLinkable', 'group', (k) => k !== formDestination),
    new Binding('toLinkable', 'group', (k) => k === formDestination),
    $('TreeExpanderButton', {
      width: 14,
      height: 14,
      'ButtonIcon.stroke': 'white',
      'ButtonIcon.strokeWidth': 2,
      'ButtonBorder.fill': '#55C6C0',
      'ButtonBorder.stroke': null,
      'ButtonBorder.figure': 'Circle',
      _buttonFillOver: '#00AAA1',
      _buttonStrokeOver: null,
      _buttonFillPressed: null
    }),
    $(
      Panel,
      'Auto',
      { position: new Point(16, -10) },
      $(
        go.Shape,
        'RoundedRectangle',
        {
          fill: 'white',
          strokeWidth: 1,
          stroke: 'transparent',
          name: 'SHAPE'
        }
      ),
      $(
        go.TextBlock,
        {
          stroke: '#616161',
          margin: new go.Margin(4, 0, 4, 0),
        },
        new go.Binding('text', 'text'),
        new go.Binding("font", "", function (node) {
          return hasChildren(node.part) ? "600 18px Figtree, sans-serif" : "16px Figtree, sans-serif";
        }).ofObject()
      )
    )
  );

  diagram.linkTemplate = $(Link);

  diagram.linkTemplate = $(
    Link,
    {
      selectable: true,
      routing: Routing.Orthogonal,
      fromEndSegmentLength: 4,
      toEndSegmentLength: 4,
      fromSpot: new Spot(0.001, 1, 7, 0),
      toSpot: Spot.Left
    },
  );

  diagram.linkTemplateMap.add(
    'Mapping',
    $(
      MappingLink,
      {
        isTreeLink: false,
        isLayoutPositioned: false,
        layerName: 'Foreground'
      },
      { fromSpot: Spot.Right, toSpot: Spot.Left },
      { relinkableFrom: true, relinkableTo: true },
      $(Shape, { name: 'link', stroke: '#C2C2C2', strokeWidth: 2 }
      ),
    )
  );

  diagram.groupTemplate = $(
    Group,
    'Auto',
    { deletable: false, layout: makeGroupLayout(), selectionObjectName: 'card' },
    new Binding('position', 'xy', Point.parse).makeTwoWay(Point.stringify),
    new Binding('layout', 'width', makeGroupLayout),
    $(
      go.Shape,
      'RoundedRectangle',
      {
        name: 'card',
        fill: 'white',
        stroke: '#E8E8E8'
      }
    ),
    $(
      Panel,
      'Vertical',
      { defaultAlignment: Spot.Left },
      $(
        TextBlock,
        {
          font: '600 20px Figtree, sans-serif',
          stroke: '#005450',
          margin: new Margin(5, 5, 0, 5)
        },
        new Binding('text')
      ),
      $(Placeholder, { padding: 5 })
    )
  );
  diagram.model = new go.GraphLinksModel(nodeDataArray.value, linkDataArray.value);

});
const checkLink = (
  fn: Node,
  fp: GraphObject,
  tn: Node,
  tp: GraphObject,
  link: Link
) => {
  // make sure the nodes are inside different Groups
  if (fn.containingGroup === null || fn.containingGroup.data.key === formDestination)
    return false;
  if (tn.containingGroup === null) return false;
  // optional limit to a single mapping link per node
  // if (fn.linksConnected.any(l => l.category === "Mapping")) return false;
  // if (tn.linksConnected.any(l => l.category === "Mapping")) return false;
  return true;
};

I’m not sure what it is that you are asking for. Could you please show a screenshot of your current situation and another screenshot or sketch of what you want instead based on some criteria?

@walter thanks for answering so fast. this is the example I’m creating.


The idea is that knows if the order of this first name because I want to use the context menu and let the user change that order, but I’ve got troubles using also this context menu I create this code in which I mix both of your codes

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>TreeMapper with Context Menu</title>
    <script src="https://unpkg.com/[email protected]/release/go.js"></script>
    <style>
        .menu {
            display: none;
            position: absolute;
            opacity: 0;
            margin: 0;
            padding: 8px 0;
            z-index: 999;
            box-shadow: 0 5px 5px -3px rgba(0, 0, 0, 0.2), 0 8px 10px 1px rgba(0, 0, 0, 0.14), 0 3px 14px 2px rgba(0, 0, 0, 0.12);
            list-style: none;
            background-color: #ffffff;
            border-radius: 4px;
        }

        .menu-item {
            display: block;
            position: relative;
            min-width: 60px;
            margin: 0;
            padding: 6px 16px;
            font: bold 12px sans-serif;
            color: rgba(0, 0, 0, 0.87);
            cursor: pointer;
        }

        .menu-item::before {
            position: absolute;
            top: 0;
            left: 0;
            opacity: 0;
            pointer-events: none;
            content: '';
            width: 100%;
            height: 100%;
            background-color: #000000;
        }

        .menu-item:hover::before {
            opacity: 0.04;
        }

        .show-menu {
            display: block;
            opacity: 1;
        }
    </style>
</head>

<body>
    <script src="https://unpkg.com/[email protected]/release/go.js"></script>
    <link href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism.min.css" rel="stylesheet" />
    <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js"></script>
    <div id="allSampleContent" class="p-4 w-full">
        <script id="code">
            class TreeNode extends go.Node {
                findVisibleNode() {
                    var n = this;
                    while (n !== null && !n.isVisible()) {
                        n = n.findTreeParentNode();
                    }
                    return n;
                }
            }
            var ROUTINGSTYLE = 'ToGroup';
            class MappingLink extends go.Link {
                getLinkPoint(node, port, spot, from, ortho, othernode, otherport) {
                    if (ROUTINGSTYLE !== 'ToGroup') {
                        return super.getLinkPoint(node, port, spot, from, ortho, othernode, otherport);
                    } else {
                        var r = port.getDocumentBounds();
                        var group = node.containingGroup;
                        var b = group !== null ? group.actualBounds : node.actualBounds;
                        var op = othernode.getDocumentPoint(go.Spot.Center);
                        var x = op.x > r.centerX ? b.right : b.left;
                        return new go.Point(x, r.centerY);
                    }
                }

                computePoints() {
                    var result = super.computePoints();
                    if (result && ROUTINGSTYLE === 'ToNode') {
                        var fn = this.fromNode;
                        var tn = this.toNode;
                        if (fn && tn) {
                            var fg = fn.containingGroup;
                            var fb = fg ? fg.actualBounds : fn.actualBounds;
                            var fpt = this.getPoint(0);
                            var tg = tn.containingGroup;
                            var tb = tg ? tg.actualBounds : tn.actualBounds;
                            var tpt = this.getPoint(this.pointsCount - 1);
                            this.setPoint(1, new go.Point(fpt.x < tpt.x ? fb.right : fb.left, fpt.y));
                            this.setPoint(
                                this.pointsCount - 2,
                                new go.Point(fpt.x < tpt.x ? tb.left : tb.right, tpt.y)
                            );
                        }
                    }
                    return result;
                }
            }
            function init() {
                function handleTreeCollapseExpand(e) {
                    e.subject.each((n) => {
                        n.findExternalTreeLinksConnected().each((l) => l.invalidateRoute());
                    });
                }

                myDiagram = new go.Diagram('myDiagramDiv', {
                    'commandHandler.copiesTree': true,
                    'commandHandler.deletesTree': true,
                    TreeCollapsed: handleTreeCollapseExpand,
                    TreeExpanded: handleTreeCollapseExpand,
                    'linkingTool.archetypeLinkData': { category: 'Mapping' },
                    'linkingTool.linkValidation': checkLink,
                    'relinkingTool.linkValidation': checkLink,
                    'undoManager.isEnabled': true,
                    ModelChanged: (e) => {
                        if (e.isTransactionFinished) {
                            document.getElementById('mySavedModel').innerHTML = e.model.toJson();
                            if (window.Prism) window.Prism.highlightAll();
                        }
                    }
                });
                function checkLink(fn, fp, tn, tp, link) {
                    if (fn.containingGroup === null || fn.containingGroup.data.key !== -1) return false;
                    if (tn.containingGroup === null || tn.containingGroup.data.key !== -2) return false;
                    return true;
                }

                var cxElement = document.getElementById('contextMenu');

                var myContextMenu = new go.HTMLInfo({
                    show: showContextMenu,
                    hide: hideContextMenu
                });
                myDiagram.nodeTemplate = new TreeNode({
                    movable: false,
                    copyable: false,
                    deletable: false,
                    selectionAdorned: false,
                    contextMenu: myContextMenu,
                    background: 'white',
                    mouseEnter: (e, node) => (node.background = 'aquamarine'),
                    mouseLeave: (e, node) => (node.background = node.isSelected ? 'skyblue' : 'white')
                })
                    .bindObject('background', 'isSelected', (s) => (s ? 'skyblue' : 'white'))
                    .bind('fromLinkable', 'group', (k) => k === -1)
                    .bind('toLinkable', 'group', (k) => k === -2)
                    .add(
                        go.GraphObject.build('TreeExpanderButton', {
                            width: 14,
                            height: 14,
                            'ButtonIcon.stroke': 'white',
                            'ButtonIcon.strokeWidth': 2,
                            'ButtonBorder.fill': 'goldenrod',
                            'ButtonBorder.stroke': null,
                            'ButtonBorder.figure': 'Rectangle',
                            _buttonFillOver: 'darkgoldenrod',
                            _buttonStrokeOver: null,
                            _buttonFillPressed: null
                        }),
                        new go.Panel('Horizontal', { position: new go.Point(16, 0) })
                            .add(
                                new go.TextBlock().bind('text', 'key', (s) => 'item ' + s)
                            )
                    );
                myDiagram.linkTemplate = new go.Link();
                myDiagram.linkTemplate =
                    new go.Link({
                        selectable: false,
                        routing: go.Routing.Orthogonal,
                        fromEndSegmentLength: 4,
                        toEndSegmentLength: 4,
                        fromSpot: new go.Spot(0.001, 1, 7, 0),
                        toSpot: go.Spot.Left
                    }).add(new go.Shape({ stroke: 'lightgray' }));
                myDiagram.linkTemplateMap.add(
                    'Mapping',
                    new MappingLink({
                        isTreeLink: false,
                        isLayoutPositioned: false,
                        layerName: 'Foreground',
                        fromSpot: go.Spot.Right,
                        toSpot: go.Spot.Left,
                        relinkableFrom: true,
                        relinkableTo: true
                    }).add(new go.Shape({ stroke: 'blue', strokeWidth: 2 }))
                );

                myDiagram.groupTemplate = new go.Group('Auto', { deletable: false, layout: makeGroupLayout() })
                    .bindTwoWay('position', 'xy', go.Point.parse, go.Point.stringify)
                    .bind('layout', 'width', makeGroupLayout)
                    .add(
                        new go.Shape({ fill: 'white', stroke: 'lightgray' }),
                        new go.Panel('Vertical', { defaultAlignment: go.Spot.Left })
                            .add(
                                new go.TextBlock({
                                    font: 'bold 14pt sans-serif',
                                    margin: new go.Margin(5, 5, 0, 5)
                                }).bind('text'),
                                new go.Placeholder({ padding: 5 })
                            )
                    );

                function makeGroupLayout() {
                    return new go.TreeLayout({
                        alignment: go.TreeAlignment.Start,
                        angle: 0,
                        compaction: go.TreeCompaction.None,
                        layerSpacing: 16,
                        layerSpacingParentOverlap: 1,
                        nodeIndentPastParent: 1.0,
                        nodeSpacing: 0,
                        setsPortSpot: false,
                        setsChildPortSpot: false,
                        commitNodes: function () {
                            go.TreeLayout.prototype.commitNodes.call(this);
                            if (ROUTINGSTYLE === 'ToGroup') {
                                updateNodeWidths(this.group, this.group.data.width || 100);
                            }
                        }
                    });
                }
                var nodeDataArray = [
                    { isGroup: true, key: -1, text: 'Left Side', xy: '0 0', width: 150 },
                    { isGroup: true, key: -2, text: 'Right Side', xy: '300 0', width: 150 }
                ];
                var linkDataArray = [
                    { from: 6, to: 1012, category: 'Mapping' },
                    { from: 4, to: 1006, category: 'Mapping' },
                    { from: 9, to: 1004, category: 'Mapping' },
                    { from: 1, to: 1009, category: 'Mapping' },
                    { from: 14, to: 1010, category: 'Mapping' }
                ];
                var root = { key: 0, group: -1 };
                nodeDataArray.push(root);
                for (var i = 0; i < 11;) {
                    i = makeTree(3, i, 17, nodeDataArray, linkDataArray, root, -1, root.key);
                }
                root = { key: 1000, group: -2 };
                nodeDataArray.push(root);
                for (var i = 0; i < 15;) {
                    i = makeTree(3, i, 15, nodeDataArray, linkDataArray, root, -2, root.key);
                }
                myDiagram.model = new go.GraphLinksModel(nodeDataArray, linkDataArray);

                cxElement.addEventListener('contextmenu', (e) => {
                    e.preventDefault();
                    return false;
                }, false);

                function hideCX() {
                    if (myDiagram.currentTool instanceof go.ContextMenuTool) {
                        myDiagram.currentTool.doCancel();
                    }
                }

                function showContextMenu(obj, diagram, tool) {
                    var cmd = diagram.commandHandler;
                    var hasMenuItem = false;
                    function maybeShowItem(elt, pred) {
                        if (pred) {
                            elt.style.display = 'block';
                            hasMenuItem = true;
                        } else {
                            elt.style.display = 'none';
                        }
                    }
                    maybeShowItem(document.getElementById('cut'), cmd.canCutSelection());
                    maybeShowItem(document.getElementById('copy'), cmd.canCopySelection());
                    maybeShowItem(document.getElementById('delete'), cmd.canDeleteSelection());
                    maybeShowItem(document.getElementById('color'), obj !== null);

                    if (hasMenuItem) {
                        cxElement.classList.add('show-menu');
                        var mousePt = diagram.lastInput.viewPoint;
                        cxElement.style.left = mousePt.x + 5 + 'px';
                        cxElement.style.top = mousePt.y + 'px';
                    }

                    window.addEventListener('pointerdown', hideCX, true);
                }

                function hideContextMenu() {
                    cxElement.classList.remove('show-menu');
                    window.removeEventListener('pointerdown', hideCX, true);
                }

                function cxcommand(event, val) {
                    if (val === undefined) val = event.currentTarget.id;
                    var diagram = myDiagram;
                    switch (val) {
                        case 'cut':
                            diagram.commandHandler.cutSelection();
                            break;
                        case 'copy':
                            diagram.commandHandler.copySelection();
                            break;
                        case 'delete':
                            diagram.commandHandler.deleteSelection();
                            break;
                        case 'color': {
                            var color = window.getComputedStyle(event.target)['background-color'];
                            changeColor(diagram, color);
                            break;
                        }
                    }
                    diagram.currentTool.stopTool();
                }

                function changeColor(diagram, color) {
                    diagram.startTransaction('change color');
                    diagram.selection.each((node) => {
                        if (node instanceof go.Node) {
                            var data = node.data;
                            diagram.model.setDataProperty(data, 'color', color);
                        }
                    });
                    diagram.commitTransaction('change color');
                }

            }
            function makeTree(level, count, max, nodeDataArray, linkDataArray, parentdata, groupkey, rootkey) {
                var numchildren = Math.floor(Math.random() * 10);
                for (var i = 0; i < numchildren; i++) {
                    if (count >= max) return count;
                    count++;
                    var childdata = { key: rootkey + count, group: groupkey };
                    nodeDataArray.push(childdata);
                    linkDataArray.push({ from: parentdata.key, to: childdata.key });
                    if (level > 0 && Math.random() > 0.5) {
                        count = makeTree(
                            level - 1,
                            count,
                            max,
                            nodeDataArray,
                            linkDataArray,
                            childdata,
                            groupkey,
                            rootkey
                        );
                    }
                }
                return count;
            }
            window.addEventListener('DOMContentLoaded', init);

            function updateNodeWidths(group, width) {
                if (isNaN(width)) {
                    group.memberParts.each((n) => {
                        if (n instanceof go.Node) n.width = NaN;
                    });
                } else {
                    var minx = Infinity;
                    group.memberParts.each((n) => {
                        if (n instanceof go.Node) {
                            minx = Math.min(minx, n.actualBounds.x);
                        }
                    });
                    if (minx === Infinity) return;
                    var right = minx + width;
                    group.memberParts.each((n) => {
                        if (n instanceof go.Node) n.width = Math.max(0, right - n.actualBounds.x);
                    });
                }
            }

            function changeStyle() {
                var stylename = 'ToGroup';
                var radio = document.getElementsByName('MyRoutingStyle');
                for (var i = 0; i < radio.length; i++) {
                    if (radio[i].checked) {
                        stylename = radio[i].value;
                        break;
                    }
                }
                if (stylename !== ROUTINGSTYLE) {
                    myDiagram.commit((diag) => {
                        ROUTINGSTYLE = stylename;
                        diag.findTopLevelGroups().each((g) => updateNodeWidths(g, NaN));
                        diag.layoutDiagram(true);
                        diag.links.each((l) => l.invalidateRoute());
                    });
                }
            }
        </script>

        <div id="sample">
            <div id="myDiagramDiv" style="border: 1px solid black; width: 700px; height: 350px"></div>
            <pre class="lang-js" style="height: 300px;"><code id="mySavedModel"></code></pre>
        </div>

        <!-- Menú contextual -->
        <ul id="contextMenu" class="menu">
            <li id="cut" class="menu-item" onpointerdown="cxcommand(event)">Cut</li>
            <li id="copy" class="menu-item" onpointerdown="cxcommand(event)">Copy</li>
            <li id="delete" class="menu-item" onpointerdown="cxcommand(event)">Delete</li>
            <li id="color" class="menu-item">
                Color
                <ul class="menu">
                    <li class="menu-item" style="background-color: #f38181" onpointerdown="cxcommand(event, 'color')">
                        Red</li>
                    <li class="menu-item" style="background-color: #eaffd0" onpointerdown="cxcommand(event, 'color')">
                        Green</li>
                    <li class="menu-item" style="background-color: #95e1d3" onpointerdown="cxcommand(event, 'color')">
                        Blue</li>
                    <li class="menu-item" style="background-color: #fce38a" onpointerdown="cxcommand(event, 'color')">
                        Yellow</li>
                </ul>
            </li>
        </ul>
    </div>
</body>

</html>

as you see in my code I use 1 as order

const mappingLink: MappingData = {
          origin: parentNode ? parentNode.data.key : null,
          from: link.data.from,
          group: 0,
          package_id: packageName.value,
          destination: toGroup ? toGroup.data.key : null,
          to: link.data.to,
          order: 1,
          config: configId
        };

But I need it dynamic and change with the context menu can you help me please, thanks

It appears that your trees are each in a Group, and the Group.layout is a TreeLayout that arranges those nodes in a vertical list appropriately indented based on the tree structure. That means the order of the child nodes for each parent node is determined by the TreeLayout.sorting and TreeLayout.comparer properties.

So your context menu just needs to modify some property of each Node, or more likely, some property of each Node.data object, such that the sorting of the children of each parent node puts them in the desired order, and then it needs to invalidate the layout by calling Node.invalidateLayout.

Remember to make the data change(s) and to call invalidateLayout within a single transaction.

@walter thanks for your answer, please can you share with me an example for that examples, I await to your comments

@walter Thank you very much for your help, I have left the code to delete or modify as follows,

diagram.addModelChangedListener((evt: go.ChangedEvent) => {
    if (!evt.isTransactionFinished) return;
    const txn = evt.object;  // a Transaction
    if (txn === null) return;

    txn.changes.each((e: go.ChangedEvent) => {
      // record node insertions and removals
      if (e.change === go.ChangeType.Property) {
        if (e.modelChange === "linkFromKey") {
          console.log(`${e.propertyName} changed From key of link: ${JSON.stringify(e.object)} from: ${e.oldValue} to: ${e.newValue}`);
        } else if (e.modelChange === "linkToKey") {
          console.log(`${e.propertyName} changed To key of link: ${JSON.stringify(e.object)} from: ${e.oldValue} to: ${e.newValue}`);
        }
      } else if (e.change === go.ChangeType.Insert && e.modelChange === "linkDataArray") {
        console.log(`${e.propertyName} added link: ${JSON.stringify(e.newValue)}`);
      } else if (e.change === go.ChangeType.Remove && e.modelChange === "linkDataArray") {
        console.log(`${e.propertyName} removed link: ${JSON.stringify(e.oldValue)}`);
        const removedLinkData = e.oldValue;
        const existingLinkIndex = mappingLinks.findIndex(link => link.from === removedLinkData.from && link.to === removedLinkData.to);
        if (existingLinkIndex > -1) {
          mappingLinks[existingLinkIndex].deleted = true;
        } else {
          const removedMappingLink: MappingData = {
            from: removedLinkData.from,
            to: removedLinkData.to,
            config: configId,
            deleted: true
          };
          mappingLinks.push(removedMappingLink);
        }
      }
  });
});
const handleSave = async () => {
  if (mappingLinks.length > 0) {
    try {
      const linksToSave = mappingLinks.filter(link => !link.deleted);
      console.log('links a GUARDAR', linksToSave)
      const linksToDelete = mappingLinks.filter(link => link.deleted);
      console.log('links a ELIMINAR', linksToDelete)
      if (linksToSave.length > 0) {
        await $treeMapperApi.saveMappingLinks(linksToSave);
        alert('Mapping saved successfully');
      }
      if (linksToDelete.length > 0) {
        await $treeMapperApi.deleteMappingLinks(linksToDelete);
        alert('Mappings deleted successfully');
      }
      mappingLinks.splice(0, mappingLinks.length);
    } catch (error) {
      console.error('Error saving mapping links:', error);
      alert('There was an error saving the mapping');
    }
  } else {
    alert('There are no links to save');
  }
};

I would like you to help me with the context menu part for this tree mapper, the idea is that when I right click it opens the option to change the order, is this possible? I await your comments.

Have you set TreeLayout.sorting and TreeLayout.comparer as I suggested? TreeLayout | GoJS API

Once you are able to control the order of the children of any node, then you can implement your context menu command(s) to change the order in whatever way(s) you wish. Or maybe you want users to be able to drag-and-drop nodes in order to re-order them. I don’t know what you want to do.

@walter


in this example I have group 1 Spouse and group 2 child and the form i589, in this moment the order is getting with the database and we increase as we created more this is the code

const getMappingLinks = async () => {
  const res: LinkData[] = await $treeMapperApi.mappingLinksData(params);
  linkDataArray.value.push(...res);
  const incomingLinksCountValue = incomingLinksCount.value;
  res.forEach(link => {
    const destinationKey = link.to;
    if (destinationKey) {
      if (!incomingLinksCountValue[destinationKey]) {
        const maxOrder = Math.max(0, ...linkDataArray.value
          .filter(l => l.to === destinationKey)
          .map(l => l.order ?? 0)
        );
        incomingLinksCountValue[destinationKey] = maxOrder;
        console.log('DE BACK Inicializando contador links para', destinationKey, 'con valor', maxOrder);
      }
      const currentOrder = link.order ?? 0;
      if (currentOrder > incomingLinksCountValue[destinationKey]) {
        incomingLinksCountValue[destinationKey] = currentOrder;
      }
    }
  });
onMounted(() => {
  getDestination();
  getOrigins();
  getMappingLinks();

  diagram = new Diagram('treeMapperDiv', {
    'commandHandler.copiesTree': true,
    'commandHandler.deletesTree': true,
    'linkingTool.archetypeLinkData': { category: 'Mapping' },
    'linkingTool.linkValidation': checkLink,
    'relinkingTool.linkValidation': checkLink,
    'undoManager.isEnabled': true,
    TreeCollapsed: handleTreeCollapseExpand,
    TreeExpanded: handleTreeCollapseExpand,
    LinkDrawn: async (e: DiagramEvent) => {
      const link = e.subject.part;
      if (link && link.data.category === 'Mapping') {
        const fromNode = link.fromNode;
        const toNode = link.toNode;
        const parentNode = fromNode.findTreeParentNode();
        const toGroup = toNode.containingGroup;
        const destinationKey = toGroup ? toNode.data.key : null;
        if (destinationKey) {
          const incomingLinksCountValue = incomingLinksCount.value;
          const extractedValue = incomingLinksCountValue[destinationKey];
          if (!(destinationKey in incomingLinksCountValue)) {
            const maxOrder = Math.max(
              0,
              ...linkDataArray.value
                .filter(l => l.to === destinationKey)
                .map(l => l.order ?? 0)
            );
            incomingLinksCountValue[destinationKey] = maxOrder;
            console.log('DE FRONT Inicializando contador links para', destinationKey, 'con valor', maxOrder);
          }
          const currentOrder = link.data.order ?? 0;
          const orderValue = (incomingLinksCountValue[destinationKey] ?? 0) + 1;
          incomingLinksCountValue[destinationKey] = orderValue;  
          const mappingLink: MappingData = {
            origin: parentNode ? parentNode.data.key : null,
            from: link.data.from,
            group: 0,
            package_id: packageName.value,
            destination: toGroup ? toGroup.data.key : null,
            to: link.data.to,
            order: orderValue,
            config: configId
          };
          mappingLinks.push(mappingLink);
        }
      }
    }
  });

The main goal we have is to open a context menu on the target node corresponding to the i589 group that shows in this case the incoming link information of group 1 and group 2 with the current order and let the user change to the order they want.
My concern with this is how can I get the information of who is connected to my target group. Our main goal is to create the way for the user to be able to decide what the order will be for this, is this possible, how can we do it? Can you have a similar example that you can share with us? For example if in the database we have 3 connections each with order 1 to 3 but I generate another link and I want it to be order 1, I must be able to allow it to change the orders, I attach an image of our database

To get a collection of all of the Links that connect to a Node, call Node.findLinksInto Node | GoJS API
Presumably you will want to look at or modify a property on each of those Link.data objects.

However, judging from your screenshot, the Links from the “Spouse” and “Child” Groups to the “Form 1589” Group, actually each connect a member Node to a member Node, not to the Groups directly. So you have to call the
Group.findExternalLinksConnected method, Group | GoJS API and check all of the Links to find the ones that you care about. For example you probably want link.toNode.containingGroup === groupi589.

Hi @walter, thank you for your response. I would like to ask you something else. Could you help me with an example of “findExternalTreeLinksConnected”? I look forward to your comments.

What have you tried so far? I don’t understand what problem you are having with it.

Hi @walter Thank you for your reply, I have some questions that I want you to help me resolve.
First I want to know how I can make the context menu only open on the target nodes, I attach the image of what I have built and the code

function showContextMenu(obj: any, myDiagram: any, tool: any) {
    const cxElement = document.getElementById('contextMenu');
    cxElement!.classList.add('show-menu');
    const mousePt = myDiagram.lastInput.viewPoint;
    cxElement!.style.left = mousePt.x + 5 + 'px';
    cxElement!.style.top = mousePt.y + 'px';
    window.addEventListener('pointerdown', hideCX, true);
  }
  function hideContextMenu() {
    cxElement!.classList.remove('show-menu');
    window.removeEventListener('pointerdown', hideCX, true);
  }
});
  1. I want to open the order and the rules in these context menus, so I would like to know how I can add these two characteristics to each of the nodes so that when I open the context menu it brings me this information, for example in my database I want to save this data, I attach the table


    At this moment I have it in code but I don’t take it from the database or from the node characteristics, but in this example that I show you this would be the behavior that I would like it to have.

    For example, in this case I have three links that come from three groups and arrive at my form i589 in first name, the problem now is that if I click on my contextual menu on any of the items it repeats the data that I put in and what I need is the real order and to be able to generate a rule field.
onMounted(() => {
  getDestination();
  getOrigins();
  getMappingLinks();

  diagram = new Diagram('treeMapperDiv', {
    'commandHandler.copiesTree': true,
    'commandHandler.deletesTree': true,
    'linkingTool.archetypeLinkData': { category: 'Mapping' },
    'linkingTool.linkValidation': checkLink,
    'relinkingTool.linkValidation': checkLink,
    'undoManager.isEnabled': true,
    TreeCollapsed: handleTreeCollapseExpand,
    TreeExpanded: handleTreeCollapseExpand,
    LinkDrawn: async (e: DiagramEvent) => {
      const link = e.subject.part;
      if (link && link.data.category === 'Mapping') {
        const fromNode = link.fromNode;
        const toNode = link.toNode;
        const parentNode = fromNode.findTreeParentNode();
        const toGroup = toNode.containingGroup;
        const mappingLink: MappingData = {
          origin: parentNode ? parentNode.data.key : null,
          from: link.data.from,
          group: 0,
          package_id: packageName.value,
          destination: toGroup ? toGroup.data.key : null,
          to: link.data.to,
          order: 1,
          config: configId,
          rule: rulesText.value
        };
        mappingLinks.push(mappingLink);
      }
    }
  });
const handleSave = async () => {
  if (mappingLinks.length > 0) {
    try {
      await $treeMapperApi.saveMappingLinks(mappingLinks);
      alert('Mapping saved successfully');
      mappingLinks.splice(0, mappingLinks.length);
    } catch (error) {
      console.error('Error saving mapping links:', error);
      alert('There was an error saving the mapping');
    }
  } else {
    alert('There are no links to save');
  }
};
const items = ref([
  { id: 1, name: 'ChildFirstName', order: 1 },
  { id: 2, name: 'CLsFirstName', order: 2 },
  { id: 3, name: 'SpouseFirstName', order: 3 }
]);

Thank you very much, I look forward to any comments.

  1. You are ignoring the first argument obj, which is the GraphObject on which the context click occurred, or else null if the context click happened in the diagram’s background. Use that obj to decide what you want the context menu’s commands to operate on.

  2. Is the context menu being shown for a Node? In this example, the “Part A.I. Info…”/“First Name” node in “Form i589”? Did you want to get a list of the input nodes (i.e. the Nodes at the other end of the Links connecting with “First Name”)?
    node.findNodesInto() will give you that if node is that “First Name” Node. I suppose you could put them into a List and sort them if you want.