mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2026-04-21 07:19:26 +00:00
Allow filtering by type + source
This commit is contained in:
@@ -32,6 +32,24 @@
|
||||
[(ngModel)]="filterText">
|
||||
</div>
|
||||
</div>
|
||||
<div ngbDropdown>
|
||||
<button class="btn btn-sm btn-outline-primary" ngbDropdownToggle>{{selectedTaskTypeLabel}}</button>
|
||||
<div class="dropdown-menu shadow" ngbDropdownMenu>
|
||||
<button ngbDropdownItem [class.active]="selectedTaskType === allFilterValue" (click)="setTaskType(allFilterValue)" i18n>All types</button>
|
||||
@for (option of taskTypeOptions; track option.value) {
|
||||
<button ngbDropdownItem [class.active]="selectedTaskType === option.value" [disabled]="isTaskTypeOptionDisabled(option.value)" (click)="setTaskType(option.value)">{{option.label}}</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div ngbDropdown>
|
||||
<button class="btn btn-sm btn-outline-primary" ngbDropdownToggle>{{selectedTriggerSourceLabel}}</button>
|
||||
<div class="dropdown-menu shadow" ngbDropdownMenu>
|
||||
<button ngbDropdownItem [class.active]="selectedTriggerSource === allFilterValue" (click)="setTriggerSource(allFilterValue)" i18n>All sources</button>
|
||||
@for (option of triggerSourceOptions; track option.value) {
|
||||
<button ngbDropdownItem [class.active]="selectedTriggerSource === option.value" [disabled]="isTriggerSourceOptionDisabled(option.value)" (click)="setTriggerSource(option.value)">{{option.label}}</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-check form-switch mb-0 ms-2">
|
||||
<input class="form-check-input" type="checkbox" role="switch" [(ngModel)]="autoRefreshEnabled">
|
||||
<label class="form-check-label" for="autoRefreshSwitch" i18n>Auto refresh</label>
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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<TaskSection, string> = {
|
||||
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<number> = 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 })
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user