summaryrefslogtreecommitdiff
path: root/www/crm/wp-content/plugins/civicrm/civicrm/ang
diff options
context:
space:
mode:
Diffstat (limited to 'www/crm/wp-content/plugins/civicrm/civicrm/ang')
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/angularFileUpload.ang.php9
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/checklist-model.ang.php10
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmApp.ang.php9
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmApp.js23
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmAttachment.ang.php15
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmAttachment.css21
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmAttachment.js167
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmAttachment/attachments.html46
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmAutosave.ang.php10
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmAutosave.js118
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmCaseType.ang.php14
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmCaseType.css43
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmCaseType.js648
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmCaseType/activityTypesTable.html46
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmCaseType/caseTypeDetails.html67
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmCaseType/edit.html63
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmCaseType/list.html78
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmCaseType/rolesTable.html38
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmCaseType/sequenceTable.html41
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmCaseType/statusTable.html35
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmCaseType/timelineTable.html117
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmCxn.ang.php12
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmCxn.css3
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmCxn.js25
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmCxn/AboutCtrl.html14
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmCxn/AboutCtrl.js5
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmCxn/AdvTable.html18
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmCxn/AdvTable.js20
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmCxn/CheckAddress.js33
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmCxn/ConfirmConnectCtrl.html15
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmCxn/ConfirmConnectCtrl.js5
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmCxn/ConfirmReconnectCtrl.html23
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmCxn/ConfirmReconnectCtrl.js5
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmCxn/Connectivity.html4
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmCxn/LinkDialogCtrl.html3
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmCxn/LinkDialogCtrl.js11
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmCxn/ManageCtrl.html120
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmCxn/ManageCtrl.js153
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmCxn/PermTable.html42
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmCxn/PermTable.js27
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmD3.ang.php16
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmD3.js3
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmExample.ang.php11
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmExample.js45
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmExample/example.html42
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing.ang.php18
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing.css113
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing.js61
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockApprove.html14
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockApprove.js5
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockHeaderFooter.html32
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockHeaderFooter.js5
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockMailing.html82
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockMailing.js5
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockPreview.html57
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockPreview.js65
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockPublication.html16
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockPublication.js5
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockRecipients.html15
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockRecipients.js5
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockResponses.html82
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockResponses.js5
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockReview.html61
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockReview.js26
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockSchedule.html13
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockSchedule.js5
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockSummary.html29
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockSummary.js5
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockTemplates.html9
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockTemplates.js5
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockTracking.html14
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockTracking.js5
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BodyHtml.html26
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BodyHtml.js5
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BodyText.html24
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BodyText.js5
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/CreateMailingCtrl.js8
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/EditMailingCtrl.js133
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/EditMailingCtrl/2step.html63
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/EditMailingCtrl/base.html8
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/EditMailingCtrl/unified.html50
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/EditMailingCtrl/unified2.html46
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/EditMailingCtrl/wizard.html66
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/EditMailingCtrl/workflow.html73
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/EditRecipCtrl.js136
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/EditRecipOptionsDialogCtrl.html41
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/EditRecipOptionsDialogCtrl.js12
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/EditUnsubGroupCtrl.js19
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/EmailAddrCtrl.js31
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/EmailBodyCtrl.js52
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/EmailBodyCtrl/tokenAlert.html76
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/FromAddress.js30
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/ListMailingsCtrl.js10
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/MsgTemplateCtrl.js44
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/PreviewComponentCtrl.js24
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/PreviewComponentDialogCtrl.html28
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/PreviewComponentDialogCtrl.js13
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/PreviewMailingDialogCtrl.js12
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/PreviewMgr/full.html10
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/PreviewMgr/html.html3
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/PreviewMgr/text.html3
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/PreviewRecipCtrl.html32
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/PreviewRecipCtrl.js10
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/RadioDate.js116
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/Recipients.js341
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/ReviewBool.js28
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/SaveMsgTemplateDialogCtrl.html17
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/SaveMsgTemplateDialogCtrl.js83
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/Templates.js131
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/Token.js28
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/ViewRecipCtrl.js128
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/services.js582
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB.ang.php19
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB.css25
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB.js44
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB/BlockMailing.html196
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB/BlockMailing.js32
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB/BlockSetup.html67
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB/BlockSetup.js32
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB/EditCtrl.js149
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB/EditCtrl/edit.html178
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB/EditCtrl/main.html10
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB/EditCtrl/report.html194
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB/ListCtrl.html63
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB/ListCtrl.js12
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB/NewCtrl.js11
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB/ReportCtrl.js56
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB/Slider.html25
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB/Slider.js60
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB/Stats.js280
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB/WinnerDialogCtrl.html19
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB/WinnerDialogCtrl.js43
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB/services.js234
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmResource.ang.php11
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmRouteBinder.ang.php12
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmRouteBinder.js111
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmRouteBinder.md106
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmStatusPage.ang.php15
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmStatusPage.css82
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmStatusPage.js19
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmStatusPage/SnoozeOptions.html11
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmStatusPage/StatusPage.html39
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmStatusPage/StatusPageCtrl.js74
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmStatusPage/StatusPageServices.js31
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmUi.ang.php14
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmUi.js1092
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmUi/field-cb.html8
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmUi/field.html6
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmUi/tabset.html12
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmUi/wizard.html15
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmUtil.ang.php10
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmUtil.js361
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/dialogService.ang.php11
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/ngRoute.ang.php9
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/ngSanitize.ang.php9
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/ui.bootstrap.ang.php11
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/ui.bootstrap.css1
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/ui.sortable.ang.php9
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/ui.utils.ang.php9
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/unsavedChanges.ang.php9
160 files changed, 9423 insertions, 0 deletions
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/angularFileUpload.ang.php b/www/crm/wp-content/plugins/civicrm/civicrm/ang/angularFileUpload.ang.php
new file mode 100644
index 00000000..68f4aa99
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/angularFileUpload.ang.php
@@ -0,0 +1,9 @@
+<?php
+// This file declares an Angular module which can be autoloaded
+// in CiviCRM. See also:
+// http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_angularModules
+
+return [
+ 'ext' => 'civicrm',
+ 'js' => ['bower_components/angular-file-upload/angular-file-upload.min.js'],
+];
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/checklist-model.ang.php b/www/crm/wp-content/plugins/civicrm/civicrm/ang/checklist-model.ang.php
new file mode 100644
index 00000000..ffa8613b
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/checklist-model.ang.php
@@ -0,0 +1,10 @@
+<?php
+// This file declares an Angular module which can be autoloaded
+// in CiviCRM. See also:
+// http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_angularModules
+
+return [
+ 'ext' => 'civicrm',
+ 'basePages' => [],
+ 'js' => ['bower_components/checklist-model/checklist-model.js'],
+];
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmApp.ang.php b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmApp.ang.php
new file mode 100644
index 00000000..d4179315
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmApp.ang.php
@@ -0,0 +1,9 @@
+<?php
+// This file declares an Angular module which can be autoloaded
+// in CiviCRM. See also:
+// http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_angularModules
+
+return [
+ 'ext' => 'civicrm',
+ 'js' => ['ang/crmApp.js'],
+];
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmApp.js b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmApp.js
new file mode 100644
index 00000000..c7bb81e2
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmApp.js
@@ -0,0 +1,23 @@
+(function(angular, CRM) {
+ // crmApp is the default application which aggregates all known modules.
+ // crmApp should not provide any significant services, and no other
+ // modules should depend on it.
+ var crmApp = angular.module('crmApp', CRM.angular.modules);
+ crmApp.config(['$routeProvider',
+ function($routeProvider) {
+
+ if (CRM.crmApp.defaultRoute) {
+ $routeProvider.when('/', {
+ template: '<div></div>',
+ controller: function($location) {
+ $location.path(CRM.crmApp.defaultRoute);
+ }
+ });
+ }
+
+ $routeProvider.otherwise({
+ template: ts('Unknown path')
+ });
+ }
+ ]);
+})(angular, CRM);
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmAttachment.ang.php b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmAttachment.ang.php
new file mode 100644
index 00000000..e278f75e
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmAttachment.ang.php
@@ -0,0 +1,15 @@
+<?php
+// This file declares an Angular module which can be autoloaded
+// in CiviCRM. See also:
+// http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_angularModules
+
+return [
+ 'ext' => 'civicrm',
+ 'js' => ['ang/crmAttachment.js'],
+ 'css' => ['ang/crmAttachment.css'],
+ 'partials' => ['ang/crmAttachment'],
+ 'settings' => [
+ 'token' => \CRM_Core_Page_AJAX_Attachment::createToken(),
+ ],
+ 'requires' => ['angularFileUpload', 'crmResource'],
+];
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmAttachment.css b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmAttachment.css
new file mode 100644
index 00000000..31c6e2f5
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmAttachment.css
@@ -0,0 +1,21 @@
+.crm-attachments {
+ border: 1px solid transparent;
+}
+
+.crm-attachments.nv-file-over {
+ border: 1px solid red;
+}
+
+.crm-attachments td.filename {
+ font-size: 0.8em;
+ font-family: 'Courier New', monospace;
+ vertical-align: middle;
+}
+
+.crm-attachments td.filename-new {
+ font-style: italic;
+}
+
+.crm-attachments td .crm-form-text {
+ width: 30em;
+}
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmAttachment.js b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmAttachment.js
new file mode 100644
index 00000000..b89fe4ca
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmAttachment.js
@@ -0,0 +1,167 @@
+/// crmFile: Manage file attachments
+(function (angular, $, _) {
+
+ angular.module('crmAttachment', CRM.angRequires('crmAttachment'));
+
+ // crmAttachment manages the list of files which are attached to a given entity
+ angular.module('crmAttachment').factory('CrmAttachments', function (crmApi, crmStatus, FileUploader, $q) {
+ // @param target an Object(entity_table:'',entity_id:'') or function which generates an object
+ function CrmAttachments(target) {
+ var crmAttachments = this;
+ this._target = target;
+ this.files = [];
+ this.trash = [];
+ this.uploader = new FileUploader({
+ url: CRM.url('civicrm/ajax/attachment'),
+ onAfterAddingFile: function onAfterAddingFile(item) {
+ item.crmData = {
+ description: ''
+ };
+ },
+ onSuccessItem: function onSuccessItem(item, response, status, headers) {
+ crmAttachments.files.push(response.file.values[response.file.id]);
+ crmAttachments.uploader.removeFromQueue(item);
+ },
+ onErrorItem: function onErrorItem(item, response, status, headers) {
+ var msg = (response && response.file && response.file.error_message) ? response.file.error_message : ts('Unknown error');
+ CRM.alert(item.file.name + ' - ' + msg, ts('Attachment failed'));
+ crmAttachments.uploader.removeFromQueue(item);
+ }
+ });
+ }
+
+ angular.extend(CrmAttachments.prototype, {
+ // @return Object(entity_table:'',entity_id:'')
+ getTarget: function () {
+ return (angular.isFunction(this._target) ? this._target() : this._target);
+ },
+ // @return Promise<Attachment>
+ load: function load() {
+ var target = this.getTarget();
+ var Attachment = this;
+
+ if (target.entity_id) {
+ var params = {
+ entity_table: target.entity_table,
+ entity_id: target.entity_id
+ };
+ return crmApi('Attachment', 'get', params).then(function (apiResult) {
+ Attachment.files = _.values(apiResult.values);
+ return Attachment;
+ });
+ }
+ else {
+ var dfr = $q.defer();
+ Attachment.files = [];
+ dfr.resolve(Attachment);
+ return dfr.promise;
+ }
+ },
+ // @return Promise
+ save: function save() {
+ var crmAttachments = this;
+ var target = this.getTarget();
+ if (!target.entity_table || !target.entity_id) {
+ throw "Cannot save attachments: unknown entity_table or entity_id";
+ }
+
+ var params = _.extend({}, target);
+ params.values = crmAttachments.files;
+ return crmApi('Attachment', 'replace', params)
+ .then(function () {
+ var dfr = $q.defer();
+
+ var newItems = crmAttachments.uploader.getNotUploadedItems();
+ if (newItems.length > 0) {
+ _.each(newItems, function (item) {
+ item.formData = [_.extend({crm_attachment_token: CRM.crmAttachment.token}, target, item.crmData)];
+ });
+ crmAttachments.uploader.onCompleteAll = function onCompleteAll() {
+ delete crmAttachments.uploader.onCompleteAll;
+ dfr.resolve(crmAttachments);
+ };
+ crmAttachments.uploader.uploadAll();
+ }
+ else {
+ dfr.resolve(crmAttachments);
+ }
+
+ return dfr.promise;
+ });
+ },
+ // Compute a digest over the list of files. The signature should change if the attachment list has changed
+ // (become dirty).
+ getAutosaveSignature: function getAutosaveSignature() {
+ var sig = [];
+ // Attachments have a special lifecycle, and attachments.queue is not properly serializable, so
+ // it takes some special effort to figure out a suitable signature. Issues which can cause gratuitous saving:
+ // - Files move from this.uploader.queue to this.files after upload.
+ // - File names are munged after upload.
+ // - Deletes are performed immediately (outside the save process).
+ angular.forEach(this.files, function(item) {
+ sig.push({f: item.name.replace(/[^a-zA0-Z0-9\.]/, '_'), d: item.description});
+ });
+ angular.forEach(this.uploader.queue, function(item) {
+ sig.push({f: item.file.name.replace(/[^a-zA0-Z0-9\.]/, '_'), d: item.crmData.description});
+ });
+ angular.forEach(this.trash, function(item) {
+ sig.push({f: item.name.replace(/[^a-zA0-Z0-9\.]/, '_'), d: item.description});
+ });
+ return _.sortBy(sig, 'name');
+ },
+ // @param Object file APIv3 attachment record (e.g. id, entity_table, entity_id, description)
+ deleteFile: function deleteFile(file) {
+ var crmAttachments = this;
+
+ var idx = _.indexOf(this.files, file);
+ if (idx != -1) {
+ this.files.splice(idx, 1);
+ }
+
+ this.trash.push(file);
+
+ if (file.id) {
+ var p = crmApi('Attachment', 'delete', {id: file.id}).then(
+ function () { // success
+ },
+ function (response) { // error; restore the file
+ var msg = angular.isObject(response) ? response.error_message : '';
+ CRM.alert(msg, ts('Deletion failed'));
+ crmAttachments.files.push(file);
+
+ var trashIdx = _.indexOf(crmAttachments.trash, file);
+ if (trashIdx != -1) {
+ crmAttachments.trash.splice(trashIdx, 1);
+ }
+ }
+ );
+ return crmStatus({start: ts('Deleting...'), success: ts('Deleted')}, p);
+ }
+ }
+ });
+
+ return CrmAttachments;
+ });
+
+ // example:
+ // $scope.myAttachments = new CrmAttachments({entity_table: 'civicrm_mailing', entity_id: 123});
+ // <div crm-attachments="myAttachments"/>
+ angular.module('crmAttachment').directive('crmAttachments', function ($parse, $timeout) {
+ return {
+ scope: {
+ crmAttachments: '@'
+ },
+ template: '<div ng-if="ready" ng-include="inclUrl"></div>',
+ link: function (scope, elm, attr) {
+ var model = $parse(attr.crmAttachments);
+ scope.att = model(scope.$parent);
+ scope.ts = CRM.ts(null);
+ scope.inclUrl = '~/crmAttachment/attachments.html';
+
+ // delay rendering of child tree until after model has been populated
+ scope.ready = true;
+ }
+ };
+ });
+
+})(angular, CRM.$, CRM._);
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmAttachment/attachments.html b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmAttachment/attachments.html
new file mode 100644
index 00000000..dcd7f866
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmAttachment/attachments.html
@@ -0,0 +1,46 @@
+<div nv-file-drop nv-file-over uploader="att.uploader" class="crm-attachments">
+ <table>
+ <tbody>
+ <!-- Files from DB -->
+ <tr ng-repeat="file in att.files">
+ <td class="filename filename-existing">
+ <a ng-href="{{file.url}}" target="_blank">{{file.name}}</a>
+ </td>
+ <td>
+ <input ng-model="file.description" class="crm-form-text" placeholder="{{ts('Description')}}"/>
+ </td>
+ <td>
+ <a
+ crm-icon="fa-trash"
+ crm-confirm="{message: ts('Deleting an attachment will completely remove it from server.')}" on-yes="att.deleteFile(file)"
+ title="{{ts('Delete attachment')}}"
+ class="crm-hover-button">
+ </a>
+ </td>
+ </tr>
+ <!-- Newly selected files -->
+ <!-- This is fairly minimal. For examples with progress-bars and file-sizes, see https://github.com/nervgh/angular-file-upload/blob/master/examples/simple/index.html -->
+ <tr ng-repeat="item in att.uploader.queue" ng-class="{nvReady: item.isReady, nvUploading:item.isUploading, nvUploaded:item.isUploaded,nvSuccess:item.isSuccess,nvCancel:item.isCancel,nvError:item.isError}">
+ <td class="filename filename-new">{{item.file.name}}</td>
+ <td>
+ <input ng-model="item.crmData.description" class="crm-form-text" placeholder="{{ts('Description')}}"/>
+ <!-- item.isReady item.isUploading item.isUploaded item.isSuccess item.isCancel item.isError -->
+ </td>
+ <td>
+ <a crm-icon="fa-times" ng-click="item.remove()" class="crm-hover-button" title="{{ts('Remove unsaved attachment')}}"></a>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+
+ <!--
+ WISHLIST Improve styling of the 'Add file' / 'Browse' button
+ e.g. http://www.quirksmode.org/dom/inputfile.html
+ -->
+ <div>
+ {{ts('Add file:')}} <input type="file" nv-file-select uploader="att.uploader" multiple/><br/>
+ </div>
+ <div>
+ {{ts('Alternatively, you may add new files using drag/drop.')}}
+ </div>
+</div>
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmAutosave.ang.php b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmAutosave.ang.php
new file mode 100644
index 00000000..6f998782
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmAutosave.ang.php
@@ -0,0 +1,10 @@
+<?php
+// This file declares an Angular module which can be autoloaded
+// in CiviCRM. See also:
+// http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_angularModules
+
+return [
+ 'ext' => 'civicrm',
+ 'js' => ['ang/crmAutosave.js'],
+ 'requires' => ['crmUtil'],
+];
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmAutosave.js b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmAutosave.js
new file mode 100644
index 00000000..359ccfeb
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmAutosave.js
@@ -0,0 +1,118 @@
+/// crmAutosave
+(function(angular, $, _) {
+
+ angular.module('crmAutosave', CRM.angRequires('crmAutosave'));
+
+ // usage:
+ // var autosave = new CrmAutosaveCtrl({
+ // save: function -- A function to handle saving. Should return a promise.
+ // If it's not a promise, then we'll assume that it completes successfully.
+ // saveIf: function -- Only allow autosave when conditional returns true. Default: !form.$invalid
+ // model: object|function -- (Re)schedule saves based on observed changes to object. We perform deep
+ // inspection on the model object. This could be a performance issue you
+ // had many concurrent autosave forms or a particularly large model, but
+ // it should be fine with typical usage.
+ // interval: object -- Interval spec. Default: {poll: 250, save: 5000}
+ // form: object|function -- FormController or its getter
+ // });
+ // autosave.start();
+ // $scope.$on('$destroy', autosave.stop);
+ // Note: if the save operation itself
+ angular.module('crmAutosave').service('CrmAutosaveCtrl', function($interval, $timeout, $q) {
+ function CrmAutosaveCtrl(options) {
+ var intervals = angular.extend({poll: 250, save: 5000}, options.interval);
+ var jobs = {poll: null, save: null}; // job handles used ot cancel/reschedule timeouts/intervals
+ var lastSeenModel = null;
+ var saving = false;
+
+ // Determine if model has changed; (re)schedule the save.
+ // This is a bit expensive and doesn't need to be continuous, so we use polling instead of watches.
+ function checkChanges() {
+ if (saving) {
+ return;
+ }
+ var currentModel = _.isFunction(options.model) ? options.model() : options.model;
+ if (!angular.equals(currentModel, lastSeenModel)) {
+ lastSeenModel = angular.copy(currentModel);
+ if (jobs.save) {
+ $timeout.cancel(jobs.save);
+ }
+ jobs.save = $timeout(doAutosave, intervals.save);
+ }
+ }
+
+ function doAutosave() {
+ jobs.save = null;
+ if (saving) {
+ return;
+ }
+
+ var form = _.isFunction(options.form) ? options.form() : options.form;
+
+ if (options.saveIf) {
+ if (!options.saveIf()) {
+ return;
+ }
+ }
+ else if (form && form.$invalid) {
+ return;
+ }
+
+ saving = true;
+ lastSeenModel = angular.copy(_.isFunction(options.model) ? options.model() : options.model);
+
+ // Set to pristine before saving -- not after saving.
+ // If an eager user continues editing concurrent with the
+ // save process, then the form should become dirty again.
+ if (form) {
+ form.$setPristine();
+ }
+ var res = options.save();
+ if (res && res.then) {
+ res.then(
+ function() {
+ saving = false;
+ },
+ function() {
+ saving = false;
+ if (form) {
+ form.$setDirty();
+ }
+ }
+ );
+ }
+ else {
+ saving = false;
+ }
+ }
+
+ var self = this;
+
+ this.start = function() {
+ if (!jobs.poll) {
+ lastSeenModel = angular.copy(_.isFunction(options.model) ? options.model() : options.model);
+ jobs.poll = $interval(checkChanges, intervals.poll);
+ }
+ };
+
+ this.stop = function() {
+ if (jobs.poll) {
+ $interval.cancel(jobs.poll);
+ jobs.poll = null;
+ }
+ if (jobs.save) {
+ $timeout.cancel(jobs.save);
+ jobs.save = null;
+ }
+ };
+
+ this.suspend = function(p) {
+ self.stop();
+ return p.finally(self.start);
+ };
+ }
+
+ return CrmAutosaveCtrl;
+ });
+
+})(angular, CRM.$, CRM._);
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmCaseType.ang.php b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmCaseType.ang.php
new file mode 100644
index 00000000..c71e9c66
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmCaseType.ang.php
@@ -0,0 +1,14 @@
+<?php
+// This file declares an Angular module which can be autoloaded
+// in CiviCRM. See also:
+// http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_angularModules
+
+// ODDITY: This only loads if CiviCase is active.
+
+return [
+ 'ext' => 'civicrm',
+ 'js' => ['ang/crmCaseType.js'],
+ 'css' => ['ang/crmCaseType.css'],
+ 'partials' => ['ang/crmCaseType'],
+ 'requires' => ['ngRoute', 'ui.utils', 'crmUi', 'unsavedChanges', 'crmUtil', 'crmResource'],
+];
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmCaseType.css b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmCaseType.css
new file mode 100644
index 00000000..6352b3d2
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmCaseType.css
@@ -0,0 +1,43 @@
+.crmCaseType .grip-n-drag {
+ vertical-align: middle;
+ cursor: move;
+}
+
+.crmCaseType .fa-pencil {
+ margin: 0.2em 0.2em 0 0;
+ cursor: pointer;
+}
+
+.crmCaseType .fa-trash {
+ margin: 0.56em 0.2em 0 0;
+ cursor: pointer;
+}
+
+.crmCaseType .ui-tabs-nav li .crm-i {
+ float: left;
+}
+
+.crmCaseType .ui-tabs-nav select {
+ float: right;
+}
+
+.crmCaseType tr.addRow td {
+ background: #ddddff;
+ padding: 0.5em 1em;
+}
+
+.crmCaseType input.number {
+ width: 3.5em;
+}
+
+.crmCaseType .add-activity {
+ width: 50%;
+}
+
+.crmCaseType table td select {
+ width: 10em;
+}
+
+tr.forked {
+ font-weight: bold;
+}
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmCaseType.js b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmCaseType.js
new file mode 100644
index 00000000..a1ff51ae
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmCaseType.js
@@ -0,0 +1,648 @@
+(function(angular, $, _) {
+
+ var crmCaseType = angular.module('crmCaseType', CRM.angRequires('crmCaseType'));
+
+ // Note: This template will be passed to cloneDeep(), so don't put any funny stuff in here!
+ var newCaseTypeTemplate = {
+ title: "",
+ name: "",
+ is_active: "1",
+ weight: "1",
+ definition: {
+ activityTypes: [
+ {name: 'Open Case', max_instances: 1},
+ {name: 'Email'},
+ {name: 'Follow up'},
+ {name: 'Meeting'},
+ {name: 'Phone Call'}
+ ],
+ activitySets: [
+ {
+ name: 'standard_timeline',
+ label: 'Standard Timeline',
+ timeline: '1', // Angular won't bind checkbox correctly with numeric 1
+ activityTypes: [
+ {name: 'Open Case', status: 'Completed' }
+ ]
+ }
+ ],
+ caseRoles: [
+ { name: 'Case Coordinator', creator: '1', manager: '1'}
+ ]
+ }
+ };
+
+ crmCaseType.config(['$routeProvider',
+ function($routeProvider) {
+ $routeProvider.when('/caseType', {
+ templateUrl: '~/crmCaseType/list.html',
+ controller: 'CaseTypeListCtrl',
+ resolve: {
+ caseTypes: function($route, crmApi) {
+ return crmApi('CaseType', 'get', {options: {limit: 0}});
+ }
+ }
+ });
+ $routeProvider.when('/caseType/:id', {
+ templateUrl: '~/crmCaseType/edit.html',
+ controller: 'CaseTypeCtrl',
+ resolve: {
+ apiCalls: function($route, crmApi) {
+ var reqs = {};
+ reqs.actStatuses = ['OptionValue', 'get', {
+ option_group_id: 'activity_status',
+ sequential: 1,
+ options: {limit: 0}
+ }];
+ reqs.caseStatuses = ['OptionValue', 'get', {
+ option_group_id: 'case_status',
+ sequential: 1,
+ options: {limit: 0}
+ }];
+ reqs.actTypes = ['OptionValue', 'get', {
+ option_group_id: 'activity_type',
+ sequential: 1,
+ options: {
+ sort: 'name',
+ limit: 0
+ }
+ }];
+ reqs.defaultAssigneeTypes = ['OptionValue', 'get', {
+ option_group_id: 'activity_default_assignee',
+ sequential: 1,
+ options: {
+ limit: 0
+ }
+ }];
+ reqs.relTypes = ['RelationshipType', 'get', {
+ sequential: 1,
+ is_active: 1,
+ options: {
+ sort: 'label_a_b',
+ limit: 0
+ }
+ }];
+ if ($route.current.params.id !== 'new') {
+ reqs.caseType = ['CaseType', 'getsingle', {
+ id: $route.current.params.id
+ }];
+ }
+ return crmApi(reqs);
+ }
+ }
+ });
+ }
+ ]);
+
+ // Add a new record by name.
+ // Ex: <crmAddName crm-options="['Alpha','Beta','Gamma']" crm-var="newItem" crm-on-add="callMyCreateFunction(newItem)" />
+ crmCaseType.directive('crmAddName', function() {
+ return {
+ restrict: 'AE',
+ template: '<input class="add-activity crm-action-menu fa-plus" type="hidden" />',
+ link: function(scope, element, attrs) {
+
+ var input = $('input', element);
+
+ scope._resetSelection = function() {
+ $(input).select2('close');
+ $(input).select2('val', '');
+ scope[attrs.crmVar] = '';
+ };
+
+ $(input).crmSelect2({
+ data: function () {
+ return { results: scope[attrs.crmOptions] };
+ },
+ createSearchChoice: function(term) {
+ return {id: term, text: term + ' (' + ts('new') + ')'};
+ },
+ createSearchChoicePosition: 'bottom',
+ placeholder: attrs.placeholder
+ });
+ $(input).on('select2-selecting', function(e) {
+ scope[attrs.crmVar] = e.val;
+ scope.$evalAsync(attrs.crmOnAdd);
+ scope.$evalAsync('_resetSelection()');
+ e.preventDefault();
+ });
+ }
+ };
+ });
+
+ crmCaseType.directive('crmEditableTabTitle', function($timeout) {
+ return {
+ restrict: 'AE',
+ link: function(scope, element, attrs) {
+ element.addClass('crm-editable crm-editable-enabled');
+ var titleLabel = $(element).find('span');
+ var penIcon = $('<i class="crm-i fa-pencil crm-editable-placeholder"></i>').prependTo(element);
+ var saveButton = $('<button type="button"><i class="crm-i fa-check"></i></button>').appendTo(element);
+ var cancelButton = $('<button type="cancel"><i class="crm-i fa-times"></i></button>').appendTo(element);
+ $('button', element).wrapAll('<div class="crm-editable-form" style="display:none" />');
+ var buttons = $('.crm-editable-form', element);
+ titleLabel.on('click', startEditMode);
+ penIcon.on('click', startEditMode);
+
+ function detectEscapeKeyPress (event) {
+ var isEscape = false;
+
+ if ("key" in event) {
+ isEscape = (event.key == "Escape" || event.key == "Esc");
+ } else {
+ isEscape = (event.keyCode == 27);
+ }
+
+ return isEscape;
+ }
+
+ function detectEnterKeyPress (event) {
+ var isEnter = false;
+
+ if ("key" in event) {
+ isEnter = (event.key == "Enter");
+ } else {
+ isEnter = (event.keyCode == 13);
+ }
+
+ return isEnter;
+ }
+
+ function startEditMode () {
+ if (titleLabel.is(":focus")) {
+ return;
+ }
+
+ penIcon.hide();
+ buttons.show();
+
+ saveButton.click(function () {
+ updateTextValue();
+ stopEditMode();
+ });
+
+ cancelButton.click(function () {
+ revertTextValue();
+ stopEditMode();
+ });
+
+ $(element).addClass('crm-editable-editing');
+
+ titleLabel
+ .attr("contenteditable", "true")
+ .focus()
+ .focusout(function (event) {
+ $timeout(function () {
+ revertTextValue();
+ stopEditMode();
+ }, 500);
+ })
+ .keydown(function(event) {
+ event.stopImmediatePropagation();
+
+ if(detectEscapeKeyPress(event)) {
+ revertTextValue();
+ stopEditMode();
+ } else if(detectEnterKeyPress(event)) {
+ event.preventDefault();
+ updateTextValue();
+ stopEditMode();
+ }
+ });
+ }
+
+ function stopEditMode () {
+ titleLabel.removeAttr("contenteditable").off("focusout");
+ titleLabel.off("keydown");
+ saveButton.off("click");
+ cancelButton.off("click");
+ $(element).removeClass('crm-editable-editing');
+
+ penIcon.show();
+ buttons.hide();
+ }
+
+ function revertTextValue () {
+ titleLabel.text(scope.activitySet.label);
+ }
+
+ function updateTextValue () {
+ var updatedTitle = titleLabel.text();
+
+ scope.$evalAsync(function () {
+ scope.activitySet.label = updatedTitle;
+ });
+ }
+ }
+ };
+ });
+
+ crmCaseType.controller('CaseTypeCtrl', function($scope, crmApi, apiCalls, crmUiHelp) {
+ var defaultAssigneeDefaultValue, ts;
+
+ (function init () {
+
+ ts = $scope.ts = CRM.ts(null);
+ $scope.hs = crmUiHelp({file: 'CRM/Case/CaseType'});
+ $scope.locks = { caseTypeName: true, activitySetName: true };
+ $scope.workflows = { timeline: 'Timeline', sequence: 'Sequence' };
+ defaultAssigneeDefaultValue = _.find(apiCalls.defaultAssigneeTypes.values, { is_default: '1' }) || {};
+
+ storeApiCallsResults();
+ initCaseType();
+ initCaseTypeDefinition();
+ initSelectedStatuses();
+ })();
+
+ /// Stores the api calls results in the $scope object
+ function storeApiCallsResults() {
+ $scope.activityStatuses = apiCalls.actStatuses.values;
+ $scope.caseStatuses = _.indexBy(apiCalls.caseStatuses.values, 'name');
+ $scope.activityTypes = _.indexBy(apiCalls.actTypes.values, 'name');
+ $scope.activityTypeOptions = _.map(apiCalls.actTypes.values, formatActivityTypeOption);
+ $scope.defaultAssigneeTypes = apiCalls.defaultAssigneeTypes.values;
+ $scope.relationshipTypeOptions = getRelationshipTypeOptions(false);
+ $scope.defaultRelationshipTypeOptions = getRelationshipTypeOptions(true);
+ // stores the default assignee values indexed by their option name:
+ $scope.defaultAssigneeTypeValues = _.chain($scope.defaultAssigneeTypes)
+ .indexBy('name').mapValues('value').value();
+ }
+
+ // Returns the relationship type options. If the relationship is
+ // bidirectional (Ex: Spouse of) it adds a single option otherwise it adds
+ // two options representing the relationship type directions (Ex: Employee
+ // of, Employer of).
+ //
+ // The default relationship field needs values that are IDs with direction,
+ // while the role field needs values that are names (with implicit
+ // direction).
+ //
+ // At any rate, the labels should follow the convention in the UI of
+ // describing case roles from the perspective of the client, while the
+ // values must follow the convention in the XML of describing case roles
+ // from the perspective of the non-client.
+ function getRelationshipTypeOptions($isDefault) {
+ return _.transform(apiCalls.relTypes.values, function(result, relType) {
+ var isBidirectionalRelationship = relType.label_a_b === relType.label_b_a;
+ if ($isDefault) {
+ result.push({
+ label: relType.label_b_a,
+ value: relType.id + '_a_b'
+ });
+
+ if (!isBidirectionalRelationship) {
+ result.push({
+ label: relType.label_a_b,
+ value: relType.id + '_b_a'
+ });
+ }
+ }
+ // TODO The ids below really should use names not labels see
+ // https://lab.civicrm.org/dev/core/issues/774
+ else {
+ result.push({
+ text: relType.label_b_a,
+ id: relType.label_a_b
+ });
+
+ if (!isBidirectionalRelationship) {
+ result.push({
+ text: relType.label_a_b,
+ id: relType.label_b_a
+ });
+ }
+ }
+ }, []);
+ }
+
+ /// initializes the case type object
+ function initCaseType() {
+ var isNewCaseType = !apiCalls.caseType;
+
+ if (isNewCaseType) {
+ $scope.caseType = _.cloneDeep(newCaseTypeTemplate);
+ } else {
+ $scope.caseType = apiCalls.caseType;
+ }
+ }
+
+ /// initializes the case type definition object
+ function initCaseTypeDefinition() {
+ $scope.caseType.definition = $scope.caseType.definition || [];
+ $scope.caseType.definition.activityTypes = $scope.caseType.definition.activityTypes || [];
+ $scope.caseType.definition.activitySets = $scope.caseType.definition.activitySets || [];
+ $scope.caseType.definition.caseRoles = $scope.caseType.definition.caseRoles || [];
+ $scope.caseType.definition.statuses = $scope.caseType.definition.statuses || [];
+ $scope.caseType.definition.timelineActivityTypes = $scope.caseType.definition.timelineActivityTypes || [];
+ $scope.caseType.definition.restrictActivityAsgmtToCmsUser = $scope.caseType.definition.restrictActivityAsgmtToCmsUser || 0;
+ $scope.caseType.definition.activityAsgmtGrps = $scope.caseType.definition.activityAsgmtGrps || [];
+
+ _.each($scope.caseType.definition.activitySets, function (set) {
+ _.each(set.activityTypes, function (type, name) {
+ var isDefaultAssigneeTypeUndefined = _.isUndefined(type.default_assignee_type);
+ var typeDefinition = $scope.activityTypes[type.name];
+ type.label = (typeDefinition && typeDefinition.label) || type.name;
+
+ if (isDefaultAssigneeTypeUndefined) {
+ type.default_assignee_type = defaultAssigneeDefaultValue.value;
+ }
+ });
+ });
+
+ // go lookup and add client-perspective labels for $scope.caseType.definition.caseRoles
+ _.each($scope.caseType.definition.caseRoles, function (set) {
+ _.each($scope.relationshipTypeOptions, function (relTypes) {
+ if (relTypes.text == set.name) {
+ set.displaylabel = relTypes.id;
+ }
+ });
+ });
+ }
+
+ /// initializes the selected statuses
+ function initSelectedStatuses() {
+ $scope.selectedStatuses = {};
+
+ _.each(apiCalls.caseStatuses.values, function (status) {
+ $scope.selectedStatuses[status.name] = !$scope.caseType.definition.statuses.length || $scope.caseType.definition.statuses.indexOf(status.name) > -1;
+ });
+ }
+
+ $scope.addActivitySet = function(workflow) {
+ var activitySet = {};
+ activitySet[workflow] = '1';
+ activitySet.activityTypes = [];
+
+ var offset = 1;
+ var names = _.pluck($scope.caseType.definition.activitySets, 'name');
+ while (_.contains(names, workflow + '_' + offset)) offset++;
+ activitySet.name = workflow + '_' + offset;
+ activitySet.label = (offset == 1 ) ? $scope.workflows[workflow] : ($scope.workflows[workflow] + ' #' + offset);
+
+ $scope.caseType.definition.activitySets.push(activitySet);
+ _.defer(function() {
+ $('.crmCaseType-acttab').tabs('refresh').tabs({active: -1});
+ });
+ };
+
+ function formatActivityTypeOption(type) {
+ return {id: type.name, text: type.label, icon: type.icon};
+ }
+
+ function addActivityToSet(activitySet, activityTypeName) {
+ activitySet.activityTypes = activitySet.activityTypes || [];
+ var activity = {
+ name: activityTypeName,
+ label: $scope.activityTypes[activityTypeName].label,
+ status: 'Scheduled',
+ reference_activity: 'Open Case',
+ reference_offset: '1',
+ reference_select: 'newest',
+ default_assignee_type: $scope.defaultAssigneeTypeValues.NONE
+ };
+ activitySet.activityTypes.push(activity);
+ if(typeof activitySet.timeline !== "undefined" && activitySet.timeline == "1") {
+ $scope.caseType.definition.timelineActivityTypes.push(activity);
+ }
+ }
+
+ function resetTimelineActivityTypes() {
+ $scope.caseType.definition.timelineActivityTypes = [];
+ angular.forEach($scope.caseType.definition.activitySets, function(activitySet) {
+ angular.forEach(activitySet.activityTypes, function(activityType) {
+ $scope.caseType.definition.timelineActivityTypes.push(activityType);
+ });
+ });
+ }
+
+ function createActivity(name, callback) {
+ CRM.loadForm(CRM.url('civicrm/admin/options/activity_type', {action: 'add', reset: 1, label: name, component_id: 7}))
+ .on('crmFormSuccess', function(e, data) {
+ $scope.activityTypes[data.optionValue.name] = data.optionValue;
+ $scope.activityTypeOptions.push(formatActivityTypeOption(data.optionValue));
+ callback(data.optionValue);
+ $scope.$digest();
+ });
+ }
+
+ // Add a new activity entry to an activity-set
+ $scope.addActivity = function(activitySet, activityType) {
+ if ($scope.activityTypes[activityType]) {
+ addActivityToSet(activitySet, activityType);
+ } else {
+ createActivity(activityType, function(newActivity) {
+ addActivityToSet(activitySet, newActivity.name);
+ });
+ }
+ };
+
+ /// Add a new top-level activity-type entry
+ $scope.addActivityType = function(activityType) {
+ var names = _.pluck($scope.caseType.definition.activityTypes, 'name');
+ if (!_.contains(names, activityType)) {
+ // Add an activity type that exists
+ if ($scope.activityTypes[activityType]) {
+ $scope.caseType.definition.activityTypes.push({name: activityType});
+ } else {
+ createActivity(activityType, function(newActivity) {
+ $scope.caseType.definition.activityTypes.push({name: newActivity.name});
+ });
+ }
+ }
+ };
+
+ /// Clears the activity's default assignee values for relationship and contact
+ $scope.clearActivityDefaultAssigneeValues = function(activity) {
+ activity.default_assignee_relationship = null;
+ activity.default_assignee_contact = null;
+ };
+
+ // TODO roleName passed to addRole is a misnomer, its passed as the
+ // label HOWEVER it should be saved to xml as the name see
+ // https://lab.civicrm.org/dev/core/issues/774
+
+ /// Add a new role
+ $scope.addRole = function(roles, roleName) {
+ var names = _.pluck($scope.caseType.definition.caseRoles, 'name');
+ if (!_.contains(names, roleName)) {
+ var matchingRoles = _.filter($scope.relationshipTypeOptions, {id: roleName});
+ if (matchingRoles.length) {
+ var matchingRole = matchingRoles.shift();
+ roles.push({name: roleName, displaylabel: matchingRole.text});
+ } else {
+ CRM.loadForm(CRM.url('civicrm/admin/reltype', {action: 'add', reset: 1, label_a_b: roleName}))
+ .on('crmFormSuccess', function(e, data) {
+ var newType = _.values(data.relationshipType)[0];
+ roles.push({name: newType.label_b_a, displaylabel: newType.label_a_b});
+ // Assume that the case role should be A-B but add both directions as options.
+ $scope.relationshipTypeOptions.push({id: newType.label_a_b, text: newType.label_a_b});
+ if (newType.label_a_b != newType.label_b_a) {
+ $scope.relationshipTypeOptions.push({id: newType.label_b_a, text: newType.label_b_a});
+ }
+ $scope.$digest();
+ });
+ }
+ }
+ };
+
+ $scope.onManagerChange = function(managerRole) {
+ angular.forEach($scope.caseType.definition.caseRoles, function(caseRole) {
+ if (caseRole != managerRole) {
+ caseRole.manager = '0';
+ }
+ });
+ };
+
+ $scope.removeItem = function(array, item) {
+ var idx = _.indexOf(array, item);
+ if (idx != -1) {
+ array.splice(idx, 1);
+ resetTimelineActivityTypes();
+ }
+ };
+
+ $scope.isForkable = function() {
+ return !$scope.caseType.id || $scope.caseType.is_forkable;
+ };
+
+ $scope.newStatus = function() {
+ CRM.loadForm(CRM.url('civicrm/admin/options/case_status', {action: 'add', reset: 1}))
+ .on('crmFormSuccess', function(e, data) {
+ $scope.caseStatuses[data.optionValue.name] = data.optionValue;
+ $scope.selectedStatuses[data.optionValue.name] = true;
+ $scope.$digest();
+ });
+ };
+
+ $scope.isNewActivitySetAllowed = function(workflow) {
+ switch (workflow) {
+ case 'timeline':
+ return true;
+ case 'sequence':
+ return 0 === _.where($scope.caseType.definition.activitySets, {sequence: '1'}).length;
+ default:
+ CRM.console('warn', 'Denied access to unrecognized workflow: (' + workflow + ')');
+ return false;
+ }
+ };
+
+ $scope.isActivityRemovable = function(activitySet, activity) {
+ return true;
+ };
+
+ $scope.isValidName = function(name) {
+ return !name || name.match(/^[a-zA-Z0-9_]+$/);
+ };
+
+ $scope.getWorkflowName = function(activitySet) {
+ var result = 'Unknown';
+ _.each($scope.workflows, function(value, key) {
+ if (activitySet[key]) result = value;
+ });
+ return result;
+ };
+
+ /**
+ * Determine which HTML partial to use for a particular
+ *
+ * @return string URL of the HTML partial
+ */
+ $scope.activityTableTemplate = function(activitySet) {
+ if (activitySet.timeline) {
+ return '~/crmCaseType/timelineTable.html';
+ } else if (activitySet.sequence) {
+ return '~/crmCaseType/sequenceTable.html';
+ } else {
+ return '';
+ }
+ };
+
+ $scope.dump = function() {
+ console.log($scope.caseType);
+ };
+
+ $scope.save = function() {
+ // Add selected statuses
+ var selectedStatuses = [];
+ _.each($scope.selectedStatuses, function(v, k) {
+ if (v) selectedStatuses.push(k);
+ });
+ // Ignore if ALL or NONE selected
+ $scope.caseType.definition.statuses = selectedStatuses.length == _.size($scope.selectedStatuses) ? [] : selectedStatuses;
+
+ if ($scope.caseType.definition.activityAsgmtGrps) {
+ $scope.caseType.definition.activityAsgmtGrps = $scope.caseType.definition.activityAsgmtGrps.toString().split(",");
+ }
+
+ function dropDisplaylabel (v) {
+ delete v.displaylabel;
+ }
+
+ // strip out labels from $scope.caseType.definition.caseRoles
+ _.map($scope.caseType.definition.caseRoles, dropDisplaylabel);
+
+ var result = crmApi('CaseType', 'create', $scope.caseType, true);
+ result.then(function(data) {
+ if (data.is_error === 0 || data.is_error == '0') {
+ $scope.caseType.id = data.id;
+ window.location.href = '#/caseType';
+ }
+ });
+ };
+
+ $scope.$watchCollection('caseType.definition.activitySets', function() {
+ _.defer(function() {
+ $('.crmCaseType-acttab').tabs('refresh');
+ });
+ });
+
+ var updateCaseTypeName = function () {
+ if (!$scope.caseType.id && $scope.locks.caseTypeName) {
+ // Should we do some filtering? Lowercase? Strip whitespace?
+ var t = $scope.caseType.title ? $scope.caseType.title : '';
+ $scope.caseType.name = t.replace(/ /g, '_').replace(/[^a-zA-Z0-9_]/g, '').toLowerCase();
+ }
+ };
+ $scope.$watch('locks.caseTypeName', updateCaseTypeName);
+ $scope.$watch('caseType.title', updateCaseTypeName);
+
+ if (!$scope.isForkable()) {
+ CRM.alert(ts('The CiviCase XML file for this case-type prohibits editing the definition.'));
+ }
+
+ });
+
+ crmCaseType.controller('CaseTypeListCtrl', function($scope, crmApi, caseTypes) {
+ var ts = $scope.ts = CRM.ts(null);
+
+ $scope.caseTypes = caseTypes.values;
+ $scope.toggleCaseType = function (caseType) {
+ caseType.is_active = (caseType.is_active == '1') ? '0' : '1';
+ crmApi('CaseType', 'create', caseType, true)
+ .catch(function (data) {
+ caseType.is_active = (caseType.is_active == '1') ? '0' : '1'; // revert
+ $scope.$digest();
+ });
+ };
+ $scope.deleteCaseType = function (caseType) {
+ crmApi('CaseType', 'delete', {id: caseType.id}, {
+ error: function (data) {
+ CRM.alert(data.error_message, ts('Error'), 'error');
+ }
+ })
+ .then(function (data) {
+ delete caseTypes.values[caseType.id];
+ });
+ };
+ $scope.revertCaseType = function (caseType) {
+ caseType.definition = 'null';
+ caseType.is_forked = '0';
+ crmApi('CaseType', 'create', caseType, true)
+ .catch(function (data) {
+ caseType.is_forked = '1'; // restore
+ $scope.$digest();
+ });
+ };
+ });
+
+})(angular, CRM.$, CRM._);
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmCaseType/activityTypesTable.html b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmCaseType/activityTypesTable.html
new file mode 100644
index 00000000..a324f895
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmCaseType/activityTypesTable.html
@@ -0,0 +1,46 @@
+<!--
+Controller: CaseTypeCtrl
+Required vars: caseType
+-->
+<table class="row-highlight">
+ <thead>
+ <tr>
+ <th></th>
+ <th>{{ts('Activity Type')}}</th>
+ <th>{{ts('Max Instances')}}</th>
+ <th></th>
+ </tr>
+ </thead>
+
+ <tbody ui-sortable ng-model="caseType.definition.activityTypes">
+ <tr ng-repeat="activityType in caseType.definition.activityTypes">
+ <td>
+ <i class="crm-i fa-arrows grip-n-drag"></i>
+ </td>
+ <td>
+ <i class="crm-i {{ activityTypes[activityType.name].icon }}"></i>
+ {{ activityType.name }}
+ </td>
+ <td>
+ <input class="crm-form-text number" type="text" ng-pattern="/^[1-9][0-9]*$/" ng-model="activityType.max_instances">
+ </td>
+ <td>
+ <a crm-icon="fa-trash" class="crm-hover-button" ng-click="removeItem(caseType.definition.activityTypes, activityType)" title="{{ts('Remove')}}"></a>
+ </td>
+ </tr>
+ </tbody>
+
+ <tfoot>
+ <tr class="addRow">
+ <td></td>
+ <td colspan="3">
+ <span crm-add-name
+ crm-options="activityTypeOptions"
+ crm-var="newActivity"
+ crm-on-add="addActivityType(newActivity)"
+ placeholder="{{ts('Add activity type')}}"
+ ></span>
+ </td>
+ </tr>
+ </tfoot>
+</table>
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmCaseType/caseTypeDetails.html b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmCaseType/caseTypeDetails.html
new file mode 100644
index 00000000..d11cc913
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmCaseType/caseTypeDetails.html
@@ -0,0 +1,67 @@
+<!--
+Controller: CaseTypeCtrl
+Required vars: caseType
+
+The original form used table layout; don't know if we have an alternative, CSS-based layout
+-->
+<div class="crm-block" ng-form="caseTypeDetailForm" crm-ui-id-scope>
+ <div class="crm-group">
+ <div crm-ui-field="{name: 'caseTypeDetailForm.title', title: ts('Title')}">
+ <input
+ crm-ui-id="caseTypeDetailForm.title"
+ type="text"
+ name="title"
+ ng-model="caseType.title"
+ class="big crm-form-text"
+ required
+ />
+ </div>
+ <div crm-ui-field="{name: 'caseTypeDetailForm.caseTypeName', title: ts('Name')}">
+ <input
+ crm-ui-id="caseTypeDetailForm.caseTypeName"
+ type="text"
+ name="caseTypeName"
+ ng-model="caseType.name"
+ ng-disabled="locks.caseTypeName"
+ required
+ class="big crm-form-text"/>
+
+ <a crm-ui-lock binding="locks.caseTypeName"></a>
+
+ <div ng-show="!isValidName(caseType.name)">
+ <em>{{ts('WARNING: The case type name includes deprecated characters.')}}</em>
+ </div>
+ <div ng-show="caseType.id && !locks.caseTypeName">
+ <em>{{ts('WARNING: If any external files or programs reference the old "Name", then they must be updated manually.')}}</em>
+ </div>
+ </div>
+ <div crm-ui-field="{name: 'caseTypeDetailForm.description', title: ts('Description')}">
+ <textarea crm-ui-id="caseTypeDetailForm.description" name="description" ng-model="caseType.description" class="big crm-form-textarea"></textarea>
+ </div>
+ <div crm-ui-field="{title: ts('Enabled?')}">
+ <input name="is_active" type="checkbox" ng-model="caseType.is_active" ng-true-value="'1'" ng-false-value="'0'"/>
+ </div>
+ <fieldset class="crm-collapsible">
+ <legend class="collapsible-title">{{ ts('Activity assignment settings') }}</legend>
+ <div>
+ <div crm-ui-field="{name: 'caseTypeDetailForm.activityAsgmtGrps', title: ts('Restrict to Groups'), help: hs('activityAsgmtGrps')}">
+ <input
+ name="activityAsgmtGrps"
+ crm-ui-id="caseTypeDetailForm.activityAsgmtGrps"
+ crm-entityref="{entity: 'Group', api: {params: {is_hidden: 0, is_active: 1}}, select: {allowClear: true, multiple: true, placeholder: ts('Select Group')}}"
+ ng-model="caseType.definition.activityAsgmtGrps"
+ />
+ </div>
+ <div crm-ui-field="{title: ts('Restrict to Website Users'), help: hs('restrictActivityAsgmtToCmsUser')}">
+ <input
+ name="restrictActivityAsgmtToCmsUser"
+ type="checkbox"
+ ng-model="caseType.definition.restrictActivityAsgmtToCmsUser"
+ ng-true-value="'1'"
+ ng-false-value="'0'"
+ />
+ </div>
+ </div>
+ </fieldset>
+ </div>
+</div>
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmCaseType/edit.html b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmCaseType/edit.html
new file mode 100644
index 00000000..55c7faf4
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmCaseType/edit.html
@@ -0,0 +1,63 @@
+<!--
+Controller: CaseTypeCtrl
+Required vars: caseType
+-->
+<h1 crm-page-title>{{caseType.title || ts('New Case Type')}}</h1>
+
+<div class="help">
+ {{ts('Use this screen to define or update the Case Roles, Activity Types, and Timelines for a case type.')}} <a href="https://docs.civicrm.org/user/en/stable/case-management/set-up/" target="_blank">{{ts('Learn more...')}}</a>
+</div>
+
+<form name="editCaseTypeForm" unsaved-warning-form>
+<div class="crm-block crm-form-block crmCaseType">
+
+ <div ng-include="'~/crmCaseType/caseTypeDetails.html'"></div>
+
+ <div ng-show="isForkable()" class="crmCaseType-acttab" ui-jq="tabs" ui-options="{show: true, hide: true}">
+ <ul>
+ <li><a href="#acttab-roles">{{ts('Case Roles')}}</a></li>
+ <li><a href="#acttab-statuses">{{ts('Case Statuses')}}</a></li>
+ <li><a href="#acttab-actType">{{ts('Activity Types')}}</a></li>
+ <li ng-repeat="activitySet in caseType.definition.activitySets">
+ <a href="#acttab-{{$index}}" class="crmCaseType-editable">
+ <div crm-editable-tab-title title="{{ts('Click to edit')}}">
+ <span>{{ activitySet.label }}</span>
+ </div>
+ </a>
+ <span class="crm-i fa-trash" title="{{ts('Remove')}}"
+ ng-hide="activitySet.name == 'standard_timeline'"
+ ng-click="removeItem(caseType.definition.activitySets, activitySet)"></span>
+ <!-- Weird spacing:
+ <a class="crm-hover-button" ng-click="removeItem(caseType.definition.activitySets, activitySet)">
+ <span class="crm-i fa-trash" title="Remove">Remove</span>
+ </a>
+ -->
+ </li>
+ <select class="crm-form-select" ng-model="newActivitySetWorkflow" ng-change="addActivitySet(newActivitySetWorkflow); newActivitySetWorkflow='';">
+ <option value="">{{ts('Add...')}}</option>
+ <option value="timeline" ng-show="isNewActivitySetAllowed('timeline')">{{ts('Timeline')}}</option>
+ <option value="sequence" ng-show="isNewActivitySetAllowed('sequence')">{{ts('Sequence')}}</option>
+ </select>
+ </ul>
+
+ <div id="acttab-roles" ng-include="'~/crmCaseType/rolesTable.html'"></div>
+
+ <div id="acttab-actType" ng-include="'~/crmCaseType/activityTypesTable.html'"></div>
+
+ <div id="acttab-statuses" ng-include="'~/crmCaseType/statusTable.html'"></div>
+
+ <div ng-repeat="activitySet in caseType.definition.activitySets" id="acttab-{{$index}}">
+ <div ng-include="activityTableTemplate(activitySet)"></div>
+ </div>
+ </div>
+
+ <div class="crm-submit-buttons">
+ <button crm-icon="fa-check" ng-click="editCaseTypeForm.$setPristine(); save()" ng-disabled="editCaseTypeForm.$invalid">
+ {{ts('Save')}}
+ </button>
+ <button crm-icon="fa-times" ng-click="editCaseTypeForm.$setPristine(); goto('caseType')">
+ {{ts('Cancel')}}
+ </button>
+ </div>
+</div>
+</form>
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmCaseType/list.html b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmCaseType/list.html
new file mode 100644
index 00000000..a9caecc3
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmCaseType/list.html
@@ -0,0 +1,78 @@
+<!--
+Controller: CaseTypeListsCtrl
+Required vars: caseTypes
+-->
+<h1 crm-page-title>{{ts('Case Types')}}</h1>
+
+<div class="help">
+ {{ts('A Case Type describes a group of related tasks, interactions, or processes.')}}
+</div>
+
+<div class="crm-content-block crm-block">
+
+ <table class="display">
+ <thead>
+ <tr>
+ <th>{{ts('Title')}}</th>
+ <th>{{ts('Name')}}</th>
+ <th>{{ts('Description')}}</th>
+ <th>{{ts('Enabled?')}}</th>
+ <th></th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr ng-repeat="caseType in caseTypes"
+ class="crm-entity"
+ ng-class-even="'even-row even'"
+ ng-class-odd="'odd-row odd'"
+ ng-class="{disabled: 0==caseType.is_active, forked: 1==caseType.is_forked}">
+ <td>{{caseType.title}}</td>
+ <td>{{caseType.name}}</td>
+ <td>{{caseType.description}}</td>
+ <td>{{caseType.is_active == 1 ? ts('Yes') : ts('No')}}</td>
+ <!-- FIXME: Can't figure out how styling in other tables gets the nowrap effect... in absence of a consistent fix, KISS -->
+ <td style="white-space: nowrap">
+ <span>
+ <a class="action-item crm-hover-button" ng-href="#/caseType/{{caseType.id}}">{{ts('Edit')}}</a>
+
+ <span class="btn-slide crm-hover-button" ng-show="!caseType.is_reserved || (!caseType.is_active || caseType.is_forked)">
+ {{ts('more')}}
+ <ul class="panel" style="display: none;">
+ <li ng-hide="caseType.is_active">
+ <a class="action-item crm-hover-button" ng-click="toggleCaseType(caseType)">
+ {{ts('Enable')}}
+ </a>
+ </li>
+ <li ng-show="caseType.is_active && !caseType.is_reserved">
+ <a class="action-item crm-hover-button"
+ crm-confirm="{type: 'disable', obj: caseType}"
+ on-yes="toggleCaseType(caseType)">
+ {{ts('Disable')}}
+ </a>
+ </li>
+ <li ng-show="caseType.is_forked">
+ <a class="action-item crm-hover-button"
+ crm-confirm="{type: 'revert', obj: caseType}"
+ on-yes="revertCaseType(caseType)">
+ {{ts('Revert')}}
+ </a>
+ </li>
+ <li ng-show="!caseType.is_reserved">
+ <a class="action-item crm-hover-button"
+ crm-confirm="{type: 'delete', obj: caseType}"
+ on-yes="deleteCaseType(caseType)">
+ {{ts('Delete')}}
+ </a>
+ </li>
+ </ul>
+ </span>
+ </span>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+
+ <div class="crm-submit-buttons">
+ <a ng-href="#/caseType/new" class="button"><span><i class="crm-i fa-plus-circle"></i> {{ts('New Case Type')}}</span></a>
+ </div>
+</div>
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmCaseType/rolesTable.html b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmCaseType/rolesTable.html
new file mode 100644
index 00000000..e7edee07
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmCaseType/rolesTable.html
@@ -0,0 +1,38 @@
+<!--
+Controller: CaseTypeCtrl
+Required vars: caseType
+-->
+<table>
+ <thead>
+ <tr>
+ <th>{{ts('Name')}}</th>
+ <th>{{ts('Assign to Creator')}}</th>
+ <th>{{ts('Is Manager')}}</th>
+ <th></th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr ng-repeat="relType in caseType.definition.caseRoles | orderBy:'name'" ng-class-even="'crm-entity even-row even'" ng-class-odd="'crm-entity odd-row odd'">
+ <!-- display label (client-perspective) -->
+ <td>{{relType.displaylabel}}</td>
+ <td><input type="checkbox" ng-model="relType.creator" ng-true-value="'1'" ng-false-value="'0'"></td>
+ <td><input type="radio" ng-model="relType.manager" value="1" ng-change="onManagerChange(relType)"></td>
+ <td>
+ <a crm-icon="fa-trash" class="crm-hover-button" ng-click="removeItem(caseType.definition.caseRoles,relType)" title="{{ts('Remove')}}"></a>
+ </td>
+ </tr>
+ </tbody>
+
+ <tfoot>
+ <tr class="addRow">
+ <td colspan="4">
+ <span crm-add-name
+ crm-options="relationshipTypeOptions"
+ crm-var="newRole"
+ crm-on-add="addRole(caseType.definition.caseRoles, newRole)"
+ placeholder="{{ts('Add role')}}"
+ ></span>
+ </td>
+ </tr>
+ </tfoot>
+</table>
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmCaseType/sequenceTable.html b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmCaseType/sequenceTable.html
new file mode 100644
index 00000000..e07a11bd
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmCaseType/sequenceTable.html
@@ -0,0 +1,41 @@
+<!--
+Controller: CaseTypeCtrl
+Required vars: activitySet
+-->
+<table>
+ <thead>
+ <tr>
+ <th></th>
+ <th>{{ts('Activity')}}</th>
+ <th></th>
+ </tr>
+ </thead>
+
+ <tbody ui-sortable ng-model="activitySet.activityTypes">
+ <tr ng-repeat="activity in activitySet.activityTypes">
+ <td>
+ <i class="crm-i fa-arrows grip-n-drag"></i>
+ </td>
+ <td>
+ <i class="crm-i {{ activityTypes[activity.name].icon }}"></i>
+ {{ activity.name }}
+ </td>
+ <td>
+ <a crm-icon="fa-trash" class="crm-hover-button" ng-click="removeItem(activitySet.activityTypes, activity)" title="{{ts('Remove')}}"></a>
+ </td>
+ </tr>
+ </tbody>
+
+ <tfoot>
+ <tr class="addRow">
+ <td colspan="3">
+ <span crm-add-name
+ crm-options="activityTypeOptions"
+ crm-var="newActivity"
+ crm-on-add="addActivity(activitySet, newActivity)"
+ placeholder="{{ts('Add activity')}}"
+ ></span>
+ </td>
+ </tr>
+ </tfoot>
+</table>
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmCaseType/statusTable.html b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmCaseType/statusTable.html
new file mode 100644
index 00000000..890989a3
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmCaseType/statusTable.html
@@ -0,0 +1,35 @@
+<!--
+Controller: CaseTypeCtrl
+Required vars: selectedStatuses
+-->
+<table>
+ <thead>
+ <tr>
+ <th></th>
+ <th>{{ts('Name')}}</th>
+ <th>{{ts('Class')}}</th>
+ </tr>
+ </thead>
+
+ <tbody ng-model="selectedStatuses">
+ <tr ng-repeat="(status,sel) in selectedStatuses">
+ <td>
+ <input class="crm-form-checkbox" type="checkbox" ng-model="selectedStatuses[status]"/>
+ </td>
+ <td>
+ {{ caseStatuses[status].label }}
+ </td>
+ <td>
+ {{ caseStatuses[status].grouping }}
+ </td>
+ </tr>
+ </tbody>
+
+ <tfoot>
+ <tr>
+ <td></td>
+ <td><a class="crm-hover-button action-item" ng-click="newStatus()" href><i class="crm-i fa-plus"></i> {{ ts('New Status') }}</a></td>
+ <td></td>
+ </tr>
+ </tfoot>
+</table>
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmCaseType/timelineTable.html b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmCaseType/timelineTable.html
new file mode 100644
index 00000000..4d044f1b
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmCaseType/timelineTable.html
@@ -0,0 +1,117 @@
+<!--
+Controller: CaseTypeCtrl
+Required vars: activitySet
+-->
+<table>
+ <thead>
+ <tr>
+ <th></th>
+ <th>{{ts('Activity')}}</th>
+ <th>{{ts('Status')}}</th>
+ <th>{{ts('Reference')}}</th>
+ <th>{{ts('Offset')}}</th>
+ <th>{{ts('Select')}}</th>
+ <th>{{ts('Default assignee')}}</th>
+ <th></th>
+ </tr>
+ </thead>
+
+ <tbody ui-sortable ng-model="activitySet.activityTypes">
+ <tr ng-repeat="activity in activitySet.activityTypes">
+ <td>
+ <i class="crm-i fa-arrows grip-n-drag"></i>
+ </td>
+ <td>
+ <i class="crm-i {{activityTypes[activity.name].icon}}"></i>
+ {{activity.label}}
+ </td>
+ <td>
+ <select
+ ui-jq="select2"
+ ui-options="{dropdownAutoWidth: true}"
+ ng-model="activity.status"
+ ng-options="actStatus.name as actStatus.label for actStatus in activityStatuses|orderBy:'label'"
+ >
+ <option value=""></option>
+ </select>
+ </td>
+ <td>
+ <select
+ ui-jq="select2"
+ ui-options="{dropdownAutoWidth: true}"
+ ng-model="activity.reference_activity"
+ ng-options="activityType.name as activityType.label for activityType in caseType.definition.timelineActivityTypes"
+ >
+ <option value="">-- Case Start --</option>
+ </select>
+ </td>
+ <td>
+ <input
+ class="number crm-form-text"
+ type="text"
+ ng-pattern="/^-?[0-9]*$/"
+ ng-model="activity.reference_offset"
+ >
+ </td>
+ <td>
+ <select
+ ui-jq="select2"
+ ui-options="{dropdownAutoWidth: true}"
+ ng-model="activity.reference_select"
+ ng-options="key as value for (key,value) in {newest: ts('Newest'), oldest: ts('Oldest')}"
+ >
+ </select>
+ </td>
+ <td>
+ <select
+ ui-jq="select2"
+ ui-options="{dropdownAutoWidth: true}"
+ ng-model="activity.default_assignee_type"
+ ng-options="option.value as option.label for option in defaultAssigneeTypes"
+ ng-change="clearActivityDefaultAssigneeValues(activity)"
+ ></select>
+
+ <p ng-if="activity.default_assignee_type === defaultAssigneeTypeValues.BY_RELATIONSHIP">
+ <select
+ ui-jq="select2"
+ ui-options="{dropdownAutoWidth: true}"
+ ng-model="activity.default_assignee_relationship"
+ ng-options="option.value as option.label for option in defaultRelationshipTypeOptions"
+ required
+ ></select>
+ </p>
+
+ <p ng-if="activity.default_assignee_type === defaultAssigneeTypeValues.SPECIFIC_CONTACT">
+ <input
+ type="text"
+ ng-model="activity.default_assignee_contact"
+ placeholder="- Select contact -"
+ crm-entityref="{ entity: 'Contact' }"
+ data-create-links="true"
+ required />
+ </p>
+ </td>
+ <td>
+ <a class="crm-hover-button"
+ crm-icon="fa-trash"
+ ng-show="isActivityRemovable(activitySet, activity)"
+ ng-click="removeItem(activitySet.activityTypes, activity)"
+ title="{{ts('Remove')}}">
+ </a>
+ </td>
+ </tr>
+ </tbody>
+
+ <tfoot>
+ <tr class="addRow">
+ <td colspan="8">
+ <span crm-add-name=""
+ crm-options="activityTypeOptions"
+ crm-var="newActivity"
+ crm-on-add="addActivity(activitySet, newActivity)"
+ placeholder="{{ts('Add activity')}}"
+ ></span>
+ </td>
+ </tr>
+ </tfoot>
+</table>
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmCxn.ang.php b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmCxn.ang.php
new file mode 100644
index 00000000..ff267991
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmCxn.ang.php
@@ -0,0 +1,12 @@
+<?php
+// This file declares an Angular module which can be autoloaded
+// in CiviCRM. See also:
+// http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_angularModules
+
+return [
+ 'ext' => 'civicrm',
+ 'js' => ['ang/crmCxn.js', 'ang/crmCxn/*.js'],
+ 'css' => ['ang/crmCxn.css'],
+ 'partials' => ['ang/crmCxn'],
+ 'requires' => ['crmUtil', 'ngRoute', 'ngSanitize', 'ui.utils', 'crmUi', 'dialogService', 'crmResource'],
+];
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmCxn.css b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmCxn.css
new file mode 100644
index 00000000..11aeb14e
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmCxn.css
@@ -0,0 +1,3 @@
+.crmCxn-footer {
+ text-align: center;
+}
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmCxn.js b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmCxn.js
new file mode 100644
index 00000000..244d038f
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmCxn.js
@@ -0,0 +1,25 @@
+(function (angular, $, _) {
+
+ angular.module('crmCxn', CRM.angRequires('crmCxn'));
+
+ angular.module('crmCxn').config([
+ '$routeProvider',
+ function ($routeProvider) {
+ $routeProvider.when('/cxn', {
+ templateUrl: '~/crmCxn/ManageCtrl.html',
+ controller: 'CrmCxnManageCtrl',
+ resolve: {
+ apiCalls: function(crmApi){
+ var reqs = {};
+ reqs.cxns = ['Cxn', 'get', {sequential: 1}];
+ reqs.appMetas = ['CxnApp', 'get', {sequential: 1, return: ['id', 'title', 'desc', 'appId', 'appUrl', 'links', 'perm']}];
+ reqs.cfg = ['Cxn', 'getcfg', {}];
+ reqs.sysCheck = ['System', 'check', {}]; // FIXME: filter on checkCxnOverrides
+ return crmApi(reqs);
+ }
+ }
+ });
+ }
+ ]);
+
+})(angular, CRM.$, CRM._);
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmCxn/AboutCtrl.html b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmCxn/AboutCtrl.html
new file mode 100644
index 00000000..8757449d
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmCxn/AboutCtrl.html
@@ -0,0 +1,14 @@
+<div ng-controller="CrmCxnConfirmAboutCtrl">
+ <div crm-ui-accordion="{title: ts('About'), collapsed: false}">
+ <div ng-bind-html="appMeta.desc"></div>
+ </div>
+ <div crm-ui-accordion="{title: ts('Permissions: Summary'), collapsed: true}">
+ <div ng-bind-html="appMeta.perm.desc"></div>
+ </div>
+ <div crm-ui-accordion="{title: ts('Permissions: Details'), collapsed: true}">
+ <div crm-cxn-perm-table="{perm: appMeta.perm}"></div>
+ </div>
+ <div crm-ui-accordion="{title: ts('Advanced'), collapsed: true}">
+ <div crm-cxn-adv-table="{appMeta: appMeta}"></div>
+ </div>
+</div>
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmCxn/AboutCtrl.js b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmCxn/AboutCtrl.js
new file mode 100644
index 00000000..f9b9491e
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmCxn/AboutCtrl.js
@@ -0,0 +1,5 @@
+(function(angular, $, _) {
+ angular.module('crmCxn').controller('CrmCxnConfirmAboutCtrl', function($scope) {
+ $scope.ts = CRM.ts(null);
+ });
+})(angular, CRM.$, CRM._);
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmCxn/AdvTable.html b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmCxn/AdvTable.html
new file mode 100644
index 00000000..7080fdd7
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmCxn/AdvTable.html
@@ -0,0 +1,18 @@
+<table>
+ <thead>
+ <tr>
+ <th>{{ts('Property')}}</th>
+ <th>{{ts('Value')}}</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr class="odd-row odd">
+ <td>App ID</td>
+ <td>{{appMeta.appId}}</td>
+ </tr>
+ <tr class="even-row even">
+ <td>App URL</td>
+ <td><code>{{appMeta.appUrl}}</code></td>
+ </tr>
+ </tbody>
+</table>
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmCxn/AdvTable.js b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmCxn/AdvTable.js
new file mode 100644
index 00000000..3ea2bc39
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmCxn/AdvTable.js
@@ -0,0 +1,20 @@
+(function(angular, $, _) {
+
+ // This directive formats the data in appMeta as a nice table.
+ // example: <div crm-cxn-perm-table="{appMeta: cxn.app_meta}"></div>
+ angular.module('crmCxn').directive('crmCxnAdvTable', function crmCxnAdvTable() {
+ return {
+ restrict: 'EA',
+ scope: {
+ crmCxnAdvTable: '='
+ },
+ templateUrl: '~/crmCxn/AdvTable.html',
+ link: function(scope, element, attrs) {
+ scope.ts = CRM.ts(null);
+ scope.$watch('crmCxnAdvTable', function(crmCxnAdvTable){
+ scope.appMeta = crmCxnAdvTable.appMeta;
+ });
+ }
+ };
+ });
+})(angular, CRM.$, CRM._);
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmCxn/CheckAddress.js b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmCxn/CheckAddress.js
new file mode 100644
index 00000000..2313ddd0
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmCxn/CheckAddress.js
@@ -0,0 +1,33 @@
+(function (angular, $, _) {
+
+ angular.module('crmCxn').factory('crmCxnCheckAddr', function($q, $timeout) {
+ var TIMEOUT = 6000, CHECK_ADDR = 'https://mycivi.org/check-addr';
+ return function(url) {
+ var dfr = $q.defer(), result = null;
+
+ function onErr() {
+ if (result !== null) return;
+ result = {url: url, valid: false};
+ dfr.resolve(result);
+ }
+
+ $.ajax({
+ url: CHECK_ADDR,
+ data: {url: url},
+ jsonp: "callback",
+ dataType: "jsonp"
+ }).fail(onErr)
+ .done(function(response) {
+ if (result !== null) return;
+ result = {url: url, valid: response.result};
+ dfr.resolve(result);
+ }
+ );
+ // JSONP may not provide errors directly.
+ $timeout(onErr, TIMEOUT);
+
+ return dfr.promise;
+ };
+ });
+
+})(angular, CRM.$, CRM._);
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmCxn/ConfirmConnectCtrl.html b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmCxn/ConfirmConnectCtrl.html
new file mode 100644
index 00000000..eadee337
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmCxn/ConfirmConnectCtrl.html
@@ -0,0 +1,15 @@
+<div ng-controller="CrmCxnConfirmConnectCtrl">
+ <p>{{ts('The application, \"%1\", requests permission to access your system.', {1: appMeta.title})}}</p>
+ <div crm-ui-accordion="{title: ts('About'), collapsed: true}">
+ <div ng-bind-html="appMeta.desc"></div>
+ </div>
+ <div crm-ui-accordion="{title: ts('Permissions: Summary'), collapsed: true}">
+ <div ng-bind-html="appMeta.perm.desc"></div>
+ </div>
+ <div crm-ui-accordion="{title: ts('Permissions: Details'), collapsed: true}">
+ <div crm-cxn-perm-table="{perm: appMeta.perm}"></div>
+ </div>
+ <div crm-ui-accordion="{title: ts('Advanced'), collapsed: true}">
+ <div crm-cxn-adv-table="{appMeta: appMeta}"></div>
+ </div>
+</div>
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmCxn/ConfirmConnectCtrl.js b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmCxn/ConfirmConnectCtrl.js
new file mode 100644
index 00000000..c303d5e9
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmCxn/ConfirmConnectCtrl.js
@@ -0,0 +1,5 @@
+(function(angular, $, _) {
+ angular.module('crmCxn').controller('CrmCxnConfirmConnectCtrl', function($scope) {
+ $scope.ts = CRM.ts(null);
+ });
+})(angular, CRM.$, CRM._);
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmCxn/ConfirmReconnectCtrl.html b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmCxn/ConfirmReconnectCtrl.html
new file mode 100644
index 00000000..0b60bd87
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmCxn/ConfirmReconnectCtrl.html
@@ -0,0 +1,23 @@
+<div ng-controller="CrmCxnConfirmReconnectCtrl">
+ <p>{{ts('Are you sure you want to reconnect \"%1\"?', {1: appMeta.title})}}</p>
+
+ <p>{{ts('Reconnecting will change the connection details (such as callback URLs and permissions). This can be useful in a few cases, such as:')}}</p>
+
+ <ul>
+ <li>{{ts('After your site has migrated to a new URL.')}}</li>
+ <li>{{ts('After the application has migrated to a new URL.')}}</li>
+ <li>{{ts('After the application has changed permission requirements.')}}</li>
+ <li>{{ts('After the application has a major failure or reset.')}}</li>
+ </ul>
+
+ <div crm-ui-accordion="{title: ts('Permissions: Summary'), collapsed: true}">
+ <div ng-bind-html="appMeta.perm.desc"></div>
+ </div>
+ <div crm-ui-accordion="{title: ts('Permissions: Details'), collapsed: true}">
+ <div crm-cxn-perm-table="{perm: appMeta.perm}"></div>
+ </div>
+ <div crm-ui-accordion="{title: ts('Advanced'), collapsed: true}">
+ <div crm-cxn-adv-table="{appMeta: appMeta}"></div>
+ </div>
+
+</div>
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmCxn/ConfirmReconnectCtrl.js b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmCxn/ConfirmReconnectCtrl.js
new file mode 100644
index 00000000..211d415d
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmCxn/ConfirmReconnectCtrl.js
@@ -0,0 +1,5 @@
+(function(angular, $, _) {
+ angular.module('crmCxn').controller('CrmCxnConfirmReconnectCtrl', function($scope) {
+ $scope.ts = CRM.ts(null);
+ });
+})(angular, CRM.$, CRM._);
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmCxn/Connectivity.html b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmCxn/Connectivity.html
new file mode 100644
index 00000000..e8a14d8a
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmCxn/Connectivity.html
@@ -0,0 +1,4 @@
+<p>{{ts('There was a problem verifying that this site is available on the public Internet.')}}</p>
+<p>{{ts('See also:')}}
+ <a href="https://civicrm.org/inapp/civiconnect-firewall" target="_blank">{{ts('Firewalls and Proxies')}}</a>
+</p> \ No newline at end of file
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmCxn/LinkDialogCtrl.html b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmCxn/LinkDialogCtrl.html
new file mode 100644
index 00000000..ad656409
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmCxn/LinkDialogCtrl.html
@@ -0,0 +1,3 @@
+<div ng-controller="CrmCxnLinkDialogCtrl">
+ <iframe crm-ui-iframe crm-ui-iframe-src="model.url"></iframe>
+</div>
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmCxn/LinkDialogCtrl.js b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmCxn/LinkDialogCtrl.js
new file mode 100644
index 00000000..d1672d83
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmCxn/LinkDialogCtrl.js
@@ -0,0 +1,11 @@
+(function(angular, $, _) {
+
+ // Controller for the "Open Link" dialog
+ // Scope members:
+ // - [input] "model": Object
+ // - "url": string
+ angular.module('crmCxn').controller('CrmCxnLinkDialogCtrl', function CrmCxnLinkDialogCtrl($scope, dialogService) {
+ var ts = $scope.ts = CRM.ts(null);
+ });
+
+})(angular, CRM.$, CRM._);
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmCxn/ManageCtrl.html b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmCxn/ManageCtrl.html
new file mode 100644
index 00000000..891942e6
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmCxn/ManageCtrl.html
@@ -0,0 +1,120 @@
+<div crm-ui-debug="appMetas"></div>
+<div crm-ui-debug="cxns"></div>
+<div crm-ui-debug="alerts"></div>
+
+<!--
+ The merits of this layout:
+ * On a fresh install, the available connections show up first.
+ * Once you've made a connection, the extant connections bubble up.
+ * Extant connections can be portrayed as enabled or disabled.
+-->
+
+<div class="help">
+ <p>{{ts('Connections provide a simplified way to link your CiviCRM installation to an external service.')}}</p>
+</div>
+
+<div ng-show="cxns.length > 0">
+ <span crm-ui-order="{var: 'cxnOrder', defaults: ['-created_date']}"></span>
+ <h3>{{ts('Existing Connections')}}</h3>
+ <table class="display">
+ <thead>
+ <tr>
+ <th>{{ts('Title')}}</th> <!-- <a crm-ui-order-by="[cxnOrder, 'app_meta.appId']"> -->
+ <th>{{ts('Description')}}</th> <!-- <a crm-ui-order-by="[cxnOrder, 'desc']"> -->
+ <th>{{ts('Status')}}</th>
+ <th></th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr ng-repeat="cxn in cxns | orderBy:cxnOrder.get()" ng-class-even="'even-row even'" ng-class-odd="'odd-row odd'">
+ <td>
+ <a class="action-item"
+ crm-confirm='{width: "65%", resizable: true, title:ts("%1: About", {1: cxn.app_meta.title}), templateUrl: "~/crmCxn/AboutCtrl.html", export: {appMeta: cxn.app_meta}}'
+ >{{cxn.app_meta.title}}</a>
+ </td>
+ <td><div ng-bind-html="cxn.app_meta.desc"></div></td>
+ <td>{{cxn.is_active=="1" ? ts('Enabled') : ts('Disabled')}}</td>
+ <td>
+ <span>
+ <a class="action-item crm-hover-button" ng-click="openLink(cxn.app_meta, 'settings', {title: ts('%1: Settings (External)', {1: cxn.app_meta.title})})" ng-show="cxn.app_meta.links.settings">{{ts('Settings')}}</a>
+ <span class="btn-slide crm-hover-button">{{ts('more')}}
+ <ul class="panel" style="display: none;">
+ <li ng-show="cxn.app_meta.links.logs">
+ <a class="action-item crm-hover-button" ng-click="openLink(cxn.app_meta, 'logs', {title: ts('%1: Logs (External)', {1: cxn.app_meta.title})})">
+ {{ts('Logs')}}
+ </a>
+ </li>
+ <li ng-show="cxn.app_meta.links.docs">
+ <a class="action-item crm-hover-button" ng-click="openLink(cxn.app_meta, 'docs', {title: ts('%1: Documentation (External)', {1: cxn.app_meta.title})})">
+ {{ts('Docs')}}
+ </a>
+ </li>
+ <li ng-show="cxn.app_meta.links.support">
+ <a class="action-item crm-hover-button" ng-click="openLink(cxn.app_meta, 'support', {title: ts('%1: Support (External)', {1: cxn.app_meta.title})})">
+ {{ts('Support')}}
+ </a>
+ </li>
+ <li>
+ <a class="action-item crm-hover-button" ng-click="toggleCxn(cxn)">{{ cxn.is_active=="1" ? ts('Disable') : ts('Enable')}}</a>
+ </li>
+ <li>
+ <a class="action-item crm-hover-button"
+ crm-confirm='{width: "65%", resizable: true, title:ts("%1: Reconnect", {1: cxn.app_meta.title}), templateUrl: "~/crmCxn/ConfirmReconnectCtrl.html", export: {cxn: cxn, appMeta: findAppByAppId(cxn.app_guid)}}'
+ on-yes="reregister(cxn.app_meta)"
+ >{{ts('Reconnect')}}</a>
+ </li>
+ <li>
+ <a class="action-item crm-hover-button"
+ crm-confirm='{width: "65%", resizable: true, title: ts("%1: Disconnect", {1: cxn.app_meta.title}), message: ts("Are you sure you want to disconnect \"%1?\". Doing so may permanently destroy data linkage.", {1: cxn.app_meta.title})}'
+ on-yes="unregister(cxn.app_meta)">
+ {{ts('Disconnect')}}
+ </a>
+ </li>
+ </ul>
+ </span>
+
+ </span>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ <br/>
+</div>
+
+<div ng-show="hasAvailApps()">
+ <span crm-ui-order="{var: 'availOrder', defaults: ['title']}"></span>
+
+<div class="crm-content-block crm-block crm-connection-block">
+ <h3>{{ts('New Connections')}}</h3>
+ <table class="display">
+ <thead>
+ <tr>
+ <th><a crm-ui-order-by="[availOrder, 'title']">{{ts('Title')}}</a></th>
+ <th><a crm-ui-order-by="[availOrder, 'desc']">{{ts('Description')}}</a></th>
+ <th></th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr ng-repeat="appMeta in appMetas | orderBy:availOrder.get()" ng-show="!findCxnByAppId(appMeta.appId)" ng-class-even="'even-row even'" ng-class-odd="'odd-row odd'">
+ <td>
+ <a crm-confirm='{width: "65%", resizable: true, title:ts("%1: About", {1: appMeta.title}), templateUrl: "~/crmCxn/AboutCtrl.html", export: {appMeta: appMeta}}'
+ >{{appMeta.title}}</a>
+ </td>
+ <td><div ng-bind-html="appMeta.desc"></div></td>
+ <td>
+ <a class="action-item crm-hover-button"
+ crm-confirm='{width: "65%", resizable: true, title:ts("%1: Connect", {1: appMeta.title}), templateUrl: "~/crmCxn/ConfirmConnectCtrl.html", export: {appMeta: appMeta}}'
+ on-yes="register(appMeta)"
+ >{{ts('Connect')}}</a>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+</div>
+
+</div>
+
+<div ng-show="appMetas.length === 0" class="messages status no-popup">
+ <i class="crm-i fa-info-circle"></i>
+ {{ts('No available applications')}}
+</div>
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmCxn/ManageCtrl.js b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmCxn/ManageCtrl.js
new file mode 100644
index 00000000..cd843c33
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmCxn/ManageCtrl.js
@@ -0,0 +1,153 @@
+(function(angular, $, _) {
+
+ angular.module('crmCxn').controller('CrmCxnManageCtrl', function CrmCxnManageCtrl($scope, apiCalls, crmApi, crmUiAlert, crmBlocker, crmStatus, $timeout, dialogService, crmCxnCheckAddr) {
+ var ts = $scope.ts = CRM.ts(null);
+ if (apiCalls.appMetas.is_error) {
+ $scope.appMetas = [];
+ CRM.alert(apiCalls.appMetas.error_message, ts('Application List Unavailable'), 'error');
+ }
+ else {
+ $scope.appMetas = apiCalls.appMetas.values;
+ }
+ $scope.cxns = apiCalls.cxns.values;
+ $scope.alerts = _.where(apiCalls.sysCheck.values, {name: 'checkCxnOverrides'});
+
+ crmCxnCheckAddr(apiCalls.cfg.values.siteCallbackUrl).then(function(response) {
+ if (response.valid) return;
+ crmUiAlert({
+ type: 'warning',
+ title: ts('Internet Access Required'),
+ templateUrl: '~/crmCxn/Connectivity.html',
+ scope: $scope.$new(),
+ options: {expires: false}
+ });
+ });
+
+ $scope.filter = {};
+ var block = $scope.block = crmBlocker();
+
+ _.each($scope.alerts, function(alert){
+ crmUiAlert({text: alert.message, title: alert.title, type: 'error'});
+ });
+
+ // Convert array [x] to x|null|error
+ function asOne(result, msg) {
+ switch (result.length) {
+ case 0:
+ return null;
+ case 1:
+ return result[0];
+ default:
+ throw msg;
+ }
+ }
+
+ $scope.findCxnByAppId = function(appId) {
+ var result = _.where($scope.cxns, {
+ app_guid: appId
+ });
+ return asOne(result, "Error: Too many connections for appId: " + appId);
+ };
+
+ $scope.findAppByAppId = function(appId) {
+ var result = _.where($scope.appMetas, {
+ appId: appId
+ });
+ return asOne(result, "Error: Too many apps for appId: " + appId);
+ };
+
+ $scope.hasAvailApps = function() {
+ // This should usu return after the 1st or 2nd item, but in testing with small# apps, we may exhaust the list.
+ for (var i = 0; i< $scope.appMetas.length; i++) {
+ if (!$scope.findCxnByAppId($scope.appMetas[i].appId)) {
+ return true;
+ }
+ }
+ return false;
+ };
+
+ $scope.refreshCxns = function() {
+ crmApi('Cxn', 'get', {sequential: 1}).then(function(result) {
+ $timeout(function(){
+ $scope.cxns = result.values;
+ });
+ });
+ };
+
+ $scope.register = function(appMeta) {
+ var reg = crmApi('Cxn', 'register', {app_guid: appMeta.appId}).then($scope.refreshCxns).then(function() {
+ if (appMeta.links.welcome) {
+ return $scope.openLink(appMeta, 'welcome', {title: ts('%1: Welcome (External)', {1: appMeta.title})});
+ }
+ });
+ return block(crmStatus({start: ts('Connecting...'), success: ts('Connected')}, reg));
+ };
+
+ $scope.reregister = function(appMeta) {
+ var reg = crmApi('Cxn', 'register', {app_guid: appMeta.appId}).then($scope.refreshCxns).then(function() {
+ if (appMeta.links.welcome) {
+ return $scope.openLink(appMeta, 'welcome', {title: ts('%1: Welcome (External)', {1: appMeta.title})});
+ }
+ });
+ return block(crmStatus({start: ts('Reconnecting...'), success: ts('Reconnected')}, reg));
+ };
+
+ $scope.unregister = function(appMeta) {
+ var reg = crmApi('Cxn', 'unregister', {app_guid: appMeta.appId, debug: 1}).then($scope.refreshCxns);
+ return block(crmStatus({start: ts('Disconnecting...'), success: ts('Disconnected')}, reg));
+ };
+
+ $scope.toggleCxn = function toggleCxn(cxn) {
+ var is_active = (cxn.is_active=="1" ? 0 : 1); // we switch the flag
+ var reg = crmApi('Cxn', 'create', {id: cxn.id, app_guid: cxn.app_meta.appId, is_active: is_active, debug: 1}).then(function(){
+ cxn.is_active = is_active;
+ });
+ return block(crmStatus({start: ts('Saving...'), success: ts('Saved')}, reg));
+ };
+
+ $scope.openLink = function openLink(appMeta, page, options) {
+ var promise = crmApi('Cxn', 'getlink', {app_guid: appMeta.appId, page_name: page}).then(function(result) {
+ var mode = result.values.mode ? result.values.mode : 'popup';
+ switch (result.values.mode) {
+ case 'iframe':
+ var passThrus = ['height', 'width']; // Options influenced by remote server.
+ options = angular.extend(_.pick(result.values, passThrus), options);
+ $scope.openIframe(result.values.url, options);
+ break;
+ case 'popup':
+ CRM.alert(ts('The page "%1" will open in a popup. If it does not appear automatically, check your browser for notifications.', {1: options.title}), '', 'info');
+ window.open(result.values.url, 'cxnSettings', 'resizable,scrollbars,status');
+ break;
+ case 'redirect':
+ window.location = result.values.url;
+ break;
+ default:
+ CRM.alert(ts('Cannot open link. Unrecognized mode.'), '', 'error');
+ }
+ });
+ return block(crmStatus({start: ts('Opening...'), success: ''}, promise));
+ };
+
+ // @param Object options -- see dialogService.open
+ $scope.openIframe = function openIframe(url, options) {
+ var model = {
+ url: url
+ };
+ options = CRM.utils.adjustDialogDefaults(angular.extend(
+ {
+ autoOpen: false,
+ height: 'auto',
+ width: '40%',
+ title: ts('External Link')
+ },
+ options
+ ));
+ return dialogService.open('cxnLinkDialog', '~/crmCxn/LinkDialogCtrl.html', model, options)
+ .then(function(item) {
+ mailing.msg_template_id = item.id;
+ return item;
+ });
+ };
+ });
+
+})(angular, CRM.$, CRM._);
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmCxn/PermTable.html b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmCxn/PermTable.html
new file mode 100644
index 00000000..d2a1eabf
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmCxn/PermTable.html
@@ -0,0 +1,42 @@
+<table>
+ <thead>
+ <tr>
+ <th>{{ts('Entity')}}</th>
+ <th>{{ts('Action(s)')}}</th>
+ <th>{{ts('Filter(s)')}}</th>
+ <th>{{ts('Field(s)')}}</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr ng-repeat="api in perm.api"
+ ng-class-even="'even-row even'"
+ ng-class-odd="'odd-row odd'">
+ <td>
+ <em ng-show="api.entity == '*'">{{ts('Any')}}</em>
+ <code ng-hide="api.entity == '*'">{{api.entity}}</code>
+ </td>
+ <td>
+ <div ng-switch="isString(api.actions)">
+ <span ng-switch-when="true">
+ <em ng-show="api.actions == '*'">{{ts('Any')}}</em>
+ <code ng-hide="api.actions == '*'">{{api.actions}}</code>
+ </span>
+ <span ng-switch-default="">
+ <span ng-repeat="action in api.actions"><code>{{action}}</code><span ng-show="!$last">, </span></span>
+ </span>
+ </div>
+ </td>
+ <td>
+ <em ng-show="!hasRequiredFilters(api)">{{ts('Any')}}</em>
+ <div ng-repeat="(field,value) in api.required"><code>{{field}}</code> = "<code>{{value}}</code>"<span ng-show="!$last">, </span></div>
+ </td>
+ <td>
+ <em ng-show="api.fields == '*'">{{ts('Any')}}</em>
+ <span ng-hide="api.fields == '*'" ng-repeat="field in api.fields"><code>{{field}}</code><span ng-show="!$last">, </span></span>
+ </td>
+ </tr>
+ </tbody>
+</table>
+<div class="crmCxn-footer">
+ <em ng-bind-html="ts('For in-depth details about entities and actions, see the <a href=\'%1\' target=\'%2\'>API Explorer</a>.', {1: apiExplorerUrl, 2: '_blank'})"></em>
+</div>
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmCxn/PermTable.js b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmCxn/PermTable.js
new file mode 100644
index 00000000..eb7da355
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmCxn/PermTable.js
@@ -0,0 +1,27 @@
+(function(angular, $, _) {
+
+ // This directive formats the data in appMeta.perm as a nice table.
+ // example: <div crm-cxn-perm-table="{perm: cxn.app_meta.perm}"></div>
+ angular.module('crmCxn').directive('crmCxnPermTable', function crmCxnPermTable() {
+ return {
+ restrict: 'EA',
+ scope: {
+ crmCxnPermTable: '='
+ },
+ templateUrl: '~/crmCxn/PermTable.html',
+ link: function(scope, element, attrs) {
+ scope.ts = CRM.ts(null);
+ scope.hasRequiredFilters = function(api) {
+ return !_.isEmpty(api.required);
+ };
+ scope.isString = function(v) {
+ return _.isString(v);
+ };
+ scope.apiExplorerUrl = CRM.url('civicrm/api');
+ scope.$watch('crmCxnPermTable', function(crmCxnPermTable){
+ scope.perm = crmCxnPermTable.perm;
+ });
+ }
+ };
+ });
+})(angular, CRM.$, CRM._);
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmD3.ang.php b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmD3.ang.php
new file mode 100644
index 00000000..0a737341
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmD3.ang.php
@@ -0,0 +1,16 @@
+<?php
+// This file declares an Angular module which can be autoloaded
+// in CiviCRM. See also:
+// http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_angularModules
+
+// ODDITY: Only loads if you have CiviMail permissions.
+// ODDITY: Extra resources loaded via CRM_Mailing_Info::getAngularModules.
+
+return [
+ 'ext' => 'civicrm',
+ 'js' => [
+ 'ang/crmD3.js',
+ 'bower_components/d3/d3.min.js',
+ ],
+ 'requires' => [],
+];
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmD3.js b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmD3.js
new file mode 100644
index 00000000..d06eeac9
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmD3.js
@@ -0,0 +1,3 @@
+(function (angular, $, _) {
+ angular.module('crmD3', CRM.angRequires('crmD3'));
+})(angular, CRM.$, CRM._);
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmExample.ang.php b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmExample.ang.php
new file mode 100644
index 00000000..763f1eac
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmExample.ang.php
@@ -0,0 +1,11 @@
+<?php
+// This file declares an Angular module which can be autoloaded
+// in CiviCRM. See also:
+// http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_angularModules
+
+return [
+ 'ext' => 'civicrm',
+ 'js' => ['ang/crmExample.js'],
+ 'partials' => ['ang/crmExample'],
+ 'requires' => ['crmUtil', 'ngRoute', 'ui.utils', 'crmUi', 'crmResource'],
+];
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmExample.js b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmExample.js
new file mode 100644
index 00000000..138ce8f1
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmExample.js
@@ -0,0 +1,45 @@
+(function(angular, $, _) {
+
+ angular.module('crmExample', CRM.angRequires('crmExample'));
+
+ angular.module('crmExample').config([
+ '$routeProvider',
+ function($routeProvider) {
+ $routeProvider.when('/example', {
+ templateUrl: '~/crmExample/example.html',
+ controller: 'ExampleCtrl'
+ });
+ }
+ ]);
+
+ angular.module('crmExample').controller('ExampleCtrl', function ExampleCtrl($scope) {
+ $scope.ts = CRM.ts(null);
+
+ //$scope.examples = {
+ // blank1: {value: '', required: false},
+ // blank2: {value: '', required: true},
+ // filled1: {value:'2014-01-02', required: false},
+ // filled2: {value:'2014-02-03', required: true}
+ //};
+
+ //$scope.examples = {
+ // blank1: {value: '', required: false},
+ // blank2: {value: '', required: true},
+ // filled1: {value:'12:34', required: false},
+ // filled2: {value:'10:09', required: true}
+ //};
+
+ $scope.examples = {
+ blank: {value: '', required: false},
+ //blankReq: {value: '', required: true},
+ filled: {value:'2014-01-02 03:04', required: false},
+ //filledReq: {value:'2014-02-03 05:06', required: true},
+ missingDate: {value:' 05:06', required: false},
+ //missingDateReq: {value:' 05:06', required: true},
+ missingTime: {value:'2014-03-04 ', required: false}
+ //missingTimeReq: {value:'2014-03-04 ', required: true}
+ };
+
+ });
+
+})(angular, CRM.$, CRM._);
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmExample/example.html b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmExample/example.html
new file mode 100644
index 00000000..5393e7c1
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmExample/example.html
@@ -0,0 +1,42 @@
+<form name="exampleForm" novalidate>
+ <table>
+ <thead>
+ <tr>
+ <th>Name</th>
+ <th>Value</th>
+ <th>Input</th>
+ <th>ngModel</th>
+ </tr>
+ </thead>
+
+ <tbody>
+
+ <tr ng-repeat="(exName, example) in examples">
+ <td>{{exName}}</td>
+ <td>{{example.value}}</td>
+ <td>
+ <div class="crmMailing-schedule-outer" crm-mailing-radio-date="schedule" ng-model="example.value"
+ name="{{exName}}">
+
+ <div class="crmMailing-schedule-inner">
+ <div>
+ <input ng-model="schedule.mode" type="radio" name="send_{{exName}}" 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_{{exName}}" 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>
+ </td>
+ <td>
+ <pre>{{exampleForm[exName]|json}}</pre>
+ </td>
+ </tr>
+
+ </tbody>
+ </table>
+
+</form>
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing.ang.php b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing.ang.php
new file mode 100644
index 00000000..a3e140c6
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing.ang.php
@@ -0,0 +1,18 @@
+<?php
+// This file declares an Angular module which can be autoloaded
+// in CiviCRM. See also:
+// http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_angularModules
+
+// ODDITY: Only loads if you have CiviMail permissions.
+// ODDITY: Extra resources loaded via CRM_Mailing_Info::getAngularModules.
+
+return [
+ 'ext' => 'civicrm',
+ 'js' => [
+ 'ang/crmMailing.js',
+ 'ang/crmMailing/*.js',
+ ],
+ 'css' => ['ang/crmMailing.css'],
+ 'partials' => ['ang/crmMailing'],
+ 'requires' => ['crmUtil', 'crmAttachment', 'crmAutosave', 'ngRoute', 'ui.utils', 'crmUi', 'dialogService', 'crmResource'],
+];
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing.css b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing.css
new file mode 100644
index 00000000..b853cb44
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing.css
@@ -0,0 +1,113 @@
+.crmMailing input[name=subject] {
+ width: 30em;
+}
+.crmMailing select, .crmMailing input[type=text] {
+ width: 36em;
+}
+.crmMailing textarea {
+ margin: 0.5em;
+ width: 95%;
+ height: 20em;
+}
+.crmMailing input.crm-form-date {
+ width: 10em;
+}
+
+.crmMailing-recip-est {
+ background: #ee8;
+ font-size: small;
+ padding: 0.33em;
+ margin: 0 0 0 0.5em;
+ width: 9em;
+ text-align: center;
+}
+
+span.crmMailing-include {
+ color: #060;
+}
+span.crmMailing-exclude {
+ color: #600;
+ text-decoration: line-through;
+}
+span.crmMailing-mandatory {
+ color: #866304;
+}
+
+.crmMailing input[name=preview_test_email], .crmMailing-preview select[name=preview_test_group] {
+ width: 80%;
+}
+
+.crmMailing .preview-popup, .crmMailing .preview-contact, .crmMailing .preview-group {
+ width: 30%;
+ height: 4.5em;
+ margin: 0.5em;
+ text-align: center;
+ vertical-align: middle;
+ float: left;
+}
+.crmMailing .preview-popup, .crmMailing .preview-contact {
+ border-right: 1px solid black;
+}
+.crmMailing .preview-group, .crmMailing .preview-contact {
+}
+
+.crmMailing .crmMailing-schedule-outer {
+ width: 98%
+}
+.crmMailing .crmMailing-schedule-inner {
+ width: 40em;
+ text-align: left;
+ margin: auto;
+}
+
+/* Odd: These placeholder directives break if combined */
+input[name=preview_test_email]:-moz-placeholder {
+ text-align: center;
+}
+input[name=preview_test_email]::-moz-placeholder {
+ text-align: center;
+}
+input[name=preview_test_email]::-webkit-input-placeholder {
+ text-align: center;
+}
+input[name=preview_test_email]:-ms-input-placeholder {
+ text-align: center;
+}
+.crmMailing-active {
+}
+.crmMailing-inactive {
+ text-decoration: line-through;
+}
+.crm-container a.crmMailing-submit-button {
+ display: inline-block;
+ padding: .2em .4em;
+ margin: 1em auto;
+ border-radius: 5px;
+ font-size: 1.2em;
+ float: none;
+}
+.crm-container a.crmMailing-submit-button div {
+ background: url(../i/check.gif) no-repeat left center;
+ padding-left: 20px;
+}
+.crm-container a.crmMailing-submit-button.disabled,
+.crm-container a.crmMailing-submit-button.blocking {
+ opacity: .6;
+ cursor: default;
+}
+.crm-container a.crmMailing-submit-button.blocking div {
+ background: url(../i/loading-2f2f2e.gif) no-repeat left center;
+}
+
+.crm-container .crm-form-block label {
+ font-size: 13px;
+}
+
+.crm-container .ui-widget-content {
+ background: none;
+}
+
+.crmMailing-error-link {
+ margin: 0.5em;
+ color: red;
+}
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing.js b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing.js
new file mode 100644
index 00000000..95148bf2
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing.js
@@ -0,0 +1,61 @@
+(function (angular, $, _) {
+
+ angular.module('crmMailing', CRM.angRequires('crmMailing'));
+
+ angular.module('crmMailing').config([
+ '$routeProvider',
+ function ($routeProvider) {
+ $routeProvider.when('/mailing', {
+ template: '<div></div>',
+ controller: 'ListMailingsCtrl'
+ });
+
+ if (!CRM || !CRM.crmMailing) {
+ return;
+ }
+
+ $routeProvider.when('/mailing/new', {
+ template: '<p>' + ts('Initializing...') + '</p>',
+ controller: 'CreateMailingCtrl',
+ resolve: {
+ selectedMail: function(crmMailingMgr) {
+ var m = crmMailingMgr.create({
+ template_type: CRM.crmMailing.templateTypes[0].name
+ });
+ return crmMailingMgr.save(m);
+ }
+ }
+ });
+
+ $routeProvider.when('/mailing/new/:templateType', {
+ template: '<p>' + ts('Initializing...') + '</p>',
+ controller: 'CreateMailingCtrl',
+ resolve: {
+ selectedMail: function($route, crmMailingMgr) {
+ var m = crmMailingMgr.create({
+ template_type: $route.current.params.templateType
+ });
+ return crmMailingMgr.save(m);
+ }
+ }
+ });
+
+ $routeProvider.when('/mailing/:id', {
+ templateUrl: '~/crmMailing/EditMailingCtrl/base.html',
+ controller: 'EditMailingCtrl',
+ resolve: {
+ selectedMail: function($route, crmMailingMgr) {
+ return crmMailingMgr.get($route.current.params.id);
+ },
+ attachments: function($route, CrmAttachments) {
+ var attachments = new CrmAttachments(function () {
+ return {entity_table: 'civicrm_mailing', entity_id: $route.current.params.id};
+ });
+ return attachments.load();
+ }
+ }
+ });
+ }
+ ]);
+
+})(angular, CRM.$, CRM._);
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockApprove.html b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockApprove.html
new file mode 100644
index 00000000..e3971ca7
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockApprove.html
@@ -0,0 +1,14 @@
+<div class="crm-block" ng-form="apprForm" crm-ui-id-scope>
+ <div class="crm-group">
+ <div crm-ui-field="{title: ts('Status')}">
+ {{mailingFields.approval_status_id.optionsMap[mailing.approval_status_id] || ts('Unreviewed')}}
+ </div>
+ <div crm-ui-field="{name: 'apprForm.approval_note', title: ts('Note')}">
+ <textarea
+ crm-ui-id="apprForm.approval_note"
+ name="approval_note"
+ ng-model="mailing.approval_note"
+ ></textarea>
+ </div>
+ </div>
+</div>
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockApprove.js b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockApprove.js
new file mode 100644
index 00000000..8f0cd599
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockApprove.js
@@ -0,0 +1,5 @@
+(function(angular, $, _) {
+ angular.module('crmMailing').directive('crmMailingBlockApprove', function(crmMailingSimpleDirective) {
+ return crmMailingSimpleDirective('crmMailingBlockApprove', '~/crmMailing/BlockApprove.html');
+ });
+})(angular, CRM.$, CRM._);
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockHeaderFooter.html b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockHeaderFooter.html
new file mode 100644
index 00000000..249560d7
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockHeaderFooter.html
@@ -0,0 +1,32 @@
+<!--
+Controller: EditMailingCtrl
+Required vars: mailing, crmMailingConst
+-->
+<div class="crm-block" ng-form="subform" crm-ui-id-scope>
+ <div class="crm-group" ng-controller="EmailBodyCtrl">
+ <div crm-ui-field="{name: 'subform.header_id', title: ts('Mailing Header'), help: hs('header')}">
+ <select
+ crm-ui-id="subform.header_id"
+ name="header_id"
+ ui-jq="select2"
+ ui-options="{dropdownAutoWidth : true, allowClear: true}"
+ ng-change="checkTokens(mailing, '*')"
+ ng-model="mailing.header_id"
+ ng-options="mc.id as mc.name for mc in crmMailingConst.headerfooterList | filter:{component_type: 'Header'} | orderBy:'name'">
+ <option value=""></option>
+ </select>
+ </div>
+ <div crm-ui-field="{name: 'subform.footer_id', title: ts('Mailing Footer'), help: hs('footer')}">
+ <select
+ crm-ui-id="subform.footer_id"
+ name="footer_id"
+ ui-jq="select2"
+ ui-options="{dropdownAutoWidth : true, allowClear: true}"
+ ng-change="checkTokens(mailing, '*')"
+ ng-model="mailing.footer_id"
+ ng-options="mc.id as mc.name for mc in crmMailingConst.headerfooterList | filter:{component_type: 'Footer'} | orderBy:'name'">
+ <option value=""></option>
+ </select>
+ </div>
+ </div>
+</div>
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockHeaderFooter.js b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockHeaderFooter.js
new file mode 100644
index 00000000..babba177
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockHeaderFooter.js
@@ -0,0 +1,5 @@
+(function(angular, $, _) {
+ angular.module('crmMailing').directive('crmMailingBlockHeaderFooter', function(crmMailingSimpleDirective) {
+ return crmMailingSimpleDirective('crmMailingBlockHeaderFooter', '~/crmMailing/BlockHeaderFooter.html');
+ });
+})(angular, CRM.$, CRM._);
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockMailing.html b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockMailing.html
new file mode 100644
index 00000000..8e099743
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockMailing.html
@@ -0,0 +1,82 @@
+<!--
+Controller: EditMailingCtrl
+Required vars: mailing, crmMailingConst
+Note: Much of this file is duplicated in crmMailing and crmMailingAB with variations on placement/title/binding.
+It could perhaps be thinned by 30-60% by making more directives.
+-->
+<div class="crm-block" ng-form="subform" crm-ui-id-scope>
+ <div class="crm-group">
+ <div crm-ui-field="{name: 'subform.msg_template_id', title: ts('Template')}">
+ <div crm-mailing-block-templates="{name: 'templates', id: 'subform.msg_template_id'}" crm-mailing="mailing"></div>
+ </div>
+ <div crm-ui-field="{name: 'subform.fromAddress', title: ts('From'), help: hs('from_email')}">
+ <div ng-controller="EmailAddrCtrl" crm-mailing-from-address="fromPlaceholder" crm-mailing="mailing">
+ <select
+ crm-ui-id="subform.fromAddress"
+ crm-ui-select="{dropdownAutoWidth : true, allowClear: false, placeholder: ts('Email address')}"
+ name="fromAddress"
+ ng-model="fromPlaceholder.label"
+ required>
+ <option value=""></option>
+ <option ng-repeat="frm in crmFromAddresses.getAll() | filter:{is_active:1} | orderBy:'weight'" value="{{frm.label}}">{{frm.label}}</option>
+ </select>
+ </div>
+ </div>
+ <div crm-ui-field="{name: 'subform.replyTo', title: ts('Reply-To')}" ng-show="crmMailingConst.enableReplyTo">
+ <div ng-controller="EmailAddrCtrl">
+ <select
+ crm-ui-id="subform.replyTo"
+ crm-ui-select="{dropdownAutoWidth : true, allowClear: true, placeholder: ts('Email address')}"
+ name="replyTo"
+ ng-change="checkReplyToChange(mailing)"
+ ng-model="mailing.replyto_email"
+ >
+ <option value=""></option>
+ <option ng-repeat="frm in crmFromAddresses.getAll() | filter:{is_active:1} | orderBy:'weight'" value="{{frm.label}}">{{frm.label}}</option>
+ </select>
+ </div>
+ </div>
+ <div crm-ui-field="{name: 'subform.recipients', title: ts('Recipients'), required: true}">
+ <div crm-mailing-block-recipients="{name: 'recipients', id: 'subform.recipients'}" crm-mailing="mailing" cm-ui-id="subform.recipients"></div>
+ </div>
+ <span ng-controller="EditUnsubGroupCtrl">
+ <div crm-ui-field="{name: 'subform.baseGroup', title: ts('Unsubscribe Group')}" ng-if="isUnsubGroupRequired(mailing)">
+ <input
+ crm-entityref="{entity: 'Group', api: {params: {is_hidden: 0, is_active: 1}}, select: {allowClear:true, minimumInputLength: 0}}"
+ crm-ui-id="subform.baseGroup"
+ name="baseGroup"
+ ng-model="mailing.recipients.groups.base[0]"
+ ng-required="true"
+ />
+ </div>
+ </span>
+ <div crm-ui-field="{name: 'subform.subject', title: ts('Subject')}">
+ <div style="float: right;">
+ <input crm-mailing-token on-select="$broadcast('insert:subject', token.name)" tabindex="-1"/>
+ </div>
+ <input
+ crm-ui-id="subform.subject"
+ crm-ui-insert-rx="insert:subject"
+ type="text"
+ class="crm-form-text"
+ ng-model="mailing.subject"
+ required
+ placeholder="Subject"
+ name="subject" />
+ </div>
+ <div ng-if="crmMailingConst.isMultiLingual">
+ <div crm-ui-field="{name: 'subform.language', title: ts('Language')}">
+ <select
+ crm-ui-id="subform.language"
+ crm-ui-select="{dropdownAutoWidth : true, allowClear: false, placeholder: ts('- choose language -')}"
+ name="language"
+ ng-model="mailing.language"
+ required
+ >
+ <option value=""></option>
+ <option ng-repeat="(key,val) in crmMailingConst.enabledLanguages" value="{{key}}">{{val}}</option>
+ </select>
+ </div>
+ </div>
+ </div>
+</div>
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockMailing.js b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockMailing.js
new file mode 100644
index 00000000..a2297d02
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockMailing.js
@@ -0,0 +1,5 @@
+(function(angular, $, _) {
+ angular.module('crmMailing').directive('crmMailingBlockMailing', function(crmMailingSimpleDirective) {
+ return crmMailingSimpleDirective('crmMailingBlockMailing', '~/crmMailing/BlockMailing.html');
+ });
+})(angular, CRM.$, CRM._);
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockPreview.html b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockPreview.html
new file mode 100644
index 00000000..6315466e
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockPreview.html
@@ -0,0 +1,57 @@
+<!--
+Vars: mailing:obj, testContact:obj, testGroup:obj, crmMailing:FormController
+-->
+<div class="crmMailing-preview">
+ <!-- Note:
+ In Firefox (at least), clicking the preview buttons causes the browser to display validation warnings
+ for unrelated fields *and* display preview. To avoid this weird UX, we disable preview buttons when the form is incomplete/invalid.
+ -->
+ <div class="preview-popup">
+ <div ng-show="!mailing.body_html && !mailing.body_text">
+ <em>({{ts('No content to preview')}})</em>
+ </div>
+ <div ng-hide="!mailing.body_html">
+ <a class="crm-hover-button action-item" crm-icon="fa-television" ng-disabled="crmMailing.$invalid" ng-click="doPreview('html')">{{ts('Preview as HTML')}}</a>
+ </div>
+ <div ng-hide="!mailing.body_html && !mailing.body_text" style="margin-top: 1em;">
+ <a class="crm-hover-button action-item" crm-icon="fa-file-text-o" ng-disabled="crmMailing.$invalid" ng-click="doPreview('text')">{{ts('Preview as Plain Text')}}</a>
+ </div>
+ <!--
+ <div ng-hide="!mailing.body_html && !mailing.body_text">
+ <button ng-disabled="crmMailing.$invalid" ng-click="doPreview('full')">{{ts('Preview')}}</button>
+ </div>
+ -->
+ </div>
+ <div class="preview-contact" ng-form="">
+ <div>
+ {{ts('Send test email to:')}}
+ <a crm-ui-help="hs({id: 'test', title: ts('Test Email')})"></a>
+ </div>
+ <div>
+ <input
+ name="preview_test_email"
+ type="text"
+ class="crm-form-text"
+ ng-model="testContact.email"
+ placeholder="example@example.org"
+ crm-multiple-email
+ />
+ </div>
+ <button crm-icon="fa-paper-plane" title="{{crmMailing.$invalid || !testContact.email ? ts('Complete all required fields first') : ts('Send test message to %1', {1: testContact.email})}}" ng-disabled="crmMailing.$invalid || !testContact.email" ng-click="doSend({email: testContact.email})" class="crmMailing-btn-primary">{{ts('Send test')}}</button>
+ </div>
+ <div class="preview-group" ng-form="">
+ <div>
+ {{ts('Send test email to group:')}}
+ <a crm-ui-help="hs({id: 'test', title: ts('Test Email')})"></a>
+ </div>
+ <div>
+ <input
+ crm-entityref="{entity: 'Group', api: {params: {is_hidden: 0, is_active: 1}}, select: {allowClear:true, minimumInputLength: 0}}"
+ ng-model="testGroup.gid"
+ class="crm-action-menu fa-envelope-o"
+ />
+ </div>
+ <button crm-icon="fa-paper-plane" title="{{crmMailing.$invalid || !testGroup.gid ? ts('Complete all required fields first') : ts('Send test message to group')}}" ng-disabled="crmMailing.$invalid || !testGroup.gid" crm-confirm="{resizable: true, width: '40%', height: '40%', open: previewTestGroup}" on-yes="doSend({gid: testGroup.gid})" class="crmMailing-btn-primary">{{ts('Send test')}}</button>
+ </div>
+ <div class="clear"></div>
+</div>
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockPreview.js b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockPreview.js
new file mode 100644
index 00000000..5e582dc0
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockPreview.js
@@ -0,0 +1,65 @@
+(function(angular, $, _) {
+ // example: <div crm-mailing-block-preview crm-mailing="myMailing" on-preview="openPreview(myMailing, preview.mode)" on-send="sendEmail(myMailing,preview.recipient)">
+ // note: the directive defines a variable called "preview" with any inputs supplied by the user (e.g. the target recipient for an example mailing)
+
+ angular.module('crmMailing').directive('crmMailingBlockPreview', function(crmUiHelp) {
+ return {
+ templateUrl: '~/crmMailing/BlockPreview.html',
+ link: function(scope, elm, attr) {
+ scope.$watch(attr.crmMailing, function(newValue) {
+ scope.mailing = newValue;
+ });
+ scope.crmMailingConst = CRM.crmMailing;
+ scope.ts = CRM.ts(null);
+ scope.hs = crmUiHelp({file: 'CRM/Mailing/MailingUI'});
+ scope.testContact = {email: CRM.crmMailing.defaultTestEmail};
+ scope.testGroup = {gid: null};
+
+ scope.doPreview = function(mode) {
+ scope.$eval(attr.onPreview, {
+ preview: {mode: mode}
+ });
+ };
+ scope.doSend = function doSend(recipient) {
+ recipient = JSON.parse(JSON.stringify(recipient).replace(/\,\s/g, ','));
+ scope.$eval(attr.onSend, {
+ preview: {recipient: recipient}
+ });
+ };
+
+ scope.previewTestGroup = function(e) {
+ var $dialog = $(this);
+ $dialog.html('<div class="crm-loading-element"></div>').parent().find('button[data-op=yes]').prop('disabled', true);
+ CRM.api3({
+ contact: ['contact', 'get', {group: scope.testGroup.gid, options: {limit: 0}, return: 'display_name,email'}],
+ group: ['group', 'getsingle', {id: scope.testGroup.gid, return: 'title'}]
+ }).done(function(data) {
+ $dialog.dialog('option', 'title', ts('Send to %1', {1: data.group.title}));
+ var count = 0,
+ // Fixme: should this be in a template?
+ markup = '<ol>';
+ _.each(data.contact.values, function(row) {
+ // Fixme: contact api doesn't seem capable of filtering out contacts with no email, so we're doing it client-side
+ if (row.email) {
+ count++;
+ markup += '<li>' + row.display_name + ' - ' + row.email + '</li>';
+ }
+ });
+ markup += '</ol>';
+ markup = '<h4>' + ts('A test message will be sent to %1 people:', {1: count}) + '</h4>' + markup;
+ if (!count) {
+ markup = '<div class="messages status"><i class="crm-i fa-exclamation-triangle"></i> ' +
+ (data.contact.count ? ts('None of the contacts in this group have an email address.') : ts('Group is empty.')) +
+ '</div>';
+ }
+ $dialog
+ .html(markup)
+ .trigger('crmLoad')
+ .parent().find('button[data-op=yes]').prop('disabled', !count);
+ });
+ };
+ }
+ };
+ });
+
+})(angular, CRM.$, CRM._);
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockPublication.html b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockPublication.html
new file mode 100644
index 00000000..6e82b9fa
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockPublication.html
@@ -0,0 +1,16 @@
+<div class="crm-block" ng-form="subform" crm-ui-id-scope>
+ <div class="crm-group">
+ <div crm-ui-field="{name: 'subform.visibility', title: ts('Mailing Visibility'), help: hs('visibility')}">
+ <select
+ crm-ui-id="subform.visibility"
+ name="visibility"
+ ui-jq="select2"
+ ui-options="{dropdownAutoWidth : true}"
+ ng-model="mailing.visibility"
+ ng-options="v.key as v.value for v in crmMailingConst.visibility"
+ required
+ >
+ </select>
+ </div>
+ </div>
+</div>
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockPublication.js b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockPublication.js
new file mode 100644
index 00000000..8bb02579
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockPublication.js
@@ -0,0 +1,5 @@
+(function(angular, $, _) {
+ angular.module('crmMailing').directive('crmMailingBlockPublication', function (crmMailingSimpleDirective) {
+ return crmMailingSimpleDirective('crmMailingBlockPublication', '~/crmMailing/BlockPublication.html');
+ });
+})(angular, CRM.$, CRM._);
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockRecipients.html b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockRecipients.html
new file mode 100644
index 00000000..cedfa6dc
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockRecipients.html
@@ -0,0 +1,15 @@
+<div ng-controller="EditRecipCtrl" class="crm-mailing-recipients-row">
+ <input
+ type="hidden"
+ crm-mailing-recipients
+ ng-model="mailing.recipients"
+ crm-mandatory-groups="crmMailingConst.groupNames | filter:{is_hidden:1}"
+ crm-ui-id="{{crmMailingBlockRecipients.id}}"
+ name="{{crmMailingBlockRecipients.name}}"
+ ng-required="true" />
+ <a crm-icon="fa-wrench" ng-click="editOptions(mailing)" class="crm-hover-button" title="{{ts('Edit Recipient Options')}}"></a>
+ <div ng-style="{display: permitRecipientRebuild ? '' : 'inline-block'}">
+ <button ng-click="rebuildRecipients()" ng-show="permitRecipientRebuild" class="crm-button" title="{{ts('Click to refresh recipient count')}}">{{getRecipientsEstimate()}}</button>
+ <a ng-click="previewRecipients()" class="crm-hover-button" title="{{ts('Preview a List of Recipients')}}" style="font-weight: bold;">{{getRecipientCount()}}</a>
+ </div>
+</div>
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockRecipients.js b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockRecipients.js
new file mode 100644
index 00000000..fdb45313
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockRecipients.js
@@ -0,0 +1,5 @@
+(function(angular, $, _) {
+ angular.module('crmMailing').directive('crmMailingBlockRecipients', function(crmMailingSimpleDirective) {
+ return crmMailingSimpleDirective('crmMailingBlockRecipients', '~/crmMailing/BlockRecipients.html');
+ });
+})(angular, CRM.$, CRM._);
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockResponses.html b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockResponses.html
new file mode 100644
index 00000000..cf56f007
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockResponses.html
@@ -0,0 +1,82 @@
+<!--
+Controller: EditMailingCtrl
+Required vars: mailing, crmMailingConst
+-->
+<div class="crm-block" ng-form="responseForm" crm-ui-id-scope>
+ <div class="crm-group">
+ <div crm-ui-field="{title: ts('Track Replies'), help: hs('override_verp')}" crm-layout="checkbox">
+ <!-- Comparing data-model and UI of "override_verp", note that true/false are inverted (enabled==0,disabled==1) -->
+ <span ng-controller="EmailAddrCtrl">
+ <input
+ name="override_verp"
+ type="checkbox"
+ ng-change="checkVerpChange(mailing)"
+ ng-model="mailing.override_verp"
+ ng-true-value="'0'"
+ ng-false-value="'1'"
+ />
+ </span>
+ </div>
+ <div crm-ui-field="{title: ts('Forward Replies'), help: hs('forward_replies')}" crm-layout="checkbox" ng-show="'0' == mailing.override_verp">
+ <input name="forward_replies" type="checkbox" ng-model="mailing.forward_replies" ng-true-value="'1'" ng-false-value="'0'" />
+ </div>
+ <div crm-ui-field="{title: ts('Auto-Respond to Replies'), help: hs('auto_responder')}" crm-layout="checkbox" ng-show="'0' == mailing.override_verp">
+ <input name="auto_responder" type="checkbox" ng-model="mailing.auto_responder" ng-true-value="'1'" ng-false-value="'0'" />
+ </div>
+ </div>
+</div>
+
+<hr/>
+
+<div class="crm-block" ng-form="subform" crm-ui-id-scope>
+ <div class="crm-group">
+ <div crm-ui-field="{name: 'subform.reply_id', title: ts('Auto-Respond Message')}" ng-show="'0' == mailing.override_verp && '1' == mailing.auto_responder">
+ <select
+ crm-ui-id="subform.reply_id"
+ name="reply_id"
+ ui-jq="select2"
+ ui-options="{dropdownAutoWidth : true}"
+ ng-model="mailing.reply_id"
+ ng-options="mc.id as mc.name for mc in crmMailingConst.headerfooterList | filter:{component_type: 'Reply'}"
+ required>
+ <option value=""></option>
+ </select>
+ </div>
+ <div crm-ui-field="{name: 'subform.optout_id', title: ts('Opt-out Message')}">
+ <select
+ crm-ui-id="subform.optout_id"
+ name="optout_id"
+ ui-jq="select2"
+ ui-options="{dropdownAutoWidth : true}"
+ ng-model="mailing.optout_id"
+ ng-options="mc.id as mc.name for mc in crmMailingConst.headerfooterList | filter:{component_type: 'OptOut'}"
+ required>
+ <option value=""></option>
+ </select>
+ </div>
+ <div crm-ui-field="{name: 'subform.resubscribe_id', title: ts('Resubscribe Message')}">
+ <select
+ crm-ui-id="subform.resubscribe_id"
+ name="resubscribe_id"
+ ui-jq="select2"
+ ui-options="{dropdownAutoWidth : true}"
+ ng-model="mailing.resubscribe_id"
+ ng-options="mc.id as mc.name for mc in crmMailingConst.headerfooterList | filter:{component_type: 'Resubscribe'}"
+ required>
+ <option value=""></option>
+ </select>
+ </div>
+ <div crm-ui-field="{name: 'subform.unsubscribe_id', title: ts('Unsubscribe Message')}">
+ <select
+ crm-ui-id="subform.unsubscribe_id"
+ name="unsubscribe_id"
+ ui-jq="select2"
+ ui-options="{dropdownAutoWidth : true}"
+ ng-model="mailing.unsubscribe_id"
+ ng-options="mc.id as mc.name for mc in crmMailingConst.headerfooterList | filter:{component_type: 'Unsubscribe'}"
+ required>
+ <option value=""></option>
+ </select>
+ </div>
+ </div>
+</div>
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockResponses.js b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockResponses.js
new file mode 100644
index 00000000..ba7d7897
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockResponses.js
@@ -0,0 +1,5 @@
+(function(angular, $, _) {
+ angular.module('crmMailing').directive('crmMailingBlockResponses', function(crmMailingSimpleDirective) {
+ return crmMailingSimpleDirective('crmMailingBlockResponses', '~/crmMailing/BlockResponses.html');
+ });
+})(angular, CRM.$, CRM._);
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockReview.html b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockReview.html
new file mode 100644
index 00000000..5a0021e4
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockReview.html
@@ -0,0 +1,61 @@
+<!--
+Controller: EditMailingCtrl
+Required vars: mailing, attachments
+-->
+<div>
+ <div class="crm-block" ng-form="reviewForm" crm-ui-id-scope>
+ <div class="crm-group">
+ <div crm-ui-field="{title: ts('Mailing Name')}">
+ {{mailing.name}}
+ </div>
+ <div crm-ui-field="{title: ts('Recipients')}">
+ <div ng-controller="ViewRecipCtrl">
+ <div ng-controller="EditRecipCtrl">
+ <div><a crm-icon="fa-users" class="crm-hover-button action-item" ng-click="previewRecipients()">{{getRecipientCount()}}</a></div>
+ <div ng-show="getIncludesAsString(mailing)">
+ (<strong>{{ts('Include:')}}</strong> {{getIncludesAsString(mailing)}})
+ </div>
+ <div ng-show="getExcludesAsString(mailing)">
+ (<strong>{{ts('Exclude:')}}</strong> <s>{{getExcludesAsString(mailing)}}</s>)
+ </div>
+ </div>
+ </div>
+ </div>
+ <div crm-ui-field="{title: ts('Content')}">
+ <span ng-show="mailing.body_html"><a crm-icon="fa-television" class="crm-hover-button action-item" ng-click="previewMailing(mailing, 'html')">{{ts('HTML')}}</a></span>
+ <span ng-show="mailing.body_html || mailing.body_text"><a crm-icon="fa-file-text-o" class="crm-hover-button action-item" ng-click="previewMailing(mailing, 'text')">{{ts('Plain Text')}}</a></span>
+ </div>
+ <div crm-ui-field="{title: ts('Attachments')}">
+ <div ng-repeat="file in attachments.files">
+ <a ng-href="{{file.url}}" target="_blank">{{file.name}}</a>
+ </div>
+ <div ng-repeat="item in attachments.uploader.queue">
+ {{item.file.name}}
+ </div>
+ <div ng-show="!attachments.files.length && !attachments.uploader.queue.length"><em>{{ts('None')}}</em></div>
+ </div>
+ <div ng-if="crmMailingConst.isMultiLingual" crm-ui-field="{title: ts('Language')}">
+ {{crmMailingConst.enabledLanguages[mailing.language]}}
+ </div>
+ <div crm-ui-field="{title: ts('Tracking')}">
+ <span crm-mailing-review-bool crm-on="mailing.url_tracking=='1'" crm-title="ts('Click-Throughs')"></span>
+ <span crm-mailing-review-bool crm-on="mailing.open_tracking=='1'" crm-title="ts('Opens')"></span>
+ </div>
+ <div crm-ui-field="{title: ts('Responding')}">
+ <div>
+ <span crm-mailing-review-bool crm-on="mailing.override_verp=='0'" crm-title="ts('Track Replies')"></span>
+ <span crm-mailing-review-bool crm-on="mailing.override_verp=='0' && mailing.forward_replies=='1'" crm-title="ts('Forward Replies')"></span>
+ </div>
+ <div ng-controller="PreviewComponentCtrl">
+ <span ng-show="mailing.override_verp == '0' && mailing.auto_responder"><a crm-icon="fa-envelope" class="crm-hover-button action-item" ng-click="previewComponent(ts('Auto-Respond'), mailing.reply_id)">{{ts('Auto-Respond')}}</a></span>
+ <span><a crm-icon="fa-envelope" class="crm-hover-button action-item" ng-click="previewComponent(ts('Opt-out'), mailing.optout_id)">{{ts('Opt-out')}}</a></span>
+ <span><a crm-icon="fa-envelope" class="crm-hover-button action-item" ng-click="previewComponent(ts('Resubscribe'), mailing.resubscribe_id)">{{ts('Resubscribe')}}</a></span>
+ <span><a crm-icon="fa-envelope" class="crm-hover-button action-item" ng-click="previewComponent(ts('Unsubscribe'), mailing.unsubscribe_id)">{{ts('Unsubscribe')}}</a></span>
+ </div>
+ </div>
+ <div crm-ui-field="{title: ts('Publication')}">
+ {{mailing.visibility}}
+ </div>
+ </div>
+ </div>
+</div>
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockReview.js b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockReview.js
new file mode 100644
index 00000000..94968e3e
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockReview.js
@@ -0,0 +1,26 @@
+(function(angular, $, _) {
+
+ angular.module('crmMailing').directive('crmMailingBlockReview', function (crmMailingPreviewMgr) {
+ return {
+ scope: {
+ crmMailing: '@',
+ crmMailingAttachments: '@'
+ },
+ templateUrl: '~/crmMailing/BlockReview.html',
+ link: function (scope, elm, attr) {
+ scope.$parent.$watch(attr.crmMailing, function(newValue){
+ scope.mailing = newValue;
+ });
+ scope.$parent.$watch(attr.crmMailingAttachments, function(newValue){
+ scope.attachments = newValue;
+ });
+ scope.crmMailingConst = CRM.crmMailing;
+ scope.ts = CRM.ts(null);
+ scope.previewMailing = function previewMailing(mailing, mode) {
+ return crmMailingPreviewMgr.preview(mailing, mode);
+ };
+ }
+ };
+ });
+
+})(angular, CRM.$, CRM._);
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockSchedule.html b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockSchedule.html
new file mode 100644
index 00000000..75cf19b2
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockSchedule.html
@@ -0,0 +1,13 @@
+<div class="crmMailing-schedule-outer" crm-mailing-radio-date="schedule" ng-model="mailing.scheduled_date">
+ <div class="crmMailing-schedule-inner">
+ <div>
+ <input ng-model="schedule.mode" type="radio" name="send" value="now" id="schedule-send-now"/>
+ <label for="schedule-send-now">{{ts('Send immediately')}}</label>
+ </div>
+ <div>
+ <input ng-model="schedule.mode" type="radio" name="send" value="at" id="schedule-send-at"/>
+ <label for="schedule-send-at">{{ts('Send at:')}}</label>
+ <input crm-ui-datepicker ng-model="schedule.datetime" ng-required="schedule.mode == 'at'"/>
+ </div>
+ </div>
+</div>
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockSchedule.js b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockSchedule.js
new file mode 100644
index 00000000..251449d7
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockSchedule.js
@@ -0,0 +1,5 @@
+(function(angular, $, _) {
+ angular.module('crmMailing').directive('crmMailingBlockSchedule', function(crmMailingSimpleDirective) {
+ return crmMailingSimpleDirective('crmMailingBlockSchedule', '~/crmMailing/BlockSchedule.html');
+ });
+})(angular, CRM.$, CRM._);
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockSummary.html b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockSummary.html
new file mode 100644
index 00000000..6050e448
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockSummary.html
@@ -0,0 +1,29 @@
+<!--
+Controller: EditMailingCtrl
+Required vars: mailing, crmMailingConst
+FIXME: Don't hardcode table-based layout!
+-->
+<div class="crm-block" ng-form="subform" crm-ui-id-scope>
+ <div class="crm-group">
+ <div crm-ui-field="{name: 'subform.mailingName', title: ts('Mailing Name'), help: hs('name')}">
+ <div>
+ <input
+ crm-ui-id="subform.mailingName"
+ type="text"
+ class="crm-form-text"
+ ng-model="mailing.name"
+ placeholder="Mailing Name"
+ required
+ name="mailingName" />
+ </div>
+ </div>
+ <div crm-ui-field="{name: 'subform.campaign', title: ts('Campaign'), help: hs({id: 'id-campaign_id', file: 'CRM/Campaign/Form/addCampaignToComponent'})}" ng-show="crmMailingConst.campaignEnabled">
+ <input
+ crm-entityref="{entity: 'Campaign', select: {allowClear: true, placeholder: ts('Select Campaign')}}"
+ crm-ui-id="subform.campaign"
+ name="campaign"
+ ng-model="mailing.campaign_id"
+ />
+ </div>
+ </div>
+</div>
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockSummary.js b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockSummary.js
new file mode 100644
index 00000000..d16d9fc3
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockSummary.js
@@ -0,0 +1,5 @@
+(function(angular, $, _) {
+ angular.module('crmMailing').directive('crmMailingBlockSummary', function(crmMailingSimpleDirective) {
+ return crmMailingSimpleDirective('crmMailingBlockSummary', '~/crmMailing/BlockSummary.html');
+ });
+})(angular, CRM.$, CRM._);
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockTemplates.html b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockTemplates.html
new file mode 100644
index 00000000..38a37a91
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockTemplates.html
@@ -0,0 +1,9 @@
+<div ng-controller="MsgTemplateCtrl" class="crm-mailing-templates-row">
+ <input
+ type="hidden"
+ crm-mailing-templates
+ ng-model="mailing.msg_template_id"
+ crm-ui-id="{{crmMailingBlockTemplates.id}}"
+ name="{{crmMailingBlockTemplates.name}}" />
+ <a crm-icon="fa-floppy-o" ng-if="checkPerm('edit message templates')" ng-click="saveTemplate(mailing)" class="crm-hover-button" title="{{ts('Save As')}}"></a>
+</div> \ No newline at end of file
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockTemplates.js b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockTemplates.js
new file mode 100644
index 00000000..9ee2efbf
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockTemplates.js
@@ -0,0 +1,5 @@
+(function(angular, $, _) {
+ angular.module('crmMailing').directive('crmMailingBlockTemplates', function(crmMailingSimpleDirective) {
+ return crmMailingSimpleDirective('crmMailingBlockTemplates', '~/crmMailing/BlockTemplates.html');
+ });
+})(angular, CRM.$, CRM._); \ No newline at end of file
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockTracking.html b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockTracking.html
new file mode 100644
index 00000000..a4ed95e0
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockTracking.html
@@ -0,0 +1,14 @@
+<!--
+Controller: EditMailingCtrl
+Required vars: mailing
+-->
+<div class="crm-block" ng-form="subform" crm-ui-id-scope>
+ <div class="crm-group">
+ <div crm-ui-field="{name: 'subform.url_tracking', title: ts('Track Click-Throughs'), help: hs('url_tracking')}" crm-layout="checkbox">
+ <input crm-ui-id="subform.url_tracking" name="url_tracking" type="checkbox" ng-model="mailing.url_tracking" ng-true-value="'1'" ng-false-value="'0'" />
+ </div>
+ <div crm-ui-field="{name: 'subform.open_tracking', title: ts('Track Opens'), help: hs('open_tracking')}" crm-layout="checkbox">
+ <input crm-ui-id="subform.open_tracking" name="open_tracking" type="checkbox" ng-model="mailing.open_tracking" ng-true-value="'1'" ng-false-value="'0'" />
+ </div>
+ </div>
+</div>
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockTracking.js b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockTracking.js
new file mode 100644
index 00000000..b8502b23
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BlockTracking.js
@@ -0,0 +1,5 @@
+(function(angular, $, _) {
+ angular.module('crmMailing').directive('crmMailingBlockTracking', function(crmMailingSimpleDirective) {
+ return crmMailingSimpleDirective('crmMailingBlockTracking', '~/crmMailing/BlockTracking.html');
+ });
+})(angular, CRM.$, CRM._);
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BodyHtml.html b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BodyHtml.html
new file mode 100644
index 00000000..b38c28c7
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BodyHtml.html
@@ -0,0 +1,26 @@
+<!--
+Required vars: mailing
+-->
+<div ng-form="htmlForm" crm-ui-id-scope>
+ <div ng-controller="EmailBodyCtrl">
+ <div style="float: right;">
+ <input crm-mailing-token on-select="$broadcast('insert:body_html', token.name)" tabindex="-1" style="z-index:1">
+ </div>
+
+ <div>
+ <textarea
+ crm-ui-id="htmlForm.body_html"
+ crm-ui-richtext
+ name="body_html"
+ crm-ui-insert-rx="insert:body_html"
+ ng-model="mailing.body_html"
+ ng-blur="checkTokens(mailing, 'body_html', 'insert:body_html')"
+ data-preset="civimail"
+ ></textarea>
+ <span ng-model="body_html_tokens" crm-ui-validate="hasAllTokens(mailing, 'body_html')"></span>
+ <div ng-show="htmlForm.$error.crmUiValidate" class="crmMailing-error-link">
+ {{ts('Required tokens are missing.')}} <a class="helpicon" ng-click="checkTokens(mailing, 'body_html', 'insert:body_html')"></a>
+ </div>
+ </div>
+ </div>
+</div>
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BodyHtml.js b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BodyHtml.js
new file mode 100644
index 00000000..242f530c
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BodyHtml.js
@@ -0,0 +1,5 @@
+(function(angular, $, _) {
+ angular.module('crmMailing').directive('crmMailingBodyHtml', function(crmMailingSimpleDirective) {
+ return crmMailingSimpleDirective('crmMailingBodyHtml', '~/crmMailing/BodyHtml.html');
+ });
+})(angular, CRM.$, CRM._);
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BodyText.html b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BodyText.html
new file mode 100644
index 00000000..598c7792
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BodyText.html
@@ -0,0 +1,24 @@
+<!--
+Required vars: mailing, crmMailingConst
+-->
+<div ng-form="textForm" crm-ui-id-scope>
+ <div ng-controller="EmailBodyCtrl">
+ <div style="float: right;">
+ <input crm-mailing-token on-select="$broadcast('insert:body_text', token.name)" tabindex="-1"/>
+ </div>
+
+ <div>
+ <textarea
+ crm-ui-id="textForm.body_text"
+ crm-ui-insert-rx="insert:body_text"
+ name="body_text"
+ ng-model="mailing.body_text"
+ ng-blur="checkTokens(mailing, 'body_text', 'insert:body_text')"
+ ></textarea>
+ <span ng-model="body_text_tokens" crm-ui-validate="hasAllTokens(mailing, 'body_text')"></span>
+ <div ng-show="textForm.$error.crmUiValidate" class="crmMailing-error-link">
+ {{ts('Required tokens are missing.')}} <a class="helpicon" ng-click="checkTokens(mailing, 'body_text', 'insert:body_text')"></a>
+ </div>
+ </div>
+ </div>
+</div>
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BodyText.js b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BodyText.js
new file mode 100644
index 00000000..0f04491c
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/BodyText.js
@@ -0,0 +1,5 @@
+(function(angular, $, _) {
+ angular.module('crmMailing').directive('crmMailingBodyText', function(crmMailingSimpleDirective) {
+ return crmMailingSimpleDirective('crmMailingBodyText', '~/crmMailing/BodyText.html');
+ });
+})(angular, CRM.$, CRM._);
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/CreateMailingCtrl.js b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/CreateMailingCtrl.js
new file mode 100644
index 00000000..08a61713
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/CreateMailingCtrl.js
@@ -0,0 +1,8 @@
+(function(angular, $, _) {
+
+ angular.module('crmMailing').controller('CreateMailingCtrl', function EditMailingCtrl($scope, selectedMail, $location) {
+ $location.path("/mailing/" + selectedMail.id);
+ $location.replace();
+ });
+
+})(angular, CRM.$, CRM._);
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/EditMailingCtrl.js b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/EditMailingCtrl.js
new file mode 100644
index 00000000..91f6db3f
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/EditMailingCtrl.js
@@ -0,0 +1,133 @@
+(function(angular, $, _) {
+
+ angular.module('crmMailing').controller('EditMailingCtrl', function EditMailingCtrl($scope, selectedMail, $location, crmMailingMgr, crmStatus, attachments, crmMailingPreviewMgr, crmBlocker, CrmAutosaveCtrl, $timeout, crmUiHelp) {
+ var APPROVAL_STATUSES = {'Approved': 1, 'Rejected': 2, 'None': 3};
+
+ $scope.mailing = selectedMail;
+ $scope.attachments = attachments;
+ $scope.crmMailingConst = CRM.crmMailing;
+ $scope.checkPerm = CRM.checkPerm;
+
+ var ts = $scope.ts = CRM.ts(null);
+ $scope.hs = crmUiHelp({file: 'CRM/Mailing/MailingUI'});
+ var block = $scope.block = crmBlocker();
+ var myAutosave = null;
+
+ var templateTypes = _.where(CRM.crmMailing.templateTypes, {name: selectedMail.template_type});
+ if (!templateTypes[0]) throw 'Unrecognized template type: ' + selectedMail.template_type;
+ $scope.mailingEditorUrl = templateTypes[0].editorUrl;
+
+ $scope.isSubmitted = function isSubmitted() {
+ return _.size($scope.mailing.jobs) > 0;
+ };
+
+ // usage: approve('Approved')
+ $scope.approve = function approve(status, options) {
+ $scope.mailing.approval_status_id = APPROVAL_STATUSES[status];
+ return myAutosave.suspend($scope.submit(options));
+ };
+
+ // @return Promise
+ $scope.previewMailing = function previewMailing(mailing, mode) {
+ return crmMailingPreviewMgr.preview(mailing, mode);
+ };
+
+ // @return Promise
+ $scope.sendTest = function sendTest(mailing, attachments, recipient) {
+ var savePromise = crmMailingMgr.save(mailing)
+ .then(function() {
+ return attachments.save();
+ });
+ return block(crmStatus({start: ts('Saving...'), success: ''}, savePromise)
+ .then(function() {
+ crmMailingPreviewMgr.sendTest(mailing, recipient);
+ }));
+ };
+
+ // @return Promise
+ $scope.submit = function submit(options) {
+ options = options || {};
+ if (block.check()) {
+ return;
+ }
+
+ var promise = crmMailingMgr.save($scope.mailing)
+ .then(function() {
+ // pre-condition: the mailing exists *before* saving attachments to it
+ return $scope.attachments.save();
+ })
+ .then(function() {
+ return crmMailingMgr.submit($scope.mailing);
+ })
+ .then(function() {
+ if (!options.stay) {
+ $scope.leave('scheduled');
+ }
+ })
+ ;
+ return block(crmStatus({start: ts('Submitting...'), success: ts('Submitted')}, promise));
+ };
+
+ // @return Promise
+ $scope.save = function save() {
+ return block(crmStatus(null,
+ crmMailingMgr
+ .save($scope.mailing)
+ .then(function() {
+ // pre-condition: the mailing exists *before* saving attachments to it
+ return $scope.attachments.save();
+ })
+ ));
+ };
+
+ // @return Promise
+ $scope.delete = function cancel() {
+ return block(crmStatus({start: ts('Deleting...'), success: ts('Deleted')},
+ crmMailingMgr.delete($scope.mailing)
+ .then(function() {
+ $scope.leave('unscheduled');
+ })
+ ));
+ };
+
+ // @param string listingScreen 'archive', 'scheduled', 'unscheduled'
+ $scope.leave = function leave(listingScreen) {
+ switch (listingScreen) {
+ case 'archive':
+ window.location = CRM.url('civicrm/mailing/browse/archived', {
+ reset: 1
+ });
+ break;
+ case 'scheduled':
+ window.location = CRM.url('civicrm/mailing/browse/scheduled', {
+ reset: 1,
+ scheduled: 'true'
+ });
+ break;
+ case 'unscheduled':
+ /* falls through */
+ default:
+ window.location = CRM.url('civicrm/mailing/browse/unscheduled', {
+ reset: 1,
+ scheduled: 'false'
+ });
+ }
+ };
+
+ myAutosave = new CrmAutosaveCtrl({
+ save: $scope.save,
+ saveIf: function() {
+ return true;
+ },
+ model: function() {
+ return [$scope.mailing, $scope.attachments.getAutosaveSignature()];
+ },
+ form: function() {
+ return $scope.crmMailing;
+ }
+ });
+ $timeout(myAutosave.start);
+ $scope.$on('$destroy', myAutosave.stop);
+ });
+
+})(angular, CRM.$, CRM._);
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/EditMailingCtrl/2step.html b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/EditMailingCtrl/2step.html
new file mode 100644
index 00000000..cabd6f30
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/EditMailingCtrl/2step.html
@@ -0,0 +1,63 @@
+<div ng-form="crmMailingSubform">
+ <div class="crm-block crm-form-block crmMailing">
+ <div crm-ui-wizard>
+ <div crm-ui-wizard-step crm-title="ts('Define Mailing')" ng-form="defineForm">
+ <div crm-ui-tab-set>
+ <div crm-ui-tab id="tab-mailing" crm-title="ts('Mailing')">
+ <div crm-mailing-block-summary crm-mailing="mailing"></div>
+ <div crm-mailing-block-mailing crm-mailing="mailing"></div>
+ <div crm-ui-accordion="{title: ts('HTML'), help: hs('html')}">
+ <div crm-mailing-body-html crm-mailing="mailing"></div>
+ </div>
+ <div crm-ui-accordion="{title: ts('Plain Text'), collapsed: !mailing.body_text, help: hs('text')}">
+ <div crm-mailing-body-text crm-mailing="mailing"></div>
+ </div>
+ <span ng-model="placeholder" crm-ui-validate="mailing.body_html || mailing.body_text"></span>
+ </div>
+ <div crm-ui-tab id="tab-attachment" crm-title="ts('Attachments')">
+ <div crm-attachments="attachments"></div>
+ </div>
+ <div crm-ui-tab id="tab-header" crm-title="ts('Header and Footer')">
+ <div crm-mailing-block-header-footer crm-mailing="mailing"></div>
+ </div>
+ <div crm-ui-tab id="tab-pub" crm-title="ts('Publication')">
+ <div crm-mailing-block-publication crm-mailing="mailing"></div>
+ </div>
+ <div crm-ui-tab id="tab-response" crm-title="ts('Responses')">
+ <div crm-mailing-block-responses crm-mailing="mailing"></div>
+ </div>
+ <div crm-ui-tab id="tab-tracking" crm-title="ts('Tracking')">
+ <div crm-mailing-block-tracking crm-mailing="mailing"></div>
+ </div>
+ </div>
+ <div crm-ui-accordion="{title: ts('Preview')}">
+ <div crm-mailing-block-preview crm-mailing="mailing" on-preview="previewMailing(mailing, preview.mode)" on-send="sendTest(mailing, attachments, preview.recipient)"></div>
+ </div>
+ </div>
+ <div crm-ui-wizard-step crm-title="ts('Review and Schedule')" ng-form="reviewForm">
+ <div crm-ui-accordion="{title: ts('Review')}">
+ <div crm-mailing-block-review crm-mailing="mailing" crm-mailing-attachments="attachments"></div>
+ </div>
+ <div crm-ui-accordion="{title: ts('Schedule')}">
+ <div crm-mailing-block-schedule crm-mailing="mailing"></div>
+ </div>
+ <center>
+ <a class="button crmMailing-submit-button crmMailing-btn-primary" ng-click="submit()" ng-class="{blocking: block.check(), disabled: crmMailingSubform.$invalid}">
+ <div>{{ts('Submit Mailing')}}</div>
+ </a>
+ </center>
+ </div>
+
+ <span crm-ui-wizard-buttons style="float:right;">
+ <button
+ crm-icon="fa-trash"
+ ng-show="checkPerm('delete in CiviMail')"
+ class="crmMailing-btn-danger-outline"
+ ng-disabled="block.check()"
+ crm-confirm="{title:ts('Delete Draft'), message:ts('Are you sure you want to permanently delete this mailing?')}"
+ on-yes="delete()">{{ts('Delete Draft')}}</button>
+ <button crm-icon="fa-floppy-o" ng-disabled="block.check()" ng-click="save().then(leave)" class="crmMailing-btn-secondary-outline">{{ts('Save Draft')}}</button>
+ </span>
+ </div>
+ </div>
+</div>
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/EditMailingCtrl/base.html b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/EditMailingCtrl/base.html
new file mode 100644
index 00000000..92a97b24
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/EditMailingCtrl/base.html
@@ -0,0 +1,8 @@
+<div crm-ui-debug="mailing"></div>
+
+<div ng-show="isSubmitted()">
+ {{ts('This mailing has been submitted.')}}
+</div>
+
+<form name="crmMailing" novalidate ng-hide="isSubmitted()" ng-include="mailingEditorUrl">
+</form> \ No newline at end of file
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/EditMailingCtrl/unified.html b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/EditMailingCtrl/unified.html
new file mode 100644
index 00000000..cc3056fa
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/EditMailingCtrl/unified.html
@@ -0,0 +1,50 @@
+<div ng-form="crmMailingSubform">
+ <div class="crm-block crm-form-block crmMailing">
+
+ <div crm-mailing-block-summary crm-mailing="mailing"></div>
+ <div crm-mailing-block-mailing crm-mailing="mailing"></div>
+
+ <div crm-ui-tab-set>
+ <div crm-ui-tab id="tab-html" crm-title="ts('HTML')">
+ <div crm-mailing-body-html crm-mailing="mailing"></div>
+ </div>
+ <div crm-ui-tab id="tab-text" crm-title="ts('Plain Text')">
+ <div crm-mailing-body-text crm-mailing="mailing"></div>
+ </div>
+ <span ng-model="placeholder" crm-ui-validate="mailing.body_html || mailing.body_text"></span>
+ <div crm-ui-tab id="tab-attachment" crm-title="ts('Attachments')">
+ <div crm-attachments="attachments"></div>
+ </div>
+ <div crm-ui-tab id="tab-header" crm-title="ts('Header and Footer')">
+ <div crm-mailing-block-header-footer crm-mailing="mailing"></div>
+ </div>
+ <div crm-ui-tab id="tab-pub" crm-title="ts('Publication')">
+ <div crm-mailing-block-publication crm-mailing="mailing"></div>
+ </div>
+ <div crm-ui-tab id="tab-response" crm-title="ts('Responses')">
+ <div crm-mailing-block-responses crm-mailing="mailing"></div>
+ </div>
+ <div crm-ui-tab id="tab-tracking" crm-title="ts('Tracking')">
+ <div crm-mailing-block-tracking crm-mailing="mailing"></div>
+ </div>
+ </div>
+
+ <div crm-ui-accordion="{title: ts('Preview')}">
+ <div crm-mailing-block-preview crm-mailing="mailing" on-preview="previewMailing(mailing, preview.mode)" on-send="sendTest(mailing, attachments, preview.recipient)"></div>
+ </div>
+
+ <div crm-ui-accordion="{title: ts('Schedule')}">
+ <div crm-mailing-block-schedule crm-mailing="mailing"></div>
+ </div>
+
+ <button crm-icon="fa-paper-plane" class="crmMailing-btn-primary" ng-disabled="block.check() || crmMailingSubform.$invalid" ng-click="submit()">{{ts('Submit Mailing')}}</button>
+ <button crm-icon="fa-floppy-o" class="crmMailing-btn-secondary-outline" ng-disabled="block.check()" ng-click="save().then(leave)">{{ts('Save Draft')}}</button>
+ <button
+ crm-icon="fa-trash"
+ ng-show="checkPerm('delete in CiviMail')"
+ class="crmMailing-btn-danger-outline"
+ ng-disabled="block.check()"
+ crm-confirm="{title:ts('Delete Draft'), message:ts('Are you sure you want to permanently delete this mailing?')}"
+ on-yes="delete()">{{ts('Delete Draft')}}</button>
+ </div>
+</div>
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/EditMailingCtrl/unified2.html b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/EditMailingCtrl/unified2.html
new file mode 100644
index 00000000..1506b8d5
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/EditMailingCtrl/unified2.html
@@ -0,0 +1,46 @@
+<div ng-form="crmMailingSubform">
+ <div class="crm-block crm-form-block crmMailing">
+
+ <div crm-mailing-block-summary crm-mailing="mailing"></div>
+ <div crm-mailing-block-mailing crm-mailing="mailing"></div>
+
+ <div crm-ui-accordion="{title: ts('HTML'), help: hs('html')}">
+ <div crm-mailing-body-html crm-mailing="mailing"></div>
+ </div>
+ <div crm-ui-accordion="{title: ts('Plain Text'), collapsed: !mailing.body_text, help: hs('text')}">
+ <div crm-mailing-body-text crm-mailing="mailing"></div>
+ </div>
+ <span ng-model="placeholder" crm-ui-validate="mailing.body_html || mailing.body_text"></span>
+ <div crm-ui-accordion="{title: ts('Header and Footer'), collapsed: true}" id="tab-header">
+ <div crm-mailing-block-header-footer crm-mailing="mailing"></div>
+ </div>
+ <div crm-ui-accordion="{title: ts('Attachments'), collapsed: true}" id="tab-attachment">
+ <div crm-attachments="attachments"></div>
+ </div>
+ <div crm-ui-accordion="{title: ts('Publication'), collapsed: true}" id="tab-pub">
+ <div crm-mailing-block-publication crm-mailing="mailing"></div>
+ </div>
+ <div crm-ui-accordion="{title: ts('Responses'), collapsed: true}" id="tab-response">
+ <div crm-mailing-block-responses crm-mailing="mailing"></div>
+ </div>
+ <div crm-ui-accordion="{title: ts('Tracking'), collapsed: true}" id="tab-tracking">
+ <div crm-mailing-block-tracking crm-mailing="mailing"></div>
+ </div>
+ <div crm-ui-accordion="{title: ts('Preview')}">
+ <div crm-mailing-block-preview crm-mailing="mailing" on-preview="previewMailing(mailing, preview.mode)" on-send="sendTest(mailing, attachments, preview.recipient)"></div>
+ </div>
+ <div crm-ui-accordion="{title: ts('Schedule')}" id="tab-schedule">
+ <div crm-mailing-block-schedule crm-mailing="mailing"></div>
+ </div>
+
+ <button crm-icon="fa-paper-plane" class="crmMailing-btn-primary" ng-disabled="block.check() || crmMailingSubform.$invalid" ng-click="submit()">{{ts('Submit Mailing')}}</button>
+ <button crm-icon="fa-floppy-o" class="crmMailing-secondary-outline" ng-disabled="block.check()" ng-click="save().then(leave)">{{ts('Save Draft')}}</button>
+ <button
+ crm-icon="fa-trash"
+ ng-show="checkPerm('delete in CiviMail')"
+ class="crmMailing-btn-danger-outline"
+ ng-disabled="block.check()"
+ crm-confirm="{title:ts('Delete Draft'), message:ts('Are you sure you want to permanently delete this mailing?')}"
+ on-yes="delete()">{{ts('Delete Draft')}}</button>
+ </div>
+</div>
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/EditMailingCtrl/wizard.html b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/EditMailingCtrl/wizard.html
new file mode 100644
index 00000000..9854cc5c
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/EditMailingCtrl/wizard.html
@@ -0,0 +1,66 @@
+<div ng-form="crmMailingSubform">
+ <div class="crm-block crm-form-block crmMailing">
+
+ <div crm-ui-wizard>
+
+ <div crm-ui-wizard-step crm-title="ts('Content')" ng-form="contentForm">
+ <div crm-mailing-block-summary crm-mailing="mailing"></div>
+ <div crm-mailing-block-mailing crm-mailing="mailing"></div>
+ <div crm-ui-accordion="{title: ts('HTML'), help: hs('html')}">
+ <div crm-mailing-body-html crm-mailing="mailing"></div>
+ </div>
+ <div crm-ui-accordion="{title: ts('Plain Text'), collapsed: !mailing.body_text, help: hs('text')}">
+ <div crm-mailing-body-text crm-mailing="mailing"></div>
+ </div>
+ <span ng-model="placeholder" crm-ui-validate="mailing.body_html || mailing.body_text"></span>
+ <div crm-ui-accordion="{title: ts('Header and Footer'), collapsed: true}">
+ <div crm-mailing-block-header-footer crm-mailing="mailing"></div>
+ </div>
+ <div crm-ui-accordion="{title: ts('Attachments'), collapsed: true}">
+ <div crm-attachments="attachments"></div>
+ </div>
+ <div crm-ui-accordion="{title: ts('Preview')}">
+ <div crm-mailing-block-preview crm-mailing="mailing" on-preview="previewMailing(mailing, preview.mode)" on-send="sendTest(mailing, attachments, preview.recipient)"></div>
+ </div>
+ </div>
+
+ <div crm-ui-wizard-step crm-title="ts('Options')" ng-form="optionsForm">
+ <div crm-ui-accordion="{title: ts('Schedule')}">
+ <div crm-mailing-block-schedule crm-mailing="mailing"></div>
+ </div>
+
+ <div crm-ui-accordion="{title: ts('Responses'), collapsed: true}">
+ <div crm-mailing-block-responses crm-mailing="mailing"></div>
+ </div>
+ <div crm-ui-accordion="{title: ts('Tracking'), collapsed: true}">
+ <div crm-mailing-block-tracking crm-mailing="mailing"></div>
+ </div>
+ <div crm-ui-accordion="{title: ts('Publication'), collapsed: true}">
+ <div crm-mailing-block-publication crm-mailing="mailing"></div>
+ </div>
+ </div>
+
+ <div crm-ui-wizard-step crm-title="ts('Review')" ng-form="reviewForm">
+ <div crm-ui-accordion="{title: ts('Review')}">
+ <div crm-mailing-block-review crm-mailing="mailing" crm-mailing-attachments="attachments"></div>
+ </div>
+ <center>
+ <a class="button crmMailing-submit-button crmMailing-btn-primary" ng-click="submit()" ng-class="{blocking: block.check(), disabled: crmMailingSubform.$invalid}">
+ <div>{{ts('Submit Mailing')}}</div>
+ </a>
+ </center>
+ </div>
+
+ <span crm-ui-wizard-buttons style="float:right;">
+ <button
+ crm-icon="fa-trash"
+ ng-show="checkPerm('delete in CiviMail')"
+ class="crmMailing-btn-danger-outline"
+ ng-disabled="block.check()"
+ crm-confirm="{title:ts('Delete Draft'), message:ts('Are you sure you want to permanently delete this mailing?')}"
+ on-yes="delete()">{{ts('Delete Draft')}}</button>
+ <button crm-icon="fa-floppy-o" class="crmMailing-btn-secondary-outline" ng-disabled="block.check()" ng-click="save().then(leave)">{{ts('Save Draft')}}</button>
+ </span>
+ </div>
+ </div>
+</div>
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/EditMailingCtrl/workflow.html b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/EditMailingCtrl/workflow.html
new file mode 100644
index 00000000..affa76d8
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/EditMailingCtrl/workflow.html
@@ -0,0 +1,73 @@
+<div ng-form="crmMailingSubform">
+ <div class="crm-block crm-form-block crmMailing">
+
+ <div crm-ui-wizard>
+
+ <div crm-ui-wizard-step="10" crm-title="ts('Content')" ng-form="contentForm" ng-if="checkPerm('create mailings') || checkPerm('access CiviMail')">
+ <div crm-mailing-block-summary crm-mailing="mailing"></div>
+ <div crm-mailing-block-mailing crm-mailing="mailing"></div>
+ <div crm-ui-accordion="{title: ts('HTML'), help: hs('html')}">
+ <div crm-mailing-body-html crm-mailing="mailing"></div>
+ </div>
+ <div crm-ui-accordion="{title: ts('Plain Text'), collapsed: !mailing.body_text, help: hs('text')}">
+ <div crm-mailing-body-text crm-mailing="mailing"></div>
+ </div>
+ <span ng-model="placeholder" crm-ui-validate="mailing.body_html || mailing.body_text"></span>
+ <div crm-ui-accordion="{title: ts('Header and Footer'), collapsed: true}">
+ <div crm-mailing-block-header-footer crm-mailing="mailing"></div>
+ </div>
+ <div crm-ui-accordion="{title: ts('Attachments'), collapsed: true}">
+ <div crm-attachments="attachments"></div>
+ </div>
+ <div crm-ui-accordion="{title: ts('Preview')}">
+ <div crm-mailing-block-preview crm-mailing="mailing" on-preview="previewMailing(mailing, preview.mode)" on-send="sendTest(mailing, attachments, preview.recipient)"></div>
+ </div>
+ </div>
+
+ <div crm-ui-wizard-step="20" crm-title="ts('Options')" ng-form="optionsForm" ng-if="checkPerm('create mailings') || checkPerm('access CiviMail')">
+ <div crm-ui-accordion="{title: ts('Responses'), collapsed: true}">
+ <div crm-mailing-block-responses crm-mailing="mailing"></div>
+ </div>
+ <div crm-ui-accordion="{title: ts('Tracking'), collapsed: true}">
+ <div crm-mailing-block-tracking crm-mailing="mailing"></div>
+ </div>
+ <div crm-ui-accordion="{title: ts('Publication'), collapsed: true}">
+ <div crm-mailing-block-publication crm-mailing="mailing"></div>
+ </div>
+ </div>
+
+ <div crm-ui-wizard-step="40" crm-title="ts('Review')" ng-form="schedForm" ng-if="checkPerm('schedule mailings') || checkPerm('access CiviMail')">
+ <div crm-ui-accordion="{title: ts('Review')}">
+ <div crm-mailing-block-review crm-mailing="mailing" crm-mailing-attachments="attachments"></div>
+ </div>
+ <div crm-ui-accordion="{title: ts('Schedule')}">
+ <div crm-mailing-block-schedule crm-mailing="mailing"></div>
+ </div>
+ <div crm-ui-accordion="{title: ts('Approval')}" ng-if="checkPerm('approve mailings') || checkPerm('access CiviMail')">
+ <div crm-mailing-block-approve crm-mailing="mailing"></div>
+ </div>
+ <center ng-if="!checkPerm('approve mailings') && !checkPerm('access CiviMail')">
+ <a class="button crmMailing-submit-button crmMailing-btn-primary" ng-click="submit()" ng-class="{blocking: block.check(), disabled: crmMailingSubform.$invalid}">
+ <div>{{ts('Submit Mailing')}}</div>
+ </a>
+ </center>
+ <center ng-if="checkPerm('approve mailings') || checkPerm('access CiviMail')">
+ <a class="button crmMailing-submit-button crmMailing-btn-primary" ng-click="approve('Approved')" ng-class="{blocking: block.check(), disabled: crmMailingSubform.$invalid}">
+ <div>{{ts('Submit and Approve Mailing')}}</div>
+ </a>
+ </center>
+ </div>
+
+ <span crm-ui-wizard-buttons style="float:right;">
+ <button
+ crm-icon="fa-trash"
+ ng-show="checkPerm('delete in CiviMail')"
+ class="crmMailing-btn-danger-outline"
+ ng-disabled="block.check()"
+ crm-confirm="{title:ts('Delete Draft'), message:ts('Are you sure you want to permanently delete this mailing?')}"
+ on-yes="delete()">{{ts('Delete Draft')}}</button>
+ <button crm-icon="fa-floppy-o" ng-disabled="block.check()" ng-click="save().then(leave)" class="crmMailing-btn-secondary-outline">{{ts('Save Draft')}}</button>
+ </span>
+ </div>
+ </div>
+</div>
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/EditRecipCtrl.js b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/EditRecipCtrl.js
new file mode 100644
index 00000000..9fca637a
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/EditRecipCtrl.js
@@ -0,0 +1,136 @@
+(function(angular, $, _) {
+
+ // Controller for the edit-recipients fields (
+ // WISHLIST: Move most of this to a (cache-enabled) service
+ // Scope members:
+ // - [input] mailing: object
+ // - [output] recipients: array of recipient records
+ angular.module('crmMailing').controller('EditRecipCtrl', function EditRecipCtrl($scope, dialogService, crmApi, crmMailingMgr, $q, crmMetadata, crmStatus, crmMailingCache) {
+ // Time to wait before triggering AJAX update to recipients list
+ var RECIPIENTS_DEBOUNCE_MS = 100;
+ var SETTING_DEBOUNCE_MS = 5000;
+ var RECIPIENTS_PREVIEW_LIMIT = 50;
+
+ var ts = $scope.ts = CRM.ts(null);
+
+ $scope.isMailingList = function isMailingList(group) {
+ var GROUP_TYPE_MAILING_LIST = '2';
+ return _.contains(group.group_type, GROUP_TYPE_MAILING_LIST);
+ };
+
+ $scope.recipients = null;
+ $scope.outdated = null;
+ $scope.permitRecipientRebuild = null;
+
+ $scope.getRecipientsEstimate = function() {
+ var ts = $scope.ts;
+ if ($scope.recipients === null) {
+ return ts('Estimating...');
+ }
+ if ($scope.recipients === 0) {
+ return ts('Estimate recipient count');
+ }
+ return ts('Refresh recipient count');
+ };
+
+ $scope.getRecipientCount = function() {
+ var ts = $scope.ts;
+ if ($scope.recipients === 0) {
+ return ts('No Recipients');
+ }
+ else if ($scope.recipients > 0) {
+ return ts('~%1 recipients', {1 : $scope.recipients});
+ }
+ else if ($scope.outdated) {
+ return ts('(unknown)');
+ }
+ else {
+ return $scope.permitRecipientRebuild ? ts('(unknown)') : ts('Estimating...');
+ }
+ };
+
+ // We monitor four fields -- use debounce so that changes across the
+ // four fields can settle-down before AJAX.
+ var refreshRecipients = _.debounce(function() {
+ $scope.$apply(function() {
+ if (!$scope.mailing) {
+ return;
+ }
+ crmMailingMgr.previewRecipientCount($scope.mailing, crmMailingCache, !$scope.permitRecipientRebuild).then(function(recipients) {
+ $scope.outdated = ($scope.permitRecipientRebuild && _.difference($scope.mailing.recipients, crmMailingCache.get('mailing-' + $scope.mailing.id + '-recipient-params')) !== 0);
+ $scope.recipients = recipients;
+ });
+ });
+ }, RECIPIENTS_DEBOUNCE_MS);
+ $scope.$watchCollection("mailing.dedupe_email", refreshRecipients);
+ $scope.$watchCollection("mailing.location_type_id", refreshRecipients);
+ $scope.$watchCollection("mailing.email_selection_method", refreshRecipients);
+ $scope.$watchCollection("mailing.recipients.groups.include", refreshRecipients);
+ $scope.$watchCollection("mailing.recipients.groups.exclude", refreshRecipients);
+ $scope.$watchCollection("mailing.recipients.mailings.include", refreshRecipients);
+ $scope.$watchCollection("mailing.recipients.mailings.exclude", refreshRecipients);
+
+ // refresh setting at a duration on 5sec
+ var refreshSetting = _.debounce(function() {
+ $scope.$apply(function() {
+ crmApi('Setting', 'getvalue', {"name": 'auto_recipient_rebuild', "return": "value"}).then(function(response) {
+ $scope.permitRecipientRebuild = (response.result === 0);
+ });
+ });
+ }, SETTING_DEBOUNCE_MS);
+ $scope.$watchCollection("permitRecipientRebuild", refreshSetting);
+
+ $scope.previewRecipients = function previewRecipients() {
+ var model = {
+ count: $scope.recipients,
+ sample: crmMailingCache.get('mailing-' + $scope.mailing.id + '-recipient-list'),
+ sampleLimit: RECIPIENTS_PREVIEW_LIMIT
+ };
+ var options = CRM.utils.adjustDialogDefaults({
+ width: '40%',
+ autoOpen: false,
+ title: ts('Preview (%1)', {1: $scope.getRecipientCount()})
+ });
+
+ // don't open preview dialog if there is no recipient to show.
+ if ($scope.recipients !== 0 && !$scope.outdated) {
+ if (!_.isEmpty(model.sample)) {
+ dialogService.open('recipDialog', '~/crmMailing/PreviewRecipCtrl.html', model, options);
+ }
+ else {
+ return crmStatus({start: ts('Previewing...'), success: ''}, crmMailingMgr.previewRecipients($scope.mailing, RECIPIENTS_PREVIEW_LIMIT).then(function(recipients) {
+ model.sample = recipients;
+ dialogService.open('recipDialog', '~/crmMailing/PreviewRecipCtrl.html', model, options);
+ }));
+ }
+ }
+ };
+
+ $scope.rebuildRecipients = function rebuildRecipients() {
+ // setting null will put 'Estimating..' text on refresh button
+ $scope.recipients = null;
+ return crmMailingMgr.previewRecipientCount($scope.mailing, crmMailingCache, true).then(function(recipients) {
+ $scope.outdated = (recipients === 0) ? true : false;
+ $scope.recipients = recipients;
+ });
+ };
+
+ // Open a dialog for editing the advanced recipient options.
+ $scope.editOptions = function editOptions(mailing) {
+ var options = CRM.utils.adjustDialogDefaults({
+ autoOpen: false,
+ width: '40%',
+ height: 'auto',
+ title: ts('Edit Options')
+ });
+ $q.when(crmMetadata.getFields('Mailing')).then(function(fields) {
+ var model = {
+ fields: fields,
+ mailing: mailing
+ };
+ dialogService.open('previewComponentDialog', '~/crmMailing/EditRecipOptionsDialogCtrl.html', model, options);
+ });
+ };
+ });
+
+})(angular, CRM.$, CRM._);
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/EditRecipOptionsDialogCtrl.html b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/EditRecipOptionsDialogCtrl.html
new file mode 100644
index 00000000..be5e2c2e
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/EditRecipOptionsDialogCtrl.html
@@ -0,0 +1,41 @@
+<div ng-controller="EditRecipOptionsDialogCtrl" class="crmMailing">
+ <div class="crm-block" ng-form="editRecipOptionsForm" crm-ui-id-scope>
+ <div class="crm-group">
+
+ <div crm-ui-field="{title: ts('Dedupe by email'), help: hs('dedupe_email')}" crm-layout="checkbox">
+ <input
+ type="checkbox"
+ ng-model="model.mailing.dedupe_email"
+ ng-true-value="'1'"
+ ng-false-value="'0'"
+ >
+ </div>
+
+ <div crm-ui-field="{name: 'editRecipOptionsForm.location_type_id', title: ts('Location Type')}">
+ <select
+ crm-ui-id="editRecipOptionsForm.location_type_id"
+ crm-ui-select="{dropdownAutoWidth : true}"
+ name="location_type_id"
+ ng-model="model.mailing.location_type_id"
+ >
+ <option value="">{{ts('Automatic')}}</option>
+ <option ng-repeat="locType in model.fields.location_type_id.options"
+ ng-value="locType.key">{{locType.value}}</option>
+ </select>
+ </div>
+
+ <div crm-ui-field="{name: 'editRecipOptionsForm.email_selection_method', title: ts('Selection Method')}">
+ <select
+ crm-ui-id="editRecipOptionsForm.email_selection_method"
+ crm-ui-select=""
+ name="email_selection_method"
+ ng-model="model.mailing.email_selection_method"
+ >
+ <option ng-repeat="selMet in model.fields.email_selection_method.options"
+ ng-value="selMet.key">{{selMet.value}}</option>
+ </select>
+ </div>
+
+ </div>
+ </div>
+</div>
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/EditRecipOptionsDialogCtrl.js b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/EditRecipOptionsDialogCtrl.js
new file mode 100644
index 00000000..43734e34
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/EditRecipOptionsDialogCtrl.js
@@ -0,0 +1,12 @@
+(function(angular, $, _) {
+
+ // Controller for the "Recipients: Edit Options" dialog
+ // Note: Expects $scope.model to be an object with properties:
+ // - "mailing" (APIv3 mailing object)
+ // - "fields" (list of fields)
+ angular.module('crmMailing').controller('EditRecipOptionsDialogCtrl', function EditRecipOptionsDialogCtrl($scope, crmUiHelp) {
+ $scope.ts = CRM.ts(null);
+ $scope.hs = crmUiHelp({file: 'CRM/Mailing/MailingUI'});
+ });
+
+})(angular, CRM.$, CRM._);
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/EditUnsubGroupCtrl.js b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/EditUnsubGroupCtrl.js
new file mode 100644
index 00000000..56070e03
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/EditUnsubGroupCtrl.js
@@ -0,0 +1,19 @@
+(function(angular, $, _) {
+
+ angular.module('crmMailing').controller('EditUnsubGroupCtrl', function EditUnsubGroupCtrl($scope) {
+ // CRM.crmMailing.groupNames is a global constant - since it doesn't change, we can digest & cache.
+ var mandatoryIds = [];
+
+ $scope.isUnsubGroupRequired = function isUnsubGroupRequired(mailing) {
+ if (!_.isEmpty(CRM.crmMailing.groupNames)) {
+ _.each(CRM.crmMailing.groupNames, function(grp) {
+ if (grp.is_hidden == "1") {
+ mandatoryIds.push(parseInt(grp.id));
+ }
+ });
+ return _.intersection(mandatoryIds, mailing.recipients.groups.include).length > 0;
+ }
+ };
+ });
+
+})(angular, CRM.$, CRM._);
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/EmailAddrCtrl.js b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/EmailAddrCtrl.js
new file mode 100644
index 00000000..942bcfe1
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/EmailAddrCtrl.js
@@ -0,0 +1,31 @@
+(function(angular, $, _) {
+
+ angular.module('crmMailing').controller('EmailAddrCtrl', function EmailAddrCtrl($scope, crmFromAddresses, crmUiAlert) {
+ var ts = CRM.ts(null);
+
+ function changeAlert(winnerField, loserField) {
+ crmUiAlert({
+ title: ts('Conflict'),
+ text: ts('The "%1" option conflicts with the "%2" option. The "%2" option has been disabled.', {
+ 1: winnerField,
+ 2: loserField
+ })
+ });
+ }
+
+ $scope.crmFromAddresses = crmFromAddresses;
+ $scope.checkReplyToChange = function checkReplyToChange(mailing) {
+ if (!_.isEmpty(mailing.replyto_email) && mailing.override_verp == '0') {
+ mailing.override_verp = '1';
+ changeAlert(ts('Reply-To'), ts('Track Replies'));
+ }
+ };
+ $scope.checkVerpChange = function checkVerpChange(mailing) {
+ if (!_.isEmpty(mailing.replyto_email) && mailing.override_verp == '0') {
+ mailing.replyto_email = '';
+ changeAlert(ts('Track Replies'), ts('Reply-To'));
+ }
+ };
+ });
+
+})(angular, CRM.$, CRM._);
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/EmailBodyCtrl.js b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/EmailBodyCtrl.js
new file mode 100644
index 00000000..7db0b7ec
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/EmailBodyCtrl.js
@@ -0,0 +1,52 @@
+(function(angular, $, _) {
+
+ var lastEmailTokenAlert = null;
+ angular.module('crmMailing').controller('EmailBodyCtrl', function EmailBodyCtrl($scope, crmMailingMgr, crmUiAlert, $timeout) {
+ var ts = CRM.ts(null);
+
+ // ex: if (!hasAllTokens(myMailing, 'body_text)) alert('Oh noes!');
+ $scope.hasAllTokens = function hasAllTokens(mailing, field) {
+ return _.isEmpty(crmMailingMgr.findMissingTokens(mailing, field));
+ };
+
+ // ex: checkTokens(myMailing, 'body_text', 'insert:body_text')
+ // ex: checkTokens(myMailing, '*')
+ $scope.checkTokens = function checkTokens(mailing, field, insertEvent) {
+ if (lastEmailTokenAlert) {
+ lastEmailTokenAlert.close();
+ }
+ var missing, insertable;
+ if (field == '*') {
+ insertable = false;
+ missing = angular.extend({},
+ crmMailingMgr.findMissingTokens(mailing, 'body_html'),
+ crmMailingMgr.findMissingTokens(mailing, 'body_text')
+ );
+ }
+ else {
+ insertable = !_.isEmpty(insertEvent);
+ missing = crmMailingMgr.findMissingTokens(mailing, field);
+ }
+ if (!_.isEmpty(missing)) {
+ lastEmailTokenAlert = crmUiAlert({
+ type: 'error',
+ title: ts('Required tokens'),
+ templateUrl: '~/crmMailing/EmailBodyCtrl/tokenAlert.html',
+ scope: angular.extend($scope.$new(), {
+ insertable: insertable,
+ insertToken: function(token) {
+ $timeout(function() {
+ $scope.$broadcast(insertEvent, '{' + token + '}');
+ $timeout(function() {
+ checkTokens(mailing, field, insertEvent);
+ });
+ });
+ },
+ missing: missing
+ })
+ });
+ }
+ };
+ });
+
+})(angular, CRM.$, CRM._);
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/EmailBodyCtrl/tokenAlert.html b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/EmailBodyCtrl/tokenAlert.html
new file mode 100644
index 00000000..46f3eac7
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/EmailBodyCtrl/tokenAlert.html
@@ -0,0 +1,76 @@
+<p ng-show="missing['domain.address']">
+ {{ts('The mailing must include the street address of the organization. Please insert the %1 token.', {1:
+ '{domain.address}'})}}
+</p>
+
+<div ng-show="missing['domain.address'] && insertable">
+ <a ng-click="insertToken('domain.address')" class="button"><span><i class="crm-i fa-plus-circle"></i> {{ts('Address')}}</span></a>
+
+ <div class="clear"></div>
+</div>
+
+<p ng-show="missing['action.optOut']">
+ {{ts('The mailing must allow recipients to (a) unsubscribe from the mailing-list or (b) completely opt-out from all mailings. Please insert an unsubscribe or opt-out token.')}}
+</p>
+
+<div ng-show="missing['action.optOut'] && insertable">
+ <table>
+ <thead>
+ <tr>
+ <th>{{ts('Via Web')}}</th>
+ <th>{{ts('Via Email')}}</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td>
+ <a ng-click="insertToken('action.unsubscribeUrl')" class="button"><span><i class="crm-i fa-plus-circle"></i> {{ts('Unsubscribe')}}</span></a>
+ </td>
+ <td>
+ <a ng-click="insertToken('action.unsubscribe')" class="button"><span><i class="crm-i fa-plus-circle"></i> {{ts('Unsubscribe')}}</span></a>
+ </td>
+ </tr>
+ <tr>
+ <td>
+ <a ng-click="insertToken('action.optOutUrl')" class="button"><span><i class="crm-i fa-plus-circle"></i> {{ts('Opt-out')}}</span></a>
+ </td>
+ <td>
+ <a ng-click="insertToken('action.optOut')" class="button"><span><i class="crm-i fa-plus-circle"></i> {{ts('Opt-out')}}</span></a>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+</div>
+
+<div ng-show="missing['action.optOut'] && !insertable">
+ <table>
+ <thead>
+ <tr>
+ <th>{{ts('Via Web')}}</th>
+ <th>{{ts('Via Email')}}</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td>
+ {action.optOutUrl}
+ </td>
+ <td>
+ {action.optOut}
+ </td>
+ </tr>
+ <tr>
+ <td>
+ {action.unsubscribeUrl}
+ </td>
+ <td>
+ {action.unsubscribe}
+ </td>
+ </tr>
+ </tbody>
+ </table>
+</div>
+
+<p>
+ {{ts('Alternatively, you may select a header or footer which includes the required tokens.')}}
+</p>
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/FromAddress.js b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/FromAddress.js
new file mode 100644
index 00000000..aae6499f
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/FromAddress.js
@@ -0,0 +1,30 @@
+(function(angular, $, _) {
+ // Convert between a mailing "From Address" (mailing.from_name,mailing.from_email) and a unified label ("Name" <e@ma.il>)
+ // example: <span crm-mailing-from-address="myPlaceholder" crm-mailing="myMailing"><select ng-model="myPlaceholder.label"></select></span>
+ // NOTE: This really doesn't belong in a directive. I've tried (and failed) to make this work with a getterSetter binding, eg
+ // <select ng-model="mailing.convertFromAddress" ng-model-options="{getterSetter: true}">
+ angular.module('crmMailing').directive('crmMailingFromAddress', function(crmFromAddresses) {
+ return {
+ link: function(scope, element, attrs) {
+ var placeholder = attrs.crmMailingFromAddress;
+ var mailing = null;
+ scope.$watch(attrs.crmMailing, function(newValue) {
+ mailing = newValue;
+ scope[placeholder] = {
+ label: crmFromAddresses.getByAuthorEmail(mailing.from_name, mailing.from_email, true).label
+ };
+ });
+ scope.$watch(placeholder + '.label', function(newValue) {
+ var addr = crmFromAddresses.getByLabel(newValue);
+ mailing.from_name = addr.author;
+ mailing.from_email = addr.email;
+ // CRM-18364: set replyTo as from_email only if custom replyTo is disabled in mail settings.
+ if (!CRM.crmMailing.enableReplyTo) {
+ mailing.replyto_email = crmFromAddresses.getByAuthorEmail(mailing.from_name, mailing.from_email, true).label;
+ }
+ });
+ // FIXME: Shouldn't we also be watching mailing.from_name and mailing.from_email?
+ }
+ };
+ });
+})(angular, CRM.$, CRM._);
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/ListMailingsCtrl.js b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/ListMailingsCtrl.js
new file mode 100644
index 00000000..e60ffe54
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/ListMailingsCtrl.js
@@ -0,0 +1,10 @@
+(function(angular, $, _) {
+
+ angular.module('crmMailing').controller('ListMailingsCtrl', ['crmLegacy', 'crmNavigator', function ListMailingsCtrl(crmLegacy, crmNavigator) {
+ // We haven't implemented this in Angular, but some users may get clever
+ // about typing URLs, so we'll provide a redirect.
+ var new_url = crmLegacy.url('civicrm/mailing/browse/unscheduled', {reset: 1, scheduled: 'false'});
+ crmNavigator.redirect(new_url);
+ }]);
+
+})(angular, CRM.$, CRM._);
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/MsgTemplateCtrl.js b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/MsgTemplateCtrl.js
new file mode 100644
index 00000000..5d200c3f
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/MsgTemplateCtrl.js
@@ -0,0 +1,44 @@
+(function(angular, $, _) {
+
+ // Controller for the in-place msg-template management
+ angular.module('crmMailing').controller('MsgTemplateCtrl', function MsgTemplateCtrl($scope, crmMsgTemplates, dialogService) {
+ var ts = $scope.ts = CRM.ts(null);
+ $scope.crmMsgTemplates = crmMsgTemplates;
+ $scope.checkPerm = CRM.checkPerm;
+ // @return Promise MessageTemplate (per APIv3)
+ $scope.saveTemplate = function saveTemplate(mailing) {
+ var model = {
+ selected_id: mailing.msg_template_id,
+ tpl: {
+ msg_title: '',
+ msg_subject: mailing.subject,
+ msg_text: mailing.body_text,
+ msg_html: mailing.body_html
+ }
+ };
+ var options = CRM.utils.adjustDialogDefaults({
+ autoOpen: false,
+ height: 'auto',
+ width: '40%',
+ title: ts('Save Template')
+ });
+ return dialogService.open('saveTemplateDialog', '~/crmMailing/SaveMsgTemplateDialogCtrl.html', model, options)
+ .then(function(item) {
+ mailing.msg_template_id = item.id;
+ return item;
+ });
+ };
+
+ // @param int id
+ // @return Promise
+ $scope.loadTemplate = function loadTemplate(mailing, id) {
+ return crmMsgTemplates.get(id).then(function(tpl) {
+ mailing.msg_template_id = tpl.id;
+ mailing.subject = tpl.msg_subject;
+ mailing.body_text = tpl.msg_text;
+ mailing.body_html = tpl.msg_html;
+ });
+ };
+ });
+
+})(angular, CRM.$, CRM._);
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/PreviewComponentCtrl.js b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/PreviewComponentCtrl.js
new file mode 100644
index 00000000..3fd928a1
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/PreviewComponentCtrl.js
@@ -0,0 +1,24 @@
+(function(angular, $, _) {
+
+ // Controller for the "Preview Mailing Component" segment
+ // which displays header/footer/auto-responder
+ angular.module('crmMailing').controller('PreviewComponentCtrl', function PreviewComponentCtrl($scope, dialogService) {
+ var ts = $scope.ts = CRM.ts(null);
+
+ $scope.previewComponent = function previewComponent(title, componentId) {
+ var component = _.where(CRM.crmMailing.headerfooterList, {id: "" + componentId});
+ if (!component || !component[0]) {
+ CRM.alert(ts('Invalid component ID (%1)', {
+ 1: componentId
+ }));
+ return;
+ }
+ var options = CRM.utils.adjustDialogDefaults({
+ autoOpen: false,
+ title: title // component[0].name
+ });
+ dialogService.open('previewComponentDialog', '~/crmMailing/PreviewComponentDialogCtrl.html', component[0], options);
+ };
+ });
+
+})(angular, CRM.$, CRM._);
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/PreviewComponentDialogCtrl.html b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/PreviewComponentDialogCtrl.html
new file mode 100644
index 00000000..71db7027
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/PreviewComponentDialogCtrl.html
@@ -0,0 +1,28 @@
+<div ng-controller="PreviewComponentDialogCtrl">
+ <div class="crm-block">
+ <div class="crm-group">
+ <div class="crm-section" ng-show="model.name">
+ <div class="label">{{ts('Name')}}</div>
+ <div class="content">
+ {{model.name}}
+ </div>
+ <div class="clear"></div>
+ </div>
+ <div class="crm-section" ng-show="model.subject">
+ <div class="label">{{ts('Subject')}}</div>
+ <div class="content">
+ {{model.subject}}
+ </div>
+ <div class="clear"></div>
+ </div>
+ </div>
+ </div>
+ <div crm-ui-tab-set>
+ <div crm-ui-tab id="preview-html" crm-title="ts('HTML')">
+ <iframe crm-ui-iframe="model.body_html"></iframe>
+ </div>
+ <div crm-ui-tab id="preview-text" crm-title="ts('Plain Text')">
+ <pre>{{model.body_text}}</pre>
+ </div>
+ </div>
+</div>
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/PreviewComponentDialogCtrl.js b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/PreviewComponentDialogCtrl.js
new file mode 100644
index 00000000..2b1d9f2c
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/PreviewComponentDialogCtrl.js
@@ -0,0 +1,13 @@
+(function(angular, $, _) {
+
+ // Controller for the "Preview Mailing Component" dialog
+ // Note: Expects $scope.model to be an object with properties:
+ // - "name"
+ // - "subject"
+ // - "body_html"
+ // - "body_text"
+ angular.module('crmMailing').controller('PreviewComponentDialogCtrl', function PreviewComponentDialogCtrl($scope) {
+ $scope.ts = CRM.ts(null);
+ });
+
+})(angular, CRM.$, CRM._);
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/PreviewMailingDialogCtrl.js b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/PreviewMailingDialogCtrl.js
new file mode 100644
index 00000000..9e339b53
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/PreviewMailingDialogCtrl.js
@@ -0,0 +1,12 @@
+(function(angular, $, _) {
+
+ // Controller for the "Preview Mailing" dialog
+ // Note: Expects $scope.model to be an object with properties:
+ // - "subject"
+ // - "body_html"
+ // - "body_text"
+ angular.module('crmMailing').controller('PreviewMailingDialogCtrl', function PreviewMailingDialogCtrl($scope) {
+ $scope.ts = CRM.ts(null);
+ });
+
+})(angular, CRM.$, CRM._);
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/PreviewMgr/full.html b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/PreviewMgr/full.html
new file mode 100644
index 00000000..0e257a11
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/PreviewMgr/full.html
@@ -0,0 +1,10 @@
+<div ng-controller="PreviewMailingDialogCtrl">
+ <div crm-ui-tab-set>
+ <div crm-ui-tab id="preview-html" crm-title="ts('HTML')">
+ <iframe crm-ui-iframe="model.body_html"></iframe>
+ </div>
+ <div crm-ui-tab id="preview-text" crm-title="ts('Plain Text')">
+ <pre>{{model.body_text}}</pre>
+ </div>
+ </div>
+</div> \ No newline at end of file
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/PreviewMgr/html.html b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/PreviewMgr/html.html
new file mode 100644
index 00000000..c47b1c02
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/PreviewMgr/html.html
@@ -0,0 +1,3 @@
+<div ng-controller="PreviewMailingDialogCtrl">
+ <iframe crm-ui-iframe="model.body_html"></iframe>
+</div> \ No newline at end of file
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/PreviewMgr/text.html b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/PreviewMgr/text.html
new file mode 100644
index 00000000..246add4b
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/PreviewMgr/text.html
@@ -0,0 +1,3 @@
+<div ng-controller="PreviewMailingDialogCtrl">
+ <pre>{{model.body_text}}</pre>
+</div> \ No newline at end of file
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/PreviewRecipCtrl.html b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/PreviewRecipCtrl.html
new file mode 100644
index 00000000..6eb6459a
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/PreviewRecipCtrl.html
@@ -0,0 +1,32 @@
+<div ng-controller="PreviewRecipCtrl">
+ <!--
+ Controller: PreviewRecipCtrl
+ Required vars: model.sample
+ -->
+
+ <div class="help">
+ <p>{{ts('Based on current data, approximately %1 contacts will receive a copy of the mailing.', {1: model.count})}}</p>
+
+ <p ng-show="model.sample.length == model.sampleLimit">{{ts('Below is a sample of the first %1 recipients.', {1: model.sampleLimit})}}</p>
+
+ <p>{{ts('If individual contacts are separately modified, added, or removed, then the final list may change.')}}</p>
+ </div>
+
+ <div ng-show="model.sample == 0">
+ {{ts('No recipients')}}
+ </div>
+ <table ng-show="model.sample.length > 0">
+ <thead>
+ <tr>
+ <th>{{ts('Name')}}</th>
+ <th>{{ts('Email')}}</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr ng-repeat="recipient in model.sample">
+ <td>{{recipient['api.contact.getvalue']}}</td>
+ <td>{{recipient['api.email.getvalue']}}</td>
+ </tr>
+ </tbody>
+ </table>
+</div>
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/PreviewRecipCtrl.js b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/PreviewRecipCtrl.js
new file mode 100644
index 00000000..371fb8ef
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/PreviewRecipCtrl.js
@@ -0,0 +1,10 @@
+(function(angular, $, _) {
+
+ // Controller for the "Preview Recipients" dialog
+ // Note: Expects $scope.model to be an object with properties:
+ // - recipients: array of contacts
+ angular.module('crmMailing').controller('PreviewRecipCtrl', function($scope) {
+ $scope.ts = CRM.ts(null);
+ });
+
+})(angular, CRM.$, CRM._);
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/RadioDate.js b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/RadioDate.js
new file mode 100644
index 00000000..037b6e22
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/RadioDate.js
@@ -0,0 +1,116 @@
+(function(angular, $, _) {
+ // "YYYY-MM-DD hh:mm:ss" => Date()
+ function parseYmdHms(d) {
+ var parts = d.split(/[\-: ]/);
+ return new Date(parts[0], parts[1]-1, parts[2], parts[3], parts[4], parts[5]);
+ }
+
+ function isDateBefore(tgt, cutoff, tolerance) {
+ var ad = parseYmdHms(tgt), bd = parseYmdHms(cutoff);
+ // We'll allow a little leeway, where tgt is considered before cutoff
+ // even if technically misses the cutoff by a little.
+ return ad < bd-tolerance;
+ }
+
+ // Represent a datetime field as if it were a radio ('schedule.mode') and a datetime ('schedule.datetime').
+ // example: <div crm-mailing-radio-date="mySchedule" ng-model="mailing.scheduled_date">...</div>
+ angular.module('crmMailing').directive('crmMailingRadioDate', function(crmUiAlert) {
+ return {
+ require: 'ngModel',
+ link: function($scope, element, attrs, ngModel) {
+ var lastAlert = null;
+
+ var schedule = $scope[attrs.crmMailingRadioDate] = {
+ mode: 'now',
+ datetime: ''
+ };
+
+ ngModel.$render = function $render() {
+ var sched = ngModel.$viewValue;
+ if (!_.isEmpty(sched)) {
+ schedule.mode = 'at';
+ schedule.datetime = sched;
+ }
+ else {
+ schedule.mode = 'now';
+ schedule.datetime = '';
+ }
+ };
+
+ var updateParent = (function() {
+ switch (schedule.mode) {
+ case 'now':
+ ngModel.$setViewValue(null);
+ schedule.datetime = '';
+ break;
+ case 'at':
+ schedule.datetime = schedule.datetime || '?';
+ ngModel.$setViewValue(schedule.datetime);
+ break;
+ default:
+ throw 'Unrecognized schedule mode: ' + schedule.mode;
+ }
+ });
+
+ element
+ // Open datepicker when clicking "At" radio
+ .on('click', ':radio[value=at]', function() {
+ $('.crm-form-date', element).focus();
+ })
+ // Reset mode if user entered an invalid date
+ .on('change', '.crm-hidden-date', function(e, context) {
+ if (context === 'userInput' && $(this).val() === '' && $(this).siblings('.crm-form-date').val().length) {
+ schedule.mode = 'at';
+ schedule.datetime = '?';
+ } else {
+ var d = new Date(),
+ month = '' + (d.getMonth() + 1),
+ day = '' + d.getDate(),
+ year = d.getFullYear(),
+ hours = '' + d.getHours(),
+ minutes = '' + d.getMinutes();
+ var submittedDate = $(this).val();
+ if (month.length < 2) month = '0' + month;
+ if (day.length < 2) day = '0' + day;
+ if (hours.length < 2) hours = '0' + hours;
+ if (minutes.length < 2) minutes = '0' + minutes;
+ date = [year, month, day].join('-');
+ time = [hours, minutes, "00"].join(':');
+ currentDate = date + ' ' + time;
+ var isInPast = (submittedDate.length && submittedDate.match(/^[0-9\-]+ [0-9\:]+$/) && isDateBefore(submittedDate, currentDate, 4*60*60*1000));
+ ngModel.$setValidity('dateTimeInThePast', !isInPast);
+ if (lastAlert && lastAlert.isOpen) {
+ lastAlert.close();
+ }
+ if (isInPast) {
+ lastAlert = crmUiAlert({
+ text: ts('The scheduled date and time is in the past'),
+ title: ts('Error')
+ });
+ }
+ }
+ });
+
+ $scope.$watch(attrs.crmMailingRadioDate + '.mode', updateParent);
+ $scope.$watch(attrs.crmMailingRadioDate + '.datetime', function(newValue, oldValue) {
+ // automatically switch mode based on datetime entry
+ if (typeof oldValue === 'undefined') {
+ oldValue = '';
+ }
+ if (typeof newValue === 'undefined') {
+ newValue = '';
+ }
+ if (oldValue !== newValue) {
+ if (_.isEmpty(newValue)) {
+ schedule.mode = 'now';
+ }
+ else {
+ schedule.mode = 'at';
+ }
+ }
+ updateParent();
+ });
+ }
+ };
+ });
+})(angular, CRM.$, CRM._);
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/Recipients.js b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/Recipients.js
new file mode 100644
index 00000000..4203d4a3
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/Recipients.js
@@ -0,0 +1,341 @@
+(function(angular, $, _) {
+ // example: <select multiple crm-mailing-recipients crm-mailing="mymailing" crm-avail-groups="myGroups" crm-avail-mailings="myMailings"></select>
+ // FIXME: participate in ngModel's validation cycle
+ angular.module('crmMailing').directive('crmMailingRecipients', function(crmUiAlert) {
+ return {
+ restrict: 'AE',
+ require: 'ngModel',
+ scope: {
+ ngRequired: '@'
+ },
+ link: function(scope, element, attrs, ngModel) {
+ scope.recips = ngModel.$viewValue;
+ scope.groups = scope.$parent.$eval(attrs.crmAvailGroups);
+ scope.mailings = scope.$parent.$eval(attrs.crmAvailMailings);
+ refreshMandatory();
+
+ var ts = scope.ts = CRM.ts(null);
+
+ /// Convert MySQL date ("yyyy-mm-dd hh:mm:ss") to JS date object
+ scope.parseDate = function(date) {
+ if (!angular.isString(date)) {
+ return date;
+ }
+ var p = date.split(/[\- :]/);
+ return new Date(parseInt(p[0]), parseInt(p[1]) - 1, parseInt(p[2]), parseInt(p[3]), parseInt(p[4]), parseInt(p[5]));
+ };
+
+ /// Remove {value} from {array}
+ function arrayRemove(array, value) {
+ var idx = array.indexOf(value);
+ if (idx >= 0) {
+ array.splice(idx, 1);
+ }
+ }
+
+ // @param string id an encoded string like "4 civicrm_mailing include"
+ // @return Object keys: entity_id, entity_type, mode
+ function convertValueToObj(id) {
+ var a = id.split(" ");
+ return {entity_id: parseInt(a[0]), entity_type: a[1], mode: a[2]};
+ }
+
+ // @param Object mailing
+ // @return array list of values like "4 civicrm_mailing include"
+ function convertMailingToValues(recipients) {
+ var r = [];
+ angular.forEach(recipients.groups.include, function(v) {
+ r.push(v + " civicrm_group include");
+ });
+ angular.forEach(recipients.groups.exclude, function(v) {
+ r.push(v + " civicrm_group exclude");
+ });
+ angular.forEach(recipients.mailings.include, function(v) {
+ r.push(v + " civicrm_mailing include");
+ });
+ angular.forEach(recipients.mailings.exclude, function(v) {
+ r.push(v + " civicrm_mailing exclude");
+ });
+ return r;
+ }
+
+ function refreshMandatory() {
+ if (ngModel.$viewValue && ngModel.$viewValue.groups) {
+ scope.mandatoryGroups = _.filter(scope.$parent.$eval(attrs.crmMandatoryGroups), function(grp) {
+ return _.contains(ngModel.$viewValue.groups.include, parseInt(grp.id));
+ });
+ scope.mandatoryIds = _.map(_.pluck(scope.$parent.$eval(attrs.crmMandatoryGroups), 'id'), function(n) {
+ return parseInt(n);
+ });
+ }
+ else {
+ scope.mandatoryGroups = [];
+ scope.mandatoryIds = [];
+ }
+ }
+
+ function isMandatory(grpId) {
+ return _.contains(scope.mandatoryIds, parseInt(grpId));
+ }
+
+ var refreshUI = ngModel.$render = function refresuhUI() {
+ scope.recips = ngModel.$viewValue;
+ if (ngModel.$viewValue) {
+ $(element).select2('val', convertMailingToValues(ngModel.$viewValue));
+ validate();
+ refreshMandatory();
+ }
+ };
+
+ // @return string HTML representing an option
+ function formatItem(item) {
+ if (!item.id) {
+ // return `text` for optgroup
+ return item.text;
+ }
+ var option = convertValueToObj(item.id);
+ var icon = (option.entity_type === 'civicrm_mailing') ? 'fa-envelope' : 'fa-users';
+ var spanClass = (option.mode == 'exclude') ? 'crmMailing-exclude' : 'crmMailing-include';
+ if (option.entity_type != 'civicrm_mailing' && isMandatory(option.entity_id)) {
+ spanClass = 'crmMailing-mandatory';
+ }
+ return '<i class="crm-i '+icon+'"></i> <span class="' + spanClass + '">' + item.text + '</span>';
+ }
+
+ function validate() {
+ if (scope.$parent.$eval(attrs.ngRequired)) {
+ var empty = (_.isEmpty(ngModel.$viewValue.groups.include) && _.isEmpty(ngModel.$viewValue.mailings.include));
+ ngModel.$setValidity('empty', !empty);
+ }
+ else {
+ ngModel.$setValidity('empty', true);
+ }
+ }
+
+ var rcpAjaxState = {
+ input: '',
+ entity: 'civicrm_group',
+ type: 'include',
+ page_n: 0,
+ page_i: 0,
+ };
+
+ $(element).select2({
+ width: '36em',
+ dropdownAutoWidth: true,
+ placeholder: "Groups or Past Recipients",
+ formatResult: formatItem,
+ formatSelection: formatItem,
+ escapeMarkup: function(m) {
+ return m;
+ },
+ multiple: true,
+ initSelection: function(el, cb) {
+ var values = el.val().split(',');
+
+ var gids = [];
+ var mids = [];
+
+ for (var i = 0; i < values.length; i++) {
+ var dv = convertValueToObj(values[i]);
+ if (dv.entity_type == 'civicrm_group') {
+ gids.push(dv.entity_id);
+ }
+ else if (dv.entity_type == 'civicrm_mailing') {
+ mids.push(dv.entity_id);
+ }
+ }
+ // push non existant 0 group/mailing id in order when no recipents group or prior mailing is selected
+ // this will allow to resuse the below code to handle datamap
+ if (gids.length === 0) {
+ gids.push(0);
+ }
+ if (mids.length === 0) {
+ mids.push(0);
+ }
+
+ CRM.api3('Group', 'getlist', { params: { id: { IN: gids }, options: { limit: 0 } }, extra: ["is_hidden"] } ).then(
+ function(glist) {
+ CRM.api3('Mailing', 'getlist', { params: { id: { IN: mids }, options: { limit: 0 } } }).then(
+ function(mlist) {
+ var datamap = [];
+
+ var groupNames = [];
+ var civiMails = [];
+
+ $(glist.values).each(function (idx, group) {
+ var key = group.id + ' civicrm_group include';
+ groupNames.push({id: parseInt(group.id), title: group.label, is_hidden: group.extra.is_hidden});
+
+ if (values.indexOf(key) >= 0) {
+ datamap.push({id: key, text: group.label});
+ }
+
+ key = group.id + ' civicrm_group exclude';
+ if (values.indexOf(key) >= 0) {
+ datamap.push({id: key, text: group.label});
+ }
+ });
+
+ $(mlist.values).each(function (idx, group) {
+ var key = group.id + ' civicrm_mailing include';
+ civiMails.push({id: parseInt(group.id), name: group.label});
+
+ if (values.indexOf(key) >= 0) {
+ datamap.push({id: key, text: group.label});
+ }
+
+ key = group.id + ' civicrm_mailing exclude';
+ if (values.indexOf(key) >= 0) {
+ datamap.push({id: key, text: group.label});
+ }
+ });
+
+ scope.$parent.crmMailingConst.groupNames = groupNames;
+ scope.$parent.crmMailingConst.civiMails = civiMails;
+
+ refreshMandatory();
+
+ cb(datamap);
+ });
+ });
+ },
+ ajax: {
+ url: CRM.url('civicrm/ajax/rest'),
+ quietMillis: 300,
+ data: function(input, page_num) {
+ if (page_num <= 1) {
+ rcpAjaxState = {
+ input: input,
+ entity: 'civicrm_group',
+ type: 'include',
+ page_n: 0,
+ };
+ }
+
+ rcpAjaxState.page_i = page_num - rcpAjaxState.page_n;
+ var filterParams = {};
+ switch(rcpAjaxState.entity) {
+ case 'civicrm_group':
+ filterParams = { is_hidden: 0, is_active: 1, group_type: {"LIKE": "%2%"} };
+ break;
+
+ case 'civicrm_mailing':
+ filterParams = { is_hidden: 0, is_active: 1 };
+ break;
+ }
+ var params = {
+ input: input,
+ page_num: rcpAjaxState.page_i,
+ params: filterParams,
+ };
+
+ if('civicrm_mailing' === rcpAjaxState.entity) {
+ params["api.MailingRecipients.getcount"] = {};
+ }
+
+ return params;
+ },
+ transport: function(params) {
+ switch(rcpAjaxState.entity) {
+ case 'civicrm_group':
+ CRM.api3('Group', 'getlist', params.data).then(params.success, params.error);
+ break;
+
+ case 'civicrm_mailing':
+ params.data.params.options = { sort: "is_archived asc, scheduled_date desc" };
+ CRM.api3('Mailing', 'getlist', params.data).then(params.success, params.error);
+ break;
+ }
+ },
+ results: function(data) {
+ results = {
+ children: $.map(data.values, function(obj) {
+ if('civicrm_mailing' === rcpAjaxState.entity) {
+ return obj["api.MailingRecipients.getcount"] > 0 ? { id: obj.id + ' ' + rcpAjaxState.entity + ' ' + rcpAjaxState.type,
+ text: obj.label } : '';
+ }
+ else {
+ return { id: obj.id + ' ' + rcpAjaxState.entity + ' ' + rcpAjaxState.type,
+ text: obj.label };
+ }
+ })
+ };
+
+ if (rcpAjaxState.page_i == 1 && data.count && results.children.length > 0) {
+ results.text = ts((rcpAjaxState.type == 'include'? 'Include ' : 'Exclude ') +
+ (rcpAjaxState.entity == 'civicrm_group'? 'Group' : 'Mailing'));
+ }
+
+ more = data.more_results || !(rcpAjaxState.entity == 'civicrm_mailing' && rcpAjaxState.type == 'exclude');
+
+ if (more && !data.more_results) {
+ if (rcpAjaxState.type == 'include') {
+ rcpAjaxState.type = 'exclude';
+ } else {
+ rcpAjaxState.type = 'include';
+ rcpAjaxState.entity = 'civicrm_mailing';
+ }
+ rcpAjaxState.page_n += rcpAjaxState.page_i;
+ }
+
+ return { more: more, results: [ results ] };
+ },
+ },
+ });
+
+ $(element).on('select2-selecting', function(e) {
+ var option = convertValueToObj(e.val);
+ var typeKey = option.entity_type == 'civicrm_mailing' ? 'mailings' : 'groups';
+ if (option.mode == 'exclude') {
+ ngModel.$viewValue[typeKey].exclude.push(option.entity_id);
+ arrayRemove(ngModel.$viewValue[typeKey].include, option.entity_id);
+ }
+ else {
+ ngModel.$viewValue[typeKey].include.push(option.entity_id);
+ arrayRemove(ngModel.$viewValue[typeKey].exclude, option.entity_id);
+ }
+ scope.$apply();
+ $(element).select2('close');
+ validate();
+ e.preventDefault();
+ });
+
+ $(element).on("select2-removing", function(e) {
+ var option = convertValueToObj(e.val);
+ var typeKey = option.entity_type == 'civicrm_mailing' ? 'mailings' : 'groups';
+ if (typeKey == 'groups' && isMandatory(option.entity_id)) {
+ crmUiAlert({
+ text: ts('This mailing was generated based on search results. The search results cannot be removed.'),
+ title: ts('Required')
+ });
+ e.preventDefault();
+ return;
+ }
+ scope.$parent.$apply(function() {
+ arrayRemove(ngModel.$viewValue[typeKey][option.mode], option.entity_id);
+ });
+ validate();
+ e.preventDefault();
+ });
+
+ scope.$watchCollection("recips.groups.include", refreshUI);
+ scope.$watchCollection("recips.groups.exclude", refreshUI);
+ scope.$watchCollection("recips.mailings.include", refreshUI);
+ scope.$watchCollection("recips.mailings.exclude", refreshUI);
+ setTimeout(refreshUI, 50);
+
+ scope.$watchCollection(attrs.crmAvailGroups, function() {
+ scope.groups = scope.$parent.$eval(attrs.crmAvailGroups);
+ });
+ scope.$watchCollection(attrs.crmAvailMailings, function() {
+ scope.mailings = scope.$parent.$eval(attrs.crmAvailMailings);
+ });
+ scope.$watchCollection(attrs.crmMandatoryGroups, function() {
+ refreshMandatory();
+ });
+ }
+ };
+ });
+
+})(angular, CRM.$, CRM._);
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/ReviewBool.js b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/ReviewBool.js
new file mode 100644
index 00000000..98ec85c5
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/ReviewBool.js
@@ -0,0 +1,28 @@
+(function(angular, $, _) {
+ angular.module('crmMailing').directive('crmMailingReviewBool', function() {
+ return {
+ scope: {
+ crmOn: '@',
+ crmTitle: '@'
+ },
+ template: '<span ng-class="spanClasses"><i class="crm-i" ng-class="iconClasses"></i> {{evalTitle}} </span>',
+ link: function(scope, element, attrs) {
+ function refresh() {
+ if (scope.$parent.$eval(attrs.crmOn)) {
+ scope.spanClasses = {'crmMailing-active': true};
+ scope.iconClasses = {'fa-check': true};
+ }
+ else {
+ scope.spanClasses = {'crmMailing-inactive': true};
+ scope.iconClasses = {'fa-times': true};
+ }
+ scope.evalTitle = scope.$parent.$eval(attrs.crmTitle);
+ }
+
+ refresh();
+ scope.$parent.$watch(attrs.crmOn, refresh);
+ scope.$parent.$watch(attrs.crmTitle, refresh);
+ }
+ };
+ });
+})(angular, CRM.$, CRM._);
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/SaveMsgTemplateDialogCtrl.html b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/SaveMsgTemplateDialogCtrl.html
new file mode 100644
index 00000000..1e5c723f
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/SaveMsgTemplateDialogCtrl.html
@@ -0,0 +1,17 @@
+<div ng-controller="SaveMsgTemplateDialogCtrl">
+ <p><em>{{ts('Save the current mailing as a template.')}}</em></p>
+
+ <div ng-hide="!selected">
+ <label for="saveopt-mode-update">
+ <input type="radio" name="mode" ng-model="saveOpt.mode" value="update" id="saveopt-mode-update">
+ {{ts('Update "%1"', {1: selected.msg_title})}}
+ </label>
+ </div>
+ <div>
+ <label type="radio" for="saveopt-mode-add">
+ <input type="radio" name="mode" ng-model="saveOpt.mode" value="add" id="saveopt-mode-add">
+ {{ts('Save as:')}}
+ </label>
+ <input type="text" ng-model="saveOpt.newTitle" ng-click="saveOpt.mode='add'" ng-change="saveOpt.mode='add'" />
+ </div>
+</div>
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/SaveMsgTemplateDialogCtrl.js b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/SaveMsgTemplateDialogCtrl.js
new file mode 100644
index 00000000..ea80522e
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/SaveMsgTemplateDialogCtrl.js
@@ -0,0 +1,83 @@
+(function(angular, $, _) {
+
+ // Controller for the "Save Message Template" dialog
+ // Scope members:
+ // - [input] "model": Object
+ // - "selected_id": int
+ // - "tpl": Object
+ // - "msg_subject": string
+ // - "msg_text": string
+ // - "msg_html": string
+ angular.module('crmMailing').controller('SaveMsgTemplateDialogCtrl', function SaveMsgTemplateDialogCtrl($scope, crmMsgTemplates, dialogService) {
+ var ts = $scope.ts = CRM.ts(null);
+ $scope.saveOpt = {mode: '', newTitle: ''};
+ $scope.selected = null;
+
+ $scope.save = function save() {
+ var tpl = _.extend({}, $scope.model.tpl);
+ switch ($scope.saveOpt.mode) {
+ case 'add':
+ tpl.msg_title = $scope.saveOpt.newTitle;
+ break;
+ case 'update':
+ tpl.id = $scope.selected.id;
+ tpl.msg_title = $scope.selected.msg_title;
+ break;
+ default:
+ throw 'SaveMsgTemplateDialogCtrl: Unrecognized mode: ' + $scope.saveOpt.mode;
+ }
+ return crmMsgTemplates.save(tpl)
+ .then(function (item) {
+ CRM.status(ts('Saved'));
+ return item;
+ });
+ };
+
+ function scopeApply(f) {
+ return function () {
+ var args = arguments;
+ $scope.$apply(function () {
+ f.apply(args);
+ });
+ };
+ }
+
+ function init() {
+ crmMsgTemplates.get($scope.model.selected_id).then(
+ function (tpl) {
+ $scope.saveOpt.mode = 'update';
+ $scope.selected = tpl;
+ },
+ function () {
+ $scope.saveOpt.mode = 'add';
+ $scope.selected = null;
+ }
+ );
+ // When using dialogService with a button bar, the major button actions
+ // need to be registered with the dialog widget (and not embedded in
+ // the body of the dialog).
+ var buttons = [
+ {
+ text: ts('Save'),
+ icons: {primary: 'fa-check'},
+ click: function () {
+ $scope.save().then(function (item) {
+ dialogService.close('saveTemplateDialog', item);
+ });
+ }
+ },
+ {
+ text: ts('Cancel'),
+ icons: {primary: 'fa-times'},
+ click: function () {
+ dialogService.cancel('saveTemplateDialog');
+ }
+ }
+ ];
+ dialogService.setButtons('saveTemplateDialog', buttons);
+ }
+
+ setTimeout(scopeApply(init), 0);
+ });
+
+})(angular, CRM.$, CRM._);
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/Templates.js b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/Templates.js
new file mode 100644
index 00000000..0c2b93dc
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/Templates.js
@@ -0,0 +1,131 @@
+(function(angular, $, _) {
+ // example <select crm-mailing-templates crm-mailing="mymailing"></select>
+ angular.module('crmMailing').directive('crmMailingTemplates', function(crmUiAlert) {
+ return {
+ restrict: 'AE',
+ require: 'ngModel',
+ scope: {
+ ngRequired: '@'
+ },
+ link: function(scope, element, attrs, ngModel) {
+ scope.template = ngModel.$viewValue;
+
+ var refreshUI = ngModel.$render = function refresuhUI() {
+ scope.template = ngModel.$viewValue;
+ if (ngModel.$viewValue) {
+ $(element).select2('val', ngModel.$viewValue);
+ }
+ };
+
+ // @return string HTML representing an option
+ function formatItem(item) {
+ if (!item.id) {
+ // return `text` for optgroup
+ return item.text;
+ }
+ return '<span class="crmMailing-template">' + item.text + '</span>';
+ }
+
+ var rcpAjaxState = {
+ input: '',
+ entity: 'civicrm_msg_templates',
+ page_n: 0,
+ page_i: 0,
+ };
+
+ $(element).select2({
+ width: '36em',
+ placeholder: "<i class='fa fa-clipboard'></i> Mailing Templates",
+ formatResult: formatItem,
+ escapeMarkup: function(m) {
+ return m;
+ },
+ multiple: false,
+ initSelection: function(el, cb) {
+
+ var value = el.val();
+
+ CRM.api3('MessageTemplate', 'getlist', { params: { id: value }, label_field: 'msg_title' }).then(function(tlist) {
+
+ var template = {};
+
+ if (tlist.count) {
+ $(tlist.values).each(function(id, val) {
+ template.id = val.id;
+ template.text = val.label;
+ });
+ }
+
+ cb(template);
+ });
+ },
+ ajax: {
+ url: CRM.url('civicrm/ajax/rest'),
+ quietMillis: 300,
+ data: function(input, page_num) {
+ if (page_num <= 1) {
+ rcpAjaxState = {
+ input: input,
+ entity: 'civicrm_msg_templates',
+ page_n: 0,
+ };
+ }
+
+ rcpAjaxState.page_i = page_num - rcpAjaxState.page_n;
+ var filterParams = { is_active: 1, workflow_id: { "IS NULL": 1 } };
+
+ var params = {
+ input: input,
+ page_num: rcpAjaxState.page_i,
+ label_field: 'msg_title',
+ search_field: 'msg_title',
+ params: filterParams,
+ };
+ return params;
+ },
+ transport: function(params) {
+ CRM.api3('MessageTemplate', 'getlist', params.data).then(params.success, params.error);
+ },
+ results: function(data) {
+
+ results = {
+ children: $.map(data.values, function(obj) {
+ return { id: obj.id, text: obj.label };
+ })
+ };
+
+ if (rcpAjaxState.page_i == 1 && data.count) {
+ results.text = ts('Message Templates');
+ }
+
+ more = data.more_results;
+
+ if (more && !data.more_results) {
+ rcpAjaxState.page_n += rcpAjaxState.page_i;
+ }
+
+ return { more: more, results: [ results ] };
+ },
+ }
+ });
+
+ $(element).on('select2-selecting', function(e) {
+ // in here is where the template HTML should be loaded
+ var entity_id = parseInt(e.val);
+ ngModel.$viewValue = entity_id;
+
+ scope.$parent.loadTemplate(scope.$parent.$parent.mailing, entity_id);
+ scope.$apply();
+ $(element).select2('close');
+ e.preventDefault();
+ });
+
+
+ scope.$watchCollection("template", refreshUI);
+ setTimeout(refreshUI, 50);
+ }
+ };
+
+
+ });
+})(angular, CRM.$, CRM._);
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/Token.js b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/Token.js
new file mode 100644
index 00000000..71131d21
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/Token.js
@@ -0,0 +1,28 @@
+(function(angular, $, _) {
+ // example: <input name="subject" /> <input crm-mailing-token on-select="doSomething(token.name)" />
+ // WISHLIST: Instead of global CRM.crmMailing.mailTokens, accept token list as an input
+ angular.module('crmMailing').directive('crmMailingToken', function() {
+ return {
+ require: '^crmUiIdScope',
+ scope: {
+ onSelect: '@'
+ },
+ template: '<input type="text" class="crmMailingToken" />',
+ link: function(scope, element, attrs, crmUiIdCtrl) {
+ $(element).addClass('crm-action-menu fa-code').crmSelect2({
+ width: "12em",
+ dropdownAutoWidth: true,
+ data: CRM.crmMailing.mailTokens,
+ placeholder: ts('Tokens')
+ });
+ $(element).on('select2-selecting', function(e) {
+ e.preventDefault();
+ $(element).select2('close').select2('val', '');
+ scope.$parent.$eval(attrs.onSelect, {
+ token: {name: e.val}
+ });
+ });
+ }
+ };
+ });
+})(angular, CRM.$, CRM._);
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/ViewRecipCtrl.js b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/ViewRecipCtrl.js
new file mode 100644
index 00000000..d72793fd
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/ViewRecipCtrl.js
@@ -0,0 +1,128 @@
+(function(angular, $, _) {
+
+ angular.module('crmMailing').controller('ViewRecipCtrl', function ViewRecipCtrl($scope) {
+ var mids = [];
+ var gids = [];
+ var groupNames = [];
+ var mailings = [];
+ var civimailings = [];
+ var civimails = [];
+
+ function getGroupNames(mailing) {
+ if (-1 == mailings.indexOf(mailing.id)) {
+ mailings.push(mailing.id);
+ _.each(mailing.recipients.groups.include, function(id) {
+ if (-1 == gids.indexOf(id)) {
+ gids.push(id);
+ }
+ });
+ _.each(mailing.recipients.groups.exclude, function(id) {
+ if (-1 == gids.indexOf(id)) {
+ gids.push(id);
+ }
+ });
+ _.each(mailing.recipients.groups.base, function(id) {
+ if (-1 == gids.indexOf(id)) {
+ gids.push(id);
+ }
+ });
+ if (!_.isEmpty(gids)) {
+ CRM.api3('Group', 'get', {'id': {"IN": gids}}).then(function(result) {
+ _.each(result.values, function(grp) {
+ if (_.isEmpty(_.where(groupNames, {id: parseInt(grp.id)}))) {
+ groupNames.push({id: parseInt(grp.id), title: grp.title, is_hidden: grp.is_hidden});
+ }
+ });
+ CRM.crmMailing.groupNames = groupNames;
+ $scope.$parent.crmMailingConst.groupNames = groupNames;
+ });
+ }
+ }
+ }
+
+ function getCiviMails(mailing) {
+ if (-1 == civimailings.indexOf(mailing.id)) {
+ civimailings.push(mailing.id);
+ _.each(mailing.recipients.mailings.include, function(id) {
+ if (-1 == mids.indexOf(id)) {
+ mids.push(id);
+ }
+ });
+ _.each(mailing.recipients.mailings.exclude, function(id) {
+ if (-1 == mids.indexOf(id)) {
+ mids.push(id);
+ }
+ });
+ if (!_.isEmpty(mids)) {
+ CRM.api3('Mailing', 'get', {'id': {"IN": mids}}).then(function(result) {
+ _.each(result.values, function(mail) {
+ if (_.isEmpty(_.where(civimails, {id: parseInt(mail.id)}))) {
+ civimails.push({id: parseInt(mail.id), name: mail.label});
+ }
+ });
+ CRM.crmMailing.civiMails = civimails;
+ $scope.$parent.crmMailingConst.civiMails = civimails;
+ });
+ }
+ }
+ }
+
+ $scope.getIncludesAsString = function(mailing) {
+ var first = true;
+ var names = '';
+ if (_.isEmpty(CRM.crmMailing.groupNames)) {
+ getGroupNames(mailing);
+ }
+ if (_.isEmpty(CRM.crmMailing.civiMails)) {
+ getCiviMails(mailing);
+ }
+ _.each(mailing.recipients.groups.include, function(id) {
+ var group = _.where(CRM.crmMailing.groupNames, {id: parseInt(id)});
+ if (group.length) {
+ if (!first) {
+ names = names + ', ';
+ }
+ names = names + group[0].title;
+ first = false;
+ }
+ });
+ _.each(mailing.recipients.mailings.include, function(id) {
+ var oldMailing = _.where(CRM.crmMailing.civiMails, {id: parseInt(id)});
+ if (oldMailing.length) {
+ if (!first) {
+ names = names + ', ';
+ }
+ names = names + oldMailing[0].name;
+ first = false;
+ }
+ });
+ return names;
+ };
+ $scope.getExcludesAsString = function(mailing) {
+ var first = true;
+ var names = '';
+ _.each(mailing.recipients.groups.exclude, function(id) {
+ var group = _.where(CRM.crmMailing.groupNames, {id: parseInt(id)});
+ if (group.length) {
+ if (!first) {
+ names = names + ', ';
+ }
+ names = names + group[0].title;
+ first = false;
+ }
+ });
+ _.each(mailing.recipients.mailings.exclude, function(id) {
+ var oldMailing = _.where(CRM.crmMailing.civiMails, {id: parseInt(id)});
+ if (oldMailing.length) {
+ if (!first) {
+ names = names + ', ';
+ }
+ names = names + oldMailing[0].name;
+ first = false;
+ }
+ });
+ return names;
+ };
+ });
+
+})(angular, CRM.$, CRM._);
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/services.js b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/services.js
new file mode 100644
index 00000000..45c20637
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailing/services.js
@@ -0,0 +1,582 @@
+(function (angular, $, _) {
+
+ // The representation of from/reply-to addresses is inconsistent in the mailing data-model,
+ // so the UI must do some adaptation. The crmFromAddresses provides a richer way to slice/dice
+ // the available "From:" addrs. Records are like the underlying OptionValues -- but add "email"
+ // and "author".
+ angular.module('crmMailing').factory('crmFromAddresses', function ($q, crmApi) {
+ var emailRegex = /^"(.*)" *<([^@>]*@[^@>]*)>$/;
+ var addrs = _.map(CRM.crmMailing.fromAddress, function (addr) {
+ var match = emailRegex.exec(addr.label);
+ return angular.extend({}, addr, {
+ email: match ? match[2] : '(INVALID)',
+ author: match ? match[1] : '(INVALID)'
+ });
+ });
+
+ function first(array) {
+ return (array.length === 0) ? null : array[0];
+ }
+
+ return {
+ getAll: function getAll() {
+ return addrs;
+ },
+ getByAuthorEmail: function getByAuthorEmail(author, email, autocreate) {
+ var result = null;
+ _.each(addrs, function (addr) {
+ if (addr.author == author && addr.email == email) {
+ result = addr;
+ }
+ });
+ if (!result && autocreate) {
+ result = {
+ label: '(INVALID) "' + author + '" <' + email + '>',
+ author: author,
+ email: email
+ };
+ addrs.push(result);
+ }
+ return result;
+ },
+ getByEmail: function getByEmail(email) {
+ return first(_.where(addrs, {email: email}));
+ },
+ getByLabel: function (label) {
+ return first(_.where(addrs, {label: label}));
+ },
+ getDefault: function getDefault() {
+ return first(_.where(addrs, {is_default: "1"}));
+ }
+ };
+ });
+
+ angular.module('crmMailing').factory('crmMsgTemplates', function ($q, crmApi) {
+ var tpls = _.map(CRM.crmMailing.mesTemplate, function (tpl) {
+ return angular.extend({}, tpl, {
+ //id: tpl parseInt(tpl.id)
+ });
+ });
+ window.tpls = tpls;
+ var lastModifiedTpl = null;
+ return {
+ // Get a template
+ // @param id MessageTemplate id (per APIv3)
+ // @return Promise MessageTemplate (per APIv3)
+ get: function get(id) {
+ return crmApi('MessageTemplate', 'getsingle', {
+ "return": "id,msg_subject,msg_html,msg_title,msg_text",
+ "id": id
+ });
+ },
+ // Save a template
+ // @param tpl MessageTemplate (per APIv3) For new templates, omit "id"
+ // @return Promise MessageTemplate (per APIv3)
+ save: function (tpl) {
+ return crmApi('MessageTemplate', 'create', tpl).then(function (response) {
+ if (!tpl.id) {
+ tpl.id = '' + response.id; //parseInt(response.id);
+ tpls.push(tpl);
+ }
+ lastModifiedTpl = tpl;
+ return tpl;
+ });
+ },
+ // @return Object MessageTemplate (per APIv3)
+ getLastModifiedTpl: function () {
+ return lastModifiedTpl;
+ },
+ getAll: function getAll() {
+ return tpls;
+ }
+ };
+ });
+
+ // The crmMailingMgr service provides business logic for loading, saving, previewing, etc
+ angular.module('crmMailing').factory('crmMailingMgr', function ($q, crmApi, crmFromAddresses, crmQueue) {
+ var qApi = crmQueue(crmApi);
+ var pickDefaultMailComponent = function pickDefaultMailComponent(type) {
+ var mcs = _.where(CRM.crmMailing.headerfooterList, {
+ component_type: type,
+ is_default: "1"
+ });
+ return (mcs.length >= 1) ? mcs[0].id : null;
+ };
+
+ return {
+ // @param scalar idExpr a number or the literal string 'new'
+ // @return Promise|Object Mailing (per APIv3)
+ getOrCreate: function getOrCreate(idExpr) {
+ return (idExpr == 'new') ? this.create() : this.get(idExpr);
+ },
+ // @return Promise Mailing (per APIv3)
+ get: function get(id) {
+ var crmMailingMgr = this;
+ var mailing;
+ return qApi('Mailing', 'getsingle', {id: id})
+ .then(function (getResult) {
+ mailing = getResult;
+ return $q.all([
+ crmMailingMgr._loadGroups(mailing),
+ crmMailingMgr._loadJobs(mailing)
+ ]);
+ })
+ .then(function () {
+ return mailing;
+ });
+ },
+ // Call MailingGroup.get and merge results into "mailing"
+ _loadGroups: function (mailing) {
+ return crmApi('MailingGroup', 'get', {mailing_id: mailing.id})
+ .then(function (groupResult) {
+ mailing.recipients = {};
+ mailing.recipients.groups = {include: [], exclude: [], base: []};
+ mailing.recipients.mailings = {include: [], exclude: []};
+ _.each(groupResult.values, function (mailingGroup) {
+ var bucket = (/^civicrm_group/.test(mailingGroup.entity_table)) ? 'groups' : 'mailings';
+ var entityId = parseInt(mailingGroup.entity_id);
+ mailing.recipients[bucket][mailingGroup.group_type.toLowerCase()].push(entityId);
+ });
+ });
+ },
+ // Call MailingJob.get and merge results into "mailing"
+ _loadJobs: function (mailing) {
+ return crmApi('MailingJob', 'get', {mailing_id: mailing.id, is_test: 0})
+ .then(function (jobResult) {
+ mailing.jobs = mailing.jobs || {};
+ angular.extend(mailing.jobs, jobResult.values);
+ });
+ },
+ // @return Object Mailing (per APIv3)
+ create: function create(params) {
+ var defaults = {
+ jobs: {}, // {jobId: JobRecord}
+ recipients: {
+ groups: {include: [], exclude: [], base: []},
+ mailings: {include: [], exclude: []}
+ },
+ template_type: "traditional",
+ // Workaround CRM-19756 w/template_options.nonce
+ template_options: {nonce: 1},
+ name: "",
+ campaign_id: null,
+ replyto_email: "",
+ subject: "",
+ body_html: "",
+ body_text: ""
+ };
+ return angular.extend({}, defaults, params);
+ },
+
+ // @param mailing Object (per APIv3)
+ // @return Promise
+ 'delete': function (mailing) {
+ if (mailing.id) {
+ return qApi('Mailing', 'delete', {id: mailing.id});
+ }
+ else {
+ var d = $q.defer();
+ d.resolve();
+ return d.promise;
+ }
+ },
+
+ // Search the body, header, and footer for required tokens.
+ // ex: var msgs = findMissingTokens(mailing, 'body_html');
+ findMissingTokens: function(mailing, field) {
+ var missing = {};
+ if (!_.isEmpty(mailing[field]) && !CRM.crmMailing.disableMandatoryTokensCheck) {
+ var body = '';
+ if (mailing.footer_id) {
+ var footer = _.where(CRM.crmMailing.headerfooterList, {id: mailing.footer_id});
+ body = body + footer[0][field];
+
+ }
+ body = body + mailing[field];
+ if (mailing.header_id) {
+ var header = _.where(CRM.crmMailing.headerfooterList, {id: mailing.header_id});
+ body = body + header[0][field];
+ }
+
+ angular.forEach(CRM.crmMailing.requiredTokens, function(value, token) {
+ if (!_.isObject(value)) {
+ if (body.indexOf('{' + token + '}') < 0) {
+ missing[token] = value;
+ }
+ }
+ else {
+ var count = 0;
+ angular.forEach(value, function(nestedValue, nestedToken) {
+ if (body.indexOf('{' + nestedToken + '}') >= 0) {
+ count++;
+ }
+ });
+ if (count === 0) {
+ angular.extend(missing, value);
+ }
+ }
+ });
+ }
+ return missing;
+ },
+
+ // Copy all data fields in (mailingFrom) to (mailingTgt) -- except for (excludes)
+ // ex: crmMailingMgr.mergeInto(newMailing, mailingTemplate, ['subject']);
+ mergeInto: function mergeInto(mailingTgt, mailingFrom, excludes) {
+ var MAILING_FIELDS = [
+ // always exclude: 'id'
+ 'name',
+ 'campaign_id',
+ 'from_name',
+ 'from_email',
+ 'replyto_email',
+ 'subject',
+ 'dedupe_email',
+ 'recipients',
+ 'body_html',
+ 'body_text',
+ 'footer_id',
+ 'header_id',
+ 'visibility',
+ 'url_tracking',
+ 'dedupe_email',
+ 'forward_replies',
+ 'auto_responder',
+ 'open_tracking',
+ 'override_verp',
+ 'optout_id',
+ 'reply_id',
+ 'resubscribe_id',
+ 'unsubscribe_id'
+ ];
+ if (!excludes) {
+ excludes = [];
+ }
+ _.each(MAILING_FIELDS, function (field) {
+ if (!_.contains(excludes, field)) {
+ mailingTgt[field] = mailingFrom[field];
+ }
+ });
+ },
+
+ // @param mailing Object (per APIv3)
+ // @return Promise an object with "subject", "body_text", "body_html"
+ preview: function preview(mailing) {
+ return this.getPreviewContent(qApi, mailing);
+ },
+
+ // @param backend
+ // @param mailing Object (per APIv3)
+ // @return preview content
+ getPreviewContent: function getPreviewContent(backend, mailing) {
+ if (CRM.crmMailing.workflowEnabled && !CRM.checkPerm('create mailings') && !CRM.checkPerm('access CiviMail')) {
+ return backend('Mailing', 'preview', {id: mailing.id}).then(function(result) {
+ return result.values;
+ });
+ }
+ else {
+ var params = angular.extend({}, mailing);
+ delete params.id;
+ return backend('Mailing', 'preview', params).then(function(result) {
+ // changes rolled back, so we don't care about updating mailing
+ return result.values;
+ });
+ }
+ },
+
+ // @param mailing Object (per APIv3)
+ // @param int previewLimit
+ // @return Promise for a list of recipients (mailing_id, contact_id, api.contact.getvalue, api.email.getvalue)
+ previewRecipients: function previewRecipients(mailing, previewLimit) {
+ // To get list of recipients, we tentatively save the mailing and
+ // get the resulting recipients -- then rollback any changes.
+ var params = angular.extend({}, mailing.recipients, {
+ id: mailing.id,
+ 'api.MailingRecipients.get': {
+ mailing_id: '$value.id',
+ options: {limit: previewLimit},
+ 'api.contact.getvalue': {'return': 'display_name'},
+ 'api.email.getvalue': {'return': 'email'}
+ }
+ });
+ delete params.scheduled_date;
+ delete params.recipients; // the content was merged in
+ return qApi('Mailing', 'create', params).then(function (recipResult) {
+ // changes rolled back, so we don't care about updating mailing
+ mailing.modified_date = recipResult.values[recipResult.id].modified_date;
+ return recipResult.values[recipResult.id]['api.MailingRecipients.get'].values;
+ });
+ },
+
+ previewRecipientCount: function previewRecipientCount(mailing, crmMailingCache, rebuild) {
+ var cachekey = 'mailing-' + mailing.id + '-recipient-count';
+ var recipientCount = crmMailingCache.get(cachekey);
+ if (rebuild || _.isEmpty(recipientCount)) {
+ // To get list of recipients, we tentatively save the mailing and
+ // get the resulting recipients -- then rollback any changes.
+ var params = angular.extend({}, mailing, mailing.recipients, {
+ id: mailing.id,
+ 'api.MailingRecipients.getcount': {
+ mailing_id: '$value.id'
+ }
+ });
+ // if this service is executed on rebuild then also fetch the recipients list
+ if (rebuild) {
+ params = angular.extend(params, {
+ 'api.MailingRecipients.get': {
+ mailing_id: '$value.id',
+ options: {limit: 50},
+ 'api.contact.getvalue': {'return': 'display_name'},
+ 'api.email.getvalue': {'return': 'email'}
+ }
+ });
+ crmMailingCache.put('mailing-' + mailing.id + '-recipient-params', params.recipients);
+ }
+ delete params.scheduled_date;
+ delete params.recipients; // the content was merged in
+ recipientCount = qApi('Mailing', 'create', params).then(function (recipResult) {
+ // changes rolled back, so we don't care about updating mailing
+ mailing.modified_date = recipResult.values[recipResult.id].modified_date;
+ if (rebuild) {
+ crmMailingCache.put('mailing-' + mailing.id + '-recipient-list', recipResult.values[recipResult.id]['api.MailingRecipients.get'].values);
+ }
+ return recipResult.values[recipResult.id]['api.MailingRecipients.getcount'];
+ });
+ crmMailingCache.put(cachekey, recipientCount);
+ }
+
+ return recipientCount;
+ },
+
+ // Save a (draft) mailing
+ // @param mailing Object (per APIv3)
+ // @return Promise
+ save: function(mailing) {
+ var params = angular.extend({}, mailing, mailing.recipients);
+
+ // Angular ngModel sometimes treats blank fields as undefined.
+ angular.forEach(mailing, function(value, key) {
+ if (value === undefined || value === null) {
+ mailing[key] = '';
+ }
+ });
+
+ // WORKAROUND: Mailing.create (aka CRM_Mailing_BAO_Mailing::create()) interprets scheduled_date
+ // as an *intent* to schedule and creates tertiary records. Saving a draft with a scheduled_date
+ // is therefore not allowed. Remove this after fixing Mailing.create's contract.
+ delete params.scheduled_date;
+
+ delete params.jobs;
+
+ delete params.recipients; // the content was merged in
+ params._skip_evil_bao_auto_recipients_ = 1; // skip recipient rebuild on simple save
+ return qApi('Mailing', 'create', params).then(function(result) {
+ if (result.id && !mailing.id) {
+ mailing.id = result.id;
+ } // no rollback, so update mailing.id
+ // Perhaps we should reload mailing based on result?
+ mailing.modified_date = result.values[result.id].modified_date;
+ return mailing;
+ });
+ },
+
+ // Schedule/send the mailing
+ // @param mailing Object (per APIv3)
+ // @return Promise
+ submit: function (mailing) {
+ var crmMailingMgr = this;
+ var params = {
+ id: mailing.id,
+ approval_date: 'now',
+ scheduled_date: mailing.scheduled_date ? mailing.scheduled_date : 'now'
+ };
+ return qApi('Mailing', 'submit', params)
+ .then(function (result) {
+ angular.extend(mailing, result.values[result.id]); // Perhaps we should reload mailing based on result?
+ return crmMailingMgr._loadJobs(mailing);
+ })
+ .then(function () {
+ return mailing;
+ });
+ },
+
+ // Immediately send a test message
+ // @param mailing Object (per APIv3)
+ // @param to Object with either key "email" (string) or "gid" (int)
+ // @return Promise for a list of delivery reports
+ sendTest: function (mailing, recipient) {
+ var params = angular.extend({}, mailing, mailing.recipients, {
+ // options: {force_rollback: 1}, // Test mailings include tracking features, so the mailing must be persistent
+ 'api.Mailing.send_test': {
+ mailing_id: '$value.id',
+ test_email: recipient.email,
+ test_group: recipient.gid
+ }
+ });
+
+ // WORKAROUND: Mailing.create (aka CRM_Mailing_BAO_Mailing::create()) interprets scheduled_date
+ // as an *intent* to schedule and creates tertiary records. Saving a draft with a scheduled_date
+ // is therefore not allowed. Remove this after fixing Mailing.create's contract.
+ delete params.scheduled_date;
+
+ delete params.jobs;
+
+ delete params.recipients; // the content was merged in
+
+ params._skip_evil_bao_auto_recipients_ = 1; // skip recipient rebuild while sending test mail
+
+ return qApi('Mailing', 'create', params).then(function (result) {
+ if (result.id && !mailing.id) {
+ mailing.id = result.id;
+ } // no rollback, so update mailing.id
+ mailing.modified_date = result.values[result.id].modified_date;
+ return result.values[result.id]['api.Mailing.send_test'].values;
+ });
+ }
+ };
+ });
+
+ // The preview manager performs preview actions while putting up a visible UI (e.g. dialogs & status alerts)
+ angular.module('crmMailing').factory('crmMailingPreviewMgr', function (dialogService, crmMailingMgr, crmStatus) {
+ return {
+ // @param mode string one of 'html', 'text', or 'full'
+ // @return Promise
+ preview: function preview(mailing, mode) {
+ var templates = {
+ html: '~/crmMailing/PreviewMgr/html.html',
+ text: '~/crmMailing/PreviewMgr/text.html',
+ full: '~/crmMailing/PreviewMgr/full.html'
+ };
+ var result = null;
+ var p = crmMailingMgr
+ .getPreviewContent(CRM.api3, mailing)
+ .then(function (content) {
+ var options = CRM.utils.adjustDialogDefaults({
+ autoOpen: false,
+ title: ts('Subject: %1', {
+ 1: content.subject
+ })
+ });
+ result = dialogService.open('previewDialog', templates[mode], content, options);
+ });
+ crmStatus({start: ts('Previewing...'), success: ''}, p);
+ return result;
+ },
+
+ // @param to Object with either key "email" (string) or "gid" (int)
+ // @return Promise
+ sendTest: function sendTest(mailing, recipient) {
+ var promise = crmMailingMgr.sendTest(mailing, recipient)
+ .then(function (deliveryInfos) {
+ var count = Object.keys(deliveryInfos).length;
+ if (count === 0) {
+ CRM.alert(ts('Could not identify any recipients. Perhaps the group is empty?'));
+ }
+ })
+ ;
+ return crmStatus({start: ts('Sending...'), success: ts('Sent')}, promise);
+ }
+ };
+ });
+
+ angular.module('crmMailing').factory('crmMailingStats', function (crmApi, crmLegacy) {
+ var statTypes = [
+ // {name: 'Recipients', title: ts('Intended Recipients'), searchFilter: '', eventsFilter: '&event=queue', reportType: 'detail', reportFilter: ''},
+ {name: 'Delivered', title: ts('Successful Deliveries'), searchFilter: '&mailing_delivery_status=Y', eventsFilter: '&event=delivered', reportType: 'detail', reportFilter: '&delivery_status_value=successful'},
+ {name: 'Opened', title: ts('Tracked Opens'), searchFilter: '&mailing_open_status=Y', eventsFilter: '&event=opened', reportType: 'opened', reportFilter: ''},
+ {name: 'Unique Clicks', title: ts('Click-throughs'), searchFilter: '&mailing_click_status=Y', eventsFilter: '&event=click&distinct=1', reportType: 'clicks', reportFilter: ''},
+ // {name: 'Forward', title: ts('Forwards'), searchFilter: '&mailing_forward=1', eventsFilter: '&event=forward', reportType: 'detail', reportFilter: '&is_forwarded_value=1'},
+ // {name: 'Replies', title: ts('Replies'), searchFilter: '&mailing_reply_status=Y', eventsFilter: '&event=reply', reportType: 'detail', reportFilter: '&is_replied_value=1'},
+ {name: 'Bounces', title: ts('Bounces'), searchFilter: '&mailing_delivery_status=N', eventsFilter: '&event=bounce', reportType: 'bounce', reportFilter: ''},
+ {name: 'Unsubscribers', title: ts('Unsubscribes'), searchFilter: '&mailing_unsubscribe=1', eventsFilter: '&event=unsubscribe', reportType: 'detail', reportFilter: '&is_unsubscribed_value=1'},
+ // {name: 'OptOuts', title: ts('Opt-Outs'), searchFilter: '&mailing_optout=1', eventsFilter: '&event=optout', reportType: 'detail', reportFilter: ''}
+ ];
+
+ return {
+ getStatTypes: function() {
+ return statTypes;
+ },
+
+ /**
+ * @param mailingIds object
+ * List of mailing IDs ({a: 123, b: 456})
+ * @return Promise
+ * List of stats for each mailing
+ * ({a: ...object..., b: ...object...})
+ */
+ getStats: function(mailingIds) {
+ var params = {};
+ angular.forEach(mailingIds, function(mailingId, name) {
+ params[name] = ['Mailing', 'stats', {mailing_id: mailingId, is_distinct: 0}];
+ });
+ return crmApi(params).then(function(result) {
+ var stats = {};
+ angular.forEach(mailingIds, function(mailingId, name) {
+ stats[name] = result[name].values[mailingId];
+ });
+ return stats;
+ });
+ },
+
+ /**
+ * Determine the legacy URL for a report about a given mailing and stat.
+ *
+ * @param mailing object
+ * @param statType object (see statTypes above)
+ * @param view string ('search', 'event', 'report')
+ * @param returnPath string|null Return path (relative to Angular base)
+ * @return string|null
+ */
+ getUrl: function getUrl(mailing, statType, view, returnPath) {
+ switch (view) {
+ case 'events':
+ var retParams = returnPath ? '&context=angPage&angPage=' + returnPath : '';
+ return crmLegacy.url('civicrm/mailing/report/event',
+ 'reset=1&mid=' + mailing.id + statType.eventsFilter + retParams);
+ case 'search':
+ return crmLegacy.url('civicrm/contact/search/advanced',
+ 'force=1&mailing_id=' + mailing.id + statType.searchFilter);
+ case 'report':
+ var reportIds = CRM.crmMailing.reportIds;
+ return crmLegacy.url('civicrm/report/instance/' + reportIds[statType.reportType],
+ 'reset=1&mailing_id_value=' + mailing.id + statType.reportFilter);
+ default:
+ return null;
+ }
+ }
+ };
+ });
+
+ // crmMailingSimpleDirective is a template/factory-function for constructing very basic
+ // directives that accept a "mailing" argument. Please don't overload it. If one continues building
+ // this, it risks becoming a second system that violates Angular architecture (and prevents one
+ // from using standard Angular docs+plugins). So this really shouldn't do much -- it is really
+ // only for simple directives. For something complex, suck it up and write 10 lines of boilerplate.
+ angular.module('crmMailing').factory('crmMailingSimpleDirective', function ($q, crmMetadata, crmUiHelp) {
+ return function crmMailingSimpleDirective(directiveName, templateUrl) {
+ return {
+ scope: {
+ crmMailing: '@'
+ },
+ templateUrl: templateUrl,
+ link: function (scope, elm, attr) {
+ scope.$parent.$watch(attr.crmMailing, function(newValue){
+ scope.mailing = newValue;
+ });
+ scope.crmMailingConst = CRM.crmMailing;
+ scope.ts = CRM.ts(null);
+ scope.hs = crmUiHelp({file: 'CRM/Mailing/MailingUI'});
+ scope[directiveName] = attr[directiveName] ? scope.$parent.$eval(attr[directiveName]) : {};
+ $q.when(crmMetadata.getFields('Mailing'), function(fields) {
+ scope.mailingFields = fields;
+ });
+ }
+ };
+ };
+ });
+
+ angular.module('crmMailing').factory('crmMailingCache', ['$cacheFactory', function($cacheFactory) {
+ return $cacheFactory('crmMailingCache');
+ }]);
+
+})(angular, CRM.$, CRM._);
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB.ang.php b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB.ang.php
new file mode 100644
index 00000000..6741399e
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB.ang.php
@@ -0,0 +1,19 @@
+<?php
+// This file declares an Angular module which can be autoloaded
+// in CiviCRM. See also:
+// http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_angularModules
+
+// ODDITY: Only loads if you have CiviMail permissions.
+// ODDITY: Extra resources loaded via CRM_Mailing_Info::getAngularModules.
+
+return [
+ 'ext' => 'civicrm',
+ 'js' => [
+ 'ang/crmMailingAB.js',
+ 'ang/crmMailingAB/*.js',
+ 'ang/crmMailingAB/*/*.js',
+ ],
+ 'css' => ['ang/crmMailingAB.css'],
+ 'partials' => ['ang/crmMailingAB'],
+ 'requires' => ['ngRoute', 'ui.utils', 'crmUi', 'crmAttachment', 'crmMailing', 'crmD3', 'crmResource'],
+];
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB.css b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB.css
new file mode 100644
index 00000000..cfee8b95
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB.css
@@ -0,0 +1,25 @@
+.crm-mailing-ab-slider .slider-test .ui-slider-range {
+ background: #5050b0;
+}
+
+.crm-mailing-ab-slider .slider-win .ui-slider-range {
+ background: #50b050;
+}
+
+.crm-mailing-ab-stats .series {
+ fill: none;
+}
+
+.crm-mailing-ab-stats .axis path, .crm-mailing-ab-stats .axis line {
+ fill: none;
+ stroke: #000;
+ shape-rendering: crispEdges;
+}
+
+.crm-mailing-ab-col {
+ width: 18em;
+}
+
+.crm-mailing-ab-table tbody tr:last-child td {
+ padding-bottom: 2em;
+}
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB.js b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB.js
new file mode 100644
index 00000000..49261ff9
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB.js
@@ -0,0 +1,44 @@
+(function(angular, $, _) {
+
+ angular.module('crmMailingAB', CRM.angRequires('crmMailingAB'));
+ angular.module('crmMailingAB').config([
+ '$routeProvider',
+ function($routeProvider) {
+ $routeProvider.when('/abtest', {
+ templateUrl: '~/crmMailingAB/ListCtrl.html',
+ controller: 'CrmMailingABListCtrl',
+ resolve: {
+ mailingABList: function($route, crmApi) {
+ return crmApi('MailingAB', 'get', {rowCount: 0});
+ },
+ fields: function(crmMetadata) {
+ return crmMetadata.getFields('MailingAB');
+ }
+ }
+ });
+ $routeProvider.when('/abtest/new', {
+ template: '<p>' + ts('Initializing...') + '</p>',
+ controller: 'CrmMailingABNewCtrl',
+ resolve: {
+ abtest: function($route, CrmMailingAB) {
+ var abtest = new CrmMailingAB(null);
+ return abtest.load().then(function() {
+ return abtest.save();
+ });
+ }
+ }
+ });
+ $routeProvider.when('/abtest/:id', {
+ templateUrl: '~/crmMailingAB/EditCtrl/main.html',
+ controller: 'CrmMailingABEditCtrl',
+ resolve: {
+ abtest: function($route, CrmMailingAB) {
+ var abtest = new CrmMailingAB($route.current.params.id == 'new' ? null : $route.current.params.id);
+ return abtest.load();
+ }
+ }
+ });
+ }
+ ]);
+
+})(angular, CRM.$, CRM._);
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB/BlockMailing.html b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB/BlockMailing.html
new file mode 100644
index 00000000..92448156
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB/BlockMailing.html
@@ -0,0 +1,196 @@
+<!--
+Required vars: abtest, fields
+
+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.
+
+This template follows a basic pattern. For each included field, there are three variants, as in this example:
+ - fromAddress: The default From: address shared by both mailings (representatively mapped to mailing A)
+ - fromAddressA: The From: address for mailing A
+ - fromAddressB: The From: address for mailing B
+Each variant is guarded with "ng-if='fields.fieldName'"; if true, the field will be displayed and
+processed by Angular; if false, the field will be hidden and completely ignored by Angular.
+-->
+<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')}" ng-if="fields.msg_template_id">
+ <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="abtest.mailings.a.msg_template_id"
+ ng-change="loadTemplate(abtest.mailings.a, abtest.mailings.a.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-click="saveTemplate(abtest.mailings.a)" class="crm-hover-button" title="{{ts('Save As')}}"></a>
+ </div>
+ </div>
+ <div crm-ui-field="{name: 'subform.msg_template_idA', title: ts('Template (A)')}" ng-if="fields.msg_template_idA">
+ <div ng-controller="MsgTemplateCtrl">
+ <select
+ crm-ui-id="subform.msg_template_idA"
+ name="msg_template_idA"
+ class="fa-clipboard"
+ crm-ui-select="{dropdownAutoWidth : true, allowClear: true, placeholder: ts('Message Template')}"
+ ng-model="abtest.mailings.a.msg_template_id"
+ ng-change="loadTemplate(abtest.mailings.a, abtest.mailings.a.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-click="saveTemplate(abtest.mailings.a)" class="crm-hover-button" title="{{ts('Save As')}}"></a>
+ </div>
+ </div>
+ <div crm-ui-field="{name: 'subform.msg_template_idB', title: ts('Template (B)')}" ng-if="fields.msg_template_idB">
+ <div ng-controller="MsgTemplateCtrl">
+ <select
+ crm-ui-id="subform.msg_template_idB"
+ name="msg_template_idB"
+ class="fa-clipboard"
+ crm-ui-select="{dropdownAutoWidth : true, allowClear: true, placeholder: ts('Message Template')}"
+ ng-model="abtest.mailings.b.msg_template_id"
+ ng-change="loadTemplate(abtest.mailings.b, abtest.mailings.b.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-click="saveTemplate(abtest.mailings.b)" 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')}" ng-if="fields.fromAddress">
+ <span ng-controller="EmailAddrCtrl" crm-mailing-from-address="fromPlaceholder" crm-mailing="abtest.mailings.a">
+ <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>
+ </span>
+ </div>
+ <div crm-ui-field="{name: 'subform.fromAddressA', title: ts('From (A)'), help: hs('from_email')}" ng-if="fields.fromAddressA">
+ <span ng-controller="EmailAddrCtrl" crm-mailing-from-address="fromPlaceholder" crm-mailing="abtest.mailings.a">
+ <select
+ crm-ui-id="subform.fromAddressA"
+ crm-ui-select="{dropdownAutoWidth : true, allowClear: false, placeholder: ts('Email address')}"
+ name="fromAddressA"
+ 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>
+ </span>
+ </div>
+ <div crm-ui-field="{name: 'subform.fromAddressB', title: ts('From (B)'), help: hs('from_email')}" ng-if="fields.fromAddressB">
+ <span ng-controller="EmailAddrCtrl" crm-mailing-from-address="fromPlaceholder" crm-mailing="abtest.mailings.b">
+ <select
+ crm-ui-id="subform.fromAddressB"
+ crm-ui-select="{dropdownAutoWidth : true, allowClear: false, placeholder: ts('Email address')}"
+ name="fromAddressB"
+ 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>
+ </span>
+ </div>
+
+
+ <div crm-ui-field="{name: 'subform.replyTo', title: ts('Reply-To')}" ng-show="crmMailingConst.enableReplyTo" ng-if="fields.replyTo">
+ <span 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(abtest.mailings.a)"
+ ng-model="abtest.mailings.a.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>
+ </span>
+ </div>
+ <div crm-ui-field="{name: 'subform.replyToA', title: ts('Reply-To (A)')}" ng-show="crmMailingConst.enableReplyTo" ng-if="fields.replyToA">
+ <span ng-controller="EmailAddrCtrl">
+ <select
+ crm-ui-id="subform.replyToA"
+ crm-ui-select="{dropdownAutoWidth : true, allowClear: true, placeholder: ts('Email address')}"
+ name="replyToA"
+ ng-change="checkReplyToChange(abtest.mailings.a)"
+ ng-model="abtest.mailings.a.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>
+ </span>
+ </div>
+ <div crm-ui-field="{name: 'subform.replyToB', title: ts('Reply-To (B)')}" ng-show="crmMailingConst.enableReplyTo" ng-if="fields.replyToB">
+ <span ng-controller="EmailAddrCtrl">
+ <select
+ crm-ui-id="subform.replyToB"
+ crm-ui-select="{dropdownAutoWidth : true, allowClear: true, placeholder: ts('Email address')}"
+ name="replyToB"
+ ng-change="checkReplyToChange(abtest.mailings.b)"
+ ng-model="abtest.mailings.b.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>
+ </span>
+ </div>
+
+
+ <div crm-ui-field="{name: 'subform.subject', title: ts('Subject')}" ng-if="fields.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="abtest.mailings.a.subject"
+ required
+ placeholder="Subject"
+ name="subject" >
+ </div>
+ <div crm-ui-field="{name: 'subform.subjectA', title: ts('Subject (A)')}" ng-if="fields.subjectA">
+ <div style="float: right;">
+ <input crm-mailing-token on-select="$broadcast('insert:subjectA', token.name)" tabindex="-1">
+ </div>
+ <input
+ crm-ui-id="subform.subjectA"
+ crm-ui-insert-rx="insert:subjectA"
+ type="text"
+ class="crm-form-text"
+ ng-model="abtest.mailings.a.subject"
+ required
+ placeholder="Subject"
+ name="subjectA" >
+ </div>
+ <div crm-ui-field="{name: 'subform.subjectB', title: ts('Subject (B)')}" ng-if="fields.subjectB">
+ <div style="float: right;">
+ <input crm-mailing-token on-select="$broadcast('insert:subjectB', token.name)" tabindex="-1">
+ </div>
+ <input
+ crm-ui-id="subform.subjectB"
+ crm-ui-insert-rx="insert:subjectB"
+ type="text"
+ class="crm-form-text"
+ ng-model="abtest.mailings.b.subject"
+ required
+ placeholder="Subject"
+ name="subjectB" >
+ </div>
+ </div>
+</div>
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB/BlockMailing.js b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB/BlockMailing.js
new file mode 100644
index 00000000..d738ab6e
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB/BlockMailing.js
@@ -0,0 +1,32 @@
+(function(angular, $, _) {
+
+ // example:
+ // scope.myAbtest = new CrmMailingAB();
+ // <crm-mailing-ab-block-mailing="{fromAddressA: 1, fromAddressB: 1}" crm-abtest="myAbtest" />
+ var simpleDirectives = {
+ crmMailingAbBlockMailing: '~/crmMailingAB/BlockMailing.html'
+ };
+ _.each(simpleDirectives, function(templateUrl, directiveName) {
+ angular.module('crmMailingAB').directive(directiveName, function($parse, crmMailingABCriteria, crmUiHelp) {
+ var scopeDesc = {crmAbtest: '@'};
+ scopeDesc[directiveName] = '@';
+
+ return {
+ scope: scopeDesc,
+ templateUrl: templateUrl,
+ link: function(scope, elm, attr) {
+ var model = $parse(attr.crmAbtest);
+ scope.abtest = model(scope.$parent);
+ scope.crmMailingConst = CRM.crmMailing;
+ scope.crmMailingABCriteria = crmMailingABCriteria;
+ scope.ts = CRM.ts(null);
+ scope.hs = crmUiHelp({file: 'CRM/Mailing/MailingUI'});
+
+ var fieldsModel = $parse(attr[directiveName]);
+ scope.fields = fieldsModel(scope.$parent);
+ }
+ };
+ });
+ });
+
+})(angular, CRM.$, CRM._);
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB/BlockSetup.html b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB/BlockSetup.html
new file mode 100644
index 00000000..7d124cc0
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB/BlockSetup.html
@@ -0,0 +1,67 @@
+<div class="crm-block" ng-form="setupForm" crm-ui-id-scope>
+ <div class="crm-group">
+ <div class="help" ng-if="fields.help">
+ {{ts('A/B testing allows you to send two test mailings to a random subset of your recipients. After collecting and comparing metrics, the more successful mailing will be sent to the remaining recipients.')}}
+ </div>
+ <div crm-ui-field="{name: 'setupForm.abName', title: ts('Name'), help: hs('name')}" ng-if="fields.abName">
+ <input type="text"
+ crm-ui-id="setupForm.abName"
+ name="abName"
+ ng-model="abtest.ab.name"
+ class="crm-form-text"
+ placeholder="A/B Test Name"
+ required/>
+ </div>
+ <div crm-ui-field="{name: 'setupForm.campaign', title: ts('Campaign'), help: hs({id: 'id-campaign_id', file: 'CRM/Campaign/Form/addCampaignToComponent'})}" ng-show="crmMailingConst.campaignEnabled"
+ ng-if="fields.campaign">
+ <input
+ crm-entityref="{entity: 'Campaign', select: {allowClear: true, placeholder: ts('Select Campaign')}}"
+ crm-ui-id="setupForm.campaign"
+ name="campaign"
+ ng-model="abtest.mailings.a.campaign_id"
+ ng-change="abtest.mailings.b.campaign_id=abtest.mailings.a.campaign_id"
+ />
+ </div>
+ <div crm-ui-field="{title: ts('Test Type')}" ng-if="fields.testing_criteria">
+ <div ng-repeat="criteria in crmMailingABCriteria.getAll()">
+ <label>
+ <input name="testing_criteria" ng-model="abtest.ab.testing_criteria" type="radio"
+ value="{{criteria.value}}" required/>
+ {{criteria.label}}
+ </label>
+ </div>
+ </div>
+ <div crm-ui-field="{name: 'setupForm.recipients', title: ts('Recipients')}" ng-if="fields.recipients">
+ <div crm-mailing-block-recipients="{name: 'recipients', id: 'setupForm.recipients'}" crm-mailing="abtest.mailings.a"></div>
+ </div>
+ <div crm-ui-field="{title: ts('Distribution')}" ng-if="fields.group_percentage">
+ <div crm-mailing-ab-slider ng-model="abtest.ab.group_percentage"></div>
+ </div>
+ <div crm-ui-field="{title: ts('Send')}" ng-if="fields.scheduled_date">
+ <div crm-mailing-radio-date="schedule" ng-model="abtest.mailings.a.scheduled_date">
+ <div>
+ <input ng-model="schedule.mode" type="radio" name="send" value="now" id="schedule-send-now"/>
+ <label for="schedule-send-now">{{ts('Send A/B test 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 A/B test at:')}}</label>
+ <input crm-ui-datepicker ng-model="schedule.datetime"/>
+ </div>
+ </div>
+ </div>
+ <div crm-ui-field="{title: ts('Assess')}" ng-if="fields.declare_winning_time">
+ <div crm-mailing-radio-date="assessSched" ng-model="abtest.ab.declare_winning_time">
+ <div>
+ <input ng-model="assessSched.mode" type="radio" name="assess" value="now" id="schedule-assess-now"/>
+ <label for="schedule-assess-now">{{ts('Assess A/B results on an on-going basis')}}</label>
+ </div>
+ <div>
+ <input ng-model="assessSched.mode" type="radio" name="assess" value="at" id="schedule-assess-at"/>
+ <label for="schedule-assess-at">{{ts('Assess A/B test at:')}}</label>
+ <input crm-ui-datepicker ng-model="assessSched.datetime"/>
+ </div>
+ </div>
+ </div>
+ </div>
+</div>
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB/BlockSetup.js b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB/BlockSetup.js
new file mode 100644
index 00000000..5809cdd0
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB/BlockSetup.js
@@ -0,0 +1,32 @@
+(function(angular, $, _) {
+
+ // example:
+ // scope.myAbtest = new CrmMailingAB();
+ // <crm-mailing-ab-block-setup="{abName: 1, group_percentage: 1}" crm-abtest="myAbtest" />
+ var simpleDirectives = {
+ crmMailingAbBlockSetup: '~/crmMailingAB/BlockSetup.html'
+ };
+ _.each(simpleDirectives, function(templateUrl, directiveName) {
+ angular.module('crmMailingAB').directive(directiveName, function($parse, crmMailingABCriteria, crmUiHelp) {
+ var scopeDesc = {crmAbtest: '@'};
+ scopeDesc[directiveName] = '@';
+
+ return {
+ scope: scopeDesc,
+ templateUrl: templateUrl,
+ link: function(scope, elm, attr) {
+ var model = $parse(attr.crmAbtest);
+ scope.abtest = model(scope.$parent);
+ scope.crmMailingConst = CRM.crmMailing;
+ scope.crmMailingABCriteria = crmMailingABCriteria;
+ scope.ts = CRM.ts(null);
+ scope.hs = crmUiHelp({file: 'CRM/Mailing/MailingUI'});
+
+ var fieldsModel = $parse(attr[directiveName]);
+ scope.fields = fieldsModel(scope.$parent);
+ }
+ };
+ });
+ });
+
+})(angular, CRM.$, CRM._);
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB/EditCtrl.js b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB/EditCtrl.js
new file mode 100644
index 00000000..b189bafc
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB/EditCtrl.js
@@ -0,0 +1,149 @@
+(function(angular, $, _) {
+
+ angular.module('crmMailingAB').controller('CrmMailingABEditCtrl', function($scope, abtest, crmMailingABCriteria, crmMailingMgr, crmMailingPreviewMgr, crmStatus, $q, $location, crmBlocker, $interval, $timeout, CrmAutosaveCtrl, dialogService) {
+ $scope.abtest = abtest;
+ var ts = $scope.ts = CRM.ts(null);
+ var block = $scope.block = crmBlocker();
+ $scope.crmUrl = CRM.url;
+ var myAutosave = null;
+ $scope.crmMailingABCriteria = crmMailingABCriteria;
+ $scope.crmMailingConst = CRM.crmMailing;
+ $scope.checkPerm = CRM.checkPerm;
+
+ $scope.isSubmitted = function isSubmitted() {
+ return _.size(abtest.mailings.a.jobs) > 0 || _.size(abtest.mailings.b.jobs) > 0;
+ };
+
+ $scope.sync = function sync() {
+ abtest.mailings.a.name = ts('Test A (%1)', {1: abtest.ab.name});
+ abtest.mailings.b.name = ts('Test B (%1)', {1: abtest.ab.name});
+ abtest.mailings.c.name = ts('Final (%1)', {1: abtest.ab.name});
+
+ if (abtest.ab.testing_criteria) {
+ // TODO review fields exposed in UI and make sure the sync rules match
+ switch (abtest.ab.testing_criteria) {
+ case 'subject':
+ var exclude_subject = [
+ 'name',
+ 'recipients',
+ 'subject'
+ ];
+ crmMailingMgr.mergeInto(abtest.mailings.b, abtest.mailings.a, exclude_subject);
+ crmMailingMgr.mergeInto(abtest.mailings.c, abtest.mailings.a, exclude_subject);
+ break;
+ case 'from':
+ var exclude_from = [
+ 'name',
+ 'recipients',
+ 'from_name',
+ 'from_email'
+ ];
+ crmMailingMgr.mergeInto(abtest.mailings.b, abtest.mailings.a, exclude_from);
+ crmMailingMgr.mergeInto(abtest.mailings.c, abtest.mailings.a, exclude_from);
+ break;
+ case 'full_email':
+ var exclude_full_email = [
+ 'name',
+ 'recipients',
+ 'subject',
+ 'from_name',
+ 'from_email',
+ 'replyto_email',
+ 'override_verp', // keep override_verp and replyto_Email linked
+ 'body_html',
+ 'body_text'
+ ];
+ crmMailingMgr.mergeInto(abtest.mailings.b, abtest.mailings.a, exclude_full_email);
+ crmMailingMgr.mergeInto(abtest.mailings.c, abtest.mailings.a, exclude_full_email);
+ break;
+ default:
+ throw "Unrecognized testing_criteria";
+ }
+ }
+ return true;
+ };
+
+ // @return Promise
+ $scope.save = function save() {
+ return block(crmStatus({start: ts('Saving...'), success: ts('Saved')}, abtest.save()));
+ };
+
+ // @return Promise
+ $scope.previewMailing = function previewMailing(mailingName, mode) {
+ return crmMailingPreviewMgr.preview(abtest.mailings[mailingName], mode);
+ };
+
+ // @return Promise
+ $scope.sendTest = function sendTest(mailingName, recipient) {
+ return block(crmStatus({start: ts('Saving...'), success: ''}, abtest.save())
+ .then(function() {
+ crmMailingPreviewMgr.sendTest(abtest.mailings[mailingName], recipient);
+ }));
+ };
+
+ // @return Promise
+ $scope.delete = function() {
+ return block(crmStatus({start: ts('Deleting...'), success: ts('Deleted')}, abtest.delete().then($scope.leave)));
+ };
+
+ // @return Promise
+ $scope.submit = function submit() {
+ if (block.check() || $scope.crmMailingAB.$invalid) {
+ return;
+ }
+ return block(crmStatus({start: ts('Saving...'), success: ''}, abtest.save())
+ .then(function() {
+ return crmStatus({
+ start: ts('Submitting...'),
+ success: ts('Submitted')
+ }, myAutosave.suspend(abtest.submitTest()));
+ // Note: We're going to leave, so we don't care that submit() modifies several server-side records.
+ // If we stayed on this page, then we'd care about updating and call: abtest.submitTest().then(...abtest.load()...)
+ })
+ );
+ };
+
+ $scope.leave = function leave() {
+ $location.path('abtest');
+ $location.replace();
+ };
+
+ $scope.selectWinner = function selectWinner(mailingName) {
+ var model = {
+ abtest: $scope.abtest,
+ mailingName: mailingName
+ };
+ var options = CRM.utils.adjustDialogDefaults({
+ autoOpen: false,
+ height: 'auto',
+ width: '40%',
+ title: ts('Select Final Mailing (Test %1)', {
+ 1: mailingName.toUpperCase()
+ })
+ });
+ return myAutosave.suspend(dialogService.open('selectWinnerDialog', '~/crmMailingAB/WinnerDialogCtrl.html', model, options));
+ };
+
+ // initialize
+ var syncJob = $interval($scope.sync, 333);
+ $scope.$on('$destroy', function() {
+ $interval.cancel(syncJob);
+ });
+
+ myAutosave = new CrmAutosaveCtrl({
+ save: $scope.save,
+ saveIf: function() {
+ return abtest.ab.status == 'Draft' && $scope.sync();
+ },
+ model: function() {
+ return abtest.getAutosaveSignature();
+ },
+ form: function() {
+ return $scope.crmMailingAB;
+ }
+ });
+ $timeout(myAutosave.start);
+ $scope.$on('$destroy', myAutosave.stop);
+ });
+
+})(angular, CRM.$, CRM._);
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB/EditCtrl/edit.html b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB/EditCtrl/edit.html
new file mode 100644
index 00000000..825f3a57
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB/EditCtrl/edit.html
@@ -0,0 +1,178 @@
+<!--
+ Implicit Controller: CrmMailingABEditCtrl
+
+ An ABTest includes two mailings, but we don't require the user to enter two complete mailings. For
+ simplicity, the email composition UI generally displays A (unless we specifically decided to expose an
+ individual field from B). At the end of the composition process, the controller's "sync" operation will
+ merge shared settings from "A" into "B".
+-->
+<div ng-form="crmMailingABEdit">
+ <div class="crm-block crm-form-block crmMailing">
+ <div crm-ui-wizard>
+ <div crm-ui-wizard-step="10" crm-title="ts('Setup')" ng-form="setupForm">
+ <div
+ crm-mailing-ab-block-setup="{
+ help: 1,
+ abName: 1,
+ campaign: 1,
+ testing_criteria: 1
+ }"
+ crm-abtest="abtest"></div>
+ </div>
+ <div crm-ui-wizard-step="11" crm-title="ts('Target')" ng-form="targetForm">
+ <div
+ crm-mailing-ab-block-setup="{
+ recipients: 1,
+ group_percentage: 1
+ }"
+ crm-abtest="abtest"></div>
+ </div>
+ <div crm-ui-wizard-step="20" crm-title="ts('Compose')" ng-if="abtest.ab.testing_criteria != 'full_email'" ng-form="composeForm">
+ <div crm-ui-tab-set>
+ <div crm-ui-tab id="tab-mailing" crm-title="ts('Mailing')">
+ <div
+ ng-if="abtest.ab.testing_criteria == 'from'"
+ crm-mailing-ab-block-mailing="{
+ msg_template_id: 1,
+ fromAddressA: 1,
+ fromAddressB: 1,
+ subject: 1
+ }"
+ crm-abtest="abtest"></div>
+ <div
+ ng-if="abtest.ab.testing_criteria == 'subject'"
+ crm-mailing-ab-block-mailing="{
+ msg_template_id: 1,
+ fromAddress: 1,
+ replyTo: 1,
+ subjectA: 1,
+ subjectB: 1
+ }"
+ crm-abtest="abtest"></div>
+ <div crm-ui-accordion="{title: ts('HTML')}">
+ <div crm-mailing-body-html crm-mailing="abtest.mailings.a"></div>
+ </div>
+ <div crm-ui-accordion="{title: ts('Plain Text'), collapsed: !abtest.mailings.a.body_text}">
+ <div crm-mailing-body-text crm-mailing="abtest.mailings.a"></div>
+ </div>
+ </div>
+ <!--
+ FIXME: Attachment UI works, but we haven't implemented backend logic for copying/sharing
+ of attachments among mailings A/B/C.
+ <div crm-ui-tab id="tab-attachment" crm-title="ts('Attachments')">
+ <div crm-attachments="abtest.attachments.a"></div>
+ </div>
+ -->
+ <div crm-ui-tab id="tab-header" crm-title="ts('Header and Footer')">
+ <div crm-mailing-block-header-footer crm-mailing="abtest.mailings.a"></div>
+ </div>
+ <div crm-ui-tab id="tab-pub" crm-title="ts('Publication')">
+ <div crm-mailing-block-publication crm-mailing="abtest.mailings.a"></div>
+ </div>
+ <div crm-ui-tab id="tab-response" crm-title="ts('Responses')">
+ <div crm-mailing-block-responses crm-mailing="abtest.mailings.a"></div>
+ </div>
+ </div>
+ <div crm-ui-accordion="{title: ts('Preview (A)')}">
+ <div crm-mailing-block-preview crm-mailing="abtest.mailings.a" on-preview="previewMailing('a', preview.mode)" on-send="sendTest('a', preview.recipient)"></div>
+ </div>
+ <div crm-ui-accordion="{title: ts('Preview (B)')}">
+ <div crm-mailing-block-preview crm-mailing="abtest.mailings.b" on-preview="previewMailing('b', preview.mode)" on-send="sendTest('b', preview.recipient)"></div>
+ </div>
+ </div>
+ <div crm-ui-wizard-step="21" crm-title="ts('Compose (A)')" ng-if="abtest.ab.testing_criteria == 'full_email'" ng-form="composeAForm">
+ <div crm-ui-tab-set>
+ <div crm-ui-tab id="tab-mailingA" crm-title="ts('Mailing')">
+ <div
+ crm-mailing-ab-block-mailing="{
+ msg_template_idA: 1,
+ fromAddressA: 1,
+ replyToA: 1,
+ subjectA: 1
+ }"
+ crm-abtest="abtest"></div>
+ <div crm-ui-accordion="{title: ts('HTML')}">
+ <div crm-mailing-body-html crm-mailing="abtest.mailings.a"></div>
+ </div>
+ <div crm-ui-accordion="{title: ts('Plain Text'), collapsed: !abtest.mailings.a.body_text}">
+ <div crm-mailing-body-text crm-mailing="abtest.mailings.a"></div>
+ </div>
+ </div>
+ <div crm-ui-tab id="tab-attachmentA" crm-title="ts('Attachments')">
+ <div crm-attachments="abtest.attachments.a"></div>
+ </div>
+ <div crm-ui-tab id="tab-headerA" crm-title="ts('Header and Footer')">
+ <div crm-mailing-block-header-footer crm-mailing="abtest.mailings.a"></div>
+ </div>
+ <div crm-ui-tab id="tab-pubA" crm-title="ts('Publication')">
+ <div crm-mailing-block-publication crm-mailing="abtest.mailings.a"></div>
+ </div>
+ <div crm-ui-tab id="tab-responseA" crm-title="ts('Responses')">
+ <div crm-mailing-block-responses crm-mailing="abtest.mailings.a"></div>
+ </div>
+ </div>
+ <div crm-ui-accordion="{title: ts('Preview')}">
+ <div crm-mailing-block-preview crm-mailing="abtest.mailings.a" on-preview="previewMailing('a', preview.mode)" on-send="sendTest('a', preview.recipient)"></div>
+ </div>
+ </div>
+ <div crm-ui-wizard-step="22" crm-title="ts('Compose (B)')" ng-if="abtest.ab.testing_criteria == 'full_email'" ng-form="composeBForm">
+ <div crm-ui-tab-set>
+ <div crm-ui-tab id="tab-mailingB" crm-title="ts('Mailing')">
+ <div
+ crm-mailing-ab-block-mailing="{
+ msg_template_idB: 1,
+ fromAddressB: 1,
+ replyToB: 1,
+ subjectB: 1
+ }"
+ crm-abtest="abtest"></div>
+ <div crm-ui-accordion="{title: ts('HTML')}">
+ <div crm-mailing-body-html crm-mailing="abtest.mailings.b"></div>
+ </div>
+ <div crm-ui-accordion="{title: ts('Plain Text'), collapsed: !abtest.mailings.b.body_text}">
+ <div crm-mailing-body-text crm-mailing="abtest.mailings.b"></div>
+ </div>
+ </div>
+ <div crm-ui-tab id="tab-attachmentB" crm-title="ts('Attachments')">
+ <div crm-attachments="abtest.attachments.b"></div>
+ </div>
+ <div crm-ui-tab id="tab-headerB" crm-title="ts('Header and Footer')">
+ <div crm-mailing-block-header-footer crm-mailing="abtest.mailings.b"></div>
+ </div>
+ <div crm-ui-tab id="tab-pubB" crm-title="ts('Publication')">
+ <div crm-mailing-block-publication crm-mailing="abtest.mailings.b"></div>
+ </div>
+ <div crm-ui-tab id="tab-responseB" crm-title="ts('Responses')">
+ <div crm-mailing-block-responses crm-mailing="abtest.mailings.b"></div>
+ </div>
+ </div>
+ <div crm-ui-accordion="{title: ts('Preview')}">
+ <div crm-mailing-block-preview crm-mailing="abtest.mailings.b" on-preview="previewMailing('b', preview.mode)" on-send="sendTest('b', preview.recipient)"></div>
+ </div>
+ </div>
+ <div crm-ui-wizard-step="30" crm-title="ts('Schedule')" ng-form="schedForm">
+ <div
+ crm-mailing-ab-block-setup="{
+ scheduled_date: 1,
+ declare_winning_time: 1
+ }"
+ crm-abtest="abtest"></div>
+ <center>
+ <a class="button crmMailing-submit-button" ng-click="submit()" ng-class="{blocking: block.check(), disabled: crmMailingAB.$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>
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB/EditCtrl/main.html b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB/EditCtrl/main.html
new file mode 100644
index 00000000..15822f6e
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB/EditCtrl/main.html
@@ -0,0 +1,10 @@
+<!--
+ Implicit Controller: CrmMailingABEditCtrl
+-->
+<div crm-ui-debug="abtest.ab"></div>
+<div crm-ui-debug="abtest.mailings"></div>
+
+<form name="crmMailingAB" novalidate>
+ <div ng-include="'~/crmMailingAB/EditCtrl/edit.html'" ng-if="!isSubmitted()"></div>
+ <div ng-include="'~/crmMailingAB/EditCtrl/report.html'" ng-if="isSubmitted()"></div>
+</form>
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB/EditCtrl/report.html b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB/EditCtrl/report.html
new file mode 100644
index 00000000..68b79245
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB/EditCtrl/report.html
@@ -0,0 +1,194 @@
+<!--
+ Implicit Controller: CrmMailingABEditCtrl
+-->
+<div class="messages help">
+ <div class="msg-title crm-title">{{ts('A/B Test Results')}}: {{abtest.ab.name}}</div>
+ {{ts('This report displays the current results for your A/B test. You can return to this page to view the latest statistics by navigating to "Manage A/B Tests" and clicking "Results".')}}
+</div>
+<div ng-controller="CrmMailingABReportCtrl">
+ <table class="crm-mailing-ab-table">
+ <thead>
+ <tr ng-show="abtest.ab.status == 'Testing'">
+ <td></td>
+ <td ng-repeat="am in getActiveMailings()">
+ <button crm-icon="fa-trophy" ng-click="selectWinner(am.name)">{{ts('Select as Final')}}</button>
+ </td>
+ <td></td>
+ </tr>
+ </thead>
+
+ <thead>
+ <tr>
+ <th>{{ts('Delivery')}}</th>
+ <th ng-repeat="am in getActiveMailings()" class="crm-mailing-ab-col">{{am.title}}</th>
+ <th ng-show="abtest.ab.status == 'Testing'">{{ts('Final')}}</th>
+ </tr>
+ </thead>
+
+ <tbody>
+ <tr>
+ <td>{{ts('Status')}}</td>
+ <td ng-repeat="am in getActiveMailings()">
+ <span ng-repeat="job in am.mailing.jobs" ng-hide="job.is_test == 1 || job.parent_id != null">{{job.status}}</span>
+ </td>
+ <td ng-show="abtest.ab.status == 'Testing'">{{ts('Not selected')}}</td>
+ </tr>
+ <tr>
+ <td>{{ts('Scheduled')}}</td>
+ <td ng-repeat="am in getActiveMailings()">
+ <div ng-repeat="job in am.mailing.jobs" ng-hide="job.is_test == 1 || job.parent_id != null">{{job.scheduled_date}}</div>
+ </td>
+ <td ng-show="abtest.ab.status == 'Testing'"></td>
+ </tr>
+ <tr>
+ <td>{{ts('Started at')}}</td>
+ <td ng-repeat="am in getActiveMailings()">
+ <div ng-repeat="job in am.mailing.jobs" ng-hide="job.is_test == 1 || job.parent_id != null">{{job.start_date || ts('Not started')}}</div>
+ </td>
+ <td ng-show="abtest.ab.status == 'Testing'"></td>
+ </tr>
+ <tr>
+ <td>{{ts('Completed at')}}</td>
+ <td ng-repeat="am in getActiveMailings()">
+ <div ng-repeat="job in am.mailing.jobs" ng-hide="job.is_test == 1 || job.parent_id != null">{{job.end_date || ts('Not completed')}}</div>
+ </td>
+ <td ng-show="abtest.ab.status == 'Testing'"></td>
+ </tr>
+ </tbody>
+
+ <thead>
+ <tr>
+ <th>{{ts('Performance')}}</th>
+ <th ng-repeat="am in getActiveMailings()" class="crm-mailing-ab-col">{{am.title}}</th>
+ <th ng-show="abtest.ab.status == 'Testing'">{{ts('Final')}}</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr ng-repeat="statType in statTypes">
+ <td>{{statType.title}}</td>
+ <td ng-repeat="am in getActiveMailings()">
+ <a
+ class="crm-hover-button action-item"
+ ng-href="{{statUrl(am.mailing, statType, 'search')}}"
+ ng-if="checkPerm('view all contacts') || checkPerm('edit all contacts')"
+ title="{{ts('Search for contacts using \'%1\'', {1: statType.title})}}"
+ crm-icon="fa-search"
+ ></a>
+ <a
+ class="crm-hover-button action-item"
+ ng-href="{{statUrl(am.mailing, statType, 'events')}}"
+ title="{{ts('Browse events of type \'%1\'', {1: statType.title})}}"
+ >{{stats[am.name][statType.name] || ts('n/a')}} </a> {{stats[am.name][rateStats[statType.name]] || ' '}}
+ <a
+ class="crm-hover-button action-item"
+ ng-href="{{statUrl(am.mailing, statType, 'report')}}"
+ title="{{ts('Reports for \'%1\'', {1: statType.title})}}"
+ crm-icon="clipboard"
+ ></a>
+ </td>
+ <td ng-show="abtest.ab.status == 'Testing'"></td>
+ </tr>
+ </tbody>
+
+ <thead>
+ <tr>
+ <th>{{ts('Details')}}</th>
+ <th ng-repeat="am in getActiveMailings()" class="crm-mailing-ab-col">{{am.title}}</th>
+ <th ng-show="abtest.ab.status == 'Testing'">{{ts('Final')}}</th>
+ </tr>
+ </thead>
+
+ <tbody>
+ <tr>
+ <td>{{ts('Mailing Name')}}</td>
+ <td ng-repeat="am in getActiveMailings()">
+ {{am.mailing.name}}
+ </td>
+ <td ng-show="abtest.ab.status == 'Testing'"></td>
+ </tr>
+ <tr>
+ <td>{{ts('From')}}</td>
+ <td ng-repeat="am in getActiveMailings()">
+ "{{am.mailing.from_name}}" &lt;{{am.mailing.from_email}}&gt;
+ </td>
+ <td ng-show="abtest.ab.status == 'Testing'"></td>
+ </tr>
+ <tr>
+ <td>{{ts('Subject')}}</td>
+ <td ng-repeat="am in getActiveMailings()">
+ {{am.mailing.subject}}
+ </td>
+ <td ng-show="abtest.ab.status == 'Testing'"></td>
+ </tr>
+ <tr ng-controller="ViewRecipCtrl">
+ <td>{{ts('Recipients')}}</td>
+ <td ng-repeat="am in getActiveMailings()">
+ <div ng-show="getIncludesAsString(am.mailing)">
+ <strong>{{ts('Include:')}}</strong> {{getIncludesAsString(am.mailing)}}
+ </div>
+ <div ng-show="getExcludesAsString(am.mailing)">
+ <strong>{{ts('Exclude:')}}</strong> <s>{{getExcludesAsString(am.mailing)}}</s>
+ </div>
+ </td>
+ <td ng-show="abtest.ab.status == 'Testing'"></td>
+ </tr>
+ <tr>
+ <td>{{ts('Content')}}</td>
+ <td ng-repeat="am in getActiveMailings()">
+ <a crm-icon="fa-television" class="crm-hover-button action-item" ng-click="previewMailing(am.name,'html')" ng-show="am.mailing.body_html">{{ts('HTML')}}</a>
+ <a crm-icon="fa-file-text-o" class="crm-hover-button action-item" ng-click="previewMailing(am.name,'text')" ng-show="am.mailing.body_text">{{ts('Text')}}</a>
+ </td>
+ <td ng-show="abtest.ab.status == 'Testing'"></td>
+ </tr>
+ <tr>
+ <td>{{ts('Attachments')}}</td>
+ <td ng-repeat="am in getActiveMailings()">
+ <div ng-repeat="file in am.attachments.files"><a ng-href="{{file.url}}" target="_blank">{{file.name}}</a></div>
+ </td>
+ <td ng-show="abtest.ab.status == 'Testing'"></td>
+ </tr>
+ <tr>
+ <td>{{ts('Tracking')}}</td>
+ <td ng-repeat="am in getActiveMailings()">
+ <div crm-mailing-review-bool crm-on="am.mailing.url_tracking=='1'" crm-title="ts('Click-Throughs')"></div>
+ <div crm-mailing-review-bool crm-on="am.mailing.open_tracking=='1'" crm-title="ts('Opens')"></div>
+ </td>
+ <td ng-show="abtest.ab.status == 'Testing'"></td>
+ </tr>
+ <tr>
+ <td>{{ts('Responding')}}</td>
+ <td ng-repeat="am in getActiveMailings()">
+ <div crm-mailing-review-bool crm-on="am.mailing.override_verp=='0'" crm-title="ts('Track Replies')"></div>
+ <div crm-mailing-review-bool crm-on="am.mailing.override_verp=='0' && mailing.forward_replies=='1'" crm-title="ts('Forward Replies')"></div>
+ <div ng-controller="PreviewComponentCtrl">
+ <div ng-show="am.mailing.override_verp == '0' && mailing.auto_responder"><a crm-icon="fa-envelope" class="crm-hover-button action-item" ng-click="previewComponent(ts('Auto-Respond'), am.mailing.reply_id)">{{ts('Auto-Respond')}}</a></div>
+ <div><a crm-icon="fa-envelope" class="crm-hover-button action-item" ng-click="previewComponent(ts('Opt-out'), am.mailing.optout_id)">{{ts('Opt-out')}}</a></div>
+ <div><a crm-icon="fa-envelope" class="crm-hover-button action-item" ng-click="previewComponent(ts('Resubscribe'), am.mailing.resubscribe_id)">{{ts('Resubscribe')}}</a></div>
+ <div><a crm-icon="fa-envelope" class="crm-hover-button action-item" ng-click="previewComponent(ts('Unsubscribe'), am.mailing.unsubscribe_id)">{{ts('Unsubscribe')}}</a></div>
+ </div>
+ </td>
+ <td ng-show="abtest.ab.status == 'Testing'"></td>
+ </tr>
+ <tr>
+ <td>{{ts('Publication')}}</td>
+ <td ng-repeat="am in getActiveMailings()">
+ {{am.mailing.visibility}}
+ </td>
+ <td ng-show="abtest.ab.status == 'Testing'"></td>
+ </tr>
+ </tbody>
+
+ </table>
+
+ <!--
+ <div crm-ui-tab-set>
+ <div crm-ui-tab id="tab-opens" crm-title="ts('Opens (WIP)')">
+ <div crm-mailing-ab-stats="{criteria: 'open', split_count: 5}" crm-abtest="abtest"></div>
+ </div>
+ <div crm-ui-tab id="tab-clicks" crm-title="ts('Total Clicks (WIP)')">
+ <div crm-mailing-ab-stats="{criteria: 'total unique clicks', split_count: 5}" crm-abtest="abtest"></div>
+ </div>
+ </div>
+ -->
+
+</div>
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB/ListCtrl.html b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB/ListCtrl.html
new file mode 100644
index 00000000..5d2c0768
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB/ListCtrl.html
@@ -0,0 +1,63 @@
+<!--
+Controller: ABListingCtrl
+Required vars: mailingABList
+-->
+
+<span crm-ui-order="{var: 'myOrder', defaults: ['-created_date']}"></span>
+
+<div crm-ui-accordion="{title: ts('Filter'), collapsed: true}">
+ <form name="filterForm">
+ <span>
+ <input class="big crm-form-text" ng-model="filter.name" placeholder="{{ts('Name')}}"/>
+ </span>
+ <span>
+ <select crm-ui-select style="width: 10em;" ng-model="filter.status">
+ <option value="">{{ts('- Status -')}}</option>
+ <option ng-repeat="o in fields.status.options" ng-value="o.key">{{o.value}}</option>
+ </select>
+ </span>
+ <span>
+ <select crm-ui-select style="width: 20em;" ng-model="filter.testing_criteria">
+ <option value="">{{ts('- Test Type -')}}</option>
+ <option ng-repeat="o in fields.testing_criteria.options" ng-value="o.key">{{o.value}}</option>
+ </select>
+ </span>
+ </form>
+</div>
+
+<div ng-show="mailingABList.length">
+ <table class="display">
+ <thead>
+ <tr>
+ <th><a crm-ui-order-by="[myOrder, 'name']">{{ts('Name')}}</a></th>
+ <th><a crm-ui-order-by="[myOrder, 'status']">{{ts('Status')}}</a></th>
+ <th><a crm-ui-order-by="[myOrder, 'testing_criteria']">{{ts('Test Type')}}</a></th>
+ <th><a crm-ui-order-by="[myOrder, 'created_date']">{{ts('Created')}}</a></th>
+ <th></th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr ng-repeat="mailingAB in mailingABList | filter:filter | orderBy:myOrder.get()">
+ <td>{{mailingAB.name}}</td>
+ <td>{{crmMailingABStatus.getByName(mailingAB.status).label}}</td>
+ <td>{{crmMailingABCriteria.get(mailingAB.testing_criteria).label}}</td>
+ <td>{{mailingAB.created_date}}</td>
+ <td>
+ <a class="action-item crm-hover-button" ng-href="#/abtest/{{mailingAB.id}}" ng-show="mailingAB.status == 'Draft'">{{ts('Continue')}}</a>
+ <a class="action-item crm-hover-button" ng-href="#/abtest/{{mailingAB.id}}" ng-show="mailingAB.status != 'Draft'">{{ts('Results')}}</a>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+</div>
+
+<div ng-show="mailingABList.length === 0" class="messages status no-popup">
+ <i class="crm-i fa-info-circle"></i>
+ {{ts('You have no A/B mailings')}}
+</div>
+
+
+<div class="crm-submit-buttons">
+ <br>
+ <a ng-href="#/abtest/new" class="button"><span><i class="crm-i fa-bar-chart"></i> {{ts('New A/B Test')}}</span></a>
+</div>
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB/ListCtrl.js b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB/ListCtrl.js
new file mode 100644
index 00000000..d0ced773
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB/ListCtrl.js
@@ -0,0 +1,12 @@
+(function(angular, $, _) {
+
+ angular.module('crmMailingAB').controller('CrmMailingABListCtrl', function($scope, mailingABList, crmMailingABCriteria, crmMailingABStatus, fields) {
+ var ts = $scope.ts = CRM.ts(null);
+ $scope.mailingABList = _.values(mailingABList.values);
+ $scope.crmMailingABCriteria = crmMailingABCriteria;
+ $scope.crmMailingABStatus = crmMailingABStatus;
+ $scope.fields = fields;
+ $scope.filter = {};
+ });
+
+})(angular, CRM.$, CRM._);
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB/NewCtrl.js b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB/NewCtrl.js
new file mode 100644
index 00000000..edcfa705
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB/NewCtrl.js
@@ -0,0 +1,11 @@
+(function(angular, $, _) {
+
+ angular.module('crmMailingAB').controller('CrmMailingABNewCtrl', function($scope, abtest, $location) {
+ // Transition URL "/abtest/new/foo" => "/abtest/123/foo"
+ var parts = $location.path().split('/'); // e.g. "/mailing/new" or "/mailing/123/wizard"
+ parts[2] = abtest.id;
+ $location.path(parts.join('/'));
+ $location.replace();
+ });
+
+})(angular, CRM.$, CRM._);
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB/ReportCtrl.js b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB/ReportCtrl.js
new file mode 100644
index 00000000..8755945e
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB/ReportCtrl.js
@@ -0,0 +1,56 @@
+(function(angular, $, _) {
+
+ angular.module('crmMailingAB').controller('CrmMailingABReportCtrl', function($scope, crmApi, crmMailingStats) {
+ var ts = $scope.ts = CRM.ts(null);
+
+ var CrmMailingABReportCnt = 1, activeMailings = null;
+ $scope.getActiveMailings = function() {
+ if ($scope.abtest.$CrmMailingABReportCnt != CrmMailingABReportCnt) {
+ $scope.abtest.$CrmMailingABReportCnt = ++CrmMailingABReportCnt;
+ activeMailings = [
+ {
+ name: 'a',
+ title: ts('Mailing A'),
+ mailing: $scope.abtest.mailings.a,
+ attachments: $scope.abtest.attachments.a
+ },
+ {
+ name: 'b',
+ title: ts('Mailing B'),
+ mailing: $scope.abtest.mailings.b,
+ attachments: $scope.abtest.attachments.b
+ }
+ ];
+ if ($scope.abtest.ab.status == 'Final') {
+ activeMailings.push({
+ name: 'c',
+ title: ts('Final'),
+ mailing: $scope.abtest.mailings.c,
+ attachments: $scope.abtest.attachments.c
+ });
+ }
+ }
+ return activeMailings;
+ };
+
+ crmMailingStats.getStats({
+ a: $scope.abtest.ab.mailing_id_a,
+ b: $scope.abtest.ab.mailing_id_b,
+ c: $scope.abtest.ab.mailing_id_c
+ }).then(function(stats) {
+ $scope.stats = stats;
+ });
+ $scope.rateStats = {
+ 'Unique Clicks': 'clickthrough_rate',
+ 'Delivered': 'delivered_rate',
+ 'Opened': 'opened_rate',
+ };
+ $scope.statTypes = crmMailingStats.getStatTypes();
+ $scope.statUrl = function statUrl(mailing, statType, view) {
+ return crmMailingStats.getUrl(mailing, statType, view, 'abtest/' + $scope.abtest.ab.id);
+ };
+
+ $scope.checkPerm = CRM.checkPerm;
+ });
+
+})(angular, CRM.$, CRM._);
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB/Slider.html b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB/Slider.html
new file mode 100644
index 00000000..cdfcad3b
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB/Slider.html
@@ -0,0 +1,25 @@
+<table class="crm-mailing-ab-slider">
+ <tbody>
+ <tr>
+ <td style="width: 10em;">{{ts('Test Mailing A')}}</td>
+ <td>
+ <div class="slider-test slider-a"></div>
+ </td>
+ <td style="width: 5em;">({{testValue}}%)</td>
+ </tr>
+ <tr>
+ <td>{{ts('Test Mailing B')}}</td>
+ <td>
+ <div class="slider-test slider-b"></div>
+ </td>
+ <td>({{testValue}}%)</td>
+ </tr>
+ </tbody>
+ <tr>
+ <td>{{ts('Final Mailing')}}</td>
+ <td>
+ <div class="slider-win slider-b"></div>
+ </td>
+ <td>({{winValue}}%)</td>
+ </tr>
+</table>
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB/Slider.js b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB/Slider.js
new file mode 100644
index 00000000..d26e35b1
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB/Slider.js
@@ -0,0 +1,60 @@
+(function(angular, $, _) {
+
+ // example: <div crm-mailing-ab-slider ng-model="abtest.ab.group_percentage"></div>
+ angular.module('crmMailingAB').directive('crmMailingAbSlider', function() {
+ return {
+ require: '?ngModel',
+ scope: {},
+ templateUrl: '~/crmMailingAB/Slider.html',
+ link: function(scope, element, attrs, ngModel) {
+ var TEST_MIN = 1, TEST_MAX = 50;
+ var sliders = $('.slider-test,.slider-win', element);
+ var sliderTests = $('.slider-test', element);
+ var sliderWin = $('.slider-win', element);
+
+ scope.ts = CRM.ts(null);
+ scope.testValue = 0;
+ scope.winValue = 100;
+
+ // set the base value (following a GUI event)
+ function setValue(value) {
+ value = Math.min(TEST_MAX, Math.max(TEST_MIN, value));
+ scope.$apply(function() {
+ ngModel.$setViewValue(value);
+ scope.testValue = value;
+ scope.winValue = 100 - (2 * scope.testValue);
+ sliderTests.slider('value', scope.testValue);
+ sliderWin.slider('value', scope.winValue);
+ });
+ }
+
+ sliders.slider({
+ min: 0,
+ max: 100,
+ range: 'min',
+ step: 1
+ });
+ sliderTests.slider({
+ slide: function slideTest(event, ui) {
+ event.preventDefault();
+ setValue(ui.value);
+ }
+ });
+ sliderWin.slider({
+ slide: function slideWinner(event, ui) {
+ event.preventDefault();
+ setValue(Math.round((100 - ui.value) / 2));
+ }
+ });
+
+ ngModel.$render = function() {
+ scope.testValue = ngModel.$viewValue;
+ scope.winValue = 100 - (2 * scope.testValue);
+ sliderTests.slider('value', scope.testValue);
+ sliderWin.slider('value', scope.winValue);
+ };
+ }
+ };
+ });
+
+})(angular, CRM.$, CRM._);
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB/Stats.js b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB/Stats.js
new file mode 100644
index 00000000..da2ebb63
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB/Stats.js
@@ -0,0 +1,280 @@
+(function (angular, $, _) {
+
+
+ // FIXME: This code is long and hasn't been fully working for me, but I've moved it into a spot
+ // where it at least fits in a bit better.
+
+ // example: <div crm-mailing-ab-stats="{split_count: 6, criteria:'Open'}" crm-abtest="myabtest" />
+ // options (see also: Mailing.graph_stats API)
+ // - split_count: int
+ // - criteria: string
+ // - target_date: string, date
+ // - target_url: string
+ angular.module('crmMailingAB').directive('crmMailingAbStats', function (crmApi, $parse) {
+ return {
+ scope: {
+ crmMailingAbStats: '@',
+ crmAbtest: '@'
+ },
+ template: '<div class="crm-mailing-ab-stats"></div>',
+ link: function (scope, element, attrs) {
+ var abtestModel = $parse(attrs.crmAbtest);
+ var optionModel = $parse(attrs.crmMailingAbStats);
+ var options = angular.extend({}, optionModel(scope.$parent), {
+ criteria: 'Open', // e.g. 'Open', 'Total Unique Clicks'
+ split_count: 5
+ });
+
+ scope.$watch(attrs.crmAbtest, refresh);
+ function refresh() {
+ var abtest = abtestModel(scope.$parent);
+ if (!abtest) {
+ console.log('failed to draw stats - missing abtest');
+ return;
+ }
+
+ scope.graph_data = [
+ {},
+ {},
+ {},
+ {},
+ {}
+ ];
+ var keep_cnt = 0;
+
+ for (var i = 1; i <= options.split_count; i++) {
+ var result = crmApi('MailingAB', 'graph_stats', {
+ id: abtest.ab.id,
+ target_date: abtest.ab.declare_winning_time ? abtest.ab.declare_winning_time : 'now',
+ target_url: null, // FIXME
+ criteria: options.criteria,
+ split_count: options.split_count,
+ split_count_select: i
+ });
+ /*jshint -W083 */
+ result.then(function (data) {
+ var temp = 0;
+ keep_cnt++;
+ for (var key in data.values.A) {
+ temp = key;
+ }
+ var t = data.values.A[temp].time.split(" ");
+ var m = t[0];
+ var year = t[2];
+ var day = t[1].substr(0, t[1].length - 3);
+ var t1, hur, hour, min;
+ if (_.isEmpty(t[3])) {
+ t1 = t[4].split(":");
+ hur = t1[0];
+ if (t[5] == "AM") {
+ hour = hur;
+ if (hour == 12) {
+ hour = 0;
+ }
+ }
+ if (t[5] == "PM") {
+ hour = parseInt(hur) + 12;
+ }
+ min = t1[1];
+ }
+ else {
+ t1 = t[3].split(":");
+ hur = t1[0];
+ if (t[4] == "AM") {
+ hour = hur;
+ if (hour == 12) {
+ hour = 0;
+ }
+ }
+ if (t[4] == "PM") {
+ hour = parseInt(hur) + 12;
+ }
+ min = t1[1];
+ }
+ var month = 0;
+ switch (m) {
+ case "January":
+ month = 0;
+ break;
+ case "February":
+ month = 1;
+ break;
+ case "March":
+ month = 2;
+ break;
+ case "April":
+ month = 3;
+ break;
+ case "May":
+ month = 4;
+ break;
+ case "June":
+ month = 5;
+ break;
+ case "July":
+ month = 6;
+ break;
+ case "August":
+ month = 7;
+ break;
+ case "September":
+ month = 8;
+ break;
+ case "October":
+ month = 9;
+ break;
+ case "November":
+ month = 10;
+ break;
+ case "December":
+ month = 11;
+ break;
+
+ }
+ var tp = new Date(year, month, day, hour, min, 0, 0);
+ scope.graph_data[temp - 1] = {
+ time: tp,
+ x: data.values.A[temp].count,
+ y: data.values.B[temp].count
+ };
+
+ if (keep_cnt == options.split_count) {
+ scope.graphload = true;
+ data = scope.graph_data;
+
+ // set up a colour variable
+ var color = d3.scale.category10();
+
+ // map one colour each to x, y and z
+ // keys grabs the key value or heading of each key value pair in the json
+ // but not time
+ color.domain(d3.keys(data[0]).filter(function (key) {
+ return key !== "time";
+ }));
+
+ // create a nested series for passing to the line generator
+ // it's best understood by console logging the data
+ var series = color.domain().map(function (name) {
+ return {
+ name: name,
+ values: data.map(function (d) {
+ return {
+ time: d.time,
+ score: +d[name]
+ };
+ })
+ };
+ });
+
+ // Set the dimensions of the canvas / graph
+ var margin = {
+ top: 30,
+ right: 20,
+ bottom: 40,
+ left: 75
+ },
+ width = 550 - margin.left - margin.right,
+ height = 350 - margin.top - margin.bottom;
+
+ // Set the ranges
+ //var x = d3.time.scale().range([0, width]).domain([0,10]);
+ var x = d3.time.scale().range([0, width]);
+ var y = d3.scale.linear().range([height, 0]);
+
+ // Define the axes
+ var xAxis = d3.svg.axis().scale(x)
+ .orient("bottom").ticks(10);
+
+ var yAxis = d3.svg.axis().scale(y)
+ .orient("left").ticks(5);
+
+ // Define the line
+ // Note you plot the time / score pair from each key you created earlier
+ var valueline = d3.svg.line()
+ .x(function (d) {
+ return x(d.time);
+ })
+ .y(function (d) {
+ return y(d.score);
+ });
+
+ // Adds the svg canvas
+ var svg = d3.select($('.crm-mailing-ab-stats', element)[0])
+ .append("svg")
+ .attr("width", width + margin.left + margin.right)
+ .attr("height", height + margin.top + margin.bottom)
+ .append("g")
+ .attr("transform", "translate(" + margin.left + "," + margin.top + ")");
+
+ // Scale the range of the data
+ x.domain(d3.extent(data, function (d) {
+ return d.time;
+ }));
+
+ // note the nested nature of this you need to dig an additional level
+ y.domain([
+ d3.min(series, function (c) {
+ return d3.min(c.values, function (v) {
+ return v.score;
+ });
+ }),
+ d3.max(series, function (c) {
+ return d3.max(c.values, function (v) {
+ return v.score;
+ });
+ })
+ ]);
+ svg.append("text") // text label for the x axis
+ .attr("x", width / 2)
+ .attr("y", height + margin.bottom)
+ .style("text-anchor", "middle")
+ .text("Time");
+
+ svg.append("text") // text label for the x axis
+ .style("text-anchor", "middle")
+ .text(scope.winnercriteria).attr("transform",function (d) {
+ return "rotate(-90)";
+ }).attr("x", -height / 2)
+ .attr("y", -30);
+
+ // create a variable called series and bind the date
+ // for each series append a g element and class it as series for css styling
+ series = svg.selectAll(".series")
+ .data(series)
+ .enter().append("g")
+ .attr("class", "series");
+
+ // create the path for each series in the variable series i.e. x, y and z
+ // pass each object called x, y nad z to the lne generator
+ series.append("path")
+ .attr("class", "line")
+ .attr("d", function (d) {
+ // console.log(d); // to see how d3 iterates through series
+ return valueline(d.values);
+ })
+ .style("stroke", function (d) {
+ return color(d.name);
+ });
+
+ // Add the X Axis
+ svg.append("g") // Add the X Axis
+ .attr("class", "x axis")
+ .attr("transform", "translate(0," + height + ")")
+ .call(xAxis)
+ .selectAll("text")
+ .attr("transform", function (d) {
+ return "rotate(-30)";
+ });
+
+ // Add the Y Axis
+ svg.append("g") // Add the Y Axis
+ .attr("class", "y axis")
+ .call(yAxis);
+ }
+ });
+ }
+ }
+ } // link()
+ };
+ });
+})(angular, CRM.$, CRM._);
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB/WinnerDialogCtrl.html b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB/WinnerDialogCtrl.html
new file mode 100644
index 00000000..0c2db4f6
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB/WinnerDialogCtrl.html
@@ -0,0 +1,19 @@
+<div ng-controller="CrmMailingABWinnerDialogCtrl">
+ <form novalidate name="winnerForm">
+ <div class="help">
+ {{ts('After selecting %1 as the winner, one must schedule the delivery for the final mailing.', {1: mailingTitle})}}
+ </div>
+
+ <div crm-mailing-radio-date="schedule" ng-model="abtest.mailings.c.scheduled_date">
+ <div>
+ <input ng-model="schedule.mode" type="radio" name="send" value="now" id="schedule-send-now"/>
+ <label for="schedule-send-now">{{ts('Send final mailing 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 final mailing at:')}}</label>
+ <input crm-ui-datepicker ng-model="schedule.datetime"/>
+ </div>
+ </div>
+ </form>
+</div>
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB/WinnerDialogCtrl.js b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB/WinnerDialogCtrl.js
new file mode 100644
index 00000000..f378f641
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB/WinnerDialogCtrl.js
@@ -0,0 +1,43 @@
+(function(angular, $, _) {
+
+ angular.module('crmMailingAB').controller('CrmMailingABWinnerDialogCtrl', function($scope, $timeout, dialogService, crmMailingMgr, crmStatus) {
+ var ts = $scope.ts = CRM.ts(null);
+ var abtest = $scope.abtest = $scope.model.abtest;
+ var mailingName = $scope.model.mailingName;
+
+ var titles = {a: ts('Mailing A'), b: ts('Mailing B')};
+ $scope.mailingTitle = titles[mailingName];
+
+ function init() {
+ // 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('Submit final mailing'),
+ icons: {primary: 'fa-paper-plane'},
+ click: function() {
+ crmStatus({start: ts('Submitting...'), success: ts('Submitted')},
+ abtest.submitFinal(abtest.mailings[mailingName].id).then(function (r) {
+ delete abtest.$CrmMailingABReportCnt;
+ }))
+ .then(function () {
+ dialogService.close('selectWinnerDialog', abtest);
+ });
+ }
+ },
+ {
+ text: ts('Cancel'),
+ icons: {primary: 'fa-times'},
+ click: function() {
+ dialogService.cancel('selectWinnerDialog');
+ }
+ }
+ ];
+ dialogService.setButtons('selectWinnerDialog', buttons);
+ }
+
+ $timeout(init);
+ });
+
+})(angular, CRM.$, CRM._);
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB/services.js b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB/services.js
new file mode 100644
index 00000000..2e9fa926
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB/services.js
@@ -0,0 +1,234 @@
+(function (angular, $, _) {
+
+ function OptionGroup(values) {
+ this.get = function get(value) {
+ var r = _.where(values, {value: '' + value});
+ return r.length > 0 ? r[0] : null;
+ };
+ this.getByName = function get(name) {
+ var r = _.where(values, {name: '' + name});
+ return r.length > 0 ? r[0] : null;
+ };
+ this.getAll = function getAll() {
+ return values;
+ };
+ }
+
+ angular.module('crmMailingAB').factory('crmMailingABCriteria', function () {
+ // TODO Get data from server
+ var values = {
+ '1': {value: 'subject', name: 'subject', label: ts('Test different "Subject" lines')},
+ '2': {value: 'from', name: 'from', label: ts('Test different "From" lines')},
+ '3': {value: 'full_email', name: 'full_email', label: ts('Test entirely different emails')}
+ };
+ return new OptionGroup(values);
+ });
+
+ angular.module('crmMailingAB').factory('crmMailingABStatus', function () {
+ // TODO Get data from server
+ var values = {
+ '1': {value: '1', name: 'Draft', label: ts('Draft')},
+ '2': {value: '2', name: 'Testing', label: ts('Testing')},
+ '3': {value: '3', name: 'Final', label: ts('Final')}
+ };
+ return new OptionGroup(values);
+ });
+
+ // CrmMailingAB is a data-model which combines an AB test (APIv3 "MailingAB"), three mailings (APIv3 "Mailing"),
+ // and three sets of attachments (APIv3 "Attachment").
+ //
+ // example:
+ // var abtest = new CrmMailingAB(123);
+ // abtest.load().then(function(){
+ // alert("Mailing A is named "+abtest.mailings.a.name);
+ // });
+ angular.module('crmMailingAB').factory('CrmMailingAB', function (crmApi, crmMailingMgr, $q, CrmAttachments) {
+ function CrmMailingAB(id) {
+ this.id = id;
+ this.mailings = {};
+ this.attachments = {};
+ }
+
+ angular.extend(CrmMailingAB.prototype, {
+ getAutosaveSignature: function() {
+ return [
+ this.ab,
+ this.mailings,
+ this.attachments.a.getAutosaveSignature(),
+ this.attachments.b.getAutosaveSignature(),
+ this.attachments.c.getAutosaveSignature()
+ ];
+ },
+ // @return Promise CrmMailingAB
+ load: function load() {
+ var crmMailingAB = this;
+ if (!crmMailingAB.id) {
+ crmMailingAB.ab = {
+ name: '',
+ status: 'Draft',
+ mailing_id_a: null,
+ mailing_id_b: null,
+ mailing_id_c: null,
+ domain_id: null,
+ testing_criteria: 'subject',
+ winner_criteria: null,
+ specific_url: '',
+ declare_winning_time: null,
+ group_percentage: 10
+ };
+ var mailingDefaults = {
+ // Most defaults provided by Mailing.create API, but we
+ // want to force-enable tracking.
+ open_tracking: "1",
+ url_tracking: "1",
+ mailing_type:"experiment"
+ };
+ crmMailingAB.mailings.a = crmMailingMgr.create(mailingDefaults);
+ crmMailingAB.mailings.b = crmMailingMgr.create(mailingDefaults);
+ mailingDefaults.mailing_type = 'winner';
+ crmMailingAB.mailings.c = crmMailingMgr.create(mailingDefaults);
+ crmMailingAB.attachments.a = new CrmAttachments(function () {
+ return {entity_table: 'civicrm_mailing', entity_id: crmMailingAB.ab.mailing_id_a};
+ });
+ crmMailingAB.attachments.b = new CrmAttachments(function () {
+ return {entity_table: 'civicrm_mailing', entity_id: crmMailingAB.ab.mailing_id_b};
+ });
+ crmMailingAB.attachments.c = new CrmAttachments(function () {
+ return {entity_table: 'civicrm_mailing', entity_id: crmMailingAB.ab.mailing_id_c};
+ });
+
+ var dfr = $q.defer();
+ dfr.resolve(crmMailingAB);
+ return dfr.promise;
+ }
+ else {
+ return crmApi('MailingAB', 'get', {id: crmMailingAB.id})
+ .then(function (abResult) {
+ if (abResult.count != 1) {
+ throw "Failed to load AB Test";
+ }
+ crmMailingAB.ab = abResult.values[abResult.id];
+ return crmMailingAB._loadMailings();
+ });
+ }
+ },
+ // @return Promise CrmMailingAB
+ save: function save() {
+ var crmMailingAB = this;
+ return crmMailingAB._saveMailings()
+ .then(function () {
+ return crmApi('MailingAB', 'create', crmMailingAB.ab)
+ .then(function (abResult) {
+ if (!crmMailingAB.id) {
+ crmMailingAB.id = crmMailingAB.ab.id = abResult.id;
+ }
+ });
+ })
+ .then(function () {
+ return crmMailingAB;
+ });
+ },
+ // Schedule the test
+ // @return Promise CrmMailingAB
+ // Note: Submission may cause the server state to change. Consider abtest.submit().then(...abtest.load()...)
+ submitTest: function submitTest() {
+ var crmMailingAB = this;
+ var params = {
+ id: this.ab.id,
+ status: 'Testing',
+ approval_date: 'now',
+ scheduled_date: this.mailings.a.scheduled_date ? this.mailings.a.scheduled_date : 'now'
+ };
+ return crmApi('MailingAB', 'submit', params)
+ .then(function () {
+ return crmMailingAB.load();
+ });
+ },
+ // Schedule the final mailing
+ // @return Promise CrmMailingAB
+ // Note: Submission may cause the server state to change. Consider abtest.submit().then(...abtest.load()...)
+ submitFinal: function submitFinal(winner_id) {
+ var crmMailingAB = this;
+ var params = {
+ id: this.ab.id,
+ status: 'Final',
+ winner_id: winner_id,
+ approval_date: 'now',
+ scheduled_date: this.mailings.c.scheduled_date ? this.mailings.c.scheduled_date : 'now'
+ };
+ return crmApi('MailingAB', 'submit', params)
+ .then(function () {
+ return crmMailingAB.load();
+ });
+ },
+ // @param mailing Object (per APIv3)
+ // @return Promise
+ 'delete': function () {
+ if (this.id) {
+ return crmApi('MailingAB', 'delete', {id: this.id});
+ }
+ else {
+ var d = $q.defer();
+ d.resolve();
+ return d.promise;
+ }
+ },
+ // Load mailings A, B, and C (if available)
+ // @return Promise CrmMailingAB
+ _loadMailings: function _loadMailings() {
+ var crmMailingAB = this;
+ var todos = {};
+ _.each(['a', 'b', 'c'], function (mkey) {
+ if (crmMailingAB.ab['mailing_id_' + mkey]) {
+ todos[mkey] = crmMailingMgr.get(crmMailingAB.ab['mailing_id_' + mkey])
+ .then(function (mailing) {
+ crmMailingAB.mailings[mkey] = mailing;
+ crmMailingAB.attachments[mkey] = new CrmAttachments(function () {
+ return {entity_table: 'civicrm_mailing', entity_id: crmMailingAB.ab['mailing_id_' + mkey]};
+ });
+ return crmMailingAB.attachments[mkey].load();
+ });
+ }
+ else {
+ crmMailingAB.mailings[mkey] = crmMailingMgr.create();
+ crmMailingAB.attachments[mkey] = new CrmAttachments(function () {
+ return {entity_table: 'civicrm_mailing', entity_id: crmMailingAB.ab['mailing_id_' + mkey]};
+ });
+ }
+ });
+ return $q.all(todos).then(function () {
+ return crmMailingAB;
+ });
+ },
+ // Save mailings A, B, and C (if available)
+ // @return Promise CrmMailingAB
+ _saveMailings: function _saveMailings() {
+ var crmMailingAB = this;
+ var todos = {};
+ var p = $q.when(true);
+ _.each(['a', 'b', 'c'], function (mkey) {
+ if (!crmMailingAB.mailings[mkey]) {
+ return;
+ }
+ if (crmMailingAB.ab['mailing_id_' + mkey]) {
+ // paranoia: in case caller forgot to manage id on mailing
+ crmMailingAB.mailings[mkey].id = crmMailingAB.ab['mailing_id_' + mkey];
+ }
+ p = p.then(function(){
+ return crmMailingMgr.save(crmMailingAB.mailings[mkey])
+ .then(function () {
+ crmMailingAB.ab['mailing_id_' + mkey] = crmMailingAB.mailings[mkey].id;
+ return crmMailingAB.attachments[mkey].save();
+ });
+ });
+ });
+ return p.then(function () {
+ return crmMailingAB;
+ });
+ }
+
+ });
+ return CrmMailingAB;
+ });
+
+})(angular, CRM.$, CRM._);
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmResource.ang.php b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmResource.ang.php
new file mode 100644
index 00000000..577f48e1
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmResource.ang.php
@@ -0,0 +1,11 @@
+<?php
+// This file declares an Angular module which can be autoloaded
+// in CiviCRM. See also:
+// http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_angularModules
+
+return [
+ 'ext' => 'civicrm',
+ // 'js' => array('js/angular-crmResource/byModule.js'), // One HTTP request per module.
+ // One HTTP request for all modules.
+ 'js' => ['js/angular-crmResource/all.js'],
+];
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmRouteBinder.ang.php b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmRouteBinder.ang.php
new file mode 100644
index 00000000..0da8aa36
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmRouteBinder.ang.php
@@ -0,0 +1,12 @@
+<?php
+// This file declares an Angular module which can be autoloaded
+// in CiviCRM. See also:
+// http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_angularModules
+
+return [
+ 'ext' => 'civicrm',
+ 'js' => ['ang/crmRouteBinder.js'],
+ 'css' => [],
+ 'partials' => [],
+ 'requires' => ['ngRoute'],
+];
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmRouteBinder.js b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmRouteBinder.js
new file mode 100644
index 00000000..a8563fd0
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmRouteBinder.js
@@ -0,0 +1,111 @@
+(function(angular, $, _) {
+ angular.module('crmRouteBinder', CRM.angRequires('crmRouteBinder'));
+
+ // While processing a change from the $watch()'d data, we set the "pendingUpdates" flag
+ // so that automated URL changes don't cause a reload.
+ var pendingUpdates = null, activeTimer = null, registered = false, ignorable = {};
+
+ function registerGlobalListener($injector) {
+ if (registered) return;
+ registered = true;
+
+ $injector.get('$rootScope').$on('$routeUpdate', function () {
+ // Only reload if someone else -- like the user or an <a href> -- changed URL.
+ if (null === pendingUpdates) {
+ $injector.get('$route').reload();
+ }
+ });
+ }
+
+ var formats = {
+ json: {
+ watcher: '$watchCollection',
+ decode: angular.fromJson,
+ encode: angular.toJson,
+ default: {}
+ },
+ raw: {
+ watcher: '$watch',
+ decode: function(v) { return v; },
+ encode: function(v) { return v; },
+ default: ''
+ },
+ int: {
+ watcher: '$watch',
+ decode: function(v) { return parseInt(v); },
+ encode: function(v) { return v; },
+ default: 0
+ },
+ bool: {
+ watcher: '$watch',
+ decode: function(v) { return v === '1'; },
+ encode: function(v) { return v ? '1' : '0'; },
+ default: false
+ }
+ };
+
+ angular.module('crmRouteBinder').config(function ($provide) {
+ $provide.decorator('$rootScope', function ($delegate, $injector, $parse) {
+ Object.getPrototypeOf($delegate).$bindToRoute = function (options) {
+ registerGlobalListener($injector);
+
+ options.format = options.format || 'json';
+ var fmt = _.clone(formats[options.format]);
+ if (options.deep) {
+ fmt.watcher = '$watch';
+ }
+ if (options.default === undefined) {
+ options.default = fmt.default;
+ }
+ var value,
+ _scope = this,
+ $route = $injector.get('$route'),
+ $timeout = $injector.get('$timeout');
+
+ if (options.param in $route.current.params) {
+ value = fmt.decode($route.current.params[options.param]);
+ }
+ else {
+ value = _.cloneDeep(options.default);
+ ignorable[options.param] = fmt.encode(options.default);
+ }
+ $parse(options.expr).assign(_scope, value);
+
+ // Keep the URL bar up-to-date.
+ _scope[fmt.watcher](options.expr, function (newValue) {
+ var encValue = fmt.encode(newValue);
+ if (!_.isEqual(newValue, options.default) && $route.current.params[options.param] === encValue) {
+ return;
+ }
+
+ pendingUpdates = pendingUpdates || {};
+ pendingUpdates[options.param] = encValue;
+ var p = angular.extend({}, $route.current.params, pendingUpdates);
+
+ angular.forEach(ignorable, function(v, k) {
+ if (p[k] === v) {
+ delete p[k];
+ }
+ });
+
+ // Remove params from url if they equal their defaults
+ if (_.isEqual(newValue, options.default)) {
+ p[options.param] = null;
+ }
+
+ $route.updateParams(p);
+
+ if (activeTimer) $timeout.cancel(activeTimer);
+ activeTimer = $timeout(function () {
+ pendingUpdates = null;
+ activeTimer = null;
+ ignorable = {};
+ }, 50);
+ }, options.deep);
+ };
+
+ return $delegate;
+ });
+ });
+
+})(angular, CRM.$, CRM._);
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmRouteBinder.md b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmRouteBinder.md
new file mode 100644
index 00000000..a297bdc4
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmRouteBinder.md
@@ -0,0 +1,106 @@
+# crmRouteBinder
+
+Live-update the URL to stay in sync with controller data.
+
+## Example
+
+```js
+angular.module('sandbox').config(function($routeProvider) {
+ $routeProvider.when('/example-route', {
+ reloadOnSearch: false,
+ template: '<input ng-model="filters.foo" />',
+ controller: function($scope) {
+ $scope.$bindToRoute({
+ param: 'f',
+ expr: 'filters',
+ default: {foo: 'default-value'}
+ });
+ }
+ });
+});
+```
+
+Things to try out:
+
+ * Navigate to `#/example-route`. Observe that the URL automatically
+ updates to `#/example-route?f={"foo":"default-value"}`.
+ * Edit the content in the `<input>` field. Observe that the URL changes.
+ * Initiate a change in the browser -- by editing the URL bar or pressing
+ the "Back" button. The page should refresh.
+
+## Functions
+
+**`$scope.$bindToRoute(options)`**
+*The `options` object should contain keys:*
+
+ * `expr` (string): The name of a scoped variable to sync.
+ * `param` (string): The name of a query-parameter to sync. (If the `param` is included in the URL, it will initialize the expr.)
+ * `format` (string): The type of data to put in `param`. May be one of:
+ * `json` (default): The `param` is JSON, and the `expr` is a decoded object.
+ * `raw`: The `param` is string, and the `expr` is a string.
+ * `int`: the `param` is an integer-like string, and the expr is an integer.
+ * `bool`: The `param` is '0'/'1', and the `expr` is false/true.
+ * `default` (object): The default data. (If the `param` is not included in the URL, it will initialize the expr.)
+ * `deep` (boolean): By default the json format will be watched using a shallow comparison. For nested objects and arrays enable this option.
+
+## Suggested Usage
+
+`$bindToRoute()` was written for a complicated routing scenario with
+multiple parameters, e.g. `caseFilters:Object`, `caseId:Int`, `tab:String`,
+`activityFilters:Object`, `activityId:Int`. If you're use-case is one or
+two scalar values, then stick to vanilla `ngRoute`. This is only for
+complicated scenarios.
+
+If you are using `$bindToRoute()`, should you split up parameters -- with
+some using `ngRoute` and some using `$bindToRoute()`? I'd pick one style
+and stick to it. You're in a complex use-case where `$bindToRoute()` makes
+sense, then you already need to put thought into the different
+flows/input-combinations. Having two technical styles will increase the
+mental load.
+
+A goal of `bindToRoute()` is to accept inputs interchangably from the URL or
+HTML fields. Using `ngRoute`'s `resolve:` option only addresses the URL
+half. If you want one piece of code handling all inputs the same way, you
+should avoid `resolve:` and instead write a controller focused on
+orchestrating I/O:
+
+```js
+angular.module('sandbox').config(function($routeProvider) {
+ $routeProvider.when('/example-route', {
+ reloadOnSearch: false,
+ template:
+ '<div filter-toolbar-a="filterSetA" />'
+ + '<div filter-toolbar-b="filterSetB" />'
+ + '<div filter-toolbar-c="filterSetC" />'
+ + '<div data-set-a="dataSetA" />'
+ + '<div data-set-b="dataSetB" />'
+ + '<div data-set-c="dataSetC" />',
+ controller: function($scope) {
+ $scope.$bindToRoute({expr:'filterSetA', param:'a', default:{}});
+ $scope.$watchCollection('filterSetA', function(){
+ crmApi(...).then(function(...){
+ $scope.dataSetA = ...;
+ });
+ });
+
+ $scope.$bindToRoute({expr:'filterSetB', param:'b', default:{}});
+ $scope.$watchCollection('filterSetB', function(){
+ crmApi(...).then(function(...){
+ $scope.dataSetB = ...;
+ });
+ });
+
+ $scope.$bindToRoute({expr:'filterSetC', param:'c', default:{}});
+ $scope.$watchCollection('filterSetC', function(){
+ crmApi(...).then(function(...){
+ $scope.dataSetC = ...;
+ });
+ });
+ }
+ });
+});
+```
+
+(This example is a little more symmetric than a real one -- because the A,
+B, and C datasets look independent. In practice, their loading may be
+intermingled.)
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmStatusPage.ang.php b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmStatusPage.ang.php
new file mode 100644
index 00000000..c72127f3
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmStatusPage.ang.php
@@ -0,0 +1,15 @@
+<?php
+// This file declares an Angular module which can be autoloaded
+// in CiviCRM. See also:
+// http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_angularModules
+
+// ODDITY: Angular name 'statuspage' doesn't match the file name 'crmStatusPage'.
+
+return [
+ 'ext' => 'civicrm',
+ 'js' => ['ang/crmStatusPage.js', 'ang/crmStatusPage/*.js'],
+ 'css' => ['ang/crmStatusPage.css'],
+ 'partials' => ['ang/crmStatusPage'],
+ 'settings' => [],
+ 'requires' => ['crmUi', 'crmUtil', 'ngRoute', 'crmResource'],
+];
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmStatusPage.css b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmStatusPage.css
new file mode 100644
index 00000000..7bc17224
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmStatusPage.css
@@ -0,0 +1,82 @@
+/* CSS rules for Angular module "statuspage" */
+
+#crm-status-list h3 {
+ color: white;
+ opacity: .85;
+}
+
+#crm-status-list h3:hover,
+#crm-status-list h3.menuopen {
+ opacity: 1;
+}
+
+/* Error Severity */
+#crm-status-list .crm-severity-emergency,
+#crm-status-list .crm-severity-alert,
+#crm-status-list .crm-severity-critical,
+#crm-status-list .crm-severity-error{
+ background-color: #E43D2B;
+}
+
+/* Warning Severity */
+#crm-status-list .crm-severity-warning {
+ background-color: #eba12d;
+}
+
+/* Not Okay - Not Warning */
+#crm-status-list .crm-severity-notice {
+ background-color: #4d90eb;
+}
+
+/* All OK Severity */
+#crm-status-list .crm-severity-info,
+#crm-status-list .crm-severity-debug {
+ background-color: #00994D;
+}
+
+#crm-status-list .crm-status-message-body {
+ margin: 1em 0;
+}
+
+#crm-status-list .hidden-until {
+ font-weight: normal;
+ font-size: .8em;
+ margin-right: 1em;
+}
+
+#tab-status-visible-1 .hush-menu > div {
+ width: 20em; /* determines max-width of popup menu */
+}
+
+#crm-status-list .hush-menu > div {
+ position: relative;
+ font-size: .8em;
+ top: -2px;
+}
+
+#crm-status-list .hush-menu button {
+ float: right;
+ cursor: pointer;
+ line-height: 1em;
+}
+
+#crm-status-list .hush-menu ul {
+ position: absolute;
+ top: 1.5em;
+ right: 0;
+ width: auto;
+ margin: 0;
+ padding: 0;
+ z-index: 99;
+}
+
+#crm-status-list .hush-menu li {
+ padding: 0.2em 0.5em;
+ background-color: rgba(255, 255, 255, 0.9);
+ z-index: 99;
+ font-weight: normal;
+}
+
+.status-debug-information {
+ font-size: smaller;
+}
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmStatusPage.js b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmStatusPage.js
new file mode 100644
index 00000000..b0db81a3
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmStatusPage.js
@@ -0,0 +1,19 @@
+(function(angular, $, _) {
+ angular.module('statuspage', CRM.angRequires('statuspage'));
+
+ // router
+ angular.module('statuspage').config( function($routeProvider) {
+ $routeProvider.when('/status', {
+ controller: 'statuspageStatusPage',
+ templateUrl: '~/statuspage/StatusPage.html',
+
+ resolve: {
+ statusData: function(crmApi) {
+ return crmApi('System', 'check', {sequential: 1});
+ }
+ }
+ });
+
+ }
+);
+})(angular, CRM.$, CRM._);
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmStatusPage/SnoozeOptions.html b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmStatusPage/SnoozeOptions.html
new file mode 100644
index 00000000..b21cb7b9
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmStatusPage/SnoozeOptions.html
@@ -0,0 +1,11 @@
+<div ng-if="!status.is_visible">
+ <button ng-click="setPref(status, '', 1)" type="button" >{{ts('Unhide')}}</button>
+</div>
+<div ng-if="status.is_visible && status.severity_id >= 2">
+ <button type="button" class="hush-menu-button">{{ts('Hide')}}</button>
+ <ul style="display:none;">
+ <li ng-click="setPref(status, 'now + 1 week', 0)">{{ts('Remind me again in a week')}}</li>
+ <li ng-click="setPref(status, 'now + 1 month', 0)">{{ts('Remind me again in a month')}}</li>
+ <li ng-click="setPref(status, '', 0)">{{ts('Never remind me again')}}</li>
+ </ul>
+</div>
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmStatusPage/StatusPage.html b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmStatusPage/StatusPage.html
new file mode 100644
index 00000000..71a9c6aa
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmStatusPage/StatusPage.html
@@ -0,0 +1,39 @@
+<div crm-ui-debug="statuses"></div>
+
+<h1 crm-page-title crm-document-title="ts('CiviCRM System Status') + ' (' + countVisible(1) + ')'">
+ {{ts('CiviCRM System Status')}}
+</h1>
+
+<div id="crm-status-list" crm-ui-tab-set>
+ <div crm-ui-tab
+ ng-repeat="tab in [{is_visible: 1, icon: 'fa-bell'}, {is_visible: 0, icon: 'fa-bell-slash-o'}]"
+ id="tab-status-visible-{{tab.is_visible}}"
+ count="{{countVisible(tab.is_visible)}}"
+ crm-title="tab.is_visible ? ts('Active') : ts('Hidden')"
+ crm-icon="{{tab.icon}}"
+ >
+ <div class="crm-status-item" ng-repeat="status in statuses | filter:{is_visible: tab.is_visible}" >
+ <h3 class="crm-severity-{{status.severity}}">
+ <i ng-if="status.icon" class="crm-i {{status.icon}}"></i>
+ {{status.title}}
+ <div statuspage-popup-menu class="hush-menu css_right"></div>
+ <div ng-if="!status.is_visible" class="hidden-until css_right">
+ ({{status.hidden_until ? ts('Hidden until %1', {1: formatDate(status.hidden_until)}) : ts('Hidden permanently')}})
+ </div>
+ </h3>
+ <div class="crm-block crm-status-message-body">
+ <span ng-bind-html="status.message | trusted"></span>
+ <a
+ ng-if="status.help"
+ class="helpicon"
+ ng-click="help(status.title, status.help);"
+ href="javascript:void(0)"
+ >
+ </a>
+ <div ng-if="status.actions" class="crm-status-item-actions">
+ <button ng-repeat="action in status.actions" ng-click="doAction(action)">{{ action.title }}</button>
+ </div>
+ </div>
+ </div>
+ </div>
+</div>
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmStatusPage/StatusPageCtrl.js b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmStatusPage/StatusPageCtrl.js
new file mode 100644
index 00000000..4abca2ab
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmStatusPage/StatusPageCtrl.js
@@ -0,0 +1,74 @@
+(function(angular, $, _) {
+
+ angular.module('statuspage').controller('statuspageStatusPage',
+ function($scope, crmApi, crmStatus, statusData) {
+ $scope.ts = CRM.ts();
+ $scope.help = CRM.help;
+ $scope.formatDate = CRM.utils.formatDate;
+ $scope.statuses = statusData.values;
+
+ // Refresh the list. Optionally execute api calls first.
+ function refresh(apiCalls, title) {
+ title = title || 'Untitled operation';
+ apiCalls = (apiCalls || []).concat([['System', 'check', {sequential: 1}]]);
+ $('#crm-status-list').block();
+ crmApi(apiCalls, true)
+ .then(function(results) {
+ $scope.statuses = results[results.length - 1].values;
+ results.forEach(function(result) {
+ if (result.is_error) {
+ var error_message = ts(result.error_message);
+ if (typeof(result.debug_information) !== 'undefined') {
+ error_message += '<div class="status-debug-information">' +
+ '<b>' + ts('Debug information') + ':</b><br>' +
+ result.debug_information + '</div>';
+ }
+ CRM.alert(error_message, ts('Operation failed: ' + title), 'error');
+ }
+ });
+ $('#crm-status-list').unblock();
+ });
+ }
+
+ // updates a status preference and refreshes status data
+ $scope.setPref = function(status, until, visible) {
+ refresh([
+ ['StatusPreference', 'create', {
+ name: status.name,
+ ignore_severity: visible ? 0 : status.severity,
+ hush_until: until
+ }]
+ ], 'Set preference');
+ };
+
+ $scope.countVisible = function(visibility) {
+ return _.filter($scope.statuses, function(s) {
+ return s.is_visible == visibility && s.severity_id >= 2;
+ }).length;
+ };
+
+ $scope.doAction = function(action) {
+ function run() {
+ switch (action.type) {
+ case 'href':
+ window.location = CRM.url(action.params.path, action.params.query, action.params.mode);
+ break;
+
+ case 'api3':
+ refresh([action.params], action.title);
+ break;
+ }
+ }
+
+ if (action.confirm) {
+ CRM.confirm({
+ title: action.title,
+ message: action.confirm
+ }).on('crmConfirm:yes', run);
+ } else {
+ run();
+ }
+ };
+ });
+
+})(angular, CRM.$, CRM._);
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmStatusPage/StatusPageServices.js b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmStatusPage/StatusPageServices.js
new file mode 100644
index 00000000..a37ceafb
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmStatusPage/StatusPageServices.js
@@ -0,0 +1,31 @@
+(function(angular, $, _) {
+
+ angular.module('statuspage')
+ .filter('trusted', function($sce){ return $sce.trustAsHtml; })
+
+ // Todo: abstract this into a generic crmUi directive?
+ .directive('statuspagePopupMenu', function($timeout) {
+ return {
+ templateUrl: '~/statuspage/SnoozeOptions.html',
+ transclude: true,
+
+ link: function(scope, element, attr) {
+ element.on('click', '.hush-menu-button', function() {
+ $timeout(function() {
+ $('ul', element).show().menu();
+ element.closest('h3').addClass('menuopen');
+ $('body').one('click', function() {
+ $('ul', element).menu('destroy').hide();
+ element.closest('h3').removeClass('menuopen');
+ });
+ });
+ });
+ // TODO: Is there a more "Angular" way to do this animation?
+ element.on('click', 'button:not(.hush-menu-button), li', function() {
+ $(this).closest('div.crm-status-item').slideUp();
+ });
+ }
+ };
+ });
+
+})(angular, CRM.$, CRM._);
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmUi.ang.php b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmUi.ang.php
new file mode 100644
index 00000000..72c6594a
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmUi.ang.php
@@ -0,0 +1,14 @@
+<?php
+// This file declares an Angular module which can be autoloaded
+// in CiviCRM. See also:
+// http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_angularModules
+
+return [
+ 'ext' => 'civicrm',
+ 'js' => ['ang/crmUi.js'],
+ 'partials' => ['ang/crmUi'],
+ 'requires' => [
+ 'crmResource',
+ 'ui.utils',
+ ],
+];
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmUi.js b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmUi.js
new file mode 100644
index 00000000..0b09c60d
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmUi.js
@@ -0,0 +1,1092 @@
+/// crmUi: Sundry UI helpers
+(function (angular, $, _) {
+
+ var uidCount = 0,
+ pageTitle = 'CiviCRM',
+ documentTitle = 'CiviCRM';
+
+ angular.module('crmUi', CRM.angRequires('crmUi'))
+
+ // example <div crm-ui-accordion crm-title="ts('My Title')" crm-collapsed="true">...content...</div>
+ // WISHLIST: crmCollapsed should support two-way/continuous binding
+ .directive('crmUiAccordion', function() {
+ return {
+ scope: {
+ crmUiAccordion: '='
+ },
+ template: '<div ng-class="cssClasses"><div class="crm-accordion-header">{{crmUiAccordion.title}} <a crm-ui-help="help" ng-if="help"></a></div><div class="crm-accordion-body" ng-transclude></div></div>',
+ transclude: true,
+ link: function (scope, element, attrs) {
+ scope.cssClasses = {
+ 'crm-accordion-wrapper': true,
+ collapsed: scope.crmUiAccordion.collapsed
+ };
+ scope.help = null;
+ scope.$watch('crmUiAccordion', function(crmUiAccordion) {
+ if (crmUiAccordion && crmUiAccordion.help) {
+ scope.help = crmUiAccordion.help.clone({}, {
+ title: crmUiAccordion.title
+ });
+ }
+ });
+ }
+ };
+ })
+
+ // Examples:
+ // crmUiAlert({text: 'My text', title: 'My title', type: 'error'});
+ // crmUiAlert({template: '<a ng-click="ok()">Hello</a>', scope: $scope.$new()});
+ // var h = crmUiAlert({templateUrl: '~/crmFoo/alert.html', scope: $scope.$new()});
+ // ... h.close(); ...
+ .service('crmUiAlert', function($compile, $rootScope, $templateRequest, $q) {
+ var count = 0;
+ return function crmUiAlert(params) {
+ var id = 'crmUiAlert_' + (++count);
+ var tpl = null;
+ if (params.templateUrl) {
+ tpl = $templateRequest(params.templateUrl);
+ }
+ else if (params.template) {
+ tpl = params.template;
+ }
+ if (tpl) {
+ params.text = '<div id="' + id + '"></div>'; // temporary stub
+ }
+ var result = CRM.alert(params.text, params.title, params.type, params.options);
+ if (tpl) {
+ $q.when(tpl, function(html) {
+ var scope = params.scope || $rootScope.$new();
+ var linker = $compile(html);
+ $('#' + id).append($(linker(scope)));
+ });
+ }
+ return result;
+ };
+ })
+
+ // Simple wrapper around $.crmDatepicker.
+ // example with no time input: <input crm-ui-datepicker="{time: false}" ng-model="myobj.datefield"/>
+ // example with custom date format: <input crm-ui-datepicker="{date: 'm/d/y'}" ng-model="myobj.datefield"/>
+ .directive('crmUiDatepicker', function () {
+ return {
+ restrict: 'AE',
+ require: 'ngModel',
+ scope: {
+ crmUiDatepicker: '='
+ },
+ link: function (scope, element, attrs, ngModel) {
+ ngModel.$render = function () {
+ element.val(ngModel.$viewValue).change();
+ };
+
+ element
+ .crmDatepicker(scope.crmUiDatepicker)
+ .on('change', function() {
+ var requiredLength = 19;
+ if (scope.crmUiDatepicker && scope.crmUiDatepicker.time === false) {
+ requiredLength = 10;
+ }
+ if (scope.crmUiDatepicker && scope.crmUiDatepicker.date === false) {
+ requiredLength = 8;
+ }
+ ngModel.$setValidity('incompleteDateTime', !($(this).val().length && $(this).val().length !== requiredLength));
+ });
+ }
+ };
+ })
+
+ // Display debug information (if available)
+ // For richer DX, checkout Batarang/ng-inspector (Chrome/Safari), or AngScope/ng-inspect (Firefox).
+ // example: <div crm-ui-debug="myobject" />
+ .directive('crmUiDebug', function ($location) {
+ return {
+ restrict: 'AE',
+ scope: {
+ crmUiDebug: '@'
+ },
+ template: function() {
+ var args = $location.search();
+ return (args && args.angularDebug) ? '<div crm-ui-accordion=\'{title: ts("Debug (%1)", {1: crmUiDebug}), collapsed: true}\'><pre>{{data|json}}</pre></div>' : '';
+ },
+ link: function(scope, element, attrs) {
+ var args = $location.search();
+ if (args && args.angularDebug) {
+ scope.ts = CRM.ts(null);
+ scope.$parent.$watch(attrs.crmUiDebug, function(data) {
+ scope.data = data;
+ });
+ }
+ }
+ };
+ })
+
+ // Display a field/row in a field list
+ // example: <div crm-ui-field="{title: ts('My Field')}"> {{mydata}} </div>
+ // example: <div crm-ui-field="{name: 'subform.myfield', title: ts('My Field')}"> <input crm-ui-id="subform.myfield" name="myfield" /> </div>
+ // example: <div crm-ui-field="{name: 'subform.myfield', title: ts('My Field')}"> <input crm-ui-id="subform.myfield" name="myfield" required /> </div>
+ // example: <div crm-ui-field="{name: 'subform.myfield', title: ts('My Field'), help: hs('help_field_name'), required: true}"> {{mydata}} </div>
+ .directive('crmUiField', function() {
+ // Note: When writing new templates, the "label" position is particular. See/patch "var label" below.
+ var templateUrls = {
+ default: '~/crmUi/field.html',
+ checkbox: '~/crmUi/field-cb.html'
+ };
+
+ return {
+ require: '^crmUiIdScope',
+ restrict: 'EA',
+ scope: {
+ // {title, name, help, helpFile}
+ crmUiField: '='
+ },
+ templateUrl: function(tElement, tAttrs){
+ var layout = tAttrs.crmLayout ? tAttrs.crmLayout : 'default';
+ return templateUrls[layout];
+ },
+ transclude: true,
+ link: function (scope, element, attrs, crmUiIdCtrl) {
+ $(element).addClass('crm-section');
+ scope.help = null;
+ scope.$watch('crmUiField', function(crmUiField) {
+ if (crmUiField && crmUiField.help) {
+ scope.help = crmUiField.help.clone({}, {
+ title: crmUiField.title
+ });
+ }
+ });
+ }
+ };
+ })
+
+ // example: <div ng-form="subform" crm-ui-id-scope><label crm-ui-for="subform.foo">Foo:</label><input crm-ui-id="subform.foo" name="foo"/></div>
+ .directive('crmUiId', function () {
+ return {
+ require: '^crmUiIdScope',
+ restrict: 'EA',
+ link: {
+ pre: function (scope, element, attrs, crmUiIdCtrl) {
+ var id = crmUiIdCtrl.get(attrs.crmUiId);
+ element.attr('id', id);
+ }
+ }
+ };
+ })
+
+ // for example, see crmUiHelp
+ .service('crmUiHelp', function(){
+ // example: var h = new FieldHelp({id: 'foo'}); h.open();
+ function FieldHelp(options) {
+ this.options = options;
+ }
+ angular.extend(FieldHelp.prototype, {
+ get: function(n) {
+ return this.options[n];
+ },
+ open: function open() {
+ CRM.help(this.options.title, {id: this.options.id, file: this.options.file});
+ },
+ clone: function clone(options, defaults) {
+ return new FieldHelp(angular.extend({}, defaults, this.options, options));
+ }
+ });
+
+ // example: var hs = crmUiHelp({file: 'CRM/Foo/Bar'});
+ return function(defaults){
+ // example: hs('myfield')
+ // example: hs({id: 'myfield', title: 'Foo Bar', file: 'Whiz/Bang'})
+ return function(options) {
+ if (_.isString(options)) {
+ options = {id: options};
+ }
+ return new FieldHelp(angular.extend({}, defaults, options));
+ };
+ };
+ })
+
+ // Display a help icon
+ // Example: Use a default *.hlp file
+ // scope.hs = crmUiHelp({file: 'Path/To/Help/File'});
+ // HTML: <a crm-ui-help="hs({title:ts('My Field'), id:'my_field'})">
+ // Example: Use an explicit *.hlp file
+ // HTML: <a crm-ui-help="hs({title:ts('My Field'), id:'my_field', file:'CRM/Foo/Bar'})">
+ .directive('crmUiHelp', function() {
+ return {
+ restrict: 'EA',
+ link: function(scope, element, attrs) {
+ setTimeout(function() {
+ var crmUiHelp = scope.$eval(attrs.crmUiHelp);
+ var title = crmUiHelp && crmUiHelp.get('title') ? ts('%1 Help', {1: crmUiHelp.get('title')}) : ts('Help');
+ element.attr('title', title);
+ }, 50);
+
+ element
+ .addClass('helpicon')
+ .attr('href', '#')
+ .on('click', function(e) {
+ e.preventDefault();
+ scope.$eval(attrs.crmUiHelp).open();
+ });
+ }
+ };
+ })
+
+ // example: <div ng-form="subform" crm-ui-id-scope><label crm-ui-for="subform.foo">Foo:</label><input crm-ui-id="subform.foo" name="foo"/></div>
+ .directive('crmUiFor', function ($parse, $timeout) {
+ return {
+ require: '^crmUiIdScope',
+ restrict: 'EA',
+ template: '<span ng-class="cssClasses"><span ng-transclude/><span crm-ui-visible="crmIsRequired" class="crm-marker" title="This field is required.">*</span></span>',
+ transclude: true,
+ link: function (scope, element, attrs, crmUiIdCtrl) {
+ scope.crmIsRequired = false;
+ scope.cssClasses = {};
+
+ if (!attrs.crmUiFor) return;
+
+ var id = crmUiIdCtrl.get(attrs.crmUiFor);
+ element.attr('for', id);
+ var ngModel = null;
+
+ var updateCss = function () {
+ scope.cssClasses['crm-error'] = !ngModel.$valid && !ngModel.$pristine;
+ };
+
+ // Note: if target element is dynamically generated (eg via ngInclude), then it may not be available
+ // immediately for initialization. Use retries/retryDelay to initialize such elements.
+ var init = function (retries, retryDelay) {
+ var input = $('#' + id);
+ if (input.length === 0 && !attrs.crmUiForceRequired) {
+ if (retries) {
+ $timeout(function(){
+ init(retries-1, retryDelay);
+ }, retryDelay);
+ }
+ return;
+ }
+
+ if (attrs.crmUiForceRequired) {
+ scope.crmIsRequired = true;
+ return;
+ }
+
+ var tgtScope = scope;//.$parent;
+ if (attrs.crmDepth) {
+ for (var i = attrs.crmDepth; i > 0; i--) {
+ tgtScope = tgtScope.$parent;
+ }
+ }
+
+ if (input.attr('ng-required')) {
+ scope.crmIsRequired = scope.$parent.$eval(input.attr('ng-required'));
+ scope.$parent.$watch(input.attr('ng-required'), function (isRequired) {
+ scope.crmIsRequired = isRequired;
+ });
+ }
+ else {
+ scope.crmIsRequired = input.prop('required');
+ }
+
+ ngModel = $parse(attrs.crmUiFor)(tgtScope);
+ if (ngModel) {
+ ngModel.$viewChangeListeners.push(updateCss);
+ }
+ };
+
+ $timeout(function(){
+ init(3, 100);
+ });
+ }
+ };
+ })
+
+ // Define a scope in which a name like "subform.foo" maps to a unique ID.
+ // example: <div ng-form="subform" crm-ui-id-scope><label crm-ui-for="subform.foo">Foo:</label><input crm-ui-id="subform.foo" name="foo"/></div>
+ .directive('crmUiIdScope', function () {
+ return {
+ restrict: 'EA',
+ scope: {},
+ controllerAs: 'crmUiIdCtrl',
+ controller: function($scope) {
+ var ids = {};
+ this.get = function(name) {
+ if (!ids[name]) {
+ ids[name] = "crmUiId_" + (++uidCount);
+ }
+ return ids[name];
+ };
+ },
+ link: function (scope, element, attrs) {}
+ };
+ })
+
+ // Display an HTML blurb inside an IFRAME.
+ // example: <iframe crm-ui-iframe="getHtmlContent()"></iframe>
+ // example: <iframe crm-ui-iframe crm-ui-iframe-src="getUrl()"></iframe>
+ .directive('crmUiIframe', function ($parse) {
+ return {
+ scope: {
+ crmUiIframeSrc: '@', // expression which evaluates to a URL
+ crmUiIframe: '@' // expression which evaluates to HTML content
+ },
+ link: function (scope, elm, attrs) {
+ var iframe = $(elm)[0];
+ iframe.setAttribute('width', '100%');
+ iframe.setAttribute('height', '250px');
+ iframe.setAttribute('frameborder', '0');
+
+ var refresh = function () {
+ if (attrs.crmUiIframeSrc) {
+ iframe.setAttribute('src', scope.$parent.$eval(attrs.crmUiIframeSrc));
+ }
+ else {
+ var iframeHtml = scope.$parent.$eval(attrs.crmUiIframe);
+
+ var doc = iframe.document;
+ if (iframe.contentDocument) {
+ doc = iframe.contentDocument;
+ }
+ else if (iframe.contentWindow) {
+ doc = iframe.contentWindow.document;
+ }
+
+ doc.open();
+ doc.writeln(iframeHtml);
+ doc.close();
+ }
+ };
+
+ // If the iframe is in a dialog, respond to resize events
+ $(elm).parent().on('dialogresize dialogopen', function(e, ui) {
+ $(this).css({padding: '0', margin: '0', overflow: 'hidden'});
+ iframe.setAttribute('height', '' + $(this).innerHeight() + 'px');
+ });
+
+ $(elm).parent().on('dialogresize', function(e, ui) {
+ iframe.setAttribute('class', 'resized');
+ });
+
+ scope.$parent.$watch(attrs.crmUiIframe, refresh);
+ }
+ };
+ })
+
+ // Example:
+ // <a ng-click="$broadcast('my-insert-target', 'some new text')>Insert</a>
+ // <textarea crm-ui-insert-rx='my-insert-target'></textarea>
+ .directive('crmUiInsertRx', function() {
+ return {
+ link: function(scope, element, attrs) {
+ scope.$on(attrs.crmUiInsertRx, function(e, tokenName) {
+ CRM.wysiwyg.insert(element, tokenName);
+ $(element).select2('close').select2('val', '');
+ CRM.wysiwyg.focus(element);
+ });
+ }
+ };
+ })
+
+ // Define a rich text editor.
+ // example: <textarea crm-ui-id="myForm.body_html" crm-ui-richtext name="body_html" ng-model="mailing.body_html"></textarea>
+ .directive('crmUiRichtext', function ($timeout) {
+ return {
+ require: '?ngModel',
+ link: function (scope, elm, attr, ngModel) {
+
+ var editor = CRM.wysiwyg.create(elm);
+ if (!ngModel) {
+ return;
+ }
+
+ if (attr.ngBlur) {
+ $(elm).on('blur', function() {
+ $timeout(function() {
+ scope.$eval(attr.ngBlur);
+ });
+ });
+ }
+
+ ngModel.$render = function(value) {
+ editor.done(function() {
+ CRM.wysiwyg.setVal(elm, ngModel.$viewValue || '');
+ });
+ };
+ }
+ };
+ })
+
+ // Display a lock icon (based on a boolean).
+ // example: <a crm-ui-lock binding="mymodel.boolfield"></a>
+ // example: <a crm-ui-lock
+ // binding="mymodel.boolfield"
+ // title-locked="ts('Boolfield is locked')"
+ // title-unlocked="ts('Boolfield is unlocked')"></a>
+ .directive('crmUiLock', function ($parse, $rootScope) {
+ var defaultVal = function (defaultValue) {
+ var f = function (scope) {
+ return defaultValue;
+ };
+ f.assign = function (scope, value) {
+ // ignore changes
+ };
+ return f;
+ };
+
+ // like $parse, but accepts a defaultValue in case expr is undefined
+ var parse = function (expr, defaultValue) {
+ return expr ? $parse(expr) : defaultVal(defaultValue);
+ };
+
+ return {
+ template: '',
+ link: function (scope, element, attrs) {
+ var binding = parse(attrs.binding, true);
+ var titleLocked = parse(attrs.titleLocked, ts('Locked'));
+ var titleUnlocked = parse(attrs.titleUnlocked, ts('Unlocked'));
+
+ $(element).addClass('crm-i lock-button');
+ var refresh = function () {
+ var locked = binding(scope);
+ if (locked) {
+ $(element)
+ .removeClass('fa-unlock')
+ .addClass('fa-lock')
+ .prop('title', titleLocked(scope))
+ ;
+ }
+ else {
+ $(element)
+ .removeClass('fa-lock')
+ .addClass('fa-unlock')
+ .prop('title', titleUnlocked(scope))
+ ;
+ }
+ };
+
+ $(element).click(function () {
+ binding.assign(scope, !binding(scope));
+ //scope.$digest();
+ $rootScope.$digest();
+ });
+
+ scope.$watch(attrs.binding, refresh);
+ scope.$watch(attrs.titleLocked, refresh);
+ scope.$watch(attrs.titleUnlocked, refresh);
+
+ refresh();
+ }
+ };
+ })
+
+ // CrmUiOrderCtrl is a controller class which manages sort orderings.
+ // Ex:
+ // JS: $scope.myOrder = new CrmUiOrderCtrl(['+field1', '-field2]);
+ // $scope.myOrder.toggle('field1');
+ // $scope.myOrder.setDir('field2', '');
+ // HTML: <tr ng-repeat="... | order:myOrder.get()">...</tr>
+ .service('CrmUiOrderCtrl', function(){
+ //
+ function CrmUiOrderCtrl(defaults){
+ this.values = defaults;
+ }
+ angular.extend(CrmUiOrderCtrl.prototype, {
+ get: function get() {
+ return this.values;
+ },
+ getDir: function getDir(name) {
+ if (this.values.indexOf(name) >= 0 || this.values.indexOf('+' + name) >= 0) {
+ return '+';
+ }
+ if (this.values.indexOf('-' + name) >= 0) {
+ return '-';
+ }
+ return '';
+ },
+ // @return bool TRUE if something is removed
+ remove: function remove(name) {
+ var idx = this.values.indexOf(name);
+ if (idx >= 0) {
+ this.values.splice(idx, 1);
+ return true;
+ }
+ else {
+ return false;
+ }
+ },
+ setDir: function setDir(name, dir) {
+ return this.toggle(name, dir);
+ },
+ // Toggle sort order on a field.
+ // To set a specific order, pass optional parameter 'next' ('+', '-', or '').
+ toggle: function toggle(name, next) {
+ if (!next && next !== '') {
+ next = '+';
+ if (this.remove(name) || this.remove('+' + name)) {
+ next = '-';
+ }
+ if (this.remove('-' + name)) {
+ next = '';
+ }
+ }
+
+ if (next == '+') {
+ this.values.unshift('+' + name);
+ }
+ else if (next == '-') {
+ this.values.unshift('-' + name);
+ }
+ }
+ });
+ return CrmUiOrderCtrl;
+ })
+
+ // Define a controller which manages sort order. You may interact with the controller
+ // directly ("myOrder.toggle('fieldname')") order using the helper, crm-ui-order-by.
+ // example:
+ // <span crm-ui-order="{var: 'myOrder', defaults: {'-myField'}}"></span>
+ // <th><a crm-ui-order-by="[myOrder,'myField']">My Field</a></th>
+ // <tr ng-repeat="... | order:myOrder.get()">...</tr>
+ // <button ng-click="myOrder.toggle('myField')">
+ .directive('crmUiOrder', function(CrmUiOrderCtrl) {
+ return {
+ link: function(scope, element, attrs){
+ var options = angular.extend({var: 'crmUiOrderBy'}, scope.$eval(attrs.crmUiOrder));
+ scope[options.var] = new CrmUiOrderCtrl(options.defaults);
+ }
+ };
+ })
+
+ // For usage, see crmUiOrder (above)
+ .directive('crmUiOrderBy', function() {
+ return {
+ link: function(scope, element, attrs) {
+ function updateClass(crmUiOrderCtrl, name) {
+ var dir = crmUiOrderCtrl.getDir(name);
+ element
+ .toggleClass('sorting_asc', dir === '+')
+ .toggleClass('sorting_desc', dir === '-')
+ .toggleClass('sorting', dir === '');
+ }
+
+ element.on('click', function(e){
+ var tgt = scope.$eval(attrs.crmUiOrderBy);
+ tgt[0].toggle(tgt[1]);
+ updateClass(tgt[0], tgt[1]);
+ e.preventDefault();
+ scope.$digest();
+ });
+
+ var tgt = scope.$eval(attrs.crmUiOrderBy);
+ updateClass(tgt[0], tgt[1]);
+ }
+ };
+ })
+
+ // Display a fancy SELECT (based on select2).
+ // usage: <select crm-ui-select="{placeholder:'Something',allowClear:true,...}" ng-model="myobj.field"><option...></select>
+ .directive('crmUiSelect', function ($parse, $timeout) {
+ return {
+ require: '?ngModel',
+ priority: 1,
+ scope: {
+ crmUiSelect: '='
+ },
+ link: function (scope, element, attrs, ngModel) {
+ // In cases where UI initiates update, there may be an extra
+ // call to refreshUI, but it doesn't create a cycle.
+
+ if (ngModel) {
+ ngModel.$render = function () {
+ $timeout(function () {
+ // ex: msg_template_id adds new item then selects it; use $timeout to ensure that
+ // new item is added before selection is made
+ var newVal = _.cloneDeep(ngModel.$modelValue);
+ // Fix possible data-type mismatch
+ if (typeof newVal === 'string' && element.select2('container').hasClass('select2-container-multi')) {
+ newVal = newVal.length ? newVal.split(',') : [];
+ }
+ element.select2('val', newVal);
+ });
+ };
+ }
+ function refreshModel() {
+ var oldValue = ngModel.$viewValue, newValue = element.select2('val');
+ if (oldValue != newValue) {
+ scope.$parent.$apply(function () {
+ ngModel.$setViewValue(newValue);
+ });
+ }
+ }
+
+ function init() {
+ // TODO watch select2-options
+ element.crmSelect2(scope.crmUiSelect || {});
+ if (ngModel) {
+ element.on('change', refreshModel);
+ }
+ }
+
+ init();
+ }
+ };
+ })
+
+ // Render a crmEntityRef widget
+ // usage: <input crm-entityref="{entity: 'Contact', select: {allowClear:true}}" ng-model="myobj.field" />
+ .directive('crmEntityref', function ($parse, $timeout) {
+ return {
+ require: '?ngModel',
+ scope: {
+ crmEntityref: '='
+ },
+ link: function (scope, element, attrs, ngModel) {
+ // In cases where UI initiates update, there may be an extra
+ // call to refreshUI, but it doesn't create a cycle.
+
+ ngModel.$render = function () {
+ $timeout(function () {
+ // ex: msg_template_id adds new item then selects it; use $timeout to ensure that
+ // new item is added before selection is made
+ var newVal = _.cloneDeep(ngModel.$modelValue);
+ // Fix possible data-type mismatch
+ if (typeof newVal === 'string' && element.select2('container').hasClass('select2-container-multi')) {
+ newVal = newVal.length ? newVal.split(',') : [];
+ }
+ element.select2('val', newVal);
+ });
+ };
+ function refreshModel() {
+ var oldValue = ngModel.$viewValue, newValue = element.select2('val');
+ if (oldValue != newValue) {
+ scope.$parent.$apply(function () {
+ ngModel.$setViewValue(newValue);
+ });
+ }
+ }
+
+ function init() {
+ // TODO can we infer "entity" from model?
+ element.crmEntityRef(scope.crmEntityref || {});
+ element.on('change', refreshModel);
+ $timeout(ngModel.$render);
+ }
+
+ init();
+ }
+ };
+ })
+
+ // validate multiple email text
+ // usage: <input crm-multiple-email type="text" ng-model="myobj.field" />
+ .directive('crmMultipleEmail', function ($parse, $timeout) {
+ return {
+ require: 'ngModel',
+ link: function(scope, element, attrs, ctrl) {
+ ctrl.$parsers.unshift(function(viewValue) {
+ // if empty value provided simply bypass validation
+ if (_.isEmpty(viewValue)) {
+ ctrl.$setValidity('crmMultipleEmail', true);
+ return viewValue;
+ }
+
+ // split email string on basis of comma
+ var emails = viewValue.split(',');
+ // regex pattern for single email
+ var emailRegex = /\S+@\S+\.\S+/;
+
+ var validityArr = emails.map(function(str){
+ return emailRegex.test(str.trim());
+ });
+
+ if ($.inArray(false, validityArr) > -1) {
+ ctrl.$setValidity('crmMultipleEmail', false);
+ } else {
+ ctrl.$setValidity('crmMultipleEmail', true);
+ }
+ return viewValue;
+ });
+ }
+ };
+ })
+ // example <div crm-ui-tab id="tab-1" crm-title="ts('My Title')" count="3">...content...</div>
+ // WISHLIST: use a full Angular component instead of an incomplete jQuery wrapper
+ .directive('crmUiTab', function($parse) {
+ return {
+ require: '^crmUiTabSet',
+ restrict: 'EA',
+ scope: {
+ crmTitle: '@',
+ crmIcon: '@',
+ count: '@',
+ id: '@'
+ },
+ template: '<div ng-transclude></div>',
+ transclude: true,
+ link: function (scope, element, attrs, crmUiTabSetCtrl) {
+ crmUiTabSetCtrl.add(scope);
+ }
+ };
+ })
+
+ // example: <div crm-ui-tab-set><div crm-ui-tab crm-title="Tab 1">...</div><div crm-ui-tab crm-title="Tab 2">...</div></div>
+ .directive('crmUiTabSet', function() {
+ return {
+ restrict: 'EA',
+ scope: {
+ crmUiTabSet: '@',
+ tabSetOptions: '@'
+ },
+ templateUrl: '~/crmUi/tabset.html',
+ transclude: true,
+ controllerAs: 'crmUiTabSetCtrl',
+ controller: function($scope, $parse) {
+ var tabs = $scope.tabs = []; // array<$scope>
+ this.add = function(tab) {
+ if (!tab.id) throw "Tab is missing 'id'";
+ tabs.push(tab);
+ };
+ },
+ link: function (scope, element, attrs) {}
+ };
+ })
+
+ // Generic, field-independent form validator.
+ // example: <span ng-model="placeholder" crm-ui-validate="foo && bar || whiz" />
+ // example: <span ng-model="placeholder" crm-ui-validate="foo && bar || whiz" crm-ui-validate-name="myError" />
+ .directive('crmUiValidate', function() {
+ return {
+ restrict: 'EA',
+ require: 'ngModel',
+ link: function(scope, element, attrs, ngModel) {
+ var validationKey = attrs.crmUiValidateName ? attrs.crmUiValidateName : 'crmUiValidate';
+ scope.$watch(attrs.crmUiValidate, function(newValue){
+ ngModel.$setValidity(validationKey, !!newValue);
+ });
+ }
+ };
+ })
+
+ // like ng-show, but hides/displays elements using "visibility" which maintains positioning
+ // example <div crm-ui-visible="false">...content...</div>
+ .directive('crmUiVisible', function($parse) {
+ return {
+ restrict: 'EA',
+ scope: {
+ crmUiVisible: '@'
+ },
+ link: function (scope, element, attrs) {
+ var model = $parse(attrs.crmUiVisible);
+ function updatecChildren() {
+ element.css('visibility', model(scope.$parent) ? 'inherit' : 'hidden');
+ }
+ updatecChildren();
+ scope.$parent.$watch(attrs.crmUiVisible, updatecChildren);
+ }
+ };
+ })
+
+ // example: <div crm-ui-wizard="myWizardCtrl"><div crm-ui-wizard-step crm-title="ts('Step 1')">...</div><div crm-ui-wizard-step crm-title="ts('Step 2')">...</div></div>
+ // example with custom nav classes: <div crm-ui-wizard crm-ui-wizard-nav-class="ng-animate-out ...">...</div>
+ // Note: "myWizardCtrl" has various actions/properties like next() and $first().
+ // WISHLIST: Allow each step to determine if it is "complete" / "valid" / "selectable"
+ // WISHLIST: Allow each step to enable/disable (show/hide) itself
+ .directive('crmUiWizard', function() {
+ return {
+ restrict: 'EA',
+ scope: {
+ crmUiWizard: '@',
+ crmUiWizardNavClass: '@' // string, A list of classes that will be added to the nav items
+ },
+ templateUrl: '~/crmUi/wizard.html',
+ transclude: true,
+ controllerAs: 'crmUiWizardCtrl',
+ controller: function($scope, $parse) {
+ var steps = $scope.steps = []; // array<$scope>
+ var crmUiWizardCtrl = this;
+ var maxVisited = 0;
+ var selectedIndex = null;
+
+ var findIndex = function() {
+ var found = null;
+ angular.forEach(steps, function(step, stepKey) {
+ if (step.selected) found = stepKey;
+ });
+ return found;
+ };
+
+ /// @return int the index of the current step
+ this.$index = function() { return selectedIndex; };
+ /// @return bool whether the currentstep is first
+ this.$first = function() { return this.$index() === 0; };
+ /// @return bool whether the current step is last
+ this.$last = function() { return this.$index() === steps.length -1; };
+ this.$maxVisit = function() { return maxVisited; };
+ this.$validStep = function() {
+ return steps[selectedIndex] && steps[selectedIndex].isStepValid();
+ };
+ this.iconFor = function(index) {
+ if (index < this.$index()) return '√';
+ if (index === this.$index()) return '»';
+ return ' ';
+ };
+ this.isSelectable = function(step) {
+ if (step.selected) return false;
+ return this.$validStep();
+ };
+
+ /*** @param Object step the $scope of the step */
+ this.select = function(step) {
+ angular.forEach(steps, function(otherStep, otherKey) {
+ otherStep.selected = (otherStep === step);
+ if (otherStep === step && maxVisited < otherKey) maxVisited = otherKey;
+ });
+ selectedIndex = findIndex();
+ };
+ /*** @param Object step the $scope of the step */
+ this.add = function(step) {
+ if (steps.length === 0) {
+ step.selected = true;
+ selectedIndex = 0;
+ }
+ steps.push(step);
+ steps.sort(function(a,b){
+ return a.crmUiWizardStep - b.crmUiWizardStep;
+ });
+ selectedIndex = findIndex();
+ };
+ this.remove = function(step) {
+ var key = null;
+ angular.forEach(steps, function(otherStep, otherKey) {
+ if (otherStep === step) key = otherKey;
+ });
+ if (key !== null) {
+ steps.splice(key, 1);
+ }
+ };
+ this.goto = function(index) {
+ if (index < 0) index = 0;
+ if (index >= steps.length) index = steps.length-1;
+ this.select(steps[index]);
+ };
+ this.previous = function() { this.goto(this.$index()-1); };
+ this.next = function() { this.goto(this.$index()+1); };
+ if ($scope.crmUiWizard) {
+ $parse($scope.crmUiWizard).assign($scope.$parent, this);
+ }
+ },
+ link: function (scope, element, attrs) {
+ scope.ts = CRM.ts(null);
+
+ element.find('.crm-wizard-buttons button[ng-click^=crmUiWizardCtrl]').click(function () {
+ // These values are captured inside the click handler to ensure the
+ // positions/sizes of the elements are captured at the time of the
+ // click vs. at the time this directive is initialized.
+ var topOfWizard = element.offset().top;
+ var heightOfMenu = $('#civicrm-menu').height() || 0;
+
+ $('html')
+ // stop any other animations that might be happening...
+ .stop()
+ // gracefully slide the user to the top of the wizard
+ .animate({scrollTop: topOfWizard - heightOfMenu}, 1000);
+ });
+ }
+ };
+ })
+
+ // Use this to add extra markup to wizard
+ .directive('crmUiWizardButtons', function() {
+ return {
+ require: '^crmUiWizard',
+ restrict: 'EA',
+ scope: {},
+ template: '<span ng-transclude></span>',
+ transclude: true,
+ link: function (scope, element, attrs, crmUiWizardCtrl) {
+ var realButtonsEl = $(element).closest('.crm-wizard').find('.crm-wizard-buttons');
+ $(element).appendTo(realButtonsEl);
+ }
+ };
+ })
+
+ // Example for Font Awesome: <button crm-icon="fa-check">Save</button>
+ // Example for jQuery UI (deprecated): <button crm-icon="check">Save</button>
+ .directive('crmIcon', function() {
+ return {
+ restrict: 'EA',
+ link: function (scope, element, attrs) {
+ if (element.is('[crm-ui-tab]')) {
+ // handled in crmUiTab ctrl
+ return;
+ }
+ if (attrs.crmIcon.substring(0,3) == 'fa-') {
+ $(element).prepend('<i class="crm-i ' + attrs.crmIcon + '"></i> ');
+ }
+ else {
+ $(element).prepend('<span class="icon ui-icon-' + attrs.crmIcon + '"></span> ');
+ }
+ if ($(element).is('button')) {
+ $(element).addClass('crm-button');
+ }
+ }
+ };
+ })
+
+ // example: <div crm-ui-wizard-step crm-title="ts('My Title')" ng-form="mySubForm">...content...</div>
+ // If there are any conditional steps, then be sure to set a weight explicitly on *all* steps to maintain ordering.
+ // example: <div crm-ui-wizard-step="100" crm-title="..." ng-if="...">...content...</div>
+ // example with custom classes: <div crm-ui-wizard-step="100" crm-ui-wizard-step-class="ng-animate-out ...">...content...</div>
+ .directive('crmUiWizardStep', function() {
+ var nextWeight = 1;
+ return {
+ require: ['^crmUiWizard', 'form'],
+ restrict: 'EA',
+ scope: {
+ crmTitle: '@', // expression, evaluates to a printable string
+ crmUiWizardStep: '@', // int, a weight which determines the ordering of the steps
+ crmUiWizardStepClass: '@' // string, A list of classes that will be added to the template
+ },
+ template: '<div class="crm-wizard-step {{crmUiWizardStepClass}}" ng-show="selected" ng-transclude/></div>',
+ transclude: true,
+ link: function (scope, element, attrs, ctrls) {
+ var crmUiWizardCtrl = ctrls[0], form = ctrls[1];
+ if (scope.crmUiWizardStep) {
+ scope.crmUiWizardStep = parseInt(scope.crmUiWizardStep);
+ } else {
+ scope.crmUiWizardStep = nextWeight++;
+ }
+ scope.isStepValid = function() {
+ return form.$valid;
+ };
+ crmUiWizardCtrl.add(scope);
+ scope.$on('$destroy', function(){
+ crmUiWizardCtrl.remove(scope);
+ });
+ }
+ };
+ })
+
+ // Example: <button crm-confirm="{message: ts('Are you sure you want to continue?')}" on-yes="frobnicate(123)">Frobincate</button>
+ // Example: <button crm-confirm="{type: 'disable', obj: myObject}" on-yes="myObject.is_active=0; myObject.save()">Disable</button>
+ // Example: <button crm-confirm="{templateUrl: '~/path/to/view.html', export: {foo: bar}}" on-yes="frobnicate(123)">Frobincate</button>
+ .directive('crmConfirm', function ($compile, $rootScope, $templateRequest, $q) {
+ // Helpers to calculate default options for CRM.confirm()
+ var defaultFuncs = {
+ 'disable': function (options) {
+ return {
+ message: ts('Are you sure you want to disable this?'),
+ options: {no: ts('Cancel'), yes: ts('Disable')},
+ width: 300,
+ title: ts('Disable %1?', {
+ 1: options.obj.title || options.obj.label || options.obj.name || ts('the record')
+ })
+ };
+ },
+ 'revert': function (options) {
+ return {
+ message: ts('Are you sure you want to revert this?'),
+ options: {no: ts('Cancel'), yes: ts('Revert')},
+ width: 300,
+ title: ts('Revert %1?', {
+ 1: options.obj.title || options.obj.label || options.obj.name || ts('the record')
+ })
+ };
+ },
+ 'delete': function (options) {
+ return {
+ message: ts('Are you sure you want to delete this?'),
+ options: {no: ts('Cancel'), yes: ts('Delete')},
+ width: 300,
+ title: ts('Delete %1?', {
+ 1: options.obj.title || options.obj.label || options.obj.name || ts('the record')
+ })
+ };
+ }
+ };
+ var confirmCount = 0;
+ return {
+ link: function (scope, element, attrs) {
+ $(element).click(function () {
+ var options = scope.$eval(attrs.crmConfirm);
+ if (attrs.title && !options.title) {
+ options.title = attrs.title;
+ }
+ var defaults = (options.type) ? defaultFuncs[options.type](options) : {};
+
+ var tpl = null, stubId = null;
+ if (!options.message) {
+ if (options.templateUrl) {
+ tpl = $templateRequest(options.templateUrl);
+ }
+ else if (options.template) {
+ tpl = options.template;
+ }
+ if (tpl) {
+ stubId = 'crmUiConfirm_' + (++confirmCount);
+ options.message = '<div id="' + stubId + '"></div>';
+ }
+ }
+
+ CRM.confirm(_.extend(defaults, options))
+ .on('crmConfirm:yes', function() { scope.$apply(attrs.onYes); })
+ .on('crmConfirm:no', function() { scope.$apply(attrs.onNo); });
+
+ if (tpl && stubId) {
+ $q.when(tpl, function(html) {
+ var scope = options.scope || $rootScope.$new();
+ if (options.export) {
+ angular.extend(scope, options.export);
+ }
+ var linker = $compile(html);
+ $('#' + stubId).append($(linker(scope)));
+ });
+ }
+ });
+ }
+ };
+ })
+
+ // Sets document title & page title; attempts to override CMS title markup for the latter
+ // WARNING: Use only once per route!
+ // WARNING: This directive works only if your AngularJS base page does not
+ // set a custom title (i.e., it has an initial title of "CiviCRM"). See the
+ // global variables pageTitle and documentTitle.
+ // Example (same title for both): <h1 crm-page-title>{{ts('Hello')}}</h1>
+ // Example (separate document title): <h1 crm-document-title="ts('Hello')" crm-page-title><i class="crm-i fa-flag"></i>{{ts('Hello')}}</h1>
+ .directive('crmPageTitle', function($timeout) {
+ return {
+ scope: {
+ crmDocumentTitle: '='
+ },
+ link: function(scope, $el, attrs) {
+ function update() {
+ $timeout(function() {
+ var newPageTitle = _.trim($el.html()),
+ newDocumentTitle = scope.crmDocumentTitle || $el.text();
+ document.title = $('title').text().replace(documentTitle, newDocumentTitle);
+ // If the CMS has already added title markup to the page, use it
+ $('h1').not('.crm-container h1').each(function() {
+ if (_.trim($(this).html()) === pageTitle) {
+ $(this).addClass('crm-page-title').html(newPageTitle);
+ $el.hide();
+ }
+ });
+ pageTitle = newPageTitle;
+ documentTitle = newDocumentTitle;
+ });
+ }
+
+ scope.$watch(function() {return scope.crmDocumentTitle + $el.html();}, update);
+ }
+ };
+ })
+
+ .run(function($rootScope, $location) {
+ /// Example: <button ng-click="goto('home')">Go home!</button>
+ $rootScope.goto = function(path) {
+ $location.path(path);
+ };
+ // useful for debugging: $rootScope.log = console.log || function() {};
+ })
+ ;
+
+})(angular, CRM.$, CRM._);
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmUi/field-cb.html b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmUi/field-cb.html
new file mode 100644
index 00000000..fe1a2ab9
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmUi/field-cb.html
@@ -0,0 +1,8 @@
+<label crm-depth="1">
+ <span ng-transclude></span>
+ <span ng-class="cssClasses">
+ {{crmUiField.title}}
+ </span>
+</label>
+<a crm-ui-help="help" ng-if="crmUiField.help"></a>
+<div class="clear"></div>
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmUi/field.html b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmUi/field.html
new file mode 100644
index 00000000..da6521a6
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmUi/field.html
@@ -0,0 +1,6 @@
+<div class="label">
+ <label crm-ui-for="{{crmUiField.name}}" crm-depth="1" crm-ui-force-required="{{crmUiField.required}}">{{crmUiField.title}}</label>
+ <a crm-ui-help="help" ng-if="help"></a>
+</div>
+<div class="content" ng-transclude></div>
+<div class="clear"></div>
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmUi/tabset.html b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmUi/tabset.html
new file mode 100644
index 00000000..6bb45711
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmUi/tabset.html
@@ -0,0 +1,12 @@
+<div ui-jq="tabs" ui-options="{{tabSetOptions}}" class="crm-tabset">
+ <ul>
+ <li ng-repeat="tab in tabs" class="ui-corner-all crm-tab-button crm-count-{{tab.count}}">
+ <a href="#{{tab.id}}">
+ <i ng-if="tab.crmIcon" class="crm-i {{tab.crmIcon}}"></i>
+ {{tab.$parent.$eval(tab.crmTitle)}}
+ <em ng-if="tab.count">{{tab.count}}</em>
+ </a>
+ </li>
+ </ul>
+ <div ng-transclude></div>
+</div> \ No newline at end of file
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmUi/wizard.html b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmUi/wizard.html
new file mode 100644
index 00000000..116f060d
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmUi/wizard.html
@@ -0,0 +1,15 @@
+<div class="crm-wizard">
+ <ul class="crm-wizard-nav wizard-bar">
+ <li ng-repeat="step in steps" ng-class="{'current-step':step.selected}" class="{{crmUiWizardNavClass}}">
+ <span>{{crmUiWizardCtrl.iconFor($index)}}</span>
+ <a href="" ng-click="crmUiWizardCtrl.select(step)" ng-show="crmUiWizardCtrl.isSelectable(step)">{{1 + $index}}. {{step.$parent.$eval(step.crmTitle)}}</a>
+ <span ng-show="!crmUiWizardCtrl.isSelectable(step)">{{1 + $index}}. {{step.$parent.$eval(step.crmTitle)}}</span>
+ <!-- Don't know a good way to localize text like "1. Title" -->
+ </li>
+ </ul>
+ <div class="crm-wizard-body" ng-transclude></div>
+ <div class="crm-wizard-buttons">
+ <button crm-icon="fa-chevron-left" ng-click="crmUiWizardCtrl.previous()" ng-show="!crmUiWizardCtrl.$first()" class="crmUi-btn-primary">{{ts('Previous')}}</button>
+ <button crm-icon="fa-chevron-right" title="{{!crmUiWizardCtrl.$validStep() ? ts('Complete all required fields first') : ts('Next step')}}" ng-click="crmUiWizardCtrl.next()" ng-show="!crmUiWizardCtrl.$last()" ng-disabled="!crmUiWizardCtrl.$validStep()" class="crmUi-btn-primary">{{ts('Next')}}</button>
+ </div>
+</div>
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmUtil.ang.php b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmUtil.ang.php
new file mode 100644
index 00000000..78ca548c
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmUtil.ang.php
@@ -0,0 +1,10 @@
+<?php
+// This file declares an Angular module which can be autoloaded
+// in CiviCRM. See also:
+// http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_angularModules
+
+return [
+ 'ext' => 'civicrm',
+ 'js' => ['ang/crmUtil.js'],
+ 'requires' => [],
+];
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmUtil.js b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmUtil.js
new file mode 100644
index 00000000..ab460ab7
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmUtil.js
@@ -0,0 +1,361 @@
+/// crmUi: Sundry UI helpers
+(function (angular, $, _) {
+ angular.module('crmUtil', CRM.angRequires('crmUtil'));
+
+ // Angular implementation of CRM.api3
+ // @link http://wiki.civicrm.org/confluence/display/CRMDOC/AJAX+Interface#AJAXInterface-CRM.api3
+ //
+ // Note: To mock API results in unit-tests, override crmApi.backend, e.g.
+ // var apiSpy = jasmine.createSpy('crmApi');
+ // crmApi.backend = apiSpy.and.returnValue(crmApi.val({
+ // is_error: 1
+ // }));
+ angular.module('crmUtil').factory('crmApi', function($q) {
+ var crmApi = function(entity, action, params, message) {
+ // JSON serialization in CRM.api3 is not aware of Angular metadata like $$hash, so use angular.toJson()
+ var deferred = $q.defer();
+ var p;
+ var backend = crmApi.backend || CRM.api3;
+ if (params && params.body_html) {
+ // CRM-18474 - remove Unicode Character 'LINE SEPARATOR' (U+2028)
+ // and 'PARAGRAPH SEPARATOR' (U+2029) from the html if present.
+ params.body_html = params.body_html.replace(/([\u2028]|[\u2029])/g, '\n');
+ }
+ if (_.isObject(entity)) {
+ // eval content is locally generated.
+ /*jshint -W061 */
+ p = backend(eval('('+angular.toJson(entity)+')'), action);
+ } else {
+ // eval content is locally generated.
+ /*jshint -W061 */
+ p = backend(entity, action, eval('('+angular.toJson(params)+')'), message);
+ }
+ // CRM.api3 returns a promise, but the promise doesn't really represent errors as errors, so we
+ // convert them
+ p.then(
+ function(result) {
+ if (result.is_error) {
+ deferred.reject(result);
+ } else {
+ deferred.resolve(result);
+ }
+ },
+ function(error) {
+ deferred.reject(error);
+ }
+ );
+ return deferred.promise;
+ };
+ crmApi.backend = null;
+ crmApi.val = function(value) {
+ var d = $.Deferred();
+ d.resolve(value);
+ return d.promise();
+ };
+ return crmApi;
+ });
+
+ // Get and cache the metadata for an API entity.
+ // usage:
+ // $q.when(crmMetadata.getFields('MyEntity'), function(fields){
+ // console.log('The fields are:', options);
+ // });
+ angular.module('crmUtil').factory('crmMetadata', function($q, crmApi) {
+
+ // Convert {key:$,value:$} sequence to unordered {$key: $value} map.
+ function convertOptionsToMap(options) {
+ var result = {};
+ angular.forEach(options, function(o) {
+ result[o.key] = o.value;
+ });
+ return result;
+ }
+
+ var cache = {}; // cache[entityName+'::'+action][fieldName].title
+ var deferreds = {}; // deferreds[cacheKey].push($q.defer())
+ var crmMetadata = {
+ // usage: $q.when(crmMetadata.getField('MyEntity', 'my_field')).then(...);
+ getField: function getField(entity, field) {
+ return $q.when(crmMetadata.getFields(entity)).then(function(fields){
+ return fields[field];
+ });
+ },
+ // usage: $q.when(crmMetadata.getFields('MyEntity')).then(...);
+ // usage: $q.when(crmMetadata.getFields(['MyEntity', 'myaction'])).then(...);
+ getFields: function getFields(entity) {
+ var action = '', cacheKey;
+ if (_.isArray(entity)) {
+ action = entity[1];
+ entity = entity[0];
+ cacheKey = entity + '::' + action;
+ } else {
+ cacheKey = entity;
+ }
+
+ if (_.isObject(cache[cacheKey])) {
+ return cache[cacheKey];
+ }
+
+ var needFetch = _.isEmpty(deferreds[cacheKey]);
+ deferreds[cacheKey] = deferreds[cacheKey] || [];
+ var deferred = $q.defer();
+ deferreds[cacheKey].push(deferred);
+
+ if (needFetch) {
+ crmApi(entity, 'getfields', {action: action, sequential: 1, options: {get_options: 'all'}})
+ .then(
+ // on success:
+ function(fields) {
+ cache[cacheKey] = _.indexBy(fields.values, 'name');
+ angular.forEach(cache[cacheKey],function (field){
+ if (field.options) {
+ field.optionsMap = convertOptionsToMap(field.options);
+ }
+ });
+ angular.forEach(deferreds[cacheKey], function(dfr) {
+ dfr.resolve(cache[cacheKey]);
+ });
+ delete deferreds[cacheKey];
+ },
+ // on error:
+ function() {
+ cache[cacheKey] = {}; // cache nack
+ angular.forEach(deferreds[cacheKey], function(dfr) {
+ dfr.reject();
+ });
+ delete deferreds[cacheKey];
+ }
+ );
+ }
+
+ return deferred.promise;
+ }
+ };
+
+ return crmMetadata;
+ });
+
+ // usage:
+ // var block = $scope.block = crmBlocker();
+ // $scope.save = function() { return block(crmApi('MyEntity','create',...)); };
+ // <button ng-click="save()" ng-disabled="block.check()">Do something</button>
+ angular.module('crmUtil').factory('crmBlocker', function() {
+ return function() {
+ var blocks = 0;
+ var result = function(promise) {
+ blocks++;
+ return promise.finally(function() {
+ blocks--;
+ });
+ };
+ result.check = function() {
+ return blocks > 0;
+ };
+ return result;
+ };
+ });
+
+ angular.module('crmUtil').factory('crmLegacy', function() {
+ return CRM;
+ });
+
+ // example: scope.$watch('foo', crmLog.wrap(function(newValue, oldValue){ ... }));
+ angular.module('crmUtil').factory('crmLog', function(){
+ var level = 0;
+ var write = console.log;
+ function indent() {
+ var s = '>';
+ for (var i = 0; i < level; i++) s = s + ' ';
+ return s;
+ }
+ var crmLog = {
+ log: function(msg, vars) {
+ write(indent() + msg, vars);
+ },
+ wrap: function(label, f) {
+ return function(){
+ level++;
+ crmLog.log(label + ": start", arguments);
+ var r;
+ try {
+ r = f.apply(this, arguments);
+ } finally {
+ crmLog.log(label + ": end");
+ level--;
+ }
+ return r;
+ };
+ }
+ };
+ return crmLog;
+ });
+
+ angular.module('crmUtil').factory('crmNavigator', ['$window', function($window) {
+ return {
+ redirect: function(path) {
+ $window.location.href = path;
+ }
+ };
+ }]);
+
+ // Wrap an async function in a queue, ensuring that independent async calls are issued in strict sequence.
+ // usage: qApi = crmQueue(crmApi); qApi(entity,action,...).then(...); qApi(entity2,action2,...).then(...);
+ // This is similar to promise-chaining, but allows chaining independent procs (without explicitly sharing promises).
+ angular.module('crmUtil').factory('crmQueue', function($q) {
+ // @param worker A function which generates promises
+ return function crmQueue(worker) {
+ var queue = [];
+ function next() {
+ var task = queue[0];
+ worker.apply(null, task.a).then(
+ function onOk(data) {
+ queue.shift();
+ task.dfr.resolve(data);
+ if (queue.length > 0) next();
+ },
+ function onErr(err) {
+ queue.shift();
+ task.dfr.reject(err);
+ if (queue.length > 0) next();
+ }
+ );
+ }
+ function enqueue() {
+ var dfr = $q.defer();
+ queue.push({a: arguments, dfr: dfr});
+ if (queue.length === 1) {
+ next();
+ }
+ return dfr.promise;
+ }
+ return enqueue;
+ };
+ });
+
+ // Adapter for CRM.status which supports Angular promises (instead of jQuery promises)
+ // example: crmStatus('Saving', crmApi(...)).then(function(result){...})
+ angular.module('crmUtil').factory('crmStatus', function($q){
+ return function(options, aPromise){
+ if (aPromise) {
+ return CRM.toAPromise($q, CRM.status(options, CRM.toJqPromise(aPromise)));
+ } else {
+ return CRM.toAPromise($q, CRM.status(options));
+ }
+ };
+ });
+
+ // crmWatcher allows one to setup event listeners and temporarily suspend
+ // them en masse.
+ //
+ // example:
+ // angular.controller(... function($scope, crmWatcher){
+ // var watcher = crmWatcher();
+ // function myfunc() {
+ // watcher.suspend('foo', function(){
+ // ...do stuff...
+ // });
+ // }
+ // watcher.setup('foo', function(){
+ // return [
+ // $scope.$watch('foo', myfunc),
+ // $scope.$watch('bar', myfunc),
+ // $scope.$watch('whiz', otherfunc)
+ // ];
+ // });
+ // });
+ angular.module('crmUtil').factory('crmWatcher', function(){
+ return function() {
+ var unwatches = {}, watchFactories = {}, suspends = {};
+
+ // Specify the list of watches
+ this.setup = function(name, newWatchFactory) {
+ watchFactories[name] = newWatchFactory;
+ unwatches[name] = watchFactories[name]();
+ suspends[name] = 0;
+ return this;
+ };
+
+ // Temporarily disable watches and run some logic
+ this.suspend = function(name, f) {
+ suspends[name]++;
+ this.teardown(name);
+ var r;
+ try {
+ r = f.apply(this, []);
+ } finally {
+ if (suspends[name] === 1) {
+ unwatches[name] = watchFactories[name]();
+ if (!angular.isArray(unwatches[name])) {
+ unwatches[name] = [unwatches[name]];
+ }
+ }
+ suspends[name]--;
+ }
+ return r;
+ };
+
+ this.teardown = function(name) {
+ if (!unwatches[name]) return;
+ _.each(unwatches[name], function(unwatch){
+ unwatch();
+ });
+ delete unwatches[name];
+ };
+
+ return this;
+ };
+ });
+
+ // Run a given function. If it is already running, wait for it to finish before running again.
+ // If multiple requests are made before the first request finishes, all but the last will be ignored.
+ // This prevents overwhelming the server with redundant queries during e.g. an autocomplete search while the user types.
+ // Given function should return an angular promise. crmThrottle will deliver the contents when resolved.
+ angular.module('crmUtil').factory('crmThrottle', function($q) {
+ var pending = [],
+ executing = [];
+ return function(func) {
+ var deferred = $q.defer();
+
+ function checkResult(result, success) {
+ _.pull(executing, func);
+ if (_.includes(pending, func)) {
+ runNext();
+ } else if (success) {
+ deferred.resolve(result);
+ } else {
+ deferred.reject(result);
+ }
+ }
+
+ function runNext() {
+ executing.push(func);
+ _.pull(pending, func);
+ func().then(function(result) {
+ checkResult(result, true);
+ }, function(result) {
+ checkResult(result, false);
+ });
+ }
+
+ if (!_.includes(executing, func)) {
+ runNext();
+ } else if (!_.includes(pending, func)) {
+ pending.push(func);
+ }
+ return deferred.promise;
+ };
+ });
+
+ angular.module('crmUtil').factory('crmLoadScript', function($q) {
+ return function(url) {
+ var deferred = $q.defer();
+
+ CRM.loadScript(url).done(function() {
+ deferred.resolve(true);
+ });
+
+ return deferred.promise;
+ };
+ });
+
+})(angular, CRM.$, CRM._);
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/dialogService.ang.php b/www/crm/wp-content/plugins/civicrm/civicrm/ang/dialogService.ang.php
new file mode 100644
index 00000000..31a9a8ac
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/dialogService.ang.php
@@ -0,0 +1,11 @@
+<?php
+// This file declares an Angular module which can be autoloaded
+// in CiviCRM. See also:
+// http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_angularModules
+
+// https://github.com/jwstadler/angular-jquery-dialog-service
+
+return [
+ 'ext' => 'civicrm',
+ 'js' => ['bower_components/angular-jquery-dialog-service/dialog-service.js'],
+];
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/ngRoute.ang.php b/www/crm/wp-content/plugins/civicrm/civicrm/ang/ngRoute.ang.php
new file mode 100644
index 00000000..11c6d868
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/ngRoute.ang.php
@@ -0,0 +1,9 @@
+<?php
+// This file declares an Angular module which can be autoloaded
+// in CiviCRM. See also:
+// http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_angularModules
+
+return [
+ 'ext' => 'civicrm',
+ 'js' => ['bower_components/angular-route/angular-route.min.js'],
+];
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/ngSanitize.ang.php b/www/crm/wp-content/plugins/civicrm/civicrm/ang/ngSanitize.ang.php
new file mode 100644
index 00000000..36329e55
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/ngSanitize.ang.php
@@ -0,0 +1,9 @@
+<?php
+// This file declares an Angular module which can be autoloaded
+// in CiviCRM. See also:
+// http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_angularModules
+
+return [
+ 'ext' => 'civicrm',
+ 'js' => ['bower_components/angular-sanitize/angular-sanitize.min.js'],
+];
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/ui.bootstrap.ang.php b/www/crm/wp-content/plugins/civicrm/civicrm/ang/ui.bootstrap.ang.php
new file mode 100644
index 00000000..417d7bf6
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/ui.bootstrap.ang.php
@@ -0,0 +1,11 @@
+<?php
+// This file declares an Angular module which can be autoloaded
+// in CiviCRM. See also:
+// http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_angularModules
+
+return [
+ 'ext' => 'civicrm',
+ 'basePages' => [],
+ 'js' => ['bower_components/angular-bootstrap/ui-bootstrap-tpls.min.js'],
+ 'css' => ['bower_components/angular-bootstrap/ui-bootstrap-csp.css', 'ang/ui.bootstrap.css'],
+];
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/ui.bootstrap.css b/www/crm/wp-content/plugins/civicrm/civicrm/ang/ui.bootstrap.css
new file mode 100644
index 00000000..e8cf3de9
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/ui.bootstrap.css
@@ -0,0 +1 @@
+.nav, .pagination, .carousel, .panel-title a { cursor: pointer; }
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/ui.sortable.ang.php b/www/crm/wp-content/plugins/civicrm/civicrm/ang/ui.sortable.ang.php
new file mode 100644
index 00000000..9679e6e6
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/ui.sortable.ang.php
@@ -0,0 +1,9 @@
+<?php
+// This file declares an Angular module which can be autoloaded
+// in CiviCRM. See also:
+// http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_angularModules
+
+return [
+ 'ext' => 'civicrm',
+ 'js' => ['bower_components/angular-ui-sortable/sortable.min.js'],
+];
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/ui.utils.ang.php b/www/crm/wp-content/plugins/civicrm/civicrm/ang/ui.utils.ang.php
new file mode 100644
index 00000000..58b798a4
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/ui.utils.ang.php
@@ -0,0 +1,9 @@
+<?php
+// This file declares an Angular module which can be autoloaded
+// in CiviCRM. See also:
+// http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_angularModules
+
+return [
+ 'ext' => 'civicrm',
+ 'js' => ['bower_components/angular-ui-utils/ui-utils.min.js'],
+];
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/unsavedChanges.ang.php b/www/crm/wp-content/plugins/civicrm/civicrm/ang/unsavedChanges.ang.php
new file mode 100644
index 00000000..f8822cd2
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/unsavedChanges.ang.php
@@ -0,0 +1,9 @@
+<?php
+// This file declares an Angular module which can be autoloaded
+// in CiviCRM. See also:
+// http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_angularModules
+
+return [
+ 'ext' => 'civicrm',
+ 'js' => ['bower_components/angular-unsavedChanges/dist/unsavedChanges.min.js'],
+];