fix: handle malformed event dates in calendar route #316
This commit is contained in:
@@ -78,7 +78,11 @@ export function EventCard({ event, calendar, variant, onClick, onMouseEnter, onM
|
|||||||
const calendarName = calendar?.name || "";
|
const calendarName = calendar?.name || "";
|
||||||
const durationMinutes = parseDuration(event.duration);
|
const durationMinutes = parseDuration(event.duration);
|
||||||
const endTime = getEventEndDate(event);
|
const endTime = getEventEndDate(event);
|
||||||
const timeString = `${format(startDate, timeFmt)} – ${format(endTime, timeFmt)}`;
|
const safeFormat = (d: Date, fmt: string) => {
|
||||||
|
if (isNaN(d.getTime())) return "--:--";
|
||||||
|
try { return format(d, fmt); } catch { return "--:--"; }
|
||||||
|
};
|
||||||
|
const timeString = `${safeFormat(startDate, timeFmt)} – ${safeFormat(endTime, timeFmt)}`;
|
||||||
const ariaLabel = `${event.title || t("events.no_title")}, ${timeString}${calendarName ? `, ${calendarName}` : ""}`;
|
const ariaLabel = `${event.title || t("events.no_title")}, ${timeString}${calendarName ? `, ${calendarName}` : ""}`;
|
||||||
|
|
||||||
const handleDragStart = useCallback((e: DragEvent) => {
|
const handleDragStart = useCallback((e: DragEvent) => {
|
||||||
|
|||||||
+10
-3
@@ -24,8 +24,14 @@ export interface TimedEventLayout {
|
|||||||
export function getEventStartDate(
|
export function getEventStartDate(
|
||||||
event: Pick<CalendarEvent, 'start' | 'utcStart' | 'showWithoutTime'>,
|
event: Pick<CalendarEvent, 'start' | 'utcStart' | 'showWithoutTime'>,
|
||||||
): Date {
|
): Date {
|
||||||
const source = !event.showWithoutTime && event.utcStart ? event.utcStart : event.start;
|
// Prefer utcStart for timed events but fall back to start if utcStart is
|
||||||
return parseISO(source);
|
// missing or unparseable - a malformed utcStart used to surface as an
|
||||||
|
// Invalid Date that crashed downstream format() calls (#316).
|
||||||
|
if (!event.showWithoutTime && event.utcStart) {
|
||||||
|
const utc = parseISO(event.utcStart);
|
||||||
|
if (!isNaN(utc.getTime())) return utc;
|
||||||
|
}
|
||||||
|
return parseISO(event.start);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function packWeekSegments(rawSegments: CalendarWeekSegment[]): CalendarWeekSegment[] {
|
export function packWeekSegments(rawSegments: CalendarWeekSegment[]): CalendarWeekSegment[] {
|
||||||
@@ -56,7 +62,8 @@ export function packWeekSegments(rawSegments: CalendarWeekSegment[]): CalendarWe
|
|||||||
|
|
||||||
export function getEventEndDate(event: CalendarEvent): Date {
|
export function getEventEndDate(event: CalendarEvent): Date {
|
||||||
if (!event.showWithoutTime && event.utcEnd) {
|
if (!event.showWithoutTime && event.utcEnd) {
|
||||||
return parseISO(event.utcEnd);
|
const utc = parseISO(event.utcEnd);
|
||||||
|
if (!isNaN(utc.getTime())) return utc;
|
||||||
}
|
}
|
||||||
|
|
||||||
const start = getEventStartDate(event);
|
const start = getEventStartDate(event);
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { normalizeAllDayDuration } from '@/lib/calendar-utils';
|
|||||||
import { parseDuration } from '@/components/calendar/event-card';
|
import { parseDuration } from '@/components/calendar/event-card';
|
||||||
import { sanitizeOutgoingCalendarEventData } from '@/lib/calendar-event-normalization';
|
import { sanitizeOutgoingCalendarEventData } from '@/lib/calendar-event-normalization';
|
||||||
import { expandRecurringEvents } from '@/lib/recurrence-expansion';
|
import { expandRecurringEvents } from '@/lib/recurrence-expansion';
|
||||||
|
import { parseISO } from 'date-fns';
|
||||||
import { generateUUID } from '@/lib/utils';
|
import { generateUUID } from '@/lib/utils';
|
||||||
import { apiFetch } from '@/lib/browser-navigation';
|
import { apiFetch } from '@/lib/browser-navigation';
|
||||||
import { BIRTHDAY_CALENDAR_ID } from '@/lib/birthday-calendar';
|
import { BIRTHDAY_CALENDAR_ID } from '@/lib/birthday-calendar';
|
||||||
@@ -302,8 +303,12 @@ export const useCalendarStore = create<CalendarStore>()(
|
|||||||
after: start,
|
after: start,
|
||||||
before: end,
|
before: end,
|
||||||
});
|
});
|
||||||
// Filter out malformed events missing required 'start' field
|
// Filter out malformed events missing required 'start' field, or
|
||||||
const validEvents = rawEvents.filter(e => typeof e.start === 'string' && e.start);
|
// whose start string fails to parse (would otherwise crash format()
|
||||||
|
// calls in the rendering path - #316).
|
||||||
|
const validEvents = rawEvents.filter(e =>
|
||||||
|
typeof e.start === 'string' && e.start && !isNaN(parseISO(e.start).getTime())
|
||||||
|
);
|
||||||
const droppedEvents = rawEvents.length - validEvents.length;
|
const droppedEvents = rawEvents.length - validEvents.length;
|
||||||
// Expand recurring events client-side (Stalwart doesn't support
|
// Expand recurring events client-side (Stalwart doesn't support
|
||||||
// mutations on synthetic IDs from server-side expandRecurrences)
|
// mutations on synthetic IDs from server-side expandRecurrences)
|
||||||
@@ -366,7 +371,9 @@ export const useCalendarStore = create<CalendarStore>()(
|
|||||||
accounts.map(async ({ client, localAccountId }) => {
|
accounts.map(async ({ client, localAccountId }) => {
|
||||||
try {
|
try {
|
||||||
const raw = await client.queryAllCalendarEvents({ after: start, before: end });
|
const raw = await client.queryAllCalendarEvents({ after: start, before: end });
|
||||||
const valid = raw.filter(e => typeof e.start === 'string' && e.start);
|
const valid = raw.filter(e =>
|
||||||
|
typeof e.start === 'string' && e.start && !isNaN(parseISO(e.start).getTime())
|
||||||
|
);
|
||||||
const expanded = expandRecurringEvents(valid, start, end);
|
const expanded = expandRecurringEvents(valid, start, end);
|
||||||
return prefixEventsWithLocalAccount(
|
return prefixEventsWithLocalAccount(
|
||||||
expanded,
|
expanded,
|
||||||
|
|||||||
Reference in New Issue
Block a user