Commit | Line | Data |
---|---|---|
25523059 CW |
1 | (function(angular, $, _) { |
2 | "use strict"; | |
3 | ||
b0422f12 | 4 | angular.module('search').component('crmSearch', { |
25523059 | 5 | bindings: { |
2c7e2f4b CW |
6 | entity: '=', |
7 | load: '<' | |
25523059 | 8 | }, |
b0422f12 | 9 | templateUrl: '~/search/crmSearch.html', |
25523059 CW |
10 | controller: function($scope, $element, $timeout, crmApi4, dialogService, searchMeta, formatForSelect2) { |
11 | var ts = $scope.ts = CRM.ts(), | |
12 | ctrl = this; | |
5fcd63f4 CW |
13 | |
14 | this.DEFAULT_AGGREGATE_FN = 'GROUP_CONCAT'; | |
15 | ||
25523059 CW |
16 | this.selectedRows = []; |
17 | this.limit = CRM.cache.get('searchPageSize', 30); | |
18 | this.page = 1; | |
19 | this.params = {}; | |
20 | // After a search this.results is an object of result arrays keyed by page, | |
c419e6ed | 21 | // 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 |
22 | this.results = ''; |
23 | this.rowCount = false; | |
24 | // Have the filters (WHERE, HAVING, GROUP BY, JOIN) changed? | |
25 | this.stale = true; | |
26 | this.allRowsSelected = false; | |
27 | ||
28 | $scope.controls = {}; | |
29 | $scope.joinTypes = [{k: false, v: ts('Optional')}, {k: true, v: ts('Required')}]; | |
f2425270 | 30 | $scope.entities = formatForSelect2(CRM.vars.search.schema, 'name', 'titlePlural', ['description', 'icon']); |
25523059 CW |
31 | this.perm = { |
32 | editGroups: CRM.checkPerm('edit groups') | |
33 | }; | |
34 | ||
35 | this.getEntity = searchMeta.getEntity; | |
36 | ||
37 | this.paramExists = function(param) { | |
38 | return _.includes(searchMeta.getEntity(ctrl.entity).params, param); | |
39 | }; | |
40 | ||
41 | $scope.getJoinEntities = function() { | |
42 | var joinEntities = _.transform(CRM.vars.search.links[ctrl.entity], function(joinEntities, link) { | |
43 | var entity = searchMeta.getEntity(link.entity); | |
44 | if (entity) { | |
45 | joinEntities.push({ | |
46 | id: link.entity + ' AS ' + link.alias, | |
f2425270 | 47 | text: entity.titlePlural, |
25523059 CW |
48 | description: '(' + link.alias + ')', |
49 | icon: entity.icon | |
50 | }); | |
51 | } | |
52 | }, []); | |
53 | return {results: joinEntities}; | |
54 | }; | |
55 | ||
56 | $scope.addJoin = function() { | |
57 | // Debounce the onchange event using timeout | |
58 | $timeout(function() { | |
59 | if ($scope.controls.join) { | |
60 | ctrl.params.join = ctrl.params.join || []; | |
61 | ctrl.params.join.push([$scope.controls.join, false]); | |
62 | loadFieldOptions(); | |
63 | } | |
64 | $scope.controls.join = ''; | |
65 | }); | |
66 | }; | |
67 | ||
68 | $scope.changeJoin = function(idx) { | |
69 | if (ctrl.params.join[idx][0]) { | |
70 | ctrl.params.join[idx].length = 2; | |
71 | loadFieldOptions(); | |
72 | } else { | |
73 | ctrl.clearParam('join', idx); | |
74 | } | |
75 | }; | |
76 | ||
77 | $scope.changeGroupBy = function(idx) { | |
78 | if (!ctrl.params.groupBy[idx]) { | |
79 | ctrl.clearParam('groupBy', idx); | |
80 | } | |
5fcd63f4 CW |
81 | // Remove aggregate functions when no grouping |
82 | if (!ctrl.params.groupBy.length) { | |
83 | _.each(ctrl.params.select, function(col, pos) { | |
84 | if (_.contains(col, '(')) { | |
85 | var info = searchMeta.parseExpr(col); | |
86 | if (info.fn.category === 'aggregate') { | |
87 | ctrl.params.select[pos] = info.path + info.suffix; | |
88 | } | |
89 | } | |
90 | }); | |
91 | } | |
25523059 CW |
92 | }; |
93 | ||
94 | /** | |
95 | * Called when clicking on a column header | |
96 | * @param col | |
97 | * @param $event | |
98 | */ | |
99 | $scope.setOrderBy = function(col, $event) { | |
100 | var dir = $scope.getOrderBy(col) === 'fa-sort-asc' ? 'DESC' : 'ASC'; | |
101 | if (!$event.shiftKey) { | |
102 | ctrl.params.orderBy = {}; | |
103 | } | |
104 | ctrl.params.orderBy[col] = dir; | |
7da9d079 CW |
105 | if (ctrl.results) { |
106 | ctrl.refreshPage(); | |
107 | } | |
25523059 CW |
108 | }; |
109 | ||
110 | /** | |
111 | * Returns crm-i icon class for a sortable column | |
112 | * @param col | |
113 | * @returns {string} | |
114 | */ | |
115 | $scope.getOrderBy = function(col) { | |
116 | var dir = ctrl.params.orderBy && ctrl.params.orderBy[col]; | |
117 | if (dir) { | |
118 | return 'fa-sort-' + dir.toLowerCase(); | |
119 | } | |
120 | return 'fa-sort disabled'; | |
121 | }; | |
122 | ||
123 | $scope.addParam = function(name) { | |
124 | if ($scope.controls[name] && !_.contains(ctrl.params[name], $scope.controls[name])) { | |
125 | ctrl.params[name].push($scope.controls[name]); | |
126 | if (name === 'groupBy') { | |
127 | // Expand the aggregate block | |
128 | $timeout(function() { | |
129 | $('#crm-search-build-group-aggregate.collapsed .collapsible-title').click(); | |
130 | }, 10); | |
131 | } | |
132 | } | |
133 | $scope.controls[name] = ''; | |
134 | }; | |
135 | ||
136 | // Deletes an item from an array param | |
137 | this.clearParam = function(name, idx) { | |
138 | ctrl.params[name].splice(idx, 1); | |
139 | }; | |
140 | ||
141 | // Prevent visual jumps in results table height during loading | |
142 | function lockTableHeight() { | |
143 | var $table = $('.crm-search-results', $element); | |
144 | $table.css('height', $table.height()); | |
145 | } | |
146 | ||
147 | function unlockTableHeight() { | |
148 | $('.crm-search-results', $element).css('height', ''); | |
149 | } | |
150 | ||
5fcd63f4 CW |
151 | // Ensure all non-grouped columns are aggregated if using GROUP BY |
152 | function aggregateGroupByColumns() { | |
153 | if (ctrl.params.groupBy.length) { | |
154 | _.each(ctrl.params.select, function(col, pos) { | |
155 | if (!_.contains(col, '(') && ctrl.canAggregate(col)) { | |
156 | ctrl.params.select[pos] = ctrl.DEFAULT_AGGREGATE_FN + '(' + col + ')'; | |
157 | } | |
158 | }); | |
159 | } | |
160 | } | |
161 | ||
25523059 CW |
162 | // Debounced callback for loadResults |
163 | function _loadResultsCallback() { | |
164 | // Multiply limit to read 2 pages at once & save ajax requests | |
165 | var params = angular.merge({debug: true, limit: ctrl.limit * 2}, ctrl.params); | |
166 | lockTableHeight(); | |
167 | $scope.error = false; | |
168 | if (ctrl.stale) { | |
169 | ctrl.page = 1; | |
170 | ctrl.rowCount = false; | |
171 | } | |
172 | if (ctrl.rowCount === false) { | |
173 | params.select.push('row_count'); | |
174 | } | |
175 | params.offset = ctrl.limit * (ctrl.page - 1); | |
176 | crmApi4(ctrl.entity, 'get', params).then(function(success) { | |
177 | if (ctrl.stale) { | |
178 | ctrl.results = {}; | |
179 | } | |
180 | if (ctrl.rowCount === false) { | |
181 | ctrl.rowCount = success.count; | |
182 | } | |
183 | ctrl.debug = success.debug; | |
184 | // populate this page & the next | |
185 | ctrl.results[ctrl.page] = success.slice(0, ctrl.limit); | |
186 | if (success.length > ctrl.limit) { | |
187 | ctrl.results[ctrl.page + 1] = success.slice(ctrl.limit); | |
188 | } | |
189 | $scope.loading = false; | |
190 | ctrl.stale = false; | |
191 | unlockTableHeight(); | |
192 | }, function(error) { | |
193 | $scope.loading = false; | |
194 | ctrl.results = {}; | |
195 | ctrl.stale = true; | |
196 | ctrl.debug = error.debug; | |
197 | $scope.error = errorMsg(error); | |
9c4a1dae CW |
198 | }) |
199 | .finally(function() { | |
200 | if (ctrl.debug) { | |
8afb2047 CW |
201 | ctrl.debug.params = JSON.stringify(_.extend({version: 4}, ctrl.params), null, 2); |
202 | if (ctrl.debug.timeIndex) { | |
203 | ctrl.debug.timeIndex = Number.parseFloat(ctrl.debug.timeIndex).toPrecision(2); | |
204 | } | |
9c4a1dae CW |
205 | } |
206 | }); | |
25523059 CW |
207 | } |
208 | ||
209 | var _loadResults = _.debounce(_loadResultsCallback, 250); | |
210 | ||
211 | function loadResults() { | |
212 | $scope.loading = true; | |
5fcd63f4 | 213 | aggregateGroupByColumns(); |
25523059 CW |
214 | _loadResults(); |
215 | } | |
216 | ||
217 | // What to tell the user when search returns an error from the server | |
218 | // Todo: parse error codes and give helpful feedback. | |
219 | function errorMsg(error) { | |
220 | return ts('Ensure all search critera are set correctly and try again.'); | |
221 | } | |
222 | ||
223 | this.changePage = function() { | |
224 | if (ctrl.stale || !ctrl.results[ctrl.page]) { | |
225 | lockTableHeight(); | |
226 | loadResults(); | |
227 | } | |
228 | }; | |
229 | ||
230 | this.refreshAll = function() { | |
231 | ctrl.stale = true; | |
232 | ctrl.selectedRows.length = 0; | |
233 | loadResults(); | |
234 | }; | |
235 | ||
236 | // Refresh results while staying on current page. | |
237 | this.refreshPage = function() { | |
238 | lockTableHeight(); | |
239 | ctrl.results = {}; | |
240 | loadResults(); | |
241 | }; | |
242 | ||
243 | $scope.onClickSearch = function() { | |
244 | if (ctrl.autoSearch) { | |
245 | ctrl.autoSearch = false; | |
246 | } else { | |
247 | ctrl.refreshAll(); | |
248 | } | |
249 | }; | |
250 | ||
251 | $scope.onClickAuto = function() { | |
252 | ctrl.autoSearch = !ctrl.autoSearch; | |
253 | if (ctrl.autoSearch && ctrl.stale) { | |
254 | ctrl.refreshAll(); | |
255 | } | |
0b1769c6 | 256 | $('.crm-search-auto-toggle').blur(); |
25523059 CW |
257 | }; |
258 | ||
259 | $scope.onChangeLimit = function() { | |
260 | // Refresh only if search has already been run | |
261 | if (ctrl.autoSearch || ctrl.results) { | |
262 | // Save page size in localStorage | |
263 | CRM.cache.set('searchPageSize', ctrl.limit); | |
264 | ctrl.refreshAll(); | |
265 | } | |
266 | }; | |
267 | ||
268 | function onChangeSelect(newSelect, oldSelect) { | |
7da9d079 CW |
269 | // When removing a column from SELECT, also remove from ORDER BY |
270 | _.each(_.difference(_.keys(ctrl.params.orderBy), newSelect), function(col) { | |
271 | delete ctrl.params.orderBy[col]; | |
272 | }); | |
25523059 CW |
273 | // Re-arranging or removing columns doesn't merit a refresh, only adding columns does |
274 | if (!oldSelect || _.difference(newSelect, oldSelect).length) { | |
275 | if (ctrl.autoSearch) { | |
276 | ctrl.refreshPage(); | |
277 | } else { | |
278 | ctrl.stale = true; | |
279 | } | |
280 | } | |
2c7e2f4b CW |
281 | if (ctrl.load) { |
282 | ctrl.load.saved = false; | |
283 | } | |
25523059 CW |
284 | } |
285 | ||
25523059 CW |
286 | function onChangeFilters() { |
287 | ctrl.stale = true; | |
288 | ctrl.selectedRows.length = 0; | |
2c7e2f4b CW |
289 | if (ctrl.load) { |
290 | ctrl.load.saved = false; | |
291 | } | |
25523059 CW |
292 | if (ctrl.autoSearch) { |
293 | ctrl.refreshAll(); | |
294 | } | |
295 | } | |
296 | ||
297 | $scope.selectAllRows = function() { | |
298 | // Deselect all | |
299 | if (ctrl.allRowsSelected) { | |
300 | ctrl.allRowsSelected = false; | |
301 | ctrl.selectedRows.length = 0; | |
302 | return; | |
303 | } | |
304 | // Select all | |
305 | ctrl.allRowsSelected = true; | |
306 | if (ctrl.page === 1 && ctrl.results[1].length < ctrl.limit) { | |
307 | ctrl.selectedRows = _.pluck(ctrl.results[1], 'id'); | |
308 | return; | |
309 | } | |
310 | // If more than one page of results, use ajax to fetch all ids | |
311 | $scope.loadingAllRows = true; | |
312 | var params = _.cloneDeep(ctrl.params); | |
313 | params.select = ['id']; | |
314 | crmApi4(ctrl.entity, 'get', params, ['id']).then(function(ids) { | |
315 | $scope.loadingAllRows = false; | |
316 | ctrl.selectedRows = _.toArray(ids); | |
317 | }); | |
318 | }; | |
319 | ||
320 | $scope.selectRow = function(row) { | |
321 | var index = ctrl.selectedRows.indexOf(row.id); | |
322 | if (index < 0) { | |
323 | ctrl.selectedRows.push(row.id); | |
324 | ctrl.allRowsSelected = (ctrl.rowCount === ctrl.selectedRows.length); | |
325 | } else { | |
326 | ctrl.allRowsSelected = false; | |
327 | ctrl.selectedRows.splice(index, 1); | |
328 | } | |
329 | }; | |
330 | ||
331 | $scope.isRowSelected = function(row) { | |
332 | return ctrl.allRowsSelected || _.includes(ctrl.selectedRows, row.id); | |
333 | }; | |
334 | ||
335 | this.getFieldLabel = function(col) { | |
336 | var info = searchMeta.parseExpr(col), | |
b6b6cb2d | 337 | label = info.field.label; |
25523059 CW |
338 | if (info.fn) { |
339 | label = '(' + info.fn.title + ') ' + label; | |
340 | } | |
341 | return label; | |
342 | }; | |
343 | ||
344 | // Is a column eligible to use an aggregate function? | |
345 | this.canAggregate = function(col) { | |
4f3a95d9 | 346 | var info = searchMeta.parseExpr(col); |
25523059 | 347 | // If the column is used for a groupBy, no |
4f3a95d9 | 348 | if (ctrl.params.groupBy.indexOf(info.path) > -1) { |
25523059 CW |
349 | return false; |
350 | } | |
351 | // If the entity this column belongs to is being grouped by id, then also no | |
25523059 CW |
352 | return ctrl.params.groupBy.indexOf(info.prefix + 'id') < 0; |
353 | }; | |
354 | ||
355 | $scope.formatResult = function formatResult(row, col) { | |
356 | var info = searchMeta.parseExpr(col), | |
357 | key = info.fn ? (info.fn.name + ':' + info.path + info.suffix) : col, | |
358 | value = row[key]; | |
7ce7b1cd | 359 | if (info.fn && info.fn.name === 'COUNT') { |
2d198bb4 CW |
360 | return value; |
361 | } | |
25523059 CW |
362 | return formatFieldValue(info.field, value); |
363 | }; | |
364 | ||
365 | function formatFieldValue(field, value) { | |
366 | var type = field.data_type; | |
7ce7b1cd CW |
367 | if (_.isArray(value)) { |
368 | return _.map(value, function(val) { | |
369 | return formatFieldValue(field, val); | |
370 | }).join(', '); | |
371 | } | |
25523059 CW |
372 | if (value && (type === 'Date' || type === 'Timestamp') && /^\d{4}-\d{2}-\d{2}/.test(value)) { |
373 | return CRM.utils.formatDate(value, null, type === 'Timestamp'); | |
374 | } | |
375 | else if (type === 'Boolean' && typeof value === 'boolean') { | |
376 | return value ? ts('Yes') : ts('No'); | |
377 | } | |
7ce7b1cd | 378 | else if (type === 'Money' && typeof value === 'number') { |
2d198bb4 CW |
379 | return CRM.formatMoney(value); |
380 | } | |
25523059 CW |
381 | return value; |
382 | } | |
383 | ||
25523059 CW |
384 | $scope.fieldsForGroupBy = function() { |
385 | return {results: getAllFields('', function(key) { | |
386 | return _.contains(ctrl.params.groupBy, key); | |
387 | }) | |
388 | }; | |
389 | }; | |
390 | ||
391 | $scope.fieldsForSelect = function() { | |
392 | return {results: getAllFields(':label', function(key) { | |
393 | return _.contains(ctrl.params.select, key); | |
394 | }) | |
395 | }; | |
396 | }; | |
397 | ||
398 | $scope.fieldsForWhere = function() { | |
399 | return {results: getAllFields(':name', _.noop)}; | |
400 | }; | |
401 | ||
402 | $scope.fieldsForHaving = function() { | |
403 | return {results: _.transform(ctrl.params.select, function(fields, name) { | |
404 | fields.push({id: name, text: ctrl.getFieldLabel(name)}); | |
405 | })}; | |
406 | }; | |
407 | ||
408 | function getDefaultSelect() { | |
c419e6ed CW |
409 | return _.filter(['id', 'display_name', 'label', 'title', 'location_type_id:label'], function(field) { |
410 | return !!searchMeta.getField(field, ctrl.entity); | |
411 | }); | |
25523059 CW |
412 | } |
413 | ||
414 | function getAllFields(suffix, disabledIf) { | |
415 | function formatFields(entityName, prefix) { | |
416 | return _.transform(searchMeta.getEntity(entityName).fields, function(result, field) { | |
417 | var item = { | |
418 | id: prefix + field.name + (field.options ? suffix : ''), | |
b6b6cb2d | 419 | text: field.label, |
25523059 CW |
420 | description: field.description |
421 | }; | |
422 | if (disabledIf(item.id)) { | |
423 | item.disabled = true; | |
424 | } | |
425 | result.push(item); | |
426 | }, []); | |
427 | } | |
428 | ||
429 | var mainEntity = searchMeta.getEntity(ctrl.entity), | |
430 | result = [{ | |
f2425270 | 431 | text: mainEntity.titlePlural, |
25523059 CW |
432 | icon: mainEntity.icon, |
433 | children: formatFields(ctrl.entity, '') | |
434 | }]; | |
435 | _.each(ctrl.params.join, function(join) { | |
436 | var joinName = join[0].split(' AS '), | |
437 | joinEntity = searchMeta.getEntity(joinName[0]); | |
438 | result.push({ | |
f2425270 | 439 | text: joinEntity.titlePlural + ' (' + joinName[1] + ')', |
25523059 CW |
440 | icon: joinEntity.icon, |
441 | children: formatFields(joinEntity.name, joinName[1] + '.') | |
442 | }); | |
443 | }); | |
444 | return result; | |
445 | } | |
446 | ||
447 | /** | |
448 | * Fetch pseudoconstants for main entity + joined entities | |
449 | * | |
450 | * Sets an optionsLoaded property on each entity to avoid duplicate requests | |
451 | */ | |
452 | function loadFieldOptions() { | |
453 | var mainEntity = searchMeta.getEntity(ctrl.entity), | |
454 | entities = {}; | |
455 | ||
456 | function enqueue(entity) { | |
457 | entity.optionsLoaded = false; | |
458 | entities[entity.name] = [entity.name, 'getFields', { | |
459 | loadOptions: CRM.vars.search.loadOptions, | |
460 | where: [['options', '!=', false]], | |
461 | select: ['options'] | |
462 | }, {name: 'options'}]; | |
463 | } | |
464 | ||
465 | if (typeof mainEntity.optionsLoaded === 'undefined') { | |
466 | enqueue(mainEntity); | |
467 | } | |
468 | _.each(ctrl.params.join, function(join) { | |
469 | var joinName = join[0].split(' AS '), | |
470 | joinEntity = searchMeta.getEntity(joinName[0]); | |
471 | if (typeof joinEntity.optionsLoaded === 'undefined') { | |
472 | enqueue(joinEntity); | |
473 | } | |
474 | }); | |
475 | if (!_.isEmpty(entities)) { | |
476 | crmApi4(entities).then(function(results) { | |
477 | _.each(results, function(fields, entityName) { | |
478 | var entity = searchMeta.getEntity(entityName); | |
479 | _.each(fields, function(options, fieldName) { | |
480 | _.find(entity.fields, {name: fieldName}).options = options; | |
481 | }); | |
482 | entity.optionsLoaded = true; | |
483 | }); | |
484 | }); | |
485 | } | |
486 | } | |
487 | ||
488 | this.$onInit = function() { | |
25523059 CW |
489 | $scope.$bindToRoute({ |
490 | expr: '$ctrl.params.select', | |
491 | param: 'select', | |
492 | format: 'json', | |
493 | default: getDefaultSelect() | |
494 | }); | |
495 | $scope.$watchCollection('$ctrl.params.select', onChangeSelect); | |
496 | ||
497 | $scope.$bindToRoute({ | |
498 | expr: '$ctrl.params.orderBy', | |
499 | param: 'orderBy', | |
500 | format: 'json', | |
501 | default: {} | |
502 | }); | |
25523059 CW |
503 | |
504 | $scope.$bindToRoute({ | |
505 | expr: '$ctrl.params.where', | |
506 | param: 'where', | |
507 | format: 'json', | |
508 | default: [], | |
509 | deep: true | |
510 | }); | |
511 | $scope.$watch('$ctrl.params.where', onChangeFilters, true); | |
512 | ||
513 | if (this.paramExists('groupBy')) { | |
514 | $scope.$bindToRoute({ | |
515 | expr: '$ctrl.params.groupBy', | |
516 | param: 'groupBy', | |
517 | format: 'json', | |
518 | default: [] | |
519 | }); | |
520 | } | |
521 | $scope.$watchCollection('$ctrl.params.groupBy', onChangeFilters); | |
522 | ||
523 | if (this.paramExists('join')) { | |
524 | $scope.$bindToRoute({ | |
525 | expr: '$ctrl.params.join', | |
526 | param: 'join', | |
527 | format: 'json', | |
528 | default: [], | |
529 | deep: true | |
530 | }); | |
531 | } | |
532 | $scope.$watch('$ctrl.params.join', onChangeFilters, true); | |
533 | ||
534 | if (this.paramExists('having')) { | |
535 | $scope.$bindToRoute({ | |
536 | expr: '$ctrl.params.having', | |
537 | param: 'having', | |
538 | format: 'json', | |
539 | default: [], | |
540 | deep: true | |
541 | }); | |
542 | } | |
543 | $scope.$watch('$ctrl.params.having', onChangeFilters, true); | |
27bed3cb | 544 | |
2c7e2f4b CW |
545 | if (this.load) { |
546 | this.params = this.load.api_params; | |
547 | $timeout(function() { | |
548 | ctrl.load.saved = true; | |
549 | }); | |
550 | } | |
551 | ||
27bed3cb | 552 | loadFieldOptions(); |
25523059 CW |
553 | }; |
554 | ||
555 | $scope.saveGroup = function() { | |
25523059 CW |
556 | var model = { |
557 | title: '', | |
558 | description: '', | |
559 | visibility: 'User and User Admin Only', | |
560 | group_type: [], | |
2c7e2f4b | 561 | id: ctrl.load ? ctrl.load.id : null, |
73d7dbcd CW |
562 | api_entity: ctrl.entity, |
563 | api_params: _.cloneDeep(angular.extend({}, ctrl.params, {version: 4})) | |
25523059 | 564 | }; |
73d7dbcd | 565 | delete model.api_params.orderBy; |
23e56526 CW |
566 | if (ctrl.load && ctrl.load.api_params && ctrl.load.api_params.select && ctrl.load.api_params.select[0]) { |
567 | model.api_params.select.unshift(ctrl.load.api_params.select[0]); | |
73d7dbcd | 568 | } |
25523059 CW |
569 | var options = CRM.utils.adjustDialogDefaults({ |
570 | autoOpen: false, | |
571 | title: ts('Save smart group') | |
572 | }); | |
2c7e2f4b CW |
573 | dialogService.open('saveSearchDialog', '~/search/saveSmartGroup.html', model, options) |
574 | .then(function() { | |
575 | if (ctrl.load) { | |
576 | ctrl.load.saved = true; | |
577 | } | |
578 | }); | |
25523059 CW |
579 | }; |
580 | } | |
581 | }); | |
582 | ||
583 | })(angular, CRM.$, CRM._); |