Compare commits

...

1 Commits

Author SHA1 Message Date
Alessio Gravili
d27d09aa9e perf: page loading states 2025-02-11 17:07:22 -07:00
6 changed files with 294 additions and 49 deletions

View File

@@ -0,0 +1,30 @@
import type { ServerProps } from 'payload'
import React from 'react'
import './index.scss'
import { NavHamburger } from './NavHamburger/index.js'
import { NavWrapper } from './NavWrapper/index.js'
const baseClass = 'nav'
import { DefaultNavClient } from './index.client.js'
export type NavProps = ServerProps
export const DefaultNavLoading: React.FC = async () => {
return (
<NavWrapper baseClass={baseClass}>
<nav className={`${baseClass}__wrap`}>
<DefaultNavClient groups={[]} navPreferences={undefined} />
<div className={`${baseClass}__controls`}>{}</div>
</nav>
<div className={`${baseClass}__header`}>
<div className={`${baseClass}__header-content`}>
<NavHamburger baseClass={baseClass} />
</div>
</div>
</NavWrapper>
)
}

View File

@@ -0,0 +1,57 @@
import {
ActionsProvider,
AppHeader,
BulkUploadProvider,
EntityVisibilityProvider,
NavToggler,
} from '@payloadcms/ui'
import './index.scss'
import React from 'react'
import { DefaultNavLoading } from '../../elements/Nav/Loading.js'
import { NavHamburger } from './NavHamburger/index.js'
import { Wrapper } from './Wrapper/index.js'
const baseClass = 'template-default'
type DefaultTemplateLoaderProps = {
children?: React.ReactNode
className?: string
}
export const DefaultTemplateLoading: React.FC<DefaultTemplateLoaderProps> = ({
children,
className,
}) => {
return (
<EntityVisibilityProvider
visibleEntities={{
collections: [],
globals: [],
}}
>
<BulkUploadProvider>
<ActionsProvider Actions={{}}>
<div style={{ position: 'relative' }}>
<div className={`${baseClass}__nav-toggler-wrapper`} id="nav-toggler">
<div className={`${baseClass}__nav-toggler-container`} id="nav-toggler">
<NavToggler className={`${baseClass}__nav-toggler`}>
<NavHamburger />
</NavToggler>
</div>
</div>
<Wrapper baseClass={baseClass} className={className}>
<DefaultNavLoading />
<div className={`${baseClass}__wrap`}>
<AppHeader />
{children}
</div>
</Wrapper>
</div>
</ActionsProvider>
</BulkUploadProvider>
</EntityVisibilityProvider>
)
}

View File

