CRM-17857 - Update api explorer to exclude participant joins
[civicrm-core.git] / templates / CRM / Admin / Page / APIExplorer.js
CommitLineData
e4176358 1(function($, _, undefined) {
89ee60d5
CW
2 "use strict";
3 /* jshint validthis: true */
e4176358
CW
4 var
5 entity,
6 action,
5b46e216 7 joins = [],
a14e9d08 8 actions = {values: ['get']},
e4176358 9 fields = [],
fc6a6a51 10 getFieldData = {},
fee3e3c3
CW
11 getFieldsCache = {},
12 getActionsCache = {},
e4176358 13 params = {},
11a28852 14 smartyPhp,
65f22b4b 15 entityDoc,
8ffa1387 16 fieldTpl = _.template($('#api-param-tpl').html()),
c275764e 17 optionsTpl = _.template($('#api-options-tpl').html()),
b07af612 18 returnTpl = _.template($('#api-return-tpl').html()),
77099ee0 19 chainTpl = _.template($('#api-chain-tpl').html()),
bc4aa590 20 docCodeTpl = _.template($('#doc-code-tpl').html()),
fee3e3c3 21 joinTpl = _.template($('#join-tpl').html()),
77099ee0 22
39eceaba
CW
23 // The following apis do not support the syntax for joins
24 // FIXME: the solution is to convert these apis to use _civicrm_api3_basic_get
25 NO_JOINS = ['Contact', 'Contribution', 'Pledge', 'Participant'],
26
f76eb8ae 27 // These types of entityRef don't require any input to open
a39f2446 28 // FIXME: ought to be in getfields metadata
f76eb8ae
CW
29 OPEN_IMMEDIATELY = ['RelationshipType', 'Event', 'Group', 'Tag'],
30
65b8c1db
CW
31 // Actions that don't support fancy operators
32 NO_OPERATORS = ['create', 'update', 'delete', 'setvalue', 'getoptions', 'getactions', 'getfields'],
33
5bd06c36 34 // Actions that don't support multiple values
26a700db 35 NO_MULTI = ['delete', 'getoptions', 'getactions', 'getfields', 'getfield', 'setvalue'],
5bd06c36 36
77099ee0
CW
37 // Operators with special properties
38 BOOL = ['IS NULL', 'IS NOT NULL'],
39 TEXT = ['LIKE', 'NOT LIKE'],
40 MULTI = ['IN', 'NOT IN', 'BETWEEN', 'NOT BETWEEN'];
0c3a6e64 41
1080dff4
CW
42 /**
43 * Call prettyPrint function and perform additional formatting
44 * @param ele
45 */
46 function prettyPrint(ele) {
47 if (typeof window.prettyPrint === 'function') {
48 $(ele).removeClass('prettyprinted').addClass('prettyprint');
49
50 window.prettyPrint();
51
52 // Highlight errors in api result
53 $('span:contains("error_code"),span:contains("error_message")', '#api-result')
54 .siblings('span.str').css('color', '#B00');
55 }
56 }
57
5b46e216
CW
58 /**
59 * Data provider for select2 "field" selectors
60 * @returns {{results: Array.<T>}}
61 */
62 function returnFields() {
63 return {results: fields.concat({id: '-', text: ts('Other') + '...', description: ts('Choose a field not in this list')})};
64 }
65
66 /**
67 * Recursively populates data for select2 "field" selectors
68 * @param fields
69 * @param entity
70 * @param action
71 * @param prefix
b93bc6ae 72 * @param required
5b46e216 73 */
b93bc6ae 74 function populateFields(fields, entity, action, prefix, required) {
5b46e216
CW
75 _.each(getFieldsCache[entity+action].values, function(field) {
76 var name = prefix + field.name,
77 pos = fields.length;
78 fields.push({
79 id: name,
80 text: field.title || field.name,
81 multi: !!field['api.multiple'],
82 description: field.description || '',
83 required: !(!field['api.required'] || field['api.required'] === '0')
84 });
85 if (joins[name]) {
86 fields[pos].children = [];
87 populateFields(fields[pos].children, joins[name], 'get', name + '.');
88 }
b93bc6ae 89 if (!prefix && required && field['api.required'] && field['api.required'] !== '0') {
5b46e216
CW
90 required.push(field.name);
91 }
92 });
93 }
94
95 /**
96 * Fetch metadata for a field by name - searches across joins
97 * @param name string
98 * @returns {*}
99 */
100 function getField(name) {
807915a9
CW
101 if (!name) {
102 return {};
103 }
5b46e216
CW
104 if (getFieldData[name]) {
105 return getFieldData[name];
106 }
107 var ent = entity,
108 act = action,
109 prefix = '';
110 _.each(name.split('.'), function(piece) {
111 if (joins[prefix]) {
112 ent = joins[prefix];
113 act = 'get';
114 }
115 name = piece;
116 prefix += (prefix.length ? '.' : '') + piece;
117 });
118 return getFieldsCache[ent+act].values[name] || {};
119 }
120
b07af612
CW
121 /**
122 * Add a "fields" row
5b46e216 123 * @param name string
b07af612 124 */
e4176358 125 function addField(name) {
65b8c1db 126 $('#api-params').append($(fieldTpl({name: name || '', noOps: _.includes(NO_OPERATORS, action)})));
e4176358 127 var $row = $('tr:last-child', '#api-params');
77099ee0 128 $('input.api-param-name', $row).crmSelect2({
5b46e216 129 data: returnFields,
29956c59
CW
130 formatSelection: function(field) {
131 return field.text +
132 (field.required ? ' <span class="crm-marker">*</span>' : '');
133 },
5c7169eb 134 formatResult: function(field) {
29956c59
CW
135 return field.text +
136 (field.required ? ' <span class="crm-marker">*</span>' : '') +
137 '<div class="api-field-desc">' + field.description + '</div>';
5c7169eb 138 }
b07af612 139 }).change();
8ffa1387
CW
140 }
141
c275764e
CW
142 /**
143 * Add a new "options" row
c275764e 144 */
cb6d8859 145 function addOptionField() {
c275764e
CW
146 if ($('.api-options-row', '#api-params').length) {
147 $('.api-options-row:last', '#api-params').after($(optionsTpl({})));
148 } else {
149 $('#api-params').append($(optionsTpl({})));
150 }
151 var $row = $('.api-options-row:last', '#api-params');
152 $('.api-option-name', $row).crmSelect2({data: [
153 {id: 'limit', text: 'limit'},
154 {id: 'offset', text: 'offset'},
155 {id: 'sort', text: 'sort'},
156 {id: 'metadata', text: 'metadata'},
157 {id: '-', text: ts('Other') + '...'}
158 ]});
159 }
160
b07af612
CW
161 /**
162 * Add an "api chain" row
163 */
8ffa1387
CW
164 function addChainField() {
165 $('#api-params').append($(chainTpl({})));
166 var $row = $('tr:last-child', '#api-params');
167 $('.api-chain-entity', $row).crmSelect2({
168 formatSelection: function(item) {
41d0b881 169 return '<i class="crm-i fa-link"></i> API ' +
3d9f5b7f 170 ($(item.element).hasClass('strikethrough') ? '<span class="strikethrough">' + item.text + '</span>' : item.text);
2b6e1174 171 },
41d0b881 172 placeholder: '<i class="crm-i fa-link"></i> ' + ts('Entity'),
ff887074 173 escapeMarkup: function(m) {return m;}
8ffa1387 174 });
0c3a6e64
CW
175 }
176
3d9f5b7f
CW
177 /**
178 * Fetch available actions for selected chained entity
179 */
180 function getChainedAction() {
181 var
182 $selector = $(this),
183 entity = $selector.val(),
184 $row = $selector.closest('tr');
185 if (entity) {
186 $selector.prop('disabled', true);
5b46e216 187 getActions(entity)
3d9f5b7f
CW
188 .done(function(actions) {
189 $selector.prop('disabled', false);
ff887074 190 CRM.utils.setOptions($('.api-chain-action', $row), _.transform(actions.values, function(ret, item) {ret.push({value: item, key: item});}));
3d9f5b7f
CW
191 });
192 }
193 }
194
b07af612 195 /**
fee3e3c3
CW
196 * Fetch metadata from the api and cache locally for performance
197 * Returns a deferred object which resolves to entity.getfields
b07af612 198 */
fee3e3c3
CW
199 function getMetadata(entity, action) {
200 var response = $.Deferred();
201 if (getFieldsCache[entity+action]) {
202 response.resolve(getFieldsCache[entity+action]);
203 } else {
204 var apiCalls = {
205 getfields: [entity, 'getfields', {
206 api_action: action,
207 options: {get_options: 'all', get_options_context: 'match'}
208 }]
209 };
210 if (!getActionsCache[entity]) {
211 apiCalls.getactions = [entity, 'getactions'];
212 }
213 CRM.api3(apiCalls)
214 .done(function(data) {
5b46e216 215 data.getfields.values = _.indexBy(data.getfields.values, 'name');
fee3e3c3
CW
216 getFieldsCache[entity+action] = data.getfields;
217 getActionsCache[entity] = getActionsCache[entity] || data.getactions;
5b46e216 218 response.resolve(getFieldsCache[entity+action]);
fee3e3c3
CW
219 });
220 }
221 return response;
222 }
223
b93bc6ae
CW
224 /**
225 * TODO: This works given the current code structure but would cause race conditions if called many times per second
226 * @param entity string
227 * @returns $.Deferred
228 */
5b46e216
CW
229 function getActions(entity) {
230 if (getActionsCache[entity]) {
231 return $.Deferred().resolve(getActionsCache[entity]);
232 } else {
233 return CRM.api3(entity, 'getactions');
234 }
235 }
236
fee3e3c3
CW
237 /**
238 * Respond to changing the main entity+action
239 */
240 function onChangeEntityOrAction(changedElement) {
e4176358
CW
241 var required = [];
242 fields = [];
b93bc6ae 243 joins = [];
932630fd 244 getFieldData = {};
e4176358
CW
245 // Special case for getfields
246 if (action === 'getfields') {
247 fields.push({
248 id: 'api_action',
b8aee616
CW
249 text: ts('Action')
250 });
251 getFieldData.api_action = {
5b46e216 252 name: 'api_action',
a39f2446
CW
253 options: _.reduce(actions.values, function(ret, item) {
254 ret[item] = item;
255 return ret;
256 }, {})
b8aee616 257 };
e4176358 258 showFields(['api_action']);
b93bc6ae 259 renderJoinSelector();
0c3a6e64
CW
260 return;
261 }
fee3e3c3
CW
262 getMetadata(entity, action).done(function(data) {
263 if ($(changedElement).is('#api-entity')) {
264 actions = getActionsCache[entity];
265 populateActions();
266 if (data.deprecated) CRM.alert(data.deprecated, entity + ' Deprecated');
267 }
268 onChangeAction(action);
5b46e216 269 getFieldData = data.values;
b93bc6ae 270 populateFields(fields, entity, action, '', required);
e4176358 271 showFields(required);
5b46e216
CW
272 renderJoinSelector();
273 if (_.includes(['get', 'getsingle', 'getvalue', 'getstat'], action)) {
3a721dfc 274 showReturn();
b07af612 275 }
0c3a6e64
CW
276 });
277 }
278
b07af612
CW
279 /**
280 * For "get" actions show the "return" options
3a721dfc
CW
281 *
282 * TODO: Too many hard-coded actions here. Need a way to fetch this from metadata
b07af612 283 */
3a721dfc
CW
284 function showReturn() {
285 var title = ts('Fields to return'),
286 params = {
5b46e216 287 data: returnFields,
3a721dfc 288 multiple: true,
5b46e216
CW
289 placeholder: ts('Leave blank for default'),
290 formatResult: function(field) {
291 return field.text + '<div class="api-field-desc">' + field.description + '</div>';
292 }
3a721dfc
CW
293 };
294 if (action == 'getstat') {
295 title = ts('Group by');
296 }
297 if (action == 'getvalue') {
298 title = ts('Return Value');
299 params.placeholder = ts('Select field');
300 params.multiple = false;
301 }
29956c59 302 $('#api-params').prepend($(returnTpl({title: title, required: action == 'getvalue'})));
3a721dfc 303 $('#api-return-value').crmSelect2(params);
b07af612
CW
304 }
305
7231faaf
CW
306 /**
307 * Test whether an action is deprecated
308 * @param action
309 * @returns {boolean}
310 */
15cbe793
CW
311 function isActionDeprecated(action) {
312 return !!(typeof actions.deprecated === 'object' && actions.deprecated[action]);
313 }
314
7231faaf
CW
315 /**
316 * Render action text depending on deprecation status
317 * @param option
318 * @returns {string}
319 */
a14e9d08 320 function renderAction(option) {
15cbe793 321 return isActionDeprecated(option.id) ? '<span class="strikethrough">' + option.text + '</span>' : option.text;
a14e9d08
CW
322 }
323
b07af612
CW
324 /**
325 * Called after getActions to populate action list
b07af612 326 */
cb6d8859 327 function populateActions() {
2f929504 328 var val = $('#api-action').val();
754927e4 329 $('#api-action').removeClass('loading').select2({
ff887074 330 data: _.transform(actions.values, function(ret, item) {ret.push({text: item, id: item});}),
a14e9d08
CW
331 formatSelection: renderAction,
332 formatResult: renderAction
2b6e1174 333 });
2f929504 334 // If previously selected action is not available, set it to 'get' if possible
ed0eaccc
CW
335 if (!_.includes(actions.values, val)) {
336 $('#api-action').select2('val', !_.includes(actions.values, 'get') ? actions.values[0] : 'get', true);
a14e9d08
CW
337 }
338 }
339
7231faaf
CW
340 /**
341 * Check for and display action-specific deprecation notices
342 * @param action
343 */
a14e9d08 344 function onChangeAction(action) {
15cbe793 345 if (isActionDeprecated(action)) {
a14e9d08 346 CRM.alert(actions.deprecated[action], action + ' deprecated');
2f929504 347 }
2b6e1174
CW
348 }
349
b07af612
CW
350 /**
351 * Called after getfields to show buttons and required fields
352 * @param required
353 */
e4176358 354 function showFields(required) {
e4176358 355 $('#api-params').empty();
8ffa1387 356 $('#api-param-buttons').show();
e4176358
CW
357 if (required.length) {
358 _.each(required, addField);
359 } else {
360 addField();
0c3a6e64
CW
361 }
362 }
363
4aed36b2 364 function isYesNo(fieldName) {
5b46e216 365 return getField(fieldName).type === 16;
4aed36b2
CW
366 }
367
d5a9020d
CW
368 /**
369 * Should we render a select or textfield?
370 *
371 * @param fieldName
372 * @param operator
373 * @returns boolean
374 */
375 function isSelect(fieldName, operator) {
5b46e216 376 var fieldSpec = getField(fieldName);
a39f2446 377 return (isYesNo(fieldName) || fieldSpec.options || fieldSpec.FKApiName) && !_.includes(TEXT, operator);
d5a9020d
CW
378 }
379
380 /**
381 * Should we render a select as single or multi?
382 *
383 * @param fieldName
384 * @param operator
385 * @returns boolean
386 */
7b6c5043 387 function isMultiSelect(fieldName, operator) {
5bd06c36 388 if (isYesNo(fieldName) || _.includes(NO_MULTI, action)) {
4aed36b2
CW
389 return false;
390 }
ed0eaccc 391 if (_.includes(MULTI, operator)) {
d5a9020d
CW
392 return true;
393 }
394 // The = operator is ambiguous but all others can be safely assumed to be single
395 if (operator !== '=') {
396 return false;
397 }
398 return true;
399 /*
400 * Attempt to resolve the ambiguity of the = operator using metadata
401 * commented out because there is not enough metadata in the api at this time
402 * to accurately figure it out.
403 */
404 // var field = fieldName && _.find(fields, 'id', fieldName);
405 // return field && field.multi;
7b6c5043
CW
406 }
407
c275764e 408 /**
fc6a6a51 409 * Render value input as a textfield, option list, entityRef, or hidden,
77099ee0 410 * Depending on selected param name and operator
c275764e 411 */
77099ee0
CW
412 function renderValueField() {
413 var $row = $(this).closest('tr'),
414 name = $('input.api-param-name', $row).val(),
415 operator = $('.api-param-op', $row).val(),
77099ee0 416 $valField = $('input.api-param-value', $row),
7b6c5043 417 multiSelect = isMultiSelect(name, operator),
6e29772f 418 currentVal = $valField.val(),
5b46e216 419 fieldSpec = getField(name),
6e29772f
CW
420 wasSelect = $valField.data('select2');
421 if (wasSelect) {
422 $valField.crmEntityRef('destroy');
423 }
424 $valField.attr('placeholder', ts('Value'));
77099ee0 425 // Boolean fields only have 1 possible value
ed0eaccc 426 if (_.includes(BOOL, operator)) {
77099ee0
CW
427 $valField.css('visibility', 'hidden').val('1');
428 return;
429 }
430 $valField.css('visibility', '');
fc6a6a51 431 // Option list or entityRef input
d5a9020d 432 if (isSelect(name, operator)) {
6e29772f 433 $valField.attr('placeholder', ts('- select -'));
77099ee0 434 // Reset value before switching to a select from something else
6e29772f 435 if ($(this).is('.api-param-name') || !wasSelect) {
77099ee0
CW
436 $valField.val('');
437 }
438 // When switching from multi-select to single select
ed0eaccc 439 else if (!multiSelect && _.includes(currentVal, ',')) {
77099ee0
CW
440 $valField.val(currentVal.split(',')[0]);
441 }
4aed36b2
CW
442 // Yes-No options
443 if (isYesNo(name)) {
444 $valField.select2({
445 data: [{id: 1, text: ts('Yes')}, {id: 0, text: ts('No')}]
446 });
447 }
fc6a6a51 448 // Select options
5b46e216 449 else if (fieldSpec.options) {
fc6a6a51 450 $valField.select2({
7b6c5043 451 multiple: multiSelect,
5b46e216 452 data: _.map(fieldSpec.options, function (value, key) {
fc6a6a51
CW
453 return {id: key, text: value};
454 })
455 });
456 }
457 // EntityRef
458 else {
5b46e216 459 var entity = fieldSpec.FKApiName;
6e29772f 460 $valField.attr('placeholder', entity == 'Contact' ? '[' + ts('Auto-Select Current User') + ']' : ts('- select -'));
fc6a6a51 461 $valField.crmEntityRef({
6e29772f 462 entity: entity,
f76eb8ae 463 select: {
7b6c5043 464 multiple: multiSelect,
e6de5a11
CW
465 minimumInputLength: _.includes(OPEN_IMMEDIATELY, entity) ? 0 : 1,
466 // If user types a numeric id, allow it as a choice
467 createSearchChoice: function(input) {
468 var match = /[1-9][0-9]*/.exec(input);
469 if (match && match[0] === input) {
470 return {id: input, label: input};
471 }
472 }
f76eb8ae 473 }
fc6a6a51
CW
474 });
475 }
e4176358 476 }
e4176358 477 }
0c3a6e64 478
e4176358
CW
479 /**
480 * Attempt to parse a string into a value of the intended type
babf9678
CW
481 * @param val string
482 * @param makeArray bool
e4176358
CW
483 */
484 function evaluate(val, makeArray) {
485 try {
486 if (!val.length) {
77099ee0 487 return makeArray ? [] : '';
e4176358
CW
488 }
489 var first = val.charAt(0),
490 last = val.slice(-1);
491 // Simple types
51f197bf 492 if (val === 'true' || val === 'false' || val === 'null') {
cb6d8859 493 /* jshint evil: true */
e4176358
CW
494 return eval(val);
495 }
496 // Quoted strings
497 if ((first === '"' || first === "'") && last === first) {
498 return val.slice(1, -1);
0c3a6e64 499 }
7231faaf 500 // Parse json - use eval rather than $.parseJSON because it's less strict about formatting
e4176358
CW
501 if ((first === '[' && last === ']') || (first === '{' && last === '}')) {
502 return eval('(' + val + ')');
503 }
504 // Transform csv to array
77099ee0
CW
505 if (makeArray) {
506 var result = [];
507 $.each(val.split(','), function(k, v) {
508 result.push(evaluate($.trim(v)) || v);
509 });
510 return result;
511 }
7231faaf 512 // Integers - skip any multidigit number that starts with 0 to avoid oddities (it will be treated as a string below)
77099ee0
CW
513 if (!isNaN(val) && val.search(/[^\d]/) < 0 && (val.length === 1 || first !== '0')) {
514 return parseInt(val, 10);
e4176358
CW
515 }
516 // Ok ok it's really a string
517 return val;
518 } catch(e) {
519 // If eval crashed return undefined
520 return undefined;
0c3a6e64 521 }
e4176358 522 }
0c3a6e64 523
e4176358
CW
524 /**
525 * Format value to look like php code
6e29772f 526 * TODO: Use short array syntax when we drop support for php 5.3
e4176358
CW
527 * @param val
528 */
529 function phpFormat(val) {
530 var ret = '';
531 if ($.isPlainObject(val)) {
532 $.each(val, function(k, v) {
c275764e 533 ret += (ret ? ', ' : '') + "'" + k + "' => " + phpFormat(v);
e4176358
CW
534 });
535 return 'array(' + ret + ')';
0c3a6e64 536 }
e4176358
CW
537 if ($.isArray(val)) {
538 $.each(val, function(k, v) {
539 ret += (ret ? ', ' : '') + phpFormat(v);
540 });
541 return 'array(' + ret + ')';
542 }
54c512e0 543 return JSON.stringify(val).replace(/\$/g, '\\$');
e4176358
CW
544 }
545
546 /**
11a28852 547 * @param value string
e4176358 548 * @param js string
d5a9020d 549 * @param key string
e4176358 550 */
11a28852
CW
551 function smartyFormat(value, js, key) {
552 var varName = 'param_' + key.replace(/[. -]/g, '_').toLowerCase();
553 // Can't pass array literals directly into smarty so we add a php snippet
ed0eaccc 554 if (_.includes(js, '[') || _.includes(js, '{')) {
11a28852
CW
555 smartyPhp.push('$this->assign("'+ varName + '", '+ phpFormat(value) +');');
556 return '$' + varName;
e4176358
CW
557 }
558 return js;
559 }
560
c275764e
CW
561 /**
562 * Create the params array from user input
563 * @param e
564 */
e4176358
CW
565 function buildParams(e) {
566 params = {};
567 $('.api-param-checkbox:checked').each(function() {
568 params[this.name] = 1;
569 });
c275764e 570 $('input.api-param-value, input.api-option-value').each(function() {
e4176358 571 var $row = $(this).closest('tr'),
d5a9020d 572 input = $(this).val(),
77099ee0 573 op = $('select.api-param-op', $row).val() || '=',
e4176358 574 name = $('input.api-param-name', $row).val(),
d5a9020d 575 // Workaround for ambiguity of the = operator
ed0eaccc 576 makeArray = (op === '=' && isSelect(name, op)) ? _.includes(input, ',') : op !== '=' && isMultiSelect(name, op),
d5a9020d 577 val = evaluate(input, makeArray);
b07af612
CW
578
579 // Ignore blank values for the return field
580 if ($(this).is('#api-return-value') && !val) {
581 return;
582 }
8ffa1387
CW
583 // Special syntax for api chaining
584 if (!name && $('select.api-chain-entity', $row).val()) {
585 name = 'api.' + $('select.api-chain-entity', $row).val() + '.' + $('select.api-chain-action', $row).val();
586 }
c275764e
CW
587 // Special handling for options
588 if ($(this).is('.api-option-value')) {
589 op = $('input.api-option-name', $row).val();
590 if (op) {
591 name = 'options';
592 }
593 }
6e29772f
CW
594 // Default for contact ref fields
595 if ($(this).is('.crm-contact-ref') && input === '') {
596 val = evaluate('user_contact_id', makeArray);
597 }
e4176358 598 if (name && val !== undefined) {
c275764e 599 params[name] = op === '=' ? val : (params[name] || {});
e4176358
CW
600 if (op !== '=') {
601 params[name][op] = val;
602 }
babf9678
CW
603 if ($(this).hasClass('crm-error')) {
604 clearError(this);
605 }
e4176358
CW
606 }
607 else if (name && (!e || e.type !== 'keyup')) {
608 setError(this);
609 }
610 });
611 if (entity && action) {
612 formatQuery();
0c3a6e64 613 }
e4176358 614 }
0c3a6e64 615
7231faaf
CW
616 /**
617 * Display error message on incorrectly-formatted params
618 * @param el
619 */
e4176358
CW
620 function setError(el) {
621 if (!$(el).hasClass('crm-error')) {
babf9678 622 var msg = ts('Syntax error: input should be valid JSON or a quoted string.');
e4176358
CW
623 $(el)
624 .addClass('crm-error')
2b6e1174 625 .css('width', '82%')
babf9678 626 .attr('title', msg)
bc8360e1 627 .before('<i class="crm-i fa-exclamation-triangle crm-i-red" title="'+msg+'"></i> ')
babf9678 628 .tooltip();
e4176358
CW
629 }
630 }
0c3a6e64 631
7231faaf
CW
632 /**
633 * Remove error message
634 * @param el
635 */
e4176358
CW
636 function clearError(el) {
637 $(el)
638 .removeClass('crm-error')
639 .attr('title', '')
2b6e1174 640 .css('width', '85%')
babf9678 641 .tooltip('destroy')
8777ffd1 642 .siblings('.fa-exclamation-triangle').remove();
e4176358 643 }
0c3a6e64 644
7231faaf
CW
645 /**
646 * Render the api request in various formats
647 */
e4176358
CW
648 function formatQuery() {
649 var i = 0, q = {
11a28852 650 smarty: "{crmAPI var='result' entity='" + entity + "' action='" + action + "'" + (params.sequential ? '' : ' sequential=0'),
e4176358
CW
651 php: "$result = civicrm_api3('" + entity + "', '" + action + "'",
652 json: "CRM.api3('" + entity + "', '" + action + "'",
2dad2b26
TO
653 drush: "drush cvapi " + entity + '.' + action + ' ',
654 wpcli: "wp cv api " + entity + '.' + action + ' ',
de69691f 655 rest: CRM.config.resourceBase + "extern/rest.php?entity=" + entity + "&action=" + action + "&api_key=userkey&key=sitekey&json=" + JSON.stringify(params)
e4176358 656 };
11a28852 657 smartyPhp = [];
e4176358 658 $.each(params, function(key, value) {
a2ea6df9
CW
659 var json = JSON.stringify(value),
660 // Encourage 'return' to be an array - at least in php & js
661 js = key === 'return' ? JSON.stringify(evaluate(value, true)) : json,
662 php = key === 'return' ? phpFormat(evaluate(value, true)) : phpFormat(value);
cb6d8859 663 if (!(i++)) {
e4176358
CW
664 q.php += ", array(\n";
665 q.json += ", {\n";
666 } else {
667 q.json += ",\n";
668 }
a2ea6df9 669 q.php += " '" + key + "' => " + php + ",\n";
e4176358 670 q.json += " \"" + key + '": ' + js;
11a28852
CW
671 // smarty already defaults to sequential
672 if (key !== 'sequential') {
a2ea6df9 673 q.smarty += ' ' + key + '=' + smartyFormat(value, json, key);
11a28852 674 }
77099ee0 675 // FIXME: This is not totally correct cli syntax
a2ea6df9
CW
676 q.drush += key + '=' + json + ' ';
677 q.wpcli += key + '=' + json + ' ';
e4176358
CW
678 });
679 if (i) {
680 q.php += ")";
681 q.json += "\n}";
682 }
683 q.php += ");";
684 q.json += ").done(function(result) {\n // do something\n});";
685 q.smarty += "}\n{foreach from=$result.values item=" + entity.toLowerCase() + "}\n {$" + entity.toLowerCase() + ".some_field}\n{/foreach}";
ed0eaccc 686 if (!_.includes(action, 'get')) {
e4176358 687 q.smarty = '{* Smarty API only works with get actions *}';
11a28852
CW
688 } else if (smartyPhp.length) {
689 q.smarty = "{php}\n " + smartyPhp.join("\n ") + "\n{/php}\n" + q.smarty;
e4176358
CW
690 }
691 $.each(q, function(type, val) {
1080dff4 692 $('#api-' + type).text(val);
e4176358 693 });
1080dff4 694 prettyPrint('#api-generated pre');
e4176358 695 }
0c3a6e64 696
7231faaf
CW
697 /**
698 * Submit button handler
699 * @param e
700 */
e4176358
CW
701 function submit(e) {
702 e.preventDefault();
703 if (!entity || !action) {
2b6e1174 704 alert(ts('Select an entity.'));
e4176358
CW
705 return;
706 }
ed0eaccc 707 if (!_.includes(action, 'get') && action != 'check') {
e4176358
CW
708 var msg = action === 'delete' ? ts('This will delete data from CiviCRM. Are you sure?') : ts('This will write to the database. Continue?');
709 CRM.confirm({title: ts('Confirm %1', {1: action}), message: msg}).on('crmConfirm:yes', execute);
0c3a6e64 710 } else {
e4176358 711 execute();
0c3a6e64 712 }
0c3a6e64
CW
713 }
714
77099ee0
CW
715 /**
716 * Execute api call and display the results
717 * Note: We have to manually execute the ajax in order to add the secret extra "prettyprint" param
718 */
e4176358 719 function execute() {
b308e50d 720 var footer;
e4176358
CW
721 $('#api-result').html('<div class="crm-loading-element"></div>');
722 $.ajax({
723 url: CRM.url('civicrm/ajax/rest'),
724 data: {
725 entity: entity,
726 action: action,
727 prettyprint: 1,
728 json: JSON.stringify(params)
729 },
ed0eaccc 730 type: _.includes(action, 'get') ? 'GET' : 'POST',
e4176358
CW
731 dataType: 'text'
732 }).done(function(text) {
b308e50d
CW
733 // There may be debug information appended to the end of the json string
734 var footerPos = text.indexOf("\n}<");
735 if (footerPos) {
736 footer = text.substr(footerPos + 2);
737 text = text.substr(0, footerPos + 2);
738 }
1080dff4
CW
739 $('#api-result').text(text);
740 prettyPrint('#api-result');
b308e50d
CW
741 if (footer) {
742 $('#api-result').append(footer);
743 }
e4176358 744 });
0c3a6e64 745 }
e4176358 746
89ee60d5
CW
747 /**
748 * Fetch list of example files for a given entity
749 */
750 function getExamples() {
751 CRM.utils.setOptions($('#example-action').prop('disabled', true).addClass('loading'), []);
752 $.getJSON(CRM.url('civicrm/ajax/apiexample', {entity: $(this).val()}))
753 .done(function(result) {
754 CRM.utils.setOptions($('#example-action').prop('disabled', false).removeClass('loading'), result);
755 });
756 }
757
758 /**
759 * Fetch and display an example file
760 */
761 function getExample() {
762 var
763 entity = $('#example-entity').val(),
764 action = $('#example-action').val();
765 if (entity && action) {
766 $('#example-result').block();
767 $.get(CRM.url('civicrm/ajax/apiexample', {file: entity + '/' + action}))
768 .done(function(result) {
1080dff4
CW
769 $('#example-result').unblock().text(result);
770 prettyPrint('#example-result');
89ee60d5
CW
771 });
772 } else {
65f22b4b 773 $('#example-result').text($('#example-result').attr('placeholder'));
89ee60d5
CW
774 }
775 }
776
bc4aa590
CW
777 /**
778 * Fetch entity docs & actions
779 */
780 function getDocEntity() {
781 CRM.utils.setOptions($('#doc-action').prop('disabled', true).addClass('loading'), []);
782 $.getJSON(CRM.url('civicrm/ajax/apidoc', {entity: $(this).val()}))
783 .done(function(result) {
65f22b4b 784 entityDoc = result.doc;
bc4aa590
CW
785 CRM.utils.setOptions($('#doc-action').prop('disabled', false).removeClass('loading'), result.actions);
786 $('#doc-result').html(result.doc);
fea52a54 787 prettyPrint('#doc-result pre');
bc4aa590
CW
788 });
789 }
790
791 /**
792 * Fetch entity+action docs & code
793 */
794 function getDocAction() {
795 var
796 entity = $('#doc-entity').val(),
797 action = $('#doc-action').val();
798 if (entity && action) {
799 $('#doc-result').block();
800 $.get(CRM.url('civicrm/ajax/apidoc', {entity: entity, action: action}))
801 .done(function(result) {
802 $('#doc-result').unblock().html(result.doc);
803 if (result.code) {
804 $('#doc-result').append(docCodeTpl(result));
805 }
fea52a54 806 prettyPrint('#doc-result pre');
bc4aa590
CW
807 });
808 } else {
65f22b4b 809 $('#doc-result').html(entityDoc);
fea52a54 810 prettyPrint('#doc-result pre');
bc4aa590 811 }
85470f85
PN
812 checkBookKeepingEntity(entity, action);
813 }
8777ffd1 814
85470f85
PN
815 /**
816 * Check if entity is Financial Trxn and Entity Financial Trxn
817 * and Action is Create, delete, update etc then display warning
818 */
8777ffd1 819 function checkBookKeepingEntity(entity, action) {
23989f49 820 if ($.inArray(entity, ['EntityFinancialTrxn', 'FinancialTrxn']) > -1 && $.inArray(action, ['delete', 'setvalue', 'replace', 'create']) > -1) {
85470f85
PN
821 var msg = ts('Given the importance of auditability, extension developers are strongly discouraged from writing code to add, update or delete entries in the civicrm_financial_item, civicrm_entity_financial_trxn, and civicrm_financial_trxn tables. Before publishing an extension on civicrm.org that does any of this, please ask for a special bookkeeping code review for the extension.');
822 CRM.alert(msg, 'warning');
823 }
bc4aa590
CW
824 }
825
5b46e216
CW
826 /**
827 * Renders nested checkboxes for adding joins to an api.get call
828 */
829 function renderJoinSelector() {
830 $('#api-join').hide();
39eceaba 831 if (!_.includes(NO_JOINS, entity) && _.includes(['get', 'getsingle'], action)) {
5b46e216 832 var joinable = {};
1f172d3f 833 (function recurse(fields, joinable, prefix, depth, entities) {
5b46e216 834 _.each(fields, function(field) {
1f172d3f
CW
835 var entity = field.FKApiName;
836 if (entity && field.FKClassName) {
5b46e216
CW
837 var name = prefix + field.name;
838 joinable[name] = {
839 title: field.title,
1f172d3f 840 entity: entity,
5b46e216
CW
841 checked: !!joins[name]
842 };
1f172d3f
CW
843 // Expose further joins if we are not over the limit or recursing onto the same entity multiple times
844 if (joins[name] && depth < CRM.vars.explorer.max_joins && !_.countBy(entities)[entity]) {
5b46e216 845 joinable[name].children = {};
1f172d3f 846 recurse(getFieldsCache[entity+'get'].values, joinable[name].children, name + '.', depth+1, entities.concat(entity));
5b46e216
CW
847 }
848 }
849 });
1f172d3f 850 })(getFieldData, joinable, '', 1, [entity]);
5b46e216
CW
851 if (!_.isEmpty(joinable)) {
852 // Send joinTpl as a param so it can recursively call itself to render children
853 $('#api-join').show().children('div').html(joinTpl({joins: joinable, tpl: joinTpl}));
854 }
855 }
856 }
857
858 /**
859 * When adding or removing a join from an api.get call
860 */
861 function onSelectJoin() {
862 var name = $(this).val(),
863 ent = $(this).data('entity');
864 fields = [];
865 $('input', '#api-join').prop('disabled', true);
866 if ($(this).is(':checked')) {
867 joins[name] = ent;
868 $('input.api-param-name, #api-return-value').addClass('loading');
869 getMetadata(ent, 'get').done(function() {
870 renderJoinSelector();
871 populateFields(fields, entity, action, '');
872 $('input.api-param-name, #api-return-value').removeClass('loading');
873 });
874 } else {
875 joins = _.omit(joins, function(entity, n) {
876 return n.indexOf(name) === 0;
877 });
878 renderJoinSelector();
879 populateFields(fields, entity, action, '');
880 }
881 }
882
e4176358 883 $(document).ready(function() {
89ee60d5
CW
884 // Set up tabs - bind active tab to document hash because... it's cool?
885 document.location.hash = document.location.hash || 'explorer';
886 $('#mainTabContainer')
887 .tabs({
888 active: $(document.location.hash + '-tab').index() - 1
889 })
890 .on('tabsactivate', function(e, ui) {
891 if (ui.newPanel) {
892 document.location.hash = ui.newPanel.attr('id').replace('-tab', '');
893 }
894 });
bc4aa590
CW
895 $(window).on('hashchange', function() {
896 $('#mainTabContainer').tabs('option', 'active', $(document.location.hash + '-tab').index() - 1);
897 });
89ee60d5
CW
898
899 // Initialize widgets
bc4aa590 900 $('#api-entity, #example-entity, #doc-entity').crmSelect2({
a14e9d08
CW
901 // Add strikethough class to selection to indicate deprecated apis
902 formatSelection: function(option) {
903 return $(option.element).hasClass('strikethrough') ? '<span class="strikethrough">' + option.text + '</span>' : option.text;
904 }
905 });
e4176358
CW
906 $('form#api-explorer')
907 .on('change', '#api-entity, #api-action', function() {
908 entity = $('#api-entity').val();
a14e9d08 909 action = $('#api-action').val();
5b46e216 910 joins = {};
2b6e1174 911 if ($(this).is('#api-entity')) {
fee3e3c3 912 $('#api-action').addClass('loading');
e4176358 913 }
fee3e3c3
CW
914 $('#api-params').html('<tr><td colspan="4" class="crm-loading-element"></td></tr>');
915 $('#api-params-table thead').show();
916 onChangeEntityOrAction(this);
917 buildParams();
918 checkBookKeepingEntity(entity, action);
e4176358 919 })
2b6e1174 920 .on('change keyup', 'input.api-input, #api-params select', buildParams)
e4176358 921 .on('submit', submit);
5b46e216 922
e4176358 923 $('#api-params')
77099ee0 924 .on('change', 'input.api-param-name, select.api-param-op', renderValueField)
5b46e216
CW
925 .on('select2-selecting', 'input.api-param-name, .api-option-name, #api-return-value', function(e) {
926 if (e.val === '-') {
927 $(this).one('change', function() {
928 $(this)
929 .crmSelect2('destroy')
930 .val('')
931 .focus();
932 });
c275764e
CW
933 }
934 })
e4176358
CW
935 .on('click', '.api-param-remove', function(e) {
936 e.preventDefault();
937 $(this).closest('tr').remove();
938 buildParams();
3d9f5b7f
CW
939 })
940 .on('change', 'select.api-chain-entity', getChainedAction);
5b46e216 941 $('#api-join').on('change', 'input', onSelectJoin);
89ee60d5
CW
942 $('#example-entity').on('change', getExamples);
943 $('#example-action').on('change', getExample);
bc4aa590
CW
944 $('#doc-entity').on('change', getDocEntity);
945 $('#doc-action').on('change', getDocAction);
e4176358 946 $('#api-params-add').on('click', function(e) {
8ffa1387 947 e.preventDefault();
e4176358 948 addField();
8ffa1387 949 });
c275764e
CW
950 $('#api-option-add').on('click', function(e) {
951 e.preventDefault();
952 addOptionField();
953 });
8ffa1387 954 $('#api-chain-add').on('click', function(e) {
e4176358 955 e.preventDefault();
8ffa1387 956 addChainField();
e4176358 957 });
fee3e3c3 958 populateActions();
0c3a6e64 959 });
e4176358 960}(CRM.$, CRM._));