1 (function(angular
, $, _
) {
4 angular
.module('crmSearchAdmin').component('crmSearchAdmin', {
8 templateUrl
: '~/crmSearchAdmin/crmSearchAdmin.html',
9 controller: function($scope
, $element
, $location
, $timeout
, crmApi4
, dialogService
, searchMeta
, formatForSelect2
) {
10 var ts
= $scope
.ts
= CRM
.ts(),
13 this.DEFAULT_AGGREGATE_FN
= 'GROUP_CONCAT';
15 this.selectedRows
= [];
16 this.limit
= CRM
.cache
.get('searchPageSize', 30);
18 this.displayTypes
= _
.indexBy(CRM
.crmSearchAdmin
.displayTypes
, 'name');
19 // After a search this.results is an object of result arrays keyed by page,
20 // Initially this.results is an empty string because 1: it's falsey (unlike an empty object) and 2: it doesn't throw an error if you try to access undefined properties (unlike null)
22 this.rowCount
= false;
23 this.allRowsSelected
= false;
24 // Have the filters (WHERE, HAVING, GROUP BY, JOIN) changed?
27 $scope
.controls
= {tab
: 'compose'};
28 $scope
.joinTypes
= [{k
: false, v
: ts('Optional')}, {k
: true, v
: ts('Required')}];
29 $scope
.groupOptions
= CRM
.crmSearchActions
.groupOptions
;
30 // Try to create a sensible list of entities one might want to search for,
31 // excluding those whos primary purpose is to provide joins or option lists to other entities
32 var primaryEntities
= _
.filter(CRM
.crmSearchAdmin
.schema
, function(entity
) {
33 return !_
.includes(entity
.type
, 'EntityBridge') && !_
.includes(entity
.type
, 'OptionList');
35 $scope
.entities
= formatForSelect2(primaryEntities
, 'name', 'title_plural', ['description', 'icon']);
37 editGroups
: CRM
.checkPerm('edit groups')
40 this.$onInit = function() {
41 this.entityTitle
= searchMeta
.getEntity(this.savedSearch
.api_entity
).title_plural
;
43 this.savedSearch
.displays
= this.savedSearch
.displays
|| [];
44 this.savedSearch
.groups
= this.savedSearch
.groups
|| [];
45 this.groupExists
= !!this.savedSearch
.groups
.length
;
47 if (!this.savedSearch
.id
) {
50 expr
: '$ctrl.savedSearch.api_params',
54 select
: getDefaultSelect(),
61 $scope
.$watchCollection('$ctrl.savedSearch.api_params.select', onChangeSelect
);
63 $scope
.$watch('$ctrl.savedSearch.api_params.where', onChangeFilters
, true);
65 if (this.paramExists('groupBy')) {
66 this.savedSearch
.api_params
.groupBy
= this.savedSearch
.api_params
.groupBy
|| [];
67 $scope
.$watchCollection('$ctrl.savedSearch.api_params.groupBy', onChangeFilters
);
70 if (this.paramExists('join')) {
71 this.savedSearch
.api_params
.join
= this.savedSearch
.api_params
.join
|| [];
72 $scope
.$watch('$ctrl.savedSearch.api_params.join', onChangeFilters
, true);
75 if (this.paramExists('having')) {
76 this.savedSearch
.api_params
.having
= this.savedSearch
.api_params
.having
|| [];
77 $scope
.$watch('$ctrl.savedSearch.api_params.having', onChangeFilters
, true);
80 $scope
.$watch('$ctrl.savedSearch', onChangeAnything
, true);
82 // After watcher runs for the first time and messes up the status, set it correctly
84 $scope
.status
= ctrl
.savedSearch
&& ctrl
.savedSearch
.id
? 'saved' : 'unsaved';
90 function onChangeAnything() {
91 $scope
.status
= 'unsaved';
94 this.save = function() {
98 $scope
.status
= 'saving';
99 var params
= _
.cloneDeep(ctrl
.savedSearch
),
102 if (ctrl
.groupExists
) {
103 chain
.groups
= ['Group', 'save', {defaults
: {saved_search_id
: '$id'}, records
: params
.groups
}];
104 delete params
.groups
;
105 } else if (params
.id
) {
106 apiCalls
.deleteGroup
= ['Group', 'delete', {where
: [['saved_search_id', '=', params
.id
]]}];
108 if (params
.displays
&& params
.displays
.length
) {
109 chain
.displays
= ['SearchDisplay', 'replace', {where
: [['saved_search_id', '=', '$id']], records
: params
.displays
}];
110 } else if (params
.id
) {
111 apiCalls
.deleteDisplays
= ['SearchDisplay', 'delete', {where
: [['saved_search_id', '=', params
.id
]]}];
113 delete params
.displays
;
114 apiCalls
.saved
= ['SavedSearch', 'save', {records
: [params
], chain
: chain
}, 0];
115 crmApi4(apiCalls
).then(function(results
) {
116 // After saving a new search, redirect to the edit url
117 if (!ctrl
.savedSearch
.id
) {
118 $location
.url('edit/' + results
.saved
.id
);
120 // Set new status to saved unless the user changed something in the interim
121 var newStatus
= $scope
.status
=== 'unsaved' ? 'unsaved' : 'saved';
122 if (results
.saved
.groups
&& results
.saved
.groups
.length
) {
123 ctrl
.savedSearch
.groups
[0].id
= results
.saved
.groups
[0].id
;
125 ctrl
.savedSearch
.displays
= results
.saved
.displays
|| [];
126 // Wait until after onChangeAnything to update status
127 $timeout(function() {
128 $scope
.status
= newStatus
;
133 this.paramExists = function(param
) {
134 return _
.includes(searchMeta
.getEntity(ctrl
.savedSearch
.api_entity
).params
, param
);
137 this.addDisplay = function(type
) {
138 ctrl
.savedSearch
.displays
.push({
142 $scope
.selectTab('display_' + (ctrl
.savedSearch
.displays
.length
- 1));
145 this.removeDisplay = function(index
) {
146 var display
= ctrl
.savedSearch
.displays
[index
];
148 display
.trashed
= !display
.trashed
;
149 if ($scope
.controls
.tab
=== ('display_' + index
) && display
.trashed
) {
150 $scope
.selectTab('compose');
151 } else if (!display
.trashed
) {
152 $scope
.selectTab('display_' + index
);
155 $scope
.selectTab('compose');
156 ctrl
.savedSearch
.displays
.splice(index
, 1);
160 this.addGroup = function() {
161 ctrl
.savedSearch
.groups
.push({
164 visibility
: 'User and User Admin Only',
167 ctrl
.groupExists
= true;
168 $scope
.selectTab('group');
171 $scope
.selectTab = function(tab
) {
172 if (tab
=== 'group') {
173 $scope
.smartGroupColumns
= searchMeta
.getSmartGroupColumns(ctrl
.savedSearch
.api_entity
, ctrl
.savedSearch
.api_params
);
174 var smartGroupColumns
= _
.map($scope
.smartGroupColumns
, 'id');
175 if (smartGroupColumns
.length
&& !_
.includes(smartGroupColumns
, ctrl
.savedSearch
.api_params
.select
[0])) {
176 ctrl
.savedSearch
.api_params
.select
.unshift(smartGroupColumns
[0]);
179 ctrl
.savedSearch
.api_params
.select
= _
.uniq(ctrl
.savedSearch
.api_params
.select
);
180 $scope
.controls
.tab
= tab
;
183 this.removeGroup = function() {
184 ctrl
.groupExists
= !ctrl
.groupExists
;
185 if (!ctrl
.groupExists
&& (!ctrl
.savedSearch
.groups
.length
|| !ctrl
.savedSearch
.groups
[0].id
)) {
186 ctrl
.savedSearch
.groups
.length
= 0;
188 if ($scope
.controls
.tab
=== 'group') {
189 $scope
.selectTab('compose');
193 $scope
.getJoin
= searchMeta
.getJoin
;
195 $scope
.getJoinEntities = function() {
196 var joinEntities
= _
.transform(CRM
.crmSearchAdmin
.joins
[ctrl
.savedSearch
.api_entity
], function(joinEntities
, join
) {
197 var entity
= searchMeta
.getEntity(join
.entity
);
200 id
: join
.entity
+ ' AS ' + join
.alias
,
201 description
: join
.description
,
207 return {results
: joinEntities
};
210 $scope
.addJoin = function() {
211 // Debounce the onchange event using timeout
212 $timeout(function() {
213 if ($scope
.controls
.join
) {
214 ctrl
.savedSearch
.api_params
.join
= ctrl
.savedSearch
.api_params
.join
|| [];
215 var join
= searchMeta
.getJoin($scope
.controls
.join
),
216 params
= [$scope
.controls
.join
, false];
217 _
.each(_
.cloneDeep(join
.conditions
), function(condition
) {
218 params
.push(condition
);
220 ctrl
.savedSearch
.api_params
.join
.push(params
);
223 $scope
.controls
.join
= '';
227 $scope
.changeJoin = function(idx
) {
228 if (ctrl
.savedSearch
.api_params
.join
[idx
][0]) {
229 ctrl
.savedSearch
.api_params
.join
[idx
].length
= 2;
232 ctrl
.clearParam('join', idx
);
236 $scope
.changeGroupBy = function(idx
) {
237 if (!ctrl
.savedSearch
.api_params
.groupBy
[idx
]) {
238 ctrl
.clearParam('groupBy', idx
);
240 // Remove aggregate functions when no grouping
241 if (!ctrl
.savedSearch
.api_params
.groupBy
.length
) {
242 _
.each(ctrl
.savedSearch
.api_params
.select
, function(col
, pos
) {
243 if (_
.contains(col
, '(')) {
244 var info
= searchMeta
.parseExpr(col
);
245 if (info
.fn
.category
=== 'aggregate') {
246 ctrl
.savedSearch
.api_params
.select
[pos
] = info
.path
+ info
.suffix
;
253 function validate() {
258 if (!ctrl
.savedSearch
.label
) {
259 errorEl
= '#crm-saved-search-label';
260 label
= ts('Search Label');
261 errors
.push(ts('%1 is a required field.', {1: label
}));
263 if (ctrl
.groupExists
&& !ctrl
.savedSearch
.groups
[0].title
) {
264 errorEl
= '#crm-search-admin-group-title';
265 label
= ts('Group Title');
266 errors
.push(ts('%1 is a required field.', {1: label
}));
269 _
.each(ctrl
.savedSearch
.displays
, function(display
, index
) {
270 if (!display
.trashed
&& !display
.label
) {
271 errorEl
= '#crm-search-admin-display-label';
272 label
= ts('Display Label');
273 errors
.push(ts('%1 is a required field.', {1: label
}));
274 tab
= 'display_' + index
;
279 $scope
.selectTab(tab
);
281 $(errorEl
).crmError(errors
.join('<br>'), ts('Error Saving'), {expires
: 5000});
283 return !errors
.length
;
287 * Called when clicking on a column header
291 $scope
.setOrderBy = function(col
, $event
) {
292 var dir
= $scope
.getOrderBy(col
) === 'fa-sort-asc' ? 'DESC' : 'ASC';
293 if (!$event
.shiftKey
|| !ctrl
.savedSearch
.api_params
.orderBy
) {
294 ctrl
.savedSearch
.api_params
.orderBy
= {};
296 ctrl
.savedSearch
.api_params
.orderBy
[col
] = dir
;
303 * Returns crm-i icon class for a sortable column
307 $scope
.getOrderBy = function(col
) {
308 var dir
= ctrl
.savedSearch
.api_params
.orderBy
&& ctrl
.savedSearch
.api_params
.orderBy
[col
];
310 return 'fa-sort-' + dir
.toLowerCase();
312 return 'fa-sort disabled';
315 $scope
.addParam = function(name
) {
316 if ($scope
.controls
[name
] && !_
.contains(ctrl
.savedSearch
.api_params
[name
], $scope
.controls
[name
])) {
317 ctrl
.savedSearch
.api_params
[name
].push($scope
.controls
[name
]);
318 if (name
=== 'groupBy') {
319 // Expand the aggregate block
320 $timeout(function() {
321 $('#crm-search-build-group-aggregate.collapsed .collapsible-title').click();
325 $scope
.controls
[name
] = '';
328 // Deletes an item from an array param
329 this.clearParam = function(name
, idx
) {
330 ctrl
.savedSearch
.api_params
[name
].splice(idx
, 1);
333 // Prevent visual jumps in results table height during loading
334 function lockTableHeight() {
335 var $table
= $('.crm-search-results', $element
);
336 $table
.css('height', $table
.height());
339 function unlockTableHeight() {
340 $('.crm-search-results', $element
).css('height', '');
343 // Ensure all non-grouped columns are aggregated if using GROUP BY
344 function aggregateGroupByColumns() {
345 if (ctrl
.savedSearch
.api_params
.groupBy
.length
) {
346 _
.each(ctrl
.savedSearch
.api_params
.select
, function(col
, pos
) {
347 if (!_
.contains(col
, '(') && ctrl
.canAggregate(col
)) {
348 ctrl
.savedSearch
.api_params
.select
[pos
] = ctrl
.DEFAULT_AGGREGATE_FN
+ '(' + col
+ ')';
354 // Debounced callback for loadResults
355 function _loadResultsCallback() {
356 // Multiply limit to read 2 pages at once & save ajax requests
357 var params
= _
.merge(_
.cloneDeep(ctrl
.savedSearch
.api_params
), {debug
: true, limit
: ctrl
.limit
* 2});
358 // Select the ids of joined entities (helps with displaying links)
359 _
.each(params
.join
, function(join
) {
360 var idField
= join
[0].split(' AS ')[1] + '.id';
361 if (!_
.includes(params
.select
, idField
) && !ctrl
.canAggregate(idField
)) {
362 params
.select
.push(idField
);
366 $scope
.error
= false;
369 ctrl
.rowCount
= false;
371 if (ctrl
.rowCount
=== false) {
372 params
.select
.push('row_count');
374 params
.offset
= ctrl
.limit
* (ctrl
.page
- 1);
375 crmApi4(ctrl
.savedSearch
.api_entity
, 'get', params
).then(function(success
) {
379 if (ctrl
.rowCount
=== false) {
380 ctrl
.rowCount
= success
.count
;
382 ctrl
.debug
= success
.debug
;
383 // populate this page & the next
384 ctrl
.results
[ctrl
.page
] = success
.slice(0, ctrl
.limit
);
385 if (success
.length
> ctrl
.limit
) {
386 ctrl
.results
[ctrl
.page
+ 1] = success
.slice(ctrl
.limit
);
388 $scope
.loading
= false;
392 $scope
.loading
= false;
395 ctrl
.debug
= error
.debug
;
396 $scope
.error
= errorMsg(error
);
398 .finally(function() {
400 ctrl
.debug
.params
= JSON
.stringify(params
, null, 2);
401 if (ctrl
.debug
.timeIndex
) {
402 ctrl
.debug
.timeIndex
= Number
.parseFloat(ctrl
.debug
.timeIndex
).toPrecision(2);
408 var _loadResults
= _
.debounce(_loadResultsCallback
, 250);
410 function loadResults() {
411 $scope
.loading
= true;
412 aggregateGroupByColumns();
416 // What to tell the user when search returns an error from the server
417 // Todo: parse error codes and give helpful feedback.
418 function errorMsg(error
) {
419 return ts('Ensure all search critera are set correctly and try again.');
422 this.changePage = function() {
423 if (ctrl
.stale
|| !ctrl
.results
[ctrl
.page
]) {
429 this.refreshAll = function() {
431 ctrl
.selectedRows
.length
= 0;
435 // Refresh results while staying on current page.
436 this.refreshPage = function() {
442 $scope
.onClickSearch = function() {
443 if (ctrl
.autoSearch
) {
444 ctrl
.autoSearch
= false;
450 $scope
.onClickAuto = function() {
451 ctrl
.autoSearch
= !ctrl
.autoSearch
;
452 if (ctrl
.autoSearch
&& ctrl
.stale
) {
455 $('.crm-search-auto-toggle').blur();
458 $scope
.onChangeLimit = function() {
459 // Refresh only if search has already been run
460 if (ctrl
.autoSearch
|| ctrl
.results
) {
461 // Save page size in localStorage
462 CRM
.cache
.set('searchPageSize', ctrl
.limit
);
467 function onChangeSelect(newSelect
, oldSelect
) {
468 // When removing a column from SELECT, also remove from ORDER BY
469 _
.each(_
.difference(_
.keys(ctrl
.savedSearch
.api_params
.orderBy
), newSelect
), function(col
) {
470 delete ctrl
.savedSearch
.api_params
.orderBy
[col
];
472 // Re-arranging or removing columns doesn't merit a refresh, only adding columns does
473 if (!oldSelect
|| _
.difference(newSelect
, oldSelect
).length
) {
474 if (ctrl
.autoSearch
) {
485 function onChangeFilters() {
487 ctrl
.selectedRows
.length
= 0;
491 if (ctrl
.autoSearch
) {
496 $scope
.selectAllRows = function() {
498 if (ctrl
.allRowsSelected
) {
499 ctrl
.allRowsSelected
= false;
500 ctrl
.selectedRows
.length
= 0;
504 ctrl
.allRowsSelected
= true;
505 if (ctrl
.page
=== 1 && ctrl
.results
[1].length
< ctrl
.limit
) {
506 ctrl
.selectedRows
= _
.pluck(ctrl
.results
[1], 'id');
509 // If more than one page of results, use ajax to fetch all ids
510 $scope
.loadingAllRows
= true;
511 var params
= _
.cloneDeep(ctrl
.savedSearch
.api_params
);
512 params
.select
= ['id'];
513 crmApi4(ctrl
.savedSearch
.api_entity
, 'get', params
, ['id']).then(function(ids
) {
514 $scope
.loadingAllRows
= false;
515 ctrl
.selectedRows
= _
.toArray(ids
);
519 $scope
.selectRow = function(row
) {
520 var index
= ctrl
.selectedRows
.indexOf(row
.id
);
522 ctrl
.selectedRows
.push(row
.id
);
523 ctrl
.allRowsSelected
= (ctrl
.rowCount
=== ctrl
.selectedRows
.length
);
525 ctrl
.allRowsSelected
= false;
526 ctrl
.selectedRows
.splice(index
, 1);
530 $scope
.isRowSelected = function(row
) {
531 return ctrl
.allRowsSelected
|| _
.includes(ctrl
.selectedRows
, row
.id
);
534 this.getFieldLabel
= searchMeta
.getDefaultLabel
;
536 // Is a column eligible to use an aggregate function?
537 this.canAggregate = function(col
) {
538 // If the query does not use grouping, never
539 if (!ctrl
.savedSearch
.api_params
.groupBy
.length
) {
542 var info
= searchMeta
.parseExpr(col
);
543 // If the column is used for a groupBy, no
544 if (ctrl
.savedSearch
.api_params
.groupBy
.indexOf(info
.path
) > -1) {
547 // If the entity this column belongs to is being grouped by id, then also no
548 return ctrl
.savedSearch
.api_params
.groupBy
.indexOf(info
.prefix
+ 'id') < 0;
551 $scope
.formatResult = function(row
, col
) {
552 var info
= searchMeta
.parseExpr(col
),
553 key
= info
.fn
? (info
.fn
.name
+ ':' + info
.path
+ info
.suffix
) : col
,
555 if (info
.fn
&& info
.fn
.name
=== 'COUNT') {
558 // Output user-facing name/label fields as a link, if possible
559 if (info
.field
&& _
.includes(['display_name', 'title', 'label', 'subject'], info
.field
.name
) && !info
.fn
&& typeof value
=== 'string') {
560 var link
= getEntityUrl(row
, info
);
562 return '<a href="' + _
.escape(link
.url
) + '" title="' + _
.escape(link
.title
) + '">' + formatFieldValue(info
.field
, value
) + '</a>';
565 return formatFieldValue(info
.field
, value
);
568 // Attempts to construct a view url for a given entity
569 function getEntityUrl(row
, info
) {
570 var entity
= searchMeta
.getEntity(info
.field
.entity
),
571 path
= _
.result(_
.findWhere(entity
.paths
, {action
: 'view'}), 'path');
572 // Only proceed if the path metadata exists for this entity
574 // Replace tokens in the path (e.g. [id])
575 var tokens
= path
.match(/\[\w*]/g) || [],
576 replacements
= _
.transform(tokens
, function(replacements
, token
) {
577 var fieldName
= info
.prefix
+ token
.slice(1, token
.length
- 1);
578 if (row
[fieldName
]) {
579 replacements
.push(row
[fieldName
]);
582 // Only proceed if the row contains all the necessary data to resolve tokens
583 if (tokens
.length
=== replacements
.length
) {
584 _
.each(tokens
, function(token
, index
) {
585 path
= path
.replace(token
, replacements
[index
]);
587 return {url
: CRM
.url(path
), title
: path
.title
};
592 function formatFieldValue(field
, value
) {
593 var type
= field
.data_type
,
595 if (_
.isArray(value
)) {
596 return _
.map(value
, function(val
) {
597 return formatFieldValue(field
, val
);
600 if (value
&& (type
=== 'Date' || type
=== 'Timestamp') && /^\d{4}-\d{2}-\d{2}/.test(value
)) {
601 result
= CRM
.utils
.formatDate(value
, null, type
=== 'Timestamp');
603 else if (type
=== 'Boolean' && typeof value
=== 'boolean') {
604 result
= value
? ts('Yes') : ts('No');
606 else if (type
=== 'Money' && typeof value
=== 'number') {
607 result
= CRM
.formatMoney(value
);
609 return _
.escape(result
);
612 $scope
.fieldsForGroupBy = function() {
613 return {results
: getAllFields('', function(key
) {
614 return _
.contains(ctrl
.savedSearch
.api_params
.groupBy
, key
);
619 $scope
.fieldsForSelect = function() {
620 return {results
: getAllFields(':label', function(key
) {
621 return _
.contains(ctrl
.savedSearch
.api_params
.select
, key
);
626 $scope
.fieldsForWhere = function() {
627 return {results
: getAllFields(':name', _
.noop
)};
630 $scope
.fieldsForHaving = function() {
631 return {results
: _
.transform(ctrl
.savedSearch
.api_params
.select
, function(fields
, name
) {
632 fields
.push({id
: name
, text
: ctrl
.getFieldLabel(name
)});
636 $scope
.sortableColumnOptions
= {
638 handle
: '.crm-draggable',
639 update: function(e
, ui
) {
640 // Don't allow items to be moved to position 0 if locked
641 if (!ui
.item
.sortable
.dropindex
&& ctrl
.groupExists
) {
642 ui
.item
.sortable
.cancel();
647 // Sets the default select clause based on commonly-named fields
648 function getDefaultSelect() {
649 var whitelist
= ['id', 'name', 'subject', 'display_name', 'label', 'title'];
650 return _
.transform(searchMeta
.getEntity(ctrl
.savedSearch
.api_entity
).fields
, function(select
, field
) {
651 if (_
.includes(whitelist
, field
.name
) || _
.includes(field
.name
, '_type_id')) {
652 select
.push(field
.name
+ (field
.options
? ':label' : ''));
657 function getAllFields(suffix
, disabledIf
) {
658 function formatFields(entityName
, join
) {
659 var prefix
= join
? join
.alias
+ '.' : '',
662 function addFields(fields
) {
663 _
.each(fields
, function(field
) {
665 id
: prefix
+ field
.name
+ (field
.options
? suffix
: ''),
667 description
: field
.description
669 if (disabledIf(item
.id
)) {
670 item
.disabled
= true;
676 // Add extra searchable fields from bridge entity
677 if (join
&& join
.bridge
) {
678 addFields(_
.filter(searchMeta
.getEntity(join
.bridge
).fields
, function(field
) {
679 return (field
.name
!== 'id' && field
.name
!== 'entity_id' && field
.name
!== 'entity_table' && !field
.fk_entity
);
683 addFields(searchMeta
.getEntity(entityName
).fields
);
687 var mainEntity
= searchMeta
.getEntity(ctrl
.savedSearch
.api_entity
),
689 text
: mainEntity
.title_plural
,
690 icon
: mainEntity
.icon
,
691 children
: formatFields(ctrl
.savedSearch
.api_entity
)
693 _
.each(ctrl
.savedSearch
.api_params
.join
, function(join
) {
694 var joinInfo
= searchMeta
.getJoin(join
[0]),
695 joinEntity
= searchMeta
.getEntity(joinInfo
.entity
);
697 text
: joinInfo
.label
,
698 description
: joinInfo
.description
,
699 icon
: joinEntity
.icon
,
700 children
: formatFields(joinEntity
.name
, joinInfo
)
707 * Fetch pseudoconstants for main entity + joined entities
709 * Sets an optionsLoaded property on each entity to avoid duplicate requests
711 function loadFieldOptions() {
712 var mainEntity
= searchMeta
.getEntity(ctrl
.savedSearch
.api_entity
),
715 function enqueue(entity
) {
716 entity
.optionsLoaded
= false;
717 entities
[entity
.name
] = [entity
.name
, 'getFields', {
718 loadOptions
: ['id', 'name', 'label', 'description', 'color', 'icon'],
719 where
: [['options', '!=', false]],
721 }, {name
: 'options'}];
724 if (typeof mainEntity
.optionsLoaded
=== 'undefined') {
727 _
.each(ctrl
.savedSearch
.api_params
.join
, function(join
) {
728 var joinInfo
= searchMeta
.getJoin(join
[0]),
729 joinEntity
= searchMeta
.getEntity(joinInfo
.entity
),
730 bridgeEntity
= joinInfo
.bridge
? searchMeta
.getEntity(joinInfo
.bridge
) : null;
731 if (typeof joinEntity
.optionsLoaded
=== 'undefined') {
734 if (bridgeEntity
&& typeof bridgeEntity
.optionsLoaded
=== 'undefined') {
735 enqueue(bridgeEntity
);
738 if (!_
.isEmpty(entities
)) {
739 crmApi4(entities
).then(function(results
) {
740 _
.each(results
, function(fields
, entityName
) {
741 var entity
= searchMeta
.getEntity(entityName
);
742 _
.each(fields
, function(options
, fieldName
) {
743 _
.find(entity
.fields
, {name
: fieldName
}).options
= options
;
745 entity
.optionsLoaded
= true;
754 })(angular
, CRM
.$, CRM
._
);