Merge branch 'master' into VM/RED-2614

This commit is contained in:
Valentin 2022-01-13 12:13:44 +02:00
commit d8af3d687a
17 changed files with 499 additions and 392 deletions

View File

@ -1,3 +1,4 @@
<router-outlet></router-outlet>
<iqser-full-page-loading-indicator></iqser-full-page-loading-indicator>
<iqser-connection-status></iqser-connection-status>
<iqser-full-page-error></iqser-full-page-error>

View File

@ -1,4 +1,4 @@
import { ChangeDetectionStrategy, Component, ElementRef, OnInit, ViewChild } from '@angular/core';
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, OnInit, ViewChild } from '@angular/core';
import { PermissionsService } from '@services/permissions.service';
import { Debounce, IconButtonTypes, LoadingService, Toaster } from '@iqser/common-ui';
import { TranslateService } from '@ngx-translate/core';
@ -38,6 +38,7 @@ export class RulesScreenComponent extends ComponentHasChanges implements OnInit
constructor(
readonly permissionsService: PermissionsService,
private readonly _rulesService: RulesService,
private readonly _changeDetectorRef: ChangeDetectorRef,
private readonly _dossierTemplatesService: DossierTemplatesService,
private readonly _toaster: Toaster,
protected readonly _translateService: TranslateService,
@ -74,6 +75,7 @@ export class RulesScreenComponent extends ComponentHasChanges implements OnInit
},
});
(window as any).monaco.editor.setTheme('redaction');
this._changeDetectorRef.detectChanges();
}
@Debounce()
@ -106,6 +108,7 @@ export class RulesScreenComponent extends ComponentHasChanges implements OnInit
revert(): void {
this.currentLines = this.initialLines;
this._decorations = this._codeEditor?.deltaDecorations(this._decorations, []) || [];
this._changeDetectorRef.detectChanges();
this._loadingService.stop();
}

View File

@ -100,7 +100,7 @@
</div>
</form>
<iqser-circle-button class="dialog-close" icon="iqser:close" mat-dialog-close></iqser-circle-button>
<iqser-circle-button class="dialog-close" icon="iqser:close" (action)="close()"></iqser-circle-button>
</section>
<ng-template #reportTemplateOptionTemplate let-option="option">

View File

