Merge pull request #16879 from civicrm/5.24
[civicrm-core.git] / ang / api4Explorer / Explorer.js
index 10a78d5ae6b34b371e28725cc0083d34ee431516..09db7d1d02a2023fee9ea88a87af0f7e462f5bf5 100644 (file)
@@ -20,7 +20,7 @@
     });
   });
 
-  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.availableParams = {};
     $scope.params = {};
     $scope.index = '';
-    $scope.resultTab = {selected: 'result'};
+    $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 = {},
-      objectParams = {orderBy: 'ASC', values: '', chain: ['Entity', '', '{}']},
+      objectParams = {orderBy: 'ASC', values: '', defaults: '', chain: ['Entity', '', '{}']},
       docs = CRM.vars.api4.docs,
       helpTitle = '',
       helpContent = {};
     $scope.status = 'default';
     $scope.loading = false;
     $scope.controls = {};
-    $scope.codeLabel = {
-      oop: ts('PHP (oop style)'),
-      php: ts('PHP (traditional)'),
-      js: ts('Javascript'),
-      cli: ts('Command Line')
-    };
-    $scope.code = codeDefaults();
-
-    function codeDefaults() {
-      return _.mapValues($scope.codeLabel, function(val, key) {
-        return key === 'oop' ? ts('Select an entity and action') : '';
-      });
-    }
+    $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: ''}
+        ]
+      },
+    ];
 
     if (!entities.length) {
       formatForSelect2(schema, entities, 'name', ['description']);
       return fields;
     }
 
