From f47634fec43c737141536fe997b391878d74751d Mon Sep 17 00:00:00 2001 From: Petr Barta Date: Tue, 17 Sep 2024 13:52:49 +0200 Subject: [PATCH 001/114] Pfda session extension --- .../session-expiration-dialog.component.ts | 12 +- .../session-expiration.component.ts | 177 +++++++++++------- src/app/core/config/config.model.ts | 3 +- src/app/fda/fda.module.ts | 16 -- .../fda/service/sso-refresh.service.spec.ts | 12 -- src/app/fda/service/sso-refresh.service.ts | 87 --------- 6 files changed, 116 insertions(+), 191 deletions(-) delete mode 100644 src/app/fda/service/sso-refresh.service.spec.ts delete mode 100644 src/app/fda/service/sso-refresh.service.ts 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 3dbae841e..da96f8478 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 @@ -1,9 +1,8 @@ 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 { SessionExpirationWarning } from '@gsrs-core/config'; +import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; @Component({ selector: 'app-session-expiration-dialog', @@ -49,11 +48,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." } 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 14fb287a5..bafcaaf28 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,12 @@ -import { Router, Event as NavigationEvent, NavigationStart } from '@angular/router'; +import { Router } 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 { UtilsService } from "@gsrs-core/utils"; @Component({ selector: 'app-session-expiration', @@ -16,8 +16,13 @@ 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 static instance?: SessionExpirationComponent = undefined; + private static sessionExpirationCheckInterval = null; constructor( private router: Router, @@ -25,72 +30,118 @@ export class SessionExpirationComponent implements OnInit { private authService: AuthService, private http: HttpClient, private dialog: MatDialog, - private overlayContainerService: OverlayContainer + 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.sessionRefreshOnActiveUser) { + 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); + } + + if (!this.configService.configData.disableSessionAutoRefresh) { + 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.dialog.closeAll(); + } + } + }, 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() { @@ -104,27 +155,17 @@ export class SessionExpirationComponent implements OnInit { disableClose: true }); this.overlayContainer.style.zIndex = '1501'; - const dialogSubscription = dialogRef.afterClosed().subscribe(response => { + dialogRef.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.dialog.openDialogs.length > 0; + } } diff --git a/src/app/core/config/config.model.ts b/src/app/core/config/config.model.ts index b546aca89..6bb4844cc 100644 --- a/src/app/core/config/config.model.ts +++ b/src/app/core/config/config.model.ts @@ -38,7 +38,8 @@ export interface Config { facetDisplay?: Array; relationshipsVisualizationUri?: string; customToolbarComponent?: string; - disableSessionRefresh?: boolean; + sessionRefreshOnActiveUser?: boolean; + disableSessionAutoRefresh?: boolean; sessionExpirationWarning?: SessionExpirationWarning; disableReferenceDocumentUpload?: boolean; externalSiteWarning?: ExternalSiteWarning; diff --git a/src/app/fda/fda.module.ts b/src/app/fda/fda.module.ts index ad4410cd5..7f83a7751 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'; @@ -57,12 +56,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) { SubstanceCountsComponent, ShowApplicationToggleComponent - ], - providers: [ - SsoRefreshService, - { - provide: APP_INITIALIZER, - useFactory: init_sso_refresh_service, - deps: [SsoRefreshService], - multi: true - } ] }) export class FdaModule { diff --git a/src/app/fda/service/sso-refresh.service.spec.ts b/src/app/fda/service/sso-refresh.service.spec.ts deleted file mode 100644 index ca2f068f5..000000000 --- a/src/app/fda/service/sso-refresh.service.spec.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { TestBed } from '@angular/core/testing'; - -import { SsoRefreshService } from './sso-refresh.service'; - -describe('SsoRefreshService', () => { - beforeEach(() => TestBed.configureTestingModule({})); - - it('should be created', () => { - const service: SsoRefreshService = TestBed.get(SsoRefreshService); - expect(service).toBeTruthy(); - }); -}); diff --git a/src/app/fda/service/sso-refresh.service.ts b/src/app/fda/service/sso-refresh.service.ts deleted file mode 100644 index aa97fb0a4..000000000 --- a/src/app/fda/service/sso-refresh.service.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { Injectable, Inject, PLATFORM_ID, OnDestroy } from '@angular/core'; -import { Router, NavigationExtras, ActivatedRoute } from '@angular/router'; -import { isPlatformBrowser } from '@angular/common'; -import { take } from 'rxjs/operators'; -import { AuthService } from '@gsrs-core/auth'; -import { UtilsService } from '@gsrs-core/utils'; -import { ConfigService } from '@gsrs-core/config/config.service'; - -@Injectable() -export class SsoRefreshService implements OnDestroy { - private iframe: HTMLIFrameElement; - private refreshInterval: any; - private baseHref: string; - private showHeaderBar = 'true'; - - constructor( - @Inject(PLATFORM_ID) private platformId: Object, - private utilsService: UtilsService, - private configService: ConfigService, - private authService: AuthService, - private activatedRoute: ActivatedRoute - ) { - if (isPlatformBrowser(this.platformId)) { - - if (window.location.pathname.indexOf('/ginas/app/ui/') > -1) { - this.baseHref = '/ginas/app/'; - } - } - } - - updateIframe(): any { - if (!this.iframe) { - this.iframe = document.createElement('IFRAME') as HTMLIFrameElement; - this.iframe.title = 'page refresher'; - this.iframe.name = 'refresher'; - this.iframe.style.height = '0'; - this.iframe.style.opacity = '0'; - this.iframe.src = `${this.baseHref || ''}api/v1/whoami?key=${this.utilsService.newUUID()}&noWarningBox=true`; - document.body.appendChild(this.iframe); - } else { - this.iframe.src = `${this.baseHref || ''}api/v1/whoami?key=${this.utilsService.newUUID()}&noWarningBox=true`; - } - } - - setup() { - this.configService.afterLoad().then(cd => { - // Session auto refresh can be explicitly disabled in config file - if (this.configService.configData.disableSessionRefresh) { - return; - } - const homeBaseUrl = this.configService.configData && this.configService.configData.gsrsHomeBaseUrl || null; - if (homeBaseUrl) { - this.baseHref = homeBaseUrl; - this.updateIframe(); - } - clearInterval(this.refreshInterval); - this.refreshInterval = setInterval(() => { - console.log("REFRESHING iFrame"); - this.updateIframe(); - }, 600000); - }); - } - - init(): any { - if(new URLSearchParams(window.location.search).get("noWarningBox") === 'true'){ - //do not do sso refresher recursively - return; - } - if (new URLSearchParams(window.location.search).get("header") === 'false') { - this.setup(); - } else { - this.authService.getAuth().subscribe(auth => { - if (auth != null && this.refreshInterval == null) { - this.setup(); - } else if (auth === null){ - clearInterval(this.refreshInterval); - this.refreshInterval = null; - } - }); - } //else - } - - ngOnDestroy() { - clearInterval(this.refreshInterval); - this.refreshInterval = null; - } -} From 019dfe29e670d37dd61bb384c9b0d2625745a943 Mon Sep 17 00:00:00 2001 From: Petr Barta Date: Wed, 18 Sep 2024 14:29:08 +0200 Subject: [PATCH 002/114] Config sessionRefreshOnActiveUserOnly field --- .../auth/session-expiration/session-expiration.component.ts | 6 ++---- src/app/core/config/config.model.ts | 3 +-- 2 files changed, 3 insertions(+), 6 deletions(-) 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 bafcaaf28..4f8b1fdbd 100644 --- a/src/app/core/auth/session-expiration/session-expiration.component.ts +++ b/src/app/core/auth/session-expiration/session-expiration.component.ts @@ -58,7 +58,7 @@ export class SessionExpirationComponent implements OnInit { 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.sessionRefreshOnActiveUser) { + if (this.configService.configData.sessionRefreshOnActiveUserOnly) { const page = document.getElementsByTagName('body')[0]; page.addEventListener('mousemove', (e) => { if (e instanceof MouseEvent) { @@ -77,9 +77,7 @@ export class SessionExpirationComponent implements OnInit { this.userActive = false; } }, 10000); - } - - if (!this.configService.configData.disableSessionAutoRefresh) { + } else { clearInterval(this.refreshInterval); this.refreshInterval = setInterval(() => { this.refreshSession(); diff --git a/src/app/core/config/config.model.ts b/src/app/core/config/config.model.ts index 6bb4844cc..00c149ab2 100644 --- a/src/app/core/config/config.model.ts +++ b/src/app/core/config/config.model.ts @@ -38,8 +38,7 @@ export interface Config { facetDisplay?: Array; relationshipsVisualizationUri?: string; customToolbarComponent?: string; - sessionRefreshOnActiveUser?: boolean; - disableSessionAutoRefresh?: boolean; + sessionRefreshOnActiveUserOnly?: boolean; sessionExpirationWarning?: SessionExpirationWarning; disableReferenceDocumentUpload?: boolean; externalSiteWarning?: ExternalSiteWarning; From 53d71dec258d325dedf82837c27ca97e1d051a53 Mon Sep 17 00:00:00 2001 From: Petr Barta Date: Thu, 19 Sep 2024 11:49:06 +0200 Subject: [PATCH 003/114] Session expiration dialog fix --- .../session-expiration.component.ts | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) 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 4f8b1fdbd..e2a55062a 100644 --- a/src/app/core/auth/session-expiration/session-expiration.component.ts +++ b/src/app/core/auth/session-expiration/session-expiration.component.ts @@ -1,11 +1,9 @@ -import { Router } 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 { MatDialog } from '@angular/material/dialog'; +import { MatDialog, MatDialogRef, MatDialogState } from '@angular/material/dialog'; import { UtilsService } from "@gsrs-core/utils"; @Component({ @@ -20,16 +18,15 @@ export class SessionExpirationComponent implements OnInit { 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 matDialog: MatDialog, private overlayContainerService: OverlayContainer, private utilsService: UtilsService ) { @@ -115,7 +112,7 @@ export class SessionExpirationComponent implements OnInit { } else if (this.sessionExpiringAt !== null && sessionTtl > 0) { // The session was externally extended (eg. in pfda) -> close the session dialog if (this.isDialogOpened()) { - this.dialog.closeAll(); + this.extendSessionDialog.close(); } } }, 5000) @@ -143,7 +140,7 @@ export class SessionExpirationComponent implements OnInit { } openDialog() { - const dialogRef = this.dialog.open(SessionExpirationDialogComponent, { + this.extendSessionDialog = this.matDialog.open(SessionExpirationDialogComponent, { data: { 'sessionExpirationWarning': this.sessionExpirationWarning, 'sessionExpiringAt': this.sessionExpiringAt @@ -153,7 +150,7 @@ export class SessionExpirationComponent implements OnInit { disableClose: true }); this.overlayContainer.style.zIndex = '1501'; - dialogRef.afterClosed().subscribe(response => { + this.extendSessionDialog.afterClosed().subscribe(response => { this.overlayContainer.style.zIndex = null; this.startSessionTimeoutInterval(); }); @@ -164,6 +161,6 @@ export class SessionExpirationComponent implements OnInit { } isDialogOpened(): boolean { - return this.dialog.openDialogs.length > 0; + return this.extendSessionDialog && this.extendSessionDialog.getState() === MatDialogState.OPEN; } } From 82cb43a4c61c0e5e31675b6fd086269293ed85bb Mon Sep 17 00:00:00 2001 From: Petr Barta Date: Thu, 10 Oct 2024 15:08:28 +0200 Subject: [PATCH 004/114] PFDA-5656 Substance registration pages do not require pFDA login to access --- src/app/core/auth/auth.service.ts | 49 +++++++++++++++-- .../session-expiration.component.ts | 6 ++- src/app/core/base/base-http.service.ts | 4 ++ src/app/core/base/base.component.html | 4 +- src/app/core/base/base.component.ts | 4 +- src/app/core/config/config.model.ts | 3 +- src/app/core/config/config.pfda.json | 4 +- src/app/core/config/config.service.ts | 3 ++ .../can-register-substance-form.ts | 51 ++++++++++-------- src/app/core/substance/substance.service.ts | 54 +++++++++++++++++-- src/environments/environment.model.ts | 1 + 11 files changed, 144 insertions(+), 39 deletions(-) diff --git a/src/app/core/auth/auth.service.ts b/src/app/core/auth/auth.service.ts index 2d354cdb4..317fb295e 100644 --- a/src/app/core/auth/auth.service.ts +++ b/src/app/core/auth/auth.service.ts @@ -303,16 +303,55 @@ export class AuthService { private fetchAuth(): Observable { return new Observable(observer => { this.configService.afterLoad().then(cd => { - const url = `${(this.configService.configData && this.configService.configData.apiBaseUrl) || '/'}api/v1/`; + const isPfdaVersion = this.configService.configData.isPfdaVersion === true; + const url = isPfdaVersion ? '/api/user' : + `${(this.configService.configData && this.configService.configData.apiBaseUrl) || '/'}api/v1/whoami`; if (this.configService.configData && this.configService.configData.dummyWhoami) { observer.next(this.configService.configData.dummyWhoami); } else { - this.http.get(`${url}whoami`) + this.http.get(url) .subscribe( auth => { - // console.log("Authorized as"); - // console.log(auth); - observer.next(auth); + if (isPfdaVersion) { + // @ts-ignore + const dxuser = auth.user.dxuser; + const pfdaAuth: Auth = { + id: 0, + version: 0, + created: 0, + modified: 0, + deprecated: false, + user: { + id: 0, + version: 0, + created: 0, + modified: 0, + deprecated: false, + username: dxuser, + email: auth.user.email, + admin: auth.user.admin + }, + active: true, + systemAuth: false, + key: 'unused', + identifier: dxuser, + groups: [], + roles: [ + "Query", + "Updater", + "SuperUpdate", + "DataEntry", + "SuperDataEntry" + ], + computedToken: 'unused', + tokenTimeToExpireMS: 9999999999999, + roleQueryOnly: false, + permissions: [] + } + observer.next(pfdaAuth); + } else { + observer.next(auth); + } }, err => { console.log("Authorized error"); 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 e2a55062a..1f903f2f1 100644 --- a/src/app/core/auth/session-expiration/session-expiration.component.ts +++ b/src/app/core/auth/session-expiration/session-expiration.component.ts @@ -84,7 +84,11 @@ export class SessionExpirationComponent implements OnInit { } refreshSession(): any { - fetch(`${this.baseHref || ''}api/v1/whoami?key=${this.utilsService.newUUID()}`) + if (this.configService.configData.isPfdaVersion) { + fetch(`${this.configService.configData.pfdaApiBaseUrl}user`) + } else { + fetch(`${this.baseHref || ''}api/v1/whoami?key=${this.utilsService.newUUID()}`) + } } startSessionTimeoutInterval() { diff --git a/src/app/core/base/base-http.service.ts b/src/app/core/base/base-http.service.ts index c8a7db553..fd27e461e 100644 --- a/src/app/core/base/base-http.service.ts +++ b/src/app/core/base/base-http.service.ts @@ -2,12 +2,16 @@ import { ConfigService } from '../config/config.service'; export abstract class BaseHttpService { public apiBaseUrl: string; + public pfdaApiBaseUrl: string = ''; public baseUrl: string; constructor( public configService: ConfigService ) { this.apiBaseUrl = `${(this.configService.configData && this.configService.configData.apiBaseUrl) || '/' }api/v1/`; + if (this.configService.configData.isPfdaVersion && this.configService.configData.pfdaApiBaseUrl) { + this.pfdaApiBaseUrl = this.configService.configData.pfdaApiBaseUrl; + } this.baseUrl = (this.configService.configData && this.configService.configData.apiBaseUrl) || '/'; } } diff --git a/src/app/core/base/base.component.html b/src/app/core/base/base.component.html index 4194f0be6..7355090e3 100644 --- a/src/app/core/base/base.component.html +++ b/src/app/core/base/base.component.html @@ -1,4 +1,4 @@ - + -
+
diff --git a/src/app/core/base/base.component.ts b/src/app/core/base/base.component.ts index 1d12fe9bb..186303abb 100644 --- a/src/app/core/base/base.component.ts +++ b/src/app/core/base/base.component.ts @@ -46,7 +46,7 @@ export class BaseComponent implements OnInit, OnDestroy { appId: string; clasicBaseHref: string; navItems: Array; - customToolbarComponent: string = ''; + isPfdaVersion: boolean = false; canRegister = false; registerNav: Array; searchNav: Array; @@ -75,7 +75,7 @@ export class BaseComponent implements OnInit, OnDestroy { private utilsService: UtilsService, private wildCardService: WildcardService ) { - this.customToolbarComponent = this.configService.configData.customToolbarComponent; + this.isPfdaVersion = this.configService.configData.isPfdaVersion === true; this.wildCardService.wildCardObservable.subscribe((data) => { this.wildCardText = data; }); diff --git a/src/app/core/config/config.model.ts b/src/app/core/config/config.model.ts index 00c149ab2..821401668 100644 --- a/src/app/core/config/config.model.ts +++ b/src/app/core/config/config.model.ts @@ -2,6 +2,7 @@ import { Auth } from "@gsrs-core/auth"; export interface Config { apiBaseUrl?: string; + pfdaApiBaseUrl?: string; gsrsHomeBaseUrl?: string; apiSSG4mBaseUrl?: string; apiUrlDomain?: string; @@ -37,7 +38,7 @@ export interface Config { advancedSearchFacetDisplay?: boolean; facetDisplay?: Array; relationshipsVisualizationUri?: string; - customToolbarComponent?: string; + isPfdaVersion?: boolean; sessionRefreshOnActiveUserOnly?: boolean; sessionExpirationWarning?: SessionExpirationWarning; disableReferenceDocumentUpload?: boolean; diff --git a/src/app/core/config/config.pfda.json b/src/app/core/config/config.pfda.json index dc35f4eb0..54955437b 100644 --- a/src/app/core/config/config.pfda.json +++ b/src/app/core/config/config.pfda.json @@ -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/config/config.service.ts b/src/app/core/config/config.service.ts index e2a78a6af..43586ac7b 100644 --- a/src/app/core/config/config.service.ts +++ b/src/app/core/config/config.service.ts @@ -47,6 +47,9 @@ export class ConfigService { if (config.apiBaseUrl == null && environment.apiBaseUrl != null) { config.apiBaseUrl = environment.apiBaseUrl; } + if (config.pfdaApiBaseUrl == null && environment.pfdaApiBaseUrl != null) { + config.pfdaApiBaseUrl = environment.pfdaApiBaseUrl; + } if (config.apiBaseUrl.indexOf('//') > -1) { const parts = config.apiBaseUrl.split('/'); config.apiUrlDomain = `${parts[0]}//${parts[2]}`; 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 8f6ce3ba4..a4cad22a6 100644 --- a/src/app/core/substance-form/can-register-substance-form.ts +++ b/src/app/core/substance-form/can-register-substance-form.ts @@ -3,13 +3,15 @@ import { Router, CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, Navig import { AuthService } from '../auth/auth.service'; import { Observable } from 'rxjs'; import {Role} from '@gsrs-core/auth/auth.model'; +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 ) {} canActivate( @@ -17,27 +19,32 @@ export class CanRegisterSubstanceForm implements CanActivate { state: RouterStateSnapshot ): Observable | Promise | (boolean | UrlTree) { return new Observable(observer => { - 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 - } - }; - observer.next(this.router.createUrlTree(['/login'], navigationExtras)); - observer.complete(); - } - }); + 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 + } + }; + observer.next(this.router.createUrlTree(['/login'], navigationExtras)); + observer.complete(); + } + }); + } }); } } diff --git a/src/app/core/substance/substance.service.ts b/src/app/core/substance/substance.service.ts index a83bf065b..acd62eada 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 } 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 { @@ -27,6 +27,7 @@ import {HierarchyNode} from '@gsrs-core/substances-browse/substance-hierarchy/hi 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); @@ -58,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, @@ -752,8 +754,16 @@ export class SubstanceService extends BaseHttpService { } + // Helper function to create an Observable that emits when the popup login window closes + waitForPopupToClose(popupWindow) { + return interval(1000).pipe( + takeWhile(() => !popupWindow.closed, true), + filter(() => popupWindow.closed) + ); + } + + saveSubstance(substance: SubstanceDetail, type?: string): Observable { - const url = `${this.apiBaseUrl}substances?view=internal`; let method = substance.uuid ? 'PUT' : 'POST'; if (type && type === 'import') { method = 'POST'; @@ -761,7 +771,43 @@ export class SubstanceService extends BaseHttpService { const options = { body: substance }; - return this.http.request(method, url, options); + + if (!this.configService.configData.isPfdaVersion) { + const url = `${this.apiBaseUrl}substances?view=internal`; + return this.http.request(method, url, options); + } else { + return this.authService.getAuth().pipe( + concatMap(auth => { + if (auth) { + // If authenticated, make the HTTP request + const url = `${this.pfdaApiBaseUrl}substances?view=internal`; + return this.http.request(method, url, options); + } else { + // If not authenticated, open the login window and wait for it to close + 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?user_return_to=%2Fgsrs-auth%2Fclose-login-window', + 'pFda Login', + `height=${height},width=${width},top=${top},left=${left}` + ); + + // Use an observable to wait for the popup window to close + return this.waitForPopupToClose(loginWindow).pipe( + concatMap(() => { + // Retry saving the substance after the window closes + return this.saveSubstance(substance, type); + }) + ); + } + }), + catchError(error => { + return throwError(() => new Error('Failed to save substance.')); + }) + ); + } } validateSubstance(substance: SubstanceDetail, stagingID?: string): Observable { @@ -1015,7 +1061,7 @@ export class SubstanceService extends BaseHttpService { public GetStagedRecord(id:string) { let url = `${(this.configService.configData && this.configService.configData.apiBaseUrl) || '/' }api/v1/substances/stagingArea/${id}`; - + return this.http.get< any >(`${url}`); } diff --git a/src/environments/environment.model.ts b/src/environments/environment.model.ts index 82c5e56d2..9debe8d9a 100644 --- a/src/environments/environment.model.ts +++ b/src/environments/environment.model.ts @@ -1,5 +1,6 @@ export interface Environment { apiBaseUrl: string; + pfdaApiBaseUrl?: string | undefined; configFileLocation?: string; baseHref: string; clasicBaseHref: string; From a78b7bc89721698f58eb1ac002ec63b9eae97cda Mon Sep 17 00:00:00 2001 From: Petr Barta Date: Tue, 22 Oct 2024 15:40:36 +0200 Subject: [PATCH 005/114] Request CSRF token before every POST request --- src/app/core/auth/csrf-token.interceptor.ts | 37 +++++++++++---------- 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/src/app/core/auth/csrf-token.interceptor.ts b/src/app/core/auth/csrf-token.interceptor.ts index 597f28061..d7eb94911 100644 --- a/src/app/core/auth/csrf-token.interceptor.ts +++ b/src/app/core/auth/csrf-token.interceptor.ts @@ -1,35 +1,36 @@ 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)) { + 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(`${this.configService.configData.apiBaseUrl}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); } } From f5d7639b12ef796bec0da3bfaac2430309fff2c9 Mon Sep 17 00:00:00 2001 From: Petr Barta Date: Wed, 23 Oct 2024 14:43:46 +0200 Subject: [PATCH 006/114] CSRF token uri --- src/app/core/auth/csrf-token.interceptor.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/core/auth/csrf-token.interceptor.ts b/src/app/core/auth/csrf-token.interceptor.ts index d7eb94911..35a5f8de0 100644 --- a/src/app/core/auth/csrf-token.interceptor.ts +++ b/src/app/core/auth/csrf-token.interceptor.ts @@ -23,7 +23,7 @@ export class CsrfTokenInterceptor implements HttpInterceptor { } private fetchCsrfToken(): Promise { - return this.http.get(`${this.configService.configData.apiBaseUrl}csrf-token`, { responseType: 'text' }).toPromise(); + return this.http.get(`/csrf-token`, { responseType: 'text' }).toPromise(); } private addCsrfToken(request: HttpRequest, token: string): HttpRequest { From bc656cc386e655d2ef42c201e64c933e150f2a3b Mon Sep 17 00:00:00 2001 From: Petr Barta Date: Wed, 20 Nov 2024 08:16:12 -0600 Subject: [PATCH 007/114] Login, logout fix; improvements --- src/app/core/auth/auth.service.ts | 11 ++- src/app/core/auth/csrf-token.interceptor.ts | 6 +- .../pfda-toolbar/pfda-toolbar.component.html | 8 +-- .../pfda-toolbar/pfda-toolbar.component.scss | 1 + .../pfda-toolbar/pfda-toolbar.component.ts | 9 +++ .../substance-form.component.ts | 5 +- src/app/core/substance/substance.service.ts | 72 +++++++++---------- 7 files changed, 66 insertions(+), 46 deletions(-) diff --git a/src/app/core/auth/auth.service.ts b/src/app/core/auth/auth.service.ts index 317fb295e..62e483afc 100644 --- a/src/app/core/auth/auth.service.ts +++ b/src/app/core/auth/auth.service.ts @@ -161,8 +161,15 @@ export class AuthService { document.cookie = name + '=;expires=Thu, 01 Jan 1970 00:00:00 GMT'; } } - const url = `${this.configService.configData.apiBaseUrl}logout`; - this.http.get(url).subscribe(() => { + 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); }, error => { diff --git a/src/app/core/auth/csrf-token.interceptor.ts b/src/app/core/auth/csrf-token.interceptor.ts index 35a5f8de0..854fce46f 100644 --- a/src/app/core/auth/csrf-token.interceptor.ts +++ b/src/app/core/auth/csrf-token.interceptor.ts @@ -9,8 +9,10 @@ export class CsrfTokenInterceptor implements HttpInterceptor { intercept(request: HttpRequest, next: HttpHandler): Observable> { - // CSRF token for GET and HEAD is not needed - if (['GET', 'HEAD'].includes(request.method) || !(this.configService.configData?.isPfdaVersion)) { + // CSRF token request needed in pFDA version only, for POST and DELETE requests made on /gsrs-auth/* and /logout endpoints + if (['GET', 'HEAD'].includes(request.method) + || (!request.url.toLowerCase().includes('/gsrs-auth/') && !request.url.toLowerCase().includes('/logout')) + || !(this.configService.configData?.isPfdaVersion)) { return next.handle(request); } 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 3e2bd168b..7f867f2b4 100644 --- a/src/app/core/base/pfda-toolbar/pfda-toolbar.component.html +++ b/src/app/core/base/pfda-toolbar/pfda-toolbar.component.html @@ -61,12 +61,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 8a863ed32..d5e1fc63f 100644 --- a/src/app/core/base/pfda-toolbar/pfda-toolbar.component.scss +++ b/src/app/core/base/pfda-toolbar/pfda-toolbar.component.scss @@ -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 f05646868..42b52f1fe 100644 --- a/src/app/core/base/pfda-toolbar/pfda-toolbar.component.ts +++ b/src/app/core/base/pfda-toolbar/pfda-toolbar.component.ts @@ -87,4 +87,13 @@ export class PfdaToolbarComponent implements OnInit { removeZindex(): void { this.overlayContainer.style.zIndex = null; } + + login(): void { + const locationEncoded = encodeURIComponent(`${window.location.pathname}${window.location.search}`); + window.location.assign(`${this.pfdaBaseUrl}login?user_return_to=${locationEncoded}`); + } + + logout(): void { + this.authService.logout(); + } } diff --git a/src/app/core/substance-form/substance-form.component.ts b/src/app/core/substance-form/substance-form.component.ts index c7c118989..0a15a2b80 100644 --- a/src/app/core/substance-form/substance-form.component.ts +++ b/src/app/core/substance-form/substance-form.component.ts @@ -1056,6 +1056,7 @@ export class SubstanceFormComponent implements OnInit, AfterViewInit, OnDestroy } this.openSuccessDialog({ type: 'submit', fileUrl: response.fileUrl }); }, (error: SubstanceFormResults) => { + console.log('error: ', error); this.showSubmissionMessages = true; this.loadingService.setLoading(false); this.isLoading = false; @@ -1099,7 +1100,9 @@ export class SubstanceFormComponent implements OnInit, AfterViewInit, OnDestroy messageType: 'ERROR', message: 'Unknown Server Error' }; - if (error && error.error && error.error.message) { + if (error && error.type === 'AUTH') { + message.message = `Authentication Error: ${error.message}`; + } else 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; diff --git a/src/app/core/substance/substance.service.ts b/src/app/core/substance/substance.service.ts index acd62eada..a5b0f1a9d 100644 --- a/src/app/core/substance/substance.service.ts +++ b/src/app/core/substance/substance.service.ts @@ -764,52 +764,50 @@ export class SubstanceService extends BaseHttpService { saveSubstance(substance: SubstanceDetail, type?: string): Observable { - let method = substance.uuid ? 'PUT' : 'POST'; - if (type && type === 'import') { - method = 'POST'; - } - const options = { - body: substance - }; + const method = type === 'import' || !substance.uuid ? 'POST' : 'PUT'; + const options = { body: substance }; + + const url = this.configService.configData.isPfdaVersion + ? `${this.pfdaApiBaseUrl}substances?view=internal` + : `${this.apiBaseUrl}substances?view=internal`; if (!this.configService.configData.isPfdaVersion) { - const url = `${this.apiBaseUrl}substances?view=internal`; return this.http.request(method, url, options); } else { return this.authService.getAuth().pipe( - concatMap(auth => { - if (auth) { - // If authenticated, make the HTTP request - const url = `${this.pfdaApiBaseUrl}substances?view=internal`; - return this.http.request(method, url, options); - } else { - // If not authenticated, open the login window and wait for it to close - 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?user_return_to=%2Fgsrs-auth%2Fclose-login-window', - 'pFda Login', - `height=${height},width=${width},top=${top},left=${left}` - ); - - // Use an observable to wait for the popup window to close - return this.waitForPopupToClose(loginWindow).pipe( - concatMap(() => { - // Retry saving the substance after the window closes - return this.saveSubstance(substance, type); - }) - ); - } - }), - catchError(error => { - return throwError(() => new Error('Failed to save substance.')); - }) + concatMap(auth => auth + ? this.http.request(method, url, options) + : this.handlePfdaLoginAndRetry(method, url, options) + ) ); } } + private handlePfdaLoginAndRetry(method: string, url: string, options: any): 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?user_return_to=%2Fgsrs-auth%2Fclose-login-window', + 'pFDA Login', + `height=${height},width=${width},top=${top},left=${left}` + ); + + return this.waitForPopupToClose(loginWindow).pipe( + concatMap(() => + this.authService.getAuth().pipe( + concatMap(authAfterLogin => + authAfterLogin + ? this.http.request(method, url, options) + : throwError(() => ({ type: 'AUTH', message: 'Authentication failed' })) + ) + ) + ) + ); + } + + validateSubstance(substance: SubstanceDetail, stagingID?: string): Observable { let url = `${this.configService.configData.apiBaseUrl}api/v1/substances/@validate`; if (stagingID) { From 65e4f6b367bddad0f6636b4d8f12a9f9efcda2c6 Mon Sep 17 00:00:00 2001 From: Petr Barta Date: Tue, 26 Nov 2024 14:40:21 +0100 Subject: [PATCH 008/114] PFDA auth update --- src/app/core/auth/auth.service.ts | 40 ++++++++++++++- .../session-expiration-dialog.component.html | 1 + .../session-expiration-dialog.component.ts | 12 ++++- .../pfda-toolbar/pfda-toolbar.component.ts | 10 ++-- src/app/core/substance/substance.service.ts | 50 +++++-------------- 5 files changed, 69 insertions(+), 44 deletions(-) diff --git a/src/app/core/auth/auth.service.ts b/src/app/core/auth/auth.service.ts index 62e483afc..3ad9860c8 100644 --- a/src/app/core/auth/auth.service.ts +++ b/src/app/core/auth/auth.service.ts @@ -1,8 +1,8 @@ import { Injectable, PLATFORM_ID, Inject } from '@angular/core'; import { ConfigService } from '../config/config.service'; import { Auth, Role, UserGroup } from './auth.model'; -import { Observable, Subject, of } from 'rxjs'; -import { map, take, catchError } from 'rxjs/operators'; +import { interval, Observable, Subject, of } from 'rxjs'; +import { catchError, concatMap, filter, map, take, takeWhile } 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'; @@ -96,6 +96,36 @@ export class AuthService { ); } + // 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?user_return_to=%2Fgsrs-auth%2Fclose-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 + ) + ) + ); + } + getAuth(): Observable { return new Observable(observer => { @@ -143,6 +173,10 @@ export class AuthService { }); } + private deleteCookie(name: string) { + document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/`; + } + logout(): void { // if ( // !this.configService.configData @@ -172,9 +206,11 @@ export class AuthService { 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'); }); } 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 da96f8478..671946484 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 @@ -3,6 +3,7 @@ import { Router } from '@angular/router'; import { HttpClient } from '@angular/common/http'; import { SessionExpirationWarning } from '@gsrs-core/config'; import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; +import { AuthService } from '@gsrs-core/auth'; @Component({ selector: 'app-session-expiration-dialog', @@ -22,7 +23,8 @@ 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 ) { this.sessionExpirationWarning = data.sessionExpirationWarning; this.sessionExpiringAt = data.sessionExpiringAt; @@ -75,4 +77,12 @@ export class SessionExpirationDialogComponent implements OnInit { login() { window.location.assign('/login'); } + + proceedAsGuest() { + clearInterval(this.updateDialogInterval); + if (this.timeRemainingSeconds > 0) { + this.authService.logout(); + } + this.closeDialog(); + } } 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 42b52f1fe..7e922b555 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({ @@ -40,7 +40,7 @@ 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`; @@ -89,8 +89,10 @@ export class PfdaToolbarComponent implements OnInit { } login(): void { - const locationEncoded = encodeURIComponent(`${window.location.pathname}${window.location.search}`); - window.location.assign(`${this.pfdaBaseUrl}login?user_return_to=${locationEncoded}`); + this.authService.pfdaLogin().pipe( + concatMap(success => { + return this.authService.getAuth(); + })).subscribe(); } logout(): void { diff --git a/src/app/core/substance/substance.service.ts b/src/app/core/substance/substance.service.ts index a5b0f1a9d..03c30e61a 100644 --- a/src/app/core/substance/substance.service.ts +++ b/src/app/core/substance/substance.service.ts @@ -754,15 +754,6 @@ export class SubstanceService extends BaseHttpService { } - // Helper function to create an Observable that emits when the popup login window closes - waitForPopupToClose(popupWindow) { - return interval(1000).pipe( - takeWhile(() => !popupWindow.closed, true), - filter(() => popupWindow.closed) - ); - } - - saveSubstance(substance: SubstanceDetail, type?: string): Observable { const method = type === 'import' || !substance.uuid ? 'POST' : 'PUT'; const options = { body: substance }; @@ -775,39 +766,24 @@ export class SubstanceService extends BaseHttpService { return this.http.request(method, url, options); } else { return this.authService.getAuth().pipe( - concatMap(auth => auth - ? this.http.request(method, url, options) - : this.handlePfdaLoginAndRetry(method, url, options) + 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', + })) + ) + ) ) ); } } - private handlePfdaLoginAndRetry(method: string, url: string, options: any): 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?user_return_to=%2Fgsrs-auth%2Fclose-login-window', - 'pFDA Login', - `height=${height},width=${width},top=${top},left=${left}` - ); - - return this.waitForPopupToClose(loginWindow).pipe( - concatMap(() => - this.authService.getAuth().pipe( - concatMap(authAfterLogin => - authAfterLogin - ? this.http.request(method, url, options) - : throwError(() => ({ type: 'AUTH', message: 'Authentication failed' })) - ) - ) - ) - ); - } - - validateSubstance(substance: SubstanceDetail, stagingID?: string): Observable { let url = `${this.configService.configData.apiBaseUrl}api/v1/substances/@validate`; if (stagingID) { From e233a8d2063eb2bc6139b1376ffeb0afce0c7baf Mon Sep 17 00:00:00 2001 From: Petr Barta Date: Thu, 28 Nov 2024 15:11:28 +0100 Subject: [PATCH 009/114] Public GSRS --- src/app/core/auth/auth.service.ts | 51 +++---------------- src/app/core/auth/csrf-token.interceptor.ts | 9 ++-- .../session-expiration-dialog.component.ts | 19 +++++-- .../session-expiration.component.ts | 6 +-- src/app/core/base/base-http.service.ts | 4 -- src/app/core/config/config.model.ts | 1 - src/app/core/config/config.service.ts | 3 -- .../substance-form.component.html | 3 +- .../substance-form.component.ts | 16 +++++- src/app/core/substance/substance.service.ts | 5 +- src/environments/environment.model.ts | 1 - 11 files changed, 44 insertions(+), 74 deletions(-) diff --git a/src/app/core/auth/auth.service.ts b/src/app/core/auth/auth.service.ts index 3ad9860c8..59b86ebb1 100644 --- a/src/app/core/auth/auth.service.ts +++ b/src/app/core/auth/auth.service.ts @@ -111,7 +111,7 @@ export class AuthService { const left = (screen.width / 2) - (width / 2); const top = (screen.height / 2) - (height / 2); const loginWindow = window.open( - '/login?user_return_to=%2Fgsrs-auth%2Fclose-login-window', + '/login?user_return_to=%2Fginas%2Fclose-pfda-login-window', 'pFDA Login', `height=${height},width=${width},top=${top},left=${left}` ); @@ -346,55 +346,16 @@ export class AuthService { private fetchAuth(): Observable { return new Observable(observer => { this.configService.afterLoad().then(cd => { - const isPfdaVersion = this.configService.configData.isPfdaVersion === true; - const url = isPfdaVersion ? '/api/user' : - `${(this.configService.configData && this.configService.configData.apiBaseUrl) || '/'}api/v1/whoami`; + 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) + this.http.get(`${url}whoami`) .subscribe( auth => { - if (isPfdaVersion) { - // @ts-ignore - const dxuser = auth.user.dxuser; - const pfdaAuth: Auth = { - id: 0, - version: 0, - created: 0, - modified: 0, - deprecated: false, - user: { - id: 0, - version: 0, - created: 0, - modified: 0, - deprecated: false, - username: dxuser, - email: auth.user.email, - admin: auth.user.admin - }, - active: true, - systemAuth: false, - key: 'unused', - identifier: dxuser, - groups: [], - roles: [ - "Query", - "Updater", - "SuperUpdate", - "DataEntry", - "SuperDataEntry" - ], - computedToken: 'unused', - tokenTimeToExpireMS: 9999999999999, - roleQueryOnly: false, - permissions: [] - } - observer.next(pfdaAuth); - } else { - observer.next(auth); - } + // console.log("Authorized as"); + // console.log(auth); + observer.next(auth); }, err => { console.log("Authorized error"); diff --git a/src/app/core/auth/csrf-token.interceptor.ts b/src/app/core/auth/csrf-token.interceptor.ts index 854fce46f..412d3b5c1 100644 --- a/src/app/core/auth/csrf-token.interceptor.ts +++ b/src/app/core/auth/csrf-token.interceptor.ts @@ -1,7 +1,7 @@ import { Injectable } from '@angular/core'; -import {HttpInterceptor, HttpRequest, HttpHandler, HttpEvent, HttpClient} from '@angular/common/http'; -import {from, Observable, switchMap} from 'rxjs'; -import {ConfigService} from "@gsrs-core/config"; +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 { @@ -9,9 +9,8 @@ export class CsrfTokenInterceptor implements HttpInterceptor { intercept(request: HttpRequest, next: HttpHandler): Observable> { - // CSRF token request needed in pFDA version only, for POST and DELETE requests made on /gsrs-auth/* and /logout endpoints + // CSRF token request needed in pFDA version only if (['GET', 'HEAD'].includes(request.method) - || (!request.url.toLowerCase().includes('/gsrs-auth/') && !request.url.toLowerCase().includes('/logout')) || !(this.configService.configData?.isPfdaVersion)) { return next.handle(request); } 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 671946484..051f1725b 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 @@ -1,9 +1,10 @@ import { Component, OnInit, Inject } from '@angular/core'; import { Router } from '@angular/router'; import { HttpClient } from '@angular/common/http'; -import { SessionExpirationWarning } from '@gsrs-core/config'; +import {ConfigService, SessionExpirationWarning} from '@gsrs-core/config'; 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,8 @@ export class SessionExpirationDialogComponent implements OnInit { // N.B. injected services has to come after data private router: Router, private http: HttpClient, - private authService: AuthService + private authService: AuthService, + public configService: ConfigService ) { this.sessionExpirationWarning = data.sessionExpirationWarning; this.sessionExpiringAt = data.sessionExpiringAt; @@ -75,7 +77,18 @@ export class SessionExpirationDialogComponent implements OnInit { } login() { - window.location.assign('/login'); + if (this.configService.configData.isPfdaVersion) { + this.authService.pfdaLogin().pipe( + concatMap(success => { + console.log('success: ', success); + if (success) { + this.closeDialog(); + return this.authService.getAuth(); + } + })).subscribe(); + } else { + window.location.assign('/login'); + } } proceedAsGuest() { 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 1f903f2f1..16f4b7967 100644 --- a/src/app/core/auth/session-expiration/session-expiration.component.ts +++ b/src/app/core/auth/session-expiration/session-expiration.component.ts @@ -84,11 +84,7 @@ export class SessionExpirationComponent implements OnInit { } refreshSession(): any { - if (this.configService.configData.isPfdaVersion) { - fetch(`${this.configService.configData.pfdaApiBaseUrl}user`) - } else { - fetch(`${this.baseHref || ''}api/v1/whoami?key=${this.utilsService.newUUID()}`) - } + fetch(`${this.baseHref || ''}api/v1/whoami?key=${this.utilsService.newUUID()}`); } startSessionTimeoutInterval() { diff --git a/src/app/core/base/base-http.service.ts b/src/app/core/base/base-http.service.ts index fd27e461e..c8a7db553 100644 --- a/src/app/core/base/base-http.service.ts +++ b/src/app/core/base/base-http.service.ts @@ -2,16 +2,12 @@ import { ConfigService } from '../config/config.service'; export abstract class BaseHttpService { public apiBaseUrl: string; - public pfdaApiBaseUrl: string = ''; public baseUrl: string; constructor( public configService: ConfigService ) { this.apiBaseUrl = `${(this.configService.configData && this.configService.configData.apiBaseUrl) || '/' }api/v1/`; - if (this.configService.configData.isPfdaVersion && this.configService.configData.pfdaApiBaseUrl) { - this.pfdaApiBaseUrl = this.configService.configData.pfdaApiBaseUrl; - } this.baseUrl = (this.configService.configData && this.configService.configData.apiBaseUrl) || '/'; } } diff --git a/src/app/core/config/config.model.ts b/src/app/core/config/config.model.ts index 821401668..f04db4df7 100644 --- a/src/app/core/config/config.model.ts +++ b/src/app/core/config/config.model.ts @@ -2,7 +2,6 @@ import { Auth } from "@gsrs-core/auth"; export interface Config { apiBaseUrl?: string; - pfdaApiBaseUrl?: string; gsrsHomeBaseUrl?: string; apiSSG4mBaseUrl?: string; apiUrlDomain?: string; diff --git a/src/app/core/config/config.service.ts b/src/app/core/config/config.service.ts index 43586ac7b..e2a78a6af 100644 --- a/src/app/core/config/config.service.ts +++ b/src/app/core/config/config.service.ts @@ -47,9 +47,6 @@ export class ConfigService { if (config.apiBaseUrl == null && environment.apiBaseUrl != null) { config.apiBaseUrl = environment.apiBaseUrl; } - if (config.pfdaApiBaseUrl == null && environment.pfdaApiBaseUrl != null) { - config.pfdaApiBaseUrl = environment.pfdaApiBaseUrl; - } if (config.apiBaseUrl.indexOf('//') > -1) { const parts = config.apiBaseUrl.split('/'); config.apiUrlDomain = `${parts[0]}//${parts[2]}`; diff --git a/src/app/core/substance-form/substance-form.component.html b/src/app/core/substance-form/substance-form.component.html index bec1f84eb..2d7f3b82e 100644 --- a/src/app/core/substance-form/substance-form.component.html +++ b/src/app/core/substance-form/substance-form.component.html @@ -212,8 +212,7 @@

