summaryrefslogtreecommitdiff
path: root/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing
diff options
context:
space:
mode:
Diffstat (limited to 'www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing')
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockApprove.html14
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockApprove.js5
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockHeaderFooter.html32
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockHeaderFooter.js5
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockMailing.html82
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockMailing.js5
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockPreview.html57
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockPreview.js65
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockPublication.html16
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockPublication.js5
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockRecipients.html15
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockRecipients.js5
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockResponses.html82
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockResponses.js5
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockReview.html61
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockReview.js26
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockSchedule.html13
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockSchedule.js5
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockSummary.html29
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockSummary.js5
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockTemplates.html9
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockTemplates.js5
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockTracking.html14
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockTracking.js5
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BodyHtml.html26
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BodyHtml.js5
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BodyText.html24
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BodyText.js5
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/CreateMailingCtrl.js8
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/EditMailingCtrl.js133
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/EditMailingCtrl/2step.html63
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/EditMailingCtrl/base.html8
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/EditMailingCtrl/unified.html50
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/EditMailingCtrl/unified2.html46
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/EditMailingCtrl/wizard.html66
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/EditMailingCtrl/workflow.html73
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/EditRecipCtrl.js136
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/EditRecipOptionsDialogCtrl.html41
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/EditRecipOptionsDialogCtrl.js12
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/EditUnsubGroupCtrl.js19
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/EmailAddrCtrl.js31
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/EmailBodyCtrl.js52
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/EmailBodyCtrl/tokenAlert.html76
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/FromAddress.js30
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/ListMailingsCtrl.js10
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/MsgTemplateCtrl.js44
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/PreviewComponentCtrl.js24
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/PreviewComponentDialogCtrl.html28
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/PreviewComponentDialogCtrl.js13
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/PreviewMailingDialogCtrl.js12
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/PreviewMgr/full.html10
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/PreviewMgr/html.html3
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/PreviewMgr/text.html3
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/PreviewRecipCtrl.html32
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/PreviewRecipCtrl.js10
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/RadioDate.js116
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/Recipients.js341
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/ReviewBool.js28
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/SaveMsgTemplateDialogCtrl.html17
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/SaveMsgTemplateDialogCtrl.js83
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/Templates.js131
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/Token.js28
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/ViewRecipCtrl.js128
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/services.js582
64 files changed, 3112 insertions, 0 deletions
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockApprove.html b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockApprove.html
new file mode 100644
index 00000000..e3971ca7
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockApprove.html
@@ -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>
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockApprove.js b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockApprove.js
new file mode 100644
index 00000000..8f0cd599
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockApprove.js
@@ -0,0 +1,5 @@
+(function(angular, $, _) {
+ angular.module('crmMailing').directive('crmMailingBlockApprove', function(crmMailingSimpleDirective) {
+ return crmMailingSimpleDirective('crmMailingBlockApprove', '~/crmMailing/BlockApprove.html');
+ });
+})(angular, CRM.$, CRM._);
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockHeaderFooter.html b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockHeaderFooter.html
new file mode 100644
index 00000000..249560d7
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockHeaderFooter.html
@@ -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>
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockHeaderFooter.js b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockHeaderFooter.js
new file mode 100644
index 00000000..babba177
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockHeaderFooter.js
@@ -0,0 +1,5 @@
+(function(angular, $, _) {
+ angular.module('crmMailing').directive('crmMailingBlockHeaderFooter', function(crmMailingSimpleDirective) {
+ return crmMailingSimpleDirective('crmMailingBlockHeaderFooter', '~/crmMailing/BlockHeaderFooter.html');
+ });
+})(angular, CRM.$, CRM._);
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockMailing.html b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockMailing.html
new file mode 100644
index 00000000..8e099743
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockMailing.html
@@ -0,0 +1,82 @@
+<!--
+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 crm-mailing-block-templates="{name: 'templates', id: 'subform.msg_template_id'}" crm-mailing="mailing"></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)">
+ <input
+ crm-entityref="{entity: 'Group', api: {params: {is_hidden: 0, is_active: 1}}, select: {allowClear:true, minimumInputLength: 0}}"
+ crm-ui-id="subform.baseGroup"
+ name="baseGroup"
+ ng-model="mailing.recipients.groups.base[0]"
+ ng-required="true"
+ />
+ </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>
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockMailing.js b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockMailing.js
new file mode 100644
index 00000000..a2297d02
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockMailing.js
@@ -0,0 +1,5 @@
+(function(angular, $, _) {
+ angular.module('crmMailing').directive('crmMailingBlockMailing', function(crmMailingSimpleDirective) {
+ return crmMailingSimpleDirective('crmMailingBlockMailing', '~/crmMailing/BlockMailing.html');
+ });
+})(angular, CRM.$, CRM._);
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockPreview.html b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockPreview.html
new file mode 100644
index 00000000..6315466e
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockPreview.html
@@ -0,0 +1,57 @@
+<!--
+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="text"
+ class="crm-form-text"
+ ng-model="testContact.email"
+ placeholder="example@example.org"
+ crm-multiple-email
+ />
+ </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})" class="crmMailing-btn-primary">{{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>
+ <input
+ crm-entityref="{entity: 'Group', api: {params: {is_hidden: 0, is_active: 1}}, select: {allowClear:true, minimumInputLength: 0}}"
+ ng-model="testGroup.gid"
+ class="crm-action-menu fa-envelope-o"
+ />
+ </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})" class="crmMailing-btn-primary">{{ts('Send test')}}</button>
+ </div>
+ <div class="clear"></div>
+</div>
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockPreview.js b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockPreview.js
new file mode 100644
index 00000000..5e582dc0
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockPreview.js
@@ -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) {
+ recipient = JSON.parse(JSON.stringify(recipient).replace(/\,\s/g, ','));
+ 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);
+ CRM.api3({
+ contact: ['contact', 'get', {group: scope.testGroup.gid, options: {limit: 0}, return: 'display_name,email'}],
+ group: ['group', 'getsingle', {id: scope.testGroup.gid, return: 'title'}]
+ }).done(function(data) {
+ $dialog.dialog('option', 'title', ts('Send to %1', {1: data.group.title}));
+ var count = 0,
+ // Fixme: should this be in a template?
+ markup = '<ol>';
+ _.each(data.contact.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.contact.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._);
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockPublication.html b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockPublication.html
new file mode 100644
index 00000000..6e82b9fa
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockPublication.html
@@ -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>
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockPublication.js b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockPublication.js
new file mode 100644
index 00000000..8bb02579
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockPublication.js
@@ -0,0 +1,5 @@
+(function(angular, $, _) {
+ angular.module('crmMailing').directive('crmMailingBlockPublication', function (crmMailingSimpleDirective) {
+ return crmMailingSimpleDirective('crmMailingBlockPublication', '~/crmMailing/BlockPublication.html');
+ });
+})(angular, CRM.$, CRM._);
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockRecipients.html b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockRecipients.html
new file mode 100644
index 00000000..cedfa6dc
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockRecipients.html
@@ -0,0 +1,15 @@
+<div ng-controller="EditRecipCtrl" class="crm-mailing-recipients-row">
+ <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 ng-style="{display: permitRecipientRebuild ? '' : 'inline-block'}">
+ <button ng-click="rebuildRecipients()" ng-show="permitRecipientRebuild" class="crm-button" title="{{ts('Click to refresh recipient count')}}">{{getRecipientsEstimate()}}</button>
+ <a ng-click="previewRecipients()" class="crm-hover-button" title="{{ts('Preview a List of Recipients')}}" style="font-weight: bold;">{{getRecipientCount()}}</a>
+ </div>
+</div>
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockRecipients.js b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockRecipients.js
new file mode 100644
index 00000000..fdb45313
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockRecipients.js
@@ -0,0 +1,5 @@
+(function(angular, $, _) {
+ angular.module('crmMailing').directive('crmMailingBlockRecipients', function(crmMailingSimpleDirective) {
+ return crmMailingSimpleDirective('crmMailingBlockRecipients', '~/crmMailing/BlockRecipients.html');
+ });
+})(angular, CRM.$, CRM._);
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockResponses.html b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockResponses.html
new file mode 100644
index 00000000..cf56f007
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockResponses.html
@@ -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>
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockResponses.js b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockResponses.js
new file mode 100644
index 00000000..ba7d7897
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockResponses.js
@@ -0,0 +1,5 @@
+(function(angular, $, _) {
+ angular.module('crmMailing').directive('crmMailingBlockResponses', function(crmMailingSimpleDirective) {
+ return crmMailingSimpleDirective('crmMailingBlockResponses', '~/crmMailing/BlockResponses.html');
+ });
+})(angular, CRM.$, CRM._);
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockReview.html b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockReview.html
new file mode 100644
index 00000000..5a0021e4
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockReview.html
@@ -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()">{{getRecipientCount()}}</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>
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockReview.js b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockReview.js
new file mode 100644
index 00000000..94968e3e
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockReview.js
@@ -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._);
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockSchedule.html b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockSchedule.html
new file mode 100644
index 00000000..75cf19b2
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockSchedule.html
@@ -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>
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockSchedule.js b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockSchedule.js
new file mode 100644
index 00000000..251449d7
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockSchedule.js
@@ -0,0 +1,5 @@
+(function(angular, $, _) {
+ angular.module('crmMailing').directive('crmMailingBlockSchedule', function(crmMailingSimpleDirective) {
+ return crmMailingSimpleDirective('crmMailingBlockSchedule', '~/crmMailing/BlockSchedule.html');
+ });
+})(angular, CRM.$, CRM._);
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockSummary.html b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockSummary.html
new file mode 100644
index 00000000..6050e448
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockSummary.html
@@ -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>
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockSummary.js b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockSummary.js
new file mode 100644
index 00000000..d16d9fc3
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockSummary.js
@@ -0,0 +1,5 @@
+(function(angular, $, _) {
+ angular.module('crmMailing').directive('crmMailingBlockSummary', function(crmMailingSimpleDirective) {
+ return crmMailingSimpleDirective('crmMailingBlockSummary', '~/crmMailing/BlockSummary.html');
+ });
+})(angular, CRM.$, CRM._);
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockTemplates.html b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockTemplates.html
new file mode 100644
index 00000000..38a37a91
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockTemplates.html
@@ -0,0 +1,9 @@
+<div ng-controller="MsgTemplateCtrl" class="crm-mailing-templates-row">
+ <input
+ type="hidden"
+ crm-mailing-templates
+ ng-model="mailing.msg_template_id"
+ crm-ui-id="{{crmMailingBlockTemplates.id}}"
+ name="{{crmMailingBlockTemplates.name}}" />
+ <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> \ No newline at end of file
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockTemplates.js b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockTemplates.js
new file mode 100644
index 00000000..9ee2efbf
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockTemplates.js
@@ -0,0 +1,5 @@
+(function(angular, $, _) {
+ angular.module('crmMailing').directive('crmMailingBlockTemplates', function(crmMailingSimpleDirective) {
+ return crmMailingSimpleDirective('crmMailingBlockTemplates', '~/crmMailing/BlockTemplates.html');
+ });
+})(angular, CRM.$, CRM._); \ No newline at end of file
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockTracking.html b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockTracking.html
new file mode 100644
index 00000000..a4ed95e0
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockTracking.html
@@ -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>
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockTracking.js b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockTracking.js
new file mode 100644
index 00000000..b8502b23
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockTracking.js
@@ -0,0 +1,5 @@
+(function(angular, $, _) {
+ angular.module('crmMailing').directive('crmMailingBlockTracking', function(crmMailingSimpleDirective) {
+ return crmMailingSimpleDirective('crmMailingBlockTracking', '~/crmMailing/BlockTracking.html');
+ });
+})(angular, CRM.$, CRM._);
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BodyHtml.html b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BodyHtml.html
new file mode 100644
index 00000000..b38c28c7
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BodyHtml.html
@@ -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>
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BodyHtml.js b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BodyHtml.js
new file mode 100644
index 00000000..242f530c
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BodyHtml.js
@@ -0,0 +1,5 @@
+(function(angular, $, _) {
+ angular.module('crmMailing').directive('crmMailingBodyHtml', function(crmMailingSimpleDirective) {
+ return crmMailingSimpleDirective('crmMailingBodyHtml', '~/crmMailing/BodyHtml.html');
+ });
+})(angular, CRM.$, CRM._);
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BodyText.html b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BodyText.html
new file mode 100644
index 00000000..598c7792
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BodyText.html
@@ -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>
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BodyText.js b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BodyText.js
new file mode 100644
index 00000000..0f04491c
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BodyText.js
@@ -0,0 +1,5 @@
+(function(angular, $, _) {
+ angular.module('crmMailing').directive('crmMailingBodyText', function(crmMailingSimpleDirective) {
+ return crmMailingSimpleDirective('crmMailingBodyText', '~/crmMailing/BodyText.html');
+ });
+})(angular, CRM.$, CRM._);
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/CreateMailingCtrl.js b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/CreateMailingCtrl.js
new file mode 100644
index 00000000..08a61713
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/CreateMailingCtrl.js
@@ -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._);
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/EditMailingCtrl.js b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/EditMailingCtrl.js
new file mode 100644
index 00000000..91f6db3f
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/EditMailingCtrl.js
@@ -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._);
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/EditMailingCtrl/2step.html b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/EditMailingCtrl/2step.html
new file mode 100644
index 00000000..cabd6f30
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/EditMailingCtrl/2step.html
@@ -0,0 +1,63 @@
+<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 crmMailing-btn-primary" 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')"
+ class="crmMailing-btn-danger-outline"
+ 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)" class="crmMailing-btn-secondary-outline">{{ts('Save Draft')}}</button>
+ </span>
+ </div>
+ </div>
+</div>
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/EditMailingCtrl/base.html b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/EditMailingCtrl/base.html
new file mode 100644
index 00000000..92a97b24
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/EditMailingCtrl/base.html
@@ -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> \ No newline at end of file
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/EditMailingCtrl/unified.html b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/EditMailingCtrl/unified.html
new file mode 100644
index 00000000..cc3056fa
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/EditMailingCtrl/unified.html
@@ -0,0 +1,50 @@
+<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" class="crmMailing-btn-primary" ng-disabled="block.check() || crmMailingSubform.$invalid" ng-click="submit()">{{ts('Submit Mailing')}}</button>
+ <button crm-icon="fa-floppy-o" class="crmMailing-btn-secondary-outline" ng-disabled="block.check()" ng-click="save().then(leave)">{{ts('Save Draft')}}</button>
+ <button
+ crm-icon="fa-trash"
+ ng-show="checkPerm('delete in CiviMail')"
+ class="crmMailing-btn-danger-outline"
+ 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>
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/EditMailingCtrl/unified2.html b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/EditMailingCtrl/unified2.html
new file mode 100644
index 00000000..1506b8d5
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/EditMailingCtrl/unified2.html
@@ -0,0 +1,46 @@
+<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" class="crmMailing-btn-primary" ng-disabled="block.check() || crmMailingSubform.$invalid" ng-click="submit()">{{ts('Submit Mailing')}}</button>
+ <button crm-icon="fa-floppy-o" class="crmMailing-secondary-outline" ng-disabled="block.check()" ng-click="save().then(leave)">{{ts('Save Draft')}}</button>
+ <button
+ crm-icon="fa-trash"
+ ng-show="checkPerm('delete in CiviMail')"
+ class="crmMailing-btn-danger-outline"
+ 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>
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/EditMailingCtrl/wizard.html b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/EditMailingCtrl/wizard.html
new file mode 100644
index 00000000..9854cc5c
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/EditMailingCtrl/wizard.html
@@ -0,0 +1,66 @@
+<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 crmMailing-btn-primary" 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')"
+ class="crmMailing-btn-danger-outline"
+ 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" class="crmMailing-btn-secondary-outline" ng-disabled="block.check()" ng-click="save().then(leave)">{{ts('Save Draft')}}</button>
+ </span>
+ </div>
+ </div>
+</div>
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/EditMailingCtrl/workflow.html b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/EditMailingCtrl/workflow.html
new file mode 100644
index 00000000..affa76d8
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/EditMailingCtrl/workflow.html
@@ -0,0 +1,73 @@
+<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 crmMailing-btn-primary" 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 crmMailing-btn-primary" 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')"
+ class="crmMailing-btn-danger-outline"
+ 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)" class="crmMailing-btn-secondary-outline">{{ts('Save Draft')}}</button>
+ </span>
+ </div>
+ </div>
+</div>
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/EditRecipCtrl.js b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/EditRecipCtrl.js
new file mode 100644
index 00000000..9fca637a
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/EditRecipCtrl.js
@@ -0,0 +1,136 @@
+(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, crmMailingCache) {
+ // Time to wait before triggering AJAX update to recipients list
+ var RECIPIENTS_DEBOUNCE_MS = 100;
+ var SETTING_DEBOUNCE_MS = 5000;
+ 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.outdated = null;
+ $scope.permitRecipientRebuild = null;
+
+ $scope.getRecipientsEstimate = function() {
+ var ts = $scope.ts;
+ if ($scope.recipients === null) {
+ return ts('Estimating...');
+ }
+ if ($scope.recipients === 0) {
+ return ts('Estimate recipient count');
+ }
+ return ts('Refresh recipient count');
+ };
+
+ $scope.getRecipientCount = function() {
+ var ts = $scope.ts;
+ if ($scope.recipients === 0) {
+ return ts('No Recipients');
+ }
+ else if ($scope.recipients > 0) {
+ return ts('~%1 recipients', {1 : $scope.recipients});
+ }
+ else if ($scope.outdated) {
+ return ts('(unknown)');
+ }
+ else {
+ return $scope.permitRecipientRebuild ? ts('(unknown)') : ts('Estimating...');
+ }
+ };
+
+ // 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() {
+ if (!$scope.mailing) {
+ return;
+ }
+ crmMailingMgr.previewRecipientCount($scope.mailing, crmMailingCache, !$scope.permitRecipientRebuild).then(function(recipients) {
+ $scope.outdated = ($scope.permitRecipientRebuild && _.difference($scope.mailing.recipients, crmMailingCache.get('mailing-' + $scope.mailing.id + '-recipient-params')) !== 0);
+ $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);
+
+ // refresh setting at a duration on 5sec
+ var refreshSetting = _.debounce(function() {
+ $scope.$apply(function() {
+ crmApi('Setting', 'getvalue', {"name": 'auto_recipient_rebuild', "return": "value"}).then(function(response) {
+ $scope.permitRecipientRebuild = (response.result === 0);
+ });
+ });
+ }, SETTING_DEBOUNCE_MS);
+ $scope.$watchCollection("permitRecipientRebuild", refreshSetting);
+
+ $scope.previewRecipients = function previewRecipients() {
+ var model = {
+ count: $scope.recipients,
+ sample: crmMailingCache.get('mailing-' + $scope.mailing.id + '-recipient-list'),
+ sampleLimit: RECIPIENTS_PREVIEW_LIMIT
+ };
+ var options = CRM.utils.adjustDialogDefaults({
+ width: '40%',
+ autoOpen: false,
+ title: ts('Preview (%1)', {1: $scope.getRecipientCount()})
+ });
+
+ // don't open preview dialog if there is no recipient to show.
+ if ($scope.recipients !== 0 && !$scope.outdated) {
+ if (!_.isEmpty(model.sample)) {
+ dialogService.open('recipDialog', '~/crmMailing/PreviewRecipCtrl.html', model, options);
+ }
+ else {
+ return crmStatus({start: ts('Previewing...'), success: ''}, crmMailingMgr.previewRecipients($scope.mailing, RECIPIENTS_PREVIEW_LIMIT).then(function(recipients) {
+ model.sample = recipients;
+ dialogService.open('recipDialog', '~/crmMailing/PreviewRecipCtrl.html', model, options);
+ }));
+ }
+ }
+ };
+
+ $scope.rebuildRecipients = function rebuildRecipients() {
+ // setting null will put 'Estimating..' text on refresh button
+ $scope.recipients = null;
+ return crmMailingMgr.previewRecipientCount($scope.mailing, crmMailingCache, true).then(function(recipients) {
+ $scope.outdated = (recipients === 0) ? true : false;
+ $scope.recipients = recipients;
+ });
+ };
+
+ // 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._);
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/EditRecipOptionsDialogCtrl.html b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/EditRecipOptionsDialogCtrl.html
new file mode 100644
index 00000000..be5e2c2e
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/EditRecipOptionsDialogCtrl.html
@@ -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>
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/EditRecipOptionsDialogCtrl.js b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/EditRecipOptionsDialogCtrl.js
new file mode 100644
index 00000000..43734e34
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/EditRecipOptionsDialogCtrl.js
@@ -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._);
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/EditUnsubGroupCtrl.js b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/EditUnsubGroupCtrl.js
new file mode 100644
index 00000000..56070e03
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/EditUnsubGroupCtrl.js
@@ -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._);
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/EmailAddrCtrl.js b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/EmailAddrCtrl.js
new file mode 100644
index 00000000..942bcfe1
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/EmailAddrCtrl.js
@@ -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._);
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/EmailBodyCtrl.js b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/EmailBodyCtrl.js
new file mode 100644
index 00000000..7db0b7ec
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/EmailBodyCtrl.js
@@ -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._);
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/EmailBodyCtrl/tokenAlert.html b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/EmailBodyCtrl/tokenAlert.html
new file mode 100644
index 00000000..46f3eac7
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/EmailBodyCtrl/tokenAlert.html
@@ -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>
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/FromAddress.js b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/FromAddress.js
new file mode 100644
index 00000000..aae6499f
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/FromAddress.js
@@ -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._);
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/ListMailingsCtrl.js b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/ListMailingsCtrl.js
new file mode 100644
index 00000000..e60ffe54
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/ListMailingsCtrl.js
@@ -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._);
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/MsgTemplateCtrl.js b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/MsgTemplateCtrl.js
new file mode 100644
index 00000000..5d200c3f
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/MsgTemplateCtrl.js
@@ -0,0 +1,44 @@
+(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.msg_template_id = tpl.id;
+ mailing.subject = tpl.msg_subject;
+ mailing.body_text = tpl.msg_text;
+ mailing.body_html = tpl.msg_html;
+ });
+ };
+ });
+
+})(angular, CRM.$, CRM._);
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/PreviewComponentCtrl.js b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/PreviewComponentCtrl.js
new file mode 100644
index 00000000..3fd928a1
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/PreviewComponentCtrl.js
@@ -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._);
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/PreviewComponentDialogCtrl.html b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/PreviewComponentDialogCtrl.html
new file mode 100644
index 00000000..71db7027
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/PreviewComponentDialogCtrl.html
@@ -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>
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/PreviewComponentDialogCtrl.js b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/PreviewComponentDialogCtrl.js
new file mode 100644
index 00000000..2b1d9f2c
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/PreviewComponentDialogCtrl.js
@@ -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._);
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/PreviewMailingDialogCtrl.js b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/PreviewMailingDialogCtrl.js
new file mode 100644
index 00000000..9e339b53
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/PreviewMailingDialogCtrl.js
@@ -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._);
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/PreviewMgr/full.html b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/PreviewMgr/full.html
new file mode 100644
index 00000000..0e257a11
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/PreviewMgr/full.html
@@ -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> \ No newline at end of file
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/PreviewMgr/html.html b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/PreviewMgr/html.html
new file mode 100644
index 00000000..c47b1c02
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/PreviewMgr/html.html
@@ -0,0 +1,3 @@
+<div ng-controller="PreviewMailingDialogCtrl">
+ <iframe crm-ui-iframe="model.body_html"></iframe>
+</div> \ No newline at end of file
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/PreviewMgr/text.html b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/PreviewMgr/text.html
new file mode 100644
index 00000000..246add4b
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/PreviewMgr/text.html
@@ -0,0 +1,3 @@
+<div ng-controller="PreviewMailingDialogCtrl">
+ <pre>{{model.body_text}}</pre>
+</div> \ No newline at end of file
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/PreviewRecipCtrl.html b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/PreviewRecipCtrl.html
new file mode 100644
index 00000000..6eb6459a
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/PreviewRecipCtrl.html
@@ -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>
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/PreviewRecipCtrl.js b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/PreviewRecipCtrl.js
new file mode 100644
index 00000000..371fb8ef
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/PreviewRecipCtrl.js
@@ -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._);
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/RadioDate.js b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/RadioDate.js
new file mode 100644
index 00000000..037b6e22
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/RadioDate.js
@@ -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._);
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/Recipients.js b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/Recipients.js
new file mode 100644
index 00000000..4203d4a3
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/Recipients.js
@@ -0,0 +1,341 @@
+(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 = 0; i < values.length; i++) {
+ 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);
+ }
+ }
+ // push non existant 0 group/mailing id in order when no recipents group or prior mailing is selected
+ // this will allow to resuse the below code to handle datamap
+ if (gids.length === 0) {
+ gids.push(0);
+ }
+ if (mids.length === 0) {
+ mids.push(0);
+ }
+
+ CRM.api3('Group', 'getlist', { params: { id: { IN: gids }, options: { limit: 0 } }, extra: ["is_hidden"] } ).then(
+ function(glist) {
+ CRM.api3('Mailing', 'getlist', { params: { id: { IN: mids }, options: { limit: 0 } } }).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,
+ };
+
+ if('civicrm_mailing' === rcpAjaxState.entity) {
+ params["api.MailingRecipients.getcount"] = {};
+ }
+
+ 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) {
+ if('civicrm_mailing' === rcpAjaxState.entity) {
+ return obj["api.MailingRecipients.getcount"] > 0 ? { id: obj.id + ' ' + rcpAjaxState.entity + ' ' + rcpAjaxState.type,
+ text: obj.label } : '';
+ }
+ else {
+ return { id: obj.id + ' ' + rcpAjaxState.entity + ' ' + rcpAjaxState.type,
+ text: obj.label };
+ }
+ })
+ };
+
+ if (rcpAjaxState.page_i == 1 && data.count && results.children.length > 0) {
+ 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._);
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/ReviewBool.js b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/ReviewBool.js
new file mode 100644
index 00000000..98ec85c5
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/ReviewBool.js
@@ -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._);
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/SaveMsgTemplateDialogCtrl.html b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/SaveMsgTemplateDialogCtrl.html
new file mode 100644
index 00000000..1e5c723f
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/SaveMsgTemplateDialogCtrl.html
@@ -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>
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/SaveMsgTemplateDialogCtrl.js b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/SaveMsgTemplateDialogCtrl.js
new file mode 100644
index 00000000..ea80522e
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/SaveMsgTemplateDialogCtrl.js
@@ -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._);
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/Templates.js b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/Templates.js
new file mode 100644
index 00000000..0c2b93dc
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/Templates.js
@@ -0,0 +1,131 @@
+(function(angular, $, _) {
+ // example <select crm-mailing-templates crm-mailing="mymailing"></select>
+ angular.module('crmMailing').directive('crmMailingTemplates', function(crmUiAlert) {
+ return {
+ restrict: 'AE',
+ require: 'ngModel',
+ scope: {
+ ngRequired: '@'
+ },
+ link: function(scope, element, attrs, ngModel) {
+ scope.template = ngModel.$viewValue;
+
+ var refreshUI = ngModel.$render = function refresuhUI() {
+ scope.template = ngModel.$viewValue;
+ if (ngModel.$viewValue) {
+ $(element).select2('val', ngModel.$viewValue);
+ }
+ };
+
+ // @return string HTML representing an option
+ function formatItem(item) {
+ if (!item.id) {
+ // return `text` for optgroup
+ return item.text;
+ }
+ return '<span class="crmMailing-template">' + item.text + '</span>';
+ }
+
+ var rcpAjaxState = {
+ input: '',
+ entity: 'civicrm_msg_templates',
+ page_n: 0,
+ page_i: 0,
+ };
+
+ $(element).select2({
+ width: '36em',
+ placeholder: "<i class='fa fa-clipboard'></i> Mailing Templates",
+ formatResult: formatItem,
+ escapeMarkup: function(m) {
+ return m;
+ },
+ multiple: false,
+ initSelection: function(el, cb) {
+
+ var value = el.val();
+
+ CRM.api3('MessageTemplate', 'getlist', { params: { id: value }, label_field: 'msg_title' }).then(function(tlist) {
+
+ var template = {};
+
+ if (tlist.count) {
+ $(tlist.values).each(function(id, val) {
+ template.id = val.id;
+ template.text = val.label;
+ });
+ }
+
+ cb(template);
+ });
+ },
+ ajax: {
+ url: CRM.url('civicrm/ajax/rest'),
+ quietMillis: 300,
+ data: function(input, page_num) {
+ if (page_num <= 1) {
+ rcpAjaxState = {
+ input: input,
+ entity: 'civicrm_msg_templates',
+ page_n: 0,
+ };
+ }
+
+ rcpAjaxState.page_i = page_num - rcpAjaxState.page_n;
+ var filterParams = { is_active: 1, workflow_id: { "IS NULL": 1 } };
+
+ var params = {
+ input: input,
+ page_num: rcpAjaxState.page_i,
+ label_field: 'msg_title',
+ search_field: 'msg_title',
+ params: filterParams,
+ };
+ return params;
+ },
+ transport: function(params) {
+ CRM.api3('MessageTemplate', 'getlist', params.data).then(params.success, params.error);
+ },
+ results: function(data) {
+
+ results = {
+ children: $.map(data.values, function(obj) {
+ return { id: obj.id, text: obj.label };
+ })
+ };
+
+ if (rcpAjaxState.page_i == 1 && data.count) {
+ results.text = ts('Message Templates');
+ }
+
+ more = data.more_results;
+
+ if (more && !data.more_results) {
+ rcpAjaxState.page_n += rcpAjaxState.page_i;
+ }
+
+ return { more: more, results: [ results ] };
+ },
+ }
+ });
+
+ $(element).on('select2-selecting', function(e) {
+ // in here is where the template HTML should be loaded
+ var entity_id = parseInt(e.val);
+ ngModel.$viewValue = entity_id;
+
+ scope.$parent.loadTemplate(scope.$parent.$parent.mailing, entity_id);
+ scope.$apply();
+ $(element).select2('close');
+ e.preventDefault();
+ });
+
+
+ scope.$watchCollection("template", refreshUI);
+ setTimeout(refreshUI, 50);
+ }
+ };
+
+
+ });
+})(angular, CRM.$, CRM._);
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/Token.js b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/Token.js
new file mode 100644
index 00000000..71131d21
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/Token.js
@@ -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._);
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/ViewRecipCtrl.js b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/ViewRecipCtrl.js
new file mode 100644
index 00000000..d72793fd
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/ViewRecipCtrl.js
@@ -0,0 +1,128 @@
+(function(angular, $, _) {
+
+ angular.module('crmMailing').controller('ViewRecipCtrl', function ViewRecipCtrl($scope) {
+ var mids = [];
+ var gids = [];
+ var groupNames = [];
+ var mailings = [];
+ var civimailings = [];
+ var civimails = [];
+
+ function getGroupNames(mailing) {
+ if (-1 == mailings.indexOf(mailing.id)) {
+ mailings.push(mailing.id);
+ _.each(mailing.recipients.groups.include, function(id) {
+ if (-1 == gids.indexOf(id)) {
+ gids.push(id);
+ }
+ });
+ _.each(mailing.recipients.groups.exclude, function(id) {
+ if (-1 == gids.indexOf(id)) {
+ gids.push(id);
+ }
+ });
+ _.each(mailing.recipients.groups.base, function(id) {
+ if (-1 == gids.indexOf(id)) {
+ gids.push(id);
+ }
+ });
+ if (!_.isEmpty(gids)) {
+ CRM.api3('Group', 'get', {'id': {"IN": gids}}).then(function(result) {
+ _.each(result.values, function(grp) {
+ if (_.isEmpty(_.where(groupNames, {id: parseInt(grp.id)}))) {
+ groupNames.push({id: parseInt(grp.id), title: grp.title, is_hidden: grp.is_hidden});
+ }
+ });
+ CRM.crmMailing.groupNames = groupNames;
+ $scope.$parent.crmMailingConst.groupNames = groupNames;
+ });
+ }
+ }
+ }
+
+ function getCiviMails(mailing) {
+ if (-1 == civimailings.indexOf(mailing.id)) {
+ civimailings.push(mailing.id);
+ _.each(mailing.recipients.mailings.include, function(id) {
+ if (-1 == mids.indexOf(id)) {
+ mids.push(id);
+ }
+ });
+ _.each(mailing.recipients.mailings.exclude, function(id) {
+ if (-1 == mids.indexOf(id)) {
+ mids.push(id);
+ }
+ });
+ if (!_.isEmpty(mids)) {
+ CRM.api3('Mailing', 'get', {'id': {"IN": mids}}).then(function(result) {
+ _.each(result.values, function(mail) {
+ if (_.isEmpty(_.where(civimails, {id: parseInt(mail.id)}))) {
+ civimails.push({id: parseInt(mail.id), name: mail.label});
+ }
+ });
+ CRM.crmMailing.civiMails = civimails;
+ $scope.$parent.crmMailingConst.civiMails = civimails;
+ });
+ }
+ }
+ }
+
+ $scope.getIncludesAsString = function(mailing) {
+ var first = true;
+ var names = '';
+ if (_.isEmpty(CRM.crmMailing.groupNames)) {
+ getGroupNames(mailing);
+ }
+ if (_.isEmpty(CRM.crmMailing.civiMails)) {
+ getCiviMails(mailing);
+ }
+ _.each(mailing.recipients.groups.include, function(id) {
+ var group = _.where(CRM.crmMailing.groupNames, {id: parseInt(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: parseInt(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: parseInt(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: parseInt(id)});
+ if (oldMailing.length) {
+ if (!first) {
+ names = names + ', ';
+ }
+ names = names + oldMailing[0].name;
+ first = false;
+ }
+ });
+ return names;
+ };
+ });
+
+})(angular, CRM.$, CRM._);
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/services.js b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/services.js
new file mode 100644
index 00000000..45c20637
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/services.js
@@ -0,0 +1,582 @@
+(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) {
+ return this.getPreviewContent(qApi, mailing);
+ },
+
+ // @param backend
+ // @param mailing Object (per APIv3)
+ // @return preview content
+ getPreviewContent: function getPreviewContent(backend, mailing) {
+ if (CRM.crmMailing.workflowEnabled && !CRM.checkPerm('create mailings') && !CRM.checkPerm('access CiviMail')) {
+ return backend('Mailing', 'preview', {id: mailing.id}).then(function(result) {
+ return result.values;
+ });
+ }
+ else {
+ var params = angular.extend({}, mailing);
+ delete params.id;
+ return backend('Mailing', 'preview', params).then(function(result) {
+ // changes rolled back, so we don't care about updating mailing
+ return result.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.recipients, {
+ id: mailing.id,
+ 'api.MailingRecipients.get': {
+ mailing_id: '$value.id',
+ options: {limit: previewLimit},
+ 'api.contact.getvalue': {'return': 'display_name'},
+ 'api.email.getvalue': {'return': 'email'}
+ }
+ });
+ delete params.scheduled_date;
+ 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, crmMailingCache, rebuild) {
+ var cachekey = 'mailing-' + mailing.id + '-recipient-count';
+ var recipientCount = crmMailingCache.get(cachekey);
+ if (rebuild || _.isEmpty(recipientCount)) {
+ // 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, {
+ id: mailing.id,
+ 'api.MailingRecipients.getcount': {
+ mailing_id: '$value.id'
+ }
+ });
+ // if this service is executed on rebuild then also fetch the recipients list
+ if (rebuild) {
+ params = angular.extend(params, {
+ 'api.MailingRecipients.get': {
+ mailing_id: '$value.id',
+ options: {limit: 50},
+ 'api.contact.getvalue': {'return': 'display_name'},
+ 'api.email.getvalue': {'return': 'email'}
+ }
+ });
+ crmMailingCache.put('mailing-' + mailing.id + '-recipient-params', params.recipients);
+ }
+ delete params.scheduled_date;
+ delete params.recipients; // the content was merged in
+ recipientCount = 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;
+ if (rebuild) {
+ crmMailingCache.put('mailing-' + mailing.id + '-recipient-list', recipResult.values[recipResult.id]['api.MailingRecipients.get'].values);
+ }
+ return recipResult.values[recipResult.id]['api.MailingRecipients.getcount'];
+ });
+ crmMailingCache.put(cachekey, recipientCount);
+ }
+
+ return recipientCount;
+ },
+
+ // 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
+ params._skip_evil_bao_auto_recipients_ = 1; // skip recipient rebuild on simple save
+ 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
+
+ params._skip_evil_bao_auto_recipients_ = 1; // skip recipient rebuild while sending test mail
+
+ 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
+ .getPreviewContent(CRM.api3, 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.module('crmMailing').factory('crmMailingCache', ['$cacheFactory', function($cacheFactory) {
+ return $cacheFactory('crmMailingCache');
+ }]);
+
+})(angular, CRM.$, CRM._);