Skip to content
Snippets Groups Projects
Unverified Commit af820d3e authored by Eileen McNaughton's avatar Eileen McNaughton Committed by GitHub
Browse files

Merge pull request #23454 from colemanw/searchKitGrandTotals

Search kit grand totals
parents dc885e5c ecd96898
Branches
Tags
No related merge requests found
Showing
with 131 additions and 13 deletions
......@@ -2,6 +2,8 @@
namespace Civi\Api4\Action\SearchDisplay;
use Civi\API\Request;
use Civi\Api4\Query\Api4SelectQuery;
use Civi\Api4\Utils\CoreUtil;
/**
......@@ -53,6 +55,34 @@ class Run extends AbstractRunAction {
unset($apiParams['orderBy'], $apiParams['limit']);
break;
case 'tally':
$this->applyFilters();
unset($apiParams['orderBy'], $apiParams['limit']);
$api = Request::create($entityName, 'get', $apiParams);
$query = new Api4SelectQuery($api);
$query->forceSelectId = FALSE;
$sql = $query->getSql();
$select = [];
foreach ($settings['columns'] as $col) {
if (!empty($col['tally']['fn']) && !empty($col['key'])) {
$fn = \CRM_Core_DAO::escapeString($col['tally']['fn']);
$key = \CRM_Core_DAO::escapeString($col['key']);
$select[] = $fn . '(`' . $key . '`) `' . $key . '`';
}
}
$query = 'SELECT ' . implode(', ', $select) . ' FROM (' . $sql . ') `api_query`';
$dao = \CRM_Core_DAO::executeQuery($query);
$dao->fetch();
$tally = [];
foreach ($settings['columns'] as $col) {
if (!empty($col['tally']['fn']) && !empty($col['key'])) {
$alias = str_replace('.', '_', $col['key']);
$tally[$col['key']] = $dao->$alias ?? NULL;
}
}
$result[] = $tally;
return;
default:
if (($settings['pager'] ?? FALSE) !== FALSE && preg_match('/^page:\d+$/', $key)) {
$page = explode(':', $key)[1];
......
......@@ -11,7 +11,7 @@
parent: '^crmSearchAdminDisplay'
},
templateUrl: '~/crmSearchAdmin/displays/searchAdminDisplayTable.html',
controller: function($scope, searchMeta) {
controller: function($scope, searchMeta, formatForSelect2) {
var ts = $scope.ts = CRM.ts('org.civicrm.search_kit'),
ctrl = this;
......@@ -56,6 +56,31 @@
ctrl.parent.initColumns({label: true, sortable: true});
};
this.toggleTally = function() {
if (ctrl.display.settings.tally) {
delete ctrl.display.settings.tally;
_.each(ctrl.display.settings.columns, function(col) {
delete col.tally;
});
} else {
ctrl.display.settings.tally = {label: ts('Total')};
_.each(ctrl.display.settings.columns, function(col) {
if (col.type === 'field') {
col.tally = {
fn: searchMeta.getDefaultAggregateFn(searchMeta.parseExpr(col.key)).fnName
};
}
});
}
};
this.getTallyFunctions = function() {
var allowedFunctions = _.filter(CRM.crmSearchAdmin.functions, function(fn) {
return fn.category === 'aggregate' && fn.params.length;
});
return {results: formatForSelect2(allowedFunctions, 'name', 'title', ['description'])};
};
}
});
......
......@@ -32,6 +32,18 @@
<label for="crm-search-admin-display-no-results-text">{{:: ts('No Results Text') }}</label>
<input class="form-control crm-flex-1" id="crm-search-admin-display-no-results-text" ng-model="$ctrl.display.settings.noResultsText" placeholder="{{:: ts('None found.') }}">
</div>
<div class="form-inline">
<div class="checkbox-inline form-control" title="{{:: ts('Shows grand totals or other statistics, configured per-column.') }}">
<label>
<input type="checkbox" ng-click="$ctrl.toggleTally()" ng-checked="!!$ctrl.display.settings.tally">
<span>{{:: ts('Show Totals in Footer') }}</span>
</label>
</div>
<div class="form-group" ng-if="$ctrl.display.settings.tally">
<label for="crm-search-admin-table-tally-title">{{:: ts('Label') }}</label>
<input id="crm-search-admin-table-tally-title" ng-model="$ctrl.display.settings.tally.label" class="form-control">
</div>
</div>
</fieldset>
<fieldset class="crm-search-admin-edit-columns-wrapper">
<legend>
......@@ -63,6 +75,12 @@
</label>
</div>
<div ng-include="'~/crmSearchAdmin/displays/colType/' + col.type + '.html'"></div>
<div class="form-inline" ng-if="col.type === 'field' && $ctrl.display.settings.tally">
<label>{{:: ts('Footer Label') }}</label>
<input class="form-control" ng-model="col.tally.label" placeholder="{{:: ts('None') }}">
<label>{{:: ts('Footer Aggregate') }}</label>
<input class="form-control" ng-model="col.tally.fn" crm-ui-select="{data: $ctrl.getTallyFunctions, placeholder: ts('None'), allowClear: true}">
</div>
</fieldset>
</div>
</fieldset>
......@@ -82,6 +82,10 @@
$scope.$watch('$ctrl.filters', onChangeFilters, true);
},
hasExtraFirstColumn: function() {
return this.settings.actions || this.settings.draggable || (this.settings.tally && this.settings.tally.label);
},
getAfformFilters: function() {
return _.pick(this.afFieldset ? this.afFieldset.getFieldData() : {}, function(val) {
return val !== null && (_.includes(['boolean', 'number'], typeof val) || val.length);
......
......@@ -8,6 +8,9 @@ return [
'partials' => [
'ang/crmSearchDisplayTable',
],
'css' => [
'css/crmSearchDisplayTable.css',
],
'basePages' => ['civicrm/search', 'civicrm/admin/search'],
'requires' => ['crmSearchDisplay', 'crmUi', 'crmSearchTasks', 'ui.bootstrap', 'ui.sortable'],
'bundles' => ['bootstrap3'],
......
......@@ -20,6 +20,25 @@
ctrl = angular.extend(this, searchDisplayBaseTrait, searchDisplayTasksTrait, searchDisplaySortableTrait);
this.$onInit = function() {
var tallyParams;
if (ctrl.settings.tally) {
ctrl.onPreRun.push(function (apiParams) {
ctrl.tally = null;
tallyParams = _.cloneDeep(apiParams);
});
ctrl.onPostRun.push(function (results, status) {
ctrl.tally = null;
if (status === 'success' && tallyParams) {
tallyParams.return = 'tally';
crmApi4('SearchDisplay', 'run', tallyParams).then(function (result) {
ctrl.tally = result[0];
});
}
});
}
this.initializeDisplay($scope, $element);
if (ctrl.settings.draggable) {
......
......@@ -6,7 +6,7 @@
<table class="{{:: $ctrl.settings.classes.join(' ') }}">
<thead>
<tr>
<th ng-class="{'crm-search-result-select': $ctrl.settings.actions}" ng-include="'~/crmSearchDisplayTable/crmSearchDisplayTaskHeader.html'" ng-if=":: $ctrl.settings.actions || $ctrl.settings.draggable">
<th ng-class="{'crm-search-result-select': $ctrl.settings.actions}" ng-include="'~/crmSearchDisplayTable/crmSearchDisplayTaskHeader.html'" ng-if=":: $ctrl.hasExtraFirstColumn()">
</th>
<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>
......@@ -17,6 +17,7 @@
<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>
<tfoot ng-if="!$ctrl.loading && $ctrl.settings.tally" ng-include="'~/crmSearchDisplayTable/crmSearchDisplayTally.html'"></tfoot>
</table>
<div ng-include="'~/crmSearchDisplay/Pager.html'"></div>
</div>
<tr ng-repeat="(rowIndex, row) in $ctrl.results">
<td ng-if=":: $ctrl.settings.actions || $ctrl.settings.draggable" class="{{:: row.cssClass }}">
<td ng-if=":: $ctrl.hasExtraFirstColumn()" 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>
......
<!-- 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 || $ctrl.settings.draggable">
<td ng-if=":: $ctrl.hasExtraFirstColumn()">
<input ng-if=":: $ctrl.settings.actions" type="checkbox" disabled>
</td>
<td ng-repeat="col in $ctrl.settings.columns">
......
<!-- Placeholder table rows shown during ajax loading -->
<tr>
<td ng-if=":: $ctrl.hasExtraFirstColumn()">
{{:: $ctrl.settings.tally.label }}
</td>
<td ng-repeat="col in $ctrl.settings.columns">
<div ng-if="!$ctrl.tally" class="crm-search-loading-placeholder"></div>
<div ng-if="$ctrl.tally">
{{:: col.tally.label }}
{{ $ctrl.tally[col.key] }}
</div>
</td>
</tr>
/* search kit table display styling */
#bootstrap-theme .crm-search-display-table > table.table > thead > tr > th.crm-search-result-select {
padding-left: 0;
padding-right: 0;
text-transform: none;
color: initial;
/* Don't allow button to be split on 2 lines */
min-width: 86px;
}
#bootstrap-theme .crm-search-display.crm-search-display-table tfoot > tr > td {
font-weight: bold;
}
......@@ -4,15 +4,6 @@
border: 1px solid lightgrey;
}
#bootstrap-theme .crm-search-display-table > table.table > thead > tr > th.crm-search-result-select {
padding-left: 0;
padding-right: 0;
text-transform: none;
color: initial;
/* Don't allow button to be split on 2 lines */
min-width: 86px;
}
.crm-search-display.crm-search-display-table td > crm-search-display-editable,
.crm-search-display.crm-search-display-table td > .crm-editable-enabled {
display: block !important;
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment