When loading or submitting, determine the status of any jobs tied to this mailing.
When attempting to edit a submitted mailing, display a message instead of a form.
Change the technique for updating URL -- instead of watching 'mailing.id',
explicitly call updateUrl after saving. This ensures a consistent, linear
sequence of operations. (Note: The watch technique was useful when there
were different variations of save() in EditMailingCtrl and
PreviewMailingCtrl, but we eliminated PreviewMailingCtrl, so it's much
easier to manage the dataflows in a centralized fashion.)
$scope.partialUrl = partialUrl;
var ts = $scope.ts = CRM.ts('CiviMail');
+ $scope.isSubmitted = function isSubmitted() {
+ return _.size($scope.mailing.jobs) > 0;
+ };
+
// @return Promise
$scope.previewMailing = function previewMailing(mailing, mode) {
return crmMailingPreviewMgr.preview(mailing, mode);
var savePromise = crmMailingMgr.save(mailing)
.then(function () {
return attachments.save();
- });
+ })
+ .then(updateUrl);
return crmStatus({start: ts('Saving...'), success: ''}, savePromise)
.then(function () {
crmMailingPreviewMgr.sendTest(mailing, recipient);
// @return Promise
$scope.submit = function submit() {
var promise = crmMailingMgr.save($scope.mailing)
- .then(function () {
- // pre-condition: the mailing exists *before* saving attachments to it
- return $scope.attachments.save();
- })
- .then(function () {
- return crmMailingMgr.submit($scope.mailing);
- });
+ .then(function () {
+ // pre-condition: the mailing exists *before* saving attachments to it
+ return $scope.attachments.save();
+ })
+ .then(function () {
+ return crmMailingMgr.submit($scope.mailing);
+ })
+ .then(function () {
+ updateUrl();
+ return $scope.mailing;
+ })
+ ;
return crmStatus({start: ts('Submitting...'), success: ts('Submitted')}, promise);
};
// pre-condition: the mailing exists *before* saving attachments to it
return $scope.attachments.save();
})
+ .then(function () {
+ updateUrl();
+ return $scope.mailing;
+ })
);
};
});
};
- // Transition URL "/mailing/new" => "/mailing/123" as soon as ID is known
- $scope.$watch('mailing.id', function (newValue, oldValue) {
- if (newValue && newValue != oldValue) {
- var parts = $location.path().split('/'); // e.g. "/mailing/new" or "/mailing/123/wizard"
- parts[2] = newValue;
+ // Transition URL "/mailing/new" => "/mailing/123"
+ function updateUrl() {
+ var parts = $location.path().split('/'); // e.g. "/mailing/new" or "/mailing/123/wizard"
+ if (parts[2] != $scope.mailing.id) {
+ parts[2] = $scope.mailing.id;
$location.path(parts.join('/'));
$location.replace();
// FIXME: Angular unnecessarily refreshes UI
+ // WARNING: Changing the URL triggers a full reload. Any pending AJAX operations
+ // could be inconsistently applied. Run updateUrl() after other changes complete.
}
- });
+ }
});
// Controller for the edit-recipients fields (
var emailRegex = /^"(.*)" \<([^@\>]*@[^@\>]*)\>$/;
var addrs = _.map(CRM.crmMailing.fromAddress, function (addr) {
var match = emailRegex.exec(addr.label);
- return _.extend({}, addr, {
+ return angular.extend({}, addr, {
email: match ? match[2] : '(INVALID)',
author: match ? match[1] : '(INVALID)'
});
angular.module('crmMailing').factory('crmMsgTemplates', function ($q, crmApi) {
var tpls = _.map(CRM.crmMailing.mesTemplate, function (tpl) {
- return _.extend({}, tpl, {
+ return angular.extend({}, tpl, {
//id: tpl parseInt(tpl.id)
});
});
},
// @return Promise Mailing (per APIv3)
get: function get(id) {
- return crmApi('Mailing', 'getsingle', {id: id}).then(function (mailing) {
- return crmApi('MailingGroup', 'get', {mailing_id: id}).then(function (groupResult) {
+ var crmMailingMgr = this;
+ var mailing;
+ return crmApi('Mailing', 'getsingle', {id: id})
+ .then(function (getResult) {
+ mailing = getResult;
+ return $q.all([
+ crmMailingMgr._loadGroups(mailing),
+ crmMailingMgr._loadJobs(mailing)
+ ]);
+ })
+ .then(function () {
+ return mailing;
+ });
+ },
+ // Call MailingGroup.get and merge results into "mailing"
+ _loadGroups: function (mailing) {
+ return crmApi('MailingGroup', 'get', {mailing_id: mailing.id})
+ .then(function (groupResult) {
mailing.groups = {include: [], exclude: []};
mailing.mailings = {include: [], exclude: []};
_.each(groupResult.values, function (mailingGroup) {
var entityId = parseInt(mailingGroup.entity_id);
mailing[bucket][mailingGroup.group_type].push(entityId);
});
- return mailing;
});
- });
+ },
+ // Call MailingJob.get and merge results into "mailing"
+ _loadJobs: function (mailing) {
+ return crmApi('MailingJob', 'get', {mailing_id: mailing.id, is_test: 0})
+ .then(function (jobResult) {
+ mailing.jobs = mailing.jobs || {};
+ angular.extend(mailing.jobs, jobResult.values);
+ });
},
// @return Object Mailing (per APIv3)
create: function create() {
return {
+ jobs: {}, // {jobId: JobRecord}
name: "revert this", // fixme
campaign_id: null,
from_name: crmFromAddresses.getDefault().author,
// @param mailing Object (per APIv3)
// @return Promise an object with "subject", "body_text", "body_html"
preview: function preview(mailing) {
- var params = _.extend({}, mailing, {
+ var params = angular.extend({}, mailing, {
options: {force_rollback: 1},
'api.Mailing.preview': {
id: '$value.id'
previewRecipients: function previewRecipients(mailing, previewLimit) {
// To get list of recipients, we tentatively save the mailing and
// get the resulting recipients -- then rollback any changes.
- var params = _.extend({}, mailing, {
+ var params = angular.extend({}, mailing, {
options: {force_rollback: 1},
'api.mailing_job.create': 1, // note: exact match to API default
'api.MailingRecipients.get': {
// @param mailing Object (per APIv3)
// @return Promise
save: function (mailing) {
- var params = _.extend({}, mailing, {
+ var params = angular.extend({}, mailing, {
'api.mailing_job.create': 0 // note: exact match to API default
});
// is therefore not allowed. Remove this after fixing Mailing.create's contract.
delete params.scheduled_date;
+ delete params.jobs;
+
return crmApi('Mailing', 'create', params).then(function (result) {
if (result.id && !mailing.id) {
mailing.id = result.id;
} // no rollback, so update mailing.id
// Perhaps we should reload mailing based on result?
- return result.values[result.id];
+ return mailing;
});
},
// @param mailing Object (per APIv3)
// @return Promise
submit: function (mailing) {
+ var crmMailingMgr = this;
var params = {
id: mailing.id,
approval_date: createNow(),
scheduled_date: mailing.scheduled_date ? mailing.scheduled_date : createNow()
};
- return crmApi('Mailing', 'submit', params).then(function (result) {
- _.extend(mailing, result.values[result.id]); // Perhaps we should reload mailing based on result?
- return mailing;
- });
+ return crmApi('Mailing', 'submit', params)
+ .then(function (result) {
+ angular.extend(mailing, result.values[result.id]); // Perhaps we should reload mailing based on result?
+ return crmMailingMgr._loadJobs(mailing);
+ })
+ .then(function () {
+ return mailing;
+ });
},
// Immediately send a test message
// @param to Object with either key "email" (string) or "gid" (int)
// @return Promise for a list of delivery reports
sendTest: function (mailing, recipient) {
- var params = _.extend({}, mailing, {
+ var params = angular.extend({}, mailing, {
// options: {force_rollback: 1}, // Test mailings include tracking features, so the mailing must be persistent
'api.Mailing.send_test': {
mailing_id: '$value.id',
// is therefore not allowed. Remove this after fixing Mailing.create's contract.
delete params.scheduled_date;
+ delete params.jobs;
+
return crmApi('Mailing', 'create', params).then(function (result) {
if (result.id && !mailing.id) {
mailing.id = result.id;
$scope.testing_criteria = crmMailingABCriteria.getAll();
});
- angular.module('crmMailingAB').controller('CrmMailingABEditCtrl', function ($scope, abtest, crmMailingABCriteria, crmMailingMgr, crmMailingPreviewMgr, crmStatus) {
- window.ab = abtest;
+ angular.module('crmMailingAB').controller('CrmMailingABEditCtrl', function ($scope, abtest, crmMailingABCriteria, crmMailingMgr, crmMailingPreviewMgr, crmStatus, $q, $location) {
$scope.abtest = abtest;
var ts = $scope.ts = CRM.ts('CiviMail');
$scope.crmMailingABCriteria = crmMailingABCriteria;
$scope.crmMailingConst = CRM.crmMailing;
$scope.partialUrl = partialUrl;
+ $scope.isSubmitted = function isSubmitted() {
+ return _.size(abtest.mailings.a.jobs) > 0 || _.size(abtest.mailings.b.jobs) > 0;
+ };
$scope.sync = function sync() {
abtest.mailings.a.name = ts('Test A (%1)', {1: abtest.ab.name});
abtest.mailings.b.name = ts('Test B (%1)', {1: abtest.ab.name});
}
crmMailingMgr.mergeInto(abtest.mailings.c, abtest.mailings.a, ['name']);
};
+
+ // @return Promise
$scope.save = function save() {
$scope.sync();
- return crmStatus({start: ts('Saving...'), success: ts('Saved')}, abtest.save());
+ return crmStatus({start: ts('Saving...'), success: ts('Saved')}, abtest.save().then(updateUrl));
};
+
// @return Promise
$scope.previewMailing = function previewMailing(mailingName, mode) {
return crmMailingPreviewMgr.preview(abtest.mailings[mailingName], mode);
// @return Promise
$scope.sendTest = function sendTest(mailingName, recipient) {
- return crmStatus({start: ts('Saving...'), success: ''}, abtest.save())
+ return crmStatus({start: ts('Saving...'), success: ''}, abtest.save().then(updateUrl))
.then(function () {
crmMailingPreviewMgr.sendTest(abtest.mailings[mailingName], recipient);
});
};
+
+ // @return Promise
$scope.delete = function () {
- throw "Not implemented: EditCtrl.delete"
+ return crmStatus({start: ts('Deleting...'), success: ts('Deleted')}, abtest.delete());
};
- $scope.submit = function () {
- throw "Not implemented: EditCtrl.submit"
+
+ // @return Promise
+ $scope.submit = function submit() {
+ return crmStatus({start: ts('Saving...'), success: ''}, abtest.save().then(updateUrl))
+ .then(function () {
+ return crmStatus({start: ts('Submitting...'), success: ts('Submitted')}, $q.all([
+ crmMailingMgr.submit(abtest.mailings.a),
+ crmMailingMgr.submit(abtest.mailings.b)
+ ]));
+ });
};
function updateCriteriaName() {
$scope.criteriaName = criteria ? criteria.name : null;
}
+ // Transition URL "/abtest/new" => "/abtest/123"
+ function updateUrl() {
+ var parts = $location.path().split('/'); // e.g. "/abtest/new" or "/abtest/123/wizard"
+ if (parts[2] != $scope.abtest.ab.id) {
+ parts[2] = $scope.abtest.ab.id;
+ $location.path(parts.join('/'));
+ $location.replace();
+ // FIXME: Angular unnecessarily refreshes UI
+ // WARNING: Changing the URL triggers a full reload. Any pending AJAX operations
+ // could be inconsistently applied. Run updateUrl() after other changes complete.
+ }
+ }
+
// initialize
updateCriteriaName();
$scope.sync();
else {
return crmApi('MailingAB', 'get', {id: crmMailingAB.id})
.then(function (abResult) {
+ if (abResult.count != 1) {
+ throw "Failed to load AB Test";
+ }
crmMailingAB.ab = abResult.values[abResult.id];
return crmMailingAB._loadMailings();
});
return crmMailingAB;
});
},
+ // @param mailing Object (per APIv3)
+ // @return Promise
+ 'delete': function () {
+ if (this.id) {
+ return crmApi('MailingAB', 'delete', {id: this.id});
+ }
+ else {
+ var d = $q.defer();
+ d.resolve();
+ return d.promise;
+ }
+ },
// Load mailings A, B, and C (if available)
// @return Promise CrmMailingAB
_loadMailings: function _loadMailings() {
<pre>{{mailing|json}}</pre>
</div>
-<form name="crmMailing">
+<div ng-show="isSubmitted()">
+ {{ts('This mailing has been submitted.')}}
+</div>
+
+<form name="crmMailing" ng-hide="isSubmitted()">
<div class="crm-block crm-form-block crmMailing">
<div crm-mailing-block-summary crm-mailing="mailing"/>
<pre>{{mailing|json}}</pre>
</div>
-<form name="crmMailing">
+<div ng-show="isSubmitted()">
+ {{ts('This mailing has been submitted.')}}
+</div>
+
+<form name="crmMailing" ng-hide="isSubmitted()">
<div class="crm-block crm-form-block crmMailing">
<div crm-mailing-block-summary crm-mailing="mailing"/>
<div crm-mailing-block-schedule crm-mailing="mailing"/>
</div>
- <button ng-click="submit().then(leave)">{{ts('Submit Mailing')}}</button>
+ <button ng-click="submit()">{{ts('Submit Mailing')}}</button>
<button ng-click="save()">{{ts('Save Draft')}}</button>
<button crm-confirm="{title:ts('Delete Draft?'), message:ts('Are you sure you want to delete the draft mailing?')}" on-yes="delete().then(leave)">{{ts('Delete Draft')}}</button>
</div>
<pre>{{mailing|json}}</pre>
</div>
-<form name="crmMailing">
+<div ng-show="isSubmitted()">
+ {{ts('This mailing has been submitted.')}}
+</div>
+
+<form name="crmMailing" ng-hide="isSubmitted()">
<div class="crm-block crm-form-block crmMailing">
<div crm-ui-wizard>
<pre>{{mailing|json}}</pre>
</div>
-<form name="crmMailing" novalidate>
+<div ng-show="isSubmitted()">
+ {{ts('This mailing has been submitted.')}}
+</div>
+
+<form name="crmMailing" novalidate ng-hide="isSubmitted()">
<div class="crm-block crm-form-block crmMailing">
<div crm-ui-wizard>
<div crm-ui-wizard-step crm-title="ts('Define Mailing')">
<div crm-mailing-block-schedule crm-mailing="mailing"/>
</div>
<center>
- <a class="crmMailing-submit-button" ng-click="submit().then(leave)">
+ <a class="crmMailing-submit-button" ng-click="submit()">
<div>{{ts('Submit Mailing')}}</div>
</a>
</center>
individual field from B). At the end of the composition process, the controller's "sync" operation will
merge shared settings from "A" into "B".
-->
+<div ng-show="isSubmitted()">
+ {{ts('This mailing has been submitted.')}}
+</div>
-<form name="crmMailingAB" novalidate>
+<form name="crmMailingAB" novalidate ng-hide="isSubmitted()">
<div class="crm-block crm-form-block crmMailing">
<div crm-ui-wizard>
<div crm-ui-wizard-step="10" crm-title="ts('Setup')">
<span crm-ui-wizard-buttons style="float:right;">
<button
crm-confirm="{title:ts('Delete Draft?'), message:ts('Are you sure you want to delete the draft mailing?')}"
- on-yes="delete()">{{ts('Delete Draft')}}
+ on-yes="delete().then(leave)">{{ts('Delete Draft')}}
</button>
<button ng-click="save()">{{ts('Save Draft')}}</button>
</span>