From 86c3a3270e4fe2e54f3e81da26fe98eb6465c6a8 Mon Sep 17 00:00:00 2001 From: Tim Otten Date: Thu, 18 Dec 2014 18:42:22 -0800 Subject: [PATCH] CRM-15578 - crmMailing(AB) - Submission & status tracking 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.) --- js/angular-crmMailing.js | 44 ++++++++++++------ js/angular-crmMailing/services.js | 62 +++++++++++++++++++------- js/angular-crmMailingAB.js | 42 ++++++++++++++--- js/angular-crmMailingAB/services.js | 15 +++++++ partials/crmMailing/edit-unified.html | 6 ++- partials/crmMailing/edit-unified2.html | 8 +++- partials/crmMailing/edit-wizard.html | 6 ++- partials/crmMailing/edit.html | 8 +++- partials/crmMailingAB/edit.html | 7 ++- 9 files changed, 154 insertions(+), 44 deletions(-) diff --git a/js/angular-crmMailing.js b/js/angular-crmMailing.js index 0f5b3bff03..1124a8a153 100644 --- a/js/angular-crmMailing.js +++ b/js/angular-crmMailing.js @@ -77,6 +77,10 @@ $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); @@ -87,7 +91,8 @@ 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); @@ -97,13 +102,18 @@ // @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); }; @@ -116,6 +126,10 @@ // pre-condition: the mailing exists *before* saving attachments to it return $scope.attachments.save(); }) + .then(function () { + updateUrl(); + return $scope.mailing; + }) ); }; @@ -133,16 +147,18 @@ }); }; - // 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 ( diff --git a/js/angular-crmMailing/services.js b/js/angular-crmMailing/services.js index 3202ee054d..483ddee045 100644 --- a/js/angular-crmMailing/services.js +++ b/js/angular-crmMailing/services.js @@ -29,7 +29,7 @@ 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)' }); @@ -74,7 +74,7 @@ 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) }); }); @@ -135,8 +135,24 @@ }, // @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) { @@ -144,13 +160,20 @@ 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, @@ -234,7 +257,7 @@ // @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' @@ -252,7 +275,7 @@ 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': { @@ -272,7 +295,7 @@ // @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 }); @@ -281,12 +304,14 @@ // 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; }); }, @@ -294,15 +319,20 @@ // @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 @@ -310,7 +340,7 @@ // @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', @@ -324,6 +354,8 @@ // 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; diff --git a/js/angular-crmMailingAB.js b/js/angular-crmMailingAB.js index 47273b8d2b..9ca3f2d603 100644 --- a/js/angular-crmMailingAB.js +++ b/js/angular-crmMailingAB.js @@ -38,14 +38,16 @@ $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}); @@ -77,10 +79,13 @@ } 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); @@ -88,16 +93,26 @@ // @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() { @@ -105,6 +120,19 @@ $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(); diff --git a/js/angular-crmMailingAB/services.js b/js/angular-crmMailingAB/services.js index 7fe65355aa..becabe19fa 100644 --- a/js/angular-crmMailingAB/services.js +++ b/js/angular-crmMailingAB/services.js @@ -70,6 +70,9 @@ 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(); }); @@ -90,6 +93,18 @@ 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() { diff --git a/partials/crmMailing/edit-unified.html b/partials/crmMailing/edit-unified.html index 005c57178d..5aaad4c4b5 100644 --- a/partials/crmMailing/edit-unified.html +++ b/partials/crmMailing/edit-unified.html @@ -2,7 +2,11 @@
{{mailing|json}}
-
+
+ {{ts('This mailing has been submitted.')}} +
+ +
diff --git a/partials/crmMailing/edit-unified2.html b/partials/crmMailing/edit-unified2.html index 9140e522cd..00725dc93f 100644 --- a/partials/crmMailing/edit-unified2.html +++ b/partials/crmMailing/edit-unified2.html @@ -2,7 +2,11 @@
{{mailing|json}}
- +
+ {{ts('This mailing has been submitted.')}} +
+ +
@@ -36,7 +40,7 @@
- +
diff --git a/partials/crmMailing/edit-wizard.html b/partials/crmMailing/edit-wizard.html index 1548e5b3c0..31b9104b4f 100644 --- a/partials/crmMailing/edit-wizard.html +++ b/partials/crmMailing/edit-wizard.html @@ -2,7 +2,11 @@
{{mailing|json}}
- +
+ {{ts('This mailing has been submitted.')}} +
+ +
diff --git a/partials/crmMailing/edit.html b/partials/crmMailing/edit.html index f6ed9c29f1..0b914e760e 100644 --- a/partials/crmMailing/edit.html +++ b/partials/crmMailing/edit.html @@ -2,7 +2,11 @@
{{mailing|json}}
- +
+ {{ts('This mailing has been submitted.')}} +
+ +
@@ -47,7 +51,7 @@
- +
{{ts('Submit Mailing')}}
diff --git a/partials/crmMailingAB/edit.html b/partials/crmMailingAB/edit.html index ae935fde70..9211e23435 100644 --- a/partials/crmMailingAB/edit.html +++ b/partials/crmMailingAB/edit.html @@ -9,8 +9,11 @@ individual field from B). At the end of the composition process, the controller's "sync" operation will merge shared settings from "A" into "B". --> +
+ {{ts('This mailing has been submitted.')}} +
- +
@@ -172,7 +175,7 @@ -- 2.25.1