feat: add calendar task list view and shared calendar grouping
- Add TaskListView component for displaying calendar tasks - Group shared calendars by account in sidebar panel - Add task view toggle to calendar toolbar - Extend calendar store with task-related state
This commit is contained in:
@@ -1,8 +1,8 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useRef, useEffect } from "react";
|
import { useState, useRef, useEffect, useMemo } from "react";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { Globe, Plus, RefreshCw, Trash2 } from "lucide-react";
|
import { Globe, Plus, RefreshCw, Share2, Trash2 } from "lucide-react";
|
||||||
import { cn, formatDateTime } from "@/lib/utils";
|
import { cn, formatDateTime } from "@/lib/utils";
|
||||||
import type { Calendar } from "@/lib/jmap/types";
|
import type { Calendar } from "@/lib/jmap/types";
|
||||||
import { CalendarColorPicker } from "@/components/settings/calendar-management-settings";
|
import { CalendarColorPicker } from "@/components/settings/calendar-management-settings";
|
||||||
@@ -42,6 +42,20 @@ export function CalendarSidebarPanel({
|
|||||||
const colorPickerRef = useRef<HTMLDivElement>(null);
|
const colorPickerRef = useRef<HTMLDivElement>(null);
|
||||||
const contextMenuRef = useRef<HTMLDivElement>(null);
|
const contextMenuRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const personalCalendars = useMemo(() => calendars.filter(c => !c.isShared), [calendars]);
|
||||||
|
const sharedAccountGroups = useMemo(() => {
|
||||||
|
const shared = calendars.filter(c => c.isShared);
|
||||||
|
const groups = new Map<string, { accountName: string; calendars: Calendar[] }>();
|
||||||
|
for (const cal of shared) {
|
||||||
|
const key = cal.accountId!;
|
||||||
|
if (!groups.has(key)) {
|
||||||
|
groups.set(key, { accountName: cal.accountName || key, calendars: [] });
|
||||||
|
}
|
||||||
|
groups.get(key)!.calendars.push(cal);
|
||||||
|
}
|
||||||
|
return Array.from(groups.values());
|
||||||
|
}, [calendars]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!colorPickerId && !contextMenuCalId) return;
|
if (!colorPickerId && !contextMenuCalId) return;
|
||||||
const handleClick = (e: MouseEvent) => {
|
const handleClick = (e: MouseEvent) => {
|
||||||
@@ -97,13 +111,7 @@ export function CalendarSidebarPanel({
|
|||||||
|
|
||||||
if (calendars.length === 0 && !onSubscribe) return null;
|
if (calendars.length === 0 && !onSubscribe) return null;
|
||||||
|
|
||||||
return (
|
const renderCalendarItem = (cal: Calendar) => {
|
||||||
<div className="mt-4">
|
|
||||||
<h3 className="text-xs font-medium text-muted-foreground uppercase tracking-wider mb-2 px-1">
|
|
||||||
{t("my_calendars")}
|
|
||||||
</h3>
|
|
||||||
<div className="space-y-0.5">
|
|
||||||
{calendars.map((cal) => {
|
|
||||||
const isVisible = selectedCalendarIds.includes(cal.id);
|
const isVisible = selectedCalendarIds.includes(cal.id);
|
||||||
const color = cal.color || "#3b82f6";
|
const color = cal.color || "#3b82f6";
|
||||||
|
|
||||||
@@ -197,8 +205,28 @@ export function CalendarSidebarPanel({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mt-4">
|
||||||
|
<h3 className="text-xs font-medium text-muted-foreground uppercase tracking-wider mb-2 px-1">
|
||||||
|
{t("my_calendars")}
|
||||||
|
</h3>
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
{personalCalendars.map(renderCalendarItem)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{sharedAccountGroups.map((group) => (
|
||||||
|
<div key={group.accountName} className="mt-4">
|
||||||
|
<h3 className="text-xs font-medium text-muted-foreground uppercase tracking-wider mb-2 px-1 flex items-center gap-1.5">
|
||||||
|
<Share2 className="w-3 h-3" />
|
||||||
|
{group.accountName}
|
||||||
|
</h3>
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
{group.calendars.map(renderCalendarItem)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -153,7 +153,7 @@ export function CalendarToolbar({
|
|||||||
{t("my_calendars")}
|
{t("my_calendars")}
|
||||||
</h3>
|
</h3>
|
||||||
<div className="space-y-0.5">
|
<div className="space-y-0.5">
|
||||||
{calendars.map((cal) => {
|
{calendars.filter(c => !c.isShared).map((cal) => {
|
||||||
const isVisible = selectedCalendarIds.includes(cal.id);
|
const isVisible = selectedCalendarIds.includes(cal.id);
|
||||||
const color = cal.color || "#3b82f6";
|
const color = cal.color || "#3b82f6";
|
||||||
return (
|
return (
|
||||||
@@ -179,6 +179,49 @@ export function CalendarToolbar({
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
{(() => {
|
||||||
|
const shared = calendars.filter(c => c.isShared);
|
||||||
|
const groups = new Map<string, { accountName: string; cals: typeof shared }>();
|
||||||
|
for (const c of shared) {
|
||||||
|
const key = c.accountId!;
|
||||||
|
if (!groups.has(key)) groups.set(key, { accountName: c.accountName || key, cals: [] });
|
||||||
|
groups.get(key)!.cals.push(c);
|
||||||
|
}
|
||||||
|
return Array.from(groups.values()).map((group) => (
|
||||||
|
<div key={group.accountName} className="mt-2">
|
||||||
|
<h3 className="text-xs font-medium text-muted-foreground uppercase tracking-wider mb-1 px-1">
|
||||||
|
{group.accountName}
|
||||||
|
</h3>
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
{group.cals.map((cal) => {
|
||||||
|
const isVisible = selectedCalendarIds.includes(cal.id);
|
||||||
|
const color = cal.color || "#3b82f6";
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={cal.id}
|
||||||
|
onClick={() => onToggleVisibility(cal.id)}
|
||||||
|
className={cn(
|
||||||
|
"flex items-center gap-2 w-full px-2 py-2 rounded-md text-sm transition-colors duration-150 touch-manipulation",
|
||||||
|
"hover:bg-muted"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"w-3.5 h-3.5 rounded-sm border-2 flex-shrink-0 transition-colors",
|
||||||
|
isVisible ? "border-transparent" : "border-muted-foreground/40 bg-transparent"
|
||||||
|
)}
|
||||||
|
style={isVisible ? { backgroundColor: color, borderColor: color } : undefined}
|
||||||
|
/>
|
||||||
|
<span className={cn("truncate", !isVisible && "text-muted-foreground")}>
|
||||||
|
{cal.name}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
));
|
||||||
|
})()}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,203 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useMemo, useCallback } from "react";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
import { format, parseISO, isPast, isToday, isTomorrow } from "date-fns";
|
||||||
|
import { Check, Circle, Flag, CalendarDays, ListTodo } from "lucide-react";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import type { CalendarTask, Calendar } from "@/lib/jmap/types";
|
||||||
|
import type { TaskViewFilter } from "@/stores/task-store";
|
||||||
|
import { useSettingsStore } from "@/stores/settings-store";
|
||||||
|
|
||||||
|
interface TaskListViewProps {
|
||||||
|
tasks: CalendarTask[];
|
||||||
|
calendars: Calendar[];
|
||||||
|
selectedCalendarIds: string[];
|
||||||
|
filter: TaskViewFilter;
|
||||||
|
showCompleted: boolean;
|
||||||
|
onSelectTask: (task: CalendarTask) => void;
|
||||||
|
onToggleComplete: (task: CalendarTask) => void;
|
||||||
|
selectedTaskId?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTaskPriorityIcon(priority: number) {
|
||||||
|
if (priority >= 1 && priority <= 4) return <Flag className="h-3.5 w-3.5 text-red-500" />;
|
||||||
|
if (priority === 5) return <Flag className="h-3.5 w-3.5 text-orange-500" />;
|
||||||
|
if (priority >= 6 && priority <= 9) return <Flag className="h-3.5 w-3.5 text-gray-400" />;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDueDateLabel(due: string, showWithoutTime: boolean, t: ReturnType<typeof useTranslations>, timeFormat: string): { label: string; className: string } {
|
||||||
|
const dueDate = parseISO(due);
|
||||||
|
const overdue = isPast(dueDate) && !isToday(dueDate);
|
||||||
|
|
||||||
|
if (isToday(dueDate)) {
|
||||||
|
return {
|
||||||
|
label: t("tasks.due_today"),
|
||||||
|
className: "text-blue-600 dark:text-blue-400",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (isTomorrow(dueDate)) {
|
||||||
|
return {
|
||||||
|
label: t("tasks.due_tomorrow"),
|
||||||
|
className: "text-muted-foreground",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (overdue) {
|
||||||
|
return {
|
||||||
|
label: t("tasks.overdue"),
|
||||||
|
className: "text-red-600 dark:text-red-400",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatted = showWithoutTime
|
||||||
|
? format(dueDate, "MMM d")
|
||||||
|
: format(dueDate, timeFormat === "12h" ? "MMM d, h:mm a" : "MMM d, HH:mm");
|
||||||
|
|
||||||
|
return {
|
||||||
|
label: formatted,
|
||||||
|
className: "text-muted-foreground",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TaskListView({
|
||||||
|
tasks,
|
||||||
|
calendars,
|
||||||
|
selectedCalendarIds,
|
||||||
|
filter,
|
||||||
|
showCompleted,
|
||||||
|
onSelectTask,
|
||||||
|
onToggleComplete,
|
||||||
|
selectedTaskId,
|
||||||
|
}: TaskListViewProps) {
|
||||||
|
const t = useTranslations("calendar");
|
||||||
|
const timeFormat = useSettingsStore((s) => s.timeFormat);
|
||||||
|
|
||||||
|
const filteredTasks = useMemo(() => {
|
||||||
|
let result = tasks.filter(task => {
|
||||||
|
const calIds = Object.keys(task.calendarIds);
|
||||||
|
return calIds.some(id => selectedCalendarIds.includes(id));
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!showCompleted) {
|
||||||
|
result = result.filter(task => task.progress !== "completed" && task.progress !== "cancelled");
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (filter) {
|
||||||
|
case "pending":
|
||||||
|
result = result.filter(task => task.progress === "needs-action" || task.progress === "in-process");
|
||||||
|
break;
|
||||||
|
case "completed":
|
||||||
|
result = result.filter(task => task.progress === "completed");
|
||||||
|
break;
|
||||||
|
case "overdue":
|
||||||
|
result = result.filter(task => {
|
||||||
|
if (!task.due || task.progress === "completed" || task.progress === "cancelled") return false;
|
||||||
|
return isPast(parseISO(task.due)) && !isToday(parseISO(task.due));
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort: overdue first, then by due date (no due date last), then by priority
|
||||||
|
result.sort((a, b) => {
|
||||||
|
// Completed tasks at the bottom
|
||||||
|
if (a.progress === "completed" && b.progress !== "completed") return 1;
|
||||||
|
if (a.progress !== "completed" && b.progress === "completed") return -1;
|
||||||
|
|
||||||
|
// Tasks with due dates before those without
|
||||||
|
if (a.due && !b.due) return -1;
|
||||||
|
if (!a.due && b.due) return 1;
|
||||||
|
if (a.due && b.due) {
|
||||||
|
const dateCompare = new Date(a.due).getTime() - new Date(b.due).getTime();
|
||||||
|
if (dateCompare !== 0) return dateCompare;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Higher priority first (lower number = higher priority, but 0 = no priority goes last)
|
||||||
|
const aPri = a.priority || 10;
|
||||||
|
const bPri = b.priority || 10;
|
||||||
|
return aPri - bPri;
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}, [tasks, selectedCalendarIds, filter, showCompleted]);
|
||||||
|
|
||||||
|
const handleToggle = useCallback((e: React.MouseEvent, task: CalendarTask) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onToggleComplete(task);
|
||||||
|
}, [onToggleComplete]);
|
||||||
|
|
||||||
|
if (filteredTasks.length === 0) {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center justify-center flex-1 text-muted-foreground py-12">
|
||||||
|
<ListTodo className="h-12 w-12 mb-3 opacity-30" />
|
||||||
|
<p className="text-sm">{t("tasks.no_tasks")}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex-1 overflow-y-auto">
|
||||||
|
<div className="divide-y divide-border">
|
||||||
|
{filteredTasks.map(task => {
|
||||||
|
const cal = calendars.find(c => task.calendarIds[c.id]);
|
||||||
|
const isCompleted = task.progress === "completed";
|
||||||
|
const priorityIcon = getTaskPriorityIcon(task.priority);
|
||||||
|
const dueDateInfo = task.due ? getDueDateLabel(task.due, task.showWithoutTime, t, timeFormat) : null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={task.id}
|
||||||
|
onClick={() => onSelectTask(task)}
|
||||||
|
className={cn(
|
||||||
|
"flex items-start gap-3 px-4 py-3 cursor-pointer hover:bg-muted/50 transition-colors",
|
||||||
|
selectedTaskId === task.id && "bg-muted",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{/* Checkbox */}
|
||||||
|
<button
|
||||||
|
onClick={(e) => handleToggle(e, task)}
|
||||||
|
className={cn(
|
||||||
|
"mt-0.5 flex-shrink-0 w-5 h-5 rounded-full border-2 flex items-center justify-center transition-colors",
|
||||||
|
isCompleted
|
||||||
|
? "bg-green-500 border-green-500 text-white"
|
||||||
|
: "border-muted-foreground/40 hover:border-primary"
|
||||||
|
)}
|
||||||
|
aria-label={isCompleted ? t("tasks.mark_incomplete") : t("tasks.mark_complete")}
|
||||||
|
>
|
||||||
|
{isCompleted && <Check className="h-3 w-3" />}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<span className={cn(
|
||||||
|
"text-sm font-medium truncate",
|
||||||
|
isCompleted && "line-through text-muted-foreground"
|
||||||
|
)}>
|
||||||
|
{task.title || t("tasks.no_title")}
|
||||||
|
</span>
|
||||||
|
{priorityIcon}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2 mt-0.5">
|
||||||
|
{dueDateInfo && (
|
||||||
|
<span className={cn("text-xs flex items-center gap-1", dueDateInfo.className)}>
|
||||||
|
<CalendarDays className="h-3 w-3" />
|
||||||
|
{dueDateInfo.label}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{cal && (
|
||||||
|
<span className="text-xs text-muted-foreground flex items-center gap-1">
|
||||||
|
<span className="w-2 h-2 rounded-full flex-shrink-0" style={{ backgroundColor: cal.color || "#3b82f6" }} />
|
||||||
|
{cal.name}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
+69
-17
@@ -92,7 +92,7 @@ export const useCalendarStore = create<CalendarStore>()(
|
|||||||
fetchCalendars: async (client) => {
|
fetchCalendars: async (client) => {
|
||||||
set({ isLoading: true, error: null });
|
set({ isLoading: true, error: null });
|
||||||
try {
|
try {
|
||||||
const calendars = await client.getCalendars();
|
const calendars = await client.getAllCalendars();
|
||||||
const { selectedCalendarIds } = get();
|
const { selectedCalendarIds } = get();
|
||||||
const validIds = calendars.map(c => c.id);
|
const validIds = calendars.map(c => c.id);
|
||||||
const stillValid = selectedCalendarIds.filter(id => validIds.includes(id));
|
const stillValid = selectedCalendarIds.filter(id => validIds.includes(id));
|
||||||
@@ -110,7 +110,7 @@ export const useCalendarStore = create<CalendarStore>()(
|
|||||||
fetchEvents: async (client, start, end) => {
|
fetchEvents: async (client, start, end) => {
|
||||||
set({ isLoadingEvents: true, error: null });
|
set({ isLoadingEvents: true, error: null });
|
||||||
try {
|
try {
|
||||||
const events = await client.queryCalendarEvents({
|
const events = await client.queryAllCalendarEvents({
|
||||||
after: start,
|
after: start,
|
||||||
before: end,
|
before: end,
|
||||||
});
|
});
|
||||||
@@ -124,7 +124,23 @@ export const useCalendarStore = create<CalendarStore>()(
|
|||||||
createEvent: async (client, event, sendSchedulingMessages) => {
|
createEvent: async (client, event, sendSchedulingMessages) => {
|
||||||
set({ error: null });
|
set({ error: null });
|
||||||
try {
|
try {
|
||||||
const created = await client.createCalendarEvent(event, sendSchedulingMessages);
|
// Resolve shared calendar context from calendarIds
|
||||||
|
let targetAccountId = event.accountId;
|
||||||
|
const cleanEvent = { ...event };
|
||||||
|
if (event.calendarIds) {
|
||||||
|
const calId = Object.keys(event.calendarIds)[0];
|
||||||
|
if (calId) {
|
||||||
|
const cal = get().calendars.find(c => c.id === calId);
|
||||||
|
if (cal?.isShared && cal.originalId) {
|
||||||
|
targetAccountId = cal.accountId;
|
||||||
|
cleanEvent.calendarIds = { [cal.originalId]: true };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (event.originalCalendarIds) {
|
||||||
|
cleanEvent.calendarIds = event.originalCalendarIds;
|
||||||
|
}
|
||||||
|
const created = await client.createCalendarEvent(cleanEvent, sendSchedulingMessages, targetAccountId);
|
||||||
set((state) => ({ events: [...state.events, created] }));
|
set((state) => ({ events: [...state.events, created] }));
|
||||||
if (sendSchedulingMessages && created.participants) {
|
if (sendSchedulingMessages && created.participants) {
|
||||||
try {
|
try {
|
||||||
@@ -144,13 +160,27 @@ export const useCalendarStore = create<CalendarStore>()(
|
|||||||
updateEvent: async (client, id, updates, sendSchedulingMessages) => {
|
updateEvent: async (client, id, updates, sendSchedulingMessages) => {
|
||||||
set({ error: null });
|
set({ error: null });
|
||||||
try {
|
try {
|
||||||
await client.updateCalendarEvent(id, updates, sendSchedulingMessages);
|
// Resolve shared event IDs
|
||||||
|
const storeEvent = get().events.find(e => e.id === id);
|
||||||
|
const realId = storeEvent?.originalId || id;
|
||||||
|
const targetAccountId = storeEvent?.accountId;
|
||||||
|
// Remap namespaced calendarIds back to original IDs
|
||||||
|
const cleanUpdates = { ...updates };
|
||||||
|
if (cleanUpdates.calendarIds) {
|
||||||
|
const remapped: Record<string, boolean> = {};
|
||||||
|
for (const [calId, v] of Object.entries(cleanUpdates.calendarIds)) {
|
||||||
|
const cal = get().calendars.find(c => c.id === calId);
|
||||||
|
remapped[cal?.originalId || calId] = v;
|
||||||
|
}
|
||||||
|
cleanUpdates.calendarIds = remapped;
|
||||||
|
}
|
||||||
|
await client.updateCalendarEvent(realId, cleanUpdates, sendSchedulingMessages, targetAccountId);
|
||||||
set((state) => ({
|
set((state) => ({
|
||||||
events: state.events.map(e => e.id === id ? { ...e, ...updates } : e),
|
events: state.events.map(e => e.id === id ? { ...e, ...updates } : e),
|
||||||
}));
|
}));
|
||||||
if (sendSchedulingMessages) {
|
if (sendSchedulingMessages) {
|
||||||
try {
|
try {
|
||||||
const updatedEvent = await client.getCalendarEvent(id);
|
const updatedEvent = await client.getCalendarEvent(realId, targetAccountId);
|
||||||
if (updatedEvent?.participants) {
|
if (updatedEvent?.participants) {
|
||||||
await client.sendImipInvitation(updatedEvent);
|
await client.sendImipInvitation(updatedEvent);
|
||||||
}
|
}
|
||||||
@@ -174,6 +204,10 @@ export const useCalendarStore = create<CalendarStore>()(
|
|||||||
throw new Error('Invalid participant ID');
|
throw new Error('Invalid participant ID');
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
// Resolve shared event IDs
|
||||||
|
const storeEvent = get().events.find(e => e.id === eventId);
|
||||||
|
const realId = storeEvent?.originalId || eventId;
|
||||||
|
const targetAccountId = storeEvent?.accountId;
|
||||||
// Escape per RFC 6901 (JSON Pointer): ~ → ~0, / → ~1
|
// Escape per RFC 6901 (JSON Pointer): ~ → ~0, / → ~1
|
||||||
const escapedId = participantId.replace(/~/g, '~0').replace(/\//g, '~1');
|
const escapedId = participantId.replace(/~/g, '~0').replace(/\//g, '~1');
|
||||||
const patchKey = `participants/${escapedId}/participationStatus`;
|
const patchKey = `participants/${escapedId}/participationStatus`;
|
||||||
@@ -184,9 +218,10 @@ export const useCalendarStore = create<CalendarStore>()(
|
|||||||
patch.replyTo = replyTo;
|
patch.replyTo = replyTo;
|
||||||
}
|
}
|
||||||
await client.updateCalendarEvent(
|
await client.updateCalendarEvent(
|
||||||
eventId,
|
realId,
|
||||||
patch as unknown as Partial<CalendarEvent>,
|
patch as unknown as Partial<CalendarEvent>,
|
||||||
true
|
true,
|
||||||
|
targetAccountId
|
||||||
);
|
);
|
||||||
set((state) => ({
|
set((state) => ({
|
||||||
events: state.events.map(e => {
|
events: state.events.map(e => {
|
||||||
@@ -209,6 +244,10 @@ export const useCalendarStore = create<CalendarStore>()(
|
|||||||
|
|
||||||
importEvents: async (client, events, calendarId) => {
|
importEvents: async (client, events, calendarId) => {
|
||||||
let imported = 0;
|
let imported = 0;
|
||||||
|
// Resolve shared calendar IDs
|
||||||
|
const cal = get().calendars.find(c => c.id === calendarId);
|
||||||
|
const realCalendarId = cal?.originalId || calendarId;
|
||||||
|
const targetAccountId = cal?.accountId;
|
||||||
for (const event of events) {
|
for (const event of events) {
|
||||||
const src = event as Partial<CalendarEvent>;
|
const src = event as Partial<CalendarEvent>;
|
||||||
try {
|
try {
|
||||||
@@ -246,7 +285,7 @@ export const useCalendarStore = create<CalendarStore>()(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const data: Partial<CalendarEvent> = {
|
const data: Partial<CalendarEvent> = {
|
||||||
calendarIds: { [calendarId]: true },
|
calendarIds: { [realCalendarId]: true },
|
||||||
uid: src.uid,
|
uid: src.uid,
|
||||||
title: src.title,
|
title: src.title,
|
||||||
description: src.description,
|
description: src.description,
|
||||||
@@ -276,7 +315,7 @@ export const useCalendarStore = create<CalendarStore>()(
|
|||||||
const v = (data as Record<string, unknown>)[k];
|
const v = (data as Record<string, unknown>)[k];
|
||||||
if (v === undefined || v === null) delete (data as Record<string, unknown>)[k];
|
if (v === undefined || v === null) delete (data as Record<string, unknown>)[k];
|
||||||
});
|
});
|
||||||
const created = await client.createCalendarEvent(data);
|
const created = await client.createCalendarEvent(data, undefined, targetAccountId);
|
||||||
set((state) => ({ events: [...state.events, created] }));
|
set((state) => ({ events: [...state.events, created] }));
|
||||||
imported++;
|
imported++;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -289,7 +328,7 @@ export const useCalendarStore = create<CalendarStore>()(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const all = await client.queryCalendarEvents({});
|
const all = await client.queryCalendarEvents({}, undefined, undefined, targetAccountId);
|
||||||
const matching = all.filter((e) => e.uid === src.uid);
|
const matching = all.filter((e) => e.uid === src.uid);
|
||||||
if (matching.length > 0) {
|
if (matching.length > 0) {
|
||||||
const existingIds = new Set(storeEvents.map((e) => e.id));
|
const existingIds = new Set(storeEvents.map((e) => e.id));
|
||||||
@@ -313,9 +352,13 @@ export const useCalendarStore = create<CalendarStore>()(
|
|||||||
deleteEvent: async (client, id, sendSchedulingMessages) => {
|
deleteEvent: async (client, id, sendSchedulingMessages) => {
|
||||||
set({ error: null });
|
set({ error: null });
|
||||||
try {
|
try {
|
||||||
|
// Resolve shared event IDs
|
||||||
|
const storeEvent = get().events.find(e => e.id === id);
|
||||||
|
const realId = storeEvent?.originalId || id;
|
||||||
|
const targetAccountId = storeEvent?.accountId;
|
||||||
if (sendSchedulingMessages) {
|
if (sendSchedulingMessages) {
|
||||||
try {
|
try {
|
||||||
const event = await client.getCalendarEvent(id);
|
const event = await client.getCalendarEvent(realId, targetAccountId);
|
||||||
if (event?.participants) {
|
if (event?.participants) {
|
||||||
await client.sendImipCancellation(event);
|
await client.sendImipCancellation(event);
|
||||||
}
|
}
|
||||||
@@ -323,7 +366,7 @@ export const useCalendarStore = create<CalendarStore>()(
|
|||||||
debug.error('Failed to send cancellation emails:', e);
|
debug.error('Failed to send cancellation emails:', e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await client.deleteCalendarEvent(id, sendSchedulingMessages);
|
await client.deleteCalendarEvent(realId, sendSchedulingMessages, targetAccountId);
|
||||||
set((state) => ({
|
set((state) => ({
|
||||||
events: state.events.filter(e => e.id !== id),
|
events: state.events.filter(e => e.id !== id),
|
||||||
selectedEventId: state.selectedEventId === id ? null : state.selectedEventId,
|
selectedEventId: state.selectedEventId === id ? null : state.selectedEventId,
|
||||||
@@ -341,7 +384,10 @@ export const useCalendarStore = create<CalendarStore>()(
|
|||||||
updateCalendar: async (client, calendarId, updates) => {
|
updateCalendar: async (client, calendarId, updates) => {
|
||||||
set({ error: null });
|
set({ error: null });
|
||||||
try {
|
try {
|
||||||
await client.updateCalendar(calendarId, updates);
|
const cal = get().calendars.find(c => c.id === calendarId);
|
||||||
|
const realId = cal?.originalId || calendarId;
|
||||||
|
const targetAccountId = cal?.accountId;
|
||||||
|
await client.updateCalendar(realId, updates, targetAccountId);
|
||||||
set((state) => ({
|
set((state) => ({
|
||||||
calendars: state.calendars.map(c =>
|
calendars: state.calendars.map(c =>
|
||||||
c.id === calendarId ? { ...c, ...updates } : c
|
c.id === calendarId ? { ...c, ...updates } : c
|
||||||
@@ -373,7 +419,10 @@ export const useCalendarStore = create<CalendarStore>()(
|
|||||||
removeCalendar: async (client, calendarId) => {
|
removeCalendar: async (client, calendarId) => {
|
||||||
set({ error: null });
|
set({ error: null });
|
||||||
try {
|
try {
|
||||||
await client.deleteCalendar(calendarId);
|
const cal = get().calendars.find(c => c.id === calendarId);
|
||||||
|
const realId = cal?.originalId || calendarId;
|
||||||
|
const targetAccountId = cal?.accountId;
|
||||||
|
await client.deleteCalendar(realId, targetAccountId);
|
||||||
set((state) => ({
|
set((state) => ({
|
||||||
calendars: state.calendars.filter(c => c.id !== calendarId),
|
calendars: state.calendars.filter(c => c.id !== calendarId),
|
||||||
selectedCalendarIds: state.selectedCalendarIds.filter(id => id !== calendarId),
|
selectedCalendarIds: state.selectedCalendarIds.filter(id => id !== calendarId),
|
||||||
@@ -389,18 +438,21 @@ export const useCalendarStore = create<CalendarStore>()(
|
|||||||
clearCalendarEvents: async (client, calendarId) => {
|
clearCalendarEvents: async (client, calendarId) => {
|
||||||
set({ error: null });
|
set({ error: null });
|
||||||
try {
|
try {
|
||||||
|
const cal = get().calendars.find(c => c.id === calendarId);
|
||||||
|
const realCalId = cal?.originalId || calendarId;
|
||||||
|
const targetAccountId = cal?.accountId;
|
||||||
let totalDeleted = 0;
|
let totalDeleted = 0;
|
||||||
// Loop to handle pagination (getCalendarEvents has a 1000 limit)
|
// Loop to handle pagination (getCalendarEvents has a 1000 limit)
|
||||||
let hasMore = true;
|
let hasMore = true;
|
||||||
while (hasMore) {
|
while (hasMore) {
|
||||||
// Query all events and filter client-side by calendarId
|
// Query all events and filter client-side by calendarId
|
||||||
// to avoid relying on server-side inCalendars filter support
|
// to avoid relying on server-side inCalendars filter support
|
||||||
const allEvents = await client.getCalendarEvents();
|
const allEvents = await client.getCalendarEvents(undefined, targetAccountId);
|
||||||
const calendarEvents = allEvents.filter(e => e.calendarIds?.[calendarId]);
|
const calendarEvents = allEvents.filter(e => e.calendarIds?.[realCalId]);
|
||||||
if (calendarEvents.length === 0) break;
|
if (calendarEvents.length === 0) break;
|
||||||
|
|
||||||
const ids = calendarEvents.map(e => e.id);
|
const ids = calendarEvents.map(e => e.id);
|
||||||
const { destroyed } = await client.batchDeleteCalendarEvents(ids);
|
const { destroyed } = await client.batchDeleteCalendarEvents(ids, targetAccountId);
|
||||||
totalDeleted += destroyed.length;
|
totalDeleted += destroyed.length;
|
||||||
|
|
||||||
// If we couldn't destroy any events, stop to avoid infinite loop
|
// If we couldn't destroy any events, stop to avoid infinite loop
|
||||||
|
|||||||
Reference in New Issue
Block a user