diff --git a/packages/next/src/views/CreateFirstUser/index.client.tsx b/packages/next/src/views/CreateFirstUser/index.client.tsx index 31b8217198..3ca31f4aa2 100644 --- a/packages/next/src/views/CreateFirstUser/index.client.tsx +++ b/packages/next/src/views/CreateFirstUser/index.client.tsx @@ -20,7 +20,7 @@ import { useServerFunctions, useTranslation, } from '@payloadcms/ui' -import { abortAndIgnore } from '@payloadcms/ui/shared' +import { abortAndIgnore, handleAbortRef } from '@payloadcms/ui/shared' import React, { useEffect } from 'react' export const CreateFirstUserClient: React.FC<{ @@ -43,16 +43,13 @@ export const CreateFirstUserClient: React.FC<{ const { t } = useTranslation() const { setUser } = useAuth() - const formStateAbortControllerRef = React.useRef(null) + const abortOnChangeRef = React.useRef(null) const collectionConfig = getEntityConfig({ collectionSlug: userSlug }) as ClientCollectionConfig const onChange: FormProps['onChange'][0] = React.useCallback( async ({ formState: prevFormState }) => { - abortAndIgnore(formStateAbortControllerRef.current) - - const controller = new AbortController() - formStateAbortControllerRef.current = controller + const controller = handleAbortRef(abortOnChangeRef) const response = await getFormState({ collectionSlug: userSlug, @@ -64,6 +61,8 @@ export const CreateFirstUserClient: React.FC<{ signal: controller.signal, }) + abortOnChangeRef.current = null + if (response && response.state) { return response.state } @@ -76,8 +75,10 @@ export const CreateFirstUserClient: React.FC<{ } useEffect(() => { + const abortOnChange = abortOnChangeRef.current + return () => { - abortAndIgnore(formStateAbortControllerRef.current) + abortAndIgnore(abortOnChange) } }, []) diff --git a/packages/next/src/views/LivePreview/index.client.tsx b/packages/next/src/views/LivePreview/index.client.tsx index b27b9c3848..bb76301b05 100644 --- a/packages/next/src/views/LivePreview/index.client.tsx +++ b/packages/next/src/views/LivePreview/index.client.tsx @@ -7,6 +7,7 @@ import type { ClientGlobalConfig, ClientUser, Data, + FormState, LivePreviewConfig, } from 'payload' @@ -25,21 +26,25 @@ import { useDocumentDrawerContext, useDocumentEvents, useDocumentInfo, + useEditDepth, useServerFunctions, useTranslation, + useUploadEdits, } from '@payloadcms/ui' import { abortAndIgnore, + formatAdminURL, + handleAbortRef, handleBackToDashboard, handleGoBack, handleTakeOver, } from '@payloadcms/ui/shared' -import { useRouter } from 'next/navigation.js' +import { useRouter, useSearchParams } from 'next/navigation.js' import React, { Fragment, useCallback, useEffect, useRef, useState } from 'react' import { useLivePreviewContext } from './Context/context.js' -import { LivePreviewProvider } from './Context/index.js' import './index.scss' +import { LivePreviewProvider } from './Context/index.js' import { LivePreview } from './Preview/index.js' import { usePopupWindow } from './usePopupWindow.js' @@ -75,10 +80,12 @@ const PreviewView: React.FC = ({ disableLeaveWithoutSaving, docPermissions, documentIsLocked, + getDocPermissions, getDocPreferences, globalSlug, hasPublishPermission, hasSavePermission, + incrementVersionCount, initialData, initialState, isEditing, @@ -88,11 +95,10 @@ const PreviewView: React.FC = ({ setDocumentIsLocked, unlockDocument, updateDocumentEditor, + updateSavedDocumentData, } = useDocumentInfo() - const { getFormState } = useServerFunctions() - - const { onSave: onSaveFromProps } = useDocumentDrawerContext() + const { onSave: onSaveFromContext } = useDocumentDrawerContext() const operation = id ? 'update' : 'create' @@ -103,13 +109,21 @@ const PreviewView: React.FC = ({ }, } = useConfig() const router = useRouter() + const params = useSearchParams() + const locale = params.get('locale') const { t } = useTranslation() const { previewWindowType } = useLivePreviewContext() const { refreshCookieAsync, user } = useAuth() const { reportUpdate } = useDocumentEvents() + const { resetUploadEdits } = useUploadEdits() + const { getFormState } = useServerFunctions() const docConfig = collectionConfig || globalConfig + const entitySlug = collectionConfig?.slug || globalConfig?.slug + + const depth = useEditDepth() + const lockDocumentsProp = docConfig?.lockDocuments !== undefined ? docConfig?.lockDocuments : true const isLockingEnabled = lockDocumentsProp !== false @@ -118,10 +132,19 @@ const PreviewView: React.FC = ({ typeof lockDocumentsProp === 'object' ? lockDocumentsProp.duration : lockDurationDefault const lockDurationInMilliseconds = lockDuration * 1000 + const autosaveEnabled = Boolean( + (collectionConfig?.versions?.drafts && collectionConfig?.versions?.drafts?.autosave) || + (globalConfig?.versions?.drafts && globalConfig?.versions?.drafts?.autosave), + ) + + const preventLeaveWithoutSaving = + typeof disableLeaveWithoutSaving !== 'undefined' ? !disableLeaveWithoutSaving : !autosaveEnabled + const [isReadOnlyForIncomingUser, setIsReadOnlyForIncomingUser] = useState(false) const [showTakeOverModal, setShowTakeOverModal] = useState(false) - const formStateAbortControllerRef = useRef(new AbortController()) + const abortOnChangeRef = useRef(null) + const abortOnSaveRef = useRef(null) const [editSessionStartTime, setEditSessionStartTime] = useState(Date.now()) @@ -140,10 +163,12 @@ const PreviewView: React.FC = ({ }) const onSave = useCallback( - (json) => { + async (json): Promise => { + const controller = handleAbortRef(abortOnSaveRef) + reportUpdate({ id, - entitySlug: collectionSlug, + entitySlug, updatedAt: json?.result?.updatedAt || new Date().toISOString(), }) @@ -153,38 +178,91 @@ const PreviewView: React.FC = ({ void refreshCookieAsync() } - // Unlock the document after save - if ((id || globalSlug) && isLockingEnabled) { - setDocumentIsLocked(false) + incrementVersionCount() + + if (typeof updateSavedDocumentData === 'function') { + void updateSavedDocumentData(json?.doc || {}) } - if (typeof onSaveFromProps === 'function') { - void onSaveFromProps({ + if (typeof onSaveFromContext === 'function') { + void onSaveFromContext({ ...json, operation: id ? 'update' : 'create', }) } + + if (!isEditing && depth < 2) { + // Redirect to the same locale if it's been set + const redirectRoute = formatAdminURL({ + adminRoute, + path: `/collections/${collectionSlug}/${json?.doc?.id}${locale ? `?locale=${locale}` : ''}`, + }) + router.push(redirectRoute) + } else { + resetUploadEdits() + } + + await getDocPermissions(json) + + if ((id || globalSlug) && !autosaveEnabled) { + const docPreferences = await getDocPreferences() + + const { state } = await getFormState({ + id, + collectionSlug, + data: json?.doc || json?.result, + docPermissions, + docPreferences, + globalSlug, + operation, + renderAllFields: true, + returnLockStatus: false, + schemaPath: entitySlug, + signal: controller.signal, + }) + + // Unlock the document after save + if (isLockingEnabled) { + setDocumentIsLocked(false) + } + + abortOnSaveRef.current = null + + return state + } }, [ + adminRoute, collectionSlug, + depth, + docPermissions, + entitySlug, + getDocPermissions, + getDocPreferences, + getFormState, globalSlug, id, + incrementVersionCount, + isEditing, isLockingEnabled, - onSaveFromProps, + locale, + onSaveFromContext, + operation, refreshCookieAsync, reportUpdate, + resetUploadEdits, + router, setDocumentIsLocked, + updateSavedDocumentData, user, userSlug, + autosaveEnabled, ], ) const onChange: FormProps['onChange'][0] = useCallback( async ({ formState: prevFormState }) => { - abortAndIgnore(formStateAbortControllerRef.current) - - const controller = new AbortController() - formStateAbortControllerRef.current = controller + const controller = handleAbortRef(abortOnChangeRef) const currentTime = Date.now() const timeSinceLastUpdate = currentTime - editSessionStartTime @@ -242,6 +320,8 @@ const PreviewView: React.FC = ({ } } + abortOnChangeRef.current = null + return state }, [ @@ -308,8 +388,12 @@ const PreviewView: React.FC = ({ ]) useEffect(() => { + const abortOnChange = abortOnChangeRef.current + const abortOnSave = abortOnSaveRef.current + return () => { - abortAndIgnore(formStateAbortControllerRef.current) + abortAndIgnore(abortOnChange) + abortAndIgnore(abortOnSave) } }) @@ -372,12 +456,7 @@ const PreviewView: React.FC = ({ }} /> )} - {((collectionConfig && - !(collectionConfig.versions?.drafts && collectionConfig.versions?.drafts?.autosave)) || - (globalConfig && - !(globalConfig.versions?.drafts && globalConfig.versions?.drafts?.autosave))) && - !disableLeaveWithoutSaving && - !isReadOnlyForIncomingUser && } + {!isReadOnlyForIncomingUser && preventLeaveWithoutSaving && } (null) + const abortOnChangeRef = React.useRef(null) const collectionConfig = getEntityConfig({ collectionSlug: docSlug }) as ClientCollectionConfig const router = useRouter() @@ -111,12 +111,10 @@ export function EditForm({ submitted }: EditFormProps) { const onChange: NonNullable[0] = useCallback( async ({ formState: prevFormState }) => { - abortAndIgnore(formStateAbortControllerRef.current) - - const controller = new AbortController() - formStateAbortControllerRef.current = controller + const controller = handleAbortRef(abortOnChangeRef) const docPreferences = await getDocPreferences() + const { state: newFormState } = await getFormState({ collectionSlug, docPermissions, @@ -127,14 +125,18 @@ export function EditForm({ submitted }: EditFormProps) { signal: controller.signal, }) + abortOnChangeRef.current = null + return newFormState }, [collectionSlug, schemaPath, getDocPreferences, getFormState, docPermissions], ) useEffect(() => { + const abortOnChange = abortOnChangeRef.current + return () => { - abortAndIgnore(formStateAbortControllerRef.current) + abortAndIgnore(abortOnChange) } }, []) diff --git a/packages/ui/src/elements/DocumentDrawer/DrawerContent.tsx b/packages/ui/src/elements/DocumentDrawer/DrawerContent.tsx index d443ad4419..89ee368d83 100644 --- a/packages/ui/src/elements/DocumentDrawer/DrawerContent.tsx +++ b/packages/ui/src/elements/DocumentDrawer/DrawerContent.tsx @@ -10,7 +10,7 @@ import { LoadingOverlay } from '../../elements/Loading/index.js' import { useConfig } from '../../providers/Config/index.js' import { useServerFunctions } from '../../providers/ServerFunctions/index.js' import { useTranslation } from '../../providers/Translation/index.js' -import { abortAndIgnore } from '../../utilities/abortAndIgnore.js' +import { abortAndIgnore, handleAbortRef } from '../../utilities/abortAndIgnore.js' import { DocumentDrawerContextProvider } from './Provider.js' export const DocumentDrawerContent: React.FC = ({ @@ -37,7 +37,7 @@ export const DocumentDrawerContent: React.FC = ({ collections.find((collection) => collection.slug === collectionSlug), ) - const documentViewAbortControllerRef = React.useRef(null) + const abortGetDocumentViewRef = React.useRef(null) const { closeModal } = useModal() const { t } = useTranslation() @@ -50,10 +50,7 @@ export const DocumentDrawerContent: React.FC = ({ const getDocumentView = useCallback( (docID?: number | string) => { - abortAndIgnore(documentViewAbortControllerRef.current) - - const controller = new AbortController() - documentViewAbortControllerRef.current = controller + const controller = handleAbortRef(abortGetDocumentViewRef) const fetchDocumentView = async () => { setIsLoading(true) @@ -81,6 +78,8 @@ export const DocumentDrawerContent: React.FC = ({ closeModal(drawerSlug) // toast.error(data?.errors?.[0].message || t('error:unspecific')) } + + abortGetDocumentViewRef.current = null } void fetchDocumentView() @@ -154,8 +153,10 @@ export const DocumentDrawerContent: React.FC = ({ // Cleanup any pending requests when the component unmounts useEffect(() => { + const abortGetDocumentView = abortGetDocumentViewRef.current + return () => { - abortAndIgnore(documentViewAbortControllerRef.current) + abortAndIgnore(abortGetDocumentView) } }, []) diff --git a/packages/ui/src/elements/EditMany/index.tsx b/packages/ui/src/elements/EditMany/index.tsx index 2522c34f92..2c262ff41b 100644 --- a/packages/ui/src/elements/EditMany/index.tsx +++ b/packages/ui/src/elements/EditMany/index.tsx @@ -23,7 +23,7 @@ import { useRouteCache } from '../../providers/RouteCache/index.js' import { SelectAllStatus, useSelection } from '../../providers/Selection/index.js' import { useServerFunctions } from '../../providers/ServerFunctions/index.js' import { useTranslation } from '../../providers/Translation/index.js' -import { abortAndIgnore } from '../../utilities/abortAndIgnore.js' +import { abortAndIgnore, handleAbortRef } from '../../utilities/abortAndIgnore.js' import { mergeListSearchAndWhere } from '../../utilities/mergeListSearchAndWhere.js' import { parseSearchParams } from '../../utilities/parseSearchParams.js' import './index.scss' @@ -156,7 +156,7 @@ export const EditMany: React.FC = (props) => { const router = useRouter() const [initialState, setInitialState] = useState() const hasInitializedState = React.useRef(false) - const formStateAbortControllerRef = React.useRef(null) + const abortFormStateRef = React.useRef(null) const { clearRouteCache } = useRouteCache() const collectionPermissions = permissions?.collections?.[slug] @@ -193,10 +193,7 @@ export const EditMany: React.FC = (props) => { const onChange: FormProps['onChange'][0] = useCallback( async ({ formState: prevFormState }) => { - abortAndIgnore(formStateAbortControllerRef.current) - - const controller = new AbortController() - formStateAbortControllerRef.current = controller + const controller = handleAbortRef(abortFormStateRef) const { state } = await getFormState({ collectionSlug: slug, @@ -208,14 +205,18 @@ export const EditMany: React.FC = (props) => { signal: controller.signal, }) + abortFormStateRef.current = null + return state }, [getFormState, slug, collectionPermissions], ) useEffect(() => { + const abortFormState = abortFormStateRef.current + return () => { - abortAndIgnore(formStateAbortControllerRef.current) + abortAndIgnore(abortFormState) } }, []) diff --git a/packages/ui/src/elements/TableColumns/index.tsx b/packages/ui/src/elements/TableColumns/index.tsx index 52852d145b..670d9cf76a 100644 --- a/packages/ui/src/elements/TableColumns/index.tsx +++ b/packages/ui/src/elements/TableColumns/index.tsx @@ -10,7 +10,7 @@ import type { Column } from '../Table/index.js' import { useConfig } from '../../providers/Config/index.js' import { usePreferences } from '../../providers/Preferences/index.js' import { useServerFunctions } from '../../providers/ServerFunctions/index.js' -import { abortAndIgnore } from '../../utilities/abortAndIgnore.js' +import { abortAndIgnore, handleAbortRef } from '../../utilities/abortAndIgnore.js' export interface ITableColumns { columns: Column[] @@ -78,11 +78,12 @@ export const TableColumnsProvider: React.FC = ({ const { getPreference } = usePreferences() const [tableColumns, setTableColumns] = React.useState(columnState) - const tableStateControllerRef = React.useRef(null) + const abortTableStateRef = React.useRef(null) + const abortToggleColumnRef = React.useRef(null) const moveColumn = useCallback( async (args: { fromIndex: number; toIndex: number }) => { - abortAndIgnore(tableStateControllerRef.current) + const controller = handleAbortRef(abortTableStateRef) const { fromIndex, toIndex } = args const withMovedColumn = [...tableColumns] @@ -91,9 +92,6 @@ export const TableColumnsProvider: React.FC = ({ setTableColumns(withMovedColumn) - const controller = new AbortController() - tableStateControllerRef.current = controller - const result = await getTableState({ collectionSlug, columns: sanitizeColumns(withMovedColumn), @@ -108,6 +106,8 @@ export const TableColumnsProvider: React.FC = ({ setTableColumns(result.state) setTable(result.Table) } + + abortTableStateRef.current = null }, [ tableColumns, @@ -123,7 +123,7 @@ export const TableColumnsProvider: React.FC = ({ const toggleColumn = useCallback( async (column: string) => { - abortAndIgnore(tableStateControllerRef.current) + const controller = handleAbortRef(abortToggleColumnRef) const { newColumnState, toggledColumns } = tableColumns.reduce<{ newColumnState: Column[] @@ -155,9 +155,6 @@ export const TableColumnsProvider: React.FC = ({ setTableColumns(newColumnState) - const controller = new AbortController() - tableStateControllerRef.current = controller - const result = await getTableState({ collectionSlug, columns: toggledColumns, @@ -172,6 +169,8 @@ export const TableColumnsProvider: React.FC = ({ setTableColumns(result.state) setTable(result.Table) } + + abortToggleColumnRef.current = null }, [ tableColumns, @@ -281,8 +280,10 @@ export const TableColumnsProvider: React.FC = ({ }, [columnState]) useEffect(() => { + const abortTableState = abortTableStateRef.current + return () => { - abortAndIgnore(tableStateControllerRef.current) + abortAndIgnore(abortTableState) } }, []) diff --git a/packages/ui/src/exports/shared/index.ts b/packages/ui/src/exports/shared/index.ts index ae7387f8eb..1ed6b2e7f4 100644 --- a/packages/ui/src/exports/shared/index.ts +++ b/packages/ui/src/exports/shared/index.ts @@ -8,7 +8,7 @@ export { mergeFieldStyles } from '../../fields/mergeFieldStyles.js' export { reduceToSerializableFields } from '../../forms/Form/reduceToSerializableFields.js' export { PayloadIcon } from '../../graphics/Icon/index.js' export { PayloadLogo } from '../../graphics/Logo/index.js' -export { abortAndIgnore } from '../../utilities/abortAndIgnore.js' +export { abortAndIgnore, handleAbortRef } from '../../utilities/abortAndIgnore.js' export { requests } from '../../utilities/api.js' export { findLocaleFromCode } from '../../utilities/findLocaleFromCode.js' export { formatAdminURL } from '../../utilities/formatAdminURL.js' diff --git a/packages/ui/src/fields/Array/ArrayRow.tsx b/packages/ui/src/fields/Array/ArrayRow.tsx index e664940ac9..395bc35a3f 100644 --- a/packages/ui/src/fields/Array/ArrayRow.tsx +++ b/packages/ui/src/fields/Array/ArrayRow.tsx @@ -130,7 +130,9 @@ export const ArrayRow: React.FC = ({ } header={
- {isLoading ? null : ( + {isLoading ? ( + + ) : ( = ({ : undefined } header={ - isLoading - ? null - : Label || ( -
- - {String(rowIndex + 1).padStart(2, '0')} - - - {getTranslation(block.labels.singular, i18n)} - - - {fieldHasErrors && } -
- ) + isLoading ? ( + + ) : ( + Label || ( +
+ + {String(rowIndex + 1).padStart(2, '0')} + + + {getTranslation(block.labels.singular, i18n)} + + + {fieldHasErrors && } +
+ ) + ) } isCollapsed={row.collapsed} key={row.id} diff --git a/packages/ui/src/forms/Form/index.tsx b/packages/ui/src/forms/Form/index.tsx index 857dfc1202..77e563e79c 100644 --- a/packages/ui/src/forms/Form/index.tsx +++ b/packages/ui/src/forms/Form/index.tsx @@ -30,7 +30,7 @@ import { useLocale } from '../../providers/Locale/index.js' import { useOperation } from '../../providers/Operation/index.js' import { useServerFunctions } from '../../providers/ServerFunctions/index.js' import { useTranslation } from '../../providers/Translation/index.js' -import { abortAndIgnore } from '../../utilities/abortAndIgnore.js' +import { abortAndIgnore, handleAbortRef } from '../../utilities/abortAndIgnore.js' import { requests } from '../../utilities/api.js' import { FormContext, @@ -93,7 +93,7 @@ export const Form: React.FC = (props) => { const [submitted, setSubmitted] = useState(false) const formRef = useRef(null) const contextRef = useRef({} as FormContextType) - const resetFormStateAbortControllerRef = useRef(null) + const abortResetFormRef = useRef(null) const fieldsReducer = useReducer(fieldReducer, {}, () => initialState) @@ -483,10 +483,7 @@ export const Form: React.FC = (props) => { const reset = useCallback( async (data: unknown) => { - abortAndIgnore(resetFormStateAbortControllerRef.current) - - const controller = new AbortController() - resetFormStateAbortControllerRef.current = controller + const controller = handleAbortRef(abortResetFormRef) const docPreferences = await getDocPreferences() @@ -507,6 +504,8 @@ export const Form: React.FC = (props) => { contextRef.current = { ...initContextState } as FormContextType setModified(false) dispatchFields({ type: 'REPLACE_STATE', state: newState }) + + abortResetFormRef.current = null }, [ collectionSlug, @@ -576,8 +575,10 @@ export const Form: React.FC = (props) => { ) useEffect(() => { + const abortOnChange = abortResetFormRef.current + return () => { - abortAndIgnore(resetFormStateAbortControllerRef.current) + abortAndIgnore(abortOnChange) } }, []) diff --git a/packages/ui/src/providers/DocumentInfo/index.tsx b/packages/ui/src/providers/DocumentInfo/index.tsx index 6bbd9ab1eb..8e31118154 100644 --- a/packages/ui/src/providers/DocumentInfo/index.tsx +++ b/packages/ui/src/providers/DocumentInfo/index.tsx @@ -95,6 +95,7 @@ const DocumentInfo: React.FC< const [mostRecentVersionIsAutosaved, setMostRecentVersionIsAutosaved] = useState( mostRecentVersionIsAutosavedFromProps, ) + const [versionCount, setVersionCount] = useState(versionCountFromProps) const [hasPublishedDoc, setHasPublishedDoc] = useState(hasPublishedDocFromProps) diff --git a/packages/ui/src/utilities/abortAndIgnore.ts b/packages/ui/src/utilities/abortAndIgnore.ts index fa1ae4070f..721afdb647 100644 --- a/packages/ui/src/utilities/abortAndIgnore.ts +++ b/packages/ui/src/utilities/abortAndIgnore.ts @@ -1,9 +1,26 @@ -export function abortAndIgnore(controller: AbortController) { - if (controller) { +export function abortAndIgnore(abortController: AbortController) { + if (abortController) { try { - controller.abort() + abortController.abort() } catch (_err) { // swallow error } } } + +export function handleAbortRef( + abortControllerRef: React.RefObject, +): AbortController { + if (abortControllerRef.current) { + try { + abortControllerRef.current.abort() + abortControllerRef.current = null + } catch (_err) { + // swallow error + } + } else { + const controller = new AbortController() + abortControllerRef.current = controller + return controller + } +} diff --git a/packages/ui/src/views/Edit/index.tsx b/packages/ui/src/views/Edit/index.tsx index 82c090038e..a3a8579fff 100644 --- a/packages/ui/src/views/Edit/index.tsx +++ b/packages/ui/src/views/Edit/index.tsx @@ -31,7 +31,7 @@ import { useEditDepth } from '../../providers/EditDepth/index.js' import { OperationProvider } from '../../providers/Operation/index.js' import { useServerFunctions } from '../../providers/ServerFunctions/index.js' import { useUploadEdits } from '../../providers/UploadEdits/index.js' -import { abortAndIgnore } from '../../utilities/abortAndIgnore.js' +import { abortAndIgnore, handleAbortRef } from '../../utilities/abortAndIgnore.js' import { formatAdminURL } from '../../utilities/formatAdminURL.js' import { handleBackToDashboard } from '../../utilities/handleBackToDashboard.js' import { handleGoBack } from '../../utilities/handleGoBack.js' @@ -120,8 +120,8 @@ export const DefaultEditView: React.FC = ({ const { resetUploadEdits } = useUploadEdits() const { getFormState } = useServerFunctions() - const onChangeAbortControllerRef = useRef(null) - const onSaveAbortControllerRef = useRef(null) + const abortOnChangeRef = useRef(null) + const abortOnSaveRef = useRef(null) const locale = params.get('locale') @@ -142,22 +142,13 @@ export const DefaultEditView: React.FC = ({ typeof lockDocumentsProp === 'object' ? lockDocumentsProp.duration : lockDurationDefault const lockDurationInMilliseconds = lockDuration * 1000 - let preventLeaveWithoutSaving = true - let autosaveEnabled = false + const autosaveEnabled = Boolean( + (collectionConfig?.versions?.drafts && collectionConfig?.versions?.drafts?.autosave) || + (globalConfig?.versions?.drafts && globalConfig?.versions?.drafts?.autosave), + ) - if (collectionConfig) { - autosaveEnabled = Boolean( - collectionConfig?.versions?.drafts && collectionConfig?.versions?.drafts?.autosave, - ) - preventLeaveWithoutSaving = !autosaveEnabled - } else if (globalConfig) { - autosaveEnabled = Boolean( - globalConfig?.versions?.drafts && globalConfig?.versions?.drafts?.autosave, - ) - preventLeaveWithoutSaving = !autosaveEnabled - } else if (typeof disableLeaveWithoutSaving !== 'undefined') { - preventLeaveWithoutSaving = !disableLeaveWithoutSaving - } + const preventLeaveWithoutSaving = + typeof disableLeaveWithoutSaving !== 'undefined' ? !disableLeaveWithoutSaving : !autosaveEnabled const [isReadOnlyForIncomingUser, setIsReadOnlyForIncomingUser] = useState(false) const [showTakeOverModal, setShowTakeOverModal] = useState(false) @@ -238,6 +229,8 @@ export const DefaultEditView: React.FC = ({ const onSave = useCallback( async (json): Promise => { + const controller = handleAbortRef(abortOnSaveRef) + reportUpdate({ id, entitySlug, @@ -277,10 +270,6 @@ export const DefaultEditView: React.FC = ({ await getDocPermissions(json) if ((id || globalSlug) && !autosaveEnabled) { - abortAndIgnore(onSaveAbortControllerRef.current) - const controller = new AbortController() - onSaveAbortControllerRef.current = controller - const docPreferences = await getDocPreferences() const { state } = await getFormState({ @@ -302,6 +291,8 @@ export const DefaultEditView: React.FC = ({ setDocumentIsLocked(false) } + abortOnSaveRef.current = null + return state } }, @@ -337,10 +328,7 @@ export const DefaultEditView: React.FC = ({ const onChange: FormProps['onChange'][0] = useCallback( async ({ formState: prevFormState }) => { - abortAndIgnore(onChangeAbortControllerRef.current) - - const controller = new AbortController() - onChangeAbortControllerRef.current = controller + const controller = handleAbortRef(abortOnChangeRef) const currentTime = Date.now() const timeSinceLastUpdate = currentTime - editSessionStartTime @@ -374,6 +362,8 @@ export const DefaultEditView: React.FC = ({ handleDocumentLocking(lockedState) } + abortOnChangeRef.current = null + return state }, [ @@ -428,9 +418,12 @@ export const DefaultEditView: React.FC = ({ ]) useEffect(() => { + const abortOnChange = abortOnChangeRef.current + const abortOnSave = abortOnSaveRef.current + return () => { - abortAndIgnore(onChangeAbortControllerRef.current) - abortAndIgnore(onSaveAbortControllerRef.current) + abortAndIgnore(abortOnChange) + abortAndIgnore(abortOnSave) } }, []) diff --git a/templates/website/src/payload-types.ts b/templates/website/src/payload-types.ts index 4663ca9feb..61387fac8b 100644 --- a/templates/website/src/payload-types.ts +++ b/templates/website/src/payload-types.ts @@ -820,80 +820,11 @@ export interface PagesSelect { layout?: | T | { - cta?: - | T - | { - richText?: T; - links?: - | T - | { - link?: - | T - | { - type?: T; - newTab?: T; - reference?: T; - url?: T; - label?: T; - appearance?: T; - }; - id?: T; - }; - id?: T; - blockName?: T; - }; - content?: - | T - | { - columns?: - | T - | { - size?: T; - richText?: T; - enableLink?: T; - link?: - | T - | { - type?: T; - newTab?: T; - reference?: T; - url?: T; - label?: T; - appearance?: T; - }; - id?: T; - }; - id?: T; - blockName?: T; - }; - mediaBlock?: - | T - | { - media?: T; - id?: T; - blockName?: T; - }; - archive?: - | T - | { - introContent?: T; - populateBy?: T; - relationTo?: T; - categories?: T; - limit?: T; - selectedDocs?: T; - id?: T; - blockName?: T; - }; - formBlock?: - | T - | { - form?: T; - enableIntro?: T; - introContent?: T; - id?: T; - blockName?: T; - }; + cta?: T | CallToActionBlockSelect; + content?: T | ContentBlockSelect; + mediaBlock?: T | MediaBlockSelect; + archive?: T | ArchiveBlockSelect; + formBlock?: T | FormBlockSelect; }; meta?: | T @@ -909,6 +840,90 @@ export interface PagesSelect { createdAt?: T; _status?: T; } +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "CallToActionBlock_select". + */ +export interface CallToActionBlockSelect { + richText?: T; + links?: + | T + | { + link?: + | T + | { + type?: T; + newTab?: T; + reference?: T; + url?: T; + label?: T; + appearance?: T; + }; + id?: T; + }; + id?: T; + blockName?: T; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "ContentBlock_select". + */ +export interface ContentBlockSelect { + columns?: + | T + | { + size?: T; + richText?: T; + enableLink?: T; + link?: + | T + | { + type?: T; + newTab?: T; + reference?: T; + url?: T; + label?: T; + appearance?: T; + }; + id?: T; + }; + id?: T; + blockName?: T; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "MediaBlock_select". + */ +export interface MediaBlockSelect { + media?: T; + id?: T; + blockName?: T; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "ArchiveBlock_select". + */ +export interface ArchiveBlockSelect { + introContent?: T; + populateBy?: T; + relationTo?: T; + categories?: T; + limit?: T; + selectedDocs?: T; + id?: T; + blockName?: T; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "FormBlock_select". + */ +export interface FormBlockSelect { + form?: T; + enableIntro?: T; + introContent?: T; + id?: T; + blockName?: T; +} /** * This interface was referenced by `Config`'s JSON-Schema * via the `definition` "posts_select". diff --git a/test/fields/payload-types.ts b/test/fields/payload-types.ts index 1ab83d80f2..352cc72fc0 100644 --- a/test/fields/payload-types.ts +++ b/test/fields/payload-types.ts @@ -310,9 +310,6 @@ export interface LexicalMigrateField { export interface LexicalLocalizedField { id: string; title: string; - /** - * Non-localized field with localized block subfields - */ lexicalBlocksSubLocalized?: { root: { type: string; @@ -328,9 +325,6 @@ export interface LexicalLocalizedField { }; [k: string]: unknown; } | null; - /** - * Localized field with localized block subfields - */ lexicalBlocksLocalized?: { root: { type: string; @@ -481,9 +475,6 @@ export interface ArrayField { id?: string | null; }[] | null; - /** - * Row labels rendered as react components. - */ rowLabelAsComponent?: | { title?: string | null; @@ -598,9 +589,6 @@ export interface BlockField { } )[] | null; - /** - * The purpose of this field is to test validateExistingBlockIsIdentical works with similar blocks with group fields - */ blocksWithSimilarGroup?: | ( | { @@ -789,18 +777,9 @@ export interface TextField { id: string; text: string; hiddenTextField?: string | null; - /** - * This field should be hidden - */ adminHiddenTextField?: string | null; - /** - * This field should be disabled - */ disabledTextField?: string | null; localizedText?: string | null; - /** - * en description - */ i18nText?: string | null; defaultString?: string | null; defaultEmptyString?: string | null; @@ -921,14 +900,8 @@ export interface ConditionalLogic { userConditional?: string | null; parentGroup?: { enableParentGroupFields?: boolean | null; - /** - * Ensures we can rely on nested fields within `data`. - */ siblingField?: string | null; }; - /** - * Ensures we can rely on nested fields within `siblingsData`. - */ reliesOnParentGroup?: string | null; groupSelection?: ('group1' | 'group2') | null; group1?: { @@ -1008,9 +981,6 @@ export interface EmailField { email: string; localizedEmail?: string | null; emailWithAutocomplete?: string | null; - /** - * en description - */ i18nEmail?: string | null; defaultEmail?: string | null; defaultEmptyString?: string | null; @@ -1040,9 +1010,6 @@ export interface RadioField { */ export interface GroupField { id: string; - /** - * This is a group. - */ group: { text: string; defaultParent?: string | null; @@ -1435,9 +1402,6 @@ export interface RichTextField { [k: string]: unknown; }; lexicalCustomFields_html?: string | null; - /** - * This rich text field uses the lexical editor. - */ lexical?: { root: { type: string; @@ -1453,9 +1417,6 @@ export interface RichTextField { }; [k: string]: unknown; } | null; - /** - * This select field is rendered here to ensure its options dropdown renders above the rich text toolbar. - */ selectHasMany?: ('one' | 'two' | 'three' | 'four' | 'five' | 'six')[] | null; richText: { [k: string]: unknown; @@ -1544,9 +1505,6 @@ export interface TabsFields2 { */ export interface TabsField { id: string; - /** - * This should not collapse despite there being many tabs pushing the main fields open. - */ sidebarField?: string | null; array: { text: string; @@ -2157,42 +2115,257 @@ export interface BlockFieldsSelect { blocks?: | T | { - content?: T | ContentBlockSelect; - number?: T | NumberBlockSelect; - subBlocks?: T | SubBlocksBlockSelect; - tabs?: T | TabsBlockSelect; + content?: + | T + | { + text?: T; + richText?: T; + id?: T; + blockName?: T; + }; + number?: + | T + | { + number?: T; + id?: T; + blockName?: T; + }; + subBlocks?: + | T + | { + subBlocks?: + | T + | { + text?: + | T + | { + text?: T; + id?: T; + blockName?: T; + }; + number?: + | T + | { + number?: T; + id?: T; + blockName?: T; + }; + }; + id?: T; + blockName?: T; + }; + tabs?: + | T + | { + textInCollapsible?: T; + textInRow?: T; + id?: T; + blockName?: T; + }; }; duplicate?: | T | { - content?: T | ContentBlockSelect; - number?: T | NumberBlockSelect; - subBlocks?: T | SubBlocksBlockSelect; - tabs?: T | TabsBlockSelect; + content?: + | T + | { + text?: T; + richText?: T; + id?: T; + blockName?: T; + }; + number?: + | T + | { + number?: T; + id?: T; + blockName?: T; + }; + subBlocks?: + | T + | { + subBlocks?: + | T + | { + text?: + | T + | { + text?: T; + id?: T; + blockName?: T; + }; + number?: + | T + | { + number?: T; + id?: T; + blockName?: T; + }; + }; + id?: T; + blockName?: T; + }; + tabs?: + | T + | { + textInCollapsible?: T; + textInRow?: T; + id?: T; + blockName?: T; + }; }; collapsedByDefaultBlocks?: | T | { - localizedContent?: T | LocalizedContentBlockSelect; - localizedNumber?: T | LocalizedNumberBlockSelect; - localizedSubBlocks?: T | LocalizedSubBlocksBlockSelect; - localizedTabs?: T | LocalizedTabsBlockSelect; + localizedContent?: + | T + | { + text?: T; + richText?: T; + id?: T; + blockName?: T; + }; + localizedNumber?: + | T + | { + number?: T; + id?: T; + blockName?: T; + }; + localizedSubBlocks?: + | T + | { + subBlocks?: + | T + | { + text?: + | T + | { + text?: T; + id?: T; + blockName?: T; + }; + number?: + | T + | { + number?: T; + id?: T; + blockName?: T; + }; + }; + id?: T; + blockName?: T; + }; + localizedTabs?: + | T + | { + textInCollapsible?: T; + textInRow?: T; + id?: T; + blockName?: T; + }; }; disableSort?: | T | { - localizedContent?: T | LocalizedContentBlockSelect; - localizedNumber?: T | LocalizedNumberBlockSelect; - localizedSubBlocks?: T | LocalizedSubBlocksBlockSelect; - localizedTabs?: T | LocalizedTabsBlockSelect; + localizedContent?: + | T + | { + text?: T; + richText?: T; + id?: T; + blockName?: T; + }; + localizedNumber?: + | T + | { + number?: T; + id?: T; + blockName?: T; + }; + localizedSubBlocks?: + | T + | { + subBlocks?: + | T + | { + text?: + | T + | { + text?: T; + id?: T; + blockName?: T; + }; + number?: + | T + | { + number?: T; + id?: T; + blockName?: T; + }; + }; + id?: T; + blockName?: T; + }; + localizedTabs?: + | T + | { + textInCollapsible?: T; + textInRow?: T; + id?: T; + blockName?: T; + }; }; localizedBlocks?: | T | { - localizedContent?: T | LocalizedContentBlockSelect; - localizedNumber?: T | LocalizedNumberBlockSelect; - localizedSubBlocks?: T | LocalizedSubBlocksBlockSelect; - localizedTabs?: T | LocalizedTabsBlockSelect; + localizedContent?: + | T + | { + text?: T; + richText?: T; + id?: T; + blockName?: T; + }; + localizedNumber?: + | T + | { + number?: T; + id?: T; + blockName?: T; + }; + localizedSubBlocks?: + | T + | { + subBlocks?: + | T + | { + text?: + | T + | { + text?: T; + id?: T; + blockName?: T; + }; + number?: + | T + | { + number?: T; + id?: T; + blockName?: T; + }; + }; + id?: T; + blockName?: T; + }; + localizedTabs?: + | T + | { + textInCollapsible?: T; + textInRow?: T; + id?: T; + blockName?: T; + }; }; i18nBlocks?: | T @@ -2330,116 +2503,6 @@ export interface BlockFieldsSelect { updatedAt?: T; createdAt?: T; } -/** - * This interface was referenced by `Config`'s JSON-Schema - * via the `definition` "ContentBlock_select". - */ -export interface ContentBlockSelect { - text?: T; - richText?: T; - id?: T; - blockName?: T; -} -/** - * This interface was referenced by `Config`'s JSON-Schema - * via the `definition` "NumberBlock_select". - */ -export interface NumberBlockSelect { - number?: T; - id?: T; - blockName?: T; -} -/** - * This interface was referenced by `Config`'s JSON-Schema - * via the `definition` "SubBlocksBlock_select". - */ -export interface SubBlocksBlockSelect { - subBlocks?: - | T - | { - text?: - | T - | { - text?: T; - id?: T; - blockName?: T; - }; - number?: - | T - | { - number?: T; - id?: T; - blockName?: T; - }; - }; - id?: T; - blockName?: T; -} -/** - * This interface was referenced by `Config`'s JSON-Schema - * via the `definition` "TabsBlock_select". - */ -export interface TabsBlockSelect { - textInCollapsible?: T; - textInRow?: T; - id?: T; - blockName?: T; -} -/** - * This interface was referenced by `Config`'s JSON-Schema - * via the `definition` "localizedContentBlock_select". - */ -export interface LocalizedContentBlockSelect { - text?: T; - richText?: T; - id?: T; - blockName?: T; -} -/** - * This interface was referenced by `Config`'s JSON-Schema - * via the `definition` "localizedNumberBlock_select". - */ -export interface LocalizedNumberBlockSelect { - number?: T; - id?: T; - blockName?: T; -} -/** - * This interface was referenced by `Config`'s JSON-Schema - * via the `definition` "localizedSubBlocksBlock_select". - */ -export interface LocalizedSubBlocksBlockSelect { - subBlocks?: - | T - | { - text?: - | T - | { - text?: T; - id?: T; - blockName?: T; - }; - number?: - | T - | { - number?: T; - id?: T; - blockName?: T; - }; - }; - id?: T; - blockName?: T; -} -/** - * This interface was referenced by `Config`'s JSON-Schema - * via the `definition` "localizedTabsBlock_select". - */ -export interface LocalizedTabsBlockSelect { - textInCollapsible?: T; - textInRow?: T; - id?: T; - blockName?: T; -} /** * This interface was referenced by `Config`'s JSON-Schema * via the `definition` "checkbox-fields_select". @@ -3022,10 +3085,53 @@ export interface TabsFieldsSelect { blocks?: | T | { - content?: T | ContentBlockSelect; - number?: T | NumberBlockSelect; - subBlocks?: T | SubBlocksBlockSelect; - tabs?: T | TabsBlockSelect; + content?: + | T + | { + text?: T; + richText?: T; + id?: T; + blockName?: T; + }; + number?: + | T + | { + number?: T; + id?: T; + blockName?: T; + }; + subBlocks?: + | T + | { + subBlocks?: + | T + | { + text?: + | T + | { + text?: T; + id?: T; + blockName?: T; + }; + number?: + | T + | { + number?: T; + id?: T; + blockName?: T; + }; + }; + id?: T; + blockName?: T; + }; + tabs?: + | T + | { + textInCollapsible?: T; + textInRow?: T; + id?: T; + blockName?: T; + }; }; group?: | T @@ -3035,7 +3141,24 @@ export interface TabsFieldsSelect { textInRow?: T; numberInRow?: T; json?: T; - tab?: T | TabWithNameSelect; + tab?: + | T + | { + array?: + | T + | { + text?: T; + id?: T; + }; + text?: T; + defaultValue?: T; + arrayInRow?: + | T + | { + textInArrayInRow?: T; + id?: T; + }; + }; namedTabWithDefaultValue?: | T | { @@ -3085,26 +3208,6 @@ export interface TabsFieldsSelect { updatedAt?: T; createdAt?: T; } -/** - * This interface was referenced by `Config`'s JSON-Schema - * via the `definition` "TabWithName_select". - */ -export interface TabWithNameSelect { - array?: - | T - | { - text?: T; - id?: T; - }; - text?: T; - defaultValue?: T; - arrayInRow?: - | T - | { - textInArrayInRow?: T; - id?: T; - }; -} /** * This interface was referenced by `Config`'s JSON-Schema * via the `definition` "text-fields_select".