Skip to content

Commit e905675

Browse files
chore!: adjusts auth hydration from server (#7545)
Fixes #6823 Allows the server to initialize the AuthProvider via props. Renames `HydrateClientUser` to `HydrateAuthProvider`. It now only hydrates the permissions as the user can be set from props. Permissions can be initialized from props, but still need to be hydrated for some pages as access control can be specific to docs/lists etc. **BREAKING CHANGE** - Renames exported `HydrateClientUser` to `HydrateAuthProvider`
1 parent 4a20a63 commit e905675

File tree

19 files changed

+190
-109
lines changed

19 files changed

+190
-109
lines changed

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

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,15 @@ import {
1414
} from '@payloadcms/ui'
1515
import { EntityType, formatAdminURL, groupNavItems } from '@payloadcms/ui/shared'
1616
import LinkWithDefault from 'next/link.js'
17+
import { usePathname } from 'next/navigation.js'
1718
import React, { Fragment } from 'react'
1819

1920
const baseClass = 'nav'
2021

2122
export const DefaultNavClient: React.FC = () => {
2223
const { permissions } = useAuth()
2324
const { isEntityVisible } = useEntityVisibility()
25+
const pathname = usePathname()
2426

2527
const {
2628
collections,
@@ -84,27 +86,23 @@ export const DefaultNavClient: React.FC = () => {
8486
LinkWithDefault) as typeof LinkWithDefault.default
8587

8688
const LinkElement = Link || 'a'
87-
88-
const activeCollection = window?.location?.pathname
89-
?.split('/')
90-
.find(
91-
(_, index, arr) =>
92-
arr[index - 1] === 'collections' || arr[index - 1] === 'globals',
93-
)
89+
const activeCollection = pathname.startsWith(href)
9490

9591
return (
9692
<LinkElement
97-
className={[`${baseClass}__link`, activeCollection === entity?.slug && `active`]
93+
className={[`${baseClass}__link`, activeCollection && `active`]
9894
.filter(Boolean)
9995
.join(' ')}
10096
href={href}
10197
id={id}
10298
key={i}
10399
tabIndex={!navOpen ? -1 : undefined}
104100
>
105-
<span className={`${baseClass}__link-icon`}>
106-
<ChevronIcon direction="right" />
107-
</span>
101+
{activeCollection && (
102+
<span className={`${baseClass}__link-icon`}>
103+
<ChevronIcon direction="right" />
104+
</span>
105+
)}
108106
<span className={`${baseClass}__link-label`}>{entityLabel}</span>
109107
</LinkElement>
110108
)

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

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -110,16 +110,9 @@
110110
&__link {
111111
display: flex;
112112
align-items: center;
113-
114-
&.active {
115-
.nav__link-icon {
116-
display: block;
117-
}
118-
}
119113
}
120114

121115
&__link-icon {
122-
display: none;
123116
margin-right: calc(var(--base) * 0.25);
124117
top: -1px;
125118
position: relative;

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

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import type { AcceptedLanguages, I18nClient } from '@payloadcms/translations'
2-
import type { SanitizedConfig } from 'payload'
2+
import type { PayloadRequest, SanitizedConfig } from 'payload'
33

44
import { initI18n, rtlLanguages } from '@payloadcms/translations'
55
import { RootProvider } from '@payloadcms/ui'
66
import '@payloadcms/ui/scss/app.scss'
77
import { buildComponentMap } from '@payloadcms/ui/utilities/buildComponentMap'
88
import { headers as getHeaders, cookies as nextCookies } from 'next/headers.js'
9-
import { createClientConfig, parseCookies } from 'payload'
9+
import { createClientConfig, createLocalReq, parseCookies } from 'payload'
10+
import * as qs from 'qs-esm'
1011
import React from 'react'
1112

1213
import { getPayloadHMR } from '../../utilities/getPayloadHMR.js'
@@ -52,6 +53,20 @@ export const RootLayout = async ({
5253
language: languageCode,
5354
})
5455

56+
const req = await createLocalReq(
57+
{
58+
fallbackLocale: null,
59+
req: {
60+
headers,
61+
host: headers.get('host'),
62+
i18n,
63+
url: `${payload.config.serverURL}`,
64+
} as PayloadRequest,
65+
},
66+
payload,
67+
)
68+
const { permissions, user } = await payload.auth({ headers, req })
69+
5570
const clientConfig = await createClientConfig({ config, t: i18n.t })
5671

5772
const dir = (rtlLanguages as unknown as AcceptedLanguages[]).includes(languageCode)
@@ -100,9 +115,11 @@ export const RootLayout = async ({
100115
fallbackLang={clientConfig.i18n.fallbackLanguage}
101116
languageCode={languageCode}
102117
languageOptions={languageOptions}
118+
permissions={permissions}
103119
switchLanguageServerAction={switchLanguageServerAction}
104120
theme={theme}
105121
translations={i18n.translations}
122+
user={user}
106123
>
107124
{wrappedChildren}
108125
</RootProvider>

packages/next/src/views/Account/index.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { AdminViewProps, ServerSideEditViewProps } from 'payload'
22

3-
import { DocumentInfoProvider, HydrateClientUser } from '@payloadcms/ui'
3+
import { DocumentInfoProvider, HydrateAuthProvider } from '@payloadcms/ui'
44
import { RenderCustomComponent } from '@payloadcms/ui/shared'
55
import { notFound } from 'next/navigation.js'
66
import React from 'react'
@@ -82,7 +82,7 @@ export const Account: React.FC<AdminViewProps> = async ({
8282
i18n={i18n}
8383
permissions={permissions}
8484
/>
85-
<HydrateClientUser permissions={permissions} user={user} />
85+
<HydrateAuthProvider permissions={permissions} />
8686
<RenderCustomComponent
8787
CustomComponent={
8888
typeof CustomAccountComponent === 'function' ? CustomAccountComponent : undefined

packages/next/src/views/Dashboard/index.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { EntityToGroup } from '@payloadcms/ui/shared'
22
import type { AdminViewProps } from 'payload'
33

4-
import { HydrateClientUser } from '@payloadcms/ui'
4+
import { HydrateAuthProvider } from '@payloadcms/ui'
55
import { EntityType, RenderCustomComponent, groupNavItems } from '@payloadcms/ui/shared'
66
import LinkImport from 'next/link.js'
77
import React, { Fragment } from 'react'
@@ -79,7 +79,6 @@ export const Dashboard: React.FC<AdminViewProps> = ({ initPageResult, params, se
7979

8080
return (
8181
<Fragment>
82-
<HydrateClientUser permissions={permissions} user={user} />
8382
<RenderCustomComponent
8483
CustomComponent={
8584
typeof CustomDashboardComponent === 'function' ? CustomDashboardComponent : undefined

packages/next/src/views/Document/index.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
import type { AdminViewComponent, AdminViewProps, EditViewComponent } from 'payload'
22

3-
import { DocumentInfoProvider, EditDepthProvider, HydrateClientUser } from '@payloadcms/ui'
3+
import { DocumentInfoProvider, EditDepthProvider, HydrateAuthProvider } from '@payloadcms/ui'
44
import {
55
RenderCustomComponent,
66
formatAdminURL,
77
isEditing as getIsEditing,
88
} from '@payloadcms/ui/shared'
99
import { notFound, redirect } from 'next/navigation.js'
10-
import { deepCopyObjectSimple } from 'payload'
1110
import React from 'react'
1211

1312
import type { GenerateEditViewMetadata } from './getMetaBySegment.js'
@@ -213,6 +212,7 @@ export const Document: React.FC<AdminViewProps> = async ({
213212
permissions={permissions}
214213
/>
215214
)}
215+
<HydrateAuthProvider permissions={permissions} />
216216
{/**
217217
* After bumping the Next.js canary to 104, and React to 19.0.0-rc-06d0b89e-20240801" we have to deepCopy the permissions object (https://github.com/payloadcms/payload/pull/7541).
218218
* If both HydrateClientUser and RenderCustomComponent receive the same permissions object (same object reference), we get a
@@ -221,7 +221,6 @@ export const Document: React.FC<AdminViewProps> = async ({
221221
*
222222
* // TODO: Revisit this in the future and figure out why this is happening. Might be a React/Next.js bug. We don't know why it happens, and a future React/Next version might unbreak this (keep an eye on this and remove deepCopyObjectSimple if that's the case)
223223
*/}
224-
<HydrateClientUser permissions={deepCopyObjectSimple(permissions)} user={user} />
225224
<EditDepthProvider
226225
depth={1}
227226
key={`${collectionSlug || globalSlug}${locale?.code ? `-${locale?.code}` : ''}`}

packages/next/src/views/List/index.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { AdminViewProps, Where } from 'payload'
22

33
import {
4-
HydrateClientUser,
4+
HydrateAuthProvider,
55
ListInfoProvider,
66
ListQueryProvider,
77
TableColumnsProvider,
@@ -138,7 +138,7 @@ export const ListView: React.FC<AdminViewProps> = async ({
138138

139139
return (
140140
<Fragment>
141-
<HydrateClientUser permissions={permissions} user={user} />
141+
<HydrateAuthProvider permissions={permissions} />
142142
<ListInfoProvider
143143
collectionConfig={createClientCollectionConfig({
144144
collection: collectionConfig,

packages/next/src/views/NotFound/index.tsx

Lines changed: 13 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { I18n } from '@payloadcms/translations'
22
import type { Metadata } from 'next'
33
import type { AdminViewComponent, SanitizedConfig } from 'payload'
44

5-
import { HydrateClientUser } from '@payloadcms/ui'
5+
import { HydrateAuthProvider } from '@payloadcms/ui'
66
import { formatAdminURL } from '@payloadcms/ui/shared'
77
import React, { Fragment } from 'react'
88

@@ -58,21 +58,18 @@ export const NotFoundPage = async ({
5858
})
5959

6060
return (
61-
<Fragment>
62-
<HydrateClientUser permissions={initPageResult.permissions} user={initPageResult.req.user} />
63-
<DefaultTemplate
64-
i18n={initPageResult.req.i18n}
65-
locale={initPageResult.locale}
66-
params={params}
67-
payload={initPageResult.req.payload}
68-
permissions={initPageResult.permissions}
69-
searchParams={searchParams}
70-
user={initPageResult.req.user}
71-
visibleEntities={initPageResult.visibleEntities}
72-
>
73-
<NotFoundClient />
74-
</DefaultTemplate>
75-
</Fragment>
61+
<DefaultTemplate
62+
i18n={initPageResult.req.i18n}
63+
locale={initPageResult.locale}
64+
params={params}
65+
payload={initPageResult.req.payload}
66+
permissions={initPageResult.permissions}
67+
searchParams={searchParams}
68+
user={initPageResult.req.user}
69+
visibleEntities={initPageResult.visibleEntities}
70+
>
71+
<NotFoundClient />
72+
</DefaultTemplate>
7673
)
7774
}
7875

packages/next/src/views/Version/RenderFieldsToDiff/fields/Relationship/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ const generateLabelFromValue = (
8282
}
8383
} else if (relatedDoc) {
8484
// Handle non-polymorphic `hasMany` relationships or fallback
85-
if (typeof relatedDoc.id !== 'undefined') {
85+
if (typeof relatedDoc?.id !== 'undefined') {
8686
valueToReturn = relatedDoc.id
8787
} else {
8888
valueToReturn = relatedDoc

packages/next/src/views/Version/index.tsx

Lines changed: 40 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
import type {
2-
CollectionPermission,
3-
Document,
4-
EditViewComponent,
5-
GlobalPermission,
6-
OptionObject,
7-
} from 'payload'
8-
91
import { notFound } from 'next/navigation.js'
2+
import {
3+
type CollectionPermission,
4+
type Document,
5+
type EditViewComponent,
6+
type GlobalPermission,
7+
type OptionObject,
8+
deepCopyObjectSimple,
9+
} from 'payload'
1010
import React from 'react'
1111

1212
import { getLatestVersion } from '../Versions/getLatestVersion.js'
@@ -55,8 +55,18 @@ export const VersionView: EditViewComponent = async (props) => {
5555
})
5656

5757
if (collectionConfig?.versions?.drafts) {
58-
latestDraftVersion = await getLatestVersion(payload, slug, 'draft', 'collection')
59-
latestPublishedVersion = await getLatestVersion(payload, slug, 'published', 'collection')
58+
latestDraftVersion = await getLatestVersion({
59+
slug,
60+
type: 'collection',
61+
payload,
62+
status: 'draft',
63+
})
64+
latestPublishedVersion = await getLatestVersion({
65+
slug,
66+
type: 'collection',
67+
payload,
68+
status: 'published',
69+
})
6070
}
6171
} catch (error) {
6272
return notFound()
@@ -80,8 +90,18 @@ export const VersionView: EditViewComponent = async (props) => {
8090
})
8191

8292
if (globalConfig?.versions?.drafts) {
83-
latestDraftVersion = await getLatestVersion(payload, slug, 'draft', 'global')
84-
latestPublishedVersion = await getLatestVersion(payload, slug, 'published', 'global')
93+
latestDraftVersion = await getLatestVersion({
94+
slug,
95+
type: 'global',
96+
payload,
97+
status: 'draft',
98+
})
99+
latestPublishedVersion = await getLatestVersion({
100+
slug,
101+
type: 'global',
102+
payload,
103+
status: 'published',
104+
})
85105
}
86106
} catch (error) {
87107
return notFound()
@@ -116,7 +136,14 @@ export const VersionView: EditViewComponent = async (props) => {
116136
return (
117137
<DefaultVersionView
118138
doc={doc}
119-
docPermissions={docPermissions}
139+
/**
140+
* After bumping the Next.js canary to 104, and React to 19.0.0-rc-06d0b89e-20240801" we have to deepCopy the permissions object (https://github.com/payloadcms/payload/pull/7541).
141+
* If both HydrateClientUser and RenderCustomComponent receive the same permissions object (same object reference), we get a
142+
* "TypeError: Cannot read properties of undefined (reading '$$typeof')" error
143+
*
144+
* // TODO: Revisit this in the future and figure out why this is happening. Might be a React/Next.js bug. We don't know why it happens, and a future React/Next version might unbreak this (keep an eye on this and remove deepCopyObjectSimple if that's the case)
145+
*/
146+
docPermissions={deepCopyObjectSimple(docPermissions)}
120147
initialComparisonDoc={latestVersion}
121148
latestDraftVersion={latestDraftVersion?.id}
122149
latestPublishedVersion={latestPublishedVersion?.id}

0 commit comments

Comments
 (0)