dev/core#651 Fix group by on export soft credits (possible recent regression, clearly...
[civicrm-core.git] / ang / crmUtil.js
1 /// crmUi: Sundry UI helpers
2 (function (angular, $, _) {
3 angular.module('crmUtil', CRM.angRequires('crmUtil'));
4
5 // Angular implementation of CRM.api3
6 // @link http://wiki.civicrm.org/confluence/display/CRMDOC/AJAX+Interface#AJAXInterface-CRM.api3
7 //
8 // Note: To mock API results in unit-tests, override crmApi.backend, e.g.
9 // var apiSpy = jasmine.createSpy('crmApi');
10 // crmApi.backend = apiSpy.and.returnValue(crmApi.val({
11 // is_error: 1
12 // }));
13 angular.module('crmUtil').factory('crmApi', function($q) {
14 var crmApi = function(entity, action, params, message) {
15 // JSON serialization in CRM.api3 is not aware of Angular metadata like $$hash, so use angular.toJson()
16 var deferred = $q.defer();
17 var p;
18 var backend = crmApi.backend || CRM.api3;
19 if (params && params.body_html) {
20 // CRM-18474 - remove Unicode Character 'LINE SEPARATOR' (U+2028)
21 // and 'PARAGRAPH SEPARATOR' (U+2029) from the html if present.
22 params.body_html = params.body_html.replace(/([\u2028]|[\u2029])/g, '\n');
23 }
24 if (_.isObject(entity)) {
25 // eval content is locally generated.
26 /*jshint -W061 */
27 p = backend(eval('('+angular.toJson(entity)+')'), action);
28 } else {
29 // eval content is locally generated.
30 /*jshint -W061 */
31 p = backend(entity, action, eval('('+angular.toJson(params)+')'), message);
32 }
33 // CRM.api3 returns a promise, but the promise doesn't really represent errors as errors, so we
34 // convert them
35 p.then(
36 function(result) {
37 if (result.is_error) {
38 deferred.reject(result);
39 } else {
40 deferred.resolve(result);
41 }
42 },
43 function(error) {
44 deferred.reject(error);
45 }
46 );
47 return deferred.promise;
48 };
49 crmApi.backend = null;
50 crmApi.val = function(value) {
51 var d = $.Deferred();
52 d.resolve(value);
53 return d.promise();
54 };
55 return crmApi;
56 });
57
58 // Get and cache the metadata for an API entity.
59 // usage:
60 // $q.when(crmMetadata.getFields('MyEntity'), function(fields){
61 // console.log('The fields are:', options);
62 // });
63 angular.module('crmUtil').factory('crmMetadata', function($q, crmApi) {
64
65 // Convert {key:$,value:$} sequence to unordered {$key: $value} map.
66 function convertOptionsToMap(options) {
67 var result = {};
68 angular.forEach(options, function(o) {
69 result[o.key] = o.value;
70 });
71 return result;
72 }
73
74 var cache = {}; // cache[entityName+'::'+action][fieldName].title
75 var deferreds = {}; // deferreds[cacheKey].push($q.defer())
76 var crmMetadata = {
77 // usage: $q.when(crmMetadata.getField('MyEntity', 'my_field')).then(...);
78 getField: function getField(entity, field) {
79 return $q.when(crmMetadata.getFields(entity)).then(function(fields){
80 return fields[field];
81 });
82 },
83 // usage: $q.when(crmMetadata.getFields('MyEntity')).then(...);
84 // usage: $q.when(crmMetadata.getFields(['MyEntity', 'myaction'])).then(...);
85 getFields: function getFields(entity) {
86 var action = '', cacheKey;
87 if (_.isArray(entity)) {
88 action = entity[1];
89 entity = entity[0];
90 cacheKey = entity + '::' + action;
91 } else {
92 cacheKey = entity;
93 }
94
95 if (_.isObject(cache[cacheKey])) {
96 return cache[cacheKey];
97 }
98
99 var needFetch = _.isEmpty(deferreds[cacheKey]);
100 deferreds[cacheKey] = deferreds[cacheKey] || [];
101 var deferred = $q.defer();
102 deferreds[cacheKey].push(deferred);
103
104 if (needFetch) {
105 crmApi(entity, 'getfields', {action: action, sequential: 1, options: {get_options: 'all'}})
106 .then(
107 // on success:
108 function(fields) {
109 cache[cacheKey] = _.indexBy(fields.values, 'name');
110 angular.forEach(cache[cacheKey],function (field){
111 if (field.options) {
112 field.optionsMap = convertOptionsToMap(field.options);
113 }
114 });
115 angular.forEach(deferreds[cacheKey], function(dfr) {
116 dfr.resolve(cache[cacheKey]);
117 });
118 delete deferreds[cacheKey];
119 },
120 // on error:
121 function() {
122 cache[cacheKey] = {}; // cache nack
123 angular.forEach(deferreds[cacheKey], function(dfr) {
124 dfr.reject();
125 });
126 delete deferreds[cacheKey];
127 }
128 );
129 }
130
131 return deferred.promise;
132 }
133 };
134
135 return crmMetadata;
136 });
137
138 // usage:
139 // var block = $scope.block = crmBlocker();
140 // $scope.save = function() { return block(crmApi('MyEntity','create',...)); };
141 // <button ng-click="save()" ng-disabled="block.check()">Do something</button>
142 angular.module('crmUtil').factory('crmBlocker', function() {
143 return function() {
144 var blocks = 0;
145 var result = function(promise) {
146 blocks++;
147 return promise.finally(function() {
148 blocks--;
149 });
150 };
151 result.check = function() {
152 return blocks > 0;
153 };
154 return result;
155 };
156 });
157
158 angular.module('crmUtil').factory('crmLegacy', function() {
159 return CRM;
160 });
161
162 // example: scope.$watch('foo', crmLog.wrap(function(newValue, oldValue){ ... }));
163 angular.module('crmUtil').factory('crmLog', function(){
164 var level = 0;
165 var write = console.log;
166 function indent() {
167 var s = '>';
168 for (var i = 0; i < level; i++) s = s + ' ';
169 return s;
170 }
171 var crmLog = {
172 log: function(msg, vars) {
173 write(indent() + msg, vars);
174 },
175 wrap: function(label, f) {
176 return function(){
177 level++;
178 crmLog.log(label + ": start", arguments);
179 var r;
180 try {
181 r = f.apply(this, arguments);
182 } finally {
183 crmLog.log(label + ": end");
184 level--;
185 }
186 return r;
187 };
188 }
189 };
190 return crmLog;
191 });
192
193 angular.module('crmUtil').factory('crmNavigator', ['$window', function($window) {
194 return {
195 redirect: function(path) {
196 $window.location.href = path;
197 }
198 };
199 }]);
200
201 // Wrap an async function in a queue, ensuring that independent async calls are issued in strict sequence.
202 // usage: qApi = crmQueue(crmApi); qApi(entity,action,...).then(...); qApi(entity2,action2,...).then(...);
203 // This is similar to promise-chaining, but allows chaining independent procs (without explicitly sharing promises).
204 angular.module('crmUtil').factory('crmQueue', function($q) {
205 // @param worker A function which generates promises
206 return function crmQueue(worker) {
207 var queue = [];
208 function next() {
209 var task = queue[0];
210 worker.apply(null, task.a).then(
211 function onOk(data) {
212 queue.shift();
213 task.dfr.resolve(data);
214 if (queue.length > 0) next();
215 },
216 function onErr(err) {
217 queue.shift();
218 task.dfr.reject(err);
219 if (queue.length > 0) next();
220 }
221 );
222 }
223 function enqueue() {
224 var dfr = $q.defer();
225 queue.push({a: arguments, dfr: dfr});
226 if (queue.length === 1) {
227 next();
228 }
229 return dfr.promise;
230 }
231 return enqueue;
232 };
233 });
234
235 // Adapter for CRM.status which supports Angular promises (instead of jQuery promises)
236 // example: crmStatus('Saving', crmApi(...)).then(function(result){...})
237 angular.module('crmUtil').factory('crmStatus', function($q){
238 return function(options, aPromise){
239 if (aPromise) {
240 return CRM.toAPromise($q, CRM.status(options, CRM.toJqPromise(aPromise)));
241 } else {
242 return CRM.toAPromise($q, CRM.status(options));
243 }
244 };
245 });
246
247 // crmWatcher allows one to setup event listeners and temporarily suspend
248 // them en masse.
249 //
250 // example:
251 // angular.controller(... function($scope, crmWatcher){
252 // var watcher = crmWatcher();
253 // function myfunc() {
254 // watcher.suspend('foo', function(){
255 // ...do stuff...
256 // });
257 // }
258 // watcher.setup('foo', function(){
259 // return [
260 // $scope.$watch('foo', myfunc),
261 // $scope.$watch('bar', myfunc),
262 // $scope.$watch('whiz', otherfunc)
263 // ];
264 // });
265 // });
266 angular.module('crmUtil').factory('crmWatcher', function(){
267 return function() {
268 var unwatches = {}, watchFactories = {}, suspends = {};
269
270 // Specify the list of watches
271 this.setup = function(name, newWatchFactory) {
272 watchFactories[name] = newWatchFactory;
273 unwatches[name] = watchFactories[name]();
274 suspends[name] = 0;
275 return this;
276 };
277
278 // Temporarily disable watches and run some logic
279 this.suspend = function(name, f) {
280 suspends[name]++;
281 this.teardown(name);
282 var r;
283 try {
284 r = f.apply(this, []);
285 } finally {
286 if (suspends[name] === 1) {
287 unwatches[name] = watchFactories[name]();
288 if (!angular.isArray(unwatches[name])) {
289 unwatches[name] = [unwatches[name]];
290 }
291 }
292 suspends[name]--;
293 }
294 return r;
295 };
296
297 this.teardown = function(name) {
298 if (!unwatches[name]) return;
299 _.each(unwatches[name], function(unwatch){
300 unwatch();
301 });
302 delete unwatches[name];
303 };
304
305 return this;
306 };
307 });
308
309 // Run a given function. If it is already running, wait for it to finish before running again.
310 // If multiple requests are made before the first request finishes, all but the last will be ignored.
311 // This prevents overwhelming the server with redundant queries during e.g. an autocomplete search while the user types.
312 // Given function should return an angular promise. crmThrottle will deliver the contents when resolved.
313 angular.module('crmUtil').factory('crmThrottle', function($q) {
314 var pending = [],
315 executing = [];
316 return function(func) {
317 var deferred = $q.defer();
318
319 function checkResult(result, success) {
320 _.pull(executing, func);
321 if (_.includes(pending, func)) {
322 runNext();
323 } else if (success) {
324 deferred.resolve(result);
325 } else {
326 deferred.reject(result);
327 }
328 }
329
330 function runNext() {
331 executing.push(func);
332 _.pull(pending, func);
333 func().then(function(result) {
334 checkResult(result, true);
335 }, function(result) {
336 checkResult(result, false);
337 });
338 }
339
340 if (!_.includes(executing, func)) {
341 runNext();
342 } else if (!_.includes(pending, func)) {
343 pending.push(func);
344 }
345 return deferred.promise;
346 };
347 });
348
349 })(angular, CRM.$, CRM._);