filter search results by dossier
This commit is contained in:
parent
e50e5cbfb2
commit
c61e6e1aa5
@ -9,7 +9,6 @@
|
||||
<redaction-popup-filter
|
||||
(filtersChanged)="filtersChanged($event)"
|
||||
[actionsTemplate]="annotationFilterActionTemplate"
|
||||
[chevron]="true"
|
||||
[filterTemplate]="annotationFilterTemplate"
|
||||
[primaryFilters]="primaryFilters"
|
||||
[secondaryFilters]="secondaryFilters"
|
||||
|
||||
@ -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 @@
|
||||
> <s>{{ term }}</s></span
|
||||
>. {{ '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"
|
||||
> <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>
|
||||
|
||||
@ -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}`
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -0,0 +1,6 @@
|
||||
export const searchPositions = {
|
||||
beforeFilters: 'beforeFilters',
|
||||
afterFilters: 'afterFilters'
|
||||
} as const;
|
||||
|
||||
export type SearchPosition = keyof typeof searchPositions;
|
||||
@ -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>
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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.",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user