* react compiler * remove compilation mode 'all', it will use infer by default * feat(companion): add theme tokens, path aliases, and component improvements - Extend tailwind.config.js with Cal.com brand color tokens (cal.text, cal.bg, cal.border, cal.accent, cal.brand) - Create theme/colors.ts for JS usage where Tailwind classes can't be used - Configure path aliases in tsconfig.json (@/components, @/hooks, @/utils, etc.) - Add expo-image and expo-haptics dependencies - Enhance AppPressable with Reanimated animations (opacity + scale) and haptic feedback - Extract shared logic from BookingListItem into useBookingListItemData hook and BookingListItemParts - Extract shared logic from EventTypeListItem into useEventTypeListItemData hook and EventTypeListItemParts - Extract shared logic from AvailabilityListItem into AvailabilityListItemParts - Update all list item components to use new theme tokens instead of hardcoded hex values * fix react compiler non memo issue * feat(companion): migrate to path aliases and expo-image - Remove theme/colors.ts and theme/index.ts (defer dark mode to future PR) - Replace colors import with hardcoded hex values in components - Migrate all files to use path aliases (@/components/*, @/hooks/*, etc.) - Migrate SvgImage to use expo-image for better performance - Update tsconfig.json to remove @/theme/* alias * fix(companion): add path aliases to extension tsconfig Add baseUrl and paths configuration to tsconfig.extension.json to support path aliases in shared types used by the extension build.
298 lines
8.6 KiB
TypeScript
298 lines
8.6 KiB
TypeScript
/**
|
|
* Event Types Query Hooks
|
|
*
|
|
* This module provides React Query hooks for fetching and mutating event types.
|
|
* It integrates with the existing CalComAPIService and provides:
|
|
* - Automatic caching with configurable stale times
|
|
* - Pull-to-refresh support via refetch
|
|
* - Optimistic updates for mutations
|
|
* - Cache invalidation on create/update/delete
|
|
*/
|
|
|
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
import { CACHE_CONFIG, queryKeys } from "@/config/cache.config";
|
|
import { CalComAPIService, type CreateEventTypeInput, type EventType } from "@/services/calcom";
|
|
|
|
/**
|
|
* Hook to fetch all event types
|
|
*
|
|
* @returns Query result with event types data, loading state, error, and refetch function
|
|
*
|
|
* @example
|
|
* ```tsx
|
|
* const { data: eventTypes, isLoading, refetch, isRefetching } = useEventTypes();
|
|
*
|
|
* // Pull-to-refresh
|
|
* <RefreshControl refreshing={isRefetching} onRefresh={refetch} />
|
|
* ```
|
|
*/
|
|
export function useEventTypes() {
|
|
return useQuery({
|
|
queryKey: queryKeys.eventTypes.lists(),
|
|
queryFn: () => CalComAPIService.getEventTypes(),
|
|
staleTime: CACHE_CONFIG.eventTypes.staleTime,
|
|
// Keep previous data while fetching new data (smoother UX)
|
|
placeholderData: (previousData) => previousData,
|
|
// Don't retry on network errors (keeps cache intact)
|
|
retry: (failureCount, error) => {
|
|
if (error?.message?.includes("Network") || error?.message?.includes("fetch")) {
|
|
return false;
|
|
}
|
|
return failureCount < 2;
|
|
},
|
|
refetchOnReconnect: true,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Hook to fetch a single event type by ID
|
|
*
|
|
* @param id - The ID of the event type
|
|
* @returns Query result with event type data
|
|
*
|
|
* @example
|
|
* ```tsx
|
|
* const { data: eventType, isLoading } = useEventTypeById(123);
|
|
* ```
|
|
*/
|
|
export function useEventTypeById(id: number | undefined) {
|
|
return useQuery({
|
|
queryKey: queryKeys.eventTypes.detail(id || 0),
|
|
queryFn: () => {
|
|
if (!id) throw new Error("id is required");
|
|
return CalComAPIService.getEventTypeById(id);
|
|
},
|
|
enabled: !!id, // Only fetch when id is provided
|
|
staleTime: CACHE_CONFIG.eventTypes.staleTime,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Hook to create a new event type
|
|
*
|
|
* @returns Mutation function and state
|
|
*
|
|
* @example
|
|
* ```tsx
|
|
* const { mutate: createEventType, isPending } = useCreateEventType();
|
|
*
|
|
* createEventType({
|
|
* title: 'Quick Chat',
|
|
* slug: 'quick-chat',
|
|
* lengthInMinutes: 15,
|
|
* });
|
|
* ```
|
|
*/
|
|
export function useCreateEventType() {
|
|
const queryClient = useQueryClient();
|
|
|
|
return useMutation({
|
|
mutationFn: (input: CreateEventTypeInput) => CalComAPIService.createEventType(input),
|
|
onSuccess: (newEventType) => {
|
|
// Invalidate the list to include the new event type
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.eventTypes.lists() });
|
|
|
|
// Optionally, add the new event type to cache immediately
|
|
queryClient.setQueryData(queryKeys.eventTypes.detail(newEventType.id), newEventType);
|
|
},
|
|
onError: (error) => {
|
|
console.error("Failed to create event type");
|
|
if (__DEV__) {
|
|
const message = error instanceof Error ? error.message : String(error);
|
|
const stack = error instanceof Error ? error.stack : undefined;
|
|
console.debug("[useCreateEventType] failed", { message, stack });
|
|
}
|
|
},
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Hook to update an event type
|
|
*
|
|
* @returns Mutation function and state
|
|
*
|
|
* @example
|
|
* ```tsx
|
|
* const { mutate: updateEventType, isPending } = useUpdateEventType();
|
|
*
|
|
* updateEventType({
|
|
* id: 123,
|
|
* updates: { title: 'Updated Title' }
|
|
* });
|
|
* ```
|
|
*/
|
|
export function useUpdateEventType() {
|
|
const queryClient = useQueryClient();
|
|
|
|
return useMutation({
|
|
mutationFn: ({ id, updates }: { id: number; updates: Partial<CreateEventTypeInput> }) =>
|
|
CalComAPIService.updateEventType(id, updates),
|
|
onSuccess: (updatedEventType, variables) => {
|
|
// Invalidate the list
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.eventTypes.lists() });
|
|
|
|
// Update the specific event type in cache
|
|
queryClient.setQueryData(queryKeys.eventTypes.detail(variables.id), updatedEventType);
|
|
},
|
|
onError: (error) => {
|
|
console.error("Failed to update event type");
|
|
if (__DEV__) {
|
|
const message = error instanceof Error ? error.message : String(error);
|
|
const stack = error instanceof Error ? error.stack : undefined;
|
|
console.debug("[useUpdateEventType] failed", { message, stack });
|
|
}
|
|
},
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Hook to delete an event type
|
|
*
|
|
* @returns Mutation function and state
|
|
*
|
|
* @example
|
|
* ```tsx
|
|
* const { mutate: deleteEventType, isPending } = useDeleteEventType();
|
|
*
|
|
* deleteEventType(123);
|
|
* ```
|
|
*/
|
|
export function useDeleteEventType() {
|
|
const queryClient = useQueryClient();
|
|
|
|
return useMutation({
|
|
mutationFn: (id: number) => CalComAPIService.deleteEventType(id),
|
|
onMutate: async (deletedId) => {
|
|
// Cancel any outgoing refetches
|
|
await queryClient.cancelQueries({ queryKey: queryKeys.eventTypes.lists() });
|
|
|
|
// Snapshot the previous value
|
|
const previousEventTypes = queryClient.getQueryData<EventType[]>(
|
|
queryKeys.eventTypes.lists()
|
|
);
|
|
|
|
// Optimistically remove from the list
|
|
if (previousEventTypes) {
|
|
queryClient.setQueryData(
|
|
queryKeys.eventTypes.lists(),
|
|
previousEventTypes.filter((et) => et.id !== deletedId)
|
|
);
|
|
}
|
|
|
|
return { previousEventTypes };
|
|
},
|
|
onError: (error, _deletedId, context) => {
|
|
// Rollback on error
|
|
if (context?.previousEventTypes) {
|
|
queryClient.setQueryData(queryKeys.eventTypes.lists(), context.previousEventTypes);
|
|
}
|
|
console.error("Failed to delete event type");
|
|
if (__DEV__) {
|
|
const message = error instanceof Error ? error.message : String(error);
|
|
const stack = error instanceof Error ? error.stack : undefined;
|
|
console.debug("[useDeleteEventType] failed", { message, stack });
|
|
}
|
|
},
|
|
onSettled: () => {
|
|
// Always refetch after error or success
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.eventTypes.lists() });
|
|
},
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Hook to duplicate an event type
|
|
*
|
|
* @returns Mutation function and state
|
|
*
|
|
* @example
|
|
* ```tsx
|
|
* const { mutate: duplicateEventType, isPending } = useDuplicateEventType();
|
|
*
|
|
* duplicateEventType({
|
|
* eventType: existingEventType,
|
|
* existingEventTypes: allEventTypes
|
|
* });
|
|
* ```
|
|
*/
|
|
export function useDuplicateEventType() {
|
|
const queryClient = useQueryClient();
|
|
|
|
return useMutation({
|
|
mutationFn: async ({
|
|
eventType,
|
|
existingEventTypes,
|
|
}: {
|
|
eventType: EventType;
|
|
existingEventTypes: EventType[];
|
|
}) => {
|
|
// Generate a new title and slug for the duplicate
|
|
const newTitle = `${eventType.title} (copy)`;
|
|
let newSlug = `${eventType.slug}-copy`;
|
|
|
|
// Check if slug already exists and append a number if needed
|
|
let counter = 1;
|
|
while (existingEventTypes.some((et) => et.slug === newSlug)) {
|
|
newSlug = `${eventType.slug}-copy-${counter}`;
|
|
counter++;
|
|
}
|
|
|
|
const duration = eventType.lengthInMinutes ?? eventType.length ?? 15;
|
|
|
|
return CalComAPIService.createEventType({
|
|
title: newTitle,
|
|
slug: newSlug,
|
|
lengthInMinutes: duration,
|
|
description: eventType.description || undefined,
|
|
});
|
|
},
|
|
onSuccess: () => {
|
|
// Invalidate the list to include the duplicated event type
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.eventTypes.lists() });
|
|
},
|
|
onError: (error) => {
|
|
console.error("Failed to duplicate event type");
|
|
if (__DEV__) {
|
|
const message = error instanceof Error ? error.message : String(error);
|
|
const stack = error instanceof Error ? error.stack : undefined;
|
|
console.debug("[useDuplicateEventType] failed", { message, stack });
|
|
}
|
|
},
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Hook to prefetch event types (useful for navigation)
|
|
*
|
|
* @returns Function to prefetch event types
|
|
*/
|
|
export function usePrefetchEventTypes() {
|
|
const queryClient = useQueryClient();
|
|
|
|
return () => {
|
|
queryClient.prefetchQuery({
|
|
queryKey: queryKeys.eventTypes.lists(),
|
|
queryFn: () => CalComAPIService.getEventTypes(),
|
|
staleTime: CACHE_CONFIG.eventTypes.staleTime,
|
|
});
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Hook to invalidate all event types cache
|
|
*
|
|
* @returns Function to invalidate event types cache
|
|
*/
|
|
export function useInvalidateEventTypes() {
|
|
const queryClient = useQueryClient();
|
|
|
|
return () => {
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.eventTypes.all });
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Type exports for consumers
|
|
*/
|
|
export type { EventType, CreateEventTypeInput };
|