From 5d4ef6f9589d419dda638a3e028eef2fbff8cee9 Mon Sep 17 00:00:00 2001
From: shamoon <4887959+shamoon@users.noreply.github.com>
Date: Tue, 21 Nov 2023 14:56:32 -0800
Subject: [PATCH] Email & Password confirmation fields
---
.../input/password/password.component.html | 2 +-
.../profile-edit-dialog.component.html | 24 +++-
.../profile-edit-dialog.component.scss | 5 +
.../profile-edit-dialog.component.spec.ts | 65 +++++++++
.../profile-edit-dialog.component.ts | 125 ++++++++++++++++--
5 files changed, 203 insertions(+), 18 deletions(-)
diff --git a/src-ui/src/app/components/common/input/password/password.component.html b/src-ui/src/app/components/common/input/password/password.component.html
index 57cdd6de8..4822be877 100644
--- a/src-ui/src/app/components/common/input/password/password.component.html
+++ b/src-ui/src/app/components/common/input/password/password.component.html
@@ -1,6 +1,6 @@
-
+
{{error}}
diff --git a/src-ui/src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html b/src-ui/src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html
index 18a04e376..67332e236 100644
--- a/src-ui/src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html
+++ b/src-ui/src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html
@@ -5,13 +5,31 @@
diff --git a/src-ui/src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.scss b/src-ui/src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.scss
index e69de29bb..bda986f82 100644
--- a/src-ui/src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.scss
+++ b/src-ui/src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.scss
@@ -0,0 +1,5 @@
+::ng-deep {
+ .accordion-body .mb-3 {
+ margin: 0 !important; // hack-ish, for animation
+ }
+}
diff --git a/src-ui/src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.spec.ts b/src-ui/src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.spec.ts
index 63b17dc09..92cf0dcb1 100644
--- a/src-ui/src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.spec.ts
+++ b/src-ui/src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.spec.ts
@@ -4,6 +4,7 @@ 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,
@@ -14,6 +15,7 @@ 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 { By } from '@angular/platform-browser'
const profile = {
email: 'foo@bar.com',
@@ -41,6 +43,7 @@ describe('ProfileEditDialogComponent', () => {
ReactiveFormsModule,
FormsModule,
NgbModalModule,
+ NgbAccordionModule,
],
})
profileService = TestBed.inject(ProfileService)
@@ -86,4 +89,66 @@ describe('ProfileEditDialogComponent', () => {
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' } } as any)
+ 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()
+ })
})
diff --git a/src-ui/src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.ts b/src-ui/src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.ts
index 9422d35d2..f75938b3f 100644
--- a/src-ui/src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.ts
+++ b/src-ui/src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.ts
@@ -1,25 +1,39 @@
-import { Component, OnInit } from '@angular/core'
+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'
@Component({
selector: 'pngx-profile-edit-dialog',
templateUrl: './profile-edit-dialog.component.html',
styleUrls: ['./profile-edit-dialog.component.scss'],
})
-export class ProfileEditDialogComponent implements OnInit {
+export class ProfileEditDialogComponent implements OnInit, OnDestroy {
public networkActive: boolean = false
public error: any
+ private unsubscribeNotifier: Subject
= 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(''),
})
+ 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
+
constructor(
private profileService: ProfileService,
public activeModal: NgbActiveModal,
@@ -27,22 +41,105 @@ export class ProfileEditDialogComponent implements OnInit {
) {}
ngOnInit(): void {
- this.profileService.get().subscribe((profile) => {
- this.form.patchValue(profile)
- })
+ 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) {
+ this.newEmail = (event.target as HTMLInputElement)?.value
+ this.onEmailChange()
+ }
+
+ onEmailConfirmKeyUp(event: KeyboardEvent) {
+ this.emailConfirm = (event.target as HTMLInputElement)?.value
+ this.onEmailChange()
+ }
+
+ onEmailChange() {
+ 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) {
+ this.newPassword = (event.target as HTMLInputElement)?.value
+ this.onPasswordChange()
+ }
+
+ onPasswordConfirmKeyUp(event: KeyboardEvent) {
+ this.passwordConfirm = (event.target as HTMLInputElement)?.value
+ this.onPasswordChange()
+ }
+
+ onPasswordChange() {
+ this.showPasswordConfirm = this.currentPassword !== this.newPassword
+ console.log(this.currentPassword, this.newPassword, this.passwordConfirm)
+
+ 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() {
const profile = Object.assign({}, this.form.value)
- this.profileService.update(profile).subscribe({
- next: () => {
- this.toastService.showInfo($localize`Profile updated successfully`)
- this.activeModal.close()
- },
- error: (error) => {
- this.toastService.showError($localize`Error saving profile`, error)
- },
- })
+ this.networkActive = true
+ this.profileService
+ .update(profile)
+ .pipe(takeUntil(this.unsubscribeNotifier))
+ .subscribe({
+ next: () => {
+ this.toastService.showInfo($localize`Profile updated successfully`)
+ this.activeModal.close()
+ },
+ error: (error) => {
+ this.toastService.showError($localize`Error saving profile`, error)
+ this.networkActive = false
+ },
+ })
}
cancel() {