@@ -6,7 +6,7 @@ import { formatAdminURL, isEditing as getIsEditing } from '@payloadcms/ui/shared
import { buildFormState } from '@payloadcms/ui/utilities/buildFormState'
import { notFound, redirect } from 'next/navigation.js'
import { logError } from 'payload'
import React from 'react'
import React, { Suspense } from 'react'
import type { GenerateEditViewMetadata } from './getMetaBySegment.js'
import type { ViewFromConfig } from './getViewsFromConfig.js'
@@ -381,10 +381,19 @@ export const renderDocument = async ({
}
}
export const Document: React.FC<AdminViewProps> = async (args) => {
const DocumentWithData: React.FC<AdminViewProps> = async (args) => {
const { Document: RenderedDocument } = await renderDocument(args)
return RenderedDocument
}
export const Document: React.FC<AdminViewProps> = (args) => {
try {
const { Document: RenderedDocument } = await renderDocument(args)
return RenderedDocument
return (
<Suspense
key={`document-view-${args?.initPageResult?.collectionConfig?.slug ?? args?.initPageResult?.globalConfig?.slug}`}
>
<DocumentWithData {...args} />
</Suspense>
)
} catch (error) {
if (error?.message === 'NEXT_REDIRECT') {
throw error

View File

@@ -12,7 +12,7 @@ import { renderFilters, renderTable, upsertPreferences } from '@payloadcms/ui/rs
import { formatAdminURL, mergeListSearchAndWhere } from '@payloadcms/ui/shared'
import { notFound } from 'next/navigation.js'
import { isNumber } from 'payload/shared'
import React, { Fragment } from 'react'
import React, { Fragment, Suspense } from 'react'
import { renderListViewSlots } from './renderListViewSlots.js'
import { resolveAllFilterOptions } from './resolveAllFilterOptions.js'
@@ -235,10 +235,20 @@ export const renderListView = async (
throw new Error('not-found')
}
export const ListView: React.FC<ListViewArgs> = async (args) => {
const ListViewWithData: React.FC<ListViewArgs> = async (args) => {
const { List: RenderedList } = await renderListView({ ...args, enableRowSelections: true })
return RenderedList
}
export const ListView: React.FC<ListViewArgs> = (args) => {
try {
const { List: RenderedList } = await renderListView({ ...args, enableRowSelections: true })
return RenderedList
return (
<Suspense
key={`list-view-${args?.initPageResult?.collectionConfig?.slug ?? args?.initPageResult?.globalConfig?.slug}`}
>
<ListViewWithData {...args} />
</Suspense>
)
} catch (error) {
if (error.message === 'not-found') {
notFound()

View File

@@ -67,7 +67,7 @@ type GetViewFromConfigArgs = {
segments: string[]
}
type GetViewFromConfigResult = {
export type GetViewFromConfigResult = {
DefaultView: ViewFromConfig
documentSubViewType?: DocumentSubViewTypes
initPageOptions: Parameters<typeof initPage>[0]

View File

@@ -1,17 +1,18 @@
import type { I18nClient } from '@payloadcms/translations'
import type { Metadata } from 'next'
import type { ImportMap, SanitizedConfig } from 'payload'
import type { ImportMap, InitPageResult, SanitizedConfig } from 'payload'
import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent'
import { formatAdminURL } from '@payloadcms/ui/shared'
import { getClientConfig } from '@payloadcms/ui/utilities/getClientConfig'
import { notFound, redirect } from 'next/navigation.js'
import React, { Fragment } from 'react'
import React, { Fragment, Suspense } from 'react'
import { DefaultTemplate } from '../../templates/Default/index.js'
import { DefaultTemplateLoading } from '../../templates/Default/Loading.js'
import { MinimalTemplate } from '../../templates/Minimal/index.js'
import { initPage } from '../../utilities/initPage/index.js'
import { getViewFromConfig } from './getViewFromConfig.js'
import { getViewFromConfig, type GetViewFromConfigResult } from './getViewFromConfig.js'
export { generatePageMetadata } from './meta.js'
@@ -58,6 +59,14 @@ export const RootPage = async ({
const searchParams = await searchParamsPromise
const getViewFromConfigResult = getViewFromConfig({
adminRoute,
config,
currentRoute,
importMap,
searchParams,
segments,
})
const {
DefaultView,
documentSubViewType,
@@ -66,17 +75,180 @@ export const RootPage = async ({
templateClassName,
templateType,
viewType,
} = getViewFromConfig({
} = getViewFromConfigResult
const rootPageProps: BaseRootPageWithDataProps = {
_createFirstUserRoute,
adminRoute,
config,
currentRoute,
getViewFromConfigResult,
importMap,
params,
searchParams,
segments,
})
userSlug,
}
return (
<Fragment>
{!templateType && (
<Suspense key={`main-view-${currentRoute}`}>
<RootPageWithData {...rootPageProps} />
</Suspense>
)}
{templateType === 'minimal' && (
<MinimalTemplate className={templateClassName}>
<Suspense key={`main-view-${currentRoute}`}>
<RootPageWithData {...rootPageProps} />
</Suspense>
</MinimalTemplate>
)}
{templateType === 'default' && (
<Suspense
fallback={
<RootPageWithDefaultTemplateFallback
currentRoute={currentRoute}
getViewFromConfigResult={{
DefaultView,
documentSubViewType,
initPageOptions,
serverProps,
templateClassName,
templateType,
viewType,
}}
params={params}
rootPageProps={rootPageProps}
searchParams={searchParams}
/>
}
key={`main-view-${currentRoute}`}
>
<RootPageWithDefaultTemplate
currentRoute={currentRoute}
getViewFromConfigResult={{
DefaultView,
documentSubViewType,
initPageOptions,
serverProps,
templateClassName,
templateType,
viewType,
}}
params={params}
rootPageProps={rootPageProps}
searchParams={searchParams}
/>
</Suspense>
)}
</Fragment>
)
}
const RootPageWithDefaultTemplateFallback: React.FC<{
currentRoute: string
getViewFromConfigResult: GetViewFromConfigResult
params: { [key: string]: string | string[] }
rootPageProps: Omit<BaseRootPageWithDataProps, 'initPageResult'>
searchParams: { [key: string]: string | string[] }
}> = (args) => {
const {
currentRoute,
getViewFromConfigResult: { documentSubViewType, initPageOptions, serverProps, viewType },
params,
rootPageProps,
searchParams,
} = args
return (
<DefaultTemplateLoading>
<Suspense key={`main-view-${currentRoute}`}>
<RootPageWithData {...rootPageProps} />
</Suspense>
</DefaultTemplateLoading>
)
}
const RootPageWithDefaultTemplate: React.FC<{
currentRoute: string
getViewFromConfigResult: GetViewFromConfigResult
params: { [key: string]: string | string[] }
rootPageProps: Omit<BaseRootPageWithDataProps, 'initPageResult'>
searchParams: { [key: string]: string | string[] }
}> = async (args) => {
const {
currentRoute,
getViewFromConfigResult: { documentSubViewType, initPageOptions, serverProps, viewType },
params,
rootPageProps,
searchParams,
} = args
const initPageResult = await initPage(initPageOptions)
return (
<DefaultTemplate
collectionSlug={initPageResult?.collectionConfig?.slug}
docID={initPageResult?.docID}
documentSubViewType={documentSubViewType}
globalSlug={initPageResult?.globalConfig?.slug}
i18n={initPageResult?.req.i18n}
locale={initPageResult?.locale}
params={params}
payload={initPageResult?.req.payload}
permissions={initPageResult?.permissions}
searchParams={searchParams}
user={initPageResult?.req.user}
viewActions={serverProps.viewActions}
viewType={viewType}
visibleEntities={{
// The reason we are not passing in initPageResult.visibleEntities directly is due to a "Cannot assign to read only property of object '#<Object>" error introduced in React 19
// which this caused as soon as initPageResult.visibleEntities is passed in
collections: initPageResult?.visibleEntities?.collections,
globals: initPageResult?.visibleEntities?.globals,
}}
>
<Suspense key={`main-view-${currentRoute}`}>
<RootPageWithData {...rootPageProps} />
</Suspense>
</DefaultTemplate>
)
}
type BaseRootPageWithDataProps = {
_createFirstUserRoute: string
adminRoute: string
config: SanitizedConfig
currentRoute: string
getViewFromConfigResult: GetViewFromConfigResult
importMap: ImportMap
initPageResult?: InitPageResult | Promise<InitPageResult>
params: { [key: string]: string | string[] }
searchParams: { [key: string]: string | string[] }
userSlug: string
}
const RootPageWithData: React.FC<BaseRootPageWithDataProps> = async (args) => {
const {
_createFirstUserRoute,
adminRoute,
config,
currentRoute,
getViewFromConfigResult: {
DefaultView,
documentSubViewType,
initPageOptions,
serverProps,
viewType,
},
importMap,
params,
searchParams,
userSlug,
} = args
const initPageResult = (await args?.initPageResult) ?? (await initPage(initPageOptions))
const dbHasUser =
initPageResult.req.user ||
(await initPageResult?.req.payload.db
@@ -122,7 +294,6 @@ export const RootPage = async ({
if (!DefaultView?.Component && !DefaultView?.payloadComponent && !dbHasUser) {
redirect(adminRoute)
}
const clientConfig = getClientConfig({
config,
i18n: initPageResult?.req.i18n,
@@ -147,37 +318,5 @@ export const RootPage = async ({
},
})
return (
<Fragment>
{!templateType && <Fragment>{RenderedView}</Fragment>}
{templateType === 'minimal' && (
<MinimalTemplate className={templateClassName}>{RenderedView}</MinimalTemplate>
)}
{templateType === 'default' && (
<DefaultTemplate
collectionSlug={initPageResult?.collectionConfig?.slug}
docID={initPageResult?.docID}
documentSubViewType={documentSubViewType}
globalSlug={initPageResult?.globalConfig?.slug}
i18n={initPageResult?.req.i18n}
locale={initPageResult?.locale}
params={params}
payload={initPageResult?.req.payload}
permissions={initPageResult?.permissions}
searchParams={searchParams}
user={initPageResult?.req.user}
viewActions={serverProps.viewActions}
viewType={viewType}
visibleEntities={{
// The reason we are not passing in initPageResult.visibleEntities directly is due to a "Cannot assign to read only property of object '#<Object>" error introduced in React 19
// which this caused as soon as initPageResult.visibleEntities is passed in
collections: initPageResult?.visibleEntities?.collections,
globals: initPageResult?.visibleEntities?.globals,
}}
>
{RenderedView}
</DefaultTemplate>
)}
</Fragment>
)
return RenderedView
}