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