fix: Do not self import @calcom/ui (#20050)

* fix: Do not self import @calcom/ui

* Make translations optional

* Fix mocking implementation of Button (never worked)

* Ensure other libraries can resolve AppListCard
This commit is contained in:
Alex van Andel
2025-03-13 18:17:42 +00:00
committed by GitHub
parent b1ebefb854
commit be404436d7
28 changed files with 201 additions and 124 deletions
@@ -1,82 +1,25 @@
"use client";
import { usePathname, useRouter } from "next/navigation";
import type { ReactNode } from "react";
import { useEffect, useRef, useState } from "react";
import { z } from "zod";
import dynamic from "next/dynamic";
import { useMemo } from "react";
import type { CredentialOwner } from "@calcom/app-store/types";
import { useIsPlatform } from "@calcom/atoms/hooks/useIsPlatform";
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery";
import { AppListCard as AppListCardComponent } from "@calcom/ui";
type ShouldHighlight =
| {
slug: string;
shouldHighlight: true;
}
| {
shouldHighlight?: never;
slug?: never;
};
export type AppListCardProps = {
logo?: string;
title: string;
description: string;
actions?: ReactNode;
isDefault?: boolean;
isTemplate?: boolean;
invalidCredential?: boolean;
children?: ReactNode;
credentialOwner?: CredentialOwner;
className?: string;
} & ShouldHighlight;
const schema = z.object({ hl: z.string().optional() });
function AppListCardPlatformWrapper(props: AppListCardProps) {
const logo = `https://app.cal.com${props.logo}`;
return <AppListCardComponent {...props} logo={logo} />;
}
function AppListCardWebWrapper(props: AppListCardProps) {
const { slug, shouldHighlight } = props;
const {
data: { hl },
} = useTypedQuery(schema);
const router = useRouter();
const [highlight, setHighlight] = useState(shouldHighlight && hl === slug);
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
const searchParams = useCompatSearchParams();
const pathname = usePathname();
useEffect(() => {
if (shouldHighlight && highlight && searchParams !== null && pathname !== null) {
timeoutRef.current = setTimeout(() => {
const _searchParams = new URLSearchParams(searchParams.toString());
_searchParams.delete("hl");
_searchParams.delete("category"); // this comes from params, not from search params
setHighlight(false);
const stringifiedSearchParams = _searchParams.toString();
router.replace(`${pathname}${stringifiedSearchParams !== "" ? `?${stringifiedSearchParams}` : ""}`);
}, 3000);
}
return () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
timeoutRef.current = null;
}
};
}, [highlight, pathname, router, searchParams, shouldHighlight]);
return <AppListCardComponent {...props} />;
}
import type { AppListCardProps } from "@calcom/ui/components/app-list-card/AppListCard";
export default function AppListCard(props: AppListCardProps) {
const isPlatform = useIsPlatform();
return isPlatform ? <AppListCardPlatformWrapper {...props} /> : <AppListCardWebWrapper {...props} />;
// Dynamically import the correct wrapper
const AppListCardWrapper = useMemo(
() =>
dynamic(
() => (isPlatform ? import("./AppListCardPlatformWrapper") : import("./AppListCardWebWrapper")),
{
ssr: false, // Prevents SSR issues if needed
}
),
[isPlatform]
);
return <AppListCardWrapper {...props} />;
}
@@ -0,0 +1,7 @@
import { AppListCard } from "@calcom/ui/components/app-list-card/AppListCard";
import type { AppListCardProps } from "@calcom/ui/components/app-list-card/AppListCard";
export default function AppListCardPlatformWrapper(props: AppListCardProps) {
const logo = `https://app.cal.com${props.logo}`;
return <AppListCard {...props} logo={logo} />;
}
@@ -0,0 +1,48 @@
"use client";
import { usePathname, useRouter } from "next/navigation";
import { useEffect, useRef, useState } from "react";
import { z } from "zod";
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery";
import { AppListCard } from "@calcom/ui/components/app-list-card/AppListCard";
import type { AppListCardProps } from "@calcom/ui/components/app-list-card/AppListCard";
const schema = z.object({ hl: z.string().optional() });
export default function AppListCardWebWrapper(props: AppListCardProps) {
const { slug, shouldHighlight } = props;
const {
data: { hl },
} = useTypedQuery(schema);
const router = useRouter();
const [highlight, setHighlight] = useState(shouldHighlight && hl === slug);
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
const searchParams = useCompatSearchParams();
const pathname = usePathname();
useEffect(() => {
if (shouldHighlight && highlight && searchParams !== null && pathname !== null) {
timeoutRef.current = setTimeout(() => {
const _searchParams = new URLSearchParams(searchParams.toString());
_searchParams.delete("hl");
_searchParams.delete("category"); // this comes from params, not from search params
setHighlight(false);
const stringifiedSearchParams = _searchParams.toString();
router.replace(`${pathname}${stringifiedSearchParams !== "" ? `?${stringifiedSearchParams}` : ""}`);
}, 3000);
}
return () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
timeoutRef.current = null;
}
};
}, [highlight, pathname, router, searchParams, shouldHighlight]);
return <AppListCard {...props} />;
}
+7
View File
@@ -0,0 +1,7 @@
export type CredentialOwner = {
name: string | null;
avatar?: string | null;
teamId?: number;
credentialId?: number;
readOnly?: boolean;
};
@@ -1,21 +1,28 @@
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Label, Input } from "@calcom/ui";
// TODO: This file is currently unused.
import { Label } from "../form/inputs/Label";
import { Input } from "../form/inputs/TextField";
type Digit = {
value: number;
onChange: () => void;
};
type Translations = {
labelText?: string;
};
type PropType = {
digits: Digit[];
digitClassName: string;
translations?: Translations;
};
const TokenHandler = ({ digits, digitClassName }: PropType) => {
const { t } = useLocale();
const TokenHandler = ({ digits, digitClassName, translations }: PropType) => {
const { labelText = "Code" } = translations ?? {};
return (
<div>
<Label htmlFor="code">{t("code")}</Label>
<Label htmlFor="code">{labelText}</Label>
<div className="flex flex-row justify-between">
{digits.map((element, index) => (
<Input
@@ -1,11 +1,40 @@
"use client";
import type { AppListCardProps } from "@calcom/features/apps/components/AppListCard";
import type { ReactNode } from "react";
import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Avatar, Badge, Icon, ListItemText } from "@calcom/ui";
import type { CredentialOwner } from "@calcom/types/CredentialOwner";
import classNames from "@calcom/ui/classNames";
import { Avatar } from "../avatar/Avatar";
import { Badge } from "../badge/Badge";
import { Icon } from "../icon/Icon";
import { ListItemText } from "../list/List";
type ShouldHighlight =
| {
slug: string;
shouldHighlight: true;
}
| {
shouldHighlight?: never;
slug?: never;
};
export type AppListCardProps = {
logo?: string;
title: string;
description: string;
actions?: ReactNode;
isDefault?: boolean;
isTemplate?: boolean;
invalidCredential?: boolean;
children?: ReactNode;
credentialOwner?: CredentialOwner;
className?: string;
} & ShouldHighlight;
export const AppListCard = (props: AppListCardProps & { highlight?: boolean }) => {
const { t } = useLocale();
const {
+3 -2
View File
@@ -13,11 +13,12 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { UserAdminTeams } from "@calcom/lib/server/repository/user";
import type { AppFrontendPayload as App } from "@calcom/types/App";
import type { CredentialFrontendPayload as Credential } from "@calcom/types/Credential";
import type { ButtonProps } from "@calcom/ui";
import { Badge, showToast } from "@calcom/ui";
import classNames from "@calcom/ui/classNames";
import { Badge } from "../badge/Badge";
import type { ButtonProps } from "../button";
import { Button } from "../button";
import { showToast } from "../toast/showToast";
interface AppCardProps {
app: App;
+2 -1
View File
@@ -2,9 +2,10 @@ import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
import { getUserAvatarUrl } from "@calcom/lib/getAvatarUrl";
import type { User } from "@calcom/prisma/client";
import type { UserProfile } from "@calcom/types/UserProfile";
import { Avatar } from "@calcom/ui";
import classNames from "@calcom/ui/classNames";
import { Avatar } from "./Avatar";
type Organization = {
id: number;
slug: string | null;
@@ -2,7 +2,8 @@ import { getUserAvatarUrl } from "@calcom/lib/getAvatarUrl";
import { getBookerBaseUrlSync } from "@calcom/lib/getBookerUrl/client";
import type { User } from "@calcom/prisma/client";
import type { UserProfile } from "@calcom/types/UserProfile";
import { AvatarGroup } from "@calcom/ui";
import { AvatarGroup } from "./AvatarGroup";
type UserAvatarProps = Omit<React.ComponentProps<typeof AvatarGroup>, "items"> & {
users: (Pick<User, "name" | "username" | "avatarUrl"> & {
@@ -3,7 +3,8 @@ import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
import { getUserAvatarUrl } from "@calcom/lib/getAvatarUrl";
import { getBookerBaseUrlSync } from "@calcom/lib/getBookerUrl/client";
import type { Team, User } from "@calcom/prisma/client";
import { AvatarGroup } from "@calcom/ui";
import { AvatarGroup } from "./AvatarGroup";
type UserAvatarProps = Omit<React.ComponentProps<typeof AvatarGroup>, "items"> & {
users: (Pick<User, "name" | "username" | "avatarUrl"> & {
+3 -3
View File
@@ -6,9 +6,9 @@ import React, { forwardRef } from "react";
import classNames from "@calcom/ui/classNames";
import { Icon } from "../icon";
import type { IconName } from "../icon";
import { Tooltip } from "../tooltip";
import { Icon } from "../icon/Icon";
import type { IconName } from "../icon/Icon";
import { Tooltip } from "../tooltip/Tooltip";
type InferredVariantProps = VariantProps<typeof buttonClasses>;
@@ -1,6 +1,9 @@
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { ButtonProps } from "@calcom/ui";
import { Button, ConfirmationDialogContent, Dialog, DialogTrigger } from "@calcom/ui";
import type { ButtonProps } from "../button/Button";
import { Button } from "../button/Button";
import { ConfirmationDialogContent } from "../dialog/ConfirmationDialogContent";
import { Dialog, DialogTrigger } from "../dialog/Dialog";
export const DisconnectIntegrationComponent = ({
label,
@@ -1,9 +1,8 @@
import type { IconName } from "@calcom/ui";
import classNames from "@calcom/ui/classNames";
import { Badge } from "../badge/Badge";
import { Button } from "../button/Button";
import {
Button,
Popover,
PopoverTrigger,
PopoverContent,
Command,
CommandInput,
CommandList,
@@ -11,10 +10,10 @@ import {
CommandGroup,
CommandItem,
CommandSeparator,
Badge,
Icon,
} from "@calcom/ui";
import classNames from "@calcom/ui/classNames";
} from "../command/Command";
import type { IconName } from "../icon/Icon";
import { Icon } from "../icon/Icon";
import { Popover, PopoverTrigger, PopoverContent } from "../popover/Popover";
export interface FilterOption {
value: string | number;
@@ -4,13 +4,18 @@ import { act } from "react-dom/test-utils";
import { vi } from "vitest";
import type { ButtonProps } from "../../button";
import { Button } from "../../button";
import ColorPicker from "./colorpicker";
vi.mock("@calcom/ui", async () => {
vi.mock("../../button/Button", async () => {
const ButtonMock = (await import("../../button/Button")).Button;
return {
Button: ({ tooltip, ...rest }: ButtonProps) => <ButtonMock {...rest}>{tooltip}</ButtonMock>,
};
});
vi.mock("../icon/Icon", async () => {
return {
Icon: () => <svg data-testid="dummy-icon" />,
Button: ({ tooltip, ...rest }: ButtonProps) => <Button {...rest}>{tooltip}</Button>,
};
});
@@ -5,9 +5,10 @@ import { useState } from "react";
import { HexColorInput, HexColorPicker } from "react-colorful";
import { fallBackHex, isValidHexCode } from "@calcom/lib/getBrandColours";
import { Button } from "@calcom/ui";
import cx from "@calcom/ui/classNames";
import { Button } from "../../button/Button";
export type ColorPickerProps = {
defaultValue: string;
onChange: (text: string) => void;
@@ -73,6 +74,7 @@ const ColorPicker = (props: ColorPickerProps) => {
target="_blank"
variant="icon"
rel="noreferrer"
aria-label="Reset to default"
StartIcon="rotate-ccw"
tooltip="Reset to default"
onClick={() => {
@@ -1,9 +1,9 @@
import * as Popover from "@radix-ui/react-popover";
import { format } from "date-fns";
import { Button } from "@calcom/ui";
import classNames from "@calcom/ui/classNames";
import { Button } from "../../button/Button";
import { Calendar } from "../date-range-picker/Calendar";
type Props = {
@@ -22,6 +22,11 @@ export function InputLeading(props: JSX.IntrinsicElements["div"]) {
);
}
type PasswordFieldTranslations = {
showPasswordText?: string;
hidePasswordText?: string;
};
export const PasswordField = forwardRef<HTMLInputElement, InputFieldProps>(function PasswordField(
props,
ref
+2 -1
View File
@@ -2,7 +2,8 @@ import type { VariantProps } from "class-variance-authority";
import type { inputStyles } from "./TextField";
export type InputFieldProps = {
export type InputFieldProps<Translations extends Record<string, string> = object> = {
translations?: Translations;
label?: React.ReactNode;
LockedIcon?: React.ReactNode;
hint?: string;
@@ -5,7 +5,7 @@ import * as React from "react";
import classNames from "@calcom/ui/classNames";
const Slider = React.forwardRef<
const RangeSlider = React.forwardRef<
React.ElementRef<typeof SliderPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>
>(({ className, ...props }, ref) => (
@@ -20,6 +20,6 @@ const Slider = React.forwardRef<
<SliderPrimitive.Thumb className="border-subtle bg-brand-default ring-offset-background focus-visible:ring-ring block h-4 w-4 rounded-full border-2 border-[1px] transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50" />
</SliderPrimitive.Root>
));
Slider.displayName = SliderPrimitive.Root.displayName;
RangeSlider.displayName = SliderPrimitive.Root.displayName;
export { Slider };
export { RangeSlider };
@@ -3,13 +3,15 @@
import * as Popover from "@radix-ui/react-popover";
import { useState } from "react";
import { Badge, Button } from "@calcom/ui";
import { Badge } from "../../badge/Badge";
import { Button } from "../../button/Button";
import { inputStyles, TextField } from "../inputs/TextField";
import { Slider } from "./index";
import { RangeSlider } from "./RangeSlider";
interface RangeSliderPopoverProps {
triggerText: string;
resetBtnText?: string;
applyBtnText?: string;
value: number[];
onChange: (value: number[]) => void;
min: number;
@@ -22,6 +24,8 @@ interface RangeSliderPopoverProps {
}
export const RangeSliderPopover = ({
resetBtnText = "Reset",
applyBtnText = "Apply",
triggerText,
value,
onChange,
@@ -62,7 +66,13 @@ export const RangeSliderPopover = ({
align="start"
className="border-subtle shadow-dropdown bg-default z-50 mt-2 w-72 rounded-lg border-[1px] px-4 pb-3 pt-4 font-[family-name:var(--font-inter)]">
<div className="mb-4">
<Slider min={min} max={max} step={step} value={internalValue} onValueChange={setInternalValue} />
<RangeSlider
min={min}
max={max}
step={step}
value={internalValue}
onValueChange={setInternalValue}
/>
</div>
<div className="mb-4 flex items-center justify-between gap-2">
<TextField
@@ -105,10 +115,10 @@ export const RangeSliderPopover = ({
<hr className="border-subtle" />
<div className="mt-3 flex items-center justify-end gap-2 px-4">
<Button color="minimal" size="sm" onClick={handleReset}>
Reset
{resetBtnText}
</Button>
<Button color="secondary" size="sm" onClick={handleApply}>
Apply
{applyBtnText}
</Button>
</div>
</div>
@@ -1,9 +1,10 @@
import * as RadixToggleGroup from "@radix-ui/react-toggle-group";
import type { ReactNode } from "react";
import { Tooltip } from "@calcom/ui";
import classNames from "@calcom/ui/classNames";
import { Tooltip } from "../../tooltip/Tooltip";
interface ToggleGroupProps extends Omit<RadixToggleGroup.ToggleGroupSingleProps, "type"> {
options: {
value: string;
+1 -1
View File
@@ -25,5 +25,5 @@ function Icon({
</svg>
);
}
export { IconName };
export { IconName, Icon };
export default Icon;
+1 -2
View File
@@ -2,8 +2,7 @@
import { fireEvent, render } from "@testing-library/react";
import { vi } from "vitest";
import { TableActions } from "@calcom/ui";
import { TableActions } from "./TableActions";
import { TableNewExampleComponent } from "./TableExamples";
const mockActions = [
@@ -1,6 +1,8 @@
import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { EmptyScreen, Avatar } from "@calcom/ui";
import { Avatar } from "../avatar/Avatar";
import { EmptyScreen } from "../empty-screen/EmptyScreen";
export type UnpublishedEntityProps = {
/**
+3 -3
View File
@@ -57,7 +57,7 @@ export {
inputStyles,
} from "./components/form";
export { Slider as RangeSlider } from "./components/form/slider";
export { RangeSlider } from "./components/form/slider/RangeSlider";
export { RangeSliderPopover } from "./components/form/slider/RangeSliderPopover";
export {
@@ -113,7 +113,7 @@ export {
ConfirmationContent,
} from "./components/dialog";
export type { DialogProps, ConfirmationDialogContentProps } from "./components/dialog";
export { AppListCard } from "./components/app-list-card";
export { AppListCard } from "./components/app-list-card/AppListCard";
export { DisconnectIntegrationComponent } from "./components/disconnect-calendar-integration";
export { CalendarSwitchComponent } from "./components/calendar-switch";
export { showToast, ErrorToast, SuccessToast, WarningToast } from "./components/toast"; // We don't export the toast components as they are only used in local storybook file
@@ -159,7 +159,7 @@ export {
CommandInput,
CommandSeparator,
CommandShortcut,
} from "./components/command";
} from "./components/command/Command";
export { HoverCard, HoverCardTrigger, HoverCardContent, HoverCardPortal } from "./components/hover-card";
+5 -1
View File
@@ -8,7 +8,11 @@ import { Toaster } from "sonner";
import { APP_NAME } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Button, SkeletonText, StepCard, Steps } from "@calcom/ui";
import { Button } from "../components/button/Button";
import { StepCard } from "../components/card/StepCard";
import { Steps } from "../components/form/step/Steps";
import { SkeletonText } from "../components/skeleton/Skeleton";
export function WizardLayout({
children,
+1
View File
@@ -55,6 +55,7 @@
"./components/icon/Spinner": "./components/icon/Spinner.tsx",
"./components/unpublished-entity/UnpublishedEntity": "./components/unpublished-entity/index.ts",
"./components/table/TableNew": "./components/table/TableNew.tsx",
"./components/app-list-card/AppListCard": "./components/app-list-card/AppListCard.tsx",
"./__mocks__/ui": "./__mocks__/ui.tsx"
},
"license": "MIT",