add new files map service

This commit is contained in:
Dan Percic 2021-11-13 18:56:09 +02:00
parent 9ba4120aa0
commit fc05e3bd42
12 changed files with 235 additions and 209 deletions

View File

@ -51,6 +51,8 @@ import { DossierTemplatesService } from '@services/entity-services/dossier-templ
import { LongPressEvent } from '@shared/directives/long-press.directive';
import { UserPreferenceService } from '@services/user-preference.service';
import { saveAsCSV } from '@utils/csv-utils';
import { FilesService } from '@services/entity-services/files.service';
import { FilesMapService } from '@services/entity-services/files-map.service';
@Component({
templateUrl: './dossier-overview-screen.component.html',
@ -70,7 +72,7 @@ export class DossierOverviewScreenComponent extends ListingComponent<File> imple
analysisForced: boolean;
displayedInFileListAttributes: IFileAttributeConfig[] = [];
displayedAttributes: IFileAttributeConfig[] = [];
readonly workflowConfig: WorkflowConfig<File, FileStatus> = this._configService.workflowConfig(() => this.reloadDossiers());
readonly workflowConfig: WorkflowConfig<File, FileStatus> = this._configService.workflowConfig(() => this.reloadFiles());
readonly actionConfigs: readonly ActionConfig[];
readonly dossier$: Observable<Dossier>;
readonly dossierId: string;
@ -102,6 +104,8 @@ export class DossierOverviewScreenComponent extends ListingComponent<File> imple
private readonly _fileAttributesService: FileAttributesService,
private readonly _configService: ConfigService,
private readonly _userPreferenceService: UserPreferenceService,
private readonly _filesService: FilesService,
private readonly _fileMapService: FilesMapService,
activatedRoute: ActivatedRoute,
) {
super(_injector);
@ -109,6 +113,11 @@ export class DossierOverviewScreenComponent extends ListingComponent<File> imple
this.actionConfigs = this._configService.actionConfig(this.dossierId);
this.dossier$ = this._dossiersService.getEntityChanged$(this.dossierId);
this.currentDossier = this._dossiersService.find(this.dossierId);
this.fileAttributeConfigs = this._fileAttributesService.getFileAttributeConfig(
this.currentDossier.dossierTemplateId,
)?.fileAttributeConfigs;
this.tableColumnConfigs = this._configService.tableConfig(this.displayedAttributes);
}
private _fileAttributeConfigs: IFileAttributeConfig[];
@ -129,10 +138,10 @@ export class DossierOverviewScreenComponent extends ListingComponent<File> imple
async actionPerformed(action?: string, file?: File) {
if (action === 'assign-reviewer') {
return this.reloadDossiers();
return this.reloadFiles();
}
this.calculateData();
await this.calculateData();
if (action === 'navigate') {
await this._router.navigate([file.routerLink]);
@ -144,30 +153,20 @@ export class DossierOverviewScreenComponent extends ListingComponent<File> imple
lastOpenedFn = (fileStatus: File) => fileStatus.lastOpened;
async ngOnInit(): Promise<void> {
this._loadingService.start();
this._loadEntitiesFromState();
this.fileAttributeConfigs = this._fileAttributesService.getFileAttributeConfig(
this.currentDossier.dossierTemplateId,
)?.fileAttributeConfigs;
this.tableColumnConfigs = this._configService.tableConfig(this.displayedAttributes);
await this._loadEntitiesFromState();
try {
this._fileDropOverlayService.initFileDropHandling();
this.calculateData();
await this.calculateData();
this.addSubscription = timer(0, 20 * 1000).subscribe(async () => {
await this._appStateService.reloadActiveDossierFiles();
this.calculateData();
await this.reloadFiles();
});
this.addSubscription = this.listingMode$.subscribe(() => {
this._computeAllFilters();
});
// this.addSubscription = this._appStateService.fileChanged$.subscribe(() => {
// this.calculateData();
// });
this.addSubscription = this._dossierTemplatesService.entityChanged$.subscribe(() => {
this.fileAttributeConfigs = this._fileAttributesService.getFileAttributeConfig(
this.currentDossier.dossierTemplateId,
@ -192,8 +191,8 @@ export class DossierOverviewScreenComponent extends ListingComponent<File> imple
}
async ngOnAttach() {
await this._appStateService.reloadActiveDossierFiles();
this._loadEntitiesFromState();
// await this._appStateService.reloadActiveDossierFiles();
// await this._loadEntitiesFromState();
await this.ngOnInit();
this._tableComponent.scrollViewport.scrollToIndex(this._lastScrolledIndex, 'smooth');
}
@ -209,24 +208,25 @@ export class DossierOverviewScreenComponent extends ListingComponent<File> imple
async reanalyseDossier() {
try {
await this._appStateService.reanalyzeDossier();
await this.reloadDossiers();
await this.reloadFiles();
this._toaster.success(_('dossier-overview.reanalyse-dossier.success'));
} catch (e) {
this._toaster.error(_('dossier-overview.reanalyse-dossier.error'));
}
}
async reloadDossiers() {
await this._appStateService.getFiles(this.currentDossier, false);
this.calculateData();
async reloadFiles() {
const files = await this._appStateService.getFiles(this.currentDossier);
this.entitiesService.setEntities(files);
await this.calculateData();
}
calculateData(): void {
async calculateData(): Promise<void> {
if (!this.dossierId) {
return;
}
this._loadEntitiesFromState();
await this._loadEntitiesFromState();
this._computeAllFilters();
this._changeDetectorRef.markForCheck();
@ -278,18 +278,18 @@ export class DossierOverviewScreenComponent extends ListingComponent<File> imple
}
async bulkActionPerformed(): Promise<void> {
await this.reloadDossiers();
await this.reloadFiles();
}
openAssignDossierMembersDialog(): void {
const data = { dossierId: this.dossierId, section: 'members' };
this._dialogService.openDialog('editDossier', null, data, async () => this.reloadDossiers());
this._dialogService.openDialog('editDossier', null, data, async () => this.reloadFiles());
}
openDossierDictionaryDialog() {
const data = { dossierId: this.dossierId, section: 'dossierDictionary' };
this._dialogService.openDialog('editDossier', null, data, async () => {
await this.reloadDossiers();
await this.reloadFiles();
});
}
@ -300,11 +300,14 @@ export class DossierOverviewScreenComponent extends ListingComponent<File> imple
recentlyModifiedChecker = (file: File) =>
moment(file.lastUpdated).add(this._appConfigService.values.RECENT_PERIOD_IN_HOURS, 'hours').isAfter(moment());
private _loadEntitiesFromState() {
private async _loadEntitiesFromState() {
this.currentDossier = this._dossiersService.find(this.dossierId);
if (this.currentDossier) {
this.entitiesService.setEntities([...this.currentDossier.files]);
if (!this._fileMapService.has(this.dossierId)) {
this._loadingService.start();
await this._appStateService.getFiles(this.currentDossier);
}
this.entitiesService.setEntities(this._fileMapService.get(this.dossierId));
}
private async _uploadFiles(files: FileUploadModel[]) {

View File

@ -22,6 +22,7 @@ import { DefaultListingServicesTmp, EntitiesService, ListingComponent, OnAttach,
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { ConfigService } from '../config.service';
import { DossiersService } from '@services/entity-services/dossiers.service';
import { FilesService } from '@services/entity-services/files.service';
@Component({
templateUrl: './dossiers-listing-screen.component.html',
@ -59,6 +60,7 @@ export class DossiersListingScreenComponent
private readonly _dialogService: DossiersDialogService,
private readonly _translateChartService: TranslateChartService,
private readonly _configService: ConfigService,
private readonly _filesService: FilesService,
) {
super(_injector);
}

View File

@ -1,4 +1,4 @@
<section *ngIf="appStateService.activeFile as file" [class.fullscreen]="fullScreen">
<section *ngIf="file$ | async as file" [class.fullscreen]="fullScreen">
<div class="page-header">
<div class="flex flex-1">
<div
@ -29,21 +29,21 @@
</div>
</div>
<div *ngIf="dossiersService.activeDossier$ | async as dossier" class="flex-1 actions-container">
<div *ngIf="dossier$ | async as dossier" class="flex-1 actions-container">
<ng-container *ngIf="!file.excluded">
<ng-container *ngIf="!file.isProcessing">
<iqser-status-bar [configs]="statusBarConfig" [small]="true"></iqser-status-bar>
<div class="all-caps-label mr-16 ml-8">
{{ translations[status] | translate }}
{{ translations[file.status] | translate }}
<span *ngIf="isUnderReviewOrApproval">{{ 'by' | translate }}:</span>
</div>
</ng-container>
<redaction-initials-avatar
*ngIf="!editingReviewer"
[user]="currentReviewer"
[withName]="!!currentReviewer"
[user]="file.currentReviewer"
[withName]="!!file.currentReviewer"
tooltipPosition="below"
></redaction-initials-avatar>
<div
@ -59,13 +59,13 @@
(save)="assignReviewer($event)"
*ngIf="editingReviewer"
[options]="singleUsersSelectOptions(dossier)"
[value]="currentReviewer"
[value]="file.currentReviewer"
></redaction-assign-user-dropdown>
<div *ngIf="canAssign" class="assign-actions-wrapper">
<div *ngIf="canAssign(file)" class="assign-actions-wrapper">
<iqser-circle-button
(action)="editingReviewer = true"
*ngIf="(permissionsService.canAssignUser() || permissionsService.canUnassignUser()) && !!currentReviewer"
*ngIf="(permissionsService.canAssignUser(file) || permissionsService.canUnassignUser(file)) && !!file.currentReviewer"
[tooltip]="assignTooltip"
icon="iqser:edit"
tooltipPosition="below"
@ -73,17 +73,17 @@
<iqser-circle-button
(action)="assignToMe()"
*ngIf="permissionsService.canAssignToSelf()"
*ngIf="permissionsService.canAssignToSelf(file)"
[tooltip]="'file-preview.assign-me' | translate"
icon="red:assign-me"
tooltipPosition="below"
></iqser-circle-button>
</div>
<ng-container *ngIf="permissionsService.isApprover() && !!lastReviewer">
<ng-container *ngIf="permissionsService.isApprover(dossier) && !!file.lastReviewer">
<div class="vertical-line"></div>
<div class="all-caps-label mr-16 ml-8" translate="file-preview.last-reviewer"></div>
<redaction-initials-avatar [user]="lastReviewer" [withName]="true"></redaction-initials-avatar>
<redaction-initials-avatar [user]="file.lastReviewer" [withName]="true"></redaction-initials-avatar>
</ng-container>
<div class="vertical-line"></div>
@ -92,6 +92,7 @@
(actionPerformed)="fileActionPerformed($event)"
[activeDocumentInfo]="viewDocumentInfo"
[activeExcludePages]="excludePages"
[file]="file"
type="file-preview"
></redaction-file-actions>

View File

@ -1,4 +1,14 @@
import { ChangeDetectorRef, Component, HostListener, NgZone, OnDestroy, OnInit, TemplateRef, ViewChild } from '@angular/core';
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
HostListener,
NgZone,
OnDestroy,
OnInit,
TemplateRef,
ViewChild,
} from '@angular/core';
import { ActivatedRoute, ActivatedRouteSnapshot, NavigationExtras, Router } from '@angular/router';
import { AppStateService } from '@state/app-state.service';
import { Core, WebViewerInstance } from '@pdftron/webviewer';
@ -23,9 +33,9 @@ import { AnnotationData, FileDataModel } from '@models/file/file-data.model';
import { FileActionService } from '../../shared/services/file-action.service';
import { AnnotationDrawService } from '../../services/annotation-draw.service';
import { AnnotationProcessingService } from '../../services/annotation-processing.service';
import { Dossier, FileStatus, User, ViewMode } from '@red/domain';
import { Dossier, File, FileStatus, User, ViewMode } from '@red/domain';
import { PermissionsService } from '@services/permissions.service';
import { timer } from 'rxjs';
import { Observable, timer } from 'rxjs';
import { UserPreferenceService } from '@services/user-preference.service';
import { UserService } from '@services/user.service';
import { PdfViewerDataService } from '../../services/pdf-viewer-data.service';
@ -42,6 +52,7 @@ import { FilesService } from '@services/entity-services/files.service';
import { DossiersService } from '@services/entity-services/dossiers.service';
import { FileManagementService } from '../../shared/services/file-management.service';
import { filter, switchMapTo, tap } from 'rxjs/operators';
import { FilesMapService } from '@services/entity-services/files-map.service';
import Annotation = Core.Annotations.Annotation;
const ALL_HOTKEY_ARRAY = ['Escape', 'F', 'f'];
@ -50,6 +61,7 @@ const ALL_HOTKEY_ARRAY = ['Escape', 'F', 'f'];
templateUrl: './file-preview-screen.component.html',
styleUrls: ['./file-preview-screen.component.scss'],
providers: [FilterService],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnInit, OnDestroy, OnAttach, OnDetach {
readonly circleButtonTypes = CircleButtonTypes;
@ -71,6 +83,10 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
@ViewChild(PdfViewerComponent) readonly viewerComponent: PdfViewerComponent;
@ViewChild('fileActions') fileActions: FileActionsComponent;
readonly dossierId: string;
readonly dossier$: Observable<Dossier>;
readonly file$: Observable<File>;
readonly fileId: string;
file: File;
private _instance: WebViewerInstance;
private _lastPage: string;
private _reloadFileOnReanalysis = false;
@ -102,10 +118,16 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
private readonly _loadingService: LoadingService,
private readonly _filterService: FilterService,
private readonly _translateService: TranslateService,
private readonly _filesMapService: FilesMapService,
private readonly _dossiersService: DossiersService,
) {
super();
this.dossierId = _activatedRoute.snapshot.paramMap.get('dossierId');
this._loadingService.start();
this.dossierId = _activatedRoute.snapshot.paramMap.get('dossierId');
this.dossier$ = this._dossiersService.getEntityChanged$(this.dossierId);
this.fileId = _activatedRoute.snapshot.paramMap.get('fileId');
this.file = _filesMapService.get(this.dossierId, this.fileId);
this.file$ = _filesMapService.watch$(this.dossierId, this.fileId);
document.documentElement.addEventListener('fullscreenchange', () => {
if (!document.fullscreenElement) {
this.fullScreen = false;
@ -114,7 +136,7 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
}
get assignTooltip(): string {
return this.appStateService.activeFile.isUnderApproval
return this.file.isUnderApproval
? this._translateService.instant('dossier-overview.assign-approver')
: this.assignOrChangeReviewerTooltip;
}
@ -147,62 +169,44 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
return this.fileData?.hasChangeLog;
}
get canAssign(): boolean {
return (
!this.editingReviewer &&
(this.permissionsService.canAssignUser() ||
this.permissionsService.canAssignToSelf() ||
this.permissionsService.canUnassignUser())
);
}
get displayData(): Blob {
return this.fileData?.fileData;
}
get fileId(): string {
return this.appStateService.activeFileId;
}
get multiSelectActive(): boolean {
return !!this._workloadComponent?.multiSelectActive;
}
get lastReviewer(): string | undefined {
return this.appStateService.activeFile.lastReviewer;
}
get assignOrChangeReviewerTooltip(): string {
return this.currentReviewer
return this.file.currentReviewer
? this._translateService.instant('file-preview.change-reviewer')
: this._translateService.instant('file-preview.assign-reviewer');
}
get currentReviewer(): string {
return this.appStateService.activeFile.currentReviewer;
}
get status(): FileStatus {
return this.appStateService.activeFile.status;
}
get statusBarConfig(): [{ length: number; color: FileStatus }] {
return [{ length: 1, color: this.status }];
return [{ length: 1, color: this.file.status }];
}
get isUnderReviewOrApproval(): boolean {
return this.status === 'UNDER_REVIEW' || this.status === 'UNDER_APPROVAL';
return this.file.status === 'UNDER_REVIEW' || this.file.status === 'UNDER_APPROVAL';
}
canAssign(file: File): boolean {
return (
!this.editingReviewer &&
(this.permissionsService.canAssignUser(file) ||
this.permissionsService.canAssignToSelf(file) ||
this.permissionsService.canUnassignUser(file))
);
}
singleUsersSelectOptions(dossier: Dossier): List {
const unassignUser = this.permissionsService.canUnassignUser() ? [undefined] : [];
return this.appStateService.activeFile?.isUnderApproval
? [...dossier.approverIds, ...unassignUser]
: [...dossier.memberIds, ...unassignUser];
const unassignUser = this.permissionsService.canUnassignUser(this.file) ? [undefined] : [];
return this.file?.isUnderApproval ? [...dossier.approverIds, ...unassignUser] : [...dossier.memberIds, ...unassignUser];
}
canAssignReviewer(dossier: Dossier): boolean {
return !this.currentReviewer && this.permissionsService.canAssignUser() && dossier.hasReviewers;
return !this.file.currentReviewer && this.permissionsService.canAssignUser(this.file) && dossier.hasReviewers;
}
updateViewMode(): void {
@ -246,7 +250,7 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
}
async ngOnAttach(previousRoute: ActivatedRouteSnapshot): Promise<boolean> {
if (!this.appStateService.activeFile.canBeOpened) {
if (!this.file.canBeOpened) {
return this._router.navigate([this.dossiersService.find(this.dossierId)?.routerLink]);
}
@ -280,8 +284,9 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
}
console.log(`[REDACTION] Delete previous annotations time: ${new Date().getTime() - startTime} ms`);
const processStartTime = new Date().getTime();
const dossier = this._dossiersService.find(this.dossierId);
const newAnnotationsData = this.fileData.getAnnotations(
this.appStateService.dictionaryData[this.dossiersService.activeDossier.dossierTemplateId],
this.appStateService.dictionaryData[dossier.dossierTemplateId],
this.userService.currentUser,
this.viewMode,
this.userPreferenceService.areDevFeaturesEnabled,
@ -456,7 +461,7 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
break;
case 'delete':
return this.dossiersService.goToActiveDossier();
return this._router.navigate([this.dossiersService.find(this.dossierId).routerLink]);
case 'reanalyse':
await this._loadFileData(true);
@ -600,7 +605,7 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
}
private async _loadFileData(performUpdate = false): Promise<void> {
const fileData = await this._fileDownloadService.loadActiveFileData().toPromise();
const fileData = await this._fileDownloadService.loadDataFor(this.file).toPromise();
if (!fileData.file?.isPending && !fileData.file?.isError) {
if (performUpdate) {
@ -617,7 +622,7 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
}
if (fileData.file.isError) {
await this.dossiersService.goToActiveDossier();
await this._router.navigate([this.dossiersService.find(this.dossierId).routerLink]);
}
}
@ -630,7 +635,7 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
/* Get the documentElement (<html>) to display the page in fullscreen */
private _cleanupAndRedrawManualAnnotations$() {
return this._fileDownloadService.loadActiveFileRedactionLog().pipe(
return this._fileDownloadService.loadRedactionLogFor(this.dossierId, this.fileId).pipe(
tap(redactionLog => (this.fileData.redactionLog = redactionLog)),
switchMapTo(this._redrawAnnotations()),
);
@ -641,7 +646,7 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
const currentPageAnnotationIds = currentPageAnnotations.map(a => a.id);
this.fileData.file = await this.appStateService.reloadActiveFile();
this.fileData.redactionLog = await this._fileDownloadService.loadActiveFileRedactionLog().toPromise();
this.fileData.redactionLog = await this._fileDownloadService.loadRedactionLogFor(this.dossierId, this.fileId).toPromise();
this.rebuildFilters();

View File

@ -2,10 +2,8 @@ import { Injectable } from '@angular/core';
import { forkJoin, Observable, of } from 'rxjs';
import { catchError, map, tap } from 'rxjs/operators';
import { FileDataModel } from '@models/file/file-data.model';
import { AppStateService } from '@state/app-state.service';
import { PermissionsService } from '@services/permissions.service';
import { File } from '@red/domain';
import { DossiersService } from '@services/entity-services/dossiers.service';
import { FileManagementService } from '../shared/services/file-management.service';
import { RedactionLogService } from './redaction-log.service';
import { ViewedPagesService } from '../shared/services/viewed-pages.service';
@ -13,36 +11,30 @@ import { ViewedPagesService } from '../shared/services/viewed-pages.service';
@Injectable()
export class PdfViewerDataService {
constructor(
private readonly _appStateService: AppStateService,
private readonly _dossiersService: DossiersService,
private readonly _permissionsService: PermissionsService,
private readonly _fileManagementService: FileManagementService,
private readonly _redactionLogService: RedactionLogService,
private readonly _viewedPagesService: ViewedPagesService,
) {}
loadActiveFileRedactionLog() {
return this._redactionLogService.getRedactionLog(this._dossiersService.activeDossierId, this._appStateService.activeFileId).pipe(
loadRedactionLogFor(dossierId: string, fileId: string) {
return this._redactionLogService.getRedactionLog(dossierId, fileId).pipe(
tap(redactionLog => redactionLog.redactionLogEntry.sort((a, b) => a.positions[0].page - b.positions[0].page)),
catchError(() => of({})),
);
}
loadActiveFileData(): Observable<FileDataModel> {
const file$ = this.downloadOriginalFile(this._appStateService.activeFile);
const reactionLog$ = this.loadActiveFileRedactionLog();
const viewedPages$ = this.getViewedPagesForActiveFile();
loadDataFor(file: File): Observable<FileDataModel> {
const file$ = this.downloadOriginalFile(file);
const reactionLog$ = this.loadRedactionLogFor(file.dossierId, file.fileId);
const viewedPages$ = this.getViewedPagesFor(file);
return forkJoin([file$, reactionLog$, viewedPages$]).pipe(
map(data => new FileDataModel(this._appStateService.activeFile, ...data)),
);
return forkJoin([file$, reactionLog$, viewedPages$]).pipe(map(data => new FileDataModel(file, ...data)));
}
getViewedPagesForActiveFile() {
if (this._permissionsService.canMarkPagesAsViewed()) {
return this._viewedPagesService
.getViewedPages(this._dossiersService.activeDossierId, this._appStateService.activeFileId)
.pipe(catchError(() => of([])));
getViewedPagesFor(file: File) {
if (this._permissionsService.canMarkPagesAsViewed(file)) {
return this._viewedPagesService.getViewedPages(file.dossierId, file.fileId).pipe(catchError(() => of([])));
}
return of([]);
}

View File

@ -1,4 +1,4 @@
import { Component, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Output, SimpleChanges } from '@angular/core';
import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core';
import { PermissionsService } from '@services/permissions.service';
import { File, FileStatus } from '@red/domain';
import { AppStateService } from '@state/app-state.service';
@ -9,29 +9,33 @@ import {
CircleButtonTypes,
ConfirmationDialogInput,
LoadingService,
OnChange,
Required,
StatusBarConfig,
Toaster,
} from '@iqser/common-ui';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { UserService } from '@services/user.service';
import { filter } from 'rxjs/operators';
import { UserPreferenceService } from '@services/user-preference.service';
import { LongPressEvent } from '@shared/directives/long-press.directive';
import { FileActionService } from '../../services/file-action.service';
import { DossiersService } from '@services/entity-services/dossiers.service';
import { FileManagementService } from '../../services/file-management.service';
import { FilesMapService } from '@services/entity-services/files-map.service';
@Component({
selector: 'redaction-file-actions',
templateUrl: './file-actions.component.html',
styleUrls: ['./file-actions.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class FileActionsComponent extends AutoUnsubscribe implements OnInit, OnDestroy, OnChanges {
export class FileActionsComponent extends AutoUnsubscribe implements OnInit, OnDestroy {
readonly circleButtonTypes = CircleButtonTypes;
readonly currentUser = this._userService.currentUser;
@Input() file: File;
@Input()
@OnChange<File, FileActionsComponent>('setup')
file: File;
@Input() activeDocumentInfo: boolean;
@Input() activeExcludePages: boolean;
@Input() @Required() type: 'file-preview' | 'dossier-overview-list' | 'dossier-overview-workflow';
@ -67,6 +71,7 @@ export class FileActionsComponent extends AutoUnsubscribe implements OnInit, OnD
private readonly _fileActionService: FileActionService,
private readonly _loadingService: LoadingService,
private readonly _fileManagementService: FileManagementService,
private readonly _filesMapService: FilesMapService,
private readonly _userService: UserService,
private readonly _toaster: Toaster,
private readonly _userPreferenceService: UserPreferenceService,
@ -99,17 +104,14 @@ export class FileActionsComponent extends AutoUnsubscribe implements OnInit, OnD
}
ngOnInit(): void {
if (!this.file) {
this.file = this.appStateService.activeFile;
}
this._setup();
this.addSubscription = this.appStateService.fileChanged$.pipe(filter(file => file.fileId === this.file?.fileId)).subscribe(file => {
this.setup();
this.addSubscription = this._filesMapService.watch$(this.file.dossierId, this.file.fileId).subscribe(file => {
this.file = file;
this._setup();
this.setup();
});
this.addSubscription = this.dossiersService.entityChanged$.subscribe(() => {
this._setup();
this.addSubscription = this.dossiersService.getEntityChanged$(this.file.dossierId).subscribe(() => {
this.setup();
});
}
@ -160,7 +162,7 @@ export class FileActionsComponent extends AutoUnsubscribe implements OnInit, OnD
$event.stopPropagation();
await this._fileActionService.assignToMe([this.file], () => {
this.reloadDossiers('reanalyse');
this.reloadFiles('reanalyse');
});
}
@ -169,22 +171,22 @@ export class FileActionsComponent extends AutoUnsubscribe implements OnInit, OnD
$event.stopPropagation();
}
this.addSubscription = this._fileActionService.reanalyseFile(this.file).subscribe(() => {
this.reloadDossiers('reanalyse');
this.reloadFiles('reanalyse');
});
}
setFileUnderApproval($event: MouseEvent) {
$event.stopPropagation();
if (this.dossiersService.activeDossier.approverIds.length > 1) {
this._fileActionService.assignFile('approver', $event, this.file, () => this.reloadDossiers('assign-reviewer'), true);
this._fileActionService.assignFile('approver', $event, this.file, () => this.reloadFiles('assign-reviewer'), true);
} else {
this.addSubscription = this._fileActionService.setFilesUnderApproval([this.file]).subscribe(() => {
this.reloadDossiers('set-under-approval');
this.reloadFiles('set-under-approval');
});
}
}
setFileApproved($event: MouseEvent) {
async setFileApproved($event: MouseEvent) {
$event.stopPropagation();
if (this.file.hasUpdates) {
this._dialogService.openDialog(
@ -194,61 +196,43 @@ export class FileActionsComponent extends AutoUnsubscribe implements OnInit, OnD
title: _('confirmation-dialog.approve-file.title'),
question: _('confirmation-dialog.approve-file.question'),
}),
() => {
this._setFileApproved();
async () => {
await this._setFileApproved();
},
);
} else {
this._setFileApproved();
await this._setFileApproved();
}
}
ocrFile($event: MouseEvent) {
$event.stopPropagation();
this.addSubscription = this._fileActionService.ocrFiles([this.file]).subscribe(() => {
this.reloadDossiers('ocr-file');
this.reloadFiles('ocr-file');
});
}
setFileUnderReview($event: MouseEvent, ignoreDialogChanges = false) {
this._fileActionService.assignFile(
'reviewer',
$event,
this.file,
() => this.reloadDossiers('assign-reviewer'),
ignoreDialogChanges,
);
this._fileActionService.assignFile('reviewer', $event, this.file, () => this.reloadFiles('assign-reviewer'), ignoreDialogChanges);
}
reloadDossiers(action: string) {
this.appStateService.getFiles().then(() => {
reloadFiles(action: string) {
this.appStateService.getFiles(this.dossiersService.find(this.file.dossierId)).then(() => {
this.actionPerformed.emit(action);
});
}
async toggleAnalysis() {
await this._fileActionService.toggleAnalysis(this.file).toPromise();
await this.appStateService.getFiles();
await this.appStateService.getFiles(this.dossiersService.find(this.file.dossierId));
this.actionPerformed.emit(this.file?.excluded ? 'enable-analysis' : 'disable-analysis');
}
ngOnChanges(changes: SimpleChanges): void {
if (changes.file) {
this._setup();
}
}
forceReanalysisAction($event: LongPressEvent) {
this.analysisForced = !$event.touchEnd && this._userPreferenceService.areDevFeaturesEnabled;
}
private _setFileApproved() {
this.addSubscription = this._fileActionService.setFilesApproved([this.file]).subscribe(() => {
this.reloadDossiers('set-approved');
});
}
private _setup() {
setup() {
this.statusBarConfig = [{ color: this.file.status, length: 1 }];
this.tooltipPosition = this.isFilePreview ? 'below' : 'above';
this.assignTooltip = this.file.isUnderApproval ? _('dossier-overview.assign-approver') : _('dossier-overview.assign-reviewer');
@ -261,7 +245,8 @@ export class FileActionsComponent extends AutoUnsubscribe implements OnInit, OnD
this.showApprove = this.permissionsService.isReadyForApproval(this.file) && !this.isDossierOverviewWorkflow;
this.canToggleAnalysis = this.permissionsService.canToggleAnalysis(this.file);
this.showDelete = this.permissionsService.canDeleteFile(this.file);
const dossier = this.dossiersService.find(this.file.dossierId);
this.showDelete = this.permissionsService.canDeleteFile(this.file, dossier);
this.showOCR = this.file.canBeOCRed;
this.canReanalyse = this.permissionsService.canReanalyseFile(this.file);
@ -277,4 +262,13 @@ export class FileActionsComponent extends AutoUnsubscribe implements OnInit, OnD
this.showExcludePages = this.isFilePreview;
this.showDocumentInfo = this.isFilePreview;
}
private async _setFileApproved() {
await this._fileActionService
.setFilesApproved([this.file])
.toPromise()
.then(() => {
this.reloadFiles('set-approved');
});
}
}

View File

@ -2,7 +2,6 @@ import { Injectable } from '@angular/core';
import { AppStateService } from '@state/app-state.service';
import { UserService } from '@services/user.service';
import { File } from '@red/domain';
import { PermissionsService } from '@services/permissions.service';
import { DossiersDialogService } from '../../services/dossiers-dialog.service';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { FilesService } from '@services/entity-services/files.service';
@ -14,13 +13,11 @@ import { ReanalysisService } from '@services/reanalysis.service';
export class FileActionService {
constructor(
private readonly _dialogService: DossiersDialogService,
private readonly _permissionsService: PermissionsService,
private readonly _userService: UserService,
private readonly _fileService: FilesService,
private readonly _filesService: FilesService,
private readonly _reanalysisService: ReanalysisService,
private readonly _appStateService: AppStateService,
private readonly _dossiersService: DossiersService,
private readonly _filesService: FilesService,
private readonly _toaster: Toaster,
) {}
@ -58,7 +55,7 @@ export class FileActionService {
approverId = this._dossiersService.activeDossier.approverIds[0];
}
return this._fileService.setUnderApprovalFor(
return this._filesService.setUnderApprovalFor(
files.map(f => f.fileId),
this._dossiersService.activeDossierId,
approverId,
@ -66,14 +63,14 @@ export class FileActionService {
}
setFilesApproved(files: File[]) {
return this._fileService.setApprovedFor(
return this._filesService.setApprovedFor(
files.map(f => f.fileId),
this._dossiersService.activeDossierId,
);
}
setFilesUnderReview(files: File[]) {
return this._fileService.setUnderReviewFor(
return this._filesService.setUnderReviewFor(
files.map(f => f.fileId),
this._dossiersService.activeDossierId,
);
@ -141,7 +138,7 @@ export class FileActionService {
}
private async _assignReviewerToCurrentUser(files: File[], callback?: Function) {
await this._fileService
await this._filesService
.setReviewerFor(
files.map(f => f.fileId),
this._dossiersService.activeDossierId,

View File

@ -70,10 +70,6 @@ export class DossiersService extends EntitiesService<Dossier, IDossier> {
return this._activeDossier$.value?.dossierId;
}
goToActiveDossier(): Promise<void> {
return this._router.navigate([this.activeDossier?.routerLink]).then();
}
find(dossierId: string): Dossier | undefined;
find(dossierId: string, fileId: string): File | undefined;
find(dossierId: string, fileId?: string): Dossier | File | undefined {

View File

@ -0,0 +1,64 @@
import { Injectable } from '@angular/core';
import { FilesService } from '@services/entity-services/files.service';
import { BehaviorSubject, Observable, Subject } from 'rxjs';
import { File } from '@red/domain';
import { filter, startWith } from 'rxjs/operators';
@Injectable({ providedIn: 'root' })
export class FilesMapService {
readonly fileReanalysed$ = new Subject<File>();
private readonly _entityChanged$ = new Subject<File>();
private readonly _map = new Map<string, BehaviorSubject<File[]>>();
constructor(private readonly _filesService: FilesService) {}
get$(dossierId: string) {
return this._map.get(dossierId).asObservable();
}
has(dossierId: string) {
return this._map.has(dossierId);
}
get(key: string): File[] | undefined;
get(key: string, id: string): File | undefined;
get(key: string, id?: string): File | File[] | undefined {
const value = this._map.get(key)?.value;
if (!id) {
return value;
}
return value?.find(item => item.id === id);
}
set(key: string, entities: File[]): void {
if (!this._map.has(key)) {
this._map.set(key, new BehaviorSubject<File[]>(entities));
return entities.forEach(entity => this._entityChanged$.next(entity));
}
// Keep old object references for unchanged entities
const newEntities = entities.map(newEntity => {
const oldEntity = this.get(key, newEntity.id);
if (oldEntity.lastProcessed !== newEntity.lastProcessed) {
this.fileReanalysed$.next(newEntity);
}
if (newEntity.isEqual(oldEntity)) {
return oldEntity;
}
this._entityChanged$.next(newEntity);
return newEntity;
});
this._map.get(key).next(newEntities);
}
watch$(key: string, entityId: string): Observable<File> {
return this._entityChanged$.pipe(
filter(entity => entity.id === entityId),
startWith(this.get(key, entityId)),
);
}
}

View File

@ -13,10 +13,6 @@ export class FilesService extends EntitiesService<File, IFile> {
super(_injector, File, 'status');
}
getExistingFilesFor(dossierId: string): List<File> {
return this.all.filter(file => file.dossierId === dossierId);
}
fetch() {
this.get().pipe(map(files => files.map(file => new File(file, this._userService.getNameForId(file.currentReviewer)))));
}

View File

@ -4,6 +4,7 @@ import { AppStateService } from './app-state.service';
import { UserService } from '@services/user.service';
import { DossiersService } from '@services/entity-services/dossiers.service';
import { DossierTemplatesService } from '@services/entity-services/dossier-templates.service';
import { FilesMapService } from '@services/entity-services/files-map.service';
@Injectable({
providedIn: 'root',
@ -12,6 +13,7 @@ export class AppStateGuard implements CanActivate {
constructor(
private readonly _appStateService: AppStateService,
private readonly _dossiersService: DossiersService,
private readonly _filesMapService: FilesMapService,
private readonly _dossierTemplatesService: DossierTemplatesService,
private readonly _userService: UserService,
private readonly _router: Router,
@ -29,7 +31,7 @@ export class AppStateGuard implements CanActivate {
}
if (this._userService.currentUser.isUser) {
await this._dossiersService.loadAllIfEmpty();
await this._appStateService.loadAllDossiersIfNecessary();
}
const { dossierId, fileId, dossierTemplateId, type } = route.params;
@ -39,7 +41,7 @@ export class AppStateGuard implements CanActivate {
return false;
}
if (fileId && !this._dossiersService.find(dossierId, fileId)) {
if (fileId && !this._filesMapService.get(dossierId, fileId)) {
await this._router.navigate(['main', 'dossiers', dossierId]);
return false;
}

View File

@ -1,5 +1,5 @@
import { Injectable } from '@angular/core';
import { Dictionary, Dossier, DossierTemplate, File, IColors, IDossier, IFile } from '@red/domain';
import { Dictionary, Dossier, DossierTemplate, File, IColors, IDossier } from '@red/domain';
import { ActivationEnd, Router } from '@angular/router';
import { UserService } from '@services/user.service';
import { forkJoin, Observable, of, Subject } from 'rxjs';
@ -13,6 +13,7 @@ import { DossierTemplatesService } from '@services/entity-services/dossier-templ
import { FileAttributesService } from '@services/entity-services/file-attributes.service';
import { ReanalysisService } from '@services/reanalysis.service';
import { DossierStatsService } from '@services/entity-services/dossier-stats.service';
import { FilesMapService } from '@services/entity-services/files-map.service';
export interface AppState {
activeFileId?: string;
@ -38,6 +39,7 @@ export class AppStateService {
private readonly _dossierTemplatesService: DossierTemplatesService,
private readonly _dossierStatsService: DossierStatsService,
private readonly _fileAttributesService: FileAttributesService,
private readonly _filesMapService: FilesMapService,
private readonly _userPreferenceService: UserPreferenceService,
) {
_router.events.pipe(currentComponentRoute).subscribe(async (event: ActivationEnd) => {
@ -85,7 +87,7 @@ export class AppStateService {
}
get activeFile(): File | undefined {
return this._dossiersService.activeDossier?.files.find(f => f.fileId === this.activeFileId);
return this._filesMapService.get(this._dossiersService.activeDossierId, this.activeFileId);
}
get activeFileId(): string | undefined {
@ -133,6 +135,7 @@ export class AppStateService {
const oldDossier = this._dossiersService.find(p.dossierId);
const type = oldDossier?.type ?? (await this._getDictionaryFor(p));
const stats = dossierStats.find(s => s.dossierId === p.dossierId);
console.log(stats);
this._dossiersService.replace(new Dossier(p, stats, [], type));
});
return Promise.all(mappedDossiers$);
@ -160,10 +163,17 @@ export class AppStateService {
return activeFile;
}
async getFiles(dossier = this._dossiersService.activeDossier, emitEvents = true) {
async getFiles(dossier = this._dossiersService.activeDossier) {
const files = await this._filesService.getFor(dossier.id).toPromise();
return this._processFiles(dossier, files, emitEvents);
const fileAttributes = this._fileAttributesService.getFileAttributeConfig(dossier.dossierTemplateId);
const newFiles = files.map(iFile => new File(iFile, this._userService.getNameForId(iFile.currentReviewer), fileAttributes));
const lastOpenedFileId = this._userPreferenceService.getLastOpenedFileForDossier(dossier.id);
newFiles.forEach(file => (file.lastOpened = file.fileId === lastOpenedFileId));
this._filesMapService.set(dossier.dossierId, newFiles);
return newFiles;
}
async reanalyzeDossier({ id } = this._dossiersService.activeDossier) {
@ -174,12 +184,9 @@ export class AppStateService {
if (this._dossiersService.activeDossierId === dossierId && this.activeFileId === fileId) {
return;
}
if (this._dossiersService.activeDossier) {
this._appState.activeFileId = fileId;
if (!this.activeFile) {
this._appState.activeFileId = null;
await this._dossiersService.goToActiveDossier();
}
}
await this._updateLastActiveFileForDossier(dossierId, fileId);
}
@ -201,7 +208,7 @@ export class AppStateService {
async reloadActiveDossierFiles() {
if (this._dossiersService.activeDossierId) {
await this.getFiles();
return this.getFiles();
}
}
@ -471,37 +478,4 @@ export class AppStateService {
await this._userPreferenceService.saveLastOpenedFileForDossier(dossierId, fileId);
}
private _processFiles(dossier: Dossier, iFiles: IFile[], emitEvents = true) {
const oldFiles = dossier.files;
const fileAttributes = this._fileAttributesService.getFileAttributeConfig(dossier.dossierTemplateId);
const newFiles = iFiles.map(iFile => new File(iFile, this._userService.getNameForId(iFile.currentReviewer), fileAttributes));
const lastOpenedFileId = this._userPreferenceService.getLastOpenedFileForDossier(dossier.id);
newFiles.forEach(file => (file.lastOpened = file.fileId === lastOpenedFileId));
for (const newFile of newFiles) {
let found = false;
for (const oldFile of oldFiles) {
if (oldFile.fileId === newFile.fileId) {
// emit when analysis count changed
if (JSON.stringify(oldFile) !== JSON.stringify(newFile) && emitEvents) {
this.fileChanged$.next(newFile);
}
if (oldFile.lastProcessed !== newFile.lastProcessed && emitEvents) {
this.fileReanalysed$.next(newFile);
}
found = true;
break;
}
}
// emit for new file
if (!found && emitEvents) {
this.fileChanged$.next(newFile);
}
}
return newFiles;
}
}