Files
calendar/companion/components/BookingActionsModal.tsx
T
Dhairyashil ShindeandGitHub 9e84f59216 feat(companion): UI Enhancements for Android and Extension (#26434)
* feat: companion-android-ui-upgrade version 1

* recurrings and unconfirmed booking filter and page implementation

* add badge and links to event type list page

* address cubics comments

* feat(companion): unify dropdown menu for Android and extension (#26486)

* feat(companion): unify dropdown menu for Android and extension

- Merge Android-specific dropdown implementations into base component files
- EventTypeListItem: Add DropdownMenu with Preview, Copy link, Edit, Duplicate, Delete actions
- BookingListItem: Add DropdownMenu with booking actions (reschedule, edit location, add guests, etc.)
- RecurringBookingListItem: Add DropdownMenu with recurring booking actions
- AvailabilityListItem: Add DropdownMenu with Set as Default, Duplicate, Delete actions
- BookingDetailScreen: Add DropdownMenu in header for Android with AlertDialog for cancel confirmation
- Delete all .android.tsx files as implementations are now unified

* fix(companion): fix typecheck errors after dropdown unification

- Remove unused props from EventTypeListItem.ios.tsx (copiedEventTypeId, handleEventTypeLongPress)
- Remove unused onActionsPress prop from BookingListItem.ios.tsx
- Remove copiedEventTypeId and handleEventTypeLongPress props from index.ios.tsx and index.tsx
- Remove onLongPress and onActionsPress props from BookingListScreen.tsx
- Remove handleScheduleLongPress, setSelectedSchedule, setShowActionsModal props from AvailabilityListScreen.tsx
- Fix BookingDetailScreen.tsx to use correct action property names (reschedule.visible instead of canReschedule, etc.)

* fix(companion): remove unused code after dropdown unification

- Remove unused handleEventTypeLongPress function and ActionSheetIOS import from index.ios.tsx
- Remove unused copiedEventTypeId state, handleEventTypeLongPress function, and ActionSheetIOS import from index.tsx
- Prefix unused setSelectedBooking with underscore in BookingListScreen.tsx
- Remove unused handleScheduleLongPress function and ActionSheetIOS import from AvailabilityListScreen.tsx

* feat(companion): unify booking filter UI for Android and extension

- Remove SegmentedControl from web/extension booking list page
- Use Header dropdown for booking status filter on both Android and web
- Use unified event type filter dropdown for both platforms
- Remove unused showFilterModal state and related code
- Pass filterOptions, activeFilter, and onFilterChange to Header for all platforms

* fix(companion): add header padding for web/extension to prevent button clipping

- Add headerLeftContainerStyle and headerRightContainerStyle with 12px padding for web platform
- Applied to root Stack and all nested tab Stack layouts
- Fixes issue where header buttons were touching edges and getting chopped off on extension/web
- Android remains unaffected as the fix is web-only

* fix(companion): add HeaderButtonWrapper for web-only header padding

- Create HeaderButtonWrapper component that adds 12px margin on web only
- Wrap all header buttons with HeaderButtonWrapper to prevent clipping
- Revert invalid screenOptions changes that caused typecheck errors
- Apply fix to all screens with native header buttons:
  - reschedule.tsx, edit-location.tsx, add-guests.tsx
  - mark-no-show.tsx, view-recordings.tsx, meeting-session-details.tsx
  - event-type-detail.tsx, BookingDetailScreen.tsx, profile-sheet.tsx
  - edit-availability-day.tsx, edit-availability-name.tsx, edit-availability-override.tsx

* update more ui-ux

* address cubics comments

* address cubics comments & open event type list page first for inttial render of app
2026-01-07 07:19:07 -03:00

332 lines
9.9 KiB
TypeScript

/**
* BookingActionsModal Component - Android/Web Implementation
*
* A reusable modal component for booking actions that can be used in both
* the bookings list screen and the booking detail screen.
*
* This component uses the centralized action gating utility for consistent
* action visibility and enabled state across the app.
*
* Note: iOS uses BookingActionsModal.ios.tsx with native Glass UI styling.
*/
import { Ionicons } from "@expo/vector-icons";
import { ScrollView, Text, TouchableOpacity, View } from "react-native";
import type { Booking } from "@/services/calcom";
import type { BookingActionsResult } from "@/utils/booking-actions";
import { FullScreenModal } from "./FullScreenModal";
export interface BookingActionsModalProps {
visible: boolean;
onClose: () => void;
booking: Booking | null;
actions: BookingActionsResult;
onReschedule: () => void;
onRequestReschedule?: () => void;
onEditLocation: () => void;
onAddGuests: () => void;
onViewRecordings: () => void;
onMeetingSessionDetails: () => void;
onMarkNoShow: () => void;
onReportBooking: () => void;
onCancelBooking: () => void;
}
// Icon mapping for actions
const ACTION_ICONS: Record<string, keyof typeof Ionicons.glyphMap> = {
reschedule: "calendar-outline",
rescheduleRequest: "send-outline",
changeLocation: "location-outline",
addGuests: "person-add-outline",
viewRecordings: "videocam-outline",
meetingSessionDetails: "information-circle-outline",
markNoShow: "eye-off-outline",
report: "flag-outline",
cancel: "close-circle-outline",
};
interface ActionButtonProps {
icon: keyof typeof Ionicons.glyphMap;
label: string;
onPress: () => void;
visible: boolean;
enabled: boolean;
isDanger?: boolean;
isLast?: boolean;
}
function ActionButton({
icon,
label,
onPress,
visible,
enabled,
isDanger = false,
isLast = false,
}: ActionButtonProps) {
if (!visible) return null;
const iconColor = !enabled ? "#D1D5DB" : isDanger ? "#800020" : "#6B7280";
const textColor = !enabled ? "#D1D5DB" : isDanger ? "#800020" : "#111827";
return (
<TouchableOpacity
onPress={() => {
if (!enabled) return;
onPress();
}}
disabled={!enabled}
className={`flex-row items-center px-4 py-3 active:bg-gray-50 ${
!isLast ? "border-b border-gray-100" : ""
}`}
activeOpacity={0.7}
>
<View className="mr-3 h-6 w-6 items-center justify-center">
<Ionicons name={icon} size={20} color={iconColor} />
</View>
<Text className="flex-1 text-[16px]" style={{ color: textColor }}>
{label}
</Text>
{!enabled && (
<View className="rounded bg-gray-100 px-2 py-0.5">
<Text className="text-xs text-gray-500">Unavailable</Text>
</View>
)}
</TouchableOpacity>
);
}
interface SectionHeaderProps {
title: string;
}
function SectionHeader({ title }: SectionHeaderProps) {
return (
<View className="bg-gray-50 px-4 py-2">
<Text className="text-[12px] font-semibold uppercase tracking-wide text-gray-500">
{title}
</Text>
</View>
);
}
export function BookingActionsModal({
visible,
onClose,
booking,
actions,
onReschedule,
onRequestReschedule,
onEditLocation,
onAddGuests,
onViewRecordings,
onMeetingSessionDetails,
onMarkNoShow,
onReportBooking,
onCancelBooking,
}: BookingActionsModalProps) {
if (!booking) return null;
const handleAction = (action: () => void) => {
onClose();
action();
};
// Check if any edit event actions are visible
const hasEditEventActions =
actions.reschedule.visible ||
actions.rescheduleRequest.visible ||
actions.changeLocation.visible ||
actions.addGuests.visible;
// Check if any after event actions are visible
const hasAfterEventActions =
actions.viewRecordings.visible ||
actions.meetingSessionDetails.visible ||
actions.markNoShow.visible;
// Define edit event actions
const editEventActions = [
{
key: "reschedule",
icon: ACTION_ICONS.reschedule,
label: "Reschedule Booking",
onPress: () => handleAction(onReschedule),
visible: actions.reschedule.visible,
enabled: actions.reschedule.enabled,
},
...(onRequestReschedule
? [
{
key: "rescheduleRequest",
icon: ACTION_ICONS.rescheduleRequest,
label: "Request Reschedule",
onPress: () => handleAction(onRequestReschedule),
visible: actions.rescheduleRequest.visible,
enabled: actions.rescheduleRequest.enabled,
},
]
: []),
{
key: "changeLocation",
icon: ACTION_ICONS.changeLocation,
label: "Edit Location",
onPress: () => handleAction(onEditLocation),
visible: actions.changeLocation.visible,
enabled: actions.changeLocation.enabled,
},
{
key: "addGuests",
icon: ACTION_ICONS.addGuests,
label: "Add Guests",
onPress: () => handleAction(onAddGuests),
visible: actions.addGuests.visible,
enabled: actions.addGuests.enabled,
},
].filter((action) => action.visible);
// Define after event actions
const afterEventActions = [
{
key: "viewRecordings",
icon: ACTION_ICONS.viewRecordings,
label: "View Recordings",
onPress: () => handleAction(onViewRecordings),
visible: actions.viewRecordings.visible,
enabled: actions.viewRecordings.enabled,
},
{
key: "meetingSessionDetails",
icon: ACTION_ICONS.meetingSessionDetails,
label: "Meeting Session Details",
onPress: () => handleAction(onMeetingSessionDetails),
visible: actions.meetingSessionDetails.visible,
enabled: actions.meetingSessionDetails.enabled,
},
{
key: "markNoShow",
icon: ACTION_ICONS.markNoShow,
label: "Mark as No-Show",
onPress: () => handleAction(onMarkNoShow),
visible: actions.markNoShow.visible,
enabled: actions.markNoShow.enabled,
},
].filter((action) => action.visible);
// Define danger zone actions
const dangerZoneActions = [
{
key: "report",
icon: ACTION_ICONS.report,
label: "Report Booking",
onPress: () => handleAction(onReportBooking),
visible: true,
enabled: true,
isDanger: true,
},
{
key: "cancel",
icon: ACTION_ICONS.cancel,
label: "Cancel Event",
onPress: () => handleAction(onCancelBooking),
visible: actions.cancel.visible,
enabled: actions.cancel.enabled,
isDanger: true,
},
].filter((action) => action.visible);
return (
<FullScreenModal visible={visible} animationType="fade" onRequestClose={onClose}>
<TouchableOpacity
className="flex-1 items-center justify-center bg-black/50 p-4"
activeOpacity={1}
onPress={onClose}
>
<TouchableOpacity
className="w-full max-w-md"
activeOpacity={1}
onPress={(e) => e.stopPropagation()}
>
{/* Actions Card */}
<View className="mb-4 overflow-hidden rounded-2xl bg-white shadow-lg">
{/* Booking Title Header */}
<View className="border-b border-gray-100 px-4 py-3">
<Text className="text-center text-[14px] font-medium text-gray-600" numberOfLines={1}>
{booking.title}
</Text>
</View>
<ScrollView className="max-h-[400px]">
{/* Edit Event Section */}
{hasEditEventActions && (
<>
<SectionHeader title="Edit Event" />
{editEventActions.map((action, index) => (
<ActionButton
key={action.key}
icon={action.icon}
label={action.label}
onPress={action.onPress}
visible={action.visible}
enabled={action.enabled}
isLast={index === editEventActions.length - 1}
/>
))}
</>
)}
{/* After Event Section */}
{hasAfterEventActions && (
<>
<SectionHeader title="After Event" />
{afterEventActions.map((action, index) => (
<ActionButton
key={action.key}
icon={action.icon}
label={action.label}
onPress={action.onPress}
visible={action.visible}
enabled={action.enabled}
isLast={index === afterEventActions.length - 1}
/>
))}
</>
)}
{/* Danger Zone Section */}
{dangerZoneActions.length > 0 && (
<>
<SectionHeader title="Danger Zone" />
{dangerZoneActions.map((action, index) => (
<ActionButton
key={action.key}
icon={action.icon}
label={action.label}
onPress={action.onPress}
visible={action.visible}
enabled={action.enabled}
isDanger={action.isDanger}
isLast={index === dangerZoneActions.length - 1}
/>
))}
</>
)}
</ScrollView>
</View>
{/* Cancel Button */}
<TouchableOpacity
className="overflow-hidden rounded-2xl bg-white shadow-lg"
onPress={onClose}
activeOpacity={0.7}
>
<View className="px-4 py-3">
<Text className="text-center text-[16px] font-semibold text-gray-700">Cancel</Text>
</View>
</TouchableOpacity>
</TouchableOpacity>
</TouchableOpacity>
</FullScreenModal>
);
}