Skip to content

Commit 58bd5ef

Browse files
committed
Merge remote-tracking branch 'origin/main' into feat/accurate-lexical-types
2 parents 30dfd9d + dc6eb93 commit 58bd5ef

210 files changed

Lines changed: 2385 additions & 1270 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/migration-guide/v4.mdx

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,60 @@ Several types and utilities that were re-exported from `@payloadcms/ui` and `@pa
151151

152152
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.
153153

154+
### `next/navigation` and `next/link` replaced by framework-agnostic `RouterAdapter` hooks
155+
156+
`@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.
157+
158+
**Hooks** — import from `@payloadcms/ui` instead of `next/navigation`:
159+
160+
| Old import | New import |
161+
| --------------------------------------------------- | -------------------------------------------------- |
162+
| `import { useRouter } from 'next/navigation'` | `import { useRouter } from '@payloadcms/ui'` |
163+
| `import { usePathname } from 'next/navigation'` | `import { usePathname } from '@payloadcms/ui'` |
164+
| `import { useSearchParams } from 'next/navigation'` | `import { useSearchParams } from '@payloadcms/ui'` |
165+
| `import { useParams } from 'next/navigation'` | `import { useParams } from '@payloadcms/ui'` |
166+
167+
**`Link` component** — use `PayloadLink` from `@payloadcms/ui`:
168+
169+
```diff
170+
- import Link from 'next/link'
171+
+ import { PayloadLink as Link } from '@payloadcms/ui'
172+
```
173+
174+
**Types**`LinkProps` from `next/link` is replaced by `LinkAdapterProps` from `payload`:
175+
176+
```diff
177+
- import type { LinkProps } from 'next/link'
178+
+ import type { LinkAdapterProps } from 'payload'
179+
```
180+
181+
`LinkAdapterProps` is a framework-neutral shape: `href: string`, optional `prefetch` / `replace` / `scroll` / `ref`, plus standard `AnchorHTMLAttributes<HTMLAnchorElement>` (minus `href`). The Next adapter forwards these to `next/link`.
182+
183+
**`RouterAdapterRouter` shape** — the object returned by `useRouter()` now has a stable, framework-agnostic surface:
184+
185+
```ts
186+
type RouterAdapterRouter = {
187+
back: () => void
188+
push: (path: string, options?: { scroll?: boolean }) => void
189+
refresh: () => void
190+
replace: (path: string, options?: { scroll?: boolean }) => void
191+
}
192+
```
193+
194+
If you were relying on Next-specific extras like `prefetch()` on the router instance, switch to the `Link` component's `prefetch` prop instead.
195+
196+
**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:
197+
198+
```diff
199+
+ import { NextRouterAdapter } from '@payloadcms/next/elements/RouterAdapter'
200+
201+
<RootProvider
202+
+ RouterAdapter={NextRouterAdapter}
203+
config={clientConfig}
204+
/* ... */
205+
/>
206+
```
207+
154208
### `title` and `setDocumentTitle` removed from `useDocumentInfo`
155209

156210
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.
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
'use client'
2+
import type { RouterAdapterContextValue } from '@payloadcms/ui'
3+
import type { LinkAdapterProps } from 'payload'
4+
5+
import { RouterAdapterContext } from '@payloadcms/ui'
6+
import NextLinkImport from 'next/link.js'
7+
import {
8+
useParams as useNextParams,
9+
usePathname as useNextPathname,
10+
useRouter as useNextRouter,
11+
useSearchParams as useNextSearchParams,
12+
} from 'next/navigation.js'
13+
import React from 'react'
14+
15+
type LinkComponent = React.FC<
16+
{
17+
children?: React.ReactNode
18+
href: string
19+
prefetch?: boolean
20+
ref?: React.Ref<HTMLAnchorElement>
21+
replace?: boolean
22+
scroll?: boolean
23+
} & Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, 'href'>
24+
>
25+
26+
const NextLink: LinkComponent = ('default' in NextLinkImport
27+
? NextLinkImport.default
28+
: NextLinkImport) as unknown as LinkComponent
29+
30+
const NextLinkAdapter: React.FC<LinkAdapterProps> = ({
31+
children,
32+
href,
33+
prefetch,
34+
ref,
35+
replace,
36+
scroll,
37+
...rest
38+
}) => {
39+
return (
40+
<NextLink href={href} prefetch={prefetch} ref={ref} replace={replace} scroll={scroll} {...rest}>
41+
{children}
42+
</NextLink>
43+
)
44+
}
45+
46+
export const NextRouterAdapter: React.FC<{ children: React.ReactNode }> = ({ children }) => {
47+
const nextRouter = useNextRouter()
48+
const pathname = useNextPathname()
49+
const searchParams = useNextSearchParams()
50+
const params = useNextParams()
51+
52+
const router = React.useMemo<RouterAdapterContextValue['router']>(
53+
() => ({
54+
back: nextRouter.back,
55+
push: nextRouter.push,
56+
refresh: nextRouter.refresh,
57+
replace: nextRouter.replace,
58+
}),
59+
[nextRouter],
60+
)
61+
62+
const value = React.useMemo<RouterAdapterContextValue>(
63+
() => ({
64+
Link: NextLinkAdapter,
65+
params: params as Record<string, string | string[]>,
66+
pathname,
67+
router,
68+
searchParams,
69+
}),
70+
[params, pathname, router, searchParams],
71+
)
72+
73+
return <RouterAdapterContext value={value}>{children}</RouterAdapterContext>
74+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import type { CookieStore, ServerAdapter } from 'payload'
2+
3+
import { cookies as getNextCookies, headers as getNextHeaders } from 'next/headers.js'
4+
import {
5+
forbidden as nextForbidden,
6+
notFound as nextNotFound,
7+
permanentRedirect as nextPermanentRedirect,
8+
redirect as nextRedirect,
9+
unauthorized as nextUnauthorized,
10+
} from 'next/navigation.js'
11+
12+
const toCookieStore = async (): Promise<CookieStore> => {
13+
const store = await getNextCookies()
14+
15+
return {
16+
get: (name) => {
17+
const cookie = store.get(name)
18+
return cookie ? { name: cookie.name, value: cookie.value } : undefined
19+
},
20+
getAll: () => store.getAll().map((cookie) => ({ name: cookie.name, value: cookie.value })),
21+
set: (name, value, options) => {
22+
store.set(name, value, options)
23+
},
24+
}
25+
}
26+
27+
/**
28+
* Adapts Next.js server-side APIs to the framework-agnostic `ServerAdapter` interface.
29+
* This way we can invoke these methods within our server components and plugins without importing Next.js modules directly.
30+
*/
31+
export const nextServerAdapter: ServerAdapter = {
32+
forbidden: () => nextForbidden(),
33+
getCookies: toCookieStore,
34+
getHeaders: () => getNextHeaders(),
35+
notFound: () => nextNotFound(),
36+
permanentRedirect: (path) => nextPermanentRedirect(path),
37+
redirect: (path) => nextRedirect(path),
38+
setCookie: async (name, value, options) => {
39+
const store = await getNextCookies()
40+
store.set(name, value, options)
41+
},
42+
unauthorized: () => nextUnauthorized(),
43+
}

packages/next/src/elements/DocumentHeader/Tabs/Tab/index.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ export const DefaultDocumentTab: React.FC<{
8282
payload: req.payload,
8383
permissions,
8484
req,
85+
server: req.server,
8586
user: req.user,
8687
} satisfies DocumentTabServerPropsOnly,
8788
})}

