*~
*.bak
+bower_components
CRM/ACL/DAO
CRM/Activity/DAO
CRM/Auction/DAO
civicrm-version.txt
civicrm.config.php
install/langs.php
+node_modules
packages/.channels
packages/.depdb
packages/.depdblock
);
});
- $res->addScriptFile('civicrm', 'packages/bower_components/angular/angular.min.js', 100, 'html-header', FALSE);
- $res->addScriptFile('civicrm', 'packages/bower_components/angular-route/angular-route.min.js', 110, 'html-header', FALSE);
+ $res->addScriptFile('civicrm', 'bower_components/angular/angular.min.js', 100, 'html-header', FALSE);
+ $res->addScriptFile('civicrm', 'bower_components/angular-route/angular-route.min.js', 110, 'html-header', FALSE);
$headOffset = 0;
foreach ($modules as $module) {
if (!empty($module['css'])) {
*/
public static function getAngularModules() {
$angularModules = array();
- $angularModules['ui.utils'] = array('ext' => 'civicrm', 'js' => array('packages/bower_components/angular-ui-utils/ui-utils.min.js'));
- $angularModules['ui.sortable'] = array('ext' => 'civicrm', 'js' => array('packages/bower_components/angular-ui-sortable/sortable.min.js'));
- $angularModules['unsavedChanges'] = array('ext' => 'civicrm', 'js' => array('packages/bower_components/angular-unsavedChanges/dist/unsavedChanges.min.js'));
+ $angularModules['ui.utils'] = array('ext' => 'civicrm', 'js' => array('bower_components/angular-ui-utils/ui-utils.min.js'));
+ $angularModules['ui.sortable'] = array('ext' => 'civicrm', 'js' => array('bower_components/angular-ui-sortable/sortable.min.js'));
+ $angularModules['unsavedChanges'] = array('ext' => 'civicrm', 'js' => array('bower_components/angular-unsavedChanges/dist/unsavedChanges.min.js'));
// https://github.com/jwstadler/angular-jquery-dialog-service
- $angularModules['angularFileUpload'] = array('ext' => 'civicrm', 'js' => array('packages/bower_components/angular-file-upload/angular-file-upload.min.js'));
- $angularModules['dialogService'] = array('ext' => 'civicrm' , 'js' => array('packages/bower_components/angular-jquery-dialog-service/dialog-service.js'));
+ $angularModules['angularFileUpload'] = array('ext' => 'civicrm', 'js' => array('bower_components/angular-file-upload/angular-file-upload.min.js'));
+ $angularModules['dialogService'] = array('ext' => 'civicrm' , 'js' => array('bower_components/angular-jquery-dialog-service/dialog-service.js'));
+ $angularModules['crmApp'] = array('ext' => 'civicrm', 'js' => array('js/angular-crmApp.js'));
$angularModules['crmAttachment'] = array('ext' => 'civicrm', 'js' => array('js/angular-crmAttachment.js'), 'css' => array('css/angular-crmAttachment.css'));
$angularModules['crmUi'] = array('ext' => 'civicrm', 'js' => array('js/angular-crm-ui.js', 'packages/ckeditor/ckeditor.js'));
$angularModules['crmUtil'] = array('ext' => 'civicrm', 'js' => array('js/angular-crm-util.js'));
$angularModules['ngSanitize'] = array('ext' => 'civicrm', 'js' => array('js/angular-sanitize.js'));
+ $angularModules['unsavedChanges'] = array('ext' => 'civicrm', 'js' => array('bower_components/angular-unsavedChanges/dist/unsavedChanges.min.js'));
+ $angularModules['crmUi'] = array('ext' => 'civicrm', 'js' => array('js/angular-crm-ui.js'));
foreach (CRM_Core_Component::getEnabledComponents() as $component) {
$angularModules = array_merge($angularModules, $component->getAngularModules());
--- /dev/null
+{
+ "name": "civicrm",
+ "description": "CiviCRM",
+ "version": "5.0.0",
+ "license": "AGPL-3.0",
+ "private": true,
+ "dependencies": {
+ "angular": "1.3.x",
+ "angular-file-upload": "~1.1.5",
+ "angular-jquery-dialog-service": "totten/angular-jquery-dialog-service#jquery-closure",
+ "angular-mocks": "1.3.x",
+ "angular-route": "1.3.x",
+ "angular-ui-sortable": "0.12.x",
+ "angular-ui-utils": "0.1.x",
+ "angular-unsavedChanges": "~0.1.1"
+ }
+}
--- /dev/null
+(function(angular, CRM) {
+ var crmApp = angular.module('crmApp', CRM.angular.modules);
+ crmApp.config(['$routeProvider',
+ function($routeProvider) {
+ $routeProvider.otherwise({
+ template: ts('Unknown path')
+ });
+ }
+ ]);
+ crmApp.factory('crmApi', function() {
+ return function(entity, action, params, message) {
+ // JSON serialization in CRM.api3 is not aware of Angular metadata like $$hash
+ if (CRM._.isObject(entity)) {
+ return CRM.api3(eval('('+angular.toJson(entity)+')'), message);
+ } else {
+ return CRM.api3(entity, action, eval('('+angular.toJson(params)+')'), message);
+ }
+ };
+ });
+ crmApp.factory('crmLegacy', function() {
+ return CRM;
+ });
+ crmApp.factory('crmNavigator', ['$window', function($window) {
+ return {
+ redirect: function(path) {
+ $window.location.href = path;
+ }
+ };
+ }]);
+})(angular, CRM);
return CRM.resourceUrls['civicrm'] + '/partials/crmCaseType/' + relPath;
};
- var crmCaseType = angular.module('crmCaseType', ['ngRoute', 'ui.utils', 'crmUi', 'unsavedChanges']);
+ var crmCaseType = angular.module('crmCaseType', ['ngRoute', 'ui.utils', 'crmUi', 'unsavedChanges', 'crmApp']);
// Note: This template will be passed to cloneDeep(), so don't put any funny stuff in here!
var newCaseTypeTemplate = {
};
angular.module('crmMailing', [
- 'crmUtil', 'crmAttachment', 'ngRoute', 'ui.utils', 'crmUi', 'dialogService'
+ 'crmUtil', 'crmAttachment', 'ngRoute', 'ui.utils', 'crmUi', 'dialogService', 'crmApp'
]); // TODO ngSanitize, unsavedChanges
// Time to wait before triggering AJAX update to recipients list
}
]);
- angular.module('crmMailing').controller('ListMailingsCtrl', function ListMailingsCtrl() {
+ 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.
- window.location = CRM.url('civicrm/mailing/browse/unscheduled', {
- reset: 1,
- scheduled: 'false'
- });
- });
+ var new_url = crmLegacy.url('civicrm/mailing/browse/unscheduled', {reset: 1, scheduled: 'false'});
+ crmNavigator.redirect(new_url);
+ }]);
angular.module('crmMailing').controller('EditMailingCtrl', function EditMailingCtrl($scope, selectedMail, $location, crmMailingMgr, crmStatus, CrmAttachments, crmMailingPreviewMgr) {
$scope.mailing = selectedMail;
--- /dev/null
+{
+ "description": "CiviCRM",
+ "main": "index.js",
+ "license": "MIT",
+ "name": "civicrm",
+ "version": "5.0.0",
+ "devDependencies": {
+ "bower": "^1.3.1",
+ "karma": "^0.12.16",
+ "karma-chrome-launcher": "^0.1.4",
+ "jasmine-core": "~2.1.2",
+ "karma-jasmine": "~0.3.2"
+ },
+ "scripts": {
+ "postinstall": "bower install",
+ "test": "node node_modules/karma/bin/karma start tests/karma.conf.js"
+ }
+}
<div ng-app="crmApp">
<div ng-view></div>
</div>
-
-<script type="text/javascript">
- (function(angular, _) {
- var crmApp = angular.module('crmApp', CRM.angular.modules);
- crmApp.config(['$routeProvider',
- function($routeProvider) {
- $routeProvider.otherwise({
- template: ts('Unknown path')
- });
- }
- ]);
- // crmApi is a function(entity,action,params,message) which is similar to CRM.api3, but
- // it follows Angular conventions (e.g. it's an injectable service which returns a $q promise)
- crmApp.factory('crmApi', function($q) {
- return function(entity, action, params, message) {
- // JSON serialization in CRM.api3 is not aware of Angular metadata like $$hash, so use angular.toJson()
- var deferred = $q.defer();
- var p;
- if (_.isObject(entity)) {
- p = CRM.api3(eval('('+angular.toJson(entity)+')'), message);
- } else {
- p = CRM.api3(entity, action, eval('('+angular.toJson(params)+')'), message);
- }
- // CRM.api3 returns a promise, but the promise doesn't really represent errors as errors, so we
- // convert them
- p.then(
- function(result) {
- if (result.is_error) {
- deferred.reject(result);
- } else {
- deferred.resolve(result);
- }
- },
- function(error) {
- deferred.reject(error);
- }
- );
- return deferred.promise;
- };
- });
- })(angular, CRM._);
-</script>
-
-{/literal}
\ No newline at end of file
+{/literal}
--- /dev/null
+module.exports = function(config) {
+ config.set({
+ autoWatch: true,
+ basePath: '..',
+ browsers: ['Chrome'],
+ exclude: [
+ ],
+ files: [
+ 'bower_components/jquery/dist/jquery.min.js',
+ 'bower_components/jquery-ui/jquery-ui.min.js',
+ 'packages/backbone/lodash.compat.min.js',
+ 'packages/jquery/plugins/select2/select2.min.js',
+ 'packages/jquery/plugins/jquery.blockUI.js',
+ 'packages/jquery/plugins/jquery.validate.js',
+ 'js/Common.js',
+ 'bower_components/angular/angular.js',
+ 'bower_components/angular-file-upload/angular-file-upload.js',
+ 'bower_components/angular-jquery-dialog-service/dialog-service.js',
+ 'bower_components/angular-route/angular-route.js',
+ 'bower_components/angular-mocks/angular-mocks.js',
+ 'bower_components/angular-ui-sortable/sortable.js',
+ 'bower_components/angular-ui-utils/ui-utils.js',
+ 'bower_components/angular-unsavedChanges/dist/unsavedChanges.js',
+ 'tests/karma/modules.js',
+ 'js/crm.ajax.js',
+ 'js/angular-*.js',
+ 'tests/karma/lib/*.js',
+ 'tests/karma/**/*.js',
+ ],
+ frameworks: ['jasmine'],
+ logLevel: config.LOG_INFO,
+ port: 9876,
+ reporters: ['progress'],
+ singleRun: false
+ });
+};
--- /dev/null
+'use strict';
+
+(function(root) {
+ var Comparitor = function() {};
+ Comparitor.prototype = {
+ compare: function(actual, expected) {
+ this.result = {
+ 'pass': true,
+ };
+ this.internal_compare('root', actual, expected);
+ return this.result;
+ },
+ internal_compare: function(context, actual, expected) {
+ if (expected instanceof Array) {
+ return this.internal_compare_array(context, actual, expected);
+ } else if (expected instanceof Object) {
+ return this.internal_compare_object(context, actual, expected);
+ } else {
+ return this.internal_compare_value(context, actual, expected);
+ }
+ return true;
+ },
+ internal_compare_array: function(context, actual, expected) {
+ if (!(actual instanceof Array)) {
+ this.result.pass = false;
+ this.result.message = "The expected data has an array at " + context + ", but the actual data has something else (" + actual + ")";
+ return false;
+ }
+ if (expected.length != actual.length) {
+ this.result.pass = false;
+ this.result.message = "The expected data has an array with " + expected.length + " items in it, but the actual data has " + actual.length + " items.";
+ return false;
+ }
+ for (var i = 0; i < expected.length; i++) {
+ var still_matches = this.internal_compare(context + "[" + i + "]", actual[i], expected[i]);
+ if (!still_matches) {
+ return false;
+ }
+ }
+ return true;
+ },
+ internal_compare_object: function(context, actual, expected) {
+ if (!(actual instanceof Object) || actual instanceof Array) {
+ this.result.pass = false;
+ this.result.message = "The expected data has an object at root, but the actual data has something else (" + actual + ")";
+ return false;
+ }
+ for (var key in expected) {
+ if (!(key in actual)) {
+ this.result.pass = false;
+ this.result.message = "Could not find key '" + key + "' in actual data at " + context + ".";
+ return false;
+ }
+ var still_matches = this.internal_compare(context + "[" + key + "]", actual[key], expected[key]);
+ if (!still_matches) {
+ return false;
+ }
+ }
+ for (var key in actual) {
+ if (!(key in expected)) {
+ this.result.pass = false;
+ this.result.message = "Did not expect key " + key + " in actual data at " + context + ".";
+ return false;
+ }
+ }
+ return true;
+ },
+ internal_compare_value: function(context, actual, expected) {
+ if (expected === actual) {
+ return true;
+ }
+ this.result.pass = false;
+ this.result.message = "Expected '" + actual + "' to be '" + expected + "' at " + context + ".";
+ return false;
+ },
+ register: function(jasmine) {
+ var comparitor = this;
+ jasmine.addMatchers({
+ toEqualData: function(expected) {
+ return {
+ compare: $.proxy(comparitor.compare, comparitor)
+ }
+ }
+ });
+ }
+ };
+ var module = angular.module('crmJsonComparitor', []);
+ module.service('crmJsonComparitor', Comparitor);
+})(angular);
--- /dev/null
+CRM.angular = {
+ modules: [
+ 'ngRoute',
+ 'ui.utils',
+ 'ui.sortable',
+ 'unsavedChanges',
+ 'angularFileUpload',
+ 'dialogService',
+ 'crmApp',
+ 'crmAttachment',
+ 'crmUi',
+ 'crmUtil',
+ 'ngSanitize',
+ ]
+};
--- /dev/null
+'use strict';
+
+describe('crmCaseType', function() {
+
+ beforeEach(function() {
+ CRM.resourceUrls = {
+ 'civicrm': ''
+ };
+ CRM.crmCaseType = {
+ 'REL_TYPE_CNAME': 'label_b_a'
+ };
+ module('crmCaseType');
+ module('crmJsonComparitor');
+ inject(function(crmJsonComparitor) {
+ crmJsonComparitor.register(jasmine);
+ });
+ });
+
+ describe('CaseTypeCtrl', function() {
+ var apiCalls;
+ var ctrl;
+ var compile;
+ var $httpBackend;
+ var scope;
+ var timeout;
+
+ beforeEach(inject(function(_$httpBackend_, $rootScope, $controller, $compile, $timeout) {
+ $httpBackend = _$httpBackend_;
+ scope = $rootScope.$new();
+ compile = $compile;
+ timeout = $timeout;
+ apiCalls = {
+ 'actStatuses': {
+ 'values': {
+ "272": {
+ "id": "272",
+ "option_group_id": "25",
+ "label": "Scheduled",
+ "value": "1",
+ "name": "Scheduled",
+ "filter": "0",
+ "is_default": "1",
+ "weight": "1",
+ "is_optgroup": "0",
+ "is_reserved": "1",
+ "is_active": "1"
+ },
+ "273": {
+ "id": "273",
+ "option_group_id": "25",
+ "label": "Completed",
+ "value": "2",
+ "name": "Completed",
+ "filter": "0",
+ "weight": "2",
+ "is_optgroup": "0",
+ "is_reserved": "1",
+ "is_active": "1"
+ }
+ }
+ },
+ 'actTypes': {
+ 'values': {
+ "784": {
+ "id": "784",
+ "option_group_id": "2",
+ "label": "ADC referral",
+ "value": "62",
+ "name": "ADC referral",
+ "filter": "0",
+ "is_default": "0",
+ "weight": "64",
+ "is_optgroup": "0",
+ "is_reserved": "0",
+ "is_active": "1",
+ "component_id": "7"
+ },
+ "32": {
+ "id": "32",
+ "option_group_id": "2",
+ "label": "Add Client To Case",
+ "value": "27",
+ "name": "Add Client To Case",
+ "filter": "0",
+ "is_default": "0",
+ "weight": "26",
+ "description": "",
+ "is_optgroup": "0",
+ "is_reserved": "1",
+ "is_active": "1",
+ "component_id": "7"
+ }
+ }
+ },
+ 'relTypes': {
+ 'values' : {
+ "14": {
+ "id": "14",
+ "name_a_b": "Benefits Specialist is",
+ "label_a_b": "Benefits Specialist is",
+ "name_b_a": "Benefits Specialist",
+ "label_b_a": "Benefits Specialist",
+ "description": "Benefits Specialist",
+ "contact_type_a": "Individual",
+ "contact_type_b": "Individual",
+ "is_reserved": "0",
+ "is_active": "1"
+ },
+ "9": {
+ "id": "9",
+ "name_a_b": "Case Coordinator is",
+ "label_a_b": "Case Coordinator is",
+ "name_b_a": "Case Coordinator",
+ "label_b_a": "Case Coordinator",
+ "description": "Case Coordinator",
+ "contact_type_a": "Individual",
+ "contact_type_b": "Individual",
+ "is_reserved": "0",
+ "is_active": "1"
+ }
+ }
+ },
+ "caseType": {
+ "id": "1",
+ "name": "housing_support",
+ "title": "Housing Support",
+ "description": "Help homeless individuals obtain temporary and long-term housing",
+ "is_active": "1",
+ "is_reserved": "0",
+ "weight": "1",
+ "is_forkable": "1",
+ "is_forked": "",
+ "definition": {
+ "activityTypes": [
+ {"name": "Open Case", "max_instances": "1"}
+ ],
+ "activitySets": [
+ {
+ "name": "standard_timeline",
+ "label": "Standard Timeline",
+ "timeline": "1",
+ "activityTypes": [
+ {
+ "name": "Open Case",
+ "status": "Completed"
+ },
+ {
+ "name": "Medical evaluation",
+ "reference_activity": "Open Case",
+ "reference_offset": "1",
+ "reference_select": "newest"
+ }
+ ]
+ }
+ ],
+ "caseRoles": [
+ {
+ "name": "Homeless Services Coordinator",
+ "creator": "1",
+ "manager": "1"
+ }
+ ]
+ }
+ }
+ };
+ ctrl = $controller('CaseTypeCtrl', {$scope: scope, apiCalls: apiCalls});
+ }));
+
+ it('should load activity statuses', function() {
+ expect(scope.activityStatuses).toEqualData([apiCalls['actStatuses']['values']['272'], apiCalls['actStatuses']['values']['273']]);
+ });
+
+ it('should load activity types', function() {
+ expect(scope.activityTypes).toEqualData(apiCalls['actTypes']['values']);
+ });
+
+ it('addActivitySet should add an activitySet to the case type', function() {
+ scope.addActivitySet('timeline');
+ var activitySets = scope.caseType.definition.activitySets;
+ var newSet = activitySets[activitySets.length - 1];
+ expect(newSet.name).toBe('timeline_1');
+ expect(newSet.timeline).toBe('1');
+ expect(newSet.label).toBe('Timeline');
+ });
+
+ it('addActivitySet handles second timeline correctly', function() {
+ scope.addActivitySet('timeline');
+ scope.addActivitySet('timeline');
+ var activitySets = scope.caseType.definition.activitySets;
+ var newSet = activitySets[activitySets.length - 1];
+ expect(newSet.name).toBe('timeline_2');
+ expect(newSet.timeline).toBe('1');
+ expect(newSet.label).toBe('Timeline #2');
+ });
+
+ });
+});
--- /dev/null
+'use strict';
+
+describe('crmJsonComparitor', function() {
+ var comparitor;
+
+ beforeEach(function() {
+ module('crmJsonComparitor');
+ });
+
+ beforeEach(function() {
+ inject(function(crmJsonComparitor) {
+ comparitor = crmJsonComparitor;
+ });
+ });
+
+ it('should return false when comparing different objects', function() {
+ var result = comparitor.compare({'foo': 'bar'}, {'bar': 'foo'});
+ expect(result.pass).toBe(false);
+ });
+
+ it('should return true when comparing equal objects', function() {
+ var result = comparitor.compare({'bar': 'foo'}, {'bar': 'foo'});
+ expect(result.pass).toBe(true);
+ });
+
+ it('should explain what part of the comparison failed when comparing objects', function() {
+ var result = comparitor.compare({'foo': 'bar'}, {'bar': 'foo'});
+ expect(result.message).toBe('Could not find key \'bar\' in actual data at root.');
+ });
+
+ it('should handle nested objects', function() {
+ var result = comparitor.compare({'foo': {'bif': 'bam'}}, {'foo': {'bif': 'bam'}});
+ expect(result.pass).toBe(true);
+ });
+
+ it('should handle differences in nested objects', function() {
+ var result = comparitor.compare({'foo': {'bif': 'bam'}}, {'foo': {'bif': 'bop'}});
+ expect(result.pass).toBe(false);
+ expect(result.message).toBe("Expected 'bam' to be 'bop' at root[foo][bif].");
+ });
+
+ it('should handle arrays', function() {
+ var result = comparitor.compare([1, 2, 3, 4], [1, 2, 3, 4]);
+ expect(result.pass).toBe(true);
+ });
+
+ it('should handle arrays with differences', function() {
+ var result = comparitor.compare([1, 2, 2, 4], [1, 2, 3, 4]);
+ expect(result.pass).toBe(false);
+ expect(result.message).toBe("Expected '2' to be '3' at root[2].");
+ });
+
+ it('should handle nested arrays and objects', function() {
+ var result = comparitor.compare([1, 2, {'foo': 'bar'}, 4], [1, 2, {'foo': 'bar'}, 4]);
+ expect(result.pass).toBe(true);
+ });
+
+ it('should handle nested arrays and objects with differences', function() {
+ var result = comparitor.compare([1, 2, {'foo': 'bar'}, 4], [1, 2, {'foo': 'bif'}, 4]);
+ expect(result.pass).toBe(false);
+ expect(result.message).toBe("Expected 'bar' to be 'bif' at root[2][foo].");
+ });
+
+ it('should complain when comparing an object to an array', function() {
+ var result = comparitor.compare({'foo': 'bar'}, [1, 2, 3]);
+ expect(result.pass).toBe(false);
+ expect(result.message).toBe("The expected data has an array at root, but the actual data has something else ([object Object])");
+ });
+
+ it('should complain when comparing an array to an object', function() {
+ var result = comparitor.compare([1, 2, 3], {'foo': 'bar'});
+ expect(result.pass).toBe(false);
+ expect(result.message).toBe("The expected data has an object at root, but the actual data has something else (1,2,3)");
+ });
+
+});
--- /dev/null
+'use strict';
+
+describe('crmMailing', function() {
+
+ beforeEach(function() {
+ module('crmApp');
+ module('crmMailing');
+ });
+
+ describe('ListMailingsCtrl', function() {
+ var ctrl;
+ var navigator;
+
+ beforeEach(function() {
+ navigator = jasmine.createSpyObj('crmNavigator', ['redirect']);
+ module(function ($provide) {
+ $provide.value('crmNavigator', navigator)
+ });
+ inject(['crmLegacy', function(crmLegacy) {
+ crmLegacy.url({back: '/*path*?*query*', front: '/*path*?*query*'});
+ }]);
+ inject(['$controller', function($controller) {
+ ctrl = $controller('ListMailingsCtrl', {});
+ }]);
+ });
+
+ it('should redirect to unscheduled', function() {
+ expect(navigator.redirect).toHaveBeenCalled();
+ });
+ });
+});