SearchKit - Support ORDER BY with aggregate functions
authorcolemanw <coleman@civicrm.org>
Fri, 17 Nov 2023 01:15:59 +0000 (20:15 -0500)
committercolemanw <coleman@civicrm.org>
Fri, 17 Nov 2023 14:33:55 +0000 (09:33 -0500)
ext/search_kit/ang/crmSearchAdmin.module.js
ext/search_kit/ang/crmSearchAdmin/crmSearchFunction.component.js
ext/search_kit/ang/crmSearchAdmin/crmSearchFunction.html
ext/search_kit/ang/crmSearchAdmin/crmSearchFunctionFlag.component.js
ext/search_kit/ang/crmSearchAdmin/crmSearchFunctionFlag.html

index 1b9adf9ffdf061767f97eab3a020c733d691f98c..c1c8f9fbcf1180d98a9be1bbb14a3298d225d002 100644 (file)
         function getKeyword(whitelist) {
           var keyword;
           _.each(_.filter(whitelist), function(flag) {
-            if (argString.indexOf(flag + ' ') === 0) {
+            if (argString.indexOf(flag + ' ') === 0 || argString === flag) {
               keyword = flag;
               argString = _.trim(argString.substr(flag.length));
               return false;
               }
               getKeyword([',']);
             }
-            if (expr && !_.isEmpty(expr.flag_after)) {
+            if (info.args.length && !_.isEmpty(param.flag_after)) {
               _.last(info.args).flag_after = getKeyword(_.keys(param.flag_after));
             }
           } else if (param.flag_before && !param.optional) {
index 366bb69105649a50b477cefd356d910946403cf1..25e472b5b396040465159130b244fb2f7115f75c 100644 (file)
         }
       });
 
-      this.addArg = function(exprType) {
+      this.addArg = function(exprType, optional) {
         var param = ctrl.getParam(ctrl.args.length),
           val = '';
         if (exprType === 'SqlNumber') {
           // Number: default to 0
           val = 0;
-        } else if (exprType === 'SqlField') {
+        } else if (exprType === 'SqlField' && !optional) {
           // Field: Default to first available field, making it easier to delete the value
           val = ctrl.getFields().results[0].children[0].id;
         }
         ctrl.args.push({
           type: ctrl.exprTypes[exprType].type,
           flag_before: _.filter(_.keys(param.flag_before))[0],
+          flag_after: _.filter(_.keys(param.flag_after))[0],
           name: param.name,
           value: val
         });
         _.each(ctrl.fn.params, function(param, index) {
           while (
             (ctrl.args.length - index < param.min_expr) &&
-            // TODO: Handle named params like "ORDER BY"
-            !(param.name && param.optional) &&
+            // Exclude 'api_default' params (should not be changed by the user)
+            !param.api_default &&
             (!param.optional || param.must_be.length === 1)
           ) {
-            ctrl.addArg(param.must_be[0]);
+            ctrl.addArg(param.must_be[0], param.optional);
           }
         });
       }
             ctrl.args.splice(pos, 0, {
               type: exprType ? ctrl.exprTypes[exprType].type : null,
               flag_before: _.filter(_.keys(ctrl.fn.params[pos].flag_before))[0],
+              flag_after: _.filter(_.keys(ctrl.fn.params[pos].flag_after))[0],
               name: ctrl.fn.params[pos].name,
               value: exprType === 'SqlNumber' ? 0 : ''
             });
           // Update fieldArg
           var fieldParam = ctrl.fn.params[pos];
           ctrl.fieldArg.flag_before = _.keys(fieldParam.flag_before)[0];
+          ctrl.fieldArg.flag_after = _.keys(fieldParam.flag_after)[0];
           ctrl.fieldArg.name = fieldParam.name;
           initFunction();
         }
       };
 
       this.changeArg = function(index) {
-        var val = ctrl.args[index].value;
-        // Delete empty value
-        if (index && !val && val !== 0 && ctrl.args.length > ctrl.fn.params[0].min_expr) {
+        var val = ctrl.args[index].value,
+          param = ctrl.getParam(index);
+        // Delete empty value if allowed
+        if (index && !val && val !== 0 && !param.optional && ctrl.args.length > param.min_expr) {
           ctrl.args.splice(index, 1);
         }
         ctrl.writeExpr();
           var args = _.transform(ctrl.args, function(args, arg, index) {
             if (arg.value || arg.value === 0 || arg.flag_before) {
               var prefix = arg.flag_before || arg.name ? (index ? ' ' : '') + (arg.flag_before || arg.name) + (arg.value ? ' ' : '') : (index ? ', ' : '');
-              args.push(prefix + (arg.type === 'string' ? JSON.stringify(arg.value) : arg.value));
+              var suffix = arg.flag_after ? ' ' + arg.flag_after : '';
+              args.push(prefix + (arg.type === 'string' ? JSON.stringify(arg.value) : arg.value) + suffix);
             }
           });
           // Replace fake function "e"
index 431c2b15d2446fcc68cff1d3d849ce384eadd8fd..08ef0944416687a772314ebf8ed033fa293392e0 100644 (file)
@@ -4,12 +4,13 @@
 <label ng-hide="$ctrl.mode !== 'select' && !$ctrl.fn">{{ $ctrl.fieldArg.field.label }}</label>
 
 <div class="form-group" ng-repeat="arg in $ctrl.args">
-  <crm-search-function-flag ng-if="$ctrl.fn" arg="arg" param="$ctrl.getParam($index)" write-expr="$ctrl.writeExpr()"></crm-search-function-flag>
+  <crm-search-function-flag ng-if="$ctrl.fn" flag="flag_before" arg="arg" param="$ctrl.getParam($index)" write-expr="$ctrl.writeExpr()"></crm-search-function-flag>
   <span ng-switch="arg.type" ng-if="arg !== $ctrl.fieldArg">
     <input ng-switch-when="number" class="form-control" type="number" ng-model="arg.value" placeholder="{{ $ctrl.getParam($index).label }}" ng-change="$ctrl.changeArg($index)" ng-model-options="{updateOn: 'blur'}">
     <input ng-switch-when="string" class="form-control" ng-model="arg.value" placeholder="{{ $ctrl.getParam($index).label }}" ng-change="$ctrl.changeArg($index)" ng-trim="false" ng-model-options="{updateOn: 'blur'}">
     <input ng-switch-when="field" class="form-control" ng-model="arg.value" crm-ui-select="{data: $ctrl.getFields, placeholder: $ctrl.getParam($index).label}" ng-change="$ctrl.changeArg($index)">
   </span>
+  <crm-search-function-flag ng-if="$ctrl.fn && arg.value" flag="flag_after" arg="arg" param="$ctrl.getParam($index)" write-expr="$ctrl.writeExpr()"></crm-search-function-flag>
 </div>
 <div class="btn-group" ng-if="$ctrl.canAddArg()">
   <button type="button" class="btn btn-sm btn-default dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
index cf2dc92af3c99182e0233205af02ed6826e7a73c..ee6948ada5847519c29297e1986340b6d02f38ed 100644 (file)
@@ -5,6 +5,7 @@
     bindings: {
       arg: '<',
       param: '<',
+      flag: '@',
       writeExpr: '&'
     },
     templateUrl: '~/crmSearchAdmin/crmSearchFunctionFlag.html',
@@ -13,9 +14,9 @@
         ctrl = this;
 
       this.$onInit = function() {
-        if (!ctrl.param || !ctrl.param.flag_before) {
+        if (!ctrl.param || !ctrl.param[ctrl.flag]) {
           this.widget = null;
-        } else if (_.keys(ctrl.param.flag_before).length === 2 && '' in ctrl.param.flag_before) {
+        } else if (_.keys(ctrl.param[ctrl.flag]).length === 2 && '' in ctrl.param[ctrl.flag]) {
           this.widget = 'checkbox';
         } else {
           this.widget = 'select';
index 8d9d19b4b0857be2a060520a82e1594e721402c7..f2227e5eeda8c305c74d4b0b9ad19ff42b5333f4 100644 (file)
@@ -1,12 +1,12 @@
 <span ng-switch="$ctrl.widget">
   <span ng-switch-when="checkbox">
-    <label ng-repeat="(val, label) in $ctrl.param.flag_before" ng-if="val">
-      <input type="checkbox" ng-checked="$ctrl.arg.flag_before === val" ng-click="$ctrl.arg.flag_before = ($ctrl.arg.flag_before === val ? null : val); $ctrl.writeExpr();" >
+    <label ng-repeat="(val, label) in $ctrl.param[$ctrl.flag]" ng-if="val">
+      <input type="checkbox" ng-checked="$ctrl.arg[$ctrl.flag] === val" ng-click="$ctrl.arg[$ctrl.flag] = ($ctrl.arg[$ctrl.flag] === val ? null : val); $ctrl.writeExpr();" >
       {{ label }}
     </label>
   </span>
-  <select ng-switch-when="select" class="form-control" ng-model="$ctrl.arg.flag_before" ng-change="$ctrl.writeExpr();">
-    <option ng-repeat="(val, label) in $ctrl.param.flag_before" value="{{ val }}">
+  <select ng-switch-when="select" class="form-control" ng-model="$ctrl.arg[$ctrl.flag]" ng-change="$ctrl.writeExpr();">
+    <option ng-repeat="(val, label) in $ctrl.param[$ctrl.flag]" value="{{ val }}">
       {{ label }}
     </option>
   </select>