mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2026-06-23 22:04:19 +00:00
Fix (beta): move task filtering to backend fully (#12956)
This commit is contained in:
@@ -84,7 +84,7 @@
|
||||
<button class="btn btn-sm btn-outline-primary" ngbDropdownToggle>{{filterTargetName}}</button>
|
||||
<div class="dropdown-menu shadow" ngbDropdownMenu>
|
||||
@for (t of filterTargets; track t.id) {
|
||||
<button ngbDropdownItem [class.active]="filterTargetID === t.id" (click)="filterTargetID = t.id">{{t.name}}</button>
|
||||
<button ngbDropdownItem [class.active]="filterTargetID === t.id" (click)="setFilterTarget(t.id)">{{t.name}}</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -29,7 +29,11 @@ import { ToastService } from 'src/app/services/toast.service'
|
||||
import { environment } from 'src/environments/environment'
|
||||
import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component'
|
||||
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
|
||||
import { TasksComponent, TaskSection } from './tasks.component'
|
||||
import {
|
||||
TaskFilterTargetID,
|
||||
TasksComponent,
|
||||
TaskSection,
|
||||
} from './tasks.component'
|
||||
|
||||
const tasks: PaperlessTask[] = [
|
||||
{
|
||||
@@ -154,6 +158,13 @@ const paginatedTasks: Results<PaperlessTask> = {
|
||||
results: tasks,
|
||||
}
|
||||
|
||||
const sectionCountResponse = {
|
||||
all: 7,
|
||||
needs_attention: 2,
|
||||
in_progress: 3,
|
||||
completed: 2,
|
||||
}
|
||||
|
||||
describe('TasksComponent', () => {
|
||||
let component: TasksComponent
|
||||
let fixture: ComponentFixture<TasksComponent>
|
||||
@@ -221,6 +232,15 @@ describe('TasksComponent', () => {
|
||||
req.params.get('page') === '1'
|
||||
)
|
||||
.flush(paginatedTasks)
|
||||
|
||||
httpTestingController
|
||||
.expectOne(
|
||||
(req) =>
|
||||
req.url === `${environment.apiBaseUrl}tasks/status_counts/` &&
|
||||
req.params.get('acknowledged') === 'false' &&
|
||||
!req.params.has('status')
|
||||
)
|
||||
.flush(sectionCountResponse)
|
||||
})
|
||||
|
||||
it('should display task sections with counts', () => {
|
||||
@@ -328,6 +348,74 @@ describe('TasksComponent', () => {
|
||||
expect(pagination).not.toBeNull()
|
||||
})
|
||||
|
||||
it('should apply the selected section to the server-side task query', () => {
|
||||
component.setSection(TaskSection.NeedsAttention)
|
||||
|
||||
const req = httpTestingController.expectOne(
|
||||
(request) =>
|
||||
request.url === `${environment.apiBaseUrl}tasks/` &&
|
||||
request.params.get('page') === '1' &&
|
||||
request.params.get('page_size') === '25' &&
|
||||
request.params.get('acknowledged') === 'false' &&
|
||||
request.params.getAll('status').includes(PaperlessTaskStatus.Failure) &&
|
||||
request.params.getAll('status').includes(PaperlessTaskStatus.Revoked)
|
||||
)
|
||||
|
||||
req.flush({ count: 2, results: [tasks[0], tasks[1]] })
|
||||
expect(component.totalTasks).toBe(2)
|
||||
})
|
||||
|
||||
it('should apply task type and trigger source filters to the server-side task query', () => {
|
||||
component.setTaskType(PaperlessTaskType.SanityCheck)
|
||||
|
||||
httpTestingController
|
||||
.expectOne(
|
||||
(request) =>
|
||||
request.url === `${environment.apiBaseUrl}tasks/` &&
|
||||
request.params.get('page_size') === '25' &&
|
||||
request.params.get('task_type') === PaperlessTaskType.SanityCheck
|
||||
)
|
||||
.flush({ count: 1, results: [tasks[6]] })
|
||||
|
||||
component.setTriggerSource(PaperlessTaskTriggerSource.System)
|
||||
|
||||
httpTestingController
|
||||
.expectOne(
|
||||
(request) =>
|
||||
request.url === `${environment.apiBaseUrl}tasks/` &&
|
||||
request.params.get('page_size') === '25' &&
|
||||
request.params.get('task_type') === PaperlessTaskType.SanityCheck &&
|
||||
request.params.get('trigger_source') ===
|
||||
PaperlessTaskTriggerSource.System
|
||||
)
|
||||
.flush({ count: 1, results: [tasks[6]] })
|
||||
})
|
||||
|
||||
it('should apply text filters to the server-side task query', () => {
|
||||
component.filterText = 'invoice'
|
||||
jest.advanceTimersByTime(150)
|
||||
|
||||
httpTestingController
|
||||
.expectOne(
|
||||
(request) =>
|
||||
request.url === `${environment.apiBaseUrl}tasks/` &&
|
||||
request.params.get('page_size') === '25' &&
|
||||
request.params.get('name') === 'invoice'
|
||||
)
|
||||
.flush({ count: 1, results: [tasks[0]] })
|
||||
|
||||
component.setFilterTarget(TaskFilterTargetID.Result)
|
||||
|
||||
httpTestingController
|
||||
.expectOne(
|
||||
(request) =>
|
||||
request.url === `${environment.apiBaseUrl}tasks/` &&
|
||||
request.params.get('page_size') === '25' &&
|
||||
request.params.get('result') === 'invoice'
|
||||
)
|
||||
.flush({ count: 0, results: [] })
|
||||
})
|
||||
|
||||
it('should load a different task page when pagination changes', () => {
|
||||
component.setPage(2)
|
||||
|
||||
@@ -351,6 +439,27 @@ describe('TasksComponent', () => {
|
||||
expect(component.pagedTasks).toEqual([tasks[0]])
|
||||
})
|
||||
|
||||
it('should not replace section counts with current-page counts', () => {
|
||||
component.setPage(2)
|
||||
|
||||
httpTestingController
|
||||
.expectOne(
|
||||
(req) =>
|
||||
req.url === `${environment.apiBaseUrl}tasks/` &&
|
||||
req.params.get('acknowledged') === 'false' &&
|
||||
req.params.get('page_size') === '25' &&
|
||||
req.params.get('page') === '2'
|
||||
)
|
||||
.flush({
|
||||
count: 30,
|
||||
results: [tasks[0]],
|
||||
})
|
||||
|
||||
expect(component.sectionCount(TaskSection.NeedsAttention)).toBe(2)
|
||||
expect(component.sectionCount(TaskSection.InProgress)).toBe(3)
|
||||
expect(component.sectionCount(TaskSection.Completed)).toBe(2)
|
||||
})
|
||||
|
||||
it('should expose stable task type options and disable empty ones', () => {
|
||||
expect(component.taskTypeOptions.map((option) => option.value)).toContain(
|
||||
PaperlessTaskType.TrainClassifier
|
||||
@@ -714,6 +823,9 @@ describe('TasksComponent', () => {
|
||||
})
|
||||
|
||||
it('should keep clearing selection independent from resetting filters', () => {
|
||||
component.resetFilter()
|
||||
expect(component.filterText).toBe('')
|
||||
|
||||
component.setTaskType(PaperlessTaskType.ConsumeFile)
|
||||
component.toggleSelected(tasks[0])
|
||||
expect(component.selectedTasks.size).toBe(1)
|
||||
|
||||
@@ -40,7 +40,7 @@ export enum TaskSection {
|
||||
Completed = 'completed',
|
||||
}
|
||||
|
||||
enum TaskFilterTargetID {
|
||||
export enum TaskFilterTargetID {
|
||||
Name,
|
||||
Result,
|
||||
}
|
||||
@@ -167,6 +167,12 @@ export class TasksComponent
|
||||
public readonly pageSize = 25
|
||||
public page: number = 1
|
||||
public totalTasks: number = 0
|
||||
public sectionCounts: Record<TaskSection, number> = {
|
||||
[TaskSection.All]: 0,
|
||||
[TaskSection.NeedsAttention]: 0,
|
||||
[TaskSection.InProgress]: 0,
|
||||
[TaskSection.Completed]: 0,
|
||||
}
|
||||
public pagedTasks: PaperlessTask[] = []
|
||||
public selectedSection: TaskSection = TaskSection.All
|
||||
public selectedTaskType: PaperlessTaskType | null = null
|
||||
@@ -282,6 +288,7 @@ export class TasksComponent
|
||||
.subscribe((query) => {
|
||||
this._filterText = query
|
||||
this.clearSelection()
|
||||
this.reloadPage(true)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -470,9 +477,7 @@ export class TasksComponent
|
||||
}
|
||||
|
||||
sectionCount(section: TaskSection): number {
|
||||
return this.pagedTasks.filter((task) =>
|
||||
this.taskBelongsToSection(task, section)
|
||||
).length
|
||||
return this.sectionCounts[section]
|
||||
}
|
||||
|
||||
sectionShowsResults(section: TaskSection): boolean {
|
||||
@@ -482,16 +487,27 @@ export class TasksComponent
|
||||
setSection(section: TaskSection) {
|
||||
this.selectedSection = section
|
||||
this.clearSelection()
|
||||
this.reloadPage(true)
|
||||
}
|
||||
|
||||
setTaskType(taskType: PaperlessTaskType | null) {
|
||||
this.selectedTaskType = taskType
|
||||
this.clearSelection()
|
||||
this.reloadPage(true)
|
||||
}
|
||||
|
||||
setTriggerSource(triggerSource: PaperlessTaskTriggerSource | null) {
|
||||
this.selectedTriggerSource = triggerSource
|
||||
this.clearSelection()
|
||||
this.reloadPage(true)
|
||||
}
|
||||
|
||||
setFilterTarget(filterTargetID: TaskFilterTargetID) {
|
||||
this.filterTargetID = filterTargetID
|
||||
if (this._filterText.length) {
|
||||
this.clearSelection()
|
||||
this.reloadPage(true)
|
||||
}
|
||||
}
|
||||
|
||||
taskTypeOptionCount(taskType: PaperlessTaskType | null): number {
|
||||
@@ -529,19 +545,32 @@ export class TasksComponent
|
||||
}
|
||||
|
||||
public resetFilter() {
|
||||
if (!this._filterText.length) {
|
||||
return
|
||||
}
|
||||
|
||||
this._filterText = ''
|
||||
this.clearSelection()
|
||||
this.reloadPage(true)
|
||||
}
|
||||
|
||||
public resetFilters() {
|
||||
const hadFilter = this.isFiltered
|
||||
this.selectedTaskType = null
|
||||
this.selectedTriggerSource = null
|
||||
this.resetFilter()
|
||||
this._filterText = ''
|
||||
this.clearSelection()
|
||||
|
||||
if (hadFilter) {
|
||||
this.reloadPage(true)
|
||||
}
|
||||
}
|
||||
|
||||
filterInputKeyup(event: KeyboardEvent) {
|
||||
if (event.key == 'Enter') {
|
||||
this._filterText = (event.target as HTMLInputElement).value
|
||||
this.clearSelection()
|
||||
this.reloadPage(true)
|
||||
} else if (event.key === 'Escape') {
|
||||
this.resetFilter()
|
||||
}
|
||||
@@ -630,19 +659,86 @@ export class TasksComponent
|
||||
)
|
||||
}
|
||||
|
||||
private reloadSectionCounts() {
|
||||
this.tasksService
|
||||
.statusCounts(this.getParamsForSection(TaskSection.All))
|
||||
.pipe(first(), takeUntil(this.unsubscribeNotifier))
|
||||
.subscribe((counts) => {
|
||||
this.sectionCounts[TaskSection.All] = counts.all
|
||||
this.sectionCounts[TaskSection.NeedsAttention] = counts.needs_attention
|
||||
this.sectionCounts[TaskSection.InProgress] = counts.in_progress
|
||||
this.sectionCounts[TaskSection.Completed] = counts.completed
|
||||
})
|
||||
}
|
||||
|
||||
private getParamsForSection(
|
||||
section: TaskSection
|
||||
): Record<string, string | number | boolean | readonly string[]> {
|
||||
const params: Record<
|
||||
string,
|
||||
string | number | boolean | readonly string[]
|
||||
> = {
|
||||
acknowledged: false,
|
||||
}
|
||||
|
||||
const statuses = this.statusesForSection(section)
|
||||
if (statuses.length) {
|
||||
params.status = statuses
|
||||
}
|
||||
|
||||
if (this.selectedTaskType !== null) {
|
||||
params.task_type = this.selectedTaskType
|
||||
}
|
||||
|
||||
if (this.selectedTriggerSource !== null) {
|
||||
params.trigger_source = this.selectedTriggerSource
|
||||
}
|
||||
|
||||
if (this._filterText.length) {
|
||||
params[
|
||||
this.filterTargetID === TaskFilterTargetID.Name ? 'name' : 'result'
|
||||
] = this._filterText
|
||||
}
|
||||
|
||||
return params
|
||||
}
|
||||
|
||||
private statusesForSection(section: TaskSection): PaperlessTaskStatus[] {
|
||||
switch (section) {
|
||||
case TaskSection.NeedsAttention:
|
||||
return [PaperlessTaskStatus.Failure, PaperlessTaskStatus.Revoked]
|
||||
case TaskSection.InProgress:
|
||||
return [PaperlessTaskStatus.Pending, PaperlessTaskStatus.Started]
|
||||
case TaskSection.Completed:
|
||||
return [PaperlessTaskStatus.Success]
|
||||
default:
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
private reloadPage(resetToFirstPage: boolean = false) {
|
||||
if (resetToFirstPage) {
|
||||
this.page = 1
|
||||
}
|
||||
|
||||
this.reloadSectionCounts()
|
||||
|
||||
this.loading = true
|
||||
this.tasksService
|
||||
.list(this.page, this.pageSize, { acknowledged: false })
|
||||
.list(
|
||||
this.page,
|
||||
this.pageSize,
|
||||
this.getParamsForSection(this.selectedSection)
|
||||
)
|
||||
.pipe(first(), takeUntil(this.unsubscribeNotifier))
|
||||
.subscribe({
|
||||
next: (result) => {
|
||||
this.pagedTasks = result.results
|
||||
this.totalTasks = result.count
|
||||
this.sectionCounts[TaskSection.All] = result.count
|
||||
if (this.selectedSection !== TaskSection.All) {
|
||||
this.sectionCounts[this.selectedSection] = result.count
|
||||
}
|
||||
this.loading = false
|
||||
if (
|
||||
this.page > 1 &&
|
||||
|
||||
Reference in New Issue
Block a user