From f0039e32d3185d64309731822b59fc9639d71d15 Mon Sep 17 00:00:00 2001 From: Tim Otten Date: Wed, 31 Jan 2024 15:08:19 -0800 Subject: [PATCH 01/13] Move the "A/B" testing options to end When we add support for "A/B" testing of different message-bodies, there will be more ways to enable "A/B" testing. We may not know on the first screen whether "A/B" is active. This moves the "A/B" distribution options to the end -- at which point, we will know for sure whether "A/B" testing is active. --- ang/crmMosaico/BlockMailing.html | 5 ++--- ang/crmMosaico/BlockSchedule.html | 6 ++++++ 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/ang/crmMosaico/BlockMailing.html b/ang/crmMosaico/BlockMailing.html index 3300880a7e..a4c0c3ca4f 100644 --- a/ang/crmMosaico/BlockMailing.html +++ b/ang/crmMosaico/BlockMailing.html @@ -65,9 +65,8 @@ -
- - +
+ ({{ts('You have chosen two subject lines. We will organize a A/B testing to determine which is better.')}})
diff --git a/ang/crmMosaico/BlockSchedule.html b/ang/crmMosaico/BlockSchedule.html index b70684daae..e22e9c7c90 100644 --- a/ang/crmMosaico/BlockSchedule.html +++ b/ang/crmMosaico/BlockSchedule.html @@ -1,5 +1,11 @@
+
+ +

{{ts('You have enabled A/B testing. We will send each test message to a subset of your subscribers. Later, you can decide the final mailing.')}}

