mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-07-30 18:27:45 -05:00
Enhancement: settings reorganization & improvements, separate admin section (#4251)
* Separate admin / manage sections * Move mail settings to its own component * Move users and groups to its own component * Move default permissions to its own settings tab * Unify list styling, add tour step, refactor components * Only patch saved views that have changed on settings save * Update messages.xlf * Remove unused methods in settings.component.ts * Drop admin section to bottom of sidebar, cleanup outdated, add docs link to dropdown * Better visually unify management list & other list pages
This commit is contained in:
@@ -1,33 +0,0 @@
|
||||
<pngx-page-header title="Consumption Templates">
|
||||
<button type="button" class="btn btn-sm btn-outline-primary ms-4" (click)="editTemplate()" *pngxIfPermissions="{ action: PermissionAction.Add, type: PermissionType.ConsumptionTemplate }">
|
||||
<svg class="sidebaricon me-1" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#plus-circle" />
|
||||
</svg>
|
||||
<ng-container i18n>Add Template</ng-container>
|
||||
</button>
|
||||
</pngx-page-header>
|
||||
|
||||
<table class="table table-striped align-middle border shadow-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" i18n>Name</th>
|
||||
<th scope="col" i18n>Sort order</th>
|
||||
<th scope="col" i18n>Document Sources</th>
|
||||
<th scope="col" i18n>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let template of templates">
|
||||
<td scope="row"><button class="btn btn-link p-0" type="button" (click)="editTemplate(template)" [disabled]="!permissionsService.currentUserCan(PermissionAction.Change, PermissionType.ConsumptionTemplate)">{{template.name}}</button></td>
|
||||
<td scope="row"><code>{{template.order}}</code></td>
|
||||
<td scope="row">{{getSourceList(template)}}</td>
|
||||
<td scope="row">
|
||||
<div class="btn-group">
|
||||
<button *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.ConsumptionTemplate }" class="btn btn-sm btn-primary" type="button" (click)="editTemplate(template)" i18n>Edit</button>
|
||||
<button *pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.ConsumptionTemplate }" class="btn btn-sm btn-outline-danger" type="button" (click)="deleteTemplate(template)" i18n>Delete</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div *ngIf="templates.length === 0" i18n>No templates defined.</div>
|
@@ -0,0 +1,44 @@
|
||||
<pngx-page-header title="Consumption Templates">
|
||||
<button type="button" class="btn btn-sm btn-outline-primary ms-4" (click)="editTemplate()" *pngxIfPermissions="{ action: PermissionAction.Add, type: PermissionType.ConsumptionTemplate }">
|
||||
<svg class="sidebaricon me-1" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#plus-circle" />
|
||||
</svg>
|
||||
<ng-container i18n>Add Template</ng-container>
|
||||
</button>
|
||||
</pngx-page-header>
|
||||
|
||||
<ul class="list-group">
|
||||
|
||||
<li class="list-group-item">
|
||||
<div class="row">
|
||||
<div class="col" i18n>Name</div>
|
||||
<div class="col" i18n>Sort order</div>
|
||||
<div class="col" i18n>Document Sources</div>
|
||||
<div class="col" i18n>Actions</div>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li *ngFor="let template of templates" class="list-group-item">
|
||||
<div class="row">
|
||||
<div class="col d-flex align-items-center"><button class="btn btn-link p-0" type="button" (click)="editTemplate(template)" [disabled]="!permissionsService.currentUserCan(PermissionAction.Change, PermissionType.ConsumptionTemplate)">{{template.name}}</button></div>
|
||||
<div class="col d-flex align-items-center"><code>{{template.order}}</code></div>
|
||||
<div class="col d-flex align-items-center">{{getSourceList(template)}}</div>
|
||||
<div class="col">
|
||||
<div class="btn-group">
|
||||
<button *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.ConsumptionTemplate }" class="btn btn-sm btn-outline-secondary" type="button" (click)="editTemplate(template)">
|
||||
<svg class="buttonicon-sm" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#pencil" />
|
||||
</svg> <ng-container i18n>Edit</ng-container>
|
||||
</button>
|
||||
<button *pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.ConsumptionTemplate }" class="btn btn-sm btn-outline-danger" type="button" (click)="deleteTemplate(template)">
|
||||
<svg class="buttonicon-sm" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#trash" />
|
||||
</svg> <ng-container i18n>Delete</ng-container>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div *ngIf="templates.length === 0" i18n>No templates defined.</div>
|
@@ -18,7 +18,7 @@ import { ConsumptionTemplateService } from 'src/app/services/rest/consumption-te
|
||||
import { ToastService } from 'src/app/services/toast.service'
|
||||
import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component'
|
||||
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
|
||||
import { ConsumptionTemplatesListComponent } from './consumption-templates-list.component'
|
||||
import { ConsumptionTemplatesComponent } from './consumption-templates.component'
|
||||
import { ConsumptionTemplateEditDialogComponent } from '../../common/edit-dialog/consumption-template-edit-dialog/consumption-template-edit-dialog.component'
|
||||
import { PermissionsService } from 'src/app/services/permissions.service'
|
||||
|
||||
@@ -48,8 +48,8 @@ const templates: PaperlessConsumptionTemplate[] = [
|
||||
]
|
||||
|
||||
describe('ConsumptionTemplatesComponent', () => {
|
||||
let component: ConsumptionTemplatesListComponent
|
||||
let fixture: ComponentFixture<ConsumptionTemplatesListComponent>
|
||||
let component: ConsumptionTemplatesComponent
|
||||
let fixture: ComponentFixture<ConsumptionTemplatesComponent>
|
||||
let consumptionTemplateService: ConsumptionTemplateService
|
||||
let modalService: NgbModal
|
||||
let toastService: ToastService
|
||||
@@ -57,7 +57,7 @@ describe('ConsumptionTemplatesComponent', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [
|
||||
ConsumptionTemplatesListComponent,
|
||||
ConsumptionTemplatesComponent,
|
||||
IfPermissionsDirective,
|
||||
PageHeaderComponent,
|
||||
ConfirmDialogComponent,
|
||||
@@ -92,7 +92,7 @@ describe('ConsumptionTemplatesComponent', () => {
|
||||
modalService = TestBed.inject(NgbModal)
|
||||
toastService = TestBed.inject(ToastService)
|
||||
|
||||
fixture = TestBed.createComponent(ConsumptionTemplatesListComponent)
|
||||
fixture = TestBed.createComponent(ConsumptionTemplatesComponent)
|
||||
component = fixture.componentInstance
|
||||
fixture.detectChanges()
|
||||
})
|
@@ -14,11 +14,11 @@ import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dial
|
||||
import { EditDialogMode } from '../../common/edit-dialog/edit-dialog.component'
|
||||
|
||||
@Component({
|
||||
selector: 'pngx-consumption-templates-list',
|
||||
templateUrl: './consumption-templates-list.component.html',
|
||||
styleUrls: ['./consumption-templates-list.component.scss'],
|
||||
selector: 'pngx-consumption-templates',
|
||||
templateUrl: './consumption-templates.component.html',
|
||||
styleUrls: ['./consumption-templates.component.scss'],
|
||||
})
|
||||
export class ConsumptionTemplatesListComponent
|
||||
export class ConsumptionTemplatesComponent
|
||||
extends ComponentWithPermissions
|
||||
implements OnInit
|
||||
{
|
@@ -1,25 +0,0 @@
|
||||
<pngx-page-header title="Logs" i18n-title>
|
||||
|
||||
</pngx-page-header>
|
||||
|
||||
<ul ngbNav #nav="ngbNav" [(activeId)]="activeLog" (activeIdChange)="reloadLogs()" class="nav-tabs">
|
||||
<li *ngFor="let logFile of logFiles" [ngbNavItem]="logFile">
|
||||
<a ngbNavLink>{{logFile}}.log</a>
|
||||
</li>
|
||||
<div *ngIf="isLoading && !logFiles.length" class="pb-2">
|
||||
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
|
||||
<ng-container i18n>Loading...</ng-container>
|
||||
</div>
|
||||
</ul>
|
||||
|
||||
<div [ngbNavOutlet]="nav" class="mt-2"></div>
|
||||
|
||||
<div class="bg-dark p-3 text-light font-monospace log-container" #logContainer>
|
||||
<div *ngIf="isLoading && logFiles.length">
|
||||
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
|
||||
<ng-container i18n>Loading...</ng-container>
|
||||
</div>
|
||||
<p
|
||||
class="m-0 p-0 log-entry-{{getLogLevel(log)}}"
|
||||
*ngFor="let log of logs">{{log}}</p>
|
||||
</div>
|
@@ -1,26 +0,0 @@
|
||||
.log-entry-10 {
|
||||
color: lightslategray !important;
|
||||
}
|
||||
|
||||
.log-entry-30 {
|
||||
color: yellow !important;
|
||||
}
|
||||
|
||||
.log-entry-40 {
|
||||
color: red !important;
|
||||
}
|
||||
|
||||
.log-entry-50 {
|
||||
color: lightcoral !important;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.log-container {
|
||||
overflow-y: scroll;
|
||||
height: calc(100vh - 200px);
|
||||
top: 70px;
|
||||
|
||||
p {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
}
|
@@ -1,71 +0,0 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||
import { LogService } from 'src/app/services/rest/log.service'
|
||||
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
|
||||
import { LogsComponent } from './logs.component'
|
||||
import { of, throwError } from 'rxjs'
|
||||
import { HttpClientTestingModule } from '@angular/common/http/testing'
|
||||
import { NgbModule, NgbNavLink } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { BrowserModule, By } from '@angular/platform-browser'
|
||||
|
||||
const paperless_logs = [
|
||||
'[2023-05-29 03:05:01,224] [DEBUG] [paperless.tasks] Training data unchanged.',
|
||||
'[2023-05-29 04:05:00,622] [DEBUG] [paperless.classifier] Gathering data from database...',
|
||||
'[2023-05-29 04:05:01,213] [DEBUG] [paperless.tasks] Training data unchanged.',
|
||||
'[2023-06-11 00:30:01,774] [INFO] [paperless.sanity_checker] Document contains no OCR data',
|
||||
'[2023-06-11 00:30:01,774] [WARNING] [paperless.sanity_checker] Made up',
|
||||
'[2023-06-11 00:30:01,774] [ERROR] [paperless.sanity_checker] Document contains no OCR data',
|
||||
'[2023-06-11 00:30:01,774] [CRITICAL] [paperless.sanity_checker] Document contains no OCR data',
|
||||
]
|
||||
const mail_logs = [
|
||||
'[2023-06-09 01:10:00,666] [DEBUG] [paperless_mail] Rule inbox@example.com.Incoming: Searching folder with criteria (SINCE 10-May-2023 UNSEEN)',
|
||||
'[2023-06-09 01:10:01,385] [DEBUG] [paperless_mail] Rule inbox@example.com.Incoming: Processed 3 matching mail(s)',
|
||||
]
|
||||
|
||||
describe('LogsComponent', () => {
|
||||
let component: LogsComponent
|
||||
let fixture: ComponentFixture<LogsComponent>
|
||||
let logService: LogService
|
||||
let logSpy
|
||||
|
||||
beforeEach(async () => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [LogsComponent, PageHeaderComponent],
|
||||
providers: [],
|
||||
imports: [HttpClientTestingModule, BrowserModule, NgbModule],
|
||||
}).compileComponents()
|
||||
|
||||
logService = TestBed.inject(LogService)
|
||||
jest.spyOn(logService, 'list').mockReturnValue(of(['paperless', 'mail']))
|
||||
logSpy = jest.spyOn(logService, 'get')
|
||||
logSpy.mockImplementation((id) => {
|
||||
return of(id === 'paperless' ? paperless_logs : mail_logs)
|
||||
})
|
||||
fixture = TestBed.createComponent(LogsComponent)
|
||||
component = fixture.componentInstance
|
||||
window.HTMLElement.prototype.scroll = function () {} // mock scroll
|
||||
fixture.detectChanges()
|
||||
})
|
||||
|
||||
it('should display logs with first log initially', () => {
|
||||
expect(logSpy).toHaveBeenCalledWith('paperless')
|
||||
fixture.detectChanges()
|
||||
expect(fixture.debugElement.nativeElement.textContent).toContain(
|
||||
paperless_logs[0]
|
||||
)
|
||||
})
|
||||
|
||||
it('should load log when tab clicked', () => {
|
||||
fixture.debugElement
|
||||
.queryAll(By.directive(NgbNavLink))[1]
|
||||
.nativeElement.dispatchEvent(new MouseEvent('click'))
|
||||
expect(logSpy).toHaveBeenCalledWith('mail')
|
||||
})
|
||||
|
||||
it('should handle error with no logs', () => {
|
||||
logSpy.mockReturnValueOnce(
|
||||
throwError(() => new Error('error getting logs'))
|
||||
)
|
||||
component.reloadLogs()
|
||||
expect(component.logs).toHaveLength(0)
|
||||
})
|
||||
})
|
@@ -1,94 +0,0 @@
|
||||
import {
|
||||
Component,
|
||||
ElementRef,
|
||||
OnInit,
|
||||
AfterViewChecked,
|
||||
ViewChild,
|
||||
OnDestroy,
|
||||
} from '@angular/core'
|
||||
import { Subject, takeUntil } from 'rxjs'
|
||||
import { LogService } from 'src/app/services/rest/log.service'
|
||||
|
||||
@Component({
|
||||
selector: 'pngx-logs',
|
||||
templateUrl: './logs.component.html',
|
||||
styleUrls: ['./logs.component.scss'],
|
||||
})
|
||||
export class LogsComponent implements OnInit, AfterViewChecked, OnDestroy {
|
||||
constructor(private logService: LogService) {}
|
||||
|
||||
public logs: string[] = []
|
||||
|
||||
public logFiles: string[] = []
|
||||
|
||||
public activeLog: string
|
||||
|
||||
private unsubscribeNotifier: Subject<any> = new Subject()
|
||||
|
||||
public isLoading: boolean = false
|
||||
|
||||
@ViewChild('logContainer') logContainer: ElementRef
|
||||
|
||||
ngOnInit(): void {
|
||||
this.isLoading = true
|
||||
this.logService
|
||||
.list()
|
||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||
.subscribe((result) => {
|
||||
this.logFiles = result
|
||||
this.isLoading = false
|
||||
if (this.logFiles.length > 0) {
|
||||
this.activeLog = this.logFiles[0]
|
||||
this.reloadLogs()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
ngAfterViewChecked() {
|
||||
this.scrollToBottom()
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.unsubscribeNotifier.next(true)
|
||||
this.unsubscribeNotifier.complete()
|
||||
}
|
||||
|
||||
reloadLogs() {
|
||||
this.isLoading = true
|
||||
this.logService
|
||||
.get(this.activeLog)
|
||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||
.subscribe({
|
||||
next: (result) => {
|
||||
this.logs = result
|
||||
this.isLoading = false
|
||||
},
|
||||
error: () => {
|
||||
this.logs = []
|
||||
this.isLoading = false
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
getLogLevel(log: string) {
|
||||
if (log.indexOf('[DEBUG]') != -1) {
|
||||
return 10
|
||||
} else if (log.indexOf('[WARNING]') != -1) {
|
||||
return 30
|
||||
} else if (log.indexOf('[ERROR]') != -1) {
|
||||
return 40
|
||||
} else if (log.indexOf('[CRITICAL]') != -1) {
|
||||
return 50
|
||||
} else {
|
||||
return 20
|
||||
}
|
||||
}
|
||||
|
||||
scrollToBottom(): void {
|
||||
this.logContainer?.nativeElement.scroll({
|
||||
top: this.logContainer.nativeElement.scrollHeight,
|
||||
left: 0,
|
||||
behavior: 'auto',
|
||||
})
|
||||
}
|
||||
}
|
107
src-ui/src/app/components/manage/mail/mail.component.html
Normal file
107
src-ui/src/app/components/manage/mail/mail.component.html
Normal file
@@ -0,0 +1,107 @@
|
||||
<pngx-page-header title="Mail Settings" i18n-title>
|
||||
</pngx-page-header>
|
||||
|
||||
<ng-container *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.MailAccount }">
|
||||
<h4>
|
||||
<ng-container i18n>Mail accounts</ng-container>
|
||||
<button type="button" class="btn btn-sm btn-outline-primary ms-4" (click)="editMailAccount()" *pngxIfPermissions="{ action: PermissionAction.Add, type: PermissionType.MailAccount }">
|
||||
<svg class="sidebaricon me-1" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#plus-circle" />
|
||||
</svg>
|
||||
<ng-container i18n>Add Account</ng-container>
|
||||
</button>
|
||||
</h4>
|
||||
<ul class="list-group">
|
||||
|
||||
<li class="list-group-item">
|
||||
<div class="row">
|
||||
<div class="col" i18n>Name</div>
|
||||
<div class="col" i18n>Server</div>
|
||||
<div class="col" i18n>Actions</div>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li *ngFor="let account of mailAccounts" class="list-group-item">
|
||||
<div class="row">
|
||||
<div class="col d-flex align-items-center"><button class="btn btn-link p-0" type="button" (click)="editMailAccount(account)" [disabled]="!permissionsService.currentUserCan(PermissionAction.Change, PermissionType.MailAccount)">{{account.name}}</button></div>
|
||||
<div class="col d-flex align-items-center">{{account.imap_server}}</div>
|
||||
<div class="col">
|
||||
<div class="btn-group">
|
||||
<button *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.MailAccount }" [disabled]="!userCanEdit(account)" class="btn btn-sm btn-outline-secondary" type="button" (click)="editMailAccount(account)">
|
||||
<svg class="buttonicon-sm" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#pencil" />
|
||||
</svg> <ng-container i18n>Edit</ng-container>
|
||||
</button>
|
||||
<button *pngxIfOwner="account" class="btn btn-sm btn-outline-secondary" type="button" (click)="editPermissions(account)">
|
||||
<svg class="buttonicon-sm" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#person-lock" />
|
||||
</svg> <ng-container i18n>Permissions</ng-container>
|
||||
</button>
|
||||
<button *pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.MailAccount }" [disabled]="!userIsOwner(account)" class="btn btn-sm btn-outline-danger" type="button" (click)="deleteMailAccount(account)">
|
||||
<svg class="buttonicon-sm" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#trash" />
|
||||
</svg> <ng-container i18n>Delete</ng-container>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<div *ngIf="mailAccounts.length === 0" i18n>No mail accounts defined.</div>
|
||||
</ul>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.MailRule }">
|
||||
<h4 class="mt-4">
|
||||
<ng-container i18n>Mail rules</ng-container>
|
||||
<button type="button" class="btn btn-sm btn-outline-primary ms-4" (click)="editMailRule()" *pngxIfPermissions="{ action: PermissionAction.Add, type: PermissionType.MailRule }">
|
||||
<svg class="sidebaricon me-1" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#plus-circle" />
|
||||
</svg>
|
||||
<ng-container i18n>Add Rule</ng-container>
|
||||
</button>
|
||||
</h4>
|
||||
<ul class="list-group">
|
||||
|
||||
<li class="list-group-item">
|
||||
<div class="row">
|
||||
<div class="col" i18n>Name</div>
|
||||
<div class="col" i18n>Account</div>
|
||||
<div class="col" i18n>Actions</div>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li *ngFor="let rule of mailRules" class="list-group-item">
|
||||
<div class="row">
|
||||
<div class="col d-flex align-items-center"><button class="btn btn-link p-0" type="button" (click)="editMailRule(rule)" [disabled]="!permissionsService.currentUserCan(PermissionAction.Change, PermissionType.MailRule)">{{rule.name}}</button></div>
|
||||
<div class="col d-flex align-items-center">{{(mailAccountService.getCached(rule.account) | async)?.name}}</div>
|
||||
<div class="col">
|
||||
<div class="btn-group">
|
||||
<button *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.MailRule }" [disabled]="!userCanEdit(rule)" class="btn btn-sm btn-outline-secondary" type="button" (click)="editMailRule(rule)">
|
||||
<svg class="buttonicon-sm" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#pencil" />
|
||||
</svg> <ng-container i18n>Edit</ng-container>
|
||||
</button>
|
||||
<button *pngxIfOwner="rule" class="btn btn-sm btn-outline-secondary" type="button" (click)="editPermissions(rule)">
|
||||
<svg class="buttonicon-sm" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#person-lock" />
|
||||
</svg> <ng-container i18n>Permissions</ng-container>
|
||||
</button>
|
||||
<button *pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.MailRule }" [disabled]="!userIsOwner(rule)" class="btn btn-sm btn-outline-danger" type="button" (click)="deleteMailRule(rule)">
|
||||
<svg class="buttonicon-sm" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#trash" />
|
||||
</svg> <ng-container i18n>Delete</ng-container>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<div *ngIf="mailRules.length === 0" i18n>No mail rules defined.</div>
|
||||
</ul>
|
||||
</ng-container>
|
||||
|
||||
<div *ngIf="!mailAccounts || !mailRules">
|
||||
<div class="spinner-border spinner-border-sm fw-normal ms-2 me-auto" role="status"></div>
|
||||
<div class="visually-hidden" i18n>Loading...</div>
|
||||
</div>
|
304
src-ui/src/app/components/manage/mail/mail.component.spec.ts
Normal file
304
src-ui/src/app/components/manage/mail/mail.component.spec.ts
Normal file
@@ -0,0 +1,304 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||
|
||||
import { MailComponent } from './mail.component'
|
||||
import { DatePipe } from '@angular/common'
|
||||
import { HttpClientTestingModule } from '@angular/common/http/testing'
|
||||
import { RouterTestingModule } from '@angular/router/testing'
|
||||
import {
|
||||
NgbModule,
|
||||
NgbAlertModule,
|
||||
NgbModalRef,
|
||||
NgbModal,
|
||||
} from '@ng-bootstrap/ng-bootstrap'
|
||||
import { NgSelectModule } from '@ng-select/ng-select'
|
||||
import { of, throwError } from 'rxjs'
|
||||
import { routes } from 'src/app/app-routing.module'
|
||||
import { PaperlessMailAccount } from 'src/app/data/paperless-mail-account'
|
||||
import { PaperlessMailRule } from 'src/app/data/paperless-mail-rule'
|
||||
import { IfOwnerDirective } from 'src/app/directives/if-owner.directive'
|
||||
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
||||
import { PermissionsGuard } from 'src/app/guards/permissions.guard'
|
||||
import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe'
|
||||
import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
|
||||
import { MailAccountService } from 'src/app/services/rest/mail-account.service'
|
||||
import { MailRuleService } from 'src/app/services/rest/mail-rule.service'
|
||||
import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component'
|
||||
import { MailAccountEditDialogComponent } from '../../common/edit-dialog/mail-account-edit-dialog/mail-account-edit-dialog.component'
|
||||
import { MailRuleEditDialogComponent } from '../../common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component'
|
||||
import { CheckComponent } from '../../common/input/check/check.component'
|
||||
import { NumberComponent } from '../../common/input/number/number.component'
|
||||
import { PasswordComponent } from '../../common/input/password/password.component'
|
||||
import { PermissionsFormComponent } from '../../common/input/permissions/permissions-form/permissions-form.component'
|
||||
import { PermissionsGroupComponent } from '../../common/input/permissions/permissions-group/permissions-group.component'
|
||||
import { PermissionsUserComponent } from '../../common/input/permissions/permissions-user/permissions-user.component'
|
||||
import { SelectComponent } from '../../common/input/select/select.component'
|
||||
import { TextComponent } from '../../common/input/text/text.component'
|
||||
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
|
||||
import { PermissionsDialogComponent } from '../../common/permissions-dialog/permissions-dialog.component'
|
||||
import { PermissionsService } from 'src/app/services/permissions.service'
|
||||
import { ToastService } from 'src/app/services/toast.service'
|
||||
import { TagsComponent } from '../../common/input/tags/tags.component'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { EditDialogMode } from '../../common/edit-dialog/edit-dialog.component'
|
||||
|
||||
const mailAccounts = [
|
||||
{ id: 1, name: 'account1' },
|
||||
{ id: 2, name: 'account2' },
|
||||
]
|
||||
const mailRules = [
|
||||
{ id: 1, name: 'rule1', owner: 1, account: 1 },
|
||||
{ id: 2, name: 'rule2', owner: 2, account: 2 },
|
||||
]
|
||||
|
||||
describe('MailComponent', () => {
|
||||
let component: MailComponent
|
||||
let fixture: ComponentFixture<MailComponent>
|
||||
let mailAccountService: MailAccountService
|
||||
let mailRuleService: MailRuleService
|
||||
let modalService: NgbModal
|
||||
let toastService: ToastService
|
||||
let permissionsService: PermissionsService
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [
|
||||
MailComponent,
|
||||
PageHeaderComponent,
|
||||
IfPermissionsDirective,
|
||||
CustomDatePipe,
|
||||
ConfirmDialogComponent,
|
||||
CheckComponent,
|
||||
SafeHtmlPipe,
|
||||
SelectComponent,
|
||||
TextComponent,
|
||||
PasswordComponent,
|
||||
NumberComponent,
|
||||
MailAccountEditDialogComponent,
|
||||
MailRuleEditDialogComponent,
|
||||
IfOwnerDirective,
|
||||
TagsComponent,
|
||||
PermissionsUserComponent,
|
||||
PermissionsGroupComponent,
|
||||
PermissionsDialogComponent,
|
||||
PermissionsFormComponent,
|
||||
],
|
||||
providers: [CustomDatePipe, DatePipe, PermissionsGuard],
|
||||
imports: [
|
||||
NgbModule,
|
||||
HttpClientTestingModule,
|
||||
RouterTestingModule.withRoutes(routes),
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
NgbAlertModule,
|
||||
NgSelectModule,
|
||||
],
|
||||
}).compileComponents()
|
||||
|
||||
mailAccountService = TestBed.inject(MailAccountService)
|
||||
mailRuleService = TestBed.inject(MailRuleService)
|
||||
modalService = TestBed.inject(NgbModal)
|
||||
toastService = TestBed.inject(ToastService)
|
||||
permissionsService = TestBed.inject(PermissionsService)
|
||||
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
|
||||
jest
|
||||
.spyOn(permissionsService, 'currentUserHasObjectPermissions')
|
||||
.mockReturnValue(true)
|
||||
jest
|
||||
.spyOn(permissionsService, 'currentUserOwnsObject')
|
||||
.mockReturnValue(true)
|
||||
|
||||
fixture = TestBed.createComponent(MailComponent)
|
||||
component = fixture.componentInstance
|
||||
fixture.detectChanges()
|
||||
})
|
||||
|
||||
function completeSetup(excludeService = null) {
|
||||
if (excludeService !== mailAccountService) {
|
||||
jest.spyOn(mailAccountService, 'listAll').mockReturnValue(
|
||||
of({
|
||||
all: mailAccounts.map((a) => a.id),
|
||||
count: mailAccounts.length,
|
||||
results: (mailAccounts as PaperlessMailAccount[]).concat([]),
|
||||
})
|
||||
)
|
||||
}
|
||||
if (excludeService !== mailRuleService) {
|
||||
jest.spyOn(mailRuleService, 'listAll').mockReturnValue(
|
||||
of({
|
||||
all: mailRules.map((r) => r.id),
|
||||
count: mailRules.length,
|
||||
results: (mailRules as PaperlessMailRule[]).concat([]),
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
fixture = TestBed.createComponent(MailComponent)
|
||||
component = fixture.componentInstance
|
||||
fixture.detectChanges()
|
||||
}
|
||||
|
||||
it('should show errors on load if load mailAccounts failure', () => {
|
||||
const toastErrorSpy = jest.spyOn(toastService, 'showError')
|
||||
jest
|
||||
.spyOn(mailAccountService, 'listAll')
|
||||
.mockImplementation(() =>
|
||||
throwError(() => new Error('failed to load mail accounts'))
|
||||
)
|
||||
completeSetup(mailAccountService)
|
||||
expect(toastErrorSpy).toBeCalled()
|
||||
})
|
||||
|
||||
it('should show errors on load if load mailRules failure', () => {
|
||||
const toastErrorSpy = jest.spyOn(toastService, 'showError')
|
||||
jest
|
||||
.spyOn(mailRuleService, 'listAll')
|
||||
.mockImplementation(() =>
|
||||
throwError(() => new Error('failed to load mail rules'))
|
||||
)
|
||||
completeSetup(mailRuleService)
|
||||
expect(toastErrorSpy).toBeCalled()
|
||||
})
|
||||
|
||||
it('should support edit / create mail account, show error if needed', () => {
|
||||
completeSetup()
|
||||
let modal: NgbModalRef
|
||||
modalService.activeInstances.subscribe((refs) => (modal = refs[0]))
|
||||
component.editMailAccount(mailAccounts[0] as PaperlessMailAccount)
|
||||
let editDialog = modal.componentInstance as MailAccountEditDialogComponent
|
||||
const toastErrorSpy = jest.spyOn(toastService, 'showError')
|
||||
const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
|
||||
editDialog.failed.emit()
|
||||
expect(toastErrorSpy).toBeCalled()
|
||||
editDialog.succeeded.emit(mailAccounts[0])
|
||||
expect(toastInfoSpy).toHaveBeenCalledWith(
|
||||
`Saved account "${mailAccounts[0].name}".`
|
||||
)
|
||||
editDialog.cancel()
|
||||
component.editMailAccount()
|
||||
})
|
||||
|
||||
it('should support delete mail account, show error if needed', () => {
|
||||
completeSetup()
|
||||
let modal: NgbModalRef
|
||||
modalService.activeInstances.subscribe((refs) => (modal = refs[0]))
|
||||
component.deleteMailAccount(mailAccounts[0] as PaperlessMailAccount)
|
||||
const deleteDialog = modal.componentInstance as ConfirmDialogComponent
|
||||
const deleteSpy = jest.spyOn(mailAccountService, 'delete')
|
||||
const toastErrorSpy = jest.spyOn(toastService, 'showError')
|
||||
const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
|
||||
const listAllSpy = jest.spyOn(mailAccountService, 'listAll')
|
||||
deleteSpy.mockReturnValueOnce(
|
||||
throwError(() => new Error('error deleting mail account'))
|
||||
)
|
||||
deleteDialog.confirm()
|
||||
expect(toastErrorSpy).toBeCalled()
|
||||
deleteSpy.mockReturnValueOnce(of(true))
|
||||
deleteDialog.confirm()
|
||||
expect(listAllSpy).toHaveBeenCalled()
|
||||
expect(toastInfoSpy).toHaveBeenCalledWith('Deleted mail account')
|
||||
})
|
||||
|
||||
it('should support edit / create mail rule, show error if needed', () => {
|
||||
completeSetup()
|
||||
let modal: NgbModalRef
|
||||
modalService.activeInstances.subscribe((refs) => (modal = refs[0]))
|
||||
component.editMailRule(mailRules[0] as PaperlessMailRule)
|
||||
const editDialog = modal.componentInstance as MailRuleEditDialogComponent
|
||||
const toastErrorSpy = jest.spyOn(toastService, 'showError')
|
||||
const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
|
||||
editDialog.failed.emit()
|
||||
expect(toastErrorSpy).toBeCalled()
|
||||
editDialog.succeeded.emit(mailRules[0])
|
||||
expect(toastInfoSpy).toHaveBeenCalledWith(
|
||||
`Saved rule "${mailRules[0].name}".`
|
||||
)
|
||||
editDialog.cancel()
|
||||
component.editMailRule()
|
||||
})
|
||||
|
||||
it('should support delete mail rule, show error if needed', () => {
|
||||
completeSetup()
|
||||
let modal: NgbModalRef
|
||||
modalService.activeInstances.subscribe((refs) => (modal = refs[0]))
|
||||
component.deleteMailRule(mailRules[0] as PaperlessMailRule)
|
||||
const deleteDialog = modal.componentInstance as ConfirmDialogComponent
|
||||
const deleteSpy = jest.spyOn(mailRuleService, 'delete')
|
||||
const toastErrorSpy = jest.spyOn(toastService, 'showError')
|
||||
const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
|
||||
const listAllSpy = jest.spyOn(mailRuleService, 'listAll')
|
||||
deleteSpy.mockReturnValueOnce(
|
||||
throwError(() => new Error('error deleting mail rule'))
|
||||
)
|
||||
deleteDialog.confirm()
|
||||
expect(toastErrorSpy).toBeCalled()
|
||||
deleteSpy.mockReturnValueOnce(of(true))
|
||||
deleteDialog.confirm()
|
||||
expect(listAllSpy).toHaveBeenCalled()
|
||||
expect(toastInfoSpy).toHaveBeenCalledWith('Deleted mail rule')
|
||||
})
|
||||
|
||||
it('should support edit permissions on mail rule objects', () => {
|
||||
completeSetup()
|
||||
const perms = {
|
||||
owner: 99,
|
||||
set_permissions: {
|
||||
view: {
|
||||
users: [1],
|
||||
groups: [2],
|
||||
},
|
||||
change: {
|
||||
users: [3],
|
||||
groups: [4],
|
||||
},
|
||||
},
|
||||
}
|
||||
let modal: NgbModalRef
|
||||
modalService.activeInstances.subscribe((refs) => (modal = refs[0]))
|
||||
const toastErrorSpy = jest.spyOn(toastService, 'showError')
|
||||
const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
|
||||
const rulePatchSpy = jest.spyOn(mailRuleService, 'patch')
|
||||
component.editPermissions(mailRules[0] as PaperlessMailRule)
|
||||
expect(modal).not.toBeUndefined()
|
||||
let dialog = modal.componentInstance as PermissionsDialogComponent
|
||||
expect(dialog.object).toEqual(mailRules[0])
|
||||
|
||||
rulePatchSpy.mockReturnValueOnce(
|
||||
throwError(() => new Error('error saving perms'))
|
||||
)
|
||||
dialog.confirmClicked.emit(perms)
|
||||
expect(rulePatchSpy).toHaveBeenCalled()
|
||||
expect(toastErrorSpy).toHaveBeenCalled()
|
||||
rulePatchSpy.mockReturnValueOnce(of(mailRules[0] as PaperlessMailRule))
|
||||
dialog.confirmClicked.emit(perms)
|
||||
expect(toastInfoSpy).toHaveBeenCalledWith('Permissions updated')
|
||||
|
||||
modalService.dismissAll()
|
||||
})
|
||||
|
||||
it('should support edit permissions on mail account objects', () => {
|
||||
completeSetup()
|
||||
const perms = {
|
||||
owner: 99,
|
||||
set_permissions: {
|
||||
view: {
|
||||
users: [1],
|
||||
groups: [2],
|
||||
},
|
||||
change: {
|
||||
users: [3],
|
||||
groups: [4],
|
||||
},
|
||||
},
|
||||
}
|
||||
let modal: NgbModalRef
|
||||
modalService.activeInstances.subscribe((refs) => (modal = refs[0]))
|
||||
const accountPatchSpy = jest.spyOn(mailAccountService, 'patch')
|
||||
component.editPermissions(mailAccounts[0] as PaperlessMailAccount)
|
||||
expect(modal).not.toBeUndefined()
|
||||
let dialog = modal.componentInstance as PermissionsDialogComponent
|
||||
expect(dialog.object).toEqual(mailAccounts[0])
|
||||
dialog = modal.componentInstance as PermissionsDialogComponent
|
||||
dialog.confirmClicked.emit(perms)
|
||||
expect(accountPatchSpy).toHaveBeenCalled()
|
||||
})
|
||||
})
|
232
src-ui/src/app/components/manage/mail/mail.component.ts
Normal file
232
src-ui/src/app/components/manage/mail/mail.component.ts
Normal file
@@ -0,0 +1,232 @@
|
||||
import { Component, OnInit, OnDestroy } from '@angular/core'
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { Subject, first, takeUntil } from 'rxjs'
|
||||
import { ObjectWithPermissions } from 'src/app/data/object-with-permissions'
|
||||
import { PaperlessMailAccount } from 'src/app/data/paperless-mail-account'
|
||||
import { PaperlessMailRule } from 'src/app/data/paperless-mail-rule'
|
||||
import {
|
||||
PermissionsService,
|
||||
PermissionAction,
|
||||
} from 'src/app/services/permissions.service'
|
||||
import { AbstractPaperlessService } from 'src/app/services/rest/abstract-paperless-service'
|
||||
import { MailAccountService } from 'src/app/services/rest/mail-account.service'
|
||||
import { MailRuleService } from 'src/app/services/rest/mail-rule.service'
|
||||
import { ToastService } from 'src/app/services/toast.service'
|
||||
import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component'
|
||||
import { EditDialogMode } from '../../common/edit-dialog/edit-dialog.component'
|
||||
import { MailAccountEditDialogComponent } from '../../common/edit-dialog/mail-account-edit-dialog/mail-account-edit-dialog.component'
|
||||
import { MailRuleEditDialogComponent } from '../../common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component'
|
||||
import { PermissionsDialogComponent } from '../../common/permissions-dialog/permissions-dialog.component'
|
||||
import { ComponentWithPermissions } from '../../with-permissions/with-permissions.component'
|
||||
|
||||
@Component({
|
||||
selector: 'pngx-mail',
|
||||
templateUrl: './mail.component.html',
|
||||
styleUrls: ['./mail.component.scss'],
|
||||
})
|
||||
export class MailComponent
|
||||
extends ComponentWithPermissions
|
||||
implements OnInit, OnDestroy
|
||||
{
|
||||
mailAccounts: PaperlessMailAccount[] = []
|
||||
mailRules: PaperlessMailRule[] = []
|
||||
|
||||
unsubscribeNotifier: Subject<any> = new Subject()
|
||||
|
||||
constructor(
|
||||
public mailAccountService: MailAccountService,
|
||||
public mailRuleService: MailRuleService,
|
||||
private toastService: ToastService,
|
||||
private modalService: NgbModal,
|
||||
public permissionsService: PermissionsService
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.mailAccountService
|
||||
.listAll(null, null, { full_perms: true })
|
||||
.pipe(first(), takeUntil(this.unsubscribeNotifier))
|
||||
.subscribe({
|
||||
next: (r) => {
|
||||
this.mailAccounts = r.results
|
||||
},
|
||||
error: (e) => {
|
||||
this.toastService.showError(
|
||||
$localize`Error retrieving mail accounts`,
|
||||
e
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
this.mailRuleService
|
||||
.listAll(null, null, { full_perms: true })
|
||||
.pipe(first(), takeUntil(this.unsubscribeNotifier))
|
||||
.subscribe({
|
||||
next: (r) => {
|
||||
this.mailRules = r.results
|
||||
},
|
||||
error: (e) => {
|
||||
this.toastService.showError($localize`Error retrieving mail rules`, e)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.unsubscribeNotifier.next(true)
|
||||
}
|
||||
|
||||
editMailAccount(account: PaperlessMailAccount = null) {
|
||||
const modal = this.modalService.open(MailAccountEditDialogComponent, {
|
||||
backdrop: 'static',
|
||||
size: 'xl',
|
||||
})
|
||||
modal.componentInstance.dialogMode = account
|
||||
? EditDialogMode.EDIT
|
||||
: EditDialogMode.CREATE
|
||||
modal.componentInstance.object = account
|
||||
modal.componentInstance.succeeded
|
||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||
.subscribe((newMailAccount) => {
|
||||
this.toastService.showInfo(
|
||||
$localize`Saved account "${newMailAccount.name}".`
|
||||
)
|
||||
this.mailAccountService.clearCache()
|
||||
this.mailAccountService
|
||||
.listAll(null, null, { full_perms: true })
|
||||
.subscribe((r) => {
|
||||
this.mailAccounts = r.results
|
||||
})
|
||||
})
|
||||
modal.componentInstance.failed
|
||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||
.subscribe((e) => {
|
||||
this.toastService.showError($localize`Error saving account.`, e)
|
||||
})
|
||||
}
|
||||
|
||||
deleteMailAccount(account: PaperlessMailAccount) {
|
||||
const modal = this.modalService.open(ConfirmDialogComponent, {
|
||||
backdrop: 'static',
|
||||
})
|
||||
modal.componentInstance.title = $localize`Confirm delete mail account`
|
||||
modal.componentInstance.messageBold = $localize`This operation will permanently delete this mail account.`
|
||||
modal.componentInstance.message = $localize`This operation cannot be undone.`
|
||||
modal.componentInstance.btnClass = 'btn-danger'
|
||||
modal.componentInstance.btnCaption = $localize`Proceed`
|
||||
modal.componentInstance.confirmClicked.subscribe(() => {
|
||||
modal.componentInstance.buttonsEnabled = false
|
||||
this.mailAccountService.delete(account).subscribe({
|
||||
next: () => {
|
||||
modal.close()
|
||||
this.toastService.showInfo($localize`Deleted mail account`)
|
||||
this.mailAccountService.clearCache()
|
||||
this.mailAccountService
|
||||
.listAll(null, null, { full_perms: true })
|
||||
.subscribe((r) => {
|
||||
this.mailAccounts = r.results
|
||||
})
|
||||
},
|
||||
error: (e) => {
|
||||
this.toastService.showError(
|
||||
$localize`Error deleting mail account.`,
|
||||
e
|
||||
)
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
editMailRule(rule: PaperlessMailRule = null) {
|
||||
const modal = this.modalService.open(MailRuleEditDialogComponent, {
|
||||
backdrop: 'static',
|
||||
size: 'xl',
|
||||
})
|
||||
modal.componentInstance.dialogMode = rule
|
||||
? EditDialogMode.EDIT
|
||||
: EditDialogMode.CREATE
|
||||
modal.componentInstance.object = rule
|
||||
modal.componentInstance.succeeded
|
||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||
.subscribe((newMailRule) => {
|
||||
this.toastService.showInfo($localize`Saved rule "${newMailRule.name}".`)
|
||||
this.mailRuleService.clearCache()
|
||||
this.mailRuleService
|
||||
.listAll(null, null, { full_perms: true })
|
||||
.subscribe((r) => {
|
||||
this.mailRules = r.results
|
||||
})
|
||||
})
|
||||
modal.componentInstance.failed
|
||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||
.subscribe((e) => {
|
||||
this.toastService.showError($localize`Error saving rule.`, e)
|
||||
})
|
||||
}
|
||||
|
||||
deleteMailRule(rule: PaperlessMailRule) {
|
||||
const modal = this.modalService.open(ConfirmDialogComponent, {
|
||||
backdrop: 'static',
|
||||
})
|
||||
modal.componentInstance.title = $localize`Confirm delete mail rule`
|
||||
modal.componentInstance.messageBold = $localize`This operation will permanently delete this mail rule.`
|
||||
modal.componentInstance.message = $localize`This operation cannot be undone.`
|
||||
modal.componentInstance.btnClass = 'btn-danger'
|
||||
modal.componentInstance.btnCaption = $localize`Proceed`
|
||||
modal.componentInstance.confirmClicked.subscribe(() => {
|
||||
modal.componentInstance.buttonsEnabled = false
|
||||
this.mailRuleService.delete(rule).subscribe({
|
||||
next: () => {
|
||||
modal.close()
|
||||
this.toastService.showInfo($localize`Deleted mail rule`)
|
||||
this.mailRuleService.clearCache()
|
||||
this.mailRuleService
|
||||
.listAll(null, null, { full_perms: true })
|
||||
.subscribe((r) => {
|
||||
this.mailRules = r.results
|
||||
})
|
||||
},
|
||||
error: (e) => {
|
||||
this.toastService.showError($localize`Error deleting mail rule.`, e)
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
editPermissions(object: PaperlessMailRule | PaperlessMailAccount) {
|
||||
const modal = this.modalService.open(PermissionsDialogComponent, {
|
||||
backdrop: 'static',
|
||||
})
|
||||
const dialog: PermissionsDialogComponent =
|
||||
modal.componentInstance as PermissionsDialogComponent
|
||||
dialog.object = object
|
||||
modal.componentInstance.confirmClicked.subscribe((permissions) => {
|
||||
modal.componentInstance.buttonsEnabled = false
|
||||
const service: AbstractPaperlessService<
|
||||
PaperlessMailRule | PaperlessMailAccount
|
||||
> = 'account' in object ? this.mailRuleService : this.mailAccountService
|
||||
object.owner = permissions['owner']
|
||||
object['set_permissions'] = permissions['set_permissions']
|
||||
service.patch(object).subscribe({
|
||||
next: () => {
|
||||
this.toastService.showInfo($localize`Permissions updated`)
|
||||
modal.close()
|
||||
},
|
||||
error: (e) => {
|
||||
this.toastService.showError($localize`Error updating permissions`, e)
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
userCanEdit(obj: ObjectWithPermissions): boolean {
|
||||
return this.permissionsService.currentUserHasObjectPermissions(
|
||||
PermissionAction.Change,
|
||||
obj
|
||||
)
|
||||
}
|
||||
|
||||
userIsOwner(obj: ObjectWithPermissions): boolean {
|
||||
return this.permissionsService.currentUserOwnsObject(obj)
|
||||
}
|
||||
}
|
@@ -9,7 +9,12 @@
|
||||
<use xlink:href="assets/bootstrap-icons.svg#person-fill-lock" />
|
||||
</svg> <ng-container i18n>Permissions</ng-container>
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-primary" (click)="openCreateDialog()" *pngxIfPermissions="{ action: PermissionAction.Add, type: permissionType }" i18n>Create</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-primary" (click)="openCreateDialog()" *pngxIfPermissions="{ action: PermissionAction.Add, type: permissionType }" i18n>
|
||||
<svg class="sidebaricon me-1" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#plus-circle" />
|
||||
</svg>
|
||||
Create
|
||||
</button>
|
||||
</pngx-page-header>
|
||||
|
||||
<div class="row mb-3">
|
||||
@@ -23,20 +28,21 @@
|
||||
<ngb-pagination class="col-auto" [pageSize]="25" [collectionSize]="collectionSize" [(page)]="page" [maxSize]="5" (pageChange)="reloadData()" size="sm" aria-label="Pagination"></ngb-pagination>
|
||||
</div>
|
||||
|
||||
<table class="table table-striped align-middle border shadow-sm">
|
||||
<div class="card border mb-3">
|
||||
<table class="table table-striped align-middle shadow-sm mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">
|
||||
<div class="form-check">
|
||||
<div class="form-check m-0 ms-2 me-n2">
|
||||
<input type="checkbox" class="form-check-input" id="all-objects" [disabled]="data.length === 0" (click)="toggleAll($event); $event.stopPropagation();">
|
||||
<label class="form-check-label" for="all-objects"></label>
|
||||
</div>
|
||||
</th>
|
||||
<th scope="col" pngxSortable="name" [currentSortField]="sortField" [currentSortReverse]="sortReverse" (sort)="onSort($event)" i18n>Name</th>
|
||||
<th scope="col" class="d-none d-sm-table-cell" pngxSortable="matching_algorithm" [currentSortField]="sortField" [currentSortReverse]="sortReverse" (sort)="onSort($event)" i18n>Matching</th>
|
||||
<th scope="col" pngxSortable="document_count" [currentSortField]="sortField" [currentSortReverse]="sortReverse" (sort)="onSort($event)" i18n>Document count</th>
|
||||
<th scope="col" *ngFor="let column of extraColumns" pngxSortable="{{column.key}}" [currentSortField]="sortField" [currentSortReverse]="sortReverse" (sort)="onSort($event)">{{column.name}}</th>
|
||||
<th scope="col" i18n>Actions</th>
|
||||
<th scope="col" class="fw-normal" pngxSortable="name" [currentSortField]="sortField" [currentSortReverse]="sortReverse" (sort)="onSort($event)" i18n>Name</th>
|
||||
<th scope="col" class="fw-normal d-none d-sm-table-cell" pngxSortable="matching_algorithm" [currentSortField]="sortField" [currentSortReverse]="sortReverse" (sort)="onSort($event)" i18n>Matching</th>
|
||||
<th scope="col" class="fw-normal" pngxSortable="document_count" [currentSortField]="sortField" [currentSortReverse]="sortReverse" (sort)="onSort($event)" i18n>Document count</th>
|
||||
<th scope="col" class="fw-normal" *ngFor="let column of extraColumns" pngxSortable="{{column.key}}" [currentSortField]="sortField" [currentSortReverse]="sortReverse" (sort)="onSort($event)">{{column.name}}</th>
|
||||
<th scope="col" class="fw-normal" i18n>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -48,12 +54,12 @@
|
||||
</tr>
|
||||
<tr *ngFor="let object of data" (click)="toggleSelected(object, $event); $event.stopPropagation();">
|
||||
<td>
|
||||
<div class="form-check">
|
||||
<div class="form-check m-0 ms-2 me-n2">
|
||||
<input type="checkbox" class="form-check-input" id="{{typeName}}{{object.id}}" [checked]="selectedObjects.has(object.id)" (click)="toggleSelected(object); $event.stopPropagation();">
|
||||
<label class="form-check-label" for="{{typeName}}{{object.id}}"></label>
|
||||
</div>
|
||||
</td>
|
||||
<td scope="row">{{ object.name }}</td>
|
||||
<td scope="row"><button class="btn btn-link ms-0 ps-0" (click)="openEditDialog(object)">{{ object.name }}</button> </td>
|
||||
<td scope="row" class="d-none d-sm-table-cell">{{ getMatching(object) }}</td>
|
||||
<td scope="row">{{ object.document_count }}</td>
|
||||
<td scope="row" *ngFor="let column of extraColumns">
|
||||
@@ -96,6 +102,7 @@
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="d-flex mb-2" *ngIf="!isLoading">
|
||||
<div *ngIf="collectionSize > 0">
|
||||
|
@@ -2,3 +2,11 @@
|
||||
.d-block.d-sm-none .dropdown-toggle::after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
tbody tr:last-child td {
|
||||
border-bottom: none !important;
|
||||
}
|
||||
|
||||
.form-check {
|
||||
min-height: 0;
|
||||
}
|
||||
|
@@ -172,7 +172,7 @@ describe('ManagementListComponent', () => {
|
||||
const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
|
||||
const reloadSpy = jest.spyOn(component, 'reloadData')
|
||||
|
||||
const editButton = fixture.debugElement.queryAll(By.css('button'))[5]
|
||||
const editButton = fixture.debugElement.queryAll(By.css('button'))[6]
|
||||
editButton.triggerEventHandler('click')
|
||||
|
||||
expect(modal).not.toBeUndefined()
|
||||
@@ -198,7 +198,7 @@ describe('ManagementListComponent', () => {
|
||||
const deleteSpy = jest.spyOn(tagService, 'delete')
|
||||
const reloadSpy = jest.spyOn(component, 'reloadData')
|
||||
|
||||
const deleteButton = fixture.debugElement.queryAll(By.css('button'))[6]
|
||||
const deleteButton = fixture.debugElement.queryAll(By.css('button'))[7]
|
||||
deleteButton.triggerEventHandler('click')
|
||||
|
||||
expect(modal).not.toBeUndefined()
|
||||
@@ -218,7 +218,7 @@ describe('ManagementListComponent', () => {
|
||||
|
||||
it('should support quick filter for objects', () => {
|
||||
const qfSpy = jest.spyOn(documentListViewService, 'quickFilter')
|
||||
const filterButton = fixture.debugElement.queryAll(By.css('button'))[4]
|
||||
const filterButton = fixture.debugElement.queryAll(By.css('button'))[5]
|
||||
filterButton.triggerEventHandler('click')
|
||||
expect(qfSpy).toHaveBeenCalledWith([
|
||||
{ rule_type: FILTER_HAS_TAGS_ALL, value: tags[0].id.toString() },
|
||||
|
@@ -1,494 +0,0 @@
|
||||
<pngx-page-header title="Settings" i18n-title>
|
||||
<button class="btn btn-sm btn-outline-primary" (click)="tourService.start()"><ng-container i18n>Start tour</ng-container></button>
|
||||
<a *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Admin }" class="btn btn-sm btn-primary ms-3" href="admin/" target="_blank">
|
||||
<ng-container i18n>Open Django Admin</ng-container>
|
||||
<svg class="sidebaricon ms-1" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#arrow-up-right"/>
|
||||
</svg>
|
||||
</a>
|
||||
</pngx-page-header>
|
||||
|
||||
<form [formGroup]="settingsForm" (ngSubmit)="saveSettings()">
|
||||
|
||||
<ul ngbNav #nav="ngbNav" (navChange)="onNavChange($event)" [(activeId)]="activeNavID" class="nav-tabs">
|
||||
<li [ngbNavItem]="SettingsNavIDs.General" (mouseover)="maybeInitializeTab(SettingsNavIDs.General)">
|
||||
<a ngbNavLink i18n>General</a>
|
||||
<ng-template ngbNavContent>
|
||||
|
||||
<h4 i18n>Appearance</h4>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-3 col-form-label pt-0">
|
||||
<span i18n>Display language</span>
|
||||
</div>
|
||||
<div class="col">
|
||||
|
||||
<select class="form-select" formControlName="displayLanguage">
|
||||
<option *ngFor="let lang of displayLanguageOptions" [ngValue]="lang.code">{{lang.name}}<span *ngIf="lang.code && currentLocale !== 'en-US'"> - {{lang.englishName}}</span></option>
|
||||
</select>
|
||||
|
||||
<small *ngIf="displayLanguageIsDirty" class="form-text text-primary" i18n>You need to reload the page after applying a new language.</small>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-3 col-form-label pt-0">
|
||||
<span i18n>Date display</span>
|
||||
</div>
|
||||
<div class="col">
|
||||
|
||||
<select class="form-select" formControlName="dateLocale">
|
||||
<option *ngFor="let lang of dateLocaleOptions" [ngValue]="lang.code">{{lang.name}}<span *ngIf="lang.code"> - {{today | customDate:'shortDate':null:lang.code}}</span></option>
|
||||
</select>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-3 col-form-label pt-0">
|
||||
<span i18n>Date format</span>
|
||||
</div>
|
||||
<div class="col">
|
||||
|
||||
<div class="form-check">
|
||||
<input type="radio" id="dateFormatShort" name="dateFormat" class="form-check-input" formControlName="dateFormat" value="shortDate">
|
||||
<label class="form-check-label" for="dateFormatShort" i18n>Short: {{today | customDate:'shortDate':null:computedDateLocale}}</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input type="radio" id="dateFormatMedium" name="dateFormat" class="form-check-input" formControlName="dateFormat" value="mediumDate">
|
||||
<label class="form-check-label" for="dateFormatMedium" i18n>Medium: {{today | customDate:'mediumDate':null:computedDateLocale}}</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input type="radio" id="dateFormatLong" name="dateFormat" class="form-check-input" formControlName="dateFormat" value="longDate">
|
||||
<label class="form-check-label" for="dateFormatLong" i18n>Long: {{today | customDate:'longDate':null:computedDateLocale}}</label>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-3 col-form-label pt-0">
|
||||
<span i18n>Items per page</span>
|
||||
</div>
|
||||
<div class="col">
|
||||
|
||||
<select class="form-select" formControlName="documentListItemPerPage">
|
||||
<option [ngValue]="10">10</option>
|
||||
<option [ngValue]="25">25</option>
|
||||
<option [ngValue]="50">50</option>
|
||||
<option [ngValue]="100">100</option>
|
||||
</select>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-3 col-form-label pt-0">
|
||||
<span i18n>Document editor</span>
|
||||
</div>
|
||||
<div class="col">
|
||||
|
||||
<pngx-input-check i18n-title title="Use PDF viewer provided by the browser" i18n-hint hint="This is usually faster for displaying large PDF documents, but it might not work on some browsers." formControlName="useNativePdfViewer"></pngx-input-check>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-3 col-form-label pt-0">
|
||||
<span i18n>Sidebar</span>
|
||||
</div>
|
||||
<div class="col">
|
||||
|
||||
<pngx-input-check i18n-title title="Use 'slim' sidebar (icons only)" formControlName="slimSidebarEnabled"></pngx-input-check>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-3 col-form-label pt-0">
|
||||
<span i18n>Dark mode</span>
|
||||
</div>
|
||||
<div class="col">
|
||||
<pngx-input-check i18n-title title="Use system settings" formControlName="darkModeUseSystem"></pngx-input-check>
|
||||
<pngx-input-check [hidden]="settingsForm.value.darkModeUseSystem" i18n-title title="Enable dark mode" formControlName="darkModeEnabled"></pngx-input-check>
|
||||
<pngx-input-check i18n-title title="Invert thumbnails in dark mode" formControlName="darkModeInvertThumbs"></pngx-input-check>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-3 col-form-label pt-0">
|
||||
<span i18n>Theme Color</span>
|
||||
</div>
|
||||
<div class="col col-md-3">
|
||||
<pngx-input-color i18n-title formControlName="themeColor" [error]="error?.color"></pngx-input-color>
|
||||
</div>
|
||||
<div class="col-2">
|
||||
<button class="btn btn-link btn-sm pt-2 ps-0" [disabled]="!this.settingsForm.get('themeColor').value" (click)="clearThemeColor()">
|
||||
<svg fill="currentColor" class="buttonicon-sm me-1">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#x"/>
|
||||
</svg><ng-container i18n>Reset</ng-container>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4 i18n>Permissions</h4>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="offset-md-3 col">
|
||||
<p i18n>
|
||||
Settings apply to this user account for objects (Tags, Mail Rules, etc.) created via the web UI
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-3 col-form-label pt-0">
|
||||
<span i18n>Default Owner</span>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<pngx-input-select [items]="users" bindLabel="username" formControlName="defaultPermsOwner" [allowNull]="true"></pngx-input-select>
|
||||
<small class="form-text text-muted text-end d-block mt-n2" i18n>Objects without an owner can be viewed and edited by all users</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-3 col-form-label pt-0">
|
||||
<span i18n>Default View Permissions</span>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="row">
|
||||
<div class="col-2">
|
||||
<span class="d-block pt-1" i18n>Users:</span>
|
||||
</div>
|
||||
<div class="col">
|
||||
<ng-container *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.User }">
|
||||
<pngx-permissions-user type="view" formControlName="defaultPermsViewUsers"></pngx-permissions-user>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-2">
|
||||
<span class="d-block pt-1" i18n>Groups:</span>
|
||||
</div>
|
||||
<div class="col">
|
||||
<ng-container *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Group }">
|
||||
<pngx-permissions-group type="view" formControlName="defaultPermsViewGroups"></pngx-permissions-group>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-3 col-form-label pt-0">
|
||||
<span i18n>Default Edit Permissions</span>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="row">
|
||||
<div class="col-2">
|
||||
<span class="d-block pt-1" i18n>Users:</span>
|
||||
</div>
|
||||
<div class="col">
|
||||
<ng-container *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.User }">
|
||||
<pngx-permissions-user type="view" formControlName="defaultPermsEditUsers"></pngx-permissions-user>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-2">
|
||||
<span class="d-block pt-1" i18n>Groups:</span>
|
||||
</div>
|
||||
<div class="col">
|
||||
<ng-container *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Group }">
|
||||
<pngx-permissions-group type="view" formControlName="defaultPermsEditGroups"></pngx-permissions-group>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<small class="form-text text-muted text-end d-block" i18n>Edit permissions also grant viewing permissions</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4 class="mt-4" id="update-checking" i18n>Update checking</h4>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="offset-md-3 col">
|
||||
<p i18n>
|
||||
Update checking works by pinging the public <a href="https://api.github.com/repos/paperless-ngx/paperless-ngx/releases/latest" target="_blank" rel="noopener noreferrer">GitHub API</a> for the latest release to determine whether a new version is available.<br/>
|
||||
Actual updating of the app must still be performed manually.
|
||||
</p>
|
||||
<p i18n>
|
||||
<em>No tracking data is collected by the app in any way.</em>
|
||||
</p>
|
||||
<pngx-input-check i18n-title title="Enable update checking" formControlName="updateCheckingEnabled" i18n-hint hint="Note that for users of third-party containers e.g. linuxserver.io this notification may be 'ahead' of the current third-party release."></pngx-input-check>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4 class="mt-4" i18n>Bulk editing</h4>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="offset-md-3 col">
|
||||
<pngx-input-check i18n-title title="Show confirmation dialogs" formControlName="bulkEditConfirmationDialogs" i18n-hint hint="Deleting documents will always ask for confirmation."></pngx-input-check>
|
||||
<pngx-input-check i18n-title title="Apply on close" formControlName="bulkEditApplyOnClose"></pngx-input-check>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4 class="mt-4" i18n>Notes</h4>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="offset-md-3 col">
|
||||
<pngx-input-check i18n-title title="Enable notes" formControlName="notesEnabled"></pngx-input-check>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</ng-template>
|
||||
</li>
|
||||
|
||||
<li [ngbNavItem]="SettingsNavIDs.Notifications">
|
||||
<a ngbNavLink i18n>Notifications</a>
|
||||
<ng-template ngbNavContent>
|
||||
|
||||
<h4 i18n>Document processing</h4>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="offset-md-3 col">
|
||||
<pngx-input-check i18n-title title="Show notifications when new documents are detected" formControlName="notificationsConsumerNewDocument"></pngx-input-check>
|
||||
<pngx-input-check i18n-title title="Show notifications when document processing completes successfully" formControlName="notificationsConsumerSuccess"></pngx-input-check>
|
||||
<pngx-input-check i18n-title title="Show notifications when document processing fails" formControlName="notificationsConsumerFailed"></pngx-input-check>
|
||||
<pngx-input-check i18n-title title="Suppress notifications on dashboard" formControlName="notificationsConsumerSuppressOnDashboard" i18n-hint hint="This will suppress all messages about document processing status on the dashboard."></pngx-input-check>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</ng-template>
|
||||
</li>
|
||||
|
||||
<li [ngbNavItem]="SettingsNavIDs.SavedViews" (mouseover)="maybeInitializeTab(SettingsNavIDs.SavedViews)" (focusin)="maybeInitializeTab(SettingsNavIDs.SavedViews)">
|
||||
<a ngbNavLink i18n>Saved views</a>
|
||||
<ng-template ngbNavContent>
|
||||
|
||||
<h4 i18n>Settings</h4>
|
||||
<div class="row mb-3">
|
||||
<div class="offset-md-3 col">
|
||||
<pngx-input-check i18n-title title="Show warning when closing saved views with unsaved changes" formControlName="savedViewsWarnOnUnsavedChange"></pngx-input-check>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4 i18n>Views</h4>
|
||||
<div formGroupName="savedViews">
|
||||
|
||||
<div *ngFor="let view of savedViews" [formGroupName]="view.id" class="row">
|
||||
<div class="mb-3 col">
|
||||
<label class="form-label" for="name_{{view.id}}" i18n>Name</label>
|
||||
<input type="text" class="form-control" formControlName="name" id="name_{{view.id}}">
|
||||
</div>
|
||||
|
||||
<div class="mb-2 col">
|
||||
<label class="form-label" for="show_on_dashboard_{{view.id}}" i18n> <span class="visually-hidden">Appears on</span></label>
|
||||
<div class="form-check form-switch">
|
||||
<input type="checkbox" class="form-check-input" id="show_on_dashboard_{{view.id}}" formControlName="show_on_dashboard">
|
||||
<label class="form-check-label" for="show_on_dashboard_{{view.id}}" i18n>Show on dashboard</label>
|
||||
</div>
|
||||
<div class="form-check form-switch">
|
||||
<input type="checkbox" class="form-check-input" id="show_in_sidebar_{{view.id}}" formControlName="show_in_sidebar">
|
||||
<label class="form-check-label" for="show_in_sidebar_{{view.id}}" i18n>Show in sidebar</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-2 col-auto">
|
||||
<label class="form-label" for="name_{{view.id}}" i18n>Actions</label>
|
||||
<button type="button" class="btn btn-sm btn-outline-danger form-control" (click)="deleteSavedView(view)" *pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.SavedView }" i18n>Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div *ngIf="savedViews && savedViews.length === 0" i18n>No saved views defined.</div>
|
||||
|
||||
<div *ngIf="!savedViews">
|
||||
<div class="spinner-border spinner-border-sm fw-normal ms-2 me-auto" role="status"></div>
|
||||
<div class="visually-hidden" i18n>Loading...</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</ng-template>
|
||||
</li>
|
||||
|
||||
<li *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.MailRule }" [ngbNavItem]="SettingsNavIDs.Mail" (mouseover)="maybeInitializeTab(SettingsNavIDs.Mail)" (focusin)="maybeInitializeTab(SettingsNavIDs.Mail)">
|
||||
<a ngbNavLink i18n>Mail</a>
|
||||
<ng-template ngbNavContent>
|
||||
|
||||
<ng-container *ngIf="mailAccounts && mailRules">
|
||||
<ng-container *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.MailAccount }">
|
||||
<h4>
|
||||
<ng-container i18n>Mail accounts</ng-container>
|
||||
<button type="button" class="btn btn-sm btn-primary ms-4" (click)="editMailAccount()" *pngxIfPermissions="{ action: PermissionAction.Add, type: PermissionType.MailAccount }">
|
||||
<svg class="sidebaricon me-1" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#plus-circle" />
|
||||
</svg>
|
||||
<ng-container i18n>Add Account</ng-container>
|
||||
</button>
|
||||
</h4>
|
||||
<ul class="list-group" formGroupName="mailAccounts">
|
||||
|
||||
<li class="list-group-item">
|
||||
<div class="row">
|
||||
<div class="col" i18n>Name</div>
|
||||
<div class="col" i18n>Server</div>
|
||||
<div class="col" i18n>Actions</div>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li *ngFor="let account of mailAccounts" class="list-group-item" [formGroupName]="account.id">
|
||||
<div class="row">
|
||||
<div class="col d-flex align-items-center"><button class="btn btn-link p-0" type="button" (click)="editMailAccount(account)" [disabled]="!permissionsService.currentUserCan(PermissionAction.Change, PermissionType.MailAccount)">{{account.name}}</button></div>
|
||||
<div class="col d-flex align-items-center">{{account.imap_server}}</div>
|
||||
<div class="col">
|
||||
<div class="btn-group">
|
||||
<button *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.MailAccount }" [disabled]="!userCanEdit(account)" class="btn btn-sm btn-primary" type="button" (click)="editMailAccount(account)" i18n>Edit</button>
|
||||
<button *pngxIfOwner="account" class="btn btn-sm btn-primary" type="button" (click)="editPermissions(account)" i18n>Permissions</button>
|
||||
<button *pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.MailAccount }" [disabled]="!userIsOwner(account)" class="btn btn-sm btn-outline-danger" type="button" (click)="deleteMailAccount(account)" i18n>Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<div *ngIf="mailAccounts.length === 0" i18n>No mail accounts defined.</div>
|
||||
</ul>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.MailRule }">
|
||||
<h4 class="mt-4">
|
||||
<ng-container i18n>Mail rules</ng-container>
|
||||
<button type="button" class="btn btn-sm btn-primary ms-4" (click)="editMailRule()" *pngxIfPermissions="{ action: PermissionAction.Add, type: PermissionType.MailRule }">
|
||||
<svg class="sidebaricon me-1" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#plus-circle" />
|
||||
</svg>
|
||||
<ng-container i18n>Add Rule</ng-container>
|
||||
</button>
|
||||
</h4>
|
||||
<ul class="list-group" formGroupName="mailRules">
|
||||
|
||||
<li class="list-group-item">
|
||||
<div class="row">
|
||||
<div class="col" i18n>Name</div>
|
||||
<div class="col" i18n>Account</div>
|
||||
<div class="col" i18n>Actions</div>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li *ngFor="let rule of mailRules" class="list-group-item" [formGroupName]="rule.id">
|
||||
<div class="row">
|
||||
<div class="col d-flex align-items-center"><button class="btn btn-link p-0" type="button" (click)="editMailRule(rule)" [disabled]="!permissionsService.currentUserCan(PermissionAction.Change, PermissionType.MailRule)">{{rule.name}}</button></div>
|
||||
<div class="col d-flex align-items-center">{{(mailAccountService.getCached(rule.account) | async)?.name}}</div>
|
||||
<div class="col">
|
||||
<div class="btn-group">
|
||||
<button *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.MailRule }" [disabled]="!userCanEdit(rule)" class="btn btn-sm btn-primary" type="button" (click)="editMailRule(rule)" i18n>Edit</button>
|
||||
<button *pngxIfOwner="rule" class="btn btn-sm btn-primary" type="button" (click)="editPermissions(rule)" i18n>Permissions</button>
|
||||
<button *pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.MailRule }" [disabled]="!userIsOwner(rule)" class="btn btn-sm btn-outline-danger" type="button" (click)="deleteMailRule(rule)" i18n>Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<div *ngIf="mailRules.length === 0" i18n>No mail rules defined.</div>
|
||||
</ul>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
<div *ngIf="!mailAccounts || !mailRules">
|
||||
<div class="spinner-border spinner-border-sm fw-normal ms-2 me-auto" role="status"></div>
|
||||
<div class="visually-hidden" i18n>Loading...</div>
|
||||
</div>
|
||||
|
||||
</ng-template>
|
||||
</li>
|
||||
|
||||
<li [ngbNavItem]="SettingsNavIDs.UsersGroups" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.User }" (mouseover)="maybeInitializeTab(SettingsNavIDs.UsersGroups)" (focusin)="maybeInitializeTab(SettingsNavIDs.UsersGroups)">
|
||||
<a ngbNavLink i18n>Users & Groups</a>
|
||||
<ng-template ngbNavContent>
|
||||
|
||||
<ng-container *ngIf="users && groups">
|
||||
<h4 class="d-flex">
|
||||
<ng-container i18n>Users</ng-container>
|
||||
<button type="button" class="btn btn-sm btn-primary ms-4" (click)="editUser()" *pngxIfPermissions="{ action: PermissionAction.Add, type: PermissionType.User }">
|
||||
<svg class="sidebaricon me-1" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#plus-circle" />
|
||||
</svg>
|
||||
<ng-container i18n>Add User</ng-container>
|
||||
</button>
|
||||
</h4>
|
||||
<ul class="list-group" formGroupName="usersGroup">
|
||||
|
||||
<li class="list-group-item">
|
||||
<div class="row">
|
||||
<div class="col" i18n>Username</div>
|
||||
<div class="col" i18n>Name</div>
|
||||
<div class="col" i18n>Groups</div>
|
||||
<div class="col" i18n>Actions</div>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li *ngFor="let user of users" class="list-group-item" [formGroupName]="user.id">
|
||||
<div class="row">
|
||||
<div class="col d-flex align-items-center"><button class="btn btn-link p-0" type="button" (click)="editUser(user)" [disabled]="!permissionsService.currentUserCan(PermissionAction.Change, PermissionType.User)">{{user.username}}</button></div>
|
||||
<div class="col d-flex align-items-center">{{user.first_name}} {{user.last_name}}</div>
|
||||
<div class="col d-flex align-items-center">{{user.groups?.map(getGroupName, this).join(', ')}}</div>
|
||||
<div class="col">
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-sm btn-primary" type="button" (click)="editUser(user)" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.User }" i18n>Edit</button>
|
||||
<button class="btn btn-sm btn-outline-danger" type="button" (click)="deleteUser(user)" *pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.User }" i18n>Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h4 class="mt-4 d-flex">
|
||||
<ng-container i18n>Groups</ng-container>
|
||||
<button type="button" class="btn btn-sm btn-primary ms-4" (click)="editGroup()" *pngxIfPermissions="{ action: PermissionAction.Add, type: PermissionType.Group }">
|
||||
<svg class="sidebaricon me-1" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#plus-circle" />
|
||||
</svg>
|
||||
<ng-container i18n>Add Group</ng-container>
|
||||
</button>
|
||||
</h4>
|
||||
<ul *ngIf="groups.length > 0" class="list-group" formGroupName="groupsGroup">
|
||||
|
||||
<li class="list-group-item">
|
||||
<div class="row">
|
||||
<div class="col" i18n>Name</div>
|
||||
<div class="col"></div>
|
||||
<div class="col"></div>
|
||||
<div class="col" i18n>Actions</div>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li *ngFor="let group of groups" class="list-group-item" [formGroupName]="group.id">
|
||||
<div class="row">
|
||||
<div class="col d-flex align-items-center"><button class="btn btn-link p-0" type="button" (click)="editGroup(group)" [disabled]="!permissionsService.currentUserCan(PermissionAction.Change, PermissionType.Group)">{{group.name}}</button></div>
|
||||
<div class="col"></div>
|
||||
<div class="col"></div>
|
||||
<div class="col">
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-sm btn-primary" type="button" (click)="editGroup(group)" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.Group }" i18n>Edit</button>
|
||||
<button class="btn btn-sm btn-outline-danger" type="button" (click)="deleteGroup(group)" *pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.Group }" i18n>Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div *ngIf="groups.length === 0">No groups defined</div>
|
||||
</ng-container>
|
||||
|
||||
<div *ngIf="!users || !groups">
|
||||
<div class="spinner-border spinner-border-sm fw-normal ms-2 me-auto" role="status"></div>
|
||||
<div class="visually-hidden" i18n>Loading...</div>
|
||||
</div>
|
||||
|
||||
</ng-template>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div [ngbNavOutlet]="nav" class="border-start border-end border-bottom p-3 mb-3 shadow-sm"></div>
|
||||
|
||||
<button type="submit" class="btn btn-primary mb-2" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.UISettings }" [disabled]="(isDirty$ | async) === false" i18n>Save</button>
|
||||
</form>
|
@@ -1,665 +0,0 @@
|
||||
import { ViewportScroller, DatePipe } from '@angular/common'
|
||||
import { HttpClientTestingModule } from '@angular/common/http/testing'
|
||||
import {
|
||||
ComponentFixture,
|
||||
TestBed,
|
||||
fakeAsync,
|
||||
tick,
|
||||
} from '@angular/core/testing'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { By } from '@angular/platform-browser'
|
||||
import { Router, ActivatedRoute, convertToParamMap } from '@angular/router'
|
||||
import { RouterTestingModule } from '@angular/router/testing'
|
||||
import {
|
||||
NgbModal,
|
||||
NgbModule,
|
||||
NgbAlertModule,
|
||||
NgbNavLink,
|
||||
NgbModalRef,
|
||||
} from '@ng-bootstrap/ng-bootstrap'
|
||||
import { NgSelectModule } from '@ng-select/ng-select'
|
||||
import { of, throwError } from 'rxjs'
|
||||
import { routes } from 'src/app/app-routing.module'
|
||||
import { PaperlessMailAccount } from 'src/app/data/paperless-mail-account'
|
||||
import { PaperlessMailRule } from 'src/app/data/paperless-mail-rule'
|
||||
import { PaperlessSavedView } from 'src/app/data/paperless-saved-view'
|
||||
import { SETTINGS_KEYS } from 'src/app/data/paperless-uisettings'
|
||||
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
||||
import { PermissionsGuard } from 'src/app/guards/permissions.guard'
|
||||
import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe'
|
||||
import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
|
||||
import { PermissionsService } from 'src/app/services/permissions.service'
|
||||
import { GroupService } from 'src/app/services/rest/group.service'
|
||||
import { MailAccountService } from 'src/app/services/rest/mail-account.service'
|
||||
import { MailRuleService } from 'src/app/services/rest/mail-rule.service'
|
||||
import { SavedViewService } from 'src/app/services/rest/saved-view.service'
|
||||
import { UserService } from 'src/app/services/rest/user.service'
|
||||
import { SettingsService } from 'src/app/services/settings.service'
|
||||
import { ToastService, Toast } from 'src/app/services/toast.service'
|
||||
import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component'
|
||||
import { GroupEditDialogComponent } from '../../common/edit-dialog/group-edit-dialog/group-edit-dialog.component'
|
||||
import { MailAccountEditDialogComponent } from '../../common/edit-dialog/mail-account-edit-dialog/mail-account-edit-dialog.component'
|
||||
import { MailRuleEditDialogComponent } from '../../common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component'
|
||||
import { UserEditDialogComponent } from '../../common/edit-dialog/user-edit-dialog/user-edit-dialog.component'
|
||||
import { CheckComponent } from '../../common/input/check/check.component'
|
||||
import { ColorComponent } from '../../common/input/color/color.component'
|
||||
import { NumberComponent } from '../../common/input/number/number.component'
|
||||
import { PasswordComponent } from '../../common/input/password/password.component'
|
||||
import { PermissionsGroupComponent } from '../../common/input/permissions/permissions-group/permissions-group.component'
|
||||
import { PermissionsUserComponent } from '../../common/input/permissions/permissions-user/permissions-user.component'
|
||||
import { SelectComponent } from '../../common/input/select/select.component'
|
||||
import { TagsComponent } from '../../common/input/tags/tags.component'
|
||||
import { TextComponent } from '../../common/input/text/text.component'
|
||||
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
|
||||
import { SettingsComponent } from './settings.component'
|
||||
import { PermissionsDialogComponent } from '../../common/permissions-dialog/permissions-dialog.component'
|
||||
import { PermissionsFormComponent } from '../../common/input/permissions/permissions-form/permissions-form.component'
|
||||
import { IfOwnerDirective } from 'src/app/directives/if-owner.directive'
|
||||
|
||||
const savedViews = [
|
||||
{ id: 1, name: 'view1' },
|
||||
{ id: 2, name: 'view2' },
|
||||
]
|
||||
const users = [
|
||||
{ id: 1, username: 'user1', is_superuser: false },
|
||||
{ id: 2, username: 'user2', is_superuser: false },
|
||||
]
|
||||
const groups = [
|
||||
{ id: 1, name: 'group1' },
|
||||
{ id: 2, name: 'group2' },
|
||||
]
|
||||
const mailAccounts = [
|
||||
{ id: 1, name: 'account1' },
|
||||
{ id: 2, name: 'account2' },
|
||||
]
|
||||
const mailRules = [
|
||||
{ id: 1, name: 'rule1', owner: 1, account: 1 },
|
||||
{ id: 2, name: 'rule2', owner: 2, account: 2 },
|
||||
]
|
||||
|
||||
describe('SettingsComponent', () => {
|
||||
let component: SettingsComponent
|
||||
let fixture: ComponentFixture<SettingsComponent>
|
||||
let modalService: NgbModal
|
||||
let router: Router
|
||||
let settingsService: SettingsService
|
||||
let savedViewService: SavedViewService
|
||||
let activatedRoute: ActivatedRoute
|
||||
let viewportScroller: ViewportScroller
|
||||
let toastService: ToastService
|
||||
let userService: UserService
|
||||
let permissionsService: PermissionsService
|
||||
let groupService: GroupService
|
||||
let mailAccountService: MailAccountService
|
||||
let mailRuleService: MailRuleService
|
||||
|
||||
beforeEach(async () => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [
|
||||
SettingsComponent,
|
||||
PageHeaderComponent,
|
||||
IfPermissionsDirective,
|
||||
CustomDatePipe,
|
||||
ConfirmDialogComponent,
|
||||
CheckComponent,
|
||||
ColorComponent,
|
||||
SafeHtmlPipe,
|
||||
SelectComponent,
|
||||
TextComponent,
|
||||
PasswordComponent,
|
||||
NumberComponent,
|
||||
TagsComponent,
|
||||
MailAccountEditDialogComponent,
|
||||
MailRuleEditDialogComponent,
|
||||
PermissionsUserComponent,
|
||||
PermissionsGroupComponent,
|
||||
IfOwnerDirective,
|
||||
PermissionsDialogComponent,
|
||||
PermissionsFormComponent,
|
||||
],
|
||||
providers: [CustomDatePipe, DatePipe, PermissionsGuard],
|
||||
imports: [
|
||||
NgbModule,
|
||||
HttpClientTestingModule,
|
||||
RouterTestingModule.withRoutes(routes),
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
NgbAlertModule,
|
||||
NgSelectModule,
|
||||
],
|
||||
}).compileComponents()
|
||||
|
||||
modalService = TestBed.inject(NgbModal)
|
||||
router = TestBed.inject(Router)
|
||||
activatedRoute = TestBed.inject(ActivatedRoute)
|
||||
viewportScroller = TestBed.inject(ViewportScroller)
|
||||
toastService = TestBed.inject(ToastService)
|
||||
settingsService = TestBed.inject(SettingsService)
|
||||
settingsService.currentUser = { id: 99, username: 'user99' }
|
||||
userService = TestBed.inject(UserService)
|
||||
permissionsService = TestBed.inject(PermissionsService)
|
||||
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
|
||||
jest
|
||||
.spyOn(permissionsService, 'currentUserHasObjectPermissions')
|
||||
.mockReturnValue(true)
|
||||
jest
|
||||
.spyOn(permissionsService, 'currentUserOwnsObject')
|
||||
.mockReturnValue(true)
|
||||
groupService = TestBed.inject(GroupService)
|
||||
savedViewService = TestBed.inject(SavedViewService)
|
||||
mailAccountService = TestBed.inject(MailAccountService)
|
||||
mailRuleService = TestBed.inject(MailRuleService)
|
||||
})
|
||||
|
||||
function completeSetup(excludeService = null) {
|
||||
if (excludeService !== userService) {
|
||||
jest.spyOn(userService, 'listAll').mockReturnValue(
|
||||
of({
|
||||
all: users.map((u) => u.id),
|
||||
count: users.length,
|
||||
results: users.concat([]),
|
||||
})
|
||||
)
|
||||
}
|
||||
if (excludeService !== groupService) {
|
||||
jest.spyOn(groupService, 'listAll').mockReturnValue(
|
||||
of({
|
||||
all: groups.map((g) => g.id),
|
||||
count: groups.length,
|
||||
results: groups.concat([]),
|
||||
})
|
||||
)
|
||||
}
|
||||
if (excludeService !== savedViewService) {
|
||||
jest.spyOn(savedViewService, 'listAll').mockReturnValue(
|
||||
of({
|
||||
all: savedViews.map((v) => v.id),
|
||||
count: savedViews.length,
|
||||
results: (savedViews as PaperlessSavedView[]).concat([]),
|
||||
})
|
||||
)
|
||||
}
|
||||
if (excludeService !== mailAccountService) {
|
||||
jest.spyOn(mailAccountService, 'listAll').mockReturnValue(
|
||||
of({
|
||||
all: mailAccounts.map((a) => a.id),
|
||||
count: mailAccounts.length,
|
||||
results: (mailAccounts as PaperlessMailAccount[]).concat([]),
|
||||
})
|
||||
)
|
||||
}
|
||||
if (excludeService !== mailRuleService) {
|
||||
jest.spyOn(mailRuleService, 'listAll').mockReturnValue(
|
||||
of({
|
||||
all: mailRules.map((r) => r.id),
|
||||
count: mailRules.length,
|
||||
results: (mailRules as PaperlessMailRule[]).concat([]),
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
fixture = TestBed.createComponent(SettingsComponent)
|
||||
component = fixture.componentInstance
|
||||
fixture.detectChanges()
|
||||
}
|
||||
|
||||
it('should support tabbed settings & change URL, prevent navigation if dirty confirmation rejected', () => {
|
||||
completeSetup()
|
||||
const navigateSpy = jest.spyOn(router, 'navigate')
|
||||
const tabButtons = fixture.debugElement.queryAll(By.directive(NgbNavLink))
|
||||
tabButtons[1].nativeElement.dispatchEvent(new MouseEvent('click'))
|
||||
expect(navigateSpy).toHaveBeenCalledWith(['settings', 'notifications'])
|
||||
tabButtons[2].nativeElement.dispatchEvent(new MouseEvent('click'))
|
||||
expect(navigateSpy).toHaveBeenCalledWith(['settings', 'savedviews'])
|
||||
tabButtons[3].nativeElement.dispatchEvent(new MouseEvent('click'))
|
||||
expect(navigateSpy).toHaveBeenCalledWith(['settings', 'mail'])
|
||||
tabButtons[4].nativeElement.dispatchEvent(new MouseEvent('click'))
|
||||
expect(navigateSpy).toHaveBeenCalledWith(['settings', 'usersgroups'])
|
||||
|
||||
const initSpy = jest.spyOn(component, 'initialize')
|
||||
component.isDirty = true // mock dirty
|
||||
navigateSpy.mockResolvedValueOnce(false) // nav rejected cause dirty
|
||||
tabButtons[0].nativeElement.dispatchEvent(new MouseEvent('click'))
|
||||
expect(navigateSpy).toHaveBeenCalledWith(['settings', 'general'])
|
||||
expect(initSpy).not.toHaveBeenCalled()
|
||||
|
||||
navigateSpy.mockResolvedValueOnce(true) // nav accepted even though dirty
|
||||
tabButtons[1].nativeElement.dispatchEvent(new MouseEvent('click'))
|
||||
expect(navigateSpy).toHaveBeenCalledWith(['settings', 'notifications'])
|
||||
expect(initSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should support direct link to tab by URL, scroll if needed', () => {
|
||||
completeSetup()
|
||||
jest
|
||||
.spyOn(activatedRoute, 'paramMap', 'get')
|
||||
.mockReturnValue(of(convertToParamMap({ section: 'mail' })))
|
||||
activatedRoute.snapshot.fragment = '#mail'
|
||||
const scrollSpy = jest.spyOn(viewportScroller, 'scrollToAnchor')
|
||||
component.ngOnInit()
|
||||
expect(component.activeNavID).toEqual(4) // Mail
|
||||
component.ngAfterViewInit()
|
||||
expect(scrollSpy).toHaveBeenCalledWith('#mail')
|
||||
})
|
||||
|
||||
it('should lazy load tab data', () => {
|
||||
completeSetup()
|
||||
const tabButtons = fixture.debugElement.queryAll(By.directive(NgbNavLink))
|
||||
|
||||
expect(component.savedViews).toBeUndefined()
|
||||
tabButtons[2].nativeElement.dispatchEvent(
|
||||
new MouseEvent('mouseover', { bubbles: true })
|
||||
)
|
||||
expect(component.savedViews).not.toBeUndefined()
|
||||
|
||||
expect(component.mailAccounts).toBeUndefined()
|
||||
tabButtons[3].nativeElement.dispatchEvent(
|
||||
new MouseEvent('mouseover', { bubbles: true })
|
||||
)
|
||||
expect(component.mailAccounts).not.toBeUndefined()
|
||||
|
||||
expect(component.groups).toBeUndefined()
|
||||
tabButtons[4].nativeElement.dispatchEvent(
|
||||
new MouseEvent('mouseover', { bubbles: true })
|
||||
)
|
||||
expect(component.groups).not.toBeUndefined()
|
||||
})
|
||||
|
||||
it('should support save saved views, show error', () => {
|
||||
completeSetup()
|
||||
component.maybeInitializeTab(3) // SavedViews
|
||||
|
||||
const toastErrorSpy = jest.spyOn(toastService, 'showError')
|
||||
const toastSpy = jest.spyOn(toastService, 'show')
|
||||
const savedViewPatchSpy = jest.spyOn(savedViewService, 'patchMany')
|
||||
|
||||
// saved views error first
|
||||
savedViewPatchSpy.mockReturnValueOnce(
|
||||
throwError(() => new Error('unable to save saved views'))
|
||||
)
|
||||
component.saveSettings()
|
||||
expect(toastErrorSpy).toHaveBeenCalled()
|
||||
expect(savedViewPatchSpy).toHaveBeenCalled()
|
||||
toastSpy.mockClear()
|
||||
toastErrorSpy.mockClear()
|
||||
savedViewPatchSpy.mockClear()
|
||||
|
||||
// succeed saved views
|
||||
savedViewPatchSpy.mockReturnValueOnce(
|
||||
of(savedViews as PaperlessSavedView[])
|
||||
)
|
||||
component.saveSettings()
|
||||
expect(toastErrorSpy).not.toHaveBeenCalled()
|
||||
expect(savedViewPatchSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should support save local settings updating appearance settings and calling API, show error', () => {
|
||||
completeSetup()
|
||||
const toastErrorSpy = jest.spyOn(toastService, 'showError')
|
||||
const toastSpy = jest.spyOn(toastService, 'show')
|
||||
const storeSpy = jest.spyOn(settingsService, 'storeSettings')
|
||||
const appearanceSettingsSpy = jest.spyOn(
|
||||
settingsService,
|
||||
'updateAppearanceSettings'
|
||||
)
|
||||
const setSpy = jest.spyOn(settingsService, 'set')
|
||||
|
||||
// error first
|
||||
storeSpy.mockReturnValueOnce(
|
||||
throwError(() => new Error('unable to save settings'))
|
||||
)
|
||||
component.saveSettings()
|
||||
expect(toastErrorSpy).toHaveBeenCalled()
|
||||
expect(storeSpy).toHaveBeenCalled()
|
||||
expect(appearanceSettingsSpy).not.toHaveBeenCalled()
|
||||
expect(setSpy).toHaveBeenCalledTimes(24)
|
||||
|
||||
// succeed
|
||||
storeSpy.mockReturnValueOnce(of(true))
|
||||
component.saveSettings()
|
||||
expect(toastSpy).toHaveBeenCalled()
|
||||
expect(appearanceSettingsSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should offer reload if settings changes require', () => {
|
||||
completeSetup()
|
||||
let toast: Toast
|
||||
toastService.getToasts().subscribe((t) => (toast = t[0]))
|
||||
component.initialize(true) // reset
|
||||
component.store.getValue()['displayLanguage'] = 'en-US'
|
||||
component.store.getValue()['updateCheckingEnabled'] = false
|
||||
component.settingsForm.value.displayLanguage = 'en-GB'
|
||||
component.settingsForm.value.updateCheckingEnabled = true
|
||||
jest.spyOn(settingsService, 'storeSettings').mockReturnValueOnce(of(true))
|
||||
component.saveSettings()
|
||||
expect(toast.actionName).toEqual('Reload now')
|
||||
})
|
||||
|
||||
it('should allow setting theme color, visually apply change immediately but not save', () => {
|
||||
completeSetup()
|
||||
const appearanceSpy = jest.spyOn(
|
||||
settingsService,
|
||||
'updateAppearanceSettings'
|
||||
)
|
||||
const colorInput = fixture.debugElement.query(By.directive(ColorComponent))
|
||||
colorInput.query(By.css('input')).nativeElement.value = '#ff0000'
|
||||
colorInput
|
||||
.query(By.css('input'))
|
||||
.nativeElement.dispatchEvent(new Event('change'))
|
||||
fixture.detectChanges()
|
||||
expect(appearanceSpy).toHaveBeenCalled()
|
||||
expect(settingsService.get(SETTINGS_KEYS.THEME_COLOR)).toEqual('')
|
||||
component.clearThemeColor()
|
||||
})
|
||||
|
||||
it('should support delete saved view', () => {
|
||||
completeSetup()
|
||||
component.maybeInitializeTab(3) // SavedViews
|
||||
const toastSpy = jest.spyOn(toastService, 'showInfo')
|
||||
const deleteSpy = jest.spyOn(savedViewService, 'delete')
|
||||
deleteSpy.mockReturnValue(of(true))
|
||||
component.deleteSavedView(savedViews[0] as PaperlessSavedView)
|
||||
expect(deleteSpy).toHaveBeenCalled()
|
||||
expect(toastSpy).toHaveBeenCalledWith(
|
||||
`Saved view "${savedViews[0].name}" deleted.`
|
||||
)
|
||||
})
|
||||
|
||||
it('should support edit / create user, show error if needed', () => {
|
||||
completeSetup()
|
||||
let modal: NgbModalRef
|
||||
modalService.activeInstances.subscribe((refs) => (modal = refs[0]))
|
||||
component.editUser(users[0])
|
||||
const editDialog = modal.componentInstance as UserEditDialogComponent
|
||||
const toastErrorSpy = jest.spyOn(toastService, 'showError')
|
||||
const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
|
||||
editDialog.failed.emit()
|
||||
expect(toastErrorSpy).toBeCalled()
|
||||
settingsService.currentUser = users[1] // simulate logged in as different user
|
||||
editDialog.succeeded.emit(users[0])
|
||||
expect(toastInfoSpy).toHaveBeenCalledWith(
|
||||
`Saved user "${users[0].username}".`
|
||||
)
|
||||
})
|
||||
|
||||
it('should support delete user, show error if needed', () => {
|
||||
completeSetup()
|
||||
let modal: NgbModalRef
|
||||
modalService.activeInstances.subscribe((refs) => (modal = refs[0]))
|
||||
component.deleteUser(users[0])
|
||||
const deleteDialog = modal.componentInstance as ConfirmDialogComponent
|
||||
const deleteSpy = jest.spyOn(userService, 'delete')
|
||||
const toastErrorSpy = jest.spyOn(toastService, 'showError')
|
||||
const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
|
||||
const listAllSpy = jest.spyOn(userService, 'listAll')
|
||||
deleteSpy.mockReturnValueOnce(
|
||||
throwError(() => new Error('error deleting user'))
|
||||
)
|
||||
deleteDialog.confirm()
|
||||
expect(toastErrorSpy).toBeCalled()
|
||||
deleteSpy.mockReturnValueOnce(of(true))
|
||||
deleteDialog.confirm()
|
||||
expect(listAllSpy).toHaveBeenCalled()
|
||||
expect(toastInfoSpy).toHaveBeenCalledWith('Deleted user')
|
||||
})
|
||||
|
||||
it('should logout current user if password changed, after delay', fakeAsync(() => {
|
||||
completeSetup()
|
||||
let modal: NgbModalRef
|
||||
modalService.activeInstances.subscribe((refs) => (modal = refs[0]))
|
||||
component.editUser(users[0])
|
||||
const editDialog = modal.componentInstance as UserEditDialogComponent
|
||||
editDialog.passwordIsSet = true
|
||||
settingsService.currentUser = users[0] // simulate logged in as same user
|
||||
editDialog.succeeded.emit(users[0])
|
||||
fixture.detectChanges()
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: {
|
||||
href: 'http://localhost/',
|
||||
},
|
||||
writable: true, // possibility to override
|
||||
})
|
||||
tick(2600)
|
||||
expect(window.location.href).toContain('logout')
|
||||
}))
|
||||
|
||||
it('should support edit / create group, show error if needed', () => {
|
||||
completeSetup()
|
||||
let modal: NgbModalRef
|
||||
modalService.activeInstances.subscribe((refs) => (modal = refs[0]))
|
||||
component.editGroup(groups[0])
|
||||
const editDialog = modal.componentInstance as GroupEditDialogComponent
|
||||
const toastErrorSpy = jest.spyOn(toastService, 'showError')
|
||||
const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
|
||||
editDialog.failed.emit()
|
||||
expect(toastErrorSpy).toBeCalled()
|
||||
editDialog.succeeded.emit(groups[0])
|
||||
expect(toastInfoSpy).toHaveBeenCalledWith(
|
||||
`Saved group "${groups[0].name}".`
|
||||
)
|
||||
})
|
||||
|
||||
it('should support delete group, show error if needed', () => {
|
||||
completeSetup()
|
||||
let modal: NgbModalRef
|
||||
modalService.activeInstances.subscribe((refs) => (modal = refs[0]))
|
||||
component.deleteGroup(users[0])
|
||||
const deleteDialog = modal.componentInstance as ConfirmDialogComponent
|
||||
const deleteSpy = jest.spyOn(groupService, 'delete')
|
||||
const toastErrorSpy = jest.spyOn(toastService, 'showError')
|
||||
const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
|
||||
const listAllSpy = jest.spyOn(groupService, 'listAll')
|
||||
deleteSpy.mockReturnValueOnce(
|
||||
throwError(() => new Error('error deleting group'))
|
||||
)
|
||||
deleteDialog.confirm()
|
||||
expect(toastErrorSpy).toBeCalled()
|
||||
deleteSpy.mockReturnValueOnce(of(true))
|
||||
deleteDialog.confirm()
|
||||
expect(listAllSpy).toHaveBeenCalled()
|
||||
expect(toastInfoSpy).toHaveBeenCalledWith('Deleted group')
|
||||
})
|
||||
|
||||
it('should get group name', () => {
|
||||
completeSetup()
|
||||
component.maybeInitializeTab(5) // UsersGroups
|
||||
expect(component.getGroupName(1)).toEqual(groups[0].name)
|
||||
expect(component.getGroupName(11)).toEqual('')
|
||||
})
|
||||
|
||||
it('should show errors on load if load mailAccounts failure', () => {
|
||||
const toastErrorSpy = jest.spyOn(toastService, 'showError')
|
||||
jest
|
||||
.spyOn(mailAccountService, 'listAll')
|
||||
.mockImplementation(() =>
|
||||
throwError(() => new Error('failed to load mail accounts'))
|
||||
)
|
||||
completeSetup(mailAccountService)
|
||||
const tabButtons = fixture.debugElement.queryAll(By.directive(NgbNavLink))
|
||||
tabButtons[3].nativeElement.dispatchEvent(new MouseEvent('click')) // mail tab
|
||||
fixture.detectChanges()
|
||||
expect(toastErrorSpy).toBeCalled()
|
||||
})
|
||||
|
||||
it('should show errors on load if load mailRules failure', () => {
|
||||
const toastErrorSpy = jest.spyOn(toastService, 'showError')
|
||||
jest
|
||||
.spyOn(mailRuleService, 'listAll')
|
||||
.mockImplementation(() =>
|
||||
throwError(() => new Error('failed to load mail rules'))
|
||||
)
|
||||
completeSetup(mailRuleService)
|
||||
const tabButtons = fixture.debugElement.queryAll(By.directive(NgbNavLink))
|
||||
tabButtons[3].nativeElement.dispatchEvent(new MouseEvent('click')) // mail tab
|
||||
fixture.detectChanges()
|
||||
// tabButtons[4].nativeElement.dispatchEvent(new MouseEvent('click'))
|
||||
expect(toastErrorSpy).toBeCalled()
|
||||
})
|
||||
|
||||
it('should show errors on load if load users failure', () => {
|
||||
const toastErrorSpy = jest.spyOn(toastService, 'showError')
|
||||
jest
|
||||
.spyOn(userService, 'listAll')
|
||||
.mockImplementation(() =>
|
||||
throwError(() => new Error('failed to load users'))
|
||||
)
|
||||
completeSetup(userService)
|
||||
const tabButtons = fixture.debugElement.queryAll(By.directive(NgbNavLink))
|
||||
tabButtons[4].nativeElement.dispatchEvent(new MouseEvent('click')) // users tab
|
||||
fixture.detectChanges()
|
||||
expect(toastErrorSpy).toBeCalled()
|
||||
})
|
||||
|
||||
it('should show errors on load if load groups failure', () => {
|
||||
const toastErrorSpy = jest.spyOn(toastService, 'showError')
|
||||
jest
|
||||
.spyOn(groupService, 'listAll')
|
||||
.mockImplementation(() =>
|
||||
throwError(() => new Error('failed to load groups'))
|
||||
)
|
||||
completeSetup(groupService)
|
||||
const tabButtons = fixture.debugElement.queryAll(By.directive(NgbNavLink))
|
||||
tabButtons[4].nativeElement.dispatchEvent(new MouseEvent('click')) // users tab
|
||||
fixture.detectChanges()
|
||||
expect(toastErrorSpy).toBeCalled()
|
||||
})
|
||||
|
||||
it('should support edit / create mail account, show error if needed', () => {
|
||||
completeSetup()
|
||||
let modal: NgbModalRef
|
||||
modalService.activeInstances.subscribe((refs) => (modal = refs[0]))
|
||||
component.editMailAccount(mailAccounts[0] as PaperlessMailAccount)
|
||||
const editDialog = modal.componentInstance as MailAccountEditDialogComponent
|
||||
const toastErrorSpy = jest.spyOn(toastService, 'showError')
|
||||
const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
|
||||
editDialog.failed.emit()
|
||||
expect(toastErrorSpy).toBeCalled()
|
||||
editDialog.succeeded.emit(mailAccounts[0])
|
||||
expect(toastInfoSpy).toHaveBeenCalledWith(
|
||||
`Saved account "${mailAccounts[0].name}".`
|
||||
)
|
||||
})
|
||||
|
||||
it('should support delete mail account, show error if needed', () => {
|
||||
completeSetup()
|
||||
let modal: NgbModalRef
|
||||
modalService.activeInstances.subscribe((refs) => (modal = refs[0]))
|
||||
component.deleteMailAccount(mailAccounts[0] as PaperlessMailAccount)
|
||||
const deleteDialog = modal.componentInstance as ConfirmDialogComponent
|
||||
const deleteSpy = jest.spyOn(mailAccountService, 'delete')
|
||||
const toastErrorSpy = jest.spyOn(toastService, 'showError')
|
||||
const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
|
||||
const listAllSpy = jest.spyOn(mailAccountService, 'listAll')
|
||||
deleteSpy.mockReturnValueOnce(
|
||||
throwError(() => new Error('error deleting mail account'))
|
||||
)
|
||||
deleteDialog.confirm()
|
||||
expect(toastErrorSpy).toBeCalled()
|
||||
deleteSpy.mockReturnValueOnce(of(true))
|
||||
deleteDialog.confirm()
|
||||
expect(listAllSpy).toHaveBeenCalled()
|
||||
expect(toastInfoSpy).toHaveBeenCalledWith('Deleted mail account')
|
||||
})
|
||||
|
||||
it('should support edit / create mail rule, show error if needed', () => {
|
||||
completeSetup()
|
||||
let modal: NgbModalRef
|
||||
modalService.activeInstances.subscribe((refs) => (modal = refs[0]))
|
||||
component.editMailRule(mailRules[0] as PaperlessMailRule)
|
||||
const editDialog = modal.componentInstance as MailRuleEditDialogComponent
|
||||
const toastErrorSpy = jest.spyOn(toastService, 'showError')
|
||||
const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
|
||||
editDialog.failed.emit()
|
||||
expect(toastErrorSpy).toBeCalled()
|
||||
editDialog.succeeded.emit(mailRules[0])
|
||||
expect(toastInfoSpy).toHaveBeenCalledWith(
|
||||
`Saved rule "${mailRules[0].name}".`
|
||||
)
|
||||
})
|
||||
|
||||
it('should support delete mail rule, show error if needed', () => {
|
||||
completeSetup()
|
||||
let modal: NgbModalRef
|
||||
modalService.activeInstances.subscribe((refs) => (modal = refs[0]))
|
||||
component.deleteMailRule(mailRules[0] as PaperlessMailRule)
|
||||
const deleteDialog = modal.componentInstance as ConfirmDialogComponent
|
||||
const deleteSpy = jest.spyOn(mailRuleService, 'delete')
|
||||
const toastErrorSpy = jest.spyOn(toastService, 'showError')
|
||||
const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
|
||||
const listAllSpy = jest.spyOn(mailRuleService, 'listAll')
|
||||
deleteSpy.mockReturnValueOnce(
|
||||
throwError(() => new Error('error deleting mail rule'))
|
||||
)
|
||||
deleteDialog.confirm()
|
||||
expect(toastErrorSpy).toBeCalled()
|
||||
deleteSpy.mockReturnValueOnce(of(true))
|
||||
deleteDialog.confirm()
|
||||
expect(listAllSpy).toHaveBeenCalled()
|
||||
expect(toastInfoSpy).toHaveBeenCalledWith('Deleted mail rule')
|
||||
})
|
||||
|
||||
it('should support edit permissions on mail rule objects', () => {
|
||||
completeSetup()
|
||||
const perms = {
|
||||
owner: 99,
|
||||
set_permissions: {
|
||||
view: {
|
||||
users: [1],
|
||||
groups: [2],
|
||||
},
|
||||
change: {
|
||||
users: [3],
|
||||
groups: [4],
|
||||
},
|
||||
},
|
||||
}
|
||||
let modal: NgbModalRef
|
||||
modalService.activeInstances.subscribe((refs) => (modal = refs[0]))
|
||||
const toastErrorSpy = jest.spyOn(toastService, 'showError')
|
||||
const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
|
||||
const rulePatchSpy = jest.spyOn(mailRuleService, 'patch')
|
||||
component.editPermissions(mailRules[0] as PaperlessMailRule)
|
||||
expect(modal).not.toBeUndefined()
|
||||
let dialog = modal.componentInstance as PermissionsDialogComponent
|
||||
expect(dialog.object).toEqual(mailRules[0])
|
||||
|
||||
rulePatchSpy.mockReturnValueOnce(
|
||||
throwError(() => new Error('error saving perms'))
|
||||
)
|
||||
dialog.confirmClicked.emit(perms)
|
||||
expect(rulePatchSpy).toHaveBeenCalled()
|
||||
expect(toastErrorSpy).toHaveBeenCalled()
|
||||
rulePatchSpy.mockReturnValueOnce(of(mailRules[0] as PaperlessMailRule))
|
||||
dialog.confirmClicked.emit(perms)
|
||||
expect(toastInfoSpy).toHaveBeenCalledWith('Permissions updated')
|
||||
|
||||
modalService.dismissAll()
|
||||
})
|
||||
|
||||
it('should support edit permissions on mail account objects', () => {
|
||||
completeSetup()
|
||||
const perms = {
|
||||
owner: 99,
|
||||
set_permissions: {
|
||||
view: {
|
||||
users: [1],
|
||||
groups: [2],
|
||||
},
|
||||
change: {
|
||||
users: [3],
|
||||
groups: [4],
|
||||
},
|
||||
},
|
||||
}
|
||||
let modal: NgbModalRef
|
||||
modalService.activeInstances.subscribe((refs) => (modal = refs[0]))
|
||||
const accountPatchSpy = jest.spyOn(mailAccountService, 'patch')
|
||||
component.editPermissions(mailAccounts[0] as PaperlessMailAccount)
|
||||
expect(modal).not.toBeUndefined()
|
||||
let dialog = modal.componentInstance as PermissionsDialogComponent
|
||||
expect(dialog.object).toEqual(mailAccounts[0])
|
||||
dialog = modal.componentInstance as PermissionsDialogComponent
|
||||
dialog.confirmClicked.emit(perms)
|
||||
expect(accountPatchSpy).toHaveBeenCalled()
|
||||
})
|
||||
})
|
File diff suppressed because it is too large
Load Diff
@@ -1,134 +0,0 @@
|
||||
<pngx-page-header title="File Tasks" i18n-title>
|
||||
<div class="btn-toolbar col col-md-auto">
|
||||
<button class="btn btn-sm btn-outline-secondary me-2" (click)="clearSelection()" [hidden]="selectedTasks.size === 0">
|
||||
<svg class="sidebaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#x"/>
|
||||
</svg> <ng-container i18n>Clear selection</ng-container>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-primary me-4" (click)="dismissTasks()" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.PaperlessTask }" [disabled]="tasksService.total === 0">
|
||||
<svg class="sidebaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#check2-all"/>
|
||||
</svg> <ng-container i18n>{{dismissButtonText}}</ng-container>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-primary" (click)="tasksService.reload()">
|
||||
<svg *ngIf="!tasksService.loading" class="sidebaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#arrow-clockwise"/>
|
||||
</svg>
|
||||
<ng-container *ngIf="tasksService.loading">
|
||||
<div class="spinner-border spinner-border-sm fw-normal" role="status"></div>
|
||||
<div class="visually-hidden" i18n>Loading...</div>
|
||||
</ng-container> <ng-container i18n>Refresh</ng-container>
|
||||
</button>
|
||||
</div>
|
||||
</pngx-page-header>
|
||||
|
||||
<ng-container *ngIf="!tasksService.completedFileTasks && 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>
|
||||
</ng-container>
|
||||
|
||||
<ng-template let-tasks="tasks" #tasksTemplate>
|
||||
<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" (click)="toggleAll($event); $event.stopPropagation();">
|
||||
<label class="form-check-label" for="all-tasks"></label>
|
||||
</div>
|
||||
</th>
|
||||
<th scope="col" i18n>Name</th>
|
||||
<th scope="col" class="d-none d-lg-table-cell" i18n>Created</th>
|
||||
<th scope="col" class="d-none d-lg-table-cell" *ngIf="activeTab !== 'started' && activeTab !== 'queued'" i18n>Results</th>
|
||||
<th scope="col" class="d-table-cell d-lg-none" i18n>Info</th>
|
||||
<th scope="col" i18n>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<ng-container *ngFor="let task of tasks | slice: (page-1) * pageSize : page * pageSize">
|
||||
<tr (click)="toggleSelected(task, $event); $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();">
|
||||
<label class="form-check-label" for="task{{task.id}}"></label>
|
||||
</div>
|
||||
</td>
|
||||
<td class="overflow-auto name-col">{{ task.task_file_name }}</td>
|
||||
<td class="d-none d-lg-table-cell">{{ task.date_created | customDate:'short' }}</td>
|
||||
<td class="d-none d-lg-table-cell" *ngIf="activeTab !== 'started' && activeTab !== 'queued'">
|
||||
<div *ngIf="task.result?.length > 50" class="result" (click)="expandTask(task); $event.stopPropagation();"
|
||||
[ngbPopover]="resultPopover" popoverClass="shadow small mobile" triggers="mouseenter:mouseleave" container="body">
|
||||
<span class="small d-none d-md-inline-block font-monospace text-muted">{{ task.result | slice:0:50 }}…</span>
|
||||
</div>
|
||||
<span *ngIf="task.result?.length <= 50" class="small d-none d-md-inline-block font-monospace text-muted">{{ task.result }}</span>
|
||||
<ng-template #resultPopover>
|
||||
<pre class="small mb-0">{{ task.result | slice:0:300 }}<ng-container *ngIf="task.result.length > 300">…</ng-container></pre>
|
||||
<ng-container *ngIf="task.result?.length > 300"><br/><em>(<ng-container i18n>click for full output</ng-container>)</em></ng-container>
|
||||
</ng-template>
|
||||
</td>
|
||||
<td class="d-lg-none">
|
||||
<button class="btn btn-link" (click)="expandTask(task); $event.stopPropagation();">
|
||||
<svg fill="currentColor" class="" width="1.2em" height="1.2em" style="vertical-align: text-top;" viewBox="0 0 16 16">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#info-circle" />
|
||||
</svg>
|
||||
</button>
|
||||
</td>
|
||||
<td scope="row">
|
||||
<div class="btn-group" role="group">
|
||||
<button class="btn btn-sm btn-outline-secondary" (click)="dismissTask(task); $event.stopPropagation();" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.PaperlessTask }">
|
||||
<svg class="sidebaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#check"/>
|
||||
</svg> <ng-container i18n>Dismiss</ng-container>
|
||||
</button>
|
||||
<ng-container *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }">
|
||||
<button *ngIf="task.related_document" class="btn btn-sm btn-outline-primary" (click)="dismissAndGo(task); $event.stopPropagation();">
|
||||
<svg class="sidebaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#file-text"/>
|
||||
</svg> <ng-container i18n>Open Document</ng-container>
|
||||
</button>
|
||||
</ng-container>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="p-0" [class.border-0]="expandedTask !== task.id" colspan="5">
|
||||
<pre #collapse="ngbCollapse" [ngbCollapse]="expandedTask !== task.id" class="small mb-0"><div class="small p-1 p-lg-3 ms-lg-3">{{ task.result }}</div></pre>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-container>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="pb-3 d-sm-flex justify-content-between align-items-center">
|
||||
<div class="pb-2 pb-sm-0" i18n *ngIf="tasks.length > 0">{tasks.length, plural, =1 {One {{this.activeTabLocalized}} task} other {{{tasks.length || 0}} total {{this.activeTabLocalized}} tasks}}</div>
|
||||
<ngb-pagination *ngIf="tasks.length > pageSize" [(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($event)">
|
||||
<li ngbNavItem="failed">
|
||||
<a ngbNavLink i18n>Failed<span *ngIf="tasksService.failedFileTasks.length > 0" class="badge bg-danger ms-2">{{tasksService.failedFileTasks.length}}</span></a>
|
||||
<ng-template ngbNavContent>
|
||||
<ng-container [ngTemplateOutlet]="tasksTemplate" [ngTemplateOutletContext]="{tasks:tasksService.failedFileTasks}"></ng-container>
|
||||
</ng-template>
|
||||
</li>
|
||||
<li ngbNavItem="completed">
|
||||
<a ngbNavLink i18n>Complete<span *ngIf="tasksService.completedFileTasks.length > 0" class="badge bg-secondary ms-2">{{tasksService.completedFileTasks.length}}</span></a>
|
||||
<ng-template ngbNavContent>
|
||||
<ng-container [ngTemplateOutlet]="tasksTemplate" [ngTemplateOutletContext]="{tasks:tasksService.completedFileTasks}"></ng-container>
|
||||
</ng-template>
|
||||
</li>
|
||||
<li ngbNavItem="started">
|
||||
<a ngbNavLink i18n>Started<span *ngIf="tasksService.startedFileTasks.length > 0" class="badge bg-secondary ms-2">{{tasksService.startedFileTasks.length}}</span></a>
|
||||
<ng-template ngbNavContent>
|
||||
<ng-container [ngTemplateOutlet]="tasksTemplate" [ngTemplateOutletContext]="{tasks:tasksService.startedFileTasks}"></ng-container>
|
||||
</ng-template>
|
||||
</li>
|
||||
<li ngbNavItem="queued">
|
||||
<a ngbNavLink i18n>Queued<span *ngIf="tasksService.queuedFileTasks.length > 0" class="badge bg-secondary ms-2">{{tasksService.queuedFileTasks.length}}</span></a>
|
||||
<ng-template ngbNavContent>
|
||||
<ng-container [ngTemplateOutlet]="tasksTemplate" [ngTemplateOutletContext]="{tasks:tasksService.queuedFileTasks}"></ng-container>
|
||||
</ng-template>
|
||||
</li>
|
||||
</ul>
|
||||
<div [ngbNavOutlet]="nav"></div>
|
@@ -1,28 +0,0 @@
|
||||
::ng-deep .popover {
|
||||
max-width: 350px;
|
||||
|
||||
pre {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
}
|
||||
|
||||
pre {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.result {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn .spinner-border-sm {
|
||||
width: 0.8rem;
|
||||
height: 0.8rem;
|
||||
}
|
||||
|
||||
@media (max-width: 575.98px) {
|
||||
.name-col {
|
||||
max-width: 150px;
|
||||
}
|
||||
}
|
@@ -1,272 +0,0 @@
|
||||
import { DatePipe } from '@angular/common'
|
||||
import {
|
||||
HttpTestingController,
|
||||
HttpClientTestingModule,
|
||||
} from '@angular/common/http/testing'
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||
import { By } from '@angular/platform-browser'
|
||||
import { Router } from '@angular/router'
|
||||
import { RouterTestingModule } from '@angular/router/testing'
|
||||
import {
|
||||
NgbModal,
|
||||
NgbModule,
|
||||
NgbNavItem,
|
||||
NgbModalRef,
|
||||
} from '@ng-bootstrap/ng-bootstrap'
|
||||
import { routes } from 'src/app/app-routing.module'
|
||||
import {
|
||||
PaperlessTask,
|
||||
PaperlessTaskType,
|
||||
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 { PermissionsService } from 'src/app/services/permissions.service'
|
||||
import { TasksService } from 'src/app/services/tasks.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 } from './tasks.component'
|
||||
import { PermissionsGuard } from 'src/app/guards/permissions.guard'
|
||||
|
||||
const tasks: PaperlessTask[] = [
|
||||
{
|
||||
id: 467,
|
||||
task_id: '11ca1a5b-9f81-442c-b2c8-7e4ae53657f1',
|
||||
task_file_name: 'test.pdf',
|
||||
date_created: new Date('2023-03-01T10:26:03.093116Z'),
|
||||
date_done: new Date('2023-03-01T10:26:07.223048Z'),
|
||||
type: PaperlessTaskType.File,
|
||||
status: PaperlessTaskStatus.Failed,
|
||||
result: 'test.pd: Not consuming test.pdf: It is a duplicate of test (#100)',
|
||||
acknowledged: false,
|
||||
related_document: null,
|
||||
},
|
||||
{
|
||||
id: 466,
|
||||
task_id: '10ca1a5b-3c08-442c-b2c8-7e4ae53657f1',
|
||||
task_file_name: '191092.pdf',
|
||||
date_created: new Date('2023-03-01T09:26:03.093116Z'),
|
||||
date_done: new Date('2023-03-01T09:26:07.223048Z'),
|
||||
type: PaperlessTaskType.File,
|
||||
status: PaperlessTaskStatus.Failed,
|
||||
result:
|
||||
'191092.pd: Not consuming 191092.pdf: It is a duplicate of 191092 (#311)',
|
||||
acknowledged: false,
|
||||
related_document: null,
|
||||
},
|
||||
{
|
||||
id: 465,
|
||||
task_id: '3612d477-bb04-44e3-985b-ac580dd496d8',
|
||||
task_file_name: 'Scan Jun 6, 2023 at 3.19 PM.pdf',
|
||||
date_created: new Date('2023-06-06T15:22:05.722323-07:00'),
|
||||
date_done: new Date('2023-06-06T15:22:14.564305-07:00'),
|
||||
type: PaperlessTaskType.File,
|
||||
status: PaperlessTaskStatus.Pending,
|
||||
result: null,
|
||||
acknowledged: false,
|
||||
related_document: null,
|
||||
},
|
||||
{
|
||||
id: 464,
|
||||
task_id: '2eac4716-2aa6-4dcd-9953-264e11656d7e',
|
||||
task_file_name: 'paperless-mail-l4dkg8ir',
|
||||
date_created: new Date('2023-06-04T11:24:32.898089-07:00'),
|
||||
date_done: new Date('2023-06-04T11:24:44.678605-07:00'),
|
||||
type: PaperlessTaskType.File,
|
||||
status: PaperlessTaskStatus.Complete,
|
||||
result: 'Success. New document id 422 created',
|
||||
acknowledged: false,
|
||||
related_document: 422,
|
||||
},
|
||||
{
|
||||
id: 463,
|
||||
task_id: '28125528-1575-4d6b-99e6-168906e8fa5c',
|
||||
task_file_name: 'onlinePaymentSummary.pdf',
|
||||
date_created: new Date('2023-06-01T13:49:51.631305-07:00'),
|
||||
date_done: new Date('2023-06-01T13:49:54.190220-07:00'),
|
||||
type: PaperlessTaskType.File,
|
||||
status: PaperlessTaskStatus.Complete,
|
||||
result: 'Success. New document id 421 created',
|
||||
acknowledged: false,
|
||||
related_document: 421,
|
||||
},
|
||||
{
|
||||
id: 462,
|
||||
task_id: 'a5b9ca47-0c8e-490f-a04c-6db5d5fc09e5',
|
||||
task_file_name: 'paperless-mail-_rrpmqk6',
|
||||
date_created: new Date('2023-06-07T02:54:35.694916Z'),
|
||||
date_done: null,
|
||||
type: PaperlessTaskType.File,
|
||||
status: PaperlessTaskStatus.Started,
|
||||
result: null,
|
||||
acknowledged: false,
|
||||
related_document: null,
|
||||
},
|
||||
]
|
||||
|
||||
describe('TasksComponent', () => {
|
||||
let component: TasksComponent
|
||||
let fixture: ComponentFixture<TasksComponent>
|
||||
let tasksService: TasksService
|
||||
let modalService: NgbModal
|
||||
let router: Router
|
||||
let httpTestingController: HttpTestingController
|
||||
|
||||
beforeEach(async () => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [
|
||||
TasksComponent,
|
||||
PageHeaderComponent,
|
||||
IfPermissionsDirective,
|
||||
CustomDatePipe,
|
||||
ConfirmDialogComponent,
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
provide: PermissionsService,
|
||||
useValue: {
|
||||
currentUserCan: () => true,
|
||||
},
|
||||
},
|
||||
CustomDatePipe,
|
||||
DatePipe,
|
||||
PermissionsGuard,
|
||||
],
|
||||
imports: [
|
||||
NgbModule,
|
||||
HttpClientTestingModule,
|
||||
RouterTestingModule.withRoutes(routes),
|
||||
],
|
||||
}).compileComponents()
|
||||
|
||||
tasksService = TestBed.inject(TasksService)
|
||||
httpTestingController = TestBed.inject(HttpTestingController)
|
||||
modalService = TestBed.inject(NgbModal)
|
||||
router = TestBed.inject(Router)
|
||||
fixture = TestBed.createComponent(TasksComponent)
|
||||
component = fixture.componentInstance
|
||||
fixture.detectChanges()
|
||||
httpTestingController
|
||||
.expectOne(`${environment.apiBaseUrl}tasks/`)
|
||||
.flush(tasks)
|
||||
})
|
||||
|
||||
it('should display file tasks in 4 tabs by status', () => {
|
||||
const tabButtons = fixture.debugElement.queryAll(By.directive(NgbNavItem))
|
||||
|
||||
let currentTasksLength = tasks.filter(
|
||||
(t) => t.status === PaperlessTaskStatus.Failed
|
||||
).length
|
||||
component.activeTab = 'failed'
|
||||
fixture.detectChanges()
|
||||
expect(tabButtons[0].nativeElement.textContent).toEqual(
|
||||
`Failed${currentTasksLength}`
|
||||
)
|
||||
expect(
|
||||
fixture.debugElement.queryAll(By.css('input[type="checkbox"]'))
|
||||
).toHaveLength(currentTasksLength + 1)
|
||||
|
||||
currentTasksLength = tasks.filter(
|
||||
(t) => t.status === PaperlessTaskStatus.Complete
|
||||
).length
|
||||
component.activeTab = 'completed'
|
||||
fixture.detectChanges()
|
||||
expect(tabButtons[1].nativeElement.textContent).toEqual(
|
||||
`Complete${currentTasksLength}`
|
||||
)
|
||||
|
||||
currentTasksLength = tasks.filter(
|
||||
(t) => t.status === PaperlessTaskStatus.Started
|
||||
).length
|
||||
component.activeTab = 'started'
|
||||
fixture.detectChanges()
|
||||
expect(tabButtons[2].nativeElement.textContent).toEqual(
|
||||
`Started${currentTasksLength}`
|
||||
)
|
||||
|
||||
currentTasksLength = tasks.filter(
|
||||
(t) => t.status === PaperlessTaskStatus.Pending
|
||||
).length
|
||||
component.activeTab = 'queued'
|
||||
fixture.detectChanges()
|
||||
expect(tabButtons[3].nativeElement.textContent).toEqual(
|
||||
`Queued${currentTasksLength}`
|
||||
)
|
||||
})
|
||||
|
||||
it('should to go page 1 between tab switch', () => {
|
||||
component.page = 10
|
||||
component.duringTabChange(2)
|
||||
expect(component.page).toEqual(1)
|
||||
})
|
||||
|
||||
it('should support expanding / collapsing one task at a time', () => {
|
||||
component.expandTask(tasks[0])
|
||||
expect(component.expandedTask).toEqual(tasks[0].id)
|
||||
component.expandTask(tasks[1])
|
||||
expect(component.expandedTask).toEqual(tasks[1].id)
|
||||
component.expandTask(tasks[1])
|
||||
expect(component.expandedTask).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should support dismiss single task', () => {
|
||||
const dismissSpy = jest.spyOn(tasksService, 'dismissTasks')
|
||||
component.dismissTask(tasks[0])
|
||||
expect(dismissSpy).toHaveBeenCalledWith(new Set([tasks[0].id]))
|
||||
})
|
||||
|
||||
it('should support dismiss specific checked tasks', () => {
|
||||
component.toggleSelected(tasks[0])
|
||||
component.toggleSelected(tasks[1])
|
||||
component.toggleSelected(tasks[3])
|
||||
component.toggleSelected(tasks[3]) // uncheck, for coverage
|
||||
const selected = new Set([tasks[0].id, tasks[1].id])
|
||||
expect(component.selectedTasks).toEqual(selected)
|
||||
let modal: NgbModalRef
|
||||
modalService.activeInstances.subscribe((m) => (modal = m[m.length - 1]))
|
||||
const dismissSpy = jest.spyOn(tasksService, 'dismissTasks')
|
||||
fixture.detectChanges()
|
||||
component.dismissTasks()
|
||||
expect(modal).not.toBeUndefined()
|
||||
modal.componentInstance.confirmClicked.emit()
|
||||
expect(dismissSpy).toHaveBeenCalledWith(selected)
|
||||
})
|
||||
|
||||
it('should support dismiss all tasks', () => {
|
||||
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)))
|
||||
})
|
||||
|
||||
it('should support toggle all tasks', () => {
|
||||
const toggleCheck = fixture.debugElement.query(
|
||||
By.css('input[type=checkbox]')
|
||||
)
|
||||
toggleCheck.nativeElement.dispatchEvent(new MouseEvent('click'))
|
||||
fixture.detectChanges()
|
||||
expect(component.selectedTasks).toEqual(
|
||||
new Set(
|
||||
tasks
|
||||
.filter((t) => t.status === PaperlessTaskStatus.Failed)
|
||||
.map((t) => t.id)
|
||||
)
|
||||
)
|
||||
toggleCheck.nativeElement.dispatchEvent(new MouseEvent('click'))
|
||||
fixture.detectChanges()
|
||||
expect(component.selectedTasks).toEqual(new Set())
|
||||
})
|
||||
|
||||
it('should support dismiss and open a document', () => {
|
||||
const routerSpy = jest.spyOn(router, 'navigate')
|
||||
component.dismissAndGo(tasks[3])
|
||||
expect(routerSpy).toHaveBeenCalledWith([
|
||||
'documents',
|
||||
tasks[3].related_document,
|
||||
])
|
||||
})
|
||||
})
|
@@ -1,139 +0,0 @@
|
||||
import { Component, OnInit, OnDestroy } from '@angular/core'
|
||||
import { Router } from '@angular/router'
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { first } from 'rxjs'
|
||||
import { PaperlessTask } from 'src/app/data/paperless-task'
|
||||
import { TasksService } from 'src/app/services/tasks.service'
|
||||
import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component'
|
||||
import { ComponentWithPermissions } from '../../with-permissions/with-permissions.component'
|
||||
|
||||
@Component({
|
||||
selector: 'pngx-tasks',
|
||||
templateUrl: './tasks.component.html',
|
||||
styleUrls: ['./tasks.component.scss'],
|
||||
})
|
||||
export class TasksComponent
|
||||
extends ComponentWithPermissions
|
||||
implements OnInit, OnDestroy
|
||||
{
|
||||
public activeTab: string
|
||||
public selectedTasks: Set<number> = new Set()
|
||||
public expandedTask: number
|
||||
|
||||
public pageSize: number = 25
|
||||
public page: number = 1
|
||||
|
||||
get dismissButtonText(): string {
|
||||
return this.selectedTasks.size > 0
|
||||
? $localize`Dismiss selected`
|
||||
: $localize`Dismiss all`
|
||||
}
|
||||
|
||||
constructor(
|
||||
public tasksService: TasksService,
|
||||
private modalService: NgbModal,
|
||||
private readonly router: Router
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.tasksService.reload()
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.tasksService.cancelPending()
|
||||
}
|
||||
|
||||
dismissTask(task: PaperlessTask) {
|
||||
this.dismissTasks(task)
|
||||
}
|
||||
|
||||
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 (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} ` + $localize`tasks?`
|
||||
modal.componentInstance.btnClass = 'btn-warning'
|
||||
modal.componentInstance.btnCaption = $localize`Dismiss`
|
||||
modal.componentInstance.confirmClicked.pipe(first()).subscribe(() => {
|
||||
modal.componentInstance.buttonsEnabled = false
|
||||
modal.close()
|
||||
this.tasksService.dismissTasks(tasks)
|
||||
this.selectedTasks.clear()
|
||||
})
|
||||
} else {
|
||||
this.tasksService.dismissTasks(tasks)
|
||||
this.selectedTasks.clear()
|
||||
}
|
||||
}
|
||||
|
||||
dismissAndGo(task: PaperlessTask) {
|
||||
this.dismissTask(task)
|
||||
this.router.navigate(['documents', task.related_document])
|
||||
}
|
||||
|
||||
expandTask(task: PaperlessTask) {
|
||||
this.expandedTask = this.expandedTask == task.id ? undefined : task.id
|
||||
}
|
||||
|
||||
toggleSelected(task: PaperlessTask) {
|
||||
this.selectedTasks.has(task.id)
|
||||
? this.selectedTasks.delete(task.id)
|
||||
: this.selectedTasks.add(task.id)
|
||||
}
|
||||
|
||||
get currentTasks(): PaperlessTask[] {
|
||||
let tasks: PaperlessTask[] = []
|
||||
switch (this.activeTab) {
|
||||
case 'queued':
|
||||
tasks = this.tasksService.queuedFileTasks
|
||||
break
|
||||
case 'started':
|
||||
tasks = this.tasksService.startedFileTasks
|
||||
break
|
||||
case 'completed':
|
||||
tasks = this.tasksService.completedFileTasks
|
||||
break
|
||||
case 'failed':
|
||||
tasks = this.tasksService.failedFileTasks
|
||||
break
|
||||
}
|
||||
return tasks
|
||||
}
|
||||
|
||||
toggleAll(event: PointerEvent) {
|
||||
if ((event.target as HTMLInputElement).checked) {
|
||||
this.selectedTasks = new Set(this.currentTasks.map((t) => t.id))
|
||||
} else {
|
||||
this.clearSelection()
|
||||
}
|
||||
}
|
||||
|
||||
clearSelection() {
|
||||
this.selectedTasks.clear()
|
||||
}
|
||||
|
||||
duringTabChange(navID: number) {
|
||||
this.page = 1
|
||||
}
|
||||
|
||||
get activeTabLocalized(): string {
|
||||
switch (this.activeTab) {
|
||||
case 'queued':
|
||||
return $localize`queued`
|
||||
case 'started':
|
||||
return $localize`started`
|
||||
case 'completed':
|
||||
return $localize`completed`
|
||||
case 'failed':
|
||||
return $localize`failed`
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user