use service for multiselect

This commit is contained in:
Dan Percic 2021-11-25 16:09:24 +02:00
parent 3318a21f0e
commit 92fda2695d
12 changed files with 120 additions and 97 deletions

View File

@ -4,7 +4,7 @@
[attr.annotation-id]="annotation.id"
[attr.annotation-page]="activeViewerPage"
[class.active]="isSelected(annotation.annotationId)"
[class.multi-select-active]="multiSelectActive"
[class.multi-select-active]="multiSelectService.active$ | async"
class="annotation-wrapper"
>
<div class="active-bar-marker"></div>
@ -30,7 +30,7 @@
<div class="active-icon-marker-container">
<iqser-round-checkbox
*ngIf="multiSelectActive && isSelected(annotation.annotationId)"
*ngIf="(multiSelectService.active$ | async) && isSelected(annotation.annotationId)"
[active]="true"
></iqser-round-checkbox>
</div>
@ -47,7 +47,7 @@
{{ annotation.comments.length }}
</div>
<div *ngIf="!multiSelectActive" class="actions">
<div *ngIf="multiSelectService.inactive$ | async" class="actions">
<ng-container
[ngTemplateOutletContext]="{ annotation: annotation }"
[ngTemplateOutlet]="annotationActionsTemplate"

View File

