First commit

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

View file

@ -0,0 +1,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 array(
'ext' => 'civicrm',
'js' => array('bower_components/angular-file-upload/angular-file-upload.min.js'),
);

View file

@ -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 array(
'ext' => 'civicrm',
'js' => array('ang/crmApp.js'),
);

View file

@ -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);

View file

@ -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 array(
'ext' => 'civicrm',
'js' => array('ang/crmAttachment.js'),
'css' => array('ang/crmAttachment.css'),
'partials' => array('ang/crmAttachment'),
'settings' => array(
'token' => \CRM_Core_Page_AJAX_Attachment::createToken(),
),
'requires' => array('angularFileUpload', 'crmResource'),
);

View file

@ -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;
}

View file

@ -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._);

View file

@ -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>

View file

@ -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 array(
'ext' => 'civicrm',
'js' => array('ang/crmAutosave.js'),
'requires' => array('crmUtil'),
);

View file

@ -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._);

View file

@ -0,0 +1,20 @@
<?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.
CRM_Core_Resources::singleton()->addSetting(array(
'crmCaseType' => array(
'REL_TYPE_CNAME' => CRM_Case_XMLProcessor::REL_TYPE_CNAME,
),
));
return array(
'ext' => 'civicrm',
'js' => array('ang/crmCaseType.js'),
'css' => array('ang/crmCaseType.css'),
'partials' => array('ang/crmCaseType'),
'requires' => array('ngRoute', 'ui.utils', 'crmUi', 'unsavedChanges', 'crmUtil', 'crmResource'),
);

View file

@ -0,0 +1,37 @@
.crmCaseType .grip-n-drag {
vertical-align: middle;
cursor: move;
}
.crmCaseType .fa-trash {
margin: 0.4em 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;
}

View file

@ -0,0 +1,401 @@
(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.relTypes = ['RelationshipType', 'get', {
sequential: 1,
options: {
sort: CRM.crmCaseType.REL_TYPE_CNAME,
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: 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();
});
scope.$watch(attrs.crmOptions, function(value) {
$(input).select2('data', scope[attrs.crmOptions]);
$(input).select2('val', '');
});
}
};
});
crmCaseType.controller('CaseTypeCtrl', function($scope, crmApi, apiCalls) {
// CRM_Case_XMLProcessor::REL_TYPE_CNAME
var REL_TYPE_CNAME = CRM.crmCaseType.REL_TYPE_CNAME,
ts = $scope.ts = CRM.ts(null);
$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.relationshipTypeOptions = _.map(apiCalls.relTypes.values, function(type) {
return {id: type[REL_TYPE_CNAME], text: type.label_b_a};
});
$scope.locks = {caseTypeName: true, activitySetName: true};
$scope.workflows = {
'timeline': 'Timeline',
'sequence': 'Sequence'
};
$scope.caseType = apiCalls.caseType ? apiCalls.caseType : _.cloneDeep(newCaseTypeTemplate);
$scope.caseType.definition = $scope.caseType.definition || [];
$scope.caseType.definition.activityTypes = $scope.caseType.definition.activityTypes || [];
$scope.caseType.definition.activitySets = $scope.caseType.definition.activitySets || [];
_.each($scope.caseType.definition.activitySets, function (set) {
_.each(set.activityTypes, function (type, name) {
type.label = $scope.activityTypes[type.name].label;
});
});
$scope.caseType.definition.caseRoles = $scope.caseType.definition.caseRoles || [];
$scope.caseType.definition.statuses = $scope.caseType.definition.statuses || [];
$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.push({
name: activityTypeName,
label: $scope.activityTypes[activityTypeName].label,
status: 'Scheduled',
reference_activity: 'Open Case',
reference_offset: '1',
reference_select: 'newest'
});
}
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});
});
}
}
};
/// Add a new role
$scope.addRole = function(roles, roleName) {
var names = _.pluck($scope.caseType.definition.caseRoles, 'name');
if (!_.contains(names, roleName)) {
if (_.where($scope.relationshipTypeOptions, {id: roleName}).length) {
roles.push({name: roleName});
} else {
CRM.loadForm(CRM.url('civicrm/admin/reltype', {action: 'add', reset: 1, label_a_b: roleName, label_b_a: roleName}))
.on('crmFormSuccess', function(e, data) {
roles.push({name: data.relationshipType[REL_TYPE_CNAME]});
$scope.relationshipTypeOptions.push({id: data.relationshipType[REL_TYPE_CNAME], text: data.relationshipType.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);
}
};
$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) {
if (activitySet.name == 'standard_timeline' && activity.name == 'Open Case') {
return false;
} else {
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;
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.$digest();
});
};
$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._);

View file

@ -0,0 +1,28 @@
<!--
Controller: CaseTypeCtrl
Required vars: activitySet
-->
<table class="form-layout-compressed">
<tbody>
<tr>
<td class="label">{{ts('Label')}}</td>
<td>
<input type="text" name="label" class="crm-form-text" ng-model="activitySet.label"/>
</td>
</tr>
<tr>
<td class="label">{{ts('Name')}}</td>
<td>
<input type="text" name="name" class="crm-form-text" ng-model="activitySet.name" ng-disabled="locks.activitySetName" />
<a crm-ui-lock binding="locks.activitySetName"></a>
</td>
</tr>
<tr>
<td class="label">{{ts('Workflow')}}</td>
<td>
{{ getWorkflowName(activitySet) }}
</td>
</tr>
</tbody>
</table>

View file

@ -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>

View file

@ -0,0 +1,45 @@
<!--
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>
</div>
</div>

View file

@ -0,0 +1,64 @@
<!--
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}}">{{ activitySet.label }}</a>
<span class="crm-i fa-trash" title="{{ts('Remove')}}"
ng-hide="activitySet.name == 'standard_timeline'"
ng-click="removeItem(caseType.definition.activitySets, activitySet)">{{ts('Remove')}}</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 class="crm-accordion-wrapper collapsed">
<div class="crm-accordion-header">{{ts('Advanced')}}</div>
<div class="crm-accordion-body" ng-include="'~/crmCaseType/activitySetDetails.html'"></div>
</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>

