chore: improves abort controller logic for server functions (#9131)
### What? Removes abort controllers that were shared globally inside the server actions provider. ### Why? Constructing them in this way will cause different fetches using the same function to cancel one another accidentally. These are currently causing issues when two components call server functions, even different functions, because the global ref inside was being overwritten and aborting the previous one. ### How? Standardizes how we construct and destroy abort controllers. This PR is focused around creating them to pass into the exposed serverAction provider functions. There are other places where this pattern can be applied.
This commit is contained in:
@@ -20,7 +20,8 @@ import {
|
||||
useServerFunctions,
|
||||
useTranslation,
|
||||
} from '@payloadcms/ui'
|
||||
import React from 'react'
|
||||
import { abortAndIgnore } from '@payloadcms/ui/shared'
|
||||
import React, { useEffect } from 'react'
|
||||
|
||||
export const CreateFirstUserClient: React.FC<{
|
||||
docPermissions: DocumentPermissions
|
||||
@@ -42,10 +43,17 @@ export const CreateFirstUserClient: React.FC<{
|
||||
const { t } = useTranslation()
|
||||
const { setUser } = useAuth()
|
||||
|
||||
const formStateAbortControllerRef = React.useRef<AbortController>(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 { state } = await getFormState({
|
||||
collectionSlug: userSlug,
|
||||
docPermissions,
|
||||
@@ -53,6 +61,7 @@ export const CreateFirstUserClient: React.FC<{
|
||||
formState: prevFormState,
|
||||
operation: 'create',
|
||||
schemaPath: `_${userSlug}.auth`,
|
||||
signal: controller.signal,
|
||||
})
|
||||
|
||||
return state
|
||||
@@ -64,6 +73,12 @@ export const CreateFirstUserClient: React.FC<{
|
||||
setUser(data)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
abortAndIgnore(formStateAbortControllerRef.current)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Form
|
||||
action={`${serverURL}${apiRoute}/${userSlug}/first-register`}
|
||||
|
||||
@@ -28,7 +28,12 @@ import {
|
||||
useServerFunctions,
|
||||
useTranslation,
|
||||
} from '@payloadcms/ui'
|
||||
import { handleBackToDashboard, handleGoBack, handleTakeOver } from '@payloadcms/ui/shared'
|
||||
import {
|
||||
abortAndIgnore,
|
||||
handleBackToDashboard,
|
||||
handleGoBack,
|
||||
handleTakeOver,
|
||||
} from '@payloadcms/ui/shared'
|
||||
import { useRouter } from 'next/navigation.js'
|
||||
import React, { Fragment, useCallback, useEffect, useRef, useState } from 'react'
|
||||
|
||||
@@ -116,7 +121,7 @@ const PreviewView: React.FC<Props> = ({
|
||||
const [isReadOnlyForIncomingUser, setIsReadOnlyForIncomingUser] = useState(false)
|
||||
const [showTakeOverModal, setShowTakeOverModal] = useState(false)
|
||||
|
||||
const abortControllerRef = useRef(new AbortController())
|
||||
const formStateAbortControllerRef = useRef(new AbortController())
|
||||
|
||||
const [editSessionStartTime, setEditSessionStartTime] = useState(Date.now())
|
||||
|
||||
@@ -176,16 +181,10 @@ const PreviewView: React.FC<Props> = ({
|
||||
|
||||
const onChange: FormProps['onChange'][0] = useCallback(
|
||||
async ({ formState: prevFormState }) => {
|
||||
if (abortControllerRef.current) {
|
||||
try {
|
||||
abortControllerRef.current.abort()
|
||||
} catch (_err) {
|
||||
// swallow error
|
||||
}
|
||||
}
|
||||
abortAndIgnore(formStateAbortControllerRef.current)
|
||||
|
||||
const abortController = new AbortController()
|
||||
abortControllerRef.current = abortController
|
||||
const controller = new AbortController()
|
||||
formStateAbortControllerRef.current = controller
|
||||
|
||||
const currentTime = Date.now()
|
||||
const timeSinceLastUpdate = currentTime - editSessionStartTime
|
||||
@@ -208,7 +207,7 @@ const PreviewView: React.FC<Props> = ({
|
||||
operation,
|
||||
returnLockStatus: isLockingEnabled ? true : false,
|
||||
schemaPath,
|
||||
signal: abortController.signal,
|
||||
signal: controller.signal,
|
||||
updateLastEdited,
|
||||
})
|
||||
|
||||
@@ -265,14 +264,6 @@ const PreviewView: React.FC<Props> = ({
|
||||
// Clean up when the component unmounts or when the document is unlocked
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (abortControllerRef.current) {
|
||||
try {
|
||||
abortControllerRef.current.abort()
|
||||
} catch (_err) {
|
||||
// swallow error
|
||||
}
|
||||
}
|
||||
|
||||
if (!isLockingEnabled) {
|
||||
return
|
||||
}
|
||||
@@ -316,6 +307,12 @@ const PreviewView: React.FC<Props> = ({
|
||||
setDocumentIsLocked,
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
abortAndIgnore(formStateAbortControllerRef.current)
|
||||
}
|
||||
})
|
||||
|
||||
const shouldShowDocumentLockedModal =
|
||||
documentIsLocked &&
|
||||
currentEditor &&
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
useServerFunctions,
|
||||
useTranslation,
|
||||
} from '@payloadcms/ui'
|
||||
import { abortAndIgnore } from '@payloadcms/ui/shared'
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
|
||||
const baseClass = 'lexical-block'
|
||||
@@ -37,7 +38,7 @@ export const BlockComponent: React.FC<Props> = (props) => {
|
||||
const {
|
||||
fieldProps: { featureClientSchemaMap, field: parentLexicalRichTextField, path, schemaPath },
|
||||
} = useEditorConfigContext()
|
||||
const abortControllerRef = useRef(new AbortController())
|
||||
const onChangeAbortControllerRef = useRef(new AbortController())
|
||||
const { docPermissions, getDocPreferences } = useDocumentInfo()
|
||||
|
||||
const { getFormState } = useServerFunctions()
|
||||
@@ -69,7 +70,6 @@ export const BlockComponent: React.FC<Props> = (props) => {
|
||||
data: formData,
|
||||
docPermissions,
|
||||
docPreferences: await getDocPreferences(),
|
||||
doNotAbort: true,
|
||||
globalSlug,
|
||||
operation: 'update',
|
||||
renderAllFields: true,
|
||||
@@ -94,11 +94,7 @@ export const BlockComponent: React.FC<Props> = (props) => {
|
||||
}
|
||||
|
||||
return () => {
|
||||
try {
|
||||
abortController.abort()
|
||||
} catch (_err) {
|
||||
// swallow error
|
||||
}
|
||||
abortAndIgnore(abortController)
|
||||
}
|
||||
}, [
|
||||
getFormState,
|
||||
@@ -108,32 +104,26 @@ export const BlockComponent: React.FC<Props> = (props) => {
|
||||
globalSlug,
|
||||
getDocPreferences,
|
||||
docPermissions,
|
||||
]) // DO NOT ADD FORMDATA HERE! Adding formData will kick you out of sub block editors while writing.
|
||||
// DO NOT ADD FORMDATA HERE! Adding formData will kick you out of sub block editors while writing.
|
||||
])
|
||||
|
||||
const onChange = useCallback(
|
||||
async ({ formState: prevFormState }) => {
|
||||
if (abortControllerRef.current) {
|
||||
try {
|
||||
abortControllerRef.current.abort()
|
||||
} catch (_err) {
|
||||
// swallow error
|
||||
}
|
||||
}
|
||||
abortAndIgnore(onChangeAbortControllerRef.current)
|
||||
|
||||
const abortController = new AbortController()
|
||||
abortControllerRef.current = abortController
|
||||
const controller = new AbortController()
|
||||
onChangeAbortControllerRef.current = controller
|
||||
|
||||
const { state: newFormState } = await getFormState({
|
||||
id,
|
||||
collectionSlug,
|
||||
docPermissions,
|
||||
docPreferences: await getDocPreferences(),
|
||||
doNotAbort: true,
|
||||
formState: prevFormState,
|
||||
globalSlug,
|
||||
operation: 'update',
|
||||
schemaPath: schemaFieldsPath,
|
||||
signal: abortController.signal,
|
||||
signal: controller.signal,
|
||||
})
|
||||
|
||||
if (!newFormState) {
|
||||
@@ -162,16 +152,9 @@ export const BlockComponent: React.FC<Props> = (props) => {
|
||||
],
|
||||
)
|
||||
|
||||
// cleanup effect
|
||||
useEffect(() => {
|
||||
const abortController = abortControllerRef.current
|
||||
|
||||
return () => {
|
||||
try {
|
||||
abortController.abort()
|
||||
} catch (_err) {
|
||||
// swallow error
|
||||
}
|
||||
abortAndIgnore(onChangeAbortControllerRef.current)
|
||||
}
|
||||
}, [])
|
||||
|
||||
@@ -243,7 +226,8 @@ export const BlockComponent: React.FC<Props> = (props) => {
|
||||
schemaFieldsPath,
|
||||
classNames,
|
||||
i18n,
|
||||
]) // DO NOT ADD FORMDATA HERE! Adding formData will kick you out of sub block editors while writing.
|
||||
// DO NOT ADD FORMDATA HERE! Adding formData will kick you out of sub block editors while writing.
|
||||
])
|
||||
|
||||
return <div className={baseClass + ' ' + baseClass + '-' + formData.blockType}>{formContent}</div>
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
useServerFunctions,
|
||||
useTranslation,
|
||||
} from '@payloadcms/ui'
|
||||
import { abortAndIgnore } from '@payloadcms/ui/shared'
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { v4 as uuid } from 'uuid'
|
||||
|
||||
@@ -28,7 +29,7 @@ export const DrawerContent: React.FC<Omit<FieldsDrawerProps, 'drawerSlug' | 'dra
|
||||
const { t } = useTranslation()
|
||||
const { id, collectionSlug, docPermissions, getDocPreferences, globalSlug } = useDocumentInfo()
|
||||
|
||||
const abortControllerRef = useRef(new AbortController())
|
||||
const onChangeAbortControllerRef = useRef(new AbortController())
|
||||
|
||||
const [initialState, setInitialState] = useState<false | FormState | undefined>(false)
|
||||
|
||||
@@ -45,7 +46,7 @@ export const DrawerContent: React.FC<Omit<FieldsDrawerProps, 'drawerSlug' | 'dra
|
||||
const fields: any = fieldMapOverride ?? featureClientSchemaMap[featureKey]?.[schemaFieldsPath] // Field Schema
|
||||
|
||||
useEffect(() => {
|
||||
const abortController = new AbortController()
|
||||
const controller = new AbortController()
|
||||
|
||||
const awaitInitialState = async () => {
|
||||
const { state } = await getFormState({
|
||||
@@ -54,12 +55,11 @@ export const DrawerContent: React.FC<Omit<FieldsDrawerProps, 'drawerSlug' | 'dra
|
||||
data: data ?? {},
|
||||
docPermissions,
|
||||
docPreferences: await getDocPreferences(),
|
||||
doNotAbort: true,
|
||||
globalSlug,
|
||||
operation: 'update',
|
||||
renderAllFields: true,
|
||||
schemaPath: schemaFieldsPath,
|
||||
signal: abortController.signal,
|
||||
signal: controller.signal,
|
||||
})
|
||||
|
||||
setInitialState(state)
|
||||
@@ -68,11 +68,7 @@ export const DrawerContent: React.FC<Omit<FieldsDrawerProps, 'drawerSlug' | 'dra
|
||||
void awaitInitialState()
|
||||
|
||||
return () => {
|
||||
try {
|
||||
abortController.abort()
|
||||
} catch (_err) {
|
||||
// swallow error
|
||||
}
|
||||
abortAndIgnore(controller)
|
||||
}
|
||||
}, [
|
||||
schemaFieldsPath,
|
||||
@@ -87,16 +83,10 @@ export const DrawerContent: React.FC<Omit<FieldsDrawerProps, 'drawerSlug' | 'dra
|
||||
|
||||
const onChange = useCallback(
|
||||
async ({ formState: prevFormState }) => {
|
||||
if (abortControllerRef.current) {
|
||||
try {
|
||||
abortControllerRef.current.abort()
|
||||
} catch (_err) {
|
||||
// swallow error
|
||||
}
|
||||
}
|
||||
abortAndIgnore(onChangeAbortControllerRef.current)
|
||||
|
||||
const abortController = new AbortController()
|
||||
abortControllerRef.current = abortController
|
||||
const controller = new AbortController()
|
||||
onChangeAbortControllerRef.current = controller
|
||||
|
||||
const { state } = await getFormState({
|
||||
id,
|
||||
@@ -107,7 +97,7 @@ export const DrawerContent: React.FC<Omit<FieldsDrawerProps, 'drawerSlug' | 'dra
|
||||
globalSlug,
|
||||
operation: 'update',
|
||||
schemaPath: schemaFieldsPath,
|
||||
signal: abortController.signal,
|
||||
signal: controller.signal,
|
||||
})
|
||||
|
||||
if (!state) {
|
||||
@@ -129,14 +119,8 @@ export const DrawerContent: React.FC<Omit<FieldsDrawerProps, 'drawerSlug' | 'dra
|
||||
|
||||
// cleanup effect
|
||||
useEffect(() => {
|
||||
const abortController = abortControllerRef.current
|
||||
|
||||
return () => {
|
||||
try {
|
||||
abortController.abort()
|
||||
} catch (_err) {
|
||||
// swallow error
|
||||
}
|
||||
abortAndIgnore(onChangeAbortControllerRef.current)
|
||||
}
|
||||
}, [])
|
||||
|
||||
|
||||
@@ -100,7 +100,6 @@ export const LinkButton: React.FC<{
|
||||
data,
|
||||
docPermissions,
|
||||
docPreferences: await getDocPreferences(),
|
||||
doNotAbort: true,
|
||||
globalSlug,
|
||||
operation: 'update',
|
||||
renderAllFields: true,
|
||||
|
||||
@@ -103,7 +103,6 @@ export const LinkElement = () => {
|
||||
data,
|
||||
docPermissions,
|
||||
docPreferences: await getDocPreferences(),
|
||||
doNotAbort: true,
|
||||
globalSlug,
|
||||
operation: 'update',
|
||||
renderAllFields: true,
|
||||
|
||||
@@ -77,7 +77,6 @@ export const UploadDrawer: React.FC<{
|
||||
data,
|
||||
docPermissions,
|
||||
docPreferences: await getDocPreferences(),
|
||||
doNotAbort: true,
|
||||
globalSlug,
|
||||
operation: 'update',
|
||||
renderAllFields: true,
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import type { ClientCollectionConfig } from 'payload'
|
||||
|
||||
import { useRouter, useSearchParams } from 'next/navigation.js'
|
||||
import React, { useCallback } from 'react'
|
||||
import React, { useCallback, useEffect } from 'react'
|
||||
|
||||
import type { EditFormProps } from './types.js'
|
||||
|
||||
@@ -17,6 +17,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 { formatAdminURL } from '../../../utilities/formatAdminURL.js'
|
||||
import { useDocumentDrawerContext } from '../../DocumentDrawer/Provider.js'
|
||||
import { DocumentFields } from '../../DocumentFields/index.js'
|
||||
@@ -55,8 +56,9 @@ export function EditForm({ submitted }: EditFormProps) {
|
||||
getEntityConfig,
|
||||
} = useConfig()
|
||||
|
||||
const collectionConfig = getEntityConfig({ collectionSlug: docSlug }) as ClientCollectionConfig
|
||||
const formStateAbortControllerRef = React.useRef<AbortController>(null)
|
||||
|
||||
const collectionConfig = getEntityConfig({ collectionSlug: docSlug }) as ClientCollectionConfig
|
||||
const router = useRouter()
|
||||
const depth = useEditDepth()
|
||||
const params = useSearchParams()
|
||||
@@ -109,6 +111,11 @@ export function EditForm({ submitted }: EditFormProps) {
|
||||
|
||||
const onChange: NonNullable<FormProps['onChange']>[0] = useCallback(
|
||||
async ({ formState: prevFormState }) => {
|
||||
abortAndIgnore(formStateAbortControllerRef.current)
|
||||
|
||||
const controller = new AbortController()
|
||||
formStateAbortControllerRef.current = controller
|
||||
|
||||
const docPreferences = await getDocPreferences()
|
||||
const { state: newFormState } = await getFormState({
|
||||
collectionSlug,
|
||||
@@ -117,6 +124,7 @@ export function EditForm({ submitted }: EditFormProps) {
|
||||
formState: prevFormState,
|
||||
operation: 'create',
|
||||
schemaPath,
|
||||
signal: controller.signal,
|
||||
})
|
||||
|
||||
return newFormState
|
||||
@@ -124,6 +132,12 @@ export function EditForm({ submitted }: EditFormProps) {
|
||||
[collectionSlug, schemaPath, getDocPreferences, getFormState, docPermissions],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
abortAndIgnore(formStateAbortControllerRef.current)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<OperationProvider operation="create">
|
||||
<BulkUploadProvider>
|
||||
|
||||
@@ -10,6 +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 { DocumentDrawerContextProvider } from './Provider.js'
|
||||
|
||||
export const DocumentDrawerContent: React.FC<DocumentDrawerProps> = ({
|
||||
@@ -35,6 +36,8 @@ export const DocumentDrawerContent: React.FC<DocumentDrawerProps> = ({
|
||||
collections.find((collection) => collection.slug === collectionSlug),
|
||||
)
|
||||
|
||||
const documentViewAbortControllerRef = React.useRef<AbortController>(null)
|
||||
|
||||
const { closeModal } = useModal()
|
||||
const { t } = useTranslation()
|
||||
|
||||
@@ -44,7 +47,13 @@ export const DocumentDrawerContent: React.FC<DocumentDrawerProps> = ({
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
|
||||
const getDocumentView = useCallback(
|
||||
async (docID?: number | string, doNotAbort?: boolean) => {
|
||||
(docID?: number | string) => {
|
||||
abortAndIgnore(documentViewAbortControllerRef.current)
|
||||
|
||||
const controller = new AbortController()
|
||||
documentViewAbortControllerRef.current = controller
|
||||
|
||||
const fetchDocumentView = async () => {
|
||||
setIsLoading(true)
|
||||
|
||||
try {
|
||||
@@ -52,12 +61,12 @@ export const DocumentDrawerContent: React.FC<DocumentDrawerProps> = ({
|
||||
collectionSlug,
|
||||
disableActions,
|
||||
docID,
|
||||
doNotAbort,
|
||||
drawerSlug,
|
||||
initialData,
|
||||
redirectAfterDelete: redirectAfterDelete !== undefined ? redirectAfterDelete : false,
|
||||
redirectAfterDuplicate:
|
||||
redirectAfterDuplicate !== undefined ? redirectAfterDuplicate : false,
|
||||
signal: controller.signal,
|
||||
})
|
||||
|
||||
if (result?.Document) {
|
||||
@@ -69,6 +78,9 @@ export const DocumentDrawerContent: React.FC<DocumentDrawerProps> = ({
|
||||
closeModal(drawerSlug)
|
||||
// toast.error(data?.errors?.[0].message || t('error:unspecific'))
|
||||
}
|
||||
}
|
||||
|
||||
void fetchDocumentView()
|
||||
},
|
||||
[
|
||||
collectionSlug,
|
||||
@@ -79,20 +91,13 @@ export const DocumentDrawerContent: React.FC<DocumentDrawerProps> = ({
|
||||
redirectAfterDuplicate,
|
||||
renderDocument,
|
||||
closeModal,
|
||||
initialState,
|
||||
t,
|
||||
],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (!DocumentView) {
|
||||
void getDocumentView(existingDocID, true)
|
||||
}
|
||||
}, [DocumentView, getDocumentView, existingDocID])
|
||||
|
||||
const onSave = useCallback<DocumentDrawerProps['onSave']>(
|
||||
(args) => {
|
||||
void getDocumentView(args.doc.id)
|
||||
getDocumentView(args.doc.id)
|
||||
|
||||
if (typeof onSaveFromProps === 'function') {
|
||||
void onSaveFromProps({
|
||||
@@ -104,9 +109,9 @@ export const DocumentDrawerContent: React.FC<DocumentDrawerProps> = ({
|
||||
[onSaveFromProps, collectionConfig, getDocumentView],
|
||||
)
|
||||
|
||||
const onDuplicate = useCallback<DocumentDrawerProps['onSave']>(
|
||||
const onDuplicate = useCallback<DocumentDrawerProps['onDuplicate']>(
|
||||
(args) => {
|
||||
void getDocumentView(args.doc.id)
|
||||
getDocumentView(args.doc.id)
|
||||
|
||||
if (typeof onDuplicateFromProps === 'function') {
|
||||
void onDuplicateFromProps({
|
||||
@@ -133,9 +138,22 @@ export const DocumentDrawerContent: React.FC<DocumentDrawerProps> = ({
|
||||
)
|
||||
|
||||
const clearDoc = useCallback(() => {
|
||||
void getDocumentView()
|
||||
getDocumentView()
|
||||
}, [getDocumentView])
|
||||
|
||||
useEffect(() => {
|
||||
if (!DocumentView) {
|
||||
getDocumentView(existingDocID)
|
||||
}
|
||||
}, [DocumentView, getDocumentView, existingDocID])
|
||||
|
||||
// Cleanup any pending requests when the component unmounts
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
abortAndIgnore(documentViewAbortControllerRef.current)
|
||||
}
|
||||
}, [])
|
||||
|
||||
if (isLoading) {
|
||||
return <LoadingOverlay />
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { ClientCollectionConfig, FormState } from 'payload'
|
||||
import { useModal } from '@faceless-ui/modal'
|
||||
import { getTranslation } from '@payloadcms/translations'
|
||||
import { useRouter } from 'next/navigation.js'
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
|
||||
import type { FormProps } from '../../forms/Form/index.js'
|
||||
|
||||
@@ -23,6 +23,7 @@ import { useSearchParams } from '../../providers/SearchParams/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 { Drawer, DrawerToggler } from '../Drawer/index.js'
|
||||
import { FieldSelect } from '../FieldSelect/index.js'
|
||||
import './index.scss'
|
||||
@@ -127,6 +128,7 @@ export const EditMany: React.FC<EditManyProps> = (props) => {
|
||||
const router = useRouter()
|
||||
const [initialState, setInitialState] = useState<FormState>()
|
||||
const hasInitializedState = React.useRef(false)
|
||||
const formStateAbortControllerRef = React.useRef<AbortController>(null)
|
||||
const { clearRouteCache } = useRouteCache()
|
||||
|
||||
const collectionPermissions = permissions?.collections?.[slug]
|
||||
@@ -135,6 +137,8 @@ export const EditMany: React.FC<EditManyProps> = (props) => {
|
||||
const drawerSlug = `edit-${slug}`
|
||||
|
||||
React.useEffect(() => {
|
||||
const controller = new AbortController()
|
||||
|
||||
if (!hasInitializedState.current) {
|
||||
const getInitialState = async () => {
|
||||
const { state: result } = await getFormState({
|
||||
@@ -144,6 +148,7 @@ export const EditMany: React.FC<EditManyProps> = (props) => {
|
||||
docPreferences: null,
|
||||
operation: 'update',
|
||||
schemaPath: slug,
|
||||
signal: controller.signal,
|
||||
})
|
||||
|
||||
setInitialState(result)
|
||||
@@ -151,11 +156,20 @@ export const EditMany: React.FC<EditManyProps> = (props) => {
|
||||
}
|
||||
|
||||
void getInitialState()
|
||||
|
||||
return () => {
|
||||
abortAndIgnore(controller)
|
||||
}
|
||||
}
|
||||
}, [apiRoute, hasInitializedState, serverURL, slug, getFormState, user, collectionPermissions])
|
||||
|
||||
const onChange: FormProps['onChange'][0] = useCallback(
|
||||
async ({ formState: prevFormState }) => {
|
||||
abortAndIgnore(formStateAbortControllerRef.current)
|
||||
|
||||
const controller = new AbortController()
|
||||
formStateAbortControllerRef.current = controller
|
||||
|
||||
const { state } = await getFormState({
|
||||
collectionSlug: slug,
|
||||
docPermissions: collectionPermissions,
|
||||
@@ -163,6 +177,7 @@ export const EditMany: React.FC<EditManyProps> = (props) => {
|
||||
formState: prevFormState,
|
||||
operation: 'update',
|
||||
schemaPath: slug,
|
||||
signal: controller.signal,
|
||||
})
|
||||
|
||||
return state
|
||||
@@ -170,6 +185,10 @@ export const EditMany: React.FC<EditManyProps> = (props) => {
|
||||
[slug, getFormState, collectionPermissions],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
abortAndIgnore(formStateAbortControllerRef.current)
|
||||
}, [])
|
||||
|
||||
if (selectAll === SelectAllStatus.None || !hasUpdatePermission) {
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ export { WithServerSideProps } from '../../elements/WithServerSideProps/index.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 { requests } from '../../utilities/api.js'
|
||||
export { findLocaleFromCode } from '../../utilities/findLocaleFromCode.js'
|
||||
export { formatAdminURL } from '../../utilities/formatAdminURL.js'
|
||||
|
||||
@@ -29,6 +29,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 { requests } from '../../utilities/api.js'
|
||||
import {
|
||||
FormContext,
|
||||
@@ -91,8 +92,7 @@ export const Form: React.FC<FormProps> = (props) => {
|
||||
const [submitted, setSubmitted] = useState(false)
|
||||
const formRef = useRef<HTMLFormElement>(null)
|
||||
const contextRef = useRef({} as FormContextType)
|
||||
const abortControllerRef = useRef(new AbortController())
|
||||
const abortControllerRef2 = useRef(new AbortController())
|
||||
const resetFormStateAbortControllerRef = useRef<AbortController>(null)
|
||||
|
||||
const fieldsReducer = useReducer(fieldReducer, {}, () => initialState)
|
||||
|
||||
@@ -456,16 +456,10 @@ export const Form: React.FC<FormProps> = (props) => {
|
||||
|
||||
const reset = useCallback(
|
||||
async (data: unknown) => {
|
||||
if (abortControllerRef.current) {
|
||||
try {
|
||||
abortControllerRef.current.abort()
|
||||
} catch (error) {
|
||||
// swallow error
|
||||
}
|
||||
}
|
||||
abortAndIgnore(resetFormStateAbortControllerRef.current)
|
||||
|
||||
const abortController = new AbortController()
|
||||
abortControllerRef.current = abortController
|
||||
const controller = new AbortController()
|
||||
resetFormStateAbortControllerRef.current = controller
|
||||
|
||||
const docPreferences = await getDocPreferences()
|
||||
|
||||
@@ -479,7 +473,7 @@ export const Form: React.FC<FormProps> = (props) => {
|
||||
operation,
|
||||
renderAllFields: true,
|
||||
schemaPath: collectionSlug ? collectionSlug : globalSlug,
|
||||
signal: abortController.signal,
|
||||
signal: controller.signal,
|
||||
})
|
||||
|
||||
contextRef.current = { ...initContextState } as FormContextType
|
||||
@@ -548,27 +542,9 @@ export const Form: React.FC<FormProps> = (props) => {
|
||||
[dispatchFields, getDataByPath],
|
||||
)
|
||||
|
||||
// clean on unmount
|
||||
useEffect(() => {
|
||||
const re1 = abortControllerRef.current
|
||||
const re2 = abortControllerRef2.current
|
||||
|
||||
return () => {
|
||||
if (re1) {
|
||||
try {
|
||||
re1.abort()
|
||||
} catch (_err) {
|
||||
// swallow error
|
||||
}
|
||||
}
|
||||
|
||||
if (re2) {
|
||||
try {
|
||||
re2.abort()
|
||||
} catch (_err) {
|
||||
// swallow error
|
||||
}
|
||||
}
|
||||
abortAndIgnore(resetFormStateAbortControllerRef.current)
|
||||
}
|
||||
}, [])
|
||||
|
||||
|
||||
@@ -6,14 +6,13 @@ import type {
|
||||
ServerFunctionClient,
|
||||
} from 'payload'
|
||||
|
||||
import React, { createContext, useCallback, useEffect, useRef } from 'react'
|
||||
import React, { createContext, useCallback } from 'react'
|
||||
|
||||
import type { buildFormStateHandler } from '../../utilities/buildFormState.js'
|
||||
import type { buildTableStateHandler } from '../../utilities/buildTableState.js'
|
||||
|
||||
type GetFormStateClient = (
|
||||
args: {
|
||||
doNotAbort?: boolean
|
||||
signal?: AbortSignal
|
||||
} & Omit<BuildFormStateArgs, 'clientConfig' | 'req'>,
|
||||
) => ReturnType<typeof buildFormStateHandler>
|
||||
@@ -28,7 +27,6 @@ type RenderDocument = (args: {
|
||||
collectionSlug: string
|
||||
disableActions?: boolean
|
||||
docID?: number | string
|
||||
doNotAbort?: boolean
|
||||
drawerSlug?: string
|
||||
initialData?: Data
|
||||
redirectAfterDelete?: boolean
|
||||
@@ -69,10 +67,6 @@ export const ServerFunctionsProvider: React.FC<{
|
||||
throw new Error('ServerFunctionsProvider requires a serverFunction prop')
|
||||
}
|
||||
|
||||
// This is the local abort controller, to abort requests when the _provider_ itself unmounts, etc.
|
||||
// Each callback also accept a remote signal, to abort requests when each _component_ unmounts, etc.
|
||||
const abortControllerRef = useRef(new AbortController())
|
||||
|
||||
const getDocumentSlots = useCallback<GetDocumentSlots>(
|
||||
async (args) =>
|
||||
await serverFunction({
|
||||
@@ -84,39 +78,16 @@ export const ServerFunctionsProvider: React.FC<{
|
||||
|
||||
const getFormState = useCallback<GetFormStateClient>(
|
||||
async (args) => {
|
||||
if (args?.doNotAbort) {
|
||||
try {
|
||||
const result = (await serverFunction({
|
||||
name: 'form-state',
|
||||
args,
|
||||
})) as ReturnType<typeof buildFormStateHandler> // TODO: infer this type when `strictNullChecks` is enabled
|
||||
|
||||
return result
|
||||
} catch (_err) {
|
||||
console.error(_err) // eslint-disable-line no-console
|
||||
}
|
||||
|
||||
return { state: null }
|
||||
}
|
||||
|
||||
if (abortControllerRef.current) {
|
||||
abortControllerRef.current.abort()
|
||||
}
|
||||
|
||||
const abortController = new AbortController()
|
||||
abortControllerRef.current = abortController
|
||||
const localSignal = abortController.signal
|
||||
|
||||
const { signal: remoteSignal, ...rest } = args || {}
|
||||
|
||||
try {
|
||||
if (!remoteSignal?.aborted && !localSignal?.aborted) {
|
||||
if (!remoteSignal?.aborted) {
|
||||
const result = (await serverFunction({
|
||||
name: 'form-state',
|
||||
args: rest,
|
||||
})) as ReturnType<typeof buildFormStateHandler> // TODO: infer this type when `strictNullChecks` is enabled
|
||||
|
||||
if (!remoteSignal?.aborted && !localSignal?.aborted) {
|
||||
if (!remoteSignal?.aborted) {
|
||||
return result
|
||||
}
|
||||
}
|
||||
@@ -131,15 +102,19 @@ export const ServerFunctionsProvider: React.FC<{
|
||||
|
||||
const getTableState = useCallback<GetTableStateClient>(
|
||||
async (args) => {
|
||||
const { ...rest } = args || {}
|
||||
const { signal: remoteSignal, ...rest } = args || {}
|
||||
|
||||
try {
|
||||
if (!remoteSignal?.aborted) {
|
||||
const result = (await serverFunction({
|
||||
name: 'table-state',
|
||||
args: rest,
|
||||
})) as ReturnType<typeof buildTableStateHandler> // TODO: infer this type when `strictNullChecks` is enabled
|
||||
|
||||
if (!remoteSignal?.aborted) {
|
||||
return result
|
||||
}
|
||||
}
|
||||
} catch (_err) {
|
||||
console.error(_err) // eslint-disable-line no-console
|
||||
}
|
||||
@@ -151,37 +126,16 @@ export const ServerFunctionsProvider: React.FC<{
|
||||
|
||||
const renderDocument = useCallback<RenderDocument>(
|
||||
async (args) => {
|
||||
if (args?.doNotAbort) {
|
||||
try {
|
||||
const result = (await serverFunction({
|
||||
name: 'render-document',
|
||||
args,
|
||||
})) as { docID: string; Document: React.ReactNode }
|
||||
|
||||
return result
|
||||
} catch (_err) {
|
||||
console.error(_err) // eslint-disable-line no-console
|
||||
return
|
||||
}
|
||||
}
|
||||
if (abortControllerRef.current) {
|
||||
abortControllerRef.current.abort()
|
||||
}
|
||||
|
||||
const abortController = new AbortController()
|
||||
abortControllerRef.current = abortController
|
||||
const localSignal = abortController.signal
|
||||
|
||||
const { signal: remoteSignal, ...rest } = args || {}
|
||||
|
||||
try {
|
||||
if (!remoteSignal?.aborted && !localSignal?.aborted) {
|
||||
if (!remoteSignal?.aborted) {
|
||||
const result = (await serverFunction({
|
||||
name: 'render-document',
|
||||
args: rest,
|
||||
})) as { docID: string; Document: React.ReactNode }
|
||||
|
||||
if (!remoteSignal?.aborted && !localSignal?.aborted) {
|
||||
if (!remoteSignal?.aborted) {
|
||||
return result
|
||||
}
|
||||
}
|
||||
@@ -192,20 +146,6 @@ export const ServerFunctionsProvider: React.FC<{
|
||||
[serverFunction],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
const controller = abortControllerRef.current
|
||||
|
||||
return () => {
|
||||
if (controller) {
|
||||
try {
|
||||
// controller.abort()
|
||||
} catch (_err) {
|
||||
// swallow error
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<ServerFunctionsContext.Provider
|
||||
value={{
|
||||
|
||||
9
packages/ui/src/utilities/abortAndIgnore.ts
Normal file
9
packages/ui/src/utilities/abortAndIgnore.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export function abortAndIgnore(controller: AbortController) {
|
||||
if (controller) {
|
||||
try {
|
||||
controller.abort()
|
||||
} catch (_err) {
|
||||
// swallow error
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,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 { formatAdminURL } from '../../utilities/formatAdminURL.js'
|
||||
import { handleBackToDashboard } from '../../utilities/handleBackToDashboard.js'
|
||||
import { handleGoBack } from '../../utilities/handleGoBack.js'
|
||||
@@ -115,7 +116,7 @@ export const DefaultEditView: React.FC<ClientSideEditViewProps> = ({
|
||||
const { resetUploadEdits } = useUploadEdits()
|
||||
const { getFormState } = useServerFunctions()
|
||||
|
||||
const abortControllerRef = useRef(new AbortController())
|
||||
const onChangeAbortControllerRef = useRef<AbortController>(null)
|
||||
|
||||
const locale = params.get('locale')
|
||||
|
||||
@@ -261,16 +262,10 @@ export const DefaultEditView: React.FC<ClientSideEditViewProps> = ({
|
||||
|
||||
const onChange: FormProps['onChange'][0] = useCallback(
|
||||
async ({ formState: prevFormState }) => {
|
||||
if (abortControllerRef.current) {
|
||||
try {
|
||||
abortControllerRef.current.abort()
|
||||
} catch (e) {
|
||||
// swallow error
|
||||
}
|
||||
}
|
||||
abortAndIgnore(onChangeAbortControllerRef.current)
|
||||
|
||||
const abortController = new AbortController()
|
||||
abortControllerRef.current = abortController
|
||||
const controller = new AbortController()
|
||||
onChangeAbortControllerRef.current = controller
|
||||
|
||||
const currentTime = Date.now()
|
||||
const timeSinceLastUpdate = currentTime - editSessionStartTime
|
||||
@@ -296,7 +291,7 @@ export const DefaultEditView: React.FC<ClientSideEditViewProps> = ({
|
||||
renderAllFields: false,
|
||||
returnLockStatus: isLockingEnabled ? true : false,
|
||||
schemaPath: schemaPathSegments.join('.'),
|
||||
// signal: abortController.signal,
|
||||
signal: controller.signal,
|
||||
updateLastEdited,
|
||||
})
|
||||
|
||||
@@ -352,14 +347,6 @@ export const DefaultEditView: React.FC<ClientSideEditViewProps> = ({
|
||||
// Clean up when the component unmounts or when the document is unlocked
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (abortControllerRef.current) {
|
||||
try {
|
||||
abortControllerRef.current.abort()
|
||||
} catch (e) {
|
||||
// swallow error
|
||||
}
|
||||
}
|
||||
|
||||
if (!isLockingEnabled) {
|
||||
return
|
||||
}
|
||||
@@ -403,6 +390,12 @@ export const DefaultEditView: React.FC<ClientSideEditViewProps> = ({
|
||||
setDocumentIsLocked,
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
abortAndIgnore(onChangeAbortControllerRef.current)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const shouldShowDocumentLockedModal =
|
||||
documentIsLocked &&
|
||||
currentEditor &&
|
||||
|
||||
@@ -481,10 +481,8 @@ describe('access control', () => {
|
||||
|
||||
await expect(page.locator('.unauthorized')).toBeVisible()
|
||||
|
||||
await page.goto(logoutURL)
|
||||
await page.waitForURL(logoutURL)
|
||||
|
||||
// Log back in for the next test
|
||||
await page.goto(logoutURL)
|
||||
await login({
|
||||
data: {
|
||||
email: devUser.email,
|
||||
@@ -528,6 +526,19 @@ describe('access control', () => {
|
||||
await page.waitForURL(unauthorizedURL)
|
||||
|
||||
await expect(page.locator('.unauthorized')).toBeVisible()
|
||||
|
||||
// Log back in for the next test
|
||||
await context.clearCookies()
|
||||
await page.goto(logoutURL)
|
||||
await page.waitForURL(logoutURL)
|
||||
await login({
|
||||
data: {
|
||||
email: devUser.email,
|
||||
password: devUser.password,
|
||||
},
|
||||
page,
|
||||
serverURL,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user