Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CRM/Core/Page.php
Original file line number Diff line number Diff line change
Expand Up @@ -470,7 +470,7 @@ public function invalidKey() {
* For ajax-loaded pages, this returns scripts, styles and settings as structured data
* rather than as markup. Those resources are handled clientside by civi.crmSnippet.refresh().
*/
private function addAjaxResources() {
protected function addAjaxResources() {
$ajaxRegion = CRM_Core_Region::instance('ajax-snippet');
// Ensure all resources are added to the region before processing it
$ajaxRegion->getFinalItems();
Expand Down
86 changes: 14 additions & 72 deletions Civi/Angular/Page/Modules.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,65 +5,24 @@
use Civi\Angular\Manager;

/**
* This page aggregates data from Angular modules.
*
* Example: Aggregate metadata about all modules in JSON format.
* civicrm/ajax/angular-modules?format=json
*
* Example: Aggregate metadata for crmUi and crmUtil modules.
* civicrm/ajax/angular-modules?format=json&modules=crmUi,crmUtil
*
* Example: Aggregate *.js files for all modules.
* civicrm/ajax/angular-modules?format=js
*
* Example: Aggregate *.css files for all modules.
* civicrm/ajax/angular-modules?format=css
* Page callback to load Angular modules.
*/
class Modules extends \CRM_Core_Page {

/**
* Generate asset content (when accessed via older, custom
* "civicrm/ajax/anulgar-modules" route).
* Ajax callback used for on-demand loading of Angular modules.
*
* @deprecated
*
* @throws \CRM_Core_Exception
* e.g. civicrm/ajax/angular-modules?modules=crmSearchDisplayList
*/
public function run() {
/**
* @var \Civi\Angular\Manager $angular
*/
$angular = \Civi::service('angular');
$moduleNames = $this->parseModuleNames(\CRM_Utils_Request::retrieve('modules', 'String'), $angular);

switch (\CRM_Utils_Request::retrieve('format', 'String')) {
case 'json':
case '':
$this->send(
'application/javascript',
json_encode($this->getMetadata($moduleNames, $angular))
);
break;
$moduleNames = $this->parseModuleNames(\CRM_Utils_Request::retrieve('modules', 'String'));

case 'js':
$this->send(
'application/javascript',
$this->digestJs($angular->getResources($moduleNames, 'js', 'path'))
);
break;

case 'css':
$this->send(
'text/css',
\CRM_Utils_File::concat($angular->getResources($moduleNames, 'css', 'path'), "\n")
);
break;

default:
throw new \CRM_Core_Exception("Unrecognized format");
if ($moduleNames) {
$loader = \Civi::service('angularjs.loader');
$loader->addModules($moduleNames);
$this->addAjaxResources();
}

\CRM_Utils_System::civiExit();
\CRM_Core_Page_AJAX::returnJsonResponse($this->ajaxResponse);
}

/**
Expand All @@ -79,13 +38,13 @@ public static function buildAngularModules($event) {

switch ($event->asset) {
case 'angular-modules.json':
$moduleNames = $page->parseModuleNames($event->params['modules'] ?? NULL, $angular);
$moduleNames = $page->parseModuleNames($event->params['modules'] ?? NULL);
$event->mimeType = 'application/json';
$event->content = json_encode($page->getMetadata($moduleNames, $angular));
break;

case 'angular-modules.js':
$moduleNames = $page->parseModuleNames($event->params['modules'] ?? NULL, $angular);
$moduleNames = $page->parseModuleNames($event->params['modules'] ?? NULL);
$event->mimeType = 'application/javascript';
$files = array_merge(
// FIXME: The `resetLocationProviderHashPrefix.js` has to stay in sync with `\Civi\Angular\AngularLoader::load()`.
Expand All @@ -96,7 +55,7 @@ public static function buildAngularModules($event) {
break;

case 'angular-modules.css':
$moduleNames = $page->parseModuleNames($event->params['modules'] ?? NULL, $angular);
$moduleNames = $page->parseModuleNames($event->params['modules'] ?? NULL);
$event->mimeType = 'text/css';
$event->content = \CRM_Utils_File::concat($angular->getResources($moduleNames, 'css', 'path'), "\n");

Expand Down Expand Up @@ -132,11 +91,10 @@ public function digestJs($files) {
/**
* @param string $modulesExpr
* Comma-separated list of module names.
* @param \Civi\Angular\Manager $angular
* @return array
* Any well-formed module names. All if moduleExpr is blank.
*/
public function parseModuleNames($modulesExpr, $angular) {
public function parseModuleNames($modulesExpr): array {
if ($modulesExpr) {
$moduleNames = preg_grep(
'/^[a-zA-Z0-9\-_\.]+$/',
Expand All @@ -145,6 +103,7 @@ public function parseModuleNames($modulesExpr, $angular) {
return $moduleNames;
}
else {
$angular = \Civi::service('angular');
$moduleNames = array_keys($angular->getModules());
return $moduleNames;
}
Expand All @@ -171,21 +130,4 @@ public function getMetadata(array $moduleNames, Manager $angular): array {
return $result;
}

/**
* Send a response.
*
* @param string $type
* Content type.
* @param string $data
* Content.
*/
public function send($type, $data) {
// Encourage browsers to cache for a long time - 1 year
$ttl = 60 * 60 * 24 * 364;
\CRM_Utils_System::setHttpHeader('Expires', gmdate('D, d M Y H:i:s \G\M\T', time() + $ttl));
\CRM_Utils_System::setHttpHeader("Content-Type", $type);
\CRM_Utils_System::setHttpHeader("Cache-Control", "max-age=$ttl, public");
echo $data;
}

}
63 changes: 63 additions & 0 deletions js/angular-crmResource/all.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,69 @@
};
});

const modulesPending = {};
angular.module('crmResource').factory('crmResourceLoader', function ($q) {
return {
// Load Angular modules dynamically
loadModules: function (moduleNames) {
// Filter out modules that are already loaded.
const loadedModules = CRM.angular.modules || [];
const modulesToLoad = moduleNames.filter(name => !loadedModules.includes(name));

// Early return if all modules are already loaded.
if (modulesToLoad.length === 0) {
return $q.resolve();
}

// Collect promises for modules that are already pending
const pendingPromises = [];
const newModulesToLoad = [];

modulesToLoad.forEach(name => {
if (modulesPending[name]) {
pendingPromises.push(modulesPending[name]);
} else {
newModulesToLoad.push(name);
}
});

// If all requested modules are pending, return combined promise
if (newModulesToLoad.length === 0) {
return $q.all(pendingPromises);
}

// Create a new promise for the modules being loaded
const deferred = $q.defer();

// Track all new modules with the same promise
newModulesToLoad.forEach(name => {
modulesPending[name] = deferred.promise;
});

const snippet = $('<div></div>');
const settings = {
url: CRM.url('civicrm/ajax/angular-modules', {modules: newModulesToLoad.join(',')}),
};

$(snippet).crmSnippet(settings)
.on('crmLoad', function () {
// Clean up pending tracking and resolve
newModulesToLoad.forEach(name => delete modulesPending[name]);
deferred.resolve();
})
.on('crmLoadFail', function () {
// Clean up pending tracking and reject
newModulesToLoad.forEach(name => delete modulesPending[name]);
deferred.reject();
})
.crmSnippet('refresh');

// Return combined promise of new and pending modules
return $q.all([deferred.promise, ...pendingPromises]);
}
};
});

angular.module('crmResource').config(function($provide) {
$provide.decorator('$templateCache', function($delegate, $http, $q, crmResource) {
var origGet = $delegate.get;
Expand Down