filter search results by dossier

This commit is contained in:
Dan Percic 2021-07-27 21:17:24 +03:00
parent e50e5cbfb2
commit c61e6e1aa5
10 changed files with 104 additions and 75 deletions

View File

@ -9,7 +9,6 @@
<redaction-popup-filter
(filtersChanged)="filtersChanged($event)"
[actionsTemplate]="annotationFilterActionTemplate"
[chevron]="true"
[filterTemplate]="annotationFilterTemplate"
[primaryFilters]="primaryFilters"
[secondaryFilters]="secondaryFilters"

View File

@ -3,6 +3,7 @@
[searchPlaceholder]="'search.placeholder' | translate"
[searchWidth]="600"
[showCloseButton]="true"
[searchPosition]="searchPositions.beforeFilters"
></redaction-page-header>
<div class="overlay-shadow"></div>
@ -25,13 +26,13 @@
class="table-item"
>
<div class="filename">
<div [matTooltip]="item.fileName" class="table-item-title heading" matTooltipPosition="above">
<div [matTooltip]="item.filename" class="table-item-title heading" matTooltipPosition="above">
<span
*ngIf="item.highlights.filename; else defaultFilename"
[innerHTML]="item.highlights.filename[0]"
class="highlights"
></span>
<ng-template #defaultFilename>{{ item.fileName }}</ng-template>
<ng-template #defaultFilename>{{ item.filename }}</ng-template>
</div>
<ng-container *ngIf="item.highlights['sections.text'] as highlights">
@ -49,7 +50,7 @@
>&nbsp;<s>{{ term }}</s></span
>.&nbsp;{{ 'search-screen.must-contain' | translate }}:
<span
(click)="$event.stopPropagation(); updateNavigation({ query: search$.getValue(), mustContain: term })"
(click)="$event.stopPropagation(); updateNavigation(search$.getValue(), term)"
*ngFor="let term of unmatched"
>&nbsp;<u>{{ term }}</u></span
>
@ -78,7 +79,7 @@
<div class="small-label stats-subtitle">
<div>
<mat-icon svgIcon="red:pages"></mat-icon>
{{ item.pages }}
{{ item.numberOfPages }}
</div>
</div>
@ -87,7 +88,7 @@
</cdk-virtual-scroll-viewport>
<redaction-scroll-button
*ngIf="(screenStateService.noData$ | async) === false"
*ngIf="searchResult.length"
[itemSize]="itemSize"
[scrollViewport]="scrollViewport"
></redaction-scroll-button>

View File