+ +
+
From b360910f849f5a3708576714d1c83e1de31a3f84 Mon Sep 17 00:00:00 2001 From: Tim Otten Date: Wed, 31 Jan 2024 16:28:17 -0800 Subject: [PATCH 02/13] (NFC) bootstrap-wizard.html - Fix misaligned whitespace --- .../EditMailingCtrl/bootstrap-wizard.html | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/ang/crmMosaico/EditMailingCtrl/bootstrap-wizard.html b/ang/crmMosaico/EditMailingCtrl/bootstrap-wizard.html index 9d41e0f605..502599ca9d 100644 --- a/ang/crmMosaico/EditMailingCtrl/bootstrap-wizard.html +++ b/ang/crmMosaico/EditMailingCtrl/bootstrap-wizard.html @@ -21,30 +21,30 @@
+ + {{ts('Back')}} + + + {{ts('Delete Draft')}} + + + {{ts('Save Draft')}} + + + {{ts('Continue')}} + + + {{ts('Submit Mailing')}} +
From 592b2cb4330f5fbd0fb3224d83cc4a44c2836026 Mon Sep 17 00:00:00 2001 From: Tim Otten Date: Wed, 31 Jan 2024 16:09:06 -0800 Subject: [PATCH 03/13] (REF) Extract helper crmMosaicoVariants --- ang/crmMosaico/SubjectList.html | 4 +-- ang/crmMosaico/SubjectList.js | 23 ++++------------ ang/crmMosaico/Variants.js | 48 +++++++++++++++++++++++++++++++++ 3 files changed, 55 insertions(+), 20 deletions(-) create mode 100644 ang/crmMosaico/Variants.js diff --git a/ang/crmMosaico/SubjectList.html b/ang/crmMosaico/SubjectList.html index 89b3e47f2a..538264d48d 100644 --- a/ang/crmMosaico/SubjectList.html +++ b/ang/crmMosaico/SubjectList.html @@ -1,4 +1,4 @@ -
+
-
+
({{labels[vid]}}) diff --git a/ang/crmMosaico/SubjectList.js b/ang/crmMosaico/SubjectList.js index d5ca3a7e72..f6ca8cad9f 100644 --- a/ang/crmMosaico/SubjectList.js +++ b/ang/crmMosaico/SubjectList.js @@ -1,6 +1,6 @@ (function(angular, $, _) { // Example usage: - angular.module('crmMosaico').directive('crmMosaicoSubjectList', function(crmUiHelp) { + angular.module('crmMosaico').directive('crmMosaicoSubjectList', function(crmUiHelp, crmMosaicoVariants) { return { scope: { crmMailing: '@' @@ -14,23 +14,10 @@ scope.hs = crmUiHelp({file: 'CRM/Mailing/MailingUI'}); scope.checkPerm = CRM.checkPerm; - scope.addSubj = function addSubj() { - scope.mailing.template_options.variants = [ - {subject: scope.mailing.subject}, - {subject: scope.mailing.subject} - ] - }; - - scope.rmSubj = function rmSubj(vid) { - var m = scope.mailing; - m.template_options.variants.splice(vid, 1); - if (m.template_options.variants.length === 1) { - m.subject = m.template_options.variants[0].subject; - delete m.template_options.variants; - } - }; - - scope.labels = ['A', 'B']; + scope.addSubj = () => crmMosaicoVariants.add(scope.mailing, 'subject'); + scope.rmSubj = (vid) => crmMosaicoVariants.remove(scope.mailing, 'subject', vid); + scope.isSplit = () => crmMosaicoVariants.isSplit(scope.mailing, 'subject'); + scope.labels = crmMosaicoVariants.getLabels(); } }; diff --git a/ang/crmMosaico/Variants.js b/ang/crmMosaico/Variants.js new file mode 100644 index 0000000000..3de2f435aa --- /dev/null +++ b/ang/crmMosaico/Variants.js @@ -0,0 +1,48 @@ +(function(angular, $, _) { + angular.module('crmMosaico').service('crmMosaicoVariants', function() { + // This utility helps you manipulate the `variants` on a mailing. + + function angularObj(obj) { + return JSON.parse(angular.toJson(obj)); + } + + return { + getLabels: () => ['A', 'B'], + + // Enable variations for a particular field + // Ex: crmMosaicoVariants.addVariation(mymailing, 'subject') + add: function add(mailing, field) { + mailing.template_options.variants = mailing.template_options.variants || []; + for (var vid = 0; vid < 2; vid++) { + mailing.template_options.variants[vid] = mailing.template_options.variants[vid] || {}; + mailing.template_options.variants[vid][field] = mailing[field]; + } + }, + + // Disable variations for a particular field. Remove a particular record. + // Ex: crmMosaicoVariants.removeVariation(mymailing, 'subject', 1) + remove: function remove(mailing, field, badVid) { + delete mailing.template_options.variants[badVid][field]; + + // If there's only one value of `field`, then move it to top. + const remainders = _.filter(mailing.template_options.variants, (v) => v[field] !== undefined); + if (remainders.length === 1) { + mailing[field] = remainders[0][field]; + for (var delVid = 0; delVid < 2; delVid++) { + delete mailing.template_options.variants[delVid][field]; + } + } + + // If the variants are empty, then delete the variant objects + const nonEmpties = _.filter(mailing.template_options.variants, (v) => !_.isEmpty(angularObj(v))); + if (nonEmpties.length === 0) { + delete mailing.template_options.variants; + } + }, + + isSplit: function isSplit(mailing, field) { + return mailing.template_options && mailing.template_options.variants && (field in mailing.template_options.variants[0]); + } + }; + }); +})(angular, CRM.$, CRM._); From 45f0361e0042a3d1f5b901fa46a8090643d5bedd Mon Sep 17 00:00:00 2001 From: Tim Otten Date: Thu, 1 Feb 2024 13:37:51 -0800 Subject: [PATCH 04/13] crmMosaicoVariants - Handle mosaico-style fields --- ang/crmMosaico/SubjectList.js | 2 +- ang/crmMosaico/Variants.js | 56 ++++++++++++++++++++++++----------- 2 files changed, 39 insertions(+), 19 deletions(-) diff --git a/ang/crmMosaico/SubjectList.js b/ang/crmMosaico/SubjectList.js index f6ca8cad9f..bdbbc5c655 100644 --- a/ang/crmMosaico/SubjectList.js +++ b/ang/crmMosaico/SubjectList.js @@ -14,7 +14,7 @@ scope.hs = crmUiHelp({file: 'CRM/Mailing/MailingUI'}); scope.checkPerm = CRM.checkPerm; - scope.addSubj = () => crmMosaicoVariants.add(scope.mailing, 'subject'); + scope.addSubj = () => crmMosaicoVariants.split(scope.mailing, 'subject'); scope.rmSubj = (vid) => crmMosaicoVariants.remove(scope.mailing, 'subject', vid); scope.isSplit = () => crmMosaicoVariants.isSplit(scope.mailing, 'subject'); scope.labels = crmMosaicoVariants.getLabels(); diff --git a/ang/crmMosaico/Variants.js b/ang/crmMosaico/Variants.js index 3de2f435aa..f544f8bb48 100644 --- a/ang/crmMosaico/Variants.js +++ b/ang/crmMosaico/Variants.js @@ -6,33 +6,52 @@ return JSON.parse(angular.toJson(obj)); } - return { + const mainPlaceholders = {subject: 'VARIANT SUBJECTS', body_html: 'VARIANT HTMLS', mosaicoTemplate: "", mosaicoMetadata: '{}', mosaicoContent: '{}'}; + + const self = { getLabels: () => ['A', 'B'], - // Enable variations for a particular field - // Ex: crmMosaicoVariants.addVariation(mymailing, 'subject') - add: function add(mailing, field) { + // Enable variations for a particular field. (By default, old value is copied to A+B. But you can optionally override B.) + // Ex: crmMosaicoVariants.split(mymailing, 'subject') + // Ex: crmMosaicoVariants.split(mymailing, ['body_html', 'mosaicoContent']) + // Ex: crmMosaicoVariants.split(mymailing, 'subject', '') + // Ex: crmMosaicoVariants.split(mymailing, ['body_html', 'mosaicoContent'], {body_html: '', mosaicoContent: null}) + split: function split(mailing, field, bValue) { + if (_.isArray(field)) { + bValue = bValue || {}; + return angular.forEach(field, (f) => self.split(mailing, f, bValue[field])); + } + mailing.template_options.variants = mailing.template_options.variants || []; - for (var vid = 0; vid < 2; vid++) { - mailing.template_options.variants[vid] = mailing.template_options.variants[vid] || {}; - mailing.template_options.variants[vid][field] = mailing[field]; + + const mainObj = field.match(/^mosaico/) ? mailing.template_options : mailing; + mailing.template_options.variants[0] = mailing.template_options.variants[0] || {}; + + mailing.template_options.variants[1] = mailing.template_options.variants[1] || {}; + mailing.template_options.variants[0][field] = mainObj[field]; + mailing.template_options.variants[1][field] = (bValue === undefined ? mainObj[field] : bValue); + + if (mainPlaceholders[field] !== undefined) { + mainObj[field] = mainPlaceholders[field]; } }, // Disable variations for a particular field. Remove a particular record. - // Ex: crmMosaicoVariants.removeVariation(mymailing, 'subject', 1) - remove: function remove(mailing, field, badVid) { - delete mailing.template_options.variants[badVid][field]; - - // If there's only one value of `field`, then move it to top. - const remainders = _.filter(mailing.template_options.variants, (v) => v[field] !== undefined); - if (remainders.length === 1) { - mailing[field] = remainders[0][field]; - for (var delVid = 0; delVid < 2; delVid++) { - delete mailing.template_options.variants[delVid][field]; - } + // Ex: crmMosaicoVariants.remove(mymailing, 'subject', 0) + // Ex: crmMosaicoVariants.remove(mymailing, ['body_html', 'mosaicoContent'], 1) + remove: function remove(mailing, field, deleteVid) { + if (_.isArray(field)) { + return angular.forEach(field, (f) => self.remove(mailing, f, deleteVid)); } + const mainObj = field.match(/^mosaico/) ? mailing.template_options : mailing; + const deadVariant = mailing.template_options.variants[deleteVid]; + const liveVariant = mailing.template_options.variants[deleteVid ? 0 : 1]; + + mainObj[field] = liveVariant[field]; + delete liveVariant[field]; + delete deadVariant[field]; + // If the variants are empty, then delete the variant objects const nonEmpties = _.filter(mailing.template_options.variants, (v) => !_.isEmpty(angularObj(v))); if (nonEmpties.length === 0) { @@ -44,5 +63,6 @@ return mailing.template_options && mailing.template_options.variants && (field in mailing.template_options.variants[0]); } }; + return self; }); })(angular, CRM.$, CRM._); From 461831562ed49f4185c350838687f9015a290e02 Mon Sep 17 00:00:00 2001 From: Tim Otten Date: Thu, 1 Feb 2024 16:44:09 -0800 Subject: [PATCH 05/13] crmMosaicoBlockDesign - Explicitly pass-through scope.attachments --- ang/crmMosaico/BlockDesign.js | 6 +++++- ang/crmMosaico/EditMailingCtrl/bootstrap-single.html | 2 +- ang/crmMosaico/EditMailingCtrl/bootstrap-wizard.html | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/ang/crmMosaico/BlockDesign.js b/ang/crmMosaico/BlockDesign.js index 703d95c131..22e79b337c 100644 --- a/ang/crmMosaico/BlockDesign.js +++ b/ang/crmMosaico/BlockDesign.js @@ -3,13 +3,17 @@ return { scope: { crmMosaicoCtrl: '@', - crmMailing: '@' + crmMailing: '@', + crmMailingAttachments: '@' }, templateUrl: '~/crmMosaico/BlockDesign.html', link: function (scope, elm, attr) { scope.$parent.$watch(attr.crmMailing, function(newValue){ scope.mailing = newValue; }); + scope.$parent.$watch(attr.crmMailingAttachments, function(newValue){ + scope.attachments = newValue; + }); scope.$parent.$watch(attr.crmMosaicoCtrl, function(newValue){ scope.mosaicoCtrl = newValue; }); diff --git a/ang/crmMosaico/EditMailingCtrl/bootstrap-single.html b/ang/crmMosaico/EditMailingCtrl/bootstrap-single.html index 019784766d..d96a30f4c3 100644 --- a/ang/crmMosaico/EditMailingCtrl/bootstrap-single.html +++ b/ang/crmMosaico/EditMailingCtrl/bootstrap-single.html @@ -12,7 +12,7 @@
-
+
diff --git a/ang/crmMosaico/EditMailingCtrl/bootstrap-wizard.html b/ang/crmMosaico/EditMailingCtrl/bootstrap-wizard.html index 502599ca9d..3582ea07a0 100644 --- a/ang/crmMosaico/EditMailingCtrl/bootstrap-wizard.html +++ b/ang/crmMosaico/EditMailingCtrl/bootstrap-wizard.html @@ -7,7 +7,7 @@
-
+
From 16390cee55e3d554719c6bd23ce6ea7d005d1070 Mon Sep 17 00:00:00 2001 From: Tim Otten Date: Thu, 1 Feb 2024 17:06:25 -0800 Subject: [PATCH 06/13] (REF) Move mosaicoCtrl into crm-mailing-block-design --- ang/crmMosaico/BlockDesign.js | 120 +++++++++++++++++- .../EditMailingCtrl/bootstrap-single.html | 2 +- .../EditMailingCtrl/bootstrap-wizard.html | 2 +- ang/crmMosaico/MixinCtrl.js | 104 +-------------- 4 files changed, 118 insertions(+), 110 deletions(-) diff --git a/ang/crmMosaico/BlockDesign.js b/ang/crmMosaico/BlockDesign.js index 22e79b337c..e9e0ac24b8 100644 --- a/ang/crmMosaico/BlockDesign.js +++ b/ang/crmMosaico/BlockDesign.js @@ -1,10 +1,11 @@ (function(angular, $, _) { - angular.module('crmMosaico').directive('crmMosaicoBlockDesign', function($q, crmUiHelp) { + angular.module('crmMosaico').directive('crmMosaicoBlockDesign', function($q, crmUiHelp, dialogService, crmMosaicoTemplates, crmStatus, CrmMosaicoIframe, $timeout) { + return { scope: { - crmMosaicoCtrl: '@', + // crmMosaicoCtrl: '@', crmMailing: '@', - crmMailingAttachments: '@' + crmMailingAttachments: '@', }, templateUrl: '~/crmMosaico/BlockDesign.html', link: function (scope, elm, attr) { @@ -14,12 +15,119 @@ scope.$parent.$watch(attr.crmMailingAttachments, function(newValue){ scope.attachments = newValue; }); - scope.$parent.$watch(attr.crmMosaicoCtrl, function(newValue){ - scope.mosaicoCtrl = newValue; - }); scope.crmMailingConst = CRM.crmMailing; scope.ts = CRM.ts(null); scope.hs = crmUiHelp({file: 'CRM/Mailing/MailingUI'}); + + const $scope = scope; + var crmMosaicoIframe = null, activeDialogs = {}; + $scope.mosaicoCtrl = { + templates: [], + // Fill a given "mailing" which the chosen "template". + select: function(mailing, template) { + var topt = mailing.template_options = mailing.template_options || {}; + var promise = crmMosaicoTemplates.getFull(template).then(function(tplCtnt){ + topt.mosaicoTemplate = template.id; + topt.mosaicoMetadata = tplCtnt.metadata; + topt.mosaicoContent = tplCtnt.content; + mailing.body_html = tplCtnt.html; + // console.log('select', {isAr1: _.isArray(mailing.template_options), isAr2: _.isArray(topt), m: mailing, t: template}); + $scope.mosaicoCtrl.edit(mailing); + }); + return crmStatus({start: ts('Loading...'), success: null}, promise); + }, + // Figure out which "template" was previously used with a "mailing." + getTemplate: function(mailing) { + if (!mailing || !mailing.template_options || !mailing.template_options.mosaicoTemplate) { + return null; + } + var matches = _.where($scope.mosaicoCtrl.templates, { + id: mailing.template_options.mosaicoTemplate + }); + return matches.length > 0 ? matches[0] : null; + }, + // Reset all Mosaico data in a "mailing'. + reset: function(mailing) { + if (crmMosaicoIframe) crmMosaicoIframe.destroy(); + crmMosaicoIframe = null; + delete mailing.template_options.mosaicoTemplate; + delete mailing.template_options.mosaicoMetadata; + delete mailing.template_options.mosaicoContent; + mailing.body_html = ''; + }, + // Edit a mailing in Mosaico. + edit: function(mailing) { + if (crmMosaicoIframe) { + crmMosaicoIframe.show(); + return; + } + + function syncModel(viewModel) { + mailing.body_html = viewModel.exportHTML(); + mailing.template_options = mailing.template_options || {}; + // Mosaico exports JSON. Keep their original encoding... or else the loader throws an error. + mailing.template_options.mosaicoMetadata = viewModel.exportMetadata(); + mailing.template_options.mosaicoContent = viewModel.exportJSON(); + } + + crmMosaicoIframe = new CrmMosaicoIframe({ + model: { + template: $scope.mosaicoCtrl.getTemplate(mailing).path, + metadata: mailing.template_options.mosaicoMetadata, + content: mailing.template_options.mosaicoContent + }, + actions: { + sync: function(ko, viewModel) { + syncModel(viewModel); + }, + close: function(ko, viewModel) { + viewModel.metadata.changed = Date.now(); + syncModel(viewModel); + // TODO: When autosave is better integrated, remove this. + $timeout(function(){ + $scope.$parent.$apply(attr.onSave); + }, 100); + crmMosaicoIframe.hide('crmMosaicoEditorDialog'); + }, + test: function(ko, viewModel) { + syncModel(viewModel); + + var model = {mailing: $scope.mailing, attachments: $scope.attachments}; + console.log('test!', model); + var options = CRM.utils.adjustDialogDefaults(angular.extend( + {autoOpen: false, title: ts('Preview / Test'), width: 550}, + options + )); + activeDialogs.crmMosaicoPreviewDialog = 1; + var pr = dialogService.open('crmMosaicoPreviewDialog', '~/crmMosaico/PreviewDialogCtrl.html', model, options) + .finally(function(){ delete activeDialogs.crmMosaicoPreviewDialog; }); + return pr; + } + } + }); + + return crmStatus({start: ts('Loading...'), success: null}, crmMosaicoIframe.open()); + } + }; + + crmMosaicoTemplates.whenLoaded().then(function(){ + $scope.mosaicoCtrl.templates = crmMosaicoTemplates.getAll(); + $scope.mosaicoCtrl.categoryFilters = _.transform(crmMosaicoTemplates.getCategories(), function(filters, category) { + filters.push({id: filters.length, text: category.label, filter: {category_id: category.value}}); + }, [{id: 0, text: ts('Base Template'), filter: {isBase: true}}]); + }); + + $scope.$on("$destroy", function() { + angular.forEach(activeDialogs, function(v,name){ + dialogService.cancel(name); + }); + if (crmMosaicoIframe) { + crmMosaicoIframe.destroy(); + crmMosaicoIframe = null; + } + }); + + } }; }); diff --git a/ang/crmMosaico/EditMailingCtrl/bootstrap-single.html b/ang/crmMosaico/EditMailingCtrl/bootstrap-single.html index d96a30f4c3..b757c85d87 100644 --- a/ang/crmMosaico/EditMailingCtrl/bootstrap-single.html +++ b/ang/crmMosaico/EditMailingCtrl/bootstrap-single.html @@ -12,7 +12,7 @@
-
+
diff --git a/ang/crmMosaico/EditMailingCtrl/bootstrap-wizard.html b/ang/crmMosaico/EditMailingCtrl/bootstrap-wizard.html index 3582ea07a0..8edc051718 100644 --- a/ang/crmMosaico/EditMailingCtrl/bootstrap-wizard.html +++ b/ang/crmMosaico/EditMailingCtrl/bootstrap-wizard.html @@ -7,7 +7,7 @@
-
+
diff --git a/ang/crmMosaico/MixinCtrl.js b/ang/crmMosaico/MixinCtrl.js index 4aea10def7..f26670cd5a 100644 --- a/ang/crmMosaico/MixinCtrl.js +++ b/ang/crmMosaico/MixinCtrl.js @@ -2,99 +2,10 @@ // This provides additional actions for editing a Mosaico mailing. // It coexists with crmMailing's EditMailingCtrl. - angular.module('crmMosaico').controller('CrmMosaicoMixinCtrl', function CrmMosaicoMixinCtrl($scope, dialogService, crmMosaicoTemplates, crmStatus, CrmMosaicoIframe, $timeout) { + angular.module('crmMosaico').controller('CrmMosaicoMixinCtrl', function CrmMosaicoMixinCtrl($scope, dialogService) { // var ts = $scope.ts = CRM.ts(null); - // Main data is in $scope.mailing, $scope.mosaicoCtrl.template - - var crmMosaicoIframe = null, activeDialogs = {}; - - // Hrm, would like `ng-controller="CrmMosaicoMixinCtrl as mosaicoCtrl`, but that's not working... - $scope.mosaicoCtrl = { - templates: [], - // Fill a given "mailing" which the chosen "template". - select: function(mailing, template) { - var topt = mailing.template_options = mailing.template_options || {}; - var promise = crmMosaicoTemplates.getFull(template).then(function(tplCtnt){ - topt.mosaicoTemplate = template.id; - topt.mosaicoMetadata = tplCtnt.metadata; - topt.mosaicoContent = tplCtnt.content; - mailing.body_html = tplCtnt.html; - // console.log('select', {isAr1: _.isArray(mailing.template_options), isAr2: _.isArray(topt), m: mailing, t: template}); - $scope.mosaicoCtrl.edit(mailing); - }); - return crmStatus({start: ts('Loading...'), success: null}, promise); - }, - // Figure out which "template" was previously used with a "mailing." - getTemplate: function(mailing) { - if (!mailing || !mailing.template_options || !mailing.template_options.mosaicoTemplate) { - return null; - } - var matches = _.where($scope.mosaicoCtrl.templates, { - id: mailing.template_options.mosaicoTemplate - }); - return matches.length > 0 ? matches[0] : null; - }, - // Reset all Mosaico data in a "mailing'. - reset: function(mailing) { - if (crmMosaicoIframe) crmMosaicoIframe.destroy(); - crmMosaicoIframe = null; - delete mailing.template_options.mosaicoTemplate; - delete mailing.template_options.mosaicoMetadata; - delete mailing.template_options.mosaicoContent; - mailing.body_html = ''; - }, - // Edit a mailing in Mosaico. - edit: function(mailing) { - if (crmMosaicoIframe) { - crmMosaicoIframe.show(); - return; - } - - function syncModel(viewModel) { - mailing.body_html = viewModel.exportHTML(); - mailing.template_options = mailing.template_options || {}; - // Mosaico exports JSON. Keep their original encoding... or else the loader throws an error. - mailing.template_options.mosaicoMetadata = viewModel.exportMetadata(); - mailing.template_options.mosaicoContent = viewModel.exportJSON(); - } - - crmMosaicoIframe = new CrmMosaicoIframe({ - model: { - template: $scope.mosaicoCtrl.getTemplate(mailing).path, - metadata: mailing.template_options.mosaicoMetadata, - content: mailing.template_options.mosaicoContent - }, - actions: { - sync: function(ko, viewModel) { - syncModel(viewModel); - }, - close: function(ko, viewModel) { - viewModel.metadata.changed = Date.now(); - syncModel(viewModel); - // TODO: When autosave is better integrated, remove this. - $timeout(function(){$scope.save();}, 100); - crmMosaicoIframe.hide('crmMosaicoEditorDialog'); - }, - test: function(ko, viewModel) { - syncModel(viewModel); - - var model = {mailing: $scope.mailing, attachments: $scope.attachments}; - var options = CRM.utils.adjustDialogDefaults(angular.extend( - {autoOpen: false, title: ts('Preview / Test'), width: 550}, - options - )); - activeDialogs.crmMosaicoPreviewDialog = 1; - var pr = dialogService.open('crmMosaicoPreviewDialog', '~/crmMosaico/PreviewDialogCtrl.html', model, options) - .finally(function(){ delete activeDialogs.crmMosaicoPreviewDialog; }); - return pr; - } - } - }); - - return crmStatus({start: ts('Loading...'), success: null}, crmMosaicoIframe.open()); - } - }; + var activeDialogs = {}; // Open a dialog of advanced options. $scope.openAdvancedOptions = function() { @@ -113,21 +24,10 @@ .finally(function(){ delete activeDialogs.crmMosaicoAdvancedDialog; }); }; - crmMosaicoTemplates.whenLoaded().then(function(){ - $scope.mosaicoCtrl.templates = crmMosaicoTemplates.getAll(); - $scope.mosaicoCtrl.categoryFilters = _.transform(crmMosaicoTemplates.getCategories(), function(filters, category) { - filters.push({id: filters.length, text: category.label, filter: {category_id: category.value}}); - }, [{id: 0, text: ts('Base Template'), filter: {isBase: true}}]); - }); - $scope.$on("$destroy", function() { angular.forEach(activeDialogs, function(v,name){ dialogService.cancel(name); }); - if (crmMosaicoIframe) { - crmMosaicoIframe.destroy(); - crmMosaicoIframe = null; - } }); }); From a27c3b4ba9c6b0311dabd74b6eeda1258d017b97 Mon Sep 17 00:00:00 2001 From: Tim Otten Date: Thu, 1 Feb 2024 17:35:30 -0800 Subject: [PATCH 07/13] (REF) crmMosaicoBlockDesign - Extract methods hasMarkup(), hasSelection() --- ang/crmMosaico/BlockDesign.html | 6 +++--- ang/crmMosaico/BlockDesign.js | 6 ++++++ 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/ang/crmMosaico/BlockDesign.html b/ang/crmMosaico/BlockDesign.html index f8b12ab649..d9c2ee8f19 100644 --- a/ang/crmMosaico/BlockDesign.html +++ b/ang/crmMosaico/BlockDesign.html @@ -1,13 +1,13 @@
- +
+ ng-show="mosaicoCtrl.hasSelection(mailing)">
-
+
diff --git a/ang/crmMosaico/BlockDesign.js b/ang/crmMosaico/BlockDesign.js index e9e0ac24b8..f64aa806c3 100644 --- a/ang/crmMosaico/BlockDesign.js +++ b/ang/crmMosaico/BlockDesign.js @@ -36,6 +36,12 @@ }); return crmStatus({start: ts('Loading...'), success: null}, promise); }, + hasSelection: function(mailing) { + return !!mailing.template_options.mosaicoTemplate; + }, + hasMarkup: function(mailing) { + return !!mailing.body_html; + }, // Figure out which "template" was previously used with a "mailing." getTemplate: function(mailing) { if (!mailing || !mailing.template_options || !mailing.template_options.mosaicoTemplate) { From 11ccb44371ac3d624e8de733e9486417621709cc Mon Sep 17 00:00:00 2001 From: Tim Otten Date: Thu, 1 Feb 2024 17:40:39 -0800 Subject: [PATCH 08/13] (REF) crmMosaicoBlockDesign - Simplify signatures. Allow implicit target (`mailing`) --- ang/crmMosaico/BlockDesign.html | 14 +++++++------- ang/crmMosaico/BlockDesign.js | 22 ++++++++++++++-------- 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/ang/crmMosaico/BlockDesign.html b/ang/crmMosaico/BlockDesign.html index d9c2ee8f19..d495f4fb62 100644 --- a/ang/crmMosaico/BlockDesign.html +++ b/ang/crmMosaico/BlockDesign.html @@ -1,20 +1,20 @@
- +
+ crm-mosaico-template-item="{state:'selected', title: ts('My Design'), subtitle: mosaicoCtrl.getTemplate().type, img: mosaicoCtrl.getTemplate().thumbnail}" + on-item-click="mosaicoCtrl.edit()" + on-item-reset="mosaicoCtrl.reset()" + ng-show="mosaicoCtrl.hasSelection()">
-
+
+ on-item-click="mosaicoCtrl.select(template)">
diff --git a/ang/crmMosaico/BlockDesign.js b/ang/crmMosaico/BlockDesign.js index f64aa806c3..027d06a6fb 100644 --- a/ang/crmMosaico/BlockDesign.js +++ b/ang/crmMosaico/BlockDesign.js @@ -24,7 +24,8 @@ $scope.mosaicoCtrl = { templates: [], // Fill a given "mailing" which the chosen "template". - select: function(mailing, template) { + select: function(template) { + const mailing = scope.mailing; var topt = mailing.template_options = mailing.template_options || {}; var promise = crmMosaicoTemplates.getFull(template).then(function(tplCtnt){ topt.mosaicoTemplate = template.id; @@ -32,18 +33,21 @@ topt.mosaicoContent = tplCtnt.content; mailing.body_html = tplCtnt.html; // console.log('select', {isAr1: _.isArray(mailing.template_options), isAr2: _.isArray(topt), m: mailing, t: template}); - $scope.mosaicoCtrl.edit(mailing); + $scope.mosaicoCtrl.edit(); }); return crmStatus({start: ts('Loading...'), success: null}, promise); }, - hasSelection: function(mailing) { + hasSelection: function() { + const mailing = scope.mailing; return !!mailing.template_options.mosaicoTemplate; }, - hasMarkup: function(mailing) { + hasMarkup: function() { + const mailing = scope.mailing; return !!mailing.body_html; }, // Figure out which "template" was previously used with a "mailing." - getTemplate: function(mailing) { + getTemplate: function() { + const mailing = scope.mailing; if (!mailing || !mailing.template_options || !mailing.template_options.mosaicoTemplate) { return null; } @@ -53,7 +57,8 @@ return matches.length > 0 ? matches[0] : null; }, // Reset all Mosaico data in a "mailing'. - reset: function(mailing) { + reset: function() { + const mailing = scope.mailing; if (crmMosaicoIframe) crmMosaicoIframe.destroy(); crmMosaicoIframe = null; delete mailing.template_options.mosaicoTemplate; @@ -62,7 +67,8 @@ mailing.body_html = ''; }, // Edit a mailing in Mosaico. - edit: function(mailing) { + edit: function() { + const mailing = scope.mailing; if (crmMosaicoIframe) { crmMosaicoIframe.show(); return; @@ -78,7 +84,7 @@ crmMosaicoIframe = new CrmMosaicoIframe({ model: { - template: $scope.mosaicoCtrl.getTemplate(mailing).path, + template: $scope.mosaicoCtrl.getTemplate().path, metadata: mailing.template_options.mosaicoMetadata, content: mailing.template_options.mosaicoContent }, From 0db9fdd495521013bb5846829e9d923509f227d4 Mon Sep 17 00:00:00 2001 From: Tim Otten Date: Thu, 1 Feb 2024 17:57:24 -0800 Subject: [PATCH 09/13] crmMosaicoBlockDesign - Accept variant. Extract methods getProp(), setProp(), deleteProp() --- ang/crmMosaico/BlockDesign.js | 58 +++++++++++++++++++---------------- 1 file changed, 32 insertions(+), 26 deletions(-) diff --git a/ang/crmMosaico/BlockDesign.js b/ang/crmMosaico/BlockDesign.js index 027d06a6fb..f41e9c21f2 100644 --- a/ang/crmMosaico/BlockDesign.js +++ b/ang/crmMosaico/BlockDesign.js @@ -6,6 +6,7 @@ // crmMosaicoCtrl: '@', crmMailing: '@', crmMailingAttachments: '@', + crmMailingVariant: '@', }, templateUrl: '~/crmMosaico/BlockDesign.html', link: function (scope, elm, attr) { @@ -15,78 +16,84 @@ scope.$parent.$watch(attr.crmMailingAttachments, function(newValue){ scope.attachments = newValue; }); + scope.$parent.$watch(attr.crmMailingVariant, function(newValue){ + scope.variantId = newValue; + }); scope.crmMailingConst = CRM.crmMailing; scope.ts = CRM.ts(null); scope.hs = crmUiHelp({file: 'CRM/Mailing/MailingUI'}); + function _object(prop) { + const mailing = scope.mailing; + mailing.template_options = mailing.template_options || {}; + + if (scope.variantId !== null) return mailing.template_options.variants[scope.variantId]; + return prop.match(/^mosaico/) ? mailing.template_options : mailing; + } + function getProp(prop) { return _object(prop)[prop]; } + function setProp(prop, value) { _object(prop)[prop] = value; } + function deleteProp(prop) { delete _object(prop)[prop]; } + const $scope = scope; var crmMosaicoIframe = null, activeDialogs = {}; $scope.mosaicoCtrl = { templates: [], // Fill a given "mailing" which the chosen "template". select: function(template) { - const mailing = scope.mailing; - var topt = mailing.template_options = mailing.template_options || {}; var promise = crmMosaicoTemplates.getFull(template).then(function(tplCtnt){ - topt.mosaicoTemplate = template.id; - topt.mosaicoMetadata = tplCtnt.metadata; - topt.mosaicoContent = tplCtnt.content; - mailing.body_html = tplCtnt.html; - // console.log('select', {isAr1: _.isArray(mailing.template_options), isAr2: _.isArray(topt), m: mailing, t: template}); + setProp('mosaicoTemplate', template.id); + setProp('mosaicoMetadata', tplCtnt.metadata); + setProp('mosaicoContent', tplCtnt.content); + setProp('body_html', tplCtnt.html); $scope.mosaicoCtrl.edit(); }); return crmStatus({start: ts('Loading...'), success: null}, promise); }, hasSelection: function() { - const mailing = scope.mailing; - return !!mailing.template_options.mosaicoTemplate; + return !!getProp('mosaicoTemplate'); }, hasMarkup: function() { - const mailing = scope.mailing; - return !!mailing.body_html; + return !!getProp('body_html'); }, // Figure out which "template" was previously used with a "mailing." getTemplate: function() { const mailing = scope.mailing; - if (!mailing || !mailing.template_options || !mailing.template_options.mosaicoTemplate) { + if (!mailing || !getProp('mosaicoTemplate')) { return null; } var matches = _.where($scope.mosaicoCtrl.templates, { - id: mailing.template_options.mosaicoTemplate + id: getProp('mosaicoTemplate') }); return matches.length > 0 ? matches[0] : null; }, // Reset all Mosaico data in a "mailing'. reset: function() { - const mailing = scope.mailing; if (crmMosaicoIframe) crmMosaicoIframe.destroy(); crmMosaicoIframe = null; - delete mailing.template_options.mosaicoTemplate; - delete mailing.template_options.mosaicoMetadata; - delete mailing.template_options.mosaicoContent; - mailing.body_html = ''; + deleteProp('mosaicoTemplate'); + deleteProp('mosaicoMetadata'); + deleteProp('mosaicoContent'); + setProp('body_html', ''); }, // Edit a mailing in Mosaico. edit: function() { - const mailing = scope.mailing; if (crmMosaicoIframe) { crmMosaicoIframe.show(); return; } function syncModel(viewModel) { - mailing.body_html = viewModel.exportHTML(); - mailing.template_options = mailing.template_options || {}; + setProp('body_html', viewModel.exportHTML()); // Mosaico exports JSON. Keep their original encoding... or else the loader throws an error. - mailing.template_options.mosaicoMetadata = viewModel.exportMetadata(); - mailing.template_options.mosaicoContent = viewModel.exportJSON(); + setProp('mosaicoMetadata', viewModel.exportMetadata()); + setProp('mosaicoContent', viewModel.exportJSON()); } crmMosaicoIframe = new CrmMosaicoIframe({ model: { template: $scope.mosaicoCtrl.getTemplate().path, - metadata: mailing.template_options.mosaicoMetadata, - content: mailing.template_options.mosaicoContent + metadata: getProp('mosaicoMetadata'), + content: getProp('mosaicoContent') }, actions: { sync: function(ko, viewModel) { @@ -105,7 +112,6 @@ syncModel(viewModel); var model = {mailing: $scope.mailing, attachments: $scope.attachments}; - console.log('test!', model); var options = CRM.utils.adjustDialogDefaults(angular.extend( {autoOpen: false, title: ts('Preview / Test'), width: 550}, options From ed7bf0f896ca2e18aa37d8bd7ff624af8e8c5283 Mon Sep 17 00:00:00 2001 From: Tim Otten Date: Thu, 1 Feb 2024 20:26:13 -0800 Subject: [PATCH 10/13] crmMosaicoBlockMailing - Fix guard on help message --- ang/crmMosaico/BlockMailing.html | 4 ++-- ang/crmMosaico/BlockMailing.js | 10 ++++++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/ang/crmMosaico/BlockMailing.html b/ang/crmMosaico/BlockMailing.html index a4c0c3ca4f..4fcc7bfa8e 100644 --- a/ang/crmMosaico/BlockMailing.html +++ b/ang/crmMosaico/BlockMailing.html @@ -65,8 +65,8 @@
-
- ({{ts('You have chosen two subject lines. We will organize a A/B testing to determine which is better.')}}) +
+ ({{ts('Define two options for the subject. We will use A/B testing to determine which is better.')}})
diff --git a/ang/crmMosaico/BlockMailing.js b/ang/crmMosaico/BlockMailing.js index 593c4132b9..80d884e139 100644 --- a/ang/crmMosaico/BlockMailing.js +++ b/ang/crmMosaico/BlockMailing.js @@ -1,5 +1,11 @@ (function(angular, $, _) { - angular.module('crmMosaico').directive('crmMosaicoBlockMailing', function(crmMailingSimpleDirective) { - return crmMailingSimpleDirective('crmMosaicoBlockMailing', '~/crmMosaico/BlockMailing.html'); + angular.module('crmMosaico').directive('crmMosaicoBlockMailing', function(crmMailingSimpleDirective, crmMosaicoVariants) { + const d = crmMailingSimpleDirective('crmMosaicoBlockMailing', '~/crmMosaico/BlockMailing.html'); + const link = d.link; + d.link = function(scope, elm, attr) { + link(scope, elm, attr); + scope.isMailingSplit = (mailing, field) => crmMosaicoVariants.isSplit(mailing, field); + }; + return d; }); })(angular, CRM.$, CRM._); From 9a8f5c33ea54506a791163e10bcbb0741f9a4cd2 Mon Sep 17 00:00:00 2001 From: Tim Otten Date: Thu, 1 Feb 2024 17:29:51 -0800 Subject: [PATCH 11/13] Update main layout to allow A/B testing for the "Design" --- .../EditMailingCtrl/bootstrap-single.html | 25 ++++++++++-- .../EditMailingCtrl/bootstrap-wizard.html | 38 ++++++++++++++++++- ang/crmMosaico/MixinCtrl.js | 25 +++++++++++- 3 files changed, 81 insertions(+), 7 deletions(-) diff --git a/ang/crmMosaico/EditMailingCtrl/bootstrap-single.html b/ang/crmMosaico/EditMailingCtrl/bootstrap-single.html index b757c85d87..efea9b3fd3 100644 --- a/ang/crmMosaico/EditMailingCtrl/bootstrap-single.html +++ b/ang/crmMosaico/EditMailingCtrl/bootstrap-single.html @@ -7,12 +7,29 @@
{{ts('Mailing')}}
-
- {{ts('Design')}} +
+
+ + +
+ + {{design.title}}
-
-
+
+
diff --git a/ang/crmMosaico/EditMailingCtrl/bootstrap-wizard.html b/ang/crmMosaico/EditMailingCtrl/bootstrap-wizard.html index 8edc051718..31baf381d0 100644 --- a/ang/crmMosaico/EditMailingCtrl/bootstrap-wizard.html +++ b/ang/crmMosaico/EditMailingCtrl/bootstrap-wizard.html @@ -7,7 +7,43 @@
-
+ +
+
+ +
+ +
+
+ +
+

({{ts('Define two options for the design. We will use A/B testing to determine which is better.')}})

+ +
+
+
+ +
+ + {{design.title}} +
+
+
+
+
+
+
+
diff --git a/ang/crmMosaico/MixinCtrl.js b/ang/crmMosaico/MixinCtrl.js index f26670cd5a..3ce61f24f0 100644 --- a/ang/crmMosaico/MixinCtrl.js +++ b/ang/crmMosaico/MixinCtrl.js @@ -2,8 +2,29 @@ // This provides additional actions for editing a Mosaico mailing. // It coexists with crmMailing's EditMailingCtrl. - angular.module('crmMosaico').controller('CrmMosaicoMixinCtrl', function CrmMosaicoMixinCtrl($scope, dialogService) { - // var ts = $scope.ts = CRM.ts(null); + angular.module('crmMosaico').controller('CrmMosaicoMixinCtrl', function CrmMosaicoMixinCtrl($scope, dialogService, crmMosaicoVariants) { + const ts = $scope.ts = CRM.ts('mosaico'); + + const singleDesign = [ + {title: ts('Design'), vid: null, action: 'split'} + ]; + const abDesign = [ + {title: ts('Design (A)'), vid: 0, action: 'unsplit'}, + {title: ts('Design (B)'), vid: 1, action: 'unsplit'}, + ]; + + $scope.getDesigns = function(mailing) { + if (!mailing) return []; + return crmMosaicoVariants.isSplit(mailing, 'body_html') ? abDesign : singleDesign; + } + $scope.unsplitDesign = function(mailing, vid) { + crmMosaicoVariants.remove(mailing, ['mosaicoTemplate', 'mosaicoMetadata', 'mosaicoContent', 'body_html'], vid); + }; + $scope.splitDesign = function(mailing) { + crmMosaicoVariants.split(mailing, ['mosaicoTemplate', 'mosaicoMetadata', 'mosaicoContent', 'body_html']); + }; + + $scope.isMailingSplit = (mailing, field) => crmMosaicoVariants.isSplit(mailing, field); var activeDialogs = {}; From d861d1264b24a781c619382cc8ded6e57342ab52 Mon Sep 17 00:00:00 2001 From: Tim Otten Date: Fri, 2 Feb 2024 14:54:04 -0800 Subject: [PATCH 12/13] Preview - When you have two designs (A/B), show the preview for the one you are editing --- ang/crmMosaico/BlockDesign.js | 2 +- ang/crmMosaico/PreviewDialogCtrl.html | 2 +- ang/crmMosaico/PreviewDialogCtrl.js | 9 +++++---- ang/crmMosaico/Variants.js | 14 ++++++++++++++ 4 files changed, 21 insertions(+), 6 deletions(-) diff --git a/ang/crmMosaico/BlockDesign.js b/ang/crmMosaico/BlockDesign.js index f41e9c21f2..de5c90d4b4 100644 --- a/ang/crmMosaico/BlockDesign.js +++ b/ang/crmMosaico/BlockDesign.js @@ -111,7 +111,7 @@ test: function(ko, viewModel) { syncModel(viewModel); - var model = {mailing: $scope.mailing, attachments: $scope.attachments}; + var model = {mailing: $scope.mailing, attachments: $scope.attachments, variantId: $scope.variantId}; var options = CRM.utils.adjustDialogDefaults(angular.extend( {autoOpen: false, title: ts('Preview / Test'), width: 550}, options diff --git a/ang/crmMosaico/PreviewDialogCtrl.html b/ang/crmMosaico/PreviewDialogCtrl.html index 8e8258f2ae..901016f935 100644 --- a/ang/crmMosaico/PreviewDialogCtrl.html +++ b/ang/crmMosaico/PreviewDialogCtrl.html @@ -1,5 +1,5 @@
-
+
diff --git a/ang/crmMosaico/PreviewDialogCtrl.js b/ang/crmMosaico/PreviewDialogCtrl.js index a42b2a74a4..2a363613e8 100644 --- a/ang/crmMosaico/PreviewDialogCtrl.js +++ b/ang/crmMosaico/PreviewDialogCtrl.js @@ -5,17 +5,18 @@ // - [input] "model": Object // - "mailing": Object, CiviMail mailing // - "attachments": Object, CrmAttachment - angular.module('crmMosaico').controller('CrmMosaicoPreviewDialogCtrl', function CrmMosaicoPreviewDialogCtrl($scope, crmMailingMgr, crmMailingPreviewMgr, crmBlocker, crmStatus) { + angular.module('crmMosaico').controller('CrmMosaicoPreviewDialogCtrl', function CrmMosaicoPreviewDialogCtrl($scope, crmMailingMgr, crmMailingPreviewMgr, crmBlocker, crmStatus, crmMosaicoVariants) { var ts = $scope.ts = CRM.ts(null); var block = $scope.block = crmBlocker(); // @return Promise - $scope.previewMailing = function previewMailing(mailing, mode) { - return crmMailingPreviewMgr.preview(mailing, mode); + $scope.previewMailing = function previewMailing(mailing, variantId, mode) { + const preview = crmMosaicoVariants.preview(mailing, variantId); + return crmMailingPreviewMgr.preview(preview, mode); }; // @return Promise - $scope.sendTest = function sendTest(mailing, attachments, recipient) { + $scope.sendTest = function sendTest(mailing, variantId, attachments, recipient) { var savePromise = crmMailingMgr.save(mailing) .then(function() { return attachments.save(); diff --git a/ang/crmMosaico/Variants.js b/ang/crmMosaico/Variants.js index f544f8bb48..f152458253 100644 --- a/ang/crmMosaico/Variants.js +++ b/ang/crmMosaico/Variants.js @@ -59,6 +59,20 @@ } }, + // Create a new, flat `mailing` record which includes overrides for a specific variant. + preview: function preview(mailing, vid) { + const preview = angular.copy(mailing, {}, 5); + if (vid !== null && vid !== undefined) { + angular.extend(preview, mailing.template_options.variants[vid]); + } + delete preview.id; + delete preview.template_options.variants; + delete preview.mosaicoTemplate; + delete preview.mosaicoMetadata; + delete preview.mosaicoContent; + return preview; + }, + isSplit: function isSplit(mailing, field) { return mailing.template_options && mailing.template_options.variants && (field in mailing.template_options.variants[0]); } From 43febe00ba0481c3d63e2329665d5842cf88689e Mon Sep 17 00:00:00 2001 From: Tim Otten Date: Fri, 2 Feb 2024 15:00:58 -0800 Subject: [PATCH 13/13] "Send test email" - Add notice about A/B testing Do you want or expect this button to send one message or two messages? It depends. * Heretofore, with subject-based A/B, the user intent was a bit ambiguous -- but the imperative was to send both. The user needs *some* way to send each A/B condition -- but there's only one button for sending. So that button sends both. * Now, with design-based A/B, you might reasonably infer user-intent. When editing "Design (A)", you send a test for "Design (A)". When editing "Design (B)", you send a test for "Design (B)". You can navigate back+forth to send whichever test you want. * But OTOH, the user now has a choice. They may do subject-based, design-based, or both. So: * If you always send the one being edited, then it's good for design-based. * If you always send both, then it's good for subject-based. * If you send one-or-both dynamically, then it's inconsistent. * IMHO, whether subject-based or design-based, it's still not bad to send both. When you go to read the messages in your email client, you can quickly compare the latest rendering of each. * The main thing is to be clear about what's happening. --- ang/crmMosaico/BlockPreview.html | 4 ++++ ang/crmMosaico/BlockPreview.js | 3 ++- ang/crmMosaico/Variants.js | 11 ++++++++++- 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/ang/crmMosaico/BlockPreview.html b/ang/crmMosaico/BlockPreview.html index b59f564c9c..456a29d6ae 100644 --- a/ang/crmMosaico/BlockPreview.html +++ b/ang/crmMosaico/BlockPreview.html @@ -44,3 +44,7 @@
+ +
+ {{ts('The draft mailing allows two variations (A/B).')}}
{{ts('If you send a test now, it will use all available variations.')}}
+
diff --git a/ang/crmMosaico/BlockPreview.js b/ang/crmMosaico/BlockPreview.js index 1d5c663c89..92dc4d3aa1 100644 --- a/ang/crmMosaico/BlockPreview.js +++ b/ang/crmMosaico/BlockPreview.js @@ -2,7 +2,7 @@ // example:
// note: the directive defines a variable called "preview" with any inputs supplied by the user (e.g. the target recipient for an example mailing) - angular.module('crmMosaico').directive('crmMosaicoBlockPreview', function(crmUiHelp) { + angular.module('crmMosaico').directive('crmMosaicoBlockPreview', function(crmUiHelp, crmMosaicoVariants) { return { templateUrl: '~/crmMosaico/BlockPreview.html', link: function(scope, elm, attr) { @@ -25,6 +25,7 @@ return ($.inArray(false, validityArr) == -1); }; + scope.isSplit = crmMosaicoVariants.isSplit; scope.doPreview = function(mode) { scope.$eval(attr.onPreview, { diff --git a/ang/crmMosaico/Variants.js b/ang/crmMosaico/Variants.js index f152458253..0fa3f79d4d 100644 --- a/ang/crmMosaico/Variants.js +++ b/ang/crmMosaico/Variants.js @@ -73,8 +73,17 @@ return preview; }, + // isSplit(mailing): Determine if there are -any- split/variant fields. + // isSplit(mailing, field): Determine if a -specific- field has variations. isSplit: function isSplit(mailing, field) { - return mailing.template_options && mailing.template_options.variants && (field in mailing.template_options.variants[0]); + if (!mailing.template_options || !mailing.template_options.variants) { + return false; + } + if (field) { + return (field in mailing.template_options.variants[0]); + } + const nonEmpties = _.filter(mailing.template_options.variants, (v) => !_.isEmpty(angularObj(v))); + return nonEmpties.length > 0; } }; return self;