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:
parent
e5690625eb
commit
f45b7a2115
@ -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) {
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{pendingInvite ? (
|
||||
<div className="rounded-md border border-border bg-card-elevated p-3 space-y-2 animate-fade-in">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-foreground">
|
||||
{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={handleInvite}
|
||||
onSelect={handleSelectConnection}
|
||||
isLoading={isInviting}
|
||||
/>
|
||||
)}
|
||||
|
||||
<CalendarMemberList
|
||||
members={members}
|
||||
|
||||
@ -106,9 +106,9 @@ export default function CalendarSidebar({ onUseTemplate, onSharedVisibilityChang
|
||||
</Button>
|
||||
</div>
|
||||
<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">
|
||||
{calendars.map((cal) => (
|
||||
{calendars.filter((c) => !c.is_shared).map((cal) => (
|
||||
<div
|
||||
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"
|
||||
@ -138,12 +138,17 @@ export default function CalendarSidebar({ onUseTemplate, onSharedVisibilityChang
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Shared calendars section */}
|
||||
{/* Shared calendars section -- owned + member */}
|
||||
{(calendars.some((c) => c.is_shared) || sharedCalendars.length > 0) && (
|
||||
<SharedCalendarSection
|
||||
ownedSharedCalendars={calendars.filter((c) => c.is_shared)}
|
||||
memberships={sharedCalendars}
|
||||
visibleSharedIds={visibleSharedIds}
|
||||
onVisibilityChange={handleSharedVisibilityChange}
|
||||
onEditCalendar={handleEdit}
|
||||
onToggleCalendar={handleToggle}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Templates section */}
|
||||
<div className="space-y-1.5">
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -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<number, boolean>) {
|
||||
}
|
||||
|
||||
interface SharedCalendarSectionProps {
|
||||
ownedSharedCalendars?: Calendar[];
|
||||
memberships: SharedCalendarMembership[];
|
||||
visibleSharedIds: Set<number>;
|
||||
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<SharedCalendarMembership | null>(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({
|
||||
</span>
|
||||
</div>
|
||||
<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) => {
|
||||
const color = m.local_color || m.calendar_color;
|
||||
const isVisible = visibleSharedIds.has(m.calendar_id);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user