SearchKit - Enable links for implicit joins
[civicrm-core.git] / ext / search / ang / crmSearchAdmin.module.js
CommitLineData
25523059
CW
1(function(angular, $, _) {
2 "use strict";
3
4 // Shared between router and searchMeta service
2c7e2f4b 5 var searchEntity,
4f0729ed 6 joinIndex,
2c7e2f4b 7 undefined;
25523059
CW
8
9 // Declare module and route/controller/services
493f83d4 10 angular.module('crmSearchAdmin', CRM.angRequires('crmSearchAdmin'))
25523059
CW
11
12 .config(function($routeProvider) {
475029f6
CW
13 $routeProvider.when('/list', {
14 controller: 'searchList',
493f83d4 15 templateUrl: '~/crmSearchAdmin/searchList.html',
475029f6
CW
16 resolve: {
17 // Load data for lists
18 savedSearches: function(crmApi4) {
19 return crmApi4('SavedSearch', 'get', {
7620b30a
CW
20 select: [
21 'id',
22 'name',
23 'label',
24 'api_entity',
b40e49df 25 'api_params',
7620b30a
CW
26 'GROUP_CONCAT(display.name ORDER BY display.id) AS display_name',
27 'GROUP_CONCAT(display.label ORDER BY display.id) AS display_label',
28 'GROUP_CONCAT(display.type:icon ORDER BY display.id) AS display_icon',
d861cc3f 29 'GROUP_CONCAT(DISTINCT group.title) AS groups'
7620b30a
CW
30 ],
31 join: [['SearchDisplay AS display'], ['Group AS group']],
475029f6
CW
32 where: [['api_entity', 'IS NOT NULL']],
33 groupBy: ['id']
34 });
35 }
36 }
37 });
2894db84
CW
38 $routeProvider.when('/create/:entity', {
39 controller: 'searchCreate',
fb687f04 40 reloadOnSearch: false,
964ecb17 41 template: '<crm-search-admin saved-search="$ctrl.savedSearch"></crm-search-admin>',
2894db84
CW
42 });
43 $routeProvider.when('/edit/:id', {
44 controller: 'searchEdit',
964ecb17 45 template: '<crm-search-admin saved-search="$ctrl.savedSearch"></crm-search-admin>',
2c7e2f4b 46 resolve: {
2894db84
CW
47 // Load saved search
48 savedSearch: function($route, crmApi4) {
49 var params = $route.current.params;
50 return crmApi4('SavedSearch', 'get', {
51 where: [['id', '=', params.id]],
52 chain: {
2badf248 53 groups: ['Group', 'get', {select: ['id', 'title', 'description', 'visibility', 'group_type'], where: [['saved_search_id', '=', '$id']]}],
2894db84
CW
54 displays: ['SearchDisplay', 'get', {where: [['saved_search_id', '=', '$id']]}]
55 }
56 }, 0);
2c7e2f4b
CW
57 }
58 }
25523059
CW
59 });
60 })
61
2894db84
CW
62 // Controller for creating a new search
63 .controller('searchCreate', function($scope, $routeParams, $location) {
64 searchEntity = $routeParams.entity;
2c7e2f4b 65 $scope.$ctrl = this;
2894db84
CW
66 this.savedSearch = {
67 api_entity: searchEntity,
68 };
25523059 69 // Changing entity will refresh the angular page
2894db84 70 $scope.$watch('$ctrl.savedSearch.api_entity', function(newEntity, oldEntity) {
25523059 71 if (newEntity && oldEntity && newEntity !== oldEntity) {
2c7e2f4b 72 $location.url('/create/' + newEntity);
25523059
CW
73 }
74 });
75 })
76
2894db84
CW
77 // Controller for editing a SavedSearch
78 .controller('searchEdit', function($scope, savedSearch) {
79 searchEntity = savedSearch.api_entity;
80 this.savedSearch = savedSearch;
81 $scope.$ctrl = this;
82 })
83
25523059
CW
84 .factory('searchMeta', function() {
85 function getEntity(entityName) {
86 if (entityName) {
4f0729ed 87 return _.find(CRM.crmSearchAdmin.schema, {name: entityName});
25523059
CW
88 }
89 }
994168e1 90 // Get join metadata matching a given expression like "Email AS Contact_Email_contact_id_01"
4f0729ed 91 function getJoin(fullNameOrAlias) {
994168e1
CW
92 var alias = _.last(fullNameOrAlias.split(' AS ')),
93 path = alias,
94 baseEntity = searchEntity,
95 label = [],
96 join,
97 result;
98 while (path.length) {
99 /* jshint -W083 */
100 join = _.find(CRM.crmSearchAdmin.joins[baseEntity], function(join) {
101 return new RegExp('^' + join.alias + '_\\d\\d').test(path);
102 });
103 if (!join) {
104 console.warn( 'Join ' + fullNameOrAlias + ' not found.');
105 return;
106 }
107 path = path.replace(join.alias + '_', '');
108 var num = parseInt(path.substr(0, 2), 10);
994168e1
CW
109 label.push(join.label + (num > 1 ? ' ' + num : ''));
110 path = path.replace(/^\d\d_?/, '');
25786bd0
CW
111 if (path.length) {
112 baseEntity = join.entity;
113 }
994168e1 114 }
25786bd0 115 result = _.assign(_.cloneDeep(join), {label: label.join(' - '), alias: alias, baseEntity: baseEntity});
994168e1
CW
116 // Add the numbered suffix to the join conditions
117 // If this is a deep join, also add the base entity prefix
118 var prefix = alias.replace(new RegExp('_?' + join.alias + '_?\\d?\\d?$'), '');
2f616560 119 function replaceRefs(condition) {
994168e1
CW
120 if (_.isArray(condition)) {
121 _.each(condition, function(ref, side) {
2f616560
CW
122 if (side !== 1 && typeof ref === 'string') {
123 if (_.includes(ref, '.')) {
124 condition[side] = ref.replace(join.alias + '.', alias + '.');
125 } else if (prefix.length && !_.includes(ref, '"') && !_.includes(ref, "'")) {
126 condition[side] = prefix + '.' + ref;
127 }
994168e1
CW
128 }
129 });
130 }
2f616560
CW
131 }
132 _.each(result.conditions, replaceRefs);
133 _.each(result.defaults, replaceRefs);
994168e1 134 return result;
4f0729ed 135 }
85b00bbf 136 function getFieldAndJoin(fieldName, entityName) {
c419e6ed 137 var dotSplit = fieldName.split('.'),
25523059 138 joinEntity = dotSplit.length > 1 ? dotSplit[0] : null,
f9cf8797 139 name = _.last(dotSplit).split(':')[0],
4f0729ed 140 join,
f9cf8797 141 field;
27bed3cb
CW
142 // Custom fields contain a dot in their fieldname
143 // If 3 segments, the first is the joinEntity and the last 2 are the custom field
144 if (dotSplit.length === 3) {
c419e6ed 145 name = dotSplit[1] + '.' + name;
27bed3cb
CW
146 }
147 // If 2 segments, it's ambiguous whether this is a custom field or joined field. Search the main entity first.
148 if (dotSplit.length === 2) {
f9cf8797 149 field = _.find(getEntity(entityName).fields, {name: dotSplit[0] + '.' + name});
27bed3cb 150 if (field) {
30d895a9 151 field.baseEntity = entityName;
85b00bbf 152 return {field: field};
27bed3cb
CW
153 }
154 }
25523059 155 if (joinEntity) {
4f0729ed
CW
156 join = getJoin(joinEntity);
157 entityName = getJoin(joinEntity).entity;
25523059 158 }
f9cf8797 159 field = _.find(getEntity(entityName).fields, {name: name});
4f0729ed
CW
160 if (!field && join && join.bridge) {
161 field = _.find(getEntity(join.bridge).fields, {name: name});
162 }
f9cf8797 163 if (field) {
30d895a9 164 field.baseEntity = entityName;
85b00bbf 165 return {field: field, join: join};
f9cf8797 166 }
25523059 167 }
03b55607 168 function parseExpr(expr) {
2ca319b3
CW
169 if (!expr) {
170 return;
171 }
172 var splitAs = expr.split(' AS '),
a3caaf9e 173 info = {fn: null, modifier: '', field: {}, alias: _.last(splitAs)},
2ca319b3
CW
174 fieldName = splitAs[0],
175 bracketPos = splitAs[0].indexOf('(');
03b55607 176 if (bracketPos >= 0) {
2ca319b3 177 var parsed = splitAs[0].substr(bracketPos).match(/[ ]?([A-Z]+[ ]+)?([\w.:]+)/);
03b55607 178 fieldName = parsed[2];
2ca319b3
CW
179 info.fn = _.find(CRM.crmSearchAdmin.functions, {name: expr.substring(0, bracketPos)});
180 info.modifier = _.trim(parsed[1]);
03b55607 181 }
2ca319b3
CW
182 var fieldAndJoin = getFieldAndJoin(fieldName, searchEntity);
183 if (fieldAndJoin) {
03b55607 184 var split = fieldName.split(':'),
85b00bbf 185 prefixPos = split[0].lastIndexOf(fieldAndJoin.field.name);
2ca319b3
CW
186 info.path = split[0];
187 info.prefix = prefixPos > 0 ? info.path.substring(0, prefixPos) : '';
188 info.suffix = !split[1] ? '' : ':' + split[1];
189 info.field = fieldAndJoin.field;
190 info.join = fieldAndJoin.join;
03b55607 191 }
2ca319b3 192 return info;
03b55607 193 }
25523059
CW
194 return {
195 getEntity: getEntity,
85b00bbf
CW
196 getField: function(fieldName, entityName) {
197 return getFieldAndJoin(fieldName, entityName).field;
198 },
4f0729ed 199 getJoin: getJoin,
03b55607
CW
200 parseExpr: parseExpr,
201 getDefaultLabel: function(col) {
202 var info = parseExpr(col),
203 label = info.field.label;
204 if (info.fn) {
205 label = '(' + info.fn.title + ') ' + label;
c419e6ed 206 }
85b00bbf
CW
207 if (info.join) {
208 label = info.join.label + ': ' + label;
209 }
03b55607 210 return label;
4b01551f
CW
211 },
212 // Find all possible search columns that could serve as contact_id for a smart group
213 getSmartGroupColumns: function(api_entity, api_params) {
25786bd0 214 var joins = _.pluck((api_params.join || []), 0);
4b01551f
CW
215 return _.transform([api_entity].concat(joins), function(columns, joinExpr) {
216 var joinName = joinExpr.split(' AS '),
25786bd0
CW
217 joinInfo = joinName[1] ? getJoin(joinName[1]) : {entity: joinName[0]},
218 entity = getEntity(joinInfo.entity),
219 prefix = joinInfo.alias ? joinInfo.alias + '.' : '';
4b01551f 220 _.each(entity.fields, function(field) {
25786bd0 221 if ((entity.name === 'Contact' && field.name === 'id') || (field.fk_entity === 'Contact' && joinInfo.baseEntity !== 'Contact')) {
4b01551f
CW
222 columns.push({
223 id: prefix + field.name,
25786bd0 224 text: (joinInfo.label ? joinInfo.label + ': ' : '') + field.label,
4b01551f
CW
225 icon: entity.icon
226 });
227 }
228 });
4b01551f 229 });
25523059
CW
230 }
231 };
25523059
CW
232 });
233
234})(angular, CRM.$, CRM._);