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() {