fix(next): autosave document rendering (#9364)

Closes #9242 and #9365. Autosave-enabled documents rendered within a
drawer were not being properly handled. This was causing multiple draft
documents to be created upon opening the drawer, as well as an empty
document returned from the server function, etc.
This commit is contained in:
Jacob Fletcher
2024-11-19 19:01:54 -05:00
committed by GitHub
parent 4030e212f5
commit 9e85be0006
7 changed files with 82 additions and 36 deletions

View File

@@ -49,7 +49,7 @@ export const renderDocument = async ({
}> => { }> => {
const { const {
collectionConfig, collectionConfig,
docID: id, docID: idFromArgs,
globalConfig, globalConfig,
locale, locale,
permissions, permissions,
@@ -72,7 +72,7 @@ export const renderDocument = async ({
const segments = Array.isArray(params?.segments) ? params.segments : [] const segments = Array.isArray(params?.segments) ? params.segments : []
const collectionSlug = collectionConfig?.slug || undefined const collectionSlug = collectionConfig?.slug || undefined
const globalSlug = globalConfig?.slug || undefined const globalSlug = globalConfig?.slug || undefined
const isEditing = getIsEditing({ id, collectionSlug, globalSlug }) let isEditing = getIsEditing({ id: idFromArgs, collectionSlug, globalSlug })
let RootViewOverride: PayloadComponent let RootViewOverride: PayloadComponent
let CustomView: ViewFromConfig<ServerSideEditViewProps> let CustomView: ViewFromConfig<ServerSideEditViewProps>
@@ -82,10 +82,10 @@ export const renderDocument = async ({
let apiURL: string let apiURL: string
// Fetch the doc required for the view // Fetch the doc required for the view
const doc = let doc =
initialData || initialData ||
(await getDocumentData({ (await getDocumentData({
id, id: idFromArgs,
collectionSlug, collectionSlug,
globalSlug, globalSlug,
locale, locale,
@@ -104,7 +104,7 @@ export const renderDocument = async ({
] = await Promise.all([ ] = await Promise.all([
// Get document preferences // Get document preferences
getDocPreferences({ getDocPreferences({
id, id: idFromArgs,
collectionSlug, collectionSlug,
globalSlug, globalSlug,
payload, payload,
@@ -113,7 +113,7 @@ export const renderDocument = async ({
// Get permissions // Get permissions
getDocumentPermissions({ getDocumentPermissions({
id, id: idFromArgs,
collectionConfig, collectionConfig,
data: doc, data: doc,
globalConfig, globalConfig,
@@ -122,7 +122,7 @@ export const renderDocument = async ({
// Fetch document lock state // Fetch document lock state
getIsLocked({ getIsLocked({
id, id: idFromArgs,
collectionConfig, collectionConfig,
globalConfig, globalConfig,
isEditing, isEditing,
@@ -135,7 +135,7 @@ export const renderDocument = async ({
{ state: formState }, { state: formState },
] = await Promise.all([ ] = await Promise.all([
getVersions({ getVersions({
id, id: idFromArgs,
collectionConfig, collectionConfig,
docPermissions, docPermissions,
globalConfig, globalConfig,
@@ -144,7 +144,7 @@ export const renderDocument = async ({
user, user,
}), }),
buildFormState({ buildFormState({
id, id: idFromArgs,
collectionSlug, collectionSlug,
data: doc, data: doc,
docPermissions, docPermissions,
@@ -152,7 +152,7 @@ export const renderDocument = async ({
fallbackLocale: false, fallbackLocale: false,
globalSlug, globalSlug,
locale: locale?.code, locale: locale?.code,
operation: (collectionSlug && id) || globalSlug ? 'update' : 'create', operation: (collectionSlug && idFromArgs) || globalSlug ? 'update' : 'create',
renderAllFields: true, renderAllFields: true,
req, req,
schemaPath: collectionSlug || globalSlug, schemaPath: collectionSlug || globalSlug,
@@ -187,7 +187,7 @@ export const renderDocument = async ({
const apiQueryParams = `?${params.toString()}` const apiQueryParams = `?${params.toString()}`
apiURL = `${serverURL}${apiRoute}/${collectionSlug}/${id}${apiQueryParams}` apiURL = `${serverURL}${apiRoute}/${collectionSlug}/${idFromArgs}${apiQueryParams}`
RootViewOverride = RootViewOverride =
collectionConfig?.admin?.components?.views?.edit?.root && collectionConfig?.admin?.components?.views?.edit?.root &&
@@ -274,8 +274,10 @@ export const renderDocument = async ({
const validateDraftData = const validateDraftData =
collectionConfig?.versions?.drafts && collectionConfig?.versions?.drafts?.validate collectionConfig?.versions?.drafts && collectionConfig?.versions?.drafts?.validate
if (shouldAutosave && !validateDraftData && !id && collectionSlug) { let id = idFromArgs
const doc = await payload.create({
if (shouldAutosave && !validateDraftData && !idFromArgs && collectionSlug) {
doc = await payload.create({
collection: collectionSlug, collection: collectionSlug,
data: initialData || {}, data: initialData || {},
depth: 0, depth: 0,
@@ -287,12 +289,18 @@ export const renderDocument = async ({
}) })
if (doc?.id) { if (doc?.id) {
const redirectURL = formatAdminURL({ id = doc.id
adminRoute, isEditing = getIsEditing({ id: doc.id, collectionSlug, globalSlug })
path: `/collections/${collectionSlug}/${doc.id}`,
serverURL, if (!drawerSlug) {
}) const redirectURL = formatAdminURL({
redirect(redirectURL) adminRoute,
path: `/collections/${collectionSlug}/${doc.id}`,
serverURL,
})
redirect(redirectURL)
}
} else { } else {
throw new Error('not-found') throw new Error('not-found')
} }

View File

@@ -1,7 +1,7 @@
'use client' 'use client'
import { useModal } from '@faceless-ui/modal' import { useModal } from '@faceless-ui/modal'
import React, { useCallback, useEffect, useState } from 'react' import React, { useCallback, useEffect, useRef, useState } from 'react'
import { toast } from 'sonner' import { toast } from 'sonner'
import type { DocumentDrawerProps } from './types.js' import type { DocumentDrawerProps } from './types.js'
@@ -45,6 +45,7 @@ export const DocumentDrawerContent: React.FC<DocumentDrawerProps> = ({
const [DocumentView, setDocumentView] = useState<React.ReactNode>(undefined) const [DocumentView, setDocumentView] = useState<React.ReactNode>(undefined)
const [isLoading, setIsLoading] = useState(true) const [isLoading, setIsLoading] = useState(true)
const hasRenderedDocument = useRef(false)
const getDocumentView = useCallback( const getDocumentView = useCallback(
(docID?: number | string) => { (docID?: number | string) => {
@@ -142,8 +143,9 @@ export const DocumentDrawerContent: React.FC<DocumentDrawerProps> = ({
}, [getDocumentView]) }, [getDocumentView])
useEffect(() => { useEffect(() => {
if (!DocumentView) { if (!DocumentView && !hasRenderedDocument.current) {
getDocumentView(existingDocID) getDocumentView(existingDocID)
hasRenderedDocument.current = true
} }
}, [DocumentView, getDocumentView, existingDocID]) }, [DocumentView, getDocumentView, existingDocID])

View File

@@ -32,7 +32,7 @@ type RenderDocument = (args: {
redirectAfterDelete?: boolean redirectAfterDelete?: boolean
redirectAfterDuplicate?: boolean redirectAfterDuplicate?: boolean
signal?: AbortSignal signal?: AbortSignal
}) => Promise<{ docID: string; Document: React.ReactNode }> }) => Promise<{ data: Data; Document: React.ReactNode }>
type GetDocumentSlots = (args: { type GetDocumentSlots = (args: {
collectionSlug: string collectionSlug: string
@@ -129,16 +129,12 @@ export const ServerFunctionsProvider: React.FC<{
const { signal: remoteSignal, ...rest } = args || {} const { signal: remoteSignal, ...rest } = args || {}
try { try {
if (!remoteSignal?.aborted) { const result = (await serverFunction({
const result = (await serverFunction({ name: 'render-document',
name: 'render-document', args: { fallbackLocale: false, ...rest },
args: { fallbackLocale: false, ...rest }, })) as { data: Data; Document: React.ReactNode }
})) as { docID: string; Document: React.ReactNode }
if (!remoteSignal?.aborted) { return result
return result
}
}
} catch (_err) { } catch (_err) {
console.error(_err) // eslint-disable-line no-console console.error(_err) // eslint-disable-line no-console
} }

View File

@@ -179,10 +179,9 @@ describe('fields - relationship', () => {
await expect(options).toHaveCount(2) // two docs await expect(options).toHaveCount(2) // two docs
await options.nth(0).click() await options.nth(0).click()
await expect(field).toContainText(relationOneDoc.id) await expect(field).toContainText(relationOneDoc.id)
await saveDocAndAssert(page) await trackNetworkRequests(page, `/api/${relationOneSlug}`, async () => {
await wait(200) await saveDocAndAssert(page)
await trackNetworkRequests(page, `/api/${relationOneSlug}`, { await wait(200)
beforePoll: async () => await page.reload(),
}) })
}) })

View File

@@ -8,6 +8,7 @@ import { expect } from '@playwright/test'
export const trackNetworkRequests = async ( export const trackNetworkRequests = async (
page: Page, page: Page,
url: string, url: string,
action: () => Promise<any>,
options?: { options?: {
allowedNumberOfRequests?: number allowedNumberOfRequests?: number
beforePoll?: () => Promise<any> | void beforePoll?: () => Promise<any> | void
@@ -26,6 +27,8 @@ export const trackNetworkRequests = async (
} }
}) })
await action()
if (typeof beforePoll === 'function') { if (typeof beforePoll === 'function') {
await beforePoll() await beforePoll()
} }

View File

@@ -25,6 +25,7 @@
import type { BrowserContext, Page } from '@playwright/test' import type { BrowserContext, Page } from '@playwright/test'
import { expect, test } from '@playwright/test' import { expect, test } from '@playwright/test'
import { navigateToDoc } from 'helpers/e2e/navigateToDoc.js'
import path from 'path' import path from 'path'
import { wait } from 'payload/shared' import { wait } from 'payload/shared'
import { fileURLToPath } from 'url' import { fileURLToPath } from 'url'
@@ -43,6 +44,7 @@ import {
throttleTest, throttleTest,
} from '../helpers.js' } from '../helpers.js'
import { AdminUrlUtil } from '../helpers/adminUrlUtil.js' import { AdminUrlUtil } from '../helpers/adminUrlUtil.js'
import { trackNetworkRequests } from '../helpers/e2e/trackNetworkRequests.js'
import { initPayloadE2ENoConfig } from '../helpers/initPayloadE2ENoConfig.js' import { initPayloadE2ENoConfig } from '../helpers/initPayloadE2ENoConfig.js'
import { reInitializeDB } from '../helpers/reInitializeDB.js' import { reInitializeDB } from '../helpers/reInitializeDB.js'
import { waitForAutoSaveToRunAndComplete } from '../helpers/waitForAutoSaveToRunAndComplete.js' import { waitForAutoSaveToRunAndComplete } from '../helpers/waitForAutoSaveToRunAndComplete.js'
@@ -394,6 +396,42 @@ describe('versions', () => {
expect(page.url()).toMatch(/\/versions$/) expect(page.url()).toMatch(/\/versions$/)
}) })
test('collection - should autosave', async () => {
await page.goto(autosaveURL.create)
await page.locator('#field-title').fill('autosave title')
await waitForAutoSaveToRunAndComplete(page)
await expect(page.locator('#field-title')).toHaveValue('autosave title')
const { id: postID } = await payload.create({
collection: postCollectionSlug,
data: {
title: 'post title',
description: 'post description',
},
})
await page.goto(postURL.edit(postID))
await trackNetworkRequests(
page,
`${serverURL}/admin/collections/${postCollectionSlug}/${postID}`,
async () => {
await page
.locator(
'#field-relationToAutosaves.field-type.relationship .relationship-add-new__add-button.doc-drawer__toggler',
)
.click()
},
{
allowedNumberOfRequests: 1,
},
)
const drawer = page.locator('[id^=doc-drawer_autosave-posts_1_]')
await expect(drawer).toBeVisible()
await expect(drawer.locator('.id-label')).toBeVisible()
})
test('global - should autosave', async () => { test('global - should autosave', async () => {
const url = new AdminUrlUtil(serverURL, autoSaveGlobalSlug) const url = new AdminUrlUtil(serverURL, autoSaveGlobalSlug)
await page.goto(url.global(autoSaveGlobalSlug)) await page.goto(url.global(autoSaveGlobalSlug))

View File

@@ -37,7 +37,7 @@
], ],
"paths": { "paths": {
"@payload-config": [ "@payload-config": [
"./test/_community/config.ts" "./test/versions/config.ts"
], ],
"@payloadcms/live-preview": [ "@payloadcms/live-preview": [
"./packages/live-preview/src" "./packages/live-preview/src"