| 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) { |
| 5 | return { |
| 6 | restrict: 'AE', |
| 7 | require: 'ngModel', |
| 8 | scope: { |
| 9 | ngRequired: '@' |
| 10 | }, |
| 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); |
| 15 | refreshMandatory(); |
| 16 | |
| 17 | var ts = scope.ts = CRM.ts(null); |
| 18 | |
| 19 | /// Convert MySQL date ("yyyy-mm-dd hh:mm:ss") to JS date object |
| 20 | scope.parseDate = function(date) { |
| 21 | if (!angular.isString(date)) { |
| 22 | return date; |
| 23 | } |
| 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])); |
| 26 | }; |
| 27 | |
| 28 | /// Remove {value} from {array} |
| 29 | function arrayRemove(array, value) { |
| 30 | var idx = array.indexOf(value); |
| 31 | if (idx >= 0) { |
| 32 | array.splice(idx, 1); |
| 33 | } |
| 34 | } |
| 35 | |
| 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]}; |
| 41 | } |
| 42 | |
| 43 | // @param Object mailing |
| 44 | // @return array list of values like "4 civicrm_mailing include" |
| 45 | function convertMailingToValues(recipients) { |
| 46 | var r = []; |
| 47 | angular.forEach(recipients.groups.include, function(v) { |
| 48 | r.push(v + " civicrm_group include"); |
| 49 | }); |
| 50 | angular.forEach(recipients.groups.exclude, function(v) { |
| 51 | r.push(v + " civicrm_group exclude"); |
| 52 | }); |
| 53 | angular.forEach(recipients.mailings.include, function(v) { |
| 54 | r.push(v + " civicrm_mailing include"); |
| 55 | }); |
| 56 | angular.forEach(recipients.mailings.exclude, function(v) { |
| 57 | r.push(v + " civicrm_mailing exclude"); |
| 58 | }); |
| 59 | return r; |
| 60 | } |
| 61 | |
| 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)); |
| 66 | }); |
| 67 | scope.mandatoryIds = _.map(_.pluck(scope.$parent.$eval(attrs.crmMandatoryGroups), 'id'), function(n) { |
| 68 | return parseInt(n); |
| 69 | }); |
| 70 | } |
| 71 | else { |
| 72 | scope.mandatoryGroups = []; |
| 73 | scope.mandatoryIds = []; |
| 74 | } |
| 75 | } |
| 76 | |
| 77 | function isMandatory(grpId) { |
| 78 | return _.contains(scope.mandatoryIds, parseInt(grpId)); |
| 79 | } |
| 80 | |
| 81 | var refreshUI = ngModel.$render = function refresuhUI() { |
| 82 | scope.recips = ngModel.$viewValue; |
| 83 | if (ngModel.$viewValue) { |
| 84 | $(element).select2('val', convertMailingToValues(ngModel.$viewValue)); |
| 85 | validate(); |
| 86 | refreshMandatory(); |
| 87 | } |
| 88 | }; |
| 89 | |
| 90 | // @return string HTML representing an option |
| 91 | function formatItem(item) { |
| 92 | if (!item.id) { |
| 93 | // return `text` for optgroup |
| 94 | return item.text; |
| 95 | } |
| 96 | var option = convertValueToObj(item.id); |
| 97 | var icon = (option.entity_type === 'civicrm_mailing') ? 'fa-envelope' : 'fa-users'; |
| 98 | var spanClass = (option.mode == 'exclude') ? 'crmMailing-exclude' : 'crmMailing-include'; |
| 99 | if (option.entity_type != 'civicrm_mailing' && isMandatory(option.entity_id)) { |
| 100 | spanClass = 'crmMailing-mandatory'; |
| 101 | } |
| 102 | return '<i class="crm-i '+icon+'"></i> <span class="' + spanClass + '">' + smartGroupMarker + item.text + '</span>'; |
| 103 | } |
| 104 | |
| 105 | function validate() { |
| 106 | if (scope.$parent.$eval(attrs.ngRequired)) { |
| 107 | var empty = (_.isEmpty(ngModel.$viewValue.groups.include) && _.isEmpty(ngModel.$viewValue.mailings.include)); |
| 108 | ngModel.$setValidity('empty', !empty); |
| 109 | } |
| 110 | else { |
| 111 | ngModel.$setValidity('empty', true); |
| 112 | } |
| 113 | } |
| 114 | |
| 115 | var rcpAjaxState = { |
| 116 | input: '', |
| 117 | entity: 'civicrm_group', |
| 118 | type: 'include', |
| 119 | page_n: 0, |
| 120 | page_i: 0, |
| 121 | }; |
| 122 | |
| 123 | $(element).select2({ |
| 124 | width: '36em', |
| 125 | dropdownAutoWidth: true, |
| 126 | placeholder: "Groups or Past Recipients", |
| 127 | formatResult: formatItem, |
| 128 | formatSelection: formatItem, |
| 129 | escapeMarkup: function(m) { |
| 130 | return m; |
| 131 | }, |
| 132 | multiple: true, |
| 133 | initSelection: function(el, cb) { |
| 134 | var values = el.val().split(','); |
| 135 | |
| 136 | var gids = []; |
| 137 | var mids = []; |
| 138 | |
| 139 | for (var i = 0; i < values.length; i++) { |
| 140 | var dv = convertValueToObj(values[i]); |
| 141 | if (dv.entity_type == 'civicrm_group') { |
| 142 | gids.push(dv.entity_id); |
| 143 | } |
| 144 | else if (dv.entity_type == 'civicrm_mailing') { |
| 145 | mids.push(dv.entity_id); |
| 146 | } |
| 147 | } |
| 148 | // push non existant 0 group/mailing id in order when no recipents group or prior mailing is selected |
| 149 | // this will allow to resuse the below code to handle datamap |
| 150 | if (gids.length === 0) { |
| 151 | gids.push(0); |
| 152 | } |
| 153 | if (mids.length === 0) { |
| 154 | mids.push(0); |
| 155 | } |
| 156 | |
| 157 | CRM.api3('Group', 'getlist', { params: { id: { IN: gids }, options: { limit: 0 } }, extra: ["is_hidden"] } ).then( |
| 158 | function(glist) { |
| 159 | CRM.api3('Mailing', 'getlist', { params: { id: { IN: mids }, options: { limit: 0 } } }).then( |
| 160 | function(mlist) { |
| 161 | var datamap = []; |
| 162 | |
| 163 | var groupNames = []; |
| 164 | var civiMails = []; |
| 165 | |
| 166 | $(glist.values).each(function (idx, group) { |
| 167 | var key = group.id + ' civicrm_group include'; |
| 168 | groupNames.push({id: parseInt(group.id), title: group.label, is_hidden: group.extra.is_hidden}); |
| 169 | if (values.indexOf(key) >= 0) { |
| 170 | datamap.push({id: key, text: group.label}); |
| 171 | } |
| 172 | |
| 173 | key = group.id + ' civicrm_group exclude'; |
| 174 | if (values.indexOf(key) >= 0) { |
| 175 | datamap.push({id: key, text: group.label}); |
| 176 | } |
| 177 | }); |
| 178 | |
| 179 | $(mlist.values).each(function (idx, group) { |
| 180 | var key = group.id + ' civicrm_mailing include'; |
| 181 | civiMails.push({id: parseInt(group.id), name: group.label}); |
| 182 | |
| 183 | if (values.indexOf(key) >= 0) { |
| 184 | datamap.push({id: key, text: group.label}); |
| 185 | } |
| 186 | |
| 187 | key = group.id + ' civicrm_mailing exclude'; |
| 188 | if (values.indexOf(key) >= 0) { |
| 189 | datamap.push({id: key, text: group.label}); |
| 190 | } |
| 191 | }); |
| 192 | |
| 193 | scope.$parent.crmMailingConst.groupNames = groupNames; |
| 194 | scope.$parent.crmMailingConst.civiMails = civiMails; |
| 195 | |
| 196 | refreshMandatory(); |
| 197 | |
| 198 | cb(datamap); |
| 199 | }); |
| 200 | }); |
| 201 | }, |
| 202 | ajax: { |
| 203 | url: CRM.url('civicrm/ajax/rest'), |
| 204 | quietMillis: 300, |
| 205 | data: function(input, page_num) { |
| 206 | if (page_num <= 1) { |
| 207 | rcpAjaxState = { |
| 208 | input: input, |
| 209 | entity: 'civicrm_group', |
| 210 | type: 'include', |
| 211 | page_n: 0, |
| 212 | }; |
| 213 | } |
| 214 | |
| 215 | rcpAjaxState.page_i = page_num - rcpAjaxState.page_n; |
| 216 | var filterParams = {}; |
| 217 | switch(rcpAjaxState.entity) { |
| 218 | case 'civicrm_group': |
| 219 | filterParams = { is_hidden: 0, is_active: 1, group_type: {"LIKE": "%2%"} }; |
| 220 | break; |
| 221 | |
| 222 | case 'civicrm_mailing': |
| 223 | filterParams = { is_hidden: 0, is_active: 1 }; |
| 224 | break; |
| 225 | } |
| 226 | var params = { |
| 227 | input: input, |
| 228 | page_num: rcpAjaxState.page_i, |
| 229 | params: filterParams, |
| 230 | }; |
| 231 | |
| 232 | if('civicrm_mailing' === rcpAjaxState.entity) { |
| 233 | params["api.MailingRecipients.getcount"] = {}; |
| 234 | } |
| 235 | |
| 236 | return params; |
| 237 | }, |
| 238 | transport: function(params) { |
| 239 | switch(rcpAjaxState.entity) { |
| 240 | case 'civicrm_group': |
| 241 | CRM.api3('Group', 'getlist', params.data).then(params.success, params.error); |
| 242 | break; |
| 243 | |
| 244 | case 'civicrm_mailing': |
| 245 | params.data.params.options = { sort: "is_archived asc, scheduled_date desc" }; |
| 246 | CRM.api3('Mailing', 'getlist', params.data).then(params.success, params.error); |
| 247 | break; |
| 248 | } |
| 249 | }, |
| 250 | results: function(data) { |
| 251 | results = { |
| 252 | children: $.map(data.values, function(obj) { |
| 253 | if('civicrm_mailing' === rcpAjaxState.entity) { |
| 254 | return obj["api.MailingRecipients.getcount"] > 0 ? { id: obj.id + ' ' + rcpAjaxState.entity + ' ' + rcpAjaxState.type, |
| 255 | text: obj.label } : ''; |
| 256 | } |
| 257 | else { |
| 258 | return { id: obj.id + ' ' + rcpAjaxState.entity + ' ' + rcpAjaxState.type, |
| 259 | text: obj.label }; |
| 260 | } |
| 261 | }) |
| 262 | }; |
| 263 | |
| 264 | if (rcpAjaxState.page_i == 1 && data.count && results.children.length > 0) { |
| 265 | results.text = ts((rcpAjaxState.type == 'include'? 'Include ' : 'Exclude ') + |
| 266 | (rcpAjaxState.entity == 'civicrm_group'? 'Group' : 'Mailing')); |
| 267 | } |
| 268 | |
| 269 | more = data.more_results || !(rcpAjaxState.entity == 'civicrm_mailing' && rcpAjaxState.type == 'exclude'); |
| 270 | |
| 271 | if (more && !data.more_results) { |
| 272 | if (rcpAjaxState.type == 'include') { |
| 273 | rcpAjaxState.type = 'exclude'; |
| 274 | } else { |
| 275 | rcpAjaxState.type = 'include'; |
| 276 | rcpAjaxState.entity = 'civicrm_mailing'; |
| 277 | } |
| 278 | rcpAjaxState.page_n += rcpAjaxState.page_i; |
| 279 | } |
| 280 | |
| 281 | return { more: more, results: [ results ] }; |
| 282 | }, |
| 283 | }, |
| 284 | }); |
| 285 | |
| 286 | $(element).on('select2-selecting', function(e) { |
| 287 | var option = convertValueToObj(e.val); |
| 288 | var typeKey = option.entity_type == 'civicrm_mailing' ? 'mailings' : 'groups'; |
| 289 | if (option.mode == 'exclude') { |
| 290 | ngModel.$viewValue[typeKey].exclude.push(option.entity_id); |
| 291 | arrayRemove(ngModel.$viewValue[typeKey].include, option.entity_id); |
| 292 | } |
| 293 | else { |
| 294 | ngModel.$viewValue[typeKey].include.push(option.entity_id); |
| 295 | arrayRemove(ngModel.$viewValue[typeKey].exclude, option.entity_id); |
| 296 | } |
| 297 | scope.$apply(); |
| 298 | $(element).select2('close'); |
| 299 | validate(); |
| 300 | e.preventDefault(); |
| 301 | }); |
| 302 | |
| 303 | $(element).on("select2-removing", function(e) { |
| 304 | var option = convertValueToObj(e.val); |
| 305 | var typeKey = option.entity_type == 'civicrm_mailing' ? 'mailings' : 'groups'; |
| 306 | if (typeKey == 'groups' && isMandatory(option.entity_id)) { |
| 307 | crmUiAlert({ |
| 308 | text: ts('This mailing was generated based on search results. The search results cannot be removed.'), |
| 309 | title: ts('Required') |
| 310 | }); |
| 311 | e.preventDefault(); |
| 312 | return; |
| 313 | } |
| 314 | scope.$parent.$apply(function() { |
| 315 | arrayRemove(ngModel.$viewValue[typeKey][option.mode], option.entity_id); |
| 316 | }); |
| 317 | validate(); |
| 318 | e.preventDefault(); |
| 319 | }); |
| 320 | |
| 321 | scope.$watchCollection("recips.groups.include", refreshUI); |
| 322 | scope.$watchCollection("recips.groups.exclude", refreshUI); |
| 323 | scope.$watchCollection("recips.mailings.include", refreshUI); |
| 324 | scope.$watchCollection("recips.mailings.exclude", refreshUI); |
| 325 | setTimeout(refreshUI, 50); |
| 326 | |
| 327 | scope.$watchCollection(attrs.crmAvailGroups, function() { |
| 328 | scope.groups = scope.$parent.$eval(attrs.crmAvailGroups); |
| 329 | }); |
| 330 | scope.$watchCollection(attrs.crmAvailMailings, function() { |
| 331 | scope.mailings = scope.$parent.$eval(attrs.crmAvailMailings); |
| 332 | }); |
| 333 | scope.$watchCollection(attrs.crmMandatoryGroups, function() { |
| 334 | refreshMandatory(); |
| 335 | }); |
| 336 | } |
| 337 | }; |
| 338 | }); |
| 339 | |
| 340 | })(angular, CRM.$, CRM._); |