350 lines
11 KiB
JavaScript
350 lines
11 KiB
JavaScript
|
/// crmUi: Sundry UI helpers
|
||
|
(function (angular, $, _) {
|
||
|
angular.module('crmUtil', CRM.angRequires('crmUtil'));
|
||
|
|
||
|
// Angular implementation of CRM.api3
|
||
|
// @link http://wiki.civicrm.org/confluence/display/CRMDOC/AJAX+Interface#AJAXInterface-CRM.api3
|
||
|
//
|
||
|
// Note: To mock API results in unit-tests, override crmApi.backend, e.g.
|
||
|
// var apiSpy = jasmine.createSpy('crmApi');
|
||
|
// crmApi.backend = apiSpy.and.returnValue(crmApi.val({
|
||
|
// is_error: 1
|
||
|
// }));
|
||
|
angular.module('crmUtil').factory('crmApi', function($q) {
|
||
|
var crmApi = function(entity, action, params, message) {
|
||
|
// JSON serialization in CRM.api3 is not aware of Angular metadata like $$hash, so use angular.toJson()
|
||
|
var deferred = $q.defer();
|
||
|
var p;
|
||
|
var backend = crmApi.backend || CRM.api3;
|
||
|
if (params && params.body_html) {
|
||
|
// CRM-18474 - remove Unicode Character 'LINE SEPARATOR' (U+2028)
|
||
|
// and 'PARAGRAPH SEPARATOR' (U+2029) from the html if present.
|
||
|
params.body_html = params.body_html.replace(/([\u2028]|[\u2029])/g, '\n');
|
||
|
}
|
||
|
if (_.isObject(entity)) {
|
||
|
// eval content is locally generated.
|
||
|
/*jshint -W061 */
|
||
|
p = backend(eval('('+angular.toJson(entity)+')'), action);
|
||
|
} else {
|
||
|
// eval content is locally generated.
|
||
|
/*jshint -W061 */
|
||
|
p = backend(entity, action, eval('('+angular.toJson(params)+')'), message);
|
||
|
}
|
||
|
// CRM.api3 returns a promise, but the promise doesn't really represent errors as errors, so we
|
||
|
// convert them
|
||
|
p.then(
|
||
|
function(result) {
|
||
|
if (result.is_error) {
|
||
|
deferred.reject(result);
|
||
|
} else {
|
||
|
deferred.resolve(result);
|
||
|
}
|
||
|
},
|
||
|
function(error) {
|
||
|
deferred.reject(error);
|
||
|
}
|
||
|
);
|
||
|
return deferred.promise;
|
||
|
};
|
||
|
crmApi.backend = null;
|
||
|
crmApi.val = function(value) {
|
||
|
var d = $.Deferred();
|
||
|
d.resolve(value);
|
||
|
return d.promise();
|
||
|
};
|
||
|
return crmApi;
|
||
|
});
|
||
|
|
||
|
// Get and cache the metadata for an API entity.
|
||
|
// usage:
|
||
|
// $q.when(crmMetadata.getFields('MyEntity'), function(fields){
|
||
|
// console.log('The fields are:', options);
|
||
|
// });
|
||
|
angular.module('crmUtil').factory('crmMetadata', function($q, crmApi) {
|
||
|
|
||
|
// Convert {key:$,value:$} sequence to unordered {$key: $value} map.
|
||
|
function convertOptionsToMap(options) {
|
||
|
var result = {};
|
||
|
angular.forEach(options, function(o) {
|
||
|
result[o.key] = o.value;
|
||
|
});
|
||
|
return result;
|
||
|
}
|
||
|
|
||
|
var cache = {}; // cache[entityName+'::'+action][fieldName].title
|
||
|
var deferreds = {}; // deferreds[cacheKey].push($q.defer())
|
||
|
var crmMetadata = {
|
||
|
// usage: $q.when(crmMetadata.getField('MyEntity', 'my_field')).then(...);
|
||
|
getField: function getField(entity, field) {
|
||
|
return $q.when(crmMetadata.getFields(entity)).then(function(fields){
|
||
|
return fields[field];
|
||
|
});
|
||
|
},
|
||
|
// usage: $q.when(crmMetadata.getFields('MyEntity')).then(...);
|
||
|
// usage: $q.when(crmMetadata.getFields(['MyEntity', 'myaction'])).then(...);
|
||
|
getFields: function getFields(entity) {
|
||
|
var action = '', cacheKey;
|
||
|
if (_.isArray(entity)) {
|
||
|
action = entity[1];
|
||
|
entity = entity[0];
|
||
|
cacheKey = entity + '::' + action;
|
||
|
} else {
|
||
|
cacheKey = entity;
|
||
|
}
|
||
|
|
||
|
if (_.isObject(cache[cacheKey])) {
|
||
|
return cache[cacheKey];
|
||
|
}
|
||
|
|
||
|
var needFetch = _.isEmpty(deferreds[cacheKey]);
|
||
|
deferreds[cacheKey] = deferreds[cacheKey] || [];
|
||
|
var deferred = $q.defer();
|
||
|
deferreds[cacheKey].push(deferred);
|
||
|
|
||
|
if (needFetch) {
|
||
|
crmApi(entity, 'getfields', {action: action, sequential: 1, options: {get_options: 'all'}})
|
||
|
.then(
|
||
|
// on success:
|
||
|
function(fields) {
|
||
|
cache[cacheKey] = _.indexBy(fields.values, 'name');
|
||
|
angular.forEach(cache[cacheKey],function (field){
|
||
|
if (field.options) {
|
||
|
field.optionsMap = convertOptionsToMap(field.options);
|
||
|
}
|
||
|
});
|
||
|
angular.forEach(deferreds[cacheKey], function(dfr) {
|
||
|
dfr.resolve(cache[cacheKey]);
|
||
|
});
|
||
|
delete deferreds[cacheKey];
|
||
|
},
|
||
|
// on error:
|
||
|
function() {
|
||
|
cache[cacheKey] = {}; // cache nack
|
||
|
angular.forEach(deferreds[cacheKey], function(dfr) {
|
||
|
dfr.reject();
|
||
|
});
|
||
|
delete deferreds[cacheKey];
|
||
|
}
|
||
|
);
|
||
|
}
|
||
|
|
||
|
return deferred.promise;
|
||
|
}
|
||
|
};
|
||
|
|
||
|
return crmMetadata;
|
||
|
});
|
||
|
|
||
|
// usage:
|
||
|
// var block = $scope.block = crmBlocker();
|
||
|
// $scope.save = function() { return block(crmApi('MyEntity','create',...)); };
|
||
|
// <button ng-click="save()" ng-disabled="block.check()">Do something</button>
|
||
|
angular.module('crmUtil').factory('crmBlocker', function() {
|
||
|
return function() {
|
||
|
var blocks = 0;
|
||
|
var result = function(promise) {
|
||
|
blocks++;
|
||
|
return promise.finally(function() {
|
||
|
blocks--;
|
||
|
});
|
||
|
};
|
||
|
result.check = function() {
|
||
|
return blocks > 0;
|
||
|
};
|
||
|
return result;
|
||
|
};
|
||
|
});
|
||
|
|
||
|
angular.module('crmUtil').factory('crmLegacy', function() {
|
||
|
return CRM;
|
||
|
});
|
||
|
|
||
|
// example: scope.$watch('foo', crmLog.wrap(function(newValue, oldValue){ ... }));
|
||
|
angular.module('crmUtil').factory('crmLog', function(){
|
||
|
var level = 0;
|
||
|
var write = console.log;
|
||
|
function indent() {
|
||
|
var s = '>';
|
||
|
for (var i = 0; i < level; i++) s = s + ' ';
|
||
|
return s;
|
||
|
}
|
||
|
var crmLog = {
|
||
|
log: function(msg, vars) {
|
||
|
write(indent() + msg, vars);
|
||
|
},
|
||
|
wrap: function(label, f) {
|
||
|
return function(){
|
||
|
level++;
|
||
|
crmLog.log(label + ": start", arguments);
|
||
|
var r;
|
||
|
try {
|
||
|
r = f.apply(this, arguments);
|
||
|
} finally {
|
||
|
crmLog.log(label + ": end");
|
||
|
level--;
|
||
|
}
|
||
|
return r;
|
||
|
};
|
||
|
}
|
||
|
};
|
||
|
return crmLog;
|
||
|
});
|
||
|
|
||
|
angular.module('crmUtil').factory('crmNavigator', ['$window', function($window) {
|
||
|
return {
|
||
|
redirect: function(path) {
|
||
|
$window.location.href = path;
|
||
|
}
|
||
|
};
|
||
|
}]);
|
||
|
|
||
|
// Wrap an async function in a queue, ensuring that independent async calls are issued in strict sequence.
|
||
|
// usage: qApi = crmQueue(crmApi); qApi(entity,action,...).then(...); qApi(entity2,action2,...).then(...);
|
||
|
// This is similar to promise-chaining, but allows chaining independent procs (without explicitly sharing promises).
|
||
|
angular.module('crmUtil').factory('crmQueue', function($q) {
|
||
|
// @param worker A function which generates promises
|
||
|
return function crmQueue(worker) {
|
||
|
var queue = [];
|
||
|
function next() {
|
||
|
var task = queue[0];
|
||
|
worker.apply(null, task.a).then(
|
||
|
function onOk(data) {
|
||
|
queue.shift();
|
||
|
task.dfr.resolve(data);
|
||
|
if (queue.length > 0) next();
|
||
|
},
|
||
|
function onErr(err) {
|
||
|
queue.shift();
|
||
|
task.dfr.reject(err);
|
||
|
if (queue.length > 0) next();
|
||
|
}
|
||
|
);
|
||
|
}
|
||
|
function enqueue() {
|
||
|
var dfr = $q.defer();
|
||
|
queue.push({a: arguments, dfr: dfr});
|
||
|
if (queue.length === 1) {
|
||
|
next();
|
||
|
}
|
||
|
return dfr.promise;
|
||
|
}
|
||
|
return enqueue;
|
||
|
};
|
||
|
});
|
||
|
|
||
|
// Adapter for CRM.status which supports Angular promises (instead of jQuery promises)
|
||
|
// example: crmStatus('Saving', crmApi(...)).then(function(result){...})
|
||
|
angular.module('crmUtil').factory('crmStatus', function($q){
|
||
|
return function(options, aPromise){
|
||
|
if (aPromise) {
|
||
|
return CRM.toAPromise($q, CRM.status(options, CRM.toJqPromise(aPromise)));
|
||
|
} else {
|
||
|
return CRM.toAPromise($q, CRM.status(options));
|
||
|
}
|
||
|
};
|
||
|
});
|
||
|
|
||
|
// crmWatcher allows one to setup event listeners and temporarily suspend
|
||
|
// them en masse.
|
||
|
//
|
||
|
// example:
|
||
|
// angular.controller(... function($scope, crmWatcher){
|
||
|
// var watcher = crmWatcher();
|
||
|
// function myfunc() {
|
||
|
// watcher.suspend('foo', function(){
|
||
|
// ...do stuff...
|
||
|
// });
|
||
|
// }
|
||
|
// watcher.setup('foo', function(){
|
||
|
// return [
|
||
|
// $scope.$watch('foo', myfunc),
|
||
|
// $scope.$watch('bar', myfunc),
|
||
|
// $scope.$watch('whiz', otherfunc)
|
||
|
// ];
|
||
|
// });
|
||
|
// });
|
||
|
angular.module('crmUtil').factory('crmWatcher', function(){
|
||
|
return function() {
|
||
|
var unwatches = {}, watchFactories = {}, suspends = {};
|
||
|
|
||
|
// Specify the list of watches
|
||
|
this.setup = function(name, newWatchFactory) {
|
||
|
watchFactories[name] = newWatchFactory;
|
||
|
unwatches[name] = watchFactories[name]();
|
||
|
suspends[name] = 0;
|
||
|
return this;
|
||
|
};
|
||
|
|
||
|
// Temporarily disable watches and run some logic
|
||
|
this.suspend = function(name, f) {
|
||
|
suspends[name]++;
|
||
|
this.teardown(name);
|
||
|
var r;
|
||
|
try {
|
||
|
r = f.apply(this, []);
|
||
|
} finally {
|
||
|
if (suspends[name] === 1) {
|
||
|
unwatches[name] = watchFactories[name]();
|
||
|
if (!angular.isArray(unwatches[name])) {
|
||
|
unwatches[name] = [unwatches[name]];
|
||
|
}
|
||
|
}
|
||
|
suspends[name]--;
|
||
|
}
|
||
|
return r;
|
||
|
};
|
||
|
|
||
|
this.teardown = function(name) {
|
||
|
if (!unwatches[name]) return;
|
||
|
_.each(unwatches[name], function(unwatch){
|
||
|
unwatch();
|
||
|
});
|
||
|
delete unwatches[name];
|
||
|
};
|
||
|
|
||
|
return this;
|
||
|
};
|
||
|
});
|
||
|
|
||
|
// Run a given function. If it is already running, wait for it to finish before running again.
|
||
|
// If multiple requests are made before the first request finishes, all but the last will be ignored.
|
||
|
// This prevents overwhelming the server with redundant queries during e.g. an autocomplete search while the user types.
|
||
|
// Given function should return an angular promise. crmThrottle will deliver the contents when resolved.
|
||
|
angular.module('crmUtil').factory('crmThrottle', function($q) {
|
||
|
var pending = [],
|
||
|
executing = [];
|
||
|
return function(func) {
|
||
|
var deferred = $q.defer();
|
||
|
|
||
|
function checkResult(result, success) {
|
||
|
_.pull(executing, func);
|
||
|
if (_.includes(pending, func)) {
|
||
|
runNext();
|
||
|
} else if (success) {
|
||
|
deferred.resolve(result);
|
||
|
} else {
|
||
|
deferred.reject(result);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function runNext() {
|
||
|
executing.push(func);
|
||
|
_.pull(pending, func);
|
||
|
func().then(function(result) {
|
||
|
checkResult(result, true);
|
||
|
}, function(result) {
|
||
|
checkResult(result, false);
|
||
|
});
|
||
|
}
|
||
|
|
||
|
if (!_.includes(executing, func)) {
|
||
|
runNext();
|
||
|
} else if (!_.includes(pending, func)) {
|
||
|
pending.push(func);
|
||
|
}
|
||
|
return deferred.promise;
|
||
|
};
|
||
|
});
|
||
|
|
||
|
})(angular, CRM.$, CRM._);
|