feat: drag-to-move and resize handles for calendar events
Add resize handles (top/bottom) on hover to change event duration, and drag-to-move support with live preview and 5-minute snapping. Includes 3px dead zone to distinguish clicks from drags, optimistic UI updates with server rollback on failure, and recurring/provider icons on time-grid events. Also fixes Google delete 410 Gone and Outlook delete 404/encodeURIComponent issues. v1.66.0 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
c040dd982d
commit
13de53fe0a
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "my-weekly-todo-list",
|
"name": "my-weekly-todo-list",
|
||||||
"version": "1.65.1",
|
"version": "1.66.0",
|
||||||
"description": "A web-based weekly task management application that organizes to-dos and calendar events in a single, intuitive weekly view",
|
"description": "A web-based weekly task management application that organizes to-dos and calendar events in a single, intuitive weekly view",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@ -3059,6 +3059,47 @@ h3 {
|
|||||||
background: rgba(0, 154, 154, 0.15);
|
background: rgba(0, 154, 154, 0.15);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Event resize handles */
|
||||||
|
.event-resize-handle {
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
width: 20px;
|
||||||
|
height: 8px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: ns-resize;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.15s;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-resize-handle::after {
|
||||||
|
content: '';
|
||||||
|
width: 10px;
|
||||||
|
height: 4px;
|
||||||
|
border-radius: 2px;
|
||||||
|
background: currentColor;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-resize-handle-top {
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-resize-handle-bottom {
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-slot-event:hover .event-resize-handle {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-slot-event.event-dragging {
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
/* Make day columns scrollable when using time grid */
|
/* Make day columns scrollable when using time grid */
|
||||||
.time-grid-wrapper .weekly-days-grid {
|
.time-grid-wrapper .weekly-days-grid {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
|||||||
@ -2202,6 +2202,20 @@ export default function WeeklyView() {
|
|||||||
initialStartTime?: string;
|
initialStartTime?: string;
|
||||||
}>({ isOpen: false });
|
}>({ isOpen: false });
|
||||||
|
|
||||||
|
// Calendar event resize/drag state
|
||||||
|
const [eventDragState, setEventDragState] = useState<{
|
||||||
|
eventId: string;
|
||||||
|
mode: 'move' | 'resize-top' | 'resize-bottom';
|
||||||
|
startY: number;
|
||||||
|
originalStartTime: string;
|
||||||
|
originalEndTime: string;
|
||||||
|
currentStartTime: string;
|
||||||
|
currentEndTime: string;
|
||||||
|
calendarId?: string;
|
||||||
|
source?: string;
|
||||||
|
hasMoved?: boolean;
|
||||||
|
} | null>(null);
|
||||||
|
|
||||||
// Dark Mode Persistence & Class Toggle
|
// Dark Mode Persistence & Class Toggle
|
||||||
const [mounted, setMounted] = useState(false);
|
const [mounted, setMounted] = useState(false);
|
||||||
|
|
||||||
@ -2232,6 +2246,7 @@ export default function WeeklyView() {
|
|||||||
|
|
||||||
const profileLoadedRef = useRef(false);
|
const profileLoadedRef = useRef(false);
|
||||||
const autoSaveTimerRef = useRef<NodeJS.Timeout | null>(null);
|
const autoSaveTimerRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
const dragJustEndedRef = useRef(false);
|
||||||
|
|
||||||
const fetchProfile = async () => {
|
const fetchProfile = async () => {
|
||||||
try {
|
try {
|
||||||
@ -3686,6 +3701,114 @@ export default function WeeklyView() {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Snap minutes to nearest 5-minute increment
|
||||||
|
const snapMinutes = (mins: number) => Math.round(mins / 5) * 5;
|
||||||
|
|
||||||
|
// Handle calendar event drag/resize
|
||||||
|
const handleEventDragStart = (e: React.MouseEvent, event: CalendarEvent, mode: 'move' | 'resize-top' | 'resize-bottom') => {
|
||||||
|
if (!event.editable) return;
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
setEventDragState({
|
||||||
|
eventId: event.id,
|
||||||
|
mode,
|
||||||
|
startY: e.clientY,
|
||||||
|
originalStartTime: event.startTime,
|
||||||
|
originalEndTime: event.endTime,
|
||||||
|
currentStartTime: event.startTime,
|
||||||
|
currentEndTime: event.endTime,
|
||||||
|
calendarId: event.calendarId,
|
||||||
|
source: event.source,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!eventDragState) return;
|
||||||
|
const pixelsPerMinute = getSlotHeight(effectiveCellDuration) / effectiveCellDuration;
|
||||||
|
|
||||||
|
const handleMouseMove = (e: MouseEvent) => {
|
||||||
|
const deltaY = e.clientY - eventDragState.startY;
|
||||||
|
// Require minimum 3px movement to start actual drag
|
||||||
|
if (!eventDragState.hasMoved && Math.abs(deltaY) < 3) return;
|
||||||
|
if (!eventDragState.hasMoved) {
|
||||||
|
setEventDragState(prev => prev ? { ...prev, hasMoved: true } : null);
|
||||||
|
}
|
||||||
|
const deltaMinutes = snapMinutes(deltaY / pixelsPerMinute);
|
||||||
|
const origStart = new Date(eventDragState.originalStartTime);
|
||||||
|
const origEnd = new Date(eventDragState.originalEndTime);
|
||||||
|
|
||||||
|
if (eventDragState.mode === 'move') {
|
||||||
|
const newStart = new Date(origStart.getTime() + deltaMinutes * 60000);
|
||||||
|
const newEnd = new Date(origEnd.getTime() + deltaMinutes * 60000);
|
||||||
|
setEventDragState(prev => prev ? { ...prev, currentStartTime: newStart.toISOString(), currentEndTime: newEnd.toISOString() } : null);
|
||||||
|
} else if (eventDragState.mode === 'resize-bottom') {
|
||||||
|
const newEnd = new Date(origEnd.getTime() + deltaMinutes * 60000);
|
||||||
|
if (newEnd.getTime() > origStart.getTime() + 5 * 60000) {
|
||||||
|
setEventDragState(prev => prev ? { ...prev, currentEndTime: newEnd.toISOString() } : null);
|
||||||
|
}
|
||||||
|
} else if (eventDragState.mode === 'resize-top') {
|
||||||
|
const newStart = new Date(origStart.getTime() + deltaMinutes * 60000);
|
||||||
|
if (newStart.getTime() < origEnd.getTime() - 5 * 60000) {
|
||||||
|
setEventDragState(prev => prev ? { ...prev, currentStartTime: newStart.toISOString() } : null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseUp = async () => {
|
||||||
|
if (!eventDragState) return;
|
||||||
|
const startChanged = eventDragState.currentStartTime !== eventDragState.originalStartTime;
|
||||||
|
const endChanged = eventDragState.currentEndTime !== eventDragState.originalEndTime;
|
||||||
|
if (eventDragState.hasMoved && (startChanged || endChanged)) {
|
||||||
|
// Optimistically update the UI
|
||||||
|
setRawCalendarEvents(prev => prev.map(ev =>
|
||||||
|
ev.id === eventDragState.eventId
|
||||||
|
? { ...ev, startTime: eventDragState.currentStartTime, endTime: eventDragState.currentEndTime }
|
||||||
|
: ev
|
||||||
|
));
|
||||||
|
// Save to server
|
||||||
|
try {
|
||||||
|
const res = await fetch("/api/calendar/events", {
|
||||||
|
method: "PATCH",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
calendarId: eventDragState.calendarId,
|
||||||
|
eventId: eventDragState.eventId,
|
||||||
|
start: { dateTime: eventDragState.currentStartTime },
|
||||||
|
end: { dateTime: eventDragState.currentEndTime },
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
// Revert on failure
|
||||||
|
setRawCalendarEvents(prev => prev.map(ev =>
|
||||||
|
ev.id === eventDragState.eventId
|
||||||
|
? { ...ev, startTime: eventDragState.originalStartTime, endTime: eventDragState.originalEndTime }
|
||||||
|
: ev
|
||||||
|
));
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
setRawCalendarEvents(prev => prev.map(ev =>
|
||||||
|
ev.id === eventDragState.eventId
|
||||||
|
? { ...ev, startTime: eventDragState.originalStartTime, endTime: eventDragState.originalEndTime }
|
||||||
|
: ev
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const didMove = eventDragState.hasMoved;
|
||||||
|
setEventDragState(null);
|
||||||
|
if (didMove) {
|
||||||
|
dragJustEndedRef.current = true;
|
||||||
|
setTimeout(() => { dragJustEndedRef.current = false; }, 200);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('mousemove', handleMouseMove);
|
||||||
|
document.addEventListener('mouseup', handleMouseUp);
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('mousemove', handleMouseMove);
|
||||||
|
document.removeEventListener('mouseup', handleMouseUp);
|
||||||
|
};
|
||||||
|
}, [eventDragState, effectiveCellDuration]);
|
||||||
|
|
||||||
// Get all-day events for a specific date
|
// Get all-day events for a specific date
|
||||||
const getAllDayEventsForDate = useCallback(
|
const getAllDayEventsForDate = useCallback(
|
||||||
(date: Date): CalendarEvent[] => {
|
(date: Date): CalendarEvent[] => {
|
||||||
@ -7540,10 +7663,15 @@ export default function WeeklyView() {
|
|||||||
)}
|
)}
|
||||||
{/* Calendar Events in time slot */}
|
{/* Calendar Events in time slot */}
|
||||||
{slotEvents.map((event) => {
|
{slotEvents.map((event) => {
|
||||||
const eventHeight = getEventDuration(event);
|
// During drag, use the drag state's current times for position/height
|
||||||
const startTime = new Date(event.startTime);
|
const isDragging = eventDragState?.eventId === event.id && eventDragState?.hasMoved;
|
||||||
const endTime = new Date(event.endTime);
|
const displayStartTime = isDragging ? new Date(eventDragState!.currentStartTime) : new Date(event.startTime);
|
||||||
const timeStr = `${startTime.getHours().toString().padStart(2, "0")}:${startTime.getMinutes().toString().padStart(2, "0")} - ${endTime.getHours().toString().padStart(2, "0")}:${endTime.getMinutes().toString().padStart(2, "0")}`;
|
const displayEndTime = isDragging ? new Date(eventDragState!.currentEndTime) : new Date(event.endTime);
|
||||||
|
const durationMs = displayEndTime.getTime() - displayStartTime.getTime();
|
||||||
|
const durationMinutes = durationMs / (1000 * 60);
|
||||||
|
const pxPerMin = getSlotHeight(effectiveCellDuration) / effectiveCellDuration;
|
||||||
|
const eventHeight = Math.max(durationMinutes * pxPerMin, 15);
|
||||||
|
const timeStr = `${displayStartTime.getHours().toString().padStart(2, "0")}:${displayStartTime.getMinutes().toString().padStart(2, "0")} - ${displayEndTime.getHours().toString().padStart(2, "0")}:${displayEndTime.getMinutes().toString().padStart(2, "0")}`;
|
||||||
|
|
||||||
// Calculate offset within the slot based on event start time
|
// Calculate offset within the slot based on event start time
|
||||||
const [slotHour, slotMinute] = slot
|
const [slotHour, slotMinute] = slot
|
||||||
@ -7551,13 +7679,11 @@ export default function WeeklyView() {
|
|||||||
.map(Number);
|
.map(Number);
|
||||||
const slotStartMinutes = slotHour * 60 + slotMinute;
|
const slotStartMinutes = slotHour * 60 + slotMinute;
|
||||||
const eventStartMinutes =
|
const eventStartMinutes =
|
||||||
startTime.getHours() * 60 +
|
displayStartTime.getHours() * 60 +
|
||||||
startTime.getMinutes();
|
displayStartTime.getMinutes();
|
||||||
const offsetMinutes =
|
const offsetMinutes =
|
||||||
eventStartMinutes - slotStartMinutes;
|
eventStartMinutes - slotStartMinutes;
|
||||||
const pixelsPerMinute =
|
const topOffset = offsetMinutes * pxPerMin;
|
||||||
getSlotHeight(effectiveCellDuration) / effectiveCellDuration;
|
|
||||||
const topOffset = offsetMinutes * pixelsPerMinute;
|
|
||||||
|
|
||||||
// Convert hex to rgba for background, or use default
|
// Convert hex to rgba for background, or use default
|
||||||
const eventColor = event.calendarColor || "#009a9a";
|
const eventColor = event.calendarColor || "#009a9a";
|
||||||
@ -7575,16 +7701,16 @@ export default function WeeklyView() {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={event.id}
|
key={event.id}
|
||||||
className="time-slot-event"
|
className={`time-slot-event${isDragging ? ' event-dragging' : ''}`}
|
||||||
title={`${event.calendarTitle}: ${event.title}\n${timeStr}`}
|
title={`${event.calendarTitle}: ${event.title}\n${timeStr}`}
|
||||||
style={{
|
style={{
|
||||||
height: `${Math.max(eventHeight, 15)}px`,
|
height: `${eventHeight}px`,
|
||||||
minHeight: `15px`,
|
minHeight: `15px`,
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
top: `${topOffset}px`,
|
top: `${topOffset}px`,
|
||||||
left: `calc(${colLeft}% + 1px)`,
|
left: `calc(${colLeft}% + 1px)`,
|
||||||
width: `calc(${colWidth}% - 3px)`,
|
width: `calc(${colWidth}% - 3px)`,
|
||||||
zIndex: 1,
|
zIndex: isDragging ? 50 : 1,
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
alignItems: "flex-start",
|
alignItems: "flex-start",
|
||||||
justifyContent: "flex-start",
|
justifyContent: "flex-start",
|
||||||
@ -7592,11 +7718,15 @@ export default function WeeklyView() {
|
|||||||
borderLeftColor: borderColor,
|
borderLeftColor: borderColor,
|
||||||
color: borderColor,
|
color: borderColor,
|
||||||
cursor: event.editable
|
cursor: event.editable
|
||||||
? "pointer"
|
? (isDragging ? "grabbing" : "grab")
|
||||||
: "default",
|
: "default",
|
||||||
|
opacity: isDragging && eventDragState?.mode === 'move' ? 0.7 : 1,
|
||||||
|
transition: isDragging ? 'none' : undefined,
|
||||||
|
userSelect: 'none',
|
||||||
}}
|
}}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
if (dragJustEndedRef.current) return; // Don't open modal after drag
|
||||||
if (event.editable) {
|
if (event.editable) {
|
||||||
setCalendarEventModal({
|
setCalendarEventModal({
|
||||||
isOpen: true,
|
isOpen: true,
|
||||||
@ -7604,7 +7734,22 @@ export default function WeeklyView() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
onMouseDown={(e) => {
|
||||||
|
if (event.editable && e.button === 0) {
|
||||||
|
handleEventDragStart(e, event, 'move');
|
||||||
|
}
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
|
{/* Top resize handle */}
|
||||||
|
{event.editable && (
|
||||||
|
<div
|
||||||
|
className="event-resize-handle event-resize-handle-top"
|
||||||
|
onMouseDown={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleEventDragStart(e, event, 'resize-top');
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<div className="event-title-row">
|
<div className="event-title-row">
|
||||||
<span className="event-indicator">📅</span>
|
<span className="event-indicator">📅</span>
|
||||||
<span className="event-title">
|
<span className="event-title">
|
||||||
@ -7612,6 +7757,44 @@ export default function WeeklyView() {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="event-time-row">{timeStr}</div>
|
<div className="event-time-row">{timeStr}</div>
|
||||||
|
{(event.isRecurring || profile.showCalendarProviderIcon) && (
|
||||||
|
<div style={{
|
||||||
|
position: "absolute",
|
||||||
|
bottom: 2,
|
||||||
|
right: 4,
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 3,
|
||||||
|
opacity: 0.5,
|
||||||
|
pointerEvents: "none",
|
||||||
|
}}>
|
||||||
|
{event.isRecurring && <Repeat size={11} />}
|
||||||
|
{profile.showCalendarProviderIcon && (
|
||||||
|
<img
|
||||||
|
src={
|
||||||
|
event.source === "google" ? "/icons/google-calendar.svg"
|
||||||
|
: event.source === "apple" ? "/icons/apple-calendar.svg"
|
||||||
|
: event.source === "outlook" ? "/icons/outlook-calendar.svg"
|
||||||
|
: event.source === "synology" ? "/icons/synology-calendar.svg"
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
alt={event.source}
|
||||||
|
style={{ width: 13, height: 13 }}
|
||||||
|
onError={(e) => { (e.target as HTMLImageElement).style.display = 'none'; }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{/* Bottom resize handle */}
|
||||||
|
{event.editable && (
|
||||||
|
<div
|
||||||
|
className="event-resize-handle event-resize-handle-bottom"
|
||||||
|
onMouseDown={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleEventDragStart(e, event, 'resize-bottom');
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@ -234,7 +234,12 @@ export const deleteEvent = async (
|
|||||||
calendarId,
|
calendarId,
|
||||||
eventId,
|
eventId,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
|
// 410 Gone means already deleted — treat as success
|
||||||
|
if (error?.code === 410 || error?.status === 410) {
|
||||||
|
console.log('[GOOGLE] Event already deleted (410 Gone), treating as success');
|
||||||
|
return;
|
||||||
|
}
|
||||||
console.error('Error deleting event:', error);
|
console.error('Error deleting event:', error);
|
||||||
throw new Error('Failed to delete event');
|
throw new Error('Failed to delete event');
|
||||||
}
|
}
|
||||||
|
|||||||
@ -377,14 +377,17 @@ export const updateEvent = async (
|
|||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const encodedEventId = encodeURIComponent(eventId);
|
||||||
|
const encodedCalendarId = encodeURIComponent(calendarId);
|
||||||
|
|
||||||
// Try non-calendar-scoped endpoint first (more reliable for all ID types)
|
// Try non-calendar-scoped endpoint first (more reliable for all ID types)
|
||||||
let response = await fetch(`${GRAPH_ENDPOINT}/me/events/${eventId}`, {
|
let response = await fetch(`${GRAPH_ENDPOINT}/me/events/${encodedEventId}`, {
|
||||||
method: 'PATCH', headers: patchHeaders, body
|
method: 'PATCH', headers: patchHeaders, body
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok && (response.status === 404 || response.status === 400)) {
|
if (!response.ok && (response.status === 404 || response.status === 400)) {
|
||||||
// Fallback: try calendar-scoped endpoint
|
// Fallback: try calendar-scoped endpoint
|
||||||
response = await fetch(`${GRAPH_ENDPOINT}/me/calendars/${calendarId}/events/${eventId}`, {
|
response = await fetch(`${GRAPH_ENDPOINT}/me/calendars/${encodedCalendarId}/events/${encodedEventId}`, {
|
||||||
method: 'PATCH', headers: patchHeaders, body
|
method: 'PATCH', headers: patchHeaders, body
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -415,19 +418,28 @@ export const deleteEvent = async (
|
|||||||
eventId: string
|
eventId: string
|
||||||
) => {
|
) => {
|
||||||
const headers = { 'Authorization': `Bearer ${accessToken}` };
|
const headers = { 'Authorization': `Bearer ${accessToken}` };
|
||||||
|
// Encode the event ID for URL safety (base64 padding = may need encoding)
|
||||||
|
const encodedEventId = encodeURIComponent(eventId);
|
||||||
|
const encodedCalendarId = encodeURIComponent(calendarId);
|
||||||
|
|
||||||
// Try non-calendar-scoped endpoint first (more reliable for all ID types)
|
// Try non-calendar-scoped endpoint first (more reliable for all ID types)
|
||||||
let response = await fetch(`${GRAPH_ENDPOINT}/me/events/${eventId}`, {
|
let response = await fetch(`${GRAPH_ENDPOINT}/me/events/${encodedEventId}`, {
|
||||||
method: 'DELETE', headers
|
method: 'DELETE', headers
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok && (response.status === 404 || response.status === 400)) {
|
if (!response.ok && (response.status === 404 || response.status === 400)) {
|
||||||
// Fallback: try calendar-scoped endpoint
|
// Fallback: try calendar-scoped endpoint
|
||||||
response = await fetch(`${GRAPH_ENDPOINT}/me/calendars/${calendarId}/events/${eventId}`, {
|
response = await fetch(`${GRAPH_ENDPOINT}/me/calendars/${encodedCalendarId}/events/${encodedEventId}`, {
|
||||||
method: 'DELETE', headers
|
method: 'DELETE', headers
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 404/410 on delete = already gone, treat as success
|
||||||
|
if (response.status === 404 || response.status === 410) {
|
||||||
|
console.log('[OUTLOOK] Event already deleted or not found, treating as success');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const err = await response.text();
|
const err = await response.text();
|
||||||
throw new Error(`Failed to delete Outlook event: ${err}`);
|
throw new Error(`Failed to delete Outlook event: ${err}`);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user