Merge in 5.25
[civicrm-core.git] / ang / api4Explorer / Explorer.js
index ecf2d45973740570c1e723ac30eea164f1a5c989..1db4d564f80503629b035488940bb9f78c762360 100644 (file)
     });
   });
 
-  angular.module('api4Explorer').controller('Api4Explorer', function($scope, $routeParams, $location, $timeout, $http, crmUiHelp, crmApi4) {
+  angular.module('api4Explorer').controller('Api4Explorer', function($scope, $routeParams, $location, $timeout, $http, crmUiHelp, crmApi4, dialogService) {
     var ts = $scope.ts = CRM.ts();
     $scope.entities = entities;
     $scope.actions = actions;
     $scope.fields = [];
+    $scope.havingOptions = [];
     $scope.fieldsAndJoins = [];
-    $scope.selectFieldsAndJoins = [];
+    $scope.fieldsAndJoinsAndFunctions = [];
+    $scope.fieldsAndJoinsAndFunctionsAndWildcards = [];
     $scope.availableParams = {};
     $scope.params = {};
     $scope.index = '';
     $scope.selectedTab = {result: 'result', code: 'php'};
     $scope.perm = {
-      accessDebugOutput: CRM.checkPerm('access debug output')
+      accessDebugOutput: CRM.checkPerm('access debug output'),
+      editGroups: CRM.checkPerm('edit groups')
     };
     marked.setOptions({highlight: prettyPrintOne});
     var getMetaParams = {},
     $scope.status = 'default';
     $scope.loading = false;
     $scope.controls = {};
-    $scope.code = [
-      {
-        lang: 'php',
-        style: [
-          {name: 'oop', label: ts('OOP Style'), code: ''},
-          {name: 'php', label: ts('Traditional'), code: ''}
-        ]
-      },
-      {
-        lang: 'js',
-        style: [
-          {name: 'js', label: ts('Single Call'), code: ''},
-          {name: 'js2', label: ts('Batch Calls'), code: ''}
-        ]
-      },
-      {
-        lang: 'ang',
-        style: [
-          {name: 'ang', label: ts('Single Call'), code: ''},
-          {name: 'ang2', label: ts('Batch Calls'), code: ''}
-        ]
-      },
-      {
-        lang: 'cli',
-        style: [
-          {name: 'cv', label: ts('CV'), code: ''}
-        ]
-      },
-    ];
+    $scope.langs = ['php', 'js', 'ang', 'cli'];
+    $scope.code = {
+      php: [
+        {name: 'oop', label: ts('OOP Style'), code: ''},
+        {name: 'php', label: ts('Traditional'), code: ''}
+      ],
+      js: [
+        {name: 'js', label: ts('Single Call'), code: ''},
+        {name: 'js2', label: ts('Batch Calls'), code: ''}
+      ],
+      ang: [
+        {name: 'ang', label: ts('Single Call'), code: ''},
+        {name: 'ang2', label: ts('Batch Calls'), code: ''}
+      ],
+      cli: [
+        {name: 'cv', label: ts('CV'), code: ''}
+      ]
+    };
 
     if (!entities.length) {
       formatForSelect2(schema, entities, 'name', ['description']);
       }
     }
 
-    // Turn a flat array into a select2 array
-    function arrayToSelect2(array) {
-      var out = [];
-      _.each(array, function(item) {
-        out.push({id: item, text: item});
-      });
-      return out;
-    }
-
     // Reformat an existing array of objects for compatibility with select2
     function formatForSelect2(input, container, key, extra, prefix) {
       _.each(input, function(item) {
         (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) {
       return isSelectRowCount($scope.params);
     };
 
+    $scope.selectLang = function(lang) {
+      $scope.selectedTab.code = lang;
+      writeCode();
+    };
+
     function isSelectRowCount(params) {
       return params && params.select && params.select.length === 1 && params.select[0] === 'row_count';
     }
     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);
           if (typeof objectParams[name] !== 'undefined') {
             $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 (name === 'select' && actionInfo.params.having) {
+            $scope.$watchCollection('params.select', function(values) {
+              $scope.havingOptions.length = 0;
+              _.each(values, function(item) {
+                var pieces = item.split(' AS '),
+                  alias = _.trim(pieces[pieces.length - 1]);
+                $scope.havingOptions.push({id: alias, text: alias});
+              });
+            });
+          }
+          if (typeof objectParams[name] !== 'undefined' || name === 'groupBy' || name === 'select') {
             $scope.$watch('controls.' + name, function(value) {
               var field = value;
               $timeout(function() {
       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) {
           results = result + 'Count';
         }
 
-        // Write javascript
-        var js = "'" + entity + "', '" + action + "', {";
-        _.each(params, function(param, key) {
-          js += "\n  " + key + ': ' + stringify(param) +
-            (++i < paramCount ? ',' : '');
-          if (key === 'checkPermissions') {
-            js += ' // IGNORED: permissions are always enforced from client-side requests';
-          }
-        });
-        js += "\n}";
-        if (index || index === 0) {
-          js += ', ' + JSON.stringify(index);
-        }
-        code.js = "CRM.api4(" + js + ").then(function(" + results + ") {\n  // do something with " + results + " array\n}, function(failure) {\n  // handle failure\n});";
-        code.js2 = "CRM.api4({" + results + ': [' + js + "]}).then(function(batch) {\n  // do something with batch." + results + " array\n}, function(failure) {\n  // handle failure\n});";
-        code.ang = "crmApi4(" + js + ").then(function(" + results + ") {\n  // do something with " + results + " array\n}, function(failure) {\n  // handle failure\n});";
-        code.ang2 = "crmApi4({" + results + ': [' + js + "]}).then(function(batch) {\n  // do something with batch." + results + " array\n}, function(failure) {\n  // handle failure\n});";
-
-        // Write php code
-        code.php = '$' + results + " = civicrm_api4('" + entity + "', '" + action + "', [";
-        _.each(params, function(param, key) {
-          code.php += "\n  '" + key + "' => " + phpFormat(param, 4) + ',';
-        });
-        code.php += "\n]";
-        if (index || index === 0) {
-          code.php += ', ' + phpFormat(index);
-        }
-        code.php += ");";
+        switch ($scope.selectedTab.code) {
+          case 'js':
+          case 'ang':
+            // Write javascript
+            var js = "'" + entity + "', '" + action + "', {";
+            _.each(params, function(param, key) {
+              js += "\n  " + key + ': ' + stringify(param) +
+                (++i < paramCount ? ',' : '');
+              if (key === 'checkPermissions') {
+                js += ' // IGNORED: permissions are always enforced from client-side requests';
+              }
+            });
+            js += "\n}";
+            if (index || index === 0) {
+              js += ', ' + JSON.stringify(index);
+            }
+            code.js = "CRM.api4(" + js + ").then(function(" + results + ") {\n  // do something with " + results + " array\n}, function(failure) {\n  // handle failure\n});";
+            code.js2 = "CRM.api4({" + results + ': [' + js + "]}).then(function(batch) {\n  // do something with batch." + results + " array\n}, function(failure) {\n  // handle failure\n});";
+            code.ang = "crmApi4(" + js + ").then(function(" + results + ") {\n  // do something with " + results + " array\n}, function(failure) {\n  // handle failure\n});";
+            code.ang2 = "crmApi4({" + results + ': [' + js + "]}).then(function(batch) {\n  // do something with batch." + results + " array\n}, function(failure) {\n  // handle failure\n});";
+            break;
+
+          case 'php':
+            // Write php code
+            code.php = '$' + results + " = civicrm_api4('" + entity + "', '" + action + "', [";
+            _.each(params, function(param, key) {
+              code.php += "\n  '" + key + "' => " + phpFormat(param, 4) + ',';
+            });
+            code.php += "\n]";
+            if (index || index === 0) {
+              code.php += ', ' + phpFormat(index);
+            }
+            code.php += ");";
+
+            // Write oop code
+            code.oop = '$' + results + " = " + formatOOP(entity, action, params, 2) + "\n  ->execute()";
+            if (isSelectRowCount(params)) {
+              code.oop += "\n  ->count()";
+            } else if (_.isNumber(index)) {
+              code.oop += !index ? '\n  ->first()' : (index === -1 ? '\n  ->last()' : '\n  ->itemAt(' + index + ')');
+            } else if (index) {
+              if (_.isString(index) || (_.isPlainObject(index) && !index[0] && !index['0'])) {
+                code.oop += "\n  ->indexBy('" + (_.isPlainObject(index) ? _.keys(index)[0] : index) + "')";
+              }
+              if (_.isArray(index) || _.isPlainObject(index)) {
+                code.oop += "\n  ->column('" + (_.isArray(index) ? index[0] : _.values(index)[0]) + "')";
+              }
+            }
+            code.oop += ";\n";
+            if (!_.isNumber(index) && !isSelectRowCount(params)) {
+              code.oop += "foreach ($" + results + ' as $' + ((_.isString(index) && index) ? index + ' => $' : '') + result + ') {\n  // do something\n}';
+            }
+            break;
 
-        // Write oop code
-        code.oop = '$' + results + " = " + formatOOP(entity, action, params, 2) + "\n  ->execute()";
-        if (isSelectRowCount(params)) {
-          code.oop += "\n  ->count()";
-        } else if (_.isNumber(index)) {
-          code.oop += !index ? '\n  ->first()' : (index === -1 ? '\n  ->last()' : '\n  ->itemAt(' + index + ')');
-        } else if (index) {
-          if (_.isString(index) || (_.isPlainObject(index) && !index[0] && !index['0'])) {
-            code.oop += "\n  ->indexBy('" + (_.isPlainObject(index) ? _.keys(index)[0] : index) + "')";
-          }
-          if (_.isArray(index) || _.isPlainObject(index)) {
-            code.oop += "\n  ->column('" + (_.isArray(index) ? index[0] : _.values(index)[0]) + "')";
-          }
+          case 'cli':
+            // Write cli code
+            code.cv = 'cv api4 ' + entity + '.' + action + " '" + stringify(params) + "'";
         }
-        code.oop += ";\n";
-        if (!_.isNumber(index) && !isSelectRowCount(params)) {
-          code.oop += "foreach ($" + results + ' as $' + ((_.isString(index) && index) ? index + ' => $' : '') + result + ') {\n  // do something\n}';
-        }
-
-        // Write cli code
-        code.cv = 'cv api4 ' + entity + '.' + action + " '" + stringify(params) + "'";
       }
       _.each($scope.code, function(vals) {
-        _.each(vals.style, function(style) {
+        _.each(vals, function(style) {
           style.code = code[style.name] ? prettyPrintOne(code[style.name]) : '';
         });
       });
       return docs.params[name];
     };
 
+    $scope.executeDoc = function() {
+      var doc = {
+        description: ts('Runs API call on the CiviCRM database.'),
+        comment: ts('Results and debugging info will be displayed below.')
+      };
+      if ($scope.action === 'delete') {
+        doc.WARNING = ts('This API call will be executed on the real database. Deleting data cannot be undone.');
+      }
+      else if ($scope.action && $scope.action.slice(0, 3) !== 'get') {
+        doc.WARNING = ts('This API call will be executed on the real database. It cannot be undone.');
+      }
+      return doc;
+    };
+
+    $scope.saveDoc = function() {
+      return {
+        description: ts('Save API call as a smart group.'),
+        comment: ts('Create a SavedSearch using these API params to populate a smart group.') +
+          '\n\n' + ts('NOTE: you must select contact id as the only field.')
+      };
+    };
+
     $scope.$watch('params', writeCode, true);
     $scope.$watch('index', writeCode);
     writeCode();
 
+    $scope.save = function() {
+      $scope.params.limit = $scope.params.offset = 0;
+      if ($scope.params.chain.length) {
+        CRM.alert(ts('Smart groups are not compatible with API chaining.'), ts('Error'), 'error', {expires: 5000});
+        return;
+      }
+      if ($scope.params.select.length !== 1 || !_.includes($scope.params.select[0], 'id')) {
+        CRM.alert(ts('To create a smart group, the API must select contact id and no other fields.'), ts('Error'), 'error', {expires: 5000});
+        return;
+      }
+      var model = {
+        title: '',
+        description: '',
+        visibility: 'User and User Admin Only',
+        group_type: [],
+        id: null,
+        entity: $scope.entity,
+        params: JSON.parse(angular.toJson($scope.params))
+      };
+      model.params.version = 4;
+      delete model.params.chain;
+      delete model.params.debug;
+      delete model.params.limit;
+      delete model.params.offset;
+      delete model.params.orderBy;
+      delete model.params.checkPermissions;
+      var options = CRM.utils.adjustDialogDefaults({
+        width: '500px',
+        autoOpen: false,
+        title: ts('Save smart group')
+      });
+      dialogService.open('saveSearchDialog', '~/api4Explorer/SaveSearch.html', model, options);
+    };
+  });
+
+  angular.module('api4Explorer').controller('SaveSearchCtrl', function($scope, crmApi4, dialogService) {
+    var ts = $scope.ts = CRM.ts(),
+      model = $scope.model;
+    $scope.groupEntityRefParams = {
+      entity: 'Group',
+      api: {
+        params: {is_hidden: 0, is_active: 1, 'saved_search_id.api_entity': model.entity},
+        extra: ['saved_search_id', 'description', 'visibility', 'group_type']
+      },
+      select: {
+        allowClear: true,
+        minimumInputLength: 0,
+        placeholder: ts('Select existing group')
+      }
+    };
+    if (!CRM.checkPerm('administer reserved groups')) {
+      $scope.groupEntityRefParams.api.params.is_reserved = 0;
+    }
+    $scope.perm = {
+      administerReservedGroups: CRM.checkPerm('administer reserved groups')
+    };
+    $scope.options = CRM.vars.api4.groupOptions;
+    $scope.$watch('model.id', function(id) {
+      if (id) {
+        _.assign(model, $('#api-save-search-select-group').select2('data').extra);
+      }
+    });
+    $scope.cancel = function() {
+      dialogService.cancel('saveSearchDialog');
+    };
+    $scope.save = function() {
+      $('.ui-dialog:visible').block();
+      var group = model.id ? {id: model.id} : {title: model.title};
+      group.description = model.description;
+      group.visibility = model.visibility;
+      group.group_type = model.group_type;
+      group.saved_search_id = '$id';
+      var savedSearch = {
+        api_entity: model.entity,
+        api_params: model.params
+      };
+      if (group.id) {
+        savedSearch.id = model.saved_search_id;
+      }
+      crmApi4('SavedSearch', 'save', {records: [savedSearch], chain: {group: ['Group', 'save', {'records': [group]}]}})
+        .then(function(result) {
+          dialogService.close('saveSearchDialog', result[0]);
+        });
+    };
   });
 
-  angular.module('api4Explorer').directive('crmApi4WhereClause', function($timeout) {
+  angular.module('api4Explorer').directive('crmApi4Clause', function($timeout) {
     return {
       scope: {
-        data: '=crmApi4WhereClause'
+        data: '=crmApi4Clause'
       },
-      templateUrl: '~/api4Explorer/WhereClause.html',
+      templateUrl: '~/api4Explorer/Clause.html',
       link: function (scope, element, attrs) {
         var ts = scope.ts = CRM.ts();
         scope.newClause = '';
         scope.operators = CRM.vars.api4.operators;
 
         scope.addGroup = function(op) {
-          scope.data.where.push([op, []]);
+          scope.data.clauses.push([op, []]);
         };
 
         scope.removeGroup = function() {
         };
 
         scope.onSort = function(event, ui) {
-          $('.api4-where-fieldset').toggleClass('api4-sorting', event.type === 'sortstart');
+          $(element).closest('.api4-clause-fieldset').toggleClass('api4-sorting', event.type === 'sortstart');
           $('.api4-input.form-inline').css('margin-left', '');
         };
 
           var field = value;
           $timeout(function() {
             if (field) {
-              scope.data.where.push([field, '=', '']);
+              scope.data.clauses.push([field, '=', '']);
               scope.newClause = null;
             }
           });
         });
-        scope.$watch('data.where', function(values) {
+        scope.$watch('data.clauses', function(values) {
           // Remove empty values
           _.each(values, function(clause, index) {
             if (typeof clause !== 'undefined' && !clause[0]) {