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