Fix pipeline edit crash: merge defaults with DB configJson
Build and Push Docker Image / build (push) Successful in 10m2s
Details
Build and Push Docker Image / build (push) Successful in 10m2s
Details
The DB configJson uses different field names than wizard types expect
(e.g., deterministic.rules vs rules, votingEnabled vs juryVotingEnabled).
The ?? operator only guards null/undefined, but configJson is {} (truthy),
so defaults never applied. This caused config.rules.map() to crash with
"Cannot read properties of undefined (reading 'map')".
Fix: spread defaults first then overlay DB values, and add defensive ??
fallbacks in all section components for potentially undefined properties.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
7d1c87e938
commit
451b483880
|
|
@ -187,10 +187,10 @@ export default function EditPipelinePage() {
|
|||
const evalStage = mainTrack?.stages.find((s) => s.stageType === 'EVALUATION')
|
||||
const liveStage = mainTrack?.stages.find((s) => s.stageType === 'LIVE_FINAL')
|
||||
|
||||
const intakeConfig = (intakeStage?.configJson ?? defaultIntakeConfig()) as unknown as IntakeConfig
|
||||
const filterConfig = (filterStage?.configJson ?? defaultFilterConfig()) as unknown as FilterConfig
|
||||
const evalConfig = (evalStage?.configJson ?? defaultEvaluationConfig()) as unknown as EvaluationConfig
|
||||
const liveConfig = (liveStage?.configJson ?? defaultLiveConfig()) as unknown as LiveFinalConfig
|
||||
const intakeConfig = { ...defaultIntakeConfig(), ...(intakeStage?.configJson ?? {}) } as IntakeConfig
|
||||
const filterConfig = { ...defaultFilterConfig(), ...(filterStage?.configJson ?? {}) } as FilterConfig
|
||||
const evalConfig = { ...defaultEvaluationConfig(), ...(evalStage?.configJson ?? {}) } as EvaluationConfig
|
||||
const liveConfig = { ...defaultLiveConfig(), ...(liveStage?.configJson ?? {}) } as LiveFinalConfig
|
||||
|
||||
const basicsValid = validateBasics(state).valid
|
||||
const tracksValid = validateTracks(state.tracks).valid
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ export function AssignmentSection({ config, onChange, isActive }: AssignmentSect
|
|||
type="number"
|
||||
min={1}
|
||||
max={20}
|
||||
value={config.requiredReviews}
|
||||
value={config.requiredReviews ?? 3}
|
||||
disabled={isActive}
|
||||
onChange={(e) =>
|
||||
updateConfig({ requiredReviews: parseInt(e.target.value) || 3 })
|
||||
|
|
@ -49,7 +49,7 @@ export function AssignmentSection({ config, onChange, isActive }: AssignmentSect
|
|||
type="number"
|
||||
min={1}
|
||||
max={100}
|
||||
value={config.maxLoadPerJuror}
|
||||
value={config.maxLoadPerJuror ?? 20}
|
||||
disabled={isActive}
|
||||
onChange={(e) =>
|
||||
updateConfig({ maxLoadPerJuror: parseInt(e.target.value) || 20 })
|
||||
|
|
@ -66,7 +66,7 @@ export function AssignmentSection({ config, onChange, isActive }: AssignmentSect
|
|||
type="number"
|
||||
min={0}
|
||||
max={50}
|
||||
value={config.minLoadPerJuror}
|
||||
value={config.minLoadPerJuror ?? 5}
|
||||
disabled={isActive}
|
||||
onChange={(e) =>
|
||||
updateConfig({ minLoadPerJuror: parseInt(e.target.value) || 5 })
|
||||
|
|
@ -78,7 +78,7 @@ export function AssignmentSection({ config, onChange, isActive }: AssignmentSect
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{config.minLoadPerJuror > config.maxLoadPerJuror && (
|
||||
{(config.minLoadPerJuror ?? 0) > (config.maxLoadPerJuror ?? 20) && (
|
||||
<p className="text-sm text-destructive">
|
||||
Min load per juror cannot exceed max load per juror.
|
||||
</p>
|
||||
|
|
@ -92,7 +92,7 @@ export function AssignmentSection({ config, onChange, isActive }: AssignmentSect
|
|||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={config.availabilityWeighting}
|
||||
checked={config.availabilityWeighting ?? true}
|
||||
onCheckedChange={(checked) =>
|
||||
updateConfig({ availabilityWeighting: checked })
|
||||
}
|
||||
|
|
@ -103,7 +103,7 @@ export function AssignmentSection({ config, onChange, isActive }: AssignmentSect
|
|||
<div className="space-y-2">
|
||||
<Label>Overflow Policy</Label>
|
||||
<Select
|
||||
value={config.overflowPolicy}
|
||||
value={config.overflowPolicy ?? 'queue'}
|
||||
onValueChange={(value) =>
|
||||
updateConfig({
|
||||
overflowPolicy: value as EvaluationConfig['overflowPolicy'],
|
||||
|
|
|
|||
|
|
@ -27,8 +27,10 @@ export function FilteringSection({ config, onChange, isActive }: FilteringSectio
|
|||
onChange({ ...config, ...updates })
|
||||
}
|
||||
|
||||
const rules = config.rules ?? []
|
||||
|
||||
const updateRule = (index: number, updates: Partial<FilterRuleConfig>) => {
|
||||
const updated = [...config.rules]
|
||||
const updated = [...rules]
|
||||
updated[index] = { ...updated[index], ...updates }
|
||||
onChange({ ...config, rules: updated })
|
||||
}
|
||||
|
|
@ -37,14 +39,14 @@ export function FilteringSection({ config, onChange, isActive }: FilteringSectio
|
|||
onChange({
|
||||
...config,
|
||||
rules: [
|
||||
...config.rules,
|
||||
...rules,
|
||||
{ field: '', operator: 'equals', value: '', weight: 1 },
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
const removeRule = (index: number) => {
|
||||
onChange({ ...config, rules: config.rules.filter((_, i) => i !== index) })
|
||||
onChange({ ...config, rules: rules.filter((_, i) => i !== index) })
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
@ -64,7 +66,7 @@ export function FilteringSection({ config, onChange, isActive }: FilteringSectio
|
|||
</Button>
|
||||
</div>
|
||||
|
||||
{config.rules.map((rule, index) => (
|
||||
{rules.map((rule, index) => (
|
||||
<Card key={index}>
|
||||
<CardContent className="pt-3 pb-3 px-4">
|
||||
<div className="flex items-center gap-2">
|
||||
|
|
@ -116,7 +118,7 @@ export function FilteringSection({ config, onChange, isActive }: FilteringSectio
|
|||
</Card>
|
||||
))}
|
||||
|
||||
{config.rules.length === 0 && (
|
||||
{rules.length === 0 && (
|
||||
<p className="text-sm text-muted-foreground text-center py-3">
|
||||
No gate rules configured. All projects will pass through.
|
||||
</p>
|
||||
|
|
@ -145,11 +147,11 @@ export function FilteringSection({ config, onChange, isActive }: FilteringSectio
|
|||
<Label className="text-xs">High Confidence Threshold</Label>
|
||||
<div className="flex items-center gap-3">
|
||||
<Slider
|
||||
value={[config.aiConfidenceThresholds.high * 100]}
|
||||
value={[(config.aiConfidenceThresholds?.high ?? 0.85) * 100]}
|
||||
onValueChange={([v]) =>
|
||||
updateConfig({
|
||||
aiConfidenceThresholds: {
|
||||
...config.aiConfidenceThresholds,
|
||||
...(config.aiConfidenceThresholds ?? { high: 0.85, medium: 0.6, low: 0.4 }),
|
||||
high: v / 100,
|
||||
},
|
||||
})
|
||||
|
|
@ -160,7 +162,7 @@ export function FilteringSection({ config, onChange, isActive }: FilteringSectio
|
|||
className="flex-1"
|
||||
/>
|
||||
<span className="text-xs font-mono w-10 text-right">
|
||||
{Math.round(config.aiConfidenceThresholds.high * 100)}%
|
||||
{Math.round((config.aiConfidenceThresholds?.high ?? 0.85) * 100)}%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -168,11 +170,11 @@ export function FilteringSection({ config, onChange, isActive }: FilteringSectio
|
|||
<Label className="text-xs">Medium Confidence Threshold</Label>
|
||||
<div className="flex items-center gap-3">
|
||||
<Slider
|
||||
value={[config.aiConfidenceThresholds.medium * 100]}
|
||||
value={[(config.aiConfidenceThresholds?.medium ?? 0.6) * 100]}
|
||||
onValueChange={([v]) =>
|
||||
updateConfig({
|
||||
aiConfidenceThresholds: {
|
||||
...config.aiConfidenceThresholds,
|
||||
...(config.aiConfidenceThresholds ?? { high: 0.85, medium: 0.6, low: 0.4 }),
|
||||
medium: v / 100,
|
||||
},
|
||||
})
|
||||
|
|
@ -183,7 +185,7 @@ export function FilteringSection({ config, onChange, isActive }: FilteringSectio
|
|||
className="flex-1"
|
||||
/>
|
||||
<span className="text-xs font-mono w-10 text-right">
|
||||
{Math.round(config.aiConfidenceThresholds.medium * 100)}%
|
||||
{Math.round((config.aiConfidenceThresholds?.medium ?? 0.6) * 100)}%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -26,8 +26,10 @@ export function IntakeSection({ config, onChange, isActive }: IntakeSectionProps
|
|||
onChange({ ...config, ...updates })
|
||||
}
|
||||
|
||||
const fileRequirements = config.fileRequirements ?? []
|
||||
|
||||
const updateFileReq = (index: number, updates: Partial<FileRequirementConfig>) => {
|
||||
const updated = [...config.fileRequirements]
|
||||
const updated = [...fileRequirements]
|
||||
updated[index] = { ...updated[index], ...updates }
|
||||
onChange({ ...config, fileRequirements: updated })
|
||||
}
|
||||
|
|
@ -36,7 +38,7 @@ export function IntakeSection({ config, onChange, isActive }: IntakeSectionProps
|
|||
onChange({
|
||||
...config,
|
||||
fileRequirements: [
|
||||
...config.fileRequirements,
|
||||
...fileRequirements,
|
||||
{
|
||||
name: '',
|
||||
description: '',
|
||||
|
|
@ -49,7 +51,7 @@ export function IntakeSection({ config, onChange, isActive }: IntakeSectionProps
|
|||
}
|
||||
|
||||
const removeFileReq = (index: number) => {
|
||||
const updated = config.fileRequirements.filter((_, i) => i !== index)
|
||||
const updated = fileRequirements.filter((_, i) => i !== index)
|
||||
onChange({ ...config, fileRequirements: updated })
|
||||
}
|
||||
|
||||
|
|
@ -71,7 +73,7 @@ export function IntakeSection({ config, onChange, isActive }: IntakeSectionProps
|
|||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={config.submissionWindowEnabled}
|
||||
checked={config.submissionWindowEnabled ?? true}
|
||||
onCheckedChange={(checked) =>
|
||||
updateConfig({ submissionWindowEnabled: checked })
|
||||
}
|
||||
|
|
@ -85,7 +87,7 @@ export function IntakeSection({ config, onChange, isActive }: IntakeSectionProps
|
|||
<div className="space-y-2">
|
||||
<Label>Late Submission Policy</Label>
|
||||
<Select
|
||||
value={config.lateSubmissionPolicy}
|
||||
value={config.lateSubmissionPolicy ?? 'flag'}
|
||||
onValueChange={(value) =>
|
||||
updateConfig({
|
||||
lateSubmissionPolicy: value as IntakeConfig['lateSubmissionPolicy'],
|
||||
|
|
@ -104,14 +106,14 @@ export function IntakeSection({ config, onChange, isActive }: IntakeSectionProps
|
|||
</Select>
|
||||
</div>
|
||||
|
||||
{config.lateSubmissionPolicy === 'flag' && (
|
||||
{(config.lateSubmissionPolicy ?? 'flag') === 'flag' && (
|
||||
<div className="space-y-2">
|
||||
<Label>Grace Period (hours)</Label>
|
||||
<Input
|
||||
type="number"
|
||||
min={0}
|
||||
max={168}
|
||||
value={config.lateGraceHours}
|
||||
value={config.lateGraceHours ?? 24}
|
||||
onChange={(e) =>
|
||||
updateConfig({ lateGraceHours: parseInt(e.target.value) || 0 })
|
||||
}
|
||||
|
|
@ -130,13 +132,13 @@ export function IntakeSection({ config, onChange, isActive }: IntakeSectionProps
|
|||
</Button>
|
||||
</div>
|
||||
|
||||
{config.fileRequirements.length === 0 && (
|
||||
{fileRequirements.length === 0 && (
|
||||
<p className="text-sm text-muted-foreground py-4 text-center">
|
||||
No file requirements configured. Projects can be submitted without files.
|
||||
</p>
|
||||
)}
|
||||
|
||||
{config.fileRequirements.map((req, index) => (
|
||||
{fileRequirements.map((req, index) => (
|
||||
<Card key={index}>
|
||||
<CardContent className="pt-4 space-y-3">
|
||||
<div className="flex items-start gap-3">
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ export function LiveFinalsSection({ config, onChange, isActive }: LiveFinalsSect
|
|||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={config.juryVotingEnabled}
|
||||
checked={config.juryVotingEnabled ?? true}
|
||||
onCheckedChange={(checked) =>
|
||||
updateConfig({ juryVotingEnabled: checked })
|
||||
}
|
||||
|
|
@ -50,7 +50,7 @@ export function LiveFinalsSection({ config, onChange, isActive }: LiveFinalsSect
|
|||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={config.audienceVotingEnabled}
|
||||
checked={config.audienceVotingEnabled ?? false}
|
||||
onCheckedChange={(checked) =>
|
||||
updateConfig({ audienceVotingEnabled: checked })
|
||||
}
|
||||
|
|
@ -58,13 +58,13 @@ export function LiveFinalsSection({ config, onChange, isActive }: LiveFinalsSect
|
|||
/>
|
||||
</div>
|
||||
|
||||
{config.audienceVotingEnabled && (
|
||||
{(config.audienceVotingEnabled ?? false) && (
|
||||
<div className="pl-4 border-l-2 border-muted space-y-3">
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs">Audience Vote Weight</Label>
|
||||
<div className="flex items-center gap-3">
|
||||
<Slider
|
||||
value={[config.audienceVoteWeight * 100]}
|
||||
value={[(config.audienceVoteWeight ?? 0) * 100]}
|
||||
onValueChange={([v]) =>
|
||||
updateConfig({ audienceVoteWeight: v / 100 })
|
||||
}
|
||||
|
|
@ -74,7 +74,7 @@ export function LiveFinalsSection({ config, onChange, isActive }: LiveFinalsSect
|
|||
className="flex-1"
|
||||
/>
|
||||
<span className="text-xs font-mono w-10 text-right">
|
||||
{Math.round(config.audienceVoteWeight * 100)}%
|
||||
{Math.round((config.audienceVoteWeight ?? 0) * 100)}%
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
|
|
@ -88,7 +88,7 @@ export function LiveFinalsSection({ config, onChange, isActive }: LiveFinalsSect
|
|||
<div className="space-y-2">
|
||||
<Label>Cohort Setup Mode</Label>
|
||||
<Select
|
||||
value={config.cohortSetupMode}
|
||||
value={config.cohortSetupMode ?? 'manual'}
|
||||
onValueChange={(value) =>
|
||||
updateConfig({
|
||||
cohortSetupMode: value as LiveFinalConfig['cohortSetupMode'],
|
||||
|
|
@ -113,7 +113,7 @@ export function LiveFinalsSection({ config, onChange, isActive }: LiveFinalsSect
|
|||
<div className="space-y-2">
|
||||
<Label>Result Reveal Policy</Label>
|
||||
<Select
|
||||
value={config.revealPolicy}
|
||||
value={config.revealPolicy ?? 'ceremony'}
|
||||
onValueChange={(value) =>
|
||||
updateConfig({
|
||||
revealPolicy: value as LiveFinalConfig['revealPolicy'],
|
||||
|
|
|
|||
Loading…
Reference in New Issue