2063c2a62f9ec655090d8d87ffc2ebd4feb1d220
[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 /**
16 * Call prettyPrint function if it successfully loaded from the cdn
17 */
18 function prettyPrint() {
19 if (window.prettyPrint) {
20 window.prettyPrint();
21 }
22 }
23
24 /**
25 * Add a "fields" row
26 * @param name
27 */
28 function addField(name) {
29 $('#api-params').append($(fieldTpl({name: name || ''})));
30 var $row = $('tr:last-child', '#api-params');
31 $('.api-param-name', $row).crmSelect2({
32 data: fields.concat({id: '-', text: ts('Other') + '...'})
33 }).change();
34 }
35
36 /**
37 * Add a new "options" row
38 * @param name
39 */
40 function addOptionField(name) {
41 if ($('.api-options-row', '#api-params').length) {
42 $('.api-options-row:last', '#api-params').after($(optionsTpl({})));
43 } else {
44 $('#api-params').append($(optionsTpl({})));
45 }
46 var $row = $('.api-options-row:last', '#api-params');
47 $('.api-option-name', $row).crmSelect2({data: [
48 {id: 'limit', text: 'limit'},
49 {id: 'offset', text: 'offset'},
50 {id: 'sort', text: 'sort'},
51 {id: 'metadata', text: 'metadata'},
52 {id: '-', text: ts('Other') + '...'}
53 ]});
54 }
55
56 /**
57 * Add an "api chain" row
58 */
59 function addChainField() {
60 $('#api-params').append($(chainTpl({})));
61 var $row = $('tr:last-child', '#api-params');
62 $('.api-chain-entity', $row).crmSelect2({
63 formatSelection: function(item) {
64 return '<span class="icon ui-icon-link"></span> API ' + item.text;
65 },
66 placeholder: '<span class="icon ui-icon-link"></span> ' + ts('Entity'),
67 escapeMarkup: function(m) {return m}
68 });
69 }
70
71 /**
72 * Fetch fields for entity+action
73 */
74 function getFields() {
75 var required = [];
76 fields = [];
77 options = {};
78 // Special case for getfields
79 if (action === 'getfields') {
80 fields.push({
81 id: 'api_action',
82 text: 'Action'
83 });
84 options.api_action = [];
85 $('option', '#api-action').each(function() {
86 if (this.value) {
87 options.api_action.push({key: this.value, value: $(this).text()});
88 }
89 });
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 * Add/remove option list for selected field's pseudoconstant
171 */
172 function toggleOptions() {
173 var name = $(this).val(),
174 $valField = $(this).closest('tr').find('.api-param-value');
175 if (options[name]) {
176 $valField.val('').select2({
177 multiple: true,
178 data: _.transform(options[name], function(result, option) {
179 result.push({id: option.key, text: option.value});
180 })
181 });
182 }
183 else if ($valField.data('select2')) {
184 $valField.select2('destroy');
185 }
186 }
187
188 /**
189 * Attempt to parse a string into a value of the intended type
190 * @param val string
191 * @param makeArray bool
192 */
193 function evaluate(val, makeArray) {
194 try {
195 if (!val.length) {
196 return val;
197 }
198 var first = val.charAt(0),
199 last = val.slice(-1);
200 // Simple types
201 if (val === 'true' || val === 'false' || val === 'null') {
202 return eval(val);
203 }
204 // Integers - quote any number that starts with 0 to avoid oddities
205 if (!isNaN(val) && val.search(/[^\d]/) < 0 && (val.length === 1 || first !== '0')) {
206 return parseInt(val, 10);
207 }
208 // Quoted strings
209 if ((first === '"' || first === "'") && last === first) {
210 return val.slice(1, -1);
211 }
212 // Parse json
213 if ((first === '[' && last === ']') || (first === '{' && last === '}')) {
214 return eval('(' + val + ')');
215 }
216 // Transform csv to array
217 if (makeArray && val.indexOf(',') > 0) {
218 return val.split(',');
219 }
220 // Ok ok it's really a string
221 return val;
222 } catch(e) {
223 // If eval crashed return undefined
224 return undefined;
225 }
226 }
227
228 /**
229 * Format value to look like php code
230 * @param val
231 */
232 function phpFormat(val) {
233 var ret = '';
234 if ($.isPlainObject(val)) {
235 $.each(val, function(k, v) {
236 ret += (ret ? ', ' : '') + "'" + k + "' => " + phpFormat(v);
237 });
238 return 'array(' + ret + ')';
239 }
240 if ($.isArray(val)) {
241 $.each(val, function(k, v) {
242 ret += (ret ? ', ' : '') + phpFormat(v);
243 });
244 return 'array(' + ret + ')';
245 }
246 return JSON.stringify(val).replace(/\$/g, '\\$');
247 }
248
249 /**
250 * Smarty doesn't support array literals so we provide a stub
251 * @param js string
252 */
253 function smartyFormat(js, key) {
254 if (js.indexOf('[') > -1 || js.indexOf('{') > -1) {
255 smartyStub = true;
256 return '$' + key.replace(/[. -]/g, '_');
257 }
258 return js;
259 }
260
261 /**
262 * Create the params array from user input
263 * @param e
264 */
265 function buildParams(e) {
266 params = {};
267 $('.api-param-checkbox:checked').each(function() {
268 params[this.name] = 1;
269 });
270 $('input.api-param-value, input.api-option-value').each(function() {
271 var $row = $(this).closest('tr'),
272 val = evaluate($(this).val(), $(this).is('.select2-offscreen')),
273 name = $('input.api-param-name', $row).val(),
274 op = $('select.api-param-op', $row).val() || '=';
275
276 // Ignore blank values for the return field
277 if ($(this).is('#api-return-value') && !val) {
278 return;
279 }
280 // Special syntax for api chaining
281 if (!name && $('select.api-chain-entity', $row).val()) {
282 name = 'api.' + $('select.api-chain-entity', $row).val() + '.' + $('select.api-chain-action', $row).val();
283 }
284 // Special handling for options
285 if ($(this).is('.api-option-value')) {
286 op = $('input.api-option-name', $row).val();
287 if (op) {
288 name = 'options';
289 }
290 }
291 if (name && val !== undefined) {
292 params[name] = op === '=' ? val : (params[name] || {});
293 if (op !== '=') {
294 params[name][op] = val;
295 }
296 if ($(this).hasClass('crm-error')) {
297 clearError(this);
298 }
299 }
300 else if (name && (!e || e.type !== 'keyup')) {
301 setError(this);
302 }
303 });
304 if (entity && action) {
305 formatQuery();
306 }
307 }
308
309 function setError(el) {
310 if (!$(el).hasClass('crm-error')) {
311 var msg = ts('Syntax error: input should be valid JSON or a quoted string.');
312 $(el)
313 .addClass('crm-error')
314 .css('width', '82%')
315 .attr('title', msg)
316 .before('<div class="icon red-icon ui-icon-alert" title="'+msg+'"/>')
317 .tooltip();
318 }
319 }
320
321 function clearError(el) {
322 $(el)
323 .removeClass('crm-error')
324 .attr('title', '')
325 .css('width', '85%')
326 .tooltip('destroy')
327 .siblings('.ui-icon-alert').remove();
328 }
329
330 function formatQuery() {
331 var i = 0, q = {
332 smarty: "{crmAPI var='result' entity='" + entity + "' action='" + action + "'",
333 php: "$result = civicrm_api3('" + entity + "', '" + action + "'",
334 json: "CRM.api3('" + entity + "', '" + action + "'",
335 drush: "drush cvapi " + entity + '.' + action + ' ',
336 wpcli: "wp cv api " + entity + '.' + action + ' ',
337 rest: CRM.config.resourceBase + "extern/rest.php?entity=" + entity + "&action=" + action + "&json=" + JSON.stringify(params) + "&api_key=yoursitekey&key=yourkey"
338 };
339 smartyStub = false;
340 $.each(params, function(key, value) {
341 var js = JSON.stringify(value);
342 if (!i++) {
343 q.php += ", array(\n";
344 q.json += ", {\n";
345 } else {
346 q.json += ",\n";
347 }
348 q.php += " '" + key + "' => " + phpFormat(value) + ",\n";
349 q.json += " \"" + key + '": ' + js;
350 q.smarty += ' ' + key + '=' + smartyFormat(js, key);
351 q.drush += key + '=' + js + ' ';
352 q.wpcli += key + '=' + js + ' ';
353 });
354 if (i) {
355 q.php += ")";
356 q.json += "\n}";
357 }
358 q.php += ");";
359 q.json += ").done(function(result) {\n // do something\n});";
360 q.smarty += "}\n{foreach from=$result.values item=" + entity.toLowerCase() + "}\n {$" + entity.toLowerCase() + ".some_field}\n{/foreach}";
361 if (action.indexOf('get') < 0) {
362 q.smarty = '{* Smarty API only works with get actions *}';
363 } else if (smartyStub) {
364 q.smarty = "{* Smarty does not have a syntax for array literals; assign complex variables from php *}\n" + q.smarty;
365 }
366 $.each(q, function(type, val) {
367 $('#api-' + type).removeClass('prettyprinted').text(val);
368 });
369 prettyPrint();
370 }
371
372 function submit(e) {
373 e.preventDefault();
374 if (!entity || !action) {
375 alert(ts('Select an entity.'));
376 return;
377 }
378 if (action.indexOf('get') < 0 && action != 'check') {
379 var msg = action === 'delete' ? ts('This will delete data from CiviCRM. Are you sure?') : ts('This will write to the database. Continue?');
380 CRM.confirm({title: ts('Confirm %1', {1: action}), message: msg}).on('crmConfirm:yes', execute);
381 } else {
382 execute();
383 }
384 }
385
386 function execute() {
387 $('#api-result').html('<div class="crm-loading-element"></div>');
388 $.ajax({
389 url: CRM.url('civicrm/ajax/rest'),
390 data: {
391 entity: entity,
392 action: action,
393 prettyprint: 1,
394 json: JSON.stringify(params)
395 },
396 type: action.indexOf('get') < 0 ? 'POST' : 'GET',
397 dataType: 'text'
398 }).done(function(text) {
399 $('#api-result').addClass('prettyprint').removeClass('prettyprinted').text(text);
400 prettyPrint();
401 });
402 }
403
404 $(document).ready(function() {
405 $('form#api-explorer')
406 .on('change', '#api-entity, #api-action', function() {
407 entity = $('#api-entity').val();
408 if ($(this).is('#api-entity')) {
409 getActions();
410 }
411 action = $('#api-action').val();
412 if (entity && action) {
413 $('#api-params').html('<tr><td colspan="4" class="crm-loading-element"></td></tr>');
414 $('#api-params-table thead').show();
415 getFields();
416 buildParams();
417 } else {
418 $('#api-params, #api-generated pre').empty();
419 $('#api-param-buttons, #api-params-table thead').hide();
420 }
421 })
422 .on('change keyup', 'input.api-input, #api-params select', buildParams)
423 .on('submit', submit);
424 $('#api-params')
425 .on('change', '.api-param-name', toggleOptions)
426 .on('change', '.api-param-name, .api-option-name', function() {
427 if ($(this).val() === '-') {
428 $(this).select2('destroy');
429 $(this).val('').focus();
430 }
431 })
432 .on('click', '.api-param-remove', function(e) {
433 e.preventDefault();
434 $(this).closest('tr').remove();
435 buildParams();
436 });
437 $('#api-params-add').on('click', function(e) {
438 e.preventDefault();
439 addField();
440 });
441 $('#api-option-add').on('click', function(e) {
442 e.preventDefault();
443 addOptionField();
444 });
445 $('#api-chain-add').on('click', function(e) {
446 e.preventDefault();
447 addChainField();
448 });
449 $('#api-entity').change();
450 });
451 }(CRM.$, CRM._));