Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add A/B testing for email design #633

Merged
merged 13 commits into from
Mar 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions ang/crmMosaico/BlockDesign.html
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
<div class="mosaico-templates-wrapper">
<div class="row">
<span ng-model="body_html_defined" crm-ui-validate="!!mailing.body_html"></span>
<span ng-model="body_html_defined" crm-ui-validate="mosaicoCtrl.hasMarkup()"></span>
<div class="col-xs-4 col-md-4 crm-mosaico-selected center-block"
crm-mosaico-template-item="{state:'selected', title: ts('My Design'), subtitle: mosaicoCtrl.getTemplate(mailing).type, img: mosaicoCtrl.getTemplate(mailing).thumbnail}"
on-item-click="mosaicoCtrl.edit(mailing)"
on-item-reset="mosaicoCtrl.reset(mailing)"
ng-show="!!mailing.template_options.mosaicoTemplate">
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()">
</div>
<div ng-if="!mailing.template_options.mosaicoTemplate && mosaicoCtrl.templates.length">
<div ng-if="!mosaicoCtrl.hasSelection() && mosaicoCtrl.templates.length">
<div class="form-inline">
<input class="form-control crm-mosaico-template-category" ng-model="mosaicoCtrl.selectedCategory" ng-change="mosaicoCtrl.categoryFilter = mosaicoCtrl.categoryFilters[mosaicoCtrl.selectedCategory].filter" crm-ui-select="{data: mosaicoCtrl.categoryFilters, placeholder: ts('All Categories')}">
</div>
<div ng-repeat="template in mosaicoCtrl.templates | filter:mosaicoCtrl.categoryFilter" class="mosaico-templates-list">
<div class="col-xs-6 col-md-4"
crm-mosaico-template-item="{state:'select', title: (template.base ? template.title : template.type), subtitle: template.category, img: template.thumbnail}"
on-item-click="mosaicoCtrl.select(mailing, template)">
on-item-click="mosaicoCtrl.select(template)">
</div>
</div>
</div>
Expand Down
140 changes: 135 additions & 5 deletions ang/crmMosaico/BlockDesign.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,151 @@
(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: '@',
crmMailing: '@'
// crmMosaicoCtrl: '@',
crmMailing: '@',
crmMailingAttachments: '@',
crmMailingVariant: '@',
},
templateUrl: '~/crmMosaico/BlockDesign.html',
link: function (scope, elm, attr) {
scope.$parent.$watch(attr.crmMailing, function(newValue){
scope.mailing = newValue;
});
scope.$parent.$watch(attr.crmMosaicoCtrl, function(newValue){
scope.mosaicoCtrl = newValue;
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) {
var promise = crmMosaicoTemplates.getFull(template).then(function(tplCtnt){
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() {
return !!getProp('mosaicoTemplate');
},
hasMarkup: function() {
return !!getProp('body_html');
},
// Figure out which "template" was previously used with a "mailing."
getTemplate: function() {
const mailing = scope.mailing;
if (!mailing || !getProp('mosaicoTemplate')) {
return null;
}
var matches = _.where($scope.mosaicoCtrl.templates, {
id: getProp('mosaicoTemplate')
});
return matches.length > 0 ? matches[0] : null;
},
// Reset all Mosaico data in a "mailing'.
reset: function() {
if (crmMosaicoIframe) crmMosaicoIframe.destroy();
crmMosaicoIframe = null;
deleteProp('mosaicoTemplate');
deleteProp('mosaicoMetadata');
deleteProp('mosaicoContent');
setProp('body_html', '');
},
// Edit a mailing in Mosaico.
edit: function() {
if (crmMosaicoIframe) {
crmMosaicoIframe.show();
return;
}

function syncModel(viewModel) {
setProp('body_html', viewModel.exportHTML());
// Mosaico exports JSON. Keep their original encoding... or else the loader throws an error.
setProp('mosaicoMetadata', viewModel.exportMetadata());
setProp('mosaicoContent', viewModel.exportJSON());
}

crmMosaicoIframe = new CrmMosaicoIframe({
model: {
template: $scope.mosaicoCtrl.getTemplate().path,
metadata: getProp('mosaicoMetadata'),
content: getProp('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, variantId: $scope.variantId};
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;
}
});


}
};
});
Expand Down
5 changes: 2 additions & 3 deletions ang/crmMosaico/BlockMailing.html
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,8 @@
</div>
</div>

<div class="form-group" ng-if="mailing.template_options.variants">
<label class="control-label">{{ts('Distribution')}}</label>
<crm-mosaico-distribution crm-mailing="mailing" />
<div class="form-group" ng-if="isMailingSplit(mailing, 'subject')">
<em>({{ts('Define two options for the subject. We will use A/B testing to determine which is better.')}})</em>
</div>

<span ng-controller="EditUnsubGroupCtrl">
Expand Down
10 changes: 8 additions & 2 deletions ang/crmMosaico/BlockMailing.js
Original file line number Diff line number Diff line change
@@ -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._);
4 changes: 4 additions & 0 deletions ang/crmMosaico/BlockPreview.html
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,7 @@
<button type="button" class="btn btn-sm btn-primary" title="{{ crmMailing.$invalid || !testGroup.gid ? ts('Complete all required-mark fields first') : ts('Send test message to group') }}" ng-disabled="crmMailing.$invalid || !testGroup.gid" crm-confirm="{resizable: true, width: '40%', height: '40%', open: previewTestGroup}"
on-yes="doSend({gid: testGroup.gid})">{{:: ts('Send test') }}</button>
</div>

<div class="form-group" ng-if="isSplit(mailing)">
<em>{{ts('The draft mailing allows two variations (A/B).')}}<br/>{{ts('If you send a test now, it will use all available variations.')}}</em>
</div>
3 changes: 2 additions & 1 deletion ang/crmMosaico/BlockPreview.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// example: <div crm-mailing-block-preview crm-mailing="myMailing" on-preview="openPreview(myMailing, preview.mode)" on-send="sendEmail(myMailing,preview.recipient)">
// 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) {
Expand All @@ -25,6 +25,7 @@

return ($.inArray(false, validityArr) == -1);
};
scope.isSplit = crmMosaicoVariants.isSplit;

scope.doPreview = function(mode) {
scope.$eval(attr.onPreview, {
Expand Down
6 changes: 6 additions & 0 deletions ang/crmMosaico/BlockSchedule.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
<div class="crmMosaico-schedule-outer" crm-mailing-radio-date="schedule" ng-model="mailing.scheduled_date">

<div class="form-group" ng-if="!!mailing.template_options.variants">
<label class="control-label">{{ts('Testing')}}</label>
<p><em>{{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.')}}</em></p>
<crm-mosaico-distribution crm-mailing="mailing" />
</div>

<label class="crmMosaico-schedule-title">{{ts('Schedule email to:')}}</label>

<div class="crmMosaico-schedule-inner form-inline form-inline-tabs" ng-init="selectedTab = 'sendNow'">
Expand Down
25 changes: 21 additions & 4 deletions ang/crmMosaico/EditMailingCtrl/bootstrap-single.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,29 @@
<div class="panel-heading">{{ts('Mailing')}}</div>
<div class="panel-body" crm-mosaico-block-mailing crm-mailing="mailing"></div>

<div class="panel-heading">
<span class="required-mark">{{ts('Design')}}</span>
<div class="panel-heading" ng-repeat-start="design in getDesigns(mailing)">
<div style="float: right" ng-if="!!design.action">
<button ng-if="design.action === 'split'"
class="btn btn-primary-outline btn-xs"
crm-confirm="{message: ts('With A/B testing, you can design two alternative versions of the mailing.')}"
on-yes="splitDesign(mailing)">
<span class="fa fa-copy" aria-hidden="true"></span>
{{ts('A/B Test')}}
</button>
<button ng-if="design.action === 'unsplit'"
class="btn btn-primary-outline btn-xs"
crm-confirm="{message: ts('Are you sure you want to delete %1? The other design will become primary.', {1: design.title})}"
on-yes="unsplitDesign(mailing, design.vid)">
<span class="fa fa-trash" aria-hidden="true"></span>
{{ts('Delete')}}
</button>
</div>

<span class="required-mark">{{design.title}}</span>
<div style="clear: both"></div>
</div>
<div class="panel-body">
<div crm-mosaico-block-design crm-mailing="mailing" crm-mosaico-ctrl="mosaicoCtrl"></div>
<div class="panel-body" ng-repeat-end>
<div crm-mosaico-block-design crm-mailing="mailing" crm-mailing-attachments="attachments" crm-mailing-variant="design.vid" on-save="save()"></div>
</div>

<div class="panel-heading">
Expand Down
68 changes: 52 additions & 16 deletions ang/crmMosaico/EditMailingCtrl/bootstrap-wizard.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,43 @@
</div>

<div crmb-wizard-step crm-title="ts('Design')" ng-form="designForm">
<div crm-mosaico-block-design crm-mailing="mailing" crm-mosaico-ctrl="mosaicoCtrl"></div>

<div ng-if="!isMailingSplit(mailing, 'body_html')">
<div crm-mosaico-block-design crm-mailing="mailing" crm-mailing-variant="null" crm-mailing-attachments="attachments" on-save="save()"></div>

<div class="text-center">
<button class="btn btn-primary-outline btn-xs"
crm-confirm="{message: ts('With A/B testing, you can design two alternative versions of the mailing.')}"
on-yes="splitDesign(mailing)">
<span class="fa fa-copy" aria-hidden="true"></span>
{{ts('Add A/B Test')}}
</button>
</div>
</div>

<div ng-if="isMailingSplit(mailing, 'body_html')">
<p class="text-center"><em>({{ts('Define two options for the design. We will use A/B testing to determine which is better.')}})</em></p>

<div class="panel panel-default" ng-repeat="design in getDesigns(mailing)">
<div class="panel-heading" >
<div style="float: right">
<button class="btn btn-primary-outline btn-xs"
crm-confirm="{message: ts('Are you sure you want to delete %1? The other design will become primary.', {1: design.title})}"
on-yes="unsplitDesign(mailing, design.vid)">
<span class="fa fa-trash" aria-hidden="true"></span>
{{ts('Delete')}}
</button>
</div>

<span class="required-mark">{{design.title}}</span>
<div style="clear: both"></div>
</div>
<div class="panel-body">
<div crm-mosaico-block-design crm-mailing="mailing" crm-mailing-attachments="attachments" crm-mailing-variant="design.vid" on-save="save()"></div>
</div>
</div>
</div>

</div>

<div crmb-wizard-step crm-title="ts('Options')" ng-form="optionsForm">
Expand All @@ -21,30 +57,30 @@
</div>

<button class="btn btn-secondary-outline" crmb-wizard-button-position="left" ng-click="crmbWizardCtrl.previous()" ng-show="!crmbWizardCtrl.$first()">
<span class="btn-icon"><i class="fa fa-chevron-left"></i></span>
{{ts('Back')}}
</button>
<span class="btn-icon"><i class="fa fa-chevron-left"></i></span>
{{ts('Back')}}
</button>

<button class="btn btn-danger-outline" crmb-wizard-button-position="left" ng-show="checkPerm('delete in CiviMail') && crmbWizardCtrl.$first()" ng-disabled="block.check()" crm-confirm="{title:ts('Delete Draft'), message:ts('Are you sure you want to permanently delete this mailing?')}"
on-yes="delete()">
<span class="btn-icon"><i class="fa fa-trash"></i></span>
{{ts('Delete Draft')}}
</button>
<span class="btn-icon"><i class="fa fa-trash"></i></span>
{{ts('Delete Draft')}}
</button>

<button class="btn btn-secondary-outline" crmb-wizard-button-position="right" ng-disabled="block.check()" ng-click="save().then(leave)">
<span class="btn-icon"><i class="fa fa-floppy-o"></i></span>
{{ts('Save Draft')}}
</button>
<span class="btn-icon"><i class="fa fa-floppy-o"></i></span>
{{ts('Save Draft')}}
</button>

<button class="btn btn-primary" crmb-wizard-button-position="right" title="{{!crmbWizardCtrl.$validStep() ? ts('Complete all required-mark fields first') : ts('Next step')}}" ng-click="crmbWizardCtrl.next()" ng-show="!crmbWizardCtrl.$last()" ng-disabled="!crmbWizardCtrl.$validStep()">
<span class="btn-icon"><i class="fa fa-chevron-right"></i></span>
{{ts('Continue')}}
</button>
<span class="btn-icon"><i class="fa fa-chevron-right"></i></span>
{{ts('Continue')}}
</button>

<button class="btn btn-primary" crmb-wizard-button-position="right" ng-show="crmbWizardCtrl.$last()" ng-disabled="block.check() || !crmbWizardCtrl.$validStep()" ng-click="submit()">
<span class="btn-icon"><i class="fa fa-send"></i></span>
{{ts('Submit Mailing')}}
</button>
<span class="btn-icon"><i class="fa fa-send"></i></span>
{{ts('Submit Mailing')}}
</button>

</div>

Expand Down
Loading
Loading