First commit
This commit is contained in:
commit
c6e2478c40
13918 changed files with 2303184 additions and 0 deletions
14
sites/all/modules/civicrm/ang/crmMailing/BlockApprove.html
Normal file
14
sites/all/modules/civicrm/ang/crmMailing/BlockApprove.html
Normal file
|
@ -0,0 +1,14 @@
|
|||
<div class="crm-block" ng-form="apprForm" crm-ui-id-scope>
|
||||
<div class="crm-group">
|
||||
<div crm-ui-field="{title: ts('Status')}">
|
||||
{{mailingFields.approval_status_id.optionsMap[mailing.approval_status_id] || ts('Unreviewed')}}
|
||||
</div>
|
||||
<div crm-ui-field="{name: 'apprForm.approval_note', title: ts('Note')}">
|
||||
<textarea
|
||||
crm-ui-id="apprForm.approval_note"
|
||||
name="approval_note"
|
||||
ng-model="mailing.approval_note"
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
5
sites/all/modules/civicrm/ang/crmMailing/BlockApprove.js
Normal file
5
sites/all/modules/civicrm/ang/crmMailing/BlockApprove.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
(function(angular, $, _) {
|
||||
angular.module('crmMailing').directive('crmMailingBlockApprove', function(crmMailingSimpleDirective) {
|
||||
return crmMailingSimpleDirective('crmMailingBlockApprove', '~/crmMailing/BlockApprove.html');
|
||||
});
|
||||
})(angular, CRM.$, CRM._);
|
|
@ -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>
|
|
@ -0,0 +1,5 @@
|
|||
(function(angular, $, _) {
|
||||
angular.module('crmMailing').directive('crmMailingBlockHeaderFooter', function(crmMailingSimpleDirective) {
|
||||
return crmMailingSimpleDirective('crmMailingBlockHeaderFooter', '~/crmMailing/BlockHeaderFooter.html');
|
||||
});
|
||||
})(angular, CRM.$, CRM._);
|
97
sites/all/modules/civicrm/ang/crmMailing/BlockMailing.html
Normal file
97
sites/all/modules/civicrm/ang/crmMailing/BlockMailing.html
Normal file
|
@ -0,0 +1,97 @@
|
|||
<!--
|
||||
Controller: EditMailingCtrl
|
||||
Required vars: mailing, crmMailingConst
|
||||
Note: Much of this file is duplicated in crmMailing and crmMailingAB with variations on placement/title/binding.
|
||||
It could perhaps be thinned by 30-60% by making more directives.
|
||||
-->
|
||||
<div class="crm-block" ng-form="subform" crm-ui-id-scope>
|
||||
<div class="crm-group">
|
||||
<div crm-ui-field="{name: 'subform.msg_template_id', title: ts('Template')}">
|
||||
<div ng-controller="MsgTemplateCtrl">
|
||||
<select
|
||||
crm-ui-id="subform.msg_template_id"
|
||||
name="msg_template_id"
|
||||
class="fa-clipboard"
|
||||
crm-ui-select="{dropdownAutoWidth : true, allowClear: true, placeholder: ts('Message Template')}"
|
||||
ng-model="mailing.msg_template_id"
|
||||
ng-change="loadTemplate(mailing, mailing.msg_template_id)"
|
||||
>
|
||||
<option value=""></option>
|
||||
<option ng-repeat="frm in crmMsgTemplates.getAll() | orderBy:'msg_title'" ng-value="frm.id">{{frm.msg_title}}</option>
|
||||
</select>
|
||||
<a crm-icon="fa-floppy-o" ng-if="checkPerm('edit message templates')" ng-click="saveTemplate(mailing)" class="crm-hover-button" title="{{ts('Save As')}}"></a>
|
||||
</div>
|
||||
</div>
|
||||
<div crm-ui-field="{name: 'subform.fromAddress', title: ts('From'), help: hs('from_email')}">
|
||||
<div ng-controller="EmailAddrCtrl" crm-mailing-from-address="fromPlaceholder" crm-mailing="mailing">
|
||||
<select
|
||||
crm-ui-id="subform.fromAddress"
|
||||
crm-ui-select="{dropdownAutoWidth : true, allowClear: false, placeholder: ts('Email address')}"
|
||||
name="fromAddress"
|
||||
ng-model="fromPlaceholder.label"
|
||||
required>
|
||||
<option value=""></option>
|
||||
<option ng-repeat="frm in crmFromAddresses.getAll() | filter:{is_active:1} | orderBy:'weight'" value="{{frm.label}}">{{frm.label}}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div crm-ui-field="{name: 'subform.replyTo', title: ts('Reply-To')}" ng-show="crmMailingConst.enableReplyTo">
|
||||
<div ng-controller="EmailAddrCtrl">
|
||||
<select
|
||||
crm-ui-id="subform.replyTo"
|
||||
crm-ui-select="{dropdownAutoWidth : true, allowClear: true, placeholder: ts('Email address')}"
|
||||
name="replyTo"
|
||||
ng-change="checkReplyToChange(mailing)"
|
||||
ng-model="mailing.replyto_email"
|
||||
>
|
||||
<option value=""></option>
|
||||
<option ng-repeat="frm in crmFromAddresses.getAll() | filter:{is_active:1} | orderBy:'weight'" value="{{frm.label}}">{{frm.label}}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div crm-ui-field="{name: 'subform.recipients', title: ts('Recipients'), required: true}">
|
||||
<div crm-mailing-block-recipients="{name: 'recipients', id: 'subform.recipients'}" crm-mailing="mailing" cm-ui-id="subform.recipients"></div>
|
||||
</div>
|
||||
<span ng-controller="EditUnsubGroupCtrl">
|
||||
<div crm-ui-field="{name: 'subform.baseGroup', title: ts('Unsubscribe Group')}" ng-if="isUnsubGroupRequired(mailing)">
|
||||
<select
|
||||
crm-ui-id="subform.baseGroup"
|
||||
crm-ui-select
|
||||
name="baseGroup"
|
||||
ng-model="mailing.recipients.groups.base[0]"
|
||||
ng-required="true"
|
||||
>
|
||||
<option ng-repeat="grp in crmMailingConst.testGroupNames | filter:{is_hidden:0} | orderBy:'title'" value="{{grp.id}}">{{grp.title}}</option>
|
||||
</select>
|
||||
</div>
|
||||
</span>
|
||||
<div crm-ui-field="{name: 'subform.subject', title: ts('Subject')}">
|
||||
<div style="float: right;">
|
||||
<input crm-mailing-token on-select="$broadcast('insert:subject', token.name)" tabindex="-1"/>
|
||||
</div>
|
||||
<input
|
||||
crm-ui-id="subform.subject"
|
||||
crm-ui-insert-rx="insert:subject"
|
||||
type="text"
|
||||
class="crm-form-text"
|
||||
ng-model="mailing.subject"
|
||||
required
|
||||
placeholder="Subject"
|
||||
name="subject" />
|
||||
</div>
|
||||
<div ng-if="crmMailingConst.isMultiLingual">
|
||||
<div crm-ui-field="{name: 'subform.language', title: ts('Language')}">
|
||||
<select
|
||||
crm-ui-id="subform.language"
|
||||
crm-ui-select="{dropdownAutoWidth : true, allowClear: false, placeholder: ts('- choose language -')}"
|
||||
name="language"
|
||||
ng-model="mailing.language"
|
||||
required
|
||||
>
|
||||
<option value=""></option>
|
||||
<option ng-repeat="(key,val) in crmMailingConst.enabledLanguages" value="{{key}}">{{val}}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
5
sites/all/modules/civicrm/ang/crmMailing/BlockMailing.js
Normal file
5
sites/all/modules/civicrm/ang/crmMailing/BlockMailing.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
(function(angular, $, _) {
|
||||
angular.module('crmMailing').directive('crmMailingBlockMailing', function(crmMailingSimpleDirective) {
|
||||
return crmMailingSimpleDirective('crmMailingBlockMailing', '~/crmMailing/BlockMailing.html');
|
||||
});
|
||||
})(angular, CRM.$, CRM._);
|
61
sites/all/modules/civicrm/ang/crmMailing/BlockPreview.html
Normal file
61
sites/all/modules/civicrm/ang/crmMailing/BlockPreview.html
Normal file
|
@ -0,0 +1,61 @@
|
|||
<!--
|
||||
Vars: mailing:obj, testContact:obj, testGroup:obj, crmMailing:FormController
|
||||
-->
|
||||
<div class="crmMailing-preview">
|
||||
<!-- Note:
|
||||
In Firefox (at least), clicking the preview buttons causes the browser to display validation warnings
|
||||
for unrelated fields *and* display preview. To avoid this weird UX, we disable preview buttons when the form is incomplete/invalid.
|
||||
-->
|
||||
<div class="preview-popup">
|
||||
<div ng-show="!mailing.body_html && !mailing.body_text">
|
||||
<em>({{ts('No content to preview')}})</em>
|
||||
</div>
|
||||
<div ng-hide="!mailing.body_html">
|
||||
<a class="crm-hover-button action-item" crm-icon="fa-television" ng-disabled="crmMailing.$invalid" ng-click="doPreview('html')">{{ts('Preview as HTML')}}</a>
|
||||
</div>
|
||||
<div ng-hide="!mailing.body_html && !mailing.body_text" style="margin-top: 1em;">
|
||||
<a class="crm-hover-button action-item" crm-icon="fa-file-text-o" ng-disabled="crmMailing.$invalid" ng-click="doPreview('text')">{{ts('Preview as Plain Text')}}</a>
|
||||
</div>
|
||||
<!--
|
||||
<div ng-hide="!mailing.body_html && !mailing.body_text">
|
||||
<button ng-disabled="crmMailing.$invalid" ng-click="doPreview('full')">{{ts('Preview')}}</button>
|
||||
</div>
|
||||
-->
|
||||
</div>
|
||||
<div class="preview-contact" ng-form="">
|
||||
<div>
|
||||
{{ts('Send test email to:')}}
|
||||
<a crm-ui-help="hs({id: 'test', title: ts('Test Email')})"></a>
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
name="preview_test_email"
|
||||
type="email"
|
||||
class="crm-form-text"
|
||||
ng-model="testContact.email"
|
||||
placeholder="example@example.org"
|
||||
/>
|
||||
</div>
|
||||
<button crm-icon="fa-paper-plane" title="{{crmMailing.$invalid || !testContact.email ? ts('Complete all required fields first') : ts('Send test message to %1', {1: testContact.email})}}" ng-disabled="crmMailing.$invalid || !testContact.email" ng-click="doSend({email: testContact.email})">{{ts('Send test')}}</button>
|
||||
</div>
|
||||
<div class="preview-group" ng-form="">
|
||||
<div>
|
||||
{{ts('Send test email to group:')}}
|
||||
<a crm-ui-help="hs({id: 'test', title: ts('Test Email')})"></a>
|
||||
</div>
|
||||
<div>
|
||||
<select
|
||||
name="preview_test_group"
|
||||
ui-jq="crmSelect2"
|
||||
ui-options="{dropdownAutoWidth : true, allowClear: true, placeholder: ts('Select Group')}"
|
||||
ng-model="testGroup.gid"
|
||||
ng-options="group.id as group.title for group in crmMailingConst.testGroupNames|orderBy:'title'"
|
||||
class="crm-action-menu fa-envelope-o"
|
||||
>
|
||||
<option value=""></option>
|
||||
</select>
|
||||
</div>
|
||||
<button crm-icon="fa-paper-plane" title="{{crmMailing.$invalid || !testGroup.gid ? ts('Complete all required fields first') : ts('Send test message to group')}}" ng-disabled="crmMailing.$invalid || !testGroup.gid" crm-confirm="{resizable: true, width: '40%', height: '40%', open: previewTestGroup}" on-yes="doSend({gid: testGroup.gid})">{{ts('Send test')}}</button>
|
||||
</div>
|
||||
<div class="clear"></div>
|
||||
</div>
|
65
sites/all/modules/civicrm/ang/crmMailing/BlockPreview.js
Normal file
65
sites/all/modules/civicrm/ang/crmMailing/BlockPreview.js
Normal file
|
@ -0,0 +1,65 @@
|
|||
(function(angular, $, _) {
|
||||
// example: <div crm-mailing-block-preview crm-mailing="myMailing" on-preview="openPreview(myMailing, preview.mode)" on-send="sendEmail(myMailing,preview.recipient)">
|
||||
// note: the directive defines a variable called "preview" with any inputs supplied by the user (e.g. the target recipient for an example mailing)
|
||||
|
||||
angular.module('crmMailing').directive('crmMailingBlockPreview', function(crmUiHelp) {
|
||||
return {
|
||||
templateUrl: '~/crmMailing/BlockPreview.html',
|
||||
link: function(scope, elm, attr) {
|
||||
scope.$watch(attr.crmMailing, function(newValue) {
|
||||
scope.mailing = newValue;
|
||||
});
|
||||
scope.crmMailingConst = CRM.crmMailing;
|
||||
scope.ts = CRM.ts(null);
|
||||
scope.hs = crmUiHelp({file: 'CRM/Mailing/MailingUI'});
|
||||
scope.testContact = {email: CRM.crmMailing.defaultTestEmail};
|
||||
scope.testGroup = {gid: null};
|
||||
|
||||
scope.doPreview = function(mode) {
|
||||
scope.$eval(attr.onPreview, {
|
||||
preview: {mode: mode}
|
||||
});
|
||||
};
|
||||
scope.doSend = function doSend(recipient) {
|
||||
scope.$eval(attr.onSend, {
|
||||
preview: {recipient: recipient}
|
||||
});
|
||||
};
|
||||
|
||||
scope.previewTestGroup = function(e) {
|
||||
var $dialog = $(this);
|
||||
$dialog.html('<div class="crm-loading-element"></div>').parent().find('button[data-op=yes]').prop('disabled', true);
|
||||
$dialog.dialog('option', 'title', ts('Send to %1', {1: _.pluck(_.where(scope.crmMailingConst.testGroupNames, {id: scope.testGroup.gid}), 'title')[0]}));
|
||||
CRM.api3('contact', 'get', {
|
||||
group: scope.testGroup.gid,
|
||||
options: {limit: 0},
|
||||
return: 'display_name,email'
|
||||
}).done(function(data) {
|
||||
var count = 0,
|
||||
// Fixme: should this be in a template?
|
||||
markup = '<ol>';
|
||||
_.each(data.values, function(row) {
|
||||
// Fixme: contact api doesn't seem capable of filtering out contacts with no email, so we're doing it client-side
|
||||
if (row.email) {
|
||||
count++;
|
||||
markup += '<li>' + row.display_name + ' - ' + row.email + '</li>';
|
||||
}
|
||||
});
|
||||
markup += '</ol>';
|
||||
markup = '<h4>' + ts('A test message will be sent to %1 people:', {1: count}) + '</h4>' + markup;
|
||||
if (!count) {
|
||||
markup = '<div class="messages status"><i class="crm-i fa-exclamation-triangle"></i> ' +
|
||||
(data.count ? ts('None of the contacts in this group have an email address.') : ts('Group is empty.')) +
|
||||
'</div>';
|
||||
}
|
||||
$dialog
|
||||
.html(markup)
|
||||
.trigger('crmLoad')
|
||||
.parent().find('button[data-op=yes]').prop('disabled', !count);
|
||||
});
|
||||
};
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
})(angular, CRM.$, CRM._);
|
|
@ -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>
|
|
@ -0,0 +1,5 @@
|
|||
(function(angular, $, _) {
|
||||
angular.module('crmMailing').directive('crmMailingBlockPublication', function (crmMailingSimpleDirective) {
|
||||
return crmMailingSimpleDirective('crmMailingBlockPublication', '~/crmMailing/BlockPublication.html');
|
||||
});
|
||||
})(angular, CRM.$, CRM._);
|
|
@ -0,0 +1,16 @@
|
|||
<div ng-controller="EditRecipCtrl" class="crm-mailing-recipients-row">
|
||||
<div style="float: right;">
|
||||
<div class="crmMailing-recip-est">
|
||||
<a href="" ng-click="previewRecipients()" title="{{ts('Preview a List of Recipients')}}">{{getRecipientsEstimate()}}</a>
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
type="hidden"
|
||||
crm-mailing-recipients
|
||||
ng-model="mailing.recipients"
|
||||
crm-mandatory-groups="crmMailingConst.groupNames | filter:{is_hidden:1}"
|
||||
crm-ui-id="{{crmMailingBlockRecipients.id}}"
|
||||
name="{{crmMailingBlockRecipients.name}}"
|
||||
ng-required="true"/>
|
||||
<a crm-icon="fa-wrench" ng-click="editOptions(mailing)" class="crm-hover-button" title="{{ts('Edit Recipient Options')}}"></a>
|
||||
</div>
|
|
@ -0,0 +1,5 @@
|
|||
(function(angular, $, _) {
|
||||
angular.module('crmMailing').directive('crmMailingBlockRecipients', function(crmMailingSimpleDirective) {
|
||||
return crmMailingSimpleDirective('crmMailingBlockRecipients', '~/crmMailing/BlockRecipients.html');
|
||||
});
|
||||
})(angular, CRM.$, CRM._);
|
82
sites/all/modules/civicrm/ang/crmMailing/BlockResponses.html
Normal file
82
sites/all/modules/civicrm/ang/crmMailing/BlockResponses.html
Normal file
|
@ -0,0 +1,82 @@
|
|||
<!--
|
||||
Controller: EditMailingCtrl
|
||||
Required vars: mailing, crmMailingConst
|
||||
-->
|
||||
<div class="crm-block" ng-form="responseForm" crm-ui-id-scope>
|
||||
<div class="crm-group">
|
||||
<div crm-ui-field="{title: ts('Track Replies'), help: hs('override_verp')}" crm-layout="checkbox">
|
||||
<!-- Comparing data-model and UI of "override_verp", note that true/false are inverted (enabled==0,disabled==1) -->
|
||||
<span ng-controller="EmailAddrCtrl">
|
||||
<input
|
||||
name="override_verp"
|
||||
type="checkbox"
|
||||
ng-change="checkVerpChange(mailing)"
|
||||
ng-model="mailing.override_verp"
|
||||
ng-true-value="'0'"
|
||||
ng-false-value="'1'"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<div crm-ui-field="{title: ts('Forward Replies'), help: hs('forward_replies')}" crm-layout="checkbox" ng-show="'0' == mailing.override_verp">
|
||||
<input name="forward_replies" type="checkbox" ng-model="mailing.forward_replies" ng-true-value="'1'" ng-false-value="'0'" />
|
||||
</div>
|
||||
<div crm-ui-field="{title: ts('Auto-Respond to Replies'), help: hs('auto_responder')}" crm-layout="checkbox" ng-show="'0' == mailing.override_verp">
|
||||
<input name="auto_responder" type="checkbox" ng-model="mailing.auto_responder" ng-true-value="'1'" ng-false-value="'0'" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr/>
|
||||
|
||||
<div class="crm-block" ng-form="subform" crm-ui-id-scope>
|
||||
<div class="crm-group">
|
||||
<div crm-ui-field="{name: 'subform.reply_id', title: ts('Auto-Respond Message')}" ng-show="'0' == mailing.override_verp && '1' == mailing.auto_responder">
|
||||
<select
|
||||
crm-ui-id="subform.reply_id"
|
||||
name="reply_id"
|
||||
ui-jq="select2"
|
||||
ui-options="{dropdownAutoWidth : true}"
|
||||
ng-model="mailing.reply_id"
|
||||
ng-options="mc.id as mc.name for mc in crmMailingConst.headerfooterList | filter:{component_type: 'Reply'}"
|
||||
required>
|
||||
<option value=""></option>
|
||||
</select>
|
||||
</div>
|
||||
<div crm-ui-field="{name: 'subform.optout_id', title: ts('Opt-out Message')}">
|
||||
<select
|
||||
crm-ui-id="subform.optout_id"
|
||||
name="optout_id"
|
||||
ui-jq="select2"
|
||||
ui-options="{dropdownAutoWidth : true}"
|
||||
ng-model="mailing.optout_id"
|
||||
ng-options="mc.id as mc.name for mc in crmMailingConst.headerfooterList | filter:{component_type: 'OptOut'}"
|
||||
required>
|
||||
<option value=""></option>
|
||||
</select>
|
||||
</div>
|
||||
<div crm-ui-field="{name: 'subform.resubscribe_id', title: ts('Resubscribe Message')}">
|
||||
<select
|
||||
crm-ui-id="subform.resubscribe_id"
|
||||
name="resubscribe_id"
|
||||
ui-jq="select2"
|
||||
ui-options="{dropdownAutoWidth : true}"
|
||||
ng-model="mailing.resubscribe_id"
|
||||
ng-options="mc.id as mc.name for mc in crmMailingConst.headerfooterList | filter:{component_type: 'Resubscribe'}"
|
||||
required>
|
||||
<option value=""></option>
|
||||
</select>
|
||||
</div>
|
||||
<div crm-ui-field="{name: 'subform.unsubscribe_id', title: ts('Unsubscribe Message')}">
|
||||
<select
|
||||
crm-ui-id="subform.unsubscribe_id"
|
||||
name="unsubscribe_id"
|
||||
ui-jq="select2"
|
||||
ui-options="{dropdownAutoWidth : true}"
|
||||
ng-model="mailing.unsubscribe_id"
|
||||
ng-options="mc.id as mc.name for mc in crmMailingConst.headerfooterList | filter:{component_type: 'Unsubscribe'}"
|
||||
required>
|
||||
<option value=""></option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,5 @@
|
|||
(function(angular, $, _) {
|
||||
angular.module('crmMailing').directive('crmMailingBlockResponses', function(crmMailingSimpleDirective) {
|
||||
return crmMailingSimpleDirective('crmMailingBlockResponses', '~/crmMailing/BlockResponses.html');
|
||||
});
|
||||
})(angular, CRM.$, CRM._);
|
61
sites/all/modules/civicrm/ang/crmMailing/BlockReview.html
Normal file
61
sites/all/modules/civicrm/ang/crmMailing/BlockReview.html
Normal file
|
@ -0,0 +1,61 @@
|
|||
<!--
|
||||
Controller: EditMailingCtrl
|
||||
Required vars: mailing, attachments
|
||||
-->
|
||||
<div>
|
||||
<div class="crm-block" ng-form="reviewForm" crm-ui-id-scope>
|
||||
<div class="crm-group">
|
||||
<div crm-ui-field="{title: ts('Mailing Name')}">
|
||||
{{mailing.name}}
|
||||
</div>
|
||||
<div crm-ui-field="{title: ts('Recipients')}">
|
||||
<div ng-controller="ViewRecipCtrl">
|
||||
<div ng-controller="EditRecipCtrl">
|
||||
<div><a crm-icon="fa-users" class="crm-hover-button action-item" ng-click="previewRecipients()">{{getRecipientsEstimate()}}</a></div>
|
||||
<div ng-show="getIncludesAsString(mailing)">
|
||||
(<strong>{{ts('Include:')}}</strong> {{getIncludesAsString(mailing)}})
|
||||
</div>
|
||||
<div ng-show="getExcludesAsString(mailing)">
|
||||
(<strong>{{ts('Exclude:')}}</strong> <s>{{getExcludesAsString(mailing)}}</s>)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div crm-ui-field="{title: ts('Content')}">
|
||||
<span ng-show="mailing.body_html"><a crm-icon="fa-television" class="crm-hover-button action-item" ng-click="previewMailing(mailing, 'html')">{{ts('HTML')}}</a></span>
|
||||
<span ng-show="mailing.body_html || mailing.body_text"><a crm-icon="fa-file-text-o" class="crm-hover-button action-item" ng-click="previewMailing(mailing, 'text')">{{ts('Plain Text')}}</a></span>
|
||||
</div>
|
||||
<div crm-ui-field="{title: ts('Attachments')}">
|
||||
<div ng-repeat="file in attachments.files">
|
||||
<a ng-href="{{file.url}}" target="_blank">{{file.name}}</a>
|
||||
</div>
|
||||
<div ng-repeat="item in attachments.uploader.queue">
|
||||
{{item.file.name}}
|
||||
</div>
|
||||
<div ng-show="!attachments.files.length && !attachments.uploader.queue.length"><em>{{ts('None')}}</em></div>
|
||||
</div>
|
||||
<div ng-if="crmMailingConst.isMultiLingual" crm-ui-field="{title: ts('Language')}">
|
||||
{{crmMailingConst.enabledLanguages[mailing.language]}}
|
||||
</div>
|
||||
<div crm-ui-field="{title: ts('Tracking')}">
|
||||
<span crm-mailing-review-bool crm-on="mailing.url_tracking=='1'" crm-title="ts('Click-Throughs')"></span>
|
||||
<span crm-mailing-review-bool crm-on="mailing.open_tracking=='1'" crm-title="ts('Opens')"></span>
|
||||
</div>
|
||||
<div crm-ui-field="{title: ts('Responding')}">
|
||||
<div>
|
||||
<span crm-mailing-review-bool crm-on="mailing.override_verp=='0'" crm-title="ts('Track Replies')"></span>
|
||||
<span crm-mailing-review-bool crm-on="mailing.override_verp=='0' && mailing.forward_replies=='1'" crm-title="ts('Forward Replies')"></span>
|
||||
</div>
|
||||
<div ng-controller="PreviewComponentCtrl">
|
||||
<span ng-show="mailing.override_verp == '0' && mailing.auto_responder"><a crm-icon="fa-envelope" class="crm-hover-button action-item" ng-click="previewComponent(ts('Auto-Respond'), mailing.reply_id)">{{ts('Auto-Respond')}}</a></span>
|
||||
<span><a crm-icon="fa-envelope" class="crm-hover-button action-item" ng-click="previewComponent(ts('Opt-out'), mailing.optout_id)">{{ts('Opt-out')}}</a></span>
|
||||
<span><a crm-icon="fa-envelope" class="crm-hover-button action-item" ng-click="previewComponent(ts('Resubscribe'), mailing.resubscribe_id)">{{ts('Resubscribe')}}</a></span>
|
||||
<span><a crm-icon="fa-envelope" class="crm-hover-button action-item" ng-click="previewComponent(ts('Unsubscribe'), mailing.unsubscribe_id)">{{ts('Unsubscribe')}}</a></span>
|
||||
</div>
|
||||
</div>
|
||||
<div crm-ui-field="{title: ts('Publication')}">
|
||||
{{mailing.visibility}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
26
sites/all/modules/civicrm/ang/crmMailing/BlockReview.js
Normal file
26
sites/all/modules/civicrm/ang/crmMailing/BlockReview.js
Normal file
|
@ -0,0 +1,26 @@
|
|||
(function(angular, $, _) {
|
||||
|
||||
angular.module('crmMailing').directive('crmMailingBlockReview', function (crmMailingPreviewMgr) {
|
||||
return {
|
||||
scope: {
|
||||
crmMailing: '@',
|
||||
crmMailingAttachments: '@'
|
||||
},
|
||||
templateUrl: '~/crmMailing/BlockReview.html',
|
||||
link: function (scope, elm, attr) {
|
||||
scope.$parent.$watch(attr.crmMailing, function(newValue){
|
||||
scope.mailing = newValue;
|
||||
});
|
||||
scope.$parent.$watch(attr.crmMailingAttachments, function(newValue){
|
||||
scope.attachments = newValue;
|
||||
});
|
||||
scope.crmMailingConst = CRM.crmMailing;
|
||||
scope.ts = CRM.ts(null);
|
||||
scope.previewMailing = function previewMailing(mailing, mode) {
|
||||
return crmMailingPreviewMgr.preview(mailing, mode);
|
||||
};
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
})(angular, CRM.$, CRM._);
|
13
sites/all/modules/civicrm/ang/crmMailing/BlockSchedule.html
Normal file
13
sites/all/modules/civicrm/ang/crmMailing/BlockSchedule.html
Normal file
|
@ -0,0 +1,13 @@
|
|||
<div class="crmMailing-schedule-outer" crm-mailing-radio-date="schedule" ng-model="mailing.scheduled_date">
|
||||
<div class="crmMailing-schedule-inner">
|
||||
<div>
|
||||
<input ng-model="schedule.mode" type="radio" name="send" value="now" id="schedule-send-now"/>
|
||||
<label for="schedule-send-now">{{ts('Send immediately')}}</label>
|
||||
</div>
|
||||
<div>
|
||||
<input ng-model="schedule.mode" type="radio" name="send" value="at" id="schedule-send-at"/>
|
||||
<label for="schedule-send-at">{{ts('Send at:')}}</label>
|
||||
<input crm-ui-datepicker ng-model="schedule.datetime" ng-required="schedule.mode == 'at'"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,5 @@
|
|||
(function(angular, $, _) {
|
||||
angular.module('crmMailing').directive('crmMailingBlockSchedule', function(crmMailingSimpleDirective) {
|
||||
return crmMailingSimpleDirective('crmMailingBlockSchedule', '~/crmMailing/BlockSchedule.html');
|
||||
});
|
||||
})(angular, CRM.$, CRM._);
|
29
sites/all/modules/civicrm/ang/crmMailing/BlockSummary.html
Normal file
29
sites/all/modules/civicrm/ang/crmMailing/BlockSummary.html
Normal file
|
@ -0,0 +1,29 @@
|
|||
<!--
|
||||
Controller: EditMailingCtrl
|
||||
Required vars: mailing, crmMailingConst
|
||||
FIXME: Don't hardcode table-based layout!
|
||||
-->
|
||||
<div class="crm-block" ng-form="subform" crm-ui-id-scope>
|
||||
<div class="crm-group">
|
||||
<div crm-ui-field="{name: 'subform.mailingName', title: ts('Mailing Name'), help: hs('name')}">
|
||||
<div>
|
||||
<input
|
||||
crm-ui-id="subform.mailingName"
|
||||
type="text"
|
||||
class="crm-form-text"
|
||||
ng-model="mailing.name"
|
||||
placeholder="Mailing Name"
|
||||
required
|
||||
name="mailingName" />
|
||||
</div>
|
||||
</div>
|
||||
<div crm-ui-field="{name: 'subform.campaign', title: ts('Campaign'), help: hs({id: 'id-campaign_id', file: 'CRM/Campaign/Form/addCampaignToComponent'})}" ng-show="crmMailingConst.campaignEnabled">
|
||||
<input
|
||||
crm-entityref="{entity: 'Campaign', select: {allowClear: true, placeholder: ts('Select Campaign')}}"
|
||||
crm-ui-id="subform.campaign"
|
||||
name="campaign"
|
||||
ng-model="mailing.campaign_id"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
5
sites/all/modules/civicrm/ang/crmMailing/BlockSummary.js
Normal file
5
sites/all/modules/civicrm/ang/crmMailing/BlockSummary.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
(function(angular, $, _) {
|
||||
angular.module('crmMailing').directive('crmMailingBlockSummary', function(crmMailingSimpleDirective) {
|
||||
return crmMailingSimpleDirective('crmMailingBlockSummary', '~/crmMailing/BlockSummary.html');
|
||||
});
|
||||
})(angular, CRM.$, CRM._);
|
14
sites/all/modules/civicrm/ang/crmMailing/BlockTracking.html
Normal file
14
sites/all/modules/civicrm/ang/crmMailing/BlockTracking.html
Normal file
|
@ -0,0 +1,14 @@
|
|||
<!--
|
||||
Controller: EditMailingCtrl
|
||||
Required vars: mailing
|
||||
-->
|
||||
<div class="crm-block" ng-form="subform" crm-ui-id-scope>
|
||||
<div class="crm-group">
|
||||
<div crm-ui-field="{name: 'subform.url_tracking', title: ts('Track Click-Throughs'), help: hs('url_tracking')}" crm-layout="checkbox">
|
||||
<input crm-ui-id="subform.url_tracking" name="url_tracking" type="checkbox" ng-model="mailing.url_tracking" ng-true-value="'1'" ng-false-value="'0'" />
|
||||
</div>
|
||||
<div crm-ui-field="{name: 'subform.open_tracking', title: ts('Track Opens'), help: hs('open_tracking')}" crm-layout="checkbox">
|
||||
<input crm-ui-id="subform.open_tracking" name="open_tracking" type="checkbox" ng-model="mailing.open_tracking" ng-true-value="'1'" ng-false-value="'0'" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,5 @@
|
|||
(function(angular, $, _) {
|
||||
angular.module('crmMailing').directive('crmMailingBlockTracking', function(crmMailingSimpleDirective) {
|
||||
return crmMailingSimpleDirective('crmMailingBlockTracking', '~/crmMailing/BlockTracking.html');
|
||||
});
|
||||
})(angular, CRM.$, CRM._);
|
26
sites/all/modules/civicrm/ang/crmMailing/BodyHtml.html
Normal file
26
sites/all/modules/civicrm/ang/crmMailing/BodyHtml.html
Normal file
|
@ -0,0 +1,26 @@
|
|||
<!--
|
||||
Required vars: mailing
|
||||
-->
|
||||
<div ng-form="htmlForm" crm-ui-id-scope>
|
||||
<div ng-controller="EmailBodyCtrl">
|
||||
<div style="float: right;">
|
||||
<input crm-mailing-token on-select="$broadcast('insert:body_html', token.name)" tabindex="-1" style="z-index:1">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<textarea
|
||||
crm-ui-id="htmlForm.body_html"
|
||||
crm-ui-richtext
|
||||
name="body_html"
|
||||
crm-ui-insert-rx="insert:body_html"
|
||||
ng-model="mailing.body_html"
|
||||
ng-blur="checkTokens(mailing, 'body_html', 'insert:body_html')"
|
||||
data-preset="civimail"
|
||||
></textarea>
|
||||
<span ng-model="body_html_tokens" crm-ui-validate="hasAllTokens(mailing, 'body_html')"></span>
|
||||
<div ng-show="htmlForm.$error.crmUiValidate" class="crmMailing-error-link">
|
||||
{{ts('Required tokens are missing.')}} <a class="helpicon" ng-click="checkTokens(mailing, 'body_html', 'insert:body_html')"></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
5
sites/all/modules/civicrm/ang/crmMailing/BodyHtml.js
Normal file
5
sites/all/modules/civicrm/ang/crmMailing/BodyHtml.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
(function(angular, $, _) {
|
||||
angular.module('crmMailing').directive('crmMailingBodyHtml', function(crmMailingSimpleDirective) {
|
||||
return crmMailingSimpleDirective('crmMailingBodyHtml', '~/crmMailing/BodyHtml.html');
|
||||
});
|
||||
})(angular, CRM.$, CRM._);
|
24
sites/all/modules/civicrm/ang/crmMailing/BodyText.html
Normal file
24
sites/all/modules/civicrm/ang/crmMailing/BodyText.html
Normal file
|
@ -0,0 +1,24 @@
|
|||
<!--
|
||||
Required vars: mailing, crmMailingConst
|
||||
-->
|
||||
<div ng-form="textForm" crm-ui-id-scope>
|
||||
<div ng-controller="EmailBodyCtrl">
|
||||
<div style="float: right;">
|
||||
<input crm-mailing-token on-select="$broadcast('insert:body_text', token.name)" tabindex="-1"/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<textarea
|
||||
crm-ui-id="textForm.body_text"
|
||||
crm-ui-insert-rx="insert:body_text"
|
||||
name="body_text"
|
||||
ng-model="mailing.body_text"
|
||||
ng-blur="checkTokens(mailing, 'body_text', 'insert:body_text')"
|
||||
></textarea>
|
||||
<span ng-model="body_text_tokens" crm-ui-validate="hasAllTokens(mailing, 'body_text')"></span>
|
||||
<div ng-show="textForm.$error.crmUiValidate" class="crmMailing-error-link">
|
||||
{{ts('Required tokens are missing.')}} <a class="helpicon" ng-click="checkTokens(mailing, 'body_text', 'insert:body_text')"></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
5
sites/all/modules/civicrm/ang/crmMailing/BodyText.js
Normal file
5
sites/all/modules/civicrm/ang/crmMailing/BodyText.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
(function(angular, $, _) {
|
||||
angular.module('crmMailing').directive('crmMailingBodyText', function(crmMailingSimpleDirective) {
|
||||
return crmMailingSimpleDirective('crmMailingBodyText', '~/crmMailing/BodyText.html');
|
||||
});
|
||||
})(angular, CRM.$, CRM._);
|
|
@ -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._);
|
133
sites/all/modules/civicrm/ang/crmMailing/EditMailingCtrl.js
Normal file
133
sites/all/modules/civicrm/ang/crmMailing/EditMailingCtrl.js
Normal file
|
@ -0,0 +1,133 @@
|
|||
(function(angular, $, _) {
|
||||
|
||||
angular.module('crmMailing').controller('EditMailingCtrl', function EditMailingCtrl($scope, selectedMail, $location, crmMailingMgr, crmStatus, attachments, crmMailingPreviewMgr, crmBlocker, CrmAutosaveCtrl, $timeout, crmUiHelp) {
|
||||
var APPROVAL_STATUSES = {'Approved': 1, 'Rejected': 2, 'None': 3};
|
||||
|
||||
$scope.mailing = selectedMail;
|
||||
$scope.attachments = attachments;
|
||||
$scope.crmMailingConst = CRM.crmMailing;
|
||||
$scope.checkPerm = CRM.checkPerm;
|
||||
|
||||
var ts = $scope.ts = CRM.ts(null);
|
||||
$scope.hs = crmUiHelp({file: 'CRM/Mailing/MailingUI'});
|
||||
var block = $scope.block = crmBlocker();
|
||||
var myAutosave = null;
|
||||
|
||||
var templateTypes = _.where(CRM.crmMailing.templateTypes, {name: selectedMail.template_type});
|
||||
if (!templateTypes[0]) throw 'Unrecognized template type: ' + selectedMail.template_type;
|
||||
$scope.mailingEditorUrl = templateTypes[0].editorUrl;
|
||||
|
||||
$scope.isSubmitted = function isSubmitted() {
|
||||
return _.size($scope.mailing.jobs) > 0;
|
||||
};
|
||||
|
||||
// usage: approve('Approved')
|
||||
$scope.approve = function approve(status, options) {
|
||||
$scope.mailing.approval_status_id = APPROVAL_STATUSES[status];
|
||||
return myAutosave.suspend($scope.submit(options));
|
||||
};
|
||||
|
||||
// @return Promise
|
||||
$scope.previewMailing = function previewMailing(mailing, mode) {
|
||||
return crmMailingPreviewMgr.preview(mailing, mode);
|
||||
};
|
||||
|
||||
// @return Promise
|
||||
$scope.sendTest = function sendTest(mailing, attachments, recipient) {
|
||||
var savePromise = crmMailingMgr.save(mailing)
|
||||
.then(function() {
|
||||
return attachments.save();
|
||||
});
|
||||
return block(crmStatus({start: ts('Saving...'), success: ''}, savePromise)
|
||||
.then(function() {
|
||||
crmMailingPreviewMgr.sendTest(mailing, recipient);
|
||||
}));
|
||||
};
|
||||
|
||||
// @return Promise
|
||||
$scope.submit = function submit(options) {
|
||||
options = options || {};
|
||||
if (block.check()) {
|
||||
return;
|
||||
}
|
||||
|
||||
var promise = crmMailingMgr.save($scope.mailing)
|
||||
.then(function() {
|
||||
// pre-condition: the mailing exists *before* saving attachments to it
|
||||
return $scope.attachments.save();
|
||||
})
|
||||
.then(function() {
|
||||
return crmMailingMgr.submit($scope.mailing);
|
||||
})
|
||||
.then(function() {
|
||||
if (!options.stay) {
|
||||
$scope.leave('scheduled');
|
||||
}
|
||||
})
|
||||
;
|
||||
return block(crmStatus({start: ts('Submitting...'), success: ts('Submitted')}, promise));
|
||||
};
|
||||
|
||||
// @return Promise
|
||||
$scope.save = function save() {
|
||||
return block(crmStatus(null,
|
||||
crmMailingMgr
|
||||
.save($scope.mailing)
|
||||
.then(function() {
|
||||
// pre-condition: the mailing exists *before* saving attachments to it
|
||||
return $scope.attachments.save();
|
||||
})
|
||||
));
|
||||
};
|
||||
|
||||
// @return Promise
|
||||
$scope.delete = function cancel() {
|
||||
return block(crmStatus({start: ts('Deleting...'), success: ts('Deleted')},
|
||||
crmMailingMgr.delete($scope.mailing)
|
||||
.then(function() {
|
||||
$scope.leave('unscheduled');
|
||||
})
|
||||
));
|
||||
};
|
||||
|
||||
// @param string listingScreen 'archive', 'scheduled', 'unscheduled'
|
||||
$scope.leave = function leave(listingScreen) {
|
||||
switch (listingScreen) {
|
||||
case 'archive':
|
||||
window.location = CRM.url('civicrm/mailing/browse/archived', {
|
||||
reset: 1
|
||||
});
|
||||
break;
|
||||
case 'scheduled':
|
||||
window.location = CRM.url('civicrm/mailing/browse/scheduled', {
|
||||
reset: 1,
|
||||
scheduled: 'true'
|
||||
});
|
||||
break;
|
||||
case 'unscheduled':
|
||||
/* falls through */
|
||||
default:
|
||||
window.location = CRM.url('civicrm/mailing/browse/unscheduled', {
|
||||
reset: 1,
|
||||
scheduled: 'false'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
myAutosave = new CrmAutosaveCtrl({
|
||||
save: $scope.save,
|
||||
saveIf: function() {
|
||||
return true;
|
||||
},
|
||||
model: function() {
|
||||
return [$scope.mailing, $scope.attachments.getAutosaveSignature()];
|
||||
},
|
||||
form: function() {
|
||||
return $scope.crmMailing;
|
||||
}
|
||||
});
|
||||
$timeout(myAutosave.start);
|
||||
$scope.$on('$destroy', myAutosave.stop);
|
||||
});
|
||||
|
||||
})(angular, CRM.$, CRM._);
|
|
@ -0,0 +1,62 @@
|
|||
<div ng-form="crmMailingSubform">
|
||||
<div class="crm-block crm-form-block crmMailing">
|
||||
<div crm-ui-wizard>
|
||||
<div crm-ui-wizard-step crm-title="ts('Define Mailing')" ng-form="defineForm">
|
||||
<div crm-ui-tab-set>
|
||||
<div crm-ui-tab id="tab-mailing" crm-title="ts('Mailing')">
|
||||
<div crm-mailing-block-summary crm-mailing="mailing"></div>
|
||||
<div crm-mailing-block-mailing crm-mailing="mailing"></div>
|
||||
<div crm-ui-accordion="{title: ts('HTML'), help: hs('html')}">
|
||||
<div crm-mailing-body-html crm-mailing="mailing"></div>
|
||||
</div>
|
||||
<div crm-ui-accordion="{title: ts('Plain Text'), collapsed: !mailing.body_text, help: hs('text')}">
|
||||
<div crm-mailing-body-text crm-mailing="mailing"></div>
|
||||
</div>
|
||||
<span ng-model="placeholder" crm-ui-validate="mailing.body_html || mailing.body_text"></span>
|
||||
</div>
|
||||
<div crm-ui-tab id="tab-attachment" crm-title="ts('Attachments')">
|
||||
<div crm-attachments="attachments"></div>
|
||||
</div>
|
||||
<div crm-ui-tab id="tab-header" crm-title="ts('Header and Footer')">
|
||||
<div crm-mailing-block-header-footer crm-mailing="mailing"></div>
|
||||
</div>
|
||||
<div crm-ui-tab id="tab-pub" crm-title="ts('Publication')">
|
||||
<div crm-mailing-block-publication crm-mailing="mailing"></div>
|
||||
</div>
|
||||
<div crm-ui-tab id="tab-response" crm-title="ts('Responses')">
|
||||
<div crm-mailing-block-responses crm-mailing="mailing"></div>
|
||||
</div>
|
||||
<div crm-ui-tab id="tab-tracking" crm-title="ts('Tracking')">
|
||||
<div crm-mailing-block-tracking crm-mailing="mailing"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div crm-ui-accordion="{title: ts('Preview')}">
|
||||
<div crm-mailing-block-preview crm-mailing="mailing" on-preview="previewMailing(mailing, preview.mode)" on-send="sendTest(mailing, attachments, preview.recipient)"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div crm-ui-wizard-step crm-title="ts('Review and Schedule')" ng-form="reviewForm">
|
||||
<div crm-ui-accordion="{title: ts('Review')}">
|
||||
<div crm-mailing-block-review crm-mailing="mailing" crm-mailing-attachments="attachments"></div>
|
||||
</div>
|
||||
<div crm-ui-accordion="{title: ts('Schedule')}">
|
||||
<div crm-mailing-block-schedule crm-mailing="mailing"></div>
|
||||
</div>
|
||||
<center>
|
||||
<a class="button crmMailing-submit-button" ng-click="submit()" ng-class="{blocking: block.check(), disabled: crmMailingSubform.$invalid}">
|
||||
<div>{{ts('Submit Mailing')}}</div>
|
||||
</a>
|
||||
</center>
|
||||
</div>
|
||||
|
||||
<span crm-ui-wizard-buttons style="float:right;">
|
||||
<button
|
||||
crm-icon="fa-trash"
|
||||
ng-show="checkPerm('delete in CiviMail')"
|
||||
ng-disabled="block.check()"
|
||||
crm-confirm="{title:ts('Delete Draft'), message:ts('Are you sure you want to permanently delete this mailing?')}"
|
||||
on-yes="delete()">{{ts('Delete Draft')}}</button>
|
||||
<button crm-icon="fa-floppy-o" ng-disabled="block.check()" ng-click="save().then(leave)">{{ts('Save Draft')}}</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -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>
|
|
@ -0,0 +1,49 @@
|
|||
<div ng-form="crmMailingSubform">
|
||||
<div class="crm-block crm-form-block crmMailing">
|
||||
|
||||
<div crm-mailing-block-summary crm-mailing="mailing"></div>
|
||||
<div crm-mailing-block-mailing crm-mailing="mailing"></div>
|
||||
|
||||
<div crm-ui-tab-set>
|
||||
<div crm-ui-tab id="tab-html" crm-title="ts('HTML')">
|
||||
<div crm-mailing-body-html crm-mailing="mailing"></div>
|
||||
</div>
|
||||
<div crm-ui-tab id="tab-text" crm-title="ts('Plain Text')">
|
||||
<div crm-mailing-body-text crm-mailing="mailing"></div>
|
||||
</div>
|
||||
<span ng-model="placeholder" crm-ui-validate="mailing.body_html || mailing.body_text"></span>
|
||||
<div crm-ui-tab id="tab-attachment" crm-title="ts('Attachments')">
|
||||
<div crm-attachments="attachments"></div>
|
||||
</div>
|
||||
<div crm-ui-tab id="tab-header" crm-title="ts('Header and Footer')">
|
||||
<div crm-mailing-block-header-footer crm-mailing="mailing"></div>
|
||||
</div>
|
||||
<div crm-ui-tab id="tab-pub" crm-title="ts('Publication')">
|
||||
<div crm-mailing-block-publication crm-mailing="mailing"></div>
|
||||
</div>
|
||||
<div crm-ui-tab id="tab-response" crm-title="ts('Responses')">
|
||||
<div crm-mailing-block-responses crm-mailing="mailing"></div>
|
||||
</div>
|
||||
<div crm-ui-tab id="tab-tracking" crm-title="ts('Tracking')">
|
||||
<div crm-mailing-block-tracking crm-mailing="mailing"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div crm-ui-accordion="{title: ts('Preview')}">
|
||||
<div crm-mailing-block-preview crm-mailing="mailing" on-preview="previewMailing(mailing, preview.mode)" on-send="sendTest(mailing, attachments, preview.recipient)"></div>
|
||||
</div>
|
||||
|
||||
<div crm-ui-accordion="{title: ts('Schedule')}">
|
||||
<div crm-mailing-block-schedule crm-mailing="mailing"></div>
|
||||
</div>
|
||||
|
||||
<button crm-icon="fa-paper-plane" ng-disabled="block.check() || crmMailingSubform.$invalid" ng-click="submit()">{{ts('Submit Mailing')}}</button>
|
||||
<button crm-icon="fa-floppy-o" ng-disabled="block.check()" ng-click="save().then(leave)">{{ts('Save Draft')}}</button>
|
||||
<button
|
||||
crm-icon="fa-trash"
|
||||
ng-show="checkPerm('delete in CiviMail')"
|
||||
ng-disabled="block.check()"
|
||||
crm-confirm="{title:ts('Delete Draft'), message:ts('Are you sure you want to permanently delete this mailing?')}"
|
||||
on-yes="delete()">{{ts('Delete Draft')}}</button>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,45 @@
|
|||
<div ng-form="crmMailingSubform">
|
||||
<div class="crm-block crm-form-block crmMailing">
|
||||
|
||||
<div crm-mailing-block-summary crm-mailing="mailing"></div>
|
||||
<div crm-mailing-block-mailing crm-mailing="mailing"></div>
|
||||
|
||||
<div crm-ui-accordion="{title: ts('HTML'), help: hs('html')}">
|
||||
<div crm-mailing-body-html crm-mailing="mailing"></div>
|
||||
</div>
|
||||
<div crm-ui-accordion="{title: ts('Plain Text'), collapsed: !mailing.body_text, help: hs('text')}">
|
||||
<div crm-mailing-body-text crm-mailing="mailing"></div>
|
||||
</div>
|
||||
<span ng-model="placeholder" crm-ui-validate="mailing.body_html || mailing.body_text"></span>
|
||||
<div crm-ui-accordion="{title: ts('Header and Footer'), collapsed: true}" id="tab-header">
|
||||
<div crm-mailing-block-header-footer crm-mailing="mailing"></div>
|
||||
</div>
|
||||
<div crm-ui-accordion="{title: ts('Attachments'), collapsed: true}" id="tab-attachment">
|
||||
<div crm-attachments="attachments"></div>
|
||||
</div>
|
||||
<div crm-ui-accordion="{title: ts('Publication'), collapsed: true}" id="tab-pub">
|
||||
<div crm-mailing-block-publication crm-mailing="mailing"></div>
|
||||
</div>
|
||||
<div crm-ui-accordion="{title: ts('Responses'), collapsed: true}" id="tab-response">
|
||||
<div crm-mailing-block-responses crm-mailing="mailing"></div>
|
||||
</div>
|
||||
<div crm-ui-accordion="{title: ts('Tracking'), collapsed: true}" id="tab-tracking">
|
||||
<div crm-mailing-block-tracking crm-mailing="mailing"></div>
|
||||
</div>
|
||||
<div crm-ui-accordion="{title: ts('Preview')}">
|
||||
<div crm-mailing-block-preview crm-mailing="mailing" on-preview="previewMailing(mailing, preview.mode)" on-send="sendTest(mailing, attachments, preview.recipient)"></div>
|
||||
</div>
|
||||
<div crm-ui-accordion="{title: ts('Schedule')}" id="tab-schedule">
|
||||
<div crm-mailing-block-schedule crm-mailing="mailing"></div>
|
||||
</div>
|
||||
|
||||
<button crm-icon="fa-paper-plane" ng-disabled="block.check() || crmMailingSubform.$invalid" ng-click="submit()">{{ts('Submit Mailing')}}</button>
|
||||
<button crm-icon="fa-floppy-o" ng-disabled="block.check()" ng-click="save().then(leave)">{{ts('Save Draft')}}</button>
|
||||
<button
|
||||
crm-icon="fa-trash"
|
||||
ng-show="checkPerm('delete in CiviMail')"
|
||||
ng-disabled="block.check()"
|
||||
crm-confirm="{title:ts('Delete Draft'), message:ts('Are you sure you want to permanently delete this mailing?')}"
|
||||
on-yes="delete()">{{ts('Delete Draft')}}</button>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,65 @@
|
|||
<div ng-form="crmMailingSubform">
|
||||
<div class="crm-block crm-form-block crmMailing">
|
||||
|
||||
<div crm-ui-wizard>
|
||||
|
||||
<div crm-ui-wizard-step crm-title="ts('Content')" ng-form="contentForm">
|
||||
<div crm-mailing-block-summary crm-mailing="mailing"></div>
|
||||
<div crm-mailing-block-mailing crm-mailing="mailing"></div>
|
||||
<div crm-ui-accordion="{title: ts('HTML'), help: hs('html')}">
|
||||
<div crm-mailing-body-html crm-mailing="mailing"></div>
|
||||
</div>
|
||||
<div crm-ui-accordion="{title: ts('Plain Text'), collapsed: !mailing.body_text, help: hs('text')}">
|
||||
<div crm-mailing-body-text crm-mailing="mailing"></div>
|
||||
</div>
|
||||
<span ng-model="placeholder" crm-ui-validate="mailing.body_html || mailing.body_text"></span>
|
||||
<div crm-ui-accordion="{title: ts('Header and Footer'), collapsed: true}">
|
||||
<div crm-mailing-block-header-footer crm-mailing="mailing"></div>
|
||||
</div>
|
||||
<div crm-ui-accordion="{title: ts('Attachments'), collapsed: true}">
|
||||
<div crm-attachments="attachments"></div>
|
||||
</div>
|
||||
<div crm-ui-accordion="{title: ts('Preview')}">
|
||||
<div crm-mailing-block-preview crm-mailing="mailing" on-preview="previewMailing(mailing, preview.mode)" on-send="sendTest(mailing, attachments, preview.recipient)"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div crm-ui-wizard-step crm-title="ts('Options')" ng-form="optionsForm">
|
||||
<div crm-ui-accordion="{title: ts('Schedule')}">
|
||||
<div crm-mailing-block-schedule crm-mailing="mailing"></div>
|
||||
</div>
|
||||
|
||||
<div crm-ui-accordion="{title: ts('Responses'), collapsed: true}">
|
||||
<div crm-mailing-block-responses crm-mailing="mailing"></div>
|
||||
</div>
|
||||
<div crm-ui-accordion="{title: ts('Tracking'), collapsed: true}">
|
||||
<div crm-mailing-block-tracking crm-mailing="mailing"></div>
|
||||
</div>
|
||||
<div crm-ui-accordion="{title: ts('Publication'), collapsed: true}">
|
||||
<div crm-mailing-block-publication crm-mailing="mailing"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div crm-ui-wizard-step crm-title="ts('Review')" ng-form="reviewForm">
|
||||
<div crm-ui-accordion="{title: ts('Review')}">
|
||||
<div crm-mailing-block-review crm-mailing="mailing" crm-mailing-attachments="attachments"></div>
|
||||
</div>
|
||||
<center>
|
||||
<a class="button crmMailing-submit-button" ng-click="submit()" ng-class="{blocking: block.check(), disabled: crmMailingSubform.$invalid}">
|
||||
<div>{{ts('Submit Mailing')}}</div>
|
||||
</a>
|
||||
</center>
|
||||
</div>
|
||||
|
||||
<span crm-ui-wizard-buttons style="float:right;">
|
||||
<button
|
||||
crm-icon="fa-trash"
|
||||
ng-show="checkPerm('delete in CiviMail')"
|
||||
ng-disabled="block.check()"
|
||||
crm-confirm="{title:ts('Delete Draft'), message:ts('Are you sure you want to permanently delete this mailing?')}"
|
||||
on-yes="delete()">{{ts('Delete Draft')}}</button>
|
||||
<button crm-icon="fa-floppy-o" ng-disabled="block.check()" ng-click="save().then(leave)">{{ts('Save Draft')}}</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,72 @@
|
|||
<div ng-form="crmMailingSubform">
|
||||
<div class="crm-block crm-form-block crmMailing">
|
||||
|
||||
<div crm-ui-wizard>
|
||||
|
||||
<div crm-ui-wizard-step="10" crm-title="ts('Content')" ng-form="contentForm" ng-if="checkPerm('create mailings') || checkPerm('access CiviMail')">
|
||||
<div crm-mailing-block-summary crm-mailing="mailing"></div>
|
||||
<div crm-mailing-block-mailing crm-mailing="mailing"></div>
|
||||
<div crm-ui-accordion="{title: ts('HTML'), help: hs('html')}">
|
||||
<div crm-mailing-body-html crm-mailing="mailing"></div>
|
||||
</div>
|
||||
<div crm-ui-accordion="{title: ts('Plain Text'), collapsed: !mailing.body_text, help: hs('text')}">
|
||||
<div crm-mailing-body-text crm-mailing="mailing"></div>
|
||||
</div>
|
||||
<span ng-model="placeholder" crm-ui-validate="mailing.body_html || mailing.body_text"></span>
|
||||
<div crm-ui-accordion="{title: ts('Header and Footer'), collapsed: true}">
|
||||
<div crm-mailing-block-header-footer crm-mailing="mailing"></div>
|
||||
</div>
|
||||
<div crm-ui-accordion="{title: ts('Attachments'), collapsed: true}">
|
||||
<div crm-attachments="attachments"></div>
|
||||
</div>
|
||||
<div crm-ui-accordion="{title: ts('Preview')}">
|
||||
<div crm-mailing-block-preview crm-mailing="mailing" on-preview="previewMailing(mailing, preview.mode)" on-send="sendTest(mailing, attachments, preview.recipient)"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div crm-ui-wizard-step="20" crm-title="ts('Options')" ng-form="optionsForm" ng-if="checkPerm('create mailings') || checkPerm('access CiviMail')">
|
||||
<div crm-ui-accordion="{title: ts('Responses'), collapsed: true}">
|
||||
<div crm-mailing-block-responses crm-mailing="mailing"></div>
|
||||
</div>
|
||||
<div crm-ui-accordion="{title: ts('Tracking'), collapsed: true}">
|
||||
<div crm-mailing-block-tracking crm-mailing="mailing"></div>
|
||||
</div>
|
||||
<div crm-ui-accordion="{title: ts('Publication'), collapsed: true}">
|
||||
<div crm-mailing-block-publication crm-mailing="mailing"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div crm-ui-wizard-step="40" crm-title="ts('Review')" ng-form="schedForm" ng-if="checkPerm('schedule mailings') || checkPerm('access CiviMail')">
|
||||
<div crm-ui-accordion="{title: ts('Review')}">
|
||||
<div crm-mailing-block-review crm-mailing="mailing" crm-mailing-attachments="attachments"></div>
|
||||
</div>
|
||||
<div crm-ui-accordion="{title: ts('Schedule')}">
|
||||
<div crm-mailing-block-schedule crm-mailing="mailing"></div>
|
||||
</div>
|
||||
<div crm-ui-accordion="{title: ts('Approval')}" ng-if="checkPerm('approve mailings') || checkPerm('access CiviMail')">
|
||||
<div crm-mailing-block-approve crm-mailing="mailing"></div>
|
||||
</div>
|
||||
<center ng-if="!checkPerm('approve mailings') && !checkPerm('access CiviMail')">
|
||||
<a class="button crmMailing-submit-button" ng-click="submit()" ng-class="{blocking: block.check(), disabled: crmMailingSubform.$invalid}">
|
||||
<div>{{ts('Submit Mailing')}}</div>
|
||||
</a>
|
||||
</center>
|
||||
<center ng-if="checkPerm('approve mailings') || checkPerm('access CiviMail')">
|
||||
<a class="button crmMailing-submit-button" ng-click="approve('Approved')" ng-class="{blocking: block.check(), disabled: crmMailingSubform.$invalid}">
|
||||
<div>{{ts('Submit and Approve Mailing')}}</div>
|
||||
</a>
|
||||
</center>
|
||||
</div>
|
||||
|
||||
<span crm-ui-wizard-buttons style="float:right;">
|
||||
<button
|
||||
crm-icon="fa-trash"
|
||||
ng-show="checkPerm('delete in CiviMail')"
|
||||
ng-disabled="block.check()"
|
||||
crm-confirm="{title:ts('Delete Draft'), message:ts('Are you sure you want to permanently delete this mailing?')}"
|
||||
on-yes="delete()">{{ts('Delete Draft')}}</button>
|
||||
<button crm-icon="fa-floppy-o" ng-disabled="block.check()" ng-click="save().then(leave)">{{ts('Save Draft')}}</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
92
sites/all/modules/civicrm/ang/crmMailing/EditRecipCtrl.js
Normal file
92
sites/all/modules/civicrm/ang/crmMailing/EditRecipCtrl.js
Normal file
|
@ -0,0 +1,92 @@
|
|||
(function(angular, $, _) {
|
||||
|
||||
// Controller for the edit-recipients fields (
|
||||
// WISHLIST: Move most of this to a (cache-enabled) service
|
||||
// Scope members:
|
||||
// - [input] mailing: object
|
||||
// - [output] recipients: array of recipient records
|
||||
angular.module('crmMailing').controller('EditRecipCtrl', function EditRecipCtrl($scope, dialogService, crmApi, crmMailingMgr, $q, crmMetadata, crmStatus) {
|
||||
// Time to wait before triggering AJAX update to recipients list
|
||||
var RECIPIENTS_DEBOUNCE_MS = 100;
|
||||
var RECIPIENTS_PREVIEW_LIMIT = 50;
|
||||
|
||||
var ts = $scope.ts = CRM.ts(null);
|
||||
|
||||
$scope.isMailingList = function isMailingList(group) {
|
||||
var GROUP_TYPE_MAILING_LIST = '2';
|
||||
return _.contains(group.group_type, GROUP_TYPE_MAILING_LIST);
|
||||
};
|
||||
|
||||
$scope.recipients = null;
|
||||
$scope.getRecipientsEstimate = function() {
|
||||
var ts = $scope.ts;
|
||||
if ($scope.recipients === null) {
|
||||
return ts('(Estimating)');
|
||||
}
|
||||
if ($scope.recipients === 0) {
|
||||
return ts('No recipients');
|
||||
}
|
||||
if ($scope.recipients === 1) {
|
||||
return ts('~1 recipient');
|
||||
}
|
||||
return ts('~%1 recipients', {1: $scope.recipients});
|
||||
};
|
||||
|
||||
// We monitor four fields -- use debounce so that changes across the
|
||||
// four fields can settle-down before AJAX.
|
||||
var refreshRecipients = _.debounce(function() {
|
||||
$scope.$apply(function() {
|
||||
$scope.recipients = null;
|
||||
if (!$scope.mailing) {
|
||||
return;
|
||||
}
|
||||
crmMailingMgr.previewRecipientCount($scope.mailing).then(function(recipients) {
|
||||
$scope.recipients = recipients;
|
||||
});
|
||||
});
|
||||
}, RECIPIENTS_DEBOUNCE_MS);
|
||||
$scope.$watchCollection("mailing.dedupe_email", refreshRecipients);
|
||||
$scope.$watchCollection("mailing.location_type_id", refreshRecipients);
|
||||
$scope.$watchCollection("mailing.email_selection_method", refreshRecipients);
|
||||
$scope.$watchCollection("mailing.recipients.groups.include", refreshRecipients);
|
||||
$scope.$watchCollection("mailing.recipients.groups.exclude", refreshRecipients);
|
||||
$scope.$watchCollection("mailing.recipients.mailings.include", refreshRecipients);
|
||||
$scope.$watchCollection("mailing.recipients.mailings.exclude", refreshRecipients);
|
||||
|
||||
$scope.previewRecipients = function previewRecipients() {
|
||||
return crmStatus({start: ts('Previewing...'), success: ''}, crmMailingMgr.previewRecipients($scope.mailing, RECIPIENTS_PREVIEW_LIMIT).then(function(recipients) {
|
||||
var model = {
|
||||
count: $scope.recipients,
|
||||
sample: recipients,
|
||||
sampleLimit: RECIPIENTS_PREVIEW_LIMIT
|
||||
};
|
||||
var options = CRM.utils.adjustDialogDefaults({
|
||||
width: '40%',
|
||||
autoOpen: false,
|
||||
title: ts('Preview (%1)', {
|
||||
1: $scope.getRecipientsEstimate()
|
||||
})
|
||||
});
|
||||
dialogService.open('recipDialog', '~/crmMailing/PreviewRecipCtrl.html', model, options);
|
||||
}));
|
||||
};
|
||||
|
||||
// Open a dialog for editing the advanced recipient options.
|
||||
$scope.editOptions = function editOptions(mailing) {
|
||||
var options = CRM.utils.adjustDialogDefaults({
|
||||
autoOpen: false,
|
||||
width: '40%',
|
||||
height: 'auto',
|
||||
title: ts('Edit Options')
|
||||
});
|
||||
$q.when(crmMetadata.getFields('Mailing')).then(function(fields) {
|
||||
var model = {
|
||||
fields: fields,
|
||||
mailing: mailing
|
||||
};
|
||||
dialogService.open('previewComponentDialog', '~/crmMailing/EditRecipOptionsDialogCtrl.html', model, options);
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
})(angular, CRM.$, CRM._);
|
|
@ -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>
|
|
@ -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._);
|
|
@ -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._);
|
31
sites/all/modules/civicrm/ang/crmMailing/EmailAddrCtrl.js
Normal file
31
sites/all/modules/civicrm/ang/crmMailing/EmailAddrCtrl.js
Normal file
|
@ -0,0 +1,31 @@
|
|||
(function(angular, $, _) {
|
||||
|
||||
angular.module('crmMailing').controller('EmailAddrCtrl', function EmailAddrCtrl($scope, crmFromAddresses, crmUiAlert) {
|
||||
var ts = CRM.ts(null);
|
||||
|
||||
function changeAlert(winnerField, loserField) {
|
||||
crmUiAlert({
|
||||
title: ts('Conflict'),
|
||||
text: ts('The "%1" option conflicts with the "%2" option. The "%2" option has been disabled.', {
|
||||
1: winnerField,
|
||||
2: loserField
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
$scope.crmFromAddresses = crmFromAddresses;
|
||||
$scope.checkReplyToChange = function checkReplyToChange(mailing) {
|
||||
if (!_.isEmpty(mailing.replyto_email) && mailing.override_verp == '0') {
|
||||
mailing.override_verp = '1';
|
||||
changeAlert(ts('Reply-To'), ts('Track Replies'));
|
||||
}
|
||||
};
|
||||
$scope.checkVerpChange = function checkVerpChange(mailing) {
|
||||
if (!_.isEmpty(mailing.replyto_email) && mailing.override_verp == '0') {
|
||||
mailing.replyto_email = '';
|
||||
changeAlert(ts('Track Replies'), ts('Reply-To'));
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
})(angular, CRM.$, CRM._);
|
52
sites/all/modules/civicrm/ang/crmMailing/EmailBodyCtrl.js
Normal file
52
sites/all/modules/civicrm/ang/crmMailing/EmailBodyCtrl.js
Normal file
|
@ -0,0 +1,52 @@
|
|||
(function(angular, $, _) {
|
||||
|
||||
var lastEmailTokenAlert = null;
|
||||
angular.module('crmMailing').controller('EmailBodyCtrl', function EmailBodyCtrl($scope, crmMailingMgr, crmUiAlert, $timeout) {
|
||||
var ts = CRM.ts(null);
|
||||
|
||||
// ex: if (!hasAllTokens(myMailing, 'body_text)) alert('Oh noes!');
|
||||
$scope.hasAllTokens = function hasAllTokens(mailing, field) {
|
||||
return _.isEmpty(crmMailingMgr.findMissingTokens(mailing, field));
|
||||
};
|
||||
|
||||
// ex: checkTokens(myMailing, 'body_text', 'insert:body_text')
|
||||
// ex: checkTokens(myMailing, '*')
|
||||
$scope.checkTokens = function checkTokens(mailing, field, insertEvent) {
|
||||
if (lastEmailTokenAlert) {
|
||||
lastEmailTokenAlert.close();
|
||||
}
|
||||
var missing, insertable;
|
||||
if (field == '*') {
|
||||
insertable = false;
|
||||
missing = angular.extend({},
|
||||
crmMailingMgr.findMissingTokens(mailing, 'body_html'),
|
||||
crmMailingMgr.findMissingTokens(mailing, 'body_text')
|
||||
);
|
||||
}
|
||||
else {
|
||||
insertable = !_.isEmpty(insertEvent);
|
||||
missing = crmMailingMgr.findMissingTokens(mailing, field);
|
||||
}
|
||||
if (!_.isEmpty(missing)) {
|
||||
lastEmailTokenAlert = crmUiAlert({
|
||||
type: 'error',
|
||||
title: ts('Required tokens'),
|
||||
templateUrl: '~/crmMailing/EmailBodyCtrl/tokenAlert.html',
|
||||
scope: angular.extend($scope.$new(), {
|
||||
insertable: insertable,
|
||||
insertToken: function(token) {
|
||||
$timeout(function() {
|
||||
$scope.$broadcast(insertEvent, '{' + token + '}');
|
||||
$timeout(function() {
|
||||
checkTokens(mailing, field, insertEvent);
|
||||
});
|
||||
});
|
||||
},
|
||||
missing: missing
|
||||
})
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
})(angular, CRM.$, CRM._);
|
|
@ -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>
|
30
sites/all/modules/civicrm/ang/crmMailing/FromAddress.js
Normal file
30
sites/all/modules/civicrm/ang/crmMailing/FromAddress.js
Normal file
|
@ -0,0 +1,30 @@
|
|||
(function(angular, $, _) {
|
||||
// Convert between a mailing "From Address" (mailing.from_name,mailing.from_email) and a unified label ("Name" <e@ma.il>)
|
||||
// example: <span crm-mailing-from-address="myPlaceholder" crm-mailing="myMailing"><select ng-model="myPlaceholder.label"></select></span>
|
||||
// NOTE: This really doesn't belong in a directive. I've tried (and failed) to make this work with a getterSetter binding, eg
|
||||
// <select ng-model="mailing.convertFromAddress" ng-model-options="{getterSetter: true}">
|
||||
angular.module('crmMailing').directive('crmMailingFromAddress', function(crmFromAddresses) {
|
||||
return {
|
||||
link: function(scope, element, attrs) {
|
||||
var placeholder = attrs.crmMailingFromAddress;
|
||||
var mailing = null;
|
||||
scope.$watch(attrs.crmMailing, function(newValue) {
|
||||
mailing = newValue;
|
||||
scope[placeholder] = {
|
||||
label: crmFromAddresses.getByAuthorEmail(mailing.from_name, mailing.from_email, true).label
|
||||
};
|
||||
});
|
||||
scope.$watch(placeholder + '.label', function(newValue) {
|
||||
var addr = crmFromAddresses.getByLabel(newValue);
|
||||
mailing.from_name = addr.author;
|
||||
mailing.from_email = addr.email;
|
||||
// CRM-18364: set replyTo as from_email only if custom replyTo is disabled in mail settings.
|
||||
if (!CRM.crmMailing.enableReplyTo) {
|
||||
mailing.replyto_email = crmFromAddresses.getByAuthorEmail(mailing.from_name, mailing.from_email, true).label;
|
||||
}
|
||||
});
|
||||
// FIXME: Shouldn't we also be watching mailing.from_name and mailing.from_email?
|
||||
}
|
||||
};
|
||||
});
|
||||
})(angular, CRM.$, CRM._);
|
10
sites/all/modules/civicrm/ang/crmMailing/ListMailingsCtrl.js
Normal file
10
sites/all/modules/civicrm/ang/crmMailing/ListMailingsCtrl.js
Normal file
|
@ -0,0 +1,10 @@
|
|||
(function(angular, $, _) {
|
||||
|
||||
angular.module('crmMailing').controller('ListMailingsCtrl', ['crmLegacy', 'crmNavigator', function ListMailingsCtrl(crmLegacy, crmNavigator) {
|
||||
// We haven't implemented this in Angular, but some users may get clever
|
||||
// about typing URLs, so we'll provide a redirect.
|
||||
var new_url = crmLegacy.url('civicrm/mailing/browse/unscheduled', {reset: 1, scheduled: 'false'});
|
||||
crmNavigator.redirect(new_url);
|
||||
}]);
|
||||
|
||||
})(angular, CRM.$, CRM._);
|
43
sites/all/modules/civicrm/ang/crmMailing/MsgTemplateCtrl.js
Normal file
43
sites/all/modules/civicrm/ang/crmMailing/MsgTemplateCtrl.js
Normal file
|
@ -0,0 +1,43 @@
|
|||
(function(angular, $, _) {
|
||||
|
||||
// Controller for the in-place msg-template management
|
||||
angular.module('crmMailing').controller('MsgTemplateCtrl', function MsgTemplateCtrl($scope, crmMsgTemplates, dialogService) {
|
||||
var ts = $scope.ts = CRM.ts(null);
|
||||
$scope.crmMsgTemplates = crmMsgTemplates;
|
||||
$scope.checkPerm = CRM.checkPerm;
|
||||
// @return Promise MessageTemplate (per APIv3)
|
||||
$scope.saveTemplate = function saveTemplate(mailing) {
|
||||
var model = {
|
||||
selected_id: mailing.msg_template_id,
|
||||
tpl: {
|
||||
msg_title: '',
|
||||
msg_subject: mailing.subject,
|
||||
msg_text: mailing.body_text,
|
||||
msg_html: mailing.body_html
|
||||
}
|
||||
};
|
||||
var options = CRM.utils.adjustDialogDefaults({
|
||||
autoOpen: false,
|
||||
height: 'auto',
|
||||
width: '40%',
|
||||
title: ts('Save Template')
|
||||
});
|
||||
return dialogService.open('saveTemplateDialog', '~/crmMailing/SaveMsgTemplateDialogCtrl.html', model, options)
|
||||
.then(function(item) {
|
||||
mailing.msg_template_id = item.id;
|
||||
return item;
|
||||
});
|
||||
};
|
||||
|
||||
// @param int id
|
||||
// @return Promise
|
||||
$scope.loadTemplate = function loadTemplate(mailing, id) {
|
||||
return crmMsgTemplates.get(id).then(function(tpl) {
|
||||
mailing.subject = tpl.msg_subject;
|
||||
mailing.body_text = tpl.msg_text;
|
||||
mailing.body_html = tpl.msg_html;
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
})(angular, CRM.$, CRM._);
|
|
@ -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._);
|
|
@ -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>
|
|
@ -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._);
|
|
@ -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._);
|
|
@ -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>
|
|
@ -0,0 +1,3 @@
|
|||
<div ng-controller="PreviewMailingDialogCtrl">
|
||||
<iframe crm-ui-iframe="model.body_html"></iframe>
|
||||
</div>
|
|
@ -0,0 +1,3 @@
|
|||
<div ng-controller="PreviewMailingDialogCtrl">
|
||||
<pre>{{model.body_text}}</pre>
|
||||
</div>
|
|
@ -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>
|
10
sites/all/modules/civicrm/ang/crmMailing/PreviewRecipCtrl.js
Normal file
10
sites/all/modules/civicrm/ang/crmMailing/PreviewRecipCtrl.js
Normal file
|
@ -0,0 +1,10 @@
|
|||
(function(angular, $, _) {
|
||||
|
||||
// Controller for the "Preview Recipients" dialog
|
||||
// Note: Expects $scope.model to be an object with properties:
|
||||
// - recipients: array of contacts
|
||||
angular.module('crmMailing').controller('PreviewRecipCtrl', function($scope) {
|
||||
$scope.ts = CRM.ts(null);
|
||||
});
|
||||
|
||||
})(angular, CRM.$, CRM._);
|
116
sites/all/modules/civicrm/ang/crmMailing/RadioDate.js
Normal file
116
sites/all/modules/civicrm/ang/crmMailing/RadioDate.js
Normal file
|
@ -0,0 +1,116 @@
|
|||
(function(angular, $, _) {
|
||||
// "YYYY-MM-DD hh:mm:ss" => Date()
|
||||
function parseYmdHms(d) {
|
||||
var parts = d.split(/[\-: ]/);
|
||||
return new Date(parts[0], parts[1]-1, parts[2], parts[3], parts[4], parts[5]);
|
||||
}
|
||||
|
||||
function isDateBefore(tgt, cutoff, tolerance) {
|
||||
var ad = parseYmdHms(tgt), bd = parseYmdHms(cutoff);
|
||||
// We'll allow a little leeway, where tgt is considered before cutoff
|
||||
// even if technically misses the cutoff by a little.
|
||||
return ad < bd-tolerance;
|
||||
}
|
||||
|
||||
// Represent a datetime field as if it were a radio ('schedule.mode') and a datetime ('schedule.datetime').
|
||||
// example: <div crm-mailing-radio-date="mySchedule" ng-model="mailing.scheduled_date">...</div>
|
||||
angular.module('crmMailing').directive('crmMailingRadioDate', function(crmUiAlert) {
|
||||
return {
|
||||
require: 'ngModel',
|
||||
link: function($scope, element, attrs, ngModel) {
|
||||
var lastAlert = null;
|
||||
|
||||
var schedule = $scope[attrs.crmMailingRadioDate] = {
|
||||
mode: 'now',
|
||||
datetime: ''
|
||||
};
|
||||
|
||||
ngModel.$render = function $render() {
|
||||
var sched = ngModel.$viewValue;
|
||||
if (!_.isEmpty(sched)) {
|
||||
schedule.mode = 'at';
|
||||
schedule.datetime = sched;
|
||||
}
|
||||
else {
|
||||
schedule.mode = 'now';
|
||||
schedule.datetime = '';
|
||||
}
|
||||
};
|
||||
|
||||
var updateParent = (function() {
|
||||
switch (schedule.mode) {
|
||||
case 'now':
|
||||
ngModel.$setViewValue(null);
|
||||
schedule.datetime = '';
|
||||
break;
|
||||
case 'at':
|
||||
schedule.datetime = schedule.datetime || '?';
|
||||
ngModel.$setViewValue(schedule.datetime);
|
||||
break;
|
||||
default:
|
||||
throw 'Unrecognized schedule mode: ' + schedule.mode;
|
||||
}
|
||||
});
|
||||
|
||||
element
|
||||
// Open datepicker when clicking "At" radio
|
||||
.on('click', ':radio[value=at]', function() {
|
||||
$('.crm-form-date', element).focus();
|
||||
})
|
||||
// Reset mode if user entered an invalid date
|
||||
.on('change', '.crm-hidden-date', function(e, context) {
|
||||
if (context === 'userInput' && $(this).val() === '' && $(this).siblings('.crm-form-date').val().length) {
|
||||
schedule.mode = 'at';
|
||||
schedule.datetime = '?';
|
||||
} else {
|
||||
var d = new Date(),
|
||||
month = '' + (d.getMonth() + 1),
|
||||
day = '' + d.getDate(),
|
||||
year = d.getFullYear(),
|
||||
hours = '' + d.getHours(),
|
||||
minutes = '' + d.getMinutes();
|
||||
var submittedDate = $(this).val();
|
||||
if (month.length < 2) month = '0' + month;
|
||||
if (day.length < 2) day = '0' + day;
|
||||
if (hours.length < 2) hours = '0' + hours;
|
||||
if (minutes.length < 2) minutes = '0' + minutes;
|
||||
date = [year, month, day].join('-');
|
||||
time = [hours, minutes, "00"].join(':');
|
||||
currentDate = date + ' ' + time;
|
||||
var isInPast = (submittedDate.length && submittedDate.match(/^[0-9\-]+ [0-9\:]+$/) && isDateBefore(submittedDate, currentDate, 4*60*60*1000));
|
||||
ngModel.$setValidity('dateTimeInThePast', !isInPast);
|
||||
if (lastAlert && lastAlert.isOpen) {
|
||||
lastAlert.close();
|
||||
}
|
||||
if (isInPast) {
|
||||
lastAlert = crmUiAlert({
|
||||
text: ts('The scheduled date and time is in the past'),
|
||||
title: ts('Error')
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
$scope.$watch(attrs.crmMailingRadioDate + '.mode', updateParent);
|
||||
$scope.$watch(attrs.crmMailingRadioDate + '.datetime', function(newValue, oldValue) {
|
||||
// automatically switch mode based on datetime entry
|
||||
if (typeof oldValue === 'undefined') {
|
||||
oldValue = '';
|
||||
}
|
||||
if (typeof newValue === 'undefined') {
|
||||
newValue = '';
|
||||
}
|
||||
if (oldValue !== newValue) {
|
||||
if (_.isEmpty(newValue)) {
|
||||
schedule.mode = 'now';
|
||||
}
|
||||
else {
|
||||
schedule.mode = 'at';
|
||||
}
|
||||
}
|
||||
updateParent();
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
})(angular, CRM.$, CRM._);
|
322
sites/all/modules/civicrm/ang/crmMailing/Recipients.js
Normal file
322
sites/all/modules/civicrm/ang/crmMailing/Recipients.js
Normal file
|
@ -0,0 +1,322 @@
|
|||
(function(angular, $, _) {
|
||||
// example: <select multiple crm-mailing-recipients crm-mailing="mymailing" crm-avail-groups="myGroups" crm-avail-mailings="myMailings"></select>
|
||||
// FIXME: participate in ngModel's validation cycle
|
||||
angular.module('crmMailing').directive('crmMailingRecipients', function(crmUiAlert) {
|
||||
return {
|
||||
restrict: 'AE',
|
||||
require: 'ngModel',
|
||||
scope: {
|
||||
ngRequired: '@'
|
||||
},
|
||||
link: function(scope, element, attrs, ngModel) {
|
||||
scope.recips = ngModel.$viewValue;
|
||||
scope.groups = scope.$parent.$eval(attrs.crmAvailGroups);
|
||||
scope.mailings = scope.$parent.$eval(attrs.crmAvailMailings);
|
||||
refreshMandatory();
|
||||
|
||||
var ts = scope.ts = CRM.ts(null);
|
||||
|
||||
/// Convert MySQL date ("yyyy-mm-dd hh:mm:ss") to JS date object
|
||||
scope.parseDate = function(date) {
|
||||
if (!angular.isString(date)) {
|
||||
return date;
|
||||
}
|
||||
var p = date.split(/[\- :]/);
|
||||
return new Date(parseInt(p[0]), parseInt(p[1]) - 1, parseInt(p[2]), parseInt(p[3]), parseInt(p[4]), parseInt(p[5]));
|
||||
};
|
||||
|
||||
/// Remove {value} from {array}
|
||||
function arrayRemove(array, value) {
|
||||
var idx = array.indexOf(value);
|
||||
if (idx >= 0) {
|
||||
array.splice(idx, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// @param string id an encoded string like "4 civicrm_mailing include"
|
||||
// @return Object keys: entity_id, entity_type, mode
|
||||
function convertValueToObj(id) {
|
||||
var a = id.split(" ");
|
||||
return {entity_id: parseInt(a[0]), entity_type: a[1], mode: a[2]};
|
||||
}
|
||||
|
||||
// @param Object mailing
|
||||
// @return array list of values like "4 civicrm_mailing include"
|
||||
function convertMailingToValues(recipients) {
|
||||
var r = [];
|
||||
angular.forEach(recipients.groups.include, function(v) {
|
||||
r.push(v + " civicrm_group include");
|
||||
});
|
||||
angular.forEach(recipients.groups.exclude, function(v) {
|
||||
r.push(v + " civicrm_group exclude");
|
||||
});
|
||||
angular.forEach(recipients.mailings.include, function(v) {
|
||||
r.push(v + " civicrm_mailing include");
|
||||
});
|
||||
angular.forEach(recipients.mailings.exclude, function(v) {
|
||||
r.push(v + " civicrm_mailing exclude");
|
||||
});
|
||||
return r;
|
||||
}
|
||||
|
||||
function refreshMandatory() {
|
||||
if (ngModel.$viewValue && ngModel.$viewValue.groups) {
|
||||
scope.mandatoryGroups = _.filter(scope.$parent.$eval(attrs.crmMandatoryGroups), function(grp) {
|
||||
return _.contains(ngModel.$viewValue.groups.include, parseInt(grp.id));
|
||||
});
|
||||
scope.mandatoryIds = _.map(_.pluck(scope.$parent.$eval(attrs.crmMandatoryGroups), 'id'), function(n) {
|
||||
return parseInt(n);
|
||||
});
|
||||
}
|
||||
else {
|
||||
scope.mandatoryGroups = [];
|
||||
scope.mandatoryIds = [];
|
||||
}
|
||||
}
|
||||
|
||||
function isMandatory(grpId) {
|
||||
return _.contains(scope.mandatoryIds, parseInt(grpId));
|
||||
}
|
||||
|
||||
var refreshUI = ngModel.$render = function refresuhUI() {
|
||||
scope.recips = ngModel.$viewValue;
|
||||
if (ngModel.$viewValue) {
|
||||
$(element).select2('val', convertMailingToValues(ngModel.$viewValue));
|
||||
validate();
|
||||
refreshMandatory();
|
||||
}
|
||||
};
|
||||
|
||||
// @return string HTML representing an option
|
||||
function formatItem(item) {
|
||||
if (!item.id) {
|
||||
// return `text` for optgroup
|
||||
return item.text;
|
||||
}
|
||||
var option = convertValueToObj(item.id);
|
||||
var icon = (option.entity_type === 'civicrm_mailing') ? 'fa-envelope' : 'fa-users';
|
||||
var spanClass = (option.mode == 'exclude') ? 'crmMailing-exclude' : 'crmMailing-include';
|
||||
if (option.entity_type != 'civicrm_mailing' && isMandatory(option.entity_id)) {
|
||||
spanClass = 'crmMailing-mandatory';
|
||||
}
|
||||
return '<i class="crm-i '+icon+'"></i> <span class="' + spanClass + '">' + item.text + '</span>';
|
||||
}
|
||||
|
||||
function validate() {
|
||||
if (scope.$parent.$eval(attrs.ngRequired)) {
|
||||
var empty = (_.isEmpty(ngModel.$viewValue.groups.include) && _.isEmpty(ngModel.$viewValue.mailings.include));
|
||||
ngModel.$setValidity('empty', !empty);
|
||||
}
|
||||
else {
|
||||
ngModel.$setValidity('empty', true);
|
||||
}
|
||||
}
|
||||
|
||||
var rcpAjaxState = {
|
||||
input: '',
|
||||
entity: 'civicrm_group',
|
||||
type: 'include',
|
||||
page_n: 0,
|
||||
page_i: 0,
|
||||
};
|
||||
|
||||
$(element).select2({
|
||||
width: '36em',
|
||||
dropdownAutoWidth: true,
|
||||
placeholder: "Groups or Past Recipients",
|
||||
formatResult: formatItem,
|
||||
formatSelection: formatItem,
|
||||
escapeMarkup: function(m) {
|
||||
return m;
|
||||
},
|
||||
multiple: true,
|
||||
initSelection: function(el, cb) {
|
||||
var values = el.val().split(',');
|
||||
|
||||
var gids = [];
|
||||
var mids = [];
|
||||
|
||||
for (var i in values) {
|
||||
var dv = convertValueToObj(values[i]);
|
||||
if (dv.entity_type == 'civicrm_group') {
|
||||
gids.push(dv.entity_id);
|
||||
}
|
||||
else if (dv.entity_type == 'civicrm_mailing') {
|
||||
mids.push(dv.entity_id);
|
||||
}
|
||||
}
|
||||
|
||||
CRM.api3('Group', 'getlist', { params: { id: { IN: gids } }, extra: ["is_hidden"] }).then(
|
||||
function(glist) {
|
||||
CRM.api3('Mailing', 'getlist', { params: { id: { IN: mids } } }).then(
|
||||
function(mlist) {
|
||||
var datamap = [];
|
||||
|
||||
var groupNames = [];
|
||||
var civiMails = [];
|
||||
|
||||
$(glist.values).each(function (idx, group) {
|
||||
var key = group.id + ' civicrm_group include';
|
||||
groupNames.push({id: parseInt(group.id), title: group.label, is_hidden: group.extra.is_hidden});
|
||||
|
||||
if (values.indexOf(key) >= 0) {
|
||||
datamap.push({id: key, text: group.label});
|
||||
}
|
||||
|
||||
key = group.id + ' civicrm_group exclude';
|
||||
if (values.indexOf(key) >= 0) {
|
||||
datamap.push({id: key, text: group.label});
|
||||
}
|
||||
});
|
||||
|
||||
$(mlist.values).each(function (idx, group) {
|
||||
var key = group.id + ' civicrm_mailing include';
|
||||
civiMails.push({id: parseInt(group.id), name: group.label});
|
||||
|
||||
if (values.indexOf(key) >= 0) {
|
||||
datamap.push({id: key, text: group.label});
|
||||
}
|
||||
|
||||
key = group.id + ' civicrm_mailing exclude';
|
||||
if (values.indexOf(key) >= 0) {
|
||||
datamap.push({id: key, text: group.label});
|
||||
}
|
||||
});
|
||||
|
||||
scope.$parent.crmMailingConst.groupNames = groupNames;
|
||||
scope.$parent.crmMailingConst.civiMails = civiMails;
|
||||
|
||||
refreshMandatory();
|
||||
|
||||
cb(datamap);
|
||||
});
|
||||
});
|
||||
},
|
||||
ajax: {
|
||||
url: CRM.url('civicrm/ajax/rest'),
|
||||
quietMillis: 300,
|
||||
data: function(input, page_num) {
|
||||
if (page_num <= 1) {
|
||||
rcpAjaxState = {
|
||||
input: input,
|
||||
entity: 'civicrm_group',
|
||||
type: 'include',
|
||||
page_n: 0,
|
||||
};
|
||||
}
|
||||
|
||||
rcpAjaxState.page_i = page_num - rcpAjaxState.page_n;
|
||||
var filterParams = {};
|
||||
switch(rcpAjaxState.entity) {
|
||||
case 'civicrm_group':
|
||||
filterParams = { is_hidden: 0, is_active: 1, group_type: {"LIKE": "%2%"} };
|
||||
break;
|
||||
|
||||
case 'civicrm_mailing':
|
||||
filterParams = { is_hidden: 0, is_active: 1 };
|
||||
break;
|
||||
}
|
||||
var params = {
|
||||
input: input,
|
||||
page_num: rcpAjaxState.page_i,
|
||||
params: filterParams,
|
||||
};
|
||||
return params;
|
||||
},
|
||||
transport: function(params) {
|
||||
switch(rcpAjaxState.entity) {
|
||||
case 'civicrm_group':
|
||||
CRM.api3('Group', 'getlist', params.data).then(params.success, params.error);
|
||||
break;
|
||||
|
||||
case 'civicrm_mailing':
|
||||
params.data.params.options = { sort: "is_archived asc, scheduled_date desc" };
|
||||
CRM.api3('Mailing', 'getlist', params.data).then(params.success, params.error);
|
||||
break;
|
||||
}
|
||||
},
|
||||
results: function(data) {
|
||||
results = {
|
||||
children: $.map(data.values, function(obj) {
|
||||
return { id: obj.id + ' ' + rcpAjaxState.entity + ' ' + rcpAjaxState.type,
|
||||
text: obj.label };
|
||||
})
|
||||
};
|
||||
|
||||
if (rcpAjaxState.page_i == 1 && data.count) {
|
||||
results.text = ts((rcpAjaxState.type == 'include'? 'Include ' : 'Exclude ') +
|
||||
(rcpAjaxState.entity == 'civicrm_group'? 'Group' : 'Mailing'));
|
||||
}
|
||||
|
||||
more = data.more_results || !(rcpAjaxState.entity == 'civicrm_mailing' && rcpAjaxState.type == 'exclude');
|
||||
|
||||
if (more && !data.more_results) {
|
||||
if (rcpAjaxState.type == 'include') {
|
||||
rcpAjaxState.type = 'exclude';
|
||||
} else {
|
||||
rcpAjaxState.type = 'include';
|
||||
rcpAjaxState.entity = 'civicrm_mailing';
|
||||
}
|
||||
rcpAjaxState.page_n += rcpAjaxState.page_i;
|
||||
}
|
||||
|
||||
return { more: more, results: [ results ] };
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
$(element).on('select2-selecting', function(e) {
|
||||
var option = convertValueToObj(e.val);
|
||||
var typeKey = option.entity_type == 'civicrm_mailing' ? 'mailings' : 'groups';
|
||||
if (option.mode == 'exclude') {
|
||||
ngModel.$viewValue[typeKey].exclude.push(option.entity_id);
|
||||
arrayRemove(ngModel.$viewValue[typeKey].include, option.entity_id);
|
||||
}
|
||||
else {
|
||||
ngModel.$viewValue[typeKey].include.push(option.entity_id);
|
||||
arrayRemove(ngModel.$viewValue[typeKey].exclude, option.entity_id);
|
||||
}
|
||||
scope.$apply();
|
||||
$(element).select2('close');
|
||||
validate();
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
$(element).on("select2-removing", function(e) {
|
||||
var option = convertValueToObj(e.val);
|
||||
var typeKey = option.entity_type == 'civicrm_mailing' ? 'mailings' : 'groups';
|
||||
if (typeKey == 'groups' && isMandatory(option.entity_id)) {
|
||||
crmUiAlert({
|
||||
text: ts('This mailing was generated based on search results. The search results cannot be removed.'),
|
||||
title: ts('Required')
|
||||
});
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
scope.$parent.$apply(function() {
|
||||
arrayRemove(ngModel.$viewValue[typeKey][option.mode], option.entity_id);
|
||||
});
|
||||
validate();
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
scope.$watchCollection("recips.groups.include", refreshUI);
|
||||
scope.$watchCollection("recips.groups.exclude", refreshUI);
|
||||
scope.$watchCollection("recips.mailings.include", refreshUI);
|
||||
scope.$watchCollection("recips.mailings.exclude", refreshUI);
|
||||
setTimeout(refreshUI, 50);
|
||||
|
||||
scope.$watchCollection(attrs.crmAvailGroups, function() {
|
||||
scope.groups = scope.$parent.$eval(attrs.crmAvailGroups);
|
||||
});
|
||||
scope.$watchCollection(attrs.crmAvailMailings, function() {
|
||||
scope.mailings = scope.$parent.$eval(attrs.crmAvailMailings);
|
||||
});
|
||||
scope.$watchCollection(attrs.crmMandatoryGroups, function() {
|
||||
refreshMandatory();
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
})(angular, CRM.$, CRM._);
|
28
sites/all/modules/civicrm/ang/crmMailing/ReviewBool.js
Normal file
28
sites/all/modules/civicrm/ang/crmMailing/ReviewBool.js
Normal file
|
@ -0,0 +1,28 @@
|
|||
(function(angular, $, _) {
|
||||
angular.module('crmMailing').directive('crmMailingReviewBool', function() {
|
||||
return {
|
||||
scope: {
|
||||
crmOn: '@',
|
||||
crmTitle: '@'
|
||||
},
|
||||
template: '<span ng-class="spanClasses"><i class="crm-i" ng-class="iconClasses"></i> {{evalTitle}} </span>',
|
||||
link: function(scope, element, attrs) {
|
||||
function refresh() {
|
||||
if (scope.$parent.$eval(attrs.crmOn)) {
|
||||
scope.spanClasses = {'crmMailing-active': true};
|
||||
scope.iconClasses = {'fa-check': true};
|
||||
}
|
||||
else {
|
||||
scope.spanClasses = {'crmMailing-inactive': true};
|
||||
scope.iconClasses = {'fa-times': true};
|
||||
}
|
||||
scope.evalTitle = scope.$parent.$eval(attrs.crmTitle);
|
||||
}
|
||||
|
||||
refresh();
|
||||
scope.$parent.$watch(attrs.crmOn, refresh);
|
||||
scope.$parent.$watch(attrs.crmTitle, refresh);
|
||||
}
|
||||
};
|
||||
});
|
||||
})(angular, CRM.$, CRM._);
|
|
@ -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>
|
|
@ -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._);
|
28
sites/all/modules/civicrm/ang/crmMailing/Token.js
Normal file
28
sites/all/modules/civicrm/ang/crmMailing/Token.js
Normal file
|
@ -0,0 +1,28 @@
|
|||
(function(angular, $, _) {
|
||||
// example: <input name="subject" /> <input crm-mailing-token on-select="doSomething(token.name)" />
|
||||
// WISHLIST: Instead of global CRM.crmMailing.mailTokens, accept token list as an input
|
||||
angular.module('crmMailing').directive('crmMailingToken', function() {
|
||||
return {
|
||||
require: '^crmUiIdScope',
|
||||
scope: {
|
||||
onSelect: '@'
|
||||
},
|
||||
template: '<input type="text" class="crmMailingToken" />',
|
||||
link: function(scope, element, attrs, crmUiIdCtrl) {
|
||||
$(element).addClass('crm-action-menu fa-code').crmSelect2({
|
||||
width: "12em",
|
||||
dropdownAutoWidth: true,
|
||||
data: CRM.crmMailing.mailTokens,
|
||||
placeholder: ts('Tokens')
|
||||
});
|
||||
$(element).on('select2-selecting', function(e) {
|
||||
e.preventDefault();
|
||||
$(element).select2('close').select2('val', '');
|
||||
scope.$parent.$eval(attrs.onSelect, {
|
||||
token: {name: e.val}
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
})(angular, CRM.$, CRM._);
|
56
sites/all/modules/civicrm/ang/crmMailing/ViewRecipCtrl.js
Normal file
56
sites/all/modules/civicrm/ang/crmMailing/ViewRecipCtrl.js
Normal file
|
@ -0,0 +1,56 @@
|
|||
(function(angular, $, _) {
|
||||
|
||||
angular.module('crmMailing').controller('ViewRecipCtrl', function ViewRecipCtrl($scope) {
|
||||
$scope.getIncludesAsString = function(mailing) {
|
||||
var first = true;
|
||||
var names = '';
|
||||
_.each(mailing.recipients.groups.include, function(id) {
|
||||
var group = _.where(CRM.crmMailing.groupNames, {id: '' + id});
|
||||
if (group.length) {
|
||||
if (!first) {
|
||||
names = names + ', ';
|
||||
}
|
||||
names = names + group[0].title;
|
||||
first = false;
|
||||
}
|
||||
});
|
||||
_.each(mailing.recipients.mailings.include, function(id) {
|
||||
var oldMailing = _.where(CRM.crmMailing.civiMails, {id: '' + id});
|
||||
if (oldMailing.length) {
|
||||
if (!first) {
|
||||
names = names + ', ';
|
||||
}
|
||||
names = names + oldMailing[0].name;
|
||||
first = false;
|
||||
}
|
||||
});
|
||||
return names;
|
||||
};
|
||||
$scope.getExcludesAsString = function(mailing) {
|
||||
var first = true;
|
||||
var names = '';
|
||||
_.each(mailing.recipients.groups.exclude, function(id) {
|
||||
var group = _.where(CRM.crmMailing.groupNames, {id: '' + id});
|
||||
if (group.length) {
|
||||
if (!first) {
|
||||
names = names + ', ';
|
||||
}
|
||||
names = names + group[0].title;
|
||||
first = false;
|
||||
}
|
||||
});
|
||||
_.each(mailing.recipients.mailings.exclude, function(id) {
|
||||
var oldMailing = _.where(CRM.crmMailing.civiMails, {id: '' + id});
|
||||
if (oldMailing.length) {
|
||||
if (!first) {
|
||||
names = names + ', ';
|
||||
}
|
||||
names = names + oldMailing[0].name;
|
||||
first = false;
|
||||
}
|
||||
});
|
||||
return names;
|
||||
};
|
||||
});
|
||||
|
||||
})(angular, CRM.$, CRM._);
|
558
sites/all/modules/civicrm/ang/crmMailing/services.js
Normal file
558
sites/all/modules/civicrm/ang/crmMailing/services.js
Normal file
|
@ -0,0 +1,558 @@
|
|||
(function (angular, $, _) {
|
||||
|
||||
// The representation of from/reply-to addresses is inconsistent in the mailing data-model,
|
||||
// so the UI must do some adaptation. The crmFromAddresses provides a richer way to slice/dice
|
||||
// the available "From:" addrs. Records are like the underlying OptionValues -- but add "email"
|
||||
// and "author".
|
||||
angular.module('crmMailing').factory('crmFromAddresses', function ($q, crmApi) {
|
||||
var emailRegex = /^"(.*)" <([^@>]*@[^@>]*)>$/;
|
||||
var addrs = _.map(CRM.crmMailing.fromAddress, function (addr) {
|
||||
var match = emailRegex.exec(addr.label);
|
||||
return angular.extend({}, addr, {
|
||||
email: match ? match[2] : '(INVALID)',
|
||||
author: match ? match[1] : '(INVALID)'
|
||||
});
|
||||
});
|
||||
|
||||
function first(array) {
|
||||
return (array.length === 0) ? null : array[0];
|
||||
}
|
||||
|
||||
return {
|
||||
getAll: function getAll() {
|
||||
return addrs;
|
||||
},
|
||||
getByAuthorEmail: function getByAuthorEmail(author, email, autocreate) {
|
||||
var result = null;
|
||||
_.each(addrs, function (addr) {
|
||||
if (addr.author == author && addr.email == email) {
|
||||
result = addr;
|
||||
}
|
||||
});
|
||||
if (!result && autocreate) {
|
||||
result = {
|
||||
label: '(INVALID) "' + author + '" <' + email + '>',
|
||||
author: author,
|
||||
email: email
|
||||
};
|
||||
addrs.push(result);
|
||||
}
|
||||
return result;
|
||||
},
|
||||
getByEmail: function getByEmail(email) {
|
||||
return first(_.where(addrs, {email: email}));
|
||||
},
|
||||
getByLabel: function (label) {
|
||||
return first(_.where(addrs, {label: label}));
|
||||
},
|
||||
getDefault: function getDefault() {
|
||||
return first(_.where(addrs, {is_default: "1"}));
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
angular.module('crmMailing').factory('crmMsgTemplates', function ($q, crmApi) {
|
||||
var tpls = _.map(CRM.crmMailing.mesTemplate, function (tpl) {
|
||||
return angular.extend({}, tpl, {
|
||||
//id: tpl parseInt(tpl.id)
|
||||
});
|
||||
});
|
||||
window.tpls = tpls;
|
||||
var lastModifiedTpl = null;
|
||||
return {
|
||||
// Get a template
|
||||
// @param id MessageTemplate id (per APIv3)
|
||||
// @return Promise MessageTemplate (per APIv3)
|
||||
get: function get(id) {
|
||||
return crmApi('MessageTemplate', 'getsingle', {
|
||||
"return": "id,msg_subject,msg_html,msg_title,msg_text",
|
||||
"id": id
|
||||
});
|
||||
},
|
||||
// Save a template
|
||||
// @param tpl MessageTemplate (per APIv3) For new templates, omit "id"
|
||||
// @return Promise MessageTemplate (per APIv3)
|
||||
save: function (tpl) {
|
||||
return crmApi('MessageTemplate', 'create', tpl).then(function (response) {
|
||||
if (!tpl.id) {
|
||||
tpl.id = '' + response.id; //parseInt(response.id);
|
||||
tpls.push(tpl);
|
||||
}
|
||||
lastModifiedTpl = tpl;
|
||||
return tpl;
|
||||
});
|
||||
},
|
||||
// @return Object MessageTemplate (per APIv3)
|
||||
getLastModifiedTpl: function () {
|
||||
return lastModifiedTpl;
|
||||
},
|
||||
getAll: function getAll() {
|
||||
return tpls;
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// The crmMailingMgr service provides business logic for loading, saving, previewing, etc
|
||||
angular.module('crmMailing').factory('crmMailingMgr', function ($q, crmApi, crmFromAddresses, crmQueue) {
|
||||
var qApi = crmQueue(crmApi);
|
||||
var pickDefaultMailComponent = function pickDefaultMailComponent(type) {
|
||||
var mcs = _.where(CRM.crmMailing.headerfooterList, {
|
||||
component_type: type,
|
||||
is_default: "1"
|
||||
});
|
||||
return (mcs.length >= 1) ? mcs[0].id : null;
|
||||
};
|
||||
|
||||
return {
|
||||
// @param scalar idExpr a number or the literal string 'new'
|
||||
// @return Promise|Object Mailing (per APIv3)
|
||||
getOrCreate: function getOrCreate(idExpr) {
|
||||
return (idExpr == 'new') ? this.create() : this.get(idExpr);
|
||||
},
|
||||
// @return Promise Mailing (per APIv3)
|
||||
get: function get(id) {
|
||||
var crmMailingMgr = this;
|
||||
var mailing;
|
||||
return qApi('Mailing', 'getsingle', {id: id})
|
||||
.then(function (getResult) {
|
||||
mailing = getResult;
|
||||
return $q.all([
|
||||
crmMailingMgr._loadGroups(mailing),
|
||||
crmMailingMgr._loadJobs(mailing)
|
||||
]);
|
||||
})
|
||||
.then(function () {
|
||||
return mailing;
|
||||
});
|
||||
},
|
||||
// Call MailingGroup.get and merge results into "mailing"
|
||||
_loadGroups: function (mailing) {
|
||||
return crmApi('MailingGroup', 'get', {mailing_id: mailing.id})
|
||||
.then(function (groupResult) {
|
||||
mailing.recipients = {};
|
||||
mailing.recipients.groups = {include: [], exclude: [], base: []};
|
||||
mailing.recipients.mailings = {include: [], exclude: []};
|
||||
_.each(groupResult.values, function (mailingGroup) {
|
||||
var bucket = (/^civicrm_group/.test(mailingGroup.entity_table)) ? 'groups' : 'mailings';
|
||||
var entityId = parseInt(mailingGroup.entity_id);
|
||||
mailing.recipients[bucket][mailingGroup.group_type.toLowerCase()].push(entityId);
|
||||
});
|
||||
});
|
||||
},
|
||||
// Call MailingJob.get and merge results into "mailing"
|
||||
_loadJobs: function (mailing) {
|
||||
return crmApi('MailingJob', 'get', {mailing_id: mailing.id, is_test: 0})
|
||||
.then(function (jobResult) {
|
||||
mailing.jobs = mailing.jobs || {};
|
||||
angular.extend(mailing.jobs, jobResult.values);
|
||||
});
|
||||
},
|
||||
// @return Object Mailing (per APIv3)
|
||||
create: function create(params) {
|
||||
var defaults = {
|
||||
jobs: {}, // {jobId: JobRecord}
|
||||
recipients: {
|
||||
groups: {include: [], exclude: [], base: []},
|
||||
mailings: {include: [], exclude: []}
|
||||
},
|
||||
template_type: "traditional",
|
||||
// Workaround CRM-19756 w/template_options.nonce
|
||||
template_options: {nonce: 1},
|
||||
name: "",
|
||||
campaign_id: null,
|
||||
replyto_email: "",
|
||||
subject: "",
|
||||
body_html: "",
|
||||
body_text: ""
|
||||
};
|
||||
return angular.extend({}, defaults, params);
|
||||
},
|
||||
|
||||
// @param mailing Object (per APIv3)
|
||||
// @return Promise
|
||||
'delete': function (mailing) {
|
||||
if (mailing.id) {
|
||||
return qApi('Mailing', 'delete', {id: mailing.id});
|
||||
}
|
||||
else {
|
||||
var d = $q.defer();
|
||||
d.resolve();
|
||||
return d.promise;
|
||||
}
|
||||
},
|
||||
|
||||
// Search the body, header, and footer for required tokens.
|
||||
// ex: var msgs = findMissingTokens(mailing, 'body_html');
|
||||
findMissingTokens: function(mailing, field) {
|
||||
var missing = {};
|
||||
if (!_.isEmpty(mailing[field]) && !CRM.crmMailing.disableMandatoryTokensCheck) {
|
||||
var body = '';
|
||||
if (mailing.footer_id) {
|
||||
var footer = _.where(CRM.crmMailing.headerfooterList, {id: mailing.footer_id});
|
||||
body = body + footer[0][field];
|
||||
|
||||
}
|
||||
body = body + mailing[field];
|
||||
if (mailing.header_id) {
|
||||
var header = _.where(CRM.crmMailing.headerfooterList, {id: mailing.header_id});
|
||||
body = body + header[0][field];
|
||||
}
|
||||
|
||||
angular.forEach(CRM.crmMailing.requiredTokens, function(value, token) {
|
||||
if (!_.isObject(value)) {
|
||||
if (body.indexOf('{' + token + '}') < 0) {
|
||||
missing[token] = value;
|
||||
}
|
||||
}
|
||||
else {
|
||||
var count = 0;
|
||||
angular.forEach(value, function(nestedValue, nestedToken) {
|
||||
if (body.indexOf('{' + nestedToken + '}') >= 0) {
|
||||
count++;
|
||||
}
|
||||
});
|
||||
if (count === 0) {
|
||||
angular.extend(missing, value);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
return missing;
|
||||
},
|
||||
|
||||
// Copy all data fields in (mailingFrom) to (mailingTgt) -- except for (excludes)
|
||||
// ex: crmMailingMgr.mergeInto(newMailing, mailingTemplate, ['subject']);
|
||||
mergeInto: function mergeInto(mailingTgt, mailingFrom, excludes) {
|
||||
var MAILING_FIELDS = [
|
||||
// always exclude: 'id'
|
||||
'name',
|
||||
'campaign_id',
|
||||
'from_name',
|
||||
'from_email',
|
||||
'replyto_email',
|
||||
'subject',
|
||||
'dedupe_email',
|
||||
'recipients',
|
||||
'body_html',
|
||||
'body_text',
|
||||
'footer_id',
|
||||
'header_id',
|
||||
'visibility',
|
||||
'url_tracking',
|
||||
'dedupe_email',
|
||||
'forward_replies',
|
||||
'auto_responder',
|
||||
'open_tracking',
|
||||
'override_verp',
|
||||
'optout_id',
|
||||
'reply_id',
|
||||
'resubscribe_id',
|
||||
'unsubscribe_id'
|
||||
];
|
||||
if (!excludes) {
|
||||
excludes = [];
|
||||
}
|
||||
_.each(MAILING_FIELDS, function (field) {
|
||||
if (!_.contains(excludes, field)) {
|
||||
mailingTgt[field] = mailingFrom[field];
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// @param mailing Object (per APIv3)
|
||||
// @return Promise an object with "subject", "body_text", "body_html"
|
||||
preview: function preview(mailing) {
|
||||
if (CRM.crmMailing.workflowEnabled && !CRM.checkPerm('create mailings') && !CRM.checkPerm('access CiviMail')) {
|
||||
return qApi('Mailing', 'preview', {id: mailing.id}).then(function(result) {
|
||||
return result.values;
|
||||
});
|
||||
}
|
||||
else {
|
||||
// Protect against races in saving and previewing by chaining create+preview.
|
||||
var params = angular.extend({}, mailing, mailing.recipients, {
|
||||
options: {force_rollback: 1},
|
||||
'api.Mailing.preview': {
|
||||
id: '$value.id'
|
||||
}
|
||||
});
|
||||
delete params.recipients; // the content was merged in
|
||||
return qApi('Mailing', 'create', params).then(function(result) {
|
||||
mailing.modified_date = result.values[result.id].modified_date;
|
||||
// changes rolled back, so we don't care about updating mailing
|
||||
return result.values[result.id]['api.Mailing.preview'].values;
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// @param mailing Object (per APIv3)
|
||||
// @param int previewLimit
|
||||
// @return Promise for a list of recipients (mailing_id, contact_id, api.contact.getvalue, api.email.getvalue)
|
||||
previewRecipients: function previewRecipients(mailing, previewLimit) {
|
||||
// To get list of recipients, we tentatively save the mailing and
|
||||
// get the resulting recipients -- then rollback any changes.
|
||||
var params = angular.extend({}, mailing, mailing.recipients, {
|
||||
name: 'placeholder', // for previewing recipients on new, incomplete mailing
|
||||
subject: 'placeholder', // for previewing recipients on new, incomplete mailing
|
||||
options: {force_rollback: 1},
|
||||
'api.mailing_job.create': 1, // note: exact match to API default
|
||||
'api.MailingRecipients.get': {
|
||||
mailing_id: '$value.id',
|
||||
options: {limit: previewLimit},
|
||||
'api.contact.getvalue': {'return': 'display_name'},
|
||||
'api.email.getvalue': {'return': 'email'}
|
||||
}
|
||||
});
|
||||
delete params.recipients; // the content was merged in
|
||||
return qApi('Mailing', 'create', params).then(function (recipResult) {
|
||||
// changes rolled back, so we don't care about updating mailing
|
||||
mailing.modified_date = recipResult.values[recipResult.id].modified_date;
|
||||
return recipResult.values[recipResult.id]['api.MailingRecipients.get'].values;
|
||||
});
|
||||
},
|
||||
|
||||
previewRecipientCount: function previewRecipientCount(mailing) {
|
||||
// To get list of recipients, we tentatively save the mailing and
|
||||
// get the resulting recipients -- then rollback any changes.
|
||||
var params = angular.extend({}, mailing, mailing.recipients, {
|
||||
name: 'placeholder', // for previewing recipients on new, incomplete mailing
|
||||
subject: 'placeholder', // for previewing recipients on new, incomplete mailing
|
||||
options: {force_rollback: 1},
|
||||
'api.mailing_job.create': 1, // note: exact match to API default
|
||||
'api.MailingRecipients.getcount': {
|
||||
mailing_id: '$value.id'
|
||||
}
|
||||
});
|
||||
delete params.recipients; // the content was merged in
|
||||
return qApi('Mailing', 'create', params).then(function (recipResult) {
|
||||
// changes rolled back, so we don't care about updating mailing
|
||||
mailing.modified_date = recipResult.values[recipResult.id].modified_date;
|
||||
return recipResult.values[recipResult.id]['api.MailingRecipients.getcount'];
|
||||
});
|
||||
},
|
||||
|
||||
// Save a (draft) mailing
|
||||
// @param mailing Object (per APIv3)
|
||||
// @return Promise
|
||||
save: function(mailing) {
|
||||
var params = angular.extend({}, mailing, mailing.recipients);
|
||||
|
||||
// Angular ngModel sometimes treats blank fields as undefined.
|
||||
angular.forEach(mailing, function(value, key) {
|
||||
if (value === undefined || value === null) {
|
||||
mailing[key] = '';
|
||||
}
|
||||
});
|
||||
|
||||
// WORKAROUND: Mailing.create (aka CRM_Mailing_BAO_Mailing::create()) interprets scheduled_date
|
||||
// as an *intent* to schedule and creates tertiary records. Saving a draft with a scheduled_date
|
||||
// is therefore not allowed. Remove this after fixing Mailing.create's contract.
|
||||
delete params.scheduled_date;
|
||||
|
||||
delete params.jobs;
|
||||
|
||||
delete params.recipients; // the content was merged in
|
||||
|
||||
return qApi('Mailing', 'create', params).then(function(result) {
|
||||
if (result.id && !mailing.id) {
|
||||
mailing.id = result.id;
|
||||
} // no rollback, so update mailing.id
|
||||
// Perhaps we should reload mailing based on result?
|
||||
mailing.modified_date = result.values[result.id].modified_date;
|
||||
return mailing;
|
||||
});
|
||||
},
|
||||
|
||||
// Schedule/send the mailing
|
||||
// @param mailing Object (per APIv3)
|
||||
// @return Promise
|
||||
submit: function (mailing) {
|
||||
var crmMailingMgr = this;
|
||||
var params = {
|
||||
id: mailing.id,
|
||||
approval_date: 'now',
|
||||
scheduled_date: mailing.scheduled_date ? mailing.scheduled_date : 'now'
|
||||
};
|
||||
return qApi('Mailing', 'submit', params)
|
||||
.then(function (result) {
|
||||
angular.extend(mailing, result.values[result.id]); // Perhaps we should reload mailing based on result?
|
||||
return crmMailingMgr._loadJobs(mailing);
|
||||
})
|
||||
.then(function () {
|
||||
return mailing;
|
||||
});
|
||||
},
|
||||
|
||||
// Immediately send a test message
|
||||
// @param mailing Object (per APIv3)
|
||||
// @param to Object with either key "email" (string) or "gid" (int)
|
||||
// @return Promise for a list of delivery reports
|
||||
sendTest: function (mailing, recipient) {
|
||||
var params = angular.extend({}, mailing, mailing.recipients, {
|
||||
// options: {force_rollback: 1}, // Test mailings include tracking features, so the mailing must be persistent
|
||||
'api.Mailing.send_test': {
|
||||
mailing_id: '$value.id',
|
||||
test_email: recipient.email,
|
||||
test_group: recipient.gid
|
||||
}
|
||||
});
|
||||
|
||||
// WORKAROUND: Mailing.create (aka CRM_Mailing_BAO_Mailing::create()) interprets scheduled_date
|
||||
// as an *intent* to schedule and creates tertiary records. Saving a draft with a scheduled_date
|
||||
// is therefore not allowed. Remove this after fixing Mailing.create's contract.
|
||||
delete params.scheduled_date;
|
||||
|
||||
delete params.jobs;
|
||||
|
||||
delete params.recipients; // the content was merged in
|
||||
|
||||
return qApi('Mailing', 'create', params).then(function (result) {
|
||||
if (result.id && !mailing.id) {
|
||||
mailing.id = result.id;
|
||||
} // no rollback, so update mailing.id
|
||||
mailing.modified_date = result.values[result.id].modified_date;
|
||||
return result.values[result.id]['api.Mailing.send_test'].values;
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// The preview manager performs preview actions while putting up a visible UI (e.g. dialogs & status alerts)
|
||||
angular.module('crmMailing').factory('crmMailingPreviewMgr', function (dialogService, crmMailingMgr, crmStatus) {
|
||||
return {
|
||||
// @param mode string one of 'html', 'text', or 'full'
|
||||
// @return Promise
|
||||
preview: function preview(mailing, mode) {
|
||||
var templates = {
|
||||
html: '~/crmMailing/PreviewMgr/html.html',
|
||||
text: '~/crmMailing/PreviewMgr/text.html',
|
||||
full: '~/crmMailing/PreviewMgr/full.html'
|
||||
};
|
||||
var result = null;
|
||||
var p = crmMailingMgr
|
||||
.preview(mailing)
|
||||
.then(function (content) {
|
||||
var options = CRM.utils.adjustDialogDefaults({
|
||||
autoOpen: false,
|
||||
title: ts('Subject: %1', {
|
||||
1: content.subject
|
||||
})
|
||||
});
|
||||
result = dialogService.open('previewDialog', templates[mode], content, options);
|
||||
});
|
||||
crmStatus({start: ts('Previewing...'), success: ''}, p);
|
||||
return result;
|
||||
},
|
||||
|
||||
// @param to Object with either key "email" (string) or "gid" (int)
|
||||
// @return Promise
|
||||
sendTest: function sendTest(mailing, recipient) {
|
||||
var promise = crmMailingMgr.sendTest(mailing, recipient)
|
||||
.then(function (deliveryInfos) {
|
||||
var count = Object.keys(deliveryInfos).length;
|
||||
if (count === 0) {
|
||||
CRM.alert(ts('Could not identify any recipients. Perhaps the group is empty?'));
|
||||
}
|
||||
})
|
||||
;
|
||||
return crmStatus({start: ts('Sending...'), success: ts('Sent')}, promise);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
angular.module('crmMailing').factory('crmMailingStats', function (crmApi, crmLegacy) {
|
||||
var statTypes = [
|
||||
// {name: 'Recipients', title: ts('Intended Recipients'), searchFilter: '', eventsFilter: '&event=queue', reportType: 'detail', reportFilter: ''},
|
||||
{name: 'Delivered', title: ts('Successful Deliveries'), searchFilter: '&mailing_delivery_status=Y', eventsFilter: '&event=delivered', reportType: 'detail', reportFilter: '&delivery_status_value=successful'},
|
||||
{name: 'Opened', title: ts('Tracked Opens'), searchFilter: '&mailing_open_status=Y', eventsFilter: '&event=opened', reportType: 'opened', reportFilter: ''},
|
||||
{name: 'Unique Clicks', title: ts('Click-throughs'), searchFilter: '&mailing_click_status=Y', eventsFilter: '&event=click&distinct=1', reportType: 'clicks', reportFilter: ''},
|
||||
// {name: 'Forward', title: ts('Forwards'), searchFilter: '&mailing_forward=1', eventsFilter: '&event=forward', reportType: 'detail', reportFilter: '&is_forwarded_value=1'},
|
||||
// {name: 'Replies', title: ts('Replies'), searchFilter: '&mailing_reply_status=Y', eventsFilter: '&event=reply', reportType: 'detail', reportFilter: '&is_replied_value=1'},
|
||||
{name: 'Bounces', title: ts('Bounces'), searchFilter: '&mailing_delivery_status=N', eventsFilter: '&event=bounce', reportType: 'bounce', reportFilter: ''},
|
||||
{name: 'Unsubscribers', title: ts('Unsubscribes'), searchFilter: '&mailing_unsubscribe=1', eventsFilter: '&event=unsubscribe', reportType: 'detail', reportFilter: '&is_unsubscribed_value=1'},
|
||||
// {name: 'OptOuts', title: ts('Opt-Outs'), searchFilter: '&mailing_optout=1', eventsFilter: '&event=optout', reportType: 'detail', reportFilter: ''}
|
||||
];
|
||||
|
||||
return {
|
||||
getStatTypes: function() {
|
||||
return statTypes;
|
||||
},
|
||||
|
||||
/**
|
||||
* @param mailingIds object
|
||||
* List of mailing IDs ({a: 123, b: 456})
|
||||
* @return Promise
|
||||
* List of stats for each mailing
|
||||
* ({a: ...object..., b: ...object...})
|
||||
*/
|
||||
getStats: function(mailingIds) {
|
||||
var params = {};
|
||||
angular.forEach(mailingIds, function(mailingId, name) {
|
||||
params[name] = ['Mailing', 'stats', {mailing_id: mailingId, is_distinct: 0}];
|
||||
});
|
||||
return crmApi(params).then(function(result) {
|
||||
var stats = {};
|
||||
angular.forEach(mailingIds, function(mailingId, name) {
|
||||
stats[name] = result[name].values[mailingId];
|
||||
});
|
||||
return stats;
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Determine the legacy URL for a report about a given mailing and stat.
|
||||
*
|
||||
* @param mailing object
|
||||
* @param statType object (see statTypes above)
|
||||
* @param view string ('search', 'event', 'report')
|
||||
* @param returnPath string|null Return path (relative to Angular base)
|
||||
* @return string|null
|
||||
*/
|
||||
getUrl: function getUrl(mailing, statType, view, returnPath) {
|
||||
switch (view) {
|
||||
case 'events':
|
||||
var retParams = returnPath ? '&context=angPage&angPage=' + returnPath : '';
|
||||
return crmLegacy.url('civicrm/mailing/report/event',
|
||||
'reset=1&mid=' + mailing.id + statType.eventsFilter + retParams);
|
||||
case 'search':
|
||||
return crmLegacy.url('civicrm/contact/search/advanced',
|
||||
'force=1&mailing_id=' + mailing.id + statType.searchFilter);
|
||||
case 'report':
|
||||
var reportIds = CRM.crmMailing.reportIds;
|
||||
return crmLegacy.url('civicrm/report/instance/' + reportIds[statType.reportType],
|
||||
'reset=1&mailing_id_value=' + mailing.id + statType.reportFilter);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// crmMailingSimpleDirective is a template/factory-function for constructing very basic
|
||||
// directives that accept a "mailing" argument. Please don't overload it. If one continues building
|
||||
// this, it risks becoming a second system that violates Angular architecture (and prevents one
|
||||
// from using standard Angular docs+plugins). So this really shouldn't do much -- it is really
|
||||
// only for simple directives. For something complex, suck it up and write 10 lines of boilerplate.
|
||||
angular.module('crmMailing').factory('crmMailingSimpleDirective', function ($q, crmMetadata, crmUiHelp) {
|
||||
return function crmMailingSimpleDirective(directiveName, templateUrl) {
|
||||
return {
|
||||
scope: {
|
||||
crmMailing: '@'
|
||||
},
|
||||
templateUrl: templateUrl,
|
||||
link: function (scope, elm, attr) {
|
||||
scope.$parent.$watch(attr.crmMailing, function(newValue){
|
||||
scope.mailing = newValue;
|
||||
});
|
||||
scope.crmMailingConst = CRM.crmMailing;
|
||||
scope.ts = CRM.ts(null);
|
||||
scope.hs = crmUiHelp({file: 'CRM/Mailing/MailingUI'});
|
||||
scope[directiveName] = attr[directiveName] ? scope.$parent.$eval(attr[directiveName]) : {};
|
||||
$q.when(crmMetadata.getFields('Mailing'), function(fields) {
|
||||
scope.mailingFields = fields;
|
||||
});
|
||||
}
|
||||
};
|
||||
};
|
||||
});
|
||||
|
||||
})(angular, CRM.$, CRM._);
|
Loading…
Add table
Add a link
Reference in a new issue