1 (function(angular
, $, _
) {
4 // Shared between router and searchMeta service
9 // Declare module and route/controller/services
10 angular
.module('crmSearchAdmin', CRM
.angRequires('crmSearchAdmin'))
12 .config(function($routeProvider
) {
13 $routeProvider
.when('/list', {
14 controller
: 'searchList',
15 templateUrl
: '~/crmSearchAdmin/searchList.html',
17 // Load data for lists
18 savedSearches: function(crmApi4
) {
19 return crmApi4('SavedSearch', 'get', {
26 'created.display_name',
27 'modified.display_name',
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'
36 join
: [['SearchDisplay AS display'], ['Group AS group']],
37 where
: [['api_entity', 'IS NOT NULL']],
43 $routeProvider
.when('/create/:entity', {
44 controller
: 'searchCreate',
45 reloadOnSearch
: false,
46 template
: '<crm-search-admin saved-search="$ctrl.savedSearch"></crm-search-admin>',
48 $routeProvider
.when('/edit/:id', {
49 controller
: 'searchEdit',
50 template
: '<crm-search-admin saved-search="$ctrl.savedSearch"></crm-search-admin>',
53 savedSearch: function($route
, crmApi4
) {
54 var params
= $route
.current
.params
;
55 return crmApi4('SavedSearch', 'get', {
56 where
: [['id', '=', params
.id
]],
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']]}]
67 // Controller for creating a new search
68 .controller('searchCreate', function($scope
, $routeParams
, $location
) {
69 searchEntity
= $routeParams
.entity
;
72 api_entity
: searchEntity
,
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
);
82 // Controller for editing a SavedSearch
83 .controller('searchEdit', function($scope
, savedSearch
) {
84 searchEntity
= savedSearch
.api_entity
;
85 this.savedSearch
= savedSearch
;
89 .factory('searchMeta', function($q
) {
90 function getEntity(entityName
) {
92 return _
.find(CRM
.crmSearchAdmin
.schema
, {name
: entityName
});
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 ')),
99 baseEntity
= searchEntity
,
103 while (path
.length
) {
105 join
= _
.find(CRM
.crmSearchAdmin
.joins
[baseEntity
], function(join
) {
106 return new RegExp('^' + join
.alias
+ '_\\d\\d').test(path
);
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_?/, '');
116 baseEntity
= join
.entity
;
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
;
136 _
.each(result
.conditions
, replaceRefs
);
137 _
.each(result
.defaults
, replaceRefs
);
140 function getFieldAndJoin(fieldName
, entityName
) {
141 var fieldPath
= fieldName
.split(':')[0],
142 dotSplit
= fieldPath
.split('.'),
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]);
151 entityName
= join
.entity
;
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
});
160 field
.baseEntity
= entityName
;
161 return {field
: field
, join
: join
};
164 function parseExpr(expr
) {
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]);
178 var fieldAndJoin
= getFieldAndJoin(fieldName
, searchEntity
);
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
;
191 getEntity
: getEntity
,
192 getField: function(fieldName
, entityName
) {
193 return getFieldAndJoin(fieldName
, entityName
).field
;
196 parseExpr
: parseExpr
,
197 getDefaultLabel: function(col
) {
198 var info
= parseExpr(col
),
199 label
= info
.field
.label
;
201 label
= '(' + info
.fn
.title
+ ') ' + label
;
204 label
= info
.join
.label
+ ': ' + label
;
208 // Find all possible search columns that could serve as contact_id for a smart group
209 getSmartGroupColumns: function(api_entity
, api_params
) {
210 var joins
= _
.pluck((api_params
.join
|| []), 0);
211 return _
.transform([api_entity
].concat(joins
), function(columns
, joinExpr
) {
212 var joinName
= joinExpr
.split(' AS '),
213 joinInfo
= joinName
[1] ? getJoin(joinName
[1]) : {entity
: joinName
[0]},
214 entity
= getEntity(joinInfo
.entity
),
215 prefix
= joinInfo
.alias
? joinInfo
.alias
+ '.' : '';
216 _
.each(entity
.fields
, function(field
) {
217 if ((entity
.name
=== 'Contact' && field
.name
=== 'id') || (field
.fk_entity
=== 'Contact' && joinInfo
.baseEntity
!== 'Contact')) {
219 id
: prefix
+ field
.name
,
220 text
: (joinInfo
.label
? joinInfo
.label
+ ': ' : '') + field
.label
,
227 pickIcon: function() {
228 var deferred
= $q
.defer();
229 $('#crm-search-admin-icon-picker').off('change').siblings('.crm-icon-picker-button').click();
230 $('#crm-search-admin-icon-picker').on('change', function() {
231 deferred
.resolve($(this).val());
233 return deferred
.promise
;
237 .directive('contenteditable', function() {
240 link: function(scope
, elm
, attrs
, ctrl
) {
242 elm
.on('blur', function() {
243 ctrl
.$setViewValue(elm
.html());
247 ctrl
.$render = function() {
248 elm
.html(ctrl
.$viewValue
);
254 // Shoehorn in a non-angular widget for picking icons
256 $('#crm-container').append('<div style="display:none"><input id="crm-search-admin-icon-picker"></div>');
257 CRM
.loadScript(CRM
.config
.resourceBase
+ 'js/jquery/jquery.crmIconPicker.js').done(function() {
258 $('#crm-search-admin-icon-picker').crmIconPicker();
262 })(angular
, CRM
.$, CRM
._
);