diff --git a/src/app/core/app.component.html b/src/app/core/app.component.html index 6c46b1de8..9cdc24699 100644 --- a/src/app/core/app.component.html +++ b/src/app/core/app.component.html @@ -1 +1,2 @@ + diff --git a/src/app/core/assets/jsdraw/Scilligence.JSDraw2.Pro.js b/src/app/core/assets/jsdraw/Scilligence.JSDraw2.Pro.js index d4e0b1148..252281548 100644 --- a/src/app/core/assets/jsdraw/Scilligence.JSDraw2.Pro.js +++ b/src/app/core/assets/jsdraw/Scilligence.JSDraw2.Pro.js @@ -28,6 +28,8 @@ JSDraw2.password = { encrypt: true, key: null, iv: null }; // Expiration Date: 2026-Jul-30 JSDraw2.licensecode='405562537916781761723242424242424131213141512181'; + + ////////////////////////////////////////////////////////////////////////////////// // JSDraw default settings diff --git a/src/app/core/auth/auth.service.ts b/src/app/core/auth/auth.service.ts index 00828d025..e4c726792 100644 --- a/src/app/core/auth/auth.service.ts +++ b/src/app/core/auth/auth.service.ts @@ -1,14 +1,35 @@ -import { Injectable, PLATFORM_ID, Inject } from '@angular/core'; -import { ConfigService } from '../config/config.service'; -import { Auth, Privilege, Role, UserGroup } from './auth.model'; -import { from, Observable, BehaviorSubject, of, firstValueFrom, throwError } from 'rxjs'; -import { map, take, catchError, concat, switchMap, tap } from 'rxjs/operators'; -import { HttpClient, HttpParams } from '@angular/common/http'; -import { isPlatformBrowser } from '@angular/common'; -import { UserDownload, AllUserDownloads } from '@gsrs-core/auth/user-downloads/download.model'; +import { Injectable, PLATFORM_ID, Inject } from "@angular/core"; +import { ConfigService } from "../config/config.service"; +import { Auth, Privilege, Role, UserGroup } from "./auth.model"; +import { + interval, + from, + Observable, + BehaviorSubject, + of, + firstValueFrom, + throwError, +} from "rxjs"; +import { + map, + take, + catchError, + concat, + switchMap, + tap, + takeWhile, + filter, + concatMap, +} from "rxjs/operators"; +import { HttpClient, HttpParams } from "@angular/common/http"; +import { isPlatformBrowser } from "@angular/common"; +import { + UserDownload, + AllUserDownloads, +} from "@gsrs-core/auth/user-downloads/download.model"; @Injectable({ - providedIn: 'root' + providedIn: "root", }) export class AuthService { private _auth: Auth; @@ -19,46 +40,50 @@ export class AuthService { constructor( public configService: ConfigService, private http: HttpClient, - @Inject(PLATFORM_ID) private platformId: any + @Inject(PLATFORM_ID) private platformId: any, ) { - this.isLoading = true; - configService.afterLoad().then(cs => { - this.fetchAuth().pipe( - take(1), - switchMap(auth => { - if (auth && auth.computedToken != null) { - this._auth = auth; - this._authUpdate.next(this._auth); - // Fetch privileges AFTER successful auth - return this.fetchPrivs(); - } else { - this._auth = null; + this.isLoading = true; + configService.afterLoad().then((cs) => { + this.fetchAuth() + .pipe( + take(1), + switchMap((auth) => { + if (auth && auth.computedToken != null) { + this._auth = auth; + this._authUpdate.next(this._auth); + // Fetch privileges AFTER successful auth + return this.fetchPrivs(); + } else { + this._auth = null; + this._authUpdate.next(null); + return of([]); + } + }), + ) + .subscribe({ + next: (privs) => { + this._privileges = privs; + this.isLoading = false; + }, + error: (err) => { + console.error("Error:", err); this._authUpdate.next(null); - return of([]); - } - }) - ).subscribe({ - next: privs => { - this._privileges = privs; - this.isLoading = false; - }, - error: err => { - console.error('Error:', err); - this._authUpdate.next(null); - this.isLoading = false; - } - }); - }); -} - + this.isLoading = false; + }, + }); + }); + } -get auth(): Auth { + get auth(): Auth { return this._auth; - } + } public checkAuth(): Observable { - const url = `${(this.configService.configData && this.configService.configData.apiBaseUrl) || '/'}api/v1/`; - if (this.configService.configData && this.configService.configData.dummyWhoami) { + const url = `${(this.configService.configData && this.configService.configData.apiBaseUrl) || "/"}api/v1/`; + if ( + this.configService.configData && + this.configService.configData.dummyWhoami + ) { return of(this.configService.configData.dummyWhoami); } else { return this.http.get(`${url}whoami`); @@ -66,26 +91,28 @@ get auth(): Auth { } login(username: string, password: string): Observable { - const options = { headers: { - 'auth-username': username, - 'auth-password': password - } + "auth-username": username, + "auth-password": password, + }, }; - const url = `${(this.configService.configData && this.configService.configData.apiBaseUrl) || '/'}api/v1/`; + const url = `${(this.configService.configData && this.configService.configData.apiBaseUrl) || "/"}api/v1/`; let obs = this.http.get(`${url}whoami`, options); - if (this.configService.configData && this.configService.configData.dummyWhoami) { + if ( + this.configService.configData && + this.configService.configData.dummyWhoami + ) { obs = of(this.configService.configData.dummyWhoami); } return obs.pipe( - switchMap(auth => { + switchMap((auth) => { if (auth && auth.computedToken) { this._auth = auth; if (isPlatformBrowser(this.platformId)) { - sessionStorage.setItem('authToken', auth.computedToken); + sessionStorage.setItem("authToken", auth.computedToken); } this._authUpdate.next(this._auth); // Fetch privileges after successful login @@ -94,14 +121,44 @@ get auth(): Auth { catchError(() => { // Privileges fetch failed, but auth succeeded - still return auth return of(this._auth); - }) + }), ); } else { this._auth = null; this._authUpdate.next(null); return of(null); } - }) + }), + ); + } + + // Helper function to create an Observable that emits when the popup login window closes + private waitForPopupToClose(popupWindow: Window): Observable { + return interval(1000).pipe( + takeWhile(() => !popupWindow.closed, true), + filter(() => popupWindow.closed), + ); + } + + // Method to handle pFDA login (using popup window) and return success/unsuccess flag + pfdaLogin(): Observable { + const height = 700; + const width = 700; + const left = screen.width / 2 - width / 2; + const top = screen.height / 2 - height / 2; + const loginWindow = window.open( + "/login?force_fda_sso_login=true&user_return_to=%2Fginas%2Fclose-pfda-login-window", + "pFDA Login", + `height=${height},width=${width},top=${top},left=${left}`, + ); + + return this.waitForPopupToClose(loginWindow).pipe( + concatMap(() => + this.getAuth().pipe( + map((authAfterLogin) => !!authAfterLogin), // Convert to boolean (true = success) + catchError(() => of(false)), // Return false if there's an error + ), + ), ); } @@ -109,58 +166,78 @@ get auth(): Auth { // Trigger a fetch if not loading and no auth yet if (this._auth == null && !this.isLoading) { this.isLoading = true; - this.fetchAuth().pipe(take(1)).subscribe({ - next: auth => { - this._auth = auth?.computedToken ? auth : null; - this._authUpdate.next(this._auth); - this.isLoading = false; - }, - error: () => { - this._auth = null; - this._authUpdate.next(null); - this.isLoading = false; - } - }); + this.fetchAuth() + .pipe(take(1)) + .subscribe({ + next: (auth) => { + this._auth = auth?.computedToken ? auth : null; + this._authUpdate.next(this._auth); + this.isLoading = false; + }, + error: () => { + this._auth = null; + this._authUpdate.next(null); + this.isLoading = false; + }, + }); } // Return the BehaviorSubject as observable - late subscribers get current value return this._authUpdate.asObservable(); } + private deleteCookie(name: string) { + document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/`; + } + logout(): void { this._privileges = []; if (isPlatformBrowser(this.platformId)) { - sessionStorage.removeItem('authToken'); - const cookies = document.cookie.split(';'); + sessionStorage.removeItem("authToken"); + const cookies = document.cookie.split(";"); for (const cookie of cookies) { - const eqPos = cookie.indexOf('='); + const eqPos = cookie.indexOf("="); const name = eqPos > -1 ? cookie.substr(0, eqPos) : cookie; - document.cookie = name + '=;expires=Thu, 01 Jan 1970 00:00:00 GMT'; + document.cookie = name + "=;expires=Thu, 01 Jan 1970 00:00:00 GMT"; } } - const url = `${this.configService.configData.apiBaseUrl}logout`; - this.http.get(url).subscribe(() => { - this._auth = null; - this._authUpdate.next(null); - }, error => { - this._auth = null; - this._authUpdate.next(null); - }); + let url = `${this.configService.configData.apiBaseUrl}logout`; + let method = "GET"; + + if (this.configService.configData.isPfdaVersion) { + url = "/logout"; + method = "DELETE"; + } + + this.http.request(method, url).subscribe( + () => { + this._auth = null; + this._authUpdate.next(null); + this.deleteCookie("sessionExpiredAt"); + }, + (error) => { + this._auth = null; + this._authUpdate.next(null); + this.deleteCookie("sessionExpiredAt"); + }, + ); } public getUser(): string { if (this._auth && this._auth.identifier) { return this._auth.identifier; } else { - return ''; + return ""; } } hasRoles(...roles: Array): boolean { const rolesList = [...roles]; - const checkableRoles = this._auth.roles.map((x: Role) => x.role.toUpperCase()); - + if (this._auth && this._auth.roles && rolesList && rolesList.length) { + const checkableRoles = this._auth.roles.map((x: Role) => + x.role.toUpperCase(), + ); for (const r of rolesList) { let role = r.toUpperCase(); if (checkableRoles.indexOf(role) === -1) { @@ -180,7 +257,7 @@ get auth(): Auth { for (const r of groupList) { let role = r.charAt(0).toLowerCase() + r.slice(1); role = role.charAt(0).toUpperCase() + role.slice(1); - this._auth.groups.forEach(group => { + this._auth.groups.forEach((group) => { if (group.name === role) { return true; } @@ -193,24 +270,29 @@ get auth(): Auth { return true; } - hasRolesAsync(...roles: Array< string>): Observable { - return new Observable(observer => { + hasRolesAsync(...roles: Array): Observable { + return new Observable((observer) => { if (this.auth != null) { observer.next(this.hasRoles(...roles)); observer.complete(); } else { - this.getAuth().pipe(take(1)).subscribe(auth => { - observer.next(this.hasRoles(...roles)); - observer.complete(); - }); + this.getAuth() + .pipe(take(1)) + .subscribe((auth) => { + observer.next(this.hasRoles(...roles)); + observer.complete(); + }); } }); } hasAnyRoles(...roles: Array): boolean { const rolesList = [...roles]; - const checkableRoles = this._auth.roles.map((x: Role) => x.role.toUpperCase()); + if (this._auth && this._auth.roles && rolesList && rolesList.length) { + const checkableRoles = this._auth.roles.map((x: Role) => + x.role.toUpperCase(), + ); for (const r of rolesList) { let role = r.toUpperCase(); if (checkableRoles.indexOf(role) === -1) { @@ -223,15 +305,19 @@ get auth(): Auth { return false; } - async canEditData( ):Promise { - return this.hasSpecificPrivilege('Edit'); + async canEditData(): Promise { + return this.hasSpecificPrivilege("Edit"); } - async hasAnyPrivilege(...privs) : Promise< boolean> { + async hasAnyPrivilege(...privs): Promise { await this.ensurePrivilegesLoaded(); - return privs.some(p=>this._privileges.some(pp=>pp.privilege.toUpperCase()== p.toUpperCase())); + return privs.some((p) => + this._privileges.some( + (pp) => pp.privilege.toUpperCase() == p.toUpperCase(), + ), + ); } - + private async ensurePrivilegesLoaded(): Promise { if (!this._privileges || this._privileges.length === 0) { try { @@ -243,48 +329,57 @@ get auth(): Auth { } } - async hasSpecificPrivilege(requestedPrivilege: string ):Promise { + async hasSpecificPrivilege(requestedPrivilege: string): Promise { await this.ensurePrivilegesLoaded(); - return this._privileges != null && this._privileges.some(p=>p.privilege==requestedPrivilege); + return ( + this._privileges != null && + this._privileges.some((p) => p.privilege == requestedPrivilege) + ); } hasAnyRolesAsync(...roles: Array): Observable { - return new Observable(observer => { + return new Observable((observer) => { if (this.auth != null) { observer.next(this.hasAnyRoles(...roles)); observer.complete(); } else { - this.getAuth().pipe(take(1)).subscribe(auth => { - observer.next(this.hasAnyRoles(...roles)); - observer.complete(); - }); + this.getAuth() + .pipe(take(1)) + .subscribe((auth) => { + observer.next(this.hasAnyRoles(...roles)); + observer.complete(); + }); } }); } - startUserDownload(fullUrl: string, privateExport: boolean, filename?: string, id?: string): Observable { - + startUserDownload( + fullUrl: string, + privateExport: boolean, + filename?: string, + id?: string, + ): Observable { let params = new HttpParams(); if (privateExport) { - params = params.append('publicOnly', 'false'); + params = params.append("publicOnly", "false"); } - if (filename && filename !== '') { - params = params.append('filename', filename); + if (filename && filename !== "") { + params = params.append("filename", filename); } if (id) { - params = params.append('exportConfigId', id); + params = params.append("exportConfigId", id); } const options = { - params: params + params: params, }; return this.http.get(fullUrl, options); } getUpdateStatus(id: string): Observable { - const url = `${(this.configService.configData && this.configService.configData.apiBaseUrl) || '/'}api/v1/`; + const url = `${(this.configService.configData && this.configService.configData.apiBaseUrl) || "/"}api/v1/`; return this.http.get(`${url}profile/downloads/${id}`); } @@ -293,55 +388,59 @@ get auth(): Auth { } deleteDownload(url: string): any { - return this.http.delete(url, { observe: 'response' }); + return this.http.delete(url, { observe: "response" }); } getAllDownloads(): Observable { - const url = `${(this.configService.configData && this.configService.configData.apiBaseUrl) || '/'}api/v1/`; + const url = `${(this.configService.configData && this.configService.configData.apiBaseUrl) || "/"}api/v1/`; return this.http.get(`${url}profile/downloads`); - } private fetchAuth(): Observable { - return new Observable(observer => { - this.configService.afterLoad().then(cd => { - const url = `${(this.configService.configData && this.configService.configData.apiBaseUrl) || '/'}api/v1/`; - if (this.configService.configData && this.configService.configData.dummyWhoami) { + return new Observable((observer) => { + this.configService.afterLoad().then((cd) => { + const url = `${(this.configService.configData && this.configService.configData.apiBaseUrl) || "/"}api/v1/`; + if ( + this.configService.configData && + this.configService.configData.dummyWhoami + ) { observer.next(this.configService.configData.dummyWhoami); } else { - this.http.get(`${url}whoami`) - .subscribe( - auth => { + this.http.get(`${url}whoami`).subscribe( + (auth) => { + // console.log("Authorized as"); + // console.log(auth); observer.next(auth); }, - err => { + (err) => { console.log("Authorized error"); observer.error(err); }, - () => observer.complete() + () => observer.complete(), ); } }); }); } - -private fetchPrivs(): Observable { - return from(this.configService.afterLoad()).pipe( - switchMap(() => { - const baseUrl = this.configService.configData?.apiBaseUrl || '/'; - const url = `${baseUrl}api/v1/allmyprivs`; - return this.http.get(url); - }), - map(response => { - const privs: Privilege[] = response.privileges.map(p => ({ privilege: p })); - this._privileges = privs; - return privs; - }), - catchError(err => { - console.error("Authorized error", err); - return throwError(() => err); - }) - ); -} + private fetchPrivs(): Observable { + return from(this.configService.afterLoad()).pipe( + switchMap(() => { + const baseUrl = this.configService.configData?.apiBaseUrl || "/"; + const url = `${baseUrl}api/v1/allmyprivs`; + return this.http.get(url); + }), + map((response) => { + const privs: Privilege[] = response.privileges.map((p) => ({ + privilege: p, + })); + this._privileges = privs; + return privs; + }), + catchError((err) => { + console.error("Authorized error", err); + return throwError(() => err); + }), + ); + } } diff --git a/src/app/core/auth/csrf-token.interceptor.ts b/src/app/core/auth/csrf-token.interceptor.ts index 597f28061..412d3b5c1 100644 --- a/src/app/core/auth/csrf-token.interceptor.ts +++ b/src/app/core/auth/csrf-token.interceptor.ts @@ -1,35 +1,37 @@ import { Injectable } from '@angular/core'; -import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent } from '@angular/common/http'; -import { Observable } from 'rxjs'; +import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent, HttpClient } from '@angular/common/http'; +import { from, Observable, switchMap } from 'rxjs'; +import { ConfigService } from "@gsrs-core/config"; @Injectable() export class CsrfTokenInterceptor implements HttpInterceptor { - - constructor() {} + constructor(private http: HttpClient, private configService: ConfigService) {} intercept(request: HttpRequest, next: HttpHandler): Observable> { - // CSRF token for GET and HEAD is not needed - if (['GET', 'HEAD'].includes(request.method)) { + // CSRF token request needed in pFDA version only + if (['GET', 'HEAD'].includes(request.method) + || !(this.configService.configData?.isPfdaVersion)) { return next.handle(request); } - // Parse CSRF token from HTML meta tag - const metaTag: HTMLMetaElement | null = document.querySelector('meta[name=csrf-token]'); - let csrfToken = metaTag?.content; - if (csrfToken === undefined) { - csrfToken = 'CSRF-TOKEN-NOT-PARSED'; - } + return from(this.fetchCsrfToken()).pipe( + switchMap((token: string) => { + const modifiedRequest = this.addCsrfToken(request, token); + return next.handle(modifiedRequest); + }) + ); + } - // Clone the request and add the CSRF token to the headers - const modifiedRequest = request.clone({ + private fetchCsrfToken(): Promise { + return this.http.get(`/csrf-token`, { responseType: 'text' }).toPromise(); + } + + private addCsrfToken(request: HttpRequest, token: string): HttpRequest { + return request.clone({ setHeaders: { - // eslint-disable-next-line @typescript-eslint/naming-convention - 'X-CSRF-Token': csrfToken + 'X-CSRF-Token': token } }); - - // Pass the modified request to the next handler - return next.handle(modifiedRequest); } } diff --git a/src/app/core/auth/session-expiration/session-expiration-dialog/session-expiration-dialog.component.html b/src/app/core/auth/session-expiration/session-expiration-dialog/session-expiration-dialog.component.html index c35a6869c..2903ea40f 100644 --- a/src/app/core/auth/session-expiration/session-expiration-dialog/session-expiration-dialog.component.html +++ b/src/app/core/auth/session-expiration/session-expiration-dialog/session-expiration-dialog.component.html @@ -6,6 +6,7 @@

