1 /// crmUi: Sundry UI helpers
2 (function (angular
, $, _
) {
3 angular
.module('crmUtil', CRM
.angRequires('crmUtil'));
5 // Angular implementation of CRM.api3
6 // @link http://wiki.civicrm.org/confluence/display/CRMDOC/AJAX+Interface#AJAXInterface-CRM.api3
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({
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();
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');
24 if (_
.isObject(entity
)) {
25 // eval content is locally generated.
27 p
= backend(eval('('+angular
.toJson(entity
)+')'), action
);
29 // eval content is locally generated.
31 p
= backend(entity
, action
, eval('('+angular
.toJson(params
)+')'), message
);
33 // CRM.api3 returns a promise, but the promise doesn't really represent errors as errors, so we
37 if (result
.is_error
) {
38 deferred
.reject(result
);
40 deferred
.resolve(result
);
44 deferred
.reject(error
);
47 return deferred
.promise
;
49 crmApi
.backend
= null;
50 crmApi
.val = function(value
) {
58 // Get and cache the metadata for an API entity.
60 // $q.when(crmMetadata.getFields('MyEntity'), function(fields){
61 // console.log('The fields are:', options);
63 angular
.module('crmUtil').factory('crmMetadata', function($q
, crmApi
) {
65 // Convert {key:$,value:$} sequence to unordered {$key: $value} map.
66 function convertOptionsToMap(options
) {
68 angular
.forEach(options
, function(o
) {
69 result
[o
.key
] = o
.value
;
74 var cache
= {}; // cache[entityName+'::'+action][fieldName].title
75 var deferreds
= {}; // deferreds[cacheKey].push($q.defer())
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
){
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
)) {
90 cacheKey
= entity
+ '::' + action
;
95 if (_
.isObject(cache
[cacheKey
])) {
96 return cache
[cacheKey
];
99 var needFetch
= _
.isEmpty(deferreds
[cacheKey
]);
100 deferreds
[cacheKey
] = deferreds
[cacheKey
] || [];
101 var deferred
= $q
.defer();
102 deferreds
[cacheKey
].push(deferred
);
105 crmApi(entity
, 'getfields', {action
: action
, sequential
: 1, options
: {get_options
: 'all'}})
109 cache
[cacheKey
] = _
.indexBy(fields
.values
, 'name');
110 angular
.forEach(cache
[cacheKey
],function (field
){
112 field
.optionsMap
= convertOptionsToMap(field
.options
);
115 angular
.forEach(deferreds
[cacheKey
], function(dfr
) {
116 dfr
.resolve(cache
[cacheKey
]);
118 delete deferreds
[cacheKey
];
122 cache
[cacheKey
] = {}; // cache nack
123 angular
.forEach(deferreds
[cacheKey
], function(dfr
) {
126 delete deferreds
[cacheKey
];
131 return deferred
.promise
;
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() {
145 var result = function(promise
) {
147 return promise
.finally(function() {
151 result
.check = function() {
158 angular
.module('crmUtil').factory('crmLegacy', function() {
162 // example: scope.$watch('foo', crmLog.wrap(function(newValue, oldValue){ ... }));
163 angular
.module('crmUtil').factory('crmLog', function(){
165 var write
= console
.log
;
168 for (var i
= 0; i
< level
; i
++) s
= s
+ ' ';
172 log: function(msg
, vars
) {
173 write(indent() + msg
, vars
);
175 wrap: function(label
, f
) {
178 crmLog
.log(label
+ ": start", arguments
);
181 r
= f
.apply(this, arguments
);
183 crmLog
.log(label
+ ": end");
193 angular
.module('crmUtil').factory('crmNavigator', ['$window', function($window
) {
195 redirect: function(path
) {
196 $window
.location
.href
= path
;
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
) {
210 worker
.apply(null, task
.a
).then(
211 function onOk(data
) {
213 task
.dfr
.resolve(data
);
214 if (queue
.length
> 0) next();
216 function onErr(err
) {
218 task
.dfr
.reject(err
);
219 if (queue
.length
> 0) next();
224 var dfr
= $q
.defer();
225 queue
.push({a
: arguments
, dfr
: dfr
});
226 if (queue
.length
=== 1) {
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
){
240 return CRM
.toAPromise($q
, CRM
.status(options
, CRM
.toJqPromise(aPromise
)));
242 return CRM
.toAPromise($q
, CRM
.status(options
));
247 // crmWatcher allows one to setup event listeners and temporarily suspend
251 // angular.controller(... function($scope, crmWatcher){
252 // var watcher = crmWatcher();
253 // function myfunc() {
254 // watcher.suspend('foo', function(){
258 // watcher.setup('foo', function(){
260 // $scope.$watch('foo', myfunc),
261 // $scope.$watch('bar', myfunc),
262 // $scope.$watch('whiz', otherfunc)
266 angular
.module('crmUtil').factory('crmWatcher', function(){
268 var unwatches
= {}, watchFactories
= {}, suspends
= {};
270 // Specify the list of watches
271 this.setup = function(name
, newWatchFactory
) {
272 watchFactories
[name
] = newWatchFactory
;
273 unwatches
[name
] = watchFactories
[name
]();
278 // Temporarily disable watches and run some logic
279 this.suspend = function(name
, f
) {
284 r
= f
.apply(this, []);
286 if (suspends
[name
] === 1) {
287 unwatches
[name
] = watchFactories
[name
]();
288 if (!angular
.isArray(unwatches
[name
])) {
289 unwatches
[name
] = [unwatches
[name
]];
297 this.teardown = function(name
) {
298 if (!unwatches
[name
]) return;
299 _
.each(unwatches
[name
], function(unwatch
){
302 delete unwatches
[name
];
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
) {
316 return function(func
) {
317 var deferred
= $q
.defer();
319 function checkResult(result
, success
) {
320 _
.pull(executing
, func
);
321 if (_
.includes(pending
, func
)) {
323 } else if (success
) {
324 deferred
.resolve(result
);
326 deferred
.reject(result
);
331 executing
.push(func
);
332 _
.pull(pending
, func
);
333 func().then(function(result
) {
334 checkResult(result
, true);
335 }, function(result
) {
336 checkResult(result
, false);
340 if (!_
.includes(executing
, func
)) {
342 } else if (!_
.includes(pending
, func
)) {
345 return deferred
.promise
;
349 })(angular
, CRM
.$, CRM
._
);