mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2026-06-11 08:09:44 +00:00
Reorganize
This commit is contained in:
@@ -5,56 +5,12 @@
|
||||
i18n-info
|
||||
>
|
||||
<div class="btn-toolbar col col-md-auto align-items-center gap-2">
|
||||
@if (isFiltered) {
|
||||
<button class="btn btn-link py-0" (click)="resetFilters()">
|
||||
<i-bs width="1em" height="1em" name="x"></i-bs><small i18n>Reset filters</small>
|
||||
</button>
|
||||
}
|
||||
<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]="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>
|
||||
<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>
|
||||
@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>
|
||||
</button>
|
||||
}
|
||||
<input #filterInput class="form-control form-control-sm" type="text"
|
||||
(keyup)="filterInputKeyup($event)"
|
||||
[(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>
|
||||
@@ -67,24 +23,80 @@
|
||||
<div class="visually-hidden" i18n>Loading...</div>
|
||||
}
|
||||
|
||||
<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>
|
||||
<div class="task-controls mb-3 btn-toolbar" [class.task-controls-scoped]="hasScopedSectionView">
|
||||
<div class="task-view-scope btn-group btn-group-sm me-3" role="group">
|
||||
<input type="radio" class="btn-check"
|
||||
[checked]="selectedSection === allTaskSections"
|
||||
id="section-all"
|
||||
/>
|
||||
<label
|
||||
class="btn btn-outline-primary"
|
||||
for="section-all"
|
||||
(click)="setSection(allTaskSections)">
|
||||
<ng-container i18n>All</ng-container>
|
||||
</label>
|
||||
@for (section of sections; track section) {
|
||||
<input
|
||||
type="radio" class="btn-check"
|
||||
[checked]="selectedSection === section"
|
||||
id="section-{{section}}"
|
||||
/>
|
||||
<label
|
||||
class="btn btn-outline-primary"
|
||||
[for]="'section-' + 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>
|
||||
}
|
||||
</label>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div ngbDropdown>
|
||||
<button class="btn btn-sm btn-outline-primary me-3" 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 me-3" 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-inline d-flex align-items-center flex-grow-1 task-search">
|
||||
<div class="input-group input-group-sm flex-fill w-auto flex-nowrap">
|
||||
<span class="input-group-text text-muted" i18n>Filter by</span>
|
||||
<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>
|
||||
@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>
|
||||
</button>
|
||||
}
|
||||
<input #filterInput class="form-control form-control-sm" type="text"
|
||||
(keyup)="filterInputKeyup($event)"
|
||||
[(ngModel)]="filterText">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (isFiltered) {
|
||||
<button class="btn btn-link py-0 ms-md-auto" (click)="resetFilters()">
|
||||
<i-bs width="1em" height="1em" name="x"></i-bs><small i18n>Reset filters</small>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -211,13 +211,15 @@ describe('TasksComponent', () => {
|
||||
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)
|
||||
fixture.detectChanges()
|
||||
|
||||
expect(text.join(' ')).toContain('All')
|
||||
expect(text.join(' ')).toContain('Needs attention2')
|
||||
expect(text.join(' ')).toContain('In progress2')
|
||||
expect(text.join(' ')).toContain('Recent completed2')
|
||||
const viewScope = fixture.debugElement.query(By.css('.task-view-scope'))
|
||||
const text = viewScope.nativeElement.textContent
|
||||
|
||||
expect(text).toContain('All')
|
||||
expect(text).toContain('Needs attention2')
|
||||
expect(text).toContain('In progress2')
|
||||
expect(text).toContain('Recent completed2')
|
||||
})
|
||||
|
||||
it('should filter visible sections by selected status', () => {
|
||||
@@ -260,13 +262,40 @@ describe('TasksComponent', () => {
|
||||
|
||||
component.resetFilters()
|
||||
|
||||
expect(component.selectedSection).toBe(ALL_TASK_SECTIONS)
|
||||
expect(component.selectedSection).toBe(TaskSection.InProgress)
|
||||
expect(component.selectedTaskType).toBe(ALL_FILTER_VALUE)
|
||||
expect(component.selectedTriggerSource).toBe(ALL_FILTER_VALUE)
|
||||
expect(component.filterText).toBe('')
|
||||
expect(component.isFiltered).toBe(false)
|
||||
})
|
||||
|
||||
it('should keep header controls focused on actions and auto refresh', () => {
|
||||
fixture.detectChanges()
|
||||
|
||||
const header = fixture.debugElement.query(By.css('pngx-page-header'))
|
||||
const headerText = header.nativeElement.textContent
|
||||
|
||||
expect(headerText).toContain('Dismiss visible')
|
||||
expect(headerText).toContain('Auto refresh')
|
||||
expect(headerText).not.toContain('All types')
|
||||
expect(headerText).not.toContain('All sources')
|
||||
expect(headerText).not.toContain('Reset filters')
|
||||
})
|
||||
|
||||
it('should render the view scope row above the filter bar', () => {
|
||||
fixture.detectChanges()
|
||||
|
||||
const controls = fixture.debugElement.query(By.css('.task-controls'))
|
||||
const controlChildren = controls.children
|
||||
|
||||
expect(controlChildren[0].nativeElement.className).toContain(
|
||||
'task-view-scope'
|
||||
)
|
||||
expect(controlChildren[1].nativeElement.className).toContain(
|
||||
'task-filter-bar'
|
||||
)
|
||||
})
|
||||
|
||||
it('should expose stable task type options and disable empty ones', () => {
|
||||
expect(component.taskTypeOptions.map((option) => option.value)).toContain(
|
||||
PaperlessTaskType.TrainClassifier
|
||||
@@ -387,6 +416,22 @@ describe('TasksComponent', () => {
|
||||
expect(dismissSpy).toHaveBeenCalledWith(new Set([467, 466]))
|
||||
})
|
||||
|
||||
it('should dismiss the currently visible scoped and filtered tasks', () => {
|
||||
component.setSection(TaskSection.InProgress)
|
||||
component.setTaskType(PaperlessTaskType.SanityCheck)
|
||||
component.setTriggerSource(PaperlessTaskTriggerSource.System)
|
||||
|
||||
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([461]))
|
||||
})
|
||||
|
||||
it('should support toggling a full section', () => {
|
||||
const toggleCheck = fixture.debugElement.query(
|
||||
By.css('#all-tasks-needs_attention')
|
||||
@@ -416,7 +461,7 @@ describe('TasksComponent', () => {
|
||||
|
||||
it('should filter tasks by file name', () => {
|
||||
const input = fixture.debugElement.query(
|
||||
By.css('pngx-page-header input[type=text]')
|
||||
By.css('.task-filter-bar input[type=text]')
|
||||
)
|
||||
input.nativeElement.value = '191092'
|
||||
input.nativeElement.dispatchEvent(new Event('input'))
|
||||
@@ -458,7 +503,7 @@ describe('TasksComponent', () => {
|
||||
component.setSection(TaskSection.NeedsAttention)
|
||||
component.filterTargetID = 1
|
||||
const input = fixture.debugElement.query(
|
||||
By.css('pngx-page-header input[type=text]')
|
||||
By.css('.task-filter-bar input[type=text]')
|
||||
)
|
||||
input.nativeElement.value = 'duplicate'
|
||||
input.nativeElement.dispatchEvent(new Event('input'))
|
||||
@@ -472,7 +517,7 @@ describe('TasksComponent', () => {
|
||||
|
||||
it('should support keyboard events for filtering', () => {
|
||||
const input = fixture.debugElement.query(
|
||||
By.css('pngx-page-header input[type=text]')
|
||||
By.css('.task-filter-bar input[type=text]')
|
||||
)
|
||||
input.nativeElement.value = '191092'
|
||||
input.nativeElement.dispatchEvent(
|
||||
@@ -484,4 +529,16 @@ describe('TasksComponent', () => {
|
||||
)
|
||||
expect(component.filterText).toEqual('')
|
||||
})
|
||||
|
||||
it('should keep clearing selection independent from resetting filters', () => {
|
||||
component.setTaskType(PaperlessTaskType.ConsumeFile)
|
||||
component.toggleSelected(tasks[0])
|
||||
expect(component.selectedTasks.size).toBe(1)
|
||||
|
||||
component.clearSelection()
|
||||
|
||||
expect(component.selectedTasks.size).toBe(0)
|
||||
expect(component.selectedTaskType).toBe(PaperlessTaskType.ConsumeFile)
|
||||
expect(component.isFiltered).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -217,13 +217,16 @@ export class TasksComponent
|
||||
|
||||
get isFiltered(): boolean {
|
||||
return (
|
||||
this.selectedSection !== ALL_TASK_SECTIONS ||
|
||||
this.selectedTaskType !== ALL_FILTER_VALUE ||
|
||||
this.selectedTriggerSource !== ALL_FILTER_VALUE ||
|
||||
this._filterText.length > 0
|
||||
)
|
||||
}
|
||||
|
||||
get hasScopedSectionView(): boolean {
|
||||
return this.selectedSection !== ALL_TASK_SECTIONS
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.tasksService.reload()
|
||||
timer(5000, 5000)
|
||||
@@ -441,7 +444,6 @@ export class TasksComponent
|
||||
}
|
||||
|
||||
public resetFilters() {
|
||||
this.selectedSection = ALL_TASK_SECTIONS
|
||||
this.selectedTaskType = ALL_FILTER_VALUE
|
||||
this.selectedTriggerSource = ALL_FILTER_VALUE
|
||||
this.resetFilter()
|
||||
|
||||
Reference in New Issue
Block a user