+
diff --git a/src/app/core/auth/session-expiration/session-expiration-dialog/session-expiration-dialog.component.ts b/src/app/core/auth/session-expiration/session-expiration-dialog/session-expiration-dialog.component.ts index 4ceec2311..b46d43a49 100644 --- a/src/app/core/auth/session-expiration/session-expiration-dialog/session-expiration-dialog.component.ts +++ b/src/app/core/auth/session-expiration/session-expiration-dialog/session-expiration-dialog.component.ts @@ -2,8 +2,9 @@ import { Component, OnInit, Inject } from '@angular/core'; import { Router } from '@angular/router'; import { HttpClient } from '@angular/common/http'; import { ConfigService, SessionExpirationWarning } from '@gsrs-core/config'; -import { MatDialog, MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; -import { AnyNsRecord } from 'dns'; +import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; +import { AuthService } from '@gsrs-core/auth'; +import { concatMap } from "rxjs" @Component({ selector: 'app-session-expiration-dialog', @@ -24,7 +25,9 @@ export class SessionExpirationDialogComponent implements OnInit { @Inject(MAT_DIALOG_DATA) public data: any, // N.B. injected services has to come after data private router: Router, - private http: HttpClient + private http: HttpClient, + private authService: AuthService, + public configService: ConfigService ) { this.sessionExpirationWarning = data.sessionExpirationWarning; this.sessionExpiringAt = data.sessionExpiringAt; @@ -50,11 +53,10 @@ export class SessionExpirationDialogComponent implements OnInit { if (this.timeRemainingSeconds > 0) { const remainingMinutes = Math.floor(this.timeRemainingSeconds / 60); - const reminaingSeconds = String(this.timeRemainingSeconds % 60).padStart(2, '0'); + const remainingSeconds = String(this.timeRemainingSeconds % 60).padStart(2, '0'); this.dialogTitle = "Session Ending Soon" - this.dialogMessage = `You will be logged out in ${remainingMinutes}:${reminaingSeconds}` - } - else { + this.dialogMessage = `You will be logged out in ${remainingMinutes}:${remainingSeconds}` + } else { this.dialogTitle = "Session Ended" this.dialogMessage = "Your session has expired, please login again." } @@ -76,6 +78,22 @@ export class SessionExpirationDialogComponent implements OnInit { } login() { - window.location.assign('/login'); + if (this.configService.configData.isPfdaVersion) { + this.authService.pfdaLogin().pipe( + concatMap(success => { + if (success) { + this.closeDialog(); + return this.authService.getAuth(); + } + })).subscribe(); + } else { + window.location.assign('/login'); + } + } + + proceedAsGuest() { + clearInterval(this.updateDialogInterval); + this.authService.logout(); + this.closeDialog(); } } diff --git a/src/app/core/auth/session-expiration/session-expiration.component.ts b/src/app/core/auth/session-expiration/session-expiration.component.ts index 5c9b7263f..d7d574855 100644 --- a/src/app/core/auth/session-expiration/session-expiration.component.ts +++ b/src/app/core/auth/session-expiration/session-expiration.component.ts @@ -1,12 +1,10 @@ -import { Router, Event as NavigationEvent, NavigationStart } from '@angular/router'; import { Component, OnInit } from '@angular/core'; import { OverlayContainer } from '@angular/cdk/overlay'; -import { HttpClient } from '@angular/common/http'; import { ConfigService, SessionExpirationWarning } from '@gsrs-core/config'; import { AuthService } from '../auth.service'; import { SessionExpirationDialogComponent } from './session-expiration-dialog/session-expiration-dialog.component' -import { Subscription } from 'rxjs'; -import { MatDialog } from '@angular/material/dialog'; +import { MatDialog, MatDialogRef, MatDialogState } from '@angular/material/dialog'; +import { UtilsService } from "@gsrs-core/utils"; @Component({ selector: 'app-session-expiration', @@ -17,85 +15,133 @@ export class SessionExpirationComponent implements OnInit { sessionExpirationWarning: SessionExpirationWarning = null; sessionExpiringAt: number; private overlayContainer: HTMLElement; - private subscriptions: Array = []; - private expirationTimer: any; + private refreshInterval: any; + private activityRefreshInterval: any; + private userActive: boolean = false; + private baseHref: string = '/ginas/app/'; + private extendSessionDialog: MatDialogRef; + + private static instance?: SessionExpirationComponent = undefined; + private static sessionExpirationCheckInterval = null; constructor( - private router: Router, private configService: ConfigService, private authService: AuthService, - private http: HttpClient, - private dialog: MatDialog, - private overlayContainerService: OverlayContainer + private matDialog: MatDialog, + private overlayContainerService: OverlayContainer, + private utilsService: UtilsService ) { + if (SessionExpirationComponent.instance !== undefined) { + return SessionExpirationComponent.instance; + } this.sessionExpirationWarning = configService.configData.sessionExpirationWarning; this.overlayContainer = this.overlayContainerService.getContainerElement(); } ngOnInit() { - // If SessionExpirationWarning is not found in configData, the intervals are never set - // and this component is inert - const authSubscription = this.authService.getAuth().subscribe(auth => { - if (this.sessionExpirationWarning) { - if (auth) { - this.resetExpirationTimer(); - } - else { - // User has logged out while timeout is active - this.clearExpirationTimer(); - } + if (SessionExpirationComponent.instance !== undefined) { + return; + } + SessionExpirationComponent.instance = this; + + const homeBaseUrl = this.configService.configData && this.configService.configData.gsrsHomeBaseUrl || null; + if (homeBaseUrl) { + this.baseHref = homeBaseUrl; + } + + this.startSessionTimeoutInterval(); + } + + setup() { + this.configService.afterLoad().then(cd => { + // If enabled in config file, this functionality periodically checks whether there was a user activity (mouse or keyboard) or not + // In case there was some activity, the session is refreshed (otherwise the session is not refreshed and may eventually expire) + if (this.configService.configData.sessionRefreshOnActiveUserOnly) { + const page = document.getElementsByTagName('body')[0]; + page.addEventListener('mousemove', (e) => { + if (e instanceof MouseEvent) { + this.userActive = true; + } + }); + page.addEventListener('keydown', (e) => { + if (e instanceof KeyboardEvent) { + this.userActive = true; + } + }); + clearInterval(this.activityRefreshInterval); + this.activityRefreshInterval = setInterval(() => { + if (this.userActive) { + this.refreshSession(); + this.userActive = false; + } + }, 10000); + } else { + clearInterval(this.refreshInterval); + this.refreshInterval = setInterval(() => { + this.refreshSession(); + }, 600000); } }); - this.subscriptions.push(authSubscription); - - // This component seems to be destroyed and recreated on route change, so maybe - // the following isn't necessary: - // const routerSubscription = this.router.events.subscribe((event: NavigationEvent) => { - // if (event instanceof NavigationStart && this.expirationTimer) { - // this.extendSession(); - // } - // }); - // this.subscriptions.push(routerSubscription); } - ngOnDestroy() { - this.subscriptions.forEach(subscription => { - subscription.unsubscribe(); - }); - this.clearExpirationTimer(); + refreshSession(): any { + fetch(`${this.baseHref || ''}api/v1/whoami?key=${this.utilsService.newUUID()}`); } - getCurrentTime() { - return Math.floor((new Date()).getTime() / 1000); + startSessionTimeoutInterval() { + this.authService.getAuth().subscribe(auth => { + if (auth != null && this.refreshInterval == null) { + this.setup(); + } else if (auth === null) { + clearInterval(this.refreshInterval); + this.refreshInterval = null; + } + }); + + clearInterval(SessionExpirationComponent.sessionExpirationCheckInterval); + SessionExpirationComponent.sessionExpirationCheckInterval = setInterval(() => { + this.sessionExpiringAt = this.getSessionExpiredAt(); + const currentTime = this.getCurrentTime(); + const sessionTtl = this.sessionExpiringAt - currentTime; + // If session is about to expire in less than 60 seconds, show dialog window + if (sessionTtl > 0 && sessionTtl < 60) { + if (!this.isDialogOpened()) { + this.openDialog(); + } + // Do not automatically (mouse/keyboard event) extend session when the dialog is opened + clearInterval(this.activityRefreshInterval); + } else if (this.sessionExpiringAt !== null && sessionTtl > 0) { + // The session was externally extended (eg. in pfda) -> close the session dialog + if (this.isDialogOpened()) { + this.extendSessionDialog.close(); + } + } + }, 5000) } - clearExpirationTimer() { - if (this.expirationTimer) { - clearTimeout(this.expirationTimer); - this.expirationTimer = null; + private getCookie(name: string) { + const cookieArr = document.cookie.split(';') + for (let i = 0; i < cookieArr.length; i++) { + const cookiePair = cookieArr[i].split('=') + if (name === cookiePair[0].trim()) { + return decodeURIComponent(cookiePair[1]) + } } + return null } - resetExpirationTimer() { - this.clearExpirationTimer(); - - const currentTime = this.getCurrentTime() - this.sessionExpiringAt = currentTime + this.sessionExpirationWarning.maxSessionDurationMinutes * 60; + private getSessionExpiredAt() { + const cookie = this.getCookie('sessionExpiredAt') + if (!cookie) return null + return parseInt(cookie) + } - const timeRemainingSeconds = this.sessionExpiringAt - currentTime; - const timeBeforeDisplayingDialogMs = (timeRemainingSeconds - 61) * 1000; - if (timeBeforeDisplayingDialogMs > 0) { - this.expirationTimer = setTimeout( () => { - this.openDialog(); - }, timeBeforeDisplayingDialogMs); - } - else { - this.login(); - } + getCurrentTime() { + return Math.floor((new Date()).getTime() / 1000); } openDialog() { - const dialogRef = this.dialog.open(SessionExpirationDialogComponent, { + this.extendSessionDialog = this.matDialog.open(SessionExpirationDialogComponent, { data: { 'sessionExpirationWarning': this.sessionExpirationWarning, 'sessionExpiringAt': this.sessionExpiringAt @@ -105,27 +151,17 @@ export class SessionExpirationComponent implements OnInit { disableClose: true }); this.overlayContainer.style.zIndex = '1501'; - const dialogSubscription = dialogRef.afterClosed().subscribe(response => { + this.extendSessionDialog.afterClosed().subscribe(response => { this.overlayContainer.style.zIndex = null; - if (response) { - // Session was extended - this.resetExpirationTimer(); - } + this.startSessionTimeoutInterval(); }); } - extendSession() { - const url = this.sessionExpirationWarning.extendSessionApiUrl; - this.http.get(url).subscribe( - data => { - this.resetExpirationTimer(); - }, - err => { console.log("Error extending session: ", err) }, - () => { } - ); - } - login() { window.location.assign('/login'); } + + isDialogOpened(): boolean { + return this.extendSessionDialog && this.extendSessionDialog.getState() === MatDialogState.OPEN; + } } diff --git a/src/app/core/auth/user-downloads/download-monitor/download-monitor.component.html b/src/app/core/auth/user-downloads/download-monitor/download-monitor.component.html index 065e72d03..9b79e0faa 100644 --- a/src/app/core/auth/user-downloads/download-monitor/download-monitor.component.html +++ b/src/app/core/auth/user-downloads/download-monitor/download-monitor.component.html @@ -126,7 +126,7 @@
- diff --git a/src/app/core/auth/user-downloads/download-monitor/download-monitor.component.ts b/src/app/core/auth/user-downloads/download-monitor/download-monitor.component.ts index f3809df13..21b6ffd30 100644 --- a/src/app/core/auth/user-downloads/download-monitor/download-monitor.component.ts +++ b/src/app/core/auth/user-downloads/download-monitor/download-monitor.component.ts @@ -80,7 +80,7 @@ export class DownloadMonitorComponent implements OnInit, OnDestroy { } deleteDownload() { - this.authService.deleteDownload(this.download.removeUrl.url).pipe(take(1)).subscribe(response => { + this.authService.deleteDownload(this.download.removeUrl?.url || this.download.cancelUrl.url.replace('/@cancel', '')).pipe(take(1)).subscribe(response => { this.deleted = true; }); } diff --git a/src/app/core/base/base.component.html b/src/app/core/base/base.component.html index b0d7f404c..d57096e33 100644 --- a/src/app/core/base/base.component.html +++ b/src/app/core/base/base.component.html @@ -1,8 +1,9 @@ @@ -388,10 +389,9 @@
-
+
- diff --git a/src/app/core/base/base.component.ts b/src/app/core/base/base.component.ts index a3fda4181..c893de771 100644 --- a/src/app/core/base/base.component.ts +++ b/src/app/core/base/base.component.ts @@ -66,6 +66,7 @@ export class BaseComponent implements OnInit, OnDestroy { appId: string; clasicBaseHref: string; navItems: Array; + isPfdaVersion: boolean = false; customToolbarComponent: string = ""; canRegister = false; registerNav: Array; @@ -95,6 +96,7 @@ export class BaseComponent implements OnInit, OnDestroy { private utilsService: UtilsService, private wildCardService: WildcardService, ) { + this.isPfdaVersion = this.configService.configData.isPfdaVersion === true; this.customToolbarComponent = this.configService.configData.customToolbarComponent; this.wildCardService.wildCardObservable.subscribe((data) => { this.wildCardText = data; diff --git a/src/app/core/base/pfda-toolbar/pfda-toolbar.component.html b/src/app/core/base/pfda-toolbar/pfda-toolbar.component.html index 73679e219..7c94c2c06 100644 --- a/src/app/core/base/pfda-toolbar/pfda-toolbar.component.html +++ b/src/app/core/base/pfda-toolbar/pfda-toolbar.component.html @@ -42,11 +42,12 @@ - +
Support
@@ -61,12 +62,12 @@
- +
Login
-
+ diff --git a/src/app/core/base/pfda-toolbar/pfda-toolbar.component.scss b/src/app/core/base/pfda-toolbar/pfda-toolbar.component.scss index 5fd25316e..ce2509a0c 100644 --- a/src/app/core/base/pfda-toolbar/pfda-toolbar.component.scss +++ b/src/app/core/base/pfda-toolbar/pfda-toolbar.component.scss @@ -10,7 +10,7 @@ $screenMedium: 1045px; .pfda-toolbar { - background-color: $pfda-navbar-blue; + background-color: $pfda-navbar-blue !important; color: white; font-family: "Lato","Helvetica Neue",Helvetica,Arial,sans-serif; font-size: 13px; @@ -47,6 +47,7 @@ $screenMedium: 1045px; align-items: center; justify-content: center; padding: 10px 6px; + cursor: pointer; &:hover { color: $pfda-navbar-item-hover; diff --git a/src/app/core/base/pfda-toolbar/pfda-toolbar.component.ts b/src/app/core/base/pfda-toolbar/pfda-toolbar.component.ts index 59fbd2a4b..f495ba6ca 100644 --- a/src/app/core/base/pfda-toolbar/pfda-toolbar.component.ts +++ b/src/app/core/base/pfda-toolbar/pfda-toolbar.component.ts @@ -5,7 +5,7 @@ import { OverlayContainer } from '@angular/cdk/overlay'; import { AuthService } from '../../auth/auth.service'; import { SubstanceTextSearchService } from '@gsrs-core/substance-text-search/substance-text-search.service'; import { Auth } from '../../auth/auth.model'; -import { Subscription } from 'rxjs'; +import { concatMap, Subscription } from 'rxjs'; import { NavItem } from '@gsrs-core/config'; @Component({ @@ -16,6 +16,7 @@ import { NavItem } from '@gsrs-core/config'; }) export class PfdaToolbarComponent implements OnInit { pfdaBaseUrl: string; + supportEmail: string; logoSrcPath: string; homeIconPath: string; auth?: Auth; @@ -41,9 +42,10 @@ export class PfdaToolbarComponent implements OnInit { ngOnInit() { this.pfdaBaseUrl = this.configService.configData.pfdaBaseUrl || '/'; - const baseHref = this.configService.environment.baseHref || '/' + const baseHref = this.configService.environment.baseHref || '/ginas/app/beta/'; this.logoSrcPath = `${baseHref}assets/images/pfda/pfda-logo.png`; this.homeIconPath = `${baseHref}assets/images/pfda/home.svg`; + this.supportEmail = this.configService.configData.contactEmail || 'fda-srs@fda.hhs.gov'; this.overlayContainer = this.overlayContainerService.getContainerElement(); @@ -88,4 +90,15 @@ export class PfdaToolbarComponent implements OnInit { removeZindex(): void { this.overlayContainer.style.zIndex = null; } + + login(): void { + this.authService.pfdaLogin().pipe( + concatMap(success => { + return this.authService.getAuth(); + })).subscribe(); + } + + logout(): void { + this.authService.logout(); + } } diff --git a/src/app/core/config/config.model.ts b/src/app/core/config/config.model.ts index e737a7efa..b1f14246c 100644 --- a/src/app/core/config/config.model.ts +++ b/src/app/core/config/config.model.ts @@ -38,8 +38,11 @@ export interface Config { advancedSearchFacetDisplay?: boolean; facetDisplay?: Array; relationshipsVisualizationUri?: string; + isPfdaVersion?: boolean; customToolbarComponent?: string; + ssg4mExportSvg?: boolean; disableSessionRefresh?: boolean; + sessionRefreshOnActiveUserOnly?: boolean; sessionExpirationWarning?: SessionExpirationWarning; disableReferenceDocumentUpload?: boolean; externalSiteWarning?: ExternalSiteWarning; @@ -196,4 +199,3 @@ export interface DownloadAsPDF { export interface roleSortConfig { [key: string]: number; } - diff --git a/src/app/core/config/config.pfda.json b/src/app/core/config/config.pfda.json index 009b4f017..12045ea9e 100644 --- a/src/app/core/config/config.pfda.json +++ b/src/app/core/config/config.pfda.json @@ -532,7 +532,7 @@ "root_codes_CAS", "root_codes_ECHA" ], - "contactEmail": "precisionfda-support@dnanexus.com", + "contactEmail": "fda-srs@fda.hhs.gov", "sessionExpirationWarning": { "extendSessionApiUrl": "/api/update_active", "maxSessionDurationMinutes": 15 @@ -544,5 +544,5 @@ "dialogMessage" : "You will be making an API call outside of the precisionFDA boundary. Do you want to continue?" }, "googleAnalyticsId": "", - "customToolbarComponent": "precisionFDA" -} \ No newline at end of file + "isPfdaVersion": true +} diff --git a/src/app/core/home/home.component.html b/src/app/core/home/home.component.html index c261896f0..d5efd9a44 100644 --- a/src/app/core/home/home.component.html +++ b/src/app/core/home/home.component.html @@ -185,6 +185,14 @@ Specified Substance Group 1 + + + + G4 Specified Substance Manufacturing + diff --git a/src/app/core/home/home.component.ts b/src/app/core/home/home.component.ts index 173333cf5..136e681f8 100644 --- a/src/app/core/home/home.component.ts +++ b/src/app/core/home/home.component.ts @@ -35,7 +35,7 @@ export class HomeComponent implements OnInit, AfterViewInit, OnDestroy { imageLoc: any; appId: string; customLinks: Array; - total: string; + total: number; isCollapsed = true; hasBackdrop = false; bannerMessage?: string; @@ -127,7 +127,7 @@ export class HomeComponent implements OnInit, AfterViewInit, OnDestroy { }); }); this.substanceService.getRecordCount().subscribe((response) => { - this.total = response; + this.total = parseInt(response); }); // this.isClosedWelcomeMessage = localStorage.getItem('isClosedWelcomeMessage') === 'false'; this.isClosedWelcomeMessage = false; diff --git a/src/app/core/main-notification/main-notification.service.ts b/src/app/core/main-notification/main-notification.service.ts index 7531b5c39..26dcad2c7 100644 --- a/src/app/core/main-notification/main-notification.service.ts +++ b/src/app/core/main-notification/main-notification.service.ts @@ -1,6 +1,6 @@ import { Injectable } from '@angular/core'; import { Subject } from 'rxjs'; -import { AppNotification } from './notification.model'; +import { AppNotification, NotificationType } from './notification.model'; @Injectable() export class MainNotificationService { @@ -11,4 +11,20 @@ export class MainNotificationService { setNotification(notification: AppNotification): void { this.notificationEvent.next(notification); } + + setSuccessNotification(message: string, duration?: number): void { + this.setNotification({ + message: message, + type: NotificationType.success, + milisecondsToShow: duration ? duration : 4000 + }) + } + + setErrorNotification(message: string, duration?: number): void { + this.setNotification({ + message: message, + type: NotificationType.error, + milisecondsToShow: duration ? duration : 0 + }) + } } diff --git a/src/app/core/main-notification/main-notification/main-notification.component.html b/src/app/core/main-notification/main-notification/main-notification.component.html index db47d4519..ced635246 100644 --- a/src/app/core/main-notification/main-notification/main-notification.component.html +++ b/src/app/core/main-notification/main-notification/main-notification.component.html @@ -1,3 +1,4 @@ \ No newline at end of file +
diff --git a/src/app/core/main-notification/main-notification/main-notification.component.scss b/src/app/core/main-notification/main-notification/main-notification.component.scss index 6f3ed1f96..352a4511d 100644 --- a/src/app/core/main-notification/main-notification/main-notification.component.scss +++ b/src/app/core/main-notification/main-notification/main-notification.component.scss @@ -4,7 +4,7 @@ width: 100%; text-align: center; position: fixed; - z-index: 100; + z-index: 2000; transition: all 200ms ease; overflow: hidden; box-sizing: border-box; @@ -16,7 +16,7 @@ height: 0; padding: 0; } - + &.showing { height: auto; box-shadow: 0px 3px 3px -2px rgba(0, 0, 0, 0.2), 0px 3px 4px 0px rgba(0, 0, 0, 0.14); @@ -36,4 +36,12 @@ background-color: var(--notif-error-bg-color); color: var(--regular-black-color); } + + >.close-link { + float: right; + text-decoration: underline; + font-style: italic; + font-size: 0.9em; + margin: 3px 10px; + } } diff --git a/src/app/core/main-notification/main-notification/main-notification.component.ts b/src/app/core/main-notification/main-notification/main-notification.component.ts index 00ad00a20..ee5747578 100644 --- a/src/app/core/main-notification/main-notification/main-notification.component.ts +++ b/src/app/core/main-notification/main-notification/main-notification.component.ts @@ -35,20 +35,28 @@ export class MainNotificationComponent implements OnInit, OnDestroy { clearTimeout(this.notificationTimer); } + // If notification.milisecondsToShow === 0, the notification is permanent (until closed by user) setNotification(notification: AppNotification): void { this.notifcationType = notification.type || NotificationType.default; this.notificationMessage = notification.message; this.appNotification.nativeElement.classList.remove('hidden'); this.appNotification.nativeElement.classList.add(NotificationType[this.notifcationType]); this.appNotification.nativeElement.classList.add('showing'); - const timeout = notification.milisecondsToShow || 5000; - this.notificationTimer = setTimeout(() => { - this.removeNotification(notification.type); + if (notification.milisecondsToShow === 0) { + if (this.notificationTimer != null) { + clearTimeout(this.notificationTimer); + } this.notificationTimer = null; - }, timeout); + } else { + const timeout = notification.milisecondsToShow || 5000; + this.notificationTimer = setTimeout(() => { + this.removeNotification(); + this.notificationTimer = null; + }, timeout); + } } - removeNotification(notificationType: NotificationType): void { + removeNotification(): void { if (this.notificationTimer != null) { clearTimeout(this.notificationTimer); } @@ -56,5 +64,4 @@ export class MainNotificationComponent implements OnInit, OnDestroy { this.appNotification.nativeElement.classList.add('hidden'); this.appNotification.nativeElement.classList.remove(NotificationType[this.notifcationType]); } - } diff --git a/src/app/core/structure/structure-image-modal/structure-image-modal.component.html b/src/app/core/structure/structure-image-modal/structure-image-modal.component.html index 22dedf3eb..e915a7e40 100644 --- a/src/app/core/structure/structure-image-modal/structure-image-modal.component.html +++ b/src/app/core/structure/structure-image-modal/structure-image-modal.component.html @@ -68,7 +68,7 @@
-
+
Substance Details Substance Edit diff --git a/src/app/core/structure/structure-image-modal/structure-image-modal.component.ts b/src/app/core/structure/structure-image-modal/structure-image-modal.component.ts index 6a3a0b3b2..47be6598b 100644 --- a/src/app/core/structure/structure-image-modal/structure-image-modal.component.ts +++ b/src/app/core/structure/structure-image-modal/structure-image-modal.component.ts @@ -25,6 +25,7 @@ export class StructureImageModalComponent implements OnInit { showSubstanceSelector = false; gsrsHomeBaseUrl = ''; inchiNote = false; + isDraft = false; constructor( private configService: ConfigService, @@ -79,6 +80,8 @@ export class StructureImageModalComponent implements OnInit { if (this.data.component && this.data.component === 'substanceSelector') { this.showSubstanceSelector = true; } + + this.isDraft = this.data && this.data.isDraft ? this.data.isDraft : false; } dismissDialog(): void { diff --git a/src/app/core/substance-details/substance-codes/substance-codes.component.html b/src/app/core/substance-details/substance-codes/substance-codes.component.html index f41966a00..01f7e03ff 100644 --- a/src/app/core/substance-details/substance-codes/substance-codes.component.html +++ b/src/app/core/substance-details/substance-codes/substance-codes.component.html @@ -136,7 +136,6 @@

- -
Select a .json file to import into a new edit form.
-
-
- -
-
- {{ filename ? filename : "no file chosen" }} -
- -
-
-
Or paste JSON here:
- -
+ + + +
Select a .json file to import into a new edit form.
+
+
+ +
+
+ {{ filename ? filename : "no file chosen" }} +
+ +
+
+ +
+
Paste JSON here:
+ +
+
+ +
+
URL:
+ +
+ Note: The URL needs to be publicly accessible +
+
+
+
{{ message }}
+ @@ -51,7 +72,7 @@

{{ title }}

mat-flat-button color="primary" [disabled]="!loaded" - (click)="useFile()" + (click)="importSubstance()" > Import diff --git a/src/app/core/substance-edit-import-dialog/substance-edit-import-dialog.component.ts b/src/app/core/substance-edit-import-dialog/substance-edit-import-dialog.component.ts index 7cb021005..b890b8537 100644 --- a/src/app/core/substance-edit-import-dialog/substance-edit-import-dialog.component.ts +++ b/src/app/core/substance-edit-import-dialog/substance-edit-import-dialog.component.ts @@ -1,6 +1,8 @@ import { Component, OnInit, Inject } from '@angular/core'; -import { MatDialogRef, MAT_DIALOG_DATA, MatDialog } from '@angular/material/dialog'; +import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; import { Router } from '@angular/router'; +import { MatTabChangeEvent } from '@angular/material/tabs'; +import { ConfigService } from '@gsrs-core/config'; @Component({ selector: 'app-substance-edit-import-dialog', @@ -15,15 +17,21 @@ export class SubstanceEditImportDialogComponent implements OnInit { record: any; filename: string; pastedJSON: string; - uploaded = false; + pastedUrl: string; title = 'Substance Import'; entity = 'Substance'; + currentTab: number = 0; + urlImportEnabled: boolean = false; + constructor( private router: Router, + private configService: ConfigService, public dialogRef: MatDialogRef, @Inject(MAT_DIALOG_DATA) public data: any - ) { } + ) { + this.urlImportEnabled = this.configService.configData.isPfdaVersion; + } ngOnInit() { if (this.data) { @@ -60,33 +68,72 @@ export class SubstanceEditImportDialogComponent implements OnInit { } }; reader.readAsText(event.target.files[0]); - this.uploaded = true; } } - useFile() { - if (!this.uploaded && this.pastedJSON) { + importSubstance() { + if (this.currentTab === 0) { + // Nothing + this.dialogRef.close(this.record); + } else if (this.currentTab === 1) { const read = JSON.parse(this.pastedJSON); - // If there is no substanceClass field in Substance JSON data if (!read['substanceClass']) { - // if JSON data is from non-substance entity, read the json from the textbox - if (read['id']) { - this.loaded = true; - this.record = this.pastedJSON; - this.message = ''; - } else { - this.message = 'Error: Invalid JSON format'; - this.loaded = false; - } + this.message = 'Error: Invalid JSON format'; + this.loaded = false; } else { this.loaded = true; this.record = this.pastedJSON; this.message = ''; + this.dialogRef.close(this.record); } + } else if (this.currentTab === 2) { + fetch(`/reverse-proxy?url=${this.pastedUrl}`).then(r => { + if (r.status !== 200) { + r.json().then(data => { + this.message = data.message ? data.message : 'Error while loading given URL'; + }).catch(_e => { + this.message = 'Error while loading given URL'; + }) + } else { + const json = r.text().then(data => { + try { + JSON.parse(data); + this.record = data; + this.dialogRef.close(this.record); + } catch (_e) { + this.message = 'Error: The URL does not point to a valid JSON file' + } + }); + } + }).catch(e => { + this.message = `Error: ${e.message}`; + }) } - this.dialogRef.close(this.record); } + // Is this method still used anywhere? + // useFile() { + // if (!this.uploaded && this.pastedJSON) { + // const read = JSON.parse(this.pastedJSON); + // // If there is no substanceClass field in Substance JSON data + // if (!read['substanceClass']) { + // // if JSON data is from non-substance entity, read the json from the textbox + // if (read['id']) { + // this.loaded = true; + // this.record = this.pastedJSON; + // this.message = ''; + // } else { + // this.message = 'Error: Invalid JSON format'; + // this.loaded = false; + // } + // } else { + // this.loaded = true; + // this.record = this.pastedJSON; + // this.message = ''; + // } + // + // } + // } checkLoaded() { this.loaded = true; @@ -99,6 +146,17 @@ export class SubstanceEditImportDialogComponent implements OnInit { } } + checkUrl() { + try { + new URL(this.pastedUrl); + this.loaded = true; + this.message = ''; + } catch (_e) { + this.message = 'Invalid URL'; + this.loaded = false; + } + } + openInput(): void { document.getElementById('fileInput').click(); @@ -113,4 +171,15 @@ export class SubstanceEditImportDialogComponent implements OnInit { return true; } + tabChanged(tabChangeEvent: MatTabChangeEvent) { + if (this.currentTab !== tabChangeEvent.index) { + this.currentTab = tabChangeEvent.index; + this.message = ''; + this.loaded = false; + this.record = ''; + this.pastedJSON = ''; + this.pastedUrl = ''; + this.filename = ''; + } + } } diff --git a/src/app/core/substance-form/can-register-substance-form.ts b/src/app/core/substance-form/can-register-substance-form.ts index 4980ebecd..2d708f24a 100644 --- a/src/app/core/substance-form/can-register-substance-form.ts +++ b/src/app/core/substance-form/can-register-substance-form.ts @@ -1,35 +1,49 @@ import { Injectable } from '@angular/core'; import { Router, CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, NavigationExtras, UrlTree } from '@angular/router'; import { AuthService } from '../auth/auth.service'; +import { Observable } from 'rxjs'; +import { ConfigService } from "@gsrs-core/config"; @Injectable() export class CanRegisterSubstanceForm implements CanActivate { constructor( private router: Router, - private authService: AuthService + private authService: AuthService, + private configService: ConfigService ) {} - async canActivate( + canActivate( route: ActivatedRouteSnapshot, state: RouterStateSnapshot - ): Promise { - - const auth = this.authService.getAuth(); - if (auth) { - const canCreate =await this.authService.hasSpecificPrivilege('Create'); - if(canCreate){ - return true; - }else { - return this.router.parseUrl('/browse-substance'); - } - } else { - const navigationExtras: NavigationExtras = { - queryParams: { + ): Observable | Promise | (boolean | UrlTree) { + return new Observable(observer => { + if (this.configService.configData.isPfdaVersion) { + observer.next(true); + observer.complete(); + } else { + this.authService.getAuth().subscribe(auth => { + if (auth) { + this.authService.hasAnyRolesAsync('DataEntry', 'SuperDataEntry').subscribe(response => { + if (response) { + observer.next(true); + observer.complete(); + } else { + observer.next(this.router.parseUrl('/browse-substance')); + observer.complete(); + } + }); + } else { + const navigationExtras: NavigationExtras = { + queryParams: { path: state.url - } - }; - return this.router.createUrlTree(['/login'], navigationExtras); - } + } + }; + observer.next(this.router.createUrlTree(['/login'], navigationExtras)); + observer.complete(); + } + }); + } + }); } } diff --git a/src/app/core/substance-form/constituents/substance-form-constituents-card.component.ts b/src/app/core/substance-form/constituents/substance-form-constituents-card.component.ts index caa842191..6586238fa 100644 --- a/src/app/core/substance-form/constituents/substance-form-constituents-card.component.ts +++ b/src/app/core/substance-form/constituents/substance-form-constituents-card.component.ts @@ -79,7 +79,7 @@ export class SubstanceFormConstituentsCardComponent extends SubstanceCardBaseFil this.formulationPercent = 0; this.components = 0; this.constituents.forEach(constituent => { - if(constituent && constituent.amount && constituent.amount.type === "WEIGHT PERCENT" + if(constituent && constituent.amount && constituent.amount.type === "WEIGHT PERCENT" && constituent.amount.units === "%" && constituent.amount.average) { this.formulationPercent = parseFloat(this.formulationPercent.toString()) + parseFloat(constituent.amount.average.toString()); this.components++; diff --git a/src/app/core/substance-form/json-dialog/json-dialog.component.ts b/src/app/core/substance-form/json-dialog/json-dialog.component.ts index 4786ed1f7..e6793eb5b 100644 --- a/src/app/core/substance-form/json-dialog/json-dialog.component.ts +++ b/src/app/core/substance-form/json-dialog/json-dialog.component.ts @@ -24,9 +24,13 @@ export class JsonDialogComponent implements OnInit { ) { } ngOnInit() { - // apply the same cleaning to remove deleted objects and return what will be sent to the server on validation / submission - this.json = this.substanceFormService.cleanSubstance(); - // this.json = this.cleanObject(substanceCopy); + // Use passed data if available, otherwise get from service + if (this.data && this.data.substance) { + this.json = this.data.substance; + } else { + // apply the same cleaning to remove deleted objects and return what will be sent to the server on validation / submission + this.json = this.substanceFormService.cleanSubstance(); + } const uri = this.sanitizer.bypassSecurityTrustUrl('data:text/json;charset=UTF-8,' + encodeURIComponent(JSON.stringify(this.json))); this.downloadJsonHref = uri; } diff --git a/src/app/core/substance-form/substance-drafts/substance-drafts.component.html b/src/app/core/substance-form/substance-drafts/substance-drafts.component.html index 1be61e935..d563ae831 100644 --- a/src/app/core/substance-form/substance-drafts/substance-drafts.component.html +++ b/src/app/core/substance-form/substance-drafts/substance-drafts.component.html @@ -1,6 +1,8 @@
-
Saved Drafts
+
Saved Drafts
+
Drafts Validation
+
Submission Results
- - - - - - - - - - - - - - + Show only current record + + - - - - - - - - - - - - - - -
Delete - - Type{{ draft.type }}Name{{ draft.name }}Date Saved - {{ draft.date | date : "medium" }} [AUTOSAVE]
- {{ draft.fromNow }}
+ -
UUID{{ draft.uuid }}Use - -
-
- No drafts were found for these conditions under local storage. Substance - drafts are stored in this browser's cache, so using an incognito tab or - clearing cache will clear or not allow access to drafts. You can save and - load stored record drafts using the buttons below. -
-
-
-
-
- - Show only current record - + Show only new registrations + +
-
- +
-
+ Validate and Submit selected + + + Save Backup + +
+ +
+ {{ filename ? filename : "no file chosen" }} +
+ +
-
- - Save Backup - -
+ -
- {{ filename ? filename : "no file chosen" }} -
-
+
+ - - + +
+ +
+
+ Type: {{draft.json.type}}, Name: {{ draft.json.name ? draft.json.name : '-' }} +
+
+ ERROR + SUCCESS + + Could not be submitted due to validation errors + Submission in progress + View Substance + Error during submission process +
+
+
+
diff --git a/src/app/core/substance-form/substance-drafts/substance-drafts.component.scss b/src/app/core/substance-form/substance-drafts/substance-drafts.component.scss index 9ecb18235..1f1c38fb3 100644 --- a/src/app/core/substance-form/substance-drafts/substance-drafts.component.scss +++ b/src/app/core/substance-form/substance-drafts/substance-drafts.component.scss @@ -33,3 +33,102 @@ ::ng-deep .mat-mdc-dialog-container .mat-mdc-dialog-content { padding: 0 24px; } + +/* Drafts validation */ +.message { + display: flex; + align-items: center; +} + +.validation-body { + max-width: 95%; + display: flex; + max-width: 960px; + word-break: break-word; +} + +.validation-message { + display: flex; + padding: 5px 0; + + .message-type { + text-transform: uppercase; + font-weight: 500; + margin-right: 20px; + padding: 2px; + border-radius: 3px; + min-width: 80px; + } +} + +.warning-message { + color: var(--warning-dialog-color); + background-color: var(--warning-dialog-bg-color); + +} + +.error-message { + color: var(--error-dialog-color); + background-color: var(--error-dialog-bg-color); +} + +.notice-message { + color: var(--notice-dialog-color); + background-color: var(--notice-dialog-bg-color); + +} + +.spinner-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(255, 255, 255, 0.6); /* Light dim background */ + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; /* Make sure it's above other content */ +} + +.draft-block { + margin-bottom: 20px; + padding-left: 8px; + border-left: 4px solid #ccc; +} + +.draft-block strong { + font-weight: bold; +} +.draft-block em { + color: #888; + font-style: italic; +} + +.message-row { + display: flex; + align-items: center; + margin: 6px 0; + padding-left: 12px; + gap: 8px; +} + +.label { + padding: 2px 6px; + border-radius: 4px; + font-size: 13px; + font-weight: 500; + white-space: nowrap; +} +.label-error { + background-color: #fce4e4; + color: #c62828; +} +.label-warning { + background-color: #fff8e1; + color: #f9a825; +} +.label-success { + background-color: #f3ffe1; + color: #13970a; +} diff --git a/src/app/core/substance-form/substance-drafts/substance-drafts.component.ts b/src/app/core/substance-form/substance-drafts/substance-drafts.component.ts index 004ea3da9..a82e1d08c 100644 --- a/src/app/core/substance-form/substance-drafts/substance-drafts.component.ts +++ b/src/app/core/substance-form/substance-drafts/substance-drafts.component.ts @@ -5,9 +5,23 @@ import {MatDialogRef, MAT_DIALOG_DATA} from '@angular/material/dialog'; import { UtilsService } from '@gsrs-core/utils'; import { Sort } from '@angular/material/sort'; import { DomSanitizer } from '@angular/platform-browser'; -import { FormGroup } from '@angular/forms'; import { Router } from '@angular/router'; import * as moment from 'moment'; +import { ValidationMessage } from '@gsrs-core/substance-form/substance-form.model' + +enum SubmissionStatus { + NONE, + CANNOT_BE_SUBMITTED, + IN_PROGRESS, + SUCCESS, + ERROR +} + +enum FormState { + DRAFT_LIST, + VALIDATION_RESULTS, + SUBMISSION +} @Component({ selector: 'app-substance-drafts', @@ -17,23 +31,28 @@ import * as moment from 'moment'; }) export class SubstanceDraftsComponent implements OnInit { draft: SubstanceDraft; - displayedColumns: string[] = ['delete', 'type', 'name', 'uuid', 'date', 'load']; + displayedColumns: string[] = ['select', 'delete', 'type', 'name', 'uuid', 'date', 'load']; json: any; values: Array; filtered: Array; + selectedKeys: Array = []; onlyRegister = false; onlyCurrent = false; downloadJsonHref: any; fileName: string; - filename: string; - uploadForm: FormGroup; + filename: string; view = 'edit'; file: any; uuid: string; - + + formState: FormState = FormState.DRAFT_LIST; + isLoading: boolean = false; + validatedDrafts: Array = []; + constructor( private substanceFormService: SubstanceFormService, + private substanceService: SubstanceService, public dialogRef: MatDialogRef, private utilsService: UtilsService, private sanitizer: DomSanitizer, @@ -50,13 +69,11 @@ export class SubstanceDraftsComponent implements OnInit { this.uuid = this.json.uuid; } - + this.fetchDrafts(); const time = new Date().getTime(); this.fileName = 'gsrs-drafts-' + time; this.download(); - - } @@ -89,11 +106,12 @@ export class SubstanceDraftsComponent implements OnInit { this.filtered = this.values; if (this.onlyCurrent) { + const isNewRegistration = !this.data || !this.data.uuid; this.filtered = this.filtered.filter(obj => { if(this.uuid) { - return obj.uuid == this.uuid; + return obj.uuid == this.uuid || (isNewRegistration && obj.uuid === 'register'); } else if (this.json && this.json.uuid){ - return obj.uuid == this.json.uuid; + return obj.uuid == this.json.uuid || (isNewRegistration && obj.uuid === 'register'); } else { return false; } @@ -110,7 +128,6 @@ export class SubstanceDraftsComponent implements OnInit { useDraft(index) { - this.dialogRef.close(index); } @@ -127,15 +144,209 @@ export class SubstanceDraftsComponent implements OnInit { }); } + toggleDraft(draft: any, checked: boolean): void { + if (!this.selectedKeys.includes(draft.key) && checked) { + this.selectedKeys.push(draft.key); + } else if (this.selectedKeys.includes(draft.key) && !checked) { + this.selectedKeys = this.selectedKeys.filter(key => key !== draft.key); + } + } + + processValidationMessages(substanceCopy, results): void { + if (results.validationMessages) { + for (let i = 0; i < substanceCopy.references.length; i++) { + const ref = substanceCopy.references[i]; + if (ref.docType !== 'SYSTEM') { + if ((!ref.citation || ref.citation === '') || (!ref.docType || ref.docType === '')) { + const invalidReferenceMessage: ValidationMessage = { + actionType: 'frontEnd', + appliedChange: false, + links: [], + message: 'All references require a non-empty source type and text/citation value', + messageType: 'WARNING', + suggestedChange: true + }; + results.validationMessages.push(invalidReferenceMessage); + break; + } + } + } + if (substanceCopy.properties) { + for (let i = 0; i < substanceCopy.properties.length; i++) { + const prop = substanceCopy.properties[i]; + if (!prop.propertyType || !prop.name) { + const invalidPropertyMessage: ValidationMessage = { + actionType: 'frontEnd', + appliedChange: false, + links: [], + message: 'Property #' + (i + 1) + ' requires a non-empty name and type', + messageType: 'ERROR', + suggestedChange: true + }; + results.validationMessages.push(invalidPropertyMessage); + results.valid = false; + } + } + } + if (substanceCopy.relationships) { + for (let i = 0; i < substanceCopy.relationships.length; i++) { + const relationship = substanceCopy.relationships[i]; + if (!relationship.relatedSubstance || !relationship.type || relationship.type === '') { + const invalidRelationshipMessage: ValidationMessage = { + actionType: 'frontEnd', + appliedChange: false, + links: [], + message: 'Relationship #' + (i + 1) + ' requires a non-empty related substance and type', + messageType: 'ERROR', + suggestedChange: true + }; + results.validationMessages.push(invalidRelationshipMessage); + results.valid = false; + } + } + } + if (substanceCopy.polymer && substanceCopy.polymer.monomers) { + for (let i = 0; i < substanceCopy.polymer.monomers.length; i++) { + const prop = substanceCopy.polymer.monomers[i]; + if (!prop.monomerSubstance || (typeof prop.monomerSubstance === 'object' && Object.keys(prop.monomerSubstance).length === 0)) { + const invalidPropertyMessage: ValidationMessage = { + actionType: 'frontEnd', + appliedChange: false, + links: [], + message: 'Monomer #' + (i + 1) + ' requires a selected substance', + messageType: 'ERROR', + suggestedChange: true + }; + results.validationMessages.push(invalidPropertyMessage); + results.valid = false; + } + } + } + if (substanceCopy.modifications && substanceCopy.modifications.physicalModifications) { + for (let i = 0; i < substanceCopy.modifications.physicalModifications.length; i++) { + const prop = substanceCopy.modifications.physicalModifications[i]; + let present = false; + if (prop && prop.parameters) { + prop.parameters.forEach(param => { + if (param.parameterName) { + present = true; + } + }); + } + + if (!prop.physicalModificationRole && !present) { + const invalidPropertyMessage: ValidationMessage = { + actionType: 'frontEnd', + appliedChange: false, + links: [], + message: 'Physical Modification #' + (i + 1) + ' requires a modification role or valid parameter', + messageType: 'ERROR', + suggestedChange: true + }; + results.validationMessages.push(invalidPropertyMessage); + results.valid = false; + } + } + } + } + } + + validateSelected(): void { + this.formState = FormState.VALIDATION_RESULTS; + this.isLoading = true; + this.validatedDrafts = []; + this.selectedKeys.forEach(key => { + const draft = JSON.parse(localStorage.getItem(key)); + const validatedDraft = { + key: key, + json: draft, + validationMessages: [], + validationResult: false, + submitStatus: SubmissionStatus.NONE, + fileUrl: undefined + } + + this.substanceService.validateSubstance(draft.substance).subscribe(results => { + + this.processValidationMessages(draft.substance, results); + validatedDraft.validationMessages = results.validationMessages.filter( + message => message.messageType.toUpperCase() === 'ERROR' || message.messageType.toUpperCase() === 'WARNING' || message.messageType.toUpperCase() === 'NOTICE' + ); + validatedDraft.validationResult = results.valid; + this.validatedDrafts.push(validatedDraft); + + if (this.validatedDrafts.length === this.selectedKeys.length) { + this.isLoading = false; + } + }, error => { + + validatedDraft.validationMessages.push({ + messageType: 'SERVER ERROR', + message: error.error?.message + + }) + + this.validatedDrafts.push(validatedDraft); + if (this.validatedDrafts.length === this.selectedKeys.length) { + this.isLoading = false; + } + }); + }) + } + + submitValid() { + this.formState = FormState.SUBMISSION; + this.isLoading = true; + let completedCount = 0; + const submittableDrafts = this.validatedDrafts.filter(draft => draft.validationResult); + this.validatedDrafts.forEach(draft => { + if (!draft.validationResult) { + draft.submitStatus = SubmissionStatus.CANNOT_BE_SUBMITTED; + } else { + draft.submitStatus = SubmissionStatus.IN_PROGRESS; + const result = { + isSuccessfull: false, + validationMessages: [], + serverError: undefined + } + this.substanceService.saveSubstance(draft.json.substance, 'import').subscribe(substance => { + draft.submitStatus = SubmissionStatus.SUCCESS; + draft.fileUrl = substance.fileUrl; + completedCount++; + if (completedCount === submittableDrafts.length) { + this.isLoading = false; + } + }, error => { + draft.submitStatus = SubmissionStatus.ERROR; + result.isSuccessfull = false; + if (error && error.error && error.error.validationMessages) { + result.validationMessages = error.error.validationMessages; + } else { + result.serverError = error; + } + completedCount++; + if (completedCount === submittableDrafts.length) { + this.isLoading = false; + } + }); + } + }) + } + + + fixLink(link: string) { + return this.substanceService.oldLinkFix(link); + } + deleteDraft(draft: any): void { - localStorage.removeItem(draft.key); - this.filtered = this.filtered.filter(function( obj ) { - return obj.key !== draft.key; + localStorage.removeItem(draft.key); + this.filtered = this.filtered.filter(function( obj ) { + return obj.key !== draft.key; }); - this.values = this.values.filter(function( obj ) { - return obj.key !== draft.key; -}); + this.values = this.values.filter(function( obj ) { + return obj.key !== draft.key; + }); } @@ -157,7 +368,7 @@ export class SubstanceDraftsComponent implements OnInit { this.values = []; let keys = Object.keys(localStorage); let i = keys.length; - + while ( i-- ) { if (keys[i].startsWith('gsrs-draft-')){ const entry = JSON.parse(localStorage.getItem(keys[i])); @@ -169,13 +380,8 @@ export class SubstanceDraftsComponent implements OnInit { } this.filtered = this.values.sort((a, b) => { return b.date - a.date; - });; - - if (this.json && this.json.uuid) { - this.filterToggle('substance'); - } else { - this.filterToggle('register'); - } + }); + this.selectedKeys = []; } @@ -207,11 +413,17 @@ export class SubstanceDraftsComponent implements OnInit { } + hasValidDrafts(): boolean { + return this.validatedDrafts.some(draft => draft.validationResult) + } + close() { this.dialogRef.close(); } - + + public readonly SubmissionStatus = SubmissionStatus + public readonly FormState = FormState } @@ -222,9 +434,9 @@ export interface SubstanceDraft { uuid: any; date: string; type: string; - name?: string; + name?: string; substance: any; auto?: boolean; file?: any; fromNow?: string; -} \ No newline at end of file +} diff --git a/src/app/core/substance-form/substance-form.component.html b/src/app/core/substance-form/substance-form.component.html index 14fd51197..b218b3dab 100644 --- a/src/app/core/substance-form/substance-form.component.html +++ b/src/app/core/substance-form/substance-form.component.html @@ -58,7 +58,7 @@ >
@@ -116,7 +116,7 @@ Switch primary and alt definitions - + {{ section.menuLabel }} (click)="submit()" [disabled]="isLoading || this.validationResult === false" > - {{ - validationMessages && validationMessages.length > 0 - ? "Dismiss All and " - : "" - }} - Submit + {{ getSubmitButtonText() }}
- + + + + (); + @Output() draftSelected = new EventEmitter(); @Input() placeholder = "Search"; @Input() label = ""; @Input() hintMessage = ""; @@ -27,6 +29,7 @@ export class SubstanceSelectorComponent implements OnInit { @Input() name?: string; @Input() hideImage?: boolean; @Input() showMorelinks? = false; + @Input() showDraftOption? = false; errorMessage: string; showOptions: boolean; previousSubstance: SubstanceSummary; @@ -53,6 +56,7 @@ export class SubstanceSelectorComponent implements OnInit { public scrollToService: ScrollToService, private dialog: MatDialog, private router: Router, + private structureService: StructureService ) {} StoreSelection() { @@ -187,21 +191,27 @@ export class SubstanceSelectorComponent implements OnInit { let molfile: string; let substance = this.selectedSubstance; + // For draft substances, use the temporary structure ID + const structureId = (substance as any).$$tmpStructureId || substance.uuid; + const isDraft = (substance as any).$$tmpStructureId ? true : false; + if (substance.substanceClass === "chemical") { data = { - structure: substance.uuid, + structure: structureId, smiles: substance.structure.smiles, uuid: substance.uuid, names: substance.names, component: "substanceSelector", + isDraft: isDraft }; molfile = substance.structure.molfile; } else { data = { - structure: substance.uuid, + structure: structureId, names: substance.names, component: "substanceSelector", uuid: substance.uuid, + isDraft: isDraft }; if (substance.polymer) { molfile = substance.polymer.idealizedStructure.molfile; @@ -284,4 +294,131 @@ export class SubstanceSelectorComponent implements OnInit { } window.open(url, "_blank"); } + + selectDraft(): void { + const dialogRef = this.dialog.open(SubstanceDraftsComponent, { + maxHeight: '85%', + width: '70%', + data: {} + }); + + this.overlayContainer.style.zIndex = '1002'; + + dialogRef.afterClosed().subscribe(response => { + this.overlayContainer.style.zIndex = null; + + if (response === null || response === undefined) { + return; + } + + // dialog may return either an index (number) or the draft object. + let draftObj: any = null; + if (typeof response === 'number') { + const comp = dialogRef.componentInstance as any; + if (comp) { + if (comp.filtered && comp.filtered[response]) { + draftObj = comp.filtered[response]; + } else if (comp.values && comp.values[response]) { + draftObj = comp.values[response]; + } + } + } else if (response && response.substance) { + draftObj = response; + } else { + draftObj = response; + } + + if (!draftObj) { + return; + } + + const substanceObj: SubstanceDetail = draftObj.substance || draftObj; + + // Determine a primary name for the draft substance + const primaryName = + substanceObj._name || + (substanceObj.names && substanceObj.names.length > 0 + ? substanceObj.names[0].name + : null) || + draftObj.name || + null; + + // If the draft contains a structure (molfile or smiles), interpret it on the server + // to obtain a temporary structure id that can be rendered via the same image API. + const mol = + substanceObj.structure && substanceObj.structure.molfile + ? substanceObj.structure.molfile + : substanceObj.structure && substanceObj.structure.smiles + ? substanceObj.structure.smiles + : null; + + if (mol) { + this.structureService.interpretStructure(mol).subscribe( + (interpretResponse) => { + if (interpretResponse && interpretResponse.structure && interpretResponse.structure.id) { + const tmpId = interpretResponse.structure.id; + (substanceObj as any).$$tmpStructureId = tmpId; + + // Also update localStorage so processLocalDrafts can find and match the draft + this.updateDraftInLocalStorage(draftObj, tmpId); + } + this.emitDraftSelection(draftObj, substanceObj, primaryName); + }, + (err) => { + // on error, still emit the draft selection without the structure id + console.log('Error interpreting draft structure: ', err); + this.emitDraftSelection(draftObj, substanceObj, primaryName); + } + ); + } else { + this.emitDraftSelection(draftObj, substanceObj, primaryName); + } + }); + } + + private updateDraftInLocalStorage(draftObj: any, tmpStructureId: string): void { + try { + // Update the draft object with the temp structure id + if (draftObj.substance) { + draftObj.substance.$$tmpStructureId = tmpStructureId; + } else { + (draftObj as any).$$tmpStructureId = tmpStructureId; + } + + // Persist back to localStorage if the draft has a key + const draftKey = draftObj.key || draftObj.file; + if (draftKey && draftKey.startsWith('gsrs-draft-')) { + localStorage.setItem(draftKey, JSON.stringify(draftObj)); + } + } catch (e) { + // Ignore storage errors + console.log('Error updating draft: ', e); + } + } + + private emitDraftSelection(draftObj: any, substanceObj: SubstanceDetail, primaryName: string): void { + const tmpStructureId = (substanceObj as any).$$tmpStructureId || null; + + // Set selectedSubstance so the template can render the draft + this.selectedSubstance = { + _name: primaryName, + uuid: substanceObj?.uuid, + approvalID: substanceObj?.approvalID + } as SubstanceSummary; + + // Attach the temp structure id for rendering + if (tmpStructureId) { + (this.selectedSubstance as any).$$tmpStructureId = tmpStructureId; + } + + // Emit the draft selection with all relevant data for the parent component to handle + this.draftSelected.emit({ + draft: draftObj, + substance: substanceObj, + primaryName: primaryName, + tmpStructureId: tmpStructureId + }); + } + + } diff --git a/src/app/core/substance-ssg2/substance-ssg2-form.component.ts b/src/app/core/substance-ssg2/substance-ssg2-form.component.ts index 41d3f2a12..ddb7da86c 100644 --- a/src/app/core/substance-ssg2/substance-ssg2-form.component.ts +++ b/src/app/core/substance-ssg2/substance-ssg2-form.component.ts @@ -203,11 +203,7 @@ export class SubstanceSsg2FormComponent implements OnInit, AfterViewInit, OnDest if (keys[i].startsWith('gsrs-draft-')) { const entry = JSON.parse(localStorage.getItem(keys[i])); entry.key = keys[i]; - if (this.id && entry.uuid === this.id) { - this.draftCount++; - } else if (!this.id && entry.type === (this.activatedRoute.snapshot.params['type']) && entry.uuid === 'register') { - this.draftCount++; - } + this.draftCount++; this.drafts.push(entry); } @@ -374,13 +370,7 @@ export class SubstanceSsg2FormComponent implements OnInit, AfterViewInit, OnDest if (keys[i].startsWith('gsrs-draft-')) { const entry = JSON.parse(localStorage.getItem(keys[i])); entry.key = keys[i]; - if (this.id && entry.uuid === this.id) { - temp++; - // this.draftCount++; - } else if (!this.id && entry.type === (this.activatedRoute.snapshot.params['type']) && entry.uuid === 'register') { - temp++; - // this.draftCount++; - } + temp++; this.drafts.push(entry); } diff --git a/src/app/core/substance-ssg4m/model/substance-ssg4m.model.ts b/src/app/core/substance-ssg4m/model/substance-ssg4m.model.ts index 5555d42fa..b31d7afb0 100644 --- a/src/app/core/substance-ssg4m/model/substance-ssg4m.model.ts +++ b/src/app/core/substance-ssg4m/model/substance-ssg4m.model.ts @@ -1,4 +1,3 @@ - export interface Ssg4mSyntheticPathway { createdBy?: string; modifiedBy?: string; @@ -16,7 +15,9 @@ export interface Ssg4mSyntheticPathway { printSbstncUuid?: string; printSbstncPrfrdNm?: string; sbmsnImage?: string; + stepViewImage?: string; ssg4mSyntheticPathwayDetailsList?: Array; + fileUrl?: string; } export interface Ssg4mSyntheticPathwayDetail { @@ -30,4 +31,3 @@ export interface Ssg4mSyntheticPathwayDetail { sbstncReactnSectNm?: string; sbstncRoleNm?: string; } - diff --git a/src/app/core/substance-ssg4m/ssg4m-process/substance-form-ssg4m-process-card.component.html b/src/app/core/substance-ssg4m/ssg4m-process/substance-form-ssg4m-process-card.component.html index e1d323029..a2025f173 100644 --- a/src/app/core/substance-ssg4m/ssg4m-process/substance-form-ssg4m-process-card.component.html +++ b/src/app/core/substance-ssg4m/ssg4m-process/substance-form-ssg4m-process-card.component.html @@ -67,16 +67,3 @@ - - - -

-     - -
- - - -
\ No newline at end of file diff --git a/src/app/core/substance-ssg4m/ssg4m-process/substance-form-ssg4m-process-card.component.ts b/src/app/core/substance-ssg4m/ssg4m-process/substance-form-ssg4m-process-card.component.ts index dd11786eb..4d98af873 100644 --- a/src/app/core/substance-ssg4m/ssg4m-process/substance-form-ssg4m-process-card.component.ts +++ b/src/app/core/substance-ssg4m/ssg4m-process/substance-form-ssg4m-process-card.component.ts @@ -56,56 +56,56 @@ export class SubstanceFormSsg4mProcessCardComponent extends SubstanceCardBaseFil this.overlayContainer = this.overlayContainerService.getContainerElement(); let loaded = false; - setInterval(() => { - if (window['schemeUtil'] && !loaded) { - loaded = true; - //setup viz stuff - //TODO: make more configurable and standardized - console.log("About to configure the scheme view"); - window['schemeUtil'].debug = false; - - window['schemeUtil'].maxContinuousSteps = 1; - window['schemeUtil'].maxTextLen = 30; - window['schemeUtil'].BREAK_GAP = 300; - window['schemeUtil'].maxTitleTextLen = 100; - - const url = `${(this.configService.configData && this.configService.configData.apiBaseUrl) || '/'}api/v1/`; - const httpp = this.http; - window['schemeUtil'].apiBaseURL = url; - - //allow resolution of svgs - window['schemeUtil'].urlResolver = (u, cb) => { - httpp.get(u, { responseType: 'text' }).subscribe(svg => { - cb(svg); - }, error => { - cb("ERROR"); - }); - }; - //TODO: - window['schemeUtil'].onClickReaction = (d) => { - //Can we add a popup dialog that would show the specific step here? - let pindex = d.processIndex; - let sindex = d.stepIndex; - let siteIndex = d.siteIndex; - if (typeof siteIndex === "undefined") { - siteIndex = 0; - } - this.showStepViewDialog(pindex, siteIndex, sindex); - - //I just want to show a dialog that shows the step/stage component rendered in a popup for now. - //maybe in the future it should instead be a side window, I don't know. - }; - - //TODO: - window['schemeUtil'].onClickMaterial = (d) => { - this.openImageModal(d.refuuid, d.name, d.bottomText); - }; - - if (window['schemeUtil'].executeWhenLoaded) { - window['schemeUtil'].executeWhenLoaded(); - } - } - }, 100); + // setInterval(() => { + // if (window['schemeUtil'] && !loaded) { + // loaded = true; + // //setup viz stuff + // //TODO: make more configurable and standardized + // console.log("About to configure the scheme view"); + // window['schemeUtil'].debug = false; + + // window['schemeUtil'].maxContinuousSteps = 1; + // window['schemeUtil'].maxTextLen = 30; + // window['schemeUtil'].BREAK_GAP = 300; + // window['schemeUtil'].maxTitleTextLen = 100; + + // const url = `${(this.configService.configData && this.configService.configData.apiBaseUrl) || '/'}api/v1/`; + // const httpp = this.http; + // window['schemeUtil'].apiBaseURL = url; + + // //allow resolution of svgs + // window['schemeUtil'].urlResolver = (u, cb) => { + // httpp.get(u, { responseType: 'text' }).subscribe(svg => { + // cb(svg); + // }, error => { + // cb("ERROR"); + // }); + // }; + // //TODO: + // window['schemeUtil'].onClickReaction = (d) => { + // //Can we add a popup dialog that would show the specific step here? + // let pindex = d.processIndex; + // let sindex = d.stepIndex; + // let siteIndex = d.siteIndex; + // if (typeof siteIndex === "undefined") { + // siteIndex = 0; + // } + // this.showStepViewDialog(pindex, siteIndex, sindex); + + // //I just want to show a dialog that shows the step/stage component rendered in a popup for now. + // //maybe in the future it should instead be a side window, I don't know. + // }; + + // //TODO: + // window['schemeUtil'].onClickMaterial = (d) => { + // this.openImageModal(d.refuuid, d.name, d.bottomText); + // }; + + // if (window['schemeUtil'].executeWhenLoaded) { + // window['schemeUtil'].executeWhenLoaded(); + // } + // } + // }, 100); // Get the parameter from URL and set the tab to either form view, step view, or scheme view. this.showView = this.activatedRoute.snapshot.queryParams['view'] || 'form'; @@ -208,28 +208,28 @@ export class SubstanceFormSsg4mProcessCardComponent extends SubstanceCardBaseFil onSelectedIndexChange(tabIndex: number) { this.tabSelectedIndex = tabIndex; - if (this.tabSelectedIndex === 2) { - document.querySelector("#scheme-viz-view").className = ""; - //This is a hacky placeholder way to force viz - //TODO finish this - const ssgjs = JSON.stringify(this.substanceFormService.cleanSubstance()); - - console.log("About to load the scheme view"); - if (window['schemeUtil']) { - if (window['schemeUtil'].debug) { - window['schemeUtil'].executeWhenLoaded = (() => { - console.log("About to render the scheme view"); - window['schemeUtil'].renderScheme(window['schemeUtil'].makeDisplayGraph(JSON.parse(ssgjs)), "#scheme-viz-view"); - window['schemeUtil'].executeWhenLoaded = null; - }); - } else { - console.log("About to render the scheme view"); - window['schemeUtil'].renderScheme(window['schemeUtil'].makeDisplayGraph(JSON.parse(ssgjs)), "#scheme-viz-view"); - } - } - } else { - document.querySelector("#scheme-viz-view").className = "hidden"; - } + // if (this.tabSelectedIndex === 2) { + // document.querySelector("#scheme-viz-view").className = ""; + // //This is a hacky placeholder way to force viz + // //TODO finish this + // const ssgjs = JSON.stringify(this.substanceFormService.cleanSubstance()); + + // console.log("About to load the scheme view"); + // if (window['schemeUtil']) { + // if (window['schemeUtil'].debug) { + // window['schemeUtil'].executeWhenLoaded = (() => { + // console.log("About to render the scheme view"); + // window['schemeUtil'].renderScheme(window['schemeUtil'].makeDisplayGraph(JSON.parse(ssgjs)), "#scheme-viz-view"); + // window['schemeUtil'].executeWhenLoaded = null; + // }); + // } else { + // console.log("About to render the scheme view"); + // window['schemeUtil'].renderScheme(window['schemeUtil'].makeDisplayGraph(JSON.parse(ssgjs)), "#scheme-viz-view"); + // } + // } + // } else { + // document.querySelector("#scheme-viz-view").className = "hidden"; + // } } tabSelectedIndexOutChange(tabIndex: number) { diff --git a/src/app/core/substance-ssg4m/ssg4m-processing-materials/ssg4m-processing-materials-form.component.html b/src/app/core/substance-ssg4m/ssg4m-processing-materials/ssg4m-processing-materials-form.component.html index 433cf18a5..766b290b7 100644 --- a/src/app/core/substance-ssg4m/ssg4m-processing-materials/ssg4m-processing-materials-form.component.html +++ b/src/app/core/substance-ssg4m/ssg4m-processing-materials/ssg4m-processing-materials-form.component.html @@ -33,11 +33,13 @@ >
diff --git a/src/app/core/substance-ssg4m/ssg4m-processing-materials/ssg4m-processing-materials-form.component.ts b/src/app/core/substance-ssg4m/ssg4m-processing-materials/ssg4m-processing-materials-form.component.ts index 47271d9b6..014d60047 100644 --- a/src/app/core/substance-ssg4m/ssg4m-processing-materials/ssg4m-processing-materials-form.component.ts +++ b/src/app/core/substance-ssg4m/ssg4m-processing-materials/ssg4m-processing-materials-form.component.ts @@ -164,12 +164,37 @@ export class Ssg4mProcessingMaterialsFormComponent implements OnInit, OnDestroy approvalID: substance.approvalID }; this.privateProcessingMaterial.substanceName = relatedSubstance; + // Clear any draft-related fields when selecting from database + delete (this.privateProcessingMaterial as any).$$tmpStructureId; } else { this.privateProcessingMaterial.substanceName = {}; } } + draftSubstanceSelected(event: any): void { + if (!event) { + return; + } + + const { substance: substanceObj, primaryName, tmpStructureId } = event; + + // Populate the processing material with draft data + this.privateProcessingMaterial.substanceName = { + refPname: primaryName, + name: primaryName, + refuuid: substanceObj?.uuid || "draft", + substanceClass: 'mention', + approvalID: substanceObj?.approvalID + }; + this.privateProcessingMaterial.verbatimName = primaryName; + + // Store the temporary structure ID for rendering + if (tmpStructureId) { + (this.privateProcessingMaterial as any).$$tmpStructureId = tmpStructureId; + } + } + addManufacturer(processIndex: number, siteIndex: number, stageIndex: number) { this.substanceFormSsg4mStagesService.addResultingManufacturerDetails(processIndex, siteIndex, stageIndex, this.processingMaterialIndex); } diff --git a/src/app/core/substance-ssg4m/ssg4m-resulting-materials/ssg4m-resulting-materials-form.component.html b/src/app/core/substance-ssg4m/ssg4m-resulting-materials/ssg4m-resulting-materials-form.component.html index 8482695f4..20c9e1925 100644 --- a/src/app/core/substance-ssg4m/ssg4m-resulting-materials/ssg4m-resulting-materials-form.component.html +++ b/src/app/core/substance-ssg4m/ssg4m-resulting-materials/ssg4m-resulting-materials-form.component.html @@ -42,11 +42,13 @@ > diff --git a/src/app/core/substance-ssg4m/ssg4m-resulting-materials/ssg4m-resulting-materials-form.component.ts b/src/app/core/substance-ssg4m/ssg4m-resulting-materials/ssg4m-resulting-materials-form.component.ts index c6aaa6555..922ba6863 100644 --- a/src/app/core/substance-ssg4m/ssg4m-resulting-materials/ssg4m-resulting-materials-form.component.ts +++ b/src/app/core/substance-ssg4m/ssg4m-resulting-materials/ssg4m-resulting-materials-form.component.ts @@ -159,12 +159,37 @@ export class Ssg4mResultingMaterialsFormComponent implements OnInit, OnDestroy { approvalID: substance.approvalID }; this.privateResultingMaterial.substanceName = relatedSubstance; + // Clear any draft-related fields when selecting from database + delete (this.privateResultingMaterial as any).$$tmpStructureId; } else { this.privateResultingMaterial.substanceName = {}; } } + draftSubstanceSelected(event: any): void { + if (!event) { + return; + } + + const { substance: substanceObj, primaryName, tmpStructureId } = event; + + // Populate the resulting material with draft data + this.privateResultingMaterial.substanceName = { + refPname: primaryName, + name: primaryName, + refuuid: substanceObj?.uuid || "draft", + substanceClass: 'mention', + approvalID: substanceObj?.approvalID + }; + this.privateResultingMaterial.verbatimName = primaryName; + + // Store the temporary structure ID for rendering + if (tmpStructureId) { + (this.privateResultingMaterial as any).$$tmpStructureId = tmpStructureId; + } + } + addAcceptanceCriteria(processIndex: number, siteIndex: number, stageIndex: number) { this.substanceFormSsg4mStagesService.addResultingAcceptanceCriteria(processIndex, siteIndex, stageIndex, this.resultingMaterialIndex); } diff --git a/src/app/core/substance-ssg4m/ssg4m-scheme-view/ssg4m-scheme-view.component.html b/src/app/core/substance-ssg4m/ssg4m-scheme-view/ssg4m-scheme-view.component.html index e3793d52d..cbd4cb7c6 100644 --- a/src/app/core/substance-ssg4m/ssg4m-scheme-view/ssg4m-scheme-view.component.html +++ b/src/app/core/substance-ssg4m/ssg4m-scheme-view/ssg4m-scheme-view.component.html @@ -65,13 +65,13 @@ + - + (click)="openImageModal(startingMaterial.$$tmpStructureId || startingMaterial.substanceName?.refuuid, startingMaterial.substanceName?.approvalID, startingMaterial.substanceName?.refPname || startingMaterial.verbatimName)"> +
- {{startingMaterial.substanceName.refPname}} @@ -113,13 +113,13 @@
- {{processingMaterial.substanceName.refPname}} @@ -162,7 +162,7 @@
@@ -174,7 +174,7 @@ --> - {{processingMaterial.substanceName.refPname}} @@ -221,8 +221,8 @@ +
- + (click)="openImageModal(resultingMaterial.$$tmpStructureId || resultingMaterial.substanceName?.refuuid, resultingMaterial.substanceName?.approvalID, resultingMaterial.substanceName?.refPname || resultingMaterial.verbatimName)"> +
- {{resultingMaterial.substanceName.refPname}} diff --git a/src/app/core/substance-ssg4m/ssg4m-scheme-view/ssg4m-scheme-view.component.scss b/src/app/core/substance-ssg4m/ssg4m-scheme-view/ssg4m-scheme-view.component.scss index 6423e5f9f..c6cf7617f 100644 --- a/src/app/core/substance-ssg4m/ssg4m-scheme-view/ssg4m-scheme-view.component.scss +++ b/src/app/core/substance-ssg4m/ssg4m-scheme-view/ssg4m-scheme-view.component.scss @@ -184,6 +184,12 @@ legend.border-step { cursor:zoom-in; } +/* Ensure structure images in the scheme/step view scale to available cell space */ +.width33percent img, .width33percent .zoom img { + max-width: 100%; + height: auto; +} + a { color: #0000EE;;; } diff --git a/src/app/core/substance-ssg4m/ssg4m-stages/ssg4m-stages-form.component.ts b/src/app/core/substance-ssg4m/ssg4m-stages/ssg4m-stages-form.component.ts index 80377d9c9..bd2b4dab3 100644 --- a/src/app/core/substance-ssg4m/ssg4m-stages/ssg4m-stages-form.component.ts +++ b/src/app/core/substance-ssg4m/ssg4m-stages/ssg4m-stages-form.component.ts @@ -1,12 +1,21 @@ -import { Component, OnInit, OnDestroy, AfterViewInit, Input } from '@angular/core'; -import { MatDialog } from '@angular/material/dialog'; -import { OverlayContainer } from '@angular/cdk/overlay'; -import { ScrollToService } from '../../scroll-to/scroll-to.service'; -import { Subscription } from 'rxjs'; -import { GoogleAnalyticsService } from '../../google-analytics/google-analytics.service'; -import { ConfigService } from '@gsrs-core/config/config.service'; -import { SubstanceCardBaseFilteredList, SubstanceCardBaseList } from '../../substance-form/base-classes/substance-form-base-filtered-list'; -import { SubstanceFormService } from '../../substance-form/substance-form.service'; +import { + Component, + OnInit, + OnDestroy, + AfterViewInit, + Input, +} from "@angular/core"; +import { MatDialog } from "@angular/material/dialog"; +import { OverlayContainer } from "@angular/cdk/overlay"; +import { ScrollToService } from "../../scroll-to/scroll-to.service"; +import { Subscription } from "rxjs"; +import { GoogleAnalyticsService } from "../../google-analytics/google-analytics.service"; +import { ConfigService } from "@gsrs-core/config/config.service"; +import { + SubstanceCardBaseFilteredList, + SubstanceCardBaseList, +} from "../../substance-form/base-classes/substance-form-base-filtered-list"; +import { SubstanceFormService } from "../../substance-form/substance-form.service"; /* import { take } from 'rxjs/operators'; import { ConfigService } from '@gsrs-core/config'; @@ -14,19 +23,25 @@ import { SubstanceFormBase } from '../../substance-form/base-classes/substance-f import { ControlledVocabularyService } from '../../controlled-vocabulary/controlled-vocabulary.service'; import { VocabularyTerm } from '../../controlled-vocabulary/vocabulary.model'; */ -import { SubstanceService } from '../../substance/substance.service'; -import { SubstanceSummary, SubstanceRelationship } from '../../substance/substance.model'; -import { SpecifiedSubstanceG4mProcess, SubstanceRelated } from '../../substance/substance.model'; -import { SubstanceDetail } from '@gsrs-core/substance/substance.model'; -import { SubstanceFormSsg4mStagesService } from './substance-form-ssg4m-stages.service'; -import { SpecifiedSubstanceG4mStage } from '@gsrs-core/substance/substance.model'; -import { ConfirmDialogComponent } from '../../../fda/confirm-dialog/confirm-dialog.component'; +import { SubstanceService } from "../../substance/substance.service"; +import { + SubstanceSummary, + SubstanceRelationship, +} from "../../substance/substance.model"; +import { + SpecifiedSubstanceG4mProcess, + SubstanceRelated, +} from "../../substance/substance.model"; +import { SubstanceDetail } from "@gsrs-core/substance/substance.model"; +import { SubstanceFormSsg4mStagesService } from "./substance-form-ssg4m-stages.service"; +import { SpecifiedSubstanceG4mStage } from "@gsrs-core/substance/substance.model"; +import { ConfirmDialogComponent } from "../../../fda/confirm-dialog/confirm-dialog.component"; @Component({ - selector: 'app-ssg4m-stages-form', - templateUrl: './ssg4m-stages-form.component.html', - styleUrls: ['./ssg4m-stages-form.component.scss'], - standalone: false + selector: "app-ssg4m-stages-form", + templateUrl: "./ssg4m-stages-form.component.html", + styleUrls: ["./ssg4m-stages-form.component.scss"], + standalone: false }) export class Ssg4mStagesFormComponent implements OnInit, OnDestroy { public configSettingsDisplay = {}; @@ -50,7 +65,7 @@ export class Ssg4mStagesFormComponent implements OnInit, OnDestroy { private scrollToService: ScrollToService, public configService: ConfigService, private dialog: MatDialog - ) { } + ) {} @Input() set stage(stage: SpecifiedSubstanceG4mStage) { @@ -83,7 +98,7 @@ export class Ssg4mStagesFormComponent implements OnInit, OnDestroy { set stageIndex(stageIndex: number) { this.privateStageIndex = stageIndex; // Set the Stage Name - // alert("STAGE INDEX: " + stageIndex); + // alert("STAGE INDEX: " + stageIndex); this.privateStage.stageNumber = String(this.privateStageIndex + 1); } @@ -113,30 +128,36 @@ export class Ssg4mStagesFormComponent implements OnInit, OnDestroy { ngOnInit(): void { // this.substance = this.substanceFormSsg4mStagesService.substance; - const subscription = this.substanceFormService.substance.subscribe(substance => { - this.substance = substance; - }); + const subscription = this.substanceFormService.substance.subscribe( + (substance) => { + this.substance = substance; + } + ); this.subscriptions.push(subscription); // Get Config variables for SSG4m - this.configSsg4Form = (this.configService.configData && this.configService.configData.ssg4Form) || null; - this.configTitleStage = 'Stage'; + this.configSsg4Form = + (this.configService.configData && + this.configService.configData.ssg4Form) || + null; + this.configTitleStage = "Stage"; this.configTitleProcessingMaterials = "Processing Materials"; if (this.configSsg4Form) { this.configTitleStage = this.configSsg4Form.titles.stage || null; if (!this.configTitleStage) { - this.configTitleStage = 'Stage'; + this.configTitleStage = "Stage"; } - this.configTitleProcessingMaterials = this.configSsg4Form.titles.processingMaterials || null; + this.configTitleProcessingMaterials = + this.configSsg4Form.titles.processingMaterials || null; if (!this.configTitleProcessingMaterials) { - this.configTitleProcessingMaterials = 'Processing Materials'; + this.configTitleProcessingMaterials = "Processing Materials"; } } } ngOnDestroy(): void { // this.substanceFormService.unloadSubstance(); - this.subscriptions.forEach(subscription => { + this.subscriptions.forEach((subscription) => { subscription.unsubscribe(); }); } @@ -144,74 +165,155 @@ export class Ssg4mStagesFormComponent implements OnInit, OnDestroy { getConfigSettings(): void { // Get SSG4 Config Settings from config.json file to show and hide fields in the form let configSsg4Form: any; - configSsg4Form = this.configService.configData && this.configService.configData.ssg4Form || null; + configSsg4Form = + (this.configService.configData && + this.configService.configData.ssg4Form) || + null; // Get 'stage' json values from config const confSettings = configSsg4Form.settingsDisplay.stage; - Object.keys(confSettings).forEach(key => { + Object.keys(confSettings).forEach((key) => { if (confSettings[key] != null) { - if (confSettings[key] === 'simple') { + if (confSettings[key] === "simple") { this.configSettingsDisplay[key] = true; - } else if (confSettings[key] === 'advanced') { + } else if (confSettings[key] === "advanced") { if (this.privateShowAdvancedSettings === true) { this.configSettingsDisplay[key] = true; } else { this.configSettingsDisplay[key] = false; } - } else if (confSettings[key] === 'removed') { + } else if (confSettings[key] === "removed") { this.configSettingsDisplay[key] = false; } } }); } - insertStage(processIndex: number, siteIndex: number, stageIndex: number, insertDirection?: string): void { - this.substanceFormSsg4mStagesService.insertStage(processIndex, siteIndex, stageIndex, insertDirection); + insertStage( + processIndex: number, + siteIndex: number, + stageIndex: number, + insertDirection?: string + ): void { + this.substanceFormSsg4mStagesService.insertStage( + processIndex, + siteIndex, + stageIndex, + insertDirection + ); setTimeout(() => { - this.scrollToService.scrollToElement(`substance-process-0`, 'center'); + this.scrollToService.scrollToElement(`substance-process-0`, "center"); }); } - duplicateStage(processIndex: number, siteIndex: number, stageIndex: number, insertDirection?: string): void { - this.substanceFormSsg4mStagesService.duplicateStage(processIndex, siteIndex, stageIndex, insertDirection); + duplicateStage( + processIndex: number, + siteIndex: number, + stageIndex: number, + insertDirection?: string + ): void { + this.substanceFormSsg4mStagesService.duplicateStage( + processIndex, + siteIndex, + stageIndex, + insertDirection + ); setTimeout(() => { - this.scrollToService.scrollToElement(`substance-stage-duplicate-0`, 'center'); + this.scrollToService.scrollToElement( + `substance-stage-duplicate-0`, + "center" + ); }); } - addCriticalParameter(processIndex: number, siteIndex: number, stageIndex: number) { - this.substanceFormSsg4mStagesService.addCriticalParameter(processIndex, siteIndex, stageIndex); + addCriticalParameter( + processIndex: number, + siteIndex: number, + stageIndex: number + ) { + this.substanceFormSsg4mStagesService.addCriticalParameter( + processIndex, + siteIndex, + stageIndex + ); setTimeout(() => { - this.scrollToService.scrollToElement(`substance-process-site-stage-criticalParam-0`, 'center'); + this.scrollToService.scrollToElement( + `substance-process-site-stage-criticalParam-0`, + "center" + ); }); } - addStartingMaterial(processIndex: number, siteIndex: number, stageIndex: number) { - this.substanceFormSsg4mStagesService.addStartingMaterials(processIndex, siteIndex, stageIndex); + addStartingMaterial( + processIndex: number, + siteIndex: number, + stageIndex: number + ) { + this.substanceFormSsg4mStagesService.addStartingMaterials( + processIndex, + siteIndex, + stageIndex + ); setTimeout(() => { - this.scrollToService.scrollToElement(`substance-process-site-stage-startMat-0`, 'center'); + this.scrollToService.scrollToElement( + `substance-process-site-stage-startMat-0`, + "center" + ); }); } - addProcessingMaterial(processIndex: number, siteIndex: number, stageIndex: number) { - this.substanceFormSsg4mStagesService.addProcessingMaterials(processIndex, siteIndex, stageIndex); + addProcessingMaterial( + processIndex: number, + siteIndex: number, + stageIndex: number + ) { + this.substanceFormSsg4mStagesService.addProcessingMaterials( + processIndex, + siteIndex, + stageIndex + ); setTimeout(() => { - this.scrollToService.scrollToElement(`substance-process-site-stage-processMat-0`, 'center'); + this.scrollToService.scrollToElement( + `substance-process-site-stage-processMat-0`, + "center" + ); }); } - addResultingMaterial(processIndex: number, siteIndex: number, stageIndex: number) { - this.substanceFormSsg4mStagesService.addResultingMaterials(processIndex, siteIndex, stageIndex); + addResultingMaterial( + processIndex: number, + siteIndex: number, + stageIndex: number + ) { + this.substanceFormSsg4mStagesService.addResultingMaterials( + processIndex, + siteIndex, + stageIndex + ); setTimeout(() => { - this.scrollToService.scrollToElement(`substance-process-site-stage-resultMat-0`, 'center'); + this.scrollToService.scrollToElement( + `substance-process-site-stage-resultMat-0`, + "center" + ); }); } confirmDeleteStage() { const dialogRef = this.dialog.open(ConfirmDialogComponent, { - data: { message: 'Are you sure you want to delele ' + this.configTitleStage + ' ' + (this.stageIndex + 1) + ' for Site ' + (this.siteIndex + 1) + ' for Process ' + (this.processIndex + 1) + '?' } + data: { + message: + "Are you sure you want to delele " + + this.configTitleStage + + " " + + (this.stageIndex + 1) + + " for Site " + + (this.siteIndex + 1) + + " for Process " + + (this.processIndex + 1) + + "?", + }, }); - dialogRef.afterClosed().subscribe(result => { + dialogRef.afterClosed().subscribe((result) => { if (result && result === true) { this.deleteStage(); } @@ -219,7 +321,9 @@ export class Ssg4mStagesFormComponent implements OnInit, OnDestroy { } deleteStage(): void { - this.substance.specifiedSubstanceG4m.process[this.processIndex].sites[this.siteIndex].stages.splice(this.stageIndex, 1); + this.substance.specifiedSubstanceG4m.process[this.processIndex].sites[ + this.siteIndex + ].stages.splice(this.stageIndex, 1); } /* @@ -231,4 +335,3 @@ export class Ssg4mStagesFormComponent implements OnInit, OnDestroy { } */ } - diff --git a/src/app/core/substance-ssg4m/ssg4m-starting-materials/ssg4m-starting-materials-form.component.html b/src/app/core/substance-ssg4m/ssg4m-starting-materials/ssg4m-starting-materials-form.component.html index bd7abd78a..abbd5dc2f 100644 --- a/src/app/core/substance-ssg4m/ssg4m-starting-materials/ssg4m-starting-materials-form.component.html +++ b/src/app/core/substance-ssg4m/ssg4m-starting-materials/ssg4m-starting-materials-form.component.html @@ -26,11 +26,13 @@ >
diff --git a/src/app/core/substance-ssg4m/ssg4m-starting-materials/ssg4m-starting-materials-form.component.scss b/src/app/core/substance-ssg4m/ssg4m-starting-materials/ssg4m-starting-materials-form.component.scss index 4b0b78ac2..17fc00d47 100644 --- a/src/app/core/substance-ssg4m/ssg4m-starting-materials/ssg4m-starting-materials-form.component.scss +++ b/src/app/core/substance-ssg4m/ssg4m-starting-materials/ssg4m-starting-materials-form.component.scss @@ -157,6 +157,16 @@ hr.style { */ } +.related-substance-image { + /* make the interpreted draft image responsive to available space */ + width: 100%; + img { + width: 100%; + height: auto; + max-width: 100%; + } +} + /* .related-substance { min-width: 387px; diff --git a/src/app/core/substance-ssg4m/ssg4m-starting-materials/ssg4m-starting-materials-form.component.ts b/src/app/core/substance-ssg4m/ssg4m-starting-materials/ssg4m-starting-materials-form.component.ts index eef48f8ee..464fc350b 100644 --- a/src/app/core/substance-ssg4m/ssg4m-starting-materials/ssg4m-starting-materials-form.component.ts +++ b/src/app/core/substance-ssg4m/ssg4m-starting-materials/ssg4m-starting-materials-form.component.ts @@ -178,11 +178,38 @@ export class Ssg4mStartingMaterialsFormComponent implements OnInit, OnDestroy { approvalID: substance.approvalID }; this.privateStartingMaterial.substanceName = relatedSubstance; + // Clear any draft-related fields when selecting from database + delete (this.privateStartingMaterial as any).$$tmpStructureId; } else { this.privateStartingMaterial.substanceName = {}; } } + draftSubstanceSelected(event: any): void { + if (!event) { + return; + } + + const { substance: substanceObj, primaryName, tmpStructureId } = event; + + console.log('Draft substance selected: ', substanceObj, primaryName, tmpStructureId); + + // Populate the starting material with draft data + this.privateStartingMaterial.substanceName = { + refPname: primaryName, + name: primaryName, + refuuid: substanceObj?.uuid || "draft", + substanceClass: "mention", + approvalID: substanceObj?.approvalID + }; + this.privateStartingMaterial.verbatimName = primaryName; + + // Store the temporary structure ID for rendering + if (tmpStructureId) { + (this.privateStartingMaterial as any).$$tmpStructureId = tmpStructureId; + } + } + confirmDeleteStartingMaterial() { const dialogRef = this.dialog.open(ConfirmDialogComponent, { data: { message: 'Are you sure you want to delele Input Material ' + (this.startingMaterialIndex + 1) + ' for Step ' + (this.stageIndex + 1) + ' for Site ' + (this.siteIndex + 1) + ' for Process ' + (this.processIndex + 1) + '?' } diff --git a/src/app/core/substance-ssg4m/substance-ssg4m-form.component.html b/src/app/core/substance-ssg4m/substance-ssg4m-form.component.html index ebcf455b8..29f5c7c22 100644 --- a/src/app/core/substance-ssg4m/substance-ssg4m-form.component.html +++ b/src/app/core/substance-ssg4m/substance-ssg4m-form.component.html @@ -25,6 +25,9 @@ Save Local Copy + @@ -36,7 +39,7 @@ + [disabled]="showFormReadOnly === 'true' || isAuthenticated === false">Save
diff --git a/src/app/core/substance-ssg4m/substance-ssg4m-form.component.ts b/src/app/core/substance-ssg4m/substance-ssg4m-form.component.ts index a05aa8d4a..1033c0c47 100644 --- a/src/app/core/substance-ssg4m/substance-ssg4m-form.component.ts +++ b/src/app/core/substance-ssg4m/substance-ssg4m-form.component.ts @@ -5,60 +5,77 @@ import { ViewChildren, ViewContainerRef, QueryList, - OnDestroy, HostListener -} from '@angular/core'; -import { ActivatedRoute, Router, Event, NavigationStart, NavigationEnd } from '@angular/router'; -import { OverlayContainer } from '@angular/cdk/overlay'; -import { MatExpansionPanel } from '@angular/material/expansion'; -import { MatDialog } from '@angular/material/dialog'; -import { take, map } from 'rxjs/operators'; -import { Subscription, Observable } from 'rxjs'; -import * as _ from 'lodash'; -import * as moment from 'moment'; -import { Title } from '@angular/platform-browser'; -import { DomSanitizer, SafeUrl } from '@angular/platform-browser'; + OnDestroy, + HostListener, +} from "@angular/core"; +import { + ActivatedRoute, + Router, + Event, + NavigationStart, + NavigationEnd, +} from "@angular/router"; +import { OverlayContainer } from "@angular/cdk/overlay"; +import { MatExpansionPanel } from "@angular/material/expansion"; +import { MatDialog } from "@angular/material/dialog"; +import { take, map } from "rxjs/operators"; +import { Subscription, Observable } from "rxjs"; +import * as _ from "lodash"; +import * as moment from "moment"; +import { Title } from "@angular/platform-browser"; +import { DomSanitizer, SafeUrl } from "@angular/platform-browser"; // GSRS Import -import { ConfigService } from '@gsrs-core/config/config.service'; -import { GoogleAnalyticsService } from '../google-analytics/google-analytics.service'; -import { DynamicComponentLoader } from '../dynamic-component-loader/dynamic-component-loader.service'; -import { formSections } from '../substance-form/form-sections.constant'; -import { SubstanceFormSection } from '../substance-form/substance-form-section'; -import { MainNotificationService } from '../main-notification/main-notification.service'; -import { AuthService } from '@gsrs-core/auth'; -import { LoadingService } from '../loading/loading.service'; -import { SubstanceService } from '../substance/substance.service'; -import { SubstanceFormService } from '../substance-form/substance-form.service'; -import { AppNotification, NotificationType } from '../main-notification/notification.model'; -import { ValidationResults } from '../substance-form/substance-form.model'; -import { SubstanceDetail } from '../substance/substance.model'; -import { ValidationMessage, SubstanceFormResults, SubstanceFormDefinition } from '../substance-form/substance-form.model'; -import { SubmitSuccessDialogComponent } from '../substance-form/submit-success-dialog/submit-success-dialog.component'; -import { MergeConceptDialogComponent } from '@gsrs-core/substance-form/merge-concept-dialog/merge-concept-dialog.component'; -import { DefinitionSwitchDialogComponent } from '@gsrs-core/substance-form/definition-switch-dialog/definition-switch-dialog.component'; -import { SubstanceEditImportDialogComponent } from '@gsrs-core/substance-edit-import-dialog/substance-edit-import-dialog.component'; -import { JsonDialogComponent } from '@gsrs-core/substance-form/json-dialog/json-dialog.component'; -import { SubstanceSsg4mService } from './substance-ssg4m-form.service'; -import { environment } from '@gsrs-core/../../environments/environment'; -import { Ssg4mSyntheticPathway } from './model/substance-ssg4m.model'; +import { ConfigService } from "@gsrs-core/config/config.service"; +import { GoogleAnalyticsService } from "../google-analytics/google-analytics.service"; +import { DynamicComponentLoader } from "../dynamic-component-loader/dynamic-component-loader.service"; +import { formSections } from "../substance-form/form-sections.constant"; +import { SubstanceFormSection } from "../substance-form/substance-form-section"; +import { MainNotificationService } from "../main-notification/main-notification.service"; +import { AuthService } from "@gsrs-core/auth"; +import { LoadingService } from "../loading/loading.service"; +import { SubstanceService } from "../substance/substance.service"; +import { SubstanceFormService } from "../substance-form/substance-form.service"; +import { + AppNotification, + NotificationType, +} from "../main-notification/notification.model"; +import { ValidationResults } from "../substance-form/substance-form.model"; +import { SubstanceDetail } from "../substance/substance.model"; +import { + ValidationMessage, + SubstanceFormResults, + SubstanceFormDefinition, +} from "../substance-form/substance-form.model"; +import { SubmitSuccessDialogComponent } from "../substance-form/submit-success-dialog/submit-success-dialog.component"; +import { MergeConceptDialogComponent } from "@gsrs-core/substance-form/merge-concept-dialog/merge-concept-dialog.component"; +import { DefinitionSwitchDialogComponent } from "@gsrs-core/substance-form/definition-switch-dialog/definition-switch-dialog.component"; +import { SubstanceEditImportDialogComponent } from "@gsrs-core/substance-edit-import-dialog/substance-edit-import-dialog.component"; +import { JsonDialogComponent } from "@gsrs-core/substance-form/json-dialog/json-dialog.component"; +import { SubstanceSsg4mService } from "./substance-ssg4m-form.service"; +import { environment } from "@gsrs-core/../../environments/environment"; +import { Ssg4mSyntheticPathway } from "./model/substance-ssg4m.model"; +import { toSvg } from "html-to-image"; import jp from 'jsonpath'; @Component({ - selector: 'app-substance-ssg4m-form', - templateUrl: './substance-ssg4m-form.component.html', - styleUrls: ['./substance-ssg4m-form.component.scss'], - standalone: false + selector: "app-substance-ssg4m-form", + templateUrl: "./substance-ssg4m-form.component.html", + styleUrls: ["./substance-ssg4m-form.component.scss"], + standalone: false }) -export class SubstanceSsg4ManufactureFormComponent implements OnInit, AfterViewInit, OnDestroy { +export class SubstanceSsg4ManufactureFormComponent + implements OnInit, AfterViewInit, OnDestroy +{ isLoading = true; id?: string; formSections: Array = []; - @ViewChildren('dynamicComponent', { read: ViewContainerRef }) dynamicComponents: QueryList; - @ViewChildren('expansionPanel', { read: MatExpansionPanel }) matExpansionPanels: QueryList; + @ViewChildren("dynamicComponent", { read: ViewContainerRef }) + dynamicComponents: QueryList; + @ViewChildren("expansionPanel", { read: MatExpansionPanel }) + matExpansionPanels: QueryList; private subClass: string; definitionType: string; - expandedComponents = [ - 'substance-form-ssg4m-process' - ]; + expandedComponents = ["substance-form-ssg4m-process"]; showSubmissionMessages = false; submissionMessage: string; validationMessages: Array; @@ -72,6 +89,9 @@ export class SubstanceSsg4ManufactureFormComponent implements OnInit, AfterViewI definition: SubstanceFormDefinition; user: string; feature: string; + isAdmin: boolean; + isUpdater: boolean; + isAuthenticated: boolean; messageField: string; errorMessage: string; microserviceStatusUp = false; @@ -79,17 +99,18 @@ export class SubstanceSsg4ManufactureFormComponent implements OnInit, AfterViewI substanceClass: string; status: string; classes = [ - 'concept', - 'protein', - 'chemical', - 'structurallyDiverse', - 'polymer', - 'nucleicAcid', - 'mixture', - 'specifiedSubstanceG1', - 'specifiedSubstanceG2', - 'specifiedSubstanceG3', - 'specifiedSubstanceG4m']; + "concept", + "protein", + "chemical", + "structurallyDiverse", + "polymer", + "nucleicAcid", + "mixture", + "specifiedSubstanceG1", + "specifiedSubstanceG2", + "specifiedSubstanceG3", + "specifiedSubstanceG4m", + ]; imported = false; forceChange = false; sameSubstance = false; @@ -97,22 +118,23 @@ export class SubstanceSsg4ManufactureFormComponent implements OnInit, AfterViewI json: SubstanceDetail; downloadJsonHref: any; jsonFileName: string; - showHeaderBar = 'true'; - showFormReadOnly = 'false'; - showRegisterEditTitle = 'true'; + showHeaderBar = "true"; + showFormReadOnly = "false"; + showRegisterEditTitle = "true"; ssg4mSyntheticPathway: Ssg4mSyntheticPathway; - saveDelayedMessage = ''; + saveDelayedMessage = ""; isCancelBtnClicked = false; isSavedSuccessful = false; public configSettingsDisplay = {}; configSsg4Form: any; configSettingReferences = false; private submitSubscription: any = null; + ssg4mExportSvg: boolean; private jsLibScriptUrls = [ - `${environment.baseHref || ''}assets/pathway/cola.min.js`, - `${environment.baseHref || ''}assets/pathway/d3v4.js`, - `${environment.baseHref || ''}assets/pathway/pathwayviz.js` + `${environment.baseHref || ""}assets/pathway/cola.min.js`, + `${environment.baseHref || ""}assets/pathway/d3v4.js`, + `${environment.baseHref || ""}assets/pathway/pathwayviz.js`, ]; constructor( @@ -130,16 +152,21 @@ export class SubstanceSsg4ManufactureFormComponent implements OnInit, AfterViewI private authService: AuthService, private titleService: Title, private configService: ConfigService, - private sanitizer: DomSanitizer - ) { - } + private sanitizer: DomSanitizer, + ) {} ngOnInit() { - this.showHeaderBar = this.activatedRoute.snapshot.queryParams['header'] || 'true'; - this.showFormReadOnly = this.activatedRoute.snapshot.queryParams['readonly'] || 'false'; + this.showHeaderBar = + this.activatedRoute.snapshot.queryParams["header"] || "true"; + this.showFormReadOnly = + this.activatedRoute.snapshot.queryParams["readonly"] || "false"; this.loadingService.setLoading(true); + this.isAdmin = this.authService.hasRoles("admin"); + this.isUpdater = this.authService.hasAnyRoles("Updater", "SuperUpdater"); + this.isAuthenticated = this.authService.getUser() !== ""; this.overlayContainer = this.overlayContainerService.getContainerElement(); this.imported = false; + this.ssg4mExportSvg = this.configService.configData.ssg4mExportSvg || false; this.getConfigSettings(); if (this.configSsg4Form) { @@ -147,97 +174,140 @@ export class SubstanceSsg4ManufactureFormComponent implements OnInit, AfterViewI this.showRegisterEditTitle = this.configSsg4Form.showRegisterEditTitle; } - this.substanceClass = 'specifiedSubstanceG4m'; - - const routeSubscription = this.activatedRoute - .params - .subscribe(params => { - if (params['id']) { - const id = params['id']; - if (id !== this.id) { - this.id = id; - this.gaService.sendPageView(`Substance Edit`); - this.titleService.setTitle('Edit - Specified Substance Group 4 Manufacturing'); - const newType = this.activatedRoute.snapshot.queryParamMap.get('switch') || null; - if (newType) { - this.getSsg4mDetails(newType); - } else { - this.getSsg4mDetails(); - } - } // if Id - } else if (this.activatedRoute.snapshot.queryParams['action']) { // import in new register form - const actionParam = this.activatedRoute.snapshot.queryParams['action'] || null; - if (actionParam && actionParam === 'import' && window.history.state) { - const record = window.history.state.record; - if ((record) && this.jsonValid(record)) { - const responseImport = JSON.parse(record); - if (responseImport) { - this.substanceFormService.loadSubstance(this.substanceClass, responseImport).pipe(take(1)).subscribe(() => { + this.substanceClass = "specifiedSubstanceG4m"; + + const routeSubscription = this.activatedRoute.params.subscribe((params) => { + if (params["id"]) { + const id = params["id"]; + if (id !== this.id) { + this.id = id; + this.gaService.sendPageView(`Substance Edit`); + this.titleService.setTitle( + "Edit - Specified Substance Group 4 Manufacturing", + ); + const newType = + this.activatedRoute.snapshot.queryParamMap.get("switch") || null; + if (newType) { + this.getSsg4mDetails(newType); + } else { + this.getSsg4mDetails(); + } + } // if Id + } else if (this.activatedRoute.snapshot.queryParams["action"]) { + // import in new register form + const actionParam = + this.activatedRoute.snapshot.queryParams["action"] || null; + if (actionParam && actionParam === "import" && window.history.state) { + const record = window.history.state.record; + if (record && this.jsonValid(record)) { + const responseImport = JSON.parse(record); + if (responseImport) { + this.substanceFormService + .loadSubstance(this.substanceClass, responseImport) + .pipe(take(1)) + .subscribe(() => { this.setFormSections(formSections[this.substanceClass]); setTimeout(() => { this.forceChange = true; this.dynamicComponents.forEach((cRef, index) => { this.dynamicComponentLoader - .getComponentFactory(this.formSections[index].dynamicComponentName) - .subscribe(componentFactory => { - this.formSections[index].dynamicComponentRef = cRef.createComponent(componentFactory); - this.formSections[index].matExpansionPanel = this.matExpansionPanels.find((item, panelIndex) => index === panelIndex); - this.formSections[index].dynamicComponentRef.instance.menuLabelUpdate.pipe(take(1)).subscribe(label => { - this.formSections[index].menuLabel = label; - }); - const hiddenStateSubscription = - this.formSections[index].dynamicComponentRef.instance.hiddenStateUpdate.subscribe(isHidden => { - this.formSections[index].isHidden = isHidden; + .getComponentFactory( + this.formSections[index].dynamicComponentName, + ) + .subscribe((componentFactory) => { + this.formSections[index].dynamicComponentRef = + cRef.createComponent(componentFactory); + this.formSections[index].matExpansionPanel = + this.matExpansionPanels.find( + (item, panelIndex) => index === panelIndex, + ); + this.formSections[ + index + ].dynamicComponentRef.instance.menuLabelUpdate + .pipe(take(1)) + .subscribe((label) => { + this.formSections[index].menuLabel = label; }); + const hiddenStateSubscription = this.formSections[ + index + ].dynamicComponentRef.instance.hiddenStateUpdate.subscribe( + (isHidden) => { + this.formSections[index].isHidden = isHidden; + }, + ); this.subscriptions.push(hiddenStateSubscription); - this.formSections[index].dynamicComponentRef.instance.canAddItemUpdate.pipe(take(1)).subscribe(isList => { - this.formSections[index].canAddItem = isList; - if (isList) { - const aieSubscription = this.formSections[index].addItemEmitter.subscribe(() => { - this.formSections[index].matExpansionPanel.open(); - this.formSections[index].dynamicComponentRef.instance.addItem(); - }); - this.formSections[index].dynamicComponentRef.instance.componentDestroyed.pipe(take(1)).subscribe(() => { - aieSubscription.unsubscribe(); - }); - } - }); - this.formSections[index].dynamicComponentRef.changeDetectorRef.detectChanges(); + this.formSections[ + index + ].dynamicComponentRef.instance.canAddItemUpdate + .pipe(take(1)) + .subscribe((isList) => { + this.formSections[index].canAddItem = isList; + if (isList) { + const aieSubscription = this.formSections[ + index + ].addItemEmitter.subscribe(() => { + this.formSections[ + index + ].matExpansionPanel.open(); + this.formSections[ + index + ].dynamicComponentRef.instance.addItem(); + }); + this.formSections[ + index + ].dynamicComponentRef.instance.componentDestroyed + .pipe(take(1)) + .subscribe(() => { + aieSubscription.unsubscribe(); + }); + } + }); + this.formSections[ + index + ].dynamicComponentRef.changeDetectorRef.detectChanges(); }); }); }); - }); // load Substance in Import on new Register page - } + }); // load Substance in Import on new Register page } } + } - this.loadingService.setLoading(false); - this.isLoading = false; + this.loadingService.setLoading(false); + this.isLoading = false; - // this.imported = true; - // this.getDetailsFromImport(record.record); - /* } else { + // this.imported = true; + // this.getDetailsFromImport(record.record); + /* } else { this.copy = this.activatedRoute.snapshot.queryParams['copy'] || null; if (this.copy) { const copyType = this.activatedRoute.snapshot.queryParams['copyType'] || null; this.getPartialSubstanceDetails(this.copy, copyType); this.gaService.sendPageView(`Substance Register`); } */ - } else { // new record - setTimeout(() => { - this.gaService.sendPageView(`Substance Register`); - this.subClass = this.activatedRoute.snapshot.params['type'] || 'specifiedSubstanceG4m'; - this.substanceClass = this.subClass; - this.titleService.setTitle('Register - Specified Substance Group 4 Manufacturing'); - this.substanceFormService.loadSubstance(this.substanceClass).pipe(take(1)).subscribe(() => { + } else { + // new record + setTimeout(() => { + this.gaService.sendPageView(`Substance Register`); + this.subClass = + this.activatedRoute.snapshot.params["type"] || + "specifiedSubstanceG4m"; + this.substanceClass = this.subClass; + this.titleService.setTitle( + "Register - Specified Substance Group 4 Manufacturing", + ); + this.substanceFormService + .loadSubstance(this.substanceClass) + .pipe(take(1)) + .subscribe(() => { this.setFormSections(formSections[this.substanceClass]); this.loadingService.setLoading(false); this.isLoading = false; }); - }); - } //else - }); + }); + } //else + }); this.subscriptions.push(routeSubscription); const routerSubscription = this.router.events.subscribe((event: Event) => { if (event instanceof NavigationStart) { @@ -264,84 +334,112 @@ export class SubstanceSsg4ManufactureFormComponent implements OnInit, AfterViewI }); */ // Scheme View loading - if (!window['schemeUtil']) { - for (let i = 0; i < this.jsLibScriptUrls.length; i++) { - const node = document.createElement('script'); - node.src = this.jsLibScriptUrls[i]; - node.type = 'text/javascript'; - node.async = false; - document.getElementsByTagName('head')[0].appendChild(node); - } - } + // if (!window['schemeUtil']) { + // for (let i = 0; i < this.jsLibScriptUrls.length; i++) { + // const node = document.createElement('script'); + // node.src = this.jsLibScriptUrls[i]; + // node.type = 'text/javascript'; + // node.async = false; + // document.getElementsByTagName('head')[0].appendChild(node); + // } + // } } ngAfterViewInit(): void { - const subscription = this.dynamicComponents.changes - .subscribe(() => { - - const total = this.formSections.length; - let finished = 0; - if (!this.forceChange) { - this.loadingService.setLoading(true); - const startTime = new Date(); - this.dynamicComponents.forEach((cRef, index) => { - this.dynamicComponentLoader - .getComponentFactory(this.formSections[index].dynamicComponentName) - .subscribe(componentFactory => { - this.loadingService.setLoading(true); - this.formSections[index].dynamicComponentRef = cRef.createComponent(componentFactory); - this.formSections[index].matExpansionPanel = this.matExpansionPanels.find((item, panelIndex) => index === panelIndex); - this.formSections[index].dynamicComponentRef.instance.menuLabelUpdate.pipe(take(1)).subscribe(label => { + const subscription = this.dynamicComponents.changes.subscribe(() => { + const total = this.formSections.length; + let finished = 0; + if (!this.forceChange) { + this.loadingService.setLoading(true); + const startTime = new Date(); + this.dynamicComponents.forEach((cRef, index) => { + this.dynamicComponentLoader + .getComponentFactory( + this.formSections[index].dynamicComponentName, + ) + .subscribe((componentFactory) => { + this.loadingService.setLoading(true); + this.formSections[index].dynamicComponentRef = + cRef.createComponent(componentFactory); + this.formSections[index].matExpansionPanel = + this.matExpansionPanels.find( + (item, panelIndex) => index === panelIndex, + ); + this.formSections[ + index + ].dynamicComponentRef.instance.menuLabelUpdate + .pipe(take(1)) + .subscribe((label) => { this.formSections[index].menuLabel = label; }); - const hiddenStateSubscription = - this.formSections[index].dynamicComponentRef.instance.hiddenStateUpdate.subscribe(isHidden => { - this.formSections[index].isHidden = isHidden; - }); - this.subscriptions.push(hiddenStateSubscription); - this.formSections[index].dynamicComponentRef.instance.canAddItemUpdate.pipe(take(1)).subscribe(isList => { + const hiddenStateSubscription = this.formSections[ + index + ].dynamicComponentRef.instance.hiddenStateUpdate.subscribe( + (isHidden) => { + this.formSections[index].isHidden = isHidden; + }, + ); + this.subscriptions.push(hiddenStateSubscription); + this.formSections[ + index + ].dynamicComponentRef.instance.canAddItemUpdate + .pipe(take(1)) + .subscribe((isList) => { this.formSections[index].canAddItem = isList; if (isList) { - const aieSubscription = this.formSections[index].addItemEmitter.subscribe(() => { + const aieSubscription = this.formSections[ + index + ].addItemEmitter.subscribe(() => { this.formSections[index].matExpansionPanel.open(); - this.formSections[index].dynamicComponentRef.instance.addItem(); - }); - this.formSections[index].dynamicComponentRef.instance.componentDestroyed.pipe(take(1)).subscribe(() => { - aieSubscription.unsubscribe(); + this.formSections[ + index + ].dynamicComponentRef.instance.addItem(); }); + this.formSections[ + index + ].dynamicComponentRef.instance.componentDestroyed + .pipe(take(1)) + .subscribe(() => { + aieSubscription.unsubscribe(); + }); } }); - this.formSections[index].dynamicComponentRef.changeDetectorRef.detectChanges(); - finished++; - if (finished >= total) { - this.loadingService.setLoading(false); - } else { - const currentTime = new Date(); - if (currentTime.getTime() - startTime.getTime() > 12000) { - if (confirm('There was a network error while fetching files, would you like to refresh?')) { - window.location.reload(); - } + this.formSections[ + index + ].dynamicComponentRef.changeDetectorRef.detectChanges(); + finished++; + if (finished >= total) { + this.loadingService.setLoading(false); + } else { + const currentTime = new Date(); + if (currentTime.getTime() - startTime.getTime() > 12000) { + if ( + confirm( + "There was a network error while fetching files, would you like to refresh?", + ) + ) { + window.location.reload(); } } - setTimeout(() => { - this.loadingService.setLoading(false); - // this.UNII = this.substanceSsg4mService.getUNII(); - }, 5); - }); - }); - // this.loadingService.setLoading(false); - - } - subscription.unsubscribe(); - }); + } + setTimeout(() => { + this.loadingService.setLoading(false); + // this.UNII = this.substanceSsg4mService.getUNII(); + }, 5); + }); + }); + // this.loadingService.setLoading(false); + } + subscription.unsubscribe(); + }); } private setFormSections(sectionNames: Array = []): void { this.formSections = []; - sectionNames.forEach(sectionName => { + sectionNames.forEach((sectionName) => { let canAdd = true; if (this.configSettingReferences === false) { - if (sectionName === 'substance-form-references') { + if (sectionName === "substance-form-references") { canAdd = false; } } @@ -355,24 +453,27 @@ export class SubstanceSsg4ManufactureFormComponent implements OnInit, AfterViewI getConfigSettings(): void { // Get SSG4 Config Settings from config.json file to show and hide fields in the form // let configSsg4Form: any; - this.configSsg4Form = this.configService.configData && this.configService.configData.ssg4Form || null; + this.configSsg4Form = + (this.configService.configData && + this.configService.configData.ssg4Form) || + null; // *** IMPORTANT: get the correct value. Get 'startingMaterial' json values from config let configReferences = this.configSsg4Form.settingsDisplay.references; this.configSettingReferences = false; - if (configReferences === 'simple') { + if (configReferences === "simple") { this.configSettingReferences = true; - } else if (configReferences === 'advanced') { + } else if (configReferences === "advanced") { this.configSettingReferences = false; - } else if (configReferences === 'removed') { + } else if (configReferences === "removed") { this.configSettingReferences = false; } } openedChange(event: any) { if (event) { - this.overlayContainer.style.zIndex = '1002'; + this.overlayContainer.style.zIndex = "1002"; } else { - this.overlayContainer.style.zIndex = '1000'; + this.overlayContainer.style.zIndex = "1000"; } } @@ -449,33 +550,35 @@ export class SubstanceSsg4ManufactureFormComponent implements OnInit, AfterViewI ngOnDestroy(): void { // this.substanceFormService.unloadSubstance(); - this.subscriptions.forEach(subscription => { + this.subscriptions.forEach((subscription) => { subscription.unsubscribe(); }); } - importDialog(): void { let data: any; data = { - title: 'Manufacturing Scheme Import' + title: "Manufacturing Scheme Import", }; const dialogRef = this.dialog.open(SubstanceEditImportDialogComponent, { - width: '650px', + width: "650px", autoFocus: false, - data: data + data: data, }); - this.overlayContainer.style.zIndex = '1002'; + this.overlayContainer.style.zIndex = "1002"; - const dialogSubscription = dialogRef.afterClosed().pipe(take(1)).subscribe(response => { - if (response) { - this.loadingService.setLoading(true); - this.overlayContainer.style.zIndex = null; + const dialogSubscription = dialogRef + .afterClosed() + .pipe(take(1)) + .subscribe((response) => { + if (response) { + this.loadingService.setLoading(true); + this.overlayContainer.style.zIndex = null; - // attempting to reload a substance without a router refresh has proven to cause issues with the relationship dropdowns - // There are probably other components affected. There is an issue with subscriptions likely due to some OnInit not firing + // attempting to reload a substance without a router refresh has proven to cause issues with the relationship dropdowns + // There are probably other components affected. There is an issue with subscriptions likely due to some OnInit not firing - /* const read = JSON.parse(response); + /* const read = JSON.parse(response); if (this.id && read.uuid && this.id === read.uuid) { this.substanceFormService.importSubstance(read, 'update'); this.submissionMessage = null; @@ -493,20 +596,30 @@ export class SubstanceSsg4ManufactureFormComponent implements OnInit, AfterViewI this.loadingService.setLoading(false); this.isLoading = false; } else {*/ - setTimeout(() => { - this.router.onSameUrlNavigation = 'reload'; - this.loadingService.setLoading(false); - if (this.id) { - this.router.navigateByUrl('/substances-ssg4m/' + this.id + '/edit?action=import&header=' + this.showHeaderBar, { state: { record: response } }); - } else { // new record - this.router.navigateByUrl('/substances-ssg4m/register?action=import&header=' + this.showHeaderBar, { state: { record: response } }); - } - }, 1000); - } - // } - // } - }); - + setTimeout(() => { + this.router.onSameUrlNavigation = "reload"; + this.loadingService.setLoading(false); + if (this.id) { + this.router.navigateByUrl( + "/substances-ssg4m/" + + this.id + + "/edit?action=import&header=" + + this.showHeaderBar, + { state: { record: response } }, + ); + } else { + // new record + this.router.navigateByUrl( + "/substances-ssg4m/register?action=import&header=" + + this.showHeaderBar, + { state: { record: response } }, + ); + } + }, 1000); + } + // } + // } + }); } test() { @@ -515,8 +628,8 @@ export class SubstanceSsg4ManufactureFormComponent implements OnInit, AfterViewI } canBeApproved(): boolean { - const action = this.activatedRoute.snapshot.queryParams['action'] || null; - if (action && action === 'import') { + const action = this.activatedRoute.snapshot.queryParams["action"] || null; + if (action && action === "import") { return false; } if (this.definition && this.definition.lastEditedBy && this.user) { @@ -524,171 +637,309 @@ export class SubstanceSsg4ManufactureFormComponent implements OnInit, AfterViewI if (!lastEdit) { return false; } - if (this.definition.status === 'approved') { + if (this.definition.status === "approved") { return false; } if (lastEdit === this.user) { return false; } return true; - } return false; } showJSON(): void { + const json = this.substanceFormService.cleanSubstance(); + this.removeTmpStructureIdFields(json); const dialogRef = this.dialog.open(JsonDialogComponent, { - width: '90%' + width: "90%", + data: { substance: json }, }); - this.overlayContainer.style.zIndex = '1002'; - - const dialogSubscription = dialogRef.afterClosed().pipe(take(1)).subscribe(response => { + this.overlayContainer.style.zIndex = "1002"; - }); + const dialogSubscription = dialogRef + .afterClosed() + .pipe(take(1)) + .subscribe((response) => {}); this.subscriptions.push(dialogSubscription); } saveJSON(): void { // apply the same cleaning to remove deleted objects and return what will be sent to the server on validation / submission this.json = this.substanceFormService.cleanSubstance(); - // this.json = this.cleanObject(substanceCopy); - const uri = this.sanitizer.bypassSecurityTrustUrl('data:text/json;charset=UTF-8,' + encodeURIComponent(JSON.stringify(this.json))); + // Remove $$tmpStructureId fields before export + this.removeTmpStructureIdFields(this.json); + const uri = this.sanitizer.bypassSecurityTrustUrl( + "data:text/json;charset=UTF-8," + + encodeURIComponent(JSON.stringify(this.json)), + ); this.downloadJsonHref = uri; const date = new Date(); - this.jsonFileName = 'SSG4m_' + moment(date).format('MMM-DD-YYYY_H-mm-ss'); + this.jsonFileName = "SSG4m_" + moment(date).format("MMM-DD-YYYY_H-mm-ss"); } - checkSsg4mServerStatus(): void { - // Check Microservice Server Status - this.substanceSsg4mService.checkSsg4mServerStatus().pipe(take(1)).subscribe(responseCheck => { - if (responseCheck) { - if (responseCheck["status"]) { - if (responseCheck["status"] === 'OK') { - this.microserviceStatusUp = true; - } + saveLocalBatch(): void { + const json = this.substanceFormService.cleanSubstance(); + this.removeTmpStructureIdFields(json); + + const timestamp = moment(new Date()).format("MMM-DD-YYYY_H-mm-ss"); + + // Download the SSG4M substance file + this.downloadFile(JSON.stringify(json), "SSG4m_" + timestamp + ".json"); + + // Collect all refuuids from materials in the SSG4M hierarchy + const referencedUuids = new Set(); + if (json.specifiedSubstanceG4m && json.specifiedSubstanceG4m.process) { + for (const process of json.specifiedSubstanceG4m.process) { + if (!process.sites) { + continue; } - } else { - this.microserviceStatusUp = false; - } - }, error => { - // if it is authentication issue or status is 0, reload the current page - - // These lines did not work. Commenting out right now. Caused forever refresh loop. - // if (error.status === 0) { - // window.location.reload(); - // } - - if (error.status === 0) { - console.log("Error Status is 0"); - let totalNumberRefresh = 2; - let preventRefresh = parseInt((new URLSearchParams(window.location.search)).get("refreshcount")); - // if parameter 'refreshcount' is NOT found in the URL, add refreshcount in the URL. - // refresh n/totalNumberRefresh times - if (!preventRefresh || (preventRefresh && Number(preventRefresh) < totalNumberRefresh)) { - let url = window.location.href; - // if question mark '?' found in the URL, add/append '&', otherwise add/append '?' in the URL - if (!preventRefresh) { // not found preventRefresh in URL - preventRefresh = 1; - if (url.indexOf('?') > -1) { - url += '&refreshcount=' + preventRefresh; - } else { - url += '?refreshcount=' + preventRefresh; + for (const site of process.sites) { + if (!site.stages) { + continue; + } + for (const stage of site.stages) { + const allMaterials = [ + ...(stage.startingMaterials || []), + ...(stage.processingMaterials || []), + ...(stage.resultingMaterials || []), + ]; + for (const material of allMaterials) { + if (material.substanceName && material.substanceName.refuuid) { + referencedUuids.add(material.substanceName.refuuid); + } } - } else { // found refreshcount in the URL - let currentCountUrl = 'refreshcount=' + preventRefresh; - // add 1 to current count - preventRefresh = preventRefresh + 1; - let newCounturl = 'refreshcount=' + preventRefresh; - // replace 'refreshcount=1' to 'refreshcount=2' - url = url.replace(currentCountUrl, newCounturl); } - // Display error message to users that the page will refresh n number of times. - this.errorMessage = "There was a connection problem loading the page. Automatically refreshing " + preventRefresh + " of " + totalNumberRefresh + " times ...

"; - - setTimeout(() => { - window.location.href = url; - }, 2000); } } + } - this.microserviceStatusUp = false; - if (!this.errorMessage) { - this.errorMessage = ''; - } - this.errorMessage = this.errorMessage + "Unable to load the data for Record ID " + this.id + "

"; - if (error && error.error && error.error.message) { - this.errorMessage = this.errorMessage + 'Server Error ' + (error.status + ': ' || ': ') + error.error.message; - } else if (error && error.error && (typeof error.error) === 'string') { - this.errorMessage = this.errorMessage + '
Server Error ' + (error.status + ': ' || '') + error.error; - } else if (error && error.message) { - this.errorMessage = this.errorMessage + '
Server Error ' + (error.status + ': ' || '') + error.message; - this.errorMessage = this.errorMessage + "
It looks like the SSG4m microservice is not running.
"; - this.errorMessage = this.errorMessage + "Please ask your system administrator to verify that the SSG4m microservice is running without error, and also to examine the website logs to:
- check if there are any database connection issues, and
- make sure the system is using valid authentication credentials into the database"; - } - else { - this.errorMessage = this.errorMessage + 'There could be an authentication issue.
-Make sure that you are logged into the GSRS website.
-Clear your browser cache.
-Reload your SSG4 page or Appian'; + // Find matching drafts in localStorage and download each as a separate file + const keys = Object.keys(localStorage); + for (const key of keys) { + if (key.startsWith("gsrs-draft-")) { + const draft = JSON.parse(localStorage.getItem(key)); + if ( + draft && + draft.substance && + referencedUuids.has(draft.substance.uuid) + ) { + const name = draft.substance.uuid || draft.name || "unknown"; + const safeName = name.replace(/[^a-zA-Z0-9_-]/g, "_"); + this.downloadFile( + JSON.stringify(draft.substance), + "Draft_" + safeName + "_" + timestamp + ".json", + ); + } } - }); + } + } + + private downloadFile(content: string, fileName: string): void { + const blob = new Blob([content], { type: "application/json" }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = fileName; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + } + + checkSsg4mServerStatus(): void { + // Check Microservice Server Status + this.substanceSsg4mService + .checkSsg4mServerStatus() + .pipe(take(1)) + .subscribe( + (responseCheck) => { + if (responseCheck) { + if (responseCheck["status"]) { + if (responseCheck["status"] === "OK") { + this.microserviceStatusUp = true; + } + } + } else { + this.microserviceStatusUp = false; + } + }, + (error) => { + // if it is authentication issue or status is 0, reload the current page + + // These lines did not work. Commenting out right now. Caused forever refresh loop. + // if (error.status === 0) { + // window.location.reload(); + // } + + if (error.status === 0) { + console.log("Error Status is 0"); + let totalNumberRefresh = 2; + let preventRefresh = parseInt( + new URLSearchParams(window.location.search).get("refreshcount"), + ); + // if parameter 'refreshcount' is NOT found in the URL, add refreshcount in the URL. + // refresh n/totalNumberRefresh times + if ( + !preventRefresh || + (preventRefresh && Number(preventRefresh) < totalNumberRefresh) + ) { + let url = window.location.href; + // if question mark '?' found in the URL, add/append '&', otherwise add/append '?' in the URL + if (!preventRefresh) { + // not found preventRefresh in URL + preventRefresh = 1; + if (url.indexOf("?") > -1) { + url += "&refreshcount=" + preventRefresh; + } else { + url += "?refreshcount=" + preventRefresh; + } + } else { + // found refreshcount in the URL + let currentCountUrl = "refreshcount=" + preventRefresh; + // add 1 to current count + preventRefresh = preventRefresh + 1; + let newCounturl = "refreshcount=" + preventRefresh; + // replace 'refreshcount=1' to 'refreshcount=2' + url = url.replace(currentCountUrl, newCounturl); + } + // Display error message to users that the page will refresh n number of times. + this.errorMessage = + "There was a connection problem loading the page. Automatically refreshing " + + preventRefresh + + " of " + + totalNumberRefresh + + " times ...

"; + + setTimeout(() => { + window.location.href = url; + }, 2000); + } + } + + this.microserviceStatusUp = false; + if (!this.errorMessage) { + this.errorMessage = ""; + } + this.errorMessage = + this.errorMessage + + "Unable to load the data for Record ID " + + this.id + + "

"; + if (error && error.error && error.error.message) { + this.errorMessage = + this.errorMessage + + "Server Error " + + (error.status + ": " || ": ") + + error.error.message; + } else if (error && error.error && typeof error.error === "string") { + this.errorMessage = + this.errorMessage + + "
Server Error " + + (error.status + ": " || "") + + error.error; + } else if (error && error.message) { + this.errorMessage = + this.errorMessage + + "
Server Error " + + (error.status + ": " || "") + + error.message; + this.errorMessage = + this.errorMessage + + "
It looks like the SSG4m microservice is not running.
"; + this.errorMessage = + this.errorMessage + + "Please ask your system administrator to verify that the SSG4m microservice is running without error, and also to examine the website logs to:
- check if there are any database connection issues, and
- make sure the system is using valid authentication credentials into the database"; + } else { + this.errorMessage = + this.errorMessage + + "There could be an authentication issue.
-Make sure that you are logged into the GSRS website.
-Clear your browser cache.
-Reload your SSG4 page or Appian"; + } + }, + ); } getSsg4mDetails(newType?: string): void { - let generateErrorMessage = "Unable to load the data for Record ID " + this.id + "

"; + let generateErrorMessage = + "Unable to load the data for Record ID " + this.id + "

"; // Check if SSG4m microservice server is UP or not this.checkSsg4mServerStatus(); - this.substanceSsg4mService.getSsg4mDetails(this.id).pipe(take(1)).subscribe(response => { - /*if (response._name) { + this.substanceSsg4mService + .getSsg4mDetails(this.id) + .pipe(take(1)) + .subscribe( + (response) => { + /*if (response._name) { this.titleService.setTitle('Edit - ' + response._name); }*/ - if (response) { - // Check for import in URL - const action = this.activatedRoute.snapshot.queryParams['action'] || null; - let substanceSsg4mFromDb: SubstanceDetail; - - this.ssg4mSyntheticPathway = response; - - if (action && action === 'import' && window.history.state) { - // this.gaService.sendPageView(`Substance Import`); - const record = window.history.state.record; - // this.imported = true; - // this.getDetailsFromImport(record.record); - if ((record) && this.jsonValid(record)) { - const responseImport = JSON.parse(record); - if (responseImport) { - this.substanceFormService.loadSubstance(this.substanceClass, responseImport).pipe(take(1)).subscribe(() => { - // this.substanceSsg4mService.loadSubstance(this.subClass).pipe(take(1)).subscribe(() => { - this.setFormSections(formSections[this.substanceClass]); - }); + if (response) { + // Check for import in URL + const action = + this.activatedRoute.snapshot.queryParams["action"] || null; + let substanceSsg4mFromDb: SubstanceDetail; + + this.ssg4mSyntheticPathway = response; + + if (action && action === "import" && window.history.state) { + // this.gaService.sendPageView(`Substance Import`); + const record = window.history.state.record; + // this.imported = true; + // this.getDetailsFromImport(record.record); + if (record && this.jsonValid(record)) { + const responseImport = JSON.parse(record); + if (responseImport) { + this.substanceFormService + .loadSubstance(this.substanceClass, responseImport) + .pipe(take(1)) + .subscribe(() => { + // this.substanceSsg4mService.loadSubstance(this.subClass).pipe(take(1)).subscribe(() => { + this.setFormSections(formSections[this.substanceClass]); + }); + } + } + } else if (response.sbmsnDataText) { + // If JSON form data found into the database, load into the form + substanceSsg4mFromDb = JSON.parse(response.sbmsnDataText); + + this.substanceFormService + .loadSubstance( + substanceSsg4mFromDb.substanceClass, + substanceSsg4mFromDb, + ) + .pipe(take(1)) + .subscribe(() => { + this.setFormSections(formSections[this.substanceClass]); + }); + } else if (!response.sbmsnDataText) { + // AS NEW FORM, If JSON form data NOT found into the database, Load the Form as a new Form + this.substanceFormService + .loadSubstance(this.substanceClass) + .pipe(take(1)) + .subscribe(() => { + // this.substanceSsg4mService.loadSubstance(this.subClass).pipe(take(1)).subscribe(() => { + this.setFormSections(formSections[this.substanceClass]); + }); } + } else if (response === null) { + this.errorMessage = + "There is no data found in the database for ID: " + this.id; } - } - else if (response.sbmsnDataText) { // If JSON form data found into the database, load into the form - substanceSsg4mFromDb = JSON.parse(response.sbmsnDataText); - - this.substanceFormService.loadSubstance(substanceSsg4mFromDb.substanceClass, substanceSsg4mFromDb).pipe(take(1)).subscribe(() => { - this.setFormSections(formSections[this.substanceClass]); - }); - } else if (!response.sbmsnDataText) { // AS NEW FORM, If JSON form data NOT found into the database, Load the Form as a new Form - this.substanceFormService.loadSubstance(this.substanceClass).pipe(take(1)).subscribe(() => { - // this.substanceSsg4mService.loadSubstance(this.subClass).pipe(take(1)).subscribe(() => { - this.setFormSections(formSections[this.substanceClass]); - }); - } - } else if (response === null) { - this.errorMessage = "There is no data found in the database for ID: " + this.id; - } - this.loadingService.setLoading(false); - this.isLoading = false; - }, error => { // Getting Error while getting Record - if (this.microserviceStatusUp === false) { - } - this.gaService.sendException('getSsg4mDetails: error from API call'); - this.loadingService.setLoading(false); - this.isLoading = false; - // this.handleSubstanceRetrivalError(); - }); + this.loadingService.setLoading(false); + this.isLoading = false; + }, + (error) => { + // Getting Error while getting Record + if (this.microserviceStatusUp === false) { + } + this.gaService.sendException("getSsg4mDetails: error from API call"); + this.loadingService.setLoading(false); + this.isLoading = false; + // this.handleSubstanceRetrivalError(); + }, + ); } jsonValid(file: any): boolean { @@ -707,103 +958,149 @@ export class SubstanceSsg4ManufactureFormComponent implements OnInit, AfterViewI this.definitionType = response.definitionType; this.substanceClass = response.substanceClass; this.status = response.status; - this.substanceFormService.loadSubstance(response.substanceClass, response, 'import').pipe(take(1)).subscribe(() => { - // this.substanceSsg4mService.loadSubstance(response.substanceClass, response, 'import').pipe(take(1)).subscribe(() => { - this.setFormSections(formSections[response.substanceClass]); - if (!same) { - setTimeout(() => { - this.forceChange = true; - this.dynamicComponents.forEach((cRef, index) => { - this.dynamicComponentLoader - .getComponentFactory(this.formSections[index].dynamicComponentName) - .subscribe(componentFactory => { - this.formSections[index].dynamicComponentRef = cRef.createComponent(componentFactory); - this.formSections[index].matExpansionPanel = this.matExpansionPanels.find((item, panelIndex) => index === panelIndex); - this.formSections[index].dynamicComponentRef.instance.menuLabelUpdate.pipe(take(1)).subscribe(label => { - this.formSections[index].menuLabel = label; - }); - const hiddenStateSubscription = - this.formSections[index].dynamicComponentRef.instance.hiddenStateUpdate.subscribe(isHidden => { - this.formSections[index].isHidden = isHidden; + this.substanceFormService + .loadSubstance(response.substanceClass, response, "import") + .pipe(take(1)) + .subscribe( + () => { + // this.substanceSsg4mService.loadSubstance(response.substanceClass, response, 'import').pipe(take(1)).subscribe(() => { + this.setFormSections(formSections[response.substanceClass]); + if (!same) { + setTimeout(() => { + this.forceChange = true; + this.dynamicComponents.forEach((cRef, index) => { + this.dynamicComponentLoader + .getComponentFactory( + this.formSections[index].dynamicComponentName, + ) + .subscribe((componentFactory) => { + this.formSections[index].dynamicComponentRef = + cRef.createComponent(componentFactory); + this.formSections[index].matExpansionPanel = + this.matExpansionPanels.find( + (item, panelIndex) => index === panelIndex, + ); + this.formSections[ + index + ].dynamicComponentRef.instance.menuLabelUpdate + .pipe(take(1)) + .subscribe((label) => { + this.formSections[index].menuLabel = label; + }); + const hiddenStateSubscription = this.formSections[ + index + ].dynamicComponentRef.instance.hiddenStateUpdate.subscribe( + (isHidden) => { + this.formSections[index].isHidden = isHidden; + }, + ); + this.subscriptions.push(hiddenStateSubscription); + this.formSections[ + index + ].dynamicComponentRef.instance.canAddItemUpdate + .pipe(take(1)) + .subscribe((isList) => { + this.formSections[index].canAddItem = isList; + if (isList) { + const aieSubscription = this.formSections[ + index + ].addItemEmitter.subscribe(() => { + this.formSections[index].matExpansionPanel.open(); + this.formSections[ + index + ].dynamicComponentRef.instance.addItem(); + }); + this.formSections[ + index + ].dynamicComponentRef.instance.componentDestroyed + .pipe(take(1)) + .subscribe(() => { + aieSubscription.unsubscribe(); + }); + } + }); + this.formSections[ + index + ].dynamicComponentRef.changeDetectorRef.detectChanges(); }); - this.subscriptions.push(hiddenStateSubscription); - this.formSections[index].dynamicComponentRef.instance.canAddItemUpdate.pipe(take(1)).subscribe(isList => { - this.formSections[index].canAddItem = isList; - if (isList) { - const aieSubscription = this.formSections[index].addItemEmitter.subscribe(() => { - this.formSections[index].matExpansionPanel.open(); - this.formSections[index].dynamicComponentRef.instance.addItem(); - }); - this.formSections[index].dynamicComponentRef.instance.componentDestroyed.pipe(take(1)).subscribe(() => { - aieSubscription.unsubscribe(); - }); - } - }); - this.formSections[index].dynamicComponentRef.changeDetectorRef.detectChanges(); }); - }); - this.canApprove = false; - }); - } - }, error => { - this.loadingService.setLoading(false); - }); + this.canApprove = false; + }); + } + }, + (error) => { + this.loadingService.setLoading(false); + }, + ); } else { this.handleSubstanceRetrivalError(); this.loadingService.setLoading(false); - } this.loadingService.setLoading(false); this.isLoading = false; } getPartialSubstanceDetails(uuid: string, type: string): void { - this.substanceService.getSubstanceDetails(uuid).pipe(take(1)).subscribe(response => { - if (response) { - this.substanceClass = response.substanceClass; - this.status = response.status; - delete response.uuid; - if (response._name) { - delete response._name; - } - this.scrub(response, type); - this.substanceSsg4mService.loadSubstance(response.substanceClass, response).pipe(take(1)).subscribe(() => { - this.setFormSections(formSections[response.substanceClass]); + this.substanceService + .getSubstanceDetails(uuid) + .pipe(take(1)) + .subscribe( + (response) => { + if (response) { + this.substanceClass = response.substanceClass; + this.status = response.status; + delete response.uuid; + if (response._name) { + delete response._name; + } + this.scrub(response, type); + this.substanceSsg4mService + .loadSubstance(response.substanceClass, response) + .pipe(take(1)) + .subscribe(() => { + this.setFormSections(formSections[response.substanceClass]); + this.loadingService.setLoading(false); + this.isLoading = false; + }); + } else { + this.handleSubstanceRetrivalError(); + } + }, + (error) => { + this.gaService.sendException( + "getSubstanceDetails: error from API call", + ); this.loadingService.setLoading(false); this.isLoading = false; - }); - } else { - this.handleSubstanceRetrivalError(); - } - }, error => { - this.gaService.sendException('getSubstanceDetails: error from API call'); - this.loadingService.setLoading(false); - this.isLoading = false; - this.handleSubstanceRetrivalError(); - }); + this.handleSubstanceRetrivalError(); + }, + ); } private handleSubstanceRetrivalError() { const notification: AppNotification = { - message: 'The substance you\'re trying to edit doesn\'t exist.', + message: "The substance you're trying to edit doesn't exist.", type: NotificationType.error, - milisecondsToShow: 4000 + milisecondsToShow: 4000, }; this.mainNotificationService.setNotification(notification); this.errorMessage = "Unable to load the data."; setTimeout(() => { - this.router.navigate(['/home']); - this.substanceSsg4mService.loadSubstance(this.subClass).pipe(take(1)).subscribe(() => { - this.setFormSections(formSections.chemical); - this.loadingService.setLoading(false); - this.isLoading = false; - }); + this.router.navigate(["/home"]); + this.substanceSsg4mService + .loadSubstance(this.subClass) + .pipe(take(1)) + .subscribe(() => { + this.setFormSections(formSections.chemical); + this.loadingService.setLoading(false); + this.isLoading = false; + }); }, 5000); } - validate(validationType?: string): void { - if (validationType && validationType === 'approval') { + async validate(validationType?: string): Promise { + if (validationType && validationType === "approval") { this.approving = true; } else { this.approving = false; @@ -822,7 +1119,7 @@ export class SubstanceSsg4ManufactureFormComponent implements OnInit, AfterViewI this.isLoading = false; // If there is no validation error, submit/save the records without displaying the warning/validation message. if (this.validationMessages.length === 0 && true === true) { - this.submit(); + await this.submit(); } /* if (this.validationMessages.length === 0 && true === true) { @@ -869,7 +1166,7 @@ export class SubstanceSsg4ManufactureFormComponent implements OnInit, AfterViewI */ validateSubstance(): Observable { - return new Observable(observer => { + return new Observable((observer) => { // const substanceCopy = this.cleanSubstance(); // CHANGING THIS NOW CHANGING THIS NOW const substanceCopy = null; @@ -971,14 +1268,176 @@ export class SubstanceSsg4ManufactureFormComponent implements OnInit, AfterViewI } observer.next(results); observer.complete(); - }, error => { + }, + (error) => { observer.error(); observer.complete(); }); }); } - submit(): void { + getPageStyles(): string { + let css = ""; + // Gather all style rules from the document + for (const sheet of Array.from(document.styleSheets)) { + try { + if (sheet.cssRules) { + css += Array.from(sheet.cssRules) + .map((rule) => rule.cssText) + .join("\n"); + } + } catch (e) { + console.warn("Cannot read styles from cross-origin stylesheet", e); + } + } + return ``; + } + + delay(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); + } + + generateTimestampId(): string { + const now = new Date(); + + const pad = (num: number, length: number = 2): string => + String(num).padStart(length, "0"); + + const datePart = `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad( + now.getDate(), + )}`; + const timePart = `${pad(now.getHours())}${pad(now.getMinutes())}${pad( + now.getSeconds(), + )}`; + + return `${datePart}${timePart}`; + } + + private removeTmpStructureIdFields(obj: any): void { + if (!obj || typeof obj !== "object") { + return; + } + if (Array.isArray(obj)) { + for (const item of obj) { + this.removeTmpStructureIdFields(item); + } + return; + } + for (const key of Object.keys(obj)) { + if (key === "$$tmpStructureId") { + try { + delete obj[key]; + } catch (e) { + // ignore + } + continue; + } + const val = obj[key]; + if (val && typeof val === "object") { + this.removeTmpStructureIdFields(val); + } + } + } + + // waitForElement(selector: string, timeout = 2000): Promise { + // return new Promise((resolve, reject) => { + // const interval = setInterval(() => { + // const element = document.querySelector(selector) as HTMLElement; + // if (element) { + // clearInterval(interval); + // resolve(element); + // } + // }, 100); + // setTimeout(() => { + // clearInterval(interval); + // reject(new Error(`Element "${selector}" not found within ${timeout}ms.`)); + // }, timeout); + // }); + // } + + async expandStepView(): Promise { + const tabProcesses = document.querySelector( + "#substance-form-ssg4m-process", + ) as HTMLElement; + if (tabProcesses.getAttribute("aria-expanded") !== "true") { + console.log("Tab Processes not selected. Clicking it..."); + tabProcesses.click(); + await this.delay(200); + } + + const allTabs = document.querySelectorAll(".mat-mdc-tab"); + const tabStepView = Array.from(allTabs).find( + (tab) => tab.textContent.trim() === "Step View", + ) as HTMLElement; + if (tabStepView.getAttribute("aria-selected") !== "true") { + console.log("Tab Step View not selected. Clicking it..."); + tabStepView.click(); + await this.delay(200); + } + } + + async exportStepView(document: Document): Promise { + const elementToConvert = document.querySelector( + "app-ssg4m-scheme-view", + ) as HTMLElement; + const clone = elementToConvert.cloneNode(true) as HTMLElement; + const initialWidth = elementToConvert.offsetWidth; + const initialHeight = elementToConvert.offsetHeight; + + const container = document.createElement("div"); + container.style.position = "absolute"; + container.style.left = "-9999px"; + container.style.padding = "0"; + container.style.margin = "0"; + container.style.display = "inline-block"; + container.style.width = `${initialWidth}px`; + container.style.height = `${initialHeight}px`; + const styles = this.getPageStyles(); + container.innerHTML = styles; + container.appendChild(clone); + document.body.appendChild(container); + clone.style.overflow = "visible"; + clone.style.maxWidth = "none"; + clone.style.boxSizing = "border-box"; + clone.style.fontFamily = "Arial, sans-serif"; + clone.style.display = "flex"; + clone.style.flexDirection = "column"; + clone.style.alignItems = "stretch"; + + await this.delay(2500); + + const finalWidth = initialWidth; + const finalHeight = initialHeight; + + function filter(node: HTMLElement) { + if (!node.tagName) { + return true; + } + + return node.tagName.toLowerCase() !== "button"; + } + const options = { + filter: filter, + width: finalWidth, + height: finalHeight, + fetchRequestInit: { + headers: new Headers(), + mode: "cors" as RequestMode, + cache: "default" as RequestCache, + }, + }; + + const dataUrl = await toSvg(clone, options); + const downloadLink = document.createElement("a"); + downloadLink.href = dataUrl; + downloadLink.download = `ssg4m_step_view_${this.generateTimestampId()}.svg`; + document.body.appendChild(downloadLink); + downloadLink.click(); + document.body.removeChild(downloadLink); + } + + async submit(): Promise { + this.ssg4mExportSvg && await this.expandStepView(); this.isLoading = true; this.loadingService.setLoading(true); this.approving = false; @@ -986,103 +1445,272 @@ export class SubstanceSsg4ManufactureFormComponent implements OnInit, AfterViewI this.json = this.substanceFormService.cleanSubstance(); let jsonValue = JSON.stringify(this.json); - // Save SVG/Image as Blob - //This is a hacky placeholder way to force viz - //TODO finish this - const ssgjs = JSON.stringify(this.substanceFormService.cleanSubstance()); - window["schemeUtil"].onFinishedLayout = (svg) => { - window["schemeUtil"].onFinishedLayout = (svg) => { }; + // Initialize pathway object if new record + if (this.ssg4mSyntheticPathway == null) { + this.ssg4mSyntheticPathway = {}; + } + + // Process and validate local drafts referenced by this form + const hasValidationErrors = await this.processLocalDrafts(jsonValue); + if (hasValidationErrors) { + return; + } - // if New Record, initialize object - if (this.ssg4mSyntheticPathway == null) { - this.ssg4mSyntheticPathway = {}; + setTimeout(() => { + if (this.isSavedSuccessful === false) { + this.saveDelayedMessage = + "Hmm ... this seems to be taking longer than normal, there may be network issues.
Click here to cancel and continue working on the form. We suggest you save a local copy of the JSON."; } + }, 8000); - // Existing Record - // get the JSON from the SSG4m Form and store as a Clob into the database - this.ssg4mSyntheticPathway.sbmsnDataText = jsonValue; + // Export step view as SVG; default disabled + this.ssg4mExportSvg && await this.exportStepView(document); - // Save SVG as Clob - this.ssg4mSyntheticPathway.sbmsnImage = document.querySelector("#scheme-viz-view").innerHTML; + // Prepare final JSON and call save endpoint + jsonValue = this.prepareFinalJson(); + this.ssg4mSyntheticPathway.sbmsnDataText = jsonValue; - // After submitting Save button, the UI waits for 5 seconds to see if it gets a response. - // after 5 seconds it displays a warning on the top of the UI form. - setTimeout(() => { - if (this.isSavedSuccessful === false) { - this.saveDelayedMessage = "Hmm ... this seems to be taking longer than normal, there may be network issues.
Click here to cancel and continue working on the form. We suggest you save a local copy of the JSON."; - } - }, 5000); + this.savePathway(); + } - this.submitSubscription = this.substanceSsg4mService.saveSsg4m(this.ssg4mSyntheticPathway).pipe(take(1)).subscribe(response => { - // Stop the spinner - this.loadingService.setLoading(false); - this.isLoading = false; + // Processes local drafts from localStorage that are referenced by the G4 form. + // Validates and saves each referenced draft. + // returns true if there are validation errors that should stop submission, false otherwise + private async processLocalDrafts(jsonStr: string): Promise { + const draftKeys = Object.keys(localStorage).filter((k) => + k.startsWith("gsrs-draft-"), + ); - // Set validation messages to null - this.validationMessages = null; - this.showSubmissionMessages = false; - this.validationResult = false; + this.validationMessages = []; - // if Saved Successfully - if (response && response.synthPathwaySkey) { - if (response.synthPathwaySkey) { - this.id = response.synthPathwaySkey.toString(); - } + for (const key of draftKeys) { + await this.processSingleDraft(key, jsonStr); + } - // if the API communication does resolve, AND the initial save went through, it will replace - // the warning message. Only show this message when user clicked on the 'Cancel' button. - // After user clicks 'Refresh' button, refresh the page manually. - this.isSavedSuccessful = true; - if (this.isCancelBtnClicked === true && this.isSavedSuccessful === true) { - this.saveDelayedMessage = " Network communication restored, click here to refresh with saved version."; - } - else { - this.saveDelayedMessage = ""; - this.isCancelBtnClicked = false; - // Only show successful dialog and refresh page, if user does not click on the cancel button. - this.openSuccessDialog(); - } - // Refresh the current page, this will not cause record locking issue - /* this.router.routeReuseStrategy.shouldReuseRoute = () => false; - this.router.onSameUrlNavigation = 'reload'; - this.router.navigate(['/substances-ssg4m', this.id, 'edit']); - */ - } - }, (error: SubstanceFormResults) => { - this.loadingService.setLoading(false); - this.isLoading = false; - // If submit was not successful, display Message - this.saveDelayedMessage = " Network communication restored, RECORD HAS NOT BEEN SAVED. Please resave the record."; + if (this.validationMessages && this.validationMessages.length > 0) { + this.loadingService.setLoading(false); + this.isLoading = false; + this.validationResult = false; + this.showSubmissionMessages = true; + return true; + } - this.showSubmissionMessages = true; - this.submissionMessage = null; - if (error.validationMessages && error.validationMessages.length) { - this.validationResult = error.isSuccessfull; - this.validationMessages = error.validationMessages - .filter(message => message.messageType.toUpperCase() === 'ERROR' || message.messageType.toUpperCase() === 'WARNING'); - this.showSubmissionMessages = true; - } else { - this.submissionMessage = 'There was a problem with your submission'; - this.addServerError(error.serverError); - setTimeout(() => { - this.showSubmissionMessages = false; - this.submissionMessage = null; - }, 8000); - } + return false; + } + + // Processes a single draft from localStorage. + private async processSingleDraft( + key: string, + jsonStr: string, + ): Promise { + try { + const entry = JSON.parse(localStorage.getItem(key)); + console.log( + "Processing draft entry from localStorage key: " + + JSON.stringify(entry), + ); + + if (!entry || !entry.substance) { + return; + } + + const draftSub = entry.substance; + + if (!this.isDraftReferencedInForm(draftSub, entry, jsonStr)) { + return; + } + + const hasErrors = await this.validateAndCollectDraftErrors( + draftSub, + entry, + ); + if (hasErrors) { + return; + } + + // Clean up temporary fields and save + if (draftSub && draftSub.$$tmpStructureId) { + delete draftSub.$$tmpStructureId; + } + + await this.saveDraftSubstance(draftSub); + } catch (e) { + this.addServerError(e.serverError); + } + } + + // Checks if a draft's temporary structure ID is referenced in the current form JSON. + private isDraftReferencedInForm( + draftSub: any, + entry: any, + jsonStr: string, + ): boolean { + const tmpStructureId = + draftSub.$$tmpStructureId || entry.$$tmpStructureId || null; + + if (!tmpStructureId) { + return false; + } + + return jsonStr.indexOf('"' + tmpStructureId + '"') !== -1; + } + + // Validates a draft and collects any validation errors/warnings. + // returns true if there are validation errors that should skip saving, false otherwise + private async validateAndCollectDraftErrors( + draftSub: any, + entry: any, + ): Promise { + const validationResult: any = await new Promise((resolve) => { + this.substanceService + .validateSubstance(draftSub) + .pipe(take(1)) + .subscribe( + (res) => resolve(res), + (err) => resolve({ error: err }), + ); + }); + + if ( + validationResult && + validationResult.validationMessages && + validationResult.validationMessages.length > 0 + ) { + const filteredValidations = this.filterAndPrefixValidationMessages( + validationResult.validationMessages, + entry, + draftSub, + ); + + this.validationMessages = [ + ...this.validationMessages, + ...filteredValidations, + ]; + + return filteredValidations.length > 0; + } + + return false; + } + + private filterAndPrefixValidationMessages( + messages: ValidationMessage[], + entry: any, + draftSub: any, + ): ValidationMessage[] { + return messages + .filter( + (message) => + message.messageType.toUpperCase() === "ERROR" || + message.messageType.toUpperCase() === "WARNING", + ) + .map((msg) => { + msg.message = + 'Draft "' + + (entry.name || draftSub.names?.[0]?.name || entry.key) + + '": ' + + msg.message; + return msg; }); - this.subscriptions.push(this.submitSubscription); + } - }; //window + private async saveDraftSubstance(draftSub: any): Promise { + return new Promise((resolve) => { + this.substanceService + .saveSubstance(draftSub) + .pipe(take(1)) + .subscribe( + (resp) => resolve(resp), + (err) => resolve({ error: err }), + ); + }); + } - let tempCallback = window["schemeUtil"].onFinishedLayout; - window["schemeUtil"].onFinishedLayout = (s)=>{ - window["schemeUtil"].onFinishedLayout =(ss)=>{}; + private prepareFinalJson(): string { + try { + if (this.json) { + this.removeTmpStructureIdFields(this.json); + return JSON.stringify(this.json); + } + } catch (e) { + // Ignore errors and return current JSON + } + return JSON.stringify(this.json); + } - setTimeout(tempCallback(s),3000); - }; - - window['schemeUtil'].renderScheme(window['schemeUtil'].makeDisplayGraph(JSON.parse(ssgjs)), "#scheme-viz-view"); + // Saves the SSG4m synthetic pathway to the backend. + private savePathway(): void { + this.submitSubscription = this.substanceSsg4mService + .saveSsg4m(this.ssg4mSyntheticPathway) + .pipe(take(1)) + .subscribe( + (response) => this.handleSaveSuccess(response), + (error: SubstanceFormResults) => this.handleSaveError(error), + ); + this.subscriptions.push(this.submitSubscription); + } + + private handleSaveSuccess(response: any): void { + this.loadingService.setLoading(false); + this.isLoading = false; + this.validationMessages = null; + this.showSubmissionMessages = false; + this.validationResult = false; + + const isSuccessful = + response && + (response.synthPathwaySkey || + this.configService.configData.isPfdaVersion); + + if (!isSuccessful) { + return; + } + + if (response.synthPathwaySkey) { + this.id = response.synthPathwaySkey.toString(); + } + this.isSavedSuccessful = true; + + // Handle UI messaging based on whether cancel was clicked + if (this.isCancelBtnClicked === true && this.isSavedSuccessful === true) { + this.saveDelayedMessage = + " Network communication restored, click here to refresh with saved version."; + } else { + this.saveDelayedMessage = ""; + this.isCancelBtnClicked = false; + this.openSuccessDialog( + undefined, + this.configService.configData.isPfdaVersion ? response.fileUrl : null, + ); + } + } + + private handleSaveError(error: SubstanceFormResults): void { + this.loadingService.setLoading(false); + this.isLoading = false; + this.saveDelayedMessage = + " Network communication restored, RECORD HAS NOT BEEN SAVED. Please resave the record."; + + this.showSubmissionMessages = true; + this.submissionMessage = null; + + if (error.validationMessages && error.validationMessages.length) { + this.validationResult = error.isSuccessfull; + this.validationMessages = error.validationMessages.filter( + (message) => + message.messageType.toUpperCase() === "ERROR" || + message.messageType.toUpperCase() === "WARNING", + ); + this.showSubmissionMessages = true; + } else { + this.submissionMessage = "There was a problem with your submission"; + this.addServerError(error.serverError); + setTimeout(() => { + this.showSubmissionMessages = false; + this.submissionMessage = null; + }, 8000); + } } cancelSubmit() { @@ -1091,18 +1719,20 @@ export class SubstanceSsg4ManufactureFormComponent implements OnInit, AfterViewI this.isLoading = false; this.isCancelBtnClicked = true; - this.saveDelayedMessage = "Warning: Network traffic is delayed, attempting to reconnect to the server ... please save a local copy of the record."; + this.saveDelayedMessage = + "Warning: Network traffic is delayed, attempting to reconnect to the server ... please save a local copy of the record."; } refreshPage() { // Refresh the current page, this will not cause record locking issue this.router.routeReuseStrategy.shouldReuseRoute = () => false; - this.router.onSameUrlNavigation = 'reload'; - if (this.showHeaderBar && this.showHeaderBar === 'false') { - const route = '/substances-ssg4m/' + this.id + '/edit?header=' + this.showHeaderBar; + this.router.onSameUrlNavigation = "reload"; + if (this.showHeaderBar && this.showHeaderBar === "false") { + const route = + "/substances-ssg4m/" + this.id + "/edit?header=" + this.showHeaderBar; this.router.navigateByUrl(route); } else { - this.router.navigate(['/substances-ssg4m', this.id, 'edit']); + this.router.navigate(["/substances-ssg4m", this.id, "edit"]); } } @@ -1110,7 +1740,7 @@ export class SubstanceSsg4ManufactureFormComponent implements OnInit, AfterViewI this.validationMessages.splice(index, 1); if (this.validationMessages.length === 0) { - this.submissionMessage = 'Substance is Valid. Would you like to submit?'; + this.submissionMessage = "Substance is Valid. Would you like to submit?"; } } @@ -1120,19 +1750,22 @@ export class SubstanceSsg4ManufactureFormComponent implements OnInit, AfterViewI this.validationMessages = null; const message: ValidationMessage = { - actionType: 'server failure', + actionType: "server failure", links: [], appliedChange: false, suggestedChange: false, - messageType: 'ERROR', - message: 'Unknown Server Error' + messageType: "ERROR", + message: "Unknown Server Error", }; if (error && error.error && error.error.message) { - message.message = 'Server Error ' + (error.status + ': ' || ': ') + error.error.message; - } else if (error && error.error && (typeof error.error) === 'string') { - message.message = 'Server Error ' + (error.status + ': ' || '') + error.error; + message.message = + "Server Error " + (error.status + ": " || ": ") + error.error.message; + } else if (error && error.error && typeof error.error === "string") { + message.message = + "Server Error " + (error.status + ": " || "") + error.error; } else if (error && error.message) { - message.message = 'Server Error ' + (error.status + ': ' || '') + error.message; + message.message = + "Server Error " + (error.status + ": " || "") + error.message; } this.validationMessages = [message]; this.showSubmissionMessages = true; @@ -1144,16 +1777,16 @@ export class SubstanceSsg4ManufactureFormComponent implements OnInit, AfterViewI dismissAllValidationMessages(): void { for (let i = this.validationMessages.length - 1; i >= 0; i--) { - if (this.validationMessages[i].messageType !== 'ERROR') { + if (this.validationMessages[i].messageType !== "ERROR") { this.validationMessages.splice(i, 1); } } if (this.validationMessages.length === 0) { - this.submissionMessage = 'Substance is Valid. Would you like to submit?'; + this.submissionMessage = "Substance is Valid. Would you like to submit?"; } } - @HostListener('window:beforeunload', ['$event']) + @HostListener("window:beforeunload", ["$event"]) unloadNotification($event: any) { if (this.substanceSsg4mService.isSubstanceUpdated) { $event.returnValue = true; @@ -1167,8 +1800,20 @@ export class SubstanceSsg4ManufactureFormComponent implements OnInit, AfterViewI .toString(16) .substring(1); } - return s4() + s4() + '-' + s4() + '-' + s4() + '-' + - s4() + '-' + s4() + s4() + s4(); + return ( + s4() + + s4() + + "-" + + s4() + + "-" + + s4() + + "-" + + s4() + + "-" + + s4() + + s4() + + s4() + ); } const old = oldraw; @@ -1208,47 +1853,48 @@ export class SubstanceSsg4ManufactureFormComponent implements OnInit, AfterViewI const refs = refHolders[i].references; for (let j = 0; j < refs.length; j++) { const or = refs[j]; - if (typeof or === 'object') { continue; } + if (typeof or === "object") { + continue; + } refs[j] = _map[or]; } } _.remove(old.codes, { - codeSystem: 'BDNUM' + codeSystem: "BDNUM", }); const createHolders = jp.query(old, '$..[?(@.created)]'); for (let i = 0; i < createHolders.length; i++) { const rec = createHolders[i]; - delete rec['created']; - delete rec['createdBy']; - delete rec['lastEdited']; - delete rec['lastEditedBy']; + delete rec["created"]; + delete rec["createdBy"]; + delete rec["lastEdited"]; + delete rec["lastEditedBy"]; } const originHolders = jp.query(old, '$..[?(@.originatorUuid)]'); for (let i = 0; i < originHolders.length; i++) { const rec = originHolders[i]; - delete rec['originatorUuid']; + delete rec["originatorUuid"]; } delete old.approvalID; delete old.approved; delete old.approvedBy; - old.status = 'pending'; - if ((importType) && (importType === 'definition')) { + old.status = "pending"; + if (importType && importType === "definition") { old.names = []; old.codes = []; old.notes = []; old.relationships = []; old.tags = []; } - delete old['createdBy']; - delete old['created']; - delete old['lastEdited']; - delete old['lastEditedBy']; - delete old['version']; - delete old['$$update']; - delete old['changeReason']; - + delete old["createdBy"]; + delete old["created"]; + delete old["lastEdited"]; + delete old["lastEditedBy"]; + delete old["version"]; + delete old["$$update"]; + delete old["changeReason"]; if (true) { const refSet = {}; @@ -1258,7 +1904,9 @@ export class SubstanceSsg4ManufactureFormComponent implements OnInit, AfterViewI const refs = refHolders2[i].references; for (let j = 0; j < refs.length; j++) { const or = refs[j]; - if (typeof or === 'object') { continue; } + if (typeof or === "object") { + continue; + } refSet[or] = true; } } @@ -1274,71 +1922,93 @@ export class SubstanceSsg4ManufactureFormComponent implements OnInit, AfterViewI .value(); old.references = nrefs; - } return old; } - openSuccessDialog(type?: string): void { + openSuccessDialog(type?: string, fileUrl?: string): void { let data = { - isCoreSubstance: 'false' + isCoreSubstance: "false", + type: null, + fileUrl: null, }; - const dialogRef = this.dialog.open(SubmitSuccessDialogComponent, { - data: data - }); - this.overlayContainer.style.zIndex = '1002'; - const dialogSubscription = dialogRef.afterClosed().pipe(take(1)).subscribe((response?: 'continue') => { - - this.substanceSsg4mService.bypassUpdateCheck(); - if (response === 'continue') { + if (this.configService.configData.isPfdaVersion) { + data = { + isCoreSubstance: "true", + type: "submit", + fileUrl: fileUrl, + }; + } - // Refresh the current page, this will not cause record locking issue - this.router.routeReuseStrategy.shouldReuseRoute = () => false; - this.router.onSameUrlNavigation = 'reload'; - if (this.showHeaderBar && this.showHeaderBar === 'false') { - const route = '/substances-ssg4m/' + this.id + '/edit?header=' + this.showHeaderBar; - this.router.navigateByUrl(route); - } else { - this.router.navigate(['/substances-ssg4m', this.id, 'edit']); - } + const dialogRef = this.dialog.open(SubmitSuccessDialogComponent, { + data: data, + }); + this.overlayContainer.style.zIndex = "1002"; + + const dialogSubscription = dialogRef + .afterClosed() + .pipe(take(1)) + .subscribe((response?: "continue") => { + this.substanceSsg4mService.bypassUpdateCheck(); + if (response === "continue") { + // Refresh the current page, this will not cause record locking issue + this.router.routeReuseStrategy.shouldReuseRoute = () => false; + this.router.onSameUrlNavigation = "reload"; + if (this.showHeaderBar && this.showHeaderBar === "false") { + const route = + "/substances-ssg4m/" + + this.id + + "/edit?header=" + + this.showHeaderBar; + this.router.navigateByUrl(route); + } else { + this.router.navigate(["/substances-ssg4m", this.id, "edit"]); + } - // } else if (response === 'browse') { - // this.router.navigate(['/browse-substance']); - // } else if (response === 'home') { - // this.router.navigate(['/home']); - // } else { - this.showSubmissionMessages = true; - this.validationResult = false; - this.submissionMessage = ''; - /* + // } else if (response === 'browse') { + // this.router.navigate(['/browse-substance']); + // } else if (response === 'home') { + // this.router.navigate(['/home']); + // } else { + this.showSubmissionMessages = true; + this.validationResult = false; + this.submissionMessage = ""; + /* setTimeout(() => { this.showSubmissionMessages = false; this.submissionMessage = ''; this.router.navigate(['/substances-ssg4m', this.id, 'edit']); }, 3000); */ - } - }); + } else if (response === "browse") { + this.router.navigate(["/browse-substance"]); + } else if (response === "viewInPfda") { + // View the submitted substance file in the user's precisionFDA home + window.location.assign(fileUrl); + } + }); this.subscriptions.push(dialogSubscription); - } mergeConcept() { this.feature = undefined; const dialogRef = this.dialog.open(MergeConceptDialogComponent, { - width: '900px', data: { uuid: this.id } + width: "900px", + data: { uuid: this.id }, }); - this.overlayContainer.style.zIndex = '1002'; + this.overlayContainer.style.zIndex = "1002"; } definitionSwitch() { this.feature = undefined; const dialogRef = this.dialog.open(DefinitionSwitchDialogComponent, { - width: '900px', data: { uuid: this.id }, autoFocus: false + width: "900px", + data: { uuid: this.id }, + autoFocus: false, }); - this.overlayContainer.style.zIndex = '1000'; + this.overlayContainer.style.zIndex = "1000"; } fixLink(link: string) { diff --git a/src/app/core/substance-ssg4m/substance-ssg4m-form.service.ts b/src/app/core/substance-ssg4m/substance-ssg4m-form.service.ts index 5cc38ad4b..dfbf0df80 100644 --- a/src/app/core/substance-ssg4m/substance-ssg4m-form.service.ts +++ b/src/app/core/substance-ssg4m/substance-ssg4m-form.service.ts @@ -7,10 +7,11 @@ import { Ssg4mSyntheticPathway, Ssg4mSyntheticPathwayDetail } from './model/subs import { SubstanceDetail } from '../substance/substance.model'; import { SubstanceName } from '../substance/substance.model'; import { SubstanceFormDefinition, SubunitSequence, ValidationResults, ValidationMessage } from '../substance-form/substance-form.model'; -import { Observable, Subject, ReplaySubject, Subscription } from 'rxjs'; +import { Observable, Subject, ReplaySubject, Subscription, concatMap, throwError } from 'rxjs'; import { SubstanceService } from '@gsrs-core/substance/substance.service'; import { UtilsService } from '@gsrs-core/utils/utils.service'; import { StructureService } from '@gsrs-core/structure'; +import { AuthService } from '@gsrs-core/auth'; @Injectable({ providedIn: 'root' @@ -55,6 +56,7 @@ export class SubstanceSsg4mService implements OnDestroy { public utilsService: UtilsService, private structureService: StructureService, public http: HttpClient, + private authService: AuthService, public configService: ConfigService ) { this.substanceEmitter = new ReplaySubject(); @@ -312,7 +314,27 @@ export class SubstanceSsg4mService implements OnDestroy { const options = { body: ssg4m }; - return this.http.request(method, url, options); + + if (!this.configService.configData.isPfdaVersion) { + return this.http.request(method, url, options); + } else { + return this.authService.getAuth().pipe( + concatMap(auth => + auth + ? this.http.request(method, url, options) + : this.authService.pfdaLogin().pipe( + concatMap(success => + success + ? this.http.request(method, url, options) + : throwError(() => ({ + type: 'AUTH', + message: 'Authentication failed', + })) + ) + ) + ) + ); + } } validateSsg4m(ssg4m: Ssg4mSyntheticPathway): Observable { diff --git a/src/app/core/substance/substance-image.directive.ts b/src/app/core/substance/substance-image.directive.ts index 8da808ed4..be521abbf 100644 --- a/src/app/core/substance/substance-image.directive.ts +++ b/src/app/core/substance/substance-image.directive.ts @@ -1,11 +1,11 @@ -import { Directive, ElementRef, Input, AfterViewInit } from '@angular/core'; -import { UtilsService } from '../utils/utils.service'; -import { HttpClient } from '@angular/common/http'; -import { ConfigService } from '@gsrs-core/config/config.service'; +import { Directive, ElementRef, Input, AfterViewInit } from "@angular/core"; +import { UtilsService } from "../utils/utils.service"; +import { HttpClient } from "@angular/common/http"; +import { ConfigService } from "@gsrs-core/config/config.service"; @Directive({ - selector: '[appSubstanceImage]', - standalone: false + selector: "[appSubstanceImage]", + standalone: false, }) export class SubstanceImageDirective implements AfterViewInit { private privateEntityId: string; @@ -21,13 +21,16 @@ export class SubstanceImageDirective implements AfterViewInit { private el: ElementRef, private utilsService: UtilsService, private configService: ConfigService, - private http: HttpClient + private http: HttpClient, ) { this.imageElement = this.el.nativeElement as HTMLImageElement; } ngAfterViewInit() { this.isAfterViewInit = true; + this.imageElement.onerror = () => { + this.setNoImage(); + }; this.setImageSrc(); } @@ -80,12 +83,20 @@ export class SubstanceImageDirective implements AfterViewInit { } private setImageSrc(): void { - const useDataUrlConfig = this.configService.configData && this.configService.configData.useDataUrl || false; + const useDataUrlConfig = + (this.configService.configData && + this.configService.configData.useDataUrl) || + false; if (this.isAfterViewInit) { if (this.privateEntityId) { if (this.privateVersion) { const srcUrl = this.utilsService.getStructureImgUrl( - this.privateEntityId, this.privateSize, this.privateStereo, this.privateAtomMaps, this.privateVersion); + this.privateEntityId, + this.privateSize, + this.privateStereo, + this.privateAtomMaps, + this.privateVersion, + ); if (useDataUrlConfig === true) { this.setImageSrcAsBlob(srcUrl); } else { @@ -93,55 +104,85 @@ export class SubstanceImageDirective implements AfterViewInit { } } else { let srcUrl = this.utilsService.getStructureImgUrl( - this.privateEntityId, this.privateSize, this.privateStereo, this.privateAtomMaps); + this.privateEntityId, + this.privateSize, + this.privateStereo, + this.privateAtomMaps, + ); if (this.privateAltId) { srcUrl = this.utilsService.getStructureImgUrl( - this.privateEntityId, this.privateSize, this.privateStereo, this.privateAtomMaps, null, this.privateAltId); + this.privateEntityId, + this.privateSize, + this.privateStereo, + this.privateAtomMaps, + null, + this.privateAltId, + ); } else { } - + if (useDataUrlConfig === true) { this.setImageSrcAsBlob(srcUrl); } else { this.imageElement.src = srcUrl; } } - } else { - let srcUrl =`${this.configService.environment.baseHref || ''}assets/images/noimage.svg`; - - if(this.privateAltId) { - srcUrl = this.utilsService.getStructureImgUrl( - this.privateEntityId, this.privateSize, this.privateStereo, this.privateAtomMaps, null, this.privateAltId); - } - if(this.privateSize){ - this.imageElement.height = this.privateSize; - this.imageElement.width = this.privateSize; - }else{ - this.imageElement.height = 150; - this.imageElement.width = 150; - } - if (useDataUrlConfig === true) { - this.setImageSrcAsBlob(srcUrl); } else { - this.imageElement.src = srcUrl; + if (this.privateAltId) { + const srcUrl = this.utilsService.getStructureImgUrl( + this.privateEntityId, + this.privateSize, + this.privateStereo, + this.privateAtomMaps, + null, + this.privateAltId, + ); + if (useDataUrlConfig === true) { + this.setImageSrcAsBlob(srcUrl); + } else { + this.imageElement.src = srcUrl; + } + } else { + this.setNoImage(); + } } - + this.imageElement.alt = "structure image"; } - this.imageElement.alt = 'structure image'; + } + + private setNoImage(): void { + const noImageUrl = `${this.configService.environment.baseHref || ""}assets/images/noimage.svg`; + // Temporarily remove onerror to avoid infinite loop if noimage.svg itself fails + this.imageElement.onerror = null; + this.imageElement.src = noImageUrl; + if (this.privateSize) { + this.imageElement.height = this.privateSize; + this.imageElement.width = this.privateSize; + } else { + this.imageElement.height = 150; + this.imageElement.width = 150; } } private setImageSrcAsBlob(srcUrl: any): void { // Getting Image Source as Blob - this.http.get(srcUrl, { responseType: "blob" }).subscribe(imgDat => { - let reader = new FileReader(); - reader.addEventListener("load", () => { - this.imageElement.src = reader.result.toString(); - }, false); - if (imgDat) { - reader.readAsDataURL(imgDat); - } - }); + this.http.get(srcUrl, { responseType: "blob" }).subscribe( + (imgDat) => { + let reader = new FileReader(); + reader.addEventListener( + "load", + () => { + this.imageElement.src = reader.result.toString(); + }, + false, + ); + if (imgDat) { + reader.readAsDataURL(imgDat); + } + }, + () => { + this.setNoImage(); + }, + ); } - } diff --git a/src/app/core/substance/substance.service.ts b/src/app/core/substance/substance.service.ts index bea4960ad..c0340a270 100644 --- a/src/app/core/substance/substance.service.ts +++ b/src/app/core/substance/substance.service.ts @@ -1,6 +1,6 @@ import { Injectable } from '@angular/core'; import { HttpClient, HttpParams, HttpClientJsonpModule, HttpParameterCodec } from '@angular/common/http'; -import { BehaviorSubject, interval, Observable, Observer, Subject, throwError } from 'rxjs'; +import { BehaviorSubject, concatMap, filter, interval, Observable, Observer, Subject, throwError } from 'rxjs'; import { ConfigService } from '../config/config.service'; import { BaseHttpService } from '../base/base-http.service'; import { @@ -26,6 +26,8 @@ import { StructuralUnit } from '@gsrs-core/substance'; import {HierarchyNode} from '@gsrs-core/substances-browse/substance-hierarchy/hierarchy.model'; import { SubstanceDependenciesImageNode } from '@gsrs-core/substance-details/substance-dependencies-image/substance-dependencies-image.model'; +import { stringify } from 'querystring'; +import { AuthService } from "@gsrs-core/auth"; class CustomEncoder implements HttpParameterCodec { encodeKey(key: string): string { return encodeURIComponent(key); @@ -57,6 +59,7 @@ export class SubstanceService extends BaseHttpService { tempObject: any; constructor( public http: HttpClient, + private authService: AuthService, public configService: ConfigService, private sanitizer: DomSanitizer, private utilsService: UtilsService, @@ -879,15 +882,35 @@ export class SubstanceService extends BaseHttpService { } saveSubstance(substance: SubstanceDetail, type?: string): Observable { + // Remove temporary structure ID if present (created when loading draft into G4SSM form) + if (substance && (substance as any).$$tmpStructureId) { + delete (substance as any).$$tmpStructureId; + } + + const method = type === 'import' || !substance.uuid ? 'POST' : 'PUT'; + const options = { body: substance }; + const url = `${this.apiBaseUrl}substances?view=internal`; - let method = substance.uuid ? 'PUT' : 'POST'; - if (type && type === 'import') { - method = 'POST'; + if (!this.configService.configData.isPfdaVersion) { + return this.http.request(method, url, options); + } else { + return this.authService.getAuth().pipe( + concatMap(auth => + auth + ? this.http.request(method, url, options) + : this.authService.pfdaLogin().pipe( + concatMap(success => + success + ? this.http.request(method, url, options) + : throwError(() => ({ + type: 'AUTH', + message: 'Authentication failed', + })) + ) + ) + ) + ); } - const options = { - body: substance - }; - return this.http.request(method, url, options); } saveSubstanceWithoutValidation(substance: SubstanceDetail, type?: string): Observable { diff --git a/src/app/core/substances-browse/substances-browse.component.ts b/src/app/core/substances-browse/substances-browse.component.ts index 54167b604..5b4d3218b 100644 --- a/src/app/core/substances-browse/substances-browse.component.ts +++ b/src/app/core/substances-browse/substances-browse.component.ts @@ -264,7 +264,7 @@ export class SubstancesBrowseComponent implements OnInit, AfterViewInit, OnDestr this.smiles = this.activatedRoute.snapshot.queryParams['smiles'] || ''; // the sort order should be set to default (similarity) for structure searches, last edited for all others this.order = this.activatedRoute.snapshot.queryParams['order'] || - (this.privateStructureSearchTerm && this.privateStructureSearchTerm !== '' ? 'default' : '$root_lastEdited'); + (this.privateStructureSearchTerm && this.privateStructureSearchTerm !== '' ? 'default' : '$root_lastEdited'); this.view = this.activatedRoute.snapshot.queryParams['view'] || 'cards'; this.pageSize = parseInt(this.activatedRoute.snapshot.queryParams['pageSize'], null) || 10; const deprecated = this.activatedRoute.snapshot.queryParams['showDeprecated']; @@ -1498,7 +1498,7 @@ export class SubstancesBrowseComponent implements OnInit, AfterViewInit, OnDestr } else { this.idLists = []; } - } // pagingResponse + } // pagingResponse }, error => { console.log('Error during search substance'); }, () => { @@ -1564,4 +1564,4 @@ export class SubstancesBrowseComponent implements OnInit, AfterViewInit, OnDestr }; } -} \ No newline at end of file +} diff --git a/src/app/fda/fda.module.ts b/src/app/fda/fda.module.ts index 43c7178af..660034a0d 100644 --- a/src/app/fda/fda.module.ts +++ b/src/app/fda/fda.module.ts @@ -27,7 +27,6 @@ import { SubstanceApplicationMatchListComponent} from './substance-browse/substa import { ApplicationsBrowseComponent } from './application/applications-browse/applications-browse.component'; import { ClinicalTrialsBrowseComponent } from './clinical-trials/clinical-trials-browse/clinical-trials-browse.component'; import { fdaSubstanceCardsFilters } from './substance-details/fda-substance-cards-filters.constant'; -import { SsoRefreshService } from './service/sso-refresh.service'; import { ProductService } from './product/service/product.service'; import { GeneralService} from './service/general.service'; import { ShowApplicationToggleComponent } from './substance-browse/show-application-toggle/show-application-toggle.component'; @@ -59,12 +58,6 @@ const fdaRoutes: Routes = [ } ]; -export function init_sso_refresh_service(ssoService: SsoRefreshService) { - return() => { - ssoService.init(); - }; -} - @NgModule({ imports: [ CommonModule, @@ -100,15 +93,6 @@ export function init_sso_refresh_service(ssoService: SsoRefreshService) { ShowApplicationToggleComponent ], exports: [], - providers: [ - SsoRefreshService, - { - provide: APP_INITIALIZER, - useFactory: init_sso_refresh_service, - deps: [SsoRefreshService], - multi: true - } - ] }) export class FdaModule { constructor( diff --git a/src/styles/_material-overrides.scss b/src/styles/_material-overrides.scss index ce80c078d..7e790cd44 100644 --- a/src/styles/_material-overrides.scss +++ b/src/styles/_material-overrides.scss @@ -1083,9 +1083,10 @@ app-loading .mat-mdc-progress-spinner { // ============================================================================ // Tablet/Mobile: Hide most navigation buttons at 1350px, show only Logo, Menu, Search, and Login +// Note: scoped to :not(.pfda-toolbar) to avoid hiding pfda-toolbar buttons @media (max-width: 1350px) { - .mat-toolbar, - .mat-mdc-toolbar { + .mat-toolbar:not(.pfda-toolbar), + .mat-mdc-toolbar:not(.pfda-toolbar) { // Logo container - always visible > .logo-container { display: flex !important;