From eb7c52a68dc25e59d6c9db11b0eb51263219db71 Mon Sep 17 00:00:00 2001 From: sunnavy Date: Mon, 23 Dec 2024 17:21:20 -0500 Subject: [PATCH 1/2] Refactor LifecycleEditor to make it work with htmx Htmx imports external javascript sources introduced in htmx responses, but the run order is not like normal web pages, and thus previously we got an error: Uncaught TypeError: RT.NewLifecycleEditor is not a constructor Besides, because of js class declaration, if lifecycleui-model.js is imported multiple times, we would get: Identifier 'LifecycleModel' has already been declared This commit makes the following changes to make it work with htmx: * Merge LifecyleModel into the anonymous js class saved in RT.NewLifecycleEditor Via js class expression instead of a standard class declaration, we don't need to worry about the code being imported multiple times. * Move editor initialization code from inline to util.js for consistency * Initialize only when editor is available --- lib/RT/Interface/Web/MenuBuilder.pm | 3 - share/html/Admin/Lifecycles/Modify.html | 1 - share/html/Elements/Lifecycle/Graph | 7 +- share/static/js/lifecycleui-editor.js | 1308 +++++++++++++++-------- share/static/js/lifecycleui-model.js | 358 ------- share/static/js/util.js | 10 + 6 files changed, 844 insertions(+), 843 deletions(-) delete mode 100644 share/static/js/lifecycleui-model.js diff --git a/lib/RT/Interface/Web/MenuBuilder.pm b/lib/RT/Interface/Web/MenuBuilder.pm index b835a0ca1ef..7087b1d4b33 100644 --- a/lib/RT/Interface/Web/MenuBuilder.pm +++ b/lib/RT/Interface/Web/MenuBuilder.pm @@ -1819,9 +1819,6 @@ sub _BuildAdminPageMenu { $page->child( basics => title => loc('Modify'), path => "/Admin/Lifecycles/Modify.html?Type=" . $Type_uri . ";Name=" . $Name_uri, - attributes => { - 'hx-boost' => 'false', - }, ); } $page->child( actions => title => loc('Actions'), path => "/Admin/Lifecycles/Actions.html?Type=" . $Type_uri . ";Name=" . $Name_uri ); diff --git a/share/html/Admin/Lifecycles/Modify.html b/share/html/Admin/Lifecycles/Modify.html index 12ae33e1f04..e8c1cebe004 100644 --- a/share/html/Admin/Lifecycles/Modify.html +++ b/share/html/Admin/Lifecycles/Modify.html @@ -55,7 +55,6 @@ -
diff --git a/share/html/Elements/Lifecycle/Graph b/share/html/Elements/Lifecycle/Graph index 1a552c39884..6b4f231f5b5 100644 --- a/share/html/Elements/Lifecycle/Graph +++ b/share/html/Elements/Lifecycle/Graph @@ -45,7 +45,7 @@ %# those contributions and any derivatives thereof. %# %# END BPS TAGGED BLOCK }}} -
+
@@ -74,11 +74,6 @@
-
diff --git a/share/static/js/lifecycleui-editor.js b/share/static/js/lifecycleui-editor.js index e887bcb8f68..bf9e9209c1f 100644 --- a/share/static/js/lifecycleui-editor.js +++ b/share/static/js/lifecycleui-editor.js @@ -1,574 +1,932 @@ -htmx.onLoad(function () { - RT.NewLifecycleEditor = class LifecycleEditorNew extends LifecycleModel { - constructor(container, config, maps, layout) { - super("LifecycleModel"); - - var self = this; - self.width = 900; - self.height = 350; - self.node_radius = 35; - self.maps = maps; - self.layout = layout; - self.enableSimulation = 1; - - if ( !self.layout ) { - jQuery('#enableSimulation').prop( "checked", true ); - } - else { - self.enableSimulation = 0; - jQuery('#enableSimulation').prop( "checked", false ); - } - - jQuery("#SaveNode").click(function(e) { - e.preventDefault(); - self.UpdateNode(); - }); - - jQuery("#CancelNode").click(function(e) { - e.preventDefault(); - jQuery("#lifeycycle-ui-edit-node").toggle(); - jQuery("#lifeycycle-ui-edit-node div.alert").addClass('hidden'); - }); +RT.NewLifecycleEditor ||= class { + constructor(container, config, maps, layout) { + var self = this; + + self.links_seq = 0; + self.nodes_seq = 0; + // Here we store the '' => transitions + self.create_nodes = []; + + self.width = 900; + self.height = 350; + self.node_radius = 35; + self.maps = maps; + self.layout = layout; + self.enableSimulation = 1; + + if ( !self.layout ) { + jQuery('#enableSimulation').prop( "checked", true ); + } + else { + self.enableSimulation = 0; + jQuery('#enableSimulation').prop( "checked", false ); + } - self.svg = d3.select(container).select('svg') - .attr("preserveAspectRatio", "xMinYMin meet") - .attr("viewBox", "0 0 "+self.width+" "+self.height) - .classed("svg-content-responsive", true) - .attr("border", 1); - - self.svg.append("rect") - .classed("rect", true) - .attr("x", 0) - .attr("y", 0) - .attr("height", self.height) - .attr("width", self.width) - .style("stroke", 'black') - .style("fill", "none") - .style("stroke-width", 1); - - self.config = config; - self.links = []; - self.nodes = []; - self.animationFactor = 1; - // mouse event vars - self.selected_node = null; - self.editing_node = null; - self.selected_link = null; - self.mousedown_link = null; - self.mousedown_node = null; - self.mouseup_node = null; - - self.NodesFromConfig(config); - self.nodes.forEach(function(source) { - self.LinksForNodeFromConfig(source.name).forEach(function(targetName) { - // Get our target node - var target = self.nodes.filter(function(source) { return source.name === targetName; })[0]; - if (!target) { return }; - - if ( source.id < target.id ) { - self.links.push({id: ++self.links_seq, source: source, target: target, start: false, end: true}); - return; - } - var link = self.links.filter(function(l) { return (l.source === target && l.target === source); })[0]; - if(link) link.start = true; - else self.links.push({id: ++self.links_seq, source: source, target: target, start: false, end: true}); - }); - if ( !self.enableSimulation ) { - if (self.layout[source.name][0]) source.x = parseInt(self.layout[source.name][0]); - if (self.layout[source.name][1]) source.y = parseInt(self.layout[source.name][1]); + jQuery("#SaveNode").click(function(e) { + e.preventDefault(); + self.UpdateNode(); + }); + + jQuery("#CancelNode").click(function(e) { + e.preventDefault(); + jQuery("#lifeycycle-ui-edit-node").toggle(); + jQuery("#lifeycycle-ui-edit-node div.alert").addClass('hidden'); + }); + + self.svg = d3.select(container).select('svg') + .attr("preserveAspectRatio", "xMinYMin meet") + .attr("viewBox", "0 0 "+self.width+" "+self.height) + .classed("svg-content-responsive", true) + .attr("border", 1); + + self.svg.append("rect") + .classed("rect", true) + .attr("x", 0) + .attr("y", 0) + .attr("height", self.height) + .attr("width", self.width) + .style("stroke", 'black') + .style("fill", "none") + .style("stroke-width", 1); + + self.config = config; + self.links = []; + self.nodes = []; + self.animationFactor = 1; + // mouse event vars + self.selected_node = null; + self.editing_node = null; + self.selected_link = null; + self.mousedown_link = null; + self.mousedown_node = null; + self.mouseup_node = null; + + self.NodesFromConfig(config); + self.nodes.forEach(function(source) { + self.LinksForNodeFromConfig(source.name).forEach(function(targetName) { + // Get our target node + var target = self.nodes.filter(function(source) { return source.name === targetName; })[0]; + if (!target) { return }; + + if ( source.id < target.id ) { + self.links.push({id: ++self.links_seq, source: source, target: target, start: false, end: true}); + return; } + var link = self.links.filter(function(l) { return (l.source === target && l.target === source); })[0]; + if(link) link.start = true; + else self.links.push({id: ++self.links_seq, source: source, target: target, start: false, end: true}); }); - - self.simulation = d3.forceSimulation(); - const link_size = self.nodes.length > 10 ? 300 : self.nodes.length * 35; if ( !self.enableSimulation ) { - self.simulation - .force("link", null) - .force("charge", null) - .force("center", null) - .force('collision', null); - } - else { - self.simulation - .force("link", d3.forceLink().distance(link_size < 100 ? 200 : link_size).strength(0.2)) - .force("charge", d3.forceManyBody().strength(-200)) - .force("center", d3.forceCenter(self.width / 2, self.height / 2)) - .force('collision', d3.forceCollide().radius(function(d) { - return d.radius - })); + if (self.layout[source.name][0]) source.x = parseInt(self.layout[source.name][0]); + if (self.layout[source.name][1]) source.y = parseInt(self.layout[source.name][1]); } + }); - self.SetUp(); - self.RenderNode(); - self.RenderLink(); - + self.simulation = d3.forceSimulation(); + const link_size = self.nodes.length > 10 ? 300 : self.nodes.length * 35; + if ( !self.enableSimulation ) { self.simulation - .nodes(self.nodes) - .on("tick", function (t) { - self.node.attr("transform", function (d) { - - var x = d.x, y = d.y; - if ( d.x + self.node_radius / 2 > self.width ) x = self.width - self.node_radius; - if ( d.x - self.node_radius / 2 <= 0 ) x = self.node_radius; - if ( d.y + self.node_radius / 2 > self.height ) y = self.height - self.node_radius; - if ( d.y - self.node_radius / 2 <= 0 ) y = self.node_radius; - - if ( !self.enableSimulation ) { - d.fx = x; - d.fy = y; - } - else { - d.fx = null; - d.fy = null; - } - - return "translate(" + x + "," + y + ")"; - }); - - self.link.attr('d', (function(d) { - var sx = d.source.x, - sy = d.source.y, - tx = d.target.x, - ty = d.target.y; - - if ( sx + self.node_radius / 2 > self.width ) sx = self.width - self.node_radius; - if ( sx - self.node_radius / 2 <= 0 ) sx = self.node_radius; - if ( sy + self.node_radius / 2 > self.height ) sy = self.height - self.node_radius; - if ( sy - self.node_radius / 2 <= 0 ) sy = self.node_radius; - if ( tx + self.node_radius / 2 > self.width ) tx = self.width - self.node_radius; - if ( tx - self.node_radius / 2 <= 0 ) tx = self.node_radius; - if ( ty + self.node_radius / 2 > self.height ) ty = self.height - self.node_radius; - if ( ty - self.node_radius / 2 <= 0 ) ty = self.node_radius; - - var deltaX = tx - sx, - deltaY = ty - sy, - dist = Math.sqrt(deltaX * deltaX + deltaY * deltaY), - normX = deltaX / dist, - normY = deltaY / dist, - sourcePadding = 45, - targetPadding = 45, - sourceX = sx + (sourcePadding * normX), - sourceY = sy + (sourcePadding * normY), - targetX = tx - (targetPadding * normX), - targetY = ty - (targetPadding * normY); - return 'M' + sourceX + ',' + sourceY + 'L' + targetX + ',' + targetY; - }) - ); - }); + .force("link", null) + .force("charge", null) + .force("center", null) + .force('collision', null); + } + else { + self.simulation + .force("link", d3.forceLink().distance(link_size < 100 ? 200 : link_size).strength(0.2)) + .force("charge", d3.forceManyBody().strength(-200)) + .force("center", d3.forceCenter(self.width / 2, self.height / 2)) + .force('collision', d3.forceCollide().radius(function(d) { + return d.radius + })); + } - self.ExportAsConfiguration(); + self.SetUp(); + self.RenderNode(); + self.RenderLink(); - self.Refresh(); - } + self.simulation + .nodes(self.nodes) + .on("tick", function (t) { + self.node.attr("transform", function (d) { + + var x = d.x, y = d.y; + if ( d.x + self.node_radius / 2 > self.width ) x = self.width - self.node_radius; + if ( d.x - self.node_radius / 2 <= 0 ) x = self.node_radius; + if ( d.y + self.node_radius / 2 > self.height ) y = self.height - self.node_radius; + if ( d.y - self.node_radius / 2 <= 0 ) y = self.node_radius; - SetUp() { - var self = this; - - // define arrow markers for graph links - self.svg.append('svg:defs').append('svg:marker') - .attr('id', 'end-arrow') - .attr('viewBox', '0 -5 10 10') - .attr('refX', 6) - .attr('markerWidth', 5) - .attr('markerHeight', 5) - .attr('orient', 'auto') - .append('svg:path') - .attr('d', 'M0,-5L10,0L0,5') - .attr('fill', '#000'); - - self.svg.append('svg:defs').append('svg:marker') - .attr('id', 'start-arrow') - .attr('viewBox', '0 -5 10 10') - .attr('refX', 6) - .attr('markerWidth', 5) - .attr('markerHeight', 5) - .attr('orient', 'auto') - .append('svg:path') - .attr('d', 'M10,-5L0,0L10,5') - .attr('fill', '#000'); - - // line displayed when dragging new nodes - self.drag_line = self.svg.append('svg:path') - .attr('class', 'dragline hidden') - .attr('d', 'M0,0L0,0') - .attr('fill', '#000') - .attr('markerWidth', 8) - .attr('markerHeight', 8) - .attr("stroke-width", 1) - .attr("style", "stroke: black; stroke-opacity: 0.6;"); - - self.svg - .on('click', function () { - d3.event.preventDefault(); - d3.event.stopPropagation(); - - if ( self.selected_node || self.editing_node ) { - self.Deselect(); + if ( !self.enableSimulation ) { + d.fx = x; + d.fy = y; } else { - self.simulation.stop(); - self.Deselect(); - - self.AddNode(d3.mouse(this)); + d.fx = null; + d.fy = null; + } - self.ExportAsConfiguration(); + return "translate(" + x + "," + y + ")"; + }); - self.Refresh(); - } + self.link.attr('d', (function(d) { + var sx = d.source.x, + sy = d.source.y, + tx = d.target.x, + ty = d.target.y; + + if ( sx + self.node_radius / 2 > self.width ) sx = self.width - self.node_radius; + if ( sx - self.node_radius / 2 <= 0 ) sx = self.node_radius; + if ( sy + self.node_radius / 2 > self.height ) sy = self.height - self.node_radius; + if ( sy - self.node_radius / 2 <= 0 ) sy = self.node_radius; + if ( tx + self.node_radius / 2 > self.width ) tx = self.width - self.node_radius; + if ( tx - self.node_radius / 2 <= 0 ) tx = self.node_radius; + if ( ty + self.node_radius / 2 > self.height ) ty = self.height - self.node_radius; + if ( ty - self.node_radius / 2 <= 0 ) ty = self.node_radius; + + var deltaX = tx - sx, + deltaY = ty - sy, + dist = Math.sqrt(deltaX * deltaX + deltaY * deltaY), + normX = deltaX / dist, + normY = deltaY / dist, + sourcePadding = 45, + targetPadding = 45, + sourceX = sx + (sourcePadding * normX), + sourceY = sy + (sourcePadding * normY), + targetX = tx - (targetPadding * normX), + targetY = ty - (targetPadding * normY); + return 'M' + sourceX + ',' + sourceY + 'L' + targetX + ',' + targetY; }) - .on('contextmenu', function() { d3.event.preventDefault(); }) - .on('mousemove', function() { self.Mousemove(this); }) - .on('mouseup', function() { self.Mouseup(this); }) - .on('mousedown', function() { self.Mousedown(this); }); + ); + }); - d3.select("body").on("keydown", function (d) { - if ( !self.editing_node && self.selected_node && ( d3.event.keyCode == 68 || d3.event.keyCode == 46 ) ) { - d3.event.preventDefault(); - d3.event.stopPropagation(); + self.ExportAsConfiguration(); - self.simulation.stop(); - self.svg.selectAll('.node-selected').each(function(d) { - self.DeleteNode(d); + self.Refresh(); + } - self.ExportAsConfiguration(); + // Generate nodes from config + NodesFromConfig(config) { + var self = this; + self.nodes = []; - self.Deselect(); - self.Refresh(); - }); - } - }); + jQuery.each(['initial', 'active', 'inactive'], function (i, type) { + if ( config[type] ) { + config[type].forEach(function(element) { + self.nodes = self.nodes.concat({id: ++self.nodes_seq, name: element, type: type}); + }); + } + }); + } - jQuery('#enableSimulation').click(function(e) { - self.ToggleSimulation(); - return true; - }); + // Find all links associated with node object + LinksForNodeFromConfig (node, config) { + var config = config || this.config; - jQuery('form[name=ModifyLifecycle]').submit(function(e) { - self.ExportAsConfiguration(); - }); + for (let [fromNode, toList] of Object.entries(config.transitions)) { + if ( fromNode == '' ) { + this.create_nodes = toList; + } + else if ( fromNode.toLowerCase() == node.toLowerCase() ) { + return toList; + } + } + return []; + } + + // Create a new node for our JS model + AddNode(point) { + var self = this; + + var i = 0, + name; + while (1) { + name = 'status #' + ++i; + var index = self.nodes.findIndex(function(x) { return x.name.toLowerCase() == name.toLowerCase() }); + if ( index < 0 ) { + break; + } + } + self.nodes.push({id: ++self.nodes_seq, name: name, type: 'active', x: point[0], y: point[1]}); + } + + AddLink(source, target) { + var self = this; + if (!source || !target) return; + + var link = self.links.filter(function(l) { return (l.source.id === target.id && l.target.id === source.id); })[0]; + + if ( link ) { + link.start = true; + } + else { + link = self.links.filter(function(l) { return (l.source.id === source.id && l.target.id === target.id); })[0]; + if (!link ) { + self.links.push({id: ++self.links_seq, source: source, target: target, start: false, end: true}); + } } + } - RenderNode() { - var self = this; + ToggleLink(d) { + var self = this; + var index = self.links.findIndex(function(x) { return x.id == d.id }); + + var link = self.links[index]; + // delete link if we have both transitions already + if ( link.start && link.end ) { + self.links.splice(index, 1); + var from = d.source.name.toLowerCase(); + var to = d.target.name.toLowerCase(); + var pattern = from + ' *-> *' + to + '|' + to + ' *-> *' + from; + self.DeleteRights(null, pattern); + self.DeleteActions(null, pattern); + } + else if( link.start ) { + link.end = true; + } + else { + link.start = true; + } + } - self.node = self.svg.selectAll(".node") - .data(self.nodes.filter(function(d) { return d.id >= 0 })); + NodeById(id) { + var self = this; - self.node.exit() - .remove(); + var nodeMap = d3.map(self.nodes, function(d) { return d.id; }); + return nodeMap.get( id ); + } - // Add new nodes and draw them - var nodeEnter = self.node.enter().append("g") - .attr("class", "node"); + NodeByStatus(status) { + var self = this; - nodeEnter.append("circle"); - nodeEnter.append("text"); - nodeEnter.append("title"); + var nodeMap = d3.map(self.nodes, function(d) { return d.name; }); + return nodeMap.get( status ); + } - self.node = nodeEnter.merge(self.node) - .attr("id", function(d) { return d.id }); + DeleteNode(d) { + var self = this; - self.node.call(d3.drag() - .on("start", function(d) { - if (!d3.event.active) self.simulation.alphaTarget(0.3).restart(); - d.fx = d.x, d.fy = d.y; - }) - .on("drag", function(d) { - d.fx = d3.event.x, d.fy = d3.event.y; - }) - .on("end", function(d) { - if (!d3.event.active) self.simulation.alphaTarget(0); - if ( !self.enableSimulation ) { - d.fx = null, d.fy = null; - } - })); + var index = self.nodes.findIndex(function(x) { return x.id == d.id }); + self.DeleteLinksForNode(self.nodes[index]); - // Add our circle to our new node - self.node.select("circle") - .attr("r", self.node_radius) - .attr("stroke", "black") - .attr("fill", function(d) { - switch(d.type) { - case 'active': - return '#547CCC'; - case 'inactive': - return '#4bb2cc'; - case 'initial': - return '#599ACC'; - } - }) - .on("click", function() { - d3.event.stopPropagation(); - d3.event.preventDefault(); + self.DeleteRights(d); + self.DeleteDefaults(d); + self.DeleteActions(d); - self.SelectNode(this); - }) - .on('mousedown', function(d) { - if(!d3.event.ctrlKey || self.mousedown_node || self.mousedown_link) return; - d3.event.preventDefault(); - d3.event.stopPropagation(); + self.nodes.splice(index, 1); + } - // select node - self.mousedown_node = d; - if ( !self.mousedown_node ) { self.mousedown_node = null; return; } + LinksForNode (node) { + var self = this; - // reposition drag line - self.drag_line - .style('marker-end', 'url(#end-arrow)') - .classed('hidden', false) - .attr('d', 'M' + self.mousedown_node.x + ',' + self.mousedown_node.y + 'L' + self.mousedown_node.x + ',' + self.mousedown_node.y); + return self.links.filter(function(link) { + if ( link.source.id === node.id ) { + return true; + } + else if ( link.target.id === node.id && link.start ) { + return true; + } + else { + return false; + } + }); + } - self.Refresh(); - }) - .on('mouseup', function(d) { - self.Mouseup(d); - }); + DeleteDefaults(d) { + var self = this; - self.node.select("text") - .text(function(d) { return d.name; }) - .each(function () { self.TruncateLabel(this, self); }) - .attr("x", function(d) { - var node = d3.select(this), textLength = node.node().getComputedTextLength(); - if ( textLength > self.node_radius*2 ) textLength = self.node_radius*2; - return -textLength/2; - }) - .attr("y", 0) - .style("font-size", "10px") - .on("click", function(d) { - d3.event.stopPropagation(); - d3.event.preventDefault(); - self.UpdateNode(d); - }); + jQuery.each(self.config.defaults, function (key, value) { + if (value && value.toLowerCase() === d.name.toLowerCase()) { + delete self.config.defaults[key]; + } + }); + } - self.node.select("title") - .text(function(d) { return d.type; }); + DeleteRights(d, pattern) { + var self = this; + if ( !pattern ) { + pattern = d.name.toLowerCase() + " *->|-> *" + d.name.toLowerCase(); } - UpdateNode(element) { - var self = this; - const nodeInput = jQuery("#lifeycycle-ui-edit-node"); + var re = new RegExp(pattern); + jQuery.each(self.config.rights, function(key, value) { + if ( re.test(key.toLowerCase()) ) { + delete self.config.rights[key]; + } + }); + } + + DeleteActions(d, pattern) { + var self = this; + if ( !pattern ) { + pattern = d.name.toLowerCase() + " *->|-> *" + d.name.toLowerCase(); + } - if ( event.pageX ) { - var posX = event.pageX; - var posY = event.pageY; + var re = new RegExp(pattern); + var actions = []; + var tempArr = self.config.actions || []; - if ( posX + nodeInput.width() > self.width ) posX = self.width - nodeInput.width(); - if ( posY + nodeInput.height() > self.height ) posY = self.height - nodeInput.height(); + var i = tempArr.length / 2; + while (i--) { + var action, info; + [action, info] = tempArr.splice(0, 2); + if (!action) continue; - nodeInput.css( {position:"absolute", top:posY - self.node_radius, left: posX - self.node_radius}); + if ( ! re.test(action) ) { + actions.push(action); + actions.push(info); } - var list = document.getElementById('lifeycycle-ui-edit-node').querySelectorAll('input, select'); + } + self.config.actions = actions; + } - if ( element ) { - for (let item of list) { - jQuery(item).val(element[item.name]); - } - self.editing_node = element; + DeleteLinksForNode(node) { + var self = this; + + if ( !node ) { + return; + } + + self.links = jQuery.grep(self.links, function (transition) { + if (transition.source.id == node.id || transition.target.id == node.id) { + return false; + } + return true; + }); + } + + UpdateNodeModel(node, args) { + var self = this; + + var nodeIndex = self.nodes.findIndex(function(x) { return x.id == node.id }); + + var oldValue = self.nodes[nodeIndex]; + + self.nodes[nodeIndex] = {...self.nodes[nodeIndex], ...args}; + var nodeUpdated = self.nodes[nodeIndex]; + + // Update any links with node being changed as source + var links = self.links.filter(function(l) { return ( + ( l.source.id === node.id ) ) + }); + links.forEach(function(link) { + var index = self.links.findIndex(function(x) { return x.id == link.id }); + self.links[index] = {...link, source: nodeUpdated} + }); + + // Update any links with node being changed as target + var links = self.links.filter(function(l) { return ( + ( l.target.id === node.id ) ) + }); + links.forEach(function(link) { + var index = self.links.findIndex(function(x) { return x.id == link.id }); + self.links[index] = {...link, target: nodeUpdated} + }); + + if ( oldValue.name === nodeUpdated.name ) return; + + // Only need to check for target + var tempArr = []; + self.create_nodes.forEach(function(target) { + if ( target != oldValue.name ) { + tempArr.push(target); } else { - var name = document.getElementsByName('name')[0].value; + tempArr.push(nodeUpdated.name); + } + }); + self.create_nodes = tempArr; - if ( self.editing_node.name != name && self.nodes.reduce(function(n, x) { return n + (x.name === name) }, 0) >= 1 || name === '' ) { - var form = jQuery('#lifeycycle-ui-edit-node'); - form.find('div.invalid-name').removeClass('hidden'); - return; - } + for (let type in self.config.defaults) { + if ( self.config.defaults[type] === oldValue.name ) { + self.config.defaults[type] = nodeUpdated.name; + } + } + + let re_from = new RegExp(oldValue.name + ' *->'); + let re_to = new RegExp('-> *' + oldValue.name); + + for (let action in self.config.rights) { + let updated = action.replace(re_from, nodeUpdated.name + ' ->').replace(re_to, '-> ' + nodeUpdated.name); + if ( action != updated ) { + self.config.rights[updated] = self.config.rights[action]; + delete self.config.rights[action]; + } + } - var values = {}; - for (let item of list) { - if ( item.name === 'id' ) { - values.index = self.nodes.findIndex(function(x) { return x.id == item.value }); + let actions = []; + if ( self.config.actions ) { + for ( let i = 0; i < self.config.actions.length; i += 2 ) { + let action = self.config.actions[i]; + let info = self.config.actions[i+1]; + let updated = action.replace(re_from, nodeUpdated.name + ' ->').replace(re_to, '-> ' + nodeUpdated.name); + actions.push(updated); + actions.push(info); + } + } + self.config.actions = actions; + + let config_name = jQuery('form[name=ModifyLifecycle] input[name=Name]').val(); + for (let item in self.maps) { + if ( item.match(config_name + ' *->')) { + let maps = self.maps[item]; + for ( let from in maps ) { + if ( from === oldValue.name ) { + maps[nodeUpdated.name] = maps[from]; + delete maps[from]; + } + } + } + else if ( item.match('-> *' + config_name) ) { + let maps = self.maps[item]; + for ( let from in maps ) { + if ( maps[from] === oldValue.name ) { + maps[from] = nodeUpdated.name; } - values[item.name] = item.value; } - self.UpdateNodeModel(self.nodes[values.index], values); - self.ExportAsConfiguration(); - self.Refresh(); } - nodeInput.toggle(); } + } - RenderLink() { - var self = this; - - self.link = self.svg.selectAll(".link") - .data(self.links); - - self.link.exit() - .each(function () { - var length = this.getTotalLength(); - var path = d3.select(this); - path.attr("stroke-dasharray", length + " " + length) - .attr("stroke-dashoffset", 0) - .style("marker-end", "none") - .style("marker-start", "none") - .transition().duration(200 * self.animationFactor).ease(d3.easeLinear) - .attr("stroke-dashoffset", length) - .remove(); - }); + ExportAsConfiguration () { + var self = this; + + var config = { + type: self.type, + initial: [], + active: [], + inactive: [], + transitions: {}, + }; + + // Grab our status nodes + ['initial', 'active', 'inactive'].forEach(function(type) { + config[type] = self.nodes.filter(function(n) { return n.type == type }).map(function(n) { return n.name }); + }); + + // Clean removed states from create_nodes + self.create_nodes = self.create_nodes.filter(target => self.nodes.find(n => n.name == target)); + + // Grab our links + config.transitions[""] = self.create_nodes; + + var seen = {}; + self.nodes.forEach(function(source) { + var links = self.LinksForNode(source); + var targets = links.map(link => { + if ( link.source.id === source.id ) { + return link.target.name; + } + else { + return link.source.name; + } + }); + config.transitions[source.name] = targets; + seen[source.name] = 1; + }); + + for (let transition in config.transitions) { + if ( transition && ( !seen[transition] || !config.transitions[transition].length ) ) { + delete config.transitions[transition]; + } + } - // Add new links and draw them - var linkEnter = self.link.enter().append("g") - .append("path") - .attr("class", 'link') - .style("marker-start", function(d) { return d.start ? 'url(#start-arrow)' : '' }) - .style("marker-end", function(d) { return d.end ? 'url(#end-arrow)' : '' }) - .attr("transform", "translate(0,0)") - .on("click", function(d) { - d3.event.stopPropagation(); + self.config = {...self.config, ...config}; + + // Set defaults on_create if it's absent + self.config.defaults ||= {}; + self.config.defaults.on_create ||= self.config.initial[0] || self.config.active[0] || null; + + var field = jQuery('form[name=ModifyLifecycle] input[name=Config]'); + field.val(JSON.stringify(self.config)); + + var pos; + if ( !jQuery('#enableSimulation').is(":checked") ) { + pos = {}; + self.nodes.forEach( function(d) { + pos[d.name] = [Math.round(d.x), Math.round(d.y)]; + }); + } + var layout = jQuery('form[name=ModifyLifecycle] input[name=Layout]'); + layout.val(pos ? JSON.stringify(pos) : ''); + + var maps = jQuery('form[name=ModifyLifecycle] input[name=Maps]'); + maps.val(JSON.stringify(self.maps)); + }; + + SetUp() { + var self = this; + + // define arrow markers for graph links + self.svg.append('svg:defs').append('svg:marker') + .attr('id', 'end-arrow') + .attr('viewBox', '0 -5 10 10') + .attr('refX', 6) + .attr('markerWidth', 5) + .attr('markerHeight', 5) + .attr('orient', 'auto') + .append('svg:path') + .attr('d', 'M0,-5L10,0L0,5') + .attr('fill', '#000'); + + self.svg.append('svg:defs').append('svg:marker') + .attr('id', 'start-arrow') + .attr('viewBox', '0 -5 10 10') + .attr('refX', 6) + .attr('markerWidth', 5) + .attr('markerHeight', 5) + .attr('orient', 'auto') + .append('svg:path') + .attr('d', 'M10,-5L0,0L10,5') + .attr('fill', '#000'); + + // line displayed when dragging new nodes + self.drag_line = self.svg.append('svg:path') + .attr('class', 'dragline hidden') + .attr('d', 'M0,0L0,0') + .attr('fill', '#000') + .attr('markerWidth', 8) + .attr('markerHeight', 8) + .attr("stroke-width", 1) + .attr("style", "stroke: black; stroke-opacity: 0.6;"); + + self.svg + .on('click', function () { + d3.event.preventDefault(); + d3.event.stopPropagation(); + + if ( self.selected_node || self.editing_node ) { + self.Deselect(); + } + else { self.simulation.stop(); - self.ToggleLink(d); + self.Deselect(); + + self.AddNode(d3.mouse(this)); self.ExportAsConfiguration(); self.Refresh(); - }); - self.link = linkEnter.merge(self.link); - self.link - .style("marker-start", function(d) { return d.start ? 'url(#start-arrow)' : '' }) - .style("marker-end", function(d) { return d.end ? 'url(#end-arrow)' : '' }); - } + } + }) + .on('contextmenu', function() { d3.event.preventDefault(); }) + .on('mousemove', function() { self.Mousemove(this); }) + .on('mouseup', function() { self.Mouseup(this); }) + .on('mousedown', function() { self.Mousedown(this); }); - Refresh() { - var self = this; + d3.select("body").on("keydown", function (d) { + if ( !self.editing_node && self.selected_node && ( d3.event.keyCode == 68 || d3.event.keyCode == 46 ) ) { + d3.event.preventDefault(); + d3.event.stopPropagation(); - const link_size = self.nodes.length > 10 ? 300 : self.nodes.length * 35; - self.simulation - .force("link", d3.forceLink().distance(link_size < 100 ? 200 : link_size).strength(0.2)) + self.simulation.stop(); + self.svg.selectAll('.node-selected').each(function(d) { + self.DeleteNode(d); - self.simulation - .nodes(self.nodes) - .force("link") - .links(self.links) - .id(function(d) { return d.id }); + self.ExportAsConfiguration(); - self.RenderLink(); - self.RenderNode(); + self.Deselect(); + self.Refresh(); + }); + } + }); - jQuery('#lifeycycle-ui-edit-node div.alert').addClass('hidden'); + jQuery('#enableSimulation').click(function(e) { + self.ToggleSimulation(); + return true; + }); - // This is our "cooling" factor - self.simulation.alpha(0.05).restart(); - } + document.querySelector('form[name=ModifyLifecycle]').addEventListener('htmx:configRequest', function(evt) { + self.ExportAsConfiguration(); + // Manually update values to confirm submitted values are always the latest + evt.detail.parameters['Config'] = evt.detail.elt.querySelector('input[name=Config]').value; + evt.detail.parameters['Layout'] = evt.detail.elt.querySelector('input[name=Layout]').value; + }); + } - SelectNode(node) { - var self = this; + RenderNode() { + var self = this; + self.node = self.svg.selectAll(".node") + .data(self.nodes.filter(function(d) { return d.id >= 0 })); + + self.node.exit() + .remove(); + + // Add new nodes and draw them + var nodeEnter = self.node.enter().append("g") + .attr("class", "node"); + + nodeEnter.append("circle"); + nodeEnter.append("text"); + nodeEnter.append("title"); + + self.node = nodeEnter.merge(self.node) + .attr("id", function(d) { return d.id }); + + self.node.call(d3.drag() + .on("start", function(d) { + if (!d3.event.active) self.simulation.alphaTarget(0.3).restart(); + d.fx = d.x, d.fy = d.y; + }) + .on("drag", function(d) { + d.fx = d3.event.x, d.fy = d3.event.y; + }) + .on("end", function(d) { + if (!d3.event.active) self.simulation.alphaTarget(0); + if ( !self.enableSimulation ) { + d.fx = null, d.fy = null; + } + })); + + // Add our circle to our new node + self.node.select("circle") + .attr("r", self.node_radius) + .attr("stroke", "black") + .attr("fill", function(d) { + switch(d.type) { + case 'active': + return '#547CCC'; + case 'inactive': + return '#4bb2cc'; + case 'initial': + return '#599ACC'; + } + }) + .on("click", function() { + d3.event.stopPropagation(); + d3.event.preventDefault(); + + self.SelectNode(this); + }) + .on('mousedown', function(d) { + if(!d3.event.ctrlKey || self.mousedown_node || self.mousedown_link) return; + d3.event.preventDefault(); + d3.event.stopPropagation(); + + // select node + self.mousedown_node = d; + if ( !self.mousedown_node ) { self.mousedown_node = null; return; } + + // reposition drag line + self.drag_line + .style('marker-end', 'url(#end-arrow)') + .classed('hidden', false) + .attr('d', 'M' + self.mousedown_node.x + ',' + self.mousedown_node.y + 'L' + self.mousedown_node.x + ',' + self.mousedown_node.y); - self.Deselect(); - self.selected_node = node; + self.Refresh(); + }) + .on('mouseup', function(d) { + self.Mouseup(d); + }); - d3.select(node) - .classed('node-selected', true); - } + self.node.select("text") + .text(function(d) { return d.name; }) + .each(function () { self.TruncateLabel(this, self); }) + .attr("x", function(d) { + var node = d3.select(this), textLength = node.node().getComputedTextLength(); + if ( textLength > self.node_radius*2 ) textLength = self.node_radius*2; + return -textLength/2; + }) + .attr("y", 0) + .style("font-size", "10px") + .on("click", function(d) { + d3.event.stopPropagation(); + d3.event.preventDefault(); + self.UpdateNode(d); + }); - Deselect() { - var self = this; + self.node.select("title") + .text(function(d) { return d.type; }); + } - if ( jQuery("#lifeycycle-ui-edit-node").is(':visible') ) { - jQuery("#lifeycycle-ui-edit-node").toggle(); - jQuery("#lifeycycle-ui-edit-node div.alert").addClass('hidden'); - } + UpdateNode(element) { + var self = this; + const nodeInput = jQuery("#lifeycycle-ui-edit-node"); - self.editing_node = null; + if ( event.pageX ) { + var posX = event.pageX; + var posY = event.pageY; - if (!self.selected_node) return; - self.selected_node = null; + if ( posX + nodeInput.width() > self.width ) posX = self.width - nodeInput.width(); + if ( posY + nodeInput.height() > self.height ) posY = self.height - nodeInput.height(); - self.svg.selectAll("foreignObject").remove(); + nodeInput.css( {position:"absolute", top:posY - self.node_radius, left: posX - self.node_radius}); + } + var list = document.getElementById('lifeycycle-ui-edit-node').querySelectorAll('input, select'); - self.svg.selectAll('.node-selected') - .classed('node-selected', false); + if ( element ) { + for (let item of list) { + if ( item.tomselect ) { + item.tomselect.setValue(element[item.name]); + } + else { + jQuery(item).val(element[item.name]); + } + } + self.editing_node = element; } + else { + var name = document.getElementsByName('name')[0].value; - TruncateLabel(element, self) { - var node = d3.select(element), textLength = node.node().getComputedTextLength(), text = node.text(); - var diameter = self.node_radius * 2 - textLength/4; + if ( self.editing_node.name != name && self.nodes.reduce(function(n, x) { return n + (x.name === name) }, 0) >= 1 || name === '' ) { + var form = jQuery('#lifeycycle-ui-edit-node'); + form.find('div.invalid-name').removeClass('hidden'); + return; + } - while (textLength > diameter && text.length > 0) { - text = text.slice(0, -1); - node.text(text + '…'); - textLength = node.node().getComputedTextLength(); + var values = {}; + for (let item of list) { + if ( item.name === 'id' ) { + values.index = self.nodes.findIndex(function(x) { return x.id == item.value }); + } + values[item.name] = item.value; } + self.UpdateNodeModel(self.nodes[values.index], values); + self.ExportAsConfiguration(); + self.Refresh(); } + nodeInput.toggle(); + } - Mousemove(d) { - var self = this; - if (!self.mousedown_node) return; + RenderLink() { + var self = this; + + self.link = self.svg.selectAll(".link") + .data(self.links); + + self.link.exit() + .each(function () { + var length = this.getTotalLength(); + var path = d3.select(this); + path.attr("stroke-dasharray", length + " " + length) + .attr("stroke-dashoffset", 0) + .style("marker-end", "none") + .style("marker-start", "none") + .transition().duration(200 * self.animationFactor).ease(d3.easeLinear) + .attr("stroke-dashoffset", length) + .remove(); + }); - self.drag_line.attr('d', 'M' + self.mousedown_node.x + ',' + self.mousedown_node.y + 'L' + d3.mouse(d)[0] + ',' + d3.mouse(d)[1]); + // Add new links and draw them + var linkEnter = self.link.enter().append("g") + .append("path") + .attr("class", 'link') + .style("marker-start", function(d) { return d.start ? 'url(#start-arrow)' : '' }) + .style("marker-end", function(d) { return d.end ? 'url(#end-arrow)' : '' }) + .attr("transform", "translate(0,0)") + .on("click", function(d) { + d3.event.stopPropagation(); + self.simulation.stop(); + self.ToggleLink(d); - self.Refresh(); - } + self.ExportAsConfiguration(); + + self.Refresh(); + }); + self.link = linkEnter.merge(self.link); + self.link + .style("marker-start", function(d) { return d.start ? 'url(#start-arrow)' : '' }) + .style("marker-end", function(d) { return d.end ? 'url(#end-arrow)' : '' }); + } - Mouseup(d) { - var self = this; + Refresh() { + var self = this; - if( self.mousedown_node ) { - // needed by FF - self.drag_line - .classed('hidden', true) - .style('marker-end', ''); + const link_size = self.nodes.length > 10 ? 300 : self.nodes.length * 35; + self.simulation + .force("link", d3.forceLink().distance(link_size < 100 ? 200 : link_size).strength(0.2)) - if ( d.id && d.id != self.mousedown_node.id ) { - self.mouseup_node = d; - self.simulation.stop(); - // add link to model - self.AddLink(self.mousedown_node, self.mouseup_node); + self.simulation + .nodes(self.nodes) + .force("link") + .links(self.links) + .id(function(d) { return d.id }); - self.ExportAsConfiguration(); - self.Refresh(); - } - self.svg.classed('ctrl', false); - } - // because :active only works in WebKit? - self.svg.classed('active', false); - self.ResetMouseVars(); - } + self.RenderLink(); + self.RenderNode(); - Mousedown(d) { - d3.event.preventDefault(); - d3.event.stopPropagation(); + jQuery('#lifeycycle-ui-edit-node div.alert').addClass('hidden'); + + // This is our "cooling" factor + self.simulation.alpha(0.05).restart(); + } + + SelectNode(node) { + var self = this; + + self.Deselect(); + self.selected_node = node; + + d3.select(node) + .classed('node-selected', true); + } + + Deselect() { + var self = this; + + if ( jQuery("#lifeycycle-ui-edit-node").is(':visible') ) { + jQuery("#lifeycycle-ui-edit-node").toggle(); + jQuery("#lifeycycle-ui-edit-node div.alert").addClass('hidden'); } - ResetMouseVars(){ - var self = this; + self.editing_node = null; + + if (!self.selected_node) return; + self.selected_node = null; - self.mousedown_link = null; - self.mousedown_node = null; - self.mouseup_node = null; + self.svg.selectAll("foreignObject").remove(); + + self.svg.selectAll('.node-selected') + .classed('node-selected', false); + } + + TruncateLabel(element, self) { + var node = d3.select(element), textLength = node.node().getComputedTextLength(), text = node.text(); + var diameter = self.node_radius * 2 - textLength/4; + + while (textLength > diameter && text.length > 0) { + text = text.slice(0, -1); + node.text(text + '…'); + textLength = node.node().getComputedTextLength(); } + } - ToggleSimulation(){ - var self = this; - self.enableSimulation = jQuery('#enableSimulation').is(":checked"); + Mousemove(d) { + var self = this; + if (!self.mousedown_node) return; - const link_size = self.nodes.length > 10 ? 300 : self.nodes.length * 35; - if ( !self.enableSimulation ) { - self.simulation - .force("link", null) - .force("charge", null) - .force("center", null) - .force('collision', null); + self.drag_line.attr('d', 'M' + self.mousedown_node.x + ',' + self.mousedown_node.y + 'L' + d3.mouse(d)[0] + ',' + d3.mouse(d)[1]); + + self.Refresh(); + } + + Mouseup(d) { + var self = this; + + if( self.mousedown_node ) { + // needed by FF + self.drag_line + .classed('hidden', true) + .style('marker-end', ''); + + if ( d.id && d.id != self.mousedown_node.id ) { + self.mouseup_node = d; + self.simulation.stop(); + // add link to model + self.AddLink(self.mousedown_node, self.mouseup_node); self.ExportAsConfiguration(); + self.Refresh(); } - else { - self.nodes.forEach(function(d) { - d.fx = null, d.fy = null; - }); + self.svg.classed('ctrl', false); + } + // because :active only works in WebKit? + self.svg.classed('active', false); + self.ResetMouseVars(); + } + + Mousedown(d) { + d3.event.preventDefault(); + d3.event.stopPropagation(); + } + + ResetMouseVars(){ + var self = this; + + self.mousedown_link = null; + self.mousedown_node = null; + self.mouseup_node = null; + } + + ToggleSimulation(){ + var self = this; + self.enableSimulation = jQuery('#enableSimulation').is(":checked"); + + const link_size = self.nodes.length > 10 ? 300 : self.nodes.length * 35; + if ( !self.enableSimulation ) { + self.simulation + .force("link", null) + .force("charge", null) + .force("center", null) + .force('collision', null); - self.simulation - .force("link", d3.forceLink().distance(link_size < 100 ? 200 : link_size).strength(0.2)) - .force("charge", d3.forceManyBody().strength(-200)) - .force("center", d3.forceCenter(self.width / 2, self.height / 2)) - .force('collision', d3.forceCollide().radius(function(d) { - return d.radius - })) - self.simulation.force("link") - .links(self.links) - .id(function(d) { return d.id }); - } self.ExportAsConfiguration(); } + else { + self.nodes.forEach(function(d) { + d.fx = null, d.fy = null; + }); + + self.simulation + .force("link", d3.forceLink().distance(link_size < 100 ? 200 : link_size).strength(0.2)) + .force("charge", d3.forceManyBody().strength(-200)) + .force("center", d3.forceCenter(self.width / 2, self.height / 2)) + .force('collision', d3.forceCollide().radius(function(d) { + return d.radius + })) + self.simulation.force("link") + .links(self.links) + .id(function(d) { return d.id }); + } + self.ExportAsConfiguration(); } -}); +} diff --git a/share/static/js/lifecycleui-model.js b/share/static/js/lifecycleui-model.js deleted file mode 100644 index 93847fee880..00000000000 --- a/share/static/js/lifecycleui-model.js +++ /dev/null @@ -1,358 +0,0 @@ - -class LifecycleModel { - constructor() { - this.links_seq = 0; - this.nodes_seq = 0; - // Here we store the '' => transitions - this.create_nodes = []; - } - - // Generate nodes from config - NodesFromConfig(config) { - var self = this; - self.nodes = []; - - jQuery.each(['initial', 'active', 'inactive'], function (i, type) { - if ( config[type] ) { - config[type].forEach(function(element) { - self.nodes = self.nodes.concat({id: ++self.nodes_seq, name: element, type: type}); - }); - } - }); - } - - // Find all links associated with node object - LinksForNodeFromConfig (node, config) { - var config = config || this.config; - - for (let [fromNode, toList] of Object.entries(config.transitions)) { - if ( fromNode == '' ) { - this.create_nodes = toList; - } - else if ( fromNode.toLowerCase() == node.toLowerCase() ) { - return toList; - } - } - return []; - } - - // Create a new node for our JS model - AddNode(point) { - var self = this; - - var i = 0, - name; - while (1) { - name = 'status #' + ++i; - var index = self.nodes.findIndex(function(x) { return x.name.toLowerCase() == name.toLowerCase() }); - if ( index < 0 ) { - break; - } - } - self.nodes.push({id: ++self.nodes_seq, name: name, type: 'active', x: point[0], y: point[1]}); - } - - AddLink(source, target) { - var self = this; - if (!source || !target) return; - - var link = self.links.filter(function(l) { return (l.source.id === target.id && l.target.id === source.id); })[0]; - - if ( link ) { - link.start = true; - } - else { - link = self.links.filter(function(l) { return (l.source.id === source.id && l.target.id === target.id); })[0]; - if (!link ) { - self.links.push({id: ++self.links_seq, source: source, target: target, start: false, end: true}); - } - } - } - - ToggleLink(d) { - var self = this; - var index = self.links.findIndex(function(x) { return x.id == d.id }); - - var link = self.links[index]; - // delete link if we have both transitions already - if ( link.start && link.end ) { - self.links.splice(index, 1); - var from = d.source.name.toLowerCase(); - var to = d.target.name.toLowerCase(); - var pattern = from + ' *-> *' + to + '|' + to + ' *-> *' + from; - self.DeleteRights(null, pattern); - self.DeleteActions(null, pattern); - } - else if( link.start ) { - link.end = true; - } - else { - link.start = true; - } - } - - NodeById(id) { - var self = this; - - var nodeMap = d3.map(self.nodes, function(d) { return d.id; }); - return nodeMap.get( id ); - } - - NodeByStatus(status) { - var self = this; - - var nodeMap = d3.map(self.nodes, function(d) { return d.name; }); - return nodeMap.get( status ); - } - - DeleteNode(d) { - var self = this; - - var index = self.nodes.findIndex(function(x) { return x.id == d.id }); - self.DeleteLinksForNode(self.nodes[index]); - - self.DeleteRights(d); - self.DeleteDefaults(d); - self.DeleteActions(d); - - self.nodes.splice(index, 1); - } - - LinksForNode (node) { - var self = this; - - return self.links.filter(function(link) { - if ( link.source.id === node.id ) { - return true; - } - else if ( link.target.id === node.id && link.start ) { - return true; - } - else { - return false; - } - }); - } - - DeleteDefaults(d) { - var self = this; - - jQuery.each(self.config.defaults, function (key, value) { - if (value && value.toLowerCase() === d.name.toLowerCase()) { - delete self.config.defaults[key]; - } - }); - } - - DeleteRights(d, pattern) { - var self = this; - if ( !pattern ) { - pattern = d.name.toLowerCase() + " *->|-> *" + d.name.toLowerCase(); - } - - var re = new RegExp(pattern); - jQuery.each(self.config.rights, function(key, value) { - if ( re.test(key.toLowerCase()) ) { - delete self.config.rights[key]; - } - }); - } - - DeleteActions(d, pattern) { - var self = this; - if ( !pattern ) { - pattern = d.name.toLowerCase() + " *->|-> *" + d.name.toLowerCase(); - } - - var re = new RegExp(pattern); - var actions = []; - var tempArr = self.config.actions || []; - - var i = tempArr.length / 2; - while (i--) { - var action, info; - [action, info] = tempArr.splice(0, 2); - if (!action) continue; - - if ( ! re.test(action) ) { - actions.push(action); - actions.push(info); - } - } - self.config.actions = actions; - } - - DeleteLinksForNode(node) { - var self = this; - - if ( !node ) { - return; - } - - self.links = jQuery.grep(self.links, function (transition) { - if (transition.source.id == node.id || transition.target.id == node.id) { - return false; - } - return true; - }); - } - - UpdateNodeModel(node, args) { - var self = this; - - var nodeIndex = self.nodes.findIndex(function(x) { return x.id == node.id }); - - var oldValue = self.nodes[nodeIndex]; - - self.nodes[nodeIndex] = {...self.nodes[nodeIndex], ...args}; - var nodeUpdated = self.nodes[nodeIndex]; - - // Update any links with node being changed as source - var links = self.links.filter(function(l) { return ( - ( l.source.id === node.id ) ) - }); - links.forEach(function(link) { - var index = self.links.findIndex(function(x) { return x.id == link.id }); - self.links[index] = {...link, source: nodeUpdated} - }); - - // Update any links with node being changed as target - var links = self.links.filter(function(l) { return ( - ( l.target.id === node.id ) ) - }); - links.forEach(function(link) { - var index = self.links.findIndex(function(x) { return x.id == link.id }); - self.links[index] = {...link, target: nodeUpdated} - }); - - if ( oldValue.name === nodeUpdated.name ) return; - - // Only need to check for target - var tempArr = []; - self.create_nodes.forEach(function(target) { - if ( target != oldValue.name ) { - tempArr.push(target); - } - else { - tempArr.push(nodeUpdated.name); - } - }); - self.create_nodes = tempArr; - - for (let type in self.config.defaults) { - if ( self.config.defaults[type] === oldValue.name ) { - self.config.defaults[type] = nodeUpdated.name; - } - } - - let re_from = new RegExp(oldValue.name + ' *->'); - let re_to = new RegExp('-> *' + oldValue.name); - - for (let action in self.config.rights) { - let updated = action.replace(re_from, nodeUpdated.name + ' ->').replace(re_to, '-> ' + nodeUpdated.name); - if ( action != updated ) { - self.config.rights[updated] = self.config.rights[action]; - delete self.config.rights[action]; - } - } - - let actions = []; - if ( self.config.actions ) { - for ( let i = 0; i < self.config.actions.length; i += 2 ) { - let action = self.config.actions[i]; - let info = self.config.actions[i+1]; - let updated = action.replace(re_from, nodeUpdated.name + ' ->').replace(re_to, '-> ' + nodeUpdated.name); - actions.push(updated); - actions.push(info); - } - } - self.config.actions = actions; - - let config_name = jQuery('form[name=ModifyLifecycle] input[name=Name]').val(); - for (let item in self.maps) { - if ( item.match(config_name + ' *->')) { - let maps = self.maps[item]; - for ( let from in maps ) { - if ( from === oldValue.name ) { - maps[nodeUpdated.name] = maps[from]; - delete maps[from]; - } - } - } - else if ( item.match('-> *' + config_name) ) { - let maps = self.maps[item]; - for ( let from in maps ) { - if ( maps[from] === oldValue.name ) { - maps[from] = nodeUpdated.name; - } - } - } - } - } - - ExportAsConfiguration () { - var self = this; - - var config = { - type: self.type, - initial: [], - active: [], - inactive: [], - transitions: {}, - }; - - // Grab our status nodes - ['initial', 'active', 'inactive'].forEach(function(type) { - config[type] = self.nodes.filter(function(n) { return n.type == type }).map(function(n) { return n.name }); - }); - - // Clean removed states from create_nodes - self.create_nodes = self.create_nodes.filter(target => self.nodes.find(n => n.name == target)); - - // Grab our links - config.transitions[""] = self.create_nodes; - - var seen = {}; - self.nodes.forEach(function(source) { - var links = self.LinksForNode(source); - var targets = links.map(link => { - if ( link.source.id === source.id ) { - return link.target.name; - } - else { - return link.source.name; - } - }); - config.transitions[source.name] = targets; - seen[source.name] = 1; - }); - - for (let transition in config.transitions) { - if ( transition && ( !seen[transition] || !config.transitions[transition].length ) ) { - delete config.transitions[transition]; - } - } - - self.config = {...self.config, ...config}; - - // Set defaults on_create if it's absent - self.config.defaults ||= {}; - self.config.defaults.on_create ||= self.config.initial[0] || self.config.active[0] || null; - - var field = jQuery('form[name=ModifyLifecycle] input[name=Config]'); - field.val(JSON.stringify(self.config)); - - var pos; - if ( !jQuery('#enableSimulation').is(":checked") ) { - pos = {}; - self.nodes.forEach( function(d) { - pos[d.name] = [Math.round(d.x), Math.round(d.y)]; - }); - } - var layout = jQuery('form[name=ModifyLifecycle] input[name=Layout]'); - layout.val(pos ? JSON.stringify(pos) : ''); - - var maps = jQuery('form[name=ModifyLifecycle] input[name=Maps]'); - maps.val(JSON.stringify(self.maps)); - }; -} diff --git a/share/static/js/util.js b/share/static/js/util.js index 3bb6cbdaf1c..f01c63e2cf5 100644 --- a/share/static/js/util.js +++ b/share/static/js/util.js @@ -1226,6 +1226,16 @@ htmx.onLoad(function(elt) { jQuery(this).closest('form').find('input[name=User]').val(ui.item.id).change(); }); + if (elt.querySelectorAll('.lifecycle-ui').length) { + const checkLifecycleEditor = setInterval(function () { + if (d3 && RT.NewLifecycleEditor) { + clearInterval(checkLifecycleEditor); + elt.querySelectorAll('.lifecycle-ui').forEach(elt => { + new RT.NewLifecycleEditor(elt, JSON.parse(elt.getAttribute('data-config')), JSON.parse(elt.getAttribute('data-maps')), elt.getAttribute('data-layout') ? JSON.parse(elt.getAttribute('data-layout')) : null); + }); + } + }, 50); + } }); function filterSearchResults (type) { From 91a453734dc8a71b0647e74fd7b2ee7aa63835fd Mon Sep 17 00:00:00 2001 From: sunnavy Date: Thu, 26 Dec 2024 16:28:48 -0500 Subject: [PATCH 2/2] Drop obsolete IE detector as IE is not supported any more --- lib/RT/Interface/Web.pm | 5 ----- lib/RT/Interface/Web/MenuBuilder.pm | 10 ++++------ share/html/Admin/Lifecycles/Advanced.html | 4 ---- share/html/Admin/Lifecycles/Modify.html | 6 ------ 4 files changed, 4 insertions(+), 21 deletions(-) diff --git a/lib/RT/Interface/Web.pm b/lib/RT/Interface/Web.pm index dea9cb4dc3b..bf9639bcbb8 100644 --- a/lib/RT/Interface/Web.pm +++ b/lib/RT/Interface/Web.pm @@ -2223,11 +2223,6 @@ sub RequestENV { return $value; } -sub ClientIsIE { - # IE 11.0 dropped "MSIE", so we can't use that alone - return RequestENV('HTTP_USER_AGENT') =~ m{MSIE|Trident/} ? 1 : 0; -} - =head2 ExpandShortenerCode $ARGS Expand shortener code and put expanded ones into C<$ARGS>. diff --git a/lib/RT/Interface/Web/MenuBuilder.pm b/lib/RT/Interface/Web/MenuBuilder.pm index 7087b1d4b33..667f0f94adc 100644 --- a/lib/RT/Interface/Web/MenuBuilder.pm +++ b/lib/RT/Interface/Web/MenuBuilder.pm @@ -1815,12 +1815,10 @@ sub _BuildAdminPageMenu { RT::Interface::Web::EscapeURI(\$Name_uri); RT::Interface::Web::EscapeURI(\$Type_uri); - unless ( RT::Interface::Web->ClientIsIE ) { - $page->child( - basics => title => loc('Modify'), - path => "/Admin/Lifecycles/Modify.html?Type=" . $Type_uri . ";Name=" . $Name_uri, - ); - } + $page->child( + basics => title => loc('Modify'), + path => "/Admin/Lifecycles/Modify.html?Type=" . $Type_uri . ";Name=" . $Name_uri, + ); $page->child( actions => title => loc('Actions'), path => "/Admin/Lifecycles/Actions.html?Type=" . $Type_uri . ";Name=" . $Name_uri ); $page->child( rights => title => loc('Rights'), path => "/Admin/Lifecycles/Rights.html?Type=" . $Type_uri . ";Name=" . $Name_uri ); $page->child( mappings => title => loc('Mappings'), path => "/Admin/Lifecycles/Mappings.html?Type=" . $Type_uri . ";Name=" . $Name_uri ); diff --git a/share/html/Admin/Lifecycles/Advanced.html b/share/html/Admin/Lifecycles/Advanced.html index e7a1cc8e329..9b7d6b3a788 100644 --- a/share/html/Admin/Lifecycles/Advanced.html +++ b/share/html/Admin/Lifecycles/Advanced.html @@ -125,10 +125,6 @@ $Config ||= JSON::to_json(RT->Config->Get('Lifecycles')->{$LifecycleObj->Name}, { canonical => 1, pretty => 1 }); -if ( $ARGS{RedirectedFromModify} && RT::Interface::Web->ClientIsIE() ) { - push @results, loc("The graphical lifecycle builder is not currently supported on IE 11. You can update the lifecycle configuration using the Advanced tab or access the lifecycle editor in a supported browser."); -} - if ( !defined $Maps && ( my $all_maps = RT->Config->Get('Lifecycles')->{__maps__} ) ) { for my $item ( grep {/^\Q$Name\E -> | -> \Q$Name\E$/} keys %$all_maps ) { $Maps->{$item} = $all_maps->{$item}; diff --git a/share/html/Admin/Lifecycles/Modify.html b/share/html/Admin/Lifecycles/Modify.html index e8c1cebe004..7dc42cdd833 100644 --- a/share/html/Admin/Lifecycles/Modify.html +++ b/share/html/Admin/Lifecycles/Modify.html @@ -70,12 +70,6 @@ <%INIT> -if ( RT::Interface::Web->ClientIsIE() ) { - RT::Interface::Web::Redirect( RT->Config->Get('WebURL') - . "Admin/Lifecycles/Advanced.html?" - . $m->comp( '/Elements/QueryString', %ARGS, RedirectedFromModify => 1 ) ); -} - my ($title, @results); my $LifecycleObj = RT::Lifecycle->new(); $LifecycleObj->Load(Name => $Name, Type => $Type);