First commit

This commit is contained in:
Theodotos Andreou 2018-01-14 13:10:16 +00:00
commit c6e2478c40
13918 changed files with 2303184 additions and 0 deletions

View file

@ -0,0 +1,14 @@
<div class="crm-block" ng-form="apprForm" crm-ui-id-scope>
<div class="crm-group">
<div crm-ui-field="{title: ts('Status')}">
{{mailingFields.approval_status_id.optionsMap[mailing.approval_status_id] || ts('Unreviewed')}}
</div>
<div crm-ui-field="{name: 'apprForm.approval_note', title: ts('Note')}">
<textarea
crm-ui-id="apprForm.approval_note"
name="approval_note"
ng-model="mailing.approval_note"
></textarea>
</div>
</div>
</div>

View file

@ -0,0 +1,5 @@
(function(angular, $, _) {
angular.module('crmMailing').directive('crmMailingBlockApprove', function(crmMailingSimpleDirective) {
return crmMailingSimpleDirective('crmMailingBlockApprove', '~/crmMailing/BlockApprove.html');
});
})(angular, CRM.$, CRM._);

View file

@ -0,0 +1,32 @@
<!--
Controller: EditMailingCtrl
Required vars: mailing, crmMailingConst
-->
<div class="crm-block" ng-form="subform" crm-ui-id-scope>
<div class="crm-group" ng-controller="EmailBodyCtrl">
<div crm-ui-field="{name: 'subform.header_id', title: ts('Mailing Header'), help: hs('header')}">
<select
crm-ui-id="subform.header_id"
name="header_id"
ui-jq="select2"
ui-options="{dropdownAutoWidth : true, allowClear: true}"
ng-change="checkTokens(mailing, '*')"
ng-model="mailing.header_id"
ng-options="mc.id as mc.name for mc in crmMailingConst.headerfooterList | filter:{component_type: 'Header'} | orderBy:'name'">
<option value=""></option>
</select>
</div>
<div crm-ui-field="{name: 'subform.footer_id', title: ts('Mailing Footer'), help: hs('footer')}">
<select
crm-ui-id="subform.footer_id"
name="footer_id"
ui-jq="select2"
ui-options="{dropdownAutoWidth : true, allowClear: true}"
ng-change="checkTokens(mailing, '*')"
ng-model="mailing.footer_id"
ng-options="mc.id as mc.name for mc in crmMailingConst.headerfooterList | filter:{component_type: 'Footer'} | orderBy:'name'">
<option value=""></option>
</select>
</div>
</div>
</div>

View file

@ -0,0 +1,5 @@
(function(angular, $, _) {
angular.module('crmMailing').directive('crmMailingBlockHeaderFooter', function(crmMailingSimpleDirective) {
return crmMailingSimpleDirective('crmMailingBlockHeaderFooter', '~/crmMailing/BlockHeaderFooter.html');
});
})(angular, CRM.$, CRM._);

View file

@ -0,0 +1,97 @@
<!--
Controller: EditMailingCtrl
Required vars: mailing, crmMailingConst
Note: Much of this file is duplicated in crmMailing and crmMailingAB with variations on placement/title/binding.
It could perhaps be thinned by 30-60% by making more directives.
-->
<div class="crm-block" ng-form="subform" crm-ui-id-scope>
<div class="crm-group">
<div crm-ui-field="{name: 'subform.msg_template_id', title: ts('Template')}">
<div ng-controller="MsgTemplateCtrl">
<select
crm-ui-id="subform.msg_template_id"
name="msg_template_id"
class="fa-clipboard"
crm-ui-select="{dropdownAutoWidth : true, allowClear: true, placeholder: ts('Message Template')}"
ng-model="mailing.msg_template_id"
ng-change="loadTemplate(mailing, mailing.msg_template_id)"
>
<option value=""></option>
<option ng-repeat="frm in crmMsgTemplates.getAll() | orderBy:'msg_title'" ng-value="frm.id">{{frm.msg_title}}</option>
</select>
<a crm-icon="fa-floppy-o" ng-if="checkPerm('edit message templates')" ng-click="saveTemplate(mailing)" class="crm-hover-button" title="{{ts('Save As')}}"></a>
</div>
</div>
<div crm-ui-field="{name: 'subform.fromAddress', title: ts('From'), help: hs('from_email')}">
<div ng-controller="EmailAddrCtrl" crm-mailing-from-address="fromPlaceholder" crm-mailing="mailing">
<select
crm-ui-id="subform.fromAddress"
crm-ui-select="{dropdownAutoWidth : true, allowClear: false, placeholder: ts('Email address')}"
name="fromAddress"
ng-model="fromPlaceholder.label"
required>
<option value=""></option>
<option ng-repeat="frm in crmFromAddresses.getAll() | filter:{is_active:1} | orderBy:'weight'" value="{{frm.label}}">{{frm.label}}</option>
</select>
</div>
</div>
<div crm-ui-field="{name: 'subform.replyTo', title: ts('Reply-To')}" ng-show="crmMailingConst.enableReplyTo">
<div ng-controller="EmailAddrCtrl">
<select
crm-ui-id="subform.replyTo"
crm-ui-select="{dropdownAutoWidth : true, allowClear: true, placeholder: ts('Email address')}"
name="replyTo"
ng-change="checkReplyToChange(mailing)"
ng-model="mailing.replyto_email"
>
<option value=""></option>
<option ng-repeat="frm in crmFromAddresses.getAll() | filter:{is_active:1} | orderBy:'weight'" value="{{frm.label}}">{{frm.label}}</option>
</select>
</div>
</div>
<div crm-ui-field="{name: 'subform.recipients', title: ts('Recipients'), required: true}">
<div crm-mailing-block-recipients="{name: 'recipients', id: 'subform.recipients'}" crm-mailing="mailing" cm-ui-id="subform.recipients"></div>
</div>
<span ng-controller="EditUnsubGroupCtrl">
<div crm-ui-field="{name: 'subform.baseGroup', title: ts('Unsubscribe Group')}" ng-if="isUnsubGroupRequired(mailing)">
<select
crm-ui-id="subform.baseGroup"
crm-ui-select
name="baseGroup"
ng-model="mailing.recipients.groups.base[0]"
ng-required="true"
>
<option ng-repeat="grp in crmMailingConst.testGroupNames | filter:{is_hidden:0} | orderBy:'title'" value="{{grp.id}}">{{grp.title}}</option>
</select>
</div>
</span>
<div crm-ui-field="{name: 'subform.subject', title: ts('Subject')}">
<div style="float: right;">
<input crm-mailing-token on-select="$broadcast('insert:subject', token.name)" tabindex="-1"/>
</div>
<input
crm-ui-id="subform.subject"
crm-ui-insert-rx="insert:subject"
type="text"
class="crm-form-text"
ng-model="mailing.subject"
required
placeholder="Subject"
name="subject" />
</div>
<div ng-if="crmMailingConst.isMultiLingual">
<div crm-ui-field="{name: 'subform.language', title: ts('Language')}">
<select
crm-ui-id="subform.language"
crm-ui-select="{dropdownAutoWidth : true, allowClear: false, placeholder: ts('- choose language -')}"
name="language"
ng-model="mailing.language"
required
>
<option value=""></option>
<option ng-repeat="(key,val) in crmMailingConst.enabledLanguages" value="{{key}}">{{val}}</option>
</select>
</div>
</div>
</div>
</div>

View file

@ -0,0 +1,5 @@
(function(angular, $, _) {
angular.module('crmMailing').directive('crmMailingBlockMailing', function(crmMailingSimpleDirective) {
return crmMailingSimpleDirective('crmMailingBlockMailing', '~/crmMailing/BlockMailing.html');
});
})(angular, CRM.$, CRM._);

View file

@ -0,0 +1,61 @@
<!--
Vars: mailing:obj, testContact:obj, testGroup:obj, crmMailing:FormController
-->
<div class="crmMailing-preview">
<!-- Note:
In Firefox (at least), clicking the preview buttons causes the browser to display validation warnings
for unrelated fields *and* display preview. To avoid this weird UX, we disable preview buttons when the form is incomplete/invalid.
-->
<div class="preview-popup">
<div ng-show="!mailing.body_html && !mailing.body_text">
<em>({{ts('No content to preview')}})</em>
</div>
<div ng-hide="!mailing.body_html">
<a class="crm-hover-button action-item" crm-icon="fa-television" ng-disabled="crmMailing.$invalid" ng-click="doPreview('html')">{{ts('Preview as HTML')}}</a>
</div>
<div ng-hide="!mailing.body_html && !mailing.body_text" style="margin-top: 1em;">
<a class="crm-hover-button action-item" crm-icon="fa-file-text-o" ng-disabled="crmMailing.$invalid" ng-click="doPreview('text')">{{ts('Preview as Plain Text')}}</a>
</div>
<!--
<div ng-hide="!mailing.body_html && !mailing.body_text">
<button ng-disabled="crmMailing.$invalid" ng-click="doPreview('full')">{{ts('Preview')}}</button>
</div>
-->
</div>
<div class="preview-contact" ng-form="">
<div>
{{ts('Send test email to:')}}
<a crm-ui-help="hs({id: 'test', title: ts('Test Email')})"></a>
</div>
<div>
<input
name="preview_test_email"
type="email"
class="crm-form-text"
ng-model="testContact.email"
placeholder="example@example.org"
/>
</div>
<button crm-icon="fa-paper-plane" title="{{crmMailing.$invalid || !testContact.email ? ts('Complete all required fields first') : ts('Send test message to %1', {1: testContact.email})}}" ng-disabled="crmMailing.$invalid || !testContact.email" ng-click="doSend({email: testContact.email})">{{ts('Send test')}}</button>
</div>
<div class="preview-group" ng-form="">
<div>
{{ts('Send test email to group:')}}
<a crm-ui-help="hs({id: 'test', title: ts('Test Email')})"></a>
</div>
<div>
<select
name="preview_test_group"
ui-jq="crmSelect2"
ui-options="{dropdownAutoWidth : true, allowClear: true, placeholder: ts('Select Group')}"
ng-model="testGroup.gid"
ng-options="group.id as group.title for group in crmMailingConst.testGroupNames|orderBy:'title'"
class="crm-action-menu fa-envelope-o"
>
<option value=""></option>
</select>
</div>
<button crm-icon="fa-paper-plane" title="{{crmMailing.$invalid || !testGroup.gid ? ts('Complete all required 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="clear"></div>
</div>

View file

@ -0,0 +1,65 @@
(function(angular, $, _) {
// 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('crmMailing').directive('crmMailingBlockPreview', function(crmUiHelp) {
return {
templateUrl: '~/crmMailing/BlockPreview.html',
link: function(scope, elm, attr) {
scope.$watch(attr.crmMailing, function(newValue) {
scope.mailing = newValue;
});
scope.crmMailingConst = CRM.crmMailing;
scope.ts = CRM.ts(null);
scope.hs = crmUiHelp({file: 'CRM/Mailing/MailingUI'});
scope.testContact = {email: CRM.crmMailing.defaultTestEmail};
scope.testGroup = {gid: null};
scope.doPreview = function(mode) {
scope.$eval(attr.onPreview, {
preview: {mode: mode}
});
};
scope.doSend = function doSend(recipient) {
scope.$eval(attr.onSend, {
preview: {recipient: recipient}
});
};
scope.previewTestGroup = function(e) {
var $dialog = $(this);
$dialog.html('<div class="crm-loading-element"></div>').parent().find('button[data-op=yes]').prop('disabled', true);
$dialog.dialog('option', 'title', ts('Send to %1', {1: _.pluck(_.where(scope.crmMailingConst.testGroupNames, {id: scope.testGroup.gid}), 'title')[0]}));
CRM.api3('contact', 'get', {
group: scope.testGroup.gid,
options: {limit: 0},
return: 'display_name,email'
}).done(function(data) {
var count = 0,
// Fixme: should this be in a template?
markup = '<ol>';
_.each(data.values, function(row) {
// Fixme: contact api doesn't seem capable of filtering out contacts with no email, so we're doing it client-side
if (row.email) {
count++;
markup += '<li>' + row.display_name + ' - ' + row.email + '</li>';
}
});
markup += '</ol>';
markup = '<h4>' + ts('A test message will be sent to %1 people:', {1: count}) + '</h4>' + markup;
if (!count) {
markup = '<div class="messages status"><i class="crm-i fa-exclamation-triangle"></i> ' +
(data.count ? ts('None of the contacts in this group have an email address.') : ts('Group is empty.')) +
'</div>';
}
$dialog
.html(markup)
.trigger('crmLoad')
.parent().find('button[data-op=yes]').prop('disabled', !count);
});
};
}
};
});
})(angular, CRM.$, CRM._);

View file

@ -0,0 +1,16 @@
<div class="crm-block" ng-form="subform" crm-ui-id-scope>
<div class="crm-group">
<div crm-ui-field="{name: 'subform.visibility', title: ts('Mailing Visibility'), help: hs('visibility')}">
<select
crm-ui-id="subform.visibility"
name="visibility"
ui-jq="select2"
ui-options="{dropdownAutoWidth : true}"
ng-model="mailing.visibility"
ng-options="v.key as v.value for v in crmMailingConst.visibility"
required
>
</select>
</div>
</div>
</div>

View file

