UMBRA/frontend/src/hooks/useConnections.ts
Kyle Pope 4e3fd35040 Phase 3: Frontend core for shared calendars
Types: CalendarPermission, SharedCalendarMembership, CalendarMemberInfo,
CalendarInvite, EventLockInfo. Calendar type extended with is_shared.

Hooks: useCalendars extended with shared calendar polling (5s).
useSharedCalendars for member CRUD, invite responses, color updates.
useConnections cascade invalidation on disconnect.

New components: PermissionBadge, CalendarMemberSearch,
CalendarMemberRow, CalendarMemberList, SharedCalendarSection,
SharedCalendarSettings (non-owner dialog with color, members, leave).

Modified: CalendarForm (sharing toggle, member management for owners),
CalendarSidebar (shared calendars section with localStorage visibility),
CalendarPage (shared calendar ID integration in event filtering),
NotificationToaster (calendar_invite toast with accept/decline),
NotificationsPage (calendar_invite inline actions + type icons).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 04:59:13 +08:00

113 lines
4.0 KiB
TypeScript

import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { toast } from 'sonner';
import api from '@/lib/api';
import type { Connection, ConnectionRequest, UmbralSearchResponse } from '@/types';
export function useConnections() {
const queryClient = useQueryClient();
const connectionsQuery = useQuery({
queryKey: ['connections'],
queryFn: async () => {
const { data } = await api.get<Connection[]>('/connections');
return data;
},
});
const incomingQuery = useQuery({
queryKey: ['connections', 'incoming'],
queryFn: async () => {
const { data } = await api.get<ConnectionRequest[]>('/connections/requests/incoming');
return data;
},
refetchOnMount: 'always',
});
const outgoingQuery = useQuery({
queryKey: ['connections', 'outgoing'],
queryFn: async () => {
const { data } = await api.get<ConnectionRequest[]>('/connections/requests/outgoing');
return data;
},
});
const searchMutation = useMutation({
mutationFn: async (umbralName: string) => {
const { data } = await api.post<UmbralSearchResponse>('/connections/search', {
umbral_name: umbralName,
});
return data;
},
});
const sendRequestMutation = useMutation({
mutationFn: async (params: { umbralName: string; personId?: number }) => {
const { data } = await api.post('/connections/request', {
umbral_name: params.umbralName,
...(params.personId != null && { person_id: params.personId }),
});
return data;
},
onSuccess: () => {
// Fire-and-forget — don't block mutateAsync on query refetches
queryClient.invalidateQueries({ queryKey: ['connections'] });
},
});
const respondMutation = useMutation({
mutationFn: async ({ requestId, action }: { requestId: number; action: 'accept' | 'reject' }) => {
const { data } = await api.put(`/connections/requests/${requestId}/respond`, { action });
return data;
},
onSuccess: (_, variables) => {
// Dismiss any lingering Sonner toast for this request
toast.dismiss(`connection-request-${variables.requestId}`);
// Fire-and-forget — errors here must not surface as mutation failures
queryClient.invalidateQueries({ queryKey: ['connections'] });
queryClient.invalidateQueries({ queryKey: ['people'] });
queryClient.invalidateQueries({ queryKey: ['notifications'] });
},
});
const cancelMutation = useMutation({
mutationFn: async (requestId: number) => {
const { data } = await api.put(`/connections/requests/${requestId}/cancel`);
return data;
},
onSuccess: () => {
// Fire-and-forget — don't block mutateAsync on query refetches
queryClient.invalidateQueries({ queryKey: ['connections'] });
queryClient.invalidateQueries({ queryKey: ['notifications'] });
},
});
const removeConnectionMutation = useMutation({
mutationFn: async (connectionId: number) => {
await api.delete(`/connections/${connectionId}`);
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['connections'] });
queryClient.invalidateQueries({ queryKey: ['people'] });
queryClient.invalidateQueries({ queryKey: ['calendars', 'shared'] });
queryClient.invalidateQueries({ queryKey: ['calendar-events'] });
},
});
return {
connections: connectionsQuery.data ?? [],
incomingRequests: incomingQuery.data ?? [],
outgoingRequests: outgoingQuery.data ?? [],
isLoading: connectionsQuery.isLoading,
isLoadingIncoming: incomingQuery.isLoading,
search: searchMutation.mutateAsync,
isSearching: searchMutation.isPending,
sendRequest: sendRequestMutation.mutateAsync,
isSending: sendRequestMutation.isPending,
respond: respondMutation.mutateAsync,
isResponding: respondMutation.isPending,
cancelRequest: cancelMutation.mutateAsync,
isCancelling: cancelMutation.isPending,
removeConnection: removeConnectionMutation.mutateAsync,
};
}