<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
<context context-type="linenumber">36</context>
</context-group>
+ <context-group purpose="location">
+ <context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.ts</context>
+ <context context-type="linenumber">30</context>
+ </context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/trash/trash.component.html</context>
<context context-type="linenumber">35</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.ts</context>
- <context context-type="linenumber">68</context>
+ <context context-type="linenumber">124</context>
</context-group>
</trans-unit>
<trans-unit id="2134950584701094962" datatype="html">
<context context-type="linenumber">147,149</context>
</context-group>
</trans-unit>
+ <trans-unit id="4880728824338713664" datatype="html">
+ <source>Filter by</source>
+ <context-group purpose="location">
+ <context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
+ <context context-type="linenumber">157</context>
+ </context-group>
+ </trans-unit>
+ <trans-unit id="2525230676386818985" datatype="html">
+ <source>Result</source>
+ <context-group purpose="location">
+ <context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.ts</context>
+ <context context-type="linenumber">31</context>
+ </context-group>
+ </trans-unit>
<trans-unit id="5404910960991552159" datatype="html">
<source>Dismiss selected</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.ts</context>
- <context context-type="linenumber">31</context>
+ <context context-type="linenumber">77</context>
</context-group>
</trans-unit>
<trans-unit id="8829078752502782653" datatype="html">
<source>Dismiss all</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.ts</context>
- <context context-type="linenumber">32</context>
+ <context context-type="linenumber">78</context>
</context-group>
</trans-unit>
<trans-unit id="1323591410517879795" datatype="html">
<source>Confirm Dismiss All</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.ts</context>
- <context context-type="linenumber">65</context>
+ <context context-type="linenumber">121</context>
</context-group>
</trans-unit>
<trans-unit id="4157200209636243740" datatype="html">
<source>Dismiss all <x id="PH" equiv-text="tasks.size"/> tasks?</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.ts</context>
- <context context-type="linenumber">66</context>
+ <context context-type="linenumber">122</context>
</context-group>
</trans-unit>
<trans-unit id="9011556615675272238" datatype="html">
<source>queued</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.ts</context>
- <context context-type="linenumber">135</context>
+ <context context-type="linenumber">207</context>
</context-group>
</trans-unit>
<trans-unit id="6415892379431855826" datatype="html">
<source>started</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.ts</context>
- <context context-type="linenumber">137</context>
+ <context context-type="linenumber">209</context>
</context-group>
</trans-unit>
<trans-unit id="7510279840486540181" datatype="html">
<source>completed</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.ts</context>
- <context context-type="linenumber">139</context>
+ <context context-type="linenumber">211</context>
</context-group>
</trans-unit>
<trans-unit id="4083337005045748464" datatype="html">
<source>failed</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.ts</context>
- <context context-type="linenumber">141</context>
+ <context context-type="linenumber">213</context>
</context-group>
</trans-unit>
<trans-unit id="3418677553313974490" datatype="html">
</div>
</ng-template>
-<ul ngbNav #nav="ngbNav" [(activeId)]="activeTab" class="nav-tabs" (hidden)="duringTabChange($event)">
+<ul ngbNav #nav="ngbNav" [(activeId)]="activeTab" class="nav-tabs" (hidden)="duringTabChange()" (navChange)="beforeTabChange()">
<li ngbNavItem="failed">
<a ngbNavLink i18n>Failed@if (tasksService.failedFileTasks.length > 0) {
<span class="badge bg-danger ms-2">{{tasksService.failedFileTasks.length}}</span>
}</a>
<ng-template ngbNavContent>
- <ng-container [ngTemplateOutlet]="tasksTemplate" [ngTemplateOutletContext]="{tasks:tasksService.failedFileTasks}"></ng-container>
+ <ng-container [ngTemplateOutlet]="tasksTemplate" [ngTemplateOutletContext]="{tasks:currentTasks}"></ng-container>
</ng-template>
</li>
<li ngbNavItem="completed">
<span class="badge bg-secondary ms-2">{{tasksService.completedFileTasks.length}}</span>
}</a>
<ng-template ngbNavContent>
- <ng-container [ngTemplateOutlet]="tasksTemplate" [ngTemplateOutletContext]="{tasks:tasksService.completedFileTasks}"></ng-container>
+ <ng-container [ngTemplateOutlet]="tasksTemplate" [ngTemplateOutletContext]="{tasks:currentTasks}"></ng-container>
</ng-template>
</li>
<li ngbNavItem="started">
<span class="badge bg-secondary ms-2">{{tasksService.startedFileTasks.length}}</span>
}</a>
<ng-template ngbNavContent>
- <ng-container [ngTemplateOutlet]="tasksTemplate" [ngTemplateOutletContext]="{tasks:tasksService.startedFileTasks}"></ng-container>
+ <ng-container [ngTemplateOutlet]="tasksTemplate" [ngTemplateOutletContext]="{tasks:currentTasks}"></ng-container>
</ng-template>
</li>
<li ngbNavItem="queued">
<span class="badge bg-secondary ms-2">{{tasksService.queuedFileTasks.length}}</span>
}</a>
<ng-template ngbNavContent>
- <ng-container [ngTemplateOutlet]="tasksTemplate" [ngTemplateOutletContext]="{tasks:tasksService.queuedFileTasks}"></ng-container>
+ <ng-container [ngTemplateOutlet]="tasksTemplate" [ngTemplateOutletContext]="{tasks:currentTasks}"></ng-container>
</ng-template>
</li>
+ <li class="ms-auto">
+ <div class="form-inline d-flex align-items-center">
+ <div class="input-group input-group-sm flex-fill w-auto flex-nowrap">
+ <span class="input-group-text text-muted" i18n>Filter by</span>
+ @if (filterTargets.length > 1) {
+ <div ngbDropdown>
+ <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>
+ }
+ </div>
+ </div>
+ } @else {
+ <span class="input-group-text">{{filterTargetName}}</span>
+ }
+ @if (filterText?.length) {
+ <button class="btn btn-link btn-sm px-2 position-absolute top-0 end-0 z-10" (click)="resetFilter()">
+ <i-bs width="1em" height="1em" name="x"></i-bs>
+ </button>
+ }
+ <input #filterInput class="form-control form-control-sm" type="text"
+ (keyup)="filterInputKeyup($event)"
+ [(ngModel)]="filterText">
+ </div>
+ </div>
+ </li>
</ul>
<div [ngbNavOutlet]="nav"></div>
max-width: 150px;
}
}
+
+.input-group .dropdown .btn {
+ border-top-right-radius: 0;
+ border-bottom-right-radius: 0;
+ border-top-left-radius: 0;
+ border-bottom-left-radius: 0;
+}
+
+.z-10 {
+ z-index: 10;
+}
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 } from './tasks.component'
+import { TasksComponent, TaskTab } from './tasks.component'
import { PermissionsGuard } from 'src/app/guards/permissions.guard'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { FormsModule } from '@angular/forms'
let currentTasksLength = tasks.filter(
(t) => t.status === PaperlessTaskStatus.Failed
).length
- component.activeTab = 'failed'
+ component.activeTab = TaskTab.Failed
fixture.detectChanges()
expect(tabButtons[0].nativeElement.textContent).toEqual(
`Failed${currentTasksLength}`
currentTasksLength = tasks.filter(
(t) => t.status === PaperlessTaskStatus.Complete
).length
- component.activeTab = 'completed'
+ component.activeTab = TaskTab.Completed
fixture.detectChanges()
expect(tabButtons[1].nativeElement.textContent).toEqual(
`Complete${currentTasksLength}`
currentTasksLength = tasks.filter(
(t) => t.status === PaperlessTaskStatus.Started
).length
- component.activeTab = 'started'
+ component.activeTab = TaskTab.Started
fixture.detectChanges()
expect(tabButtons[2].nativeElement.textContent).toEqual(
`Started${currentTasksLength}`
currentTasksLength = tasks.filter(
(t) => t.status === PaperlessTaskStatus.Pending
).length
- component.activeTab = 'queued'
+ component.activeTab = TaskTab.Queued
fixture.detectChanges()
expect(tabButtons[3].nativeElement.textContent).toEqual(
`Queued${currentTasksLength}`
it('should to go page 1 between tab switch', () => {
component.page = 10
- component.duringTabChange(2)
+ component.duringTabChange()
expect(component.page).toEqual(1)
})
jest.advanceTimersByTime(6000)
expect(reloadSpy).toHaveBeenCalledTimes(2)
})
+
+ it('should filter tasks by file name', () => {
+ const input = fixture.debugElement.query(By.css('ul input[type=text]'))
+ input.nativeElement.value = '191092'
+ input.nativeElement.dispatchEvent(new Event('input'))
+ jest.advanceTimersByTime(150) // debounce time
+ fixture.detectChanges()
+ expect(component.filterText).toEqual('191092')
+ expect(
+ fixture.debugElement.queryAll(By.css('table tbody tr')).length
+ ).toEqual(2) // 1 task x 2 lines
+ })
+
+ it('should filter tasks by result', () => {
+ component.activeTab = TaskTab.Failed
+ fixture.detectChanges()
+ component.filterTargetID = 1
+ const input = fixture.debugElement.query(By.css('ul input[type=text]'))
+ input.nativeElement.value = 'duplicate'
+ input.nativeElement.dispatchEvent(new Event('input'))
+ jest.advanceTimersByTime(150) // debounce time
+ fixture.detectChanges()
+ expect(component.filterText).toEqual('duplicate')
+ expect(
+ fixture.debugElement.queryAll(By.css('table tbody tr')).length
+ ).toEqual(4) // 2 tasks x 2 lines
+ })
+
+ it('should support keyboard events for filtering', () => {
+ const input = fixture.debugElement.query(By.css('ul input[type=text]'))
+ input.nativeElement.value = '191092'
+ input.nativeElement.dispatchEvent(
+ new KeyboardEvent('keyup', { key: 'Enter' })
+ )
+ expect(component.filterText).toEqual('191092') // no debounce needed
+ input.nativeElement.dispatchEvent(
+ new KeyboardEvent('keyup', { key: 'Escape' })
+ )
+ expect(component.filterText).toEqual('')
+ })
+
+ it('should reset filter and target on tab switch', () => {
+ component.filterText = '191092'
+ component.filterTargetID = 1
+ component.activeTab = TaskTab.Completed
+ component.beforeTabChange()
+ expect(component.filterText).toEqual('')
+ expect(component.filterTargetID).toEqual(0)
+ })
})
import { Component, OnInit, OnDestroy } from '@angular/core'
import { Router } from '@angular/router'
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
-import { first } from 'rxjs'
+import {
+ debounceTime,
+ distinctUntilChanged,
+ filter,
+ first,
+ Subject,
+ takeUntil,
+} from 'rxjs'
import { PaperlessTask } from 'src/app/data/paperless-task'
import { TasksService } from 'src/app/services/tasks.service'
import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component'
import { ComponentWithPermissions } from '../../with-permissions/with-permissions.component'
+export enum TaskTab {
+ Queued = 'queued',
+ Started = 'started',
+ Completed = 'completed',
+ Failed = 'failed',
+}
+
+enum TaskFilterTargetID {
+ Name,
+ Result,
+}
+
+const FILTER_TARGETS = [
+ { id: TaskFilterTargetID.Name, name: $localize`Name` },
+ { id: TaskFilterTargetID.Result, name: $localize`Result` },
+]
+
@Component({
selector: 'pngx-tasks',
templateUrl: './tasks.component.html',
extends ComponentWithPermissions
implements OnInit, OnDestroy
{
- public activeTab: string
+ public activeTab: TaskTab
public selectedTasks: Set<number> = new Set()
public togggleAll: boolean = false
public expandedTask: number
public autoRefreshInterval: any
+ private _filterText: string = ''
+ get filterText() {
+ return this._filterText
+ }
+ set filterText(value: string) {
+ this.filterDebounce.next(value)
+ }
+
+ public filterTargetID: TaskFilterTargetID = TaskFilterTargetID.Name
+ public get filterTargetName(): string {
+ return this.filterTargets.find((t) => t.id == this.filterTargetID).name
+ }
+ private filterDebounce: Subject<string> = new Subject<string>()
+
+ public get filterTargets(): Array<{ id: number; name: string }> {
+ return [TaskTab.Failed, TaskTab.Completed].includes(this.activeTab)
+ ? FILTER_TARGETS
+ : FILTER_TARGETS.slice(0, 1)
+ }
+
+ private unsubscribeNotifier: Subject<any> = new Subject()
+
get dismissButtonText(): string {
return this.selectedTasks.size > 0
? $localize`Dismiss selected`
ngOnInit() {
this.tasksService.reload()
this.toggleAutoRefresh()
+
+ this.filterDebounce
+ .pipe(
+ takeUntil(this.unsubscribeNotifier),
+ debounceTime(100),
+ distinctUntilChanged(),
+ filter((query) => !query.length || query.length > 2)
+ )
+ .subscribe((query) => (this._filterText = query))
}
ngOnDestroy() {
this.tasksService.cancelPending()
clearInterval(this.autoRefreshInterval)
+ this.unsubscribeNotifier.next(this)
}
dismissTask(task: PaperlessTask) {
get currentTasks(): PaperlessTask[] {
let tasks: PaperlessTask[] = []
switch (this.activeTab) {
- case 'queued':
+ case TaskTab.Queued:
tasks = this.tasksService.queuedFileTasks
break
- case 'started':
+ case TaskTab.Started:
tasks = this.tasksService.startedFileTasks
break
- case 'completed':
+ case TaskTab.Completed:
tasks = this.tasksService.completedFileTasks
break
- case 'failed':
+ case TaskTab.Failed:
tasks = this.tasksService.failedFileTasks
break
}
+ if (this._filterText.length) {
+ tasks = tasks.filter((t) => {
+ if (this.filterTargetID == TaskFilterTargetID.Name) {
+ return t.task_file_name
+ .toLowerCase()
+ .includes(this._filterText.toLowerCase())
+ } else if (this.filterTargetID == TaskFilterTargetID.Result) {
+ return t.result.toLowerCase().includes(this._filterText.toLowerCase())
+ }
+ })
+ }
return tasks
}
this.selectedTasks.clear()
}
- duringTabChange(navID: number) {
+ duringTabChange() {
this.page = 1
}
+ beforeTabChange() {
+ this.resetFilter()
+ this.filterTargetID = TaskFilterTargetID.Name
+ }
+
get activeTabLocalized(): string {
switch (this.activeTab) {
- case 'queued':
+ case TaskTab.Queued:
return $localize`queued`
- case 'started':
+ case TaskTab.Started:
return $localize`started`
- case 'completed':
+ case TaskTab.Completed:
return $localize`completed`
- case 'failed':
+ case TaskTab.Failed:
return $localize`failed`
}
}
}, 5000)
}
}
+
+ public resetFilter() {
+ this._filterText = ''
+ }
+
+ filterInputKeyup(event: KeyboardEvent) {
+ if (event.key == 'Enter') {
+ this._filterText = (event.target as HTMLInputElement).value
+ } else if (event.key === 'Escape') {
+ this.resetFilter()
+ }
+ }
}