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'