1 (function($, _
, undefined) {
3 /* jshint validthis: true */
8 actions
= {values
: ['get']},
16 fieldTpl
= _
.template($('#api-param-tpl').html()),
17 optionsTpl
= _
.template($('#api-options-tpl').html()),
18 returnTpl
= _
.template($('#api-return-tpl').html()),
19 chainTpl
= _
.template($('#api-chain-tpl').html()),
20 docCodeTpl
= _
.template($('#doc-code-tpl').html()),
21 joinTpl
= _
.template($('#join-tpl').html()),
23 // The following apis do not use Api3SelectQuery so do not support advanced features like joins or OR
24 NO_JOINS
= ['Contact', 'Contribution', 'Pledge', 'Participant'],
26 // These types of entityRef don't require any input to open
27 // FIXME: ought to be in getfields metadata
28 OPEN_IMMEDIATELY
= ['RelationshipType', 'Event', 'Group', 'Tag'],
30 // Actions that don't support fancy operators
31 NO_OPERATORS
= ['create', 'update', 'delete', 'setvalue', 'getoptions', 'getactions', 'getfields'],
33 // Actions that don't support multiple values
34 NO_MULTI
= ['delete', 'getoptions', 'getactions', 'getfields', 'getfield', 'setvalue'],
36 // Operators with special properties
37 BOOL
= ['IS NULL', 'IS NOT NULL'],
38 TEXT
= ['LIKE', 'NOT LIKE'],
39 MULTI
= ['IN', 'NOT IN', 'BETWEEN', 'NOT BETWEEN'];
42 * Call prettyPrint function and perform additional formatting
45 function prettyPrint(ele
) {
46 if (typeof window
.prettyPrint
=== 'function') {
47 $(ele
).removeClass('prettyprinted').addClass('prettyprint');
51 // Highlight errors in api result
52 $('span:contains("error_code"),span:contains("error_message")', '#api-result')
53 .siblings('span.str').css('color', '#B00');
58 * Data provider for select2 "fields to return" selector
59 * @returns {{results: Array.<T>}}
61 function returnFields() {
62 return {results
: fields
.concat({id
: '-', text
: ts('Other') + '...', description
: ts('Choose a field not in this list')})};
66 * Data provider for select2 "field" selectors
67 * @returns {{results: Array.<T>}}
69 function selectFields() {
70 var items
= _
.filter(fields
, function(field
) {
71 return params
[field
.id
] === undefined;
73 return {results
: items
.concat({id
: '-', text
: ts('Other') + '...', description
: ts('Choose a field not in this list')})};
77 * Recursively populates data for select2 "field" selectors
84 function populateFields(fields
, entity
, action
, prefix
, required
) {
85 _
.each(getFieldsCache
[entity
+action
].values
, function(field
) {
86 var name
= prefix
+ field
.name
,
90 text
: field
.title
|| field
.name
,
91 multi
: !!field
['api.multiple'],
92 description
: field
.description
|| '',
93 required
: !(!field
['api.required'] || field
['api.required'] === '0')
95 if (typeof joins
[name
] === 'string') {
96 fields
[pos
].children
= [];
97 populateFields(fields
[pos
].children
, joins
[name
], 'get', name
+ '.');
99 if (!prefix
&& required
&& field
['api.required'] && field
['api.required'] !== '0') {
100 required
.push(field
.name
);
106 * Fetch metadata for a field by name - searches across joins
110 function getField(name
) {
112 if (name
&& getFieldData
[name
]) {
113 field
= _
.cloneDeep(getFieldData
[name
]);
118 _
.each(name
.split('.'), function(piece
) {
124 prefix
+= (prefix
.length
? '.' : '') + piece
;
126 if (getFieldsCache
[ent
+act
].values
[name
]) {
127 field
= _
.cloneDeep(getFieldsCache
[ent
+act
].values
[name
]);
130 addJoinInfo(field
, name
);
134 function addJoinInfo(field
, name
) {
135 if (field
.name
=== 'entity_id') {
136 var entityTableParam
= name
.slice(0, -2) + 'table',
137 entityField
= params
[entityTableParam
] ? getField(entityTableParam
) : {};
138 if (entityField
.options
) {
139 field
.FKApiName
= entityField
.options
[params
[entityTableParam
]];
142 if (field
.pseudoconstant
&& field
.pseudoconstant
.optionGroupName
) {
143 field
.FKApiName
= 'OptionValue';
151 function addField(name
) {
152 $('#api-params').append($(fieldTpl({name
: name
|| '', noOps
: _
.includes(NO_OPERATORS
, action
)})));
153 var $row
= $('tr:last-child', '#api-params');
154 $('input.api-param-name', $row
).crmSelect2({
157 formatSelection: function(field
) {
159 (field
.required
? ' <span class="crm-marker">*</span>' : '');
161 formatResult: function(field
) {
163 (field
.required
? ' <span class="crm-marker">*</span>' : '') +
164 '<div class="api-field-desc">' + field
.description
+ '</div>';
170 * Add a new "options" row
172 function addOptionField() {
173 if ($('.api-options-row', '#api-params').length
) {
174 $('.api-options-row:last', '#api-params').after($(optionsTpl({})));
176 $('#api-params').append($(optionsTpl({})));
178 var $row
= $('.api-options-row:last', '#api-params');
179 $('.api-option-name', $row
).crmSelect2({
181 {id
: 'limit', text
: 'limit'},
182 {id
: 'offset', text
: 'offset'},
183 {id
: 'match', text
: 'match'},
184 {id
: 'match-mandatory', text
: 'match-mandatory'},
185 {id
: 'metadata', text
: 'metadata'},
186 {id
: 'reload', text
: 'reload'},
187 {id
: 'sort', text
: 'sort'},
188 {id
: '-', text
: ts('Other') + '...'}
196 * Add an "api chain" row
198 function addChainField() {
199 $('#api-params').append($(chainTpl({})));
200 var $row
= $('tr:last-child', '#api-params');
201 $('.api-chain-entity', $row
).crmSelect2({
202 formatSelection: function(item
) {
203 return '<i class="crm-i fa-link"></i> API ' +
204 ($(item
.element
).hasClass('strikethrough') ? '<span class="strikethrough">' + item
.text
+ '</span>' : item
.text
);
206 placeholder
: '<i class="crm-i fa-link"></i> ' + ts('Entity'),
208 escapeMarkup: function(m
) {return m
;}
214 * Fetch available actions for selected chained entity
216 function getChainedAction() {
219 entity
= $selector
.val(),
220 $row
= $selector
.closest('tr');
222 $selector
.prop('disabled', true);
224 .then(function(actions
) {
225 $selector
.prop('disabled', false);
226 CRM
.utils
.setOptions($('.api-chain-action', $row
), _
.transform(actions
.values
, function(ret
, item
) {ret
.push({value
: item
, key
: item
});}));
232 * Fetch metadata from the api and cache locally for performance
233 * Returns a deferred object which resolves to entity.getfields
235 function getMetadata(entity
, action
) {
236 var response
= $.Deferred();
237 if (getFieldsCache
[entity
+action
]) {
238 response
.resolve(getFieldsCache
[entity
+action
]);
241 getfields
: [entity
, 'getfields', {
243 options
: {get_options
: 'all', get_options_context
: 'match'}
246 if (!getActionsCache
[entity
]) {
247 apiCalls
.getactions
= [entity
, 'getactions'];
250 .then(function(data
) {
251 data
.getfields
.values
= _
.indexBy(data
.getfields
.values
, 'name');
252 getFieldsCache
[entity
+action
] = data
.getfields
;
253 getActionsCache
[entity
] = getActionsCache
[entity
] || data
.getactions
;
254 response
.resolve(getFieldsCache
[entity
+action
]);
261 * TODO: This works given the current code structure but would cause race conditions if called many times per second
262 * @param entity string
263 * @returns $.Deferred
265 function getActions(entity
) {
266 if (getActionsCache
[entity
]) {
267 return $.Deferred().resolve(getActionsCache
[entity
]);
269 return CRM
.api3(entity
, 'getactions');
274 * Respond to changing the main entity+action
276 function onChangeEntityOrAction(changedElement
) {
281 // Sequential doesn't make sense in getsingle context, and is only a sensible default for get
282 $('label[for=sequential-checkbox]').toggle(action
!== 'getsingle').find('input').prop('checked', action
=== 'get').change();
283 // Special case for getfields
284 if (action
=== 'getfields') {
289 getFieldData
.api_action
= {
291 options
: _
.reduce(actions
.values
, function(ret
, item
) {
296 getFieldsCache
[entity
+action
] = {values
: _
.cloneDeep(getFieldData
)};
297 showFields(['api_action']);
298 renderJoinSelector();
301 getMetadata(entity
, action
).then(function(data
) {
302 if ($(changedElement
).is('#api-entity')) {
303 actions
= getActionsCache
[entity
];
305 if (data
.deprecated
) CRM
.alert(data
.deprecated
, entity
+ ' Deprecated');
307 onChangeAction(action
);
308 getFieldData
= data
.values
;
309 populateFields(fields
, entity
, action
, '', required
);
310 showFields(required
);
311 renderJoinSelector();
312 if (_
.includes(['get', 'getsingle', 'getvalue', 'getstat', 'gettree'], action
)) {
318 function changeFKEntity() {
319 var $row
= $(this).closest('tr'),
320 name
= $('input.api-param-name', $row
).val(),
321 operator
= $('.api-param-op', $row
).val();
322 if (name
&& name
.slice(-12) === 'entity_table') {
323 $('input[value=' + name
.slice(0, -5) + 'id]', '#api-join').prop('checked', false).change();
328 * For "get" actions show the "return" options
330 * TODO: Too many hard-coded actions here. Need a way to fetch this from metadata
332 function showReturn() {
333 var title
= ts('Fields to return'),
337 placeholder
: ts('Leave blank for default'),
338 formatResult: function(field
) {
339 return field
.text
+ '<div class="api-field-desc">' + field
.description
+ '</div>';
342 if (action
== 'getstat') {
343 title
= ts('Group by');
345 if (action
== 'getvalue') {
346 title
= ts('Return Value');
347 params
.placeholder
= ts('Select field');
348 params
.multiple
= false;
350 $('#api-params').prepend($(returnTpl({title
: title
, required
: action
== 'getvalue'})));
351 $('#api-return-value').crmSelect2(params
);
352 $("#api-return-value").select2("container").find("ul.select2-choices").sortable({
353 containment
: 'parent',
354 start: function() { $("#api-return-value").select2("onSortStart"); },
355 update: function() { $("#api-return-value").select2("onSortEnd"); }
360 * Test whether an action is deprecated
364 function isActionDeprecated(action
) {
365 return !!(typeof actions
.deprecated
=== 'object' && actions
.deprecated
[action
]);
369 * Render action text depending on deprecation status
373 function renderAction(option
) {
374 return isActionDeprecated(option
.id
) ? '<span class="strikethrough">' + option
.text
+ '</span>' : option
.text
;
378 * Called after getActions to populate action list
380 function populateActions() {
381 var val
= $('#api-action').val();
382 $('#api-action').removeClass('loading').select2({
383 data
: _
.transform(actions
.values
, function(ret
, item
) {ret
.push({text
: item
, id
: item
});}),
384 formatSelection
: renderAction
,
385 formatResult
: renderAction
387 // If previously selected action is not available, set it to 'get' if possible
388 if (!_
.includes(actions
.values
, val
)) {
389 $('#api-action').select2('val', !_
.includes(actions
.values
, 'get') ? actions
.values
[0] : 'get', true);
394 * Check for and display action-specific deprecation notices
397 function onChangeAction(action
) {
398 if (isActionDeprecated(action
)) {
399 CRM
.alert(actions
.deprecated
[action
], action
+ ' deprecated');
404 * Called after getfields to show buttons and required fields
407 function showFields(required
) {
408 $('#api-params').empty();
409 $('#api-param-buttons').show();
410 if (required
.length
) {
411 _
.each(required
, addField
);
417 function isYesNo(fieldName
) {
418 return getField(fieldName
).type
=== 16;
422 * Should we render a select or textfield?
428 function isSelect(fieldName
, operator
) {
429 var fieldSpec
= getField(fieldName
);
430 return (isYesNo(fieldName
) || fieldSpec
.options
|| fieldSpec
.FKApiName
) && !_
.includes(TEXT
, operator
);
434 * Should we render a select as single or multi?
440 function isMultiSelect(fieldName
, operator
) {
441 if (isYesNo(fieldName
) || _
.includes(NO_MULTI
, action
)) {
444 if (_
.includes(MULTI
, operator
)) {
447 // The = operator is ambiguous but all others can be safely assumed to be single
448 if (operator
!== '=') {
451 return fieldName
!== 'entity_table';
453 * Attempt to resolve the ambiguity of the = operator using metadata
454 * commented out because there is not enough metadata in the api at this time
455 * to accurately figure it out.
457 // var field = fieldName && _.find(fields, 'id', fieldName);
458 // return field && field.multi;
462 * Render value input as a textfield, option list, entityRef, or hidden,
463 * Depending on selected param name and operator
465 function renderValueField() {
466 var $row
= $(this).closest('tr'),
467 name
= $('input.api-param-name', $row
).val(),
468 operator
= $('.api-param-op', $row
).val(),
469 $valField
= $('input.api-param-value', $row
),
470 multiSelect
= isMultiSelect(name
, operator
),
471 currentVal
= $valField
.val(),
472 fieldSpec
= getField(name
),
473 wasSelect
= $valField
.data('select2');
475 $valField
.crmEntityRef('destroy');
477 $valField
.attr('placeholder', ts('Value'));
478 // Boolean fields only have 1 possible value
479 if (_
.includes(BOOL
, operator
)) {
480 $valField
.css('visibility', 'hidden').val('1');
483 $valField
.css('visibility', '');
484 // Option list or entityRef input
485 if (isSelect(name
, operator
)) {
486 $valField
.attr('placeholder', ts('- select -'));
487 // Reset value before switching to a select from something else
488 if ($(this).is('.api-param-name') || !wasSelect
) {
491 // When switching from multi-select to single select
492 else if (!multiSelect
&& _
.includes(currentVal
, ',')) {
493 $valField
.val(currentVal
.split(',')[0]);
498 data
: [{id
: 1, text
: ts('Yes')}, {id
: 0, text
: ts('No')}]
502 else if (fieldSpec
.options
) {
504 multiple
: multiSelect
,
505 data
: _
.map(fieldSpec
.options
, function (value
, key
) {
506 return {id
: key
, text
: value
};
512 var entity
= fieldSpec
.FKApiName
;
513 $valField
.attr('placeholder', entity
== 'Contact' ? '[' + ts('Auto-Select Current User') + ']' : ts('- select -'));
514 $valField
.crmEntityRef({
517 multiple
: multiSelect
,
518 minimumInputLength
: _
.includes(OPEN_IMMEDIATELY
, entity
) ? 0 : 1,
519 // If user types a numeric id, allow it as a choice
520 createSearchChoice: function(input
) {
521 var match
= /[1-9][0-9]*/.exec(input
);
522 if (match
&& match
[0] === input
) {
523 return {id
: input
, label
: input
};
533 * Attempt to parse a string into a value of the intended type
535 * @param makeArray bool
537 function evaluate(val
, makeArray
) {
540 return makeArray
? [] : '';
542 var first
= val
.charAt(0),
543 last
= val
.slice(-1);
545 if (val
=== 'true' || val
=== 'false' || val
=== 'null') {
546 /* jshint evil: true */
550 if ((first
=== '"' || first
=== "'") && last
=== first
) {
551 return val
.slice(1, -1);
553 // Parse json - use eval rather than $.parseJSON because it's less strict about formatting
554 if ((first
=== '[' && last
=== ']') || (first
=== '{' && last
=== '}')) {
555 return eval('(' + val
+ ')');
557 // Transform csv to array
560 $.each(val
.split(','), function(k
, v
) {
561 result
.push(evaluate($.trim(v
)) || v
);
565 // Integers - skip any multidigit number that starts with 0 to avoid oddities (it will be treated as a string below)
566 if (!isNaN(val
) && val
.search(/[^\d]/) < 0 && (val
.length
=== 1 || first
!== '0')) {
567 return parseInt(val
, 10);
569 // Ok ok it's really a string
572 // If eval crashed return undefined
578 * Format value to look like php code
581 function phpFormat(val
) {
583 if ($.isPlainObject(val
)) {
584 $.each(val
, function(k
, v
) {
585 ret
+= (ret
? ', ' : '') + "'" + k
+ "' => " + phpFormat(v
);
587 return '[' + ret
+ ']';
589 if ($.isArray(val
)) {
590 $.each(val
, function(k
, v
) {
591 ret
+= (ret
? ', ' : '') + phpFormat(v
);
593 return '[' + ret
+ ']';
595 return JSON
.stringify(val
).replace(/\$/g, '\\$');
599 * @param value string
603 function smartyFormat(value
, js
, key
) {
604 var varName
= 'param_' + key
.replace(/[. -]/g, '_').toLowerCase();
605 // Can't pass array literals directly into smarty so we add a php snippet
606 if (_
.includes(js
, '[') || _
.includes(js
, '{')) {
607 smartyPhp
.push('$this->assign("'+ varName
+ '", '+ phpFormat(value
) +');');
608 return '$' + varName
;
614 * Create the params array from user input
617 function buildParams(e
) {
619 $('.api-param-checkbox:checked').each(function() {
620 params
[this.name
] = 1;
622 $('input.api-param-value, input.api-option-value').each(function() {
623 var $row
= $(this).closest('tr'),
624 input
= $(this).val(),
625 op
= $('select.api-param-op', $row
).val() || '=',
626 name
= $('input.api-param-name', $row
).val(),
627 // Workaround for ambiguity of the = operator
628 makeArray
= (op
=== '=' && isSelect(name
, op
)) ? _
.includes(input
, ',') : op
!== '=' && isMultiSelect(name
, op
),
629 val
= evaluate(input
, makeArray
);
631 // Ignore blank values for the return field
632 if ($(this).is('#api-return-value') && !val
) {
635 // Special syntax for api chaining
636 if (!name
&& $('select.api-chain-entity', $row
).val()) {
637 name
= 'api.' + $('select.api-chain-entity', $row
).val() + '.' + $('select.api-chain-action', $row
).val();
639 // Special handling for options
640 if ($(this).is('.api-option-value')) {
641 op
= $('input.api-option-name', $row
).val();
646 // Default for contact ref fields
647 if ($(this).is('.crm-contact-ref') && input
=== '') {
648 val
= evaluate('user_contact_id', makeArray
);
650 if (name
&& val
!== undefined) {
651 params
[name
] = op
=== '=' ? val
: (params
[name
] || {});
653 params
[name
][op
] = val
;
655 if ($(this).hasClass('crm-error')) {
659 else if (name
&& (!e
|| e
.type
!== 'keyup')) {
663 if (entity
&& action
) {
670 * Display error message on incorrectly-formatted params
673 function setError(el
) {
674 if (!$(el
).hasClass('crm-error')) {
675 var msg
= ts('Syntax error: input should be valid JSON or a quoted string.');
677 .addClass('crm-error')
680 .before('<i class="crm-i fa-exclamation-triangle crm-i-red" title="'+msg
+'"></i> ')
686 * Remove error message
689 function clearError(el
) {
691 .removeClass('crm-error')
695 .siblings('.fa-exclamation-triangle').remove();
699 * Render the api request in various formats
701 function formatQuery() {
703 smarty
: "{crmAPI var='result' entity='" + entity
+ "' action='" + action
+ "'" + (params
.sequential
? '' : ' sequential=0'),
704 php
: "$result = civicrm_api3('" + entity
+ "', '" + action
+ "'",
705 json
: "CRM.api3('" + entity
+ "', '" + action
+ "'",
706 cv
: "cv api " + entity
+ '.' + action
+ ' ',
707 drush
: "drush cvapi " + entity
+ '.' + action
+ ' ',
708 wpcli
: "wp cv api " + entity
+ '.' + action
+ ' ',
709 rest
: CRM
.config
.resourceBase
+ "extern/rest.php?entity=" + entity
+ "&action=" + action
+ "&api_key=userkey&key=sitekey&json=" + JSON
.stringify(params
)
712 $.each(params
, function(key
, value
) {
713 var json
= JSON
.stringify(value
),
714 // Encourage 'return' to be an array - at least in php & js
715 js
= key
=== 'return' && action
!== 'getvalue' ? JSON
.stringify(evaluate(value
, true)) : json
,
716 php
= key
=== 'return' && action
!== 'getvalue' ? phpFormat(evaluate(value
, true)) : phpFormat(value
);
723 q
.php
+= " '" + key
+ "' => " + php
+ ",\n";
724 q
.json
+= " \"" + key
+ '": ' + js
;
725 // smarty already defaults to sequential
726 if (key
!== 'sequential') {
727 q
.smarty
+= ' ' + key
+ '=' + smartyFormat(value
, json
, key
);
729 // FIXME: This is not totally correct cli syntax
730 q
.cv
+= key
+ '=' + json
+ ' ';
731 q
.drush
+= key
+ '=' + json
+ ' ';
732 q
.wpcli
+= key
+ '=' + json
+ ' ';
739 q
.json
+= ").then(function(result) {\n // do something with result\n}, function(error) {\n // oops\n});";
740 q
.smarty
+= "}\n{foreach from=$result.values item=" + entity
.toLowerCase() + "}\n {$" + entity
.toLowerCase() + ".some_field}\n{/foreach}";
741 if (!_
.includes(action
, 'get')) {
742 q
.smarty
= '{* Smarty API only works with get actions *}';
743 } else if (smartyPhp
.length
) {
744 q
.smarty
= "{php}\n " + smartyPhp
.join("\n ") + "\n{/php}\n" + q
.smarty
;
746 $.each(q
, function(type
, val
) {
747 $('#api-' + type
).text(val
);
749 prettyPrint('#api-generated pre');
753 * Submit button handler
758 if (!entity
|| !action
) {
759 alert(ts('Select an entity.'));
762 if (!_
.includes(action
, 'get') && !_
.includes(action
, 'check')) {
763 var msg
= action
=== 'delete' ? ts('This will delete data from CiviCRM. Are you sure?') : ts('This will write to the database. Continue?');
764 CRM
.confirm({title
: ts('Confirm %1', {1: action
}), message
: msg
}).on('crmConfirm:yes', execute
);
771 * Execute api call and display the results
772 * Note: We have to manually execute the ajax in order to add the secret extra "prettyprint" param
776 $('#api-result').html('<div class="crm-loading-element"></div>');
778 url
: CRM
.url('civicrm/ajax/rest'),
783 json
: JSON
.stringify(params
)
785 type
: _
.includes(action
, 'get') ? 'GET' : 'POST',
787 }).then(function(text
) {
788 // There may be debug information appended to the end of the json string
789 var footerPos
= text
.indexOf("\n}<");
791 footer
= text
.substr(footerPos
+ 2);
792 text
= text
.substr(0, footerPos
+ 2);
794 $('#api-result').text(text
);
795 prettyPrint('#api-result');
797 $('#api-result').append(footer
);
803 * Fetch list of example files for a given entity
805 function getExamples() {
806 CRM
.utils
.setOptions($('#example-action').prop('disabled', true).addClass('loading'), []);
807 $.getJSON(CRM
.url('civicrm/ajax/apiexample', {entity
: $(this).val()}))
808 .then(function(result
) {
809 CRM
.utils
.setOptions($('#example-action').prop('disabled', false).removeClass('loading'), result
);
814 * Fetch and display an example file
816 function getExample() {
818 entity
= $('#example-entity').val(),
819 action
= $('#example-action').val();
820 if (entity
&& action
) {
821 $('#example-result').block();
822 $.get(CRM
.url('civicrm/ajax/apiexample', {file
: entity
+ '/' + action
}))
823 .then(function(result
) {
824 $('#example-result').unblock().text(result
);
825 prettyPrint('#example-result');
828 $('#example-result').text($('#example-result').attr('placeholder'));
833 * Fetch entity docs & actions
835 function getDocEntity() {
836 CRM
.utils
.setOptions($('#doc-action').prop('disabled', true).addClass('loading'), []);
837 $.getJSON(CRM
.url('civicrm/ajax/apidoc', {entity
: $(this).val()}))
838 .then(function(result
) {
839 entityDoc
= result
.doc
;
840 CRM
.utils
.setOptions($('#doc-action').prop('disabled', false).removeClass('loading'), result
.actions
);
841 $('#doc-result').html(result
.doc
);
842 prettyPrint('#doc-result pre');
847 * Fetch entity+action docs & code
849 function getDocAction() {
851 entity
= $('#doc-entity').val(),
852 action
= $('#doc-action').val();
853 if (entity
&& action
) {
854 $('#doc-result').block();
855 $.get(CRM
.url('civicrm/ajax/apidoc', {entity
: entity
, action
: action
}))
856 .then(function(result
) {
857 $('#doc-result').unblock().html(result
.doc
);
859 $('#doc-result').append(docCodeTpl(result
));
861 prettyPrint('#doc-result pre');
864 $('#doc-result').html(entityDoc
);
865 prettyPrint('#doc-result pre');
867 checkBookKeepingEntity(entity
, action
);
871 * Check if entity is Financial Trxn and Entity Financial Trxn
872 * and Action is Create, delete, update etc then display warning
874 function checkBookKeepingEntity(entity
, action
) {
875 if ($.inArray(entity
, ['EntityFinancialTrxn', 'FinancialTrxn']) > -1 && $.inArray(action
, ['delete', 'setvalue', 'replace', 'create']) > -1) {
876 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.');
877 CRM
.alert(msg
, 'warning');
882 * Renders nested checkboxes for adding joins to an api.get call
884 function renderJoinSelector() {
885 $('#api-join').hide();
886 if (!_
.includes(NO_JOINS
, entity
) && _
.includes(['get', 'getsingle', 'getcount'], action
)) {
888 (function recurse(fields
, joinable
, prefix
, depth
, entities
) {
889 _
.each(fields
, function(field
) {
890 var name
= prefix
+ field
.name
;
891 addJoinInfo(field
, name
);
892 var entity
= field
.FKApiName
;
893 if (entity
&& (field
.is_core_field
|| field
.supports_joins
)) {
895 title
: field
.title
+ ' (' + field
.FKApiName
+ ')',
897 checked
: !!joins
[name
]
899 // Expose further joins if we are not over the limit or recursing onto the same entity multiple times
900 if (joins
[name
] && depth
< CRM
.vars
.explorer
.max_joins
&& !_
.countBy(entities
)[entity
]) {
901 joinable
[name
].children
= {};
902 recurse(getFieldsCache
[entity
+'get'].values
, joinable
[name
].children
, name
+ '.', depth
+1, entities
.concat(entity
));
904 } else if (field
.name
== 'entity_id' && fields
.entity_table
&& fields
.entity_table
.options
) {
906 title
: field
.title
+ ' (' + ts('First select %1', {1: fields
.entity_table
.title
}) + ')',
912 })(_
.cloneDeep(getFieldData
), joinable
, '', 1, [entity
]);
913 if (!_
.isEmpty(joinable
)) {
914 // Send joinTpl as a param so it can recursively call itself to render children
915 $('#api-join').show().children('div').html(joinTpl({joins
: joinable
, tpl
: joinTpl
}));
921 * When adding or removing a join from an api.get call
923 function onSelectJoin() {
924 var name
= $(this).val(),
925 ent
= $(this).data('entity');
927 $('input', '#api-join').prop('disabled', true);
928 if ($(this).is(':checked')) {
930 $('input.api-param-name, #api-return-value').addClass('loading');
931 getMetadata(ent
, 'get').then(function() {
932 renderJoinSelector();
933 populateFields(fields
, entity
, action
, '');
934 $('input.api-param-name, #api-return-value').removeClass('loading');
937 joins
= _
.omit(joins
, function(entity
, n
) {
938 return n
.indexOf(name
) === 0;
940 renderJoinSelector();
941 populateFields(fields
, entity
, action
, '');
945 function handleAndOr() {
946 if (!_
.includes(NO_JOINS
, entity
) && _
.includes(['get', 'getsingle', 'getcount'], action
)) {
948 $('tr.api-param-row').each(function() {
949 if ($(this).next().is('tr.api-param-row') && $('input.api-param-name', this).val()) {
950 $('.api-and-or', this).show();
952 $(this).removeClass('or').find('.api-and-or').hide();
955 $('tr.api-param-row.or').each(function() {
956 var val
= $(this).next().find('input.api-param-name').val();
958 if ($(this).prev().is('.or')) {
959 or
[or
.length
- 1].push(val
);
961 or
.push([$('input.api-param-name', this).val(), val
]);
966 params
.options
= params
.options
|| {};
967 params
.options
.or
= or
;
970 $('.api-and-or').hide();
974 function toggleAndOr() {
975 $(this).closest('tr').toggleClass('or');
979 $(document
).ready(function() {
980 // Set up tabs - bind active tab to document hash because... it's cool?
981 document
.location
.hash
= document
.location
.hash
|| 'explorer';
982 $('#mainTabContainer')
984 active
: $(document
.location
.hash
+ '-tab').index() - 1
986 .on('tabsactivate', function(e
, ui
) {
988 document
.location
.hash
= ui
.newPanel
.attr('id').replace('-tab', '');
991 $(window
).on('hashchange', function() {
992 $('#mainTabContainer').tabs('option', 'active', $(document
.location
.hash
+ '-tab').index() - 1);
995 // Initialize widgets
996 $('#api-entity, #example-entity, #doc-entity').crmSelect2({
997 // Add strikethough class to selection to indicate deprecated apis
998 formatSelection: function(option
) {
999 return $(option
.element
).hasClass('strikethrough') ? '<span class="strikethrough">' + option
.text
+ '</span>' : option
.text
;
1002 $('form#api-explorer')
1003 .on('change', '#api-entity, #api-action', function() {
1004 entity
= $('#api-entity').val();
1005 action
= $('#api-action').val();
1007 if ($(this).is('#api-entity')) {
1008 $('#api-action').addClass('loading');
1010 $('#api-params').html('<tr><td colspan="4" class="crm-loading-element"></td></tr>');
1011 $('#api-params-table thead').show();
1012 onChangeEntityOrAction(this);
1014 checkBookKeepingEntity(entity
, action
);
1016 .on('change keyup', 'input.api-input, #api-params select', buildParams
)
1017 .on('change', '.api-param-name, .api-param-value, .api-param-op', changeFKEntity
)
1018 .on('submit', submit
);
1021 .on('change', 'input.api-param-name, select.api-param-op', renderValueField
)
1022 .on('select2-selecting', 'input.api-param-name, .api-option-name, #api-return-value', function(e
) {
1023 if (e
.val
=== '-') {
1024 $(this).one('change', function() {
1026 .crmSelect2('destroy')
1032 .on('click', '.api-param-remove', function(e
) {
1034 $(this).closest('tr').remove();
1037 .on('click', '.api-and-or > span', toggleAndOr
)
1038 .on('change', 'select.api-chain-entity', getChainedAction
)
1039 .on('sortupdate', buildParams
)
1041 handle
: '.api-sort-handle',
1042 items
: '.api-chain-row, .api-param-row'
1044 $('#api-join').on('change', 'input', onSelectJoin
);
1045 $('#example-entity').on('change', getExamples
);
1046 $('#example-action').on('change', getExample
);
1047 $('#doc-entity').on('change', getDocEntity
);
1048 $('#doc-action').on('change', getDocAction
);
1049 $('#api-params-add').on('click', function(e
) {
1052 $('tr:last-child input.api-param-name', '#api-params').select2('open');
1054 $('#api-option-add').on('click', function(e
) {
1058 $('#api-chain-add').on('click', function(e
) {