AI Implementation feature(902): Admin Area Challenges: List, Import/Export and Add/Edit Modal 1.02 (#43)

This commit was merged in pull request #43.
This commit is contained in:
2026-07-22 21:47:52 +00:00
parent d3307c3225
commit fff0a600df
8 changed files with 351 additions and 159 deletions
@@ -11,7 +11,7 @@ import {
signal,
} from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormBuilder, FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
import { AbstractControl, FormBuilder, FormControl, FormGroup, ReactiveFormsModule, ValidationErrors, Validators } from '@angular/forms';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import {
AdminCategory,
@@ -25,6 +25,7 @@ import {
ChallengeFormErrors,
ChallengeFormFileItem,
ChallengeStagedFileUi,
CATEGORY_ID_PATTERN,
buildInitialFiles,
defaultMinimumPoints,
deriveMinimumPoints,
@@ -61,7 +62,7 @@ export function buildChallengeFormGroup(fb: FormBuilder): FormGroup<ChallengeFor
return fb.nonNullable.group({
name: fb.nonNullable.control('', [Validators.required, Validators.maxLength(128)]),
descriptionMd: fb.nonNullable.control('', [Validators.required]),
categoryId: fb.nonNullable.control('', [Validators.required]),
categoryId: fb.nonNullable.control('', [Validators.required, categoryIdUuidValidator]),
difficulty: fb.nonNullable.control<'LOW' | 'MEDIUM' | 'HIGH'>('MEDIUM'),
initialPoints: fb.nonNullable.control(100, [Validators.required, Validators.min(1), Validators.max(100000)]),
minimumPoints: fb.nonNullable.control(50, [Validators.required, Validators.min(1)]),
@@ -439,6 +440,9 @@ export class ChallengeFormModalComponent {
if (ctrl.errors?.['pattern']) {
return `${humanLabel(field)} must be a whole number`;
}
if (ctrl.errors?.['uuid']) {
return `${humanLabel(field)} is required`;
}
return null;
}
@@ -521,6 +525,15 @@ export class ChallengeFormModalComponent {
this.fileUploadError.set('Wait for uploads to finish before saving.');
return;
}
const categoryRaw = this.form.controls.categoryId.value;
if (!categoryRaw || !CATEGORY_ID_PATTERN.test(categoryRaw)) {
this.clientFieldErrors.set({ categoryId: 'Category is required' });
this.form.controls.categoryId.markAsTouched();
this.form.controls.categoryId.markAsDirty();
this.form.controls.categoryId.setErrors({ ...(this.form.controls.categoryId.errors ?? {}), uuid: true });
this.tab.set('general');
return;
}
const portRaw = this.form.controls.port.value;
const portAsNumber =
portRaw === null || portRaw === undefined || portRaw === ''
@@ -604,5 +617,13 @@ function humanLabel(field: keyof ChallengeFormErrors): string {
}
}
function categoryIdUuidValidator(control: AbstractControl): ValidationErrors | null {
const raw = control.value;
if (raw === null || raw === undefined || raw === '') {
return null;
}
return CATEGORY_ID_PATTERN.test(String(raw)) ? null : { uuid: true };
}
// Re-export so tests can find default helpers.
export { defaultMinimumPoints };
@@ -11,6 +11,8 @@ export type Protocol = 'NC' | 'WEB';
export const DEFAULT_INITIAL_POINTS = 100;
export const DEFAULT_DECAY_SOLVES = 10;
export const CATEGORY_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
export function defaultMinimumPoints(initial: number): number {
if (!Number.isFinite(initial) || initial < 1) return 1;
return Math.max(1, Math.floor(initial / 2));
@@ -83,7 +85,9 @@ export function validateChallengeForm(values: ChallengeFormDefaults): ChallengeF
if (!values.name.trim()) errs.name = 'Name is required';
else if (values.name.length > 128) errs.name = 'Name must be 128 characters or fewer';
if (!values.descriptionMd.trim()) errs.descriptionMd = 'Description is required';
if (!values.categoryId) errs.categoryId = 'Category is required';
if (!values.categoryId || !CATEGORY_ID_PATTERN.test(values.categoryId)) {
errs.categoryId = 'Category is required';
}
if (!Number.isInteger(values.initialPoints) || values.initialPoints < 1 || values.initialPoints > 100000) {
errs.initialPoints = 'Initial points must be between 1 and 100000';
}