diff --git a/apps/red-ui/src/app/app-routing.module.ts b/apps/red-ui/src/app/app-routing.module.ts index aac15bf42..1a7fc7bb3 100644 --- a/apps/red-ui/src/app/app-routing.module.ts +++ b/apps/red-ui/src/app/app-routing.module.ts @@ -79,7 +79,7 @@ const routes = [ ]; @NgModule({ - imports: [RouterModule.forRoot(routes)], + imports: [RouterModule.forRoot(routes, { scrollPositionRestoration: 'enabled' })], exports: [RouterModule] }) export class AppRoutingModule {} diff --git a/apps/red-ui/src/app/app.module.ts b/apps/red-ui/src/app/app.module.ts index d89bb2958..0a0221504 100644 --- a/apps/red-ui/src/app/app.module.ts +++ b/apps/red-ui/src/app/app.module.ts @@ -1,7 +1,7 @@ import { BrowserModule } from '@angular/platform-browser'; import { APP_INITIALIZER, NgModule } from '@angular/core'; import { AppComponent } from './app.component'; -import { ActivatedRoute, Router } from '@angular/router'; +import { ActivatedRoute, Router, RouteReuseStrategy } from '@angular/router'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { HTTP_INTERCEPTORS, HttpClient, HttpClientModule } from '@angular/common/http'; import { BaseScreenComponent } from './components/base-screen/base-screen.component'; @@ -30,6 +30,7 @@ import { AppRoutingModule } from './app-routing.module'; import { SharedModule } from './modules/shared/shared.module'; import { FileUploadDownloadModule } from './modules/upload-download/file-upload-download.module'; import { UserProfileScreenComponent } from './components/user-profile/user-profile-screen.component'; +import { CustomRouteReuseStrategy } from './utils/custom-route-reuse.strategy'; export function HttpLoaderFactory(httpClient: HttpClient) { return new TranslateHttpLoader(httpClient, '/assets/i18n/', '.json'); @@ -92,7 +93,8 @@ const components = [AppComponent, LogoComponent, AuthErrorComponent, ToastCompon monthYearA11yLabel: 'YYYY' } } - } + }, + { provide: RouteReuseStrategy, useClass: CustomRouteReuseStrategy } ], bootstrap: [AppComponent] }) diff --git a/apps/red-ui/src/app/modules/projects/projects-routing.module.ts b/apps/red-ui/src/app/modules/projects/projects-routing.module.ts index 2bbe0381d..2cb088ff9 100644 --- a/apps/red-ui/src/app/modules/projects/projects-routing.module.ts +++ b/apps/red-ui/src/app/modules/projects/projects-routing.module.ts @@ -1,5 +1,4 @@ import { NgModule } from '@angular/core'; -import { CommonModule } from '@angular/common'; import { RouterModule } from '@angular/router'; import { ProjectListingScreenComponent } from './screens/project-listing-screen/project-listing-screen.component'; import { CompositeRouteGuard } from '../../guards/composite-route.guard'; @@ -15,7 +14,8 @@ const routes = [ component: ProjectListingScreenComponent, canActivate: [CompositeRouteGuard], data: { - routeGuards: [AuthGuard, RedRoleGuard, AppStateGuard] + routeGuards: [AuthGuard, RedRoleGuard, AppStateGuard], + reuse: true } }, { @@ -23,7 +23,8 @@ const routes = [ component: ProjectOverviewScreenComponent, canActivate: [CompositeRouteGuard], data: { - routeGuards: [AuthGuard, RedRoleGuard, AppStateGuard] + routeGuards: [AuthGuard, RedRoleGuard, AppStateGuard], + reuse: true } }, { @@ -32,6 +33,7 @@ const routes = [ canActivate: [CompositeRouteGuard], data: { routeGuards: [AuthGuard, RedRoleGuard, AppStateGuard] + // reuse: true } } ]; diff --git a/apps/red-ui/src/app/modules/projects/screens/file-preview-screen/file-preview-screen.component.ts b/apps/red-ui/src/app/modules/projects/screens/file-preview-screen/file-preview-screen.component.ts index ada33a26a..5623b6425 100644 --- a/apps/red-ui/src/app/modules/projects/screens/file-preview-screen/file-preview-screen.component.ts +++ b/apps/red-ui/src/app/modules/projects/screens/file-preview-screen/file-preview-screen.component.ts @@ -7,7 +7,6 @@ import { debounce } from '../../../../utils/debounce'; import { MatDialogRef, MatDialogState } from '@angular/material/dialog'; import { ManualRedactionEntryWrapper } from '../../../../models/file/manual-redaction-entry.wrapper'; import { AnnotationWrapper } from '../../../../models/file/annotation.wrapper'; -import { ManualAnnotationService } from '../../services/manual-annotation.service'; import { ManualAnnotationResponse } from '../../../../models/file/manual-annotation-response'; import { AnnotationData, FileDataModel } from '../../../../models/file/file-data.model'; import { FileActionService } from '../../services/file-action.service'; @@ -16,7 +15,6 @@ import { AnnotationProcessingService } from '../../services/annotation-processin import { FilterModel } from '../../../shared/components/filter/model/filter.model'; import { tap } from 'rxjs/operators'; import { NotificationService } from '../../../../services/notification.service'; -import { TranslateService } from '@ngx-translate/core'; import { FileStatusWrapper } from '../../../../models/file/file-status.wrapper'; import { PermissionsService } from '../../../../services/permissions.service'; import { Subscription, timer } from 'rxjs'; @@ -49,7 +47,6 @@ export class FilePreviewScreenComponent implements OnInit, OnDestroy { public analysisProgress: number; public analysisInterval: number; fileData: FileDataModel; - fileId: string; annotationData: AnnotationData; selectedAnnotations: AnnotationWrapper[]; viewReady = false; @@ -60,7 +57,6 @@ export class FilePreviewScreenComponent implements OnInit, OnDestroy { fileReanalysedSubscription: Subscription; hideSkipped = false; public viewDocumentInfo = false; - private projectId: string; private _instance: WebViewerInstance; @ViewChild('fileWorkloadComponent') private _workloadComponent: FileWorkloadComponent; @@ -77,25 +73,17 @@ export class FilePreviewScreenComponent implements OnInit, OnDestroy { private readonly _dialogService: ProjectsDialogService, private readonly _router: Router, private readonly _notificationService: NotificationService, - private readonly _translateService: TranslateService, private readonly _annotationProcessingService: AnnotationProcessingService, private readonly _annotationDrawService: AnnotationDrawService, private readonly _fileActionService: FileActionService, - private readonly _manualAnnotationService: ManualAnnotationService, private readonly _fileDownloadService: PdfViewerDataService, private readonly _formBuilder: FormBuilder, private readonly _statusControllerService: StatusControllerService, - private readonly ngZone: NgZone, + private readonly _ngZone: NgZone, private readonly _fileManagementControllerService: FileManagementControllerService ) { - this._activatedRoute.params.subscribe((params) => { - this.projectId = params.projectId; - this.fileId = params.fileId; - this.appStateService.activateFile(this.projectId, this.fileId); - - this.reviewerForm = this._formBuilder.group({ - reviewer: [this.appStateService.activeFile.currentReviewer] - }); + this.reviewerForm = this._formBuilder.group({ + reviewer: [this.appStateService.activeFile.currentReviewer] }); } @@ -129,6 +117,14 @@ export class FilePreviewScreenComponent implements OnInit, OnDestroy { ); // on less than 3 seconds show indeterminate } + get projectId() { + return this.appStateService.activeProjectId; + } + + get fileId() { + return this.appStateService.activeFileId; + } + updateViewMode() { const allAnnotations = this._instance.annotManager.getAnnotationsList(); @@ -262,7 +258,7 @@ export class FilePreviewScreenComponent implements OnInit, OnDestroy { } openManualAnnotationDialog($event: ManualRedactionEntryWrapper) { - this.ngZone.run(() => { + this._ngZone.run(() => { this.dialogRef = this._dialogService.openManualAnnotationDialog($event, async (response: ManualAnnotationResponse) => { if (response?.annotationId) { const annotation = this.activeViewer.annotManager.getAnnotationById(response.manualRedactionEntryWrapper.rectId); @@ -545,7 +541,7 @@ export class FilePreviewScreenComponent implements OnInit, OnDestroy { private _openFullScreen() { const documentElement = document.documentElement; if (documentElement.requestFullscreen) { - documentElement.requestFullscreen(); + documentElement.requestFullscreen().then(); } } diff --git a/apps/red-ui/src/app/modules/projects/screens/project-listing-screen/project-listing-screen.component.ts b/apps/red-ui/src/app/modules/projects/screens/project-listing-screen/project-listing-screen.component.ts index 5a00e5daf..8ff5697e1 100644 --- a/apps/red-ui/src/app/modules/projects/screens/project-listing-screen/project-listing-screen.component.ts +++ b/apps/red-ui/src/app/modules/projects/screens/project-listing-screen/project-listing-screen.component.ts @@ -1,5 +1,5 @@ import { Component, Injector, OnDestroy, OnInit, ViewChild } from '@angular/core'; -import { FileManagementControllerService, Project, RuleSetModel } from '@redaction/red-ui-http'; +import { Project, RuleSetModel } from '@redaction/red-ui-http'; import { AppStateService } from '../../../../state/app-state.service'; import { UserService } from '../../../../services/user.service'; import { DoughnutChartConfig } from '../../../shared/components/simple-doughnut-chart/simple-doughnut-chart.component'; @@ -16,13 +16,14 @@ import { TranslateService } from '@ngx-translate/core'; import { PermissionsService } from '../../../../services/permissions.service'; import { ProjectWrapper } from '../../../../state/model/project.wrapper'; import { Subscription, timer } from 'rxjs'; -import { tap } from 'rxjs/operators'; +import { filter, tap } from 'rxjs/operators'; import { TranslateChartService } from '../../../../services/translate-chart.service'; import { RedactionFilterSorter } from '../../../../utils/sorters/redaction-filter-sorter'; import { StatusSorter } from '../../../../utils/sorters/status-sorter'; -import { Router } from '@angular/router'; +import { NavigationEnd, NavigationStart, Router } from '@angular/router'; import { FilterComponent } from '../../../shared/components/filter/filter.component'; import { ProjectsDialogService } from '../../services/projects-dialog.service'; +import { CdkVirtualScrollViewport } from '@angular/cdk/scrolling'; import { BaseListingComponent } from '../../../shared/base/base-listing.component'; @Component({ @@ -37,8 +38,6 @@ export class ProjectListingScreenComponent extends BaseListingComponent { await this._appStateService.loadAllProjects(); @@ -86,10 +87,19 @@ export class ProjectListingScreenComponent extends BaseListingComponent { this._calculateData(); }); + + this._router.events.pipe(filter((events) => events instanceof NavigationStart || events instanceof NavigationEnd)).subscribe((event) => { + if (event instanceof NavigationStart && event.url !== '/ui/projects') { + this._lastScrollPosition = this._scrollBar.getOffsetToRenderedContentStart() + this._scrollBar.getRenderedRange().end; + } + if (event instanceof NavigationEnd && event.url === '/ui/projects') { + this._scrollBar.scrollTo({ top: this._lastScrollPosition }); + } + }); } ngOnDestroy(): void { - this.projectAutoUpdateTimer.unsubscribe(); + this._projectAutoUpdateTimer.unsubscribe(); } private _loadEntitiesFromState() { diff --git a/apps/red-ui/src/app/modules/projects/screens/project-overview-screen/project-overview-screen.component.ts b/apps/red-ui/src/app/modules/projects/screens/project-overview-screen/project-overview-screen.component.ts index c68cd34d4..3e0bf8054 100644 --- a/apps/red-ui/src/app/modules/projects/screens/project-overview-screen/project-overview-screen.component.ts +++ b/apps/red-ui/src/app/modules/projects/screens/project-overview-screen/project-overview-screen.component.ts @@ -1,5 +1,5 @@ import { Component, HostListener, Injector, OnDestroy, OnInit, ViewChild } from '@angular/core'; -import { ActivatedRoute, Router } from '@angular/router'; +import { NavigationEnd, NavigationStart, Router } from '@angular/router'; import { NotificationService, NotificationType } from '../../../../services/notification.service'; import { AppStateService } from '../../../../state/app-state.service'; import { FileDropOverlayService } from '../../../upload-download/services/file-drop-overlay.service'; @@ -7,7 +7,6 @@ import { FileUploadModel } from '../../../upload-download/model/file-upload.mode import { FileUploadService } from '../../../upload-download/services/file-upload.service'; import { StatusOverlayService } from '../../../upload-download/services/status-overlay.service'; import { TranslateService } from '@ngx-translate/core'; -import { FileActionService } from '../../services/file-action.service'; import { FilterModel } from '../../../shared/components/filter/model/filter.model'; import * as moment from 'moment'; import { ProjectDetailsComponent } from '../../components/project-details/project-details.component'; @@ -15,15 +14,16 @@ import { FileStatusWrapper } from '../../../../models/file/file-status.wrapper'; import { annotationFilterChecker, keyChecker, processFilters } from '../../../shared/components/filter/utils/filter-utils'; import { PermissionsService } from '../../../../services/permissions.service'; import { UserService } from '../../../../services/user.service'; -import { FileManagementControllerService, FileStatus } from '@redaction/red-ui-http'; +import { FileStatus } from '@redaction/red-ui-http'; import { Subscription, timer } from 'rxjs'; -import { tap } from 'rxjs/operators'; +import { filter, tap } from 'rxjs/operators'; import { RedactionFilterSorter } from '../../../../utils/sorters/redaction-filter-sorter'; import { StatusSorter } from '../../../../utils/sorters/status-sorter'; import { FormGroup } from '@angular/forms'; import { convertFiles, handleFileDrop } from '../../../../utils/file-drop-utils'; import { FilterComponent } from '../../../shared/components/filter/filter.component'; import { ProjectsDialogService } from '../../services/projects-dialog.service'; +import { CdkVirtualScrollViewport } from '@angular/cdk/scrolling'; import { BaseListingComponent } from '../../../shared/base/base-listing.component'; import { ProjectWrapper } from '../../../../state/model/project.wrapper'; @@ -50,34 +50,30 @@ export class ProjectOverviewScreenComponent extends BaseListingComponent { - this._appStateService.activateProject(params.projectId); - this._loadEntitiesFromState(); - }); + this._loadEntitiesFromState(); this._appStateService.fileChanged.subscribe(() => { this.calculateData(); @@ -85,7 +81,7 @@ export class ProjectOverviewScreenComponent extends BaseListingComponent { await this._appStateService.reloadActiveProjectFilesIfNecessary(); @@ -95,11 +91,21 @@ export class ProjectOverviewScreenComponent extends BaseListingComponent events instanceof NavigationStart || events instanceof NavigationEnd)).subscribe((event) => { + if (event instanceof NavigationStart && !event.url.endsWith(this._appStateService.activeProjectId)) { + this._lastScrollPosition = this._scrollBar.getOffsetToRenderedContentStart() + this._scrollBar.getRenderedRange().end; + } + + if (event instanceof NavigationEnd && event.url.endsWith(this._appStateService.activeProjectId)) { + this._scrollBar.scrollTo({ top: this._lastScrollPosition }); + } + }); } ngOnDestroy(): void { this._fileDropOverlayService.cleanupFileDropHandling(); - this.filesAutoUpdateTimer.unsubscribe(); + this._filesAutoUpdateTimer.unsubscribe(); } public get activeProject(): ProjectWrapper { @@ -143,7 +149,7 @@ export class ProjectOverviewScreenComponent extends BaseListingComponent { + if (AppStateService._isFileOverviewRoute(event)) { + const url = (event as ResolveStart).url.replace('/ui/projects/', ''); + const [projectId, , fileId] = url.split('/'); + this.activateFile(projectId, fileId); + } + if (AppStateService._isProjectOverviewRoute(event)) { + const projectId = (event as ResolveStart).url.replace('/ui/projects/', ''); + this.activateProject(projectId); + } + if (AppStateService._isRandomRoute(event)) { + this._appState.activeProjectId = null; + } + }); + } + + private static _isFileOverviewRoute(event: Event) { + return event instanceof ResolveStart && event.url.includes('/ui/projects/') && event.url.includes('/file/'); + } + + private static _isProjectOverviewRoute(event: Event) { + return event instanceof ResolveStart && event.url.includes('/ui/projects/') && !event.url.includes('/file/'); + } + + private static _isRandomRoute(event: Event) { + return event instanceof NavigationEnd && !event.url.includes('/ui/projects/') && !event.url.includes('/file/'); } async reloadActiveProjectFilesIfNecessary() { @@ -327,6 +352,7 @@ export class AppStateService { } activateFile(projectId: string, fileId: string) { + if (this._appState.activeProjectId === projectId && this._appState.activeFileId === fileId) return; this.activateProject(projectId); if (this.activeProject) { this._appState.activeFileId = fileId; diff --git a/apps/red-ui/src/app/utils/custom-route-reuse.strategy.ts b/apps/red-ui/src/app/utils/custom-route-reuse.strategy.ts new file mode 100644 index 000000000..a8836368d --- /dev/null +++ b/apps/red-ui/src/app/utils/custom-route-reuse.strategy.ts @@ -0,0 +1,26 @@ +import { ActivatedRouteSnapshot, DetachedRouteHandle, RouteReuseStrategy } from '@angular/router'; + +export class CustomRouteReuseStrategy implements RouteReuseStrategy { + private _handlers: { [key: string]: DetachedRouteHandle } = {}; + + shouldDetach(route: ActivatedRouteSnapshot): boolean { + return !!route.routeConfig.data?.reuse; + } + + store(route: ActivatedRouteSnapshot, handle: DetachedRouteHandle): void { + if (handle === null) return; + this._handlers[route.toString()] = handle; + } + + shouldAttach(route: ActivatedRouteSnapshot): boolean { + return !!this._handlers[route.toString()]; + } + + retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle { + return this._handlers[route.toString()] as DetachedRouteHandle; + } + + shouldReuseRoute(future: ActivatedRouteSnapshot, current: ActivatedRouteSnapshot): boolean { + return future.routeConfig === current.routeConfig; + } +}