From 232bdd3ef26ef1a965f7129014dedabb56d19ba9 Mon Sep 17 00:00:00 2001 From: Kyle Pope Date: Sun, 22 Feb 2026 01:00:16 +0800 Subject: [PATCH] Fix recurrence_rule validation + smoother Sheet animation - Add field_validator to coerce recurrence_rule from legacy strings, empty strings, and JSON strings into RecurrenceRule or None - Increase Sheet slide-in duration to 350ms with cubic-bezier(0.16, 1, 0.3, 1) for a more premium feel Co-Authored-By: Claude Opus 4.6 --- backend/app/schemas/calendar_event.py | 38 ++++++++++++++++++++++++--- frontend/src/components/ui/sheet.tsx | 7 ++--- 2 files changed, 39 insertions(+), 6 deletions(-) diff --git a/backend/app/schemas/calendar_event.py b/backend/app/schemas/calendar_event.py index 038cbc0..c219539 100644 --- a/backend/app/schemas/calendar_event.py +++ b/backend/app/schemas/calendar_event.py @@ -1,4 +1,6 @@ -from pydantic import BaseModel, ConfigDict +import json as _json + +from pydantic import BaseModel, ConfigDict, field_validator from datetime import datetime from typing import Literal, Optional @@ -16,6 +18,26 @@ class RecurrenceRule(BaseModel): day: Optional[int] = None # 1-31 +def _coerce_recurrence_rule(v): + """Accept None, dict, RecurrenceRule, or JSON/legacy strings gracefully.""" + if v is None or v == "" or v == "null": + return None + if isinstance(v, dict): + return v + if isinstance(v, RecurrenceRule): + return v + if isinstance(v, str): + try: + parsed = _json.loads(v) + if isinstance(parsed, dict): + return parsed + except (_json.JSONDecodeError, TypeError): + pass + # Legacy simple strings like "daily", "weekly" — discard (not structured) + return None + return v + + class CalendarEventCreate(BaseModel): title: str description: Optional[str] = None @@ -24,10 +46,15 @@ class CalendarEventCreate(BaseModel): all_day: bool = False color: Optional[str] = None location_id: Optional[int] = None - recurrence_rule: Optional[RecurrenceRule] = None # structured; router serializes to JSON string + recurrence_rule: Optional[RecurrenceRule] = None is_starred: bool = False calendar_id: Optional[int] = None # If None, server assigns default calendar + @field_validator("recurrence_rule", mode="before") + @classmethod + def coerce_recurrence(cls, v): + return _coerce_recurrence_rule(v) + class CalendarEventUpdate(BaseModel): title: Optional[str] = None @@ -37,12 +64,17 @@ class CalendarEventUpdate(BaseModel): all_day: Optional[bool] = None color: Optional[str] = None location_id: Optional[int] = None - recurrence_rule: Optional[RecurrenceRule] = None # structured; router serializes to JSON string + recurrence_rule: Optional[RecurrenceRule] = None is_starred: Optional[bool] = None calendar_id: Optional[int] = None # Controls which occurrences an edit applies to; absent = non-recurring or whole-series edit_scope: Optional[Literal["this", "this_and_future"]] = None + @field_validator("recurrence_rule", mode="before") + @classmethod + def coerce_recurrence(cls, v): + return _coerce_recurrence_rule(v) + class CalendarEventResponse(BaseModel): id: int diff --git a/frontend/src/components/ui/sheet.tsx b/frontend/src/components/ui/sheet.tsx index 69a6fbe..f6e3fa3 100644 --- a/frontend/src/components/ui/sheet.tsx +++ b/frontend/src/components/ui/sheet.tsx @@ -22,7 +22,7 @@ const Sheet: React.FC = ({ open, onOpenChange, children }) => { document.body.style.overflow = 'hidden'; } else { setVisible(false); - const timer = setTimeout(() => setMounted(false), 250); + const timer = setTimeout(() => setMounted(false), 350); document.body.style.overflow = ''; return () => clearTimeout(timer); } @@ -44,16 +44,17 @@ const Sheet: React.FC = ({ open, onOpenChange, children }) => {
onOpenChange(false)} />
{children}