Improve API explorer handling of operators
[civicrm-core.git] / templates / CRM / Admin / Page / APIExplorer.js
index 0a4f89e198bf79210efa9d6663385c32017a34a5..e530b27a49b566139fc6d036690e2db7d7c3d1e4 100644 (file)
     fieldTpl = _.template($('#api-param-tpl').html()),
     optionsTpl = _.template($('#api-options-tpl').html()),
     returnTpl = _.template($('#api-return-tpl').html()),
-    chainTpl = _.template($('#api-chain-tpl').html());
+    chainTpl = _.template($('#api-chain-tpl').html()),
+
+    // Operators with special properties
+    BOOL = ['IS NULL', 'IS NOT NULL'],
+    TEXT = ['LIKE', 'NOT LIKE'],
+    MULTI = ['IN', 'NOT IN', 'BETWEEN', 'NOT BETWEEN'];
 
   /**
    * Call prettyPrint function if it successfully loaded from the cdn
@@ -28,7 +33,7 @@
   function addField(name) {
     $('#api-params').append($(fieldTpl({name: name || ''})));
     var $row = $('tr:last-child', '#api-params');
-    $('.api-param-name', $row).crmSelect2({
+    $('input.api-param-name', $row).crmSelect2({
       data: fields.concat({id: '-', text: ts('Other') + '...'})
     }).change();
   }
    */
   function getActions() {
     if (entity) {
+      $('#api-action').addClass('loading');
       CRM.api3(entity, 'getactions').done(function(data) {
-        // Ensure 'get' is always an action
-        actions = _.union(['get'], data.values);
+        actions = data.values || ['get'];
         populateActions();
       });
     } else {
    * @param el
    */
   function populateActions(el) {
-    $('#api-action').select2({
+    var val = $('#api-action').val();
+    $('#api-action').removeClass('loading').select2({
       data: _.transform(actions, function(ret, item) {ret.push({text: item, id: item})})
     });
+    // If previously selected action is not available, set it to 'get' if possible
+    if (_.indexOf(actions, val) < 0) {
+      $('#api-action').select2('val', _.indexOf(actions, 'get') < 0 ? actions[0] : 'get', true);
+    }
   }
 
   /**
   }
 
   /**
-   * Add/remove option list for selected field's pseudoconstant
+   * Render value input as a textfield, option list, or hidden,
+   * Depending on selected param name and operator
    */
-  function toggleOptions() {
-    var name = $(this).val(),
-      $valField = $(this).closest('tr').find('.api-param-value');
-    if (options[name]) {
-      $valField.val('').select2({
-        multiple: true,
+  function renderValueField() {
+    var $row = $(this).closest('tr'),
+      name = $('input.api-param-name', $row).val(),
+      operator = $('.api-param-op', $row).val(),
+      operatorType = $.inArray(operator, MULTI) > -1 ? 'multi' : ($.inArray(operator, BOOL) > -1 ? 'bool' : 'single'),
+      $valField = $('input.api-param-value', $row),
+      currentVal = $valField.val();
+    // Boolean fields only have 1 possible value
+    if (operatorType == 'bool') {
+      if ($valField.data('select2')) {
+        $valField.select2('destroy');
+      }
+      $valField.css('visibility', 'hidden').val('1');
+      return;
+    }
+    $valField.css('visibility', '');
+    // Option list input
+    if (options[name] && $.inArray(operator, TEXT) < 0) {
+      // Reset value before switching to a select from something else
+      if ($(this).is('.api-param-name') || !$valField.data('select2')) {
+        $valField.val('');
+      }
+      // When switching from multi-select to single select
+      else if (operatorType == 'single' && currentVal.indexOf(',') > -1) {
+        $valField.val(currentVal.split(',')[0]);
+      }
+      $valField.select2({
+        multiple: (operatorType === 'multi'),
         data: _.transform(options[name], function(result, option) {
           result.push({id: option.key, text: option.value});
         })
       });
+      return;
     }
-    else if ($valField.data('select2')) {
+    // Plain text input
+    if ($valField.data('select2')) {
       $valField.select2('destroy');
     }
   }
   function evaluate(val, makeArray) {
     try {
       if (!val.length) {
-        return val;
+        return makeArray ? [] : '';
       }
       var first = val.charAt(0),
         last = val.slice(-1);
       // Simple types
-      if (val === 'true' || val === 'false' || val === 'null' || !isNaN(val)) {
+      if (val === 'true' || val === 'false' || val === 'null') {
         return eval(val);
       }
       // Quoted strings
         return eval('(' + val + ')');
       }
       // Transform csv to array
-      if (makeArray && val.indexOf(',') > 0) {
-        return val.split(',');
+      if (makeArray) {
+        var result = [];
+        $.each(val.split(','), function(k, v) {
+          result.push(evaluate($.trim(v)) || v);
+        });
+        return result;
+      }
+      // Integers - quote any number that starts with 0 to avoid oddities
+      if (!isNaN(val) && val.search(/[^\d]/) < 0 && (val.length === 1 || first !== '0')) {
+        return parseInt(val, 10);
       }
       // Ok ok it's really a string
       return val;
       });
       return 'array(' + ret + ')';
     }
-    return JSON.stringify(val);
+    return JSON.stringify(val).replace(/\$/g, '\\$');
   }
 
   /**
     });
     $('input.api-param-value, input.api-option-value').each(function() {
       var $row = $(this).closest('tr'),
-        val = evaluate($(this).val(), $(this).is('.select2-offscreen')),
+        op = $('select.api-param-op', $row).val() || '=',
         name = $('input.api-param-name', $row).val(),
-        op = $('select.api-param-op', $row).val() || '=';
+        val = evaluate($(this).val(), $.inArray(op, MULTI) > -1);
 
       // Ignore blank values for the return field
       if ($(this).is('#api-return-value') && !val) {
       smarty: "{crmAPI var='result' entity='" + entity + "' action='" + action + "'",
       php: "$result = civicrm_api3('" + entity + "', '" + action + "'",
       json: "CRM.api3('" + entity + "', '" + action + "'",
+      drush: "drush cvapi " + entity + '.' + action + ' ',
+      wpcli: "wp cv api " + entity + '.' + action + ' ',
       rest: CRM.config.resourceBase + "extern/rest.php?entity=" + entity + "&action=" + action + "&json=" + JSON.stringify(params) + "&api_key=yoursitekey&key=yourkey"
     };
     smartyStub = false;
       q.php += "  '" + key + "' => " + phpFormat(value) + ",\n";
       q.json += "  \"" + key + '": ' + js;
       q.smarty += ' ' + key + '=' + smartyFormat(js, key);
+      // FIXME: This is not totally correct cli syntax
+      q.drush += key + '=' + js + ' ';
+      q.wpcli += key + '=' + js + ' ';
     });
     if (i) {
       q.php += ")";
     if (action.indexOf('get') < 0) {
       q.smarty = '{* Smarty API only works with get actions *}';
     } else if (smartyStub) {
-      q.smarty = "{* Smarty does not have a syntax for array literals; assign complex variables on the server *}\n" + q.smarty;
+      q.smarty = "{* Smarty does not have a syntax for array literals; assign complex variables from php *}\n" + q.smarty;
     }
     $.each(q, function(type, val) {
       $('#api-' + type).removeClass('prettyprinted').text(val);
       alert(ts('Select an entity.'));
       return;
     }
-    if (action.indexOf('get') < 0) {
+    if (action.indexOf('get') < 0 && action != 'check') {
       var msg = action === 'delete' ? ts('This will delete data from CiviCRM. Are you sure?') : ts('This will write to the database. Continue?');
       CRM.confirm({title: ts('Confirm %1', {1: action}), message: msg}).on('crmConfirm:yes', execute);
     } else {
     }
   }
 
+  /**
+   * Execute api call and display the results
+   * Note: We have to manually execute the ajax in order to add the secret extra "prettyprint" param
+   */
   function execute() {
     $('#api-result').html('<div class="crm-loading-element"></div>');
     $.ajax({
       .on('change', '#api-entity, #api-action', function() {
         entity = $('#api-entity').val();
         if ($(this).is('#api-entity')) {
-          $('#api-action').select2('val', 'get');
           getActions();
         }
         action = $('#api-action').val();
       .on('change keyup', 'input.api-input, #api-params select', buildParams)
       .on('submit', submit);
     $('#api-params')
-      .on('change', '.api-param-name', toggleOptions)
-      .on('change', '.api-param-name, .api-option-name', function() {
+      .on('change', 'input.api-param-name, select.api-param-op', renderValueField)
+      .on('change', 'input.api-param-name, .api-option-name', function() {
         if ($(this).val() === '-') {
           $(this).select2('destroy');
           $(this).val('').focus();