From 9e85be00069ea95d44d2228295ec90a990eceea5 Mon Sep 17 00:00:00 2001 From: Jacob Fletcher Date: Tue, 19 Nov 2024 19:01:54 -0500 Subject: [PATCH] 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. --- packages/next/src/views/Document/index.tsx | 46 +++++++++++-------- .../elements/DocumentDrawer/DrawerContent.tsx | 6 ++- .../src/providers/ServerFunctions/index.tsx | 16 +++---- test/fields-relationship/e2e.spec.ts | 7 ++- test/helpers/e2e/trackNetworkRequests.ts | 3 ++ test/versions/e2e.spec.ts | 38 +++++++++++++++ tsconfig.json | 2 +- 7 files changed, 82 insertions(+), 36 deletions(-) diff --git a/packages/next/src/views/Document/index.tsx b/packages/next/src/views/Document/index.tsx index a88ba63cf..d0d4742c0 100644 --- a/packages/next/src/views/Document/index.tsx +++ b/packages/next/src/views/Document/index.tsx @@ -49,7 +49,7 @@ export const renderDocument = async ({ }> => { const { collectionConfig, - docID: id, + docID: idFromArgs, globalConfig, locale, permissions, @@ -72,7 +72,7 @@ export const renderDocument = async ({ const segments = Array.isArray(params?.segments) ? params.segments : [] const collectionSlug = collectionConfig?.slug || undefined const globalSlug = globalConfig?.slug || undefined - const isEditing = getIsEditing({ id, collectionSlug, globalSlug }) + let isEditing = getIsEditing({ id: idFromArgs, collectionSlug, globalSlug }) let RootViewOverride: PayloadComponent let CustomView: ViewFromConfig @@ -82,10 +82,10 @@ export const renderDocument = async ({ let apiURL: string // Fetch the doc required for the view - const doc = + let doc = initialData || (await getDocumentData({ - id, + id: idFromArgs, collectionSlug, globalSlug, locale, @@ -104,7 +104,7 @@ export const renderDocument = async ({ ] = await Promise.all([ // Get document preferences getDocPreferences({ - id, + id: idFromArgs, collectionSlug, globalSlug, payload, @@ -113,7 +113,7 @@ export const renderDocument = async ({ // Get permissions getDocumentPermissions({ - id, + id: idFromArgs, collectionConfig, data: doc, globalConfig, @@ -122,7 +122,7 @@ export const renderDocument = async ({ // Fetch document lock state getIsLocked({ - id, + id: idFromArgs, collectionConfig, globalConfig, isEditing, @@ -135,7 +135,7 @@ export const renderDocument = async ({ { state: formState }, ] = await Promise.all([ getVersions({ - id, + id: idFromArgs, collectionConfig, docPermissions, globalConfig, @@ -144,7 +144,7 @@ export const renderDocument = async ({ user, }), buildFormState({ - id, + id: idFromArgs, collectionSlug, data: doc, docPermissions, @@ -152,7 +152,7 @@ export const renderDocument = async ({ fallbackLocale: false, globalSlug, locale: locale?.code, - operation: (collectionSlug && id) || globalSlug ? 'update' : 'create', + operation: (collectionSlug && idFromArgs) || globalSlug ? 'update' : 'create', renderAllFields: true, req, schemaPath: collectionSlug || globalSlug, @@ -187,7 +187,7 @@ export const renderDocument = async ({ const apiQueryParams = `?${params.toString()}` - apiURL = `${serverURL}${apiRoute}/${collectionSlug}/${id}${apiQueryParams}` + apiURL = `${serverURL}${apiRoute}/${collectionSlug}/${idFromArgs}${apiQueryParams}` RootViewOverride = collectionConfig?.admin?.components?.views?.edit?.root && @@ -274,8 +274,10 @@ export const renderDocument = async ({ const validateDraftData = collectionConfig?.versions?.drafts && collectionConfig?.versions?.drafts?.validate - if (shouldAutosave && !validateDraftData && !id && collectionSlug) { - const doc = await payload.create({ + let id = idFromArgs + + if (shouldAutosave && !validateDraftData && !idFromArgs && collectionSlug) { + doc = await payload.create({ collection: collectionSlug, data: initialData || {}, depth: 0, @@ -287,12 +289,18 @@ export const renderDocument = async ({ }) if (doc?.id) { - const redirectURL = formatAdminURL({ - adminRoute, - path: `/collections/${collectionSlug}/${doc.id}`, - serverURL, - }) - redirect(redirectURL) + id = doc.id + isEditing = getIsEditing({ id: doc.id, collectionSlug, globalSlug }) + + if (!drawerSlug) { + const redirectURL = formatAdminURL({ + adminRoute, + path: `/collections/${collectionSlug}/${doc.id}`, + serverURL, + }) + + redirect(redirectURL) + } } else { throw new Error('not-found') } diff --git a/packages/ui/src/elements/DocumentDrawer/DrawerContent.tsx b/packages/ui/src/elements/DocumentDrawer/DrawerContent.tsx index 8dc69f2b3..63b7e92a7 100644 --- a/packages/ui/src/elements/DocumentDrawer/DrawerContent.tsx +++ b/packages/ui/src/elements/DocumentDrawer/DrawerContent.tsx @@ -1,7 +1,7 @@ 'use client' 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 type { DocumentDrawerProps } from './types.js' @@ -45,6 +45,7 @@ export const DocumentDrawerContent: React.FC = ({ const [DocumentView, setDocumentView] = useState(undefined) const [isLoading, setIsLoading] = useState(true) + const hasRenderedDocument = useRef(false) const getDocumentView = useCallback( (docID?: number | string) => { @@ -142,8 +143,9 @@ export const DocumentDrawerContent: React.FC = ({ }, [getDocumentView]) useEffect(() => { - if (!DocumentView) { + if (!DocumentView && !hasRenderedDocument.current) { getDocumentView(existingDocID) + hasRenderedDocument.current = true } }, [DocumentView, getDocumentView, existingDocID]) diff --git a/packages/ui/src/providers/ServerFunctions/index.tsx b/packages/ui/src/providers/ServerFunctions/index.tsx index 144c363a2..7309fd1cb 100644 --- a/packages/ui/src/providers/ServerFunctions/index.tsx +++ b/packages/ui/src/providers/ServerFunctions/index.tsx @@ -32,7 +32,7 @@ type RenderDocument = (args: { redirectAfterDelete?: boolean redirectAfterDuplicate?: boolean signal?: AbortSignal -}) => Promise<{ docID: string; Document: React.ReactNode }> +}) => Promise<{ data: Data; Document: React.ReactNode }> type GetDocumentSlots = (args: { collectionSlug: string @@ -129,16 +129,12 @@ export const ServerFunctionsProvider: React.FC<{ const { signal: remoteSignal, ...rest } = args || {} try { - if (!remoteSignal?.aborted) { - const result = (await serverFunction({ - name: 'render-document', - args: { fallbackLocale: false, ...rest }, - })) as { docID: string; Document: React.ReactNode } + const result = (await serverFunction({ + name: 'render-document', + args: { fallbackLocale: false, ...rest }, + })) as { data: Data; Document: React.ReactNode } - if (!remoteSignal?.aborted) { - return result - } - } + return result } catch (_err) { console.error(_err) // eslint-disable-line no-console } diff --git a/test/fields-relationship/e2e.spec.ts b/test/fields-relationship/e2e.spec.ts index 2673ce42e..c34cd4207 100644 --- a/test/fields-relationship/e2e.spec.ts +++ b/test/fields-relationship/e2e.spec.ts @@ -179,10 +179,9 @@ describe('fields - relationship', () => { await expect(options).toHaveCount(2) // two docs await options.nth(0).click() await expect(field).toContainText(relationOneDoc.id) - await saveDocAndAssert(page) - await wait(200) - await trackNetworkRequests(page, `/api/${relationOneSlug}`, { - beforePoll: async () => await page.reload(), + await trackNetworkRequests(page, `/api/${relationOneSlug}`, async () => { + await saveDocAndAssert(page) + await wait(200) }) }) diff --git a/test/helpers/e2e/trackNetworkRequests.ts b/test/helpers/e2e/trackNetworkRequests.ts index e13399ff9..64c684b2c 100644 --- a/test/helpers/e2e/trackNetworkRequests.ts +++ b/test/helpers/e2e/trackNetworkRequests.ts @@ -8,6 +8,7 @@ import { expect } from '@playwright/test' export const trackNetworkRequests = async ( page: Page, url: string, + action: () => Promise, options?: { allowedNumberOfRequests?: number beforePoll?: () => Promise | void @@ -26,6 +27,8 @@ export const trackNetworkRequests = async ( } }) + await action() + if (typeof beforePoll === 'function') { await beforePoll() } diff --git a/test/versions/e2e.spec.ts b/test/versions/e2e.spec.ts index fdc54f1a5..ef300e4db 100644 --- a/test/versions/e2e.spec.ts +++ b/test/versions/e2e.spec.ts @@ -25,6 +25,7 @@ import type { BrowserContext, Page } from '@playwright/test' import { expect, test } from '@playwright/test' +import { navigateToDoc } from 'helpers/e2e/navigateToDoc.js' import path from 'path' import { wait } from 'payload/shared' import { fileURLToPath } from 'url' @@ -43,6 +44,7 @@ import { throttleTest, } from '../helpers.js' import { AdminUrlUtil } from '../helpers/adminUrlUtil.js' +import { trackNetworkRequests } from '../helpers/e2e/trackNetworkRequests.js' import { initPayloadE2ENoConfig } from '../helpers/initPayloadE2ENoConfig.js' import { reInitializeDB } from '../helpers/reInitializeDB.js' import { waitForAutoSaveToRunAndComplete } from '../helpers/waitForAutoSaveToRunAndComplete.js' @@ -394,6 +396,42 @@ describe('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 () => { const url = new AdminUrlUtil(serverURL, autoSaveGlobalSlug) await page.goto(url.global(autoSaveGlobalSlug)) diff --git a/tsconfig.json b/tsconfig.json index 314fd332c..ac5eff743 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -37,7 +37,7 @@ ], "paths": { "@payload-config": [ - "./test/_community/config.ts" + "./test/versions/config.ts" ], "@payloadcms/live-preview": [ "./packages/live-preview/src"