APIv4 Explorer - Support selectable fields from explicit joins
authorColeman Watts <coleman@civicrm.org>
Tue, 23 Jun 2020 14:42:09 +0000 (10:42 -0400)
committerColeman Watts <coleman@civicrm.org>
Tue, 23 Jun 2020 14:53:15 +0000 (10:53 -0400)
ang/api4Explorer/Explorer.html
ang/api4Explorer/Explorer.js

index fe618337ec3c7705dfa42247a02e1a16a9e6bc74..a06a7ba5f60fe94dfb7a5de2b4064c424324fb6a 100644 (file)
@@ -69,7 +69,7 @@
               <fieldset ng-repeat="item in params.join track by $index">
                 <div class="api4-input form-inline">
                   <i class="crm-i fa-arrows"></i>
-                  <input class="form-control twenty" type="text" ng-model="params.join[$index][0]" />
+                  <input class="form-control twenty" type="text" ng-model="params.join[$index][0]" ng-model-options="{updateOn: 'blur'}" ng-change="$ctrl.buildFieldList()"/>
                   <select class="form-control" ng-model="params.join[$index][1]" ng-options="o.k as o.v for o in ::joinTypes" ></select>
                   <a href class="crm-hover-button" title="Clear" ng-click="clearParam('join', $index)"><i class="crm-i fa-times"></i></a>
                 </div>
index faacc68efa3a63d709334a9d396b69c542a58164..d9d8d425e9f44b4968f1d1bb7038cc29cf10f7f7 100644 (file)
@@ -10,6 +10,8 @@
   var actions = [];
   // Field options
   var fieldOptions = {};