@ -2,6 +2,7 @@ import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output, Templa
import { AnnotationWrapper } from '@models/file/annotation.wrapper';
import { IqserEventTarget } from '@iqser/common-ui';
import { File } from '@red/domain';
import { MultiSelectService } from '../../services/multi-select.service';
@Component({
selector: 'redaction-annotations-list',
@ -14,19 +15,16 @@ export class AnnotationsListComponent {
@Input() annotations: AnnotationWrapper[];
@Input() selectedAnnotations: AnnotationWrapper[];
@Input() annotationActionsTemplate: TemplateRef<unknown>;
@Input() multiSelectActive = false;
@Input() activeViewerPage: number;
@Input() canMultiSelect = true;
@Output() readonly multiSelectActiveChange = new EventEmitter<boolean>();
@Output() readonly pagesPanelActive = new EventEmitter<boolean>();
@Output() readonly selectAnnotations = new EventEmitter<
AnnotationWrapper[] | { annotations: AnnotationWrapper[]; multiSelect: boolean }
>();
@Output() readonly selectAnnotations = new EventEmitter<AnnotationWrapper[]>();
@Output() readonly deselectAnnotations = new EventEmitter<AnnotationWrapper[]>();
constructor(readonly multiSelectService: MultiSelectService) {}
annotationClicked(annotation: AnnotationWrapper, $event: MouseEvent): void {
console.log(annotation);
if (($event.target as IqserEventTarget).localName === 'input') {
return;
}
@ -35,13 +33,9 @@ export class AnnotationsListComponent {
this.deselectAnnotations.emit([annotation]);
} else {
if (this.canMultiSelect && ($event.ctrlKey || $event.metaKey) && this.selectedAnnotations.length > 0) {
this.multiSelectActive = true;
this.multiSelectActiveChange.emit(true);
this.multiSelectService.activate();
}
this.selectAnnotations.emit({
annotations: [annotation],
multiSelect: this.multiSelectActive,
});
this.selectAnnotations.emit([annotation]);
}
}

View File

@ -1,5 +1,5 @@
<div
*ngIf="excludedPageService.show$ | async; else selectAndFilter"
*ngIf="excludedPageService.shown$ | async; else selectAndFilter"
class="right-title heading"
translate="file-preview.tabs.exclude-pages.label"
>
@ -17,8 +17,8 @@
<div class="right-title heading" translate="file-preview.tabs.annotations.label">
<div>
<div
(click)="multiSelectActive = true"
*ngIf="!multiSelectActive && !isReadOnly"
(click)="multiSelectService.activate()"
*ngIf="!isReadOnly && (multiSelectInactive$ | async)"
class="all-caps-label primary pointer"
iqserHelpMode="bulk-select-annotations"
translate="file-preview.tabs.annotations.select"
@ -48,14 +48,16 @@
</div>
</div>
<div *ngIf="multiSelectActive" class="multi-select">
<div *ngIf="multiSelectActive$ | async" class="multi-select">
<div class="selected-wrapper">
<iqser-round-checkbox
(click)="!!selectedAnnotations?.length && selectAnnotations.emit()"
[indeterminate]="!!selectedAnnotations?.length"
type="with-bg"
></iqser-round-checkbox>
<span class="all-caps-label">{{ selectedAnnotations?.length || 0 }} selected </span>
<redaction-annotation-actions
(annotationsChanged)="annotationsChanged.emit($event)"
*ngIf="selectedAnnotations?.length > 0"
@ -70,13 +72,13 @@
</div>
<iqser-circle-button
(action)="multiSelectActive = false"
(action)="multiSelectService.deactivate()"
[type]="circleButtonTypes.primary"
icon="iqser:close"
></iqser-circle-button>
</div>
<div [class.lower-height]="multiSelectActive || isReadOnly" class="annotations-wrapper">
<div [class.lower-height]="(multiSelectActive$ | async) || isReadOnly" class="annotations-wrapper">
<div
#quickNavigation
(keydown)="preventKeyDefault($event)"
@ -120,7 +122,7 @@
</div>
<div style="overflow: hidden; width: 100%">
<ng-container *ngIf="(excludedPageService.show$ | async) === false">
<ng-container *ngIf="excludedPageService.hidden$ | async">
<div [attr.anotation-page-header]="activeViewerPage" [class.padding-left-0]="currentPageIsExcluded" class="page-separator">
<span *ngIf="!!activeViewerPage" class="flex-align-items-center">
<iqser-circle-button
@ -138,7 +140,7 @@
</span>
</span>
<div *ngIf="multiSelectActive">
<div *ngIf="multiSelectActive$ | async">
<div
(click)="selectAllOnActivePage()"
class="all-caps-label primary pointer"
@ -203,7 +205,6 @@
(deselectAnnotations)="deselectAnnotations.emit($event)"
(pagesPanelActive)="pagesPanelActive = $event"
(selectAnnotations)="selectAnnotations.emit($event)"
[(multiSelectActive)]="multiSelectActive"
[activeViewerPage]="activeViewerPage"
[annotationActionsTemplate]="annotationActionsTemplate"
[annotations]="(displayedAnnotations$ | async)?.get(activeViewerPage)"
@ -215,7 +216,7 @@
</div>
</ng-container>
<redaction-page-exclusion *ngIf="excludedPageService.show$ | async" [file]="file"></redaction-page-exclusion>
<redaction-page-exclusion *ngIf="excludedPageService.shown$ | async" [file]="file"></redaction-page-exclusion>
</div>
</div>
</div>

View File

@ -14,13 +14,23 @@ import { AnnotationWrapper } from '@models/file/annotation.wrapper';
import { AnnotationProcessingService } from '../../../../services/annotation-processing.service';
import { MatDialogRef, MatDialogState } from '@angular/material/dialog';
import scrollIntoView from 'scroll-into-view-if-needed';
import { CircleButtonTypes, Debounce, FilterService, IconButtonTypes, INestedFilter, IqserEventTarget, Required } from '@iqser/common-ui';
import {
CircleButtonTypes,
Debounce,
FilterService,
IconButtonTypes,
INestedFilter,
IqserEventTarget,
Required,
shareDistinctLast,
} from '@iqser/common-ui';
import { PermissionsService } from '@services/permissions.service';
import { WebViewerInstance } from '@pdftron/webviewer';
import { BehaviorSubject, combineLatest, Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { map, tap } from 'rxjs/operators';
import { File, IViewedPage } from '@red/domain';
import { ExcludedPagesService } from '../../services/excluded-pages.service';
import { MultiSelectService } from '../../services/multi-select.service';
const COMMAND_KEY_ARRAY = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Escape'];
const ALL_HOTKEY_ARRAY = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'];
@ -46,9 +56,7 @@ export class FileWorkloadComponent {
@Input() annotationActionsTemplate: TemplateRef<unknown>;
@Input() viewer: WebViewerInstance;
@Output() readonly shouldDeselectAnnotationsOnPageChangeChange = new EventEmitter<boolean>();
@Output() readonly selectAnnotations = new EventEmitter<
AnnotationWrapper[] | { annotations: AnnotationWrapper[]; multiSelect: boolean }
>();
@Output() readonly selectAnnotations = new EventEmitter<AnnotationWrapper[]>();
@Output() readonly deselectAnnotations = new EventEmitter<AnnotationWrapper[]>();
@Output() readonly selectPage = new EventEmitter<number>();
@Output() readonly toggleSkipped = new EventEmitter<MouseEvent>();
@ -56,18 +64,23 @@ export class FileWorkloadComponent {
displayedPages: number[] = [];
pagesPanelActive = true;
readonly displayedAnnotations$: Observable<Map<number, AnnotationWrapper[]>>;
readonly multiSelectActive$: Observable<boolean>;
readonly multiSelectInactive$: Observable<boolean>;
private _annotations$ = new BehaviorSubject<AnnotationWrapper[]>([]);
@ViewChild('annotationsElement') private readonly _annotationsElement: ElementRef;
@ViewChild('quickNavigation') private readonly _quickNavigationElement: ElementRef;
constructor(
readonly excludedPageService: ExcludedPagesService,
readonly multiSelectService: MultiSelectService,
private readonly _permissionsService: PermissionsService,
private readonly _changeDetectorRef: ChangeDetectorRef,
private readonly _filterService: FilterService,
private readonly _annotationProcessingService: AnnotationProcessingService,
) {
this.displayedAnnotations$ = this._displayedAnnotations$;
this.multiSelectActive$ = this._multiSelectActive$;
this.multiSelectInactive$ = this._multiSelectInactive$;
}
@Input()
@ -75,22 +88,6 @@ export class FileWorkloadComponent {
this._annotations$.next(value);
}
private _multiSelectActive = false;
get multiSelectActive(): boolean {
return this._multiSelectActive;
}
set multiSelectActive(value: boolean) {
this._multiSelectActive = value;
if (!value) {
this.selectAnnotations.emit();
} else {
this.shouldDeselectAnnotationsOnPageChange = false;
this.shouldDeselectAnnotationsOnPageChangeChange.emit(false);
}
}
get activeAnnotations(): AnnotationWrapper[] | undefined {
return this.displayedAnnotations.get(this.activeViewerPage);
}
@ -103,6 +100,21 @@ export class FileWorkloadComponent {
return this.file?.excludedPages?.includes(this.activeViewerPage);
}
private get _multiSelectInactive$() {
return this.multiSelectService.inactive$.pipe(
tap(() => this.selectAnnotations.emit()),
shareDistinctLast(),
);
}
private get _multiSelectActive$() {
const disableDeselectOnPageChange = () => {
this.shouldDeselectAnnotationsOnPageChange = false;
this.shouldDeselectAnnotationsOnPageChangeChange.emit(false);
};
return this.multiSelectService.active$.pipe(tap(disableDeselectOnPageChange), shareDistinctLast());
}
private get _firstSelectedAnnotation() {
return this.selectedAnnotations?.length ? this.selectedAnnotations[0] : null;
}
@ -133,7 +145,7 @@ export class FileWorkloadComponent {
}
pageHasSelection(page: number) {
return this.multiSelectActive && !!this.selectedAnnotations?.find(a => a.pageNumber === page);
return this.multiSelectService.isActive && !!this.selectedAnnotations?.find(a => a.pageNumber === page);
}
selectAllOnActivePage() {
@ -164,7 +176,7 @@ export class FileWorkloadComponent {
// if we activated annotationsPanel -
// select first annotation from this page in case there is no
// selected annotation on this page and not in multi select mode
if (!this.pagesPanelActive && !this.multiSelectActive) {
if (!this.pagesPanelActive && !this.multiSelectService.isActive) {
this._selectFirstAnnotationOnCurrentPageIfNecessary();
}
return;
@ -173,7 +185,7 @@ export class FileWorkloadComponent {
if (!this.pagesPanelActive) {
// Disable annotation navigation in multi select mode
// => TODO: maybe implement selection on enter?
if (!this.multiSelectActive) {
if (!this.multiSelectService.isActive) {
this._navigateAnnotations($event);
}
} else {

View File

@ -36,6 +36,7 @@ import { ActivatedRoute } from '@angular/router';
import { toPosition } from '../../../../utils/pdf-calculation.utils';
import { DossiersService } from '@services/entity-services/dossiers.service';
import { ViewModeService } from '../../services/view-mode.service';
import { MultiSelectService } from '../../services/multi-select.service';
import Tools = Core.Tools;
import TextTool = Tools.TextTool;
import Annotation = Core.Annotations.Annotation;
@ -65,7 +66,6 @@ export class PdfViewerComponent implements OnInit, OnChanges {
@Input() canPerformActions = false;
@Input() annotations: AnnotationWrapper[];
@Input() shouldDeselectAnnotationsOnPageChange = true;
@Input() multiSelectActive: boolean;
@Output() readonly fileReady = new EventEmitter();
@Output() readonly annotationSelected = new EventEmitter<string[]>();
@Output() readonly manualAnnotationRequested = new EventEmitter<ManualRedactionEntryWrapper>();
@ -96,6 +96,7 @@ export class PdfViewerComponent implements OnInit, OnChanges {
private readonly _loadingService: LoadingService,
private readonly _dossiersService: DossiersService,
readonly viewModeService: ViewModeService,
readonly multiSelectService: MultiSelectService,
) {}
private get _dossier(): Dossier {
@ -119,10 +120,6 @@ export class PdfViewerComponent implements OnInit, OnChanges {
if (changes.canPerformActions) {
this._handleCustomActions();
}
if (changes.multiSelectActive) {
this.utils.multiSelectActive = this.multiSelectActive;
}
}
uploadFile(files: any) {
@ -228,7 +225,7 @@ export class PdfViewerComponent implements OnInit, OnChanges {
this.documentViewer = this.instance.Core.documentViewer;
this.annotationManager = this.instance.Core.annotationManager;
this.utils = new PdfViewerUtils(this.instance, this.viewModeService, this.multiSelectActive);
this.utils = new PdfViewerUtils(this.instance, this.viewModeService);
this._setSelectionMode();
this._disableElements();

View File

@ -86,6 +86,7 @@ export class UserManagementComponent implements OnChanges {
const { dossierId, fileId, filename } = file;
this.loadingService.start();
if (!assigneeId) {
await this.filesService.setUnassigned([fileId], dossierId).toPromise();
} else if (file.isUnderReview) {
@ -93,6 +94,7 @@ export class UserManagementComponent implements OnChanges {
} else if (file.isUnderApproval) {
await this.filesService.setUnderApprovalFor([fileId], dossierId, assigneeId).toPromise();
}
this.loadingService.stop();
this.toaster.info(_('assignment.reviewer'), { params: { reviewerName, filename } });

View File

@ -73,23 +73,22 @@
*ngIf="displayPDFViewer"
[annotations]="annotations"
[canPerformActions]="canPerformAnnotationActions$ | async"
[fileData]="displayData"
[fileData]="fileData.fileData"
[file]="file"
[multiSelectActive]="multiSelectActive"
[shouldDeselectAnnotationsOnPageChange]="shouldDeselectAnnotationsOnPageChange"
></redaction-pdf-viewer>
</div>
<div class="right-container">
<iqser-empty-state
*ngIf="file.excluded && !viewDocumentInfo && (excludedPagesService.show$ | async) === false"
*ngIf="file.excluded && !viewDocumentInfo && excludedPagesService.hidden$ | async"
[horizontalPadding]="40"
[text]="'file-preview.tabs.is-excluded' | translate"
icon="red:needs-work"
></iqser-empty-state>
<redaction-document-info
(closeDocumentInfoView)="toggleViewDocumentInfo()"
(closeDocumentInfoView)="viewDocumentInfo = false"
*ngIf="viewDocumentInfo"
[file]="fileData.file"
></redaction-document-info>

View File

@ -23,6 +23,7 @@ import {
OnDetach,
processFilters,
shareDistinctLast,
shareLast,
} from '@iqser/common-ui';
import { MatDialogRef, MatDialogState } from '@angular/material/dialog';
import { ManualRedactionEntryWrapper } from '@models/file/manual-redaction-entry.wrapper';
@ -52,6 +53,7 @@ import { FilesMapService } from '@services/entity-services/files-map.service';
import { WatermarkService } from '@shared/services/watermark.service';
import { ExcludedPagesService } from './services/excluded-pages.service';
import { ViewModeService } from './services/view-mode.service';
import { MultiSelectService } from './services/multi-select.service';
import Annotation = Core.Annotations.Annotation;
import PDFNet = Core.PDFNet;
@ -60,7 +62,7 @@ const ALL_HOTKEY_ARRAY = ['Escape', 'F', 'f'];
@Component({
templateUrl: './file-preview-screen.component.html',
styleUrls: ['./file-preview-screen.component.scss'],
providers: [FilterService, ExcludedPagesService, ViewModeService],
providers: [FilterService, ExcludedPagesService, ViewModeService, MultiSelectService],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnInit, OnDestroy, OnAttach, OnDetach {
@ -116,6 +118,7 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
private readonly _dossiersService: DossiersService,
readonly excludedPagesService: ExcludedPagesService,
readonly viewModeService: ViewModeService,
readonly multiSelectService: MultiSelectService,
) {
super();
this.dossierId = _activatedRoute.snapshot.paramMap.get('dossierId');
@ -125,6 +128,7 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
tap(async file => {
await this._reloadFile(file);
}),
shareLast(),
);
this.showExcludedPages$ = this._showExcludedPages$;
this.canPerformAnnotationActions$ = this._canPerformAnnotationActions$;
@ -152,16 +156,8 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
return this.viewModeService.isStandard ? currentPage : currentPage % 2 === 0 ? currentPage / 2 : (currentPage + 1) / 2;
}
get displayData(): Blob {
return this.fileData?.fileData;
}
get multiSelectActive(): boolean {
return !!this._workloadComponent?.multiSelectActive;
}
private get _showExcludedPages$() {
return this.excludedPagesService.show$.pipe(tap(() => this._disableMultiSelectAndDocumentInfo()));
return this.excludedPagesService.shown$.pipe(tap(() => this._disableMultiSelectAndDocumentInfo()));
}
private get _canPerformAnnotationActions$() {
@ -288,17 +284,17 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
.map(id => this.annotations.find(annotationWrapper => annotationWrapper.id === id))
.filter(ann => ann !== undefined);
if (this.selectedAnnotations.length > 1) {
this._workloadComponent.multiSelectActive = true;
this.multiSelectService.activate();
}
this._workloadComponent.scrollToSelectedAnnotation();
this._changeDetectorRef.markForCheck();
}
selectAnnotations(annotations?: AnnotationWrapper[] | { annotations: AnnotationWrapper[]; multiSelect: boolean }) {
selectAnnotations(annotations?: AnnotationWrapper[]) {
if (annotations) {
this.viewerComponent.utils.selectAnnotations(annotations);
this.viewerComponent?.utils.selectAnnotations(annotations, this.multiSelectService.isActive);
} else {
this.viewerComponent.utils.deselectAllAnnotations();
this.viewerComponent?.utils.deselectAllAnnotations();
}
}
@ -379,7 +375,7 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
}
this._scrollViews();
if (!this._workloadComponent.multiSelectActive) {
if (!this.multiSelectService.isActive) {
this.shouldDeselectAnnotationsOnPageChange = true;
}
@ -420,8 +416,8 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
}
toggleViewDocumentInfo(): void {
this.viewDocumentInfo = !this.viewDocumentInfo;
this._workloadComponent.multiSelectActive = false;
this.viewDocumentInfo = true;
this.multiSelectService.deactivate();
this.excludedPagesService.hide();
}
@ -455,15 +451,13 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
}
private async _reloadFile(file: File): Promise<void> {
this._loadingService.start();
await this._loadFileData(file, true);
await this._cleanupAndRedrawManualAnnotations$().toPromise();
await this._stampPDF();
this._loadingService.stop();
}
private _disableMultiSelectAndDocumentInfo(): void {
this._workloadComponent.multiSelectActive = false;
this.multiSelectService.deactivate();
this.viewDocumentInfo = false;
}
@ -551,12 +545,11 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
if (performUpdate && !!this.fileData) {
this.fileData.redactionLog = fileData.redactionLog;
this.fileData.viewedPages = fileData.viewedPages;
this.rebuildFilters(true);
} else {
this.fileData = fileData;
this.rebuildFilters();
}
this.rebuildFilters();
return;
}

View File

@ -1,14 +1,17 @@
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
import { shareDistinctLast } from '@iqser/common-ui';
import { map } from 'rxjs/operators';
@Injectable()
export class ExcludedPagesService {
readonly show$: Observable<boolean>;
readonly shown$: Observable<boolean>;
readonly hidden$: Observable<boolean>;
private readonly _show$ = new BehaviorSubject(false);
constructor() {
this.show$ = this._show$.asObservable().pipe(shareDistinctLast());
this.shown$ = this._show$.asObservable().pipe(shareDistinctLast());
this.hidden$ = this.shown$.pipe(map(value => !value));
}
show() {

View File

@ -0,0 +1,32 @@
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
import { shareDistinctLast } from '@iqser/common-ui';
import { map } from 'rxjs/operators';
@Injectable()
export class MultiSelectService {
readonly active$: Observable<boolean>;
readonly inactive$: Observable<boolean>;
private readonly _active$ = new BehaviorSubject(false);
constructor() {
this.active$ = this._active$.asObservable().pipe(shareDistinctLast());
this.inactive$ = this.active$.pipe(map(value => !value));
}
get isActive() {
return this._active$.value;
}
activate() {
this._active$.next(true);
}
deactivate() {
this._active$.next(false);
}
toggle() {
this._active$.next(!this._active$.value);
}
}

View File

@ -73,7 +73,7 @@
<iqser-circle-button
(action)="excludedPagesService.toggle()"
*ngIf="excludedPagesService && showExcludePages"
[attr.aria-expanded]="excludedPagesService.show$ | async"
[attr.aria-expanded]="excludedPagesService.shown$ | async"
[showDot]="!!file.excludedPages?.length"
[tooltip]="'file-preview.exclude-pages' | translate"
icon="red:exclude-pages"

View File

@ -41,7 +41,7 @@ export class PdfViewerUtils {
ready = false;
excludedPages: number[] = [];
constructor(readonly instance: WebViewerInstance, readonly viewModeService: ViewModeService, public multiSelectActive: boolean) {}
constructor(readonly instance: WebViewerInstance, readonly viewModeService: ViewModeService) {}
get paginationOffset() {
return this.viewModeService.isCompare ? 2 : 1;
@ -120,18 +120,8 @@ export class PdfViewerUtils {
this._annotationManager.deselectAllAnnotations();
}
selectAnnotations($event: AnnotationWrapper[] | { annotations: AnnotationWrapper[]; multiSelect: boolean }) {
let annotations: AnnotationWrapper[];
let multiSelect: boolean;
if ($event instanceof Array) {
annotations = $event;
multiSelect = false;
} else {
annotations = $event.annotations;
multiSelect = $event.multiSelect;
}
if (!this.multiSelectActive && !multiSelect) {
selectAnnotations(annotations: AnnotationWrapper[], multiSelectActive: boolean) {
if (!multiSelectActive) {
this.deselectAllAnnotations();
}