{{ section.menuLabel }}

+ + + + + + + + + + + + + + + + + + + +
@@ -97,6 +117,15 @@
+
+
+
+ {{pauseStructureSearch}} +
+
+
+ {{asyncFinished}} +
diff --git a/src/app/core/substances-browse/substances-browse.component.ts b/src/app/core/substances-browse/substances-browse.component.ts index bed97c53c..806094534 100644 --- a/src/app/core/substances-browse/substances-browse.component.ts +++ b/src/app/core/substances-browse/substances-browse.component.ts @@ -219,7 +219,7 @@ export class SubstancesBrowseComponent implements OnInit, AfterViewInit, OnDestr }); }); - + this.title.setTitle('Browse Substances'); this.pageSize = 10; @@ -248,7 +248,7 @@ export class SubstancesBrowseComponent implements OnInit, AfterViewInit, OnDestr this.privateSearchSeqType = this.activatedRoute.snapshot.queryParams['seq_type'] || ''; 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.order = this.activatedRoute.snapshot.queryParams['order'] || (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; @@ -516,7 +516,7 @@ export class SubstancesBrowseComponent implements OnInit, AfterViewInit, OnDestr id:'structure-dialog' }); this.overlayContainer.style.zIndex = '1002'; - + this.structureSearchDialog.afterClosed().subscribe(result => { this.overlayContainer.style.zIndex = null; this.loadingService.setLoading(false); @@ -525,7 +525,7 @@ export class SubstancesBrowseComponent implements OnInit, AfterViewInit, OnDestr }); this.structureDialogOpened = true; } - + } searchSubstances() { @@ -580,7 +580,7 @@ export class SubstancesBrowseComponent implements OnInit, AfterViewInit, OnDestr // this.pauseStructureSearch = true; iterations++; } - + this.privateBulkSearchStatusKey = pagingResponse.statusKey; this.isError = false; @@ -620,7 +620,7 @@ export class SubstancesBrowseComponent implements OnInit, AfterViewInit, OnDestr this.etag = pagingResponse.etag; if (pagingResponse.facets && pagingResponse.facets.length > 0) { this.rawFacets = pagingResponse.facets; - + } this.narrowSearchSuggestions = {}; this.matchTypes = []; @@ -760,7 +760,7 @@ searchTermOkforBeginsWithSearch(): boolean { maxHeight: '85%', width: '60%', - + data: { 'extension': extension } }); @@ -1287,21 +1287,21 @@ searchTermOkforBeginsWithSearch(): boolean { addToList(): void { let data = {view: 'add', etag: this.etag, lists: this.userLists}; - + const dialogRef = this.dialog.open(UserQueryListDialogComponent, { width: '800px', autoFocus: false, data: data - + }); this.overlayContainer.style.zIndex = '1002'; - + const dialogSubscription = dialogRef.afterClosed().pipe(take(1)).subscribe(response => { if (response) { this.overlayContainer.style.zIndex = null; } }); } - + } diff --git a/src/app/fda/config/config.json b/src/app/fda/config/config.json index a16732a6c..23c417119 100644 --- a/src/app/fda/config/config.json +++ b/src/app/fda/config/config.json @@ -10,6 +10,7 @@ "bannerMessage": null, "showNameStandardizeButton": true, "advancedSearchFacetDisplay": false, + "apiBaseUrl": "https://gsrs.ncats.nih.gov/ginas/app/", "approvalCodeName": "UNII", "primaryCode": "BDNUM", "useDataUrl": false, From 2e66cf3e26126945ef615902709fb57b7648be09 Mon Sep 17 00:00:00 2001 From: NikoAnderson Date: Mon, 23 Dec 2024 10:26:50 -0500 Subject: [PATCH 019/114] final version for testing --- .../substances-browse.component.html | 31 ------------------- .../substances-browse.component.scss | 5 ++- 2 files changed, 2 insertions(+), 34 deletions(-) diff --git a/src/app/core/substances-browse/substances-browse.component.html b/src/app/core/substances-browse/substances-browse.component.html index 25e36e5f8..aa6ead17c 100644 --- a/src/app/core/substances-browse/substances-browse.component.html +++ b/src/app/core/substances-browse/substances-browse.component.html @@ -75,26 +75,6 @@ Or start a new chemical registration using this Smiles
- - - - - - - - - - - - - - - - - - - -
@@ -117,15 +97,6 @@
-
-
-
- {{pauseStructureSearch}} -
-
-
- {{asyncFinished}} -
@@ -730,5 +701,3 @@

{{privateSearchType | titlecase }} Search is Processing...


- - diff --git a/src/app/core/substances-browse/substances-browse.component.scss b/src/app/core/substances-browse/substances-browse.component.scss index a342a6572..6011c967b 100644 --- a/src/app/core/substances-browse/substances-browse.component.scss +++ b/src/app/core/substances-browse/substances-browse.component.scss @@ -346,7 +346,7 @@ display: flex; font-family: Menlo,Monaco,Consolas,"Courier New",monospace; color: var(--pink-span-color); } - + .similarity-label { font-style: italic; } @@ -626,7 +626,7 @@ display: flex; ::ng-deep .mat-select-value { max-width: 100%; width: auto; - } + } } .page-label { @@ -774,4 +774,3 @@ margin-left: 20px; line-height: 28px; margin-left: 20px; } - From bb02e750337ea9a7df28ac2a37099fd142902d67 Mon Sep 17 00:00:00 2001 From: NikoAnderson Date: Mon, 23 Dec 2024 10:28:11 -0500 Subject: [PATCH 020/114] adding service --- src/app/core/substance/substance.service.ts | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/app/core/substance/substance.service.ts b/src/app/core/substance/substance.service.ts index 8c4bd867b..1cffdfaee 100644 --- a/src/app/core/substance/substance.service.ts +++ b/src/app/core/substance/substance.service.ts @@ -267,7 +267,6 @@ export class SubstanceService extends BaseHttpService { sync = true; } if (!sync && this.searchKeys[structureFacetsKey]) { - console.log('not sync'); url += `status(${this.searchKeys[structureFacetsKey]})/results`; params = params.appendFacetParams(facets, this.showDeprecated); if(querySearchTerm.length > 0) { @@ -285,12 +284,8 @@ export class SubstanceService extends BaseHttpService { if (order != null && order !== '') { params = params.append('order', order); } - console.log(url); - console.log(params); } else { - console.log(sync); - console.log(type); params = params.append('q', (searchTerm)); if (type) { params = params.append('type', type); @@ -315,7 +310,6 @@ export class SubstanceService extends BaseHttpService { } } url += 'substances/structureSearch'; - console.log(url); } const options = { @@ -324,10 +318,8 @@ export class SubstanceService extends BaseHttpService { this.http.get(url, options).subscribe( response => { - console.log(response); // call async if (response.results) { - console.log('call async'); const resultKey = response.key; this.searchKeys[structureFacetsKey] = resultKey; this.processAsyncSearchResults( @@ -342,7 +334,6 @@ export class SubstanceService extends BaseHttpService { skip ); } else { - console.log('complete'); observer.next(response); observer.complete(); } @@ -488,8 +479,6 @@ export class SubstanceService extends BaseHttpService { response => { // call async if (response.results) { - console.log('has results'); - console.log(response); const resultKey = response.key; this.searchKeys[bulkFacetsKey] = resultKey; this.processAsyncSearchResults( @@ -505,7 +494,6 @@ export class SubstanceService extends BaseHttpService { ); } else { // consider making API backend provide statusKey in JSON - console.log('not results)'); if(this.searchKeys && this.searchKeys[bulkFacetsKey]) { response.statusKey = this.searchKeys[bulkFacetsKey]; } From be0b95e736277fb07773fc95368d7e474d431b12 Mon Sep 17 00:00:00 2001 From: NikoAnderson Date: Mon, 23 Dec 2024 10:31:05 -0500 Subject: [PATCH 021/114] removing local changes --- src/app/fda/config/config.json | 1 - 1 file changed, 1 deletion(-) diff --git a/src/app/fda/config/config.json b/src/app/fda/config/config.json index 23c417119..a16732a6c 100644 --- a/src/app/fda/config/config.json +++ b/src/app/fda/config/config.json @@ -10,7 +10,6 @@ "bannerMessage": null, "showNameStandardizeButton": true, "advancedSearchFacetDisplay": false, - "apiBaseUrl": "https://gsrs.ncats.nih.gov/ginas/app/", "approvalCodeName": "UNII", "primaryCode": "BDNUM", "useDataUrl": false, From 1588625e3a38f896cf41c2968e34f1c44fee678a Mon Sep 17 00:00:00 2001 From: NikoAnderson Date: Tue, 21 Jan 2025 15:16:12 -0500 Subject: [PATCH 022/114] adding formulation autofill option for ssg1 --- .../constituents/substance-form-constituents-card.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 a0a25052a..4b6b092e8 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 @@ -78,7 +78,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++; From fee2e109f7d589f92f8e20f964edda32429083e2 Mon Sep 17 00:00:00 2001 From: NikoAnderson Date: Thu, 23 Jan 2025 11:09:12 -0500 Subject: [PATCH 023/114] adding formulation percentage for G1ss, det.pagin --- .../substance-codes/substance-codes.component.html | 8 ++++---- .../substance-names/substance-names.component.html | 10 +++++----- .../substance-references.component.html | 2 +- .../substance-relationships.component.html | 4 ++-- 4 files changed, 12 insertions(+), 12 deletions(-) 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 fae32366b..282837881 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 @@ -93,7 +93,7 @@ + [disabled] = "!code.comments && !code.codeText" >{{!code.comments && !code.codeText ? 'None' : 'View'}}

Code Comments

@@ -110,17 +110,17 @@

Code Comments

- +
- + References - diff --git a/src/app/core/substance-details/substance-names/substance-names.component.html b/src/app/core/substance-details/substance-names/substance-names.component.html index 2dc7a32c0..b8b5876b1 100644 --- a/src/app/core/substance-details/substance-names/substance-names.component.html +++ b/src/app/core/substance-details/substance-names/substance-names.component.html @@ -14,7 +14,7 @@ '>Both
- + {{showHideFilterText}} @@ -162,22 +162,22 @@ Details -

Details

- + -
- Naming organizations: + Naming organizations:
- {{org.nameOrg}}{{!last? ', ':''}} + {{org.nameOrg}}{{!last? ', ':''}}
diff --git a/src/app/core/substance-details/substance-references/substance-references.component.html b/src/app/core/substance-details/substance-references/substance-references.component.html index e33d554d8..9ec599324 100644 --- a/src/app/core/substance-details/substance-references/substance-references.component.html +++ b/src/app/core/substance-details/substance-references/substance-references.component.html @@ -97,7 +97,7 @@
Access + diff --git a/src/app/core/substance-details/substance-relationships/substance-relationships.component.html b/src/app/core/substance-details/substance-relationships/substance-relationships.component.html index d213b4b8b..3798f0ea2 100644 --- a/src/app/core/substance-details/substance-relationships/substance-relationships.component.html +++ b/src/app/core/substance-details/substance-relationships/substance-relationships.component.html @@ -33,12 +33,12 @@ Details -
{{filename? filename: 'no file chosen'}}
- - -
-
Or paste JSON here:
- -
-
- {{message}} -
+ +
+
+
{{filename? filename: 'no file chosen'}}
+ +
+ + +
+
Paste JSON here:
+ +
+
+ +
+
URL:
+ +
Note: The URL needs to be publicly accessible
+
+
+ +
+ {{message}} +

- -
\ No newline at end of file + + 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 dd28c539b..774d23b60 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', @@ -14,15 +16,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) { @@ -59,36 +67,70 @@ export class SubstanceEditImportDialogComponent implements OnInit { } }; reader.readAsText(event.target.files[0]); - this.uploaded = true; } } - useFile() { - if (!this.uploaded && this.pastedJSON) { - const read = JSON.parse(this.pastedJSON); - if (!read['substanceClass']) { - this.message = 'Error: Invalid JSON format'; - this.loaded = false; + importSubstance() { + if (this.currentTab === 0) { + // Nothing + this.dialogRef.close(this.record); + } else if (this.currentTab === 1) { + const read = JSON.parse(this.pastedJSON); + if (!read['substanceClass']) { + 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 { - this.loaded = true; - this.record = this.pastedJSON; - this.message = ''; + 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); } - checkLoaded() { this.loaded = true; try { JSON.parse(this.pastedJSON); this.message = ''; - } catch (e) { - this.message = 'Error: Invalid JSON format in pasted string'; - this.loaded = false; + } catch (e) { + this.message = 'Error: Invalid JSON format in pasted string'; + this.loaded = false; + } + } + + checkUrl() { + try { + new URL(this.pastedUrl); + this.loaded = true; + this.message = ''; + } catch (_e) { + this.message = 'Invalid URL'; + this.loaded = false; + } } -} openInput(): void { @@ -104,4 +146,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 = ''; + } + } } From 8ed0fb830ad3afc468712b96cffda80d66150ab0 Mon Sep 17 00:00:00 2001 From: Petr Barta Date: Mon, 24 Feb 2025 12:16:18 +0100 Subject: [PATCH 033/114] Success dialog after saving G4SSM in pfda --- .../model/substance-ssg4m.model.ts | 1 + .../substance-ssg4m-form.component.ts | 26 +++++++++++++++---- 2 files changed, 22 insertions(+), 5 deletions(-) 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..9a11ebdfd 100644 --- a/src/app/core/substance-ssg4m/model/substance-ssg4m.model.ts +++ b/src/app/core/substance-ssg4m/model/substance-ssg4m.model.ts @@ -17,6 +17,7 @@ export interface Ssg4mSyntheticPathway { printSbstncPrfrdNm?: string; sbmsnImage?: string; ssg4mSyntheticPathwayDetailsList?: Array; + fileUrl?: string; } export interface Ssg4mSyntheticPathwayDetail { 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 070477b57..975f8ea2a 100644 --- a/src/app/core/substance-ssg4m/substance-ssg4m-form.component.ts +++ b/src/app/core/substance-ssg4m/substance-ssg4m-form.component.ts @@ -1027,7 +1027,7 @@ export class SubstanceSsg4ManufactureFormComponent implements OnInit, AfterViewI this.validationResult = false; // if Saved Successfully - if (response && response.synthPathwaySkey) { + if (response && (response.synthPathwaySkey || this.configService.configData.isPfdaVersion)) { if (response.synthPathwaySkey) { this.id = response.synthPathwaySkey.toString(); } @@ -1043,7 +1043,7 @@ export class SubstanceSsg4ManufactureFormComponent implements OnInit, AfterViewI this.saveDelayedMessage = ""; this.isCancelBtnClicked = false; // Only show successful dialog and refresh page, if user does not click on the cancel button. - this.openSuccessDialog(); + this.openSuccessDialog(undefined, this.configService.configData.isPfdaVersion ? response.fileUrl : null); } // Refresh the current page, this will not cause record locking issue /* this.router.routeReuseStrategy.shouldReuseRoute = () => false; @@ -1083,7 +1083,7 @@ export class SubstanceSsg4ManufactureFormComponent implements OnInit, AfterViewI setTimeout(tempCallback(s),3000); }; - + window['schemeUtil'].renderScheme(window['schemeUtil'].makeDisplayGraph(JSON.parse(ssgjs)), "#scheme-viz-view"); } @@ -1284,10 +1284,21 @@ export class SubstanceSsg4ManufactureFormComponent implements OnInit, AfterViewI return old; } - openSuccessDialog(type?: string): void { + openSuccessDialog(type?: string, fileUrl?: string): void { let data = { - isCoreSubstance: 'false' + isCoreSubstance: 'false', + type: null, + fileUrl: null }; + + if (this.configService.configData.isPfdaVersion) { + data = { + isCoreSubstance: 'true', + type: 'submit', + fileUrl: fileUrl + } + } + const dialogRef = this.dialog.open(SubmitSuccessDialogComponent, { data: data }); @@ -1323,6 +1334,11 @@ export class SubstanceSsg4ManufactureFormComponent implements OnInit, AfterViewI 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); From 0313387343b5a2eafe677efc8555d8020c6943a7 Mon Sep 17 00:00:00 2001 From: Petr Barta Date: Thu, 27 Feb 2025 11:21:50 +0100 Subject: [PATCH 034/114] Login required when trying to submit SSG4m in pfda --- .../substance-ssg4m-form.service.ts | 26 +++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) 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 877df79bd..cad349ce9 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(); @@ -310,7 +312,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 { From 1dcfce9c110898082964ffb6dcab15fb60ef2ef9 Mon Sep 17 00:00:00 2001 From: Petr Barta Date: Thu, 27 Mar 2025 01:12:11 -0600 Subject: [PATCH 035/114] Import G4SSM from PFDA --- src/app/core/substance-form/substance-form.component.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/app/core/substance-form/substance-form.component.ts b/src/app/core/substance-form/substance-form.component.ts index 86fcce163..af570dd9f 100644 --- a/src/app/core/substance-form/substance-form.component.ts +++ b/src/app/core/substance-form/substance-form.component.ts @@ -245,6 +245,12 @@ export class SubstanceFormComponent implements OnInit, AfterViewInit, OnDestroy // There are probably other components affected. There is an issue with subscriptions likely due to some OnInit not firing const read = JSON.parse(response); + + if (read.substanceClass === 'specifiedSubstanceG4m') { + this.router.navigateByUrl('/substances-ssg4m/register?action=import&header=' + true, { state: { record: response } }); + return; + } + if (this.id && read.uuid && this.id === read.uuid) { this.substanceFormService.importSubstance(read, 'update'); this.submissionMessage = null; From 5d387825d204d80179204208571f8875ed4d6176 Mon Sep 17 00:00:00 2001 From: Petr Barta Date: Wed, 23 Apr 2025 11:46:21 +0200 Subject: [PATCH 036/114] PFDA Support email address loaded from config file --- src/app/core/base/pfda-toolbar/pfda-toolbar.component.html | 2 +- src/app/core/base/pfda-toolbar/pfda-toolbar.component.ts | 2 ++ src/app/core/config/config.pfda.json | 2 +- src/app/fda/config/config.json | 2 +- 4 files changed, 5 insertions(+), 3 deletions(-) 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 7f867f2b4..1d0177b47 100644 --- a/src/app/core/base/pfda-toolbar/pfda-toolbar.component.html +++ b/src/app/core/base/pfda-toolbar/pfda-toolbar.component.html @@ -46,7 +46,7 @@ (closed)="removeZindex()"> - +
Support
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 7e922b555..60d727f5d 100644 --- a/src/app/core/base/pfda-toolbar/pfda-toolbar.component.ts +++ b/src/app/core/base/pfda-toolbar/pfda-toolbar.component.ts @@ -15,6 +15,7 @@ import { NavItem } from '@gsrs-core/config'; }) export class PfdaToolbarComponent implements OnInit { pfdaBaseUrl: string; + supportEmail: string; logoSrcPath: string; homeIconPath: string; auth?: Auth; @@ -43,6 +44,7 @@ export class PfdaToolbarComponent implements OnInit { 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(); diff --git a/src/app/core/config/config.pfda.json b/src/app/core/config/config.pfda.json index 54955437b..c036b5f06 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\\ \\(EC\/EINECS\\)" ], - "contactEmail": "precisionfda-support@dnanexus.com", + "contactEmail": "fda-srs@fda.hhs.gov", "sessionExpirationWarning": { "extendSessionApiUrl": "/api/update_active", "maxSessionDurationMinutes": 15 diff --git a/src/app/fda/config/config.json b/src/app/fda/config/config.json index a16732a6c..4e365cfad 100644 --- a/src/app/fda/config/config.json +++ b/src/app/fda/config/config.json @@ -1,6 +1,6 @@ { "version": "3.1.1", - "contactEmail": "GSRSSupport@fda.hhs.gov", + "contactEmail": "fda-srs@fda.hhs.gov", "displayMatchApplication": "true", "adverseEventShinyHomepageDisplay": "true", "adverseEventShinySubstanceNameDisplay": "true", From 36212f85168a142378bf970b20684feda0fac2ff Mon Sep 17 00:00:00 2001 From: Petr Barta Date: Wed, 23 Apr 2025 11:16:18 +0200 Subject: [PATCH 037/114] Draft bulk submission --- .../substance-drafts.component.html | 92 +++++-- .../substance-drafts.component.scss | 99 +++++++ .../substance-drafts.component.ts | 249 ++++++++++++++++-- 3 files changed, 405 insertions(+), 35 deletions(-) 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 27917d9e4..3c64aa446 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,16 +1,31 @@
- +
-
Saved Drafts
+
Saved Drafts
+
Drafts Validation
+
Submission Results
- - - +
+ +
+
+ +
+ + + + + @@ -37,18 +52,13 @@ -
Select + + Delete
+
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.
- - - - - - +
@@ -57,7 +67,7 @@ Show only current record
- +
Show only new registrations @@ -66,6 +76,9 @@
+ Save Backup @@ -74,8 +87,57 @@
{{filename? filename: 'no file chosen'}}
- +
-
\ No newline at end of file + + + + +
+ +
+
+ Type: {{draft.json.type}}, Name: {{ draft.json.name ? draft.json.name : '-' }} + This substance cannot be submitted +
+
+ + {{ message.messageType }} + + + {{ message.message }} +
+ {{ link.text }} +
+
+
+
+
+
+ +
+
+ +
+ +
+
+ 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 cd265d50e..daa38e4e1 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 @@ -26,3 +26,102 @@ display: flex; flex-direction: row; } + +/* 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 a1fa75743..0a567866a 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', @@ -16,23 +30,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, @@ -49,13 +68,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(); - - } @@ -109,7 +126,6 @@ export class SubstanceDraftsComponent implements OnInit { useDraft(index) { - this.dialogRef.close(index); } @@ -126,15 +142,201 @@ 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 || prop.monomerSubstance == {}) { + 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; + 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; + 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; + } + }); + } + }) + } + + + 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; + }); } @@ -156,7 +358,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])); @@ -168,7 +370,8 @@ export class SubstanceDraftsComponent implements OnInit { } this.filtered = this.values.sort((a, b) => { return b.date - a.date; - });; + }); + this.selectedKeys = []; if (this.json && this.json.uuid) { this.filterToggle('substance'); @@ -206,11 +409,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 } @@ -221,9 +430,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 +} From ba040d21b68a7efb66271f847e10c4a39b2deea6 Mon Sep 17 00:00:00 2001 From: Petr Barta Date: Thu, 24 Apr 2025 15:09:52 +0200 Subject: [PATCH 038/114] Revert default FDA support email --- src/app/fda/config/config.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/fda/config/config.json b/src/app/fda/config/config.json index 4e365cfad..a16732a6c 100644 --- a/src/app/fda/config/config.json +++ b/src/app/fda/config/config.json @@ -1,6 +1,6 @@ { "version": "3.1.1", - "contactEmail": "fda-srs@fda.hhs.gov", + "contactEmail": "GSRSSupport@fda.hhs.gov", "displayMatchApplication": "true", "adverseEventShinyHomepageDisplay": "true", "adverseEventShinySubstanceNameDisplay": "true", From c9ea49be502bf08d2310494d216e885dc18ddde4 Mon Sep 17 00:00:00 2001 From: Petr Barta Date: Tue, 6 May 2025 12:42:04 +0300 Subject: [PATCH 039/114] missing property fixed --- .../session-expiration-dialog.component.ts | 1 - .../substance-edit-import-dialog.component.ts | 45 ++++++++++--------- 2 files changed, 23 insertions(+), 23 deletions(-) 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 071c55c64..12d22c520 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 @@ -80,7 +80,6 @@ export class SessionExpirationDialogComponent implements OnInit { if (this.configService.configData.isPfdaVersion) { this.authService.pfdaLogin().pipe( concatMap(success => { - console.log('success: ', success); if (success) { this.closeDialog(); return this.authService.getAuth(); 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 f52eed7e7..5f5895bca 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 @@ -110,28 +110,29 @@ export class SubstanceEditImportDialogComponent implements OnInit { } } - 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 = ''; - } - - } - } + // 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; From e4baa10499b4ac62cc7b5ad707c48bba25428742 Mon Sep 17 00:00:00 2001 From: Jaroslav Iha Date: Wed, 13 Aug 2025 12:14:39 +0200 Subject: [PATCH 040/114] update JSDraw license --- src/app/core/assets/jsdraw/Scilligence.JSDraw2.Pro.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/core/assets/jsdraw/Scilligence.JSDraw2.Pro.js b/src/app/core/assets/jsdraw/Scilligence.JSDraw2.Pro.js index b007bf5b5..a3fad8d9d 100644 --- a/src/app/core/assets/jsdraw/Scilligence.JSDraw2.Pro.js +++ b/src/app/core/assets/jsdraw/Scilligence.JSDraw2.Pro.js @@ -25,8 +25,8 @@ JSDraw2.password = { encrypt: true, key: null, iv: null }; // Place the license code below // Licensed to: FDA // Product: JSDraw -// Expiration Date: 2025-Jul-30 -JSDraw2.licensecode='405562538916781761723242424242424131213141512181'; +// Expiration Date: 2026-Jul-30 +JSDraw2.licensecode='405562537916781761723242424242424131213141512181'; ////////////////////////////////////////////////////////////////////////////////// From 0bc02331db4ed86be55656c4b7b2f7d334b17653 Mon Sep 17 00:00:00 2001 From: Jaroslav Iha Date: Tue, 9 Sep 2025 13:57:39 +0200 Subject: [PATCH 041/114] add: svg saving for stepview --- src/app/core/substance-ssg4m/model/substance-ssg4m.model.ts | 3 +-- .../core/substance-ssg4m/substance-ssg4m-form.component.ts | 5 ++++- 2 files changed, 5 insertions(+), 3 deletions(-) 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 9a11ebdfd..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,6 +15,7 @@ export interface Ssg4mSyntheticPathway { printSbstncUuid?: string; printSbstncPrfrdNm?: string; sbmsnImage?: string; + stepViewImage?: string; ssg4mSyntheticPathwayDetailsList?: Array; fileUrl?: string; } @@ -31,4 +31,3 @@ export interface Ssg4mSyntheticPathwayDetail { sbstncReactnSectNm?: string; sbstncRoleNm?: string; } - 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 975f8ea2a..5f193253e 100644 --- a/src/app/core/substance-ssg4m/substance-ssg4m-form.component.ts +++ b/src/app/core/substance-ssg4m/substance-ssg4m-form.component.ts @@ -41,6 +41,7 @@ import { JsonDialogComponent } from '@gsrs-core/substance-form/json-dialog/json- 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'; @Component({ selector: 'app-substance-ssg4m-form', @@ -993,7 +994,7 @@ export class SubstanceSsg4ManufactureFormComponent implements OnInit, AfterViewI //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 = async (svg) => { window["schemeUtil"].onFinishedLayout = (svg) => { }; // if New Record, initialize object @@ -1008,6 +1009,8 @@ export class SubstanceSsg4ManufactureFormComponent implements OnInit, AfterViewI // Save SVG as Clob this.ssg4mSyntheticPathway.sbmsnImage = document.querySelector("#scheme-viz-view").innerHTML; + this.ssg4mSyntheticPathway.stepViewImage= await toSvg(document.querySelector('app-ssg4m-scheme-view') as HTMLElement); + // 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(() => { From 9d55408eb49c742072cec496eebd272a9abf2158 Mon Sep 17 00:00:00 2001 From: Jaroslav Iha Date: Wed, 10 Sep 2025 15:16:23 +0200 Subject: [PATCH 042/114] svg save test --- .../substance-ssg4m-form.component.ts | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) 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 5f193253e..95a0cf00c 100644 --- a/src/app/core/substance-ssg4m/substance-ssg4m-form.component.ts +++ b/src/app/core/substance-ssg4m/substance-ssg4m-form.component.ts @@ -1009,7 +1009,27 @@ export class SubstanceSsg4ManufactureFormComponent implements OnInit, AfterViewI // Save SVG as Clob this.ssg4mSyntheticPathway.sbmsnImage = document.querySelector("#scheme-viz-view").innerHTML; - this.ssg4mSyntheticPathway.stepViewImage= await toSvg(document.querySelector('app-ssg4m-scheme-view') as HTMLElement); + const dataUrl = await toSvg(document.querySelector('app-ssg4m-scheme-view') as HTMLElement); + const commaIndex = dataUrl.indexOf(','); + const encodedSvg = dataUrl.slice(commaIndex + 1); + const downloadLink = document.createElement('a'); + + // 2. Set the href attribute to the data URL + downloadLink.href = dataUrl; + + // 3. Set the download attribute to the desired file name + downloadLink.download = 'img.svg'; + + // 4. Append the link to the document. This is required for Firefox. + document.body.appendChild(downloadLink); + + // 5. Programmatically click the link to initiate the download + downloadLink.click(); + + // 6. Remove the link from the document + document.body.removeChild(downloadLink); + + this.ssg4mSyntheticPathway.stepViewImage = decodeURIComponent(encodedSvg); // 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. From af67f9ecdf0ae4447965b884b1156eaf2d1e3d0c Mon Sep 17 00:00:00 2001 From: Jaroslav Iha Date: Thu, 11 Sep 2025 12:37:17 +0200 Subject: [PATCH 043/114] svg save test --- .../substance-ssg4m/substance-ssg4m-form.component.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) 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 95a0cf00c..e96b3bf38 100644 --- a/src/app/core/substance-ssg4m/substance-ssg4m-form.component.ts +++ b/src/app/core/substance-ssg4m/substance-ssg4m-form.component.ts @@ -1009,7 +1009,16 @@ export class SubstanceSsg4ManufactureFormComponent implements OnInit, AfterViewI // Save SVG as Clob this.ssg4mSyntheticPathway.sbmsnImage = document.querySelector("#scheme-viz-view").innerHTML; - const dataUrl = await toSvg(document.querySelector('app-ssg4m-scheme-view') as HTMLElement); + const options = { + fetchRequestInit: { + headers: new Headers(), + mode: 'cors' as RequestMode, // Important for fetching from other domains like Google Fonts + cache: 'default' as RequestCache + }, + // We can explicitly tell it to include all fonts. + fontEmbedCSS: '@font-face' + } + const dataUrl = await toSvg(document.querySelector('app-ssg4m-scheme-view') as HTMLElement, options); const commaIndex = dataUrl.indexOf(','); const encodedSvg = dataUrl.slice(commaIndex + 1); const downloadLink = document.createElement('a'); From f6d82de85d1e2903d31230af1c9685acebd11909 Mon Sep 17 00:00:00 2001 From: Jaroslav Iha Date: Mon, 15 Sep 2025 13:43:21 +0200 Subject: [PATCH 044/114] svg save test --- .../substance-ssg4m-form.component.ts | 51 ++++++++++++++++--- 1 file changed, 45 insertions(+), 6 deletions(-) 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 e96b3bf38..965da64a5 100644 --- a/src/app/core/substance-ssg4m/substance-ssg4m-form.component.ts +++ b/src/app/core/substance-ssg4m/substance-ssg4m-form.component.ts @@ -982,6 +982,23 @@ export class SubstanceSsg4ManufactureFormComponent implements OnInit, AfterViewI }); } + 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 ``; + } + submit(): void { this.isLoading = true; this.loadingService.setLoading(true); @@ -1009,16 +1026,37 @@ export class SubstanceSsg4ManufactureFormComponent implements OnInit, AfterViewI // Save SVG as Clob this.ssg4mSyntheticPathway.sbmsnImage = document.querySelector("#scheme-viz-view").innerHTML; + // const options = { + // fetchRequestInit: { + // headers: new Headers(), + // mode: 'cors' as RequestMode, // Important for fetching from other domains like Google Fonts + // cache: 'default' as RequestCache + // }, + // // We can explicitly tell it to include all fonts. + // fontEmbedCSS: '@font-face' + // } + const elementToConvert = document.querySelector('app-ssg4m-scheme-view') as HTMLElement; + const clone = elementToConvert.cloneNode(true) as HTMLElement; + const container = document.createElement('div'); + container.style.position = 'absolute'; + container.style.left = '-9999px'; + const styles = this.getPageStyles(); + container.innerHTML = styles; + container.appendChild(clone); + document.body.appendChild(container); + const options = { + width: elementToConvert.offsetWidth, + height: elementToConvert.offsetHeight, + // You may still need the fetch options for external images/fonts fetchRequestInit: { headers: new Headers(), - mode: 'cors' as RequestMode, // Important for fetching from other domains like Google Fonts + mode: 'cors' as RequestMode, cache: 'default' as RequestCache - }, - // We can explicitly tell it to include all fonts. - fontEmbedCSS: '@font-face' - } - const dataUrl = await toSvg(document.querySelector('app-ssg4m-scheme-view') as HTMLElement, options); + } + }; + + const dataUrl = await toSvg(clone, options); const commaIndex = dataUrl.indexOf(','); const encodedSvg = dataUrl.slice(commaIndex + 1); const downloadLink = document.createElement('a'); @@ -1037,6 +1075,7 @@ export class SubstanceSsg4ManufactureFormComponent implements OnInit, AfterViewI // 6. Remove the link from the document document.body.removeChild(downloadLink); + document.body.removeChild(container); this.ssg4mSyntheticPathway.stepViewImage = decodeURIComponent(encodedSvg); From 381ef791268e6d41bc05acd81234a68d493dd229 Mon Sep 17 00:00:00 2001 From: Jaroslav Iha Date: Mon, 15 Sep 2025 18:12:14 +0200 Subject: [PATCH 045/114] svg save test --- .../core/substance-ssg4m/substance-ssg4m-form.component.ts | 5 +++++ 1 file changed, 5 insertions(+) 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 965da64a5..67ead515b 100644 --- a/src/app/core/substance-ssg4m/substance-ssg4m-form.component.ts +++ b/src/app/core/substance-ssg4m/substance-ssg4m-form.component.ts @@ -999,6 +999,10 @@ export class SubstanceSsg4ManufactureFormComponent implements OnInit, AfterViewI return ``; } + delay(ms: number): Promise { + return new Promise(resolve => setTimeout(resolve, ms)); + } + submit(): void { this.isLoading = true; this.loadingService.setLoading(true); @@ -1044,6 +1048,7 @@ export class SubstanceSsg4ManufactureFormComponent implements OnInit, AfterViewI container.innerHTML = styles; container.appendChild(clone); document.body.appendChild(container); + await this.delay(5000) const options = { width: elementToConvert.offsetWidth, From 247fdc02d9b376e7b1ddd9333ddeb4da8531e0e1 Mon Sep 17 00:00:00 2001 From: Jaroslav Iha Date: Tue, 16 Sep 2025 14:52:53 +0200 Subject: [PATCH 046/114] svg save test --- .../substance-ssg4m-form.component.ts | 84 +++++++------------ 1 file changed, 32 insertions(+), 52 deletions(-) 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 67ead515b..45f8411a0 100644 --- a/src/app/core/substance-ssg4m/substance-ssg4m-form.component.ts +++ b/src/app/core/substance-ssg4m/substance-ssg4m-form.component.ts @@ -1003,6 +1003,37 @@ export class SubstanceSsg4ManufactureFormComponent implements OnInit, AfterViewI return new Promise(resolve => setTimeout(resolve, ms)); } + async exportStepView(document: Document): Promise { + const elementToConvert = document.querySelector('app-ssg4m-scheme-view') as HTMLElement; + const clone = elementToConvert.cloneNode(true) as HTMLElement; + + const container = document.createElement('div'); + container.style.position = 'absolute'; + container.style.left = '-9999px'; + const styles = this.getPageStyles(); + container.innerHTML = styles; + container.appendChild(clone); + document.body.appendChild(container); + + await this.delay(5000) + + const options = { + width: elementToConvert.offsetWidth, + height: elementToConvert.offsetHeight, + // You may still need the fetch options for external images/fonts + fetchRequestInit: { + headers: new Headers(), + mode: 'cors' as RequestMode, + cache: 'default' as RequestCache + } + }; + + const dataUrl = await toSvg(clone, options); + const commaIndex = dataUrl.indexOf(','); + document.body.removeChild(container); + return dataUrl.slice(commaIndex + 1); + } + submit(): void { this.isLoading = true; this.loadingService.setLoading(true); @@ -1029,59 +1060,8 @@ export class SubstanceSsg4ManufactureFormComponent implements OnInit, AfterViewI // Save SVG as Clob this.ssg4mSyntheticPathway.sbmsnImage = document.querySelector("#scheme-viz-view").innerHTML; - - // const options = { - // fetchRequestInit: { - // headers: new Headers(), - // mode: 'cors' as RequestMode, // Important for fetching from other domains like Google Fonts - // cache: 'default' as RequestCache - // }, - // // We can explicitly tell it to include all fonts. - // fontEmbedCSS: '@font-face' - // } - const elementToConvert = document.querySelector('app-ssg4m-scheme-view') as HTMLElement; - const clone = elementToConvert.cloneNode(true) as HTMLElement; - const container = document.createElement('div'); - container.style.position = 'absolute'; - container.style.left = '-9999px'; - const styles = this.getPageStyles(); - container.innerHTML = styles; - container.appendChild(clone); - document.body.appendChild(container); - await this.delay(5000) - - const options = { - width: elementToConvert.offsetWidth, - height: elementToConvert.offsetHeight, - // You may still need the fetch options for external images/fonts - fetchRequestInit: { - headers: new Headers(), - mode: 'cors' as RequestMode, - cache: 'default' as RequestCache - } - }; - - const dataUrl = await toSvg(clone, options); - const commaIndex = dataUrl.indexOf(','); - const encodedSvg = dataUrl.slice(commaIndex + 1); - const downloadLink = document.createElement('a'); - - // 2. Set the href attribute to the data URL - downloadLink.href = dataUrl; - - // 3. Set the download attribute to the desired file name - downloadLink.download = 'img.svg'; - - // 4. Append the link to the document. This is required for Firefox. - document.body.appendChild(downloadLink); - - // 5. Programmatically click the link to initiate the download - downloadLink.click(); - - // 6. Remove the link from the document - document.body.removeChild(downloadLink); - document.body.removeChild(container); + const encodedSvg = await this.exportStepView(document) this.ssg4mSyntheticPathway.stepViewImage = decodeURIComponent(encodedSvg); // After submitting Save button, the UI waits for 5 seconds to see if it gets a response. From 8c548dd8d7c7dedd7c7e4aac05c3e18e1dc8cf7e Mon Sep 17 00:00:00 2001 From: Jaroslav Iha Date: Wed, 17 Sep 2025 11:49:02 +0200 Subject: [PATCH 047/114] svg save test --- .../substance-ssg4m-form.component.ts | 35 ++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) 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 45f8411a0..371590997 100644 --- a/src/app/core/substance-ssg4m/substance-ssg4m-form.component.ts +++ b/src/app/core/substance-ssg4m/substance-ssg4m-form.component.ts @@ -1003,7 +1003,37 @@ export class SubstanceSsg4ManufactureFormComponent implements OnInit, AfterViewI return new Promise(resolve => setTimeout(resolve, ms)); } + // 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 exportStepView(document: Document): Promise { + const tabProcesses = document.querySelector("#mat-expansion-panel-header-2") as HTMLElement; + if (tabProcesses.getAttribute('aria-expanded') !== 'true') { + console.log('Tab Processes not selected. Clicking it...'); + tabProcesses.click(); + await this.delay(200) + } + + const tabStepView = document.querySelector("#mat-tab-label-0-1") as HTMLElement; + if (tabStepView.getAttribute('aria-selected') !== 'true') { + console.log('Tab Step View not selected. Clicking it...'); + tabStepView.click(); + await this.delay(200) + } + const elementToConvert = document.querySelector('app-ssg4m-scheme-view') as HTMLElement; const clone = elementToConvert.cloneNode(true) as HTMLElement; @@ -1017,10 +1047,13 @@ export class SubstanceSsg4ManufactureFormComponent implements OnInit, AfterViewI await this.delay(5000) + function filter (node) { + return (node.tagName !== 'button'); + } const options = { + filter: filter, width: elementToConvert.offsetWidth, height: elementToConvert.offsetHeight, - // You may still need the fetch options for external images/fonts fetchRequestInit: { headers: new Headers(), mode: 'cors' as RequestMode, From 0dd9cf4f24d1f74932e23dcf95330ba9a800dcc6 Mon Sep 17 00:00:00 2001 From: Jaroslav Iha Date: Wed, 17 Sep 2025 14:15:11 +0200 Subject: [PATCH 048/114] svg save test --- .../substance-ssg4m/substance-ssg4m-form.component.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) 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 371590997..636aa245e 100644 --- a/src/app/core/substance-ssg4m/substance-ssg4m-form.component.ts +++ b/src/app/core/substance-ssg4m/substance-ssg4m-form.component.ts @@ -806,7 +806,7 @@ export class SubstanceSsg4ManufactureFormComponent implements OnInit, AfterViewI }, 5000); } - validate(validationType?: string): void { + async validate(validationType?: string): Promise { if (validationType && validationType === 'approval') { this.approving = true; } else { @@ -826,7 +826,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) { @@ -1019,7 +1019,7 @@ export class SubstanceSsg4ManufactureFormComponent implements OnInit, AfterViewI // }); // } - async exportStepView(document: Document): Promise { + async expandStepView(): Promise { const tabProcesses = document.querySelector("#mat-expansion-panel-header-2") as HTMLElement; if (tabProcesses.getAttribute('aria-expanded') !== 'true') { console.log('Tab Processes not selected. Clicking it...'); @@ -1033,7 +1033,9 @@ export class SubstanceSsg4ManufactureFormComponent implements OnInit, AfterViewI 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; @@ -1067,7 +1069,8 @@ export class SubstanceSsg4ManufactureFormComponent implements OnInit, AfterViewI return dataUrl.slice(commaIndex + 1); } - submit(): void { + async submit(): Promise { + await this.expandStepView() this.isLoading = true; this.loadingService.setLoading(true); this.approving = false; From 50aec927629783c1aacd33058c3c6fea387f4f31 Mon Sep 17 00:00:00 2001 From: Jaroslav Iha Date: Wed, 17 Sep 2025 14:59:37 +0200 Subject: [PATCH 049/114] svg save test --- .../substance-ssg4m-form.component.ts | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) 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 636aa245e..6a31b022b 100644 --- a/src/app/core/substance-ssg4m/substance-ssg4m-form.component.ts +++ b/src/app/core/substance-ssg4m/substance-ssg4m-form.component.ts @@ -806,7 +806,7 @@ export class SubstanceSsg4ManufactureFormComponent implements OnInit, AfterViewI }, 5000); } - async validate(validationType?: string): Promise { + validate(validationType?: string): void { if (validationType && validationType === 'approval') { this.approving = true; } else { @@ -826,7 +826,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) { - await this.submit(); + this.submit(); } /* if (this.validationMessages.length === 0 && true === true) { @@ -1019,7 +1019,7 @@ export class SubstanceSsg4ManufactureFormComponent implements OnInit, AfterViewI // }); // } - async expandStepView(): Promise { + async exportStepView(document: Document): Promise { const tabProcesses = document.querySelector("#mat-expansion-panel-header-2") as HTMLElement; if (tabProcesses.getAttribute('aria-expanded') !== 'true') { console.log('Tab Processes not selected. Clicking it...'); @@ -1033,9 +1033,7 @@ export class SubstanceSsg4ManufactureFormComponent implements OnInit, AfterViewI 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; @@ -1069,10 +1067,7 @@ export class SubstanceSsg4ManufactureFormComponent implements OnInit, AfterViewI return dataUrl.slice(commaIndex + 1); } - async submit(): Promise { - await this.expandStepView() - this.isLoading = true; - this.loadingService.setLoading(true); + submit(): void { this.approving = false; this.json = this.substanceFormService.cleanSubstance(); @@ -1089,6 +1084,9 @@ export class SubstanceSsg4ManufactureFormComponent implements OnInit, AfterViewI if (this.ssg4mSyntheticPathway == null) { this.ssg4mSyntheticPathway = {}; } + const encodedSvg = await this.exportStepView(document); + this.isLoading = true; + this.loadingService.setLoading(true); // Existing Record // get the JSON from the SSG4m Form and store as a Clob into the database @@ -1097,7 +1095,6 @@ export class SubstanceSsg4ManufactureFormComponent implements OnInit, AfterViewI // Save SVG as Clob this.ssg4mSyntheticPathway.sbmsnImage = document.querySelector("#scheme-viz-view").innerHTML; - const encodedSvg = await this.exportStepView(document) this.ssg4mSyntheticPathway.stepViewImage = decodeURIComponent(encodedSvg); // After submitting Save button, the UI waits for 5 seconds to see if it gets a response. From 4d97ce9817d9ca19b8e6ef536ac3d5902212d94d Mon Sep 17 00:00:00 2001 From: Jaroslav Iha Date: Wed, 17 Sep 2025 15:01:45 +0200 Subject: [PATCH 050/114] svg save test --- .../core/substance-ssg4m/substance-ssg4m-form.component.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 6a31b022b..8c453d91c 100644 --- a/src/app/core/substance-ssg4m/substance-ssg4m-form.component.ts +++ b/src/app/core/substance-ssg4m/substance-ssg4m-form.component.ts @@ -1033,6 +1033,8 @@ export class SubstanceSsg4ManufactureFormComponent implements OnInit, AfterViewI tabStepView.click(); await this.delay(200) } + this.isLoading = true; + this.loadingService.setLoading(true); const elementToConvert = document.querySelector('app-ssg4m-scheme-view') as HTMLElement; const clone = elementToConvert.cloneNode(true) as HTMLElement; @@ -1085,8 +1087,6 @@ export class SubstanceSsg4ManufactureFormComponent implements OnInit, AfterViewI this.ssg4mSyntheticPathway = {}; } const encodedSvg = await this.exportStepView(document); - this.isLoading = true; - this.loadingService.setLoading(true); // Existing Record // get the JSON from the SSG4m Form and store as a Clob into the database From 7fac01b443d5c395a2618c99de668f27b7ec736e Mon Sep 17 00:00:00 2001 From: Jaroslav Iha Date: Wed, 17 Sep 2025 16:21:57 +0200 Subject: [PATCH 051/114] svg save test --- .../substance-ssg4m-form.component.ts | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) 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 8c453d91c..8271d9225 100644 --- a/src/app/core/substance-ssg4m/substance-ssg4m-form.component.ts +++ b/src/app/core/substance-ssg4m/substance-ssg4m-form.component.ts @@ -806,7 +806,7 @@ export class SubstanceSsg4ManufactureFormComponent implements OnInit, AfterViewI }, 5000); } - validate(validationType?: string): void { + async validate(validationType?: string): Promise { if (validationType && validationType === 'approval') { this.approving = true; } else { @@ -826,7 +826,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) { @@ -1019,8 +1019,8 @@ export class SubstanceSsg4ManufactureFormComponent implements OnInit, AfterViewI // }); // } - async exportStepView(document: Document): Promise { - const tabProcesses = document.querySelector("#mat-expansion-panel-header-2") as HTMLElement; + 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(); @@ -1033,9 +1033,9 @@ export class SubstanceSsg4ManufactureFormComponent implements OnInit, AfterViewI tabStepView.click(); await this.delay(200) } - this.isLoading = true; - this.loadingService.setLoading(true); + } + async exportStepView(document: Document): Promise { const elementToConvert = document.querySelector('app-ssg4m-scheme-view') as HTMLElement; const clone = elementToConvert.cloneNode(true) as HTMLElement; @@ -1069,7 +1069,10 @@ export class SubstanceSsg4ManufactureFormComponent implements OnInit, AfterViewI return dataUrl.slice(commaIndex + 1); } - submit(): void { + async submit(): Promise { + await this.expandStepView() + this.isLoading = true; + this.loadingService.setLoading(true); this.approving = false; this.json = this.substanceFormService.cleanSubstance(); @@ -1086,7 +1089,6 @@ export class SubstanceSsg4ManufactureFormComponent implements OnInit, AfterViewI if (this.ssg4mSyntheticPathway == null) { this.ssg4mSyntheticPathway = {}; } - const encodedSvg = await this.exportStepView(document); // Existing Record // get the JSON from the SSG4m Form and store as a Clob into the database @@ -1095,6 +1097,7 @@ export class SubstanceSsg4ManufactureFormComponent implements OnInit, AfterViewI // Save SVG as Clob this.ssg4mSyntheticPathway.sbmsnImage = document.querySelector("#scheme-viz-view").innerHTML; + const encodedSvg = await this.exportStepView(document) this.ssg4mSyntheticPathway.stepViewImage = decodeURIComponent(encodedSvg); // After submitting Save button, the UI waits for 5 seconds to see if it gets a response. From d371478391d734e7dbbf8b79237fd282167b467f Mon Sep 17 00:00:00 2001 From: Jaroslav Iha Date: Wed, 17 Sep 2025 18:47:08 +0200 Subject: [PATCH 052/114] svg save test --- .../substance-ssg4m/substance-ssg4m-form.component.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 8271d9225..edd9e47f5 100644 --- a/src/app/core/substance-ssg4m/substance-ssg4m-form.component.ts +++ b/src/app/core/substance-ssg4m/substance-ssg4m-form.component.ts @@ -1047,7 +1047,7 @@ export class SubstanceSsg4ManufactureFormComponent implements OnInit, AfterViewI container.appendChild(clone); document.body.appendChild(container); - await this.delay(5000) + await this.delay(2500) function filter (node) { return (node.tagName !== 'button'); @@ -1070,7 +1070,7 @@ export class SubstanceSsg4ManufactureFormComponent implements OnInit, AfterViewI } async submit(): Promise { - await this.expandStepView() + // await this.expandStepView() this.isLoading = true; this.loadingService.setLoading(true); this.approving = false; @@ -1097,8 +1097,8 @@ export class SubstanceSsg4ManufactureFormComponent implements OnInit, AfterViewI // Save SVG as Clob this.ssg4mSyntheticPathway.sbmsnImage = document.querySelector("#scheme-viz-view").innerHTML; - const encodedSvg = await this.exportStepView(document) - this.ssg4mSyntheticPathway.stepViewImage = decodeURIComponent(encodedSvg); + // const encodedSvg = await this.exportStepView(document) + // this.ssg4mSyntheticPathway.stepViewImage = decodeURIComponent(encodedSvg); // 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. From 7885470c8eabffe05bd85b0db7db8bb738447cab Mon Sep 17 00:00:00 2001 From: Jaroslav Iha Date: Thu, 18 Sep 2025 11:26:46 +0200 Subject: [PATCH 053/114] svg save test --- .../core/substance-ssg4m/substance-ssg4m-form.component.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 edd9e47f5..9e46d7f7a 100644 --- a/src/app/core/substance-ssg4m/substance-ssg4m-form.component.ts +++ b/src/app/core/substance-ssg4m/substance-ssg4m-form.component.ts @@ -806,7 +806,7 @@ export class SubstanceSsg4ManufactureFormComponent implements OnInit, AfterViewI }, 5000); } - async validate(validationType?: string): Promise { + validate(validationType?: string): void { if (validationType && validationType === 'approval') { this.approving = true; } else { @@ -826,7 +826,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) { - await this.submit(); + this.submit(); } /* if (this.validationMessages.length === 0 && true === true) { @@ -1069,7 +1069,7 @@ export class SubstanceSsg4ManufactureFormComponent implements OnInit, AfterViewI return dataUrl.slice(commaIndex + 1); } - async submit(): Promise { + submit(): void { // await this.expandStepView() this.isLoading = true; this.loadingService.setLoading(true); From 260a204e07552104c89db5bd3ac197531a7cc48f Mon Sep 17 00:00:00 2001 From: Jaroslav Iha Date: Thu, 18 Sep 2025 14:23:10 +0200 Subject: [PATCH 054/114] code polishing --- .../substance-ssg4m-form.component.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) 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 9e46d7f7a..cf17fa875 100644 --- a/src/app/core/substance-ssg4m/substance-ssg4m-form.component.ts +++ b/src/app/core/substance-ssg4m/substance-ssg4m-form.component.ts @@ -806,7 +806,7 @@ export class SubstanceSsg4ManufactureFormComponent implements OnInit, AfterViewI }, 5000); } - validate(validationType?: string): void { + async validate(validationType?: string): Promise { if (validationType && validationType === 'approval') { this.approving = true; } else { @@ -826,7 +826,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) { @@ -1069,8 +1069,8 @@ export class SubstanceSsg4ManufactureFormComponent implements OnInit, AfterViewI return dataUrl.slice(commaIndex + 1); } - submit(): void { - // await this.expandStepView() + async submit(): Promise { + await this.expandStepView() this.isLoading = true; this.loadingService.setLoading(true); this.approving = false; @@ -1097,8 +1097,8 @@ export class SubstanceSsg4ManufactureFormComponent implements OnInit, AfterViewI // Save SVG as Clob this.ssg4mSyntheticPathway.sbmsnImage = document.querySelector("#scheme-viz-view").innerHTML; - // const encodedSvg = await this.exportStepView(document) - // this.ssg4mSyntheticPathway.stepViewImage = decodeURIComponent(encodedSvg); + const encodedSvg = await this.exportStepView(document) + this.ssg4mSyntheticPathway.stepViewImage = decodeURIComponent(encodedSvg); // 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. From 4d3222518c946e16bbc96359ac0e9a96dee1f70c Mon Sep 17 00:00:00 2001 From: Jaroslav Iha Date: Fri, 19 Sep 2025 14:11:29 +0200 Subject: [PATCH 055/114] add: filter for button --- .../substance-ssg4m/substance-ssg4m-form.component.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 cf17fa875..97b3c8c92 100644 --- a/src/app/core/substance-ssg4m/substance-ssg4m-form.component.ts +++ b/src/app/core/substance-ssg4m/substance-ssg4m-form.component.ts @@ -1049,8 +1049,8 @@ export class SubstanceSsg4ManufactureFormComponent implements OnInit, AfterViewI await this.delay(2500) - function filter (node) { - return (node.tagName !== 'button'); + function filter (node: HTMLElement) { + return (node.tagName.toLowerCase() !== 'button'); } const options = { filter: filter, @@ -1100,13 +1100,13 @@ export class SubstanceSsg4ManufactureFormComponent implements OnInit, AfterViewI const encodedSvg = await this.exportStepView(document) this.ssg4mSyntheticPathway.stepViewImage = decodeURIComponent(encodedSvg); - // After submitting Save button, the UI waits for 5 seconds to see if it gets a response. + // After submitting Save button, the UI waits for 8 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); + }, 8000); this.submitSubscription = this.substanceSsg4mService.saveSsg4m(this.ssg4mSyntheticPathway).pipe(take(1)).subscribe(response => { // Stop the spinner From 6e1613db24d93c10353fc4744145e8a66ded9736 Mon Sep 17 00:00:00 2001 From: Jaroslav Iha Date: Wed, 1 Oct 2025 11:22:24 +0200 Subject: [PATCH 056/114] fix filter for button --- .../core/substance-ssg4m/substance-ssg4m-form.component.ts | 4 ++++ 1 file changed, 4 insertions(+) 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 97b3c8c92..0cb45ee2c 100644 --- a/src/app/core/substance-ssg4m/substance-ssg4m-form.component.ts +++ b/src/app/core/substance-ssg4m/substance-ssg4m-form.component.ts @@ -1050,6 +1050,10 @@ export class SubstanceSsg4ManufactureFormComponent implements OnInit, AfterViewI await this.delay(2500) function filter (node: HTMLElement) { + if (!node.tagName) { + return true; + } + return (node.tagName.toLowerCase() !== 'button'); } const options = { From 88f1e69afc8a0a0638f30a9e8631bec968b88fb8 Mon Sep 17 00:00:00 2001 From: Jaroslav Iha Date: Tue, 7 Oct 2025 10:38:44 +0200 Subject: [PATCH 057/114] allow UUID generator --- src/app/core/substance-form/substance-form.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/core/substance-form/substance-form.component.html b/src/app/core/substance-form/substance-form.component.html index aa021f55b..d1615e3f0 100644 --- a/src/app/core/substance-form/substance-form.component.html +++ b/src/app/core/substance-form/substance-form.component.html @@ -20,7 +20,7 @@ {{ showSubmissionMessages ? 'Hide' : 'Show' }} messages -
+
Advanced Features From b5791db835207ac720d0d403e79a49ad825e6436 Mon Sep 17 00:00:00 2001 From: Jaroslav Iha Date: Tue, 7 Oct 2025 11:41:39 +0200 Subject: [PATCH 058/114] allow UUID generator for pfda version --- src/app/core/substance-form/substance-form.component.html | 4 ++-- src/app/core/substance-form/substance-form.component.ts | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/app/core/substance-form/substance-form.component.html b/src/app/core/substance-form/substance-form.component.html index d1615e3f0..4c566991c 100644 --- a/src/app/core/substance-form/substance-form.component.html +++ b/src/app/core/substance-form/substance-form.component.html @@ -20,7 +20,7 @@ {{ showSubmissionMessages ? 'Hide' : 'Show' }} messages -
+
Advanced Features @@ -59,7 +59,7 @@ Switch primary and alt definitions - + Predict N-Glycosylation Sites diff --git a/src/app/core/substance-form/substance-form.component.ts b/src/app/core/substance-form/substance-form.component.ts index fdd05fefa..fb32ea43c 100644 --- a/src/app/core/substance-form/substance-form.component.ts +++ b/src/app/core/substance-form/substance-form.component.ts @@ -91,6 +91,7 @@ export class SubstanceFormComponent implements OnInit, AfterViewInit, OnDestroy feature: string; isAdmin: boolean; isUpdater: boolean; + isPfdaVersion: boolean = false; messageField: string; uuid: string; substanceClass: string; @@ -310,6 +311,7 @@ export class SubstanceFormComponent implements OnInit, AfterViewInit, OnDestroy } this.isAdmin = this.authService.hasRoles('admin'); this.isUpdater = this.authService.hasAnyRoles('Updater', 'SuperUpdater'); + this.isPfdaVersion = this.configService.configData.isPfdaVersion; this.overlayContainer = this.overlayContainerService.getContainerElement(); this.imported = false; if(this.location.path().includes('chemical-simplified')) { From cc752747640685ecc70a3fdd7ae78f5b0f22d387 Mon Sep 17 00:00:00 2001 From: Jaroslav Iha Date: Tue, 7 Oct 2025 16:09:16 +0200 Subject: [PATCH 059/114] disable save ssg4m btn for non logged users --- .../core/substance-ssg4m/substance-ssg4m-form.component.html | 2 +- src/app/core/substance-ssg4m/substance-ssg4m-form.component.ts | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) 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 0560b73ad..09715caec 100644 --- a/src/app/core/substance-ssg4m/substance-ssg4m-form.component.html +++ b/src/app/core/substance-ssg4m/substance-ssg4m-form.component.html @@ -51,7 +51,7 @@ + [disabled]="showFormReadOnly === 'true' || isAuthenticated === false">Save -

-     - - - - - -
\ 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 465ffe461..5cc399657 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 @@ -207,28 +207,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/substance-ssg4m-form.component.ts b/src/app/core/substance-ssg4m/substance-ssg4m-form.component.ts index 78efd5963..bcf30a31b 100644 --- a/src/app/core/substance-ssg4m/substance-ssg4m-form.component.ts +++ b/src/app/core/substance-ssg4m/substance-ssg4m-form.component.ts @@ -1005,6 +1005,17 @@ export class SubstanceSsg4ManufactureFormComponent implements OnInit, AfterViewI 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}`; +} + // waitForElement(selector: string, timeout = 2000): Promise { // return new Promise((resolve, reject) => { // const interval = setInterval(() => { @@ -1040,7 +1051,7 @@ export class SubstanceSsg4ManufactureFormComponent implements OnInit, AfterViewI } } - async exportStepView(document: Document): Promise { + 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; @@ -1092,13 +1103,13 @@ export class SubstanceSsg4ManufactureFormComponent implements OnInit, AfterViewI const dataUrl = await toSvg(clone, options); const downloadLink = document.createElement('a'); downloadLink.href = dataUrl; - downloadLink.download = `ssg4m_step_view_${Date.now().toString(36) + Math.random().toString(36).substring(2, 9)}.svg`; + downloadLink.download = `ssg4m_step_view_${this.generateTimestampId()}.svg`; document.body.appendChild(downloadLink); downloadLink.click(); document.body.removeChild(downloadLink); - const commaIndex = dataUrl.indexOf(','); - document.body.removeChild(container); - return dataUrl.slice(commaIndex + 1); + // const commaIndex = dataUrl.indexOf(','); + // document.body.removeChild(container); + // return dataUrl.slice(commaIndex + 1); } async submit(): Promise { @@ -1127,10 +1138,11 @@ export class SubstanceSsg4ManufactureFormComponent implements OnInit, AfterViewI this.ssg4mSyntheticPathway.sbmsnDataText = jsonValue; // Save SVG as Clob - this.ssg4mSyntheticPathway.sbmsnImage = document.querySelector("#scheme-viz-view").innerHTML; + // this.ssg4mSyntheticPathway.sbmsnImage = document.querySelector("#scheme-viz-view").innerHTML; - const encodedSvg = await this.exportStepView(document) - this.ssg4mSyntheticPathway.stepViewImage = decodeURIComponent(encodedSvg); + await this.exportStepView(document) + // const encodedSvg = await this.exportStepView(document) + // this.ssg4mSyntheticPathway.stepViewImage = decodeURIComponent(encodedSvg); // After submitting Save button, the UI waits for 8 seconds to see if it gets a response. // after 5 seconds it displays a warning on the top of the UI form. @@ -1208,7 +1220,7 @@ export class SubstanceSsg4ManufactureFormComponent implements OnInit, AfterViewI setTimeout(tempCallback(s),3000); }; - window['schemeUtil'].renderScheme(window['schemeUtil'].makeDisplayGraph(JSON.parse(ssgjs)), "#scheme-viz-view"); + // window['schemeUtil'].renderScheme(window['schemeUtil'].makeDisplayGraph(JSON.parse(ssgjs)), "#scheme-viz-view"); } From 1b7c459cb8d8c3e4905af22bcd90009951ca14cc Mon Sep 17 00:00:00 2001 From: Jaroslav Iha Date: Thu, 27 Nov 2025 09:39:54 +0100 Subject: [PATCH 064/114] remove scheme view --- ...tance-form-ssg4m-process-card.component.ts | 100 +++++++++--------- .../substance-ssg4m-form.component.ts | 34 +++--- 2 files changed, 67 insertions(+), 67 deletions(-) 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 5cc399657..ba65f948a 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 @@ -55,56 +55,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'; 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 bcf30a31b..6d3cd8321 100644 --- a/src/app/core/substance-ssg4m/substance-ssg4m-form.component.ts +++ b/src/app/core/substance-ssg4m/substance-ssg4m-form.component.ts @@ -270,15 +270,15 @@ 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 { @@ -1125,8 +1125,8 @@ export class SubstanceSsg4ManufactureFormComponent implements OnInit, AfterViewI //This is a hacky placeholder way to force viz //TODO finish this const ssgjs = JSON.stringify(this.substanceFormService.cleanSubstance()); - window["schemeUtil"].onFinishedLayout = async (svg) => { - window["schemeUtil"].onFinishedLayout = (svg) => { }; + // window["schemeUtil"].onFinishedLayout = async (svg) => { + // window["schemeUtil"].onFinishedLayout = (svg) => { }; // if New Record, initialize object if (this.ssg4mSyntheticPathway == null) { @@ -1211,14 +1211,14 @@ export class SubstanceSsg4ManufactureFormComponent implements OnInit, AfterViewI }); this.subscriptions.push(this.submitSubscription); - }; //window + // }; //window - let tempCallback = window["schemeUtil"].onFinishedLayout; - window["schemeUtil"].onFinishedLayout = (s)=>{ - window["schemeUtil"].onFinishedLayout =(ss)=>{}; + // let tempCallback = window["schemeUtil"].onFinishedLayout; + // window["schemeUtil"].onFinishedLayout = (s)=>{ + // window["schemeUtil"].onFinishedLayout =(ss)=>{}; - setTimeout(tempCallback(s),3000); - }; + // setTimeout(tempCallback(s),3000); + // }; // window['schemeUtil'].renderScheme(window['schemeUtil'].makeDisplayGraph(JSON.parse(ssgjs)), "#scheme-viz-view"); From 6dac361949bd5b6f97fd2fcb5e3a86606652c5d5 Mon Sep 17 00:00:00 2001 From: Jaroslav Iha Date: Tue, 2 Dec 2025 14:29:59 +0100 Subject: [PATCH 065/114] fix delete export; disable step view export for pfda --- .../download-monitor/download-monitor.component.ts | 3 +++ .../core/substance-ssg4m/substance-ssg4m-form.component.ts | 5 +++-- 2 files changed, 6 insertions(+), 2 deletions(-) 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 2cc8343f6..1b807dde5 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 @@ -79,6 +79,9 @@ export class DownloadMonitorComponent implements OnInit, OnDestroy { } deleteDownload() { + if (!this.download.removeUrl.url) { + this.cancel(); + } this.authService.deleteDownload(this.download.removeUrl.url).pipe(take(1)).subscribe(response => { this.deleted = true; }); 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 6d3cd8321..47bd8f8f3 100644 --- a/src/app/core/substance-ssg4m/substance-ssg4m-form.component.ts +++ b/src/app/core/substance-ssg4m/substance-ssg4m-form.component.ts @@ -1032,6 +1032,7 @@ export class SubstanceSsg4ManufactureFormComponent implements OnInit, AfterViewI // }); // } + // Step View cannot be hidden in order to export it async expandStepView(): Promise { const tabProcesses = document.querySelector("#substance-form-ssg4m-process") as HTMLElement; if (tabProcesses.getAttribute('aria-expanded') !== 'true') { @@ -1113,7 +1114,7 @@ export class SubstanceSsg4ManufactureFormComponent implements OnInit, AfterViewI } async submit(): Promise { - await this.expandStepView() + !this.configService.configData.isPfdaVersion && await this.expandStepView(); this.isLoading = true; this.loadingService.setLoading(true); this.approving = false; @@ -1140,7 +1141,7 @@ export class SubstanceSsg4ManufactureFormComponent implements OnInit, AfterViewI // Save SVG as Clob // this.ssg4mSyntheticPathway.sbmsnImage = document.querySelector("#scheme-viz-view").innerHTML; - await this.exportStepView(document) + !this.configService.configData.isPfdaVersion && await this.exportStepView(document) // const encodedSvg = await this.exportStepView(document) // this.ssg4mSyntheticPathway.stepViewImage = decodeURIComponent(encodedSvg); From 79cf6fdcdac0521a5718c03b7ed6c16ce5d16e88 Mon Sep 17 00:00:00 2001 From: Jaroslav Iha Date: Tue, 2 Dec 2025 15:23:02 +0100 Subject: [PATCH 066/114] fix delete export --- .../download-monitor/download-monitor.component.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 1b807dde5..eb36b0bca 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 @@ -79,8 +79,9 @@ export class DownloadMonitorComponent implements OnInit, OnDestroy { } deleteDownload() { - if (!this.download.removeUrl.url) { + if (!this.download.removeUrl || !this.download.removeUrl.url) { this.cancel(); + this.refresh(); } this.authService.deleteDownload(this.download.removeUrl.url).pipe(take(1)).subscribe(response => { this.deleted = true; From b69f4cffc7dcabb37cb55ab35bcaadccd72c0ea7 Mon Sep 17 00:00:00 2001 From: Jaroslav Iha Date: Tue, 2 Dec 2025 15:46:35 +0100 Subject: [PATCH 067/114] enable step view svg for pfda --- .../core/substance-ssg4m/substance-ssg4m-form.component.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) 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 47bd8f8f3..6d3cd8321 100644 --- a/src/app/core/substance-ssg4m/substance-ssg4m-form.component.ts +++ b/src/app/core/substance-ssg4m/substance-ssg4m-form.component.ts @@ -1032,7 +1032,6 @@ export class SubstanceSsg4ManufactureFormComponent implements OnInit, AfterViewI // }); // } - // Step View cannot be hidden in order to export it async expandStepView(): Promise { const tabProcesses = document.querySelector("#substance-form-ssg4m-process") as HTMLElement; if (tabProcesses.getAttribute('aria-expanded') !== 'true') { @@ -1114,7 +1113,7 @@ export class SubstanceSsg4ManufactureFormComponent implements OnInit, AfterViewI } async submit(): Promise { - !this.configService.configData.isPfdaVersion && await this.expandStepView(); + await this.expandStepView() this.isLoading = true; this.loadingService.setLoading(true); this.approving = false; @@ -1141,7 +1140,7 @@ export class SubstanceSsg4ManufactureFormComponent implements OnInit, AfterViewI // Save SVG as Clob // this.ssg4mSyntheticPathway.sbmsnImage = document.querySelector("#scheme-viz-view").innerHTML; - !this.configService.configData.isPfdaVersion && await this.exportStepView(document) + await this.exportStepView(document) // const encodedSvg = await this.exportStepView(document) // this.ssg4mSyntheticPathway.stepViewImage = decodeURIComponent(encodedSvg); From 8660c53468d891ff22f3a79ecff74600e9dd3d91 Mon Sep 17 00:00:00 2001 From: Jaroslav Iha Date: Wed, 3 Dec 2025 08:09:19 +0100 Subject: [PATCH 068/114] update export delete; --- .../download-monitor.component.ts | 35 ++++++++++++++----- 1 file changed, 26 insertions(+), 9 deletions(-) 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 eb36b0bca..e3e9c2a91 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 @@ -1,9 +1,10 @@ import { Component, OnInit, Input, Output, EventEmitter, OnDestroy } from '@angular/core'; import { AuthService } from '@gsrs-core/auth/auth.service'; import * as moment from 'moment'; -import { take } from 'rxjs/operators'; +import { switchMap, take, tap } from 'rxjs/operators'; import { ConfigService } from '@gsrs-core/config'; import { NavigationExtras } from '@angular/router'; +import { EMPTY, Observable, of } from 'rxjs'; @Component({ selector: 'app-download-monitor', @@ -67,9 +68,12 @@ export class DownloadMonitorComponent implements OnInit, OnDestroy { } cancel() { - this.authService.changeDownload(this.download.cancelUrl.url).pipe(take(1)).subscribe(response => { - this.refresh(); - }); + // Note: The cancel logic performs a changeDownload and then refreshes. + // We will now return the Observable so we can chain it. + return this.authService.changeDownload(this.download.cancelUrl.url).pipe( + take(1), + tap(() => this.refresh()) // Use tap to call refresh, but pass the Observable on + ); } downloadExport() { @@ -79,11 +83,24 @@ export class DownloadMonitorComponent implements OnInit, OnDestroy { } deleteDownload() { - if (!this.download.removeUrl || !this.download.removeUrl.url) { - this.cancel(); - this.refresh(); - } - this.authService.deleteDownload(this.download.removeUrl.url).pipe(take(1)).subscribe(response => { + // 1. Determine the URL to use for the initial action. + const action$: Observable = this.download.removeUrl?.url + ? of(null) // If remove URL exists, start with a resolved Observable (no initial action needed). + : this.cancel(); // If remove URL is missing, execute the cancel logic. + + action$.pipe( + // 2. Once the first action (cancel or no-op) is complete, switch to the deletion logic. + switchMap(() => { + // We only proceed to delete if the remove URL is defined. + if (this.download.removeUrl?.url) { + return this.authService.deleteDownload(this.download.removeUrl.url).pipe(take(1)); + } + + // If the remove URL was missing, we just ran 'cancel()' and stop here. + return EMPTY; + }) + ).subscribe(response => { + // 3. This runs only if the deletion was attempted and succeeded. this.deleted = true; }); } From ab881224c7eb32eadc18dc4e8c2d8bce46041cfc Mon Sep 17 00:00:00 2001 From: Jaroslav Iha Date: Wed, 3 Dec 2025 10:21:29 +0100 Subject: [PATCH 069/114] test fix delete download --- .../download-monitor.component.ts | 66 +++++++++++-------- 1 file changed, 38 insertions(+), 28 deletions(-) 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 e3e9c2a91..61fe96b6e 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 @@ -1,10 +1,9 @@ import { Component, OnInit, Input, Output, EventEmitter, OnDestroy } from '@angular/core'; import { AuthService } from '@gsrs-core/auth/auth.service'; import * as moment from 'moment'; -import { switchMap, take, tap } from 'rxjs/operators'; +import { take } from 'rxjs/operators'; import { ConfigService } from '@gsrs-core/config'; import { NavigationExtras } from '@angular/router'; -import { EMPTY, Observable, of } from 'rxjs'; @Component({ selector: 'app-download-monitor', @@ -68,12 +67,13 @@ export class DownloadMonitorComponent implements OnInit, OnDestroy { } cancel() { - // Note: The cancel logic performs a changeDownload and then refreshes. - // We will now return the Observable so we can chain it. - return this.authService.changeDownload(this.download.cancelUrl.url).pipe( - take(1), - tap(() => this.refresh()) // Use tap to call refresh, but pass the Observable on - ); + const obs = this.authService.changeDownload(this.download.cancelUrl.url).pipe(take(1)); + // keep existing behavior when called from template (subscribe and refresh) + obs.subscribe(response => { + this.refresh(); + }); + // also return the observable so callers can chain (used by deleteDownload) + return obs; } downloadExport() { @@ -83,26 +83,36 @@ export class DownloadMonitorComponent implements OnInit, OnDestroy { } deleteDownload() { - // 1. Determine the URL to use for the initial action. - const action$: Observable = this.download.removeUrl?.url - ? of(null) // If remove URL exists, start with a resolved Observable (no initial action needed). - : this.cancel(); // If remove URL is missing, execute the cancel logic. - - action$.pipe( - // 2. Once the first action (cancel or no-op) is complete, switch to the deletion logic. - switchMap(() => { - // We only proceed to delete if the remove URL is defined. - if (this.download.removeUrl?.url) { - return this.authService.deleteDownload(this.download.removeUrl.url).pipe(take(1)); - } - - // If the remove URL was missing, we just ran 'cancel()' and stop here. - return EMPTY; - }) - ).subscribe(response => { - // 3. This runs only if the deletion was attempted and succeeded. - this.deleted = true; - }); + if (this.download && this.download.removeUrl && this.download.removeUrl.url) { + this.authService.deleteDownload(this.download.removeUrl.url).pipe(take(1)).subscribe(response => { + this.deleted = true; + }); + } else { + // If there is no removeUrl yet, attempt to cancel the download first (which should create the removeUrl), + // then try deleting. If cancel or removeUrl are not available, mark as deleted to remove from view. + if (this.download && this.download.cancelUrl && this.download.cancelUrl.url) { + this.cancel().pipe(take(1)).subscribe(() => { + // After cancel completes, request the latest status to get any newly-created removeUrl + this.authService.getUpdateStatus(this.id).pipe(take(1)).subscribe(response => { + this.download = response; + if (this.download && this.download.removeUrl && this.download.removeUrl.url) { + this.authService.deleteDownload(this.download.removeUrl.url).pipe(take(1)).subscribe(resp => { + this.deleted = true; + }); + } else { + // fallback: if still no removeUrl, mark deleted to hide the entry + this.deleted = true; + } + }, err => { + // on error getting status, fallback to hiding the entry + this.deleted = true; + }); + }); + } else { + // No cancel URL either; nothing to call on server — hide it locally + this.deleted = true; + } + } } processQuery(url: string) { From 1c85259d2dc85fdb77e3aac4fbb193e380989746 Mon Sep 17 00:00:00 2001 From: Jaroslav Iha Date: Wed, 3 Dec 2025 12:07:23 +0100 Subject: [PATCH 070/114] fix delete download --- .../download-monitor.component.html | 9 ++++- .../download-monitor.component.ts | 39 ++----------------- 2 files changed, 12 insertions(+), 36 deletions(-) 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..a968abb8e 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,14 @@
- 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 61fe96b6e..2cc8343f6 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 @@ -67,13 +67,9 @@ export class DownloadMonitorComponent implements OnInit, OnDestroy { } cancel() { - const obs = this.authService.changeDownload(this.download.cancelUrl.url).pipe(take(1)); - // keep existing behavior when called from template (subscribe and refresh) - obs.subscribe(response => { + this.authService.changeDownload(this.download.cancelUrl.url).pipe(take(1)).subscribe(response => { this.refresh(); }); - // also return the observable so callers can chain (used by deleteDownload) - return obs; } downloadExport() { @@ -83,36 +79,9 @@ export class DownloadMonitorComponent implements OnInit, OnDestroy { } deleteDownload() { - if (this.download && this.download.removeUrl && this.download.removeUrl.url) { - this.authService.deleteDownload(this.download.removeUrl.url).pipe(take(1)).subscribe(response => { - this.deleted = true; - }); - } else { - // If there is no removeUrl yet, attempt to cancel the download first (which should create the removeUrl), - // then try deleting. If cancel or removeUrl are not available, mark as deleted to remove from view. - if (this.download && this.download.cancelUrl && this.download.cancelUrl.url) { - this.cancel().pipe(take(1)).subscribe(() => { - // After cancel completes, request the latest status to get any newly-created removeUrl - this.authService.getUpdateStatus(this.id).pipe(take(1)).subscribe(response => { - this.download = response; - if (this.download && this.download.removeUrl && this.download.removeUrl.url) { - this.authService.deleteDownload(this.download.removeUrl.url).pipe(take(1)).subscribe(resp => { - this.deleted = true; - }); - } else { - // fallback: if still no removeUrl, mark deleted to hide the entry - this.deleted = true; - } - }, err => { - // on error getting status, fallback to hiding the entry - this.deleted = true; - }); - }); - } else { - // No cancel URL either; nothing to call on server — hide it locally - this.deleted = true; - } - } + this.authService.deleteDownload(this.download.removeUrl.url).pipe(take(1)).subscribe(response => { + this.deleted = true; + }); } processQuery(url: string) { From 10971702e083ee9d395a112e019c154f2fa245c6 Mon Sep 17 00:00:00 2001 From: Jaroslav Iha Date: Wed, 3 Dec 2025 12:44:30 +0100 Subject: [PATCH 071/114] fix delete download --- .../download-monitor/download-monitor.component.html | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) 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 a968abb8e..065e72d03 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,14 +126,7 @@
- From 691a73e9becc2824e5038e54a8827fcec79b3849 Mon Sep 17 00:00:00 2001 From: Jaroslav Iha Date: Wed, 3 Dec 2025 13:36:48 +0100 Subject: [PATCH 072/114] update enable delete button --- .../download-monitor/download-monitor.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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..609ba4096 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 @@
- From 567f5c521691f5f229d491d69d5276135bd328cf Mon Sep 17 00:00:00 2001 From: Jaroslav Iha Date: Wed, 3 Dec 2025 14:10:22 +0100 Subject: [PATCH 073/114] try to disable button --- .../download-monitor/download-monitor.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 609ba4096..7fd9d1940 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 @@
- From a0a25bba9e21bdcc06ea2572cdab7c6a5ee79453 Mon Sep 17 00:00:00 2001 From: Jaroslav Iha Date: Wed, 3 Dec 2025 14:36:17 +0100 Subject: [PATCH 074/114] build remove url from cancel one --- .../download-monitor.component.ts | 45 +++++++++++++++++-- 1 file changed, 41 insertions(+), 4 deletions(-) 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 2cc8343f6..9150f8f61 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 @@ -67,9 +67,13 @@ export class DownloadMonitorComponent implements OnInit, OnDestroy { } cancel() { - this.authService.changeDownload(this.download.cancelUrl.url).pipe(take(1)).subscribe(response => { + const obs = this.authService.changeDownload(this.download.cancelUrl.url).pipe(take(1)); + // keep existing behavior when called from template (subscribe and refresh) + obs.subscribe(response => { this.refresh(); }); + // also return the observable so callers can chain (used by deleteDownload) + return obs; } downloadExport() { @@ -79,9 +83,42 @@ export class DownloadMonitorComponent implements OnInit, OnDestroy { } deleteDownload() { - this.authService.deleteDownload(this.download.removeUrl.url).pipe(take(1)).subscribe(response => { - this.deleted = true; - }); + if (this.download && this.download.removeUrl && this.download.removeUrl.url) { + this.authService.deleteDownload(this.download.removeUrl.url).pipe(take(1)).subscribe(response => { + this.deleted = true; + }); + } else { + // If there is no removeUrl yet, attempt to cancel the download first (which should create the removeUrl), + // then try deleting. If cancel or removeUrl are not available, mark as deleted to remove from view. + if (this.download && this.download.cancelUrl && this.download.cancelUrl.url) { + this.cancel().pipe(take(1)).subscribe(() => { + // After cancel completes, request the latest status to get any newly-created removeUrl + this.authService.getUpdateStatus(this.id).pipe(take(1)).subscribe(response => { + this.download = response; + if (this.download && this.download.removeUrl && this.download.removeUrl.url) { + this.authService.deleteDownload(this.download.removeUrl.url).pipe(take(1)).subscribe(resp => { + this.deleted = true; + }); + } else { + // fallback: if still no removeUrl, mark deleted to hide the entry + if (this.download.cancelUrl?.url) { + this.authService.deleteDownload(this.download.cancelUrl.url.replace('/@cancel', '')).pipe(take(1)).subscribe(resp => { + this.deleted = true; + }); + } else { + this.deleted = true; + } + } + }, err => { + // on error getting status, fallback to hiding the entry + this.deleted = true; + }); + }); + } else { + // No cancel URL either; nothing to call on server — hide it locally + this.deleted = true; + } + } } processQuery(url: string) { From e792968f268245770c43d33da32b554124a6e381 Mon Sep 17 00:00:00 2001 From: Jaroslav Iha Date: Wed, 3 Dec 2025 15:03:30 +0100 Subject: [PATCH 075/114] revert button disable --- .../download-monitor/download-monitor.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 7fd9d1940..065e72d03 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 @@
- From d549b413bdb1d1f3853e8c8e4e7f094938a4e8a4 Mon Sep 17 00:00:00 2001 From: Jaroslav Iha Date: Wed, 3 Dec 2025 15:10:19 +0100 Subject: [PATCH 076/114] clean delete fix code --- .../download-monitor/download-monitor.component.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 9150f8f61..29fe6dc8a 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 @@ -89,7 +89,7 @@ export class DownloadMonitorComponent implements OnInit, OnDestroy { }); } else { // If there is no removeUrl yet, attempt to cancel the download first (which should create the removeUrl), - // then try deleting. If cancel or removeUrl are not available, mark as deleted to remove from view. + // then try deleting. If cancel or removeUrl are not available, build the remove url from cancel one, otherwise mark as deleted to remove from view. if (this.download && this.download.cancelUrl && this.download.cancelUrl.url) { this.cancel().pipe(take(1)).subscribe(() => { // After cancel completes, request the latest status to get any newly-created removeUrl @@ -100,7 +100,7 @@ export class DownloadMonitorComponent implements OnInit, OnDestroy { this.deleted = true; }); } else { - // fallback: if still no removeUrl, mark deleted to hide the entry + // fallback: if still no removeUrl, try to create it from cancelUrl if (this.download.cancelUrl?.url) { this.authService.deleteDownload(this.download.cancelUrl.url.replace('/@cancel', '')).pipe(take(1)).subscribe(resp => { this.deleted = true; From d3b90b9e39ace994c2271778b405651b7bb5b149 Mon Sep 17 00:00:00 2001 From: Jaroslav Iha Date: Wed, 3 Dec 2025 15:29:29 +0100 Subject: [PATCH 077/114] first test of add Draft to g4ssm --- .../ssg4m-stages-form.component.html | 6 + .../ssg4m-stages-form.component.ts | 283 ++++++++++++++---- 2 files changed, 232 insertions(+), 57 deletions(-) diff --git a/src/app/core/substance-ssg4m/ssg4m-stages/ssg4m-stages-form.component.html b/src/app/core/substance-ssg4m/ssg4m-stages/ssg4m-stages-form.component.html index b6b891b16..3bb62749b 100644 --- a/src/app/core/substance-ssg4m/ssg4m-stages/ssg4m-stages-form.component.html +++ b/src/app/core/substance-ssg4m/ssg4m-stages/ssg4m-stages-form.component.html @@ -100,6 +100,12 @@ (click)="addStartingMaterial(processIndex, siteIndex, stageIndex)"> Add Input Material + +
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 1ac2de87c..b13189cc3 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,21 +23,29 @@ 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 { SubstanceDraftsComponent } from "@gsrs-core/substance-form/substance-drafts/substance-drafts.component"; +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'] + selector: "app-ssg4m-stages-form", + templateUrl: "./ssg4m-stages-form.component.html", + styleUrls: ["./ssg4m-stages-form.component.scss"], }) export class Ssg4mStagesFormComponent implements OnInit, OnDestroy { public configSettingsDisplay = {}; + private overlayContainer: HTMLElement; configSsg4Form: any; configTitleStage: string; configTitleProcessingMaterials: string; @@ -49,7 +66,7 @@ export class Ssg4mStagesFormComponent implements OnInit, OnDestroy { private scrollToService: ScrollToService, public configService: ConfigService, private dialog: MatDialog - ) { } + ) {} @Input() set stage(stage: SpecifiedSubstanceG4mStage) { @@ -82,7 +99,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); } @@ -112,30 +129,39 @@ 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); + // overlay container for dialogs + this.overlayContainer = this.overlayContainerService.getContainerElement(); + // 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(); }); } @@ -143,74 +169,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(); } @@ -218,7 +325,70 @@ 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); + } + + /** + * Open drafts dialog and add selected draft as a Starting Material for this stage + */ + addStartingMaterialFromDraft( + processIndex: number, + siteIndex: number, + stageIndex: number + ) { + const dialogRef = this.dialog.open(SubstanceDraftsComponent, { + maxHeight: "85%", + width: "70%", + data: { uuid: this.substance ? this.substance.uuid : null }, + }); + if (this.overlayContainer) { + this.overlayContainer.style.zIndex = "1002"; + } + + const sub = dialogRef.afterClosed().subscribe((response) => { + if (this.overlayContainer) { + this.overlayContainer.style.zIndex = null; + } + if (response && response.substance) { + const read = response.substance; + + // Add a new starting material then set its substanceName to reference the selected draft + this.substanceFormSsg4mStagesService.addStartingMaterials( + processIndex, + siteIndex, + stageIndex + ); + + const stageObj = + this.substance.specifiedSubstanceG4m.process[processIndex].sites[ + siteIndex + ].stages[stageIndex]; + const newStartIndex = stageObj.startingMaterials.length - 1; + + // Determine a display name for the substance + let displayName = read._name + ? String(read._name).replace(/<[^>]*>?/gm, "") + : null; + if (!displayName && read.names && read.names.length > 0) { + const n = read.names.find((x) => x.stdName) || read.names[0]; + displayName = n.stdName || n.name || null; + } + + stageObj.startingMaterials[newStartIndex].substanceName = { + refuuid: read.uuid, + name: displayName, + substanceClass: read.substanceClass, + } as any; + + // notify subscribers about the change + this.substanceFormSsg4mStagesService.propertyEmitter.next( + stageObj.startingMaterials + ); + } + sub.unsubscribe(); + }); } /* @@ -230,4 +400,3 @@ export class Ssg4mStagesFormComponent implements OnInit, OnDestroy { } */ } - From b248170eb1bca071f4d669f6a42f74aff37b4580 Mon Sep 17 00:00:00 2001 From: Jaroslav Iha Date: Mon, 8 Dec 2025 14:29:48 +0100 Subject: [PATCH 078/114] displayname fix --- .../ssg4m-stages/ssg4m-stages-form.component.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 b13189cc3..f80ae130a 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 @@ -368,8 +368,8 @@ export class Ssg4mStagesFormComponent implements OnInit, OnDestroy { const newStartIndex = stageObj.startingMaterials.length - 1; // Determine a display name for the substance - let displayName = read._name - ? String(read._name).replace(/<[^>]*>?/gm, "") + let displayName = response.name + ? String(response.name).replace(/<[^>]*>?/gm, "") : null; if (!displayName && read.names && read.names.length > 0) { const n = read.names.find((x) => x.stdName) || read.names[0]; From a1715797216e30b6f405a43733afb9e989e18911 Mon Sep 17 00:00:00 2001 From: Jaroslav Iha Date: Tue, 16 Dec 2025 10:57:37 +0100 Subject: [PATCH 079/114] revert g4ssm add draft changes --- .../ssg4m-stages-form.component.html | 6 - .../ssg4m-stages-form.component.ts | 283 ++++-------------- 2 files changed, 57 insertions(+), 232 deletions(-) diff --git a/src/app/core/substance-ssg4m/ssg4m-stages/ssg4m-stages-form.component.html b/src/app/core/substance-ssg4m/ssg4m-stages/ssg4m-stages-form.component.html index 3bb62749b..b6b891b16 100644 --- a/src/app/core/substance-ssg4m/ssg4m-stages/ssg4m-stages-form.component.html +++ b/src/app/core/substance-ssg4m/ssg4m-stages/ssg4m-stages-form.component.html @@ -100,12 +100,6 @@ (click)="addStartingMaterial(processIndex, siteIndex, stageIndex)"> Add Input Material - - 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 f80ae130a..1ac2de87c 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,21 +1,12 @@ -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'; @@ -23,29 +14,21 @@ 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 { SubstanceDraftsComponent } from "@gsrs-core/substance-form/substance-drafts/substance-drafts.component"; -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"], + selector: 'app-ssg4m-stages-form', + templateUrl: './ssg4m-stages-form.component.html', + styleUrls: ['./ssg4m-stages-form.component.scss'] }) export class Ssg4mStagesFormComponent implements OnInit, OnDestroy { public configSettingsDisplay = {}; - private overlayContainer: HTMLElement; configSsg4Form: any; configTitleStage: string; configTitleProcessingMaterials: string; @@ -66,7 +49,7 @@ export class Ssg4mStagesFormComponent implements OnInit, OnDestroy { private scrollToService: ScrollToService, public configService: ConfigService, private dialog: MatDialog - ) {} + ) { } @Input() set stage(stage: SpecifiedSubstanceG4mStage) { @@ -99,7 +82,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); } @@ -129,39 +112,30 @@ 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); - // overlay container for dialogs - this.overlayContainer = this.overlayContainerService.getContainerElement(); - // 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(); }); } @@ -169,155 +143,74 @@ 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(); } @@ -325,70 +218,7 @@ export class Ssg4mStagesFormComponent implements OnInit, OnDestroy { } deleteStage(): void { - this.substance.specifiedSubstanceG4m.process[this.processIndex].sites[ - this.siteIndex - ].stages.splice(this.stageIndex, 1); - } - - /** - * Open drafts dialog and add selected draft as a Starting Material for this stage - */ - addStartingMaterialFromDraft( - processIndex: number, - siteIndex: number, - stageIndex: number - ) { - const dialogRef = this.dialog.open(SubstanceDraftsComponent, { - maxHeight: "85%", - width: "70%", - data: { uuid: this.substance ? this.substance.uuid : null }, - }); - if (this.overlayContainer) { - this.overlayContainer.style.zIndex = "1002"; - } - - const sub = dialogRef.afterClosed().subscribe((response) => { - if (this.overlayContainer) { - this.overlayContainer.style.zIndex = null; - } - if (response && response.substance) { - const read = response.substance; - - // Add a new starting material then set its substanceName to reference the selected draft - this.substanceFormSsg4mStagesService.addStartingMaterials( - processIndex, - siteIndex, - stageIndex - ); - - const stageObj = - this.substance.specifiedSubstanceG4m.process[processIndex].sites[ - siteIndex - ].stages[stageIndex]; - const newStartIndex = stageObj.startingMaterials.length - 1; - - // Determine a display name for the substance - let displayName = response.name - ? String(response.name).replace(/<[^>]*>?/gm, "") - : null; - if (!displayName && read.names && read.names.length > 0) { - const n = read.names.find((x) => x.stdName) || read.names[0]; - displayName = n.stdName || n.name || null; - } - - stageObj.startingMaterials[newStartIndex].substanceName = { - refuuid: read.uuid, - name: displayName, - substanceClass: read.substanceClass, - } as any; - - // notify subscribers about the change - this.substanceFormSsg4mStagesService.propertyEmitter.next( - stageObj.startingMaterials - ); - } - sub.unsubscribe(); - }); + this.substance.specifiedSubstanceG4m.process[this.processIndex].sites[this.siteIndex].stages.splice(this.stageIndex, 1); } /* @@ -400,3 +230,4 @@ export class Ssg4mStagesFormComponent implements OnInit, OnDestroy { } */ } + From 2a19a0fcefd95c18f8b8c8c1664d208809aa02be Mon Sep 17 00:00:00 2001 From: Jaroslav Iha Date: Tue, 16 Dec 2025 11:18:32 +0100 Subject: [PATCH 080/114] revert all logic; disable delete button --- .../download-monitor.component.html | 2 +- .../download-monitor.component.ts | 45 ++----------------- 2 files changed, 5 insertions(+), 42 deletions(-) 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..7f46dce89 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 29fe6dc8a..2cc8343f6 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 @@ -67,13 +67,9 @@ export class DownloadMonitorComponent implements OnInit, OnDestroy { } cancel() { - const obs = this.authService.changeDownload(this.download.cancelUrl.url).pipe(take(1)); - // keep existing behavior when called from template (subscribe and refresh) - obs.subscribe(response => { + this.authService.changeDownload(this.download.cancelUrl.url).pipe(take(1)).subscribe(response => { this.refresh(); }); - // also return the observable so callers can chain (used by deleteDownload) - return obs; } downloadExport() { @@ -83,42 +79,9 @@ export class DownloadMonitorComponent implements OnInit, OnDestroy { } deleteDownload() { - if (this.download && this.download.removeUrl && this.download.removeUrl.url) { - this.authService.deleteDownload(this.download.removeUrl.url).pipe(take(1)).subscribe(response => { - this.deleted = true; - }); - } else { - // If there is no removeUrl yet, attempt to cancel the download first (which should create the removeUrl), - // then try deleting. If cancel or removeUrl are not available, build the remove url from cancel one, otherwise mark as deleted to remove from view. - if (this.download && this.download.cancelUrl && this.download.cancelUrl.url) { - this.cancel().pipe(take(1)).subscribe(() => { - // After cancel completes, request the latest status to get any newly-created removeUrl - this.authService.getUpdateStatus(this.id).pipe(take(1)).subscribe(response => { - this.download = response; - if (this.download && this.download.removeUrl && this.download.removeUrl.url) { - this.authService.deleteDownload(this.download.removeUrl.url).pipe(take(1)).subscribe(resp => { - this.deleted = true; - }); - } else { - // fallback: if still no removeUrl, try to create it from cancelUrl - if (this.download.cancelUrl?.url) { - this.authService.deleteDownload(this.download.cancelUrl.url.replace('/@cancel', '')).pipe(take(1)).subscribe(resp => { - this.deleted = true; - }); - } else { - this.deleted = true; - } - } - }, err => { - // on error getting status, fallback to hiding the entry - this.deleted = true; - }); - }); - } else { - // No cancel URL either; nothing to call on server — hide it locally - this.deleted = true; - } - } + this.authService.deleteDownload(this.download.removeUrl.url).pipe(take(1)).subscribe(response => { + this.deleted = true; + }); } processQuery(url: string) { From e7d1601bc507d75225e4e23d4537339c66f7ef7d Mon Sep 17 00:00:00 2001 From: Jaroslav Iha Date: Tue, 16 Dec 2025 14:27:49 +0100 Subject: [PATCH 081/114] Add draft test --- .../ssg4m-stages-form.component.html | 6 + .../ssg4m-stages-form.component.ts | 307 ++++++++++++++---- 2 files changed, 256 insertions(+), 57 deletions(-) diff --git a/src/app/core/substance-ssg4m/ssg4m-stages/ssg4m-stages-form.component.html b/src/app/core/substance-ssg4m/ssg4m-stages/ssg4m-stages-form.component.html index b6b891b16..6ba9f861c 100644 --- a/src/app/core/substance-ssg4m/ssg4m-stages/ssg4m-stages-form.component.html +++ b/src/app/core/substance-ssg4m/ssg4m-stages/ssg4m-stages-form.component.html @@ -100,6 +100,12 @@ (click)="addStartingMaterial(processIndex, siteIndex, stageIndex)"> Add Input Material +   +
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 1ac2de87c..0ab0ddebf 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,18 +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"; +import { SubstanceDraftsComponent } from "@gsrs-core/substance-form/substance-drafts/substance-drafts.component"; @Component({ - selector: 'app-ssg4m-stages-form', - templateUrl: './ssg4m-stages-form.component.html', - styleUrls: ['./ssg4m-stages-form.component.scss'] + selector: "app-ssg4m-stages-form", + templateUrl: "./ssg4m-stages-form.component.html", + styleUrls: ["./ssg4m-stages-form.component.scss"], }) export class Ssg4mStagesFormComponent implements OnInit, OnDestroy { public configSettingsDisplay = {}; @@ -49,7 +65,7 @@ export class Ssg4mStagesFormComponent implements OnInit, OnDestroy { private scrollToService: ScrollToService, public configService: ConfigService, private dialog: MatDialog - ) { } + ) {} @Input() set stage(stage: SpecifiedSubstanceG4mStage) { @@ -82,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); } @@ -112,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(); }); } @@ -143,74 +165,244 @@ 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" + ); + }); + } + + addDraft(processIndex: number, siteIndex: number, stageIndex: number) { + const dialogRef = this.dialog.open(SubstanceDraftsComponent, { + maxHeight: "85%", + width: "70%", + data: { uuid: this.substance && this.substance.uuid }, + }); + + const overlayContainer = this.overlayContainerService.getContainerElement(); + if (overlayContainer) { + overlayContainer.style.zIndex = "1002"; + } + + dialogRef.afterClosed().subscribe((response) => { + if (overlayContainer) { + 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") { + // try to read from the dialog component instance + 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 = draftObj.substance || draftObj; + + // Add a new starting material and populate basic fields from the draft + this.substanceFormSsg4mStagesService.addStartingMaterials( + processIndex, + siteIndex, + stageIndex + ); + + // Locate the newly added starting material (last in the list) + const startList = + this.substance.specifiedSubstanceG4m.process[processIndex].sites[ + siteIndex + ].stages[stageIndex].startingMaterials; + if (!startList || startList.length === 0) { + return; + } + const idx = startList.length - 1; + const newStart = startList[idx]; + + // 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; + + newStart.substanceName = { + name: primaryName, + refuuid: substanceObj.uuid, + substanceClass: substanceObj.substanceClass, + }; + newStart.verbatimName = primaryName; + + // Scroll to newly added starting material entry + setTimeout(() => { + 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(); } @@ -218,7 +410,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); } /* @@ -230,4 +424,3 @@ export class Ssg4mStagesFormComponent implements OnInit, OnDestroy { } */ } - From 8345d64ec2af5e4e4996d5d333103a4b082e2de6 Mon Sep 17 00:00:00 2001 From: Jaroslav Iha Date: Wed, 17 Dec 2025 12:43:03 +0100 Subject: [PATCH 082/114] update add draft functionality --- .../ssg4m-stages/ssg4m-stages-form.component.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 0ab0ddebf..b14b2e447 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 @@ -334,9 +334,11 @@ export class Ssg4mStagesFormComponent implements OnInit, OnDestroy { null; newStart.substanceName = { + refPname: primaryName, name: primaryName, refuuid: substanceObj.uuid, - substanceClass: substanceObj.substanceClass, + substanceClass: "reference", + approvalID: substanceObj.approvalID, }; newStart.verbatimName = primaryName; From 1d2dfda5b404876a627691fe90f5b27e64f30af1 Mon Sep 17 00:00:00 2001 From: Jaroslav Iha Date: Wed, 17 Dec 2025 13:33:24 +0100 Subject: [PATCH 083/114] try image rendering --- .../ssg4m-stages-form.component.ts | 30 ++++++++++++++++++- ...g4m-starting-materials-form.component.html | 6 +++- 2 files changed, 34 insertions(+), 2 deletions(-) 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 b14b2e447..003e88c03 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 @@ -34,6 +34,7 @@ import { } from "../../substance/substance.model"; import { SubstanceDetail } from "@gsrs-core/substance/substance.model"; import { SubstanceFormSsg4mStagesService } from "./substance-form-ssg4m-stages.service"; +import { StructureService } from "@gsrs-core/structure/structure.service"; import { SpecifiedSubstanceG4mStage } from "@gsrs-core/substance/substance.model"; import { ConfirmDialogComponent } from "../../../fda/confirm-dialog/confirm-dialog.component"; import { SubstanceDraftsComponent } from "@gsrs-core/substance-form/substance-drafts/substance-drafts.component"; @@ -64,7 +65,8 @@ export class Ssg4mStagesFormComponent implements OnInit, OnDestroy { private overlayContainerService: OverlayContainer, private scrollToService: ScrollToService, public configService: ConfigService, - private dialog: MatDialog + private dialog: MatDialog, + private structureService: StructureService ) {} @Input() @@ -342,6 +344,32 @@ export class Ssg4mStagesFormComponent implements OnInit, OnDestroy { }; newStart.verbatimName = primaryName; + // 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. + try { + 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( + (response) => { + if (response && response.structure && response.structure.id) { + // store a temp id used by the image directive + (newStart as any).$$tmpStructureId = response.structure.id; + } + }, + (error) => { + // ignore failures to interpret + } + ); + } + } catch (e) { + // swallow any unexpected errors + } + // Scroll to newly added starting material entry setTimeout(() => { this.scrollToService.scrollToElement( 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 a0c727477..9054de33b 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 @@ -19,8 +19,12 @@ *ngIf="configSettingsDisplay['substanceName'] || (configSettingsDisplay['substanceName'] === undefined && true)"> + [subuuid]="startingMaterial.substanceName?.refuuid" [showMorelinks]="true"> + + From c712dcf9ec1d1a3c7d90bc677d9e6c582161213c Mon Sep 17 00:00:00 2001 From: Jaroslav Iha Date: Wed, 17 Dec 2025 14:59:57 +0100 Subject: [PATCH 084/114] update image layout --- .../ssg4m-starting-materials-form.component.html | 12 ++++++++---- .../ssg4m-starting-materials-form.component.scss | 11 +++++++++++ 2 files changed, 19 insertions(+), 4 deletions(-) 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 9054de33b..02954856c 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 @@ -17,10 +17,14 @@
- 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 2cc8343f6..29bd9b9b9 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 @@ -16,6 +16,7 @@ export class DownloadMonitorComponent implements OnInit, OnDestroy { @Output() deletedEmitter = new EventEmitter(); download: any; deleted = false; + canceled = false; exists: boolean; browseLink = false; parameters: NavigationExtras = {}; @@ -68,6 +69,7 @@ export class DownloadMonitorComponent implements OnInit, OnDestroy { cancel() { this.authService.changeDownload(this.download.cancelUrl.url).pipe(take(1)).subscribe(response => { + this.canceled = true; this.refresh(); }); } @@ -79,7 +81,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; }); } From 127cf16bfee6504f5eff04f009f07d349e7cda2d Mon Sep 17 00:00:00 2001 From: Jaroslav Iha Date: Fri, 16 Jan 2026 14:17:52 +0100 Subject: [PATCH 099/114] fix bug with cancelled export --- .../download-monitor/download-monitor.component.html | 2 +- .../download-monitor/download-monitor.component.ts | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) 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 2c2e50cfb..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 29bd9b9b9..68bf17a19 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 @@ -16,7 +16,6 @@ export class DownloadMonitorComponent implements OnInit, OnDestroy { @Output() deletedEmitter = new EventEmitter(); download: any; deleted = false; - canceled = false; exists: boolean; browseLink = false; parameters: NavigationExtras = {}; @@ -69,7 +68,6 @@ export class DownloadMonitorComponent implements OnInit, OnDestroy { cancel() { this.authService.changeDownload(this.download.cancelUrl.url).pipe(take(1)).subscribe(response => { - this.canceled = true; this.refresh(); }); } @@ -81,7 +79,7 @@ export class DownloadMonitorComponent implements OnInit, OnDestroy { } deleteDownload() { - this.authService.deleteDownload(this.download.removeUrl.url || this.download.cancelUrl.url.replace('/@cancel', '')).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; }); } From bcfd7251edeeaf2ef3e9dae1c0e312fd6882f324 Mon Sep 17 00:00:00 2001 From: Jaroslav Iha Date: Tue, 20 Jan 2026 11:40:27 +0100 Subject: [PATCH 100/114] final cleanup of add draft G4ssm feature --- .../substance-ssg4m-form.component.ts | 457 ++++++++---------- 1 file changed, 209 insertions(+), 248 deletions(-) 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 04be6ba0c..c1278e262 100644 --- a/src/app/core/substance-ssg4m/substance-ssg4m-form.component.ts +++ b/src/app/core/substance-ssg4m/substance-ssg4m-form.component.ts @@ -1390,13 +1390,10 @@ export class SubstanceSsg4ManufactureFormComponent document.body.appendChild(downloadLink); downloadLink.click(); document.body.removeChild(downloadLink); - // const commaIndex = dataUrl.indexOf(','); - // document.body.removeChild(container); - // return dataUrl.slice(commaIndex + 1); } async submit(): Promise { - await this.expandStepView(); + !this.configService.configData.isPfdaVersion && await this.expandStepView(); this.isLoading = true; this.loadingService.setLoading(true); this.approving = false; @@ -1404,294 +1401,258 @@ export class SubstanceSsg4ManufactureFormComponent 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 = async (svg) => { - // window["schemeUtil"].onFinishedLayout = (svg) => { }; - - // if New Record, initialize object + // Initialize pathway object if new record if (this.ssg4mSyntheticPathway == null) { this.ssg4mSyntheticPathway = {}; } - // Save SVG as Clob - // this.ssg4mSyntheticPathway.sbmsnImage = document.querySelector("#scheme-viz-view").innerHTML; + // Process and validate local drafts referenced by this form + const hasValidationErrors = await this.processLocalDrafts(jsonValue); + if (hasValidationErrors) { + return; + } + + 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); - // await this.exportStepView(document); - // const encodedSvg = await this.exportStepView(document) - // this.ssg4mSyntheticPathway.stepViewImage = decodeURIComponent(encodedSvg); + // Export step view as SVG; Disabled for PFDA + !this.configService.configData.isPfdaVersion && await this.exportStepView(document); - // Before saving, find any local drafts that are referenced by this form and validate/save them first. - const jsonStr = JSON.stringify(this.json || {}); + // Prepare final JSON and call save endpoint + jsonValue = this.prepareFinalJson(); + this.ssg4mSyntheticPathway.sbmsnDataText = jsonValue; + + this.savePathway(); + } + + // 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-") ); - const draftErrors: Array = []; - // const savedUuidMap: { [oldUuid: string]: string } = {}; - // Ensure validationMessages is an array before concatenating into it this.validationMessages = []; for (const key of draftKeys) { - try { - const entry = JSON.parse(localStorage.getItem(key)); - console.log( - "Processing draft entry from localStorage key: " + - JSON.stringify(entry) - ); - if (!entry || !entry.substance) { - continue; - } - const draftSub = entry.substance; - - // If this draft's structure ID appears in the current form JSON, treat it as included and process it - const tmpStructureId = - draftSub.$$tmpStructureId || entry.$$tmpStructureId || null; - if ( - (tmpStructureId && - jsonStr.indexOf('"' + tmpStructureId + '"') === -1) || - !tmpStructureId - ) { - // not referenced - continue; - } + await this.processSingleDraft(key, jsonStr); + } - // Validate draft - // validation may return ValidationResults or throw error - // Use take(1) on observable - // eslint-disable-next-line @typescript-eslint/no-shadow - const validationResult: any = await new Promise((resolve) => { - this.substanceService - .validateSubstance(draftSub) - .pipe(take(1)) - .subscribe( - (res) => { - resolve(res); - }, - (err) => { - resolve({ error: err }); - } - ); - }); + if (this.validationMessages && this.validationMessages.length > 0) { + this.loadingService.setLoading(false); + this.isLoading = false; + this.validationResult = false; + this.showSubmissionMessages = true; + return true; + } - if ( - validationResult && - validationResult.validationMessages && - validationResult.validationMessages.length > 0 - ) { - const filteredValidations = validationResult.validationMessages - .filter( - (message) => - message.messageType.toUpperCase() === "ERROR" || - message.messageType.toUpperCase() === "WARNING" - ) - .map((msg) => { - // prepend draft name/identifier to message - msg.message = - 'Draft "' + - (entry.name || draftSub.names?.[0]?.name || entry.key) + - '": ' + - msg.message; - return msg; - }); - this.validationMessages = [ - ...this.validationMessages, - ...filteredValidations, - ]; + return false; + } - // skip save for this draft if there are validation errors/warnings - if (filteredValidations.length > 0) { - continue; - } - } + // 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 (draftSub && draftSub.$$tmpStructureId) { - delete draftSub.$$tmpStructureId; - } - // Submit (save) the draft - const saveResult: any = await new Promise((resolve) => { - this.substanceService - .saveSubstance(draftSub) - .pipe(take(1)) - .subscribe( - (resp) => resolve(resp), - (err) => resolve({ error: err }) - ); - }); + if (!entry || !entry.substance) { + return; + } - // if (saveResult && saveResult.error) { - // this.submissionMessage = "There was a problem with your submission"; - // this.addServerError(saveResult.error.serverError); - // draftErrors.push( - // 'Draft "' + - // (entry.name || draftSub.uuid) + - // '" could not be saved: ' + - // (saveResult.error.message || JSON.stringify(saveResult.error)) - // ); - // } // else if (saveResult && saveResult.uuid) { - // // record mapping from draft uuid to newly saved uuid - // try { - // savedUuidMap[draftSub.uuid] = saveResult.uuid; - // } catch (e) {} - // } - } catch (e) { - // ignore malformed localStorage entries but record error - this.addServerError(e.serverError); - // draftErrors.push("Error processing draft " + key + ": " + e.message); + 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); } + } - if (this.validationMessages && this.validationMessages.length > 0) { - // Stop spinner and display errors similar to other submission failures - this.loadingService.setLoading(false); - this.isLoading = false; + // 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; - this.validationResult = false; - this.showSubmissionMessages = true; - return; + if (!tmpStructureId) { + return false; } - // If we saved any drafts, replace their refuuids in the form JSON with the returned uuids - // const savedKeys = Object.keys(savedUuidMap || {}); - // if (savedKeys && savedKeys.length > 0) { - // try { - // let updatedJson = jsonValue; - // savedKeys.forEach((oldUuid) => { - // const newUuid = savedUuidMap[oldUuid]; - // if (oldUuid && newUuid) { - // // replace occurrences of the old uuid (as JSON string) with the new uuid - // updatedJson = updatedJson - // .split('"' + oldUuid + '"') - // .join('"' + newUuid + '"'); - // } - // }); - // // update in-memory json and the payload stored on ssg4mSyntheticPathway - // this.json = JSON.parse(updatedJson); - // jsonValue = updatedJson; - // if (this.ssg4mSyntheticPathway == null) { - // this.ssg4mSyntheticPathway = {}; - // } - // this.ssg4mSyntheticPathway.sbmsnDataText = jsonValue; - // } catch (e) { - // // ignore replace errors but continue - // } - // } + return jsonStr.indexOf('"' + tmpStructureId + '"') !== -1; + } - // After submitting Save button, the UI waits for 8 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."; - } - }, 8000); + // 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; + }); + } - await this.exportStepView(document); + 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 }) + ); + }); + } - // Existing Record - // get the JSON from the SSG4m Form and store as a Clob into the database - // Remove any $$tmpStructureId markers from the in-memory JSON before saving + private prepareFinalJson(): string { try { if (this.json) { this.removeTmpStructureIdFields(this.json); - jsonValue = JSON.stringify(this.json); + return JSON.stringify(this.json); } - } catch (e) {} - this.ssg4mSyntheticPathway.sbmsnDataText = jsonValue; + } catch (e) { + // Ignore errors and return current JSON + } + return JSON.stringify(this.json); + } + // Saves the SSG4m synthetic pathway to the backend. + private savePathway(): void { this.submitSubscription = this.substanceSsg4mService .saveSsg4m(this.ssg4mSyntheticPathway) .pipe(take(1)) .subscribe( - (response) => { - // Stop the spinner - this.loadingService.setLoading(false); - this.isLoading = false; + (response) => this.handleSaveSuccess(response), + (error: SubstanceFormResults) => this.handleSaveError(error) + ); + this.subscriptions.push(this.submitSubscription); + } - // Set validation messages to null - this.validationMessages = null; - this.showSubmissionMessages = false; - this.validationResult = false; + private handleSaveSuccess(response: any): void { + this.loadingService.setLoading(false); + this.isLoading = false; + this.validationMessages = null; + this.showSubmissionMessages = false; + this.validationResult = false; - // if Saved Successfully - if ( - response && - (response.synthPathwaySkey || - this.configService.configData.isPfdaVersion) - ) { - if (response.synthPathwaySkey) { - this.id = response.synthPathwaySkey.toString(); - } + const isSuccessful = + response && + (response.synthPathwaySkey || this.configService.configData.isPfdaVersion); - // 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( - undefined, - this.configService.configData.isPfdaVersion - ? response.fileUrl - : null - ); - } - // 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 (!isSuccessful) { + return; + } - 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); - } - } - ); - this.subscriptions.push(this.submitSubscription); + if (response.synthPathwaySkey) { + this.id = response.synthPathwaySkey.toString(); + } - // }; //window + this.isSavedSuccessful = true; - // let tempCallback = window["schemeUtil"].onFinishedLayout; - // window["schemeUtil"].onFinishedLayout = (s)=>{ - // window["schemeUtil"].onFinishedLayout =(ss)=>{}; + // 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."; - // setTimeout(tempCallback(s),3000); - // }; + this.showSubmissionMessages = true; + this.submissionMessage = null; - // window['schemeUtil'].renderScheme(window['schemeUtil'].makeDisplayGraph(JSON.parse(ssgjs)), "#scheme-viz-view"); + 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() { From b64700a915b371e9c9f77d95196b8e1654e5f684 Mon Sep 17 00:00:00 2001 From: Jaroslav Iha Date: Tue, 20 Jan 2026 12:32:42 +0100 Subject: [PATCH 101/114] add G4 to side bar; remove tmpstructure id when saving draft --- src/app/core/home/home.component.html | 5 +++++ src/app/core/substance/substance.service.ts | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/src/app/core/home/home.component.html b/src/app/core/home/home.component.html index b8d9e65fa..16391b62a 100644 --- a/src/app/core/home/home.component.html +++ b/src/app/core/home/home.component.html @@ -156,6 +156,11 @@ Specified Substance Group 1 + + + + G4 Specified Substance Manufacturing + diff --git a/src/app/core/substance/substance.service.ts b/src/app/core/substance/substance.service.ts index 0d0ae7fd9..6414c9137 100644 --- a/src/app/core/substance/substance.service.ts +++ b/src/app/core/substance/substance.service.ts @@ -777,6 +777,11 @@ 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 }; From 33a21544c804478020dc383cb6b9936868d43f46 Mon Sep 17 00:00:00 2001 From: Jaroslav Iha Date: Tue, 20 Jan 2026 12:40:30 +0100 Subject: [PATCH 102/114] clean add draft feature code --- .../ssg4m-stages/ssg4m-stages-form.component.ts | 4 ---- 1 file changed, 4 deletions(-) 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 6c4de47c5..5c888f9e8 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 @@ -287,7 +287,6 @@ export class Ssg4mStagesFormComponent implements OnInit, OnDestroy { // dialog may return either an index (number) or the draft object. let draftObj: any = null; if (typeof response === "number") { - // try to read from the dialog component instance const comp = dialogRef.componentInstance as any; if (comp) { if (comp.filtered && comp.filtered[response]) { @@ -307,9 +306,6 @@ export class Ssg4mStagesFormComponent implements OnInit, OnDestroy { } const substanceObj = draftObj.substance || draftObj; - console.log( - "Adding draft starting material: " + JSON.stringify(substanceObj) - ); // Add a new starting material and populate basic fields from the draft this.substanceFormSsg4mStagesService.addStartingMaterials( From 8238f73833623c8786ceae20e74c65b3ea484b8a Mon Sep 17 00:00:00 2001 From: Jaroslav Iha Date: Fri, 23 Jan 2026 14:45:21 +0100 Subject: [PATCH 103/114] enable svg for pFDA --- .../core/substance-ssg4m/substance-ssg4m-form.component.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 c1278e262..ed38888f8 100644 --- a/src/app/core/substance-ssg4m/substance-ssg4m-form.component.ts +++ b/src/app/core/substance-ssg4m/substance-ssg4m-form.component.ts @@ -1393,7 +1393,7 @@ export class SubstanceSsg4ManufactureFormComponent } async submit(): Promise { - !this.configService.configData.isPfdaVersion && await this.expandStepView(); + await this.expandStepView(); this.isLoading = true; this.loadingService.setLoading(true); this.approving = false; @@ -1420,7 +1420,7 @@ export class SubstanceSsg4ManufactureFormComponent }, 8000); // Export step view as SVG; Disabled for PFDA - !this.configService.configData.isPfdaVersion && await this.exportStepView(document); + await this.exportStepView(document); // Prepare final JSON and call save endpoint jsonValue = this.prepareFinalJson(); From d07e91e8d8f12090ca47c79f21862190be5c93c6 Mon Sep 17 00:00:00 2001 From: Jaroslav Iha Date: Tue, 10 Feb 2026 11:19:51 +0100 Subject: [PATCH 104/114] address requested changes --- .../structure-image-modal.component.html | 2 +- .../structure-image-modal.component.ts | 3 + .../json-dialog/json-dialog.component.ts | 10 +- .../substance-selector.component.html | 17 +- .../substance-selector.component.ts | 147 +++++++++++++++++- ...m-processing-materials-form.component.html | 16 +- ...g4m-processing-materials-form.component.ts | 25 +++ ...4m-resulting-materials-form.component.html | 16 +- ...sg4m-resulting-materials-form.component.ts | 25 +++ .../ssg4m-scheme-view.component.html | 20 +-- .../ssg4m-stages-form.component.html | 6 - .../ssg4m-stages-form.component.ts | 140 +---------------- ...g4m-starting-materials-form.component.html | 11 +- ...ssg4m-starting-materials-form.component.ts | 27 ++++ .../substance-ssg4m-form.component.ts | 10 +- 15 files changed, 289 insertions(+), 186 deletions(-) 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 2506b0278..f5722c443 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 @@ -24,6 +24,7 @@ export class StructureImageModalComponent implements OnInit { showSubstanceSelector = false; gsrsHomeBaseUrl = ''; inchiNote = false; + isDraft = false; constructor( private configService: ConfigService, @@ -78,6 +79,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-form/json-dialog/json-dialog.component.ts b/src/app/core/substance-form/json-dialog/json-dialog.component.ts index 14f0f61b2..50f7704e4 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 @@ -23,9 +23,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-selector/substance-selector.component.html b/src/app/core/substance-selector/substance-selector.component.html index 4f7194ed8..6f59e6786 100644 --- a/src/app/core/substance-selector/substance-selector.component.html +++ b/src/app/core/substance-selector/substance-selector.component.html @@ -31,12 +31,19 @@ {{selectedSubstance.approvalID}} + + Draft: {{selectedSubstance._name}} +
- + + + +
NO SUBSTANCES FOUND
diff --git a/src/app/core/substance-selector/substance-selector.component.ts b/src/app/core/substance-selector/substance-selector.component.ts index 601023da7..180732700 100644 --- a/src/app/core/substance-selector/substance-selector.component.ts +++ b/src/app/core/substance-selector/substance-selector.component.ts @@ -1,14 +1,15 @@ import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core'; import { SubstanceService } from '../substance/substance.service'; -import { SubstanceSummary } from '../substance/substance.model'; +import { SubstanceSummary, SubstanceDetail } from '../substance/substance.model'; import { ConfigService } from '@gsrs-core/config'; import { MatDialog } from '@angular/material/dialog'; import { OverlayContainer } from '@angular/cdk/overlay'; import { AdvancedSelectorDialogComponent } from '@gsrs-core/substance-selector/advanced-selector-dialog/advanced-selector-dialog.component'; -import { StructureImageModalComponent } from '@gsrs-core/structure'; +import { StructureImageModalComponent, StructureService } from '@gsrs-core/structure'; import { SubstanceFormService } from '@gsrs-core/substance-form/substance-form.service'; import { Router } from '@angular/router'; import { ScrollToService } from '@gsrs-core/scroll-to/scroll-to.service'; +import { SubstanceDraftsComponent } from '@gsrs-core/substance-form/substance-drafts/substance-drafts.component'; @Component({ selector: 'app-substance-selector', @@ -19,12 +20,14 @@ export class SubstanceSelectorComponent implements OnInit { selectedSubstance?: SubstanceSummary; @Input() eventCategory: string; @Output() selectionUpdated = new EventEmitter(); + @Output() draftSelected = new EventEmitter(); @Input() placeholder = 'Search'; @Input() hintMessage = ''; @Input() header = 'Substance'; @Input() name?: string; @Input() hideImage?: boolean; @Input() showMorelinks? = false; + @Input() showDraftOption? = false; errorMessage: string; showOptions: boolean; previousSubstance: SubstanceSummary; @@ -50,7 +53,8 @@ export class SubstanceSelectorComponent implements OnInit { private overlayContainerService: OverlayContainer, public scrollToService: ScrollToService, private dialog: MatDialog, - private router: Router + private router: Router, + private structureService: StructureService ) { } StoreSelection() { @@ -156,21 +160,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' + 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; @@ -242,4 +252,129 @@ 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-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 2723ccc7b..0076d4f33 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 @@ -23,10 +23,18 @@
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 cd005c945..0616381f3 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 @@ -163,12 +163,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: '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 54a44a98a..b12c91874 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 @@ -29,10 +29,18 @@
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 590cc6450..ce1efa88c 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 @@ -158,12 +158,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: '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 34e0d49c7..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-stages/ssg4m-stages-form.component.html b/src/app/core/substance-ssg4m/ssg4m-stages/ssg4m-stages-form.component.html index 7cfcf7812..b6b891b16 100644 --- a/src/app/core/substance-ssg4m/ssg4m-stages/ssg4m-stages-form.component.html +++ b/src/app/core/substance-ssg4m/ssg4m-stages/ssg4m-stages-form.component.html @@ -100,12 +100,6 @@ (click)="addStartingMaterial(processIndex, siteIndex, stageIndex)"> Add Input Material - -
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 5c888f9e8..f52ad6138 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 @@ -34,10 +34,8 @@ import { } from "../../substance/substance.model"; import { SubstanceDetail } from "@gsrs-core/substance/substance.model"; import { SubstanceFormSsg4mStagesService } from "./substance-form-ssg4m-stages.service"; -import { StructureService } from "@gsrs-core/structure/structure.service"; import { SpecifiedSubstanceG4mStage } from "@gsrs-core/substance/substance.model"; import { ConfirmDialogComponent } from "../../../fda/confirm-dialog/confirm-dialog.component"; -import { SubstanceDraftsComponent } from "@gsrs-core/substance-form/substance-drafts/substance-drafts.component"; @Component({ selector: "app-ssg4m-stages-form", @@ -65,8 +63,7 @@ export class Ssg4mStagesFormComponent implements OnInit, OnDestroy { private overlayContainerService: OverlayContainer, private scrollToService: ScrollToService, public configService: ConfigService, - private dialog: MatDialog, - private structureService: StructureService + private dialog: MatDialog ) {} @Input() @@ -263,141 +260,6 @@ export class Ssg4mStagesFormComponent implements OnInit, OnDestroy { }); } - addDraft(processIndex: number, siteIndex: number, stageIndex: number) { - const dialogRef = this.dialog.open(SubstanceDraftsComponent, { - maxHeight: "85%", - width: "70%", - data: { uuid: this.substance && this.substance.uuid }, - }); - - const overlayContainer = this.overlayContainerService.getContainerElement(); - if (overlayContainer) { - overlayContainer.style.zIndex = "1002"; - } - - dialogRef.afterClosed().subscribe((response) => { - if (overlayContainer) { - 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 = draftObj.substance || draftObj; - - // Add a new starting material and populate basic fields from the draft - this.substanceFormSsg4mStagesService.addStartingMaterials( - processIndex, - siteIndex, - stageIndex - ); - - // Locate the newly added starting material (last in the list) - const startList = - this.substance.specifiedSubstanceG4m.process[processIndex].sites[ - siteIndex - ].stages[stageIndex].startingMaterials; - if (!startList || startList.length === 0) { - return; - } - const idx = startList.length - 1; - const newStart = startList[idx]; - - // 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; - - newStart.substanceName = { - refPname: primaryName, - name: primaryName, - refuuid: substanceObj.uuid, - substanceClass: "reference", - approvalID: substanceObj.approvalID, - }; - newStart.verbatimName = primaryName; - - // 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. - try { - 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( - (response) => { - if (response && response.structure && response.structure.id) { - // store a temp id used by the image directive - (newStart as any).$$tmpStructureId = response.structure.id; - // also attach the temp id to the original draft object so it can be referenced later - try { - if (draftObj) { - if (draftObj.substance) { - draftObj.substance.$$tmpStructureId = response.structure.id; - } else { - (draftObj as any).$$tmpStructureId = response.structure.id; - } - if ((draftObj as any).file) { - try { - localStorage.setItem((draftObj as any).file, JSON.stringify(draftObj)); - } catch (e) { - // ignore storage errors - } - } - } - } catch (e) { - // ignore any errors updating draft - } - } - }, - (error) => { - // ignore failures to interpret - } - ); - } - } catch (e) { - // swallow any unexpected errors - } - - // Scroll to newly added starting material entry - setTimeout(() => { - this.scrollToService.scrollToElement( - `substance-process-site-stage-startMat-0`, - "center" - ); - }); - }); - } - addProcessingMaterial( processIndex: number, siteIndex: number, 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 c9c199339..bb5a12173 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 @@ -18,17 +18,18 @@ 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 2401cd71a..faed5a6a0 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 @@ -177,11 +177,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: "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.ts b/src/app/core/substance-ssg4m/substance-ssg4m-form.component.ts index ed38888f8..7e8c19c29 100644 --- a/src/app/core/substance-ssg4m/substance-ssg4m-form.component.ts +++ b/src/app/core/substance-ssg4m/substance-ssg4m-form.component.ts @@ -648,8 +648,11 @@ export class SubstanceSsg4ManufactureFormComponent } showJSON(): void { + const json = this.substanceFormService.cleanSubstance(); + this.removeTmpStructureIdFields(json); const dialogRef = this.dialog.open(JsonDialogComponent, { width: "90%", + data: { substance: json } }); this.overlayContainer.style.zIndex = "1002"; @@ -663,7 +666,8 @@ export class SubstanceSsg4ManufactureFormComponent 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); + // 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)) @@ -1393,7 +1397,7 @@ export class SubstanceSsg4ManufactureFormComponent } async submit(): Promise { - await this.expandStepView(); + !this.configService.configData.isPfdaVersion && await this.expandStepView(); this.isLoading = true; this.loadingService.setLoading(true); this.approving = false; @@ -1420,7 +1424,7 @@ export class SubstanceSsg4ManufactureFormComponent }, 8000); // Export step view as SVG; Disabled for PFDA - await this.exportStepView(document); + !this.configService.configData.isPfdaVersion && await this.exportStepView(document); // Prepare final JSON and call save endpoint jsonValue = this.prepareFinalJson(); From 3de37f88e0379a8f5fd4b275761b10506ddab7b1 Mon Sep 17 00:00:00 2001 From: Jaroslav Iha Date: Tue, 10 Feb 2026 14:38:21 +0100 Subject: [PATCH 105/114] enable svg generation on pfda for demo --- .../core/substance-ssg4m/substance-ssg4m-form.component.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 7e8c19c29..cb2bfcad0 100644 --- a/src/app/core/substance-ssg4m/substance-ssg4m-form.component.ts +++ b/src/app/core/substance-ssg4m/substance-ssg4m-form.component.ts @@ -1397,7 +1397,7 @@ export class SubstanceSsg4ManufactureFormComponent } async submit(): Promise { - !this.configService.configData.isPfdaVersion && await this.expandStepView(); + await this.expandStepView(); this.isLoading = true; this.loadingService.setLoading(true); this.approving = false; @@ -1424,7 +1424,7 @@ export class SubstanceSsg4ManufactureFormComponent }, 8000); // Export step view as SVG; Disabled for PFDA - !this.configService.configData.isPfdaVersion && await this.exportStepView(document); + await this.exportStepView(document); // Prepare final JSON and call save endpoint jsonValue = this.prepareFinalJson(); From dad1077754765b57acd93445c3d9ef75c0ec5186 Mon Sep 17 00:00:00 2001 From: Jaroslav Iha Date: Tue, 17 Feb 2026 12:59:25 +0100 Subject: [PATCH 106/114] add: generate uuid for drafts; save local batch to G4SSM --- .../substance-drafts.component.html | 2 +- .../substance-drafts.component.ts | 11 +- .../substance-form.component.ts | 7 +- ...g4m-processing-materials-form.component.ts | 2 +- ...sg4m-resulting-materials-form.component.ts | 2 +- ...ssg4m-starting-materials-form.component.ts | 2 +- .../substance-ssg4m-form.component.html | 3 + .../substance-ssg4m-form.component.ts | 175 +++++++++++++----- 8 files changed, 145 insertions(+), 59 deletions(-) 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 3c64aa446..6ab11ac32 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 @@ -22,7 +22,7 @@ Select - + 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 0a567866a..95f642cc5 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 @@ -105,11 +105,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; } @@ -372,12 +373,6 @@ export class SubstanceDraftsComponent implements OnInit { return b.date - a.date; }); this.selectedKeys = []; - - if (this.json && this.json.uuid) { - this.filterToggle('substance'); - } else { - this.filterToggle('register'); - } } diff --git a/src/app/core/substance-form/substance-form.component.ts b/src/app/core/substance-form/substance-form.component.ts index fb32ea43c..27209fa08 100644 --- a/src/app/core/substance-form/substance-form.component.ts +++ b/src/app/core/substance-form/substance-form.component.ts @@ -201,7 +201,7 @@ export class SubstanceFormComponent implements OnInit, AfterViewInit, OnDestroy this.isLoading = false; this.overlayContainer.style.zIndex = null; }, 1000); - } else if (response.uuid && response.uuid != 'register') { + } else if (this.id && response.uuid && response.uuid != 'register') { const url = '/substances/' + response.uuid + '/edit?action=import&source=draft'; this.router.navigateByUrl(url, {state: {record: response.substance}}); } else { @@ -1404,6 +1404,11 @@ export class SubstanceFormComponent implements OnInit, AfterViewInit, OnDestroy const json = this.substanceFormService.cleanSubstance(); const time = new Date().getTime(); + if (!json.uuid) { + this.substanceFormService.regenUUID(); + json.uuid = this.substanceFormService.cleanSubstance().uuid; + } + const uuid = json.uuid ? json.uuid : 'register'; const type = json.substanceClass; let primary = null; 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 0616381f3..ff12714da 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 @@ -182,7 +182,7 @@ export class Ssg4mProcessingMaterialsFormComponent implements OnInit, OnDestroy this.privateProcessingMaterial.substanceName = { refPname: primaryName, name: primaryName, - refuuid: 'draft', + refuuid: substanceObj?.uuid || "draft", substanceClass: 'mention', approvalID: substanceObj?.approvalID }; 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 ce1efa88c..3a1c41d52 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 @@ -177,7 +177,7 @@ export class Ssg4mResultingMaterialsFormComponent implements OnInit, OnDestroy { this.privateResultingMaterial.substanceName = { refPname: primaryName, name: primaryName, - refuuid: 'draft', + refuuid: substanceObj?.uuid || "draft", substanceClass: 'mention', approvalID: substanceObj?.approvalID }; 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 faed5a6a0..e919dbc49 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 @@ -197,7 +197,7 @@ export class Ssg4mStartingMaterialsFormComponent implements OnInit, OnDestroy { this.privateStartingMaterial.substanceName = { refPname: primaryName, name: primaryName, - refuuid: "draft", + refuuid: substanceObj?.uuid || "draft", substanceClass: "mention", approvalID: substanceObj?.approvalID }; 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 09715caec..23b24d011 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 + 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 cb2bfcad0..eaed6e385 100644 --- a/src/app/core/substance-ssg4m/substance-ssg4m-form.component.ts +++ b/src/app/core/substance-ssg4m/substance-ssg4m-form.component.ts @@ -150,7 +150,7 @@ export class SubstanceSsg4ManufactureFormComponent private authService: AuthService, private titleService: Title, private configService: ConfigService, - private sanitizer: DomSanitizer + private sanitizer: DomSanitizer, ) {} ngOnInit() { @@ -180,7 +180,7 @@ export class SubstanceSsg4ManufactureFormComponent this.id = id; this.gaService.sendPageView(`Substance Edit`); this.titleService.setTitle( - "Edit - Specified Substance Group 4 Manufacturing" + "Edit - Specified Substance Group 4 Manufacturing", ); const newType = this.activatedRoute.snapshot.queryParamMap.get("switch") || null; @@ -210,14 +210,14 @@ export class SubstanceSsg4ManufactureFormComponent this.dynamicComponents.forEach((cRef, index) => { this.dynamicComponentLoader .getComponentFactory( - this.formSections[index].dynamicComponentName + this.formSections[index].dynamicComponentName, ) .subscribe((componentFactory) => { this.formSections[index].dynamicComponentRef = cRef.createComponent(componentFactory); this.formSections[index].matExpansionPanel = this.matExpansionPanels.find( - (item, panelIndex) => index === panelIndex + (item, panelIndex) => index === panelIndex, ); this.formSections[ index @@ -231,7 +231,7 @@ export class SubstanceSsg4ManufactureFormComponent ].dynamicComponentRef.instance.hiddenStateUpdate.subscribe( (isHidden) => { this.formSections[index].isHidden = isHidden; - } + }, ); this.subscriptions.push(hiddenStateSubscription); this.formSections[ @@ -292,7 +292,7 @@ export class SubstanceSsg4ManufactureFormComponent "specifiedSubstanceG4m"; this.substanceClass = this.subClass; this.titleService.setTitle( - "Register - Specified Substance Group 4 Manufacturing" + "Register - Specified Substance Group 4 Manufacturing", ); this.substanceFormService .loadSubstance(this.substanceClass) @@ -311,7 +311,7 @@ export class SubstanceSsg4ManufactureFormComponent if (event instanceof NavigationStart) { this.substanceSsg4mService.unloadSubstance(); } - } + }, ); this.subscriptions.push(routerSubscription); this.approving = false; @@ -354,7 +354,7 @@ export class SubstanceSsg4ManufactureFormComponent this.dynamicComponents.forEach((cRef, index) => { this.dynamicComponentLoader .getComponentFactory( - this.formSections[index].dynamicComponentName + this.formSections[index].dynamicComponentName, ) .subscribe((componentFactory) => { this.loadingService.setLoading(true); @@ -362,7 +362,7 @@ export class SubstanceSsg4ManufactureFormComponent cRef.createComponent(componentFactory); this.formSections[index].matExpansionPanel = this.matExpansionPanels.find( - (item, panelIndex) => index === panelIndex + (item, panelIndex) => index === panelIndex, ); this.formSections[ index @@ -376,7 +376,7 @@ export class SubstanceSsg4ManufactureFormComponent ].dynamicComponentRef.instance.hiddenStateUpdate.subscribe( (isHidden) => { this.formSections[index].isHidden = isHidden; - } + }, ); this.subscriptions.push(hiddenStateSubscription); this.formSections[ @@ -414,7 +414,7 @@ export class SubstanceSsg4ManufactureFormComponent if (currentTime.getTime() - startTime.getTime() > 12000) { if ( confirm( - "There was a network error while fetching files, would you like to refresh?" + "There was a network error while fetching files, would you like to refresh?", ) ) { window.location.reload(); @@ -604,14 +604,14 @@ export class SubstanceSsg4ManufactureFormComponent this.id + "/edit?action=import&header=" + this.showHeaderBar, - { state: { record: response } } + { state: { record: response } }, ); } else { // new record this.router.navigateByUrl( "/substances-ssg4m/register?action=import&header=" + this.showHeaderBar, - { state: { record: response } } + { state: { record: response } }, ); } }, 1000); @@ -652,7 +652,7 @@ export class SubstanceSsg4ManufactureFormComponent this.removeTmpStructureIdFields(json); const dialogRef = this.dialog.open(JsonDialogComponent, { width: "90%", - data: { substance: json } + data: { substance: json }, }); this.overlayContainer.style.zIndex = "1002"; @@ -670,7 +670,7 @@ export class SubstanceSsg4ManufactureFormComponent this.removeTmpStructureIdFields(this.json); const uri = this.sanitizer.bypassSecurityTrustUrl( "data:text/json;charset=UTF-8," + - encodeURIComponent(JSON.stringify(this.json)) + encodeURIComponent(JSON.stringify(this.json)), ); this.downloadJsonHref = uri; @@ -678,6 +678,75 @@ export class SubstanceSsg4ManufactureFormComponent this.jsonFileName = "SSG4m_" + moment(date).format("MMM-DD-YYYY_H-mm-ss"); } + 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; + } + 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); + } + } + } + } + } + } + + // 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 @@ -707,7 +776,7 @@ export class SubstanceSsg4ManufactureFormComponent console.log("Error Status is 0"); let totalNumberRefresh = 2; let preventRefresh = parseInt( - new URLSearchParams(window.location.search).get("refreshcount") + 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 @@ -786,7 +855,7 @@ export class SubstanceSsg4ManufactureFormComponent 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"; } - } + }, ); } @@ -837,7 +906,7 @@ export class SubstanceSsg4ManufactureFormComponent this.substanceFormService .loadSubstance( substanceSsg4mFromDb.substanceClass, - substanceSsg4mFromDb + substanceSsg4mFromDb, ) .pipe(take(1)) .subscribe(() => { @@ -868,7 +937,7 @@ export class SubstanceSsg4ManufactureFormComponent this.loadingService.setLoading(false); this.isLoading = false; // this.handleSubstanceRetrivalError(); - } + }, ); } @@ -901,14 +970,14 @@ export class SubstanceSsg4ManufactureFormComponent this.dynamicComponents.forEach((cRef, index) => { this.dynamicComponentLoader .getComponentFactory( - this.formSections[index].dynamicComponentName + this.formSections[index].dynamicComponentName, ) .subscribe((componentFactory) => { this.formSections[index].dynamicComponentRef = cRef.createComponent(componentFactory); this.formSections[index].matExpansionPanel = this.matExpansionPanels.find( - (item, panelIndex) => index === panelIndex + (item, panelIndex) => index === panelIndex, ); this.formSections[ index @@ -922,7 +991,7 @@ export class SubstanceSsg4ManufactureFormComponent ].dynamicComponentRef.instance.hiddenStateUpdate.subscribe( (isHidden) => { this.formSections[index].isHidden = isHidden; - } + }, ); this.subscriptions.push(hiddenStateSubscription); this.formSections[ @@ -961,7 +1030,7 @@ export class SubstanceSsg4ManufactureFormComponent }, (error) => { this.loadingService.setLoading(false); - } + }, ); } else { this.handleSubstanceRetrivalError(); @@ -999,12 +1068,12 @@ export class SubstanceSsg4ManufactureFormComponent }, (error) => { this.gaService.sendException( - "getSubstanceDetails: error from API call" + "getSubstanceDetails: error from API call", ); this.loadingService.setLoading(false); this.isLoading = false; this.handleSubstanceRetrivalError(); - } + }, ); } @@ -1231,7 +1300,7 @@ export class SubstanceSsg4ManufactureFormComponent (error) => { observer.error(); observer.complete(); - } + }, ); }); } @@ -1264,10 +1333,10 @@ export class SubstanceSsg4ManufactureFormComponent String(num).padStart(length, "0"); const datePart = `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad( - now.getDate() + now.getDate(), )}`; const timePart = `${pad(now.getHours())}${pad(now.getMinutes())}${pad( - now.getSeconds() + now.getSeconds(), )}`; return `${datePart}${timePart}`; @@ -1317,7 +1386,7 @@ export class SubstanceSsg4ManufactureFormComponent async expandStepView(): Promise { const tabProcesses = document.querySelector( - "#substance-form-ssg4m-process" + "#substance-form-ssg4m-process", ) as HTMLElement; if (tabProcesses.getAttribute("aria-expanded") !== "true") { console.log("Tab Processes not selected. Clicking it..."); @@ -1327,7 +1396,7 @@ export class SubstanceSsg4ManufactureFormComponent const allTabs = document.querySelectorAll(".mat-tab-label"); const tabStepView = Array.from(allTabs).find( - (tab) => tab.textContent.trim() === "Step View" + (tab) => tab.textContent.trim() === "Step View", ) as HTMLElement; if (tabStepView.getAttribute("aria-selected") !== "true") { console.log("Tab Step View not selected. Clicking it..."); @@ -1338,7 +1407,7 @@ export class SubstanceSsg4ManufactureFormComponent async exportStepView(document: Document): Promise { const elementToConvert = document.querySelector( - "app-ssg4m-scheme-view" + "app-ssg4m-scheme-view", ) as HTMLElement; const clone = elementToConvert.cloneNode(true) as HTMLElement; const initialWidth = elementToConvert.offsetWidth; @@ -1438,7 +1507,7 @@ export class SubstanceSsg4ManufactureFormComponent // 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-") + k.startsWith("gsrs-draft-"), ); this.validationMessages = []; @@ -1459,12 +1528,15 @@ export class SubstanceSsg4ManufactureFormComponent } // Processes a single draft from localStorage. - private async processSingleDraft(key: string, jsonStr: string): Promise { + 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) + JSON.stringify(entry), ); if (!entry || !entry.substance) { @@ -1477,7 +1549,10 @@ export class SubstanceSsg4ManufactureFormComponent return; } - const hasErrors = await this.validateAndCollectDraftErrors(draftSub, entry); + const hasErrors = await this.validateAndCollectDraftErrors( + draftSub, + entry, + ); if (hasErrors) { return; } @@ -1494,7 +1569,11 @@ export class SubstanceSsg4ManufactureFormComponent } // Checks if a draft's temporary structure ID is referenced in the current form JSON. - private isDraftReferencedInForm(draftSub: any, entry: any, jsonStr: string): boolean { + private isDraftReferencedInForm( + draftSub: any, + entry: any, + jsonStr: string, + ): boolean { const tmpStructureId = draftSub.$$tmpStructureId || entry.$$tmpStructureId || null; @@ -1507,14 +1586,17 @@ export class SubstanceSsg4ManufactureFormComponent // 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 { + 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 }) + (err) => resolve({ error: err }), ); }); @@ -1526,7 +1608,7 @@ export class SubstanceSsg4ManufactureFormComponent const filteredValidations = this.filterAndPrefixValidationMessages( validationResult.validationMessages, entry, - draftSub + draftSub, ); this.validationMessages = [ @@ -1543,13 +1625,13 @@ export class SubstanceSsg4ManufactureFormComponent private filterAndPrefixValidationMessages( messages: ValidationMessage[], entry: any, - draftSub: any + draftSub: any, ): ValidationMessage[] { return messages .filter( (message) => message.messageType.toUpperCase() === "ERROR" || - message.messageType.toUpperCase() === "WARNING" + message.messageType.toUpperCase() === "WARNING", ) .map((msg) => { msg.message = @@ -1568,7 +1650,7 @@ export class SubstanceSsg4ManufactureFormComponent .pipe(take(1)) .subscribe( (resp) => resolve(resp), - (err) => resolve({ error: err }) + (err) => resolve({ error: err }), ); }); } @@ -1592,7 +1674,7 @@ export class SubstanceSsg4ManufactureFormComponent .pipe(take(1)) .subscribe( (response) => this.handleSaveSuccess(response), - (error: SubstanceFormResults) => this.handleSaveError(error) + (error: SubstanceFormResults) => this.handleSaveError(error), ); this.subscriptions.push(this.submitSubscription); } @@ -1606,7 +1688,8 @@ export class SubstanceSsg4ManufactureFormComponent const isSuccessful = response && - (response.synthPathwaySkey || this.configService.configData.isPfdaVersion); + (response.synthPathwaySkey || + this.configService.configData.isPfdaVersion); if (!isSuccessful) { return; @@ -1627,7 +1710,7 @@ export class SubstanceSsg4ManufactureFormComponent this.isCancelBtnClicked = false; this.openSuccessDialog( undefined, - this.configService.configData.isPfdaVersion ? response.fileUrl : null + this.configService.configData.isPfdaVersion ? response.fileUrl : null, ); } } @@ -1646,7 +1729,7 @@ export class SubstanceSsg4ManufactureFormComponent this.validationMessages = error.validationMessages.filter( (message) => message.messageType.toUpperCase() === "ERROR" || - message.messageType.toUpperCase() === "WARNING" + message.messageType.toUpperCase() === "WARNING", ); this.showSubmissionMessages = true; } else { From f023b933079945efa5e1f4d2bb513d5e6ae11512 Mon Sep 17 00:00:00 2001 From: Jaroslav Iha Date: Tue, 24 Feb 2026 12:31:26 +0100 Subject: [PATCH 107/114] show no image for no structure --- .../substance/substance-image.directive.ts | 121 ++++++++++++------ 1 file changed, 81 insertions(+), 40 deletions(-) diff --git a/src/app/core/substance/substance-image.directive.ts b/src/app/core/substance/substance-image.directive.ts index 9ce0b932d..3cbbfaf9a 100644 --- a/src/app/core/substance/substance-image.directive.ts +++ b/src/app/core/substance/substance-image.directive.ts @@ -1,10 +1,10 @@ -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]' + selector: "[appSubstanceImage]", }) export class SubstanceImageDirective implements AfterViewInit { private privateEntityId: string; @@ -20,13 +20,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(); } @@ -79,12 +82,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 { @@ -92,55 +103,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(); + }, + ); } - } From b6a8df6bc1929d934b110fa96c50f57d1f122e4c Mon Sep 17 00:00:00 2001 From: Jaroslav Iha Date: Tue, 10 Mar 2026 17:41:12 +0100 Subject: [PATCH 108/114] fix counter; drafts uuid generation --- .../substance-form.component.ts | 20 ++++--------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/src/app/core/substance-form/substance-form.component.ts b/src/app/core/substance-form/substance-form.component.ts index 27209fa08..b8d097489 100644 --- a/src/app/core/substance-form/substance-form.component.ts +++ b/src/app/core/substance-form/substance-form.component.ts @@ -225,11 +225,7 @@ export class SubstanceFormComponent implements OnInit, AfterViewInit, OnDestroy 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); } @@ -512,13 +508,7 @@ export class SubstanceFormComponent implements OnInit, AfterViewInit, OnDestroy 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); } @@ -1404,10 +1394,8 @@ export class SubstanceFormComponent implements OnInit, AfterViewInit, OnDestroy const json = this.substanceFormService.cleanSubstance(); const time = new Date().getTime(); - if (!json.uuid) { - this.substanceFormService.regenUUID(); - json.uuid = this.substanceFormService.cleanSubstance().uuid; - } + this.substanceFormService.regenUUID(); + json.uuid = this.substanceFormService.cleanSubstance().uuid; const uuid = json.uuid ? json.uuid : 'register'; const type = json.substanceClass; From a15e1610d7937b64b868642395dd922251cf5280 Mon Sep 17 00:00:00 2001 From: Jaroslav Iha Date: Wed, 25 Mar 2026 19:28:57 +0100 Subject: [PATCH 109/114] resolve errors caused by merge conflicts --- src/app/core/auth/auth.service.ts | 24 ++++++++++++++++--- src/app/core/config/config.model.ts | 2 ++ .../substance-drafts.component.ts | 2 +- .../substance/substance-image.directive.ts | 3 ++- src/app/fda/fda.module.ts | 4 ---- 5 files changed, 26 insertions(+), 9 deletions(-) diff --git a/src/app/core/auth/auth.service.ts b/src/app/core/auth/auth.service.ts index 81e211e01..458ef42f2 100644 --- a/src/app/core/auth/auth.service.ts +++ b/src/app/core/auth/auth.service.ts @@ -1,8 +1,26 @@ import { Injectable, PLATFORM_ID, Inject } from "@angular/core"; import { ConfigService } from "../config/config.service"; -import { Auth, Role, UserGroup } from "./auth.model"; -import { Observable, Subject, of } from "rxjs"; -import { map, take, catchError } from "rxjs/operators"; +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 { diff --git a/src/app/core/config/config.model.ts b/src/app/core/config/config.model.ts index 3465d5629..5ccbfca61 100644 --- a/src/app/core/config/config.model.ts +++ b/src/app/core/config/config.model.ts @@ -39,6 +39,8 @@ export interface Config { facetDisplay?: Array; relationshipsVisualizationUri?: string; isPfdaVersion?: boolean; + customToolbarComponent?: string; + disableSessionRefresh?: boolean; sessionRefreshOnActiveUserOnly?: boolean; sessionExpirationWarning?: SessionExpirationWarning; disableReferenceDocumentUpload?: boolean; 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 6e801f898..c18ca1a02 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 @@ -208,7 +208,7 @@ export class SubstanceDraftsComponent implements OnInit { 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 || prop.monomerSubstance == {}) { + if (!prop.monomerSubstance || (typeof prop.monomerSubstance === 'object' && Object.keys(prop.monomerSubstance).length === 0)) { const invalidPropertyMessage: ValidationMessage = { actionType: 'frontEnd', appliedChange: false, diff --git a/src/app/core/substance/substance-image.directive.ts b/src/app/core/substance/substance-image.directive.ts index 5544f580f..be521abbf 100644 --- a/src/app/core/substance/substance-image.directive.ts +++ b/src/app/core/substance/substance-image.directive.ts @@ -4,7 +4,8 @@ import { HttpClient } from "@angular/common/http"; import { ConfigService } from "@gsrs-core/config/config.service"; @Directive({ - selector: "[appSubstanceImage]" + selector: "[appSubstanceImage]", + standalone: false, }) export class SubstanceImageDirective implements AfterViewInit { private privateEntityId: string; diff --git a/src/app/fda/fda.module.ts b/src/app/fda/fda.module.ts index 729c32632..660034a0d 100644 --- a/src/app/fda/fda.module.ts +++ b/src/app/fda/fda.module.ts @@ -93,10 +93,6 @@ const fdaRoutes: Routes = [ ShowApplicationToggleComponent ], exports: [], - entryComponents: [ - SubstanceCountsComponent, - ShowApplicationToggleComponent - ] }) export class FdaModule { constructor( From 66886f243cd86d4b8a048988a8d49d9069f39795 Mon Sep 17 00:00:00 2001 From: Jaroslav Iha Date: Wed, 1 Apr 2026 13:52:55 +0200 Subject: [PATCH 110/114] after merge fixes --- src/app/core/auth/auth.service.ts | 13 +++++++------ .../base/pfda-toolbar/pfda-toolbar.component.html | 1 + .../substance-form/substance-form.component.ts | 4 ---- .../substance-ssg2-form.component.ts | 14 ++------------ .../substance-ssg4m-form.component.ts | 2 +- 5 files changed, 11 insertions(+), 23 deletions(-) diff --git a/src/app/core/auth/auth.service.ts b/src/app/core/auth/auth.service.ts index 458ef42f2..e4c726792 100644 --- a/src/app/core/auth/auth.service.ts +++ b/src/app/core/auth/auth.service.ts @@ -233,11 +233,11 @@ export class AuthService { 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) { @@ -288,10 +288,11 @@ export class AuthService { 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) { 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 bf49c3f14..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,6 +42,7 @@ diff --git a/src/app/core/substance-form/substance-form.component.ts b/src/app/core/substance-form/substance-form.component.ts index 370490d15..a42ff68b6 100644 --- a/src/app/core/substance-form/substance-form.component.ts +++ b/src/app/core/substance-form/substance-form.component.ts @@ -98,8 +98,6 @@ export class SubstanceFormComponent implements OnInit, AfterViewInit, AfterViewC definition: SubstanceFormDefinition; user: string; feature: string; - isAdmin: boolean; - isUpdater: boolean; isPfdaVersion: boolean = false; canUpdate: boolean; canMakeAdvancedEdits: boolean; @@ -321,8 +319,6 @@ export class SubstanceFormComponent implements OnInit, AfterViewInit, AfterViewC if (this.configService.configData && this.configService.configData.useApprovalAPI) { this.useApprovalAPI = this.configService.configData.useApprovalAPI; } - this.isAdmin = this.authService.hasRoles('admin'); - this.isUpdater = this.authService.hasAnyRoles('Updater', 'SuperUpdater'); this.isPfdaVersion = this.configService.configData.isPfdaVersion; this.canUpdate = await this.authService.hasSpecificPrivilege("Edit"); this.canMakeAdvancedEdits = await this.authService.hasSpecificPrivilege("Edit Public Data"); 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/substance-ssg4m-form.component.ts b/src/app/core/substance-ssg4m/substance-ssg4m-form.component.ts index 9ab56172c..a030f5eac 100644 --- a/src/app/core/substance-ssg4m/substance-ssg4m-form.component.ts +++ b/src/app/core/substance-ssg4m/substance-ssg4m-form.component.ts @@ -1363,7 +1363,7 @@ export class SubstanceSsg4ManufactureFormComponent await this.delay(200); } - const allTabs = document.querySelectorAll(".mat-tab-label"); + const allTabs = document.querySelectorAll(".mat-mdc-tab"); const tabStepView = Array.from(allTabs).find( (tab) => tab.textContent.trim() === "Step View", ) as HTMLElement; From f4fb1cdf0464d3639ad3e46f9def7115a91b2ec8 Mon Sep 17 00:00:00 2001 From: Jaroslav Iha Date: Wed, 1 Apr 2026 14:03:12 +0200 Subject: [PATCH 111/114] configrable ssg4m svg generation --- src/app/core/config/config.model.ts | 1 + .../substance-ssg4m/substance-ssg4m-form.component.ts | 8 +++++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/app/core/config/config.model.ts b/src/app/core/config/config.model.ts index 5ccbfca61..7e82d51ea 100644 --- a/src/app/core/config/config.model.ts +++ b/src/app/core/config/config.model.ts @@ -40,6 +40,7 @@ export interface Config { relationshipsVisualizationUri?: string; isPfdaVersion?: boolean; customToolbarComponent?: string; + ssg4mExportSvg?: boolean; disableSessionRefresh?: boolean; sessionRefreshOnActiveUserOnly?: boolean; sessionExpirationWarning?: SessionExpirationWarning; 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 a030f5eac..1033c0c47 100644 --- a/src/app/core/substance-ssg4m/substance-ssg4m-form.component.ts +++ b/src/app/core/substance-ssg4m/substance-ssg4m-form.component.ts @@ -129,6 +129,7 @@ export class SubstanceSsg4ManufactureFormComponent configSsg4Form: any; configSettingReferences = false; private submitSubscription: any = null; + ssg4mExportSvg: boolean; private jsLibScriptUrls = [ `${environment.baseHref || ""}assets/pathway/cola.min.js`, @@ -165,6 +166,7 @@ export class SubstanceSsg4ManufactureFormComponent 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) { @@ -1435,7 +1437,7 @@ export class SubstanceSsg4ManufactureFormComponent } async submit(): Promise { - await this.expandStepView(); + this.ssg4mExportSvg && await this.expandStepView(); this.isLoading = true; this.loadingService.setLoading(true); this.approving = false; @@ -1461,8 +1463,8 @@ export class SubstanceSsg4ManufactureFormComponent } }, 8000); - // Export step view as SVG; Disabled for PFDA - await this.exportStepView(document); + // Export step view as SVG; default disabled + this.ssg4mExportSvg && await this.exportStepView(document); // Prepare final JSON and call save endpoint jsonValue = this.prepareFinalJson(); From 74918524c2846bbf21c23f9ecfbff9ff9e507796 Mon Sep 17 00:00:00 2001 From: Jaroslav Iha Date: Wed, 1 Apr 2026 15:11:08 +0200 Subject: [PATCH 112/114] fix pfda toolbar color change --- src/app/core/base/pfda-toolbar/pfda-toolbar.component.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 7efa5fb10..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; From c09f2aa92e113d049df8bb9bea76ed572e3edc8d Mon Sep 17 00:00:00 2001 From: Jaroslav Iha Date: Thu, 2 Apr 2026 14:01:31 +0200 Subject: [PATCH 113/114] update substance-drafts submit flow loading state --- .../substance-drafts/substance-drafts.component.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) 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 c18ca1a02..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 @@ -297,6 +297,8 @@ export class SubstanceDraftsComponent implements OnInit { 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; @@ -310,8 +312,10 @@ export class SubstanceDraftsComponent implements OnInit { this.substanceService.saveSubstance(draft.json.substance, 'import').subscribe(substance => { draft.submitStatus = SubmissionStatus.SUCCESS; draft.fileUrl = substance.fileUrl; - this.isLoading = false; - + completedCount++; + if (completedCount === submittableDrafts.length) { + this.isLoading = false; + } }, error => { draft.submitStatus = SubmissionStatus.ERROR; result.isSuccessfull = false; @@ -320,6 +324,10 @@ export class SubstanceDraftsComponent implements OnInit { } else { result.serverError = error; } + completedCount++; + if (completedCount === submittableDrafts.length) { + this.isLoading = false; + } }); } }) From 481d54c8ed008f6ef85c96e06fbdd893206fb149 Mon Sep 17 00:00:00 2001 From: Jaroslav Iha Date: Mon, 13 Apr 2026 12:50:58 +0200 Subject: [PATCH 114/114] fix hiding of pfda toolbar buttons --- src/styles/_material-overrides.scss | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/styles/_material-overrides.scss b/src/styles/_material-overrides.scss index 08cfe370a..e270f08a0 100644 --- a/src/styles/_material-overrides.scss +++ b/src/styles/_material-overrides.scss @@ -1080,9 +1080,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;