View file

@ -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">
{{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">
<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>
<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>

View file

@ -0,0 +1,37 @@
<!--
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'">
<td>{{relType.name}}</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>

View file

@ -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>

View file

@ -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>

View file

@ -0,0 +1,92 @@
<!--
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></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 activitySet.activityTypes"
ng-hide="activity.name == 'Open Case'"
ng-required="activity.name != 'Open Case'"
>
</select>
</td>
<td>
<input
class="number crm-form-text"
type="text"
ng-pattern="/^-?[0-9]*$/"
ng-model="activity.reference_offset"
ng-hide="activity.name == 'Open Case'"
ng-required="activity.name != 'Open Case'"
>
</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')}"
ng-hide="activity.name == 'Open Case'"
ng-required="activity.name != 'Open Case'"
>
</select>
</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="6">
<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>

View file

@ -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 array(
'ext' => 'civicrm',
'js' => array('ang/crmCxn.js', 'ang/crmCxn/*.js'),
'css' => array('ang/crmCxn.css'),
'partials' => array('ang/crmCxn'),
'requires' => array('crmUtil', 'ngRoute', 'ngSanitize', 'ui.utils', 'crmUi', 'dialogService', 'crmResource'),
);

View file

@ -0,0 +1,3 @@
.crmCxn-footer {
text-align: center;
}

View file

@ -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._);

View file

@ -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>

View file

@ -0,0 +1,5 @@
(function(angular, $, _) {
angular.module('crmCxn').controller('CrmCxnConfirmAboutCtrl', function($scope) {
$scope.ts = CRM.ts(null);
});
})(angular, CRM.$, CRM._);

View file

@ -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>

View file

@ -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._);

View file

@ -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._);

View file

@ -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>

View file

@ -0,0 +1,5 @@
(function(angular, $, _) {
angular.module('crmCxn').controller('CrmCxnConfirmConnectCtrl', function($scope) {
$scope.ts = CRM.ts(null);
});
})(angular, CRM.$, CRM._);

View file

@ -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>

View file

@ -0,0 +1,5 @@
(function(angular, $, _) {
angular.module('crmCxn').controller('CrmCxnConfirmReconnectCtrl', function($scope) {
$scope.ts = CRM.ts(null);
});
})(angular, CRM.$, CRM._);

View file

@ -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>

View file

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

View file

@ -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._);

View file

@ -0,0 +1,117 @@
<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>
<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 ng-show="appMetas.length === 0" class="messages status no-popup">
<i class="crm-i fa-info-circle"></i>
{{ts('No available applications')}}
</div>

View file

@ -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._);

View file

@ -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>

View file

@ -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._);

View file

@ -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 array(
'ext' => 'civicrm',
'js' => array(
'ang/crmD3.js',
'bower_components/d3/d3.min.js',
),
'requires' => array(),
);

View file

@ -0,0 +1,3 @@
(function (angular, $, _) {
angular.module('crmD3', CRM.angRequires('crmD3'));
})(angular, CRM.$, CRM._);

View file

@ -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 array(
'ext' => 'civicrm',
'js' => array('ang/crmExample.js'),
'partials' => array('ang/crmExample'),
'requires' => array('crmUtil', 'ngRoute', 'ui.utils', 'crmUi', 'crmResource'),
);

View file

@ -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._);

View file

@ -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>

View file

@ -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 array(
'ext' => 'civicrm',
'js' => array(
'ang/crmMailing.js',
'ang/crmMailing/*.js',
),
'css' => array('ang/crmMailing.css'),
'partials' => array('ang/crmMailing'),
'requires' => array('crmUtil', 'crmAttachment', 'crmAutosave', 'ngRoute', 'ui.utils', 'crmUi', 'dialogService', 'crmResource'),
);

View file

@ -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;
}

View file

@ -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._);

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,26 @@
(function(angular, $, _) {
angular.module('crmMailing').directive('crmMailingBlockReview', function (crmMailingPreviewMgr) {
return {
scope: {
crmMailing: '@',
crmMailingAttachments: '@'
},
templateUrl: '~/crmMailing/BlockReview.html',
link: function (scope, elm, attr) {
scope.$parent.$watch(attr.crmMailing, function(newValue){
scope.mailing = newValue;
});
scope.$parent.$watch(attr.crmMailingAttachments, function(newValue){
scope.attachments = newValue;
});
scope.crmMailingConst = CRM.crmMailing;
scope.ts = CRM.ts(null);
scope.previewMailing = function previewMailing(mailing, mode) {
return crmMailingPreviewMgr.preview(mailing, mode);
};
}
};
});
})(angular, CRM.$, CRM._);

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

Some files were not shown because too many files have changed in this diff Show more