Merge branch 'master' into VM/RED-7340

This commit is contained in:
Valentin Mihai 2024-10-08 00:52:34 +03:00
commit d67bef3001
6 changed files with 116 additions and 106 deletions

View File

@ -1,29 +1,31 @@
<div
*ngIf="noSelection && changesTooltip"
[matTooltip]="changesTooltip"
class="chip"
matTooltipClass="multiline"
matTooltipPosition="above"
>
<mat-icon [svgIcon]="'red:redaction-changes'"></mat-icon>
</div>
@if (_noSelection() && _changesTooltip()) {
<div [matTooltip]="_changesTooltip()" class="chip" matTooltipClass="multiline" matTooltipPosition="above">
<mat-icon [svgIcon]="'red:redaction-changes'"></mat-icon>
</div>
}
<ng-container *ngIf="noSelection && engines">
<div #trigger="cdkOverlayOrigin" (mouseout)="isPopoverOpen = false" (mouseover)="isPopoverOpen = true" cdkOverlayOrigin class="chip">
<mat-icon *ngFor="let engine of engines" [svgIcon]="engine.icon"></mat-icon>
@if (_noSelection() && _engines()) {
<div
#trigger="cdkOverlayOrigin"
(mouseout)="_isPopoverOpen.set(false)"
(mouseover)="_isPopoverOpen.set(true)"
cdkOverlayOrigin
class="chip"
>
<mat-icon *ngFor="let engine of _engines()" [svgIcon]="engine.icon"></mat-icon>
</div>
<ng-template
[cdkConnectedOverlayOffsetY]="-8"
[cdkConnectedOverlayOpen]="isPopoverOpen"
[cdkConnectedOverlayOpen]="_isPopoverOpen()"
[cdkConnectedOverlayOrigin]="trigger"
cdkConnectedOverlay
>
<div class="popover">
<div *ngFor="let engine of engines" class="flex-align-items-center">
<div *ngFor="let engine of _engines()" class="flex-align-items-center">
<mat-icon [svgIcon]="engine.icon"></mat-icon>
<span [innerHTML]="engine.description | translate : engine.translateParams"></span>
<span [innerHTML]="engine.description | translate: engine.translateParams"></span>
</div>
</div>
</ng-template>
</ng-container>
}

View File

