Improve API explorer handling of operators
[civicrm-core.git] / templates / CRM / Admin / Page / APIExplorer.js
CommitLineData
e4176358
CW
1(function($, _, undefined) {
2 var
3 entity,
4 action,
2b6e1174 5 actions = ['get'],
e4176358
CW
6 fields = [],
7 options = {},
8 params = {},
2b6e1174 9 smartyStub,
8ffa1387 10 fieldTpl = _.template($('#api-param-tpl').html()),
c275764e 11 optionsTpl = _.template($('#api-options-tpl').html()),
b07af612 12 returnTpl = _.template($('#api-return-tpl').html()),
77099ee0
CW
13 chainTpl = _.template($('#api-chain-tpl').html()),
14
15 // Operators with special properties
16 BOOL = ['IS NULL', 'IS NOT NULL'],
17 TEXT = ['LIKE', 'NOT LIKE'],
18 MULTI = ['IN', 'NOT IN', 'BETWEEN', 'NOT BETWEEN'];
0c3a6e64 19
2b6e1174
CW
20 /**
21 * Call prettyPrint function if it successfully loaded from the cdn
22 */
cd77ffa6
CW
23 function prettyPrint() {
24 if (window.prettyPrint) {
25 window.prettyPrint();
26 }
27 }
28
b07af612
CW
29 /**
30 * Add a "fields" row
31 * @param name
32 */
e4176358
CW
33 function addField(name) {
34 $('#api-params').append($(fieldTpl({name: name || ''})));
35 var $row = $('tr:last-child', '#api-params');
77099ee0 36 $('input.api-param-name', $row).crmSelect2({
b07af612
CW
37 data: fields.concat({id: '-', text: ts('Other') + '...'})
38 }).change();
8ffa1387
CW
39 }
40
c275764e
CW
41 /**
42 * Add a new "options" row
43 * @param name
44 */
45 function addOptionField(name) {
46 if ($('.api-options-row', '#api-params').length) {
47 $('.api-options-row:last', '#api-params').after($(optionsTpl({})));
48 } else {
49 $('#api-params').append($(optionsTpl({})));
50 }
51 var $row = $('.api-options-row:last', '#api-params');
52 $('.api-option-name', $row).crmSelect2({data: [
53 {id: 'limit', text: 'limit'},
54 {id: 'offset', text: 'offset'},
55 {id: 'sort', text: 'sort'},
56 {id: 'metadata', text: 'metadata'},
57 {id: '-', text: ts('Other') + '...'}
58 ]});
59 }
60
b07af612
CW
61 /**
62 * Add an "api chain" row
63 */
8ffa1387
CW
64 function addChainField() {
65 $('#api-params').append($(chainTpl({})));
66 var $row = $('tr:last-child', '#api-params');
67 $('.api-chain-entity', $row).crmSelect2({
68 formatSelection: function(item) {
69 return '<span class="icon ui-icon-link"></span> API ' + item.text;
2b6e1174
CW
70 },
71 placeholder: '<span class="icon ui-icon-link"></span> ' + ts('Entity'),
72 escapeMarkup: function(m) {return m}
8ffa1387 73 });
0c3a6e64
CW
74 }
75
b07af612
CW
76 /**
77 * Fetch fields for entity+action
78 */
e4176358
CW
79 function getFields() {
80 var required = [];
81 fields = [];
82 options = {};
83 // Special case for getfields
84 if (action === 'getfields') {
85 fields.push({
86 id: 'api_action',
87 text: 'Action'
88 });
89 options.api_action = [];
90 $('option', '#api-action').each(function() {
91 if (this.value) {
92 options.api_action.push({key: this.value, value: $(this).text()});
93 }
94 });
95 showFields(['api_action']);
0c3a6e64
CW
96 return;
97 }
e4176358
CW
98 CRM.api3(entity, 'getFields', {'api_action': action, sequential: 1, options: {get_options: 'all'}}).done(function(data) {
99 _.each(data.values, function(field) {
100 if (field.name) {
101 fields.push({
102 id: field.name,
103 text: field.title || field.name,
104 required: field['api.required'] || false
105 });
106 if (field['api.required']) {
107 required.push(field.name);
108 }
109 if (field.options) {
110 options[field.name] = field.options;
111 }
112 }
113 });
114 showFields(required);
b07af612
CW
115 if (action === 'get' || action === 'getsingle') {
116 showReturn();
117 }
0c3a6e64
CW
118 });
119 }
120
b07af612
CW
121 /**
122 * For "get" actions show the "return" options
123 */
124 function showReturn() {
125 $('#api-params').prepend($(returnTpl({})));
b07af612
CW
126 $('#api-return-value').crmSelect2({data: fields, multiple: true});
127 }
128
129 /**
130 * Fetch actions for entity
131 */
2b6e1174
CW
132 function getActions() {
133 if (entity) {
754927e4 134 $('#api-action').addClass('loading');
2b6e1174 135 CRM.api3(entity, 'getactions').done(function(data) {
2f929504 136 actions = data.values || ['get'];
2b6e1174
CW
137 populateActions();
138 });
139 } else {
140 actions = ['get'];
141 populateActions();
142 }
143 }
144
b07af612
CW
145 /**
146 * Called after getActions to populate action list
147 * @param el
148 */
2b6e1174 149 function populateActions(el) {
2f929504 150 var val = $('#api-action').val();
754927e4 151 $('#api-action').removeClass('loading').select2({
2b6e1174
CW
152 data: _.transform(actions, function(ret, item) {ret.push({text: item, id: item})})
153 });
2f929504
CW
154 // If previously selected action is not available, set it to 'get' if possible
155 if (_.indexOf(actions, val) < 0) {
156 $('#api-action').select2('val', _.indexOf(actions, 'get') < 0 ? actions[0] : 'get', true);
157 }
2b6e1174
CW
158 }
159
b07af612
CW
160 /**
161 * Called after getfields to show buttons and required fields
162 * @param required
163 */
e4176358 164 function showFields(required) {
e4176358 165 $('#api-params').empty();
8ffa1387 166 $('#api-param-buttons').show();
e4176358
CW
167 if (required.length) {
168 _.each(required, addField);
169 } else {
170 addField();
0c3a6e64
CW
171 }
172 }
173
c275764e 174 /**
77099ee0
CW
175 * Render value input as a textfield, option list, or hidden,
176 * Depending on selected param name and operator
c275764e 177 */
77099ee0
CW
178 function renderValueField() {
179 var $row = $(this).closest('tr'),
180 name = $('input.api-param-name', $row).val(),
181 operator = $('.api-param-op', $row).val(),
182 operatorType = $.inArray(operator, MULTI) > -1 ? 'multi' : ($.inArray(operator, BOOL) > -1 ? 'bool' : 'single'),
183 $valField = $('input.api-param-value', $row),
184 currentVal = $valField.val();
185 // Boolean fields only have 1 possible value
186 if (operatorType == 'bool') {
187 if ($valField.data('select2')) {
188 $valField.select2('destroy');
189 }
190 $valField.css('visibility', 'hidden').val('1');
191 return;
192 }
193 $valField.css('visibility', '');
194 // Option list input
195 if (options[name] && $.inArray(operator, TEXT) < 0) {
196 // Reset value before switching to a select from something else
197 if ($(this).is('.api-param-name') || !$valField.data('select2')) {
198 $valField.val('');
199 }
200 // When switching from multi-select to single select
201 else if (operatorType == 'single' && currentVal.indexOf(',') > -1) {
202 $valField.val(currentVal.split(',')[0]);
203 }
204 $valField.select2({
205 multiple: (operatorType === 'multi'),
e4176358
CW
206 data: _.transform(options[name], function(result, option) {
207 result.push({id: option.key, text: option.value});
208 })
209 });
77099ee0 210 return;
e4176358 211 }
77099ee0
CW
212 // Plain text input
213 if ($valField.data('select2')) {
e4176358
CW
214 $valField.select2('destroy');
215 }
e4176358 216 }
0c3a6e64 217
e4176358
CW
218 /**
219 * Attempt to parse a string into a value of the intended type
babf9678
CW
220 * @param val string
221 * @param makeArray bool
e4176358
CW
222 */
223 function evaluate(val, makeArray) {
224 try {
225 if (!val.length) {
77099ee0 226 return makeArray ? [] : '';
e4176358
CW
227 }
228 var first = val.charAt(0),
229 last = val.slice(-1);
230 // Simple types
51f197bf 231 if (val === 'true' || val === 'false' || val === 'null') {
e4176358
CW
232 return eval(val);
233 }
234 // Quoted strings
235 if ((first === '"' || first === "'") && last === first) {
236 return val.slice(1, -1);
0c3a6e64 237 }
e4176358
CW
238 // Parse json
239 if ((first === '[' && last === ']') || (first === '{' && last === '}')) {
240 return eval('(' + val + ')');
241 }
242 // Transform csv to array
77099ee0
CW
243 if (makeArray) {
244 var result = [];
245 $.each(val.split(','), function(k, v) {
246 result.push(evaluate($.trim(v)) || v);
247 });
248 return result;
249 }
250 // Integers - quote any number that starts with 0 to avoid oddities
251 if (!isNaN(val) && val.search(/[^\d]/) < 0 && (val.length === 1 || first !== '0')) {
252 return parseInt(val, 10);
e4176358
CW
253 }
254 // Ok ok it's really a string
255 return val;
256 } catch(e) {
257 // If eval crashed return undefined
258 return undefined;
0c3a6e64 259 }
e4176358 260 }
0c3a6e64 261
e4176358
CW
262 /**
263 * Format value to look like php code
264 * @param val
265 */
266 function phpFormat(val) {
267 var ret = '';
268 if ($.isPlainObject(val)) {
269 $.each(val, function(k, v) {
c275764e 270 ret += (ret ? ', ' : '') + "'" + k + "' => " + phpFormat(v);
e4176358
CW
271 });
272 return 'array(' + ret + ')';
0c3a6e64 273 }
e4176358
CW
274 if ($.isArray(val)) {
275 $.each(val, function(k, v) {
276 ret += (ret ? ', ' : '') + phpFormat(v);
277 });
278 return 'array(' + ret + ')';
279 }
54c512e0 280 return JSON.stringify(val).replace(/\$/g, '\\$');
e4176358
CW
281 }
282
283 /**
284 * Smarty doesn't support array literals so we provide a stub
285 * @param js string
286 */
287 function smartyFormat(js, key) {
288 if (js.indexOf('[') > -1 || js.indexOf('{') > -1) {
2b6e1174 289 smartyStub = true;
e4176358
CW
290 return '$' + key.replace(/[. -]/g, '_');
291 }
292 return js;
293 }
294
c275764e
CW
295 /**
296 * Create the params array from user input
297 * @param e
298 */
e4176358
CW
299 function buildParams(e) {
300 params = {};
301 $('.api-param-checkbox:checked').each(function() {
302 params[this.name] = 1;
303 });
c275764e 304 $('input.api-param-value, input.api-option-value').each(function() {
e4176358 305 var $row = $(this).closest('tr'),
77099ee0 306 op = $('select.api-param-op', $row).val() || '=',
e4176358 307 name = $('input.api-param-name', $row).val(),
77099ee0 308 val = evaluate($(this).val(), $.inArray(op, MULTI) > -1);
b07af612
CW
309
310 // Ignore blank values for the return field
311 if ($(this).is('#api-return-value') && !val) {
312 return;
313 }
8ffa1387
CW
314 // Special syntax for api chaining
315 if (!name && $('select.api-chain-entity', $row).val()) {
316 name = 'api.' + $('select.api-chain-entity', $row).val() + '.' + $('select.api-chain-action', $row).val();
317 }
c275764e
CW
318 // Special handling for options
319 if ($(this).is('.api-option-value')) {
320 op = $('input.api-option-name', $row).val();
321 if (op) {
322 name = 'options';
323 }
324 }
e4176358 325 if (name && val !== undefined) {
c275764e 326 params[name] = op === '=' ? val : (params[name] || {});
e4176358
CW
327 if (op !== '=') {
328 params[name][op] = val;
329 }
babf9678
CW
330 if ($(this).hasClass('crm-error')) {
331 clearError(this);
332 }
e4176358
CW
333 }
334 else if (name && (!e || e.type !== 'keyup')) {
335 setError(this);
336 }
337 });
338 if (entity && action) {
339 formatQuery();
0c3a6e64 340 }
e4176358 341 }
0c3a6e64 342
e4176358
CW
343 function setError(el) {
344 if (!$(el).hasClass('crm-error')) {
babf9678 345 var msg = ts('Syntax error: input should be valid JSON or a quoted string.');
e4176358
CW
346 $(el)
347 .addClass('crm-error')
2b6e1174 348 .css('width', '82%')
babf9678
CW
349 .attr('title', msg)
350 .before('<div class="icon red-icon ui-icon-alert" title="'+msg+'"/>')
351 .tooltip();
e4176358
CW
352 }
353 }
0c3a6e64 354
e4176358
CW
355 function clearError(el) {
356 $(el)
357 .removeClass('crm-error')
358 .attr('title', '')
2b6e1174 359 .css('width', '85%')
babf9678 360 .tooltip('destroy')
e4176358
CW
361 .siblings('.ui-icon-alert').remove();
362 }
0c3a6e64 363
e4176358
CW
364 function formatQuery() {
365 var i = 0, q = {
366 smarty: "{crmAPI var='result' entity='" + entity + "' action='" + action + "'",
367 php: "$result = civicrm_api3('" + entity + "', '" + action + "'",
368 json: "CRM.api3('" + entity + "', '" + action + "'",
2dad2b26
TO
369 drush: "drush cvapi " + entity + '.' + action + ' ',
370 wpcli: "wp cv api " + entity + '.' + action + ' ',
e4176358
CW
371 rest: CRM.config.resourceBase + "extern/rest.php?entity=" + entity + "&action=" + action + "&json=" + JSON.stringify(params) + "&api_key=yoursitekey&key=yourkey"
372 };
2b6e1174 373 smartyStub = false;
e4176358
CW
374 $.each(params, function(key, value) {
375 var js = JSON.stringify(value);
376 if (!i++) {
377 q.php += ", array(\n";
378 q.json += ", {\n";
379 } else {
380 q.json += ",\n";
381 }
382 q.php += " '" + key + "' => " + phpFormat(value) + ",\n";
383 q.json += " \"" + key + '": ' + js;
e4176358 384 q.smarty += ' ' + key + '=' + smartyFormat(js, key);
77099ee0 385 // FIXME: This is not totally correct cli syntax
555d256f 386 q.drush += key + '=' + js + ' ';
2dad2b26 387 q.wpcli += key + '=' + js + ' ';
e4176358
CW
388 });
389 if (i) {
390 q.php += ")";
391 q.json += "\n}";
392 }
393 q.php += ");";
394 q.json += ").done(function(result) {\n // do something\n});";
395 q.smarty += "}\n{foreach from=$result.values item=" + entity.toLowerCase() + "}\n {$" + entity.toLowerCase() + ".some_field}\n{/foreach}";
396 if (action.indexOf('get') < 0) {
397 q.smarty = '{* Smarty API only works with get actions *}';
2b6e1174 398 } else if (smartyStub) {
9405a07b 399 q.smarty = "{* Smarty does not have a syntax for array literals; assign complex variables from php *}\n" + q.smarty;
e4176358
CW
400 }
401 $.each(q, function(type, val) {
cd77ffa6 402 $('#api-' + type).removeClass('prettyprinted').text(val);
e4176358 403 });
cd77ffa6 404 prettyPrint();
e4176358 405 }
0c3a6e64 406
e4176358
CW
407 function submit(e) {
408 e.preventDefault();
409 if (!entity || !action) {
2b6e1174 410 alert(ts('Select an entity.'));
e4176358
CW
411 return;
412 }
2f929504 413 if (action.indexOf('get') < 0 && action != 'check') {
e4176358
CW
414 var msg = action === 'delete' ? ts('This will delete data from CiviCRM. Are you sure?') : ts('This will write to the database. Continue?');
415 CRM.confirm({title: ts('Confirm %1', {1: action}), message: msg}).on('crmConfirm:yes', execute);
0c3a6e64 416 } else {
e4176358 417 execute();
0c3a6e64 418 }
0c3a6e64
CW
419 }
420
77099ee0
CW
421 /**
422 * Execute api call and display the results
423 * Note: We have to manually execute the ajax in order to add the secret extra "prettyprint" param
424 */
e4176358
CW
425 function execute() {
426 $('#api-result').html('<div class="crm-loading-element"></div>');
427 $.ajax({
428 url: CRM.url('civicrm/ajax/rest'),
429 data: {
430 entity: entity,
431 action: action,
432 prettyprint: 1,
433 json: JSON.stringify(params)
434 },
435 type: action.indexOf('get') < 0 ? 'POST' : 'GET',
436 dataType: 'text'
437 }).done(function(text) {
2b6e1174 438 $('#api-result').addClass('prettyprint').removeClass('prettyprinted').text(text);
cd77ffa6 439 prettyPrint();
e4176358 440 });
0c3a6e64 441 }
e4176358
CW
442
443 $(document).ready(function() {
444 $('form#api-explorer')
445 .on('change', '#api-entity, #api-action', function() {
446 entity = $('#api-entity').val();
2b6e1174 447 if ($(this).is('#api-entity')) {
2b6e1174
CW
448 getActions();
449 }
e4176358
CW
450 action = $('#api-action').val();
451 if (entity && action) {
452 $('#api-params').html('<tr><td colspan="4" class="crm-loading-element"></td></tr>');
453 $('#api-params-table thead').show();
454 getFields();
455 buildParams();
456 } else {
457 $('#api-params, #api-generated pre').empty();
8ffa1387 458 $('#api-param-buttons, #api-params-table thead').hide();
e4176358
CW
459 }
460 })
2b6e1174 461 .on('change keyup', 'input.api-input, #api-params select', buildParams)
e4176358
CW
462 .on('submit', submit);
463 $('#api-params')
77099ee0
CW
464 .on('change', 'input.api-param-name, select.api-param-op', renderValueField)
465 .on('change', 'input.api-param-name, .api-option-name', function() {
c275764e
CW
466 if ($(this).val() === '-') {
467 $(this).select2('destroy');
468 $(this).val('').focus();
469 }
470 })
e4176358
CW
471 .on('click', '.api-param-remove', function(e) {
472 e.preventDefault();
473 $(this).closest('tr').remove();
474 buildParams();
475 });
476 $('#api-params-add').on('click', function(e) {
8ffa1387 477 e.preventDefault();
e4176358 478 addField();
8ffa1387 479 });
c275764e
CW
480 $('#api-option-add').on('click', function(e) {
481 e.preventDefault();
482 addOptionField();
483 });
8ffa1387 484 $('#api-chain-add').on('click', function(e) {
e4176358 485 e.preventDefault();
8ffa1387 486 addChainField();
e4176358
CW
487 });
488 $('#api-entity').change();
0c3a6e64 489 });
e4176358 490}(CRM.$, CRM._));