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
.availableParams
= {};
32 var getMetaParams
= {},
33 objectParams
= {orderBy
: 'ASC', values
: '', chain
: ['Entity', '', '{}']},
36 $scope
.helpTitle
= '';
37 $scope
.helpContent
= {};
38 $scope
.entity
= $routeParams
.api4entity
;
40 $scope
.status
= 'default';
41 $scope
.loading
= false;
44 oop
: ts('PHP (oop style)'),
45 php
: ts('PHP (traditional)'),
47 cli
: ts('Command Line')
49 $scope
.code
= codeDefaults();
51 function codeDefaults() {
52 return _
.mapValues($scope
.codeLabel
, function(val
, key
) {
53 return key
=== 'oop' ? ts('Select an entity and action') : '';
57 if (!entities
.length
) {
58 formatForSelect2(schema
, entities
, 'name', ['description']);
67 function ucfirst(str
) {
68 return str
[0].toUpperCase() + str
.slice(1);
71 function lcfirst(str
) {
72 return str
[0].toLowerCase() + str
.slice(1);
75 function pluralize(str
) {
76 switch (str
[str
.length
-1]) {
80 return str
.slice(0, -1) + 'ies';
86 // Turn a flat array into a select2 array
87 function arrayToSelect2(array
) {
89 _
.each(array
, function(item
) {
90 out
.push({id
: item
, text
: item
});
95 // Reformat an existing array of objects for compatibility with select2
96 function formatForSelect2(input
, container
, key
, extra
, prefix
) {
97 _
.each(input
, function(item
) {
98 var id
= (prefix
|| '') + item
[key
];
99 var formatted
= {id
: id
, text
: id
};
101 _
.merge(formatted
, _
.pick(item
, extra
));
103 container
.push(formatted
);
108 function getFieldList(source
) {
110 fieldInfo
= _
.findWhere(getEntity().actions
, {name
: $scope
.action
}).fields
;
111 formatForSelect2(fieldInfo
, fields
, 'name', ['description', 'required', 'default_value']);
115 function addJoins(fieldList
) {
116 var fields
= _
.cloneDeep(fieldList
),
117 fks
= _
.findWhere(links
, {entity
: $scope
.entity
}) || {};
118 _
.each(fks
.links
, function(link
) {
119 var linkFields
= entityFields(link
.entity
);
123 description
: 'Join to ' + link
.entity
,
124 children
: formatForSelect2(linkFields
, [], 'name', ['description'], link
.alias
+ '.')
131 $scope
.help = function(title
, param
) {
133 $scope
.helpTitle
= helpTitle
;
134 $scope
.helpContent
= helpContent
;
136 $scope
.helpTitle
= title
;
137 $scope
.helpContent
= param
;
141 $scope
.fieldHelp = function(fieldName
) {
142 var field
= getField(fieldName
, $scope
.entity
, $scope
.action
);
147 description
: field
.description
,
148 type
: field
.data_type
150 if (field
.default_value
) {
151 info
.default = field
.default_value
;
153 if (field
.required_if
) {
154 info
.required_if
= field
.required_if
;
155 } else if (field
.required
) {
156 info
.required
= 'true';
161 $scope
.valuesFields = function() {
162 var fields
= _
.cloneDeep($scope
.fields
);
163 // Disable fields that are already in use
164 _
.each($scope
.params
.values
|| [], function(val
) {
165 (_
.findWhere(fields
, {id
: val
[0]}) || {}).disabled
= true;
167 return {results
: fields
};
170 $scope
.formatSelect2Item = function(row
) {
171 return _
.escape(row
.text
) +
172 (row
.required
? '<span class="crm-marker"> *</span>' : '') +
173 (row
.description
? '<div class="crm-select2-row-description"><p>' + _
.escape(row
.description
) + '</p></div>' : '');
176 $scope
.clearParam = function(name
) {
177 $scope
.params
[name
] = $scope
.availableParams
[name
].default;
180 $scope
.isSpecial = function(name
) {
181 var specialParams
= ['select', 'fields', 'action', 'where', 'values', 'orderBy', 'chain'];
182 return _
.contains(specialParams
, name
);
185 $scope
.selectRowCount = function() {
186 if ($scope
.isSelectRowCount()) {
187 $scope
.params
.select
= [];
189 $scope
.params
.select
= ['row_count'];
190 if ($scope
.params
.limit
== 25) {
191 $scope
.params
.limit
= 0;
196 $scope
.isSelectRowCount = function() {
197 return $scope
.params
&& $scope
.params
.select
&& $scope
.params
.select
.length
=== 1 && $scope
.params
.select
[0] === 'row_count';
200 function getEntity(entityName
) {
201 return _
.findWhere(schema
, {name
: entityName
|| $scope
.entity
});
204 // Get all params that have been set
205 function getParams() {
207 _
.each($scope
.params
, function(param
, key
) {
208 if (param
!= $scope
.availableParams
[key
].default && !(typeof param
=== 'object' && _
.isEmpty(param
))) {
209 if (_
.contains($scope
.availableParams
[key
].type
, 'array') && (typeof objectParams
[key
] === 'undefined')) {
210 params
[key
] = parseYaml(_
.cloneDeep(param
));
216 _
.each(objectParams
, function(defaultVal
, key
) {
219 _
.each(params
[key
], function(item
) {
220 var val
= _
.cloneDeep(item
[1]);
221 // Remove blank items from "chain" array
222 if (_
.isArray(val
)) {
223 _
.eachRight(item
[1], function(v
, k
) {
230 newParam
[item
[0]] = parseYaml(val
);
232 params
[key
] = newParam
;
238 function parseYaml(input
) {
239 if (typeof input
=== 'undefined') {
242 if (_
.isObject(input
) || _
.isArray(input
)) {
243 _
.each(input
, function(item
, index
) {
244 input
[index
] = parseYaml(item
);
249 var output
= (input
=== '>') ? '>' : jsyaml
.safeLoad(input
);
250 // We don't want dates parsed to js objects
251 return _
.isDate(output
) ? input
: output
;
257 function selectAction() {
258 $scope
.action
= $routeParams
.api4action
;
259 $scope
.fieldsAndJoins
= [];
260 if (!actions
.length
) {
261 formatForSelect2(getEntity().actions
, actions
, 'name', ['description', 'params']);
264 var actionInfo
= _
.findWhere(actions
, {id
: $scope
.action
});
265 $scope
.fields
= getFieldList();
266 if (_
.contains(['get', 'update', 'delete', 'replace'], $scope
.action
)) {
267 $scope
.fieldsAndJoins
= addJoins($scope
.fields
);
269 $scope
.fieldsAndJoins
= $scope
.fields
;
271 _
.each(actionInfo
.params
, function (param
, name
) {
273 defaultVal
= _
.cloneDeep(param
.default);
275 switch (param
.type
[0]) {
278 format
= param
.type
[0];
289 if (name
== 'limit') {
292 if (name
=== 'values') {
293 defaultVal
= defaultValues(defaultVal
);
295 $scope
.$bindToRoute({
296 expr
: 'params["' + name
+ '"]',
300 deep
: format
=== 'json'
303 if (typeof objectParams
[name
] !== 'undefined') {
304 $scope
.$watch('params.' + name
, function(values
) {
305 // Remove empty values
306 _
.each(values
, function(clause
, index
) {
307 if (!clause
|| !clause
[0]) {
308 $scope
.params
[name
].splice(index
, 1);
312 $scope
.$watch('controls.' + name
, function(value
) {
314 $timeout(function() {
316 var defaultOp
= _
.cloneDeep(objectParams
[name
]);
317 if (name
=== 'chain') {
318 var num
= $scope
.params
.chain
.length
;
319 defaultOp
[0] = field
;
320 field
= 'name_me_' + num
;
322 $scope
.params
[name
].push([field
, defaultOp
]);
323 $scope
.controls
[name
] = null;
329 $scope
.availableParams
= actionInfo
.params
;
334 function defaultValues(defaultVal
) {
335 _
.each($scope
.fields
, function(field
) {
336 if (field
.required
) {
337 defaultVal
.push([field
.id
, '']);
343 function stringify(value
, trim
) {
344 if (typeof value
=== 'undefined') {
347 var str
= JSON
.stringify(value
).replace(/,/g
, ', ');
349 str
= str
.slice(1, -1);
354 function writeCode() {
355 var code
= codeDefaults(),
356 entity
= $scope
.entity
,
357 action
= $scope
.action
,
358 params
= getParams(),
359 index
= isInt($scope
.index
) ? +$scope
.index
: $scope
.index
,
361 if ($scope
.entity
&& $scope
.action
) {
362 if (action
.slice(0, 3) === 'get') {
363 result
= entity
.substr(0, 7) === 'Custom_' ? _
.camelCase(entity
.substr(7)) : entity
;
364 result
= lcfirst(action
.replace(/s$/, '').slice(3) || result
);
366 var results
= lcfirst(_
.isNumber(index
) ? result
: pluralize(result
)),
367 paramCount
= _
.size(params
),
368 isSelectRowCount
= params
.select
&& params
.select
.length
=== 1 && params
.select
[0] === 'row_count',
371 if (isSelectRowCount
) {
372 results
= result
+ 'Count';
376 code
.js
= "CRM.api4('" + entity
+ "', '" + action
+ "', {";
377 _
.each(params
, function(param
, key
) {
378 code
.js
+= "\n " + key
+ ': ' + stringify(param
) +
379 (++i
< paramCount
? ',' : '');
380 if (key
=== 'checkPermissions') {
381 code
.js
+= ' // IGNORED: permissions are always enforced from client-side requests';
385 if (index
|| index
=== 0) {
386 code
.js
+= ', ' + JSON
.stringify(index
);
388 code
.js
+= ").then(function(" + results
+ ") {\n // do something with " + results
+ " array\n}, function(failure) {\n // handle failure\n});";
391 code
.php
= '$' + results
+ " = civicrm_api4('" + entity
+ "', '" + action
+ "', [";
392 _
.each(params
, function(param
, key
) {
393 code
.php
+= "\n '" + key
+ "' => " + phpFormat(param
, 4) + ',';
396 if (index
|| index
=== 0) {
397 code
.php
+= ', ' + phpFormat(index
);
402 if (entity
.substr(0, 7) !== 'Custom_') {
403 code
.oop
= '$' + results
+ " = \\Civi\\Api4\\" + entity
+ '::' + action
+ '()';
405 code
.oop
= '$' + results
+ " = \\Civi\\Api4\\CustomValue::" + action
+ "('" + entity
.substr(7) + "')";
407 _
.each(params
, function(param
, key
) {
409 if (typeof objectParams
[key
] !== 'undefined' && key
!== 'chain') {
410 _
.each(param
, function(item
, index
) {
411 val
= phpFormat(index
) + ', ' + phpFormat(item
, 4);
412 code
.oop
+= "\n ->add" + ucfirst(key
).replace(/s$/, '') + '(' + val
+ ')';
414 } else if (key
=== 'where') {
415 _
.each(param
, function (clause
) {
416 if (clause
[0] === 'AND' || clause
[0] === 'OR' || clause
[0] === 'NOT') {
417 code
.oop
+= "\n ->addClause(" + phpFormat(clause
[0]) + ", " + phpFormat(clause
[1]).slice(1, -1) + ')';
419 code
.oop
+= "\n ->addWhere(" + phpFormat(clause
).slice(1, -1) + ")";
422 } else if (key
=== 'select' && isSelectRowCount
) {
423 code
.oop
+= "\n ->selectRowCount()";
425 code
.oop
+= "\n ->set" + ucfirst(key
) + '(' + phpFormat(param
, 4) + ')';
428 code
.oop
+= "\n ->execute()";
429 if (_
.isNumber(index
)) {
430 code
.oop
+= !index
? '\n ->first()' : (index
=== -1 ? '\n ->last()' : '\n ->itemAt(' + index
+ ')');
432 code
.oop
+= "\n ->indexBy('" + index
+ "')";
433 } else if (isSelectRowCount
) {
434 code
.oop
+= "\n ->count()";
437 if (!_
.isNumber(index
) && !isSelectRowCount
) {
438 code
.oop
+= "foreach ($" + results
+ ' as $' + ((_
.isString(index
) && index
) ? index
+ ' => $' : '') + result
+ ') {\n // do something\n}';
442 code
.cli
= 'cv api4 ' + entity
+ '.' + action
+ " '" + stringify(params
) + "'";
444 _
.each(code
, function(val
, type
) {
445 $scope
.code
[type
] = prettyPrintOne(val
);
449 function isInt(value
) {
450 if (_
.isFinite(value
)) {
453 if (!_
.isString(value
)) {
456 return /^-{0,1}\d+$/.test(value
);
459 function formatMeta(resp
) {
461 _
.each(resp
, function(val
, key
) {
462 if (key
!== 'values' && !_
.isPlainObject(val
) && !_
.isFunction(val
)) {
463 ret
+= (ret
.length
? ', ' : '') + key
+ ': ' + (_
.isArray(val
) ? '[' + val
+ ']' : val
);
466 return prettyPrintOne(ret
);
469 $scope
.execute = function() {
470 $scope
.status
= 'warning';
471 $scope
.loading
= true;
472 $http
.get(CRM
.url('civicrm/ajax/api4/' + $scope
.entity
+ '/' + $scope
.action
, {
473 params
: angular
.toJson(getParams()),
475 })).then(function(resp
) {
476 $scope
.loading
= false;
477 $scope
.status
= 'success';
478 $scope
.result
= [formatMeta(resp
.data
), prettyPrintOne(JSON
.stringify(resp
.data
.values
, null, 2), 'js', 1)];
480 $scope
.loading
= false;
481 $scope
.status
= 'danger';
482 $scope
.result
= [formatMeta(resp
), prettyPrintOne(JSON
.stringify(resp
.data
, null, 2))];
487 * Format value to look like php code
489 function phpFormat(val
, indent
) {
490 if (typeof val
=== 'undefined') {
493 indent
= (typeof indent
=== 'number') ? _
.repeat(' ', indent
) : (indent
|| '');
495 baseLine
= indent
? indent
.slice(0, -2) : '',
496 newLine
= indent
? '\n' : '',
497 trailingComma
= indent
? ',' : '';
498 if ($.isPlainObject(val
)) {
499 $.each(val
, function(k
, v
) {
500 ret
+= (ret
? ', ' : '') + newLine
+ indent
+ "'" + k
+ "' => " + phpFormat(v
);
502 return '[' + ret
+ trailingComma
+ newLine
+ baseLine
+ ']';
504 if ($.isArray(val
)) {
505 $.each(val
, function(k
, v
) {
506 ret
+= (ret
? ', ' : '') + newLine
+ indent
+ phpFormat(v
);
508 return '[' + ret
+ trailingComma
+ newLine
+ baseLine
+ ']';
510 if (_
.isString(val
) && !_
.contains(val
, "'")) {
511 return "'" + val
+ "'";
513 return JSON
.stringify(val
).replace(/\$/g, '\\$');
516 function fetchMeta() {
517 crmApi4(getMetaParams
)
518 .then(function(data
) {
520 getEntity().actions
= data
.actions
;
526 // Help for an entity with no action selected
527 function showEntityHelp(entityName
) {
528 var entityInfo
= getEntity(entityName
);
529 $scope
.helpTitle
= helpTitle
= $scope
.entity
;
530 $scope
.helpContent
= helpContent
= {
531 description
: entityInfo
.description
,
532 comment
: entityInfo
.comment
536 if (!$scope
.entity
) {
537 $scope
.helpTitle
= helpTitle
= ts('Help');
538 $scope
.helpContent
= helpContent
= {description
: ts('Welcome to the api explorer.'), comment
: ts('Select an entity to begin.')};
539 } else if (!actions
.length
&& !getEntity().actions
) {
540 getMetaParams
.actions
= [$scope
.entity
, 'getActions', {chain
: {fields
: [$scope
.entity
, 'getFields', {action
: '$name'}]}}];
547 showEntityHelp($scope
.entity
);
550 // Update route when changing entity
551 $scope
.$watch('entity', function(newVal
, oldVal
) {
552 if (oldVal
!== newVal
) {
553 // Flush actions cache to re-fetch for new entity
555 $location
.url('/explorer/' + newVal
);
559 // Update route when changing actions
560 $scope
.$watch('action', function(newVal
, oldVal
) {
561 if ($scope
.entity
&& $routeParams
.api4action
!== newVal
&& !_
.isUndefined(newVal
)) {
562 $location
.url('/explorer/' + $scope
.entity
+ '/' + newVal
);
564 $scope
.helpTitle
= helpTitle
= $scope
.entity
+ '::' + newVal
;
565 $scope
.helpContent
= helpContent
= _
.pick(_
.findWhere(getEntity().actions
, {name
: newVal
}), ['description', 'comment']);
570 description
: ts('(string|int) Index results or select by index.'),
571 comment
: ts('Pass a string to index the results by a field value. E.g. index: "name" will return an associative array with names as keys.') + '\n\n' +
572 ts('Pass an integer to return a single result; e.g. index: 0 will return the first result, 1 will return the second, and -1 will return the last.')
575 $scope
.$watch('params', writeCode
, true);
576 $scope
.$watch('index', writeCode
);
581 angular
.module('api4Explorer').directive('crmApi4WhereClause', function($timeout
) {
584 data
: '=crmApi4WhereClause'
586 templateUrl
: '~/api4Explorer/WhereClause.html',
587 link: function (scope
, element
, attrs
) {
588 var ts
= scope
.ts
= CRM
.ts();
589 scope
.newClause
= '';
590 scope
.conjunctions
= ['AND', 'OR', 'NOT'];
591 scope
.operators
= CRM
.vars
.api4
.operators
;
593 scope
.addGroup = function(op
) {
594 scope
.data
.where
.push([op
, []]);
597 scope
.removeGroup = function() {
598 scope
.data
.groupParent
.splice(scope
.data
.groupIndex
, 1);
601 scope
.onSort = function(event
, ui
) {
602 $('.api4-where-fieldset').toggleClass('api4-sorting', event
.type
=== 'sortstart');
603 $('.api4-input.form-inline').css('margin-left', '');
606 // Indent clause while dragging between nested groups
607 scope
.onSortOver = function(event
, ui
) {
610 offset
= $(ui
.placeholder
).offset().left
- $(ui
.sender
).offset().left
;
612 $('.api4-input.form-inline.ui-sortable-helper').css('margin-left', '' + offset
+ 'px');
615 scope
.$watch('newClause', function(value
) {
617 $timeout(function() {
619 scope
.data
.where
.push([field
, '=', '']);
620 scope
.newClause
= null;
624 scope
.$watch('data.where', function(values
) {
625 // Remove empty values
626 _
.each(values
, function(clause
, index
) {
627 if (typeof clause
!== 'undefined' && !clause
[0]) {
628 values
.splice(index
, 1);
636 angular
.module('api4Explorer').directive('api4ExpValue', function($routeParams
, crmApi4
) {
639 data
: '=api4ExpValue'
642 link: function (scope
, element
, attrs
, ctrl
) {
643 var ts
= scope
.ts
= CRM
.ts(),
644 multi
= _
.includes(['IN', 'NOT IN', 'BETWEEN', 'NOT BETWEEN'], scope
.data
.op
),
645 entity
= $routeParams
.api4entity
,
646 action
= $routeParams
.api4action
;
648 function destroyWidget() {
649 var $el
= $(element
);
650 if ($el
.is('.crm-form-date-wrapper .crm-hidden-date')) {
651 $el
.crmDatepicker('destroy');
653 if ($el
.is('.select2-container + input')) {
654 $el
.crmEntityRef('destroy');
656 $(element
).removeData().removeAttr('type').removeAttr('placeholder').show();
659 function makeWidget(field
, op
) {
660 var $el
= $(element
),
661 inputType
= field
.input_type
;
662 dataType
= field
.data_type
;
664 op
= field
.serialize
|| dataType
=== 'Array' ? 'IN' : '=';
666 multi
= _
.includes(['IN', 'NOT IN', 'BETWEEN', 'NOT BETWEEN'], op
);
667 if (op
=== 'IS NULL' || op
=== 'IS NOT NULL') {
671 if (inputType
=== 'Date') {
672 if (_
.includes(['=', '!=', '<>', '<', '>=', '<', '<='], op
)) {
673 $el
.crmDatepicker({time
: (field
.input_attrs
&& field
.input_attrs
.time
) || false});
675 } else if (_
.includes(['=', '!=', '<>', 'IN', 'NOT IN'], op
) && (field
.fk_entity
|| field
.options
|| dataType
=== 'Boolean')) {
676 if (field
.fk_entity
) {
677 $el
.crmEntityRef({entity
: field
.fk_entity
, select
:{multiple
: multi
}});
678 } else if (field
.options
) {
679 $el
.addClass('loading').attr('placeholder', ts('- select -')).crmSelect2({multiple
: multi
, data
: [{id
: '', text
: ''}]});
680 loadFieldOptions(field
.entity
|| entity
).then(function(data
) {
682 _
.each(_
.findWhere(data
, {name
: field
.name
}).options
, function(val
, key
) {
683 options
.push({id
: key
, text
: val
});
685 $el
.removeClass('loading').select2({data
: options
, multiple
: multi
});
687 } else if (dataType
=== 'Boolean') {
688 $el
.attr('placeholder', ts('- select -')).crmSelect2({allowClear
: false, multiple
: multi
, placeholder
: ts('- select -'), data
: [
689 {id
: '1', text
: ts('Yes')},
690 {id
: '0', text
: ts('No')}
693 } else if (dataType
=== 'Integer' && !multi
) {
694 $el
.attr('type', 'number');
698 function loadFieldOptions(entity
) {
699 if (!fieldOptions
[entity
+ action
]) {
700 fieldOptions
[entity
+ action
] = crmApi4(entity
, 'getFields', {
703 where
: [["options", "!=", false]],
704 select
: ["name", "options"]
707 return fieldOptions
[entity
+ action
];
710 // Copied from ng-list but applied conditionally if field is multi-valued
711 var parseList = function(viewValue
) {
712 // If the viewValue is invalid (say required but empty) it will be `undefined`
713 if (_
.isUndefined(viewValue
)) return;
722 _
.each(viewValue
.split(','), function(value
) {
723 if (value
) list
.push(_
.trim(value
));
730 // Copied from ng-list
731 ctrl
.$parsers
.push(parseList
);
732 ctrl
.$formatters
.push(function(value
) {
733 return _
.isArray(value
) ? value
.join(', ') : value
;
736 // Copied from ng-list
737 ctrl
.$isEmpty = function(value
) {
738 return !value
|| !value
.length
;
741 scope
.$watchCollection('data', function(data
) {
743 var field
= getField(data
.field
, entity
, action
);
745 makeWidget(field
, data
.op
);
753 angular
.module('api4Explorer').directive('api4ExpChain', function(crmApi4
) {
756 chain
: '=api4ExpChain',
760 templateUrl
: '~/api4Explorer/Chain.html',
761 link: function (scope
, element
, attrs
) {
762 var ts
= scope
.ts
= CRM
.ts();
764 function changeEntity(newEntity
, oldEntity
) {
765 // When clearing entity remove this chain
770 // Reset action && index
771 if (newEntity
!== oldEntity
) {
772 scope
.chain
[1][1] = scope
.chain
[1][2] = '';
774 if (getEntity(newEntity
).actions
) {
777 crmApi4(newEntity
, 'getActions', {chain
: {fields
: [newEntity
, 'getFields', {action
: '$name'}]}})
778 .then(function(data
) {
779 getEntity(data
.entity
).actions
= data
;
780 if (data
.entity
=== scope
.chain
[1][0]) {
787 function setActions() {
788 scope
.actions
= [''].concat(_
.pluck(getEntity(scope
.chain
[1][0]).actions
, 'name'));
791 // Set default params when choosing action
792 function changeAction(newAction
, oldAction
) {
795 if (newAction
&& newAction
!== oldAction
) {
797 scope
.chain
[1][3] = '';
798 // Look for links back to main entity
799 _
.each(entityFields(scope
.chain
[1][0]), function(field
) {
800 if (field
.fk_entity
=== scope
.mainEntity
) {
801 link
= [field
.name
, '$id'];
804 // Look for links from main entity
805 if (!link
&& newAction
!== 'create') {
806 _
.each(entityFields(scope
.mainEntity
), function(field
) {
807 if (field
.fk_entity
=== scope
.chain
[1][0]) {
808 link
= ['id', '$' + field
.name
];
809 // Since we're specifying the id, set index to getsingle
810 scope
.chain
[1][3] = '0';
814 if (link
&& _
.contains(['get', 'update', 'replace', 'delete'], newAction
)) {
815 scope
.chain
[1][2] = '{where: [[' + link
[0] + ', =, ' + link
[1] + ']]}';
817 else if (link
&& _
.contains(['create'], newAction
)) {
818 scope
.chain
[1][2] = '{values: {' + link
[0] + ': ' + link
[1] + '}}';
820 else if (link
&& _
.contains(['save'], newAction
)) {
821 scope
.chain
[1][2] = '{records: [{' + link
[0] + ': ' + link
[1] + '}]}';
823 scope
.chain
[1][2] = '{}';
828 scope
.$watch("chain[1][0]", changeEntity
);
829 scope
.$watch("chain[1][1]", changeAction
);
834 function getEntity(entityName
) {
835 return _
.findWhere(schema
, {name
: entityName
});
838 function entityFields(entityName
, action
) {
839 var entity
= getEntity(entityName
);
840 if (entity
&& action
&& entity
.actions
) {
841 return _
.findWhere(entity
.actions
, {name
: action
}).fields
;
843 return _
.result(entity
, 'fields');
846 function getField(fieldName
, entity
, action
) {
847 var fieldNames
= fieldName
.split('.');
848 return get(entity
, fieldNames
);
850 function get(entity
, fieldNames
) {
851 if (fieldNames
.length
=== 1) {
852 return _
.findWhere(entityFields(entity
, action
), {name
: fieldNames
[0]});
854 var comboName
= _
.findWhere(entityFields(entity
, action
), {name
: fieldNames
[0] + '.' + fieldNames
[1]});
858 var linkName
= fieldNames
.shift(),
859 entityLinks
= _
.findWhere(links
, {entity
: entity
}).links
,
860 newEntity
= _
.findWhere(entityLinks
, {alias
: linkName
}).entity
;
861 return get(newEntity
, fieldNames
);
865 // Collapsible optgroups for select2
868 .on('select2-open', function(e
) {
869 if ($(e
.target
).hasClass('collapsible-optgroups')) {
871 .off('.collapseOptionGroup')
872 .addClass('collapsible-optgroups-enabled')
873 .on('click.collapseOptionGroup', '.select2-result-with-children > .select2-result-label', function() {
874 $(this).parent().toggleClass('optgroup-expanded');
878 .on('select2-close', function() {
879 $('#select2-drop').off('.collapseOptionGroup').removeClass('collapsible-optgroups-enabled');
882 })(angular
, CRM
.$, CRM
._
);