From 026c608f427269743725eb5820297cf0489facb1 Mon Sep 17 00:00:00 2001 From: Juan Ferrer Toribio Date: Fri, 1 Nov 2019 13:02:47 +0100 Subject: [PATCH] Faster module loading & vnAgencyCalendar bug fixes --- front/core/components/dialog/index.spec.js | 2 +- front/core/lib/modified.js | 15 ++- front/core/lib/module-loader.js | 107 ++++++++++++--------- front/core/lib/specs/module-loader.spec.js | 102 +++++++++++++++++--- front/core/services/modules.js | 2 + front/salix/routes.js | 5 +- modules/agency/front/calendar/index.js | 2 +- 7 files changed, 165 insertions(+), 70 deletions(-) diff --git a/front/core/components/dialog/index.spec.js b/front/core/components/dialog/index.spec.js index 8a0b9118e7..a898261fb9 100644 --- a/front/core/components/dialog/index.spec.js +++ b/front/core/components/dialog/index.spec.js @@ -94,7 +94,7 @@ describe('Component vnDialog', () => { expect(controller.onAccept).toHaveBeenCalledWith({$response: 'accept'}); }); - it(`should resolve the promise returned by show() with response`, () => { + it(`should resolve the promise returned by show() with response when hidden`, () => { let response; controller.show().then(res => response = res); controller.respond('response'); diff --git a/front/core/lib/modified.js b/front/core/lib/modified.js index 46e61b4f5d..620dc75b37 100644 --- a/front/core/lib/modified.js +++ b/front/core/lib/modified.js @@ -1,22 +1,21 @@ import isEqual from './equals'; export default function getModifiedData(object, objectOld) { - var newObject = {}; + let newObject = {}; if (objectOld === null) return object; - for (var k in object) { - var val = object[k]; - var valOld = objectOld[k] === undefined ? null : objectOld[k]; + for (let k in object) { + let val = object[k]; + let valOld = objectOld[k] === undefined ? null : objectOld[k]; if (!isEqual(val, valOld)) { - if (val instanceof Date) { + if (val instanceof Date) newObject[k] = new Date(val.getTime()); - } else if (val instanceof Object) { + else if (val instanceof Object) newObject[k] = getModifiedData(val, valOld); - } else { + else newObject[k] = val; - } } } diff --git a/front/core/lib/module-loader.js b/front/core/lib/module-loader.js index 477b1c943a..18f0a3ea3b 100644 --- a/front/core/lib/module-loader.js +++ b/front/core/lib/module-loader.js @@ -3,64 +3,80 @@ import moduleImport from 'module-import'; factory.$inject = ['$http', '$window', '$ocLazyLoad', '$translatePartialLoader', '$translate', '$q']; export function factory($http, $window, $ocLazyLoad, $translatePartialLoader, $translate, $q) { + /** + * Used to load application modules lazily. + */ class ModuleLoader { constructor() { - this._loaded = {}; + this.loaded = {}; + this.imports = {}; + this.moduleImport = moduleImport; + this.modelInfo = $http.get(`modelInfo`) + .then(json => { + this.onModelInfoReady(json); + this.modelInfo = true; + }); } - load(moduleName, validations) { - let moduleConf = $window.routes.find(i => i && i.module == moduleName); + + /** + * Loads the passed module and it's dependencies. Loading a module + * implies load the webpack chunk, translations, recursively load + * module dependencies and finally register all of them into Angular. + * + * @param {String} mod The module name to load + * @return {Promise} Will be resolved when loaded, when module is + * already loaded it returns a resolved promise + */ + load(mod) { + let mods = []; + return this.loadRec(mod, mods); + } + + loadRec(mod, mods) { + let loaded = this.loaded[mod]; + + if (loaded === true || mods.indexOf(mod) != -1) + return $q.resolve(true); + if (loaded instanceof $q) + return loaded; + + let moduleConf = $window.routes.find(i => i && i.module == mod); if (!moduleConf) - return $q.reject(new Error(`Module not found: ${moduleName}`)); + return $q.reject(new Error(`Module not found: ${mod}`)); - let loaded = this._loaded; + let promises = []; - if (loaded[moduleName] === true) - return Promise.resolve(true); - if (loaded[moduleName] instanceof Promise) - return loaded[moduleName]; - if (loaded[moduleName] === false) - return Promise.resolve(true); + if (this.modelInfo instanceof $q) + promises.push(this.modelInfo); - loaded[moduleName] = false; + $translatePartialLoader.addPart(mod); + promises.push($translate.refresh()); + + let modImport = this.imports[mod]; + + if (!modImport) { + modImport = this.imports[mod] = this.moduleImport(mod) + .then(() => this.imports[mod] = true); + } + if (modImport && modImport.then) + promises.push(modImport); - let depPromises = []; let deps = moduleConf.dependencies; if (deps) { + mods.push(mod); for (let dep of deps) - depPromises.push(this.load(dep, validations)); + promises.push(this.loadRec(dep, mods)); + mods.pop(); } - loaded[moduleName] = new Promise((resolve, reject) => { - Promise.all(depPromises).then(() => { - let promises = []; - - $translatePartialLoader.addPart(moduleName); - promises.push(new Promise(resolve => { - $translate.refresh().then(resolve, resolve); - })); - - if (validations) { - promises.push(new Promise(resolve => { - $http.get(`/${moduleName}/api/modelInfo`).then( - json => this.onValidationsReady(json, resolve), - () => resolve() - ); - })); - } - - promises.push(moduleImport(moduleName)); - - Promise.all(promises).then(() => { - loaded[moduleName] = true; - resolve($ocLazyLoad.load({name: moduleName})); - }).catch(reject); - }).catch(reject); - }); - - return loaded[moduleName]; + this.loaded[mod] = $q.all(promises) + .then(() => $ocLazyLoad.load({name: mod})) + .then(() => this.loaded[mod] = true); + return this.loaded[mod]; } - onValidationsReady(json, resolve) { + + onModelInfoReady(json) { let entities = json.data; for (let entity in entities) { let fields = entities[entity].validations; @@ -72,12 +88,13 @@ export function factory($http, $window, $ocLazyLoad, $translatePartialLoader, $t } Object.assign($window.validations, json.data); - resolve(); } + parseValidation(val) { switch (val.validation) { case 'custom': - // TODO: Replace eval + // TODO: Don't use eval() because it's "evil". + // How to do the same without eval? val.bindedFunction = eval(`(${val.bindedFunction})`); break; case 'format': diff --git a/front/core/lib/specs/module-loader.spec.js b/front/core/lib/specs/module-loader.spec.js index 4a02c1bccf..2f756f7be8 100644 --- a/front/core/lib/specs/module-loader.spec.js +++ b/front/core/lib/specs/module-loader.spec.js @@ -1,30 +1,108 @@ describe('factory vnModuleLoader', () => { let vnModuleLoader; + let $rootScope; + let $window; beforeEach(ngModule('vnCore')); - beforeEach(angular.mock.inject((_vnModuleLoader_, $rootScope, $window) => { + beforeEach(angular.mock.inject((_vnModuleLoader_, _$rootScope_, $httpBackend, _$window_, $q) => { vnModuleLoader = _vnModuleLoader_; - $window.routes = [{module: 'myModule'}]; + $rootScope = _$rootScope_; + $window = _$window_; + + $window.validations = {}; + $window.routes = [ + { + module: 'myModule', + dependencies: ['fooModule', 'barModule'] + }, { + module: 'fooModule', + dependencies: ['myModule'] + }, { + module: 'barModule' + } + ]; + + $httpBackend.whenGET('modelInfo') + .respond({ + FooModel: { + properties: { + id: {type: 'Number'}, + email: {type: 'String'}, + field: {type: 'Boolean'} + }, + validations: { + id: [{ + validation: 'presence' + }], + email: [{ + validation: 'format', + with: '/@/' + }], + field: [{ + validation: 'custom', + bindedFunction: '() => true' + }] + } + } + }); + $httpBackend.flush(); + + vnModuleLoader.moduleImport = () => $q.resolve(); })); describe('load()', () => { - it('should return truthy promise if the module was loaded', async() => { - vnModuleLoader._loaded.myModule = true; + it('should throw error if module does not exist', async() => { + let errorThrown; - let result = await vnModuleLoader.load('myModule', {myValidations: () => {}}); + vnModuleLoader.load('unexistentModule') + .catch(() => errorThrown = true); + $rootScope.$apply(); - expect(result).toEqual(true); + expect(errorThrown).toBeTruthy(); }); - it('should return a promise if the module was still a promise', () => { - vnModuleLoader._loaded.myModule = new Promise(() => { - return 'I promise you a module!'; - }); + it('should set module loaded to true when it is loaded', async() => { + vnModuleLoader.load('barModule'); + $rootScope.$apply(); - let result = vnModuleLoader.load('myModule', {myValidations: () => {}}); + expect(vnModuleLoader.loaded['barModule']).toBeTruthy(); + }); - expect(result).toEqual(jasmine.any(Promise)); + it('should resolve returned promise when module is loaded', async() => { + let loaded; + + vnModuleLoader.load('barModule') + .then(() => loaded = true); + $rootScope.$apply(); + + expect(loaded).toBeTruthy(); + }); + + it('should load dependencies', async() => { + vnModuleLoader.load('fooModule'); + $rootScope.$apply(); + + expect(vnModuleLoader.loaded['barModule']).toBeTruthy(); + }); + + it('should work with circular dependencies', async() => { + vnModuleLoader.load('myModule'); + $rootScope.$apply(); + + expect(vnModuleLoader.loaded['fooModule']).toBeTruthy(); + }); + + it('should load models information and parse validations', async() => { + vnModuleLoader.load('barModule'); + + let FooModel = $window.validations.FooModel; + let validations = FooModel && FooModel.validations; + + expect(FooModel).toBeDefined(); + expect(validations).toBeDefined(); + expect(validations.email[0].with).toBeInstanceOf(RegExp); + expect(validations.field[0].bindedFunction).toBeInstanceOf(Function); }); }); }); diff --git a/front/core/services/modules.js b/front/core/services/modules.js index 2c1862fa80..1021bc4fab 100644 --- a/front/core/services/modules.js +++ b/front/core/services/modules.js @@ -8,9 +8,11 @@ export default class Modules { $window }); } + reset() { this.modules = null; } + get() { if (this.modules) return this.modules; diff --git a/front/salix/routes.js b/front/salix/routes.js index a31c116518..48a92795e6 100644 --- a/front/salix/routes.js +++ b/front/salix/routes.js @@ -1,10 +1,10 @@ import ngModule from './module'; import getMainRoute from 'core/lib/get-main-route'; -function loader(moduleName, validations) { +function loader(moduleName) { load.$inject = ['vnModuleLoader']; function load(moduleLoader) { - return moduleLoader.load(moduleName, validations); + return moduleLoader.load(moduleName); } return load; } @@ -31,7 +31,6 @@ function config($stateProvider, $urlRouterProvider) { if (!route.params) return params; - Object.keys(route.params).forEach(key => { temporalParams.push(`${key} = "${route.params[key]}"`); }); diff --git a/modules/agency/front/calendar/index.js b/modules/agency/front/calendar/index.js index a4772e8318..f324bb122d 100644 --- a/modules/agency/front/calendar/index.js +++ b/modules/agency/front/calendar/index.js @@ -91,7 +91,7 @@ class Controller extends Component { this.days = {}; let day = new Date(this.firstDay.getTime()); - while (day < this.lastDay) { + while (day <= this.lastDay) { let stamp = day.getTime(); let wday = day.getDay(); let dayEvents = [];