diff --git a/src-ui/src/app/components/admin/tasks/tasks.component.html b/src-ui/src/app/components/admin/tasks/tasks.component.html
index c45b448b3..dc4e0baae 100644
--- a/src-ui/src/app/components/admin/tasks/tasks.component.html
+++ b/src-ui/src/app/components/admin/tasks/tasks.component.html
@@ -32,6 +32,24 @@
[(ngModel)]="filterText">
+
+
+
+
+ @for (option of taskTypeOptions; track option.value) {
+
+ }
+
+
+
+
+
+
+ @for (option of triggerSourceOptions; track option.value) {
+
+ }
+
+
diff --git a/src-ui/src/app/components/admin/tasks/tasks.component.spec.ts b/src-ui/src/app/components/admin/tasks/tasks.component.spec.ts
index a352daa7d..15dcbce1a 100644
--- a/src-ui/src/app/components/admin/tasks/tasks.component.spec.ts
+++ b/src-ui/src/app/components/admin/tasks/tasks.component.spec.ts
@@ -29,6 +29,7 @@ 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 {
+ ALL_FILTER_VALUE,
ALL_TASK_SECTIONS,
TasksComponent,
TaskSection,
@@ -206,6 +207,8 @@ describe('TasksComponent', () => {
it('should display task sections with counts', () => {
expect(component.selectedSection).toBe(ALL_TASK_SECTIONS)
+ expect(component.selectedTaskType).toBe(ALL_FILTER_VALUE)
+ expect(component.selectedTriggerSource).toBe(ALL_FILTER_VALUE)
const buttons = fixture.debugElement.queryAll(By.css('.btn.btn-sm'))
const text = buttons.map((button) => button.nativeElement.textContent)
@@ -225,6 +228,54 @@ describe('TasksComponent', () => {
expect(fixture.nativeElement.textContent).not.toContain('Recent completed')
})
+ it('should filter tasks by task type', () => {
+ component.setSection(TaskSection.InProgress)
+ component.setTaskType(PaperlessTaskType.SanityCheck)
+
+ expect(component.tasksForSection(TaskSection.InProgress)).toHaveLength(1)
+ expect(component.tasksForSection(TaskSection.InProgress)[0].task_type).toBe(
+ PaperlessTaskType.SanityCheck
+ )
+ })
+
+ it('should filter tasks by trigger source', () => {
+ component.setSection(TaskSection.InProgress)
+ component.setTriggerSource(PaperlessTaskTriggerSource.EmailConsume)
+
+ expect(component.tasksForSection(TaskSection.InProgress)).toHaveLength(1)
+ expect(
+ component.tasksForSection(TaskSection.InProgress)[0].trigger_source
+ ).toBe(PaperlessTaskTriggerSource.EmailConsume)
+ })
+
+ it('should expose stable task type options and disable empty ones', () => {
+ expect(component.taskTypeOptions.map((option) => option.value)).toContain(
+ PaperlessTaskType.TrainClassifier
+ )
+ expect(
+ component.isTaskTypeOptionDisabled(PaperlessTaskType.TrainClassifier)
+ ).toBe(true)
+ expect(
+ component.isTaskTypeOptionDisabled(PaperlessTaskType.ConsumeFile)
+ ).toBe(false)
+ })
+
+ it('should expose stable trigger source options and disable empty ones', () => {
+ expect(
+ component.triggerSourceOptions.map((option) => option.value)
+ ).toContain(PaperlessTaskTriggerSource.ApiUpload)
+ expect(
+ component.isTriggerSourceOptionDisabled(
+ PaperlessTaskTriggerSource.ApiUpload
+ )
+ ).toBe(true)
+ expect(
+ component.isTriggerSourceOptionDisabled(
+ PaperlessTaskTriggerSource.EmailConsume
+ )
+ ).toBe(false)
+ })
+
it('should support expanding / collapsing one task at a time', () => {
component.expandTask(tasks[0])
expect(component.expandedTask).toEqual(tasks[0].id)
@@ -345,6 +396,17 @@ describe('TasksComponent', () => {
)
})
+ it('should match task type and source in name filtering', () => {
+ component.setSection(TaskSection.InProgress)
+ component.filterText = 'system'
+ jest.advanceTimersByTime(150)
+
+ expect(component.tasksForSection(TaskSection.InProgress)).toHaveLength(1)
+ expect(component.tasksForSection(TaskSection.InProgress)[0].task_type).toBe(
+ PaperlessTaskType.SanityCheck
+ )
+ })
+
it('should fall back to task type when filename is unavailable', () => {
component.setSection(TaskSection.InProgress)
fixture.detectChanges()
diff --git a/src-ui/src/app/components/admin/tasks/tasks.component.ts b/src-ui/src/app/components/admin/tasks/tasks.component.ts
index ef18394bf..bc1b87aee 100644
--- a/src-ui/src/app/components/admin/tasks/tasks.component.ts
+++ b/src-ui/src/app/components/admin/tasks/tasks.component.ts
@@ -18,7 +18,12 @@ import {
takeUntil,
timer,
} from 'rxjs'
-import { PaperlessTask, PaperlessTaskStatus } from 'src/app/data/paperless-task'
+import {
+ PaperlessTask,
+ PaperlessTaskStatus,
+ PaperlessTaskTriggerSource,
+ PaperlessTaskType,
+} from 'src/app/data/paperless-task'
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe'
import { TasksService } from 'src/app/services/tasks.service'
@@ -43,14 +48,57 @@ const FILTER_TARGETS = [
{ id: TaskFilterTargetID.Result, name: $localize`Result` },
]
-export const ALL_TASK_SECTIONS = 'all'
+export const ALL_FILTER_VALUE = 'all'
+export const ALL_TASK_SECTIONS = ALL_FILTER_VALUE
-const SECTION_LABELS: Record = {
+const SECTION_LABELS = {
[TaskSection.NeedsAttention]: $localize`Needs attention`,
[TaskSection.InProgress]: $localize`In progress`,
[TaskSection.Completed]: $localize`Recently completed`,
}
+const TASK_TYPE_OPTIONS = [
+ { value: PaperlessTaskType.ConsumeFile, label: $localize`Consume File` },
+ {
+ value: PaperlessTaskType.TrainClassifier,
+ label: $localize`Train Classifier`,
+ },
+ { value: PaperlessTaskType.SanityCheck, label: $localize`Sanity Check` },
+ { value: PaperlessTaskType.MailFetch, label: $localize`Mail Fetch` },
+ { value: PaperlessTaskType.LlmIndex, label: $localize`AI Index` },
+ { value: PaperlessTaskType.EmptyTrash, label: $localize`Empty Trash` },
+ {
+ value: PaperlessTaskType.CheckWorkflows,
+ label: $localize`Check Workflows`,
+ },
+ { value: PaperlessTaskType.BulkUpdate, label: $localize`Bulk Update` },
+ {
+ value: PaperlessTaskType.ReprocessDocument,
+ label: $localize`Reprocess Document`,
+ },
+ {
+ value: PaperlessTaskType.BuildShareLink,
+ label: $localize`Build Share Link`,
+ },
+ { value: PaperlessTaskType.BulkDelete, label: $localize`Bulk Delete` },
+]
+
+const TRIGGER_SOURCE_OPTIONS = [
+ { value: PaperlessTaskTriggerSource.Scheduled, label: $localize`Scheduled` },
+ { value: PaperlessTaskTriggerSource.WebUI, label: $localize`Web UI` },
+ { value: PaperlessTaskTriggerSource.ApiUpload, label: $localize`API Upload` },
+ {
+ value: PaperlessTaskTriggerSource.FolderConsume,
+ label: $localize`Folder Consume`,
+ },
+ {
+ value: PaperlessTaskTriggerSource.EmailConsume,
+ label: $localize`Email Consume`,
+ },
+ { value: PaperlessTaskTriggerSource.System, label: $localize`System` },
+ { value: PaperlessTaskTriggerSource.Manual, label: $localize`Manual` },
+]
+
@Component({
selector: 'pngx-tasks',
templateUrl: './tasks.component.html',
@@ -84,11 +132,14 @@ export class TasksComponent
TaskSection.Completed,
]
readonly allTaskSections = ALL_TASK_SECTIONS
+ readonly allFilterValue = ALL_FILTER_VALUE
public selectedTasks: Set = new Set()
public expandedTask: number
public autoRefreshEnabled: boolean = true
public selectedSection: TaskSection | typeof ALL_TASK_SECTIONS =
ALL_TASK_SECTIONS
+ public selectedTaskType: string = ALL_FILTER_VALUE
+ public selectedTriggerSource: string = ALL_FILTER_VALUE
private _filterText: string = ''
get filterText() {
@@ -108,6 +159,38 @@ export class TasksComponent
return FILTER_TARGETS
}
+ public get taskTypeOptions(): Array<{ value: string; label: string }> {
+ return TASK_TYPE_OPTIONS
+ }
+
+ public get triggerSourceOptions(): Array<{ value: string; label: string }> {
+ return TRIGGER_SOURCE_OPTIONS
+ }
+
+ public get selectedTaskTypeLabel(): string {
+ if (this.selectedTaskType === ALL_FILTER_VALUE) {
+ return $localize`All types`
+ }
+
+ return (
+ this.taskTypeOptions.find(
+ (option) => option.value === this.selectedTaskType
+ )?.label ?? this.selectedTaskType
+ )
+ }
+
+ public get selectedTriggerSourceLabel(): string {
+ if (this.selectedTriggerSource === ALL_FILTER_VALUE) {
+ return $localize`All sources`
+ }
+
+ return (
+ this.triggerSourceOptions.find(
+ (option) => option.value === this.selectedTriggerSource
+ )?.label ?? this.selectedTriggerSource
+ )
+ }
+
get dismissButtonText(): string {
return this.selectedTasks.size > 0
? $localize`Dismiss selected`
@@ -238,21 +321,7 @@ export class TasksComponent
this.taskBelongsToSection(task, section)
)
- if (this._filterText.length) {
- tasks = tasks.filter((task) => {
- if (this.filterTargetID == TaskFilterTargetID.Name) {
- return this.taskDisplayName(task)
- ?.toLowerCase()
- .includes(this._filterText.toLowerCase())
- } else if (this.filterTargetID == TaskFilterTargetID.Result) {
- return task.result_message
- ?.toLowerCase()
- .includes(this._filterText.toLowerCase())
- }
- })
- }
-
- return tasks
+ return tasks.filter((task) => this.taskMatchesCurrentFilters(task))
}
sectionLabel(section: TaskSection): string {
@@ -274,6 +343,32 @@ export class TasksComponent
this.clearSelection()
}
+ setTaskType(taskType: string) {
+ this.selectedTaskType = taskType
+ this.clearSelection()
+ }
+
+ setTriggerSource(triggerSource: string) {
+ this.selectedTriggerSource = triggerSource
+ this.clearSelection()
+ }
+
+ taskTypeOptionCount(taskType: string): number {
+ return this.tasksForOptionCounts({ taskType }).length
+ }
+
+ triggerSourceOptionCount(triggerSource: string): number {
+ return this.tasksForOptionCounts({ triggerSource }).length
+ }
+
+ isTaskTypeOptionDisabled(taskType: string): boolean {
+ return this.taskTypeOptionCount(taskType) === 0
+ }
+
+ isTriggerSourceOptionDisabled(triggerSource: string): boolean {
+ return this.triggerSourceOptionCount(triggerSource) === 0
+ }
+
clearSelection() {
this.selectedTasks.clear()
}
@@ -309,4 +404,70 @@ export class TasksComponent
return task.status === PaperlessTaskStatus.Success
}
}
+
+ private taskMatchesCurrentFilters(task: PaperlessTask): boolean {
+ return this.taskMatchesFilters(task, {
+ taskType: this.selectedTaskType,
+ triggerSource: this.selectedTriggerSource,
+ })
+ }
+
+ private taskMatchesFilters(
+ task: PaperlessTask,
+ {
+ taskType,
+ triggerSource,
+ }: {
+ taskType: string
+ triggerSource: string
+ }
+ ): boolean {
+ if (taskType !== ALL_FILTER_VALUE && task.task_type !== taskType) {
+ return false
+ }
+
+ if (
+ triggerSource !== ALL_FILTER_VALUE &&
+ task.trigger_source !== triggerSource
+ ) {
+ return false
+ }
+
+ if (!this._filterText.length) {
+ return true
+ }
+
+ const query = this._filterText.toLowerCase()
+
+ if (this.filterTargetID == TaskFilterTargetID.Name) {
+ return [
+ this.taskDisplayName(task),
+ task.task_type_display,
+ task.trigger_source_display,
+ ]
+ .filter(Boolean)
+ .some((value) => value.toLowerCase().includes(query))
+ }
+
+ return task.result_message?.toLowerCase().includes(query)
+ }
+
+ private tasksForOptionCounts({
+ taskType = this.selectedTaskType,
+ triggerSource = this.selectedTriggerSource,
+ }: {
+ taskType?: string
+ triggerSource?: string
+ }): PaperlessTask[] {
+ const sections =
+ this.selectedSection === ALL_TASK_SECTIONS
+ ? this.sections
+ : [this.selectedSection]
+
+ return this.tasksService.allFileTasks.filter(
+ (task) =>
+ sections.some((section) => this.taskBelongsToSection(task, section)) &&
+ this.taskMatchesFilters(task, { taskType, triggerSource })
+ )
+ }
}