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 { 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>
|
||||||
|
|
||||||
|
{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
|
<CalendarMemberSearch
|
||||||
connections={connections}
|
connections={connections}
|
||||||
existingMembers={members}
|
existingMembers={members}
|
||||||
onSelect={handleInvite}
|
onSelect={handleSelectConnection}
|
||||||
isLoading={isInviting}
|
isLoading={isInviting}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<CalendarMemberList
|
<CalendarMemberList
|
||||||
members={members}
|
members={members}
|
||||||
|
|||||||
@ -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 */}
|
||||||
|
{(calendars.some((c) => c.is_shared) || sharedCalendars.length > 0) && (
|
||||||
<SharedCalendarSection
|
<SharedCalendarSection
|
||||||
|
ownedSharedCalendars={calendars.filter((c) => c.is_shared)}
|
||||||
memberships={sharedCalendars}
|
memberships={sharedCalendars}
|
||||||
visibleSharedIds={visibleSharedIds}
|
visibleSharedIds={visibleSharedIds}
|
||||||
onVisibilityChange={handleSharedVisibilityChange}
|
onVisibilityChange={handleSharedVisibilityChange}
|
||||||
|
onEditCalendar={handleEdit}
|
||||||
|
onToggleCalendar={handleToggle}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Templates section */}
|
{/* Templates section */}
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
|
|||||||
@ -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({
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user