From 16072ce154f3c84c6b041d618454688d3f75a526 Mon Sep 17 00:00:00 2001 From: Tim Otten Date: Thu, 15 Jan 2015 22:28:28 -0800 Subject: [PATCH] CRM-15832 - getAngularModules - Move from CRM_Core_Page_Angular to Civi\Angular\Manager --- CRM/Core/Page/Angular.php | 109 ++------- Civi/Angular/Manager.php | 256 ++++++++++++++++++++ Civi/Core/Container.php | 13 + tests/phpunit/CRM/Core/Page/AngularTest.php | 54 ----- tests/phpunit/Civi/Angular/ManagerTest.php | 118 +++++++++ 5 files changed, 402 insertions(+), 148 deletions(-) create mode 100644 Civi/Angular/Manager.php delete mode 100644 tests/phpunit/CRM/Core/Page/AngularTest.php create mode 100644 tests/phpunit/Civi/Angular/ManagerTest.php diff --git a/CRM/Core/Page/Angular.php b/CRM/Core/Page/Angular.php index 3bdac40234..2e950361f2 100644 --- a/CRM/Core/Page/Angular.php +++ b/CRM/Core/Page/Angular.php @@ -17,6 +17,12 @@ class CRM_Core_Page_Angular extends CRM_Core_Page { */ protected $res; + + /** + * @var Civi\Angular\Manager + */ + protected $angular; + /** * @param string $title * Title of the page. @@ -28,6 +34,7 @@ class CRM_Core_Page_Angular extends CRM_Core_Page { public function __construct($title = NULL, $mode = NULL, $res = NULL) { parent::__construct($title, $mode); $this->res = CRM_Core_Resources::singleton(); + $this->angular = Civi\Core\Container::singleton()->get('angular'); } /** @@ -47,7 +54,7 @@ class CRM_Core_Page_Angular extends CRM_Core_Page { * Register resources required by Angular. */ public function registerResources() { - $modules = $this->getAngularModules(); + $modules = $this->angular->getModules(); $this->res->addSettingsFactory(function () use (&$modules) { // TODO optimization; client-side caching @@ -65,101 +72,15 @@ class CRM_Core_Page_Angular extends CRM_Core_Page { $this->res->addScriptFile('civicrm', 'bower_components/angular/angular.min.js', 100, 'html-header', FALSE); $this->res->addScriptFile('civicrm', 'bower_components/angular-route/angular-route.min.js', 110, 'html-header', FALSE); $headOffset = 0; - foreach ($modules as $module) { - if (!empty($module['css'])) { - foreach ($module['css'] as $file) { - $this->res->addStyleFile($module['ext'], $file, self::DEFAULT_MODULE_WEIGHT + (++$headOffset), 'html-header', TRUE); - } - } - if (!empty($module['js'])) { - foreach ($module['js'] as $file) { - $this->res->addScriptFile($module['ext'], $file, self::DEFAULT_MODULE_WEIGHT + (++$headOffset), 'html-header', TRUE); - } + foreach ($modules as $moduleName => $module) { + foreach ($this->angular->getStyleUrls($moduleName) as $url) { + $this->res->addStyleUrl($url, self::DEFAULT_MODULE_WEIGHT + (++$headOffset), 'html-header'); } - } - } - - /** - * Get a list of AngularJS modules which should be autoloaded - * - * @return array - * (string $name => array('ext' => string $key, 'js' => array $paths, 'css' => array $paths)) - */ - public function getAngularModules() { - $angularModules = array(); - $angularModules['angularFileUpload'] = array( - 'ext' => 'civicrm', - 'js' => array('bower_components/angular-file-upload/angular-file-upload.min.js'), - ); - $angularModules['crmApp'] = array( - 'ext' => 'civicrm', - 'js' => array('js/angular-crmApp.js'), - ); - $angularModules['crmAttachment'] = array( - 'ext' => 'civicrm', - 'js' => array('js/angular-crmAttachment.js'), - 'css' => array('css/angular-crmAttachment.css'), - 'partials' => array('partials/crmAttachment/*.html'), - ); - $angularModules['crmUi'] = array( - 'ext' => 'civicrm', - 'js' => array('js/angular-crm-ui.js', 'packages/ckeditor/ckeditor.js'), - 'partials' => array('partials/crmUi/*.html'), - ); - $angularModules['crmUtil'] = array( - 'ext' => 'civicrm', - 'js' => array('js/angular-crm-util.js'), - ); - // https://github.com/jwstadler/angular-jquery-dialog-service - $angularModules['dialogService'] = array( - 'ext' => 'civicrm', - 'js' => array('bower_components/angular-jquery-dialog-service/dialog-service.js'), - ); - $angularModules['ngSanitize'] = array( - 'ext' => 'civicrm', - 'js' => array('js/angular-sanitize.js'), - ); - $angularModules['ui.utils'] = array( - 'ext' => 'civicrm', - 'js' => array('bower_components/angular-ui-utils/ui-utils.min.js'), - ); - $angularModules['ui.sortable'] = array( - 'ext' => 'civicrm', - 'js' => array('bower_components/angular-ui-sortable/sortable.min.js'), - ); - $angularModules['unsavedChanges'] = array( - 'ext' => 'civicrm', - 'js' => array('bower_components/angular-unsavedChanges/dist/unsavedChanges.min.js'), - ); - - foreach (CRM_Core_Component::getEnabledComponents() as $component) { - $angularModules = array_merge($angularModules, $component->getAngularModules()); - } - CRM_Utils_Hook::angularModules($angularModules); - $angularModules = $this->resolvePatterns($angularModules); - return $angularModules; - } - - /** - * @param array $modules - * List of Angular modules. - * @return array - * Updated list of Angular modules - */ - public function resolvePatterns($modules) { - $newModules = array(); - - foreach ($modules as $moduleKey => $module) { - foreach (array('js', 'css', 'partials') as $fileset) { - if (!isset($module[$fileset])) { - continue; - } - $module[$fileset] = $this->res->glob($module['ext'], $module[$fileset]); + foreach ($this->angular->getScriptUrls($moduleName) as $url) { + $this->res->addScriptUrl($url, self::DEFAULT_MODULE_WEIGHT + (++$headOffset), 'html-header'); + // addScriptUrl() bypasses the normal string-localization of addScriptFile(), + // but that's OK because all Angular strings (JS+HTML) will load via crmResource. } - $newModules[$moduleKey] = $module; } - - return $newModules; } - } diff --git a/Civi/Angular/Manager.php b/Civi/Angular/Manager.php new file mode 100644 index 0000000000..852a0e685a --- /dev/null +++ b/Civi/Angular/Manager.php @@ -0,0 +1,256 @@ +res = $res; + } + + /** + * Get a list of AngularJS modules which should be autoloaded + * + * @return array + * (string $name => array('ext' => string $key, 'js' => array $paths, 'css' => array $paths)) + */ + public function getModules() { + if ($this->modules === NULL) { + + $angularModules = array(); + $angularModules['angularFileUpload'] = array( + 'ext' => 'civicrm', + 'js' => array('bower_components/angular-file-upload/angular-file-upload.min.js'), + ); + $angularModules['crmApp'] = array( + 'ext' => 'civicrm', + 'js' => array('js/angular-crmApp.js'), + ); + $angularModules['crmAttachment'] = array( + 'ext' => 'civicrm', + 'js' => array('js/angular-crmAttachment.js'), + 'css' => array('css/angular-crmAttachment.css'), + 'partials' => array('partials/crmAttachment/*.html'), + ); + $angularModules['crmUi'] = array( + 'ext' => 'civicrm', + 'js' => array('js/angular-crm-ui.js', 'packages/ckeditor/ckeditor.js'), + 'partials' => array('partials/crmUi/*.html'), + ); + $angularModules['crmUtil'] = array( + 'ext' => 'civicrm', + 'js' => array('js/angular-crm-util.js'), + ); + // https://github.com/jwstadler/angular-jquery-dialog-service + $angularModules['dialogService'] = array( + 'ext' => 'civicrm', + 'js' => array('bower_components/angular-jquery-dialog-service/dialog-service.js'), + ); + $angularModules['ngSanitize'] = array( + 'ext' => 'civicrm', + 'js' => array('js/angular-sanitize.js'), + ); + $angularModules['ui.utils'] = array( + 'ext' => 'civicrm', + 'js' => array('bower_components/angular-ui-utils/ui-utils.min.js'), + ); + $angularModules['ui.sortable'] = array( + 'ext' => 'civicrm', + 'js' => array('bower_components/angular-ui-sortable/sortable.min.js'), + ); + $angularModules['unsavedChanges'] = array( + 'ext' => 'civicrm', + 'js' => array('bower_components/angular-unsavedChanges/dist/unsavedChanges.min.js'), + ); + + foreach (\CRM_Core_Component::getEnabledComponents() as $component) { + $angularModules = array_merge($angularModules, $component->getAngularModules()); + } + \CRM_Utils_Hook::angularModules($angularModules); + $this->modules = $this->resolvePatterns($angularModules); + } + + return $this->modules; + } + + /** + * Get the descriptor for an Angular module. + * + * @param string $name + * Module name. + * @return array + * Details about the module: + * - ext: string, the name of the Civi extension which defines the module + * - js: array(string $relativeFilePath). + * - css: array(string $relativeFilePath). + * - partials: array(string $relativeFilePath). + * @throws \Exception + */ + public function getModule($name) { + $modules = $this->getModules(); + if (!isset($modules[$name])) { + throw new \Exception("Unrecognized Angular module"); + } + return $modules[$name]; + } + + /** + * Convert any globs in an Angular module to file names. + * + * @param array $modules + * List of Angular modules. + * @return array + * Updated list of Angular modules + */ + protected function resolvePatterns($modules) { + $newModules = array(); + + foreach ($modules as $moduleKey => $module) { + foreach (array('js', 'css', 'partials') as $fileset) { + if (!isset($module[$fileset])) { + continue; + } + $module[$fileset] = $this->res->glob($module['ext'], $module[$fileset]); + } + $newModules[$moduleKey] = $module; + } + + return $newModules; + } + + /** + * Get the partial HTML documents for a module. + * + * @param string $name + * Angular module name. + * @return array + * Array(string $extFilePath => string $html) + */ + 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)); + } + } + return $result; + } + + + /** + * Get list of translated strings for a module. + * + * @param string $name + * Angular module name. + * @return array + * Translated strings: array(string $orig => string $translated). + */ + public function getTranslatedStrings($name) { + $result = array(); + $strings = $this->getStrings($name); + foreach ($strings as $string) { + // TODO: should we pass translation domain based on $module[ext] or $module[tsDomain]? + // It doesn't look like client side really supports the domain right now... + $translated = ts($string); + if ($translated != $string) { + $result[$string] = $translated; + } + } + return $result; + } + + /** + * Get list of translatable strings for a module. + * + * @param string $name + * Angular module name. + * @return array + * Translatable strings. + */ + public function getStrings($name) { + $module = $this->getModule($name); + $result = array(); + if (isset($module['js'])) { + foreach ($module['js'] as $file) { + $strings = $this->res->getStrings()->get( + $module['ext'], + $this->res->getPath($module['ext'], $file), + 'text/javascript' + ); + $result = array_unique(array_merge($result, $strings)); + } + } + 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)); + } + } + return $result; + } + + /** + * @param string $name + * Module name. + * @return array + * List of URLs. + * @throws \Exception + */ + public function getScriptUrls($name) { + $module = $this->getModule($name); + $result = array(); + if (isset($module['js'])) { + foreach ($module['js'] as $file) { + $result[] = $this->res->getUrl($module['ext'], $file, TRUE); + } + } + return $result; + } + + /** + * @param string $name + * Module name. + * @return array + * List of URLs. + * @throws \Exception + */ + public function getStyleUrls($name) { + $module = $this->getModule($name); + $result = array(); + if (isset($module['css'])) { + foreach ($module['css'] as $file) { + $result[] = $this->res->getUrl($module['ext'], $file, TRUE); + } + } + return $result; + } +} diff --git a/Civi/Core/Container.php b/Civi/Core/Container.php index fa56a554c7..4fc052b79b 100644 --- a/Civi/Core/Container.php +++ b/Civi/Core/Container.php @@ -66,6 +66,12 @@ class Container { // } // } + $container->setDefinition('angular', new Definition( + '\Civi\Angular\Manager', + array() + )) + ->setFactoryService(self::SELF)->setFactoryMethod('createAngularManager'); + $container->setDefinition('dispatcher', new Definition( '\Symfony\Component\EventDispatcher\EventDispatcher', array() @@ -86,6 +92,13 @@ class Container { return $container; } + /** + * @return \Civi\Angular\Manager + */ + public function createAngularManager() { + return new \Civi\Angular\Manager(\CRM_Core_Resources::singleton()); + } + /** * @return \Symfony\Component\EventDispatcher\EventDispatcher */ diff --git a/tests/phpunit/CRM/Core/Page/AngularTest.php b/tests/phpunit/CRM/Core/Page/AngularTest.php deleted file mode 100644 index f0b88f9b85..0000000000 --- a/tests/phpunit/CRM/Core/Page/AngularTest.php +++ /dev/null @@ -1,54 +0,0 @@ -useTransaction(TRUE); - parent::setUp(); - } - - /** - * Ensure that valid partials appear on the example module (crmUi). - */ - public function testPartialPattern() { - $this->createLoggedInUser(); - $page = new CRM_Core_Page_Angular(); - $angularModules = $page->getAngularModules(); - $matches = preg_grep(':/tabset.html$:', $angularModules['crmUi']['partials']); - $this->assertTrue(count($matches) > 0, - 'Expect to find example tabset.html. If it has been reorganized, then update this test with a different example.'); - } -} diff --git a/tests/phpunit/Civi/Angular/ManagerTest.php b/tests/phpunit/Civi/Angular/ManagerTest.php new file mode 100644 index 0000000000..28ce0d5619 --- /dev/null +++ b/tests/phpunit/Civi/Angular/ManagerTest.php @@ -0,0 +1,118 @@ +useTransaction(TRUE); + parent::setUp(); + $this->createLoggedInUser(); + $this->res = \CRM_Core_Resources::singleton(); + $this->angular = new Manager($this->res); + } + + /** + * Modules appear to be well-defined. + */ + public function testGetModules() { + $modules = $this->angular->getModules(); + + $counts = array( + 'js' => 0, + 'css' => 0, + 'partials' => 0, + ); + + foreach ($modules as $module) { + $this->assertTrue(is_array($module)); + $this->assertTrue(is_string($module['ext'])); + if (isset($module['js'])) { + $this->assertTrue(is_array($module['js'])); + foreach ($module['js'] as $file) { + $this->assertTrue(file_exists($this->res->getPath($module['ext'], $file))); + $counts['js']++; + } + } + if (isset($module['css'])) { + $this->assertTrue(is_array($module['css'])); + foreach ($module['css'] as $file) { + $this->assertTrue(file_exists($this->res->getPath($module['ext'], $file))); + $counts['css']++; + } + } + 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))); + $counts['partials']++; + } + } + } + + $this->assertTrue($counts['js'] > 0, 'Expect to find at least one JS file'); + $this->assertTrue($counts['css'] > 0, 'Expect to find at least one CSS file'); + $this->assertTrue($counts['partials'] > 0, 'Expect to find at least one partial HTML file'); + } + + /** + * Get HTML fragments from an example module. + */ + public function testGetPartials() { + $partials = $this->angular->getPartials('crmMailing'); + $this->assertRegExp('/\angular->getStrings('crmMailing'); + $this->assertTrue(in_array('Save Draft', $strings)); + // If crmMailing changes, feel free to use a different example. + } +} -- 2.25.1