Merge pull request #22188 from totten/master-uninstall
[civicrm-core.git] / ext / search_kit / 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',
7d527c18 9 controller: function($scope, $element, $location, $timeout, crmApi4, dialogService, searchMeta) {
33e81cf6 10 var ts = $scope.ts = CRM.ts('org.civicrm.search_kit'),
3dfd2744 11 ctrl = this,
02c7fc51 12 afformLoad,
3dfd2744 13 fieldsForJoinGetters = {};
5fcd63f4
CW
14
15 this.DEFAULT_AGGREGATE_FN = 'GROUP_CONCAT';
02c7fc51
CW
16 this.afformEnabled = CRM.crmSearchAdmin.afformEnabled;
17 this.afformAdminEnabled = CRM.crmSearchAdmin.afformAdminEnabled;
ecb9d1eb 18 this.displayTypes = _.indexBy(CRM.crmSearchAdmin.displayTypes, 'id');
5c952e51
CW
19 this.searchDisplayPath = CRM.url('civicrm/search');
20 this.afformPath = CRM.url('civicrm/admin/afform');
25523059 21
266e8deb
CW
22 $scope.controls = {tab: 'compose', joinType: 'LEFT'};
23 $scope.joinTypes = [
24 {k: 'LEFT', v: ts('With (optional)')},
25 {k: 'INNER', v: ts('With (required)')},
26 {k: 'EXCLUDE', v: ts('Without')},
27 ];
a019205f
CW
28 $scope.getEntity = searchMeta.getEntity;
29 $scope.getField = searchMeta.getField;
25523059
CW
30 this.perm = {
31 editGroups: CRM.checkPerm('edit groups')
32 };
33
2894db84
CW
34 this.$onInit = function() {
35 this.entityTitle = searchMeta.getEntity(this.savedSearch.api_entity).title_plural;
36
f9197b41 37 this.savedSearch.displays = this.savedSearch.displays || [];
44402a2e 38 this.savedSearch.groups = this.savedSearch.groups || [];
94d356ed 39 this.savedSearch.tag_id = this.savedSearch.tag_id || [];
44402a2e 40 this.groupExists = !!this.savedSearch.groups.length;
f9197b41 41
b40e49df 42 if (!this.savedSearch.id) {
2b02bf9b
CW
43 var defaults = {
44 version: 4,
45 select: getDefaultSelect(),
46 orderBy: {},
47 where: [],
48 };
49 _.each(['groupBy', 'join', 'having'], function(param) {
50 if (ctrl.paramExists(param)) {
51 defaults[param] = [];
52 }
53 });
54
b40e49df
CW
55 $scope.$bindToRoute({
56 param: 'params',
57 expr: '$ctrl.savedSearch.api_params',
b1603dbd 58 deep: true,
2b02bf9b 59 default: defaults
b40e49df 60 });
2894db84
CW
61 }
62
94d356ed 63 $scope.mainEntitySelect = searchMeta.getPrimaryAndSecondaryEntitySelect();
4736288b 64
2894db84
CW
65 $scope.$watchCollection('$ctrl.savedSearch.api_params.select', onChangeSelect);
66
44402a2e
CW
67 $scope.$watch('$ctrl.savedSearch', onChangeAnything, true);
68
2badf248
CW
69 // After watcher runs for the first time and messes up the status, set it correctly
70 $timeout(function() {
71 $scope.status = ctrl.savedSearch && ctrl.savedSearch.id ? 'saved' : 'unsaved';
72 });
44402a2e 73
2894db84 74 loadFieldOptions();
02c7fc51 75 loadAfforms();
2894db84 76 };
25523059 77
44402a2e
CW
78 function onChangeAnything() {
79 $scope.status = 'unsaved';
80 }
81
82 this.save = function() {
2badf248
CW
83 if (!validate()) {
84 return;
85 }
44402a2e
CW
86 $scope.status = 'saving';
87 var params = _.cloneDeep(ctrl.savedSearch),
88 apiCalls = {},
89 chain = {};
90 if (ctrl.groupExists) {
91 chain.groups = ['Group', 'save', {defaults: {saved_search_id: '$id'}, records: params.groups}];
92 delete params.groups;
93 } else if (params.id) {
94 apiCalls.deleteGroup = ['Group', 'delete', {where: [['saved_search_id', '=', params.id]]}];
95 }
5af55119 96 _.remove(params.displays, {trashed: true});
44402a2e
CW
97 if (params.displays && params.displays.length) {
98 chain.displays = ['SearchDisplay', 'replace', {where: [['saved_search_id', '=', '$id']], records: params.displays}];
99 } else if (params.id) {
100 apiCalls.deleteDisplays = ['SearchDisplay', 'delete', {where: [['saved_search_id', '=', params.id]]}];
101 }
102 delete params.displays;
94d356ed
CW
103 if (params.tag_id && params.tag_id.length) {
104 chain.tag_id = ['EntityTag', 'replace', {
105 where: [['entity_id', '=', '$id'], ['entity_table', '=', 'civicrm_saved_search']],
106 records: _.transform(params.tag_id, function(records, id) {records.push({tag_id: id});})
107 }];
108 } else if (params.id) {
109 chain.tag_id = ['EntityTag', 'delete', {
110 where: [['entity_id', '=', '$id'], ['entity_table', '=', 'civicrm_saved_search']]
111 }];
112 }
113 delete params.tag_id;
44402a2e
CW
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) {
6f0c7312 138 var count = _.filter(ctrl.savedSearch.displays, {type: type}).length;
f9197b41 139 ctrl.savedSearch.displays.push({
44402a2e 140 type: type,
6f0c7312 141 label: ctrl.displayTypes[type].label + (count ? ' ' + (++count) : '')
f9197b41 142 });
4b01551f
CW
143 $scope.selectTab('display_' + (ctrl.savedSearch.displays.length - 1));
144 };
145
146 this.removeDisplay = function(index) {
147 var display = ctrl.savedSearch.displays[index];
148 if (display.id) {
149 display.trashed = !display.trashed;
2badf248
CW
150 if ($scope.controls.tab === ('display_' + index) && display.trashed) {
151 $scope.selectTab('compose');
152 } else if (!display.trashed) {
153 $scope.selectTab('display_' + index);
154 }
02c7fc51
CW
155 if (display.trashed && afformLoad) {
156 afformLoad.then(function() {
5c952e51
CW
157 var displayForms = _.filter(ctrl.afforms, function(form) {
158 return _.includes(form.displays, ctrl.savedSearch.name + '.' + display.name);
159 });
160 if (displayForms.length) {
161 var msg = displayForms.length === 1 ?
162 ts('Form "%1" will be deleted if the embedded display "%2" is deleted.', {1: displayForms[0].title, 2: display.label}) :
163 ts('%1 forms will be deleted if the embedded display "%2" is deleted.', {1: displayForms.length, 2: display.label});
02c7fc51
CW
164 CRM.alert(msg, ts('Display embedded'), 'alert');
165 }
166 });
167 }
4b01551f
CW
168 } else {
169 $scope.selectTab('compose');
170 ctrl.savedSearch.displays.splice(index, 1);
171 }
172 };
173
174 this.addGroup = function() {
44402a2e 175 ctrl.savedSearch.groups.push({
4b01551f
CW
176 title: '',
177 description: '',
178 visibility: 'User and User Admin Only',
179 group_type: []
44402a2e 180 });
4b01551f
CW
181 ctrl.groupExists = true;
182 $scope.selectTab('group');
183 };
184
185 $scope.selectTab = function(tab) {
186 if (tab === 'group') {
a019205f 187 loadFieldOptions('Group');
4b01551f
CW
188 $scope.smartGroupColumns = searchMeta.getSmartGroupColumns(ctrl.savedSearch.api_entity, ctrl.savedSearch.api_params);
189 var smartGroupColumns = _.map($scope.smartGroupColumns, 'id');
190 if (smartGroupColumns.length && !_.includes(smartGroupColumns, ctrl.savedSearch.api_params.select[0])) {
191 ctrl.savedSearch.api_params.select.unshift(smartGroupColumns[0]);
192 }
193 }
194 ctrl.savedSearch.api_params.select = _.uniq(ctrl.savedSearch.api_params.select);
195 $scope.controls.tab = tab;
196 };
197
198 this.removeGroup = function() {
199 ctrl.groupExists = !ctrl.groupExists;
a019205f 200 $scope.status = 'unsaved';
44402a2e
CW
201 if (!ctrl.groupExists && (!ctrl.savedSearch.groups.length || !ctrl.savedSearch.groups[0].id)) {
202 ctrl.savedSearch.groups.length = 0;
4b01551f 203 }
2badf248
CW
204 if ($scope.controls.tab === 'group') {
205 $scope.selectTab('compose');
206 }
f9197b41
CW
207 };
208
994168e1
CW
209 function addNum(name, num) {
210 return name + (num < 10 ? '_0' : '_') + num;
211 }
212
213 function getExistingJoins() {
214 return _.transform(ctrl.savedSearch.api_params.join || [], function(joins, join) {
215 joins[join[0].split(' AS ')[1]] = searchMeta.getJoin(join[0]);
216 }, {});
217 }
218
4f0729ed
CW
219 $scope.getJoin = searchMeta.getJoin;
220
25523059 221 $scope.getJoinEntities = function() {
994168e1
CW
222 var existingJoins = getExistingJoins();
223
224 function addEntityJoins(entity, stack, baseEntity) {
225 return _.transform(CRM.crmSearchAdmin.joins[entity], function(joinEntities, join) {
226 var num = 0;
227 // Add all joins that don't just point directly back to the original entity
228 if (!(baseEntity === join.entity && !join.multi)) {
229 do {
230 appendJoin(joinEntities, join, ++num, stack, entity);
231 } while (addNum((stack ? stack + '_' : '') + join.alias, num) in existingJoins);
232 }
233 }, []);
234 }
235
236 function appendJoin(collection, join, num, stack, baseEntity) {
237 var alias = addNum((stack ? stack + '_' : '') + join.alias, num),
238 opt = {
239 id: join.entity + ' AS ' + alias,
4f0729ed 240 description: join.description,
994168e1
CW
241 text: join.label + (num > 1 ? ' ' + num : ''),
242 icon: searchMeta.getEntity(join.entity).icon,
243 disabled: alias in existingJoins
244 };
245 if (alias in existingJoins) {
246 opt.children = addEntityJoins(join.entity, (stack ? stack + '_' : '') + alias, baseEntity);
25523059 247 }
994168e1
CW
248 collection.push(opt);
249 }
250
251 return {results: addEntityJoins(ctrl.savedSearch.api_entity)};
25523059
CW
252 };
253
7c219eb3
CW
254 this.addJoin = function(value) {
255 if (value) {
256 ctrl.savedSearch.api_params.join = ctrl.savedSearch.api_params.join || [];
257 var join = searchMeta.getJoin(value),
258 entity = searchMeta.getEntity(join.entity),
259 params = [value, $scope.controls.joinType || 'LEFT'];
260 _.each(_.cloneDeep(join.conditions), function(condition) {
261 params.push(condition);
262 });
263 _.each(_.cloneDeep(join.defaults), function(condition) {
264 params.push(condition);
265 });
266 ctrl.savedSearch.api_params.join.push(params);
267 if (entity.label_field && $scope.controls.joinType !== 'EXCLUDE') {
268 ctrl.savedSearch.api_params.select.push(join.alias + '.' + entity.label_field);
25523059 269 }
7c219eb3
CW
270 loadFieldOptions();
271 }
25523059
CW
272 };
273
c15d4a0c
CW
274 // Remove an explicit join + all SELECT, WHERE & other JOINs that use it
275 this.removeJoin = function(index) {
276 var alias = searchMeta.getJoin(ctrl.savedSearch.api_params.join[index][0]).alias;
277 ctrl.clearParam('join', index);
266e8deb
CW
278 removeJoinStuff(alias);
279 };
280
281 function removeJoinStuff(alias) {
c15d4a0c 282 _.remove(ctrl.savedSearch.api_params.select, function(item) {
133f6a11
CW
283 var pattern = new RegExp('\\b' + alias + '\\.');
284 return pattern.test(item.split(' AS ')[0]);
c15d4a0c
CW
285 });
286 _.remove(ctrl.savedSearch.api_params.where, function(clause) {
287 return clauseUsesJoin(clause, alias);
288 });
289 _.eachRight(ctrl.savedSearch.api_params.join, function(item, i) {
266e8deb
CW
290 var joinAlias = searchMeta.getJoin(item[0]).alias;
291 if (joinAlias !== alias && joinAlias.indexOf(alias) === 0) {
c15d4a0c
CW
292 ctrl.removeJoin(i);
293 }
294 });
266e8deb
CW
295 }
296
297 this.changeJoinType = function(join) {
298 if (join[1] === 'EXCLUDE') {
299 removeJoinStuff(searchMeta.getJoin(join[0]).alias);
300 }
c15d4a0c
CW
301 };
302
25523059 303 $scope.changeGroupBy = function(idx) {
1cb03463 304 // When clearing a selection
2894db84 305 if (!ctrl.savedSearch.api_params.groupBy[idx]) {
25523059
CW
306 ctrl.clearParam('groupBy', idx);
307 }
1cb03463 308 reconcileAggregateColumns();
25523059
CW
309 };
310
1cb03463
CW
311 function reconcileAggregateColumns() {
312 _.each(ctrl.savedSearch.api_params.select, function(col, pos) {
313 var info = searchMeta.parseExpr(col),
7891ca94 314 fieldExpr = (_.findWhere(info.args, {type: 'field'}) || {}).value;
1cb03463
CW
315 if (ctrl.canAggregate(col)) {
316 // Ensure all non-grouped columns are aggregated if using GROUP BY
317 if (!info.fn || info.fn.category !== 'aggregate') {
2fe33e6c 318 ctrl.savedSearch.api_params.select[pos] = ctrl.DEFAULT_AGGREGATE_FN + '(DISTINCT ' + fieldExpr + ') AS ' + ctrl.DEFAULT_AGGREGATE_FN + '_' + fieldExpr.replace(/[.:]/g, '_');
1cb03463
CW
319 }
320 } else {
321 // Remove aggregate functions when no grouping
322 if (info.fn && info.fn.category === 'aggregate') {
323 ctrl.savedSearch.api_params.select[pos] = fieldExpr;
324 }
325 }
326 });
327 }
328
c15d4a0c
CW
329 function clauseUsesJoin(clause, alias) {
330 if (clause[0].indexOf(alias + '.') === 0) {
331 return true;
332 }
333 if (_.isArray(clause[1])) {
334 return clause[1].some(function(subClause) {
335 return clauseUsesJoin(subClause, alias);
336 });
337 }
338 return false;
339 }
340
341 // Returns true if a clause contains one of the
342 function clauseUsesFields(clause, fields) {
c15d4a0c
CW
343 if (!fields || !fields.length) {
344 return false;
345 }
346 if (_.includes(fields, clause[0])) {
347 return true;
348 }
349 if (_.isArray(clause[1])) {
350 return clause[1].some(function(subClause) {
351 return clauseUsesField(subClause, fields);
352 });
353 }
354 return false;
355 }
356
2badf248
CW
357 function validate() {
358 var errors = [],
359 errorEl,
360 label,
361 tab;
362 if (!ctrl.savedSearch.label) {
363 errorEl = '#crm-saved-search-label';
364 label = ts('Search Label');
365 errors.push(ts('%1 is a required field.', {1: label}));
366 }
367 if (ctrl.groupExists && !ctrl.savedSearch.groups[0].title) {
368 errorEl = '#crm-search-admin-group-title';
369 label = ts('Group Title');
370 errors.push(ts('%1 is a required field.', {1: label}));
371 tab = 'group';
372 }
373 _.each(ctrl.savedSearch.displays, function(display, index) {
374 if (!display.trashed && !display.label) {
375 errorEl = '#crm-search-admin-display-label';
376 label = ts('Display Label');
377 errors.push(ts('%1 is a required field.', {1: label}));
378 tab = 'display_' + index;
379 }
380 });
381 if (errors.length) {
382 if (tab) {
383 $scope.selectTab(tab);
384 }
385 $(errorEl).crmError(errors.join('<br>'), ts('Error Saving'), {expires: 5000});
386 }
387 return !errors.length;
388 }
389
7c219eb3
CW
390 this.addParam = function(name, value) {
391 if (value && !_.contains(ctrl.savedSearch.api_params[name], value)) {
392 ctrl.savedSearch.api_params[name].push(value);
1cb03463
CW
393 // This needs to be called when adding a field as well as changing groupBy
394 reconcileAggregateColumns();
25523059 395 }
25523059
CW
396 };
397
398 // Deletes an item from an array param
399 this.clearParam = function(name, idx) {
173405e2
CW
400 if (name === 'select') {
401 // Function selectors use `ng-repeat` with `track by $index` so must be refreshed when splicing the array
402 ctrl.hideFuncitons();
403 }
2894db84 404 ctrl.savedSearch.api_params[name].splice(idx, 1);
25523059
CW
405 };
406
173405e2
CW
407 this.hideFuncitons = function() {
408 $scope.controls.showFunctions = false;
409 };
410
25523059 411 function onChangeSelect(newSelect, oldSelect) {
57fa023b
CW
412 // When removing a column from SELECT, also remove from ORDER BY & HAVING
413 _.each(_.difference(oldSelect, newSelect), function(col) {
414 col = _.last(col.split(' AS '));
2894db84 415 delete ctrl.savedSearch.api_params.orderBy[col];
c15d4a0c 416 _.remove(ctrl.savedSearch.api_params.having, function(clause) {
57fa023b 417 return clauseUsesFields(clause, [col]);
c15d4a0c 418 });
57fa023b 419 });
3b801069
CW
420 }
421
03b55607 422 this.getFieldLabel = searchMeta.getDefaultLabel;
25523059
CW
423
424 // Is a column eligible to use an aggregate function?
425 this.canAggregate = function(col) {
f9cf8797
CW
426 // If the query does not use grouping, never
427 if (!ctrl.savedSearch.api_params.groupBy.length) {
428 return false;
429 }
7891ca94
CW
430 var arg = _.findWhere(searchMeta.parseExpr(col).args, {type: 'field'}) || {};
431 // If the column is not a database field, no
432 if (!arg.field || !arg.field.entity || arg.field.type !== 'Field') {
433 return false;
434 }
25523059 435 // If the column is used for a groupBy, no
7891ca94 436 if (ctrl.savedSearch.api_params.groupBy.indexOf(arg.path) > -1) {
25523059
CW
437 return false;
438 }
b7d8183b 439 // If the entity this column belongs to is being grouped by primary key, then also no
7891ca94
CW
440 var idField = searchMeta.getEntity(arg.field.entity).primary_key[0];
441 return ctrl.savedSearch.api_params.groupBy.indexOf(arg.prefix + idField) < 0;
25523059
CW
442 };
443
25523059 444 $scope.fieldsForGroupBy = function() {
a1415a02 445 return {results: ctrl.getAllFields('', ['Field', 'Custom'], function(key) {
2894db84 446 return _.contains(ctrl.savedSearch.api_params.groupBy, key);
25523059
CW
447 })
448 };
449 };
450
3dfd2744 451 function getFieldsForJoin(joinEntity) {
ba149e21 452 return {results: ctrl.getAllFields(':name', ['Field'], null, joinEntity)};
3dfd2744
CW
453 }
454
ba149e21 455 // @return {function}
3dfd2744
CW
456 $scope.fieldsForJoin = function(joinEntity) {
457 if (!fieldsForJoinGetters[joinEntity]) {
458 fieldsForJoinGetters[joinEntity] = _.wrap(joinEntity, getFieldsForJoin);
459 }
460 return fieldsForJoinGetters[joinEntity];
461 };
462
25523059 463 $scope.fieldsForWhere = function() {
9e827e8e 464 return {results: ctrl.getAllFields(':name')};
25523059
CW
465 };
466
467 $scope.fieldsForHaving = function() {
9e827e8e 468 return {results: ctrl.getSelectFields()};
25523059
CW
469 };
470
4b01551f 471 // Sets the default select clause based on commonly-named fields
25523059 472 function getDefaultSelect() {
b0f5b67a
CW
473 var entity = searchMeta.getEntity(ctrl.savedSearch.api_entity);
474 return _.transform(entity.fields, function(defaultSelect, field) {
475 if (field.name === 'id' || field.name === entity.label_field) {
476 defaultSelect.push(field.name);
4b01551f 477 }
c419e6ed 478 });
25523059
CW
479 }
480
a1415a02 481 this.getAllFields = function(suffix, allowedTypes, disabledIf, topJoin) {
9e827e8e 482 disabledIf = disabledIf || _.noop;
7d527c18
CW
483
484 function formatEntityFields(entityName, join) {
4f0729ed
CW
485 var prefix = join ? join.alias + '.' : '',
486 result = [];
487
4f0729ed
CW
488 // Add extra searchable fields from bridge entity
489 if (join && join.bridge) {
7d527c18 490 formatFields(_.filter(searchMeta.getEntity(join.bridge).fields, function(field) {
07e7a46b 491 return (field.name !== 'id' && field.name !== 'entity_id' && field.name !== 'entity_table' && !field.fk_entity);
7d527c18 492 }), result, prefix);
4f0729ed
CW
493 }
494
7d527c18
CW
495 formatFields(searchMeta.getEntity(entityName).fields, result, prefix);
496 return result;
497 }
498
499 function formatFields(fields, result, prefix) {
500 prefix = typeof prefix === 'undefined' ? '' : prefix;
501 _.each(fields, function(field) {
502 var item = {
503 id: prefix + field.name + (field.options ? suffix : ''),
504 text: field.label,
505 description: field.description
506 };
507 if (disabledIf(item.id)) {
508 item.disabled = true;
509 }
510 if (!allowedTypes || _.includes(allowedTypes, field.type)) {
511 result.push(item);
512 }
513 });
4f0729ed 514 return result;
25523059
CW
515 }
516
2894db84 517 var mainEntity = searchMeta.getEntity(ctrl.savedSearch.api_entity),
3dfd2744
CW
518 joinEntities = _.map(ctrl.savedSearch.api_params.join, 0),
519 result = [];
520
521 function addJoin(join) {
522 var joinInfo = searchMeta.getJoin(join),
4f0729ed 523 joinEntity = searchMeta.getEntity(joinInfo.entity);
25523059 524 result.push({
4f0729ed
CW
525 text: joinInfo.label,
526 description: joinInfo.description,
25523059 527 icon: joinEntity.icon,
7d527c18 528 children: formatEntityFields(joinEntity.name, joinInfo)
25523059 529 });
3dfd2744
CW
530 }
531
532 // Place specified join at top of list
533 if (topJoin) {
534 addJoin(topJoin);
535 _.pull(joinEntities, topJoin);
536 }
537
538 result.push({
539 text: mainEntity.title_plural,
540 icon: mainEntity.icon,
7d527c18 541 children: formatEntityFields(ctrl.savedSearch.api_entity)
25523059 542 });
7d527c18
CW
543
544 // Include SearchKit's pseudo-fields if specifically requested
545 if (allowedTypes && _.includes(allowedTypes, 'Pseudo')) {
546 result.push({
547 text: ts('Extra'),
548 icon: 'fa-gear',
549 children: formatFields(CRM.crmSearchAdmin.pseudoFields, [])
550 });
551 }
552
3dfd2744 553 _.each(joinEntities, addJoin);
25523059 554 return result;
9e827e8e
CW
555 };
556
557 this.getSelectFields = function(disabledIf) {
558 disabledIf = disabledIf || _.noop;
559 return _.transform(ctrl.savedSearch.api_params.select, function(fields, name) {
560 var info = searchMeta.parseExpr(name);
561 var item = {
2b055918 562 id: info.alias,
9e827e8e 563 text: ctrl.getFieldLabel(name),
7891ca94 564 description: info.fn ? info.fn.description : info.args[0].field && info.args[0].field.description
9e827e8e
CW
565 };
566 if (disabledIf(item.id)) {
567 item.disabled = true;
568 }
569 fields.push(item);
570 });
571 };
25523059 572
7d527c18
CW
573 this.isPseudoField = function(name) {
574 return _.findIndex(CRM.crmSearchAdmin.pseudoFields, {name: name}) >= 0;
575 };
576
25523059
CW
577 /**
578 * Fetch pseudoconstants for main entity + joined entities
579 *
580 * Sets an optionsLoaded property on each entity to avoid duplicate requests
a019205f
CW
581 *
582 * @var string entity - optional additional entity to load
25523059 583 */
a019205f 584 function loadFieldOptions(entity) {
2894db84 585 var mainEntity = searchMeta.getEntity(ctrl.savedSearch.api_entity),
25523059
CW
586 entities = {};
587
588 function enqueue(entity) {
589 entity.optionsLoaded = false;
590 entities[entity.name] = [entity.name, 'getFields', {
22601c92 591 loadOptions: ['id', 'name', 'label', 'description', 'color', 'icon'],
25523059
CW
592 where: [['options', '!=', false]],
593 select: ['options']
594 }, {name: 'options'}];
595 }
596
597 if (typeof mainEntity.optionsLoaded === 'undefined') {
598 enqueue(mainEntity);
599 }
a019205f
CW
600
601 // Optional additional entity
602 if (entity && typeof searchMeta.getEntity(entity).optionsLoaded === 'undefined') {
603 enqueue(searchMeta.getEntity(entity));
604 }
605
2894db84 606 _.each(ctrl.savedSearch.api_params.join, function(join) {
4f0729ed
CW
607 var joinInfo = searchMeta.getJoin(join[0]),
608 joinEntity = searchMeta.getEntity(joinInfo.entity),
609 bridgeEntity = joinInfo.bridge ? searchMeta.getEntity(joinInfo.bridge) : null;
25523059
CW
610 if (typeof joinEntity.optionsLoaded === 'undefined') {
611 enqueue(joinEntity);
612 }
4f0729ed
CW
613 if (bridgeEntity && typeof bridgeEntity.optionsLoaded === 'undefined') {
614 enqueue(bridgeEntity);
615 }
25523059
CW
616 });
617 if (!_.isEmpty(entities)) {
618 crmApi4(entities).then(function(results) {
619 _.each(results, function(fields, entityName) {
620 var entity = searchMeta.getEntity(entityName);
621 _.each(fields, function(options, fieldName) {
622 _.find(entity.fields, {name: fieldName}).options = options;
623 });
624 entity.optionsLoaded = true;
625 });
626 });
627 }
628 }
629
957358aa 630 // Build a list of all possible links to main entity & join entities
9446fbaa 631 // @return {Array}
957358aa
CW
632 this.buildLinks = function() {
633 function addTitle(link, entityName) {
9446fbaa 634 link.text = link.text.replace('%1', entityName);
957358aa
CW
635 }
636
637 // Links to main entity
638 var mainEntity = searchMeta.getEntity(ctrl.savedSearch.api_entity),
9446fbaa 639 links = _.cloneDeep(mainEntity.links || []);
957358aa
CW
640 _.each(links, function(link) {
641 link.join = '';
642 addTitle(link, mainEntity.title);
643 });
644 // Links to explicitly joined entities
645 _.each(ctrl.savedSearch.api_params.join, function(joinClause) {
646 var join = searchMeta.getJoin(joinClause[0]),
647 joinEntity = searchMeta.getEntity(join.entity),
648 bridgeEntity = _.isString(joinClause[2]) ? searchMeta.getEntity(joinClause[2]) : null;
9446fbaa 649 _.each(_.cloneDeep(joinEntity.links), function(link) {
957358aa
CW
650 link.join = join.alias;
651 addTitle(link, join.label);
652 links.push(link);
653 });
9446fbaa 654 _.each(_.cloneDeep(bridgeEntity && bridgeEntity.links), function(link) {
957358aa
CW
655 link.join = join.alias;
656 addTitle(link, join.label + (bridgeEntity.bridge_title ? ' ' + bridgeEntity.bridge_title : ''));
657 links.push(link);
658 });
659 });
660 // Links to implicit joins
661 _.each(ctrl.savedSearch.api_params.select, function(fieldName) {
662 if (!_.includes(fieldName, ' AS ')) {
7891ca94 663 var info = searchMeta.parseExpr(fieldName).args[0];
7d527c18 664 if (info.field && !info.suffix && !info.fn && info.field.type === 'Field' && (info.field.fk_entity || info.field.name !== info.field.fieldName)) {
957358aa 665 var idFieldName = info.field.fk_entity ? fieldName : fieldName.substr(0, fieldName.lastIndexOf('.')),
7891ca94 666 idField = searchMeta.parseExpr(idFieldName).args[0].field;
957358aa
CW
667 if (!ctrl.canAggregate(idFieldName)) {
668 var joinEntity = searchMeta.getEntity(idField.fk_entity),
669 label = (idField.join ? idField.join.label + ': ' : '') + (idField.input_attrs && idField.input_attrs.label || idField.label);
9446fbaa 670 _.each(_.cloneDeep(joinEntity && joinEntity.links), function(link) {
957358aa
CW
671 link.join = idFieldName;
672 addTitle(link, label);
673 links.push(link);
674 });
675 }
676 }
677 }
678 });
9446fbaa 679 return links;
957358aa
CW
680 };
681
02c7fc51 682 function loadAfforms() {
5c952e51 683 ctrl.afforms = null;
02c7fc51
CW
684 if (ctrl.afformEnabled && ctrl.savedSearch.id) {
685 var findDisplays = _.transform(ctrl.savedSearch.displays, function(findDisplays, display) {
686 if (display.id && display.name) {
687 findDisplays.push(['search_displays', 'CONTAINS', ctrl.savedSearch.name + '.' + display.name]);
688 }
ea04af0c
CW
689 }, [['search_displays', 'CONTAINS', ctrl.savedSearch.name]]);
690 afformLoad = crmApi4('Afform', 'get', {
691 select: ['name', 'title', 'search_displays'],
692 where: [['OR', findDisplays]]
693 }).then(function(afforms) {
5c952e51 694 ctrl.afforms = [];
ea04af0c 695 _.each(afforms, function(afform) {
5c952e51
CW
696 ctrl.afforms.push({
697 title: afform.title,
698 displays: afform.search_displays,
699 link: ctrl.afformAdminEnabled ? CRM.url('civicrm/admin/afform#/edit/' + afform.name) : '',
02c7fc51
CW
700 });
701 });
5c952e51 702 ctrl.afformCount = ctrl.afforms.length;
ea04af0c 703 });
02c7fc51
CW
704 }
705 }
706
5c952e51 707 // Creating an Afform opens a new tab, so when switching back after > 10 sec, re-check for Afforms
02c7fc51
CW
708 $(window).on('focus', _.debounce(function() {
709 $scope.$apply(loadAfforms);
710 }, 10000, {leading: true, trailing: false}));
711
25523059
CW
712 }
713 });
714
715})(angular, CRM.$, CRM._);