diff --git a/angular.json b/angular.json index 5884be806..91fd53f5f 100644 --- a/angular.json +++ b/angular.json @@ -243,22 +243,6 @@ "with": "src/environments/environment.fda.local.ts" } ] - }, - "cbg.prod": { - "fileReplacements": [ - { - "replace": "src/environments/environment.ts", - "with": "src/environments/environment.cbg.prod.ts" - } - ], - "optimization": true, - "outputHashing": "all", - "sourceMap": true, - "namedChunks": false, - "aot": true, - "extractLicenses": true, - "vendorChunk": false, - "buildOptimizer": true } } }, @@ -291,9 +275,6 @@ }, "gsrs.prod": { "browserTarget": "gsrs-client:build:gsrs.prod" - }, - "cbg.prod": { - "browserTarget": "gsrs-client:build:cbg.prod" } } }, diff --git a/package.dev.json b/package.dev.json index e44eca575..e6a0f9c06 100644 --- a/package.dev.json +++ b/package.dev.json @@ -25,7 +25,6 @@ "build:fda:prod": "npm run build-libraries && npm run clear-libs-src && ng build --base-href=/ginas/app/beta/ --configuration=fda.prod && node process-index.js && cd lib && extract-zip dojo-custom-jsdraw.zip && cd .. && npm run copy-libs-dist && npm run build-server", "build:gsrs:pre-prod": "npm run build-libraries && npm run clear-libs-src && ng build --base-href=/ginas/app/beta/ --aot=true --configuration=gsrs.pre-prod --sourceMap=true && node process-index.js && cd lib && extract-zip dojo-custom-jsdraw.zip && cd .. && npm run copy-libs-dist && npm run build-server", "build:gsrs:prod": "npm run build-libraries && npm run clear-libs-src && ng build --base-href=/ginas/app/beta/ --aot=true --configuration=gsrs.prod --sourceMap=true && node process-index.js && cd lib && extract-zip dojo-custom-jsdraw.zip && cd .. && npm run copy-libs-dist && npm run build-server", - "build:cbg:prod": "npm run build-libraries && npm run clear-libs-src && ng build --base-href=/gsrs/app/beta/ --aot=true --configuration=cbg.prod --sourceMap=true && node process-index.js && cd lib && extract-zip dojo-custom-jsdraw.zip && cd .. && npm run copy-libs-dist && npm run build-server", "test": "ng test", "lint": "ng lint", "tslint": "tslint", diff --git a/package.json b/package.json index 01eafe93b..e70ebb5fe 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,6 @@ "build:fda:prod": "npm run build-libraries && npm run clear-libs-src && ng build --base-href=/ginas/app/beta/ --configuration=fda.prod && node process-index.js && cd lib && extract-zip dojo-custom-jsdraw.zip && cd .. && npm run copy-libs-dist && npm run build-server", "build:gsrs:pre-prod": "npm run build-libraries && npm run clear-libs-src && ng build --base-href=/ginas/app/beta/ --aot=true --configuration=gsrs.pre-prod --sourceMap=true && node process-index.js && cd lib && extract-zip dojo-custom-jsdraw.zip && cd .. && npm run copy-libs-dist && npm run build-server", "build:gsrs:prod": "npm run build-libraries && npm run clear-libs-src && ng build --base-href=/ginas/app/beta/ --aot=true --configuration=gsrs.prod --sourceMap=true && node process-index.js && cd lib && extract-zip dojo-custom-jsdraw.zip && cd .. && npm run copy-libs-dist && npm run build-server", - "build:cbg:prod": "npm run build-libraries && npm run clear-libs-src && ng build --base-href=/gsrs/app/beta/ --aot=true --configuration=cbg.prod --sourceMap=true && node process-index.js && cd lib && extract-zip dojo-custom-jsdraw.zip && cd .. && npm run copy-libs-dist && npm run build-server", "test": "ng test", "lint": "ng lint", "tslint": "tslint", diff --git a/package.real.json b/package.real.json index 58766d0c9..dcb30c71c 100644 --- a/package.real.json +++ b/package.real.json @@ -25,7 +25,6 @@ "build:fda:prod": "npm run build-libraries && npm run clear-libs-src && ng build --base-href=/ginas/app/beta/ --configuration=fda.prod && node process-index.js && cd lib && extract-zip dojo-custom-jsdraw.zip && cd .. && npm run copy-libs-dist && npm run build-server", "build:gsrs:pre-prod": "npm run build-libraries && npm run clear-libs-src && ng build --base-href=/ginas/app/beta/ --aot=true --configuration=gsrs.pre-prod --sourceMap=true && node process-index.js && cd lib && extract-zip dojo-custom-jsdraw.zip && cd .. && npm run copy-libs-dist && npm run build-server", "build:gsrs:prod": "npm run build-libraries && npm run clear-libs-src && ng build --base-href=/ginas/app/beta/ --aot=true --configuration=gsrs.prod --sourceMap=true && node process-index.js && cd lib && extract-zip dojo-custom-jsdraw.zip && cd .. && npm run copy-libs-dist && npm run build-server", - "build:cbg:prod": "npm run build-libraries && npm run clear-libs-src && ng build --base-href=/gsrs/app/beta/ --aot=true --configuration=cbg.prod --sourceMap=true && node process-index.js && cd lib && extract-zip dojo-custom-jsdraw.zip && cd .. && npm run copy-libs-dist && npm run build-server", "test": "ng test", "lint": "ng lint", "tslint": "tslint", diff --git a/src/app/core/admin/import-browse/import-browse.component.ts b/src/app/core/admin/import-browse/import-browse.component.ts index f87773fc5..6bed16207 100644 --- a/src/app/core/admin/import-browse/import-browse.component.ts +++ b/src/app/core/admin/import-browse/import-browse.component.ts @@ -35,7 +35,6 @@ import { Title } from '@angular/platform-browser'; import { ControlledVocabularyService } from '@gsrs-core/controlled-vocabulary'; import { FormControl } from '@angular/forms'; import { WildcardService } from '@gsrs-core/utils/wildcard.service'; -import { I } from '@angular/cdk/keycodes'; import { SubstanceDetail, SubstanceName, SubstanceCode, SubstanceService } from '@gsrs-core/substance'; import { ConfigService } from '@gsrs-core/config'; import { SubBrowseEmitterService } from '@gsrs-core/substances-browse/sub-browse-emitter.service'; @@ -43,12 +42,12 @@ import { LoadingService } from '@gsrs-core/loading'; import { MainNotificationService, AppNotification, NotificationType } from '@gsrs-core/main-notification'; import { GoogleAnalyticsService } from '@gsrs-core/google-analytics'; import { AuthService } from '@gsrs-core/auth'; -import { environment } from '@environment/environment.cbg.prod'; import { AdminService } from '@gsrs-core/admin/admin.service'; import { Observable } from 'rxjs'; import { Subject } from 'rxjs'; import { BulkActionDialogComponent } from '@gsrs-core/admin/import-browse/bulk-action-dialog/bulk-action-dialog.component'; import { ImportScrubberComponent } from '@gsrs-core/admin/import-management/import-scrubber/import-scrubber.component'; +import {Environment} from "@environment/environment.model"; @Component({ selector: 'app-import-browse', @@ -73,6 +72,7 @@ export class ImportBrowseComponent implements OnInit, AfterViewInit, OnDestroy { public exactMatchSubstances: Array; + environment: Environment; searchOnIdentifiers: boolean; searchEntity: string; pageIndex: number; @@ -201,7 +201,7 @@ export class ImportBrowseComponent implements OnInit, AfterViewInit, OnDestroy { } else { this.bulkList[recordId] = {"checked": checked, "substance": event.substance}; } - + } bulkActionDialog() { @@ -219,7 +219,7 @@ export class ImportBrowseComponent implements OnInit, AfterViewInit, OnDestroy { this.bulkList = response; } this.overlayContainer.style.zIndex = null; - + }); } @@ -235,7 +235,7 @@ export class ImportBrowseComponent implements OnInit, AfterViewInit, OnDestroy { let added = 0; if (pagingResponse.content){ - + pagingResponse.content.forEach(record => { if ( record && record.id) { if (this.bulkList[record.id]) { @@ -276,11 +276,11 @@ export class ImportBrowseComponent implements OnInit, AfterViewInit, OnDestroy { else { this.bulkList[record.id] = {"checked": true, "name": record.name, "id": record.id}; } - + }); } - - + + } deselectAll() { @@ -299,14 +299,14 @@ export class ImportBrowseComponent implements OnInit, AfterViewInit, OnDestroy { } }); this.overlayContainer.style.zIndex = '1002'; - + dialogref.afterClosed().subscribe(result => { this.overlayContainer.style.zIndex = null; - + if(result) { this.scrubberModel = result; } - + }); } @@ -317,7 +317,7 @@ export class ImportBrowseComponent implements OnInit, AfterViewInit, OnDestroy { this.adminService.getImportScrubberSchema().subscribe(response => { this.scrubberSchema = response; }); - + this.gaService.sendPageView('Staging Area'); this.cvService.getDomainVocabulary('CODE_SYSTEM').pipe(take(1)).subscribe(response => { this.codeSystem = response['CODE_SYSTEM'].dictionary; @@ -370,6 +370,8 @@ export class ImportBrowseComponent implements OnInit, AfterViewInit, OnDestroy { }); this.facetManagerService.registerGetFacetsHandler(this.substanceService.getStagingFacets ); + this.environment = this.configService.environment; + this.subscriptions.push(authSubscription); this.isComponentInit = true; this.loadComponent(); @@ -386,10 +388,10 @@ export class ImportBrowseComponent implements OnInit, AfterViewInit, OnDestroy { this.adminService.GetStagedRecord(record.recordId).subscribe( resp => { this.records.push(resp); this.idMapping[resp.uuid] = record.recordId; - + }); }); - }); + }); } setUpPrivateSearchTerm() { @@ -589,7 +591,7 @@ export class ImportBrowseComponent implements OnInit, AfterViewInit, OnDestroy { } } - getRecord(id: string): Observable { + getRecord(id: string): Observable { let subject = new Subject(); let ids = []; let sources = []; @@ -607,7 +609,7 @@ export class ImportBrowseComponent implements OnInit, AfterViewInit, OnDestroy { } } - + }); }); let items = []; @@ -712,7 +714,7 @@ export class ImportBrowseComponent implements OnInit, AfterViewInit, OnDestroy { this.lastPage = Math.floor(this.totalSubstances / this.pageSize + 1); } - + pagingResponse.content.forEach(entry => { this.getRecord(entry._metadata.recordId).subscribe(response => { this.substances.push(response); @@ -727,7 +729,7 @@ export class ImportBrowseComponent implements OnInit, AfterViewInit, OnDestroy { this.matchTypes = []; this.narrowSearchSuggestionsCount = 0; this.loadingService.setLoading(false); - + // this.substanceService.setResult(pagingResponse.etag, pagingResponse.content, pagingResponse.total); }, error => { this.gaService.sendException('getSubstancesDetails: error from API call'); @@ -770,7 +772,7 @@ export class ImportBrowseComponent implements OnInit, AfterViewInit, OnDestroy { }, () => { subscription.unsubscribe(); - /* + /* this.substances.forEach(substance => { this.setSubstanceNames(substance.uuid); this.setSubstanceCodes(substance.uuid); @@ -888,11 +890,11 @@ searchTermOkforBeginsWithSearch(): boolean { populateUrlQueryParameters(): void { - + } editAdvancedSearch(): void { - const eventLabel = environment.isAnalyticsPrivate ? 'advanced search term' : + const eventLabel = this.environment.isAnalyticsPrivate ? 'advanced search term' : `${this.privateSearchTerm}`; this.gaService.sendEvent('substancesFiltering', 'icon-button:edit-advanced-search', eventLabel); // ** BEGIN: Store in Local Storage for Advanced Search @@ -946,7 +948,7 @@ searchTermOkforBeginsWithSearch(): boolean { } editStructureSearch(): void { - const eventLabel = environment.isAnalyticsPrivate ? 'structure search term' : + const eventLabel = this.environment.isAnalyticsPrivate ? 'structure search term' : `${this.privateStructureSearchTerm}-${this.privateSearchType}-${this.privateSearchCutoff}`; this.gaService.sendEvent('substancesFiltering', 'icon-button:edit-structure-search', eventLabel); @@ -966,7 +968,7 @@ searchTermOkforBeginsWithSearch(): boolean { clearStructureSearch(): void { - const eventLabel = environment.isAnalyticsPrivate ? 'structure search term' : + const eventLabel = this.environment.isAnalyticsPrivate ? 'structure search term' : `${this.privateStructureSearchTerm}-${this.privateSearchType}-${this.privateSearchCutoff}`; this.gaService.sendEvent('substancesFiltering', 'icon-button:clear-structure-search', eventLabel); @@ -981,7 +983,7 @@ searchTermOkforBeginsWithSearch(): boolean { } editSequenceSearh(): void { - const eventLabel = environment.isAnalyticsPrivate ? 'sequence search term' : + const eventLabel = this.environment.isAnalyticsPrivate ? 'sequence search term' : `${this.privateSequenceSearchTerm}-${this.privateSearchType}-${this.privateSearchCutoff}-${this.privateSearchSeqType}`; this.gaService.sendEvent('substancesFiltering', 'icon-button:edit-sequence-search', eventLabel); @@ -1001,7 +1003,7 @@ searchTermOkforBeginsWithSearch(): boolean { clearSequenceSearch(): void { - const eventLabel = environment.isAnalyticsPrivate ? 'sequence search term' : + const eventLabel = this.environment.isAnalyticsPrivate ? 'sequence search term' : `${this.privateSequenceSearchTerm}-${this.privateSearchType}-${this.privateSearchCutoff}-${this.privateSearchSeqType}`; this.gaService.sendEvent('substancesFiltering', 'icon-button:clear-sequence-search', eventLabel); @@ -1017,7 +1019,7 @@ searchTermOkforBeginsWithSearch(): boolean { } editBulkSearch(): void { - const eventLabel = environment.isAnalyticsPrivate ? 'bulk search term' : + const eventLabel = this.environment.isAnalyticsPrivate ? 'bulk search term' : `${this.searchEntity}-bulk-search-${this.privateBulkSearchQueryId}`; this.gaService.sendEvent('substancesFiltering', 'icon-button:edit-bulk-search', eventLabel); @@ -1033,7 +1035,7 @@ searchTermOkforBeginsWithSearch(): boolean { clearBulkSearch(): void { - const eventLabel = environment.isAnalyticsPrivate ? 'bulk search term' : + const eventLabel = this.environment.isAnalyticsPrivate ? 'bulk search term' : `${this.searchEntity}-bulk-search-${this.privateBulkSearchQueryId}`; this.gaService.sendEvent('substancesFiltering', 'icon-button:clear-bulk-search', eventLabel); @@ -1052,7 +1054,7 @@ searchTermOkforBeginsWithSearch(): boolean { clearSearch(): void { - const eventLabel = environment.isAnalyticsPrivate ? 'search term' : this.privateSearchTerm; + const eventLabel = this.environment.isAnalyticsPrivate ? 'search term' : this.privateSearchTerm; this.gaService.sendEvent('substancesFiltering', 'icon-button:clear-search', eventLabel); this.privateSearchTerm = ''; @@ -1173,7 +1175,7 @@ searchTermOkforBeginsWithSearch(): boolean { } openImageModal(substance: any): void { - const eventLabel = environment.isAnalyticsPrivate ? 'substance' : substance._name; + const eventLabel = this.environment.isAnalyticsPrivate ? 'substance' : substance._name; this.gaService.sendEvent('substancesContent', 'link:structure-zoom', eventLabel); let data: any; diff --git a/src/app/core/app.module.ts b/src/app/core/app.module.ts index 87cc2591c..6aea37b90 100644 --- a/src/app/core/app.module.ts +++ b/src/app/core/app.module.ts @@ -36,6 +36,7 @@ import { MatBottomSheetModule } from '@angular/material/bottom-sheet'; import { MatProgressBarModule } from '@angular/material/progress-bar'; import { PageNotFoundComponent } from './page-not-found/page-not-found.component'; import { BaseComponent } from './base/base.component'; +import { PfdaToolbarComponent } from './base/pfda-toolbar/pfda-toolbar.component'; import { HomeComponent } from './home/home.component'; import { RegistrarsComponent } from './registrars/registrars.component'; import { SubstancesBrowseComponent } from './substances-browse/substances-browse.component'; @@ -124,6 +125,7 @@ import { PrivacyStatementModule } from './privacy-statement/privacy-statement.mo AppComponent, PageNotFoundComponent, BaseComponent, + PfdaToolbarComponent, HomeComponent, UnauthorizedComponent, SubstancesBrowseComponent, diff --git a/src/app/core/assets/icons/pfda/gsrs-logo-round-bw.svg b/src/app/core/assets/icons/pfda/gsrs-logo-round-bw.svg new file mode 100644 index 000000000..d33ecd415 --- /dev/null +++ b/src/app/core/assets/icons/pfda/gsrs-logo-round-bw.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/src/app/core/assets/icons/pfda/home.svg b/src/app/core/assets/icons/pfda/home.svg new file mode 100644 index 000000000..991b35a92 --- /dev/null +++ b/src/app/core/assets/icons/pfda/home.svg @@ -0,0 +1,13 @@ + diff --git a/src/app/core/assets/icons/pfda/profile.svg b/src/app/core/assets/icons/pfda/profile.svg new file mode 100644 index 000000000..3b6727689 --- /dev/null +++ b/src/app/core/assets/icons/pfda/profile.svg @@ -0,0 +1,10 @@ + + + diff --git a/src/app/core/assets/icons/pfda/questionmark.svg b/src/app/core/assets/icons/pfda/questionmark.svg new file mode 100644 index 000000000..52dafbde8 --- /dev/null +++ b/src/app/core/assets/icons/pfda/questionmark.svg @@ -0,0 +1,11 @@ + diff --git a/src/app/core/assets/icons/pfda/support.svg b/src/app/core/assets/icons/pfda/support.svg new file mode 100644 index 000000000..05368ee92 --- /dev/null +++ b/src/app/core/assets/icons/pfda/support.svg @@ -0,0 +1,14 @@ + \ No newline at end of file diff --git a/src/app/core/assets/images/pfda/pfda-logo.png b/src/app/core/assets/images/pfda/pfda-logo.png new file mode 100644 index 000000000..980274504 Binary files /dev/null and b/src/app/core/assets/images/pfda/pfda-logo.png differ diff --git a/src/app/core/auth/auth.module.ts b/src/app/core/auth/auth.module.ts index 5d458787c..109295219 100644 --- a/src/app/core/auth/auth.module.ts +++ b/src/app/core/auth/auth.module.ts @@ -17,6 +17,8 @@ import { RouterModule } from '@angular/router'; import { DecodeUriPipe } from '@gsrs-core/auth/user-downloads/download-monitor/decodeURI.pipe'; import { FileSizePipe } from '@gsrs-core/auth/user-downloads/download-monitor/fileSize.pipe'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { SessionExpirationComponent } from './session-expiration/session-expiration.component'; +import { SessionExpirationDialogComponent } from './session-expiration/session-expiration-dialog/session-expiration-dialog.component'; @NgModule({ imports: [ @@ -39,13 +41,19 @@ import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; UserProfileComponent, UserDownloadsComponent, DownloadMonitorComponent, + SessionExpirationComponent, + SessionExpirationDialogComponent, DecodeUriPipe, FileSizePipe ], + entryComponents: [ + SessionExpirationDialogComponent + ], exports: [ LoginComponent, UserProfileComponent, DownloadMonitorComponent, + SessionExpirationComponent, UserDownloadsComponent ] }) 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 new file mode 100644 index 000000000..c35a6869c --- /dev/null +++ b/src/app/core/auth/session-expiration/session-expiration-dialog/session-expiration-dialog.component.html @@ -0,0 +1,11 @@ +

+ {{dialogTitle}} +

+
+ {{dialogMessage}} +
+
+ + + +
diff --git a/src/app/core/auth/session-expiration/session-expiration-dialog/session-expiration-dialog.component.scss b/src/app/core/auth/session-expiration/session-expiration-dialog/session-expiration-dialog.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src/app/core/auth/session-expiration/session-expiration-dialog/session-expiration-dialog.component.spec.ts b/src/app/core/auth/session-expiration/session-expiration-dialog/session-expiration-dialog.component.spec.ts new file mode 100644 index 000000000..b3156517c --- /dev/null +++ b/src/app/core/auth/session-expiration/session-expiration-dialog/session-expiration-dialog.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { SessionExpirationDialogComponent } from './session-expiration-dialog.component'; + +describe('SessionExpirationDialogComponent', () => { + let component: SessionExpirationDialogComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ SessionExpirationDialogComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(SessionExpirationDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); 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 new file mode 100644 index 000000000..3dbae841e --- /dev/null +++ b/src/app/core/auth/session-expiration/session-expiration-dialog/session-expiration-dialog.component.ts @@ -0,0 +1,80 @@ +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'; + +@Component({ + selector: 'app-session-expiration-dialog', + templateUrl: './session-expiration-dialog.component.html', + styleUrls: ['./session-expiration-dialog.component.scss'] +}) +export class SessionExpirationDialogComponent implements OnInit { + sessionExpirationWarning: SessionExpirationWarning = null; + sessionExpiringAt: number; + timeRemainingSeconds: number; + dialogTitle: string; + dialogMessage: string; + private updateDialogInterval: any; + + constructor( + public dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: any, + // N.B. injected services has to come after data + private router: Router, + private http: HttpClient + ) { + this.sessionExpirationWarning = data.sessionExpirationWarning; + this.sessionExpiringAt = data.sessionExpiringAt; + } + + ngOnInit() { + // If SessionExpirationWarning is not found in configData, the intervals are never set + // and this component is inert + this.updateDialogInterval = setInterval(() => { this.updateDialog(); }); + } + + ngOnDestroy() { + clearInterval(this.updateDialogInterval); + } + + getCurrentTime() { + return Math.floor((new Date()).getTime() / 1000); + } + + updateDialog() { + const currentTime = this.getCurrentTime() + this.timeRemainingSeconds = this.sessionExpiringAt - currentTime; + + if (this.timeRemainingSeconds > 0) { + const remainingMinutes = Math.floor(this.timeRemainingSeconds / 60); + const reminaingSeconds = String(this.timeRemainingSeconds % 60).padStart(2, '0'); + this.dialogTitle = "Session Ending Soon" + this.dialogMessage = `You will be logged out in ${remainingMinutes}:${reminaingSeconds}` + } + else { + this.dialogTitle = "Session Ended" + this.dialogMessage = "Your session has expired, please login again." + } + } + + closeDialog() { + this.dialogRef.close(false); + } + + extendSession() { + const url = this.sessionExpirationWarning.extendSessionApiUrl; + this.http.get(url).subscribe( + data => { + this.dialogRef.close(true); + }, + err => { console.log("Error extending session: ", err) }, + () => { } + ); + } + + login() { + window.location.assign('/login'); + } +} diff --git a/src/app/core/auth/session-expiration/session-expiration.component.html b/src/app/core/auth/session-expiration/session-expiration.component.html new file mode 100644 index 000000000..e69de29bb diff --git a/src/app/core/auth/session-expiration/session-expiration.component.spec.ts b/src/app/core/auth/session-expiration/session-expiration.component.spec.ts new file mode 100644 index 000000000..ae9bf0c23 --- /dev/null +++ b/src/app/core/auth/session-expiration/session-expiration.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { SessionExpirationComponent } from './session-expiration.component'; + +describe('SessionExpirationComponent', () => { + let component: SessionExpirationComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ SessionExpirationComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(SessionExpirationComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/core/auth/session-expiration/session-expiration.component.ts b/src/app/core/auth/session-expiration/session-expiration.component.ts new file mode 100644 index 000000000..14fb287a5 --- /dev/null +++ b/src/app/core/auth/session-expiration/session-expiration.component.ts @@ -0,0 +1,130 @@ +import { Router, Event as NavigationEvent, NavigationStart } from '@angular/router'; +import { Component, OnInit } from '@angular/core'; +import { OverlayContainer } from '@angular/cdk/overlay'; +import { HttpClient } from '@angular/common/http'; +import { ConfigService, SessionExpirationWarning } from '@gsrs-core/config'; +import { AuthService } from '../auth.service'; +import { SessionExpirationDialogComponent } from './session-expiration-dialog/session-expiration-dialog.component' +import { Subscription } from 'rxjs'; +import { MatDialog } from '@angular/material/dialog'; + +@Component({ + selector: 'app-session-expiration', + templateUrl: './session-expiration.component.html' +}) +export class SessionExpirationComponent implements OnInit { + sessionExpirationWarning: SessionExpirationWarning = null; + sessionExpiringAt: number; + private overlayContainer: HTMLElement; + private subscriptions: Array = []; + private expirationTimer: any; + + constructor( + private router: Router, + private configService: ConfigService, + private authService: AuthService, + private http: HttpClient, + private dialog: MatDialog, + private overlayContainerService: OverlayContainer + ) { + 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(); + } + } + }); + 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(); + } + + getCurrentTime() { + return Math.floor((new Date()).getTime() / 1000); + } + + clearExpirationTimer() { + if (this.expirationTimer) { + clearTimeout(this.expirationTimer); + this.expirationTimer = null; + } + } + + resetExpirationTimer() { + this.clearExpirationTimer(); + + const currentTime = this.getCurrentTime() + this.sessionExpiringAt = currentTime + this.sessionExpirationWarning.maxSessionDurationMinutes * 60; + + const timeRemainingSeconds = this.sessionExpiringAt - currentTime; + const timeBeforeDisplayingDialogMs = (timeRemainingSeconds - 61) * 1000; + if (timeBeforeDisplayingDialogMs > 0) { + this.expirationTimer = setTimeout( () => { + this.openDialog(); + }, timeBeforeDisplayingDialogMs); + } + else { + this.login(); + } + } + + openDialog() { + const dialogRef = this.dialog.open(SessionExpirationDialogComponent, { + data: { + 'sessionExpirationWarning': this.sessionExpirationWarning, + 'sessionExpiringAt': this.sessionExpiringAt + }, + width: '650px', + autoFocus: false, + disableClose: true + }); + this.overlayContainer.style.zIndex = '1501'; + const dialogSubscription = dialogRef.afterClosed().subscribe(response => { + this.overlayContainer.style.zIndex = null; + if (response) { + // Session was extended + this.resetExpirationTimer(); + } + }); + } + + 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'); + } +} diff --git a/src/app/core/base/base.component.html b/src/app/core/base/base.component.html index ece895904..2aa31b201 100644 --- a/src/app/core/base/base.component.html +++ b/src/app/core/base/base.component.html @@ -1,4 +1,4 @@ - +
-
@@ -211,6 +211,10 @@
+
+ +
+ - \ No newline at end of file + diff --git a/src/app/core/base/base.component.ts b/src/app/core/base/base.component.ts index 623ca00bd..1d12fe9bb 100644 --- a/src/app/core/base/base.component.ts +++ b/src/app/core/base/base.component.ts @@ -3,6 +3,7 @@ import { Router, RouterEvent, NavigationExtras, ActivatedRoute, NavigationStart, import { Environment } from '../../../environments/environment.model'; import { AuthService } from '../auth/auth.service'; import { Auth } from '../auth/auth.model'; +import { SessionExpirationComponent } from '../auth/session-expiration/session-expiration.component' import { ConfigService } from '../config/config.service'; import { OverlayContainer } from '@angular/cdk/overlay'; import { LoadingService } from '../loading/loading.service'; @@ -45,6 +46,7 @@ export class BaseComponent implements OnInit, OnDestroy { appId: string; clasicBaseHref: string; navItems: Array; + customToolbarComponent: string = ''; canRegister = false; registerNav: Array; searchNav: Array; @@ -73,6 +75,7 @@ export class BaseComponent implements OnInit, OnDestroy { private utilsService: UtilsService, private wildCardService: WildcardService ) { + this.customToolbarComponent = this.configService.configData.customToolbarComponent; this.wildCardService.wildCardObservable.subscribe((data) => { this.wildCardText = data; }); diff --git a/src/app/core/base/pfda-toolbar/pfda-toolbar.component.html b/src/app/core/base/pfda-toolbar/pfda-toolbar.component.html new file mode 100644 index 000000000..63df8dd5b --- /dev/null +++ b/src/app/core/base/pfda-toolbar/pfda-toolbar.component.html @@ -0,0 +1,91 @@ + +
+ +
+ + +
+ + +
Back Home
+
+
+ +
+ + +
+ +
GSRS
+
+
+ + + + + + + +
+ +
Support
+
+
+ + +
+ +
Getting Started
+
+
+ + +
diff --git a/src/app/core/base/pfda-toolbar/pfda-toolbar.component.scss b/src/app/core/base/pfda-toolbar/pfda-toolbar.component.scss new file mode 100644 index 000000000..8a863ed32 --- /dev/null +++ b/src/app/core/base/pfda-toolbar/pfda-toolbar.component.scss @@ -0,0 +1,88 @@ +$pfda-padding-main: 32px; +$pfda-navbar-height: 64px; +$pfda-navbar-blue: #343E4D; +$pfda-navbar-active-blue: #2f5373; +$pfda-navbar-item-hover: #a0c2e0; +$pfda-navbar-spacer-colour: #5f768a; + +$screenSmall: 850px; +$screenMedium: 1045px; + + +.pfda-toolbar { + background-color: $pfda-navbar-blue; + color: white; + font-family: "Lato","Helvetica Neue",Helvetica,Arial,sans-serif; + font-size: 13px; + font-weight: 400; + line-height: 16px; + padding: 0 8px; + height: auto; + + a { + color: inherit; + text-decoration: none; + } + + @media(min-width: $screenSmall) { + padding: 0 $pfda-padding-main; + height: $pfda-navbar-height; + } +} + +.pfda-toolbar-inner { + display: flex; + align-items: center; +} + +.pfda-logo img { + width: 144px; + margin-top: 4px; +} + +.pfda-toolbar-button { + display: flex; + flex-flow: column nowrap; + align-self: flex-end; + align-items: center; + justify-content: center; + padding: 10px 6px; + + &:hover { + color: $pfda-navbar-item-hover; + } + + &.active { + color: white; + background-color: $pfda-navbar-active-blue; + } + + &-icon, .mat-icon { + display: flex; + align-items: flex-end; + height: 18px; + margin: 3px 0px 2px 0px; + } + + &-title { + display: none; + margin-top: 4px; + + @media(min-width: $screenSmall) { + display: inline; + } + } +} + +.pfda-toolbar-spacer { + border-right: 1px solid $pfda-navbar-spacer-colour; + width: 1px; + height: 38px; + margin: 0px 4px; +} + +.pfda-user-button { + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} diff --git a/src/app/core/base/pfda-toolbar/pfda-toolbar.component.spec.ts b/src/app/core/base/pfda-toolbar/pfda-toolbar.component.spec.ts new file mode 100644 index 000000000..59dc19d1f --- /dev/null +++ b/src/app/core/base/pfda-toolbar/pfda-toolbar.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { PfdaToolbarComponent } from './pfda-toolbar.component'; + +describe('PfdaToolbarComponent', () => { + let component: PfdaToolbarComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ PfdaToolbarComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(PfdaToolbarComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/core/base/pfda-toolbar/pfda-toolbar.component.ts b/src/app/core/base/pfda-toolbar/pfda-toolbar.component.ts new file mode 100644 index 000000000..1bc5a3ab2 --- /dev/null +++ b/src/app/core/base/pfda-toolbar/pfda-toolbar.component.ts @@ -0,0 +1,83 @@ +import { Component, OnInit } from '@angular/core'; +import { Router, ActivatedRoute, NavigationExtras } from '@angular/router'; +import { ConfigService } from '../../config/config.service'; +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'; + +@Component({ + selector: 'app-pfda-toolbar', + templateUrl: './pfda-toolbar.component.html', + styleUrls: ['./pfda-toolbar.component.scss'] +}) +export class PfdaToolbarComponent implements OnInit { + pfdaBaseUrl: string; + logoSrcPath: string; + homeIconPath: string; + auth?: Auth; + searchValue: string; + private overlayContainer: HTMLElement; + private subscriptions: Array = []; + + constructor( + private router: Router, + private activatedRoute: ActivatedRoute, + private configService: ConfigService, + private overlayContainerService: OverlayContainer, + private substanceTextSearchService: SubstanceTextSearchService, + public authService: AuthService + ) { + const authSubscription = this.authService.getAuth().subscribe(auth => { + this.auth = auth; + }); + this.subscriptions.push(authSubscription); + } + + ngOnInit() { + this.pfdaBaseUrl = this.configService.configData.pfdaBaseUrl || '/'; + + const baseHref = this.configService.environment.baseHref || '/' + this.logoSrcPath = `${baseHref}assets/images/pfda/pfda-logo.png`; + this.homeIconPath = `${baseHref}assets/images/pfda/home.svg`; + + this.overlayContainer = this.overlayContainerService.getContainerElement(); + + if (this.activatedRoute.snapshot.queryParamMap.has('search')) { + this.searchValue = this.activatedRoute.snapshot.queryParamMap.get('search'); + } + + const paramsSubscription = this.activatedRoute.queryParamMap.subscribe(params => { + this.searchValue = params.get('search'); + }); + this.subscriptions.push(paramsSubscription); + + this.substanceTextSearchService.registerSearchComponent('main-substance-search'); + const cleanSearchSubscription = this.substanceTextSearchService.setSearchComponentValueEvent('main-substance-search') + .subscribe(value => { + this.searchValue = value; + }); + this.subscriptions.push(cleanSearchSubscription); + } + + processSubstanceSearch(searchValue: string) { + this.navigateToSearchResults(searchValue); + } + + navigateToSearchResults(searchTerm: string) { + const navigationExtras: NavigationExtras = { + queryParams: searchTerm ? { 'search': searchTerm } : null + }; + + this.router.navigate(['/browse-substance'], navigationExtras); + } + + increaseMenuZindex(): void { + this.overlayContainer.style.zIndex = '1001'; + } + + removeZindex(): void { + this.overlayContainer.style.zIndex = null; + } +} diff --git a/src/app/core/config/config.json b/src/app/core/config/config.json index a40a1bdbb..c50ccc603 100644 --- a/src/app/core/config/config.json +++ b/src/app/core/config/config.json @@ -662,7 +662,7 @@ "order": 210 } ] - }, + }, { "display": "Help", "order": 60, diff --git a/src/app/core/config/config.model.ts b/src/app/core/config/config.model.ts index 73bde3bac..889aaf4f6 100644 --- a/src/app/core/config/config.model.ts +++ b/src/app/core/config/config.model.ts @@ -34,6 +34,11 @@ export interface Config { advancedSearchFacetDisplay?: boolean; facetDisplay?: Array; relationshipsVisualizationUri?: string; + customToolbarComponent?: string; + sessionExpirationWarning?: SessionExpirationWarning; + disableReferenceDocumentUpload?: boolean; + externalSiteWarning?: ExternalSiteWarning; + pfdaBaseUrl?: string; homeDynamicLinks?: Array; registrarDynamicLinks?: Array; registrarDynamicLinks2?: Array; @@ -119,3 +124,14 @@ export interface AuthenticateAs { apiKey?: string, apiToken?: string; } + +export interface SessionExpirationWarning { + maxSessionDurationMinutes: number; + extendSessionApiUrl: string; +} + +export interface ExternalSiteWarning { + enabled: boolean; + dialogTitle: string; + dialogMessage: string; +} diff --git a/src/app/core/config/config.pfda.json b/src/app/core/config/config.pfda.json new file mode 100644 index 000000000..dc35f4eb0 --- /dev/null +++ b/src/app/core/config/config.pfda.json @@ -0,0 +1,548 @@ +{ + "version": "2.7.1", + "substanceDetailsCards": [ + { + "card": "substance-overview", + "title": "overview" + }, + { + "card": "substance-primary-definition", + "title": "primary definition", + "filters": [ + { + "filterName": "equals", + "propertyToCheck": "definitionType", + "value": "ALTERNATIVE" + } + ] + }, + { + "card": "substance-alternative-definition", + "type": "SUBSTANCE->SUB_ALTERNATE", + "title": "variant concepts", + "filters": [ + { + "filterName": "exists", + "propertyToCheck": "relationships" + }, + { + "filterName": "substanceRelationships", + "value": "SUBSTANCE->SUB_ALTERNATE" + } + ] + }, + { + "card": "structure-details", + "title": "structure", + "filters": [ + { + "filterName": "equals", + "propertyToCheck": "substanceClass", + "value": "chemical|polymer" + }, + { + "filterName": "exists", + "propertyToCheck": "structure" + } + ] + }, + { + "card": "substance-names", + "title": "names", + "filters": [ + { + "filterName": "exists", + "propertyToCheck": "names" + } + ] + }, + { + "card": "substance-codes", + "type": "classification", + "filters": [ + { + "filterName": "exists", + "propertyToCheck": "codes" + }, + { + "filterName": "substanceCodes", + "value": "classification" + } + ] + }, + { + "card": "substance-codes", + "type": "identifiers", + "filters": [ + { + "filterName": "exists", + "propertyToCheck": "codes" + }, + { + "filterName": "substanceCodes", + "value": "identifiers" + } + ] + }, + { + "card": "substance-subunits", + "title": "subunits", + "filters": [ + { + "filterName": "equals", + "propertyToCheck": "substanceClass", + "value": "protein" + }, + { + "filterName": "exists", + "propertyToCheck": "protein.subunits" + } + ] + }, + { + "card": "substance-subunits", + "title": "subunits", + "filters": [ + { + "filterName": "equals", + "propertyToCheck": "substanceClass", + "value": "nucleicAcid" + }, + { + "filterName": "exists", + "propertyToCheck": "nucleicAcid.subunits" + } + ] + }, + { + "card": "substance-glycosylation", + "title": "glycosylation", + "filters": [ + { + "filterName": "equals", + "propertyToCheck": "substanceClass", + "value": "protein" + }, + { + "filterName": "anyExists", + "propertyToCheck": "protein.glycosylation.glycosylationType|protein.glycosylation.CGlycosylationSites|protein.glycosylation.NGlycosylationSites|protein.glycosylation.OGlycosylationSites" + } + ] + }, + { + "card": "substance-disulfide-links", + "title": "disulfide links", + "filters": [ + { + "filterName": "equals", + "propertyToCheck": "substanceClass", + "value": "protein" + }, + { + "filterName": "exists", + "propertyToCheck": "protein.disulfideLinks" + } + ] + }, + { + "card": "substance-other-links", + "title": "other links", + "filters": [ + { + "filterName": "equals", + "propertyToCheck": "substanceClass", + "value": "protein" + }, + { + "filterName": "exists", + "propertyToCheck": "protein.otherLinks" + } + ] + }, + { + "card": "substance-relationships", + "type": "IMPURITY", + "title": "impurities", + "filters": [ + { + "filterName": "exists", + "propertyToCheck": "relationships" + }, + { + "filterName": "substanceRelationships", + "value": "IMPURITY" + } + ] + }, + { + "card": "substance-relationships", + "type": "METABOLITE", + "title": "metabolites", + "filters": [ + { + "filterName": "exists", + "propertyToCheck": "relationships" + }, + { + "filterName": "substanceRelationships", + "value": "METABOLITE" + } + ] + }, + { + "card": "substance-relationships", + "type": "ACTIVE MOIETY", + "title": "active moiety", + "filters": [ + { + "filterName": "exists", + "propertyToCheck": "relationships" + }, + { + "filterName": "substanceRelationships", + "value": "ACTIVE MOIETY" + } + ] + }, + { + "card": "substance-relationships", + "type": "CONSTITUENT", + "title": "constituents", + "filters": [ + { + "filterName": "exists", + "propertyToCheck": "relationships" + }, + { + "filterName": "substanceRelationships", + "value": "CONSTITUENT" + } + ] + }, + { + "card": "substance-relationships", + "type": "SUB_CONCEPT->SUBSTANCE", + "title": "variant concepts", + "filters": [ + { + "filterName": "exists", + "propertyToCheck": "relationships" + }, + { + "filterName": "substanceRelationships", + "value": "SUB_CONCEPT->SUBSTANCE" + } + ] + }, + { + "card": "substance-relationships", + "type": "RELATIONSHIPS", + "title": "relationships", + "filters": [ + { + "filterName": "exists", + "propertyToCheck": "relationships" + }, + { + "filterName": "substanceRelationships", + "value": [ + "METABOLITE", + "IMPURITY", + "ACTIVE MOIETY", + "CONSTITUENT", + "SUB_CONCEPT->SUBSTANCE" + ] + } + ] + }, + { + "card": "substance-notes", + "title": "notes", + "filters": [ + { + "filterName": "exists", + "propertyToCheck": "notes" + } + ] + }, + { + "card": "substance-audit-info", + "title": "audit info" + }, + { + "card": "substance-references", + "title": "references", + "filters": [ + { + "filterName": "exists", + "propertyToCheck": "references" + } + ] + }, + { + "card": "substance-moieties", + "title": "moieties", + "filters": [ + { + "filterName": "equals", + "propertyToCheck": "substanceClass", + "value": "chemical" + }, + { + "filterName": "exists", + "propertyToCheck": "moieties" + } + ] + }, + { + "card": "substance-concept-definition", + "title": "concept definition", + "filters": [ + { + "filterName": "equals", + "propertyToCheck": "substanceClass", + "value": "concept" + } + ] + }, + { + "card": "substance-polymer-structure", + "title": "display structure", + "filters": [ + { + "filterName": "equals", + "propertyToCheck": "substanceClass", + "value": "polymer" + } + ] + }, + { + "card": "substance-monomers", + "title": "monomers", + "filters": [ + { + "filterName": "equals", + "propertyToCheck": "substanceClass", + "value": "polymer" + }, + { + "filterName": "exists", + "propertyToCheck": "polymer.monomers" + } + ] + }, + { + "card": "substance-structural-units", + "title": "Structural Units", + "filters": [ + { + "filterName": "equals", + "propertyToCheck": "substanceClass", + "value": "polymer" + }, + { + "filterName": "exists", + "propertyToCheck": "polymer.structuralUnits" + } + ] + }, + { + "card": "substance-mixture-components", + "title": "mixture components", + "filters": [ + { + "filterName": "equals", + "propertyToCheck": "substanceClass", + "value": "mixture" + }, + { + "filterName": "exists", + "propertyToCheck": "mixture.components" + } + ] + }, + { + "card": "substance-constituents", + "title": "specified substance constituents", + "filters": [ + { + "filterName": "equals", + "propertyToCheck": "substanceClass", + "value": "specifiedSubstanceG1" + }, + { + "filterName": "exists", + "propertyToCheck": "specifiedSubstance.constituents" + } + ] + }, + { + "card": "substance-mixture-source", + "title": "mixture source", + "filters": [ + { + "filterName": "equals", + "propertyToCheck": "substanceClass", + "value": "mixture" + }, + { + "filterName": "exists", + "propertyToCheck": "mixture.parentSubstance" + } + ] + }, + { + "card": "substance-modifications", + "title": "substance modifications", + "filters": [ + { + "filterName": "anyExists", + "propertyToCheck": "modifications.structuralModifications|modifications.physicalModifications|modifications.agentModifications" + } + ] + }, + { + "card": "substance-na-sugars", + "title": "sugars", + "filters": [ + { + "filterName": "equals", + "propertyToCheck": "substanceClass", + "value": "nucleicAcid" + }, + { + "filterName": "exists", + "propertyToCheck": "nucleicAcid.sugars" + } + ] + }, + { + "card": "substance-na-linkages", + "title": "linkages", + "filters": [ + { + "filterName": "equals", + "propertyToCheck": "substanceClass", + "value": "nucleicAcid" + }, + { + "filterName": "exists", + "propertyToCheck": "nucleicAcid.linkages" + } + ] + }, + { + "card": "substance-properties", + "title": "characteristic attributes", + "filters": [ + { + "filterName": "exists", + "propertyToCheck": "properties" + } + ] + }, + { + "card": "substance-history", + "title": "history", + "filters": [ + { + "filterName": "hasCredentials", + "propertyToCheck": "admin" + } + ] + }, + { + "card": "substance-ssg-definition", + "title": "definition", + "filters": [ + { + "filterName": "exists", + "propertyToCheck": "specifiedSubstanceG3.definition" + } + ] + }, + { + "card": "substance-ssg-parent-substance", + "title": "parent substance", + "filters": [ + { + "filterName": "exists", + "propertyToCheck": "specifiedSubstanceG3.parentSubstance" + } + ] + }, + { + "card": "substance-ssg-grade", + "title": "grade", + "filters": [ + { + "filterName": "exists", + "propertyToCheck": "specifiedSubstanceG3.grade" + } + ] + } + ], + "facets": { + "substances": { + "default": [ + "Deprecated", + "Record Status", + "Substance Class", + "GInAS Tag", + "Code System", + "ATC Level 1", + "ATC Level 2", + "ATC Level 3", + "ATC Level 4", + "DME Reactions", + "Moiety Type", + "Molecular Weight", + "SubstanceStereochemistry", + "Relationships", + "Record Level Access", + "Display Name Level Access", + "Definition Level Access", + "Protein Type", + "Protein Subtype", + "modified" + ], + "admin": [ + "Record Created By", + "root_lastEdited", + "root_lastEditedBy", + "root_approved", + "Approved By", + "Material Type", + "Family", + "Parts" + ] + } + }, + "codeSystemOrder": [ + "BDNUM", + "CAS", + "WHO-ATC", + "EVMPD", + "NCI" + ], + "substanceSelectorProperties": [ + "root_names_name", + "root_approvalID", + "root_codes_BDNUM", + "root_codes_CAS", + "root_codes_ECHA\\ \\(EC\/EINECS\\)" + ], + "contactEmail": "precisionfda-support@dnanexus.com", + "sessionExpirationWarning": { + "extendSessionApiUrl": "/api/update_active", + "maxSessionDurationMinutes": 15 + }, + "disableReferenceDocumentUpload": true, + "externalSiteWarning": { + "enabled": true, + "dialogTitle": "Accessing External Site", + "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 diff --git a/src/app/core/facets-manager/facet-display.pipe.ts b/src/app/core/facets-manager/facet-display.pipe.ts index 755554476..cf14e66e8 100644 --- a/src/app/core/facets-manager/facet-display.pipe.ts +++ b/src/app/core/facets-manager/facet-display.pipe.ts @@ -46,6 +46,18 @@ export class FacetDisplayPipe implements PipeTransform { } } } + if(this.configService && this.configService.configData.facetDisplay) { + let returned = name; + this.configService.configData.facetDisplay.forEach(facet => { + if (name === facet.value) { + returned = facet.display; + return facet.display; + } + }); + if (returned !== name) { + return returned; + } + } if (name.toLowerCase() === 'substancestereochemistry') { return 'Stereochemistry'; } diff --git a/src/app/core/name-resolver/external-site-warning-dialog/external-site-warning-dialog.component.html b/src/app/core/name-resolver/external-site-warning-dialog/external-site-warning-dialog.component.html new file mode 100644 index 000000000..7ff20f703 --- /dev/null +++ b/src/app/core/name-resolver/external-site-warning-dialog/external-site-warning-dialog.component.html @@ -0,0 +1,12 @@ +

+ {{externalSiteWarning.dialogTitle}} +

+
+ {{externalSiteWarning.dialogMessage}} +
+
+ Don't Ask Again + + + +
diff --git a/src/app/core/name-resolver/external-site-warning-dialog/external-site-warning-dialog.component.scss b/src/app/core/name-resolver/external-site-warning-dialog/external-site-warning-dialog.component.scss new file mode 100644 index 000000000..af8ab23c1 --- /dev/null +++ b/src/app/core/name-resolver/external-site-warning-dialog/external-site-warning-dialog.component.scss @@ -0,0 +1,16 @@ +.dialog-title { + margin-top: 12px; +} + +.dialog-actions { + margin-top: 12px; + + button { + margin-left: 12px; + } +} + +.dialog-checkbox { + font-size: 14px; + margin: 12px; +} diff --git a/src/app/core/name-resolver/external-site-warning-dialog/external-site-warning-dialog.component.spec.ts b/src/app/core/name-resolver/external-site-warning-dialog/external-site-warning-dialog.component.spec.ts new file mode 100644 index 000000000..705322fcd --- /dev/null +++ b/src/app/core/name-resolver/external-site-warning-dialog/external-site-warning-dialog.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ExternalSiteWarningDialogComponent } from './external-site-warning-dialog.component'; + +describe('ExternalSiteWarningDialogComponent', () => { + let component: ExternalSiteWarningDialogComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ ExternalSiteWarningDialogComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ExternalSiteWarningDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/core/name-resolver/external-site-warning-dialog/external-site-warning-dialog.component.ts b/src/app/core/name-resolver/external-site-warning-dialog/external-site-warning-dialog.component.ts new file mode 100644 index 000000000..abd1628ad --- /dev/null +++ b/src/app/core/name-resolver/external-site-warning-dialog/external-site-warning-dialog.component.ts @@ -0,0 +1,33 @@ +import { Component, OnInit } from '@angular/core'; +import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; +import { ConfigService, ExternalSiteWarning } from '@gsrs-core/config'; + +@Component({ + selector: 'app-external-site-warning-dialog', + templateUrl: './external-site-warning-dialog.component.html', + styleUrls: ['./external-site-warning-dialog.component.scss'] +}) +export class ExternalSiteWarningDialogComponent implements OnInit { + externalSiteWarning: ExternalSiteWarning; + dontAskAgain: boolean; + + constructor( + public dialogRef: MatDialogRef, + private configService: ConfigService + ) { } + + ngOnInit() { + this.externalSiteWarning = this.configService.configData.externalSiteWarning; + this.dontAskAgain = localStorage.getItem('externalSiteWarningDontAskAgain') === 'true'; + } + + acceptDialog() { + localStorage.setItem('externalSiteWarningDontAskAgain', this.dontAskAgain.toString()); + + this.dialogRef.close(true); + } + + cancelDialog() { + this.dialogRef.close(false); + } +} diff --git a/src/app/core/name-resolver/name-resolver.component.ts b/src/app/core/name-resolver/name-resolver.component.ts index 2696ae0f1..ad8a4a8c4 100644 --- a/src/app/core/name-resolver/name-resolver.component.ts +++ b/src/app/core/name-resolver/name-resolver.component.ts @@ -7,7 +7,10 @@ import {forkJoin} from 'rxjs'; import { ResolverResponse } from '../structure/structure-post-response.model'; import { SubstanceService } from '../substance/substance.service'; import { StructureService } from '../structure/structure.service'; -import {SafeUrl} from '@angular/platform-browser'; +import { ConfigService, ExternalSiteWarning } from '@gsrs-core/config'; +import { OverlayContainer } from '@angular/cdk/overlay'; +import { ExternalSiteWarningDialogComponent } from './external-site-warning-dialog/external-site-warning-dialog.component'; +import { MatDialog } from '@angular/material/dialog'; @Component({ selector: 'app-name-resolver', @@ -22,14 +25,23 @@ export class NameResolverComponent implements OnInit { matchedNames: PagingResponse; @Output() structureSelected = new EventEmitter(); @Input() startingName?: string; + // External site warning dialog for precisionFDA + externalSiteWarning: ExternalSiteWarning; + private overlayContainer: HTMLElement; constructor( + private configService: ConfigService, private loadingService: LoadingService, private substanceService: SubstanceService, - private structureService: StructureService - ) { } + private structureService: StructureService, + private dialog: MatDialog, + private overlayContainerService: OverlayContainer + ) { } ngOnInit() { + this.externalSiteWarning = this.configService.configData.externalSiteWarning; + this.overlayContainer = this.overlayContainerService.getContainerElement(); + if (this.startingName) { this.resolverControl.setValue(this.startingName); setTimeout( () => { @@ -45,6 +57,15 @@ export class NameResolverComponent implements OnInit { } resolveName(name: string): void { + if (this.shouldShowExternalSiteWarningDialog() === true) { + this.showExternalSiteWarningDialog(); + return; + } + + this.resolveNameInternal(name); + } + + resolveNameInternal(name: string): void { this.errorMessage = ''; this.resolvedNames = []; this.matchedNames = null; @@ -71,4 +92,24 @@ export class NameResolverComponent implements OnInit { this.structureSelected.emit(molfile); } + shouldShowExternalSiteWarningDialog(): boolean { + if (!this.externalSiteWarning || !this.externalSiteWarning.enabled) { + return false; + } + return localStorage.getItem('externalSiteWarningDontAskAgain') != 'true'; + } + + showExternalSiteWarningDialog(): void { + const dialogRef = this.dialog.open(ExternalSiteWarningDialogComponent, { + width: '800px', + autoFocus: false + }); + this.overlayContainer.style.zIndex = '1002'; + const dialogSubscription = dialogRef.afterClosed().subscribe(response => { + this.overlayContainer.style.zIndex = null; + if (response) { + this.resolveNameInternal(this.resolverControl.value); + } + }); + } } diff --git a/src/app/core/name-resolver/name-resolver.module.ts b/src/app/core/name-resolver/name-resolver.module.ts index 7f0cce2a0..76240ec5b 100644 --- a/src/app/core/name-resolver/name-resolver.module.ts +++ b/src/app/core/name-resolver/name-resolver.module.ts @@ -8,7 +8,9 @@ import { MatIconModule } from '@angular/material/icon'; import { RouterModule } from '@angular/router'; import { LoadingModule } from '../loading/loading.module'; import { NameResolverDialogComponent } from './name-resolver-dialog.component'; +import { ExternalSiteWarningDialogComponent } from './external-site-warning-dialog/external-site-warning-dialog.component'; import { SubstanceImageModule } from '@gsrs-core/substance/substance-image.module'; +import { MatCheckboxModule } from '@angular/material/checkbox'; @NgModule({ imports: [ @@ -17,6 +19,7 @@ import { SubstanceImageModule } from '@gsrs-core/substance/substance-image.modul FormsModule, MatInputModule, MatButtonModule, + MatCheckboxModule, MatIconModule, RouterModule, LoadingModule, @@ -24,14 +27,16 @@ import { SubstanceImageModule } from '@gsrs-core/substance/substance-image.modul ], declarations: [ NameResolverComponent, - NameResolverDialogComponent + NameResolverDialogComponent, + ExternalSiteWarningDialogComponent ], exports: [ NameResolverComponent, NameResolverDialogComponent ], entryComponents: [ - NameResolverDialogComponent + NameResolverDialogComponent, + ExternalSiteWarningDialogComponent ] }) export class NameResolverModule { } diff --git a/src/app/core/substance-form/references/reference-form.component.html b/src/app/core/substance-form/references/reference-form.component.html index c5ef69d5e..88f1f125b 100644 --- a/src/app/core/substance-form/references/reference-form.component.html +++ b/src/app/core/substance-form/references/reference-form.component.html @@ -40,7 +40,7 @@ -
+
+ -
\ No newline at end of file +
+
+ + + + +
diff --git a/src/app/core/substance-form/submit-success-dialog/submit-success-dialog.component.scss b/src/app/core/substance-form/submit-success-dialog/submit-success-dialog.component.scss index e69de29bb..6c4b2ba41 100644 --- a/src/app/core/substance-form/submit-success-dialog/submit-success-dialog.component.scss +++ b/src/app/core/substance-form/submit-success-dialog/submit-success-dialog.component.scss @@ -0,0 +1,3 @@ +.dialog-actions { + margin-top: 12px; +} diff --git a/src/app/core/substance-form/submit-success-dialog/submit-success-dialog.component.ts b/src/app/core/substance-form/submit-success-dialog/submit-success-dialog.component.ts index a7ea3437b..ef3ed492f 100644 --- a/src/app/core/substance-form/submit-success-dialog/submit-success-dialog.component.ts +++ b/src/app/core/substance-form/submit-success-dialog/submit-success-dialog.component.ts @@ -7,6 +7,9 @@ import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; styleUrls: ['./submit-success-dialog.component.scss'] }) export class SubmitSuccessDialogComponent implements OnInit { + dialogTitle: string; + dialogMessage: string = "Update was performed."; + fileUrl: string = null; public isCoreSubstance = 'true'; public staging = false; @@ -14,7 +17,26 @@ export class SubmitSuccessDialogComponent implements OnInit { constructor( public dialogRef: MatDialogRef, @Inject(MAT_DIALOG_DATA) public data: any - ) { } + ) { + switch (data.type) { + case 'submit': + this.dialogTitle = 'Submission Success'; + this.dialogMessage = 'Substance was submitted successfully'; + break; + case 'approve': + this.dialogTitle = 'Submission Approved'; + this.dialogMessage = 'Substance was approved successfully'; + break; + } + + // If data.fileUrl is not null, this is assumed to be GSRS running within the precisionFDA + // whereby we display a button that reveals the substance file uploaded onto the pFDA environment + if (data.fileUrl) { + this.fileUrl = data.fileUrl; + this.dialogTitle = 'Substance Saved'; + this.dialogMessage = 'The substance was saved successfully as a file in your pFDA My Home area.'; + } + } ngOnInit() { if (this.data) { @@ -27,7 +49,7 @@ export class SubmitSuccessDialogComponent implements OnInit { } } - dismissDialog(action: 'continue' | 'browse' | 'view' | 'home' | 'staging'): void { + dismissDialog(action: 'continue' | 'browse' | 'view' | 'home' | 'staging' | 'viewInPfda'): void { this.dialogRef.close(action); } diff --git a/src/app/core/substance-form/substance-form.component.ts b/src/app/core/substance-form/substance-form.component.ts index 6da494349..7ec705670 100644 --- a/src/app/core/substance-form/substance-form.component.ts +++ b/src/app/core/substance-form/substance-form.component.ts @@ -193,7 +193,7 @@ export class SubstanceFormComponent implements OnInit, AfterViewInit, OnDestroy this.loadingService.setLoading(false); this.router.onSameUrlNavigation = 'reload'; this.router.navigateByUrl('/substances/register/' + response.substance.substanceClass + '?action=import', { state: { record: response.substance } }); - + }, 1000); } } @@ -218,7 +218,7 @@ export class SubstanceFormComponent implements OnInit, AfterViewInit, OnDestroy } }); } - + importDialog(): void { const dialogRef = this.dialog.open(SubstanceEditImportDialogComponent, { @@ -293,15 +293,15 @@ export class SubstanceFormComponent implements OnInit, AfterViewInit, OnDestroy const routeSubscription = this.activatedRoute .params .subscribe(params => { - + const action = this.activatedRoute.snapshot.queryParams['action'] || null; - + if (params['id']) { - + if(action && action === 'import' && window.history.state) { const record = window.history.state; this.imported = true; - + this.getDetailsFromImport(record.record); } else { const id = params['id']; @@ -347,12 +347,12 @@ export class SubstanceFormComponent implements OnInit, AfterViewInit, OnDestroy this.isLoading = false; this.loadingService.setLoading(false); }); - + } }, error => { this.isLoading = false; this.loadingService.setLoading(false); - }); + }); } else { this.copy = this.activatedRoute.snapshot.queryParams['copy'] || null; if (this.copy) { @@ -401,7 +401,7 @@ export class SubstanceFormComponent implements OnInit, AfterViewInit, OnDestroy }); }); - + } @@ -423,7 +423,7 @@ setStructureFromUrl(structure: string, type: string):void { gunzip(t): string{ - + const gezipedData = Buffer.from(t, 'base64') const gzipedDataArray = Uint8Array.from(gezipedData); const ungzipedData = ungzip(gzipedDataArray); @@ -470,8 +470,8 @@ getDrafts() { if (json) { let decode = decodeURI(json); } - - + + const subscription = this.dynamicComponents.changes .subscribe(() => { @@ -593,7 +593,7 @@ getDrafts() { this.regenRefs(); } - + } @@ -670,7 +670,7 @@ getDrafts() { return false; } return true; - + } return false; //default to 'lastEditedBy' if not set in config @@ -690,7 +690,7 @@ getDrafts() { return true; } } - + } showJSON(): void { @@ -902,7 +902,7 @@ getDrafts() { this.loadingService.setLoading(false); this.isLoading = false; this.validationMessages = null; - this.openSuccessDialog('approve'); + this.openSuccessDialog({ type: 'approve' }); this.submissionMessage = 'Substance was approved successfully'; this.showSubmissionMessages = true; this.validationResult = false; @@ -931,7 +931,7 @@ getDrafts() { if (!this.id) { this.id = response.uuid; } - this.openSuccessDialog('staging'); + this.openSuccessDialog({type: 'staging'}); }) } @@ -951,7 +951,7 @@ getDrafts() { if (!this.id) { this.id = response.uuid; } - this.openSuccessDialog(); + this.openSuccessDialog({ type: 'submit', fileUrl: response.fileUrl }); }, (error: SubstanceFormResults) => { this.showSubmissionMessages = true; this.loadingService.setLoading(false); @@ -971,7 +971,7 @@ getDrafts() { }, 8000); } }); - + } } @@ -1094,7 +1094,7 @@ getDrafts() { codeSystem: code }); }) - + const createHolders = defiant.json.search(old, '//*[created]'); for (let i = 0; i < createHolders.length; i++) { const rec = createHolders[i]; @@ -1160,8 +1160,14 @@ getDrafts() { return old; } - openSuccessDialog(type?: string): void { - const dialogRef = this.dialog.open(SubmitSuccessDialogComponent, {data: {'type':type}}); + openSuccessDialog({ type, fileUrl }: { type?: 'submit'|'approve'|'staging', fileUrl?: string }): void { + const dialogRef = this.dialog.open(SubmitSuccessDialogComponent, { + data: { + type: type, + fileUrl: fileUrl + }, + disableClose: true + }); this.overlayContainer.style.zIndex = '1002'; const dialogSubscription = dialogRef.afterClosed().pipe(take(1)).subscribe((response?: 'continue' | 'browse' | 'view' | 'staging') => { @@ -1175,6 +1181,9 @@ getDrafts() { this.router.navigate(['/admin/staging-area']); } else if (response === 'view') { this.router.navigate(['/substances', this.id]); + } else if (response === 'viewInPfda') { + // View the submitted substance file in the user's precisionFDA home + window.location.assign(fileUrl); } else { this.submissionMessage = 'Substance was saved successfully!'; if (type && type === 'approve') { @@ -1222,7 +1231,7 @@ mergeConcept() { saveDraft(auto?: boolean) { const json = this.substanceFormService.cleanSubstance(); const time = new Date().getTime(); - + const uuid = json.uuid ? json.uuid : 'register'; const type = json.substanceClass; let primary = null; @@ -1246,7 +1255,7 @@ mergeConcept() { 'auto': false, 'file': file } - + localStorage.setItem(file, JSON.stringify(draft)); this.draftCount++; @@ -1309,11 +1318,6 @@ mergeConcept() { localStorage.setItem(file, JSON.stringify(draft)); - } - - - - } } diff --git a/src/app/core/substance-form/substance-form.model.ts b/src/app/core/substance-form/substance-form.model.ts index 332b08243..b4e42b844 100644 --- a/src/app/core/substance-form/substance-form.model.ts +++ b/src/app/core/substance-form/substance-form.model.ts @@ -15,6 +15,7 @@ export interface SubstanceFormDefinition { status?: string; approvalID?: string; _name?: string; + _name_html?: string; _nameHTML?: string; relationships?: Array; tags?: Array; @@ -26,6 +27,7 @@ export interface SubstanceFormResults { valid?: boolean; validationMessages?: Array; serverError?: any; + fileUrl?: string; // For precisionFDA } export interface ValidationResults { diff --git a/src/app/core/substance-form/substance-form.service.ts b/src/app/core/substance-form/substance-form.service.ts index 4897f7a3c..93642c907 100644 --- a/src/app/core/substance-form/substance-form.service.ts +++ b/src/app/core/substance-form/substance-form.service.ts @@ -620,6 +620,8 @@ export class SubstanceFormService implements OnDestroy { lastEdited: this.privateSubstance.lastEdited, lastEditedBy: this.privateSubstance.lastEditedBy, _name: this.privateSubstance._name, + _name_html: this.privateSubstance._nameHTML, + tags: this.privateSubstance.tags }; if (this.privateSubstance.status) { @@ -1619,6 +1621,12 @@ export class SubstanceFormService implements OnDestroy { } else if (this.privateSubstance.substanceClass === 'mixture') { this.substanceSubunitsEmitter.next(this.privateSubstance.mixture.components); } + + // For precisionFDA + if (substance.fileUrl) { + results.fileUrl = substance.fileUrl; + } + this.substanceChangeReasonEmitter.next(this.privateSubstance.changeReason); this.substanceService.getSubstanceDetails(results.uuid).subscribe(resp => { this.privateSubstance = resp; diff --git a/src/app/core/substance-selector/substance-selector.component.html b/src/app/core/substance-selector/substance-selector.component.html index cd33373c6..4f7194ed8 100644 --- a/src/app/core/substance-selector/substance-selector.component.html +++ b/src/app/core/substance-selector/substance-selector.component.html @@ -37,7 +37,7 @@ [entityId]="selectedSubstance.uuid">
- Change Selection - - + Change Selection + +
@@ -80,4 +80,4 @@
- \ No newline at end of file + diff --git a/src/app/core/substance-ssg4m/ssg4m-sites/ssg4m-sites.module.ts b/src/app/core/substance-ssg4m/ssg4m-sites/ssg4m-sites.module.ts index 64b6f0ef4..468ba5dfb 100644 --- a/src/app/core/substance-ssg4m/ssg4m-sites/ssg4m-sites.module.ts +++ b/src/app/core/substance-ssg4m/ssg4m-sites/ssg4m-sites.module.ts @@ -10,10 +10,7 @@ import { MatTooltipModule } from '@angular/material/tooltip'; import { MatButtonModule } from '@angular/material/button'; import { MatBadgeModule } from '@angular/material/badge'; import { ScrollToModule } from '../../scroll-to/scroll-to.module'; -// import { SubstanceFormNotesCardComponent } from './substance-form-notes-card.component'; -import { DynamicComponentLoaderModule } from '../../dynamic-component-loader/dynamic-component-loader.module'; import { SubstanceFormModule } from '../../substance-form/substance-form.module'; -// import { SubstanceFormSsg4mSitesCardComponent} from '../ssg4m-sites/ssg4m-sites.module' import { Ssg4mStagesModule } from '../ssg4m-stages/substance-form-ssg4m-stages.module'; import { Ssg4mSitesComponent } from './ssg4m-sites.component'; diff --git a/src/app/core/substance/substance.model.ts b/src/app/core/substance/substance.model.ts index ec52fffa2..405a8440a 100644 --- a/src/app/core/substance/substance.model.ts +++ b/src/app/core/substance/substance.model.ts @@ -79,6 +79,7 @@ export interface SubstanceDetail extends SubstanceBase, SubstanceBaseExtended { specifiedSubstanceG3?: SpecifiedSubstanceG3; specifiedSubstanceG4m?: SpecifiedSubstanceG4m; _matchContext?: MatchContext; + fileUrl?: string; // For precisionFDA } export interface StructurallyDiverse extends SubstanceBase { diff --git a/src/app/core/substances-browse/substance-summary-card/substance-summary-card.component.html b/src/app/core/substances-browse/substance-summary-card/substance-summary-card.component.html index dc9b9f9f2..d11ceb814 100644 --- a/src/app/core/substances-browse/substance-summary-card/substance-summary-card.component.html +++ b/src/app/core/substances-browse/substance-summary-card/substance-summary-card.component.html @@ -1,13 +1,14 @@ - - + + +
- +
Add to List @@ -19,20 +20,20 @@ - + - + - +
{{substance.approvalID}}

- - + +
@@ -105,7 +106,7 @@ - + @@ -247,7 +247,7 @@ + - + - + - +
- +

Results below are an incomplete preview

@@ -667,6 +667,6 @@ [codeSystems]="codes[substance.uuid] && codes[substance.uuid].codeSystems" [userLists] = "userLists"> - +
diff --git a/src/environments/environment.cbg.prod.ts b/src/environments/environment.cbg.prod.ts deleted file mode 100644 index 71a62ee25..000000000 --- a/src/environments/environment.cbg.prod.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { baseEnvironment } from './_base-environment'; - -export const environment = baseEnvironment; -environment.apiBaseUrl = '/gsrs/app/'; -environment.production = true; -environment.baseHref = ''; -environment.clasicBaseHref = '/gsrs/app/'; -environment.appId = 'cbg'; -environment.googleAnalyticsId = 'UA-136176848-1'; - -export { GsrsModule as EnvironmentModule } from '../app/core/gsrs.module'; diff --git a/src/environments/environment.model.ts b/src/environments/environment.model.ts index 6d45ea1be..82c5e56d2 100644 --- a/src/environments/environment.model.ts +++ b/src/environments/environment.model.ts @@ -4,7 +4,7 @@ export interface Environment { baseHref: string; clasicBaseHref: string; production: boolean; - appId: 'fda' | 'gsrs' | 'cbg'; + appId: 'fda' | 'gsrs'; structureEditor: 'ketcher' | 'jsdraw'; googleAnalyticsId: string; version: string;