Add inplace edit for timeline name
authorDebarshi Bhaumik <deb1990@gmail.com>
Thu, 19 Apr 2018 07:19:36 +0000 (12:49 +0530)
committerColeman Watts <coleman@civicrm.org>
Tue, 15 May 2018 17:30:39 +0000 (13:30 -0400)
ang/crmCaseType.css
ang/crmCaseType.js
ang/crmCaseType/activitySetDetails.html [deleted file]
ang/crmCaseType/edit.html
tests/karma/unit/crmCaseTypeSpec.js

index d5fa1d62208771e935f0d93290cb2ae26d3b43a0..6352b3d24dc8716b3b870a69f452295596f092cf 100644 (file)
@@ -2,8 +2,14 @@
     vertical-align: middle;
     cursor: move;
 }
+
+.crmCaseType .fa-pencil {
+    margin: 0.2em 0.2em 0 0;
+    cursor: pointer;
+}
+
 .crmCaseType .fa-trash {
-    margin: 0.4em 0.2em 0 0;
+    margin: 0.56em 0.2em 0 0;
     cursor: pointer;
 }
 
index 2ae4b98b3f12c5bc57674516aca855e18972a463..ee9efb960306a0300db811fe211f5b17a779b9fe 100644 (file)
     };
   });
 
