Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 18 additions & 18 deletions companion/app/(tabs)/(event-types)/event-type-detail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -178,7 +184,8 @@ export default function EventTypeDetail() {
const [conferencingLoading, setConferencingLoading] = useState(false);
const [eventTypeData, setEventTypeData] = useState<EventType | null>(null);
const [bookingUrl, setBookingUrl] = useState<string>("");
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);
Expand Down Expand Up @@ -989,7 +996,7 @@ export default function EventTypeDetail() {
}

try {
await CalComAPIService.deleteEventType(eventTypeId);
await deleteEventType(eventTypeId);

showSuccessAlert("Success", "Event type deleted successfully");
router.back();
Expand Down Expand Up @@ -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 = {
Expand All @@ -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
Expand All @@ -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
}
};

Expand Down Expand Up @@ -1331,12 +1331,12 @@ export default function EventTypeDetail() {
{/* Save Button */}
<AppPressable
onPress={handleSave}
disabled={saving || !isDirty}
className={`px-2 py-2 ${saving || !isDirty ? "opacity-50" : ""}`}
disabled={isSaving || !isDirty}
className={`px-2 py-2 ${isSaving || !isDirty ? "opacity-50" : ""}`}
>
<Text
className={`text-[16px] font-semibold ${
saving || !isDirty ? "text-[#C7C7CC]" : "text-[#000000]"
isSaving || !isDirty ? "text-[#C7C7CC]" : "text-[#000000]"
}`}
>
{saveButtonText}
Expand Down Expand Up @@ -1396,7 +1396,7 @@ export default function EventTypeDetail() {
</Stack.Header.Menu>
<Stack.Header.Button
onPress={handleSave}
disabled={saving || !isDirty}
disabled={isSaving || !isDirty}
variant="prominent"
tintColor="#000"
>
Expand Down
69 changes: 63 additions & 6 deletions companion/hooks/useEventTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
*
Expand All @@ -127,14 +127,71 @@ export function useUpdateEventType() {
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() });
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() });

// Snapshot the previous values for rollback
const previousEventType = queryClient.getQueryData<EventType | null>(
queryKeys.eventTypes.detail(id)
);
const previousEventTypes = queryClient.getQueryData<EventType[]>(
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);
}

// Update the specific event type in cache
return { previousEventType, previousEventTypes };
},
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<EventType[]>(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) => {
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);
Expand Down