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 <noreply@anthropic.com>
This commit is contained in:
Kyle 2026-02-22 01:00:16 +08:00
parent d811890509
commit 232bdd3ef2
2 changed files with 39 additions and 6 deletions

View File

@ -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

View File

@ -22,7 +22,7 @@ const Sheet: React.FC<SheetProps> = ({ 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<SheetProps> = ({ open, onOpenChange, children }) => {
<div className="fixed inset-0 z-50">
<div
className={cn(
'fixed inset-0 bg-background/80 backdrop-blur-sm transition-opacity duration-250',
'fixed inset-0 bg-background/80 backdrop-blur-sm transition-opacity duration-350 ease-out',
visible ? 'opacity-100' : 'opacity-0'
)}
onClick={() => onOpenChange(false)}
/>
<div
className={cn(
'fixed right-0 top-0 h-full w-full max-w-[540px] transition-transform duration-250 ease-out',
'fixed right-0 top-0 h-full w-full max-w-[540px] transition-transform duration-350',
visible ? 'translate-x-0' : 'translate-x-full'
)}
style={{ transitionTimingFunction: 'cubic-bezier(0.16, 1, 0.3, 1)' }}
>
{children}
</div>