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 | 243 | var join = searchMeta.getJoin($scope.controls.join), |
524aa12c | 244 | entity = searchMeta.getEntity(join.entity), |
4f0729ed CW |
245 | params = [$scope.controls.join, false]; |
246 | _.each(_.cloneDeep(join.conditions), function(condition) { | |
247 | params.push(condition); | |
248 | }); | |
2f616560 CW |
249 | _.each(_.cloneDeep(join.defaults), function(condition) { |
250 | params.push(condition); | |
251 | }); | |
4f0729ed | 252 | ctrl.savedSearch.api_params.join.push(params); |
524aa12c CW |
253 | if (entity.label_field) { |
254 | ctrl.savedSearch.api_params.select.push(join.alias + '.' + entity.label_field); | |
255 | } | |
25523059 CW |
256 | loadFieldOptions(); |
257 | } | |
258 | $scope.controls.join = ''; | |
259 | }); | |
260 | }; | |
261 | ||
25523059 | 262 | $scope.changeGroupBy = function(idx) { |
2894db84 | 263 | if (!ctrl.savedSearch.api_params.groupBy[idx]) { |
25523059 CW |
264 | ctrl.clearParam('groupBy', idx); |
265 | } | |
5fcd63f4 | 266 | // Remove aggregate functions when no grouping |
2894db84 CW |
267 | if (!ctrl.savedSearch.api_params.groupBy.length) { |
268 | _.each(ctrl.savedSearch.api_params.select, function(col, pos) { | |
5fcd63f4 CW |
269 | if (_.contains(col, '(')) { |
270 | var info = searchMeta.parseExpr(col); | |
271 | if (info.fn.category === 'aggregate') { | |
2894db84 | 272 | ctrl.savedSearch.api_params.select[pos] = info.path + info.suffix; |
5fcd63f4 CW |
273 | } |
274 | } | |
275 | }); | |
276 | } | |
25523059 CW |
277 | }; |
278 | ||
2badf248 CW |
279 | function validate() { |
280 | var errors = [], | |
281 | errorEl, | |
282 | label, | |
283 | tab; | |
284 | if (!ctrl.savedSearch.label) { | |
285 | errorEl = '#crm-saved-search-label'; | |
286 | label = ts('Search Label'); | |
287 | errors.push(ts('%1 is a required field.', {1: label})); | |
288 | } | |
289 | if (ctrl.groupExists && !ctrl.savedSearch.groups[0].title) { | |
290 | errorEl = '#crm-search-admin-group-title'; | |
291 | label = ts('Group Title'); | |
292 | errors.push(ts('%1 is a required field.', {1: label})); | |
293 | tab = 'group'; | |
294 | } | |
295 | _.each(ctrl.savedSearch.displays, function(display, index) { | |
296 | if (!display.trashed && !display.label) { | |
297 | errorEl = '#crm-search-admin-display-label'; | |
298 | label = ts('Display Label'); | |
299 | errors.push(ts('%1 is a required field.', {1: label})); | |
300 | tab = 'display_' + index; | |
301 | } | |
302 | }); | |
303 | if (errors.length) { | |
304 | if (tab) { | |
305 | $scope.selectTab(tab); | |
306 | } | |
307 | $(errorEl).crmError(errors.join('<br>'), ts('Error Saving'), {expires: 5000}); | |
308 | } | |
309 | return !errors.length; | |
310 | } | |
311 | ||
25523059 CW |
312 | /** |
313 | * Called when clicking on a column header | |
314 | * @param col | |
315 | * @param $event | |
316 | */ | |
317 | $scope.setOrderBy = function(col, $event) { | |
318 | var dir = $scope.getOrderBy(col) === 'fa-sort-asc' ? 'DESC' : 'ASC'; | |
b1603dbd | 319 | if (!$event.shiftKey || !ctrl.savedSearch.api_params.orderBy) { |
2894db84 | 320 | ctrl.savedSearch.api_params.orderBy = {}; |
25523059 | 321 | } |
2894db84 | 322 | ctrl.savedSearch.api_params.orderBy[col] = dir; |
7da9d079 CW |
323 | if (ctrl.results) { |
324 | ctrl.refreshPage(); | |
325 | } | |
25523059 CW |
326 | }; |
327 | ||
328 | /** | |
329 | * Returns crm-i icon class for a sortable column | |
330 | * @param col | |
331 | * @returns {string} | |
332 | */ | |
333 | $scope.getOrderBy = function(col) { | |
2894db84 | 334 | var dir = ctrl.savedSearch.api_params.orderBy && ctrl.savedSearch.api_params.orderBy[col]; |
25523059 CW |
335 | if (dir) { |
336 | return 'fa-sort-' + dir.toLowerCase(); | |
337 | } | |
338 | return 'fa-sort disabled'; | |
339 | }; | |
340 | ||
341 | $scope.addParam = function(name) { | |
2894db84 CW |
342 | if ($scope.controls[name] && !_.contains(ctrl.savedSearch.api_params[name], $scope.controls[name])) { |
343 | ctrl.savedSearch.api_params[name].push($scope.controls[name]); | |
25523059 CW |
344 | if (name === 'groupBy') { |
345 | // Expand the aggregate block | |
346 | $timeout(function() { | |
347 | $('#crm-search-build-group-aggregate.collapsed .collapsible-title').click(); | |
348 | }, 10); | |
349 | } | |
350 | } | |
351 | $scope.controls[name] = ''; | |
352 | }; | |
353 | ||
354 | // Deletes an item from an array param | |
355 | this.clearParam = function(name, idx) { | |
2894db84 | 356 | ctrl.savedSearch.api_params[name].splice(idx, 1); |
25523059 CW |
357 | }; |
358 | ||
359 | // Prevent visual jumps in results table height during loading | |
360 | function lockTableHeight() { | |
361 | var $table = $('.crm-search-results', $element); | |
362 | $table.css('height', $table.height()); | |
363 | } | |
364 | ||
365 | function unlockTableHeight() { | |
366 | $('.crm-search-results', $element).css('height', ''); | |
367 | } | |
368 | ||
5fcd63f4 CW |
369 | // Ensure all non-grouped columns are aggregated if using GROUP BY |
370 | function aggregateGroupByColumns() { | |
2894db84 CW |
371 | if (ctrl.savedSearch.api_params.groupBy.length) { |
372 | _.each(ctrl.savedSearch.api_params.select, function(col, pos) { | |
5fcd63f4 | 373 | if (!_.contains(col, '(') && ctrl.canAggregate(col)) { |
2ca319b3 | 374 | ctrl.savedSearch.api_params.select[pos] = ctrl.DEFAULT_AGGREGATE_FN + '(DISTINCT ' + col + ')'; |
5fcd63f4 CW |
375 | } |
376 | }); | |
377 | } | |
378 | } | |
379 | ||
25523059 CW |
380 | // Debounced callback for loadResults |
381 | function _loadResultsCallback() { | |
382 | // Multiply limit to read 2 pages at once & save ajax requests | |
f9cf8797 CW |
383 | var params = _.merge(_.cloneDeep(ctrl.savedSearch.api_params), {debug: true, limit: ctrl.limit * 2}); |
384 | // Select the ids of joined entities (helps with displaying links) | |
385 | _.each(params.join, function(join) { | |
386 | var idField = join[0].split(' AS ')[1] + '.id'; | |
387 | if (!_.includes(params.select, idField) && !ctrl.canAggregate(idField)) { | |
388 | params.select.push(idField); | |
389 | } | |
390 | }); | |
25523059 CW |
391 | lockTableHeight(); |
392 | $scope.error = false; | |
393 | if (ctrl.stale) { | |
394 | ctrl.page = 1; | |
395 | ctrl.rowCount = false; | |
396 | } | |
25523059 | 397 | params.offset = ctrl.limit * (ctrl.page - 1); |
2894db84 | 398 | crmApi4(ctrl.savedSearch.api_entity, 'get', params).then(function(success) { |
25523059 CW |
399 | if (ctrl.stale) { |
400 | ctrl.results = {}; | |
adb3290a | 401 | // Get row count for pager |
1dee5068 CW |
402 | if (success.length < params.limit) { |
403 | ctrl.rowCount = success.count; | |
404 | } else { | |
405 | var countParams = _.cloneDeep(params); | |
406 | // Select is only needed needed by HAVING | |
407 | countParams.select = countParams.having && countParams.having.length ? countParams.select : []; | |
408 | countParams.select.push('row_count'); | |
409 | delete countParams.debug; | |
410 | crmApi4(ctrl.savedSearch.api_entity, 'get', countParams).then(function(result) { | |
411 | ctrl.rowCount = result.count; | |
412 | }); | |
413 | } | |
25523059 CW |
414 | } |
415 | ctrl.debug = success.debug; | |
416 | // populate this page & the next | |
417 | ctrl.results[ctrl.page] = success.slice(0, ctrl.limit); | |
418 | if (success.length > ctrl.limit) { | |
419 | ctrl.results[ctrl.page + 1] = success.slice(ctrl.limit); | |
420 | } | |
421 | $scope.loading = false; | |
422 | ctrl.stale = false; | |
423 | unlockTableHeight(); | |
424 | }, function(error) { | |
425 | $scope.loading = false; | |
426 | ctrl.results = {}; | |
427 | ctrl.stale = true; | |
428 | ctrl.debug = error.debug; | |
429 | $scope.error = errorMsg(error); | |
9c4a1dae CW |
430 | }) |
431 | .finally(function() { | |
432 | if (ctrl.debug) { | |
f9cf8797 | 433 | ctrl.debug.params = JSON.stringify(params, null, 2); |
8afb2047 CW |
434 | if (ctrl.debug.timeIndex) { |
435 | ctrl.debug.timeIndex = Number.parseFloat(ctrl.debug.timeIndex).toPrecision(2); | |
436 | } | |
9c4a1dae CW |
437 | } |
438 | }); | |
25523059 CW |
439 | } |
440 | ||
441 | var _loadResults = _.debounce(_loadResultsCallback, 250); | |
442 | ||
443 | function loadResults() { | |
444 | $scope.loading = true; | |
5fcd63f4 | 445 | aggregateGroupByColumns(); |
25523059 CW |
446 | _loadResults(); |
447 | } | |
448 | ||
449 | // What to tell the user when search returns an error from the server | |
450 | // Todo: parse error codes and give helpful feedback. | |
451 | function errorMsg(error) { | |
452 | return ts('Ensure all search critera are set correctly and try again.'); | |
453 | } | |
454 | ||
455 | this.changePage = function() { | |
456 | if (ctrl.stale || !ctrl.results[ctrl.page]) { | |
457 | lockTableHeight(); | |
458 | loadResults(); | |
459 | } | |
460 | }; | |
461 | ||
462 | this.refreshAll = function() { | |
463 | ctrl.stale = true; | |
464 | ctrl.selectedRows.length = 0; | |
465 | loadResults(); | |
466 | }; | |
467 | ||
468 | // Refresh results while staying on current page. | |
469 | this.refreshPage = function() { | |
470 | lockTableHeight(); | |
471 | ctrl.results = {}; | |
472 | loadResults(); | |
473 | }; | |
474 | ||
475 | $scope.onClickSearch = function() { | |
476 | if (ctrl.autoSearch) { | |
477 | ctrl.autoSearch = false; | |
478 | } else { | |
479 | ctrl.refreshAll(); | |
480 | } | |
481 | }; | |
482 | ||
483 | $scope.onClickAuto = function() { | |
484 | ctrl.autoSearch = !ctrl.autoSearch; | |
485 | if (ctrl.autoSearch && ctrl.stale) { | |
486 | ctrl.refreshAll(); | |
487 | } | |
0b1769c6 | 488 | $('.crm-search-auto-toggle').blur(); |
25523059 CW |
489 | }; |
490 | ||
491 | $scope.onChangeLimit = function() { | |
492 | // Refresh only if search has already been run | |
493 | if (ctrl.autoSearch || ctrl.results) { | |
494 | // Save page size in localStorage | |
495 | CRM.cache.set('searchPageSize', ctrl.limit); | |
496 | ctrl.refreshAll(); | |
497 | } | |
498 | }; | |
499 | ||
500 | function onChangeSelect(newSelect, oldSelect) { | |
7da9d079 | 501 | // When removing a column from SELECT, also remove from ORDER BY |
2894db84 CW |
502 | _.each(_.difference(_.keys(ctrl.savedSearch.api_params.orderBy), newSelect), function(col) { |
503 | delete ctrl.savedSearch.api_params.orderBy[col]; | |
7da9d079 | 504 | }); |
25523059 CW |
505 | // Re-arranging or removing columns doesn't merit a refresh, only adding columns does |
506 | if (!oldSelect || _.difference(newSelect, oldSelect).length) { | |
507 | if (ctrl.autoSearch) { | |
508 | ctrl.refreshPage(); | |
509 | } else { | |
510 | ctrl.stale = true; | |
511 | } | |
512 | } | |
2c7e2f4b | 513 | if (ctrl.load) { |
f9197b41 | 514 | ctrl.saved = false; |
2c7e2f4b | 515 | } |
25523059 CW |
516 | } |
517 | ||
25523059 CW |
518 | function onChangeFilters() { |
519 | ctrl.stale = true; | |
520 | ctrl.selectedRows.length = 0; | |
2c7e2f4b | 521 | if (ctrl.load) { |
f9197b41 | 522 | ctrl.saved = false; |
2c7e2f4b | 523 | } |
25523059 CW |
524 | if (ctrl.autoSearch) { |
525 | ctrl.refreshAll(); | |
526 | } | |
527 | } | |
528 | ||
529 | $scope.selectAllRows = function() { | |
530 | // Deselect all | |
531 | if (ctrl.allRowsSelected) { | |
532 | ctrl.allRowsSelected = false; | |
533 | ctrl.selectedRows.length = 0; | |
534 | return; | |
535 | } | |
536 | // Select all | |
537 | ctrl.allRowsSelected = true; | |
538 | if (ctrl.page === 1 && ctrl.results[1].length < ctrl.limit) { | |
539 | ctrl.selectedRows = _.pluck(ctrl.results[1], 'id'); | |
540 | return; | |
541 | } | |
542 | // If more than one page of results, use ajax to fetch all ids | |
543 | $scope.loadingAllRows = true; | |
2894db84 | 544 | var params = _.cloneDeep(ctrl.savedSearch.api_params); |
adb3290a CW |
545 | // Select is only needed needed by HAVING |
546 | params.select = params.having && params.having.length ? params.select : []; | |
547 | params.select.push('id'); | |
2894db84 | 548 | crmApi4(ctrl.savedSearch.api_entity, 'get', params, ['id']).then(function(ids) { |
25523059 CW |
549 | $scope.loadingAllRows = false; |
550 | ctrl.selectedRows = _.toArray(ids); | |
551 | }); | |
552 | }; | |
553 | ||
554 | $scope.selectRow = function(row) { | |
555 | var index = ctrl.selectedRows.indexOf(row.id); | |
556 | if (index < 0) { | |
557 | ctrl.selectedRows.push(row.id); | |
558 | ctrl.allRowsSelected = (ctrl.rowCount === ctrl.selectedRows.length); | |
559 | } else { | |
560 | ctrl.allRowsSelected = false; | |
561 | ctrl.selectedRows.splice(index, 1); | |
562 | } | |
563 | }; | |
564 | ||
565 | $scope.isRowSelected = function(row) { | |
566 | return ctrl.allRowsSelected || _.includes(ctrl.selectedRows, row.id); | |
567 | }; | |
568 | ||
03b55607 | 569 | this.getFieldLabel = searchMeta.getDefaultLabel; |
25523059 CW |
570 | |
571 | // Is a column eligible to use an aggregate function? | |
572 | this.canAggregate = function(col) { | |
f9cf8797 CW |
573 | // If the query does not use grouping, never |
574 | if (!ctrl.savedSearch.api_params.groupBy.length) { | |
575 | return false; | |
576 | } | |
4f3a95d9 | 577 | var info = searchMeta.parseExpr(col); |
25523059 | 578 | // If the column is used for a groupBy, no |
2894db84 | 579 | if (ctrl.savedSearch.api_params.groupBy.indexOf(info.path) > -1) { |
25523059 CW |
580 | return false; |
581 | } | |
582 | // If the entity this column belongs to is being grouped by id, then also no | |
2894db84 | 583 | return ctrl.savedSearch.api_params.groupBy.indexOf(info.prefix + 'id') < 0; |
25523059 CW |
584 | }; |
585 | ||
f9cf8797 | 586 | $scope.formatResult = function(row, col) { |
25523059 | 587 | var info = searchMeta.parseExpr(col), |
1dee5068 | 588 | value = row[info.alias + info.suffix]; |
7ce7b1cd | 589 | if (info.fn && info.fn.name === 'COUNT') { |
2d198bb4 CW |
590 | return value; |
591 | } | |
f9cf8797 CW |
592 | // Output user-facing name/label fields as a link, if possible |
593 | if (info.field && _.includes(['display_name', 'title', 'label', 'subject'], info.field.name) && !info.fn && typeof value === 'string') { | |
594 | var link = getEntityUrl(row, info); | |
595 | if (link) { | |
596 | return '<a href="' + _.escape(link.url) + '" title="' + _.escape(link.title) + '">' + formatFieldValue(info.field, value) + '</a>'; | |
597 | } | |
598 | } | |
25523059 CW |
599 | return formatFieldValue(info.field, value); |
600 | }; | |
601 | ||
f9cf8797 CW |
602 | // Attempts to construct a view url for a given entity |
603 | function getEntityUrl(row, info) { | |
604 | var entity = searchMeta.getEntity(info.field.entity), | |
605 | path = _.result(_.findWhere(entity.paths, {action: 'view'}), 'path'); | |
606 | // Only proceed if the path metadata exists for this entity | |
607 | if (path) { | |
608 | // Replace tokens in the path (e.g. [id]) | |
609 | var tokens = path.match(/\[\w*]/g) || [], | |
610 | replacements = _.transform(tokens, function(replacements, token) { | |
611 | var fieldName = info.prefix + token.slice(1, token.length - 1); | |
612 | if (row[fieldName]) { | |
613 | replacements.push(row[fieldName]); | |
614 | } | |
615 | }); | |
616 | // Only proceed if the row contains all the necessary data to resolve tokens | |
617 | if (tokens.length === replacements.length) { | |
618 | _.each(tokens, function(token, index) { | |
619 | path = path.replace(token, replacements[index]); | |
620 | }); | |
621 | return {url: CRM.url(path), title: path.title}; | |
622 | } | |
623 | } | |
624 | } | |
625 | ||
25523059 | 626 | function formatFieldValue(field, value) { |
f9cf8797 CW |
627 | var type = field.data_type, |
628 | result = value; | |
7ce7b1cd CW |
629 | if (_.isArray(value)) { |
630 | return _.map(value, function(val) { | |
631 | return formatFieldValue(field, val); | |
632 | }).join(', '); | |
633 | } | |
25523059 | 634 | if (value && (type === 'Date' || type === 'Timestamp') && /^\d{4}-\d{2}-\d{2}/.test(value)) { |
f9cf8797 | 635 | result = CRM.utils.formatDate(value, null, type === 'Timestamp'); |
25523059 CW |
636 | } |
637 | else if (type === 'Boolean' && typeof value === 'boolean') { | |
f9cf8797 | 638 | result = value ? ts('Yes') : ts('No'); |
25523059 | 639 | } |
7ce7b1cd | 640 | else if (type === 'Money' && typeof value === 'number') { |
f9cf8797 | 641 | result = CRM.formatMoney(value); |
2d198bb4 | 642 | } |
f9cf8797 | 643 | return _.escape(result); |
25523059 CW |
644 | } |
645 | ||
25523059 CW |
646 | $scope.fieldsForGroupBy = function() { |
647 | return {results: getAllFields('', function(key) { | |
2894db84 | 648 | return _.contains(ctrl.savedSearch.api_params.groupBy, key); |
25523059 CW |
649 | }) |
650 | }; | |
651 | }; | |
652 | ||
653 | $scope.fieldsForSelect = function() { | |
654 | return {results: getAllFields(':label', function(key) { | |
2894db84 | 655 | return _.contains(ctrl.savedSearch.api_params.select, key); |
25523059 CW |
656 | }) |
657 | }; | |
658 | }; | |
659 | ||
660 | $scope.fieldsForWhere = function() { | |
661 | return {results: getAllFields(':name', _.noop)}; | |
662 | }; | |
663 | ||
664 | $scope.fieldsForHaving = function() { | |
2894db84 | 665 | return {results: _.transform(ctrl.savedSearch.api_params.select, function(fields, name) { |
ba226c04 CW |
666 | var info = searchMeta.parseExpr(name); |
667 | fields.push({id: info.alias + info.suffix, text: ctrl.getFieldLabel(name)}); | |
25523059 CW |
668 | })}; |
669 | }; | |
670 | ||
4b01551f CW |
671 | $scope.sortableColumnOptions = { |
672 | axis: 'x', | |
44402a2e | 673 | handle: '.crm-draggable', |
4b01551f CW |
674 | update: function(e, ui) { |
675 | // Don't allow items to be moved to position 0 if locked | |
676 | if (!ui.item.sortable.dropindex && ctrl.groupExists) { | |
677 | ui.item.sortable.cancel(); | |
678 | } | |
679 | } | |
680 | }; | |
681 | ||
682 | // Sets the default select clause based on commonly-named fields | |
25523059 | 683 | function getDefaultSelect() { |
b0f5b67a CW |
684 | var entity = searchMeta.getEntity(ctrl.savedSearch.api_entity); |
685 | return _.transform(entity.fields, function(defaultSelect, field) { | |
686 | if (field.name === 'id' || field.name === entity.label_field) { | |
687 | defaultSelect.push(field.name); | |
4b01551f | 688 | } |
c419e6ed | 689 | }); |
25523059 CW |
690 | } |
691 | ||
692 | function getAllFields(suffix, disabledIf) { | |
4f0729ed CW |
693 | function formatFields(entityName, join) { |
694 | var prefix = join ? join.alias + '.' : '', | |
695 | result = []; | |
696 | ||
697 | function addFields(fields) { | |
698 | _.each(fields, function(field) { | |
699 | var item = { | |
700 | id: prefix + field.name + (field.options ? suffix : ''), | |
701 | text: field.label, | |
702 | description: field.description | |
703 | }; | |
704 | if (disabledIf(item.id)) { | |
705 | item.disabled = true; | |
706 | } | |
707 | result.push(item); | |
708 | }); | |
709 | } | |
710 | ||
711 | // Add extra searchable fields from bridge entity | |
712 | if (join && join.bridge) { | |
713 | addFields(_.filter(searchMeta.getEntity(join.bridge).fields, function(field) { | |
714 | return (field.name !== 'id' && field.name !== 'entity_id' && field.name !== 'entity_table' && !field.fk_entity); | |
715 | })); | |
716 | } | |
717 | ||
718 | addFields(searchMeta.getEntity(entityName).fields); | |
719 | return result; | |
25523059 CW |
720 | } |
721 | ||
2894db84 | 722 | var mainEntity = searchMeta.getEntity(ctrl.savedSearch.api_entity), |
25523059 | 723 | result = [{ |
9813ae79 | 724 | text: mainEntity.title_plural, |
25523059 | 725 | icon: mainEntity.icon, |
4f0729ed | 726 | children: formatFields(ctrl.savedSearch.api_entity) |
25523059 | 727 | }]; |
2894db84 | 728 | _.each(ctrl.savedSearch.api_params.join, function(join) { |
4f0729ed CW |
729 | var joinInfo = searchMeta.getJoin(join[0]), |
730 | joinEntity = searchMeta.getEntity(joinInfo.entity); | |
25523059 | 731 | result.push({ |
4f0729ed CW |
732 | text: joinInfo.label, |
733 | description: joinInfo.description, | |
25523059 | 734 | icon: joinEntity.icon, |
4f0729ed | 735 | children: formatFields(joinEntity.name, joinInfo) |
25523059 CW |
736 | }); |
737 | }); | |
738 | return result; | |
739 | } | |
740 | ||
741 | /** | |
742 | * Fetch pseudoconstants for main entity + joined entities | |
743 | * | |
744 | * Sets an optionsLoaded property on each entity to avoid duplicate requests | |
745 | */ | |
746 | function loadFieldOptions() { | |
2894db84 | 747 | var mainEntity = searchMeta.getEntity(ctrl.savedSearch.api_entity), |
25523059 CW |
748 | entities = {}; |
749 | ||
750 | function enqueue(entity) { | |
751 | entity.optionsLoaded = false; | |
752 | entities[entity.name] = [entity.name, 'getFields', { | |
22601c92 | 753 | loadOptions: ['id', 'name', 'label', 'description', 'color', 'icon'], |
25523059 CW |
754 | where: [['options', '!=', false]], |
755 | select: ['options'] | |
756 | }, {name: 'options'}]; | |
757 | } | |
758 | ||
759 | if (typeof mainEntity.optionsLoaded === 'undefined') { | |
760 | enqueue(mainEntity); | |
761 | } | |
2894db84 | 762 | _.each(ctrl.savedSearch.api_params.join, function(join) { |
4f0729ed CW |
763 | var joinInfo = searchMeta.getJoin(join[0]), |
764 | joinEntity = searchMeta.getEntity(joinInfo.entity), | |
765 | bridgeEntity = joinInfo.bridge ? searchMeta.getEntity(joinInfo.bridge) : null; | |
25523059 CW |
766 | if (typeof joinEntity.optionsLoaded === 'undefined') { |
767 | enqueue(joinEntity); | |
768 | } | |
4f0729ed CW |
769 | if (bridgeEntity && typeof bridgeEntity.optionsLoaded === 'undefined') { |
770 | enqueue(bridgeEntity); | |
771 | } | |
25523059 CW |
772 | }); |
773 | if (!_.isEmpty(entities)) { | |
774 | crmApi4(entities).then(function(results) { | |
775 | _.each(results, function(fields, entityName) { | |
776 | var entity = searchMeta.getEntity(entityName); | |
777 | _.each(fields, function(options, fieldName) { | |
778 | _.find(entity.fields, {name: fieldName}).options = options; | |
779 | }); | |
780 | entity.optionsLoaded = true; | |
781 | }); | |
782 | }); | |
783 | } | |
784 | } | |
785 | ||
25523059 CW |
786 | } |
787 | }); | |
788 | ||
789 | })(angular, CRM.$, CRM._); |