feat(next): export views, pass all props to custom dashboard view (#14094)

- Exports additional modules from `@payloadcms/next` like
`DashboardView`, which are useful for when you provide your own view but
want to render parts of the default view
- Passes all props to custom dashboard views, giving you access to
things like `req`
- Makes use of `select` API in `payload.find` call of the dashboard view
for improved performance
This commit is contained in:
Alessio Gravili
2025-10-06 14:42:47 -07:00
committed by GitHub
parent 9ceee8ea3c
commit abebd24dad
15 changed files with 98 additions and 24 deletions

View File

@@ -64,6 +64,16 @@
"import": "./src/exports/views.ts",
"types": "./src/exports/views.ts",
"default": "./src/exports/views.ts"
},
"./client": {
"import": "./src/exports/client.ts",
"types": "./src/exports/client.ts",
"default": "./src/exports/client.ts"
},
"./rsc": {
"import": "./src/exports/rsc.ts",
"types": "./src/exports/rsc.ts",
"default": "./src/exports/rsc.ts"
}
},
"main": "./src/index.js",
@@ -177,6 +187,16 @@
"import": "./dist/exports/views.js",
"types": "./dist/exports/views.d.ts",
"default": "./dist/exports/views.js"
},
"./client": {
"import": "./dist/exports/client.js",
"types": "./dist/exports/client.d.ts",
"default": "./dist/exports/client.js"
},
"./rsc": {
"import": "./dist/exports/rsc.js",
"types": "./dist/exports/rsc.d.ts",
"default": "./dist/exports/rsc.js"
}
},
"main": "./dist/index.js",

View File

