move filters to classes, add searchKey to entities

This commit is contained in:
Dan Percic 2021-09-25 21:03:40 +03:00
parent d1cc15ec23
commit 5387fef462
17 changed files with 226 additions and 191 deletions

View File

@ -64,7 +64,6 @@ export class FileStatusWrapper implements FileStatus, IListable {
}
}
readonly excludedPagesCount = this.excludedPages?.length ?? 0;
readonly statusSort = StatusSorter[this.status];
readonly pages = this._pages;
readonly cacheIdentifier = btoa(this.lastUploaded + this.lastOCRTime);
@ -88,6 +87,10 @@ export class FileStatusWrapper implements FileStatus, IListable {
return this.fileId;
}
get searchKey(): string {
return this.filename;
}
private get _pages() {
if (this.fileStatus.status === 'ERROR') {
return -1;

View File

@ -100,7 +100,7 @@
(click)="toggleFieldActive(field)"
(mouseenter)="setHoveredColumn(field.csvColumn)"
(mouseleave)="setHoveredColumn()"
*ngFor="let field of sortedDisplayedEntities$ | async; trackBy: trackByPrimaryKey"
*ngFor="let field of sortedDisplayedEntities$ | async"
class="csv-header-pill-wrapper"
>
<div [class.selected]="isActive(field)" class="csv-header-pill">

View File

@ -50,7 +50,7 @@
<div *ngIf="hasFiles" class="mt-24 legend pb-32">
<div
(click)="filterService.toggleFilter('needsWorkFilters', filter.key)"
(click)="filterService.toggleFilter('needsWorkFilters', filter.id)"
*ngFor="let filter of needsWorkFilters$ | async"
[class.active]="filter.checked"
>

View File

@ -3,7 +3,7 @@ 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, IqserEventTarget, NestedFilter } from '@iqser/common-ui';
import { CircleButtonTypes, Debounce, FilterService, IconButtonTypes, INestedFilter, IqserEventTarget } from '@iqser/common-ui';
import { FileDataModel } from '@models/file/file-data.model';
import { PermissionsService } from '@services/permissions.service';
import { WebViewerInstance } from '@pdftron/webviewer';
@ -122,8 +122,8 @@ export class FileWorkloadComponent {
private _filterAnnotations(
annotations: AnnotationWrapper[],
primary: NestedFilter[],
secondary: NestedFilter[] = []
primary: INestedFilter[],
secondary: INestedFilter[] = []
): Map<number, AnnotationWrapper[]> {
if (!primary) {
return;

View File

@ -1,57 +1,57 @@
<ng-container *ngIf="!filter.icon">
<redaction-annotation-icon
*ngIf="filter.key === 'redaction'"
*ngIf="filter.id === 'redaction'"
[color]="dictionaryColor"
label="R"
type="square"
></redaction-annotation-icon>
<redaction-annotation-icon
*ngIf="filter.key === 'recommendation'"
*ngIf="filter.id === 'recommendation'"
[color]="dictionaryColor"
label="R"
type="hexagon"
></redaction-annotation-icon>
<redaction-annotation-icon *ngIf="filter.key === 'hint'" [color]="dictionaryColor" label="H" type="circle"></redaction-annotation-icon>
<redaction-annotation-icon *ngIf="filter.id === 'hint'" [color]="dictionaryColor" label="H" type="circle"></redaction-annotation-icon>
<redaction-annotation-icon
*ngIf="filter.key === 'manual-redaction'"
*ngIf="filter.id === 'manual-redaction'"
[color]="dictionaryColor"
label="M"
type="square"
></redaction-annotation-icon>
<redaction-annotation-icon
*ngIf="filter.key === 'skipped'"
*ngIf="filter.id === 'skipped'"
[color]="dictionaryColor"
label="S"
type="square"
></redaction-annotation-icon>
<redaction-annotation-icon
*ngIf="isSuggestion(filter.key)"
*ngIf="isSuggestion(filter.id)"
[color]="dictionaryColor"
label="S"
type="rhombus"
></redaction-annotation-icon>
<redaction-annotation-icon
*ngIf="needsAnalysis(filter.key)"
*ngIf="needsAnalysis(filter.id)"
[color]="dictionaryColor"
label="A"
type="square"
></redaction-annotation-icon>
<redaction-annotation-icon
*ngIf="filter.key === 'declined-suggestion'"
*ngIf="filter.id === 'declined-suggestion'"
[color]="dictionaryColor"
label="S"
type="rhombus"
></redaction-annotation-icon>
<redaction-annotation-icon *ngIf="filter.key === 'none'" color="transparent" label="-" type="none"></redaction-annotation-icon>
<redaction-annotation-icon *ngIf="filter.id === 'none'" color="transparent" label="-" type="none"></redaction-annotation-icon>
<redaction-annotation-icon
*ngIf="filter.key === 'updated'"
*ngIf="filter.id === 'updated'"
[color]="dictionaryColor"
label="U"
type="square"
></redaction-annotation-icon>
<redaction-annotation-icon *ngIf="filter.key === 'image'" [color]="dictionaryColor" label="I" type="square"></redaction-annotation-icon>
<redaction-annotation-icon *ngIf="filter.id === 'image'" [color]="dictionaryColor" label="I" type="square"></redaction-annotation-icon>
<div *ngIf="filter.key === 'comment'">
<div *ngIf="filter.id === 'comment'">
<mat-icon svgIcon="red:comment"></mat-icon>
</div>
</ng-container>

View File

@ -1,6 +1,6 @@
import { Component, Input, OnInit } from '@angular/core';
import { AppStateService } from '@state/app-state.service';
import { NestedFilter } from '@iqser/common-ui';
import { INestedFilter } from '@iqser/common-ui';
@Component({
selector: 'redaction-type-filter',
@ -8,7 +8,7 @@ import { NestedFilter } from '@iqser/common-ui';
styleUrls: ['./type-filter.component.scss']
})
export class TypeFilterComponent implements OnInit {
@Input() filter: NestedFilter;
@Input() filter: INestedFilter;
dictionaryColor: string;
@ -36,6 +36,6 @@ export class TypeFilterComponent implements OnInit {
needsAnalysis = (key: string) => this._needsAnalysisKeys.includes(key);
ngOnInit(): void {
this.dictionaryColor = this._appStateService.getDictionaryColor(this.filter.key);
this.dictionaryColor = this._appStateService.getDictionaryColor(this.filter.id);
}
}

View File

@ -1,6 +1,6 @@
import { Component, EventEmitter, forwardRef, Injector, Input, OnInit, Output, TemplateRef, ViewChild } from '@angular/core';
import { EditDossierSectionInterface } from '../edit-dossier-section.interface';
import { Dossier } from '../../../../../state/model/dossier';
import { Dossier } from '@state/model/dossier';
import {
CircleButtonTypes,
DefaultListingServices,
@ -49,7 +49,6 @@ export class EditDossierDeletedDocumentsComponent extends ListingComponent<FileL
@ViewChild('pagesTemplate', { static: true }) pagesTemplate: TemplateRef<never>;
@ViewChild('deletedDateTemplate', { static: true }) deletedDateTemplate: TemplateRef<never>;
@ViewChild('restoreDateTemplate', { static: true }) restoreDateTemplate: TemplateRef<never>;
protected readonly _primaryKey = 'fileId';
constructor(
protected readonly _injector: Injector,
@ -167,6 +166,7 @@ export class EditDossierDeletedDocumentsComponent extends ListingComponent<FileL
id: file.fileId,
...file,
restoreDate,
searchKey: file.filename,
canRestore: this._canRestoreFile(restoreDate)
};
}

View File

@ -39,7 +39,7 @@
<div class="small-label stats-subtitle mb-6">
<div>
<mat-icon svgIcon="red:template"></mat-icon>
{{ dossier.dossierTemplateName }}
{{ getDossierTemplateNameFor(dossier.dossierTemplateId) }}
</div>
</div>
<div class="small-label stats-subtitle">
@ -53,7 +53,7 @@
</div>
<div>
<mat-icon svgIcon="red:user"></mat-icon>
{{ dossier.memberCount }}
{{ dossier.memberIds.length }}
</div>
<div>
<mat-icon svgIcon="red:calendar"></mat-icon>

View File

@ -16,7 +16,15 @@ import { DossiersDialogService } from '../../services/dossiers-dialog.service';
import { OnAttach, OnDetach } from '@utils/custom-route-reuse.strategy';
import { UserPreferenceService } from '@services/user-preference.service';
import { ButtonConfig } from '@shared/components/page-header/models/button-config.model';
import { DefaultListingServices, keyChecker, ListingComponent, NestedFilter, TableColumnConfig, TableComponent } from '@iqser/common-ui';
import {
DefaultListingServices,
INestedFilter,
keyChecker,
ListingComponent,
NestedFilter,
TableColumnConfig,
TableComponent
} from '@iqser/common-ui';
import { workloadTranslations } from '../../translations/workload-translations';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { fileStatusTranslations } from '../../translations/file-status-translations';
@ -46,11 +54,10 @@ export class DossierListingScreenComponent
tableColumnConfigs: TableColumnConfig<Dossier>[];
dossiersChartData: DoughnutChartConfig[] = [];
documentsChartData: DoughnutChartConfig[] = [];
@ViewChild('nameTemplate', { static: true }) nameTemplate: TemplateRef<never>;
@ViewChild('needsWorkTemplate', { static: true }) needsWorkTemplate: TemplateRef<never>;
@ViewChild('ownerTemplate', { static: true }) ownerTemplate: TemplateRef<never>;
@ViewChild('statusTemplate', { static: true }) statusTemplate: TemplateRef<never>;
protected readonly _primaryKey = 'dossierName';
@ViewChild('nameTemplate', { static: true }) nameTemplate: TemplateRef<unknown>;
@ViewChild('needsWorkTemplate', { static: true }) needsWorkTemplate: TemplateRef<unknown>;
@ViewChild('ownerTemplate', { static: true }) ownerTemplate: TemplateRef<unknown>;
@ViewChild('statusTemplate', { static: true }) statusTemplate: TemplateRef<unknown>;
private _lastScrolledIndex: number;
@ViewChild('needsWorkFilterTemplate', {
read: TemplateRef,
@ -85,6 +92,10 @@ export class DossierListingScreenComponent
routerLinkFn = (dossier: Dossier) => ['/main/dossiers/' + dossier.id];
getDossierTemplateNameFor(dossierTemplateId: string): string {
return this._appStateService.getDossierTemplateById(dossierTemplateId).name;
}
ngOnInit(): void {
this._configureTableColumns();
this.calculateData();
@ -156,7 +167,7 @@ export class DossierListingScreenComponent
this.tableColumnConfigs = [
{
label: _('dossier-listing.table-col-names.name'),
sortByKey: 'dossierName',
sortByKey: 'searchKey',
template: this.nameTemplate,
width: '2fr'
},
@ -214,23 +225,29 @@ export class DossierListingScreenComponent
allDistinctDossierTemplates.add(entry.dossierTemplateId);
});
const statusFilters = [...allDistinctFileStatus].map<NestedFilter>(status => ({
key: status,
label: this._translateService.instant(fileStatusTranslations[status])
}));
const statusFilters = [...allDistinctFileStatus].map<INestedFilter>(
status =>
new NestedFilter({
id: status,
label: this._translateService.instant(fileStatusTranslations[status])
})
);
this.filterService.addFilterGroup({
slug: 'statusFilters',
label: this._translateService.instant('filters.status'),
icon: 'red:status',
filters: statusFilters.sort(StatusSorter.byStatus),
filters: statusFilters.sort((a, b) => StatusSorter[a.id] - StatusSorter[b.id]),
checker: dossierStatusChecker
});
const peopleFilters = [...allDistinctPeople].map<NestedFilter>(userId => ({
key: userId,
label: this._userService.getNameForId(userId)
}));
const peopleFilters = [...allDistinctPeople].map<INestedFilter>(
userId =>
new NestedFilter({
id: userId,
label: this._userService.getNameForId(userId)
})
);
this.filterService.addFilterGroup({
slug: 'peopleFilters',
@ -240,25 +257,31 @@ export class DossierListingScreenComponent
checker: dossierMemberChecker
});
const needsWorkFilters = [...allDistinctNeedsWork].map<NestedFilter>(type => ({
key: type,
label: workloadTranslations[type]
}));
const needsWorkFilters = [...allDistinctNeedsWork].map<INestedFilter>(
type =>
new NestedFilter({
id: type,
label: workloadTranslations[type]
})
);
this.filterService.addFilterGroup({
slug: 'needsWorkFilters',
label: this._translateService.instant('filters.needs-work'),
icon: 'red:needs-work',
filterTemplate: this._needsWorkFilterTemplate,
filters: needsWorkFilters.sort(RedactionFilterSorter.byKey),
filters: needsWorkFilters.sort((a, b) => RedactionFilterSorter[a.id] - RedactionFilterSorter[b.id]),
checker: annotationFilterChecker,
matchAll: true
});
const dossierTemplateFilters = [...allDistinctDossierTemplates].map<NestedFilter>(id => ({
key: id,
label: this._appStateService.getDossierTemplateById(id).name
}));
const dossierTemplateFilters = [...allDistinctDossierTemplates].map<INestedFilter>(
id =>
new NestedFilter({
id: id,
label: this._appStateService.getDossierTemplateById(id).name
})
);
this.filterService.addFilterGroup({
slug: 'dossierTemplateFilters',
@ -276,10 +299,13 @@ export class DossierListingScreenComponent
checker: (dw: Dossier) => quickFilters.reduce((acc, f) => acc || (f.checked && f.checker(dw)), false)
});
const dossierFilters = this.entitiesService.all.map<NestedFilter>(dossier => ({
key: dossier.dossierName,
label: dossier.dossierName
}));
const dossierFilters = this.entitiesService.all.map<INestedFilter>(
dossier =>
new NestedFilter({
id: dossier.dossierName,
label: dossier.dossierName
})
);
this.filterService.addFilterGroup({
slug: 'dossierNameFilter',
label: this._translateService.instant('dossier-listing.filters.label'),
@ -290,30 +316,30 @@ export class DossierListingScreenComponent
});
}
private _createQuickFilters() {
private _createQuickFilters(): INestedFilter[] {
const myDossiersLabel = this._translateService.instant('dossier-listing.quick-filters.my-dossiers');
const filters: NestedFilter[] = [
const filters: INestedFilter[] = [
{
key: 'my-dossiers',
id: 'my-dossiers',
label: myDossiersLabel,
checker: (dw: Dossier) => dw.ownerId === this.currentUser.id
},
{
key: 'to-approve',
id: 'to-approve',
label: this._translateService.instant('dossier-listing.quick-filters.to-approve'),
checker: (dw: Dossier) => dw.approverIds.includes(this.currentUser.id)
},
{
key: 'to-review',
id: 'to-review',
label: this._translateService.instant('dossier-listing.quick-filters.to-review'),
checker: (dw: Dossier) => dw.memberIds.includes(this.currentUser.id)
},
{
key: 'other',
id: 'other',
label: this._translateService.instant('dossier-listing.quick-filters.other'),
checker: (dw: Dossier) => !dw.memberIds.includes(this.currentUser.id)
}
];
].map(filter => new NestedFilter(filter));
return filters.filter(f => f.label === myDossiersLabel || this._userPreferenceService.areDevFeaturesEnabled);
}

View File

@ -33,6 +33,7 @@ import { ActionConfig } from '@shared/components/page-header/models/action-confi
import {
CircleButtonTypes,
DefaultListingServices,
INestedFilter,
keyChecker,
ListingComponent,
ListingModes,
@ -45,7 +46,6 @@ import {
} from '@iqser/common-ui';
import { DossierAttributesService } from '@shared/services/controller-wrappers/dossier-attributes.service';
import { DossierAttributeWithValue } from '@models/dossier-attributes.model';
import { UserPreferenceService } from '@services/user-preference.service';
import { workloadTranslations } from '../../translations/workload-translations';
import { fileStatusTranslations } from '../../translations/file-status-translations';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
@ -80,15 +80,14 @@ export class DossierOverviewScreenComponent extends ListingComponent<FileStatusW
collapsedDetails = false;
dossierAttributes: DossierAttributeWithValue[] = [];
fileAttributeConfigs: FileAttributeConfig[];
@ViewChild('filenameTemplate', { static: true }) filenameTemplate: TemplateRef<never>;
@ViewChild('addedOnTemplate', { static: true }) addedOnTemplate: TemplateRef<never>;
@ViewChild('attributeTemplate', { static: true }) attributeTemplate: TemplateRef<never>;
@ViewChild('needsWorkTemplate', { static: true }) needsWorkTemplate: TemplateRef<never>;
@ViewChild('reviewerTemplate', { static: true }) reviewerTemplate: TemplateRef<never>;
@ViewChild('pagesTemplate', { static: true }) pagesTemplate: TemplateRef<never>;
@ViewChild('statusTemplate', { static: true }) statusTemplate: TemplateRef<never>;
@ViewChild('filenameTemplate', { static: true }) filenameTemplate: TemplateRef<unknown>;
@ViewChild('addedOnTemplate', { static: true }) addedOnTemplate: TemplateRef<unknown>;
@ViewChild('attributeTemplate', { static: true }) attributeTemplate: TemplateRef<unknown>;
@ViewChild('needsWorkTemplate', { static: true }) needsWorkTemplate: TemplateRef<unknown>;
@ViewChild('reviewerTemplate', { static: true }) reviewerTemplate: TemplateRef<unknown>;
@ViewChild('pagesTemplate', { static: true }) pagesTemplate: TemplateRef<unknown>;
@ViewChild('statusTemplate', { static: true }) statusTemplate: TemplateRef<unknown>;
readonly workflowConfig: WorkflowConfig<FileStatusWrapper, StatusEnum>;
protected readonly _primaryKey = 'filename';
@ViewChild(DossierDetailsComponent, { static: false })
private readonly _dossierDetailsComponent: DossierDetailsComponent;
private _lastScrolledIndex: number;
@ -112,7 +111,6 @@ export class DossierOverviewScreenComponent extends ListingComponent<FileStatusW
private readonly _changeDetectorRef: ChangeDetectorRef,
private readonly _fileUploadService: FileUploadService,
private readonly _statusOverlayService: StatusOverlayService,
private readonly _userPreferenceService: UserPreferenceService,
private readonly _fileDropOverlayService: FileDropOverlayService,
private readonly _dossierAttributesService: DossierAttributesService,
private readonly _fileActionService: FileActionService
@ -243,8 +241,6 @@ export class DossierOverviewScreenComponent extends ListingComponent<FileStatusW
.pipe(tap(index => (this._lastScrolledIndex = index)))
.subscribe();
this.searchService.setSearchKey('filename');
this.dossierAttributes = await this._dossierAttributesService.getValues(this.currentDossier);
} catch (e) {
} finally {
@ -296,22 +292,22 @@ export class DossierOverviewScreenComponent extends ListingComponent<FileStatusW
}
@HostListener('drop', ['$event'])
onDrop(event: DragEvent) {
onDrop(event: DragEvent): void {
handleFileDrop(event, this.currentDossier, this._uploadFiles.bind(this));
}
@HostListener('dragover', ['$event'])
onDragOver(event) {
onDragOver(event): void {
event.stopPropagation();
event.preventDefault();
}
async uploadFiles(files: File[] | FileList) {
async uploadFiles(files: File[] | FileList): Promise<void> {
await this._uploadFiles(convertFiles(files, this.currentDossier));
this._fileInput.nativeElement.value = null;
}
async bulkActionPerformed() {
async bulkActionPerformed(): Promise<void> {
this.entitiesService.setSelected([]);
await this.reloadDossiers();
}
@ -351,7 +347,7 @@ export class DossierOverviewScreenComponent extends ListingComponent<FileStatusW
this.tableColumnConfigs = [
{
label: _('dossier-overview.table-col-names.name'),
sortByKey: 'filename',
sortByKey: 'searchKey',
template: this.filenameTemplate,
width: '3fr'
},
@ -462,33 +458,40 @@ export class DossierOverviewScreenComponent extends ListingComponent<FileStatusW
});
});
const statusFilters = [...allDistinctFileStatusWrapper].map<NestedFilter>(item => ({
key: item,
label: this._translateService.instant(fileStatusTranslations[item])
}));
const statusFilters = [...allDistinctFileStatusWrapper].map<INestedFilter>(
item =>
new NestedFilter({
id: item,
label: this._translateService.instant(fileStatusTranslations[item])
})
);
this.filterService.addFilterGroup({
slug: 'statusFilters',
label: this._translateService.instant('filters.status'),
icon: 'red:status',
filters: statusFilters.sort(StatusSorter.byStatus),
filters: statusFilters.sort((a, b) => StatusSorter[a.id] - StatusSorter[b.id]),
checker: keyChecker('status')
});
const peopleFilters = [];
const peopleFilters: INestedFilter[] = [];
if (allDistinctPeople.has(undefined) || allDistinctPeople.has(null)) {
allDistinctPeople.delete(undefined);
allDistinctPeople.delete(null);
peopleFilters.push({
key: null,
label: this._translateService.instant('initials-avatar.unassigned')
});
peopleFilters.push(
new NestedFilter({
id: null,
label: this._translateService.instant('initials-avatar.unassigned')
})
);
}
allDistinctPeople.forEach(userId => {
peopleFilters.push({
key: userId,
label: this._userService.getNameForId(userId)
});
peopleFilters.push(
new NestedFilter({
id: userId,
label: this._userService.getNameForId(userId)
})
);
});
this.filterService.addFilterGroup({
slug: 'peopleFilters',
@ -498,10 +501,13 @@ export class DossierOverviewScreenComponent extends ListingComponent<FileStatusW
checker: keyChecker('currentReviewer')
});
const needsWorkFilters = [...allDistinctNeedsWork].map<NestedFilter>(item => ({
key: item,
label: workloadTranslations[item]
}));
const needsWorkFilters = [...allDistinctNeedsWork].map<INestedFilter>(
item =>
new NestedFilter({
id: item,
label: workloadTranslations[item]
})
);
this.filterService.addFilterGroup({
slug: 'needsWorkFilters',
@ -520,11 +526,14 @@ export class DossierOverviewScreenComponent extends ListingComponent<FileStatusW
slug: key,
label: key,
icon: 'red:template',
filters: [...filterValue].map<NestedFilter>((value: string) => ({
key: value,
label: value === '-' ? this._translateService.instant('filters.empty') : value
})),
checker: (input: FileStatusWrapper, filter: NestedFilter) => filter.key === input.fileAttributes.attributeIdToValue[id]
filters: [...filterValue].map<INestedFilter>(
(value: string) =>
new NestedFilter({
id: value,
label: value === '-' ? this._translateService.instant('filters.empty') : value
})
),
checker: (input: FileStatusWrapper, filter: INestedFilter) => filter.id === input.fileAttributes.attributeIdToValue[id]
});
});
@ -537,10 +546,13 @@ export class DossierOverviewScreenComponent extends ListingComponent<FileStatusW
this.checkedNotRequiredFilters.reduce((acc, f) => acc || f.checker(file), false))
});
const filesNamesFilters = this.entitiesService.all.map<NestedFilter>(file => ({
key: file.filename,
label: file.filename
}));
const filesNamesFilters = this.entitiesService.all.map<INestedFilter>(
file =>
new NestedFilter({
id: file.filename,
label: file.filename
})
);
this.filterService.addFilterGroup({
slug: 'filesNamesFilter',
@ -552,13 +564,13 @@ export class DossierOverviewScreenComponent extends ListingComponent<FileStatusW
});
}
private _createQuickFilters() {
let quickFilters = [];
private _createQuickFilters(): INestedFilter[] {
let quickFilters: INestedFilter[] = [];
if (this.entitiesService.all.filter(this.recentlyModifiedChecker).length > 0) {
const recentPeriod = this._configService.values.RECENT_PERIOD_IN_HOURS;
quickFilters = [
{
key: 'recent',
id: 'recent',
label: this._translateService.instant('dossier-overview.quick-filters.recent', {
hours: recentPeriod
}),
@ -571,20 +583,20 @@ export class DossierOverviewScreenComponent extends ListingComponent<FileStatusW
return [
...quickFilters,
{
key: 'assigned-to-me',
id: 'assigned-to-me',
label: this._translateService.instant('dossier-overview.quick-filters.assigned-to-me'),
checker: (file: FileStatusWrapper) => file.currentReviewer === this.currentUser.id
},
{
key: 'unassigned',
id: 'unassigned',
label: this._translateService.instant('dossier-overview.quick-filters.unassigned'),
checker: (file: FileStatusWrapper) => !file.currentReviewer
},
{
key: 'assigned-to-others',
id: 'assigned-to-others',
label: this._translateService.instant('dossier-overview.quick-filters.assigned-to-others'),
checker: (file: FileStatusWrapper) => !!file.currentReviewer && file.currentReviewer !== this.currentUser.id
}
];
].map(filter => new NestedFilter(filter));
}
}

View File

@ -8,8 +8,8 @@ import {
CircleButtonTypes,
Debounce,
FilterService,
INestedFilter,
LoadingService,
NestedFilter,
processFilters,
Toaster
} from '@iqser/common-ui';
@ -80,7 +80,7 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
read: TemplateRef,
static: true
})
private readonly _filterTemplate: TemplateRef<NestedFilter>;
private readonly _filterTemplate: TemplateRef<INestedFilter>;
@ViewChild('fileActions') fileActions: FileActionsComponent;
constructor(

View File

@ -1,23 +1,29 @@
import { Component, forwardRef, Injector, OnDestroy, OnInit, TemplateRef, ViewChild } from '@angular/core';
import { DefaultListingServices, IListable, keyChecker, ListingComponent, LoadingService, TableColumnConfig } from '@iqser/common-ui';
import { MatchedDocument, SearchControllerService, SearchResult } from '@redaction/red-ui-http';
import {
DefaultListingServices,
IListable,
keyChecker,
ListingComponent,
LoadingService,
NestedFilter,
TableColumnConfig
} from '@iqser/common-ui';
import { List, MatchedDocument, SearchControllerService, SearchResult } from '@redaction/red-ui-http';
import { BehaviorSubject, Observable } from 'rxjs';
import { debounceTime, map, skip, switchMap, tap } from 'rxjs/operators';
import { ActivatedRoute, Router } from '@angular/router';
import { AppStateService } from '@state/app-state.service';
import { FileStatusWrapper } from '@models/file/file-status.wrapper';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { fileStatusTranslations } from '../../translations/file-status-translations';
import { SearchPositions } from '@shared/components/page-header/models/search-positions.type';
import { Dossier } from '../../../../state/model/dossier';
import { TranslateService } from '@ngx-translate/core';
import { RouterHistoryService } from '@services/router-history.service';
interface ListItem extends IListable {
readonly dossierId: string;
readonly filename: string;
readonly unmatched: readonly string[] | null;
readonly highlights: Record<string, readonly string[]>;
readonly unmatched: List | null;
readonly highlights: Record<string, List>;
readonly routerLink: string;
readonly status: string;
readonly dossierName: string;
@ -26,7 +32,7 @@ interface ListItem extends IListable {
interface SearchInput {
readonly query: string;
readonly dossierIds?: readonly string[];
readonly dossierIds?: List;
}
@Component({
@ -53,7 +59,6 @@ export class SearchScreenComponent extends ListingComponent<ListItem> implements
tap(result => this.entitiesService.setEntities(result)),
tap(() => this._loadingService.stop())
);
protected readonly _primaryKey = 'filename';
constructor(
private readonly _router: Router,
@ -72,10 +77,13 @@ export class SearchScreenComponent extends ListingComponent<ListItem> implements
label: this._translateService.instant('search-screen.filters.by-dossier'),
filterceptionPlaceholder: this._translateService.instant('search-screen.filters.search-placeholder'),
icon: 'red:folder',
filters: this._appStateService.allDossiers.map(dossier => ({
key: dossier.id,
label: dossier.dossierName
})),
filters: this._appStateService.allDossiers.map(
dossier =>
new NestedFilter({
id: dossier.id,
label: dossier.dossierName
})
),
checker: keyChecker('id')
});
@ -89,17 +97,13 @@ export class SearchScreenComponent extends ListingComponent<ListItem> implements
this.addSubscription = this.searchService.valueChanges$.pipe(debounceTime(300)).subscribe(value => this.updateNavigation(value));
this.addSubscription = this.filterService.filterGroups$.pipe(skip(1)).subscribe(group => {
const dossierIds = group[0].filters.filter(v => v.checked).map(v => v.key);
const dossierIds = group[0].filters.filter(v => v.checked).map(v => v.id);
this.search$.next({ query: this.searchService.searchValue, dossierIds: dossierIds });
});
}
routerLinkFn = (entity: ListItem) => [entity.routerLink];
setInitialConfig(): void {
return;
}
updateNavigation(query: string, mustContain?: string): void {
const newQuery = query?.replace(mustContain, `"${mustContain}"`);
const queryParams = newQuery && newQuery !== '' ? { query: newQuery } : {};
@ -137,14 +141,6 @@ export class SearchScreenComponent extends ListingComponent<ListItem> implements
this.search$.next({ query, dossierIds: dossierId ? [dossierId] : [] });
}
private _getFileWrapper(dossierId: string, fileId: string): FileStatusWrapper {
return this._appStateService.getFileById(dossierId, fileId);
}
private _getDossierWrapper(dossierId: string): Dossier {
return this._appStateService.getDossierById(dossierId);
}
private _toMatchedDocuments({ matchedDocuments }: SearchResult): MatchedDocument[] {
return matchedDocuments.filter(doc => doc.score > 0 && doc.matchedTerms.length > 0);
}
@ -154,7 +150,7 @@ export class SearchScreenComponent extends ListingComponent<ListItem> implements
}
private _toListItem({ dossierId, fileId, unmatchedTerms, highlights }: MatchedDocument): ListItem {
const fileWrapper = this._getFileWrapper(dossierId, fileId);
const fileWrapper = this._appStateService.getFileById(dossierId, fileId);
if (!fileWrapper) {
return undefined;
}
@ -166,8 +162,9 @@ export class SearchScreenComponent extends ListingComponent<ListItem> implements
highlights,
status: fileWrapper.status,
numberOfPages: fileWrapper.numberOfPages,
dossierName: this._getDossierWrapper(dossierId).dossierName,
dossierName: this._appStateService.getDossierById(dossierId).dossierName,
filename: fileWrapper.filename,
searchKey: fileWrapper.filename,
routerLink: `/main/dossiers/${dossierId}/file/${fileId}`
};
}

View File

@ -1,38 +1,36 @@
import { Injectable } from '@angular/core';
import { AnnotationWrapper } from '@models/file/annotation.wrapper';
import { SuperTypeSorter } from '@utils/sorters/super-type-sorter';
import { handleCheckedValue, NestedFilter } from '@iqser/common-ui';
import { handleCheckedValue, IFilter, INestedFilter, NestedFilter } from '@iqser/common-ui';
import { annotationTypesTranslations } from '../../../translations/annotation-types-translations';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
@Injectable()
export class AnnotationProcessingService {
static get secondaryAnnotationFilters(): NestedFilter[] {
static get secondaryAnnotationFilters(): INestedFilter[] {
return [
{
key: 'with-comments',
id: 'with-comments',
icon: 'red:comment',
label: _('filter-menu.with-comments'),
checked: false,
topLevelFilter: true,
children: [],
checker: (annotation: AnnotationWrapper) => annotation?.comments?.length > 0
},
{
key: 'with-reason-changes',
id: 'with-reason-changes',
icon: 'red:reason',
label: _('filter-menu.with-reason-changes'),
checked: false,
topLevelFilter: true,
children: [],
checker: (annotation: AnnotationWrapper) => annotation?.legalBasisChangeValue?.length > 0
}
];
].map(item => new NestedFilter(item));
}
getAnnotationFilter(annotations: AnnotationWrapper[]): NestedFilter[] {
const filterMap = new Map<string, NestedFilter>();
const filters: NestedFilter[] = [];
getAnnotationFilter(annotations: AnnotationWrapper[]): INestedFilter[] {
const filterMap = new Map<string, INestedFilter>();
const filters: INestedFilter[] = [];
annotations?.forEach(a => {
const topLevelFilter = a.superType !== 'hint' && a.superType !== 'redaction' && a.superType !== 'recommendation';
@ -49,11 +47,11 @@ export class AnnotationProcessingService {
if (!parentFilter) {
parentFilter = this._createParentFilter(a.superType, filterMap, filters);
}
const childFilter = {
key: a.type,
const childFilter: IFilter = {
id: a.type,
label: a.type,
searchKey: a.type,
checked: false,
filters: [],
matches: 1
};
filterMap.set(key, childFilter);
@ -63,7 +61,7 @@ export class AnnotationProcessingService {
});
for (const filter of filters) {
filter.children.sort((a, b) => a.key.localeCompare(b.key));
filter.children.sort((a, b) => a.id.localeCompare(b.id));
handleCheckedValue(filter);
if (filter.checked || filter.indeterminate) {
filter.expanded = true;
@ -73,13 +71,13 @@ export class AnnotationProcessingService {
}
}
return filters.sort((a, b) => SuperTypeSorter[a.key] - SuperTypeSorter[b.key]);
return filters.sort((a, b) => SuperTypeSorter[a.id] - SuperTypeSorter[b.id]);
}
filterAndGroupAnnotations(
annotations: AnnotationWrapper[],
primaryFilters: NestedFilter[],
secondaryFilters?: NestedFilter[]
primaryFilters: INestedFilter[],
secondaryFilters?: INestedFilter[]
): Map<number, AnnotationWrapper[]> {
const obj = new Map<number, AnnotationWrapper[]>();
@ -116,21 +114,20 @@ export class AnnotationProcessingService {
return obj;
}
private _createParentFilter(key: string, filterMap: Map<string, NestedFilter>, filters: NestedFilter[]) {
const filter: NestedFilter = {
key: key,
private _createParentFilter(key: string, filterMap: Map<string, INestedFilter>, filters: INestedFilter[]) {
const filter: INestedFilter = new NestedFilter({
id: key,
topLevelFilter: true,
matches: 1,
label: annotationTypesTranslations[key],
children: []
};
label: annotationTypesTranslations[key]
});
filterMap.set(key, filter);
filters.push(filter);
return filter;
}
private _getFlatFilters(filters: NestedFilter[], filterBy?: (f: NestedFilter) => boolean) {
const flatFilters: NestedFilter[] = [];
private _getFlatFilters(filters: INestedFilter[], filterBy?: (f: INestedFilter) => boolean) {
const flatFilters: INestedFilter[] = [];
filters.forEach(filter => {
flatFilters.push(filter);
@ -140,7 +137,7 @@ export class AnnotationProcessingService {
return filterBy ? flatFilters.filter(f => filterBy(f)) : flatFilters;
}
private _matchesOne = (filters: NestedFilter[], condition: (filter: NestedFilter) => boolean): boolean => {
private _matchesOne = (filters: INestedFilter[], condition: (filter: INestedFilter) => boolean): boolean => {
if (filters.length === 0) {
return true;
}
@ -154,7 +151,7 @@ export class AnnotationProcessingService {
return false;
};
private _matchesAll = (filters: NestedFilter[], condition: (filter: NestedFilter) => boolean): boolean => {
private _matchesAll = (filters: INestedFilter[], condition: (filter: INestedFilter) => boolean): boolean => {
if (filters.length === 0) {
return true;
}
@ -168,11 +165,11 @@ export class AnnotationProcessingService {
return true;
};
private _checkByFilterKey = (filter: NestedFilter, annotation: AnnotationWrapper) => {
private _checkByFilterKey = (filter: INestedFilter, annotation: AnnotationWrapper) => {
const superType = annotation.superType;
const isNotTopLevelFilter = superType === 'hint' || superType === 'redaction' || superType === 'recommendation';
return filter.key === superType || (filter.key === annotation.type && isNotTopLevelFilter);
return filter.id === superType || (filter.id === annotation.type && isNotTopLevelFilter);
};
private _sortAnnotations(annotations: AnnotationWrapper[]): AnnotationWrapper[] {

View File

@ -1,7 +1,7 @@
import { Component, EventEmitter, Input, Optional, Output, TemplateRef } from '@angular/core';
import { ActionConfig } from '@shared/components/page-header/models/action-config.model';
import { ButtonConfig } from '@shared/components/page-header/models/button-config.model';
import { FilterService, IconButtonTypes, Listable, SearchService } from '@iqser/common-ui';
import { FilterService, IListable, SearchService } from '@iqser/common-ui';
import { distinctUntilChanged, map } from 'rxjs/operators';
import { combineLatest, Observable, of } from 'rxjs';
import { SearchPosition, SearchPositions } from '@shared/components/page-header/models/search-positions.type';
@ -12,7 +12,7 @@ import { FileAttributeConfig } from '@redaction/red-ui-http';
templateUrl: './page-header.component.html',
styleUrls: ['./page-header.component.scss']
})
export class PageHeaderComponent<T extends Listable> {
export class PageHeaderComponent<T extends IListable> {
readonly searchPositions = SearchPositions;
readonly iconButtonTypes = IconButtonTypes;

View File

@ -56,7 +56,7 @@ export class SimpleDoughnutChartComponent implements OnChanges {
}
filterChecked$(key: string): Observable<boolean> {
return this.statusFilters$.pipe(map(all => all?.find(e => e.key === key)?.checked));
return this.statusFilters$.pipe(map(all => all?.find(e => e.id === key)?.checked));
}
calculateChartData() {

View File

@ -1,38 +1,38 @@
import { FileStatusWrapper } from '@models/file/file-status.wrapper';
import { Dossier } from '../state/model/dossier';
import { handleCheckedValue, NestedFilter } from '@iqser/common-ui';
import { handleCheckedValue, INestedFilter } from '@iqser/common-ui';
export function handleFilterDelta(oldFilters: NestedFilter[], newFilters: NestedFilter[], allFilters: NestedFilter[]) {
export function handleFilterDelta(oldFilters: INestedFilter[], newFilters: INestedFilter[], allFilters: INestedFilter[]) {
const newFiltersDelta = {};
for (const newFilter of newFilters) {
const oldFilter = oldFilters.find(f => f.key === newFilter.key);
const oldFilter = oldFilters.find(f => f.id === newFilter.id);
if (!oldFilter || oldFilter.matches !== newFilter.matches) {
newFiltersDelta[newFilter.key] = {};
newFilter.children.forEach(filter => (newFiltersDelta[newFilter.key][filter.key] = {}));
newFiltersDelta[newFilter.id] = {};
newFilter.children.forEach(filter => (newFiltersDelta[newFilter.id][filter.id] = {}));
}
if (!oldFilter) {
for (const childFilter of newFilter.children) {
const oldFilterChild = oldFilter?.children.find(f => f.key === childFilter.key);
const oldFilterChild = oldFilter?.children.find(f => f.id === childFilter.id);
if (!oldFilterChild || oldFilterChild.matches !== childFilter.matches) {
if (!newFiltersDelta[newFilter.key]) {
newFiltersDelta[newFilter.key] = {};
if (!newFiltersDelta[newFilter.id]) {
newFiltersDelta[newFilter.id] = {};
}
newFiltersDelta[newFilter.key][childFilter.key] = {};
newFiltersDelta[newFilter.id][childFilter.id] = {};
}
}
}
}
for (const key of Object.keys(newFiltersDelta)) {
const foundFilter = allFilters.find(f => f.key === key);
const foundFilter = allFilters.find(f => f.id === key);
if (foundFilter) {
// if has children
if (!foundFilter.children?.length) {
foundFilter.checked = true;
} else {
for (const subKey of Object.keys(newFiltersDelta[key])) {
const childFilter = foundFilter.children.find(f => f.key === subKey);
const childFilter = foundFilter.children.find(f => f.id === subKey);
if (childFilter) {
childFilter.checked = true;
}
@ -45,8 +45,8 @@ export function handleFilterDelta(oldFilters: NestedFilter[], newFilters: Nested
});
}
export const annotationFilterChecker = (input: FileStatusWrapper | Dossier, filter: NestedFilter) => {
switch (filter.key) {
export const annotationFilterChecker = (input: FileStatusWrapper | Dossier, filter: INestedFilter) => {
switch (filter.id) {
case 'analysis': {
if (input instanceof Dossier) {
return input.reanalysisRequired;
@ -78,10 +78,10 @@ export const annotationFilterChecker = (input: FileStatusWrapper | Dossier, filt
}
};
export const dossierStatusChecker = (dw: Dossier, filter: NestedFilter) => dw.hasStatus(filter.key);
export const dossierStatusChecker = (dw: Dossier, filter: INestedFilter) => dw.hasStatus(filter.id);
export const dossierMemberChecker = (dw: Dossier, filter: NestedFilter) => dw.hasMember(filter.key);
export const dossierMemberChecker = (dw: Dossier, filter: INestedFilter) => dw.hasMember(filter.id);
export const dossierTemplateChecker = (dw: Dossier, filter: NestedFilter) => dw.dossierTemplateId === filter.key;
export const dossierTemplateChecker = (dw: Dossier, filter: INestedFilter) => dw.dossierTemplateId === filter.id;
export const dossierApproverChecker = (dw: Dossier, filter: NestedFilter) => dw.approverIds.includes(filter.key);
export const dossierApproverChecker = (dw: Dossier, filter: INestedFilter) => dw.approverIds.includes(filter.id);

View File

@ -6,5 +6,5 @@ export const RedactionFilterSorter = {
hint: 4,
suggestion: 5,
none: 6,
byKey: (a: { key: string }, b: { key: string }) => RedactionFilterSorter[a.key] - RedactionFilterSorter[b.key]
byKey: (a: { id: string }, b: { id: string }) => RedactionFilterSorter[a.id] - RedactionFilterSorter[b.id]
};