diff --git a/apps/red-ui/src/app/app.module.ts b/apps/red-ui/src/app/app.module.ts index fcd5ce499..68d0d847e 100644 --- a/apps/red-ui/src/app/app.module.ts +++ b/apps/red-ui/src/app/app.module.ts @@ -63,6 +63,7 @@ import { HumanizePipe } from './utils/humanize.pipe'; import { ManualAnnotationDialogComponent } from './dialogs/manual-redaction-dialog/manual-annotation-dialog.component'; import { FileNotAvailableOverlayComponent } from './screens/file/file-not-available-overlay/file-not-available-overlay.component'; import { ToastComponent } from './components/toast/toast.component'; +import { AnnotationFilterComponent } from './screens/file/annotation-filter/annotation-filter.component'; export function HttpLoaderFactory(httpClient: HttpClient) { return new TranslateHttpLoader(httpClient, '/assets/i18n/', '.json'); @@ -90,7 +91,8 @@ export function HttpLoaderFactory(httpClient: HttpClient) { AuthErrorComponent, HumanizePipe, ToastComponent, - FileNotAvailableOverlayComponent + FileNotAvailableOverlayComponent, + AnnotationFilterComponent ], imports: [ BrowserModule, diff --git a/apps/red-ui/src/app/screens/file/annotation-filter/annotation-filter.component.html b/apps/red-ui/src/app/screens/file/annotation-filter/annotation-filter.component.html new file mode 100644 index 000000000..4980531ef --- /dev/null +++ b/apps/red-ui/src/app/screens/file/annotation-filter/annotation-filter.component.html @@ -0,0 +1,75 @@ +
+ +
+ +
+
+
+
+
+
+
+
+
+
+ + + + +
+ + + + {{ 'file-preview.filter-menu.' + filter.key + '.label' | translate }} + +
+
+
+ + + {{ appStateService.getDictionaryLabel(subFilter.key) }} + +
+
+
+
+
diff --git a/apps/red-ui/src/app/screens/file/annotation-filter/annotation-filter.component.scss b/apps/red-ui/src/app/screens/file/annotation-filter/annotation-filter.component.scss new file mode 100644 index 000000000..05d2d7362 --- /dev/null +++ b/apps/red-ui/src/app/screens/file/annotation-filter/annotation-filter.component.scss @@ -0,0 +1,27 @@ +@import '../../../../assets/styles/red-variables'; + +.filter-root { + position: relative; + + .dot { + background: $primary; + height: 10px; + width: 10px; + border-radius: 50%; + position: absolute; + top: 0; + left: 0; + } +} + +.filter-menu-header { + display: flex; + justify-content: space-between; + padding: 7px 15px 15px; + width: 350px; + + .actions { + display: flex; + gap: 8px; + } +} diff --git a/apps/red-ui/src/app/screens/file/annotation-filter/annotation-filter.component.ts b/apps/red-ui/src/app/screens/file/annotation-filter/annotation-filter.component.ts new file mode 100644 index 000000000..d68cc6fac --- /dev/null +++ b/apps/red-ui/src/app/screens/file/annotation-filter/annotation-filter.component.ts @@ -0,0 +1,118 @@ +import { Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core'; +import { AnnotationWrapper } from '../model/annotation.wrapper'; +import { ManualRedactions } from '@redaction/red-ui-http'; +import { AnnotationFilter } from '../../../utils/types'; +import { AppStateService } from '../../../state/app-state.service'; + +@Component({ + selector: 'redaction-annotation-filter', + templateUrl: './annotation-filter.component.html', + styleUrls: ['./annotation-filter.component.scss'] +}) +export class AnnotationFilterComponent implements OnChanges { + @Input() annotations: AnnotationWrapper[]; + @Input() manualRedactions: ManualRedactions; + + @Output() filtersChanged = new EventEmitter(); + + filters: AnnotationFilter[] = []; + + constructor(public readonly appStateService: AppStateService) {} + + ngOnChanges(changes: SimpleChanges): void { + if (this.annotations) { + this.filters = this._getFilters(); + this.filtersChanged.emit(this.filters); + } + } + + filterCheckboxClicked($event: any, filter: AnnotationFilter, parent?: AnnotationFilter) { + filter.checked = !filter.checked; + if (parent) { + this._handleCheckedValue(parent); + } else { + filter.indeterminate = false; + filter.filters.forEach((f) => (f.checked = filter.checked)); + } + } + + activateAllFilters() { + this._setAlLFilters(true); + } + + deactivateAllFilters() { + this._setAlLFilters(false); + } + + get hasActiveFilters(): boolean { + for (const filter of this.filters) { + if (filter.checked || filter.indeterminate) { + return true; + } + } + return false; + } + + applyFilters() { + this.filtersChanged.emit(this.filters); + } + + toggleFilterExpanded($event: MouseEvent, filter: AnnotationFilter) { + $event.stopPropagation(); + filter.expanded = !filter.expanded; + } + + private _setAlLFilters(value: boolean) { + this.filters.forEach((f) => { + f.checked = value; + f.indeterminate = value; + f.filters.forEach((ff) => { + ff.checked = value; + }); + }); + } + + private _getFilters(): AnnotationFilter[] { + const filters: AnnotationFilter[] = []; + const availableAnnotationTypes = {}; + + this.annotations?.forEach((a) => { + if (a.superType === 'hint' || a.superType === 'redaction') { + const entry = availableAnnotationTypes[a.superType]; + if (!entry) { + availableAnnotationTypes[a.superType] = new Set([a.dictionary]); + } else { + entry.add(a.dictionary); + } + } else { + availableAnnotationTypes[a.superType] = new Set(); + } + }); + + for (const key of Object.keys(availableAnnotationTypes)) { + const filter: AnnotationFilter = { + key: key, + filters: Array.from(availableAnnotationTypes[key]).map((dc) => { + const defaultFilter = this.appStateService.dictionaryData[dc]?.defaultFilter; + return { key: dc, checked: defaultFilter, filters: [] }; + }) + }; + this._handleCheckedValue(filter); + if (filter.checked || filter.indeterminate) { + filter.expanded = true; + } + filters.push(filter); + } + + return filters; + } + + private _handleCheckedValue(filter: AnnotationFilter) { + filter.checked = filter.filters.reduce((acc, next) => acc && next.checked, true); + if (filter.checked) { + filter.indeterminate = false; + } else { + filter.indeterminate = filter.filters.reduce((acc, next) => acc || next.checked, false); + } + } +} diff --git a/apps/red-ui/src/app/screens/file/file-preview-screen/file-preview-screen.component.html b/apps/red-ui/src/app/screens/file/file-preview-screen/file-preview-screen.component.html index 502e773e0..cfd9dd2cf 100644 --- a/apps/red-ui/src/app/screens/file/file-preview-screen/file-preview-screen.component.html +++ b/apps/red-ui/src/app/screens/file/file-preview-screen/file-preview-screen.component.html @@ -87,93 +87,11 @@
-
- -
- -
-
-
-
-
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
+
@@ -199,7 +117,7 @@
div { - position: relative; - - .dot { - background: $primary; - height: 10px; - width: 10px; - border-radius: 50%; - position: absolute; - top: 0; - left: 0; - } - } - .close-icon { height: 14px; width: 14px; @@ -165,15 +151,3 @@ redaction-pdf-viewer { } } } - -.filter-menu-header { - display: flex; - justify-content: space-between; - padding: 7px 15px 15px; - width: 350px; - - .actions { - display: flex; - gap: 8px; - } -} diff --git a/apps/red-ui/src/app/screens/file/file-preview-screen/file-preview-screen.component.ts b/apps/red-ui/src/app/screens/file/file-preview-screen/file-preview-screen.component.ts index aa9f42429..3fa3b37c1 100644 --- a/apps/red-ui/src/app/screens/file/file-preview-screen/file-preview-screen.component.ts +++ b/apps/red-ui/src/app/screens/file/file-preview-screen/file-preview-screen.component.ts @@ -16,7 +16,6 @@ import { AnnotationUtils } from '../../../utils/annotation-utils'; import { UserService } from '../../../user/user.service'; import { debounce } from '../../../utils/debounce'; import scrollIntoView from 'scroll-into-view-if-needed'; -import { FiltersService } from '../service/filters.service'; import { FileDownloadService } from '../service/file-download.service'; import { saveAs } from 'file-saver'; import { FileType } from '../model/file-type'; @@ -42,7 +41,7 @@ export class FilePreviewScreenComponent implements OnInit { private _dialogRef: MatDialogRef; @ViewChild(PdfViewerComponent) private _viewerComponent: PdfViewerComponent; - @ViewChild('annotations') private _annotationsElement: ElementRef; + @ViewChild('annotationsElement') private _annotationsElement: ElementRef; @ViewChild('quickNavigation') private _quickNavigationElement: ElementRef; public fileData: FileDataModel; @@ -50,7 +49,6 @@ export class FilePreviewScreenComponent implements OnInit { public annotations: AnnotationWrapper[] = []; public displayedAnnotations: { [key: number]: { annotations: AnnotationWrapper[] } } = {}; public selectedAnnotation: AnnotationWrapper; - public filters: AnnotationFilter[]; public pagesPanelActive = true; public viewReady = false; @@ -64,7 +62,6 @@ export class FilePreviewScreenComponent implements OnInit { private readonly _manualAnnotationService: ManualAnnotationService, private readonly _fileDownloadService: FileDownloadService, private readonly _reanalysisControllerService: ReanalysisControllerService, - private readonly _filtersService: FiltersService, private ngZone: NgZone ) { this._activatedRoute.params.subscribe((params) => { @@ -78,14 +75,6 @@ export class FilePreviewScreenComponent implements OnInit { return this.userService.user; } - public filterKeys(key?: string) { - if (key) { - return Object.keys(this.filters[key]); - } - - return Object.keys(this.filters); - } - public get redactedView() { return this._activeViewer === 'REDACTED'; } @@ -95,7 +84,6 @@ export class FilePreviewScreenComponent implements OnInit { } public ngOnInit(): void { - this.filters = this._filtersService.getFilters(this.appStateService.dictionaryData); this._loadFileData(); this.appStateService.fileStatusChanged.subscribe((fileStatus) => { if (fileStatus.fileId === this.fileId) { @@ -106,19 +94,15 @@ export class FilePreviewScreenComponent implements OnInit { } private _loadFileData() { - this._fileDownloadService.loadFileData(this.fileId).subscribe((fileDataModel) => { - this.fileData = fileDataModel; - this.annotations = fileDataModel.redactionLog.redactionLogEntry.map( - (rde) => new AnnotationWrapper(rde, null) - ); - this.filters = this._filtersService.getFilters( - this.appStateService.dictionaryData, - this.annotations - ); - this.applyFilters(); - this._changeDetectorRef.detectChanges(); - console.log(this.annotations); - }); + this._fileDownloadService + .loadFileData(this.appStateService.activeProjectId, this.fileId) + .subscribe((fileDataModel) => { + this.fileData = fileDataModel; + this.annotations = fileDataModel.redactionLog.redactionLogEntry.map( + (rde) => new AnnotationWrapper(rde, null) + ); + this._changeDetectorRef.detectChanges(); + }); } public openFileDetailsDialog($event: MouseEvent) { @@ -157,13 +141,6 @@ export class FilePreviewScreenComponent implements OnInit { return this.instance; } - public applyFilters() { - this.displayedAnnotations = AnnotationUtils.parseAnnotations( - this.annotations, - this.filters - ); - } - public get displayedPages(): number[] { return Object.keys(this.displayedAnnotations).map((key) => Number(key)); } @@ -221,7 +198,7 @@ export class FilePreviewScreenComponent implements OnInit { } get activeViewerPage() { - return this.instance.docViewer.getCurrentPage(); + return this.instance?.docViewer?.getCurrentPage(); } @debounce() @@ -286,43 +263,6 @@ export class FilePreviewScreenComponent implements OnInit { }); } - public setAllFilters(filter: any, value: boolean, rootKey?: string) { - if (rootKey) { - this.filters[rootKey] = value; - } else { - for (const key of Object.keys(filter)) { - if (AnnotationUtils.hasSubsections(filter[key])) { - this.setAllFilters(filter[key], value); - } else { - filter[key] = value; - } - } - } - } - - public isChecked(key: string): boolean { - return AnnotationUtils.isChecked(this.filters[key]); - } - - public isIndeterminate(key: string): boolean { - return AnnotationUtils.isIndeterminate(this.filters[key]); - } - - public get hasActiveFilters(): boolean { - // return AnnotationUtils.hasActiveFilters(this.filters); - return true; - } - - public hasSubsections(filter: AnnotationFilter[]) { - // return AnnotationUtils.hasSubsections(filter); - } - - public setExpanded(key: string, value: boolean, $event: MouseEvent) { - $event.stopPropagation(); - //this.expandedFilters[key] = value; - this._changeDetectorRef.detectChanges(); - } - @HostListener('window:keyup', ['$event']) handleKeyEvent($event: KeyboardEvent) { const keyArray = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown']; @@ -491,15 +431,11 @@ export class FilePreviewScreenComponent implements OnInit { return new this.activeViewer.Annotations.Color(rgbColor.r, rgbColor.g, rgbColor.b); } - filterClicked($event: MouseEvent, key: string, subkey?: string) { - $event.preventDefault(); - $event.stopPropagation(); - if (subkey) { - this.filters[key][subkey] = !this.filters[key][subkey]; - } else { - //this.setAllFilters(this.filters[key],) - this.filters[key] = !this.filters[key]; - } - return false; + filtersChanged(filters: AnnotationFilter[]) { + this.displayedAnnotations = AnnotationUtils.filterAndGroupAnnotations( + this.annotations, + filters + ); + this._changeDetectorRef.detectChanges(); } } diff --git a/apps/red-ui/src/app/screens/file/model/file-data.model.ts b/apps/red-ui/src/app/screens/file/model/file-data.model.ts index c15915b92..902219f23 100644 --- a/apps/red-ui/src/app/screens/file/model/file-data.model.ts +++ b/apps/red-ui/src/app/screens/file/model/file-data.model.ts @@ -1,9 +1,10 @@ -import { RedactionLog } from '@redaction/red-ui-http'; +import { ManualRedactions, RedactionLog } from '@redaction/red-ui-http'; export class FileDataModel { constructor( public annotatedFileData: Blob, public redactedFileData: Blob, - public redactionLog: RedactionLog + public redactionLog: RedactionLog, + public manualRedactions: ManualRedactions ) {} } diff --git a/apps/red-ui/src/app/screens/file/pdf-viewer/pdf-viewer.component.ts b/apps/red-ui/src/app/screens/file/pdf-viewer/pdf-viewer.component.ts index 9acb10c61..b36e4999f 100644 --- a/apps/red-ui/src/app/screens/file/pdf-viewer/pdf-viewer.component.ts +++ b/apps/red-ui/src/app/screens/file/pdf-viewer/pdf-viewer.component.ts @@ -74,7 +74,6 @@ export class PdfViewerComponent implements OnInit, AfterViewInit, OnChanges { ngOnInit() { this._restoreViewerState = this._restoreViewerState.bind(this); - this._manualAnnotationService.loadManualAnnotationsForActiveFile().subscribe(() => {}); } ngOnChanges(changes: SimpleChanges): void { diff --git a/apps/red-ui/src/app/screens/file/service/file-download.service.ts b/apps/red-ui/src/app/screens/file/service/file-download.service.ts index 8a2b51918..d01693296 100644 --- a/apps/red-ui/src/app/screens/file/service/file-download.service.ts +++ b/apps/red-ui/src/app/screens/file/service/file-download.service.ts @@ -1,7 +1,11 @@ import { Injectable } from '@angular/core'; import { forkJoin, Observable, of } from 'rxjs'; import { map, tap } from 'rxjs/operators'; -import { FileUploadControllerService, RedactionLogControllerService } from '@redaction/red-ui-http'; +import { + FileUploadControllerService, + ManualRedactionControllerService, + RedactionLogControllerService +} from '@redaction/red-ui-http'; import { FileType } from '../model/file-type'; import { FileDataModel } from '../model/file-data.model'; @@ -11,15 +15,20 @@ import { FileDataModel } from '../model/file-data.model'; export class FileDownloadService { constructor( private readonly _fileUploadControllerService: FileUploadControllerService, + private readonly _manualRedactionControllerService: ManualRedactionControllerService, private readonly _redactionLogControllerService: RedactionLogControllerService ) {} - public loadFileData(fileId: string): Observable { + public loadFileData(projectId: string, fileId: string): Observable { const annotatedObs = this.loadFile('ANNOTATED', fileId); const redactedObs = this.loadFile('REDACTED', fileId); const reactionLogObs = this._redactionLogControllerService.getRedactionLog(fileId); + const manualRedactionsObs = this._manualRedactionControllerService.getManualRedaction( + projectId, + fileId + ); - return forkJoin([annotatedObs, redactedObs, reactionLogObs]).pipe( + return forkJoin([annotatedObs, redactedObs, reactionLogObs, manualRedactionsObs]).pipe( map((data) => new FileDataModel(...data)) ); } diff --git a/apps/red-ui/src/app/screens/file/service/filters.service.ts b/apps/red-ui/src/app/screens/file/service/filters.service.ts deleted file mode 100644 index 8b8d303b7..000000000 --- a/apps/red-ui/src/app/screens/file/service/filters.service.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { Injectable } from '@angular/core'; -import { AnnotationFilter } from '../../../utils/types'; -import { TypeValue } from '@redaction/red-ui-http'; -import { AnnotationWrapper } from '../model/annotation.wrapper'; - -@Injectable({ - providedIn: 'root' -}) -export class FiltersService { - constructor() {} - - public getFilters( - dictionaryData: { [key: string]: TypeValue }, - annotations?: AnnotationWrapper[] - ): AnnotationFilter[] { - const availableAnnotationTypes: Set = new Set(); - annotations?.forEach((a) => { - availableAnnotationTypes.add(a.superType); - availableAnnotationTypes.add(a.dictionary); - }); - const filters: AnnotationFilter[] = []; - for (const key of Object.keys(dictionaryData)) { - if (availableAnnotationTypes.has(key)) { - const typeValue = dictionaryData[key]; - const filter: AnnotationFilter = this._addOrGetGroup( - filters, - typeValue.hint ? 'hint' : 'redaction' - ); - filter.filters.push({ - key: key - }); - } - } - - return filters; - } - - private _addOrGetGroup(filters: AnnotationFilter[], name: string) { - return { key: name, filters: [] }; - } -} diff --git a/apps/red-ui/src/app/screens/file/service/manual-annotation.service.ts b/apps/red-ui/src/app/screens/file/service/manual-annotation.service.ts index dd2c4cb50..bbabd7500 100644 --- a/apps/red-ui/src/app/screens/file/service/manual-annotation.service.ts +++ b/apps/red-ui/src/app/screens/file/service/manual-annotation.service.ts @@ -16,12 +16,6 @@ import { UserService } from '../../../user/user.service'; providedIn: 'root' }) export class ManualAnnotationService { - private _manualAnnotationsResponse: ManualRedactions; - - get manualEntries(): ManualRedactionEntry[] { - return this._manualAnnotationsResponse ? this._manualAnnotationsResponse.entriesToAdd : []; - } - constructor( private readonly _appStateService: AppStateService, private readonly _userService: UserService, @@ -68,7 +62,6 @@ export class ManualAnnotationService { } public rejectSuggestion(annotationWrapper: AnnotationWrapper) { - console.log(annotationWrapper); // if you're the owner, you undo, otherwise you reject const observable = annotationWrapper.manualRedactionOwner === this._userService.userId @@ -83,22 +76,16 @@ export class ManualAnnotationService { annotationWrapper.uuid ); - return observable - .pipe( - tap( - () => { - this._notify('manual-annotation.reject-request.success'); - }, - () => { - this._notify('manual-annotation.reject-request.error'); - } - ) + return observable.pipe( + tap( + () => { + this._notify('manual-annotation.reject-request.success'); + }, + () => { + this._notify('manual-annotation.reject-request.error'); + } ) - .pipe( - mergeMap((result) => { - return this.loadManualAnnotationsForActiveFile().pipe(map(() => result)); - }) - ); + ); } public removeRedaction(annotationWrapper: AnnotationWrapper) {} @@ -140,11 +127,6 @@ export class ManualAnnotationService { ); } ) - ) - .pipe( - mergeMap((result) => { - return this.loadManualAnnotationsForActiveFile().pipe(map(() => result)); - }) ); } @@ -165,11 +147,6 @@ export class ManualAnnotationService { ); } ) - ) - .pipe( - mergeMap((result) => { - return this.loadManualAnnotationsForActiveFile().pipe(map(() => result)); - }) ); } @@ -196,17 +173,4 @@ export class ManualAnnotationService { } } } - - loadManualAnnotationsForActiveFile() { - return this._manualRedactionControllerService - .getManualRedaction( - this._appStateService.activeProject.project.projectId, - this._appStateService.activeFile.fileId - ) - .pipe( - tap((response) => { - this._manualAnnotationsResponse = response; - }) - ); - } } diff --git a/apps/red-ui/src/app/utils/annotation-utils.ts b/apps/red-ui/src/app/utils/annotation-utils.ts index ae675f0ad..bfbde64d6 100644 --- a/apps/red-ui/src/app/utils/annotation-utils.ts +++ b/apps/red-ui/src/app/utils/annotation-utils.ts @@ -15,53 +15,36 @@ export class AnnotationUtils { }); } - public static hasSubsections(filter: AnnotationFilter | boolean) { - return filter instanceof Object; + public static hasActiveFilters(filters: AnnotationFilter[]): boolean { + return filters.reduce((acc, next) => acc || next.checked || next.indeterminate, false); } - public static checkedSubkeys(filter: AnnotationFilter | boolean) { - return Object.keys(filter).filter((subkey) => this.isChecked(filter[subkey])).length; - } - - // Only some of the sub-items are selected - public static isIndeterminate(filter: AnnotationFilter | boolean): boolean { - return this.hasSubsections(filter) - ? AnnotationUtils.checkedSubkeys(filter) > 0 && !this.isChecked(filter) - : false; - } - - // All sub-items are selected - public static isChecked(filter: AnnotationFilter | boolean): boolean { - return this.hasSubsections(filter) - ? AnnotationUtils.checkedSubkeys(filter) === Object.keys(filter).length - : (filter as boolean); - } - - public static hasActiveFilters(filter: AnnotationFilter[]): boolean { - const activeFilters = Object.keys(filter).filter((key) => { - return this.isChecked(filter[key]) || this.isIndeterminate(filter[key]); - }); - // return activeFilters.length > 0; - return false; - } - - public static parseAnnotations( + public static filterAndGroupAnnotations( annotations: AnnotationWrapper[], filters: AnnotationFilter[] ): { [key: number]: { annotations: AnnotationWrapper[] } } { const obj = {}; + const hasActiveFilters = AnnotationUtils.hasActiveFilters(filters); + + const flatFilters = []; + filters.forEach((filter) => { + flatFilters.push(filter); + flatFilters.push(...filter.filters); + }); for (const annotation of annotations) { const pageNumber = annotation.pageNumber; const type = annotation.superType; - const dictionary = annotation.dictionary; - if (this.hasActiveFilters(filters)) { - if (!this.hasSubsections(filters[type]) && !filters[type]) { - continue; + if (hasActiveFilters) { + let found = false; + for (const filter of flatFilters) { + if (filter.key === annotation.dictionary && filter.checked) { + found = true; + break; + } } - - if (this.hasSubsections(filters[type]) && !filters[type][dictionary]) { + if (!found) { continue; } } diff --git a/apps/red-ui/src/app/utils/types.d.ts b/apps/red-ui/src/app/utils/types.d.ts index 01afaabba..569d5b93d 100644 --- a/apps/red-ui/src/app/utils/types.d.ts +++ b/apps/red-ui/src/app/utils/types.d.ts @@ -12,5 +12,6 @@ export interface AnnotationFilter { key: string; checked?: boolean; indeterminate?: boolean; + expanded?: boolean; filters?: AnnotationFilter[]; }