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