diff --git a/apps/red-ui/src/app/modules/projects/components/file-workload/file-workload.component.html b/apps/red-ui/src/app/modules/projects/components/file-workload/file-workload.component.html index f4e7949f3..4f38afebd 100644 --- a/apps/red-ui/src/app/modules/projects/components/file-workload/file-workload.component.html +++ b/apps/red-ui/src/app/modules/projects/components/file-workload/file-workload.component.html @@ -11,7 +11,8 @@ [chevron]="true" [filterTemplate]="annotationFilterTemplate" [actionsTemplate]="annotationFilterActionTemplate" - [filters]="annotationFilters" + [primaryFilters]="primaryFilters" + [secondaryFilters]="secondaryFilters" > diff --git a/apps/red-ui/src/app/modules/projects/components/file-workload/file-workload.component.ts b/apps/red-ui/src/app/modules/projects/components/file-workload/file-workload.component.ts index fa54fbbc3..39dfecfa7 100644 --- a/apps/red-ui/src/app/modules/projects/components/file-workload/file-workload.component.ts +++ b/apps/red-ui/src/app/modules/projects/components/file-workload/file-workload.component.ts @@ -6,7 +6,6 @@ import { MatDialogRef, MatDialogState } from '@angular/material/dialog'; import scrollIntoView from 'scroll-into-view-if-needed'; import { debounce } from '../../../../utils/debounce'; import { FileDataModel } from '../../../../models/file/file-data.model'; -import { PermissionsService } from '../../../../services/permissions.service'; const COMMAND_KEY_ARRAY = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Escape']; const ALL_HOTKEY_ARRAY = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown']; @@ -29,7 +28,8 @@ export class FileWorkloadComponent { @Input() activeViewerPage: number; @Input() shouldDeselectAnnotationsOnPageChange: boolean; @Input() dialogRef: MatDialogRef; - @Input() annotationFilters: FilterModel[]; + @Input() primaryFilters: FilterModel[]; + @Input() secondaryFilters: FilterModel[]; @Input() fileData: FileDataModel; @Input() hideSkipped: boolean; @Input() annotationActionsTemplate: TemplateRef; @@ -45,7 +45,6 @@ export class FileWorkloadComponent { public quickScrollLastEnabled = false; public displayedPages: number[] = []; public pagesPanelActive = true; - public filterOnlyWithComments = false; @ViewChild('annotationsElement') private _annotationsElement: ElementRef; @ViewChild('quickNavigation') private _quickNavigationElement: ElementRef; @@ -106,8 +105,8 @@ export class FileWorkloadComponent { } @debounce(0) - public filtersChanged(filters: FilterModel[]) { - this.displayedAnnotations = this._annotationProcessingService.filterAndGroupAnnotations(this._annotations, filters); + public filtersChanged(filters: { primary: FilterModel[]; secondary?: FilterModel[] }) { + this.displayedAnnotations = this._annotationProcessingService.filterAndGroupAnnotations(this._annotations, filters.primary, filters.secondary); this.displayedPages = Object.keys(this.displayedAnnotations).map((key) => Number(key)); this.computeQuickNavButtonsState(); this._changeDetectorRef.markForCheck(); diff --git a/apps/red-ui/src/app/modules/projects/components/type-filter/type-filter.component.html b/apps/red-ui/src/app/modules/projects/components/type-filter/type-filter.component.html index b2c483b43..ad82d6e8c 100644 --- a/apps/red-ui/src/app/modules/projects/components/type-filter/type-filter.component.html +++ b/apps/red-ui/src/app/modules/projects/components/type-filter/type-filter.component.html @@ -36,4 +36,6 @@ + + {{ filter.label | translate }} diff --git a/apps/red-ui/src/app/modules/projects/components/type-filter/type-filter.component.scss b/apps/red-ui/src/app/modules/projects/components/type-filter/type-filter.component.scss index 5f2090afe..93aa005d7 100644 --- a/apps/red-ui/src/app/modules/projects/components/type-filter/type-filter.component.scss +++ b/apps/red-ui/src/app/modules/projects/components/type-filter/type-filter.component.scss @@ -6,4 +6,11 @@ redaction-annotation-icon { margin-right: 8px; } + + mat-icon { + width: 16px; + height: 16px; + margin-right: 8px; + opacity: 50%; + } } diff --git a/apps/red-ui/src/app/modules/projects/screens/file-preview-screen/file-preview-screen.component.html b/apps/red-ui/src/app/modules/projects/screens/file-preview-screen/file-preview-screen.component.html index bff887116..1aef2499d 100644 --- a/apps/red-ui/src/app/modules/projects/screens/file-preview-screen/file-preview-screen.component.html +++ b/apps/red-ui/src/app/modules/projects/screens/file-preview-screen/file-preview-screen.component.html @@ -233,7 +233,8 @@ [activeViewerPage]="activeViewerPage" [(shouldDeselectAnnotationsOnPageChange)]="shouldDeselectAnnotationsOnPageChange" [dialogRef]="dialogRef" - [annotationFilters]="annotationFilters" + [primaryFilters]="primaryFilters" + [secondaryFilters]="secondaryFilters" [fileData]="fileData" [hideSkipped]="hideSkipped" [annotationActionsTemplate]="annotationActionsTemplate" diff --git a/apps/red-ui/src/app/modules/projects/screens/file-preview-screen/file-preview-screen.component.ts b/apps/red-ui/src/app/modules/projects/screens/file-preview-screen/file-preview-screen.component.ts index e09b48f8c..0dbd7b33b 100644 --- a/apps/red-ui/src/app/modules/projects/screens/file-preview-screen/file-preview-screen.component.ts +++ b/apps/red-ui/src/app/modules/projects/screens/file-preview-screen/file-preview-screen.component.ts @@ -51,7 +51,8 @@ export class FilePreviewScreenComponent implements OnInit, OnDestroy, OnAttach, annotationData: AnnotationData; selectedAnnotations: AnnotationWrapper[]; viewReady = false; - annotationFilters: FilterModel[]; + primaryFilters: FilterModel[]; + secondaryFilters: FilterModel[]; loadingMessage: string; canPerformAnnotationActions: boolean; filesAutoUpdateTimer: Subscription; @@ -217,8 +218,12 @@ export class FilePreviewScreenComponent implements OnInit, OnDestroy, OnAttach, this.userPreferenceService.areDevFeaturesEnabled ); const annotationFilters = this._annotationProcessingService.getAnnotationFilter(this.annotations); - this.annotationFilters = processFilters(this.annotationFilters, annotationFilters); - this._workloadComponent.filtersChanged(this.annotationFilters); + this.primaryFilters = processFilters(this.primaryFilters, annotationFilters); + this.secondaryFilters = processFilters(this.secondaryFilters, AnnotationProcessingService.secondaryAnnotationFilters); + this._workloadComponent.filtersChanged({ + primary: this.primaryFilters, + secondary: this.secondaryFilters + }); console.log('[REDACTION] Process time: ' + (new Date().getTime() - processStartTime) + 'ms'); console.log( '[REDACTION] Annotation Redraw and filter rebuild time: ' + @@ -522,12 +527,12 @@ export class FilePreviewScreenComponent implements OnInit, OnDestroy, OnAttach, } private _handleDeltaAnnotationFilters(currentPageAnnotations: AnnotationWrapper[], newPageAnnotations: AnnotationWrapper[]) { - const hasAnyFilterSet = this.annotationFilters.find((f) => f.checked || f.indeterminate); + const hasAnyFilterSet = this.primaryFilters.find((f) => f.checked || f.indeterminate); if (hasAnyFilterSet) { const oldPageSpecificFilters = this._annotationProcessingService.getAnnotationFilter(currentPageAnnotations); const newPageSpecificFilters = this._annotationProcessingService.getAnnotationFilter(newPageAnnotations); - handleFilterDelta(oldPageSpecificFilters, newPageSpecificFilters, this.annotationFilters); - this._workloadComponent.filtersChanged(this.annotationFilters); + handleFilterDelta(oldPageSpecificFilters, newPageSpecificFilters, this.primaryFilters); + this._workloadComponent.filtersChanged({ primary: this.primaryFilters }); } } diff --git a/apps/red-ui/src/app/modules/projects/screens/project-listing-screen/project-listing-screen.component.html b/apps/red-ui/src/app/modules/projects/screens/project-listing-screen/project-listing-screen.component.html index fadb6c9a2..947dac1f6 100644 --- a/apps/red-ui/src/app/modules/projects/screens/project-listing-screen/project-listing-screen.component.html +++ b/apps/red-ui/src/app/modules/projects/screens/project-listing-screen/project-listing-screen.component.html @@ -6,14 +6,14 @@ #statusFilter (filtersChanged)="filtersChanged()" [filterLabel]="'filters.status'" - [filters]="statusFilters" + [primaryFilters]="statusFilters" [icon]="'red:status'" > diff --git a/apps/red-ui/src/app/modules/projects/screens/project-overview-screen/project-overview-screen.component.html b/apps/red-ui/src/app/modules/projects/screens/project-overview-screen/project-overview-screen.component.html index 587c1606f..b77660f68 100644 --- a/apps/red-ui/src/app/modules/projects/screens/project-overview-screen/project-overview-screen.component.html +++ b/apps/red-ui/src/app/modules/projects/screens/project-overview-screen/project-overview-screen.component.html @@ -6,14 +6,14 @@ #statusFilter (filtersChanged)="filtersChanged()" [filterLabel]="'filters.status'" - [filters]="statusFilters" + [primaryFilters]="statusFilters" [icon]="'red:status'" > diff --git a/apps/red-ui/src/app/modules/projects/services/annotation-processing.service.ts b/apps/red-ui/src/app/modules/projects/services/annotation-processing.service.ts index 0f7691e0b..82cfd20b2 100644 --- a/apps/red-ui/src/app/modules/projects/services/annotation-processing.service.ts +++ b/apps/red-ui/src/app/modules/projects/services/annotation-processing.service.ts @@ -1,17 +1,14 @@ import { Injectable } from '@angular/core'; -import { AppStateService } from '../../../state/app-state.service'; import { AnnotationWrapper } from '../../../models/file/annotation.wrapper'; -import { FilterModel, FilterTypes } from '../../shared/components/filter/model/filter.model'; +import { FilterModel } from '../../shared/components/filter/model/filter.model'; import { handleCheckedValue } from '../../shared/components/filter/utils/filter-utils'; import { SuperTypeSorter } from '../../../utils/sorters/super-type-sorter'; @Injectable() export class AnnotationProcessingService { - constructor(private readonly _appStateService: AppStateService) {} - getAnnotationFilter(annotations: AnnotationWrapper[]): FilterModel[] { const filterMap = new Map(); - let filters: FilterModel[] = []; + const filters: FilterModel[] = []; annotations?.forEach((a) => { const topLevelFilter = a.superType !== 'hint' && a.superType !== 'redaction' && a.superType !== 'recommendation'; @@ -30,7 +27,6 @@ export class AnnotationProcessingService { } const childFilter = { key: a.dictionary, - type: FilterTypes.primary, checked: false, filters: [], matches: 1 @@ -52,16 +48,12 @@ export class AnnotationProcessingService { } } - filters = filters.sort((a, b) => SuperTypeSorter[a.key] - SuperTypeSorter[b.key]); - filters.push(...AnnotationProcessingService._secondaryFilters); - - return filters; + return filters.sort((a, b) => SuperTypeSorter[a.key] - SuperTypeSorter[b.key]); } - private _createParentFilter(key: string, filterMap: Map, filters: FilterModel[], type?: FilterTypes) { + private _createParentFilter(key: string, filterMap: Map, filters: FilterModel[]) { const filter: FilterModel = { key: key, - type: type || FilterTypes.primary, topLevelFilter: true, matches: 1, label: 'annotation-type.' + key, @@ -72,48 +64,27 @@ export class AnnotationProcessingService { return filter; } - filterAndGroupAnnotations(annotations: AnnotationWrapper[], filters: FilterModel[]): { [key: number]: { annotations: AnnotationWrapper[] } } { + filterAndGroupAnnotations( + annotations: AnnotationWrapper[], + primaryFilters: FilterModel[], + secondaryFilters?: FilterModel[] + ): { [key: number]: { annotations: AnnotationWrapper[] } } { const obj = {}; - const hasActiveFilters = this._hasActiveFilters(filters); - const flatFilters: FilterModel[] = []; - filters.forEach((filter) => { - flatFilters.push(filter); - flatFilters.push(...filter?.filters); - }); - - const primaryFilters = flatFilters.filter((f) => f.type === FilterTypes.primary && f.checked); - const secondaryFilters = flatFilters.filter((f) => f.type === FilterTypes.secondary && f.checked && f.action); - console.log(secondaryFilters); + const primaryFlatFilters = this._getFlatFilters(primaryFilters, (f) => f.checked); + const secondaryFlatFilters = this._getFlatFilters(secondaryFilters, (f) => f.checked && !!f.checker); for (const annotation of annotations) { - const pageNumber = annotation.pageNumber; - const type = annotation.superType; - - if (hasActiveFilters) { - let found = false; - for (const filter of primaryFilters) { - if ( - (filter.key === annotation.dictionary && - (annotation.superType === 'hint' || annotation.superType === 'redaction' || annotation.superType === 'recommendation')) || - filter.key === annotation.superType - ) { - let secondaryFiltersNotMatched = false; - for (const secondaryFilter of secondaryFilters) { - if (!secondaryFilter.action(annotation)) { - secondaryFiltersNotMatched = true; - } - } - - found = !secondaryFiltersNotMatched; - break; - } - } - if (!found) { - continue; - } + if (!this._matchesOne(primaryFlatFilters, (f) => this._checkByFilterKey(f, annotation))) { + continue; } + if (!this._matchesAll(secondaryFlatFilters, (f) => f.checker(annotation))) { + continue; + } + + const pageNumber = annotation.pageNumber; + if (!obj[pageNumber]) { obj[pageNumber] = { annotations: [], @@ -125,7 +96,7 @@ export class AnnotationProcessingService { } obj[pageNumber].annotations.push(annotation); - obj[pageNumber][type]++; + obj[pageNumber][annotation.superType]++; } Object.keys(obj).map((page) => { @@ -135,6 +106,44 @@ export class AnnotationProcessingService { return obj; } + private _getFlatFilters(filters: FilterModel[], filterBy?: (f: FilterModel) => boolean) { + const flatFilters: FilterModel[] = []; + + filters.forEach((filter) => { + flatFilters.push(filter); + flatFilters.push(...filter.filters); + }); + + return !!filterBy ? flatFilters.filter((f) => filterBy(f)) : flatFilters; + } + + private _matchesOne = (filters: FilterModel[], condition: (filter: FilterModel) => boolean): boolean => { + if (filters.length === 0) return true; + + for (const filter of filters) { + if (condition(filter)) return true; + } + + return false; + }; + + private _matchesAll = (filters: FilterModel[], condition: (filter: FilterModel) => boolean): boolean => { + if (filters.length === 0) return true; + + for (const filter of filters) { + if (!condition(filter)) return false; + } + + return true; + }; + + private _checkByFilterKey = (filter: FilterModel, annotation: AnnotationWrapper) => { + const superType = annotation.superType; + const isNotTopLevelFilter = superType === 'hint' || superType === 'redaction' || superType === 'recommendation'; + + return filter.key === superType || (filter.key === annotation.dictionary && isNotTopLevelFilter); + }; + private _sortAnnotations(annotations: AnnotationWrapper[]): AnnotationWrapper[] { return annotations.sort((ann1, ann2) => { if (ann1.pageNumber === ann2.pageNumber) { @@ -148,22 +157,15 @@ export class AnnotationProcessingService { }); } - private _hasActiveFilters(filters: FilterModel[]): boolean { - return filters.reduce((acc, next) => acc || next.checked || next.indeterminate, false); - } - - private static get _secondaryFilters(): FilterModel[] { + static get secondaryAnnotationFilters(): FilterModel[] { return [ { - key: 'with-comments', + key: 'red:comment', label: 'filter-menu.with-comments', checked: false, topLevelFilter: true, - type: FilterTypes.secondary, filters: [], - action: (obj) => { - return obj?.comments?.length > 0; - } + checker: (annotation: AnnotationWrapper) => annotation?.comments?.length > 0 } ]; } diff --git a/apps/red-ui/src/app/modules/shared/components/filter/filter.component.html b/apps/red-ui/src/app/modules/shared/components/filter/filter.component.html index 9bce292ef..b4c002677 100644 --- a/apps/red-ui/src/app/modules/shared/components/filter/filter.component.html +++ b/apps/red-ui/src/app/modules/shared/components/filter/filter.component.html @@ -9,7 +9,7 @@ -
+
@@ -22,16 +22,10 @@ *ngTemplateOutlet="defaultFilterTemplate; context: { filter: filter, atLeastOneIsExpandable: atLeastOneFilterIsExpandable }" >
-
+
- - - - - -
- {{ filter?.label }} + {{ _(filter)?.label }}
- - + +
 
@@ -64,8 +58,8 @@
-
-
+
+
diff --git a/apps/red-ui/src/app/modules/shared/components/filter/filter.component.scss b/apps/red-ui/src/app/modules/shared/components/filter/filter.component.scss index cffaf242c..9ea49fb32 100644 --- a/apps/red-ui/src/app/modules/shared/components/filter/filter.component.scss +++ b/apps/red-ui/src/app/modules/shared/components/filter/filter.component.scss @@ -24,12 +24,6 @@ .filter-options { background-color: $grey-2; padding-bottom: 8px; - - mat-icon { - width: 16px; - height: 16px; - margin-right: 8px; - } } ::ng-deep .mat-menu-panel .mat-menu-content:not(:empty) { diff --git a/apps/red-ui/src/app/modules/shared/components/filter/filter.component.ts b/apps/red-ui/src/app/modules/shared/components/filter/filter.component.ts index 5f5153f5a..71457c8ce 100644 --- a/apps/red-ui/src/app/modules/shared/components/filter/filter.component.ts +++ b/apps/red-ui/src/app/modules/shared/components/filter/filter.component.ts @@ -1,6 +1,5 @@ import { ChangeDetectorRef, Component, EventEmitter, Input, OnChanges, Output, SimpleChanges, TemplateRef } from '@angular/core'; -import { AppStateService } from '../../../../state/app-state.service'; -import { FilterModel, FilterTypes } from './model/filter.model'; +import { FilterModel } from './model/filter.model'; import { handleCheckedValue } from './utils/filter-utils'; import { MAT_CHECKBOX_DEFAULT_OPTIONS } from '@angular/material/checkbox'; @@ -19,10 +18,11 @@ import { MAT_CHECKBOX_DEFAULT_OPTIONS } from '@angular/material/checkbox'; ] }) export class FilterComponent implements OnChanges { - @Output() filtersChanged = new EventEmitter(); + @Output() filtersChanged = new EventEmitter<{ primary: FilterModel[]; secondary?: FilterModel[] }>(); @Input() filterTemplate: TemplateRef; @Input() actionsTemplate: TemplateRef; - @Input() filters: FilterModel[] = []; + @Input() primaryFilters: FilterModel[] = []; + @Input() secondaryFilters: FilterModel[] = []; @Input() filterLabel = 'filter-menu.label'; @Input() icon: string; @Input() chevron = false; @@ -33,15 +33,16 @@ export class FilterComponent implements OnChanges { atLeastOneFilterIsExpandable = false; atLeastOneSecondaryFilterIsExpandable = false; - constructor(public readonly appStateService: AppStateService, private readonly _changeDetectorRef: ChangeDetectorRef) {} + constructor(private readonly _changeDetectorRef: ChangeDetectorRef) {} ngOnChanges(changes: SimpleChanges): void { this.atLeastOneFilterIsExpandable = false; this.atLeastOneSecondaryFilterIsExpandable = false; - this.filters?.forEach((f) => { - if (f.type === FilterTypes.primary) this.atLeastOneFilterIsExpandable = this.atLeastOneFilterIsExpandable || this.isExpandable(f); - if (f.type === FilterTypes.secondary) - this.atLeastOneSecondaryFilterIsExpandable = this.atLeastOneSecondaryFilterIsExpandable || this.isExpandable(f); + this.primaryFilters?.forEach((f) => { + this.atLeastOneFilterIsExpandable = this.atLeastOneFilterIsExpandable || this.isExpandable(f); + }); + this.secondaryFilters?.forEach((f) => { + this.atLeastOneSecondaryFilterIsExpandable = this.atLeastOneSecondaryFilterIsExpandable || this.isExpandable(f); }); } @@ -71,7 +72,7 @@ export class FilterComponent implements OnChanges { } get hasActiveFilters(): boolean { - for (const filter of this.filters ? this.filters : []) { + for (const filter of this.primaryFilters ? this.primaryFilters : []) { if (filter.checked || filter.indeterminate) { return true; } @@ -79,16 +80,8 @@ export class FilterComponent implements OnChanges { return false; } - get primaryFilters() { - return this.filters?.filter((f) => f.type === FilterTypes.primary || f.type === undefined); - } - - get secondaryFilters() { - return this.filters?.filter((f) => f.type === FilterTypes.secondary); - } - applyFilters() { - this.filtersChanged.emit(this.filters); + this.filtersChanged.emit({ primary: this.primaryFilters, secondary: this.secondaryFilters }); } toggleFilterExpanded($event: MouseEvent, filter: FilterModel) { @@ -97,7 +90,7 @@ export class FilterComponent implements OnChanges { } private _setAllFilters(value: boolean) { - this.filters?.forEach((f) => { + this.primaryFilters?.forEach((f) => { f.checked = value; f.indeterminate = false; f.filters?.forEach((ff) => { @@ -123,4 +116,8 @@ export class FilterComponent implements OnChanges { isExpandable(filter: FilterModel) { return filter.filters && filter.filters.length > 0; } + + _(obj): FilterModel { + return obj as FilterModel; + } } diff --git a/apps/red-ui/src/app/modules/shared/components/filter/model/filter.model.ts b/apps/red-ui/src/app/modules/shared/components/filter/model/filter.model.ts index 08012cbf3..717fa16d3 100644 --- a/apps/red-ui/src/app/modules/shared/components/filter/model/filter.model.ts +++ b/apps/red-ui/src/app/modules/shared/components/filter/model/filter.model.ts @@ -1,11 +1,5 @@ -export enum FilterTypes { - primary = 'primary', - secondary = 'secondary' -} - export interface FilterModel { key: string; - type: FilterTypes; label?: string; checked?: boolean; indeterminate?: boolean; @@ -13,5 +7,5 @@ export interface FilterModel { topLevelFilter?: boolean; matches?: number; filters?: FilterModel[]; - action?: (obj?) => boolean; + checker?: (obj?) => boolean; } diff --git a/apps/red-ui/src/assets/icons/general/comment.svg b/apps/red-ui/src/assets/icons/general/comment.svg index b345a774c..6190f8052 100644 --- a/apps/red-ui/src/assets/icons/general/comment.svg +++ b/apps/red-ui/src/assets/icons/general/comment.svg @@ -1,13 +1,8 @@ - - status - - - - - - - - + + + - \ No newline at end of file + diff --git a/apps/red-ui/src/assets/styles/red-page-layout.scss b/apps/red-ui/src/assets/styles/red-page-layout.scss index 6007ddc1f..bb8e49431 100644 --- a/apps/red-ui/src/assets/styles/red-page-layout.scss +++ b/apps/red-ui/src/assets/styles/red-page-layout.scss @@ -229,6 +229,10 @@ body { margin-top: 20px; } +.pb-24 { + padding-bottom: 24px; +} + .pb-32 { padding-bottom: 32px; }