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 evalStage = mainTrack?.stages.find((s) => s.stageType === 'EVALUATION')
|
||||||
const liveStage = mainTrack?.stages.find((s) => s.stageType === 'LIVE_FINAL')
|
const liveStage = mainTrack?.stages.find((s) => s.stageType === 'LIVE_FINAL')
|
||||||
|
|
||||||
const intakeConfig = (intakeStage?.configJson ?? defaultIntakeConfig()) as unknown as IntakeConfig
|
const intakeConfig = { ...defaultIntakeConfig(), ...(intakeStage?.configJson ?? {}) } as IntakeConfig
|
||||||
const filterConfig = (filterStage?.configJson ?? defaultFilterConfig()) as unknown as FilterConfig
|
const filterConfig = { ...defaultFilterConfig(), ...(filterStage?.configJson ?? {}) } as FilterConfig
|
||||||
const evalConfig = (evalStage?.configJson ?? defaultEvaluationConfig()) as unknown as EvaluationConfig
|
const evalConfig = { ...defaultEvaluationConfig(), ...(evalStage?.configJson ?? {}) } as EvaluationConfig
|
||||||
const liveConfig = (liveStage?.configJson ?? defaultLiveConfig()) as unknown as LiveFinalConfig
|
const liveConfig = { ...defaultLiveConfig(), ...(liveStage?.configJson ?? {}) } as LiveFinalConfig
|
||||||
|
|
||||||
const basicsValid = validateBasics(state).valid
|
const basicsValid = validateBasics(state).valid
|
||||||
const tracksValid = validateTracks(state.tracks).valid
|
const tracksValid = validateTracks(state.tracks).valid
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ export function AssignmentSection({ config, onChange, isActive }: AssignmentSect
|
||||||
type="number"
|
type="number"
|
||||||
min={1}
|
min={1}
|
||||||
max={20}
|
max={20}
|
||||||
value={config.requiredReviews}
|
value={config.requiredReviews ?? 3}
|
||||||
disabled={isActive}
|
disabled={isActive}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
updateConfig({ requiredReviews: parseInt(e.target.value) || 3 })
|
updateConfig({ requiredReviews: parseInt(e.target.value) || 3 })
|
||||||
|
|
@ -49,7 +49,7 @@ export function AssignmentSection({ config, onChange, isActive }: AssignmentSect
|
||||||
type="number"
|
type="number"
|
||||||
min={1}
|
min={1}
|
||||||
max={100}
|
max={100}
|
||||||
value={config.maxLoadPerJuror}
|
value={config.maxLoadPerJuror ?? 20}
|
||||||
disabled={isActive}
|
disabled={isActive}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
updateConfig({ maxLoadPerJuror: parseInt(e.target.value) || 20 })
|
updateConfig({ maxLoadPerJuror: parseInt(e.target.value) || 20 })
|
||||||
|
|
@ -66,7 +66,7 @@ export function AssignmentSection({ config, onChange, isActive }: AssignmentSect
|
||||||
type="number"
|
type="number"
|
||||||
min={0}
|
min={0}
|
||||||
max={50}
|
max={50}
|
||||||
value={config.minLoadPerJuror}
|
value={config.minLoadPerJuror ?? 5}
|
||||||
disabled={isActive}
|
disabled={isActive}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
updateConfig({ minLoadPerJuror: parseInt(e.target.value) || 5 })
|
updateConfig({ minLoadPerJuror: parseInt(e.target.value) || 5 })
|
||||||
|
|
@ -78,7 +78,7 @@ export function AssignmentSection({ config, onChange, isActive }: AssignmentSect
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{config.minLoadPerJuror > config.maxLoadPerJuror && (
|
{(config.minLoadPerJuror ?? 0) > (config.maxLoadPerJuror ?? 20) && (
|
||||||
<p className="text-sm text-destructive">
|
<p className="text-sm text-destructive">
|
||||||
Min load per juror cannot exceed max load per juror.
|
Min load per juror cannot exceed max load per juror.
|
||||||
</p>
|
</p>
|
||||||
|
|
@ -92,7 +92,7 @@ export function AssignmentSection({ config, onChange, isActive }: AssignmentSect
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Switch
|
<Switch
|
||||||
checked={config.availabilityWeighting}
|
checked={config.availabilityWeighting ?? true}
|
||||||
onCheckedChange={(checked) =>
|
onCheckedChange={(checked) =>
|
||||||
updateConfig({ availabilityWeighting: checked })
|
updateConfig({ availabilityWeighting: checked })
|
||||||
}
|
}
|
||||||
|
|
@ -103,7 +103,7 @@ export function AssignmentSection({ config, onChange, isActive }: AssignmentSect
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label>Overflow Policy</Label>
|
<Label>Overflow Policy</Label>
|
||||||
<Select
|
<Select
|
||||||
value={config.overflowPolicy}
|
value={config.overflowPolicy ?? 'queue'}
|
||||||
onValueChange={(value) =>
|
onValueChange={(value) =>
|
||||||
updateConfig({
|
updateConfig({
|
||||||
overflowPolicy: value as EvaluationConfig['overflowPolicy'],
|
overflowPolicy: value as EvaluationConfig['overflowPolicy'],
|
||||||
|
|
|
||||||
|
|
@ -27,8 +27,10 @@ export function FilteringSection({ config, onChange, isActive }: FilteringSectio
|
||||||
onChange({ ...config, ...updates })
|
onChange({ ...config, ...updates })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const rules = config.rules ?? []
|
||||||
|
|
||||||
const updateRule = (index: number, updates: Partial<FilterRuleConfig>) => {
|
const updateRule = (index: number, updates: Partial<FilterRuleConfig>) => {
|
||||||
const updated = [...config.rules]
|
const updated = [...rules]
|
||||||
updated[index] = { ...updated[index], ...updates }
|
updated[index] = { ...updated[index], ...updates }
|
||||||
onChange({ ...config, rules: updated })
|
onChange({ ...config, rules: updated })
|
||||||
}
|
}
|
||||||
|
|
@ -37,14 +39,14 @@ export function FilteringSection({ config, onChange, isActive }: FilteringSectio
|
||||||
onChange({
|
onChange({
|
||||||
...config,
|
...config,
|
||||||
rules: [
|
rules: [
|
||||||
...config.rules,
|
...rules,
|
||||||
{ field: '', operator: 'equals', value: '', weight: 1 },
|
{ field: '', operator: 'equals', value: '', weight: 1 },
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const removeRule = (index: number) => {
|
const removeRule = (index: number) => {
|
||||||
onChange({ ...config, rules: config.rules.filter((_, i) => i !== index) })
|
onChange({ ...config, rules: rules.filter((_, i) => i !== index) })
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -64,7 +66,7 @@ export function FilteringSection({ config, onChange, isActive }: FilteringSectio
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{config.rules.map((rule, index) => (
|
{rules.map((rule, index) => (
|
||||||
<Card key={index}>
|
<Card key={index}>
|
||||||
<CardContent className="pt-3 pb-3 px-4">
|
<CardContent className="pt-3 pb-3 px-4">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
|
|
@ -116,7 +118,7 @@ export function FilteringSection({ config, onChange, isActive }: FilteringSectio
|
||||||
</Card>
|
</Card>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{config.rules.length === 0 && (
|
{rules.length === 0 && (
|
||||||
<p className="text-sm text-muted-foreground text-center py-3">
|
<p className="text-sm text-muted-foreground text-center py-3">
|
||||||
No gate rules configured. All projects will pass through.
|
No gate rules configured. All projects will pass through.
|
||||||
</p>
|
</p>
|
||||||
|
|
@ -145,11 +147,11 @@ export function FilteringSection({ config, onChange, isActive }: FilteringSectio
|
||||||
<Label className="text-xs">High Confidence Threshold</Label>
|
<Label className="text-xs">High Confidence Threshold</Label>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<Slider
|
<Slider
|
||||||
value={[config.aiConfidenceThresholds.high * 100]}
|
value={[(config.aiConfidenceThresholds?.high ?? 0.85) * 100]}
|
||||||
onValueChange={([v]) =>
|
onValueChange={([v]) =>
|
||||||
updateConfig({
|
updateConfig({
|
||||||
aiConfidenceThresholds: {
|
aiConfidenceThresholds: {
|
||||||
...config.aiConfidenceThresholds,
|
...(config.aiConfidenceThresholds ?? { high: 0.85, medium: 0.6, low: 0.4 }),
|
||||||
high: v / 100,
|
high: v / 100,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
@ -160,7 +162,7 @@ export function FilteringSection({ config, onChange, isActive }: FilteringSectio
|
||||||
className="flex-1"
|
className="flex-1"
|
||||||
/>
|
/>
|
||||||
<span className="text-xs font-mono w-10 text-right">
|
<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>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -168,11 +170,11 @@ export function FilteringSection({ config, onChange, isActive }: FilteringSectio
|
||||||
<Label className="text-xs">Medium Confidence Threshold</Label>
|
<Label className="text-xs">Medium Confidence Threshold</Label>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<Slider
|
<Slider
|
||||||
value={[config.aiConfidenceThresholds.medium * 100]}
|
value={[(config.aiConfidenceThresholds?.medium ?? 0.6) * 100]}
|
||||||
onValueChange={([v]) =>
|
onValueChange={([v]) =>
|
||||||
updateConfig({
|
updateConfig({
|
||||||
aiConfidenceThresholds: {
|
aiConfidenceThresholds: {
|
||||||
...config.aiConfidenceThresholds,
|
...(config.aiConfidenceThresholds ?? { high: 0.85, medium: 0.6, low: 0.4 }),
|
||||||
medium: v / 100,
|
medium: v / 100,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
@ -183,7 +185,7 @@ export function FilteringSection({ config, onChange, isActive }: FilteringSectio
|
||||||
className="flex-1"
|
className="flex-1"
|
||||||
/>
|
/>
|
||||||
<span className="text-xs font-mono w-10 text-right">
|
<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>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -26,8 +26,10 @@ export function IntakeSection({ config, onChange, isActive }: IntakeSectionProps
|
||||||
onChange({ ...config, ...updates })
|
onChange({ ...config, ...updates })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const fileRequirements = config.fileRequirements ?? []
|
||||||
|
|
||||||
const updateFileReq = (index: number, updates: Partial<FileRequirementConfig>) => {
|
const updateFileReq = (index: number, updates: Partial<FileRequirementConfig>) => {
|
||||||
const updated = [...config.fileRequirements]
|
const updated = [...fileRequirements]
|
||||||
updated[index] = { ...updated[index], ...updates }
|
updated[index] = { ...updated[index], ...updates }
|
||||||
onChange({ ...config, fileRequirements: updated })
|
onChange({ ...config, fileRequirements: updated })
|
||||||
}
|
}
|
||||||
|
|
@ -36,7 +38,7 @@ export function IntakeSection({ config, onChange, isActive }: IntakeSectionProps
|
||||||
onChange({
|
onChange({
|
||||||
...config,
|
...config,
|
||||||
fileRequirements: [
|
fileRequirements: [
|
||||||
...config.fileRequirements,
|
...fileRequirements,
|
||||||
{
|
{
|
||||||
name: '',
|
name: '',
|
||||||
description: '',
|
description: '',
|
||||||
|
|
@ -49,7 +51,7 @@ export function IntakeSection({ config, onChange, isActive }: IntakeSectionProps
|
||||||
}
|
}
|
||||||
|
|
||||||
const removeFileReq = (index: number) => {
|
const removeFileReq = (index: number) => {
|
||||||
const updated = config.fileRequirements.filter((_, i) => i !== index)
|
const updated = fileRequirements.filter((_, i) => i !== index)
|
||||||
onChange({ ...config, fileRequirements: updated })
|
onChange({ ...config, fileRequirements: updated })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -71,7 +73,7 @@ export function IntakeSection({ config, onChange, isActive }: IntakeSectionProps
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Switch
|
<Switch
|
||||||
checked={config.submissionWindowEnabled}
|
checked={config.submissionWindowEnabled ?? true}
|
||||||
onCheckedChange={(checked) =>
|
onCheckedChange={(checked) =>
|
||||||
updateConfig({ submissionWindowEnabled: checked })
|
updateConfig({ submissionWindowEnabled: checked })
|
||||||
}
|
}
|
||||||
|
|
@ -85,7 +87,7 @@ export function IntakeSection({ config, onChange, isActive }: IntakeSectionProps
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label>Late Submission Policy</Label>
|
<Label>Late Submission Policy</Label>
|
||||||
<Select
|
<Select
|
||||||
value={config.lateSubmissionPolicy}
|
value={config.lateSubmissionPolicy ?? 'flag'}
|
||||||
onValueChange={(value) =>
|
onValueChange={(value) =>
|
||||||
updateConfig({
|
updateConfig({
|
||||||
lateSubmissionPolicy: value as IntakeConfig['lateSubmissionPolicy'],
|
lateSubmissionPolicy: value as IntakeConfig['lateSubmissionPolicy'],
|
||||||
|
|
@ -104,14 +106,14 @@ export function IntakeSection({ config, onChange, isActive }: IntakeSectionProps
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{config.lateSubmissionPolicy === 'flag' && (
|
{(config.lateSubmissionPolicy ?? 'flag') === 'flag' && (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label>Grace Period (hours)</Label>
|
<Label>Grace Period (hours)</Label>
|
||||||
<Input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
min={0}
|
min={0}
|
||||||
max={168}
|
max={168}
|
||||||
value={config.lateGraceHours}
|
value={config.lateGraceHours ?? 24}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
updateConfig({ lateGraceHours: parseInt(e.target.value) || 0 })
|
updateConfig({ lateGraceHours: parseInt(e.target.value) || 0 })
|
||||||
}
|
}
|
||||||
|
|
@ -130,13 +132,13 @@ export function IntakeSection({ config, onChange, isActive }: IntakeSectionProps
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{config.fileRequirements.length === 0 && (
|
{fileRequirements.length === 0 && (
|
||||||
<p className="text-sm text-muted-foreground py-4 text-center">
|
<p className="text-sm text-muted-foreground py-4 text-center">
|
||||||
No file requirements configured. Projects can be submitted without files.
|
No file requirements configured. Projects can be submitted without files.
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{config.fileRequirements.map((req, index) => (
|
{fileRequirements.map((req, index) => (
|
||||||
<Card key={index}>
|
<Card key={index}>
|
||||||
<CardContent className="pt-4 space-y-3">
|
<CardContent className="pt-4 space-y-3">
|
||||||
<div className="flex items-start gap-3">
|
<div className="flex items-start gap-3">
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ export function LiveFinalsSection({ config, onChange, isActive }: LiveFinalsSect
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Switch
|
<Switch
|
||||||
checked={config.juryVotingEnabled}
|
checked={config.juryVotingEnabled ?? true}
|
||||||
onCheckedChange={(checked) =>
|
onCheckedChange={(checked) =>
|
||||||
updateConfig({ juryVotingEnabled: checked })
|
updateConfig({ juryVotingEnabled: checked })
|
||||||
}
|
}
|
||||||
|
|
@ -50,7 +50,7 @@ export function LiveFinalsSection({ config, onChange, isActive }: LiveFinalsSect
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Switch
|
<Switch
|
||||||
checked={config.audienceVotingEnabled}
|
checked={config.audienceVotingEnabled ?? false}
|
||||||
onCheckedChange={(checked) =>
|
onCheckedChange={(checked) =>
|
||||||
updateConfig({ audienceVotingEnabled: checked })
|
updateConfig({ audienceVotingEnabled: checked })
|
||||||
}
|
}
|
||||||
|
|
@ -58,13 +58,13 @@ export function LiveFinalsSection({ config, onChange, isActive }: LiveFinalsSect
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{config.audienceVotingEnabled && (
|
{(config.audienceVotingEnabled ?? false) && (
|
||||||
<div className="pl-4 border-l-2 border-muted space-y-3">
|
<div className="pl-4 border-l-2 border-muted space-y-3">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label className="text-xs">Audience Vote Weight</Label>
|
<Label className="text-xs">Audience Vote Weight</Label>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<Slider
|
<Slider
|
||||||
value={[config.audienceVoteWeight * 100]}
|
value={[(config.audienceVoteWeight ?? 0) * 100]}
|
||||||
onValueChange={([v]) =>
|
onValueChange={([v]) =>
|
||||||
updateConfig({ audienceVoteWeight: v / 100 })
|
updateConfig({ audienceVoteWeight: v / 100 })
|
||||||
}
|
}
|
||||||
|
|
@ -74,7 +74,7 @@ export function LiveFinalsSection({ config, onChange, isActive }: LiveFinalsSect
|
||||||
className="flex-1"
|
className="flex-1"
|
||||||
/>
|
/>
|
||||||
<span className="text-xs font-mono w-10 text-right">
|
<span className="text-xs font-mono w-10 text-right">
|
||||||
{Math.round(config.audienceVoteWeight * 100)}%
|
{Math.round((config.audienceVoteWeight ?? 0) * 100)}%
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
|
|
@ -88,7 +88,7 @@ export function LiveFinalsSection({ config, onChange, isActive }: LiveFinalsSect
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label>Cohort Setup Mode</Label>
|
<Label>Cohort Setup Mode</Label>
|
||||||
<Select
|
<Select
|
||||||
value={config.cohortSetupMode}
|
value={config.cohortSetupMode ?? 'manual'}
|
||||||
onValueChange={(value) =>
|
onValueChange={(value) =>
|
||||||
updateConfig({
|
updateConfig({
|
||||||
cohortSetupMode: value as LiveFinalConfig['cohortSetupMode'],
|
cohortSetupMode: value as LiveFinalConfig['cohortSetupMode'],
|
||||||
|
|
@ -113,7 +113,7 @@ export function LiveFinalsSection({ config, onChange, isActive }: LiveFinalsSect
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label>Result Reveal Policy</Label>
|
<Label>Result Reveal Policy</Label>
|
||||||
<Select
|
<Select
|
||||||
value={config.revealPolicy}
|
value={config.revealPolicy ?? 'ceremony'}
|
||||||
onValueChange={(value) =>
|
onValueChange={(value) =>
|
||||||
updateConfig({
|
updateConfig({
|
||||||
revealPolicy: value as LiveFinalConfig['revealPolicy'],
|
revealPolicy: value as LiveFinalConfig['revealPolicy'],
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue