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',
|
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;
|
||||||
|
|||||||
@ -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'}
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user