Merge pull request #6482 from sudhabisht/CRM-17005
[civicrm-core.git] / ang / crmUtil.js
CommitLineData
60ebf0a5
TO
1/// crmUi: Sundry UI helpers
2(function (angular, $, _) {
3 angular.module('crmUtil', []);
4
81eab931
TO
5 // usage:
6 // crmApi('Entity', 'action', {...}).then(function(apiResult){...})
3cf58cc3 7 //
81eab931 8 // Note: To mock API results in unit-tests, override crmApi.backend, e.g.
3cf58cc3 9 // var apiSpy = jasmine.createSpy('crmApi');
81eab931
TO
10 // crmApi.backend = apiSpy.and.returnValue(crmApi.val({
11 // is_error: 1
12 // }));
a8e65974 13 angular.module('crmUtil').factory('crmApi', function($q) {
81eab931 14 var crmApi = function(entity, action, params, message) {
a8e65974
TO
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;
81eab931 18 var backend = crmApi.backend || CRM.api3;
a8e65974 19 if (_.isObject(entity)) {
f91e4af5
TO
20 // eval content is locally generated.
21 /*jshint -W061 */
81eab931 22 p = backend(eval('('+angular.toJson(entity)+')'), message);
a8e65974 23 } else {
f91e4af5
TO
24 // eval content is locally generated.
25 /*jshint -W061 */
81eab931 26 p = backend(entity, action, eval('('+angular.toJson(params)+')'), message);
a8e65974
TO
27 }
28 // CRM.api3 returns a promise, but the promise doesn't really represent errors as errors, so we
29 // convert them
30 p.then(
31 function(result) {
32 if (result.is_error) {
33 deferred.reject(result);
34 } else {
35 deferred.resolve(result);
36 }
37 },
38 function(error) {
39 deferred.reject(error);
40 }
41 );
42 return deferred.promise;
43 };
81eab931
TO
44 crmApi.backend = null;
45 crmApi.val = function(value) {
46 var d = $.Deferred();
47 d.resolve(value);
48 return d.promise();
49 };
50 return crmApi;
51 });
52
53 // Get and cache the metadata for an API entity.
54 // usage:
55 // $q.when(crmMetadata.getFields('MyEntity'), function(fields){
56 // console.log('The fields are:', options);
57 // });
58 angular.module('crmUtil').factory('crmMetadata', function($q, crmApi) {
8a941d14
TO
59
60 // Convert {key:$,value:$} sequence to unordered {$key: $value} map.
61 function convertOptionsToMap(options) {
62 var result = {};
63 angular.forEach(options, function(o) {
64 result[o.key] = o.value;
65 });
66 return result;
67 }
68
81eab931
TO
69 var cache = {}; // cache[entityName+'::'+action][fieldName].title
70 var deferreds = {}; // deferreds[cacheKey].push($q.defer())
71 var crmMetadata = {
72 // usage: $q.when(crmMetadata.getField('MyEntity', 'my_field')).then(...);
73 getField: function getField(entity, field) {
74 return $q.when(crmMetadata.getFields(entity)).then(function(fields){
75 return fields[field];
76 });
77 },
78 // usage: $q.when(crmMetadata.getFields('MyEntity')).then(...);
79 // usage: $q.when(crmMetadata.getFields(['MyEntity', 'myaction'])).then(...);
80 getFields: function getFields(entity) {
81 var action = '', cacheKey;
82 if (_.isArray(entity)) {
83 action = entity[1];
84 entity = entity[0];
85 cacheKey = entity + '::' + action;
86 } else {
87 cacheKey = entity;
88 }
89
90 if (_.isObject(cache[cacheKey])) {
91 return cache[cacheKey];
92 }
93
94 var needFetch = _.isEmpty(deferreds[cacheKey]);
95 deferreds[cacheKey] = deferreds[cacheKey] || [];
96 var deferred = $q.defer();
97 deferreds[cacheKey].push(deferred);
98
99 if (needFetch) {
a3b34c78 100 crmApi(entity, 'getfields', {action: action, sequential: 1, options: {get_options: 'all'}})
81eab931
TO
101 .then(
102 // on success:
103 function(fields) {
a3b34c78 104 cache[cacheKey] = _.indexBy(fields.values, 'name');
8a941d14
TO
105 angular.forEach(cache[cacheKey],function (field){
106 if (field.options) {
107 field.optionsMap = convertOptionsToMap(field.options);
108 }
109 });
81eab931 110 angular.forEach(deferreds[cacheKey], function(dfr) {
a3b34c78 111 dfr.resolve(cache[cacheKey]);
81eab931
TO
112 });
113 delete deferreds[cacheKey];
114 },
115 // on error:
116 function() {
117 cache[cacheKey] = {}; // cache nack
118 angular.forEach(deferreds[cacheKey], function(dfr) {
119 dfr.reject();
120 });
121 delete deferreds[cacheKey];
122 }
123 );
124 }
125
126 return deferred.promise;
127 }
128 };
129
130 return crmMetadata;
a8e65974
TO
131 });
132
bdd3f781
TO
133 // usage:
134 // var block = $scope.block = crmBlocker();
135 // $scope.save = function() { return block(crmApi('MyEntity','create',...)); };
136 // <button ng-click="save()" ng-disabled="block.check()">Do something</button>
137 angular.module('crmUtil').factory('crmBlocker', function() {
138 return function() {
139 var blocks = 0;
140 var result = function(promise) {
141 blocks++;
650a6ffc 142 return promise.finally(function() {
bdd3f781
TO
143 blocks--;
144 });
145 };
146 result.check = function() {
147 return blocks > 0;
148 };
149 return result;
150 };
151 });
152
a8e65974
TO
153 angular.module('crmUtil').factory('crmLegacy', function() {
154 return CRM;
155 });
156
226ef186
TO
157 // example: scope.$watch('foo', crmLog.wrap(function(newValue, oldValue){ ... }));
158 angular.module('crmUtil').factory('crmLog', function(){
159 var level = 0;
160 var write = console.log;
161 function indent() {
162 var s = '>';
163 for (var i = 0; i < level; i++) s = s + ' ';
164 return s;
165 }
166 var crmLog = {
167 log: function(msg, vars) {
168 write(indent() + msg, vars);
169 },
170 wrap: function(label, f) {
171 return function(){
172 level++;
173 crmLog.log(label + ": start", arguments);
174 var r;
175 try {
176 r = f.apply(this, arguments);
177 } finally {
178 crmLog.log(label + ": end");
179 level--;
180 }
181 return r;
f2bad133 182 };
226ef186 183 }
f286acec 184 };
226ef186 185 return crmLog;
f286acec
TO
186 });
187
a8e65974
TO
188 angular.module('crmUtil').factory('crmNavigator', ['$window', function($window) {
189 return {
190 redirect: function(path) {
191 $window.location.href = path;
192 }
193 };
194 }]);
195
a4f8f900
TO
196 // Wrap an async function in a queue, ensuring that independent async calls are issued in strict sequence.
197 // usage: qApi = crmQueue(crmApi); qApi(entity,action,...).then(...); qApi(entity2,action2,...).then(...);
198 // This is similar to promise-chaining, but allows chaining independent procs (without explicitly sharing promises).
199 angular.module('crmUtil').factory('crmQueue', function($q) {
200 // @param worker A function which generates promises
201 return function crmQueue(worker) {
202 var queue = [];
203 function next() {
204 var task = queue[0];
205 worker.apply(null, task.a).then(
206 function onOk(data) {
207 queue.shift();
208 task.dfr.resolve(data);
209 if (queue.length > 0) next();
210 },
211 function onErr(err) {
212 queue.shift();
213 task.dfr.reject(err);
214 if (queue.length > 0) next();
215 }
216 );
217 }
218 function enqueue() {
219 var dfr = $q.defer();
220 queue.push({a: arguments, dfr: dfr});
221 if (queue.length === 1) {
222 next();
223 }
224 return dfr.promise;
225 }
226 return enqueue;
227 };
228 });
229
226ef186
TO
230 // Adapter for CRM.status which supports Angular promises (instead of jQuery promises)
231 // example: crmStatus('Saving', crmApi(...)).then(function(result){...})
232 angular.module('crmUtil').factory('crmStatus', function($q){
233 return function(options, aPromise){
a66571ab
TO
234 if (aPromise) {
235 return CRM.toAPromise($q, CRM.status(options, CRM.toJqPromise(aPromise)));
236 } else {
237 return CRM.toAPromise($q, CRM.status(options));
238 }
226ef186
TO
239 };
240 });
241
60ebf0a5
TO
242 // crmWatcher allows one to setup event listeners and temporarily suspend
243 // them en masse.
244 //
245 // example:
246 // angular.controller(... function($scope, crmWatcher){
247 // var watcher = crmWatcher();
248 // function myfunc() {
249 // watcher.suspend('foo', function(){
250 // ...do stuff...
251 // });
252 // }
253 // watcher.setup('foo', function(){
254 // return [
255 // $scope.$watch('foo', myfunc),
256 // $scope.$watch('bar', myfunc),
257 // $scope.$watch('whiz', otherfunc)
258 // ];
259 // });
260 // });
261 angular.module('crmUtil').factory('crmWatcher', function(){
262 return function() {
263 var unwatches = {}, watchFactories = {}, suspends = {};
264
265 // Specify the list of watches
266 this.setup = function(name, newWatchFactory) {
267 watchFactories[name] = newWatchFactory;
268 unwatches[name] = watchFactories[name]();
269 suspends[name] = 0;
270 return this;
271 };
272
273 // Temporarily disable watches and run some logic
274 this.suspend = function(name, f) {
275 suspends[name]++;
276 this.teardown(name);
277 var r;
278 try {
279 r = f.apply(this, []);
280 } finally {
281 if (suspends[name] === 1) {
282 unwatches[name] = watchFactories[name]();
283 if (!angular.isArray(unwatches[name])) {
284 unwatches[name] = [unwatches[name]];
285 }
286 }
287 suspends[name]--;
288 }
289 return r;
290 };
291
292 this.teardown = function(name) {
293 if (!unwatches[name]) return;
294 _.each(unwatches[name], function(unwatch){
295 unwatch();
296 });
297 delete unwatches[name];
298 };
299
300 return this;
f2bad133 301 };
60ebf0a5
TO
302 });
303
60ebf0a5 304})(angular, CRM.$, CRM._);