113 lines
3.9 KiB
TypeScript
113 lines
3.9 KiB
TypeScript
|
|
import type { ValidationResult, WizardState, WizardTrackConfig, WizardStageConfig } from '@/types/pipeline-wizard'
|
||
|
|
|
||
|
|
function ok(): ValidationResult {
|
||
|
|
return { valid: true, errors: [], warnings: [] }
|
||
|
|
}
|
||
|
|
|
||
|
|
function fail(errors: string[], warnings: string[] = []): ValidationResult {
|
||
|
|
return { valid: false, errors, warnings }
|
||
|
|
}
|
||
|
|
|
||
|
|
export function validateBasics(state: WizardState): ValidationResult {
|
||
|
|
const errors: string[] = []
|
||
|
|
if (!state.name.trim()) errors.push('Pipeline name is required')
|
||
|
|
if (!state.slug.trim()) errors.push('Pipeline slug is required')
|
||
|
|
else if (!/^[a-z0-9-]+$/.test(state.slug)) errors.push('Slug must be lowercase alphanumeric with hyphens only')
|
||
|
|
if (!state.programId) errors.push('Program must be selected')
|
||
|
|
return errors.length ? fail(errors) : ok()
|
||
|
|
}
|
||
|
|
|
||
|
|
export function validateStage(stage: WizardStageConfig): ValidationResult {
|
||
|
|
const errors: string[] = []
|
||
|
|
if (!stage.name.trim()) errors.push(`Stage name is required`)
|
||
|
|
if (!stage.slug.trim()) errors.push(`Stage slug is required`)
|
||
|
|
else if (!/^[a-z0-9-]+$/.test(stage.slug)) errors.push(`Stage slug "${stage.slug}" is invalid`)
|
||
|
|
return errors.length ? fail(errors) : ok()
|
||
|
|
}
|
||
|
|
|
||
|
|
export function validateTrack(track: WizardTrackConfig): ValidationResult {
|
||
|
|
const errors: string[] = []
|
||
|
|
const warnings: string[] = []
|
||
|
|
|
||
|
|
if (!track.name.trim()) errors.push('Track name is required')
|
||
|
|
if (!track.slug.trim()) errors.push('Track slug is required')
|
||
|
|
if (track.stages.length === 0) errors.push(`Track "${track.name}" must have at least one stage`)
|
||
|
|
|
||
|
|
// Check for duplicate slugs within track
|
||
|
|
const slugs = new Set<string>()
|
||
|
|
for (const stage of track.stages) {
|
||
|
|
if (slugs.has(stage.slug)) {
|
||
|
|
errors.push(`Duplicate stage slug "${stage.slug}" in track "${track.name}"`)
|
||
|
|
}
|
||
|
|
slugs.add(stage.slug)
|
||
|
|
const stageResult = validateStage(stage)
|
||
|
|
errors.push(...stageResult.errors)
|
||
|
|
}
|
||
|
|
|
||
|
|
// MAIN track should ideally have at least INTAKE and one other stage
|
||
|
|
if (track.kind === 'MAIN' && track.stages.length < 2) {
|
||
|
|
warnings.push('Main track should have at least 2 stages')
|
||
|
|
}
|
||
|
|
|
||
|
|
// AWARD tracks need awardConfig
|
||
|
|
if (track.kind === 'AWARD' && !track.awardConfig?.name) {
|
||
|
|
errors.push(`Award track "${track.name}" requires an award name`)
|
||
|
|
}
|
||
|
|
|
||
|
|
return errors.length ? fail(errors, warnings) : { valid: true, errors: [], warnings }
|
||
|
|
}
|
||
|
|
|
||
|
|
export function validateTracks(tracks: WizardTrackConfig[]): ValidationResult {
|
||
|
|
const errors: string[] = []
|
||
|
|
const warnings: string[] = []
|
||
|
|
|
||
|
|
if (tracks.length === 0) {
|
||
|
|
errors.push('At least one track is required')
|
||
|
|
return fail(errors)
|
||
|
|
}
|
||
|
|
|
||
|
|
const mainTracks = tracks.filter((t) => t.kind === 'MAIN')
|
||
|
|
if (mainTracks.length === 0) {
|
||
|
|
errors.push('At least one MAIN track is required')
|
||
|
|
} else if (mainTracks.length > 1) {
|
||
|
|
warnings.push('Multiple MAIN tracks detected — typically only one is needed')
|
||
|
|
}
|
||
|
|
|
||
|
|
// Check for duplicate track slugs
|
||
|
|
const trackSlugs = new Set<string>()
|
||
|
|
for (const track of tracks) {
|
||
|
|
if (trackSlugs.has(track.slug)) {
|
||
|
|
errors.push(`Duplicate track slug "${track.slug}"`)
|
||
|
|
}
|
||
|
|
trackSlugs.add(track.slug)
|
||
|
|
const trackResult = validateTrack(track)
|
||
|
|
errors.push(...trackResult.errors)
|
||
|
|
warnings.push(...trackResult.warnings)
|
||
|
|
}
|
||
|
|
|
||
|
|
return errors.length ? fail(errors, warnings) : { valid: true, errors: [], warnings }
|
||
|
|
}
|
||
|
|
|
||
|
|
export function validateNotifications(config: Record<string, boolean>): ValidationResult {
|
||
|
|
// Notifications are optional — just validate structure
|
||
|
|
return ok()
|
||
|
|
}
|
||
|
|
|
||
|
|
export function validateAll(state: WizardState): {
|
||
|
|
valid: boolean
|
||
|
|
sections: {
|
||
|
|
basics: ValidationResult
|
||
|
|
tracks: ValidationResult
|
||
|
|
notifications: ValidationResult
|
||
|
|
}
|
||
|
|
} {
|
||
|
|
const basics = validateBasics(state)
|
||
|
|
const tracks = validateTracks(state.tracks)
|
||
|
|
const notifications = validateNotifications(state.notificationConfig)
|
||
|
|
|
||
|
|
return {
|
||
|
|
valid: basics.valid && tracks.valid && notifications.valid,
|
||
|
|
sections: { basics, tracks, notifications },
|
||
|
|
}
|
||
|
|
}
|