Merge pull request #540 from paperless-ngx/filtering-query-params

Feature: Filtering query params aka browser navigation for filtering
This commit is contained in:
shamoon 2022-03-28 00:07:46 -07:00 committed by GitHub
commit ce5fe61e67
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 159 additions and 67 deletions

View File

@ -62,7 +62,7 @@
</a> </a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" routerLink="documents" routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}" (click)="closeMenu()"> <a class="nav-link" routerLink="documents" routerLinkActive="active" (click)="closeMenu()">
<svg class="sidebaricon" fill="currentColor"> <svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#files"/> <use xlink:href="assets/bootstrap-icons.svg#files"/>
</svg>&nbsp;<ng-container i18n>Documents</ng-container> </svg>&nbsp;<ng-container i18n>Documents</ng-container>

View File

@ -81,7 +81,10 @@ export class AppFrameComponent {
search() { search() {
this.closeMenu() this.closeMenu()
this.list.quickFilter([ this.list.quickFilter([
{ rule_type: FILTER_FULLTEXT_QUERY, value: this.searchField.value }, {
rule_type: FILTER_FULLTEXT_QUERY,
value: (this.searchField.value as string).trim(),
},
]) ])
} }

View File

@ -1,2 +1,2 @@
<span *ngIf="!clickable" class="badge" [style.background]="tag.color" [style.color]="tag.text_color">{{tag.name}}</span> <span *ngIf="!clickable" class="badge" [style.background]="tag.color" [style.color]="tag.text_color">{{tag.name}}</span>
<a [routerLink]="[]" [title]="linkTitle" *ngIf="clickable" class="badge" [style.background]="tag.color" [style.color]="tag.text_color">{{tag.name}}</a> <a [title]="linkTitle" *ngIf="clickable" class="badge" [style.background]="tag.color" [style.color]="tag.text_color">{{tag.name}}</a>

View File

@ -0,0 +1,3 @@
a {
cursor: pointer;
}

View File

@ -17,7 +17,7 @@
<div class="d-flex justify-content-between align-items-center"> <div class="d-flex justify-content-between align-items-center">
<h5 class="card-title"> <h5 class="card-title">
<ng-container *ngIf="document.correspondent"> <ng-container *ngIf="document.correspondent">
<a *ngIf="clickCorrespondent.observers.length ; else nolink" [routerLink]="[]" title="Filter by correspondent" i18n-title (click)="clickCorrespondent.emit(document.correspondent);$event.stopPropagation()" class="fw-bold btn-link">{{(document.correspondent$ | async)?.name}}</a> <a *ngIf="clickCorrespondent.observers.length ; else nolink" title="Filter by correspondent" i18n-title (click)="clickCorrespondent.emit(document.correspondent);$event.stopPropagation()" class="fw-bold btn-link">{{(document.correspondent$ | async)?.name}}</a>
<ng-template #nolink>{{(document.correspondent$ | async)?.name}}</ng-template>: <ng-template #nolink>{{(document.correspondent$ | async)?.name}}</ng-template>:
</ng-container> </ng-container>
{{document.title | documentTitle}} {{document.title | documentTitle}}

View File

@ -90,3 +90,7 @@ span ::ng-deep .match {
color: black; color: black;
background-color: rgb(255, 211, 66); background-color: rgb(255, 211, 66);
} }
a {
cursor: pointer;
}

View File

@ -23,7 +23,7 @@
<div class="card-body p-2"> <div class="card-body p-2">
<p class="card-text"> <p class="card-text">
<ng-container *ngIf="document.correspondent"> <ng-container *ngIf="document.correspondent">
<a [routerLink]="[]" title="Filter by correspondent" i18n-title (click)="clickCorrespondent.emit(document.correspondent);$event.stopPropagation()" class="fw-bold btn-link">{{(document.correspondent$ | async)?.name}}</a>: <a title="Filter by correspondent" i18n-title (click)="clickCorrespondent.emit(document.correspondent);$event.stopPropagation()" class="fw-bold btn-link">{{(document.correspondent$ | async)?.name}}</a>:
</ng-container> </ng-container>
{{document.title | documentTitle}} {{document.title | documentTitle}}
</p> </p>

View File

@ -76,3 +76,7 @@
text-align: left !important; text-align: left !important;
font-size: 90%; font-size: 90%;
} }
a {
cursor: pointer;
}