@ -0,0 +1,5 @@
(function(angular, $, _) {
angular.module('crmMailing').directive('crmMailingBlockPublication', function (crmMailingSimpleDirective) {
return crmMailingSimpleDirective('crmMailingBlockPublication', '~/crmMailing/BlockPublication.html');
});
})(angular, CRM.$, CRM._);

View file

@ -0,0 +1,16 @@
<div ng-controller="EditRecipCtrl" class="crm-mailing-recipients-row">
<div style="float: right;">
<div class="crmMailing-recip-est">
<a href="" ng-click="previewRecipients()" title="{{ts('Preview a List of Recipients')}}">{{getRecipientsEstimate()}}</a>
</div>
</div>
<input
type="hidden"
crm-mailing-recipients
ng-model="mailing.recipients"
crm-mandatory-groups="crmMailingConst.groupNames | filter:{is_hidden:1}"
crm-ui-id="{{crmMailingBlockRecipients.id}}"
name="{{crmMailingBlockRecipients.name}}"
ng-required="true"/>
<a crm-icon="fa-wrench" ng-click="editOptions(mailing)" class="crm-hover-button" title="{{ts('Edit Recipient Options')}}"></a>
</div>

View file

@ -0,0 +1,5 @@
(function(angular, $, _) {
angular.module('crmMailing').directive('crmMailingBlockRecipients', function(crmMailingSimpleDirective) {
return crmMailingSimpleDirective('crmMailingBlockRecipients', '~/crmMailing/BlockRecipients.html');
});
})(angular, CRM.$, CRM._);

View file

@ -0,0 +1,82 @@
<!--
Controller: EditMailingCtrl
Required vars: mailing, crmMailingConst
-->
<div class="crm-block" ng-form="responseForm" crm-ui-id-scope>
<div class="crm-group">
<div crm-ui-field="{title: ts('Track Replies'), help: hs('override_verp')}" crm-layout="checkbox">
<!-- Comparing data-model and UI of "override_verp", note that true/false are inverted (enabled==0,disabled==1) -->
<span ng-controller="EmailAddrCtrl">
<input
name="override_verp"
type="checkbox"
ng-change="checkVerpChange(mailing)"
ng-model="mailing.override_verp"
ng-true-value="'0'"
ng-false-value="'1'"
/>
</span>
</div>
<div crm-ui-field="{title: ts('Forward Replies'), help: hs('forward_replies')}" crm-layout="checkbox" ng-show="'0' == mailing.override_verp">
<input name="forward_replies" type="checkbox" ng-model="mailing.forward_replies" ng-true-value="'1'" ng-false-value="'0'" />
</div>
<div crm-ui-field="{title: ts('Auto-Respond to Replies'), help: hs('auto_responder')}" crm-layout="checkbox" ng-show="'0' == mailing.override_verp">
<input name="auto_responder" type="checkbox" ng-model="mailing.auto_responder" ng-true-value="'1'" ng-false-value="'0'" />
</div>
</div>
</div>
<hr/>
<div class="crm-block" ng-form="subform" crm-ui-id-scope>
<div class="crm-group">
<div crm-ui-field="{name: 'subform.reply_id', title: ts('Auto-Respond Message')}" ng-show="'0' == mailing.override_verp && '1' == mailing.auto_responder">
<select
crm-ui-id="subform.reply_id"
name="reply_id"
ui-jq="select2"
ui-options="{dropdownAutoWidth : true}"
ng-model="mailing.reply_id"
ng-options="mc.id as mc.name for mc in crmMailingConst.headerfooterList | filter:{component_type: 'Reply'}"
required>
<option value=""></option>
</select>
</div>
<div crm-ui-field="{name: 'subform.optout_id', title: ts('Opt-out Message')}">
<select
crm-ui-id="subform.optout_id"
name="optout_id"
ui-jq="select2"
ui-options="{dropdownAutoWidth : true}"
ng-model="mailing.optout_id"
ng-options="mc.id as mc.name for mc in crmMailingConst.headerfooterList | filter:{component_type: 'OptOut'}"
required>
<option value=""></option>
</select>
</div>
<div crm-ui-field="{name: 'subform.resubscribe_id', title: ts('Resubscribe Message')}">
<select
crm-ui-id="subform.resubscribe_id"
name="resubscribe_id"
ui-jq="select2"
ui-options="{dropdownAutoWidth : true}"
ng-model="mailing.resubscribe_id"
ng-options="mc.id as mc.name for mc in crmMailingConst.headerfooterList | filter:{component_type: 'Resubscribe'}"
required>
<option value=""></option>
</select>
</div>
<div crm-ui-field="{name: 'subform.unsubscribe_id', title: ts('Unsubscribe Message')}">
<select
crm-ui-id="subform.unsubscribe_id"
name="unsubscribe_id"
ui-jq="select2"
ui-options="{dropdownAutoWidth : true}"
ng-model="mailing.unsubscribe_id"
ng-options="mc.id as mc.name for mc in crmMailingConst.headerfooterList | filter:{component_type: 'Unsubscribe'}"
required>
<option value=""></option>
</select>
</div>
</div>
</div>

View file

@ -0,0 +1,5 @@
(function(angular, $, _) {
angular.module('crmMailing').directive('crmMailingBlockResponses', function(crmMailingSimpleDirective) {
return crmMailingSimpleDirective('crmMailingBlockResponses', '~/crmMailing/BlockResponses.html');
});
})(angular, CRM.$, CRM._);

View file

@ -0,0 +1,61 @@
<!--
Controller: EditMailingCtrl
Required vars: mailing, attachments
-->
<div>
<div class="crm-block" ng-form="reviewForm" crm-ui-id-scope>
<div class="crm-group">
<div crm-ui-field="{title: ts('Mailing Name')}">
{{mailing.name}}
</div>
<div crm-ui-field="{title: ts('Recipients')}">
<div ng-controller="ViewRecipCtrl">
<div ng-controller="EditRecipCtrl">
<div><a crm-icon="fa-users" class="crm-hover-button action-item" ng-click="previewRecipients()">{{getRecipientsEstimate()}}</a></div>
<div ng-show="getIncludesAsString(mailing)">
(<strong>{{ts('Include:')}}</strong> {{getIncludesAsString(mailing)}})
</div>
<div ng-show="getExcludesAsString(mailing)">
(<strong>{{ts('Exclude:')}}</strong> <s>{{getExcludesAsString(mailing)}}</s>)
</div>
</div>
</div>
</div>
<div crm-ui-field="{title: ts('Content')}">
<span ng-show="mailing.body_html"><a crm-icon="fa-television" class="crm-hover-button action-item" ng-click="previewMailing(mailing, 'html')">{{ts('HTML')}}</a></span>
<span ng-show="mailing.body_html || mailing.body_text"><a crm-icon="fa-file-text-o" class="crm-hover-button action-item" ng-click="previewMailing(mailing, 'text')">{{ts('Plain Text')}}</a></span>
</div>
<div crm-ui-field="{title: ts('Attachments')}">
<div ng-repeat="file in attachments.files">
<a ng-href="{{file.url}}" target="_blank">{{file.name}}</a>
</div>
<div ng-repeat="item in attachments.uploader.queue">
{{item.file.name}}
</div>
<div ng-show="!attachments.files.length && !attachments.uploader.queue.length"><em>{{ts('None')}}</em></div>
</div>
<div ng-if="crmMailingConst.isMultiLingual" crm-ui-field="{title: ts('Language')}">
{{crmMailingConst.enabledLanguages[mailing.language]}}
</div>
<div crm-ui-field="{title: ts('Tracking')}">
<span crm-mailing-review-bool crm-on="mailing.url_tracking=='1'" crm-title="ts('Click-Throughs')"></span>
<span crm-mailing-review-bool crm-on="mailing.open_tracking=='1'" crm-title="ts('Opens')"></span>
</div>
<div crm-ui-field="{title: ts('Responding')}">
<div>
<span crm-mailing-review-bool crm-on="mailing.override_verp=='0'" crm-title="ts('Track Replies')"></span>
<span crm-mailing-review-bool crm-on="mailing.override_verp=='0' && mailing.forward_replies=='1'" crm-title="ts('Forward Replies')"></span>
</div>
<div ng-controller="PreviewComponentCtrl">
<span ng-show="mailing.override_verp == '0' && mailing.auto_responder"><a crm-icon="fa-envelope" class="crm-hover-button action-item" ng-click="previewComponent(ts('Auto-Respond'), mailing.reply_id)">{{ts('Auto-Respond')}}</a></span>
<span><a crm-icon="fa-envelope" class="crm-hover-button action-item" ng-click="previewComponent(ts('Opt-out'), mailing.optout_id)">{{ts('Opt-out')}}</a></span>
<span><a crm-icon="fa-envelope" class="crm-hover-button action-item" ng-click="previewComponent(ts('Resubscribe'), mailing.resubscribe_id)">{{ts('Resubscribe')}}</a></span>
<span><a crm-icon="fa-envelope" class="crm-hover-button action-item" ng-click="previewComponent(ts('Unsubscribe'), mailing.unsubscribe_id)">{{ts('Unsubscribe')}}</a></span>
</div>
</div>
<div crm-ui-field="{title: ts('Publication')}">
{{mailing.visibility}}
</div>
</div>
</div>
</div>

View file

@ -0,0 +1,26 @@
(function(angular, $, _) {
angular.module('crmMailing').directive('crmMailingBlockReview', function (crmMailingPreviewMgr) {
return {
scope: {
crmMailing: '@',
crmMailingAttachments: '@'
},
templateUrl: '~/crmMailing/BlockReview.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.crmMailingConst = CRM.crmMailing;
scope.ts = CRM.ts(null);
scope.previewMailing = function previewMailing(mailing, mode) {
return crmMailingPreviewMgr.preview(mailing, mode);
};
}
};
});
})(angular, CRM.$, CRM._);

View file

@ -0,0 +1,13 @@
<div class="crmMailing-schedule-outer" crm-mailing-radio-date="schedule" ng-model="mailing.scheduled_date">
<div class="crmMailing-schedule-inner">
<div>
<input ng-model="schedule.mode" type="radio" name="send" value="now" id="schedule-send-now"/>
<label for="schedule-send-now">{{ts('Send immediately')}}</label>
</div>
<div>
<input ng-model="schedule.mode" type="radio" name="send" value="at" id="schedule-send-at"/>
<label for="schedule-send-at">{{ts('Send at:')}}</label>
<input crm-ui-datepicker ng-model="schedule.datetime" ng-required="schedule.mode == 'at'"/>
</div>
</div>
</div>

View file

@ -0,0 +1,5 @@
(function(angular, $, _) {
angular.module('crmMailing').directive('crmMailingBlockSchedule', function(crmMailingSimpleDirective) {
return crmMailingSimpleDirective('crmMailingBlockSchedule', '~/crmMailing/BlockSchedule.html');
});
})(angular, CRM.$, CRM._);

View file

@ -0,0 +1,29 @@
<!--
Controller: EditMailingCtrl
Required vars: mailing, crmMailingConst
FIXME: Don't hardcode table-based layout!
-->
<div class="crm-block" ng-form="subform" crm-ui-id-scope>
<div class="crm-group">
<div crm-ui-field="{name: 'subform.mailingName', title: ts('Mailing Name'), help: hs('name')}">
<div>
<input
crm-ui-id="subform.mailingName"
type="text"
class="crm-form-text"
ng-model="mailing.name"
placeholder="Mailing Name"
required
name="mailingName" />
</div>
</div>
<div crm-ui-field="{name: 'subform.campaign', title: ts('Campaign'), help: hs({id: 'id-campaign_id', file: 'CRM/Campaign/Form/addCampaignToComponent'})}" ng-show="crmMailingConst.campaignEnabled">
<input
crm-entityref="{entity: 'Campaign', select: {allowClear: true, placeholder: ts('Select Campaign')}}"
crm-ui-id="subform.campaign"
name="campaign"
ng-model="mailing.campaign_id"
/>
</div>
</div>
</div>

View file

@ -0,0 +1,5 @@
(function(angular, $, _) {
angular.module('crmMailing').directive('crmMailingBlockSummary', function(crmMailingSimpleDirective) {
return crmMailingSimpleDirective('crmMailingBlockSummary', '~/crmMailing/BlockSummary.html');
});
})(angular, CRM.$, CRM._);

View file

@ -0,0 +1,14 @@
<!--
Controller: EditMailingCtrl
Required vars: mailing
-->
<div class="crm-block" ng-form="subform" crm-ui-id-scope>
<div class="crm-group">
<div crm-ui-field="{name: 'subform.url_tracking', title: ts('Track Click-Throughs'), help: hs('url_tracking')}" crm-layout="checkbox">
<input crm-ui-id="subform.url_tracking" name="url_tracking" type="checkbox" ng-model="mailing.url_tracking" ng-true-value="'1'" ng-false-value="'0'" />
</div>
<div crm-ui-field="{name: 'subform.open_tracking', title: ts('Track Opens'), help: hs('open_tracking')}" crm-layout="checkbox">
<input crm-ui-id="subform.open_tracking" name="open_tracking" type="checkbox" ng-model="mailing.open_tracking" ng-true-value="'1'" ng-false-value="'0'" />
</div>
</div>
</div>

View file

@ -0,0 +1,5 @@
(function(angular, $, _) {
angular.module('crmMailing').directive('crmMailingBlockTracking', function(crmMailingSimpleDirective) {
return crmMailingSimpleDirective('crmMailingBlockTracking', '~/crmMailing/BlockTracking.html');
});
})(angular, CRM.$, CRM._);

