Pull request #161: RED-1307

Merge in RED/ui from RED-1307 to master

* commit '0913e12cfa16036115ff041f022e297a45d30cfb':
  resolve conflicts
  fix fileId and projectId
  add route reuse for projects listing and project overview
  throws undefined error
  activate current file in appStateService
  loading component works, wip
This commit is contained in:
Timo Bejan 2021-04-20 09:58:34 +02:00
commit ff1c79f1a4
9 changed files with 129 additions and 57 deletions

View File

@ -79,7 +79,7 @@ const routes = [
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
imports: [RouterModule.forRoot(routes, { scrollPositionRestoration: 'enabled' })],
exports: [RouterModule]
})
export class AppRoutingModule {}

View File

@ -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]
})

View File

@ -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
}
}
];

View File

@ -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();
}
}

View File

@ -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<ProjectW
public projectsChartData: DoughnutChartConfig[] = [];
public documentsChartData: DoughnutChartConfig[] = [];
public actionMenuOpen: boolean;
public statusFilters: FilterModel[];
public peopleFilters: FilterModel[];
public needsWorkFilters: FilterModel[];
@ -50,7 +49,10 @@ export class ProjectListingScreenComponent extends BaseListingComponent<ProjectW
statusFilters: []
};
private projectAutoUpdateTimer: Subscription;
private _projectAutoUpdateTimer: Subscription;
private _lastScrollPosition: number;
@ViewChild(CdkVirtualScrollViewport) private _scrollBar: CdkVirtualScrollViewport;
@ViewChild('statusFilter') private _statusFilterComponent: FilterComponent;
@ViewChild('peopleFilter') private _peopleFilterComponent: FilterComponent;
@ -58,14 +60,13 @@ export class ProjectListingScreenComponent extends BaseListingComponent<ProjectW
@ViewChild('ruleSetFilter') private _ruleSetFilterComponent: FilterComponent;
constructor(
private readonly _appStateService: AppStateService,
public readonly userService: UserService,
public readonly translateChartService: TranslateChartService,
public readonly permissionsService: PermissionsService,
private readonly _dialogService: ProjectsDialogService,
private readonly _translateService: TranslateService,
private readonly _router: Router,
public readonly translateChartService: TranslateChartService,
private readonly _fileManagementControllerService: FileManagementControllerService,
private readonly _appStateService: AppStateService,
protected readonly _injector: Injector
) {
super(_injector);
@ -74,7 +75,7 @@ export class ProjectListingScreenComponent extends BaseListingComponent<ProjectW
}
public ngOnInit(): void {
this.projectAutoUpdateTimer = timer(0, 10000)
this._projectAutoUpdateTimer = timer(0, 10000)
.pipe(
tap(async () => {
await this._appStateService.loadAllProjects();
@ -86,10 +87,19 @@ export class ProjectListingScreenComponent extends BaseListingComponent<ProjectW
this._appStateService.fileChanged.subscribe(() => {
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() {

View File

@ -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<FileSta
@ViewChild('projectDetailsComponent', { static: false })
private _projectDetailsComponent: ProjectDetailsComponent;
private filesAutoUpdateTimer: Subscription;
private _filesAutoUpdateTimer: Subscription;
private _lastScrollPosition: number;
@ViewChild(CdkVirtualScrollViewport) private _scrollBar: CdkVirtualScrollViewport;
@ViewChild('statusFilter') private _statusFilterComponent: FilterComponent;
@ViewChild('peopleFilter') private _peopleFilterComponent: FilterComponent;
@ViewChild('needsWorkFilter') private _needsWorkFilterComponent: FilterComponent;
constructor(
private readonly _appStateService: AppStateService,
public readonly userService: UserService,
public readonly permissionsService: PermissionsService,
private readonly _activatedRoute: ActivatedRoute,
private readonly _notificationService: NotificationService,
private readonly _dialogService: ProjectsDialogService,
private readonly _fileActionService: FileActionService,
private readonly _fileUploadService: FileUploadService,
private readonly _statusOverlayService: StatusOverlayService,
private readonly _router: Router,
private readonly _translateService: TranslateService,
private readonly _fileDropOverlayService: FileDropOverlayService,
private readonly _fileManagementControllerService: FileManagementControllerService,
private readonly _appStateService: AppStateService,
protected readonly _injector: Injector
) {
super(_injector);
this._activatedRoute.params.subscribe((params) => {
this._appStateService.activateProject(params.projectId);
this._loadEntitiesFromState();
});
this._loadEntitiesFromState();
this._appStateService.fileChanged.subscribe(() => {
this.calculateData();
@ -85,7 +81,7 @@ export class ProjectOverviewScreenComponent extends BaseListingComponent<FileSta
}
ngOnInit(): void {
this.filesAutoUpdateTimer = timer(0, 7500)
this._filesAutoUpdateTimer = timer(0, 7500)
.pipe(
tap(async () => {
await this._appStateService.reloadActiveProjectFilesIfNecessary();
@ -95,11 +91,21 @@ export class ProjectOverviewScreenComponent extends BaseListingComponent<FileSta
.subscribe();
this._fileDropOverlayService.initFileDropHandling();
this.calculateData();
this._router.events.pipe(filter((events) => 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<FileSta
}
private _loadEntitiesFromState() {
this.allEntities = this._appStateService.activeProject.files;
if (this.activeProject) this.allEntities = this.activeProject.files;
}
reloadProjects() {

View File

@ -38,12 +38,16 @@ export class SyncWidthDirective implements AfterViewInit, OnDestroy {
const hasExtraColumns = headerItems.length !== length ? 1 : 0;
for (let idx = 0; idx < length - hasExtraColumns - 1; ++idx) {
headerItems[idx].style.width = `${tableRow.children[idx].getBoundingClientRect().width}px`;
headerItems[idx].style.minWidth = `${tableRow.children[idx].getBoundingClientRect().width}px`;
if (headerItems[idx]) {
headerItems[idx].style.width = `${tableRow.children[idx].getBoundingClientRect().width}px`;
headerItems[idx].style.minWidth = `${tableRow.children[idx].getBoundingClientRect().width}px`;
}
}
for (let idx = length - hasExtraColumns - 1; idx < headerItems.length; ++idx) {
headerItems[idx].style.minWidth = `0`;
if (headerItems[idx]) {
headerItems[idx].style.minWidth = `0`;
}
}
}

View File

@ -3,7 +3,6 @@ import {
DictionaryControllerService,
FileAttributeConfig,
FileAttributesControllerService,
FileManagementControllerService,
FileStatus,
Project,
ProjectControllerService,
@ -16,7 +15,7 @@ import {
} from '@redaction/red-ui-http';
import { NotificationService, NotificationType } from '../services/notification.service';
import { TranslateService } from '@ngx-translate/core';
import { Router } from '@angular/router';
import { Event, NavigationEnd, ResolveStart, Router } from '@angular/router';
import { UserService } from '../services/user.service';
import { forkJoin, Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
@ -50,7 +49,6 @@ export class AppStateService {
constructor(
private readonly _router: Router,
private readonly _userService: UserService,
private readonly _fileManagementControllerService: FileManagementControllerService,
private readonly _projectControllerService: ProjectControllerService,
private readonly _notificationService: NotificationService,
private readonly _reanalysisControllerService: ReanalysisControllerService,
@ -71,6 +69,33 @@ export class AppStateService {
activeDictionaryType: null,
versions: {}
};
this._router.events.subscribe((event: Event) => {
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;

View File

@ -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;
}
}