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.afformEnabled
= 'org.civicrm.afform' in CRM
.crmSearchAdmin
.modules
;
16 this.afformAdminEnabled
= (CRM
.checkPerm('administer CiviCRM') || CRM
.checkPerm('administer afform')) &&
17 'org.civicrm.afform_admin' in CRM
.crmSearchAdmin
.modules
;
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 select
: getDefaultSelect(),
49 _
.each(['groupBy', 'join', 'having'], function(param
) {
50 if (ctrl
.paramExists(param
)) {
54 // Default to Individuals
55 if (this.savedSearch
.api_entity
=== 'Contact' && CRM
.crmSearchAdmin
.defaultContactType
) {
56 defaults
.where
.push(['contact_type:name', '=', CRM
.crmSearchAdmin
.defaultContactType
]);
61 expr
: '$ctrl.savedSearch.api_params',
68 expr
: '$ctrl.savedSearch.label',
74 $scope
.mainEntitySelect
= searchMeta
.getPrimaryAndSecondaryEntitySelect();
76 $scope
.$watchCollection('$ctrl.savedSearch.api_params.select', onChangeSelect
);
78 $scope
.$watch('$ctrl.savedSearch', onChangeAnything
, true);
80 // After watcher runs for the first time and messes up the status, set it correctly
82 $scope
.status
= ctrl
.savedSearch
&& ctrl
.savedSearch
.id
? 'saved' : 'unsaved';
89 function onChangeAnything() {
90 $scope
.status
= 'unsaved';
93 this.save = function() {
97 $scope
.status
= 'saving';
98 var params
= _
.cloneDeep(ctrl
.savedSearch
),
101 if (ctrl
.groupExists
) {
102 chain
.groups
= ['Group', 'save', {defaults
: {saved_search_id
: '$id'}, records
: params
.groups
}];
103 delete params
.groups
;
104 } else if (params
.id
) {
105 apiCalls
.deleteGroup
= ['Group', 'delete', {where
: [['saved_search_id', '=', params
.id
]]}];
107 _
.remove(params
.displays
, {trashed
: true});
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
]]}];
113 delete params
.displays
;
114 if (params
.tag_id
&& params
.tag_id
.length
) {
115 chain
.tag_id
= ['EntityTag', 'replace', {
116 where
: [['entity_id', '=', '$id'], ['entity_table', '=', 'civicrm_saved_search']],
117 records
: _
.transform(params
.tag_id
, function(records
, id
) {records
.push({tag_id
: id
});})
119 } else if (params
.id
) {
120 chain
.tag_id
= ['EntityTag', 'delete', {
121 where
: [['entity_id', '=', '$id'], ['entity_table', '=', 'civicrm_saved_search']]
124 delete params
.tag_id
;
125 apiCalls
.saved
= ['SavedSearch', 'save', {records
: [params
], chain
: chain
}, 0];
126 crmApi4(apiCalls
).then(function(results
) {
127 // After saving a new search, redirect to the edit url
128 if (!ctrl
.savedSearch
.id
) {
129 $location
.url('edit/' + results
.saved
.id
);
131 // Set new status to saved unless the user changed something in the interim
132 var newStatus
= $scope
.status
=== 'unsaved' ? 'unsaved' : 'saved';
133 if (results
.saved
.groups
&& results
.saved
.groups
.length
) {
134 ctrl
.savedSearch
.groups
[0].id
= results
.saved
.groups
[0].id
;
136 ctrl
.savedSearch
.displays
= results
.saved
.displays
|| [];
137 // Wait until after onChangeAnything to update status
138 $timeout(function() {
139 $scope
.status
= newStatus
;
144 this.paramExists = function(param
) {
145 return _
.includes(searchMeta
.getEntity(ctrl
.savedSearch
.api_entity
).params
, param
);
148 this.hasFunction = function(expr
) {
149 return expr
.indexOf('(') > -1;
152 this.addDisplay = function(type
) {
153 var count
= _
.filter(ctrl
.savedSearch
.displays
, {type
: type
}).length
,
154 searchLabel
= ctrl
.savedSearch
.label
|| searchMeta
.getEntity(ctrl
.savedSearch
.api_entity
).title_plural
;
155 ctrl
.savedSearch
.displays
.push({
157 label
: searchLabel
+ ' ' + ctrl
.displayTypes
[type
].label
+ ' ' + (count
+ 1),
159 $scope
.selectTab('display_' + (ctrl
.savedSearch
.displays
.length
- 1));
162 this.removeDisplay = function(index
) {
163 var display
= ctrl
.savedSearch
.displays
[index
];
165 display
.trashed
= !display
.trashed
;
166 if ($scope
.controls
.tab
=== ('display_' + index
) && display
.trashed
) {
167 $scope
.selectTab('compose');
168 } else if (!display
.trashed
) {
169 $scope
.selectTab('display_' + index
);
171 if (display
.trashed
&& afformLoad
) {
172 afformLoad
.then(function() {
173 var displayForms
= _
.filter(ctrl
.afforms
, function(form
) {
174 return _
.includes(form
.displays
, ctrl
.savedSearch
.name
+ '.' + display
.name
);
176 if (displayForms
.length
) {
177 var msg
= displayForms
.length
=== 1 ?
178 ts('Form "%1" will be deleted if the embedded display "%2" is deleted.', {1: displayForms
[0].title
, 2: display
.label
}) :
179 ts('%1 forms will be deleted if the embedded display "%2" is deleted.', {1: displayForms
.length
, 2: display
.label
});
180 CRM
.alert(msg
, ts('Display embedded'), 'alert');
185 $scope
.selectTab('compose');
186 ctrl
.savedSearch
.displays
.splice(index
, 1);
190 this.addGroup = function() {
191 ctrl
.savedSearch
.groups
.push({
194 visibility
: 'User and User Admin Only',
197 ctrl
.groupExists
= true;
198 $scope
.selectTab('group');
201 $scope
.selectTab = function(tab
) {
202 if (tab
=== 'group') {
203 loadFieldOptions('Group');
204 $scope
.smartGroupColumns
= searchMeta
.getSmartGroupColumns(ctrl
.savedSearch
.api_entity
, ctrl
.savedSearch
.api_params
);
205 var smartGroupColumns
= _
.map($scope
.smartGroupColumns
, 'id');
206 if (smartGroupColumns
.length
&& !_
.includes(smartGroupColumns
, ctrl
.savedSearch
.api_params
.select
[0])) {
207 ctrl
.savedSearch
.api_params
.select
.unshift(smartGroupColumns
[0]);
210 ctrl
.savedSearch
.api_params
.select
= _
.uniq(ctrl
.savedSearch
.api_params
.select
);
211 $scope
.controls
.tab
= tab
;
214 this.removeGroup = function() {
215 ctrl
.groupExists
= !ctrl
.groupExists
;
216 $scope
.status
= 'unsaved';
217 if (!ctrl
.groupExists
&& (!ctrl
.savedSearch
.groups
.length
|| !ctrl
.savedSearch
.groups
[0].id
)) {
218 ctrl
.savedSearch
.groups
.length
= 0;
220 if ($scope
.controls
.tab
=== 'group') {
221 $scope
.selectTab('compose');
225 function addNum(name
, num
) {
226 return name
+ (num
< 10 ? '_0' : '_') + num
;
229 function getExistingJoins() {
230 return _
.transform(ctrl
.savedSearch
.api_params
.join
|| [], function(joins
, join
) {
231 joins
[join
[0].split(' AS ')[1]] = searchMeta
.getJoin(join
[0]);
235 $scope
.getJoin
= searchMeta
.getJoin
;
237 $scope
.getJoinEntities = function() {
238 var existingJoins
= getExistingJoins();
240 function addEntityJoins(entity
, stack
, baseEntity
) {
241 return _
.transform(CRM
.crmSearchAdmin
.joins
[entity
], function(joinEntities
, join
) {
244 // Exclude joins that singly point back to the original entity
245 !(baseEntity
=== join
.entity
&& !join
.multi
) &&
246 // Exclude joins to bridge tables
247 !searchMeta
.getEntity(join
.entity
).bridge
250 appendJoin(joinEntities
, join
, ++num
, stack
, entity
);
251 } while (addNum((stack
? stack
+ '_' : '') + join
.alias
, num
) in existingJoins
);
256 function appendJoin(collection
, join
, num
, stack
, baseEntity
) {
257 var alias
= addNum((stack
? stack
+ '_' : '') + join
.alias
, num
),
259 id
: join
.entity
+ ' AS ' + alias
,
260 description
: join
.description
,
261 text
: join
.label
+ (num
> 1 ? ' ' + num
: ''),
262 icon
: searchMeta
.getEntity(join
.entity
).icon
,
263 disabled
: alias
in existingJoins
265 if (alias
in existingJoins
) {
266 opt
.children
= addEntityJoins(join
.entity
, (stack
? stack
+ '_' : '') + alias
, baseEntity
);
268 collection
.push(opt
);
271 return {results
: addEntityJoins(ctrl
.savedSearch
.api_entity
)};
274 this.addJoin = function(value
) {
276 ctrl
.savedSearch
.api_params
.join
= ctrl
.savedSearch
.api_params
.join
|| [];
277 var join
= searchMeta
.getJoin(value
),
278 entity
= searchMeta
.getEntity(join
.entity
),
279 params
= [value
, $scope
.controls
.joinType
|| 'LEFT'];
280 _
.each(_
.cloneDeep(join
.conditions
), function(condition
) {
281 params
.push(condition
);
283 _
.each(_
.cloneDeep(join
.defaults
), function(condition
) {
284 params
.push(condition
);
286 ctrl
.savedSearch
.api_params
.join
.push(params
);
287 if (entity
.label_field
&& $scope
.controls
.joinType
!== 'EXCLUDE') {
288 ctrl
.savedSearch
.api_params
.select
.push(join
.alias
+ '.' + entity
.label_field
);
294 // Remove an explicit join + all SELECT, WHERE & other JOINs that use it
295 this.removeJoin = function(index
) {
296 var alias
= searchMeta
.getJoin(ctrl
.savedSearch
.api_params
.join
[index
][0]).alias
;
297 ctrl
.clearParam('join', index
);
298 removeJoinStuff(alias
);
301 function removeJoinStuff(alias
) {
302 _
.remove(ctrl
.savedSearch
.api_params
.select
, function(item
) {
303 var pattern
= new RegExp('\\b' + alias
+ '\\.');
304 return pattern
.test(item
.split(' AS ')[0]);
306 _
.remove(ctrl
.savedSearch
.api_params
.where
, function(clause
) {
307 return clauseUsesJoin(clause
, alias
);
309 _
.eachRight(ctrl
.savedSearch
.api_params
.join
, function(item
, i
) {
310 var joinAlias
= searchMeta
.getJoin(item
[0]).alias
;
311 if (joinAlias
!== alias
&& joinAlias
.indexOf(alias
) === 0) {
317 this.changeJoinType = function(join
) {
318 if (join
[1] === 'EXCLUDE') {
319 removeJoinStuff(searchMeta
.getJoin(join
[0]).alias
);
323 $scope
.changeGroupBy = function(idx
) {
324 // When clearing a selection
325 if (!ctrl
.savedSearch
.api_params
.groupBy
[idx
]) {
326 ctrl
.clearParam('groupBy', idx
);
328 reconcileAggregateColumns();
331 function reconcileAggregateColumns() {
332 _
.each(ctrl
.savedSearch
.api_params
.select
, function(col
, pos
) {
333 var info
= searchMeta
.parseExpr(col
),
334 fieldExpr
= (_
.findWhere(info
.args
, {type
: 'field'}) || {}).value
;
335 if (ctrl
.canAggregate(col
)) {
336 // Ensure all non-grouped columns are aggregated if using GROUP BY
337 if (!info
.fn
|| info
.fn
.category
!== 'aggregate') {
338 var dflFn
= searchMeta
.getDefaultAggregateFn(info
) || 'GROUP_CONCAT',
339 flagBefore
= dflFn
=== 'GROUP_CONCAT' ? 'DISTINCT ' : '';
340 ctrl
.savedSearch
.api_params
.select
[pos
] = dflFn
+ '(' + flagBefore
+ fieldExpr
+ ') AS ' + dflFn
+ '_' + fieldExpr
.replace(/[.:]/g, '_');
343 // Remove aggregate functions when no grouping
344 if (info
.fn
&& info
.fn
.category
=== 'aggregate') {
345 ctrl
.savedSearch
.api_params
.select
[pos
] = fieldExpr
;
351 function clauseUsesJoin(clause
, alias
) {
352 if (clause
[0].indexOf(alias
+ '.') === 0) {
355 if (_
.isArray(clause
[1])) {
356 return clause
[1].some(function(subClause
) {
357 return clauseUsesJoin(subClause
, alias
);
363 // Returns true if a clause contains one of the
364 function clauseUsesFields(clause
, fields
) {
365 if (!fields
|| !fields
.length
) {
368 if (_
.includes(fields
, clause
[0])) {
371 if (_
.isArray(clause
[1])) {
372 return clause
[1].some(function(subClause
) {
373 return clauseUsesField(subClause
, fields
);
379 function validate() {
384 if (!ctrl
.savedSearch
.label
) {
385 errorEl
= '#crm-saved-search-label';
386 label
= ts('Search Label');
387 errors
.push(ts('%1 is a required field.', {1: label
}));
389 if (ctrl
.groupExists
&& !ctrl
.savedSearch
.groups
[0].title
) {
390 errorEl
= '#crm-search-admin-group-title';
391 label
= ts('Group Title');
392 errors
.push(ts('%1 is a required field.', {1: label
}));
395 _
.each(ctrl
.savedSearch
.displays
, function(display
, index
) {
396 if (!display
.trashed
&& !display
.label
) {
397 errorEl
= '#crm-search-admin-display-label';
398 label
= ts('Display Label');
399 errors
.push(ts('%1 is a required field.', {1: label
}));
400 tab
= 'display_' + index
;
405 $scope
.selectTab(tab
);
407 $(errorEl
).crmError(errors
.join('<br>'), ts('Error Saving'), {expires
: 5000});
409 return !errors
.length
;
412 this.addParam = function(name
, value
) {
413 if (value
&& !_
.contains(ctrl
.savedSearch
.api_params
[name
], value
)) {
414 ctrl
.savedSearch
.api_params
[name
].push(value
);
415 // This needs to be called when adding a field as well as changing groupBy
416 reconcileAggregateColumns();
420 // Deletes an item from an array param
421 this.clearParam = function(name
, idx
) {
422 if (name
=== 'select') {
423 // Function selectors use `ng-repeat` with `track by $index` so must be refreshed when splicing the array
424 ctrl
.hideFuncitons();
426 ctrl
.savedSearch
.api_params
[name
].splice(idx
, 1);
429 this.hideFuncitons = function() {
430 $scope
.controls
.showFunctions
= false;
433 function onChangeSelect(newSelect
, oldSelect
) {
434 // When removing a column from SELECT, also remove from ORDER BY & HAVING
435 _
.each(_
.difference(oldSelect
, newSelect
), function(col
) {
436 col
= _
.last(col
.split(' AS '));
437 delete ctrl
.savedSearch
.api_params
.orderBy
[col
];
438 _
.remove(ctrl
.savedSearch
.api_params
.having
, function(clause
) {
439 return clauseUsesFields(clause
, [col
]);
444 this.getFieldLabel
= searchMeta
.getDefaultLabel
;
446 // Is a column eligible to use an aggregate function?
447 this.canAggregate = function(col
) {
448 // If the query does not use grouping, never
449 if (!ctrl
.savedSearch
.api_params
.groupBy
|| !ctrl
.savedSearch
.api_params
.groupBy
.length
) {
452 var arg
= _
.findWhere(searchMeta
.parseExpr(col
).args
, {type
: 'field'}) || {};
453 // If the column is not a database field, no
454 if (!arg
.field
|| !arg
.field
.entity
|| !_
.includes(['Field', 'Custom', 'Extra'], arg
.field
.type
)) {
457 // If the column is used for a groupBy, no
458 if (ctrl
.savedSearch
.api_params
.groupBy
.indexOf(arg
.path
) > -1) {
461 // If the entity this column belongs to is being grouped by primary key, then also no
462 var idField
= searchMeta
.getEntity(arg
.field
.entity
).primary_key
[0];
463 return ctrl
.savedSearch
.api_params
.groupBy
.indexOf(arg
.prefix
+ idField
) < 0;
466 $scope
.fieldsForGroupBy = function() {
467 return {results
: ctrl
.getAllFields('', ['Field', 'Custom', 'Extra'], function(key
) {
468 return _
.contains(ctrl
.savedSearch
.api_params
.groupBy
, key
);
473 function getFieldsForJoin(joinEntity
) {
474 return {results
: ctrl
.getAllFields(':name', ['Field'], null, joinEntity
)};
477 // @return {function}
478 $scope
.fieldsForJoin = function(joinEntity
) {
479 if (!fieldsForJoinGetters
[joinEntity
]) {
480 fieldsForJoinGetters
[joinEntity
] = _
.wrap(joinEntity
, getFieldsForJoin
);
482 return fieldsForJoinGetters
[joinEntity
];
485 $scope
.fieldsForWhere = function() {
486 return {results
: ctrl
.getAllFields(':name')};
489 $scope
.fieldsForHaving = function() {
490 return {results
: ctrl
.getSelectFields()};
493 // Sets the default select clause based on commonly-named fields
494 function getDefaultSelect() {
495 var entity
= searchMeta
.getEntity(ctrl
.savedSearch
.api_entity
);
496 return _
.transform(entity
.fields
, function(defaultSelect
, field
) {
497 if (field
.name
=== 'id' || field
.name
=== entity
.label_field
) {
498 defaultSelect
.push(field
.name
);
503 this.getAllFields = function(suffix
, allowedTypes
, disabledIf
, topJoin
) {
504 disabledIf
= disabledIf
|| _
.noop
;
505 allowedTypes
= allowedTypes
|| ['Field', 'Custom', 'Extra', 'Filter'];
507 function formatEntityFields(entityName
, join
) {
508 var prefix
= join
? join
.alias
+ '.' : '',
511 // Add extra searchable fields from bridge entity
512 if (join
&& join
.bridge
) {
513 formatFields(_
.filter(searchMeta
.getEntity(join
.bridge
).fields
, function(field
) {
514 return (field
.name
!== 'id' && field
.name
!== 'entity_id' && field
.name
!== 'entity_table' && !field
.fk_entity
);
518 formatFields(searchMeta
.getEntity(entityName
).fields
, result
, prefix
);
522 function formatFields(fields
, result
, prefix
) {
523 prefix
= typeof prefix
=== 'undefined' ? '' : prefix
;
524 _
.each(fields
, function(field
) {
526 id
: prefix
+ field
.name
+ (field
.suffixes
&& _
.includes(field
.suffixes
, suffix
.replace(':', '')) ? suffix
: ''),
528 description
: field
.description
530 if (disabledIf(item
.id
)) {
531 item
.disabled
= true;
533 if (_
.includes(allowedTypes
, field
.type
)) {
540 var mainEntity
= searchMeta
.getEntity(ctrl
.savedSearch
.api_entity
),
541 joinEntities
= _
.map(ctrl
.savedSearch
.api_params
.join
, 0),
544 function addJoin(join
) {
545 var joinInfo
= searchMeta
.getJoin(join
),
546 joinEntity
= searchMeta
.getEntity(joinInfo
.entity
);
548 text
: joinInfo
.label
,
549 description
: joinInfo
.description
,
550 icon
: joinEntity
.icon
,
551 children
: formatEntityFields(joinEntity
.name
, joinInfo
)
555 // Place specified join at top of list
558 _
.pull(joinEntities
, topJoin
);
562 text
: mainEntity
.title_plural
,
563 icon
: mainEntity
.icon
,
564 children
: formatEntityFields(ctrl
.savedSearch
.api_entity
)
567 // Include SearchKit's pseudo-fields if specifically requested
568 if (_
.includes(allowedTypes
, 'Pseudo')) {
572 children
: formatFields(CRM
.crmSearchAdmin
.pseudoFields
, [])
576 _
.each(joinEntities
, addJoin
);
580 this.getSelectFields = function(disabledIf
) {
581 disabledIf
= disabledIf
|| _
.noop
;
582 return _
.transform(ctrl
.savedSearch
.api_params
.select
, function(fields
, name
) {
583 var info
= searchMeta
.parseExpr(name
);
586 text
: ctrl
.getFieldLabel(name
),
587 description
: info
.fn
? info
.fn
.description
: info
.args
[0].field
&& info
.args
[0].field
.description
589 if (disabledIf(item
.id
)) {
590 item
.disabled
= true;
596 this.isPseudoField = function(name
) {
597 return _
.findIndex(CRM
.crmSearchAdmin
.pseudoFields
, {name
: name
}) >= 0;
600 // Ensure options are loaded for main entity + joined entities
601 // And an optional additional entity
602 function loadFieldOptions(entity
) {
604 var entitiesToLoad
= [ctrl
.savedSearch
.api_entity
];
606 // Join entities + bridge entities
607 _
.each(ctrl
.savedSearch
.api_params
.join
, function(join
) {
608 var joinInfo
= searchMeta
.getJoin(join
[0]);
609 entitiesToLoad
.push(joinInfo
.entity
);
610 if (joinInfo
.bridge
) {
611 entitiesToLoad
.push(joinInfo
.bridge
);
615 // Optional additional entity
617 entitiesToLoad
.push(entity
);
620 searchMeta
.loadFieldOptions(entitiesToLoad
);
623 // Build a list of all possible links to main entity & join entities
625 this.buildLinks = function() {
626 function addTitle(link
, entityName
) {
627 link
.text
= link
.text
.replace('%1', entityName
);
630 // Links to main entity
631 var mainEntity
= searchMeta
.getEntity(ctrl
.savedSearch
.api_entity
),
632 links
= _
.cloneDeep(mainEntity
.links
|| []);
633 _
.each(links
, function(link
) {
635 addTitle(link
, mainEntity
.title
);
637 // Links to explicitly joined entities
638 _
.each(ctrl
.savedSearch
.api_params
.join
, function(joinClause
) {
639 var join
= searchMeta
.getJoin(joinClause
[0]),
640 joinEntity
= searchMeta
.getEntity(join
.entity
),
641 bridgeEntity
= _
.isString(joinClause
[2]) ? searchMeta
.getEntity(joinClause
[2]) : null;
642 _
.each(_
.cloneDeep(joinEntity
.links
), function(link
) {
643 link
.join
= join
.alias
;
644 addTitle(link
, join
.label
);
647 _
.each(_
.cloneDeep(bridgeEntity
&& bridgeEntity
.links
), function(link
) {
648 link
.join
= join
.alias
;
649 addTitle(link
, join
.label
+ (bridgeEntity
.bridge_title
? ' ' + bridgeEntity
.bridge_title
: ''));
653 // Links to implicit joins
654 _
.each(ctrl
.savedSearch
.api_params
.select
, function(fieldName
) {
655 if (!_
.includes(fieldName
, ' AS ')) {
656 var info
= searchMeta
.parseExpr(fieldName
).args
[0];
657 if (info
.field
&& !info
.suffix
&& !info
.fn
&& info
.field
.type
=== 'Field' && (info
.field
.fk_entity
|| info
.field
.name
!== info
.field
.fieldName
)) {
658 var idFieldName
= info
.field
.fk_entity
? fieldName
: fieldName
.substr(0, fieldName
.lastIndexOf('.')),
659 idField
= searchMeta
.parseExpr(idFieldName
).args
[0].field
;
660 if (!ctrl
.canAggregate(idFieldName
)) {
661 var joinEntity
= searchMeta
.getEntity(idField
.fk_entity
),
662 label
= (idField
.join
? idField
.join
.label
+ ': ' : '') + (idField
.input_attrs
&& idField
.input_attrs
.label
|| idField
.label
);
663 _
.each(_
.cloneDeep(joinEntity
&& joinEntity
.links
), function(link
) {
664 link
.join
= idFieldName
;
665 addTitle(link
, label
);
675 function loadAfforms() {
677 if (ctrl
.afformEnabled
&& ctrl
.savedSearch
.id
) {
678 var findDisplays
= _
.transform(ctrl
.savedSearch
.displays
, function(findDisplays
, display
) {
679 if (display
.id
&& display
.name
) {
680 findDisplays
.push(['search_displays', 'CONTAINS', ctrl
.savedSearch
.name
+ '.' + display
.name
]);
682 }, [['search_displays', 'CONTAINS', ctrl
.savedSearch
.name
]]);
683 afformLoad
= crmApi4('Afform', 'get', {
684 select
: ['name', 'title', 'search_displays'],
685 where
: [['OR', findDisplays
]]
686 }).then(function(afforms
) {
688 _
.each(afforms
, function(afform
) {
691 displays
: afform
.search_displays
,
692 link
: ctrl
.afformAdminEnabled
? CRM
.url('civicrm/admin/afform#/edit/' + afform
.name
) : '',
695 ctrl
.afformCount
= ctrl
.afforms
.length
;
700 // Creating an Afform opens a new tab, so when switching back after > 10 sec, re-check for Afforms
701 $(window
).on('focus', _
.debounce(function() {
702 $scope
.$apply(loadAfforms
);
703 }, 10000, {leading
: true, trailing
: false}));
708 })(angular
, CRM
.$, CRM
._
);