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 3b2c58f63e..a3198296e0 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 @@ -7,10 +7,15 @@ import { import { ComponentFixture, TestBed } from '@angular/core/testing' import { FormsModule } from '@angular/forms' import { By } from '@angular/platform-browser' -import { Router } from '@angular/router' +import { + ActivatedRoute, + convertToParamMap, + ParamMap, + Router, +} from '@angular/router' import { RouterTestingModule } from '@angular/router/testing' import { NgbModal, NgbModalRef, NgbModule } from '@ng-bootstrap/ng-bootstrap' -import { of, throwError } from 'rxjs' +import { BehaviorSubject, of, throwError } from 'rxjs' import { routes } from 'src/app/app-routing.module' import { PaperlessTask, @@ -159,6 +164,7 @@ const sectionCountResponse = { } describe('TasksComponent', () => { + let queryParams: BehaviorSubject let component: TasksComponent let fixture: ComponentFixture let tasksService: TasksService @@ -169,6 +175,7 @@ describe('TasksComponent', () => { let toastService: ToastService beforeEach(async () => { + queryParams = new BehaviorSubject(convertToParamMap({})) TestBed.configureTestingModule({ imports: [ NgbModule, @@ -181,6 +188,17 @@ describe('TasksComponent', () => { ConfirmDialogComponent, ], providers: [ + { + provide: ActivatedRoute, + useValue: { + queryParamMap: queryParams.asObservable(), + snapshot: { + get queryParamMap() { + return queryParams.value + }, + }, + }, + }, { provide: PermissionsService, useValue: { @@ -238,6 +256,71 @@ describe('TasksComponent', () => { flushTaskLists(25, count) } + it('consumes a task link on load into the normal resettable Task ID filter', () => { + jest + .spyOn(tasksService, 'list') + .mockReturnValue(of({ count: 0, results: [] })) + jest + .spyOn(tasksService, 'statusCounts') + .mockReturnValue(of(sectionCountResponse)) + jest.spyOn(router, 'navigate').mockResolvedValue(true) + queryParams.next(convertToParamMap({ task_id: 'linked-task' })) + fixture.destroy() + fixture = TestBed.createComponent(TasksComponent) + component = fixture.componentInstance + fixture.detectChanges() + expect(router.navigate).toHaveBeenCalledTimes(1) + expect(router.navigate).toHaveBeenCalledWith( + [], + expect.objectContaining({ + queryParams: { task_id: null }, + replaceUrl: true, + }) + ) + expect(component.filterTargetName).toBe('Task ID') + expect(component.filterText).toBe('linked-task') + expect(component.isFiltered).toBe(true) + component.setSection(TaskSection.InProgress) + expect(tasksService.list).toHaveBeenLastCalledWith(1, 25, { + task_id: 'linked-task', + status: [PaperlessTaskStatus.Pending, PaperlessTaskStatus.Started], + }) + component.resetFilters() + expect(component.filterText).toBe('') + expect(component.isFiltered).toBe(false) + expect(tasksService.list).toHaveBeenLastCalledWith(1, 25, { + acknowledged: false, + status: [PaperlessTaskStatus.Pending, PaperlessTaskStatus.Started], + }) + expect(router.navigate).toHaveBeenCalledTimes(1) + }) + + it('supports manually filtering by Task ID and clearing the text filter', () => { + jest + .spyOn(tasksService, 'list') + .mockReturnValue(of({ count: 0, results: [] })) + jest + .spyOn(tasksService, 'statusCounts') + .mockReturnValue(of(sectionCountResponse)) + component.setSection(TaskSection.InProgress) + component.setFilterTarget(TaskFilterTargetID.TaskId) + component.filterText = tasks[0].task_id + jest.advanceTimersByTime(150) + expect(tasksService.list).toHaveBeenLastCalledWith(1, 25, { + task_id: tasks[0].task_id, + status: [PaperlessTaskStatus.Pending, PaperlessTaskStatus.Started], + }) + component.pagedTasks.set(tasks) + expect(component.tasksForSection(TaskSection.NeedsAttention)).toEqual([ + tasks[0], + ]) + component.resetFilter() + expect(tasksService.list).toHaveBeenLastCalledWith(1, 25, { + acknowledged: false, + status: [PaperlessTaskStatus.Pending, PaperlessTaskStatus.Started], + }) + }) + it('should display task sections with counts', () => { expect(component.selectedSection).toBe(TaskSection.All) expect(component.selectedTaskType).toBeNull() 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 20f3107ca4..7aae9fca21 100644 --- a/src-ui/src/app/components/admin/tasks/tasks.component.ts +++ b/src-ui/src/app/components/admin/tasks/tasks.component.ts @@ -1,7 +1,7 @@ import { JsonPipe, NgTemplateOutlet } from '@angular/common' import { Component, inject, OnDestroy, OnInit, signal } from '@angular/core' import { FormsModule, ReactiveFormsModule } from '@angular/forms' -import { Router, RouterLink } from '@angular/router' +import { ActivatedRoute, Router, RouterLink } from '@angular/router' import { NgbCollapseModule, NgbDropdownModule, @@ -47,11 +47,13 @@ export enum TaskSection { export enum TaskFilterTargetID { Name, Result, + TaskId, } const FILTER_TARGETS = [ { id: TaskFilterTargetID.Name, name: $localize`Name` }, { id: TaskFilterTargetID.Result, name: $localize`Result` }, + { id: TaskFilterTargetID.TaskId, name: $localize`Task ID` }, ] const SECTION_LABELS = { @@ -161,6 +163,7 @@ export class TasksComponent tasksService = inject(TasksService) private modalService = inject(NgbModal) private readonly router = inject(Router) + private readonly route = inject(ActivatedRoute) private readonly toastService = inject(ToastService) readonly TaskSection = TaskSection @@ -275,6 +278,17 @@ export class TasksComponent } ngOnInit() { + const taskId = this.route.snapshot.queryParamMap.get('task_id') + if (taskId) { + this.filterTargetID = TaskFilterTargetID.TaskId + this._filterText = taskId + this.router.navigate([], { + relativeTo: this.route, + queryParams: { task_id: null }, + queryParamsHandling: 'merge', + replaceUrl: true, + }) + } this.tasksService.reload() this.reloadPage() timer(5000, 5000) @@ -636,6 +650,9 @@ export class TasksComponent const query = this._filterText.toLowerCase() + if (this.filterTargetID === TaskFilterTargetID.TaskId) { + return task.task_id.toLowerCase() === query + } if (this.filterTargetID == TaskFilterTargetID.Name) { return [ this.taskDisplayName(task), @@ -706,9 +723,15 @@ export class TasksComponent } if (this._filterText.length) { - params[ - this.filterTargetID === TaskFilterTargetID.Name ? 'name' : 'result' - ] = this._filterText + const parameter = { + [TaskFilterTargetID.Name]: 'name', + [TaskFilterTargetID.Result]: 'result', + [TaskFilterTargetID.TaskId]: 'task_id', + }[this.filterTargetID] + params[parameter] = this._filterText + if (this.filterTargetID === TaskFilterTargetID.TaskId) { + delete params.acknowledged + } } return params