@ -1,4 +1,4 @@
import { Component, inject, Input, OnChanges } from '@angular/core';
import { Component, computed, inject, input, signal } from '@angular/core';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { KeysOf } from '@iqser/common-ui/lib/utils';
import { AnnotationWrapper } from '@models/file/annotation.wrapper';
@ -33,30 +33,27 @@ const changesProperties: KeysOf<AnnotationWrapper>[] = [
];
@Component({
selector: 'redaction-annotation-details [annotation]',
selector: 'redaction-annotation-details',
templateUrl: './annotation-details.component.html',
styleUrls: ['./annotation-details.component.scss'],
standalone: true,
imports: [NgIf, MatTooltip, MatIcon, CdkOverlayOrigin, NgForOf, CdkConnectedOverlay, TranslateModule],
})
export class AnnotationDetailsComponent implements OnChanges {
@Input() annotation: ListItem<AnnotationWrapper>;
isPopoverOpen = false;
engines: Engine[];
changesTooltip: string;
noSelection: boolean;
export class AnnotationDetailsComponent {
readonly annotation = input.required<ListItem<AnnotationWrapper>>();
protected readonly _isPopoverOpen = signal(false);
protected readonly _engines = computed(() => this.#extractEngines(this.annotation().item).filter(engine => engine.show));
private readonly _translateService = inject(TranslateService);
private readonly _multiSelectService = inject(MultiSelectService);
protected readonly _changesTooltip = computed(() => {
const annotation = this.annotation().item;
const changes = changesProperties.filter(key => annotation[key]);
getChangesTooltip(): string | undefined {
const changes = changesProperties.filter(key => this.annotation.item[key]);
if (!changes.length && !this.annotation.item.engines?.includes(LogEntryEngines.MANUAL)) {
if (!changes.length && !annotation.engines?.includes(LogEntryEngines.MANUAL)) {
return;
}
const details = [];
if (this.annotation.item.engines?.includes(LogEntryEngines.MANUAL)) {
if (annotation.engines?.includes(LogEntryEngines.MANUAL)) {
details.push(this._translateService.instant(_('annotation-changes.added-locally')));
}
@ -66,13 +63,9 @@ export class AnnotationDetailsComponent implements OnChanges {
const header = this._translateService.instant(_('annotation-changes.header'));
return [header, ...details.map(change => `${change}`)].join('\n');
}
ngOnChanges() {
this.engines = this.#extractEngines(this.annotation.item).filter(engine => engine.show);
this.changesTooltip = this.getChangesTooltip();
this.noSelection = !this.annotation.isSelected || this._multiSelectService.inactive();
}
});
private readonly _multiSelectService = inject(MultiSelectService);
protected readonly _noSelection = computed(() => !this.annotation().isSelected || this._multiSelectService.inactive());
#extractEngines(annotation: AnnotationWrapper): Engine[] {
return [

View File

@ -1,6 +1,6 @@
<div class="active-bar-marker"></div>
<div [class.removed]="annotation().item.isRemoved" class="annotation">
<div #annotationDiv [class.removed]="annotation().item.isRemoved" class="annotation">
<redaction-annotation-card
[annotation]="annotation().item"
[isSelected]="annotation().isSelected"
@ -24,13 +24,15 @@
}
@if (multiSelectService.inactive()) {
<div class="actions">
<redaction-annotation-actions
[actionsHelpModeKey]="actionsHelpModeKey()"
[annotations]="[annotation().item]"
[canPerformAnnotationActions]="pdfProxyService.canPerformActions()"
></redaction-annotation-actions>
</div>
@defer (on hover(annotationDiv)) {
<div class="actions">
<redaction-annotation-actions
[actionsHelpModeKey]="actionsHelpModeKey()"
[annotations]="[annotation().item]"
[canPerformAnnotationActions]="pdfProxyService.canPerformActions()"
></redaction-annotation-actions>
</div>
}
}
</div>
}

View File

@ -11,7 +11,7 @@ import { AnnotationReferencesService } from '../../services/annotation-reference
import { AnnotationsListingService } from '../../services/annotations-listing.service';
import { MultiSelectService } from '../../services/multi-select.service';
import { ViewModeService } from '../../services/view-mode.service';
import { NgForOf, NgIf } from '@angular/common';
import { JsonPipe, NgForOf, NgIf } from '@angular/common';
import { HighlightsSeparatorComponent } from '../highlights-separator/highlights-separator.component';
import { AnnotationWrapperComponent } from '../annotation-wrapper/annotation-wrapper.component';
import { AnnotationReferencesListComponent } from '../annotation-references-list/annotation-references-list.component';
@ -21,7 +21,7 @@ import { AnnotationReferencesListComponent } from '../annotation-references-list
templateUrl: './annotations-list.component.html',
styleUrls: ['./annotations-list.component.scss'],
standalone: true,
imports: [NgForOf, NgIf, HighlightsSeparatorComponent, AnnotationWrapperComponent, AnnotationReferencesListComponent],
imports: [NgForOf, NgIf, HighlightsSeparatorComponent, AnnotationWrapperComponent, AnnotationReferencesListComponent, JsonPipe],
})
export class AnnotationsListComponent extends HasScrollbarDirective {
readonly annotations = input.required<ListItem<AnnotationWrapper>[]>();

View File

@ -32,11 +32,11 @@
</ng-template>
</ng-container>
@if (displayedAnnotations$ | async; as annotations) {
@if (filteredAnnotations$ | async; as annotations) {
<div class="right-content">
<ng-container *ngIf="!isDocumine">
<redaction-readonly-banner
*ngIf="showAnalysisDisabledBanner; else readOnlyBanner"
*ngIf="showAnalysisDisabledBanner(); else readOnlyBanner"
[customTranslation]="translations.analysisDisabled"
></redaction-readonly-banner>
<ng-template #readOnlyBanner>
@ -112,7 +112,7 @@
></iqser-circle-button>
<span
[translateParams]="{ page: pdf.currentPage(), count: activeAnnotations.length }"
[translateParams]="{ page: pdf.currentPage(), count: activeAnnotations().length }"
[translate]="'page'"
class="all-caps-label"
></span>
@ -200,7 +200,7 @@
<redaction-annotations-list
(pagesPanelActive)="pagesPanelActive = $event"
[annotations]="annotations.get(pdf.currentPage())"
[annotations]="annotationsList$ | async"
></redaction-annotations-list>
</div>
}
@ -224,7 +224,7 @@
</ng-template>
<ng-template #documineHeader>
<span [translate]="'annotations'" [attr.help-mode-key]="'annotations_list'"></span>
<span [attr.help-mode-key]="'annotations_list'" [translate]="'annotations'"></span>
<ng-container *ngTemplateOutlet="annotationsFilter"></ng-container>
</ng-template>

View File

@ -9,6 +9,7 @@ import {
OnDestroy,
OnInit,
TemplateRef,
untracked,
viewChild,
} from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
@ -89,23 +90,30 @@ const ALL_HOTKEY_ARRAY = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'];
],
})
export class FileWorkloadComponent extends AutoUnsubscribe implements OnInit, OnDestroy {
private readonly _annotationsElement = viewChild<ElementRef>('annotationsElement');
private readonly _quickNavigationElement = viewChild<ElementRef>('quickNavigation');
readonly multiSelectTemplate = viewChild<TemplateRef<any>>('multiSelect');
readonly #isIqserDevMode = this._userPreferenceService.isIqserDevMode;
readonly annotationsList$: Observable<ListItem<AnnotationWrapper>[]>;
readonly allPages = computed(() => Array.from({ length: this.state.file()?.numberOfPages }, (_x, i) => i + 1));
protected readonly iconButtonTypes = IconButtonTypes;
protected readonly circleButtonTypes = CircleButtonTypes;
protected readonly displayedAnnotations$: Observable<Map<number, ListItem<AnnotationWrapper>[]>>;
protected readonly filteredAnnotations$: Observable<Map<number, ListItem<AnnotationWrapper>[]>>;
protected readonly title = computed(() =>
this.viewModeService.isEarmarks() ? _('file-preview.tabs.highlights.label') : _('file-preview.tabs.annotations.label'),
);
protected readonly currentPageIsExcluded = computed(() => this.state.file().excludedPages.includes(this.pdf.currentPage()));
protected readonly translations = workloadTranslations;
protected readonly isDocumine = getConfig().IS_DOCUMINE;
readonly showAnalysisDisabledBanner = computed(() => {
const file = this.state.file();
return this.isDocumine && file.excludedFromAutomaticAnalysis && file.workflowStatus !== WorkflowFileStatuses.APPROVED;
});
protected displayedAnnotations = new Map<number, AnnotationWrapper[]>();
readonly activeAnnotations = computed(() => this.displayedAnnotations.get(this.pdf.currentPage()) || []);
protected displayedPages: number[] = [];
protected pagesPanelActive = true;
protected enabledFilters = [];
private readonly _annotationsElement = viewChild<ElementRef>('annotationsElement');
private readonly _quickNavigationElement = viewChild<ElementRef>('quickNavigation');
readonly #isIqserDevMode = this._userPreferenceService.isIqserDevMode;
#displayedPagesChanged = false;
constructor(
@ -129,10 +137,9 @@ export class FileWorkloadComponent extends AutoUnsubscribe implements OnInit, On
) {
super();
// TODO: ngOnDetach is not called here, so we need to unsubscribe manually
this.addActiveScreenSubscription = this.pdf.currentPage$.subscribe(pageNumber => {
effect(() => {
this._scrollViews();
this.scrollAnnotationsToPage(pageNumber, 'always');
this.scrollAnnotationsToPage(this.pdf.currentPage(), 'always');
});
this.addActiveScreenSubscription = this.listingService.selected$.subscribe(annotationIds => {
@ -146,7 +153,11 @@ export class FileWorkloadComponent extends AutoUnsubscribe implements OnInit, On
this.handleKeyEvent($event);
});
this.displayedAnnotations$ = this._displayedAnnotations$;
this.filteredAnnotations$ = this._filteredAnnotations$;
this.annotationsList$ = combineLatest([this.filteredAnnotations$, this.pdf.currentPage$]).pipe(
map(([annotations, page]) => annotations.get(page)),
);
effect(() => {
if (this.multiSelectService.inactive()) {
@ -169,20 +180,11 @@ export class FileWorkloadComponent extends AutoUnsubscribe implements OnInit, On
);
}
get activeAnnotations(): AnnotationWrapper[] {
return this.displayedAnnotations.get(this.pdf.currentPage()) || [];
}
get showAnalysisDisabledBanner() {
const file = this.state.file();
return this.isDocumine && file.excludedFromAutomaticAnalysis && file.workflowStatus !== WorkflowFileStatuses.APPROVED;
}
private get _firstSelectedAnnotation() {
return this.listingService.selected.length ? this.listingService.selected[0] : null;
}
private get _displayedAnnotations$(): Observable<Map<number, ListItem<AnnotationWrapper>[]>> {
private get _filteredAnnotations$(): Observable<Map<number, ListItem<AnnotationWrapper>[]>> {
const primary$ = this.filterService.getFilterModels$('primaryFilters');
const secondary$ = this.filterService.getFilterModels$('secondaryFilters');
@ -205,10 +207,6 @@ export class FileWorkloadComponent extends AutoUnsubscribe implements OnInit, On
);
}
get #allPages() {
return Array.from({ length: this.state.file()?.numberOfPages }, (_x, i) => i + 1);
}
private static _scrollToFirstElement(elements: HTMLElement[], mode: 'always' | 'if-needed' = 'if-needed') {
if (elements.length > 0) {
scrollIntoView(elements[0], {
@ -222,12 +220,13 @@ export class FileWorkloadComponent extends AutoUnsubscribe implements OnInit, On
ngOnInit(): void {
setTimeout(() => {
const showExcludePages = getLocalStorageDataByFileId(this.state.file()?.id, 'show-exclude-pages') ?? false;
const file = untracked(this.state.file);
const showExcludePages = getLocalStorageDataByFileId(file?.id, 'show-exclude-pages') ?? false;
if (showExcludePages) {
this.excludedPagesService.show();
}
const showDocumentInfo = getLocalStorageDataByFileId(this.state.file()?.id, 'show-document-info') ?? false;
const showDocumentInfo = getLocalStorageDataByFileId(file?.id, 'show-document-info') ?? false;
if (showDocumentInfo) {
this.documentInfoService.show();
}
@ -235,16 +234,20 @@ export class FileWorkloadComponent extends AutoUnsubscribe implements OnInit, On
}
selectAllOnActivePage() {
this.listingService.selectAnnotations(this.activeAnnotations);
const activeAnnotations = untracked(this.activeAnnotations);
this.listingService.selectAnnotations(activeAnnotations);
}
deselectAllOnActivePage(): void {
this.listingService.deselect(this.activeAnnotations);
this.annotationManager.deselect(this.activeAnnotations);
const activeAnnotations = untracked(this.activeAnnotations);
this.listingService.deselect(activeAnnotations);
this.annotationManager.deselect(activeAnnotations);
}
@HostListener('window:keyup', ['$event'])
handleKeyEvent($event: KeyboardEvent): void {
const multiSelectServiceInactive = untracked(this.multiSelectService.inactive);
if (
!ALL_HOTKEY_ARRAY.includes($event.key) ||
this._dialog.openDialogs.length ||
@ -264,7 +267,7 @@ export class FileWorkloadComponent extends AutoUnsubscribe implements OnInit, On
// 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.multiSelectService.inactive()) {
if (!this.pagesPanelActive && multiSelectServiceInactive) {
this._documentViewer.clearSelection();
this.#selectFirstAnnotationOnCurrentPageIfNecessary();
}
@ -275,7 +278,7 @@ export class FileWorkloadComponent extends AutoUnsubscribe implements OnInit, On
if (!this.pagesPanelActive) {
// Disable annotation navigation in multi select mode
// => TODO: maybe implement selection on enter?
if (this.multiSelectService.inactive()) {
if (multiSelectServiceInactive) {
this.navigateAnnotations($event);
}
} else {
@ -286,7 +289,7 @@ export class FileWorkloadComponent extends AutoUnsubscribe implements OnInit, On
}
scrollAnnotations(): void {
const currentPage = this.pdf.currentPage();
const currentPage = untracked(this.pdf.currentPage);
if (this._firstSelectedAnnotation?.pageNumber === currentPage) {
return;
}
@ -294,27 +297,27 @@ export class FileWorkloadComponent extends AutoUnsubscribe implements OnInit, On
}
scrollAnnotationsToPage(page: number, mode: 'always' | 'if-needed' = 'if-needed'): void {
if (this._annotationsElement()) {
const elements: HTMLElement[] = this._annotationsElement().nativeElement.querySelectorAll(
`div[anotation-page-header="${page}"]`,
);
const annotationsElement = untracked(this._annotationsElement);
if (annotationsElement) {
const elements: HTMLElement[] = annotationsElement.nativeElement.querySelectorAll(`div[anotation-page-header="${page}"]`);
FileWorkloadComponent._scrollToFirstElement(elements, mode);
}
}
@Debounce()
scrollToSelectedAnnotation(): void {
if (this.listingService.selected.length === 0 || !this._annotationsElement()) {
const annotationsElement = untracked(this._annotationsElement);
if (this.listingService.selected.length === 0 || annotationsElement) {
return;
}
const elements: HTMLElement[] = this._annotationsElement().nativeElement.querySelectorAll(
const elements: HTMLElement[] = annotationsElement.nativeElement.querySelectorAll(
`[annotation-id="${this._firstSelectedAnnotation?.id}"]`,
);
FileWorkloadComponent._scrollToFirstElement(elements);
}
scrollQuickNavigation(): void {
const currentPage = this.pdf.currentPage();
const currentPage = untracked(this.pdf.currentPage);
let quickNavPageIndex = this.displayedPages.findIndex(p => p >= currentPage);
if (quickNavPageIndex === -1 || this.displayedPages[quickNavPageIndex] !== currentPage) {
quickNavPageIndex = Math.max(0, quickNavPageIndex - 1);
@ -327,7 +330,8 @@ export class FileWorkloadComponent extends AutoUnsubscribe implements OnInit, On
}
scrollQuickNavLast() {
this.pdf.navigateTo(this.state.file().numberOfPages);
const file = untracked(this.state.file);
this.pdf.navigateTo(file.numberOfPages);
}
preventKeyDefault($event: KeyboardEvent): void {
@ -345,11 +349,12 @@ export class FileWorkloadComponent extends AutoUnsubscribe implements OnInit, On
}
navigateAnnotations($event: KeyboardEvent) {
const currentPage = this.pdf.currentPage();
const currentPage = untracked(this.pdf.currentPage);
const activeAnnotations = untracked(this.activeAnnotations);
if (!this._firstSelectedAnnotation || currentPage !== this._firstSelectedAnnotation.pageNumber) {
if (this.displayedPages.indexOf(currentPage) !== -1) {
// Displayed page has annotations
return this.listingService.selectAnnotations(this.activeAnnotations ? this.activeAnnotations[0] : null);
return this.listingService.selectAnnotations(activeAnnotations ? activeAnnotations[0] : null);
}
// Displayed page doesn't have annotations
if ($event.key === 'ArrowDown') {
@ -422,14 +427,16 @@ export class FileWorkloadComponent extends AutoUnsubscribe implements OnInit, On
secondary: INestedFilter[] = [],
componentReferenceIds: string[],
): Map<number, AnnotationWrapper[]> {
const onlyPageWithAnnotations = this.viewModeService.onlyPagesWithAnnotations();
const onlyPageWithAnnotations = untracked(this.viewModeService.onlyPagesWithAnnotations);
const isRedacted = untracked(this.viewModeService.isRedacted);
const allPages = untracked(this.allPages);
if (!primary || primary.length === 0) {
const pages = onlyPageWithAnnotations ? [] : this.#allPages;
const pages = onlyPageWithAnnotations ? [] : allPages;
this.#setDisplayedPages(pages);
return;
}
if (this.viewModeService.isRedacted()) {
if (isRedacted) {
annotations = annotations.filter(a => !a.isRemoved);
}
@ -447,7 +454,7 @@ export class FileWorkloadComponent extends AutoUnsubscribe implements OnInit, On
this.enabledFilters = this.filterService.enabledFlatFilters;
if (this.enabledFilters.some(f => f.id === 'pages-without-annotations')) {
if (this.enabledFilters.length === 1 && !onlyPageWithAnnotations) {
const pages = this.#allPages.filter(page => !pagesThatDisplayAnnotations.includes(page));
const pages = allPages.filter(page => !pagesThatDisplayAnnotations.includes(page));
this.#setDisplayedPages(pages);
} else {
this.#setDisplayedPages([]);
@ -456,7 +463,7 @@ export class FileWorkloadComponent extends AutoUnsubscribe implements OnInit, On
} else if (this.enabledFilters.length || onlyPageWithAnnotations || componentReferenceIds) {
this.#setDisplayedPages(pagesThatDisplayAnnotations);
} else {
this.#setDisplayedPages(this.#allPages);
this.#setDisplayedPages(allPages);
}
this.displayedPages.sort((a, b) => a - b);
@ -464,18 +471,20 @@ export class FileWorkloadComponent extends AutoUnsubscribe implements OnInit, On
}
#selectFirstAnnotationOnCurrentPageIfNecessary() {
const currentPage = this.pdf.currentPage();
const currentPage = untracked(this.pdf.currentPage);
const activeAnnotations = untracked(this.activeAnnotations);
if (
(!this._firstSelectedAnnotation || currentPage !== this._firstSelectedAnnotation.pageNumber) &&
this.displayedPages.indexOf(currentPage) >= 0 &&
this.activeAnnotations.length > 0
activeAnnotations.length > 0
) {
this.listingService.selectAnnotations(this.activeAnnotations[0]);
this.listingService.selectAnnotations(activeAnnotations[0]);
}
}
#navigatePages($event: KeyboardEvent) {
const pageIdx = this.displayedPages.indexOf(this.pdf.currentPage());
const currentPage = untracked(this.pdf.currentPage);
const pageIdx = this.displayedPages.indexOf(currentPage);
if ($event.key !== 'ArrowDown') {
if (pageIdx === -1) {
@ -507,9 +516,10 @@ export class FileWorkloadComponent extends AutoUnsubscribe implements OnInit, On
}
#nextPageWithAnnotations() {
const currentPage = untracked(this.pdf.currentPage);
let idx = 0;
for (const page of this.displayedPages) {
if (page > this.pdf.currentPage() && this.displayedAnnotations.get(page)) {
if (page > currentPage && this.displayedAnnotations.get(page)) {
break;
}
++idx;
@ -518,10 +528,11 @@ export class FileWorkloadComponent extends AutoUnsubscribe implements OnInit, On
}
#prevPageWithAnnotations() {
const currentPage = untracked(this.pdf.currentPage);
let idx = this.displayedPages.length - 1;
const reverseDisplayedPages = [...this.displayedPages].reverse();
for (const page of reverseDisplayedPages) {
if (page < this.pdf.currentPage() && this.displayedAnnotations.get(page)) {
if (page < currentPage && this.displayedAnnotations.get(page)) {
break;
}
--idx;
@ -530,8 +541,9 @@ export class FileWorkloadComponent extends AutoUnsubscribe implements OnInit, On
}
#scrollQuickNavigationToPage(page: number) {
if (this._quickNavigationElement()) {
const elements: HTMLElement[] = this._quickNavigationElement().nativeElement.querySelectorAll(`#quick-nav-page-${page}`);
const quickNavigationElement = untracked(this._quickNavigationElement);
if (quickNavigationElement) {
const elements: HTMLElement[] = quickNavigationElement.nativeElement.querySelectorAll(`#quick-nav-page-${page}`);
FileWorkloadComponent._scrollToFirstElement(elements);
}
}
@ -552,7 +564,8 @@ export class FileWorkloadComponent extends AutoUnsubscribe implements OnInit, On
}
#scrollToFirstAnnotationPage(annotations: Map<number, ListItem<AnnotationWrapper>[]>) {
if (this.isDocumine && annotations.size && this.#displayedPagesChanged && !this.displayedPages.includes(this.pdf.currentPage())) {
const currentPage = untracked(this.pdf.currentPage);
if (this.isDocumine && annotations.size && this.#displayedPagesChanged && !this.displayedPages.includes(currentPage)) {
const page = annotations.keys().next().value;
this.pdf.navigateTo(page);
}