Merge pull request #17362 from colemanw/dev-core-1757-hide-toggle-menu-button
[civicrm-core.git] / ang / api4Explorer / Explorer.js
index 2affbcfdf2ca640caed9d1a3089a707e9ef51520..63cb5dedd5bf9ef682c307189e69c81dedddfbc0 100644 (file)
     $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.joinTypes = [{k: false, v: ts('Optional')}, {k: true, v: ts('Required')}];
+    $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']);
       return container;
     }
 
-    function getFieldList(action) {
+    // Returns field list formatted for select2
+    function getFieldList(action, addPseudoconstant) {
       var fields = [],
         fieldInfo = _.findWhere(getEntity().actions, {name: action}).fields;
+      if (addPseudoconstant) {
+        fieldInfo = _.cloneDeep(fieldInfo);
+        addPseudoconstants(fieldInfo, addPseudoconstant);
+      }
       formatForSelect2(fieldInfo, fields, 'name', ['description', 'required', 'default_value']);
       return fields;
     }
 
-    function addJoins(fieldList, addWildcard) {
-      var fields = _.cloneDeep(fieldList),
-        fks = _.findWhere(links, {entity: $scope.entity}) || {};
-      _.each(fks.links, function(link) {
+    // Note: this function expects fieldList to be select2-formatted already
+    function addJoins(fieldList, addWildcard, addPseudoconstant) {
+      var fields = _.cloneDeep(fieldList);
+      _.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 (linkFields) {
+          if (addPseudoconstant) {
+            addPseudoconstants(linkFields, addPseudoconstant);
+          }
           fields.push({
             text: link.alias,
-            description: 'Join to ' + link.entity,
+            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
+    function addPseudoconstants(fieldList, toAdd) {
+      var optionFields = _.filter(fieldList, 'options');
+      _.each(optionFields, function(field) {
+        var pos = _.findIndex(fieldList, {name: field.name}) + 1;
+        _.each(toAdd, function(suffix) {
+          var newField = _.cloneDeep(field);
+          newField.name += ':' + suffix;
+          fieldList.splice(pos, 0, newField);
+        });
+      });
+    }
+
     $scope.help = function(title, content) {
       if (!content) {
         $scope.helpTitle = helpTitle;
       return info;
     };
 
+    // Returns field list for write params (values, defaults)
     $scope.fieldList = function(param) {
       return function() {
-        var fields = _.cloneDeep($scope.action === 'getFields' ? getFieldList($scope.params.action || 'get') : $scope.fields);
+        var fields = _.cloneDeep(getFieldList($scope.action === 'getFields' ? ($scope.params.action || 'get') : $scope.action, ['name']));
         // Disable fields that are already in use
         _.each($scope.params[param] || [], function(val) {
-          (_.findWhere(fields, {id: val[0]}) || {}).disabled = true;
+          var usedField = val[0].replace(':name', '');
+          (_.findWhere(fields, {id: usedField}) || {}).disabled = true;
+          (_.findWhere(fields, {id: usedField + ':name'}) || {}).disabled = true;
         });
         return {results: fields};
       };
       }
     };
 
-    $scope.isSpecial = function(name) {
-      var specialParams = ['select', 'fields', 'action', 'where', 'values', 'defaults', 'orderBy', 'chain', 'groupBy', 'having'];
+    // Gets params that should be represented as generic input fields in the explorer
+    // This fn doesn't have to be particularly efficient as its output is cached in one-time bindings
+    $scope.getGenericParams = function(paramType, defaultNull) {
+      // Returns undefined if params are not yet set; one-time bindings will stabilize when this function returns a value
+      if (_.isEmpty($scope.availableParams)) {
+        return;
+      }
+      var specialParams = ['select', 'fields', 'action', 'where', 'values', 'defaults', 'orderBy', 'chain', 'groupBy', 'having', 'join'];
       if ($scope.availableParams.limit && $scope.availableParams.offset) {
         specialParams.push('limit', 'offset');
       }
-      return _.contains(specialParams, name);
+      return _.transform($scope.availableParams, function(genericParams, param, name) {
+        if (!_.contains(specialParams, name) &&
+          !(typeof paramType !== 'undefined' && !_.contains(paramType, param.type[0])) &&
+          !(typeof defaultNull !== 'undefined' && ((param.default === null) !== defaultNull))
+        ) {
+          genericParams[name] = param;
+        }
+      });
     };
 
     $scope.selectRowCount = function() {
       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';
     }
         var actionInfo = _.findWhere(actions, {id: $scope.action});
         $scope.fields = getFieldList($scope.action);
         if (_.contains(['get', 'update', 'delete', 'replace'], $scope.action)) {
-          $scope.fieldsAndJoins = addJoins($scope.fields);
-          var fieldsAndFunctions = _.cloneDeep($scope.fields);
+          $scope.fieldsAndJoins = addJoins(getFieldList($scope.action, ['name']));
+          var functions = [];
           // SQL functions are supported if HAVING is
           if (actionInfo.params.having) {
-            fieldsAndFunctions.push({
+            functions.push({
               text: ts('FUNCTION'),
               description: ts('Calculate result of a SQL function'),
               children: _.transform(CRM.vars.api4.functions, function(result, fn) {
               })
             });
           }
-          $scope.fieldsAndJoinsAndFunctions = addJoins(fieldsAndFunctions, true);
-          $scope.fieldsAndJoinsAndFunctionsAndWildcards = addJoins(fieldsAndFunctions, true);
+          $scope.fieldsAndJoinsAndFunctions = addJoins($scope.fields.concat(functions), true);
+          $scope.fieldsAndJoinsAndFunctionsAndWildcards = addJoins(getFieldList($scope.action, ['name', 'label']).concat(functions), true, ['name', 'label']);
         } else {
-          $scope.fieldsAndJoins = $scope.fields;
+          $scope.fieldsAndJoins = getFieldList($scope.action, ['name']);
           $scope.fieldsAndJoinsAndFunctions = $scope.fields;
-          $scope.fieldsAndJoinsAndFunctionsAndWildcards = _.cloneDeep($scope.fields);
+          $scope.fieldsAndJoinsAndFunctionsAndWildcards = getFieldList($scope.action, ['name', 'label']);
         }
         $scope.fieldsAndJoinsAndFunctionsAndWildcards.unshift({id: '*', text: '*', 'description': 'All core ' + $scope.entity + ' fields'});
         _.each(actionInfo.params, function (param, name) {
             if (name === 'values') {
               defaultVal = defaultValues(defaultVal);
             }
+            if (name === 'loadOptions' && $scope.action === 'getFields') {
+              param.options = [
+                false,
+                true,
+                ['id', 'name', 'label'],
+                ['id', 'name', 'label', 'abbr', 'description', 'color', 'icon']
+              ];
+              format = 'json';
+              defaultVal = false;
+              param.type = ['string'];
+            }
             $scope.$bindToRoute({
               expr: 'params["' + name + '"]',
               param: name,
               $scope.havingOptions.length = 0;
               _.each(values, function(item) {
                 var pieces = item.split(' AS '),
-                  alias = _.trim(pieces[pieces.length - 1]);
+                  alias = _.trim(pieces[pieces.length - 1]).replace(':label', ':name');
                 $scope.havingOptions.push({id: alias, text: alias});
               });
             });
           }
-          if (typeof objectParams[name] !== 'undefined' || name === 'groupBy' || name === 'select') {
+          if (typeof objectParams[name] !== 'undefined' || name === 'groupBy' || name === 'select' || name === 'join') {
             $scope.$watch('controls.' + name, function(value) {
               var field = value;
               $timeout(function() {
                 if (field) {
-                  if (typeof objectParams[name] === 'undefined') {
+                  if (name === 'join') {
+                    $scope.params[name].push([field + ' AS ' + _.snakeCase(field), false, '[]']);
+                  }
+                  else if (typeof objectParams[name] === 'undefined') {
                     $scope.params[name].push(field);
                   } else {
                     var defaultOp = _.cloneDeep(objectParams[name]);
           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]) + "')";
-          }
-        }
-        code.oop += ";\n";
-        if (!_.isNumber(index) && !isSelectRowCount(params)) {
-          code.oop += "foreach ($" + results + ' as $' + ((_.isString(index) && index) ? index + ' => $' : '') + result + ') {\n  // do something\n}';
+          case 'cli':
+            // Write cli code
+            code.cv = 'cv api4 ' + entity + '.' + action + " '" + stringify(params) + "'";
         }
-
-        // 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]) : '';
         });
       });
     }
 
     $scope.execute = function() {
-      $scope.status = 'warning';
+      $scope.status = 'info';
       $scope.loading = true;
       $http.post(CRM.url('civicrm/ajax/api4/' + $scope.entity + '/' + $scope.action, {
         params: angular.toJson(getParams()),
         }
       }).then(function(resp) {
           $scope.loading = false;
-          $scope.status = 'success';
+          $scope.status = resp.data && resp.data.debug && resp.data.debug.log ? 'warning' : 'success';
           $scope.debug = debugFormat(resp.data);
           $scope.result = [formatMeta(resp.data), prettyPrintOne(_.escape(JSON.stringify(resp.data.values, null, 2)), 'js', 1)];
         }, function(resp) {
     $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.'),
+        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.')
       };
     };
 
     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: '',
         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.offset;
+      delete model.params.orderBy;
       delete model.params.checkPermissions;
       var options = CRM.utils.adjustDialogDefaults({
         width: '500px',
               $el.crmDatepicker({time: (field.input_attrs && field.input_attrs.time) || false});
             }
           } else if (_.includes(['=', '!=', '<>', 'IN', 'NOT IN'], op) && (field.fk_entity || field.options || dataType === 'Boolean')) {
-            if (field.fk_entity) {
-              $el.crmEntityRef({entity: field.fk_entity, select:{multiple: multi}});
-            } else if (field.options) {
+           if (field.options) {
+              var id = field.pseudoconstant || 'id';
               $el.addClass('loading').attr('placeholder', ts('- select -')).crmSelect2({multiple: multi, data: [{id: '', text: ''}]});
               loadFieldOptions(field.entity || entity).then(function(data) {
-                var options = [];
-                _.each(_.findWhere(data, {name: field.name}).options, function(val, key) {
-                  options.push({id: key, text: val});
-                });
-                $el.removeClass('loading').select2({data: options, multiple: multi});
+                var options = _.transform(data[field.name].options, function(options, opt) {
+                  options.push({id: opt[id], text: opt.label, description: opt.description, color: opt.color, icon: opt.icon});
+                }, []);
+                $el.removeClass('loading').crmSelect2({data: options, multiple: multi});
               });
+            } else if (field.fk_entity) {
+              $el.crmEntityRef({entity: field.fk_entity, select:{multiple: multi}});
             } else if (dataType === 'Boolean') {
               $el.attr('placeholder', ts('- select -')).crmSelect2({allowClear: false, multiple: multi, placeholder: ts('- select -'), data: [
                 {id: 'true', text: ts('Yes')},
         function loadFieldOptions(entity) {
           if (!fieldOptions[entity + action]) {
             fieldOptions[entity + action] = crmApi4(entity, 'getFields', {
-              loadOptions: true,
+              loadOptions: ['id', 'name', 'label', 'description', 'color', 'icon'],
               action: action,
-              where: [["options", "!=", false]],
-              select: ["name", "options"]
-            });
+              where: [['options', '!=', false]],
+              select: ['options']
+            }, 'name');
           }
           return fieldOptions[entity + action];
         }
   }
 
   function getField(fieldName, entity, action) {
+    var suffix = fieldName.split(':')[1];
+    fieldName = fieldName.split(':')[0];
     var fieldNames = fieldName.split('.');
-    return get(entity, fieldNames);
+    var field = get(entity, fieldNames);
+    if (field && suffix) {
+      field.pseudoconstant = suffix;
+    }
+    return field;
 
     function get(entity, fieldNames) {
       if (fieldNames.length === 1) {
         return comboName;
       }
       var linkName = fieldNames.shift(),
-        entityLinks = _.findWhere(links, {entity: entity}).links,
-        newEntity = _.findWhere(entityLinks, {alias: linkName}).entity;
+        newEntity = _.findWhere(links[entity], {alias: linkName}).entity;
       return get(newEntity, fieldNames);
     }
   }