diff --git a/frontend/src/components/calendar/CalendarForm.tsx b/frontend/src/components/calendar/CalendarForm.tsx
index d718ec8..842b694 100644
--- a/frontend/src/components/calendar/CalendarForm.tsx
+++ b/frontend/src/components/calendar/CalendarForm.tsx
@@ -1,4 +1,4 @@
-import { useState, FormEvent } from 'react';
+import { useState, FormEvent, useCallback } from 'react';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { toast } from 'sonner';
import api, { getErrorMessage } from '@/lib/api';
@@ -15,6 +15,7 @@ import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Button } from '@/components/ui/button';
import { Switch } from '@/components/ui/switch';
+import { Select } from '@/components/ui/select';
import { useConnections } from '@/hooks/useConnections';
import { useSharedCalendars } from '@/hooks/useSharedCalendars';
import CalendarMemberSearch from './CalendarMemberSearch';
@@ -36,6 +37,8 @@ export default function CalendarForm({ calendar, onClose }: CalendarFormProps) {
const [color, setColor] = useState(calendar?.color || '#3b82f6');
const [isShared, setIsShared] = useState(calendar?.is_shared ?? false);
+ const [pendingInvite, setPendingInvite] = useState<{ conn: Connection; permission: CalendarPermission } | null>(null);
+
const { connections } = useConnections();
const { invite, isInviting, updateMember, removeMember } = useSharedCalendars();
@@ -93,14 +96,19 @@ export default function CalendarForm({ calendar, onClose }: CalendarFormProps) {
mutation.mutate();
};
- const handleInvite = async (conn: Connection) => {
- if (!calendar) return;
+ const handleSelectConnection = useCallback((conn: Connection) => {
+ setPendingInvite({ conn, permission: 'read_only' });
+ }, []);
+
+ const handleSendInvite = async () => {
+ if (!calendar || !pendingInvite) return;
await invite({
calendarId: calendar.id,
- connectionId: conn.id,
- permission: 'read_only',
+ connectionId: pendingInvite.conn.id,
+ permission: pendingInvite.permission,
canAddOthers: false,
});
+ setPendingInvite(null);
};
const handleUpdatePermission = async (memberId: number, permission: CalendarPermission) => {
@@ -179,12 +187,48 @@ export default function CalendarForm({ calendar, onClose }: CalendarFormProps) {
-
+ {pendingInvite ? (
+
+
+
+ {pendingInvite.conn.connected_preferred_name || pendingInvite.conn.connected_umbral_name}
+
+
+
+
+
+
+
+
+ ) : (
+
+ )}
- {/* Owned calendars list */}
+ {/* Owned calendars list (non-shared only) */}
- {calendars.map((cal) => (
+ {calendars.filter((c) => !c.is_shared).map((cal) => (
- {/* Shared calendars section */}
-
+ {/* Shared calendars section -- owned + member */}
+ {(calendars.some((c) => c.is_shared) || sharedCalendars.length > 0) && (
+
c.is_shared)}
+ memberships={sharedCalendars}
+ visibleSharedIds={visibleSharedIds}
+ onVisibilityChange={handleSharedVisibilityChange}
+ onEditCalendar={handleEdit}
+ onToggleCalendar={handleToggle}
+ />
+ )}
{/* Templates section */}
diff --git a/frontend/src/components/calendar/EventDetailPanel.tsx b/frontend/src/components/calendar/EventDetailPanel.tsx
index 1aa2449..b4e0e0e 100644
--- a/frontend/src/components/calendar/EventDetailPanel.tsx
+++ b/frontend/src/components/calendar/EventDetailPanel.tsx
@@ -227,8 +227,13 @@ export default function EventDetailPanel({
isSharedEvent = false,
}: EventDetailPanelProps) {
const queryClient = useQueryClient();
- const { data: calendars = [] } = useCalendars();
- const selectableCalendars = calendars.filter((c) => !c.is_system);
+ const { data: calendars = [], sharedData: sharedMemberships = [] } = useCalendars();
+ const selectableCalendars = [
+ ...calendars.filter((c) => !c.is_system),
+ ...sharedMemberships
+ .filter((m) => m.permission === 'create_modify' || m.permission === 'full_access')
+ .map((m) => ({ id: m.calendar_id, name: m.calendar_name, color: m.local_color || m.calendar_color, is_default: false })),
+ ];
const defaultCalendar = calendars.find((c) => c.is_default);
const { data: locations = [] } = useQuery({
diff --git a/frontend/src/components/calendar/SharedCalendarSection.tsx b/frontend/src/components/calendar/SharedCalendarSection.tsx
index 9d52124..d42308c 100644
--- a/frontend/src/components/calendar/SharedCalendarSection.tsx
+++ b/frontend/src/components/calendar/SharedCalendarSection.tsx
@@ -1,7 +1,7 @@
import { useState } from 'react';
import { Ghost, Pencil } from 'lucide-react';
import { Checkbox } from '@/components/ui/checkbox';
-import type { SharedCalendarMembership } from '@/types';
+import type { Calendar, SharedCalendarMembership } from '@/types';
import SharedCalendarSettings from './SharedCalendarSettings';
const STORAGE_KEY = 'umbra_shared_cal_visibility';
@@ -19,19 +19,25 @@ function saveVisibility(v: Record) {
}
interface SharedCalendarSectionProps {
+ ownedSharedCalendars?: Calendar[];
memberships: SharedCalendarMembership[];
visibleSharedIds: Set;
onVisibilityChange: (calendarId: number, visible: boolean) => void;
+ onEditCalendar?: (calendar: Calendar) => void;
+ onToggleCalendar?: (calendar: Calendar) => void;
}
export default function SharedCalendarSection({
+ ownedSharedCalendars = [],
memberships,
visibleSharedIds,
onVisibilityChange,
+ onEditCalendar,
+ onToggleCalendar,
}: SharedCalendarSectionProps) {
const [settingsFor, setSettingsFor] = useState(null);
- if (memberships.length === 0) return null;
+ if (memberships.length === 0 && ownedSharedCalendars.length === 0) return null;
return (
<>
@@ -43,6 +49,36 @@ export default function SharedCalendarSection({
+ {ownedSharedCalendars.map((cal) => (
+
+
onToggleCalendar?.(cal)}
+ className="shrink-0"
+ style={{
+ accentColor: cal.color,
+ borderColor: cal.is_visible ? cal.color : undefined,
+ backgroundColor: cal.is_visible ? cal.color : undefined,
+ }}
+ />
+
+ {cal.name}
+ Owner
+
+
+ ))}
{memberships.map((m) => {
const color = m.local_color || m.calendar_color;
const isVisible = visibleSharedIds.has(m.calendar_id);