Chorehancement: update to Angular v22, 'zoneless' / 'reactive' (#13114)

This commit is contained in:
shamoon
2026-07-10 00:42:16 -07:00
committed by GitHub
parent f244442c65
commit 106b41a15c
213 changed files with 5363 additions and 5842 deletions
@@ -34,16 +34,16 @@
</div>
</li>
@if (loadingAccounts) {
@if (loadingAccounts()) {
<li class="list-group-item">
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
<ng-container i18n>Loading...</ng-container>
</li>
}
@for (account of mailAccounts; track account) {
@for (account of mailAccounts(); track account) {
<li class="list-group-item">
<div class="row fade" [class.show]="showAccounts">
<div class="row fade" [class.show]="showAccounts()">
<div class="col d-flex align-items-center">
<button class="btn btn-link p-0 text-start" type="button" (click)="editMailAccount(account)" [disabled]="!permissionsService.currentUserCan(PermissionAction.Change, PermissionType.MailAccount) || !userCanEdit(account)">
{{account.name}}@switch (account.account_type) {
@@ -91,7 +91,7 @@
</div>
</li>
}
@if (!loadingAccounts && mailAccounts.length === 0) {
@if (!loadingAccounts() && mailAccounts().length === 0) {
<li class="list-group-item" i18n>No mail accounts defined.</li>
}
</ul>
@@ -117,19 +117,19 @@
</div>
</li>
@if (loadingRules) {
@if (loadingRules()) {
<li class="list-group-item">
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
<ng-container i18n>Loading...</ng-container>
</li>
}
@for (rule of mailRules; track rule) {
@for (rule of mailRules(); track rule) {
<li class="list-group-item">
<div class="row fade" [class.show]="showRules">
<div class="row fade" [class.show]="showRules()">
<div class="col d-flex align-items-center"><button class="btn btn-link p-0 text-start" type="button" (click)="editMailRule(rule)" [disabled]="!permissionsService.currentUserCan(PermissionAction.Change, PermissionType.MailRule) || !userCanEdit(rule)">{{rule.name}}</button></div>
<div class="col-1 d-flex align-items-center d-none d-sm-flex">{{rule.order}}</div>
<div class="col-2 d-flex align-items-center">{{ mailAccountsById.get(rule.account)?.name }}</div>
<div class="col-2 d-flex align-items-center">{{ mailAccountsById().get(rule.account)?.name }}</div>
<div class="col-2 d-flex align-items-center d-none d-sm-flex">
<div class="form-check form-switch mb-0">
<input #inputField type="checkbox" class="form-check-input cursor-pointer" [id]="rule.id+'_enable'" [(ngModel)]="rule.enabled" (change)="onMailRuleEnableToggled(rule)" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.MailRule }">
@@ -179,14 +179,14 @@
</div>
</li>
}
@if (!loadingRules && mailRules.length === 0) {
@if (!loadingRules() && mailRules().length === 0) {
<li class="list-group-item" i18n>No mail rules defined.</li>
}
</ul>
</ng-container>
@if (!mailAccounts || !mailRules) {
@if (!mailAccounts() || !mailRules()) {
<div>
<div class="spinner-border spinner-border-sm fw-normal ms-2 me-auto" role="status"></div>
<div class="visually-hidden" i18n>Loading...</div>
@@ -113,7 +113,7 @@ describe('MailComponent', () => {
permissionsService = TestBed.inject(PermissionsService)
activatedRoute = TestBed.inject(ActivatedRoute)
settingsService = TestBed.inject(SettingsService)
settingsService.currentUser = { id: 1 }
settingsService.currentUser.set({ id: 1 })
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
jest
.spyOn(permissionsService, 'currentUserHasObjectPermissions')
@@ -261,7 +261,7 @@ describe('MailComponent', () => {
const editDialog = modal.componentInstance as MailRuleEditDialogComponent
expect(editDialog.object.id).toBeNull()
expect(editDialog.object.name).toEqual(`${mailRules[0].name} (copy)`)
expect(editDialog.dialogMode).toEqual(EditDialogMode.CREATE)
expect(editDialog.dialogMode()).toEqual(EditDialogMode.CREATE)
})
it('should support delete mail rule, show error if needed', () => {
@@ -414,6 +414,6 @@ describe('MailComponent', () => {
modalService.activeInstances.subscribe((refs) => (modal = refs[0]))
component.viewProcessedMail(mailRules[0] as MailRule)
const dialog = modal.componentInstance as any
expect(dialog.rule).toEqual(mailRules[0])
expect(dialog.rule()).toEqual(mailRules[0])
})
})
@@ -1,9 +1,9 @@
import { Component, OnDestroy, OnInit, inject } from '@angular/core'
import { Component, OnDestroy, OnInit, inject, signal } from '@angular/core'
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { ActivatedRoute } from '@angular/router'
import { NgbDropdownModule, NgbModal } from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { Subject, delay, first, takeUntil, tap } from 'rxjs'
import { Subject, first, takeUntil, tap } from 'rxjs'
import { MailAccount, MailAccountType } from 'src/app/data/mail-account'
import { MailRule } from 'src/app/data/mail-rule'
import { ObjectWithPermissions } from 'src/app/data/object-with-permissions'
@@ -56,19 +56,17 @@ export class MailComponent
public MailAccountType = MailAccountType
private _mailAccounts: MailAccount[] = []
readonly mailAccounts = signal<MailAccount[]>([])
public get mailAccounts() {
return this._mailAccounts
}
private set mailAccounts(accounts: MailAccount[]) {
this._mailAccounts = accounts
this.mailAccountsById = new Map(
accounts.map((account) => [account.id, account])
private setMailAccounts(accounts: MailAccount[]) {
this.mailAccounts.set(accounts)
this.mailAccountsById.set(
new Map(accounts.map((account) => [account.id, account]))
)
}
public mailAccountsById: Map<number, MailAccount> = new Map()
public mailRules: MailRule[] = []
readonly mailAccountsById = signal<Map<number, MailAccount>>(new Map())
readonly mailRules = signal<MailRule[]>([])
unsubscribeNotifier: Subject<any> = new Subject()
oAuthAccountId: number
@@ -81,10 +79,10 @@ export class MailComponent
return this.settingsService.get(SETTINGS_KEYS.OUTLOOK_OAUTH_URL)
}
public loadingRules: boolean = true
public showRules: boolean = false
public loadingAccounts: boolean = true
public showAccounts: boolean = false
readonly loadingRules = signal(true)
readonly showRules = signal(false)
readonly loadingAccounts = signal(true)
readonly showAccounts = signal(false)
ngOnInit(): void {
this.mailAccountService
@@ -93,23 +91,21 @@ export class MailComponent
first(),
takeUntil(this.unsubscribeNotifier),
tap((r) => {
this.mailAccounts = r.results
this.setMailAccounts(r.results)
this.loadingAccounts.set(false)
this.showAccounts.set(true)
if (this.oAuthAccountId) {
this.editMailAccount(
this.mailAccounts.find(
this.mailAccounts().find(
(account) => account.id === this.oAuthAccountId
)
)
}
}),
delay(100)
})
)
.subscribe({
next: () => {
this.loadingAccounts = false
this.showAccounts = true
},
error: (e) => {
this.loadingAccounts.set(false)
this.toastService.showError(
$localize`Error retrieving mail accounts`,
e
@@ -123,16 +119,14 @@ export class MailComponent
first(),
takeUntil(this.unsubscribeNotifier),
tap((r) => {
this.mailRules = r.results
}),
delay(100)
this.mailRules.set(r.results)
this.loadingRules.set(false)
this.showRules.set(true)
})
)
.subscribe({
next: (r) => {
this.loadingRules = false
this.showRules = true
},
error: (e) => {
this.loadingRules.set(false)
this.toastService.showError($localize`Error retrieving mail rules`, e)
},
})
@@ -143,9 +137,9 @@ export class MailComponent
if (success) {
this.toastService.showInfo($localize`OAuth2 authentication success`)
this.oAuthAccountId = parseInt(params.get('account_id'))
if (this.mailAccounts.length > 0) {
if (this.mailAccounts().length > 0) {
this.editMailAccount(
this.mailAccounts.find(
this.mailAccounts().find(
(account) => account.id === this.oAuthAccountId
)
)
@@ -168,9 +162,9 @@ export class MailComponent
backdrop: 'static',
size: 'xl',
})
modal.componentInstance.dialogMode = account
? EditDialogMode.EDIT
: EditDialogMode.CREATE
modal.componentInstance.dialogMode.set(
account ? EditDialogMode.EDIT : EditDialogMode.CREATE
)
modal.componentInstance.object = account
modal.componentInstance.succeeded
.pipe(takeUntil(this.unsubscribeNotifier))
@@ -182,7 +176,7 @@ export class MailComponent
this.mailAccountService
.listAll(null, null, { full_perms: true })
.subscribe((r) => {
this.mailAccounts = r.results
this.setMailAccounts(r.results)
})
})
modal.componentInstance.failed
@@ -213,7 +207,7 @@ export class MailComponent
this.mailAccountService
.listAll(null, null, { full_perms: true })
.subscribe((r) => {
this.mailAccounts = r.results
this.setMailAccounts(r.results)
})
},
error: (e) => {
@@ -247,8 +241,9 @@ export class MailComponent
backdrop: 'static',
size: 'xl',
})
modal.componentInstance.dialogMode =
modal.componentInstance.dialogMode.set(
rule && !forceCreate ? EditDialogMode.EDIT : EditDialogMode.CREATE
)
modal.componentInstance.object = rule
modal.componentInstance.succeeded
.pipe(takeUntil(this.unsubscribeNotifier))
@@ -258,7 +253,7 @@ export class MailComponent
this.mailRuleService
.listAll(null, null, { full_perms: true })
.subscribe((r) => {
this.mailRules = r.results
this.mailRules.set(r.results)
})
})
modal.componentInstance.failed
@@ -314,7 +309,7 @@ export class MailComponent
this.mailRuleService
.listAll(null, null, { full_perms: true })
.subscribe((r) => {
this.mailRules = r.results
this.mailRules.set(r.results)
})
},
error: (e) => {
@@ -336,7 +331,7 @@ export class MailComponent
dialog.object = object
modal.componentInstance.confirmClicked.subscribe(
({ permissions, merge }) => {
modal.componentInstance.buttonsEnabled = false
modal.componentInstance.buttonsEnabled.set(false)
const service: AbstractPaperlessService<MailRule | MailAccount> =
'account' in object ? this.mailRuleService : this.mailAccountService
object.owner = permissions['owner']
@@ -362,7 +357,7 @@ export class MailComponent
backdrop: 'static',
size: 'xl',
})
modal.componentInstance.rule = rule
modal.componentInstance.rule.set(rule)
}
userCanEdit(obj: ObjectWithPermissions): boolean {
@@ -1,5 +1,5 @@
<div class="modal-header">
<h6 class="modal-title" id="modal-basic-title" i18n>Processed Mail for <em>{{ rule.name }}</em></h6>
<h6 class="modal-title" id="modal-basic-title" i18n>Processed Mail for <em>{{ rule().name }}</em></h6>
<button class="btn btn-sm btn-link text-muted me-auto p-0 p-md-2" title="What's this?" i18n-title type="button" [ngbPopover]="infoPopover" [autoClose]="true">
<i-bs name="question-circle"></i-bs>
</button>
@@ -10,13 +10,13 @@
<button type="button" class="btn-close" aria-label="Close" (click)="close()"></button>
</div>
<div class="modal-body">
@if (loading) {
@if (loading()) {
<div class="text-center my-5">
<div class="spinner-border" role="status">
<span class="visually-hidden" i18n>Loading...</span>
</div>
</div>
} @else if (processedMails.length === 0) {
} @else if (processedMails().length === 0) {
<span i18n>No processed email messages found.</span>
} @else {
<div class="table-responsive">
@@ -25,7 +25,7 @@
<tr>
<th scope="col" style="width: 40px;">
<div class="form-check m-0 ms-2 me-n2">
<input type="checkbox" class="form-check-input" id="all-objects" [(ngModel)]="toggleAllEnabled" [disabled]="processedMails.length === 0" (click)="toggleAll($event); $event.stopPropagation();">
<input type="checkbox" class="form-check-input" id="all-objects" [ngModel]="toggleAllEnabled()" (ngModelChange)="toggleAllEnabled.set($event)" [disabled]="processedMails().length === 0" (click)="toggleAll($event); $event.stopPropagation();">
<label class="form-check-label" for="all-objects"></label>
</div>
</th>
@@ -37,7 +37,7 @@
</tr>
</thead>
<tbody>
@for (mail of processedMails; track mail.id) {
@for (mail of processedMails(); track mail.id) {
<ng-template #statusTooltip>
<div class="small text-light font-monospace">
{{mail.status}}
@@ -46,7 +46,7 @@
<tr>
<td>
<div class="form-check m-0 ms-2 me-n2">
<input type="checkbox" class="form-check-input" [id]="mail.id" [checked]="selectedMailIds.has(mail.id)" (click)="toggleSelected(mail); $event.stopPropagation();">
<input type="checkbox" class="form-check-input" [id]="mail.id" [checked]="selectedMailIds().has(mail.id)" (click)="toggleSelected(mail); $event.stopPropagation();">
<label class="form-check-label" [for]="mail.id"></label>
</div>
</td>
@@ -82,7 +82,7 @@
</table>
</div>
<div class="btn-toolbar">
<button type="button" class="btn btn-outline-secondary me-2" (click)="clearSelection()" [disabled]="selectedMailIds.size === 0" i18n>Clear</button>
<button type="button" class="btn btn-outline-secondary me-2" (click)="clearSelection()" [disabled]="selectedMailIds().size === 0" i18n>Clear</button>
<pngx-confirm-button
label="Delete selected"
i18n-label
@@ -90,12 +90,12 @@
i18n-title
buttonClasses="btn-outline-danger"
iconName="trash"
[disabled]="selectedMailIds.size === 0"
[disabled]="selectedMailIds().size === 0"
(confirm)="deleteSelected()">
</pngx-confirm-button>
<div class="ms-auto">
<ngb-pagination
[collectionSize]="processedMails.length"
[collectionSize]="processedMails().length"
[(page)]="page"
[pageSize]="50"
[maxSize]="5"
@@ -64,7 +64,7 @@ describe('ProcessedMailDialogComponent', () => {
toastService = TestBed.inject(ToastService)
fixture = TestBed.createComponent(ProcessedMailDialogComponent)
component = fixture.componentInstance
component.rule = rule
component.rule.set(rule)
})
afterEach(() => {
@@ -83,8 +83,8 @@ describe('ProcessedMailDialogComponent', () => {
fixture.detectChanges()
const req = expectListRequest(rule.id)
req.flush({ count: 2, results: mails })
expect(component.loading).toBeFalsy()
expect(component.processedMails).toEqual(mails)
expect(component.loading()).toBeFalsy()
expect(component.processedMails()).toEqual(mails)
})
it('should delete selected mails and reload', () => {
@@ -94,8 +94,8 @@ describe('ProcessedMailDialogComponent', () => {
initialReq.flush({ count: 0, results: [] })
// select a couple of mails and delete
component.selectedMailIds.add(5)
component.selectedMailIds.add(6)
component.selectedMailIds().add(5)
component.selectedMailIds().add(6)
const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
component.deleteSelected()
@@ -127,18 +127,18 @@ describe('ProcessedMailDialogComponent', () => {
header.dispatchEvent(new Event('click'))
header.checked = true
header.dispatchEvent(new Event('click'))
expect(component.selectedMailIds.size).toEqual(mails.length)
expect(component.selectedMailIds().size).toEqual(mails.length)
// toggle a single mail
component.toggleSelected(mails[0] as any)
expect(component.selectedMailIds.has(mails[0].id)).toBeFalsy()
expect(component.selectedMailIds().has(mails[0].id)).toBeFalsy()
component.toggleSelected(mails[0] as any)
expect(component.selectedMailIds.has(mails[0].id)).toBeTruthy()
expect(component.selectedMailIds().has(mails[0].id)).toBeTruthy()
// clear selection
component.clearSelection()
expect(component.selectedMailIds.size).toEqual(0)
expect(component.toggleAllEnabled).toBeFalsy()
expect(component.selectedMailIds().size).toEqual(0)
expect(component.toggleAllEnabled()).toBeFalsy()
})
it('should close the dialog', () => {
@@ -1,5 +1,5 @@
import { SlicePipe } from '@angular/common'
import { Component, inject, Input, OnInit } from '@angular/core'
import { Component, inject, OnInit, signal } from '@angular/core'
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import {
NgbActiveModal,
@@ -36,16 +36,14 @@ export class ProcessedMailDialogComponent implements OnInit {
private readonly processedMailService = inject(ProcessedMailService)
private readonly toastService = inject(ToastService)
public processedMails: ProcessedMail[] = []
public loading: boolean = true
public toggleAllEnabled: boolean = false
public readonly selectedMailIds: Set<number> = new Set<number>()
readonly rule = signal<MailRule>(undefined)
readonly processedMails = signal<ProcessedMail[]>([])
readonly loading = signal(true)
readonly toggleAllEnabled = signal(false)
readonly selectedMailIds = signal<Set<number>>(new Set())
public page: number = 1
@Input() rule: MailRule
ngOnInit(): void {
this.loadProcessedMails()
}
@@ -55,19 +53,19 @@ export class ProcessedMailDialogComponent implements OnInit {
}
private loadProcessedMails(): void {
this.loading = true
this.loading.set(true)
this.clearSelection()
this.processedMailService
.list(this.page, 50, 'processed_at', true, { rule: this.rule.id })
.list(this.page, 50, 'processed_at', true, { rule: this.rule().id })
.subscribe((result) => {
this.processedMails = result.results
this.loading = false
this.processedMails.set(result.results)
this.loading.set(false)
})
}
public deleteSelected(): void {
this.processedMailService
.bulk_delete(Array.from(this.selectedMailIds))
.bulk_delete(Array.from(this.selectedMailIds()))
.subscribe(() => {
this.toastService.showInfo($localize`Processed mail(s) deleted`)
this.loadProcessedMails()
@@ -75,22 +73,25 @@ export class ProcessedMailDialogComponent implements OnInit {
}
public toggleAll(event: PointerEvent) {
const selectedMailIds = new Set<number>()
if ((event.target as HTMLInputElement).checked) {
this.selectedMailIds.clear()
this.processedMails.forEach((mail) => this.selectedMailIds.add(mail.id))
this.processedMails().forEach((mail) => selectedMailIds.add(mail.id))
this.selectedMailIds.set(selectedMailIds)
} else {
this.clearSelection()
}
}
public clearSelection() {
this.toggleAllEnabled = false
this.selectedMailIds.clear()
this.toggleAllEnabled.set(false)
this.selectedMailIds.set(new Set())
}
public toggleSelected(mail: ProcessedMail) {
this.selectedMailIds.has(mail.id)
? this.selectedMailIds.delete(mail.id)
: this.selectedMailIds.add(mail.id)
const selectedMailIds = new Set(this.selectedMailIds())
selectedMailIds.has(mail.id)
? selectedMailIds.delete(mail.id)
: selectedMailIds.add(mail.id)
this.selectedMailIds.set(selectedMailIds)
}
}