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:
parent
d811890509
commit
232bdd3ef2
@ -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
|
||||
|
||||
@ -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>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user