From e69bb94f8f741387fde6c29128e952ef7bfaf936 Mon Sep 17 00:00:00 2001 From: Dan Percic Date: Mon, 9 Jan 2023 21:58:28 +0200 Subject: [PATCH] RED-5908: update viewed pages when a change occurs --- .../file-workload.component.html | 16 ++-- .../file-workload.component.scss | 6 -- .../file-workload/file-workload.component.ts | 14 ---- .../page-indicator.component.ts | 79 ++++++------------- .../components/pages/pages.component.html | 11 +++ .../components/pages/pages.component.scss | 11 +++ .../components/pages/pages.component.ts | 46 +++++++++++ .../file-preview/file-preview.module.ts | 2 + .../services/annotation-processing.service.ts | 6 +- .../services/file-data.service.ts | 19 +++-- .../files/viewed-pages-map.service.ts | 8 ++ .../services/files/viewed-pages.service.ts | 11 +-- libs/red-domain/src/lib/pages/index.ts | 1 + .../src/lib/pages/viewed-page.model.ts | 24 ++++++ libs/red-domain/src/lib/pages/viewed-page.ts | 9 +-- 15 files changed, 159 insertions(+), 104 deletions(-) create mode 100644 apps/red-ui/src/app/modules/file-preview/components/pages/pages.component.html create mode 100644 apps/red-ui/src/app/modules/file-preview/components/pages/pages.component.scss create mode 100644 apps/red-ui/src/app/modules/file-preview/components/pages/pages.component.ts create mode 100644 apps/red-ui/src/app/services/files/viewed-pages-map.service.ts create mode 100644 libs/red-domain/src/lib/pages/viewed-page.model.ts diff --git a/apps/red-ui/src/app/modules/file-preview/components/file-workload/file-workload.component.html b/apps/red-ui/src/app/modules/file-preview/components/file-workload/file-workload.component.html index e5f5dfaae..e9f4b7a15 100644 --- a/apps/red-ui/src/app/modules/file-preview/components/file-workload/file-workload.component.html +++ b/apps/red-ui/src/app/modules/file-preview/components/file-workload/file-workload.component.html @@ -86,16 +86,12 @@ -
- -
+
annotation.manual); - return hasOnlyManualRedactions && this.file.excludedPages.includes(pageNumber); - } - - pageHasSelection(page: number) { - return this.multiSelectService.isActive && !!this.listingService.selected.find(a => a.pageNumber === page); - } - selectAllOnActivePage() { this.listingService.selectAnnotations(this.activeAnnotations); } @@ -286,11 +277,6 @@ export class FileWorkloadComponent extends AutoUnsubscribe implements OnDestroy return firstValueFrom(this.state.file$).then(file => this.pdf.navigateTo(file.numberOfPages)); } - pageSelectedByClick($event: number): void { - this.pagesPanelActive = true; - this.pdf.navigateTo($event); - } - preventKeyDefault($event: KeyboardEvent): void { if (COMMAND_KEY_ARRAY.includes($event.key) && !(($event.target as any).localName === 'input')) { $event.preventDefault(); diff --git a/apps/red-ui/src/app/modules/file-preview/components/page-indicator/page-indicator.component.ts b/apps/red-ui/src/app/modules/file-preview/components/page-indicator/page-indicator.component.ts index 09a7a3a2a..674188379 100644 --- a/apps/red-ui/src/app/modules/file-preview/components/page-indicator/page-indicator.component.ts +++ b/apps/red-ui/src/app/modules/file-preview/components/page-indicator/page-indicator.component.ts @@ -1,59 +1,45 @@ -import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, OnChanges, OnInit, Output } from '@angular/core'; +import { ChangeDetectorRef, Component, EventEmitter, Input, OnChanges, OnInit, Output } from '@angular/core'; import { PermissionsService } from '@services/permissions.service'; -import { ConfigService } from '@services/config.service'; import { ViewedPagesService } from '@services/files/viewed-pages.service'; import { FilePreviewStateService } from '../../services/file-preview-state.service'; import { PageRotationService } from '../../../pdf-viewer/services/page-rotation.service'; -import { ContextComponent } from '@iqser/common-ui'; +import { ContextComponent, getConfig } from '@iqser/common-ui'; import { tap } from 'rxjs/operators'; -import { FileDataService } from '../../services/file-data.service'; +import { AppConfig, ViewedPage } from '@red/domain'; +import { ViewedPagesMapService } from '@services/files/viewed-pages-map.service'; interface PageIndicatorContext { isRotated: boolean; } @Component({ - selector: 'redaction-page-indicator', + selector: 'redaction-page-indicator [number] [read]', templateUrl: './page-indicator.component.html', styleUrls: ['./page-indicator.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush, }) export class PageIndicatorComponent extends ContextComponent implements OnChanges, OnInit { @Input() active = false; @Input() showDottedIcon = false; @Input() number: number; @Input() activeSelection = false; - + @Input() read = false; @Output() readonly pageSelected = new EventEmitter(); pageReadTimeout: number = null; - read = false; isRotated = false; + readonly #config = getConfig(); constructor( private readonly _viewedPagesService: ViewedPagesService, - private readonly _configService: ConfigService, + private readonly _viewedPagesMapService: ViewedPagesMapService, private readonly _changeDetectorRef: ChangeDetectorRef, private readonly _permissionService: PermissionsService, - private readonly _stateService: FilePreviewStateService, - private readonly _fileDataService: FileDataService, + private readonly _state: FilePreviewStateService, readonly pageRotationService: PageRotationService, ) { super(); } - get activePage() { - return this._fileDataService.viewedPages.find(p => p.page === this.number); - } - - get dossierId() { - return this._stateService.dossierId; - } - - get fileId() { - return this._stateService.fileId; - } - ngOnInit() { const isRotated$ = this.pageRotationService.isRotated$(this.number).pipe( tap(value => { @@ -65,24 +51,21 @@ export class PageIndicatorComponent extends ContextComponent { if (this.active && !this.read) { - await this._markPageRead(); + await this.#markPageRead(); } - }, this._configService.values.AUTO_READ_TIME * 1000); + }, this.#config.AUTO_READ_TIME * 1000); } } - private _setReadState() { - const activePage = this.activePage; - if (!activePage) { - this.read = false; - } else { - this.read = !activePage.showAsUnseen; - } - this._changeDetectorRef.markForCheck(); + async #markPageRead() { + const fileId = this._state.fileId; + await this._viewedPagesService.add({ page: this.number }, this._state.dossierId, fileId); + const viewedPage = new ViewedPage({ page: this.number, fileId }); + this._viewedPagesMapService.add(fileId, viewedPage); } - private async _markPageRead() { - await this._viewedPagesService.addPage({ page: this.number }, this.dossierId, this.fileId); - if (this.activePage) { - this.activePage.showAsUnseen = false; - } else { - this._fileDataService.viewedPages.push({ page: this.number, fileId: this.fileId }); - } - this._setReadState(); - } - - private async _markPageUnread() { - await this._viewedPagesService.removePage(this.dossierId, this.fileId, this.number); - const pageToDelete = this._fileDataService.viewedPages.findIndex(p => p.page === this.number); - this._fileDataService.viewedPages.splice(pageToDelete, 1); - this._setReadState(); + async #markPageUnread() { + const fileId = this._state.fileId; + await this._viewedPagesService.remove(this._state.dossierId, fileId, this.number); + this._viewedPagesMapService.delete(fileId, this.number); } } diff --git a/apps/red-ui/src/app/modules/file-preview/components/pages/pages.component.html b/apps/red-ui/src/app/modules/file-preview/components/pages/pages.component.html new file mode 100644 index 000000000..ce9328da0 --- /dev/null +++ b/apps/red-ui/src/app/modules/file-preview/components/pages/pages.component.html @@ -0,0 +1,11 @@ +
+ +
diff --git a/apps/red-ui/src/app/modules/file-preview/components/pages/pages.component.scss b/apps/red-ui/src/app/modules/file-preview/components/pages/pages.component.scss new file mode 100644 index 000000000..4e021bd6d --- /dev/null +++ b/apps/red-ui/src/app/modules/file-preview/components/pages/pages.component.scss @@ -0,0 +1,11 @@ +@use 'common-mixins'; + +:host { + display: contents; +} + +.pages { + @include common-mixins.no-scroll-bar; + overflow: auto; + flex: 1; +} diff --git a/apps/red-ui/src/app/modules/file-preview/components/pages/pages.component.ts b/apps/red-ui/src/app/modules/file-preview/components/pages/pages.component.ts new file mode 100644 index 000000000..fe0c578bf --- /dev/null +++ b/apps/red-ui/src/app/modules/file-preview/components/pages/pages.component.ts @@ -0,0 +1,46 @@ +import { ChangeDetectionStrategy, Component, inject, Input } from '@angular/core'; +import { List } from '@iqser/common-ui'; +import { PdfViewer } from '../../../pdf-viewer/services/pdf-viewer.service'; +import { MultiSelectService } from '../../services/multi-select.service'; +import { AnnotationsListingService } from '../../services/annotations-listing.service'; +import { AnnotationWrapper } from '@models/file/annotation.wrapper'; +import { FilePreviewStateService } from '../../services/file-preview-state.service'; +import { ViewedPagesMapService } from '@services/files/viewed-pages-map.service'; +import { ViewedPage } from '@red/domain'; + +@Component({ + selector: 'redaction-pages [pages] [activePage] [displayedAnnotations]', + templateUrl: './pages.component.html', + styleUrls: ['./pages.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class PagesComponent { + @Input() pages: List; + @Input() activePage: number; + @Input() displayedAnnotations: Map; + + readonly #pdf = inject(PdfViewer); + readonly #state = inject(FilePreviewStateService); + readonly viewedPages$ = inject(ViewedPagesMapService).get$(this.#state.fileId); + readonly #multiSelectService = inject(MultiSelectService); + readonly #listingService = inject(AnnotationsListingService); + + pageSelectedByClick($event: number): void { + this.#pdf.navigateTo($event); + } + + readonly trackBy = (_index: number, item: number) => item; + + pageHasSelection(page: number) { + return this.#multiSelectService.isActive && !!this.#listingService.selected.find(a => a.pageNumber === page); + } + + hasOnlyManualRedactionsAndIsExcluded(pageNumber: number): boolean { + const hasOnlyManualRedactions = this.displayedAnnotations.get(pageNumber)?.every(annotation => annotation.manual); + return hasOnlyManualRedactions && this.#state.file.excludedPages.includes(pageNumber); + } + + getViewedPage(viewedPages: ViewedPage[], pageNumber: number) { + return viewedPages.find(p => p.page === pageNumber); + } +} diff --git a/apps/red-ui/src/app/modules/file-preview/file-preview.module.ts b/apps/red-ui/src/app/modules/file-preview/file-preview.module.ts index 792a0cb22..578db0e0e 100644 --- a/apps/red-ui/src/app/modules/file-preview/file-preview.module.ts +++ b/apps/red-ui/src/app/modules/file-preview/file-preview.module.ts @@ -54,6 +54,7 @@ import { FilePreviewRightContainerComponent } from './components/right-container import { RssDialogComponent } from './dialogs/rss-dialog/rss-dialog.component'; import { ReadonlyBannerComponent } from './components/readonly-banner/readonly-banner.component'; import { SuggestionsService } from './services/suggestions.service'; +import { PagesComponent } from './components/pages/pages.component'; const routes: Routes = [ { @@ -85,6 +86,7 @@ const components = [ AnnotationWrapperComponent, AnnotationsListComponent, PageIndicatorComponent, + PagesComponent, PageExclusionComponent, AnnotationActionsComponent, CommentsComponent, diff --git a/apps/red-ui/src/app/modules/file-preview/services/annotation-processing.service.ts b/apps/red-ui/src/app/modules/file-preview/services/annotation-processing.service.ts index 4611431f2..ee9c58efc 100644 --- a/apps/red-ui/src/app/modules/file-preview/services/annotation-processing.service.ts +++ b/apps/red-ui/src/app/modules/file-preview/services/annotation-processing.service.ts @@ -6,13 +6,15 @@ import { annotationTypesTranslations } from '@translations/annotation-types-tran import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'; import { annotationDefaultColorConfig } from '@red/domain'; import { FilePreviewStateService } from './file-preview-state.service'; -import { FileDataService } from './file-data.service'; import { DefaultColorsService } from '@services/entity-services/default-colors.service'; import { of } from 'rxjs'; +import { ViewedPagesMapService } from '@services/files/viewed-pages-map.service'; +import { FileDataService } from './file-data.service'; @Injectable() export class AnnotationProcessingService { constructor( + private readonly _viewedPagesMapService: ViewedPagesMapService, private readonly _fileDataService: FileDataService, private readonly _state: FilePreviewStateService, private readonly _defaultColorsService: DefaultColorsService, @@ -43,7 +45,7 @@ export class AnnotationProcessingService { checked: false, topLevelFilter: true, checker: (annotation: AnnotationWrapper) => - !this._fileDataService.viewedPages.some(page => page.page === annotation.pageNumber), + !this._viewedPagesMapService.get(this._state.fileId).some(page => page.page === annotation.pageNumber), }, { id: 'pages-without-annotations', diff --git a/apps/red-ui/src/app/modules/file-preview/services/file-data.service.ts b/apps/red-ui/src/app/modules/file-preview/services/file-data.service.ts index 2153b59c6..9b49a34c6 100644 --- a/apps/red-ui/src/app/modules/file-preview/services/file-data.service.ts +++ b/apps/red-ui/src/app/modules/file-preview/services/file-data.service.ts @@ -1,4 +1,4 @@ -import { ChangeType, File, IRedactionLog, IRedactionLogEntry, IViewedPage, ManualRedactionType, ViewMode, ViewModes } from '@red/domain'; +import { ChangeType, File, IRedactionLog, IRedactionLogEntry, ManualRedactionType, ViewMode, ViewModes } from '@red/domain'; import { AnnotationWrapper } from '@models/file/annotation.wrapper'; import { BehaviorSubject, firstValueFrom, iif, Observable, Subject } from 'rxjs'; import { RedactionLogEntry } from '@models/file/redaction-log.entry'; @@ -20,12 +20,12 @@ import { FilesService } from '@services/files/files.service'; import { DefaultColorsService } from '@services/entity-services/default-colors.service'; import { DictionaryService } from '@services/entity-services/dictionary.service'; import { SuggestionsService } from './suggestions.service'; +import { ViewedPagesMapService } from '@services/files/viewed-pages-map.service'; const DELTA_VIEW_TIME = 10 * 60 * 1000; // 10 minutes; @Injectable() export class FileDataService extends EntitiesService { - viewedPages: IViewedPage[] = []; missingTypes = new Set(); readonly hasChangeLog$ = new BehaviorSubject(false); readonly annotations$: Observable; @@ -37,6 +37,7 @@ export class FileDataService extends EntitiesService= 1 ? viableChanges[viableChanges.length - 1] : undefined; const page = redactionLogEntry.positions?.[0].page; - - const viewedPage = this.viewedPages.filter(p => p.page === page).pop(); + const viewedPage = this._viewedPagesMapService.get(file.fileId, page); // page has been seen -> let's see if it's a change if (viewedPage) { @@ -279,7 +280,11 @@ export class FileDataService extends EntitiesService 0) { // at least 1 relevant change - viewedPage.showAsUnseen = dayjs(viewedPage.viewedTime).valueOf() < dayjs(lastChange.dateTime).valueOf(); + const showAsUnseen = dayjs(viewedPage.viewedTime).valueOf() < dayjs(lastChange.dateTime).valueOf(); + if (showAsUnseen) { + this._viewedPagesMapService.delete(this._state.fileId, viewedPage); + } + this.hasChangeLog$.next(true); return { changeLogType: relevantChanges[relevantChanges.length - 1].type, diff --git a/apps/red-ui/src/app/services/files/viewed-pages-map.service.ts b/apps/red-ui/src/app/services/files/viewed-pages-map.service.ts new file mode 100644 index 000000000..76713e0a2 --- /dev/null +++ b/apps/red-ui/src/app/services/files/viewed-pages-map.service.ts @@ -0,0 +1,8 @@ +import { Injectable } from '@angular/core'; +import { IViewedPage, ViewedPage } from '@red/domain'; +import { EntitiesMapService } from '@iqser/common-ui'; + +@Injectable({ providedIn: 'root' }) +export class ViewedPagesMapService extends EntitiesMapService { + protected readonly _primaryKey = 'id'; +} diff --git a/apps/red-ui/src/app/services/files/viewed-pages.service.ts b/apps/red-ui/src/app/services/files/viewed-pages.service.ts index 03ea82bf5..52e2cd20c 100644 --- a/apps/red-ui/src/app/services/files/viewed-pages.service.ts +++ b/apps/red-ui/src/app/services/files/viewed-pages.service.ts @@ -1,7 +1,7 @@ import { Injectable } from '@angular/core'; -import { GenericService, RequiredParam, Validate } from '@iqser/common-ui'; +import { GenericService, mapEach, RequiredParam, Validate } from '@iqser/common-ui'; import { catchError, map } from 'rxjs/operators'; -import { IViewedPage, IViewedPagesRequest } from '@red/domain'; +import { IViewedPage, IViewedPagesRequest, ViewedPage } from '@red/domain'; import { firstValueFrom, of } from 'rxjs'; @Injectable({ @@ -11,22 +11,23 @@ export class ViewedPagesService extends GenericService { protected readonly _defaultModelPath = 'viewedPages'; @Validate() - addPage(@RequiredParam() body: IViewedPagesRequest, @RequiredParam() dossierId: string, @RequiredParam() fileId: string) { + add(@RequiredParam() body: IViewedPagesRequest, @RequiredParam() dossierId: string, @RequiredParam() fileId: string) { const modelPath = `${this._defaultModelPath}/${dossierId}/${fileId}`; return firstValueFrom(this._post(body, modelPath)); } @Validate() - removePage(@RequiredParam() dossierId: string, @RequiredParam() fileId: string, @RequiredParam() page: number) { + remove(@RequiredParam() dossierId: string, @RequiredParam() fileId: string, @RequiredParam() page: number) { const modelPath = `${this._defaultModelPath}/${dossierId}/${fileId}/${page}`; return firstValueFrom(super.delete({}, modelPath)); } @Validate() - getViewedPages(@RequiredParam() dossierId: string, @RequiredParam() fileId: string) { + load(@RequiredParam() dossierId: string, @RequiredParam() fileId: string) { const request = this._getOne<{ pages?: IViewedPage[] }>([dossierId, fileId]).pipe( map(res => res.pages), catchError(() => of([] as IViewedPage[])), + mapEach(page => new ViewedPage(page)), ); return firstValueFrom(request); diff --git a/libs/red-domain/src/lib/pages/index.ts b/libs/red-domain/src/lib/pages/index.ts index 26d32e65d..45fc808a9 100644 --- a/libs/red-domain/src/lib/pages/index.ts +++ b/libs/red-domain/src/lib/pages/index.ts @@ -1,5 +1,6 @@ export * from './page-range'; export * from './viewed-page'; +export * from './viewed-page.model'; export * from './viewed-pages.request'; export * from './page-exclusion.request'; export * from './page-rotation.request'; diff --git a/libs/red-domain/src/lib/pages/viewed-page.model.ts b/libs/red-domain/src/lib/pages/viewed-page.model.ts new file mode 100644 index 000000000..21fb8bc6f --- /dev/null +++ b/libs/red-domain/src/lib/pages/viewed-page.model.ts @@ -0,0 +1,24 @@ +import { Entity } from '@iqser/common-ui'; +import { IViewedPage } from '.'; + +export class ViewedPage extends Entity { + readonly fileId: string; + readonly page: number; + readonly userId?: string; + readonly viewedTime?: string; + + override readonly routerLink = undefined; + override readonly searchKey = ''; + + constructor(viewedPage: IViewedPage) { + super(viewedPage); + this.fileId = viewedPage.fileId; + this.page = viewedPage.page; + this.userId = viewedPage.userId; + this.viewedTime = viewedPage.viewedTime; + } + + override get id() { + return this.page; + } +} diff --git a/libs/red-domain/src/lib/pages/viewed-page.ts b/libs/red-domain/src/lib/pages/viewed-page.ts index 4de1e3c17..0efb7dd23 100644 --- a/libs/red-domain/src/lib/pages/viewed-page.ts +++ b/libs/red-domain/src/lib/pages/viewed-page.ts @@ -1,7 +1,6 @@ export interface IViewedPage { - fileId?: string; - page?: number; - userId?: string; - viewedTime?: string; - showAsUnseen?: boolean; + readonly fileId: string; + readonly page: number; + readonly userId?: string; + readonly viewedTime?: string; }