[Bps-public-commit] rt-extension-lifecycleui branch, master, updated. 8a4a824c8e8e9e81913d52565ba9953fb0352d80

Shawn Moore shawn at bestpractical.com
Fri Sep 8 17:55:43 EDT 2017


The branch, master has been updated
       via  8a4a824c8e8e9e81913d52565ba9953fb0352d80 (commit)
       via  2286fdfe95e1c06f54c8ead1422c8f9f73510a04 (commit)
       via  5b4e82c8955f5d5d8186bdd61dce7916bf2992ae (commit)
       via  2e82183c182e9d6be205c20db02488cf0e5cabfa (commit)
       via  eccbfb727d04f9f206b77665d31bccabf34a22bb (commit)
       via  f74fb3355daf56e5938d8e973f3b7e193a7ae991 (commit)
       via  6437939ec363baeb225c34a659bcdd32424c3b7c (commit)
       via  5b7636420ac1c9558de6d1f325ff0a6d00688f58 (commit)
       via  f54ebb71151b05d0be77330bfb344686dae04a2c (commit)
       via  8dabf9ca82d8cc2c8c44e737eec2b11cde559200 (commit)
       via  4f8527c9ee6fa2dbda16398b5ca7c6317fd94bc3 (commit)
       via  a90d4b93c03a495ba50cc48e3014c0799d45ccc9 (commit)
       via  0debf62d56b9b269f69a8175e0ab8e0e2251702d (commit)
       via  2ba8415e627ed29bf9b275a0b82fa1b35c30b2af (commit)
       via  fca968ad066e92cb6dfbe82b645bbd01226ba4d0 (commit)
       via  86b6519b2d2052a4648ea4a2f0404bec03f18f65 (commit)
       via  0c6f9ac75c0f802e00b6f67d7c2d35334d79ea98 (commit)
       via  5b583b6e365890177574285ee8eaa10b7f4552c5 (commit)
       via  d54601dc9b0fa5a6b2683f60353488f8a11d37a0 (commit)
       via  0653f4538d4d6576b70b06d76f54ec3b3c3927f2 (commit)
       via  f058230e7537f1f2724cace1aef2e0789e0803ae (commit)
       via  3b276849ea7979ce874e8332f25b2bfea7f20a9e (commit)
       via  54d36bcd20035d4c33854102a0b4cb3195313a47 (commit)
       via  3e9de82b31c76628d8b38084c55d8d69a7386f84 (commit)
       via  f7bbd0fbaa4f721d64f6e0423cf130c3d8e603b2 (commit)
       via  2ce3c325523e07be4e9ab794020996d6631e7526 (commit)
       via  9ad50859cc3ec1eb7affa1b5f9a641ce04a487ac (commit)
       via  66f3d135d9fb234649fcf2b327e90e916ec5b2c7 (commit)
       via  cb6456483533ac3be4999f9ac5767a6ec647d1d6 (commit)
       via  cc68b0133ac0287977080ab86e1c220c2386ec1b (commit)
       via  12d61a4d739996496d1af6e8dcde1d1e38f51d9f (commit)
      from  b26cb45ab608b0d0844cf07e65f5e659a5c65c04 (commit)

Summary of changes:
 MANIFEST                                           |  47 ++++++
 html/Admin/Lifecycles/Create.html                  |  85 +++++++----
 html/Admin/Lifecycles/Mappings.html                |  88 +++++++++++
 .../Elements/Tabs/Privileged                       |   1 +
 html/Elements/LifecycleGraphExtras                 |  11 ++
 html/Elements/LifecycleInspectorCanvas             |  50 ++++--
 html/Elements/LifecycleInspectorCircle             |   1 +
 html/Elements/LifecycleInspectorLine               |   1 +
 html/Elements/LifecycleInspectorPolygon            |   1 +
 html/Elements/LifecycleInspectorStatus             |  34 +++--
 html/Elements/LifecycleInspectorText               |   5 +-
 lib/RT/Extension/LifecycleUI.pm                    |  22 +++
 static/css/lifecycleui-editor.css                  |  53 +++++--
 static/css/lifecycleui-viewer-interactive.css      |  38 ++++-
 static/js/lifecycleui-editor.js                    | 170 ++++++++++++++++-----
 static/js/lifecycleui-model.js                     |  75 ++++++---
 static/js/lifecycleui-viewer-interactive.js        |  10 ++
 static/js/lifecycleui-viewer.js                    |  60 ++++++--
 18 files changed, 591 insertions(+), 161 deletions(-)
 create mode 100644 MANIFEST
 create mode 100644 html/Admin/Lifecycles/Mappings.html

- Log -----------------------------------------------------------------
commit 12d61a4d739996496d1af6e8dcde1d1e38f51d9f
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Fri Sep 8 15:13:58 2017 +0000

    Enhance interactive menus with superfish

diff --git a/static/css/lifecycleui-viewer-interactive.css b/static/css/lifecycleui-viewer-interactive.css
index 6a9f6c6..0e08906 100644
--- a/static/css/lifecycleui-viewer-interactive.css
+++ b/static/css/lifecycleui-viewer-interactive.css
@@ -18,3 +18,6 @@
     text-decoration: none;
 }
 
+.lifecycle-ui .status-menu .sf-menu.sf-vertical {
+    width: 12em;
+}
diff --git a/static/js/lifecycleui-viewer-interactive.js b/static/js/lifecycleui-viewer-interactive.js
index ee107d4..47400a8 100644
--- a/static/js/lifecycleui-viewer-interactive.js
+++ b/static/js/lifecycleui-viewer-interactive.js
@@ -34,6 +34,8 @@ jQuery(function () {
         this.menuContainer.find('.status-menu.selected').removeClass('selected');
         this.selectedMenu.addClass('selected');
 
+        this.selectedMenu.find(".toplevel").addClass('sf-menu sf-vertical sf-js-enabled sf-shadow').supersubs().superfish({ speed: 'fast' });
+
         this._setMenuPosition();
     };
 

commit cc68b0133ac0287977080ab86e1c220c2386ec1b
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Fri Sep 8 15:14:17 2017 +0000

    Don't highlight disabled menu items on hover

diff --git a/static/css/lifecycleui-viewer-interactive.css b/static/css/lifecycleui-viewer-interactive.css
index 0e08906..c20a93f 100644
--- a/static/css/lifecycleui-viewer-interactive.css
+++ b/static/css/lifecycleui-viewer-interactive.css
@@ -9,15 +9,22 @@
     display: block;
 }
 
-.lifecycle-ui .status-menu .not-current,
-.lifecycle-ui .status-menu .no-transition,
-.lifecycle-ui .status-menu .no-permission,
-.lifecycle-ui .status-menu .hide-resolve-with-deps {
+.lifecycle-ui .status-menu a.not-current,
+.lifecycle-ui .status-menu a.no-transition,
+.lifecycle-ui .status-menu a.no-permission,
+.lifecycle-ui .status-menu a.hide-resolve-with-deps {
     color: #AAAAAA;
     cursor: default;
     text-decoration: none;
 }
 
+.lifecycle-ui .status-menu .not-current:hover,
+.lifecycle-ui .status-menu .no-transition:hover,
+.lifecycle-ui .status-menu .no-permission:hover,
+.lifecycle-ui .status-menu .hide-resolve-with-deps:hover {
+    background-color: #fff;
+}
+
 .lifecycle-ui .status-menu .sf-menu.sf-vertical {
     width: 12em;
 }
diff --git a/static/js/lifecycleui-viewer-interactive.js b/static/js/lifecycleui-viewer-interactive.js
index 47400a8..79a0e8f 100644
--- a/static/js/lifecycleui-viewer-interactive.js
+++ b/static/js/lifecycleui-viewer-interactive.js
@@ -61,6 +61,14 @@ jQuery(function () {
          Super.prototype.initializeViewer.call(self, node, name, config, focusStatus);
          self.menuContainer = jQuery(node).find('.status-menus');
          self.svg.on('click', function () { self.deselectStatus() });
+
+         // copy classes from <a> to <li> for improved styling
+         self.menuContainer.find('.status-menu li a').each(function () {
+             var link = jQuery(this);
+             var item = link.closest('li');
+             item.addClass(link.attr("class"));
+             item.removeClass('menu-item');
+         });
     };
 
     RT.LifecycleViewerInteractive = Interactive;

commit cb6456483533ac3be4999f9ac5767a6ec647d1d6
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Fri Sep 8 15:19:17 2017 +0000

    More styling improvements for status menus

diff --git a/static/css/lifecycleui-viewer-interactive.css b/static/css/lifecycleui-viewer-interactive.css
index c20a93f..191a048 100644
--- a/static/css/lifecycleui-viewer-interactive.css
+++ b/static/css/lifecycleui-viewer-interactive.css
@@ -9,6 +9,12 @@
     display: block;
 }
 
+.lifecycle-ui .status-menu .sf-menu a:visited,
+.lifecycle-ui .status-menu .sf-menu a {
+    border: none;
+    color: #000;
+}
+
 .lifecycle-ui .status-menu a.not-current,
 .lifecycle-ui .status-menu a.no-transition,
 .lifecycle-ui .status-menu a.no-permission,
@@ -26,5 +32,19 @@
 }
 
 .lifecycle-ui .status-menu .sf-menu.sf-vertical {
-    width: 12em;
+    width: 10em;
+}
+
+.lifecycle-ui .status-menu .sf-menu.sf-vertical li {
+    width: 100%;
 }
+
+.lifecycle-ui .status-menu .sf-menu.sf-shadow {
+    -moz-border-radius: 0;
+    -webkit-border-radius: 0;
+    border-radius: 0;
+    -moz-box-shadow: 2px 2px 8px -2px #999;
+    -webkit-box-shadow: 2px 2px 8px -2px #999;
+    box-shadow: 2px 2px 8px -2px #999;
+}
+

commit 66f3d135d9fb234649fcf2b327e90e916ec5b2c7
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Fri Sep 8 15:46:12 2017 +0000

    Improve canvas menu using superfish

diff --git a/html/Elements/LifecycleInspectorCanvas b/html/Elements/LifecycleInspectorCanvas
index 20f5bb6..b64410a 100644
--- a/html/Elements/LifecycleInspectorCanvas
+++ b/html/Elements/LifecycleInspectorCanvas
@@ -58,38 +58,39 @@
                        {{/each}}
                      {{/select}}
                    </select><br>
-
-        <button class="add-status"><&|/l&>Add Status</&></button><br>
-        <ul>
-          <li>Add Decoration...
+        <br>
+        
+        <ul class="toplevel">
+          <li><a href="javascript:void(0)" class="menu-item add-status"><&|/l&>Add Status</&></a></li>
+          <li class="has-children"><a href="javascript:void(0)" class="menu-item">Add Decoration...</a>
               <ul>
