[Bps-public-commit] rt-extension-lifecycleui branch, master, updated. 490515b2479484b621d958d9ac7380fc50a39105

Shawn Moore shawn at bestpractical.com
Thu Aug 31 17:30:47 EDT 2017


The branch, master has been updated
       via  490515b2479484b621d958d9ac7380fc50a39105 (commit)
       via  1d295fee98bf1538ccf58e1bf53010df090ff69c (commit)
       via  12e9ecdf9c467f59c3e7dc6c3f610e90c3af5e65 (commit)
       via  c9e46a4a229f5c05f26199f8b084babbe3220255 (commit)
       via  dccd5d3312a00768fb9e80529e04ca568a6890dc (commit)
       via  646404cdf4c3b42f66a3077c9685a3759e1c64be (commit)
       via  7d959ecf20b06a27a1b9dad7a2fc682110bfc8d1 (commit)
       via  cd286cdbd7d65fc1de16b81a027a2aaf7f7345a4 (commit)
       via  880a6cb3557e23f60bf149f2349b3f2cfaf0dead (commit)
       via  634abeddf05348fce04ede710f2205c699995b6b (commit)
       via  3f171e367f7148d8ffa0fd4f7871096553d6c604 (commit)
       via  2e5960030115737cc7e5f3753ac51f8cec856c3b (commit)
       via  f5af8d9ecb57dfe0d46d7bf2852827b2f16bfd28 (commit)
       via  7191bfcce809782127d01a2ffe108786169a9d7f (commit)
       via  1d84fcd9987a26e5c2809c1b79b2341fad1662da (commit)
       via  96e771728d7db8e957f7e725882df6d365168a42 (commit)
       via  150b2987415d9cc8e3f63617c37ce06c566e170d (commit)
       via  f0d92beba30c8251962bdeec24505532a6edf15b (commit)
       via  468de0011277fa6f0dae23516ff025dd1c90e731 (commit)
       via  168621703b9fdcd325a6cd6e4ee941c5cf049936 (commit)
       via  904872de2db6fa0750a875fd8dd389735a9dca90 (commit)
       via  6064b171c0577e79d6babb84b39f64c9fc24d6c0 (commit)
       via  c312784ff6ddb5030c34372f14d817ad6ff989e7 (commit)
       via  341b9ffe202e9390f268cefb9d51a5cb522d32ea (commit)
       via  7a039501e021e9e7fb2e17a566a4eeda0869d85a (commit)
       via  b2a9af3ff3d772ae0eb10d926070f1d4998ebb37 (commit)
      from  9bda424c2f34e273a37391f9c799bee99409bc5c (commit)

Summary of changes:
 html/Elements/LifecycleGraph           |  19 +-
 html/Elements/LifecycleInspectorCanvas |   2 +-
 html/Elements/LifecycleInspectorStatus |   8 +-
 html/Elements/LifecycleInspectorText   |   4 +-
 lib/RT/Extension/LifecycleUI.pm        |   3 +-
 static/js/lifecycleui-editor.js        | 584 ---------------------------------
 static/js/lifecycleui-model.js         | 312 ++++++++++++++++++
 static/js/lifecycleui-viewer.js        | 355 ++++++++++++++++++++
 8 files changed, 690 insertions(+), 597 deletions(-)
 delete mode 100644 static/js/lifecycleui-editor.js
 create mode 100644 static/js/lifecycleui-model.js
 create mode 100644 static/js/lifecycleui-viewer.js

- Log -----------------------------------------------------------------
commit b2a9af3ff3d772ae0eb10d926070f1d4998ebb37
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Thu Aug 31 15:16:00 2017 +0000

    Implement deleting transitions

