[Bps-public-commit] rt-extension-lifecycleui branch, master, updated. 9ce2cc6d0513672a0ae05074aea0bfa29d068f6d

Shawn Moore shawn at bestpractical.com
Fri Sep 1 17:44:00 EDT 2017


The branch, master has been updated
       via  9ce2cc6d0513672a0ae05074aea0bfa29d068f6d (commit)
       via  a7a74853481197c88dffb832c017f37fd4b5e8f2 (commit)
       via  0fef6365a64cd593199c9e62c236af591588c6eb (commit)
       via  89a290ed288f51ad9062cdea0f3b1ea38b72b299 (commit)
       via  14aacdfb26388e17c7236f03100520ac0cf018de (commit)
       via  9a4e0e291e3b276e07a2e7207ce82c68d4016de0 (commit)
       via  c233bf82aa878969fb2cdfd3892288d5fd6dcb22 (commit)
       via  8751b9490e00857666d1221a372f689e2bae5619 (commit)
       via  06417cb3a5aec1a59bb520a5467270480c71447f (commit)
       via  8065bf22463b2d99bf3124dd2c34f7bd15a0ec2d (commit)
       via  ba6cb8fd833b7fc705e593b79c47435260a39304 (commit)
       via  2d2767b1b888447a57274306b8fa73e86fb75f60 (commit)
       via  cd30ce1d1492ccfe6c3fb672ecb22ff7b7f5989e (commit)
       via  31d708fd767bd8e2d5e01d5be7879139dc5a6803 (commit)
       via  5b2da2bf178d1fdee66e41740eae74173e8227e7 (commit)
       via  a4b2f06df65b5834f68e64b859a307334db1b738 (commit)
       via  03a2af892b0022eceb0f3b057890cd3300c39384 (commit)
       via  2992aac19a5db198f5aa688b1a5be967c70d779e (commit)
       via  808485c2b06bb62c1b4e9e165fa595e07fd48aee (commit)
       via  895e12f7c3c28be5c081ba32e3ff695ad0db2c45 (commit)
       via  c74b8ed568f1034dcfa9b6310345dce07cdbfb0e (commit)
       via  e7d1d48568d25d7feff6d3cb1f285f30a71dcb64 (commit)
       via  adf869758688789534b88cb29ff3134524649df3 (commit)
       via  69a229d1494642b8e28b992a676e0a724a2ddccd (commit)
       via  e73926ee05bfdff2b75635db2a7854074d059770 (commit)
       via  d4d3f7da5cb0ec457e5b9cd5f1d95a9c37f9f489 (commit)
       via  327167b25db4b972dab5deaf02c08fe441ea51f8 (commit)
       via  9bcde8447e70ed2725d0192a42a4daeb348c7678 (commit)
       via  3a25c2726e1b0c1d7a150fa16a5bc1a1b2ac0852 (commit)
       via  10b55a2f11a398c6cda6ced24d881c15bd68f478 (commit)
       via  30d9a3883a8b860fc374db778f2e4ea6d50e4825 (commit)
       via  cde92658e5b824012fc9334ab13e4aba2631315b (commit)
       via  24099fc0b5f00e411cb4d9123803b76238b4c17a (commit)
       via  3f26d5081710f72c5a83fe37e3cc3a516826f8d2 (commit)
       via  a503a550a3df35fab616ee1c76dc1dca0c625663 (commit)
      from  490515b2479484b621d958d9ac7380fc50a39105 (commit)

Summary of changes:
 html/Elements/LifecycleGraph               |  17 +-
 html/Elements/LifecycleInspector           |   1 +
 html/Elements/LifecycleInspectorAction     |  14 +
 html/Elements/LifecycleInspectorStatus     |  10 +-
 html/Elements/LifecycleInspectorTransition |   9 +-
 lib/RT/Extension/LifecycleUI.pm            |   8 +-
 static/css/lifecycleui-display.css         |  97 ------
 static/css/lifecycleui-editor.css          |  73 ++++-
 static/css/lifecycleui-viewer.css          |  43 +++
 static/js/lifecycleui-editor.js            | 287 +++++++++++++++++
 static/js/lifecycleui-model.js             | 161 ++++++++--
 static/js/lifecycleui-viewer.js            | 485 ++++++++++-------------------
 12 files changed, 757 insertions(+), 448 deletions(-)
 create mode 100644 html/Elements/LifecycleInspectorAction
 delete mode 100644 static/css/lifecycleui-display.css
 create mode 100644 static/css/lifecycleui-viewer.css
 create mode 100644 static/js/lifecycleui-editor.js

- Log -----------------------------------------------------------------
commit a503a550a3df35fab616ee1c76dc1dca0c625663
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Fri Sep 1 13:59:33 2017 +0000

    Refactor viewer to use more method calls

diff --git a/html/Elements/LifecycleGraph b/html/Elements/LifecycleGraph
index 4491f80..9512693 100644
--- a/html/Elements/LifecycleGraph
+++ b/html/Elements/LifecycleGraph
@@ -1,5 +1,9 @@
 <div class="lifecycle-ui" id="lifecycle-<% $id %>">
-    <svg<% $Editing ? ' class="editing"' : '' |n %>></svg>
+    <svg<% $Editing ? ' class="editing"' : '' |n %>>
+        <g class="transitions"></g>
+        <g class="statuses"></g>
+        <g class="decorations"></g>
+    </svg>
 % if ($Editing) {
     <& /Elements/LifecycleInspector &>
 % }
