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