View File

@ -163,7 +163,7 @@
</td> </td>
<td class="d-none d-md-table-cell"> <td class="d-none d-md-table-cell">
<ng-container *ngIf="d.correspondent"> <ng-container *ngIf="d.correspondent">
<a [routerLink]="[]" (click)="clickCorrespondent(d.correspondent);$event.stopPropagation()" title="Filter by correspondent">{{(d.correspondent$ | async)?.name}}</a> <a (click)="clickCorrespondent(d.correspondent);$event.stopPropagation()" title="Filter by correspondent">{{(d.correspondent$ | async)?.name}}</a>
</ng-container> </ng-container>
</td> </td>
<td> <td>
@ -172,7 +172,7 @@
</td> </td>
<td class="d-none d-xl-table-cell"> <td class="d-none d-xl-table-cell">
<ng-container *ngIf="d.document_type"> <ng-container *ngIf="d.document_type">
<a [routerLink]="[]" (click)="clickDocumentType(d.document_type);$event.stopPropagation()" title="Filter by document type">{{(d.document_type$ | async)?.name}}</a> <a (click)="clickDocumentType(d.document_type);$event.stopPropagation()" title="Filter by document type">{{(d.document_type$ | async)?.name}}</a>
</ng-container> </ng-container>
</td> </td>
<td> <td>

View File

@ -53,3 +53,7 @@ $paperless-card-breakpoints: (
margin-left: 0; margin-left: 0;
} }
} }
a {
cursor: pointer;
}

View File

