mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-04-09 09:58:20 -05:00
Merge pull request #540 from paperless-ngx/filtering-query-params
Feature: Filtering query params aka browser navigation for filtering
This commit is contained in:
commit
ce5fe61e67
@ -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> <ng-container i18n>Documents</ng-container>
|
</svg> <ng-container i18n>Documents</ng-container>
|
||||||
|
@ -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(),
|
||||||
|
},
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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>
|
||||||
|
@ -0,0 +1,3 @@
|
|||||||
|
a {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
@ -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}}
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
@ -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>
|
||||||
|
@ -76,3 +76,7 @@
|
|||||||
text-align: left !important;
|
text-align: left !important;
|
||||||
font-size: 90%;
|
font-size: 90%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
@ -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>
|
||||||
|
@ -53,3 +53,7 @@ $paperless-card-breakpoints: (
|
|||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
@ -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
|
||||||
|
},
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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 {
|
||||||
|
@ -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) {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user