Fix advancement targets stripped by Zod, remove redundant save bar
All checks were successful
Build and Push Docker Image / build (push) Successful in 9m32s
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:
@@ -2006,40 +2006,26 @@ export default function RoundDetailPage() {
|
|||||||
)}
|
)}
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
{/* Floating save bar — appears when config has unsaved changes */}
|
{/* Autosave error bar — only shows when save fails */}
|
||||||
{hasUnsavedConfig && (
|
{autosaveStatus === 'error' && (
|
||||||
<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="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">
|
<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="flex items-center gap-2 text-sm text-red-700 dark:text-red-300">
|
||||||
<div className="h-2 w-2 rounded-full bg-amber-500 animate-pulse" />
|
<AlertTriangle className="h-4 w-4" />
|
||||||
<span className="text-muted-foreground">You have unsaved changes</span>
|
<span>Auto-save failed</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<Button
|
||||||
{autosaveStatus === 'error' && (
|
size="sm"
|
||||||
<span className="text-xs text-red-500 mr-2">Save failed — try again</span>
|
onClick={saveConfig}
|
||||||
|
disabled={updateMutation.isPending}
|
||||||
|
className="bg-[#de0f1e] hover:bg-[#c00d1a] text-white"
|
||||||
|
>
|
||||||
|
{updateMutation.isPending ? (
|
||||||
|
<><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" />Retry Save</>
|
||||||
)}
|
)}
|
||||||
<Button
|
</Button>
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => {
|
|
||||||
setConfig(serverConfig)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Discard
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
onClick={saveConfig}
|
|
||||||
disabled={updateMutation.isPending}
|
|
||||||
className="bg-[#de0f1e] hover:bg-[#c00d1a] text-white"
|
|
||||||
>
|
|
||||||
{updateMutation.isPending ? (
|
|
||||||
<><Loader2 className="h-3.5 w-3.5 mr-1.5 animate-spin" />Saving...</>
|
|
||||||
) : (
|
|
||||||
<><Save className="h-3.5 w-3.5 mr-1.5" />Save Changes</>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user