@ -1,4 +1,5 @@
import { import {
AfterViewInit,
Component, Component,
OnDestroy, OnDestroy,
OnInit, OnInit,
@ -8,9 +9,20 @@ import {
} from '@angular/core' } from '@angular/core'
import { ActivatedRoute, Router } from '@angular/router' import { ActivatedRoute, Router } from '@angular/router'
import { NgbModal } from '@ng-bootstrap/ng-bootstrap' import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
import { Subscription } from 'rxjs' import {
filter,
first,
map,
Subject,
Subscription,
switchMap,
takeUntil,
} from 'rxjs'
import { FilterRule, isFullTextFilterRule } from 'src/app/data/filter-rule' import { FilterRule, isFullTextFilterRule } from 'src/app/data/filter-rule'
import { FILTER_FULLTEXT_MORELIKE } from 'src/app/data/filter-rule-type' import {
FILTER_FULLTEXT_MORELIKE,
FILTER_RULE_TYPES,
} from 'src/app/data/filter-rule-type'
import { PaperlessDocument } from 'src/app/data/paperless-document' import { PaperlessDocument } from 'src/app/data/paperless-document'
import { PaperlessSavedView } from 'src/app/data/paperless-saved-view' import { PaperlessSavedView } from 'src/app/data/paperless-saved-view'
import { import {
@ -20,6 +32,7 @@ import {
import { ConsumerStatusService } from 'src/app/services/consumer-status.service' import { ConsumerStatusService } from 'src/app/services/consumer-status.service'
import { DocumentListViewService } from 'src/app/services/document-list-view.service' import { DocumentListViewService } from 'src/app/services/document-list-view.service'
import { import {
DocumentService,
DOCUMENT_SORT_FIELDS, DOCUMENT_SORT_FIELDS,
DOCUMENT_SORT_FIELDS_FULLTEXT, DOCUMENT_SORT_FIELDS_FULLTEXT,
} from 'src/app/services/rest/document.service' } from 'src/app/services/rest/document.service'
@ -33,9 +46,10 @@ import { SaveViewConfigDialogComponent } from './save-view-config-dialog/save-vi
templateUrl: './document-list.component.html', templateUrl: './document-list.component.html',
styleUrls: ['./document-list.component.scss'], styleUrls: ['./document-list.component.scss'],
}) })
export class DocumentListComponent implements OnInit, OnDestroy { export class DocumentListComponent implements OnInit, OnDestroy, AfterViewInit {
constructor( constructor(
public list: DocumentListViewService, public list: DocumentListViewService,
private documentService: DocumentService,
public savedViewService: SavedViewService, public savedViewService: SavedViewService,
public route: ActivatedRoute, public route: ActivatedRoute,
private router: Router, private router: Router,
@ -53,7 +67,7 @@ export class DocumentListComponent implements OnInit, OnDestroy {
unmodifiedFilterRules: FilterRule[] = [] unmodifiedFilterRules: FilterRule[] = []
private consumptionFinishedSubscription: Subscription private unsubscribeNotifier: Subject<any> = new Subject()
get isFiltered() { get isFiltered() {
return this.list.filterRules?.length > 0 return this.list.filterRules?.length > 0
@ -85,34 +99,97 @@ export class DocumentListComponent implements OnInit, OnDestroy {
if (localStorage.getItem('document-list:displayMode') != null) { if (localStorage.getItem('document-list:displayMode') != null) {
this.displayMode = localStorage.getItem('document-list:displayMode') this.displayMode = localStorage.getItem('document-list:displayMode')
} }
this.consumptionFinishedSubscription = this.consumerStatusService
this.consumerStatusService
.onDocumentConsumptionFinished() .onDocumentConsumptionFinished()
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe(() => { .subscribe(() => {
this.list.reload() this.list.reload()
}) })
this.route.paramMap.subscribe((params) => {
if (params.has('id')) { this.route.paramMap
this.savedViewService.getCached(+params.get('id')).subscribe((view) => { .pipe(
if (!view) { filter((params) => params.has('id')), // only on saved view
this.router.navigate(['404']) switchMap((params) => {
return return this.savedViewService
} .getCached(+params.get('id'))
this.list.activateSavedView(view) .pipe(map((view) => ({ params, view })))
this.list.reload()
this.unmodifiedFilterRules = view.filter_rules
}) })
} else { )
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe(({ view, params }) => {
if (!view) {
this.router.navigate(['404'])
return
}
this.list.activateSavedView(view)
this.list.reload()
this.unmodifiedFilterRules = view.filter_rules
})
const allFilterRuleQueryParams: string[] = FILTER_RULE_TYPES.map(
(rt) => rt.filtervar
)
this.route.queryParamMap
.pipe(
filter(() => !this.route.snapshot.paramMap.has('id')), // only when not on saved view
takeUntil(this.unsubscribeNotifier)
)
.subscribe((queryParams) => {
// transform query params to filter rules
let filterRulesFromQueryParams: FilterRule[] = []
allFilterRuleQueryParams
.filter((frqp) => queryParams.has(frqp))
.forEach((filterQueryParamName) => {
const filterQueryParamValues: string[] = queryParams
.get(filterQueryParamName)
.split(',')
filterRulesFromQueryParams = filterRulesFromQueryParams.concat(
// map all values to filter rules
filterQueryParamValues.map((val) => {
return {
rule_type: FILTER_RULE_TYPES.find(
(rt) => rt.filtervar == filterQueryParamName
).id,
value: val,
}
})
)
})
this.list.activateSavedView(null) this.list.activateSavedView(null)
this.list.filterRules = filterRulesFromQueryParams
this.list.reload() this.list.reload()
this.unmodifiedFilterRules = [] this.unmodifiedFilterRules = []
} })
}) }
ngAfterViewInit(): void {
this.filterEditor.filterRulesChange
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe({
next: (filterRules) => {
const params =
this.documentService.filterRulesToQueryParams(filterRules)
// if we were on a saved view we navigate 'away' to /documents
let base = []
if (this.route.snapshot.paramMap.has('id')) base = ['/documents']
this.router.navigate(base, {
relativeTo: this.route,
queryParams: params,
})
},
})
} }
ngOnDestroy() { ngOnDestroy() {
if (this.consumptionFinishedSubscription) { // unsubscribes all
this.consumptionFinishedSubscription.unsubscribe() this.unsubscribeNotifier.next(this)
} this.unsubscribeNotifier.complete()
} }
loadViewConfig(view: PaperlessSavedView) { loadViewConfig(view: PaperlessSavedView) {
@ -128,12 +205,15 @@ export class DocumentListComponent implements OnInit, OnDestroy {
sort_field: this.list.sortField, sort_field: this.list.sortField,
sort_reverse: this.list.sortReverse, sort_reverse: this.list.sortReverse,
} }
this.savedViewService.patch(savedView).subscribe((result) => { this.savedViewService
this.toastService.showInfo( .patch(savedView)
$localize`View "${this.list.activeSavedViewTitle}" saved successfully.` .pipe(first())
) .subscribe((result) => {
this.unmodifiedFilterRules = this.list.filterRules this.toastService.showInfo(
}) $localize`View "${this.list.activeSavedViewTitle}" saved successfully.`
)
this.unmodifiedFilterRules = this.list.filterRules
})
} }
} }
@ -142,7 +222,7 @@ export class DocumentListComponent implements OnInit, OnDestroy {
backdrop: 'static', backdrop: 'static',
}) })
modal.componentInstance.defaultName = this.filterEditor.generateFilterName() modal.componentInstance.defaultName = this.filterEditor.generateFilterName()
modal.componentInstance.saveClicked.subscribe((formValue) => { modal.componentInstance.saveClicked.pipe(first()).subscribe((formValue) => {
modal.componentInstance.buttonsEnabled = false modal.componentInstance.buttonsEnabled = false
let savedView: PaperlessSavedView = { let savedView: PaperlessSavedView = {
name: formValue.name, name: formValue.name,
@ -153,18 +233,21 @@ export class DocumentListComponent implements OnInit, OnDestroy {
sort_field: this.list.sortField, sort_field: this.list.sortField,
} }
this.savedViewService.create(savedView).subscribe( this.savedViewService
() => { .create(savedView)
modal.close() .pipe(first())
this.toastService.showInfo( .subscribe({
$localize`View "${savedView.name}" created successfully.` next: () => {
) modal.close()
}, this.toastService.showInfo(
(error) => { $localize`View "${savedView.name}" created successfully.`
modal.componentInstance.error = error.error )
modal.componentInstance.buttonsEnabled = true },
} error: (error) => {
) modal.componentInstance.error = error.error
modal.componentInstance.buttonsEnabled = true
},
})
}) })
} }

