mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2026-09-17 07:07:58 +00:00
Chorehancement: update to Angular v22, 'zoneless' / 'reactive' (#13114)
This commit is contained in:
+15
-15
@@ -7,38 +7,38 @@
|
||||
<div class="modal-body">
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-6">
|
||||
<pngx-input-text i18n-title title="Email" formControlName="email" (keyup)="onEmailKeyUp($event)" [error]="error?.email"></pngx-input-text>
|
||||
<pngx-input-text i18n-title title="Email" formControlName="email" (keyup)="onEmailKeyUp($event)" [error]="error()?.email"></pngx-input-text>
|
||||
<div ngbAccordion>
|
||||
<div ngbAccordionItem="first" [collapsed]="!showEmailConfirm" class="border-0 bg-transparent">
|
||||
<div ngbAccordionItem="first" [collapsed]="!showEmailConfirm()" class="border-0 bg-transparent">
|
||||
<div ngbAccordionCollapse>
|
||||
<div ngbAccordionBody class="p-0 pb-3">
|
||||
<pngx-input-text i18n-title title="Confirm Email" formControlName="email_confirm" (keyup)="onEmailConfirmKeyUp($event)" autocomplete="email" [error]="error?.email_confirm"></pngx-input-text>
|
||||
<pngx-input-text i18n-title title="Confirm Email" formControlName="email_confirm" (keyup)="onEmailConfirmKeyUp($event)" autocomplete="email" [error]="error()?.email_confirm"></pngx-input-text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<pngx-input-password i18n-title title="Password" formControlName="password" (keyup)="onPasswordKeyUp($event)" [showReveal]="true" autocomplete="current-password" [error]="error?.password"></pngx-input-password>
|
||||
<pngx-input-password i18n-title title="Password" formControlName="password" (keyup)="onPasswordKeyUp($event)" [showReveal]="true" autocomplete="current-password" [error]="error()?.password"></pngx-input-password>
|
||||
<div ngbAccordion>
|
||||
<div ngbAccordionItem="first" [collapsed]="!showPasswordConfirm" class="border-0 bg-transparent">
|
||||
<div ngbAccordionItem="first" [collapsed]="!showPasswordConfirm()" class="border-0 bg-transparent">
|
||||
<div ngbAccordionCollapse>
|
||||
<div ngbAccordionBody class="p-0 pb-3">
|
||||
<pngx-input-password i18n-title title="Confirm Password" formControlName="password_confirm" (keyup)="onPasswordConfirmKeyUp($event)" autocomplete="new-password" [error]="error?.password_confirm"></pngx-input-password>
|
||||
<pngx-input-password i18n-title title="Confirm Password" formControlName="password_confirm" (keyup)="onPasswordConfirmKeyUp($event)" autocomplete="new-password" [error]="error()?.password_confirm"></pngx-input-password>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<pngx-input-text i18n-title title="First name" formControlName="first_name" [error]="error?.first_name"></pngx-input-text>
|
||||
<pngx-input-text i18n-title title="Last name" formControlName="last_name" [error]="error?.first_name"></pngx-input-text>
|
||||
<pngx-input-text i18n-title title="First name" formControlName="first_name" [error]="error()?.first_name"></pngx-input-text>
|
||||
<pngx-input-text i18n-title title="Last name" formControlName="last_name" [error]="error()?.first_name"></pngx-input-text>
|
||||
<div class="mb-3">
|
||||
<label class="form-label" i18n>API Auth Token</label>
|
||||
<div class="position-relative">
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" formControlName="auth_token" readonly>
|
||||
<button type="button" class="btn btn-outline-secondary" (click)="copyAuthToken()" i18n-title title="Copy">
|
||||
@if (!copied) {
|
||||
@if (!copied()) {
|
||||
<i-bs width="1em" height="1em" name="clipboard-fill"></i-bs>
|
||||
}
|
||||
@if (copied) {
|
||||
@if (copied()) {
|
||||
<i-bs width="1em" height="1em" name="clipboard-check-fill"></i-bs>
|
||||
}
|
||||
<span class="visually-hidden" i18n>Copy</span>
|
||||
@@ -51,7 +51,7 @@
|
||||
(confirm)="generateAuthToken()">
|
||||
</pngx-confirm-button>
|
||||
</div>
|
||||
<span class="badge copied-badge bg-primary small fade ms-4 position-absolute top-50 translate-middle-y pe-none z-3" [class.show]="copied" i18n>Copied!</span>
|
||||
<span class="badge copied-badge bg-primary small fade ms-4 position-absolute top-50 translate-middle-y pe-none z-3" [class.show]="copied()" i18n>Copied!</span>
|
||||
</div>
|
||||
<div class="form-text text-muted text-end fst-italic" i18n>Warning: changing the token cannot be undone</div>
|
||||
</div>
|
||||
@@ -155,10 +155,10 @@
|
||||
}
|
||||
</ul>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary ms-2" (click)="copyRecoveryCodes()" i18n-title title="Copy">
|
||||
@if (!codesCopied) {
|
||||
@if (!codesCopied()) {
|
||||
<i-bs width="1em" height="1em" name="clipboard-fill" class="me-1"></i-bs><span i18n>Copy codes</span>
|
||||
}
|
||||
@if (codesCopied) {
|
||||
@if (codesCopied()) {
|
||||
<i-bs width="1em" height="1em" name="clipboard-check-fill" class="text-primary me-1"></i-bs><span class="text-primary" i18n>Copied!</span>
|
||||
}
|
||||
</button>
|
||||
@@ -179,7 +179,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-secondary" (click)="cancel()" i18n [disabled]="networkActive">Cancel</button>
|
||||
<button type="submit" class="btn btn-primary" i18n [disabled]="networkActive || saveDisabled">Save</button>
|
||||
<button type="button" class="btn btn-outline-secondary" (click)="cancel()" i18n [disabled]="networkActive()">Cancel</button>
|
||||
<button type="submit" class="btn btn-primary" i18n [disabled]="networkActive() || saveDisabled">Save</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
+18
-17
@@ -1,9 +1,4 @@
|
||||
import {
|
||||
ComponentFixture,
|
||||
TestBed,
|
||||
fakeAsync,
|
||||
tick,
|
||||
} from '@angular/core/testing'
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||
|
||||
import { Clipboard } from '@angular/cdk/clipboard'
|
||||
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
|
||||
@@ -191,7 +186,8 @@ describe('ProfileEditDialogComponent', () => {
|
||||
expect(component.saveDisabled).toBeFalsy()
|
||||
})
|
||||
|
||||
it('should logout on save if password changed', fakeAsync(() => {
|
||||
it('should logout on save if password changed', () => {
|
||||
jest.useFakeTimers()
|
||||
const getSpy = jest.spyOn(profileService, 'get')
|
||||
getSpy.mockReturnValue(of(profile))
|
||||
const getProvidersSpy = jest.spyOn(
|
||||
@@ -211,13 +207,15 @@ describe('ProfileEditDialogComponent', () => {
|
||||
.mockImplementation(() => {})
|
||||
component.save()
|
||||
expect(updateSpy).toHaveBeenCalled()
|
||||
tick(2600)
|
||||
jest.advanceTimersByTime(2600)
|
||||
expect(navSpy).toHaveBeenCalledWith(
|
||||
`${window.location.origin}/accounts/logout/?next=/accounts/login/?next=/`
|
||||
)
|
||||
}))
|
||||
jest.useRealTimers()
|
||||
})
|
||||
|
||||
it('should support auth token copy', fakeAsync(() => {
|
||||
it('should support auth token copy', () => {
|
||||
jest.useFakeTimers()
|
||||
const getSpy = jest.spyOn(profileService, 'get')
|
||||
getSpy.mockReturnValue(of(profile))
|
||||
const getProvidersSpy = jest.spyOn(
|
||||
@@ -229,10 +227,11 @@ describe('ProfileEditDialogComponent', () => {
|
||||
const copySpy = jest.spyOn(clipboard, 'copy')
|
||||
component.copyAuthToken()
|
||||
expect(copySpy).toHaveBeenCalledWith(profile.auth_token)
|
||||
expect(component.copied).toBeTruthy()
|
||||
tick(3000)
|
||||
expect(component.copied).toBeFalsy()
|
||||
}))
|
||||
expect(component.copied()).toBeTruthy()
|
||||
jest.advanceTimersByTime(3000)
|
||||
expect(component.copied()).toBeFalsy()
|
||||
jest.useRealTimers()
|
||||
})
|
||||
|
||||
it('should support generate token, display error if needed', () => {
|
||||
const getSpy = jest.spyOn(profileService, 'get')
|
||||
@@ -366,11 +365,13 @@ describe('ProfileEditDialogComponent', () => {
|
||||
expect(component.isTotpEnabled).toBeFalsy()
|
||||
})
|
||||
|
||||
it('should copy recovery codes', fakeAsync(() => {
|
||||
it('should copy recovery codes', () => {
|
||||
jest.useFakeTimers()
|
||||
const copySpy = jest.spyOn(clipboard, 'copy')
|
||||
component.recoveryCodes = ['1', '2', '3']
|
||||
component.copyRecoveryCodes()
|
||||
expect(copySpy).toHaveBeenCalledWith('1\n2\n3')
|
||||
tick(3000)
|
||||
}))
|
||||
jest.advanceTimersByTime(3000)
|
||||
jest.useRealTimers()
|
||||
})
|
||||
})
|
||||
|
||||
+43
-32
@@ -1,5 +1,5 @@
|
||||
import { Clipboard } from '@angular/cdk/clipboard'
|
||||
import { Component, OnInit, inject } from '@angular/core'
|
||||
import { Component, OnInit, inject, signal } from '@angular/core'
|
||||
import {
|
||||
FormControl,
|
||||
FormGroup,
|
||||
@@ -50,8 +50,12 @@ export class ProfileEditDialogComponent
|
||||
private toastService = inject(ToastService)
|
||||
private clipboard = inject(Clipboard)
|
||||
|
||||
public networkActive: boolean = false
|
||||
public error: any
|
||||
readonly networkActive = signal(false)
|
||||
readonly error = signal<any>(undefined)
|
||||
readonly showPasswordConfirm = signal(false)
|
||||
readonly showEmailConfirm = signal(false)
|
||||
readonly copied = signal(false)
|
||||
readonly codesCopied = signal(false)
|
||||
|
||||
public form = new FormGroup({
|
||||
email: new FormControl(''),
|
||||
@@ -67,23 +71,18 @@ export class ProfileEditDialogComponent
|
||||
private currentPassword: string
|
||||
private newPassword: string
|
||||
private passwordConfirm: string
|
||||
public showPasswordConfirm: boolean = false
|
||||
|
||||
public hasUsablePassword: boolean = false
|
||||
|
||||
private currentEmail: string
|
||||
private newEmail: string
|
||||
private emailConfirm: string
|
||||
public showEmailConfirm: boolean = false
|
||||
|
||||
public isTotpEnabled: boolean = false
|
||||
public totpSettings: TotpSettings
|
||||
public totpSettingsLoading: boolean = false
|
||||
public totpLoading: boolean = false
|
||||
public recoveryCodes: string[]
|
||||
|
||||
public copied: boolean = false
|
||||
public codesCopied: boolean = false
|
||||
|
||||
public socialAccounts: SocialAccount[] = []
|
||||
public socialAccountProviders: SocialAccountProvider[] = []
|
||||
|
||||
@@ -95,12 +94,12 @@ export class ProfileEditDialogComponent
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.networkActive = true
|
||||
this.networkActive.set(true)
|
||||
this.profileService
|
||||
.get()
|
||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||
.subscribe((profile) => {
|
||||
this.networkActive = false
|
||||
this.networkActive.set(false)
|
||||
this.form.patchValue(profile)
|
||||
this.currentEmail = profile.email
|
||||
this.form.get('email').valueChanges.subscribe((newEmail) => {
|
||||
@@ -126,7 +125,7 @@ export class ProfileEditDialogComponent
|
||||
}
|
||||
|
||||
get saveDisabled(): boolean {
|
||||
return this.error?.password_confirm || this.error?.email_confirm
|
||||
return this.error()?.password_confirm || this.error()?.email_confirm
|
||||
}
|
||||
|
||||
onEmailKeyUp(event: KeyboardEvent): void {
|
||||
@@ -140,18 +139,17 @@ export class ProfileEditDialogComponent
|
||||
}
|
||||
|
||||
onEmailChange(): void {
|
||||
this.showEmailConfirm = this.currentEmail !== this.newEmail
|
||||
if (this.showEmailConfirm) {
|
||||
this.showEmailConfirm.set(this.currentEmail !== this.newEmail)
|
||||
if (this.showEmailConfirm()) {
|
||||
this.form.get('email_confirm').enable()
|
||||
if (this.newEmail !== this.emailConfirm) {
|
||||
if (!this.error) this.error = {}
|
||||
this.error.email_confirm = $localize`Emails must match`
|
||||
this.setFieldError('email_confirm', $localize`Emails must match`)
|
||||
} else {
|
||||
delete this.error?.email_confirm
|
||||
this.clearFieldError('email_confirm')
|
||||
}
|
||||
} else {
|
||||
this.form.get('email_confirm').disable()
|
||||
delete this.error?.email_confirm
|
||||
this.clearFieldError('email_confirm')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,29 +165,42 @@ export class ProfileEditDialogComponent
|
||||
}
|
||||
|
||||
onPasswordChange(): void {
|
||||
this.showPasswordConfirm = this.currentPassword !== this.newPassword
|
||||
this.showPasswordConfirm.set(this.currentPassword !== this.newPassword)
|
||||
|
||||
if (this.showPasswordConfirm) {
|
||||
if (this.showPasswordConfirm()) {
|
||||
this.form.get('password_confirm').enable()
|
||||
if (this.newPassword !== this.passwordConfirm) {
|
||||
if (!this.error) this.error = {}
|
||||
this.error.password_confirm = $localize`Passwords must match`
|
||||
this.setFieldError('password_confirm', $localize`Passwords must match`)
|
||||
} else {
|
||||
delete this.error?.password_confirm
|
||||
this.clearFieldError('password_confirm')
|
||||
}
|
||||
} else {
|
||||
this.form.get('password_confirm').disable()
|
||||
delete this.error?.password_confirm
|
||||
this.clearFieldError('password_confirm')
|
||||
}
|
||||
}
|
||||
|
||||
private setFieldError(fieldName: string, message: string): void {
|
||||
this.error.set({
|
||||
...this.error(),
|
||||
[fieldName]: message,
|
||||
})
|
||||
}
|
||||
|
||||
private clearFieldError(fieldName: string): void {
|
||||
if (!this.error()) return
|
||||
const error = { ...this.error() }
|
||||
delete error[fieldName]
|
||||
this.error.set(Object.keys(error).length ? error : undefined)
|
||||
}
|
||||
|
||||
save(): void {
|
||||
const passwordChanged =
|
||||
this.newPassword && this.currentPassword !== this.newPassword
|
||||
const profile = Object.assign({}, this.form.value)
|
||||
delete profile.totp_code
|
||||
this.error = null
|
||||
this.networkActive = true
|
||||
this.error.set(null)
|
||||
this.networkActive.set(true)
|
||||
this.profileService
|
||||
.update(profile)
|
||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||
@@ -210,8 +221,8 @@ export class ProfileEditDialogComponent
|
||||
},
|
||||
error: (error) => {
|
||||
this.toastService.showError($localize`Error saving profile`, error)
|
||||
this.error = error?.error
|
||||
this.networkActive = false
|
||||
this.error.set(error?.error)
|
||||
this.networkActive.set(false)
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -236,9 +247,9 @@ export class ProfileEditDialogComponent
|
||||
|
||||
copyAuthToken(): void {
|
||||
this.clipboard.copy(this.form.get('auth_token').value)
|
||||
this.copied = true
|
||||
this.copied.set(true)
|
||||
setTimeout(() => {
|
||||
this.copied = false
|
||||
this.copied.set(false)
|
||||
}, 3000)
|
||||
}
|
||||
|
||||
@@ -330,9 +341,9 @@ export class ProfileEditDialogComponent
|
||||
|
||||
public copyRecoveryCodes(): void {
|
||||
this.clipboard.copy(this.recoveryCodes.join('\n'))
|
||||
this.codesCopied = true
|
||||
this.codesCopied.set(true)
|
||||
setTimeout(() => {
|
||||
this.codesCopied = false
|
||||
this.codesCopied.set(false)
|
||||
}, 3000)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user