APIv4 Explorer - Support SQL functions
authorColeman Watts <coleman@civicrm.org>
Wed, 8 Apr 2020 20:34:57 +0000 (16:34 -0400)
committerColeman Watts <coleman@civicrm.org>
Thu, 9 Apr 2020 20:24:50 +0000 (16:24 -0400)
Adding "track by" to the loops is necessary so they aren't re-drawn during typing (causing the input to lose focus on every keystroke)

ang/api4Explorer/Explorer.html
ang/api4Explorer/Explorer.js
css/api4-explorer.css

index 02b125fcfbcee609d494046cf0f75b0ec237c542..752a912bbb1480fe25fb3728e9b2fe256e70ce28 100644 (file)
           <fieldset class="api4-input form-inline" ng-mouseenter="help('select', availableParams.select)" ng-mouseleave="help()" ng-if="availableParams.select && !isSelectRowCount()">
             <legend>select<span class="crm-marker" ng-if="availableParams.select.required"> *</span></legend>
             <div ng-model="params.select" ui-sortable="{axis: 'y'}">
-              <div class="api4-input form-inline" ng-repeat="item in params.select">
+              <div class="api4-input form-inline" ng-repeat="item in params.select track by $index">
                 <i class="crm-i fa-arrows"></i>
-                <input class="collapsible-optgroups form-control" ng-model="item" crm-ui-select="{data: selectFieldsAndJoins, allowClear: true, placeholder: 'Field'}" />
+                <input class="form-control huge" type="text" ng-model="params.select[$index]" />
+                <a href class="crm-hover-button" title="Clear" ng-click="clearParam('select', $index)"><i class="crm-i fa-times"></i></a>
               </div>
             </div>
             <div class="api4-input form-inline">
-              <input class="collapsible-optgroups form-control" ng-model="controls.select" crm-ui-select="{data: selectFieldsAndJoins}" placeholder="Add select" />
+              <input class="collapsible-optgroups form-control huge" ng-model="controls.select" crm-ui-select="{data: fieldsAndJoinsAndFunctionsAndWildcards}" placeholder="Add select" />
             </div>
           </fieldset>
           <div class="api4-input form-inline" ng-mouseenter="help('fields', availableParams.fields)" ng-mouseleave="help()"ng-if="availableParams.fields">
           <fieldset ng-if="availableParams.groupBy" ng-mouseenter="help('groupBy', availableParams.groupBy)" ng-mouseleave="help()">
             <legend>groupBy<span class="crm-marker" ng-if="availableParams.groupBy.required"> *</span></legend>
             <div ng-model="params.groupBy" ui-sortable="{axis: 'y'}">
-              <div class="api4-input form-inline" ng-repeat="(pos, field) in params.groupBy">
+              <div class="api4-input form-inline" ng-repeat="item in params.groupBy track by $index">
                 <i class="crm-i fa-arrows"></i>
-                <input class="collapsible-optgroups form-control" ng-model="params.groupBy[pos]" crm-ui-select="{data: fieldsAndJoins, allowClear: true, placeholder: 'Field'}" />
+                <input class="form-control huge" type="text" ng-model="params.groupBy[$index]" />
+                <a href class="crm-hover-button" title="Clear" ng-click="clearParam('groupBy', $index)"><i class="crm-i fa-times"></i></a>
               </div>
             </div>
             <div class="api4-input form-inline">
-              <input class="collapsible-optgroups form-control" ng-model="controls.groupBy" crm-ui-select="{data: fieldsAndJoins}" placeholder="Add groupBy" />
+              <input class="collapsible-optgroups form-control huge" ng-model="controls.groupBy" crm-ui-select="{data: fieldsAndJoinsAndFunctions}" placeholder="Add groupBy" />
             </div>
           </fieldset>
           <fieldset ng-if="availableParams.orderBy" ng-mouseenter="help('orderBy', availableParams.orderBy)" ng-mouseleave="help()">
             <div ng-model="params.orderBy" ui-sortable="{axis: 'y'}">
               <div class="api4-input form-inline" ng-repeat="clause in params.orderBy">
                 <i class="crm-i fa-arrows"></i>
-                <input class="collapsible-optgroups form-control" ng-model="clause[0]" crm-ui-select="{data: fieldsAndJoins, allowClear: true, placeholder: 'Field'}" />
+                <input class="form-control huge" type="text" ng-model="clause[0]" />
                 <select class="form-control" ng-model="clause[1]">
                   <option value="ASC">ASC</option>
                   <option value="DESC">DESC</option>
                 </select>
+                <a href class="crm-hover-button" title="Clear" ng-click="clearParam('orderBy', $index)"><i class="crm-i fa-times"></i></a>
               </div>
             </div>
             <div class="api4-input form-inline">
-              <input class="collapsible-optgroups form-control" ng-model="controls.orderBy" crm-ui-select="{data: fieldsAndJoins}" placeholder="Add orderBy" />
+              <input class="collapsible-optgroups form-control huge" ng-model="controls.orderBy" crm-ui-select="{data: fieldsAndJoinsAndFunctions}" placeholder="Add orderBy" />
             </div>
           </fieldset>
           <fieldset ng-if="availableParams.limit && availableParams.offset">
index 9d6d26a075327be43713a15a12641f17906ddefb..413fc07b516ec8fc01d153dfde146fc5c775f997 100644 (file)
@@ -26,7 +26,8 @@
     $scope.actions = actions;
     $scope.fields = [];
     $scope.fieldsAndJoins = [];
