Fix advancement targets stripped by Zod, remove redundant save bar
All checks were successful
Build and Push Docker Image / build (push) Successful in 9m32s

- Add general settings fields (startupAdvanceCount, conceptAdvanceCount,
  notifyOnEntry, notifyOnAdvance) to ALL round config schemas, not just
  FilteringConfig. Zod was stripping them on save for other round types.
- Replace floating save bar with error-only bar since autosave handles
  all config persistence (800ms debounce)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Matt
2026-02-18 14:59:23 +01:00
parent 9c19661400
commit cab311fbbb
2 changed files with 32 additions and 35 deletions

View File

@@ -2006,27 +2006,14 @@ export default function RoundDetailPage() {
)} )}
</Tabs> </Tabs>
{/* Floating save bar — appears when config has unsaved changes */} {/* Autosave error bar — only shows when save fails */}
{hasUnsavedConfig && (
<div className="fixed bottom-0 left-0 right-0 z-50 border-t bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/80 shadow-[0_-4px_12px_rgba(0,0,0,0.1)]">
<div className="container flex items-center justify-between py-3 px-4 max-w-5xl mx-auto">
<div className="flex items-center gap-2 text-sm">
<div className="h-2 w-2 rounded-full bg-amber-500 animate-pulse" />
<span className="text-muted-foreground">You have unsaved changes</span>
</div>
<div className="flex items-center gap-2">
{autosaveStatus === 'error' && ( {autosaveStatus === 'error' && (
<span className="text-xs text-red-500 mr-2">Save failed try again</span> <div className="fixed bottom-0 left-0 right-0 z-50 border-t bg-red-50 dark:bg-red-950/50 shadow-[0_-4px_12px_rgba(0,0,0,0.1)]">
)} <div className="container flex items-center justify-between py-3 px-4 max-w-5xl mx-auto">
<Button <div className="flex items-center gap-2 text-sm text-red-700 dark:text-red-300">
size="sm" <AlertTriangle className="h-4 w-4" />
variant="outline" <span>Auto-save failed</span>
onClick={() => { </div>
setConfig(serverConfig)
}}
>
Discard
</Button>
<Button <Button
size="sm" size="sm"
onClick={saveConfig} onClick={saveConfig}
@@ -2034,14 +2021,13 @@ export default function RoundDetailPage() {
className="bg-[#de0f1e] hover:bg-[#c00d1a] text-white" className="bg-[#de0f1e] hover:bg-[#c00d1a] text-white"
> >
{updateMutation.isPending ? ( {updateMutation.isPending ? (
<><Loader2 className="h-3.5 w-3.5 mr-1.5 animate-spin" />Saving...</> <><Loader2 className="h-3.5 w-3.5 mr-1.5 animate-spin" />Retrying...</>
) : ( ) : (
<><Save className="h-3.5 w-3.5 mr-1.5" />Save Changes</> <><Save className="h-3.5 w-3.5 mr-1.5" />Retry Save</>
)} )}
</Button> </Button>
</div> </div>
</div> </div>
</div>
)} )}
</div> </div>
) )

View File

