diff --git a/docs/admin/react-hooks.mdx b/docs/admin/react-hooks.mdx index 5640f185caf..2d021daf20a 100644 --- a/docs/admin/react-hooks.mdx +++ b/docs/admin/react-hooks.mdx @@ -739,7 +739,7 @@ The `useDocumentInfo` hook provides information about the current document being | **`lastUpdateTime`** | Timestamp of the last update to the document. | | **`mostRecentVersionIsAutosaved`** | Whether the most recent version is an autosaved version. | | **`preferencesKey`** | The `preferences` key to use when interacting with document-level user preferences. [More details](./preferences). | -| **`savedDocumentData`** | The saved data of the document. | +| **`data`** | The saved data of the document. | | **`setDocFieldPreferences`** | Method to set preferences for a specific field. [More details](./preferences). | | **`setDocumentTitle`** | Method to set the document title. | | **`setHasPublishedDoc`** | Method to update whether the document has been published. | diff --git a/packages/ui/src/elements/Autosave/index.tsx b/packages/ui/src/elements/Autosave/index.tsx index 2215f7e659d..f341efa21e8 100644 --- a/packages/ui/src/elements/Autosave/index.tsx +++ b/packages/ui/src/elements/Autosave/index.tsx @@ -5,7 +5,6 @@ import type { ClientCollectionConfig, ClientGlobalConfig } from 'payload' import { dequal } from 'dequal/lite' import { reduceFieldsToValues, versionDefaults } from 'payload/shared' import React, { useDeferredValue, useEffect, useRef, useState } from 'react' -import { toast } from 'sonner' import { useAllFormFields, @@ -17,13 +16,11 @@ import { useDebounce } from '../../hooks/useDebounce.js' import { useEffectEvent } from '../../hooks/useEffectEvent.js' import { useQueues } from '../../hooks/useQueues.js' import { useConfig } from '../../providers/Config/index.js' -import { useDocumentEvents } from '../../providers/DocumentEvents/index.js' import { useDocumentInfo } from '../../providers/DocumentInfo/index.js' import { useLocale } from '../../providers/Locale/index.js' import { useTranslation } from '../../providers/Translation/index.js' import { formatTimeToNow } from '../../utilities/formatDocTitle/formatDateTitle.js' import { reduceFieldsToValuesWithValidation } from '../../utilities/reduceFieldsToValuesWithValidation.js' -import { useDocumentDrawerContext } from '../DocumentDrawer/Provider.js' import { LeaveWithoutSaving } from '../LeaveWithoutSaving/index.js' import './index.scss' @@ -51,16 +48,11 @@ export const Autosave: React.FC = ({ id, collection, global: globalDoc }) incrementVersionCount, lastUpdateTime, mostRecentVersionIsAutosaved, - setLastUpdateTime, setMostRecentVersionIsAutosaved, setUnpublishedVersionCount, - updateSavedDocumentData, } = useDocumentInfo() - const { onSave: onSaveFromDocumentDrawer } = useDocumentDrawerContext() - - const { reportUpdate } = useDocumentEvents() - const { dispatchFields, isValid, setBackgroundProcessing, setIsValid } = useForm() + const { isValid, setBackgroundProcessing, submit } = useForm() const [formState] = useAllFormFields() const modified = useFormModified() @@ -151,118 +143,38 @@ export const Autosave: React.FC = ({ id, collection, global: globalDoc }) method = 'POST' } - if (url) { - if (modifiedRef.current) { - const { data, valid } = reduceFieldsToValuesWithValidation(formStateRef.current, true) - - data._status = 'draft' - - const skipSubmission = - submitted && !valid && versionsConfig?.drafts && versionsConfig?.drafts?.validate - - if (!skipSubmission) { - let res - - try { - res = await fetch(url, { - body: JSON.stringify(data), - credentials: 'include', - headers: { - 'Accept-Language': i18n.language, - 'Content-Type': 'application/json', - }, - method, - }) - } catch (_err) { - // Swallow Error - } - - const newDate = new Date() - // We need to log the time in order to figure out if we need to trigger the state off later - endTimestamp = newDate.getTime() - - const json = await res.json() - - if (res.status === 200) { - setLastUpdateTime(newDate.getTime()) - - reportUpdate({ - id, - entitySlug, - updatedAt: newDate.toISOString(), - }) - - // if onSaveFromDocumentDrawer is defined, call it - if (typeof onSaveFromDocumentDrawer === 'function') { - void onSaveFromDocumentDrawer({ - ...json, - operation: 'update', - }) - } - - if (!mostRecentVersionIsAutosaved) { - incrementVersionCount() - setMostRecentVersionIsAutosaved(true) - setUnpublishedVersionCount((prev) => prev + 1) - } - } - - if (versionsConfig?.drafts && versionsConfig?.drafts?.validate && json?.errors) { - if (Array.isArray(json.errors)) { - const [fieldErrors, nonFieldErrors] = json.errors.reduce( - ([fieldErrs, nonFieldErrs], err) => { - const newFieldErrs = [] - const newNonFieldErrs = [] - - if (err?.message) { - newNonFieldErrs.push(err) - } - - if (Array.isArray(err?.data)) { - err.data.forEach((dataError) => { - if (dataError?.field) { - newFieldErrs.push(dataError) - } else { - newNonFieldErrs.push(dataError) - } - }) - } - - return [ - [...fieldErrs, ...newFieldErrs], - [...nonFieldErrs, ...newNonFieldErrs], - ] - }, - [[], []], - ) - - dispatchFields({ - type: 'ADD_SERVER_ERRORS', - errors: fieldErrors, - }) - - nonFieldErrors.forEach((err) => { - toast.error(err.message || i18n.t('error:unknown')) - }) - - setIsValid(false) - hideIndicator() - return - } - } else { - // If it's not an error then we can update the document data inside the context - const document = json?.doc || json?.result - - // Manually update the data since this function doesn't fire the `submit` function from useForm - if (document) { - setIsValid(true) - updateSavedDocumentData(document) - } - } - - hideIndicator() - } + const { valid } = reduceFieldsToValuesWithValidation(formStateRef.current, true) + + const skipSubmission = + submitted && !valid && versionsConfig?.drafts && versionsConfig?.drafts?.validate + + if (!skipSubmission && modifiedRef.current && url) { + const result = await submit({ + action: url, + context: { + incrementVersionCount: false, + }, + disableFormWhileProcessing: false, + disableSuccessStatus: true, + method, + overrides: { + _status: 'draft', + }, + skipValidation: versionsConfig?.drafts && !versionsConfig?.drafts?.validate, + }) + + if (result && result?.res?.ok && !mostRecentVersionIsAutosaved) { + incrementVersionCount() + setMostRecentVersionIsAutosaved(true) + setUnpublishedVersionCount((prev) => prev + 1) } + + const newDate = new Date() + + // We need to log the time in order to figure out if we need to trigger the state off later + endTimestamp = newDate.getTime() + + hideIndicator() } } }, diff --git a/packages/ui/src/elements/DocumentDrawer/Provider.tsx b/packages/ui/src/elements/DocumentDrawer/Provider.tsx index 6b6c8c2b513..1bc46690535 100644 --- a/packages/ui/src/elements/DocumentDrawer/Provider.tsx +++ b/packages/ui/src/elements/DocumentDrawer/Provider.tsx @@ -20,6 +20,11 @@ export type DocumentDrawerContextProps = { }) => Promise | void readonly onSave?: (args: { collectionConfig?: ClientCollectionConfig + /** + * @experimental - Note: this property is experimental and may change in the future. Use as your own discretion. + * If you want to pass additional data to the onSuccess callback, you can use this context object. + */ + context?: Record doc: TypeWithID operation: 'create' | 'update' result: Data diff --git a/packages/ui/src/elements/Upload/index.tsx b/packages/ui/src/elements/Upload/index.tsx index e9f0c927199..475a9efd286 100644 --- a/packages/ui/src/elements/Upload/index.tsx +++ b/packages/ui/src/elements/Upload/index.tsx @@ -161,7 +161,7 @@ export const Upload_v4: React.FC = (props) => { const { t } = useTranslation() const { setModified } = useForm() - const { id, docPermissions, savedDocumentData, setUploadStatus } = useDocumentInfo() + const { id, data, docPermissions, setUploadStatus } = useDocumentInfo() const isFormSubmitting = useFormProcessing() const { errorMessage, setValue, showError, value } = useField({ path: 'file', @@ -349,7 +349,7 @@ export const Upload_v4: React.FC = (props) => { const acceptMimeTypes = uploadConfig.mimeTypes?.join(', ') - const imageCacheTag = uploadConfig?.cacheTags && savedDocumentData?.updatedAt + const imageCacheTag = uploadConfig?.cacheTags && data?.updatedAt useEffect(() => { const handleControlFileUrl = async () => { @@ -375,11 +375,11 @@ export const Upload_v4: React.FC = (props) => { return (
- {savedDocumentData && savedDocumentData.filename && !removedFile && ( + {data && data.filename && !removedFile && ( = (props) => { uploadConfig={uploadConfig} /> )} - {((!uploadConfig.hideFileInputOnCreate && !savedDocumentData?.filename) || removedFile) && ( + {((!uploadConfig.hideFileInputOnCreate && !data?.filename) || removedFile) && (
{!value && !showUrlInput && ( @@ -506,7 +506,7 @@ export const Upload_v4: React.FC = (props) => {
@@ -523,17 +523,17 @@ export const Upload_v4: React.FC = (props) => { )}
)} - {(value || savedDocumentData?.filename) && ( + {(value || data?.filename) && ( = (props) => { )} - {savedDocumentData && hasImageSizes && ( + {data && hasImageSizes && ( - + )} diff --git a/packages/ui/src/forms/Form/index.tsx b/packages/ui/src/forms/Form/index.tsx index 79e12b29ff9..ddb1cb115de 100644 --- a/packages/ui/src/forms/Form/index.tsx +++ b/packages/ui/src/forms/Form/index.tsx @@ -18,6 +18,7 @@ import type { Context as FormContextType, FormProps, GetDataByPath, + Submit, SubmitOptions, } from './types.js' @@ -199,14 +200,19 @@ export const Form: React.FC = (props) => { return isValid }, [collectionSlug, config, dispatchFields, id, operation, t, user, documentForm]) - const submit = useCallback( - async (options: SubmitOptions = {}, e): Promise => { + const submit = useCallback( + async (options, e) => { const { action: actionArg = action, + context, + disableFormWhileProcessing = true, + disableSuccessStatus: disableSuccessStatusFromArgs, method: methodToUse = method, overrides: overridesFromArgs = {}, skipValidation, - } = options + } = options || ({} as SubmitOptions) + + const disableToast = disableSuccessStatusFromArgs ?? disableSuccessStatus if (disabled) { if (e) { @@ -217,6 +223,7 @@ export const Form: React.FC = (props) => { // create new toast promise which will resolve manually later let errorToast, successToast + const promise = new Promise((resolve, reject) => { successToast = resolve errorToast = reject @@ -225,7 +232,7 @@ export const Form: React.FC = (props) => { const hasFormSubmitAction = actionArg || typeof action === 'string' || typeof action === 'function' - if (redirect || disableSuccessStatus || !hasFormSubmitAction) { + if (redirect || disableToast || !hasFormSubmitAction) { // Do not show submitting toast, as the promise toast may never disappear under these conditions. // Instead, make successToast() or errorToast() throw toast.success / toast.error successToast = (data) => toast.success(data) @@ -247,8 +254,10 @@ export const Form: React.FC = (props) => { e.preventDefault() } - setProcessing(true) - setDisabled(true) + if (disableFormWhileProcessing) { + setProcessing(true) + setDisabled(true) + } if (waitForAutocomplete) { await wait(100) @@ -290,6 +299,7 @@ export const Form: React.FC = (props) => { skipValidation || disableValidationOnSubmit ? true : await contextRef.current.validateForm() setIsValid(isValid) + // If not valid, prevent submission if (!isValid) { errorToast(t('error:correctInvalidFields')) @@ -366,9 +376,10 @@ export const Form: React.FC = (props) => { if (isJSON) { json = await res.json() } + if (res.status < 400) { if (typeof onSuccess === 'function') { - const newFormState = await onSuccess(json) + const newFormState = await onSuccess(json, context) if (newFormState) { dispatchFields({ @@ -379,12 +390,13 @@ export const Form: React.FC = (props) => { }) } } + setSubmitted(false) setProcessing(false) if (redirect) { startRouteTransition(() => router.push(redirect)) - } else if (!disableSuccessStatus) { + } else if (!disableToast) { successToast(json.message || t('general:submissionSuccessful')) } } else { @@ -392,6 +404,7 @@ export const Form: React.FC = (props) => { setSubmitted(true) contextRef.current = { ...contextRef.current } // triggers rerender of all components that subscribe to form + if (json.message) { errorToast(json.message) return @@ -443,6 +456,8 @@ export const Form: React.FC = (props) => { errorToast(message) } + + return { formState: contextRef.current.fields, res } } catch (err) { console.error('Error submitting form', err) // eslint-disable-line no-console setProcessing(false) diff --git a/packages/ui/src/forms/Form/types.ts b/packages/ui/src/forms/Form/types.ts index 9cfd1ef6441..a10350506ad 100644 --- a/packages/ui/src/forms/Form/types.ts +++ b/packages/ui/src/forms/Form/types.ts @@ -52,7 +52,7 @@ export type FormProps = { log?: boolean onChange?: ((args: { formState: FormState; submitted?: boolean }) => Promise)[] onSubmit?: (fields: FormState, data: Data) => void - onSuccess?: (json: unknown) => Promise | void + onSuccess?: (json: unknown, context?: Record) => Promise | void redirect?: string submitted?: boolean uuid?: string @@ -70,16 +70,40 @@ export type FormProps = { export type SubmitOptions = { action?: string + /** + * @experimental - Note: this property is experimental and may change in the future. Use as your own discretion. + * If you want to pass additional data to the onSuccess callback, you can use this context object. + */ + context?: Record + /** + * When true, will disable the form while it is processing. + * @default true + */ + disableFormWhileProcessing?: boolean + /** + * When true, will disable the success toast after form submission. + * @default false + */ + disableSuccessStatus?: boolean method?: string overrides?: ((formState) => FormData) | Record + /** + * When true, will skip validation before submitting the form. + * @default false + */ skipValidation?: boolean } export type DispatchFields = React.Dispatch + export type Submit = ( options?: SubmitOptions, e?: React.FormEvent, -) => Promise +) => Promise export type ValidateForm = () => Promise diff --git a/packages/ui/src/providers/DocumentInfo/index.tsx b/packages/ui/src/providers/DocumentInfo/index.tsx index 0fdb4d93241..12ad4646481 100644 --- a/packages/ui/src/providers/DocumentInfo/index.tsx +++ b/packages/ui/src/providers/DocumentInfo/index.tsx @@ -97,6 +97,7 @@ const DocumentInfo: React.FC< const [versionCount, setVersionCount] = useState(versionCountFromProps) const [hasPublishedDoc, setHasPublishedDoc] = useState(hasPublishedDocFromProps) + const [unpublishedVersionCount, setUnpublishedVersionCount] = useState( unpublishedVersionCountFromProps, ) @@ -104,11 +105,14 @@ const DocumentInfo: React.FC< const [documentIsLocked, setDocumentIsLocked] = useControllableState( isLockedFromProps, ) + const [currentEditor, setCurrentEditor] = useControllableState( currentEditorFromProps, ) const [lastUpdateTime, setLastUpdateTime] = useControllableState(lastUpdateTimeFromProps) - const [savedDocumentData, setSavedDocumentData] = useControllableState(initialData) + + const [data, setData] = useControllableState(initialData) + const [uploadStatus, setUploadStatus] = useControllableState<'failed' | 'idle' | 'uploading'>( 'idle', ) @@ -294,13 +298,6 @@ const DocumentInfo: React.FC< } }, [collectionConfig, globalConfig, versionCount]) - const updateSavedDocumentData = React.useCallback( - (json) => { - setSavedDocumentData(json) - }, - [setSavedDocumentData], - ) - /** * @todo: Remove this in v4 * Users should use the `DocumentTitleContext` instead. @@ -309,14 +306,14 @@ const DocumentInfo: React.FC< setDocumentTitle( formatDocTitle({ collectionConfig, - data: { ...savedDocumentData, id }, + data: { ...data, id }, dateFormat, fallback: id?.toString(), globalConfig, i18n, }), ) - }, [collectionConfig, globalConfig, savedDocumentData, dateFormat, i18n, id]) + }, [collectionConfig, globalConfig, data, dateFormat, i18n, id]) // clean on unmount useEffect(() => { @@ -351,6 +348,7 @@ const DocumentInfo: React.FC< ...props, action, currentEditor, + data, docConfig, docPermissions, documentIsLocked, @@ -367,8 +365,9 @@ const DocumentInfo: React.FC< lastUpdateTime, mostRecentVersionIsAutosaved, preferencesKey, - savedDocumentData, + savedDocumentData: data, setCurrentEditor, + setData, setDocFieldPreferences, setDocumentIsLocked, setDocumentTitle, @@ -381,7 +380,7 @@ const DocumentInfo: React.FC< unlockDocument, unpublishedVersionCount, updateDocumentEditor, - updateSavedDocumentData, + updateSavedDocumentData: setData, uploadStatus, versionCount, } diff --git a/packages/ui/src/providers/DocumentInfo/types.ts b/packages/ui/src/providers/DocumentInfo/types.ts index 7f9233e3654..4d8d6404c20 100644 --- a/packages/ui/src/providers/DocumentInfo/types.ts +++ b/packages/ui/src/providers/DocumentInfo/types.ts @@ -49,6 +49,7 @@ export type DocumentInfoProps = { export type DocumentInfoContext = { currentEditor?: ClientUser | null | number | string + data?: Data docConfig?: ClientCollectionConfig | ClientGlobalConfig documentIsLocked?: boolean documentLockState: React.RefObject<{ @@ -61,21 +62,26 @@ export type DocumentInfoContext = { incrementVersionCount: () => void isInitializing: boolean preferencesKey?: string + /** + * @deprecated This property is deprecated and will be removed in v4. + * Use `data` instead. + */ savedDocumentData?: Data setCurrentEditor?: React.Dispatch> + setData: (data: Data) => void setDocFieldPreferences: ( field: string, fieldPreferences: { [key: string]: unknown } & Partial, ) => void setDocumentIsLocked?: React.Dispatch> /** - * * @deprecated This property is deprecated and will be removed in v4. - * This is for performance reasons. Use the `DocumentTitleContext` instead. + * This is for performance reasons. Use the `DocumentTitleContext` instead + * via the `useDocumentTitle` hook. * @example * ```tsx * import { useDocumentTitle } from '@payloadcms/ui' - * const { setDocumentTitle } = useDocumentTitle() + * const { setDocumentTitle } = useDocumentTitle() * ``` */ setDocumentTitle: React.Dispatch> @@ -86,17 +92,22 @@ export type DocumentInfoContext = { setUploadStatus?: (status: 'failed' | 'idle' | 'uploading') => void /** * @deprecated This property is deprecated and will be removed in v4. - * This is for performance reasons. Use the `DocumentTitleContext` instead. + * This is for performance reasons. Use the `DocumentTitleContext` instead + * via the `useDocumentTitle` hook. * @example * ```tsx * import { useDocumentTitle } from '@payloadcms/ui' - * const { title } = useDocumentTitle() + * const { title } = useDocumentTitle() * ``` */ title: string unlockDocument: (docID: number | string, slug: string) => Promise unpublishedVersionCount: number updateDocumentEditor: (docID: number | string, slug: string, user: ClientUser) => Promise + /** + * @deprecated This property is deprecated and will be removed in v4. + * Use `setData` instead. + */ updateSavedDocumentData: (data: Data) => void uploadStatus?: 'failed' | 'idle' | 'uploading' versionCount: number diff --git a/packages/ui/src/providers/DocumentTitle/index.tsx b/packages/ui/src/providers/DocumentTitle/index.tsx index 22942196818..1202a9ca472 100644 --- a/packages/ui/src/providers/DocumentTitle/index.tsx +++ b/packages/ui/src/providers/DocumentTitle/index.tsx @@ -19,8 +19,7 @@ export const useDocumentTitle = (): IDocumentTitleContext => use(DocumentTitleCo export const DocumentTitleProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { - const { id, collectionSlug, docConfig, globalSlug, initialData, savedDocumentData } = - useDocumentInfo() + const { id, collectionSlug, data, docConfig, globalSlug, initialData } = useDocumentInfo() const { config: { @@ -45,14 +44,14 @@ export const DocumentTitleProvider: React.FC<{ setDocumentTitle( formatDocTitle({ collectionConfig: collectionSlug ? (docConfig as ClientCollectionConfig) : undefined, - data: { ...savedDocumentData, id }, + data: { ...data, id }, dateFormat, fallback: id?.toString(), globalConfig: globalSlug ? (docConfig as ClientGlobalConfig) : undefined, i18n, }), ) - }, [savedDocumentData, dateFormat, i18n, id, collectionSlug, docConfig, globalSlug]) + }, [data, dateFormat, i18n, id, collectionSlug, docConfig, globalSlug]) return {children} } diff --git a/packages/ui/src/views/Edit/index.tsx b/packages/ui/src/views/Edit/index.tsx index b621fc87d9f..1e78cf46ac6 100644 --- a/packages/ui/src/views/Edit/index.tsx +++ b/packages/ui/src/views/Edit/index.tsx @@ -65,6 +65,7 @@ export function DefaultEditView({ BeforeFields, collectionSlug, currentEditor, + data, disableActions, disableCreate, disableLeaveWithoutSaving, @@ -86,12 +87,12 @@ export function DefaultEditView({ redirectAfterDelete, redirectAfterDuplicate, redirectAfterRestore, - savedDocumentData, setCurrentEditor, + setData, setDocumentIsLocked, + setLastUpdateTime, unlockDocument, updateDocumentEditor, - updateSavedDocumentData, } = useDocumentInfo() const { @@ -237,7 +238,7 @@ export function DefaultEditView({ setDocumentIsLocked(false) setCurrentEditor(null) } catch (err) { - console.error('Failed to unlock before leave', err) + console.error('Failed to unlock before leave', err) // eslint-disable-line no-console } } } @@ -256,15 +257,17 @@ export function DefaultEditView({ ]) const onSave = useCallback( - async (json): Promise => { + async (json, context?: Record): Promise => { const controller = handleAbortRef(abortOnSaveRef) const document = json?.doc || json?.result + const updatedAt = document?.updatedAt || new Date().toISOString() + reportUpdate({ id, entitySlug, - updatedAt: document?.updatedAt || new Date().toISOString(), + updatedAt, }) // If we're editing the doc of the logged-in user, @@ -273,10 +276,14 @@ export function DefaultEditView({ void refreshCookieAsync() } - incrementVersionCount() + setLastUpdateTime(updatedAt) + + if (context?.incrementVersionCount !== false) { + incrementVersionCount() + } - if (typeof updateSavedDocumentData === 'function') { - void updateSavedDocumentData(document || {}) + if (typeof setData === 'function') { + void setData(document || {}) } if (typeof onSaveFromContext === 'function') { @@ -284,6 +291,7 @@ export function DefaultEditView({ void onSaveFromContext({ ...json, + context, operation, updatedAt: operation === 'update' @@ -306,7 +314,7 @@ export function DefaultEditView({ await getDocPermissions(json) - if ((id || globalSlug) && !autosaveEnabled) { + if (id || globalSlug) { const docPreferences = await getDocPreferences() const { state } = await getFormState({ @@ -341,18 +349,19 @@ export function DefaultEditView({ user, collectionSlug, userSlug, - incrementVersionCount, - updateSavedDocumentData, + setLastUpdateTime, + setData, onSaveFromContext, - redirectAfterCreate, isEditing, depth, + redirectAfterCreate, getDocPermissions, globalSlug, - autosaveEnabled, refreshCookieAsync, + incrementVersionCount, adminRoute, locale, + startRouteTransition, router, resetUploadEdits, getDocPreferences, @@ -362,7 +371,6 @@ export function DefaultEditView({ schemaPathSegments, isLockingEnabled, setDocumentIsLocked, - startRouteTransition, ], ) @@ -549,7 +557,7 @@ export function DefaultEditView({ SaveButton, SaveDraftButton, }} - data={savedDocumentData} + data={data} disableActions={disableActions || isFolderCollection || isTrashed} disableCreate={disableCreate} EditMenuItems={EditMenuItems} @@ -612,14 +620,14 @@ export function DefaultEditView({ className={`${baseClass}__auth`} collectionSlug={collectionConfig.slug} disableLocalStrategy={collectionConfig.auth?.disableLocalStrategy} - email={savedDocumentData?.email} + email={data?.email} loginWithUsername={auth?.loginWithUsername} operation={operation} readOnly={!hasSavePermission} requirePassword={!id} setValidateBeforeSubmit={setValidateBeforeSubmit} useAPIKey={auth.useAPIKey} - username={savedDocumentData?.username} + username={data?.username} verify={auth.verify} /> )} diff --git a/test/_community/payload-types.ts b/test/_community/payload-types.ts index 6d7c9640105..599c9dec1d7 100644 --- a/test/_community/payload-types.ts +++ b/test/_community/payload-types.ts @@ -84,7 +84,7 @@ export interface Config { 'payload-migrations': PayloadMigrationsSelect | PayloadMigrationsSelect; }; db: { - defaultIDType: number; + defaultIDType: string; }; globals: { menu: Menu; @@ -124,7 +124,7 @@ export interface UserAuthOperations { * via the `definition` "posts". */ export interface Post { - id: number; + id: string; title?: string | null; content?: { root: { @@ -149,7 +149,7 @@ export interface Post { * via the `definition` "media". */ export interface Media { - id: number; + id: string; updatedAt: string; createdAt: string; url?: string | null; @@ -193,7 +193,7 @@ export interface Media { * via the `definition` "users". */ export interface User { - id: number; + id: string; updatedAt: string; createdAt: string; email: string; @@ -217,24 +217,24 @@ export interface User { * via the `definition` "payload-locked-documents". */ export interface PayloadLockedDocument { - id: number; + id: string; document?: | ({ relationTo: 'posts'; - value: number | Post; + value: string | Post; } | null) | ({ relationTo: 'media'; - value: number | Media; + value: string | Media; } | null) | ({ relationTo: 'users'; - value: number | User; + value: string | User; } | null); globalSlug?: string | null; user: { relationTo: 'users'; - value: number | User; + value: string | User; }; updatedAt: string; createdAt: string; @@ -244,10 +244,10 @@ export interface PayloadLockedDocument { * via the `definition` "payload-preferences". */ export interface PayloadPreference { - id: number; + id: string; user: { relationTo: 'users'; - value: number | User; + value: string | User; }; key?: string | null; value?: @@ -267,7 +267,7 @@ export interface PayloadPreference { * via the `definition` "payload-migrations". */ export interface PayloadMigration { - id: number; + id: string; name?: string | null; batch?: number | null; updatedAt: string; @@ -393,7 +393,7 @@ export interface PayloadMigrationsSelect { * via the `definition` "menu". */ export interface Menu { - id: number; + id: string; globalText?: string | null; updatedAt?: string | null; createdAt?: string | null; diff --git a/test/versions/collections/Autosave.ts b/test/versions/collections/Autosave.ts index ff18f7cb1bc..7350ff2b876 100644 --- a/test/versions/collections/Autosave.ts +++ b/test/versions/collections/Autosave.ts @@ -53,6 +53,14 @@ const AutosavePosts: CollectionConfig = { unique: true, localized: true, }, + { + name: 'computedTitle', + label: 'Computed Title', + type: 'text', + hooks: { + beforeChange: [({ data }) => data?.title], + }, + }, { name: 'description', label: 'Description', diff --git a/test/versions/e2e.spec.ts b/test/versions/e2e.spec.ts index 13b2428d254..8973c23714c 100644 --- a/test/versions/e2e.spec.ts +++ b/test/versions/e2e.spec.ts @@ -1285,6 +1285,44 @@ describe('Versions', () => { // Remove listener page.removeListener('dialog', acceptAlert) }) + + test('- with autosave - applies afterChange hooks to form state after autosave runs', async () => { + const url = new AdminUrlUtil(serverURL, autosaveCollectionSlug) + await page.goto(url.create) + const titleField = page.locator('#field-title') + await titleField.fill('Initial') + await waitForAutoSaveToRunAndComplete(page) + const computedTitleField = page.locator('#field-computedTitle') + await expect(computedTitleField).toHaveValue('Initial') + }) + + test('- with autosave - does not display success toast after autosave complete', async () => { + const url = new AdminUrlUtil(serverURL, autosaveCollectionSlug) + await page.goto(url.create) + const titleField = page.locator('#field-title') + await titleField.fill('Initial') + + let hasDisplayedToast = false + + const startTime = Date.now() + const timeout = 5000 + const interval = 100 + + while (Date.now() - startTime < timeout) { + const isHidden = await page.locator('.payload-toast-item').isHidden() + console.log(`Toast is hidden: ${isHidden}`) + + // eslint-disable-next-line playwright/no-conditional-in-test + if (!isHidden) { + hasDisplayedToast = true + break + } + + await wait(interval) + } + + expect(hasDisplayedToast).toBe(false) + }) }) describe('Globals - publish individual locale', () => { diff --git a/test/versions/payload-types.ts b/test/versions/payload-types.ts index ddfc1db287f..d393912cf4a 100644 --- a/test/versions/payload-types.ts +++ b/test/versions/payload-types.ts @@ -197,6 +197,7 @@ export interface Post { export interface AutosavePost { id: string; title: string; + computedTitle?: string | null; description: string; updatedAt: string; createdAt: string; @@ -366,7 +367,6 @@ export interface Diff { textInNamedTab1InBlock?: string | null; }; textInUnnamedTab2InBlock?: string | null; - textInUnnamedTab2InBlockAccessFalse?: string | null; id?: string | null; blockName?: string | null; blockType: 'TabsBlock'; @@ -469,7 +469,6 @@ export interface Diff { }; textInUnnamedTab2?: string | null; text?: string | null; - textCannotRead?: string | null; textArea?: string | null; upload?: (string | null) | Media; uploadHasMany?: (string | Media)[] | null; @@ -787,6 +786,7 @@ export interface PostsSelect { */ export interface AutosavePostsSelect { title?: T; + computedTitle?: T; description?: T; updatedAt?: T; createdAt?: T; @@ -960,7 +960,6 @@ export interface DiffSelect { textInNamedTab1InBlock?: T; }; textInUnnamedTab2InBlock?: T; - textInUnnamedTab2InBlockAccessFalse?: T; id?: T; blockName?: T; }; @@ -995,7 +994,6 @@ export interface DiffSelect { }; textInUnnamedTab2?: T; text?: T; - textCannotRead?: T; textArea?: T; upload?: T; uploadHasMany?: T;