pages loading twice poc

Cherry-picked from original branch started from Angular 13 PR
This commit is contained in:
Art Lowel
2022-04-26 11:15:42 +02:00
committed by Yura Bondarenko
parent e4f483c308
commit 3bc5ee0253
31 changed files with 15697 additions and 15452 deletions

2
.gitignore vendored
View File

@@ -36,3 +36,5 @@ package-lock.json
.env
/nbproject/
junit.xml

View File

@@ -142,7 +142,7 @@
"@types/node": "^14.14.9",
"axe-core": "^4.3.3",
"codelyzer": "^6.0.0",
"compression-webpack-plugin": "^3.0.1",
"compression-webpack-plugin": "^6.1.1",
"copy-webpack-plugin": "^6.4.1",
"cross-env": "^7.0.3",
"css-loader": "3.4.0",

View File

@@ -1,5 +1,5 @@
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { RouterModule, NoPreloading } from '@angular/router';
import { AuthBlockingGuard } from './core/auth/auth-blocking.guard';
import { AuthenticatedGuard } from './core/auth/authenticated.guard';
@@ -30,6 +30,7 @@ import { ThemedForbiddenComponent } from './forbidden/themed-forbidden.component
import { GroupAdministratorGuard } from './core/data/feature-authorization/feature-authorization-guard/group-administrator.guard';
import { ThemedPageInternalServerErrorComponent } from './page-internal-server-error/themed-page-internal-server-error.component';
import { ServerCheckGuard } from './core/server-check/server-check.guard';
import { MenuResolver } from './menu.resolver';
@NgModule({
imports: [
@@ -39,6 +40,7 @@ import { ServerCheckGuard } from './core/server-check/server-check.guard';
path: '',
canActivate: [AuthBlockingGuard],
canActivateChild: [ServerCheckGuard],
resolve: [MenuResolver],
children: [
{ path: '', redirectTo: '/home', pathMatch: 'full' },
{
@@ -217,6 +219,12 @@ import { ServerCheckGuard } from './core/server-check/server-check.guard';
]
}
], {
// enableTracing: true,
useHash: false,
scrollPositionRestoration: 'enabled',
anchorScrolling: 'enabled',
initialNavigation: 'enabledBlocking',
preloadingStrategy: NoPreloading,
onSameUrlNavigation: 'reload',
})
],

View File

@@ -72,7 +72,7 @@ export class AppComponent implements OnInit, AfterViewInit {
/**
* Whether or not the app is in the process of rerouting
*/
isRouteLoading$: BehaviorSubject<boolean> = new BehaviorSubject(true);
isRouteLoading$: BehaviorSubject<boolean> = new BehaviorSubject(false);
/**
* Whether or not the theme is in the process of being swapped
@@ -121,7 +121,7 @@ export class AppComponent implements OnInit, AfterViewInit {
this.themeService.getThemeName$().subscribe((themeName: string) => {
if (isPlatformBrowser(this.platformId)) {
// the theme css will never download server side, so this should only happen on the browser
this.isThemeCSSLoading$.next(true);
this.distinctNext(this.isThemeCSSLoading$, true);
}
if (hasValue(themeName)) {
this.loadGlobalThemeConfig(themeName);
@@ -200,8 +200,8 @@ export class AppComponent implements OnInit, AfterViewInit {
this.router.events.subscribe((event) => {
if (event instanceof NavigationStart) {
resolveEndFound = false;
this.isRouteLoading$.next(true);
this.isThemeLoading$.next(true);
this.distinctNext(this.isRouteLoading$, true);
this.distinctNext(this.isThemeLoading$, true);
} else if (event instanceof ResolveEnd) {
resolveEndFound = true;
const activatedRouteSnapShot: ActivatedRouteSnapshot = event.state.root;
@@ -214,16 +214,16 @@ export class AppComponent implements OnInit, AfterViewInit {
}
})
).subscribe((changed) => {
this.isThemeLoading$.next(changed);
this.distinctNext(this.isThemeLoading$, changed);
});
} else if (
event instanceof NavigationEnd ||
event instanceof NavigationCancel
) {
if (!resolveEndFound) {
this.isThemeLoading$.next(false);
this.distinctNext(this.isThemeLoading$, false);
}
this.isRouteLoading$.next(false);
this.distinctNext(this.isRouteLoading$, false);
}
});
}
@@ -281,7 +281,7 @@ export class AppComponent implements OnInit, AfterViewInit {
});
}
// the fact that this callback is used, proves we're on the browser.
this.isThemeCSSLoading$.next(false);
this.distinctNext(this.isThemeCSSLoading$, false);
};
head.appendChild(link);
}
@@ -376,4 +376,17 @@ export class AppComponent implements OnInit, AfterViewInit {
}
});
}
/**
* Use nextValue to update a given BehaviorSubject, only if it differs from its current value
*
* @param bs a BehaviorSubject
* @param nextValue the next value for that BehaviorSubject
* @protected
*/
protected distinctNext<T>(bs: BehaviorSubject<T>, nextValue: T): void {
if (bs.getValue() !== nextValue) {
bs.next(nextValue);
}
}
}

View File

