mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2026-08-03 09:32:17 +00:00
Ok, move to tasks grouped by priority
This commit is contained in:
@@ -8,24 +8,20 @@
|
||||
<button class="btn btn-sm btn-outline-secondary me-2" (click)="clearSelection()" [hidden]="selectedTasks.size === 0">
|
||||
<i-bs name="x" class="me-1"></i-bs><ng-container i18n>Clear selection</ng-container>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-primary me-2" (click)="dismissTasks()" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.PaperlessTask }" [disabled]="tasksService.total === 0">
|
||||
<button class="btn btn-sm btn-outline-primary me-2" (click)="dismissTasks()" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.PaperlessTask }" [disabled]="visibleTasks.length === 0">
|
||||
<i-bs name="check2-all" class="me-1"></i-bs>{{dismissButtonText}}
|
||||
</button>
|
||||
<div class="form-inline d-flex align-items-center">
|
||||
<div class="input-group input-group-sm flex-fill w-auto flex-nowrap">
|
||||
<span class="input-group-text text-muted" i18n>Filter by</span>
|
||||
@if (filterTargets.length > 1) {
|
||||
<div ngbDropdown>
|
||||
<button class="btn btn-sm btn-outline-primary" ngbDropdownToggle>{{filterTargetName}}</button>
|
||||
<div class="dropdown-menu shadow" ngbDropdownMenu>
|
||||
@for (t of filterTargets; track t.id) {
|
||||
<button ngbDropdownItem [class.active]="filterTargetID === t.id" (click)="filterTargetID = t.id">{{t.name}}</button>
|
||||
}
|
||||
</div>
|
||||
<div ngbDropdown>
|
||||
<button class="btn btn-sm btn-outline-primary" ngbDropdownToggle>{{filterTargetName}}</button>
|
||||
<div class="dropdown-menu shadow" ngbDropdownMenu>
|
||||
@for (t of filterTargets; track t.id) {
|
||||
<button ngbDropdownItem [class.active]="filterTargetID === t.id" (click)="filterTargetID = t.id">{{t.name}}</button>
|
||||
}
|
||||
</div>
|
||||
} @else {
|
||||
<span class="input-group-text">{{filterTargetName}}</span>
|
||||
}
|
||||
</div>
|
||||
@if (filterText?.length) {
|
||||
<button class="btn btn-link btn-sm px-2 position-absolute top-0 end-0 z-10" (click)="resetFilter()">
|
||||
<i-bs width="1em" height="1em" name="x"></i-bs>
|
||||
@@ -48,19 +44,50 @@
|
||||
<div class="visually-hidden" i18n>Loading...</div>
|
||||
}
|
||||
|
||||
<ng-template let-tasks="tasks" #tasksTemplate>
|
||||
<div class="d-flex flex-wrap gap-2 mb-3">
|
||||
<button
|
||||
class="btn btn-sm"
|
||||
[class.btn-primary]="selectedSection === allTaskSections"
|
||||
[class.btn-outline-primary]="selectedSection !== allTaskSections"
|
||||
(click)="setSection(allTaskSections)">
|
||||
<ng-container i18n>All</ng-container>
|
||||
</button>
|
||||
@for (section of sections; track section) {
|
||||
<button
|
||||
class="btn btn-sm"
|
||||
[class.btn-primary]="selectedSection === section"
|
||||
[class.btn-outline-primary]="selectedSection !== section"
|
||||
(click)="setSection(section)">
|
||||
{{ sectionLabel(section) }}
|
||||
@if (sectionCount(section) > 0) {
|
||||
<span class="badge ms-2" [class.bg-danger]="section === TaskSection.NeedsAttention" [class.bg-secondary]="section !== TaskSection.NeedsAttention">{{sectionCount(section)}}</span>
|
||||
}
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
|
||||
<ng-template let-tasks="tasks" let-section="section" #tasksTemplate>
|
||||
<div class="section-header d-flex align-items-center justify-content-between mb-2">
|
||||
<div>
|
||||
<h5 class="mb-0">{{ sectionLabel(section) }}</h5>
|
||||
<div class="small text-muted">
|
||||
<ng-container i18n>{tasks.length, plural, =1 {1 task} other {{{tasks.length}} tasks}}</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table class="table table-striped align-middle border shadow-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" id="all-tasks" [disabled]="currentTasks.length === 0" [(ngModel)]="togggleAll" (click)="toggleAll($event); $event.stopPropagation();">
|
||||
<label class="form-check-label" for="all-tasks"></label>
|
||||
<input type="checkbox" class="form-check-input" [id]="'all-tasks-' + section" [disabled]="tasks.length === 0" [checked]="areAllSelected(tasks)" (click)="toggleSection(section, $event); $event.stopPropagation();">
|
||||
<label class="form-check-label" [for]="'all-tasks-' + section"></label>
|
||||
</div>
|
||||
</th>
|
||||
<th scope="col" i18n>Name</th>
|
||||
<th scope="col" class="d-none d-lg-table-cell" i18n>Created</th>
|
||||
@if (activeTab !== 'started' && activeTab !== 'queued') {
|
||||
@if (sectionShowsResults(section)) {
|
||||
<th scope="col" class="d-none d-lg-table-cell" i18n>Results</th>
|
||||
}
|
||||
<th scope="col" class="d-table-cell d-lg-none" i18n>Info</th>
|
||||
@@ -68,11 +95,11 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (task of tasks | slice: (page-1) * pageSize : page * pageSize; track task.id) {
|
||||
<tr (click)="toggleSelected(task, $event); $event.stopPropagation();">
|
||||
@for (task of tasks; track task.id) {
|
||||
<tr (click)="toggleSelected(task); $event.stopPropagation();">
|
||||
<td>
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" id="task{{task.id}}" [checked]="selectedTasks.has(task.id)" (click)="toggleSelected(task, $event); $event.stopPropagation();">
|
||||
<input type="checkbox" class="form-check-input" id="task{{task.id}}" [checked]="selectedTasks.has(task.id)" (click)="toggleSelected(task); $event.stopPropagation();">
|
||||
<label class="form-check-label" for="task{{task.id}}"></label>
|
||||
</div>
|
||||
</td>
|
||||
@@ -87,7 +114,7 @@
|
||||
</div>
|
||||
</td>
|
||||
<td class="d-none d-lg-table-cell">{{ task.date_created | customDate:'short' }}</td>
|
||||
@if (activeTab !== 'started' && activeTab !== 'queued') {
|
||||
@if (sectionShowsResults(section)) {
|
||||
<td class="d-none d-lg-table-cell">
|
||||
@if (task.result_message?.length > 50) {
|
||||
<div class="result" (click)="expandTask(task); $event.stopPropagation();"
|
||||
@@ -129,61 +156,21 @@
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="p-0" [class.border-0]="expandedTask !== task.id" colspan="5">
|
||||
<td class="p-0" [class.border-0]="expandedTask !== task.id" [attr.colspan]="sectionShowsResults(section) ? 5 : 4">
|
||||
<pre #collapse="ngbCollapse" [ngbCollapse]="expandedTask !== task.id" class="small mb-0"><div class="small p-1 p-lg-3 ms-lg-3">{{ task.result_message }}</div></pre>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="pb-3 d-sm-flex justify-content-between align-items-center">
|
||||
@if (tasks.length > 0) {
|
||||
<div class="pb-2 pb-sm-0">
|
||||
<ng-container i18n>{tasks.length, plural, =1 {One {{this.activeTabLocalized}} task} other {{{tasks.length || 0}} total {{this.activeTabLocalized}} tasks}}</ng-container>
|
||||
@if (selectedTasks.size > 0) {
|
||||
<ng-container i18n> ({{selectedTasks.size}} selected)</ng-container>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
@if (tasks.length > pageSize) {
|
||||
<ngb-pagination [(page)]="page" [pageSize]="pageSize" [collectionSize]="tasks.length" maxSize="8" size="sm"></ngb-pagination>
|
||||
}
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<ul ngbNav #nav="ngbNav" [(activeId)]="activeTab" class="nav-tabs" (hidden)="duringTabChange()" (navChange)="beforeTabChange()">
|
||||
<li ngbNavItem="failed">
|
||||
<a ngbNavLink i18n>Failed@if (tasksService.failedFileTasks.length > 0) {
|
||||
<span class="badge bg-danger ms-2">{{tasksService.failedFileTasks.length}}</span>
|
||||
}</a>
|
||||
<ng-template ngbNavContent>
|
||||
<ng-container [ngTemplateOutlet]="tasksTemplate" [ngTemplateOutletContext]="{tasks:currentTasks}"></ng-container>
|
||||
</ng-template>
|
||||
</li>
|
||||
<li ngbNavItem="completed">
|
||||
<a ngbNavLink i18n>Complete@if (tasksService.completedFileTasks.length > 0) {
|
||||
<span class="badge bg-secondary ms-2">{{tasksService.completedFileTasks.length}}</span>
|
||||
}</a>
|
||||
<ng-template ngbNavContent>
|
||||
<ng-container [ngTemplateOutlet]="tasksTemplate" [ngTemplateOutletContext]="{tasks:currentTasks}"></ng-container>
|
||||
</ng-template>
|
||||
</li>
|
||||
<li ngbNavItem="started">
|
||||
<a ngbNavLink i18n>Started@if (tasksService.startedFileTasks.length > 0) {
|
||||
<span class="badge bg-secondary ms-2">{{tasksService.startedFileTasks.length}}</span>
|
||||
}</a>
|
||||
<ng-template ngbNavContent>
|
||||
<ng-container [ngTemplateOutlet]="tasksTemplate" [ngTemplateOutletContext]="{tasks:currentTasks}"></ng-container>
|
||||
</ng-template>
|
||||
</li>
|
||||
<li ngbNavItem="queued">
|
||||
<a ngbNavLink i18n>Queued@if (tasksService.queuedFileTasks.length > 0) {
|
||||
<span class="badge bg-secondary ms-2">{{tasksService.queuedFileTasks.length}}</span>
|
||||
}</a>
|
||||
<ng-template ngbNavContent>
|
||||
<ng-container [ngTemplateOutlet]="tasksTemplate" [ngTemplateOutletContext]="{tasks:currentTasks}"></ng-container>
|
||||
</ng-template>
|
||||
</li>
|
||||
</ul>
|
||||
<div [ngbNavOutlet]="nav"></div>
|
||||
@if (visibleSections.length > 0) {
|
||||
@for (section of visibleSections; track section) {
|
||||
<div class="mb-4">
|
||||
<ng-container [ngTemplateOutlet]="tasksTemplate" [ngTemplateOutletContext]="{tasks: tasksForSection(section), section: section}"></ng-container>
|
||||
</div>
|
||||
}
|
||||
} @else {
|
||||
<div class="alert alert-secondary fst-italic" i18n>No tasks match the current filters.</div>
|
||||
}
|
||||
|
||||
@@ -9,12 +9,7 @@ import { FormsModule } from '@angular/forms'
|
||||
import { By } from '@angular/platform-browser'
|
||||
import { Router } from '@angular/router'
|
||||
import { RouterTestingModule } from '@angular/router/testing'
|
||||
import {
|
||||
NgbModal,
|
||||
NgbModalRef,
|
||||
NgbModule,
|
||||
NgbNavItem,
|
||||
} from '@ng-bootstrap/ng-bootstrap'
|
||||
import { NgbModal, NgbModalRef, NgbModule } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { allIcons, NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||
import { throwError } from 'rxjs'
|
||||
import { routes } from 'src/app/app-routing.module'
|
||||
@@ -33,7 +28,11 @@ import { ToastService } from 'src/app/services/toast.service'
|
||||
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 { TasksComponent, TaskTab } from './tasks.component'
|
||||
import {
|
||||
ALL_TASK_SECTIONS,
|
||||
TasksComponent,
|
||||
TaskSection,
|
||||
} from './tasks.component'
|
||||
|
||||
const tasks: PaperlessTask[] = [
|
||||
{
|
||||
@@ -205,53 +204,25 @@ describe('TasksComponent', () => {
|
||||
.flush(tasks)
|
||||
})
|
||||
|
||||
it('should display file tasks in 4 tabs by status', () => {
|
||||
const tabButtons = fixture.debugElement.queryAll(By.directive(NgbNavItem))
|
||||
it('should display task sections with counts', () => {
|
||||
expect(component.selectedSection).toBe(ALL_TASK_SECTIONS)
|
||||
|
||||
let currentTasksLength = tasks.filter(
|
||||
(t) => t.status === PaperlessTaskStatus.Failure
|
||||
).length
|
||||
component.activeTab = TaskTab.Failed
|
||||
fixture.detectChanges()
|
||||
expect(tabButtons[0].nativeElement.textContent).toEqual(
|
||||
`Failed${currentTasksLength}`
|
||||
)
|
||||
expect(
|
||||
fixture.debugElement.queryAll(By.css('table input[type="checkbox"]'))
|
||||
).toHaveLength(currentTasksLength + 1)
|
||||
const buttons = fixture.debugElement.queryAll(By.css('.btn.btn-sm'))
|
||||
const text = buttons.map((button) => button.nativeElement.textContent)
|
||||
|
||||
currentTasksLength = tasks.filter(
|
||||
(t) => t.status === PaperlessTaskStatus.Success
|
||||
).length
|
||||
component.activeTab = TaskTab.Completed
|
||||
fixture.detectChanges()
|
||||
expect(tabButtons[1].nativeElement.textContent).toEqual(
|
||||
`Complete${currentTasksLength}`
|
||||
)
|
||||
|
||||
currentTasksLength = tasks.filter(
|
||||
(t) => t.status === PaperlessTaskStatus.Started
|
||||
).length
|
||||
component.activeTab = TaskTab.Started
|
||||
fixture.detectChanges()
|
||||
expect(tabButtons[2].nativeElement.textContent).toEqual(
|
||||
`Started${currentTasksLength}`
|
||||
)
|
||||
|
||||
currentTasksLength = tasks.filter(
|
||||
(t) => t.status === PaperlessTaskStatus.Pending
|
||||
).length
|
||||
component.activeTab = TaskTab.Queued
|
||||
fixture.detectChanges()
|
||||
expect(tabButtons[3].nativeElement.textContent).toEqual(
|
||||
`Queued${currentTasksLength}`
|
||||
)
|
||||
expect(text.join(' ')).toContain('All')
|
||||
expect(text.join(' ')).toContain('Needs attention2')
|
||||
expect(text.join(' ')).toContain('In progress2')
|
||||
expect(text.join(' ')).toContain('Recent completed2')
|
||||
})
|
||||
|
||||
it('should to go page 1 between tab switch', () => {
|
||||
component.page = 10
|
||||
component.duringTabChange()
|
||||
expect(component.page).toEqual(1)
|
||||
it('should filter visible sections by selected status', () => {
|
||||
component.setSection(TaskSection.InProgress)
|
||||
fixture.detectChanges()
|
||||
|
||||
expect(component.visibleSections).toEqual([TaskSection.InProgress])
|
||||
expect(fixture.nativeElement.textContent).toContain('In progress')
|
||||
expect(fixture.nativeElement.textContent).not.toContain('Recent completed')
|
||||
})
|
||||
|
||||
it('should support expanding / collapsing one task at a time', () => {
|
||||
@@ -273,7 +244,7 @@ describe('TasksComponent', () => {
|
||||
component.toggleSelected(tasks[0])
|
||||
component.toggleSelected(tasks[1])
|
||||
component.toggleSelected(tasks[3])
|
||||
component.toggleSelected(tasks[3]) // uncheck, for coverage
|
||||
component.toggleSelected(tasks[3])
|
||||
const selected = new Set([tasks[0].id, tasks[1].id])
|
||||
expect(component.selectedTasks).toEqual(selected)
|
||||
let modal: NgbModalRef
|
||||
@@ -322,32 +293,24 @@ describe('TasksComponent', () => {
|
||||
expect(component.selectedTasks.size).toBe(0)
|
||||
})
|
||||
|
||||
it('should support dismiss all tasks', () => {
|
||||
it('should support dismiss visible tasks', () => {
|
||||
component.setSection(TaskSection.NeedsAttention)
|
||||
let modal: NgbModalRef
|
||||
modalService.activeInstances.subscribe((m) => (modal = m[m.length - 1]))
|
||||
const dismissSpy = jest.spyOn(tasksService, 'dismissTasks')
|
||||
component.dismissTasks()
|
||||
expect(modal).not.toBeUndefined()
|
||||
modal.componentInstance.confirmClicked.emit()
|
||||
expect(dismissSpy).toHaveBeenCalledWith(new Set(tasks.map((t) => t.id)))
|
||||
expect(dismissSpy).toHaveBeenCalledWith(new Set([467, 466]))
|
||||
})
|
||||
|
||||
it('should support toggle all tasks', () => {
|
||||
it('should support toggling a full section', () => {
|
||||
const toggleCheck = fixture.debugElement.query(
|
||||
By.css('table input[type=checkbox]')
|
||||
By.css('#all-tasks-needs_attention')
|
||||
)
|
||||
toggleCheck.nativeElement.dispatchEvent(new MouseEvent('click'))
|
||||
fixture.detectChanges()
|
||||
expect(component.selectedTasks).toEqual(
|
||||
new Set(
|
||||
tasks
|
||||
.filter((t) => t.status === PaperlessTaskStatus.Failure)
|
||||
.map((t) => t.id)
|
||||
)
|
||||
)
|
||||
toggleCheck.nativeElement.dispatchEvent(new MouseEvent('click'))
|
||||
fixture.detectChanges()
|
||||
expect(component.selectedTasks).toEqual(new Set())
|
||||
expect(component.selectedTasks).toEqual(new Set([467, 466]))
|
||||
})
|
||||
|
||||
it('should support dismiss and open a document', () => {
|
||||
@@ -374,16 +337,16 @@ describe('TasksComponent', () => {
|
||||
)
|
||||
input.nativeElement.value = '191092'
|
||||
input.nativeElement.dispatchEvent(new Event('input'))
|
||||
jest.advanceTimersByTime(150) // debounce time
|
||||
jest.advanceTimersByTime(150)
|
||||
fixture.detectChanges()
|
||||
expect(component.filterText).toEqual('191092')
|
||||
expect(
|
||||
fixture.debugElement.queryAll(By.css('table tbody tr')).length
|
||||
).toEqual(2) // 1 task x 2 lines
|
||||
expect(component.tasksForSection(TaskSection.NeedsAttention)).toHaveLength(
|
||||
1
|
||||
)
|
||||
})
|
||||
|
||||
it('should fall back to task type when filename is unavailable', () => {
|
||||
component.activeTab = TaskTab.Started
|
||||
component.setSection(TaskSection.InProgress)
|
||||
fixture.detectChanges()
|
||||
|
||||
const nameColumn = fixture.debugElement.queryAll(
|
||||
@@ -398,20 +361,19 @@ describe('TasksComponent', () => {
|
||||
})
|
||||
|
||||
it('should filter tasks by result', () => {
|
||||
component.activeTab = TaskTab.Failed
|
||||
fixture.detectChanges()
|
||||
component.setSection(TaskSection.NeedsAttention)
|
||||
component.filterTargetID = 1
|
||||
const input = fixture.debugElement.query(
|
||||
By.css('pngx-page-header input[type=text]')
|
||||
)
|
||||
input.nativeElement.value = 'duplicate'
|
||||
input.nativeElement.dispatchEvent(new Event('input'))
|
||||
jest.advanceTimersByTime(150) // debounce time
|
||||
jest.advanceTimersByTime(150)
|
||||
fixture.detectChanges()
|
||||
expect(component.filterText).toEqual('duplicate')
|
||||
expect(
|
||||
fixture.debugElement.queryAll(By.css('table tbody tr')).length
|
||||
).toEqual(4) // 2 tasks x 2 lines
|
||||
expect(component.tasksForSection(TaskSection.NeedsAttention)).toHaveLength(
|
||||
2
|
||||
)
|
||||
})
|
||||
|
||||
it('should support keyboard events for filtering', () => {
|
||||
@@ -422,19 +384,10 @@ describe('TasksComponent', () => {
|
||||
input.nativeElement.dispatchEvent(
|
||||
new KeyboardEvent('keyup', { key: 'Enter' })
|
||||
)
|
||||
expect(component.filterText).toEqual('191092') // no debounce needed
|
||||
expect(component.filterText).toEqual('191092')
|
||||
input.nativeElement.dispatchEvent(
|
||||
new KeyboardEvent('keyup', { key: 'Escape' })
|
||||
)
|
||||
expect(component.filterText).toEqual('')
|
||||
})
|
||||
|
||||
it('should reset filter and target on tab switch', () => {
|
||||
component.filterText = '191092'
|
||||
component.filterTargetID = 1
|
||||
component.activeTab = TaskTab.Completed
|
||||
component.beforeTabChange()
|
||||
expect(component.filterText).toEqual('')
|
||||
expect(component.filterTargetID).toEqual(0)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { NgTemplateOutlet, SlicePipe } from '@angular/common'
|
||||
import { NgTemplateOutlet } from '@angular/common'
|
||||
import { Component, inject, OnDestroy, OnInit } from '@angular/core'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { Router } from '@angular/router'
|
||||
@@ -6,8 +6,6 @@ import {
|
||||
NgbCollapseModule,
|
||||
NgbDropdownModule,
|
||||
NgbModal,
|
||||
NgbNavModule,
|
||||
NgbPaginationModule,
|
||||
NgbPopoverModule,
|
||||
} from '@ng-bootstrap/ng-bootstrap'
|
||||
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||
@@ -20,7 +18,7 @@ import {
|
||||
takeUntil,
|
||||
timer,
|
||||
} from 'rxjs'
|
||||
import { PaperlessTask } from 'src/app/data/paperless-task'
|
||||
import { PaperlessTask, PaperlessTaskStatus } 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'
|
||||
@@ -29,11 +27,10 @@ import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dial
|
||||
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
|
||||
import { LoadingComponentWithPermissions } from '../../loading-component/loading.component'
|
||||
|
||||
export enum TaskTab {
|
||||
Queued = 'queued',
|
||||
Started = 'started',
|
||||
export enum TaskSection {
|
||||
NeedsAttention = 'needs_attention',
|
||||
InProgress = 'in_progress',
|
||||
Completed = 'completed',
|
||||
Failed = 'failed',
|
||||
}
|
||||
|
||||
enum TaskFilterTargetID {
|
||||
@@ -46,6 +43,14 @@ const FILTER_TARGETS = [
|
||||
{ id: TaskFilterTargetID.Result, name: $localize`Result` },
|
||||
]
|
||||
|
||||
export const ALL_TASK_SECTIONS = 'all'
|
||||
|
||||
const SECTION_LABELS: Record<TaskSection, string> = {
|
||||
[TaskSection.NeedsAttention]: $localize`Needs attention`,
|
||||
[TaskSection.InProgress]: $localize`In progress`,
|
||||
[TaskSection.Completed]: $localize`Recently completed`,
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'pngx-tasks',
|
||||
templateUrl: './tasks.component.html',
|
||||
@@ -54,14 +59,11 @@ const FILTER_TARGETS = [
|
||||
PageHeaderComponent,
|
||||
IfPermissionsDirective,
|
||||
CustomDatePipe,
|
||||
SlicePipe,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
NgTemplateOutlet,
|
||||
NgbCollapseModule,
|
||||
NgbDropdownModule,
|
||||
NgbNavModule,
|
||||
NgbPaginationModule,
|
||||
NgbPopoverModule,
|
||||
NgxBootstrapIconsModule,
|
||||
],
|
||||
@@ -75,15 +77,18 @@ export class TasksComponent
|
||||
private readonly router = inject(Router)
|
||||
private readonly toastService = inject(ToastService)
|
||||
|
||||
public activeTab: TaskTab
|
||||
readonly TaskSection = TaskSection
|
||||
readonly sections = [
|
||||
TaskSection.NeedsAttention,
|
||||
TaskSection.InProgress,
|
||||
TaskSection.Completed,
|
||||
]
|
||||
readonly allTaskSections = ALL_TASK_SECTIONS
|
||||
public selectedTasks: Set<number> = new Set()
|
||||
public togggleAll: boolean = false
|
||||
public expandedTask: number
|
||||
|
||||
public pageSize: number = 25
|
||||
public page: number = 1
|
||||
|
||||
public autoRefreshEnabled: boolean = true
|
||||
public selectedSection: TaskSection | typeof ALL_TASK_SECTIONS =
|
||||
ALL_TASK_SECTIONS
|
||||
|
||||
private _filterText: string = ''
|
||||
get filterText() {
|
||||
@@ -95,20 +100,35 @@ export class TasksComponent
|
||||
|
||||
public filterTargetID: TaskFilterTargetID = TaskFilterTargetID.Name
|
||||
public get filterTargetName(): string {
|
||||
return this.filterTargets.find((t) => t.id == this.filterTargetID).name
|
||||
return FILTER_TARGETS.find((t) => t.id == this.filterTargetID).name
|
||||
}
|
||||
private filterDebounce: Subject<string> = new Subject<string>()
|
||||
|
||||
public get filterTargets(): Array<{ id: number; name: string }> {
|
||||
return [TaskTab.Failed, TaskTab.Completed].includes(this.activeTab)
|
||||
? FILTER_TARGETS
|
||||
: FILTER_TARGETS.slice(0, 1)
|
||||
return FILTER_TARGETS
|
||||
}
|
||||
|
||||
get dismissButtonText(): string {
|
||||
return this.selectedTasks.size > 0
|
||||
? $localize`Dismiss selected`
|
||||
: $localize`Dismiss all`
|
||||
: $localize`Dismiss visible`
|
||||
}
|
||||
|
||||
get visibleSections(): TaskSection[] {
|
||||
const sections =
|
||||
this.selectedSection === ALL_TASK_SECTIONS
|
||||
? this.sections
|
||||
: [this.selectedSection]
|
||||
|
||||
return sections.filter(
|
||||
(section) => this.tasksForSection(section).length > 0
|
||||
)
|
||||
}
|
||||
|
||||
get visibleTasks(): PaperlessTask[] {
|
||||
return this.visibleSections.flatMap((section) =>
|
||||
this.tasksForSection(section)
|
||||
)
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
@@ -143,14 +163,16 @@ export class TasksComponent
|
||||
|
||||
dismissTasks(task: PaperlessTask = undefined) {
|
||||
let tasks = task ? new Set([task.id]) : new Set(this.selectedTasks.values())
|
||||
if (!task && tasks.size == 0)
|
||||
tasks = new Set(this.tasksService.allFileTasks.map((t) => t.id))
|
||||
if (!task && tasks.size == 0) {
|
||||
tasks = new Set(this.visibleTasks.map((t) => t.id))
|
||||
}
|
||||
|
||||
if (tasks.size > 1) {
|
||||
let modal = this.modalService.open(ConfirmDialogComponent, {
|
||||
backdrop: 'static',
|
||||
})
|
||||
modal.componentInstance.title = $localize`Confirm Dismiss All`
|
||||
modal.componentInstance.messageBold = $localize`Dismiss all ${tasks.size} tasks?`
|
||||
modal.componentInstance.title = $localize`Confirm Dismiss`
|
||||
modal.componentInstance.messageBold = $localize`Dismiss ${tasks.size} tasks?`
|
||||
modal.componentInstance.btnClass = 'btn-warning'
|
||||
modal.componentInstance.btnCaption = $localize`Dismiss`
|
||||
modal.componentInstance.confirmClicked.pipe(first()).subscribe(() => {
|
||||
@@ -164,7 +186,7 @@ export class TasksComponent
|
||||
})
|
||||
this.clearSelection()
|
||||
})
|
||||
} else {
|
||||
} else if (tasks.size === 1) {
|
||||
this.tasksService.dismissTasks(tasks).subscribe({
|
||||
error: (e) =>
|
||||
this.toastService.showError($localize`Error dismissing task`, e),
|
||||
@@ -188,6 +210,21 @@ export class TasksComponent
|
||||
: this.selectedTasks.add(task.id)
|
||||
}
|
||||
|
||||
toggleSection(section: TaskSection, event: PointerEvent) {
|
||||
const sectionTasks = this.tasksForSection(section)
|
||||
if ((event.target as HTMLInputElement).checked) {
|
||||
sectionTasks.forEach((task) => this.selectedTasks.add(task.id))
|
||||
} else {
|
||||
sectionTasks.forEach((task) => this.selectedTasks.delete(task.id))
|
||||
}
|
||||
}
|
||||
|
||||
areAllSelected(tasks: PaperlessTask[]): boolean {
|
||||
return (
|
||||
tasks.length > 0 && tasks.every((task) => this.selectedTasks.has(task.id))
|
||||
)
|
||||
}
|
||||
|
||||
taskDisplayName(task: PaperlessTask): string {
|
||||
return task.input_data?.filename?.toString() || task.task_type_display
|
||||
}
|
||||
@@ -196,73 +233,51 @@ export class TasksComponent
|
||||
return this.taskDisplayName(task) !== task.task_type_display
|
||||
}
|
||||
|
||||
get currentTasks(): PaperlessTask[] {
|
||||
let tasks: PaperlessTask[] = []
|
||||
switch (this.activeTab) {
|
||||
case TaskTab.Queued:
|
||||
tasks = this.tasksService.queuedFileTasks
|
||||
break
|
||||
case TaskTab.Started:
|
||||
tasks = this.tasksService.startedFileTasks
|
||||
break
|
||||
case TaskTab.Completed:
|
||||
tasks = this.tasksService.completedFileTasks
|
||||
break
|
||||
case TaskTab.Failed:
|
||||
tasks = this.tasksService.failedFileTasks
|
||||
break
|
||||
}
|
||||
tasksForSection(section: TaskSection): PaperlessTask[] {
|
||||
let tasks = this.tasksService.allFileTasks.filter((task) =>
|
||||
this.taskBelongsToSection(task, section)
|
||||
)
|
||||
|
||||
if (this._filterText.length) {
|
||||
tasks = tasks.filter((t) => {
|
||||
tasks = tasks.filter((task) => {
|
||||
if (this.filterTargetID == TaskFilterTargetID.Name) {
|
||||
return this.taskDisplayName(t)
|
||||
return this.taskDisplayName(task)
|
||||
?.toLowerCase()
|
||||
.includes(this._filterText.toLowerCase())
|
||||
} else if (this.filterTargetID == TaskFilterTargetID.Result) {
|
||||
return t.result_message
|
||||
return task.result_message
|
||||
?.toLowerCase()
|
||||
.includes(this._filterText.toLowerCase())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return tasks
|
||||
}
|
||||
|
||||
toggleAll(event: PointerEvent) {
|
||||
if ((event.target as HTMLInputElement).checked) {
|
||||
this.selectedTasks = new Set(this.currentTasks.map((t) => t.id))
|
||||
} else {
|
||||
this.clearSelection()
|
||||
}
|
||||
sectionLabel(section: TaskSection): string {
|
||||
return SECTION_LABELS[section]
|
||||
}
|
||||
|
||||
sectionCount(section: TaskSection): number {
|
||||
return this.tasksService.allFileTasks.filter((task) =>
|
||||
this.taskBelongsToSection(task, section)
|
||||
).length
|
||||
}
|
||||
|
||||
sectionShowsResults(section: TaskSection): boolean {
|
||||
return section !== TaskSection.InProgress
|
||||
}
|
||||
|
||||
setSection(section: TaskSection | typeof ALL_TASK_SECTIONS) {
|
||||
this.selectedSection = section
|
||||
this.clearSelection()
|
||||
}
|
||||
|
||||
clearSelection() {
|
||||
this.togggleAll = false
|
||||
this.selectedTasks.clear()
|
||||
}
|
||||
|
||||
duringTabChange() {
|
||||
this.page = 1
|
||||
}
|
||||
|
||||
beforeTabChange() {
|
||||
this.resetFilter()
|
||||
this.filterTargetID = TaskFilterTargetID.Name
|
||||
}
|
||||
|
||||
get activeTabLocalized(): string {
|
||||
switch (this.activeTab) {
|
||||
case TaskTab.Queued:
|
||||
return $localize`queued`
|
||||
case TaskTab.Started:
|
||||
return $localize`started`
|
||||
case TaskTab.Completed:
|
||||
return $localize`completed`
|
||||
case TaskTab.Failed:
|
||||
return $localize`failed`
|
||||
}
|
||||
}
|
||||
|
||||
public resetFilter() {
|
||||
this._filterText = ''
|
||||
}
|
||||
@@ -274,4 +289,24 @@ export class TasksComponent
|
||||
this.resetFilter()
|
||||
}
|
||||
}
|
||||
|
||||
private taskBelongsToSection(
|
||||
task: PaperlessTask,
|
||||
section: TaskSection
|
||||
): boolean {
|
||||
switch (section) {
|
||||
case TaskSection.NeedsAttention:
|
||||
return [
|
||||
PaperlessTaskStatus.Failure,
|
||||
PaperlessTaskStatus.Revoked,
|
||||
].includes(task.status)
|
||||
case TaskSection.InProgress:
|
||||
return [
|
||||
PaperlessTaskStatus.Pending,
|
||||
PaperlessTaskStatus.Started,
|
||||
].includes(task.status)
|
||||
case TaskSection.Completed:
|
||||
return task.status === PaperlessTaskStatus.Success
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user