109d3d442e1686d8cb51232a78e08187e13a6b33
1 (function($, _
, undefined) {
3 /* jshint validthis: true */
7 actions
= {values
: ['get']},
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()),
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'],
23 // Actions that don't support fancy operators
24 NO_OPERATORS
= ['create', 'update', 'delete', 'setvalue', 'getoptions', 'getactions', 'getfields'],
26 // Actions that don't support multiple values
27 NO_MULTI
= ['delete', 'getoptions', 'getactions', 'getfields', 'getfield', 'setvalue'],
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'];
35 * Call prettyPrint function and perform additional formatting
38 function prettyPrint(ele
) {
39 if (typeof window
.prettyPrint
=== 'function') {
40 $(ele
).removeClass('prettyprinted').addClass('prettyprint');
44 // Highlight errors in api result
45 $('span:contains("error_code"),span:contains("error_message")', '#api-result')
46 .siblings('span.str').css('color', '#B00');
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
) {
61 (field
.required
? ' <span class="crm-marker">*</span>' : '');
63 formatResult: function(field
) {
65 (field
.required
? ' <span class="crm-marker">*</span>' : '') +
66 '<div class="api-field-desc">' + field
.description
+ '</div>';
72 * Add a new "options" row
74 function addOptionField() {
75 if ($('.api-options-row', '#api-params').length
) {
76 $('.api-options-row:last', '#api-params').after($(optionsTpl({})));
78 $('#api-params').append($(optionsTpl({})));
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') + '...'}
91 * Add an "api chain" row
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
);
101 placeholder
: '<span class="icon ui-icon-link"></span> ' + ts('Entity'),
102 escapeMarkup: function(m
) {return m
;}
107 * Fetch available actions for selected chained entity
109 function getChainedAction() {
112 entity
= $selector
.val(),
113 $row
= $selector
.closest('tr');
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
});}));
125 * Fetch fields for entity+action
127 function getFields(changedElement
) {
131 // Special case for getfields
132 if (action
=== 'getfields') {
136 options
: _
.reduce(actions
.values
, function(ret
, item
) {
141 showFields(['api_action']);
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
) {
147 getFieldData
[field
.name
] = field
;
150 text
: field
.title
|| field
.name
,
151 multi
: !!field
['api.multiple'],
152 description
: field
.description
|| '',
153 required
: !(!field
['api.required'] || field
['api.required'] === '0')
155 if (field
['api.required'] && field
['api.required'] !== '0') {
156 required
.push(field
.name
);
160 if ($(changedElement
).is('#api-entity') && data
.deprecated
) {
161 CRM
.alert(data
.deprecated
, entity
+ ' Deprecated');
163 showFields(required
);
164 if (action
=== 'get' || action
=== 'getsingle' || action
== 'getvalue' || action
=== 'getstat') {
171 * For "get" actions show the "return" options
173 * TODO: Too many hard-coded actions here. Need a way to fetch this from metadata
175 function showReturn() {
176 var title
= ts('Fields to return'),
180 placeholder
: ts('Leave blank for default')
182 if (action
== 'getstat') {
183 title
= ts('Group by');
185 if (action
== 'getvalue') {
186 title
= ts('Return Value');
187 params
.placeholder
= ts('Select field');
188 params
.multiple
= false;
190 $('#api-params').prepend($(returnTpl({title
: title
, required
: action
== 'getvalue'})));
191 $('#api-return-value').crmSelect2(params
);
195 * Fetch actions for entity
197 function getActions() {
199 $('#api-action').addClass('loading');
200 CRM
.api3(entity
, 'getactions').done(function(data
) {
205 actions
= {values
: ['get']};
211 * Test whether an action is deprecated
215 function isActionDeprecated(action
) {
216 return !!(typeof actions
.deprecated
=== 'object' && actions
.deprecated
[action
]);
220 * Render action text depending on deprecation status
224 function renderAction(option
) {
225 return isActionDeprecated(option
.id
) ? '<span class="strikethrough">' + option
.text
+ '</span>' : option
.text
;
229 * Called after getActions to populate action list
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
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);
245 * Check for and display action-specific deprecation notices
248 function onChangeAction(action
) {
249 if (isActionDeprecated(action
)) {
250 CRM
.alert(actions
.deprecated
[action
], action
+ ' deprecated');
255 * Called after getfields to show buttons and required fields
258 function showFields(required
) {
259 $('#api-params').empty();
260 $('#api-param-buttons').show();
261 if (required
.length
) {
262 _
.each(required
, addField
);
268 function isYesNo(fieldName
) {
269 return getFieldData
[fieldName
] && getFieldData
[fieldName
].type
=== 16;
273 * Should we render a select or textfield?
279 function isSelect(fieldName
, operator
) {
280 var fieldSpec
= getFieldData
[fieldName
] || {};
281 return (isYesNo(fieldName
) || fieldSpec
.options
|| fieldSpec
.FKApiName
) && !_
.includes(TEXT
, operator
);
285 * Should we render a select as single or multi?
291 function isMultiSelect(fieldName
, operator
) {
292 if (isYesNo(fieldName
) || _
.includes(NO_MULTI
, action
)) {
295 if (_
.includes(MULTI
, operator
)) {
298 // The = operator is ambiguous but all others can be safely assumed to be single
299 if (operator
!== '=') {
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.
308 // var field = fieldName && _.find(fields, 'id', fieldName);
309 // return field && field.multi;
313 * Render value input as a textfield, option list, entityRef, or hidden,
314 * Depending on selected param name and operator
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');
325 $valField
.crmEntityRef('destroy');
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');
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
) {
341 // When switching from multi-select to single select
342 else if (!multiSelect
&& _
.includes(currentVal
, ',')) {
343 $valField
.val(currentVal
.split(',')[0]);
348 data
: [{id
: 1, text
: ts('Yes')}, {id
: 0, text
: ts('No')}]
352 else if (getFieldData
[name
].options
) {
354 multiple
: multiSelect
,
355 data
: _
.map(getFieldData
[name
].options
, function (value
, key
) {
356 return {id
: key
, text
: value
};
362 var entity
= getFieldData
[name
].FKApiName
;
363 $valField
.attr('placeholder', entity
== 'Contact' ? '[' + ts('Auto-Select Current User') + ']' : ts('- select -'));
364 $valField
.crmEntityRef({
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
};
383 * Attempt to parse a string into a value of the intended type
385 * @param makeArray bool
387 function evaluate(val
, makeArray
) {
390 return makeArray
? [] : '';
392 var first
= val
.charAt(0),
393 last
= val
.slice(-1);
395 if (val
=== 'true' || val
=== 'false' || val
=== 'null') {
396 /* jshint evil: true */
400 if ((first
=== '"' || first
=== "'") && last
=== first
) {
401 return val
.slice(1, -1);
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
+ ')');
407 // Transform csv to array
410 $.each(val
.split(','), function(k
, v
) {
411 result
.push(evaluate($.trim(v
)) || v
);
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);
419 // Ok ok it's really a string
422 // If eval crashed return undefined
428 * Format value to look like php code
429 * TODO: Use short array syntax when we drop support for php 5.3
432 function phpFormat(val
) {
434 if ($.isPlainObject(val
)) {
435 $.each(val
, function(k
, v
) {
436 ret
+= (ret
? ', ' : '') + "'" + k
+ "' => " + phpFormat(v
);
438 return 'array(' + ret
+ ')';
440 if ($.isArray(val
)) {
441 $.each(val
, function(k
, v
) {
442 ret
+= (ret
? ', ' : '') + phpFormat(v
);
444 return 'array(' + ret
+ ')';
446 return JSON
.stringify(val
).replace(/\$/g, '\\$');
450 * @param value string
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
;
465 * Create the params array from user input
468 function buildParams(e
) {
470 $('.api-param-checkbox:checked').each(function() {
471 params
[this.name
] = 1;
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
);
482 // Ignore blank values for the return field
483 if ($(this).is('#api-return-value') && !val
) {
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();
490 // Special handling for options
491 if ($(this).is('.api-option-value')) {
492 op
= $('input.api-option-name', $row
).val();
497 // Default for contact ref fields
498 if ($(this).is('.crm-contact-ref') && input
=== '') {
499 val
= evaluate('user_contact_id', makeArray
);
501 if (name
&& val
!== undefined) {
502 params
[name
] = op
=== '=' ? val
: (params
[name
] || {});
504 params
[name
][op
] = val
;
506 if ($(this).hasClass('crm-error')) {
510 else if (name
&& (!e
|| e
.type
!== 'keyup')) {
514 if (entity
&& action
) {
520 * Display error message on incorrectly-formatted params
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.');
527 .addClass('crm-error')
530 .before('<div class="icon red-icon ui-icon-alert" title="'+msg
+'"/>')
536 * Remove error message
539 function clearError(el
) {
541 .removeClass('crm-error')
545 .siblings('.ui-icon-alert').remove();
549 * Render the api request in various formats
551 function formatQuery() {
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
)
561 $.each(params
, function(key
, value
) {
562 var js
= JSON
.stringify(value
);
564 q
.php
+= ", array(\n";
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
);
575 // FIXME: This is not totally correct cli syntax
576 q
.drush
+= key
+ '=' + js
+ ' ';
577 q
.wpcli
+= key
+ '=' + js
+ ' ';
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
;
591 $.each(q
, function(type
, val
) {
592 $('#api-' + type
).text(val
);
594 prettyPrint('#api-generated pre');
598 * Submit button handler
603 if (!entity
|| !action
) {
604 alert(ts('Select an entity.'));
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
);
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
621 $('#api-result').html('<div class="crm-loading-element"></div>');
623 url
: CRM
.url('civicrm/ajax/rest'),
628 json
: JSON
.stringify(params
)
630 type
: _
.includes(action
, 'get') ? 'GET' : 'POST',
632 }).done(function(text
) {
633 // There may be debug information appended to the end of the json string
634 var footerPos
= text
.indexOf("\n}<");
636 footer
= text
.substr(footerPos
+ 2);
637 text
= text
.substr(0, footerPos
+ 2);
639 $('#api-result').text(text
);
640 prettyPrint('#api-result');
642 $('#api-result').append(footer
);
648 * Fetch list of example files for a given entity
650 function getExamples() {
651 CRM
.utils
.setOptions($('#example-action').prop('disabled', true).addClass('loading'), []);
652 $.getJSON(CRM
.url('civicrm/ajax/apiexample', {entity
: $(this).val()}))
653 .done(function(result
) {
654 CRM
.utils
.setOptions($('#example-action').prop('disabled', false).removeClass('loading'), result
);
659 * Fetch and display an example file
661 function getExample() {
663 entity
= $('#example-entity').val(),
664 action
= $('#example-action').val();
665 if (entity
&& action
) {
666 $('#example-result').block();
667 $.get(CRM
.url('civicrm/ajax/apiexample', {file
: entity
+ '/' + action
}))
668 .done(function(result
) {
669 $('#example-result').unblock().text(result
);
670 prettyPrint('#example-result');
673 $('#example-result').text($('#example-result').attr('placeholder'));
678 * Fetch entity docs & actions
680 function getDocEntity() {
681 CRM
.utils
.setOptions($('#doc-action').prop('disabled', true).addClass('loading'), []);
682 $.getJSON(CRM
.url('civicrm/ajax/apidoc', {entity
: $(this).val()}))
683 .done(function(result
) {
684 entityDoc
= result
.doc
;
685 CRM
.utils
.setOptions($('#doc-action').prop('disabled', false).removeClass('loading'), result
.actions
);
686 $('#doc-result').html(result
.doc
);
687 prettyPrint('#doc-result pre');
692 * Fetch entity+action docs & code
694 function getDocAction() {
696 entity
= $('#doc-entity').val(),
697 action
= $('#doc-action').val();
698 if (entity
&& action
) {
699 $('#doc-result').block();
700 $.get(CRM
.url('civicrm/ajax/apidoc', {entity
: entity
, action
: action
}))
701 .done(function(result
) {
702 $('#doc-result').unblock().html(result
.doc
);
704 $('#doc-result').append(docCodeTpl(result
));
706 prettyPrint('#doc-result pre');
709 $('#doc-result').html(entityDoc
);
710 prettyPrint('#doc-result pre');
712 checkBookKeepingEntity(entity
, action
);
716 * Check if entity is Financial Trxn and Entity Financial Trxn
717 * and Action is Create, delete, update etc then display warning
719 function checkBookKeepingEntity(entity
, action
) {
720 if ($.inArray(entity
, ['EntityFinancialTrxn', 'FinancialTrxn']) > -1 && $.inArray(action
, ['delete', 'setvalue', 'replace', 'create']) > -1) {
721 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.');
722 CRM
.alert(msg
, 'warning');
726 $(document
).ready(function() {
727 // Set up tabs - bind active tab to document hash because... it's cool?
728 document
.location
.hash
= document
.location
.hash
|| 'explorer';
729 $('#mainTabContainer')
731 active
: $(document
.location
.hash
+ '-tab').index() - 1
733 .on('tabsactivate', function(e
, ui
) {
735 document
.location
.hash
= ui
.newPanel
.attr('id').replace('-tab', '');
738 $(window
).on('hashchange', function() {
739 $('#mainTabContainer').tabs('option', 'active', $(document
.location
.hash
+ '-tab').index() - 1);
742 // Initialize widgets
743 $('#api-entity, #example-entity, #doc-entity').crmSelect2({
744 // Add strikethough class to selection to indicate deprecated apis
745 formatSelection: function(option
) {
746 return $(option
.element
).hasClass('strikethrough') ? '<span class="strikethrough">' + option
.text
+ '</span>' : option
.text
;
749 $('form#api-explorer')
750 .on('change', '#api-entity, #api-action', function() {
751 entity
= $('#api-entity').val();
752 action
= $('#api-action').val();
753 if ($(this).is('#api-entity')) {
756 onChangeAction(action
);
758 if (entity
&& action
) {
759 $('#api-params').html('<tr><td colspan="4" class="crm-loading-element"></td></tr>');
760 $('#api-params-table thead').show();
763 checkBookKeepingEntity(entity
, action
);
765 $('#api-params, #api-generated pre').empty();
766 $('#api-param-buttons, #api-params-table thead').hide();
769 .on('change keyup', 'input.api-input, #api-params select', buildParams
)
770 .on('submit', submit
);
772 .on('change', 'input.api-param-name, select.api-param-op', renderValueField
)
773 .on('change', 'input.api-param-name, .api-option-name', function() {
774 if ($(this).val() === '-' && $(this).data('select2')) {
776 .crmSelect2('destroy')
781 .on('click', '.api-param-remove', function(e
) {
783 $(this).closest('tr').remove();
786 .on('change', 'select.api-chain-entity', getChainedAction
);
787 $('#example-entity').on('change', getExamples
);
788 $('#example-action').on('change', getExample
);
789 $('#doc-entity').on('change', getDocEntity
);
790 $('#doc-action').on('change', getDocAction
);
791 $('#api-params-add').on('click', function(e
) {
795 $('#api-option-add').on('click', function(e
) {
799 $('#api-chain-add').on('click', function(e
) {
803 $('#api-entity').change();