feat: overlapping calendar events side by side + Outlook delete fix
- Overlapping calendar events on the same time slot are now rendered
side by side instead of stacked on top of each other. Uses a greedy
column assignment algorithm to compute layout per day.
- Outlook delete/update: use /me/events/{id} as primary endpoint
(without encodeURIComponent) instead of calendar-scoped endpoint,
with fallback. Fixes ErrorItemNotFound for recurring series masters.
v1.65.0
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
e00834e0bb
commit
ba4f1ca280
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "my-weekly-todo-list",
|
||||
"version": "1.64.0",
|
||||
"version": "1.65.0",
|
||||
"description": "A web-based weekly task management application that organizes to-dos and calendar events in a single, intuitive weekly view",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
|
||||
@ -3592,6 +3592,79 @@ export default function WeeklyView() {
|
||||
[calendarEvents, effectiveCellDuration],
|
||||
);
|
||||
|
||||
// Precompute overlap layout: { [eventId]: { column, totalColumns } }
|
||||
const eventOverlapLayout = useMemo(() => {
|
||||
const layout: Record<string, { column: number; totalColumns: number }> = {};
|
||||
// Group events by day
|
||||
const dayMap = new Map<string, CalendarEvent[]>();
|
||||
for (const event of calendarEvents) {
|
||||
if (isAllDayEvent(event)) continue;
|
||||
const d = new Date(event.startTime);
|
||||
const key = `${d.getFullYear()}-${d.getMonth()}-${d.getDate()}`;
|
||||
if (!dayMap.has(key)) dayMap.set(key, []);
|
||||
dayMap.get(key)!.push(event);
|
||||
}
|
||||
for (const events of dayMap.values()) {
|
||||
// Sort by start time, then by duration (longer first)
|
||||
events.sort((a, b) => {
|
||||
const diff = new Date(a.startTime).getTime() - new Date(b.startTime).getTime();
|
||||
if (diff !== 0) return diff;
|
||||
return (new Date(b.endTime).getTime() - new Date(b.startTime).getTime()) -
|
||||
(new Date(a.endTime).getTime() - new Date(a.startTime).getTime());
|
||||
});
|
||||
// Build overlap groups using a greedy column assignment
|
||||
const columns: { end: number; eventId: string }[][] = [];
|
||||
for (const event of events) {
|
||||
const start = new Date(event.startTime).getTime();
|
||||
const end = new Date(event.endTime).getTime();
|
||||
// Find first column where this event doesn't overlap
|
||||
let placed = false;
|
||||
for (let col = 0; col < columns.length; col++) {
|
||||
const lastInCol = columns[col][columns[col].length - 1];
|
||||
if (lastInCol.end <= start) {
|
||||
columns[col].push({ end, eventId: event.id });
|
||||
placed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!placed) {
|
||||
columns.push([{ end, eventId: event.id }]);
|
||||
}
|
||||
}
|
||||
// Now find the max columns each event actually shares with
|
||||
// For each event, find all events that overlap it and determine the group width
|
||||
for (const event of events) {
|
||||
const start = new Date(event.startTime).getTime();
|
||||
const end = new Date(event.endTime).getTime();
|
||||
// Count how many columns have events overlapping this time range
|
||||
let overlappingCols = 0;
|
||||
for (const col of columns) {
|
||||
for (const item of col) {
|
||||
const itemStart = calendarEvents.find(e => e.id === item.eventId);
|
||||
if (itemStart) {
|
||||
const iStart = new Date(itemStart.startTime).getTime();
|
||||
const iEnd = new Date(itemStart.endTime).getTime();
|
||||
if (iStart < end && iEnd > start) {
|
||||
overlappingCols++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Find which column this event is in
|
||||
let eventCol = 0;
|
||||
for (let col = 0; col < columns.length; col++) {
|
||||
if (columns[col].some(item => item.eventId === event.id)) {
|
||||
eventCol = col;
|
||||
break;
|
||||
}
|
||||
}
|
||||
layout[event.id] = { column: eventCol, totalColumns: Math.max(overlappingCols, 1) };
|
||||
}
|
||||
}
|
||||
return layout;
|
||||
}, [calendarEvents]);
|
||||
|
||||
// Calculate event duration in pixels for proper height display
|
||||
const getEventDuration = (event: CalendarEvent): number => {
|
||||
if (isAllDayEvent(event)) return 0; // All-day events handled separately
|
||||
@ -7495,6 +7568,10 @@ export default function WeeklyView() {
|
||||
? eventColor
|
||||
: "var(--weekly-teal)";
|
||||
|
||||
const overlap = eventOverlapLayout[event.id] || { column: 0, totalColumns: 1 };
|
||||
const colWidth = 100 / overlap.totalColumns;
|
||||
const colLeft = overlap.column * colWidth;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={event.id}
|
||||
@ -7505,8 +7582,8 @@ export default function WeeklyView() {
|
||||
minHeight: `15px`,
|
||||
position: "absolute",
|
||||
top: `${topOffset}px`,
|
||||
left: "1px",
|
||||
right: "2px",
|
||||
left: `calc(${colLeft}% + 1px)`,
|
||||
width: `calc(${colWidth}% - 3px)`,
|
||||
zIndex: 1,
|
||||
flexDirection: "column",
|
||||
alignItems: "flex-start",
|
||||
|
||||
@ -377,14 +377,14 @@ export const updateEvent = async (
|
||||
'Content-Type': 'application/json'
|
||||
};
|
||||
|
||||
// Try calendar-scoped endpoint first
|
||||
let response = await fetch(`${GRAPH_ENDPOINT}/me/calendars/${encodeURIComponent(calendarId)}/events/${encodeURIComponent(eventId)}`, {
|
||||
// Try non-calendar-scoped endpoint first (more reliable for all ID types)
|
||||
let response = await fetch(`${GRAPH_ENDPOINT}/me/events/${eventId}`, {
|
||||
method: 'PATCH', headers: patchHeaders, body
|
||||
});
|
||||
|
||||
if (!response.ok && response.status === 404) {
|
||||
// Fallback: try non-calendar-scoped endpoint
|
||||
response = await fetch(`${GRAPH_ENDPOINT}/me/events/${encodeURIComponent(eventId)}`, {
|
||||
if (!response.ok && (response.status === 404 || response.status === 400)) {
|
||||
// Fallback: try calendar-scoped endpoint
|
||||
response = await fetch(`${GRAPH_ENDPOINT}/me/calendars/${calendarId}/events/${eventId}`, {
|
||||
method: 'PATCH', headers: patchHeaders, body
|
||||
});
|
||||
}
|
||||
@ -414,21 +414,17 @@ export const deleteEvent = async (
|
||||
calendarId: string,
|
||||
eventId: string
|
||||
) => {
|
||||
// Try calendar-scoped endpoint first
|
||||
let response = await fetch(`${GRAPH_ENDPOINT}/me/calendars/${encodeURIComponent(calendarId)}/events/${encodeURIComponent(eventId)}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${accessToken}`
|
||||
}
|
||||
const headers = { 'Authorization': `Bearer ${accessToken}` };
|
||||
|
||||
// Try non-calendar-scoped endpoint first (more reliable for all ID types)
|
||||
let response = await fetch(`${GRAPH_ENDPOINT}/me/events/${eventId}`, {
|
||||
method: 'DELETE', headers
|
||||
});
|
||||
|
||||
if (!response.ok && response.status === 404) {
|
||||
// Fallback: try non-calendar-scoped endpoint (works better for series master IDs)
|
||||
response = await fetch(`${GRAPH_ENDPOINT}/me/events/${encodeURIComponent(eventId)}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${accessToken}`
|
||||
}
|
||||
if (!response.ok && (response.status === 404 || response.status === 400)) {
|
||||
// Fallback: try calendar-scoped endpoint
|
||||
response = await fetch(`${GRAPH_ENDPOINT}/me/calendars/${calendarId}/events/${eventId}`, {
|
||||
method: 'DELETE', headers
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user