View File

@ -143,8 +143,8 @@ export class DocumentListViewService {
activeListViewState.sortReverse, activeListViewState.sortReverse,
activeListViewState.filterRules activeListViewState.filterRules
) )
.subscribe( .subscribe({
(result) => { next: (result) => {
this.isReloading = false this.isReloading = false
activeListViewState.collectionSize = result.count activeListViewState.collectionSize = result.count
activeListViewState.documents = result.results activeListViewState.documents = result.results
@ -153,7 +153,7 @@ export class DocumentListViewService {
} }
this.rangeSelectionAnchorIndex = this.lastRangeSelectionToIndex = null this.rangeSelectionAnchorIndex = this.lastRangeSelectionToIndex = null
}, },
(error) => { error: (error) => {
this.isReloading = false this.isReloading = false
if (activeListViewState.currentPage != 1 && error.status == 404) { if (activeListViewState.currentPage != 1 && error.status == 404) {
// this happens when applying a filter: the current page might not be available anymore due to the reduced result set. // this happens when applying a filter: the current page might not be available anymore due to the reduced result set.
@ -162,8 +162,8 @@ export class DocumentListViewService {
} else { } else {
this.error = error.error this.error = error.error
} }
} },
) })
} }
set filterRules(filterRules: FilterRule[]) { set filterRules(filterRules: FilterRule[]) {
@ -249,20 +249,11 @@ export class DocumentListViewService {
} }
quickFilter(filterRules: FilterRule[]) { quickFilter(filterRules: FilterRule[]) {
this._activeSavedViewId = null const params = this.documentService.filterRulesToQueryParams(filterRules)
this.activeListViewState.filterRules = filterRules this.router.navigate(['/documents'], {
this.activeListViewState.currentPage = 1 relativeTo: this.route,
if (isFullTextFilterRule(filterRules)) { queryParams: params,
this.activeListViewState.sortField = 'score' })
this.activeListViewState.sortReverse = false
}
this.reduceSelectionToFilter()
this.saveDocumentListView()
if (this.router.url == '/documents') {
this.reload()
} else {
this.router.navigate(['documents'])
}
} }
getLastPage(): number { getLastPage(): number {

View File

@ -57,7 +57,7 @@ export class DocumentService extends AbstractPaperlessService<PaperlessDocument>
super(http, 'documents') super(http, 'documents')
} }
private filterRulesToQueryParams(filterRules: FilterRule[]) { public filterRulesToQueryParams(filterRules: FilterRule[]): Object {
if (filterRules) { if (filterRules) {
let params = {} let params = {}
for (let rule of filterRules) { for (let rule of filterRules) {