AI Implementation feature(845): First-Start Create Admin Modal 1.04 (#7)
This commit was merged in pull request #7.
This commit is contained in:
@@ -10,11 +10,11 @@
|
||||
type="text"
|
||||
autocomplete="username"
|
||||
formControlName="username"
|
||||
[attr.aria-invalid]="!!usernameErrors()"
|
||||
[attr.aria-invalid]="!!usernameError()"
|
||||
data-testid="setup-username"
|
||||
/>
|
||||
@if (usernameErrors()) {
|
||||
<small class="field-error" data-testid="setup-username-error">{{ usernameErrors() }}</small>
|
||||
@if (usernameError()) {
|
||||
<small class="field-error" data-testid="setup-username-error">{{ usernameError() }}</small>
|
||||
}
|
||||
</label>
|
||||
|
||||
@@ -24,9 +24,13 @@
|
||||
type="password"
|
||||
autocomplete="new-password"
|
||||
formControlName="password"
|
||||
[attr.aria-invalid]="!!passwordError()"
|
||||
data-testid="setup-password"
|
||||
/>
|
||||
<small class="hint">{{ policyDescription() }}</small>
|
||||
@if (passwordError()) {
|
||||
<small class="field-error" data-testid="setup-password-error">{{ passwordError() }}</small>
|
||||
}
|
||||
</label>
|
||||
|
||||
<label class="field">
|
||||
@@ -35,10 +39,11 @@
|
||||
type="password"
|
||||
autocomplete="new-password"
|
||||
formControlName="passwordConfirm"
|
||||
[attr.aria-invalid]="!!confirmError()"
|
||||
data-testid="setup-password-confirm"
|
||||
/>
|
||||
@if (confirmErrors()) {
|
||||
<small class="field-error" data-testid="setup-confirm-error">{{ confirmErrors() }}</small>
|
||||
@if (confirmError()) {
|
||||
<small class="field-error" data-testid="setup-confirm-error">{{ confirmError() }}</small>
|
||||
}
|
||||
</label>
|
||||
|
||||
|
||||
@@ -17,6 +17,11 @@ import { AuthService } from '../../core/services/auth.service';
|
||||
import { BootstrapService } from '../../core/services/bootstrap.service';
|
||||
import { SetupCreateAdminService, CreateAdminFailure } from './setup-create-admin.service';
|
||||
import { passwordMatchValidator } from './setup-create-admin.validators';
|
||||
import {
|
||||
usernameError as computeUsernameError,
|
||||
passwordError as computePasswordError,
|
||||
confirmError as computeConfirmError,
|
||||
} from './setup-create-admin.field-errors';
|
||||
|
||||
@Component({
|
||||
selector: 'app-setup-create-admin',
|
||||
@@ -67,24 +72,33 @@ export class SetupCreateAdminComponent {
|
||||
() => !this.submitting() && this.serverError() !== null && this.serverErrorCode() !== 'USERNAME_TAKEN',
|
||||
);
|
||||
|
||||
readonly usernameErrors = computed(() => {
|
||||
usernameError(): string | null {
|
||||
const c = this.form.controls.username;
|
||||
if (!c.touched || c.valid) return null;
|
||||
if (c.errors?.['required']) return 'Username is required';
|
||||
if (c.errors?.['minlength']) return 'Username must be at least 3 characters';
|
||||
if (c.errors?.['maxlength']) return 'Username must be at most 32 characters';
|
||||
if (c.errors?.['pattern']) return 'Username may only contain letters, digits, dot, dash and underscore';
|
||||
return 'Invalid username';
|
||||
});
|
||||
return computeUsernameError({
|
||||
value: c.value,
|
||||
touched: c.touched,
|
||||
errors: (c.errors as { required?: true; minlength?: true; maxlength?: true; pattern?: true } | null) ?? null,
|
||||
});
|
||||
}
|
||||
|
||||
readonly confirmErrors = computed(() => {
|
||||
passwordError(): string | null {
|
||||
const c = this.form.controls.password;
|
||||
return computePasswordError({
|
||||
value: c.value,
|
||||
touched: c.touched,
|
||||
errors: (c.errors as { required?: true } | null) ?? null,
|
||||
});
|
||||
}
|
||||
|
||||
confirmError(): string | null {
|
||||
const c = this.form.controls.passwordConfirm;
|
||||
const groupMismatch = this.form.errors?.['passwordMismatch'];
|
||||
if ((!c.touched || c.valid) && !groupMismatch) return null;
|
||||
if (c.errors?.['required']) return 'Please confirm your password';
|
||||
if (groupMismatch) return 'Passwords do not match';
|
||||
return 'Invalid value';
|
||||
});
|
||||
return computeConfirmError({
|
||||
value: c.value,
|
||||
touched: c.touched,
|
||||
errors: (c.errors as { required?: true } | null) ?? null,
|
||||
groupMismatch: !!this.form.errors?.['passwordMismatch'],
|
||||
});
|
||||
}
|
||||
|
||||
@HostListener('document:keydown.escape', ['$event'])
|
||||
swallowEscape(event: KeyboardEvent): void {
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
export type FieldErrorKey =
|
||||
| 'required'
|
||||
| 'minlength'
|
||||
| 'maxlength'
|
||||
| 'pattern'
|
||||
| 'passwordMismatch';
|
||||
|
||||
export interface FieldErrorContext {
|
||||
value: string;
|
||||
touched: boolean;
|
||||
errors: Partial<Record<FieldErrorKey, true>> | null;
|
||||
groupMismatch?: boolean;
|
||||
}
|
||||
|
||||
export function usernameError(ctx: FieldErrorContext): string | null {
|
||||
const { touched, errors } = ctx;
|
||||
if (!touched || !errors) return null;
|
||||
if (errors.required) return 'Username is required';
|
||||
if (errors.minlength) return 'Username must be at least 3 characters';
|
||||
if (errors.maxlength) return 'Username must be at most 32 characters';
|
||||
if (errors.pattern) return 'Username may only contain letters, digits, dot, dash and underscore';
|
||||
return 'Invalid username';
|
||||
}
|
||||
|
||||
export function passwordError(ctx: FieldErrorContext): string | null {
|
||||
const { touched, errors } = ctx;
|
||||
if (!touched || !errors) return null;
|
||||
if (errors.required) return 'Password is required';
|
||||
return null;
|
||||
}
|
||||
|
||||
export function confirmError(ctx: FieldErrorContext): string | null {
|
||||
const { touched, errors, groupMismatch } = ctx;
|
||||
if (!touched && !groupMismatch) return null;
|
||||
if (errors?.required) return 'Please confirm your password';
|
||||
if (groupMismatch) return 'Passwords do not match';
|
||||
return 'Invalid value';
|
||||
}
|
||||
Reference in New Issue
Block a user