[Rt-commit] rt branch, 4.6/lifecycle-ui-dev, repushed
Craig Kaiser
craig at bestpractical.com
Thu Nov 7 16:51:48 EST 2019
The branch 4.6/lifecycle-ui-dev was deleted and repushed:
was c3ffb47e27da103975523ed3e045af7c49ce2bc1
now 94f88d59dbc3362c030dc6aba95db60378bc97d6
1: c3ffb47e27 ! 1: 94f88d59db Core Lifecycle-UI
@@ -925,7 +925,7 @@
+%#
+%# END BPS TAGGED BLOCK }}}
+<div class="lifecycle-ui" id="lifecycle-<% $id %>">
-+ <div class="lifecycle-view">
++ <div class="lifecycle-ui">
+ <svg>
+ </svg>
+ </div>
@@ -937,7 +937,7 @@
+ var config = <% JSON($config) |n %>;
+ var name = <% $Lifecycle | j%>;
+
-+ var editor = new RT.NewEditor( container, name, config );
++ var editor = new RT.NewEditor( container, config );
+ });
+ });
+ </script>
@@ -1310,386 +1310,31 @@
+ || $Ticket->CurrentUserHasRight('WatchAsAdminCc');
+</%INIT>
-diff --git a/share/static/css/elevator-light/lifecycleui-editor.css b/share/static/css/elevator-light/lifecycleui-editor.css
-new file mode 100644
---- /dev/null
-+++ b/share/static/css/elevator-light/lifecycleui-editor.css
-@@
-+.lifecycle-ui {
-+}
-+
-+.lifecycle-ui.editing svg {
-+ width: 100%;
-+ height: 500px;
-+}
-+
-+.lifecycle-ui.editing .overlay-buttons {
-+ left: 85%;
-+}
-+
-+.lifecycle-ui .inspector {
-+ display: inline-block;
-+ min-height: 500px;
-+ border: 1px solid black;
-+}
-+
-+.lifecycle-ui .inspector .content {
-+ padding: 1em;
-+ border-top: 1px solid black;
-+}
-+
-+.lifecycle-ui .inspector .header .toplevel {
-+ padding-top: 1em;
-+}
-+
-+.lifecycle-ui .inspector .header .controls {
-+ padding: 0 0 1em 1em;
-+}
-+
-+.lifecycle-ui .inspector input[type=text] {
-+ width: 10em;
-+}
-+
-+.lifecycle-ui .inspector .color-control span.current-color {
-+ display: inline;
-+ padding-left: 1em;
-+ border: 1px solid black;
-+}
-+
-+.lifecycle-ui .removing {
-+ opacity: 0;
-+}
-+
-+.lifecycle-ui .has-focus .point-handle {
-+ stroke: black;
-+ fill: steelblue;
-+}
-+
-+.lifecycle-ui .inspector .actions {
-+ list-style-type: none;
-+ padding: 0;
-+}
-+
-+.lifecycle-ui .inspector .actions .action {
-+ border: 1px solid black;
-+ margin-bottom: .5em;
-+ padding: .5em;
-+}
-+
-+.lifecycle-ui.editing svg.has-focus .decorations > * {
-+ opacity: .15;
-+}
-+
-+.lifecycle-ui.editing svg.has-focus .decorations .focus,
-+.lifecycle-ui.editing svg.has-focus .transitions .focus {
-+ opacity: 1;
-+}
-+
-+.lifecycle-ui.editing svg.has-focus[data-focus-type=transition] .statuses .focus-from {
-+ opacity: 1;
-+}
-+
-+.lifecycle-ui.editing .decorations .text-background {
-+ stroke: none;
-+ fill: none;
-+}
-+
-+.lifecycle-ui.editing svg.has-focus .decorations .focus.text-background {
-+ fill: white;
-+ filter: url(#focus);
-+}
-+
-+.lifecycle-ui .inspector button.delete {
-+ margin-top: 2em;
-+}
-+
-+.lifecycle-ui .inspector .actions button.delete {
-+ margin-top: 0;
-+}
-+
-+.lifecycle-ui .inspector .hint {
-+ vertical-align: super;
-+ font-size: .8em;
-+ color: gray;
-+}
-+
-+.lifecycle-ui .invisible {
-+ visibility: hidden;
-+}
-+
-+.lifecycle-ui .inspector .sf-menu a:visited,
-+.lifecycle-ui .inspector .sf-menu a {
-+ border: none;
-+ color: #000;
-+}
-+
-+.lifecycle-ui .inspector .sf-menu.sf-vertical {
-+ margin-left: -1em;
-+}
-+
-+.lifecycle-ui .inspector .sf-menu.sf-vertical,
-+.lifecycle-ui .inspector .sf-menu.sf-vertical > li {
-+ width: 175px;
-+}
-+
-+.lifecycle-ui .inspector .sf-vertical li:hover ul,
-+.lifecycle-ui .inspector .sf-vertical li.sfHover ul {
-+ left: 175px;
-+}
-+
-+.lifecycle-ui .statuses .hover {
-+ opacity: 1;
-+}
-+
-+.lifecycle-ui .statuses .hover circle,
-+.lifecycle-ui .transitions .hover,
-+.lifecycle-ui .decorations :not(text).hover {
-+ opacity: 1;
-+ filter: url(#hover);
-+}
-+
-+.lifecycle-ui.editing .decorations .hover.text-background {
-+ fill: white;
-+}
-+
-+.lifecycle-ui .inspector button:disabled {
-+ background: hsl(222, 20%, 40%);
-+ cursor: not-allowed;
-+}
-+
-+.lifecycle-ui .inspector tr.section td {
-+ padding-top: 1em;
-+}
-+
-+
-+
-+
-+
-+/* NEW */
-+g text {
-+ cursor: text;
-+}
-+
-+g circle.node-selected {
-+ fill: #98b9eb !important;
-+}
-
-diff --git a/share/static/css/elevator-light/lifecycleui-viewer-interactive.css b/share/static/css/elevator-light/lifecycleui-viewer-interactive.css
-new file mode 100644
---- /dev/null
-+++ b/share/static/css/elevator-light/lifecycleui-viewer-interactive.css
-@@
-+.lifecycle-ui .status-menu {
-+ position: absolute;
-+ top: 0;
-+ left: 0;
-+ display: none;
-+}
-+
-+.lifecycle-ui .status-menu.selected {
-+ display: block;
-+}
-+
-+.lifecycle-ui .status-menu .sf-menu a:visited,
-+.lifecycle-ui .status-menu .sf-menu a {
-+ border: none;
-+ color: #000;
-+}
-+
-+.lifecycle-ui .status-menu a.not-current,
-+.lifecycle-ui .status-menu a.no-transition,
-+.lifecycle-ui .status-menu a.no-permission,
-+.lifecycle-ui .status-menu a.hide-resolve-with-deps {
-+ color: #AAAAAA;
-+ cursor: default;
-+ text-decoration: none;
-+}
-+
-+.lifecycle-ui .status-menu .not-current:hover,
-+.lifecycle-ui .status-menu .no-transition:hover,
-+.lifecycle-ui .status-menu .no-permission:hover,
-+.lifecycle-ui .status-menu .hide-resolve-with-deps:hover {
-+ background-color: #fff;
-+}
-+
-+.lifecycle-ui .status-menu .sf-menu.sf-vertical {
-+ width: 10em;
-+}
-+
-+.lifecycle-ui .status-menu .sf-menu.sf-vertical li {
-+ width: 100%;
-+}
-+
-+.lifecycle-ui .status-menu .sf-menu.sf-shadow {
-+ -moz-border-radius: 0;
-+ -webkit-border-radius: 0;
-+ border-radius: 0;
-+ -moz-box-shadow: 2px 2px 8px -2px #999;
-+ -webkit-box-shadow: 2px 2px 8px -2px #999;
-+ box-shadow: 2px 2px 8px -2px #999;
-+}
-+
-+.lifecycle-ui .statuses .selected circle {
-+ filter: url(#hover);
-+}
-+
-
-diff --git a/share/static/css/elevator-light/lifecycleui-viewer.css b/share/static/css/elevator-light/lifecycleui-viewer.css
-new file mode 100644
---- /dev/null
-+++ b/share/static/css/elevator-light/lifecycleui-viewer.css
-@@
-+.lifecycle-ui svg {
-+ border: 1px solid black;
-+ width: 100%;
-+ height: 100%;
-+}
-+
-+.lifecycle-ui.center-fit:not(.zoomable) svg {
-+ border: none;
-+}
-+
-+.lifecycle-ui .overlay-buttons {
-+ position: absolute;
-+ top: 1em;
-+ left: 391px;
-+}
-+
-+.lifecycle-ui .overlay-buttons .zoom {
-+ display: none;
-+}
-+
-+.lifecycle-ui.zoomable .overlay-buttons .zoom {
-+ display: inline-block;
-+}
-+
-+.lifecycle-ui .statuses circle {
-+ stroke: black;
-+ stroke-width: 2px;
-+}
-+
-+.lifecycle-ui .transitions path {
-+ stroke: black;
-+ stroke-width: 3px;
-+ fill: none;
-+}
-+
-+.lifecycle-ui .dotted {
-+ stroke-dasharray: 2;
-+}
-+
-+.lifecycle-ui .dashed {
-+ stroke-dasharray: 5;
-+}
-+
-+.lifecycle-ui .statuses text {
-+ text-anchor: middle;
-+ alignment-baseline: middle;
-+}
-+
-+.lifecycle-ui .decorations line {
-+ stroke: #000000;
-+}
-+
-+.lifecycle-ui .decorations polygon,
-+.lifecycle-ui .decorations circle,
-+.lifecycle-ui .decorations line {
-+ stroke-width: 2px;
-+}
-+
-+.lifecycle-ui text {
-+ cursor: default;
-+ -webkit-user-select: none;
-+ -moz-user-select: none;
-+ -ms-user-select: none;
-+ user-select: none;
-+}
-+.lifecycle-ui text::selection {
-+ background: none;
-+}
-+
-+.lifecycle-ui .statuses > *,
-+.lifecycle-ui .transitions > *,
-+.lifecycle-ui .decorations > * {
-+ transition: opacity .2s;
-+}
-+
-+.lifecycle-ui .has-focus .statuses > * {
-+ opacity: .15;
-+}
-+
-+.lifecycle-ui .has-focus .transitions > * {
-+ opacity: 0;
-+}
-+
-+.lifecycle-ui .has-focus .statuses .focus {
-+ opacity: 1;
-+}
-+
-+.lifecycle-ui .has-focus .statuses .focus circle {
-+ stroke-width: 6px;
-+}
-+
-+.lifecycle-ui .has-focus .statuses .focus circle,
-+.lifecycle-ui .has-focus .transitions .focus,
-+.lifecycle-ui .has-focus .decorations :not(text).focus {
-+ filter: url(#focus);
-+}
-+
-+.lifecycle-ui .has-focus .statuses .focus-to,
-+.lifecycle-ui .has-focus .transitions .focus-to {
-+ opacity: 1;
-+}
-+
-+.lifecycle-ui .decorations text.bold {
-+ font-weight: bold;
-+}
-+
-+.lifecycle-ui .decorations text.italic {
-+ font-style: italic;
-+}
-+
-+.lifecycle-ui .lifecycle-view {
-+ position: relative;
-+ display: inline-block;
-+}
-+
-+.lifecycle-ui .overlay-buttons button {
-+ border: none;
-+}
-
diff --git a/share/static/css/elevator-light/lifecycleui.css b/share/static/css/elevator-light/lifecycleui.css
new file mode 100644
--- /dev/null
+++ b/share/static/css/elevator-light/lifecycleui.css
@@
-+body#comp-Admin-Lifecycles ul + h2 {
-+ margin-top: 2em;
++path.link {
++ fill: none;
++ stroke: #000;
++ stroke-width: 3px;
++ cursor: pointer;
+}
+
-+.ticket-info-lifecycle .titlebox-title .widget a {
-+ background-position: center -7px
++g text {
++ cursor: text;
+}
+
-+.ticket-info-lifecycle .titlebox-title {
-+ margin-left: 1em
++g circle.node-selected {
++ fill: #98b9eb !important;
+}
+
-+.ticket-info-lifecycle .titlebox-title .left {
-+ padding-left: 2.25em;
-+ margin-left: 0;
-+ padding-bottom: 4px;
-+ margin-bottom: 8px;
-+ -webkit-border-top-left-radius: 0.3em;
-+ -webkit-border-top-right-radius: 0.3em;
-+ -moz-border-radius-topleft: 0.3em;
-+ -moz-border-radius-topright: 0.3em;
-+ border-radius: 0.3em 0.3em 0 0;
-+}
-+
-+.ticket-info-lifecycle .titlebox .titlebox-title .left {
-+ background-color: #b32;
-+ color: #fff;
++.dragline {
++ fill: none;
++ stroke: #000;
++ stroke-width: 3px;
++ cursor: pointer;
+}
diff --git a/share/static/css/elevator-light/main.css b/share/static/css/elevator-light/main.css
@@ -1700,9 +1345,6 @@
@import "print.css";
@import "ckeditor5.css";
+
-+ at import "lifecycleui-editor.css";
-+ at import "lifecycleui-viewer-interactive.css";
-+ at import "lifecycleui-viewer.css";
+ at import "lifecycleui.css";
diff --git a/share/static/js/d3.min.js b/share/static/js/d3.min.js
@@ -1720,12 +1362,12 @@
@@
+jQuery( document ).ready(function () {
+ RT.NewEditor = class LifecycleEditorNew extends LifecycleModel {
-+ constructor(container, name, config) {
++ constructor(container, config) {
+ super("LifecycleModel");
+
+ var self = this;
+ self.width = 960;
-+ self.height = 500;
++ self.height = 350;
+ self.node_radius = 35;
+
+ self.svg = d3.select(container).select('svg')
@@ -1801,8 +1443,8 @@
+ dist = Math.sqrt(deltaX * deltaX + deltaY * deltaY),
+ normX = deltaX / dist,
+ normY = deltaY / dist,
-+ sourcePadding = 40,
-+ targetPadding = 40,
++ sourcePadding = 45,
++ targetPadding = 45,
+ sourceX = sx + (sourcePadding * normX),
+ sourceY = sy + (sourcePadding * normY),
+ targetX = tx - (targetPadding * normX),
@@ -1814,6 +1456,12 @@
+ .force("link")
+ .links(self.links)
+ .id(d => d.id);
++
++ // Add our current config to the DOM
++ var form = jQuery('form[name="ModifyLifecycle"]');
++ var field = jQuery('<input type="hidden" name="Config">');
++ field.val(JSON.stringify(self.config));
++ form.append(field);
+ }
+
+ SetUp() {
@@ -1824,8 +1472,8 @@
+ .attr('id', 'end-arrow')
+ .attr('viewBox', '0 -5 10 10')
+ .attr('refX', 6)
-+ .attr('markerWidth', 8)
-+ .attr('markerHeight', 8)
++ .attr('markerWidth', 6)
++ .attr('markerHeight', 6)
+ .attr('orient', 'auto')
+ .append('svg:path')
+ .attr('d', 'M0,-5L10,0L0,5')
@@ -1835,8 +1483,8 @@
+ .attr('id', 'start-arrow')
+ .attr('viewBox', '0 -5 10 10')
+ .attr('refX', 4)
-+ .attr('markerWidth', 8)
-+ .attr('markerHeight', 8)
++ .attr('markerWidth', 6)
++ .attr('markerHeight', 6)
+ .attr('orient', 'auto')
+ .append('svg:path')
+ .attr('d', 'M10,-5L0,0L10,5')
@@ -1845,27 +1493,45 @@
+ // line displayed when dragging new nodes
+ self.drag_line = self.svg.append('svg:path')
+ .attr('class', 'dragline hidden')
-+ .attr('d', 'M0,0L0,0');
-+
-+ self.svg.on('contextmenu', function () {
-+ d3.event.preventDefault();
-+
-+ var newNodes = self.AddNode(d3.mouse(this));
-+ self.Refresh(newNodes);
-+ });
-+
-+ self.svg.on("click", function () {
-+ d3.event.stopPropagation();
-+ self.Deselect();
-+ })
++ .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.Deselect();
++ }
++ else {
++ self.simulation.stop();
++ self.AddNode(d3.mouse(this));
++
++ self.ExportAsConfiguration();
++
++ self.Refresh();
++ }
++ })
++ .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 ( d3.event.keyCode == 68 || d3.event.keyCode == 46 && d ) {
+ d3.event.preventDefault();
+ d3.event.stopPropagation();
+
++ self.simulation.stop();
+ self.svg.selectAll('.node-selected').each(function(d) {
+ self.DeleteNode(d);
++
++ self.ExportAsConfiguration();
+
+ self.Deselect();
+ self.Refresh();
@@ -1881,11 +1547,6 @@
+ .data(self.nodes.filter(d => d.id >= 0));
+
+ self.node.exit()
-+ .classed("removing", true)
-+ .append('circle')
-+ .append('text')
-+ .append('title')
-+ .transition().duration(200 * self.animationFactor).ease(d3.easeLinear)
+ .remove();
+
+ // Add new nodes and draw them
@@ -1920,6 +1581,39 @@
+ d3.event.stopPropagation();
+ 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 ) 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.Refresh();
++ })
++ .on('mouseup', function(d) {
++ self.mouseup_node = d;
++ // needed by FF
++ self.drag_line
++ .classed('hidden', true)
++ .style('marker-end', '');
++
++ self.simulation.stop();
++ // add link to model
++ self.AddLink(self.mousedown_node, self.mouseup_node);
++
++ self.ExportAsConfiguration();
++
++ self.ResetMouseVars();
++ self.Refresh();
++ });
+
+ self.node.select("text")
+ .text(function(d) { return d.name; })
@@ -2020,26 +1714,36 @@
+ var linkEnter = self.link.enter().append("g")
+ .append("path")
+ .attr("class", 'link')
-+ .attr("stroke-width", 1)
-+ .attr("style", "stroke: black; stroke-opacity: 0.6;")
+ .style("marker-start", (d => d.right ? 'url(#start-arrow)' : '' ))
-+ .style("marker-end", (d => d.left ? 'url(#end-arrow)' : '' ))
-+ .attr("transform", "translate(0,0)");
++ .style("marker-end", (d => d.left ? 'url(#end-arrow)' : '' ))
++ .attr("transform", "translate(0,0)")
++ .on("click", d => {
++ d3.event.stopPropagation();
++ self.simulation.stop();
++ self.ToggleLink(d);
++
++ self.ExportAsConfiguration();
++
++ self.Refresh();
++ });
+ self.link = linkEnter.merge(self.link);
++ self.link
++ .style("marker-start", (d => d.right ? 'url(#start-arrow)' : '' ))
++ .style("marker-end", (d => d.left ? 'url(#end-arrow)' : '' ));
+ }
+
+ Refresh() {
+ var self = this;
+
-+ self.simulation.stop();
-+ self.simulation.nodes(self.nodes);
-+ self.simulation.force("link").links(self.links);
-+
++ self.simulation
++ .nodes(self.nodes)
++ .force("link")
++ .links(self.links)
++ .id(d => d.id);
++
++ self.RenderLink();
+ self.RenderNode();
-+ self.RenderLink();
-+
-+ self.simulation.alpha(1);
-+ self.simulation.restart();
++ self.simulation.alpha(0.5).restart();
+ }
+
+ SelectNode(node) {
@@ -2053,7 +1757,7 @@
+ Deselect() {
+ this.selected_node = null;
+
-+ var node = this.svg.selectAll('.node-selected')
++ this.svg.selectAll('.node-selected')
+ .classed('node-selected', false);
+ }
+
@@ -2065,6 +1769,39 @@
+ node.text(text + '…');
+ textLength = node.node().getComputedTextLength();
+ }
++ }
++
++ Mousemove(d) {
++ var self = this;
++ if (!self.mousedown_node) return;
++
++ this.drag_line.attr('d', 'M' + self.mousedown_node.x + ',' + self.mousedown_node.y + 'L' + d3.mouse(d)[0] + ',' + d3.mouse(d)[1]);
++
++ this.Refresh();
++ }
++
++ Mouseup() {
++ var self = this;
++
++ if(self.mousedown_node) {
++ // hide drag line
++ self.drag_line
++ .classed('hidden', true)
++ .style('marker-end', '');
++ }
++ }
++
++ Mousedown(d) {
++ d3.event.preventDefault();
++ d3.event.stopPropagation();
++ }
++
++ ResetMouseVars(){
++ var self = this;
++
++ self.mousedown_link = null;
++ self.mousedown_node = null;
++ self.mouseup_node = null;
+ }
+ }
+});
@@ -2120,6 +1857,32 @@
+ this.nodes.push({id: ++this.nodes_seq, name: name, type: 'active', x: point[0], y: point[1]});
+ }
+
++ AddLink(source, target) {
++ self = this;
++ if (!source || !target) return;
++
++ var link = self.links.filter(function(l) { return (l.source === target && l.target === source); })[0];
++ if(link) link.right = true;
++ else self.links.push({id: ++self.links_seq, source: source, target: target, right: false, left: true});
++ }
++
++ ToggleLink(d) {
++ var self = this;
++ var index = self.links.findIndex(x => x.id == d.id);
++
++ var link = self.links[index];
++ // delete link if we have both transitions already
++ if ( link.right && link.left ) {
++ self.links.splice(index, 1);
++ }
++ else if( link.right ) {
++ link.left = true;
++ }
++ else {
++ link.right = true;
++ }
++ }
++
+ NodeById(id) {
+ var nodeMap = d3.map(this.nodes, function(d) { return d.id; });
+ return nodeMap.get( id );
@@ -2135,6 +1898,20 @@
+ this.DeleteLinksForNode(this.nodes[index]);
+
+ this.nodes.splice(index, 1);
++ }
++
++ LinksForNode (node) {
++ return this.links.filter(link => {
++ if ( link.source.id === node.id ) {
++ return true;
++ }
++ else if ( link.target.id === node.id && link.left ) {
++ return true;
++ }
++ else {
++ return false;
++ }
++ });
+ }
+
+ DeleteLinksForNode(node) {
@@ -2170,6 +1947,35 @@
+ this.links[index] = {...link, target: nodeUpdated}
+ });
+ }
++
++ ExportAsConfiguration () {
++ var self = this;
++ var config = {
++ type: self.type,
++ initial: [],
++ active: [],
++ inactive: [],
++ actions: [],
++ rights: {},
++ transitions: [],
++ };
++
++ // Grab our status nodes
++ ['initial', 'active', 'inactive'].forEach(type => {
++ config[type] = self.nodes.filter(n => n.type == type);
++ });
++
++ // Grab our links
++ config.transitions[""] = self.config.transitions[""];
++
++ self.nodes.forEach(source => {
++ config.transitions.push({ [source.name] : self.LinksForNode(source).forEach(l => {return l.target.name;}) });
++ });
++ self.config = config;
++
++ var field = jQuery('<input type="hidden" name="Config">');
++ field.val(JSON.stringify(self.config));
++ };
+}
diff --git a/share/static/js/lifecycleui-viewer-interactive.js b/share/static/js/lifecycleui-viewer-interactive.js
More information about the rt-commit
mailing list