1 (function(angular
, $, _
, undefined) {
4 var schema
= CRM
.vars
.api4
.schema
;
6 var links
= CRM
.vars
.api4
.links
;
7 // Cache list of entities
9 // Cache list of actions
12 var fieldOptions
= {};
15 angular
.module('api4Explorer').config(function($routeProvider
) {
16 $routeProvider
.when('/explorer/:api4entity?/:api4action?', {
17 controller
: 'Api4Explorer',
18 templateUrl
: '~/api4Explorer/Explorer.html',
23 angular
.module('api4Explorer').controller('Api4Explorer', function($scope
, $routeParams
, $location
, $timeout
, $http
, crmUiHelp
, crmApi4
) {
24 var ts
= $scope
.ts
= CRM
.ts();
25 $scope
.entities
= entities
;
26 $scope
.actions
= actions
;
28 $scope
.fieldsAndJoins
= [];
29 $scope
.selectFieldsAndJoins
= [];
30 $scope
.availableParams
= {};
33 $scope
.selectedTab
= {result
: 'result', code
: 'php'};
35 accessDebugOutput
: CRM
.checkPerm('access debug output')
37 marked
.setOptions({highlight
: prettyPrintOne
});
38 var getMetaParams
= {},
39 objectParams
= {orderBy
: 'ASC', values
: '', chain
: ['Entity', '', '{}']},
40 docs
= CRM
.vars
.api4
.docs
,
43 $scope
.helpTitle
= '';
44 $scope
.helpContent
= {};
45 $scope
.entity
= $routeParams
.api4entity
;
48 $scope
.status
= 'default';
49 $scope
.loading
= false;
55 {name
: 'oop', label
: ts('OOP Style'), code
: ''},
56 {name
: 'php', label
: ts('Traditional'), code
: ''}
62 {name
: 'js', label
: ts('Single Call'), code
: ''},
63 {name
: 'js2', label
: ts('Batch Calls'), code
: ''}
69 {name
: 'ang', label
: ts('Single Call'), code
: ''},
70 {name
: 'ang2', label
: ts('Batch Calls'), code
: ''}
76 {name
: 'cv', label
: ts('CV'), code
: ''}
81 if (!entities
.length
) {
82 formatForSelect2(schema
, entities
, 'name', ['description']);
91 function ucfirst(str
) {
92 return str
[0].toUpperCase() + str
.slice(1);
95 function lcfirst(str
) {
96 return str
[0].toLowerCase() + str
.slice(1);
99 function pluralize(str
) {
100 switch (str
[str
.length
-1]) {
104 return str
.slice(0, -1) + 'ies';
110 // Turn a flat array into a select2 array
111 function arrayToSelect2(array
) {
113 _
.each(array
, function(item
) {
114 out
.push({id
: item
, text
: item
});
119 // Reformat an existing array of objects for compatibility with select2
120 function formatForSelect2(input
, container
, key
, extra
, prefix
) {
121 _
.each(input
, function(item
) {
122 var id
= (prefix
|| '') + item
[key
];
123 var formatted
= {id
: id
, text
: id
};
125 _
.merge(formatted
, _
.pick(item
, extra
));
127 container
.push(formatted
);
132 function getFieldList(action
) {
134 fieldInfo
= _
.findWhere(getEntity().actions
, {name
: action
}).fields
;
135 formatForSelect2(fieldInfo
, fields
, 'name', ['description', 'required', 'default_value']);
139 function addJoins(fieldList
, addWildcard
) {
140 var fields
= _
.cloneDeep(fieldList
),
141 fks
= _
.findWhere(links
, {entity
: $scope
.entity
}) || {};
142 _
.each(fks
.links
, function(link
) {
143 var linkFields
= _
.cloneDeep(entityFields(link
.entity
)),
144 wildCard
= addWildcard
? [{id
: link
.alias
+ '.*', text
: link
.alias
+ '.*', 'description': 'All core ' + link
.entity
+ ' fields'}] : [];
148 description
: 'Join to ' + link
.entity
,
149 children
: wildCard
.concat(formatForSelect2(linkFields
, [], 'name', ['description'], link
.alias
+ '.'))
156 $scope
.help = function(title
, content
) {
158 $scope
.helpTitle
= helpTitle
;
159 $scope
.helpContent
= helpContent
;
161 $scope
.helpTitle
= title
;
162 $scope
.helpContent
= convertMarkdown(content
);
166 // Sets the static help text (which gets overridden by mousing over other elements)
167 function setHelp(title
, content
) {
168 $scope
.helpTitle
= helpTitle
= title
;
169 $scope
.helpContent
= helpContent
= convertMarkdown(content
);
172 function convertMarkdown(rawContent
) {
173 var formatted
= _
.cloneDeep(rawContent
);
174 if (formatted
.description
) {
175 formatted
.description
= marked(formatted
.description
);
177 if (formatted
.comment
) {
178 formatted
.comment
= marked(formatted
.comment
);
183 // Format the href for a @see help annotation
184 $scope
.formatRef = function(see
) {
185 var match
= see
.match(/^\\Civi\\Api4\\([a-zA-Z]+)$/);
187 return '#/explorer/' + match
[1];
189 if (see
[0] === '\\') {
190 return 'https://github.com/civicrm/civicrm-core/blob/master' + see
.replace(/\\/i, '/') + '.php
';
195 $scope.fieldHelp = function(fieldName) {
196 var field = getField(fieldName, $scope.entity, $scope.action);
201 description: field.description,
202 type: field.data_type
204 if (field.default_value) {
205 info.default = field.default_value;
207 if (field.required_if) {
208 info.required_if = field.required_if;
209 } else if (field.required) {
210 info.required = 'true';
215 $scope.valuesFields = function() {
216 var fields = _.cloneDeep($scope.action === 'getFields
' ? getFieldList($scope.params.action || 'get') : $scope.fields);
217 // Disable fields that are already in use
218 _.each($scope.params.values || [], function(val) {
219 (_.findWhere(fields, {id: val[0]}) || {}).disabled = true;
221 return {results: fields};
224 $scope.formatSelect2Item = function(row) {
225 return _.escape(row.text) +
226 (row.required ? '<span
class="crm-marker"> *</span
>' : '') +
227 (row.description ? '<div
class="crm-select2-row-description"><p
>' + _.escape(row.description) + '</p></div>' : '');
230 $scope.clearParam = function(name) {
231 $scope.params[name] = $scope.availableParams[name].default;
234 $scope.isSpecial = function(name) {
235 var specialParams = ['select
', 'fields
', 'action
', 'where
', 'values
', 'orderBy
', 'chain
'];
236 return _.contains(specialParams, name);
239 $scope.selectRowCount = function() {
240 if ($scope.isSelectRowCount()) {
241 $scope.params.select = [];
243 $scope.params.select = ['row_count
'];
245 if ($scope.params.limit == 25) {
246 $scope.params.limit = 0;
251 $scope.isSelectRowCount = function() {
252 return $scope.params && $scope.params.select && $scope.params.select.length === 1 && $scope.params.select[0] === 'row_count
';
255 function getEntity(entityName) {
256 return _.findWhere(schema, {name: entityName || $scope.entity});
259 // Get all params that have been set
260 function getParams() {
262 _.each($scope.params, function(param, key) {
263 if (param != $scope.availableParams[key].default && !(typeof param === 'object
' && _.isEmpty(param))) {
264 if (_.contains($scope.availableParams[key].type, 'array
') && (typeof objectParams[key] === 'undefined')) {
265 params[key] = parseYaml(JSON.parse(angular.toJson(param)));
271 _.each(objectParams, function(defaultVal, key) {
274 _.each(params[key], function(item) {
275 var val = _.cloneDeep(item[1]);
276 // Remove blank items from "chain" array
277 if (_.isArray(val)) {
278 _.eachRight(item[1], function(v, k) {
285 newParam[item[0]] = parseYaml(val);
287 params[key] = newParam;
293 function parseYaml(input) {
294 if (typeof input === 'undefined') {
300 if (_.isObject(input) || _.isArray(input)) {
301 _.each(input, function(item, index) {
302 input[index] = parseYaml(item);
307 var output = (input === '>') ? '>' : jsyaml.safeLoad(input);
308 // We don't want dates parsed to js objects
309 return _
.isDate(output
) ? input
: output
;
315 function selectAction() {
316 $scope
.action
= $routeParams
.api4action
;
317 $scope
.fieldsAndJoins
.length
= 0;
318 $scope
.selectFieldsAndJoins
.length
= 0;
319 if (!actions
.length
) {
320 formatForSelect2(getEntity().actions
, actions
, 'name', ['description', 'params']);
323 var actionInfo
= _
.findWhere(actions
, {id
: $scope
.action
});
324 $scope
.fields
= getFieldList($scope
.action
);
325 if (_
.contains(['get', 'update', 'delete', 'replace'], $scope
.action
)) {
326 $scope
.fieldsAndJoins
= addJoins($scope
.fields
);
327 $scope
.selectFieldsAndJoins
= addJoins($scope
.fields
, true);
329 $scope
.fieldsAndJoins
= $scope
.fields
;
330 $scope
.selectFieldsAndJoins
= _
.cloneDeep($scope
.fields
);
332 $scope
.selectFieldsAndJoins
.unshift({id
: '*', text
: '*', 'description': 'All core ' + $scope
.entity
+ ' fields'});
333 _
.each(actionInfo
.params
, function (param
, name
) {
335 defaultVal
= _
.cloneDeep(param
.default);
337 switch (param
.type
[0]) {
340 format
= param
.type
[0];
351 if (name
=== 'limit') {
354 if (name
=== 'debug') {
357 if (name
=== 'values') {
358 defaultVal
= defaultValues(defaultVal
);
360 $scope
.$bindToRoute({
361 expr
: 'params["' + name
+ '"]',
365 deep
: format
=== 'json'
368 if (typeof objectParams
[name
] !== 'undefined') {
369 $scope
.$watch('params.' + name
, function(values
) {
370 // Remove empty values
371 _
.each(values
, function(clause
, index
) {
372 if (!clause
|| !clause
[0]) {
373 $scope
.params
[name
].splice(index
, 1);
377 $scope
.$watch('controls.' + name
, function(value
) {
379 $timeout(function() {
381 var defaultOp
= _
.cloneDeep(objectParams
[name
]);
382 if (name
=== 'chain') {
383 var num
= $scope
.params
.chain
.length
;
384 defaultOp
[0] = field
;
385 field
= 'name_me_' + num
;
387 $scope
.params
[name
].push([field
, defaultOp
]);
388 $scope
.controls
[name
] = null;
394 $scope
.availableParams
= actionInfo
.params
;
399 function defaultValues(defaultVal
) {
400 _
.each($scope
.fields
, function(field
) {
401 if (field
.required
) {
402 defaultVal
.push([field
.id
, '']);
408 function stringify(value
, trim
) {
409 if (typeof value
=== 'undefined') {
412 var str
= JSON
.stringify(value
).replace(/,/g
, ', ');
414 str
= str
.slice(1, -1);
419 function writeCode() {
421 entity
= $scope
.entity
,
422 action
= $scope
.action
,
423 params
= getParams(),
424 index
= isInt($scope
.index
) ? +$scope
.index
: parseYaml($scope
.index
),
426 if ($scope
.entity
&& $scope
.action
) {
428 if (action
.slice(0, 3) === 'get') {
429 result
= entity
.substr(0, 7) === 'Custom_' ? _
.camelCase(entity
.substr(7)) : entity
;
430 result
= lcfirst(action
.replace(/s$/, '').slice(3) || result
);
432 var results
= lcfirst(_
.isNumber(index
) ? result
: pluralize(result
)),
433 paramCount
= _
.size(params
),
434 isSelectRowCount
= params
.select
&& params
.select
.length
=== 1 && params
.select
[0] === 'row_count',
437 if (isSelectRowCount
) {
438 results
= result
+ 'Count';
442 var js
= "'" + entity
+ "', '" + action
+ "', {";
443 _
.each(params
, function(param
, key
) {
444 js
+= "\n " + key
+ ': ' + stringify(param
) +
445 (++i
< paramCount
? ',' : '');
446 if (key
=== 'checkPermissions') {
447 js
+= ' // IGNORED: permissions are always enforced from client-side requests';
451 if (index
|| index
=== 0) {
452 js
+= ', ' + JSON
.stringify(index
);
454 code
.js
= "CRM.api4(" + js
+ ").then(function(" + results
+ ") {\n // do something with " + results
+ " array\n}, function(failure) {\n // handle failure\n});";
455 code
.js2
= "CRM.api4({" + results
+ ': [' + js
+ "]}).then(function(batch) {\n // do something with batch." + results
+ " array\n}, function(failure) {\n // handle failure\n});";
456 code
.ang
= "crmApi4(" + js
+ ").then(function(" + results
+ ") {\n // do something with " + results
+ " array\n}, function(failure) {\n // handle failure\n});";
457 code
.ang2
= "crmApi4({" + results
+ ': [' + js
+ "]}).then(function(batch) {\n // do something with batch." + results
+ " array\n}, function(failure) {\n // handle failure\n});";
460 code
.php
= '$' + results
+ " = civicrm_api4('" + entity
+ "', '" + action
+ "', [";
461 _
.each(params
, function(param
, key
) {
462 code
.php
+= "\n '" + key
+ "' => " + phpFormat(param
, 4) + ',';
465 if (index
|| index
=== 0) {
466 code
.php
+= ', ' + phpFormat(index
);
471 if (entity
.substr(0, 7) !== 'Custom_') {
472 code
.oop
= '$' + results
+ " = \\Civi\\Api4\\" + entity
+ '::' + action
+ '()';
474 code
.oop
= '$' + results
+ " = \\Civi\\Api4\\CustomValue::" + action
+ "('" + entity
.substr(7) + "')";
476 _
.each(params
, function(param
, key
) {
478 if (typeof objectParams
[key
] !== 'undefined' && key
!== 'chain') {
479 _
.each(param
, function(item
, index
) {
480 val
= phpFormat(index
) + ', ' + phpFormat(item
, 4);
481 code
.oop
+= "\n ->add" + ucfirst(key
).replace(/s$/, '') + '(' + val
+ ')';
483 } else if (key
=== 'where') {
484 _
.each(param
, function (clause
) {
485 if (clause
[0] === 'AND' || clause
[0] === 'OR' || clause
[0] === 'NOT') {
486 code
.oop
+= "\n ->addClause(" + phpFormat(clause
[0]) + ", " + phpFormat(clause
[1]).slice(1, -1) + ')';
488 code
.oop
+= "\n ->addWhere(" + phpFormat(clause
).slice(1, -1) + ")";
491 } else if (key
=== 'select' && isSelectRowCount
) {
492 code
.oop
+= "\n ->selectRowCount()";
494 code
.oop
+= "\n ->set" + ucfirst(key
) + '(' + phpFormat(param
, 4) + ')';
497 code
.oop
+= "\n ->execute()";
498 if (isSelectRowCount
) {
499 code
.oop
+= "\n ->count()";
500 } else if (_
.isNumber(index
)) {
501 code
.oop
+= !index
? '\n ->first()' : (index
=== -1 ? '\n ->last()' : '\n ->itemAt(' + index
+ ')');
503 if (_
.isString(index
) || (_
.isPlainObject(index
) && !index
[0] && !index
['0'])) {
504 code
.oop
+= "\n ->indexBy('" + (_
.isPlainObject(index
) ? _
.keys(index
)[0] : index
) + "')";
506 if (_
.isArray(index
) || _
.isPlainObject(index
)) {
507 code
.oop
+= "\n ->column('" + (_
.isArray(index
) ? index
[0] : _
.values(index
)[0]) + "')";
511 if (!_
.isNumber(index
) && !isSelectRowCount
) {
512 code
.oop
+= "foreach ($" + results
+ ' as $' + ((_
.isString(index
) && index
) ? index
+ ' => $' : '') + result
+ ') {\n // do something\n}';
516 code
.cv
= 'cv api4 ' + entity
+ '.' + action
+ " '" + stringify(params
) + "'";
518 _
.each($scope
.code
, function(vals
) {
519 _
.each(vals
.style
, function(style
) {
520 style
.code
= code
[style
.name
] ? prettyPrintOne(code
[style
.name
]) : '';
525 function isInt(value
) {
526 if (_
.isFinite(value
)) {
529 if (!_
.isString(value
)) {
532 return /^-{0,1}\d+$/.test(value
);
535 function formatMeta(resp
) {
537 _
.each(resp
, function(val
, key
) {
538 if (key
!== 'values' && !_
.isPlainObject(val
) && !_
.isFunction(val
)) {
539 ret
+= (ret
.length
? ', ' : '') + key
+ ': ' + (_
.isArray(val
) ? '[' + val
+ ']' : val
);
542 return prettyPrintOne(_
.escape(ret
));
545 $scope
.execute = function() {
546 $scope
.status
= 'warning';
547 $scope
.loading
= true;
548 $http
.post(CRM
.url('civicrm/ajax/api4/' + $scope
.entity
+ '/' + $scope
.action
, {
549 params
: angular
.toJson(getParams()),
550 index
: isInt($scope
.index
) ? +$scope
.index
: parseYaml($scope
.index
)
553 'X-Requested-With': 'XMLHttpRequest'
555 }).then(function(resp
) {
556 $scope
.loading
= false;
557 $scope
.status
= 'success';
558 $scope
.debug
= debugFormat(resp
.data
);
559 $scope
.result
= [formatMeta(resp
.data
), prettyPrintOne(_
.escape(JSON
.stringify(resp
.data
.values
, null, 2)), 'js', 1)];
561 $scope
.loading
= false;
562 $scope
.status
= 'danger';
563 $scope
.debug
= debugFormat(resp
.data
);
564 $scope
.result
= [formatMeta(resp
), prettyPrintOne(_
.escape(JSON
.stringify(resp
.data
, null, 2)))];
568 function debugFormat(data
) {
569 var debug
= data
.debug
? prettyPrintOne(_
.escape(JSON
.stringify(data
.debug
, null, 2)).replace(/\\n/g, "\n")) : null;
575 * Format value to look like php code
577 function phpFormat(val
, indent
) {
578 if (typeof val
=== 'undefined') {
581 if (val
=== null || val
=== true || val
=== false) {
582 return JSON
.stringify(val
).toUpperCase();
584 indent
= (typeof indent
=== 'number') ? _
.repeat(' ', indent
) : (indent
|| '');
586 baseLine
= indent
? indent
.slice(0, -2) : '',
587 newLine
= indent
? '\n' : '',
588 trailingComma
= indent
? ',' : '';
589 if ($.isPlainObject(val
)) {
590 $.each(val
, function(k
, v
) {
591 ret
+= (ret
? ', ' : '') + newLine
+ indent
+ "'" + k
+ "' => " + phpFormat(v
);
593 return '[' + ret
+ trailingComma
+ newLine
+ baseLine
+ ']';
595 if ($.isArray(val
)) {
596 $.each(val
, function(k
, v
) {
597 ret
+= (ret
? ', ' : '') + newLine
+ indent
+ phpFormat(v
);
599 return '[' + ret
+ trailingComma
+ newLine
+ baseLine
+ ']';
601 if (_
.isString(val
) && !_
.contains(val
, "'")) {
602 return "'" + val
+ "'";
604 return JSON
.stringify(val
).replace(/\$/g, '\\$');
607 function fetchMeta() {
608 crmApi4(getMetaParams
)
609 .then(function(data
) {
611 getEntity().actions
= data
.actions
;
617 // Help for an entity with no action selected
618 function showEntityHelp(entityName
) {
619 var entityInfo
= getEntity(entityName
);
620 setHelp($scope
.entity
, {
621 description
: entityInfo
.description
,
622 comment
: entityInfo
.comment
,
627 if (!$scope
.entity
) {
628 setHelp(ts('APIv4 Explorer'), {description
: docs
.description
, comment
: docs
.comment
, see
: docs
.see
});
629 } else if (!actions
.length
&& !getEntity().actions
) {
630 getMetaParams
.actions
= [$scope
.entity
, 'getActions', {chain
: {fields
: [$scope
.entity
, 'getFields', {action
: '$name'}]}}];
637 showEntityHelp($scope
.entity
);
640 // Update route when changing entity
641 $scope
.$watch('entity', function(newVal
, oldVal
) {
642 if (oldVal
!== newVal
) {
643 // Flush actions cache to re-fetch for new entity
645 $location
.url('/explorer/' + newVal
);
649 // Update route when changing actions
650 $scope
.$watch('action', function(newVal
, oldVal
) {
651 if ($scope
.entity
&& $routeParams
.api4action
!== newVal
&& !_
.isUndefined(newVal
)) {
652 $location
.url('/explorer/' + $scope
.entity
+ '/' + newVal
);
654 setHelp($scope
.entity
+ '::' + newVal
, _
.pick(_
.findWhere(getEntity().actions
, {name
: newVal
}), ['description', 'comment', 'see']));
658 $scope
.paramDoc = function(name
) {
659 return docs
.params
[name
];
662 $scope
.$watch('params', writeCode
, true);
663 $scope
.$watch('index', writeCode
);
668 angular
.module('api4Explorer').directive('crmApi4WhereClause', function($timeout
) {
671 data
: '=crmApi4WhereClause'
673 templateUrl
: '~/api4Explorer/WhereClause.html',
674 link: function (scope
, element
, attrs
) {
675 var ts
= scope
.ts
= CRM
.ts();
676 scope
.newClause
= '';
677 scope
.conjunctions
= ['AND', 'OR', 'NOT'];
678 scope
.operators
= CRM
.vars
.api4
.operators
;
680 scope
.addGroup = function(op
) {
681 scope
.data
.where
.push([op
, []]);
684 scope
.removeGroup = function() {
685 scope
.data
.groupParent
.splice(scope
.data
.groupIndex
, 1);
688 scope
.onSort = function(event
, ui
) {
689 $('.api4-where-fieldset').toggleClass('api4-sorting', event
.type
=== 'sortstart');
690 $('.api4-input.form-inline').css('margin-left', '');
693 // Indent clause while dragging between nested groups
694 scope
.onSortOver = function(event
, ui
) {
697 offset
= $(ui
.placeholder
).offset().left
- $(ui
.sender
).offset().left
;
699 $('.api4-input.form-inline.ui-sortable-helper').css('margin-left', '' + offset
+ 'px');
702 scope
.$watch('newClause', function(value
) {
704 $timeout(function() {
706 scope
.data
.where
.push([field
, '=', '']);
707 scope
.newClause
= null;
711 scope
.$watch('data.where', function(values
) {
712 // Remove empty values
713 _
.each(values
, function(clause
, index
) {
714 if (typeof clause
!== 'undefined' && !clause
[0]) {
715 values
.splice(index
, 1);
717 if (typeof clause
[1] === 'string' && _
.contains(clause
[1], 'NULL')) {
719 } else if (typeof clause
[1] === 'string' && clause
.length
== 2) {
728 angular
.module('api4Explorer').directive('api4ExpValue', function($routeParams
, crmApi4
) {
731 data
: '=api4ExpValue'
734 link: function (scope
, element
, attrs
, ctrl
) {
735 var ts
= scope
.ts
= CRM
.ts(),
736 multi
= _
.includes(['IN', 'NOT IN', 'BETWEEN', 'NOT BETWEEN'], scope
.data
.op
),
737 entity
= $routeParams
.api4entity
,
738 action
= scope
.data
.action
|| $routeParams
.api4action
;
740 function destroyWidget() {
741 var $el
= $(element
);
742 if ($el
.is('.crm-form-date-wrapper .crm-hidden-date')) {
743 $el
.crmDatepicker('destroy');
745 if ($el
.is('.select2-container + input')) {
746 $el
.crmEntityRef('destroy');
748 $(element
).removeData().removeAttr('type').removeAttr('placeholder').show();
751 function makeWidget(field
, op
) {
752 var $el
= $(element
),
753 inputType
= field
.input_type
,
754 dataType
= field
.data_type
;
756 op
= field
.serialize
|| dataType
=== 'Array' ? 'IN' : '=';
758 multi
= _
.includes(['IN', 'NOT IN', 'BETWEEN', 'NOT BETWEEN'], op
);
759 if (op
=== 'IS NULL' || op
=== 'IS NOT NULL') {
763 if (inputType
=== 'Date') {
764 if (_
.includes(['=', '!=', '<>', '<', '>=', '<', '<='], op
)) {
765 $el
.crmDatepicker({time
: (field
.input_attrs
&& field
.input_attrs
.time
) || false});
767 } else if (_
.includes(['=', '!=', '<>', 'IN', 'NOT IN'], op
) && (field
.fk_entity
|| field
.options
|| dataType
=== 'Boolean')) {
768 if (field
.fk_entity
) {
769 $el
.crmEntityRef({entity
: field
.fk_entity
, select
:{multiple
: multi
}});
770 } else if (field
.options
) {
771 $el
.addClass('loading').attr('placeholder', ts('- select -')).crmSelect2({multiple
: multi
, data
: [{id
: '', text
: ''}]});
772 loadFieldOptions(field
.entity
|| entity
).then(function(data
) {
774 _
.each(_
.findWhere(data
, {name
: field
.name
}).options
, function(val
, key
) {
775 options
.push({id
: key
, text
: val
});
777 $el
.removeClass('loading').select2({data
: options
, multiple
: multi
});
779 } else if (dataType
=== 'Boolean') {
780 $el
.attr('placeholder', ts('- select -')).crmSelect2({allowClear
: false, multiple
: multi
, placeholder
: ts('- select -'), data
: [
781 {id
: 'true', text
: ts('Yes')},
782 {id
: 'false', text
: ts('No')}
785 } else if (dataType
=== 'Integer' && !multi
) {
786 $el
.attr('type', 'number');
790 function loadFieldOptions(entity
) {
791 if (!fieldOptions
[entity
+ action
]) {
792 fieldOptions
[entity
+ action
] = crmApi4(entity
, 'getFields', {
795 where
: [["options", "!=", false]],
796 select
: ["name", "options"]
799 return fieldOptions
[entity
+ action
];
802 // Copied from ng-list but applied conditionally if field is multi-valued
803 var parseList = function(viewValue
) {
804 // If the viewValue is invalid (say required but empty) it will be `undefined`
805 if (_
.isUndefined(viewValue
)) return;
814 _
.each(viewValue
.split(','), function(value
) {
815 if (value
) list
.push(_
.trim(value
));
822 // Copied from ng-list
823 ctrl
.$parsers
.push(parseList
);
824 ctrl
.$formatters
.push(function(value
) {
825 return _
.isArray(value
) ? value
.join(', ') : value
;
828 // Copied from ng-list
829 ctrl
.$isEmpty = function(value
) {
830 return !value
|| !value
.length
;
833 scope
.$watchCollection('data', function(data
) {
835 var field
= getField(data
.field
, entity
, action
);
837 makeWidget(field
, data
.op
);
845 angular
.module('api4Explorer').directive('api4ExpChain', function(crmApi4
) {
848 chain
: '=api4ExpChain',
852 templateUrl
: '~/api4Explorer/Chain.html',
853 link: function (scope
, element
, attrs
) {
854 var ts
= scope
.ts
= CRM
.ts();
856 function changeEntity(newEntity
, oldEntity
) {
857 // When clearing entity remove this chain
862 // Reset action && index
863 if (newEntity
!== oldEntity
) {
864 scope
.chain
[1][1] = scope
.chain
[1][2] = '';
866 if (getEntity(newEntity
).actions
) {
869 crmApi4(newEntity
, 'getActions', {chain
: {fields
: [newEntity
, 'getFields', {action
: '$name'}]}})
870 .then(function(data
) {
871 getEntity(data
.entity
).actions
= data
;
872 if (data
.entity
=== scope
.chain
[1][0]) {
879 function setActions() {
880 scope
.actions
= [''].concat(_
.pluck(getEntity(scope
.chain
[1][0]).actions
, 'name'));
883 // Set default params when choosing action
884 function changeAction(newAction
, oldAction
) {
887 if (newAction
&& newAction
!== oldAction
) {
889 scope
.chain
[1][3] = '';
890 // Look for links back to main entity
891 _
.each(entityFields(scope
.chain
[1][0]), function(field
) {
892 if (field
.fk_entity
=== scope
.mainEntity
) {
893 link
= [field
.name
, '$id'];
896 // Look for links from main entity
897 if (!link
&& newAction
!== 'create') {
898 _
.each(entityFields(scope
.mainEntity
), function(field
) {
899 if (field
.fk_entity
=== scope
.chain
[1][0]) {
900 link
= ['id', '$' + field
.name
];
901 // Since we're specifying the id, set index to getsingle
902 scope
.chain
[1][3] = '0';
906 if (link
&& _
.contains(['get', 'update', 'replace', 'delete'], newAction
)) {
907 scope
.chain
[1][2] = '{where: [[' + link
[0] + ', =, ' + link
[1] + ']]}';
909 else if (link
&& _
.contains(['create'], newAction
)) {
910 scope
.chain
[1][2] = '{values: {' + link
[0] + ': ' + link
[1] + '}}';
912 else if (link
&& _
.contains(['save'], newAction
)) {
913 scope
.chain
[1][2] = '{records: [{' + link
[0] + ': ' + link
[1] + '}]}';
915 scope
.chain
[1][2] = '{}';
920 scope
.$watch("chain[1][0]", changeEntity
);
921 scope
.$watch("chain[1][1]", changeAction
);
926 function getEntity(entityName
) {
927 return _
.findWhere(schema
, {name
: entityName
});
930 function entityFields(entityName
, action
) {
931 var entity
= getEntity(entityName
);
932 if (entity
&& action
&& entity
.actions
) {
933 return _
.findWhere(entity
.actions
, {name
: action
}).fields
;
935 return _
.result(entity
, 'fields');
938 function getField(fieldName
, entity
, action
) {
939 var fieldNames
= fieldName
.split('.');
940 return get(entity
, fieldNames
);
942 function get(entity
, fieldNames
) {
943 if (fieldNames
.length
=== 1) {
944 return _
.findWhere(entityFields(entity
, action
), {name
: fieldNames
[0]});
946 var comboName
= _
.findWhere(entityFields(entity
, action
), {name
: fieldNames
[0] + '.' + fieldNames
[1]});
950 var linkName
= fieldNames
.shift(),
951 entityLinks
= _
.findWhere(links
, {entity
: entity
}).links
,
952 newEntity
= _
.findWhere(entityLinks
, {alias
: linkName
}).entity
;
953 return get(newEntity
, fieldNames
);
957 // Collapsible optgroups for select2
960 .on('select2-open', function(e
) {
961 if ($(e
.target
).hasClass('collapsible-optgroups')) {
963 .off('.collapseOptionGroup')
964 .addClass('collapsible-optgroups-enabled')
965 .on('click.collapseOptionGroup', '.select2-result-with-children > .select2-result-label', function() {
966 $(this).parent().toggleClass('optgroup-expanded');
970 .on('select2-close', function() {
971 $('#select2-drop').off('.collapseOptionGroup').removeClass('collapsible-optgroups-enabled');
974 })(angular
, CRM
.$, CRM
._
);