mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-07-28 18:24:38 -05:00
Feature: update user profile (#4678)
This commit is contained in:
@@ -1,8 +1,15 @@
|
||||
<div class="mb-3">
|
||||
<label class="form-label" [for]="inputId">{{title}}</label>
|
||||
<input #inputField type="password" class="form-control" [class.is-invalid]="error" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)">
|
||||
<small *ngIf="hint" class="form-text text-muted" [innerHTML]="hint | safeHtml"></small>
|
||||
<div class="input-group" [class.is-invalid]="error">
|
||||
<input #inputField [type]="showReveal && textVisible ? 'text' : 'password'" class="form-control" [class.is-invalid]="error" [id]="inputId" [(ngModel)]="value" (focus)="onFocus()" (focusout)="onFocusOut()" (change)="onChange(value)" [disabled]="disabled" [autocomplete]="autocomplete">
|
||||
<button *ngIf="showReveal" type="button" class="btn btn-outline-secondary" (click)="toggleVisibility()" i18n-title title="Show password" [disabled]="disabled || disableRevealToggle">
|
||||
<svg width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#eye" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="invalid-feedback">
|
||||
{{error}}
|
||||
</div>
|
||||
<small *ngIf="hint" class="form-text text-muted" [innerHTML]="hint | safeHtml"></small>
|
||||
</div>
|
||||
|
@@ -5,6 +5,7 @@ import {
|
||||
NG_VALUE_ACCESSOR,
|
||||
} from '@angular/forms'
|
||||
import { PasswordComponent } from './password.component'
|
||||
import { By } from '@angular/platform-browser'
|
||||
|
||||
describe('PasswordComponent', () => {
|
||||
let component: PasswordComponent
|
||||
@@ -33,4 +34,26 @@ describe('PasswordComponent', () => {
|
||||
// fixture.detectChanges()
|
||||
// expect(component.value).toEqual('foo')
|
||||
})
|
||||
|
||||
it('should support toggling field visibility', () => {
|
||||
expect(input.type).toEqual('password')
|
||||
component.showReveal = true
|
||||
fixture.detectChanges()
|
||||
fixture.debugElement.query(By.css('button')).triggerEventHandler('click')
|
||||
fixture.detectChanges()
|
||||
expect(input.type).toEqual('text')
|
||||
})
|
||||
|
||||
it('should empty field if password is obfuscated on focus', () => {
|
||||
component.value = '*********'
|
||||
component.onFocus()
|
||||
expect(component.value).toEqual('')
|
||||
component.onFocusOut()
|
||||
expect(component.value).toEqual('**********')
|
||||
})
|
||||
|
||||
it('should disable toggle button if no real password', () => {
|
||||
component.value = '*********'
|
||||
expect(component.disableRevealToggle).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { Component, forwardRef } from '@angular/core'
|
||||
import { Component, Input, forwardRef } from '@angular/core'
|
||||
import { NG_VALUE_ACCESSOR } from '@angular/forms'
|
||||
import { AbstractInputComponent } from '../abstract-input'
|
||||
|
||||
@@ -15,7 +15,32 @@ import { AbstractInputComponent } from '../abstract-input'
|
||||
styleUrls: ['./password.component.scss'],
|
||||
})
|
||||
export class PasswordComponent extends AbstractInputComponent<string> {
|
||||
constructor() {
|
||||
super()
|
||||
@Input()
|
||||
showReveal: boolean = false
|
||||
|
||||
@Input()
|
||||
autocomplete: string
|
||||
|
||||
public textVisible: boolean = false
|
||||
|
||||
public toggleVisibility(): void {
|
||||
this.textVisible = !this.textVisible
|
||||
}
|
||||
|
||||
public onFocus() {
|
||||
if (this.value?.replace(/\*/g, '').length === 0) {
|
||||
this.writeValue('')
|
||||
}
|
||||
}
|
||||
|
||||
public onFocusOut() {
|
||||
if (this.value?.length === 0) {
|
||||
this.writeValue('**********')
|
||||
this.onChange(this.value)
|
||||
}
|
||||
}
|
||||
|
||||
get disableRevealToggle(): boolean {
|
||||
return this.value?.replace(/\*/g, '').length === 0
|
||||
}
|
||||
}
|
||||
|
@@ -9,7 +9,7 @@
|
||||
</button>
|
||||
</div>
|
||||
<div class="position-relative" [class.col-md-9]="horizontal">
|
||||
<input #inputField type="text" class="form-control" [class.is-invalid]="error" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)" [disabled]="disabled">
|
||||
<input #inputField type="text" class="form-control" [class.is-invalid]="error" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)" [disabled]="disabled" [autocomplete]="autocomplete">
|
||||
<small *ngIf="hint" class="form-text text-muted" [innerHTML]="hint | safeHtml"></small>
|
||||
<div class="invalid-feedback position-absolute top-100">
|
||||
{{error}}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { Component, forwardRef } from '@angular/core'
|
||||
import { Component, Input, forwardRef } from '@angular/core'
|
||||
import { NG_VALUE_ACCESSOR } from '@angular/forms'
|
||||
import { AbstractInputComponent } from '../abstract-input'
|
||||
|
||||
@@ -15,6 +15,9 @@ import { AbstractInputComponent } from '../abstract-input'
|
||||
styleUrls: ['./text.component.scss'],
|
||||
})
|
||||
export class TextComponent extends AbstractInputComponent<string> {
|
||||
@Input()
|
||||
autocomplete: string
|
||||
|
||||
constructor() {
|
||||
super()
|
||||
}
|
||||
|
@@ -0,0 +1,56 @@
|
||||
<form [formGroup]="form" (ngSubmit)="save()" autocomplete="off">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title" id="modal-basic-title" i18n>Edit Profile</h4>
|
||||
<button type="button" class="btn-close" aria-label="Close" (click)="cancel()">
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<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 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>
|
||||
</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>
|
||||
<div ngbAccordion>
|
||||
<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>
|
||||
</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>
|
||||
<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">
|
||||
<svg class="buttonicon-sm" fill="currentColor">
|
||||
<use *ngIf="!copied" xlink:href="assets/bootstrap-icons.svg#clipboard-fill" />
|
||||
<use *ngIf="copied" xlink:href="assets/bootstrap-icons.svg#clipboard-check-fill" />
|
||||
</svg><span class="visually-hidden" i18n>Copy</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary" (click)="generateAuthToken()" i18n-title title="Regenerate auth token">
|
||||
<svg class="buttonicon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#arrow-repeat" />
|
||||
</svg>
|
||||
</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>
|
||||
</div>
|
||||
<div class="form-text text-muted text-end fst-italic" i18n>Warning: changing the token cannot be undone</div>
|
||||
</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>
|
||||
</div>
|
||||
</form>
|
@@ -0,0 +1,9 @@
|
||||
::ng-deep {
|
||||
.accordion-body .mb-3 {
|
||||
margin: 0 !important; // hack-ish, for animation
|
||||
}
|
||||
}
|
||||
|
||||
.copied-badge {
|
||||
right: 8em;
|
||||
}
|
@@ -0,0 +1,222 @@
|
||||
import {
|
||||
ComponentFixture,
|
||||
TestBed,
|
||||
fakeAsync,
|
||||
tick,
|
||||
} from '@angular/core/testing'
|
||||
|
||||
import { ProfileEditDialogComponent } from './profile-edit-dialog.component'
|
||||
import { ProfileService } from 'src/app/services/profile.service'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import {
|
||||
NgbAccordionModule,
|
||||
NgbActiveModal,
|
||||
NgbModal,
|
||||
NgbModalModule,
|
||||
} from '@ng-bootstrap/ng-bootstrap'
|
||||
import { HttpClientModule } from '@angular/common/http'
|
||||
import { TextComponent } from '../input/text/text.component'
|
||||
import { PasswordComponent } from '../input/password/password.component'
|
||||
import { of, throwError } from 'rxjs'
|
||||
import { ToastService } from 'src/app/services/toast.service'
|
||||
import { Clipboard } from '@angular/cdk/clipboard'
|
||||
|
||||
const profile = {
|
||||
email: 'foo@bar.com',
|
||||
password: '*********',
|
||||
first_name: 'foo',
|
||||
last_name: 'bar',
|
||||
auth_token: '123456789abcdef',
|
||||
}
|
||||
|
||||
describe('ProfileEditDialogComponent', () => {
|
||||
let component: ProfileEditDialogComponent
|
||||
let fixture: ComponentFixture<ProfileEditDialogComponent>
|
||||
let profileService: ProfileService
|
||||
let toastService: ToastService
|
||||
let clipboard: Clipboard
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [
|
||||
ProfileEditDialogComponent,
|
||||
TextComponent,
|
||||
PasswordComponent,
|
||||
],
|
||||
providers: [NgbActiveModal],
|
||||
imports: [
|
||||
HttpClientModule,
|
||||
ReactiveFormsModule,
|
||||
FormsModule,
|
||||
NgbModalModule,
|
||||
NgbAccordionModule,
|
||||
],
|
||||
})
|
||||
profileService = TestBed.inject(ProfileService)
|
||||
toastService = TestBed.inject(ToastService)
|
||||
clipboard = TestBed.inject(Clipboard)
|
||||
fixture = TestBed.createComponent(ProfileEditDialogComponent)
|
||||
component = fixture.componentInstance
|
||||
fixture.detectChanges()
|
||||
})
|
||||
|
||||
it('should get profile on init, display in form', () => {
|
||||
const getSpy = jest.spyOn(profileService, 'get')
|
||||
getSpy.mockReturnValue(of(profile))
|
||||
component.ngOnInit()
|
||||
expect(getSpy).toHaveBeenCalled()
|
||||
fixture.detectChanges()
|
||||
expect(component.form.get('email').value).toEqual(profile.email)
|
||||
})
|
||||
|
||||
it('should update profile on save, display error if needed', () => {
|
||||
const newProfile = {
|
||||
email: 'foo@bar2.com',
|
||||
password: profile.password,
|
||||
first_name: 'foo2',
|
||||
last_name: profile.last_name,
|
||||
auth_token: profile.auth_token,
|
||||
}
|
||||
const updateSpy = jest.spyOn(profileService, 'update')
|
||||
const errorSpy = jest.spyOn(toastService, 'showError')
|
||||
updateSpy.mockReturnValueOnce(throwError(() => new Error('failed to save')))
|
||||
component.save()
|
||||
expect(errorSpy).toHaveBeenCalled()
|
||||
|
||||
updateSpy.mockClear()
|
||||
const infoSpy = jest.spyOn(toastService, 'showInfo')
|
||||
component.form.patchValue(newProfile)
|
||||
updateSpy.mockReturnValueOnce(of(newProfile))
|
||||
component.save()
|
||||
expect(updateSpy).toHaveBeenCalledWith(newProfile)
|
||||
expect(infoSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should close on cancel', () => {
|
||||
const closeSpy = jest.spyOn(component.activeModal, 'close')
|
||||
component.cancel()
|
||||
expect(closeSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should show additional confirmation field when email changes, warn with error & disable save', () => {
|
||||
expect(component.form.get('email_confirm').enabled).toBeFalsy()
|
||||
const getSpy = jest.spyOn(profileService, 'get')
|
||||
getSpy.mockReturnValue(of(profile))
|
||||
component.ngOnInit()
|
||||
component.form.get('email').patchValue('foo@bar2.com')
|
||||
component.onEmailKeyUp({ target: { value: 'foo@bar2.com' } } as any)
|
||||
fixture.detectChanges()
|
||||
expect(component.form.get('email_confirm').enabled).toBeTruthy()
|
||||
expect(fixture.debugElement.nativeElement.textContent).toContain(
|
||||
'Emails must match'
|
||||
)
|
||||
expect(component.saveDisabled).toBeTruthy()
|
||||
|
||||
component.form.get('email_confirm').patchValue('foo@bar2.com')
|
||||
component.onEmailConfirmKeyUp({ target: { value: 'foo@bar2.com' } } as any)
|
||||
fixture.detectChanges()
|
||||
expect(fixture.debugElement.nativeElement.textContent).not.toContain(
|
||||
'Emails must match'
|
||||
)
|
||||
expect(component.saveDisabled).toBeFalsy()
|
||||
|
||||
component.form.get('email').patchValue(profile.email)
|
||||
fixture.detectChanges()
|
||||
expect(component.form.get('email_confirm').enabled).toBeFalsy()
|
||||
expect(fixture.debugElement.nativeElement.textContent).not.toContain(
|
||||
'Emails must match'
|
||||
)
|
||||
expect(component.saveDisabled).toBeFalsy()
|
||||
})
|
||||
|
||||
it('should show additional confirmation field when password changes, warn with error & disable save', () => {
|
||||
expect(component.form.get('password_confirm').enabled).toBeFalsy()
|
||||
const getSpy = jest.spyOn(profileService, 'get')
|
||||
getSpy.mockReturnValue(of(profile))
|
||||
component.ngOnInit()
|
||||
component.form.get('password').patchValue('new*pass')
|
||||
component.onPasswordKeyUp({
|
||||
target: { value: 'new*pass', tagName: 'input' },
|
||||
} as any)
|
||||
component.onPasswordKeyUp({ target: { tagName: 'button' } } as any) // coverage
|
||||
fixture.detectChanges()
|
||||
expect(component.form.get('password_confirm').enabled).toBeTruthy()
|
||||
expect(fixture.debugElement.nativeElement.textContent).toContain(
|
||||
'Passwords must match'
|
||||
)
|
||||
expect(component.saveDisabled).toBeTruthy()
|
||||
|
||||
component.form.get('password_confirm').patchValue('new*pass')
|
||||
component.onPasswordConfirmKeyUp({ target: { value: 'new*pass' } } as any)
|
||||
fixture.detectChanges()
|
||||
expect(fixture.debugElement.nativeElement.textContent).not.toContain(
|
||||
'Passwords must match'
|
||||
)
|
||||
expect(component.saveDisabled).toBeFalsy()
|
||||
|
||||
component.form.get('password').patchValue(profile.password)
|
||||
fixture.detectChanges()
|
||||
expect(component.form.get('password_confirm').enabled).toBeFalsy()
|
||||
expect(fixture.debugElement.nativeElement.textContent).not.toContain(
|
||||
'Passwords must match'
|
||||
)
|
||||
expect(component.saveDisabled).toBeFalsy()
|
||||
})
|
||||
|
||||
it('should logout on save if password changed', fakeAsync(() => {
|
||||
const getSpy = jest.spyOn(profileService, 'get')
|
||||
getSpy.mockReturnValue(of(profile))
|
||||
component.ngOnInit()
|
||||
component['newPassword'] = 'new*pass'
|
||||
component.form.get('password').patchValue('new*pass')
|
||||
component.form.get('password_confirm').patchValue('new*pass')
|
||||
|
||||
const updateSpy = jest.spyOn(profileService, 'update')
|
||||
updateSpy.mockReturnValue(of(null))
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: {
|
||||
href: 'http://localhost/',
|
||||
},
|
||||
writable: true, // possibility to override
|
||||
})
|
||||
component.save()
|
||||
expect(updateSpy).toHaveBeenCalled()
|
||||
tick(2600)
|
||||
expect(window.location.href).toContain('logout')
|
||||
}))
|
||||
|
||||
it('should support auth token copy', fakeAsync(() => {
|
||||
const getSpy = jest.spyOn(profileService, 'get')
|
||||
getSpy.mockReturnValue(of(profile))
|
||||
component.ngOnInit()
|
||||
const copySpy = jest.spyOn(clipboard, 'copy')
|
||||
component.copyAuthToken()
|
||||
expect(copySpy).toHaveBeenCalledWith(profile.auth_token)
|
||||
expect(component.copied).toBeTruthy()
|
||||
tick(3000)
|
||||
expect(component.copied).toBeFalsy()
|
||||
}))
|
||||
|
||||
it('should support generate token, display error if needed', () => {
|
||||
const getSpy = jest.spyOn(profileService, 'get')
|
||||
getSpy.mockReturnValue(of(profile))
|
||||
|
||||
const generateSpy = jest.spyOn(profileService, 'generateAuthToken')
|
||||
const errorSpy = jest.spyOn(toastService, 'showError')
|
||||
generateSpy.mockReturnValueOnce(
|
||||
throwError(() => new Error('failed to generate'))
|
||||
)
|
||||
component.generateAuthToken()
|
||||
expect(errorSpy).toHaveBeenCalled()
|
||||
|
||||
generateSpy.mockClear()
|
||||
const newToken = '789101112hijk'
|
||||
generateSpy.mockReturnValueOnce(of(newToken))
|
||||
component.generateAuthToken()
|
||||
expect(generateSpy).toHaveBeenCalled()
|
||||
expect(component.form.get('auth_token').value).not.toEqual(
|
||||
profile.auth_token
|
||||
)
|
||||
expect(component.form.get('auth_token').value).toEqual(newToken)
|
||||
})
|
||||
})
|
@@ -0,0 +1,184 @@
|
||||
import { Component, OnDestroy, OnInit } from '@angular/core'
|
||||
import { FormControl, FormGroup } from '@angular/forms'
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { ProfileService } from 'src/app/services/profile.service'
|
||||
import { ToastService } from 'src/app/services/toast.service'
|
||||
import { Subject, takeUntil } from 'rxjs'
|
||||
import { Clipboard } from '@angular/cdk/clipboard'
|
||||
|
||||
@Component({
|
||||
selector: 'pngx-profile-edit-dialog',
|
||||
templateUrl: './profile-edit-dialog.component.html',
|
||||
styleUrls: ['./profile-edit-dialog.component.scss'],
|
||||
})
|
||||
export class ProfileEditDialogComponent implements OnInit, OnDestroy {
|
||||
public networkActive: boolean = false
|
||||
public error: any
|
||||
private unsubscribeNotifier: Subject<any> = new Subject()
|
||||
|
||||
public form = new FormGroup({
|
||||
email: new FormControl(''),
|
||||
email_confirm: new FormControl({ value: null, disabled: true }),
|
||||
password: new FormControl(null),
|
||||
password_confirm: new FormControl({ value: null, disabled: true }),
|
||||
first_name: new FormControl(''),
|
||||
last_name: new FormControl(''),
|
||||
auth_token: new FormControl(''),
|
||||
})
|
||||
|
||||
private currentPassword: string
|
||||
private newPassword: string
|
||||
private passwordConfirm: string
|
||||
public showPasswordConfirm: boolean = false
|
||||
|
||||
private currentEmail: string
|
||||
private newEmail: string
|
||||
private emailConfirm: string
|
||||
public showEmailConfirm: boolean = false
|
||||
|
||||
public copied: boolean = false
|
||||
|
||||
constructor(
|
||||
private profileService: ProfileService,
|
||||
public activeModal: NgbActiveModal,
|
||||
private toastService: ToastService,
|
||||
private clipboard: Clipboard
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.networkActive = true
|
||||
this.profileService
|
||||
.get()
|
||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||
.subscribe((profile) => {
|
||||
this.networkActive = false
|
||||
this.form.patchValue(profile)
|
||||
this.currentEmail = profile.email
|
||||
this.form.get('email').valueChanges.subscribe((newEmail) => {
|
||||
this.newEmail = newEmail
|
||||
this.onEmailChange()
|
||||
})
|
||||
this.currentPassword = profile.password
|
||||
this.form.get('password').valueChanges.subscribe((newPassword) => {
|
||||
this.newPassword = newPassword
|
||||
this.onPasswordChange()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.unsubscribeNotifier.next(true)
|
||||
this.unsubscribeNotifier.complete()
|
||||
}
|
||||
|
||||
get saveDisabled(): boolean {
|
||||
return this.error?.password_confirm || this.error?.email_confirm
|
||||
}
|
||||
|
||||
onEmailKeyUp(event: KeyboardEvent): void {
|
||||
this.newEmail = (event.target as HTMLInputElement)?.value
|
||||
this.onEmailChange()
|
||||
}
|
||||
|
||||
onEmailConfirmKeyUp(event: KeyboardEvent): void {
|
||||
this.emailConfirm = (event.target as HTMLInputElement)?.value
|
||||
this.onEmailChange()
|
||||
}
|
||||
|
||||
onEmailChange(): void {
|
||||
this.showEmailConfirm = 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`
|
||||
} else {
|
||||
delete this.error?.email_confirm
|
||||
}
|
||||
} else {
|
||||
this.form.get('email_confirm').disable()
|
||||
delete this.error?.email_confirm
|
||||
}
|
||||
}
|
||||
|
||||
onPasswordKeyUp(event: KeyboardEvent): void {
|
||||
if ((event.target as HTMLElement).tagName !== 'input') return // toggle button can trigger this handler
|
||||
this.newPassword = (event.target as HTMLInputElement)?.value
|
||||
this.onPasswordChange()
|
||||
}
|
||||
|
||||
onPasswordConfirmKeyUp(event: KeyboardEvent): void {
|
||||
this.passwordConfirm = (event.target as HTMLInputElement)?.value
|
||||
this.onPasswordChange()
|
||||
}
|
||||
|
||||
onPasswordChange(): void {
|
||||
this.showPasswordConfirm = this.currentPassword !== this.newPassword
|
||||
|
||||
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`
|
||||
} else {
|
||||
delete this.error?.password_confirm
|
||||
}
|
||||
} else {
|
||||
this.form.get('password_confirm').disable()
|
||||
delete this.error?.password_confirm
|
||||
}
|
||||
}
|
||||
|
||||
save(): void {
|
||||
const passwordChanged = this.currentPassword !== this.newPassword
|
||||
const profile = Object.assign({}, this.form.value)
|
||||
this.networkActive = true
|
||||
this.profileService
|
||||
.update(profile)
|
||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||
.subscribe({
|
||||
next: () => {
|
||||
this.toastService.showInfo($localize`Profile updated successfully`)
|
||||
if (passwordChanged) {
|
||||
this.toastService.showInfo(
|
||||
$localize`Password has been changed, you will be logged out momentarily.`
|
||||
)
|
||||
setTimeout(() => {
|
||||
window.location.href = `${window.location.origin}/accounts/logout/?next=/accounts/login/?next=/`
|
||||
}, 2500)
|
||||
}
|
||||
this.activeModal.close()
|
||||
},
|
||||
error: (error) => {
|
||||
this.toastService.showError($localize`Error saving profile`, error)
|
||||
this.networkActive = false
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
cancel(): void {
|
||||
this.activeModal.close()
|
||||
}
|
||||
|
||||
generateAuthToken(): void {
|
||||
this.profileService.generateAuthToken().subscribe({
|
||||
next: (token: string) => {
|
||||
this.form.patchValue({ auth_token: token })
|
||||
},
|
||||
error: (error) => {
|
||||
this.toastService.showError(
|
||||
$localize`Error generating auth token`,
|
||||
error
|
||||
)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
copyAuthToken(): void {
|
||||
this.clipboard.copy(this.form.get('auth_token').value)
|
||||
this.copied = true
|
||||
setTimeout(() => {
|
||||
this.copied = false
|
||||
}, 3000)
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user