-                  <li><a href="#" class="add-text">Add Text</a></li>
-                  <li><a href="#" class="add-polygon" data-type="Triangle">Add Triangle</a></li>
-                  <li><a href="#" class="add-polygon" data-type="Rectangle">Add Rectangle</a></li>
-                  <li><a href="#" class="add-circle">Add Circle</a></li>
-                  <li><a href="#" class="add-line">Add Line</a></li>
+                  <li><a href="javascript:void(0)" class="menu-item add-text">Add Text</a></li>
+                  <li><a href="javascript:void(0)" class="menu-item add-polygon" data-type="Triangle">Add Triangle</a></li>
+                  <li><a href="javascript:void(0)" class="menu-item add-polygon" data-type="Rectangle">Add Rectangle</a></li>
+                  <li><a href="javascript:void(0)" class="menu-item add-circle">Add Circle</a></li>
+                  <li><a href="javascript:void(0)" class="menu-item add-line">Add Line</a></li>
               </ul>
           </li>
-          <li>Select Status...
+          <li class="has-children"><a href="javascript:void(0)" class="menu-item">Select Status...</a>
               <ul>
               {{#each lifecycle.statuses}}
-              <li><a href="#" class="select-status" data-name="{{this}}">{{this}}</a></li>
+              <li><a href="javascript:void(0)" class="menu-item select-status" data-name="{{this}}">{{this}}</a></li>
               {{/each}}
               </ul>
            </li>
-           <li>Select Decoration...
+           <li class="has-children"><a href="javascript:void(0)" class="menu-item">Select Decoration...</a>
               <ul>
               {{#each lifecycle.decorations.text}}
-              <li><a href="#" class="select-decoration" data-key="{{this._key}}">{{truncate this.text}}</a></li>
+              <li><a href="javascript:void(0)" class="menu-item select-decoration" data-key="{{this._key}}">{{truncate this.text}}</a></li>
               {{/each}}
               {{#each lifecycle.decorations.polygon}}
-              <li><a href="#" class="select-decoration" data-key="{{this._key}}">{{truncate this.label}}</a></li>
+              <li><a href="javascript:void(0)" class="menu-item select-decoration" data-key="{{this._key}}">{{truncate this.label}}</a></li>
               {{/each}}
               {{#each lifecycle.decorations.circle}}
-              <li><a href="#" class="select-decoration" data-key="{{this._key}}">{{truncate this.label}}</a></li>
+              <li><a href="javascript:void(0)" class="menu-item select-decoration" data-key="{{this._key}}">{{truncate this.label}}</a></li>
               {{/each}}
               {{#each lifecycle.decorations.line}}
-              <li><a href="#" class="select-decoration" data-key="{{this._key}}">{{truncate this.label}}</a></li>
+              <li><a href="javascript:void(0)" class="menu-item select-decoration" data-key="{{this._key}}">{{truncate this.label}}</a></li>
               {{/each}}
               </ul>
            </li>
diff --git a/static/css/lifecycleui-editor.css b/static/css/lifecycleui-editor.css
index e047a1d..741d1c8 100644
--- a/static/css/lifecycleui-editor.css
+++ b/static/css/lifecycleui-editor.css
@@ -89,16 +89,6 @@
     margin-top: 0;
 }
 
-.lifecycle-ui .inspector .canvas ul {
-    list-style-type: none;
-    padding: 0;
-}
-
-.lifecycle-ui .inspector .canvas ul ul {
-    padding-left: 3em;
-    margin-bottom: 1em;
-}
-
 .lifecycle-ui .inspector .hint {
     vertical-align: super;
     font-size: .8em;
@@ -108,3 +98,24 @@
 .lifecycle-ui .invisible {
     visibility: hidden;
 }
+
+.lifecycle-ui .inspector .sf-menu a:visited,
+.lifecycle-ui .inspector .sf-menu a {
+    border: none;
+    color: #000;
+}
+
+.lifecycle-ui .inspector .sf-menu.sf-vertical {
+    margin-left: -1em;
+}
+
+.lifecycle-ui .inspector .sf-menu.sf-vertical,
+.lifecycle-ui .inspector .sf-menu.sf-vertical > li {
+    width: 175px;
+}
+
+.lifecycle-ui .inspector .sf-vertical li:hover ul,
+.lifecycle-ui .inspector .sf-vertical li.sfHover ul {
+    left: 175px;
+}
+
diff --git a/static/js/lifecycleui-editor.js b/static/js/lifecycleui-editor.js
index b43e6f6..2b94259 100644
--- a/static/js/lifecycleui-editor.js
+++ b/static/js/lifecycleui-editor.js
@@ -58,6 +58,8 @@ jQuery(function () {
 
         inspector.find('.content').html(self.templates[type](params));
 
+        inspector.find(".toplevel").addClass('sf-menu sf-vertical sf-js-enabled sf-shadow').supersubs().superfish({ speed: 'fast' });
+
         inspector.find(':checkbox[data-show-hide]').each(function () {
             var field = jQuery(this);
             var selector = field.data('show-hide');

commit 9ad50859cc3ec1eb7affa1b5f9a641ce04a487ac
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Fri Sep 8 15:48:46 2017 +0000

    Add missing </span>

diff --git a/html/Elements/LifecycleInspectorStatus b/html/Elements/LifecycleInspectorStatus
index 035fa6c..206fd3b 100644
--- a/html/Elements/LifecycleInspectorStatus
+++ b/html/Elements/LifecycleInspectorStatus
@@ -8,7 +8,7 @@
                   <option value="inactive"><&|/l&>inactive</&></option>
                   {{/select}}
               </select><br>
-        <span title="<&|/l&>Can this status be selected on creation?</&>">Creation<span class="hint" >[?]</span>: <input type="checkbox" name="creation" {{#if status.creation}}checked=checked{{/if}}><br>
+        <span title="<&|/l&>Can this status be selected on creation?</&>">Creation<span class="hint" >[?]</span>: <input type="checkbox" name="creation" {{#if status.creation}}checked=checked{{/if}}></span><br>
         Color: <span class="color-control" data-field="color"><span class="current-color" title="{{status.color}}" style="background-color: {{status.color}}"> </span> <button class="change-color"><&|/l&>Change</&></button></span><br>
 
         Add Transition:

commit 2ce3c325523e07be4e9ab794020996d6631e7526
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Fri Sep 8 15:51:37 2017 +0000

    Improve status transition menu to use superfish

diff --git a/html/Elements/LifecycleInspectorStatus b/html/Elements/LifecycleInspectorStatus
index 206fd3b..8aec7eb 100644
--- a/html/Elements/LifecycleInspectorStatus
+++ b/html/Elements/LifecycleInspectorStatus
@@ -11,21 +11,25 @@
         <span title="<&|/l&>Can this status be selected on creation?</&>">Creation<span class="hint" >[?]</span>: <input type="checkbox" name="creation" {{#if status.creation}}checked=checked{{/if}}></span><br>
         Color: <span class="color-control" data-field="color"><span class="current-color" title="{{status.color}}" style="background-color: {{status.color}}"> </span> <button class="change-color"><&|/l&>Change</&></button></span><br>
 
-        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}}"><&|/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}}"><&|/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 class="toplevel">
+          <li><a href="javascript:void(0)" class="menu-item">Add Transition...</a>
+            <ul>
+            {{#each lifecycle.statuses}}
+              <li class="{{#if (canAddTransition ../status.name this ../lifecycle)}}{{else}}hidden{{/if}}"><a href="javascript:void(0)" class="menu-item add-transition" data-from="{{../status.name}}" data-to="{{this}}"><&|/l, "{{this}}"&>to [_1]</&></a></li>
+            {{/each}}
+            </ul>
+          <li><a href="javascript:void(0)" class="menu-item">Select Transition...</a>
+            <ul>
+              {{#each lifecycle.statuses}}
+                <li class="{{#if (canSelectTransition ../status.name this ../lifecycle)}}{{else}}hidden{{/if}}"><a href="#" class="menu-item 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="menu-item select-transition" data-to="{{../status.name}}" data-from="{{this}}"><&|/l, "{{this}}"&>from [_1]</&></a></li>
+              {{/each}}
+            </ul>
+          </li>
         </ul>
+
         <button class="delete"><&|/l&>Delete Status</&></button>
     </div>
 </script>

commit f7bbd0fbaa4f721d64f6e0423cf130c3d8e603b2
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Fri Sep 8 16:02:25 2017 +0000

    Always render a text background rect

diff --git a/static/css/lifecycleui-editor.css b/static/css/lifecycleui-editor.css
index 741d1c8..f8809ea 100644
--- a/static/css/lifecycleui-editor.css
+++ b/static/css/lifecycleui-editor.css
@@ -74,10 +74,13 @@
     opacity: 1;
 }
 
-.lifecycle-ui.editing svg.has-focus .decorations .text-background {
+.lifecycle-ui.editing .decorations .text-background {
     stroke: none;
+    fill: none;
+}
+
+.lifecycle-ui.editing svg.has-focus .decorations .focus.text-background {
     fill: white;
-    opacity: 1;
     filter: url(#focus);
 }
 
diff --git a/static/js/lifecycleui-editor.js b/static/js/lifecycleui-editor.js
index 2b94259..959f2c8 100644
--- a/static/js/lifecycleui-editor.js
+++ b/static/js/lifecycleui-editor.js
@@ -309,36 +309,47 @@ jQuery(function () {
         this.renderDisplay();
     };
 
-    // add a rect under the focused text decoration for highlighting
+    // add rects under text decorations for highlighting
     Editor.prototype.renderTextDecorations = function (initial) {
         Super.prototype.renderTextDecorations.call(this, initial);
         var self = this;
 
-        if (!self._focusItem || self._focusItem._type != 'text') {
-            self.decorationContainer.selectAll("rect")
-                .data([])
-                .exit()
-                .remove();
-            return;
-        }
+        self.renderTextDecorationBackgrounds(initial);
+    };
 
-        var d = self._focusItem;
-        var label = self.decorationContainer.select("text[data-key='"+d._key+"']");
-        var rect = label.node().getBoundingClientRect();
-        var width = rect.width;
-        var height = rect.height;
-        var padding = 5;
+    Editor.prototype.renderTextDecorationBackgrounds = function (initial) {
+        var self = this;
+        var rects = self.decorationContainer.selectAll("rect.text-background")
+                         .data(self.lifecycle.decorations.text, function (d) { return d._key });
 
-        var background = self.decorationContainer.selectAll("rect")
-                             .data([d], function (d) { return d._key });
+        rects.exit()
+            .classed("removing", true)
+            .transition().duration(200)
+              .remove();
 
-        background.enter().insert("rect", ":first-child")
+        rects.enter().insert("rect", ":first-child")
+                     .attr("data-key", function (d) { return d._key })
                      .classed("text-background", true)
-              .merge(background)
-                     .attr("x", self.xScale(d.x)-padding)
-                     .attr("y", self.yScale(d.y)-height-padding)
-                     .attr("width", width+padding*2)
-                     .attr("height", height+padding*2)
+                     .on("click", function (d) {
+                         d3.event.stopPropagation();
+                         self.clickedDecoration(d);
+                     })
+                     .call(function (rects) { self.didEnterTextDecorations(rects) })
+              .merge(rects)
+                      .classed("focus", function (d) { return self.isFocused(d) })
+                      .each(function (d) {
+                          var rect = d3.select(this);
+                          var label = self.decorationContainer.select("text[data-key='"+d._key+"']");
+                          var bbox = label.node().getBoundingClientRect();
+                          var width = bbox.width;
+                          var height = bbox.height;
+                          var padding = 5;
+
+                          rect.attr("x", self.xScale(d.x)-padding)
+                              .attr("y", self.yScale(d.y)-height-padding)
+                              .attr("width", width+padding*2)
+                              .attr("height", height+padding*2);
+                      });
     };
 
     Editor.prototype.renderPolygonDecorations = function (initial) {

commit 3e9de82b31c76628d8b38084c55d8d69a7386f84
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Fri Sep 8 16:16:06 2017 +0000

    Highlight on hover

diff --git a/html/Elements/LifecycleGraphExtras b/html/Elements/LifecycleGraphExtras
index a46f56f..ad954b0 100644
--- a/html/Elements/LifecycleGraphExtras
+++ b/html/Elements/LifecycleGraphExtras
@@ -21,3 +21,14 @@
   </feMerge>
 </filter>
 
+<filter id="hover" x="-100%" y="-100%" height="300%" width="300%">
+  <feFlood result="flood" flood-color="#80A8FF" flood-opacity="1"></feFlood>
+  <feComposite in="flood" result="mask" in2="SourceGraphic" operator="in"></feComposite>
+  <feMorphology in="mask" result="dilated" operator="dilate" radius="5"></feMorphology>
+  <feGaussianBlur in="dilated" result="blurred" stdDeviation="3"></feGaussianBlur>
+  <feMerge>
+    <feMergeNode in="blurred"></feMergeNode>
+    <feMergeNode in="SourceGraphic"></feMergeNode>
+  </feMerge>
+</filter>
+
diff --git a/static/css/lifecycleui-editor.css b/static/css/lifecycleui-editor.css
index f8809ea..b76e4b2 100644
--- a/static/css/lifecycleui-editor.css
+++ b/static/css/lifecycleui-editor.css
@@ -122,3 +122,13 @@
     left: 175px;
 }
 
+.lifecycle-ui .statuses circle.hover,
+.lifecycle-ui .transitions .hover,
+.lifecycle-ui .decorations :not(text).hover {
+    opacity: 1;
+    filter: url(#hover);
+}
+
+.lifecycle-ui.editing .decorations .hover.text-background {
+    fill: white;
+}
diff --git a/static/js/lifecycleui-editor.js b/static/js/lifecycleui-editor.js
index 959f2c8..5857017 100644
--- a/static/js/lifecycleui-editor.js
+++ b/static/js/lifecycleui-editor.js
@@ -190,6 +190,12 @@ jQuery(function () {
             self.focusItem(d);
         });
 
+        inspector.on('mouseenter', 'a.select-status', function (e) {
+            var statusName = jQuery(this).data('name');
+            var d = self.lifecycle.statusObjectForName(statusName);
+            self.hoverItem(d);
+        });
+
         inspector.on('click', 'a.select-transition', function (e) {
             e.preventDefault();
             var button = jQuery(this);
@@ -200,6 +206,15 @@ jQuery(function () {
             self.focusItem(d);
         });
 
+        inspector.on('mouseenter', 'a.select-transition', function (e) {
+            var button = jQuery(this);
+            var fromStatus = button.data('from');
+            var toStatus   = button.data('to');
+
+            var d = self.lifecycle.hasTransition(fromStatus, toStatus);
+            self.hoverItem(d);
+        });
+
         inspector.on('click', 'a.select-decoration', function (e) {
             e.preventDefault();
             var key = jQuery(this).data('key');
@@ -207,6 +222,16 @@ jQuery(function () {
             self.focusItem(d);
         });
 
+        inspector.on('mouseenter', 'a.select-decoration', function (e) {
+            var key = jQuery(this).data('key');
+            var d = self.lifecycle.itemForKey(key);
+            self.hoverItem(d);
+        });
+
+        inspector.on('mouseleave', 'a.select-status, a.select-transition, a.select-decoration', function () {
+            self.hoverItem(null);
+        });
+
         inspector.on('click', '.add-status', function (e) {
             e.preventDefault();
             self.addNewStatus();
@@ -509,6 +534,7 @@ jQuery(function () {
         Super.prototype.defocus.call(this);
         this.setInspectorContent(null);
         this.removePointHandles();
+        this.hoverItem(null);
         this.renderDisplay();
     };
 
@@ -523,5 +549,13 @@ jQuery(function () {
         this.renderDisplay();
     };
 
+    Editor.prototype.hoverItem = function (item) {
+        this.svg.selectAll(".hover").classed('hover', false);
+
+        if (item) {
+            this.svg.selectAll("*[data-key='"+item._key+"']").classed('hover', true);
+        }
+    };
+
     RT.LifecycleEditor = Editor;
 });

commit 54d36bcd20035d4c33854102a0b4cb3195313a47
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Fri Sep 8 16:28:21 2017 +0000

    Add select transition from/to menu

diff --git a/html/Elements/LifecycleInspectorCanvas b/html/Elements/LifecycleInspectorCanvas
index b64410a..602e702 100644
--- a/html/Elements/LifecycleInspectorCanvas
+++ b/html/Elements/LifecycleInspectorCanvas
@@ -78,6 +78,23 @@
               {{/each}}
               </ul>
            </li>
+          <li class="has-children"><a href="javascript:void(0)" class="menu-item">Select Transition...</a>
+              <ul>
+              {{#each lifecycle.statuses}}
+              <li class="has-children"><a href="javascript:void(0)" class="menu-item select-status" data-name="{{this}}"><&|/l, "{{this}}"&>from [_1]</&></a>
+                <ul>
+                  {{#with this as |from|}}
+                  {{#with ../lifecycle as |lc|}}
+                  {{#each lc.statuses}}
+                    <li class="menu-item {{#if (canSelectTransition from this lc)}}{{else}}hidden{{/if}}"><a href="#" class="menu-item select-transition" data-from="{{from}}" data-to="{{this}}"><&|/l, "{{this}}"&>to [_1]</&></a></li>
+                  {{/each}}
+                  {{/with}}
+                  {{/with}}
+                </ul>
+              </li>
+              {{/each}}
+              </ul>
+           </li>
            <li class="has-children"><a href="javascript:void(0)" class="menu-item">Select Decoration...</a>
               <ul>
               {{#each lifecycle.decorations.text}}

commit 3b276849ea7979ce874e8332f25b2bfea7f20a9e
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Fri Sep 8 16:36:36 2017 +0000

    Scaffolding for mappings UI

diff --git a/html/Admin/Lifecycles/Mappings.html b/html/Admin/Lifecycles/Mappings.html
new file mode 100644
index 0000000..cc18044
--- /dev/null
+++ b/html/Admin/Lifecycles/Mappings.html
@@ -0,0 +1,31 @@
+<& /Admin/Elements/Header, Title => $title &>
+<& /Elements/Tabs &>
+<& /Elements/ListActions, actions => \@results &>
+
+<form action="<%RT->Config->Get('WebPath')%>/Admin/Lifecycles/Mappings.html" name="ModifyMappings" method="post" enctype="multipart/form-data">
+<input type="hidden" class="hidden" name="Name" value="<% $LifecycleObj->Name %>" />
+<input type="hidden" class="hidden" name="Type" value="<% $LifecycleObj->Type %>" />
+
+<& /Elements/Submit, Label => loc('Save Changes') &>
+
+</form>
+<%INIT>
+my ($title, @results);
+my $LifecycleObj = RT::Lifecycle->new();
+$LifecycleObj->Load(Name => $Name, Type => $Type);
+
+Abort("Invalid lifecycle") unless $LifecycleObj->Name
+                               && $LifecycleObj->{data}{type} eq $Type;
+
+$title = loc("Modify lifecycle [_1]", $LifecycleObj->Name);
+
+# This code does automatic redirection if any updates happen.
+MaybeRedirectForResults(
+    Actions   => \@results,
+    Arguments => { Name => $LifecycleObj->Name, Type => $LifecycleObj->Type },
+);
+</%INIT>
+<%ARGS>
+$Name => undef
+$Type => undef
+</%ARGS>
diff --git a/html/Callbacks/RT-Extension-LifecycleUI/Elements/Tabs/Privileged b/html/Callbacks/RT-Extension-LifecycleUI/Elements/Tabs/Privileged
index f2388ac..ea9bd5f 100644
--- a/html/Callbacks/RT-Extension-LifecycleUI/Elements/Tabs/Privileged
+++ b/html/Callbacks/RT-Extension-LifecycleUI/Elements/Tabs/Privileged
@@ -34,6 +34,7 @@ if ( $Path =~ m{^/Admin/Lifecycles} ) {
 
             my $menu = PageMenu();
             $menu->child( basics => title => loc('Modify'),  path => "/Admin/Lifecycles/Modify.html?Type=" . $Type_uri . "&Name=" . $Name_uri );
+            $menu->child( mappings => title => loc('Mappings'),  path => "/Admin/Lifecycles/Mappings.html?Type=" . $Type_uri . "&Name=" . $Name_uri );
         }
     }
     else {

commit f058230e7537f1f2724cace1aef2e0789e0803ae
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Fri Sep 8 17:39:04 2017 +0000

    Copy the initial points
    
    Otherwise adding multiple polygons will result in their point handles being
    linked

diff --git a/static/js/lifecycleui-model.js b/static/js/lifecycleui-model.js
index f196f51..5d119fe 100644
--- a/static/js/lifecycleui-model.js
+++ b/static/js/lifecycleui-model.js
@@ -604,7 +604,7 @@ jQuery(function () {
             renderFill: true,
             x: 0.5,
             y: 0.5,
-            points: this._initialPointsForPolygon[type]
+            points: JSON.parse(JSON.stringify(this._initialPointsForPolygon[type]))
         };
         this.decorations.polygon.push(item);
         this._keyMap[item._key] = item;

commit 0653f4538d4d6576b70b06d76f54ec3b3c3927f2
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Fri Sep 8 17:43:43 2017 +0000

    Put point handles in transform container
    
    This way they render over top of everything else

diff --git a/static/css/lifecycleui-editor.css b/static/css/lifecycleui-editor.css
index b76e4b2..ac972a0 100644
--- a/static/css/lifecycleui-editor.css
+++ b/static/css/lifecycleui-editor.css
@@ -65,7 +65,6 @@
 }
 
 .lifecycle-ui.editing svg.has-focus .decorations .focus,
-.lifecycle-ui.editing svg.has-focus .decorations .point-handle,
 .lifecycle-ui.editing svg.has-focus .transitions .focus {
     opacity: 1;
 }
diff --git a/static/js/lifecycleui-editor.js b/static/js/lifecycleui-editor.js
index 5857017..e4fe8b7 100644
--- a/static/js/lifecycleui-editor.js
+++ b/static/js/lifecycleui-editor.js
@@ -381,7 +381,7 @@ jQuery(function () {
         Super.prototype.renderPolygonDecorations.call(this, initial);
 
         var self = this;
-        var handles = self.decorationContainer.selectAll("circle.point-handle")
+        var handles = self.transformContainer.selectAll("circle.point-handle")
                            .data(self.pointHandles || [], function (d) { return d._key });
 
         handles.exit()

commit d54601dc9b0fa5a6b2683f60353488f8a11d37a0
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Fri Sep 8 18:08:13 2017 +0000

    First pass at lifecycle mapping UI

diff --git a/html/Admin/Lifecycles/Mappings.html b/html/Admin/Lifecycles/Mappings.html
index cc18044..0118fa2 100644
--- a/html/Admin/Lifecycles/Mappings.html
+++ b/html/Admin/Lifecycles/Mappings.html
@@ -6,7 +6,37 @@
 <input type="hidden" class="hidden" name="Name" value="<% $LifecycleObj->Name %>" />
 <input type="hidden" class="hidden" name="Type" value="<% $LifecycleObj->Type %>" />
 
-<& /Elements/Submit, Label => loc('Save Changes') &>
+% for my $Other (@lifecycles) {
+% my $FromMapping = $Other->MoveMap($LifecycleObj);
+% my $ToMapping = $LifecycleObj->MoveMap($Other);
+% my @OtherStatuses = $Other->Valid;
+
+    <&| /Widgets/TitleBox, title => $Other->Name &>
+
+<h3><&|/l, $Other->Name, $LifecycleObj->Name &>Changing from [_1] to [_2]:</&></h3>
+<table>
+% for my $OtherStatus (@OtherStatuses) {
+  <tr>
+    <td><% $OtherStatus %>:</td>
+    <td><& /Elements/SelectStatus, Statuses => \@MyStatuses, Default => $FromMapping->{$OtherStatus}, Name => 'map-' . $Other->Name . '-' . $OtherStatus . '--' . $LifecycleObj->Name&></td>
+  </tr>
+% }
+</table>
+
+<h3><&|/l, $LifecycleObj->Name, $Other->Name &>Changing from [_1] to [_2]:</&></h3>
+<table>
+% for my $MyStatus (@MyStatuses) {
+  <tr>
+    <td><% $MyStatus %>:</td>
+    <td><& /Elements/SelectStatus, Statuses => \@OtherStatuses, Default => $ToMapping->{$MyStatus}, Name => 'map-' . $LifecycleObj->Name . '-' . $MyStatus . '--' . $Other->Name &></td>
+  </tr>
+% }
+</table>
+
+    </&>
+% }
+
+<& /Elements/Submit, Name => 'Update', Label => loc('Save Changes') &>
 
 </form>
 <%INIT>
@@ -17,15 +47,42 @@ $LifecycleObj->Load(Name => $Name, Type => $Type);
 Abort("Invalid lifecycle") unless $LifecycleObj->Name
                                && $LifecycleObj->{data}{type} eq $Type;
 
-$title = loc("Modify lifecycle [_1]", $LifecycleObj->Name);
+my @MyStatuses = $LifecycleObj->Valid;
+
+$title = loc("Lifecycle [_1] Mappings", $LifecycleObj->Name);
 
 # This code does automatic redirection if any updates happen.
 MaybeRedirectForResults(
     Actions   => \@results,
     Arguments => { Name => $LifecycleObj->Name, Type => $LifecycleObj->Type },
 );
+
+my @lifecycle_names = grep { $_ ne 'approvals' } RT::Lifecycle->ListAll($Type);
+
+if ($Update) {
+    my %maps;
+    my $lifecycle_re = join '|', map { quotemeta($_) } @lifecycle_names;
+    for my $key (keys %ARGS) {
+        my ($from_lifecycle, $from_status, $to_lifecycle) = $key =~ /^map-($lifecycle_re)-(.*)--($lifecycle_re)$/ or next;
+        if (my $to_status = $ARGS{$key}) {
+            $maps{"$from_lifecycle -> $to_lifecycle"}{$from_status} = $to_status;
+        }
+    }
+
+    my ($ok, $msg) = RT::Extension::LifecycleUI->UpdateMaps(
+        CurrentUser  => $session{CurrentUser},
+        Maps         => \%maps,
+    );
+    push @results, $msg;
+}
+
+my @lifecycles = map { RT::Lifecycle->Load(Name => $_, Type => $Type) }
+                 sort { loc($a) cmp loc($b) }
+                 grep { $_ ne $Name }
+                 @lifecycle_names;
 </%INIT>
 <%ARGS>
 $Name => undef
 $Type => undef
+$Update => undef
 </%ARGS>
diff --git a/lib/RT/Extension/LifecycleUI.pm b/lib/RT/Extension/LifecycleUI.pm
index 0bade1a..bac5221 100644
--- a/lib/RT/Extension/LifecycleUI.pm
+++ b/lib/RT/Extension/LifecycleUI.pm
@@ -156,6 +156,28 @@ sub UpdateLifecycle {
     return (1, $CurrentUser->loc("Lifecycle [_1] updated", $name));
 }
 
+sub UpdateMaps {
+    my $class = shift;
+    my %args = (
+        CurrentUser  => undef,
+        Maps         => undef,
+        @_,
+    );
+
+    my $CurrentUser = $args{CurrentUser};
+    my $lifecycles = RT->Config->Get('Lifecycles');
+
+    %{ $lifecycles->{__maps__} } = (
+        %{ $lifecycles->{__maps__} || {} },
+        %{ $args{Maps} },
+    );
+
+    my ($ok, $msg) = $class->_SaveLifecycles($lifecycles, $CurrentUser);
+    return ($ok, $msg) if !$ok;
+
+    return (1, $CurrentUser->loc("Lifecycle mappings updated"));
+}
+
 =head1 NAME
 
 RT-Extension-LifecycleUI - manage lifecycles via admin UI

commit 5b583b6e365890177574285ee8eaa10b7f4552c5
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Fri Sep 8 18:10:58 2017 +0000

    Hide "Lifecycle created" or "Lifecycle updated" message
    
    It's valuable screen real estate

diff --git a/static/js/lifecycleui-editor.js b/static/js/lifecycleui-editor.js
index e4fe8b7..4ac57a4 100644
--- a/static/js/lifecycleui-editor.js
+++ b/static/js/lifecycleui-editor.js
@@ -528,6 +528,10 @@ jQuery(function () {
             d3.select(node).select('button.redo').classed('invisible', !self.lifecycle.hasRedoStack());
         };
         self.lifecycle.undoStateChangedCallback();
+
+        setTimeout(function () {
+            jQuery('.results').slideUp();
+        }, 10*1000);
     };
 
     Editor.prototype.defocus = function () {

commit 0c6f9ac75c0f802e00b6f67d7c2d35334d79ea98
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Fri Sep 8 18:49:36 2017 +0000

    Create objects at center of current viewport

diff --git a/static/js/lifecycleui-editor.js b/static/js/lifecycleui-editor.js
index 4ac57a4..22ea669 100644
--- a/static/js/lifecycleui-editor.js
+++ b/static/js/lifecycleui-editor.js
@@ -469,23 +469,34 @@ jQuery(function () {
         circles.call(this._createDrag());
     };
 
+    Editor.prototype.viewportCenterPoint = function () {
+        var rect = this.svg.node().getBoundingClientRect();
+        var x = (rect.width / 2 - this._currentZoom.x)/this._currentZoom.k;
+        var y = (rect.height / 2 - this._currentZoom.y)/this._currentZoom.k;
+        return [this.xScaleInvert(x), this.yScaleInvert(y)];
+    };
+
     Editor.prototype.addNewStatus = function () {
-        var status = this.lifecycle.createStatus();
+        var p = this.viewportCenterPoint();
+        var status = this.lifecycle.createStatus(p[0], p[1]);
         this.focusItem(status);
     };
 
     Editor.prototype.addNewTextDecoration = function () {
-        var text = this.lifecycle.createTextDecoration();
+        var p = this.viewportCenterPoint();
+        var text = this.lifecycle.createTextDecoration(p[0], p[1]);
         this.focusItem(text);
     };
 
     Editor.prototype.addNewPolygonDecoration = function (type) {
-        var polygon = this.lifecycle.createPolygonDecoration(type);
+        var p = this.viewportCenterPoint();
+        var polygon = this.lifecycle.createPolygonDecoration(p[0], p[1], type);
         this.focusItem(polygon);
     };
 
     Editor.prototype.addNewCircleDecoration = function () {
-        var circle = this.lifecycle.createCircleDecoration();
+        var p = this.viewportCenterPoint();
+        var circle = this.lifecycle.createCircleDecoration(p[0], p[1]);
         this.focusItem(circle);
     };
 
diff --git a/static/js/lifecycleui-model.js b/static/js/lifecycleui-model.js
index 5d119fe..8c8324e 100644
--- a/static/js/lifecycleui-model.js
+++ b/static/js/lifecycleui-model.js
@@ -546,7 +546,7 @@ jQuery(function () {
         point.y = y;
     };
 
-    Lifecycle.prototype.createStatus = function () {
+    Lifecycle.prototype.createStatus = function (x, y) {
         this._saveUndoEntry();
 
         var name;
@@ -565,8 +565,8 @@ jQuery(function () {
             _type: 'status',
             name:  name,
             type:  'initial',
-            x:     0.5,
-            y:     0.5,
+            x:     x,
+            y:     y
         };
         item.color = defaultColors(item._key);
 
@@ -575,22 +575,22 @@ jQuery(function () {
         return item;
     };
 
-    Lifecycle.prototype.createTextDecoration = function () {
+    Lifecycle.prototype.createTextDecoration = function (x, y) {
         this._saveUndoEntry();
 
         var item = {
             _key: _ELEMENT_KEY_SEQ++,
             _type: 'text',
             text:  'New label',
-            x:     0.5,
-            y:     0.5,
+            x:     x,
+            y:     y
         };
         this.decorations.text.push(item);
         this._keyMap[item._key] = item;
         return item;
     };
 
-    Lifecycle.prototype.createPolygonDecoration = function (type) {
+    Lifecycle.prototype.createPolygonDecoration = function (x, y, type) {
         this._saveUndoEntry();
 
         var item = {
@@ -602,8 +602,8 @@ jQuery(function () {
             strokeStyle: 'solid',
             fill: '#ffffff',
             renderFill: true,
-            x: 0.5,
-            y: 0.5,
+            x: x,
+            y: y,
             points: JSON.parse(JSON.stringify(this._initialPointsForPolygon[type]))
         };
         this.decorations.polygon.push(item);
@@ -611,7 +611,7 @@ jQuery(function () {
         return item;
     };
 
-    Lifecycle.prototype.createCircleDecoration = function () {
+    Lifecycle.prototype.createCircleDecoration = function (x, y) {
         this._saveUndoEntry();
 
         var item = {
@@ -623,8 +623,8 @@ jQuery(function () {
             strokeStyle: 'solid',
             fill: '#ffffff',
             renderFill: true,
-            x: 0.5,
-            y: 0.5,
+            x: x,
+            y: y,
             r: 35
         };
         this.decorations.circle.push(item);
diff --git a/static/js/lifecycleui-viewer.js b/static/js/lifecycleui-viewer.js
index 3a81e9f..03c87f4 100644
--- a/static/js/lifecycleui-viewer.js
+++ b/static/js/lifecycleui-viewer.js
@@ -32,6 +32,7 @@ jQuery(function () {
     };
 
     Viewer.prototype.didZoom = function () {
+        this._currentZoom = d3.event.transform;
         this.transformContainer.attr("transform", d3.event.transform);
     };
 
@@ -393,7 +394,7 @@ jQuery(function () {
         self._yScale = self.createScale(self.height, self.padding);
         self._xScaleZero = self.createScale(self.width, 0);
         self._yScaleZero = self.createScale(self.height, 0);
-        self._zoomIdentity = d3.zoomIdentity;
+        self._zoomIdentity = self._currentZoom = d3.zoomIdentity;
 
         self.lifecycle = new RT.Lifecycle(name);
         self.lifecycle.initializeFromConfig(config);

commit 86b6519b2d2052a4648ea4a2f0404bec03f18f65
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Fri Sep 8 18:50:01 2017 +0000

    Take radius as input parameter for creating circles

diff --git a/static/js/lifecycleui-editor.js b/static/js/lifecycleui-editor.js
index 22ea669..950bab0 100644
--- a/static/js/lifecycleui-editor.js
+++ b/static/js/lifecycleui-editor.js
@@ -496,7 +496,7 @@ jQuery(function () {
 
     Editor.prototype.addNewCircleDecoration = function () {
         var p = this.viewportCenterPoint();
-        var circle = this.lifecycle.createCircleDecoration(p[0], p[1]);
+        var circle = this.lifecycle.createCircleDecoration(p[0], p[1], self.statusCircleRadius);
         this.focusItem(circle);
     };
 
diff --git a/static/js/lifecycleui-model.js b/static/js/lifecycleui-model.js
index 8c8324e..4e7a863 100644
--- a/static/js/lifecycleui-model.js
+++ b/static/js/lifecycleui-model.js
@@ -611,7 +611,7 @@ jQuery(function () {
         return item;
     };
 
-    Lifecycle.prototype.createCircleDecoration = function (x, y) {
+    Lifecycle.prototype.createCircleDecoration = function (x, y, r) {
         this._saveUndoEntry();
 
         var item = {
@@ -625,7 +625,7 @@ jQuery(function () {
             renderFill: true,
             x: x,
             y: y,
-            r: 35
+            r: r
         };
         this.decorations.circle.push(item);
         this._keyMap[item._key] = item;

commit fca968ad066e92cb6dfbe82b645bbd01226ba4d0
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Fri Sep 8 18:58:08 2017 +0000

    Give lines an x,y

diff --git a/static/js/lifecycleui-editor.js b/static/js/lifecycleui-editor.js
index 950bab0..1e1e257 100644
--- a/static/js/lifecycleui-editor.js
+++ b/static/js/lifecycleui-editor.js
@@ -294,11 +294,7 @@ jQuery(function () {
                 _key: d._key + '-' + i,
                 i: i,
                 x: d.points[i].x,
-                y: d.points[i].y,
-                xScale: d._type == 'polygon' ? function (v) { return self.xScaleZero(v) } : function (v) { return self.xScale(v) },
-                yScale: d._type == 'polygon' ? function (v) { return self.yScaleZero(v) } : function (v) { return self.yScale(v) },
-                xScaleInvert: d._type == 'polygon' ? function (v) { return self.xScaleZeroInvert(v) } : function (v) { return self.xScaleInvert(v) },
-                yScaleInvert: d._type == 'polygon' ? function (v) { return self.yScaleZeroInvert(v) } : function (v) { return self.yScaleInvert(v) }
+                y: d.points[i].y
             });
         }
         self.pointHandles = points;
@@ -314,10 +310,10 @@ jQuery(function () {
     };
 
     Editor.prototype.didDragPointHandle = function (d, node) {
-        var x = d.xScaleInvert(d3.event.x);
-        var y = d.yScaleInvert(d3.event.y);
+        var x = this.xScaleZeroInvert(d3.event.x);
+        var y = this.yScaleZeroInvert(d3.event.y);
 
-        if (d.xScale(x) == d.xScale(d.x) && d.yScale(y) == d.yScale(d.y)) {
+        if (this.xScaleZero(x) == this.xScaleZero(d.x) && this.yScaleZero(y) == this.yScaleZero(d.y)) {
             return;
         }
 
@@ -390,15 +386,22 @@ jQuery(function () {
         handles.enter().append("circle")
                      .classed("point-handle", true)
                      .call(d3.drag()
-                         .subject(function (d) { return { x: d.xScale(d.x), y : d.yScale(d.y) } })
+                         .subject(function (d) { return { x: self.xScaleZero(d.x), y : self.yScaleZero(d.y) } })
                          .on("start", function (d) { self.didBeginDrag(d, this) })
                          .on("drag", function (d) { self.didDragPointHandle(d) })
                          .on("end", function (d) { self.didEndDrag(d, this) })
                      )
               .merge(handles)
-                     .attr("transform", function (d) { return self.inspectorNode._type == 'polygon' ? "translate(" + self.xScale(self.inspectorNode.x) + ", " + self.yScale(self.inspectorNode.y) + ")" : 'translate(0, 20)'})
-                     .attr("cx", function (d) { return d.xScale(d.x) })
-                     .attr("cy", function (d) { return d.yScale(d.y) })
+                     .attr("transform", function (d) {
+                         var x = self.xScale(self.inspectorNode.x);
+                         var y = self.yScale(self.inspectorNode.y);
+                         if (self.inspectorNode._type == 'line') {
+                             y += 20;
+                         }
+                         return "translate(" + x + ", " + y + ")";
+                     })
+                     .attr("cx", function (d) { return self.xScaleZero(d.x) })
+                     .attr("cy", function (d) { return self.yScaleZero(d.y) })
     };
 
     Editor.prototype.clickedStatus = function (d) {
@@ -501,7 +504,8 @@ jQuery(function () {
     };
 
     Editor.prototype.addNewLineDecoration = function () {
-        var line = this.lifecycle.createLineDecoration();
+        var p = this.viewportCenterPoint();
+        var line = this.lifecycle.createLineDecoration(p[0], p[1]);
         this.focusItem(line);
     };
 
diff --git a/static/js/lifecycleui-model.js b/static/js/lifecycleui-model.js
index 4e7a863..28d208d 100644
--- a/static/js/lifecycleui-model.js
+++ b/static/js/lifecycleui-model.js
@@ -17,6 +17,10 @@ jQuery(function () {
     };
 
     Lifecycle.prototype._initialPointsForPolygon = {
+        Line: [
+            {x: -.07, y: 0},
+            {x:  .07, y: 0},
+        ],
         Triangle: [
             {x:  .07, y: .2},
             {x:    0, y:  0},
@@ -632,7 +636,7 @@ jQuery(function () {
         return item;
     };
 
-    Lifecycle.prototype.createLineDecoration = function () {
+    Lifecycle.prototype.createLineDecoration = function (x, y) {
         this._saveUndoEntry();
 
         var item = {
@@ -642,7 +646,9 @@ jQuery(function () {
             style: 'solid',
             startMarker: 'none',
             endMarker: 'arrowhead',
-            points: [{x:0.4, y:0.5}, {x:0.6, y:0.5}]
+            x: x,
+            y: y,
+            points: JSON.parse(JSON.stringify(this._initialPointsForPolygon.Line))
         };
         this.decorations.line.push(item);
         this._keyMap[item._key] = item;
diff --git a/static/js/lifecycleui-viewer.js b/static/js/lifecycleui-viewer.js
index 03c87f4..e0364a9 100644
--- a/static/js/lifecycleui-viewer.js
+++ b/static/js/lifecycleui-viewer.js
@@ -277,10 +277,11 @@ jQuery(function () {
               .merge(lines)
                      .classed("dashed", function (d) { return d.style == 'dashed' })
                      .classed("dotted", function (d) { return d.style == 'dotted' })
-                     .attr("x1", function (d) { return self.xScale(d.points[0].x) })
-                     .attr("y1", function (d) { return self.yScale(d.points[0].y) })
-                     .attr("x2", function (d) { return self.xScale(d.points[1].x) })
-                     .attr("y2", function (d) { return self.yScale(d.points[1].y) })
+                     .attr("transform", function (d) { return "translate(" + self.xScale(d.x) + ", " + self.yScale(d.y) + ")" })
+                     .attr("x1", function (d) { return self.xScaleZero(d.points[0].x) })
+                     .attr("y1", function (d) { return self.yScaleZero(d.points[0].y) })
+                     .attr("x2", function (d) { return self.xScaleZero(d.points[1].x) })
+                     .attr("y2", function (d) { return self.yScaleZero(d.points[1].y) })
                      .classed("focus", function (d) { return self.isFocused(d) })
                      .attr("marker-start", function (d) { return d.startMarker == 'none' ? undefined : "url(#line_marker_" + d.startMarker + ")" })
                      .attr("marker-end", function (d) { return d.endMarker == 'none' ? undefined : "url(#line_marker_" + d.endMarker + ")" })

commit 2ba8415e627ed29bf9b275a0b82fa1b35c30b2af
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Fri Sep 8 18:58:41 2017 +0000

    Drag and drop for line decorations

diff --git a/static/js/lifecycleui-editor.js b/static/js/lifecycleui-editor.js
index 1e1e257..59aec0a 100644
--- a/static/js/lifecycleui-editor.js
+++ b/static/js/lifecycleui-editor.js
@@ -472,6 +472,10 @@ jQuery(function () {
         circles.call(this._createDrag());
     };
 
+    Editor.prototype.didEnterLineDecorations = function (lines) {
+        lines.call(this._createDrag());
+    };
+
     Editor.prototype.viewportCenterPoint = function () {
         var rect = this.svg.node().getBoundingClientRect();
         var x = (rect.width / 2 - this._currentZoom.x)/this._currentZoom.k;

commit 0debf62d56b9b269f69a8175e0ab8e0e2251702d
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Fri Sep 8 19:01:47 2017 +0000

    Adjust domain from 0,1 to 0,10000, and floor() on invert
    
    This should help with floating point accuracy issues

diff --git a/static/js/lifecycleui-model.js b/static/js/lifecycleui-model.js
index 28d208d..862eb83 100644
--- a/static/js/lifecycleui-model.js
+++ b/static/js/lifecycleui-model.js
@@ -18,19 +18,19 @@ jQuery(function () {
 
     Lifecycle.prototype._initialPointsForPolygon = {
         Line: [
-            {x: -.07, y: 0},
-            {x:  .07, y: 0},
+            {x: -700, y: 0},
+            {x:  700, y: 0},
         ],
         Triangle: [
-            {x:  .07, y: .2},
-            {x:    0, y:  0},
-            {x: -.06, y: .2}
+            {x:  700, y: 2000},
+            {x:   0, y:  0},
+            {x: -600, y: 2000}
         ],
         Rectangle: [
-            {x: -.06, y: -.06},
-            {x:  .06, y: -.06},
-            {x:  .06, y:  .06},
-            {x: -.06, y:  .06}
+            {x: -600, y: -600},
+            {x:  600, y: -600},
+            {x:  600, y:  600},
+            {x: -600, y:  600}
         ]
     };
 
@@ -73,8 +73,8 @@ jQuery(function () {
             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;
+                meta.x = 10000 * (Math.sin(2 * Math.PI * (i/statusCount)) + 1) / 2;
+                meta.y = 10000 * (Math.cos(2 * Math.PI * (i/statusCount)) + 1) / 2;
             };
 
             if (!meta.color) {
diff --git a/static/js/lifecycleui-viewer.js b/static/js/lifecycleui-viewer.js
index e0364a9..a691c1c 100644
--- a/static/js/lifecycleui-viewer.js
+++ b/static/js/lifecycleui-viewer.js
@@ -9,7 +9,7 @@ jQuery(function () {
 
     Viewer.prototype.createScale = function (size, padding) {
         return d3.scaleLinear()
-                 .domain([0, 1])
+                 .domain([0, 10000])
                  .range([padding, size - padding]);
     };
 
@@ -18,10 +18,10 @@ jQuery(function () {
     Viewer.prototype.yScale = function (y) { return this.gridScale(this._yScale(y)) };
     Viewer.prototype.xScaleZero = function (x) { return this.gridScale(this._xScaleZero(x)) };
     Viewer.prototype.yScaleZero = function (y) { return this.gridScale(this._yScaleZero(y)) };
-    Viewer.prototype.xScaleInvert = function (x) { return this._xScale.invert(x) };
-    Viewer.prototype.yScaleInvert = function (y) { return this._yScale.invert(y) };
-    Viewer.prototype.xScaleZeroInvert = function (x) { return this._xScaleZero.invert(x) };
-    Viewer.prototype.yScaleZeroInvert = function (y) { return this._yScaleZero.invert(y) };
+    Viewer.prototype.xScaleInvert = function (x) { return Math.floor(this._xScale.invert(x)) };
+    Viewer.prototype.yScaleInvert = function (y) { return Math.floor(this._yScale.invert(y)) };
+    Viewer.prototype.xScaleZeroInvert = function (x) { return Math.floor(this._xScaleZero.invert(x)) };
+    Viewer.prototype.yScaleZeroInvert = function (y) { return Math.floor(this._yScaleZero.invert(y)) };
 
     Viewer.prototype.addZoomBehavior = function () {
         var self = this;

commit a90d4b93c03a495ba50cc48e3014c0799d45ccc9
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Fri Sep 8 19:03:36 2017 +0000

    Fix label bounding box not scaling

diff --git a/static/js/lifecycleui-editor.js b/static/js/lifecycleui-editor.js
index 59aec0a..285bd33 100644
--- a/static/js/lifecycleui-editor.js
+++ b/static/js/lifecycleui-editor.js
@@ -362,9 +362,9 @@ jQuery(function () {
                           var rect = d3.select(this);
                           var label = self.decorationContainer.select("text[data-key='"+d._key+"']");
                           var bbox = label.node().getBoundingClientRect();
-                          var width = bbox.width;
-                          var height = bbox.height;
-                          var padding = 5;
+                          var width = bbox.width / self._currentZoom.k;
+                          var height = bbox.height / self._currentZoom.k;
+                          var padding = 5 / self._currentZoom.k;
 
                           rect.attr("x", self.xScale(d.x)-padding)
                               .attr("y", self.yScale(d.y)-height-padding)

commit 4f8527c9ee6fa2dbda16398b5ca7c6317fd94bc3
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Fri Sep 8 19:29:02 2017 +0000

    Implement multi-line text decorations

diff --git a/html/Elements/LifecycleInspectorText b/html/Elements/LifecycleInspectorText
index 7c7baae..d36f415 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">
-        Text: <input type="text" name="text" value="{{text.text}}" /><br><br>
+        Text: <textarea name="text" rows=5>{{text.text}}</textarea><br><br>
         Bold: <input type="checkbox" name="bold" {{#if text.bold}}checked=checked{{/if}}><br>
         Italic: <input type="checkbox" name="italic" {{#if text.italic}}checked=checked{{/if}}><br>
         <button class="delete"><&|/l&>Delete Text</&></button>
diff --git a/static/js/lifecycleui-editor.js b/static/js/lifecycleui-editor.js
index 285bd33..b7d01a8 100644
--- a/static/js/lifecycleui-editor.js
+++ b/static/js/lifecycleui-editor.js
@@ -367,7 +367,7 @@ jQuery(function () {
                           var padding = 5 / self._currentZoom.k;
 
                           rect.attr("x", self.xScale(d.x)-padding)
-                              .attr("y", self.yScale(d.y)-height-padding)
+                              .attr("y", self.yScale(d.y)-padding)
                               .attr("width", width+padding*2)
                               .attr("height", height+padding*2);
                       });
diff --git a/static/js/lifecycleui-viewer.js b/static/js/lifecycleui-viewer.js
index a691c1c..7fce6b4 100644
--- a/static/js/lifecycleui-viewer.js
+++ b/static/js/lifecycleui-viewer.js
@@ -171,6 +171,24 @@ jQuery(function () {
                       .classed("focus-to", function (d) { return self.isFocusedTransition(d, false) });
     };
 
+    Viewer.prototype._wrapTextDecoration = function (node, text) {
+        if (node.attr('data-text') == text) {
+            return;
+        }
+
+        var lines = text.split(/\n/),
+            lineHeight = 1.1;
+
+        if (node.attr('data-text')) {
+            node.selectAll("*").remove();
+        }
+        node.attr('data-text', text);
+
+        for (var i = 0; i < lines.length; ++i) {
+            node.append("tspan").attr("dy", (i+1) * lineHeight + "em").text(lines[i]);
+        }
+    };
+
     Viewer.prototype.renderTextDecorations = function (initial) {
         var self = this;
         var labels = self.decorationContainer.selectAll("text")
@@ -191,10 +209,13 @@ jQuery(function () {
               .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 })
                       .classed("bold", function (d) { return d.bold })
                       .classed("italic", function (d) { return d.italic })
                       .classed("focus", function (d) { return self.isFocused(d) })
+                      .each(function (d) { self._wrapTextDecoration(d3.select(this), d.text) })
+              .selectAll("tspan")
+                      .attr("x", function (d) { return self.xScale(d.x) })
+                      .attr("y", function (d) { return self.yScale(d.y) })
     };
 
     Viewer.prototype.renderPolygonDecorations = function (initial) {

commit 8dabf9ca82d8cc2c8c44e737eec2b11cde559200
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Fri Sep 8 19:40:02 2017 +0000

    Use a consistent padding across editor and viewer
    
    This is important because gridSize can cause a jump in position

diff --git a/static/js/lifecycleui-editor.js b/static/js/lifecycleui-editor.js
index b7d01a8..cd0c480 100644
--- a/static/js/lifecycleui-editor.js
+++ b/static/js/lifecycleui-editor.js
@@ -3,7 +3,6 @@ jQuery(function () {
 
     function Editor (container) {
         Super.call(this);
-        this.padding = this.statusCircleRadius * 2;
     };
     Editor.prototype = Object.create(Super.prototype);
 
diff --git a/static/js/lifecycleui-viewer.js b/static/js/lifecycleui-viewer.js
index 7fce6b4..5e07ab7 100644
--- a/static/js/lifecycleui-viewer.js
+++ b/static/js/lifecycleui-viewer.js
@@ -4,7 +4,7 @@ jQuery(function () {
         this.height = 500;
         this.statusCircleRadius = 35;
         this.gridSize = 10;
-        this.padding = this.statusCircleRadius;
+        this.padding = this.statusCircleRadius * 2;
     };
 
     Viewer.prototype.createScale = function (size, padding) {

commit f54ebb71151b05d0be77330bfb344686dae04a2c
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Fri Sep 8 19:47:26 2017 +0000

    Zoom out on ticket display

diff --git a/static/js/lifecycleui-viewer.js b/static/js/lifecycleui-viewer.js
index 5e07ab7..1b60a60 100644
--- a/static/js/lifecycleui-viewer.js
+++ b/static/js/lifecycleui-viewer.js
@@ -324,9 +324,10 @@ jQuery(function () {
 
     Viewer.prototype.centerOnItem = function (item, animated) {
         var rect = this.svg.node().getBoundingClientRect();
-        var x = rect.width/2 - this.xScale(item.x);
-        var y = rect.height/2 - this.yScale(item.y);
-        this._zoomIdentity = d3.zoomIdentity.translate(x, y);
+        var scale = this._zoomIdentityScale;
+        var x = rect.width/2 - this.xScale(item.x) * scale;
+        var y = rect.height/2 - this.yScale(item.y) * scale;
+        this._zoomIdentity = d3.zoomIdentity.translate(x, y).scale(this._zoomIdentityScale);
         this.resetZoom(animated);
     };
 
@@ -416,7 +417,13 @@ jQuery(function () {
         self._yScale = self.createScale(self.height, self.padding);
         self._xScaleZero = self.createScale(self.width, 0);
         self._yScaleZero = self.createScale(self.height, 0);
-        self._zoomIdentity = self._currentZoom = d3.zoomIdentity;
+
+        // zoom in a bit, but not too much
+        var scale = self.svg.node().getBoundingClientRect().width / self.width;
+        scale = scale ** .6;
+
+        self._zoomIdentityScale = scale;
+        self._zoomIdentity = self._currentZoom = d3.zoomIdentity.scale(self._zoomIdentityScale);
 
         self.lifecycle = new RT.Lifecycle(name);
         self.lifecycle.initializeFromConfig(config);

commit 5b7636420ac1c9558de6d1f325ff0a6d00688f58
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Fri Sep 8 20:04:55 2017 +0000

    Re-implement create lifecycle page

diff --git a/html/Admin/Lifecycles/Create.html b/html/Admin/Lifecycles/Create.html
index 097abcd..802b782 100644
--- a/html/Admin/Lifecycles/Create.html
+++ b/html/Admin/Lifecycles/Create.html
@@ -1,23 +1,36 @@
 <& /Admin/Elements/Header, Title => loc("Admin Lifecycles") &>
 <& /Elements/Tabs &>
+<& /Elements/ListActions, actions => \@results &>
 
 <form action="<%RT->Config->Get('WebPath')%>/Admin/Lifecycles/Create.html" name="CreateLifecycle" method="post" enctype="multipart/form-data">
 
-<&|/l&>New lifecycle name</&>: <input name="Name" value="" />
-<hr />
+<table>
+<tr><td align="right"><&|/l&>Lifecycle Name</&>:</td>
+<td><input name="Name" value="<% $Name %>" /></td>
+</tr>
 
+<tr><td align="right"><&|/l&>Type</&>:</td>
+<td><select name="Type">
 % for my $type (@types) {
-% my @lifecycles = @{ $lifecycles{$type} };
-<h2><&|/l, $type&>"[_1]" lifecycles</&></h2>
-% for my $lifecycle (@lifecycles) {
-<button type="submit" name="Clone" value="<% $type %>--<% $lifecycle %>"><&|/l, $lifecycle&>Clone "[_1]" lifecycle</&></button><br>
+<option value="<% $type %>" <% $type eq $Type ? "checked=checked" : "" %>><% loc($type) %></option>
 % }
-<br>
-<button type="submit" name="Blank" value="<% $type %>"><&|/l, $type&>Blank "[_1]" lifecycle</&></button><br>
+</select></td></tr>
+
+<tr><td align="right"><&|/l&>Clone Lifecycle</&>:</td><td>
+<label><input type="radio" name="Clone" value="" <% ($Clone//'') eq '' ? "checked=checked" : "" %> /> (none)</label>
+
+% for my $type (@types) {
+% for my $lifecycle (@{ $lifecycles{$type} }) {
+<label><input type="radio" name="Clone" value="<% $lifecycle %>" <% ($Clone//'') eq $lifecycle ? "checked=checked" : "" %> /> <% $lifecycle %></label><br>
 % }
-</form>
+% }
+</td></tr>
+</table>
 
+<& /Elements/Submit, Name => 'Create', Label => loc('Create') &>
 <%INIT>
+my @results;
+
 my @types = List::MoreUtils::uniq(
     'ticket',
     'asset',
@@ -32,34 +45,29 @@ for my $type (@types) {
                               RT::Lifecycle->ListAll($type);
 }
 
-if (defined($Clone) || defined($Blank)) {
-    my ($type, $clone_of);
-
-    if (defined($Clone)) {
-        ($type, $clone_of) = split '--', $Clone;
-    }
-    else {
-        $type = $Blank;
-    }
-
+if ($Create) {
     my ($ok, $msg) = RT::Extension::LifecycleUI->CreateLifecycle(
         CurrentUser => $session{CurrentUser},
         Name        => $Name,
-        Type        => $type,
-        Clone       => $clone_of,
+        Type        => $Type,
+        Clone       => $Clone,
     );
 
-    Abort($msg) if !$ok;
-
-    MaybeRedirectForResults(
-        Actions   => [ $msg ],
-        Path      => 'Admin/Lifecycles/Modify.html',
-        Arguments => { Type => $type, Name => $Name },
-    );
+    if ($ok) {
+        MaybeRedirectForResults(
+            Actions   => [ $msg ],
+            Path      => 'Admin/Lifecycles/Modify.html',
+            Arguments => { Type => $Type, Name => $Name },
+        );
+    }
+    else {
+        push @results, $msg if !$ok;
+    }
 }
 </%INIT>
 <%ARGS>
-$Clone => undef
-$Blank => undef
 $Name => undef
+$Type => 'ticket'
+$Clone => undef
+$Create => undef
 </%ARGS>

commit 6437939ec363baeb225c34a659bcdd32424c3b7c
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Fri Sep 8 20:12:40 2017 +0000

    <option> uses selected not checked

diff --git a/html/Admin/Lifecycles/Create.html b/html/Admin/Lifecycles/Create.html
index 802b782..7f73e87 100644
--- a/html/Admin/Lifecycles/Create.html
+++ b/html/Admin/Lifecycles/Create.html
@@ -12,7 +12,7 @@
 <tr><td align="right"><&|/l&>Type</&>:</td>
 <td><select name="Type">
 % for my $type (@types) {
-<option value="<% $type %>" <% $type eq $Type ? "checked=checked" : "" %>><% loc($type) %></option>
+<option value="<% $type %>" <% $type eq $Type ? "selected=selected" : "" %>><% loc($type) %></option>
 % }
 </select></td></tr>
 

commit f74fb3355daf56e5938d8e973f3b7e193a7ae991
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Fri Sep 8 20:12:49 2017 +0000

    Show/hide lifecycles based on type dropdown

diff --git a/html/Admin/Lifecycles/Create.html b/html/Admin/Lifecycles/Create.html
index 7f73e87..b2d25a5 100644
--- a/html/Admin/Lifecycles/Create.html
+++ b/html/Admin/Lifecycles/Create.html
@@ -20,14 +20,31 @@
 <label><input type="radio" name="Clone" value="" <% ($Clone//'') eq '' ? "checked=checked" : "" %> /> (none)</label>
 
 % for my $type (@types) {
+<div class="type" data-type="<% $type %>">
 % for my $lifecycle (@{ $lifecycles{$type} }) {
 <label><input type="radio" name="Clone" value="<% $lifecycle %>" <% ($Clone//'') eq $lifecycle ? "checked=checked" : "" %> /> <% $lifecycle %></label><br>
 % }
+</div>
 % }
 </td></tr>
 </table>
 
 <& /Elements/Submit, Name => 'Create', Label => loc('Create') &>
+
+<script type="text/javascript">
+jQuery(function () {
+    var showType = function (resetClone) {
+        var type = jQuery('select[name=Type]').val();
+        jQuery('.type').hide();
+        jQuery('.type[data-type="'+type+'"]').show();
+        if (resetClone) {
+            jQuery('input[name=Clone][value=""]').prop('checked', true);
+        }
+    };
+    showType(false);
+    jQuery('select[name=Type]').change(function () { showType(true) });
+});
+</script>
 <%INIT>
 my @results;
 

commit eccbfb727d04f9f206b77665d31bccabf34a22bb
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Fri Sep 8 20:47:52 2017 +0000

    Hover focus on status we're adding transition to

diff --git a/static/css/lifecycleui-editor.css b/static/css/lifecycleui-editor.css
index ac972a0..08393a9 100644
--- a/static/css/lifecycleui-editor.css
+++ b/static/css/lifecycleui-editor.css
@@ -121,6 +121,10 @@
     left: 175px;
 }
 
+.lifecycle-ui .statuses text.hover {
+    opacity: 1;
+}
+
 .lifecycle-ui .statuses circle.hover,
 .lifecycle-ui .transitions .hover,
 .lifecycle-ui .decorations :not(text).hover {
diff --git a/static/js/lifecycleui-editor.js b/static/js/lifecycleui-editor.js
index cd0c480..85411cc 100644
--- a/static/js/lifecycleui-editor.js
+++ b/static/js/lifecycleui-editor.js
@@ -195,6 +195,12 @@ jQuery(function () {
             self.hoverItem(d);
         });
 
+        inspector.on('mouseenter', 'a.add-transition', function (e) {
+            var statusName = jQuery(this).data('to');
+            var d = self.lifecycle.statusObjectForName(statusName);
+            self.hoverItem(d);
+        });
+
         inspector.on('click', 'a.select-transition', function (e) {
             e.preventDefault();
             var button = jQuery(this);
@@ -227,7 +233,7 @@ jQuery(function () {
             self.hoverItem(d);
         });
 
-        inspector.on('mouseleave', 'a.select-status, a.select-transition, a.select-decoration', function () {
+        inspector.on('mouseleave', 'a.select-status, a.add-transition, a.select-transition, a.select-decoration', function () {
             self.hoverItem(null);
         });
 

commit 2e82183c182e9d6be205c20db02488cf0e5cabfa
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Fri Sep 8 21:19:53 2017 +0000

    Rename Text to Label in the UI

diff --git a/html/Elements/LifecycleInspectorCanvas b/html/Elements/LifecycleInspectorCanvas
index 602e702..1c54de7 100644
--- a/html/Elements/LifecycleInspectorCanvas
+++ b/html/Elements/LifecycleInspectorCanvas
@@ -64,7 +64,7 @@
           <li><a href="javascript:void(0)" class="menu-item add-status"><&|/l&>Add Status</&></a></li>
           <li class="has-children"><a href="javascript:void(0)" class="menu-item">Add Decoration...</a>
               <ul>
-                  <li><a href="javascript:void(0)" class="menu-item add-text">Add Text</a></li>
+                  <li><a href="javascript:void(0)" class="menu-item add-text">Add Label</a></li>
                   <li><a href="javascript:void(0)" class="menu-item add-polygon" data-type="Triangle">Add Triangle</a></li>
                   <li><a href="javascript:void(0)" class="menu-item add-polygon" data-type="Rectangle">Add Rectangle</a></li>
                   <li><a href="javascript:void(0)" class="menu-item add-circle">Add Circle</a></li>
diff --git a/html/Elements/LifecycleInspectorText b/html/Elements/LifecycleInspectorText
index d36f415..27e40b0 100644
--- a/html/Elements/LifecycleInspectorText
+++ b/html/Elements/LifecycleInspectorText
@@ -3,6 +3,6 @@
         Text: <textarea name="text" rows=5>{{text.text}}</textarea><br><br>
         Bold: <input type="checkbox" name="bold" {{#if text.bold}}checked=checked{{/if}}><br>
         Italic: <input type="checkbox" name="italic" {{#if text.italic}}checked=checked{{/if}}><br>
-        <button class="delete"><&|/l&>Delete Text</&></button>
+        <button class="delete"><&|/l&>Delete Label</&></button>
     </div>
 </script>

commit 5b4e82c8955f5d5d8186bdd61dce7916bf2992ae
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Fri Sep 8 21:24:34 2017 +0000

    Automatically clear label placeholder text

diff --git a/html/Elements/LifecycleInspectorText b/html/Elements/LifecycleInspectorText
index 27e40b0..4481cd6 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">
-        Text: <textarea name="text" rows=5>{{text.text}}</textarea><br><br>
+        Text: <textarea name="text" rows=5 data-default="New label">{{text.text}}</textarea><br><br>
         Bold: <input type="checkbox" name="bold" {{#if text.bold}}checked=checked{{/if}}><br>
         Italic: <input type="checkbox" name="italic" {{#if text.italic}}checked=checked{{/if}}><br>
         <button class="delete"><&|/l&>Delete Label</&></button>
diff --git a/static/js/lifecycleui-editor.js b/static/js/lifecycleui-editor.js
index 85411cc..5a92bd1 100644
--- a/static/js/lifecycleui-editor.js
+++ b/static/js/lifecycleui-editor.js
@@ -289,6 +289,12 @@ jQuery(function () {
                 self.defocus();
             }
         });
+
+        inspector.on('focus', 'textarea[name=text]', function (e) {
+            if (jQuery(this).val() == jQuery(this).data('default')) {
+                jQuery(this).val("");
+            }
+        });
     };
 
     Editor.prototype.addPointHandles = function (d) {

commit 2286fdfe95e1c06f54c8ead1422c8f9f73510a04
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Fri Sep 8 21:29:43 2017 +0000

    Implement cloning for decorations

diff --git a/html/Elements/LifecycleInspectorCircle b/html/Elements/LifecycleInspectorCircle
index b435af1..72f0cfe 100644
--- a/html/Elements/LifecycleInspectorCircle
+++ b/html/Elements/LifecycleInspectorCircle
@@ -11,6 +11,7 @@
              </select>
         </div>
         Fill: <input type="checkbox" name="renderFill" {{#if circle.renderFill}}checked=checked{{/if}} data-show-hide=".color-control[data-field=fill]"> <span class="color-control" data-field="fill"><span class="current-color" title="{{circle.fill}}" style="background-color: {{circle.fill}}"> </span> <button class="change-color"><&|/l&>Change</&></button></span><br>
+        <button class="clone"><&|/l&>Clone Circle</&></button><br>
         <button class="delete"><&|/l&>Delete Circle</&></button>
     </div>
 </script>
diff --git a/html/Elements/LifecycleInspectorLine b/html/Elements/LifecycleInspectorLine
index 076cbc9..9a66c56 100644
--- a/html/Elements/LifecycleInspectorLine
+++ b/html/Elements/LifecycleInspectorLine
@@ -22,6 +22,7 @@
                  <option value="dotted"><&|/l&>dotted</&></option>
                  {{/select}}
              </select><br>
+        <button class="clone"><&|/l&>Clone Line</&></button><br>
         <button class="delete"><&|/l&>Delete Line</&></button>
     </div>
 </script>
diff --git a/html/Elements/LifecycleInspectorPolygon b/html/Elements/LifecycleInspectorPolygon
index bcc99dc..3d9c054 100644
--- a/html/Elements/LifecycleInspectorPolygon
+++ b/html/Elements/LifecycleInspectorPolygon
@@ -11,6 +11,7 @@
              </select>
         </div>
         Fill: <input type="checkbox" name="renderFill" {{#if polygon.renderFill}}checked=checked{{/if}} data-show-hide=".color-control[data-field=fill]"> <span class="color-control" data-field="fill"><span class="current-color" title="{{polygon.fill}}" style="background-color: {{polygon.fill}}"> </span> <button class="change-color"><&|/l&>Change</&></button></span><br>
+        <button class="clone"><&|/l&>Clone Polygon</&></button><br>
         <button class="delete"><&|/l&>Delete Polygon</&></button>
     </div>
 </script>
diff --git a/html/Elements/LifecycleInspectorText b/html/Elements/LifecycleInspectorText
index 4481cd6..652dba3 100644
--- a/html/Elements/LifecycleInspectorText
+++ b/html/Elements/LifecycleInspectorText
@@ -3,6 +3,7 @@
         Text: <textarea name="text" rows=5 data-default="New label">{{text.text}}</textarea><br><br>
         Bold: <input type="checkbox" name="bold" {{#if text.bold}}checked=checked{{/if}}><br>
         Italic: <input type="checkbox" name="italic" {{#if text.italic}}checked=checked{{/if}}><br>
+        <button class="clone"><&|/l&>Clone Label</&></button><br>
         <button class="delete"><&|/l&>Delete Label</&></button>
     </div>
 </script>
diff --git a/static/js/lifecycleui-editor.js b/static/js/lifecycleui-editor.js
index 5a92bd1..e25951a 100644
--- a/static/js/lifecycleui-editor.js
+++ b/static/js/lifecycleui-editor.js
@@ -156,6 +156,13 @@ jQuery(function () {
             }
         });
 
+        inspector.on('click', 'button.clone', function (e) {
+            e.preventDefault();
+            var p = self.viewportCenterPoint();
+            var clone = self.lifecycle.cloneItem(self.inspectorNode, p[0], p[1]);
+            self.focusItem(clone);
+        });
+
         inspector.on('click', 'button.add-action', function (e) {
             e.preventDefault();
             var action = lifecycle.createActionForTransition(self.inspectorNode);
@@ -514,7 +521,7 @@ jQuery(function () {
 
     Editor.prototype.addNewCircleDecoration = function () {
         var p = this.viewportCenterPoint();
-        var circle = this.lifecycle.createCircleDecoration(p[0], p[1], self.statusCircleRadius);
+        var circle = this.lifecycle.createCircleDecoration(p[0], p[1], this.statusCircleRadius);
         this.focusItem(circle);
     };
 
diff --git a/static/js/lifecycleui-model.js b/static/js/lifecycleui-model.js
index 862eb83..47e37b9 100644
--- a/static/js/lifecycleui-model.js
+++ b/static/js/lifecycleui-model.js
@@ -776,6 +776,25 @@ jQuery(function () {
         return frame;
     };
 
+    Lifecycle.prototype.cloneItem = function (source, x, y) {
+        this._saveUndoEntry();
+
+        var clone = JSON.parse(JSON.stringify(source));
+        clone._key = _ELEMENT_KEY_SEQ++;
+        clone.x = x;
+        clone.y = y;
+
+        if (clone._type == 'polygon' || clone._type == 'circle' || clone._type == 'line' || clone._type == 'text') {
+            this.decorations[clone._type].push(clone);
+        }
+        else {
+            console.error("Unhandled type for clone: " + clone._type);
+        }
+
+        this._keyMap[clone._key] = clone;
+        return clone;
+    };
+
     RT.Lifecycle = Lifecycle;
 });
 

commit 8a4a824c8e8e9e81913d52565ba9953fb0352d80
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Fri Sep 8 21:53:22 2017 +0000

    0.01 releng

diff --git a/MANIFEST b/MANIFEST
new file mode 100644
index 0000000..2f17a61
--- /dev/null
+++ b/MANIFEST
@@ -0,0 +1,47 @@
+html/Admin/Lifecycles/Create.html
+html/Admin/Lifecycles/index.html
+html/Admin/Lifecycles/Mappings.html
+html/Admin/Lifecycles/Modify.html
+html/Callbacks/RT-Extension-LifecycleUI/Elements/Tabs/Privileged
+html/Callbacks/RT-Extension-LifecycleUI/Ticket/Elements/ShowSummary/AfterReminders
+html/Elements/LifecycleGraph
+html/Elements/LifecycleGraphExtras
+html/Elements/LifecycleInspector
+html/Elements/LifecycleInspectorAction
+html/Elements/LifecycleInspectorCanvas
+html/Elements/LifecycleInspectorCircle
+html/Elements/LifecycleInspectorLine
+html/Elements/LifecycleInspectorPolygon
+html/Elements/LifecycleInspectorStatus
+html/Elements/LifecycleInspectorText
+html/Elements/LifecycleInspectorTransition
+html/Elements/LifecycleInteractive
+inc/Module/Install.pm
+inc/Module/Install/Base.pm
+inc/Module/Install/Can.pm
+inc/Module/Install/Fetch.pm
+inc/Module/Install/Include.pm
+inc/Module/Install/Makefile.pm
+inc/Module/Install/Metadata.pm
+inc/Module/Install/ReadmeFromPod.pm
+inc/Module/Install/RTx.pm
+inc/Module/Install/RTx/Runtime.pm
+inc/Module/Install/Win32.pm
+inc/Module/Install/WriteAll.pm
+inc/unicore/Name.pm
+inc/YAML/Tiny.pm
+lib/RT/Extension/LifecycleUI.pm
+Makefile.PL
+MANIFEST			This list of files
+META.yml
+README
+static/css/lifecycleui-editor.css
+static/css/lifecycleui-viewer-interactive.css
+static/css/lifecycleui-viewer.css
+static/css/lifecycleui.css
+static/js/d3.min.js
+static/js/handlebars-4.0.6.min.js
+static/js/lifecycleui-editor.js
+static/js/lifecycleui-model.js
+static/js/lifecycleui-viewer-interactive.js
+static/js/lifecycleui-viewer.js

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


More information about the Bps-public-commit mailing list