View file

@ -0,0 +1,26 @@
<!--
Required vars: mailing
-->
<div ng-form="htmlForm" crm-ui-id-scope>
<div ng-controller="EmailBodyCtrl">
<div style="float: right;">
<input crm-mailing-token on-select="$broadcast('insert:body_html', token.name)" tabindex="-1" style="z-index:1">
</div>
<div>
<textarea
crm-ui-id="htmlForm.body_html"
crm-ui-richtext
name="body_html"
crm-ui-insert-rx="insert:body_html"
ng-model="mailing.body_html"
ng-blur="checkTokens(mailing, 'body_html', 'insert:body_html')"
data-preset="civimail"
></textarea>
<span ng-model="body_html_tokens" crm-ui-validate="hasAllTokens(mailing, 'body_html')"></span>
<div ng-show="htmlForm.$error.crmUiValidate" class="crmMailing-error-link">
{{ts('Required tokens are missing.')}} <a class="helpicon" ng-click="checkTokens(mailing, 'body_html', 'insert:body_html')"></a>
</div>
</div>
</div>
</div>

View file

@ -0,0 +1,5 @@
(function(angular, $, _) {
angular.module('crmMailing').directive('crmMailingBodyHtml', function(crmMailingSimpleDirective) {
return crmMailingSimpleDirective('crmMailingBodyHtml', '~/crmMailing/BodyHtml.html');
});
})(angular, CRM.$, CRM._);

View file

@ -0,0 +1,24 @@
<!--
Required vars: mailing, crmMailingConst
-->
<div ng-form="textForm" crm-ui-id-scope>
<div ng-controller="EmailBodyCtrl">
<div style="float: right;">
<input crm-mailing-token on-select="$broadcast('insert:body_text', token.name)" tabindex="-1"/>
</div>
<div>
<textarea
crm-ui-id="textForm.body_text"
crm-ui-insert-rx="insert:body_text"
name="body_text"
ng-model="mailing.body_text"
ng-blur="checkTokens(mailing, 'body_text', 'insert:body_text')"
></textarea>
<span ng-model="body_text_tokens" crm-ui-validate="hasAllTokens(mailing, 'body_text')"></span>
<div ng-show="textForm.$error.crmUiValidate" class="crmMailing-error-link">
{{ts('Required tokens are missing.')}} <a class="helpicon" ng-click="checkTokens(mailing, 'body_text', 'insert:body_text')"></a>
</div>
</div>
</div>
</div>

View file

@ -0,0 +1,5 @@
(function(angular, $, _) {
angular.module('crmMailing').directive('crmMailingBodyText', function(crmMailingSimpleDirective) {
return crmMailingSimpleDirective('crmMailingBodyText', '~/crmMailing/BodyText.html');
});
})(angular, CRM.$, CRM._);

View file

@ -0,0 +1,8 @@
(function(angular, $, _) {
angular.module('crmMailing').controller('CreateMailingCtrl', function EditMailingCtrl($scope, selectedMail, $location) {
$location.path("/mailing/" + selectedMail.id);
$location.replace();
});
})(angular, CRM.$, CRM._);

View file

@ -0,0 +1,133 @@
(function(angular, $, _) {
angular.module('crmMailing').controller('EditMailingCtrl', function EditMailingCtrl($scope, selectedMail, $location, crmMailingMgr, crmStatus, attachments, crmMailingPreviewMgr, crmBlocker, CrmAutosaveCtrl, $timeout, crmUiHelp) {
var APPROVAL_STATUSES = {'Approved': 1, 'Rejected': 2, 'None': 3};
$scope.mailing = selectedMail;
$scope.attachments = attachments;
$scope.crmMailingConst = CRM.crmMailing;
$scope.checkPerm = CRM.checkPerm;
var ts = $scope.ts = CRM.ts(null);
$scope.hs = crmUiHelp({file: 'CRM/Mailing/MailingUI'});
var block = $scope.block = crmBlocker();
var myAutosave = null;
var templateTypes = _.where(CRM.crmMailing.templateTypes, {name: selectedMail.template_type});
if (!templateTypes[0]) throw 'Unrecognized template type: ' + selectedMail.template_type;
$scope.mailingEditorUrl = templateTypes[0].editorUrl;
$scope.isSubmitted = function isSubmitted() {
return _.size($scope.mailing.jobs) > 0;
};
// usage: approve('Approved')
$scope.approve = function approve(status, options) {
$scope.mailing.approval_status_id = APPROVAL_STATUSES[status];
return myAutosave.suspend($scope.submit(options));
};
// @return Promise
$scope.previewMailing = function previewMailing(mailing, mode) {
return crmMailingPreviewMgr.preview(mailing, mode);
};
// @return Promise
$scope.sendTest = function sendTest(mailing, attachments, recipient) {
var savePromise = crmMailingMgr.save(mailing)
.then(function() {
return attachments.save();
});
return block(crmStatus({start: ts('Saving...'), success: ''}, savePromise)
.then(function() {
crmMailingPreviewMgr.sendTest(mailing, recipient);
}));
};
// @return Promise
$scope.submit = function submit(options) {
options = options || {};
if (block.check()) {
return;
}
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() {
if (!options.stay) {
$scope.leave('scheduled');
}
})
;
return block(crmStatus({start: ts('Submitting...'), success: ts('Submitted')}, promise));
};
// @return Promise
$scope.save = function save() {
return block(crmStatus(null,
crmMailingMgr
.save($scope.mailing)
.then(function() {
// pre-condition: the mailing exists *before* saving attachments to it
return $scope.attachments.save();
})
));
};
// @return Promise
$scope.delete = function cancel() {
return block(crmStatus({start: ts('Deleting...'), success: ts('Deleted')},
crmMailingMgr.delete($scope.mailing)
.then(function() {
$scope.leave('unscheduled');
})
));
};
// @param string listingScreen 'archive', 'scheduled', 'unscheduled'
$scope.leave = function leave(listingScreen) {
switch (listingScreen) {
case 'archive':
window.location = CRM.url('civicrm/mailing/browse/archived', {
reset: 1
});
break;
case 'scheduled':
window.location = CRM.url('civicrm/mailing/browse/scheduled', {
reset: 1,
scheduled: 'true'
});
break;
case 'unscheduled':
/* falls through */
default:
window.location = CRM.url('civicrm/mailing/browse/unscheduled', {
reset: 1,
scheduled: 'false'
});
}
};
myAutosave = new CrmAutosaveCtrl({
save: $scope.save,
saveIf: function() {
return true;
},
model: function() {
return [$scope.mailing, $scope.attachments.getAutosaveSignature()];
},
form: function() {
return $scope.crmMailing;
}
});
$timeout(myAutosave.start);
$scope.$on('$destroy', myAutosave.stop);
});
})(angular, CRM.$, CRM._);

View file

@ -0,0 +1,62 @@
<div ng-form="crmMailingSubform">
<div class="crm-block crm-form-block crmMailing">
<div crm-ui-wizard>
<div crm-ui-wizard-step crm-title="ts('Define Mailing')" ng-form="defineForm">
<div crm-ui-tab-set>
<div crm-ui-tab id="tab-mailing" crm-title="ts('Mailing')">
<div crm-mailing-block-summary crm-mailing="mailing"></div>
<div crm-mailing-block-mailing crm-mailing="mailing"></div>
<div crm-ui-accordion="{title: ts('HTML'), help: hs('html')}">
<div crm-mailing-body-html crm-mailing="mailing"></div>
</div>
<div crm-ui-accordion="{title: ts('Plain Text'), collapsed: !mailing.body_text, help: hs('text')}">
<div crm-mailing-body-text crm-mailing="mailing"></div>
</div>
<span ng-model="placeholder" crm-ui-validate="mailing.body_html || mailing.body_text"></span>
</div>
<div crm-ui-tab id="tab-attachment" crm-title="ts('Attachments')">
<div crm-attachments="attachments"></div>
</div>
<div crm-ui-tab id="tab-header" crm-title="ts('Header and Footer')">
<div crm-mailing-block-header-footer crm-mailing="mailing"></div>
</div>
<div crm-ui-tab id="tab-pub" crm-title="ts('Publication')">
<div crm-mailing-block-publication crm-mailing="mailing"></div>
</div>
<div crm-ui-tab id="tab-response" crm-title="ts('Responses')">
<div crm-mailing-block-responses crm-mailing="mailing"></div>
</div>
<div crm-ui-tab id="tab-tracking" crm-title="ts('Tracking')">
<div crm-mailing-block-tracking crm-mailing="mailing"></div>
</div>
</div>
<div crm-ui-accordion="{title: ts('Preview')}">
<div crm-mailing-block-preview crm-mailing="mailing" on-preview="previewMailing(mailing, preview.mode)" on-send="sendTest(mailing, attachments, preview.recipient)"></div>
</div>
</div>
<div crm-ui-wizard-step crm-title="ts('Review and Schedule')" ng-form="reviewForm">
<div crm-ui-accordion="{title: ts('Review')}">
<div crm-mailing-block-review crm-mailing="mailing" crm-mailing-attachments="attachments"></div>
</div>
<div crm-ui-accordion="{title: ts('Schedule')}">
<div crm-mailing-block-schedule crm-mailing="mailing"></div>
</div>
<center>
<a class="button crmMailing-submit-button" ng-click="submit()" ng-class="{blocking: block.check(), disabled: crmMailingSubform.$invalid}">
<div>{{ts('Submit Mailing')}}</div>
</a>
</center>
</div>
<span crm-ui-wizard-buttons style="float:right;">
<button
crm-icon="fa-trash"
ng-show="checkPerm('delete in CiviMail')"
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()">{{ts('Delete Draft')}}</button>
<button crm-icon="fa-floppy-o" ng-disabled="block.check()" ng-click="save().then(leave)">{{ts('Save Draft')}}</button>
</span>
</div>
</div>
</div>

View file

@ -0,0 +1,8 @@
<div crm-ui-debug="mailing"></div>
<div ng-show="isSubmitted()">
{{ts('This mailing has been submitted.')}}
</div>
<form name="crmMailing" novalidate ng-hide="isSubmitted()" ng-include="mailingEditorUrl">
</form>

View file

@ -0,0 +1,49 @@
<div ng-form="crmMailingSubform">
<div class="crm-block crm-form-block crmMailing">
<div crm-mailing-block-summary crm-mailing="mailing"></div>
<div crm-mailing-block-mailing crm-mailing="mailing"></div>
<div crm-ui-tab-set>
<div crm-ui-tab id="tab-html" crm-title="ts('HTML')">
<div crm-mailing-body-html crm-mailing="mailing"></div>
</div>
<div crm-ui-tab id="tab-text" crm-title="ts('Plain Text')">
<div crm-mailing-body-text crm-mailing="mailing"></div>
</div>
<span ng-model="placeholder" crm-ui-validate="mailing.body_html || mailing.body_text"></span>
<div crm-ui-tab id="tab-attachment" crm-title="ts('Attachments')">
<div crm-attachments="attachments"></div>
</div>
<div crm-ui-tab id="tab-header" crm-title="ts('Header and Footer')">
<div crm-mailing-block-header-footer crm-mailing="mailing"></div>
</div>
<div crm-ui-tab id="tab-pub" crm-title="ts('Publication')">
<div crm-mailing-block-publication crm-mailing="mailing"></div>
</div>
<div crm-ui-tab id="tab-response" crm-title="ts('Responses')">
<div crm-mailing-block-responses crm-mailing="mailing"></div>
</div>
<div crm-ui-tab id="tab-tracking" crm-title="ts('Tracking')">
<div crm-mailing-block-tracking crm-mailing="mailing"></div>
</div>
</div>
<div crm-ui-accordion="{title: ts('Preview')}">
<div crm-mailing-block-preview crm-mailing="mailing" on-preview="previewMailing(mailing, preview.mode)" on-send="sendTest(mailing, attachments, preview.recipient)"></div>
</div>
<div crm-ui-accordion="{title: ts('Schedule')}">
<div crm-mailing-block-schedule crm-mailing="mailing"></div>
</div>
<button crm-icon="fa-paper-plane" ng-disabled="block.check() || crmMailingSubform.$invalid" ng-click="submit()">{{ts('Submit Mailing')}}</button>
<button crm-icon="fa-floppy-o" ng-disabled="block.check()" ng-click="save().then(leave)">{{ts('Save Draft')}}</button>
<button
crm-icon="fa-trash"
ng-show="checkPerm('delete in CiviMail')"
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()">{{ts('Delete Draft')}}</button>
</div>
</div>

View file

