Files
cal.diy/companion/hooks/useBookingActionModals.ts
T
Dhairyashil Shinde 2a84d46fe9 feat(companion): add Biome for linting and formatting (#26214)
* feat(companion): add ESLint with Prettier integration

- Add ESLint with eslint-config-expo/flat for SDK 55
- Integrate Prettier via eslint-plugin-prettier
- Configure environment-specific settings for Node.js config files and browser extension
- Add lint script to package.json
- Fix Prettier formatting issues in AvailabilityTab.tsx and types/index.ts

* fix(companion): use bun lockfile instead of npm

- Remove package-lock.json that was accidentally committed
- Update bun.lock with ESLint dependencies

* fix(companion): fix all ESLint errors (16 total)

- Fix unescaped entities in JSX (react/no-unescaped-entities):
  - app/(tabs)/(event-types)/index.tsx: escape quotes in delete confirmation
  - components/LoginScreen.tsx: escape apostrophe in "Don't have an account"
  - components/NetworkStatusBanner.tsx: escape apostrophes in offline message
  - components/booking-modals/BookingModals.tsx: escape apostrophe in rejection message
  - components/event-type-detail/tabs/AdvancedTab.tsx: escape quotes and apostrophes
  - components/screens/AvailabilityListScreen.tsx: escape quotes in delete confirmation

- Fix react-hooks/rules-of-hooks in RescheduleScreen.tsx:
  - Move useMemo calls before conditional early return to ensure hooks
    are called in the same order on every render

* fix(companion): fix all ESLint warnings (68 total)

- Fix import/no-named-as-default warnings by using named imports
- Fix @typescript-eslint/no-unused-vars warnings by removing unused imports/variables
- Fix react-hooks/exhaustive-deps warnings with eslint-disable comments
- Convert unused error variables in catch blocks to bare catch
- Remove unused imports (useRouter, Alert, useSafeAreaInsets, etc.)
- All ESLint checks now pass with 0 errors and 0 warnings

* feat(companion): add pre-commit lint check with --max-warnings=0

- Add companion lint check to root lint-staged.config.mjs
- Runs 'bun run lint -- --fix --max-warnings=0' for companion files
- Follows same pattern as apps/packages lint check
- Supports SKIP_WARNINGS=1 env var to bypass warning check if needed
- Commit will fail if any ESLint errors or warnings are present

* fix(companion): restore state variables and imports removed during ESLint fixes

- Restore locationAddress, locationLink, locationPhone state variables in event-type-detail.tsx (still used in fetchEventTypeData)
- Restore Alert import in BookingListScreen.tsx (still used for report booking)
- Add eslint-disable comments for the restored state variables since they're only used by setters

* test

* test

* fix eslint pre commit check

* fix: In React Native, HTML entities like ' are rendered literally as text, not decoded like in web browsers

* refactor(companion): remove ESLint and Prettier config in preparation for Biome

* feat(companion): add Biome for linting and formatting

- Install @biomejs/biome with exact version pinning (2.3.10)
- Add biome.json with formatting settings matching previous Prettier config
- Add lint, format, check, and check:ci scripts to package.json
- Update lint-staged to use Biome instead of Prettier
- Remove Prettier and prettier-plugin-tailwindcss dependencies
- Delete .prettierrc.js configuration file

* style(companion): apply Biome formatting and fix lint-staged config

- Update lint-staged to use 'biome format' instead of 'biome check'
  (pre-commit should only format, not lint)
- Apply Biome formatting to all companion files

* feat(companion): configure Biome for strict linting with pre-commit enforcement

- Apply Biome unsafe fixes (unused imports, optional chaining, etc.)
- Configure biome.json rules to disable overly strict rules for existing code:
  - noExplicitAny, noArrayIndexKey, useIterableCallbackReturn (suspicious)
  - useExhaustiveDependencies, noUnusedFunctionParameters (correctness)
  - noStaticOnlyClass (complexity)
  - noNonNullAssertion (style)
  - noStaticElementInteractions (a11y)
- Update lint-staged to use 'biome check --write --error-on-warnings'
  (pre-commit now runs both formatting AND linting, fails on warnings)

* fix(companion): enable noNonNullAssertion rule and fix all violations

- Enable noNonNullAssertion rule in biome.json (set to 'error')
- Fix 7 violations across the codebase:
  - hooks/useBookings.ts: Add runtime check before API call
  - hooks/useEventTypes.ts: Add runtime check before API call
  - hooks/useSchedules.ts: Add runtime check before API call
  - extension/entrypoints/content.ts: Add null checks for cache and getAttribute

* fix(companion): enable noStaticOnlyClass and noStaticElementInteractions rules

- Enable noStaticOnlyClass rule in biome.json
- Enable noStaticElementInteractions rule in biome.json
- Convert CalComAPIService from static class to object literal pattern
- Convert WebAuthService from static class to object literal pattern
- Add role="tooltip" to Tooltip.web.tsx for accessibility

* fix(companion): resolve TypeScript type errors in useEffect dependencies and remove unused state property

* fix(companion): enable noUnusedFunctionParameters rule and fix 9 violations

* fix(companion): enable noUnusedVariables rule and fix 4 violations

* fix(companion): enable useIterableCallbackReturn rule and fix 11 violations

* fix(companion): enable useExhaustiveDependencies rule and fix 7 violations

* fix(companion): enable noArrayIndexKey rule and fix 10 violations

* fix(companion): enable noExplicitAny rule and fix all violations

- Enable noExplicitAny rule in biome.json
- Replace any types with proper specific types across all files:
  - calcom.ts: Add proper type definitions for API responses and inputs
  - event-types.types.ts: Add BookingLimitsCount, BookingLimitsDuration, ConfirmationPolicy types
  - buildPartialUpdatePayload.ts: Fix type mismatches for booking limits functions
  - booking-actions.ts: Add proper types for booking action handlers
  - BookingDetailScreen.tsx, MarkNoShowScreen.tsx: Add types for attendee data
  - AvailabilityListItem.ios.tsx, BookingListItem.ios.tsx: Import SFSymbols7_0 type
  - extension/content.ts: Fix slot types and __cleanup invocation issues
- Remove unused imports and interfaces

* address cubics comments

* add useHookAtTopLevel rule
2025-12-27 08:19:53 -03:00

339 lines
11 KiB
TypeScript

/**
* useBookingActionModals Hook
*
* Centralized hook for managing all booking action modals and their API calls.
* This hook is designed to be used across iOS, Android, and extension platforms.
*/
import { useQueryClient } from "@tanstack/react-query";
import { useCallback, useState } from "react";
import { Alert, Linking } from "react-native";
import { queryKeys } from "../config/cache.config";
import { type Booking, CalComAPIService } from "../services/calcom";
import type {
AddGuestInput,
BookingRecording,
ConferencingSession,
} from "../services/types/bookings.types";
import { showErrorAlert } from "../utils/alerts";
interface UseBookingActionModalsReturn {
// Selected booking for actions
selectedBooking: Booking | null;
setSelectedBooking: (booking: Booking | null) => void;
// Add Guests Modal
showAddGuestsModal: boolean;
isAddingGuests: boolean;
openAddGuestsModal: (booking: Booking) => void;
closeAddGuestsModal: () => void;
handleAddGuests: (guests: AddGuestInput[]) => Promise<void>;
// Edit Location Modal
showEditLocationModal: boolean;
isUpdatingLocation: boolean;
openEditLocationModal: (booking: Booking) => void;
closeEditLocationModal: () => void;
handleUpdateLocation: (location: string) => Promise<void>;
// View Recordings Modal
showViewRecordingsModal: boolean;
isLoadingRecordings: boolean;
recordings: BookingRecording[];
openViewRecordingsModal: (booking: Booking) => void;
closeViewRecordingsModal: () => void;
// Meeting Session Details Modal
showMeetingSessionDetailsModal: boolean;
isLoadingSessions: boolean;
sessions: ConferencingSession[];
openMeetingSessionDetailsModal: (booking: Booking) => void;
closeMeetingSessionDetailsModal: () => void;
// Mark No-Show Modal
showMarkNoShowModal: boolean;
isMarkingNoShow: boolean;
openMarkNoShowModal: (booking: Booking) => void;
closeMarkNoShowModal: () => void;
handleMarkNoShow: (attendeeEmail: string, absent: boolean) => Promise<void>;
// Request Reschedule (deep link to web)
handleRequestReschedule: (booking: Booking) => void;
}
export function useBookingActionModals(): UseBookingActionModalsReturn {
const queryClient = useQueryClient();
// Selected booking state
const [selectedBooking, setSelectedBooking] = useState<Booking | null>(null);
// Add Guests Modal state
const [showAddGuestsModal, setShowAddGuestsModal] = useState(false);
const [isAddingGuests, setIsAddingGuests] = useState(false);
// Edit Location Modal state
const [showEditLocationModal, setShowEditLocationModal] = useState(false);
const [isUpdatingLocation, setIsUpdatingLocation] = useState(false);
// View Recordings Modal state
const [showViewRecordingsModal, setShowViewRecordingsModal] = useState(false);
const [isLoadingRecordings, setIsLoadingRecordings] = useState(false);
const [recordings, setRecordings] = useState<BookingRecording[]>([]);
// Meeting Session Details Modal state
const [showMeetingSessionDetailsModal, setShowMeetingSessionDetailsModal] = useState(false);
const [isLoadingSessions, setIsLoadingSessions] = useState(false);
const [sessions, setSessions] = useState<ConferencingSession[]>([]);
// Mark No-Show Modal state
const [showMarkNoShowModal, setShowMarkNoShowModal] = useState(false);
const [isMarkingNoShow, setIsMarkingNoShow] = useState(false);
// Invalidate booking queries after mutations
const invalidateBookingQueries = useCallback(() => {
queryClient.invalidateQueries({ queryKey: queryKeys.bookings.all });
if (selectedBooking?.uid) {
queryClient.invalidateQueries({ queryKey: queryKeys.bookings.detail(selectedBooking.uid) });
}
}, [queryClient, selectedBooking?.uid]);
// ============================================================================
// Add Guests
// ============================================================================
const openAddGuestsModal = useCallback((booking: Booking) => {
setSelectedBooking(booking);
setShowAddGuestsModal(true);
}, []);
const closeAddGuestsModal = useCallback(() => {
setShowAddGuestsModal(false);
}, []);
const handleAddGuests = useCallback(
async (guests: AddGuestInput[]) => {
if (!selectedBooking) {
throw new Error("No booking selected");
}
setIsAddingGuests(true);
try {
await CalComAPIService.addGuests(selectedBooking.uid, guests);
Alert.alert(
"Success",
"Guests added successfully. They will receive an email notification."
);
invalidateBookingQueries();
closeAddGuestsModal();
} catch (error) {
const message = error instanceof Error ? error.message : "Failed to add guests";
throw new Error(message);
} finally {
setIsAddingGuests(false);
}
},
[selectedBooking, invalidateBookingQueries, closeAddGuestsModal]
);
// ============================================================================
// Edit Location
// ============================================================================
const openEditLocationModal = useCallback((booking: Booking) => {
setSelectedBooking(booking);
setShowEditLocationModal(true);
}, []);
const closeEditLocationModal = useCallback(() => {
setShowEditLocationModal(false);
}, []);
const handleUpdateLocation = useCallback(
async (location: string) => {
if (!selectedBooking) {
throw new Error("No booking selected");
}
setIsUpdatingLocation(true);
try {
await CalComAPIService.updateLocation(selectedBooking.uid, location);
Alert.alert(
"Success",
"Location updated successfully. Note: The calendar event may not be automatically updated."
);
invalidateBookingQueries();
closeEditLocationModal();
} catch (error) {
const message = error instanceof Error ? error.message : "Failed to update location";
throw new Error(message);
} finally {
setIsUpdatingLocation(false);
}
},
[selectedBooking, invalidateBookingQueries, closeEditLocationModal]
);
// ============================================================================
// View Recordings
// ============================================================================
const openViewRecordingsModal = useCallback(async (booking: Booking) => {
setSelectedBooking(booking);
setShowViewRecordingsModal(true);
setIsLoadingRecordings(true);
setRecordings([]);
try {
const recordingsData = await CalComAPIService.getRecordings(booking.uid);
setRecordings(recordingsData);
} catch (error) {
if (__DEV__) {
console.debug("[useBookingActionModals] Failed to load recordings:", error);
}
showErrorAlert("Error", "Failed to load recordings. Please try again.");
setShowViewRecordingsModal(false);
} finally {
setIsLoadingRecordings(false);
}
}, []);
const closeViewRecordingsModal = useCallback(() => {
setShowViewRecordingsModal(false);
setRecordings([]);
}, []);
// ============================================================================
// Meeting Session Details
// ============================================================================
const openMeetingSessionDetailsModal = useCallback(async (booking: Booking) => {
setSelectedBooking(booking);
setShowMeetingSessionDetailsModal(true);
setIsLoadingSessions(true);
setSessions([]);
try {
const sessionsData = await CalComAPIService.getConferencingSessions(booking.uid);
setSessions(sessionsData);
} catch (error) {
if (__DEV__) {
console.debug("[useBookingActionModals] Failed to load sessions:", error);
}
showErrorAlert("Error", "Failed to load meeting session details. Please try again.");
setShowMeetingSessionDetailsModal(false);
} finally {
setIsLoadingSessions(false);
}
}, []);
const closeMeetingSessionDetailsModal = useCallback(() => {
setShowMeetingSessionDetailsModal(false);
setSessions([]);
}, []);
// ============================================================================
// Mark No-Show
// ============================================================================
const openMarkNoShowModal = useCallback((booking: Booking) => {
setSelectedBooking(booking);
setShowMarkNoShowModal(true);
}, []);
const closeMarkNoShowModal = useCallback(() => {
setShowMarkNoShowModal(false);
}, []);
const handleMarkNoShow = useCallback(
async (attendeeEmail: string, absent: boolean) => {
if (!selectedBooking) {
throw new Error("No booking selected");
}
setIsMarkingNoShow(true);
try {
await CalComAPIService.markAbsent(selectedBooking.uid, attendeeEmail, absent);
Alert.alert("Success", absent ? "Attendee marked as no-show." : "No-show status removed.");
invalidateBookingQueries();
closeMarkNoShowModal();
} catch (error) {
const message = error instanceof Error ? error.message : "Failed to update no-show status";
throw new Error(message);
} finally {
setIsMarkingNoShow(false);
}
},
[selectedBooking, invalidateBookingQueries, closeMarkNoShowModal]
);
// ============================================================================
// Request Reschedule (deep link to web)
// ============================================================================
const handleRequestReschedule = useCallback((booking: Booking) => {
// Request reschedule is a server-driven operation that requires the web app
// We deep link to the booking detail page where the user can trigger it
const webUrl = `https://app.cal.com/booking/${booking.uid}`;
Alert.alert(
"Request Reschedule",
"This action will open the Cal.com web app where you can request a reschedule from the attendee.",
[
{ text: "Cancel", style: "cancel" },
{
text: "Open in Browser",
onPress: () => {
Linking.openURL(webUrl).catch(() => {
showErrorAlert("Error", "Failed to open the web app. Please try again.");
});
},
},
]
);
}, []);
return {
// Selected booking
selectedBooking,
setSelectedBooking,
// Add Guests
showAddGuestsModal,
isAddingGuests,
openAddGuestsModal,
closeAddGuestsModal,
handleAddGuests,
// Edit Location
showEditLocationModal,
isUpdatingLocation,
openEditLocationModal,
closeEditLocationModal,
handleUpdateLocation,
// View Recordings
showViewRecordingsModal,
isLoadingRecordings,
recordings,
openViewRecordingsModal,
closeViewRecordingsModal,
// Meeting Session Details
showMeetingSessionDetailsModal,
isLoadingSessions,
sessions,
openMeetingSessionDetailsModal,
closeMeetingSessionDetailsModal,
// Mark No-Show
showMarkNoShowModal,
isMarkingNoShow,
openMarkNoShowModal,
closeMarkNoShowModal,
handleMarkNoShow,
// Request Reschedule
handleRequestReschedule,
};
}