* @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;
}, $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;
},
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'})) || {};
}
};
+ 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});
}
// 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});
};
-<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>
<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') }}
sort: [],
isSortable: function(col) {
- return col.type === 'field' && col.sortable !== false;
+ return !this.settings.draggable && col.type === 'field' && col.sortable !== false;
},
getSort: function(col) {
'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',
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);
+ }
+ });
+ }
+ };
+ }
};
}
<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>
<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>
<!-- 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>
/* 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,