@ -0,0 +1,45 @@
<div ng-form="crmMailingSubform">
<div class="crm-block crm-form-block crmMailing">
<div crm-mailing-block-summary crm-mailing="mailing"></div>
<div crm-mailing-block-mailing crm-mailing="mailing"></div>
<div crm-ui-accordion="{title: ts('HTML'), help: hs('html')}">
<div crm-mailing-body-html crm-mailing="mailing"></div>
</div>
<div crm-ui-accordion="{title: ts('Plain Text'), collapsed: !mailing.body_text, help: hs('text')}">
<div crm-mailing-body-text crm-mailing="mailing"></div>
</div>
<span ng-model="placeholder" crm-ui-validate="mailing.body_html || mailing.body_text"></span>
<div crm-ui-accordion="{title: ts('Header and Footer'), collapsed: true}" id="tab-header">
<div crm-mailing-block-header-footer crm-mailing="mailing"></div>
</div>
<div crm-ui-accordion="{title: ts('Attachments'), collapsed: true}" id="tab-attachment">
<div crm-attachments="attachments"></div>
</div>
<div crm-ui-accordion="{title: ts('Publication'), collapsed: true}" id="tab-pub">
<div crm-mailing-block-publication crm-mailing="mailing"></div>
</div>
<div crm-ui-accordion="{title: ts('Responses'), collapsed: true}" id="tab-response">
<div crm-mailing-block-responses crm-mailing="mailing"></div>
</div>
<div crm-ui-accordion="{title: ts('Tracking'), collapsed: true}" id="tab-tracking">
<div crm-mailing-block-tracking crm-mailing="mailing"></div>
</div>
<div crm-ui-accordion="{title: ts('Preview')}">
<div crm-mailing-block-preview crm-mailing="mailing" on-preview="previewMailing(mailing, preview.mode)" on-send="sendTest(mailing, attachments, preview.recipient)"></div>
</div>
<div crm-ui-accordion="{title: ts('Schedule')}" id="tab-schedule">
<div crm-mailing-block-schedule crm-mailing="mailing"></div>
</div>
<button crm-icon="fa-paper-plane" ng-disabled="block.check() || crmMailingSubform.$invalid" ng-click="submit()">{{ts('Submit Mailing')}}</button>
<button crm-icon="fa-floppy-o" ng-disabled="block.check()" ng-click="save().then(leave)">{{ts('Save Draft')}}</button>
<button
crm-icon="fa-trash"
ng-show="checkPerm('delete in CiviMail')"
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()">{{ts('Delete Draft')}}</button>
</div>
</div>

View file

@ -0,0 +1,65 @@
<div ng-form="crmMailingSubform">
<div class="crm-block crm-form-block crmMailing">
<div crm-ui-wizard>
<div crm-ui-wizard-step crm-title="ts('Content')" ng-form="contentForm">
<div crm-mailing-block-summary crm-mailing="mailing"></div>
<div crm-mailing-block-mailing crm-mailing="mailing"></div>
<div crm-ui-accordion="{title: ts('HTML'), help: hs('html')}">
<div crm-mailing-body-html crm-mailing="mailing"></div>
</div>
<div crm-ui-accordion="{title: ts('Plain Text'), collapsed: !mailing.body_text, help: hs('text')}">
<div crm-mailing-body-text crm-mailing="mailing"></div>
</div>
<span ng-model="placeholder" crm-ui-validate="mailing.body_html || mailing.body_text"></span>
<div crm-ui-accordion="{title: ts('Header and Footer'), collapsed: true}">
<div crm-mailing-block-header-footer crm-mailing="mailing"></div>
</div>
<div crm-ui-accordion="{title: ts('Attachments'), collapsed: true}">
<div crm-attachments="attachments"></div>
</div>
<div crm-ui-accordion="{title: ts('Preview')}">
<div crm-mailing-block-preview crm-mailing="mailing" on-preview="previewMailing(mailing, preview.mode)" on-send="sendTest(mailing, attachments, preview.recipient)"></div>
</div>
</div>
<div crm-ui-wizard-step crm-title="ts('Options')" ng-form="optionsForm">
<div crm-ui-accordion="{title: ts('Schedule')}">
<div crm-mailing-block-schedule crm-mailing="mailing"></div>
</div>
<div crm-ui-accordion="{title: ts('Responses'), collapsed: true}">
<div crm-mailing-block-responses crm-mailing="mailing"></div>
</div>
<div crm-ui-accordion="{title: ts('Tracking'), collapsed: true}">
<div crm-mailing-block-tracking crm-mailing="mailing"></div>
</div>
<div crm-ui-accordion="{title: ts('Publication'), collapsed: true}">
<div crm-mailing-block-publication crm-mailing="mailing"></div>
</div>
</div>
<div crm-ui-wizard-step crm-title="ts('Review')" ng-form="reviewForm">
<div crm-ui-accordion="{title: ts('Review')}">
<div crm-mailing-block-review crm-mailing="mailing" crm-mailing-attachments="attachments"></div>
</div>
<center>
<a class="button crmMailing-submit-button" ng-click="submit()" ng-class="{blocking: block.check(), disabled: crmMailingSubform.$invalid}">
<div>{{ts('Submit Mailing')}}</div>
</a>
</center>
</div>
<span crm-ui-wizard-buttons style="float:right;">
<button
crm-icon="fa-trash"
ng-show="checkPerm('delete in CiviMail')"
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()">{{ts('Delete Draft')}}</button>
<button crm-icon="fa-floppy-o" ng-disabled="block.check()" ng-click="save().then(leave)">{{ts('Save Draft')}}</button>
</span>
</div>
</div>
</div>

View file

@ -0,0 +1,72 @@
<div ng-form="crmMailingSubform">
<div class="crm-block crm-form-block crmMailing">
<div crm-ui-wizard>
<div crm-ui-wizard-step="10" crm-title="ts('Content')" ng-form="contentForm" ng-if="checkPerm('create mailings') || checkPerm('access CiviMail')">
<div crm-mailing-block-summary crm-mailing="mailing"></div>
<div crm-mailing-block-mailing crm-mailing="mailing"></div>
<div crm-ui-accordion="{title: ts('HTML'), help: hs('html')}">
<div crm-mailing-body-html crm-mailing="mailing"></div>
</div>
<div crm-ui-accordion="{title: ts('Plain Text'), collapsed: !mailing.body_text, help: hs('text')}">
<div crm-mailing-body-text crm-mailing="mailing"></div>
</div>
<span ng-model="placeholder" crm-ui-validate="mailing.body_html || mailing.body_text"></span>
<div crm-ui-accordion="{title: ts('Header and Footer'), collapsed: true}">
<div crm-mailing-block-header-footer crm-mailing="mailing"></div>
</div>
<div crm-ui-accordion="{title: ts('Attachments'), collapsed: true}">
<div crm-attachments="attachments"></div>
</div>
<div crm-ui-accordion="{title: ts('Preview')}">
<div crm-mailing-block-preview crm-mailing="mailing" on-preview="previewMailing(mailing, preview.mode)" on-send="sendTest(mailing, attachments, preview.recipient)"></div>
</div>
</div>
<div crm-ui-wizard-step="20" crm-title="ts('Options')" ng-form="optionsForm" ng-if="checkPerm('create mailings') || checkPerm('access CiviMail')">
<div crm-ui-accordion="{title: ts('Responses'), collapsed: true}">
<div crm-mailing-block-responses crm-mailing="mailing"></div>
</div>
<div crm-ui-accordion="{title: ts('Tracking'), collapsed: true}">
<div crm-mailing-block-tracking crm-mailing="mailing"></div>
</div>
<div crm-ui-accordion="{title: ts('Publication'), collapsed: true}">
<div crm-mailing-block-publication crm-mailing="mailing"></div>
</div>
</div>
<div crm-ui-wizard-step="40" crm-title="ts('Review')" ng-form="schedForm" ng-if="checkPerm('schedule mailings') || checkPerm('access CiviMail')">
<div crm-ui-accordion="{title: ts('Review')}">
<div crm-mailing-block-review crm-mailing="mailing" crm-mailing-attachments="attachments"></div>
</div>
<div crm-ui-accordion="{title: ts('Schedule')}">
<div crm-mailing-block-schedule crm-mailing="mailing"></div>
</div>
<div crm-ui-accordion="{title: ts('Approval')}" ng-if="checkPerm('approve mailings') || checkPerm('access CiviMail')">
<div crm-mailing-block-approve crm-mailing="mailing"></div>
</div>
<center ng-if="!checkPerm('approve mailings') && !checkPerm('access CiviMail')">
<a class="button crmMailing-submit-button" ng-click="submit()" ng-class="{blocking: block.check(), disabled: crmMailingSubform.$invalid}">
<div>{{ts('Submit Mailing')}}</div>
</a>
</center>
<center ng-if="checkPerm('approve mailings') || checkPerm('access CiviMail')">
<a class="button crmMailing-submit-button" ng-click="approve('Approved')" ng-class="{blocking: block.check(), disabled: crmMailingSubform.$invalid}">
<div>{{ts('Submit and Approve Mailing')}}</div>
</a>
</center>
</div>
<span crm-ui-wizard-buttons style="float:right;">
<button
crm-icon="fa-trash"
ng-show="checkPerm('delete in CiviMail')"
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()">{{ts('Delete Draft')}}</button>
<button crm-icon="fa-floppy-o" ng-disabled="block.check()" ng-click="save().then(leave)">{{ts('Save Draft')}}</button>
</span>
</div>
</div>
</div>

View file

@ -0,0 +1,92 @@
(function(angular, $, _) {
// Controller for the edit-recipients fields (
// WISHLIST: Move most of this to a (cache-enabled) service
// Scope members:
// - [input] mailing: object
// - [output] recipients: array of recipient records
angular.module('crmMailing').controller('EditRecipCtrl', function EditRecipCtrl($scope, dialogService, crmApi, crmMailingMgr, $q, crmMetadata, crmStatus) {
// Time to wait before triggering AJAX update to recipients list
var RECIPIENTS_DEBOUNCE_MS = 100;
var RECIPIENTS_PREVIEW_LIMIT = 50;
var ts = $scope.ts = CRM.ts(null);
$scope.isMailingList = function isMailingList(group) {
var GROUP_TYPE_MAILING_LIST = '2';
return _.contains(group.group_type, GROUP_TYPE_MAILING_LIST);
};
$scope.recipients = null;
$scope.getRecipientsEstimate = function() {
var ts = $scope.ts;
if ($scope.recipients === null) {
return ts('(Estimating)');
}
if ($scope.recipients === 0) {
return ts('No recipients');
}
if ($scope.recipients === 1) {
return ts('~1 recipient');
}
return ts('~%1 recipients', {1: $scope.recipients});
};
// We monitor four fields -- use debounce so that changes across the
// four fields can settle-down before AJAX.
var refreshRecipients = _.debounce(function() {
$scope.$apply(function() {
$scope.recipients = null;
if (!$scope.mailing) {
return;
}
crmMailingMgr.previewRecipientCount($scope.mailing).then(function(recipients) {
$scope.recipients = recipients;
});
});
}, RECIPIENTS_DEBOUNCE_MS);
$scope.$watchCollection("mailing.dedupe_email", refreshRecipients);
$scope.$watchCollection("mailing.location_type_id", refreshRecipients);
$scope.$watchCollection("mailing.email_selection_method", refreshRecipients);
$scope.$watchCollection("mailing.recipients.groups.include", refreshRecipients);
$scope.$watchCollection("mailing.recipients.groups.exclude", refreshRecipients);
$scope.$watchCollection("mailing.recipients.mailings.include", refreshRecipients);
$scope.$watchCollection("mailing.recipients.mailings.exclude", refreshRecipients);
$scope.previewRecipients = function previewRecipients() {
return crmStatus({start: ts('Previewing...'), success: ''}, crmMailingMgr.previewRecipients($scope.mailing, RECIPIENTS_PREVIEW_LIMIT).then(function(recipients) {
var model = {
count: $scope.recipients,
sample: recipients,
sampleLimit: RECIPIENTS_PREVIEW_LIMIT
};
var options = CRM.utils.adjustDialogDefaults({
width: '40%',
autoOpen: false,
title: ts('Preview (%1)', {
1: $scope.getRecipientsEstimate()
})
});
dialogService.open('recipDialog', '~/crmMailing/PreviewRecipCtrl.html', model, options);
}));
};
// Open a dialog for editing the advanced recipient options.
$scope.editOptions = function editOptions(mailing) {
var options = CRM.utils.adjustDialogDefaults({
autoOpen: false,
width: '40%',
height: 'auto',
title: ts('Edit Options')
});
$q.when(crmMetadata.getFields('Mailing')).then(function(fields) {
var model = {
fields: fields,
mailing: mailing
};
dialogService.open('previewComponentDialog', '~/crmMailing/EditRecipOptionsDialogCtrl.html', model, options);
});
};
});
})(angular, CRM.$, CRM._);

View file

@ -0,0 +1,41 @@
<div ng-controller="EditRecipOptionsDialogCtrl" class="crmMailing">
<div class="crm-block" ng-form="editRecipOptionsForm" crm-ui-id-scope>
<div class="crm-group">
<div crm-ui-field="{title: ts('Dedupe by email'), help: hs('dedupe_email')}" crm-layout="checkbox">
<input
type="checkbox"
ng-model="model.mailing.dedupe_email"
ng-true-value="'1'"
ng-false-value="'0'"
>
</div>
<div crm-ui-field="{name: 'editRecipOptionsForm.location_type_id', title: ts('Location Type')}">
<select
crm-ui-id="editRecipOptionsForm.location_type_id"
crm-ui-select="{dropdownAutoWidth : true}"
name="location_type_id"
ng-model="model.mailing.location_type_id"
>
<option value="">{{ts('Automatic')}}</option>
<option ng-repeat="locType in model.fields.location_type_id.options"
ng-value="locType.key">{{locType.value}}</option>
</select>
</div>
<div crm-ui-field="{name: 'editRecipOptionsForm.email_selection_method', title: ts('Selection Method')}">
<select
crm-ui-id="editRecipOptionsForm.email_selection_method"
crm-ui-select=""
name="email_selection_method"
ng-model="model.mailing.email_selection_method"
>
<option ng-repeat="selMet in model.fields.email_selection_method.options"
ng-value="selMet.key">{{selMet.value}}</option>
</select>
</div>
</div>
</div>
</div>

