feat: Admin Area Challenges: List, Import/Export and Add/Edit Modal 1.02
This commit is contained in:
@@ -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';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user