RED-4175 - As a user I want to filter the search results
This commit is contained in:
parent
e118b507c2
commit
6fa2f6fdfa
@ -89,3 +89,5 @@
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<ng-container *ngIf="dossierTemplates$ | async"></ng-container>
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user