CRM-15832 - crmResource - Add module to load partials+strings in batches.
authorTim Otten <totten@civicrm.org>
Fri, 16 Jan 2015 11:14:48 +0000 (03:14 -0800)
committerTim Otten <totten@civicrm.org>
Mon, 19 Jan 2015 06:17:35 +0000 (22:17 -0800)
CRM/Core/Page/Angular.php
CRM/Utils/File.php
Civi/Angular/Manager.php
js/angular-crmResource/all.js [new file with mode: 0644]
js/angular-crmResource/byModule.js [new file with mode: 0644]
tests/phpunit/Civi/Angular/ManagerTest.php

index 2e950361f2c13f4b14ea130fbded6c457eef0208..59c4dc81cefd6e7247cc8d42c03de3a7aa1e3409 100644 (file)
@@ -62,6 +62,7 @@ class CRM_Core_Page_Angular extends CRM_Core_Page {
         'resourceUrls' => CRM_Extension_System::singleton()->getMapper()->getActiveModuleUrls(),
         'angular' => array(
           'modules' => array_merge(array('ngRoute'), array_keys($modules)),
+          'cacheCode' => $this->res->getCacheCode(),
         ),
         'crmAttachment' => array(
           'token' => CRM_Core_Page_AJAX_Attachment::createToken(),
index d9a115e9a2990102dd165cbecdfa134957aa3570..f5ae019b0860eb7a796822561fdbf5bb763c4c46 100644 (file)
@@ -610,9 +610,12 @@ HTACCESS;
    *   base dir.
    * @param string $pattern
    *   glob pattern, eg "*.txt".
+   * @param bool $relative
+   *   TRUE if paths should be made relative to $dir
    * @return array(string)
    */
-  public static function findFiles($dir, $pattern) {
+  public static function findFiles($dir, $pattern, $relative = FALSE) {
+    $dir = rtrim($dir, '/');
     $todos = array($dir);
     $result = array();
     while (!empty($todos)) {
@@ -621,7 +624,7 @@ HTACCESS;
       if (is_array($matches)) {
         foreach ($matches as $match) {
           if (!is_dir($match)) {
-            $result[] = $match;
+            $result[] = $relative ? CRM_Utils_File::relativize($match, "$dir/") : $match;
           }
         }
       }
index 852a0e685a899dbd5a50567bf5c4c54577a448df..8466b937a178f744c3e87d7e8cef165a121565a8 100644 (file)
@@ -55,6 +55,11 @@ class Manager {
         'css' => array('css/angular-crmAttachment.css'),
         'partials' => array('partials/crmAttachment/*.html'),
       );
+      $angularModules['crmResource'] = array(
+        'ext' => 'civicrm',
+        // 'js' => array('js/angular-crmResource/byModule.js'), // One HTTP request per module.
+        'js' => array('js/angular-crmResource/all.js'), // One HTTP request for all modules.
+      );
       $angularModules['crmUi'] = array(
         'ext' => 'civicrm',
         'js' => array('js/angular-crm-ui.js', 'packages/ckeditor/ckeditor.js'),
@@ -148,20 +153,25 @@ class Manager {
    *   Angular module name.
    * @return array
    *   Array(string $extFilePath => string $html)
+   * @throws \Exception
+   *   Invalid partials configuration.
    */
   public function getPartials($name) {
     $module = $this->getModule($name);
     $result = array();
     if (isset($module['partials'])) {
-      foreach ($module['partials'] as $file) {
-        $filename = $name . '/' . $file;
-        $result[$filename] = file_get_contents($this->res->getPath($module['ext'], $file));
+      foreach ($module['partials'] as $partialDir) {
+        $partialDir = $this->res->getPath($module['ext']) . '/' . $partialDir;
+        $files = \CRM_Utils_File::findFiles($partialDir, '*.html', TRUE);
+        foreach ($files as $file) {
+          $filename = '~/' . $name . '/' . $file;
+          $result[$filename] = file_get_contents($partialDir . '/' . $file);
+        }
       }
     }
     return $result;
   }
 
-
   /**
    * Get list of translated strings for a module.
    *
@@ -206,13 +216,17 @@ class Manager {
       }
     }
     if (isset($module['partials'])) {
-      foreach ($module['partials'] as $file) {
-        $strings = $this->res->getStrings()->get(
-          $module['ext'],
-          $this->res->getPath($module['ext'], $file),
-          'text/html'
-        );
-        $result = array_unique(array_merge($result, $strings));
+      foreach ($module['partials'] as $partialDir) {
+        $partialDir = $this->res->getPath($module['ext']) . '/' . $partialDir;
+        $files = \CRM_Utils_File::findFiles($partialDir, '*.html');
+        foreach ($files as $file) {
+          $strings = $this->res->getStrings()->get(
+            $module['ext'],
+            $file,
+            'text/html'
+          );
+          $result = array_unique(array_merge($result, $strings));
+        }
       }
     }
     return $result;
diff --git a/js/angular-crmResource/all.js b/js/angular-crmResource/all.js
new file mode 100644 (file)
index 0000000..28ef6d6
--- /dev/null
@@ -0,0 +1,88 @@
+// crmResource: Given a templateUrl "~/mymodule/myfile.html", load the matching HTML.
+// This implementation loads all partials and strings in one batch.
+(function(angular, $, _) {
+  angular.module('crmResource', []);
+
+  angular.module('crmResource').factory('crmResource', function($q, $http) {
+    var deferreds = {}; // null|object; deferreds[url][idx] = Deferred;
+    var templates = null; // null|object; templates[url] = HTML;
+
+    var notify = function notify() {
+      var oldDfrds = deferreds;
+      deferreds = null;
+
+      angular.forEach(oldDfrds, function(dfrs, url) {
+        if (templates[url]) {
+          angular.forEach(dfrs, function(dfr) {
+            dfr.resolve({
+              status: 200,
+              headers: function(name) {
+                var headers = {'Content-type': 'text/html'};
+                return name ? headers[name] : headers;
+              },
+              data: templates[url]
+            });
+          });
+        }
+        else {
+          angular.forEach(dfrs, function(dfr) {
+            dfr.reject({status: 500}); // FIXME
+          });
+        }
+      });
+    };
+
+    var moduleUrl = CRM.url('civicrm/ajax/angular-modules', {r: CRM.angular.cacheCode});
+    $http.get(moduleUrl)
+      .success(function httpSuccess(data) {
+        templates = [];
+        angular.forEach(data, function(module) {
+          if (module.partials) {
+            angular.extend(templates, module.partials);
+          }
+          if (module.strings) {
+            angular.extend(CRM.strings, module.strings);
+          }
+        });
+        notify();
+      })
+      .error(function httpError() {
+        templates = [];
+        notify();
+      });
+
+    return {
+      // @return string|Promise<string>
+      getUrl: function getUrl(url) {
+        if (templates !== null) {
+          return templates[url];
+        }
+        else {
+          var deferred = $q.defer();
+          if (!deferreds[url]) {
+            deferreds[url] = [];
+          }
+          deferreds[url].push(deferred);
+          return deferred.promise;
+        }
+      }
+    };
+  });
+
+  angular.module('crmResource').config(function($provide) {
+    $provide.decorator('$templateCache', function($delegate, $http, $q, crmResource) {
+      var origGet = $delegate.get;
+      var urlPat = /^~\//;
+      $delegate.get = function(url) {
+        if (urlPat.test(url)) {
+          return crmResource.getUrl(url);
+        }
+        else {
+          return origGet.call(this, url);
+        }
+      };
+      return $delegate;
+    });
+  });
+
+})(angular, CRM.$, CRM._);
diff --git a/js/angular-crmResource/byModule.js b/js/angular-crmResource/byModule.js
new file mode 100644 (file)
index 0000000..0050c19
--- /dev/null
@@ -0,0 +1,137 @@
+// crmResource: Given a templateUrl "~/mymodule/myfile.html", load the matching HTML.
+// This implementation loads partials and strings in per-module batches.
+// FIXME: handling of CRM.strings not well tested; may be racy
+(function(angular, $, _) {
+  angular.module('crmResource', []);
+
+  angular.module('crmResource').factory('crmResource', function($q, $http) {
+    var modules = {}; // moduleQueue[module] = 'loading'|Object;
+    var templates = {}; // templates[url] = HTML;
+
+    function CrmResourceModule(name) {
+      this.name = name;
+      this.status = 'new';  // loading|loaded|error
+      this.data = null;
+      this.deferreds = [];
+    }
+
+    angular.extend(CrmResourceModule.prototype, {
+      createDeferred: function createDeferred() {
+        var deferred = $q.defer();
+        switch (this.status) {
+          case 'new':
+          case 'loading':
+            this.deferreds.push(deferred);
+            break;
+          case 'loaded':
+            deferred.resolve(this.data);
+            break;
+          case 'error':
+            deferred.reject();
+            break;
+          default:
+            throw 'Unknown status: ' + this.status;
+        }
+        return deferred.promise;
+      },
+      load: function load() {
+        var module = this;
+        this.status = 'loading';
+        var moduleUrl = CRM.url('civicrm/ajax/angular-modules', {modules: module.name, r: CRM.angular.cacheCode});
+        $http.get(moduleUrl)
+          .success(function httpSuccess(data) {
+            if (data[module.name]) {
+              module.onSuccess(data[module.name]);
+            }
+            else {
+              module.onError();
+            }
+          })
+          .error(function httpError() {
+            module.onError();
+          });
+      },
+      onSuccess: function onSuccess(data) {
+        var module = this;
+        this.data = data;
+        this.status = 'loaded';
+        if (this.data.partials) {
+          angular.extend(templates, this.data.partials);
+        }
+        if (this.data.strings) {
+          angular.extend(CRM.strings, this.data.strings);
+        }
+        angular.forEach(this.deferreds, function(deferred) {
+          deferred.resolve(module.data);
+        });
+        delete this.deferreds;
+      },
+      onError: function onError() {
+        this.status = 'error';
+        angular.forEach(this.deferreds, function(deferred) {
+          deferred.reject();
+        });
+        delete this.deferreds;
+      }
+    });
+
+    return {
+      // @return Promise<ModuleData>
+      getModule: function getModule(name) {
+        if (!modules[name]) {
+          modules[name] = new CrmResourceModule(name);
+          modules[name].load();
+        }
+        return modules[name].createDeferred();
+      },
+      // @return string|Promise<string>
+      getUrl: function getUrl(url) {
+        if (templates[url]) {
+          return templates[url];
+        }
+
+        var parts = url.split('/');
+        var deferred = $q.defer();
+        this.getModule(parts[1]).then(
+          function() {
+            if (templates[url]) {
+              deferred.resolve({
+                status: 200,
+                headers: function(name) {
+                  var headers = {'Content-type': 'text/html'};
+                  return name ? headers[name] : headers;
+                },
+                data: templates[url]
+              });
+            }
+            else {
+              deferred.reject({status: 500}); // FIXME
+            }
+          },
+          function() {
+            deferred.reject({status: 500}); // FIXME
+          }
+        );
+
+        return deferred.promise;
+      }
+    };
+  });
+
+  angular.module('crmResource').config(function($provide) {
+    $provide.decorator('$templateCache', function($delegate, $http, $q, crmResource) {
+      var origGet = $delegate.get;
+      var urlPat = /^~\//;
+      $delegate.get = function(url) {
+        if (urlPat.test(url)) {
+          return crmResource.getUrl(url);
+        }
+        else {
+          return origGet.call(this, url);
+        }
+      };
+      return $delegate;
+    });
+  });
+
+})(angular, CRM.$, CRM._);
index 28ce0d5619ac15c414714408bf4934c7a11fbbda..230cc98e916f6f4bda3d00e5f306c76fa9bd5a23 100644 (file)
@@ -86,8 +86,8 @@ class ManagerTest extends \CiviUnitTestCase {
       }
       if (isset($module['partials'])) {
         $this->assertTrue(is_array($module['partials']));
-        foreach ($module['partials'] as $file) {
-          $this->assertTrue(file_exists($this->res->getPath($module['ext'], $file)));
+        foreach ((array) $module['partials'] as $basedir) {
+          $this->assertTrue(is_dir($this->res->getPath($module['ext']) . '/' . $basedir));
           $counts['partials']++;
         }
       }
@@ -103,7 +103,7 @@ class ManagerTest extends \CiviUnitTestCase {
    */
   public function testGetPartials() {
     $partials = $this->angular->getPartials('crmMailing');
-    $this->assertRegExp('/\<form.*name="crmMailing"/', $partials['crmMailing/partials/crmMailing/edit.html']);
+    $this->assertRegExp('/\<form.*name="crmMailing"/', $partials['~/crmMailing/edit.html']);
     // If crmMailing changes, feel free to use a different example.
   }