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:
parent
eedfaaf859
commit
e5690625eb
@ -101,25 +101,21 @@ export default function CalendarForm({ calendar, onClose }: CalendarFormProps) {
|
||||
permission: 'read_only',
|
||||
canAddOthers: false,
|
||||
});
|
||||
membersQuery.refetch();
|
||||
};
|
||||
|
||||
const handleUpdatePermission = async (memberId: number, permission: CalendarPermission) => {
|
||||
if (!calendar) return;
|
||||
await updateMember({ calendarId: calendar.id, memberId, permission });
|
||||
membersQuery.refetch();
|
||||
};
|
||||
|
||||
const handleUpdateCanAddOthers = async (memberId: number, canAddOthers: boolean) => {
|
||||
if (!calendar) return;
|
||||
await updateMember({ calendarId: calendar.id, memberId, canAddOthers });
|
||||
membersQuery.refetch();
|
||||
};
|
||||
|
||||
const handleRemoveMember = async (memberId: number) => {
|
||||
if (!calendar) return;
|
||||
await removeMember({ calendarId: calendar.id, memberId });
|
||||
membersQuery.refetch();
|
||||
};
|
||||
|
||||
const canDelete = calendar && !calendar.is_default && !calendar.is_system;
|
||||
|
||||
@ -79,6 +79,7 @@ export default function CalendarMemberRow({
|
||||
)}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleRemoveClick}
|
||||
className="text-muted-foreground hover:text-destructive transition-colors"
|
||||
title={confirming ? 'Click again to confirm' : 'Remove member'}
|
||||
|
||||
@ -5,6 +5,7 @@ import { format, parseISO } from 'date-fns';
|
||||
import {
|
||||
X, Pencil, Trash2, Save, Clock, MapPin, AlignLeft, Repeat, Star, Calendar, Loader2,
|
||||
} from 'lucide-react';
|
||||
import axios from 'axios';
|
||||
import api, { getErrorMessage } from '@/lib/api';
|
||||
import type { CalendarEvent, Location as LocationType, RecurrenceRule, CalendarPermission, EventLockInfo } from '@/types';
|
||||
import { useCalendars } from '@/hooks/useCalendars';
|
||||
@ -328,11 +329,11 @@ export default function EventDetailPanel({
|
||||
toast.success(isCreating ? 'Event created' : 'Event updated');
|
||||
if (isCreating) {
|
||||
onClose();
|
||||
onSaved?.();
|
||||
} else {
|
||||
setIsEditing(false);
|
||||
setEditScope(null);
|
||||
}
|
||||
onSaved?.();
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(getErrorMessage(error, isCreating ? 'Failed to create event' : 'Failed to update event'));
|
||||
@ -367,18 +368,16 @@ export default function EventDetailPanel({
|
||||
await acquireLock();
|
||||
setLockInfo(null);
|
||||
} catch (err: unknown) {
|
||||
if (err && typeof err === 'object' && 'response' in err) {
|
||||
const axErr = err as { response?: { status?: number; data?: { detail?: string; locked_by_name?: string; expires_at?: string; is_permanent?: boolean } } };
|
||||
if (axErr.response?.status === 423) {
|
||||
if (axios.isAxiosError(err) && err.response?.status === 423) {
|
||||
const data = err.response.data as { locked_by_name?: string; expires_at?: string; is_permanent?: boolean } | undefined;
|
||||
setLockInfo({
|
||||
locked: true,
|
||||
locked_by_name: axErr.response.data?.locked_by_name || 'another user',
|
||||
expires_at: axErr.response.data?.expires_at || null,
|
||||
is_permanent: axErr.response.data?.is_permanent || false,
|
||||
locked_by_name: data?.locked_by_name || 'another user',
|
||||
expires_at: data?.expires_at || null,
|
||||
is_permanent: data?.is_permanent || false,
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
toast.error('Failed to acquire edit lock');
|
||||
return;
|
||||
}
|
||||
|
||||
@ -20,7 +20,7 @@ export default function EventLockBanner({ lockedByName, expiresAt, isPermanent =
|
||||
</p>
|
||||
{!isPermanent && expiresAt && (
|
||||
<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>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { useState } from 'react';
|
||||
import { Pencil } from 'lucide-react';
|
||||
import { Ghost, Pencil } from 'lucide-react';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import type { SharedCalendarMembership } from '@/types';
|
||||
import SharedCalendarSettings from './SharedCalendarSettings';
|
||||
@ -36,7 +36,8 @@ export default function SharedCalendarSection({
|
||||
return (
|
||||
<>
|
||||
<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">
|
||||
Shared Calendars
|
||||
</span>
|
||||
|
||||
@ -14,9 +14,9 @@ export function useEventLock(eventId: number | null) {
|
||||
);
|
||||
return data;
|
||||
},
|
||||
onSuccess: () => {
|
||||
onSuccess: (_data, lockedId) => {
|
||||
lockHeldRef.current = true;
|
||||
activeEventIdRef.current = eventId;
|
||||
activeEventIdRef.current = lockedId;
|
||||
},
|
||||
});
|
||||
|
||||
@ -64,6 +64,5 @@ export function useEventLock(eventId: number | null) {
|
||||
release,
|
||||
isAcquiring: acquireMutation.isPending,
|
||||
acquireError: acquireMutation.error,
|
||||
lockHeld: lockHeldRef.current,
|
||||
};
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user