View file

@ -0,0 +1,12 @@
(function(angular, $, _) {
// Controller for the "Recipients: Edit Options" dialog
// Note: Expects $scope.model to be an object with properties:
// - "mailing" (APIv3 mailing object)
// - "fields" (list of fields)
angular.module('crmMailing').controller('EditRecipOptionsDialogCtrl', function EditRecipOptionsDialogCtrl($scope, crmUiHelp) {
$scope.ts = CRM.ts(null);
$scope.hs = crmUiHelp({file: 'CRM/Mailing/MailingUI'});
});
})(angular, CRM.$, CRM._);

View file

@ -0,0 +1,19 @@
(function(angular, $, _) {
angular.module('crmMailing').controller('EditUnsubGroupCtrl', function EditUnsubGroupCtrl($scope) {
// CRM.crmMailing.groupNames is a global constant - since it doesn't change, we can digest & cache.
var mandatoryIds = [];
$scope.isUnsubGroupRequired = function isUnsubGroupRequired(mailing) {
if (!_.isEmpty(CRM.crmMailing.groupNames)) {
_.each(CRM.crmMailing.groupNames, function(grp) {
if (grp.is_hidden == "1") {
mandatoryIds.push(parseInt(grp.id));
}
});
return _.intersection(mandatoryIds, mailing.recipients.groups.include).length > 0;
}
};
});
})(angular, CRM.$, CRM._);

View file

@ -0,0 +1,31 @@
(function(angular, $, _) {
angular.module('crmMailing').controller('EmailAddrCtrl', function EmailAddrCtrl($scope, crmFromAddresses, crmUiAlert) {
var ts = CRM.ts(null);
function changeAlert(winnerField, loserField) {
crmUiAlert({
title: ts('Conflict'),
text: ts('The "%1" option conflicts with the "%2" option. The "%2" option has been disabled.', {
1: winnerField,
2: loserField
})
});
}
$scope.crmFromAddresses = crmFromAddresses;
$scope.checkReplyToChange = function checkReplyToChange(mailing) {
if (!_.isEmpty(mailing.replyto_email) && mailing.override_verp == '0') {
mailing.override_verp = '1';
changeAlert(ts('Reply-To'), ts('Track Replies'));
}
};
$scope.checkVerpChange = function checkVerpChange(mailing) {
if (!_.isEmpty(mailing.replyto_email) && mailing.override_verp == '0') {
mailing.replyto_email = '';
changeAlert(ts('Track Replies'), ts('Reply-To'));
}
};
});
})(angular, CRM.$, CRM._);

View file

@ -0,0 +1,52 @@
(function(angular, $, _) {
var lastEmailTokenAlert = null;
angular.module('crmMailing').controller('EmailBodyCtrl', function EmailBodyCtrl($scope, crmMailingMgr, crmUiAlert, $timeout) {
var ts = CRM.ts(null);
// ex: if (!hasAllTokens(myMailing, 'body_text)) alert('Oh noes!');
$scope.hasAllTokens = function hasAllTokens(mailing, field) {
return _.isEmpty(crmMailingMgr.findMissingTokens(mailing, field));
};
// ex: checkTokens(myMailing, 'body_text', 'insert:body_text')
// ex: checkTokens(myMailing, '*')
$scope.checkTokens = function checkTokens(mailing, field, insertEvent) {
if (lastEmailTokenAlert) {
lastEmailTokenAlert.close();
}
var missing, insertable;
if (field == '*') {
insertable = false;
missing = angular.extend({},
crmMailingMgr.findMissingTokens(mailing, 'body_html'),
crmMailingMgr.findMissingTokens(mailing, 'body_text')
);
}
else {
insertable = !_.isEmpty(insertEvent);
missing = crmMailingMgr.findMissingTokens(mailing, field);
}
if (!_.isEmpty(missing)) {
lastEmailTokenAlert = crmUiAlert({
type: 'error',
title: ts('Required tokens'),
templateUrl: '~/crmMailing/EmailBodyCtrl/tokenAlert.html',
scope: angular.extend($scope.$new(), {
insertable: insertable,
insertToken: function(token) {
$timeout(function() {
$scope.$broadcast(insertEvent, '{' + token + '}');
$timeout(function() {
checkTokens(mailing, field, insertEvent);
});
});
},
missing: missing
})
});
}
};
});
})(angular, CRM.$, CRM._);

View file

@ -0,0 +1,76 @@
<p ng-show="missing['domain.address']">
{{ts('The mailing must include the street address of the organization. Please insert the %1 token.', {1:
'{domain.address}'})}}
</p>
<div ng-show="missing['domain.address'] && insertable">
<a ng-click="insertToken('domain.address')" class="button"><span><i class="crm-i fa-plus-circle"></i> {{ts('Address')}}</span></a>
<div class="clear"></div>
</div>
<p ng-show="missing['action.optOut']">
{{ts('The mailing must allow recipients to (a) unsubscribe from the mailing-list or (b) completely opt-out from all mailings. Please insert an unsubscribe or opt-out token.')}}
</p>
<div ng-show="missing['action.optOut'] && insertable">
<table>
<thead>
<tr>
<th>{{ts('Via Web')}}</th>
<th>{{ts('Via Email')}}</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<a ng-click="insertToken('action.unsubscribeUrl')" class="button"><span><i class="crm-i fa-plus-circle"></i> {{ts('Unsubscribe')}}</span></a>
</td>
<td>
<a ng-click="insertToken('action.unsubscribe')" class="button"><span><i class="crm-i fa-plus-circle"></i> {{ts('Unsubscribe')}}</span></a>
</td>
</tr>
<tr>
<td>
<a ng-click="insertToken('action.optOutUrl')" class="button"><span><i class="crm-i fa-plus-circle"></i> {{ts('Opt-out')}}</span></a>
</td>
<td>
<a ng-click="insertToken('action.optOut')" class="button"><span><i class="crm-i fa-plus-circle"></i> {{ts('Opt-out')}}</span></a>
</td>
</tr>
</tbody>
</table>
</div>
<div ng-show="missing['action.optOut'] && !insertable">
<table>
<thead>
<tr>
<th>{{ts('Via Web')}}</th>
<th>{{ts('Via Email')}}</th>
</tr>
</thead>
<tbody>
<tr>
<td>
{action.optOutUrl}
</td>
<td>
{action.optOut}
</td>
</tr>
<tr>
<td>
{action.unsubscribeUrl}
</td>
<td>
{action.unsubscribe}
</td>
</tr>
</tbody>
</table>
</div>
<p>
{{ts('Alternatively, you may select a header or footer which includes the required tokens.')}}
</p>

View file

@ -0,0 +1,30 @@
(function(angular, $, _) {
// Convert between a mailing "From Address" (mailing.from_name,mailing.from_email) and a unified label ("Name" <e@ma.il>)
// example: <span crm-mailing-from-address="myPlaceholder" crm-mailing="myMailing"><select ng-model="myPlaceholder.label"></select></span>
// NOTE: This really doesn't belong in a directive. I've tried (and failed) to make this work with a getterSetter binding, eg
// <select ng-model="mailing.convertFromAddress" ng-model-options="{getterSetter: true}">
angular.module('crmMailing').directive('crmMailingFromAddress', function(crmFromAddresses) {
return {
link: function(scope, element, attrs) {
var placeholder = attrs.crmMailingFromAddress;
var mailing = null;
scope.$watch(attrs.crmMailing, function(newValue) {
mailing = newValue;
scope[placeholder] = {
label: crmFromAddresses.getByAuthorEmail(mailing.from_name, mailing.from_email, true).label
};
});
scope.$watch(placeholder + '.label', function(newValue) {
var addr = crmFromAddresses.getByLabel(newValue);
mailing.from_name = addr.author;
mailing.from_email = addr.email;
// CRM-18364: set replyTo as from_email only if custom replyTo is disabled in mail settings.
if (!CRM.crmMailing.enableReplyTo) {
mailing.replyto_email = crmFromAddresses.getByAuthorEmail(mailing.from_name, mailing.from_email, true).label;
}
});
// FIXME: Shouldn't we also be watching mailing.from_name and mailing.from_email?
}
};
});
})(angular, CRM.$, CRM._);

View file

@ -0,0 +1,10 @@
(function(angular, $, _) {
angular.module('crmMailing').controller('ListMailingsCtrl', ['crmLegacy', 'crmNavigator', function ListMailingsCtrl(crmLegacy, crmNavigator) {
// We haven't implemented this in Angular, but some users may get clever
// about typing URLs, so we'll provide a redirect.
var new_url = crmLegacy.url('civicrm/mailing/browse/unscheduled', {reset: 1, scheduled: 'false'});
crmNavigator.redirect(new_url);
}]);
})(angular, CRM.$, CRM._);

View file

@ -0,0 +1,43 @@
(function(angular, $, _) {
// Controller for the in-place msg-template management
angular.module('crmMailing').controller('MsgTemplateCtrl', function MsgTemplateCtrl($scope, crmMsgTemplates, dialogService) {
var ts = $scope.ts = CRM.ts(null);
$scope.crmMsgTemplates = crmMsgTemplates;
$scope.checkPerm = CRM.checkPerm;
// @return Promise MessageTemplate (per APIv3)
$scope.saveTemplate = function saveTemplate(mailing) {
var model = {
selected_id: mailing.msg_template_id,
tpl: {
msg_title: '',
msg_subject: mailing.subject,
msg_text: mailing.body_text,
msg_html: mailing.body_html
}
};
var options = CRM.utils.adjustDialogDefaults({
autoOpen: false,
height: 'auto',
width: '40%',
title: ts('Save Template')
});
return dialogService.open('saveTemplateDialog', '~/crmMailing/SaveMsgTemplateDialogCtrl.html', model, options)
.then(function(item) {
mailing.msg_template_id = item.id;
return item;
});
};
// @param int id
// @return Promise
$scope.loadTemplate = function loadTemplate(mailing, id) {
return crmMsgTemplates.get(id).then(function(tpl) {
mailing.subject = tpl.msg_subject;
mailing.body_text = tpl.msg_text;
mailing.body_html = tpl.msg_html;
});
};
});
})(angular, CRM.$, CRM._);

View file

@ -0,0 +1,24 @@
(function(angular, $, _) {
// Controller for the "Preview Mailing Component" segment
// which displays header/footer/auto-responder
angular.module('crmMailing').controller('PreviewComponentCtrl', function PreviewComponentCtrl($scope, dialogService) {
var ts = $scope.ts = CRM.ts(null);
$scope.previewComponent = function previewComponent(title, componentId) {
var component = _.where(CRM.crmMailing.headerfooterList, {id: "" + componentId});
if (!component || !component[0]) {
CRM.alert(ts('Invalid component ID (%1)', {
1: componentId
}));
return;
}
var options = CRM.utils.adjustDialogDefaults({
autoOpen: false,
title: title // component[0].name
});
dialogService.open('previewComponentDialog', '~/crmMailing/PreviewComponentDialogCtrl.html', component[0], options);
};
});
})(angular, CRM.$, CRM._);

View file

@ -0,0 +1,28 @@
<div ng-controller="PreviewComponentDialogCtrl">
<div class="crm-block">
<div class="crm-group">
<div class="crm-section" ng-show="model.name">
<div class="label">{{ts('Name')}}</div>
<div class="content">
{{model.name}}
</div>
<div class="clear"></div>
</div>
<div class="crm-section" ng-show="model.subject">
<div class="label">{{ts('Subject')}}</div>
<div class="content">
{{model.subject}}
</div>
<div class="clear"></div>
</div>
</div>
</div>
<div crm-ui-tab-set>
<div crm-ui-tab id="preview-html" crm-title="ts('HTML')">
<iframe crm-ui-iframe="model.body_html"></iframe>
</div>
<div crm-ui-tab id="preview-text" crm-title="ts('Plain Text')">
<pre>{{model.body_text}}</pre>
</div>
</div>
</div>

View file

@ -0,0 +1,13 @@
(function(angular, $, _) {
// Controller for the "Preview Mailing Component" dialog
// Note: Expects $scope.model to be an object with properties:
// - "name"
// - "subject"
// - "body_html"
// - "body_text"
angular.module('crmMailing').controller('PreviewComponentDialogCtrl', function PreviewComponentDialogCtrl($scope) {
$scope.ts = CRM.ts(null);
});
})(angular, CRM.$, CRM._);

View file

@ -0,0 +1,12 @@
(function(angular, $, _) {
// Controller for the "Preview Mailing" dialog
// Note: Expects $scope.model to be an object with properties:
// - "subject"
// - "body_html"
// - "body_text"
angular.module('crmMailing').controller('PreviewMailingDialogCtrl', function PreviewMailingDialogCtrl($scope) {
$scope.ts = CRM.ts(null);
});
})(angular, CRM.$, CRM._);

View file

@ -0,0 +1,10 @@
<div ng-controller="PreviewMailingDialogCtrl">
<div crm-ui-tab-set>
<div crm-ui-tab id="preview-html" crm-title="ts('HTML')">
<iframe crm-ui-iframe="model.body_html"></iframe>
</div>
<div crm-ui-tab id="preview-text" crm-title="ts('Plain Text')">
<pre>{{model.body_text}}</pre>
</div>
</div>
</div>

