Compare commits

...
Author SHA1 Message Date
shamoon f00a6db939 Fix: prevent pending tasks refresh from overwriting dismissed 2026-07-27 07:00:43 -07:00
3 changed files with 101 additions and 57 deletions
@@ -21,7 +21,7 @@
</div>
</pngx-page-header>
@if (!tasksService.completedFileTasks && tasksService.loading) {
@if (!tasksService.completedTasks && tasksService.loading) {
<div class="spinner-border spinner-border-sm fw-normal ms-2 me-auto" role="status"></div>
<div class="visually-hidden" i18n>Loading...</div>
}
+56 -9
View File
@@ -50,13 +50,66 @@ describe('TasksService', () => {
req.flush({ count: 0, results: [] })
})
it('does not call tasks api endpoint on reload if already loading', () => {
tasksService.loading = true
it('cancels an in-progress reload when reloading again', () => {
tasksService.reload()
httpTestingController.expectNone(
const staleReload = httpTestingController.expectOne(
(req: HttpRequest<unknown>) =>
req.url === `${environment.apiBaseUrl}tasks/`
)
tasksService.reload()
expect(staleReload.cancelled).toBe(true)
httpTestingController
.expectOne(
(req: HttpRequest<unknown>) =>
req.url === `${environment.apiBaseUrl}tasks/`
)
.flush({ count: 0, results: [] })
})
it('continues reloading after a reload request fails', () => {
tasksService.reload()
httpTestingController
.expectOne(
(req: HttpRequest<unknown>) =>
req.url === `${environment.apiBaseUrl}tasks/`
)
.flush('error', { status: 500, statusText: 'error' })
expect(tasksService.loading).toBe(false)
tasksService.reload()
httpTestingController
.expectOne(
(req: HttpRequest<unknown>) =>
req.url === `${environment.apiBaseUrl}tasks/`
)
.flush({ count: 0, results: [] })
})
it('reloads after dismissing a task while a reload is already in progress', () => {
tasksService.reload()
const staleReload = httpTestingController.expectOne(
(req: HttpRequest<unknown>) =>
req.url === `${environment.apiBaseUrl}tasks/` &&
req.params.get('acknowledged') === 'false'
)
tasksService.dismissTasks(new Set([1])).subscribe()
httpTestingController
.expectOne(`${environment.apiBaseUrl}tasks/acknowledge/`)
.flush([])
expect(staleReload.cancelled).toBe(true)
httpTestingController
.expectOne(
(req: HttpRequest<unknown>) =>
req.url === `${environment.apiBaseUrl}tasks/` &&
req.params.get('acknowledged') === 'false'
)
.flush({ count: 0, results: [] })
expect(tasksService.needsAttentionTasks).toHaveLength(0)
})
it('calls acknowledge_tasks api endpoint on dismiss and reloads', () => {
@@ -166,12 +219,6 @@ describe('TasksService', () => {
)
req.flush({ count: mockTasks.length, results: mockTasks })
expect(tasksService.allFileTasks).toHaveLength(5)
expect(tasksService.completedFileTasks).toHaveLength(2)
expect(tasksService.failedFileTasks).toHaveLength(1)
expect(tasksService.queuedFileTasks).toHaveLength(1)
expect(tasksService.startedFileTasks).toHaveLength(1)
})
it('includes revoked tasks in needs attention', () => {
+44 -47
View File
@@ -1,7 +1,15 @@
import { HttpClient } from '@angular/common/http'
import { Injectable, inject, signal } from '@angular/core'
import { Observable, Subject } from 'rxjs'
import { first, map, takeUntil, tap } from 'rxjs/operators'
import { EMPTY, Observable, Subject } from 'rxjs'
import {
catchError,
finalize,
first,
map,
switchMap,
takeUntil,
tap,
} from 'rxjs/operators'
import {
PaperlessTask,
PaperlessTaskStatus,
@@ -23,44 +31,48 @@ export class TasksService {
public loading: boolean = false
private readonly fileTasks = signal<PaperlessTask[]>([])
private readonly tasks = signal<PaperlessTask[]>([])
private readonly reloadNotifier = new Subject<void>()
private unsubscribeNotifer: Subject<any> = new Subject()
constructor() {
this.reloadNotifier
.pipe(
switchMap(() => {
this.loading = true
return this.http
.get<Results<PaperlessTask>>(`${this.baseUrl}${this.endpoint}/`, {
params: {
acknowledged: 'false',
page_size: this.defaultReloadPageSize,
},
})
.pipe(
map((response) => response.results),
takeUntil(this.unsubscribeNotifer),
catchError(() => EMPTY),
finalize(() => {
this.loading = false
})
)
})
)
.subscribe((tasks) => {
this.tasks.set(tasks)
})
}
public get total(): number {
return this.fileTasks().length
return this.tasks().length
}
public get allFileTasks(): PaperlessTask[] {
return this.fileTasks().slice(0)
}
public get queuedFileTasks(): PaperlessTask[] {
return this.fileTasks().filter(
(t) => t.status === PaperlessTaskStatus.Pending
)
}
public get startedFileTasks(): PaperlessTask[] {
return this.fileTasks().filter(
(t) => t.status === PaperlessTaskStatus.Started
)
}
public get completedFileTasks(): PaperlessTask[] {
return this.fileTasks().filter(
(t) => t.status === PaperlessTaskStatus.Success
)
}
public get failedFileTasks(): PaperlessTask[] {
return this.fileTasks().filter(
(t) => t.status === PaperlessTaskStatus.Failure
)
public get completedTasks(): PaperlessTask[] {
return this.tasks().filter((t) => t.status === PaperlessTaskStatus.Success)
}
public get needsAttentionTasks(): PaperlessTask[] {
return this.fileTasks().filter((t) =>
return this.tasks().filter((t) =>
[PaperlessTaskStatus.Failure, PaperlessTaskStatus.Revoked].includes(
t.status
)
@@ -68,22 +80,7 @@ export class TasksService {
}
public reload() {
if (this.loading) return
this.loading = true
this.http
.get<Results<PaperlessTask>>(`${this.baseUrl}${this.endpoint}/`, {
params: {
acknowledged: 'false',
page_size: this.defaultReloadPageSize,
},
})
.pipe(map((r) => r.results))
.pipe(takeUntil(this.unsubscribeNotifer), first())
.subscribe((r) => {
this.fileTasks.set(r)
this.loading = false
})
this.reloadNotifier.next()
}
public list(