SearchKit - add support for non-aggregate functions
authorColeman Watts <coleman@civicrm.org>
Sun, 25 Jul 2021 00:22:06 +0000 (20:22 -0400)
committerColeman Watts <coleman@civicrm.org>
Thu, 29 Jul 2021 04:24:34 +0000 (00:24 -0400)
ext/search_kit/ang/crmSearchAdmin/compose/criteria.html
ext/search_kit/ang/crmSearchAdmin/crmSearchAdmin.component.js
ext/search_kit/ang/crmSearchAdmin/crmSearchFunction.component.js
ext/search_kit/ang/crmSearchAdmin/crmSearchFunction.html
ext/search_kit/css/crmSearchAdmin.css

index 8aeb1187816c4fa4fc01a3c869f533fc2586e418..64b1694b268f8821245b2ea1f0a327bd81f0a91c 100644 (file)
                crm-ui-select="{placeholder: ts('Group By'), data: fieldsForGroupBy, dropdownCss: {width: '300px'}}"
                on-crm-ui-select="$ctrl.addParam('groupBy', selection)" >
       </div>
-      <fieldset id="crm-search-build-group-aggregate" ng-if="$ctrl.savedSearch.api_params.groupBy.length" class="crm-collapsible collapsed">
-        <legend class="collapsible-title">{{:: ts('Aggregate fields') }}</legend>
-        <div>
-          <fieldset ng-repeat="col in $ctrl.savedSearch.api_params.select" ng-if="$ctrl.canAggregate(col)">
-            <crm-search-function expr="$ctrl.savedSearch.api_params.select[$index]" cat="'aggregate'"></crm-search-function>
+      <fieldset id="crm-search-build-functions">
+        <legend ng-click="controls.showFunctions = !controls.showFunctions">
+          <i class="crm-i fa-caret-{{ !controls.showFunctions ? 'right' : 'down' }}"></i>
+          {{:: ts('Field Transformations') }}
+        </legend>
+        <div ng-if="!!controls.showFunctions">
+          <fieldset ng-repeat="col in $ctrl.savedSearch.api_params.select">
+            <crm-search-function expr="$ctrl.savedSearch.api_params.select[$index]"></crm-search-function>
           </fieldset>
         </div>
       </fieldset>
index 79ffe0ed4d516e41fe4665fba71f8ce7e2b2b0af..2472af053bba34aca14710868d140f4c193eea01 100644 (file)
       };
 
       $scope.changeGroupBy = function(idx) {
+        // When clearing a selection
         if (!ctrl.savedSearch.api_params.groupBy[idx]) {
           ctrl.clearParam('groupBy', idx);
         }
-        // Remove aggregate functions when no grouping
-        if (!ctrl.savedSearch.api_params.groupBy.length) {
-          _.each(ctrl.savedSearch.api_params.select, function(col, pos) {
-            if (_.contains(col, '(')) {
-              var info = searchMeta.parseExpr(col);
-              if (info.fn.category === 'aggregate') {
-                ctrl.savedSearch.api_params.select[pos] = info.path + info.suffix;
-              }
-            }
-          });
-        }
+        reconcileAggregateColumns();
       };
 
