Fix 4 reported bugs from Phase 4 testing

1. Invite auto-sends at read_only: now stages connection with permission
   selector (Read Only / Create Modify / Full Access) before sending
2. Shared calendars missing from event create dropdown: members with
   create_modify+ permission now see shared calendars in calendar picker
3. Shared calendar category not showing for owner: owner's shared calendars
   now appear under SHARED CALENDARS section with "Owner" badge
4. Event creation not updating calendar: handlePanelClose now invalidates
   calendar-events query to ensure FullCalendar refreshes

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Kyle 2026-03-06 06:23:45 +08:00
parent e5690625eb
commit f45b7a2115
4 changed files with 113 additions and 23 deletions

View File

@ -1,4 +1,4 @@
import { useState, FormEvent } from 'react'; import { useState, FormEvent, useCallback } from 'react';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { toast } from 'sonner'; import { toast } from 'sonner';
import api, { getErrorMessage } from '@/lib/api'; import api, { getErrorMessage } from '@/lib/api';
@ -15,6 +15,7 @@ import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label'; import { Label } from '@/components/ui/label';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Switch } from '@/components/ui/switch'; import { Switch } from '@/components/ui/switch';
import { Select } from '@/components/ui/select';
import { useConnections } from '@/hooks/useConnections'; import { useConnections } from '@/hooks/useConnections';
import { useSharedCalendars } from '@/hooks/useSharedCalendars'; import { useSharedCalendars } from '@/hooks/useSharedCalendars';
import CalendarMemberSearch from './CalendarMemberSearch'; import CalendarMemberSearch from './CalendarMemberSearch';
@ -36,6 +37,8 @@ export default function CalendarForm({ calendar, onClose }: CalendarFormProps) {
const [color, setColor] = useState(calendar?.color || '#3b82f6'); const [color, setColor] = useState(calendar?.color || '#3b82f6');
const [isShared, setIsShared] = useState(calendar?.is_shared ?? false); const [isShared, setIsShared] = useState(calendar?.is_shared ?? false);
const [pendingInvite, setPendingInvite] = useState<{ conn: Connection; permission: CalendarPermission } | null>(null);
const { connections } = useConnections(); const { connections } = useConnections();
const { invite, isInviting, updateMember, removeMember } = useSharedCalendars(); const { invite, isInviting, updateMember, removeMember } = useSharedCalendars();
@ -93,14 +96,19 @@ export default function CalendarForm({ calendar, onClose }: CalendarFormProps) {
mutation.mutate(); mutation.mutate();
}; };
const handleInvite = async (conn: Connection) => { const handleSelectConnection = useCallback((conn: Connection) => {
if (!calendar) return; setPendingInvite({ conn, permission: 'read_only' });
}, []);
const handleSendInvite = async () => {
if (!calendar || !pendingInvite) return;
await invite({ await invite({
calendarId: calendar.id, calendarId: calendar.id,
connectionId: conn.id, connectionId: pendingInvite.conn.id,
permission: 'read_only', permission: pendingInvite.permission,
canAddOthers: false, canAddOthers: false,
}); });
setPendingInvite(null);
}; };
const handleUpdatePermission = async (memberId: number, permission: CalendarPermission) => { const handleUpdatePermission = async (memberId: number, permission: CalendarPermission) => {
@ -179,12 +187,48 @@ export default function CalendarForm({ calendar, onClose }: CalendarFormProps) {
</span> </span>
</div> </div>
<CalendarMemberSearch {pendingInvite ? (
connections={connections} <div className="rounded-md border border-border bg-card-elevated p-3 space-y-2 animate-fade-in">
existingMembers={members} <div className="flex items-center justify-between">
onSelect={handleInvite} <span className="text-sm text-foreground">
isLoading={isInviting} {pendingInvite.conn.connected_preferred_name || pendingInvite.conn.connected_umbral_name}
/> </span>
<button
type="button"
onClick={() => setPendingInvite(null)}
className="text-xs text-muted-foreground hover:text-foreground"
>
Cancel
</button>
</div>
<div className="flex items-center gap-2">
<Select
value={pendingInvite.permission}
onChange={(e) => setPendingInvite((prev) => prev ? { ...prev, permission: e.target.value as CalendarPermission } : null)}
className="text-xs flex-1"
>
<option value="read_only">Read Only</option>
<option value="create_modify">Create / Modify</option>
<option value="full_access">Full Access</option>
</Select>
<Button
type="button"
size="sm"
onClick={handleSendInvite}
disabled={isInviting}
>
{isInviting ? 'Sending...' : 'Send Invite'}
</Button>
</div>
</div>
) : (
<CalendarMemberSearch
connections={connections}
existingMembers={members}
onSelect={handleSelectConnection}
isLoading={isInviting}
/>
)}
<CalendarMemberList <CalendarMemberList
members={members} members={members}

View File

@ -106,9 +106,9 @@ export default function CalendarSidebar({ onUseTemplate, onSharedVisibilityChang
</Button> </Button>
</div> </div>
<div className="flex-1 overflow-y-auto p-3 space-y-4"> <div className="flex-1 overflow-y-auto p-3 space-y-4">
{/* Owned calendars list */} {/* Owned calendars list (non-shared only) */}
<div className="space-y-0.5"> <div className="space-y-0.5">
{calendars.map((cal) => ( {calendars.filter((c) => !c.is_shared).map((cal) => (
<div <div
key={cal.id} key={cal.id}
className="group flex items-center gap-2.5 rounded-md px-2 py-1.5 hover:bg-card-elevated transition-colors duration-150" className="group flex items-center gap-2.5 rounded-md px-2 py-1.5 hover:bg-card-elevated transition-colors duration-150"
@ -138,12 +138,17 @@ export default function CalendarSidebar({ onUseTemplate, onSharedVisibilityChang
))} ))}
</div> </div>
{/* Shared calendars section */} {/* Shared calendars section -- owned + member */}
<SharedCalendarSection {(calendars.some((c) => c.is_shared) || sharedCalendars.length > 0) && (
memberships={sharedCalendars} <SharedCalendarSection
visibleSharedIds={visibleSharedIds} ownedSharedCalendars={calendars.filter((c) => c.is_shared)}
onVisibilityChange={handleSharedVisibilityChange} memberships={sharedCalendars}
/> visibleSharedIds={visibleSharedIds}
onVisibilityChange={handleSharedVisibilityChange}
onEditCalendar={handleEdit}
onToggleCalendar={handleToggle}
/>
)}
{/* Templates section */} {/* Templates section */}
<div className="space-y-1.5"> <div className="space-y-1.5">

View File

@ -227,8 +227,13 @@ export default function EventDetailPanel({
isSharedEvent = false, isSharedEvent = false,
}: EventDetailPanelProps) { }: EventDetailPanelProps) {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { data: calendars = [] } = useCalendars(); const { data: calendars = [], sharedData: sharedMemberships = [] } = useCalendars();
const selectableCalendars = calendars.filter((c) => !c.is_system); 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 defaultCalendar = calendars.find((c) => c.is_default);
const { data: locations = [] } = useQuery({ const { data: locations = [] } = useQuery({

View File

@ -1,7 +1,7 @@
import { useState } from 'react'; import { useState } from 'react';
import { Ghost, Pencil } from 'lucide-react'; import { Ghost, Pencil } from 'lucide-react';
import { Checkbox } from '@/components/ui/checkbox'; import { Checkbox } from '@/components/ui/checkbox';
import type { SharedCalendarMembership } from '@/types'; import type { Calendar, SharedCalendarMembership } from '@/types';
import SharedCalendarSettings from './SharedCalendarSettings'; import SharedCalendarSettings from './SharedCalendarSettings';
const STORAGE_KEY = 'umbra_shared_cal_visibility'; const STORAGE_KEY = 'umbra_shared_cal_visibility';
@ -19,19 +19,25 @@ function saveVisibility(v: Record<number, boolean>) {
} }
interface SharedCalendarSectionProps { interface SharedCalendarSectionProps {
ownedSharedCalendars?: Calendar[];
memberships: SharedCalendarMembership[]; memberships: SharedCalendarMembership[];
visibleSharedIds: Set<number>; visibleSharedIds: Set<number>;
onVisibilityChange: (calendarId: number, visible: boolean) => void; onVisibilityChange: (calendarId: number, visible: boolean) => void;
onEditCalendar?: (calendar: Calendar) => void;
onToggleCalendar?: (calendar: Calendar) => void;
} }
export default function SharedCalendarSection({ export default function SharedCalendarSection({
ownedSharedCalendars = [],
memberships, memberships,
visibleSharedIds, visibleSharedIds,
onVisibilityChange, onVisibilityChange,
onEditCalendar,
onToggleCalendar,
}: SharedCalendarSectionProps) { }: SharedCalendarSectionProps) {
const [settingsFor, setSettingsFor] = useState<SharedCalendarMembership | null>(null); const [settingsFor, setSettingsFor] = useState<SharedCalendarMembership | null>(null);
if (memberships.length === 0) return null; if (memberships.length === 0 && ownedSharedCalendars.length === 0) return null;
return ( return (
<> <>
@ -43,6 +49,36 @@ export default function SharedCalendarSection({
</span> </span>
</div> </div>
<div className="space-y-0.5"> <div className="space-y-0.5">
{ownedSharedCalendars.map((cal) => (
<div
key={`owned-${cal.id}`}
className="group flex items-center gap-2.5 rounded-md px-2 py-1.5 hover:bg-card-elevated transition-colors duration-150"
>
<Checkbox
checked={cal.is_visible}
onChange={() => onToggleCalendar?.(cal)}
className="shrink-0"
style={{
accentColor: cal.color,
borderColor: cal.is_visible ? cal.color : undefined,
backgroundColor: cal.is_visible ? cal.color : undefined,
}}
/>
<span
className="h-2.5 w-2.5 rounded-full shrink-0"
style={{ backgroundColor: cal.color }}
/>
<span className="text-sm text-foreground truncate flex-1">{cal.name}</span>
<span className="text-[10px] text-muted-foreground shrink-0">Owner</span>
<button
type="button"
onClick={() => onEditCalendar?.(cal)}
className="opacity-0 group-hover:opacity-100 transition-opacity duration-150 text-muted-foreground hover:text-foreground"
>
<Pencil className="h-3.5 w-3.5" />
</button>
</div>
))}
{memberships.map((m) => { {memberships.map((m) => {
const color = m.local_color || m.calendar_color; const color = m.local_color || m.calendar_color;
const isVisible = visibleSharedIds.has(m.calendar_id); const isVisible = visibleSharedIds.has(m.calendar_id);