SearchKit - Add drag-sortable weight functionality
authorColeman Watts <coleman@civicrm.org>
Mon, 15 Nov 2021 14:47:37 +0000 (09:47 -0500)
committerColeman Watts <coleman@civicrm.org>
Tue, 30 Nov 2021 15:19:18 +0000 (10:19 -0500)
Drag-sortable weights are similar to in-place edit in that it uses the API
to update records in the table. In this case it updates the "weight" column when
the user drags a row into a different position.

ext/search_kit/Civi/Api4/Action/SearchDisplay/AbstractRunAction.php
ext/search_kit/ang/crmSearchAdmin/crmSearchAdminDisplay.component.js
ext/search_kit/ang/crmSearchAdmin/displays/searchAdminDisplayTable.component.js
ext/search_kit/ang/crmSearchAdmin/displays/searchAdminDisplayTable.html
ext/search_kit/ang/crmSearchDisplay/traits/searchDisplaySortableTrait.service.js
ext/search_kit/ang/crmSearchDisplayTable.ang.php
ext/search_kit/ang/crmSearchDisplayTable/crmSearchDisplayTable.component.js
ext/search_kit/ang/crmSearchDisplayTable/crmSearchDisplayTable.html
ext/search_kit/ang/crmSearchDisplayTable/crmSearchDisplayTableBody.html
ext/search_kit/ang/crmSearchDisplayTable/crmSearchDisplayTableLoading.html
ext/search_kit/css/crmSearchDisplay.css

