Merge branch '4.5' of https://github.com/civicrm/civicrm-core into 4.6
[civicrm-core.git] / js / angular-crm-util.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
226ef186
TO
196 // Adapter for CRM.status which supports Angular promises (instead of jQuery promises)
197 // example: crmStatus('Saving', crmApi(...)).then(function(result){...})
198 angular.module('crmUtil').factory('crmStatus', function($q){
199 return function(options, aPromise){
a66571ab
TO
200 if (aPromise) {
201 return CRM.toAPromise($q, CRM.status(options, CRM.toJqPromise(aPromise)));
202 } else {
203 return CRM.toAPromise($q, CRM.status(options));
204 }
226ef186
TO
205 };
206 });
207
60ebf0a5
TO
208 // crmWatcher allows one to setup event listeners and temporarily suspend
209 // them en masse.
210 //
211 // example:
212 // angular.controller(... function($scope, crmWatcher){
213 // var watcher = crmWatcher();
214 // function myfunc() {
215 // watcher.suspend('foo', function(){
216 // ...do stuff...
217 // });
218 // }
219 // watcher.setup('foo', function(){
220 // return [
221 // $scope.$watch('foo', myfunc),
222 // $scope.$watch('bar', myfunc),
223 // $scope.$watch('whiz', otherfunc)
224 // ];
225 // });
226 // });
227 angular.module('crmUtil').factory('crmWatcher', function(){
228 return function() {
229 var unwatches = {}, watchFactories = {}, suspends = {};
230
231 // Specify the list of watches
232 this.setup = function(name, newWatchFactory) {
233 watchFactories[name] = newWatchFactory;
234 unwatches[name] = watchFactories[name]();
235 suspends[name] = 0;
236 return this;
237 };
238
239 // Temporarily disable watches and run some logic
240 this.suspend = function(name, f) {
241 suspends[name]++;
242 this.teardown(name);
243 var r;
244 try {
245 r = f.apply(this, []);
246 } finally {
247 if (suspends[name] === 1) {
248 unwatches[name] = watchFactories[name]();
249 if (!angular.isArray(unwatches[name])) {
250 unwatches[name] = [unwatches[name]];
251 }
252 }
253 suspends[name]--;
254 }
255 return r;
256 };
257
258 this.teardown = function(name) {
259 if (!unwatches[name]) return;
260 _.each(unwatches[name], function(unwatch){
261 unwatch();
262 });
263 delete unwatches[name];
264 };
265
266 return this;
f2bad133 267 };
60ebf0a5
TO
268 });
269
60ebf0a5 270})(angular, CRM.$, CRM._);