1 (function (angular
, $, _
) {
3 angular
.module('crmMailing', [
4 'crmUtil', 'crmAttachment', 'crmAutosave', 'ngRoute', 'ui.utils', 'crmUi', 'dialogService'
7 // Time to wait before triggering AJAX update to recipients list
8 var RECIPIENTS_DEBOUNCE_MS
= 100;
9 var RECIPIENTS_PREVIEW_LIMIT
= 10000;
11 var APPROVAL_STATUSES
= {'Approved': 1, 'Rejected': 2, 'None': 3};
13 angular
.module('crmMailing').config([
15 function ($routeProvider
) {
16 $routeProvider
.when('/mailing', {
17 template
: '<div></div>',
18 controller
: 'ListMailingsCtrl'
22 '': '~/crmMailing/edit.html',
23 '/unified': '~/crmMailing/edit-unified.html',
24 '/unified2': '~/crmMailing/edit-unified2.html',
25 '/wizard': '~/crmMailing/edit-wizard.html'
27 angular
.forEach(editorPaths
, function(editTemplate
, pathSuffix
) {
28 if (CRM
&& CRM
.crmMailing
&& CRM
.crmMailing
.workflowEnabled
) {
29 editTemplate
= '~/crmMailing/edit-workflow.html'; // override
31 $routeProvider
.when('/mailing/new' + pathSuffix
, {
32 template
: '<p>' + ts('Initializing...') + '</p>',
33 controller
: 'CreateMailingCtrl',
35 selectedMail: function(crmMailingMgr
) {
36 var m
= crmMailingMgr
.create();
37 return crmMailingMgr
.save(m
);
41 $routeProvider
.when('/mailing/:id' + pathSuffix
, {
42 templateUrl
: editTemplate
,
43 controller
: 'EditMailingCtrl',
45 selectedMail: function($route
, crmMailingMgr
) {
46 return crmMailingMgr
.get($route
.current
.params
.id
);
48 attachments: function($route
, CrmAttachments
) {
49 var attachments
= new CrmAttachments(function () {
50 return {entity_table
: 'civicrm_mailing', entity_id
: $route
.current
.params
.id
};
52 return attachments
.load();
60 angular
.module('crmMailing').controller('ListMailingsCtrl', ['crmLegacy', 'crmNavigator', function ListMailingsCtrl(crmLegacy
, crmNavigator
) {
61 // We haven't implemented this in Angular, but some users may get clever
62 // about typing URLs, so we'll provide a redirect.
63 var new_url
= crmLegacy
.url('civicrm/mailing/browse/unscheduled', {reset
: 1, scheduled
: 'false'});
64 crmNavigator
.redirect(new_url
);
67 angular
.module('crmMailing').controller('CreateMailingCtrl', function EditMailingCtrl($scope
, selectedMail
, $location
) {
68 // Transition URL "/mailing/new/foo" => "/mailing/123/foo"
69 var parts
= $location
.path().split('/'); // e.g. "/mailing/new" or "/mailing/123/wizard"
70 parts
[2] = selectedMail
.id
;
71 $location
.path(parts
.join('/'));
75 angular
.module('crmMailing').controller('EditMailingCtrl', function EditMailingCtrl($scope
, selectedMail
, $location
, crmMailingMgr
, crmStatus
, attachments
, crmMailingPreviewMgr
, crmBlocker
, CrmAutosaveCtrl
, $timeout
) {
76 $scope
.mailing
= selectedMail
;
77 $scope
.attachments
= attachments
;
78 $scope
.crmMailingConst
= CRM
.crmMailing
;
79 $scope
.checkPerm
= CRM
.checkPerm
;
81 var ts
= $scope
.ts
= CRM
.ts(null);
82 var block
= $scope
.block
= crmBlocker();
83 var myAutosave
= null;
85 $scope
.isSubmitted
= function isSubmitted() {
86 return _
.size($scope
.mailing
.jobs
) > 0;
89 // usage: approve('Approved')
90 $scope
.approve
= function approve(status
, options
) {
91 $scope
.mailing
.approval_status_id
= APPROVAL_STATUSES
[status
];
92 return myAutosave
.suspend($scope
.submit(options
));
96 $scope
.previewMailing
= function previewMailing(mailing
, mode
) {
97 return crmMailingPreviewMgr
.preview(mailing
, mode
);
101 $scope
.sendTest
= function sendTest(mailing
, attachments
, recipient
) {
102 var savePromise
= crmMailingMgr
.save(mailing
)
104 return attachments
.save();
106 return block(crmStatus({start
: ts('Saving...'), success
: ''}, savePromise
)
108 crmMailingPreviewMgr
.sendTest(mailing
, recipient
);
113 $scope
.submit
= function submit(options
) {
114 options
= options
|| {};
115 if (block
.check() || $scope
.crmMailing
.$invalid
) {
119 var promise
= crmMailingMgr
.save($scope
.mailing
)
121 // pre-condition: the mailing exists *before* saving attachments to it
122 return $scope
.attachments
.save();
125 return crmMailingMgr
.submit($scope
.mailing
);
129 $scope
.leave('scheduled');
133 return block(crmStatus({start
: ts('Submitting...'), success
: ts('Submitted')}, promise
));
137 $scope
.save
= function save() {
138 return block(crmStatus(null,
140 .save($scope
.mailing
)
142 // pre-condition: the mailing exists *before* saving attachments to it
143 return $scope
.attachments
.save();
149 $scope
.delete = function cancel() {
150 return block(crmStatus({start
: ts('Deleting...'), success
: ts('Deleted')},
151 crmMailingMgr
.delete($scope
.mailing
)
153 $scope
.leave('unscheduled');
158 // @param string listingScreen 'archive', 'scheduled', 'unscheduled'
159 $scope
.leave
= function leave(listingScreen
) {
160 switch (listingScreen
) {
162 window
.location
= CRM
.url('civicrm/mailing/browse/archived', {
167 window
.location
= CRM
.url('civicrm/mailing/browse/scheduled', {
175 window
.location
= CRM
.url('civicrm/mailing/browse/unscheduled', {
182 myAutosave
= new CrmAutosaveCtrl({
188 return [$scope
.mailing
, $scope
.attachments
.getAutosaveSignature()];
191 return $scope
.crmMailing
;
194 $timeout(myAutosave
.start
);
195 $scope
.$on('$destroy', myAutosave
.stop
);
198 angular
.module('crmMailing').controller('ViewRecipCtrl', function EditRecipCtrl($scope
) {
199 $scope
.getIncludesAsString = function(mailing
) {
202 _
.each(mailing
.recipients
.groups
.include
, function (id
) {
204 names
= names
+ ', ';
206 var group
= _
.where(CRM
.crmMailing
.groupNames
, {id
: '' + id
});
207 names
= names
+ group
[0].title
;
210 _
.each(mailing
.recipients
.mailings
.include
, function (id
) {
212 names
= names
+ ', ';
214 var oldMailing
= _
.where(CRM
.crmMailing
.civiMails
, {id
: '' + id
});
215 names
= names
+ oldMailing
[0].name
;
220 $scope
.getExcludesAsString = function (mailing
) {
223 _
.each(mailing
.recipients
.groups
.exclude
, function (id
) {
225 names
= names
+ ', ';
227 var group
= _
.where(CRM
.crmMailing
.groupNames
, {id
: '' + id
});
228 names
= names
+ group
[0].title
;
231 _
.each(mailing
.recipients
.mailings
.exclude
, function (id
) {
233 names
= names
+ ', ';
235 var oldMailing
= _
.where(CRM
.crmMailing
.civiMails
, {id
: '' + id
});
236 names
= names
+ oldMailing
[0].name
;
243 // Controller for the edit-recipients fields (
244 // WISHLIST: Move most of this to a (cache-enabled) service
246 // - [input] mailing: object
247 // - [output] recipients: array of recipient records
248 angular
.module('crmMailing').controller('EditRecipCtrl', function EditRecipCtrl($scope
, dialogService
, crmApi
, crmMailingMgr
, $q
, crmMetadata
) {
249 var ts
= $scope
.ts
= CRM
.ts(null);
251 $scope
.isMailingList
= function isMailingList(group
) {
252 var GROUP_TYPE_MAILING_LIST
= '2';
253 return _
.contains(group
.group_type
, GROUP_TYPE_MAILING_LIST
);
256 $scope
.recipients
= null;
257 $scope
.getRecipientsEstimate = function () {
259 if ($scope
.recipients
=== null) {
260 return ts('(Estimating)');
262 if ($scope
.recipients
.length
=== 0) {
263 return ts('No recipients');
265 if ($scope
.recipients
.length
=== 1) {
266 return ts('~1 recipient');
268 if (RECIPIENTS_PREVIEW_LIMIT
> 0 && $scope
.recipients
.length
>= RECIPIENTS_PREVIEW_LIMIT
) {
269 return ts('>%1 recipients', {1: RECIPIENTS_PREVIEW_LIMIT
});
271 return ts('~%1 recipients', {1: $scope
.recipients
.length
});
274 // We monitor four fields -- use debounce so that changes across the
275 // four fields can settle-down before AJAX.
276 var refreshRecipients
= _
.debounce(function () {
277 $scope
.$apply(function () {
278 $scope
.recipients
= null;
279 if (!$scope
.mailing
) return;
280 crmMailingMgr
.previewRecipients($scope
.mailing
, RECIPIENTS_PREVIEW_LIMIT
).then(function (recipients
) {
281 $scope
.recipients
= recipients
;
284 }, RECIPIENTS_DEBOUNCE_MS
);
285 $scope
.$watchCollection("mailing.recipients.groups.include", refreshRecipients
);
286 $scope
.$watchCollection("mailing.recipients.groups.exclude", refreshRecipients
);
287 $scope
.$watchCollection("mailing.recipients.mailings.include", refreshRecipients
);
288 $scope
.$watchCollection("mailing.recipients.mailings.exclude", refreshRecipients
);
290 $scope
.previewRecipients
= function previewRecipients() {
292 recipients
: $scope
.recipients
294 var options
= CRM
.utils
.adjustDialogDefaults({
296 title
: ts('Preview (%1)', {
297 1: $scope
.getRecipientsEstimate()
300 dialogService
.open('recipDialog', '~/crmMailing/dialog/recipients.html', model
, options
);
303 // Open a dialog for editing the advanced recipient options.
304 $scope
.editOptions
= function editOptions(mailing
) {
305 var options
= CRM
.utils
.adjustDialogDefaults({
307 title
: ts('Edit Options')
309 $q
.when(crmMetadata
.getFields('Mailing')).then(function(fields
) {
314 dialogService
.open('previewComponentDialog', '~/crmMailing/dialog/recipientOptions.html', model
, options
);
319 // Controller for the "Preview Recipients" dialog
320 // Note: Expects $scope.model to be an object with properties:
321 // - recipients: array of contacts
322 angular
.module('crmMailing').controller('PreviewRecipCtrl', function ($scope
) {
323 $scope
.ts
= CRM
.ts(null);
326 // Controller for the "Preview Mailing" dialog
327 // Note: Expects $scope.model to be an object with properties:
331 angular
.module('crmMailing').controller('PreviewMailingDialogCtrl', function PreviewMailingDialogCtrl($scope
) {
332 $scope
.ts
= CRM
.ts(null);
335 // Controller for the "Recipients: Edit Options" dialog
336 // Note: Expects $scope.model to be an object with properties:
337 // - "mailing" (APIv3 mailing object)
338 // - "fields" (list of fields)
339 angular
.module('crmMailing').controller('EditRecipOptionsDialogCtrl', function EditRecipOptionsDialogCtrl($scope
, crmUiHelp
) {
340 $scope
.ts
= CRM
.ts(null);
341 $scope
.hs
= crmUiHelp({file
: 'CRM/Mailing/MailingUI'});
344 // Controller for the "Preview Mailing Component" segment
345 // which displays header/footer/auto-responder
346 angular
.module('crmMailing').controller('PreviewComponentCtrl', function PreviewComponentCtrl($scope
, dialogService
) {
347 var ts
= $scope
.ts
= CRM
.ts(null);
349 $scope
.previewComponent
= function previewComponent(title
, componentId
) {
350 var component
= _
.where(CRM
.crmMailing
.headerfooterList
, {id
: "" + componentId
});
351 if (!component
|| !component
[0]) {
352 CRM
.alert(ts('Invalid component ID (%1)', {
357 var options
= CRM
.utils
.adjustDialogDefaults({
359 title
: title
// component[0].name
361 dialogService
.open('previewComponentDialog', '~/crmMailing/dialog/previewComponent.html', component
[0], options
);
365 // Controller for the "Preview Mailing Component" dialog
366 // Note: Expects $scope.model to be an object with properties:
371 angular
.module('crmMailing').controller('PreviewComponentDialogCtrl', function PreviewComponentDialogCtrl($scope
) {
372 $scope
.ts
= CRM
.ts(null);
375 // Controller for the in-place msg-template management
376 angular
.module('crmMailing').controller('MsgTemplateCtrl', function MsgTemplateCtrl($scope
, crmMsgTemplates
, dialogService
) {
377 var ts
= $scope
.ts
= CRM
.ts(null);
378 $scope
.crmMsgTemplates
= crmMsgTemplates
;
380 // @return Promise MessageTemplate (per APIv3)
381 $scope
.saveTemplate
= function saveTemplate(mailing
) {
383 selected_id
: mailing
.msg_template_id
,
386 msg_subject
: mailing
.subject
,
387 msg_text
: mailing
.body_text
,
388 msg_html
: mailing
.body_html
391 var options
= CRM
.utils
.adjustDialogDefaults({
393 title
: ts('Save Template')
395 return dialogService
.open('saveTemplateDialog', '~/crmMailing/dialog/saveTemplate.html', model
, options
)
396 .then(function (item
) {
397 mailing
.msg_template_id
= item
.id
;
404 $scope
.loadTemplate
= function loadTemplate(mailing
, id
) {
405 return crmMsgTemplates
.get(id
).then(function (tpl
) {
406 mailing
.subject
= tpl
.msg_subject
;
407 mailing
.body_text
= tpl
.msg_text
;
408 mailing
.body_html
= tpl
.msg_html
;
413 // Controller for the "Save Message Template" dialog
415 // - [input] "model": Object
416 // - "selected_id": int
418 // - "msg_subject": string
419 // - "msg_text": string
420 // - "msg_html": string
421 angular
.module('crmMailing').controller('SaveMsgTemplateDialogCtrl', function SaveMsgTemplateDialogCtrl($scope
, crmMsgTemplates
, dialogService
) {
422 var ts
= $scope
.ts
= CRM
.ts(null);
423 $scope
.saveOpt
= {mode
: '', newTitle
: ''};
424 $scope
.selected
= null;
426 $scope
.save
= function save() {
427 var tpl
= _
.extend({}, $scope
.model
.tpl
);
428 switch ($scope
.saveOpt
.mode
) {
430 tpl
.msg_title
= $scope
.saveOpt
.newTitle
;
433 tpl
.id
= $scope
.selected
.id
;
434 tpl
.msg_title
= $scope
.selected
.msg_title
;
437 throw 'SaveMsgTemplateDialogCtrl: Unrecognized mode: ' + $scope
.saveOpt
.mode
;
439 return crmMsgTemplates
.save(tpl
)
440 .then(function (item
) {
441 CRM
.status(ts('Saved'));
446 function scopeApply(f
) {
448 var args
= arguments
;
449 $scope
.$apply(function () {
456 crmMsgTemplates
.get($scope
.model
.selected_id
).then(
458 $scope
.saveOpt
.mode
= 'update';
459 $scope
.selected
= tpl
;
462 $scope
.saveOpt
.mode
= 'add';
463 $scope
.selected
= null;
466 // When using dialogService with a button bar, the major button actions
467 // need to be registered with the dialog widget (and not embedded in
468 // the body of the dialog).
472 icons
: {primary
: 'ui-icon-check'},
474 $scope
.save().then(function (item
) {
475 dialogService
.close('saveTemplateDialog', item
);
481 icons
: {primary
: 'ui-icon-close'},
483 dialogService
.cancel('saveTemplateDialog');
487 dialogService
.setButtons('saveTemplateDialog', buttons
);
490 setTimeout(scopeApply(init
), 0);
493 angular
.module('crmMailing').controller('EmailAddrCtrl', function EmailAddrCtrl($scope
, crmFromAddresses
, crmUiAlert
) {
494 var ts
= CRM
.ts(null);
495 function changeAlert(winnerField
, loserField
) {
497 title
: ts('Conflict'),
498 text
: ts('The "%1" option conflicts with the "%2" option. The "%2" option has been disabled.', {
505 $scope
.crmFromAddresses
= crmFromAddresses
;
506 $scope
.checkReplyToChange
= function checkReplyToChange(mailing
) {
507 if (!_
.isEmpty(mailing
.replyto_email
) && mailing
.override_verp
== '0') {
508 mailing
.override_verp
= '1';
509 changeAlert(ts('Reply-To'), ts('Track Replies'));
512 $scope
.checkVerpChange
= function checkVerpChange(mailing
) {
513 if (!_
.isEmpty(mailing
.replyto_email
) && mailing
.override_verp
== '0') {
514 mailing
.replyto_email
= '';
515 changeAlert(ts('Track Replies'), ts('Reply-To'));
520 var lastEmailTokenAlert
= null;
521 angular
.module('crmMailing').controller('EmailBodyCtrl', function EmailBodyCtrl($scope
, crmMailingMgr
, crmUiAlert
, $timeout
) {
522 var ts
= CRM
.ts(null);
524 // ex: if (!hasAllTokens(myMailing, 'body_text)) alert('Oh noes!');
525 $scope
.hasAllTokens
= function hasAllTokens(mailing
, field
) {
526 return _
.isEmpty(crmMailingMgr
.findMissingTokens(mailing
, field
));
529 // ex: checkTokens(myMailing, 'body_text', 'insert:body_text')
530 // ex: checkTokens(myMailing, '*')
531 $scope
.checkTokens
= function checkTokens(mailing
, field
, insertEvent
) {
532 if (lastEmailTokenAlert
) {
533 lastEmailTokenAlert
.close();
535 var missing
, insertable
;
538 missing
= angular
.extend({},
539 crmMailingMgr
.findMissingTokens(mailing
, 'body_html'),
540 crmMailingMgr
.findMissingTokens(mailing
, 'body_text')
543 insertable
= !_
.isEmpty(insertEvent
);
544 missing
= crmMailingMgr
.findMissingTokens(mailing
, field
);
546 if (!_
.isEmpty(missing
)) {
547 lastEmailTokenAlert
= crmUiAlert({
549 title
: ts('Required tokens'),
550 templateUrl
: '~/crmMailing/dialog/tokenAlert.html',
551 scope
: angular
.extend($scope
.$new(), {
552 insertable
: insertable
,
553 insertToken: function(token
) {
555 $scope
.$broadcast(insertEvent
, '{' + token
+ '}');
557 checkTokens(mailing
, field
, insertEvent
);
568 angular
.module('crmMailing').controller('EditUnsubGroupCtrl', function EditUnsubGroupCtrl($scope
) {
569 // CRM.crmMailing.groupNames is a global constant - since it doesn't change, we can digest & cache.
570 var mandatoryIds
= [];
571 _
.each(CRM
.crmMailing
.groupNames
, function(grp
){
572 if (grp
.is_hidden
== "1") {
573 mandatoryIds
.push(parseInt(grp
.id
));
577 $scope
.isUnsubGroupRequired
= function isUnsubGroupRequired(mailing
) {
578 return _
.intersection(mandatoryIds
, mailing
.recipients
.groups
.include
).length
> 0;
581 })(angular
, CRM
.$, CRM
._
);