| 1 | /// crmUi: Sundry UI helpers |
| 2 | (function (angular, $, _) { |
| 3 | angular.module('crmUtil', CRM.angRequires('crmUtil')); |
| 4 | |
| 5 | // Angular implementation of CRM.api3 |
| 6 | // @link https://docs.civicrm.org/dev/en/latest/api/interfaces/#angularjs |
| 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.module('crmUtil').factory('crmLoadScript', function($q) { |
| 350 | return function(url) { |
| 351 | var deferred = $q.defer(); |
| 352 | |
| 353 | CRM.loadScript(url).done(function() { |
| 354 | deferred.resolve(true); |
| 355 | }); |
| 356 | |
| 357 | return deferred.promise; |
| 358 | }; |
| 359 | }); |
| 360 | |
| 361 | })(angular, CRM.$, CRM._); |