Files
calendar/apps/web/modules/insights/components/ChartCard.tsx
T
Benny JooGitHubDevin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
404d46c991 refactor: Move trpc-dependent components from features to web [2] (#26420)
* fix

* move event types components to web

* update import paths

* mv apps components

* migrate form builder

* fix

* mv sso

* fix

* mv

* update import paths

* update import paths

* mv

* mv

* mv

* fix

* update Booker

* fix

* fix

* fix

* fix

* mv video

* mv embed components to web

* update import paths

* mv calendar weekly view components

* update import paths

* fix

* fixp

* fix

* fix

* fix

* fix: update FormBuilder imports to use @calcom/features/form-builder

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* fix: update broken import paths after file migrations

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* fix: correct import paths for platform atoms and moved components

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* fix: apply CSS type fixes and add missing atoms exports

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* fix: resolve type errors in test files after component migrations

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* fix: resolve remaining type errors in test files

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* fix

* migrate

* fix: resolve type errors in test and mock files

- Add missing bookingForm, bookerFormErrorRef, instantConnectCooldownMs to Booker.test.tsx bookings prop
- Add all required BookerEvent properties to event.mock.ts
- Add vi import from vitest to all mock files
- Fix date parameter types in packages/dayjs/__mocks__/index.ts
- Add verificationCode and setVerificationCode to test-utils.tsx mock store
- Remove children.type access in Section.tsx mock to fix type error
- Fix lint issues: remove unused React imports, use import type where needed, add return types
- Add biome-ignore comments for pre-existing lint warnings in test files

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* migrate

* migrate

* migrate

* update import paths

* update import paths

* update import paths

* fix

* migrate data table

* migrate data table

* fix

* fix

* fix

* migrate insights components

* migrate insights components

* fix

* mv

* update import paths

* fix

* fix

* fix

* fix

* fix

* fix: resolve type errors in test mocks

- Booker.test.tsx: Add all required UseFormReturn methods to bookingForm mock
- event.mock.ts: Fix entity, subsetOfHosts, instantMeetingParameters, fieldTranslations, image types
- dayjs/__mocks__/index.ts: Use Object.assign for proper typing of mock properties
- Section.tsx: Change 'class' to 'className' in JSX with biome-ignore comment

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* fix: add missing hasDataErrors and dataErrors to bookings.errors mock

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* fix: add missing loadingStates properties to bookings mock

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* fix: add missing slots properties (setTentativeSelectedTimeslots, tentativeSelectedTimeslots, slotReservationId)

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* fix: update quickAvailabilityChecks to include utcEndIso and use valid status type

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* fix: add missing bookerForm properties (formName, beforeVerifyEmail, formErrors)

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* fix: add missing UseFormReturn properties to bookerForm.bookingForm mock

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* fix: add hasFormErrors and formErrors to bookerForm.formErrors mock

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* fix: add hasFormErrors and formErrors to bookerForm.errors mock

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* fix: add missing isError property to mockEvent

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* fix: use complete BookerEvent mock in Booker.test.tsx

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* fix: use branded bookingFields type in event mock

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* fix: add missing schedule mock properties (isError, isSuccess, isLoading, dataUpdatedAt)

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* revert

* fix

* fix

* fix

* fix build

* fix

* fix

* fix

* fix: correct AddMembersWithSwitch test wrapper to use initial assignAllTeamMembers value

- Fixed test wrapper to initialize useState with componentProps.assignAllTeamMembers
  instead of hardcoded false, allowing tests to properly test different states
- Updated test expectations for ALL_TEAM_MEMBERS_ENABLED_AND_SEGMENT_NOT_APPLICABLE
  state to match actual component behavior (toggle should be present and checked)
- Fixed 'should show Segment when toggled on' test to start with assignAllTeamMembers: false
  to properly test the flow of enabling it
- Added explicit types to satisfy biome lint requirements

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* fix: use JSX.Element instead of React.JSX.Element for type compatibility

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-01-05 12:53:51 -03:00

161 lines
4.7 KiB
TypeScript

"use client";
import { Fragment, useMemo, type ReactNode } from "react";
import classNames from "@calcom/ui/classNames";
import { PanelCard } from "@calcom/ui/components/card";
import { Spinner } from "@calcom/ui/components/icon";
import { SkeletonText } from "@calcom/ui/components/skeleton";
import { Tooltip } from "@calcom/ui/components/tooltip";
type PanelCardProps = React.ComponentProps<typeof PanelCard>;
type LegendItem = {
label: string;
color: string; // hex format
};
export type LegendSize = "sm" | "default";
export function ChartCard({
legend,
legendSize,
enabledLegend,
onSeriesToggle,
isPending,
isError,
...panelCardProps
}: Omit<PanelCardProps, 'children'> & {
legend?: Array<LegendItem>;
legendSize?: LegendSize;
enabledLegend?: Array<LegendItem>;
onSeriesToggle?: (label: string) => void;
isPending?: boolean;
isError?: boolean;
children?: ReactNode;
}) {
const legendComponent =
legend && legend.length > 0 ? (
<Legend items={legend} size={legendSize} enabledItems={enabledLegend} onItemToggle={onSeriesToggle} />
) : null;
// Generate a chart ID from the title for testing purposes
const chartId = useMemo(() => {
if (typeof panelCardProps.title === "string") {
return panelCardProps.title.toLowerCase().replace(/\s+/g, "-");
}
return undefined;
}, [panelCardProps.title]);
// Calculate loading state from isPending/isError
const computedLoadingState = useMemo(() => {
if (isPending) return "loading";
if (isError) return "error";
return "loaded";
}, [isPending, isError]);
const shouldShowDefaultLoading = isPending && !panelCardProps.children;
const displayTitle = shouldShowDefaultLoading ? <SkeletonText className="w-32" /> : panelCardProps.title;
return (
<PanelCard
{...panelCardProps}
title={displayTitle}
data-testid="chart-card"
data-chart-id={chartId}
data-loading-state={computedLoadingState}
headerContent={
panelCardProps.headerContent ? (
<div className="flex items-center gap-2">
{panelCardProps.headerContent}
{legendComponent}
</div>
) : (
legendComponent
)
}>
{shouldShowDefaultLoading ? (
<div className="m-auto flex h-80 flex-col items-center justify-center">
<Spinner className="h-6 w-6" />
</div>
) : (
panelCardProps.children
)}
</PanelCard>
);
}
export function ChartCardItem({
count,
className,
children,
}: {
count?: number | string;
className?: string;
children: ReactNode;
}) {
return (
<div
className={classNames(
"text-default border-muted flex items-center justify-between border-b px-3 py-3.5 last:border-b-0",
className
)}>
<div className="grow text-sm font-medium">{children}</div>
{count !== undefined && <div className="text-sm font-medium">{count}</div>}
</div>
);
}
function Legend({
items,
size = "default",
enabledItems,
onItemToggle,
}: {
items: LegendItem[];
size?: LegendSize;
enabledItems?: LegendItem[];
onItemToggle?: (label: string) => void;
}) {
const enabledSet = useMemo(() => new Set((enabledItems ?? []).map((i) => i.label)), [enabledItems]);
const isClickable = Boolean(onItemToggle);
return (
<div className="bg-default flex items-center gap-2 rounded-lg px-1.5 py-1">
{items.map((item, index) => {
const isEnabled = enabledItems ? enabledSet.has(item.label) : true;
return (
<Fragment key={item.label}>
<button
type="button"
className={classNames(
"relative flex items-center gap-2 rounded-md px-1.5 py-0.5 transition-opacity",
isClickable && "cursor-pointer hover:bg-gray-100",
!isEnabled && "opacity-25"
)}
style={{ backgroundColor: `${item.color}33` }}
aria-pressed={isClickable ? isEnabled : undefined}
aria-label={`Toggle ${item.label}`}
disabled={!isClickable}
onClick={isClickable ? () => onItemToggle?.(item.label) : undefined}>
<div className="h-2 w-2 rounded-full" style={{ backgroundColor: item.color }} />
<Tooltip content={item.label}>
<span
className={classNames(
"text-default truncate py-0.5 text-sm font-medium leading-none",
size === "sm" ? "w-16" : ""
)}>
{item.label}
</span>
</Tooltip>
</button>
{index < items.length - 1 && <div className="bg-cal-muted h-5 w-px" />}
</Fragment>
);
})}
</div>
);
}