@ -15,19 +15,19 @@ import { FileDropOverlayService } from '@upload-download/services/file-drop-over
import { FileUploadModel } from '@upload-download/model/file-upload.model';
import { FileUploadService } from '@upload-download/services/file-upload.service';
import { StatusOverlayService } from '@upload-download/services/status-overlay.service';
import * as moment from 'moment';
import { Observable, timer } from 'rxjs';
import { Observable, Subscription } from 'rxjs';
import { filter, skip, switchMap, tap } from 'rxjs/operators';
import { convertFiles, Files, handleFileDrop } from '@utils/index';
import {
CircleButtonTypes,
CustomError,
DefaultListingServices,
ErrorService,
ListingComponent,
ListingModes,
LoadingService,
NestedFilter,
OnAttach,
OnDetach,
TableColumnConfig,
TableComponent,
WorkflowConfig,
@ -45,7 +45,7 @@ import { LongPressEvent } from '@shared/directives/long-press.directive';
import { UserPreferenceService } from '@services/user-preference.service';
import { FilesMapService } from '@services/entity-services/files-map.service';
import { FilesService } from '@services/entity-services/files.service';
import { CHANGED_CHECK_INTERVAL } from '@utils/constants';
import { DossierStatsService } from '@services/entity-services/dossier-stats.service';
@Component({
templateUrl: './dossier-overview-screen.component.html',
@ -53,7 +53,7 @@ import { CHANGED_CHECK_INTERVAL } from '@utils/constants';
providers: [...DefaultListingServices, { provide: ListingComponent, useExisting: forwardRef(() => DossierOverviewScreenComponent) }],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class DossierOverviewScreenComponent extends ListingComponent<File> implements OnInit, OnDestroy, OnDetach, OnAttach {
export class DossierOverviewScreenComponent extends ListingComponent<File> implements OnInit, OnDestroy, OnAttach {
readonly listingModes = ListingModes;
readonly circleButtonTypes = CircleButtonTypes;
readonly tableHeaderLabel = _('dossier-overview.table-header.title');
@ -74,6 +74,7 @@ export class DossierOverviewScreenComponent extends ListingComponent<File> imple
@ViewChild('fileInput', { static: true }) private readonly _fileInput: ElementRef;
@ViewChild(TableComponent) private readonly _tableComponent: TableComponent<Dossier>;
private _fileAttributeConfigs: IFileAttributeConfig[];
private readonly _removableSubscriptions = new Subscription();
constructor(
protected readonly _injector: Injector,
@ -81,6 +82,7 @@ export class DossierOverviewScreenComponent extends ListingComponent<File> imple
readonly permissionsService: PermissionsService,
private readonly _loadingService: LoadingService,
private readonly _dossiersService: DossiersService,
private readonly _dossierStatsService: DossierStatsService,
private readonly _dossierTemplatesService: DossierTemplatesService,
private readonly _appConfigService: AppConfigService,
private readonly _fileUploadService: FileUploadService,
@ -92,6 +94,7 @@ export class DossierOverviewScreenComponent extends ListingComponent<File> imple
readonly configService: ConfigService,
private readonly _userPreferenceService: UserPreferenceService,
private readonly _fileMapService: FilesMapService,
private readonly _errorService: ErrorService,
activatedRoute: ActivatedRoute,
) {
super(_injector);
@ -128,18 +131,17 @@ export class DossierOverviewScreenComponent extends ListingComponent<File> imple
this._fileDropOverlayService.initFileDropHandling(this.dossierId);
this.addSubscription = timer(CHANGED_CHECK_INTERVAL, CHANGED_CHECK_INTERVAL)
.pipe(
switchMap(() => this._filesService.hasChanges$(this.dossierId)),
filter(changed => changed),
switchMap(() => this._reloadFiles()),
)
.subscribe();
this.addSubscription = this.configService.listingMode$.subscribe(() => {
this._computeAllFilters();
});
this.addSubscription = this._dossiersService.dossierFileChanges$
.pipe(
filter(dossierId => dossierId === this.dossierId),
switchMap(dossierId => this._filesService.loadAll(dossierId)),
)
.subscribe();
this.addSubscription = this._dossierTemplatesService
.getEntityChanged$(this.currentDossier.dossierTemplateId)
.pipe(
@ -166,11 +168,10 @@ export class DossierOverviewScreenComponent extends ListingComponent<File> imple
ngOnAttach() {
this._fileDropOverlayService.initFileDropHandling(this.dossierId);
this._setRemovableSubscriptions();
this._tableComponent?.scrollToLastIndex();
}
ngOnDetach() {}
forceReanalysisAction($event: LongPressEvent) {
this.analysisForced = !$event.touchEnd && this._userPreferenceService.areDevFeaturesEnabled;
}
@ -192,8 +193,20 @@ export class DossierOverviewScreenComponent extends ListingComponent<File> imple
(this._fileInput as any).nativeElement.value = null;
}
recentlyModifiedChecker = (file: File) =>
moment(file.lastUpdated).add(this._appConfigService.values.RECENT_PERIOD_IN_HOURS, 'hours').isAfter(moment());
private _setRemovableSubscriptions(): void {
this._removableSubscriptions.add(
this._dossiersService
.getEntityDeleted$(this.dossierId)
.pipe(tap(() => this._handleDeletedDossier()))
.subscribe(),
);
}
private _handleDeletedDossier(): void {
this._errorService.set(
new CustomError(_('error.deleted-entity.dossier.label'), _('error.deleted-entity.dossier.action'), 'iqser:expand'),
);
}
private _updateFileAttributes(): void {
this._fileAttributeConfigs =
@ -204,11 +217,6 @@ export class DossierOverviewScreenComponent extends ListingComponent<File> imple
this._computeAllFilters();
}
private async _reloadFiles() {
await this._filesService.loadAll(this.dossierId).toPromise();
this._computeAllFilters();
}
private _loadEntitiesFromState() {
this.currentDossier = this._dossiersService.find(this.dossierId);
this._computeAllFilters();

View File

@ -12,7 +12,7 @@
*ngIf="currentUser.isUser"
[tooltip]="(currentUser.isManager ? 'dossier-listing.edit.action' : 'dossier-listing.dossier-info.action') | translate"
[type]="circleButtonTypes.dark"
[icon]="currentUser.isManager ? 'iqser:edit' : 'iqser:dossier-info'"
[icon]="currentUser.isManager ? 'iqser:edit' : 'red:info'"
iqserHelpMode="edit-dossier-from-list"
></iqser-circle-button>

View File

@ -9,28 +9,28 @@
</div>
</div>
<div class="small-label stats-subtitle">
<div>
<div class="stats-subtitle">
<div class="small-label">
<mat-icon svgIcon="iqser:document"></mat-icon>
{{ dossierStats.numberOfFiles }}
</div>
<div>
<div class="small-label">
<mat-icon svgIcon="iqser:pages"></mat-icon>
{{ dossierStats.numberOfPages }}
</div>
<div>
<div class="small-label">
<mat-icon svgIcon="red:user"></mat-icon>
{{ dossier.memberIds.length }}
</div>
<div>
<div class="small-label">
<mat-icon svgIcon="red:calendar"></mat-icon>
{{ dossier.date | date: 'mediumDate' }}
</div>
<div *ngIf="dossier.dueDate">
<div *ngIf="dossier.dueDate" [class.error]="passedDueDate" [class.warn]="approachingDueDate" class="small-label">
<mat-icon svgIcon="red:lightning"></mat-icon>
{{ dossier.dueDate | date: 'mediumDate' }}
</div>

View File

@ -3,6 +3,9 @@ import { Dossier, DossierStats } from '@red/domain';
import { DossierTemplatesService } from '@services/entity-services/dossier-templates.service';
import { DossierStatsService } from '@services/entity-services/dossier-stats.service';
import { DossiersService } from '@services/entity-services/dossiers.service';
import * as moment from 'moment';
const DUE_DATE_WARN_DAYS = 14;
@Component({
selector: 'redaction-dossiers-listing-dossier-name',
@ -20,6 +23,18 @@ export class DossiersListingDossierNameComponent {
private readonly _dossiersService: DossiersService,
) {}
get approachingDueDate(): boolean {
return this._dueDateDaysDiff >= 0 && this._dueDateDaysDiff <= DUE_DATE_WARN_DAYS;
}
get passedDueDate(): boolean {
return this._dueDateDaysDiff < 0;
}
private get _dueDateDaysDiff(): number {
return moment(this.dossier.dueDate).diff(moment().startOf('day'), 'days');
}
getDossierTemplateNameFor(dossierTemplateId: string): string {
return this._dossierTemplatesService.find(dossierTemplateId).name;
}

View File

@ -1,19 +1,17 @@
import { ChangeDetectionStrategy, Component, forwardRef, Injector, OnDestroy, OnInit, TemplateRef, ViewChild } from '@angular/core';
import { ChangeDetectionStrategy, Component, forwardRef, Injector, OnInit, TemplateRef, ViewChild } from '@angular/core';
import { Dossier } from '@red/domain';
import { UserService } from '@services/user.service';
import { PermissionsService } from '@services/permissions.service';
import { TranslateChartService } from '@services/translate-chart.service';
import { timer } from 'rxjs';
import { Router } from '@angular/router';
import { DossiersDialogService } from '../../../services/dossiers-dialog.service';
import { DefaultListingServicesTmp, EntitiesService, ListingComponent, OnAttach, OnDetach, TableComponent } from '@iqser/common-ui';
import { DefaultListingServicesTmp, EntitiesService, ListingComponent, OnAttach, TableComponent } from '@iqser/common-ui';
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';
import { DossierTemplatesService } from '@services/entity-services/dossier-templates.service';
import { switchMap, tap } from 'rxjs/operators';
import { CHANGED_CHECK_INTERVAL } from '@utils/constants';
import { tap } from 'rxjs/operators';
@Component({
templateUrl: './dossiers-listing-screen.component.html',
@ -25,7 +23,7 @@ import { CHANGED_CHECK_INTERVAL } from '@utils/constants';
],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class DossiersListingScreenComponent extends ListingComponent<Dossier> implements OnInit, OnDestroy, OnAttach, OnDetach {
export class DossiersListingScreenComponent extends ListingComponent<Dossier> implements OnInit, OnAttach {
readonly currentUser = this._userService.currentUser;
readonly tableColumnConfigs = this._configService.tableConfig;
readonly tableHeaderLabel = _('dossier-listing.table-header.title');
@ -57,22 +55,13 @@ export class DossiersListingScreenComponent extends ListingComponent<Dossier> im
}
ngOnInit(): void {
this.addSubscription = timer(CHANGED_CHECK_INTERVAL, CHANGED_CHECK_INTERVAL)
.pipe(switchMap(() => this._dossiersService.loadAllIfChanged()))
.subscribe();
this.addSubscription = this._dossiersService.all$.pipe(tap(() => this._computeAllFilters())).subscribe();
}
ngOnAttach(): void {
this.ngOnInit();
this._tableComponent?.scrollToLastIndex();
}
ngOnDetach(): void {
this.ngOnDestroy();
}
openAddDossierDialog(): void {
this._dialogService.openDialog('addDossier', null, null, async (addResponse: { dossier: Dossier; addMembers: boolean }) => {
await this._router.navigate([addResponse.dossier.routerLink]);

View File

@ -5,7 +5,9 @@ import { PdfViewerComponent } from './components/pdf-viewer/pdf-viewer.component
import {
AutoUnsubscribe,
CircleButtonTypes,
CustomError,
Debounce,
ErrorService,
FilterService,
LoadingService,
OnAttach,
@ -43,19 +45,12 @@ import { ViewModeService } from './services/view-mode.service';
import { MultiSelectService } from './services/multi-select.service';
import { DocumentInfoService } from './services/document-info.service';
import { ReanalysisService } from '../../../../services/reanalysis.service';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import Annotation = Core.Annotations.Annotation;
import PDFNet = Core.PDFNet;
const ALL_HOTKEY_ARRAY = ['Escape', 'F', 'f', 'ArrowUp', 'ArrowDown'];
function diff<T>(first: readonly T[], second: readonly T[]): T[] {
// symmetrical difference between two arrays
const a = new Set(first);
const b = new Set(second);
return [...first.filter(x => !b.has(x)), ...second.filter(x => !a.has(x))];
}
@Component({
templateUrl: './file-preview-screen.component.html',
styleUrls: ['./file-preview-screen.component.scss'],
@ -108,6 +103,7 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
private readonly _filesMapService: FilesMapService,
private readonly _dossiersService: DossiersService,
private readonly _reanalysisService: ReanalysisService,
private readonly _errorService: ErrorService,
readonly excludedPagesService: ExcludedPagesService,
readonly viewModeService: ViewModeService,
readonly multiSelectService: MultiSelectService,
@ -190,7 +186,7 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
ngOnDetach(): void {
this.displayPdfViewer = false;
super.ngOnDestroy();
super.ngOnDetach();
this._changeDetectorRef.markForCheck();
}
@ -529,10 +525,10 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
}
private _subscribeToFileUpdates(): void {
this.addSubscription = timer(0, 5000)
this.addActiveScreenSubscription = timer(0, 5000)
.pipe(switchMap(() => this._filesService.reload(this.dossierId, this.fileId)))
.subscribe();
this.addSubscription = this._filesMapService.fileReanalysed$
this.addActiveScreenSubscription = this._filesMapService.fileReanalysed$
.pipe(filter(file => file.fileId === this.fileId))
.subscribe(async file => {
if (file.lastProcessed !== this.fileData?.file.lastProcessed) {
@ -541,6 +537,28 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
}
this._loadingService.stop();
});
this.addActiveScreenSubscription = this._dossiersService
.getEntityDeleted$(this.dossierId)
.pipe(tap(() => this._handleDeletedDossier()))
.subscribe();
this.addActiveScreenSubscription = this._filesMapService
.watchDeleted$(this.fileId)
.pipe(tap(() => this._handleDeletedFile()))
.subscribe();
}
private _handleDeletedDossier(): void {
this._errorService.set(
new CustomError(_('error.deleted-entity.file-dossier.label'), _('error.deleted-entity.file-dossier.action'), 'iqser:expand'),
);
}
private _handleDeletedFile(): void {
this._errorService.set(
new CustomError(_('error.deleted-entity.file.label'), _('error.deleted-entity.file.action'), 'iqser:expand'),
);
}
private async _loadFileData(file: File): Promise<void | boolean> {

View File

@ -1,5 +1,6 @@
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
EventEmitter,
HostBinding,
@ -96,6 +97,7 @@ export class FileActionsComponent extends AutoUnsubscribe implements OnDestroy,
private readonly _userPreferenceService: UserPreferenceService,
private readonly _reanalysisService: ReanalysisService,
private readonly _router: Router,
private readonly _changeRef: ChangeDetectorRef,
) {
super();
}
@ -224,7 +226,10 @@ export class FileActionsComponent extends AutoUnsubscribe implements OnDestroy,
}
ngOnInit() {
this._dossiersService.getEntityChanged$(this.file.dossierId).pipe(tap(() => this._setup()));
this.addSubscription = this._dossiersService
.getEntityChanged$(this.file.dossierId)
.pipe(tap(() => this._setup()))
.subscribe();
}
ngOnChanges() {
@ -353,6 +358,8 @@ export class FileActionsComponent extends AutoUnsubscribe implements OnDestroy,
this.showReanalyseDossierOverview = this.canReanalyse && this.isDossierOverview && this.analysisForced;
this.buttons = this._buttons;
this._changeRef.markForCheck();
}
private async _setFileApproved() {

View File

@ -2,16 +2,27 @@ import { Injectable, Injector } from '@angular/core';
import { EntitiesService, List, mapEach, QueryParam, RequiredParam, shareLast, Toaster, Validate } from '@iqser/common-ui';
import { Dossier, IDossier, IDossierRequest } from '@red/domain';
import { catchError, filter, map, mapTo, switchMap, tap } from 'rxjs/operators';
import { combineLatest, iif, Observable, of, throwError } from 'rxjs';
import { combineLatest, Observable, of, Subject, throwError, timer } from 'rxjs';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { HttpErrorResponse, HttpStatusCode } from '@angular/common/http';
import { DossierStatsService } from '@services/entity-services/dossier-stats.service';
import { CHANGED_CHECK_INTERVAL } from '@utils/constants';
export interface IDossiersStats {
totalPeople: number;
totalAnalyzedPages: number;
}
interface ChangesDetails {
dossierChanges: [
{
dossierChanges: boolean;
dossierId: string;
fileChanges: boolean;
},
];
}
const DOSSIER_EXISTS_MSG = _('add-dossier-dialog.errors.dossier-already-exists');
const GENERIC_MGS = _('add-dossier-dialog.errors.generic');
@ -20,6 +31,7 @@ const GENERIC_MGS = _('add-dossier-dialog.errors.generic');
})
export class DossiersService extends EntitiesService<Dossier, IDossier> {
readonly generalStats$ = this.all$.pipe(switchMap(entities => this._generalStats$(entities)));
readonly dossierFileChanges$ = new Subject<string>();
constructor(
private readonly _toaster: Toaster,
@ -27,6 +39,13 @@ export class DossiersService extends EntitiesService<Dossier, IDossier> {
private readonly _dossierStatsService: DossierStatsService,
) {
super(_injector, Dossier, 'dossier');
timer(CHANGED_CHECK_INTERVAL, CHANGED_CHECK_INTERVAL)
.pipe(
switchMap(() => this.loadAllIfChanged()),
tap(changes => this._emitFileChanges(changes)),
)
.subscribe();
}
loadAll(): Observable<Dossier[]> {
@ -39,8 +58,15 @@ export class DossiersService extends EntitiesService<Dossier, IDossier> {
);
}
loadAllIfChanged(): Observable<boolean> {
return this.hasChanges$().pipe(switchMap(changed => iif(() => changed, this.loadAll()).pipe(mapTo(changed))));
loadAllIfChanged(): Observable<ChangesDetails> {
return this.hasChangesDetails$().pipe(switchMap(changes => this.loadAll().pipe(mapTo(changes))));
}
hasChangesDetails$(): Observable<ChangesDetails> {
const body = { value: this._lastCheckedForChanges.get('root') ?? '0' };
return this._post<ChangesDetails>(body, `${this._defaultModelPath}/changes/details`).pipe(
filter(changes => changes.dossierChanges.length > 0),
);
}
@Validate()
@ -82,6 +108,10 @@ export class DossiersService extends EntitiesService<Dossier, IDossier> {
return super.delete(body, 'deleted-dossiers/hard-delete', body).toPromise();
}
private _emitFileChanges(changes: ChangesDetails): void {
changes.dossierChanges.filter(change => change.fileChanges).forEach(change => this.dossierFileChanges$.next(change.dossierId));
}
private _computeStats(entities: List<Dossier>): IDossiersStats {
let totalAnalyzedPages = 0;
const totalPeople = new Set<string>();

View File

@ -7,6 +7,7 @@ import { filter, startWith } from 'rxjs/operators';
export class FilesMapService {
readonly fileReanalysed$ = new Subject<File>();
private readonly _entityChanged$ = new Subject<File>();
private readonly _entityDeleted$ = new Subject<File>();
private readonly _map = new Map<string, BehaviorSubject<File[]>>();
get$(dossierId: string) {
@ -39,6 +40,7 @@ export class FilesMapService {
const reanalysedEntities = [];
const changedEntities = [];
const deletedEntities = this.get(key).filter(oldEntity => !entities.find(newEntity => newEntity.id === oldEntity.id));
// Keep old object references for unchanged entities
const newEntities = entities.map(newEntity => {
@ -67,6 +69,10 @@ export class FilesMapService {
for (const file of changedEntities) {
this._entityChanged$.next(file);
}
for (const file of deletedEntities) {
this._entityDeleted$.next(file);
}
}
replace(entity: File) {
@ -83,4 +89,8 @@ export class FilesMapService {
startWith(this.get(key, entityId)),
);
}
watchDeleted$(entityId: string): Observable<File> {
return this._entityDeleted$.pipe(filter(entity => entity.id === entityId));
}
}

View File

@ -944,6 +944,20 @@
"side-nav-title": "Configurations"
},
"error": {
"deleted-entity": {
"dossier": {
"action": "Back to overview",
"label": "This dossier has been deleted!"
},
"file-dossier": {
"action": "Back to overview",
"label": "The dossier of this file has been deleted!"
},
"file": {
"action": "Back to dossier",
"label": "This file has been deleted!"
}
},
"http": {
"generic": "Action failed with code {status}"
},

@ -1 +1 @@
Subproject commit afed414030f936da36aa993a1945591b5c451861
Subproject commit f7004520a71b36a00109115a5f4e860f918dcca9

View File

@ -1,6 +1,6 @@
{
"name": "redaction",
"version": "3.143.0",
"version": "3.148.0",
"private": true,
"license": "MIT",
"scripts": {

Binary file not shown.

File diff suppressed because it is too large Load Diff