feat: add iCal subscription editing and batch event import

This commit is contained in:
Linus Rath
2026-03-29 18:27:16 +02:00
parent 5583d95ecc
commit 4c853f176b
9 changed files with 278 additions and 109 deletions
+13 -1
View File
@@ -2,7 +2,7 @@
import { useState, useRef, useEffect, useMemo } from "react";
import { useTranslations } from "next-intl";
import { Globe, ListTodo, RefreshCw, Share2, Trash2 } from "lucide-react";
import { Globe, ListTodo, Pencil, RefreshCw, Share2, Trash2 } from "lucide-react";
import { cn, formatDateTime } from "@/lib/utils";
import type { Calendar } from "@/lib/jmap/types";
import { CalendarColorPicker } from "@/components/settings/calendar-management-settings";
@@ -18,6 +18,7 @@ interface CalendarSidebarPanelProps {
onToggleVisibility: (id: string) => void;
onColorChange?: (calendarId: string, color: string) => void;
onSubscribe?: () => void;
onEditSubscription?: (subscriptionId: string) => void;
client?: IJMAPClient | null;
}
@@ -27,6 +28,7 @@ export function CalendarSidebarPanel({
onToggleVisibility,
onColorChange,
onSubscribe,
onEditSubscription,
client,
}: CalendarSidebarPanelProps) {
const t = useTranslations("calendar");
@@ -173,6 +175,16 @@ export function CalendarSidebarPanel({
ref={contextMenuRef}
className="absolute left-6 top-full mt-1 z-50 bg-background border border-border rounded-lg shadow-lg py-1 w-48"
>
<button
onClick={() => {
setContextMenuCalId(null);
onEditSubscription?.(sub.id);
}}
className="flex items-center gap-2 w-full px-3 py-1.5 text-sm hover:bg-muted transition-colors"
>
<Pencil className="w-3.5 h-3.5" />
{tSub('edit')}
</button>
<button
onClick={() => handleRefreshSubscription(sub.id)}
className="flex items-center gap-2 w-full px-3 py-1.5 text-sm hover:bg-muted transition-colors"
+29 -14
View File
@@ -5,24 +5,28 @@ import { useTranslations } from "next-intl";
import { Button } from "@/components/ui/button";
import { X, Loader2, Globe } from "lucide-react";
import type { IJMAPClient } from '@/lib/jmap/client-interface';
import { useCalendarStore } from "@/stores/calendar-store";
import { useCalendarStore, type ICalSubscription } from "@/stores/calendar-store";
import { CalendarColorPicker } from "@/components/settings/calendar-management-settings";
import { toast } from "@/stores/toast-store";
interface ICalSubscriptionModalProps {
client: IJMAPClient;
onClose: () => void;
editSubscription?: ICalSubscription;
}
export function ICalSubscriptionModal({ client, onClose }: ICalSubscriptionModalProps) {
export function ICalSubscriptionModal({ client, onClose, editSubscription }: ICalSubscriptionModalProps) {
const t = useTranslations("calendar.subscription");
const tCommon = useTranslations("common");
const addICalSubscription = useCalendarStore((s) => s.addICalSubscription);
const updateICalSubscription = useCalendarStore((s) => s.updateICalSubscription);
const [url, setUrl] = useState("");
const [name, setName] = useState("");
const [color, setColor] = useState("#3b82f6");
const [refreshInterval, setRefreshInterval] = useState(60);
const isEdit = !!editSubscription;
const [url, setUrl] = useState(editSubscription?.url || "");
const [name, setName] = useState(editSubscription?.name || "");
const [color, setColor] = useState(editSubscription?.color || "#3b82f6");
const [refreshInterval, setRefreshInterval] = useState(editSubscription?.refreshInterval || 60);
const [isSubmitting, setIsSubmitting] = useState(false);
const [error, setError] = useState<string | null>(null);
const modalRef = useRef<HTMLDivElement>(null);
@@ -49,15 +53,26 @@ export function ICalSubscriptionModal({ client, onClose }: ICalSubscriptionModal
setIsSubmitting(true);
try {
const subscription = await addICalSubscription(client, trimmedUrl, name.trim(), color, refreshInterval);
if (subscription) {
toast.success(t("success", { name: name.trim() }));
if (isEdit && editSubscription) {
const updates: { url?: string; name?: string; color?: string; refreshInterval?: number } = {};
if (trimmedUrl !== editSubscription.url) updates.url = trimmedUrl;
if (name.trim() !== editSubscription.name) updates.name = name.trim();
if (color !== editSubscription.color) updates.color = color;
if (refreshInterval !== editSubscription.refreshInterval) updates.refreshInterval = refreshInterval;
await updateICalSubscription(client, editSubscription.id, updates);
toast.success(t("updated", { name: name.trim() }));
onClose();
} else {
setError(t("error"));
const subscription = await addICalSubscription(client, trimmedUrl, name.trim(), color, refreshInterval);
if (subscription) {
toast.success(t("success", { name: name.trim() }));
onClose();
} else {
setError(t("error"));
}
}
} catch {
setError(t("error"));
setError(isEdit ? t("update_error") : t("error"));
} finally {
setIsSubmitting(false);
}
@@ -108,7 +123,7 @@ export function ICalSubscriptionModal({ client, onClose }: ICalSubscriptionModal
<div className="flex items-center justify-between px-6 py-4 border-b border-border">
<div className="flex items-center gap-2">
<Globe className="w-5 h-5 text-primary" />
<h2 className="text-lg font-semibold">{t("title")}</h2>
<h2 className="text-lg font-semibold">{isEdit ? t("edit_title") : t("title")}</h2>
</div>
<button
onClick={onClose}
@@ -192,10 +207,10 @@ export function ICalSubscriptionModal({ client, onClose }: ICalSubscriptionModal
{isSubmitting ? (
<>
<Loader2 className="w-4 h-4 animate-spin mr-2" />
{t("subscribing")}
{isEdit ? t("saving") : t("subscribing")}
</>
) : (
t("subscribe")
isEdit ? t("save") : t("subscribe")
)}
</Button>
</div>
@@ -164,6 +164,7 @@ export function CalendarManagementSettings() {
const [colorPickerId, setColorPickerId] = useState<string | null>(null);
const [showImportModal, setShowImportModal] = useState(false);
const [showSubscriptionModal, setShowSubscriptionModal] = useState(false);
const [editingSubscription, setEditingSubscription] = useState<typeof icalSubscriptions[0] | null>(null);
const [deletingSubId, setDeletingSubId] = useState<string | null>(null);
const [refreshingSubId, setRefreshingSubId] = useState<string | null>(null);
const tImport = useTranslations('calendar.import');
@@ -629,6 +630,14 @@ export function CalendarManagementSettings() {
)}
</div>
<div className="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
<button
type="button"
onClick={() => setEditingSubscription(sub)}
className="p-1.5 rounded-md hover:bg-muted text-muted-foreground hover:text-foreground transition-colors"
title={tSub('edit')}
>
<Pencil className="w-3.5 h-3.5" />
</button>
<button
type="button"
onClick={() => handleRefreshSubscription(sub.id)}
@@ -667,6 +676,14 @@ export function CalendarManagementSettings() {
onClose={() => setShowSubscriptionModal(false)}
/>
)}
{editingSubscription && client && (
<ICalSubscriptionModal
client={client}
editSubscription={editingSubscription}
onClose={() => setEditingSubscription(null)}
/>
)}
</SettingsSection>
);
}