@ -2,7 +2,7 @@ import { Component, Injector, OnDestroy } from '@angular/core';
import { BaseListingComponent } from '../../../shared/base/base-listing.component';
import { MatchedDocument, SearchControllerService, SearchResult } from '@redaction/red-ui-http';
import { BehaviorSubject, Observable } from 'rxjs';
import { debounceTime, map, switchMap, tap } from 'rxjs/operators';
import { debounceTime, map, skip, switchMap, tap } from 'rxjs/operators';
import { ActivatedRoute, Router } from '@angular/router';
import { TableColConfig } from '../../../shared/components/table-col-name/table-col-name.component';
import { FilterService } from '../../../shared/services/filter.service';
@ -14,16 +14,20 @@ import { FileStatusWrapper } from '../../../../models/file/file-status.wrapper';
import { LoadingService } from '../../../../services/loading.service';
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 { keyChecker } from '../../../shared/components/filters/popup-filter/utils/filter-utils';
import { DossierWrapper } from '../../../../state/model/dossier.wrapper';
import { TranslateService } from '@ngx-translate/core';
interface ListItem {
fileName: string;
matchedDocument: MatchedDocument;
unmatched: string[] | null;
highlights: { [key: string]: string[] };
routerLink: string;
status: string;
dossierName: string;
pages: number;
readonly dossierId: string;
readonly filename: string;
readonly unmatched: string[] | null;
readonly highlights: { [key: string]: string[] };
readonly routerLink: string;
readonly status: string;
readonly dossierName: string;
readonly numberOfPages: number;
}
@Component({
@ -32,14 +36,15 @@ interface ListItem {
providers: [FilterService, SearchService, ScreenStateService, SortingService]
})
export class SearchScreenComponent extends BaseListingComponent<ListItem> implements OnDestroy {
fileStatusTranslations = fileStatusTranslations;
readonly fileStatusTranslations = fileStatusTranslations;
readonly searchPositions = searchPositions;
readonly itemSize = 85;
readonly search$ = new BehaviorSubject<string>(null);
readonly searchResults$: Observable<ListItem[]> = this.search$.asObservable().pipe(
switchMap(query => this._search(query)),
map(searchResult => this._toMatchedDocuments(searchResult)),
map(documents => this._toListItems(documents)),
map(docs => this._toListItems(docs)),
tap(result => this.screenStateService.setEntities(result)),
tap(() => this._loadingService.stop())
);
@ -58,7 +63,7 @@ export class SearchScreenComponent extends BaseListingComponent<ListItem> implem
}
];
protected readonly _primaryKey = 'fileName';
protected _tableHeaderLabel = _('search-screen.table-header');
protected readonly _tableHeaderLabel = _('search-screen.table-header');
private _dossierId: string;
constructor(
@ -67,38 +72,47 @@ export class SearchScreenComponent extends BaseListingComponent<ListItem> implem
private readonly _activatedRoute: ActivatedRoute,
private readonly _appStateService: AppStateService,
private readonly _loadingService: LoadingService,
private readonly _translateService: TranslateService,
private readonly _router: Router
) {
super(_injector);
this.filterService.addFilterGroup({
slug: 'dossiers',
label: this._translateService.instant('search-screen.filters.by-dossier'),
icon: 'red:folder',
values: this._appStateService.allDossiers.map(dossier => ({
key: dossier.dossierId,
label: dossier.dossierName
})),
checker: keyChecker('dossierId')
});
this.addSubscription = _activatedRoute.queryParamMap
.pipe(
tap(() => this._loadingService.start()),
map(value => ({ query: value.get('query'), dossierId: value.get('dossierId') })),
tap(mappedValue => this._updateValues(mappedValue))
map(value => ({ query: value.get('query'), dossierId: value.get('dossierId') }))
)
.subscribe();
.subscribe(mappedValue => this._updateValues(mappedValue));
this.addSubscription = this.searchService.searchForm
.get('query')
.valueChanges.pipe(debounceTime(300))
.subscribe(value => this.updateNavigation({ query: value }));
.subscribe(value => this.updateNavigation(value));
this.addSubscription = this.filterService.filterGroups$
.pipe(skip(1))
.subscribe(() => this.updateNavigation(this.search$.getValue()));
}
setInitialConfig() {
return;
}
updateNavigation({ query, mustContain }: { readonly query: string; readonly mustContain?: string }) {
updateNavigation(query: string, mustContain?: string) {
const newQuery = query?.replace(mustContain, `"${mustContain}"`);
const queryParams = newQuery && newQuery !== '' ? { query: newQuery } : {};
const queryParamsHandling = this._dossierId ? 'merge' : '';
this._router
.navigate([], {
queryParams,
queryParamsHandling
})
.then();
this._router.navigate([], { queryParams }).then();
}
private _search(query: string): Observable<SearchResult> {
@ -112,6 +126,7 @@ export class SearchScreenComponent extends BaseListingComponent<ListItem> implem
}
private _updateValues({ query, dossierId }: { readonly query: string; readonly dossierId: string }) {
if (dossierId) this.filterService.toggleFilter('dossiers', dossierId);
this._dossierId = dossierId;
this.searchService.searchValue = query;
this.search$.next(query);
@ -121,34 +136,31 @@ export class SearchScreenComponent extends BaseListingComponent<ListItem> implem
return this._appStateService.getFileById(dossierId, fileId);
}
private _getDossierWrapper(dossierId: string) {
private _getDossierWrapper(dossierId: string): DossierWrapper {
return this._appStateService.getDossierById(dossierId);
}
private _toMatchedDocuments({ matchedDocuments }: SearchResult) {
private _toMatchedDocuments({ matchedDocuments }: SearchResult): MatchedDocument[] {
return matchedDocuments.filter(doc => doc.score > 0 && doc.matchedTerms.length > 0);
}
private _toListItems(matchedDocuments: MatchedDocument[]) {
return matchedDocuments
.map<ListItem>(document => {
const fileStatus = this._getFileWrapper(document.dossierId, document.fileId);
if (!fileStatus) {
return undefined;
}
private _toListItems(matchedDocuments: MatchedDocument[]): ListItem[] {
return matchedDocuments.map(document => this._toListItem(document)).filter(value => value);
}
const { dossierId, dossierName } = this._getDossierWrapper(document.dossierId);
return {
matchedDocument: document,
unmatched: document.unmatchedTerms.length ? document.unmatchedTerms : null,
highlights: document.highlights,
status: fileStatus.status,
pages: fileStatus.numberOfPages,
dossierName: dossierName,
fileName: fileStatus.filename,
routerLink: `/main/dossiers/${dossierId}/file/${fileStatus.fileId}`
} as ListItem;
})
.filter(value => value);
private _toListItem({ dossierId, fileId, unmatchedTerms, highlights }: MatchedDocument): ListItem {
const fileWrapper = this._getFileWrapper(dossierId, fileId);
if (!fileWrapper) return undefined;
return {
dossierId,
unmatched: unmatchedTerms || null,
highlights,
status,
numberOfPages: fileWrapper.numberOfPages,
dossierName: this._getDossierWrapper(dossierId).dossierName,
filename: fileWrapper.filename,
routerLink: `/main/dossiers/${dossierId}/file/${fileId}`
};
}
}

View File

@ -1,5 +1,5 @@
<redaction-icon-button
*ngIf="!chevron"
*ngIf="icon"
[icon]="icon"
[label]="filterLabel || ('filter-menu.label' | translate)"
[matMenuTriggerFor]="filterMenu"
@ -7,7 +7,7 @@
></redaction-icon-button>
<redaction-chevron-button
*ngIf="chevron"
*ngIf="!icon"
[label]="filterLabel || ('filter-menu.label' | translate)"
[matMenuTriggerFor]="filterMenu"
[showDot]="hasActiveFilters"

View File

@ -2,7 +2,6 @@ import { ChangeDetectorRef, Component, EventEmitter, Input, OnChanges, Output, T
import { FilterModel } from './model/filter.model';
import { handleCheckedValue } from './utils/filter-utils';
import { MAT_CHECKBOX_DEFAULT_OPTIONS } from '@angular/material/checkbox';
import { TranslateService } from '@ngx-translate/core';
@Component({
selector: 'redaction-popup-filter',
@ -29,12 +28,11 @@ export class PopupFilterComponent implements OnChanges {
@Input() secondaryFilters: FilterModel[] = [];
@Input() filterLabel;
@Input() icon: string;
@Input() chevron = false;
atLeastOneFilterIsExpandable = false;
atLeastOneSecondaryFilterIsExpandable = false;
constructor(private readonly _changeDetectorRef: ChangeDetectorRef, private readonly _translateService: TranslateService) {}
constructor(private readonly _changeDetectorRef: ChangeDetectorRef) {}
get hasActiveFilters(): boolean {
return !!this._allFilters.find(f => f.checked || f.indeterminate);

View File

@ -0,0 +1,6 @@
export const searchPositions = {
beforeFilters: 'beforeFilters',
afterFilters: 'afterFilters'
} as const;
export type SearchPosition = keyof typeof searchPositions;

View File

@ -1,8 +1,10 @@
<div class="page-header">
<div *ngIf="pageLabel" class="breadcrumb">{{ pageLabel }}</div>
<div *ngIf="filters$ | async as filters" [style.max-width]="computedWidth" [style.width]="computedWidth" class="filters">
<div *ngIf="filters.length" translate="filters.filter-by"></div>
<div *ngIf="filters$ | async as filters" class="filters">
<div *ngIf="filters.length && searchPosition !== searchPositions.beforeFilters" translate="filters.filter-by"></div>
<ng-container *ngIf="searchPosition === searchPositions.beforeFilters" [ngTemplateOutlet]="searchBar"></ng-container>
<ng-container *ngFor="let config of filters; trackBy: trackByLabel">
<redaction-popup-filter
@ -15,13 +17,7 @@
></redaction-popup-filter>
</ng-container>
<redaction-input-with-action
*ngIf="searchPlaceholder"
[form]="searchService.searchForm"
[placeholder]="searchPlaceholder"
[width]="searchWidth"
type="search"
></redaction-input-with-action>
<ng-container *ngIf="searchPosition === searchPositions.afterFilters" [ngTemplateOutlet]="searchBar"></ng-container>
<div (click)="resetFilters()" *ngIf="showResetFilters$ | async" class="reset-filters" translate="reset-filters"></div>
</div>
@ -60,3 +56,14 @@
></redaction-circle-button>
</div>
</div>
<ng-template #searchBar>
<redaction-input-with-action
*ngIf="searchPlaceholder && searchService"
[form]="searchService.searchForm"
[placeholder]="searchPlaceholder"
[width]="searchWidth"
[class.mr-8]="searchPosition === searchPositions.beforeFilters"
type="search"
></redaction-input-with-action>
</ng-template>

View File

@ -5,6 +5,7 @@ import { FilterService } from '@shared/services/filter.service';
import { SearchService } from '@shared/services/search.service';
import { distinctUntilChanged, map } from 'rxjs/operators';
import { combineLatest, Observable, of } from 'rxjs';
import { SearchPosition, searchPositions } from '@shared/components/page-header/models/search-positions.type';
@Component({
selector: 'redaction-page-header',
@ -12,12 +13,15 @@ import { combineLatest, Observable, of } from 'rxjs';
styleUrls: ['./page-header.component.scss']
})
export class PageHeaderComponent {
readonly searchPositions = searchPositions;
@Input() pageLabel: string;
@Input() showCloseButton: boolean;
@Input() actionConfigs: ActionConfig[];
@Input() buttonConfigs: ButtonConfig[];
@Input() searchPlaceholder: string;
@Input() searchWidth: number | 'full';
@Input() searchPosition: SearchPosition = this.searchPositions.afterFilters;
readonly filters$ = this.filterService?.filterGroups$.pipe(map(all => all.filter(f => f.icon)));
readonly showResetFilters$ = this._showResetFilters$;
@ -37,10 +41,6 @@ export class PageHeaderComponent {
);
}
get computedWidth() {
return this.searchWidth === 'full' ? '100%' : `${this.searchWidth}px`;
}
resetFilters(): void {
this.filterService.reset();
this.searchService.reset();

View File

@ -2,14 +2,17 @@ import { Injectable } from '@angular/core';
import { FilterModel } from '@shared/components/filters/popup-filter/model/filter.model';
import { processFilters } from '@shared/components/filters/popup-filter/utils/filter-utils';
import { FilterGroup } from '@shared/components/filters/popup-filter/model/filter-wrapper.model';
import { BehaviorSubject, Observable } from 'rxjs';
import { distinctUntilChanged, map, switchMap } from 'rxjs/operators';
import { BehaviorSubject, Observable, Subject } from 'rxjs';
import { distinctUntilChanged, map, startWith, switchMap } from 'rxjs/operators';
@Injectable()
export class FilterService {
private readonly _filterGroups$ = new BehaviorSubject<FilterGroup[]>([]);
private readonly _refresh$ = new BehaviorSubject(null);
readonly filterGroups$ = this._refresh$.pipe(switchMap(() => this._filterGroups$.asObservable()));
private readonly _refresh$ = new Subject();
readonly filterGroups$ = this._refresh$.pipe(
startWith(''),
switchMap(() => this._filterGroups$.asObservable())
);
readonly showResetFilters$ = this._showResetFilters$;
get filterGroups(): FilterGroup[] {
@ -25,7 +28,7 @@ export class FilterService {
}
refresh(): void {
this._refresh$.next(null);
this._refresh$.next();
}
toggleFilter(slug: string, key: string) {

View File

@ -1205,6 +1205,9 @@
"pages": "Pages",
"status": "Status"
},
"filters": {
"by-dossier": "Filter by Dossier"
},
"missing": "Missing",
"must-contain": "Must contain",
"no-data": "Please enter a keyword in the search input to look for documents or document content.",