namespace Civi\Api4\Action\SearchDisplay;
+use Civi\API\Request;
+use Civi\Api4\Query\Api4SelectQuery;
use Civi\Api4\Utils\CoreUtil;
/**
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];
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;
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'])};
+ };
+
}
});
<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>
</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>
$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);
'partials' => [
'ang/crmSearchDisplayTable',
],
+ 'css' => [
+ 'css/crmSearchDisplayTable.css',
+ ],
'basePages' => ['civicrm/search', 'civicrm/admin/search'],
'requires' => ['crmSearchDisplay', 'crmUi', 'crmSearchTasks', 'ui.bootstrap', 'ui.sortable'],
'bundles' => ['bootstrap3'],
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) {
<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>
<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">
--- /dev/null
+<!-- 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>
--- /dev/null
+/* 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;
+}
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;