crmMailing,crmMailingAB - Rename scope.help() to scope.hs()
[civicrm-core.git] / js / angular-crmMailing.js
1 (function (angular, $, _) {
2
3 angular.module('crmMailing', [
4 'crmUtil', 'crmAttachment', 'crmAutosave', 'ngRoute', 'ui.utils', 'crmUi', 'dialogService'
5 ]);
6
7 // Time to wait before triggering AJAX update to recipients list
8 var RECIPIENTS_DEBOUNCE_MS = 100;
9 var RECIPIENTS_PREVIEW_LIMIT = 10000;
10
11 var APPROVAL_STATUSES = {'Approved': 1, 'Rejected': 2, 'None': 3};
12
13 angular.module('crmMailing').config([
14 '$routeProvider',
15 function ($routeProvider) {
16 $routeProvider.when('/mailing', {
17 template: '<div></div>',
18 controller: 'ListMailingsCtrl'
19 });
20
21 var editorPaths = {
22 '': '~/crmMailing/edit.html',
23 '/unified': '~/crmMailing/edit-unified.html',
24 '/unified2': '~/crmMailing/edit-unified2.html',
25 '/wizard': '~/crmMailing/edit-wizard.html'
26 };
27 angular.forEach(editorPaths, function(editTemplate, pathSuffix) {
28 if (CRM && CRM.crmMailing && CRM.crmMailing.workflowEnabled) {
29 editTemplate = '~/crmMailing/edit-workflow.html'; // override
30 }
31 $routeProvider.when('/mailing/new' + pathSuffix, {
32 template: '<p>' + ts('Initializing...') + '</p>',
33 controller: 'CreateMailingCtrl',
34 resolve: {
35 selectedMail: function(crmMailingMgr) {
36 var m = crmMailingMgr.create();
37 return crmMailingMgr.save(m);
38 }
39 }
40 });
41 $routeProvider.when('/mailing/:id' + pathSuffix, {
42 templateUrl: editTemplate,
43 controller: 'EditMailingCtrl',
44 resolve: {
45 selectedMail: function($route, crmMailingMgr) {
46 return crmMailingMgr.get($route.current.params.id);
47 },
48 attachments: function($route, CrmAttachments) {
49 var attachments = new CrmAttachments(function () {
50 return {entity_table: 'civicrm_mailing', entity_id: $route.current.params.id};
51 });
52 return attachments.load();
53 }
54 }
55 });
56 });
57 }
58 ]);
59
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);
65 }]);
66
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('/'));
72 $location.replace();
73 });
74
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;
80
81 var ts = $scope.ts = CRM.ts(null);
82 var block = $scope.block = crmBlocker();
83 var myAutosave = null;
84
85 $scope.isSubmitted = function isSubmitted() {
86 return _.size($scope.mailing.jobs) > 0;
87 };
88
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));
93 };
94
95 // @return Promise
96 $scope.previewMailing = function previewMailing(mailing, mode) {
97 return crmMailingPreviewMgr.preview(mailing, mode);
98 };
99
100 // @return Promise
101 $scope.sendTest = function sendTest(mailing, attachments, recipient) {
102 var savePromise = crmMailingMgr.save(mailing)
103 .then(function () {
104 return attachments.save();
105 });
106 return block(crmStatus({start: ts('Saving...'), success: ''}, savePromise)
107 .then(function () {
108 crmMailingPreviewMgr.sendTest(mailing, recipient);
109 }));
110 };
111
112 // @return Promise
113 $scope.submit = function submit(options) {
114 options = options || {};
115 if (block.check() || $scope.crmMailing.$invalid) {
116 return;
117 }
118
119 var promise = crmMailingMgr.save($scope.mailing)
120 .then(function () {
121 // pre-condition: the mailing exists *before* saving attachments to it
122 return $scope.attachments.save();
123 })
124 .then(function () {
125 return crmMailingMgr.submit($scope.mailing);
126 })
127 .then(function () {
128 if (!options.stay) {
129 $scope.leave('scheduled');
130 }
131 })
132 ;
133 return block(crmStatus({start: ts('Submitting...'), success: ts('Submitted')}, promise));
134 };
135
136 // @return Promise
137 $scope.save = function save() {
138 return block(crmStatus(null,
139 crmMailingMgr
140 .save($scope.mailing)
141 .then(function () {
142 // pre-condition: the mailing exists *before* saving attachments to it
143 return $scope.attachments.save();
144 })
145 ));
146 };
147
148 // @return Promise
149 $scope.delete = function cancel() {
150 return block(crmStatus({start: ts('Deleting...'), success: ts('Deleted')},
151 crmMailingMgr.delete($scope.mailing)
152 .then(function () {
153 $scope.leave('unscheduled');
154 })
155 ));
156 };
157
158 // @param string listingScreen 'archive', 'scheduled', 'unscheduled'
159 $scope.leave = function leave(listingScreen) {
160 switch (listingScreen) {
161 case 'archive':
162 window.location = CRM.url('civicrm/mailing/browse/archived', {
163 reset: 1
164 });
165 break;
166 case 'scheduled':
167 window.location = CRM.url('civicrm/mailing/browse/scheduled', {
168 reset: 1,
169 scheduled: 'true'
170 });
171 break;
172 case 'unscheduled':
173 /* falls through */
174 default:
175 window.location = CRM.url('civicrm/mailing/browse/unscheduled', {
176 reset: 1,
177 scheduled: 'false'
178 });
179 }
180 };
181
182 myAutosave = new CrmAutosaveCtrl({
183 save: $scope.save,
184 saveIf: function() {
185 return true;
186 },
187 model: function() {
188 return [$scope.mailing, $scope.attachments.getAutosaveSignature()];
189 },
190 form: function() {
191 return $scope.crmMailing;
192 }
193 });
194 $timeout(myAutosave.start);
195 $scope.$on('$destroy', myAutosave.stop);
196 });
197
198 angular.module('crmMailing').controller('ViewRecipCtrl', function EditRecipCtrl($scope) {
199 $scope.getIncludesAsString = function(mailing) {
200 var first = true;
201 var names = '';
202 _.each(mailing.recipients.groups.include, function (id) {
203 if (!first) {
204 names = names + ', ';
205 }
206 var group = _.where(CRM.crmMailing.groupNames, {id: '' + id});
207 names = names + group[0].title;
208 first = false;
209 });
210 _.each(mailing.recipients.mailings.include, function (id) {
211 if (!first) {
212 names = names + ', ';
213 }
214 var oldMailing = _.where(CRM.crmMailing.civiMails, {id: '' + id});
215 names = names + oldMailing[0].name;
216 first = false;
217 });
218 return names;
219 };
220 $scope.getExcludesAsString = function (mailing) {
221 var first = true;
222 var names = '';
223 _.each(mailing.recipients.groups.exclude, function (id) {
224 if (!first) {
225 names = names + ', ';
226 }
227 var group = _.where(CRM.crmMailing.groupNames, {id: '' + id});
228 names = names + group[0].title;
229 first = false;
230 });
231 _.each(mailing.recipients.mailings.exclude, function (id) {
232 if (!first) {
233 names = names + ', ';
234 }
235 var oldMailing = _.where(CRM.crmMailing.civiMails, {id: '' + id});
236 names = names + oldMailing[0].name;
237 first = false;
238 });
239 return names;
240 };
241 });
242
243 // Controller for the edit-recipients fields (
244 // WISHLIST: Move most of this to a (cache-enabled) service
245 // Scope members:
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);
250
251 $scope.isMailingList = function isMailingList(group) {
252 var GROUP_TYPE_MAILING_LIST = '2';
253 return _.contains(group.group_type, GROUP_TYPE_MAILING_LIST);
254 };
255
256 $scope.recipients = null;
257 $scope.getRecipientsEstimate = function () {
258 var ts = $scope.ts;
259 if ($scope.recipients === null) {
260 return ts('(Estimating)');
261 }
262 if ($scope.recipients.length === 0) {
263 return ts('No recipients');
264 }
265 if ($scope.recipients.length === 1) {
266 return ts('~1 recipient');
267 }
268 if (RECIPIENTS_PREVIEW_LIMIT > 0 && $scope.recipients.length >= RECIPIENTS_PREVIEW_LIMIT) {
269 return ts('>%1 recipients', {1: RECIPIENTS_PREVIEW_LIMIT});
270 }
271 return ts('~%1 recipients', {1: $scope.recipients.length});
272 };
273
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;
282 });
283 });
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);
289
290 $scope.previewRecipients = function previewRecipients() {
291 var model = {
292 recipients: $scope.recipients
293 };
294 var options = CRM.utils.adjustDialogDefaults({
295 autoOpen: false,
296 title: ts('Preview (%1)', {
297 1: $scope.getRecipientsEstimate()
298 })
299 });
300 dialogService.open('recipDialog', '~/crmMailing/dialog/recipients.html', model, options);
301 };
302
303 // Open a dialog for editing the advanced recipient options.
304 $scope.editOptions = function editOptions(mailing) {
305 var options = CRM.utils.adjustDialogDefaults({
306 autoOpen: false,
307 title: ts('Edit Options')
308 });
309 $q.when(crmMetadata.getFields('Mailing')).then(function(fields) {
310 var model = {
311 fields: fields,
312 mailing: mailing
313 };
314 dialogService.open('previewComponentDialog', '~/crmMailing/dialog/recipientOptions.html', model, options);
315 });
316 };
317 });
318
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);
324 });
325
326 // Controller for the "Preview Mailing" dialog
327 // Note: Expects $scope.model to be an object with properties:
328 // - "subject"
329 // - "body_html"
330 // - "body_text"
331 angular.module('crmMailing').controller('PreviewMailingDialogCtrl', function PreviewMailingDialogCtrl($scope) {
332 $scope.ts = CRM.ts(null);
333 });
334
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'});
342 });
343
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);
348
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)', {
353 1: componentId
354 }));
355 return;
356 }
357 var options = CRM.utils.adjustDialogDefaults({
358 autoOpen: false,
359 title: title // component[0].name
360 });
361 dialogService.open('previewComponentDialog', '~/crmMailing/dialog/previewComponent.html', component[0], options);
362 };
363 });
364
365 // Controller for the "Preview Mailing Component" dialog
366 // Note: Expects $scope.model to be an object with properties:
367 // - "name"
368 // - "subject"
369 // - "body_html"
370 // - "body_text"
371 angular.module('crmMailing').controller('PreviewComponentDialogCtrl', function PreviewComponentDialogCtrl($scope) {
372 $scope.ts = CRM.ts(null);
373 });
374
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;
379
380 // @return Promise MessageTemplate (per APIv3)
381 $scope.saveTemplate = function saveTemplate(mailing) {
382 var model = {
383 selected_id: mailing.msg_template_id,
384 tpl: {
385 msg_title: '',
386 msg_subject: mailing.subject,
387 msg_text: mailing.body_text,
388 msg_html: mailing.body_html
389 }
390 };
391 var options = CRM.utils.adjustDialogDefaults({
392 autoOpen: false,
393 title: ts('Save Template')
394 });
395 return dialogService.open('saveTemplateDialog', '~/crmMailing/dialog/saveTemplate.html', model, options)
396 .then(function (item) {
397 mailing.msg_template_id = item.id;
398 return item;
399 });
400 };
401
402 // @param int id
403 // @return Promise
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;
409 });
410 };
411 });
412
413 // Controller for the "Save Message Template" dialog
414 // Scope members:
415 // - [input] "model": Object
416 // - "selected_id": int
417 // - "tpl": Object
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;
425
426 $scope.save = function save() {
427 var tpl = _.extend({}, $scope.model.tpl);
428 switch ($scope.saveOpt.mode) {
429 case 'add':
430 tpl.msg_title = $scope.saveOpt.newTitle;
431 break;
432 case 'update':
433 tpl.id = $scope.selected.id;
434 tpl.msg_title = $scope.selected.msg_title;
435 break;
436 default:
437 throw 'SaveMsgTemplateDialogCtrl: Unrecognized mode: ' + $scope.saveOpt.mode;
438 }
439 return crmMsgTemplates.save(tpl)
440 .then(function (item) {
441 CRM.status(ts('Saved'));
442 return item;
443 });
444 };
445
446 function scopeApply(f) {
447 return function () {
448 var args = arguments;
449 $scope.$apply(function () {
450 f.apply(args);
451 });
452 };
453 }
454
455 function init() {
456 crmMsgTemplates.get($scope.model.selected_id).then(
457 function (tpl) {
458 $scope.saveOpt.mode = 'update';
459 $scope.selected = tpl;
460 },
461 function () {
462 $scope.saveOpt.mode = 'add';
463 $scope.selected = null;
464 }
465 );
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).
469 var buttons = [
470 {
471 text: ts('Save'),
472 icons: {primary: 'ui-icon-check'},
473 click: function () {
474 $scope.save().then(function (item) {
475 dialogService.close('saveTemplateDialog', item);
476 });
477 }
478 },
479 {
480 text: ts('Cancel'),
481 icons: {primary: 'ui-icon-close'},
482 click: function () {
483 dialogService.cancel('saveTemplateDialog');
484 }
485 }
486 ];
487 dialogService.setButtons('saveTemplateDialog', buttons);
488 }
489
490 setTimeout(scopeApply(init), 0);
491 });
492
493 angular.module('crmMailing').controller('EmailAddrCtrl', function EmailAddrCtrl($scope, crmFromAddresses, crmUiAlert) {
494 var ts = CRM.ts(null);
495 function changeAlert(winnerField, loserField) {
496 crmUiAlert({
497 title: ts('Conflict'),
498 text: ts('The "%1" option conflicts with the "%2" option. The "%2" option has been disabled.', {
499 1: winnerField,
500 2: loserField
501 })
502 });
503 }
504
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'));
510 }
511 };
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'));
516 }
517 };
518 });
519
520 var lastEmailTokenAlert = null;
521 angular.module('crmMailing').controller('EmailBodyCtrl', function EmailBodyCtrl($scope, crmMailingMgr, crmUiAlert, $timeout) {
522 var ts = CRM.ts(null);
523
524 // ex: if (!hasAllTokens(myMailing, 'body_text)) alert('Oh noes!');
525 $scope.hasAllTokens = function hasAllTokens(mailing, field) {
526 return _.isEmpty(crmMailingMgr.findMissingTokens(mailing, field));
527 };
528
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();
534 }
535 var missing, insertable;
536 if (field == '*') {
537 insertable = false;
538 missing = angular.extend({},
539 crmMailingMgr.findMissingTokens(mailing, 'body_html'),
540 crmMailingMgr.findMissingTokens(mailing, 'body_text')
541 );
542 } else {
543 insertable = !_.isEmpty(insertEvent);
544 missing = crmMailingMgr.findMissingTokens(mailing, field);
545 }
546 if (!_.isEmpty(missing)) {
547 lastEmailTokenAlert = crmUiAlert({
548 type: 'error',
549 title: ts('Required tokens'),
550 templateUrl: '~/crmMailing/dialog/tokenAlert.html',
551 scope: angular.extend($scope.$new(), {
552 insertable: insertable,
553 insertToken: function(token) {
554 $timeout(function(){
555 $scope.$broadcast(insertEvent, '{' + token + '}');
556 $timeout(function(){
557 checkTokens(mailing, field, insertEvent);
558 });
559 });
560 },
561 missing: missing
562 })
563 });
564 }
565 };
566 });
567
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));
574 }
575 });
576
577 $scope.isUnsubGroupRequired = function isUnsubGroupRequired(mailing) {
578 return _.intersection(mandatoryIds, mailing.recipients.groups.include).length > 0;
579 };
580 });
581 })(angular, CRM.$, CRM._);