Merge pull request #3113 from deepak-srivastava/CRM-14431
[civicrm-core.git] / templates / CRM / Admin / Page / APIExplorer.js
index cf04eb6f4bfe535bbda385b730a9fff7ce2a697b..36fe3ab2a9848b16f1065e27e5e0c448a521390e 100644 (file)
-CRM.$(function($) {
-  var restURL = CRM.url("civicrm/ajax/rest");
+(function($, _, undefined) {
+  var
+    entity,
+    action,
+    actions = ['get'],
+    fields = [],
+    options = {},
+    params = {},
+    smartyStub,
+    fieldTpl = _.template($('#api-param-tpl').html()),
+    optionsTpl = _.template($('#api-options-tpl').html()),
+    returnTpl = _.template($('#api-return-tpl').html()),
+    chainTpl = _.template($('#api-chain-tpl').html());
 
-  function toggleField (name, label, type) {
-    var h = '<div>\
-      <label for="' + name + '">'+label+'</label>: <input name="' + name + '" data-id="'+name+ '" />\
-      <a href="#" class="remove-extra" title=' + ts('Remove Field') + '>X</a>\
-    </div>';
-    if ( $('#extra [name=' + name + ']').length > 0) {
-      $('#extra [name=' + name + ']').parent().remove();
+  /**
+   * Call prettyPrint function if it successfully loaded from the cdn
+   */
+  function prettyPrint() {
+    if (window.prettyPrint) {
+      window.prettyPrint();
     }
-    else {
-      $('#extra').append (h);
+  }
+
+  /**
+   * Add a "fields" row
+   * @param name
+   */
+  function addField(name) {
+    $('#api-params').append($(fieldTpl({name: name || ''})));
+    var $row = $('tr:last-child', '#api-params');
+    $('.api-param-name', $row).crmSelect2({
+      data: fields.concat({id: '-', text: ts('Other') + '...'})
+    }).change();
+  }
+
+  /**
+   * Add a new "options" row
+   * @param name
+   */
+  function addOptionField(name) {
+    if ($('.api-options-row', '#api-params').length) {
+      $('.api-options-row:last', '#api-params').after($(optionsTpl({})));
+    } else {
+      $('#api-params').append($(optionsTpl({})));
     }
+    var $row = $('.api-options-row:last', '#api-params');
+    $('.api-option-name', $row).crmSelect2({data: [
+      {id: 'limit', text: 'limit'},
+      {id: 'offset', text: 'offset'},
+      {id: 'sort', text: 'sort'},
+      {id: 'metadata', text: 'metadata'},
+      {id: '-', text: ts('Other') + '...'}
+    ]});
   }
 
-  function buildForm (params) {
-    var h = '<label>ID</label><input data-id="id" size="3" maxlength="20" />';
-    if (params.action == 'delete') {
-      $('#extra').html(h);
+  /**
+   * Add an "api chain" row
+   */
+  function addChainField() {
+    $('#api-params').append($(chainTpl({})));
+    var $row = $('tr:last-child', '#api-params');
+    $('.api-chain-entity', $row).crmSelect2({
+      formatSelection: function(item) {
+        return '<span class="icon ui-icon-link"></span> API ' + item.text;
+      },
+      placeholder: '<span class="icon ui-icon-link"></span> ' + ts('Entity'),
+      escapeMarkup: function(m) {return m}
+    });
+  }
+
+  /**
+   * Fetch fields for entity+action
+   */
+  function getFields() {
+    var required = [];
+    fields = [];
+    options = {};
+    // Special case for getfields
+    if (action === 'getfields') {
+      fields.push({
+        id: 'api_action',
+        text: 'Action'
+      });
+      options.api_action = [];
+      $('option', '#api-action').each(function() {
+        if (this.value) {
+          options.api_action.push({key: this.value, value: $(this).text()});
+        }
+      });
+      showFields(['api_action']);
       return;
     }
-
-    CRM.api(params.entity, 'getFields', {}, {
-      success:function (data) {
-        h = '<i>' + ts('Available fields (click to add/remove):') + '</i>';
-        $.each(data.values, function(key, value) {
-          var required = value.required ? " required" : "";
-          h += "<a data-id='" + key + "' class='type_" + value.type + required + "'>" + value.title + "</a>";
-        });
-        $('#selector').html(h);
+    CRM.api3(entity, 'getFields', {'api_action': action, sequential: 1, options: {get_options: 'all'}}).done(function(data) {
+      _.each(data.values, function(field) {
+        if (field.name) {
+          fields.push({
+            id: field.name,
+            text: field.title || field.name,
+            required: field['api.required'] || false
+          });
+          if (field['api.required']) {
+            required.push(field.name);
+          }
+          if (field.options) {
+            options[field.name] = field.options;
+          }
+        }
+      });
+      showFields(required);
+      if (action === 'get' || action === 'getsingle') {
+        showReturn();
       }
     });
   }
 
-  function generateQuery () {
-    var params = {};
-    $('#api-explorer input:checkbox:checked, #api-explorer select, #extra input').each(function() {
-      var val = $(this).val();
-      if (val) {
-        params[$(this).data('id')] = val;
-      }
+  /**
+   * For "get" actions show the "return" options
+   */
+  function showReturn() {
+    $('#api-params').prepend($(returnTpl({})));
+    console.log($(returnTpl({})));
+    $('#api-return-value').crmSelect2({data: fields, multiple: true});
+  }
+
+  /**
+   * Fetch actions for entity
+   */
+  function getActions() {
+    if (entity) {
+      CRM.api3(entity, 'getactions').done(function(data) {
+        // Ensure 'get' is always an action
+        actions = _.union(['get'], data.values);
+        populateActions();
+      });
+    } else {
+      actions = ['get'];
+      populateActions();
+    }
+  }
+
+  /**
+   * Called after getActions to populate action list
+   * @param el
+   */
+  function populateActions(el) {
+    $('#api-action').select2({
+      data: _.transform(actions, function(ret, item) {ret.push({text: item, id: item})})
     });
-    query = CRM.url("civicrm/ajax/rest", params);
-    $('#query').val(query);
-    if (params.action == 'delete' && $('#selector a').length == 0) {
-      buildForm (params);
-      return;
+  }
+
+  /**
+   * Called after getfields to show buttons and required fields
+   * @param required
+   */
+  function showFields(required) {
+    $('#api-params').empty();
+    $('#api-param-buttons').show();
+    if (required.length) {
+      _.each(required, addField);
+    } else {
+      addField();
     }
-    if (params.action == 'create' && $('#selector a').length == 0) {
-      buildForm (params);
-      return;
+  }
+
+  /**
+   * Add/remove option list for selected field's pseudoconstant
+   */
+  function toggleOptions() {
+    var name = $(this).val(),
+      $valField = $(this).closest('tr').find('.api-param-value');
+    if (options[name]) {
+      $valField.val('').select2({
+        multiple: true,
+        data: _.transform(options[name], function(result, option) {
+          result.push({id: option.key, text: option.value});
+        })
+      });
+    }
+    else if ($valField.data('select2')) {
+      $valField.select2('destroy');
     }
   }
 
-  function runQuery() {
-    var vars = [],
-      hash,
-      smarty = '',
-      php = "$params = array(<br />&nbsp;&nbsp;'version' => 3,",
-      json = "{",
-      link = "",
-      key,
-      value,
-      entity,
-      action,
-      query = $('#query').val(),
-      hashes = query.slice(query.indexOf('?') + 1).split('&');
-    for(var i = 0; i < hashes.length; i++) {
-      hash = hashes[i].split('=');
-      key = hash[0];
-      value = hash[1];
-
-      switch (key) {
-        case 'version':
-        case 'debug':
-        case 'json':
-          break;
-        case 'action':
-          action = value.toLowerCase();
-          $('#action').val(action);
-          break;
-        case 'entity':
-          entity = value.charAt(0).toUpperCase() + value.substr(1);
-          $('#entity').val(entity);
-          break;
-        default:
-          if (typeof value == 'undefined') {
-            break;
-          }
-          value = isNaN(value) ? "'" + value + "'" : value;
-          smarty += ' ' + key + '=' + value;
-          php += "<br />&nbsp;&nbsp'" + key +"' => " + value + ",";
-          json += "'" + key + "': " + value + ", ";
+  /**
+   * Attempt to parse a string into a value of the intended type
+   * @param val
+   */
+  function evaluate(val, makeArray) {
+    try {
+      if (!val.length) {
+        return val;
       }
+      var first = val.charAt(0),
+        last = val.slice(-1);
+      // Simple types
+      if (val === 'true' || val === 'false' || val === 'null' || !isNaN(val)) {
+        return eval(val);
+      }
+      // Quoted strings
+      if ((first === '"' || first === "'") && last === first) {
+        return val.slice(1, -1);
+      }
+      // Parse json
+      if ((first === '[' && last === ']') || (first === '{' && last === '}')) {
+        return eval('(' + val + ')');
+      }
+      // Transform csv to array
+      if (makeArray && val.indexOf(',') > 0) {
+        return val.split(',');
+      }
+      // Ok ok it's really a string
+      return val;
+    } catch(e) {
+      // If eval crashed return undefined
+      return undefined;
     }
+  }
 
-    if (!entity) {
-      $('#query').val(ts('Choose an entity.'));
-      $('#entity').val('');
-      window.location.hash = 'explorer';
-      return;
+  /**
+   * Format value to look like php code
+   * @param val
+   */
+  function phpFormat(val) {
+    var ret = '';
+    if ($.isPlainObject(val)) {
+      $.each(val, function(k, v) {
+        ret += (ret ? ', ' : '') + "'" + k + "' => " + phpFormat(v);
+      });
+      return 'array(' + ret + ')';
     }
-    if (!action) {
-      $('#query').val(ts('Choose an action.'));
-      $('#action').val('');
-      window.location.hash = 'explorer';
-      return;
+    if ($.isArray(val)) {
+      $.each(val, function(k, v) {
+        ret += (ret ? ', ' : '') + phpFormat(v);
+      });
+      return 'array(' + ret + ')';
     }
+    return JSON.stringify(val);
+  }
 
-    window.location.hash = query;
-    $('#result').block();
-    $.post(query,function(data) {
-      $('#result').unblock().text(data);
-    },'text');
-    link="<a href='"+query+"' title='open in a new tab' target='_blank'>ajax query</a>&nbsp;";
-    var RESTquery = CRM.config.resourceBase + "extern/rest.php?"+ query.substring(restURL.length,query.length) + "&api_key={yoursitekey}&key={yourkey}";
-    $("#link").html(link+"|<a href='"+RESTquery+"' title='open in a new tab' target='_blank'>REST query</a>.");
+  /**
+   * Smarty doesn't support array literals so we provide a stub
+   * @param js string
+   */
+  function smartyFormat(js, key) {
+    if (js.indexOf('[') > -1 || js.indexOf('{') > -1) {
+      smartyStub = true;
+      return '$' + key.replace(/[. -]/g, '_');
+    }
+    return js;
+  }
 
+  /**
+   * Create the params array from user input
+   * @param e
+   */
+  function buildParams(e) {
+    params = {};
+    $('.api-param-checkbox:checked').each(function() {
+      params[this.name] = 1;
+    });
+    $('input.api-param-value, input.api-option-value').each(function() {
+      var $row = $(this).closest('tr'),
+        val = evaluate($(this).val(), $(this).is('.select2-offscreen')),
+        name = $('input.api-param-name', $row).val(),
+        op = $('select.api-param-op', $row).val() || '=';
 
-    json = (json.length > 1 ? json.slice (0,-2) : '{') + '}';
-    php += "<br />);<br />";
-    $('#php').html(php + "$result = civicrm_api('" + entity + "', '" + action + "', $params);");
-    $('#jQuery').html ("CRM.api('"+entity+"', '"+action+"', "+json+",<br />&nbsp;&nbsp;{success: function(data) {<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;cj.each(data, function(key, value) {// do something  });<br />&nbsp;&nbsp;&nbsp;&nbsp;}<br />&nbsp;&nbsp;}<br />);");
+      // Ignore blank values for the return field
+      if ($(this).is('#api-return-value') && !val) {
+        return;
+      }
+      // Special syntax for api chaining
+      if (!name && $('select.api-chain-entity', $row).val()) {
+        name = 'api.' + $('select.api-chain-entity', $row).val() + '.' + $('select.api-chain-action', $row).val();
+      }
+      // Special handling for options
+      if ($(this).is('.api-option-value')) {
+        op = $('input.api-option-name', $row).val();
+        if (op) {
+          name = 'options';
+        }
+      }
+      if (name && val !== undefined) {
+        params[name] = op === '=' ? val : (params[name] || {});
+        if (op !== '=') {
+          params[name][op] = val;
+        }
+        clearError(this);
+      }
+      else if (name && (!e || e.type !== 'keyup')) {
+        setError(this);
+      }
+    });
+    if (entity && action) {
+      formatQuery();
+    }
+  }
 
-    if (action.substring(0, 3) == "get") {//using smarty only make sense for get actions
-      $('#smarty').html("{crmAPI var='result' entity='" + entity + "' action='" + action + "' " + smarty + '}<br />{foreach from=$result.values item=' + entity + '}<br/>&nbsp;&nbsp;&lt;li&gt;{$' + entity +'.some_field}&lt;/li&gt;<br />{/foreach}');
-    } else {
-      $('#smarty').html("smarty uses only 'get' actions");
+  function setError(el) {
+    if (!$(el).hasClass('crm-error')) {
+      $(el)
+        .addClass('crm-error')
+        .css('width', '82%')
+        .attr('title', ts('Syntax error'))
+        .before('<div class="icon red-icon ui-icon-alert"/>');
     }
-    $('#generated').show();
   }
 
-  var query = window.location.hash;
-  if (query.substring(1, restURL.length + 1) === restURL) {
-    $('#query').val (query.substring(1)).focus();
-    runQuery();
-  } else {
-    window.location.hash="explorer"; //to be sure to display the result under the generated code in the viewport
+  function clearError(el) {
+    $(el)
+      .removeClass('crm-error')
+      .attr('title', '')
+      .css('width', '85%')
+      .siblings('.ui-icon-alert').remove();
   }
-  $('#entity, #action').change (function() {
-    $("#selector, #extra").empty();
-    generateQuery();
-    runQuery();
-  });
-  $('#api-explorer input:checkbox').change(function() {
-    generateQuery(); runQuery();
-  });
-  $('#api-explorer').submit(function(e) {
+
+  function formatQuery() {
+    var i = 0, q = {
+      smarty: "{crmAPI var='result' entity='" + entity + "' action='" + action + "'",
+      php: "$result = civicrm_api3('" + entity + "', '" + action + "'",
+      json: "CRM.api3('" + entity + "', '" + action + "'",
+      rest: CRM.config.resourceBase + "extern/rest.php?entity=" + entity + "&action=" + action + "&json=" + JSON.stringify(params) + "&api_key=yoursitekey&key=yourkey"
+    };
+    smartyStub = false;
+    $.each(params, function(key, value) {
+      var js = JSON.stringify(value);
+      if (!i++) {
+        q.php += ", array(\n";
+        q.json += ", {\n";
+      } else {
+        q.json += ",\n";
+      }
+      q.php += "  '" + key + "' => " + phpFormat(value) + ",\n";
+      q.json += "  \"" + key + '": ' + js;
+      q.smarty += ' ' + key + '=' + smartyFormat(js, key);
+    });
+    if (i) {
+      q.php += ")";
+      q.json += "\n}";
+    }
+    q.php += ");";
+    q.json += ").done(function(result) {\n  // do something\n});";
+    q.smarty += "}\n{foreach from=$result.values item=" + entity.toLowerCase() + "}\n  {$" + entity.toLowerCase() + ".some_field}\n{/foreach}";
+    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;
+    }
+    $.each(q, function(type, val) {
+      $('#api-' + type).removeClass('prettyprinted').text(val);
+    });
+    prettyPrint();
+  }
+
+  function submit(e) {
     e.preventDefault();
-    runQuery();
-  });
-  $('#extra').on('keyup', 'input', generateQuery);
-  $('#extra').on('click', 'a.remove-extra', function() {
-    $(this).parent().remove();
-    generateQuery();
-  });
-  $('#selector').on('click', 'a', function() {
-    toggleField($(this).data('id'), this.innerHTML, this.class);
+    if (!entity || !action) {
+      alert(ts('Select an entity.'));
+      return;
+    }
+    if (action.indexOf('get') < 0) {
+      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();
+    }
+  }
+
+  function execute() {
+    $('#api-result').html('<div class="crm-loading-element"></div>');
+    $.ajax({
+      url: CRM.url('civicrm/ajax/rest'),
+      data: {
+        entity: entity,
+        action: action,
+        prettyprint: 1,
+        json: JSON.stringify(params)
+      },
+      type: action.indexOf('get') < 0 ? 'POST' : 'GET',
+      dataType: 'text'
+    }).done(function(text) {
+      $('#api-result').addClass('prettyprint').removeClass('prettyprinted').text(text);
+      prettyPrint();
+    });
+  }
+
+  $(document).ready(function() {
+    $('form#api-explorer')
+      .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();
+        if (entity && action) {
+          $('#api-params').html('<tr><td colspan="4" class="crm-loading-element"></td></tr>');
+          $('#api-params-table thead').show();
+          getFields();
+          buildParams();
+        } else {
+          $('#api-params, #api-generated pre').empty();
+          $('#api-param-buttons, #api-params-table thead').hide();
+        }
+      })
+      .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() {
+        if ($(this).val() === '-') {
+          $(this).select2('destroy');
+          $(this).val('').focus();
+        }
+      })
+      .on('click', '.api-param-remove', function(e) {
+        e.preventDefault();
+        $(this).closest('tr').remove();
+        buildParams();
+      });
+    $('#api-params-add').on('click', function(e) {
+      e.preventDefault();
+      addField();
+    });
+    $('#api-option-add').on('click', function(e) {
+      e.preventDefault();
+      addOptionField();
+    });
+    $('#api-chain-add').on('click', function(e) {
+      e.preventDefault();
+      addChainField();
+    });
+    $('#api-entity').change();
   });
-});
+}(CRM.$, CRM._));