@@ -16,10 +16,6 @@ import {
} from '@ng-dynamic-forms/core';
import { TranslateModule } from '@ngx-translate/core';
import { ScrollToModule } from '@nicky-lenaers/ngx-scroll-to';
import { AdminSidebarSectionComponent } from './admin/admin-sidebar/admin-sidebar-section/admin-sidebar-section.component';
import { AdminSidebarComponent } from './admin/admin-sidebar/admin-sidebar.component';
import { ExpandableAdminSidebarSectionComponent } from './admin/admin-sidebar/expandable-admin-sidebar-section/expandable-admin-sidebar-section.component';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { appEffects } from './app.effects';
@@ -28,36 +24,18 @@ import { appReducers, AppState, storeModuleConfig } from './app.reducer';
import { CheckAuthenticationTokenAction } from './core/auth/auth.actions';
import { CoreModule } from './core/core.module';
import { ClientCookieService } from './core/services/client-cookie.service';
import { FooterComponent } from './footer/footer.component';
import { HeaderNavbarWrapperComponent } from './header-nav-wrapper/header-navbar-wrapper.component';
import { HeaderComponent } from './header/header.component';
import { NavbarModule } from './navbar/navbar.module';
import { PageNotFoundComponent } from './pagenotfound/pagenotfound.component';
import { DSpaceRouterStateSerializer } from './shared/ngrx/dspace-router-state-serializer';
import { NotificationComponent } from './shared/notifications/notification/notification.component';
import { NotificationsBoardComponent } from './shared/notifications/notifications-board/notifications-board.component';
import { SharedModule } from './shared/shared.module';
import { BreadcrumbsComponent } from './breadcrumbs/breadcrumbs.component';
import { environment } from '../environments/environment';
import { ForbiddenComponent } from './forbidden/forbidden.component';
import { AuthInterceptor } from './core/auth/auth.interceptor';
import { LocaleInterceptor } from './core/locale/locale.interceptor';
import { XsrfInterceptor } from './core/xsrf/xsrf.interceptor';
import { LogInterceptor } from './core/log/log.interceptor';
import { RootComponent } from './root/root.component';
import { ThemedRootComponent } from './root/themed-root.component';
import { ThemedEntryComponentModule } from '../themes/themed-entry-component.module';
import { ThemedPageNotFoundComponent } from './pagenotfound/themed-pagenotfound.component';
import { ThemedForbiddenComponent } from './forbidden/themed-forbidden.component';
import { ThemedHeaderComponent } from './header/themed-header.component';
import { ThemedFooterComponent } from './footer/themed-footer.component';
import { ThemedBreadcrumbsComponent } from './breadcrumbs/themed-breadcrumbs.component';
import { ThemedHeaderNavbarWrapperComponent } from './header-nav-wrapper/themed-header-navbar-wrapper.component';
import { IdleModalComponent } from './shared/idle-modal/idle-modal.component';
import { ThemedPageInternalServerErrorComponent } from './page-internal-server-error/themed-page-internal-server-error.component';
import { PageInternalServerErrorComponent } from './page-internal-server-error/page-internal-server-error.component';
import { EagerThemesModule } from '../themes/eager-themes.module';
import { APP_CONFIG, AppConfig } from '../config/app-config.interface';
import { RootModule } from './root.module';
export function getConfig() {
return environment;
@@ -92,7 +70,8 @@ const IMPORTS = [
EffectsModule.forRoot(appEffects),
StoreModule.forRoot(appReducers, storeModuleConfig),
StoreRouterConnectingModule.forRoot(),
ThemedEntryComponentModule.withEntryComponents(),
EagerThemesModule,
RootModule,
];
IMPORTS.push(
@@ -164,28 +143,6 @@ const PROVIDERS = [
const DECLARATIONS = [
AppComponent,
RootComponent,
ThemedRootComponent,
HeaderComponent,
ThemedHeaderComponent,
HeaderNavbarWrapperComponent,
ThemedHeaderNavbarWrapperComponent,
AdminSidebarComponent,
AdminSidebarSectionComponent,
ExpandableAdminSidebarSectionComponent,
FooterComponent,
ThemedFooterComponent,
PageNotFoundComponent,
ThemedPageNotFoundComponent,
NotificationComponent,
NotificationsBoardComponent,
BreadcrumbsComponent,
ThemedBreadcrumbsComponent,
ForbiddenComponent,
ThemedForbiddenComponent,
IdleModalComponent,
ThemedPageInternalServerErrorComponent,
PageInternalServerErrorComponent
];
const EXPORTS = [

View File

@@ -92,11 +92,15 @@ export function authReducer(state: any = initialState, action: AuthActions): Aut
});
case AuthActionTypes.AUTHENTICATED:
return Object.assign({}, state, {
loading: true,
blocking: true
});
case AuthActionTypes.CHECK_AUTHENTICATION_TOKEN:
case AuthActionTypes.CHECK_AUTHENTICATION_TOKEN_COOKIE:
return Object.assign({}, state, {
loading: true,
blocking: true
});
case AuthActionTypes.AUTHENTICATED_ERROR:
@@ -210,7 +214,6 @@ export function authReducer(state: any = initialState, action: AuthActions): Aut
case AuthActionTypes.RETRIEVE_AUTH_METHODS:
return Object.assign({}, state, {
loading: true,
blocking: true
});
case AuthActionTypes.RETRIEVE_AUTH_METHODS_SUCCESS:

View File

@@ -75,7 +75,6 @@ import { RegistryService } from './registry/registry.service';
import { RoleService } from './roles/role.service';
import { FeedbackDataService } from './feedback/feedback-data.service';
import { ApiService } from './services/api.service';
import { ServerResponseService } from './services/server-response.service';
import { NativeWindowFactory, NativeWindowService } from './services/window.service';
import { BitstreamFormat } from './shared/bitstream-format.model';
@@ -186,7 +185,6 @@ const DECLARATIONS = [];
const EXPORTS = [];
const PROVIDERS = [
ApiService,
AuthenticatedGuard,
CommunityDataService,
CollectionDataService,

View File

@@ -1,24 +0,0 @@
import { throwError as observableThrowError } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
@Injectable()
export class ApiService {
constructor(public _http: HttpClient) {
}
/**
* whatever domain/feature method name
*/
get(url: string, options?: any) {
return this._http.get(url, options).pipe(
catchError((err) => {
console.log('Error: ', err);
return observableThrowError(err);
}));
}
}

View File

@@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';
import { MenuResolver } from './menu.resolver';
describe('MenuResolver', () => {
let resolver: MenuResolver;
beforeEach(() => {
TestBed.configureTestingModule({});
resolver = TestBed.inject(MenuResolver);
});
it('should be created', () => {
expect(resolver).toBeTruthy();
});
});

89
src/app/menu.resolver.ts Normal file
View File

@@ -0,0 +1,89 @@
import { Injectable } from '@angular/core';
import { Resolve, RouterStateSnapshot, ActivatedRouteSnapshot } from '@angular/router';
import { Observable } from 'rxjs';
import { MenuItemType, MenuID } from './shared/menu/initial-menus-state';
import { LinkMenuItemModel } from './shared/menu/menu-item/models/link.model';
import { getFirstCompletedRemoteData } from './core/shared/operators';
import { PaginatedList } from './core/data/paginated-list.model';
import { BrowseDefinition } from './core/shared/browse-definition.model';
import { RemoteData } from './core/data/remote-data';
import { TextMenuItemModel } from './shared/menu/menu-item/models/text.model';
import { BrowseService } from './core/browse/browse.service';
import { MenuService } from './shared/menu/menu.service';
import { MenuState } from './shared/menu/menu.reducer';
import { find, map } from 'rxjs/operators';
import { hasValue } from './shared/empty.util';
@Injectable({
providedIn: 'root'
})
export class MenuResolver implements Resolve<boolean> {
constructor(
protected menuService: MenuService,
public browseService: BrowseService,
) {
}
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
this.createPublicMenu();
return this.menuService.getMenu(MenuID.PUBLIC).pipe(
find((menu: MenuState) => hasValue(menu)),
map(() => true)
);
}
createPublicMenu() {
const menuList: any[] = [
/* Communities & Collections tree */
{
id: `browse_global_communities_and_collections`,
active: false,
visible: true,
index: 0,
model: {
type: MenuItemType.LINK,
text: `menu.section.browse_global_communities_and_collections`,
link: `/community-list`
} as LinkMenuItemModel
}
];
// Read the different Browse-By types from config and add them to the browse menu
this.browseService.getBrowseDefinitions()
.pipe(getFirstCompletedRemoteData<PaginatedList<BrowseDefinition>>())
.subscribe((browseDefListRD: RemoteData<PaginatedList<BrowseDefinition>>) => {
if (browseDefListRD.hasSucceeded) {
browseDefListRD.payload.page.forEach((browseDef: BrowseDefinition) => {
menuList.push({
id: `browse_global_by_${browseDef.id}`,
parentID: 'browse_global',
active: false,
visible: true,
model: {
type: MenuItemType.LINK,
text: `menu.section.browse_global_by_${browseDef.id}`,
link: `/browse/${browseDef.id}`
} as LinkMenuItemModel
});
});
menuList.push(
/* Browse */
{
id: 'browse_global',
active: false,
visible: true,
index: 1,
model: {
type: MenuItemType.TEXT,
text: 'menu.section.browse_global'
} as TextMenuItemModel,
}
);
}
menuList.forEach((menuSection) => this.menuService.addSection(MenuID.PUBLIC, Object.assign(menuSection, {
shouldPersistOnRouteChange: true
})));
});
}
}

View File

@@ -2,15 +2,9 @@ import { Component, Injector } from '@angular/core';
import { slideMobileNav } from '../shared/animations/slide';
import { MenuComponent } from '../shared/menu/menu.component';
import { MenuService } from '../shared/menu/menu.service';
import { MenuID, MenuItemType } from '../shared/menu/initial-menus-state';
import { TextMenuItemModel } from '../shared/menu/menu-item/models/text.model';
import { LinkMenuItemModel } from '../shared/menu/menu-item/models/link.model';
import { MenuID } from '../shared/menu/initial-menus-state';
import { HostWindowService } from '../shared/host-window.service';
import { BrowseService } from '../core/browse/browse.service';
import { getFirstCompletedRemoteData } from '../core/shared/operators';
import { PaginatedList } from '../core/data/paginated-list.model';
import { BrowseDefinition } from '../core/shared/browse-definition.model';
import { RemoteData } from '../core/data/remote-data';
import { ActivatedRoute } from '@angular/router';
import { AuthorizationDataService } from '../core/data/feature-authorization/authorization-data.service';
@@ -41,64 +35,6 @@ export class NavbarComponent extends MenuComponent {
}
ngOnInit(): void {
this.createMenu();
super.ngOnInit();
}
/**
* Initialize all menu sections and items for this menu
*/
createMenu() {
const menuList: any[] = [
/* Communities & Collections tree */
{
id: `browse_global_communities_and_collections`,
active: false,
visible: true,
index: 0,
model: {
type: MenuItemType.LINK,
text: `menu.section.browse_global_communities_and_collections`,
link: `/community-list`
} as LinkMenuItemModel
}
];
// Read the different Browse-By types from config and add them to the browse menu
this.browseService.getBrowseDefinitions()
.pipe(getFirstCompletedRemoteData<PaginatedList<BrowseDefinition>>())
.subscribe((browseDefListRD: RemoteData<PaginatedList<BrowseDefinition>>) => {
if (browseDefListRD.hasSucceeded) {
browseDefListRD.payload.page.forEach((browseDef: BrowseDefinition) => {
menuList.push({
id: `browse_global_by_${browseDef.id}`,
parentID: 'browse_global',
active: false,
visible: true,
model: {
type: MenuItemType.LINK,
text: `menu.section.browse_global_by_${browseDef.id}`,
link: `/browse/${browseDef.id}`
} as LinkMenuItemModel
});
});
menuList.push(
/* Browse */
{
id: 'browse_global',
active: false,
visible: true,
index: 1,
model: {
type: MenuItemType.TEXT,
text: 'menu.section.browse_global'
} as TextMenuItemModel,
}
);
}
menuList.forEach((menuSection) => this.menuService.addSection(this.menuID, Object.assign(menuSection, {
shouldPersistOnRouteChange: true
})));
});
}
}

98
src/app/root.module.ts Normal file
View File

@@ -0,0 +1,98 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import {
AdminSidebarSectionComponent
} from './admin/admin-sidebar/admin-sidebar-section/admin-sidebar-section.component';
import { AdminSidebarComponent } from './admin/admin-sidebar/admin-sidebar.component';
import {
ExpandableAdminSidebarSectionComponent
} from './admin/admin-sidebar/expandable-admin-sidebar-section/expandable-admin-sidebar-section.component';
import { FooterComponent } from './footer/footer.component';
import { HeaderNavbarWrapperComponent } from './header-nav-wrapper/header-navbar-wrapper.component';
import { HeaderComponent } from './header/header.component';
import { NavbarModule } from './navbar/navbar.module';
import { PageNotFoundComponent } from './pagenotfound/pagenotfound.component';
import { NotificationComponent } from './shared/notifications/notification/notification.component';
import {
NotificationsBoardComponent
} from './shared/notifications/notifications-board/notifications-board.component';
import { SharedModule } from './shared/shared.module';
import { BreadcrumbsComponent } from './breadcrumbs/breadcrumbs.component';
import { ForbiddenComponent } from './forbidden/forbidden.component';
import { RootComponent } from './root/root.component';
import { ThemedRootComponent } from './root/themed-root.component';
import { ThemedPageNotFoundComponent } from './pagenotfound/themed-pagenotfound.component';
import { ThemedForbiddenComponent } from './forbidden/themed-forbidden.component';
import { ThemedHeaderComponent } from './header/themed-header.component';
import { ThemedFooterComponent } from './footer/themed-footer.component';
import { ThemedBreadcrumbsComponent } from './breadcrumbs/themed-breadcrumbs.component';
import {
ThemedHeaderNavbarWrapperComponent
} from './header-nav-wrapper/themed-header-navbar-wrapper.component';
import { IdleModalComponent } from './shared/idle-modal/idle-modal.component';
import {
ThemedPageInternalServerErrorComponent
} from './page-internal-server-error/themed-page-internal-server-error.component';
import {
PageInternalServerErrorComponent
} from './page-internal-server-error/page-internal-server-error.component';
const IMPORTS = [
CommonModule,
SharedModule.withEntryComponents(),
NavbarModule,
NgbModule,
];
const PROVIDERS = [
];
const DECLARATIONS = [
RootComponent,
ThemedRootComponent,
HeaderComponent,
ThemedHeaderComponent,
HeaderNavbarWrapperComponent,
ThemedHeaderNavbarWrapperComponent,
AdminSidebarComponent,
AdminSidebarSectionComponent,
ExpandableAdminSidebarSectionComponent,
FooterComponent,
ThemedFooterComponent,
PageNotFoundComponent,
ThemedPageNotFoundComponent,
NotificationComponent,
NotificationsBoardComponent,
BreadcrumbsComponent,
ThemedBreadcrumbsComponent,
ForbiddenComponent,
ThemedForbiddenComponent,
IdleModalComponent,
ThemedPageInternalServerErrorComponent,
PageInternalServerErrorComponent
];
const EXPORTS = [
];
@NgModule({
imports: [
...IMPORTS
],
providers: [
...PROVIDERS
],
declarations: [
...DECLARATIONS,
],
exports: [
...EXPORTS,
...DECLARATIONS,
]
})
export class RootModule {
}

View File

@@ -37,7 +37,7 @@ const menuByIDSelector = (menuID: MenuID): MemoizedSelector<AppState, MenuState>
return keySelector<MenuState>(menuID, menusStateSelector);
};
const menuSectionStateSelector = (state: MenuState) => state.sections;
const menuSectionStateSelector = (state: MenuState) => hasValue(state) ? state.sections : {};
const menuSectionByIDSelector = (id: string): MemoizedSelector<MenuState, MenuSection> => {
return menuKeySelector<MenuSection>(id, menuSectionStateSelector);
@@ -164,7 +164,7 @@ export class MenuService {
*/
isMenuCollapsed(menuID: MenuID): Observable<boolean> {
return this.getMenu(menuID).pipe(
map((state: MenuState) => state.collapsed)
map((state: MenuState) => hasValue(state) ? state.collapsed : undefined)
);
}
@@ -175,7 +175,7 @@ export class MenuService {
*/
isMenuPreviewCollapsed(menuID: MenuID): Observable<boolean> {
return this.getMenu(menuID).pipe(
map((state: MenuState) => state.previewCollapsed)
map((state: MenuState) => hasValue(state) ? state.previewCollapsed : undefined)
);
}
@@ -186,7 +186,7 @@ export class MenuService {
*/
isMenuVisible(menuID: MenuID): Observable<boolean> {
return this.getMenu(menuID).pipe(
map((state: MenuState) => state.visible)
map((state: MenuState) => hasValue(state) ? state.visible : undefined)
);
}

View File

@@ -2,11 +2,10 @@ import { HttpClient, HttpClientModule } from '@angular/common/http';
import { APP_INITIALIZER, NgModule } from '@angular/core';
import { BrowserModule, makeStateKey, TransferState } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { RouterModule, NoPreloading } from '@angular/router';
import { REQUEST } from '@nguniversal/express-engine/tokens';
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
import { TranslateJson5HttpLoader } from '../../ngx-translate-loaders/translate-json5-http.loader';
import { TranslateBrowserLoader } from '../../ngx-translate-loaders/translate-browser.loader';
import { IdlePreloadModule } from 'angular-idle-preload';
@@ -42,8 +41,8 @@ import { environment } from '../../environments/environment';
export const REQ_KEY = makeStateKey<string>('req');
export function createTranslateLoader(http: HttpClient) {
return new TranslateJson5HttpLoader(http, 'assets/i18n/', '.json5');
export function createTranslateLoader(transferState: TransferState, http: HttpClient) {
return new TranslateBrowserLoader(transferState, http, 'assets/i18n/', '.json5');
}
export function getRequest(transferState: TransferState): any {
@@ -59,13 +58,6 @@ export function getRequest(transferState: TransferState): any {
HttpClientModule,
// forRoot ensures the providers are only created once
IdlePreloadModule.forRoot(),
RouterModule.forRoot([], {
// enableTracing: true,
useHash: false,
scrollPositionRestoration: 'enabled',
anchorScrolling: 'enabled',
preloadingStrategy: NoPreloading
}),
StatisticsModule.forRoot(),
Angulartics2RouterlessModule.forRoot(),
BrowserAnimationsModule,
@@ -74,7 +66,7 @@ export function getRequest(transferState: TransferState): any {
loader: {
provide: TranslateLoader,
useFactory: (createTranslateLoader),
deps: [HttpClient]
deps: [TransferState, HttpClient]
}
}),
AppModule
@@ -92,9 +84,11 @@ export function getRequest(transferState: TransferState): any {
// extend environment with app config for browser
extendEnvironmentWithAppConfig(environment, appConfig);
}
dspaceTransferState.transfer();
return () =>
dspaceTransferState.transfer().then((b: boolean) => {
correlationIdService.initCorrelationId();
return () => true;
return b;
});
},
deps: [TransferState, DSpaceTransferState, CorrelationIdService],
multi: true

View File

@@ -15,7 +15,7 @@ import { AppComponent } from '../../app/app.component';
import { AppModule } from '../../app/app.module';
import { DSpaceServerTransferStateModule } from '../transfer-state/dspace-server-transfer-state.module';
import { DSpaceTransferState } from '../transfer-state/dspace-transfer-state.service';
import { TranslateJson5UniversalLoader } from '../../ngx-translate-loaders/translate-json5-universal.loader';
import { TranslateServerLoader } from '../../ngx-translate-loaders/translate-server.loader';
import { CookieService } from '../../app/core/services/cookie.service';
import { ServerCookieService } from '../../app/core/services/server-cookie.service';
import { AuthService } from '../../app/core/auth/auth.service';
@@ -37,8 +37,8 @@ import { AppConfig, APP_CONFIG_STATE } from '../../config/app-config.interface';
import { environment } from '../../environments/environment';
export function createTranslateLoader() {
return new TranslateJson5UniversalLoader('dist/server/assets/i18n/', '.json5');
export function createTranslateLoader(transferState: TransferState) {
return new TranslateServerLoader(transferState, 'dist/server/assets/i18n/', '.json5');
}
@NgModule({
@@ -47,16 +47,13 @@ export function createTranslateLoader() {
BrowserModule.withServerTransition({
appId: 'dspace-angular'
}),
RouterModule.forRoot([], {
useHash: false
}),
NoopAnimationsModule,
DSpaceServerTransferStateModule,
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useFactory: (createTranslateLoader),
deps: []
deps: [TransferState]
}
}),
ServerModule,

View File

@@ -1,12 +1,19 @@
import { Injectable } from '@angular/core';
import { coreSelector } from 'src/app/core/core.selectors';
import { StoreAction, StoreActionTypes } from '../../app/store.actions';
import { DSpaceTransferState } from './dspace-transfer-state.service';
import { find, map } from 'rxjs/operators';
import { isNotEmpty } from '../../app/shared/empty.util';
@Injectable()
export class DSpaceBrowserTransferState extends DSpaceTransferState {
transfer() {
async transfer(): Promise<boolean> {
const state = this.transferState.get<any>(DSpaceTransferState.NGRX_STATE, null);
this.transferState.remove(DSpaceTransferState.NGRX_STATE);
this.store.dispatch(new StoreAction(StoreActionTypes.REHYDRATE, state));
return this.store.select(coreSelector).pipe(
find((core: any) => isNotEmpty(core)),
map(() => true)
).toPromise();
}
}

View File

@@ -5,7 +5,7 @@ import { DSpaceTransferState } from './dspace-transfer-state.service';
@Injectable()
export class DSpaceServerTransferState extends DSpaceTransferState {
transfer() {
transfer(): Promise<boolean> {
this.transferState.onSerialize(DSpaceTransferState.NGRX_STATE, () => {
let state;
this.store.pipe(take(1)).subscribe((saveState: any) => {
@@ -14,5 +14,7 @@ export class DSpaceServerTransferState extends DSpaceTransferState {
return state;
});
return new Promise<boolean>(() => true);
}
}

View File

@@ -14,5 +14,5 @@ export abstract class DSpaceTransferState {
) {
}
abstract transfer(): void;
abstract transfer(): Promise<boolean>;
}

View File

@@ -0,0 +1,15 @@
import { makeStateKey } from '@angular/platform-browser';
/**
* Represents ngx-translate messages in different languages in the TransferState
*/
export class NgxTranslateState {
[lang: string]: {
[key: string]: string
}
}
/**
* The key to store the NgxTranslateState as part of the TransferState
*/
export const NGX_TRANSLATE_STATE = makeStateKey<NgxTranslateState>('NGX_TRANSLATE_STATE');

View File

@@ -0,0 +1,44 @@
import { TranslateLoader } from '@ngx-translate/core';
import { HttpClient } from '@angular/common/http';
import { TransferState } from '@angular/platform-browser';
import { NGX_TRANSLATE_STATE, NgxTranslateState } from './ngx-translate-state';
import { hasValue } from '../app/shared/empty.util';
import { map } from 'rxjs/operators';
import { of as observableOf, Observable } from 'rxjs';
import * as JSON5 from 'json5';
/**
* A TranslateLoader for ngx-translate to retrieve i18n messages from the TransferState, or download
* them if they're not available there
*/
export class TranslateBrowserLoader implements TranslateLoader {
constructor(
protected transferState: TransferState,
protected http: HttpClient,
protected prefix?: string,
protected suffix?: string
) {
}
/**
* Return the i18n messages for a given language, first try to find them in the TransferState
* retrieve them using HttpClient if they're not available there
*
* @param lang the language code
*/
getTranslation(lang: string): Observable<any> {
// Get the ngx-translate messages from the transfer state, to speed up the initial page load
// client side
const state = this.transferState.get<NgxTranslateState>(NGX_TRANSLATE_STATE, {});
const messages = state[lang];
if (hasValue(messages)) {
return observableOf(messages);
} else {
// If they're not available on the transfer state (e.g. when running in dev mode), retrieve
// them using HttpClient
return this.http.get('' + this.prefix + lang + this.suffix, { responseType: 'text' }).pipe(
map((json: any) => JSON5.parse(json))
);
}
}
}

View File

@@ -1,15 +0,0 @@
import { HttpClient } from '@angular/common/http';
import { TranslateLoader } from '@ngx-translate/core';
import { map } from 'rxjs/operators';
import * as JSON5 from 'json5';
export class TranslateJson5HttpLoader implements TranslateLoader {
constructor(private http: HttpClient, public prefix?: string, public suffix?: string) {
}
getTranslation(lang: string): any {
return this.http.get('' + this.prefix + lang + this.suffix, {responseType: 'text'}).pipe(
map((json: any) => JSON5.parse(json))
);
}
}

View File

@@ -1,17 +0,0 @@
import { TranslateLoader } from '@ngx-translate/core';
import { Observable } from 'rxjs';
import * as JSON5 from 'json5'
import * as fs from 'fs';
export class TranslateJson5UniversalLoader implements TranslateLoader {
constructor(private prefix: string = 'dist/assets/i18n/', private suffix: string = '.json') { }
public getTranslation(lang: string): Observable<any> {
return Observable.create((observer: any) => {
observer.next(JSON5.parse(fs.readFileSync(`${this.prefix}${lang}${this.suffix}`, 'utf8')));
observer.complete();
});
}
}

View File

@@ -0,0 +1,52 @@
import { TranslateLoader } from '@ngx-translate/core';
import { Observable, of as observableOf } from 'rxjs';
import * as fs from 'fs';
import { TransferState } from '@angular/platform-browser';
import { NGX_TRANSLATE_STATE, NgxTranslateState } from './ngx-translate-state';
import * as JSON5 from 'json5';
/**
* A TranslateLoader for ngx-translate to parse json5 files server-side, and store them in the
* TransferState
*/
export class TranslateServerLoader implements TranslateLoader {
constructor(
protected transferState: TransferState,
protected prefix: string = 'dist/assets/i18n/',
protected suffix: string = '.json'
) {
}
/**
* Return the i18n messages for a given language, and store them in the TransferState
*
* @param lang the language code
*/
public getTranslation(lang: string): Observable<any> {
// Retrieve the file for the given language, and parse it
const messages = JSON5.parse(fs.readFileSync(`${this.prefix}${lang}${this.suffix}`, 'utf8'));
// Store the parsed messages in the transfer state so they'll be available immediately when the
// app loads on the client
this.storeInTransferState(lang, messages);
// Return the parsed messages to translate things server side
return observableOf(messages);
}
/**
* Store the i18n messages for the given language code in the transfer state, so they can be
* retrieved client side
*
* @param lang the language code
* @param messages the i18n messages
* @protected
*/
protected storeInTransferState(lang: string, messages) {
const prevState = this.transferState.get<NgxTranslateState>(NGX_TRANSLATE_STATE, {});
const nextState = Object.assign({}, prevState, {
[lang]: messages
});
this.transferState.set(NGX_TRANSLATE_STATE, nextState);
}
}

View File

@@ -84,6 +84,7 @@ import { SearchModule } from '../../app/shared/search/search.module';
import { ResourcePoliciesModule } from '../../app/shared/resource-policies/resource-policies.module';
import { ComcolModule } from '../../app/shared/comcol/comcol.module';
import { FeedbackComponent } from './app/info/feedback/feedback.component';
import { RootModule } from '../../app/root.module';
const DECLARATIONS = [
FileSectionComponent,
@@ -136,6 +137,7 @@ const DECLARATIONS = [
AdminSearchModule,
AdminWorkflowModuleModule,
AppModule,
RootModule,
BitstreamFormatsModule,
BrowseByModule,
CollectionFormModule,

View File

@@ -0,0 +1,53 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { SharedModule } from '../../app/shared/shared.module';
import { HomeNewsComponent } from './app/home-page/home-news/home-news.component';
import { NavbarComponent } from './app/navbar/navbar.component';
import { HeaderComponent } from './app/header/header.component';
import {
HeaderNavbarWrapperComponent
} from './app/header-nav-wrapper/header-navbar-wrapper.component';
import { SearchModule } from '../../app/shared/search/search.module';
import { RootModule } from '../../app/root.module';
import { NavbarModule } from '../../app/navbar/navbar.module';
/**
* Add components that use a custom decorator to ENTRY_COM&PONENTS as well as DECLARATIONS. This will
* ensure that decorator gets picked up when the app loads
*/
const ENTRY_COMPONENTS = [
];
const DECLARATIONS = [
HomeNewsComponent,
HeaderComponent,
HeaderNavbarWrapperComponent,
NavbarComponent
];
@NgModule({
imports: [
CommonModule,
SharedModule,
SearchModule,
FormsModule,
RootModule,
NavbarModule,
],
declarations: DECLARATIONS,
providers: [
...ENTRY_COMPONENTS.map((component) => ({ provide: component }))
]
})
/**
* This module is included in the main bundle that gets downloaded at first page load. So it should
* contain only the themed components that have to be available immediately for the first page load,
* and the minimal set of imports required to make them work. Anything you can cut from it will make
* the initial page load faster, but may cause the page to flicker as components that were already
* rendered server side need to be lazy-loaded again client side
*
* Themed EntryComponents should also be added here
*/
export class EagerThemeModule {
}

View File

@@ -1,2 +0,0 @@
export const ENTRY_COMPONENTS = [
];

View File

@@ -2,10 +2,16 @@ import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { AdminRegistriesModule } from '../../app/admin/admin-registries/admin-registries.module';
import { AdminSearchModule } from '../../app/admin/admin-search-page/admin-search.module';
import { AdminWorkflowModuleModule } from '../../app/admin/admin-workflow-page/admin-workflow.module';
import { BitstreamFormatsModule } from '../../app/admin/admin-registries/bitstream-formats/bitstream-formats.module';
import {
AdminWorkflowModuleModule
} from '../../app/admin/admin-workflow-page/admin-workflow.module';
import {
BitstreamFormatsModule
} from '../../app/admin/admin-registries/bitstream-formats/bitstream-formats.module';
import { BrowseByModule } from '../../app/browse-by/browse-by.module';
import { CollectionFormModule } from '../../app/collection-page/collection-form/collection-form.module';
import {
CollectionFormModule
} from '../../app/collection-page/collection-form/collection-form.module';
import { CommunityFormModule } from '../../app/community-page/community-form/community-form.module';
import { CoreModule } from '../../app/core/core.module';
import { DragDropModule } from '@angular/cdk/drag-drop';
@@ -13,14 +19,18 @@ import { EditItemPageModule } from '../../app/item-page/edit-item-page/edit-item
import { FormsModule } from '@angular/forms';
import { HttpClientModule } from '@angular/common/http';
import { IdlePreloadModule } from 'angular-idle-preload';
import { JournalEntitiesModule } from '../../app/entity-groups/journal-entities/journal-entities.module';
import {
JournalEntitiesModule
} from '../../app/entity-groups/journal-entities/journal-entities.module';
import { MyDspaceSearchModule } from '../../app/my-dspace-page/my-dspace-search.module';
import { MenuModule } from '../../app/shared/menu/menu.module';
import { NavbarModule } from '../../app/navbar/navbar.module';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { ProfilePageModule } from '../../app/profile-page/profile-page.module';
import { RegisterEmailFormModule } from '../../app/register-email-form/register-email-form.module';
import { ResearchEntitiesModule } from '../../app/entity-groups/research-entities/research-entities.module';
import {
ResearchEntitiesModule
} from '../../app/entity-groups/research-entities/research-entities.module';
import { ScrollToModule } from '@nicky-lenaers/ngx-scroll-to';
import { SearchPageModule } from '../../app/search-page/search-page.module';
import { SharedModule } from '../../app/shared/shared.module';
@@ -28,7 +38,6 @@ import { StatisticsModule } from '../../app/statistics/statistics.module';
import { StoreModule } from '@ngrx/store';
import { StoreRouterConnectingModule } from '@ngrx/router-store';
import { TranslateModule } from '@ngx-translate/core';
import { HomeNewsComponent } from './app/home-page/home-news/home-news.component';
import { HomePageModule } from '../../app/home-page/home-page.module';
import { AppModule } from '../../app/app.module';
import { ItemPageModule } from '../../app/item-page/item-page.module';
@@ -40,18 +49,14 @@ import { CommunityPageModule } from '../../app/community-page/community-page.mod
import { CollectionPageModule } from '../../app/collection-page/collection-page.module';
import { SubmissionModule } from '../../app/submission/submission.module';
import { MyDSpacePageModule } from '../../app/my-dspace-page/my-dspace-page.module';
import { NavbarComponent } from './app/navbar/navbar.component';
import { HeaderComponent } from './app/header/header.component';
import { HeaderNavbarWrapperComponent } from './app/header-nav-wrapper/header-navbar-wrapper.component';
import { SearchModule } from '../../app/shared/search/search.module';
import { ResourcePoliciesModule } from '../../app/shared/resource-policies/resource-policies.module';
import {
ResourcePoliciesModule
} from '../../app/shared/resource-policies/resource-policies.module';
import { ComcolModule } from '../../app/shared/comcol/comcol.module';
import { RootModule } from '../../app/root.module';
const DECLARATIONS = [
HomeNewsComponent,
HeaderComponent,
HeaderNavbarWrapperComponent,
NavbarComponent
];
@NgModule({
@@ -60,6 +65,7 @@ const DECLARATIONS = [
AdminSearchModule,
AdminWorkflowModuleModule,
AppModule,
RootModule,
BitstreamFormatsModule,
BrowseByModule,
CollectionFormModule,
@@ -105,12 +111,12 @@ const DECLARATIONS = [
declarations: DECLARATIONS
})
/**
/**
* This module serves as an index for all the components in this theme.
* It should import all other modules, so the compiler knows where to find any components referenced
* from a component in this theme
* It is purposefully not exported, it should never be imported anywhere else, its only purpose is
* to give lazily loaded components a context in which they can be compiled successfully
*/
class ThemeModule {
class LazyThemeModule {
}

View File

@@ -0,0 +1,15 @@
import { NgModule } from '@angular/core';
import { EagerThemeModule as DSpaceEagerThemeModule } from './dspace/eager-theme.module';
/**
* This module only serves to ensure themed entry components are discoverable. It's kept separate
* from the theme modules to ensure only the minimal number of theme components is loaded ahead of
* time
*/
@NgModule({
imports: [
DSpaceEagerThemeModule
],
})
export class EagerThemesModule {
}

View File

@@ -1,23 +0,0 @@
import { NgModule } from '@angular/core';
import { ENTRY_COMPONENTS as CUSTOM } from './custom/entry-components';
const ENTRY_COMPONENTS = [
...CUSTOM,
];
/**
* This module only serves to ensure themed entry components are discoverable. It's kept separate
* from the theme modules to ensure only the minimal number of theme components is loaded ahead of
* time
*/
@NgModule()
export class ThemedEntryComponentModule {
static withEntryComponents() {
return {
ngModule: ThemedEntryComponentModule,
providers: ENTRY_COMPONENTS.map((component) => ({provide: component}))
};
}
}

View File

@@ -3,11 +3,36 @@ import { join } from 'path';
import { buildAppConfig } from '../src/config/config.server';
import { commonExports } from './webpack.common';
const CompressionPlugin = require('compression-webpack-plugin');
const zlib = require('zlib');
module.exports = Object.assign({}, commonExports, {
target: 'web',
node: {
module: 'empty'
},
plugins: [
...commonExports.plugins,
new CompressionPlugin({
filename: '[path][base].gz',
algorithm: 'gzip',
test: /\.(js|css|html|svg)$/,
threshold: 10240,
minRatio: 0.8,
}),
new CompressionPlugin({
filename: '[path][base].br',
algorithm: 'brotliCompress',
test: /\.(js|css|html|svg)$/,
compressionOptions: {
params: {
[zlib.constants.BROTLI_PARAM_QUALITY]: 11,
},
},
threshold: 10240,
minRatio: 0.8,
}),
],
devServer: {
before(app, server) {
buildAppConfig(join(process.cwd(), 'src/assets/config.json'));

View File

@@ -4112,17 +4112,16 @@ compressible@~2.0.16:
dependencies:
mime-db ">= 1.43.0 < 2"
compression-webpack-plugin@^3.0.1:
version "3.1.0"
resolved "https://registry.yarnpkg.com/compression-webpack-plugin/-/compression-webpack-plugin-3.1.0.tgz#9f510172a7b5fae5aad3b670652e8bd7997aeeca"
integrity sha512-iqTHj3rADN4yHwXMBrQa/xrncex/uEQy8QHlaTKxGchT/hC0SdlJlmL/5eRqffmWq2ep0/Romw6Ld39JjTR/ug==
compression-webpack-plugin@^6.1.1:
version "6.1.1"
resolved "https://registry.yarnpkg.com/compression-webpack-plugin/-/compression-webpack-plugin-6.1.1.tgz#ae8e4b2ffdb7396bb776e66918d751a20d8ccf0e"
integrity sha512-BEHft9M6lwOqVIQFMS/YJGmeCYXVOakC5KzQk05TFpMBlODByh1qNsZCWjUBxCQhUP9x0WfGidxTbGkjbWO/TQ==
dependencies:
cacache "^13.0.1"
find-cache-dir "^3.0.0"
neo-async "^2.5.0"
schema-utils "^2.6.1"
serialize-javascript "^2.1.2"
webpack-sources "^1.0.1"
cacache "^15.0.5"
find-cache-dir "^3.3.1"
schema-utils "^3.0.0"
serialize-javascript "^5.0.1"
webpack-sources "^1.4.3"
compression@^1.7.4:
version "1.7.4"
@@ -6237,7 +6236,7 @@ find-cache-dir@^2.1.0:
make-dir "^2.0.0"
pkg-dir "^3.0.0"
find-cache-dir@^3.0.0, find-cache-dir@^3.3.1:
find-cache-dir@^3.3.1:
version "3.3.2"
resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-3.3.2.tgz#b30c5b6eff0730731aea9bbd9dbecbd80256d64b"
integrity sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==
@@ -12521,7 +12520,7 @@ schema-utils@^1.0.0:
ajv-errors "^1.0.0"
ajv-keywords "^3.1.0"
schema-utils@^2.6.0, schema-utils@^2.6.1, schema-utils@^2.6.5, schema-utils@^2.6.6, schema-utils@^2.7.0:
schema-utils@^2.6.0, schema-utils@^2.6.5, schema-utils@^2.6.6, schema-utils@^2.7.0:
version "2.7.1"
resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.7.1.tgz#1ca4f32d1b24c590c203b8e7a50bf0ea4cd394d7"
integrity sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg==
@@ -12671,11 +12670,6 @@ send@0.17.1:
range-parser "~1.2.1"
statuses "~1.5.0"
serialize-javascript@^2.1.2:
version "2.1.2"
resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-2.1.2.tgz#ecec53b0e0317bdc95ef76ab7074b7384785fa61"
integrity sha512-rs9OggEUF0V4jUSecXazOYsLfu7OGK2qIn3c7IPBiffz32XniEp/TX9Xmc9LQfK2nQ2QKHvZ2oygKUGU0lG4jQ==
serialize-javascript@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-4.0.0.tgz#b525e1238489a5ecfc42afacc3fe99e666f4b1aa"
@@ -14712,7 +14706,7 @@ webpack-merge@^5.7.3:
clone-deep "^4.0.1"
wildcard "^2.0.0"
webpack-sources@1.4.3, webpack-sources@^1.0.1, webpack-sources@^1.1.0, webpack-sources@^1.2.0, webpack-sources@^1.3.0, webpack-sources@^1.4.0, webpack-sources@^1.4.1, webpack-sources@^1.4.3:
webpack-sources@1.4.3, webpack-sources@^1.1.0, webpack-sources@^1.2.0, webpack-sources@^1.3.0, webpack-sources@^1.4.0, webpack-sources@^1.4.1, webpack-sources@^1.4.3:
version "1.4.3"
resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.4.3.tgz#eedd8ec0b928fbf1cbfe994e22d2d890f330a933"
integrity sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==