diff --git a/companion/app/(tabs)/(event-types)/event-type-detail.tsx b/companion/app/(tabs)/(event-types)/event-type-detail.tsx index 78552bdfa2..6130b535bd 100644 --- a/companion/app/(tabs)/(event-types)/event-type-detail.tsx +++ b/companion/app/(tabs)/(event-types)/event-type-detail.tsx @@ -34,6 +34,7 @@ import { type EventType, type Schedule, } from "@/services/calcom"; +import { useCreateEventType, useDeleteEventType, useUpdateEventType } from "@/hooks"; import type { LocationItem, LocationOptionGroup } from "@/types/locations"; import { showErrorAlert, showInfoAlert, showSuccessAlert } from "@/utils/alerts"; import { openInAppBrowser } from "@/utils/browser"; @@ -140,6 +141,11 @@ export default function EventTypeDetail() { slug?: string; }>(); + // Mutation hooks for optimistic updates + const { mutateAsync: updateEventType, isPending: isUpdating } = useUpdateEventType(); + const { mutateAsync: createEventType, isPending: isCreating } = useCreateEventType(); + const { mutateAsync: deleteEventType } = useDeleteEventType(); + const [activeTab, setActiveTab] = useState("basics"); // Form state @@ -178,7 +184,8 @@ export default function EventTypeDetail() { const [conferencingLoading, setConferencingLoading] = useState(false); const [eventTypeData, setEventTypeData] = useState(null); const [bookingUrl, setBookingUrl] = useState(""); - const [saving, setSaving] = useState(false); + // Use mutation hooks' isPending states instead of local saving state + const isSaving = isUpdating || isCreating; const [beforeEventBuffer, setBeforeEventBuffer] = useState("No buffer time"); const [afterEventBuffer, setAfterEventBuffer] = useState("No buffer time"); const [showBeforeBufferDropdown, setShowBeforeBufferDropdown] = useState(false); @@ -989,7 +996,7 @@ export default function EventTypeDetail() { } try { - await CalComAPIService.deleteEventType(eventTypeId); + await deleteEventType(eventTypeId); showSuccessAlert("Success", "Event type deleted successfully"); router.back(); @@ -1202,8 +1209,6 @@ export default function EventTypeDetail() { // Extract values with optional chaining outside try/catch for React Compiler const selectedScheduleId = selectedSchedule?.id; - setSaving(true); - if (isCreateMode) { // For CREATE mode, build full payload const payload: CreateEventTypePayload = { @@ -1226,18 +1231,16 @@ export default function EventTypeDetail() { payload.hidden = isHidden; - // Create new event type + // Create new event type using mutation hook try { - await CalComAPIService.createEventType(payload); + await createEventType(payload); } catch (error) { safeLogError("Failed to save event type:", error); showErrorAlert("Error", "Failed to create event type. Please try again."); - setSaving(false); return; } showSuccessAlert("Success", "Event type created successfully"); router.back(); - setSaving(false); } else { // For UPDATE mode, use partial update - only send changed fields // Using the memoized currentFormState @@ -1246,22 +1249,19 @@ export default function EventTypeDetail() { if (Object.keys(payload).length === 0) { // This should theoretically strictly not be reached if button is disabled, // but it acts as a safeguard. - // setSaving(false); // return; } + // Update event type using mutation hook with optimistic updates try { - await CalComAPIService.updateEventType(parseInt(id, 10), payload); + await updateEventType({ id: parseInt(id, 10), updates: payload }); } catch (error) { safeLogError("Failed to save event type:", error); showErrorAlert("Error", "Failed to update event type. Please try again."); - setSaving(false); return; } showSuccessAlert("Success", "Event type updated successfully"); - // Refresh event type data to sync with server - await fetchEventTypeData(); - setSaving(false); + // No need to manually refresh - cache is updated by the mutation hook } }; @@ -1331,12 +1331,12 @@ export default function EventTypeDetail() { {/* Save Button */} {saveButtonText} @@ -1396,7 +1396,7 @@ export default function EventTypeDetail() { diff --git a/companion/hooks/useEventTypes.ts b/companion/hooks/useEventTypes.ts index b97e303409..f06a0da838 100644 --- a/companion/hooks/useEventTypes.ts +++ b/companion/hooks/useEventTypes.ts @@ -107,7 +107,7 @@ export function useCreateEventType() { } /** - * Hook to update an event type + * Hook to update an event type with optimistic updates * * @returns Mutation function and state * @@ -125,16 +125,73 @@ export function useUpdateEventType() { const queryClient = useQueryClient(); return useMutation({ - mutationFn: ({ id, updates }: { id: number; updates: Partial }) => + mutationFn: ({ id, updates }: { id: number; updates: Record }) => CalComAPIService.updateEventType(id, updates), - onSuccess: (updatedEventType, variables) => { - // Invalidate the list - queryClient.invalidateQueries({ queryKey: queryKeys.eventTypes.lists() }); + onMutate: async ({ id, updates }) => { + // Cancel any outgoing refetches to prevent overwriting optimistic update + await queryClient.cancelQueries({ queryKey: queryKeys.eventTypes.detail(id) }); + await queryClient.cancelQueries({ queryKey: queryKeys.eventTypes.lists() }); - // Update the specific event type in cache - queryClient.setQueryData(queryKeys.eventTypes.detail(variables.id), updatedEventType); + // Snapshot the previous values for rollback + const previousEventType = queryClient.getQueryData( + queryKeys.eventTypes.detail(id) + ); + const previousEventTypes = queryClient.getQueryData( + queryKeys.eventTypes.lists() + ); + + // Optimistically update the detail cache if it exists + if (previousEventType) { + const optimisticEventType: EventType = { + ...previousEventType, + ...updates, + }; + queryClient.setQueryData(queryKeys.eventTypes.detail(id), optimisticEventType); + } + + // Update the list cache optimistically (even if detail cache doesn't exist) + if (previousEventTypes) { + const updatedList = previousEventTypes.map((et) => { + if (et.id === id) { + return { + ...et, + ...updates, + }; + } + return et; + }); + queryClient.setQueryData(queryKeys.eventTypes.lists(), updatedList); + } + + return { previousEventType, previousEventTypes }; }, - onError: (error) => { + onSuccess: (updatedEventType, variables) => { + // Update the specific event type in cache with server response + queryClient.setQueryData(queryKeys.eventTypes.detail(variables.id), updatedEventType); + + // Update the list cache with the server response + const currentEventTypes = queryClient.getQueryData(queryKeys.eventTypes.lists()); + if (currentEventTypes) { + const updatedList = currentEventTypes.map((et) => + et.id === variables.id ? updatedEventType : et + ); + queryClient.setQueryData(queryKeys.eventTypes.lists(), updatedList); + } else { + // If list cache doesn't exist, invalidate to trigger refetch when user navigates to list + queryClient.invalidateQueries({ queryKey: queryKeys.eventTypes.lists() }); + } + }, + onError: (error, variables, context) => { + // Rollback to previous values on error + if (context?.previousEventType) { + queryClient.setQueryData( + queryKeys.eventTypes.detail(variables.id), + context.previousEventType + ); + } + if (context?.previousEventTypes) { + queryClient.setQueryData(queryKeys.eventTypes.lists(), context.previousEventTypes); + } console.error("Failed to update event type"); if (__DEV__) { const message = error instanceof Error ? error.message : String(error);