From 6fa2f6fdfa7551b9a3664ed47e1229e30ba6b9d0 Mon Sep 17 00:00:00 2001 From: Valentin Mihai Date: Tue, 14 Jun 2022 19:27:11 +0300 Subject: [PATCH] RED-4175 - As a user I want to filter the search results --- .../search-screen.component.html | 2 + .../search-screen/search-screen.component.ts | 138 ++++++++++++++++-- .../platform-search.service.ts | 13 +- apps/red-ui/src/assets/i18n/de.json | 9 +- apps/red-ui/src/assets/i18n/en.json | 10 +- libs/common-ui | 2 +- .../red-domain/src/lib/search/search-input.ts | 4 + .../src/lib/search/search-list-item.ts | 1 + .../src/lib/search/search.request.ts | 3 + 9 files changed, 161 insertions(+), 21 deletions(-) diff --git a/apps/red-ui/src/app/modules/search/search-screen/search-screen.component.html b/apps/red-ui/src/app/modules/search/search-screen/search-screen.component.html index 89d1de2b1..3d80ba881 100644 --- a/apps/red-ui/src/app/modules/search/search-screen/search-screen.component.html +++ b/apps/red-ui/src/app/modules/search/search-screen/search-screen.component.html @@ -89,3 +89,5 @@ + + diff --git a/apps/red-ui/src/app/modules/search/search-screen/search-screen.component.ts b/apps/red-ui/src/app/modules/search/search-screen/search-screen.component.ts index 5eefcaee8..22caab14b 100644 --- a/apps/red-ui/src/app/modules/search/search-screen/search-screen.component.ts +++ b/apps/red-ui/src/app/modules/search/search-screen/search-screen.component.ts @@ -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 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 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 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 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 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 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 imp this.searchService.searchValue = newQuery ?? ''; } - private _updateNavigation(query?: string, dossierIds?: string[], onlyActive?: boolean): Promise { - const queryParams = { query, dossierIds: dossierIds.join(','), onlyActive }; + private _updateNavigation( + query?: string, + dossierIds?: string[], + workflowStatus?: WorkflowFileStatus, + assignee?: string, + dossierTemplateIds?: string[], + onlyActive?: boolean, + ): Promise { + 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 imp return { id: fileId, dossierId, + dossierTemplateId: file.dossierTemplateId, deleted: dossierDeleted, archived: dossierArchived, unmatched: unmatchedTerms || null, diff --git a/apps/red-ui/src/app/services/entity-services/platform-search.service.ts b/apps/red-ui/src/app/services/entity-services/platform-search.service.ts index 7f51abf7c..6e8398581 100644 --- a/apps/red-ui/src/app/services/entity-services/platform-search.service.ts +++ b/apps/red-ui/src/app/services/entity-services/platform-search.service.ts @@ -18,7 +18,15 @@ export class PlatformSearchService extends GenericService { super(_injector, 'search-v2'); } - search({ dossierIds, query, includeDeletedDossiers, includeArchivedDossiers }: ISearchInput): Observable { + search({ + dossierIds, + dossierTemplateIds, + workflowStatus, + assignee, + query, + includeDeletedDossiers, + includeArchivedDossiers, + }: ISearchInput): Observable { if (!query) { return of({ matchedDocuments: [], @@ -28,6 +36,9 @@ export class PlatformSearchService extends GenericService { const body: ISearchRequest = { dossierIds, + dossierTemplateIds, + workflowStatus, + assignee, queryString: query ?? '', page: 0, returnSections: false, diff --git a/apps/red-ui/src/assets/i18n/de.json b/apps/red-ui/src/assets/i18n/de.json index 3fa044f0e..09e61b815 100644 --- a/apps/red-ui/src/assets/i18n/de.json +++ b/apps/red-ui/src/assets/i18n/de.json @@ -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", diff --git a/apps/red-ui/src/assets/i18n/en.json b/apps/red-ui/src/assets/i18n/en.json index 6a412681c..6be17fe89 100644 --- a/apps/red-ui/src/assets/i18n/en.json +++ b/apps/red-ui/src/assets/i18n/en.json @@ -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", diff --git a/libs/common-ui b/libs/common-ui index 9ee7b12e4..d4e2bd43a 160000 --- a/libs/common-ui +++ b/libs/common-ui @@ -1 +1 @@ -Subproject commit 9ee7b12e4a51c7d1ee8923dc87e359b12e5be118 +Subproject commit d4e2bd43ada36ae0b11be539ed613efec7e8ca57 diff --git a/libs/red-domain/src/lib/search/search-input.ts b/libs/red-domain/src/lib/search/search-input.ts index c2524d142..020d8bac9 100644 --- a/libs/red-domain/src/lib/search/search-input.ts +++ b/libs/red-domain/src/lib/search/search-input.ts @@ -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; } diff --git a/libs/red-domain/src/lib/search/search-list-item.ts b/libs/red-domain/src/lib/search/search-list-item.ts index 70932948b..f8a815ddc 100644 --- a/libs/red-domain/src/lib/search/search-list-item.ts +++ b/libs/red-domain/src/lib/search/search-list-item.ts @@ -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; diff --git a/libs/red-domain/src/lib/search/search.request.ts b/libs/red-domain/src/lib/search/search.request.ts index c142351d3..a7d19af88 100644 --- a/libs/red-domain/src/lib/search/search.request.ts +++ b/libs/red-domain/src/lib/search/search.request.ts @@ -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; }