diff --git a/static/js/lifecycleui-editor.js b/static/js/lifecycleui-editor.js
index e8d7bdb..71ae19a 100644
--- a/static/js/lifecycleui-editor.js
+++ b/static/js/lifecycleui-editor.js
@@ -110,6 +110,15 @@ jQuery(function () {
         // rights
     };
 
+    var deleteTransition = function (state, key) {
+        state.transitions = jQuery.grep(state.transitions, function (transition) {
+            if (transition._key == key) {
+                return false;
+            }
+            return true;
+        });
+    };
+
     var createArrowHead = function (svg) {
         var defs = svg.append('defs');
         defs.append('marker')
@@ -328,6 +337,9 @@ jQuery(function () {
                 if (type == 'status') {
                     deleteStatus(state, node.name);
                 }
+                else if (type == 'transition') {
+                    deleteTransition(state, node._key);
+                }
 
                 deselectAll(true);
                 refreshDisplay();

commit 7a039501e021e9e7fb2e17a566a4eeda0869d85a
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Thu Aug 31 15:17:25 2017 +0000

    Implement deleting text

diff --git a/html/Elements/LifecycleInspectorText b/html/Elements/LifecycleInspectorText
index 7693b14..7eefb8a 100644
--- a/html/Elements/LifecycleInspectorText
+++ b/html/Elements/LifecycleInspectorText
@@ -1,6 +1,6 @@
 <script type="text/x-template" class="lifecycle-inspector-template" data-type="text">
     <div class="text">
-        Body: <input type="text" name="text" value="{{text.text}}" />
-        Delete Text<br>
+        Body: <input type="text" name="text" value="{{text.text}}" /><br>
+        <button class="delete"><&|/l&>Delete Text</&></button>
     </div>
 </script>
diff --git a/static/js/lifecycleui-editor.js b/static/js/lifecycleui-editor.js
index 71ae19a..fa70bb9 100644
--- a/static/js/lifecycleui-editor.js
+++ b/static/js/lifecycleui-editor.js
@@ -119,6 +119,15 @@ jQuery(function () {
         });
     };
 
+    var deleteText = function (state, key) {
+        state.decorations.text = jQuery.grep(state.decorations.text, function (decoration) {
+            if (decoration._key == key) {
+                return false;
+            }
+            return true;
+        });
+    };
+
     var createArrowHead = function (svg) {
         var defs = svg.append('defs');
         defs.append('marker')
@@ -340,6 +349,9 @@ jQuery(function () {
                 else if (type == 'transition') {
                     deleteTransition(state, node._key);
                 }
+                else if (type == 'text') {
+                    deleteText(state, node._key);
+                }
 
                 deselectAll(true);
                 refreshDisplay();

commit 341b9ffe202e9390f268cefb9d51a5cb522d32ea
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Thu Aug 31 15:30:43 2017 +0000

    Use _key to select DOM elements
    
    Status name could potentially contain nasties

diff --git a/static/js/lifecycleui-editor.js b/static/js/lifecycleui-editor.js
index fa70bb9..84d878d 100644
--- a/static/js/lifecycleui-editor.js
+++ b/static/js/lifecycleui-editor.js
@@ -413,12 +413,13 @@ jQuery(function () {
             deselectAll(false);
 
             svg.classed('selection', true);
-            statusContainer.selectAll('circle[data-name="'+name+'"], text[data-name="'+name+'"]').classed('selected', true);
-            transitionContainer.selectAll('path[data-from="'+name+'"]').classed('selected', true);
+            statusContainer.selectAll('*[data-key="'+d._key+'"]').classed('selected', true);
 
             jQuery.each(state.transitions, function (i, transition) {
                 if (transition.from == name) {
-                    statusContainer.selectAll('circle[data-name="'+transition.to+'"], text[data-name="'+transition.to+'"]').classed('reachable', true);
+                    var key = state.statusMeta[transition.to]._key;
+                    statusContainer.selectAll('*[data-key="'+key+'"]').classed('reachable', true);
+                    transitionContainer.selectAll('path[data-key="'+transition._key+'"]').classed('selected', true);
                 }
             });
         };
@@ -435,9 +436,12 @@ jQuery(function () {
             deselectAll(false);
 
             svg.classed('selection', true);
-            statusContainer.selectAll('circle[data-name="'+fromStatus+'"], text[data-name="'+fromStatus+'"]').classed('selected-source', true);
-            statusContainer.selectAll('circle[data-name="'+toStatus+'"], text[data-name="'+toStatus+'"]').classed('selected-sink', true);
-            transitionContainer.select('path[data-from="'+fromStatus+'"][data-to="'+toStatus+'"]').classed('selected', true);
+
+            var fromKey = state.statusMeta[fromStatus]._key;
+            var toKey = state.statusMeta[toStatus]._key;
+            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 (type, key) {
@@ -481,8 +485,7 @@ jQuery(function () {
                     .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 })
-                            .attr("data-name", function (d) { return d.name })
+                            .attr("fill", function (d) { return d.color });
         };
 
         function truncateLabel () {
@@ -515,7 +518,6 @@ jQuery(function () {
                           .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' })
-                          .attr("data-name", function (d) { return d.name })
                           .text(function (d) { return d.name }).each(truncateLabel)
         };
 
@@ -545,8 +547,6 @@ jQuery(function () {
                          })
                   .merge(paths)
                           .attr("d", linkArc)
-                          .attr("data-from", function (d) { return d.from })
-                          .attr("data-to", function (d) { return d.to })
                           .classed("dashed", function (d) { return d.style == 'dashed' })
                           .classed("dotted", function (d) { return d.style == 'dotted' })
         };

commit c312784ff6ddb5030c34372f14d817ad6ff989e7
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Thu Aug 31 15:34:15 2017 +0000

    Make inspector events more consistent

diff --git a/static/js/lifecycleui-editor.js b/static/js/lifecycleui-editor.js
index 84d878d..53fc8b6 100644
--- a/static/js/lifecycleui-editor.js
+++ b/static/js/lifecycleui-editor.js
@@ -378,20 +378,6 @@ jQuery(function () {
                 refreshDisplay();
                 selectStatus(node.name);
             });
-
-            inspector.find('a.select-status').click(function (e) {
-                e.preventDefault();
-                selectStatus(jQuery(this).data('name'));
-            });
-
-            inspector.find('a.select-transition').click(function (e) {
-                e.preventDefault();
-                var button = jQuery(this);
-                var fromStatus = button.data('from');
-                var toStatus   = button.data('to');
-
-                selectTransition(fromStatus, toStatus);
-            });
         };
 
         var deselectAll = function (inspectCanvas) {
@@ -583,12 +569,21 @@ jQuery(function () {
             refreshDecorations();
         };
 
-        jQuery('.inspector').on('click', 'a.select-status', function (e) {
+        inspector.on('click', 'a.select-status', function (e) {
             e.preventDefault();
             var statusName = jQuery(this).data('name');
             selectStatus(statusName);
         });
 
+        inspector.on('click', 'a.select-transition', function (e) {
+            e.preventDefault();
+            var button = jQuery(this);
+            var fromStatus = button.data('from');
+            var toStatus   = button.data('to');
+
+            selectTransition(fromStatus, toStatus);
+        });
+
         setInspectorContent('canvas');
 
         svg.on('click', function () { deselectAll(true) });

commit 6064b171c0577e79d6babb84b39f64c9fc24d6c0
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Thu Aug 31 15:55:43 2017 +0000

    Begin a lifecycle-model.js

diff --git a/html/Elements/LifecycleInspectorCanvas b/html/Elements/LifecycleInspectorCanvas
index 9746552..82bf634 100644
--- a/html/Elements/LifecycleInspectorCanvas
+++ b/html/Elements/LifecycleInspectorCanvas
@@ -7,7 +7,7 @@
         <ul class="toplevel sf-menu sf-vertical sf-js-enabled sf-shadow">
           <li class="has-children">Select Status
               <ul>
-              {{#each state.statuses}}
+              {{#each lifecycle.statuses}}
               <li><a href="#" class="select-status menu-item" data-name="{{this}}">{{this}}</a></li>
               {{/each}}
               </ul>
diff --git a/html/Elements/LifecycleInspectorStatus b/html/Elements/LifecycleInspectorStatus
index 3b6669c..20d8b19 100644
--- a/html/Elements/LifecycleInspectorStatus
+++ b/html/Elements/LifecycleInspectorStatus
@@ -12,14 +12,14 @@
 
         Add Transition:
         <ul>
-          {{#each state.statuses}}
-            <li class="{{#if (canAddTransition ../status.name this ../state)}}{{else}}hidden{{/if}}"><a href="#" class="add-transition" data-from="{{../status.name}}" data-to="{{this}}">{{this}}</a></li>
+          {{#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>
           {{/each}}
         </ul>
         Select Transition:
         <ul>
-          {{#each state.statuses}}
-            <li class="{{#if (canSelectTransition ../status.name this ../state)}}{{else}}hidden{{/if}}"><a href="#" class="select-transition" data-from="{{../status.name}}" data-to="{{this}}">{{this}}</a></li>
+          {{#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>
           {{/each}}
         </ul>
         <button class="delete"><&|/l&>Delete Status</&></button>
diff --git a/lib/RT/Extension/LifecycleUI.pm b/lib/RT/Extension/LifecycleUI.pm
index a36dba1..b7bec9d 100644
--- a/lib/RT/Extension/LifecycleUI.pm
+++ b/lib/RT/Extension/LifecycleUI.pm
@@ -7,6 +7,7 @@ our $VERSION = '0.01';
 
 RT->AddJavaScript("d3.min.js");
 RT->AddJavaScript("handlebars-4.0.6.min.js");
+RT->AddJavaScript("lifecycleui-model.js");
 RT->AddJavaScript("lifecycleui-editor.js");
 
 RT->AddStyleSheets("lifecycleui.css");
diff --git a/static/js/lifecycleui-editor.js b/static/js/lifecycleui-editor.js
index 53fc8b6..8e7a09d 100644
--- a/static/js/lifecycleui-editor.js
+++ b/static/js/lifecycleui-editor.js
@@ -1,10 +1,6 @@
 jQuery(function () {
     var STATUS_CIRCLE_RADIUS = 35;
 
-    var _ELEMENT_KEY_SEQ = 0;
-
-    var defaultColors = d3.scaleOrdinal(d3.schemeCategory10);
-
     var templates = {};
 
     jQuery('script.lifecycle-inspector-template').each(function () {
@@ -20,13 +16,13 @@ jQuery(function () {
         return node.html();
     });
 
-    Handlebars.registerHelper('canAddTransition', function(fromStatus, toStatus, state) {
+    Handlebars.registerHelper('canAddTransition', function(fromStatus, toStatus, lifecycle) {
         if (fromStatus == toStatus) {
             return false;
         }
 
         var hasTransition = false;
-        jQuery.each(state.transitions, function (i, transition) {
+        jQuery.each(lifecycle.transitions, function (i, transition) {
             if (transition.from == fromStatus && transition.to == toStatus) {
                 hasTransition = true;
             }
@@ -35,13 +31,13 @@ jQuery(function () {
         return !hasTransition;
     });
 
-    Handlebars.registerHelper('canSelectTransition', function(fromStatus, toStatus, state) {
+    Handlebars.registerHelper('canSelectTransition', function(fromStatus, toStatus, lifecycle) {
         if (fromStatus == toStatus) {
             return false;
         }
 
         var hasTransition = false;
-        jQuery.each(state.transitions, function (i, transition) {
+        jQuery.each(lifecycle.transitions, function (i, transition) {
             if (transition.from == fromStatus && transition.to == toStatus) {
                 hasTransition = true;
             }
@@ -50,27 +46,27 @@ jQuery(function () {
         return hasTransition;
     });
 
-    var updateStatusName = function (state, oldValue, newValue) {
+    var updateStatusName = function (lifecycle, oldValue, newValue) {
         // statusMeta key
-        var oldMeta = state.statusMeta[oldValue];
-        delete state.statusMeta[oldValue];
-        state.statusMeta[newValue] = oldMeta;
+        var oldMeta = lifecycle.statusMeta[oldValue];
+        delete lifecycle.statusMeta[oldValue];
+        lifecycle.statusMeta[newValue] = oldMeta;
 
         // statuses array value
-        var index = state.statuses.indexOf(oldValue);
-        state.statuses[index] = newValue;
+        var index = lifecycle.statuses.indexOf(oldValue);
+        lifecycle.statuses[index] = newValue;
 
         // defaults
-        jQuery.each(state.defaults, function (key, statusName) {
+        jQuery.each(lifecycle.defaults, function (key, statusName) {
             if (statusName == oldValue) {
-                state.defaults[key] = newValue;
+                lifecycle.defaults[key] = newValue;
             }
         });
 
         // actions
 
         // transitions
-        jQuery.each(state.transitions, function (i, transition) {
+        jQuery.each(lifecycle.transitions, function (i, transition) {
             if (transition.from == oldValue) {
                 transition.from = newValue;
             }
@@ -82,25 +78,25 @@ jQuery(function () {
         // rights
     };
 
-    var deleteStatus = function (state, statusName) {
+    var deleteStatus = function (lifecycle, statusName) {
         // statusMeta key
-        delete state.statusMeta[statusName];
+        delete lifecycle.statusMeta[statusName];
 
         // statuses array value
-        var index = state.statuses.indexOf(statusName);
-        state.statuses.splice(index, 1);
+        var index = lifecycle.statuses.indexOf(statusName);
+        lifecycle.statuses.splice(index, 1);
 
         // defaults
-        jQuery.each(state.defaults, function (key, value) {
+        jQuery.each(lifecycle.defaults, function (key, value) {
             if (value == statusName) {
-                delete state.defaults[key];
+                delete lifecycle.defaults[key];
             }
         });
 
         // actions
 
         // transitions
-        state.transitions = jQuery.grep(state.transitions, function (transition) {
+        lifecycle.transitions = jQuery.grep(lifecycle.transitions, function (transition) {
             if (transition.from == statusName || transition.to == statusName) {
                 return false;
             }
@@ -110,8 +106,8 @@ jQuery(function () {
         // rights
     };
 
-    var deleteTransition = function (state, key) {
-        state.transitions = jQuery.grep(state.transitions, function (transition) {
+    var deleteTransition = function (lifecycle, key) {
+        lifecycle.transitions = jQuery.grep(lifecycle.transitions, function (transition) {
             if (transition._key == key) {
                 return false;
             }
@@ -119,8 +115,8 @@ jQuery(function () {
         });
     };
 
-    var deleteText = function (state, key) {
-        state.decorations.text = jQuery.grep(state.decorations.text, function (decoration) {
+    var deleteText = function (lifecycle, key) {
+        lifecycle.decorations.text = jQuery.grep(lifecycle.decorations.text, function (decoration) {
             if (decoration._key == key) {
                 return false;
             }
@@ -144,119 +140,6 @@ jQuery(function () {
               .attr('fill', 'black');
     };
 
-    var newState = function (clone) {
-        return {
-            statuses: [],
-            defaults: {},
-            transitions: [],
-            rights: {},
-            actions: [],
-            decorations: {},
-
-            statusMeta: {}
-        };
-    };
-
-    var initializeStateFromConfig = function (config) {
-        var state = newState();
-
-        jQuery.each(['initial', 'active', 'inactive'], function (i, type) {
-            if (config[type]) {
-                state.statuses = state.statuses.concat(config[type]);
-                jQuery.each(config[type], function (j, statusName) {
-                    state.statusMeta[statusName] = {
-                        _key: _ELEMENT_KEY_SEQ++,
-                        name: statusName,
-                        type: type
-                    };
-                });
-            }
-        });
-
-        var statusCount = state.statuses.length;
-
-        jQuery.each(state.statuses, function (i, statusName) {
-            var meta = state.statusMeta[statusName];
-            // arrange statuses evenly-spaced around a circle
-            if (!meta.x) {
-                meta.x = (Math.sin(2 * Math.PI * (i/statusCount)) + 1) / 2;
-                meta.y = (Math.cos(2 * Math.PI * (i/statusCount)) + 1) / 2;
-            };
-
-            if (!meta.color) {
-                meta.color = defaultColors(meta._key);
-            };
-        });
-
-        if (config.defaults) {
-            state.defaults = config.defaults;
-        }
-
-        if (config.transitions) {
-            jQuery.each(config.transitions, function (fromStatus, toList) {
-                if (fromStatus == "") {
-                }
-                else {
-                    jQuery.each(toList, function (i, toStatus) {
-                        var transition = {
-                            _key  : _ELEMENT_KEY_SEQ++,
-                            from  : fromStatus,
-                            to    : toStatus,
-                            style : 'solid'
-                        };
-                        state.transitions.push(transition);
-                    });
-                }
-            });
-        }
-
-        if (config.rights) {
-            state.rights = config.rights;
-        }
-
-        if (config.actions) {
-            state.actions = config.actions;
-        }
-
-        if (config.decorations) {
-            state.decorations = config.decorations;
-        }
-
-        state.decorations.text = state.decorations.text || [];
-
-        return state;
-    };
-
-    var exportConfigFromState = function (state) {
-        var config = {
-            initial: [],
-            active: [],
-            inactive: [],
-            defaults: state.defaults,
-            actions: state.actions,
-            rights: state.rights,
-            transitions: state.transitions
-        };
-
-        jQuery.each(state.statuses, function (i, statusName) {
-            var statusType = state.statusMeta[statusName].type;
-            config[statusType].push(statusName);
-        });
-
-        var transitions = {};
-        jQuery.each(state.transitions, function (i, transition) {
-            var from = transition.from;
-            var to = transition.to;
-            if (!transitions[from]) {
-                transitions[from] = [];
-            }
-            transitions[from].push(to);
-            config.transitions = transitions;
-        });
-
-        return config;
-    };
-
     var createScale = function (size, padding) {
         return d3.scaleLinear()
                  .domain([0, 1])
@@ -279,7 +162,8 @@ jQuery(function () {
         var width = svg.node().getBoundingClientRect().width;
         var height = svg.node().getBoundingClientRect().height;
 
-        var state = initializeStateFromConfig(config);
+        var lifecycle = new RT.Lifecycle();
+        lifecycle.initializeFromConfig(config);
 
         var xScale = createScale(width, STATUS_CIRCLE_RADIUS * 2);
         var yScale = createScale(height, STATUS_CIRCLE_RADIUS * 2);
@@ -287,7 +171,7 @@ jQuery(function () {
         createArrowHead(svg);
 
         var setInspectorContent = function (type, node) {
-            var params = { state: state };
+            var params = { lifecycle: lifecycle };
             params[type] = node;
 
             inspector.html(templates[type](params));
@@ -302,7 +186,7 @@ jQuery(function () {
                 node[key] = value;
 
                 if (type == 'status' && key == 'name') {
-                    updateStatusName(state, oldValue, value);
+                    updateStatusName(lifecycle, oldValue, value);
                 }
 
                 refreshDisplay();
@@ -344,13 +228,13 @@ jQuery(function () {
                 e.preventDefault();
 
                 if (type == 'status') {
-                    deleteStatus(state, node.name);
+                    deleteStatus(lifecycle, node.name);
                 }
                 else if (type == 'transition') {
-                    deleteTransition(state, node._key);
+                    deleteTransition(lifecycle, node._key);
                 }
                 else if (type == 'text') {
-                    deleteText(state, node._key);
+                    deleteText(lifecycle, node._key);
                 }
 
                 deselectAll(true);
@@ -363,13 +247,7 @@ jQuery(function () {
                 var fromStatus = button.data('from');
                 var toStatus   = button.data('to');
 
-                var transition = {
-                    _key  : _ELEMENT_KEY_SEQ++,
-                    from  : fromStatus,
-                    to    : toStatus,
-                    style : 'solid'
-                };
-                state.transitions.push(transition);
+                lifecycle.addTransition(fromStatus, toStatus);
 
                 button.closest('li').addClass('hidden');
 
@@ -393,7 +271,7 @@ jQuery(function () {
         };
 
         var selectStatus = function (name) {
-            var d = state.statusMeta[name];
+            var d = lifecycle.statusMeta[name];
             setInspectorContent('status', d);
 
             deselectAll(false);
@@ -401,9 +279,9 @@ jQuery(function () {
             svg.classed('selection', true);
             statusContainer.selectAll('*[data-key="'+d._key+'"]').classed('selected', true);
 
-            jQuery.each(state.transitions, function (i, transition) {
+            jQuery.each(lifecycle.transitions, function (i, transition) {
                 if (transition.from == name) {
-                    var key = state.statusMeta[transition.to]._key;
+                    var key = lifecycle.statusMeta[transition.to]._key;
                     statusContainer.selectAll('*[data-key="'+key+'"]').classed('reachable', true);
                     transitionContainer.selectAll('path[data-key="'+transition._key+'"]').classed('selected', true);
                 }
@@ -412,7 +290,7 @@ jQuery(function () {
 
         var selectTransition = function (fromStatus, toStatus) {
             var d;
-            jQuery.each(state.transitions, function (i, transition) {
+            jQuery.each(lifecycle.transitions, function (i, transition) {
                 if (transition.from == fromStatus && transition.to == toStatus) {
                     d = transition;
                 }
@@ -423,8 +301,8 @@ jQuery(function () {
 
             svg.classed('selection', true);
 
-            var fromKey = state.statusMeta[fromStatus]._key;
-            var toKey = state.statusMeta[toStatus]._key;
+            var fromKey = lifecycle.statusMeta[fromStatus]._key;
+            var toKey = lifecycle.statusMeta[toStatus]._key;
             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);
@@ -432,7 +310,7 @@ jQuery(function () {
 
         var selectDecoration = function (type, key) {
             var d;
-            jQuery.each(state.decorations[type], function (i, node) {
+            jQuery.each(lifecycle.decorations[type], function (i, node) {
                 if (node._key == key) {
                     d = node;
                 }
@@ -453,7 +331,7 @@ jQuery(function () {
 
         var refreshStatusNodes = function () {
             var statuses = statusContainer.selectAll("circle")
-                                          .data(Object.values(state.statusMeta), function (d) { return d._key });
+                                          .data(Object.values(lifecycle.statusMeta), function (d) { return d._key });
 
             statuses.exit()
                   .classed("removing", true)
@@ -487,7 +365,7 @@ jQuery(function () {
 
         var refreshStatusLabels = function () {
             var labels = statusContainer.selectAll("text")
-                                        .data(Object.values(state.statusMeta), function (d) { return d._key });
+                                        .data(Object.values(lifecycle.statusMeta), function (d) { return d._key });
 
             labels.exit()
                 .classed("removing", true)
@@ -508,8 +386,8 @@ jQuery(function () {
         };
 
         var linkArc = function (d) {
-          var from = state.statusMeta[d.from];
-          var to = state.statusMeta[d.to];
+          var from = lifecycle.statusMeta[d.from];
+          var to = lifecycle.statusMeta[d.to];
           var dx = xScale(to.x - from.x),
               dy = yScale(to.y - from.y),
               dr = Math.abs(dx*6) + Math.abs(dy*6);
@@ -518,7 +396,7 @@ jQuery(function () {
 
         var refreshTransitions = function () {
             var paths = transitionContainer.selectAll("path")
-                            .data(state.transitions, function (d) { return d._key });
+                            .data(lifecycle.transitions, function (d) { return d._key });
 
             paths.exit()
                 .classed("removing", true)
@@ -539,7 +417,7 @@ jQuery(function () {
 
         var refreshTextDecorations = function () {
             var labels = decorationContainer.selectAll("text")
-                            .data(state.decorations.text, function (d) { return d._key });
+                            .data(lifecycle.decorations.text, function (d) { return d._key });
 
             labels.exit()
                 .classed("removing", true)
@@ -589,7 +467,7 @@ jQuery(function () {
         svg.on('click', function () { deselectAll(true) });
 
         jQuery('form[name=ModifyLifecycle]').submit(function (e) {
-            var config = exportConfigFromState(state);
+            var config = lifecycle.exportAsConfiguration();
             console.log(config);
             e.preventDefault();
             return false;
diff --git a/static/js/lifecycleui-model.js b/static/js/lifecycleui-model.js
new file mode 100644
index 0000000..ac03257
--- /dev/null
+++ b/static/js/lifecycleui-model.js
@@ -0,0 +1,128 @@
+jQuery(function () {
+    var _ELEMENT_KEY_SEQ = 0;
+    var defaultColors = d3.scaleOrdinal(d3.schemeCategory10);
+
+    function Lifecycle () {
+        this.statuses = [];
+        this.defaults = {};
+        this.transitions = [];
+        this.rights = {};
+        this.actions = [];
+        this.decorations = {};
+
+        this.statusMeta = {};
+    };
+
+    Lifecycle.prototype.initializeFromConfig = function (config) {
+        var self = this;
+
+        jQuery.each(['initial', 'active', 'inactive'], function (i, type) {
+            if (config[type]) {
+                self.statuses = self.statuses.concat(config[type]);
+                jQuery.each(config[type], function (j, statusName) {
+                    self.statusMeta[statusName] = {
+                        _key: _ELEMENT_KEY_SEQ++,
+                        name: statusName,
+                        type: type
+                    };
+                });
+            }
+        });
+
+        var statusCount = self.statuses.length;
+
+        jQuery.each(self.statuses, function (i, statusName) {
+            var meta = self.statusMeta[statusName];
+            // arrange statuses evenly-spaced around a circle
+            if (!meta.x) {
+                meta.x = (Math.sin(2 * Math.PI * (i/statusCount)) + 1) / 2;
+                meta.y = (Math.cos(2 * Math.PI * (i/statusCount)) + 1) / 2;
+            };
+
+            if (!meta.color) {
+                meta.color = defaultColors(meta._key);
+            };
+        });
+
+        if (config.defaults) {
+            self.defaults = config.defaults;
+        }
+
+        if (config.transitions) {
+            jQuery.each(config.transitions, function (fromStatus, toList) {
+                if (fromStatus == "") {
+                }
+                else {
+                    jQuery.each(toList, function (i, toStatus) {
+                        var transition = {
+                            _key  : _ELEMENT_KEY_SEQ++,
+                            from  : fromStatus,
+                            to    : toStatus,
+                            style : 'solid'
+                        };
+                        self.transitions.push(transition);
+                    });
+                }
+            });
+        }
+
+        if (config.rights) {
+            self.rights = config.rights;
+        }
+
+        if (config.actions) {
+            self.actions = config.actions;
+        }
+
+        if (config.decorations) {
+            self.decorations = config.decorations;
+        }
+
+        self.decorations.text = self.decorations.text || [];
+    };
+
+    Lifecycle.prototype.exportAsConfiguration = function () {
+        var self = this;
+        var config = {
+            initial: [],
+            active: [],
+            inactive: [],
+            defaults: self.defaults,
+            actions: self.actions,
+            rights: self.rights,
+            transitions: self.transitions
+        };
+
+        jQuery.each(self.statuses, function (i, statusName) {
+            var statusType = self.statusMeta[statusName].type;
+            config[statusType].push(statusName);
+        });
+
+        var transitions = {};
+        jQuery.each(self.transitions, function (i, transition) {
+            var from = transition.from;
+            var to = transition.to;
+            if (!transitions[from]) {
+                transitions[from] = [];
+            }
+            transitions[from].push(to);
+            config.transitions = transitions;
+        });
+
+        return config;
+    };
+
+    Lifecycle.prototype.addTransition = function (fromStatus, toStatus) {
+        var transition = {
+            _key  : _ELEMENT_KEY_SEQ++,
+            from  : fromStatus,
+            to    : toStatus,
+            style : 'solid'
+        };
+        this.transitions.push(transition);
+        return transition;
+    };
+
+    RT.Lifecycle = Lifecycle;
+});
+

commit 904872de2db6fa0750a875fd8dd389735a9dca90
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Thu Aug 31 16:00:51 2017 +0000

    Lifecycle transition selectors

diff --git a/static/js/lifecycleui-editor.js b/static/js/lifecycleui-editor.js
index 8e7a09d..608d538 100644
--- a/static/js/lifecycleui-editor.js
+++ b/static/js/lifecycleui-editor.js
@@ -17,33 +17,11 @@ jQuery(function () {
     });
 
     Handlebars.registerHelper('canAddTransition', function(fromStatus, toStatus, lifecycle) {
-        if (fromStatus == toStatus) {
-            return false;
-        }
-
-        var hasTransition = false;
-        jQuery.each(lifecycle.transitions, function (i, transition) {
-            if (transition.from == fromStatus && transition.to == toStatus) {
-                hasTransition = true;
-            }
-        });
-
-        return !hasTransition;
+        return !lifecycle.hasTransition(fromStatus, toStatus);
     });
 
     Handlebars.registerHelper('canSelectTransition', function(fromStatus, toStatus, lifecycle) {
-        if (fromStatus == toStatus) {
-            return false;
-        }
-
-        var hasTransition = false;
-        jQuery.each(lifecycle.transitions, function (i, transition) {
-            if (transition.from == fromStatus && transition.to == toStatus) {
-                hasTransition = true;
-            }
-        });
-
-        return hasTransition;
+        return lifecycle.hasTransition(fromStatus, toStatus);
     });
 
     var updateStatusName = function (lifecycle, oldValue, newValue) {
@@ -279,22 +257,15 @@ jQuery(function () {
             svg.classed('selection', true);
             statusContainer.selectAll('*[data-key="'+d._key+'"]').classed('selected', true);
 
-            jQuery.each(lifecycle.transitions, function (i, transition) {
-                if (transition.from == name) {
-                    var key = lifecycle.statusMeta[transition.to]._key;
-                    statusContainer.selectAll('*[data-key="'+key+'"]').classed('reachable', true);
-                    transitionContainer.selectAll('path[data-key="'+transition._key+'"]').classed('selected', true);
-                }
+            jQuery.each(lifecycle.transitionsFrom(name), function (i, transition) {
+                var key = lifecycle.statusMeta[transition.to]._key;
+                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;
-            jQuery.each(lifecycle.transitions, function (i, transition) {
-                if (transition.from == fromStatus && transition.to == toStatus) {
-                    d = transition;
-                }
-            });
+            var d = lifecycle.hasTransition(fromStatus, toStatus);
             setInspectorContent('transition', d);
 
             deselectAll(false);
diff --git a/static/js/lifecycleui-model.js b/static/js/lifecycleui-model.js
index ac03257..0ded014 100644
--- a/static/js/lifecycleui-model.js
+++ b/static/js/lifecycleui-model.js
@@ -123,6 +123,43 @@ jQuery(function () {
         return transition;
     };
 
+    Lifecycle.prototype.hasTransition = function (fromStatus, toStatus) {
+        if (fromStatus == toStatus) {
+            return false;
+        }
+
+        for (var i = 0; i < this.transitions.length; ++i) {
+            var transition = this.transitions[i];
+            if (transition.from == fromStatus && transition.to == toStatus) {
+                return transition;
+            }
+        };
+
+        return false;
+    };
+
+    Lifecycle.prototype.transitionsFrom = function (fromStatus) {
+        var transitions = [];
+        for (var i = 0; i < this.transitions.length; ++i) {
+            var transition = this.transitions[i];
+            if (transition.from == fromStatus) {
+                transitions.push(transition);
+            }
+        };
+        return transitions;
+    };
+
+    Lifecycle.prototype.transitionsTo = function (toStatus) {
+        var transitions = [];
+        for (var i = 0; i < this.transitions.length; ++i) {
+            var transition = this.transitions[i];
+            if (transition.to == toStatus) {
+                transitions.push(transition);
+            }
+        };
+        return transitions;
+    };
+
     RT.Lifecycle = Lifecycle;
 });
 

commit 168621703b9fdcd325a6cd6e4ee941c5cf049936
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Thu Aug 31 16:03:09 2017 +0000

    lifecycle.deleteTransition

diff --git a/static/js/lifecycleui-editor.js b/static/js/lifecycleui-editor.js
index 608d538..28d50e1 100644
--- a/static/js/lifecycleui-editor.js
+++ b/static/js/lifecycleui-editor.js
@@ -84,15 +84,6 @@ jQuery(function () {
         // rights
     };
 
-    var deleteTransition = function (lifecycle, key) {
-        lifecycle.transitions = jQuery.grep(lifecycle.transitions, function (transition) {
-            if (transition._key == key) {
-                return false;
-            }
-            return true;
-        });
-    };
-
     var deleteText = function (lifecycle, key) {
         lifecycle.decorations.text = jQuery.grep(lifecycle.decorations.text, function (decoration) {
             if (decoration._key == key) {
@@ -209,7 +200,7 @@ jQuery(function () {
                     deleteStatus(lifecycle, node.name);
                 }
                 else if (type == 'transition') {
-                    deleteTransition(lifecycle, node._key);
+                    lifecycle.deleteTransition(node._key);
                 }
                 else if (type == 'text') {
                     deleteText(lifecycle, node._key);
diff --git a/static/js/lifecycleui-model.js b/static/js/lifecycleui-model.js
index 0ded014..85758cb 100644
--- a/static/js/lifecycleui-model.js
+++ b/static/js/lifecycleui-model.js
@@ -160,6 +160,15 @@ jQuery(function () {
         return transitions;
     };
 
+    Lifecycle.prototype.deleteTransition = function (key) {
+        this.transitions = jQuery.grep(this.transitions, function (transition) {
+            if (transition._key == key) {
+                return false;
+            }
+            return true;
+        });
+    };
+
     RT.Lifecycle = Lifecycle;
 });
 

commit 468de0011277fa6f0dae23516ff025dd1c90e731
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Thu Aug 31 16:05:59 2017 +0000

    Lifecycle.updateStatusName

diff --git a/static/js/lifecycleui-editor.js b/static/js/lifecycleui-editor.js
index 28d50e1..08d0ce0 100644
--- a/static/js/lifecycleui-editor.js
+++ b/static/js/lifecycleui-editor.js
@@ -24,38 +24,6 @@ jQuery(function () {
         return lifecycle.hasTransition(fromStatus, toStatus);
     });
 
-    var updateStatusName = function (lifecycle, oldValue, newValue) {
-        // statusMeta key
-        var oldMeta = lifecycle.statusMeta[oldValue];
-        delete lifecycle.statusMeta[oldValue];
-        lifecycle.statusMeta[newValue] = oldMeta;
-
-        // statuses array value
-        var index = lifecycle.statuses.indexOf(oldValue);
-        lifecycle.statuses[index] = newValue;
-
-        // defaults
-        jQuery.each(lifecycle.defaults, function (key, statusName) {
-            if (statusName == oldValue) {
-                lifecycle.defaults[key] = newValue;
-            }
-        });
-
-        // actions
-
-        // transitions
-        jQuery.each(lifecycle.transitions, function (i, transition) {
-            if (transition.from == oldValue) {
-                transition.from = newValue;
-            }
-            if (transition.to == oldValue) {
-                transition.to = newValue;
-            }
-        });
-
-        // rights
-    };
-
     var deleteStatus = function (lifecycle, statusName) {
         // statusMeta key
         delete lifecycle.statusMeta[statusName];
@@ -155,7 +123,7 @@ jQuery(function () {
                 node[key] = value;
 
                 if (type == 'status' && key == 'name') {
-                    updateStatusName(lifecycle, oldValue, value);
+                    lifecycle.updateStatusName(oldValue, value);
                 }
 
                 refreshDisplay();
diff --git a/static/js/lifecycleui-model.js b/static/js/lifecycleui-model.js
index 85758cb..d9f9421 100644
--- a/static/js/lifecycleui-model.js
+++ b/static/js/lifecycleui-model.js
@@ -112,6 +112,40 @@ jQuery(function () {
         return config;
     };
 
+    Lifecycle.prototype.updateStatusName = function (oldValue, newValue) {
+        var self = this;
+
+        // statusMeta key
+        var oldMeta = self.statusMeta[oldValue];
+        delete self.statusMeta[oldValue];
+        self.statusMeta[newValue] = oldMeta;
+
+        // statuses array value
+        var index = self.statuses.indexOf(oldValue);
+        self.statuses[index] = newValue;
+
+        // defaults
+        jQuery.each(self.defaults, function (key, statusName) {
+            if (statusName == oldValue) {
+                self.defaults[key] = newValue;
+            }
+        });
+
+        // actions
+
+        // transitions
+        jQuery.each(self.transitions, function (i, transition) {
+            if (transition.from == oldValue) {
+                transition.from = newValue;
+            }
+            if (transition.to == oldValue) {
+                transition.to = newValue;
+            }
+        });
+
+        // rights
+    };
+
     Lifecycle.prototype.addTransition = function (fromStatus, toStatus) {
         var transition = {
             _key  : _ELEMENT_KEY_SEQ++,

commit f0d92beba30c8251962bdeec24505532a6edf15b
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Thu Aug 31 16:22:49 2017 +0000

    Lifecycle.deleteStatus

diff --git a/static/js/lifecycleui-editor.js b/static/js/lifecycleui-editor.js
index 08d0ce0..f6f5da0 100644
--- a/static/js/lifecycleui-editor.js
+++ b/static/js/lifecycleui-editor.js
@@ -24,34 +24,6 @@ jQuery(function () {
         return lifecycle.hasTransition(fromStatus, toStatus);
     });
 
-    var deleteStatus = function (lifecycle, statusName) {
-        // statusMeta key
-        delete lifecycle.statusMeta[statusName];
-
-        // statuses array value
-        var index = lifecycle.statuses.indexOf(statusName);
-        lifecycle.statuses.splice(index, 1);
-
-        // defaults
-        jQuery.each(lifecycle.defaults, function (key, value) {
-            if (value == statusName) {
-                delete lifecycle.defaults[key];
-            }
-        });
-
-        // actions
-
-        // transitions
-        lifecycle.transitions = jQuery.grep(lifecycle.transitions, function (transition) {
-            if (transition.from == statusName || transition.to == statusName) {
-                return false;
-            }
-            return true;
-        });
-
-        // rights
-    };
-
     var deleteText = function (lifecycle, key) {
         lifecycle.decorations.text = jQuery.grep(lifecycle.decorations.text, function (decoration) {
             if (decoration._key == key) {
@@ -165,7 +137,7 @@ jQuery(function () {
                 e.preventDefault();
 
                 if (type == 'status') {
-                    deleteStatus(lifecycle, node.name);
+                    lifecycle.deleteStatus(node._key);
                 }
                 else if (type == 'transition') {
                     lifecycle.deleteTransition(node._key);
diff --git a/static/js/lifecycleui-model.js b/static/js/lifecycleui-model.js
index d9f9421..ff2ca1b 100644
--- a/static/js/lifecycleui-model.js
+++ b/static/js/lifecycleui-model.js
@@ -146,6 +146,51 @@ jQuery(function () {
         // rights
     };
 
+    Lifecycle.prototype.statusNameForKey = function (key) {
+        for (var name in this.statusMeta) {
+            var meta = this.statusMeta[name];
+            if (meta._key == key) {
+                return name;
+            }
+        }
+        return null;
+    };
+
+    Lifecycle.prototype.deleteStatus = function (key) {
+        var self = this;
+
+        var statusName = self.statusNameForKey(key);
+        if (!statusName) {
+            console.error("no status for key '" + key + "'; did you accidentally pass status name?");
+        }
+
+        // statusMeta key
+        delete self.statusMeta[statusName];
+
+        // statuses array value
+        var index = self.statuses.indexOf(statusName);
+        self.statuses.splice(index, 1);
+
+        // defaults
+        jQuery.each(self.defaults, function (key, value) {
+            if (value == statusName) {
+                delete self.defaults[key];
+            }
+        });
+
+        // actions
+
+        // transitions
+        self.transitions = jQuery.grep(self.transitions, function (transition) {
+            if (transition.from == statusName || transition.to == statusName) {
+                return false;
+            }
+            return true;
+        });
+
+        // rights
+    };
+
     Lifecycle.prototype.addTransition = function (fromStatus, toStatus) {
         var transition = {
             _key  : _ELEMENT_KEY_SEQ++,

commit 150b2987415d9cc8e3f63617c37ce06c566e170d
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Thu Aug 31 16:23:11 2017 +0000

    Factor out lifecycle.statusObjects()

diff --git a/static/js/lifecycleui-editor.js b/static/js/lifecycleui-editor.js
index f6f5da0..cc65669 100644
--- a/static/js/lifecycleui-editor.js
+++ b/static/js/lifecycleui-editor.js
@@ -233,7 +233,7 @@ jQuery(function () {
 
         var refreshStatusNodes = function () {
             var statuses = statusContainer.selectAll("circle")
-                                          .data(Object.values(lifecycle.statusMeta), function (d) { return d._key });
+                                          .data(lifecycle.statusObjects(), function (d) { return d._key });
 
             statuses.exit()
                   .classed("removing", true)
@@ -267,7 +267,7 @@ jQuery(function () {
 
         var refreshStatusLabels = function () {
             var labels = statusContainer.selectAll("text")
-                                        .data(Object.values(lifecycle.statusMeta), function (d) { return d._key });
+                                        .data(lifecycle.statusObjects(), function (d) { return d._key });
 
             labels.exit()
                 .classed("removing", true)
diff --git a/static/js/lifecycleui-model.js b/static/js/lifecycleui-model.js
index ff2ca1b..42e2752 100644
--- a/static/js/lifecycleui-model.js
+++ b/static/js/lifecycleui-model.js
@@ -156,6 +156,10 @@ jQuery(function () {
         return null;
     };
 
+    Lifecycle.prototype.statusObjects = function () {
+        return Object.values(this.statusMeta);
+    };
+
     Lifecycle.prototype.deleteStatus = function (key) {
         var self = this;
 

commit 96e771728d7db8e957f7e725882df6d365168a42
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Thu Aug 31 16:23:39 2017 +0000

    lifecycle.statusObjectForName

diff --git a/static/js/lifecycleui-editor.js b/static/js/lifecycleui-editor.js
index cc65669..8639437 100644
--- a/static/js/lifecycleui-editor.js
+++ b/static/js/lifecycleui-editor.js
@@ -180,7 +180,7 @@ jQuery(function () {
         };
 
         var selectStatus = function (name) {
-            var d = lifecycle.statusMeta[name];
+            var d = lifecycle.statusObjectForName(name);
             setInspectorContent('status', d);
 
             deselectAll(false);
@@ -288,8 +288,8 @@ jQuery(function () {
         };
 
         var linkArc = function (d) {
-          var from = lifecycle.statusMeta[d.from];
-          var to = lifecycle.statusMeta[d.to];
+          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);
diff --git a/static/js/lifecycleui-model.js b/static/js/lifecycleui-model.js
index 42e2752..0e358fb 100644
--- a/static/js/lifecycleui-model.js
+++ b/static/js/lifecycleui-model.js
@@ -160,6 +160,10 @@ jQuery(function () {
         return Object.values(this.statusMeta);
     };
 
+    Lifecycle.prototype.statusObjectForName = function (statusName) {
+        return this.statusMeta[statusName];
+    };
+
     Lifecycle.prototype.deleteStatus = function (key) {
         var self = this;
 

commit 1d84fcd9987a26e5c2809c1b79b2341fad1662da
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Thu Aug 31 16:23:51 2017 +0000

    lifecycle.keyForStatusName

diff --git a/static/js/lifecycleui-editor.js b/static/js/lifecycleui-editor.js
index 8639437..59b2cbd 100644
--- a/static/js/lifecycleui-editor.js
+++ b/static/js/lifecycleui-editor.js
@@ -189,7 +189,7 @@ jQuery(function () {
             statusContainer.selectAll('*[data-key="'+d._key+'"]').classed('selected', true);
 
             jQuery.each(lifecycle.transitionsFrom(name), function (i, transition) {
-                var key = lifecycle.statusMeta[transition.to]._key;
+                var key = lifecycle.keyForStatusName(transition.to);
                 statusContainer.selectAll('*[data-key="'+key+'"]').classed('reachable', true);
                 transitionContainer.selectAll('path[data-key="'+transition._key+'"]').classed('selected', true);
             });
@@ -203,8 +203,8 @@ jQuery(function () {
 
             svg.classed('selection', true);
 
-            var fromKey = lifecycle.statusMeta[fromStatus]._key;
-            var toKey = lifecycle.statusMeta[toStatus]._key;
+            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);
diff --git a/static/js/lifecycleui-model.js b/static/js/lifecycleui-model.js
index 0e358fb..d5f1fb4 100644
--- a/static/js/lifecycleui-model.js
+++ b/static/js/lifecycleui-model.js
@@ -160,6 +160,10 @@ jQuery(function () {
         return Object.values(this.statusMeta);
     };
 
+    Lifecycle.prototype.keyForStatusName = function (statusName) {
+        return this.statusMeta[statusName]._key;
+    };
+
     Lifecycle.prototype.statusObjectForName = function (statusName) {
         return this.statusMeta[statusName];
     };

commit 7191bfcce809782127d01a2ffe108786169a9d7f
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Thu Aug 31 16:31:54 2017 +0000

    deleteDecoration

diff --git a/static/js/lifecycleui-editor.js b/static/js/lifecycleui-editor.js
index 59b2cbd..d69e455 100644
--- a/static/js/lifecycleui-editor.js
+++ b/static/js/lifecycleui-editor.js
@@ -24,15 +24,6 @@ jQuery(function () {
         return lifecycle.hasTransition(fromStatus, toStatus);
     });
 
-    var deleteText = function (lifecycle, key) {
-        lifecycle.decorations.text = jQuery.grep(lifecycle.decorations.text, function (decoration) {
-            if (decoration._key == key) {
-                return false;
-            }
-            return true;
-        });
-    };
-
     var createArrowHead = function (svg) {
         var defs = svg.append('defs');
         defs.append('marker')
@@ -143,7 +134,7 @@ jQuery(function () {
                     lifecycle.deleteTransition(node._key);
                 }
                 else if (type == 'text') {
-                    deleteText(lifecycle, node._key);
+                    lifecycle.deleteDecoration(type, node._key);
                 }
 
                 deselectAll(true);
diff --git a/static/js/lifecycleui-model.js b/static/js/lifecycleui-model.js
index d5f1fb4..2584bb9 100644
--- a/static/js/lifecycleui-model.js
+++ b/static/js/lifecycleui-model.js
@@ -260,6 +260,15 @@ jQuery(function () {
         });
     };
 
+    Lifecycle.prototype.deleteDecoration = function (type, key) {
+        this.decorations[type] = jQuery.grep(this.decorations[type], function (decoration) {
+            if (decoration._key == key) {
+                return false;
+            }
+            return true;
+        });
+    };
+
     RT.Lifecycle = Lifecycle;
 });
 

commit f5af8d9ecb57dfe0d46d7bf2852827b2f16bfd28
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Thu Aug 31 16:32:05 2017 +0000

    decorationForKey

diff --git a/static/js/lifecycleui-editor.js b/static/js/lifecycleui-editor.js
index d69e455..6634bb3 100644
--- a/static/js/lifecycleui-editor.js
+++ b/static/js/lifecycleui-editor.js
@@ -202,12 +202,7 @@ jQuery(function () {
         };
 
         var selectDecoration = function (type, key) {
-            var d;
-            jQuery.each(lifecycle.decorations[type], function (i, node) {
-                if (node._key == key) {
-                    d = node;
-                }
-            });
+            var d = lifecycle.decorationForKey(type, key);
 
             if (type == 'text') {
                 setInspectorContent('text', d);
diff --git a/static/js/lifecycleui-model.js b/static/js/lifecycleui-model.js
index 2584bb9..8f980ae 100644
--- a/static/js/lifecycleui-model.js
+++ b/static/js/lifecycleui-model.js
@@ -269,6 +269,18 @@ jQuery(function () {
         });
     };
 
+    Lifecycle.prototype.decorationForKey = function (type, key) {
+        var decorations = this.decorations[type];
+        for (var i = 0; i < decorations.length; ++i) {
+            var decoration = decorations[i];
+            if (decoration._key == key) {
+                return decoration;
+            }
+        };
+
+        return false;
+    };
+
     RT.Lifecycle = Lifecycle;
 });
 

commit 2e5960030115737cc7e5f3753ac51f8cec856c3b
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Thu Aug 31 16:33:22 2017 +0000

    statusObjectForKey

diff --git a/static/js/lifecycleui-model.js b/static/js/lifecycleui-model.js
index 8f980ae..164d8e9 100644
--- a/static/js/lifecycleui-model.js
+++ b/static/js/lifecycleui-model.js
@@ -168,6 +168,11 @@ jQuery(function () {
         return this.statusMeta[statusName];
     };
 
+    Lifecycle.prototype.statusObjectForKey = function (key) {
+        var statusName = this.statusNameForKey(key);
+        return this.statusMeta[statusName];
+    };
+
     Lifecycle.prototype.deleteStatus = function (key) {
         var self = this;
 

commit 3f171e367f7148d8ffa0fd4f7871096553d6c604
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Thu Aug 31 16:36:12 2017 +0000

    deleteItemForKey

diff --git a/static/js/lifecycleui-editor.js b/static/js/lifecycleui-editor.js
index 6634bb3..b97cbab 100644
--- a/static/js/lifecycleui-editor.js
+++ b/static/js/lifecycleui-editor.js
@@ -126,16 +126,7 @@ jQuery(function () {
 
             inspector.find('button.delete').click(function (e) {
                 e.preventDefault();
-
-                if (type == 'status') {
-                    lifecycle.deleteStatus(node._key);
-                }
-                else if (type == 'transition') {
-                    lifecycle.deleteTransition(node._key);
-                }
-                else if (type == 'text') {
-                    lifecycle.deleteDecoration(type, node._key);
-                }
+                lifecycle.deleteItemForKey(type, node._key);
 
                 deselectAll(true);
                 refreshDisplay();
diff --git a/static/js/lifecycleui-model.js b/static/js/lifecycleui-model.js
index 164d8e9..550839a 100644
--- a/static/js/lifecycleui-model.js
+++ b/static/js/lifecycleui-model.js
@@ -286,6 +286,22 @@ jQuery(function () {
         return false;
     };
 
+
+    Lifecycle.prototype.deleteItemForKey = function (type, key) {
+        if (type == 'status') {
+            this.deleteStatus(key);
+        }
+        else if (type == 'transition') {
+            this.deleteTransition(key);
+        }
+        else if (type == 'text') {
+            this.deleteDecoration(type, key);
+        }
+        else {
+            console.error("unhandled type '" + type + "'");
+        }
+    };
+
     RT.Lifecycle = Lifecycle;
 });
 

commit 634abeddf05348fce04ede710f2205c699995b6b
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Thu Aug 31 18:38:49 2017 +0000

    Give each item a type

diff --git a/static/js/lifecycleui-model.js b/static/js/lifecycleui-model.js
index 550839a..139a2e0 100644
--- a/static/js/lifecycleui-model.js
+++ b/static/js/lifecycleui-model.js
@@ -21,9 +21,10 @@ jQuery(function () {
                 self.statuses = self.statuses.concat(config[type]);
                 jQuery.each(config[type], function (j, statusName) {
                     self.statusMeta[statusName] = {
-                        _key: _ELEMENT_KEY_SEQ++,
-                        name: statusName,
-                        type: type
+                        _key:  _ELEMENT_KEY_SEQ++,
+                        _type: 'status',
+                        name:  statusName,
+                        type:  type
                     };
                 });
             }
@@ -56,6 +57,7 @@ jQuery(function () {
                     jQuery.each(toList, function (i, toStatus) {
                         var transition = {
                             _key  : _ELEMENT_KEY_SEQ++,
+                            _type : 'transition',
                             from  : fromStatus,
                             to    : toStatus,
                             style : 'solid'
@@ -211,6 +213,7 @@ jQuery(function () {
     Lifecycle.prototype.addTransition = function (fromStatus, toStatus) {
         var transition = {
             _key  : _ELEMENT_KEY_SEQ++,
+            _type : 'transition',
             from  : fromStatus,
             to    : toStatus,
             style : 'solid'

commit 880a6cb3557e23f60bf149f2349b3f2cfaf0dead
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Thu Aug 31 18:41:26 2017 +0000

    Make statusMeta private

diff --git a/static/js/lifecycleui-model.js b/static/js/lifecycleui-model.js
index 139a2e0..f7ce67c 100644
--- a/static/js/lifecycleui-model.js
+++ b/static/js/lifecycleui-model.js
@@ -10,7 +10,7 @@ jQuery(function () {
         this.actions = [];
         this.decorations = {};
 
-        this.statusMeta = {};
+        this._statusMeta = {};
     };
 
     Lifecycle.prototype.initializeFromConfig = function (config) {
@@ -20,7 +20,7 @@ jQuery(function () {
             if (config[type]) {
                 self.statuses = self.statuses.concat(config[type]);
                 jQuery.each(config[type], function (j, statusName) {
-                    self.statusMeta[statusName] = {
+                    self._statusMeta[statusName] = {
                         _key:  _ELEMENT_KEY_SEQ++,
                         _type: 'status',
                         name:  statusName,
@@ -33,7 +33,7 @@ jQuery(function () {
         var statusCount = self.statuses.length;
 
         jQuery.each(self.statuses, function (i, statusName) {
-            var meta = self.statusMeta[statusName];
+            var meta = self._statusMeta[statusName];
             // arrange statuses evenly-spaced around a circle
             if (!meta.x) {
                 meta.x = (Math.sin(2 * Math.PI * (i/statusCount)) + 1) / 2;
@@ -96,7 +96,7 @@ jQuery(function () {
         };
 
         jQuery.each(self.statuses, function (i, statusName) {
-            var statusType = self.statusMeta[statusName].type;
+            var statusType = self._statusMeta[statusName].type;
             config[statusType].push(statusName);
         });
 
@@ -118,9 +118,9 @@ jQuery(function () {
         var self = this;
 
         // statusMeta key
-        var oldMeta = self.statusMeta[oldValue];
-        delete self.statusMeta[oldValue];
-        self.statusMeta[newValue] = oldMeta;
+        var oldMeta = self._statusMeta[oldValue];
+        delete self._statusMeta[oldValue];
+        self._statusMeta[newValue] = oldMeta;
 
         // statuses array value
         var index = self.statuses.indexOf(oldValue);
@@ -149,8 +149,8 @@ jQuery(function () {
     };
 
     Lifecycle.prototype.statusNameForKey = function (key) {
-        for (var name in this.statusMeta) {
-            var meta = this.statusMeta[name];
+        for (var name in this._statusMeta) {
+            var meta = this._statusMeta[name];
             if (meta._key == key) {
                 return name;
             }
@@ -159,20 +159,20 @@ jQuery(function () {
     };
 
     Lifecycle.prototype.statusObjects = function () {
-        return Object.values(this.statusMeta);
+        return Object.values(this._statusMeta);
     };
 
     Lifecycle.prototype.keyForStatusName = function (statusName) {
-        return this.statusMeta[statusName]._key;
+        return this._statusMeta[statusName]._key;
     };
 
     Lifecycle.prototype.statusObjectForName = function (statusName) {
-        return this.statusMeta[statusName];
+        return this._statusMeta[statusName];
     };
 
     Lifecycle.prototype.statusObjectForKey = function (key) {
         var statusName = this.statusNameForKey(key);
-        return this.statusMeta[statusName];
+        return this._statusMeta[statusName];
     };
 
     Lifecycle.prototype.deleteStatus = function (key) {
@@ -184,7 +184,7 @@ jQuery(function () {
         }
 
         // statusMeta key
-        delete self.statusMeta[statusName];
+        delete self._statusMeta[statusName];
 
         // statuses array value
         var index = self.statuses.indexOf(statusName);

commit cd286cdbd7d65fc1de16b81a027a2aaf7f7345a4
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Thu Aug 31 18:47:39 2017 +0000

    Maintain a _keyMap

diff --git a/static/js/lifecycleui-model.js b/static/js/lifecycleui-model.js
index f7ce67c..8cfe684 100644
--- a/static/js/lifecycleui-model.js
+++ b/static/js/lifecycleui-model.js
@@ -10,6 +10,7 @@ jQuery(function () {
         this.actions = [];
         this.decorations = {};
 
+        this._keyMap = {};
         this._statusMeta = {};
     };
 
@@ -20,12 +21,14 @@ jQuery(function () {
             if (config[type]) {
                 self.statuses = self.statuses.concat(config[type]);
                 jQuery.each(config[type], function (j, statusName) {
-                    self._statusMeta[statusName] = {
+                    var item = {
                         _key:  _ELEMENT_KEY_SEQ++,
                         _type: 'status',
                         name:  statusName,
                         type:  type
                     };
+                    self._statusMeta[statusName] = item;
+                    self._keyMap[item._key] = item;
                 });
             }
         });
@@ -63,6 +66,7 @@ jQuery(function () {
                             style : 'solid'
                         };
                         self.transitions.push(transition);
+                        self._keyMap[transition._key] = transition;
                     });
                 }
             });
@@ -149,13 +153,7 @@ jQuery(function () {
     };
 
     Lifecycle.prototype.statusNameForKey = function (key) {
-        for (var name in this._statusMeta) {
-            var meta = this._statusMeta[name];
-            if (meta._key == key) {
-                return name;
-            }
-        }
-        return null;
+        return this._keyMap[key].name;
     };
 
     Lifecycle.prototype.statusObjects = function () {
@@ -183,8 +181,9 @@ jQuery(function () {
             console.error("no status for key '" + key + "'; did you accidentally pass status name?");
         }
 
-        // statusMeta key
+        // internal book-keeping
         delete self._statusMeta[statusName];
+        delete self._keyMap[key];
 
         // statuses array value
         var index = self.statuses.indexOf(statusName);
@@ -219,6 +218,7 @@ jQuery(function () {
             style : 'solid'
         };
         this.transitions.push(transition);
+        this._keyMap[transition._key] = transition;
         return transition;
     };
 
@@ -266,6 +266,7 @@ jQuery(function () {
             }
             return true;
         });
+        delete this._keyMap[key];
     };
 
     Lifecycle.prototype.deleteDecoration = function (type, key) {
@@ -275,6 +276,7 @@ jQuery(function () {
             }
             return true;
         });
+        delete this._keyMap[key];
     };
 
     Lifecycle.prototype.decorationForKey = function (type, key) {

commit 7d959ecf20b06a27a1b9dad7a2fc682110bfc8d1
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Thu Aug 31 18:49:13 2017 +0000

    Generalize an itemForKey

diff --git a/static/js/lifecycleui-editor.js b/static/js/lifecycleui-editor.js
index b97cbab..9e036e6 100644
--- a/static/js/lifecycleui-editor.js
+++ b/static/js/lifecycleui-editor.js
@@ -193,7 +193,7 @@ jQuery(function () {
         };
 
         var selectDecoration = function (type, key) {
-            var d = lifecycle.decorationForKey(type, key);
+            var d = lifecycle.itemForKey(key);
 
             if (type == 'text') {
                 setInspectorContent('text', d);
diff --git a/static/js/lifecycleui-model.js b/static/js/lifecycleui-model.js
index 8cfe684..69b4b6c 100644
--- a/static/js/lifecycleui-model.js
+++ b/static/js/lifecycleui-model.js
@@ -168,11 +168,6 @@ jQuery(function () {
         return this._statusMeta[statusName];
     };
 
-    Lifecycle.prototype.statusObjectForKey = function (key) {
-        var statusName = this.statusNameForKey(key);
-        return this._statusMeta[statusName];
-    };
-
     Lifecycle.prototype.deleteStatus = function (key) {
         var self = this;
 
@@ -279,19 +274,10 @@ jQuery(function () {
         delete this._keyMap[key];
     };
 
-    Lifecycle.prototype.decorationForKey = function (type, key) {
-        var decorations = this.decorations[type];
-        for (var i = 0; i < decorations.length; ++i) {
-            var decoration = decorations[i];
-            if (decoration._key == key) {
-                return decoration;
-            }
-        };
-
-        return false;
+    Lifecycle.prototype.itemForKey = function (key) {
+        return this._keyMap[key];
     };
 
-
     Lifecycle.prototype.deleteItemForKey = function (type, key) {
         if (type == 'status') {
             this.deleteStatus(key);

commit 646404cdf4c3b42f66a3077c9685a3759e1c64be
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Thu Aug 31 18:50:15 2017 +0000

    No longer need to pass type to deleteItemForKey

diff --git a/static/js/lifecycleui-editor.js b/static/js/lifecycleui-editor.js
index 9e036e6..a809b34 100644
--- a/static/js/lifecycleui-editor.js
+++ b/static/js/lifecycleui-editor.js
@@ -126,7 +126,7 @@ jQuery(function () {
 
             inspector.find('button.delete').click(function (e) {
                 e.preventDefault();
-                lifecycle.deleteItemForKey(type, node._key);
+                lifecycle.deleteItemForKey(node._key);
 
                 deselectAll(true);
                 refreshDisplay();
diff --git a/static/js/lifecycleui-model.js b/static/js/lifecycleui-model.js
index 69b4b6c..c9dd583 100644
--- a/static/js/lifecycleui-model.js
+++ b/static/js/lifecycleui-model.js
@@ -278,7 +278,10 @@ jQuery(function () {
         return this._keyMap[key];
     };
 
-    Lifecycle.prototype.deleteItemForKey = function (type, key) {
+    Lifecycle.prototype.deleteItemForKey = function (key) {
+        var item = this.itemForKey(key);
+        var type = item._type;
+
         if (type == 'status') {
             this.deleteStatus(key);
         }

commit dccd5d3312a00768fb9e80529e04ca568a6890dc
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Thu Aug 31 19:05:02 2017 +0000

    lifecycle.updateItem

diff --git a/static/js/lifecycleui-editor.js b/static/js/lifecycleui-editor.js
index a809b34..562e1b7 100644
--- a/static/js/lifecycleui-editor.js
+++ b/static/js/lifecycleui-editor.js
@@ -78,17 +78,9 @@ jQuery(function () {
             inspector.find('sf-menu').supersubs().superfish({ dropShadows: false, speed: 'fast', delay: 0 }).supposition()
 
             inspector.find(':input').change(function () {
-                var key = this.name;
+                var field = this.name;
                 var value = jQuery(this).val();
-
-                var oldValue = node[key];
-
-                node[key] = value;
-
-                if (type == 'status' && key == 'name') {
-                    lifecycle.updateStatusName(oldValue, value);
-                }
-
+                lifecycle.updateItem(node, field, value);
                 refreshDisplay();
             });
 
diff --git a/static/js/lifecycleui-model.js b/static/js/lifecycleui-model.js
index c9dd583..3a02a65 100644
--- a/static/js/lifecycleui-model.js
+++ b/static/js/lifecycleui-model.js
@@ -296,6 +296,17 @@ jQuery(function () {
         }
     };
 
+    Lifecycle.prototype.updateItem = function (item, field, newValue) {
+        var oldValue = item[field];
+
+        item[field] = newValue;
+
+        if (item._type == 'status' && field == 'name') {
+            this.updateStatusName(oldValue, newValue);
+        }
+    };
+
+
     RT.Lifecycle = Lifecycle;
 });
 

commit c9e46a4a229f5c05f26199f8b084babbe3220255
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Thu Aug 31 19:06:49 2017 +0000

    Have setInspectorContent identify what type to show

diff --git a/static/js/lifecycleui-editor.js b/static/js/lifecycleui-editor.js
index 562e1b7..80ea405 100644
--- a/static/js/lifecycleui-editor.js
+++ b/static/js/lifecycleui-editor.js
@@ -70,7 +70,9 @@ jQuery(function () {
 
         createArrowHead(svg);
 
-        var setInspectorContent = function (type, node) {
+        var setInspectorContent = function (node) {
+            var type = node ? node._type : 'canvas';
+
             var params = { lifecycle: lifecycle };
             params[type] = node;
 
@@ -143,7 +145,7 @@ jQuery(function () {
 
         var deselectAll = function (inspectCanvas) {
             if (inspectCanvas) {
-                setInspectorContent('canvas');
+                setInspectorContent(null);
             }
 
             svg.classed('selection', false);
@@ -155,7 +157,7 @@ jQuery(function () {
 
         var selectStatus = function (name) {
             var d = lifecycle.statusObjectForName(name);
-            setInspectorContent('status', d);
+            setInspectorContent(d);
 
             deselectAll(false);
 
@@ -171,7 +173,7 @@ jQuery(function () {
 
         var selectTransition = function (fromStatus, toStatus) {
             var d = lifecycle.hasTransition(fromStatus, toStatus);
-            setInspectorContent('transition', d);
+            setInspectorContent(d);
 
             deselectAll(false);
 
@@ -184,15 +186,9 @@ jQuery(function () {
             transitionContainer.select('path[data-key="'+d._key+'"]').classed('selected', true);
         };
 
-        var selectDecoration = function (type, key) {
+        var selectDecoration = function (key) {
             var d = lifecycle.itemForKey(key);
-
-            if (type == 'text') {
-                setInspectorContent('text', d);
-            }
-            else {
-                setInspectorContent('shape', d);
-            }
+            setInspectorContent(d);
 
             deselectAll(false);
 
@@ -299,7 +295,7 @@ jQuery(function () {
                          .attr("data-key", function (d) { return d._key })
                          .on("click", function (d) {
                              d3.event.stopPropagation();
-                             selectDecoration('text', d._key);
+                             selectDecoration(d._key);
                          })
                   .merge(labels)
                           .attr("x", function (d) { return xScale(d.x) })
@@ -333,7 +329,7 @@ jQuery(function () {
             selectTransition(fromStatus, toStatus);
         });
 
-        setInspectorContent('canvas');
+        setInspectorContent(null);
 
         svg.on('click', function () { deselectAll(true) });
 

commit 12e9ecdf9c467f59c3e7dc6c3f610e90c3af5e65
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Thu Aug 31 19:07:40 2017 +0000

    Have updating color go through updateItem

diff --git a/static/js/lifecycleui-editor.js b/static/js/lifecycleui-editor.js
index 80ea405..6557794 100644
--- a/static/js/lifecycleui-editor.js
+++ b/static/js/lifecycleui-editor.js
@@ -97,7 +97,7 @@ jQuery(function () {
                         return;
                     }
                     inspector.find('.status-color').val(newColor);
-                    node.color = newColor;
+                    lifecycle.updateItem(node, 'color', newColor);
                     refreshDisplay();
                 });
                 farb.setColor(node.color);
@@ -111,7 +111,7 @@ jQuery(function () {
                         farb.setColor(newColor);
                         skipUpdateCallback = 0;
 
-                        node.color = newColor;
+                        lifecycle.updateItem(node, 'color', newColor);
                         refreshDisplay();
                     }
                 });

commit 1d295fee98bf1538ccf58e1bf53010df090ff69c
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Thu Aug 31 19:26:03 2017 +0000

    Rename editor js to viewer js

diff --git a/lib/RT/Extension/LifecycleUI.pm b/lib/RT/Extension/LifecycleUI.pm
index b7bec9d..22f13a3 100644
--- a/lib/RT/Extension/LifecycleUI.pm
+++ b/lib/RT/Extension/LifecycleUI.pm
@@ -8,7 +8,7 @@ our $VERSION = '0.01';
 RT->AddJavaScript("d3.min.js");
 RT->AddJavaScript("handlebars-4.0.6.min.js");
 RT->AddJavaScript("lifecycleui-model.js");
-RT->AddJavaScript("lifecycleui-editor.js");
+RT->AddJavaScript("lifecycleui-viewer.js");
 
 RT->AddStyleSheets("lifecycleui.css");
 RT->AddStyleSheets("lifecycleui-display.css");
diff --git a/static/js/lifecycleui-editor.js b/static/js/lifecycleui-viewer.js
similarity index 100%
rename from static/js/lifecycleui-editor.js
rename to static/js/lifecycleui-viewer.js

commit 490515b2479484b621d958d9ac7380fc50a39105
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Thu Aug 31 21:29:34 2017 +0000

    First pass at switching viewer JS to a class
    
    This way we can subclass it for the editor

diff --git a/html/Elements/LifecycleGraph b/html/Elements/LifecycleGraph
index a8f9ec3..4491f80 100644
--- a/html/Elements/LifecycleGraph
+++ b/html/Elements/LifecycleGraph
@@ -1,13 +1,20 @@
-<div class="lifecycle-ui" data-name="<% $Lifecycle %>">
-    <script type="text/javascript">
-        RT.LifecycleConfig = RT.LifecycleConfig || {};
-        RT.LifecycleConfig['<% $Lifecycle %>'] = <% JSON($config) |n %>;
-    </script>
+<div class="lifecycle-ui" id="lifecycle-<% $id %>">
     <svg<% $Editing ? ' class="editing"' : '' |n %>></svg>
 % 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 () {
+                var viewer = new RT.LifecycleViewer();
+                var container = this;
+                var config = <% JSON($config) |n %>;
+                viewer.initializeEditor(container, config);
+            });
+        });
+    </script>
 </div>
 <%ARGS>
 $Editing => 0
@@ -20,4 +27,6 @@ $Lifecycle ||= $Ticket->Lifecycle
 
 my $config = RT->Config->Get('Lifecycles')->{$Lifecycle};
 Abort("Invalid Lifecycle") if !$Lifecycle || !$config;
+
+my $id = $Lifecycle . '-' . int(rand(2**31));
 </%INIT>
diff --git a/static/js/lifecycleui-viewer.js b/static/js/lifecycleui-viewer.js
index 6557794..be92f1d 100644
--- a/static/js/lifecycleui-viewer.js
+++ b/static/js/lifecycleui-viewer.js
@@ -1,31 +1,38 @@
 jQuery(function () {
     var STATUS_CIRCLE_RADIUS = 35;
 
-    var templates = {};
-
-    jQuery('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;
-    });
-
-    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 createArrowHead = function (svg) {
-        var defs = svg.append('defs');
+    function Viewer (container) {
+    };
+
+    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;
+    };
+
+    Viewer.prototype.createArrowHead = function () {
+        var defs = this.svg.append('defs');
         defs.append('marker')
             .attr('id', 'marker_arrowhead')
             .attr('markerHeight', 5)
@@ -40,20 +47,20 @@ jQuery(function () {
               .attr('fill', 'black');
     };
 
-    var createScale = function (size, padding) {
+    Viewer.prototype.createScale = function (size, padding) {
         return d3.scaleLinear()
                  .domain([0, 1])
                  .range([padding, size - padding]);
     };
 
-    var initializeEditor = function (node) {
-        var container = jQuery(node);
-        var name = container.data('name');
-        var config = RT.LifecycleConfig[name];
-        var inspector = container.find('.inspector');
+    Viewer.prototype.initializeEditor = function (node, config) {
+        var self = this;
+
+        var container  = self.container = jQuery(node);
+        var svg        = self.svg = d3.select(node).select('svg');
+        self.templates = self._initializeTemplates(container);
 
-        var svg = d3.select(node)
-                    .select('svg');
+        var inspector = self.container.find('.inspector');
 
         var transitionContainer = svg.append('g').classed('transitions', true);
         var statusContainer = svg.append('g').classed('statuses', true);
@@ -65,10 +72,10 @@ jQuery(function () {
         var lifecycle = new RT.Lifecycle();
         lifecycle.initializeFromConfig(config);
 
-        var xScale = createScale(width, STATUS_CIRCLE_RADIUS * 2);
-        var yScale = createScale(height, STATUS_CIRCLE_RADIUS * 2);
+        var xScale = self.createScale(width, STATUS_CIRCLE_RADIUS * 2);
+        var yScale = self.createScale(height, STATUS_CIRCLE_RADIUS * 2);
 
-        createArrowHead(svg);
+        self.createArrowHead();
 
         var setInspectorContent = function (node) {
             var type = node ? node._type : 'canvas';
@@ -76,7 +83,7 @@ jQuery(function () {
             var params = { lifecycle: lifecycle };
             params[type] = node;
 
-            inspector.html(templates[type](params));
+            inspector.html(self.templates[type](params));
             inspector.find('sf-menu').supersubs().superfish({ dropShadows: false, speed: 'fast', delay: 0 }).supposition()
 
             inspector.find(':input').change(function () {
@@ -343,6 +350,6 @@ jQuery(function () {
         refreshDisplay();
     };
 
-    jQuery(".lifecycle-ui").each(function () { initializeEditor(this) });
+    RT.LifecycleViewer = Viewer;
 });
 

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


More information about the Bps-public-commit mailing list