fix: move form data retrieval logic to client (#5411)

* fix: only execute onChange if form modified

* fix: move document loading logic from RSC to DocumentInfoProvider

* fix: make it work for globals

* chore: remove unnecessary diffs

---------

Co-authored-by: Jarrod Flesch <jarrodmflesch@gmail.com>
This commit is contained in:
Alessio Gravili
2024-03-21 16:38:12 -04:00
committed by GitHub
parent 11cbb774bc
commit 35b0d213a6
6 changed files with 135 additions and 35 deletions

View File

@@ -67,10 +67,11 @@ export const Document: React.FC<AdminViewProps> = async ({
let DefaultView: EditViewComponent let DefaultView: EditViewComponent
let ErrorView: AdminViewComponent = NotFoundView let ErrorView: AdminViewComponent = NotFoundView
/**
let data: DocumentType let data: DocumentType
let docPermissions: DocumentPermissions
let preferencesKey: string let preferencesKey: string
let fields: Field[] let fields: Field[] **/
let docPermissions: DocumentPermissions
let hasSavePermission: boolean let hasSavePermission: boolean
let apiURL: string let apiURL: string
let action: string let action: string
@@ -88,7 +89,8 @@ export const Document: React.FC<AdminViewProps> = async ({
return <NotFoundClient /> return <NotFoundClient />
} }
fields = collectionConfig.fields /**
fields = collectionConfig.fields **/
action = `${serverURL}${apiRoute}/${collectionSlug}${isEditing ? `/${id}` : ''}` action = `${serverURL}${apiRoute}/${collectionSlug}${isEditing ? `/${id}` : ''}`
hasSavePermission = hasSavePermission =
@@ -120,6 +122,7 @@ export const Document: React.FC<AdminViewProps> = async ({
return <ErrorView initPageResult={initPageResult} searchParams={searchParams} /> return <ErrorView initPageResult={initPageResult} searchParams={searchParams} />
} }
/**
if (id) { if (id) {
try { try {
data = await payload.findByID({ data = await payload.findByID({
@@ -140,13 +143,15 @@ export const Document: React.FC<AdminViewProps> = async ({
preferencesKey = `collection-${collectionSlug}-${id}` preferencesKey = `collection-${collectionSlug}-${id}`
} }
**/
} }
if (globalConfig) { if (globalConfig) {
docPermissions = permissions?.globals?.[globalSlug] docPermissions = permissions?.globals?.[globalSlug]
fields = globalConfig.fields
hasSavePermission = isEditing && docPermissions?.update?.permission hasSavePermission = isEditing && docPermissions?.update?.permission
action = `${serverURL}${apiRoute}/globals/${globalSlug}` action = `${serverURL}${apiRoute}/globals/${globalSlug}`
/**
fields = globalConfig.fields **/
apiURL = `${serverURL}${apiRoute}/${globalSlug}?locale=${locale.code}${ apiURL = `${serverURL}${apiRoute}/${globalSlug}?locale=${locale.code}${
globalConfig.versions?.drafts ? '&draft=true' : '' globalConfig.versions?.drafts ? '&draft=true' : ''
@@ -172,6 +177,7 @@ export const Document: React.FC<AdminViewProps> = async ({
return <ErrorView initPageResult={initPageResult} searchParams={searchParams} /> return <ErrorView initPageResult={initPageResult} searchParams={searchParams} />
} }
/**
try { try {
data = await payload.findGlobal({ data = await payload.findGlobal({
slug: globalSlug, slug: globalSlug,
@@ -188,29 +194,31 @@ export const Document: React.FC<AdminViewProps> = async ({
return <NotFoundClient /> return <NotFoundClient />
} }
preferencesKey = `global-${globalSlug}` preferencesKey = `global-${globalSlug}` **/
} }
} }
const { docs: [{ value: docPreferences } = { value: null }] = [] } = (await payload.find({ /**
collection: 'payload-preferences', const { docs: [{ value: docPreferences } = { value: null }] = [] } = (await payload.find({
depth: 0, collection: 'payload-preferences',
limit: 1, depth: 0,
where: { limit: 1,
key: { where: {
equals: preferencesKey, key: {
}, equals: preferencesKey,
}, },
})) as any as { docs: { value: DocumentPreferences }[] } // eslint-disable-line @typescript-eslint/no-explicit-any },
})) as any as { docs: { value: DocumentPreferences }[] } // eslint-disable-line @typescript-eslint/no-explicit-any
const initialState = await buildStateFromSchema({ const initialState = await buildStateFromSchema({
id, id,
data: data || {}, data: data || {},
fieldSchema: formatFields(fields, isEditing), fieldSchema: formatFields(fields, isEditing),
operation: isEditing ? 'update' : 'create', operation: isEditing ? 'update' : 'create',
preferences: docPreferences, preferences: docPreferences,
req, req,
}) })
*/
const viewComponentProps: ServerSideEditViewProps = { const viewComponentProps: ServerSideEditViewProps = {
initPageResult, initPageResult,
@@ -228,9 +236,9 @@ export const Document: React.FC<AdminViewProps> = async ({
globalSlug={globalConfig?.slug} globalSlug={globalConfig?.slug}
hasSavePermission={hasSavePermission} hasSavePermission={hasSavePermission}
id={id} id={id}
/**
initialData={data} initialData={data}
initialState={initialState} initialState={initialState}
isEditing={isEditing}
title={formatDocTitle({ title={formatDocTitle({
collectionConfig, collectionConfig,
data, data,
@@ -238,7 +246,8 @@ export const Document: React.FC<AdminViewProps> = async ({
fallback: id?.toString(), fallback: id?.toString(),
globalConfig, globalConfig,
i18n, i18n,
})} })} **/
isEditing={isEditing}
> >
{!ViewOverride && ( {!ViewOverride && (
<DocumentHeader <DocumentHeader

View File

@@ -546,7 +546,7 @@ export const Form: React.FC<FormProps> = (props) => {
} }
} }
void executeOnChange() // eslint-disable-line @typescript-eslint/no-floating-promises if (modified) void executeOnChange()
}, },
150, 150,
[fields, dispatchFields, onChange], [fields, dispatchFields, onChange],

View File

@@ -1,10 +1,14 @@
'use client' 'use client'
import type { PaginatedDocs, TypeWithVersion } from 'payload/database' import type { PaginatedDocs, TypeWithVersion } from 'payload/database'
import type { TypeWithTimestamps } from 'payload/types' import type { FormState, TypeWithTimestamps } from 'payload/types'
import type { DocumentPermissions, DocumentPreferences, TypeWithID, Where } from 'payload/types' import type { DocumentPermissions, DocumentPreferences, TypeWithID, Where } from 'payload/types'
import { LoadingOverlay } from '@payloadcms/ui/elements/Loading'
import usePayloadAPI from '@payloadcms/ui/hooks/usePayloadAPI'
import { formatDocTitle } from '@payloadcms/ui/utilities/formatDocTitle'
import { getFormState } from '@payloadcms/ui/utilities/getFormState'
import qs from 'qs' import qs from 'qs'
import React, { createContext, useCallback, useContext, useEffect, useState } from 'react' import React, { createContext, useCallback, useContext, useEffect, useRef, useState } from 'react'
import type { DocumentInfoContext, DocumentInfoProps } from './types.js' import type { DocumentInfoContext, DocumentInfoProps } from './types.js'
@@ -24,12 +28,15 @@ export const DocumentInfoProvider: React.FC<
DocumentInfoProps & { DocumentInfoProps & {
children: React.ReactNode children: React.ReactNode
} }
> = ({ children, ...props }) => { > = ({ children, initialState: initialStateFromProps, title: titleFromProps, ...props }) => {
const [documentTitle, setDocumentTitle] = useState(props.title) const [documentTitle, setDocumentTitle] = useState(titleFromProps)
const [initialState, setInitialState] = useState<FormState>(initialStateFromProps)
const { id, collectionSlug, globalSlug } = props const { id, collectionSlug, globalSlug } = props
const { const {
admin: { dateFormat },
collections, collections,
globals, globals,
routes: { api }, routes: { api },
@@ -73,6 +80,66 @@ export const DocumentInfoProvider: React.FC<
} }
} }
/**
*
*
* Fetching state from Server on the Client. Should be moved back to Document/index.tsx once next allows us to disable the router cache
*
*
*/
const hasInitializedState = useRef(!!initialStateFromProps)
// no need to an additional requests when creating new documents
const isEditing = Boolean(id)
const [{ data, isError, isLoading: isLoadingDocument }] = usePayloadAPI(
!hasInitializedState.current
? `${baseURL}/${globalSlug ? 'globals/' : ''}${slug}${collectionSlug ? `/${id}` : ''}`
: null,
{ initialParams: { depth: 0, draft: 'true', 'fallback-locale': 'null' } },
)
useEffect(() => {
if (!hasInitializedState.current && data) {
const getInitialState = async () => {
let docPreferences: DocumentPreferences = { fields: {} }
if (id) {
docPreferences = await getPreference(
`${id ? 'collection' : 'global'}-${collectionSlug}-${id}`,
)
}
const result = await getFormState({
apiRoute: api,
body: {
id,
collectionSlug,
data: data || {},
docPreferences,
globalSlug,
operation: isEditing ? 'update' : 'create',
schemaPath: collectionSlug || globalSlug,
},
serverURL,
})
setInitialState(result)
hasInitializedState.current = true
}
void getInitialState()
}
}, [api, data, isEditing, collectionSlug, serverURL, id, getPreference, globalSlug])
/**
*
*
* Fetching state from Server on the Client DONE
*
*
*/
const getVersions = useCallback(async () => { const getVersions = useCallback(async () => {
let versionFetchURL let versionFetchURL
let publishedFetchURL let publishedFetchURL
@@ -264,8 +331,21 @@ export const DocumentInfoProvider: React.FC<
}, [getVersions]) }, [getVersions])
useEffect(() => { useEffect(() => {
setDocumentTitle(props.title) if (titleFromProps) {
}, [props.title]) setDocumentTitle(titleFromProps)
} else {
setDocumentTitle(
formatDocTitle({
collectionConfig,
data,
dateFormat,
fallback: id?.toString(),
globalConfig,
i18n,
}),
)
}
}, [collectionConfig, data, dateFormat, i18n, titleFromProps, id, globalConfig])
useEffect(() => { useEffect(() => {
const loadDocPermissions = async () => { const loadDocPermissions = async () => {
@@ -292,6 +372,12 @@ export const DocumentInfoProvider: React.FC<
globalSlug, globalSlug,
]) ])
if (!initialState || isLoadingDocument) {
return <LoadingOverlay />
}
if (isError) return null
const value: DocumentInfoContext = { const value: DocumentInfoContext = {
...props, ...props,
docConfig, docConfig,
@@ -300,6 +386,7 @@ export const DocumentInfoProvider: React.FC<
getDocPreferences, getDocPreferences,
getVersions, getVersions,
hasSavePermission, hasSavePermission,
initialState,
onSave: props.onSave, onSave: props.onSave,
publishedDoc, publishedDoc,
setDocFieldPreferences, setDocFieldPreferences,

View File

@@ -209,6 +209,10 @@ export default buildConfigWithDefaults({
type: 'relationship', type: 'relationship',
relationTo: 'users', relationTo: 'users',
}, },
{
name: 'text',
type: 'text',
},
], ],
}, },
], ],

View File

@@ -42,7 +42,7 @@ describe('auth', () => {
page = await context.newPage() page = await context.newPage()
initPageConsoleErrorCatch(page) initPageConsoleErrorCatch(page)
//await delayNetwork({ context, page, delay: 'Fast 3G' }) //await delayNetwork({ context, page, delay: 'Slow 4G' })
await login({ await login({
page, page,

View File

@@ -25,12 +25,12 @@ const networkConditions = {
'Fast 3G': { 'Fast 3G': {
download: ((1.6 * 1000 * 1000) / 8) * 0.9, download: ((1.6 * 1000 * 1000) / 8) * 0.9,
upload: ((750 * 1000) / 8) * 0.9, upload: ((750 * 1000) / 8) * 0.9,
latency: 150 * 3.75, latency: 1000,
}, },
'Slow 4G': { 'Slow 4G': {
download: ((4 * 1000 * 1000) / 8) * 0.8, download: ((4 * 1000 * 1000) / 8) * 0.8,
upload: ((3 * 1000 * 1000) / 8) * 0.8, upload: ((3 * 1000 * 1000) / 8) * 0.8,
latency: 20 * 3.75, latency: 1000,
}, },
} }