1 (function(angular
, $, _
) {
4 angular
.module('crmSearchAdmin').component('crmSearchAdmin', {
8 templateUrl
: '~/crmSearchAdmin/crmSearchAdmin.html',
9 controller: function($scope
, $element
, $location
, $timeout
, crmApi4
, dialogService
, searchMeta
) {
10 var ts
= $scope
.ts
= CRM
.ts('org.civicrm.search_kit'),
13 fieldsForJoinGetters
= {};
15 this.DEFAULT_AGGREGATE_FN
= 'GROUP_CONCAT';
16 this.afformEnabled
= CRM
.crmSearchAdmin
.afformEnabled
;
17 this.afformAdminEnabled
= CRM
.crmSearchAdmin
.afformAdminEnabled
;
18 this.displayTypes
= _
.indexBy(CRM
.crmSearchAdmin
.displayTypes
, 'id');
19 this.searchDisplayPath
= CRM
.url('civicrm/search');
20 this.afformPath
= CRM
.url('civicrm/admin/afform');
22 $scope
.controls
= {tab
: 'compose', joinType
: 'LEFT'};
24 {k
: 'LEFT', v
: ts('With (optional)')},
25 {k
: 'INNER', v
: ts('With (required)')},
26 {k
: 'EXCLUDE', v
: ts('Without')},
28 $scope
.getEntity
= searchMeta
.getEntity
;
29 $scope
.getField
= searchMeta
.getField
;
31 editGroups
: CRM
.checkPerm('edit groups')
34 this.$onInit = function() {
35 this.entityTitle
= searchMeta
.getEntity(this.savedSearch
.api_entity
).title_plural
;
37 this.savedSearch
.displays
= this.savedSearch
.displays
|| [];
38 this.savedSearch
.groups
= this.savedSearch
.groups
|| [];
39 this.savedSearch
.tag_id
= this.savedSearch
.tag_id
|| [];
40 this.groupExists
= !!this.savedSearch
.groups
.length
;
42 if (!this.savedSearch
.id
) {
45 expr
: '$ctrl.savedSearch.api_params',
49 select
: getDefaultSelect(),
56 $scope
.mainEntitySelect
= searchMeta
.getPrimaryAndSecondaryEntitySelect();
58 $scope
.$watchCollection('$ctrl.savedSearch.api_params.select', onChangeSelect
);
60 if (this.paramExists('groupBy')) {
61 this.savedSearch
.api_params
.groupBy
= this.savedSearch
.api_params
.groupBy
|| [];
64 if (this.paramExists('join')) {
65 this.savedSearch
.api_params
.join
= this.savedSearch
.api_params
.join
|| [];
68 if (this.paramExists('having')) {
69 this.savedSearch
.api_params
.having
= this.savedSearch
.api_params
.having
|| [];
72 $scope
.$watch('$ctrl.savedSearch', onChangeAnything
, true);
74 // After watcher runs for the first time and messes up the status, set it correctly
76 $scope
.status
= ctrl
.savedSearch
&& ctrl
.savedSearch
.id
? 'saved' : 'unsaved';
83 function onChangeAnything() {
84 $scope
.status
= 'unsaved';
87 this.save = function() {
91 $scope
.status
= 'saving';
92 var params
= _
.cloneDeep(ctrl
.savedSearch
),
95 if (ctrl
.groupExists
) {
96 chain
.groups
= ['Group', 'save', {defaults
: {saved_search_id
: '$id'}, records
: params
.groups
}];
98 } else if (params
.id
) {
99 apiCalls
.deleteGroup
= ['Group', 'delete', {where
: [['saved_search_id', '=', params
.id
]]}];
101 _
.remove(params
.displays
, {trashed
: true});
102 if (params
.displays
&& params
.displays
.length
) {
103 chain
.displays
= ['SearchDisplay', 'replace', {where
: [['saved_search_id', '=', '$id']], records
: params
.displays
}];
104 } else if (params
.id
) {
105 apiCalls
.deleteDisplays
= ['SearchDisplay', 'delete', {where
: [['saved_search_id', '=', params
.id
]]}];
107 delete params
.displays
;
108 if (params
.tag_id
&& params
.tag_id
.length
) {
109 chain
.tag_id
= ['EntityTag', 'replace', {
110 where
: [['entity_id', '=', '$id'], ['entity_table', '=', 'civicrm_saved_search']],
111 records
: _
.transform(params
.tag_id
, function(records
, id
) {records
.push({tag_id
: id
});})
113 } else if (params
.id
) {
114 chain
.tag_id
= ['EntityTag', 'delete', {
115 where
: [['entity_id', '=', '$id'], ['entity_table', '=', 'civicrm_saved_search']]
118 delete params
.tag_id
;
119 apiCalls
.saved
= ['SavedSearch', 'save', {records
: [params
], chain
: chain
}, 0];
120 crmApi4(apiCalls
).then(function(results
) {
121 // After saving a new search, redirect to the edit url
122 if (!ctrl
.savedSearch
.id
) {
123 $location
.url('edit/' + results
.saved
.id
);
125 // Set new status to saved unless the user changed something in the interim
126 var newStatus
= $scope
.status
=== 'unsaved' ? 'unsaved' : 'saved';
127 if (results
.saved
.groups
&& results
.saved
.groups
.length
) {
128 ctrl
.savedSearch
.groups
[0].id
= results
.saved
.groups
[0].id
;
130 ctrl
.savedSearch
.displays
= results
.saved
.displays
|| [];
131 // Wait until after onChangeAnything to update status
132 $timeout(function() {
133 $scope
.status
= newStatus
;
138 this.paramExists = function(param
) {
139 return _
.includes(searchMeta
.getEntity(ctrl
.savedSearch
.api_entity
).params
, param
);
142 this.addDisplay = function(type
) {
143 ctrl
.savedSearch
.displays
.push({
147 $scope
.selectTab('display_' + (ctrl
.savedSearch
.displays
.length
- 1));
150 this.removeDisplay = function(index
) {
151 var display
= ctrl
.savedSearch
.displays
[index
];
153 display
.trashed
= !display
.trashed
;
154 if ($scope
.controls
.tab
=== ('display_' + index
) && display
.trashed
) {
155 $scope
.selectTab('compose');
156 } else if (!display
.trashed
) {
157 $scope
.selectTab('display_' + index
);
159 if (display
.trashed
&& afformLoad
) {
160 afformLoad
.then(function() {
161 var displayForms
= _
.filter(ctrl
.afforms
, function(form
) {
162 return _
.includes(form
.displays
, ctrl
.savedSearch
.name
+ '.' + display
.name
);
164 if (displayForms
.length
) {
165 var msg
= displayForms
.length
=== 1 ?
166 ts('Form "%1" will be deleted if the embedded display "%2" is deleted.', {1: displayForms
[0].title
, 2: display
.label
}) :
167 ts('%1 forms will be deleted if the embedded display "%2" is deleted.', {1: displayForms
.length
, 2: display
.label
});
168 CRM
.alert(msg
, ts('Display embedded'), 'alert');
173 $scope
.selectTab('compose');
174 ctrl
.savedSearch
.displays
.splice(index
, 1);
178 this.addGroup = function() {
179 ctrl
.savedSearch
.groups
.push({
182 visibility
: 'User and User Admin Only',
185 ctrl
.groupExists
= true;
186 $scope
.selectTab('group');
189 $scope
.selectTab = function(tab
) {
190 if (tab
=== 'group') {
191 loadFieldOptions('Group');
192 $scope
.smartGroupColumns
= searchMeta
.getSmartGroupColumns(ctrl
.savedSearch
.api_entity
, ctrl
.savedSearch
.api_params
);
193 var smartGroupColumns
= _
.map($scope
.smartGroupColumns
, 'id');
194 if (smartGroupColumns
.length
&& !_
.includes(smartGroupColumns
, ctrl
.savedSearch
.api_params
.select
[0])) {
195 ctrl
.savedSearch
.api_params
.select
.unshift(smartGroupColumns
[0]);
198 ctrl
.savedSearch
.api_params
.select
= _
.uniq(ctrl
.savedSearch
.api_params
.select
);
199 $scope
.controls
.tab
= tab
;
202 this.removeGroup = function() {
203 ctrl
.groupExists
= !ctrl
.groupExists
;
204 $scope
.status
= 'unsaved';
205 if (!ctrl
.groupExists
&& (!ctrl
.savedSearch
.groups
.length
|| !ctrl
.savedSearch
.groups
[0].id
)) {
206 ctrl
.savedSearch
.groups
.length
= 0;
208 if ($scope
.controls
.tab
=== 'group') {
209 $scope
.selectTab('compose');
213 function addNum(name
, num
) {
214 return name
+ (num
< 10 ? '_0' : '_') + num
;
217 function getExistingJoins() {
218 return _
.transform(ctrl
.savedSearch
.api_params
.join
|| [], function(joins
, join
) {
219 joins
[join
[0].split(' AS ')[1]] = searchMeta
.getJoin(join
[0]);
223 $scope
.getJoin
= searchMeta
.getJoin
;
225 $scope
.getJoinEntities = function() {
226 var existingJoins
= getExistingJoins();
228 function addEntityJoins(entity
, stack
, baseEntity
) {
229 return _
.transform(CRM
.crmSearchAdmin
.joins
[entity
], function(joinEntities
, join
) {
231 // Add all joins that don't just point directly back to the original entity
232 if (!(baseEntity
=== join
.entity
&& !join
.multi
)) {
234 appendJoin(joinEntities
, join
, ++num
, stack
, entity
);
235 } while (addNum((stack
? stack
+ '_' : '') + join
.alias
, num
) in existingJoins
);
240 function appendJoin(collection
, join
, num
, stack
, baseEntity
) {
241 var alias
= addNum((stack
? stack
+ '_' : '') + join
.alias
, num
),
243 id
: join
.entity
+ ' AS ' + alias
,
244 description
: join
.description
,
245 text
: join
.label
+ (num
> 1 ? ' ' + num
: ''),
246 icon
: searchMeta
.getEntity(join
.entity
).icon
,
247 disabled
: alias
in existingJoins
249 if (alias
in existingJoins
) {
250 opt
.children
= addEntityJoins(join
.entity
, (stack
? stack
+ '_' : '') + alias
, baseEntity
);
252 collection
.push(opt
);
255 return {results
: addEntityJoins(ctrl
.savedSearch
.api_entity
)};
258 this.addJoin = function(value
) {
260 ctrl
.savedSearch
.api_params
.join
= ctrl
.savedSearch
.api_params
.join
|| [];
261 var join
= searchMeta
.getJoin(value
),
262 entity
= searchMeta
.getEntity(join
.entity
),
263 params
= [value
, $scope
.controls
.joinType
|| 'LEFT'];
264 _
.each(_
.cloneDeep(join
.conditions
), function(condition
) {
265 params
.push(condition
);
267 _
.each(_
.cloneDeep(join
.defaults
), function(condition
) {
268 params
.push(condition
);
270 ctrl
.savedSearch
.api_params
.join
.push(params
);
271 if (entity
.label_field
&& $scope
.controls
.joinType
!== 'EXCLUDE') {
272 ctrl
.savedSearch
.api_params
.select
.push(join
.alias
+ '.' + entity
.label_field
);
278 // Remove an explicit join + all SELECT, WHERE & other JOINs that use it
279 this.removeJoin = function(index
) {
280 var alias
= searchMeta
.getJoin(ctrl
.savedSearch
.api_params
.join
[index
][0]).alias
;
281 ctrl
.clearParam('join', index
);
282 removeJoinStuff(alias
);
285 function removeJoinStuff(alias
) {
286 _
.remove(ctrl
.savedSearch
.api_params
.select
, function(item
) {
287 var pattern
= new RegExp('\\b' + alias
+ '\\.');
288 return pattern
.test(item
.split(' AS ')[0]);
290 _
.remove(ctrl
.savedSearch
.api_params
.where
, function(clause
) {
291 return clauseUsesJoin(clause
, alias
);
293 _
.eachRight(ctrl
.savedSearch
.api_params
.join
, function(item
, i
) {
294 var joinAlias
= searchMeta
.getJoin(item
[0]).alias
;
295 if (joinAlias
!== alias
&& joinAlias
.indexOf(alias
) === 0) {
301 this.changeJoinType = function(join
) {
302 if (join
[1] === 'EXCLUDE') {
303 removeJoinStuff(searchMeta
.getJoin(join
[0]).alias
);
307 $scope
.changeGroupBy = function(idx
) {
308 // When clearing a selection
309 if (!ctrl
.savedSearch
.api_params
.groupBy
[idx
]) {
310 ctrl
.clearParam('groupBy', idx
);
312 reconcileAggregateColumns();
315 function reconcileAggregateColumns() {
316 _
.each(ctrl
.savedSearch
.api_params
.select
, function(col
, pos
) {
317 var info
= searchMeta
.parseExpr(col
),
318 fieldExpr
= (_
.findWhere(info
.args
, {type
: 'field'}) || {}).value
;
319 if (ctrl
.canAggregate(col
)) {
320 // Ensure all non-grouped columns are aggregated if using GROUP BY
321 if (!info
.fn
|| info
.fn
.category
!== 'aggregate') {
322 ctrl
.savedSearch
.api_params
.select
[pos
] = ctrl
.DEFAULT_AGGREGATE_FN
+ '(DISTINCT ' + fieldExpr
+ ') AS ' + ctrl
.DEFAULT_AGGREGATE_FN
+ '_' + fieldExpr
.replace(/[.:]/g, '_');
325 // Remove aggregate functions when no grouping
326 if (info
.fn
&& info
.fn
.category
=== 'aggregate') {
327 ctrl
.savedSearch
.api_params
.select
[pos
] = fieldExpr
;
333 function clauseUsesJoin(clause
, alias
) {
334 if (clause
[0].indexOf(alias
+ '.') === 0) {
337 if (_
.isArray(clause
[1])) {
338 return clause
[1].some(function(subClause
) {
339 return clauseUsesJoin(subClause
, alias
);
345 // Returns true if a clause contains one of the
346 function clauseUsesFields(clause
, fields
) {
347 if (!fields
|| !fields
.length
) {
350 if (_
.includes(fields
, clause
[0])) {
353 if (_
.isArray(clause
[1])) {
354 return clause
[1].some(function(subClause
) {
355 return clauseUsesField(subClause
, fields
);
361 function validate() {
366 if (!ctrl
.savedSearch
.label
) {
367 errorEl
= '#crm-saved-search-label';
368 label
= ts('Search Label');
369 errors
.push(ts('%1 is a required field.', {1: label
}));
371 if (ctrl
.groupExists
&& !ctrl
.savedSearch
.groups
[0].title
) {
372 errorEl
= '#crm-search-admin-group-title';
373 label
= ts('Group Title');
374 errors
.push(ts('%1 is a required field.', {1: label
}));
377 _
.each(ctrl
.savedSearch
.displays
, function(display
, index
) {
378 if (!display
.trashed
&& !display
.label
) {
379 errorEl
= '#crm-search-admin-display-label';
380 label
= ts('Display Label');
381 errors
.push(ts('%1 is a required field.', {1: label
}));
382 tab
= 'display_' + index
;
387 $scope
.selectTab(tab
);
389 $(errorEl
).crmError(errors
.join('<br>'), ts('Error Saving'), {expires
: 5000});
391 return !errors
.length
;
394 this.addParam = function(name
, value
) {
395 if (value
&& !_
.contains(ctrl
.savedSearch
.api_params
[name
], value
)) {
396 ctrl
.savedSearch
.api_params
[name
].push(value
);
397 // This needs to be called when adding a field as well as changing groupBy
398 reconcileAggregateColumns();
402 // Deletes an item from an array param
403 this.clearParam = function(name
, idx
) {
404 if (name
=== 'select') {
405 // Function selectors use `ng-repeat` with `track by $index` so must be refreshed when splicing the array
406 ctrl
.hideFuncitons();
408 ctrl
.savedSearch
.api_params
[name
].splice(idx
, 1);
411 this.hideFuncitons = function() {
412 $scope
.controls
.showFunctions
= false;
415 function onChangeSelect(newSelect
, oldSelect
) {
416 // When removing a column from SELECT, also remove from ORDER BY & HAVING
417 _
.each(_
.difference(oldSelect
, newSelect
), function(col
) {
418 col
= _
.last(col
.split(' AS '));
419 delete ctrl
.savedSearch
.api_params
.orderBy
[col
];
420 _
.remove(ctrl
.savedSearch
.api_params
.having
, function(clause
) {
421 return clauseUsesFields(clause
, [col
]);
426 this.getFieldLabel
= searchMeta
.getDefaultLabel
;
428 // Is a column eligible to use an aggregate function?
429 this.canAggregate = function(col
) {
430 // If the query does not use grouping, never
431 if (!ctrl
.savedSearch
.api_params
.groupBy
.length
) {
434 var arg
= _
.findWhere(searchMeta
.parseExpr(col
).args
, {type
: 'field'}) || {};
435 // If the column is not a database field, no
436 if (!arg
.field
|| !arg
.field
.entity
|| arg
.field
.type
!== 'Field') {
439 // If the column is used for a groupBy, no
440 if (ctrl
.savedSearch
.api_params
.groupBy
.indexOf(arg
.path
) > -1) {
443 // If the entity this column belongs to is being grouped by primary key, then also no
444 var idField
= searchMeta
.getEntity(arg
.field
.entity
).primary_key
[0];
445 return ctrl
.savedSearch
.api_params
.groupBy
.indexOf(arg
.prefix
+ idField
) < 0;
448 $scope
.fieldsForGroupBy = function() {
449 return {results
: ctrl
.getAllFields('', ['Field', 'Custom'], function(key
) {
450 return _
.contains(ctrl
.savedSearch
.api_params
.groupBy
, key
);
455 function getFieldsForJoin(joinEntity
) {
456 return {results
: ctrl
.getAllFields(':name', ['Field'], null, joinEntity
)};
459 // @return {function}
460 $scope
.fieldsForJoin = function(joinEntity
) {
461 if (!fieldsForJoinGetters
[joinEntity
]) {
462 fieldsForJoinGetters
[joinEntity
] = _
.wrap(joinEntity
, getFieldsForJoin
);
464 return fieldsForJoinGetters
[joinEntity
];
467 $scope
.fieldsForWhere = function() {
468 return {results
: ctrl
.getAllFields(':name')};
471 $scope
.fieldsForHaving = function() {
472 return {results
: ctrl
.getSelectFields()};
475 // Sets the default select clause based on commonly-named fields
476 function getDefaultSelect() {
477 var entity
= searchMeta
.getEntity(ctrl
.savedSearch
.api_entity
);
478 return _
.transform(entity
.fields
, function(defaultSelect
, field
) {
479 if (field
.name
=== 'id' || field
.name
=== entity
.label_field
) {
480 defaultSelect
.push(field
.name
);
485 this.getAllFields = function(suffix
, allowedTypes
, disabledIf
, topJoin
) {
486 disabledIf
= disabledIf
|| _
.noop
;
488 function formatEntityFields(entityName
, join
) {
489 var prefix
= join
? join
.alias
+ '.' : '',
492 // Add extra searchable fields from bridge entity
493 if (join
&& join
.bridge
) {
494 formatFields(_
.filter(searchMeta
.getEntity(join
.bridge
).fields
, function(field
) {
495 return (field
.name
!== 'id' && field
.name
!== 'entity_id' && field
.name
!== 'entity_table' && !field
.fk_entity
);
499 formatFields(searchMeta
.getEntity(entityName
).fields
, result
, prefix
);
503 function formatFields(fields
, result
, prefix
) {
504 prefix
= typeof prefix
=== 'undefined' ? '' : prefix
;
505 _
.each(fields
, function(field
) {
507 id
: prefix
+ field
.name
+ (field
.options
? suffix
: ''),
509 description
: field
.description
511 if (disabledIf(item
.id
)) {
512 item
.disabled
= true;
514 if (!allowedTypes
|| _
.includes(allowedTypes
, field
.type
)) {
521 var mainEntity
= searchMeta
.getEntity(ctrl
.savedSearch
.api_entity
),
522 joinEntities
= _
.map(ctrl
.savedSearch
.api_params
.join
, 0),
525 function addJoin(join
) {
526 var joinInfo
= searchMeta
.getJoin(join
),
527 joinEntity
= searchMeta
.getEntity(joinInfo
.entity
);
529 text
: joinInfo
.label
,
530 description
: joinInfo
.description
,
531 icon
: joinEntity
.icon
,
532 children
: formatEntityFields(joinEntity
.name
, joinInfo
)
536 // Place specified join at top of list
539 _
.pull(joinEntities
, topJoin
);
543 text
: mainEntity
.title_plural
,
544 icon
: mainEntity
.icon
,
545 children
: formatEntityFields(ctrl
.savedSearch
.api_entity
)
548 // Include SearchKit's pseudo-fields if specifically requested
549 if (allowedTypes
&& _
.includes(allowedTypes
, 'Pseudo')) {
553 children
: formatFields(CRM
.crmSearchAdmin
.pseudoFields
, [])
557 _
.each(joinEntities
, addJoin
);
561 this.getSelectFields = function(disabledIf
) {
562 disabledIf
= disabledIf
|| _
.noop
;
563 return _
.transform(ctrl
.savedSearch
.api_params
.select
, function(fields
, name
) {
564 var info
= searchMeta
.parseExpr(name
);
567 text
: ctrl
.getFieldLabel(name
),
568 description
: info
.fn
? info
.fn
.description
: info
.args
[0].field
&& info
.args
[0].field
.description
570 if (disabledIf(item
.id
)) {
571 item
.disabled
= true;
577 this.isPseudoField = function(name
) {
578 return _
.findIndex(CRM
.crmSearchAdmin
.pseudoFields
, {name
: name
}) >= 0;
582 * Fetch pseudoconstants for main entity + joined entities
584 * Sets an optionsLoaded property on each entity to avoid duplicate requests
586 * @var string entity - optional additional entity to load
588 function loadFieldOptions(entity
) {
589 var mainEntity
= searchMeta
.getEntity(ctrl
.savedSearch
.api_entity
),
592 function enqueue(entity
) {
593 entity
.optionsLoaded
= false;
594 entities
[entity
.name
] = [entity
.name
, 'getFields', {
595 loadOptions
: ['id', 'name', 'label', 'description', 'color', 'icon'],
596 where
: [['options', '!=', false]],
598 }, {name
: 'options'}];
601 if (typeof mainEntity
.optionsLoaded
=== 'undefined') {
605 // Optional additional entity
606 if (entity
&& typeof searchMeta
.getEntity(entity
).optionsLoaded
=== 'undefined') {
607 enqueue(searchMeta
.getEntity(entity
));
610 _
.each(ctrl
.savedSearch
.api_params
.join
, function(join
) {
611 var joinInfo
= searchMeta
.getJoin(join
[0]),
612 joinEntity
= searchMeta
.getEntity(joinInfo
.entity
),
613 bridgeEntity
= joinInfo
.bridge
? searchMeta
.getEntity(joinInfo
.bridge
) : null;
614 if (typeof joinEntity
.optionsLoaded
=== 'undefined') {
617 if (bridgeEntity
&& typeof bridgeEntity
.optionsLoaded
=== 'undefined') {
618 enqueue(bridgeEntity
);
621 if (!_
.isEmpty(entities
)) {
622 crmApi4(entities
).then(function(results
) {
623 _
.each(results
, function(fields
, entityName
) {
624 var entity
= searchMeta
.getEntity(entityName
);
625 _
.each(fields
, function(options
, fieldName
) {
626 _
.find(entity
.fields
, {name
: fieldName
}).options
= options
;
628 entity
.optionsLoaded
= true;
634 // Build a list of all possible links to main entity & join entities
636 this.buildLinks = function() {
637 function addTitle(link
, entityName
) {
638 link
.text
= link
.text
.replace('%1', entityName
);
641 // Links to main entity
642 var mainEntity
= searchMeta
.getEntity(ctrl
.savedSearch
.api_entity
),
643 links
= _
.cloneDeep(mainEntity
.links
|| []);
644 _
.each(links
, function(link
) {
646 addTitle(link
, mainEntity
.title
);
648 // Links to explicitly joined entities
649 _
.each(ctrl
.savedSearch
.api_params
.join
, function(joinClause
) {
650 var join
= searchMeta
.getJoin(joinClause
[0]),
651 joinEntity
= searchMeta
.getEntity(join
.entity
),
652 bridgeEntity
= _
.isString(joinClause
[2]) ? searchMeta
.getEntity(joinClause
[2]) : null;
653 _
.each(_
.cloneDeep(joinEntity
.links
), function(link
) {
654 link
.join
= join
.alias
;
655 addTitle(link
, join
.label
);
658 _
.each(_
.cloneDeep(bridgeEntity
&& bridgeEntity
.links
), function(link
) {
659 link
.join
= join
.alias
;
660 addTitle(link
, join
.label
+ (bridgeEntity
.bridge_title
? ' ' + bridgeEntity
.bridge_title
: ''));
664 // Links to implicit joins
665 _
.each(ctrl
.savedSearch
.api_params
.select
, function(fieldName
) {
666 if (!_
.includes(fieldName
, ' AS ')) {
667 var info
= searchMeta
.parseExpr(fieldName
).args
[0];
668 if (info
.field
&& !info
.suffix
&& !info
.fn
&& info
.field
.type
=== 'Field' && (info
.field
.fk_entity
|| info
.field
.name
!== info
.field
.fieldName
)) {
669 var idFieldName
= info
.field
.fk_entity
? fieldName
: fieldName
.substr(0, fieldName
.lastIndexOf('.')),
670 idField
= searchMeta
.parseExpr(idFieldName
).args
[0].field
;
671 if (!ctrl
.canAggregate(idFieldName
)) {
672 var joinEntity
= searchMeta
.getEntity(idField
.fk_entity
),
673 label
= (idField
.join
? idField
.join
.label
+ ': ' : '') + (idField
.input_attrs
&& idField
.input_attrs
.label
|| idField
.label
);
674 _
.each(_
.cloneDeep(joinEntity
&& joinEntity
.links
), function(link
) {
675 link
.join
= idFieldName
;
676 addTitle(link
, label
);
686 function loadAfforms() {
688 if (ctrl
.afformEnabled
&& ctrl
.savedSearch
.id
) {
689 var findDisplays
= _
.transform(ctrl
.savedSearch
.displays
, function(findDisplays
, display
) {
690 if (display
.id
&& display
.name
) {
691 findDisplays
.push(['search_displays', 'CONTAINS', ctrl
.savedSearch
.name
+ '.' + display
.name
]);
693 }, [['search_displays', 'CONTAINS', ctrl
.savedSearch
.name
]]);
694 afformLoad
= crmApi4('Afform', 'get', {
695 select
: ['name', 'title', 'search_displays'],
696 where
: [['OR', findDisplays
]]
697 }).then(function(afforms
) {
699 _
.each(afforms
, function(afform
) {
702 displays
: afform
.search_displays
,
703 link
: ctrl
.afformAdminEnabled
? CRM
.url('civicrm/admin/afform#/edit/' + afform
.name
) : '',
706 ctrl
.afformCount
= ctrl
.afforms
.length
;
711 // Creating an Afform opens a new tab, so when switching back after > 10 sec, re-check for Afforms
712 $(window
).on('focus', _
.debounce(function() {
713 $scope
.$apply(loadAfforms
);
714 }, 10000, {leading
: true, trailing
: false}));
719 })(angular
, CRM
.$, CRM
._
);