| 1 | /// crmUi: Sundry UI helpers |
| 2 | (function (angular, $, _) { |
| 3 | angular.module('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 (_.isObject(entity)) { |
| 20 | // eval content is locally generated. |
| 21 | /*jshint -W061 */ |
| 22 | p = backend(eval('('+angular.toJson(entity)+')'), action); |
| 23 | } else { |
| 24 | // eval content is locally generated. |
| 25 | /*jshint -W061 */ |
| 26 | p = backend(entity, action, eval('('+angular.toJson(params)+')'), message); |
| 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 | }; |
| 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) { |
| 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 | |
| 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) { |
| 100 | crmApi(entity, 'getfields', {action: action, sequential: 1, options: {get_options: 'all'}}) |
| 101 | .then( |
| 102 | // on success: |
| 103 | function(fields) { |
| 104 | cache[cacheKey] = _.indexBy(fields.values, 'name'); |
| 105 | angular.forEach(cache[cacheKey],function (field){ |
| 106 | if (field.options) { |
| 107 | field.optionsMap = convertOptionsToMap(field.options); |
| 108 | } |
| 109 | }); |
| 110 | angular.forEach(deferreds[cacheKey], function(dfr) { |
| 111 | dfr.resolve(cache[cacheKey]); |
| 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; |
| 131 | }); |
| 132 | |
| 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++; |
| 142 | return promise.finally(function() { |
| 143 | blocks--; |
| 144 | }); |
| 145 | }; |
| 146 | result.check = function() { |
| 147 | return blocks > 0; |
| 148 | }; |
| 149 | return result; |
| 150 | }; |
| 151 | }); |
| 152 | |
| 153 | angular.module('crmUtil').factory('crmLegacy', function() { |
| 154 | return CRM; |
| 155 | }); |
| 156 | |
| 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; |
| 182 | }; |
| 183 | } |
| 184 | }; |
| 185 | return crmLog; |
| 186 | }); |
| 187 | |
| 188 | angular.module('crmUtil').factory('crmNavigator', ['$window', function($window) { |
| 189 | return { |
| 190 | redirect: function(path) { |
| 191 | $window.location.href = path; |
| 192 | } |
| 193 | }; |
| 194 | }]); |
| 195 | |
| 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 | |
| 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){ |
| 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 | } |
| 239 | }; |
| 240 | }); |
| 241 | |
| 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; |
| 301 | }; |
| 302 | }); |
| 303 | |
| 304 | })(angular, CRM.$, CRM._); |