diff --git a/static/js/lifecycleui-viewer.js b/static/js/lifecycleui-viewer.js
index be92f1d..46ba344 100644
--- a/static/js/lifecycleui-viewer.js
+++ b/static/js/lifecycleui-viewer.js
@@ -1,7 +1,6 @@
 jQuery(function () {
-    var STATUS_CIRCLE_RADIUS = 35;
-
     function Viewer (container) {
+        this.statusCircleRadius = 35;
     };
 
     Viewer.prototype._initializeTemplates = function (container) {
@@ -39,7 +38,7 @@ jQuery(function () {
             .attr('markerWidth', 5)
             .attr('markerUnits', 'strokeWidth')
             .attr('orient', 'auto')
-            .attr('refX', STATUS_CIRCLE_RADIUS + 5)
+            .attr('refX', this.statusCircleRadius + 5)
             .attr('refY', 0)
             .attr('viewBox', '-5 -5 10 10')
             .append('path')
@@ -53,301 +52,310 @@ jQuery(function () {
                  .range([padding, size - padding]);
     };
 
-    Viewer.prototype.initializeEditor = function (node, config) {
+    Viewer.prototype.setInspectorContent = function (node) {
         var self = this;
+        var lifecycle = self.lifecycle;
+        var inspector = self.inspector;
 
-        var container  = self.container = jQuery(node);
-        var svg        = self.svg = d3.select(node).select('svg');
-        self.templates = self._initializeTemplates(container);
-
-        var inspector = self.container.find('.inspector');
-
-        var transitionContainer = svg.append('g').classed('transitions', true);
-        var statusContainer = svg.append('g').classed('statuses', true);
-        var decorationContainer = svg.append('g').classed('decorations', true);
-
-        var width = svg.node().getBoundingClientRect().width;
-        var height = svg.node().getBoundingClientRect().height;
-
-        var lifecycle = new RT.Lifecycle();
-        lifecycle.initializeFromConfig(config);
-
-        var xScale = self.createScale(width, STATUS_CIRCLE_RADIUS * 2);
-        var yScale = self.createScale(height, STATUS_CIRCLE_RADIUS * 2);
-
-        self.createArrowHead();
-
-        var setInspectorContent = function (node) {
-            var type = node ? node._type : 'canvas';
+        var type = node ? node._type : 'canvas';
 
-            var params = { lifecycle: lifecycle };
-            params[type] = node;
+        var params = { lifecycle: lifecycle };
+        params[type] = node;
 
-            inspector.html(self.templates[type](params));
-            inspector.find('sf-menu').supersubs().superfish({ dropShadows: false, speed: 'fast', delay: 0 }).supposition()
+        inspector.html(self.templates[type](params));
+        inspector.find('sf-menu').supersubs().superfish({ dropShadows: false, speed: 'fast', delay: 0 }).supposition()
 
-            inspector.find(':input').change(function () {
-                var field = this.name;
-                var value = jQuery(this).val();
-                lifecycle.updateItem(node, field, value);
-                refreshDisplay();
-            });
+        inspector.find(':input').change(function () {
+            var field = this.name;
+            var value = jQuery(this).val();
+            lifecycle.updateItem(node, field, value);
+            self.refreshDisplay();
+        });
 
-            inspector.find('button.change-color').click(function (e) {
-                e.preventDefault();
-                var picker = jQuery('<div class="color-picker"></div>');
-                jQuery(this).replaceWith(picker);
-
-                var skipUpdateCallback = 0;
-                var farb = jQuery.farbtastic(picker, function (newColor) {
-                    if (skipUpdateCallback) {
-                        return;
-                    }
-                    inspector.find('.status-color').val(newColor);
-                    lifecycle.updateItem(node, 'color', newColor);
-                    refreshDisplay();
-                });
-                farb.setColor(node.color);
-
-                var input = jQuery('<input class="status-color" size=8 maxlength=7>');
-                inspector.find('.status-color').replaceWith(input);
-                input.on('input', function () {
-                    var newColor = input.val();
-                    if (newColor.match(/^#[a-fA-F0-9]{6}$/)) {
-                        skipUpdateCallback = 1;
-                        farb.setColor(newColor);
-                        skipUpdateCallback = 0;
-
-                        lifecycle.updateItem(node, 'color', newColor);
-                        refreshDisplay();
-                    }
-                });
-                input.val(node.color);
+        inspector.find('button.change-color').click(function (e) {
+            e.preventDefault();
+            var picker = jQuery('<div class="color-picker"></div>');
+            jQuery(this).replaceWith(picker);
+
+            var skipUpdateCallback = 0;
+            var farb = jQuery.farbtastic(picker, function (newColor) {
+                if (skipUpdateCallback) {
+                    return;
+                }
+                inspector.find('.status-color').val(newColor);
+                lifecycle.updateItem(node, 'color', newColor);
+                self.refreshDisplay();
             });
+            farb.setColor(node.color);
 
-            inspector.find('button.delete').click(function (e) {
-                e.preventDefault();
-                lifecycle.deleteItemForKey(node._key);
+            var input = jQuery('<input class="status-color" size=8 maxlength=7>');
+            inspector.find('.status-color').replaceWith(input);
+            input.on('input', function () {
+                var newColor = input.val();
+                if (newColor.match(/^#[a-fA-F0-9]{6}$/)) {
+                    skipUpdateCallback = 1;
+                    farb.setColor(newColor);
+                    skipUpdateCallback = 0;
 
-                deselectAll(true);
-                refreshDisplay();
+                    lifecycle.updateItem(node, 'color', newColor);
+                    self.refreshDisplay();
+                }
             });
+            input.val(node.color);
+        });
 
-            inspector.find('a.add-transition').click(function (e) {
-                e.preventDefault();
-                var button = jQuery(this);
-                var fromStatus = button.data('from');
-                var toStatus   = button.data('to');
-
-                lifecycle.addTransition(fromStatus, toStatus);
-
-                button.closest('li').addClass('hidden');
-
-                inspector.find('a.select-transition[data-from="'+fromStatus+'"][data-to="'+toStatus+'"]').closest('li').removeClass('hidden');
+        inspector.find('button.delete').click(function (e) {
+            e.preventDefault();
+            lifecycle.deleteItemForKey(node._key);
 
-                refreshDisplay();
-                selectStatus(node.name);
-            });
-        };
+            self.deselectAll(true);
+            self.refreshDisplay();
+        });
 
-        var deselectAll = function (inspectCanvas) {
-            if (inspectCanvas) {
-                setInspectorContent(null);
-            }
+        inspector.find('a.add-transition').click(function (e) {
+            e.preventDefault();
+            var button = jQuery(this);
+            var fromStatus = button.data('from');
+            var toStatus   = button.data('to');
 
-            svg.classed('selection', false);
-            svg.selectAll('.selected').classed('selected', false);
-            svg.selectAll('.selected-source').classed('selected-source', false);
-            svg.selectAll('.selected-sink').classed('selected-sink', false);
-            svg.selectAll('.reachable').classed('reachable', false);
-        };
+            lifecycle.addTransition(fromStatus, toStatus);
 
-        var selectStatus = function (name) {
-            var d = lifecycle.statusObjectForName(name);
-            setInspectorContent(d);
+            button.closest('li').addClass('hidden');
 
-            deselectAll(false);
+            inspector.find('a.select-transition[data-from="'+fromStatus+'"][data-to="'+toStatus+'"]').closest('li').removeClass('hidden');
 
-            svg.classed('selection', true);
-            statusContainer.selectAll('*[data-key="'+d._key+'"]').classed('selected', true);
+            self.refreshDisplay();
+            self.selectStatus(node.name);
+        });
 
-            jQuery.each(lifecycle.transitionsFrom(name), function (i, transition) {
-                var key = lifecycle.keyForStatusName(transition.to);
-                statusContainer.selectAll('*[data-key="'+key+'"]').classed('reachable', true);
-                transitionContainer.selectAll('path[data-key="'+transition._key+'"]').classed('selected', true);
-            });
-        };
-
-        var selectTransition = function (fromStatus, toStatus) {
-            var d = lifecycle.hasTransition(fromStatus, toStatus);
-            setInspectorContent(d);
-
-            deselectAll(false);
-
-            svg.classed('selection', true);
-
-            var fromKey = lifecycle.keyForStatusName(fromStatus);
-            var toKey = lifecycle.keyForStatusName(toStatus);
-            statusContainer.selectAll('*[data-key="'+fromKey+'"]').classed('selected-source', true);
-            statusContainer.selectAll('*[data-key="'+toKey+'"]').classed('selected-sink', true);
-            transitionContainer.select('path[data-key="'+d._key+'"]').classed('selected', true);
-        };
-
-        var selectDecoration = function (key) {
-            var d = lifecycle.itemForKey(key);
-            setInspectorContent(d);
-
-            deselectAll(false);
-
-            svg.classed('selection', true);
-            decorationContainer.selectAll('*[data-key="'+key+'"]').classed('selected', true);
-        };
-
-        var refreshStatusNodes = function () {
-            var statuses = statusContainer.selectAll("circle")
-                                          .data(lifecycle.statusObjects(), function (d) { return d._key });
-
-            statuses.exit()
-                  .classed("removing", true)
-                  .transition().duration(200)
-                    .attr("r", STATUS_CIRCLE_RADIUS * .8)
-                    .remove();
-
-            statuses.enter().append("circle")
-                            .attr("r", STATUS_CIRCLE_RADIUS)
-                            .attr("data-key", function (d) { return d._key })
-                            .on("click", function (d) {
-                                d3.event.stopPropagation();
-                                selectStatus(d.name);
-                            })
-                    .merge(statuses)
-                            .attr("cx", function (d) { return xScale(d.x) })
-                            .attr("cy", function (d) { return yScale(d.y) })
-                            .attr("fill", function (d) { return d.color });
-        };
-
-        function truncateLabel () {
-            var self = d3.select(this),
-                textLength = self.node().getComputedTextLength(),
-                text = self.text();
-            while (textLength > STATUS_CIRCLE_RADIUS*1.8 && text.length > 0) {
-                text = text.slice(0, -1);
-                self.text(text + '…');
-                textLength = self.node().getComputedTextLength();
-            }
-        };
-
-        var refreshStatusLabels = function () {
-            var labels = statusContainer.selectAll("text")
-                                        .data(lifecycle.statusObjects(), function (d) { return d._key });
-
-            labels.exit()
-                .classed("removing", true)
-                .transition().duration(200)
-                  .remove();
-
-            labels.enter().append("text")
-                          .attr("data-key", function (d) { return d._key })
-                          .on("click", function (d) {
-                              d3.event.stopPropagation();
-                              selectStatus(d.name);
-                          })
-                  .merge(labels)
-                          .attr("x", function (d) { return xScale(d.x) })
-                          .attr("y", function (d) { return yScale(d.y) })
-                          .attr("fill", function (d) { return d3.hsl(d.color).l > 0.35 ? '#000' : '#fff' })
-                          .text(function (d) { return d.name }).each(truncateLabel)
-        };
-
-        var linkArc = function (d) {
-          var from = lifecycle.statusObjectForName(d.from);
-          var to = lifecycle.statusObjectForName(d.to);
-          var dx = xScale(to.x - from.x),
-              dy = yScale(to.y - from.y),
-              dr = Math.abs(dx*6) + Math.abs(dy*6);
-          return "M" + xScale(from.x) + "," + yScale(from.y) + "A" + dr + "," + dr + " 0 0,1 " + xScale(to.x) + "," + yScale(to.y);
-        };
-
-        var refreshTransitions = function () {
-            var paths = transitionContainer.selectAll("path")
-                            .data(lifecycle.transitions, function (d) { return d._key });
-
-            paths.exit()
-                .classed("removing", true)
-                .transition().duration(200)
-                  .remove();
-
-            paths.enter().append("path")
-                         .attr("data-key", function (d) { return d._key })
-                         .on("click", function (d) {
-                             d3.event.stopPropagation();
-                             selectTransition(d.from, d.to);
-                         })
-                  .merge(paths)
-                          .attr("d", linkArc)
-                          .classed("dashed", function (d) { return d.style == 'dashed' })
-                          .classed("dotted", function (d) { return d.style == 'dotted' })
-        };
-
-        var refreshTextDecorations = function () {
-            var labels = decorationContainer.selectAll("text")
-                            .data(lifecycle.decorations.text, function (d) { return d._key });
-
-            labels.exit()
-                .classed("removing", true)
-                .transition().duration(200)
-                  .remove();
-
-            labels.enter().append("text")
-                         .attr("data-key", function (d) { return d._key })
-                         .on("click", function (d) {
-                             d3.event.stopPropagation();
-                             selectDecoration(d._key);
-                         })
-                  .merge(labels)
-                          .attr("x", function (d) { return xScale(d.x) })
-                          .attr("y", function (d) { return yScale(d.y) })
-                          .text(function (d) { return d.text });
-        };
-
-        var refreshDecorations = function () {
-            refreshTextDecorations();
-        };
-
-        var refreshDisplay = function () {
-            refreshTransitions();
-            refreshStatusNodes();
-            refreshStatusLabels();
-            refreshDecorations();
-        };
-
-        inspector.on('click', 'a.select-status', function (e) {
+        inspector.find('a.select-status').on('click', function (e) {
             e.preventDefault();
             var statusName = jQuery(this).data('name');
-            selectStatus(statusName);
+            self.selectStatus(statusName);
         });
 
-        inspector.on('click', 'a.select-transition', function (e) {
+        inspector.find('a.select-transition').on('click', function (e) {
             e.preventDefault();
             var button = jQuery(this);
             var fromStatus = button.data('from');
             var toStatus   = button.data('to');
 
-            selectTransition(fromStatus, toStatus);
+            self.selectTransition(fromStatus, toStatus);
         });
+    };
+
+    Viewer.prototype.deselectAll = function (inspectCanvas) {
+        if (inspectCanvas) {
+            this.setInspectorContent(null);
+        }
+
+        var svg = this.svg;
+        svg.classed('selection', false);
+        svg.selectAll('.selected').classed('selected', false);
+        svg.selectAll('.selected-source').classed('selected-source', false);
+        svg.selectAll('.selected-sink').classed('selected-sink', false);
+        svg.selectAll('.reachable').classed('reachable', false);
+    };
+
+    Viewer.prototype.selectStatus = function (name) {
+        var self = this;
+        var d = self.lifecycle.statusObjectForName(name);
+        self.setInspectorContent(d);
+
+        self.deselectAll(false);
+
+        self.svg.classed('selection', true);
+        self.statusContainer.selectAll('*[data-key="'+d._key+'"]').classed('selected', true);
+
+        jQuery.each(self.lifecycle.transitionsFrom(name), function (i, transition) {
+            var key = self.lifecycle.keyForStatusName(transition.to);
+            self.statusContainer.selectAll('*[data-key="'+key+'"]').classed('reachable', true);
+            self.transitionContainer.selectAll('path[data-key="'+transition._key+'"]').classed('selected', true);
+        });
+    };
+
+    Viewer.prototype.selectTransition = function (fromStatus, toStatus) {
+        var self = this;
+        var d = self.lifecycle.hasTransition(fromStatus, toStatus);
+        self.setInspectorContent(d);
+
+        self.deselectAll(false);
+
+        self.svg.classed('selection', true);
+
+        var fromKey = self.lifecycle.keyForStatusName(fromStatus);
+        var toKey = self.lifecycle.keyForStatusName(toStatus);
+        self.statusContainer.selectAll('*[data-key="'+fromKey+'"]').classed('selected-source', true);
+        self.statusContainer.selectAll('*[data-key="'+toKey+'"]').classed('selected-sink', true);
+        self.transitionContainer.select('path[data-key="'+d._key+'"]').classed('selected', true);
+    };
+
+    Viewer.prototype.selectDecoration = function (key) {
+        var d = this.lifecycle.itemForKey(key);
+        this.setInspectorContent(d);
+
+        this.deselectAll(false);
+
+        this.svg.classed('selection', true);
+        this.decorationContainer.selectAll('*[data-key="'+key+'"]').classed('selected', true);
+    };
+
+    Viewer.prototype.refreshStatusNodes = function () {
+        var self = this;
+        var statuses = self.statusContainer.selectAll("circle")
+                                           .data(self.lifecycle.statusObjects(), function (d) { return d._key });
+
+        statuses.exit()
+              .classed("removing", true)
+              .transition().duration(200)
+                .attr("r", self.statusCircleRadius * .8)
+                .remove();
+
+        statuses.enter().append("circle")
+                        .attr("r", self.statusCircleRadius)
+                        .attr("data-key", function (d) { return d._key })
+                        .on("click", function (d) {
+                            d3.event.stopPropagation();
+                            self.selectStatus(d.name);
+                        })
+                .merge(statuses)
+                        .attr("cx", function (d) { return self.xScale(d.x) })
+                        .attr("cy", function (d) { return self.yScale(d.y) })
+                        .attr("fill", function (d) { return d.color });
+    };
+
+    Viewer.prototype.truncateLabel = function (element) {
+        var node = d3.select(element),
+            textLength = node.node().getComputedTextLength(),
+            text = node.text();
+        while (textLength > this.statusCircleRadiuds*1.8 && text.length > 0) {
+            text = text.slice(0, -1);
+            node.text(text + '…');
+            textLength = node.node().getComputedTextLength();
+        }
+    };
+
+    Viewer.prototype.refreshStatusLabels = function () {
+        var self = this;
+        var labels = self.statusContainer.selectAll("text")
+                                         .data(self.lifecycle.statusObjects(), function (d) { return d._key });
+
+        labels.exit()
+            .classed("removing", true)
+            .transition().duration(200)
+              .remove();
+
+        labels.enter().append("text")
+                      .attr("data-key", function (d) { return d._key })
+                      .on("click", function (d) {
+                          d3.event.stopPropagation();
+                          self.selectStatus(d.name);
+                      })
+              .merge(labels)
+                      .attr("x", function (d) { return self.xScale(d.x) })
+                      .attr("y", function (d) { return self.yScale(d.y) })
+                      .attr("fill", function (d) { return d3.hsl(d.color).l > 0.35 ? '#000' : '#fff' })
+                      .text(function (d) { return d.name }).each(function () { self.truncateLabel(this) })
+    };
+
+    Viewer.prototype.transitionArc = function (d) {
+      var from = this.lifecycle.statusObjectForName(d.from);
+      var to = this.lifecycle.statusObjectForName(d.to);
+      var dx = this.xScale(to.x - from.x),
+          dy = this.yScale(to.y - from.y),
+          dr = Math.abs(dx*6) + Math.abs(dy*6);
+      return "M" + this.xScale(from.x) + "," + this.yScale(from.y) + "A" + dr + "," + dr + " 0 0,1 " + this.xScale(to.x) + "," + this.yScale(to.y);
+    };
+
+    Viewer.prototype.refreshTransitions = function () {
+        var self = this;
+        var paths = self.transitionContainer.selectAll("path")
+                        .data(self.lifecycle.transitions, function (d) { return d._key });
+
+        paths.exit()
+            .classed("removing", true)
+            .transition().duration(200)
+              .remove();
+
+        paths.enter().append("path")
+                     .attr("data-key", function (d) { return d._key })
+                     .on("click", function (d) {
+                         d3.event.stopPropagation();
+                         self.selectTransition(d.from, d.to);
+                     })
+              .merge(paths)
+                      .attr("d", function (d) { return self.transitionArc(d) })
+                      .classed("dashed", function (d) { return d.style == 'dashed' })
+                      .classed("dotted", function (d) { return d.style == 'dotted' })
+    };
+
+    Viewer.prototype.refreshTextDecorations = function () {
+        var self = this;
+        var labels = self.decorationContainer.selectAll("text")
+                         .data(self.lifecycle.decorations.text, function (d) { return d._key });
+
+        labels.exit()
+            .classed("removing", true)
+            .transition().duration(200)
+              .remove();
+
+        labels.enter().append("text")
+                     .attr("data-key", function (d) { return d._key })
+                     .on("click", function (d) {
+                         d3.event.stopPropagation();
+                         self.selectDecoration(d._key);
+                     })
+              .merge(labels)
+                      .attr("x", function (d) { return self.xScale(d.x) })
+                      .attr("y", function (d) { return self.yScale(d.y) })
+                      .text(function (d) { return d.text });
+    };
+
+    Viewer.prototype.refreshDecorations = function () {
+        this.refreshTextDecorations();
+    };
+
+    Viewer.prototype.refreshDisplay = function () {
+        this.refreshTransitions();
+        this.refreshStatusNodes();
+        this.refreshStatusLabels();
+        this.refreshDecorations();
+    };
+
+    Viewer.prototype.initializeEditor = function (node, config) {
+        var self = this;
+
+        self.container = jQuery(node);
+        self.svg       = d3.select(node).select('svg');
+        self.templates = self._initializeTemplates(self.container);
+        self.inspector = self.container.find('.inspector');
+
+        self.transitionContainer = self.svg.select('g.transitions');
+        self.statusContainer     = self.svg.select('g.statuses');
+        self.decorationContainer = self.svg.select('g.decorations');
+
+        self.width  = self.svg.node().getBoundingClientRect().width;
+        self.height = self.svg.node().getBoundingClientRect().height;
+
+        self.xScale = self.createScale(self.width, self.statusCircleRadius * 2);
+        self.yScale = self.createScale(self.height, self.statusCircleRadius * 2);
+
+        self.lifecycle = new RT.Lifecycle();
+        self.lifecycle.initializeFromConfig(config);
+
+        self.createArrowHead();
 
-        setInspectorContent(null);
+        self.setInspectorContent(null);
 
-        svg.on('click', function () { deselectAll(true) });
+        self.svg.on('click', function () { self.deselectAll(true) });
 
         jQuery('form[name=ModifyLifecycle]').submit(function (e) {
             var config = lifecycle.exportAsConfiguration();
-            console.log(config);
             e.preventDefault();
             return false;
         });
 
-        refreshDisplay();
+        self.refreshDisplay();
     };
 
     RT.LifecycleViewer = Viewer;

commit 3f26d5081710f72c5a83fe37e3cc3a516826f8d2
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Fri Sep 1 14:01:24 2017 +0000

    initializeEditor -> initializeViewer

diff --git a/html/Elements/LifecycleGraph b/html/Elements/LifecycleGraph
index 9512693..b155c46 100644
--- a/html/Elements/LifecycleGraph
+++ b/html/Elements/LifecycleGraph
@@ -15,7 +15,7 @@
                 var viewer = new RT.LifecycleViewer();
                 var container = this;
                 var config = <% JSON($config) |n %>;
-                viewer.initializeEditor(container, config);
+                viewer.initializeViewer(container, config);
             });
         });
     </script>
diff --git a/static/js/lifecycleui-viewer.js b/static/js/lifecycleui-viewer.js
index 46ba344..d9049a4 100644
--- a/static/js/lifecycleui-viewer.js
+++ b/static/js/lifecycleui-viewer.js
@@ -322,7 +322,7 @@ jQuery(function () {
         this.refreshDecorations();
     };
 
-    Viewer.prototype.initializeEditor = function (node, config) {
+    Viewer.prototype.initializeViewer = function (node, config) {
         var self = this;
 
         self.container = jQuery(node);

commit 24099fc0b5f00e411cb4d9123803b76238b4c17a
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Fri Sep 1 14:25:05 2017 +0000

    Add separate JS for lifecycle editor

diff --git a/html/Elements/LifecycleGraph b/html/Elements/LifecycleGraph
index b155c46..73583b1 100644
--- a/html/Elements/LifecycleGraph
+++ b/html/Elements/LifecycleGraph
@@ -12,10 +12,15 @@
     <script type="text/javascript">
         jQuery(function () {
             jQuery(".lifecycle-ui#lifecycle-<% $id %>").each(function () {
-                var viewer = new RT.LifecycleViewer();
                 var container = this;
                 var config = <% JSON($config) |n %>;
+% if ($Editing) {
+                var editor = new RT.LifecycleEditor();
+                editor.initializeEditor(container, config);
+% } else {
+                var viewer = new RT.LifecycleViewer();
                 viewer.initializeViewer(container, config);
+% }
             });
         });
     </script>
diff --git a/lib/RT/Extension/LifecycleUI.pm b/lib/RT/Extension/LifecycleUI.pm
index 22f13a3..0ab2f48 100644
--- a/lib/RT/Extension/LifecycleUI.pm
+++ b/lib/RT/Extension/LifecycleUI.pm
@@ -9,6 +9,7 @@ RT->AddJavaScript("d3.min.js");
 RT->AddJavaScript("handlebars-4.0.6.min.js");
 RT->AddJavaScript("lifecycleui-model.js");
 RT->AddJavaScript("lifecycleui-viewer.js");
+RT->AddJavaScript("lifecycleui-editor.js");
 
 RT->AddStyleSheets("lifecycleui.css");
 RT->AddStyleSheets("lifecycleui-display.css");
diff --git a/static/css/lifecycleui-display.css b/static/css/lifecycleui-display.css
index 1622d19..95925ae 100644
--- a/static/css/lifecycleui-display.css
+++ b/static/css/lifecycleui-display.css
@@ -1,5 +1,7 @@
 .lifecycle-ui svg {
     border: 1px solid black;
+    width: 500px;
+    height: 309px;
 }
 
 .lifecycle-ui .statuses circle {
diff --git a/static/js/lifecycleui-editor.js b/static/js/lifecycleui-editor.js
new file mode 100644
index 0000000..a50ffdc
--- /dev/null
+++ b/static/js/lifecycleui-editor.js
@@ -0,0 +1,168 @@
+jQuery(function () {
+    var Super = RT.LifecycleViewer;
+
+    function Editor (container) {
+        Super.call(this);
+        this.padding = this.statusCircleRadius * 2;
+    };
+    Editor.prototype = Object.create(Super.prototype);
+
+    Editor.prototype._initializeTemplates = function (container) {
+        var self = this;
+
+        Handlebars.registerHelper('select', function(value, options) {
+            var node = jQuery('<select />').html( options.fn(this) );
+            node.find('[value="' + value + '"]').attr({'selected':'selected'});
+            return node.html();
+        });
+
+        Handlebars.registerHelper('canAddTransition', function(fromStatus, toStatus, lifecycle) {
+            return !lifecycle.hasTransition(fromStatus, toStatus);
+        });
+
+        Handlebars.registerHelper('canSelectTransition', function(fromStatus, toStatus, lifecycle) {
+            return lifecycle.hasTransition(fromStatus, toStatus);
+        });
+
+        var templates = {};
+        self.container.find('script.lifecycle-inspector-template').each(function () {
+            var type = jQuery(this).data('type');
+            var template = jQuery(this).html();
+            var fn = Handlebars.compile(template);
+            templates[type] = fn;
+        });
+        return templates;
+    };
+
+    Editor.prototype.setInspectorContent = function (node) {
+        var self = this;
+        var lifecycle = self.lifecycle;
+        var inspector = self.inspector;
+
+        var type = node ? node._type : 'canvas';
+
+        var params = { lifecycle: lifecycle };
+        params[type] = node;
+
+        inspector.html(self.templates[type](params));
+        inspector.find('sf-menu').supersubs().superfish({ dropShadows: false, speed: 'fast', delay: 0 }).supposition()
+
+        inspector.find(':input').change(function () {
+            var field = this.name;
+            var value = jQuery(this).val();
+            lifecycle.updateItem(node, field, value);
+            self.refreshDisplay();
+        });
+
+        inspector.find('button.change-color').click(function (e) {
+            e.preventDefault();
+            var picker = jQuery('<div class="color-picker"></div>');
+            jQuery(this).replaceWith(picker);
+
+            var skipUpdateCallback = 0;
+            var farb = jQuery.farbtastic(picker, function (newColor) {
+                if (skipUpdateCallback) {
+                    return;
+                }
+                inspector.find('.status-color').val(newColor);
+                lifecycle.updateItem(node, 'color', newColor);
+                self.refreshDisplay();
+            });
+            farb.setColor(node.color);
+
+            var input = jQuery('<input class="status-color" size=8 maxlength=7>');
+            inspector.find('.status-color').replaceWith(input);
+            input.on('input', function () {
+                var newColor = input.val();
+                if (newColor.match(/^#[a-fA-F0-9]{6}$/)) {
+                    skipUpdateCallback = 1;
+                    farb.setColor(newColor);
+                    skipUpdateCallback = 0;
+
+                    lifecycle.updateItem(node, 'color', newColor);
+                    self.refreshDisplay();
+                }
+            });
+            input.val(node.color);
+        });
+
+        inspector.find('button.delete').click(function (e) {
+            e.preventDefault();
+            lifecycle.deleteItemForKey(node._key);
+
+            self.deselectAll(true);
+            self.refreshDisplay();
+        });
+
+        inspector.find('a.add-transition').click(function (e) {
+            e.preventDefault();
+            var button = jQuery(this);
+            var fromStatus = button.data('from');
+            var toStatus   = button.data('to');
+
+            lifecycle.addTransition(fromStatus, toStatus);
+
+            button.closest('li').addClass('hidden');
+
+            inspector.find('a.select-transition[data-from="'+fromStatus+'"][data-to="'+toStatus+'"]').closest('li').removeClass('hidden');
+
+            self.refreshDisplay();
+            self.selectStatus(node.name);
+        });
+
+        inspector.find('a.select-status').on('click', function (e) {
+            e.preventDefault();
+            var statusName = jQuery(this).data('name');
+            self.selectStatus(statusName);
+        });
+
+        inspector.find('a.select-transition').on('click', function (e) {
+            e.preventDefault();
+            var button = jQuery(this);
+            var fromStatus = button.data('from');
+            var toStatus   = button.data('to');
+
+            self.selectTransition(fromStatus, toStatus);
+        });
+    };
+
+    Editor.prototype.deselectAll = function (clearSelection) {
+        Super.prototype.deselectAll.call(this);
+        if (clearSelection) {
+            this.setInspectorContent(null);
+        }
+    };
+
+    Editor.prototype.selectStatus = function (name) {
+        var d = Super.prototype.selectStatus.call(this, name);
+        this.setInspectorContent(d);
+    };
+
+    Editor.prototype.selectTransition = function (fromStatus, toStatus) {
+        var d = Super.prototype.selectTransition.call(this, fromStatus, toStatus);
+        this.setInspectorContent(d);
+    };
+
+    Editor.prototype.selectDecoration = function (key) {
+        var d = Super.prototype.selectDecoration.call(this, key);
+        this.setInspectorContent(d);
+    };
+
+    Editor.prototype.initializeEditor = function (node, config) {
+        var self = this;
+        self.initializeViewer(node, config);
+
+        self.templates = self._initializeTemplates(self.container);
+        self.inspector = self.container.find('.inspector');
+
+        self.setInspectorContent(null);
+
+        jQuery('form[name=ModifyLifecycle]').submit(function (e) {
+            var config = lifecycle.exportAsConfiguration();
+            e.preventDefault();
+            return false;
+        });
+    };
+
+    RT.LifecycleEditor = Editor;
+});
diff --git a/static/js/lifecycleui-viewer.js b/static/js/lifecycleui-viewer.js
index d9049a4..2256f50 100644
--- a/static/js/lifecycleui-viewer.js
+++ b/static/js/lifecycleui-viewer.js
@@ -1,33 +1,7 @@
 jQuery(function () {
     function Viewer (container) {
         this.statusCircleRadius = 35;
-    };
-
-    Viewer.prototype._initializeTemplates = function (container) {
-        var self = this;
-
-        Handlebars.registerHelper('select', function(value, options) {
-            var node = jQuery('<select />').html( options.fn(this) );
-            node.find('[value="' + value + '"]').attr({'selected':'selected'});
-            return node.html();
-        });
-
-        Handlebars.registerHelper('canAddTransition', function(fromStatus, toStatus, lifecycle) {
-            return !lifecycle.hasTransition(fromStatus, toStatus);
-        });
-
-        Handlebars.registerHelper('canSelectTransition', function(fromStatus, toStatus, lifecycle) {
-            return lifecycle.hasTransition(fromStatus, toStatus);
-        });
-
-        var templates = {};
-        self.container.find('script.lifecycle-inspector-template').each(function () {
-            var type = jQuery(this).data('type');
-            var template = jQuery(this).html();
-            var fn = Handlebars.compile(template);
-            templates[type] = fn;
-        });
-        return templates;
+        this.padding = this.statusCircleRadius;
     };
 
     Viewer.prototype.createArrowHead = function () {
@@ -52,103 +26,7 @@ jQuery(function () {
                  .range([padding, size - padding]);
     };
 
-    Viewer.prototype.setInspectorContent = function (node) {
-        var self = this;
-        var lifecycle = self.lifecycle;
-        var inspector = self.inspector;
-
-        var type = node ? node._type : 'canvas';
-
-        var params = { lifecycle: lifecycle };
-        params[type] = node;
-
-        inspector.html(self.templates[type](params));
-        inspector.find('sf-menu').supersubs().superfish({ dropShadows: false, speed: 'fast', delay: 0 }).supposition()
-
-        inspector.find(':input').change(function () {
-            var field = this.name;
-            var value = jQuery(this).val();
-            lifecycle.updateItem(node, field, value);
-            self.refreshDisplay();
-        });
-
-        inspector.find('button.change-color').click(function (e) {
-            e.preventDefault();
-            var picker = jQuery('<div class="color-picker"></div>');
-            jQuery(this).replaceWith(picker);
-
-            var skipUpdateCallback = 0;
-            var farb = jQuery.farbtastic(picker, function (newColor) {
-                if (skipUpdateCallback) {
-                    return;
-                }
-                inspector.find('.status-color').val(newColor);
-                lifecycle.updateItem(node, 'color', newColor);
-                self.refreshDisplay();
-            });
-            farb.setColor(node.color);
-
-            var input = jQuery('<input class="status-color" size=8 maxlength=7>');
-            inspector.find('.status-color').replaceWith(input);
-            input.on('input', function () {
-                var newColor = input.val();
-                if (newColor.match(/^#[a-fA-F0-9]{6}$/)) {
-                    skipUpdateCallback = 1;
-                    farb.setColor(newColor);
-                    skipUpdateCallback = 0;
-
-                    lifecycle.updateItem(node, 'color', newColor);
-                    self.refreshDisplay();
-                }
-            });
-            input.val(node.color);
-        });
-
-        inspector.find('button.delete').click(function (e) {
-            e.preventDefault();
-            lifecycle.deleteItemForKey(node._key);
-
-            self.deselectAll(true);
-            self.refreshDisplay();
-        });
-
-        inspector.find('a.add-transition').click(function (e) {
-            e.preventDefault();
-            var button = jQuery(this);
-            var fromStatus = button.data('from');
-            var toStatus   = button.data('to');
-
-            lifecycle.addTransition(fromStatus, toStatus);
-
-            button.closest('li').addClass('hidden');
-
-            inspector.find('a.select-transition[data-from="'+fromStatus+'"][data-to="'+toStatus+'"]').closest('li').removeClass('hidden');
-
-            self.refreshDisplay();
-            self.selectStatus(node.name);
-        });
-
-        inspector.find('a.select-status').on('click', function (e) {
-            e.preventDefault();
-            var statusName = jQuery(this).data('name');
-            self.selectStatus(statusName);
-        });
-
-        inspector.find('a.select-transition').on('click', function (e) {
-            e.preventDefault();
-            var button = jQuery(this);
-            var fromStatus = button.data('from');
-            var toStatus   = button.data('to');
-
-            self.selectTransition(fromStatus, toStatus);
-        });
-    };
-
-    Viewer.prototype.deselectAll = function (inspectCanvas) {
-        if (inspectCanvas) {
-            this.setInspectorContent(null);
-        }
-
+    Viewer.prototype.deselectAll = function (clearSelection) {
         var svg = this.svg;
         svg.classed('selection', false);
         svg.selectAll('.selected').classed('selected', false);
@@ -160,7 +38,6 @@ jQuery(function () {
     Viewer.prototype.selectStatus = function (name) {
         var self = this;
         var d = self.lifecycle.statusObjectForName(name);
-        self.setInspectorContent(d);
 
         self.deselectAll(false);
 
@@ -172,12 +49,13 @@ jQuery(function () {
             self.statusContainer.selectAll('*[data-key="'+key+'"]').classed('reachable', true);
             self.transitionContainer.selectAll('path[data-key="'+transition._key+'"]').classed('selected', true);
         });
+
+        return d;
     };
 
     Viewer.prototype.selectTransition = function (fromStatus, toStatus) {
         var self = this;
         var d = self.lifecycle.hasTransition(fromStatus, toStatus);
-        self.setInspectorContent(d);
 
         self.deselectAll(false);
 
@@ -188,16 +66,18 @@ jQuery(function () {
         self.statusContainer.selectAll('*[data-key="'+fromKey+'"]').classed('selected-source', true);
         self.statusContainer.selectAll('*[data-key="'+toKey+'"]').classed('selected-sink', true);
         self.transitionContainer.select('path[data-key="'+d._key+'"]').classed('selected', true);
+
+        return d;
     };
 
     Viewer.prototype.selectDecoration = function (key) {
         var d = this.lifecycle.itemForKey(key);
-        this.setInspectorContent(d);
 
         this.deselectAll(false);
 
         this.svg.classed('selection', true);
         this.decorationContainer.selectAll('*[data-key="'+key+'"]').classed('selected', true);
+        return d;
     };
 
     Viewer.prototype.refreshStatusNodes = function () {
@@ -327,8 +207,6 @@ jQuery(function () {
 
         self.container = jQuery(node);
         self.svg       = d3.select(node).select('svg');
-        self.templates = self._initializeTemplates(self.container);
-        self.inspector = self.container.find('.inspector');
 
         self.transitionContainer = self.svg.select('g.transitions');
         self.statusContainer     = self.svg.select('g.statuses');
@@ -337,24 +215,16 @@ jQuery(function () {
         self.width  = self.svg.node().getBoundingClientRect().width;
         self.height = self.svg.node().getBoundingClientRect().height;
 
-        self.xScale = self.createScale(self.width, self.statusCircleRadius * 2);
-        self.yScale = self.createScale(self.height, self.statusCircleRadius * 2);
+        self.xScale = self.createScale(self.width, self.padding);
+        self.yScale = self.createScale(self.height, self.padding);
 
         self.lifecycle = new RT.Lifecycle();
         self.lifecycle.initializeFromConfig(config);
 
         self.createArrowHead();
 
-        self.setInspectorContent(null);
-
         self.svg.on('click', function () { self.deselectAll(true) });
 
-        jQuery('form[name=ModifyLifecycle]').submit(function (e) {
-            var config = lifecycle.exportAsConfiguration();
-            e.preventDefault();
-            return false;
-        });
-
         self.refreshDisplay();
     };
 

commit cde92658e5b824012fc9334ab13e4aba2631315b
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Fri Sep 1 14:29:42 2017 +0000

    Move selection logic from viewer to editor

diff --git a/static/js/lifecycleui-editor.js b/static/js/lifecycleui-editor.js
index a50ffdc..9bfec45 100644
--- a/static/js/lifecycleui-editor.js
+++ b/static/js/lifecycleui-editor.js
@@ -127,27 +127,75 @@ jQuery(function () {
     };
 
     Editor.prototype.deselectAll = function (clearSelection) {
-        Super.prototype.deselectAll.call(this);
+        var svg = this.svg;
+        svg.classed('selection', false);
+        svg.selectAll('.selected').classed('selected', false);
+        svg.selectAll('.selected-source').classed('selected-source', false);
+        svg.selectAll('.selected-sink').classed('selected-sink', false);
+        svg.selectAll('.reachable').classed('reachable', false);
+
         if (clearSelection) {
             this.setInspectorContent(null);
         }
     };
 
     Editor.prototype.selectStatus = function (name) {
-        var d = Super.prototype.selectStatus.call(this, name);
-        this.setInspectorContent(d);
+        var self = this;
+        var d = self.lifecycle.statusObjectForName(name);
+
+        self.deselectAll(false);
+
+        self.svg.classed('selection', true);
+        self.statusContainer.selectAll('*[data-key="'+d._key+'"]').classed('selected', true);
+
+        jQuery.each(self.lifecycle.transitionsFrom(name), function (i, transition) {
+            var key = self.lifecycle.keyForStatusName(transition.to);
+            self.statusContainer.selectAll('*[data-key="'+key+'"]').classed('reachable', true);
+            self.transitionContainer.selectAll('path[data-key="'+transition._key+'"]').classed('selected', true);
+        });
+
+        self.setInspectorContent(d);
     };
 
     Editor.prototype.selectTransition = function (fromStatus, toStatus) {
-        var d = Super.prototype.selectTransition.call(this, fromStatus, toStatus);
-        this.setInspectorContent(d);
+        var self = this;
+        var d = self.lifecycle.hasTransition(fromStatus, toStatus);
+
+        self.deselectAll(false);
+
+        self.svg.classed('selection', true);
+
+        var fromKey = self.lifecycle.keyForStatusName(fromStatus);
+        var toKey = self.lifecycle.keyForStatusName(toStatus);
+        self.statusContainer.selectAll('*[data-key="'+fromKey+'"]').classed('selected-source', true);
+        self.statusContainer.selectAll('*[data-key="'+toKey+'"]').classed('selected-sink', true);
+        self.transitionContainer.select('path[data-key="'+d._key+'"]').classed('selected', true);
+
+        self.setInspectorContent(d);
     };
 
     Editor.prototype.selectDecoration = function (key) {
-        var d = Super.prototype.selectDecoration.call(this, key);
+        var d = this.lifecycle.itemForKey(key);
+
+        this.deselectAll(false);
+
+        this.svg.classed('selection', true);
+        this.decorationContainer.selectAll('*[data-key="'+key+'"]').classed('selected', true);
         this.setInspectorContent(d);
     };
 
+    Editor.prototype.clickedStatus = function (d) {
+        this.selectStatus(d.name);
+    };
+
+    Editor.prototype.clickedTransition = function (d) {
+        this.selectTransition(d.from, d.to);
+    };
+
+    Editor.prototype.clickedDecoration = function (d) {
+        this.selectDecoration(d._key);
+    };
+
     Editor.prototype.initializeEditor = function (node, config) {
         var self = this;
         self.initializeViewer(node, config);
@@ -162,6 +210,8 @@ jQuery(function () {
             e.preventDefault();
             return false;
         });
+
+        self.svg.on('click', function () { self.deselectAll(true) });
     };
 
     RT.LifecycleEditor = Editor;
diff --git a/static/js/lifecycleui-viewer.js b/static/js/lifecycleui-viewer.js
index 2256f50..ffa0a87 100644
--- a/static/js/lifecycleui-viewer.js
+++ b/static/js/lifecycleui-viewer.js
@@ -26,60 +26,6 @@ jQuery(function () {
                  .range([padding, size - padding]);
     };
 
-    Viewer.prototype.deselectAll = function (clearSelection) {
-        var svg = this.svg;
-        svg.classed('selection', false);
-        svg.selectAll('.selected').classed('selected', false);
-        svg.selectAll('.selected-source').classed('selected-source', false);
-        svg.selectAll('.selected-sink').classed('selected-sink', false);
-        svg.selectAll('.reachable').classed('reachable', false);
-    };
-
-    Viewer.prototype.selectStatus = function (name) {
-        var self = this;
-        var d = self.lifecycle.statusObjectForName(name);
-
-        self.deselectAll(false);
-
-        self.svg.classed('selection', true);
-        self.statusContainer.selectAll('*[data-key="'+d._key+'"]').classed('selected', true);
-
-        jQuery.each(self.lifecycle.transitionsFrom(name), function (i, transition) {
-            var key = self.lifecycle.keyForStatusName(transition.to);
-            self.statusContainer.selectAll('*[data-key="'+key+'"]').classed('reachable', true);
-            self.transitionContainer.selectAll('path[data-key="'+transition._key+'"]').classed('selected', true);
-        });
-
-        return d;
-    };
-
-    Viewer.prototype.selectTransition = function (fromStatus, toStatus) {
-        var self = this;
-        var d = self.lifecycle.hasTransition(fromStatus, toStatus);
-
-        self.deselectAll(false);
-
-        self.svg.classed('selection', true);
-
-        var fromKey = self.lifecycle.keyForStatusName(fromStatus);
-        var toKey = self.lifecycle.keyForStatusName(toStatus);
-        self.statusContainer.selectAll('*[data-key="'+fromKey+'"]').classed('selected-source', true);
-        self.statusContainer.selectAll('*[data-key="'+toKey+'"]').classed('selected-sink', true);
-        self.transitionContainer.select('path[data-key="'+d._key+'"]').classed('selected', true);
-
-        return d;
-    };
-
-    Viewer.prototype.selectDecoration = function (key) {
-        var d = this.lifecycle.itemForKey(key);
-
-        this.deselectAll(false);
-
-        this.svg.classed('selection', true);
-        this.decorationContainer.selectAll('*[data-key="'+key+'"]').classed('selected', true);
-        return d;
-    };
-
     Viewer.prototype.refreshStatusNodes = function () {
         var self = this;
         var statuses = self.statusContainer.selectAll("circle")
@@ -96,7 +42,7 @@ jQuery(function () {
                         .attr("data-key", function (d) { return d._key })
                         .on("click", function (d) {
                             d3.event.stopPropagation();
-                            self.selectStatus(d.name);
+                            self.clickedStatus(d);
                         })
                 .merge(statuses)
                         .attr("cx", function (d) { return self.xScale(d.x) })
@@ -104,6 +50,10 @@ jQuery(function () {
                         .attr("fill", function (d) { return d.color });
     };
 
+    Viewer.prototype.clickedStatus = function (d) { };
+    Viewer.prototype.clickedTransition = function (d) { };
+    Viewer.prototype.clickedDecoration = function (d) { };
+
     Viewer.prototype.truncateLabel = function (element) {
         var node = d3.select(element),
             textLength = node.node().getComputedTextLength(),
@@ -129,7 +79,7 @@ jQuery(function () {
                       .attr("data-key", function (d) { return d._key })
                       .on("click", function (d) {
                           d3.event.stopPropagation();
-                          self.selectStatus(d.name);
+                          self.clickedStatus(d);
                       })
               .merge(labels)
                       .attr("x", function (d) { return self.xScale(d.x) })
@@ -161,7 +111,7 @@ jQuery(function () {
                      .attr("data-key", function (d) { return d._key })
                      .on("click", function (d) {
                          d3.event.stopPropagation();
-                         self.selectTransition(d.from, d.to);
+                         self.clickedTransition(d);
                      })
               .merge(paths)
                       .attr("d", function (d) { return self.transitionArc(d) })
@@ -183,7 +133,7 @@ jQuery(function () {
                      .attr("data-key", function (d) { return d._key })
                      .on("click", function (d) {
                          d3.event.stopPropagation();
-                         self.selectDecoration(d._key);
+                         self.clickedDecoration(d);
                      })
               .merge(labels)
                       .attr("x", function (d) { return self.xScale(d.x) })
@@ -223,8 +173,6 @@ jQuery(function () {
 
         self.createArrowHead();
 
-        self.svg.on('click', function () { self.deselectAll(true) });
-
         self.refreshDisplay();
     };
 

commit 30d9a3883a8b860fc374db778f2e4ea6d50e4825
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Fri Sep 1 14:32:28 2017 +0000

    Rename css file for parity

diff --git a/lib/RT/Extension/LifecycleUI.pm b/lib/RT/Extension/LifecycleUI.pm
index 0ab2f48..1a169c7 100644
--- a/lib/RT/Extension/LifecycleUI.pm
+++ b/lib/RT/Extension/LifecycleUI.pm
@@ -12,7 +12,7 @@ RT->AddJavaScript("lifecycleui-viewer.js");
 RT->AddJavaScript("lifecycleui-editor.js");
 
 RT->AddStyleSheets("lifecycleui.css");
-RT->AddStyleSheets("lifecycleui-display.css");
+RT->AddStyleSheets("lifecycleui-viewer.css");
 RT->AddStyleSheets("lifecycleui-editor.css");
 
 $RT::Config::META{Lifecycles}{EditLink} = RT->Config->Get('WebURL') . 'Admin/Lifecycles/';
diff --git a/static/css/lifecycleui-display.css b/static/css/lifecycleui-viewer.css
similarity index 100%
rename from static/css/lifecycleui-display.css
rename to static/css/lifecycleui-viewer.css

commit 10b55a2f11a398c6cda6ced24d881c15bd68f478
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Fri Sep 1 14:33:16 2017 +0000

    Remove unused "state" css and textarea

diff --git a/html/Elements/LifecycleGraph b/html/Elements/LifecycleGraph
index 73583b1..da6ddfd 100644
--- a/html/Elements/LifecycleGraph
+++ b/html/Elements/LifecycleGraph
@@ -7,8 +7,6 @@
 % if ($Editing) {
     <& /Elements/LifecycleInspector &>
 % }
-    <textarea rows=24 cols=80 class="state"></textarea>
-
     <script type="text/javascript">
         jQuery(function () {
             jQuery(".lifecycle-ui#lifecycle-<% $id %>").each(function () {
diff --git a/static/css/lifecycleui-editor.css b/static/css/lifecycleui-editor.css
index 8d3b7cd..7039810 100644
--- a/static/css/lifecycleui-editor.css
+++ b/static/css/lifecycleui-editor.css
@@ -19,11 +19,6 @@
     border: 1px solid black;
 }
 
-.lifecycle-ui .state {
-    display: none;
-    font-family: Consolas, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, Bitstream Vera Sans Mono, Courier New, monospace;
-}
-
 .lifecycle-ui .statuses circle.selected {
     stroke-width: 6px;
 }

commit 3a25c2726e1b0c1d7a150fa16a5bc1a1b2ac0852
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Fri Sep 1 14:37:09 2017 +0000

    Better match viewer/editor in CSS organization

diff --git a/static/css/lifecycleui-editor.css b/static/css/lifecycleui-editor.css
index 7039810..e7ce572 100644
--- a/static/css/lifecycleui-editor.css
+++ b/static/css/lifecycleui-editor.css
@@ -3,6 +3,8 @@
     float: left;
     width: 809px;
     height: 500px;
+
+    /* checkerboard pattern */
     background: #F9F9F9 url('data:image/svg+xml,\
         <svg xmlns="http://www.w3.org/2000/svg" width="400" height="400"         fill-opacity=".05" >\
             <rect x="200" width="200" height="200" />\
@@ -19,6 +21,14 @@
     border: 1px solid black;
 }
 
+.lifecycle-ui .statuses circle,
+.lifecycle-ui .statuses text,
+.lifecycle-ui .transitions path,
+.lifecycle-ui .selection .decorations > *,
+.lifecycle-ui .removing {
+    transition: opacity .2s;
+}
+
 .lifecycle-ui .statuses circle.selected {
     stroke-width: 6px;
 }
@@ -31,6 +41,53 @@
 
 .lifecycle-ui .removing {
     opacity: 0 !important;
-    transition: opacity .2s;
 }
 
+.lifecycle-ui .selection .statuses circle,
+.lifecycle-ui .selection .statuses text {
+    opacity: 0.25;
+}
+
+.lifecycle-ui .selection .statuses circle.reachable,
+.lifecycle-ui .selection .statuses circle.selected,
+.lifecycle-ui .selection .statuses circle.selected-source,
+.lifecycle-ui .selection .statuses circle.selected-sink,
+.lifecycle-ui .selection .statuses text.reachable,
+.lifecycle-ui .selection .statuses text.selected,
+.lifecycle-ui .selection .statuses text.selected-source,
+.lifecycle-ui .selection .statuses text.selected-sink {
+    opacity: 1;
+}
+
+.lifecycle-ui .selection .transitions path {
+    opacity: 0;
+}
+
+.lifecycle-ui .selection .transitions path.selected {
+    opacity: 1;
+}
+
+.lifecycle-ui .selection .decorations > * {
+    opacity: .2;
+}
+
+.lifecycle-ui .selection .decorations > .selected {
+    opacity: 1;
+}
+
+.lifecycle-ui .inspector li.has-children {
+    background: transparent;
+}
+
+.lifecycle-ui .inspector a {
+    border: none;
+    color: #000;
+}
+
+.lifecycle-ui .inspector a.sf-with-ul {
+    padding-top: .3em;
+}
+
+.lifecycle-ui .inspector .sf-sub-indicator {
+    top: .2em;
+}
diff --git a/static/css/lifecycleui-viewer.css b/static/css/lifecycleui-viewer.css
index 95925ae..ff8f954 100644
--- a/static/css/lifecycleui-viewer.css
+++ b/static/css/lifecycleui-viewer.css
@@ -7,27 +7,6 @@
 .lifecycle-ui .statuses circle {
     stroke: black;
     stroke-width: 2px;
-    transition: opacity .2s;
-}
-
-.lifecycle-ui .statuses text {
-    transition: opacity .2s;
-}
-
-.lifecycle-ui .selection .statuses circle,
-.lifecycle-ui .selection .statuses text {
-    opacity: 0.25;
-}
-
-.lifecycle-ui .selection .statuses circle.reachable,
-.lifecycle-ui .selection .statuses circle.selected,
-.lifecycle-ui .selection .statuses circle.selected-source,
-.lifecycle-ui .selection .statuses circle.selected-sink,
-.lifecycle-ui .selection .statuses text.reachable,
-.lifecycle-ui .selection .statuses text.selected,
-.lifecycle-ui .selection .statuses text.selected-source,
-.lifecycle-ui .selection .statuses text.selected-sink {
-    opacity: 1;
 }
 
 .lifecycle-ui .transitions path {
@@ -36,25 +15,6 @@
     fill: none;
     opacity: .15;
     marker-end: url(#marker_arrowhead);
-
-    transition: opacity .2s;
-}
-
-.lifecycle-ui .selection .transitions path {
-    opacity: 0;
-}
-
-.lifecycle-ui .selection .transitions path.selected {
-    opacity: 1;
-}
-
-.lifecycle-ui .selection .decorations > * {
-    opacity: .2;
-    transition: opacity .2s;
-}
-
-.lifecycle-ui .selection .decorations > .selected {
-    opacity: 1;
 }
 
 .lifecycle-ui .transitions path.dotted {
@@ -81,19 +41,3 @@
     background: none;
 }
 
-.inspector li.has-children {
-    background: transparent;
-}
-
-.inspector a {
-    border: none;
-    color: #000;
-}
-
-.inspector a.sf-with-ul {
-    padding-top: .3em;
-}
-
-.inspector .sf-sub-indicator {
-    top: .2em;
-}

commit 9bcde8447e70ed2725d0192a42a4daeb348c7678
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Fri Sep 1 14:39:31 2017 +0000

    Rename status-color to current-color
    
    It may be used for other things. Unfortunately not for transitions,
    since the arrowhead marker cannot inherit the <path>'s stroke color

diff --git a/html/Elements/LifecycleInspectorStatus b/html/Elements/LifecycleInspectorStatus
index 20d8b19..10a127e 100644
--- a/html/Elements/LifecycleInspectorStatus
+++ b/html/Elements/LifecycleInspectorStatus
@@ -8,7 +8,7 @@
                   <option value="inactive"><&|/l&>inactive</&></option>
                   {{/select}}
               </select><br>
-        Color: <span class="status-color" title="{{status.color}}" style="background-color: {{status.color}}"> </span> <button class="change-color"><&|/l&>Change</&></button><br>
+        Color: <span class="current-color" title="{{status.color}}" style="background-color: {{status.color}}"> </span> <button class="change-color"><&|/l&>Change</&></button><br>
 
         Add Transition:
         <ul>
diff --git a/static/css/lifecycleui-editor.css b/static/css/lifecycleui-editor.css
index e7ce572..3d87f89 100644
--- a/static/css/lifecycleui-editor.css
+++ b/static/css/lifecycleui-editor.css
@@ -33,7 +33,7 @@
     stroke-width: 6px;
 }
 
-.lifecycle-ui .inspector .status span.status-color {
+.lifecycle-ui .inspector .status span.current-color {
     display: inline;
     padding-left: 1em;
     border: 1px solid black;
diff --git a/static/js/lifecycleui-editor.js b/static/js/lifecycleui-editor.js
index 9bfec45..bb53f9a 100644
--- a/static/js/lifecycleui-editor.js
+++ b/static/js/lifecycleui-editor.js
@@ -64,14 +64,14 @@ jQuery(function () {
                 if (skipUpdateCallback) {
                     return;
                 }
-                inspector.find('.status-color').val(newColor);
+                inspector.find('.current-color').val(newColor);
                 lifecycle.updateItem(node, 'color', newColor);
                 self.refreshDisplay();
             });
             farb.setColor(node.color);
 
-            var input = jQuery('<input class="status-color" size=8 maxlength=7>');
-            inspector.find('.status-color').replaceWith(input);
+            var input = jQuery('<input class="current-color" size=8 maxlength=7>');
+            inspector.find('.current-color').replaceWith(input);
             input.on('input', function () {
                 var newColor = input.val();
                 if (newColor.match(/^#[a-fA-F0-9]{6}$/)) {

commit 327167b25db4b972dab5deaf02c08fe441ea51f8
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Fri Sep 1 14:41:36 2017 +0000

    Fix regression of being able to add reflexive transition

diff --git a/static/js/lifecycleui-editor.js b/static/js/lifecycleui-editor.js
index bb53f9a..76f93dc 100644
--- a/static/js/lifecycleui-editor.js
+++ b/static/js/lifecycleui-editor.js
@@ -17,6 +17,9 @@ jQuery(function () {
         });
 
         Handlebars.registerHelper('canAddTransition', function(fromStatus, toStatus, lifecycle) {
+            if (fromStatus == toStatus) {
+                return false;
+            }
             return !lifecycle.hasTransition(fromStatus, toStatus);
         });
 

commit d4d3f7da5cb0ec457e5b9cd5f1d95a9c37f9f489
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Fri Sep 1 14:44:04 2017 +0000

    Rename refresh* to render*

diff --git a/static/js/lifecycleui-editor.js b/static/js/lifecycleui-editor.js
index 76f93dc..32faef2 100644
--- a/static/js/lifecycleui-editor.js
+++ b/static/js/lifecycleui-editor.js
@@ -54,7 +54,7 @@ jQuery(function () {
             var field = this.name;
             var value = jQuery(this).val();
             lifecycle.updateItem(node, field, value);
-            self.refreshDisplay();
+            self.renderDisplay();
         });
 
         inspector.find('button.change-color').click(function (e) {
@@ -69,7 +69,7 @@ jQuery(function () {
                 }
                 inspector.find('.current-color').val(newColor);
                 lifecycle.updateItem(node, 'color', newColor);
-                self.refreshDisplay();
+                self.renderDisplay();
             });
             farb.setColor(node.color);
 
@@ -83,7 +83,7 @@ jQuery(function () {
                     skipUpdateCallback = 0;
 
                     lifecycle.updateItem(node, 'color', newColor);
-                    self.refreshDisplay();
+                    self.renderDisplay();
                 }
             });
             input.val(node.color);
@@ -94,7 +94,7 @@ jQuery(function () {
             lifecycle.deleteItemForKey(node._key);
 
             self.deselectAll(true);
-            self.refreshDisplay();
+            self.renderDisplay();
         });
 
         inspector.find('a.add-transition').click(function (e) {
@@ -109,7 +109,7 @@ jQuery(function () {
 
             inspector.find('a.select-transition[data-from="'+fromStatus+'"][data-to="'+toStatus+'"]').closest('li').removeClass('hidden');
 
-            self.refreshDisplay();
+            self.renderDisplay();
             self.selectStatus(node.name);
         });
 
diff --git a/static/js/lifecycleui-viewer.js b/static/js/lifecycleui-viewer.js
index ffa0a87..1ec7df6 100644
--- a/static/js/lifecycleui-viewer.js
+++ b/static/js/lifecycleui-viewer.js
@@ -26,7 +26,7 @@ jQuery(function () {
                  .range([padding, size - padding]);
     };
 
-    Viewer.prototype.refreshStatusNodes = function () {
+    Viewer.prototype.renderStatusNodes = function () {
         var self = this;
         var statuses = self.statusContainer.selectAll("circle")
                                            .data(self.lifecycle.statusObjects(), function (d) { return d._key });
@@ -65,7 +65,7 @@ jQuery(function () {
         }
     };
 
-    Viewer.prototype.refreshStatusLabels = function () {
+    Viewer.prototype.renderStatusLabels = function () {
         var self = this;
         var labels = self.statusContainer.selectAll("text")
                                          .data(self.lifecycle.statusObjects(), function (d) { return d._key });
@@ -97,7 +97,7 @@ jQuery(function () {
       return "M" + this.xScale(from.x) + "," + this.yScale(from.y) + "A" + dr + "," + dr + " 0 0,1 " + this.xScale(to.x) + "," + this.yScale(to.y);
     };
 
-    Viewer.prototype.refreshTransitions = function () {
+    Viewer.prototype.renderTransitions = function () {
         var self = this;
         var paths = self.transitionContainer.selectAll("path")
                         .data(self.lifecycle.transitions, function (d) { return d._key });
@@ -119,7 +119,7 @@ jQuery(function () {
                       .classed("dotted", function (d) { return d.style == 'dotted' })
     };
 
-    Viewer.prototype.refreshTextDecorations = function () {
+    Viewer.prototype.renderTextDecorations = function () {
         var self = this;
         var labels = self.decorationContainer.selectAll("text")
                          .data(self.lifecycle.decorations.text, function (d) { return d._key });
@@ -141,15 +141,15 @@ jQuery(function () {
                       .text(function (d) { return d.text });
     };
 
-    Viewer.prototype.refreshDecorations = function () {
-        this.refreshTextDecorations();
+    Viewer.prototype.renderDecorations = function () {
+        this.renderTextDecorations();
     };
 
-    Viewer.prototype.refreshDisplay = function () {
-        this.refreshTransitions();
-        this.refreshStatusNodes();
-        this.refreshStatusLabels();
-        this.refreshDecorations();
+    Viewer.prototype.renderDisplay = function () {
+        this.renderTransitions();
+        this.renderStatusNodes();
+        this.renderStatusLabels();
+        this.renderDecorations();
     };
 
     Viewer.prototype.initializeViewer = function (node, config) {
@@ -173,7 +173,7 @@ jQuery(function () {
 
         self.createArrowHead();
 
-        self.refreshDisplay();
+        self.renderDisplay();
     };
 
     RT.LifecycleViewer = Viewer;

commit e73926ee05bfdff2b75635db2a7854074d059770
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Fri Sep 1 14:46:15 2017 +0000

    Improve form submission handler

diff --git a/static/js/lifecycleui-editor.js b/static/js/lifecycleui-editor.js
index 32faef2..9930c77 100644
--- a/static/js/lifecycleui-editor.js
+++ b/static/js/lifecycleui-editor.js
@@ -208,9 +208,10 @@ jQuery(function () {
 
         self.setInspectorContent(null);
 
-        jQuery('form[name=ModifyLifecycle]').submit(function (e) {
-            var config = lifecycle.exportAsConfiguration();
+        self.container.closest('form[name=ModifyLifecycle]').submit(function (e) {
             e.preventDefault();
+            var config = self.lifecycle.exportAsConfiguration();
+            console.log(config);
             return false;
         });
 

commit 69a229d1494642b8e28b992a676e0a724a2ddccd
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Fri Sep 1 14:47:23 2017 +0000

    Have render* methods return their d3 selections for subclassing

diff --git a/static/js/lifecycleui-viewer.js b/static/js/lifecycleui-viewer.js
index 1ec7df6..7dc34de 100644
--- a/static/js/lifecycleui-viewer.js
+++ b/static/js/lifecycleui-viewer.js
@@ -48,6 +48,8 @@ jQuery(function () {
                         .attr("cx", function (d) { return self.xScale(d.x) })
                         .attr("cy", function (d) { return self.yScale(d.y) })
                         .attr("fill", function (d) { return d.color });
+
+        return statuses;
     };
 
     Viewer.prototype.clickedStatus = function (d) { };
@@ -86,6 +88,8 @@ jQuery(function () {
                       .attr("y", function (d) { return self.yScale(d.y) })
                       .attr("fill", function (d) { return d3.hsl(d.color).l > 0.35 ? '#000' : '#fff' })
                       .text(function (d) { return d.name }).each(function () { self.truncateLabel(this) })
+
+        return labels;
     };
 
     Viewer.prototype.transitionArc = function (d) {
@@ -117,6 +121,8 @@ jQuery(function () {
                       .attr("d", function (d) { return self.transitionArc(d) })
                       .classed("dashed", function (d) { return d.style == 'dashed' })
                       .classed("dotted", function (d) { return d.style == 'dotted' })
+
+        return paths;
     };
 
     Viewer.prototype.renderTextDecorations = function () {
@@ -139,6 +145,8 @@ jQuery(function () {
                       .attr("x", function (d) { return self.xScale(d.x) })
                       .attr("y", function (d) { return self.yScale(d.y) })
                       .text(function (d) { return d.text });
+
+        return labels;
     };
 
     Viewer.prototype.renderDecorations = function () {

commit adf869758688789534b88cb29ff3134524649df3
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Fri Sep 1 14:57:29 2017 +0000

    Handle statuses available for initial creation
    
    This manages to the transitions => { "" => [...] } list

diff --git a/html/Elements/LifecycleInspectorStatus b/html/Elements/LifecycleInspectorStatus
index 10a127e..a70ee67 100644
--- a/html/Elements/LifecycleInspectorStatus
+++ b/html/Elements/LifecycleInspectorStatus
@@ -8,6 +8,7 @@
                   <option value="inactive"><&|/l&>inactive</&></option>
                   {{/select}}
               </select><br>
+        Creation: <input type="checkbox" name="creation" {{#if status.creation}}checked=checked{{/if}}><br>
         Color: <span class="current-color" title="{{status.color}}" style="background-color: {{status.color}}"> </span> <button class="change-color"><&|/l&>Change</&></button><br>
 
         Add Transition:
diff --git a/static/js/lifecycleui-editor.js b/static/js/lifecycleui-editor.js
index 9930c77..5392b74 100644
--- a/static/js/lifecycleui-editor.js
+++ b/static/js/lifecycleui-editor.js
@@ -52,7 +52,13 @@ jQuery(function () {
 
         inspector.find(':input').change(function () {
             var field = this.name;
-            var value = jQuery(this).val();
+            var value;
+            if (jQuery(this).is(':checkbox')) {
+                value = this.checked;
+            }
+            else {
+                value = jQuery(this).val();
+            }
             lifecycle.updateItem(node, field, value);
             self.renderDisplay();
         });
diff --git a/static/js/lifecycleui-model.js b/static/js/lifecycleui-model.js
index 3a02a65..5e8109f 100644
--- a/static/js/lifecycleui-model.js
+++ b/static/js/lifecycleui-model.js
@@ -55,6 +55,9 @@ jQuery(function () {
         if (config.transitions) {
             jQuery.each(config.transitions, function (fromStatus, toList) {
                 if (fromStatus == "") {
+                    jQuery.each(toList, function (i, toStatus) {
+                        self._statusMeta[toStatus].creation = true;
+                    });
                 }
                 else {
                     jQuery.each(toList, function (i, toStatus) {
@@ -99,12 +102,16 @@ jQuery(function () {
             transitions: self.transitions
         };
 
+        var transitions = { "": [] };
+
         jQuery.each(self.statuses, function (i, statusName) {
             var statusType = self._statusMeta[statusName].type;
             config[statusType].push(statusName);
+            if (self._statusMeta[statusName].creation) {
+                transitions[""].push(statusName);
+            }
         });
 
-        var transitions = {};
         jQuery.each(self.transitions, function (i, transition) {
             var from = transition.from;
             var to = transition.to;

commit e7d1d48568d25d7feff6d3cb1f285f30a71dcb64
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Fri Sep 1 15:09:06 2017 +0000

    Manage the permission required for a transition

diff --git a/html/Elements/LifecycleInspectorTransition b/html/Elements/LifecycleInspectorTransition
index a9ad438..8bbd9e1 100644
--- a/html/Elements/LifecycleInspectorTransition
+++ b/html/Elements/LifecycleInspectorTransition
@@ -2,6 +2,7 @@
     <div class="transition">
        From: <a href="#" class="select-status" data-name="{{transition.from}}">{{transition.from}}</a><br>
        To: <a href="#" class="select-status" data-name="{{transition.to}}">{{transition.to}}</a><br>
+       Right: <input type="text" name="right" value="{{transition.right}}"><br>
        Style: <select name="style">
                  {{#select transition.style}}
                  <option value="solid"><&|/l&>solid</&></option>
diff --git a/static/js/lifecycleui-model.js b/static/js/lifecycleui-model.js
index 5e8109f..16b0bd6 100644
--- a/static/js/lifecycleui-model.js
+++ b/static/js/lifecycleui-model.js
@@ -6,7 +6,6 @@ jQuery(function () {
         this.statuses = [];
         this.defaults = {};
         this.transitions = [];
-        this.rights = {};
         this.actions = [];
         this.decorations = {};
 
@@ -76,7 +75,19 @@ jQuery(function () {
         }
 
         if (config.rights) {
-            self.rights = config.rights;
+            jQuery.each(config.rights, function (description, right) {
+                jQuery.each(self.transitions, function (i, transition) {
+                    var from = transition.from;
+                    var to = transition.to;
+
+                    if (description == (from + ' -> ' + to)
+                     || description == ('* -> ' + to)
+                     || description == (from + ' -> *')
+                     || description == ('* -> *')) {
+                        transition.right = right;
+                    }
+                });
+            });
         }
 
         if (config.actions) {
@@ -98,7 +109,7 @@ jQuery(function () {
             inactive: [],
             defaults: self.defaults,
             actions: self.actions,
-            rights: self.rights,
+            rights: {},
             transitions: self.transitions
         };
 
@@ -119,9 +130,15 @@ jQuery(function () {
                 transitions[from] = [];
             }
             transitions[from].push(to);
-            config.transitions = transitions;
+
+            if (transition.right) {
+                var description = transition.from + ' -> ' + transition.to;
+                config.rights[description] = transition.right;
+            }
         });
 
+        config.transitions = transitions;
+
         return config;
     };
 
@@ -155,8 +172,6 @@ jQuery(function () {
                 transition.to = newValue;
             }
         });
-
-        // rights
     };
 
     Lifecycle.prototype.statusNameForKey = function (key) {
@@ -207,8 +222,6 @@ jQuery(function () {
             }
             return true;
         });
-
-        // rights
     };
 
     Lifecycle.prototype.addTransition = function (fromStatus, toStatus) {

commit c74b8ed568f1034dcfa9b6310345dce07cdbfb0e
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Fri Sep 1 15:36:01 2017 +0000

    Track, render, and update transition actions

diff --git a/html/Elements/LifecycleInspector b/html/Elements/LifecycleInspector
index 67e5e16..17b5127 100644
--- a/html/Elements/LifecycleInspector
+++ b/html/Elements/LifecycleInspector
@@ -6,3 +6,4 @@
 <& LifecycleInspectorTransition, %ARGS &>
 <& LifecycleInspectorText, %ARGS &>
 <& LifecycleInspectorShape, %ARGS &>
+<& LifecycleInspectorAction, %ARGS &>
diff --git a/html/Elements/LifecycleInspectorAction b/html/Elements/LifecycleInspectorAction
new file mode 100644
index 0000000..ec698bd
--- /dev/null
+++ b/html/Elements/LifecycleInspectorAction
@@ -0,0 +1,14 @@
+<script type="text/x-template" class="lifecycle-inspector-template" data-type="action">
+    <div class="action">
+        Label: <input type="text" name="label" value="{{action.label}}"></input><br>
+        Update: <select name="update">
+                  {{#select action.update}}
+                  <option value=""><&|/l&>quick</&></option>
+                  <option value="Comment"><&|/l&>comment</&></option>
+                  <option value="Respond"><&|/l&>respond</&></option>
+                  {{/select}}
+              </select><br>
+        <button class="delete"><&|/l&>Delete Action</&></button>
+    </div>
+</script>
+
diff --git a/html/Elements/LifecycleInspectorTransition b/html/Elements/LifecycleInspectorTransition
index 8bbd9e1..6beeb13 100644
--- a/html/Elements/LifecycleInspectorTransition
+++ b/html/Elements/LifecycleInspectorTransition
@@ -10,7 +10,12 @@
                  <option value="dotted"><&|/l&>dotted</&></option>
                  {{/select}}
              </select><br>
-    <br>
+
+        <ul class="actions">
+        {{#each transition.actions}}
+            <li class="action" data-index="{{@index}}">{{> lifecycleui_action action=this}}</li>
+        {{/each}}
+        </ul>
         <button class="delete"><&|/l&>Delete Transition</&></button>
     </div>
 </script>
diff --git a/static/css/lifecycleui-editor.css b/static/css/lifecycleui-editor.css
index 3d87f89..4cea326 100644
--- a/static/css/lifecycleui-editor.css
+++ b/static/css/lifecycleui-editor.css
@@ -91,3 +91,14 @@
 .lifecycle-ui .inspector .sf-sub-indicator {
     top: .2em;
 }
+
+.lifecycle-ui .inspector ul.actions {
+    list-style-type: none;
+    padding: 0;
+}
+
+.lifecycle-ui .inspector ul.actions li.action {
+    border: 1px solid black;
+    margin-bottom: .5em;
+    padding: .5em;
+}
diff --git a/static/js/lifecycleui-editor.js b/static/js/lifecycleui-editor.js
index 5392b74..ab7b789 100644
--- a/static/js/lifecycleui-editor.js
+++ b/static/js/lifecycleui-editor.js
@@ -33,6 +33,7 @@ jQuery(function () {
             var template = jQuery(this).html();
             var fn = Handlebars.compile(template);
             templates[type] = fn;
+            Handlebars.registerPartial('lifecycleui_' + type, fn);
         });
         return templates;
     };
@@ -59,7 +60,15 @@ jQuery(function () {
             else {
                 value = jQuery(this).val();
             }
-            lifecycle.updateItem(node, field, value);
+
+            var action = jQuery(this).closest('li.action');
+            if (action.length) {
+                var action = node.actions[action.data('index')];
+                lifecycle.updateItem(action, field, value);
+            }
+            else {
+                lifecycle.updateItem(node, field, value);
+            }
             self.renderDisplay();
         });
 
diff --git a/static/js/lifecycleui-model.js b/static/js/lifecycleui-model.js
index 16b0bd6..9500358 100644
--- a/static/js/lifecycleui-model.js
+++ b/static/js/lifecycleui-model.js
@@ -6,7 +6,6 @@ jQuery(function () {
         this.statuses = [];
         this.defaults = {};
         this.transitions = [];
-        this.actions = [];
         this.decorations = {};
 
         this._keyMap = {};
@@ -61,11 +60,12 @@ jQuery(function () {
                 else {
                     jQuery.each(toList, function (i, toStatus) {
                         var transition = {
-                            _key  : _ELEMENT_KEY_SEQ++,
-                            _type : 'transition',
-                            from  : fromStatus,
-                            to    : toStatus,
-                            style : 'solid'
+                            _key    : _ELEMENT_KEY_SEQ++,
+                            _type   : 'transition',
+                            from    : fromStatus,
+                            to      : toStatus,
+                            style   : 'solid',
+                            actions : []
                         };
                         self.transitions.push(transition);
                         self._keyMap[transition._key] = transition;
@@ -91,7 +91,25 @@ jQuery(function () {
         }
 
         if (config.actions) {
-            self.actions = config.actions;
+            for (var i = 0; i < config.actions.length; i += 2) {
+                var description = config.actions[i];
+                var action = config.actions[i+1];
+
+                jQuery.each(self.transitions, function (i, transition) {
+                    var from = transition.from;
+                    var to = transition.to;
+
+                    if (description == (from + ' -> ' + to)
+                     || description == ('* -> ' + to)
+                     || description == (from + ' -> *')
+                     || description == ('* -> *')) {
+                        action._key = _ELEMENT_KEY_SEQ++;
+                        action._type = 'action';
+                        transition.actions.push(action);
+                        self._keyMap[action._key] = action;
+                    }
+                });
+            }
         }
 
         if (config.decorations) {

commit 895e12f7c3c28be5c081ba32e3ff695ad0db2c45
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Fri Sep 1 15:36:54 2017 +0000

    Export actions

diff --git a/static/js/lifecycleui-model.js b/static/js/lifecycleui-model.js
index 9500358..bb3a62d 100644
--- a/static/js/lifecycleui-model.js
+++ b/static/js/lifecycleui-model.js
@@ -126,7 +126,7 @@ jQuery(function () {
             active: [],
             inactive: [],
             defaults: self.defaults,
-            actions: self.actions,
+            actions: [],
             rights: {},
             transitions: self.transitions
         };
@@ -144,15 +144,24 @@ jQuery(function () {
         jQuery.each(self.transitions, function (i, transition) {
             var from = transition.from;
             var to = transition.to;
+            var description = transition.from + ' -> ' + transition.to;
+
             if (!transitions[from]) {
                 transitions[from] = [];
             }
             transitions[from].push(to);
 
             if (transition.right) {
-                var description = transition.from + ' -> ' + transition.to;
                 config.rights[description] = transition.right;
             }
+
+            jQuery.each(transition.actions, function (i, action) {
+                var serialized = { label : action.label };
+                if (action.update) {
+                    serialized.update = action.update;
+                }
+                config.actions.push(description, serialized);
+            });
         });
 
         config.transitions = transitions;
@@ -179,8 +188,6 @@ jQuery(function () {
             }
         });
 
-        // actions
-
         // transitions
         jQuery.each(self.transitions, function (i, transition) {
             if (transition.from == oldValue) {
@@ -231,8 +238,6 @@ jQuery(function () {
             }
         });
 
-        // actions
-
         // transitions
         self.transitions = jQuery.grep(self.transitions, function (transition) {
             if (transition.from == statusName || transition.to == statusName) {

commit 808485c2b06bb62c1b4e9e165fa595e07fd48aee
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Fri Sep 1 15:42:12 2017 +0000

    Convert hash-based actions to array of pairs

diff --git a/static/js/lifecycleui-model.js b/static/js/lifecycleui-model.js
index bb3a62d..d4bef7e 100644
--- a/static/js/lifecycleui-model.js
+++ b/static/js/lifecycleui-model.js
@@ -91,9 +91,19 @@ jQuery(function () {
         }
 
         if (config.actions) {
-            for (var i = 0; i < config.actions.length; i += 2) {
-                var description = config.actions[i];
-                var action = config.actions[i+1];
+            var actions = config.actions;
+
+            // convert hash-based actions to array of pairs
+            if (jQuery.type(config.actions) == "object") {
+                actions = [];
+                jQuery.each(config.actions, function(description, action) {
+                    actions.push(description, action);
+                });
+            }
+
+            for (var i = 0; i < actions.length; i += 2) {
+                var description = actions[i];
+                var action = actions[i+1];
 
                 jQuery.each(self.transitions, function (i, transition) {
                     var from = transition.from;

commit 2992aac19a5db198f5aa688b1a5be967c70d779e
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Fri Sep 1 16:11:08 2017 +0000

    Convert array-of-objects-based rights to array of pairs

diff --git a/static/js/lifecycleui-model.js b/static/js/lifecycleui-model.js
index d4bef7e..a93db3c 100644
--- a/static/js/lifecycleui-model.js
+++ b/static/js/lifecycleui-model.js
@@ -101,9 +101,20 @@ jQuery(function () {
                 });
             }
 
-            for (var i = 0; i < actions.length; i += 2) {
-                var description = actions[i];
-                var action = actions[i+1];
+            for (var i = 0; i < actions.length; ++i) {
+                var description;
+                var action;
+
+                if (jQuery.type(actions[i]) == "string") {
+                    description = actions[i];
+                    action = actions[++i];
+                }
+                else {
+                    action = actions[i];
+                    var from = (delete action.from) || '*';
+                    var to = (delete action.to) || '*';
+                    description = from + ' -> ' + to;
+                }
 
                 jQuery.each(self.transitions, function (i, transition) {
                     var from = transition.from;

commit 03a2af892b0022eceb0f3b057890cd3300c39384
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Fri Sep 1 16:11:34 2017 +0000

    Implement default rights

diff --git a/static/js/lifecycleui-model.js b/static/js/lifecycleui-model.js
index a93db3c..b374ac6 100644
--- a/static/js/lifecycleui-model.js
+++ b/static/js/lifecycleui-model.js
@@ -90,6 +90,12 @@ jQuery(function () {
             });
         }
 
+        jQuery.each(self.transitions, function (i, transition) {
+            if (!transition.right) {
+                transition.right = self.defaultRightForTransition(transition);
+            }
+        });
+
         if (config.actions) {
             var actions = config.actions;
 
@@ -140,6 +146,14 @@ jQuery(function () {
         self.decorations.text = self.decorations.text || [];
     };
 
+    Lifecycle.prototype.defaultRightForTransition = function (transition) {
+        if (transition.to == 'deleted') {
+            return 'DeleteTicket';
+        }
+
+        return 'ModifyTicket';
+    };
+
     Lifecycle.prototype.exportAsConfiguration = function () {
         var self = this;
         var config = {

commit a4b2f06df65b5834f68e64b859a307334db1b738
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Fri Sep 1 17:37:41 2017 +0000

    Re-enabled disabled config

diff --git a/lib/RT/Extension/LifecycleUI.pm b/lib/RT/Extension/LifecycleUI.pm
index 1a169c7..a016a25 100644
--- a/lib/RT/Extension/LifecycleUI.pm
+++ b/lib/RT/Extension/LifecycleUI.pm
@@ -66,6 +66,11 @@ sub _CreateLifecycle {
     my $setting = RT::DatabaseSetting->new($CurrentUser);
     $setting->Load('Lifecycles');
     if ($setting->Id) {
+        if ($setting->Disabled) {
+            my ($ok, $msg) = $setting->SetDisabled(0);
+            return ($ok, $msg) if !$ok;
+        }
+
         my ($ok, $msg) = $setting->SetContent($lifecycles);
         return ($ok, $msg) if !$ok;
     }

commit 5b2da2bf178d1fdee66e41740eae74173e8227e7
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Fri Sep 1 17:49:57 2017 +0000

    Improvements to updating actions
    
    This fixes a bug where multiple action instances would get the same _key

diff --git a/html/Elements/LifecycleInspectorTransition b/html/Elements/LifecycleInspectorTransition
index 6beeb13..043c699 100644
--- a/html/Elements/LifecycleInspectorTransition
+++ b/html/Elements/LifecycleInspectorTransition
@@ -13,7 +13,7 @@
 
         <ul class="actions">
         {{#each transition.actions}}
-            <li class="action" data-index="{{@index}}">{{> lifecycleui_action action=this}}</li>
+            <li class="action" data-key="{{this._key}}">{{> lifecycleui_action action=this}}</li>
         {{/each}}
         </ul>
         <button class="delete"><&|/l&>Delete Transition</&></button>
diff --git a/static/js/lifecycleui-editor.js b/static/js/lifecycleui-editor.js
index ab7b789..caaefb5 100644
--- a/static/js/lifecycleui-editor.js
+++ b/static/js/lifecycleui-editor.js
@@ -63,7 +63,7 @@ jQuery(function () {
 
             var action = jQuery(this).closest('li.action');
             if (action.length) {
-                var action = node.actions[action.data('index')];
+                var action = lifecycle.itemForKey(action.data('key'));
                 lifecycle.updateItem(action, field, value);
             }
             else {
diff --git a/static/js/lifecycleui-model.js b/static/js/lifecycleui-model.js
index b374ac6..0033101 100644
--- a/static/js/lifecycleui-model.js
+++ b/static/js/lifecycleui-model.js
@@ -109,16 +109,16 @@ jQuery(function () {
 
             for (var i = 0; i < actions.length; ++i) {
                 var description;
-                var action;
+                var spec;
 
                 if (jQuery.type(actions[i]) == "string") {
                     description = actions[i];
-                    action = actions[++i];
+                    spec = actions[++i];
                 }
                 else {
-                    action = actions[i];
-                    var from = (delete action.from) || '*';
-                    var to = (delete action.to) || '*';
+                    spec = actions[i];
+                    var from = (delete spec.from) || '*';
+                    var to = (delete spec.to) || '*';
                     description = from + ' -> ' + to;
                 }
 
@@ -130,6 +130,7 @@ jQuery(function () {
                      || description == ('* -> ' + to)
                      || description == (from + ' -> *')
                      || description == ('* -> *')) {
+                        var action = jQuery.extend({}, spec);
                         action._key = _ELEMENT_KEY_SEQ++;
                         action._type = 'action';
                         transition.actions.push(action);

commit 31d708fd767bd8e2d5e01d5be7879139dc5a6803
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Fri Sep 1 17:50:16 2017 +0000

    Delete action

diff --git a/static/js/lifecycleui-editor.js b/static/js/lifecycleui-editor.js
index caaefb5..2001510 100644
--- a/static/js/lifecycleui-editor.js
+++ b/static/js/lifecycleui-editor.js
@@ -106,10 +106,17 @@ jQuery(function () {
 
         inspector.find('button.delete').click(function (e) {
             e.preventDefault();
-            lifecycle.deleteItemForKey(node._key);
 
-            self.deselectAll(true);
-            self.renderDisplay();
+            var action = jQuery(this).closest('li.action');
+            if (action.length) {
+                lifecycle.deleteActionForTransition(node, action.data('key'));
+                action.slideUp(200, function () { jQuery(this).remove() });
+            }
+            else {
+                lifecycle.deleteItemForKey(node._key);
+                self.deselectAll(true);
+                self.renderDisplay();
+            }
         });
 
         inspector.find('a.add-transition').click(function (e) {
diff --git a/static/js/lifecycleui-model.js b/static/js/lifecycleui-model.js
index 0033101..4dd6808 100644
--- a/static/js/lifecycleui-model.js
+++ b/static/js/lifecycleui-model.js
@@ -375,6 +375,16 @@ jQuery(function () {
         }
     };
 
+    Lifecycle.prototype.deleteActionForTransition = function (transition, key) {
+        transition.actions = jQuery.grep(transition.actions, function (action) {
+            if (action._key == key) {
+                return false;
+            }
+            return true;
+        });
+        delete this._keyMap[key];
+    };
+
     Lifecycle.prototype.updateItem = function (item, field, newValue) {
         var oldValue = item[field];
 

commit cd30ce1d1492ccfe6c3fb672ecb22ff7b7f5989e
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Fri Sep 1 17:58:49 2017 +0000

    Simplify markup for actions

diff --git a/html/Elements/LifecycleInspectorAction b/html/Elements/LifecycleInspectorAction
index ec698bd..a49ad8a 100644
--- a/html/Elements/LifecycleInspectorAction
+++ b/html/Elements/LifecycleInspectorAction
@@ -1,5 +1,5 @@
 <script type="text/x-template" class="lifecycle-inspector-template" data-type="action">
-    <div class="action">
+    <li class="action" data-key="{{action._key}}">
         Label: <input type="text" name="label" value="{{action.label}}"></input><br>
         Update: <select name="update">
                   {{#select action.update}}
@@ -9,6 +9,6 @@
                   {{/select}}
               </select><br>
         <button class="delete"><&|/l&>Delete Action</&></button>
-    </div>
+    </li>
 </script>
 
diff --git a/html/Elements/LifecycleInspectorTransition b/html/Elements/LifecycleInspectorTransition
index 043c699..4f73a04 100644
--- a/html/Elements/LifecycleInspectorTransition
+++ b/html/Elements/LifecycleInspectorTransition
@@ -13,7 +13,7 @@
 
         <ul class="actions">
         {{#each transition.actions}}
-            <li class="action" data-key="{{this._key}}">{{> lifecycleui_action action=this}}</li>
+            {{> lifecycleui_action action=this lifecycle=../lifecycle}}
         {{/each}}
         </ul>
         <button class="delete"><&|/l&>Delete Transition</&></button>
diff --git a/static/css/lifecycleui-editor.css b/static/css/lifecycleui-editor.css
index 4cea326..4a08ec3 100644
--- a/static/css/lifecycleui-editor.css
+++ b/static/css/lifecycleui-editor.css
@@ -92,12 +92,12 @@
     top: .2em;
 }
 
-.lifecycle-ui .inspector ul.actions {
+.lifecycle-ui .inspector .actions {
     list-style-type: none;
     padding: 0;
 }
 
-.lifecycle-ui .inspector ul.actions li.action {
+.lifecycle-ui .inspector .actions .action {
     border: 1px solid black;
     margin-bottom: .5em;
     padding: .5em;

commit 2d2767b1b888447a57274306b8fa73e86fb75f60
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Fri Sep 1 19:16:48 2017 +0000

    Switch to event delegation for inspector

diff --git a/static/js/lifecycleui-editor.js b/static/js/lifecycleui-editor.js
index 2001510..d26e8c4 100644
--- a/static/js/lifecycleui-editor.js
+++ b/static/js/lifecycleui-editor.js
@@ -42,6 +42,7 @@ jQuery(function () {
         var self = this;
         var lifecycle = self.lifecycle;
         var inspector = self.inspector;
+        self.inspectorNode = node;
 
         var type = node ? node._type : 'canvas';
 
@@ -50,8 +51,14 @@ jQuery(function () {
 
         inspector.html(self.templates[type](params));
         inspector.find('sf-menu').supersubs().superfish({ dropShadows: false, speed: 'fast', delay: 0 }).supposition()
+    };
+
+    Editor.prototype.bindInspectorEvents = function () {
+        var self = this;
+        var lifecycle = self.lifecycle;
+        var inspector = self.inspector;
 
-        inspector.find(':input').change(function () {
+        inspector.on('change', ':input', function () {
             var field = this.name;
             var value;
             if (jQuery(this).is(':checkbox')) {
@@ -67,12 +74,12 @@ jQuery(function () {
                 lifecycle.updateItem(action, field, value);
             }
             else {
-                lifecycle.updateItem(node, field, value);
+                lifecycle.updateItem(self.inspectorNode, field, value);
             }
             self.renderDisplay();
         });
 
-        inspector.find('button.change-color').click(function (e) {
+        inspector.on('click', 'button.change-color', function (e) {
             e.preventDefault();
             var picker = jQuery('<div class="color-picker"></div>');
             jQuery(this).replaceWith(picker);
@@ -83,10 +90,10 @@ jQuery(function () {
                     return;
                 }
                 inspector.find('.current-color').val(newColor);
-                lifecycle.updateItem(node, 'color', newColor);
+                lifecycle.updateItem(self.inspectorNode, 'color', newColor);
                 self.renderDisplay();
             });
-            farb.setColor(node.color);
+            farb.setColor(self.inspectorNode.color);
 
             var input = jQuery('<input class="current-color" size=8 maxlength=7>');
             inspector.find('.current-color').replaceWith(input);
@@ -97,29 +104,29 @@ jQuery(function () {
                     farb.setColor(newColor);
                     skipUpdateCallback = 0;
 
-                    lifecycle.updateItem(node, 'color', newColor);
+                    lifecycle.updateItem(self.inspectorNode, 'color', newColor);
                     self.renderDisplay();
                 }
             });
-            input.val(node.color);
+            input.val(self.inspectorNode.color);
         });
 
-        inspector.find('button.delete').click(function (e) {
+        inspector.on('click', 'button.delete', function (e) {
             e.preventDefault();
 
             var action = jQuery(this).closest('li.action');
             if (action.length) {
-                lifecycle.deleteActionForTransition(node, action.data('key'));
+                lifecycle.deleteActionForTransition(self.inspectorNode, action.data('key'));
                 action.slideUp(200, function () { jQuery(this).remove() });
             }
             else {
-                lifecycle.deleteItemForKey(node._key);
+                lifecycle.deleteItemForKey(self.inspectorNode._key);
                 self.deselectAll(true);
                 self.renderDisplay();
             }
         });
 
-        inspector.find('a.add-transition').click(function (e) {
+        inspector.on('click', 'a.add-transition', function (e) {
             e.preventDefault();
             var button = jQuery(this);
             var fromStatus = button.data('from');
@@ -132,16 +139,16 @@ jQuery(function () {
             inspector.find('a.select-transition[data-from="'+fromStatus+'"][data-to="'+toStatus+'"]').closest('li').removeClass('hidden');
 
             self.renderDisplay();
-            self.selectStatus(node.name);
+            self.selectStatus(self.inspectorNode.name);
         });
 
-        inspector.find('a.select-status').on('click', function (e) {
+        inspector.on('click', 'a.select-status', function (e) {
             e.preventDefault();
             var statusName = jQuery(this).data('name');
             self.selectStatus(statusName);
         });
 
-        inspector.find('a.select-transition').on('click', function (e) {
+        inspector.on('click', 'a.select-transition', function (e) {
             e.preventDefault();
             var button = jQuery(this);
             var fromStatus = button.data('from');
@@ -229,6 +236,7 @@ jQuery(function () {
         self.inspector = self.container.find('.inspector');
 
         self.setInspectorContent(null);
+        self.bindInspectorEvents();
 
         self.container.closest('form[name=ModifyLifecycle]').submit(function (e) {
             e.preventDefault();

commit ba6cb8fd833b7fc705e593b79c47435260a39304
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Fri Sep 1 19:17:36 2017 +0000

    Add action

diff --git a/html/Elements/LifecycleInspectorTransition b/html/Elements/LifecycleInspectorTransition
index 4f73a04..d2fac11 100644
--- a/html/Elements/LifecycleInspectorTransition
+++ b/html/Elements/LifecycleInspectorTransition
@@ -16,6 +16,7 @@
             {{> lifecycleui_action action=this lifecycle=../lifecycle}}
         {{/each}}
         </ul>
+        <button class="add-action"><&|/l&>Add Action</&></button><br>
         <button class="delete"><&|/l&>Delete Transition</&></button>
     </div>
 </script>
diff --git a/static/js/lifecycleui-editor.js b/static/js/lifecycleui-editor.js
index d26e8c4..f18d381 100644
--- a/static/js/lifecycleui-editor.js
+++ b/static/js/lifecycleui-editor.js
@@ -126,6 +126,17 @@ jQuery(function () {
             }
         });
 
+        inspector.on('click', 'button.add-action', function (e) {
+            e.preventDefault();
+            var action = lifecycle.createActionForTransition(self.inspectorNode);
+
+            var params = {action:action, lifecycle:lifecycle};
+            var html = self.templates.action(params);
+            jQuery(html).appendTo(inspector.find('ul.actions'))
+                        .hide()
+                        .slideDown(200);
+        });
+
         inspector.on('click', 'a.add-transition', function (e) {
             e.preventDefault();
             var button = jQuery(this);
diff --git a/static/js/lifecycleui-model.js b/static/js/lifecycleui-model.js
index 4dd6808..dfc59b5 100644
--- a/static/js/lifecycleui-model.js
+++ b/static/js/lifecycleui-model.js
@@ -395,6 +395,15 @@ jQuery(function () {
         }
     };
 
+    Lifecycle.prototype.createActionForTransition = function (transition) {
+        var action = {
+            _type : 'action',
+            _key  : _ELEMENT_KEY_SEQ++,
+        };
+        transition.actions.push(action);
+        this._keyMap[action._key] = action;
+        return action;
+    };
 
     RT.Lifecycle = Lifecycle;
 });

commit 8065bf22463b2d99bf3124dd2c34f7bd15a0ec2d
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Fri Sep 1 19:22:32 2017 +0000

    Don't export actions that have no label

diff --git a/static/js/lifecycleui-model.js b/static/js/lifecycleui-model.js
index dfc59b5..22601a4 100644
--- a/static/js/lifecycleui-model.js
+++ b/static/js/lifecycleui-model.js
@@ -192,11 +192,13 @@ jQuery(function () {
             }
 
             jQuery.each(transition.actions, function (i, action) {
-                var serialized = { label : action.label };
-                if (action.update) {
-                    serialized.update = action.update;
+                if (action.label) {
+                    var serialized = { label : action.label };
+                    if (action.update) {
+                        serialized.update = action.update;
+                    }
+                    config.actions.push(description, serialized);
                 }
-                config.actions.push(description, serialized);
             });
         });
 

commit 06417cb3a5aec1a59bb520a5467270480c71447f
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Fri Sep 1 19:24:34 2017 +0000

    Round-trip lifecycle type (eg asset)

diff --git a/static/js/lifecycleui-model.js b/static/js/lifecycleui-model.js
index 22601a4..aee7ecc 100644
--- a/static/js/lifecycleui-model.js
+++ b/static/js/lifecycleui-model.js
@@ -3,6 +3,7 @@ jQuery(function () {
     var defaultColors = d3.scaleOrdinal(d3.schemeCategory10);
 
     function Lifecycle () {
+        this.type = 'ticket';
         this.statuses = [];
         this.defaults = {};
         this.transitions = [];
@@ -15,6 +16,10 @@ jQuery(function () {
     Lifecycle.prototype.initializeFromConfig = function (config) {
         var self = this;
 
+        if (config.type) {
+            self.type = config.type;
+        }
+
         jQuery.each(['initial', 'active', 'inactive'], function (i, type) {
             if (config[type]) {
                 self.statuses = self.statuses.concat(config[type]);
@@ -167,6 +172,8 @@ jQuery(function () {
             transitions: self.transitions
         };
 
+        config.type = self.type;
+
         var transitions = { "": [] };
 
         jQuery.each(self.statuses, function (i, statusName) {

commit 8751b9490e00857666d1221a372f689e2bae5619
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Fri Sep 1 19:27:18 2017 +0000

    Add default right for new transitions

diff --git a/static/js/lifecycleui-model.js b/static/js/lifecycleui-model.js
index aee7ecc..8813091 100644
--- a/static/js/lifecycleui-model.js
+++ b/static/js/lifecycleui-model.js
@@ -302,6 +302,9 @@ jQuery(function () {
         };
         this.transitions.push(transition);
         this._keyMap[transition._key] = transition;
+
+        transition.right = this.defaultRightForTransition(transition);
+
         return transition;
     };
 

commit c233bf82aa878969fb2cdfd3892288d5fd6dcb22
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Fri Sep 1 19:27:26 2017 +0000

    Asset lifecycles use ModifyAsset permission

diff --git a/static/js/lifecycleui-model.js b/static/js/lifecycleui-model.js
index 8813091..6b8be77 100644
--- a/static/js/lifecycleui-model.js
+++ b/static/js/lifecycleui-model.js
@@ -153,6 +153,10 @@ jQuery(function () {
     };
 
     Lifecycle.prototype.defaultRightForTransition = function (transition) {
+        if (this.type == 'asset') {
+            return 'ModifyAsset';
+        }
+
         if (transition.to == 'deleted') {
             return 'DeleteTicket';
         }

commit 9a4e0e291e3b276e07a2e7207ce82c68d4016de0
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Fri Sep 1 20:27:03 2017 +0000

    Add pan and zoom

diff --git a/static/js/lifecycleui-viewer.js b/static/js/lifecycleui-viewer.js
index 7dc34de..4761478 100644
--- a/static/js/lifecycleui-viewer.js
+++ b/static/js/lifecycleui-viewer.js
@@ -26,6 +26,25 @@ jQuery(function () {
                  .range([padding, size - padding]);
     };
 
+    Viewer.prototype.addZoomBehavior = function () {
+        var self = this;
+        self._zoom = d3.zoom()
+                       .scaleExtent([.3, 2])
+                       .on("zoom", function () { self.didZoom() });
+        self.svg.call(self._zoom);
+    };
+
+    Viewer.prototype.didZoom = function () {
+        this.svg.selectAll("g").attr("transform", d3.event.transform);
+    };
+
+    Viewer.prototype.resetZoom = function () {
+        this.svg.selectAll("g")
+                .transition()
+                .duration(750)
+                .call(self._zoom.transform, d3.zoomIdentity);
+    };
+
     Viewer.prototype.renderStatusNodes = function () {
         var self = this;
         var statuses = self.statusContainer.selectAll("circle")
@@ -180,6 +199,7 @@ jQuery(function () {
         self.lifecycle.initializeFromConfig(config);
 
         self.createArrowHead();
+        self.addZoomBehavior();
 
         self.renderDisplay();
     };

commit 14aacdfb26388e17c7236f03100520ac0cf018de
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Fri Sep 1 20:30:08 2017 +0000

    Typo fix
    
    This was causing text truncation to silently fail

diff --git a/static/js/lifecycleui-viewer.js b/static/js/lifecycleui-viewer.js
index 4761478..6d1ea1d 100644
--- a/static/js/lifecycleui-viewer.js
+++ b/static/js/lifecycleui-viewer.js
@@ -79,7 +79,7 @@ jQuery(function () {
         var node = d3.select(element),
             textLength = node.node().getComputedTextLength(),
             text = node.text();
-        while (textLength > this.statusCircleRadiuds*1.8 && text.length > 0) {
+        while (textLength > this.statusCircleRadius*1.8 && text.length > 0) {
             text = text.slice(0, -1);
             node.text(text + '…');
             textLength = node.node().getComputedTextLength();

commit 89a290ed288f51ad9062cdea0f3b1ea38b72b299
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Fri Sep 1 20:37:22 2017 +0000

    Add "from" transitions

diff --git a/html/Elements/LifecycleInspectorStatus b/html/Elements/LifecycleInspectorStatus
index a70ee67..868324c 100644
--- a/html/Elements/LifecycleInspectorStatus
+++ b/html/Elements/LifecycleInspectorStatus
@@ -14,13 +14,16 @@
         Add Transition:
         <ul>
           {{#each lifecycle.statuses}}
-            <li class="{{#if (canAddTransition ../status.name this ../lifecycle)}}{{else}}hidden{{/if}}"><a href="#" class="add-transition" data-from="{{../status.name}}" data-to="{{this}}">{{this}}</a></li>
+            <li class="{{#if (canAddTransition ../status.name this ../lifecycle)}}{{else}}hidden{{/if}}"><a href="#" class="add-transition" data-from="{{../status.name}}" data-to="{{this}}"><&|/l, "{{this}}"&>to [_1]</&></a></li>
           {{/each}}
         </ul>
         Select Transition:
         <ul>
           {{#each lifecycle.statuses}}
-            <li class="{{#if (canSelectTransition ../status.name this ../lifecycle)}}{{else}}hidden{{/if}}"><a href="#" class="select-transition" data-from="{{../status.name}}" data-to="{{this}}">{{this}}</a></li>
+            <li class="{{#if (canSelectTransition ../status.name this ../lifecycle)}}{{else}}hidden{{/if}}"><a href="#" class="select-transition" data-from="{{../status.name}}" data-to="{{this}}"><&|/l, "{{this}}"&>to [_1]</&></a></li>
+          {{/each}}
+          {{#each lifecycle.statuses}}
+            <li class="{{#if (canSelectTransition this ../status.name ../lifecycle)}}{{else}}hidden{{/if}}"><a href="#" class="select-transition" data-to="{{../status.name}}" data-from="{{this}}"><&|/l, "{{this}}"&>from [_1]</&></a></li>
           {{/each}}
         </ul>
         <button class="delete"><&|/l&>Delete Status</&></button>

commit 0fef6365a64cd593199c9e62c236af591588c6eb
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Fri Sep 1 21:12:39 2017 +0000

    Switch from return to callback
    
    The d3 selection apparently doesn't gracefully handle calling .enter()
    multiple times, making it less useful for its intended purpose (adding
    drag behavior)

diff --git a/static/js/lifecycleui-viewer.js b/static/js/lifecycleui-viewer.js
index 6d1ea1d..829de9c 100644
--- a/static/js/lifecycleui-viewer.js
+++ b/static/js/lifecycleui-viewer.js
@@ -45,6 +45,11 @@ jQuery(function () {
                 .call(self._zoom.transform, d3.zoomIdentity);
     };
 
+    Viewer.prototype.didEnterStatusNodes = function (statuses) { };
+    Viewer.prototype.didEnterStatusLabels = function (labels) { };
+    Viewer.prototype.didEnterTransitions = function (paths) { };
+    Viewer.prototype.didEnterTextDecorations = function (labels) { };
+
     Viewer.prototype.renderStatusNodes = function () {
         var self = this;
         var statuses = self.statusContainer.selectAll("circle")
@@ -63,12 +68,11 @@ jQuery(function () {
                             d3.event.stopPropagation();
                             self.clickedStatus(d);
                         })
+                        .call(function (statuses) { self.didEnterStatusNodes(statuses) })
                 .merge(statuses)
                         .attr("cx", function (d) { return self.xScale(d.x) })
                         .attr("cy", function (d) { return self.yScale(d.y) })
                         .attr("fill", function (d) { return d.color });
-
-        return statuses;
     };
 
     Viewer.prototype.clickedStatus = function (d) { };
@@ -102,13 +106,12 @@ jQuery(function () {
                           d3.event.stopPropagation();
                           self.clickedStatus(d);
                       })
+                     .call(function (labels) { self.didEnterStatusLabels(labels) })
               .merge(labels)
                       .attr("x", function (d) { return self.xScale(d.x) })
                       .attr("y", function (d) { return self.yScale(d.y) })
                       .attr("fill", function (d) { return d3.hsl(d.color).l > 0.35 ? '#000' : '#fff' })
                       .text(function (d) { return d.name }).each(function () { self.truncateLabel(this) })
-
-        return labels;
     };
 
     Viewer.prototype.transitionArc = function (d) {
@@ -136,12 +139,11 @@ jQuery(function () {
                          d3.event.stopPropagation();
                          self.clickedTransition(d);
                      })
+                     .call(function (paths) { self.didEnterTransitions(paths) })
               .merge(paths)
                       .attr("d", function (d) { return self.transitionArc(d) })
                       .classed("dashed", function (d) { return d.style == 'dashed' })
                       .classed("dotted", function (d) { return d.style == 'dotted' })
-
-        return paths;
     };
 
     Viewer.prototype.renderTextDecorations = function () {
@@ -160,12 +162,11 @@ jQuery(function () {
                          d3.event.stopPropagation();
                          self.clickedDecoration(d);
                      })
+                     .call(function (labels) { self.didEnterTextDecorations(labels) })
               .merge(labels)
                       .attr("x", function (d) { return self.xScale(d.x) })
                       .attr("y", function (d) { return self.yScale(d.y) })
                       .text(function (d) { return d.text });
-
-        return labels;
     };
 
     Viewer.prototype.renderDecorations = function () {

commit a7a74853481197c88dffb832c017f37fd4b5e8f2
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Fri Sep 1 21:13:35 2017 +0000

    Implement moving statuses with drag and drop

diff --git a/static/js/lifecycleui-editor.js b/static/js/lifecycleui-editor.js
index f18d381..84be644 100644
--- a/static/js/lifecycleui-editor.js
+++ b/static/js/lifecycleui-editor.js
@@ -239,6 +239,26 @@ jQuery(function () {
         this.selectDecoration(d._key);
     };
 
+    Editor.prototype.didDragItem = function (d, node) {
+        this.lifecycle.moveItem(d, this.xScale.invert(d3.event.x), this.yScale.invert(d3.event.y));
+        this.renderDisplay();
+    };
+
+    Editor.prototype._createDrag = function () {
+        var self = this;
+        return d3.drag()
+                 .subject(function (d) { return { x: self.xScale(d.x), y : self.yScale(d.y) } })
+                 .on("drag", function (d) { self.didDragItem(d, this) });
+    };
+
+    Editor.prototype.didEnterStatusNodes = function (statuses) {
+        statuses.call(this._createDrag());
+    };
+
+    Editor.prototype.didEnterStatusLabels = function (statuses) {
+        statuses.call(this._createDrag());
+    };
+
     Editor.prototype.initializeEditor = function (node, config) {
         var self = this;
         self.initializeViewer(node, config);
diff --git a/static/js/lifecycleui-model.js b/static/js/lifecycleui-model.js
index 6b8be77..e2de2ee 100644
--- a/static/js/lifecycleui-model.js
+++ b/static/js/lifecycleui-model.js
@@ -421,6 +421,11 @@ jQuery(function () {
         return action;
     };
 
+    Lifecycle.prototype.moveItem = function (item, x, y) {
+        item.x = x;
+        item.y = y;
+    };
+
     RT.Lifecycle = Lifecycle;
 });
 

commit 9ce2cc6d0513672a0ae05074aea0bfa29d068f6d
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Fri Sep 1 21:28:05 2017 +0000

    Drag and drop for text decorations

diff --git a/static/js/lifecycleui-editor.js b/static/js/lifecycleui-editor.js
index 84be644..70934c2 100644
--- a/static/js/lifecycleui-editor.js
+++ b/static/js/lifecycleui-editor.js
@@ -259,6 +259,10 @@ jQuery(function () {
         statuses.call(this._createDrag());
     };
 
+    Editor.prototype.didEnterTextDecorations = function (labels) {
+        labels.call(this._createDrag());
+    };
+
     Editor.prototype.initializeEditor = function (node, config) {
         var self = this;
         self.initializeViewer(node, config);

-----------------------------------------------------------------------


More information about the Bps-public-commit mailing list