+      function reconcileAggregateColumns() {
+        _.each(ctrl.savedSearch.api_params.select, function(col, pos) {
+          var info = searchMeta.parseExpr(col),
+            fieldExpr = info.path + info.suffix;
+          if (ctrl.canAggregate(col)) {
+            // Ensure all non-grouped columns are aggregated if using GROUP BY
+            if (!info.fn || info.fn.category !== 'aggregate') {
+              ctrl.savedSearch.api_params.select[pos] = ctrl.DEFAULT_AGGREGATE_FN + '(DISTINCT ' + fieldExpr + ') AS ' + ctrl.DEFAULT_AGGREGATE_FN + '_DISTINCT_' + fieldExpr.replace(/[.:]/g, '_');
+            }
+          } else {
+            // Remove aggregate functions when no grouping
+            if (info.fn && info.fn.category === 'aggregate') {
+              ctrl.savedSearch.api_params.select[pos] = fieldExpr;
+            }
+          }
+        });
+      }
+
       function clauseUsesJoin(clause, alias) {
         if (clause[0].indexOf(alias + '.') === 0) {
           return true;
       this.addParam = function(name, value) {
         if (value && !_.contains(ctrl.savedSearch.api_params[name], value)) {
           ctrl.savedSearch.api_params[name].push(value);
-          if (name === 'groupBy') {
-            // Expand the aggregate block
-            $timeout(function() {
-              $('#crm-search-build-group-aggregate.collapsed .collapsible-title').click();
-            }, 10);
-          }
+          // This needs to be called when adding a field as well as changing groupBy
+          reconcileAggregateColumns();
         }
       };
 
         $('.crm-search-results', $element).css('height', '');
       }
 
-      // Ensure all non-grouped columns are aggregated if using GROUP BY
-      function aggregateGroupByColumns() {
-        if (ctrl.savedSearch.api_params.groupBy.length) {
-          _.each(ctrl.savedSearch.api_params.select, function(col, pos) {
-            if (!_.contains(col, '(') && ctrl.canAggregate(col)) {
-              ctrl.savedSearch.api_params.select[pos] = ctrl.DEFAULT_AGGREGATE_FN + '(DISTINCT ' + col + ') AS ' + ctrl.DEFAULT_AGGREGATE_FN + '_DISTINCT_' + col.replace(/[.:]/g, '_');
-            }
-          });
-        }
-      }
-
       // Debounced callback for loadResults
       function _loadResultsCallback() {
         // Multiply limit to read 2 pages at once & save ajax requests
 
       function loadResults() {
         $scope.loading = true;
-        aggregateGroupByColumns();
         _loadResults();
       }
 
index 24f1f35f5813408b9d3b4c4e9395a830fd0ef225..2ebd28e86d48a46ac76be92cc1507e77eff91f50 100644 (file)
@@ -3,23 +3,32 @@
 
   angular.module('crmSearchAdmin').component('crmSearchFunction', {
     bindings: {
-      expr: '=',
-      cat: '<'
+      expr: '='
+    },
+    require: {
+      crmSearchAdmin: '^crmSearchAdmin'
     },
     templateUrl: '~/crmSearchAdmin/crmSearchFunction.html',
     controller: function($scope, formatForSelect2, searchMeta) {
       var ts = $scope.ts = CRM.ts('org.civicrm.search_kit'),
         ctrl = this;
 
-      this.$onInit = function() {
-        ctrl.functions = formatForSelect2(_.where(CRM.crmSearchAdmin.functions, {category: ctrl.cat}), 'name', 'title');
-        var fieldInfo = searchMeta.parseExpr(ctrl.expr);
+      var allTypes = {
+        aggregate: ts('Aggregate'),
+        comparison: ts('Comparison'),
+        date: ts('Date'),
+        math: ts('Math'),
+        string: ts('Text')
+      };
+
+      $scope.$watch('$ctrl.expr', function(expr) {
+        var fieldInfo = searchMeta.parseExpr(expr);
         ctrl.path = fieldInfo.path + fieldInfo.suffix;
         ctrl.field = fieldInfo.field;
         ctrl.fn = !fieldInfo.fn ? '' : fieldInfo.fn.name;
         ctrl.modifier = fieldInfo.modifier || null;
         initFunction();
-      };
+      });
 
       function initFunction() {
         ctrl.fnInfo = _.find(CRM.crmSearchAdmin.functions, {name: ctrl.fn});
         }
       }
 
+      this.getFunctions = function() {
+        var allowedTypes = [], functions = [];
+        if (ctrl.expr && ctrl.field) {
+          if (ctrl.crmSearchAdmin.canAggregate(ctrl.expr)) {
+            allowedTypes.push('aggregate');
+          } else {
+            allowedTypes.push('comparison', 'string');
+            if (_.includes(['Integer', 'Float', 'Date', 'Timestamp'], ctrl.field.data_type)) {
+              allowedTypes.push('math');
+            }
+            if (_.includes(['Date', 'Timestamp'], ctrl.field.data_type)) {
+              allowedTypes.push('date');
+            }
+          }
+          _.each(allowedTypes, function (type) {
+            functions.push({
+              text: allTypes[type],
+              children: formatForSelect2(_.where(CRM.crmSearchAdmin.functions, {category: type}), 'name', 'title')
+            });
+          });
+        }
+        return {results: functions};
+      };
+
       this.selectFunction = function() {
-        initFunction();
         ctrl.writeExpr();
       };
 
index d8a380e78092e7c41f7f9b91a693901db6e3e3f2..2274f262d975da0d722f524c5cdb8d4e7388583e 100644 (file)
@@ -1,6 +1,6 @@
 <div class="form-inline">
   <label>{{ $ctrl.field.label }}:</label>
-  <input class="form-control" style="width: 15em;" ng-model="$ctrl.fn" crm-ui-select="{data: $ctrl.functions, placeholder: ts('Select')}" ng-change="$ctrl.selectFunction()">
+  <input class="form-control" style="width: 15em;" ng-model="$ctrl.fn" crm-ui-select="{data: $ctrl.getFunctions, placeholder: ts('Select')}" ng-change="$ctrl.selectFunction()">
   <label ng-if="$ctrl.modifierName">
     <input type="checkbox" ng-checked="!!$ctrl.modifier" ng-click="$ctrl.toggleModifier()">
     {{ $ctrl.modifierLabel }}
index 6e6749c3584113669a1788d294abe716d47e0a88..eceab7fcf6a73b24199bb5cfda4f2b0f1f171df7 100644 (file)
   min-height: 3.5em;
 }
 
+#bootstrap-theme.crm-search legend[ng-click] {
+  cursor: pointer;
+}
+
 #bootstrap-theme.crm-search .api4-input-group {
   display: inline-block;
 }