Skip to content
Draft
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
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@
"@nguniversal/express-engine": "^13.0.2",
"@ngx-translate/core": "^13.0.0",
"@nicky-lenaers/ngx-scroll-to": "^9.0.0",
"@rezonant/preboot": "^8.0.0",
"angular-idle-preload": "3.0.0",
"angulartics2": "^12.0.0",
"axios": "^0.27.2",
Expand Down Expand Up @@ -116,6 +117,7 @@
"ngx-moment": "^5.0.0",
"ngx-pagination": "5.0.0",
"ngx-sortablejs": "^11.1.0",
"ngx-ui-switch": "^11.0.1",
"nouislider": "^14.6.3",
"pem": "1.14.4",
"postcss-cli": "^9.1.0",
Expand All @@ -128,8 +130,7 @@
"url-parse": "^1.5.6",
"uuid": "^8.3.2",
"webfontloader": "1.6.28",
"zone.js": "~0.11.5",
"ngx-ui-switch": "^11.0.1"
"zone.js": "~0.11.5"
},
"devDependencies": {
"@angular-builders/custom-webpack": "~13.1.0",
Expand Down
11 changes: 10 additions & 1 deletion src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import { APP_CONFIG, AppConfig } from '../config/app-config.interface';
import { NgxMaskModule } from 'ngx-mask';
import { StoreDevModules } from '../config/store/devtools';
import { RootModule } from './root.module';
import { PrebootModule } from '@rezonant/preboot';

export function getConfig() {
return environment;
Expand Down Expand Up @@ -77,6 +78,15 @@ const IMPORTS = [
StoreDevModules,
EagerThemesModule,
RootModule,
BrowserModule.withServerTransition({ appId: 'dspace-angular' }),
PrebootModule.withConfig({
appRoot: 'ds-app',

// Event replay doesn't work well for most of our components because it requires elements to have a unique id.
// By not defining any selectors for event capture we can turn it off for now. (shouldn't set replay:false because that also affects the transition itself)
eventSelectors: [
]
})
];

const PROVIDERS = [
Expand Down Expand Up @@ -148,7 +158,6 @@ const EXPORTS = [

@NgModule({
imports: [
BrowserModule.withServerTransition({ appId: 'dspace-angular' }),
...IMPORTS
],
providers: [
Expand Down
18 changes: 10 additions & 8 deletions src/app/core/auth/auth.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import { NotificationsServiceStub } from '../../shared/testing/notifications-ser
import { SetUserAsIdleAction, UnsetUserAsIdleAction } from './auth.actions';
import { SpecialGroupDataMock, SpecialGroupDataMock$ } from '../../shared/testing/special-group.mock';
import { cold } from 'jasmine-marbles';
import { NgZone } from '@angular/core';

describe('AuthService test', () => {

Expand Down Expand Up @@ -134,6 +135,7 @@ describe('AuthService test', () => {
{ provide: HardRedirectService, useValue: hardRedirectService },
{ provide: NotificationsService, useValue: NotificationsServiceStub },
{ provide: TranslateService, useValue: getMockTranslateService() },
{ provide: NgZone, useValue: new NgZone({}) },
CookieService,
AuthService
],
Expand Down Expand Up @@ -254,13 +256,13 @@ describe('AuthService test', () => {
}).compileComponents();
}));

beforeEach(inject([CookieService, AuthRequestService, Store, Router, RouteService], (cookieService: CookieService, authReqService: AuthRequestService, store: Store<AppState>, router: Router, routeService: RouteService, notificationsService: NotificationsService, translateService: TranslateService) => {
beforeEach(inject([CookieService, AuthRequestService, Store, Router, RouteService], (cookieService: CookieService, authReqService: AuthRequestService, store: Store<AppState>, router: Router, routeService: RouteService, notificationsService: NotificationsService, translateService: TranslateService, zone: NgZone) => {
store
.subscribe((state) => {
(state as any).core = Object.create({});
(state as any).core.auth = authenticatedState;
});
authService = new AuthService({}, window, undefined, authReqService, mockEpersonDataService, router, routeService, cookieService, store, hardRedirectService, notificationsService, translateService);
authService = new AuthService({}, window, undefined, authReqService, mockEpersonDataService, router, routeService, cookieService, store, hardRedirectService, notificationsService, translateService, zone);
}));

it('should return true when user is logged in', () => {
Expand Down Expand Up @@ -330,7 +332,7 @@ describe('AuthService test', () => {
}).compileComponents();
}));

beforeEach(inject([ClientCookieService, AuthRequestService, Store, Router, RouteService], (cookieService: ClientCookieService, authReqService: AuthRequestService, store: Store<AppState>, router: Router, routeService: RouteService, notificationsService: NotificationsService, translateService: TranslateService) => {
beforeEach(inject([ClientCookieService, AuthRequestService, Store, Router, RouteService], (cookieService: ClientCookieService, authReqService: AuthRequestService, store: Store<AppState>, router: Router, routeService: RouteService, notificationsService: NotificationsService, translateService: TranslateService, zone: NgZone) => {
const expiredToken: AuthTokenInfo = new AuthTokenInfo('test_token');
expiredToken.expires = Date.now() - (1000 * 60 * 60);
authenticatedState = {
Expand All @@ -345,7 +347,7 @@ describe('AuthService test', () => {
(state as any).core = Object.create({});
(state as any).core.auth = authenticatedState;
});
authService = new AuthService({}, window, undefined, authReqService, mockEpersonDataService, router, routeService, cookieService, store, hardRedirectService, notificationsService, translateService);
authService = new AuthService({}, window, undefined, authReqService, mockEpersonDataService, router, routeService, cookieService, store, hardRedirectService, notificationsService, translateService, zone);
storage = (authService as any).storage;
routeServiceMock = TestBed.inject(RouteService);
routerStub = TestBed.inject(Router);
Expand Down Expand Up @@ -559,13 +561,13 @@ describe('AuthService test', () => {
}).compileComponents();
}));

beforeEach(inject([CookieService, AuthRequestService, Store, Router, RouteService], (cookieService: CookieService, authReqService: AuthRequestService, store: Store<AppState>, router: Router, routeService: RouteService, notificationsService: NotificationsService, translateService: TranslateService) => {
beforeEach(inject([CookieService, AuthRequestService, Store, Router, RouteService], (cookieService: CookieService, authReqService: AuthRequestService, store: Store<AppState>, router: Router, routeService: RouteService, notificationsService: NotificationsService, translateService: TranslateService, zone: NgZone) => {
store
.subscribe((state) => {
(state as any).core = Object.create({});
(state as any).core.auth = unAuthenticatedState;
});
authService = new AuthService({}, window, undefined, authReqService, mockEpersonDataService, router, routeService, cookieService, store, hardRedirectService, notificationsService, translateService);
authService = new AuthService({}, window, undefined, authReqService, mockEpersonDataService, router, routeService, cookieService, store, hardRedirectService, notificationsService, translateService, zone);
}));

it('should return null for the shortlived token', () => {
Expand Down Expand Up @@ -599,13 +601,13 @@ describe('AuthService test', () => {
}).compileComponents();
}));

beforeEach(inject([CookieService, AuthRequestService, Store, Router, RouteService], (cookieService: CookieService, authReqService: AuthRequestService, store: Store<AppState>, router: Router, routeService: RouteService, notificationsService: NotificationsService, translateService: TranslateService) => {
beforeEach(inject([CookieService, AuthRequestService, Store, Router, RouteService], (cookieService: CookieService, authReqService: AuthRequestService, store: Store<AppState>, router: Router, routeService: RouteService, notificationsService: NotificationsService, translateService: TranslateService, zone: NgZone) => {
store
.subscribe((state) => {
(state as any).core = Object.create({});
(state as any).core.auth = idleState;
});
authService = new AuthService({}, window, undefined, authReqService, mockEpersonDataService, router, routeService, cookieService, store, hardRedirectService, notificationsService, translateService);
authService = new AuthService({}, window, undefined, authReqService, mockEpersonDataService, router, routeService, cookieService, store, hardRedirectService, notificationsService, translateService, zone);
}));

it('isUserIdle should return true when user is not idle', () => {
Expand Down
22 changes: 15 additions & 7 deletions src/app/core/auth/auth.service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Inject, Injectable, Optional } from '@angular/core';
import { Inject, Injectable, NgZone, Optional } from '@angular/core';
import { Router } from '@angular/router';
import { HttpHeaders } from '@angular/common/http';
import { REQUEST, RESPONSE } from '@nguniversal/express-engine/tokens';
Expand Down Expand Up @@ -90,7 +90,8 @@ export class AuthService {
protected store: Store<AppState>,
protected hardRedirectService: HardRedirectService,
private notificationService: NotificationsService,
private translateService: TranslateService
private translateService: TranslateService,
private zone: NgZone,
) {
this.store.pipe(
select(isAuthenticated),
Expand Down Expand Up @@ -350,7 +351,11 @@ export class AuthService {
if (currentlyRefreshingToken && token !== undefined && authTokenInfo === undefined) {
// Token refresh failed => Error notification => 10 second wait => Page reloads & user logged out
this.notificationService.error(this.translateService.get('auth.messages.token-refresh-failed'));
setTimeout(() => this.navigateToRedirectUrl(this.hardRedirectService.getCurrentRoute()), 10000);

this.zone.runOutsideAngular(() => {
setTimeout(() => this.navigateToRedirectUrl(this.hardRedirectService.getCurrentRoute()), 10000);
});

currentlyRefreshingToken = false;
}
// If new token.expires is different => Refresh succeeded
Expand All @@ -368,10 +373,13 @@ export class AuthService {
if (hasValue(this.tokenRefreshTimer)) {
clearTimeout(this.tokenRefreshTimer);
}
this.tokenRefreshTimer = setTimeout(() => {
this.store.dispatch(new RefreshTokenAction(token));
currentlyRefreshingToken = true;
}, timeLeftBeforeRefresh);

this.zone.runOutsideAngular(() => {
this.tokenRefreshTimer = setTimeout(() => {
this.store.dispatch(new RefreshTokenAction(token));
currentlyRefreshingToken = true;
}, timeLeftBeforeRefresh);
});
}
}
});
Expand Down
2 changes: 2 additions & 0 deletions src/app/root/root.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
value: (!(sidebarVisible | async) ? 'hidden' : (slideSidebarOver | async) ? 'shown' : 'expanded'),
params: {collapsedSidebarWidth: (collapsedSidebarWidth | async), totalSidebarWidth: (totalSidebarWidth | async)}
}">

<ds-loading-csr></ds-loading-csr>
<ds-themed-header-navbar-wrapper></ds-themed-header-navbar-wrapper>
<main class="main-content">
<ds-themed-breadcrumbs></ds-themed-breadcrumbs>
Expand Down
5 changes: 3 additions & 2 deletions src/app/root/root.component.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { map } from 'rxjs/operators';
import { map, startWith } from 'rxjs/operators';
import { Component, Inject, Input, OnInit } from '@angular/core';
import { Router } from '@angular/router';

Expand Down Expand Up @@ -71,7 +71,8 @@ export class RootComponent implements OnInit {
const sidebarCollapsed = this.menuService.isMenuCollapsed(MenuID.ADMIN);
this.slideSidebarOver = combineLatestObservable([sidebarCollapsed, this.windowService.isXsOrSm()])
.pipe(
map(([collapsed, mobile]) => collapsed || mobile)
map(([collapsed, mobile]) => collapsed || mobile),
startWith(true),
);

if (this.router.url === getPageInternalServerErrorRoute()) {
Expand Down
2 changes: 1 addition & 1 deletion src/app/search-navbar/search-navbar.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<input #searchInput [@toggleAnimation]="isExpanded" [attr.aria-label]="('nav.search' | translate)" name="query"
formControlName="query" type="text" placeholder="{{searchExpanded ? ('nav.search' | translate) : ''}}"
class="d-inline-block bg-transparent position-absolute form-control dropdown-menu-right p-1" [attr.data-test]="'header-search-box' | dsBrowserOnly">
<a class="submit-icon" [routerLink]="" (click)="searchExpanded ? onSubmit(searchForm.value) : expand()" [attr.data-test]="'header-search-icon' | dsBrowserOnly">
<a class="submit-icon" href="javascript:void(0);" (click)="searchExpanded ? onSubmit(searchForm.value) : expand()" [attr.data-test]="'header-search-icon' | dsBrowserOnly">
<em class="fas fa-search fa-lg fa-fw"></em>
</a>
</form>
Expand Down
1 change: 1 addition & 0 deletions src/app/shared/loading-csr/loading-csr.component.html
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<div class="csr-progress-bar fixed-top" role="progressbar" *ngIf="loading"></div>
20 changes: 20 additions & 0 deletions src/app/shared/loading-csr/loading-csr.component.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
.csr-progress-bar {
background: linear-gradient(to left, transparent 50%, var(--ds-csr-loading-color) 50%);
background-size: var(--ds-csr-loading-dash);
height: var(--ds-csr-loading-height);
width: 100%;

// make sure it stays above the navbar but below the admin sidebar
z-index: calc(var(--ds-sidebar-z-index) - 1);

animation: csr-loading-animation 0.5s linear infinite;
}

@keyframes csr-loading-animation {
0% {
background-position-x: 0
}
100% {
background-position-x: var(--ds-csr-loading-dash)
}
}
46 changes: 46 additions & 0 deletions src/app/shared/loading-csr/loading-csr.component.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';

import { LoadingCsrComponent } from './loading-csr.component';
import { PLATFORM_ID } from '@angular/core';

describe('LoadingCsrComponent', () => {
let component: LoadingCsrComponent;
let fixture: ComponentFixture<LoadingCsrComponent>;

const init = async (platformId) => {

await TestBed.configureTestingModule({
declarations: [ LoadingCsrComponent ],
providers: [
{
provide: PLATFORM_ID,
useValue: platformId,
},
]
}).compileComponents();

fixture = TestBed.createComponent(LoadingCsrComponent);
component = fixture.componentInstance;
fixture.detectChanges();
};

describe('on the server', () => {
beforeEach(async () => {
await init('server');
});

it('should have loading=true', () => {
expect(component.loading).toBe(true);
});
});

describe('in the browser', () => {
beforeEach(async () => {
await init('browser');
});

it('should have loading=false', () => {
expect(component.loading).toBe(false);
});
});
});
20 changes: 20 additions & 0 deletions src/app/shared/loading-csr/loading-csr.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Component, Inject, PLATFORM_ID } from '@angular/core';
import { isPlatformServer } from '@angular/common';

/**
* Shows a loading animation when rendered on the server
*/
@Component({
selector: 'ds-loading-csr',
templateUrl: './loading-csr.component.html',
styleUrls: ['./loading-csr.component.scss']
})
export class LoadingCsrComponent {
loading: boolean;

constructor(
@Inject(PLATFORM_ID) private platformId: any,
) {
this.loading = isPlatformServer(this.platformId);
}
}
2 changes: 2 additions & 0 deletions src/app/shared/shared.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,7 @@ import { BrowserOnlyPipe } from './utils/browser-only.pipe';
import { ThemedLoadingComponent } from './loading/themed-loading.component';
import { PersonPageClaimButtonComponent } from './dso-page/person-page-claim-button/person-page-claim-button.component';
import { SearchExportCsvComponent } from './search/search-export-csv/search-export-csv.component';
import { LoadingCsrComponent } from './loading-csr/loading-csr.component';

const MODULES = [
CommonModule,
Expand Down Expand Up @@ -351,6 +352,7 @@ const COMPONENTS = [
LangSwitchComponent,
LoadingComponent,
ThemedLoadingComponent,
LoadingCsrComponent,
LogInComponent,
LogOutComponent,
NumberPickerComponent,
Expand Down
7 changes: 4 additions & 3 deletions src/app/statistics/google-analytics.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
createSuccessfulRemoteDataObject$
} from '../shared/remote-data.utils';
import { ConfigurationProperty } from '../core/shared/configuration-property.model';
import { NgZone } from '@angular/core';

describe('GoogleAnalyticsService', () => {
const trackingIdProp = 'google.analytics.key';
Expand Down Expand Up @@ -51,7 +52,7 @@ describe('GoogleAnalyticsService', () => {
body: bodyElementSpy,
});

service = new GoogleAnalyticsService(angularticsSpy, configSpy, documentSpy);
service = new GoogleAnalyticsService(angularticsSpy, configSpy, documentSpy, new NgZone({}));
});

it('should be created', () => {
Expand All @@ -71,7 +72,7 @@ describe('GoogleAnalyticsService', () => {
findByPropertyName: createFailedRemoteDataObject$(),
});

service = new GoogleAnalyticsService(angularticsSpy, configSpy, documentSpy);
service = new GoogleAnalyticsService(angularticsSpy, configSpy, documentSpy, new NgZone({}));
});

it('should NOT add a script to the body', () => {
Expand All @@ -89,7 +90,7 @@ describe('GoogleAnalyticsService', () => {
describe('when the tracking id is empty', () => {
beforeEach(() => {
configSpy = createConfigSuccessSpy();
service = new GoogleAnalyticsService(angularticsSpy, configSpy, documentSpy);
service = new GoogleAnalyticsService(angularticsSpy, configSpy, documentSpy, new NgZone({}));
});

it('should NOT add a script to the body', () => {
Expand Down
Loading