refactor(ui): deprecates Link props (#11155)

Deprecates all cases where `Link` could be sent as a prop. This was a
relic from the past, where we attempted to make our UI library
router-agnostic. This was a pipe dream and created more problems than it
solved, for example the logout button was missing this prop, causing it
to render an anchor tag and perform a hard navigation (caught in #9275).

Does so in a non-breaking way, where these props are now optional and
simply unused, as opposed to removing them outright.
This commit is contained in:
Jacob Fletcher
2025-02-13 11:10:57 -05:00
committed by GitHub
parent 3f550bc0ec
commit cd1117515b
16 changed files with 62 additions and 67 deletions

View File

@@ -1,7 +1,7 @@
import type { EntityToGroup } from '@payloadcms/ui/shared'
import type { ServerProps } from 'payload'
import { Link, Logout } from '@payloadcms/ui'
import { Logout } from '@payloadcms/ui'
import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent'
import { EntityType, groupNavItems } from '@payloadcms/ui/shared'
import React from 'react'
@@ -73,7 +73,6 @@ export const DefaultNav: React.FC<NavProps> = async (props) => {
const LogoutComponent = RenderServerComponent({
clientProps: {
documentSubViewType,
Link,
viewType,
},
Component: logout?.Button,

View File

@@ -17,7 +17,12 @@ export type DashboardProps = {
lockDuration?: number
slug: string
}>
Link: React.ComponentType<any>
/**
* @deprecated
* This prop is deprecated and will be removed in the next major version.
* Components now import their own `Link` directly from `next/link`.
*/
Link?: React.ComponentType
navGroups?: ReturnType<typeof groupNavItems>
permissions: SanitizedPermissions
visibleEntities: VisibleEntities
@@ -28,7 +33,6 @@ export const DefaultDashboard: React.FC<DashboardProps> = (props) => {
globalData,
i18n,
i18n: { t },
Link,
locale,
navGroups,
params,
@@ -146,7 +150,6 @@ export const DefaultDashboard: React.FC<DashboardProps> = (props) => {
el="link"
icon="plus"
iconStyle="with-border"
Link={Link}
round
to={createHREF}
/>
@@ -155,7 +158,6 @@ export const DefaultDashboard: React.FC<DashboardProps> = (props) => {
buttonAriaLabel={buttonAriaLabel}
href={href}
id={`card-${slug}`}
Link={Link}
title={getTranslation(label, i18n)}
titleAs="h3"
/>

View File

@@ -1,7 +1,7 @@
import type { EntityToGroup } from '@payloadcms/ui/shared'
import type { AdminViewProps } from 'payload'
import { HydrateAuthProvider, Link, SetStepNav } from '@payloadcms/ui'
import { HydrateAuthProvider, SetStepNav } from '@payloadcms/ui'
import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent'
import { EntityType, groupNavItems } from '@payloadcms/ui/shared'
import React, { Fragment } from 'react'
@@ -107,7 +107,6 @@ export const Dashboard: React.FC<AdminViewProps> = async ({
<SetStepNav nav={[]} />
{RenderServerComponent({
clientProps: {
Link,
locale,
},
Component: config.admin?.components?.views?.dashboard?.Component,

View File

@@ -52,7 +52,7 @@ export const ForgotPasswordView: React.FC<AdminViewProps> = ({ initPageResult })
}
heading={i18n.t('authentication:alreadyLoggedIn')}
/>
<Button buttonStyle="secondary" el="link" Link={Link} size="large" to={adminRoute}>
<Button buttonStyle="secondary" el="link" size="large" to={adminRoute}>
{i18n.t('general:backToDashboard')}
</Button>
</Fragment>

View File

@@ -1,7 +1,6 @@
'use client'
import {
Button,
Link,
LoadingOverlay,
toast,
useAuth,
@@ -69,7 +68,7 @@ export const LogoutClient: React.FC<{
return (
<div className={`${baseClass}__wrap`}>
<h2>{t('authentication:loggedOutInactivity')}</h2>
<Button buttonStyle="secondary" el="link" Link={Link} size="large" url={loginRoute}>
<Button buttonStyle="secondary" el="link" size="large" url={loginRoute}>
{t('authentication:logBackIn')}
</Button>
</div>

View File

@@ -1,5 +1,5 @@
'use client'
import { Button, Gutter, Link, useConfig, useStepNav, useTranslation } from '@payloadcms/ui'
import { Button, Gutter, useConfig, useStepNav, useTranslation } from '@payloadcms/ui'
import React, { useEffect } from 'react'
import './index.scss'
@@ -39,13 +39,7 @@ export const NotFoundClient: React.FC<{
<h1>{t('general:nothingFound')}</h1>
<p>{t('general:sorryNotFound')}</p>
</div>
<Button
className={`${baseClass}__button`}
el="link"
Link={Link}
size="large"
to={adminRoute}
>
<Button className={`${baseClass}__button`} el="link" size="large" to={adminRoute}>
{t('general:backToDashboard')}
</Button>
</Gutter>

View File

@@ -5,8 +5,8 @@ import { formatAdminURL, Translation } from '@payloadcms/ui/shared'
import React from 'react'
import { FormHeader } from '../../elements/FormHeader/index.js'
import './index.scss'
import { ResetPasswordForm } from './ResetPasswordForm/index.js'
import './index.scss'
export const resetPasswordBaseClass = 'reset-password'
@@ -57,7 +57,7 @@ export const ResetPassword: React.FC<AdminViewProps> = ({ initPageResult, params
}
heading={i18n.t('authentication:alreadyLoggedIn')}
/>
<Button buttonStyle="secondary" el="link" Link={Link} size="large" to={adminRoute}>
<Button buttonStyle="secondary" el="link" size="large" to={adminRoute}>
{i18n.t('general:backToDashboard')}
</Button>
</div>

View File

@@ -1,6 +1,6 @@
import type { AdminViewComponent, PayloadServerReactComponent } from 'payload'
import { Button, Link } from '@payloadcms/ui'
import { Button } from '@payloadcms/ui'
import { formatAdminURL } from '@payloadcms/ui/shared'
import React from 'react'
@@ -41,7 +41,6 @@ export const UnauthorizedView: PayloadServerReactComponent<AdminViewComponent> =
<Button
className={`${baseClass}__button`}
el="link"
Link={Link}
size="large"
to={formatAdminURL({
adminRoute,

View File

@@ -13,8 +13,8 @@ import { LocalizerLabel } from '../Localizer/LocalizerLabel/index.js'
import { useNav } from '../Nav/context.js'
import { NavToggler } from '../Nav/NavToggler/index.js'
import { RenderCustomComponent } from '../RenderCustomComponent/index.js'
import './index.scss'
import { StepNav } from '../StepNav/index.js'
import './index.scss'
const baseClass = 'app-header'
@@ -73,7 +73,7 @@ export function AppHeader({ CustomAvatar, CustomIcon }: Props) {
</NavToggler>
<div className={`${baseClass}__controls-wrapper`}>
<div className={`${baseClass}__step-nav-wrapper`}>
<StepNav className={`${baseClass}__step-nav`} CustomIcon={CustomIcon} Link={Link} />
<StepNav className={`${baseClass}__step-nav`} CustomIcon={CustomIcon} />
</div>
<div className={`${baseClass}__actions-wrapper`}>
<div className={`${baseClass}__actions`} ref={customControlsRef}>

View File

@@ -9,6 +9,7 @@ import { LinkIcon } from '../../icons/Link/index.js'
import { PlusIcon } from '../../icons/Plus/index.js'
import { SwapIcon } from '../../icons/Swap/index.js'
import { XIcon } from '../../icons/X/index.js'
import { Link } from '../Link/index.js'
import { Popup } from '../Popup/index.js'
import { Tooltip } from '../Tooltip/index.js'
import './index.scss'
@@ -61,7 +62,6 @@ export const Button: React.FC<Props> = (props) => {
icon,
iconPosition = 'right',
iconStyle = 'without-border',
Link,
newTab,
onClick,
onMouseDown,
@@ -143,26 +143,24 @@ export const Button: React.FC<Props> = (props) => {
break
case 'link':
if (!Link) {
console.error('Link is required when using el="link"', children)
return null
}
let LinkTag = Link // eslint-disable-line no-case-declarations
if (disabled) {
LinkTag = 'div'
} else {
prefetch = false
buttonElement = (
<div {...buttonProps}>
<ButtonContents icon={icon} showTooltip={showTooltip} tooltip={tooltip}>
{children}
</ButtonContents>
</div>
)
}
buttonElement = (
<LinkTag {...buttonProps} href={to || url} prefetch={prefetch} to={to || url}>
<Link {...buttonProps} href={to || url} prefetch={prefetch}>
<ButtonContents icon={icon} showTooltip={showTooltip} tooltip={tooltip}>
{children}
</ButtonContents>
</LinkTag>
</Link>
)
break
default:

View File

@@ -22,6 +22,11 @@ export type Props = {
iconPosition?: 'left' | 'right'
iconStyle?: 'none' | 'with-border' | 'without-border'
id?: string
/**
* @deprecated
* This prop is deprecated and will be removed in the next major version.
* Components now import their own `Link` directly from `next/link`.
*/
Link?: React.ElementType
newTab?: boolean
onClick?: (event: MouseEvent) => void

View File

@@ -1,5 +1,4 @@
'use client'
import type { ElementType } from 'react'
import React from 'react'
@@ -11,16 +10,21 @@ export type Props = {
buttonAriaLabel?: string
href?: string
id?: string
Link?: ElementType
/**
* @deprecated
* This prop is deprecated and will be removed in the next major version.
* Components now import their own `Link` directly from `next/link`.
*/
Link?: React.ElementType
onClick?: () => void
title: string
titleAs?: ElementType
titleAs?: React.ElementType
}
const baseClass = 'card'
export const Card: React.FC<Props> = (props) => {
const { id, actions, buttonAriaLabel, href, Link, onClick, title, titleAs } = props
const { id, actions, buttonAriaLabel, href, onClick, title, titleAs } = props
const classes = [baseClass, id, (onClick || href) && `${baseClass}--has-onclick`]
.filter(Boolean)
@@ -38,7 +42,6 @@ export const Card: React.FC<Props> = (props) => {
buttonStyle="none"
className={`${baseClass}__click`}
el="link"
Link={Link}
onClick={onClick}
to={href}
/>

View File

@@ -5,13 +5,19 @@ import { LogOutIcon } from '../../icons/LogOut/index.js'
import { useConfig } from '../../providers/Config/index.js'
import { useTranslation } from '../../providers/Translation/index.js'
import { formatAdminURL } from '../../utilities/formatAdminURL.js'
import { Link } from '../Link/index.js'
const baseClass = 'nav'
export const Logout: React.FC<{
/**
* @deprecated
* This prop is deprecated and will be removed in the next major version.
* Components now import their own `Link` directly from `next/link`.
*/
Link?: React.ComponentType
tabIndex?: number
}> = ({ Link, tabIndex = 0 }) => {
}> = ({ tabIndex = 0 }) => {
const { t } = useTranslation()
const { config } = useConfig()
@@ -23,7 +29,6 @@ export const Logout: React.FC<{
} = config
const basePath = process.env.NEXT_BASE_PATH ?? ''
const LinkElement = Link || 'a'
const props = {
'aria-label': t('authentication:logOut'),
@@ -34,7 +39,7 @@ export const Logout: React.FC<{
}
return (
<LinkElement
<Link
{...props}
href={formatAdminURL({
adminRoute,
@@ -43,6 +48,6 @@ export const Logout: React.FC<{
})}
>
<LogOutIcon />
</LinkElement>
</Link>
)
}

View File

@@ -8,6 +8,7 @@ import type { StepNavItem } from './types.js'
import { PayloadIcon } from '../../graphics/Icon/index.js'
import { useConfig } from '../../providers/Config/index.js'
import { useTranslation } from '../../providers/Translation/index.js'
import { Link } from '../Link/index.js'
import { RenderCustomComponent } from '../RenderCustomComponent/index.js'
import { StepNavProvider, useStepNav } from './context.js'
import './index.scss'
@@ -19,8 +20,13 @@ const baseClass = 'step-nav'
const StepNav: React.FC<{
readonly className?: string
readonly CustomIcon?: React.ReactNode
/**
* @deprecated
* This prop is deprecated and will be removed in the next major version.
* Components now import their own `Link` directly from `next/link`.
*/
readonly Link?: React.ComponentType
}> = ({ className, CustomIcon, Link }) => {
}> = ({ className, CustomIcon }) => {
const { i18n } = useTranslation()
const { stepNav } = useStepNav()
@@ -33,26 +39,15 @@ const StepNav: React.FC<{
const { t } = useTranslation()
const LinkElement = Link || 'a'
const baseLinkProps = {
prefetch: Link ? false : undefined,
}
return (
<Fragment>
{stepNav.length > 0 ? (
<nav className={[baseClass, className].filter(Boolean).join(' ')}>
<LinkElement
className={`${baseClass}__home`}
href={admin}
tabIndex={0}
{...baseLinkProps}
>
<Link className={`${baseClass}__home`} href={admin} prefetch={false} tabIndex={0}>
<span title={t('general:dashboard')}>
<RenderCustomComponent CustomComponent={CustomIcon} Fallback={<PayloadIcon />} />
</span>
</LinkElement>
</Link>
<span>/</span>
{stepNav.map((item, i) => {
const StepLabel = getTranslation(item.label, i18n)
@@ -65,9 +60,9 @@ const StepNav: React.FC<{
) : (
<Fragment key={i}>
{item.url ? (
<LinkElement href={item.url} {...baseLinkProps}>
<Link href={item.url} prefetch={false}>
<span key={i}>{StepLabel}</span>
</LinkElement>
</Link>
) : (
<span key={i}>{StepLabel}</span>
)}

View File

@@ -6,7 +6,6 @@ import { getTranslation } from '@payloadcms/translations'
import React from 'react'
import { Button } from '../../../elements/Button/index.js'
import { Link } from '../../../elements/Link/index.js'
import { useListDrawerContext } from '../../../elements/ListDrawer/Provider.js'
import { ListSelection } from '../../../elements/ListSelection/index.js'
import { Pill } from '../../../elements/Pill/index.js'
@@ -55,7 +54,6 @@ const DefaultListHeader: React.FC<ListHeaderProps> = ({
})}
buttonStyle="pill"
el={'link'}
Link={Link}
size="small"
to={newDocumentURL}
>

View File

@@ -14,7 +14,6 @@ import { Button } from '../../elements/Button/index.js'
import { DeleteMany } from '../../elements/DeleteMany/index.js'
import { EditMany } from '../../elements/EditMany/index.js'
import { Gutter } from '../../elements/Gutter/index.js'
import { Link } from '../../elements/Link/index.js'
import { ListControls } from '../../elements/ListControls/index.js'
import { useListDrawerContext } from '../../elements/ListDrawer/Provider.js'
import { ListSelection } from '../../elements/ListSelection/index.js'
@@ -238,7 +237,7 @@ export const DefaultListView: React.FC<ListViewClientProps> = (props) => {
})}
</Button>
) : (
<Button el="link" Link={Link} to={newDocumentURL}>
<Button el="link" to={newDocumentURL}>
{i18n.t('general:createNewLabel', {
label: getTranslation(labels?.singular, i18n),
})}