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:
2026-07-21 16:49:50 +00:00
parent 2957b14d38
commit cabd393288
12 changed files with 393 additions and 244 deletions
@@ -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';
}