From 52f2ea79f67fe739933eb65441ae138636f9ceef Mon Sep 17 00:00:00 2001 From: Jake Fletcher Date: Wed, 27 May 2026 15:10:56 -0400 Subject: [PATCH 1/3] feat!: admin router adapter --- docs/migration-guide/v4.mdx | 54 +++++++ .../next/src/elements/RouterAdapter/index.tsx | 74 +++++++++ packages/next/src/layouts/Root/index.tsx | 2 + .../src/views/Dashboard/Default/index.tsx | 6 - packages/payload/src/admin/adapters.ts | 68 +++++++++ packages/payload/src/index.ts | 3 +- .../TenantSelectionProvider/index.client.tsx | 3 +- .../src/Search/ui/LinkToDoc/index.client.tsx | 2 +- .../Search/ui/ReindexButton/index.client.tsx | 2 +- packages/ui/package.json | 1 - packages/ui/src/elements/Autosave/index.tsx | 1 - packages/ui/src/elements/Button/types.ts | 6 - packages/ui/src/elements/Card/index.tsx | 6 - .../CloseModalOnRouteChange/index.tsx | 2 +- .../ui/src/elements/CopyLocaleData/index.tsx | 2 +- .../elements/DefaultListViewTabs/index.tsx | 2 +- .../ui/src/elements/DeleteDocument/index.tsx | 2 +- packages/ui/src/elements/DeleteMany/index.tsx | 2 +- .../src/elements/DuplicateDocument/index.tsx | 2 +- .../src/elements/EditMany/DrawerContent.tsx | 2 +- .../Hierarchy/Tree/HierarchySidebarTab.tsx | 2 +- .../ui/src/elements/Hierarchy/Tree/index.tsx | 2 +- .../LeaveWithoutSaving/usePreventLeave.tsx | 6 +- packages/ui/src/elements/Link/index.tsx | 22 +-- .../TitleActions/ListBulkUploadButton.tsx | 2 +- .../TitleActions/ListEmptyTrashButton.tsx | 2 +- packages/ui/src/elements/Localizer/index.tsx | 2 +- packages/ui/src/elements/Logout/index.tsx | 6 - packages/ui/src/elements/Nav/context.tsx | 2 +- packages/ui/src/elements/PerPage/index.tsx | 1 - .../PermanentlyDeleteButton/index.tsx | 2 +- .../elements/Popup/PopupButtonList/index.tsx | 4 +- .../elements/PublishMany/DrawerContent.tsx | 2 +- .../ui/src/elements/RestoreButton/index.tsx | 2 +- .../ui/src/elements/RestoreMany/index.tsx | 2 +- .../ui/src/elements/SortComplex/index.tsx | 4 +- .../ui/src/elements/StayLoggedIn/index.tsx | 2 +- .../elements/UnpublishMany/DrawerContent.tsx | 2 +- .../Relationship/optionsReducer.spec.ts | 18 ++- packages/ui/src/exports/client/index.ts | 7 + packages/ui/src/forms/Form/index.tsx | 2 +- packages/ui/src/graphics/Account/index.tsx | 2 +- packages/ui/src/providers/Auth/index.tsx | 2 +- packages/ui/src/providers/Hierarchy/index.tsx | 2 +- packages/ui/src/providers/ListQuery/index.tsx | 2 +- packages/ui/src/providers/Locale/index.tsx | 2 +- packages/ui/src/providers/Params/index.tsx | 15 +- packages/ui/src/providers/Root/index.tsx | 143 ++++++++++-------- .../ui/src/providers/RouteCache/index.tsx | 2 +- .../src/providers/RouteTransition/index.tsx | 3 +- .../ui/src/providers/RouterAdapter/index.tsx | 56 +++++++ .../ui/src/providers/SearchParams/index.tsx | 15 +- packages/ui/src/providers/Selection/index.tsx | 2 +- .../ui/src/providers/Translation/index.tsx | 3 +- .../ui/src/utilities/getRequestLanguage.ts | 4 +- .../src/utilities/handleBackToDashboard.tsx | 4 +- packages/ui/src/utilities/handleGoBack.tsx | 4 +- .../ui/src/utilities/parseSearchParams.ts | 6 +- packages/ui/src/views/Edit/index.tsx | 2 +- packages/ui/src/views/HierarchyList/index.tsx | 2 +- packages/ui/src/views/List/index.tsx | 2 +- .../ui/src/widgets/CollectionCards/index.tsx | 5 +- pnpm-lock.yaml | 31 ---- 63 files changed, 418 insertions(+), 227 deletions(-) create mode 100644 packages/next/src/elements/RouterAdapter/index.tsx create mode 100644 packages/payload/src/admin/adapters.ts create mode 100644 packages/ui/src/providers/RouterAdapter/index.tsx diff --git a/docs/migration-guide/v4.mdx b/docs/migration-guide/v4.mdx index 4ea68268c3c..932234c7b4c 100644 --- a/docs/migration-guide/v4.mdx +++ b/docs/migration-guide/v4.mdx @@ -151,6 +151,60 @@ Several types and utilities that were re-exported from `@payloadcms/ui` and `@pa Run `npx @payloadcms/codemod --transform migrate-aliased-exports` to migrate automatically. Renamed types are imported using an `as` alias (e.g. `import type { CollectionPreferences as ListPreferences } from 'payload'`) so existing usages keep compiling — drop the alias and rename usages manually if you want to fully commit to the new name. +### `next/navigation` and `next/link` replaced by framework-agnostic `RouterAdapter` hooks + +`@payloadcms/ui` no longer depends on `next` directly. Custom admin components (field components, custom views, plugins) that previously imported router hooks or the `Link` component from `next/*` must now import the framework-agnostic equivalents from `@payloadcms/ui`. Behavior is identical when running on Next.js — the `NextRouterAdapter` shipped by `@payloadcms/next` wires `next/navigation` hooks and `next/link` into the same context — but ui code can now also run on non-Next adapters (e.g. TanStack Start) without modification. + +**Hooks** — import from `@payloadcms/ui` instead of `next/navigation`: + +| Old import | New import | +| --------------------------------------------------- | -------------------------------------------------- | +| `import { useRouter } from 'next/navigation'` | `import { useRouter } from '@payloadcms/ui'` | +| `import { usePathname } from 'next/navigation'` | `import { usePathname } from '@payloadcms/ui'` | +| `import { useSearchParams } from 'next/navigation'` | `import { useSearchParams } from '@payloadcms/ui'` | +| `import { useParams } from 'next/navigation'` | `import { useParams } from '@payloadcms/ui'` | + +**`Link` component** — use `PayloadLink` from `@payloadcms/ui`: + +```diff +- import Link from 'next/link' ++ import { PayloadLink as Link } from '@payloadcms/ui' +``` + +**Types** — `LinkProps` from `next/link` is replaced by `LinkAdapterProps` from `payload`: + +```diff +- import type { LinkProps } from 'next/link' ++ import type { LinkAdapterProps } from 'payload' +``` + +`LinkAdapterProps` is a framework-neutral shape: `href: string`, optional `prefetch` / `replace` / `scroll` / `ref`, plus standard `AnchorHTMLAttributes` (minus `href`). The Next adapter forwards these to `next/link`. + +**`RouterAdapterRouter` shape** — the object returned by `useRouter()` now has a stable, framework-agnostic surface: + +```ts +type RouterAdapterRouter = { + back: () => void + push: (path: string, options?: { scroll?: boolean }) => void + refresh: () => void + replace: (path: string, options?: { scroll?: boolean }) => void +} +``` + +If you were relying on Next-specific extras like `prefetch()` on the router instance, switch to the `Link` component's `prefetch` prop instead. + +**Custom apps embedding `RootProvider`** — `RootProvider` now requires a `RouterAdapter` prop. Apps using the default Next.js admin layout get this wired automatically. If you assemble `RootProvider` yourself, pass the framework's adapter component: + +```diff ++ import { NextRouterAdapter } from '@payloadcms/next/elements/RouterAdapter' + + +``` + ### `title` and `setDocumentTitle` removed from `useDocumentInfo` For performance reasons, the document title state has been split out of `DocumentInfoContext` into its own `DocumentTitleContext`. Access it through the `useDocumentTitle` hook, which exposes the same `title` and `setDocumentTitle` API. Components that subscribed to `useDocumentInfo` solely for the title will no longer re-render when unrelated document state changes. diff --git a/packages/next/src/elements/RouterAdapter/index.tsx b/packages/next/src/elements/RouterAdapter/index.tsx new file mode 100644 index 00000000000..f97d77d296f --- /dev/null +++ b/packages/next/src/elements/RouterAdapter/index.tsx @@ -0,0 +1,74 @@ +'use client' +import type { RouterAdapterContextValue } from '@payloadcms/ui' +import type { LinkAdapterProps } from 'payload' + +import { RouterAdapterContext } from '@payloadcms/ui' +import NextLinkImport from 'next/link.js' +import { + useParams as useNextParams, + usePathname as useNextPathname, + useRouter as useNextRouter, + useSearchParams as useNextSearchParams, +} from 'next/navigation.js' +import React from 'react' + +type LinkComponent = React.FC< + { + children?: React.ReactNode + href: string + prefetch?: boolean + ref?: React.Ref + replace?: boolean + scroll?: boolean + } & Omit, 'href'> +> + +const NextLink: LinkComponent = ('default' in NextLinkImport + ? NextLinkImport.default + : NextLinkImport) as unknown as LinkComponent + +const NextLinkAdapter: React.FC = ({ + children, + href, + prefetch, + ref, + replace, + scroll, + ...rest +}) => { + return ( + + {children} + + ) +} + +export const NextRouterAdapter: React.FC<{ children: React.ReactNode }> = ({ children }) => { + const nextRouter = useNextRouter() + const pathname = useNextPathname() + const searchParams = useNextSearchParams() + const params = useNextParams() + + const router = React.useMemo( + () => ({ + back: nextRouter.back, + push: nextRouter.push, + refresh: nextRouter.refresh, + replace: nextRouter.replace, + }), + [nextRouter], + ) + + const value = React.useMemo( + () => ({ + Link: NextLinkAdapter, + params: params as Record, + pathname, + router, + searchParams, + }), + [params, pathname, router, searchParams], + ) + + return {children} +} diff --git a/packages/next/src/layouts/Root/index.tsx b/packages/next/src/layouts/Root/index.tsx index a9e0b4b9d63..87dc629e807 100644 --- a/packages/next/src/layouts/Root/index.tsx +++ b/packages/next/src/layouts/Root/index.tsx @@ -11,6 +11,7 @@ import { applyLocaleFiltering } from 'payload/shared' import React, { Suspense } from 'react' import { getNavPrefs } from '../../elements/Nav/getNavPrefs.js' +import { NextRouterAdapter } from '../../elements/RouterAdapter/index.js' import { getRequestHighContrast } from '../../utilities/getRequestHighContrast.js' import { getRequestTheme } from '../../utilities/getRequestTheme.js' import { initReq } from '../../utilities/initReq.js' @@ -185,6 +186,7 @@ const RootLayoutContent = async ({ languageOptions={languageOptions} locale={req.locale} permissions={req.user ? permissions : null} + RouterAdapter={NextRouterAdapter} serverFunction={serverFunction} switchLanguageServerAction={switchLanguageServerAction} theme={theme} diff --git a/packages/next/src/views/Dashboard/Default/index.tsx b/packages/next/src/views/Dashboard/Default/index.tsx index 6e996dabf84..92ddd867d65 100644 --- a/packages/next/src/views/Dashboard/Default/index.tsx +++ b/packages/next/src/views/Dashboard/Default/index.tsx @@ -28,12 +28,6 @@ export type DashboardViewServerPropsOnly = { lockDuration?: number slug: 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.ComponentType navGroups?: ReturnType } & AdminViewServerPropsOnly diff --git a/packages/payload/src/admin/adapters.ts b/packages/payload/src/admin/adapters.ts new file mode 100644 index 00000000000..cc3acfb125e --- /dev/null +++ b/packages/payload/src/admin/adapters.ts @@ -0,0 +1,68 @@ +import type React from 'react' + +/** + * Client-side router adapter to abstract away framework-specific routing implementations. + * This way plugins and server components can use server-only APIs without directly importing any framework-specific modules. + * + * @example + * ```tsx + * // Next.js router adapter (simplified): + * import { useRouter as useNextRouter, usePathname as useNextPathname } from 'next/navigation' + * + * const NextRouterAdapter: RouterAdapterComponent = ({ children }) => { + * const router = useNextRouter() + * const pathname = useNextPathname() + * + * return ( + * + * {children} + * + * ) + * } + * ``` + */ +export type RouterAdapterComponent = React.ComponentType<{ children: React.ReactNode }> + +export type RouterAdapterRouter = { + /** + * Navigate back to the previous page in the history stack. + */ + back: () => void + /** + * Navigate to a new path. + */ + push: (path: string, options?: { scroll?: boolean }) => void + /** + * Refresh the current route. + */ + refresh: () => void + /** + * Replace the current path with a new one. + */ + replace: (path: string, options?: { scroll?: boolean }) => void +} + +export type CookieStore = { + get: (name: string) => { name: string; value: string } | null | undefined + getAll?: () => Array<{ name: string; value: string }> + set?: (name: string, value: string, options?: CookieOptions) => void +} + +export type CookieOptions = { + domain?: string + expires?: Date + httpOnly?: boolean + maxAge?: number + path?: string + sameSite?: 'lax' | 'none' | 'strict' + secure?: boolean +} + +export type LinkAdapterProps = { + children?: React.ReactNode + href: string + prefetch?: boolean + ref?: React.Ref + replace?: boolean + scroll?: boolean +} & Omit, 'href'> diff --git a/packages/payload/src/index.ts b/packages/payload/src/index.ts index 4daa5d612e2..317cfc50c06 100644 --- a/packages/payload/src/index.ts +++ b/packages/payload/src/index.ts @@ -37,7 +37,7 @@ import { verifyEmailLocal, type Options as VerifyEmailOptions, } from './auth/operations/local/verifyEmail.js' -export type { FieldState } from './admin/forms/Form.js' +export type * from './admin/adapters.js' import type { InitOptions, SanitizedConfig } from './config/types.js' import type { BaseDatabaseAdapter, PaginatedDistinctDocs, PaginatedDocs } from './database/types.js' import type { InitializedEmailAdapter } from './email/types.js' @@ -119,6 +119,7 @@ import { updateGlobalLocal, type Options as UpdateGlobalOptions, } from './globals/operations/local/update.js' +export type { FieldState } from './admin/forms/Form.js' export type * from './admin/types.js' export { EntityType } from './admin/views/dashboard.js' /** diff --git a/packages/plugin-multi-tenant/src/providers/TenantSelectionProvider/index.client.tsx b/packages/plugin-multi-tenant/src/providers/TenantSelectionProvider/index.client.tsx index 0fe0b67982e..94a08951205 100644 --- a/packages/plugin-multi-tenant/src/providers/TenantSelectionProvider/index.client.tsx +++ b/packages/plugin-multi-tenant/src/providers/TenantSelectionProvider/index.client.tsx @@ -2,8 +2,7 @@ import type { OptionObject } from 'payload' -import { toast, useAuth, useConfig } from '@payloadcms/ui' -import { useRouter } from 'next/navigation.js' +import { toast, useAuth, useConfig, useRouter } from '@payloadcms/ui' import { formatAdminURL } from 'payload/shared' import React, { createContext } from 'react' 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 6c3ccd25cd8..31bfa9d26ad 100644 --- a/packages/plugin-search/src/Search/ui/LinkToDoc/index.client.tsx +++ b/packages/plugin-search/src/Search/ui/LinkToDoc/index.client.tsx @@ -46,7 +46,7 @@ export const LinkToDocClient: React.FC = () => { textOverflow: 'ellipsis', }} > - + {href} diff --git a/packages/plugin-search/src/Search/ui/ReindexButton/index.client.tsx b/packages/plugin-search/src/Search/ui/ReindexButton/index.client.tsx index bb1d256789e..e5d2b70e37b 100644 --- a/packages/plugin-search/src/Search/ui/ReindexButton/index.client.tsx +++ b/packages/plugin-search/src/Search/ui/ReindexButton/index.client.tsx @@ -8,9 +8,9 @@ import { useConfig, useLocale, useModal, + useRouter, useTranslation, } from '@payloadcms/ui' -import { useRouter } from 'next/navigation.js' import { formatAdminURL } from 'payload/shared' import React, { useCallback, useMemo, useState } from 'react' diff --git a/packages/ui/package.json b/packages/ui/package.json index 30b12f8d2c6..5605ab73d84 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -172,7 +172,6 @@ "payload": "workspace:*" }, "peerDependencies": { - "next": ">=16.2.6 <17.0.0", "payload": "workspace:*", "react": "^19.0.1 || ^19.1.2 || ^19.2.1", "react-dom": "^19.0.1 || ^19.1.2 || ^19.2.1" diff --git a/packages/ui/src/elements/Autosave/index.tsx b/packages/ui/src/elements/Autosave/index.tsx index 69326decb11..147f8853eda 100644 --- a/packages/ui/src/elements/Autosave/index.tsx +++ b/packages/ui/src/elements/Autosave/index.tsx @@ -1,5 +1,4 @@ 'use client' -// TODO: abstract the `next/navigation` dependency out from this component import type { ClientCollectionConfig, ClientGlobalConfig } from 'payload' import { dequal } from 'dequal/lite' diff --git a/packages/ui/src/elements/Button/types.ts b/packages/ui/src/elements/Button/types.ts index 87366e3ca34..229b635f69b 100644 --- a/packages/ui/src/elements/Button/types.ts +++ b/packages/ui/src/elements/Button/types.ts @@ -22,12 +22,6 @@ export type Props = { icon?: ['chevron' | 'edit' | 'plus' | 'x'] | React.ReactNode iconPosition?: 'left' | 'right' 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 /** * Shows a loading spinner and hides content. Disables interactions. */ diff --git a/packages/ui/src/elements/Card/index.tsx b/packages/ui/src/elements/Card/index.tsx index 5f991acfa3a..6ab7a634671 100644 --- a/packages/ui/src/elements/Card/index.tsx +++ b/packages/ui/src/elements/Card/index.tsx @@ -10,12 +10,6 @@ export type Props = { buttonAriaLabel?: string href?: string 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 onClick?: () => void title: string titleAs?: React.ElementType diff --git a/packages/ui/src/elements/CloseModalOnRouteChange/index.tsx b/packages/ui/src/elements/CloseModalOnRouteChange/index.tsx index 551ebecfd46..2dc521d3124 100644 --- a/packages/ui/src/elements/CloseModalOnRouteChange/index.tsx +++ b/packages/ui/src/elements/CloseModalOnRouteChange/index.tsx @@ -1,10 +1,10 @@ 'use client' import { useModal } from '@faceless-ui/modal' -import { usePathname } from 'next/navigation.js' import { useEffect, useRef } from 'react' import { useEffectEvent } from '../../hooks/useEffectEvent.js' +import { usePathname } from '../../providers/RouterAdapter/index.js' export function CloseModalOnRouteChange() { const { closeAllModals } = useModal() diff --git a/packages/ui/src/elements/CopyLocaleData/index.tsx b/packages/ui/src/elements/CopyLocaleData/index.tsx index 83ebcbecdc4..ae6a0418667 100644 --- a/packages/ui/src/elements/CopyLocaleData/index.tsx +++ b/packages/ui/src/elements/CopyLocaleData/index.tsx @@ -2,7 +2,6 @@ import { useModal } from '@faceless-ui/modal' import { getTranslation } from '@payloadcms/translations' -import { useRouter } from 'next/navigation.js' import { formatAdminURL } from 'payload/shared' import React, { useCallback } from 'react' import { toast } from 'sonner' @@ -13,6 +12,7 @@ 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 { useRouter } from '../../providers/RouterAdapter/index.js' import { useRouteTransition } from '../../providers/RouteTransition/index.js' import { useServerFunctions } from '../../providers/ServerFunctions/index.js' import { useTranslation } from '../../providers/Translation/index.js' diff --git a/packages/ui/src/elements/DefaultListViewTabs/index.tsx b/packages/ui/src/elements/DefaultListViewTabs/index.tsx index 64c1d062c20..4b29876b3fb 100644 --- a/packages/ui/src/elements/DefaultListViewTabs/index.tsx +++ b/packages/ui/src/elements/DefaultListViewTabs/index.tsx @@ -3,11 +3,11 @@ import type { ClientCollectionConfig, ClientConfig, ViewTypes } from 'payload' import { getTranslation } from '@payloadcms/translations' -import { useRouter } from 'next/navigation.js' import { formatAdminURL } from 'payload/shared' import React from 'react' import { usePreferences } from '../../providers/Preferences/index.js' +import { useRouter } from '../../providers/RouterAdapter/index.js' import { useTranslation } from '../../providers/Translation/index.js' import { Button } from '../Button/index.js' import './index.scss' diff --git a/packages/ui/src/elements/DeleteDocument/index.tsx b/packages/ui/src/elements/DeleteDocument/index.tsx index 62eaf95acbb..fe8776486bf 100644 --- a/packages/ui/src/elements/DeleteDocument/index.tsx +++ b/packages/ui/src/elements/DeleteDocument/index.tsx @@ -3,7 +3,6 @@ import type { SanitizedCollectionConfig } from 'payload' import { useModal } from '@faceless-ui/modal' import { getTranslation } from '@payloadcms/translations' -import { useRouter } from 'next/navigation.js' import { formatAdminURL } from 'payload/shared' import React, { Fragment, useCallback, useState } from 'react' import { toast } from 'sonner' @@ -15,6 +14,7 @@ import { useForm } from '../../forms/Form/context.js' import { useConfig } from '../../providers/Config/index.js' import { useDocumentInfo } from '../../providers/DocumentInfo/index.js' import { useDocumentTitle } from '../../providers/DocumentTitle/index.js' +import { useRouter } from '../../providers/RouterAdapter/index.js' import { useRouteTransition } from '../../providers/RouteTransition/index.js' import { useTranslation } from '../../providers/Translation/index.js' import { requests } from '../../utilities/api.js' diff --git a/packages/ui/src/elements/DeleteMany/index.tsx b/packages/ui/src/elements/DeleteMany/index.tsx index 078aabff49a..b59b6d0b082 100644 --- a/packages/ui/src/elements/DeleteMany/index.tsx +++ b/packages/ui/src/elements/DeleteMany/index.tsx @@ -3,7 +3,6 @@ import type { ClientCollectionConfig, ViewTypes, Where } from 'payload' import { useModal } from '@faceless-ui/modal' import { getTranslation } from '@payloadcms/translations' -import { useRouter, useSearchParams } from 'next/navigation.js' import { formatAdminURL, mergeListSearchAndWhere } from 'payload/shared' import * as qs from 'qs-esm' import React from 'react' @@ -14,6 +13,7 @@ import { useAuth } from '../../providers/Auth/index.js' import { useConfig } from '../../providers/Config/index.js' import { useLocale } from '../../providers/Locale/index.js' import { useRouteCache } from '../../providers/RouteCache/index.js' +import { useRouter, useSearchParams } from '../../providers/RouterAdapter/index.js' import { SelectAllStatus, useSelection } from '../../providers/Selection/index.js' import { useTranslation } from '../../providers/Translation/index.js' import { requests } from '../../utilities/api.js' diff --git a/packages/ui/src/elements/DuplicateDocument/index.tsx b/packages/ui/src/elements/DuplicateDocument/index.tsx index bdb500a5aa6..cebbc961c9e 100644 --- a/packages/ui/src/elements/DuplicateDocument/index.tsx +++ b/packages/ui/src/elements/DuplicateDocument/index.tsx @@ -4,7 +4,6 @@ import type { SanitizedCollectionConfig } from 'payload' import { useModal } from '@faceless-ui/modal' import { getTranslation } from '@payloadcms/translations' -import { useRouter } from 'next/navigation.js' import { formatAdminURL, hasDraftsEnabled } from 'payload/shared' import * as qs from 'qs-esm' import React, { useCallback, useMemo } from 'react' @@ -15,6 +14,7 @@ import type { DocumentDrawerContextType } from '../DocumentDrawer/Provider.js' import { useForm, useFormModified } from '../../forms/Form/context.js' import { useConfig } from '../../providers/Config/index.js' import { useLocale } from '../../providers/Locale/index.js' +import { useRouter } from '../../providers/RouterAdapter/index.js' import { useRouteTransition } from '../../providers/RouteTransition/index.js' import { useTranslation } from '../../providers/Translation/index.js' import { requests } from '../../utilities/api.js' diff --git a/packages/ui/src/elements/EditMany/DrawerContent.tsx b/packages/ui/src/elements/EditMany/DrawerContent.tsx index 967f67257d4..81e24ea1355 100644 --- a/packages/ui/src/elements/EditMany/DrawerContent.tsx +++ b/packages/ui/src/elements/EditMany/DrawerContent.tsx @@ -4,7 +4,6 @@ import type { SelectType, Where } from 'payload' import { useModal } from '@faceless-ui/modal' import { getTranslation } from '@payloadcms/translations' -import { useRouter, useSearchParams } from 'next/navigation.js' import { combineWhereConstraints, formatAdminURL, @@ -28,6 +27,7 @@ import { useConfig } from '../../providers/Config/index.js' import { DocumentInfoProvider } from '../../providers/DocumentInfo/index.js' import { useLocale } from '../../providers/Locale/index.js' import { OperationContext } from '../../providers/Operation/index.js' +import { useRouter, useSearchParams } from '../../providers/RouterAdapter/index.js' import { useServerFunctions } from '../../providers/ServerFunctions/index.js' import { useTranslation } from '../../providers/Translation/index.js' import { abortAndIgnore, handleAbortRef } from '../../utilities/abortAndIgnore.js' diff --git a/packages/ui/src/elements/Hierarchy/Tree/HierarchySidebarTab.tsx b/packages/ui/src/elements/Hierarchy/Tree/HierarchySidebarTab.tsx index 312bf664eae..e30a5f169fd 100644 --- a/packages/ui/src/elements/Hierarchy/Tree/HierarchySidebarTab.tsx +++ b/packages/ui/src/elements/Hierarchy/Tree/HierarchySidebarTab.tsx @@ -2,7 +2,6 @@ import type { SidebarTabClientProps } from 'payload' -import { useRouter, useSearchParams } from 'next/navigation.js' import { formatAdminURL } from 'payload/shared' import React, { useCallback, useEffect, useRef, useState } from 'react' @@ -10,6 +9,7 @@ import type { HierarchyInitialData } from './types.js' import { useConfig } from '../../../providers/Config/index.js' import { useHierarchy } from '../../../providers/Hierarchy/index.js' +import { useRouter, useSearchParams } from '../../../providers/RouterAdapter/index.js' import { useRouteTransition } from '../../../providers/RouteTransition/index.js' import { useSidebarTabs } from '../../../providers/SidebarTabs/index.js' import { HydrateHierarchyProvider } from '../HydrateProvider/index.js' diff --git a/packages/ui/src/elements/Hierarchy/Tree/index.tsx b/packages/ui/src/elements/Hierarchy/Tree/index.tsx index e5a7fbde6ac..cbb990aff78 100644 --- a/packages/ui/src/elements/Hierarchy/Tree/index.tsx +++ b/packages/ui/src/elements/Hierarchy/Tree/index.tsx @@ -2,7 +2,6 @@ import { useModal } from '@faceless-ui/modal' import { getTranslation } from '@payloadcms/translations' -import { useRouter } from 'next/navigation.js' import { DEFAULT_HIERARCHY_TREE_LIMIT } from 'payload/shared' import React, { useCallback, useId, useMemo, useRef, useState } from 'react' @@ -12,6 +11,7 @@ import { PlusIcon } from '../../../icons/Plus/index.js' import { useAuth } from '../../../providers/Auth/index.js' import { useConfig } from '../../../providers/Config/index.js' import { useHierarchy } from '../../../providers/Hierarchy/index.js' +import { useRouter } from '../../../providers/RouterAdapter/index.js' import { useTranslation } from '../../../providers/Translation/index.js' import { Button } from '../../Button/index.js' import { CreateDocumentButton } from '../../CreateDocumentButton/index.js' diff --git a/packages/ui/src/elements/LeaveWithoutSaving/usePreventLeave.tsx b/packages/ui/src/elements/LeaveWithoutSaving/usePreventLeave.tsx index 256abb23cc2..6c97d3f37f8 100644 --- a/packages/ui/src/elements/LeaveWithoutSaving/usePreventLeave.tsx +++ b/packages/ui/src/elements/LeaveWithoutSaving/usePreventLeave.tsx @@ -1,11 +1,11 @@ 'use client' +import { useCallback, useEffect, useRef } from 'react' + // Credit: @Taiki92777 // - Source: https://github.com/vercel/next.js/discussions/32231#discussioncomment-7284386 // Credit: `react-use` maintainers // - Source: https://github.com/streamich/react-use/blob/ade8d3905f544305515d010737b4ae604cc51024/src/useBeforeUnload.ts#L2 -import { useRouter } from 'next/navigation.js' -import { useCallback, useEffect, useRef } from 'react' - +import { useRouter } from '../../providers/RouterAdapter/index.js' import { useRouteTransition } from '../../providers/RouteTransition/index.js' function on( diff --git a/packages/ui/src/elements/Link/index.tsx b/packages/ui/src/elements/Link/index.tsx index ab19ca363e2..59f22b1ceaf 100644 --- a/packages/ui/src/elements/Link/index.tsx +++ b/packages/ui/src/elements/Link/index.tsx @@ -1,14 +1,11 @@ 'use client' -import NextLinkImport from 'next/link.js' -import { useRouter } from 'next/navigation.js' +import type { LinkAdapterProps } from 'payload' + import React from 'react' +import { PayloadLink, useRouter } from '../../providers/RouterAdapter/index.js' import { useRouteTransition } from '../../providers/RouteTransition/index.js' -import { formatUrl } from './formatUrl.js' - -const NextLink = 'default' in NextLinkImport ? NextLinkImport.default : NextLinkImport -// Copied from https://github.com/vercel/next.js/blob/canary/packages/next/src/client/link.tsx#L180-L191 function isModifiedEvent(event: React.MouseEvent): boolean { const eventTarget = event.currentTarget as HTMLAnchorElement | SVGAElement const target = eventTarget.getAttribute('target') @@ -17,7 +14,7 @@ function isModifiedEvent(event: React.MouseEvent): boolean { event.metaKey || event.ctrlKey || event.shiftKey || - event.altKey || // triggers resource download + event.altKey || (event.nativeEvent && event.nativeEvent.which === 2) ) } @@ -33,7 +30,7 @@ type Props = { * @default true */ preventDefault?: boolean -} & Parameters[0] +} & { ref?: React.Ref } & LinkAdapterProps export const Link: React.FC = ({ children, @@ -50,7 +47,7 @@ export const Link: React.FC = ({ const { startRouteTransition } = useRouteTransition() return ( - { if (isModifiedEvent(e)) { @@ -61,13 +58,11 @@ export const Link: React.FC = ({ onClick(e) } - // We need a preventDefault here so that a clicked link doesn't trigger twice, - // once for default browser navigation and once for startRouteTransition if (preventDefault) { e.preventDefault() } - const url = typeof href === 'string' ? href : formatUrl(href) + const url = href if (forceReload) { window.location.href = url @@ -82,13 +77,12 @@ export const Link: React.FC = ({ } } - // Call startRouteTransition if available, otherwise navigate directly startRouteTransition(navigate) }} ref={ref} {...rest} > {children} - + ) } diff --git a/packages/ui/src/elements/ListHeader/TitleActions/ListBulkUploadButton.tsx b/packages/ui/src/elements/ListHeader/TitleActions/ListBulkUploadButton.tsx index daaeb1a9f77..8043400119e 100644 --- a/packages/ui/src/elements/ListHeader/TitleActions/ListBulkUploadButton.tsx +++ b/packages/ui/src/elements/ListHeader/TitleActions/ListBulkUploadButton.tsx @@ -2,11 +2,11 @@ import type { CollectionSlug } from 'payload' import { useModal } from '@faceless-ui/modal' -import { useRouter } from 'next/navigation.js' import React from 'react' import { useBulkUpload } from '../../../elements/BulkUpload/index.js' import { useHierarchy } from '../../../providers/Hierarchy/index.js' +import { useRouter } from '../../../providers/RouterAdapter/index.js' import { useTranslation } from '../../../providers/Translation/index.js' import { Button } from '../../Button/index.js' diff --git a/packages/ui/src/elements/ListHeader/TitleActions/ListEmptyTrashButton.tsx b/packages/ui/src/elements/ListHeader/TitleActions/ListEmptyTrashButton.tsx index 0d46fb49b4d..fed7cf04d4d 100644 --- a/packages/ui/src/elements/ListHeader/TitleActions/ListEmptyTrashButton.tsx +++ b/packages/ui/src/elements/ListHeader/TitleActions/ListEmptyTrashButton.tsx @@ -3,7 +3,6 @@ import type { ClientCollectionConfig } from 'payload' import { useModal } from '@faceless-ui/modal' import { getTranslation } from '@payloadcms/translations' -import { useRouter, useSearchParams } from 'next/navigation.js' import { formatAdminURL } from 'payload/shared' import * as qs from 'qs-esm' import React from 'react' @@ -12,6 +11,7 @@ import { toast } from 'sonner' import { useConfig } from '../../../providers/Config/index.js' import { useLocale } from '../../../providers/Locale/index.js' import { useRouteCache } from '../../../providers/RouteCache/index.js' +import { useRouter, useSearchParams } from '../../../providers/RouterAdapter/index.js' import { useTranslation } from '../../../providers/Translation/index.js' import { requests } from '../../../utilities/api.js' import { Button } from '../../Button/index.js' diff --git a/packages/ui/src/elements/Localizer/index.tsx b/packages/ui/src/elements/Localizer/index.tsx index ed52a7a8515..b251d3d9419 100644 --- a/packages/ui/src/elements/Localizer/index.tsx +++ b/packages/ui/src/elements/Localizer/index.tsx @@ -1,6 +1,5 @@ 'use client' import { getTranslation } from '@payloadcms/translations' -import { useRouter } from 'next/navigation.js' import * as qs from 'qs-esm' import React, { Fragment } from 'react' @@ -8,6 +7,7 @@ import { ChevronIcon } from '../../icons/Chevron/index.js' import { LanguageIcon } from '../../icons/Language/index.js' import { useConfig } from '../../providers/Config/index.js' import { useLocale, useLocaleLoading } from '../../providers/Locale/index.js' +import { useRouter } from '../../providers/RouterAdapter/index.js' import { useRouteTransition } from '../../providers/RouteTransition/index.js' import { useTranslation } from '../../providers/Translation/index.js' import { Button } from '../Button/index.js' diff --git a/packages/ui/src/elements/Logout/index.tsx b/packages/ui/src/elements/Logout/index.tsx index 182fd7a1cb8..f1eda6bff44 100644 --- a/packages/ui/src/elements/Logout/index.tsx +++ b/packages/ui/src/elements/Logout/index.tsx @@ -10,12 +10,6 @@ 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 }> = ({ tabIndex = 0 }) => { const { t } = useTranslation() diff --git a/packages/ui/src/elements/Nav/context.tsx b/packages/ui/src/elements/Nav/context.tsx index 6f44ad5df7a..1021dbd7e28 100644 --- a/packages/ui/src/elements/Nav/context.tsx +++ b/packages/ui/src/elements/Nav/context.tsx @@ -1,10 +1,10 @@ 'use client' import { useWindowInfo } from '@faceless-ui/window-info' -import { usePathname } from 'next/navigation.js' import { PREFERENCE_KEYS } from 'payload/shared' import React, { useEffect, useRef } from 'react' import { usePreferences } from '../../providers/Preferences/index.js' +import { usePathname } from '../../providers/RouterAdapter/index.js' type NavContextType = { hydrated: boolean diff --git a/packages/ui/src/elements/PerPage/index.tsx b/packages/ui/src/elements/PerPage/index.tsx index bb431b1ec7f..8d4c087e5bc 100644 --- a/packages/ui/src/elements/PerPage/index.tsx +++ b/packages/ui/src/elements/PerPage/index.tsx @@ -1,5 +1,4 @@ 'use client' -// TODO: abstract the `next/navigation` dependency out from this component import { collectionDefaults, isNumber } from 'payload/shared' import React from 'react' diff --git a/packages/ui/src/elements/PermanentlyDeleteButton/index.tsx b/packages/ui/src/elements/PermanentlyDeleteButton/index.tsx index a5446d38717..48e6275ae99 100644 --- a/packages/ui/src/elements/PermanentlyDeleteButton/index.tsx +++ b/packages/ui/src/elements/PermanentlyDeleteButton/index.tsx @@ -4,7 +4,6 @@ import type { SanitizedCollectionConfig } from 'payload' import { useModal } from '@faceless-ui/modal' import { getTranslation } from '@payloadcms/translations' -import { useRouter } from 'next/navigation.js' import { formatAdminURL } from 'payload/shared' import * as qs from 'qs-esm' import React, { Fragment, useCallback } from 'react' @@ -14,6 +13,7 @@ import type { DocumentDrawerContextType } from '../DocumentDrawer/Provider.js' import { useConfig } from '../../providers/Config/index.js' import { useDocumentTitle } from '../../providers/DocumentTitle/index.js' +import { useRouter } from '../../providers/RouterAdapter/index.js' import { useRouteTransition } from '../../providers/RouteTransition/index.js' import { useTranslation } from '../../providers/Translation/index.js' import { requests } from '../../utilities/api.js' diff --git a/packages/ui/src/elements/Popup/PopupButtonList/index.tsx b/packages/ui/src/elements/Popup/PopupButtonList/index.tsx index 26be58337f8..8d4d5dbef8c 100644 --- a/packages/ui/src/elements/Popup/PopupButtonList/index.tsx +++ b/packages/ui/src/elements/Popup/PopupButtonList/index.tsx @@ -1,5 +1,5 @@ 'use client' -import type { LinkProps } from 'next/link.js' +import type { LinkAdapterProps } from 'payload' import * as React from 'react' @@ -46,7 +46,7 @@ type MenuButtonProps = { children: React.ReactNode className?: string disabled?: boolean - href?: LinkProps['href'] + href?: LinkAdapterProps['href'] icon?: React.ReactNode id?: string onClick?: (e?: React.MouseEvent) => void diff --git a/packages/ui/src/elements/PublishMany/DrawerContent.tsx b/packages/ui/src/elements/PublishMany/DrawerContent.tsx index 4a4ae911ddc..e12ae7ab20d 100644 --- a/packages/ui/src/elements/PublishMany/DrawerContent.tsx +++ b/packages/ui/src/elements/PublishMany/DrawerContent.tsx @@ -1,7 +1,6 @@ import type { Where } from 'payload' import { getTranslation } from '@payloadcms/translations' -import { useRouter, useSearchParams } from 'next/navigation.js' import { combineWhereConstraints, formatAdminURL, mergeListSearchAndWhere } from 'payload/shared' import * as qs from 'qs-esm' import React, { useCallback } from 'react' @@ -12,6 +11,7 @@ import type { PublishManyProps } from './index.js' import { useConfig } from '../../providers/Config/index.js' import { useLocale } from '../../providers/Locale/index.js' import { useRouteCache } from '../../providers/RouteCache/index.js' +import { useRouter, useSearchParams } from '../../providers/RouterAdapter/index.js' import { useTranslation } from '../../providers/Translation/index.js' import { requests } from '../../utilities/api.js' import { parseSearchParams } from '../../utilities/parseSearchParams.js' diff --git a/packages/ui/src/elements/RestoreButton/index.tsx b/packages/ui/src/elements/RestoreButton/index.tsx index 6f8fcfb36c9..e05f25a4580 100644 --- a/packages/ui/src/elements/RestoreButton/index.tsx +++ b/packages/ui/src/elements/RestoreButton/index.tsx @@ -4,7 +4,6 @@ import type { SanitizedCollectionConfig } from 'payload' import { useModal } from '@faceless-ui/modal' import { getTranslation } from '@payloadcms/translations' -import { useRouter } from 'next/navigation.js' import { formatAdminURL } from 'payload/shared' import * as qs from 'qs-esm' import React, { Fragment, useCallback, useState } from 'react' @@ -15,6 +14,7 @@ import type { DocumentDrawerContextType } from '../DocumentDrawer/Provider.js' import { CheckboxInput } from '../../fields/Checkbox/Input.js' import { useConfig } from '../../providers/Config/index.js' import { useDocumentTitle } from '../../providers/DocumentTitle/index.js' +import { useRouter } from '../../providers/RouterAdapter/index.js' import { useRouteTransition } from '../../providers/RouteTransition/index.js' import { useTranslation } from '../../providers/Translation/index.js' import { requests } from '../../utilities/api.js' diff --git a/packages/ui/src/elements/RestoreMany/index.tsx b/packages/ui/src/elements/RestoreMany/index.tsx index 38f48e13884..8ab3d8f5a45 100644 --- a/packages/ui/src/elements/RestoreMany/index.tsx +++ b/packages/ui/src/elements/RestoreMany/index.tsx @@ -3,7 +3,6 @@ import type { ClientCollectionConfig, ViewTypes, Where } from 'payload' import { useModal } from '@faceless-ui/modal' import { getTranslation } from '@payloadcms/translations' -import { useRouter, useSearchParams } from 'next/navigation.js' import { formatAdminURL, mergeListSearchAndWhere } from 'payload/shared' import * as qs from 'qs-esm' import React from 'react' @@ -14,6 +13,7 @@ import { useAuth } from '../../providers/Auth/index.js' import { useConfig } from '../../providers/Config/index.js' import { useLocale } from '../../providers/Locale/index.js' import { useRouteCache } from '../../providers/RouteCache/index.js' +import { useRouter, useSearchParams } from '../../providers/RouterAdapter/index.js' import { SelectAllStatus, useSelection } from '../../providers/Selection/index.js' import { useTranslation } from '../../providers/Translation/index.js' import { requests } from '../../utilities/api.js' diff --git a/packages/ui/src/elements/SortComplex/index.tsx b/packages/ui/src/elements/SortComplex/index.tsx index 4dc38d4b3ae..95fec0745b4 100644 --- a/packages/ui/src/elements/SortComplex/index.tsx +++ b/packages/ui/src/elements/SortComplex/index.tsx @@ -2,13 +2,13 @@ import type { OptionObject, SanitizedCollectionConfig } from 'payload' import { getTranslation } from '@payloadcms/translations' -// TODO: abstract the `next/navigation` dependency out from this component -import { usePathname, useRouter, useSearchParams } from 'next/navigation.js' import { sortableFieldTypes } from 'payload' import { fieldAffectsData } from 'payload/shared' import * as qs from 'qs-esm' import React, { useEffect, useState } from 'react' +import { usePathname, useRouter, useSearchParams } from '../../providers/RouterAdapter/index.js' + export type SortComplexProps = { collection: SanitizedCollectionConfig handleChange?: (sort: string) => void diff --git a/packages/ui/src/elements/StayLoggedIn/index.tsx b/packages/ui/src/elements/StayLoggedIn/index.tsx index bd4b8c1b15c..9c24603fb3a 100644 --- a/packages/ui/src/elements/StayLoggedIn/index.tsx +++ b/packages/ui/src/elements/StayLoggedIn/index.tsx @@ -1,5 +1,4 @@ 'use client' -import { useRouter } from 'next/navigation.js' import { formatAdminURL } from 'payload/shared' import React, { useCallback } from 'react' @@ -7,6 +6,7 @@ import type { OnCancel } from '../ConfirmationModal/index.js' import { useAuth } from '../../providers/Auth/index.js' import { useConfig } from '../../providers/Config/index.js' +import { useRouter } from '../../providers/RouterAdapter/index.js' import { useRouteTransition } from '../../providers/RouteTransition/index.js' import { useTranslation } from '../../providers/Translation/index.js' import { ConfirmationModal } from '../ConfirmationModal/index.js' diff --git a/packages/ui/src/elements/UnpublishMany/DrawerContent.tsx b/packages/ui/src/elements/UnpublishMany/DrawerContent.tsx index 3ce7c06791f..264e9744f8d 100644 --- a/packages/ui/src/elements/UnpublishMany/DrawerContent.tsx +++ b/packages/ui/src/elements/UnpublishMany/DrawerContent.tsx @@ -1,7 +1,6 @@ import type { Where } from 'payload' import { getTranslation } from '@payloadcms/translations' -import { useRouter, useSearchParams } from 'next/navigation.js' import { combineWhereConstraints, formatAdminURL, mergeListSearchAndWhere } from 'payload/shared' import * as qs from 'qs-esm' import React, { useCallback } from 'react' @@ -12,6 +11,7 @@ import type { UnpublishManyProps } from './index.js' import { useConfig } from '../../providers/Config/index.js' import { useLocale } from '../../providers/Locale/index.js' import { useRouteCache } from '../../providers/RouteCache/index.js' +import { useRouter, useSearchParams } from '../../providers/RouterAdapter/index.js' import { useTranslation } from '../../providers/Translation/index.js' import { requests } from '../../utilities/api.js' import { parseSearchParams } from '../../utilities/parseSearchParams.js' diff --git a/packages/ui/src/elements/WhereBuilder/Condition/Relationship/optionsReducer.spec.ts b/packages/ui/src/elements/WhereBuilder/Condition/Relationship/optionsReducer.spec.ts index 30f156f40ec..1e1ccbe2ffb 100644 --- a/packages/ui/src/elements/WhereBuilder/Condition/Relationship/optionsReducer.spec.ts +++ b/packages/ui/src/elements/WhereBuilder/Condition/Relationship/optionsReducer.spec.ts @@ -113,18 +113,20 @@ describe('optionsReducer', () => { describe('CLEAR', () => { it('returns empty array when field is required', () => { - const result = optionsReducer( - [{ label: 'Post A', value: 'id-1' }], - { type: 'CLEAR', required: true, i18n: mockI18n }, - ) + const result = optionsReducer([{ label: 'Post A', value: 'id-1' }], { + type: 'CLEAR', + required: true, + i18n: mockI18n, + }) expect(result).toEqual([]) }) it('returns none option when field is not required', () => { - const result = optionsReducer( - [{ label: 'Post A', value: 'id-1' }], - { type: 'CLEAR', required: false, i18n: mockI18n }, - ) + const result = optionsReducer([{ label: 'Post A', value: 'id-1' }], { + type: 'CLEAR', + required: false, + i18n: mockI18n, + }) expect(result).toHaveLength(1) expect(result[0]?.value).toBe('null') }) diff --git a/packages/ui/src/exports/client/index.ts b/packages/ui/src/exports/client/index.ts index 4c22001765d..2ccd2bf759c 100644 --- a/packages/ui/src/exports/client/index.ts +++ b/packages/ui/src/exports/client/index.ts @@ -393,6 +393,13 @@ export { OperationProvider, useOperation } from '../../providers/Operation/index export { ParamsProvider, useParams } from '../../providers/Params/index.js' export { PreferencesProvider, usePreferences } from '../../providers/Preferences/index.js' export { RootProvider } from '../../providers/Root/index.js' +export { + PayloadLink, + RouterAdapterContext, + usePathname, + useRouter, +} from '../../providers/RouterAdapter/index.js' +export type { RouterAdapterContextValue } from '../../providers/RouterAdapter/index.js' export { RouteCache as RouteCacheProvider, useRouteCache, diff --git a/packages/ui/src/forms/Form/index.tsx b/packages/ui/src/forms/Form/index.tsx index 04c990eb702..2f5fa177300 100644 --- a/packages/ui/src/forms/Form/index.tsx +++ b/packages/ui/src/forms/Form/index.tsx @@ -1,6 +1,5 @@ 'use client' import { dequal } from 'dequal/lite' // lite: no need for Map and Set support -import { useRouter } from 'next/navigation.js' import { serialize } from 'object-to-formdata' import { type FormState, type PayloadRequest } from 'payload' import { @@ -33,6 +32,7 @@ import { useConfig } from '../../providers/Config/index.js' import { useDocumentInfo } from '../../providers/DocumentInfo/index.js' import { useLocale } from '../../providers/Locale/index.js' import { useOperation } from '../../providers/Operation/index.js' +import { useRouter } from '../../providers/RouterAdapter/index.js' import { useRouteTransition } from '../../providers/RouteTransition/index.js' import { useServerFunctions } from '../../providers/ServerFunctions/index.js' import { useTranslation } from '../../providers/Translation/index.js' diff --git a/packages/ui/src/graphics/Account/index.tsx b/packages/ui/src/graphics/Account/index.tsx index e071dd79be3..c3463791162 100644 --- a/packages/ui/src/graphics/Account/index.tsx +++ b/packages/ui/src/graphics/Account/index.tsx @@ -1,10 +1,10 @@ 'use client' -import { usePathname } from 'next/navigation.js' import { formatAdminURL } from 'payload/shared' import React from 'react' import { useAuth } from '../../providers/Auth/index.js' import { useConfig } from '../../providers/Config/index.js' +import { usePathname } from '../../providers/RouterAdapter/index.js' import { DefaultAccountIcon } from './Default/index.js' import { GravatarAccountIcon } from './Gravatar/index.js' diff --git a/packages/ui/src/providers/Auth/index.tsx b/packages/ui/src/providers/Auth/index.tsx index ddcff9e9dd7..35d5a084c16 100644 --- a/packages/ui/src/providers/Auth/index.tsx +++ b/packages/ui/src/providers/Auth/index.tsx @@ -2,7 +2,6 @@ import type { ClientUser, SanitizedPermissions, TypedUser } from 'payload' import { useModal } from '@faceless-ui/modal' -import { usePathname, useRouter } from 'next/navigation.js' import { formatAdminURL } from 'payload/shared' import * as qs from 'qs-esm' import React, { createContext, use, useCallback, useEffect, useState } from 'react' @@ -13,6 +12,7 @@ import { useEffectEvent } from '../../hooks/useEffectEvent.js' import { useTranslation } from '../../providers/Translation/index.js' import { requests } from '../../utilities/api.js' import { useConfig } from '../Config/index.js' +import { usePathname, useRouter } from '../RouterAdapter/index.js' import { useRouteTransition } from '../RouteTransition/index.js' export type UserWithToken = { diff --git a/packages/ui/src/providers/Hierarchy/index.tsx b/packages/ui/src/providers/Hierarchy/index.tsx index 3a6c90d253f..61149c0bfcb 100644 --- a/packages/ui/src/providers/Hierarchy/index.tsx +++ b/packages/ui/src/providers/Hierarchy/index.tsx @@ -2,7 +2,6 @@ import type { TypeWithID, Where } from 'payload' -import { useRouter } from 'next/navigation.js' import { DEFAULT_HIERARCHY_TREE_LIMIT, formatAdminURL, PREFERENCE_KEYS } from 'payload/shared' import * as qs from 'qs-esm' import React, { createContext, use, useCallback, useState } from 'react' @@ -19,6 +18,7 @@ import type { import { useDebouncedCallback } from '../../hooks/useDebouncedCallback.js' import { useConfig } from '../Config/index.js' import { usePreferences } from '../Preferences/index.js' +import { useRouter } from '../RouterAdapter/index.js' const HierarchyContext = createContext(undefined) diff --git a/packages/ui/src/providers/ListQuery/index.tsx b/packages/ui/src/providers/ListQuery/index.tsx index ebbacb95d49..460f04c85af 100644 --- a/packages/ui/src/providers/ListQuery/index.tsx +++ b/packages/ui/src/providers/ListQuery/index.tsx @@ -1,5 +1,4 @@ 'use client' -import { useRouter, useSearchParams } from 'next/navigation.js' import { type ListQuery, type Where } from 'payload' import * as qs from 'qs-esm' import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' @@ -11,6 +10,7 @@ import { useEffectEvent } from '../../hooks/useEffectEvent.js' import { useRouteTransition } from '../../providers/RouteTransition/index.js' import { parseSearchParams } from '../../utilities/parseSearchParams.js' import { useConfig } from '../Config/index.js' +import { useRouter, useSearchParams } from '../RouterAdapter/index.js' import { ListQueryContext, ListQueryModifiedContext } from './context.js' import { mergeQuery } from './mergeQuery.js' import { sanitizeQuery } from './sanitizeQuery.js' diff --git a/packages/ui/src/providers/Locale/index.tsx b/packages/ui/src/providers/Locale/index.tsx index 1e20b756af4..0b23b2b55c3 100644 --- a/packages/ui/src/providers/Locale/index.tsx +++ b/packages/ui/src/providers/Locale/index.tsx @@ -2,13 +2,13 @@ import type { Locale } from 'payload' -import { useSearchParams } from 'next/navigation.js' import { formatAdminURL } from 'payload/shared' import React, { createContext, use, useEffect, useRef, useState } from 'react' import { findLocaleFromCode } from '../../utilities/findLocaleFromCode.js' import { useAuth } from '../Auth/index.js' import { useConfig } from '../Config/index.js' +import { useSearchParams } from '../RouterAdapter/index.js' const LocaleContext = createContext({} as Locale) diff --git a/packages/ui/src/providers/Params/index.tsx b/packages/ui/src/providers/Params/index.tsx index 15d9a2bc514..f9a7503f019 100644 --- a/packages/ui/src/providers/Params/index.tsx +++ b/packages/ui/src/providers/Params/index.tsx @@ -1,8 +1,9 @@ 'use client' -import { useParams as useNextParams } from 'next/navigation.js' import React, { createContext, use } from 'react' +import { useParams as useNextParams } from '../RouterAdapter/index.js' + export type Params = ReturnType interface IParamsContext extends Params {} @@ -10,11 +11,7 @@ const Context = createContext({} as IParamsContext) /** * @deprecated - * The ParamsProvider is deprecated and will be removed in the next major release. Instead, use the `useParams` hook from `next/navigation` directly. See https://github.com/payloadcms/payload/pull/9581. - * @example - * ```tsx - * import { useParams } from 'next/navigation' - * ``` + * The ParamsProvider is deprecated and will be removed in the next major release. Instead, use the `useParams` hook from `@payloadcms/ui` directly. See https://github.com/payloadcms/payload/pull/9581. */ export const ParamsProvider: React.FC<{ children?: React.ReactNode }> = ({ children }) => { const params = useNextParams() @@ -23,10 +20,6 @@ export const ParamsProvider: React.FC<{ children?: React.ReactNode }> = ({ child /** * @deprecated - * The `useParams` hook is deprecated and will be removed in the next major release. Instead, use the `useParams` hook from `next/navigation` directly. See https://github.com/payloadcms/payload/pull/9581. - * @example - * ```tsx - * import { useParams } from 'next/navigation' - * ``` + * The `useParams` hook is deprecated and will be removed in the next major release. Instead, use the `useParams` hook from `@payloadcms/ui` directly. See https://github.com/payloadcms/payload/pull/9581. */ export const useParams = (): IParamsContext => use(Context) diff --git a/packages/ui/src/providers/Root/index.tsx b/packages/ui/src/providers/Root/index.tsx index 46cd08bcb5b..712d29a4961 100644 --- a/packages/ui/src/providers/Root/index.tsx +++ b/packages/ui/src/providers/Root/index.tsx @@ -4,6 +4,7 @@ import type { ClientConfig, LanguageOptions, Locale, + RouterAdapterComponent, SanitizedPermissions, ServerFunctionClient, TypedUser, @@ -51,6 +52,7 @@ type Props = { readonly languageOptions: LanguageOptions readonly locale?: Locale['code'] readonly permissions: SanitizedPermissions + readonly RouterAdapter: RouterAdapterComponent readonly serverFunction: ServerFunctionClient readonly switchLanguageServerAction?: (lang: string) => Promise readonly theme: Theme @@ -69,6 +71,7 @@ export const RootProvider: React.FC = ({ languageOptions, locale, permissions, + RouterAdapter, serverFunction, switchLanguageServerAction, theme, @@ -79,74 +82,80 @@ export const RootProvider: React.FC = ({ return ( - - - - - - - + + + + + + - - - - - - - - - - - - - - - - - {children} - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + {children} + + + + + + + + + + + + + + + + + + + + + + + + + ) diff --git a/packages/ui/src/providers/RouteCache/index.tsx b/packages/ui/src/providers/RouteCache/index.tsx index 36e8bf5ee37..8a0601a1397 100644 --- a/packages/ui/src/providers/RouteCache/index.tsx +++ b/packages/ui/src/providers/RouteCache/index.tsx @@ -1,9 +1,9 @@ 'use client' -import { usePathname, useRouter } from 'next/navigation.js' import React, { createContext, use, useCallback, useEffect, useRef } from 'react' import { useEffectEvent } from '../../hooks/useEffectEvent.js' +import { usePathname, useRouter } from '../RouterAdapter/index.js' export type RouteCacheContext = { cachingEnabled: boolean diff --git a/packages/ui/src/providers/RouteTransition/index.tsx b/packages/ui/src/providers/RouteTransition/index.tsx index 06c529f65de..c5c228b185a 100644 --- a/packages/ui/src/providers/RouteTransition/index.tsx +++ b/packages/ui/src/providers/RouteTransition/index.tsx @@ -110,8 +110,7 @@ const RouteTransitionContext = React.createContext( * @example * 'use client' * import React, { useCallback } from 'react' - * import { useTransition } from '@payloadcms/ui' - * import { useRouter } from 'next/navigation' + * import { useTransition, useRouter } from '@payloadcms/ui' * * const MyComponent: React.FC = () => { * const router = useRouter() diff --git a/packages/ui/src/providers/RouterAdapter/index.tsx b/packages/ui/src/providers/RouterAdapter/index.tsx new file mode 100644 index 00000000000..a6e2e7804bc --- /dev/null +++ b/packages/ui/src/providers/RouterAdapter/index.tsx @@ -0,0 +1,56 @@ +'use client' +import type { LinkAdapterProps, RouterAdapterRouter } from 'payload' + +import React, { createContext, use } from 'react' + +export type RouterAdapterContextValue = { + Link: React.ComponentType + params: Record + pathname: string + router: RouterAdapterRouter + searchParams: URLSearchParams +} + +/** + * The RouterAdapter context. Framework adapters populate this by rendering a + * provider component that calls framework-specific hooks (e.g. next/navigation) + * and passes the values here. This avoids calling dynamic hooks from props, + * which would violate React compiler rules. + */ +export const RouterAdapterContext = createContext(null) + +function useRouterAdapterContext(): RouterAdapterContextValue { + const ctx = use(RouterAdapterContext) + if (!ctx) { + throw new Error( + 'useRouter/usePathname/useSearchParams/useParams must be used within a RouterAdapterProvider', + ) + } + return ctx +} + +export function useRouter(): RouterAdapterRouter { + return useRouterAdapterContext().router +} + +export function usePathname(): string { + return useRouterAdapterContext().pathname +} + +export function useSearchParams(): URLSearchParams { + return useRouterAdapterContext().searchParams +} + +export function useParams(): Record { + return useRouterAdapterContext().params +} + +export const PayloadLink: React.FC<{ ref?: React.Ref } & LinkAdapterProps> = ({ + ref, + ...props +}: { ref?: React.RefObject } & LinkAdapterProps) => { + const { Link: LinkComponent } = useRouterAdapterContext() + return +} + +export type { LinkAdapterProps, RouterAdapterRouter } diff --git a/packages/ui/src/providers/SearchParams/index.tsx b/packages/ui/src/providers/SearchParams/index.tsx index cb66385ea84..708a0b030cb 100644 --- a/packages/ui/src/providers/SearchParams/index.tsx +++ b/packages/ui/src/providers/SearchParams/index.tsx @@ -1,9 +1,10 @@ 'use client' -import { useSearchParams as useNextSearchParams } from 'next/navigation.js' import * as qs from 'qs-esm' import React, { createContext, use } from 'react' +import { useSearchParams as useNextSearchParams } from '../RouterAdapter/index.js' + export type SearchParamsContext = { searchParams: qs.ParsedQs stringifyParams: ({ params, replace }: { params: qs.ParsedQs; replace?: boolean }) => string @@ -18,11 +19,7 @@ const Context = createContext(initialContext) /** * @deprecated - * The SearchParamsProvider is deprecated and will be removed in the next major release. Instead, use the `useSearchParams` hook from `next/navigation` directly. See https://github.com/payloadcms/payload/pull/9581. - * @example - * ```tsx - * import { useSearchParams } from 'next/navigation' - * ``` + * The SearchParamsProvider is deprecated and will be removed in the next major release. Instead, use the `useSearchParams` hook from `@payloadcms/ui` directly. See https://github.com/payloadcms/payload/pull/9581. */ export const SearchParamsProvider: React.FC<{ children?: React.ReactNode }> = ({ children }) => { const nextSearchParams = useNextSearchParams() @@ -55,11 +52,7 @@ export const SearchParamsProvider: React.FC<{ children?: React.ReactNode }> = ({ /** * @deprecated - * The `useSearchParams` hook is deprecated and will be removed in the next major release. Instead, use the `useSearchParams` hook from `next/navigation` directly. See https://github.com/payloadcms/payload/pull/9581. - * @example - * ```tsx - * import { useSearchParams } from 'next/navigation' - * ``` + * The `useSearchParams` hook is deprecated and will be removed in the next major release. Instead, use the `useSearchParams` hook from `@payloadcms/ui` directly. See https://github.com/payloadcms/payload/pull/9581. * If you need to parse the `where` query, you can do so with the `parseSearchParams` utility. * ```tsx * import { parseSearchParams } from '@payloadcms/ui' diff --git a/packages/ui/src/providers/Selection/index.tsx b/packages/ui/src/providers/Selection/index.tsx index f55ec5d87a6..09a6ebb2e63 100644 --- a/packages/ui/src/providers/Selection/index.tsx +++ b/packages/ui/src/providers/Selection/index.tsx @@ -1,7 +1,6 @@ 'use client' import type { Where } from 'payload' -import { useSearchParams } from 'next/navigation.js' import * as qs from 'qs-esm' import React, { createContext, use, useCallback, useEffect, useMemo, useRef, useState } from 'react' @@ -9,6 +8,7 @@ import { parseSearchParams } from '../../utilities/parseSearchParams.js' import { useAuth } from '../Auth/index.js' import { useListQuery } from '../ListQuery/index.js' import { useLocale } from '../Locale/index.js' +import { useSearchParams } from '../RouterAdapter/index.js' export enum SelectAllStatus { AllAvailable = 'allAvailable', diff --git a/packages/ui/src/providers/Translation/index.tsx b/packages/ui/src/providers/Translation/index.tsx index 01ba55fbd22..3938e07bd20 100644 --- a/packages/ui/src/providers/Translation/index.tsx +++ b/packages/ui/src/providers/Translation/index.tsx @@ -13,9 +13,10 @@ import type { LanguageOptions } from 'payload' import { importDateFNSLocale, t } from '@payloadcms/translations' import { enUS } from 'date-fns/locale/en-US' -import { useRouter } from 'next/navigation.js' import React, { createContext, use, useEffect, useState } from 'react' +import { useRouter } from '../RouterAdapter/index.js' + type ContextType< TAdditionalTranslations = {}, TAdditionalClientTranslationKeys extends string = never, diff --git a/packages/ui/src/utilities/getRequestLanguage.ts b/packages/ui/src/utilities/getRequestLanguage.ts index 1c6813a8361..27a697d559b 100644 --- a/packages/ui/src/utilities/getRequestLanguage.ts +++ b/packages/ui/src/utilities/getRequestLanguage.ts @@ -1,7 +1,7 @@ -import type { ReadonlyRequestCookies } from 'next/dist/server/web/spec-extension/adapters/request-cookies.js' +import type { CookieStore } from 'payload' type GetRequestLanguageArgs = { - cookies: Map | ReadonlyRequestCookies + cookies: CookieStore | Map defaultLanguage?: string headers: Request['headers'] } diff --git a/packages/ui/src/utilities/handleBackToDashboard.tsx b/packages/ui/src/utilities/handleBackToDashboard.tsx index d5b25efc159..13799382297 100644 --- a/packages/ui/src/utilities/handleBackToDashboard.tsx +++ b/packages/ui/src/utilities/handleBackToDashboard.tsx @@ -1,10 +1,10 @@ -import type { AppRouterInstance } from 'next/dist/shared/lib/app-router-context.shared-runtime.js' +import type { RouterAdapterRouter } from 'payload' import { formatAdminURL } from 'payload/shared' type BackToDashboardProps = { adminRoute: string - router: AppRouterInstance + router: RouterAdapterRouter serverURL?: string } diff --git a/packages/ui/src/utilities/handleGoBack.tsx b/packages/ui/src/utilities/handleGoBack.tsx index 5a4cec75055..5008f785e93 100644 --- a/packages/ui/src/utilities/handleGoBack.tsx +++ b/packages/ui/src/utilities/handleGoBack.tsx @@ -1,11 +1,11 @@ -import type { AppRouterInstance } from 'next/dist/shared/lib/app-router-context.shared-runtime.js' +import type { RouterAdapterRouter } from 'payload' import { formatAdminURL } from 'payload/shared' type GoBackProps = { adminRoute: string collectionSlug: string - router: AppRouterInstance + router: RouterAdapterRouter serverURL?: string } diff --git a/packages/ui/src/utilities/parseSearchParams.ts b/packages/ui/src/utilities/parseSearchParams.ts index d6631321010..03e4cbefaa8 100644 --- a/packages/ui/src/utilities/parseSearchParams.ts +++ b/packages/ui/src/utilities/parseSearchParams.ts @@ -1,5 +1,3 @@ -import type { ReadonlyURLSearchParams } from 'next/navigation.js' - import * as qs from 'qs-esm' /** @@ -7,10 +5,10 @@ import * as qs from 'qs-esm' * This function is a wrapper around the `qs` library. * In Next.js, the `useSearchParams()` hook from `next/navigation` returns a `URLSearchParams` object. * This function can be used to parse that object into a more usable format. - * @param {ReadonlyURLSearchParams} searchParams - The URLSearchParams object to parse. + * @param {URLSearchParams} searchParams - The URLSearchParams object to parse. * @returns {qs.ParsedQs} - The parsed query string object. */ -export function parseSearchParams(searchParams: ReadonlyURLSearchParams): qs.ParsedQs { +export function parseSearchParams(searchParams: URLSearchParams): qs.ParsedQs { const search = searchParams.toString() return qs.parse(search, { diff --git a/packages/ui/src/views/Edit/index.tsx b/packages/ui/src/views/Edit/index.tsx index cd7cc5ae44f..706def70077 100644 --- a/packages/ui/src/views/Edit/index.tsx +++ b/packages/ui/src/views/Edit/index.tsx @@ -2,7 +2,6 @@ import type { ClientUser, DocumentViewClientProps } from 'payload' -import { useRouter, useSearchParams } from 'next/navigation.js' import { formatAdminURL, hasAutosaveEnabled } from 'payload/shared' import React, { Fragment, useCallback, useEffect, useMemo, useRef, useState } from 'react' import { toast } from 'sonner' @@ -30,6 +29,7 @@ import { useEditDepth } from '../../providers/EditDepth/index.js' import { useLivePreviewContext, usePreviewURL } from '../../providers/LivePreview/context.js' import { OperationProvider } from '../../providers/Operation/index.js' import { useRouteCache } from '../../providers/RouteCache/index.js' +import { useRouter, useSearchParams } from '../../providers/RouterAdapter/index.js' import { useRouteTransition } from '../../providers/RouteTransition/index.js' import { useServerFunctions } from '../../providers/ServerFunctions/index.js' import { UploadControlsProvider } from '../../providers/UploadControls/index.js' diff --git a/packages/ui/src/views/HierarchyList/index.tsx b/packages/ui/src/views/HierarchyList/index.tsx index f7c875fb342..3a719cd89d0 100644 --- a/packages/ui/src/views/HierarchyList/index.tsx +++ b/packages/ui/src/views/HierarchyList/index.tsx @@ -3,7 +3,6 @@ import type { ListViewClientProps } from 'payload' import { getTranslation } from '@payloadcms/translations' -import { useRouter, useSearchParams } from 'next/navigation.js' import { formatAdminURL } from 'payload/shared' import * as qs from 'qs-esm' import React, { Fragment, useCallback, useEffect, useMemo, useState } from 'react' @@ -23,6 +22,7 @@ import { useConfig } from '../../providers/Config/index.js' import { DocumentSelectionProvider } from '../../providers/DocumentSelection/index.js' import { useHierarchy } from '../../providers/Hierarchy/index.js' import { useRouteCache } from '../../providers/RouteCache/index.js' +import { useRouter, useSearchParams } from '../../providers/RouterAdapter/index.js' import { useRouteTransition } from '../../providers/RouteTransition/index.js' import { useTranslation } from '../../providers/Translation/index.js' import { HierarchyListHeader } from './HierarchyListHeader/index.js' diff --git a/packages/ui/src/views/List/index.tsx b/packages/ui/src/views/List/index.tsx index 851c094bfda..cf814124eb1 100644 --- a/packages/ui/src/views/List/index.tsx +++ b/packages/ui/src/views/List/index.tsx @@ -3,7 +3,6 @@ import type { ListViewClientProps } from 'payload' import { getTranslation } from '@payloadcms/translations' -import { useRouter } from 'next/navigation.js' import { formatAdminURL, formatFilesize } from 'payload/shared' import React, { Fragment, useEffect } from 'react' @@ -25,6 +24,7 @@ import { useControllableState } from '../../hooks/useControllableState.js' import { useConfig } from '../../providers/Config/index.js' import { DocumentSelectionProvider } from '../../providers/DocumentSelection/index.js' import { useListQuery } from '../../providers/ListQuery/index.js' +import { useRouter } from '../../providers/RouterAdapter/index.js' import { SelectionProvider } from '../../providers/Selection/index.js' import { TableColumnsProvider } from '../../providers/TableColumns/index.js' import { useTranslation } from '../../providers/Translation/index.js' diff --git a/packages/ui/src/widgets/CollectionCards/index.tsx b/packages/ui/src/widgets/CollectionCards/index.tsx index 704df5758f4..d9df0e8d692 100644 --- a/packages/ui/src/widgets/CollectionCards/index.tsx +++ b/packages/ui/src/widgets/CollectionCards/index.tsx @@ -5,9 +5,8 @@ import { EntityType, getAccessResults } from 'payload' import { formatAdminURL } from 'payload/shared' import React from 'react' -import { Button } from '../../elements/Button/index.js' -import { Card } from '../../elements/Card/index.js' -import { Locked } from '../../elements/Locked/index.js' +// eslint-disable-next-line payload/no-imports-from-exports-dir -- Server component must reference exports dir for proper client boundary +import { Button, Card, Locked } from '../../exports/client/index.js' import { getGlobalData } from '../../utilities/getGlobalData.js' import { getNavGroups } from '../../utilities/getNavGroups.js' import { getVisibleEntities } from '../../utilities/getVisibleEntities.js' diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 93acfd2da7c..a84e7f32bc9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1723,9 +1723,6 @@ importers: md5: specifier: 2.3.0 version: 2.3.0 - next: - specifier: '>=16.2.6 <17.0.0' - version: 16.2.6(@babel/core@7.27.3)(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(babel-plugin-react-compiler@19.1.0-rc.3)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.99.0) object-to-formdata: specifier: 4.5.1 version: 4.5.1 @@ -26265,34 +26262,6 @@ snapshots: - '@babel/core' - babel-plugin-macros - next@16.2.6(@babel/core@7.27.3)(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(babel-plugin-react-compiler@19.1.0-rc.3)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.99.0): - dependencies: - '@next/env': 16.2.6 - '@swc/helpers': 0.5.15 - baseline-browser-mapping: 2.10.29 - caniuse-lite: 1.0.30001792 - postcss: 8.4.31 - react: 19.2.6 - react-dom: 19.2.6(react@19.2.6) - styled-jsx: 5.1.6(@babel/core@7.27.3)(react@19.2.6) - optionalDependencies: - '@next/swc-darwin-arm64': 16.2.6 - '@next/swc-darwin-x64': 16.2.6 - '@next/swc-linux-arm64-gnu': 16.2.6 - '@next/swc-linux-arm64-musl': 16.2.6 - '@next/swc-linux-x64-gnu': 16.2.6 - '@next/swc-linux-x64-musl': 16.2.6 - '@next/swc-win32-arm64-msvc': 16.2.6 - '@next/swc-win32-x64-msvc': 16.2.6 - '@opentelemetry/api': 1.9.1 - '@playwright/test': 1.59.1 - babel-plugin-react-compiler: 19.1.0-rc.3 - sass: 1.99.0 - sharp: 0.34.5 - transitivePeerDependencies: - - '@babel/core' - - babel-plugin-macros - next@16.2.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.99.0): dependencies: '@next/env': 16.2.6 From 6b31308d6751491c11205447ad3efae8389882d6 Mon Sep 17 00:00:00 2001 From: Jake Fletcher Date: Wed, 27 May 2026 16:19:14 -0400 Subject: [PATCH 2/3] refactor(next): move RouterAdapter to adapters/ directory MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Groups framework adapter implementations under packages/next/src/adapters/ instead of mixing them into elements/ (UI primitives) and utilities/ (misc helpers). The Next adapter is neither — it bridges next/navigation into the RouterAdapterContext contract from payload core. Sets up the structure for the upcoming ServerAdapter to live alongside as adapters/server.ts. --- .../{elements/RouterAdapter/index.tsx => adapters/router.tsx} | 0 packages/next/src/layouts/Root/index.tsx | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename packages/next/src/{elements/RouterAdapter/index.tsx => adapters/router.tsx} (100%) diff --git a/packages/next/src/elements/RouterAdapter/index.tsx b/packages/next/src/adapters/router.tsx similarity index 100% rename from packages/next/src/elements/RouterAdapter/index.tsx rename to packages/next/src/adapters/router.tsx diff --git a/packages/next/src/layouts/Root/index.tsx b/packages/next/src/layouts/Root/index.tsx index 87dc629e807..c9743847e9b 100644 --- a/packages/next/src/layouts/Root/index.tsx +++ b/packages/next/src/layouts/Root/index.tsx @@ -10,8 +10,8 @@ import { cookies as nextCookies } from 'next/headers.js' import { applyLocaleFiltering } from 'payload/shared' import React, { Suspense } from 'react' +import { NextRouterAdapter } from '../../adapters/router.js' import { getNavPrefs } from '../../elements/Nav/getNavPrefs.js' -import { NextRouterAdapter } from '../../elements/RouterAdapter/index.js' import { getRequestHighContrast } from '../../utilities/getRequestHighContrast.js' import { getRequestTheme } from '../../utilities/getRequestTheme.js' import { initReq } from '../../utilities/initReq.js' From 6202bd036fd56148772fad191d6c3926059bc4cd Mon Sep 17 00:00:00 2001 From: Jake Fletcher Date: Wed, 27 May 2026 16:34:59 -0400 Subject: [PATCH 3/3] refactor(payload): split admin/adapters.ts into adapters/ directory Separates router contracts from cookie contracts: - adapters/router.ts: RouterAdapterComponent, RouterAdapterRouter, LinkAdapterProps - adapters/cookies.ts: CookieStore, CookieOptions - adapters/index.ts: barrel re-export Makes room for ServerAdapter to land as adapters/server.ts on its own branch without re-mingling concerns in a single file. --- packages/payload/src/admin/adapters/cookies.ts | 15 +++++++++++++++ packages/payload/src/admin/adapters/index.ts | 2 ++ .../admin/{adapters.ts => adapters/router.ts} | 16 ---------------- packages/payload/src/index.ts | 2 +- 4 files changed, 18 insertions(+), 17 deletions(-) create mode 100644 packages/payload/src/admin/adapters/cookies.ts create mode 100644 packages/payload/src/admin/adapters/index.ts rename packages/payload/src/admin/{adapters.ts => adapters/router.ts} (78%) diff --git a/packages/payload/src/admin/adapters/cookies.ts b/packages/payload/src/admin/adapters/cookies.ts new file mode 100644 index 00000000000..aa071d94bf3 --- /dev/null +++ b/packages/payload/src/admin/adapters/cookies.ts @@ -0,0 +1,15 @@ +export type CookieStore = { + get: (name: string) => { name: string; value: string } | null | undefined + getAll?: () => Array<{ name: string; value: string }> + set?: (name: string, value: string, options?: CookieOptions) => void +} + +export type CookieOptions = { + domain?: string + expires?: Date + httpOnly?: boolean + maxAge?: number + path?: string + sameSite?: 'lax' | 'none' | 'strict' + secure?: boolean +} diff --git a/packages/payload/src/admin/adapters/index.ts b/packages/payload/src/admin/adapters/index.ts new file mode 100644 index 00000000000..e96db91b170 --- /dev/null +++ b/packages/payload/src/admin/adapters/index.ts @@ -0,0 +1,2 @@ +export type * from './cookies.js' +export type * from './router.js' diff --git a/packages/payload/src/admin/adapters.ts b/packages/payload/src/admin/adapters/router.ts similarity index 78% rename from packages/payload/src/admin/adapters.ts rename to packages/payload/src/admin/adapters/router.ts index cc3acfb125e..88583f986b8 100644 --- a/packages/payload/src/admin/adapters.ts +++ b/packages/payload/src/admin/adapters/router.ts @@ -42,22 +42,6 @@ export type RouterAdapterRouter = { replace: (path: string, options?: { scroll?: boolean }) => void } -export type CookieStore = { - get: (name: string) => { name: string; value: string } | null | undefined - getAll?: () => Array<{ name: string; value: string }> - set?: (name: string, value: string, options?: CookieOptions) => void -} - -export type CookieOptions = { - domain?: string - expires?: Date - httpOnly?: boolean - maxAge?: number - path?: string - sameSite?: 'lax' | 'none' | 'strict' - secure?: boolean -} - export type LinkAdapterProps = { children?: React.ReactNode href: string diff --git a/packages/payload/src/index.ts b/packages/payload/src/index.ts index 317cfc50c06..578cbbb5220 100644 --- a/packages/payload/src/index.ts +++ b/packages/payload/src/index.ts @@ -37,7 +37,7 @@ import { verifyEmailLocal, type Options as VerifyEmailOptions, } from './auth/operations/local/verifyEmail.js' -export type * from './admin/adapters.js' +export type * from './admin/adapters/index.js' import type { InitOptions, SanitizedConfig } from './config/types.js' import type { BaseDatabaseAdapter, PaginatedDistinctDocs, PaginatedDocs } from './database/types.js' import type { InitializedEmailAdapter } from './email/types.js'