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