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