From 0b60bf2eff4af148174cd77bdba38e06010bc390 Mon Sep 17 00:00:00 2001 From: jacobsfletch Date: Thu, 14 Aug 2025 19:36:02 -0400 Subject: [PATCH] fix(ui): significantly more predictable autosave form state (#13460) --- docs/performance/overview.mdx | 2 +- packages/payload/src/admin/forms/Form.ts | 14 +++- packages/ui/src/elements/Autosave/index.tsx | 4 +- .../src/elements/DocumentDrawer/Provider.tsx | 2 +- packages/ui/src/forms/Form/fieldReducer.ts | 13 +++- packages/ui/src/forms/Form/index.tsx | 19 +++-- .../ui/src/forms/Form/mergeServerFormState.ts | 9 ++- packages/ui/src/forms/Form/types.ts | 23 +++--- .../forms/fieldSchemasToFormState/index.tsx | 2 +- .../fieldSchemasToFormState/iterateFields.ts | 1 - packages/ui/src/utilities/buildFormState.ts | 2 + packages/ui/src/views/Edit/index.tsx | 16 +++-- .../form-state/collections/Autosave/index.tsx | 31 ++++++++ test/form-state/collections/Posts/index.ts | 8 +++ test/form-state/config.ts | 3 +- test/form-state/e2e.spec.ts | 57 +++++++++++++++ test/form-state/int.spec.ts | 70 +++++++++---------- test/form-state/payload-types.ts | 45 ++++++++++++ test/versions/collections/Autosave.ts | 8 +++ test/versions/payload-types.ts | 26 +++++++ 20 files changed, 278 insertions(+), 77 deletions(-) create mode 100644 test/form-state/collections/Autosave/index.tsx diff --git a/docs/performance/overview.mdx b/docs/performance/overview.mdx index 274312d4c..9756ca45f 100644 --- a/docs/performance/overview.mdx +++ b/docs/performance/overview.mdx @@ -207,7 +207,7 @@ Everything mentioned above applies to local development as well, but there are a ### Enable Turbopack - **Note:** In the future this will be the default. Use as your own risk. + **Note:** In the future this will be the default. Use at your own risk. Add `--turbo` to your dev script to significantly speed up your local development server start time. diff --git a/packages/payload/src/admin/forms/Form.ts b/packages/payload/src/admin/forms/Form.ts index 42e5cfebc..97dbc5589 100644 --- a/packages/payload/src/admin/forms/Form.ts +++ b/packages/payload/src/admin/forms/Form.ts @@ -56,6 +56,12 @@ export type FieldState = { fieldSchema?: Field filterOptions?: FilterOptionsResult initialValue?: unknown + /** + * @experimental - Note: this property is experimental and may change in the future. Use at your own discretion. + * Every time a field is changed locally, this flag is set to true. Prevents form state from server from overwriting local changes. + * After merging server form state, this flag is reset. + */ + isModified?: boolean /** * The path of the field when its custom components were last rendered. * This is used to denote if a field has been rendered, and if so, @@ -114,9 +120,11 @@ export type BuildFormStateArgs = { mockRSCs?: boolean operation?: 'create' | 'update' readOnly?: boolean - /* - If true, will render field components within their state object - */ + /** + * If true, will render field components within their state object. + * Performance optimization: Setting to `false` ensures that only fields that have changed paths will re-render, e.g. new array rows, etc. + * For example, you only need to render ALL fields on initial render, not on every onChange. + */ renderAllFields?: boolean req: PayloadRequest returnLockStatus?: boolean diff --git a/packages/ui/src/elements/Autosave/index.tsx b/packages/ui/src/elements/Autosave/index.tsx index 96b121792..54628cc8c 100644 --- a/packages/ui/src/elements/Autosave/index.tsx +++ b/packages/ui/src/elements/Autosave/index.tsx @@ -149,7 +149,9 @@ export const Autosave: React.FC = ({ id, collection, global: globalDoc }) submitted && !valid && versionsConfig?.drafts && versionsConfig?.drafts?.validate if (!skipSubmission && modifiedRef.current && url) { - const result = await submit({ + const result = await submit<{ + incrementVersionCount: boolean + }>({ acceptValues: { overrideLocalChanges: false, }, diff --git a/packages/ui/src/elements/DocumentDrawer/Provider.tsx b/packages/ui/src/elements/DocumentDrawer/Provider.tsx index 1bc466905..a4bdf7dd1 100644 --- a/packages/ui/src/elements/DocumentDrawer/Provider.tsx +++ b/packages/ui/src/elements/DocumentDrawer/Provider.tsx @@ -21,7 +21,7 @@ export type DocumentDrawerContextProps = { readonly onSave?: (args: { collectionConfig?: ClientCollectionConfig /** - * @experimental - Note: this property is experimental and may change in the future. Use as your own discretion. + * @experimental - Note: this property is experimental and may change in the future. Use at your own discretion. * If you want to pass additional data to the onSuccess callback, you can use this context object. */ context?: Record diff --git a/packages/ui/src/forms/Form/fieldReducer.ts b/packages/ui/src/forms/Form/fieldReducer.ts index a2910cee8..3dfadf09f 100644 --- a/packages/ui/src/forms/Form/fieldReducer.ts +++ b/packages/ui/src/forms/Form/fieldReducer.ts @@ -189,12 +189,11 @@ export function fieldReducer(state: FormState, action: FieldAction): FormState { } case 'MERGE_SERVER_STATE': { - const { acceptValues, formStateAtTimeOfRequest, prevStateRef, serverState } = action + const { acceptValues, prevStateRef, serverState } = action const newState = mergeServerFormState({ acceptValues, currentState: state || {}, - formStateAtTimeOfRequest, incomingState: serverState, }) @@ -385,6 +384,7 @@ export function fieldReducer(state: FormState, action: FieldAction): FormState { return { ...field, [key]: value, + ...(key === 'value' ? { isModified: true } : {}), } } @@ -398,6 +398,15 @@ export function fieldReducer(state: FormState, action: FieldAction): FormState { [action.path]: newField, } + // reset `isModified` in all other fields + if ('value' in action) { + for (const [path, field] of Object.entries(newState)) { + if (path !== action.path && 'isModified' in field) { + delete newState[path].isModified + } + } + } + return newState } diff --git a/packages/ui/src/forms/Form/index.tsx b/packages/ui/src/forms/Form/index.tsx index aa545e499..59d5006f1 100644 --- a/packages/ui/src/forms/Form/index.tsx +++ b/packages/ui/src/forms/Form/index.tsx @@ -264,23 +264,21 @@ export const Form: React.FC = (props) => { await wait(100) } - /** - * Take copies of the current form state and data here. This will ensure it is consistent. - * For example, it is possible for the form state ref to change in the background while this submit function is running. - * TODO: can we send the `formStateCopy` through `reduceFieldsToValues` to even greater consistency? Doing this currently breaks uploads. - */ - const formStateCopy = deepCopyObjectSimpleWithoutReactComponents(contextRef.current.fields) const data = reduceFieldsToValues(contextRef.current.fields, true) // Execute server side validations if (Array.isArray(beforeSubmit)) { + const serializableFormState = deepCopyObjectSimpleWithoutReactComponents( + contextRef.current.fields, + ) + let revalidatedFormState: FormState await beforeSubmit.reduce(async (priorOnChange, beforeSubmitFn) => { await priorOnChange const result = await beforeSubmitFn({ - formState: formStateCopy, + formState: serializableFormState, }) revalidatedFormState = result @@ -328,7 +326,7 @@ export const Form: React.FC = (props) => { data[key] = value } - onSubmit(formStateCopy, data) + onSubmit(contextRef.current.fields, data) } if (!hasFormSubmitAction) { @@ -379,13 +377,14 @@ export const Form: React.FC = (props) => { if (res.status < 400) { if (typeof onSuccess === 'function') { - const newFormState = await onSuccess(json, context) + const newFormState = await onSuccess(json, { + context, + }) if (newFormState) { dispatchFields({ type: 'MERGE_SERVER_STATE', acceptValues, - formStateAtTimeOfRequest: formStateCopy, prevStateRef: prevFormState, serverState: newFormState, }) diff --git a/packages/ui/src/forms/Form/mergeServerFormState.ts b/packages/ui/src/forms/Form/mergeServerFormState.ts index adfd6062f..2c1f2bf36 100644 --- a/packages/ui/src/forms/Form/mergeServerFormState.ts +++ b/packages/ui/src/forms/Form/mergeServerFormState.ts @@ -21,7 +21,6 @@ export type AcceptValues = type Args = { acceptValues?: AcceptValues currentState?: FormState - formStateAtTimeOfRequest?: FormState incomingState: FormState } @@ -37,7 +36,6 @@ type Args = { export const mergeServerFormState = ({ acceptValues, currentState = {}, - formStateAtTimeOfRequest, incomingState, }: Args): FormState => { const newState = { ...currentState } @@ -49,8 +47,9 @@ export const mergeServerFormState = ({ /** * If it's a new field added by the server, always accept the value. - * Otherwise, only accept the values if explicitly requested, e.g. on submit. - * Can also control this granularly by only accepting unmodified values, e.g. for autosave. + * Otherwise: + * a. accept all values when explicitly requested, e.g. on submit + * b. only accept values for unmodified fields, e.g. on autosave */ if ( !incomingField.addedByServer && @@ -59,7 +58,7 @@ export const mergeServerFormState = ({ (typeof acceptValues === 'object' && acceptValues !== null && acceptValues?.overrideLocalChanges === false && - currentState[path]?.value !== formStateAtTimeOfRequest?.[path]?.value)) + currentState[path].isModified)) ) { delete incomingField.value delete incomingField.initialValue diff --git a/packages/ui/src/forms/Form/types.ts b/packages/ui/src/forms/Form/types.ts index a52ed3466..d6047ef01 100644 --- a/packages/ui/src/forms/Form/types.ts +++ b/packages/ui/src/forms/Form/types.ts @@ -54,7 +54,15 @@ export type FormProps = { log?: boolean onChange?: ((args: { formState: FormState; submitted?: boolean }) => Promise)[] onSubmit?: (fields: FormState, data: Data) => void - onSuccess?: (json: unknown, context?: Record) => Promise | void + onSuccess?: ( + json: unknown, + options?: { + /** + * Arbitrary context passed to the onSuccess callback. + */ + context?: Record + }, + ) => Promise | void redirect?: string submitted?: boolean uuid?: string @@ -70,14 +78,14 @@ export type FormProps = { } ) -export type SubmitOptions = { +export type SubmitOptions> = { acceptValues?: AcceptValues action?: string /** - * @experimental - Note: this property is experimental and may change in the future. Use as your own discretion. + * @experimental - Note: this property is experimental and may change in the future. Use at your own discretion. * If you want to pass additional data to the onSuccess callback, you can use this context object. */ - context?: Record + context?: T /** * When true, will disable the form while it is processing. * @default true @@ -99,11 +107,11 @@ export type SubmitOptions = { export type DispatchFields = React.Dispatch -export type Submit = ( - options?: SubmitOptions, +export type Submit = >( + options?: SubmitOptions, e?: React.FormEvent, ) => Promise @@ -185,7 +193,6 @@ export type ADD_ROW = { export type MERGE_SERVER_STATE = { acceptValues?: AcceptValues - formStateAtTimeOfRequest?: FormState prevStateRef: React.RefObject serverState: FormState type: 'MERGE_SERVER_STATE' diff --git a/packages/ui/src/forms/fieldSchemasToFormState/index.tsx b/packages/ui/src/forms/fieldSchemasToFormState/index.tsx index f1f7f4725..741f139dc 100644 --- a/packages/ui/src/forms/fieldSchemasToFormState/index.tsx +++ b/packages/ui/src/forms/fieldSchemasToFormState/index.tsx @@ -5,7 +5,6 @@ import type { DocumentPreferences, Field, FieldSchemaMap, - FieldState, FormState, FormStateWithoutComponents, PayloadRequest, @@ -105,6 +104,7 @@ export const fieldSchemasToFormState = async ({ skipValidation, }: Args): Promise => { if (!clientFieldSchemaMap && renderFieldFn) { + // eslint-disable-next-line no-console console.warn( 'clientFieldSchemaMap is not passed to fieldSchemasToFormState - this will reduce performance', ) diff --git a/packages/ui/src/forms/fieldSchemasToFormState/iterateFields.ts b/packages/ui/src/forms/fieldSchemasToFormState/iterateFields.ts index 026af2715..766a47bf9 100644 --- a/packages/ui/src/forms/fieldSchemasToFormState/iterateFields.ts +++ b/packages/ui/src/forms/fieldSchemasToFormState/iterateFields.ts @@ -5,7 +5,6 @@ import type { DocumentPreferences, Field as FieldSchema, FieldSchemaMap, - FieldState, FormState, FormStateWithoutComponents, PayloadRequest, diff --git a/packages/ui/src/utilities/buildFormState.ts b/packages/ui/src/utilities/buildFormState.ts index 1c718d1e5..f8ab5c21e 100644 --- a/packages/ui/src/utilities/buildFormState.ts +++ b/packages/ui/src/utilities/buildFormState.ts @@ -182,11 +182,13 @@ export const buildFormState = async ( } let documentData = undefined + if (documentFormState) { documentData = reduceFieldsToValues(documentFormState, true) } let blockData = initialBlockData + if (initialBlockFormState) { blockData = reduceFieldsToValues(initialBlockFormState, true) } diff --git a/packages/ui/src/views/Edit/index.tsx b/packages/ui/src/views/Edit/index.tsx index 1e78cf46a..15e8f8fb4 100644 --- a/packages/ui/src/views/Edit/index.tsx +++ b/packages/ui/src/views/Edit/index.tsx @@ -1,7 +1,7 @@ /* eslint-disable react-compiler/react-compiler -- TODO: fix */ 'use client' -import type { ClientUser, DocumentViewClientProps, FormState } from 'payload' +import type { ClientUser, DocumentViewClientProps } from 'payload' import { useRouter, useSearchParams } from 'next/navigation.js' import { formatAdminURL } from 'payload/shared' @@ -256,10 +256,13 @@ export function DefaultEditView({ user?.id, ]) - const onSave = useCallback( - async (json, context?: Record): Promise => { + const onSave = useCallback( + async (json, options) => { + const { context } = options || {} + const controller = handleAbortRef(abortOnSaveRef) + // @ts-expect-error can ignore const document = json?.doc || json?.result const updatedAt = document?.updatedAt || new Date().toISOString() @@ -290,9 +293,10 @@ export function DefaultEditView({ const operation = id ? 'update' : 'create' void onSaveFromContext({ - ...json, + ...(json as Record), context, operation, + // @ts-expect-error todo: this is not right, should be under `doc`? updatedAt: operation === 'update' ? new Date().toISOString() @@ -397,13 +401,11 @@ export function DefaultEditView({ formState: prevFormState, globalSlug, operation, - skipValidation: !submitted, - // Performance optimization: Setting it to false ensure that only fields that have explicit requireRender set in the form state will be rendered (e.g. new array rows). - // We only want to render ALL fields on initial render, not in onChange. renderAllFields: false, returnLockStatus: isLockingEnabled, schemaPath: schemaPathSegments.join('.'), signal: controller.signal, + skipValidation: !submitted, updateLastEdited, }) diff --git a/test/form-state/collections/Autosave/index.tsx b/test/form-state/collections/Autosave/index.tsx new file mode 100644 index 000000000..939b4c463 --- /dev/null +++ b/test/form-state/collections/Autosave/index.tsx @@ -0,0 +1,31 @@ +import type { CollectionConfig } from 'payload' + +export const autosavePostsSlug = 'autosave-posts' + +export const AutosavePostsCollection: CollectionConfig = { + slug: autosavePostsSlug, + admin: { + useAsTitle: 'title', + }, + fields: [ + { + name: 'title', + type: 'text', + }, + { + name: 'computedTitle', + type: 'text', + hooks: { + beforeChange: [({ data }) => data?.title], + }, + label: 'Computed Title', + }, + ], + versions: { + drafts: { + autosave: { + interval: 100, + }, + }, + }, +} diff --git a/test/form-state/collections/Posts/index.ts b/test/form-state/collections/Posts/index.ts index 54183c26f..da28f9320 100644 --- a/test/form-state/collections/Posts/index.ts +++ b/test/form-state/collections/Posts/index.ts @@ -12,6 +12,14 @@ export const PostsCollection: CollectionConfig = { name: 'title', type: 'text', }, + { + name: 'computedTitle', + type: 'text', + hooks: { + beforeChange: [({ data }) => data?.title], + }, + label: 'Computed Title', + }, { name: 'renderTracker', type: 'text', diff --git a/test/form-state/config.ts b/test/form-state/config.ts index 07145a705..61338952b 100644 --- a/test/form-state/config.ts +++ b/test/form-state/config.ts @@ -3,13 +3,14 @@ import path from 'path' import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js' import { devUser } from '../credentials.js' +import { AutosavePostsCollection } from './collections/Autosave/index.js' import { PostsCollection, postsSlug } from './collections/Posts/index.js' const filename = fileURLToPath(import.meta.url) const dirname = path.dirname(filename) export default buildConfigWithDefaults({ - collections: [PostsCollection], + collections: [PostsCollection, AutosavePostsCollection], admin: { importMap: { baseDir: path.resolve(dirname), diff --git a/test/form-state/e2e.spec.ts b/test/form-state/e2e.spec.ts index c71ad322b..f141d96d0 100644 --- a/test/form-state/e2e.spec.ts +++ b/test/form-state/e2e.spec.ts @@ -7,6 +7,7 @@ import { addBlock } from 'helpers/e2e/addBlock.js' import { assertElementStaysVisible } from 'helpers/e2e/assertElementStaysVisible.js' import { assertNetworkRequests } from 'helpers/e2e/assertNetworkRequests.js' import { assertRequestBody } from 'helpers/e2e/assertRequestBody.js' +import { waitForAutoSaveToRunAndComplete } from 'helpers/e2e/waitForAutoSaveToRunAndComplete.js' import * as path from 'path' import { fileURLToPath } from 'url' @@ -21,6 +22,7 @@ import { import { AdminUrlUtil } from '../helpers/adminUrlUtil.js' import { initPayloadE2ENoConfig } from '../helpers/initPayloadE2ENoConfig.js' import { TEST_TIMEOUT, TEST_TIMEOUT_LONG } from '../playwright.config.js' +import { autosavePostsSlug } from './collections/Autosave/index.js' import { postsSlug } from './collections/Posts/index.js' const { describe, beforeEach, afterEach } = test @@ -36,11 +38,13 @@ let serverURL: string test.describe('Form State', () => { let page: Page let postsUrl: AdminUrlUtil + let autosavePostsUrl: AdminUrlUtil test.beforeAll(async ({ browser }, testInfo) => { testInfo.setTimeout(TEST_TIMEOUT_LONG) ;({ payload, serverURL } = await initPayloadE2ENoConfig({ dirname })) postsUrl = new AdminUrlUtil(serverURL, postsSlug) + autosavePostsUrl = new AdminUrlUtil(serverURL, autosavePostsSlug) context = await browser.newContext() page = await context.newPage() @@ -296,6 +300,59 @@ test.describe('Form State', () => { await cdpSession.detach() }) + test('should render computed values after save', async () => { + await page.goto(postsUrl.create) + const titleField = page.locator('#field-title') + const computedTitleField = page.locator('#field-computedTitle') + + await titleField.fill('Test Title') + + await expect(computedTitleField).toHaveValue('') + + await saveDocAndAssert(page) + + await expect(computedTitleField).toHaveValue('Test Title') + }) + + test('autosave - should render computed values after autosave', async () => { + await page.goto(autosavePostsUrl.create) + const titleField = page.locator('#field-title') + const computedTitleField = page.locator('#field-computedTitle') + + await titleField.fill('Test Title') + + await waitForAutoSaveToRunAndComplete(page) + + await expect(computedTitleField).toHaveValue('Test Title') + }) + + test('autosave - should not overwrite computed values that are being actively edited', async () => { + await page.goto(autosavePostsUrl.create) + const titleField = page.locator('#field-title') + const computedTitleField = page.locator('#field-computedTitle') + + await titleField.fill('Test Title') + + await expect(computedTitleField).toHaveValue('Test Title') + + // Put cursor at end of text + await computedTitleField.evaluate((el: HTMLInputElement) => { + el.focus() + el.setSelectionRange(el.value.length, el.value.length) + }) + + await computedTitleField.pressSequentially(' - Edited', { delay: 100 }) + + await waitForAutoSaveToRunAndComplete(page) + + await expect(computedTitleField).toHaveValue('Test Title - Edited') + + // but then when editing another field, the computed field should update + await titleField.fill('Test Title 2') + await waitForAutoSaveToRunAndComplete(page) + await expect(computedTitleField).toHaveValue('Test Title 2') + }) + describe('Throttled tests', () => { let cdpSession: CDPSession diff --git a/test/form-state/int.spec.ts b/test/form-state/int.spec.ts index a417b41f8..71a36db3c 100644 --- a/test/form-state/int.spec.ts +++ b/test/form-state/int.spec.ts @@ -1,4 +1,4 @@ -import type { FormState, Payload, User } from 'payload' +import type { FieldState, FormState, Payload, User } from 'payload' import { buildFormState } from '@payloadcms/ui/utilities/buildFormState' import path from 'path' @@ -567,12 +567,17 @@ describe('Form State', () => { }) it('should accept all values from the server regardless of local modifications, e.g. on submit', () => { - const currentState = { + const title: FieldState = { + value: 'Test Post (modified on the client)', + initialValue: 'Test Post', + valid: true, + passesCondition: true, + } + + const currentState: Record = { title: { - value: 'Test Post (modified on the client)', - initialValue: 'Test Post', - valid: true, - passesCondition: true, + ...title, + isModified: true, }, computedTitle: { value: 'Test Post (computed on the client)', @@ -582,17 +587,7 @@ describe('Form State', () => { }, } - const formStateAtTimeOfRequest = { - ...currentState, - title: { - value: 'Test Post (modified on the client 2)', - initialValue: 'Test Post', - valid: true, - passesCondition: true, - }, - } - - const incomingStateFromServer = { + const incomingStateFromServer: Record = { title: { value: 'Test Post (modified on the server)', initialValue: 'Test Post', @@ -610,20 +605,30 @@ describe('Form State', () => { const newState = mergeServerFormState({ acceptValues: true, currentState, - formStateAtTimeOfRequest, incomingState: incomingStateFromServer, }) - expect(newState).toStrictEqual(incomingStateFromServer) + expect(newState).toStrictEqual({ + ...incomingStateFromServer, + title: { + ...incomingStateFromServer.title, + isModified: true, + }, + }) }) it('should not accept values from the server if they have been modified locally since the request was made, e.g. on autosave', () => { - const currentState = { + const title: FieldState = { + value: 'Test Post (modified on the client 1)', + initialValue: 'Test Post', + valid: true, + passesCondition: true, + } + + const currentState: Record = { title: { - value: 'Test Post (modified on the client 1)', - initialValue: 'Test Post', - valid: true, - passesCondition: true, + ...title, + isModified: true, }, computedTitle: { value: 'Test Post', @@ -633,17 +638,7 @@ describe('Form State', () => { }, } - const formStateAtTimeOfRequest = { - ...currentState, - title: { - value: 'Test Post (modified on the client 2)', - initialValue: 'Test Post', - valid: true, - passesCondition: true, - }, - } - - const incomingStateFromServer = { + const incomingStateFromServer: Record = { title: { value: 'Test Post (modified on the server)', initialValue: 'Test Post', @@ -661,12 +656,15 @@ describe('Form State', () => { const newState = mergeServerFormState({ acceptValues: { overrideLocalChanges: false }, currentState, - formStateAtTimeOfRequest, incomingState: incomingStateFromServer, }) expect(newState).toStrictEqual({ ...currentState, + title: { + ...currentState.title, + isModified: true, + }, computedTitle: incomingStateFromServer.computedTitle, // This field was not modified locally, so should be updated from the server }) }) diff --git a/test/form-state/payload-types.ts b/test/form-state/payload-types.ts index dd00cd42e..adfb458a7 100644 --- a/test/form-state/payload-types.ts +++ b/test/form-state/payload-types.ts @@ -68,6 +68,7 @@ export interface Config { blocks: {}; collections: { posts: Post; + 'autosave-posts': AutosavePost; users: User; 'payload-locked-documents': PayloadLockedDocument; 'payload-preferences': PayloadPreference; @@ -76,6 +77,7 @@ export interface Config { collectionsJoins: {}; collectionsSelect: { posts: PostsSelect | PostsSelect; + 'autosave-posts': AutosavePostsSelect | AutosavePostsSelect; users: UsersSelect | UsersSelect; 'payload-locked-documents': PayloadLockedDocumentsSelect | PayloadLockedDocumentsSelect; 'payload-preferences': PayloadPreferencesSelect | PayloadPreferencesSelect; @@ -120,6 +122,7 @@ export interface UserAuthOperations { export interface Post { id: string; title?: string | null; + computedTitle?: string | null; renderTracker?: string | null; /** * This field should only validate on submit. Try typing "Not allowed" and submitting the form. @@ -151,6 +154,18 @@ export interface Post { updatedAt: string; createdAt: string; } +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "autosave-posts". + */ +export interface AutosavePost { + id: string; + title?: string | null; + computedTitle?: string | null; + updatedAt: string; + createdAt: string; + _status?: ('draft' | 'published') | null; +} /** * This interface was referenced by `Config`'s JSON-Schema * via the `definition` "users". @@ -166,6 +181,13 @@ export interface User { hash?: string | null; loginAttempts?: number | null; lockUntil?: string | null; + sessions?: + | { + id: string; + createdAt?: string | null; + expiresAt: string; + }[] + | null; password?: string | null; } /** @@ -179,6 +201,10 @@ export interface PayloadLockedDocument { relationTo: 'posts'; value: string | Post; } | null) + | ({ + relationTo: 'autosave-posts'; + value: string | AutosavePost; + } | null) | ({ relationTo: 'users'; value: string | User; @@ -231,6 +257,7 @@ export interface PayloadMigration { */ export interface PostsSelect { title?: T; + computedTitle?: T; renderTracker?: T; validateUsingEvent?: T; blocks?: @@ -261,6 +288,17 @@ export interface PostsSelect { updatedAt?: T; createdAt?: T; } +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "autosave-posts_select". + */ +export interface AutosavePostsSelect { + title?: T; + computedTitle?: T; + updatedAt?: T; + createdAt?: T; + _status?: T; +} /** * This interface was referenced by `Config`'s JSON-Schema * via the `definition` "users_select". @@ -275,6 +313,13 @@ export interface UsersSelect { hash?: T; loginAttempts?: T; lockUntil?: T; + sessions?: + | T + | { + id?: T; + createdAt?: T; + expiresAt?: T; + }; } /** * This interface was referenced by `Config`'s JSON-Schema diff --git a/test/versions/collections/Autosave.ts b/test/versions/collections/Autosave.ts index fff406223..5f0df3b64 100644 --- a/test/versions/collections/Autosave.ts +++ b/test/versions/collections/Autosave.ts @@ -61,6 +61,14 @@ const AutosavePosts: CollectionConfig = { beforeChange: [({ data }) => data?.title], }, }, + { + name: 'richText', + type: 'richText', + }, + { + name: 'json', + type: 'json', + }, { name: 'description', label: 'Description', diff --git a/test/versions/payload-types.ts b/test/versions/payload-types.ts index 5090bd68d..52f8bec83 100644 --- a/test/versions/payload-types.ts +++ b/test/versions/payload-types.ts @@ -198,6 +198,30 @@ export interface AutosavePost { id: string; title: string; computedTitle?: string | null; + richText?: { + root: { + type: string; + children: { + type: string; + version: number; + [k: string]: unknown; + }[]; + direction: ('ltr' | 'rtl') | null; + format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | ''; + indent: number; + version: number; + }; + [k: string]: unknown; + } | null; + json?: + | { + [k: string]: unknown; + } + | unknown[] + | string + | number + | boolean + | null; description: string; array?: | { @@ -793,6 +817,8 @@ export interface PostsSelect { export interface AutosavePostsSelect { title?: T; computedTitle?: T; + richText?: T; + json?: T; description?: T; array?: | T