CRM-16142 - Variable scope fixes
[civicrm-core.git] / js / angular-crm-util.js
index 1d73ef7bbe2b167304ee9f21c825de51fb8901da..3a1a8fd54ff177827d12d023028c6dbe7c0deb58 100644 (file)
 (function (angular, $, _) {
   angular.module('crmUtil', []);
 
-  // Adapter for CRM.status which supports Angular promises (instead of jQuery promises)
-  // example: crmStatus('Saving', crmApi(...)).then(function(result){...})
-  angular.module('crmUtil').factory('crmStatus', function($q){
-    return function(options, aPromise){
-      return CRM.toAPromise($q, CRM.status(options, CRM.toJqPromise(aPromise)));
+  // usage:
+  //   crmApi('Entity', 'action', {...}).then(function(apiResult){...})
+  //
+  // Note: To mock API results in unit-tests, override crmApi.backend, e.g.
+  //   var apiSpy = jasmine.createSpy('crmApi');
+  //   crmApi.backend = apiSpy.and.returnValue(crmApi.val({
+  //     is_error: 1
+  //   }));
+  angular.module('crmUtil').factory('crmApi', function($q) {
+    var crmApi = function(entity, action, params, message) {
+      // JSON serialization in CRM.api3 is not aware of Angular metadata like $$hash, so use angular.toJson()
+      var deferred = $q.defer();
+      var p;
+      var backend = crmApi.backend || CRM.api3;
+      if (_.isObject(entity)) {
+        // eval content is locally generated.
+        /*jshint -W061 */
+        p = backend(eval('('+angular.toJson(entity)+')'), message);
+      } else {
+        // eval content is locally generated.
+        /*jshint -W061 */
+        p = backend(entity, action, eval('('+angular.toJson(params)+')'), message);
+      }
+      // CRM.api3 returns a promise, but the promise doesn't really represent errors as errors, so we
+      // convert them
+      p.then(
+        function(result) {
+          if (result.is_error) {
+            deferred.reject(result);
+          } else {
+            deferred.resolve(result);
+          }
+        },
+        function(error) {
+          deferred.reject(error);
+        }
+      );
+      return deferred.promise;
+    };
+    crmApi.backend = null;
+    crmApi.val = function(value) {
+      var d = $.Deferred();
+      d.resolve(value);
+      return d.promise();
+    };
+    return crmApi;
+  });
+
+  // Get and cache the metadata for an API entity.
+  // usage:
+  //   $q.when(crmMetadata.getFields('MyEntity'), function(fields){
+  //     console.log('The fields are:', options);
+  //   });
+  angular.module('crmUtil').factory('crmMetadata', function($q, crmApi) {
+
+    // Convert {key:$,value:$} sequence to unordered {$key: $value} map.
+    function convertOptionsToMap(options) {
+      var result = {};
+      angular.forEach(options, function(o) {
+        result[o.key] = o.value;
+      });
+      return result;
+    }
+
+    var cache = {}; // cache[entityName+'::'+action][fieldName].title
+    var deferreds = {}; // deferreds[cacheKey].push($q.defer())
+    var crmMetadata = {
+      // usage: $q.when(crmMetadata.getField('MyEntity', 'my_field')).then(...);
+      getField: function getField(entity, field) {
+        return $q.when(crmMetadata.getFields(entity)).then(function(fields){
+          return fields[field];
+        });
+      },
+      // usage: $q.when(crmMetadata.getFields('MyEntity')).then(...);
+      // usage: $q.when(crmMetadata.getFields(['MyEntity', 'myaction'])).then(...);
+      getFields: function getFields(entity) {
+        var action = '', cacheKey;
+        if (_.isArray(entity)) {
+          action = entity[1];
+          entity = entity[0];
+          cacheKey = entity + '::' + action;
+        } else {
+          cacheKey = entity;
+        }
+
+        if (_.isObject(cache[cacheKey])) {
+          return cache[cacheKey];
+        }
+
+        var needFetch = _.isEmpty(deferreds[cacheKey]);
+        deferreds[cacheKey] = deferreds[cacheKey] || [];
+        var deferred = $q.defer();
+        deferreds[cacheKey].push(deferred);
+
+        if (needFetch) {
+          crmApi(entity, 'getfields', {action: action, sequential: 1, options: {get_options: 'all'}})
+            .then(
+            // on success:
+            function(fields) {
+              cache[cacheKey] = _.indexBy(fields.values, 'name');
+              angular.forEach(cache[cacheKey],function (field){
+                if (field.options) {
+                  field.optionsMap = convertOptionsToMap(field.options);
+                }
+              });
+              angular.forEach(deferreds[cacheKey], function(dfr) {
+                dfr.resolve(cache[cacheKey]);
+              });
+              delete deferreds[cacheKey];
+            },
+            // on error:
+            function() {
+              cache[cacheKey] = {}; // cache nack
+              angular.forEach(deferreds[cacheKey], function(dfr) {
+                dfr.reject();
+              });
+              delete deferreds[cacheKey];
+            }
+          );
+        }
+
+        return deferred.promise;
+      }
     };
+
+    return crmMetadata;
   });
 
+  // usage:
+  // var block = $scope.block = crmBlocker();
+  // $scope.save = function() { return block(crmApi('MyEntity','create',...)); };
+  // <button ng-click="save()" ng-disabled="block.check()">Do something</button>
+  angular.module('crmUtil').factory('crmBlocker', function() {
+    return function() {
+      var blocks = 0;
+      var result = function(promise) {
+        blocks++;
+        return promise.finally(function() {
+          blocks--;
+        });
+      };
+      result.check = function() {
+        return blocks > 0;
+      };
+      return result;
+    };
+  });
+
+  angular.module('crmUtil').factory('crmLegacy', function() {
+    return CRM;
+  });
+
+  // example: scope.$watch('foo', crmLog.wrap(function(newValue, oldValue){ ... }));
+  angular.module('crmUtil').factory('crmLog', function(){
+    var level = 0;
+    var write = console.log;
+    function indent() {
+      var s = '>';
+      for (var i = 0; i < level; i++) s = s + '  ';
+      return s;
+    }
+    var crmLog = {
+      log: function(msg, vars) {
+        write(indent() + msg, vars);
+      },
+      wrap: function(label, f) {
+        return function(){
+          level++;
+          crmLog.log(label + ": start", arguments);
+          var r;
+          try {
+            r = f.apply(this, arguments);
+          } finally {
+            crmLog.log(label + ": end");
+            level--;
+          }
+          return r;
+        };
+      }
+    };
+    return crmLog;
+  });
+
+  angular.module('crmUtil').factory('crmNavigator', ['$window', function($window) {
+    return {
+      redirect: function(path) {
+        $window.location.href = path;
+      }
+    };
+  }]);
+
   angular.module('crmUtil').factory('crmNow', function($q){
     // FIXME: surely there's already some helper which can do this in one line?
     // @return string "YYYY-MM-DD hh:mm:ss"
     };
   });
 
+  // Adapter for CRM.status which supports Angular promises (instead of jQuery promises)
+  // example: crmStatus('Saving', crmApi(...)).then(function(result){...})
+  angular.module('crmUtil').factory('crmStatus', function($q){
+    return function(options, aPromise){
+      if (aPromise) {
+        return CRM.toAPromise($q, CRM.status(options, CRM.toJqPromise(aPromise)));
+      } else {
+        return CRM.toAPromise($q, CRM.status(options));
+      }
+    };
+  });
+
   // crmWatcher allows one to setup event listeners and temporarily suspend
   // them en masse.
   //
       };
 
       return this;
-    }
-  });
-
-  // example: scope.$watch('foo', crmLog.wrap(function(newValue, oldValue){ ... }));
-  angular.module('crmUtil').factory('crmLog', function(){
-    var level = 0;
-    var write = console.log;
-    function indent() {
-      var s = '>';
-      for (var i = 0; i < level; i++) s = s + '  ';
-      return s;
-    }
-    var crmLog = {
-      log: function(msg, vars) {
-        write(indent() + msg, vars);
-      },
-      wrap: function(label, f) {
-        return function(){
-          level++;
-          crmLog.log(label + ": start", arguments);
-          var r;
-          try {
-            r = f.apply(this, arguments);
-          } finally {
-            crmLog.log(label + ": end");
-            level--;
-          }
-          return r;
-        }
-      }
     };
-    return crmLog;
   });
 
 })(angular, CRM.$, CRM._);