@@ -13,6 +13,9 @@ import './index.scss'
const baseClass = `doc-header`
/**
* @internal
*/
export const DocumentHeader: React.FC<{
AfterHeader?: React.ReactNode
collectionConfig?: SanitizedCollectionConfig

View File

@@ -2,6 +2,9 @@
import { Hamburger, useNav } from '@payloadcms/ui'
import React from 'react'
/**
* @internal
*/
export const NavHamburger: React.FC<{
baseClass?: string
}> = ({ baseClass }) => {

View File

@@ -4,6 +4,9 @@ import React from 'react'
import './index.scss'
/**
* @internal
*/
export const NavWrapper: React.FC<{
baseClass?: string
children: React.ReactNode

View File

@@ -12,6 +12,9 @@ import React, { Fragment } from 'react'
const baseClass = 'nav'
/**
* @internal
*/
export const DefaultNavClient: React.FC<{
groups: ReturnType<typeof groupNavItems>
navPreferences: NavPreferences

View File

@@ -1,4 +1,4 @@
export { RootLayout } from './layouts/Root/index.js'
export { Dashboard as DashboardPage } from './views/Dashboard/index.js'
export { DashboardView } from './views/Dashboard/index.js'
export { LoginView } from './views/Login/index.js'
export { RootPage } from './views/Root/index.js'

View File

@@ -0,0 +1,5 @@
'use client'
export { DefaultNavClient } from '../elements/Nav/index.client.js'
export { NavHamburger } from '../elements/Nav/NavHamburger/index.js'
export { NavWrapper } from '../elements/Nav/NavWrapper/index.js'

View File

@@ -0,0 +1,3 @@
export { DocumentHeader } from '../elements/DocumentHeader/index.js'
export { Logo } from '../elements/Logo/index.js'
export { DefaultNav } from '../elements/Nav/index.js'

View File

@@ -1,3 +1,15 @@
export { AccountView } from '../views/Account/index.js'
export {
type DashboardViewClientProps,
type DashboardViewServerProps,
type DashboardViewServerPropsOnly,
DefaultDashboard,
} from '../views/Dashboard/Default/index.js'
export { DashboardView } from '../views/Dashboard/index.js'
export { ListView, renderListView, type RenderListViewArgs } from '../views/List/index.js'
export { LoginView } from '../views/Login/index.js'
export { NotFoundPage } from '../views/NotFound/index.js'
export { type GenerateViewMetadata, RootPage } from '../views/Root/index.js'
export { generatePageMetadata } from '../views/Root/metadata.js'

View File

@@ -16,7 +16,7 @@ import { EditView } from '../Edit/index.js'
import { AccountClient } from './index.client.js'
import { Settings } from './Settings/index.js'
export async function Account({ initPageResult, params, searchParams }: AdminViewServerProps) {
export async function AccountView({ initPageResult, params, searchParams }: AdminViewServerProps) {
const {
languageOptions,
locale,

View File

@@ -1,5 +1,5 @@
import type { groupNavItems } from '@payloadcms/ui/shared'
import type { ClientUser, Locale, ServerProps } from 'payload'
import type { AdminViewServerPropsOnly, ClientUser, Locale, ServerProps } from 'payload'
import { getTranslation } from '@payloadcms/translations'
import { Button, Card, Gutter, Locked } from '@payloadcms/ui'
@@ -29,7 +29,7 @@ export type DashboardViewServerPropsOnly = {
*/
Link?: React.ComponentType
navGroups?: ReturnType<typeof groupNavItems>
} & ServerProps
} & AdminViewServerPropsOnly
export type DashboardViewServerProps = DashboardViewClientProps & DashboardViewServerPropsOnly

View File

@@ -1,5 +1,5 @@
import type { EntityToGroup } from '@payloadcms/ui/shared'
import type { AdminViewServerProps } from 'payload'
import type { AdminViewServerProps, TypedUser } from 'payload'
import { HydrateAuthProvider, SetStepNav } from '@payloadcms/ui'
import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent'
@@ -10,7 +10,9 @@ import type { DashboardViewClientProps, DashboardViewServerPropsOnly } from './D
import { DefaultDashboard } from './Default/index.js'
export async function Dashboard({ initPageResult, params, searchParams }: AdminViewServerProps) {
const globalLockDurationDefault = 300
export async function DashboardView(props: AdminViewServerProps) {
const {
locale,
permissions,
@@ -22,8 +24,7 @@ export async function Dashboard({ initPageResult, params, searchParams }: AdminV
},
req,
visibleEntities,
} = initPageResult
} = props.initPageResult
const collections = config.collections.filter(
(collection) =>
permissions?.collections?.[collection.slug]?.read &&
@@ -36,7 +37,7 @@ export async function Dashboard({ initPageResult, params, searchParams }: AdminV
)
// Query locked global documents only if there are globals in the config
let globalData = []
let globalData: DashboardViewServerPropsOnly['globalData'] = []
if (config.globals.length > 0) {
const lockedDocuments = await payload.find({
@@ -45,6 +46,11 @@ export async function Dashboard({ initPageResult, params, searchParams }: AdminV
overrideAccess: false,
pagination: false,
req,
select: {
globalSlug: true,
updatedAt: true,
user: true,
},
where: {
globalSlug: {
exists: true,
@@ -54,11 +60,10 @@ export async function Dashboard({ initPageResult, params, searchParams }: AdminV
// Map over globals to include `lockDuration` and lock data for each global slug
globalData = config.globals.map((global) => {
const lockDurationDefault = 300
const lockDuration =
typeof global.lockDocuments === 'object'
? global.lockDocuments.duration
: lockDurationDefault
: globalLockDurationDefault
const lockedDoc = lockedDocuments.docs.find((doc) => doc.globalSlug === global.slug)
@@ -66,8 +71,8 @@ export async function Dashboard({ initPageResult, params, searchParams }: AdminV
slug: global.slug,
data: {
_isLocked: !!lockedDoc,
_lastEditedAt: lockedDoc?.updatedAt ?? null,
_userEditing: lockedDoc?.user?.value ?? null,
_lastEditedAt: (lockedDoc?.updatedAt as string) ?? null,
_userEditing: (lockedDoc?.user as { value?: TypedUser })?.value ?? null,
},
lockDuration,
}
@@ -109,14 +114,13 @@ export async function Dashboard({ initPageResult, params, searchParams }: AdminV
Fallback: DefaultDashboard,
importMap: payload.importMap,
serverProps: {
...props,
globalData,
i18n,
locale,
navGroups,
params,
payload,
permissions,
searchParams,
user,
visibleEntities,
} satisfies DashboardViewServerPropsOnly,

View File

@@ -422,7 +422,7 @@ export const renderDocument = async ({
}
}
export async function Document(props: AdminViewServerProps) {
export async function DocumentView(props: AdminViewServerProps) {
try {
const { Document: RenderedDocument } = await renderDocument(props)
return RenderedDocument

View File

@@ -7,6 +7,7 @@ import type {
ListViewClientProps,
ListViewServerPropsOnly,
PaginatedDocs,
PayloadComponent,
QueryPreset,
SanitizedCollectionPermission,
} from 'payload'
@@ -32,7 +33,17 @@ import { renderListViewSlots } from './renderListViewSlots.js'
import { resolveAllFilterOptions } from './resolveAllFilterOptions.js'
import { transformColumnsToSelect } from './transformColumnsToSelect.js'
type RenderListViewArgs = {
/**
* @internal
*/
export type RenderListViewArgs = {
/**
* Allows providing your own list view component. This will override the default list view component and
* the collection's configured list view component (if any).
*/
ComponentOverride?:
| PayloadComponent
| React.ComponentType<ListViewClientProps | (ListViewClientProps & ListViewServerPropsOnly)>
customCellProps?: Record<string, any>
disableBulkDelete?: boolean
disableBulkEdit?: boolean
@@ -40,7 +51,10 @@ type RenderListViewArgs = {
drawerSlug?: string
enableRowSelections: boolean
overrideEntityVisibility?: boolean
query: ListQuery
/**
* If not ListQuery is provided, `req.query` will be used.
*/
query?: ListQuery
redirectAfterDelete?: boolean
redirectAfterDuplicate?: boolean
/**
@@ -54,6 +68,8 @@ type RenderListViewArgs = {
* the list view on the server for both:
* - default list view
* - list view within drawers
*
* @internal
*/
export const renderListView = async (
args: RenderListViewArgs,
@@ -62,6 +78,7 @@ export const renderListView = async (
}> => {
const {
clientConfig,
ComponentOverride,
customCellProps,
disableBulkDelete,
disableBulkEdit,
@@ -385,7 +402,8 @@ export const renderListView = async (
Table,
viewType,
} satisfies ListViewClientProps,
Component: collectionConfig?.admin?.components?.views?.list?.Component,
Component:
ComponentOverride ?? collectionConfig?.admin?.components?.views?.list?.Component,
Fallback: DefaultListView,
importMap: payload.importMap,
serverProps,

View File

@@ -16,13 +16,13 @@ import type React from 'react'
import { parseDocumentID } from 'payload'
import { formatAdminURL, isNumber } from 'payload/shared'
import { Account } from '../Account/index.js'
import { AccountView } from '../Account/index.js'
import { BrowseByFolder } from '../BrowseByFolder/index.js'
import { CollectionFolderView } from '../CollectionFolders/index.js'
import { TrashView } from '../CollectionTrash/index.js'
import { CreateFirstUserView } from '../CreateFirstUser/index.js'
import { Dashboard } from '../Dashboard/index.js'
import { Document as DocumentView } from '../Document/index.js'
import { DashboardView } from '../Dashboard/index.js'
import { DocumentView } from '../Document/index.js'
import { forgotPasswordBaseClass, ForgotPasswordView } from '../ForgotPassword/index.js'
import { ListView } from '../List/index.js'
import { loginBaseClass, LoginView } from '../Login/index.js'
@@ -54,7 +54,7 @@ export type ViewFromConfig = {
}
const oneSegmentViews: OneSegmentViews = {
account: Account,
account: AccountView,
browseByFolder: BrowseByFolder,
createFirstUser: CreateFirstUserView,
forgot: ForgotPasswordView,
@@ -141,7 +141,7 @@ export const getRouteData = ({
case 0: {
if (currentRoute === adminRoute) {
ViewToRender = {
Component: Dashboard,
Component: DashboardView,
}
templateClassName = 'dashboard'
templateType = 'default'