-    $scope.help = function(title, param) {
-      if (!param) {
+    $scope.help = function(title, content) {
+      if (!content) {
         $scope.helpTitle = helpTitle;
         $scope.helpContent = helpContent;
       } else {
         $scope.helpTitle = title;
-        $scope.helpContent = param;
+        $scope.helpContent = formatHelp(content);
       }
     };
 
+    // Sets the static help text (which gets overridden by mousing over other elements)
+    function setHelp(title, content) {
+      $scope.helpTitle = helpTitle = title;
+      $scope.helpContent = helpContent = formatHelp(content);
+    }
+
+    // Convert plain-text help to markdown; replace variables and format links
+    function formatHelp(rawContent) {
+      function formatRefs(see) {
+        _.each(see, function(ref, idx) {
+          var match = ref.match(/^\\Civi\\Api4\\([a-zA-Z]+)$/);
+          if (match) {
+            ref = '#/explorer/' + match[1];
+          }
+          if (ref[0] === '\\') {
+            ref = 'https://github.com/civicrm/civicrm-core/blob/master' + ref.replace(/\\/i, '/') + '.php';
+          }
+          see[idx] = '<a target="' + (ref[0] === '#' ? '_self' : '_blank') + '" href="' + ref + '">' + see[idx] + '</a>';
+        });
+      }
+      var formatted = _.cloneDeep(rawContent);
+      if (formatted.description) {
+        formatted.description = marked(formatted.description);
+      }
+      if (formatted.comment) {
+        formatted.comment = marked(formatted.comment);
+      }
+      formatRefs(formatted.see);
+      return formatted;
+    }
+
     $scope.fieldHelp = function(fieldName) {
       var field = getField(fieldName, $scope.entity, $scope.action);
       if (!field) {
       return info;
     };
 
-    $scope.valuesFields = function() {
-      var fields = _.cloneDeep($scope.action === 'getFields' ? getFieldList($scope.params.action || 'get') : $scope.fields);
-      // Disable fields that are already in use
-      _.each($scope.params.values || [], function(val) {
-        (_.findWhere(fields, {id: val[0]}) || {}).disabled = true;
-      });
-      return {results: fields};
+    $scope.fieldList = function(param) {
+      return function() {
+        var fields = _.cloneDeep($scope.action === 'getFields' ? getFieldList($scope.params.action || 'get') : $scope.fields);
+        // Disable fields that are already in use
+        _.each($scope.params[param] || [], function(val) {
+          (_.findWhere(fields, {id: val[0]}) || {}).disabled = true;
+        });
+        return {results: fields};
+      };
     };
 
     $scope.formatSelect2Item = function(row) {
     };
 
     $scope.isSpecial = function(name) {
-      var specialParams = ['select', 'fields', 'action', 'where', 'values', 'orderBy', 'chain'];
+      var specialParams = ['select', 'fields', 'action', 'where', 'values', 'defaults', 'orderBy', 'chain'];
       return _.contains(specialParams, name);
     };
 
     };
 
     $scope.isSelectRowCount = function() {
-      return $scope.params && $scope.params.select && $scope.params.select.length === 1 && $scope.params.select[0] === 'row_count';
+      return isSelectRowCount($scope.params);
     };
 
+    function isSelectRowCount(params) {
+      return params && params.select && params.select.length === 1 && params.select[0] === 'row_count';
+    }
+
     function getEntity(entityName) {
       return _.findWhere(schema, {name: entityName || $scope.entity});
     }
               default:
                 format = 'raw';
             }
-            if (name == 'limit') {
+            if (name === 'limit') {
               defaultVal = 25;
             }
+            if (name === 'debug') {
+              defaultVal = true;
+            }
             if (name === 'values') {
               defaultVal = defaultValues(defaultVal);
             }
     }
 
     function writeCode() {
-      var code = codeDefaults(),
+      var code = {},
         entity = $scope.entity,
         action = $scope.action,
         params = getParams(),
         index = isInt($scope.index) ? +$scope.index : parseYaml($scope.index),
         result = 'result';
       if ($scope.entity && $scope.action) {
+        delete params.debug;
         if (action.slice(0, 3) === 'get') {
           result = entity.substr(0, 7) === 'Custom_' ? _.camelCase(entity.substr(7)) : entity;
           result = lcfirst(action.replace(/s$/, '').slice(3) || result);
         }
         var results = lcfirst(_.isNumber(index) ? result : pluralize(result)),
           paramCount = _.size(params),
-          isSelectRowCount = params.select && params.select.length === 1 && params.select[0] === 'row_count',
           i = 0;
 
-        if (isSelectRowCount) {
+        if (isSelectRowCount(params)) {
           results = result + 'Count';
         }
 
         // Write javascript
-        code.js = "CRM.api4('" + entity + "', '" + action + "', {";
+        var js = "'" + entity + "', '" + action + "', {";
         _.each(params, function(param, key) {
-          code.js += "\n  " + key + ': ' + stringify(param) +
+          js += "\n  " + key + ': ' + stringify(param) +
             (++i < paramCount ? ',' : '');
           if (key === 'checkPermissions') {
-            code.js += ' // IGNORED: permissions are always enforced from client-side requests';
+            js += ' // IGNORED: permissions are always enforced from client-side requests';
           }
         });
-        code.js += "\n}";
+        js += "\n}";
         if (index || index === 0) {
-          code.js += ', ' + JSON.stringify(index);
+          js += ', ' + JSON.stringify(index);
         }
-        code.js += ").then(function(" + results + ") {\n  // do something with " + results + " array\n}, function(failure) {\n  // handle failure\n});";
+        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 + "', [";
         code.php += ");";
 
         // Write oop code
-        if (entity.substr(0, 7) !== 'Custom_') {
-          code.oop = '$' + results + " = \\Civi\\Api4\\" + entity + '::' + action + '()';
-        } else {
-          code.oop = '$' + results + " = \\Civi\\Api4\\CustomValue::" + action + "('" + entity.substr(7) + "')";
-        }
-        _.each(params, function(param, key) {
-          var val = '';
-          if (typeof objectParams[key] !== 'undefined' && key !== 'chain') {
-            _.each(param, function(item, index) {
-              val = phpFormat(index) + ', ' + phpFormat(item, 4);
-              code.oop += "\n  ->add" + ucfirst(key).replace(/s$/, '') + '(' + val + ')';
-            });
-          } else if (key === 'where') {
-            _.each(param, function (clause) {
-              if (clause[0] === 'AND' || clause[0] === 'OR' || clause[0] === 'NOT') {
-                code.oop += "\n  ->addClause(" + phpFormat(clause[0]) + ", " + phpFormat(clause[1]).slice(1, -1) + ')';
-              } else {
-                code.oop += "\n  ->addWhere(" + phpFormat(clause).slice(1, -1) + ")";
-              }
-            });
-          } else if (key === 'select' && isSelectRowCount) {
-            code.oop += "\n  ->selectRowCount()";
-          } else {
-            code.oop += "\n  ->set" + ucfirst(key) + '(' + phpFormat(param, 4) + ')';
-          }
-        });
-        code.oop += "\n  ->execute()";
-        if (isSelectRowCount) {
+        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 + ')');
           }
         }
         code.oop += ";\n";
-        if (!_.isNumber(index) && !isSelectRowCount) {
+        if (!_.isNumber(index) && !isSelectRowCount(params)) {
           code.oop += "foreach ($" + results + ' as $' + ((_.isString(index) && index) ? index + ' => $' : '') + result + ') {\n  // do something\n}';
         }
 
         // Write cli code
-        code.cli = 'cv api4 ' + entity + '.' + action + " '" + stringify(params) + "'";
+        code.cv = 'cv api4 ' + entity + '.' + action + " '" + stringify(params) + "'";
+      }
+      _.each($scope.code, function(vals) {
+        _.each(vals.style, function(style) {
+          style.code = code[style.name] ? prettyPrintOne(code[style.name]) : '';
+        });
+      });
+    }
+
+    // Format oop params
+    function formatOOP(entity, action, params, indent) {
+      var code = '',
+        newLine = "\n" + _.repeat(' ', indent);
+      if (entity.substr(0, 7) !== 'Custom_') {
+        code = "\\Civi\\Api4\\" + entity + '::' + action + '()';
+      } else {
+        code = "\\Civi\\Api4\\CustomValue::" + action + "('" + entity.substr(7) + "')";
       }
-      _.each(code, function(val, type) {
-        $scope.code[type] = prettyPrintOne(_.escape(val));
+      _.each(params, function(param, key) {
+        var val = '';
+        if (typeof objectParams[key] !== 'undefined' && key !== 'chain') {
+          _.each(param, function(item, index) {
+            val = phpFormat(index) + ', ' + phpFormat(item, 2 + indent);
+            code += newLine + "->add" + ucfirst(key).replace(/s$/, '') + '(' + val + ')';
+          });
+        } else if (key === 'where') {
+          _.each(param, function (clause) {
+            if (clause[0] === 'AND' || clause[0] === 'OR' || clause[0] === 'NOT') {
+              code += newLine + "->addClause(" + phpFormat(clause[0]) + ", " + phpFormat(clause[1]).slice(1, -1) + ')';
+            } else {
+              code += newLine + "->addWhere(" + phpFormat(clause).slice(1, -1) + ")";
+            }
+          });
+        } else if (key === 'select') {
+          code += newLine;
+          // addSelect() is a variadic function & can take multiple arguments; selectRowCount() is a shortcut for addSelect('row_count')
+          code += isSelectRowCount(params) ? '->selectRowCount()' : '->addSelect(' + phpFormat(param).slice(1, -1) + ')';
+        } else if (key === 'chain') {
+          _.each(param, function(chain, name) {
+            code += newLine + "->addChain('" + name + "', " + formatOOP(chain[0], chain[1], chain[2], 2 + indent);
+            code += (chain.length > 3 ? ',' : '') + (!_.isEmpty(chain[2]) ? newLine : ' ') + (chain.length > 3 ? phpFormat(chain[3]) : '') + ')';
+          });
+        }
+        else {
+          code += newLine + "->set" + ucfirst(key) + '(' + phpFormat(param, 2 + indent) + ')';
+        }
       });
+      return code;
     }
 
     function isInt(value) {
     // Help for an entity with no action selected
     function showEntityHelp(entityName) {
       var entityInfo = getEntity(entityName);
-      $scope.helpTitle = helpTitle = $scope.entity;
-      $scope.helpContent = helpContent = {
+      setHelp($scope.entity, {
         description: entityInfo.description,
-        comment: entityInfo.comment
-      };
+        comment: entityInfo.comment,
+        see: entityInfo.see
+      });
     }
 
     if (!$scope.entity) {
-      $scope.helpTitle = helpTitle = ts('APIv4 Explorer');
-      $scope.helpContent = helpContent = {description: docs.description, comment: docs.comment};
+      setHelp(ts('APIv4 Explorer'), {description: docs.description, comment: docs.comment, see: docs.see});
     } else if (!actions.length && !getEntity().actions) {
       getMetaParams.actions = [$scope.entity, 'getActions', {chain: {fields: [$scope.entity, 'getFields', {action: '$name'}]}}];
       fetchMeta();
       if ($scope.entity && $routeParams.api4action !== newVal && !_.isUndefined(newVal)) {
         $location.url('/explorer/' + $scope.entity + '/' + newVal);
       } else if (newVal) {
-        $scope.helpTitle = helpTitle = $scope.entity + '::' + newVal;
-        $scope.helpContent = helpContent = _.pick(_.findWhere(getEntity().actions, {name: newVal}), ['description', 'comment']);
+        setHelp($scope.entity + '::' + newVal, _.pick(_.findWhere(getEntity().actions, {name: newVal}), ['description', 'comment', 'see']));
       }
     });
 
       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('Allows you to create a SavedSearch containing the WHERE clause of this API call.'),
+      };
+    };
+
     $scope.$watch('params', writeCode, true);
     $scope.$watch('index', writeCode);
     writeCode();
 
+    $scope.save = function() {
+      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.select;
+      delete model.params.chain;
+      delete model.params.debug;
+      delete model.params.limit;
+      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) {