From 06debf5e142abef2e746eed9c1eddf1e50b5f0da Mon Sep 17 00:00:00 2001 From: Paul Date: Tue, 18 Feb 2025 19:12:41 +0000 Subject: [PATCH] fix(ui): issues with prevent leave and autosave when the form is submitted but invalid (#11233) Fixes https://github.com/payloadcms/payload/issues/11224 Fixes https://github.com/payloadcms/payload/issues/10492 This PR fixes a few weird behaviours when `validate: true` is set on drafts: - when autosave is on and you submit an invalid form it would get stuck in an infinite loop - PreventLeave would not trigger for submitted but invalid forms leading to potential data loss Changes: - Adds e2e tests for the above scenarios - Adds a new `isValid` flag on the `Form` context provider to signal globally if the form is in a valid or invalid state - Components like Autosave will manage this internally since it manages its own submission flow as well - Adds PreventLeave to Autosave too for when form is invalid meaning data hasn't been actually saved so we want to prevent the user accidentally losing data by reloading or closing the page The following tests have been added ![image](https://github.com/user-attachments/assets/db208aa4-6ed6-4287-b200-59575cd3c9d0) --- packages/ui/src/elements/Autosave/index.tsx | 40 ++- .../src/elements/LeaveWithoutSaving/index.tsx | 5 +- packages/ui/src/forms/Form/index.tsx | 14 ++ .../ui/src/forms/Form/initContextState.ts | 3 + packages/ui/src/forms/Form/types.ts | 6 + .../waitForAutoSaveToRunAndComplete.ts | 28 ++- .../collections/AutosaveWithValidate.ts | 33 +++ .../collections/DraftsWithValidate.ts | 21 ++ test/versions/config.ts | 4 + test/versions/e2e.spec.ts | 233 +++++++++++++++++- test/versions/payload-types.ts | 54 ++++ test/versions/seed.ts | 14 +- test/versions/slugs.ts | 4 + 13 files changed, 441 insertions(+), 18 deletions(-) create mode 100644 test/versions/collections/AutosaveWithValidate.ts create mode 100644 test/versions/collections/DraftsWithValidate.ts diff --git a/packages/ui/src/elements/Autosave/index.tsx b/packages/ui/src/elements/Autosave/index.tsx index 61870cd1a..ca42856c3 100644 --- a/packages/ui/src/elements/Autosave/index.tsx +++ b/packages/ui/src/elements/Autosave/index.tsx @@ -22,6 +22,7 @@ import { useTranslation } from '../../providers/Translation/index.js' import './index.scss' import { formatTimeToNow } from '../../utilities/formatDate.js' import { reduceFieldsToValuesWithValidation } from '../../utilities/reduceFieldsToValuesWithValidation.js' +import { LeaveWithoutSaving } from '../LeaveWithoutSaving/index.js' const baseClass = 'autosave' // The minimum time the saving state should be shown @@ -55,25 +56,36 @@ export const Autosave: React.FC = ({ id, collection, global: globalDoc }) const isProcessingRef = useRef(false) const { reportUpdate } = useDocumentEvents() - const { dispatchFields, setSubmitted } = useForm() - const submitted = useFormSubmitted() - const versionsConfig = docConfig?.versions + const { dispatchFields, isValid, setIsValid, setSubmitted } = useForm() const [fields] = useAllFormFields() const modified = useFormModified() + const submitted = useFormSubmitted() + const { code: locale } = useLocale() const { i18n, t } = useTranslation() + const versionsConfig = docConfig?.versions let interval = versionDefaults.autosaveInterval + if (versionsConfig.drafts && versionsConfig.drafts.autosave) { interval = versionsConfig.drafts.autosave.interval } + const validateOnDraft = Boolean( + docConfig?.versions?.drafts && docConfig?.versions?.drafts.validate, + ) + const [saving, setSaving] = useState(false) const debouncedFields = useDebounce(fields, interval) const fieldRef = useRef(fields) const modifiedRef = useRef(modified) const localeRef = useRef(locale) + /** + * Track the validation internally so Autosave can determine when to run queue processing again + * Helps us prevent infinite loops when the queue is processing and the form is invalid + */ + const isValidRef = useRef(isValid) const debouncedRef = useRef(debouncedFields) debouncedRef.current = debouncedFields @@ -97,6 +109,14 @@ export const Autosave: React.FC = ({ id, collection, global: globalDoc }) return } + if (!isValidRef.current) { + // Clear queue so we don't end up in an infinite loop + queueRef.current = [] + // Reset internal validation state so queue processing can run again + isValidRef.current = true + return + } + isProcessingRef.current = true const latestAction = queueRef.current[queueRef.current.length - 1] queueRef.current = [] @@ -149,7 +169,7 @@ export const Autosave: React.FC = ({ id, collection, global: globalDoc }) const skipSubmission = submitted && !valid && versionsConfig?.drafts && versionsConfig?.drafts?.validate - if (!skipSubmission) { + if (!skipSubmission && isValidRef.current) { await fetch(url, { body: JSON.stringify(data), credentials: 'include', @@ -222,8 +242,11 @@ export const Autosave: React.FC = ({ id, collection, global: globalDoc }) toast.error(err.message || i18n.t('error:unknown')) }) + // Set valid to false internally so the queue doesn't process + isValidRef.current = false + setIsValid(false) setSubmitted(true) - setSaving(false) + return } } else { @@ -232,11 +255,15 @@ export const Autosave: React.FC = ({ id, collection, global: globalDoc }) // Manually update the data since this function doesn't fire the `submit` function from useForm if (document) { + setIsValid(true) + + // Reset internal state allowing the queue to process + isValidRef.current = true updateSavedDocumentData(document) } } }) - .then(() => { + .finally(() => { // If request was faster than minimum animation time, animate the difference if (endTimestamp - startTimestamp < minimumAnimationTime) { autosaveTimeout = setTimeout( @@ -282,6 +309,7 @@ export const Autosave: React.FC = ({ id, collection, global: globalDoc }) return (
+ {validateOnDraft && !isValid && } {saving && t('general:saving')} {!saving && Boolean(lastUpdateTime) && ( diff --git a/packages/ui/src/elements/LeaveWithoutSaving/index.tsx b/packages/ui/src/elements/LeaveWithoutSaving/index.tsx index 1ff29e766..5232f8d87 100644 --- a/packages/ui/src/elements/LeaveWithoutSaving/index.tsx +++ b/packages/ui/src/elements/LeaveWithoutSaving/index.tsx @@ -1,7 +1,7 @@ 'use client' import React, { useCallback, useEffect } from 'react' -import { useFormModified } from '../../forms/Form/index.js' +import { useForm, useFormModified } from '../../forms/Form/index.js' import { useAuth } from '../../providers/Auth/index.js' import { useTranslation } from '../../providers/Translation/index.js' import { Button } from '../Button/index.js' @@ -59,11 +59,12 @@ const Component: React.FC<{ export const LeaveWithoutSaving: React.FC = () => { const { closeModal } = useModal() const modified = useFormModified() + const { isValid } = useForm() const { user } = useAuth() const [show, setShow] = React.useState(false) const [hasAccepted, setHasAccepted] = React.useState(false) - const prevent = Boolean(modified && user) + const prevent = Boolean((modified || !isValid) && user) const onPrevent = useCallback(() => { setShow(true) diff --git a/packages/ui/src/forms/Form/index.tsx b/packages/ui/src/forms/Form/index.tsx index 78793dfc0..2243430ab 100644 --- a/packages/ui/src/forms/Form/index.tsx +++ b/packages/ui/src/forms/Form/index.tsx @@ -96,6 +96,11 @@ export const Form: React.FC = (props) => { const [disabled, setDisabled] = useState(disabledFromProps || false) const [isMounted, setIsMounted] = useState(false) const [modified, setModified] = useState(false) + /** + * Tracks wether the form state passes validation. + * For example the state could be submitted but invalid as field errors have been returned. + */ + const [isValid, setIsValid] = useState(true) const [initializing, setInitializing] = useState(initializingFromProps) const [processing, setProcessing] = useState(false) const [submitted, setSubmitted] = useState(false) @@ -177,6 +182,8 @@ export const Form: React.FC = (props) => { dispatchFields({ type: 'REPLACE_STATE', state: validatedFieldState }) } + setIsValid(isValid) + return isValid }, [collectionSlug, config, dispatchFields, id, operation, t, user, documentForm]) @@ -257,6 +264,8 @@ export const Form: React.FC = (props) => { ([, field]) => field.valid !== false, ) + setIsValid(isValid) + if (!isValid) { setProcessing(false) setSubmitted(true) @@ -268,6 +277,7 @@ export const Form: React.FC = (props) => { const isValid = skipValidation || disableValidationOnSubmit ? true : await contextRef.current.validateForm() + setIsValid(isValid) // If not valid, prevent submission if (!isValid) { errorToast(t('error:correctInvalidFields')) @@ -408,6 +418,8 @@ export const Form: React.FC = (props) => { [[], []], ) + setIsValid(false) + dispatchFields({ type: 'ADD_SERVER_ERRORS', errors: fieldErrors, @@ -615,6 +627,7 @@ export const Form: React.FC = (props) => { contextRef.current.setModified = setModified contextRef.current.setProcessing = setProcessing contextRef.current.setSubmitted = setSubmitted + contextRef.current.setIsValid = setIsValid contextRef.current.disabled = disabled contextRef.current.setDisabled = setDisabled contextRef.current.formRef = formRef @@ -626,6 +639,7 @@ export const Form: React.FC = (props) => { contextRef.current.replaceFieldRow = replaceFieldRow contextRef.current.uuid = uuid contextRef.current.initializing = initializing + contextRef.current.isValid = isValid useEffect(() => { setIsMounted(true) diff --git a/packages/ui/src/forms/Form/initContextState.ts b/packages/ui/src/forms/Form/initContextState.ts index 78b821e0a..3f49c2a31 100644 --- a/packages/ui/src/forms/Form/initContextState.ts +++ b/packages/ui/src/forms/Form/initContextState.ts @@ -39,14 +39,17 @@ export const initContextState: Context = { getFields: (): FormState => ({}), getSiblingData, initializing: undefined, + isValid: true, removeFieldRow: () => undefined, replaceFieldRow: () => undefined, replaceState: () => undefined, reset, setDisabled: () => undefined, + setIsValid: () => undefined, setModified, setProcessing, setSubmitted, submit, + validateForm, } diff --git a/packages/ui/src/forms/Form/types.ts b/packages/ui/src/forms/Form/types.ts index cc40f0b6a..b8a33ee82 100644 --- a/packages/ui/src/forms/Form/types.ts +++ b/packages/ui/src/forms/Form/types.ts @@ -227,6 +227,11 @@ export type Context = { getFields: GetFields getSiblingData: GetSiblingData initializing: boolean + /** + * Tracks wether the form state passes validation. + * For example the state could be submitted but invalid as field errors have been returned. + */ + isValid: boolean removeFieldRow: ({ path, rowIndex }: { path: string; rowIndex: number }) => void replaceFieldRow: ({ blockType, @@ -244,6 +249,7 @@ export type Context = { replaceState: (state: FormState) => void reset: Reset setDisabled: (disabled: boolean) => void + setIsValid: (processing: boolean) => void setModified: SetModified setProcessing: SetProcessing setSubmitted: SetSubmitted diff --git a/test/helpers/waitForAutoSaveToRunAndComplete.ts b/test/helpers/waitForAutoSaveToRunAndComplete.ts index 0d01bab93..79e0f1a0b 100644 --- a/test/helpers/waitForAutoSaveToRunAndComplete.ts +++ b/test/helpers/waitForAutoSaveToRunAndComplete.ts @@ -4,20 +4,32 @@ import { expect } from '@playwright/test' import { wait } from 'payload/shared' import { POLL_TOPASS_TIMEOUT } from 'playwright.config.js' -export async function waitForAutoSaveToRunAndComplete(page: Page) { +export async function waitForAutoSaveToRunAndComplete( + page: Page, + expectation: 'error' | 'success' = 'success', +) { await expect(async () => { await expect(page.locator('.autosave:has-text("Saving...")')).toBeVisible() }).toPass({ timeout: POLL_TOPASS_TIMEOUT, + intervals: [50], }) await wait(500) - await expect(async () => { - await expect( - page.locator('.autosave:has-text("Last saved less than a minute ago")'), - ).toBeVisible() - }).toPass({ - timeout: POLL_TOPASS_TIMEOUT, - }) + if (expectation === 'success') { + await expect(async () => { + await expect( + page.locator('.autosave:has-text("Last saved less than a minute ago")'), + ).toBeVisible() + }).toPass({ + timeout: POLL_TOPASS_TIMEOUT, + }) + } else { + await expect(async () => { + await expect(page.locator('.payload-toast-container .toast-error')).toBeVisible() + }).toPass({ + timeout: POLL_TOPASS_TIMEOUT, + }) + } } diff --git a/test/versions/collections/AutosaveWithValidate.ts b/test/versions/collections/AutosaveWithValidate.ts new file mode 100644 index 000000000..f412a75e3 --- /dev/null +++ b/test/versions/collections/AutosaveWithValidate.ts @@ -0,0 +1,33 @@ +import type { CollectionConfig } from 'payload' + +import { autosaveWithValidateCollectionSlug } from '../slugs.js' + +const AutosaveWithValidatePosts: CollectionConfig = { + slug: autosaveWithValidateCollectionSlug, + labels: { + singular: 'Autosave with Validate Post', + plural: 'Autosave with Validate Posts', + }, + admin: { + useAsTitle: 'title', + defaultColumns: ['title', 'subtitle', 'createdAt', '_status'], + }, + versions: { + maxPerDoc: 35, + drafts: { + validate: true, + autosave: { + interval: 250, + }, + }, + }, + fields: [ + { + name: 'title', + type: 'text', + required: true, + }, + ], +} + +export default AutosaveWithValidatePosts diff --git a/test/versions/collections/DraftsWithValidate.ts b/test/versions/collections/DraftsWithValidate.ts new file mode 100644 index 000000000..65abf5fb8 --- /dev/null +++ b/test/versions/collections/DraftsWithValidate.ts @@ -0,0 +1,21 @@ +import type { CollectionConfig } from 'payload' + +import { draftWithValidateCollectionSlug } from '../slugs.js' + +const DraftsWithValidate: CollectionConfig = { + slug: draftWithValidateCollectionSlug, + fields: [ + { + name: 'title', + type: 'text', + required: true, + }, + ], + versions: { + drafts: { + validate: true, + }, + }, +} + +export default DraftsWithValidate diff --git a/test/versions/config.ts b/test/versions/config.ts index fdc46cfae..a60b7d13d 100644 --- a/test/versions/config.ts +++ b/test/versions/config.ts @@ -4,11 +4,13 @@ const filename = fileURLToPath(import.meta.url) const dirname = path.dirname(filename) import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js' import AutosavePosts from './collections/Autosave.js' +import AutosaveWithValidate from './collections/AutosaveWithValidate.js' import CustomIDs from './collections/CustomIDs.js' import { Diff } from './collections/Diff.js' import DisablePublish from './collections/DisablePublish.js' import DraftPosts from './collections/Drafts.js' import DraftWithMax from './collections/DraftsWithMax.js' +import DraftsWithValidate from './collections/DraftsWithValidate.js' import LocalizedPosts from './collections/Localized.js' import { Media } from './collections/Media.js' import Posts from './collections/Posts.js' @@ -32,8 +34,10 @@ export default buildConfigWithDefaults({ DisablePublish, Posts, AutosavePosts, + AutosaveWithValidate, DraftPosts, DraftWithMax, + DraftsWithValidate, LocalizedPosts, VersionPosts, CustomIDs, diff --git a/test/versions/e2e.spec.ts b/test/versions/e2e.spec.ts index ef9154f14..51043ab75 100644 --- a/test/versions/e2e.spec.ts +++ b/test/versions/e2e.spec.ts @@ -22,7 +22,7 @@ * - specify locales to show */ -import type { BrowserContext, Page } from '@playwright/test' +import type { BrowserContext, Dialog, Page } from '@playwright/test' import { expect, test } from '@playwright/test' import path from 'path' @@ -51,6 +51,7 @@ import { titleToDelete } from './shared.js' import { autosaveCollectionSlug, autoSaveGlobalSlug, + autosaveWithValidateCollectionSlug, customIDSlug, diffCollectionSlug, disablePublishGlobalSlug, @@ -59,6 +60,7 @@ import { draftGlobalSlug, draftWithMaxCollectionSlug, draftWithMaxGlobalSlug, + draftWithValidateCollectionSlug, localizedCollectionSlug, localizedGlobalSlug, postCollectionSlug, @@ -79,6 +81,8 @@ describe('Versions', () => { let url: AdminUrlUtil let serverURL: string let autosaveURL: AdminUrlUtil + let autosaveWithValidateURL: AdminUrlUtil + let draftWithValidateURL: AdminUrlUtil let disablePublishURL: AdminUrlUtil let customIDURL: AdminUrlUtil let postURL: AdminUrlUtil @@ -115,6 +119,7 @@ describe('Versions', () => { beforeAll(() => { url = new AdminUrlUtil(serverURL, draftCollectionSlug) autosaveURL = new AdminUrlUtil(serverURL, autosaveCollectionSlug) + autosaveWithValidateURL = new AdminUrlUtil(serverURL, autosaveWithValidateCollectionSlug) disablePublishURL = new AdminUrlUtil(serverURL, disablePublishSlug) customIDURL = new AdminUrlUtil(serverURL, customIDSlug) postURL = new AdminUrlUtil(serverURL, postCollectionSlug) @@ -812,6 +817,232 @@ describe('Versions', () => { }) }) + describe('Collections with draft validation', () => { + beforeAll(() => { + autosaveWithValidateURL = new AdminUrlUtil(serverURL, autosaveWithValidateCollectionSlug) + draftWithValidateURL = new AdminUrlUtil(serverURL, draftWithValidateCollectionSlug) + }) + + test('- can save', async () => { + await page.goto(draftWithValidateURL.create) + + const titleField = page.locator('#field-title') + await titleField.fill('Initial') + await saveDocAndAssert(page, '#action-save-draft') + + await expect(titleField).toBeEnabled() + await titleField.fill('New title') + await saveDocAndAssert(page, '#action-save-draft') + + await page.reload() + + // Ensure its saved + await expect(page.locator('#field-title')).toHaveValue('New title') + }) + + test('- can safely trigger validation errors and then continue editing', async () => { + await page.goto(draftWithValidateURL.create) + + const titleField = page.locator('#field-title') + await titleField.fill('Initial') + await saveDocAndAssert(page, '#action-save-draft') + await page.reload() + + await expect(titleField).toBeEnabled() + await titleField.fill('') + await saveDocAndAssert(page, '#action-save-draft', 'error') + + await titleField.fill('New title') + + await saveDocAndAssert(page, '#action-save-draft') + + await page.reload() + + // Ensure its saved + await expect(page.locator('#field-title')).toHaveValue('New title') + }) + + test('- shows a prevent leave alert when form is submitted but invalid', async () => { + await page.goto(draftWithValidateURL.create) + + // Flag to check against if window alert has been displayed and dismissed since we can only check via events + let alertDisplayed = false + + async function dismissAlert(dialog: Dialog) { + alertDisplayed = true + + await dialog.dismiss() + } + + async function acceptAlert(dialog: Dialog) { + await dialog.accept() + } + + const titleField = page.locator('#field-title') + await titleField.fill('Initial') + await saveDocAndAssert(page, '#action-save-draft') + + // Remove required data, then let autosave trigger + await expect(titleField).toBeEnabled() + await titleField.fill('') + await saveDocAndAssert(page, '#action-save-draft', 'error') + + // Expect the prevent leave and then dismiss it + page.on('dialog', dismissAlert) + await expect(async () => { + await page.reload({ timeout: 500 }) // custom short timeout since we want this to fail + }).not.toPass({ + timeout: POLL_TOPASS_TIMEOUT, + }) + + await expect(() => { + expect(alertDisplayed).toEqual(true) + }).toPass({ + timeout: POLL_TOPASS_TIMEOUT, + }) + + // Remove event listener and reset our flag + page.removeListener('dialog', dismissAlert) + + await expect(page.locator('#field-title')).toHaveValue('') + + // Now has updated data + await titleField.fill('New title') + await saveDocAndAssert(page, '#action-save-draft') + await expect(page.locator('#field-title')).toHaveValue('New title') + + await page.reload() + + page.on('dialog', acceptAlert) + + // Ensure data is saved + await expect(page.locator('#field-title')).toHaveValue('New title') + + // Fill with invalid data again, then reload and accept the warning, should contain previous data + await titleField.fill('') + + await page.reload() + + await expect(titleField).toBeEnabled() + + // Contains previous data + await expect(page.locator('#field-title')).toHaveValue('New title') + + // Remove listener + page.removeListener('dialog', acceptAlert) + }) + + test('- with autosave - can save', async () => { + await page.goto(autosaveWithValidateURL.create) + + const titleField = page.locator('#field-title') + await titleField.fill('Initial') + await saveDocAndAssert(page, '#action-save-draft') + + await expect(titleField).toBeEnabled() + await titleField.fill('New title') + await waitForAutoSaveToRunAndComplete(page) + + await page.reload() + + // Ensure its saved + await expect(page.locator('#field-title')).toHaveValue('New title') + }) + + test('- with autosave - can safely trigger validation errors and then continue editing', async () => { + // This test has to make sure we don't enter an infinite loop when draft.validate is on and we have autosave enabled + await page.goto(autosaveWithValidateURL.create) + + const titleField = page.locator('#field-title') + await titleField.fill('Initial') + await saveDocAndAssert(page, '#action-save-draft') + await page.reload() + + await expect(titleField).toBeEnabled() + await titleField.fill('') + await waitForAutoSaveToRunAndComplete(page, 'error') + + await titleField.fill('New title') + + await waitForAutoSaveToRunAndComplete(page) + + await page.reload() + + // Ensure its saved + await expect(page.locator('#field-title')).toHaveValue('New title') + }) + + test('- with autosave - shows a prevent leave alert when form is submitted but invalid', async () => { + await page.goto(autosaveWithValidateURL.create) + + // Flag to check against if window alert has been displayed and dismissed since we can only check via events + let alertDisplayed = false + + async function dismissAlert(dialog: Dialog) { + alertDisplayed = true + + await dialog.dismiss() + } + + async function acceptAlert(dialog: Dialog) { + await dialog.accept() + } + + const titleField = page.locator('#field-title') + await titleField.fill('Initial') + await saveDocAndAssert(page, '#action-save-draft') + + // Remove required data, then let autosave trigger + await expect(titleField).toBeEnabled() + await titleField.fill('') + await waitForAutoSaveToRunAndComplete(page, 'error') + + // Expect the prevent leave and then dismiss it + page.on('dialog', dismissAlert) + await expect(async () => { + await page.reload({ timeout: 500 }) // custom short timeout since we want this to fail + }).not.toPass({ + timeout: POLL_TOPASS_TIMEOUT, + }) + + await expect(() => { + expect(alertDisplayed).toEqual(true) + }).toPass({ + timeout: POLL_TOPASS_TIMEOUT, + }) + + // Remove event listener and reset our flag + page.removeListener('dialog', dismissAlert) + + await expect(page.locator('#field-title')).toHaveValue('') + + // Now has updated data + await titleField.fill('New title') + await waitForAutoSaveToRunAndComplete(page) + await expect(page.locator('#field-title')).toHaveValue('New title') + + await page.reload() + + page.on('dialog', acceptAlert) + + // Ensure data is saved + await expect(page.locator('#field-title')).toHaveValue('New title') + + // Fill with invalid data again, then reload and accept the warning, should contain previous data + await titleField.fill('') + + await page.reload() + + await expect(titleField).toBeEnabled() + + // Contains previous data + await expect(page.locator('#field-title')).toHaveValue('New title') + + // Remove listener + page.removeListener('dialog', acceptAlert) + }) + }) + describe('Globals - publish individual locale', () => { beforeAll(() => { url = new AdminUrlUtil(serverURL, localizedGlobalSlug) diff --git a/test/versions/payload-types.ts b/test/versions/payload-types.ts index 8e681731f..ca3849e1d 100644 --- a/test/versions/payload-types.ts +++ b/test/versions/payload-types.ts @@ -69,8 +69,10 @@ export interface Config { 'disable-publish': DisablePublish; posts: Post; 'autosave-posts': AutosavePost; + 'autosave-with-validate-posts': AutosaveWithValidatePost; 'draft-posts': DraftPost; 'draft-with-max-posts': DraftWithMaxPost; + 'draft-with-validate-posts': DraftWithValidatePost; 'localized-posts': LocalizedPost; 'version-posts': VersionPost; 'custom-ids': CustomId; @@ -87,8 +89,10 @@ export interface Config { 'disable-publish': DisablePublishSelect | DisablePublishSelect; posts: PostsSelect | PostsSelect; 'autosave-posts': AutosavePostsSelect | AutosavePostsSelect; + 'autosave-with-validate-posts': AutosaveWithValidatePostsSelect | AutosaveWithValidatePostsSelect; 'draft-posts': DraftPostsSelect | DraftPostsSelect; 'draft-with-max-posts': DraftWithMaxPostsSelect | DraftWithMaxPostsSelect; + 'draft-with-validate-posts': DraftWithValidatePostsSelect | DraftWithValidatePostsSelect; 'localized-posts': LocalizedPostsSelect | LocalizedPostsSelect; 'version-posts': VersionPostsSelect | VersionPostsSelect; 'custom-ids': CustomIdsSelect | CustomIdsSelect; @@ -221,6 +225,17 @@ export interface DraftPost { createdAt: string; _status?: ('draft' | 'published') | null; } +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "autosave-with-validate-posts". + */ +export interface AutosaveWithValidatePost { + id: string; + title: string; + updatedAt: string; + createdAt: string; + _status?: ('draft' | 'published') | null; +} /** * This interface was referenced by `Config`'s JSON-Schema * via the `definition` "draft-with-max-posts". @@ -245,6 +260,17 @@ export interface DraftWithMaxPost { createdAt: string; _status?: ('draft' | 'published') | null; } +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "draft-with-validate-posts". + */ +export interface DraftWithValidatePost { + id: string; + title: string; + updatedAt: string; + createdAt: string; + _status?: ('draft' | 'published') | null; +} /** * This interface was referenced by `Config`'s JSON-Schema * via the `definition` "localized-posts". @@ -497,6 +523,10 @@ export interface PayloadLockedDocument { relationTo: 'autosave-posts'; value: string | AutosavePost; } | null) + | ({ + relationTo: 'autosave-with-validate-posts'; + value: string | AutosaveWithValidatePost; + } | null) | ({ relationTo: 'draft-posts'; value: string | DraftPost; @@ -505,6 +535,10 @@ export interface PayloadLockedDocument { relationTo: 'draft-with-max-posts'; value: string | DraftWithMaxPost; } | null) + | ({ + relationTo: 'draft-with-validate-posts'; + value: string | DraftWithValidatePost; + } | null) | ({ relationTo: 'localized-posts'; value: string | LocalizedPost; @@ -607,6 +641,16 @@ export interface AutosavePostsSelect { createdAt?: T; _status?: T; } +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "autosave-with-validate-posts_select". + */ +export interface AutosaveWithValidatePostsSelect { + title?: T; + updatedAt?: T; + createdAt?: T; + _status?: T; +} /** * This interface was referenced by `Config`'s JSON-Schema * via the `definition` "draft-posts_select". @@ -660,6 +704,16 @@ export interface DraftWithMaxPostsSelect { createdAt?: T; _status?: T; } +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "draft-with-validate-posts_select". + */ +export interface DraftWithValidatePostsSelect { + title?: T; + updatedAt?: T; + createdAt?: T; + _status?: T; +} /** * This interface was referenced by `Config`'s JSON-Schema * via the `definition` "localized-posts_select". diff --git a/test/versions/seed.ts b/test/versions/seed.ts index 40644db89..5e255219e 100644 --- a/test/versions/seed.ts +++ b/test/versions/seed.ts @@ -7,7 +7,12 @@ import type { DraftPost } from './payload-types.js' import { devUser } from '../credentials.js' import { executePromises } from '../helpers/executePromises.js' import { titleToDelete } from './shared.js' -import { diffCollectionSlug, draftCollectionSlug, mediaCollectionSlug } from './slugs.js' +import { + autosaveWithValidateCollectionSlug, + diffCollectionSlug, + draftCollectionSlug, + mediaCollectionSlug, +} from './slugs.js' import { textToLexicalJSON } from './textToLexicalJSON.js' const filename = fileURLToPath(import.meta.url) @@ -120,6 +125,13 @@ export async function seed(_payload: Payload, parallel: boolean = false) { draft: true, }) + await _payload.create({ + collection: autosaveWithValidateCollectionSlug, + data: { + title: 'Initial seeded title', + }, + }) + const diffDoc = await _payload.create({ collection: diffCollectionSlug, locale: 'en', diff --git a/test/versions/slugs.ts b/test/versions/slugs.ts index 007aa3dd5..42f87526c 100644 --- a/test/versions/slugs.ts +++ b/test/versions/slugs.ts @@ -1,8 +1,12 @@ export const autosaveCollectionSlug = 'autosave-posts' +export const autosaveWithValidateCollectionSlug = 'autosave-with-validate-posts' + export const customIDSlug = 'custom-ids' export const draftCollectionSlug = 'draft-posts' + +export const draftWithValidateCollectionSlug = 'draft-with-validate-posts' export const draftWithMaxCollectionSlug = 'draft-with-max-posts' export const postCollectionSlug = 'posts'