Unverified Commit 60a6e73a authored by Nestor Diaz's avatar Nestor Diaz Committed by GitHub

Upgrade to Angular 10 (#401)

- Upgrade to Angular 10
parent 55eb8bd8
Pipeline #100173 passed with stages
in 21 minutes and 19 seconds
# BioStudies - Submission Tool
- Angular v9
- Angular v10
- ExpressJS v4
- Node v10.16.2
......
......@@ -7,6 +7,7 @@
"root": "",
"sourceRoot": "src",
"projectType": "application",
"prefix": "st",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
......@@ -15,7 +16,7 @@
"outputPath": ".build",
"index": "src/index.html",
"main": "src/main.ts",
"tsConfig": "src/tsconfig.app.json",
"tsConfig": "tsconfig.app.json",
"polyfills": "src/polyfills.ts",
"assets": [
"src/images",
......@@ -81,10 +82,11 @@
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": [
"src/tsconfig.app.json",
"src/tsconfig.spec.json"
"tsconfig.app.json"
],
"exclude": []
"exclude": [
"**/node_modules/**"
]
}
}
}
......
This diff is collapsed.
......@@ -16,6 +16,7 @@
"node:prod": "cross-env NODE_ENV=production node .",
"start:dev": "run-p node:dev ng:start",
"pretest": "npm run lint",
"postinstall": "ngcc",
"test": "jest --ci"
},
"jest": {
......@@ -49,14 +50,14 @@
]
},
"dependencies": {
"@angular/animations": "9.1.12",
"@angular/common": "9.1.12",
"@angular/compiler": "9.1.12",
"@angular/core": "9.1.12",
"@angular/forms": "9.1.12",
"@angular/platform-browser": "9.1.12",
"@angular/platform-browser-dynamic": "9.1.12",
"@angular/router": "9.1.12",
"@angular/animations": "10.0.12",
"@angular/common": "10.0.12",
"@angular/compiler": "10.0.12",
"@angular/core": "10.0.12",
"@angular/forms": "10.0.12",
"@angular/platform-browser": "10.0.12",
"@angular/platform-browser-dynamic": "10.0.12",
"@angular/router": "10.0.12",
"@biostudies/ckeditor5-build-balloon": "19.0.0",
"@ckeditor/ckeditor5-angular": "1.2.3",
"@ckeditor/ckeditor5-clipboard": "19.0.1",
......@@ -64,25 +65,26 @@
"@rxweb/reactive-form-validators": "2.0.0",
"ag-grid-angular": "23.2.1",
"ag-grid-community": "23.2.1",
"fp-ts": "1.9.0",
"fp-ts": "1.19.5",
"helmet": "3.21.1",
"http-status-codes": "1.3.2",
"lodash": "4.17.20",
"lodash.debounce": "4.0.8",
"lodash.isempty": "4.4.0",
"ng-recaptcha": "5.0.0",
"ng2-cookies": "1.0.12",
"ngx-bootstrap": "5.6.1",
"ngx-cookie-service": "10.0.1",
"ngx-markdown": "9.1.1",
"ngx-sortablejs": "3.1.4",
"pluralize": "8.0.0",
"rxjs": "6.6.2",
"sortablejs": "1.10.2",
"tslib": "1.10.0",
"zone.js": "0.10.2"
"tslib": "2.0.0",
"zone.js": "0.10.3"
},
"devDependencies": {
"@angular-devkit/build-angular": "~0.901.12",
"@angular/cli": "9.1.12",
"@angular/compiler-cli": "9.1.12",
"@angular-devkit/build-angular": "0.1000.7",
"@angular/cli": "10.0.7",
"@angular/compiler-cli": "10.0.12",
"@testing-library/angular": "10.0.2",
"@testing-library/jest-dom": "5.11.4",
"@types/amqplib": "0.5.13",
......@@ -95,12 +97,12 @@
"@types/jest": "26.0.10",
"@types/jsdom": "16.2.3",
"@types/node": "14.6.0",
"@types/pluralize": "0.0.28",
"@types/pluralize": "0.0.29",
"@types/request": "2.48.5",
"@types/sortablejs": "1.10.4",
"amqplib": "0.5.6",
"body-parser": "1.19.0",
"codelyzer": "5.1.2",
"codelyzer": "6.0.0",
"compression": "1.7.4",
"config": "3.1.0",
"cross-env": "5.2.1",
......@@ -116,12 +118,14 @@
"jest-preset-angular": "8.3.1",
"nodemon": "1.19.1",
"npm-run-all": "4.1.5",
"protractor": "6.0.0",
"rxjs-tslint-rules": "4.23.1",
"prettier": "^2.1.1",
"protractor": "7.0.0",
"ts-node": "2.0.0",
"ts-node-dev": "1.0.0-pre.50",
"tslint": "5.11.0",
"typescript": "3.8.3",
"tslint": "6.1.0",
"tslint-config-prettier": "1.18.0",
"tslint-plugin-prettier": "2.3.0",
"typescript": "3.9.7",
"winston": "3.3.3"
}
}
......@@ -19,17 +19,22 @@ export class AppComponent implements OnInit {
setTheme('bs3');
}
ngOnInit() {
ngOnInit(): void {
const bannerEl = document.createElement('script');
// Loads the GDPR bottom panel logic.
// NOTE: The banner should be called with 'other' to indicate a framework different from EBI's is in use.
// NOTE: ebiFrameworkRunDataProtectionBanner is defined after the script loads.
bannerEl.src = this.appConfig.bannerUrl;
bannerEl.onload = function () {
window['ebiFrameworkRunDataProtectionBanner']('other');
bannerEl.onload = () => {
if (window.ebiFrameworkRunDataProtectionBanner !== undefined) {
window.ebiFrameworkRunDataProtectionBanner('other');
}
};
document.head!.appendChild(bannerEl);
if (document.head !== undefined) {
document.head.appendChild(bannerEl);
}
this.userSession.init();
}
......
......@@ -23,7 +23,7 @@ export class AppConfig {
/**
* Synonym getter providing the threshold below which the current screen size will trigger
* tablet/mobile-geared layout.
* @returns {number} Upper-limit screen size in pixels for tablet-like devices.
* @returns Upper-limit screen size in pixels for tablet-like devices.
*/
get tabletBreak(): number {
return this.config.APP_TABLET_BREAKPOINT;
......@@ -31,7 +31,7 @@ export class AppConfig {
/**
* Synonym getter providing the format in which dates should be displayed when listing submissions.
* @returns {string} Format expressed in Angular's date pipe notation.
* @returns Format expressed in Angular's date pipe notation.
* @see {@link https://angular.io/api/common/DatePipe}
*/
get dateListFormat(): string {
......@@ -40,7 +40,7 @@ export class AppConfig {
/**
* Synonym getter providing the format in which dates should be displayed when editing submissions.
* @returns {string} Format following the Moment.js' notation.
* @returns Format following the Moment.js' notation.
* @see {@link https://momentjs.com/docs/#/parsing/string-format/}
*/
get dateInputFormat(): string {
......@@ -50,7 +50,7 @@ export class AppConfig {
/**
* Synonym getter providing the number of years ahead of the current date that the date picker will render
* selectable dates of.
* @returns {number} Maximum number of years into the future.
* @returns Maximum number of years into the future.
*/
get maxDateYears(): number {
return this.config.APP_MAX_DATE_YEARS;
......@@ -58,7 +58,7 @@ export class AppConfig {
/**
* Synonym getter providing the maximum number of suggested entries in a typeahead box.
* @returns {number} Maximum length of the suggestion list.
* @returns Maximum length of the suggestion list.
*/
get maxSuggestLength(): number {
return this.config.APP_MAX_SUGGEST_LENGTH;
......@@ -66,7 +66,7 @@ export class AppConfig {
/**
* Synonym getter providing the URL for the script containing the GDPR banner's logic.
* @returns {string} URL.
* @returns URL.
*/
get bannerUrl(): string {
return this.config.GDPR_BANNER_URL;
......@@ -76,7 +76,7 @@ export class AppConfig {
* Maximum number of concurrent connections supported by the browser. It should be in accordance to
* a ball-park average for different browsers.
* @see {@link http://www.browserscope.org/?category=network&v=top}
* @returns {number} Number of allowed concurrent connections.
* @returns Number of allowed concurrent connections.
*/
get maxConcurrent(): number {
return this.config.MAX_CONCURRENT;
......@@ -84,7 +84,7 @@ export class AppConfig {
/**
* The current environment where tha application is running.
* @returns {string} Name of the current environment.
* @returns Name of the current environment.
*/
get environment(): string {
return this.config.APP_ENV;
......@@ -97,7 +97,7 @@ export class AppConfig {
* for that through the "APP_INITIALIZER" injector token. However, this feature is still experimental in v4,
* requiring strict use of promises for it to be dependable.
* TODO: Since Angular has been bumped up to v7, the much cleaner "APP_INITIALIZER" approach should be followed instead.
* @returns {Promise<any>} Promise fulfilled once the config data has been fetched.
* @returns Promise fulfilled once the config data has been fetched.
* @see {@link /src/app/app.module.ts}
* @see {@link https://stackoverflow.com/a/40222544}
*/
......
......@@ -9,10 +9,9 @@ import { UserSession } from 'app/auth/shared';
@Injectable()
export class AuthGuard implements CanActivate {
constructor(private userSession: UserSession,
private router: Router) {
}
constructor(private userSession: UserSession, private router: Router) {}
// tslint:disable-next-line: variable-name
canActivate(_route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
if (this.userSession.isAnonymous()) {
this.router.navigate(['/signin', {next: state.url}]);
......
......@@ -17,16 +17,16 @@ export class ActivationLinkReqComponent implements AfterViewInit {
showSuccess: boolean = false;
@ViewChild('emailEl')
private focusRef?: ElementRef;
private focusRef!: ElementRef;
@ViewChild('recaptchaEl')
private recaptchaRef?: RecaptchaComponent;
private recaptchaRef!: RecaptchaComponent;
constructor(private authService: AuthService) {}
// TODO: Turn autofocus on render into a directive
ngAfterViewInit(): void {
this.focusRef!.nativeElement.focus();
this.focusRef.nativeElement.focus();
}
onRecaptchaResolved(captchaToken: string): void {
......@@ -50,7 +50,7 @@ export class ActivationLinkReqComponent implements AfterViewInit {
}
}
onSubmit(form: NgForm) {
onSubmit(form: NgForm): void {
if (this.hasError) {
this.resetRecaptcha();
this.hasError = false;
......@@ -59,7 +59,7 @@ export class ActivationLinkReqComponent implements AfterViewInit {
if (form.valid) {
this.isLoading = true;
this.recaptchaRef!.execute();
this.recaptchaRef.execute();
} else {
Object.keys(form.controls).forEach((key) => {
form.controls[key].markAsTouched({onlySelf: true});
......@@ -68,7 +68,7 @@ export class ActivationLinkReqComponent implements AfterViewInit {
}
resetRecaptcha(): void {
this.recaptchaRef!.reset();
this.recaptchaRef.reset();
this.model.resetCaptcha();
}
}
......@@ -10,6 +10,7 @@ import { PasswordResetReqComponent } from './password-reset/password-reset-req.c
import { PasswordResetComponent } from './password-reset/password-reset.component';
import { SignUpComponent } from './signup/signup.component';
import { Equals2Directive } from './password-reset/equals2.directive';
import { CookieService } from 'ngx-cookie-service';
@NgModule({
imports: [
......@@ -21,7 +22,8 @@ import { Equals2Directive } from './password-reset/equals2.directive';
providers: [
AuthService,
UserSession,
UserData
UserData,
CookieService
],
declarations: [
SignInComponent,
......
......@@ -17,16 +17,16 @@ export class PasswordResetReqComponent implements AfterViewInit {
showSuccess: boolean = false;
@ViewChild('emailEl')
private focusRef?: ElementRef;
private focusRef!: ElementRef;
@ViewChild('recaptchaEl')
private recaptcha?: RecaptchaComponent;
private recaptcha!: RecaptchaComponent;
constructor(private authService: AuthService) {}
// TODO: Turn autofocus on render into a directive
ngAfterViewInit(): void {
this.focusRef!.nativeElement.focus();
this.focusRef.nativeElement.focus();
}
onRecaptchaResolved(captchaToken: string): void {
......@@ -58,7 +58,7 @@ export class PasswordResetReqComponent implements AfterViewInit {
if (form.valid) {
this.isLoading = true;
this.recaptcha!.execute();
this.recaptcha.execute();
} else {
Object.keys(form.controls).forEach((key) => {
form.controls[key].markAsTouched({onlySelf: true});
......@@ -67,7 +67,7 @@ export class PasswordResetReqComponent implements AfterViewInit {
}
resetRecaptcha(): void {
this.recaptcha!.reset();
this.recaptcha.reset();
this.model.resetCaptcha();
}
}
......@@ -18,10 +18,10 @@ export class PasswordResetComponent implements OnInit, AfterViewInit {
showSuccess: boolean = false;
@ViewChild('focusEl')
private focusRef?: ElementRef;
private focusRef!: ElementRef;
@ViewChild('recaptchaEl')
private recaptcha?: RecaptchaComponent;
private recaptcha!: RecaptchaComponent;
constructor(
private authService: AuthService,
......@@ -30,7 +30,7 @@ export class PasswordResetComponent implements OnInit, AfterViewInit {
// TODO: Turn autofocus on render into a directive
ngAfterViewInit(): void {
this.focusRef!.nativeElement.focus();
this.focusRef.nativeElement.focus();
}
ngOnInit(): void {
......@@ -71,7 +71,7 @@ export class PasswordResetComponent implements OnInit, AfterViewInit {
if (form.valid) {
this.isLoading = true;
this.recaptcha!.execute();
this.recaptcha.execute();
} else {
Object.keys(form.controls).forEach((key) => {
form.controls[key].markAsTouched({onlySelf: true});
......@@ -80,7 +80,7 @@ export class PasswordResetComponent implements OnInit, AfterViewInit {
}
resetRecaptcha(): void {
this.recaptcha!.reset();
this.recaptcha.reset();
this.model.resetCaptcha();
}
}
......@@ -87,12 +87,12 @@ export class AuthService {
this.userSession.destroy();
}
return <T>(response.body || {});
return (response.body || {}) as T;
}
private checkStatus<R>(response: HttpResponse<R>): R {
if (isSuccessStatusCode(response.status)) {
return <R>(response.body || {});
return (response.body || {}) as R;
}
throw ServerError.dataError(response.body);
......
......@@ -15,18 +15,18 @@ export class DataWithCaptcha {
}
export class DataWithCaptchaAndPath extends DataWithCaptcha {
private _path: string;
private innerPath: string;
constructor(path: string) {
super();
this._path = path;
this.innerPath = path;
}
get path(): string {
return this._path;
return this.innerPath;
}
snapshot(): any {
return { ...super.snapshot(), 'path': this.path };
return { ...super.snapshot(), path: this.path };
}
}
......@@ -8,7 +8,7 @@ class EmailRequestData extends DataWithCaptchaAndPath {
}
snapshot(): { [key: string]: string } {
return { ...super.snapshot(), 'email': this.email };
return { ...super.snapshot(), email: this.email };
}
}
......
......@@ -6,6 +6,6 @@ export class PasswordResetData extends DataWithCaptcha {
passwordRepeat: string = '';
snapshot(): { [key: string]: string } {
return { ...super.snapshot(), 'activationKey': this.key, 'password': this.password };
return { ...super.snapshot(), activationKey: this.key, password: this.password };
}
}
......@@ -13,10 +13,10 @@ export class RegistrationData extends DataWithCaptchaAndPath {
snapshot(): any {
return {
...super.snapshot(),
'aux': [`orcid:${this.orcid}`],
'email': this.email,
'password': this.password,
'name': this.name
aux: [`orcid:${this.orcid}`],
email: this.email,
password: this.password,
name: this.name
};
}
}
import { Cookie } from 'ng2-cookies/ng2-cookies';
import { CookieService } from 'ngx-cookie-service';
import { UserInfo } from './model';
import { Injectable } from '@angular/core';
const LOGIN_TOKEN_NAME = 'BioStudiesToken';
const USER = 'BioStudiesUser';
const COOKIE_PATH = '/';
@Injectable({
providedIn: 'root'
})
export class UserCookies {
COOKIE_EXPIRES: number = 365;
COOKIE_NAME: string = 'BioStudiesToken';
COOKIE_PATH: string = '/';
USER: string = 'BioStudiesUser';
export function setLoginToken(token: string): void {
Cookie.set(LOGIN_TOKEN_NAME, token, 365, COOKIE_PATH);
}
constructor(private cookieService: CookieService) {}
export function getLoginToken(): string {
return Cookie.get(LOGIN_TOKEN_NAME) || '';
}
destroyLoginToken(): void {
this.cookieService.delete(this.COOKIE_NAME, this.COOKIE_PATH);
}
export function destroyLoginToken(): void {
Cookie.delete(LOGIN_TOKEN_NAME, COOKIE_PATH);
}
destroyUser(): void {
localStorage.removeItem(this.USER);
}
export function destroyUser(): void {
localStorage.removeItem(USER);
}
getLoginToken(): string {
return this.cookieService.get(this.COOKIE_NAME);
}
export function setUser(user: UserInfo): void {
localStorage.setItem(USER, JSON.stringify(user));
}
getUser(): UserInfo {
return JSON.parse(localStorage.getItem(this.USER) || '{}');
}
setLoginToken(token: string): void {
this.cookieService.set(this.COOKIE_NAME, token, this.COOKIE_EXPIRES, this.COOKIE_PATH);
}
export function getUser(): UserInfo {
return JSON.parse(localStorage.getItem(USER) || '{}');
setUser(user: UserInfo): void {
localStorage.setItem(this.USER, JSON.stringify(user));
}
}
import { async } from '@angular/core/testing';
import { AuthService, UserData, UserSession } from 'app/auth/shared';
import { of } from 'rxjs';
import { of, Observable } from 'rxjs';
import { UserInfo, ExtendedUserInfo } from './model';
describe('UserData', () => {
let submService;
let appConfig;
let userCookies;
beforeEach(() => {
submService = {
getProjects() {
getProjects(): Observable<[]> {
return of([]);
}
};
appConfig = {
environment: 'DEV'