<section class="im-p-8 im-flex im-flex-col im-w-full im-gap-6">
<PageIndicator item1="Home" item2="Submission Settings" item3="Key Mapping Documents" item4="Form I 589" />
<Card :title="title"
subtitle="On this screen, the user can configure the fields of the uploaded document and map them to their corresponding sources."
button1="Save" @handleBtn1="handleSave">
<div class="im-flex im-p-8 im-self-stretch im-items-stretch im-h-full h-screen">
<div id="treeMapperDiv" class="im-w-full im-h-screen im-bg-primary-neutral-200 im-rounded-4"></div>
<ul id="contextMenu" class="menu">
<li id="rules" class="menu-item">Rules</li>
</ul>
</div>
</Card>
</section>
</template>
<script setup lang="ts">
import type { LinkData, MappingData, NodeData, TreeMapperParams } from "~/@types/imigrateTypes";
const route = useRoute();
const packageName = ref();
const title: any = route.query.t;
packageName.value = route.query.pi;
const { $treeMapperApi, } = useNuxtApp();
definePageMeta({
layout: 'home'
});
import go, {
Binding,
Diagram,
DiagramEvent,
GraphObject,
Group,
Link,
Margin,
Node,
Panel,
Placeholder,
Point,
Routing,
Shape,
Spot,
TextBlock,
TreeAlignment,
TreeCompaction,
TreeLayout
} from 'gojs';
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 staticNodeDataArray: NodeData[] = [
{
isGroup: true,
key: 'Form_I589',
text: 'Form i589',
xy: '1120 -500',
width: 150
},
];
const staticLinkDataArray: LinkData[] = [];
const nodeDataArray = ref<NodeData[]>(staticNodeDataArray);
const linkDataArray = ref<LinkData[]>(staticLinkDataArray);
const formDestination = 'Form_I589';
const configId = 'CRD001';
const params: TreeMapperParams = {
d: formDestination,
c: configId
}
const mappingLinks: MappingData[] = [];
const $ = GraphObject.make;
let ROUTINGSTYLE = 'ToNode';
const getDestination = async () => {
const res: any = await $treeMapperApi.nodeDataLink(formDestination);
const newNodeDataArray: NodeData[] = res.nodeDataArray;
const newLinkDataArray: LinkData[] = res.linkDataArray;
nodeDataArray.value.push(...newNodeDataArray);
linkDataArray.value.push(...newLinkDataArray);
diagram.model = new go.GraphLinksModel(nodeDataArray.value, linkDataArray.value);
};
const getOrigins = async () => {
const res: any = await $treeMapperApi.originGroups();
const newNodeDataOrigin: NodeData[] = res.nodes;
const newLinkDataOrigin: LinkData[] = res.links;
nodeDataArray.value.push(...newNodeDataOrigin);
linkDataArray.value.push(...newLinkDataOrigin);
diagram.model = new go.GraphLinksModel(nodeDataArray.value, linkDataArray.value);
}
const getMappingLinks = async () => {
const res: LinkData[] = await $treeMapperApi.mappingLinksData(params);
linkDataArray.value.push(...res);
diagram.model = new go.GraphLinksModel(nodeDataArray.value, linkDataArray.value);
}
class MyLayout extends TreeLayout {
constructor() {
super();
}
commitNodes(): void {
super.commitNodes.call(this);
if (ROUTINGSTYLE === 'ToGroup') {
updateNodeWidths(this.group!, this.group?.data.width ?? 100);
}
}
}
class TreeNode extends Node {
findVisibleNode(): Node | null {
let n: Node | null = this;
while (n !== null && !n.isVisible()) {
n = n.findTreeParentNode();
}
return n;
}
}
class MappingLink extends Link {
getLinkPoint(
node: Node | null,
port: GraphObject,
spot: Spot,
from: boolean,
ortho: boolean,
othernode: Node | null,
otherport: GraphObject
) {
if (ROUTINGSTYLE !== 'ToGroup') {
return super.getLinkPoint(
node,
port,
spot,
from,
ortho,
othernode,
otherport
);
} else {
const r = port.getDocumentBounds();
const group = node!.containingGroup;
const b = group !== null ? group.actualBounds : node!.actualBounds;
const op = othernode!.getDocumentPoint(Spot.Center);
const x = op.x > r.centerX ? b.right : b.left;
return new Point(x, r.centerY);
}
}
checkFromNodeToNode(fn: Node, tn: Node) {
const fg = fn.containingGroup;
const fb = fg ? fg.actualBounds : fn.actualBounds;
const fpt = this.getPoint(0);
const tg = tn.containingGroup;
const tb = tg ? tg.actualBounds : tn.actualBounds;
const tpt = this.getPoint(this.pointsCount - 1);
this.setPoint(1, new Point(fpt.x < tpt.x ? fb.right : fb.left, fpt.y));
this.setPoint(
this.pointsCount - 2,
new Point(fpt.x < tpt.x ? tb.left : tb.right, tpt.y)
);
}
computePoints() {
const result = super.computePoints();
if (result && ROUTINGSTYLE === 'ToNode') {
const fn = this.fromNode;
const tn = this.toNode;
if (fn && tn) {
this.checkFromNodeToNode(fn, tn);
}
}
return result;
}
}
function hasChildren(node: Node) {
return node.findTreeChildrenNodes().count > 0;
}
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;
};
const handleTreeCollapseExpand = (e: DiagramEvent) => {
e.subject.each((n: any) => {
n.findExternalTreeLinksConnected().each((l: any) => l.invalidateRoute());
});
};
const updateNodeWidths = (group: Group, width: any) => {
if (isNaN(width)) {
group.memberParts.each((n) => {
if (n instanceof Node) n.width = NaN; // back to natural width
});
} else {
let minx = Infinity; // figure out minimum group width
group.memberParts.each((n) => {
if (n instanceof Node) {
minx = Math.min(minx, n.actualBounds.x);
}
});
if (minx === Infinity) return;
let right = minx + width;
group.memberParts.each((n) => {
if (n instanceof Node) n.width = Math.max(0, right - n.actualBounds.x);
});
}
};
const makeGroupLayout = () => {
return $(MyLayout, {
alignment: TreeAlignment.Start,
angle: 0,
compaction: TreeCompaction.None,
layerSpacing: 16,
layerSpacingParentOverlap: 1,
nodeIndentPastParent: 1.0,
nodeSpacing: 5,
setsPortSpot: false,
setsChildPortSpot: false
});
};
const makeTree = (
level: number,
count: number,
max: number,
nodeDataArray: NodeData[],
linkDataArray: LinkData[],
parentdata: NodeData,
groupkey: number,
rootkey: number
) => {
const numchildren = Math.floor(Math.random() * 10);
for (let i = 0; i < numchildren; i++) {
if (count >= max) return count;
count++;
const childData: any = { 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;
};
// Asegúrate de que contextMenuRef esté correctamente asignado
const contextMenuRef = ref<HTMLElement | null>(null);
const handleContextMenuShowing = (e: go.InputEvent) => {
console.log('Context menu is being shown', e);
const menu = contextMenuRef.value;
if (menu) {
// Ajusta la posición del menú usando las coordenadas del evento
menu.style.display = 'block';
menu.style.left = `${e.viewPoint.x}px`;
menu.style.top = `${e.viewPoint.y}px`;
menu.style.opacity = '1';
e.handled = true; // Previene el comportamiento por defecto del menú contextual
}
};
const handleContextMenuHiding = () => {
const menu = contextMenuRef.value;
if (menu) {
menu.style.display = 'none';
}
};
let diagram: Diagram;
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,
'contextMenuTool': new go.ContextMenuTool(),
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);
}
}
});
const selectionAdornmentTemplate = $(
go.Adornment, 'Auto',
$(go.Shape, 'RoundedRectangle', {
fill: null,
stroke: '#55C6C0',
strokeWidth: 2
}),
$(go.Placeholder)
);
diagram.nodeTemplate = $(
TreeNode,
{ isTreeExpanded: false, movable: false, copyable: false, deletable: false },
{
selectionAdorned: true,
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);
});
</script>
<style type="text/css">
/* CSS for the traditional context menu */
.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;
}
.menu .menu {
top: -8px;
left: 100%;
}
.show-menu,
.menu-item:hover>.menu {
display: block;
opacity: 1;
}
</style>
In this VUE code I have tried to use the context menu but it has not been possible, I have not had any errors but I cannot get the behavior of the demo.
Basically I was trying to make the mini modal with the word Rules appear when I right-click on the link or node, I put an address
const contextMenuTool = diagram.toolManager.contextMenuTool as go.ContextMenuTool;
// Configure the context menu for the diagram
contextMenuTool.showContextMenu = (contextMenu: go.HTMLInfo, obj: go.GraphObject | null) => {
console.log(‘Context menu tool is triggered’);
if (contextMenuRef.value) {
handleContextMenuShowing(contextMenu as unknown as go.InputEvent);
}
};
contextMenuTool.hideContextMenu = handleContextMenuHiding;
document.getElementById(‘rules’)?.addEventListener(‘click’, () => {
window.open(‘https://www.google.com’, ‘_blank’);
});
contextMenuRef.value = document.getElementById(‘contextMenu’);
Could someone please guide me? I really appreciate your help