From 44dc107f5648669cf12779295504c729363df319 Mon Sep 17 00:00:00 2001 From: cv5ch <176032962+cv5ch@users.noreply.github.com> Date: Wed, 30 Jul 2025 13:16:53 +0200 Subject: [PATCH] AccountSettingsPage now showing field and form errors, fixed workflow and added unittests --- .../acc-settings/acc-settings.component.html | 15 +- .../acc-settings.component.spec.ts | 367 +++++++++++++----- .../acc-settings/acc-settings.component.ts | 59 ++- .../core/_validators/password.validator.ts | 22 +- src/app/shared/buttons/button-submit.ts | 24 +- src/app/shared/input/text/text.component.html | 48 ++- src/app/shared/input/text/text.component.ts | 13 +- 7 files changed, 391 insertions(+), 157 deletions(-) diff --git a/src/app/account/settings/acc-settings/acc-settings.component.html b/src/app/account/settings/acc-settings/acc-settings.component.html index 2cd72327f..0233cbfa5 100644 --- a/src/app/account/settings/acc-settings/acc-settings.component.html +++ b/src/app/account/settings/acc-settings/acc-settings.component.html @@ -10,9 +10,9 @@ data-testid="input-registered-since" > - + - +
@@ -38,6 +38,8 @@ [inputType]="showNewPassword ? 'text' : 'password'" data-testid="input-newPassword" [isRequired]="true" + [minLength]="pwdMin" + [maxLength]="pwdMax" [showPasswordToggle]="true" [passwordIsVisible]="showNewPassword" (ShowPasswordEmit)="showNewPassword = $event" @@ -49,6 +51,8 @@ [inputType]="showConfirmNewPassword ? 'text' : 'password'" data-testid="input-confirmNewPassword" [isRequired]="true" + [minLength]="pwdMin" + [maxLength]="pwdMax" [passwordIsVisible]="showConfirmNewPassword" [showPasswordToggle]="true" (ShowPasswordEmit)="showConfirmNewPassword = $event" @@ -56,8 +60,13 @@
+ + + Passwords do not match + + - +
diff --git a/src/app/account/settings/acc-settings/acc-settings.component.spec.ts b/src/app/account/settings/acc-settings/acc-settings.component.spec.ts index 0941e9757..a2942298f 100644 --- a/src/app/account/settings/acc-settings/acc-settings.component.spec.ts +++ b/src/app/account/settings/acc-settings/acc-settings.component.spec.ts @@ -10,12 +10,19 @@ import { PipesModule } from 'src/app/shared/pipes.module'; import { CommonModule } from '@angular/common'; import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; -import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { MatButtonModule } from '@angular/material/button'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatIconModule } from '@angular/material/icon'; +import { MatInputModule } from '@angular/material/input'; +import { MatSelectModule } from '@angular/material/select'; import { MatSnackBarModule } from '@angular/material/snack-bar'; import { By } from '@angular/platform-browser'; import { provideAnimations } from '@angular/platform-browser/animations'; -import { Params, provideRouter } from '@angular/router'; +import { Params, Router } from '@angular/router'; + +import { AlertService } from '@services/shared/alert.service'; import { AccountSettingsComponent } from '@src/app/account/settings/acc-settings/acc-settings.component'; @@ -23,6 +30,9 @@ describe('AccountSettingsComponent', () => { let component: AccountSettingsComponent; let fixture: ComponentFixture; + let routerSpy: jasmine.SpyObj; + let alertSpy: jasmine.SpyObj; + const userResponse = { type: 'user', id: 1, @@ -43,27 +53,34 @@ describe('AccountSettingsComponent', () => { } }; - // Define a partial mock service to simulate service calls. + // Your existing mockService const mockService: Partial = { - // Simulate the 'get' method to return an empty observable. - // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any + // eslint-disable-next-line @typescript-eslint/no-unused-vars get(_serviceConfig, _id: number, _routerParams?: Params): Observable { if (_serviceConfig.URL === SERV.USERS.URL) { - return of({ - data: userResponse - }); + return of({ data: userResponse }); } return of([]); }, - // Simulate the 'create' method to return an empty observable. - // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any + // eslint-disable-next-line @typescript-eslint/no-unused-vars create(serviceConfig, _object: any): Observable { return of({}); }, - + // eslint-disable-next-line @typescript-eslint/no-unused-vars + update(_serviceConfig, id, _object: any): Observable { + return of({}); + }, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + chelper(_serviceConfig, option: string, _payload: any): Observable { + return of({}); + }, userId: 1 }; + beforeEach(() => { + routerSpy = jasmine.createSpyObj('Router', ['navigate']); + alertSpy = jasmine.createSpyObj('AlertService', ['showSuccessMessage', 'showErrorMessage']); + TestBed.configureTestingModule({ declarations: [AccountSettingsComponent], imports: [ @@ -75,7 +92,12 @@ describe('AccountSettingsComponent', () => { ComponentsModule, PipesModule, NgbModule, - MatSnackBarModule + MatSnackBarModule, + MatFormFieldModule, + MatInputModule, + MatSelectModule, + MatIconModule, + MatButtonModule ], providers: [ provideAnimations(), @@ -83,8 +105,15 @@ describe('AccountSettingsComponent', () => { provide: GlobalService, useValue: mockService }, - provideHttpClient(withInterceptorsFromDi()), - provideRouter([]) + { + provide: Router, + useValue: routerSpy + }, + { + provide: AlertService, + useValue: alertSpy + }, + provideHttpClient(withInterceptorsFromDi()) ] }).compileComponents(); @@ -92,108 +121,240 @@ describe('AccountSettingsComponent', () => { component = fixture.componentInstance; fixture.detectChanges(); }); - // --- Test Methods --- + it('creates the component', () => { expect(component).toBeTruthy(); }); - it('initializes the form with default values', () => { - const formValue = component.form.getRawValue(); - expect(formValue.name).toBe(userResponse.attributes.name); - expect(formValue.registeredSince).toBe('08/04/2025 6:25:56'); - expect(formValue.email).toBe(userResponse.attributes.email); + describe('Main form tests', () => { + it('initializes the form with default values', () => { + const formValue = component.form.getRawValue(); + expect(formValue.name).toBe(userResponse.attributes.name); + expect(formValue.registeredSince).toBe('08/04/2025 6:25:56'); + expect(formValue.email).toBe(userResponse.attributes.email); + }); - const passFormValue = component.changepasswordFormGroup.getRawValue(); - expect(passFormValue.oldPassword).toBe(''); - expect(passFormValue.newPassword).toBe(''); - expect(passFormValue.confirmNewPassword).toBe(''); - }); + it('validates email as required', async () => { + const emailControl = component.form.get('email'); + emailControl?.patchValue(null); + component.form.updateValueAndValidity(); + expect(emailControl?.hasError('required')).toBeTrue(); + }); - it('validates email as required', async () => { - const emailControl = component.form.get('email'); - emailControl?.patchValue(null); - expect(emailControl?.hasError('required')).toBeTrue(); - }); + it('validates email format', () => { + const emailControl = component.form.get('email'); - it('validates email format', () => { - const emailControl = component.form.get('email'); + emailControl?.patchValue('invalid-email'); + component.form.updateValueAndValidity(); + expect(emailControl.hasError('email')).toBeTrue(); - // Set an invalid email format - emailControl?.patchValue('invalid-email'); - expect(emailControl.hasError('email')).toBeTrue(); + emailControl?.patchValue('test@example.com'); + component.form.updateValueAndValidity(); + expect(emailControl.hasError('email')).toBeFalse(); + }); - // Set a valid email format - emailControl?.patchValue('test@example.com'); - expect(emailControl.hasError('email')).toBeFalse(); - }); + it('enables form submission when form is valid', () => { + component.form.patchValue({ email: 'test@example.com' }); + component.form.updateValueAndValidity(); + fixture.detectChanges(); - it('validates password length', () => { - const newPasswordControl = component.form.get('newpassword'); - const confirmPasswordControl = component.form.get('confirmpass'); - - // Set a password with length less than PWD_MIN - newPasswordControl.patchValue('123'); - confirmPasswordControl.patchValue('123'); - expect(newPasswordControl.hasError('minlength')).toBe(true); - expect(confirmPasswordControl.hasError('minlength')).toBe(true); - - // Set a password with length equal to PWD_MIN - newPasswordControl.patchValue('1234'); - confirmPasswordControl.patchValue('1234'); - expect(newPasswordControl.hasError('minlength')).toBe(false); - expect(confirmPasswordControl.hasError('minlength')).toBe(false); - - // Set a password with length greater than PWD_MAX - newPasswordControl.patchValue('1234567890123'); - confirmPasswordControl.patchValue('1234567890123'); - expect(newPasswordControl.hasError('maxlength')).toBe(true); - expect(confirmPasswordControl.hasError('maxlength')).toBe(true); - - // Set a password with length equal to PWD_MAX - newPasswordControl.patchValue('123456789012'); - confirmPasswordControl.patchValue('123456789012'); - expect(newPasswordControl.hasError('maxlength')).toBe(false); - expect(confirmPasswordControl.hasError('maxlength')).toBe(false); - }); + expect(component.form.valid).toBe(true); + const btn = fixture.debugElement.query(By.css('[data-testid="button-update-submit"]')); + const nativeBtn = btn.nativeElement.querySelector('button'); + expect(nativeBtn?.disabled).toBeFalse(); + }); - it('validates password match', () => { - const newPasswordControl = component.changepasswordFormGroup.get('newPassword'); - const confirmPasswordControl = component.changepasswordFormGroup.get('confirmNewPassword'); - // Set different passwords - newPasswordControl.patchValue('password123'); - confirmPasswordControl.patchValue('password1234'); - expect(component.changepasswordFormGroup.hasError('mismatch')).toBe(true); - - // Set matching passwords - newPasswordControl.patchValue('password123'); - confirmPasswordControl.patchValue('password123'); - fixture.detectChanges(); - expect(component.changepasswordFormGroup.hasError('mismatch')).toBe(false); - }); + it('disables form submission when form is invalid', () => { + component.form.patchValue({ email: 'invalid-email' }); + component.form.updateValueAndValidity(); + fixture.detectChanges(); + + expect(component.form.valid).toBe(false); + const btn = fixture.debugElement.query(By.css('[data-testid="button-update-submit"]')); + const nativeBtn = btn.nativeElement.querySelector('button'); + expect(nativeBtn?.disabled).toBeTrue(); + }); + + it('submits form with valid email', () => { + spyOn(component['gs'], 'update').and.returnValue(of({})); + + component.form.patchValue({ email: 'test@example.com' }); + component.form.updateValueAndValidity(); + fixture.detectChanges(); + + const btnDebugEl = fixture.debugElement.query(By.css('[data-testid="button-update-submit"]')); + btnDebugEl.nativeElement.click(); + fixture.detectChanges(); + + expect(component['gs'].update).toHaveBeenCalledWith(SERV.USERS, component['gs'].userId, component.form.value); + expect(component['alert'].showSuccessMessage).toHaveBeenCalled(); + expect(routerSpy.navigate).toHaveBeenCalledOnceWith(['users/all-users']); + }); + + it('does not submit form with invalid email', () => { + const updateSpy = spyOn(component['gs'], 'update'); + component.form.patchValue({ email: 'invalid-email' }); + component.form.updateValueAndValidity(); + fixture.detectChanges(); - it('enables form submission when form is valid', () => { - const EmailControl = component.form.get('email'); - EmailControl.patchValue('test@example.com'); - // The form should now be valid, and the submit button should be enabled - expect(component.form.valid).toBe(true); - // Find all button-submit elements - const btns = fixture.debugElement.queryAll(By.css('[data-testid="button-submit"]')); - // Select the correct button (e.g., first for account update) - const btn = btns[0]; - const disabledAttr = btn.nativeElement.attributes.getNamedItem('ng-reflect-disabled'); - expect(disabledAttr ? disabledAttr.value : null).toEqual('false'); + const btnDebugEl = fixture.debugElement.query(By.css('[data-testid="button-update-submit"]')); + btnDebugEl.nativeElement.click(); + fixture.detectChanges(); + + expect(component.form.valid).toBeFalse(); + expect(updateSpy).not.toHaveBeenCalled(); + expect(component['alert'].showSuccessMessage).not.toHaveBeenCalled(); + expect(routerSpy.navigate).not.toHaveBeenCalled(); + }); }); - it('disables form submission when form is invalid', () => { - const EmailControl = component.form.get('email'); - EmailControl.patchValue('invalid-email'); - // The form should now be invalid, and the submit button should be disabled - expect(component.form.valid).toBe(false); - // Find all button-submit elements - const btns = fixture.debugElement.queryAll(By.css('[data-testid="button-submit"]')); - // Select the correct button (e.g., first for account update) - const btn = btns[0]; - const disabledAttr = btn.nativeElement.attributes.getNamedItem('ng-reflect-disabled'); - expect(disabledAttr ? disabledAttr.value : null).toEqual('true'); + describe('Password change form tests', () => { + it('initializes the password form with empty default values', () => { + const formValue = component.changepasswordFormGroup.getRawValue(); + expect(formValue.oldPassword).toBe(''); + expect(formValue.newPassword).toBe(''); + expect(formValue.confirmNewPassword).toBe(''); + }); + + it('validates all password fields as required', () => { + const form = component.changepasswordFormGroup; + + // Clear all values + form.patchValue({ + oldPassword: '', + newPassword: '', + confirmNewPassword: '' + }); + + form.markAllAsTouched(); + form.updateValueAndValidity(); + fixture.detectChanges(); + + const oldPasswordControl = form.get('oldPassword'); + const newPasswordControl = form.get('newPassword'); + const confirmPasswordControl = form.get('confirmNewPassword'); + + expect(oldPasswordControl?.hasError('required')).toBeTrue(); + expect(newPasswordControl?.hasError('required')).toBeTrue(); + expect(confirmPasswordControl?.hasError('required')).toBeTrue(); + }); + + it('validates password length', () => { + const newPasswordControl = component.changepasswordFormGroup.get('newPassword'); + const confirmPasswordControl = component.changepasswordFormGroup.get('confirmNewPassword'); + + // Too short + newPasswordControl.patchValue('123'); + confirmPasswordControl.patchValue('123'); + component.changepasswordFormGroup.updateValueAndValidity(); + + expect(newPasswordControl.hasError('minlength')).toBeTrue(); + expect(confirmPasswordControl.hasError('minlength')).toBeTrue(); + + // Exactly min + newPasswordControl.patchValue('1234'); + confirmPasswordControl.patchValue('1234'); + component.changepasswordFormGroup.updateValueAndValidity(); + + expect(newPasswordControl.hasError('minlength')).toBeFalse(); + expect(confirmPasswordControl.hasError('minlength')).toBeFalse(); + + // Too long + newPasswordControl.patchValue('VeryNiceLongPasswordButTooLongForHashtopolis'); + confirmPasswordControl.patchValue('VeryNiceLongPasswordButTooLongForHashtopolis'); + component.changepasswordFormGroup.updateValueAndValidity(); + + expect(newPasswordControl.hasError('maxlength')).toBeTrue(); + expect(confirmPasswordControl.hasError('maxlength')).toBeTrue(); + + // Exactly max + newPasswordControl.patchValue('Password12!#'); + confirmPasswordControl.patchValue('Password12!#'); + component.changepasswordFormGroup.updateValueAndValidity(); + + expect(newPasswordControl.hasError('maxlength')).toBeFalse(); + expect(confirmPasswordControl.hasError('maxlength')).toBeFalse(); + }); + + it('validates password match', () => { + // Set different passwords + component.changepasswordFormGroup.patchValue({ + newPassword: 'password1234', + confirmNewPassword: 'P4ssw0rd1234' + }); + component.changepasswordFormGroup.updateValueAndValidity(); + expect(component.changepasswordFormGroup.hasError('passwordMismatch')).toBe(true); + + // Set matching passwords + component.changepasswordFormGroup.patchValue({ + newPassword: 'password1234', + confirmNewPassword: 'password1234' + }); + component.changepasswordFormGroup.updateValueAndValidity(); + expect(component.changepasswordFormGroup.hasError('passwordMismatch')).toBe(false); + }); + + it('Submits password form and resets on success', fakeAsync(() => { + // Arrange + const chelperSpy = spyOn(component['gs'], 'chelper').and.returnValue( + of({ meta: { 'Change password': 'Password changed successfully' } }) + ); + const resetSpy = spyOn(component, 'resetPasswordForm'); + + // Fill valid form values + component.changepasswordFormGroup.patchValue({ + oldPassword: 'oldPass123', + newPassword: 'newPass456', + confirmNewPassword: 'newPass456' + }); + component.changepasswordFormGroup.updateValueAndValidity(); + fixture.detectChanges(); + + // Click the submit button + const btn = fixture.debugElement.query(By.css('[data-testid="button-password-submit"]')); + expect(btn).toBeTruthy(); + btn.nativeElement.click(); + + tick(); // flush observable + fixture.detectChanges(); + + // Assert + expect(chelperSpy).toHaveBeenCalledOnceWith(SERV.HELPER, 'changeOwnPassword', { + oldPassword: 'oldPass123', + newPassword: 'newPass456', + confirmPassword: 'newPass456' + }); + expect(component['alert'].showSuccessMessage).toHaveBeenCalledOnceWith('Password changed successfully'); + expect(resetSpy).toHaveBeenCalled(); + expect(component.isUpdatingPassLoading).toBeFalse(); + })); + + it('Does not submit password form if invalid', () => { + const chelperSpy = spyOn(component['gs'], 'chelper'); + const resetSpy = spyOn(component, 'resetPasswordForm'); + + // Make the form invalid by patching passwords not matching + component.changepasswordFormGroup.patchValue({ + oldPassword: 'oldpassword', + newPassword: 'newpassword', + confirmNewPassword: 'NewPassword' + }); + component.changepasswordFormGroup.updateValueAndValidity(); + fixture.detectChanges(); + + // Click the submit button + const btn = fixture.debugElement.query(By.css('[data-testid="button-password-submit"]')); + expect(btn).toBeTruthy(); + btn.nativeElement.click(); + + // Service should not be called + expect(chelperSpy).not.toHaveBeenCalled(); + // Alert should not be called + expect(component['alert'].showSuccessMessage).not.toHaveBeenCalled(); + // Reset should not be called + expect(resetSpy).not.toHaveBeenCalled(); + // isUpdatingPassLoading should remain false (no submission started) + expect(component.isUpdatingPassLoading).toBeFalse(); + }); }); }); diff --git a/src/app/account/settings/acc-settings/acc-settings.component.ts b/src/app/account/settings/acc-settings/acc-settings.component.ts index 5c969e93c..25259671a 100644 --- a/src/app/account/settings/acc-settings/acc-settings.component.ts +++ b/src/app/account/settings/acc-settings/acc-settings.component.ts @@ -14,6 +14,7 @@ import { ResponseWrapper } from '@src/app/core/_models/response.model'; import { JUser } from '@src/app/core/_models/user.model'; import { JsonAPISerializer } from '@src/app/core/_services/api/serializer-service'; import { RequestParamBuilder } from '@src/app/core/_services/params/builder-implementation.service'; +import { passwordMatchValidator } from '@src/app/core/_validators/password.validator'; export interface UpdateUserPassword { oldPassword: string; @@ -30,6 +31,9 @@ export class AccountSettingsComponent implements OnInit, OnDestroy { static readonly PWD_MIN = 4; static readonly PWD_MAX = 12; + pwdMin = AccountSettingsComponent.PWD_MIN; + pwdMax = AccountSettingsComponent.PWD_MAX; + pageTitle = 'Account Settings'; pageSubtitlePassword = 'Password Update'; @@ -40,7 +44,8 @@ export class AccountSettingsComponent implements OnInit, OnDestroy { /** On form update show a spinner loading */ isUpdatingLoading = false; isUpdatingPassLoading = false; - /* + + /** * Toggles for showing/hiding password fields in the form. * These are used to toggle visibility of the old, new, and confirm new password fields. * Hides the password input by default. @@ -49,10 +54,18 @@ export class AccountSettingsComponent implements OnInit, OnDestroy { showOldPassword: boolean = false; showNewPassword: boolean = false; showConfirmNewPassword: boolean = false; - strongPassword = false; + + /** + * Array to hold subscriptions for cleanup on component destruction. + * This prevents memory leaks by unsubscribing from observables when the component is destroyed. + */ subscriptions: Subscription[] = []; - emailControl: FormControl; + /** + * FormControl reference for easier access to form controls. + * This is used to access form controls in the template without needing to reference the entire form group. + */ + protected readonly FormControl = FormControl; constructor( private titleService: AutoTitleService, @@ -87,7 +100,7 @@ export class AccountSettingsComponent implements OnInit, OnDestroy { * @param {string} registeredSince - Account registration date. * @param {string} email - The user's email. */ - createForm(name = '', registeredSince = '', email = ''): void { + createForm(name: string = '', registeredSince: string = '', email: string = ''): void { this.form = new FormGroup({ name: new FormControl({ value: name, @@ -99,24 +112,27 @@ export class AccountSettingsComponent implements OnInit, OnDestroy { }), email: new FormControl(email, [Validators.required, Validators.email]) }); - //this.emailControl = this.form.get('email') as FormControl; } createUpdatePassForm() { - this.changepasswordFormGroup = new FormGroup({ - oldPassword: new FormControl('', Validators.required), - newPassword: new FormControl('', [ - Validators.required, - Validators.minLength(AccountSettingsComponent.PWD_MIN), - Validators.maxLength(AccountSettingsComponent.PWD_MAX) - ]), - confirmNewPassword: new FormControl('', [ - Validators.required, - Validators.minLength(AccountSettingsComponent.PWD_MIN), - Validators.maxLength(AccountSettingsComponent.PWD_MAX) - ]) - }); + this.changepasswordFormGroup = new FormGroup( + { + oldPassword: new FormControl('', Validators.required), + newPassword: new FormControl('', [ + Validators.required, + Validators.minLength(AccountSettingsComponent.PWD_MIN), + Validators.maxLength(AccountSettingsComponent.PWD_MAX) + ]), + confirmNewPassword: new FormControl('', [ + Validators.required, + Validators.minLength(AccountSettingsComponent.PWD_MIN), + Validators.maxLength(AccountSettingsComponent.PWD_MAX) + ]) + }, + { validators: passwordMatchValidator() } + ); } + get oldPasswordValueFromForm() { return this.changepasswordFormGroup.get('oldPassword').value; } @@ -143,7 +159,7 @@ export class AccountSettingsComponent implements OnInit, OnDestroy { this.gs.update(SERV.USERS, this.gs.userId, this.form.value).subscribe(() => { this.alert.showSuccessMessage('User saved'); this.isUpdatingLoading = false; - this.router.navigate(['users/all-users']); + void this.router.navigate(['users/all-users']); }) ); } @@ -153,6 +169,9 @@ export class AccountSettingsComponent implements OnInit, OnDestroy { * Handles password submission */ onSubmitPass() { + if (this.changepasswordFormGroup.invalid || this.changepasswordFormGroup.pending) { + return; + } this.isUpdatingPassLoading = true; const payload: UpdateUserPassword = { oldPassword: this.oldPasswordValueFromForm, @@ -185,6 +204,4 @@ export class AccountSettingsComponent implements OnInit, OnDestroy { }) ); } - - protected readonly FormControl = FormControl; } diff --git a/src/app/core/_validators/password.validator.ts b/src/app/core/_validators/password.validator.ts index 947bd391b..42e436580 100644 --- a/src/app/core/_validators/password.validator.ts +++ b/src/app/core/_validators/password.validator.ts @@ -1,9 +1,19 @@ import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms'; +export function passwordMatchValidator(): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + const newPassword = control.get('newPassword')?.value; + const confirmNewPassword = control.get('confirmNewPassword')?.value; -export const passwordMatchValidator: ValidatorFn = (control: AbstractControl): ValidationErrors | null => { - const a = control.get('newpassword').value - const b = control.get('confirmpass').value - - return a === b ? null : { 'mismatch': true }; -} \ No newline at end of file + if (newPassword !== confirmNewPassword) { + control.get('confirmNewPassword')?.setErrors({ passwordMismatch: true }); + return { passwordMismatch: true }; + } else { + const confirmCtrl = control.get('confirmNewPassword'); + if (confirmCtrl?.hasError('passwordMismatch')) { + confirmCtrl.updateValueAndValidity({ onlySelf: true, emitEvent: false }); + } + return null; + } + }; +} diff --git a/src/app/shared/buttons/button-submit.ts b/src/app/shared/buttons/button-submit.ts index 9576f4dd1..4e9f36689 100644 --- a/src/app/shared/buttons/button-submit.ts +++ b/src/app/shared/buttons/button-submit.ts @@ -1,6 +1,6 @@ +import { Location } from '@angular/common'; import { Component, Input, ViewEncapsulation } from '@angular/core'; import { Router } from '@angular/router'; -import { Location } from '@angular/common'; /** * Component for rendering a submit or cancel button. @@ -27,21 +27,21 @@ import { Location } from '@angular/common'; * ``` */ @Component({ - selector: 'button-submit', - template: ` + selector: 'button-submit', + template: ` `, - encapsulation: ViewEncapsulation.None, - standalone: false + encapsulation: ViewEncapsulation.None, + standalone: false }) export class ButtonSubmitComponent { /** @@ -78,12 +78,16 @@ export class ButtonSubmitComponent { /** * Handle the button click based on its type. */ - handleClick(): void { - if (this.type === 'cancel') { - this.location.back(); // Go back to the previous window - } else { + handleClick(event: Event): void { + if (this.disabled) { + event.preventDefault(); + event.stopImmediatePropagation(); return; } + + if (this.type === 'cancel') { + this.location.back(); + } } /** diff --git a/src/app/shared/input/text/text.component.html b/src/app/shared/input/text/text.component.html index aae17a8b7..3345405a4 100644 --- a/src/app/shared/input/text/text.component.html +++ b/src/app/shared/input/text/text.component.html @@ -1,35 +1,44 @@ - - {{ title }} + + + {{ title }} + {{ icon }} + + + + @if (showPasswordToggle) {