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:
@@ -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')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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])
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(),
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user