Merge pull request #18772 from eileenmcnaughton/ids
[civicrm-core.git] / ext / search / ang / crmSearchAdmin / crmSearchAdmin.component.js
1 (function(angular, $, _) {
2 "use strict";
3
4 angular.module('crmSearchAdmin').component('crmSearchAdmin', {
5 bindings: {
6 savedSearch: '<'
7 },
8 templateUrl: '~/crmSearchAdmin/crmSearchAdmin.html',
9 controller: function($scope, $element, $location, $timeout, crmApi4, dialogService, searchMeta, formatForSelect2) {
10 var ts = $scope.ts = CRM.ts(),
11 ctrl = this;
12
13 this.DEFAULT_AGGREGATE_FN = 'GROUP_CONCAT';
14
15 this.selectedRows = [];
16 this.limit = CRM.cache.get('searchPageSize', 30);
17 this.page = 1;
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)
21 this.results = '';
22 this.rowCount = false;
23 this.allRowsSelected = false;
24 // Have the filters (WHERE, HAVING, GROUP BY, JOIN) changed?
25 this.stale = true;
26
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 $scope.entities = formatForSelect2(CRM.vars.search.schema, 'name', 'title_plural', ['description', 'icon']);
31 this.perm = {
32 editGroups: CRM.checkPerm('edit groups')
33 };
34
35 this.$onInit = function() {
36 this.entityTitle = searchMeta.getEntity(this.savedSearch.api_entity).title_plural;
37
38 this.savedSearch.displays = this.savedSearch.displays || [];
39 this.savedSearch.groups = this.savedSearch.groups || [];
40 this.groupExists = !!this.savedSearch.groups.length;
41
42 if (!this.savedSearch.id) {
43 $scope.$bindToRoute({
44 param: 'params',
45 expr: '$ctrl.savedSearch.api_params',
46 default: {
47 version: 4,
48 select: getDefaultSelect(),
49 orderBy: {},
50 where: [],
51 }
52 });
53 }
54
55 $scope.$watchCollection('$ctrl.savedSearch.api_params.select', onChangeSelect);
56
57 $scope.$watch('$ctrl.savedSearch.api_params.where', onChangeFilters, true);
58
59 if (this.paramExists('groupBy')) {
60 this.savedSearch.api_params.groupBy = this.savedSearch.api_params.groupBy || [];
61 $scope.$watchCollection('$ctrl.savedSearch.api_params.groupBy', onChangeFilters);
62 }
63
64 if (this.paramExists('join')) {
65 this.savedSearch.api_params.join = this.savedSearch.api_params.join || [];
66 $scope.$watch('$ctrl.savedSearch.api_params.join', onChangeFilters, true);
67 }
68
69 if (this.paramExists('having')) {
70 this.savedSearch.api_params.having = this.savedSearch.api_params.having || [];
71 $scope.$watch('$ctrl.savedSearch.api_params.having', onChangeFilters, true);
72 }
73
74 $scope.$watch('$ctrl.savedSearch', onChangeAnything, true);
75
76 // After watcher runs for the first time and messes up the status, set it correctly
77 $timeout(function() {
78 $scope.status = ctrl.savedSearch && ctrl.savedSearch.id ? 'saved' : 'unsaved';
79 });
80
81 loadFieldOptions();
82 };
83
84 function onChangeAnything() {
85 $scope.status = 'unsaved';
86 }
87
88 this.save = function() {
89 if (!validate()) {
90 return;
91 }
92 $scope.status = 'saving';
93 var params = _.cloneDeep(ctrl.savedSearch),
94 apiCalls = {},
95 chain = {};
96 if (ctrl.groupExists) {
97 chain.groups = ['Group', 'save', {defaults: {saved_search_id: '$id'}, records: params.groups}];
98 delete params.groups;
99 } else if (params.id) {
100 apiCalls.deleteGroup = ['Group', 'delete', {where: [['saved_search_id', '=', params.id]]}];
101 }
102 if (params.displays && params.displays.length) {
103 chain.displays = ['SearchDisplay', 'replace', {where: [['saved_search_id', '=', '$id']], records: params.displays}];
104 } else if (params.id) {
105 apiCalls.deleteDisplays = ['SearchDisplay', 'delete', {where: [['saved_search_id', '=', params.id]]}];
106 }
107 delete params.displays;
108 apiCalls.saved = ['SavedSearch', 'save', {records: [params], chain: chain}, 0];
109 crmApi4(apiCalls).then(function(results) {
110 // After saving a new search, redirect to the edit url
111 if (!ctrl.savedSearch.id) {
112 $location.url('edit/' + results.saved.id);
113 }
114 // Set new status to saved unless the user changed something in the interim
115 var newStatus = $scope.status === 'unsaved' ? 'unsaved' : 'saved';
116 if (results.saved.groups && results.saved.groups.length) {
117 ctrl.savedSearch.groups[0].id = results.saved.groups[0].id;
118 }
119 ctrl.savedSearch.displays = results.saved.displays || [];
120 // Wait until after onChangeAnything to update status
121 $timeout(function() {
122 $scope.status = newStatus;
123 });
124 });
125 };
126
127 this.paramExists = function(param) {
128 return _.includes(searchMeta.getEntity(ctrl.savedSearch.api_entity).params, param);
129 };
130
131 this.addDisplay = function(type) {
132 ctrl.savedSearch.displays.push({
133 type: type,
134 label: ''
135 });
136 $scope.selectTab('display_' + (ctrl.savedSearch.displays.length - 1));
137 };
138
139 this.removeDisplay = function(index) {
140 var display = ctrl.savedSearch.displays[index];
141 if (display.id) {
142 display.trashed = !display.trashed;
143 if ($scope.controls.tab === ('display_' + index) && display.trashed) {
144 $scope.selectTab('compose');
145 } else if (!display.trashed) {
146 $scope.selectTab('display_' + index);
147 }
148 } else {
149 $scope.selectTab('compose');
150 ctrl.savedSearch.displays.splice(index, 1);
151 }
152 };
153
154 this.addGroup = function() {
155 ctrl.savedSearch.groups.push({
156 title: '',
157 description: '',
158 visibility: 'User and User Admin Only',
159 group_type: []
160 });
161 ctrl.groupExists = true;
162 $scope.selectTab('group');
163 };
164
165 $scope.selectTab = function(tab) {
166 if (tab === 'group') {
167 $scope.smartGroupColumns = searchMeta.getSmartGroupColumns(ctrl.savedSearch.api_entity, ctrl.savedSearch.api_params);
168 var smartGroupColumns = _.map($scope.smartGroupColumns, 'id');
169 if (smartGroupColumns.length && !_.includes(smartGroupColumns, ctrl.savedSearch.api_params.select[0])) {
170 ctrl.savedSearch.api_params.select.unshift(smartGroupColumns[0]);
171 }
172 }
173 ctrl.savedSearch.api_params.select = _.uniq(ctrl.savedSearch.api_params.select);
174 $scope.controls.tab = tab;
175 };
176
177 this.removeGroup = function() {
178 ctrl.groupExists = !ctrl.groupExists;
179 if (!ctrl.groupExists && (!ctrl.savedSearch.groups.length || !ctrl.savedSearch.groups[0].id)) {
180 ctrl.savedSearch.groups.length = 0;
181 }
182 if ($scope.controls.tab === 'group') {
183 $scope.selectTab('compose');
184 }
185 };
186
187 $scope.getJoinEntities = function() {
188 var joinEntities = _.transform(CRM.vars.search.links[ctrl.savedSearch.api_entity], function(joinEntities, link) {
189 var entity = searchMeta.getEntity(link.entity);
190 if (entity) {
191 joinEntities.push({
192 id: link.entity + ' AS ' + link.alias,
193 text: entity.title_plural,
194 description: '(' + link.alias + ')',
195 icon: entity.icon
196 });
197 }
198 }, []);
199 return {results: joinEntities};
200 };
201
202 $scope.addJoin = function() {
203 // Debounce the onchange event using timeout
204 $timeout(function() {
205 if ($scope.controls.join) {
206 ctrl.savedSearch.api_params.join = ctrl.savedSearch.api_params.join || [];
207 ctrl.savedSearch.api_params.join.push([$scope.controls.join, false]);
208 loadFieldOptions();
209 }
210 $scope.controls.join = '';
211 });
212 };
213
214 $scope.changeJoin = function(idx) {
215 if (ctrl.savedSearch.api_params.join[idx][0]) {
216 ctrl.savedSearch.api_params.join[idx].length = 2;
217 loadFieldOptions();
218 } else {
219 ctrl.clearParam('join', idx);
220 }
221 };
222
223 $scope.changeGroupBy = function(idx) {
224 if (!ctrl.savedSearch.api_params.groupBy[idx]) {
225 ctrl.clearParam('groupBy', idx);
226 }
227 // Remove aggregate functions when no grouping
228 if (!ctrl.savedSearch.api_params.groupBy.length) {
229 _.each(ctrl.savedSearch.api_params.select, function(col, pos) {
230 if (_.contains(col, '(')) {
231 var info = searchMeta.parseExpr(col);
232 if (info.fn.category === 'aggregate') {
233 ctrl.savedSearch.api_params.select[pos] = info.path + info.suffix;
234 }
235 }
236 });
237 }
238 };
239
240 function validate() {
241 var errors = [],
242 errorEl,
243 label,
244 tab;
245 if (!ctrl.savedSearch.label) {
246 errorEl = '#crm-saved-search-label';
247 label = ts('Search Label');
248 errors.push(ts('%1 is a required field.', {1: label}));
249 }
250 if (ctrl.groupExists && !ctrl.savedSearch.groups[0].title) {
251 errorEl = '#crm-search-admin-group-title';
252 label = ts('Group Title');
253 errors.push(ts('%1 is a required field.', {1: label}));
254 tab = 'group';
255 }
256 _.each(ctrl.savedSearch.displays, function(display, index) {
257 if (!display.trashed && !display.label) {
258 errorEl = '#crm-search-admin-display-label';
259 label = ts('Display Label');
260 errors.push(ts('%1 is a required field.', {1: label}));
261 tab = 'display_' + index;
262 }
263 });
264 if (errors.length) {
265 if (tab) {
266 $scope.selectTab(tab);
267 }
268 $(errorEl).crmError(errors.join('<br>'), ts('Error Saving'), {expires: 5000});
269 }
270 return !errors.length;
271 }
272
273 /**
274 * Called when clicking on a column header
275 * @param col
276 * @param $event
277 */
278 $scope.setOrderBy = function(col, $event) {
279 var dir = $scope.getOrderBy(col) === 'fa-sort-asc' ? 'DESC' : 'ASC';
280 if (!$event.shiftKey) {
281 ctrl.savedSearch.api_params.orderBy = {};
282 }
283 ctrl.savedSearch.api_params.orderBy[col] = dir;
284 if (ctrl.results) {
285 ctrl.refreshPage();
286 }
287 };
288
289 /**
290 * Returns crm-i icon class for a sortable column
291 * @param col
292 * @returns {string}
293 */
294 $scope.getOrderBy = function(col) {
295 var dir = ctrl.savedSearch.api_params.orderBy && ctrl.savedSearch.api_params.orderBy[col];
296 if (dir) {
297 return 'fa-sort-' + dir.toLowerCase();
298 }
299 return 'fa-sort disabled';
300 };
301
302 $scope.addParam = function(name) {
303 if ($scope.controls[name] && !_.contains(ctrl.savedSearch.api_params[name], $scope.controls[name])) {
304 ctrl.savedSearch.api_params[name].push($scope.controls[name]);
305 if (name === 'groupBy') {
306 // Expand the aggregate block
307 $timeout(function() {
308 $('#crm-search-build-group-aggregate.collapsed .collapsible-title').click();
309 }, 10);
310 }
311 }
312 $scope.controls[name] = '';
313 };
314
315 // Deletes an item from an array param
316 this.clearParam = function(name, idx) {
317 ctrl.savedSearch.api_params[name].splice(idx, 1);
318 };
319
320 // Prevent visual jumps in results table height during loading
321 function lockTableHeight() {
322 var $table = $('.crm-search-results', $element);
323 $table.css('height', $table.height());
324 }
325
326 function unlockTableHeight() {
327 $('.crm-search-results', $element).css('height', '');
328 }
329
330 // Ensure all non-grouped columns are aggregated if using GROUP BY
331 function aggregateGroupByColumns() {
332 if (ctrl.savedSearch.api_params.groupBy.length) {
333 _.each(ctrl.savedSearch.api_params.select, function(col, pos) {
334 if (!_.contains(col, '(') && ctrl.canAggregate(col)) {
335 ctrl.savedSearch.api_params.select[pos] = ctrl.DEFAULT_AGGREGATE_FN + '(' + col + ')';
336 }
337 });
338 }
339 }
340
341 // Debounced callback for loadResults
342 function _loadResultsCallback() {
343 // Multiply limit to read 2 pages at once & save ajax requests
344 var params = _.merge(_.cloneDeep(ctrl.savedSearch.api_params), {debug: true, limit: ctrl.limit * 2});
345 // Select the ids of joined entities (helps with displaying links)
346 _.each(params.join, function(join) {
347 var idField = join[0].split(' AS ')[1] + '.id';
348 if (!_.includes(params.select, idField) && !ctrl.canAggregate(idField)) {
349 params.select.push(idField);
350 }
351 });
352 lockTableHeight();
353 $scope.error = false;
354 if (ctrl.stale) {
355 ctrl.page = 1;
356 ctrl.rowCount = false;
357 }
358 if (ctrl.rowCount === false) {
359 params.select.push('row_count');
360 }
361 params.offset = ctrl.limit * (ctrl.page - 1);
362 crmApi4(ctrl.savedSearch.api_entity, 'get', params).then(function(success) {
363 if (ctrl.stale) {
364 ctrl.results = {};
365 }
366 if (ctrl.rowCount === false) {
367 ctrl.rowCount = success.count;
368 }
369 ctrl.debug = success.debug;
370 // populate this page & the next
371 ctrl.results[ctrl.page] = success.slice(0, ctrl.limit);
372 if (success.length > ctrl.limit) {
373 ctrl.results[ctrl.page + 1] = success.slice(ctrl.limit);
374 }
375 $scope.loading = false;
376 ctrl.stale = false;
377 unlockTableHeight();
378 }, function(error) {
379 $scope.loading = false;
380 ctrl.results = {};
381 ctrl.stale = true;
382 ctrl.debug = error.debug;
383 $scope.error = errorMsg(error);
384 })
385 .finally(function() {
386 if (ctrl.debug) {
387 ctrl.debug.params = JSON.stringify(params, null, 2);
388 if (ctrl.debug.timeIndex) {
389 ctrl.debug.timeIndex = Number.parseFloat(ctrl.debug.timeIndex).toPrecision(2);
390 }
391 }
392 });
393 }
394
395 var _loadResults = _.debounce(_loadResultsCallback, 250);
396
397 function loadResults() {
398 $scope.loading = true;
399 aggregateGroupByColumns();
400 _loadResults();
401 }
402
403 // What to tell the user when search returns an error from the server
404 // Todo: parse error codes and give helpful feedback.
405 function errorMsg(error) {
406 return ts('Ensure all search critera are set correctly and try again.');
407 }
408
409 this.changePage = function() {
410 if (ctrl.stale || !ctrl.results[ctrl.page]) {
411 lockTableHeight();
412 loadResults();
413 }
414 };
415
416 this.refreshAll = function() {
417 ctrl.stale = true;
418 ctrl.selectedRows.length = 0;
419 loadResults();
420 };
421
422 // Refresh results while staying on current page.
423 this.refreshPage = function() {
424 lockTableHeight();
425 ctrl.results = {};
426 loadResults();
427 };
428
429 $scope.onClickSearch = function() {
430 if (ctrl.autoSearch) {
431 ctrl.autoSearch = false;
432 } else {
433 ctrl.refreshAll();
434 }
435 };
436
437 $scope.onClickAuto = function() {
438 ctrl.autoSearch = !ctrl.autoSearch;
439 if (ctrl.autoSearch && ctrl.stale) {
440 ctrl.refreshAll();
441 }
442 $('.crm-search-auto-toggle').blur();
443 };
444
445 $scope.onChangeLimit = function() {
446 // Refresh only if search has already been run
447 if (ctrl.autoSearch || ctrl.results) {
448 // Save page size in localStorage
449 CRM.cache.set('searchPageSize', ctrl.limit);
450 ctrl.refreshAll();
451 }
452 };
453
454 function onChangeSelect(newSelect, oldSelect) {
455 // When removing a column from SELECT, also remove from ORDER BY
456 _.each(_.difference(_.keys(ctrl.savedSearch.api_params.orderBy), newSelect), function(col) {
457 delete ctrl.savedSearch.api_params.orderBy[col];
458 });
459 // Re-arranging or removing columns doesn't merit a refresh, only adding columns does
460 if (!oldSelect || _.difference(newSelect, oldSelect).length) {
461 if (ctrl.autoSearch) {
462 ctrl.refreshPage();
463 } else {
464 ctrl.stale = true;
465 }
466 }
467 if (ctrl.load) {
468 ctrl.saved = false;
469 }
470 }
471
472 function onChangeFilters() {
473 ctrl.stale = true;
474 ctrl.selectedRows.length = 0;
475 if (ctrl.load) {
476 ctrl.saved = false;
477 }
478 if (ctrl.autoSearch) {
479 ctrl.refreshAll();
480 }
481 }
482
483 $scope.selectAllRows = function() {
484 // Deselect all
485 if (ctrl.allRowsSelected) {
486 ctrl.allRowsSelected = false;
487 ctrl.selectedRows.length = 0;
488 return;
489 }
490 // Select all
491 ctrl.allRowsSelected = true;
492 if (ctrl.page === 1 && ctrl.results[1].length < ctrl.limit) {
493 ctrl.selectedRows = _.pluck(ctrl.results[1], 'id');
494 return;
495 }
496 // If more than one page of results, use ajax to fetch all ids
497 $scope.loadingAllRows = true;
498 var params = _.cloneDeep(ctrl.savedSearch.api_params);
499 params.select = ['id'];
500 crmApi4(ctrl.savedSearch.api_entity, 'get', params, ['id']).then(function(ids) {
501 $scope.loadingAllRows = false;
502 ctrl.selectedRows = _.toArray(ids);
503 });
504 };
505
506 $scope.selectRow = function(row) {
507 var index = ctrl.selectedRows.indexOf(row.id);
508 if (index < 0) {
509 ctrl.selectedRows.push(row.id);
510 ctrl.allRowsSelected = (ctrl.rowCount === ctrl.selectedRows.length);
511 } else {
512 ctrl.allRowsSelected = false;
513 ctrl.selectedRows.splice(index, 1);
514 }
515 };
516
517 $scope.isRowSelected = function(row) {
518 return ctrl.allRowsSelected || _.includes(ctrl.selectedRows, row.id);
519 };
520
521 this.getFieldLabel = function(col) {
522 var info = searchMeta.parseExpr(col),
523 label = info.field.label;
524 if (info.fn) {
525 label = '(' + info.fn.title + ') ' + label;
526 }
527 return label;
528 };
529
530 // Is a column eligible to use an aggregate function?
531 this.canAggregate = function(col) {
532 // If the query does not use grouping, never
533 if (!ctrl.savedSearch.api_params.groupBy.length) {
534 return false;
535 }
536 var info = searchMeta.parseExpr(col);
537 // If the column is used for a groupBy, no
538 if (ctrl.savedSearch.api_params.groupBy.indexOf(info.path) > -1) {
539 return false;
540 }
541 // If the entity this column belongs to is being grouped by id, then also no
542 return ctrl.savedSearch.api_params.groupBy.indexOf(info.prefix + 'id') < 0;
543 };
544
545 $scope.formatResult = function(row, col) {
546 var info = searchMeta.parseExpr(col),
547 key = info.fn ? (info.fn.name + ':' + info.path + info.suffix) : col,
548 value = row[key];
549 if (info.fn && info.fn.name === 'COUNT') {
550 return value;
551 }
552 // Output user-facing name/label fields as a link, if possible
553 if (info.field && _.includes(['display_name', 'title', 'label', 'subject'], info.field.name) && !info.fn && typeof value === 'string') {
554 var link = getEntityUrl(row, info);
555 if (link) {
556 return '<a href="' + _.escape(link.url) + '" title="' + _.escape(link.title) + '">' + formatFieldValue(info.field, value) + '</a>';
557 }
558 }
559 return formatFieldValue(info.field, value);
560 };
561
562 // Attempts to construct a view url for a given entity
563 function getEntityUrl(row, info) {
564 var entity = searchMeta.getEntity(info.field.entity),
565 path = _.result(_.findWhere(entity.paths, {action: 'view'}), 'path');
566 // Only proceed if the path metadata exists for this entity
567 if (path) {
568 // Replace tokens in the path (e.g. [id])
569 var tokens = path.match(/\[\w*]/g) || [],
570 replacements = _.transform(tokens, function(replacements, token) {
571 var fieldName = info.prefix + token.slice(1, token.length - 1);
572 if (row[fieldName]) {
573 replacements.push(row[fieldName]);
574 }
575 });
576 // Only proceed if the row contains all the necessary data to resolve tokens
577 if (tokens.length === replacements.length) {
578 _.each(tokens, function(token, index) {
579 path = path.replace(token, replacements[index]);
580 });
581 return {url: CRM.url(path), title: path.title};
582 }
583 }
584 }
585
586 function formatFieldValue(field, value) {
587 var type = field.data_type,
588 result = value;
589 if (_.isArray(value)) {
590 return _.map(value, function(val) {
591 return formatFieldValue(field, val);
592 }).join(', ');
593 }
594 if (value && (type === 'Date' || type === 'Timestamp') && /^\d{4}-\d{2}-\d{2}/.test(value)) {
595 result = CRM.utils.formatDate(value, null, type === 'Timestamp');
596 }
597 else if (type === 'Boolean' && typeof value === 'boolean') {
598 result = value ? ts('Yes') : ts('No');
599 }
600 else if (type === 'Money' && typeof value === 'number') {
601 result = CRM.formatMoney(value);
602 }
603 return _.escape(result);
604 }
605
606 $scope.fieldsForGroupBy = function() {
607 return {results: getAllFields('', function(key) {
608 return _.contains(ctrl.savedSearch.api_params.groupBy, key);
609 })
610 };
611 };
612
613 $scope.fieldsForSelect = function() {
614 return {results: getAllFields(':label', function(key) {
615 return _.contains(ctrl.savedSearch.api_params.select, key);
616 })
617 };
618 };
619
620 $scope.fieldsForWhere = function() {
621 return {results: getAllFields(':name', _.noop)};
622 };
623
624 $scope.fieldsForHaving = function() {
625 return {results: _.transform(ctrl.savedSearch.api_params.select, function(fields, name) {
626 fields.push({id: name, text: ctrl.getFieldLabel(name)});
627 })};
628 };
629
630 $scope.sortableColumnOptions = {
631 axis: 'x',
632 handle: '.crm-draggable',
633 update: function(e, ui) {
634 // Don't allow items to be moved to position 0 if locked
635 if (!ui.item.sortable.dropindex && ctrl.groupExists) {
636 ui.item.sortable.cancel();
637 }
638 }
639 };
640
641 // Sets the default select clause based on commonly-named fields
642 function getDefaultSelect() {
643 var whitelist = ['id', 'name', 'subject', 'display_name', 'label', 'title'];
644 return _.transform(searchMeta.getEntity(ctrl.savedSearch.api_entity).fields, function(select, field) {
645 if (_.includes(whitelist, field.name) || _.includes(field.name, '_type_id')) {
646 select.push(field.name + (field.options ? ':label' : ''));
647 }
648 });
649 }
650
651 function getAllFields(suffix, disabledIf) {
652 function formatFields(entityName, prefix) {
653 return _.transform(searchMeta.getEntity(entityName).fields, function(result, field) {
654 var item = {
655 id: prefix + field.name + (field.options ? suffix : ''),
656 text: field.label,
657 description: field.description
658 };
659 if (disabledIf(item.id)) {
660 item.disabled = true;
661 }
662 result.push(item);
663 }, []);
664 }
665
666 var mainEntity = searchMeta.getEntity(ctrl.savedSearch.api_entity),
667 result = [{
668 text: mainEntity.title_plural,
669 icon: mainEntity.icon,
670 children: formatFields(ctrl.savedSearch.api_entity, '')
671 }];
672 _.each(ctrl.savedSearch.api_params.join, function(join) {
673 var joinName = join[0].split(' AS '),
674 joinEntity = searchMeta.getEntity(joinName[0]);
675 result.push({
676 text: joinEntity.title_plural + ' (' + joinName[1] + ')',
677 icon: joinEntity.icon,
678 children: formatFields(joinEntity.name, joinName[1] + '.')
679 });
680 });
681 return result;
682 }
683
684 /**
685 * Fetch pseudoconstants for main entity + joined entities
686 *
687 * Sets an optionsLoaded property on each entity to avoid duplicate requests
688 */
689 function loadFieldOptions() {
690 var mainEntity = searchMeta.getEntity(ctrl.savedSearch.api_entity),
691 entities = {};
692
693 function enqueue(entity) {
694 entity.optionsLoaded = false;
695 entities[entity.name] = [entity.name, 'getFields', {
696 loadOptions: ['id', 'name', 'label', 'description', 'color', 'icon'],
697 where: [['options', '!=', false]],
698 select: ['options']
699 }, {name: 'options'}];
700 }
701
702 if (typeof mainEntity.optionsLoaded === 'undefined') {
703 enqueue(mainEntity);
704 }
705 _.each(ctrl.savedSearch.api_params.join, function(join) {
706 var joinName = join[0].split(' AS '),
707 joinEntity = searchMeta.getEntity(joinName[0]);
708 if (typeof joinEntity.optionsLoaded === 'undefined') {
709 enqueue(joinEntity);
710 }
711 });
712 if (!_.isEmpty(entities)) {
713 crmApi4(entities).then(function(results) {
714 _.each(results, function(fields, entityName) {
715 var entity = searchMeta.getEntity(entityName);
716 _.each(fields, function(options, fieldName) {
717 _.find(entity.fields, {name: fieldName}).options = options;
718 });
719 entity.optionsLoaded = true;
720 });
721 });
722 }
723 }
724
725 }
726 });
727
728 })(angular, CRM.$, CRM._);