Merge pull request #21057 from eileenmcnaughton/tok_resolve
[civicrm-core.git] / ext / search_kit / ang / crmSearchAdmin.module.js
1 (function(angular, $, _) {
2 "use strict";
3
4 // Shared between router and searchMeta service
5 var searchEntity,
6 joinIndex,
7 undefined;
8
9 // Declare module and route/controller/services
10 angular.module('crmSearchAdmin', CRM.angRequires('crmSearchAdmin'))
11
12 .config(function($routeProvider) {
13 $routeProvider.when('/list', {
14 controller: 'searchList',
15 templateUrl: '~/crmSearchAdmin/searchList.html',
16 resolve: {
17 // Load data for lists
18 savedSearches: function(crmApi4) {
19 return crmApi4('SavedSearch', 'get', {
20 select: [
21 'id',
22 'name',
23 'label',
24 'api_entity',
25 'api_params',
26 'created_id.display_name',
27 'modified_id.display_name',
28 'created_date',
29 'modified_date',
30 'GROUP_CONCAT(display.name ORDER BY display.id) AS display_name',
31 'GROUP_CONCAT(display.label ORDER BY display.id) AS display_label',
32 'GROUP_CONCAT(display.type:icon ORDER BY display.id) AS display_icon',
33 'GROUP_CONCAT(display.acl_bypass ORDER BY display.id) AS display_acl_bypass',
34 'GROUP_CONCAT(DISTINCT group.title) AS groups'
35 ],
36 join: [['SearchDisplay AS display'], ['Group AS group']],
37 where: [['api_entity', 'IS NOT NULL']],
38 groupBy: ['id']
39 });
40 }
41 }
42 });
43 $routeProvider.when('/create/:entity', {
44 controller: 'searchCreate',
45 reloadOnSearch: false,
46 template: '<crm-search-admin saved-search="$ctrl.savedSearch"></crm-search-admin>',
47 });
48 $routeProvider.when('/edit/:id', {
49 controller: 'searchEdit',
50 template: '<crm-search-admin saved-search="$ctrl.savedSearch"></crm-search-admin>',
51 resolve: {
52 // Load saved search
53 savedSearch: function($route, crmApi4) {
54 var params = $route.current.params;
55 return crmApi4('SavedSearch', 'get', {
56 where: [['id', '=', params.id]],
57 chain: {
58 groups: ['Group', 'get', {select: ['id', 'title', 'description', 'visibility', 'group_type', 'custom.*'], where: [['saved_search_id', '=', '$id']]}],
59 displays: ['SearchDisplay', 'get', {where: [['saved_search_id', '=', '$id']]}]
60 }
61 }, 0);
62 }
63 }
64 });
65 })
66
67 // Controller for creating a new search
68 .controller('searchCreate', function($scope, $routeParams, $location) {
69 searchEntity = $routeParams.entity;
70 $scope.$ctrl = this;
71 this.savedSearch = {
72 api_entity: searchEntity,
73 };
74 // Changing entity will refresh the angular page
75 $scope.$watch('$ctrl.savedSearch.api_entity', function(newEntity, oldEntity) {
76 if (newEntity && oldEntity && newEntity !== oldEntity) {
77 $location.url('/create/' + newEntity);
78 }
79 });
80 })
81
82 // Controller for editing a SavedSearch
83 .controller('searchEdit', function($scope, savedSearch) {
84 searchEntity = savedSearch.api_entity;
85 this.savedSearch = savedSearch;
86 $scope.$ctrl = this;
87 })
88
89 .factory('searchMeta', function($q) {
90 function getEntity(entityName) {
91 if (entityName) {
92 return _.find(CRM.crmSearchAdmin.schema, {name: entityName});
93 }
94 }
95 // Get join metadata matching a given expression like "Email AS Contact_Email_contact_id_01"
96 function getJoin(fullNameOrAlias) {
97 var alias = _.last(fullNameOrAlias.split(' AS ')),
98 path = alias,
99 baseEntity = searchEntity,
100 label = [],
101 join,
102 result;
103 while (path.length) {
104 /* jshint -W083 */
105 join = _.find(CRM.crmSearchAdmin.joins[baseEntity], function(join) {
106 return new RegExp('^' + join.alias + '_\\d\\d').test(path);
107 });
108 if (!join) {
109 return;
110 }
111 path = path.replace(join.alias + '_', '');
112 var num = parseInt(path.substr(0, 2), 10);
113 label.push(join.label + (num > 1 ? ' ' + num : ''));
114 path = path.replace(/^\d\d_?/, '');
115 if (path.length) {
116 baseEntity = join.entity;
117 }
118 }
119 result = _.assign(_.cloneDeep(join), {label: label.join(' - '), alias: alias, baseEntity: baseEntity});
120 // Add the numbered suffix to the join conditions
121 // If this is a deep join, also add the base entity prefix
122 var prefix = alias.replace(new RegExp('_?' + join.alias + '_?\\d?\\d?$'), '');
123 function replaceRefs(condition) {
124 if (_.isArray(condition)) {
125 _.each(condition, function(ref, side) {
126 if (side !== 1 && typeof ref === 'string') {
127 if (_.includes(ref, '.')) {
128 condition[side] = ref.replace(join.alias + '.', alias + '.');
129 } else if (prefix.length && !_.includes(ref, '"') && !_.includes(ref, "'")) {
130 condition[side] = prefix + '.' + ref;
131 }
132 }
133 });
134 }
135 }
136 _.each(result.conditions, replaceRefs);
137 _.each(result.defaults, replaceRefs);
138 return result;
139 }
140 function getFieldAndJoin(fieldName, entityName) {
141 var fieldPath = fieldName.split(':')[0],
142 dotSplit = fieldPath.split('.'),
143 name,
144 join,
145 field;
146 // If 2 or more segments, the first might be the name of a join
147 if (dotSplit.length > 1) {
148 join = getJoin(dotSplit[0]);
149 if (join) {
150 dotSplit.shift();
151 entityName = join.entity;
152 }
153 }
154 name = dotSplit.join('.');
155 field = _.find(getEntity(entityName).fields, {name: name});
156 if (!field && join && join.bridge) {
157 field = _.find(getEntity(join.bridge).fields, {name: name});
158 }
159 if (field) {
160 field.baseEntity = entityName;
161 return {field: field, join: join};
162 }
163 }
164 function parseExpr(expr) {
165 if (!expr) {
166 return;
167 }
168 var splitAs = expr.split(' AS '),
169 info = {fn: null, modifier: '', field: {}, alias: _.last(splitAs)},
170 fieldName = splitAs[0],
171 bracketPos = splitAs[0].indexOf('(');
172 if (bracketPos >= 0) {
173 var parsed = splitAs[0].substr(bracketPos).match(/[ ]?([A-Z]+[ ]+)?([\w.:]+)/);
174 fieldName = parsed[2];
175 info.fn = _.find(CRM.crmSearchAdmin.functions, {name: expr.substring(0, bracketPos)});
176 info.modifier = _.trim(parsed[1]);
177 }
178 var fieldAndJoin = getFieldAndJoin(fieldName, searchEntity);
179 if (fieldAndJoin) {
180 var split = fieldName.split(':'),
181 prefixPos = split[0].lastIndexOf(fieldAndJoin.field.name);
182 info.path = split[0];
183 info.prefix = prefixPos > 0 ? info.path.substring(0, prefixPos) : '';
184 info.suffix = !split[1] ? '' : ':' + split[1];
185 info.field = fieldAndJoin.field;
186 info.join = fieldAndJoin.join;
187 }
188 return info;
189 }
190 function getDefaultLabel(col) {
191 var info = parseExpr(col),
192 label = info.field.label;
193 if (info.fn) {
194 label = '(' + info.fn.title + ') ' + label;
195 }
196 if (info.join) {
197 label = info.join.label + ': ' + label;
198 }
199 return label;
200 }
201 function fieldToColumn(fieldExpr, defaults) {
202 var info = parseExpr(fieldExpr),
203 values = _.merge({
204 type: 'field',
205 key: info.alias,
206 dataType: (info.fn && info.fn.dataType) || (info.field && info.field.data_type)
207 }, defaults);
208 if (defaults.label) {
209 values.label = getDefaultLabel(fieldExpr);
210 }
211 return values;
212 }
213 return {
214 getEntity: getEntity,
215 getField: function(fieldName, entityName) {
216 return getFieldAndJoin(fieldName, entityName).field;
217 },
218 getJoin: getJoin,
219 parseExpr: parseExpr,
220 getDefaultLabel: getDefaultLabel,
221 fieldToColumn: fieldToColumn,
222 // Find all possible search columns that could serve as contact_id for a smart group
223 getSmartGroupColumns: function(api_entity, api_params) {
224 var joins = _.pluck((api_params.join || []), 0);
225 return _.transform([api_entity].concat(joins), function(columns, joinExpr) {
226 var joinName = joinExpr.split(' AS '),
227 joinInfo = joinName[1] ? getJoin(joinName[1]) : {entity: joinName[0]},
228 entity = getEntity(joinInfo.entity),
229 prefix = joinInfo.alias ? joinInfo.alias + '.' : '';
230 _.each(entity.fields, function(field) {
231 if ((entity.name === 'Contact' && field.name === 'id') || (field.fk_entity === 'Contact' && joinInfo.baseEntity !== 'Contact')) {
232 columns.push({
233 id: prefix + field.name,
234 text: (joinInfo.label ? joinInfo.label + ': ' : '') + field.label,
235 icon: entity.icon
236 });
237 }
238 });
239 });
240 },
241 pickIcon: function() {
242 var deferred = $q.defer();
243 $('#crm-search-admin-icon-picker').off('change').siblings('.crm-icon-picker-button').click();
244 $('#crm-search-admin-icon-picker').on('change', function() {
245 deferred.resolve($(this).val());
246 });
247 return deferred.promise;
248 }
249 };
250 })
251 .directive('contenteditable', function() {
252 return {
253 require: 'ngModel',
254 link: function(scope, elm, attrs, ctrl) {
255 // view -> model
256 elm.on('blur', function() {
257 ctrl.$setViewValue(elm.html());
258 });
259
260 // model -> view
261 ctrl.$render = function() {
262 elm.html(ctrl.$viewValue);
263 };
264 }
265 };
266 });
267
268 // Shoehorn in a non-angular widget for picking icons
269 $(function() {
270 $('#crm-container').append('<div style="display:none"><input id="crm-search-admin-icon-picker"></div>');
271 CRM.loadScript(CRM.config.resourceBase + 'js/jquery/jquery.crmIconPicker.js').done(function() {
272 $('#crm-search-admin-icon-picker').crmIconPicker();
273 });
274 });
275
276 })(angular, CRM.$, CRM._);