View file

@ -0,0 +1,3 @@
<div ng-controller="PreviewMailingDialogCtrl">
<iframe crm-ui-iframe="model.body_html"></iframe>
</div>

View file

@ -0,0 +1,3 @@
<div ng-controller="PreviewMailingDialogCtrl">
<pre>{{model.body_text}}</pre>
</div>

View file

@ -0,0 +1,32 @@
<div ng-controller="PreviewRecipCtrl">
<!--
Controller: PreviewRecipCtrl
Required vars: model.sample
-->
<div class="help">
<p>{{ts('Based on current data, approximately %1 contacts will receive a copy of the mailing.', {1: model.count})}}</p>
<p ng-show="model.sample.length == model.sampleLimit">{{ts('Below is a sample of the first %1 recipients.', {1: model.sampleLimit})}}</p>
<p>{{ts('If individual contacts are separately modified, added, or removed, then the final list may change.')}}</p>
</div>
<div ng-show="model.sample == 0">
{{ts('No recipients')}}
</div>
<table ng-show="model.sample.length > 0">
<thead>
<tr>
<th>{{ts('Name')}}</th>
<th>{{ts('Email')}}</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="recipient in model.sample">
<td>{{recipient['api.contact.getvalue']}}</td>
<td>{{recipient['api.email.getvalue']}}</td>
</tr>
</tbody>
</table>
</div>

View file

@ -0,0 +1,10 @@
(function(angular, $, _) {
// Controller for the "Preview Recipients" dialog
// Note: Expects $scope.model to be an object with properties:
// - recipients: array of contacts
angular.module('crmMailing').controller('PreviewRecipCtrl', function($scope) {
$scope.ts = CRM.ts(null);
});
})(angular, CRM.$, CRM._);

View file

@ -0,0 +1,116 @@
(function(angular, $, _) {
// "YYYY-MM-DD hh:mm:ss" => Date()
function parseYmdHms(d) {
var parts = d.split(/[\-: ]/);
return new Date(parts[0], parts[1]-1, parts[2], parts[3], parts[4], parts[5]);
}
function isDateBefore(tgt, cutoff, tolerance) {
var ad = parseYmdHms(tgt), bd = parseYmdHms(cutoff);
// We'll allow a little leeway, where tgt is considered before cutoff
// even if technically misses the cutoff by a little.
return ad < bd-tolerance;
}
// Represent a datetime field as if it were a radio ('schedule.mode') and a datetime ('schedule.datetime').
// example: <div crm-mailing-radio-date="mySchedule" ng-model="mailing.scheduled_date">...</div>
angular.module('crmMailing').directive('crmMailingRadioDate', function(crmUiAlert) {
return {
require: 'ngModel',
link: function($scope, element, attrs, ngModel) {
var lastAlert = null;
var schedule = $scope[attrs.crmMailingRadioDate] = {
mode: 'now',
datetime: ''
};
ngModel.$render = function $render() {
var sched = ngModel.$viewValue;
if (!_.isEmpty(sched)) {
schedule.mode = 'at';
schedule.datetime = sched;
}
else {
schedule.mode = 'now';
schedule.datetime = '';
}
};
var updateParent = (function() {
switch (schedule.mode) {
case 'now':
ngModel.$setViewValue(null);
schedule.datetime = '';
break;
case 'at':
schedule.datetime = schedule.datetime || '?';
ngModel.$setViewValue(schedule.datetime);
break;
default:
throw 'Unrecognized schedule mode: ' + schedule.mode;
}
});
element
// Open datepicker when clicking "At" radio
.on('click', ':radio[value=at]', function() {
$('.crm-form-date', element).focus();
})
// Reset mode if user entered an invalid date
.on('change', '.crm-hidden-date', function(e, context) {
if (context === 'userInput' && $(this).val() === '' && $(this).siblings('.crm-form-date').val().length) {
schedule.mode = 'at';
schedule.datetime = '?';
} else {
var d = new Date(),
month = '' + (d.getMonth() + 1),
day = '' + d.getDate(),
year = d.getFullYear(),
hours = '' + d.getHours(),
minutes = '' + d.getMinutes();
var submittedDate = $(this).val();
if (month.length < 2) month = '0' + month;
if (day.length < 2) day = '0' + day;
if (hours.length < 2) hours = '0' + hours;
if (minutes.length < 2) minutes = '0' + minutes;
date = [year, month, day].join('-');
time = [hours, minutes, "00"].join(':');
currentDate = date + ' ' + time;
var isInPast = (submittedDate.length && submittedDate.match(/^[0-9\-]+ [0-9\:]+$/) && isDateBefore(submittedDate, currentDate, 4*60*60*1000));
ngModel.$setValidity('dateTimeInThePast', !isInPast);
if (lastAlert && lastAlert.isOpen) {
lastAlert.close();
}
if (isInPast) {
lastAlert = crmUiAlert({
text: ts('The scheduled date and time is in the past'),
title: ts('Error')
});
}
}
});
$scope.$watch(attrs.crmMailingRadioDate + '.mode', updateParent);
$scope.$watch(attrs.crmMailingRadioDate + '.datetime', function(newValue, oldValue) {
// automatically switch mode based on datetime entry
if (typeof oldValue === 'undefined') {
oldValue = '';
}
if (typeof newValue === 'undefined') {
newValue = '';
}
if (oldValue !== newValue) {
if (_.isEmpty(newValue)) {
schedule.mode = 'now';
}
else {
schedule.mode = 'at';
}
}
updateParent();
});
}
};
});
})(angular, CRM.$, CRM._);

View file

@ -0,0 +1,322 @@
(function(angular, $, _) {
// example: <select multiple crm-mailing-recipients crm-mailing="mymailing" crm-avail-groups="myGroups" crm-avail-mailings="myMailings"></select>
// FIXME: participate in ngModel's validation cycle
angular.module('crmMailing').directive('crmMailingRecipients', function(crmUiAlert) {
return {
restrict: 'AE',
require: 'ngModel',
scope: {
ngRequired: '@'
},
link: function(scope, element, attrs, ngModel) {
scope.recips = ngModel.$viewValue;
scope.groups = scope.$parent.$eval(attrs.crmAvailGroups);
scope.mailings = scope.$parent.$eval(attrs.crmAvailMailings);
refreshMandatory();
var ts = scope.ts = CRM.ts(null);
/// Convert MySQL date ("yyyy-mm-dd hh:mm:ss") to JS date object
scope.parseDate = function(date) {
if (!angular.isString(date)) {
return date;
}
var p = date.split(/[\- :]/);
return new Date(parseInt(p[0]), parseInt(p[1]) - 1, parseInt(p[2]), parseInt(p[3]), parseInt(p[4]), parseInt(p[5]));
};
/// Remove {value} from {array}
function arrayRemove(array, value) {
var idx = array.indexOf(value);
if (idx >= 0) {
array.splice(idx, 1);
}
}
// @param string id an encoded string like "4 civicrm_mailing include"
// @return Object keys: entity_id, entity_type, mode
function convertValueToObj(id) {
var a = id.split(" ");
return {entity_id: parseInt(a[0]), entity_type: a[1], mode: a[2]};
}
// @param Object mailing
// @return array list of values like "4 civicrm_mailing include"
function convertMailingToValues(recipients) {
var r = [];
angular.forEach(recipients.groups.include, function(v) {
r.push(v + " civicrm_group include");
});
angular.forEach(recipients.groups.exclude, function(v) {
r.push(v + " civicrm_group exclude");
});
angular.forEach(recipients.mailings.include, function(v) {
r.push(v + " civicrm_mailing include");
});
angular.forEach(recipients.mailings.exclude, function(v) {
r.push(v + " civicrm_mailing exclude");
});
return r;
}
function refreshMandatory() {
if (ngModel.$viewValue && ngModel.$viewValue.groups) {
scope.mandatoryGroups = _.filter(scope.$parent.$eval(attrs.crmMandatoryGroups), function(grp) {
return _.contains(ngModel.$viewValue.groups.include, parseInt(grp.id));
});
scope.mandatoryIds = _.map(_.pluck(scope.$parent.$eval(attrs.crmMandatoryGroups), 'id'), function(n) {
return parseInt(n);
});
}
else {
scope.mandatoryGroups = [];
scope.mandatoryIds = [];
}
}
function isMandatory(grpId) {
return _.contains(scope.mandatoryIds, parseInt(grpId));
}
var refreshUI = ngModel.$render = function refresuhUI() {
scope.recips = ngModel.$viewValue;
if (ngModel.$viewValue) {
$(element).select2('val', convertMailingToValues(ngModel.$viewValue));
validate();
refreshMandatory();
}
};
// @return string HTML representing an option
function formatItem(item) {
if (!item.id) {
// return `text` for optgroup
return item.text;
}
var option = convertValueToObj(item.id);
var icon = (option.entity_type === 'civicrm_mailing') ? 'fa-envelope' : 'fa-users';
var spanClass = (option.mode == 'exclude') ? 'crmMailing-exclude' : 'crmMailing-include';
if (option.entity_type != 'civicrm_mailing' && isMandatory(option.entity_id)) {
spanClass = 'crmMailing-mandatory';
}
return '<i class="crm-i '+icon+'"></i> <span class="' + spanClass + '">' + item.text + '</span>';
}
function validate() {
if (scope.$parent.$eval(attrs.ngRequired)) {
var empty = (_.isEmpty(ngModel.$viewValue.groups.include) && _.isEmpty(ngModel.$viewValue.mailings.include));
ngModel.$setValidity('empty', !empty);
}
else {
ngModel.$setValidity('empty', true);
}
}
var rcpAjaxState = {
input: '',
entity: 'civicrm_group',
type: 'include',
page_n: 0,
page_i: 0,
};
$(element).select2({
width: '36em',
dropdownAutoWidth: true,
placeholder: "Groups or Past Recipients",
formatResult: formatItem,
formatSelection: formatItem,
escapeMarkup: function(m) {
return m;
},
multiple: true,
initSelection: function(el, cb) {
var values = el.val().split(',');
var gids = [];
var mids = [];
for (var i in values) {
var dv = convertValueToObj(values[i]);
if (dv.entity_type == 'civicrm_group') {
gids.push(dv.entity_id);
}
else if (dv.entity_type == 'civicrm_mailing') {
mids.push(dv.entity_id);
}
}
CRM.api3('Group', 'getlist', { params: { id: { IN: gids } }, extra: ["is_hidden"] }).then(
function(glist) {
CRM.api3('Mailing', 'getlist', { params: { id: { IN: mids } } }).then(
function(mlist) {
var datamap = [];
var groupNames = [];
var civiMails = [];
$(glist.values).each(function (idx, group) {
var key = group.id + ' civicrm_group include';
groupNames.push({id: parseInt(group.id), title: group.label, is_hidden: group.extra.is_hidden});
if (values.indexOf(key) >= 0) {
datamap.push({id: key, text: group.label});
}
key = group.id + ' civicrm_group exclude';
if (values.indexOf(key) >= 0) {
datamap.push({id: key, text: group.label});
}
});
$(mlist.values).each(function (idx, group) {
var key = group.id + ' civicrm_mailing include';
civiMails.push({id: parseInt(group.id), name: group.label});
if (values.indexOf(key) >= 0) {
datamap.push({id: key, text: group.label});
}
key = group.id + ' civicrm_mailing exclude';
if (values.indexOf(key) >= 0) {
datamap.push({id: key, text: group.label});
}
});
scope.$parent.crmMailingConst.groupNames = groupNames;
scope.$parent.crmMailingConst.civiMails = civiMails;
refreshMandatory();
cb(datamap);
});
});
},
ajax: {
url: CRM.url('civicrm/ajax/rest'),
quietMillis: 300,
data: function(input, page_num) {
if (page_num <= 1) {
rcpAjaxState = {
input: input,
entity: 'civicrm_group',
type: 'include',
page_n: 0,
};
}
rcpAjaxState.page_i = page_num - rcpAjaxState.page_n;
var filterParams = {};
switch(rcpAjaxState.entity) {
case 'civicrm_group':
filterParams = { is_hidden: 0, is_active: 1, group_type: {"LIKE": "%2%"} };
break;
case 'civicrm_mailing':
filterParams = { is_hidden: 0, is_active: 1 };
break;
}
var params = {
input: input,
page_num: rcpAjaxState.page_i,
params: filterParams,
};
return params;
},
transport: function(params) {
switch(rcpAjaxState.entity) {
case 'civicrm_group':
CRM.api3('Group', 'getlist', params.data).then(params.success, params.error);
break;
case 'civicrm_mailing':
params.data.params.options = { sort: "is_archived asc, scheduled_date desc" };
CRM.api3('Mailing', 'getlist', params.data).then(params.success, params.error);
break;
}
},
results: function(data) {
results = {
children: $.map(data.values, function(obj) {
return { id: obj.id + ' ' + rcpAjaxState.entity + ' ' + rcpAjaxState.type,
text: obj.label };
})
};
if (rcpAjaxState.page_i == 1 && data.count) {
results.text = ts((rcpAjaxState.type == 'include'? 'Include ' : 'Exclude ') +
(rcpAjaxState.entity == 'civicrm_group'? 'Group' : 'Mailing'));
}
more = data.more_results || !(rcpAjaxState.entity == 'civicrm_mailing' && rcpAjaxState.type == 'exclude');
if (more && !data.more_results) {
if (rcpAjaxState.type == 'include') {
rcpAjaxState.type = 'exclude';
} else {
rcpAjaxState.type = 'include';
rcpAjaxState.entity = 'civicrm_mailing';
}
rcpAjaxState.page_n += rcpAjaxState.page_i;
}
return { more: more, results: [ results ] };
},
},
});
$(element).on('select2-selecting', function(e) {
var option = convertValueToObj(e.val);
var typeKey = option.entity_type == 'civicrm_mailing' ? 'mailings' : 'groups';
if (option.mode == 'exclude') {
ngModel.$viewValue[typeKey].exclude.push(option.entity_id);
arrayRemove(ngModel.$viewValue[typeKey].include, option.entity_id);
}
else {
ngModel.$viewValue[typeKey].include.push(option.entity_id);
arrayRemove(ngModel.$viewValue[typeKey].exclude, option.entity_id);
}
scope.$apply();
$(element).select2('close');
validate();
e.preventDefault();
});
$(element).on("select2-removing", function(e) {
var option = convertValueToObj(e.val);
var typeKey = option.entity_type == 'civicrm_mailing' ? 'mailings' : 'groups';
if (typeKey == 'groups' && isMandatory(option.entity_id)) {
crmUiAlert({
text: ts('This mailing was generated based on search results. The search results cannot be removed.'),
title: ts('Required')
});
e.preventDefault();
return;
}
scope.$parent.$apply(function() {
arrayRemove(ngModel.$viewValue[typeKey][option.mode], option.entity_id);
});
validate();
e.preventDefault();
});
scope.$watchCollection("recips.groups.include", refreshUI);
scope.$watchCollection("recips.groups.exclude", refreshUI);
scope.$watchCollection("recips.mailings.include", refreshUI);
scope.$watchCollection("recips.mailings.exclude", refreshUI);
setTimeout(refreshUI, 50);
scope.$watchCollection(attrs.crmAvailGroups, function() {
scope.groups = scope.$parent.$eval(attrs.crmAvailGroups);
});
scope.$watchCollection(attrs.crmAvailMailings, function() {
scope.mailings = scope.$parent.$eval(attrs.crmAvailMailings);
});
scope.$watchCollection(attrs.crmMandatoryGroups, function() {
refreshMandatory();
});
}
};
});
})(angular, CRM.$, CRM._);