@@ -8,9 +8,18 @@ import type { RoundType, AwardEligibilityMode, AwardScoringMode, AwardStatus } f
// These replace the loosely-typed pipeline-wizard.ts configs with // These replace the loosely-typed pipeline-wizard.ts configs with
// Zod-validated, compile-time-safe contracts. // Zod-validated, compile-time-safe contracts.
// Shared fields that appear in "General Settings" for all round types
const generalSettingsFields = {
startupAdvanceCount: z.number().int().nonnegative().optional(),
conceptAdvanceCount: z.number().int().nonnegative().optional(),
notifyOnEntry: z.boolean().default(false),
notifyOnAdvance: z.boolean().default(false),
}
// ─── 1. IntakeConfig ───────────────────────────────────────────────────────── // ─── 1. IntakeConfig ─────────────────────────────────────────────────────────
export const IntakeConfigSchema = z.object({ export const IntakeConfigSchema = z.object({
...generalSettingsFields,
allowDrafts: z.boolean().default(true), allowDrafts: z.boolean().default(true),
draftExpiryDays: z.number().int().positive().default(30), draftExpiryDays: z.number().int().positive().default(30),
@@ -43,6 +52,7 @@ export type IntakeConfig = z.infer<typeof IntakeConfigSchema>
// ─── 2. FilteringConfig ────────────────────────────────────────────────────── // ─── 2. FilteringConfig ──────────────────────────────────────────────────────
export const FilteringConfigSchema = z.object({ export const FilteringConfigSchema = z.object({
...generalSettingsFields,
rules: z rules: z
.array( .array(
z.object({ z.object({
@@ -71,10 +81,6 @@ export const FilteringConfigSchema = z.object({
batchSize: z.number().int().positive().default(20), batchSize: z.number().int().positive().default(20),
aiParseFiles: z.boolean().default(false), aiParseFiles: z.boolean().default(false),
startupAdvanceCount: z.number().int().nonnegative().optional(),
conceptAdvanceCount: z.number().int().nonnegative().optional(),
notifyOnEntry: z.boolean().default(false),
notifyOnAdvance: z.boolean().default(false),
}) })
export type FilteringConfig = z.infer<typeof FilteringConfigSchema> export type FilteringConfig = z.infer<typeof FilteringConfigSchema>
@@ -82,6 +88,7 @@ export type FilteringConfig = z.infer<typeof FilteringConfigSchema>
// ─── 3. EvaluationConfig ───────────────────────────────────────────────────── // ─── 3. EvaluationConfig ─────────────────────────────────────────────────────
export const EvaluationConfigSchema = z.object({ export const EvaluationConfigSchema = z.object({
...generalSettingsFields,
requiredReviewsPerProject: z.number().int().positive().default(3), requiredReviewsPerProject: z.number().int().positive().default(3),
scoringMode: z.enum(['criteria', 'global', 'binary']).default('criteria'), scoringMode: z.enum(['criteria', 'global', 'binary']).default('criteria'),
@@ -121,6 +128,7 @@ export type EvaluationConfig = z.infer<typeof EvaluationConfigSchema>
// ─── 4. SubmissionConfig ───────────────────────────────────────────────────── // ─── 4. SubmissionConfig ─────────────────────────────────────────────────────
export const SubmissionConfigSchema = z.object({ export const SubmissionConfigSchema = z.object({
...generalSettingsFields,
eligibleStatuses: z eligibleStatuses: z
.array( .array(
z.enum([ z.enum([
@@ -143,6 +151,7 @@ export type SubmissionConfig = z.infer<typeof SubmissionConfigSchema>
// ─── 5. MentoringConfig ────────────────────────────────────────────────────── // ─── 5. MentoringConfig ──────────────────────────────────────────────────────
export const MentoringConfigSchema = z.object({ export const MentoringConfigSchema = z.object({
...generalSettingsFields,
eligibility: z eligibility: z
.enum(['all_advancing', 'requested_only', 'admin_selected']) .enum(['all_advancing', 'requested_only', 'admin_selected'])
.default('requested_only'), .default('requested_only'),
@@ -161,6 +170,7 @@ export type MentoringConfig = z.infer<typeof MentoringConfigSchema>
// ─── 6. LiveFinalConfig ────────────────────────────────────────────────────── // ─── 6. LiveFinalConfig ──────────────────────────────────────────────────────
export const LiveFinalConfigSchema = z.object({ export const LiveFinalConfigSchema = z.object({
...generalSettingsFields,
juryVotingEnabled: z.boolean().default(true), juryVotingEnabled: z.boolean().default(true),
votingMode: z.enum(['simple', 'criteria']).default('simple'), votingMode: z.enum(['simple', 'criteria']).default('simple'),
@@ -193,6 +203,7 @@ export type LiveFinalConfig = z.infer<typeof LiveFinalConfigSchema>
// ─── 7. DeliberationConfig ─────────────────────────────────────────────────── // ─── 7. DeliberationConfig ───────────────────────────────────────────────────
export const DeliberationConfigSchema = z.object({ export const DeliberationConfigSchema = z.object({
...generalSettingsFields,
juryGroupId: z.string(), juryGroupId: z.string(),
mode: z mode: z