fix: Remove all links legacyBehavior (#25585)
* Remove all links legacyBehavior * fix: resolve type errors in Button and Dropdown when using Link without legacyBehavior - Button.tsx: Only pass ref to button element, not to Link (Link manages its own anchor) - Dropdown.tsx: Strip ref from props when using Link to avoid type incompatibility This fixes the type errors that were causing API V1 and V2 builds to fail. Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com> * fix: only pass disabled and type props to button element, not to Link Link component doesn't accept disabled or type props, so these should only be passed when rendering a button element. Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com> * fix: strip ref from passThroughProps when rendering Link The passThroughProps spread was including a ref property that's incompatible with Link's expected ref type. This strips the ref when rendering a Link component. Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com> * fix: use type assertion for React.createElement props to handle union types The Button component uses a union type for props that can be either Link or button props. TypeScript can't narrow the union type properly when using React.createElement with a dynamic element type, so we use a type assertion to cast the props to the correct type. Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com> * fix: render Link and button separately to avoid type conflicts Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com> * fix: preserve data-testid when rendering Button as Link Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com> --------- Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com> Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
Volnei Munhoz
Anik Dhabal Babu
Devin AI
parent
067fa7e137
commit
19ac0139c1
@@ -1,5 +1,4 @@
|
||||
import type { InferGetServerSidePropsType } from "next";
|
||||
import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
import { Toaster } from "sonner";
|
||||
|
||||
@@ -71,9 +70,10 @@ export default function MakeSetup({ inviteLink }: InferGetServerSidePropsType<ty
|
||||
<div className="bg-default m-auto max-w-[43em] overflow-auto rounded pb-10 md:p-10">
|
||||
<div className="md:flex md:flex-row">
|
||||
<div className="invisible md:visible">
|
||||
{/* eslint-disable @next/next/no-img-element */}
|
||||
<img className="h-11" src="/api/app-store/make/icon.svg" alt="Make Logo" />
|
||||
</div>
|
||||
<div className="ml-2 ltr:mr-2 rtl:ml-2 md:ml-5">
|
||||
<div className="ml-2 md:ml-5 ltr:mr-2 rtl:ml-2">
|
||||
<div className="text-default">{t("setting_up_make")}</div>
|
||||
|
||||
<>
|
||||
@@ -134,9 +134,9 @@ export default function MakeSetup({ inviteLink }: InferGetServerSidePropsType<ty
|
||||
<li>{t("make_setup_instructions_5")}</li>
|
||||
<li>{t("make_setup_instructions_6")}</li>
|
||||
</ol>
|
||||
<Link href="/apps/installed/automation?hl=make" passHref={true} legacyBehavior>
|
||||
<Button color="secondary">{t("done")}</Button>
|
||||
</Link>
|
||||
<Button href="/apps/installed/automation?hl=make" color="secondary">
|
||||
{t("done")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -45,12 +45,10 @@ function Expired() {
|
||||
<h2 className="text-emphasis text-center text-3xl font-extrabold">{t("request_is_expired")}</h2>
|
||||
</div>
|
||||
<p>{t("request_is_expired_instructions")}</p>
|
||||
<Link href="/auth/forgot-password" passHref legacyBehavior>
|
||||
<button
|
||||
type="button"
|
||||
className="flex w-full justify-center px-4 py-2 text-sm font-medium text-blue-600 focus:outline-none focus:ring-2 focus:ring-black focus:ring-offset-2">
|
||||
{t("try_again")}
|
||||
</button>
|
||||
<Link
|
||||
href="/auth/forgot-password"
|
||||
className="flex w-full justify-center px-4 py-2 text-sm font-medium text-blue-600 focus:outline-none focus:ring-2 focus:ring-black focus:ring-offset-2">
|
||||
{t("try_again")}
|
||||
</Link>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -53,7 +53,6 @@ import { EmptyScreen } from "@calcom/ui/components/empty-screen";
|
||||
import { EmailInput, TextArea } from "@calcom/ui/components/form";
|
||||
import { Icon } from "@calcom/ui/components/icon";
|
||||
import { showToast } from "@calcom/ui/components/toast";
|
||||
import { Tooltip } from "@calcom/ui/components/tooltip";
|
||||
import { useCalcomTheme } from "@calcom/ui/styles";
|
||||
import CancelBooking from "@calcom/web/components/booking/CancelBooking";
|
||||
import EventReservationSchema from "@calcom/web/components/schemas/EventReservationSchema";
|
||||
@@ -801,15 +800,14 @@ export default function Success(props: PageProps) {
|
||||
</span>
|
||||
{/* Login button but redirect to here */}
|
||||
<span className="text-default inline">
|
||||
<span className="underline" data-testid="reschedule-link">
|
||||
<Link
|
||||
href={`/auth/login?callbackUrl=${encodeURIComponent(
|
||||
`/booking/${bookingInfo?.uid}`
|
||||
)}`}
|
||||
legacyBehavior>
|
||||
{t("login")}
|
||||
</Link>
|
||||
</span>
|
||||
<Link
|
||||
href={`/auth/login?callbackUrl=${encodeURIComponent(
|
||||
`/booking/${bookingInfo?.uid}`
|
||||
)}`}
|
||||
className="underline"
|
||||
data-testid="reschedule-link">
|
||||
{t("login")}
|
||||
</Link>
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
@@ -839,17 +837,16 @@ export default function Success(props: PageProps) {
|
||||
canReschedule &&
|
||||
!isRescheduleDisabled && (
|
||||
<span className="text-default inline">
|
||||
<span className="underline" data-testid="reschedule-link">
|
||||
<Link
|
||||
href={`/reschedule/${seatReferenceUid || bookingInfo?.uid}${
|
||||
currentUserEmail
|
||||
? `?rescheduledBy=${encodeURIComponent(currentUserEmail)}`
|
||||
: ""
|
||||
}`}
|
||||
legacyBehavior>
|
||||
{t("reschedule")}
|
||||
</Link>
|
||||
</span>
|
||||
<Link
|
||||
href={`/reschedule/${seatReferenceUid || bookingInfo?.uid}${
|
||||
currentUserEmail
|
||||
? `?rescheduledBy=${encodeURIComponent(currentUserEmail)}`
|
||||
: ""
|
||||
}`}
|
||||
className="underline"
|
||||
data-testid="reschedule-link">
|
||||
{t("reschedule")}
|
||||
</Link>
|
||||
{!isBookingInPast && canCancel && (
|
||||
<span className="mx-2">{t("or_lowercase")}</span>
|
||||
)}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import Link from "next/link";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
import { Icon } from "@calcom/ui/components/icon";
|
||||
@@ -17,12 +15,10 @@ export default function AppNotInstalledMessage({ appName }: { appName: string })
|
||||
<p className="text-subtle px-1 leading-normal">{t("visit_our_app_store")}</p>
|
||||
|
||||
<div className="mt-5">
|
||||
<Link href={`/apps/${appName}`} passHref={true} legacyBehavior>
|
||||
<Button type="button" color="secondary">
|
||||
{t("go_to_app_store")}
|
||||
<Icon name="arrow-up-right" className="ml-1" size={20} />
|
||||
</Button>
|
||||
</Link>
|
||||
<Button href={`/apps/${appName}`} type="button" color="secondary">
|
||||
{t("go_to_app_store")}
|
||||
<Icon name="arrow-up-right" className="ml-1" size={20} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -239,25 +239,17 @@ export const Button = forwardRef<HTMLAnchorElement | HTMLButtonElement, ButtonPr
|
||||
...passThroughProps
|
||||
} = props;
|
||||
// Buttons are **always** disabled if we're in a `loading` state
|
||||
const disabled = props.disabled || loading;
|
||||
// If pass an `href`-attr is passed it's `<a>`, otherwise it's a `<button />`
|
||||
const disabled = props.disabled || loading || false;
|
||||
// If pass an `href`-attr is passed it's Link, otherwise it's a `<button />`
|
||||
const isLink = typeof props.href !== "undefined";
|
||||
const elementType = isLink ? "a" : "button";
|
||||
const element = React.createElement(
|
||||
elementType,
|
||||
{
|
||||
...passThroughProps,
|
||||
disabled,
|
||||
type: !isLink ? type : undefined,
|
||||
ref: forwardedRef,
|
||||
className: classNames(buttonClasses({ color, size, loading, variant }), props.className),
|
||||
// if we click a disabled button, we prevent going through the click handler
|
||||
onClick: disabled
|
||||
? (e: React.MouseEvent<HTMLElement, MouseEvent>) => {
|
||||
e.preventDefault();
|
||||
}
|
||||
: props.onClick,
|
||||
},
|
||||
const buttonClassName = classNames(buttonClasses({ color, size, loading, variant }), props.className);
|
||||
const handleClick = disabled
|
||||
? (e: React.MouseEvent<HTMLElement, MouseEvent>) => {
|
||||
e.preventDefault();
|
||||
}
|
||||
: props.onClick;
|
||||
|
||||
const buttonContent = (
|
||||
<>
|
||||
{CustomStartIcon ||
|
||||
(StartIcon && (
|
||||
@@ -334,18 +326,36 @@ export const Button = forwardRef<HTMLAnchorElement | HTMLButtonElement, ButtonPr
|
||||
</>
|
||||
);
|
||||
|
||||
return props.href ? (
|
||||
<Link data-testid="link-component" passHref href={props.href} shallow={shallow && shallow} legacyBehavior>
|
||||
{element}
|
||||
</Link>
|
||||
) : (
|
||||
// Render Link or button separately to avoid type conflicts
|
||||
// Link manages its own anchor element, so we don't pass ref to it
|
||||
if (isLink) {
|
||||
return (
|
||||
<Link
|
||||
{...(passThroughProps as Omit<JSX.IntrinsicElements["a"], "href" | "onClick" | "ref"> & LinkProps)}
|
||||
shallow={shallow && shallow}
|
||||
className={buttonClassName}
|
||||
onClick={handleClick}>
|
||||
{buttonContent}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Wrapper
|
||||
data-testid="wrapper"
|
||||
tooltip={props.tooltip}
|
||||
tooltipSide={tooltipSide}
|
||||
tooltipOffset={tooltipOffset}
|
||||
tooltipClassName={tooltipClassName}>
|
||||
{element}
|
||||
<button
|
||||
{...(passThroughProps as Omit<JSX.IntrinsicElements["button"], "onClick" | "ref">)}
|
||||
ref={forwardedRef as React.Ref<HTMLButtonElement>}
|
||||
disabled={disabled}
|
||||
type={type as "button" | "submit" | "reset"}
|
||||
className={buttonClassName}
|
||||
onClick={handleClick}>
|
||||
{buttonContent}
|
||||
</button>
|
||||
</Wrapper>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -143,19 +143,14 @@ type ButtonOrLinkProps = ComponentProps<"button"> & ComponentProps<"a">;
|
||||
|
||||
export function ButtonOrLink({ href, ...props }: ButtonOrLinkProps) {
|
||||
const isLink = typeof href !== "undefined";
|
||||
const ButtonOrLink = isLink ? "a" : "button";
|
||||
|
||||
const content = <ButtonOrLink {...props} />;
|
||||
|
||||
if (isLink) {
|
||||
return (
|
||||
<Link href={href} legacyBehavior>
|
||||
{content}
|
||||
</Link>
|
||||
);
|
||||
// Strip ref from props when using Link (Link manages its own anchor element)
|
||||
const { ref: _ref, ...linkProps } = props;
|
||||
return <Link href={href} {...linkProps} />;
|
||||
}
|
||||
|
||||
return content;
|
||||
return <button {...props} />;
|
||||
}
|
||||
|
||||
export const DropdownItem = (props: DropdownItemProps) => {
|
||||
|
||||
@@ -26,32 +26,39 @@ function Stepper<T extends DefaultStep>(props: {
|
||||
<ol role="list" className="ml-8 flex items-center space-x-5" ref={stepperRef}>
|
||||
{steps.map((mapStep, index) => (
|
||||
<li key={mapStep.title}>
|
||||
<Link
|
||||
href={props.disableSteps ? "#" : `${href}?step=${index + 1}`}
|
||||
shallow
|
||||
replace
|
||||
legacyBehavior>
|
||||
{index + 1 < props.step ? (
|
||||
<a className="hover:bg-inverted block h-2.5 w-2.5 rounded-full bg-gray-600">
|
||||
<span className="sr-only">{mapStep.title}</span>
|
||||
</a>
|
||||
) : index + 1 === props.step ? (
|
||||
<a className="relative flex items-center justify-center" aria-current="step">
|
||||
<span className="absolute flex h-5 w-5 p-px" aria-hidden="true">
|
||||
<span className="bg-emphasis h-full w-full rounded-full" />
|
||||
</span>
|
||||
<span
|
||||
className="relative block h-2.5 w-2.5 rounded-full bg-gray-600"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span className="sr-only">{mapStep.title}</span>
|
||||
</a>
|
||||
) : (
|
||||
<a className="bg-emphasis block h-2.5 w-2.5 rounded-full hover:bg-gray-400">
|
||||
<span className="sr-only">{mapStep.title}</span>
|
||||
</a>
|
||||
)}
|
||||
</Link>
|
||||
{index + 1 < props.step ? (
|
||||
<Link
|
||||
href={props.disableSteps ? "#" : `${href}?step=${index + 1}`}
|
||||
shallow
|
||||
replace
|
||||
className="hover:bg-inverted block h-2.5 w-2.5 rounded-full bg-gray-600">
|
||||
<span className="sr-only">{mapStep.title}</span>
|
||||
</Link>
|
||||
) : index + 1 === props.step ? (
|
||||
<Link
|
||||
href={props.disableSteps ? "#" : `${href}?step=${index + 1}`}
|
||||
shallow
|
||||
replace
|
||||
className="relative flex items-center justify-center"
|
||||
aria-current="step">
|
||||
<span className="absolute flex h-5 w-5 p-px" aria-hidden="true">
|
||||
<span className="bg-emphasis h-full w-full rounded-full" />
|
||||
</span>
|
||||
<span
|
||||
className="relative block h-2.5 w-2.5 rounded-full bg-gray-600"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span className="sr-only">{mapStep.title}</span>
|
||||
</Link>
|
||||
) : (
|
||||
<Link
|
||||
href={props.disableSteps ? "#" : `${href}?step=${index + 1}`}
|
||||
shallow
|
||||
replace
|
||||
className="bg-emphasis block h-2.5 w-2.5 rounded-full hover:bg-gray-400">
|
||||
<span className="sr-only">{mapStep.title}</span>
|
||||
</Link>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
|
||||
@@ -57,7 +57,7 @@ export function ListItem(props: ListItemProps) {
|
||||
);
|
||||
|
||||
return href ? (
|
||||
<Link passHref href={href} legacyBehavior>
|
||||
<Link href={href}>
|
||||
{element}
|
||||
</Link>
|
||||
) : (
|
||||
|
||||
Reference in New Issue
Block a user