View file

@ -0,0 +1,28 @@
(function(angular, $, _) {
angular.module('crmMailing').directive('crmMailingReviewBool', function() {
return {
scope: {
crmOn: '@',
crmTitle: '@'
},
template: '<span ng-class="spanClasses"><i class="crm-i" ng-class="iconClasses"></i> {{evalTitle}} </span>',
link: function(scope, element, attrs) {
function refresh() {
if (scope.$parent.$eval(attrs.crmOn)) {
scope.spanClasses = {'crmMailing-active': true};
scope.iconClasses = {'fa-check': true};
}
else {
scope.spanClasses = {'crmMailing-inactive': true};
scope.iconClasses = {'fa-times': true};
}
scope.evalTitle = scope.$parent.$eval(attrs.crmTitle);
}
refresh();
scope.$parent.$watch(attrs.crmOn, refresh);
scope.$parent.$watch(attrs.crmTitle, refresh);
}
};
});
})(angular, CRM.$, CRM._);

View file

@ -0,0 +1,17 @@
<div ng-controller="SaveMsgTemplateDialogCtrl">
<p><em>{{ts('Save the current mailing as a template.')}}</em></p>
<div ng-hide="!selected">
<label for="saveopt-mode-update">
<input type="radio" name="mode" ng-model="saveOpt.mode" value="update" id="saveopt-mode-update">
{{ts('Update "%1"', {1: selected.msg_title})}}
</label>
</div>
<div>
<label type="radio" for="saveopt-mode-add">
<input type="radio" name="mode" ng-model="saveOpt.mode" value="add" id="saveopt-mode-add">
{{ts('Save as:')}}
</label>
<input type="text" ng-model="saveOpt.newTitle" ng-click="saveOpt.mode='add'" ng-change="saveOpt.mode='add'" />
</div>
</div>

View file

@ -0,0 +1,83 @@
(function(angular, $, _) {
// Controller for the "Save Message Template" dialog
// Scope members:
// - [input] "model": Object
// - "selected_id": int
// - "tpl": Object
// - "msg_subject": string
// - "msg_text": string
// - "msg_html": string
angular.module('crmMailing').controller('SaveMsgTemplateDialogCtrl', function SaveMsgTemplateDialogCtrl($scope, crmMsgTemplates, dialogService) {
var ts = $scope.ts = CRM.ts(null);
$scope.saveOpt = {mode: '', newTitle: ''};
$scope.selected = null;
$scope.save = function save() {
var tpl = _.extend({}, $scope.model.tpl);
switch ($scope.saveOpt.mode) {
case 'add':
tpl.msg_title = $scope.saveOpt.newTitle;
break;
case 'update':
tpl.id = $scope.selected.id;
tpl.msg_title = $scope.selected.msg_title;
break;
default:
throw 'SaveMsgTemplateDialogCtrl: Unrecognized mode: ' + $scope.saveOpt.mode;
}
return crmMsgTemplates.save(tpl)
.then(function (item) {
CRM.status(ts('Saved'));
return item;
});
};
function scopeApply(f) {
return function () {
var args = arguments;
$scope.$apply(function () {
f.apply(args);
});
};
}
function init() {
crmMsgTemplates.get($scope.model.selected_id).then(
function (tpl) {
$scope.saveOpt.mode = 'update';
$scope.selected = tpl;
},
function () {
$scope.saveOpt.mode = 'add';
$scope.selected = null;
}
);
// When using dialogService with a button bar, the major button actions
// need to be registered with the dialog widget (and not embedded in
// the body of the dialog).
var buttons = [
{
text: ts('Save'),
icons: {primary: 'fa-check'},
click: function () {
$scope.save().then(function (item) {
dialogService.close('saveTemplateDialog', item);
});
}
},
{
text: ts('Cancel'),
icons: {primary: 'fa-times'},
click: function () {
dialogService.cancel('saveTemplateDialog');
}
}
];
dialogService.setButtons('saveTemplateDialog', buttons);
}
setTimeout(scopeApply(init), 0);
});
})(angular, CRM.$, CRM._);

View file

@ -0,0 +1,28 @@
(function(angular, $, _) {
// example: <input name="subject" /> <input crm-mailing-token on-select="doSomething(token.name)" />
// WISHLIST: Instead of global CRM.crmMailing.mailTokens, accept token list as an input
angular.module('crmMailing').directive('crmMailingToken', function() {
return {
require: '^crmUiIdScope',
scope: {
onSelect: '@'
},
template: '<input type="text" class="crmMailingToken" />',
link: function(scope, element, attrs, crmUiIdCtrl) {
$(element).addClass('crm-action-menu fa-code').crmSelect2({
width: "12em",
dropdownAutoWidth: true,
data: CRM.crmMailing.mailTokens,
placeholder: ts('Tokens')
});
$(element).on('select2-selecting', function(e) {
e.preventDefault();
$(element).select2('close').select2('val', '');
scope.$parent.$eval(attrs.onSelect, {
token: {name: e.val}
});
});
}
};
});
})(angular, CRM.$, CRM._);

View file

@ -0,0 +1,56 @@
(function(angular, $, _) {
angular.module('crmMailing').controller('ViewRecipCtrl', function ViewRecipCtrl($scope) {
$scope.getIncludesAsString = function(mailing) {
var first = true;
var names = '';
_.each(mailing.recipients.groups.include, function(id) {
var group = _.where(CRM.crmMailing.groupNames, {id: '' + id});
if (group.length) {
if (!first) {
names = names + ', ';
}
names = names + group[0].title;
first = false;
}
});
_.each(mailing.recipients.mailings.include, function(id) {
var oldMailing = _.where(CRM.crmMailing.civiMails, {id: '' + id});
if (oldMailing.length) {
if (!first) {
names = names + ', ';
}
names = names + oldMailing[0].name;
first = false;
}
});
return names;
};
$scope.getExcludesAsString = function(mailing) {
var first = true;
var names = '';
_.each(mailing.recipients.groups.exclude, function(id) {
var group = _.where(CRM.crmMailing.groupNames, {id: '' + id});
if (group.length) {
if (!first) {
names = names + ', ';
}
names = names + group[0].title;
first = false;
}
});
_.each(mailing.recipients.mailings.exclude, function(id) {
var oldMailing = _.where(CRM.crmMailing.civiMails, {id: '' + id});
if (oldMailing.length) {
if (!first) {
names = names + ', ';
}
names = names + oldMailing[0].name;
first = false;
}
});
return names;
};
});
})(angular, CRM.$, CRM._);

View file

