Files
calendar/packages/features/bookings/components/Header.tsx
T
Eunjae LeeGitHubClaude Opus 4.5unknown <>Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
bf348ee209 refactor: migrate Icon to component icons from @coss/ui/icons (#27458)
* feat: add @coss/ui/icons package and migrate static icon usages

Create a new icon system in @coss/ui/icons that wraps lucide-react icons
with controlled props (size and className only). Migrate 44 component
files from <Icon name="xxx" /> to <XxxIcon /> pattern.

This is Phase 1 of the icon migration, covering only static icon names.
Dynamic icon usages (e.g., name={variable}) are skipped for now.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: migrate more static icons to @coss/ui/icons (batch 2)

Migrated 18 files from <Icon name="xxx" /> to <XxxIcon /> pattern:
- Event meta, insights, shell, user table bulk actions
- Webhooks, blocklist, form-builder, form components

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: migrate more static icons to @coss/ui/icons (batch 3)

Migrated 7 files:
- CancelBooking, AddGuestsDialog, ChargeCardDialog
- ReassignDialog, RescheduleDialog
- Categories, Slider

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: migrate more static icons to @coss/ui/icons (batch 4)

Migrated 6 files:
- Organizations: AboutOrganizationForm, PaymentStatusView, profile
- Workflows: CallDetailsSheet, SkeletonLoaderList, TimeTimeUnitInput

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: migrate more static icons to @coss/ui/icons (batch 5)

Migrated 4 files:
- TwoFactorModalHeader
- App setups: alby, make, btcpayserver

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: migrate static icons to @coss/ui/icons (batch 6)

- confirmDialog.tsx: clock icon
- ics-feedcalendar/Setup.tsx: trash, plus icons
- hitpay/Setup.tsx: info icon
- paypal/Setup.tsx: circle-alert icons

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: migrate static icons to @coss/ui/icons (batch 7)

- webhook-edit-view.tsx: external-link icon
- webhook-new-view.tsx: external-link icon
- UserDropdown.tsx: chevron-down, user, settings, moon, map, circle-help, download, blocks, log-out icons
- TopNav.tsx: settings icon

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: migrate static icons to @coss/ui/icons (batch 8)

- EmailInviteForm.tsx: x icon
- InviteOptions.tsx: mail, upload, link icons
- SkeletonLoader.tsx: search, clock, user icons
- Embed.tsx: sun, arrow-left icons
- CalendarSwitch.tsx: arrow-left, rotate-cw icons

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: migrate static icons to @coss/ui/icons (batch 9)

- apps-view.tsx: search icon
- categories-view.tsx: arrow-left, arrow-right icons
- DatePicker.tsx: calendar icon
- EditLocationDialog.tsx: map-pin icon
- AppPage.tsx: circle-alert, book-open, external-link, mail, file, shield, flag icons

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: migrate static icons to @coss/ui/icons (batch 10)

- LicenseSelection.tsx: loader, check icons
- AppConnectionItem.tsx: circle-alert, arrow-right icons
- oauth-client-form/index.tsx: info icons (x2)
- auth/error/page.tsx: x icon
- troubleshoot/layout.tsx: loader icon

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: migrate static icons to @coss/ui/icons (batch 11)

- UpgradeTipWrapper.tsx: users, refresh-ccw, user-plus icons
- PbacOptInModal.tsx: shield-check, lock, users, shield icons
- AdvancedPermissionGroup.tsx: chevron-right, info icons

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: migrate static icons to @coss/ui/icons (batch 12)

- AdminAppsList.tsx: pencil icon
- AvailableTimes.tsx: calendar-x-2 icon
- BookingDetailsSheet.tsx: external-link (x2), repeat icons
- BookingCalendarContainer.tsx: chevron-left, chevron-right icons

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: migrate static icons to @coss/ui/icons (batch 13)

- videos-single-view.tsx: chevron-right icon
- TroubleshooterSidebar.tsx: arrow-left icon
- RuleBuilder.tsx: filter icon

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: migrate static icons to @coss/ui/icons (batch 14)

- FieldMappingBuilder.tsx: arrow-right, link icons
- IntegrationAttributeSyncCard.tsx: key icon
- onboarding-view.tsx: loader icon

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: migrate static icons to @coss/ui/icons (batch 15)

- team-profile-view.tsx: copy icon
- DateRangeFilter.tsx: check icon
- ColumnVisibilityButton.tsx: check icon

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: migrate static Icon to @coss/ui/icons (batch 16)

- EventWorkflowsTab.tsx: lock
- event-types-listing-view.tsx: search
- onboarding-migrate-members-browser-view.tsx: arrow-left, arrow-right, rotate-cw, lock, ellipsis-vertical, check, user-plus
- onboarding-migrate-teams-browser-view.tsx: arrow-left, arrow-right, rotate-cw, lock, ellipsis-vertical, check, arrow-right, users
- EventAvailabilityTab.tsx: globe, user
- onboarding-teams-browser-view.tsx: arrow-left, arrow-right, rotate-cw, lock, ellipsis-vertical, check, arrow-right, users
- FormBuilder.tsx: mail, phone, arrow-up, arrow-down
- AIEventController.tsx: info
- EventTeamAssignmentTab.tsx: x

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: migrate static Icon to @coss/ui/icons (batch 17)

- EventAdvancedTab.tsx: pencil, info
- HostLocations.tsx: triangle-alert, loader
- EventTypeLayout.tsx: loader
- Locations.tsx: x, corner-down-right, check
- EditWeightsForAllTeamMembers.tsx: upload, search, info, chevron-down
- WorkflowPage.tsx: pencil

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: migrate static Icon to @coss/ui/icons (batch 18)

- TestPhoneCallDialog.tsx: phone
- WorkflowListPage.tsx: link
- UsersTable.tsx: building, lock
- TeamList.tsx: building, paintbrush, chart-line, user-plus, users, pencil
- bookings-single-view.tsx: chevron-left, check, calendar, x, external-link

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: migrate static Icon to @coss/ui/icons (batch 19)

- TeamsListing.tsx: users, refresh-ccw, user-plus, mail, video, eye-off, info
- ConnectionInfo.tsx: clipboard
- IntercomContactForm.tsx: message-circle, loader, send

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: migrate static Icon to @coss/ui/icons in teams components

- EventTypesList.tsx: check → CheckIcon
- MemberInvitationModal.tsx: user, users, building → UserIcon, UsersIcon, BuildingIcon

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: migrate static Icon to @coss/ui/icons in workflow components

- ImportNumberDialog.tsx: info → InfoIcon
- VoiceSelector.tsx: user → UserIcon
- IncomingCallsTab.tsx: info, phone-incoming → InfoIcon, PhoneIncomingIcon
- PhoneNumberTab.tsx: loader, phone → LoaderIcon, PhoneIcon
- VoiceSelectionDialog.tsx: pause, play, user → PauseIcon, PlayIcon, UserIcon
- WebCallDialog.tsx: loader, phone, phone-off, triangle-alert, trash, message-circle → new icons (kept Icon import for dynamic mic icon)
- WorkflowDetailsPage.tsx: zap, arrow-right → ZapIcon, ArrowRightIcon
- AddActionDialog.tsx: info → InfoIcon

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: migrate static Icon to @coss/ui/icons in WorkflowStepContainer

- info → InfoIcon (2 instances)
- phone → PhoneIcon (2 instances)
- circle-help → CircleHelpIcon

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: migrate static Icon to @coss/ui/icons in data-table and org components

- attributes-list-view.tsx: tags → TagsIcon
- AddFilterButton.tsx: plus, eye-off → PlusIcon, EyeOffIcon
- BaseSelectFilterOptions.tsx: check → CheckIcon
- DataTable.tsx: arrow-up, arrow-down, check, chevrons-up-down, eye-off → new icons
- booking-dry-run-success-view.tsx: check → CheckIcon

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: migrate static Icon to @coss/ui/icons in username and routing components

- UsernameTextfield.tsx: check → CheckIcon
- PremiumTextfield.tsx: check, external-link → CheckIcon, ExternalLinkIcon (kept Icon for star with fill prop)
- Header.tsx (routing-forms): pencil, menu, waypoints → PencilIcon, MenuIcon, WaypointsIcon

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: migrate Icon to component icons in routing-forms pages

Batch 25: Migrate static Icon usages to @coss/ui/icons components.

Files changed:
- Forms.tsx: file-text, shuffle, chart-bar, circle-check, mail, download → FileTextIcon, ShuffleIcon, ChartBarIcon, CircleCheckIcon, MailIcon, DownloadIcon
- FormEdit.tsx: chevron-down, menu → ChevronDownIcon, MenuIcon
- IncompleteBooking.tsx: globe → GlobeIcon

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: migrate Icon to component icons in UI package

Batch 26: Migrate static Icon usages to @coss/ui/icons components.

Files changed:
- ProgressToast.tsx: file-down, x → FileDownIcon, XIcon
- showToast.tsx: check, info, x → CheckIcon, InfoIcon, XIcon
- InfoBadge.tsx: info → InfoIcon
- MeetingTimeInTimezones.tsx: globe → GlobeIcon
- InputError.tsx: info → InfoIcon
- ConfirmationDialogContent.tsx: circle-alert, check → CircleAlertIcon, CheckIcon
- CalendarSwitch.tsx: arrow-left, rotate-cw → ArrowLeftIcon, RotateCwIcon
- ArrowButton.tsx: arrow-up, arrow-down → ArrowUpIcon, ArrowDownIcon
- FileUploader.tsx: upload → UploadIcon (kept Icon for dynamic)
- Checkbox.tsx: check → CheckIcon
- EditableHeading.tsx: pencil → PencilIcon

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: migrate Icon to component icons in UI and app-store packages

Batch 27: Migrate static Icon usages to @coss/ui/icons components.

Files changed:
- MultiEmail.tsx: x → XIcon (converted width to size)
- AppListCard.tsx: circle-alert → CircleAlertIcon
- SplitButton.tsx: chevron-down → ChevronDownIcon (kept Icon for dynamic)
- Calendar.tsx: chevron-left, chevron-right → ChevronLeftIcon, ChevronRightIcon
- AppDependencyComponent.tsx: check, circle-x, arrow-right → CheckIcon, CircleXIcon, ArrowRightIcon

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: migrate Icon to component icons in form input components

Batch 28: Migrate static Icon usages to @coss/ui/icons components.

Files changed:
- TextField.tsx: info → InfoIcon (kept Icon for x with onClick)
- Input.tsx: eye, eye-off, search → EyeIcon, EyeOffIcon, SearchIcon

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: use @coss/ui/src/icons in packages with legacy moduleResolution

Packages like packages/ui and packages/app-store use a legacy moduleResolution
setting that doesn't support subpath exports. This changes the import path from
@coss/ui/icons to @coss/ui/src/icons for these packages.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: migrate Icon to component icons in UI form and editor components

Migrates static Icon usages:
- MultiOptionInput: x (2x), arrow-up, arrow-down
- components (select): check
- AddVariablesDropdown: chevron-down (3x)
- ToolbarPlugin: chevron-down

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: migrate Icon to component icons in settings and platform atoms

Migrates static Icon usages:
- SettingsLayoutAppDirClient: chevron-down (2x), chevron-right (2x), arrow-left
- platform/atoms dialog: x
- platform/atoms toast: x

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: resolve @coss/ui/icons module resolution for legacy moduleResolution

Add typesVersions to @coss/ui package.json to help TypeScript resolve
subpath exports even with legacy moduleResolution: "node" setting.

Change imports from @coss/ui/src/icons to @coss/ui/icons to avoid
double-src path issue when Next.js applies import maps.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: localize 'Select field type' fallback label in FormEdit

Address Cubic AI review feedback (confidence 9/10) by using t() for
the hardcoded 'Select field type' string to ensure proper i18n support.

Co-Authored-By: unknown <>

* fix: add typesVersions wildcards for @coss/ui subpath imports

Add lib/*, components/*, and hooks/* to typesVersions to support
legacy moduleResolution with all package subpath patterns.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: resolve missing Icon references in settings and workflows

- Add Icon import for dynamic icon usages in SettingsLayoutAppDirClient
- Replace static Icon usages with component icons (InfoIcon, PhoneIcon)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: resolve icon component type errors

- Add onClick prop support to IconProps
- Fix size prop type (use number instead of string)
- Rename local MailOpenIcon to MailOpenIconWrapper to avoid shadowing

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: add data-testid to icon components for test compatibility

- Add testId parameter to createIcon function
- Auto-generate data-testid for all icons (e.g., "check-icon", "search-icon")
- Remove outdated mock for @calcom/ui/components/icon in tests

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Update package.json

* Update package.json

* fix: migrate Icon to CalendarDaysIcon in videos-single-view.tsx

Co-Authored-By: unknown <>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-02-12 14:30:27 +01:00

260 lines
8.8 KiB
TypeScript

import { useCallback, useMemo } from "react";
import { shallow } from "zustand/shallow";
import { useIsPlatform } from "@calcom/atoms/hooks/useIsPlatform";
import dayjs from "@calcom/dayjs";
import { useIsEmbed } from "@calcom/embed-core/embed-iframe";
import { useBookerStoreContext } from "@calcom/features/bookings/Booker/BookerStoreProvider";
import { useInitializeWeekStart } from "@calcom/features/bookings/hooks/useInitializeWeekStart";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { formatDateTime } from "@calcom/lib/dateTimeFormatter";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { BookerLayouts } from "@calcom/prisma/zod-utils";
import { Button } from "@calcom/ui/components/button";
import { ButtonGroup } from "@calcom/ui/components/buttonGroup";
import { ToggleGroup } from "@calcom/ui/components/form";
import { CalendarIcon, Columns3Icon, Grid3x3Icon } from "@coss/ui/icons";
import { Tooltip } from "@calcom/ui/components/tooltip";
import { TimeFormatToggle } from "@calcom/features/bookings/components/TimeFormatToggle";
import type { BookerLayout } from "@calcom/features/bookings/Booker/types";
export function Header({
extraDays,
isMobile,
enabledLayouts,
nextSlots,
eventSlug,
isMyLink,
renderOverlay,
isCalendarView,
}: {
extraDays: number;
isMobile: boolean;
enabledLayouts: BookerLayouts[];
nextSlots: number;
eventSlug: string;
isMyLink: boolean;
renderOverlay?: () => JSX.Element | null;
isCalendarView?: boolean;
}) {
const { t, i18n } = useLocale();
const isEmbed = useIsEmbed();
const isPlatform = useIsPlatform();
const [layout, setLayout] = useBookerStoreContext((state) => [state.layout, state.setLayout], shallow);
const selectedDateString = useBookerStoreContext((state) => state.selectedDate);
const setSelectedDate = useBookerStoreContext((state) => state.setSelectedDate);
const addToSelectedDate = useBookerStoreContext((state) => state.addToSelectedDate);
const isMonthView = isCalendarView !== undefined ? !isCalendarView : layout === BookerLayouts.MONTH_VIEW;
const today = dayjs();
const selectedDate = selectedDateString ? dayjs(selectedDateString) : today;
const selectedDateMin3DaysDifference = useMemo(() => {
const diff = today.diff(selectedDate, "days");
return diff > 3 || diff < -3;
}, [today, selectedDate]);
useInitializeWeekStart(isPlatform, isCalendarView ?? false);
const onLayoutToggle = useCallback(
(newLayout: string) => {
if (layout === newLayout || !newLayout) return;
setLayout(newLayout as BookerLayout);
},
[setLayout, layout]
);
if (isMobile || !enabledLayouts) return null;
// In month view we only show the layout toggle.
if (isMonthView) {
return (
<div className="flex gap-2">
{isMyLink && !isEmbed ? (
<Tooltip content={t("troubleshooter_tooltip")} side="bottom">
<Button
color="primary"
target="_blank"
href={`${WEBAPP_URL}/availability/troubleshoot?eventType=${eventSlug}`}>
{t("need_help")}
</Button>
</Tooltip>
) : (
renderOverlay?.()
)}
<LayoutToggleWithData
layout={layout}
enabledLayouts={enabledLayouts}
onLayoutToggle={onLayoutToggle}
/>
</div>
);
}
const endDate = selectedDate.add(layout === BookerLayouts.COLUMN_VIEW ? extraDays : extraDays - 1, "days");
const isSameMonth = () => {
return selectedDate.format("MMM") === endDate.format("MMM");
};
const isSameYear = () => {
return selectedDate.format("YYYY") === endDate.format("YYYY");
};
const formatMonthLabel = (date: Date) =>
formatDateTime(date, { locale: i18n.language ?? "en", month: "short" });
const FormattedSelectedDateRange = () => {
return (
<h3 className="min-w-[150px] text-base font-semibold leading-4">
{formatMonthLabel(selectedDate.toDate())} {selectedDate.format("D")}
{!isSameYear() && <span className="text-subtle">, {selectedDate.format("YYYY")} </span>}-{" "}
{!isSameMonth() && formatMonthLabel(endDate.toDate())} {endDate.format("D")},{" "}
<span className="text-subtle">
{isSameYear() ? selectedDate.format("YYYY") : endDate.format("YYYY")}
</span>
</h3>
);
};
return (
<div className="border-default relative z-10 flex border-b px-5 py-4 ltr:border-l rtl:border-r">
<div className="flex items-center gap-5 rtl:grow">
<FormattedSelectedDateRange />
<ButtonGroup>
<Button
className="group rtl:ml-1 rtl:rotate-180"
variant="icon"
color="minimal"
StartIcon="chevron-left"
aria-label="Previous Day"
onClick={() => addToSelectedDate(layout === BookerLayouts.COLUMN_VIEW ? -nextSlots : -extraDays)}
/>
<Button
className="group rtl:mr-1 rtl:rotate-180"
variant="icon"
color="minimal"
StartIcon="chevron-right"
aria-label="Next Day"
onClick={() => addToSelectedDate(layout === BookerLayouts.COLUMN_VIEW ? nextSlots : extraDays)}
/>
{selectedDateMin3DaysDifference && (
<Button
className="capitalize ltr:ml-2 rtl:mr-2"
color="secondary"
onClick={() => {
const selectedDate = (isCalendarView ? today.startOf("week") : today).format("YYYY-MM-DD");
setSelectedDate({ date: selectedDate });
}}>
{t("today")}
</Button>
)}
</ButtonGroup>
</div>
<div className="ml-auto flex gap-2">
{renderOverlay?.()}
<TimeFormatToggle />
<div className="fixed top-4 ltr:right-4 rtl:left-4">
<LayoutToggleWithData
layout={layout}
enabledLayouts={enabledLayouts}
onLayoutToggle={onLayoutToggle}
/>
</div>
{/*
This second layout toggle is hidden, but needed to reserve the correct spot in the DIV
for the fixed toggle above to fit into. If we wouldn't make it fixed in this view, the transition
would be really weird, because the element is positioned fixed in the month view, and then
when switching layouts wouldn't anymore, causing it to animate from the center to the top right,
while it actually already was on place. That's why we have this element twice.
*/}
<div className="pointer-events-none opacity-0" aria-hidden>
<LayoutToggleWithData
layout={layout}
enabledLayouts={enabledLayouts}
onLayoutToggle={onLayoutToggle}
/>
</div>
</div>
</div>
);
}
const LayoutToggle = ({
onLayoutToggle,
layout,
enabledLayouts,
}: {
onLayoutToggle: (layout: string) => void;
layout: string;
enabledLayouts?: BookerLayouts[];
}) => {
const isEmbed = useIsEmbed();
const isPlatform = useIsPlatform();
const { t } = useLocale();
const layoutOptions = useMemo(() => {
return [
{
value: BookerLayouts.MONTH_VIEW,
label: (
<>
<CalendarIcon className="h-4 w-4" />
<span className="sr-only">${t("switch_monthly")}</span>
</>
),
tooltip: t("switch_monthly"),
},
{
value: BookerLayouts.WEEK_VIEW,
label: (
<>
<Grid3x3Icon className="h-4 w-4" />
<span className="sr-only">${t("switch_weekly")}</span>
</>
),
tooltip: t("switch_weekly"),
},
{
value: BookerLayouts.COLUMN_VIEW,
label: (
<>
<Columns3Icon className="h-4 w-4" />
<span className="sr-only">${t("switch_columnview")}</span>
</>
),
tooltip: t("switch_columnview"),
},
].filter((layout) => enabledLayouts?.includes(layout.value as BookerLayouts));
}, [t, enabledLayouts]);
// We don't want to show the layout toggle in embed mode as of now as it doesn't look rightly placed when embedded.
// There is a Embed API to control the layout toggle from outside of the iframe.
if (isEmbed) {
return null;
}
// just like embed the layout toggle doesn't look rightly placed in platform
// the layout can be toggled via props in the booker atom
if (isPlatform) return null;
return (
<ToggleGroup
onValueChange={onLayoutToggle}
defaultValue={layout}
aria-label={t("layout")}
options={layoutOptions}
/>
);
};
const LayoutToggleWithData = ({
enabledLayouts,
onLayoutToggle,
layout,
}: {
enabledLayouts: BookerLayouts[];
onLayoutToggle: (layout: string) => void;
layout: string;
}) => {
return enabledLayouts.length <= 1 ? null : (
<LayoutToggle onLayoutToggle={onLayoutToggle} layout={layout} enabledLayouts={enabledLayouts} />
);
};