From 72263c5c7bb04fad4113b64439c91bda822f1d87 Mon Sep 17 00:00:00 2001 From: mabashian Date: Fri, 16 Nov 2018 12:12:39 -0500 Subject: [PATCH] Addresses a number of workflow related bugs --- .../workflow-chart/workflow-chart.block.less | 12 +- .../workflow-chart.directive.js | 209 ++++++++++-------- .../workflow-chart/workflow-chart.service.js | 1 - .../forms/workflow-link-form.partial.html | 8 +- .../forms/workflow-node-form.partial.html | 8 +- .../workflow-maker.controller.js | 199 +++++++++-------- 6 files changed, 229 insertions(+), 208 deletions(-) diff --git a/awx/ui/client/src/templates/workflows/workflow-chart/workflow-chart.block.less b/awx/ui/client/src/templates/workflows/workflow-chart/workflow-chart.block.less index 0d5130a418..830a02dcd1 100644 --- a/awx/ui/client/src/templates/workflows/workflow-chart/workflow-chart.block.less +++ b/awx/ui/client/src/templates/workflows/workflow-chart/workflow-chart.block.less @@ -186,16 +186,8 @@ margin: auto; } -.WorkflowChart-tooltipArrow--right { - width: 0; - height: 0; - border-top: 10px solid transparent; - border-bottom: 10px solid transparent; - border-left: 10px solid @default-interface-txt; - margin: auto; - position: relative; - right: -55px; - top: -34px; +.WorkflowChart-tooltipArrow { + fill: @default-interface-txt; } .WorkflowChart-dashedNode { diff --git a/awx/ui/client/src/templates/workflows/workflow-chart/workflow-chart.directive.js b/awx/ui/client/src/templates/workflows/workflow-chart/workflow-chart.directive.js index 6d6520a1fa..4ec4f2495e 100644 --- a/awx/ui/client/src/templates/workflows/workflow-chart/workflow-chart.directive.js +++ b/awx/ui/client/src/templates/workflows/workflow-chart/workflow-chart.directive.js @@ -240,6 +240,81 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge function update() { if(scope.dimensionsSet) { + const buildLinkTooltip = (d) => { + let sourceNode = d3.select(`#node-${d.source.id}`); + const sourceNodeX = d3.transform(sourceNode.attr("transform")).translate[0]; + const sourceNodeY = d3.transform(sourceNode.attr("transform")).translate[1]; + let targetNode = d3.select(`#node-${d.target.id}`); + const targetNodeX = d3.transform(targetNode.attr("transform")).translate[0]; + const targetNodeY = d3.transform(targetNode.attr("transform")).translate[1]; + let xPos, yPos, arrowPoints; + if (nodePositionMap[d.source.id].y === nodePositionMap[d.target.id].y) { + xPos = (sourceNodeX + nodeW + targetNodeX)/2 - 50; + yPos = (sourceNodeY + nodeH + targetNodeY)/2 - 70; + arrowPoints = { + pt1: { + x: xPos + 40, + y: yPos + 47 + }, + pt2: { + x: xPos + 60, + y: yPos + 47 + }, + pt3: { + x: xPos + 50, + y: yPos + 57 + } + }; + } else { + xPos = (sourceNodeX + nodeW + targetNodeX)/2 - 120; + yPos = (sourceNodeY + nodeH + targetNodeY)/2 - 30; + arrowPoints = { + pt1: { + x: xPos + 100, + y: yPos + 17 + }, + pt2: { + x: xPos + 100, + y: yPos + 33 + }, + pt3: { + x: xPos + 110, + y: yPos + 25 + } + }; + } + let edgeTypeLabel; + switch(d.edgeType) { + case "always": + edgeTypeLabel = TemplatesStrings.get('workflow_maker.ALWAYS'); + break; + case "success": + edgeTypeLabel = TemplatesStrings.get('workflow_maker.ON_SUCCESS'); + break; + case "failure": + edgeTypeLabel = TemplatesStrings.get('workflow_maker.ON_FAILURE'); + break; + } + let linkInstructionText = !scope.readOnly ? TemplatesStrings.get('workflow_maker.EDIT_LINK_TOOLTIP') : TemplatesStrings.get('workflow_maker.VIEW_LINK_TOOLTIP'); + let linkTooltip = svgGroup.append("g") + .attr("class", "WorkflowChart-tooltip"); + linkTooltip.append("foreignObject") + .attr("transform", `translate(${xPos},${yPos})`) + .attr("width", 100) + .attr("height", 50) + .html(function(){ + return `
+
${TemplatesStrings.get('workflow_maker.RUN')}: ${edgeTypeLabel}
+
${linkInstructionText}
+
`; + }); + linkTooltip.append("polygon") + .attr("class", "WorkflowChart-tooltipArrow") + .attr("points", function() { + return `${arrowPoints.pt1.x},${arrowPoints.pt1.y} ${arrowPoints.pt2.x},${arrowPoints.pt2.y} ${arrowPoints.pt3.x},${arrowPoints.pt3.y}`; + }); + }; + var g = new dagre.graphlib.Graph(); g.setGraph({rankdir: 'LR', nodesep: 30, ranksep: 120}); @@ -336,7 +411,7 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge baseSvg.selectAll(".WorkflowChart-circleBetweenNodes") .attr("id", function(d){return "link-" + d.source.id + "-" + d.target.id + "-add";}) - .style("display", function(d) { return (scope.graphState.isLinkMode || d.source.id === scope.graphState.nodeBeingAdded || d.target.id === scope.graphState.nodeBeingAdded || scope.readOnly) ? "none" : null; }) + .style("display", function(d) { return (d.edgeType === 'placeholder' || scope.graphState.isLinkMode || d.source.id === scope.graphState.nodeBeingAdded || d.target.id === scope.graphState.nodeBeingAdded || scope.readOnly) ? "none" : null; }) .attr("cx", function(d) { return (nodePositionMap[d.source.id].x + nodePositionMap[d.source.id].width + nodePositionMap[d.target.id].x)/2; }) @@ -356,7 +431,7 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge }); baseSvg.selectAll(".WorkflowChart-betweenNodesIcon") - .style("display", function(d) { return (scope.graphState.isLinkMode || d.source.id === scope.graphState.nodeBeingAdded || d.target.id === scope.graphState.nodeBeingAdded || scope.readOnly) ? "none" : null; }) + .style("display", function(d) { return (d.edgeType === 'placeholder' || scope.graphState.isLinkMode || d.source.id === scope.graphState.nodeBeingAdded || d.target.id === scope.graphState.nodeBeingAdded || scope.readOnly) ? "none" : null; }) .attr("transform", function(d) { let translate; @@ -411,48 +486,20 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge return [pt1, pt2, pt3, pt4].join(" "); }) .on("mouseover", function(d) { - if(!scope.graphState.isLinkMode && !d.source.isStartNode && d.source.id !== scope.graphState.nodeBeingAdded && d.target.id !== scope.graphState.nodeBeingAdded && scope.mode !== 'details') { + if( + d.edgeType !== 'placeholder' && + !scope.graphState.isLinkMode && + !d.source.isStartNode && + d.source.id !== scope.graphState.nodeBeingAdded && + d.target.id !== scope.graphState.nodeBeingAdded && + scope.mode !== 'details' + ) { $(`#link-${d.source.id}-${d.target.id}`).appendTo(`#aw-workflow-chart-g`); d3.select(`#link-${d.source.id}-${d.target.id}`) .classed("WorkflowChart-linkHovering", true); - let xPos, yPos, arrowClass; - if (nodePositionMap[d.source.id].y === nodePositionMap[d.target.id].y) { - xPos = (nodePositionMap[d.source.id].x + nodePositionMap[d.target.id].x)/2 + 45; - yPos = (nodePositionMap[d.source.id].y + nodePositionMap[d.target.id].y)/2 - 107; - arrowClass = 'WorkflowChart-tooltipArrow--down'; - } else { - xPos = (nodePositionMap[d.source.id].x + nodePositionMap[d.target.id].x)/2 - 30; - yPos = (nodePositionMap[d.source.id].y + nodePositionMap[d.target.id].y)/2 - 70; - arrowClass = 'WorkflowChart-tooltipArrow--right'; - } - - let edgeTypeLabel; - - switch(d.edgeType) { - case "always": - edgeTypeLabel = TemplatesStrings.get('workflow_maker.ALWAYS'); - break; - case "success": - edgeTypeLabel = TemplatesStrings.get('workflow_maker.ON_SUCCESS'); - break; - case "failure": - edgeTypeLabel = TemplatesStrings.get('workflow_maker.ON_FAILURE'); - break; - } - - let linkInstructionText = !scope.readOnly ? TemplatesStrings.get('workflow_maker.EDIT_LINK_TOOLTIP') : TemplatesStrings.get('workflow_maker.VIEW_LINK_TOOLTIP'); - - svgGroup.append("foreignObject") - .attr("transform", `translate(${xPos},${yPos})`) - .attr("width", 100) - .attr("height", 60) - .attr("class", "WorkflowChart-tooltip") - .html(function(){ - return `
${TemplatesStrings.get('workflow_maker.RUN')}: ${edgeTypeLabel}
${linkInstructionText}
`; - }); + buildLinkTooltip(d); } - }) .on("mouseout", function(d){ if(!d.source.isStartNode && d.target.id !== scope.graphState.nodeBeingAdded && scope.mode !== 'details') { @@ -471,46 +518,19 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge .attr("d", lineData) .call(edit_link) .on("mouseenter", function(d) { - if(!scope.graphState.isLinkMode && !d.source.isStartNode && d.source.id !== scope.graphState.nodeBeingAdded && d.target.id !== scope.graphState.nodeBeingAdded && scope.mode !== 'details') { + if( + d.edgeType !== 'placeholder' && + !scope.graphState.isLinkMode && + !d.source.isStartNode && + d.source.id !== scope.graphState.nodeBeingAdded && + d.target.id !== scope.graphState.nodeBeingAdded && + scope.mode !== 'details' + ) { $(`#link-${d.source.id}-${d.target.id}`).appendTo(`#aw-workflow-chart-g`); - d3.select("#link-" + d.source.id + "-" + d.target.id) + d3.select(`#link-${d.source.id}-${d.target.id}`) .classed("WorkflowChart-linkHovering", true); - let xPos, yPos, arrowClass; - if (nodePositionMap[d.source.id].y === nodePositionMap[d.target.id].y) { - xPos = (nodePositionMap[d.source.id].x + nodePositionMap[d.target.id].x)/2 + 45; - yPos = (nodePositionMap[d.source.id].y + nodePositionMap[d.target.id].y)/2 - 107; - arrowClass = 'WorkflowChart-tooltipArrow--down'; - } else { - xPos = (nodePositionMap[d.source.id].x + nodePositionMap[d.target.id].x)/2 - 30; - yPos = (nodePositionMap[d.source.id].y + nodePositionMap[d.target.id].y)/2 - 70; - arrowClass = 'WorkflowChart-tooltipArrow--right'; - } - - let edgeTypeLabel; - - switch(d.edgeType) { - case "always": - edgeTypeLabel = TemplatesStrings.get('workflow_maker.ALWAYS'); - break; - case "success": - edgeTypeLabel = TemplatesStrings.get('workflow_maker.ON_SUCCESS'); - break; - case "failure": - edgeTypeLabel = TemplatesStrings.get('workflow_maker.ON_FAILURE'); - break; - } - - let linkInstructionText = !scope.readOnly ? TemplatesStrings.get('workflow_maker.EDIT_LINK_TOOLTIP') : TemplatesStrings.get('workflow_maker.VIEW_LINK_TOOLTIP'); - - svgGroup.append("foreignObject") - .attr("transform", `translate(${xPos},${yPos})`) - .attr("width", 100) - .attr("height", 60) - .attr("class", "WorkflowChart-tooltip") - .html(function(){ - return `
${TemplatesStrings.get('workflow_maker.RUN')}: ${edgeTypeLabel}
${linkInstructionText}
`; - }); + buildLinkTooltip(d); } }) .on("mouseleave", function(d){ @@ -543,7 +563,7 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge .attr("id", function(d){return "link-" + d.source.id + "-" + d.target.id + "-add";}) .attr("r", 10) .attr("class", "WorkflowChart-addCircle WorkflowChart-circleBetweenNodes") - .style("display", function(d) { return (scope.graphState.isLinkMode || d.source.id === scope.graphState.nodeBeingAdded || d.target.id === scope.graphState.nodeBeingAdded || scope.readOnly) ? "none" : null; }) + .style("display", function(d) { return (d.edgeType === 'placeholder' || scope.graphState.isLinkMode || d.source.id === scope.graphState.nodeBeingAdded || d.target.id === scope.graphState.nodeBeingAdded || scope.readOnly) ? "none" : null; }) .attr("cx", function(d) { return (nodePositionMap[d.source.id].x + nodePositionMap[d.source.id].width + nodePositionMap[d.target.id].x)/2; }) @@ -580,7 +600,7 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge .size(60) .type("cross") ) - .style("display", function(d) { return (scope.graphState.isLinkMode || d.source.id === scope.graphState.nodeBeingAdded || d.target.id === scope.graphState.nodeBeingAdded || scope.readOnly) ? "none" : null; }) + .style("display", function(d) { return (d.edgeType === 'placeholder' || scope.graphState.isLinkMode || d.source.id === scope.graphState.nodeBeingAdded || d.target.id === scope.graphState.nodeBeingAdded || scope.readOnly) ? "none" : null; }) .attr("transform", function(d) { let translate; @@ -1263,7 +1283,7 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge function add_node_with_child() { this.on("click", function(d) { - if(!scope.readOnly && !scope.graphState.isLinkMode) { + if(!scope.readOnly && !scope.graphState.isLinkMode && d.edgeType !== 'placeholder') { scope.addNodeWithChild({ link: d }); @@ -1340,25 +1360,32 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge } }; - if(d.job.id) { + if(d.job.type) { if(d.unifiedJobTemplate) { goToJobResults(d.unifiedJobTemplate.unified_job_type); } else { - // We don't have access to the unified resource and have to make + // We don't have access to the job type and have to make // a GET request in order to find out what type job this was // so that we can route the user to the correct stdout view - - Rest.setUrl(GetBasePath("unified_jobs") + "?id=" + d.job.id); + Rest.setUrl(GetBasePath("workflow_jobs") + `${d.originalNodeObj.workflow_job}/workflow_nodes/?order_by=id`); Rest.get() - .then(function (res) { - if(res.data.results && res.data.results.length > 0) { - goToJobResults(res.data.results[0].type); - } - }) - .catch(({data, status}) => { - ProcessErrors(scope, data, status, null, { hdr: 'Error!', msg: 'Unable to get job: ' + status }); - }); + .then(function (res) { + if (res.data.results && res.data.results.length > 0) { + const { results } = res.data; + const job = results.filter(result => result.summary_fields.job.id === d.job.id); + goToJobResults(job[0].summary_fields.job.type); + } + }) + .catch(({ + data, + status + }) => { + ProcessErrors(scope, data, status, null, { + hdr: 'Error!', + msg: 'Unable to get job: ' + status + }); + }); } } }); diff --git a/awx/ui/client/src/templates/workflows/workflow-chart/workflow-chart.service.js b/awx/ui/client/src/templates/workflows/workflow-chart/workflow-chart.service.js index 2973dc1e70..d807f7d24c 100644 --- a/awx/ui/client/src/templates/workflows/workflow-chart/workflow-chart.service.js +++ b/awx/ui/client/src/templates/workflows/workflow-chart/workflow-chart.service.js @@ -97,7 +97,6 @@ export default [function(){ return { arrayOfNodesForChart, arrayOfLinksForChart, - chartNodeIdToIndexMapping, nodeIdToChartNodeIdMapping, nodeRef, workflowMakerNodeIdCounter: nodeIdCounter diff --git a/awx/ui/client/src/templates/workflows/workflow-maker/forms/workflow-link-form.partial.html b/awx/ui/client/src/templates/workflows/workflow-maker/forms/workflow-link-form.partial.html index 2bf0315447..cf384b6411 100644 --- a/awx/ui/client/src/templates/workflows/workflow-maker/forms/workflow-link-form.partial.html +++ b/awx/ui/client/src/templates/workflows/workflow-maker/forms/workflow-link-form.partial.html @@ -1,8 +1,8 @@ -
{{readOnly ? strings.get('workflow_maker.VIEW_LINK') : (linkConfig.mode === 'add' ? strings.get('workflow_maker.ADD_LINK') : strings.get('workflow_maker.EDIT_LINK')) }} | {{linkConfig.parent.name}} {{linkConfig.child ? 'to ' + linkConfig.child.name : ''}}
+
{{readOnly ? strings.get('workflow_maker.VIEW_LINK') : (linkConfig.mode === 'add' ? strings.get('workflow_maker.ADD_LINK') : strings.get('workflow_maker.EDIT_LINK')) }} | {{linkConfig.source.name}} {{linkConfig.target ? 'to ' + linkConfig.target.name : ''}}
-
{{:: strings.get('workflow_maker.NEW_LINK')}}
- +
{{:: strings.get('workflow_maker.NEW_LINK')}}
+
diff --git a/awx/ui/client/src/templates/workflows/workflow-maker/forms/workflow-node-form.partial.html b/awx/ui/client/src/templates/workflows/workflow-maker/forms/workflow-node-form.partial.html index b8866b47b1..e48a4e4f08 100644 --- a/awx/ui/client/src/templates/workflows/workflow-maker/forms/workflow-node-form.partial.html +++ b/awx/ui/client/src/templates/workflows/workflow-maker/forms/workflow-node-form.partial.html @@ -1,11 +1,11 @@
-
{{nodeConfig.mode === 'edit' ? nodeConfig.node.unifiedJobTemplate.name : strings.get('workflow_maker.ADD_A_TEMPLATE')}}
-
+
{{nodeConfig.mode === 'edit' ? nodeConfig.node.fullUnifiedJobTemplateObject.name || nodeConfig.node.unifiedJobTemplate.name : strings.get('workflow_maker.ADD_A_TEMPLATE')}}
+
{{strings.get('workflow_maker.JOBS')}}
{{strings.get('workflow_maker.PROJECT_SYNC')}}
{{strings.get('workflow_maker.INVENTORY_SYNC')}}
-
+
@@ -141,7 +141,7 @@
-
+
{ - if (foo.source.id === link.source.id && foo.target.id === link.target.id) { - foo.source = $scope.graphState.arrayOfNodesForChart[chartNodeIdToIndexMapping[workflowMakerNodeIdCounter]]; + $scope.graphState.arrayOfLinksForChart.forEach((linkToCompare) => { + if (linkToCompare.source.id === link.source.id && linkToCompare.target.id === link.target.id) { + linkToCompare.source = {id: workflowMakerNodeIdCounter}; } }); @@ -440,7 +433,7 @@ export default ['$scope', 'TemplatesService', }; $scope.confirmNodeForm = function(selectedTemplate, promptData, edgeType) { - const nodeIndex = chartNodeIdToIndexMapping[$scope.nodeConfig.nodeId]; + const nodeId = $scope.nodeConfig.nodeId; if ($scope.nodeConfig.mode === "add") { if (selectedTemplate && edgeType && edgeType.value) { nodeRef[$scope.nodeConfig.nodeId] = { @@ -449,11 +442,10 @@ export default ['$scope', 'TemplatesService', isNew: true }; - $scope.graphState.arrayOfNodesForChart[nodeIndex].unifiedJobTemplate = selectedTemplate; $scope.graphState.nodeBeingAdded = null; $scope.graphState.arrayOfLinksForChart.map( (link) => { - if (link.target.index === nodeIndex) { + if (link.target.id === nodeId) { link.edgeType = edgeType.value; } }); @@ -463,11 +455,16 @@ export default ['$scope', 'TemplatesService', nodeRef[$scope.nodeConfig.nodeId].fullUnifiedJobTemplateObject = selectedTemplate; nodeRef[$scope.nodeConfig.nodeId].promptData = _.cloneDeep(promptData); nodeRef[$scope.nodeConfig.nodeId].isEdited = true; - $scope.graphState.arrayOfNodesForChart[nodeIndex].unifiedJobTemplate = selectedTemplate; $scope.graphState.nodeBeingEdited = null; } } + $scope.graphState.arrayOfNodesForChart.map( (node) => { + if (node.id === nodeId) { + node.unifiedJobTemplate = selectedTemplate; + } + }); + $scope.formState.showNodeForm = false; $scope.nodeConfig = null; @@ -475,10 +472,15 @@ export default ['$scope', 'TemplatesService', }; $scope.cancelNodeForm = function() { - const nodeIndex = chartNodeIdToIndexMapping[$scope.nodeConfig.nodeId]; + const nodeId = $scope.nodeConfig.nodeId; if ($scope.nodeConfig.mode === "add") { // Remove the placeholder node from the array - $scope.graphState.arrayOfNodesForChart.splice(nodeIndex, 1); + for( let i = $scope.graphState.arrayOfNodesForChart.length; i--; ){ + if ($scope.graphState.arrayOfNodesForChart[i].id === nodeId) { + $scope.graphState.arrayOfNodesForChart.splice(i, 1); + i = 0; + } + } // Update the links let parents = []; @@ -488,47 +490,30 @@ export default ['$scope', 'TemplatesService', for( let i = $scope.graphState.arrayOfLinksForChart.length; i--; ){ const link = $scope.graphState.arrayOfLinksForChart[i]; - if (link.source.index === nodeIndex || link.target.index === nodeIndex) { - if (link.source.index === nodeIndex) { - const targetIndex = link.target.index < nodeIndex ? link.target.index : link.target.index - 1; - children.push({index: targetIndex, edgeType: link.edgeType}); - } else if (link.target.index === nodeIndex) { - const sourceIndex = link.source.index < nodeIndex ? link.source.index : link.source.index - 1; - parents.push(sourceIndex); + if (link.source.id === nodeId || link.target.id === nodeId) { + if (link.source.id === nodeId) { + children.push({id: link.target.id, edgeType: link.edgeType}); + } else if (link.target.id === nodeId) { + parents.push(link.source.id); } $scope.graphState.arrayOfLinksForChart.splice(i, 1); - } else { - if (link.source.index > nodeIndex) { - link.source.index--; - } - if (link.target.index > nodeIndex) { - link.target.index--; - } } } // Add the new links - parents.forEach((parentIndex) => { + parents.forEach((parentId) => { children.forEach((child) => { - if (parentIndex === 0) { + if (parentId === 1) { child.edgeType = "always"; } $scope.graphState.arrayOfLinksForChart.push({ - source: $scope.graphState.arrayOfNodesForChart[parentIndex], - target: $scope.graphState.arrayOfNodesForChart[child.index], + source: {id: parentId}, + target: {id: child.id}, edgeType: child.edgeType }); }); }); - delete chartNodeIdToIndexMapping[$scope.nodeConfig.nodeId]; - - for (const key in chartNodeIdToIndexMapping) { - if (chartNodeIdToIndexMapping[key] > nodeIndex) { - chartNodeIdToIndexMapping[key]--; - } - } - } else if ($scope.nodeConfig.mode === "edit") { $scope.graphState.nodeBeingEdited = null; } @@ -544,7 +529,7 @@ export default ['$scope', 'TemplatesService', $scope.cancelLinkForm(); } - if (!$scope.nodeConfig || ($scope.nodeConfig && $scope.nodeConfig.nodeId !== nodeToEdit.index)) { + if (!$scope.nodeConfig || ($scope.nodeConfig && $scope.nodeConfig.nodeId !== nodeToEdit.id)) { if ($scope.nodeConfig) { $scope.cancelNodeForm(); } @@ -583,11 +568,11 @@ export default ['$scope', 'TemplatesService', $scope.linkConfig = { mode: "edit", - parent: { + source: { id: linkToEdit.source.id, name: _.get(linkToEdit, 'source.unifiedJobTemplate.name') || "" }, - child: { + target: { id: linkToEdit.target.id, name: _.get(linkToEdit, 'target.unifiedJobTemplate.name') || "" }, @@ -604,7 +589,7 @@ export default ['$scope', 'TemplatesService', } if ($scope.linkConfig) { - if ($scope.linkConfig.parent.id !== linkToEdit.source.id || $scope.linkConfig.child.id !== linkToEdit.target.id) { + if ($scope.linkConfig.source.id !== linkToEdit.source.id || $scope.linkConfig.target.id !== linkToEdit.target.id) { // User is going from editing one link to editing another if ($scope.linkConfig.mode === "add") { $scope.graphState.arrayOfLinksForChart.splice($scope.graphState.arrayOfLinksForChart.length-1, 1); @@ -621,27 +606,31 @@ export default ['$scope', 'TemplatesService', if ($scope.nodeConfig) { $scope.cancelNodeForm(); } + // User was add/editing a link and then hit the link icon + if ($scope.linkConfig && $scope.linkConfig.target) { + $scope.cancelLinkForm(); + } if ($scope.linkConfig) { // This is the second node selected - $scope.linkConfig.child = { + $scope.linkConfig.target = { id: node.id, name: node.unifiedJobTemplate.name }; $scope.linkConfig.edgeType = "success"; - $scope.graphState.arrayOfNodesForChart.forEach((node) => { - node.isInvalidLinkTarget = false; + $scope.graphState.arrayOfNodesForChart.forEach((nodeToUpdate) => { + nodeToUpdate.isInvalidLinkTarget = false; }); $scope.graphState.arrayOfLinksForChart.push({ - target: $scope.graphState.arrayOfNodesForChart[node.index], - source: $scope.graphState.arrayOfNodesForChart[chartNodeIdToIndexMapping[$scope.linkConfig.parent.id]], + source: {id: $scope.linkConfig.source.id}, + target: {id: node.id}, edgeType: "placeholder" }); $scope.graphState.linkBeingEdited = { - source: $scope.graphState.arrayOfNodesForChart[node.index].id, - target: $scope.graphState.arrayOfNodesForChart[chartNodeIdToIndexMapping[$scope.linkConfig.parent.id]].id + source: {id: $scope.linkConfig.source.id}, + target: {id: node.id} }; $scope.graphState.arrayOfLinksForChart.forEach((link, index) => { @@ -656,7 +645,7 @@ export default ['$scope', 'TemplatesService', $scope.graphState.addLinkSource = node.id; $scope.linkConfig = { mode: "add", - parent: { + source: { id: node.id, name: node.unifiedJobTemplate.name } @@ -693,7 +682,11 @@ export default ['$scope', 'TemplatesService', // Filter out the duplicates invalidLinkTargetIds.filter((element, index, array) => index === array.indexOf(element)).forEach((ancestorId) => { - $scope.graphState.arrayOfNodesForChart[chartNodeIdToIndexMapping[ancestorId]].isInvalidLinkTarget = true; + $scope.graphState.arrayOfNodesForChart.forEach((node) => { + if (node.id === ancestorId) { + node.isInvalidLinkTarget = true; + } + }); }); $scope.graphState.isLinkMode = true; @@ -706,7 +699,7 @@ export default ['$scope', 'TemplatesService', $scope.confirmLinkForm = (newEdgeType) => { $scope.graphState.arrayOfLinksForChart.forEach((link) => { - if (link.source.id === $scope.linkConfig.parent.id && link.target.id === $scope.linkConfig.child.id) { + if (link.source.id === $scope.linkConfig.source.id && link.target.id === $scope.linkConfig.target.id) { link.edgeType = newEdgeType; } }); @@ -729,7 +722,7 @@ export default ['$scope', 'TemplatesService', for( let i = $scope.graphState.arrayOfLinksForChart.length; i--; ){ const link = $scope.graphState.arrayOfLinksForChart[i]; - if (link.source.id === $scope.linkConfig.parent.id && link.target.id === $scope.linkConfig.child.id) { + if (link.source.id === $scope.linkConfig.source.id && link.target.id === $scope.linkConfig.target.id) { $scope.graphState.arrayOfLinksForChart.splice(i, 1); } } @@ -740,19 +733,19 @@ export default ['$scope', 'TemplatesService', }; $scope.cancelLinkForm = () => { - if ($scope.linkConfig.mode === "add" && $scope.linkConfig.child) { + if ($scope.linkConfig.mode === "add" && $scope.linkConfig.target) { $scope.graphState.arrayOfLinksForChart.splice($scope.graphState.arrayOfLinksForChart.length-1, 1); let targetIsOrphaned = true; $scope.graphState.arrayOfLinksForChart.forEach((link) => { - if (link.target.id === $scope.linkConfig.child.id) { + if (link.target.id === $scope.linkConfig.target.id) { targetIsOrphaned = false; } }); if (targetIsOrphaned) { // Link it to the start node $scope.graphState.arrayOfLinksForChart.push({ - source: $scope.graphState.arrayOfNodesForChart[0], - target: $scope.graphState.arrayOfNodesForChart[chartNodeIdToIndexMapping[$scope.linkConfig.child.id]], + source: {id: 1}, + target: {id: $scope.linkConfig.target.id}, edgeType: "always" }); } @@ -782,53 +775,69 @@ export default ['$scope', 'TemplatesService', $scope.confirmDeleteNode = function () { if ($scope.nodeToBeDeleted) { - const nodeIndex = $scope.nodeToBeDeleted.index; + const nodeId = $scope.nodeToBeDeleted.id; - if ($scope.linkBeingWorkedOn) { + if ($scope.linkConfig) { $scope.cancelLinkForm(); } // Remove the node from the array - $scope.graphState.arrayOfNodesForChart.splice(nodeIndex, 1); + for( let i = $scope.graphState.arrayOfNodesForChart.length; i--; ){ + if ($scope.graphState.arrayOfNodesForChart[i].id === nodeId) { + $scope.graphState.arrayOfNodesForChart.splice(i, 1); + i = 0; + } + } // Update the links let parents = []; let children = []; + let linkParentMapping = {}; // Remove any links that reference this node for( let i = $scope.graphState.arrayOfLinksForChart.length; i--; ){ const link = $scope.graphState.arrayOfLinksForChart[i]; - if (link.source.index === nodeIndex || link.target.index === nodeIndex) { - if (link.source.index === nodeIndex) { - const targetIndex = link.target.index < nodeIndex ? link.target.index : link.target.index - 1; - children.push({index: targetIndex, edgeType: link.edgeType}); - } else if (link.target.index === nodeIndex) { - const sourceIndex = link.source.index < nodeIndex ? link.source.index : link.source.index - 1; - parents.push(sourceIndex); + if (!linkParentMapping[link.target.id]) { + linkParentMapping[link.target.id] = []; + } + + linkParentMapping[link.target.id].push(link.source.id); + + if (link.source.id === nodeId || link.target.id === nodeId) { + if (link.source.id === nodeId) { + children.push({id: link.target.id, edgeType: link.edgeType}); + } else if (link.target.id === nodeId) { + parents.push(link.source.id); } $scope.graphState.arrayOfLinksForChart.splice(i, 1); - } else { - // if (link.source.index > nodeIndex) { - // link.source.index = link.source.index - 1; - // } - // if (link.target.index > nodeIndex) { - // link.target.index = link.target.index - 1; - // } } } // Add the new links - parents.forEach((parentIndex) => { + parents.forEach((parentId) => { children.forEach((child) => { - if (parentIndex === 0) { - child.edgeType = "always"; + if (parentId === 1) { + // We only want to create a link from the start node to this node if it + // doesn't have any other parents + if(linkParentMapping[child.id].length === 1) { + $scope.graphState.arrayOfLinksForChart.push({ + source: {id: parentId}, + target: {id: child.id}, + edgeType: "always" + }); + } + } else { + // We don't want to add a link that already exists + if (!linkParentMapping[child.id].includes(parentId)) { + $scope.graphState.arrayOfLinksForChart.push({ + source: {id: parentId}, + target: {id: child.id}, + edgeType: child.edgeType + }); + } } - $scope.graphState.arrayOfLinksForChart.push({ - source: $scope.graphState.arrayOfNodesForChart[parentIndex], - target: $scope.graphState.arrayOfNodesForChart[child.index], - edgeType: child.edgeType - }); + }); }); @@ -838,12 +847,6 @@ export default ['$scope', 'TemplatesService', delete nodeRef[$scope.nodeToBeDeleted.id]; - for (const key in chartNodeIdToIndexMapping) { - if (chartNodeIdToIndexMapping[key] > $scope.nodeToBeDeleted.index) { - chartNodeIdToIndexMapping[key]--; - } - } - $scope.deleteOverlayVisible = false; $scope.nodeToBeDeleted = null; @@ -902,7 +905,7 @@ export default ['$scope', 'TemplatesService', let arrayOfLinksForChart = []; let arrayOfNodesForChart = []; - ({arrayOfNodesForChart, arrayOfLinksForChart, chartNodeIdToIndexMapping, nodeIdToChartNodeIdMapping, nodeRef, workflowMakerNodeIdCounter} = WorkflowChartService.generateArraysOfNodesAndLinks(allNodes)); + ({arrayOfNodesForChart, arrayOfLinksForChart, nodeIdToChartNodeIdMapping, nodeRef, workflowMakerNodeIdCounter} = WorkflowChartService.generateArraysOfNodesAndLinks(allNodes)); $scope.graphState = { arrayOfNodesForChart, arrayOfLinksForChart };