Skip to content

Commit f6bdc0a

Browse files
authored
feat(next): initializes nav group prefs on the server and consolidates records (#9145)
1 parent a8e3095 commit f6bdc0a

File tree

11 files changed

+109
-82
lines changed

11 files changed

+109
-82
lines changed
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import type { NavPreferences, Payload, User } from 'payload'
2+
3+
import { cache } from 'react'
4+
5+
export const getNavPrefs = cache(
6+
async ({ payload, user }: { payload: Payload; user: User }): Promise<NavPreferences> =>
7+
user
8+
? await payload
9+
.find({
10+
collection: 'payload-preferences',
11+
depth: 0,
12+
limit: 1,
13+
user,
14+
where: {
15+
and: [
16+
{
17+
key: {
18+
equals: 'nav',
19+
},
20+
},
21+
{
22+
'user.relationTo': {
23+
equals: user.collection,
24+
},
25+
},
26+
{
27+
'user.value': {
28+
equals: user.id,
29+
},
30+
},
31+
],
32+
},
33+
})
34+
?.then((res) => res?.docs?.[0]?.value)
35+
: null,
36+
)

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
'use client'
22

33
import type { groupNavItems } from '@payloadcms/ui/shared'
4+
import type { NavPreferences } from 'payload'
45

56
import { getTranslation } from '@payloadcms/translations'
67
import { NavGroup, useConfig, useNav, useTranslation } from '@payloadcms/ui'
@@ -13,7 +14,8 @@ const baseClass = 'nav'
1314

1415
export const DefaultNavClient: React.FC<{
1516
groups: ReturnType<typeof groupNavItems>
16-
}> = ({ groups }) => {
17+
navPreferences: NavPreferences
18+
}> = ({ groups, navPreferences }) => {
1719
const pathname = usePathname()
1820

1921
const {
@@ -29,7 +31,7 @@ export const DefaultNavClient: React.FC<{
2931
<Fragment>
3032
{groups.map(({ entities, label }, key) => {
3133
return (
32-
<NavGroup key={key} label={label}>
34+
<NavGroup isOpen={navPreferences?.groups?.[label]?.open} key={key} label={label}>
3335
{entities.map(({ slug, type, label }, i) => {
3436
let href: string
3537
let id: string

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,12 @@ import { NavWrapper } from './NavWrapper/index.js'
1212

1313
const baseClass = 'nav'
1414

15+
import { getNavPrefs } from './getNavPrefs.js'
1516
import { DefaultNavClient } from './index.client.js'
1617

1718
export type NavProps = ServerProps
1819

19-
export const DefaultNav: React.FC<NavProps> = (props) => {
20+
export const DefaultNav: React.FC<NavProps> = async (props) => {
2021
const { i18n, locale, params, payload, permissions, searchParams, user, visibleEntities } = props
2122

2223
if (!payload?.config) {
@@ -56,6 +57,8 @@ export const DefaultNav: React.FC<NavProps> = (props) => {
5657
i18n,
5758
)
5859

60+
const navPreferences = await getNavPrefs({ payload, user })
61+
5962
return (
6063
<NavWrapper baseClass={baseClass}>
6164
<nav className={`${baseClass}__wrap`}>
@@ -72,7 +75,7 @@ export const DefaultNav: React.FC<NavProps> = (props) => {
7275
user,
7376
}}
7477
/>
75-
<DefaultNavClient groups={groups} />
78+
<DefaultNavClient groups={groups} navPreferences={navPreferences} />
7679
<RenderServerComponent
7780
Component={afterNavLinks}
7881
importMap={payload.importMap}

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

Lines changed: 3 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { headers as getHeaders, cookies as nextCookies } from 'next/headers.js'
88
import { checkDependencies, parseCookies } from 'payload'
99
import React from 'react'
1010

11+
import { getNavPrefs } from '../../elements/Nav/getNavPrefs.js'
1112
import { getClientConfig } from '../../utilities/getClientConfig.js'
1213
import { getPayloadHMR } from '../../utilities/getPayloadHMR.js'
1314
import { getRequestLanguage } from '../../utilities/getRequestLanguage.js'
@@ -131,38 +132,7 @@ export const RootLayout = async ({
131132
})
132133
}
133134

134-
const navPreferences = user
135-
? (
136-
await payload.find({
137-
collection: 'payload-preferences',
138-
depth: 0,
139-
limit: 1,
140-
req,
141-
user,
142-
where: {
143-
and: [
144-
{
145-
key: {
146-
equals: 'nav',
147-
},
148-
},
149-
{
150-
'user.relationTo': {
151-
equals: user.collection,
152-
},
153-
},
154-
{
155-
'user.value': {
156-
equals: user.id,
157-
},
158-
},
159-
],
160-
},
161-
})
162-
)?.docs?.[0]
163-
: null
164-
165-
const isNavOpen = navPreferences?.value?.open ?? true
135+
const navPrefs = await getNavPrefs({ payload, user })
166136

167137
const clientConfig = await getClientConfig({
168138
config,
@@ -176,7 +146,7 @@ export const RootLayout = async ({
176146
config={clientConfig}
177147
dateFNSKey={i18n.dateFNSKey}
178148
fallbackLang={config.i18n.fallbackLanguage}
179-
isNavOpen={isNavOpen}
149+
isNavOpen={navPrefs?.open}
180150
languageCode={languageCode}
181151
languageOptions={languageOptions}
182152
permissions={permissions}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
export type NavPreferences = {
2+
groups: NavGroupPreferences
3+
open: boolean
4+
}
5+
6+
export type NavGroupPreferences = {
7+
[key: string]: {
8+
open: boolean
9+
}
10+
}

packages/payload/src/admin/types.ts

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import type {
1919
export type { DefaultCellComponentProps } from './elements/Cell.js'
2020
export type { ConditionalDateProps } from './elements/DatePicker.js'
2121
export type { DayPickerProps, SharedProps, TimePickerProps } from './elements/DatePicker.js'
22+
export type { NavGroupPreferences, NavPreferences } from './elements/Nav.js'
2223
export type { CustomPreviewButton } from './elements/PreviewButton.js'
2324
export type { CustomPublishButton } from './elements/PublishButton.js'
2425
export type { CustomSaveButton } from './elements/SaveButton.js'
@@ -29,6 +30,7 @@ export type {
2930
DocumentTabConfig,
3031
DocumentTabProps,
3132
} from './elements/Tab.js'
33+
3234
export type { CustomUpload } from './elements/Upload.js'
3335

3436
export type {
@@ -333,16 +335,6 @@ export type {
333335
GenericErrorProps,
334336
} from './forms/Error.js'
335337

336-
export type {
337-
ClientComponentProps,
338-
ClientFieldBase,
339-
ClientFieldWithOptionalType,
340-
FieldClientComponent,
341-
FieldServerComponent,
342-
ServerComponentProps,
343-
ServerFieldBase,
344-
} from './forms/Field.js'
345-
346338
export type {
347339
BuildFormStateArgs,
348340
Data,
@@ -354,6 +346,16 @@ export type {
354346
Row,
355347
}
356348

349+
export type {
350+
ClientComponentProps,
351+
ClientFieldBase,
352+
ClientFieldWithOptionalType,
353+
FieldClientComponent,
354+
FieldServerComponent,
355+
ServerComponentProps,
356+
ServerFieldBase,
357+
} from './forms/Field.js'
358+
357359
export type {
358360
FieldLabelClientComponent,
359361
FieldLabelClientProps,
@@ -377,8 +379,6 @@ export type {
377379
ServerFunctionHandler,
378380
} from './functions/index.js'
379381

380-
export type { LanguageOptions } from './LanguageOptions.js'
381-
382382
export type MappedServerComponent<TComponentClientProps extends JsonObject = JsonObject> = {
383383
Component?: React.ComponentType<TComponentClientProps>
384384
props?: Partial<any>
@@ -458,6 +458,8 @@ export type DocumentSlots = {
458458
Upload?: React.ReactNode
459459
}
460460

461+
export type { LanguageOptions } from './LanguageOptions.js'
462+
461463
export type { RichTextAdapter, RichTextAdapterProvider, RichTextHooks } from './RichText.js'
462464

463465
export type {

packages/ui/src/elements/Nav/NavToggler/index.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,13 @@ export const NavToggler: React.FC<{
4040
// only when the user explicitly toggles the nav on desktop do we want to set the preference
4141
// this is because the js may open or close the nav based on the window size, routing, etc
4242
if (!largeBreak) {
43-
await setPreference('nav', {
44-
open: !navOpen,
45-
})
43+
await setPreference(
44+
'nav',
45+
{
46+
open: !navOpen,
47+
},
48+
true,
49+
)
4650
}
4751
}}
4852
tabIndex={tabIndex}

packages/ui/src/elements/NavGroup/index.tsx

Lines changed: 21 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
'use client'
2-
import React, { useEffect, useState } from 'react'
2+
import type { NavPreferences } from 'payload'
3+
4+
import React, { useState } from 'react'
35
import AnimateHeightImport from 'react-animate-height'
46

57
import { ChevronIcon } from '../../icons/Chevron/index.js'
68
import { usePreferences } from '../../providers/Preferences/index.js'
7-
import { useNav } from '../Nav/context.js'
89
import './index.scss'
10+
import { useNav } from '../Nav/context.js'
911

1012
const AnimateHeight = (AnimateHeightImport.default ||
1113
AnimateHeightImport) as typeof AnimateHeightImport.default
@@ -14,37 +16,33 @@ const baseClass = 'nav-group'
1416

1517
type Props = {
1618
children: React.ReactNode
19+
isOpen?: boolean
1720
label: string
1821
}
1922

20-
export const NavGroup: React.FC<Props> = ({ children, label }) => {
21-
const [collapsed, setCollapsed] = useState(true)
22-
const [animate, setAnimate] = useState(false)
23-
const { getPreference, setPreference } = usePreferences()
24-
const { navOpen } = useNav()
23+
const preferencesKey = 'nav'
2524

26-
const preferencesKey = `collapsed-${label}-groups`
25+
export const NavGroup: React.FC<Props> = ({ children, isOpen: isOpenFromProps, label }) => {
26+
const [collapsed, setCollapsed] = useState(
27+
typeof isOpenFromProps !== 'undefined' ? !isOpenFromProps : false,
28+
)
2729

28-
useEffect(() => {
29-
if (label) {
30-
const setCollapsedFromPreferences = async () => {
31-
const preferences = (await getPreference(preferencesKey)) || []
32-
setCollapsed(preferences.indexOf(label) !== -1)
33-
}
34-
void setCollapsedFromPreferences()
35-
}
36-
}, [getPreference, label, preferencesKey])
30+
const [animate, setAnimate] = useState(false)
31+
const { setPreference } = usePreferences()
32+
const { navOpen } = useNav()
3733

3834
if (label) {
39-
const toggleCollapsed = async () => {
35+
const toggleCollapsed = () => {
4036
setAnimate(true)
41-
let preferences: string[] = (await getPreference(preferencesKey)) || []
42-
if (collapsed) {
43-
preferences = preferences.filter((preference) => label !== preference)
37+
const newGroupPrefs: NavPreferences['groups'] = {}
38+
39+
if (!newGroupPrefs?.[label]) {
40+
newGroupPrefs[label] = { open: Boolean(collapsed) }
4441
} else {
45-
preferences.push(label)
42+
newGroupPrefs[label].open = Boolean(collapsed)
4643
}
47-
void setPreference(preferencesKey, preferences)
44+
45+
void setPreference(preferencesKey, { groups: newGroupPrefs }, true)
4846
setCollapsed(!collapsed)
4947
}
5048

packages/ui/src/exports/shared/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ export { formatDocTitle } from '../../utilities/formatDocTitle.js'
1616
export {
1717
type EntityToGroup,
1818
EntityType,
19-
type Group,
2019
groupNavItems,
20+
type NavGroupType,
2121
} from '../../utilities/groupNavItems.js'
2222
export { handleBackToDashboard } from '../../utilities/handleBackToDashboard.js'
2323
export { handleGoBack } from '../../utilities/handleGoBack.js'

packages/ui/src/providers/Preferences/index.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import React, { createContext, useCallback, useContext, useEffect, useRef } from
44

55
import { useTranslation } from '../../providers/Translation/index.js'
66
import { requests } from '../../utilities/api.js'
7+
import { deepMergeSimple } from '../../utilities/deepMerge.js'
78
import { useAuth } from '../Auth/index.js'
89
import { useConfig } from '../Config/index.js'
910

@@ -90,14 +91,15 @@ export const PreferencesProvider: React.FC<{ children?: React.ReactNode }> = ({
9091

9192
let newValue = value
9293
const currentPreference = await getPreference(key)
94+
9395
// handle value objects where multiple values can be set under one key
9496
if (
9597
typeof value === 'object' &&
9698
typeof currentPreference === 'object' &&
9799
typeof newValue === 'object'
98100
) {
99101
// merge the value with any existing preference for the key
100-
newValue = { ...(currentPreference || {}), ...value }
102+
newValue = deepMergeSimple(currentPreference, newValue)
101103

102104
if (dequal(newValue, currentPreference)) {
103105
return

0 commit comments

Comments
 (0)