Files
payload/test/admin/e2e/document-view/e2e.spec.ts
Jacob Fletcher c80b6e92c4 fix(ui): prevent document drawer from remounting on save (#13005)
Supersedes #12992. Partially closes #12975.

Right now autosave-enabled documents opened within a drawer will
unnecessarily remount on every autosave interval, causing loss of input
focus, etc. This makes it nearly impossible to edit these documents,
especially if the interval is very short.

But the same is true for non-autosave documents when "manually" saving,
e.g. pressing the "save draft" or "publish changes" buttons. This has
gone largely unnoticed, however, as the user has already lost focus of
the form to interact with these controls, and they somewhat expect this
behavior or at least accept it.

Now, the form remains mounted across autosave events and the user's
cursor never loses focus. Much better.

Before:


https://github.com/user-attachments/assets/a159cdc0-21e8-45f6-a14d-6256e53bc3df

After:


https://github.com/user-attachments/assets/cd697439-1cd3-4033-8330-a5642f7810e8

Related: #12842

---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
  - https://app.asana.com/0/0/1210689077645986
2025-07-02 09:07:08 -04:00

742 lines
27 KiB
TypeScript

import type { Page } from '@playwright/test'
import { expect, test } from '@playwright/test'
import { wait } from 'payload/shared'
import type { Config, Post } from '../../payload-types.js'
import {
checkBreadcrumb,
checkPageTitle,
ensureCompilationIsDone,
exactText,
initPageConsoleErrorCatch,
saveDocAndAssert,
} from '../../../helpers.js'
import { AdminUrlUtil } from '../../../helpers/adminUrlUtil.js'
import { initPayloadE2ENoConfig } from '../../../helpers/initPayloadE2ENoConfig.js'
import {
customAdminRoutes,
customEditLabel,
customNestedTabViewPath,
customNestedTabViewTitle,
customTabAdminDescription,
customTabComponent,
customTabLabel,
customTabViewPath,
customTabViewTitle,
overriddenDefaultRouteTabLabel,
} from '../../shared.js'
import {
customFieldsSlug,
customGlobalViews2GlobalSlug,
customViews2CollectionSlug,
editMenuItemsSlug,
globalSlug,
group1Collection1Slug,
group1GlobalSlug,
noApiViewCollectionSlug,
noApiViewGlobalSlug,
placeholderCollectionSlug,
postsCollectionSlug,
reorderTabsSlug,
} from '../../slugs.js'
const { beforeAll, beforeEach, describe } = test
const title = 'Title'
const description = 'Description'
let payload: PayloadTestSDK<Config>
import { navigateToDoc } from 'helpers/e2e/navigateToDoc.js'
import { openNav } from 'helpers/e2e/toggleNav.js'
import path from 'path'
import { fileURLToPath } from 'url'
import type { PayloadTestSDK } from '../../../helpers/sdk/index.js'
import { reInitializeDB } from '../../../helpers/reInitializeDB.js'
import { TEST_TIMEOUT_LONG } from '../../../playwright.config.js'
const filename = fileURLToPath(import.meta.url)
const currentFolder = path.dirname(filename)
const dirname = path.resolve(currentFolder, '../../')
describe('Document View', () => {
let page: Page
let postsUrl: AdminUrlUtil
let globalURL: AdminUrlUtil
let serverURL: string
let customViewsURL: AdminUrlUtil
let customFieldsURL: AdminUrlUtil
let placeholderURL: AdminUrlUtil
let collectionCustomViewPathId: string
let editMenuItemsURL: AdminUrlUtil
let reorderTabsURL: AdminUrlUtil
beforeAll(async ({ browser }, testInfo) => {
const prebuild = false // Boolean(process.env.CI)
testInfo.setTimeout(TEST_TIMEOUT_LONG)
process.env.SEED_IN_CONFIG_ONINIT = 'false' // Makes it so the payload config onInit seed is not run. Otherwise, the seed would be run unnecessarily twice for the initial test run - once for beforeEach and once for onInit
;({ payload, serverURL } = await initPayloadE2ENoConfig<Config>({
dirname,
prebuild,
}))
postsUrl = new AdminUrlUtil(serverURL, postsCollectionSlug)
globalURL = new AdminUrlUtil(serverURL, globalSlug)
customViewsURL = new AdminUrlUtil(serverURL, customViews2CollectionSlug)
customFieldsURL = new AdminUrlUtil(serverURL, customFieldsSlug)
placeholderURL = new AdminUrlUtil(serverURL, placeholderCollectionSlug)
editMenuItemsURL = new AdminUrlUtil(serverURL, editMenuItemsSlug)
reorderTabsURL = new AdminUrlUtil(serverURL, reorderTabsSlug)
const context = await browser.newContext()
page = await context.newPage()
initPageConsoleErrorCatch(page)
await ensureCompilationIsDone({ customAdminRoutes, page, serverURL })
})
beforeEach(async () => {
await reInitializeDB({
serverURL,
snapshotKey: 'adminTests',
})
await ensureCompilationIsDone({ customAdminRoutes, page, serverURL })
})
describe('API view', () => {
test('collection — should not show API tab when disabled in config', async () => {
await page.goto(postsUrl.collection(noApiViewCollectionSlug))
await page.locator('.collection-list .table a').click()
await expect(page.locator('.doc-tabs__tabs-container')).not.toContainText('API')
})
test('collection — should not enable API route when disabled in config', async () => {
const collectionItems = await payload.find({
collection: noApiViewCollectionSlug,
limit: 1,
})
expect(collectionItems.docs.length).toBe(1)
await page.goto(
`${postsUrl.collection(noApiViewGlobalSlug)}/${collectionItems?.docs[0]?.id}/api`,
)
await expect(page.locator('.not-found')).toHaveCount(1)
})
test('collection — sidebar fields should respond to permission', async () => {
const { id } = await createPost()
await page.goto(postsUrl.edit(id))
await expect(page.locator('#field-sidebarField')).toBeDisabled()
})
test('collection — depth field should have value 0 when empty', async () => {
const { id } = await createPost()
await page.goto(`${postsUrl.edit(id)}/api`)
const depthField = page.locator('#field-depth')
await depthField.fill('')
await expect(depthField).toHaveValue('0')
})
test('global — should not show API tab when disabled in config', async () => {
await page.goto(postsUrl.global(noApiViewGlobalSlug))
await expect(page.locator('.doc-tabs__tabs-container')).not.toContainText('API')
})
test('global — should not enable API route when disabled in config', async () => {
await page.goto(`${postsUrl.global(noApiViewGlobalSlug)}/api`)
await expect(page.locator('.not-found')).toHaveCount(1)
})
})
describe('preview button', () => {
test('collection — should render preview button when `admin.preview` is set', async () => {
const collectionWithPreview = new AdminUrlUtil(serverURL, postsCollectionSlug)
await page.goto(collectionWithPreview.create)
await page.locator('#field-title').fill(title)
await saveDocAndAssert(page)
await expect(page.locator('button#preview-button')).toBeVisible()
})
test('collection — should not render preview button when `admin.preview` is not set', async () => {
const collectionWithoutPreview = new AdminUrlUtil(serverURL, group1Collection1Slug)
await page.goto(collectionWithoutPreview.create)
await page.locator('#field-title').fill(title)
await saveDocAndAssert(page)
await expect(page.locator('button#preview-button')).toBeHidden()
})
test('global — should render preview button when `admin.preview` is set', async () => {
const globalWithPreview = new AdminUrlUtil(serverURL, globalSlug)
await page.goto(globalWithPreview.global(globalSlug))
await expect(page.locator('button#preview-button')).toBeVisible()
})
test('global — should not render preview button when `admin.preview` is not set', async () => {
const globalWithoutPreview = new AdminUrlUtil(serverURL, group1GlobalSlug)
await page.goto(globalWithoutPreview.global(group1GlobalSlug))
await page.locator('#field-title').fill(title)
await saveDocAndAssert(page)
await expect(page.locator('button#preview-button')).toBeHidden()
})
})
describe('document titles', () => {
test('collection — should render fallback titles when creating new', async () => {
await page.goto(postsUrl.create)
await checkPageTitle(page, '[Untitled]')
await checkBreadcrumb(page, 'Create New')
await saveDocAndAssert(page)
})
test('collection — should render `useAsTitle` field', async () => {
await page.goto(postsUrl.create)
await page.locator('#field-title')?.fill(title)
await saveDocAndAssert(page)
await wait(500)
await checkPageTitle(page, title)
await checkBreadcrumb(page, title)
})
test('collection — should render `id` as `useAsTitle` fallback', async () => {
const { id } = await createPost()
const postURL = postsUrl.edit(id)
await page.goto(postURL)
await wait(500)
await page.locator('#field-title')?.fill('')
await expect(page.locator('.doc-header__title.render-title:has-text("ID:")')).toBeVisible()
await saveDocAndAssert(page)
})
test('global — should render custom, localized label', async () => {
await page.goto(globalURL.global(globalSlug))
await openNav(page)
const label = 'My Global Label'
const globalLabel = page.locator(`#nav-global-global`)
await expect(globalLabel).toContainText(label)
await globalLabel.click()
await checkPageTitle(page, label)
await checkBreadcrumb(page, label)
await page.locator('#field-title').fill(title)
await saveDocAndAssert(page)
await checkPageTitle(page, label)
await checkBreadcrumb(page, label)
})
test('global — should render simple label strings', async () => {
await page.goto(postsUrl.admin)
await openNav(page)
const label = 'Group Globals 1'
const globalLabel = page.locator(`#nav-global-group-globals-one`)
await expect(globalLabel).toContainText(label)
await globalLabel.click()
await checkPageTitle(page, label)
await checkBreadcrumb(page, label)
})
test('global — should render slug in sentence case as fallback', async () => {
await page.goto(postsUrl.admin)
await openNav(page)
const label = 'Group Globals Two'
const globalLabel = page.locator(`#nav-global-group-globals-two`)
await expect(globalLabel).toContainText(label)
await globalLabel.click()
await checkPageTitle(page, label)
await checkBreadcrumb(page, label)
})
})
describe('breadcrumbs', () => {
test('List drawer should not effect underlying breadcrumbs', async () => {
await navigateToDoc(page, postsUrl)
await expect(page.locator('.step-nav.app-header__step-nav a').nth(1)).toHaveText('Posts')
await page.locator('#field-upload button.upload__listToggler').click()
await expect(page.locator('[id^=list-drawer_1_]')).toBeVisible()
await wait(100) // wait for the component to re-render
await expect(
page.locator('.step-nav.app-header__step-nav .step-nav__last'),
).not.toContainText('Uploads')
await expect(page.locator('.step-nav.app-header__step-nav a').nth(1)).toHaveText('Posts')
})
})
describe('custom document views', () => {
test('collection — should render custom tab view', async () => {
await page.goto(customViewsURL.create)
await page.locator('#field-title').fill('Test')
await saveDocAndAssert(page)
const pageURL = page.url()
const customViewURL = `${pageURL}${customTabViewPath}`
await page.goto(customViewURL)
expect(page.url()).toEqual(customViewURL)
await expect(page.locator('h1#custom-view-title')).toContainText(customTabViewTitle)
})
test('collection — should render custom nested tab view', async () => {
await page.goto(customViewsURL.create)
await page.locator('#field-title').fill('Test')
await saveDocAndAssert(page)
// wait for the update view to load
await page.waitForURL(/\/(?!create$)[\w-]+$/)
const pageURL = page.url()
const customNestedTabViewURL = `${pageURL}${customNestedTabViewPath}`
await page.goto(customNestedTabViewURL)
await expect(page.locator('h1#custom-view-title')).toContainText(customNestedTabViewTitle)
})
test('collection — should render custom tab label', async () => {
await page.goto(customViewsURL.create)
await page.locator('#field-title').fill('Test')
await saveDocAndAssert(page)
// wait for the update view to load
await page.waitForURL(/\/(?!create$)[\w-]+$/)
const editTab = page.locator(`.doc-tab:has-text("${customEditLabel}")`)
await expect(editTab).toBeVisible()
})
test('collection - should allow to override the tab for the default view', async () => {
await page.goto(customViewsURL.create)
await page.locator('#field-title').fill('Test')
await saveDocAndAssert(page)
const customTab = page.locator(
`.custom-doc-tab a:has-text("${overriddenDefaultRouteTabLabel}")`,
)
await expect(customTab).toBeVisible()
})
test('collection — should render custom tab component', async () => {
await page.goto(customViewsURL.create)
await page.locator('#field-title').fill('Test')
await saveDocAndAssert(page)
const customTab = page.locator(`a.doc-tab:has-text("${customTabLabel}")`)
await expect(customTab).toBeVisible()
})
test('global — should render custom tab label', async () => {
await page.goto(globalURL.global(customGlobalViews2GlobalSlug) + '/custom-tab-view')
const title = page.locator('#custom-view-title')
const docTab = page.locator('.doc-tab:has-text("Custom")')
await expect(docTab).toBeVisible()
await expect(title).toContainText('Custom Tab Label View')
})
test('global — should render custom tab component', async () => {
await page.goto(globalURL.global(customGlobalViews2GlobalSlug) + '/custom-tab-component')
const title = page.locator('#custom-view-title')
const docTab = page.locator('.custom-doc-tab').first()
await expect(docTab).toBeVisible()
await expect(docTab).toContainText(customTabComponent)
await expect(title).toContainText('Custom View With Tab Component')
})
test('global — should allow to override the tab for the default view', async () => {
await page.goto(globalURL.global(customGlobalViews2GlobalSlug))
const customTab = page.locator(
`.custom-doc-tab a:has-text("${overriddenDefaultRouteTabLabel}")`,
)
await expect(customTab).toBeVisible()
})
})
describe('drawers', () => {
test('document drawers do not unmount across save events', async () => {
// Navigate to a post document
await navigateToDoc(page, postsUrl)
// Open the relationship drawer
await page
.locator('.field-type.relationship .relationship--single-value__drawer-toggler')
.click()
const drawer = page.locator('[id^=doc-drawer_posts_1_]')
const drawerEditView = drawer.locator('.drawer__content .collection-edit')
await expect(drawerEditView).toBeVisible()
const drawerTitleField = drawerEditView.locator('#field-title')
const testTitle = 'Test Title for Persistence'
await drawerTitleField.fill(testTitle)
await expect(drawerTitleField).toHaveValue(testTitle)
await drawerEditView.evaluate((el) => {
el.setAttribute('data-test-instance', 'This is a test')
})
await expect(drawerEditView).toHaveAttribute('data-test-instance', 'This is a test')
await saveDocAndAssert(page, '[id^=doc-drawer_posts_1_] .drawer__content #action-save')
await expect(drawerEditView).toBeVisible()
await expect(drawerTitleField).toHaveValue(testTitle)
// Verify the element instance hasn't changed (i.e., it wasn't re-mounted and discarded the custom attribute)
await expect
.poll(async () => {
return await drawerEditView.getAttribute('data-test-instance')
})
.toBe('This is a test')
})
test('document drawers are visually stacking', async () => {
await navigateToDoc(page, postsUrl)
await page.locator('#field-title').fill(title)
await saveDocAndAssert(page)
await page
.locator('.field-type.relationship .relationship--single-value__drawer-toggler')
.click()
await wait(500)
const drawer1Content = page.locator('[id^=doc-drawer_posts_1_] .drawer__content')
await expect(drawer1Content).toBeVisible()
const drawer1Box = await drawer1Content.boundingBox()
await expect.poll(() => drawer1Box).not.toBeNull()
const drawerLeft = drawer1Box!.x
await drawer1Content
.locator('.field-type.relationship .relationship--single-value__drawer-toggler')
.click()
const drawer2Content = page.locator('[id^=doc-drawer_posts_2_] .drawer__content')
await expect(drawer2Content).toBeVisible()
const drawer2Box = await drawer2Content.boundingBox()
await expect.poll(() => drawer2Box).not.toBeNull()
const drawer2Left = drawer2Box!.x
await expect.poll(() => drawer2Left > drawerLeft).toBe(true)
})
test('document drawer displays a link to document', async () => {
await navigateToDoc(page, postsUrl)
// change the relationship to a document which is a different one than the current one
await page.locator('#field-relationship').click()
await wait(200)
await page.locator('#field-relationship .rs__option').nth(2).click()
await wait(500)
await saveDocAndAssert(page)
// open relationship drawer
await page
.locator('.field-type.relationship .relationship--single-value__drawer-toggler')
.click()
await wait(200)
const drawer1Content = page.locator('[id^=doc-drawer_posts_1_] .drawer__content')
await expect(drawer1Content).toBeVisible()
// modify the title to trigger the leave page modal
await page.locator('.drawer__content #field-title').fill('New Title')
await wait(200)
// Open link in a new tab by holding down the Meta or Control key
const documentLink = page.locator('.id-label a')
const documentId = String(await documentLink.textContent())
await documentLink.click()
await wait(200)
const leavePageModal = page.locator('#leave-without-saving #confirm-action').last()
await expect(leavePageModal).toBeVisible()
await leavePageModal.click()
await page.waitForURL(postsUrl.edit(documentId))
})
test('document can be opened in a new tab from within the drawer', async () => {
await navigateToDoc(page, postsUrl)
await page
.locator('.field-type.relationship .relationship--single-value__drawer-toggler')
.click()
await wait(500)
const drawer1Content = page.locator('[id^=doc-drawer_posts_1_] .drawer__content')
await expect(drawer1Content).toBeVisible()
const currentUrl = page.url()
// Open link in a new tab by holding down the Meta or Control key
const documentLink = page.locator('.id-label a')
const documentId = String(await documentLink.textContent())
const [newPage] = await Promise.all([
page.context().waitForEvent('page'),
documentLink.click({ modifiers: ['ControlOrMeta'] }),
])
// Wait for navigation to complete in the new tab and ensure correct URL
await expect(newPage.locator('.doc-header')).toBeVisible()
// using contain here, because after load the lists view will add query params like "?limit=10"
expect(newPage.url()).toContain(postsUrl.edit(documentId))
// Ensure the original page did not change
expect(page.url()).toBe(currentUrl)
})
})
describe('descriptions', () => {
test('should render tab admin description', async () => {
await page.goto(postsUrl.create)
const tabsContent = page.locator('.tabs-field__content-wrap')
await expect(tabsContent.locator('.field-description')).toHaveText(customTabAdminDescription)
})
test('should render tab admin description as a translation function', async () => {
await page.goto(postsUrl.create)
const secondTab = page.locator('.tabs-field__tab-button').nth(1)
await secondTab.click()
await wait(500)
const tabsContent = page.locator('.tabs-field__content-wrap')
await expect(
tabsContent.locator('.field-description', { hasText: `t:${customTabAdminDescription}` }),
).toBeVisible()
})
})
describe('custom fields', () => {
test('should render custom field component', async () => {
await page.goto(customFieldsURL.create)
await expect(page.locator('#field-customTextClientField')).toBeVisible()
})
test('renders custom label component', async () => {
await page.goto(customFieldsURL.create)
await expect(page.locator('#custom-client-field-label')).toBeVisible()
await expect(page.locator('#custom-server-field-label')).toBeVisible()
})
test('renders custom field description text', async () => {
await page.goto(customFieldsURL.create)
await expect(page.locator('#custom-client-field-description')).toBeVisible()
await expect(page.locator('#custom-server-field-description')).toBeVisible()
})
test('custom server components should receive field props', async () => {
await page.goto(customFieldsURL.create)
await expect(
page.locator('#custom-server-field-label', {
hasText: exactText('Label: the max length of this field is: 100'),
}),
).toBeVisible()
await expect(
page.locator('#custom-server-field-description', {
hasText: exactText('Description: the max length of this field is: 100'),
}),
).toBeVisible()
})
test('custom client components should receive field props', async () => {
await page.goto(customFieldsURL.create)
await expect(
page.locator('#custom-client-field-label', {
hasText: exactText('Label: the max length of this field is: 100'),
}),
).toBeVisible()
await expect(
page.locator('#custom-client-field-description', {
hasText: exactText('Description: the max length of this field is: 100'),
}),
).toBeVisible()
})
test('custom select input can have its value cleared', async () => {
await page.goto(customFieldsURL.create)
await expect(page.locator('#field-customSelectInput')).toBeVisible()
await page.locator('#field-customSelectInput .rs__control').click()
await page.locator('#field-customSelectInput .rs__option').first().click()
await expect(page.locator('#field-customSelectInput .rs__single-value')).toHaveText(
'Option 1',
)
await page.locator('.clear-value').click()
await expect(page.locator('#field-customSelectInput .rs__placeholder')).toHaveText(
'Select a value',
)
})
describe('field descriptions', () => {
test('should render static field description', async () => {
await page.goto(customFieldsURL.create)
await expect(page.locator('.field-description-descriptionAsString')).toContainText(
'Static field description.',
)
})
test('should render functional field description', async () => {
await page.goto(customFieldsURL.create)
await page.locator('#field-descriptionAsFunction').fill('functional')
await expect(page.locator('.field-description-descriptionAsFunction')).toContainText(
'Function description',
)
})
})
test('should render component field description', async () => {
await page.goto(customFieldsURL.create)
await page.locator('#field-descriptionAsComponent').fill('component')
await expect(page.locator('.field-description-descriptionAsComponent')).toContainText(
'Component description: descriptionAsComponent - component',
)
})
test('should render custom error component', async () => {
await page.goto(customFieldsURL.create)
const input = page.locator('input[id="field-customTextClientField"]')
await input.fill('ab')
await expect(input).toHaveValue('ab')
const error = page.locator('.custom-error:near(input[id="field-customTextClientField"])')
const submit = page.locator('button[type="button"][id="action-save"]')
await submit.click()
await expect(error).toHaveText('#custom-error')
})
test('should render beforeInput and afterInput', async () => {
await page.goto(customFieldsURL.create)
const input = page.locator('input[id="field-customTextClientField"]')
const prevSibling = await input.evaluateHandle((el) => {
return el.previousElementSibling
})
const prevSiblingText = await page.evaluate((el) => el?.textContent, prevSibling)
expect(prevSiblingText).toEqual('#before-input')
const nextSibling = await input.evaluateHandle((el) => {
return el.nextElementSibling
})
const nextSiblingText = await page.evaluate((el) => el?.textContent, nextSibling)
expect(nextSiblingText).toEqual('#after-input')
})
describe('select field', () => {
test('should render custom select options', async () => {
await page.goto(customFieldsURL.create)
await page.locator('#field-customSelectField .rs__control').click()
await expect(page.locator('#field-customSelectField .rs__option')).toHaveCount(2)
})
})
})
describe('publish button', () => {
test('should show publish active locale button with defaultLocalePublishOption', async () => {
await navigateToDoc(page, postsUrl)
const publishButton = page.locator('#action-save')
await expect(publishButton).toBeVisible()
await expect(publishButton).toContainText('Publish in English')
})
})
describe('reserved field names', () => {
test('should allow creation of field named file in non-upload enabled collection', async () => {
await page.goto(postsUrl.create)
const fileField = page.locator('#field-file')
await fileField.fill('some file text')
await saveDocAndAssert(page)
await expect(fileField).toHaveValue('some file text')
})
})
describe('custom document controls', () => {
test('should show custom elements in document controls in collection', async () => {
await page.goto(postsUrl.create)
const customDraftButton = page.locator('#custom-draft-button')
const customSaveButton = page.locator('#custom-save-button')
await expect(customDraftButton).toBeVisible()
await expect(customSaveButton).toBeVisible()
})
test('should show custom elements in document controls in global', async () => {
await page.goto(globalURL.global(globalSlug))
const customDraftButton = page.locator('#custom-draft-button')
await expect(customDraftButton).toBeVisible()
})
})
describe('reordering tabs', () => {
beforeEach(async () => {
await page.goto(reorderTabsURL.create)
await page.locator('#field-title').fill('Reorder Tabs')
await saveDocAndAssert(page)
})
test('collection — should show api as first tab', async () => {
const tabs = page.locator('.doc-tabs__tabs-container .doc-tab')
const firstTab = tabs.first()
await expect(firstTab).toContainText('API')
})
test('collection — should show edit as third tab', async () => {
const tabs = page.locator('.doc-tabs__tabs-container .doc-tab')
const secondTab = tabs.nth(2)
await expect(secondTab).toContainText('Edit')
})
})
describe('custom editMenuItem components', () => {
test('should render custom editMenuItems component', async () => {
await page.goto(editMenuItemsURL.create)
await page.locator('#field-title')?.fill(title)
await saveDocAndAssert(page)
const threeDotMenu = page.getByRole('main').locator('.doc-controls__popup')
await expect(threeDotMenu).toBeVisible()
await threeDotMenu.click()
const customEditMenuItem = page.locator('.popup-button-list__button', {
hasText: 'Custom Edit Menu Item',
})
await expect(customEditMenuItem).toBeVisible()
})
})
})
async function createPost(overrides?: Partial<Post>): Promise<Post> {
return payload.create({
collection: postsCollectionSlug,
data: {
description,
title,
...overrides,
},
}) as unknown as Promise<Post>
}