+  crmCaseType.directive('crmEditableTabTitle', function($timeout) {
+    return {
+      restrict: 'AE',
+      link: function(scope, element, attrs) {
+        element.addClass('crm-editable crm-editable-enabled');
+        var titleLabel = $(element).find('span');
+        var penIcon = $('<i class="crm-i fa-pencil crm-editable-placeholder"></i>').prependTo(element);
+        var saveButton = $('<button type="button"><i class="crm-i fa-check"></i></button>').appendTo(element);
+        var cancelButton = $('<button type="cancel"><i class="crm-i fa-times"></i></button>').appendTo(element);
+        $('button', element).wrapAll('<div class="crm-editable-form" style="display:none" />');
+        var buttons = $('.crm-editable-form', element);
+        titleLabel.on('click', startEditMode);
+        penIcon.on('click', startEditMode);
+
+        function detectEscapeKeyPress (event) {
+          var isEscape = false;
+
+          if ("key" in event) {
+              isEscape = (event.key == "Escape" || event.key == "Esc");
+          } else {
+              isEscape = (event.keyCode == 27);
+          }
+
+          return isEscape;
+        }
+
+        function detectEnterKeyPress (event) {
+          var isEnter = false;
+
+          if ("key" in event) {
+            isEnter = (event.key == "Enter");
+          } else {
+            isEnter = (event.keyCode == 13);
+          }
+
+          return isEnter;
+        }
+
+        function startEditMode () {
+          if (titleLabel.is(":focus")) {
+            return;
+          }
+
+          penIcon.hide();
+          buttons.show();
+
+          saveButton.click(function () {
+            updateTextValue();
+            stopEditMode();
+          });
+
+          cancelButton.click(function () {
+            revertTextValue();
+            stopEditMode();
+          });
+
+          $(element).addClass('crm-editable-editing');
+
+          titleLabel
+            .attr("contenteditable", "true")
+            .focus()
+            .focusout(function (event) {
+              $timeout(function () {
+                revertTextValue();
+                stopEditMode();
+              }, 500);
+            })
+            .keydown(function(event) {
+              event.stopImmediatePropagation();
+
+              if(detectEscapeKeyPress(event)) {
+                revertTextValue();
+                stopEditMode();
+              } else if(detectEnterKeyPress(event)) {
+                event.preventDefault();
+                updateTextValue();
+                stopEditMode();
+              }
+            });
+        }
+
+        function stopEditMode () {
+          titleLabel.removeAttr("contenteditable").off("focusout");
+          titleLabel.off("keydown");
+          saveButton.off("click");
+          cancelButton.off("click");
+          $(element).removeClass('crm-editable-editing');
+
+          penIcon.show();
+          buttons.hide();
+        }
+
+        function revertTextValue () {
+          titleLabel.text(scope.activitySet.label);
+        }
+
+        function updateTextValue () {
+          var updatedTitle = titleLabel.text();
+
+          scope.$evalAsync(function () {
+            scope.activitySet.label = updatedTitle;
+          });
+        }
+      }
+    };
+  });
+
   crmCaseType.controller('CaseTypeCtrl', function($scope, crmApi, apiCalls) {
     // CRM_Case_XMLProcessor::REL_TYPE_CNAME
     var REL_TYPE_CNAME = CRM.crmCaseType.REL_TYPE_CNAME,
diff --git a/ang/crmCaseType/activitySetDetails.html b/ang/crmCaseType/activitySetDetails.html
deleted file mode 100644 (file)
index 58701a1..0000000
+++ /dev/null
@@ -1,28 +0,0 @@
-<!--
-Controller: CaseTypeCtrl
-Required vars: activitySet
--->
-<table class="form-layout-compressed">
-  <tbody>
-  <tr>
-    <td class="label">{{ts('Label')}}</td>
-    <td>
-      <input type="text" name="label" class="crm-form-text" ng-model="activitySet.label"/>
-    </td>
-  </tr>
-  <tr>
-    <td class="label">{{ts('Name')}}</td>
-    <td>
-      <input type="text" name="name" class="crm-form-text" ng-model="activitySet.name" ng-disabled="locks.activitySetName" />
-      <a crm-ui-lock binding="locks.activitySetName"></a>
-
-    </td>
-  </tr>
-  <tr>
-    <td class="label">{{ts('Workflow')}}</td>
-    <td>
-      {{ getWorkflowName(activitySet) }}
-    </td>
-  </tr>
-  </tbody>
-</table>
index 48b464f60d30aadaa4c9afbba3a528ac24f00285..55c7faf4fc547248503cb931c2094887669ffd1b 100644 (file)
@@ -19,10 +19,14 @@ Required vars: caseType
       <li><a href="#acttab-statuses">{{ts('Case Statuses')}}</a></li>
       <li><a href="#acttab-actType">{{ts('Activity Types')}}</a></li>
       <li ng-repeat="activitySet in caseType.definition.activitySets">
-        <a href="#acttab-{{$index}}">{{ activitySet.label }}</a>
+        <a href="#acttab-{{$index}}" class="crmCaseType-editable">
+          <div crm-editable-tab-title title="{{ts('Click to edit')}}">
+            <span>{{ activitySet.label }}</span>
+          </div>
+        </a>
         <span class="crm-i fa-trash" title="{{ts('Remove')}}"
           ng-hide="activitySet.name == 'standard_timeline'"
-          ng-click="removeItem(caseType.definition.activitySets, activitySet)">{{ts('Remove')}}</span>
+          ng-click="removeItem(caseType.definition.activitySets, activitySet)"></span>
         <!-- Weird spacing:
         <a class="crm-hover-button" ng-click="removeItem(caseType.definition.activitySets, activitySet)">
           <span class="crm-i fa-trash" title="Remove">Remove</span>
@@ -44,11 +48,6 @@ Required vars: caseType
 
     <div ng-repeat="activitySet in caseType.definition.activitySets" id="acttab-{{$index}}">
       <div ng-include="activityTableTemplate(activitySet)"></div>
-
-      <div class="crm-accordion-wrapper collapsed">
-        <div class="crm-accordion-header">{{ts('Advanced')}}</div>
-        <div class="crm-accordion-body" ng-include="'~/crmCaseType/activitySetDetails.html'"></div>
-      </div>
     </div>
   </div>
 
index b647afc2fe86cafbe2d89778dbff4f048bebb5c7..1193b4573d55850e9d2f659b92c8b91960d4e736 100644 (file)
@@ -7,6 +7,7 @@ describe('crmCaseType', function() {
   var $httpBackend;
   var $q;
   var $rootScope;
+  var $timeout;
   var apiCalls;
   var ctrl;
   var compile;
@@ -27,12 +28,13 @@ describe('crmCaseType', function() {
     });
   });
 
-  beforeEach(inject(function(_$controller_, _$compile_, _$httpBackend_, _$q_, _$rootScope_) {
+  beforeEach(inject(function(_$controller_, _$compile_, _$httpBackend_, _$q_, _$rootScope_, _$timeout_) {
     $controller = _$controller_;
     $compile = _$compile_;
     $httpBackend = _$httpBackend_;
     $q = _$q_;
     $rootScope = _$rootScope_;
+    $timeout = _$timeout_;
   }));
 
   describe('CaseTypeCtrl', function() {
@@ -291,6 +293,189 @@ describe('crmCaseType', function() {
     });
   });
 
+  describe('crmEditableTabTitle', function () {
+    var element, titleLabel, penIcon, saveButton, cancelButton;
+
+    beforeEach(function() {
+      scope = $rootScope.$new();
+      element = '<div crm-editable-tab-title title="Click to edit">' +
+        '<span ng-keydown="$event.stopImmediatePropagation()">{{ activitySet.label }}</span>' +
+        '</div>';
+
+      scope.activitySet = { label: 'Title'};
+      element = $compile(element)(scope);
+
+      titleLabel = $(element).find('span');
+      penIcon = $(element).find('i.fa-pencil');
+      saveButton = $(element).find('button[type=button]');
+      cancelButton = $(element).find('button[type=cancel]');
+
+      scope.$digest();
+    });
+
+    describe('when initialized', function () {
+      it('hides the save and cancel button', function () {
+        expect(saveButton.parent().css('display') === 'none').toBe(true);
+        expect(cancelButton.parent().css('display') === 'none').toBe(true);
+      });
+    });
+
+    describe('when clicked on title label', function () {
+      beforeEach(function () {
+        titleLabel.click();
+      });
+
+      it('hides the pen icon', function () {
+        expect(penIcon.css('display') === 'none').toBe(true);
+      });
+
+      it('shows the save button', function () {
+        expect(saveButton.parent().css('display') !== 'none').toBe(true);
+      });
+
+      it('makes the title editable', function () {
+        expect(titleLabel.attr('contenteditable')).toBe('true');
+      });
+    });
+
+    describe('when clicked outside of the editable area', function () {
+      beforeEach(function () {
+        titleLabel.click();
+        titleLabel.text('Updated Title');
+        titleLabel.blur();
+        $timeout.flush();
+        scope.$digest();
+      });
+
+      it('shows the pen icon', function () {
+        expect(penIcon.css('display') !== 'none').toBe(true);
+      });
+
+      it('hides the save and cancel button', function () {
+        expect(saveButton.parent().css('display') === 'none').toBe(true);
+        expect(cancelButton.parent().css('display') === 'none').toBe(true);
+      });
+
+      it('makes the title non editable', function () {
+        expect(titleLabel.attr('contenteditable')).not.toBe('true');
+      });
+
+      it('does not update the title in angular context', function () {
+        expect(scope.activitySet.label).toBe('Title');
+      });
+    });
+
+    describe('when ESCAPE key is pressed while typing', function () {
+      beforeEach(function () {
+        var eventObj = $.Event('keydown');
+        eventObj.key = 'Escape';
+
+        titleLabel.click();
+        titleLabel.text('Updated Title');
+        titleLabel.trigger(eventObj);
+        scope.$digest();
+      });
+
+      it('shows the pen icon', function () {
+        expect(penIcon.css('display') !== 'none').toBe(true);
+      });
+
+      it('hides the save and cancel button', function () {
+        expect(saveButton.parent().css('display') === 'none').toBe(true);
+        expect(cancelButton.parent().css('display') === 'none').toBe(true);
+      });
+
+      it('makes the title non editable', function () {
+        expect(titleLabel.attr('contenteditable')).not.toBe('true');
+      });
+
+      it('does not update the title', function () {
+        expect(scope.activitySet.label).toBe('Title');
+      });
+    });
+
+    describe('when ENTER key is pressed while typing', function () {
+      beforeEach(function () {
+        var eventObj = $.Event('keydown');
+        eventObj.key = 'Enter';
+
+        titleLabel.click();
+        titleLabel.text('Updated Title');
+        titleLabel.trigger(eventObj);
+        scope.$digest();
+      });
+
+      it('shows the pen icon', function () {
+        expect(penIcon.css('display') !== 'none').toBe(true);
+      });
+
+      it('hides the save and cancel button', function () {
+        expect(saveButton.parent().css('display') === 'none').toBe(true);
+        expect(cancelButton.parent().css('display') === 'none').toBe(true);
+      });
+
+      it('makes the title non editable', function () {
+        expect(titleLabel.attr('contenteditable')).not.toBe('true');
+      });
+
+      it('updates the title in angular context', function () {
+        expect(scope.activitySet.label).toBe('Updated Title');
+      });
+    });
+
+    describe('when SAVE button is clicked', function () {
+      beforeEach(function () {
+        titleLabel.click();
+        titleLabel.text('Updated Title');
+        saveButton.click();
+        scope.$digest();
+      });
+
+      it('shows the pen icon', function () {
+        expect(penIcon.css('display') !== 'none').toBe(true);
+      });
+
+      it('hides the save and cancel button', function () {
+        expect(saveButton.parent().css('display') === 'none').toBe(true);
+        expect(cancelButton.parent().css('display') === 'none').toBe(true);
+      });
+
+      it('makes the title non editable', function () {
+        expect(titleLabel.attr('contenteditable')).not.toBe('true');
+      });
+
+      it('updates the title in angular context', function () {
+        expect(scope.activitySet.label).toBe('Updated Title');
+      });
+    });
+
+    describe('when CANCEL button is clicked', function () {
+      beforeEach(function () {
+        titleLabel.click();
+        titleLabel.text('Updated Title');
+        cancelButton.click();
+        scope.$digest();
+      });
+
+      it('shows the pen icon', function () {
+        expect(penIcon.css('display') !== 'none').toBe(true);
+      });
+
+      it('hides the save and cancel button', function () {
+        expect(saveButton.parent().css('display') === 'none').toBe(true);
+        expect(cancelButton.parent().css('display') === 'none').toBe(true);
+      });
+
+      it('makes the title non editable', function () {
+        expect(titleLabel.attr('contenteditable')).not.toBe('true');
+      });
+
+      it('does not update the title in angular context', function () {
+        expect(scope.activitySet.label).toBe('Title');
+      });
+    });
+  });
+
   describe('CaseTypeListCtrl', function() {
     var caseTypes, crmApiSpy;