Fix member removal bug + QA fixes + shared calendar sidebar styling

Bug fix:
- CalendarMemberRow: add type="button" to remove button (was submitting parent form)

QA fixes:
- EventDetailPanel: use axios.isAxiosError() instead of duck-typing for lock errors
- EventDetailPanel: only call onSaved on create (edits return to view mode, not close)
- CalendarForm: remove 4 redundant membersQuery.refetch() calls (mutations already invalidate)
- useEventLock: remove unused lockHeld ref from return, fix stale eventId in onSuccess
- EventLockBanner: guard against invalid date parse

UI:
- SharedCalendarSection: add purple Ghost icon next to "SHARED CALENDARS" header

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Kyle 2026-03-06 06:07:55 +08:00
parent eedfaaf859
commit e5690625eb
6 changed files with 18 additions and 22 deletions

View File

@ -101,25 +101,21 @@ export default function CalendarForm({ calendar, onClose }: CalendarFormProps) {
permission: 'read_only', permission: 'read_only',
canAddOthers: false, canAddOthers: false,
}); });
membersQuery.refetch();
}; };
const handleUpdatePermission = async (memberId: number, permission: CalendarPermission) => { const handleUpdatePermission = async (memberId: number, permission: CalendarPermission) => {
if (!calendar) return; if (!calendar) return;
await updateMember({ calendarId: calendar.id, memberId, permission }); await updateMember({ calendarId: calendar.id, memberId, permission });
membersQuery.refetch();
}; };
const handleUpdateCanAddOthers = async (memberId: number, canAddOthers: boolean) => { const handleUpdateCanAddOthers = async (memberId: number, canAddOthers: boolean) => {
if (!calendar) return; if (!calendar) return;
await updateMember({ calendarId: calendar.id, memberId, canAddOthers }); await updateMember({ calendarId: calendar.id, memberId, canAddOthers });
membersQuery.refetch();
}; };
const handleRemoveMember = async (memberId: number) => { const handleRemoveMember = async (memberId: number) => {
if (!calendar) return; if (!calendar) return;
await removeMember({ calendarId: calendar.id, memberId }); await removeMember({ calendarId: calendar.id, memberId });
membersQuery.refetch();
}; };
const canDelete = calendar && !calendar.is_default && !calendar.is_system; const canDelete = calendar && !calendar.is_default && !calendar.is_system;

View File

@ -79,6 +79,7 @@ export default function CalendarMemberRow({
)} )}
<button <button
type="button"
onClick={handleRemoveClick} onClick={handleRemoveClick}
className="text-muted-foreground hover:text-destructive transition-colors" className="text-muted-foreground hover:text-destructive transition-colors"
title={confirming ? 'Click again to confirm' : 'Remove member'} title={confirming ? 'Click again to confirm' : 'Remove member'}

View File

@ -5,6 +5,7 @@ import { format, parseISO } from 'date-fns';
import { import {
X, Pencil, Trash2, Save, Clock, MapPin, AlignLeft, Repeat, Star, Calendar, Loader2, X, Pencil, Trash2, Save, Clock, MapPin, AlignLeft, Repeat, Star, Calendar, Loader2,
} from 'lucide-react'; } from 'lucide-react';
import axios from 'axios';
import api, { getErrorMessage } from '@/lib/api'; import api, { getErrorMessage } from '@/lib/api';
import type { CalendarEvent, Location as LocationType, RecurrenceRule, CalendarPermission, EventLockInfo } from '@/types'; import type { CalendarEvent, Location as LocationType, RecurrenceRule, CalendarPermission, EventLockInfo } from '@/types';
import { useCalendars } from '@/hooks/useCalendars'; import { useCalendars } from '@/hooks/useCalendars';
@ -328,11 +329,11 @@ export default function EventDetailPanel({
toast.success(isCreating ? 'Event created' : 'Event updated'); toast.success(isCreating ? 'Event created' : 'Event updated');
if (isCreating) { if (isCreating) {
onClose(); onClose();
onSaved?.();
} else { } else {
setIsEditing(false); setIsEditing(false);
setEditScope(null); setEditScope(null);
} }
onSaved?.();
}, },
onError: (error) => { onError: (error) => {
toast.error(getErrorMessage(error, isCreating ? 'Failed to create event' : 'Failed to update event')); toast.error(getErrorMessage(error, isCreating ? 'Failed to create event' : 'Failed to update event'));
@ -367,18 +368,16 @@ export default function EventDetailPanel({
await acquireLock(); await acquireLock();
setLockInfo(null); setLockInfo(null);
} catch (err: unknown) { } catch (err: unknown) {
if (err && typeof err === 'object' && 'response' in err) { if (axios.isAxiosError(err) && err.response?.status === 423) {
const axErr = err as { response?: { status?: number; data?: { detail?: string; locked_by_name?: string; expires_at?: string; is_permanent?: boolean } } }; const data = err.response.data as { locked_by_name?: string; expires_at?: string; is_permanent?: boolean } | undefined;
if (axErr.response?.status === 423) {
setLockInfo({ setLockInfo({
locked: true, locked: true,
locked_by_name: axErr.response.data?.locked_by_name || 'another user', locked_by_name: data?.locked_by_name || 'another user',
expires_at: axErr.response.data?.expires_at || null, expires_at: data?.expires_at || null,
is_permanent: axErr.response.data?.is_permanent || false, is_permanent: data?.is_permanent || false,
}); });
return; return;
} }
}
toast.error('Failed to acquire edit lock'); toast.error('Failed to acquire edit lock');
return; return;
} }

View File

@ -20,7 +20,7 @@ export default function EventLockBanner({ lockedByName, expiresAt, isPermanent =
</p> </p>
{!isPermanent && expiresAt && ( {!isPermanent && expiresAt && (
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
Lock expires at {format(parseISO(expiresAt), 'h:mm a')} Lock expires at {(() => { try { return format(parseISO(expiresAt), 'h:mm a'); } catch { return 'unknown'; } })()}
</p> </p>
)} )}
</div> </div>

View File

@ -1,5 +1,5 @@
import { useState } from 'react'; import { useState } from 'react';
import { 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 { SharedCalendarMembership } from '@/types';
import SharedCalendarSettings from './SharedCalendarSettings'; import SharedCalendarSettings from './SharedCalendarSettings';
@ -36,7 +36,8 @@ export default function SharedCalendarSection({
return ( return (
<> <>
<div className="space-y-1.5"> <div className="space-y-1.5">
<div className="px-2"> <div className="flex items-center gap-1.5 px-2">
<Ghost className="h-3.5 w-3.5 text-violet-400 shrink-0" />
<span className="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground"> <span className="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground">
Shared Calendars Shared Calendars
</span> </span>

View File

@ -14,9 +14,9 @@ export function useEventLock(eventId: number | null) {
); );
return data; return data;
}, },
onSuccess: () => { onSuccess: (_data, lockedId) => {
lockHeldRef.current = true; lockHeldRef.current = true;
activeEventIdRef.current = eventId; activeEventIdRef.current = lockedId;
}, },
}); });
@ -64,6 +64,5 @@ export function useEventLock(eventId: number | null) {
release, release,
isAcquiring: acquireMutation.isPending, isAcquiring: acquireMutation.isPending,
acquireError: acquireMutation.error, acquireError: acquireMutation.error,
lockHeld: lockHeldRef.current,
}; };
} }