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"> +
+ + +
+
+ + +
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 }) + ) + } }