RED-4175 - As a user I want to filter the search results

This commit is contained in:
Valentin Mihai 2022-06-14 19:27:11 +03:00
parent e118b507c2
commit 6fa2f6fdfa
9 changed files with 161 additions and 21 deletions

View File

@ -89,3 +89,5 @@
</div>
</div>
</ng-template>
<ng-container *ngIf="dossierTemplates$ | async"></ng-container>

View File

@ -1,4 +1,4 @@
import { ChangeDetectionStrategy, Component, forwardRef, Injector, OnDestroy } from '@angular/core';
import { ChangeDetectionStrategy, Component, forwardRef, Injector, OnDestroy, OnInit } from '@angular/core';
import {
DefaultListingServices,
IFilterGroup,
@ -10,18 +10,29 @@ import {
SortingOrders,
TableColumnConfig,
} from '@iqser/common-ui';
import { combineLatest, Observable, of } from 'rxjs';
import { combineLatest, firstValueFrom, Observable, of } from 'rxjs';
import { debounceTime, map, startWith, switchMap, tap } from 'rxjs/operators';
import { ActivatedRoute, Router } from '@angular/router';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { workflowFileStatusTranslations } from '@translations/file-status-translations';
import { TranslateService } from '@ngx-translate/core';
import { RouterHistoryService } from '@services/router-history.service';
import { Dossier, DOSSIERS_ARCHIVE, IMatchedDocument, ISearchListItem, ISearchResponse } from '@red/domain';
import {
Dossier,
DOSSIERS_ARCHIVE,
DossierTemplate,
IMatchedDocument,
ISearchListItem,
ISearchResponse,
WorkflowFileStatus,
WorkflowFileStatuses,
} from '@red/domain';
import { FilesMapService } from '@services/files/files-map.service';
import { PlatformSearchService } from '@services/entity-services/platform-search.service';
import { FeaturesService } from '@services/features.service';
import { DossiersCacheService } from '../../../services/dossiers/dossiers-cache.service';
import { DossierTemplatesService } from '../../../services/dossier-templates/dossier-templates.service';
import { UserService } from '../../../services/user.service';
@Component({
templateUrl: './search-screen.component.html',
@ -44,11 +55,16 @@ export class SearchScreenComponent extends ListingComponent<ISearchListItem> imp
readonly searchResults$ = combineLatest([this._queryChanged, this._filtersChanged$]).pipe(
tap(() => this._loadingService.start()),
tap(([query, [dossierIds, onlyActive]]) => this._updateNavigation(query, dossierIds, onlyActive)),
switchMap(([query, [dossierIds, onlyActive]]) =>
tap(([query, [dossierIds, workflowStatus, assignee, dossierTemplateIds, onlyActive]]) =>
this._updateNavigation(query, dossierIds, workflowStatus, assignee, dossierTemplateIds, onlyActive),
),
switchMap(([query, [dossierIds, workflowStatus, assignee, dossierTemplateIds, onlyActive]]) =>
this._platformSearchService.search({
query,
dossierIds,
dossierTemplateIds,
workflowStatus,
assignee,
includeDeletedDossiers: false,
includeArchivedDossiers: !onlyActive,
}),
@ -59,6 +75,8 @@ export class SearchScreenComponent extends ListingComponent<ISearchListItem> imp
tap(() => this._loadingService.stop()),
);
readonly dossierTemplates$ = this._dossierTemplateService.loadAll().pipe(tap(templates => this._addTemplateFilter(templates)));
constructor(
private readonly _router: Router,
protected readonly _injector: Injector,
@ -70,11 +88,16 @@ export class SearchScreenComponent extends ListingComponent<ISearchListItem> imp
private readonly _filesMapService: FilesMapService,
private readonly _platformSearchService: PlatformSearchService,
private readonly _featuresService: FeaturesService,
private readonly _dossierTemplateService: DossierTemplatesService,
private readonly _userService: UserService,
) {
super(_injector);
this.searchService.skip = true;
this.sortingService.setSortingOption({ column: 'searchKey', order: SortingOrders.desc });
this._initFilters();
}
private _initFilters() {
const dossierIds = this._routeDossierIds;
const dossierToFilter = ({ dossierName, id }: Dossier) => {
const checked = dossierIds.includes(id);
@ -88,17 +111,86 @@ export class SearchScreenComponent extends ListingComponent<ISearchListItem> imp
filters: this._dossiersCacheService.all.map(dossierToFilter),
checker: keyChecker('dossierId'),
};
this.filterService.addFilterGroups([dossierNameFilter]);
const status = this._routeStatus;
const statusToFilter = (workflowStatus: WorkflowFileStatus) => {
const checked = status === workflowStatus;
return new NestedFilter({
id: workflowStatus,
label: this._translateService.instant(workflowFileStatusTranslations[workflowStatus]),
checked,
});
};
const workflowStatusFilter: IFilterGroup = {
slug: 'status',
label: this._translateService.instant('search-screen.filters.status'),
icon: 'red:status',
filters: Object.values(WorkflowFileStatuses).map(statusToFilter),
singleSelect: true,
checker: keyChecker('status'),
};
const assignee = this._routeAssignee;
const assigneeToFilter = (userId: string) => {
const checked = assignee === userId;
return new NestedFilter({ id: userId, label: this._userService.getNameForId(userId), checked });
};
const assigneeFilter: IFilterGroup = {
slug: 'assignee',
label: this._translateService.instant('search-screen.filters.assignee'),
icon: 'red:user',
filters: this._userService.all.map(user => user.id).map(assigneeToFilter),
singleSelect: true,
checker: keyChecker('assignee'),
};
assigneeFilter.filters.push(
new NestedFilter({
id: null,
label: this._translateService.instant('initials-avatar.unassigned'),
}),
);
this.filterService.addFilterGroups([dossierNameFilter, workflowStatusFilter, assigneeFilter]);
const onlyActiveLabel = this._translateService.instant('search-screen.filters.only-active');
if (this.#enabledArchive) {
this.filterService.addSingleFilter({ id: 'onlyActiveDossiers', label: onlyActiveLabel, checked: this._routeOnlyActive });
}
}
private _addTemplateFilter(templates: DossierTemplate[]) {
const templatesIds = this._routeDossierTemplateIds;
const templateToFilter = ({ name, id }: DossierTemplate) => {
const checked = templatesIds?.includes(id);
return new NestedFilter({ id, label: name, checked });
};
const templateNameFilter: IFilterGroup = {
slug: 'templates',
label: this._translateService.instant('search-screen.filters.by-template'),
filterceptionPlaceholder: this._translateService.instant('search-screen.filters.search-by-template-placeholder'),
icon: 'red:template',
filters: templates.map(templateToFilter),
checker: keyChecker('dossierTemplateId'),
};
this.filterService.addFilterGroups([templateNameFilter]);
}
private get _routeDossierIds(): string[] {
return this._activatedRoute.snapshot.queryParamMap.get('dossierIds').split(',');
}
private get _routeDossierTemplateIds(): string[] {
return this._activatedRoute.snapshot.queryParamMap.get('dossierTemplateIds')?.split(',');
}
private get _routeStatus(): WorkflowFileStatus {
return this._activatedRoute.snapshot.queryParamMap.get('status') as WorkflowFileStatus;
}
private get _routeAssignee(): string {
return this._activatedRoute.snapshot.queryParamMap.get('assignee');
}
private get _routeOnlyActive(): boolean {
return this._activatedRoute.snapshot.queryParamMap.get('onlyActive') === 'true';
}
@ -119,7 +211,7 @@ export class SearchScreenComponent extends ListingComponent<ISearchListItem> imp
return this._featuresService.isEnabled(DOSSIERS_ARCHIVE);
}
private get _filtersChanged$(): Observable<[string[], boolean]> {
private get _filtersChanged$(): Observable<[string[], WorkflowFileStatus, string, string[], boolean]> {
const onlyActiveDossiers$ = this.#enabledArchive
? this.filterService.getSingleFilter('onlyActiveDossiers').pipe(map(f => !!f.checked))
: of(true);
@ -127,9 +219,18 @@ export class SearchScreenComponent extends ListingComponent<ISearchListItem> imp
return combineLatest([filterGroups$, onlyActiveDossiers$]).pipe(
map(([groups, onlyActive]) => {
const dossierIds: string[] = groups[0].filters.filter(v => v.checked).map(v => v.id);
return [dossierIds, onlyActive];
const workflowStatus: WorkflowFileStatus = groups[1].filters.filter(v => v.checked).map(v => v.id)[0] as WorkflowFileStatus;
const assignee: string = groups[2].filters.filter(v => v.checked).map(v => v.id)[0];
const dossierTemplateIds: string[] = groups[3]?.filters.filter(v => v.checked).map(v => v.id);
return [dossierIds, workflowStatus, assignee, dossierTemplateIds, onlyActive];
}),
startWith<[string[], boolean]>([this._routeDossierIds, this._routeOnlyActive]),
startWith<[string[], WorkflowFileStatus, string, string[], boolean]>([
this._routeDossierIds,
this._routeStatus,
this._routeAssignee,
this._routeDossierTemplateIds,
this._routeOnlyActive,
]),
);
}
@ -138,8 +239,22 @@ export class SearchScreenComponent extends ListingComponent<ISearchListItem> imp
this.searchService.searchValue = newQuery ?? '';
}
private _updateNavigation(query?: string, dossierIds?: string[], onlyActive?: boolean): Promise<boolean> {
const queryParams = { query, dossierIds: dossierIds.join(','), onlyActive };
private _updateNavigation(
query?: string,
dossierIds?: string[],
workflowStatus?: WorkflowFileStatus,
assignee?: string,
dossierTemplateIds?: string[],
onlyActive?: boolean,
): Promise<boolean> {
const queryParams = {
query,
dossierIds: dossierIds.join(','),
dossierTemplateIds: dossierTemplateIds?.join(','),
workflowStatus,
assignee,
onlyActive,
};
return this._router.navigate([], { queryParams, replaceUrl: true });
}
@ -171,6 +286,7 @@ export class SearchScreenComponent extends ListingComponent<ISearchListItem> imp
return {
id: fileId,
dossierId,
dossierTemplateId: file.dossierTemplateId,
deleted: dossierDeleted,
archived: dossierArchived,
unmatched: unmatchedTerms || null,

View File

@ -18,7 +18,15 @@ export class PlatformSearchService extends GenericService<ISearchResponse> {
super(_injector, 'search-v2');
}
search({ dossierIds, query, includeDeletedDossiers, includeArchivedDossiers }: ISearchInput): Observable<ISearchResponse> {
search({
dossierIds,
dossierTemplateIds,
workflowStatus,
assignee,
query,
includeDeletedDossiers,
includeArchivedDossiers,
}: ISearchInput): Observable<ISearchResponse> {
if (!query) {
return of({
matchedDocuments: [],
@ -28,6 +36,9 @@ export class PlatformSearchService extends GenericService<ISearchResponse> {
const body: ISearchRequest = {
dossierIds,
dossierTemplateIds,
workflowStatus,
assignee,
queryString: query ?? '',
page: 0,
returnSections: false,

View File

@ -690,7 +690,6 @@
},
"digital-signature-screen": {
"action": {
"certificate-not-valid-error": "",
"delete-error": "Die digitale Signatur konnte nicht entfernt werden, bitte versuchen Sie es erneut.",
"delete-success": "Die digitale Signatur wurde gelöscht. Geschwärzte Dateien werden nicht länger mit einer Signatur versehen!",
"remove": "",
@ -1460,13 +1459,11 @@
"labels": {
"download-cleanup-download-files-hours": "",
"download-cleanup-not-download-files-hours": "",
"remove-digital-signature-on-upload": "",
"soft-delete-cleanup-time": ""
},
"placeholders": {
"download-cleanup-download-files-hours": "",
"download-cleanup-not-download-files-hours": "",
"remove-digital-signature-on-upload": "",
"soft-delete-cleanup-time": ""
},
"title": ""
@ -1887,9 +1884,13 @@
"status": "Status"
},
"filters": {
"assignee": "",
"by-dossier": "Nach Dossier filtern",
"by-template": "",
"only-active": "",
"search-placeholder": "Dossiername..."
"search-by-template-placeholder": "",
"search-placeholder": "Dossiername...",
"status": ""
},
"missing": "Fehlt",
"must-contain": "Muss enthalten",

View File

@ -1460,13 +1460,11 @@
"labels": {
"download-cleanup-download-files-hours": "Deletion time (hours) for download packages that have been generated and downloaded",
"download-cleanup-not-download-files-hours": "Deletion time (hours) for download packages that have been generated but not yet downloaded",
"remove-digital-signature-on-upload": "Remove digital signature on upload",
"soft-delete-cleanup-time": "Deletion time (hours) for deleted files in Trash"
},
"placeholders": {
"download-cleanup-download-files-hours": "(hours)",
"download-cleanup-not-download-files-hours": "(hours)",
"remove-digital-signature-on-upload": "True / False",
"soft-delete-cleanup-time": "(hours)"
},
"title": "System Preferences"
@ -1887,9 +1885,13 @@
"status": "Status"
},
"filters": {
"by-dossier": "Filter by Dossier",
"assignee": "Assignee",
"by-dossier": "Dossier",
"by-template": "Dossier Template",
"only-active": "Active dossiers only",
"search-placeholder": "Dossier name..."
"search-by-template-placeholder": "Dossier Template name...",
"search-placeholder": "Dossier name...",
"status": "Status"
},
"missing": "Missing",
"must-contain": "Must contain",

@ -1 +1 @@
Subproject commit 9ee7b12e4a51c7d1ee8923dc87e359b12e5be118
Subproject commit d4e2bd43ada36ae0b11be539ed613efec7e8ca57

View File

@ -1,8 +1,12 @@
import { List } from '@iqser/common-ui';
import { WorkflowFileStatus } from '../files';
export interface ISearchInput {
readonly query: string;
readonly dossierIds?: List;
readonly dossierTemplateIds?: List;
readonly workflowStatus?: WorkflowFileStatus;
readonly assignee?: string;
readonly includeDeletedDossiers: boolean;
readonly includeArchivedDossiers: boolean;
}

View File

@ -2,6 +2,7 @@ import { IListable, List } from '@iqser/common-ui';
export interface ISearchListItem extends IListable {
readonly dossierId: string;
readonly dossierTemplateId: string;
readonly filename: string;
readonly assignee: string;
readonly unmatched: List | null;

View File

@ -1,4 +1,5 @@
import { List } from '@iqser/common-ui';
import { WorkflowFileStatus } from '../files';
export interface ISearchRequest {
readonly dossierIds?: List;
@ -10,4 +11,6 @@ export interface ISearchRequest {
readonly pageSize?: number;
readonly queryString?: string;
readonly returnSections?: boolean;
readonly workflowStatus?: WorkflowFileStatus;
readonly assignee?: string;
}