diff --git a/docs/admin/hooks.mdx b/docs/admin/hooks.mdx index 2497ed669f..083e642653 100644 --- a/docs/admin/hooks.mdx +++ b/docs/admin/hooks.mdx @@ -1113,5 +1113,40 @@ setParams({ depth: 2 }) This is useful for scenarios where you need to trigger another fetch regardless of the `url` argument changing. +## useRouteTransition +Route transitions are useful in showing immediate visual feedback to the user when navigating between pages. This is especially useful on slow networks when navigating to data heavy or process intensive pages. +By default, any instances of `Link` from `@payloadcms/ui` will trigger route transitions dy default. + +```tsx +import { Link } from '@payloadcms/ui' + +const MyComponent = () => { + return ( + + Go Somewhere + + ) +} +``` + +You can also trigger route transitions programmatically, such as when using `router.push` from `next/router`. To do this, wrap your function calls with the `startRouteTransition` method provided by the `useRouteTransition` hook. + +```ts +'use client' +import React, { useCallback } from 'react' +import { useTransition } from '@payloadcms/ui' +import { useRouter } from 'next/navigation' + +const MyComponent: React.FC = () => { + const router = useRouter() + const { startRouteTransition } = useRouteTransition() + + const redirectSomewhere = useCallback(() => { + startRouteTransition(() => router.push('/somewhere')) + }, [startRouteTransition, router]) + + // ... +} +``` diff --git a/packages/next/src/elements/DocumentHeader/Tabs/Tab/TabLink.tsx b/packages/next/src/elements/DocumentHeader/Tabs/Tab/TabLink.tsx index 20e117e731..526551dfbb 100644 --- a/packages/next/src/elements/DocumentHeader/Tabs/Tab/TabLink.tsx +++ b/packages/next/src/elements/DocumentHeader/Tabs/Tab/TabLink.tsx @@ -1,13 +1,11 @@ 'use client' import type { SanitizedConfig } from 'payload' +import { Link } from '@payloadcms/ui' import { formatAdminURL } from '@payloadcms/ui/shared' -import LinkImport from 'next/link.js' import { useParams, usePathname, useSearchParams } from 'next/navigation.js' import React from 'react' -const Link = (LinkImport.default || LinkImport) as unknown as typeof LinkImport.default - export const DocumentTabLink: React.FC<{ adminRoute: SanitizedConfig['routes']['admin'] ariaLabel?: string diff --git a/packages/next/src/elements/Nav/index.client.tsx b/packages/next/src/elements/Nav/index.client.tsx index bada443a98..aeedc49576 100644 --- a/packages/next/src/elements/Nav/index.client.tsx +++ b/packages/next/src/elements/Nav/index.client.tsx @@ -4,9 +4,8 @@ import type { groupNavItems } from '@payloadcms/ui/shared' import type { NavPreferences } from 'payload' import { getTranslation } from '@payloadcms/translations' -import { NavGroup, useConfig, useTranslation } from '@payloadcms/ui' +import { Link, NavGroup, useConfig, useTranslation } from '@payloadcms/ui' import { EntityType, formatAdminURL } from '@payloadcms/ui/shared' -import LinkWithDefault from 'next/link.js' import { usePathname } from 'next/navigation.js' import React, { Fragment } from 'react' @@ -45,9 +44,6 @@ export const DefaultNavClient: React.FC<{ id = `nav-global-${slug}` } - const Link = (LinkWithDefault.default || - LinkWithDefault) as typeof LinkWithDefault.default - const LinkElement = Link || 'a' const activeCollection = pathname.startsWith(href) && ['/', undefined].includes(pathname[href.length]) diff --git a/packages/next/src/elements/Nav/index.tsx b/packages/next/src/elements/Nav/index.tsx index a1d09e70e5..2cd4ec6eeb 100644 --- a/packages/next/src/elements/Nav/index.tsx +++ b/packages/next/src/elements/Nav/index.tsx @@ -1,14 +1,14 @@ import type { EntityToGroup } from '@payloadcms/ui/shared' import type { ServerProps } from 'payload' -import { Logout } from '@payloadcms/ui' +import { Link, Logout } from '@payloadcms/ui' import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent' import { EntityType, groupNavItems } from '@payloadcms/ui/shared' import React from 'react' -import './index.scss' import { NavHamburger } from './NavHamburger/index.js' import { NavWrapper } from './NavWrapper/index.js' +import './index.scss' const baseClass = 'nav' @@ -73,6 +73,7 @@ export const DefaultNav: React.FC = async (props) => { const LogoutComponent = RenderServerComponent({ clientProps: { documentSubViewType, + Link, viewType, }, Component: logout?.Button, diff --git a/packages/next/src/layouts/Root/index.tsx b/packages/next/src/layouts/Root/index.tsx index 198547d523..5ec8273be7 100644 --- a/packages/next/src/layouts/Root/index.tsx +++ b/packages/next/src/layouts/Root/index.tsx @@ -2,7 +2,7 @@ import type { AcceptedLanguages } from '@payloadcms/translations' import type { ImportMap, LanguageOptions, SanitizedConfig, ServerFunctionClient } from 'payload' import { rtlLanguages } from '@payloadcms/translations' -import { RootProvider } from '@payloadcms/ui' +import { ProgressBar, RootProvider } from '@payloadcms/ui' import { getClientConfig } from '@payloadcms/ui/utilities/getClientConfig' import { headers as getHeaders, cookies as nextCookies } from 'next/headers.js' import { getPayload, getRequestLanguage, parseCookies } from 'payload' @@ -135,6 +135,7 @@ export const RootLayout = async ({ translations={req.i18n.translations} user={req.user} > + {Array.isArray(config.admin?.components?.providers) && config.admin?.components?.providers.length > 0 ? ( = async ({ initPageResult, params, @@ -119,7 +116,6 @@ export const Dashboard: React.FC = async ({ serverProps: { globalData, i18n, - Link, locale, navGroups, params, diff --git a/packages/next/src/views/ForgotPassword/index.tsx b/packages/next/src/views/ForgotPassword/index.tsx index 1c729292a2..5337ed0897 100644 --- a/packages/next/src/views/ForgotPassword/index.tsx +++ b/packages/next/src/views/ForgotPassword/index.tsx @@ -1,8 +1,7 @@ import type { AdminViewProps } from 'payload' -import { Button } from '@payloadcms/ui' +import { Button, Link } from '@payloadcms/ui' import { formatAdminURL, Translation } from '@payloadcms/ui/shared' -import LinkImport from 'next/link.js' import React, { Fragment } from 'react' import { FormHeader } from '../../elements/FormHeader/index.js' @@ -10,7 +9,6 @@ import { ForgotPasswordForm } from './ForgotPasswordForm/index.js' export { generateForgotPasswordMetadata } from './meta.js' -const Link = (LinkImport.default || LinkImport) as unknown as typeof LinkImport.default export const forgotPasswordBaseClass = 'forgot-password' export const ForgotPasswordView: React.FC = ({ initPageResult }) => { diff --git a/packages/next/src/views/LivePreview/index.client.tsx b/packages/next/src/views/LivePreview/index.client.tsx index d1c28a1770..a1cb8eb93d 100644 --- a/packages/next/src/views/LivePreview/index.client.tsx +++ b/packages/next/src/views/LivePreview/index.client.tsx @@ -28,6 +28,7 @@ import { useDocumentEvents, useDocumentInfo, useEditDepth, + useRouteTransition, useServerFunctions, useTranslation, useUploadEdits, @@ -131,6 +132,7 @@ const PreviewView: React.FC = ({ const { reportUpdate } = useDocumentEvents() const { resetUploadEdits } = useUploadEdits() const { getFormState } = useServerFunctions() + const { startRouteTransition } = useRouteTransition() const docConfig = collectionConfig || globalConfig @@ -211,7 +213,8 @@ const PreviewView: React.FC = ({ adminRoute, path: `/collections/${collectionSlug}/${json?.doc?.id}${locale ? `?locale=${locale}` : ''}`, }) - router.push(redirectRoute) + + startRouteTransition(() => router.push(redirectRoute)) } else { resetUploadEdits() } @@ -269,6 +272,7 @@ const PreviewView: React.FC = ({ router, setDocumentIsLocked, updateSavedDocumentData, + startRouteTransition, user, userSlug, autosaveEnabled, diff --git a/packages/next/src/views/Login/LoginForm/index.tsx b/packages/next/src/views/Login/LoginForm/index.tsx index 462cea6a9b..1816c8b8cd 100644 --- a/packages/next/src/views/Login/LoginForm/index.tsx +++ b/packages/next/src/views/Login/LoginForm/index.tsx @@ -1,15 +1,21 @@ 'use client' -import LinkImport from 'next/link.js' import React from 'react' const baseClass = 'login__form' -const Link = (LinkImport.default || LinkImport) as unknown as typeof LinkImport.default import type { UserWithToken } from '@payloadcms/ui' import type { FormState } from 'payload' -import { Form, FormSubmit, PasswordField, useAuth, useConfig, useTranslation } from '@payloadcms/ui' +import { + Form, + FormSubmit, + Link, + PasswordField, + useAuth, + useConfig, + useTranslation, +} from '@payloadcms/ui' import { formatAdminURL } from '@payloadcms/ui/shared' import { getLoginOptions } from 'payload/shared' diff --git a/packages/next/src/views/Login/index.tsx b/packages/next/src/views/Login/index.tsx index 7e1dbc8f44..905ff2777d 100644 --- a/packages/next/src/views/Login/index.tsx +++ b/packages/next/src/views/Login/index.tsx @@ -5,8 +5,8 @@ import { redirect } from 'next/navigation.js' import React, { Fragment } from 'react' import { Logo } from '../../elements/Logo/index.js' -import './index.scss' import { LoginForm } from './LoginForm/index.js' +import './index.scss' export { generateLoginMetadata } from './meta.js' diff --git a/packages/next/src/views/Logout/LogoutClient.tsx b/packages/next/src/views/Logout/LogoutClient.tsx index 2e1195e630..397ebec52b 100644 --- a/packages/next/src/views/Logout/LogoutClient.tsx +++ b/packages/next/src/views/Logout/LogoutClient.tsx @@ -1,7 +1,14 @@ 'use client' -import { Button, LoadingOverlay, toast, useAuth, useTranslation } from '@payloadcms/ui' +import { + Button, + Link, + LoadingOverlay, + toast, + useAuth, + useRouteTransition, + useTranslation, +} from '@payloadcms/ui' import { formatAdminURL } from '@payloadcms/ui/shared' -import LinkImport from 'next/link.js' import { useRouter } from 'next/navigation.js' import React, { useEffect } from 'react' @@ -9,8 +16,6 @@ import './index.scss' const baseClass = 'logout' -const Link = (LinkImport.default || LinkImport) as unknown as typeof LinkImport.default - export const LogoutClient: React.FC<{ adminRoute: string inactivity?: boolean @@ -20,6 +25,8 @@ export const LogoutClient: React.FC<{ const { logOut, user } = useAuth() + const { startRouteTransition } = useRouteTransition() + const [isLoggedOut, setIsLoggedOut] = React.useState(!user) const logOutSuccessRef = React.useRef(false) @@ -41,21 +48,22 @@ export const LogoutClient: React.FC<{ const handleLogOut = React.useCallback(async () => { const loggedOut = await logOut() setIsLoggedOut(loggedOut) + if (!inactivity && loggedOut && !logOutSuccessRef.current) { toast.success(t('authentication:loggedOutSuccessfully')) logOutSuccessRef.current = true - router.push(loginRoute) + startRouteTransition(() => router.push(loginRoute)) return } - }, [inactivity, logOut, loginRoute, router, t]) + }, [inactivity, logOut, loginRoute, router, startRouteTransition, t]) useEffect(() => { if (!isLoggedOut) { void handleLogOut() } else { - router.push(loginRoute) + startRouteTransition(() => router.push(loginRoute)) } - }, [handleLogOut, isLoggedOut, loginRoute, router]) + }, [handleLogOut, isLoggedOut, loginRoute, router, startRouteTransition]) if (isLoggedOut && inactivity) { return ( diff --git a/packages/next/src/views/NotFound/index.client.tsx b/packages/next/src/views/NotFound/index.client.tsx index 172fdf40dd..5a47b99383 100644 --- a/packages/next/src/views/NotFound/index.client.tsx +++ b/packages/next/src/views/NotFound/index.client.tsx @@ -1,14 +1,11 @@ 'use client' -import { Button, Gutter, useConfig, useStepNav, useTranslation } from '@payloadcms/ui' -import LinkImport from 'next/link.js' +import { Button, Gutter, Link, useConfig, useStepNav, useTranslation } from '@payloadcms/ui' import React, { useEffect } from 'react' import './index.scss' const baseClass = 'not-found' -const Link = (LinkImport.default || LinkImport) as unknown as typeof LinkImport.default - export const NotFoundClient: React.FC<{ marginTop?: 'large' }> = (props) => { diff --git a/packages/next/src/views/ResetPassword/index.tsx b/packages/next/src/views/ResetPassword/index.tsx index e24439cb57..1f1330b04c 100644 --- a/packages/next/src/views/ResetPassword/index.tsx +++ b/packages/next/src/views/ResetPassword/index.tsx @@ -1,8 +1,7 @@ import type { AdminViewProps } from 'payload' -import { Button } from '@payloadcms/ui' +import { Button, Link } from '@payloadcms/ui' import { formatAdminURL, Translation } from '@payloadcms/ui/shared' -import LinkImport from 'next/link.js' import React from 'react' import { FormHeader } from '../../elements/FormHeader/index.js' @@ -11,8 +10,6 @@ import { ResetPasswordForm } from './ResetPasswordForm/index.js' export const resetPasswordBaseClass = 'reset-password' -const Link = (LinkImport.default || LinkImport) as unknown as typeof LinkImport.default - export { generateResetPasswordMetadata } from './meta.js' export const ResetPassword: React.FC = ({ initPageResult, params }) => { diff --git a/packages/next/src/views/Unauthorized/index.tsx b/packages/next/src/views/Unauthorized/index.tsx index d6f10a6471..e90b558e57 100644 --- a/packages/next/src/views/Unauthorized/index.tsx +++ b/packages/next/src/views/Unauthorized/index.tsx @@ -1,15 +1,12 @@ import type { AdminViewComponent, PayloadServerReactComponent } from 'payload' -import { Button } from '@payloadcms/ui' +import { Button, Link } from '@payloadcms/ui' import { formatAdminURL } from '@payloadcms/ui/shared' -import LinkImport from 'next/link.js' import React from 'react' import { FormHeader } from '../../elements/FormHeader/index.js' import './index.scss' -const Link = (LinkImport.default || LinkImport) as unknown as typeof LinkImport.default - export { generateUnauthorizedMetadata } from './meta.js' const baseClass = 'unauthorized' diff --git a/packages/next/src/views/Verify/index.client.tsx b/packages/next/src/views/Verify/index.client.tsx index 889e3bf389..ebd032792e 100644 --- a/packages/next/src/views/Verify/index.client.tsx +++ b/packages/next/src/views/Verify/index.client.tsx @@ -1,5 +1,5 @@ 'use client' -import { toast } from '@payloadcms/ui' +import { toast, useRouteTransition } from '@payloadcms/ui' import { useRouter } from 'next/navigation.js' import React, { useEffect } from 'react' @@ -9,15 +9,17 @@ type Props = { } export function ToastAndRedirect({ message, redirectTo }: Props) { const router = useRouter() + const { startRouteTransition } = useRouteTransition() const hasToastedRef = React.useRef(false) useEffect(() => { let timeoutID + if (toast) { timeoutID = setTimeout(() => { toast.success(message) hasToastedRef.current = true - router.push(redirectTo) + startRouteTransition(() => router.push(redirectTo)) }, 100) } @@ -26,7 +28,7 @@ export function ToastAndRedirect({ message, redirectTo }: Props) { clearTimeout(timeoutID) } } - }, [router, redirectTo, message]) + }, [router, redirectTo, message, startRouteTransition]) return null } diff --git a/packages/next/src/views/Version/Default/index.tsx b/packages/next/src/views/Version/Default/index.tsx index dcab43c711..d25dfb9261 100644 --- a/packages/next/src/views/Version/Default/index.tsx +++ b/packages/next/src/views/Version/Default/index.tsx @@ -1,7 +1,14 @@ 'use client' import type { OptionObject } from 'payload' -import { CheckboxInput, Gutter, useConfig, useDocumentInfo, useTranslation } from '@payloadcms/ui' +import { + CheckboxInput, + Gutter, + useConfig, + useDocumentInfo, + useRouteTransition, + useTranslation, +} from '@payloadcms/ui' import { formatDate } from '@payloadcms/ui/shared' import { usePathname, useRouter, useSearchParams } from 'next/navigation.js' import React, { useEffect, useMemo, useState } from 'react' @@ -42,6 +49,7 @@ export const DefaultVersionView: React.FC = ({ const { i18n } = useTranslation() const { id, collectionSlug, globalSlug } = useDocumentInfo() + const { startRouteTransition } = useRouteTransition() const [collectionConfig] = useState(() => getEntityConfig({ collectionSlug })) @@ -59,8 +67,8 @@ export const DefaultVersionView: React.FC = ({ } useEffect(() => { - // If the selected comparison doc or locales change, update URL params so that version page RSC - // can update the version comparison state + // If the selected comparison doc or locales change, update URL params so that version page + // This is so that RSC can update the version comparison state const current = new URLSearchParams(Array.from(searchParams.entries())) if (!compareValue) { @@ -68,6 +76,7 @@ export const DefaultVersionView: React.FC = ({ } else { current.set('compareValue', compareValue?.value) } + if (!selectedLocales) { current.delete('localeCodes') } else { @@ -82,8 +91,18 @@ export const DefaultVersionView: React.FC = ({ const search = current.toString() const query = search ? `?${search}` : '' - router.push(`${pathname}${query}`) - }, [compareValue, pathname, router, searchParams, selectedLocales, modifiedOnly]) + + // TODO: this transition occurs multiple times during the initial rendering phases, need to evaluate + startRouteTransition(() => router.push(`${pathname}${query}`)) + }, [ + compareValue, + pathname, + router, + searchParams, + selectedLocales, + modifiedOnly, + startRouteTransition, + ]) const { admin: { dateFormat }, diff --git a/packages/next/src/views/Version/Restore/index.tsx b/packages/next/src/views/Version/Restore/index.tsx index 2bba551696..ec042c3ae7 100644 --- a/packages/next/src/views/Version/Restore/index.tsx +++ b/packages/next/src/views/Version/Restore/index.tsx @@ -9,6 +9,7 @@ import { PopupList, useConfig, useModal, + useRouteTransition, useTranslation, } from '@payloadcms/ui' import { formatAdminURL, requests } from '@payloadcms/ui/shared' @@ -48,6 +49,7 @@ const Restore: React.FC = ({ const router = useRouter() const { i18n, t } = useTranslation() const [draft, setDraft] = useState(false) + const { startRouteTransition } = useRouteTransition() const restoreMessage = t('version:aboutToRestoreGlobal', { label: getTranslation(label, i18n), @@ -87,11 +89,12 @@ const Restore: React.FC = ({ if (res.status === 200) { const json = await res.json() toast.success(json.message) - router.push(redirectURL) + startRouteTransition(() => router.push(redirectURL)) } else { toast.error(t('version:problemRestoringVersion')) } - }, [fetchURL, redirectURL, t, i18n, router]) + }, [fetchURL, redirectURL, t, i18n, router, startRouteTransition]) + return (
diff --git a/packages/next/src/views/Versions/cells/CreatedAt/index.tsx b/packages/next/src/views/Versions/cells/CreatedAt/index.tsx index 0b5e6bc941..9bee7ca9c5 100644 --- a/packages/next/src/views/Versions/cells/CreatedAt/index.tsx +++ b/packages/next/src/views/Versions/cells/CreatedAt/index.tsx @@ -1,11 +1,8 @@ 'use client' -import { useConfig, useTranslation } from '@payloadcms/ui' +import { Link, useConfig, useTranslation } from '@payloadcms/ui' import { formatAdminURL, formatDate } from '@payloadcms/ui/shared' -import LinkImport from 'next/link.js' import React from 'react' -const Link = (LinkImport.default || LinkImport) as unknown as typeof LinkImport.default - type CreatedAtCellProps = { collectionSlug?: string docID?: number | string diff --git a/packages/plugin-search/src/Search/ui/LinkToDoc/index.client.tsx b/packages/plugin-search/src/Search/ui/LinkToDoc/index.client.tsx index 39dd16ad8e..b3960fd24f 100644 --- a/packages/plugin-search/src/Search/ui/LinkToDoc/index.client.tsx +++ b/packages/plugin-search/src/Search/ui/LinkToDoc/index.client.tsx @@ -1,12 +1,9 @@ 'use client' -import { CopyToClipboard, useConfig, useField } from '@payloadcms/ui' +import { CopyToClipboard, Link, useConfig, useField } from '@payloadcms/ui' import { formatAdminURL } from '@payloadcms/ui/shared' -import LinkImport from 'next/link.js' import React from 'react' -const Link = (LinkImport.default || LinkImport) as unknown as typeof LinkImport.default - export const LinkToDocClient: React.FC = () => { const { config } = useConfig() diff --git a/packages/richtext-lexical/src/cell/rscEntry.tsx b/packages/richtext-lexical/src/cell/rscEntry.tsx index 11790ada18..7ecec35bee 100644 --- a/packages/richtext-lexical/src/cell/rscEntry.tsx +++ b/packages/richtext-lexical/src/cell/rscEntry.tsx @@ -2,15 +2,13 @@ import type { SerializedLexicalNode } from 'lexical' import type { Payload } from 'payload' import { getTranslation, type I18nClient } from '@payloadcms/translations' +import { Link } from '@payloadcms/ui' import { formatAdminURL } from '@payloadcms/ui/shared' -import LinkImport from 'next/link.js' import React from 'react' import type { SanitizedServerEditorConfig } from '../lexical/config/types.js' import type { LexicalFieldAdminProps, LexicalRichTextCellProps } from '../types.js' -const Link = (LinkImport.default || LinkImport) as unknown as typeof LinkImport.default - function recurseEditorState( editorState: SerializedLexicalNode[], textContent: React.ReactNode[], diff --git a/packages/richtext-slate/src/cell/rscEntry.tsx b/packages/richtext-slate/src/cell/rscEntry.tsx index 26e52614d0..3bc44aff75 100644 --- a/packages/richtext-slate/src/cell/rscEntry.tsx +++ b/packages/richtext-slate/src/cell/rscEntry.tsx @@ -1,12 +1,10 @@ import type { DefaultServerCellComponentProps, Payload } from 'payload' import { getTranslation, type I18nClient } from '@payloadcms/translations' +import { Link } from '@payloadcms/ui' import { formatAdminURL } from '@payloadcms/ui/shared' -import LinkImport from 'next/link.js' import React from 'react' -const Link = (LinkImport.default || LinkImport) as unknown as typeof LinkImport.default - export const RscEntrySlateCell: React.FC< { i18n: I18nClient diff --git a/packages/ui/src/elements/AppHeader/index.tsx b/packages/ui/src/elements/AppHeader/index.tsx index e92f90e1da..65f272bc70 100644 --- a/packages/ui/src/elements/AppHeader/index.tsx +++ b/packages/ui/src/elements/AppHeader/index.tsx @@ -1,5 +1,4 @@ 'use client' -import LinkWithDefault from 'next/link.js' import React, { useEffect, useRef, useState } from 'react' import { Account } from '../../graphics/Account/index.js' @@ -8,13 +7,14 @@ import { useConfig } from '../../providers/Config/index.js' import { useTranslation } from '../../providers/Translation/index.js' import { formatAdminURL } from '../../utilities/formatAdminURL.js' import { Hamburger } from '../Hamburger/index.js' +import { Link } from '../Link/index.js' import { Localizer } from '../Localizer/index.js' 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 { StepNav } from '../StepNav/index.js' import './index.scss' +import { StepNav } from '../StepNav/index.js' const baseClass = 'app-header' @@ -59,8 +59,6 @@ export function AppHeader({ CustomAvatar, CustomIcon }: Props) { } }, [Actions]) - const Link = LinkWithDefault.default - const LinkElement = Link || 'a' const ActionComponents = Actions ? Object.values(Actions) : [] diff --git a/packages/ui/src/elements/Banner/index.tsx b/packages/ui/src/elements/Banner/index.tsx index d694aeff2e..9d991344c7 100644 --- a/packages/ui/src/elements/Banner/index.tsx +++ b/packages/ui/src/elements/Banner/index.tsx @@ -1,12 +1,10 @@ 'use client' import type { MouseEvent } from 'react' -import LinkImport from 'next/link.js' // TODO: abstract this out to support all routers import React from 'react' import './index.scss' - -const Link = (LinkImport.default || LinkImport) as unknown as typeof LinkImport.default +import { Link } from '../Link/index.js' const baseClass = 'banner' diff --git a/packages/ui/src/elements/BulkUpload/EditForm/index.tsx b/packages/ui/src/elements/BulkUpload/EditForm/index.tsx index 1a95d3ae6b..3cc517d09e 100644 --- a/packages/ui/src/elements/BulkUpload/EditForm/index.tsx +++ b/packages/ui/src/elements/BulkUpload/EditForm/index.tsx @@ -13,6 +13,7 @@ import { useDocumentEvents } from '../../../providers/DocumentEvents/index.js' import { useDocumentInfo } from '../../../providers/DocumentInfo/index.js' import { useEditDepth } from '../../../providers/EditDepth/index.js' import { OperationProvider } from '../../../providers/Operation/index.js' +import { useRouteTransition } from '../../../providers/RouteTransition/index.js' import { useServerFunctions } from '../../../providers/ServerFunctions/index.js' import { useUploadEdits } from '../../../providers/UploadEdits/index.js' import { abortAndIgnore, handleAbortRef } from '../../../utilities/abortAndIgnore.js' @@ -62,6 +63,7 @@ export function EditForm({ submitted }: EditFormProps) { const params = useSearchParams() const { reportUpdate } = useDocumentEvents() const { resetUploadEdits } = useUploadEdits() + const { startRouteTransition } = useRouteTransition() const locale = params.get('locale') @@ -89,7 +91,8 @@ export function EditForm({ submitted }: EditFormProps) { adminRoute, path: `/collections/${collectionSlug}/${json?.doc?.id}${locale ? `?locale=${locale}` : ''}`, }) - router.push(redirectRoute) + + startRouteTransition(() => router.push(redirectRoute)) } else { resetUploadEdits() } @@ -104,6 +107,7 @@ export function EditForm({ submitted }: EditFormProps) { reportUpdate, resetUploadEdits, router, + startRouteTransition, ], ) diff --git a/packages/ui/src/elements/CopyLocaleData/index.tsx b/packages/ui/src/elements/CopyLocaleData/index.tsx index 995a0bb5d7..1057fbbade 100644 --- a/packages/ui/src/elements/CopyLocaleData/index.tsx +++ b/packages/ui/src/elements/CopyLocaleData/index.tsx @@ -12,13 +12,14 @@ import { useFormModified } from '../../forms/Form/context.js' import { useConfig } from '../../providers/Config/index.js' import { useDocumentInfo } from '../../providers/DocumentInfo/index.js' import { useLocale } from '../../providers/Locale/index.js' +import { useRouteTransition } from '../../providers/RouteTransition/index.js' import { useServerFunctions } from '../../providers/ServerFunctions/index.js' import { useTranslation } from '../../providers/Translation/index.js' import { DrawerHeader } from '../BulkUpload/Header/index.js' import { Button } from '../Button/index.js' import { Drawer } from '../Drawer/index.js' -import './index.scss' import { PopupList } from '../Popup/index.js' +import './index.scss' const baseClass = 'copy-locale-data' @@ -38,6 +39,7 @@ export const CopyLocaleData: React.FC = () => { const { toggleModal } = useModal() const { copyDataFromLocale } = useServerFunctions() const router = useRouter() + const { startRouteTransition } = useRouteTransition() const localeOptions = (localization && @@ -77,9 +79,13 @@ export const CopyLocaleData: React.FC = () => { }) setCopying(false) - router.push( - `${serverURL}${admin}/${collectionSlug ? `collections/${collectionSlug}/${id}` : `globals/${globalSlug}`}?locale=${to}`, + + startRouteTransition(() => + router.push( + `${serverURL}${admin}/${collectionSlug ? `collections/${collectionSlug}/${id}` : `globals/${globalSlug}`}?locale=${to}`, + ), ) + toggleModal(drawerSlug) } catch (error) { toast.error(error.message) @@ -95,6 +101,7 @@ export const CopyLocaleData: React.FC = () => { router, serverURL, admin, + startRouteTransition, ], ) diff --git a/packages/ui/src/elements/DeleteDocument/index.tsx b/packages/ui/src/elements/DeleteDocument/index.tsx index 2fe965a31c..9d13461881 100644 --- a/packages/ui/src/elements/DeleteDocument/index.tsx +++ b/packages/ui/src/elements/DeleteDocument/index.tsx @@ -1,5 +1,5 @@ 'use client' -import type { ClientCollectionConfig, SanitizedCollectionConfig } from 'payload' +import type { SanitizedCollectionConfig } from 'payload' import { Modal, useModal } from '@faceless-ui/modal' import { getTranslation } from '@payloadcms/translations' @@ -13,6 +13,7 @@ import { useForm } from '../../forms/Form/context.js' import { useConfig } from '../../providers/Config/index.js' import { useDocumentInfo } from '../../providers/DocumentInfo/index.js' import { useEditDepth } from '../../providers/EditDepth/index.js' +import { useRouteTransition } from '../../providers/RouteTransition/index.js' import { useTranslation } from '../../providers/Translation/index.js' import { requests } from '../../utilities/api.js' import { formatAdminURL } from '../../utilities/formatAdminURL.js' @@ -63,6 +64,7 @@ export const DeleteDocument: React.FC = (props) => { const { i18n, t } = useTranslation() const { title } = useDocumentInfo() const editDepth = useEditDepth() + const { startRouteTransition } = useRouteTransition() const titleToRender = titleFromProps || title || id @@ -105,11 +107,13 @@ export const DeleteDocument: React.FC = (props) => { ) if (redirectAfterDelete) { - return router.push( - formatAdminURL({ - adminRoute, - path: `/collections/${collectionSlug}`, - }), + return startRouteTransition(() => + router.push( + formatAdminURL({ + adminRoute, + path: `/collections/${collectionSlug}`, + }), + ), ) } @@ -155,6 +159,7 @@ export const DeleteDocument: React.FC = (props) => { redirectAfterDelete, onDelete, collectionConfig, + startRouteTransition, ]) if (id) { diff --git a/packages/ui/src/elements/DeleteMany/index.tsx b/packages/ui/src/elements/DeleteMany/index.tsx index c37d905508..0407a429be 100644 --- a/packages/ui/src/elements/DeleteMany/index.tsx +++ b/packages/ui/src/elements/DeleteMany/index.tsx @@ -16,8 +16,8 @@ import { useTranslation } from '../../providers/Translation/index.js' import { requests } from '../../utilities/api.js' import { mergeListSearchAndWhere } from '../../utilities/mergeListSearchAndWhere.js' import { Button } from '../Button/index.js' -import './index.scss' import { Pill } from '../Pill/index.js' +import './index.scss' const baseClass = 'delete-documents' diff --git a/packages/ui/src/elements/DocumentLocked/index.tsx b/packages/ui/src/elements/DocumentLocked/index.tsx index fcd7a59fec..eb6677632c 100644 --- a/packages/ui/src/elements/DocumentLocked/index.tsx +++ b/packages/ui/src/elements/DocumentLocked/index.tsx @@ -3,6 +3,7 @@ import type { ClientUser } from 'payload' import React, { useEffect } from 'react' +import { useRouteTransition } from '../../providers/RouteTransition/index.js' import { useTranslation } from '../../providers/Translation/index.js' import { isClientUserObject } from '../../utilities/isClientUserObject.js' import { Button } from '../Button/index.js' @@ -37,6 +38,7 @@ export const DocumentLocked: React.FC<{ }> = ({ handleGoBack, isActive, onReadOnly, onTakeOver, updatedAt, user }) => { const { closeModal, openModal } = useModal() const { t } = useTranslation() + const { startRouteTransition } = useRouteTransition() useEffect(() => { if (isActive) { @@ -47,7 +49,13 @@ export const DocumentLocked: React.FC<{ }, [isActive, openModal, closeModal]) return ( - + { + startRouteTransition(() => handleGoBack()) + }} + slug={modalSlug} + >

{t('general:documentLocked')}

@@ -65,7 +73,9 @@ export const DocumentLocked: React.FC<{