packages/next/src/elements/DocumentHeader/Tabs/index.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ export const DocumentTabs: React.FC<{
6161
payload: req.payload,
6262
permissions,
6363
req,
64+
server: req.server,
6465
user: req.user,
6566
} satisfies DocumentTabServerPropsOnly,
6667
})

packages/next/src/elements/Nav/NavWrapper/index.scss

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,9 @@
99
height: 100vh;
1010
width: var(--nav-width);
1111
border-right: 1px solid var(--theme-elevation-100);
12-
opacity: 0;
13-
1412
[dir='rtl'] & {
1513
border-right: none;
1614
border-left: 1px solid var(--theme-elevation-100);
1715
}
18-
19-
&--nav-animate {
20-
transition: opacity var(--nav-trans-time) ease-in-out;
21-
}
22-
23-
&--nav-open {
24-
opacity: 1;
25-
}
2616
}
2717
}

packages/next/src/elements/Nav/NavWrapper/index.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,13 @@ export const NavWrapper: React.FC<{
1313
}> = (props) => {
1414
const { baseClass, children } = props
1515

16-
const { hydrated, navOpen, navRef, shouldAnimate } = useNav()
16+
const { hydrated, navOpen, navRef } = useNav()
1717

1818
return (
1919
<aside
2020
className={[
2121
baseClass,
2222
navOpen && `${baseClass}--nav-open`,
23-
shouldAnimate && `${baseClass}--nav-animate`,
2423
hydrated && `${baseClass}--nav-hydrated`,
2524
]
2625
.filter(Boolean)

packages/next/src/elements/Nav/index.css

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
height: 100vh;
88
width: var(--nav-width);
99
border-right: 1px solid var(--color-border);
10-
opacity: 0;
1110
overflow: hidden;
1211
--nav-padding-inline-start: var(--spacer-2-5);
1312
--nav-padding-inline-end: var(--spacer-2-5);
@@ -20,14 +19,6 @@
2019
border-left: 1px solid var(--color-border);
2120
}
2221

23-
.nav--nav-animate {
24-
transition: opacity var(--nav-trans-time) ease-in-out;
25-
}
26-
27-
.nav--nav-open {
28-
opacity: 1;
29-
}
30-
3122
.nav__mobile-close {
3223
display: none;
3324
background: none;

packages/next/src/elements/Nav/index.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,7 @@ export const DefaultNav: React.FC<NavProps> = async (props) => {
226226
permissions={permissions}
227227
req={req}
228228
searchParams={searchParams}
229+
server={req?.server}
229230
tabs={allTabs}
230231
user={user}
231232
viewType={viewType}

packages/next/src/layouts/Root/index.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { cookies as nextCookies } from 'next/headers.js'
1010
import { applyLocaleFiltering } from 'payload/shared'
1111
import React, { Suspense } from 'react'
1212

13+
import { NextRouterAdapter } from '../../adapters/router.js'
1314
import { getNavPrefs } from '../../elements/Nav/getNavPrefs.js'
1415
import { getRequestHighContrast } from '../../utilities/getRequestHighContrast.js'
1516
import { getRequestTheme } from '../../utilities/getRequestTheme.js'
@@ -185,6 +186,7 @@ const RootLayoutContent = async ({
185186
languageOptions={languageOptions}
186187
locale={req.locale}
187188
permissions={req.user ? permissions : null}
189+
RouterAdapter={NextRouterAdapter}
188190
serverFunction={serverFunction}
189191
switchLanguageServerAction={switchLanguageServerAction}
190192
theme={theme}
@@ -201,6 +203,7 @@ const RootLayoutContent = async ({
201203
i18n: req.i18n,
202204
payload: req.payload,
203205
permissions,
206+
server: req.server,
204207
user: req.user,
205208
}}
206209
>

0 commit comments

Comments
 (0)