diff options
Diffstat (limited to 'www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing')
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._); |