-    $scope.selectFieldsAndJoins = [];
+    $scope.fieldsAndJoinsAndFunctions = [];
+    $scope.fieldsAndJoinsAndFunctionsAndWildcards = [];
     $scope.availableParams = {};
     $scope.params = {};
     $scope.index = '';
         (row.description ? '<div class="crm-select2-row-description"><p>' + _.escape(row.description) + '</p></div>' : '');
     };
 
-    $scope.clearParam = function(name) {
-      $scope.params[name] = $scope.availableParams[name].default;
+    $scope.clearParam = function(name, idx) {
+      if (typeof idx === 'undefined') {
+        $scope.params[name] = $scope.availableParams[name].default;
+      } else {
+        $scope.params[name].splice(idx, 1);
+      }
     };
 
     $scope.isSpecial = function(name) {
     function selectAction() {
       $scope.action = $routeParams.api4action;
       $scope.fieldsAndJoins.length = 0;
-      $scope.selectFieldsAndJoins.length = 0;
+      $scope.fieldsAndJoinsAndFunctions.length = 0;
+      $scope.fieldsAndJoinsAndFunctionsAndWildcards.length = 0;
       if (!actions.length) {
         formatForSelect2(getEntity().actions, actions, 'name', ['description', 'params']);
       }
         $scope.fields = getFieldList($scope.action);
         if (_.contains(['get', 'update', 'delete', 'replace'], $scope.action)) {
           $scope.fieldsAndJoins = addJoins($scope.fields);
-          $scope.selectFieldsAndJoins = addJoins($scope.fields, true);
+          var fieldsAndFunctions = _.cloneDeep($scope.fields);
+          // SQL functions are supported if HAVING is
+          if (actionInfo.params.having) {
+            fieldsAndFunctions.push({
+              text: ts('FUNCTION'),
+              description: ts('Calculate result of a SQL function'),
+              children: _.transform(CRM.vars.api4.functions, function(result, fn) {
+                result.push({
+                  id: fn.name + '() AS ' + fn.name.toLowerCase(),
+                  text: fn.name + '()',
+                  description: fn.name + '(' + describeSqlFn(fn.params) + ')'
+                });
+              })
+            });
+          }
+          $scope.fieldsAndJoinsAndFunctions = addJoins(fieldsAndFunctions, true);
+          $scope.fieldsAndJoinsAndFunctionsAndWildcards = addJoins(fieldsAndFunctions, true);
         } else {
           $scope.fieldsAndJoins = $scope.fields;
-          $scope.selectFieldsAndJoins = _.cloneDeep($scope.fields);
+          $scope.fieldsAndJoinsAndFunctions = $scope.fields;
+          $scope.fieldsAndJoinsAndFunctionsAndWildcards = _.cloneDeep($scope.fields);
         }
-        $scope.selectFieldsAndJoins.unshift({id: '*', text: '*', 'description': 'All core ' + $scope.entity + ' fields'});
+        $scope.fieldsAndJoinsAndFunctionsAndWildcards.unshift({id: '*', text: '*', 'description': 'All core ' + $scope.entity + ' fields'});
         _.each(actionInfo.params, function (param, name) {
           var format,
             defaultVal = _.cloneDeep(param.default);
               deep: format === 'json'
             });
           }
-          if (typeof objectParams[name] !== 'undefined' || name === 'groupBy' || name === 'select') {
-            $scope.$watch('params.' + name, function(values) {
+          if (typeof objectParams[name] !== 'undefined' && name !== 'orderBy') {
+            $scope.$watch('params.' + name, function (values) {
               // Remove empty values
-              _.each(values, function(clause, index) {
+              _.each(values, function (clause, index) {
                 if (!clause || !clause[0]) {
-                  $scope.params[name].splice(index, 1);
+                  $scope.clearParam(name, index);
                 }
               });
             }, true);
+          }
+          if (typeof objectParams[name] !== 'undefined' || name === 'groupBy' || name === 'select') {
             $scope.$watch('controls.' + name, function(value) {
               var field = value;
               $timeout(function() {
                 if (field) {
-                  if (name === 'groupBy' || name === 'select') {
+                  if (typeof objectParams[name] === 'undefined') {
                     $scope.params[name].push(field);
                   } else {
                     var defaultOp = _.cloneDeep(objectParams[name]);
       writeCode();
     }
 
+    function describeSqlFn(params) {
+      var desc = ' ';
+      _.each(params, function(param) {
+        desc += ' ';
+        if (param.prefix) {
+          desc += _.filter(param.prefix).join('|') + ' ';
+        }
+        if (param.expr === 1) {
+          desc += 'expr ';
+        } else if (param.expr > 1) {
+          desc += 'expr, ... ';
+        }
+        if (param.suffix) {
+          desc += ' ' + _.filter(param.suffix).join('|') + ' ';
+        }
+      });
+      return desc.replace(/[ ]+/g, ' ');
+    }
+
     function defaultValues(defaultVal) {
       _.each($scope.fields, function(field) {
         if (field.required) {
index 8dcc817cf6f6a2eb5d33455598882c898de4d31b..64403ba70d54ac0975be615f4d724be35e1be273 100644 (file)
@@ -203,6 +203,10 @@ div.select2-drop.collapsible-optgroups-enabled .select2-result-with-children.opt
   content: "\f0d7";
 }
 
+#bootstrap-theme.api4-explorer-page .form-control.huge {
+  width: 25em;
+}
+
 /* Another weird shoreditch fix */
 #bootstrap-theme .form-inline div.checkbox {
   margin-right: 1em;