1 (function(angular
, $, _
) {
2 // example: <select multiple crm-mailing-recipients crm-mailing="mymailing" crm-avail-groups="myGroups" crm-avail-mailings="myMailings"></select>
3 // FIXME: participate in ngModel's validation cycle
4 angular
.module('crmMailing').directive('crmMailingRecipients', function(crmUiAlert
) {
11 link: function(scope
, element
, attrs
, ngModel
) {
12 scope
.recips
= ngModel
.$viewValue
;
13 scope
.groups
= scope
.$parent
.$eval(attrs
.crmAvailGroups
);
14 scope
.mailings
= scope
.$parent
.$eval(attrs
.crmAvailMailings
);
17 var ts
= scope
.ts
= CRM
.ts(null);
19 /// Convert MySQL date ("yyyy-mm-dd hh:mm:ss") to JS date object
20 scope
.parseDate = function(date
) {
21 if (!angular
.isString(date
)) {
24 var p
= date
.split(/[\- :]/);
25 return new Date(parseInt(p
[0]), parseInt(p
[1]) - 1, parseInt(p
[2]), parseInt(p
[3]), parseInt(p
[4]), parseInt(p
[5]));
28 /// Remove {value} from {array}
29 function arrayRemove(array
, value
) {
30 var idx
= array
.indexOf(value
);
36 // @param string id an encoded string like "4 civicrm_mailing include"
37 // @return Object keys: entity_id, entity_type, mode
38 function convertValueToObj(id
) {
39 var a
= id
.split(" ");
40 return {entity_id
: parseInt(a
[0]), entity_type
: a
[1], mode
: a
[2]};
43 // @param Object mailing
44 // @return array list of values like "4 civicrm_mailing include"
45 function convertMailingToValues(recipients
) {
47 angular
.forEach(recipients
.groups
.include
, function(v
) {
48 r
.push(v
+ " civicrm_group include");
50 angular
.forEach(recipients
.groups
.exclude
, function(v
) {
51 r
.push(v
+ " civicrm_group exclude");
53 angular
.forEach(recipients
.mailings
.include
, function(v
) {
54 r
.push(v
+ " civicrm_mailing include");
56 angular
.forEach(recipients
.mailings
.exclude
, function(v
) {
57 r
.push(v
+ " civicrm_mailing exclude");
62 function refreshMandatory() {
63 if (ngModel
.$viewValue
&& ngModel
.$viewValue
.groups
) {
64 scope
.mandatoryGroups
= _
.filter(scope
.$parent
.$eval(attrs
.crmMandatoryGroups
), function(grp
) {
65 return _
.contains(ngModel
.$viewValue
.groups
.include
, parseInt(grp
.id
));
67 scope
.mandatoryIds
= _
.map(_
.pluck(scope
.$parent
.$eval(attrs
.crmMandatoryGroups
), 'id'), function(n
) {
72 scope
.mandatoryGroups
= [];
73 scope
.mandatoryIds
= [];
77 function isMandatory(grpId
) {
78 return _
.contains(scope
.mandatoryIds
, parseInt(grpId
));
81 var refreshUI
= ngModel
.$render
= function refresuhUI() {
82 scope
.recips
= ngModel
.$viewValue
;
83 if (ngModel
.$viewValue
) {
84 $(element
).select2('val', convertMailingToValues(ngModel
.$viewValue
));
90 // @return string HTML representing an option
91 function formatItem(item
) {
93 // return `text` for optgroup
96 var option
= convertValueToObj(item
.id
);
97 var icon
= (option
.entity_type
=== 'civicrm_mailing') ? 'fa-envelope' : 'fa-users';
98 var smartGroupMarker
= item
.is_smart
? '* ' : '';
99 var spanClass
= (option
.mode
== 'exclude') ? 'crmMailing-exclude' : 'crmMailing-include';
100 if (option
.entity_type
!= 'civicrm_mailing' && isMandatory(option
.entity_id
)) {
101 spanClass
= 'crmMailing-mandatory';
103 return '<i class="crm-i '+icon
+'"></i> <span class="' + spanClass
+ '">' + smartGroupMarker
+ item
.text
+ '</span>';
106 function validate() {
107 if (scope
.$parent
.$eval(attrs
.ngRequired
)) {
108 var empty
= (_
.isEmpty(ngModel
.$viewValue
.groups
.include
) && _
.isEmpty(ngModel
.$viewValue
.mailings
.include
));
109 ngModel
.$setValidity('empty', !empty
);
112 ngModel
.$setValidity('empty', true);
118 entity
: 'civicrm_group',
126 dropdownAutoWidth
: true,
127 placeholder
: "Groups or Past Recipients",
128 formatResult
: formatItem
,
129 formatSelection
: formatItem
,
130 escapeMarkup: function(m
) {
134 initSelection: function(el
, cb
) {
135 var values
= el
.val().split(',');
140 for (var i
= 0; i
< values
.length
; i
++) {
141 var dv
= convertValueToObj(values
[i
]);
142 if (dv
.entity_type
== 'civicrm_group') {
143 gids
.push(dv
.entity_id
);
145 else if (dv
.entity_type
== 'civicrm_mailing') {
146 mids
.push(dv
.entity_id
);
149 // push non existant 0 group/mailing id in order when no recipents group or prior mailing is selected
150 // this will allow to resuse the below code to handle datamap
151 if (gids
.length
=== 0) {
154 if (mids
.length
=== 0) {
158 CRM
.api3('Group', 'getlist', { params
: { id
: { IN
: gids
}, options
: { limit
: 0 } }, extra
: ["is_hidden"] }).then(
160 CRM
.api3('Mailing', 'getlist', { params
: { id
: { IN
: mids
}, options
: { limit
: 0 } } }).then(
167 $(glist
.values
).each(function (idx
, group
) {
168 var key
= group
.id
+ ' civicrm_group include';
170 groupNames
.push({id
: parseInt(group
.id
), title
: group
.label
, is_hidden
: group
.extra
.is_hidden
});
171 if (values
.indexOf(key
) >= 0) {
172 datamap
.push({id
: key
, text
: group
.label
});
175 key
= group
.id
+ ' civicrm_group exclude';
176 if (values
.indexOf(key
) >= 0) {
177 datamap
.push({id
: key
, text
: group
.label
});
181 $(mlist
.values
).each(function (idx
, group
) {
182 var key
= group
.id
+ ' civicrm_mailing include';
183 civiMails
.push({id
: parseInt(group
.id
), name
: group
.label
});
185 if (values
.indexOf(key
) >= 0) {
186 datamap
.push({id
: key
, text
: group
.label
});
189 key
= group
.id
+ ' civicrm_mailing exclude';
190 if (values
.indexOf(key
) >= 0) {
191 datamap
.push({id
: key
, text
: group
.label
});
195 scope
.$parent
.crmMailingConst
.groupNames
= groupNames
;
196 scope
.$parent
.crmMailingConst
.civiMails
= civiMails
;
205 url
: CRM
.url('civicrm/ajax/rest'),
207 data: function(input
, page_num
) {
211 entity
: 'civicrm_group',
217 rcpAjaxState
.page_i
= page_num
- rcpAjaxState
.page_n
;
218 var filterParams
= {};
219 switch(rcpAjaxState
.entity
) {
220 case 'civicrm_group':
221 filterParams
= { is_hidden
: 0, is_active
: 1, group_type
: {"LIKE": "%2%"} };
224 case 'civicrm_mailing':
225 filterParams
= { is_hidden
: 0, is_active
: 1 };
230 page_num
: rcpAjaxState
.page_i
,
231 params
: filterParams
,
234 if('civicrm_mailing' === rcpAjaxState
.entity
) {
235 params
["api.MailingRecipients.getcount"] = {};
237 else if ('civicrm_group' === rcpAjaxState
.entity
) {
238 params
.extra
= ["saved_search_id"];
243 transport: function(params
) {
244 switch(rcpAjaxState
.entity
) {
245 case 'civicrm_group':
246 CRM
.api3('Group', 'getlist', params
.data
).then(params
.success
, params
.error
);
249 case 'civicrm_mailing':
250 params
.data
.params
.options
= { sort
: "is_archived asc, scheduled_date desc" };
251 CRM
.api3('Mailing', 'getlist', params
.data
).then(params
.success
, params
.error
);
255 results: function(data
) {
257 children
: $.map(data
.values
, function(obj
) {
258 if('civicrm_mailing' === rcpAjaxState
.entity
) {
259 return obj
["api.MailingRecipients.getcount"] > 0 ? { id
: obj
.id
+ ' ' + rcpAjaxState
.entity
+ ' ' + rcpAjaxState
.type
,
260 text
: obj
.label
} : '';
263 return { id
: obj
.id
+ ' ' + rcpAjaxState
.entity
+ ' ' + rcpAjaxState
.type
, text
: obj
.label
,
264 is_smart
: (!_
.isEmpty(obj
.extra
.saved_search_id
)) };
269 if (rcpAjaxState
.page_i
== 1 && data
.count
&& results
.children
.length
> 0) {
270 results
.text
= ts((rcpAjaxState
.type
== 'include'? 'Include ' : 'Exclude ') +
271 (rcpAjaxState
.entity
== 'civicrm_group'? 'Group' : 'Mailing'));
274 more
= data
.more_results
|| !(rcpAjaxState
.entity
== 'civicrm_mailing' && rcpAjaxState
.type
== 'exclude');
276 if (more
&& !data
.more_results
) {
277 if (rcpAjaxState
.type
== 'include') {
278 rcpAjaxState
.type
= 'exclude';
280 rcpAjaxState
.type
= 'include';
281 rcpAjaxState
.entity
= 'civicrm_mailing';
283 rcpAjaxState
.page_n
+= rcpAjaxState
.page_i
;
286 return { more
: more
, results
: [ results
] };
291 $(element
).on('select2-selecting', function(e
) {
292 var option
= convertValueToObj(e
.val
);
293 var typeKey
= option
.entity_type
== 'civicrm_mailing' ? 'mailings' : 'groups';
294 if (option
.mode
== 'exclude') {
295 ngModel
.$viewValue
[typeKey
].exclude
.push(option
.entity_id
);
296 arrayRemove(ngModel
.$viewValue
[typeKey
].include
, option
.entity_id
);
299 ngModel
.$viewValue
[typeKey
].include
.push(option
.entity_id
);
300 arrayRemove(ngModel
.$viewValue
[typeKey
].exclude
, option
.entity_id
);
303 $(element
).select2('close');
308 $(element
).on("select2-removing", function(e
) {
309 var option
= convertValueToObj(e
.val
);
310 var typeKey
= option
.entity_type
== 'civicrm_mailing' ? 'mailings' : 'groups';
311 if (typeKey
== 'groups' && isMandatory(option
.entity_id
)) {
313 text
: ts('This mailing was generated based on search results. The search results cannot be removed.'),
314 title
: ts('Required')
319 scope
.$parent
.$apply(function() {
320 arrayRemove(ngModel
.$viewValue
[typeKey
][option
.mode
], option
.entity_id
);
326 scope
.$watchCollection("recips.groups.include", refreshUI
);
327 scope
.$watchCollection("recips.groups.exclude", refreshUI
);
328 scope
.$watchCollection("recips.mailings.include", refreshUI
);
329 scope
.$watchCollection("recips.mailings.exclude", refreshUI
);
330 setTimeout(refreshUI
, 50);
332 scope
.$watchCollection(attrs
.crmAvailGroups
, function() {
333 scope
.groups
= scope
.$parent
.$eval(attrs
.crmAvailGroups
);
335 scope
.$watchCollection(attrs
.crmAvailMailings
, function() {
336 scope
.mailings
= scope
.$parent
.$eval(attrs
.crmAvailMailings
);
338 scope
.$watchCollection(attrs
.crmMandatoryGroups
, function() {
345 })(angular
, CRM
.$, CRM
._
);