index d705319fbecd26719171f8ba6706abf907ae510a..d71beeddbf78ce4a147fa6d93248840782eb3bb2 100644 (file)
@@ -712,6 +712,11 @@ abstract class AbstractRunAction extends \Civi\Api4\Generic\AbstractAction {
    * @return array
    */
   protected function getOrderByFromSort() {
+    // Drag-sortable tables have a forced order
+    if (!empty($this->display['settings']['draggable'])) {
+      return [$this->display['settings']['draggable'] => 'ASC'];
+    }
+
     $defaultSort = $this->display['settings']['sort'] ?? [];
     $currentSort = $this->sort;
 
@@ -745,9 +750,13 @@ abstract class AbstractRunAction extends \Civi\Api4\Generic\AbstractAction {
     }, $apiParams['select']);
     $additions = [];
     // Add primary key field if actions are enabled
-    if (!empty($this->display['settings']['actions'])) {
+    if (!empty($this->display['settings']['actions']) || !empty($this->display['settings']['draggable'])) {
       $additions = CoreUtil::getInfoItem($this->savedSearch['api_entity'], 'primary_key');
     }
+    // Add draggable column (typically "weight")
+    if (!empty($this->display['settings']['draggable'])) {
+      $additions[] = $this->display['settings']['draggable'];
+    }
     // Add style conditions for the display
     foreach ($this->getCssRulesSelect($this->display['settings']['cssRules'] ?? []) as $addition) {
       $additions[] = $addition;
index e6d17dd5dc79c27ee209c4943ac23f22ff912858..94d4007ab3ffce8cd74f195974ef14e25521ddff 100644 (file)
@@ -34,8 +34,7 @@
     },
     controller: function($scope, $timeout, searchMeta) {
       var ts = $scope.ts = CRM.ts('org.civicrm.search_kit'),
-        ctrl = this,
-        afforms;
+        ctrl = this;
 
       this.isSuperAdmin = CRM.checkPerm('all CiviCRM permissions and ACLs');
       this.aclBypassHelp = ts('Only users with "all CiviCRM permissions and ACLs" can disable permission checks.');
       // Checks if a column contains a sortable value
       // Must be a real sql expression (not a pseudo-field like `result_row_num`)
       this.canBeSortable = function(col) {
+        // Column-header sorting is incompatible with draggable sorting
+        if (ctrl.display.settings.draggable) {
+          return false;
+        }
         var expr = ctrl.getExprFromSelect(col.key),
           info = searchMeta.parseExpr(expr),
           arg = (info && info.args && _.findWhere(info.args, {type: 'field'})) || {};
index 55b4fbbd5fa86cb19ecadabab36137c44c11e2f6..b8cdfee5a5b37eb654810728a4cbd4082a7e93d5 100644 (file)
         }
       };
 
+      this.toggleDraggable = function() {
+        if (ctrl.display.settings.draggable) {
+          delete ctrl.display.settings.draggable;
+        } else {
+          ctrl.display.settings.sort = [];
+          ctrl.display.settings.draggable = searchMeta.getEntity(ctrl.apiEntity).order_by;
+        }
+      };
+
       this.$onInit = function () {
         if (!ctrl.display.settings) {
           ctrl.display.settings = _.extend({}, _.cloneDeep(CRM.crmSearchAdmin.defaultDisplay.settings), {columns: null});
@@ -42,6 +51,8 @@
         }
         // Displays created prior to 5.43 may not have this property
         ctrl.display.settings.classes = ctrl.display.settings.classes || [];
+        // Table can be draggable if the main entity is a SortableEntity.
+        ctrl.canBeDraggable = _.includes(searchMeta.getEntity(ctrl.apiEntity).type, 'SortableEntity');
         ctrl.parent.initColumns({label: true, sortable: true});
       };
 
index a3090f9203261d8ac07a97b87ef294639291bdad..4794ea0e409a2f3de6eac023fce7b039c2656267 100644 (file)
@@ -1,5 +1,13 @@
-<fieldset ng-include="'~/crmSearchAdmin/crmSearchAdminDisplaySort.html'"></fieldset>
+<fieldset ng-if="!$ctrl.display.settings.draggable" ng-include="'~/crmSearchAdmin/crmSearchAdminDisplaySort.html'"></fieldset>
 <fieldset>
+  <div ng-if="$ctrl.canBeDraggable" class="form-inline">
+    <div class="checkbox-inline form-control">
+      <label>
+        <input type="checkbox" ng-checked="!!$ctrl.display.settings.draggable" ng-click="$ctrl.toggleDraggable()">
+        <span>{{:: ts('Drag and drop sorting') }}</span>
+      </label>
+    </div>
+  </div>
   <div class="form-inline">
     <div class="checkbox-inline form-control">
       <label>
@@ -44,7 +52,7 @@
           <option value="text-right">{{:: ts('Right') }}</option>
         </select>
       </div>
-      <div class="form-inline" ng-if=":: $ctrl.parent.canBeSortable(col)">
+      <div class="form-inline" ng-if="$ctrl.parent.canBeSortable(col)">
         <label title="{{:: ts('Allow user to click on header to sort table by this column') }}">
           <input type="checkbox" ng-checked="col.sortable !== false" ng-click="col.sortable = col.sortable === false" >
           {{:: ts('Sortable Header') }}
index 403ddeab5398fffd752fc4684ed0053aca39a0ce..b671627c4af8d687058b5d1ccc0c4ce26e0c5927 100644 (file)
@@ -11,7 +11,7 @@
       sort: [],
 
       isSortable: function(col) {
-        return col.type === 'field' && col.sortable !== false;
+        return !this.settings.draggable && col.type === 'field' && col.sortable !== false;
       },
 
       getSort: function(col) {
index d010a2f97d6af24312033a5f6cff79a5b773a235..c52b7f3e173c15e13555ebc2f793b53313e9209e 100644 (file)
@@ -9,7 +9,7 @@ return [
     'ang/crmSearchDisplayTable',
   ],
   'basePages' => ['civicrm/search', 'civicrm/admin/search'],
-  'requires' => ['crmSearchDisplay', 'crmUi', 'crmSearchTasks', 'ui.bootstrap'],
+  'requires' => ['crmSearchDisplay', 'crmUi', 'crmSearchTasks', 'ui.bootstrap', 'ui.sortable'],
   'bundles' => ['bootstrap3'],
   'exports' => [
     'crm-search-display-table' => 'E',
index 096c033d959463dd9a94a836df405a93249fa4ce..d1c859ef49401100e632a6d5fff73e3982d10670 100644 (file)
       afFieldset: '?^^afFieldset'
     },
     templateUrl: '~/crmSearchDisplayTable/crmSearchDisplayTable.html',
-    controller: function($scope, $element, searchDisplayBaseTrait, searchDisplayTasksTrait, searchDisplaySortableTrait) {
+    controller: function($scope, $element, searchDisplayBaseTrait, searchDisplayTasksTrait, searchDisplaySortableTrait, crmApi4, crmStatus) {
       var ts = $scope.ts = CRM.ts('org.civicrm.search_kit'),
         // Mix in traits to this controller
         ctrl = angular.extend(this, searchDisplayBaseTrait, searchDisplayTasksTrait, searchDisplaySortableTrait);
 
+
       this.$onInit = function() {
         this.initializeDisplay($scope, $element);
+
+        if (ctrl.settings.draggable) {
+          ctrl.draggableOptions = {
+            containment: 'table',
+            direction: 'vertical',
+            handle: '.crm-draggable',
+            forcePlaceholderSize: true,
+            helper: function(e, ui) {
+              // Prevent table row width from changing during drag
+              ui.children().each(function() {
+                $(this).width($(this).width());
+              });
+              return ui;
+            },
+            stop: function(e, ui) {
+              $scope.$apply(function() {
+                var movedItem = ui.item.sortable.model,
+                  oldPosition = ui.item.sortable.index,
+                  newPosition = ctrl.results.indexOf(movedItem),
+                  displacement = newPosition < oldPosition ? -1 : 1,
+                  displacedItem = ctrl.results[newPosition - displacement],
+                  weightColumn = ctrl.settings.draggable,
+                  updateParams = {where: [['id', '=', movedItem.data.id]], values: {}};
+                if (newPosition > -1 && oldPosition !== newPosition) {
+                  updateParams.values[weightColumn] = displacedItem.data[weightColumn];
+                  ctrl.runSearch([[ctrl.apiEntity, 'update', updateParams]], {}, movedItem);
+                }
+              });
+            }
+          };
+        }
       };
 
     }
index b5e28566f74daaa788b7a9088c40e21d44d66af1..2731e1f056816a3ce263d4d5ae5dd6bdac7d7710 100644 (file)
@@ -6,16 +6,19 @@
   <table class="{{:: $ctrl.settings.classes.join(' ') }}">
     <thead>
       <tr>
-        <th class="crm-search-result-select" ng-if=":: $ctrl.settings.actions">
-          <input type="checkbox" ng-disabled="$ctrl.loading || !$ctrl.results.length" ng-checked="$ctrl.allRowsSelected" ng-click="$ctrl.selectAllRows()" >
+        <th class="crm-search-result-select" ng-if=":: $ctrl.settings.actions || $ctrl.settings.draggable">
+          <i ng-if=":: $ctrl.settings.draggable" class="crm-i fa-sort-amount-asc" title="{{:: ts('Drag columns to reposition') }}"></i>
+          <input type="checkbox" ng-if=":: $ctrl.settings.actions" ng-disabled="$ctrl.loading || !$ctrl.results.length" ng-checked="$ctrl.allRowsSelected" ng-click="$ctrl.selectAllRows()" >
         </th>
-        <th ng-repeat="col in $ctrl.settings.columns" ng-click="$ctrl.setSort(col, $event)" title="{{:: $ctrl.isSortable(col) ? ts('Click to sort results (shift-click to sort by multiple).') : '' }}">
+        <th ng-repeat="col in $ctrl.settings.columns" ng-click="$ctrl.setSort(col, $event)" class="{{:: $ctrl.isSortable(col) ? 'crm-sortable-col' : ''}}" title="{{:: $ctrl.isSortable(col) ? ts('Click to sort results (shift-click to sort by multiple).') : '' }}">
           <i ng-if=":: $ctrl.isSortable(col)" class="crm-i {{ $ctrl.getSort(col) }}"></i>
           <span>{{:: col.label }}</span>
         </th>
       </tr>
     </thead>
-    <tbody ng-include="'~/crmSearchDisplayTable/crmSearchDisplayTable' + ($ctrl.loading ? 'Loading' : 'Body') + '.html'"></tbody>
+    <tbody ng-if="$ctrl.loading" ng-include="'~/crmSearchDisplayTable/crmSearchDisplayTableLoading.html'"></tbody>
+    <tbody ng-if="!$ctrl.loading && !$ctrl.settings.draggable" ng-include="'~/crmSearchDisplayTable/crmSearchDisplayTableBody.html'"></tbody>
+    <tbody ng-if="!$ctrl.loading && $ctrl.settings.draggable" ng-include="'~/crmSearchDisplayTable/crmSearchDisplayTableBody.html'" ui-sortable="$ctrl.draggableOptions" ng-model="$ctrl.results"></tbody>
   </table>
   <div ng-include="'~/crmSearchDisplay/Pager.html'"></div>
 </div>
index f3f525aa6f1115c8330911cf5231bdcb0e6f12df..1bb55193a4f431cb46beef807b508842937716ee 100644 (file)
@@ -1,6 +1,9 @@
 <tr ng-repeat="(rowIndex, row) in $ctrl.results">
-  <td ng-if=":: $ctrl.settings.actions" class="{{:: row.cssClass }}">
-    <input type="checkbox" ng-checked="$ctrl.isRowSelected(row)" ng-click="$ctrl.selectRow(row)" ng-disabled="!!$ctrl.loadingAllRows">
+  <td ng-if=":: $ctrl.settings.actions || $ctrl.settings.draggable" class="{{:: row.cssClass }}">
+    <span ng-if=":: $ctrl.settings.draggable" class="crm-draggable" title="{{:: ts('Drag to reposition') }}">
+      <i class="crm-i fa-arrows-v"></i>
+    </span>
+    <input ng-if=":: $ctrl.settings.actions" type="checkbox" ng-checked="$ctrl.isRowSelected(row)" ng-click="$ctrl.selectRow(row)" ng-disabled="!!$ctrl.loadingAllRows">
   </td>
   <td ng-repeat="(colIndex, colData) in row.columns" ng-include="'~/crmSearchDisplay/colType/' + $ctrl.settings.columns[colIndex].type + '.html'" title="{{:: colData.title }}" class="{{:: row.cssClass }} {{:: colData.cssClass }}">
   </td>
index a3d01656ae73d8c25d98f45bc29e2f4581add756..35738b7edd6b4adae646a79839fc0fa105785d82 100644 (file)
@@ -1,7 +1,7 @@
 <!-- Placeholder table rows shown during ajax loading -->
 <tr ng-repeat="num in [1,2,3,4,5] track by $index">
-  <td ng-if=":: $ctrl.settings.actions">
-    <input type="checkbox" disabled>
+  <td ng-if=":: $ctrl.settings.actions || $ctrl.settings.draggable">
+    <input ng-if=":: $ctrl.settings.actions" type="checkbox" disabled>
   </td>
   <td ng-repeat="col in $ctrl.settings.columns">
     <div class="crm-search-loading-placeholder"></div>
index 631f3d74a008195a014aabd8efea63e66cd00128..a2c84a7a4597b9e1f5a652175f10a4fad865b0de 100644 (file)
@@ -1,5 +1,5 @@
 /* Sortable headers */
-#bootstrap-theme .crm-search-display th[ng-click] {
+#bootstrap-theme .crm-search-display th.crm-sortable-col {
   cursor: pointer;
 }
 #bootstrap-theme .crm-search-display th i.fa-sort-desc,