+  // Api params
+  var params;
 
 
   angular.module('api4Explorer').config(function($routeProvider) {
@@ -21,7 +23,8 @@
   });
 
   angular.module('api4Explorer').controller('Api4Explorer', function($scope, $routeParams, $location, $timeout, $http, crmUiHelp, crmApi4, dialogService) {
-    var ts = $scope.ts = CRM.ts();
+    var ts = $scope.ts = CRM.ts(),
+      ctrl = $scope.$ctrl = this;
     $scope.entities = entities;
     $scope.actions = actions;
     $scope.fields = [];
@@ -31,7 +34,7 @@
     $scope.fieldsAndJoinsAndFunctionsWithSuffixes = [];
     $scope.fieldsAndJoinsAndFunctionsAndWildcards = [];
     $scope.availableParams = {};
-    $scope.params = {};
+    params = $scope.params = {};
     $scope.index = '';
     $scope.selectedTab = {result: 'result', code: 'php'};
     $scope.perm = {
       return container;
     }
 
-    // Returns field list formatted for select2
-    function getFieldList(action, addPseudoconstant) {
-      var fields = [],
-        fieldInfo = _.findWhere(getEntity().actions, {name: action}).fields;
+    // Replaces contents of fieldList array with current fields formatted for select2
+    function getFieldList(fieldList, action, addPseudoconstant) {
+      var fieldInfo = _.cloneDeep(_.findWhere(getEntity().actions, {name: action}).fields);
+      fieldList.length = 0;
       if (addPseudoconstant) {
-        fieldInfo = _.cloneDeep(fieldInfo);
         addPseudoconstants(fieldInfo, addPseudoconstant);
       }
-      formatForSelect2(fieldInfo, fields, 'name', ['description', 'required', 'default_value']);
-      return fields;
+      formatForSelect2(fieldInfo, fieldList, 'name', ['description', 'required', 'default_value']);
     }
 
     // Note: this function expects fieldList to be select2-formatted already
     function addJoins(fieldList, addWildcard, addPseudoconstant) {
-      var fields = _.cloneDeep(fieldList);
+      // Add entities specified by the join param
+      _.each(getExplicitJoins(), function(joinEntity, joinAlias) {
+        var wildCard = addWildcard ? [{id: joinAlias + '.*', text: joinAlias + '.*', 'description': 'All core ' + joinEntity + ' fields'}] : [],
+          joinFields = _.cloneDeep(entityFields(joinEntity));
+        if (joinFields) {
+          if (addPseudoconstant) {
+            addPseudoconstants(joinFields, addPseudoconstant);
+          }
+          fieldList.push({
+            text: joinEntity + ' AS ' + joinAlias,
+            description: 'Explicit join to ' + joinEntity,
+            children: wildCard.concat(formatForSelect2(joinFields, [], 'name', ['description'], joinAlias + '.'))
+          });
+        }
+      });
+      // Add implicit joins based on schema links
       _.each(links[$scope.entity], function(link) {
         var linkFields = _.cloneDeep(entityFields(link.entity)),
           wildCard = addWildcard ? [{id: link.alias + '.*', text: link.alias + '.*', 'description': 'All core ' + link.entity + ' fields'}] : [];
           if (addPseudoconstant) {
             addPseudoconstants(linkFields, addPseudoconstant);
           }
-          fields.push({
+          fieldList.push({
             text: link.alias,
             description: 'Implicit join to ' + link.entity,
             children: wildCard.concat(formatForSelect2(linkFields, [], 'name', ['description'], link.alias + '.'))
           });
         }
       });
-      return fields;
     }
 
     // Note: this function transforms a raw list a-la getFields; not a select2-formatted list
     // Returns field list for write params (values, defaults)
     $scope.fieldList = function(param) {
       return function() {
-        var fields = _.cloneDeep(getFieldList($scope.action === 'getFields' ? ($scope.params.action || 'get') : $scope.action, ['name']));
+        var fields = [];
+        getFieldList(fields, $scope.action === 'getFields' ? ($scope.params.action || 'get') : $scope.action, ['name']);
         // Disable fields that are already in use
         _.each($scope.params[param] || [], function(val) {
           var usedField = val[0].replace(':name', '');
     }
 
     function parseYaml(input) {
-      if (typeof input === 'undefined') {
-        return undefined;
+      if (typeof input === 'undefined' || input === '') {
+        return input;
       }
-      if (input === '') {
-        return '';
+      // Return literal quoted string without removing quotes - for the sake of JOIN ON clauses
+      if (_.isString(input) && input[0] === input[input.length - 1] && _.includes(["'", '"'], input[0])) {
+        return input;
       }
       if (_.isObject(input) || _.isArray(input)) {
         _.each(input, function(item, index) {
       }
     }
 
+    this.buildFieldList = function() {
+      var actionInfo = _.findWhere(actions, {id: $scope.action});
+      getFieldList($scope.fields, $scope.action);
+      getFieldList($scope.fieldsAndJoins, $scope.action, ['name']);
+      getFieldList($scope.fieldsAndJoinsAndFunctions, $scope.action);
+      getFieldList($scope.fieldsAndJoinsAndFunctionsWithSuffixes, $scope.action, ['name', 'label']);
+      getFieldList($scope.fieldsAndJoinsAndFunctionsAndWildcards, $scope.action, ['name', 'label']);
+      if (_.contains(['get', 'update', 'delete', 'replace'], $scope.action)) {
+        addJoins($scope.fieldsAndJoins);
+        // SQL functions are supported if HAVING is
+        if (actionInfo.params.having) {
+          var functions = {
+            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.push(functions);
+          $scope.fieldsAndJoinsAndFunctionsWithSuffixes.push(functions);
+          $scope.fieldsAndJoinsAndFunctionsAndWildcards.push(functions);
+        }
+        addJoins($scope.fieldsAndJoinsAndFunctions, true);
+        addJoins($scope.fieldsAndJoinsAndFunctionsWithSuffixes, false, ['name', 'label']);
+        addJoins($scope.fieldsAndJoinsAndFunctionsAndWildcards, true, ['name', 'label']);
+      }
+      $scope.fieldsAndJoinsAndFunctionsAndWildcards.unshift({id: '*', text: '*', 'description': 'All core ' + $scope.entity + ' fields'});
+    };
+
     function selectAction() {
       $scope.action = $routeParams.api4action;
-      $scope.fieldsAndJoins.length = 0;
-      $scope.fieldsAndJoinsAndFunctions.length = 0;
-      $scope.fieldsAndJoinsAndFunctionsWithSuffixes.length = 0;
-      $scope.fieldsAndJoinsAndFunctionsAndWildcards.length = 0;
       if (!actions.length) {
         formatForSelect2(getEntity().actions, actions, 'name', ['description', 'params']);
       }
       if ($scope.action) {
         var actionInfo = _.findWhere(actions, {id: $scope.action});
-        $scope.fields = getFieldList($scope.action);
-        if (_.contains(['get', 'update', 'delete', 'replace'], $scope.action)) {
-          $scope.fieldsAndJoins = addJoins(getFieldList($scope.action, ['name']));
-          var functions = [];
-          // SQL functions are supported if HAVING is
-          if (actionInfo.params.having) {
-            functions.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($scope.fields.concat(functions), true);
-          $scope.fieldsAndJoinsAndFunctionsWithSuffixes = addJoins(getFieldList($scope.action, ['name', 'label']).concat(functions), false, ['name', 'label']);
-          $scope.fieldsAndJoinsAndFunctionsAndWildcards = addJoins(getFieldList($scope.action, ['name', 'label']).concat(functions), true, ['name', 'label']);
-        } else {
-          $scope.fieldsAndJoins = getFieldList($scope.action, ['name']);
-          $scope.fieldsAndJoinsAndFunctions = $scope.fields;
-          $scope.fieldsAndJoinsAndFunctionsWithSuffixes = getFieldList($scope.action, ['name', 'label']);
-          $scope.fieldsAndJoinsAndFunctionsAndWildcards = getFieldList($scope.action, ['name', 'label']);
-        }
-        $scope.fieldsAndJoinsAndFunctionsAndWildcards.unshift({id: '*', text: '*', 'description': 'All core ' + $scope.entity + ' fields'});
         _.each(actionInfo.params, function (param, name) {
           var format,
             defaultVal = _.cloneDeep(param.default);
                 if (field) {
                   if (name === 'join') {
                     $scope.params[name].push([field + ' AS ' + _.snakeCase(field), false]);
+                    ctrl.buildFieldList();
                   }
                   else if (typeof objectParams[name] === 'undefined') {
                     $scope.params[name].push(field);
             });
           }
         });
+        ctrl.buildFieldList();
         $scope.availableParams = actionInfo.params;
       }
       writeCode();
       },
       templateUrl: '~/api4Explorer/Clause.html',
       controller: function ($scope, $element, $timeout) {
-        var ts = $scope.ts = CRM.ts();
-        var ctrl = $scope.$ctrl = this;
+        var ts = $scope.ts = CRM.ts(),
+          ctrl = $scope.$ctrl = this;
         this.conjunctions = {AND: ts('And'), OR: ts('Or'), NOT: ts('Not')};
         this.operators = CRM.vars.api4.operators;
         this.sortOptions = {
     return _.result(entity, 'fields');
   }
 
+  function getExplicitJoins() {
+    return _.transform(params.join, function(joins, join) {
+      var j = join[0].split(' AS '),
+        joinEntity = _.trim(j[0]),
+        joinAlias = _.trim(j[1]) || joinEntity.toLowerCase();
+      joins[joinAlias] = joinEntity;
+    }, {});
+  }
+
   function getField(fieldName, entity, action) {
     var suffix = fieldName.split(':')[1];
     fieldName = fieldName.split(':')[0];
         return comboName;
       }
       var linkName = fieldNames.shift(),
-        newEntity = _.findWhere(links[entity], {alias: linkName}).entity;
+        newEntity = getExplicitJoins()[linkName] || _.findWhere(links[entity], {alias: linkName}).entity;
       return get(newEntity, fieldNames);
     }
   }