@ -0,0 +1,558 @@
(function (angular, $, _) {
// The representation of from/reply-to addresses is inconsistent in the mailing data-model,
// so the UI must do some adaptation. The crmFromAddresses provides a richer way to slice/dice
// the available "From:" addrs. Records are like the underlying OptionValues -- but add "email"
// and "author".
angular.module('crmMailing').factory('crmFromAddresses', function ($q, crmApi) {
var emailRegex = /^"(.*)" <([^@>]*@[^@>]*)>$/;
var addrs = _.map(CRM.crmMailing.fromAddress, function (addr) {
var match = emailRegex.exec(addr.label);
return angular.extend({}, addr, {
email: match ? match[2] : '(INVALID)',
author: match ? match[1] : '(INVALID)'
});
});
function first(array) {
return (array.length === 0) ? null : array[0];
}
return {
getAll: function getAll() {
return addrs;
},
getByAuthorEmail: function getByAuthorEmail(author, email, autocreate) {
var result = null;
_.each(addrs, function (addr) {
if (addr.author == author && addr.email == email) {
result = addr;
}
});
if (!result && autocreate) {
result = {
label: '(INVALID) "' + author + '" <' + email + '>',
author: author,
email: email
};
addrs.push(result);
}
return result;
},
getByEmail: function getByEmail(email) {
return first(_.where(addrs, {email: email}));
},
getByLabel: function (label) {
return first(_.where(addrs, {label: label}));
},
getDefault: function getDefault() {
return first(_.where(addrs, {is_default: "1"}));
}
};
});
angular.module('crmMailing').factory('crmMsgTemplates', function ($q, crmApi) {
var tpls = _.map(CRM.crmMailing.mesTemplate, function (tpl) {
return angular.extend({}, tpl, {
//id: tpl parseInt(tpl.id)
});
});
window.tpls = tpls;
var lastModifiedTpl = null;
return {
// Get a template
// @param id MessageTemplate id (per APIv3)
// @return Promise MessageTemplate (per APIv3)
get: function get(id) {
return crmApi('MessageTemplate', 'getsingle', {
"return": "id,msg_subject,msg_html,msg_title,msg_text",
"id": id
});
},
// Save a template
// @param tpl MessageTemplate (per APIv3) For new templates, omit "id"
// @return Promise MessageTemplate (per APIv3)
save: function (tpl) {
return crmApi('MessageTemplate', 'create', tpl).then(function (response) {
if (!tpl.id) {
tpl.id = '' + response.id; //parseInt(response.id);
tpls.push(tpl);
}
lastModifiedTpl = tpl;
return tpl;
});
},
// @return Object MessageTemplate (per APIv3)
getLastModifiedTpl: function () {
return lastModifiedTpl;
},
getAll: function getAll() {
return tpls;
}
};
});
// The crmMailingMgr service provides business logic for loading, saving, previewing, etc
angular.module('crmMailing').factory('crmMailingMgr', function ($q, crmApi, crmFromAddresses, crmQueue) {
var qApi = crmQueue(crmApi);
var pickDefaultMailComponent = function pickDefaultMailComponent(type) {
var mcs = _.where(CRM.crmMailing.headerfooterList, {
component_type: type,
is_default: "1"
});
return (mcs.length >= 1) ? mcs[0].id : null;
};
return {
// @param scalar idExpr a number or the literal string 'new'
// @return Promise|Object Mailing (per APIv3)
getOrCreate: function getOrCreate(idExpr) {
return (idExpr == 'new') ? this.create() : this.get(idExpr);
},
// @return Promise Mailing (per APIv3)
get: function get(id) {
var crmMailingMgr = this;
var mailing;
return qApi('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.recipients = {};
mailing.recipients.groups = {include: [], exclude: [], base: []};
mailing.recipients.mailings = {include: [], exclude: []};
_.each(groupResult.values, function (mailingGroup) {
var bucket = (/^civicrm_group/.test(mailingGroup.entity_table)) ? 'groups' : 'mailings';
var entityId = parseInt(mailingGroup.entity_id);
mailing.recipients[bucket][mailingGroup.group_type.toLowerCase()].push(entityId);
});
});
},
// 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(params) {
var defaults = {
jobs: {}, // {jobId: JobRecord}
recipients: {
groups: {include: [], exclude: [], base: []},
mailings: {include: [], exclude: []}
},
template_type: "traditional",
// Workaround CRM-19756 w/template_options.nonce
template_options: {nonce: 1},
name: "",
campaign_id: null,
replyto_email: "",
subject: "",
body_html: "",
body_text: ""
};
return angular.extend({}, defaults, params);
},
// @param mailing Object (per APIv3)
// @return Promise
'delete': function (mailing) {
if (mailing.id) {
return qApi('Mailing', 'delete', {id: mailing.id});
}
else {
var d = $q.defer();
d.resolve();
return d.promise;
}
},
// Search the body, header, and footer for required tokens.
// ex: var msgs = findMissingTokens(mailing, 'body_html');
findMissingTokens: function(mailing, field) {
var missing = {};
if (!_.isEmpty(mailing[field]) && !CRM.crmMailing.disableMandatoryTokensCheck) {
var body = '';
if (mailing.footer_id) {
var footer = _.where(CRM.crmMailing.headerfooterList, {id: mailing.footer_id});
body = body + footer[0][field];
}
body = body + mailing[field];
if (mailing.header_id) {
var header = _.where(CRM.crmMailing.headerfooterList, {id: mailing.header_id});
body = body + header[0][field];
}
angular.forEach(CRM.crmMailing.requiredTokens, function(value, token) {
if (!_.isObject(value)) {
if (body.indexOf('{' + token + '}') < 0) {
missing[token] = value;
}
}
else {
var count = 0;
angular.forEach(value, function(nestedValue, nestedToken) {
if (body.indexOf('{' + nestedToken + '}') >= 0) {
count++;
}
});
if (count === 0) {
angular.extend(missing, value);
}
}
});
}
return missing;
},
// Copy all data fields in (mailingFrom) to (mailingTgt) -- except for (excludes)
// ex: crmMailingMgr.mergeInto(newMailing, mailingTemplate, ['subject']);
mergeInto: function mergeInto(mailingTgt, mailingFrom, excludes) {
var MAILING_FIELDS = [
// always exclude: 'id'
'name',
'campaign_id',
'from_name',
'from_email',
'replyto_email',
'subject',
'dedupe_email',
'recipients',
'body_html',
'body_text',
'footer_id',
'header_id',
'visibility',
'url_tracking',
'dedupe_email',
'forward_replies',
'auto_responder',
'open_tracking',
'override_verp',
'optout_id',
'reply_id',
'resubscribe_id',
'unsubscribe_id'
];
if (!excludes) {
excludes = [];
}
_.each(MAILING_FIELDS, function (field) {
if (!_.contains(excludes, field)) {
mailingTgt[field] = mailingFrom[field];
}
});
},
// @param mailing Object (per APIv3)
// @return Promise an object with "subject", "body_text", "body_html"
preview: function preview(mailing) {
if (CRM.crmMailing.workflowEnabled && !CRM.checkPerm('create mailings') && !CRM.checkPerm('access CiviMail')) {
return qApi('Mailing', 'preview', {id: mailing.id}).then(function(result) {
return result.values;
});
}
else {
// Protect against races in saving and previewing by chaining create+preview.
var params = angular.extend({}, mailing, mailing.recipients, {
options: {force_rollback: 1},
'api.Mailing.preview': {
id: '$value.id'
}
});
delete params.recipients; // the content was merged in
return qApi('Mailing', 'create', params).then(function(result) {
mailing.modified_date = result.values[result.id].modified_date;
// changes rolled back, so we don't care about updating mailing
return result.values[result.id]['api.Mailing.preview'].values;
});
}
},
// @param mailing Object (per APIv3)
// @param int previewLimit
// @return Promise for a list of recipients (mailing_id, contact_id, api.contact.getvalue, api.email.getvalue)
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 = angular.extend({}, mailing, mailing.recipients, {
name: 'placeholder', // for previewing recipients on new, incomplete mailing
subject: 'placeholder', // for previewing recipients on new, incomplete mailing
options: {force_rollback: 1},
'api.mailing_job.create': 1, // note: exact match to API default
'api.MailingRecipients.get': {
mailing_id: '$value.id',
options: {limit: previewLimit},
'api.contact.getvalue': {'return': 'display_name'},
'api.email.getvalue': {'return': 'email'}
}
});
delete params.recipients; // the content was merged in
return qApi('Mailing', 'create', params).then(function (recipResult) {
// changes rolled back, so we don't care about updating mailing
mailing.modified_date = recipResult.values[recipResult.id].modified_date;
return recipResult.values[recipResult.id]['api.MailingRecipients.get'].values;
});
},
previewRecipientCount: function previewRecipientCount(mailing) {
// To get list of recipients, we tentatively save the mailing and
// get the resulting recipients -- then rollback any changes.
var params = angular.extend({}, mailing, mailing.recipients, {
name: 'placeholder', // for previewing recipients on new, incomplete mailing
subject: 'placeholder', // for previewing recipients on new, incomplete mailing
options: {force_rollback: 1},
'api.mailing_job.create': 1, // note: exact match to API default
'api.MailingRecipients.getcount': {
mailing_id: '$value.id'
}
});
delete params.recipients; // the content was merged in
return qApi('Mailing', 'create', params).then(function (recipResult) {
// changes rolled back, so we don't care about updating mailing
mailing.modified_date = recipResult.values[recipResult.id].modified_date;
return recipResult.values[recipResult.id]['api.MailingRecipients.getcount'];
});
},
// Save a (draft) mailing
// @param mailing Object (per APIv3)
// @return Promise
save: function(mailing) {
var params = angular.extend({}, mailing, mailing.recipients);
// Angular ngModel sometimes treats blank fields as undefined.
angular.forEach(mailing, function(value, key) {
if (value === undefined || value === null) {
mailing[key] = '';
}
});
// WORKAROUND: Mailing.create (aka CRM_Mailing_BAO_Mailing::create()) interprets scheduled_date
// as an *intent* to schedule and creates tertiary records. Saving a draft with a scheduled_date
// is therefore not allowed. Remove this after fixing Mailing.create's contract.
delete params.scheduled_date;
delete params.jobs;
delete params.recipients; // the content was merged in
return qApi('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?
mailing.modified_date = result.values[result.id].modified_date;
return mailing;
});
},
// Schedule/send the mailing
// @param mailing Object (per APIv3)
// @return Promise
submit: function (mailing) {
var crmMailingMgr = this;
var params = {
id: mailing.id,
approval_date: 'now',
scheduled_date: mailing.scheduled_date ? mailing.scheduled_date : 'now'
};
return qApi('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 mailing Object (per APIv3)
// @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 = angular.extend({}, mailing, mailing.recipients, {
// options: {force_rollback: 1}, // Test mailings include tracking features, so the mailing must be persistent
'api.Mailing.send_test': {
mailing_id: '$value.id',
test_email: recipient.email,
test_group: recipient.gid
}
});
// WORKAROUND: Mailing.create (aka CRM_Mailing_BAO_Mailing::create()) interprets scheduled_date
// as an *intent* to schedule and creates tertiary records. Saving a draft with a scheduled_date
// is therefore not allowed. Remove this after fixing Mailing.create's contract.
delete params.scheduled_date;
delete params.jobs;
delete params.recipients; // the content was merged in
return qApi('Mailing', 'create', params).then(function (result) {
if (result.id && !mailing.id) {
mailing.id = result.id;
} // no rollback, so update mailing.id
mailing.modified_date = result.values[result.id].modified_date;
return result.values[result.id]['api.Mailing.send_test'].values;
});
}
};
});
// The preview manager performs preview actions while putting up a visible UI (e.g. dialogs & status alerts)
angular.module('crmMailing').factory('crmMailingPreviewMgr', function (dialogService, crmMailingMgr, crmStatus) {
return {
// @param mode string one of 'html', 'text', or 'full'
// @return Promise
preview: function preview(mailing, mode) {
var templates = {
html: '~/crmMailing/PreviewMgr/html.html',
text: '~/crmMailing/PreviewMgr/text.html',
full: '~/crmMailing/PreviewMgr/full.html'
};
var result = null;
var p = crmMailingMgr
.preview(mailing)
.then(function (content) {
var options = CRM.utils.adjustDialogDefaults({
autoOpen: false,
title: ts('Subject: %1', {
1: content.subject
})
});
result = dialogService.open('previewDialog', templates[mode], content, options);
});
crmStatus({start: ts('Previewing...'), success: ''}, p);
return result;
},
// @param to Object with either key "email" (string) or "gid" (int)
// @return Promise
sendTest: function sendTest(mailing, recipient) {
var promise = crmMailingMgr.sendTest(mailing, recipient)
.then(function (deliveryInfos) {
var count = Object.keys(deliveryInfos).length;
if (count === 0) {
CRM.alert(ts('Could not identify any recipients. Perhaps the group is empty?'));
}
})
;
return crmStatus({start: ts('Sending...'), success: ts('Sent')}, promise);
}
};
});
angular.module('crmMailing').factory('crmMailingStats', function (crmApi, crmLegacy) {
var statTypes = [
// {name: 'Recipients', title: ts('Intended Recipients'), searchFilter: '', eventsFilter: '&event=queue', reportType: 'detail', reportFilter: ''},
{name: 'Delivered', title: ts('Successful Deliveries'), searchFilter: '&mailing_delivery_status=Y', eventsFilter: '&event=delivered', reportType: 'detail', reportFilter: '&delivery_status_value=successful'},
{name: 'Opened', title: ts('Tracked Opens'), searchFilter: '&mailing_open_status=Y', eventsFilter: '&event=opened', reportType: 'opened', reportFilter: ''},
{name: 'Unique Clicks', title: ts('Click-throughs'), searchFilter: '&mailing_click_status=Y', eventsFilter: '&event=click&distinct=1', reportType: 'clicks', reportFilter: ''},
// {name: 'Forward', title: ts('Forwards'), searchFilter: '&mailing_forward=1', eventsFilter: '&event=forward', reportType: 'detail', reportFilter: '&is_forwarded_value=1'},
// {name: 'Replies', title: ts('Replies'), searchFilter: '&mailing_reply_status=Y', eventsFilter: '&event=reply', reportType: 'detail', reportFilter: '&is_replied_value=1'},
{name: 'Bounces', title: ts('Bounces'), searchFilter: '&mailing_delivery_status=N', eventsFilter: '&event=bounce', reportType: 'bounce', reportFilter: ''},
{name: 'Unsubscribers', title: ts('Unsubscribes'), searchFilter: '&mailing_unsubscribe=1', eventsFilter: '&event=unsubscribe', reportType: 'detail', reportFilter: '&is_unsubscribed_value=1'},
// {name: 'OptOuts', title: ts('Opt-Outs'), searchFilter: '&mailing_optout=1', eventsFilter: '&event=optout', reportType: 'detail', reportFilter: ''}
];
return {
getStatTypes: function() {
return statTypes;
},
/**
* @param mailingIds object
* List of mailing IDs ({a: 123, b: 456})
* @return Promise
* List of stats for each mailing
* ({a: ...object..., b: ...object...})
*/
getStats: function(mailingIds) {
var params = {};
angular.forEach(mailingIds, function(mailingId, name) {
params[name] = ['Mailing', 'stats', {mailing_id: mailingId, is_distinct: 0}];
});
return crmApi(params).then(function(result) {
var stats = {};
angular.forEach(mailingIds, function(mailingId, name) {
stats[name] = result[name].values[mailingId];
});
return stats;
});
},
/**
* Determine the legacy URL for a report about a given mailing and stat.
*
* @param mailing object
* @param statType object (see statTypes above)
* @param view string ('search', 'event', 'report')
* @param returnPath string|null Return path (relative to Angular base)
* @return string|null
*/
getUrl: function getUrl(mailing, statType, view, returnPath) {
switch (view) {
case 'events':
var retParams = returnPath ? '&context=angPage&angPage=' + returnPath : '';
return crmLegacy.url('civicrm/mailing/report/event',
'reset=1&mid=' + mailing.id + statType.eventsFilter + retParams);
case 'search':
return crmLegacy.url('civicrm/contact/search/advanced',
'force=1&mailing_id=' + mailing.id + statType.searchFilter);
case 'report':
var reportIds = CRM.crmMailing.reportIds;
return crmLegacy.url('civicrm/report/instance/' + reportIds[statType.reportType],
'reset=1&mailing_id_value=' + mailing.id + statType.reportFilter);
default:
return null;
}
}
};
});
// crmMailingSimpleDirective is a template/factory-function for constructing very basic
// directives that accept a "mailing" argument. Please don't overload it. If one continues building
// this, it risks becoming a second system that violates Angular architecture (and prevents one
// from using standard Angular docs+plugins). So this really shouldn't do much -- it is really
// only for simple directives. For something complex, suck it up and write 10 lines of boilerplate.
angular.module('crmMailing').factory('crmMailingSimpleDirective', function ($q, crmMetadata, crmUiHelp) {
return function crmMailingSimpleDirective(directiveName, templateUrl) {
return {
scope: {
crmMailing: '@'
},
templateUrl: templateUrl,
link: function (scope, elm, attr) {
scope.$parent.$watch(attr.crmMailing, function(newValue){
scope.mailing = newValue;
});
scope.crmMailingConst = CRM.crmMailing;
scope.ts = CRM.ts(null);
scope.hs = crmUiHelp({file: 'CRM/Mailing/MailingUI'});
scope[directiveName] = attr[directiveName] ? scope.$parent.$eval(attr[directiveName]) : {};
$q.when(crmMetadata.getFields('Mailing'), function(fields) {
scope.mailingFields = fields;
});
}
};
};
});
})(angular, CRM.$, CRM._);