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,
|
useServerFunctions,
|
||||||
useTranslation,
|
useTranslation,
|
||||||
} from '@payloadcms/ui'
|
} from '@payloadcms/ui'
|
||||||
import React from 'react'
|
import { abortAndIgnore } from '@payloadcms/ui/shared'
|
||||||
|
import React, { useEffect } from 'react'
|
||||||
|
|
||||||
export const CreateFirstUserClient: React.FC<{
|
export const CreateFirstUserClient: React.FC<{
|
||||||
docPermissions: DocumentPermissions
|
docPermissions: DocumentPermissions
|
||||||
@@ -42,10 +43,17 @@ export const CreateFirstUserClient: React.FC<{
|
|||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { setUser } = useAuth()
|
const { setUser } = useAuth()
|
||||||
|
|
||||||
|
const formStateAbortControllerRef = React.useRef<AbortController>(null)
|
||||||
|
|
||||||
const collectionConfig = getEntityConfig({ collectionSlug: userSlug }) as ClientCollectionConfig
|
const collectionConfig = getEntityConfig({ collectionSlug: userSlug }) as ClientCollectionConfig
|
||||||
|
|
||||||
const onChange: FormProps['onChange'][0] = React.useCallback(
|
const onChange: FormProps['onChange'][0] = React.useCallback(
|
||||||
async ({ formState: prevFormState }) => {
|
async ({ formState: prevFormState }) => {
|
||||||
|
abortAndIgnore(formStateAbortControllerRef.current)
|
||||||
|
|
||||||
|
const controller = new AbortController()
|
||||||
|
formStateAbortControllerRef.current = controller
|
||||||
|
|
||||||
const { state } = await getFormState({
|
const { state } = await getFormState({
|
||||||
collectionSlug: userSlug,
|
collectionSlug: userSlug,
|
||||||
docPermissions,
|
docPermissions,
|
||||||
@@ -53,6 +61,7 @@ export const CreateFirstUserClient: React.FC<{
|
|||||||
formState: prevFormState,
|
formState: prevFormState,
|
||||||
operation: 'create',
|
operation: 'create',
|
||||||
schemaPath: `_${userSlug}.auth`,
|
schemaPath: `_${userSlug}.auth`,
|
||||||
|
signal: controller.signal,
|
||||||
})
|
})
|
||||||
|
|
||||||
return state
|
return state
|
||||||
@@ -64,6 +73,12 @@ export const CreateFirstUserClient: React.FC<{
|
|||||||
setUser(data)
|
setUser(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
abortAndIgnore(formStateAbortControllerRef.current)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form
|
<Form
|
||||||
action={`${serverURL}${apiRoute}/${userSlug}/first-register`}
|
action={`${serverURL}${apiRoute}/${userSlug}/first-register`}
|
||||||
|
|||||||
@@ -28,7 +28,12 @@ import {
|
|||||||
useServerFunctions,
|
useServerFunctions,
|
||||||
useTranslation,
|
useTranslation,
|
||||||
} from '@payloadcms/ui'
|
} 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 { useRouter } from 'next/navigation.js'
|
||||||
import React, { Fragment, useCallback, useEffect, useRef, useState } from 'react'
|
import React, { Fragment, useCallback, useEffect, useRef, useState } from 'react'
|
||||||
|
|
||||||
@@ -116,7 +121,7 @@ const PreviewView: React.FC<Props> = ({
|
|||||||
const [isReadOnlyForIncomingUser, setIsReadOnlyForIncomingUser] = useState(false)
|
const [isReadOnlyForIncomingUser, setIsReadOnlyForIncomingUser] = useState(false)
|
||||||
const [showTakeOverModal, setShowTakeOverModal] = useState(false)
|
const [showTakeOverModal, setShowTakeOverModal] = useState(false)
|
||||||
|
|
||||||
const abortControllerRef = useRef(new AbortController())
|
const formStateAbortControllerRef = useRef(new AbortController())
|
||||||
|
|
||||||
const [editSessionStartTime, setEditSessionStartTime] = useState(Date.now())
|
const [editSessionStartTime, setEditSessionStartTime] = useState(Date.now())
|
||||||
|
|
||||||
@@ -176,16 +181,10 @@ const PreviewView: React.FC<Props> = ({
|
|||||||
|
|
||||||
const onChange: FormProps['onChange'][0] = useCallback(
|
const onChange: FormProps['onChange'][0] = useCallback(
|
||||||
async ({ formState: prevFormState }) => {
|
async ({ formState: prevFormState }) => {
|
||||||
if (abortControllerRef.current) {
|
abortAndIgnore(formStateAbortControllerRef.current)
|
||||||
try {
|
|
||||||
abortControllerRef.current.abort()
|
|
||||||
} catch (_err) {
|
|
||||||
// swallow error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const abortController = new AbortController()
|
const controller = new AbortController()
|
||||||
abortControllerRef.current = abortController
|
formStateAbortControllerRef.current = controller
|
||||||
|
|
||||||
const currentTime = Date.now()
|
const currentTime = Date.now()
|
||||||
const timeSinceLastUpdate = currentTime - editSessionStartTime
|
const timeSinceLastUpdate = currentTime - editSessionStartTime
|
||||||
@@ -208,7 +207,7 @@ const PreviewView: React.FC<Props> = ({
|
|||||||
operation,
|
operation,
|
||||||
returnLockStatus: isLockingEnabled ? true : false,
|
returnLockStatus: isLockingEnabled ? true : false,
|
||||||
schemaPath,
|
schemaPath,
|
||||||
signal: abortController.signal,
|
signal: controller.signal,
|
||||||
updateLastEdited,
|
updateLastEdited,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -265,14 +264,6 @@ const PreviewView: React.FC<Props> = ({
|
|||||||
// Clean up when the component unmounts or when the document is unlocked
|
// Clean up when the component unmounts or when the document is unlocked
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
if (abortControllerRef.current) {
|
|
||||||
try {
|
|
||||||
abortControllerRef.current.abort()
|
|
||||||
} catch (_err) {
|
|
||||||
// swallow error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isLockingEnabled) {
|
if (!isLockingEnabled) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -316,6 +307,12 @@ const PreviewView: React.FC<Props> = ({
|
|||||||
setDocumentIsLocked,
|
setDocumentIsLocked,
|
||||||
])
|
])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
abortAndIgnore(formStateAbortControllerRef.current)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const shouldShowDocumentLockedModal =
|
const shouldShowDocumentLockedModal =
|
||||||
documentIsLocked &&
|
documentIsLocked &&
|
||||||
currentEditor &&
|
currentEditor &&
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
useServerFunctions,
|
useServerFunctions,
|
||||||
useTranslation,
|
useTranslation,
|
||||||
} from '@payloadcms/ui'
|
} from '@payloadcms/ui'
|
||||||
|
import { abortAndIgnore } from '@payloadcms/ui/shared'
|
||||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
|
|
||||||
const baseClass = 'lexical-block'
|
const baseClass = 'lexical-block'
|
||||||
@@ -37,7 +38,7 @@ export const BlockComponent: React.FC<Props> = (props) => {
|
|||||||
const {
|
const {
|
||||||
fieldProps: { featureClientSchemaMap, field: parentLexicalRichTextField, path, schemaPath },
|
fieldProps: { featureClientSchemaMap, field: parentLexicalRichTextField, path, schemaPath },
|
||||||
} = useEditorConfigContext()
|
} = useEditorConfigContext()
|
||||||
const abortControllerRef = useRef(new AbortController())
|
const onChangeAbortControllerRef = useRef(new AbortController())
|
||||||
const { docPermissions, getDocPreferences } = useDocumentInfo()
|
const { docPermissions, getDocPreferences } = useDocumentInfo()
|
||||||
|
|
||||||
const { getFormState } = useServerFunctions()
|
const { getFormState } = useServerFunctions()
|
||||||
@@ -69,7 +70,6 @@ export const BlockComponent: React.FC<Props> = (props) => {
|
|||||||
data: formData,
|
data: formData,
|
||||||
docPermissions,
|
docPermissions,
|
||||||
docPreferences: await getDocPreferences(),
|
docPreferences: await getDocPreferences(),
|
||||||
doNotAbort: true,
|
|
||||||
globalSlug,
|
globalSlug,
|
||||||
operation: 'update',
|
operation: 'update',
|
||||||
renderAllFields: true,
|
renderAllFields: true,
|
||||||
@@ -94,11 +94,7 @@ export const BlockComponent: React.FC<Props> = (props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
try {
|
abortAndIgnore(abortController)
|
||||||
abortController.abort()
|
|
||||||
} catch (_err) {
|
|
||||||
// swallow error
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
getFormState,
|
getFormState,
|
||||||
@@ -108,32 +104,26 @@ export const BlockComponent: React.FC<Props> = (props) => {
|
|||||||
globalSlug,
|
globalSlug,
|
||||||
getDocPreferences,
|
getDocPreferences,
|
||||||
docPermissions,
|
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(
|
const onChange = useCallback(
|
||||||
async ({ formState: prevFormState }) => {
|
async ({ formState: prevFormState }) => {
|
||||||
if (abortControllerRef.current) {
|
abortAndIgnore(onChangeAbortControllerRef.current)
|
||||||
try {
|
|
||||||
abortControllerRef.current.abort()
|
|
||||||
} catch (_err) {
|
|
||||||
// swallow error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const abortController = new AbortController()
|
const controller = new AbortController()
|
||||||
abortControllerRef.current = abortController
|
onChangeAbortControllerRef.current = controller
|
||||||
|
|
||||||
const { state: newFormState } = await getFormState({
|
const { state: newFormState } = await getFormState({
|
||||||
id,
|
id,
|
||||||
collectionSlug,
|
collectionSlug,
|
||||||
docPermissions,
|
docPermissions,
|
||||||
docPreferences: await getDocPreferences(),
|
docPreferences: await getDocPreferences(),
|
||||||
doNotAbort: true,
|
|
||||||
formState: prevFormState,
|
formState: prevFormState,
|
||||||
globalSlug,
|
globalSlug,
|
||||||
operation: 'update',
|
operation: 'update',
|
||||||
schemaPath: schemaFieldsPath,
|
schemaPath: schemaFieldsPath,
|
||||||
signal: abortController.signal,
|
signal: controller.signal,
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!newFormState) {
|
if (!newFormState) {
|
||||||
@@ -162,16 +152,9 @@ export const BlockComponent: React.FC<Props> = (props) => {
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
// cleanup effect
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const abortController = abortControllerRef.current
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
try {
|
abortAndIgnore(onChangeAbortControllerRef.current)
|
||||||
abortController.abort()
|
|
||||||
} catch (_err) {
|
|
||||||
// swallow error
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
@@ -243,7 +226,8 @@ export const BlockComponent: React.FC<Props> = (props) => {
|
|||||||
schemaFieldsPath,
|
schemaFieldsPath,
|
||||||
classNames,
|
classNames,
|
||||||
i18n,
|
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>
|
return <div className={baseClass + ' ' + baseClass + '-' + formData.blockType}>{formContent}</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
useServerFunctions,
|
useServerFunctions,
|
||||||
useTranslation,
|
useTranslation,
|
||||||
} from '@payloadcms/ui'
|
} from '@payloadcms/ui'
|
||||||
|
import { abortAndIgnore } from '@payloadcms/ui/shared'
|
||||||
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
||||||
import { v4 as uuid } from 'uuid'
|
import { v4 as uuid } from 'uuid'
|
||||||
|
|
||||||
@@ -28,7 +29,7 @@ export const DrawerContent: React.FC<Omit<FieldsDrawerProps, 'drawerSlug' | 'dra
|
|||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { id, collectionSlug, docPermissions, getDocPreferences, globalSlug } = useDocumentInfo()
|
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)
|
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
|
const fields: any = fieldMapOverride ?? featureClientSchemaMap[featureKey]?.[schemaFieldsPath] // Field Schema
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const abortController = new AbortController()
|
const controller = new AbortController()
|
||||||
|
|
||||||
const awaitInitialState = async () => {
|
const awaitInitialState = async () => {
|
||||||
const { state } = await getFormState({
|
const { state } = await getFormState({
|
||||||
@@ -54,12 +55,11 @@ export const DrawerContent: React.FC<Omit<FieldsDrawerProps, 'drawerSlug' | 'dra
|
|||||||
data: data ?? {},
|
data: data ?? {},
|
||||||
docPermissions,
|
docPermissions,
|
||||||
docPreferences: await getDocPreferences(),
|
docPreferences: await getDocPreferences(),
|
||||||
doNotAbort: true,
|
|
||||||
globalSlug,
|
globalSlug,
|
||||||
operation: 'update',
|
operation: 'update',
|
||||||
renderAllFields: true,
|
renderAllFields: true,
|
||||||
schemaPath: schemaFieldsPath,
|
schemaPath: schemaFieldsPath,
|
||||||
signal: abortController.signal,
|
signal: controller.signal,
|
||||||
})
|
})
|
||||||
|
|
||||||
setInitialState(state)
|
setInitialState(state)
|
||||||
@@ -68,11 +68,7 @@ export const DrawerContent: React.FC<Omit<FieldsDrawerProps, 'drawerSlug' | 'dra
|
|||||||
void awaitInitialState()
|
void awaitInitialState()
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
try {
|
abortAndIgnore(controller)
|
||||||
abortController.abort()
|
|
||||||
} catch (_err) {
|
|
||||||
// swallow error
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
schemaFieldsPath,
|
schemaFieldsPath,
|
||||||
@@ -87,16 +83,10 @@ export const DrawerContent: React.FC<Omit<FieldsDrawerProps, 'drawerSlug' | 'dra
|
|||||||
|
|
||||||
const onChange = useCallback(
|
const onChange = useCallback(
|
||||||
async ({ formState: prevFormState }) => {
|
async ({ formState: prevFormState }) => {
|
||||||
if (abortControllerRef.current) {
|
abortAndIgnore(onChangeAbortControllerRef.current)
|
||||||
try {
|
|
||||||
abortControllerRef.current.abort()
|
|
||||||
} catch (_err) {
|
|
||||||
// swallow error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const abortController = new AbortController()
|
const controller = new AbortController()
|
||||||
abortControllerRef.current = abortController
|
onChangeAbortControllerRef.current = controller
|
||||||
|
|
||||||
const { state } = await getFormState({
|
const { state } = await getFormState({
|
||||||
id,
|
id,
|
||||||
@@ -107,7 +97,7 @@ export const DrawerContent: React.FC<Omit<FieldsDrawerProps, 'drawerSlug' | 'dra
|
|||||||
globalSlug,
|
globalSlug,
|
||||||
operation: 'update',
|
operation: 'update',
|
||||||
schemaPath: schemaFieldsPath,
|
schemaPath: schemaFieldsPath,
|
||||||
signal: abortController.signal,
|
signal: controller.signal,
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!state) {
|
if (!state) {
|
||||||
@@ -129,14 +119,8 @@ export const DrawerContent: React.FC<Omit<FieldsDrawerProps, 'drawerSlug' | 'dra
|
|||||||
|
|
||||||
// cleanup effect
|
// cleanup effect
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const abortController = abortControllerRef.current
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
try {
|
abortAndIgnore(onChangeAbortControllerRef.current)
|
||||||
abortController.abort()
|
|
||||||
} catch (_err) {
|
|
||||||
// swallow error
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
|||||||
@@ -100,7 +100,6 @@ export const LinkButton: React.FC<{
|
|||||||
data,
|
data,
|
||||||
docPermissions,
|
docPermissions,
|
||||||
docPreferences: await getDocPreferences(),
|
docPreferences: await getDocPreferences(),
|
||||||
doNotAbort: true,
|
|
||||||
globalSlug,
|
globalSlug,
|
||||||
operation: 'update',
|
operation: 'update',
|
||||||
renderAllFields: true,
|
renderAllFields: true,
|
||||||
|
|||||||
@@ -103,7 +103,6 @@ export const LinkElement = () => {
|
|||||||
data,
|
data,
|
||||||
docPermissions,
|
docPermissions,
|
||||||
docPreferences: await getDocPreferences(),
|
docPreferences: await getDocPreferences(),
|
||||||
doNotAbort: true,
|
|
||||||
globalSlug,
|
globalSlug,
|
||||||
operation: 'update',
|
operation: 'update',
|
||||||
renderAllFields: true,
|
renderAllFields: true,
|
||||||
|
|||||||
@@ -77,7 +77,6 @@ export const UploadDrawer: React.FC<{
|
|||||||
data,
|
data,
|
||||||
docPermissions,
|
docPermissions,
|
||||||
docPreferences: await getDocPreferences(),
|
docPreferences: await getDocPreferences(),
|
||||||
doNotAbort: true,
|
|
||||||
globalSlug,
|
globalSlug,
|
||||||
operation: 'update',
|
operation: 'update',
|
||||||
renderAllFields: true,
|
renderAllFields: true,
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import type { ClientCollectionConfig } from 'payload'
|
import type { ClientCollectionConfig } from 'payload'
|
||||||
|
|
||||||
import { useRouter, useSearchParams } from 'next/navigation.js'
|
import { useRouter, useSearchParams } from 'next/navigation.js'
|
||||||
import React, { useCallback } from 'react'
|
import React, { useCallback, useEffect } from 'react'
|
||||||
|
|
||||||
import type { EditFormProps } from './types.js'
|
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 { OperationProvider } from '../../../providers/Operation/index.js'
|
||||||
import { useServerFunctions } from '../../../providers/ServerFunctions/index.js'
|
import { useServerFunctions } from '../../../providers/ServerFunctions/index.js'
|
||||||
import { useUploadEdits } from '../../../providers/UploadEdits/index.js'
|
import { useUploadEdits } from '../../../providers/UploadEdits/index.js'
|
||||||
|
import { abortAndIgnore } from '../../../utilities/abortAndIgnore.js'
|
||||||
import { formatAdminURL } from '../../../utilities/formatAdminURL.js'
|
import { formatAdminURL } from '../../../utilities/formatAdminURL.js'
|
||||||
import { useDocumentDrawerContext } from '../../DocumentDrawer/Provider.js'
|
import { useDocumentDrawerContext } from '../../DocumentDrawer/Provider.js'
|
||||||
import { DocumentFields } from '../../DocumentFields/index.js'
|
import { DocumentFields } from '../../DocumentFields/index.js'
|
||||||
@@ -55,8 +56,9 @@ export function EditForm({ submitted }: EditFormProps) {
|
|||||||
getEntityConfig,
|
getEntityConfig,
|
||||||
} = useConfig()
|
} = 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 router = useRouter()
|
||||||
const depth = useEditDepth()
|
const depth = useEditDepth()
|
||||||
const params = useSearchParams()
|
const params = useSearchParams()
|
||||||
@@ -109,6 +111,11 @@ export function EditForm({ submitted }: EditFormProps) {
|
|||||||
|
|
||||||
const onChange: NonNullable<FormProps['onChange']>[0] = useCallback(
|
const onChange: NonNullable<FormProps['onChange']>[0] = useCallback(
|
||||||
async ({ formState: prevFormState }) => {
|
async ({ formState: prevFormState }) => {
|
||||||
|
abortAndIgnore(formStateAbortControllerRef.current)
|
||||||
|
|
||||||
|
const controller = new AbortController()
|
||||||
|
formStateAbortControllerRef.current = controller
|
||||||
|
|
||||||
const docPreferences = await getDocPreferences()
|
const docPreferences = await getDocPreferences()
|
||||||
const { state: newFormState } = await getFormState({
|
const { state: newFormState } = await getFormState({
|
||||||
collectionSlug,
|
collectionSlug,
|
||||||
@@ -117,6 +124,7 @@ export function EditForm({ submitted }: EditFormProps) {
|
|||||||
formState: prevFormState,
|
formState: prevFormState,
|
||||||
operation: 'create',
|
operation: 'create',
|
||||||
schemaPath,
|
schemaPath,
|
||||||
|
signal: controller.signal,
|
||||||
})
|
})
|
||||||
|
|
||||||
return newFormState
|
return newFormState
|
||||||
@@ -124,6 +132,12 @@ export function EditForm({ submitted }: EditFormProps) {
|
|||||||
[collectionSlug, schemaPath, getDocPreferences, getFormState, docPermissions],
|
[collectionSlug, schemaPath, getDocPreferences, getFormState, docPermissions],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
abortAndIgnore(formStateAbortControllerRef.current)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<OperationProvider operation="create">
|
<OperationProvider operation="create">
|
||||||
<BulkUploadProvider>
|
<BulkUploadProvider>
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { LoadingOverlay } from '../../elements/Loading/index.js'
|
|||||||
import { useConfig } from '../../providers/Config/index.js'
|
import { useConfig } from '../../providers/Config/index.js'
|
||||||
import { useServerFunctions } from '../../providers/ServerFunctions/index.js'
|
import { useServerFunctions } from '../../providers/ServerFunctions/index.js'
|
||||||
import { useTranslation } from '../../providers/Translation/index.js'
|
import { useTranslation } from '../../providers/Translation/index.js'
|
||||||
|
import { abortAndIgnore } from '../../utilities/abortAndIgnore.js'
|
||||||
import { DocumentDrawerContextProvider } from './Provider.js'
|
import { DocumentDrawerContextProvider } from './Provider.js'
|
||||||
|
|
||||||
export const DocumentDrawerContent: React.FC<DocumentDrawerProps> = ({
|
export const DocumentDrawerContent: React.FC<DocumentDrawerProps> = ({
|
||||||
@@ -35,6 +36,8 @@ export const DocumentDrawerContent: React.FC<DocumentDrawerProps> = ({
|
|||||||
collections.find((collection) => collection.slug === collectionSlug),
|
collections.find((collection) => collection.slug === collectionSlug),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const documentViewAbortControllerRef = React.useRef<AbortController>(null)
|
||||||
|
|
||||||
const { closeModal } = useModal()
|
const { closeModal } = useModal()
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
@@ -44,31 +47,40 @@ export const DocumentDrawerContent: React.FC<DocumentDrawerProps> = ({
|
|||||||
const [isLoading, setIsLoading] = useState(true)
|
const [isLoading, setIsLoading] = useState(true)
|
||||||
|
|
||||||
const getDocumentView = useCallback(
|
const getDocumentView = useCallback(
|
||||||
async (docID?: number | string, doNotAbort?: boolean) => {
|
(docID?: number | string) => {
|
||||||
setIsLoading(true)
|
abortAndIgnore(documentViewAbortControllerRef.current)
|
||||||
|
|
||||||
try {
|
const controller = new AbortController()
|
||||||
const result = await renderDocument({
|
documentViewAbortControllerRef.current = controller
|
||||||
collectionSlug,
|
|
||||||
disableActions,
|
|
||||||
docID,
|
|
||||||
doNotAbort,
|
|
||||||
drawerSlug,
|
|
||||||
initialData,
|
|
||||||
redirectAfterDelete: redirectAfterDelete !== undefined ? redirectAfterDelete : false,
|
|
||||||
redirectAfterDuplicate:
|
|
||||||
redirectAfterDuplicate !== undefined ? redirectAfterDuplicate : false,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (result?.Document) {
|
const fetchDocumentView = async () => {
|
||||||
setDocumentView(result.Document)
|
setIsLoading(true)
|
||||||
setIsLoading(false)
|
|
||||||
|
try {
|
||||||
|
const result = await renderDocument({
|
||||||
|
collectionSlug,
|
||||||
|
disableActions,
|
||||||
|
docID,
|
||||||
|
drawerSlug,
|
||||||
|
initialData,
|
||||||
|
redirectAfterDelete: redirectAfterDelete !== undefined ? redirectAfterDelete : false,
|
||||||
|
redirectAfterDuplicate:
|
||||||
|
redirectAfterDuplicate !== undefined ? redirectAfterDuplicate : false,
|
||||||
|
signal: controller.signal,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (result?.Document) {
|
||||||
|
setDocumentView(result.Document)
|
||||||
|
setIsLoading(false)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
toast.error(error?.message || t('error:unspecific'))
|
||||||
|
closeModal(drawerSlug)
|
||||||
|
// toast.error(data?.errors?.[0].message || t('error:unspecific'))
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
toast.error(error?.message || t('error:unspecific'))
|
|
||||||
closeModal(drawerSlug)
|
|
||||||
// toast.error(data?.errors?.[0].message || t('error:unspecific'))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void fetchDocumentView()
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
collectionSlug,
|
collectionSlug,
|
||||||
@@ -79,20 +91,13 @@ export const DocumentDrawerContent: React.FC<DocumentDrawerProps> = ({
|
|||||||
redirectAfterDuplicate,
|
redirectAfterDuplicate,
|
||||||
renderDocument,
|
renderDocument,
|
||||||
closeModal,
|
closeModal,
|
||||||
initialState,
|
|
||||||
t,
|
t,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!DocumentView) {
|
|
||||||
void getDocumentView(existingDocID, true)
|
|
||||||
}
|
|
||||||
}, [DocumentView, getDocumentView, existingDocID])
|
|
||||||
|
|
||||||
const onSave = useCallback<DocumentDrawerProps['onSave']>(
|
const onSave = useCallback<DocumentDrawerProps['onSave']>(
|
||||||
(args) => {
|
(args) => {
|
||||||
void getDocumentView(args.doc.id)
|
getDocumentView(args.doc.id)
|
||||||
|
|
||||||
if (typeof onSaveFromProps === 'function') {
|
if (typeof onSaveFromProps === 'function') {
|
||||||
void onSaveFromProps({
|
void onSaveFromProps({
|
||||||
@@ -104,9 +109,9 @@ export const DocumentDrawerContent: React.FC<DocumentDrawerProps> = ({
|
|||||||
[onSaveFromProps, collectionConfig, getDocumentView],
|
[onSaveFromProps, collectionConfig, getDocumentView],
|
||||||
)
|
)
|
||||||
|
|
||||||
const onDuplicate = useCallback<DocumentDrawerProps['onSave']>(
|
const onDuplicate = useCallback<DocumentDrawerProps['onDuplicate']>(
|
||||||
(args) => {
|
(args) => {
|
||||||
void getDocumentView(args.doc.id)
|
getDocumentView(args.doc.id)
|
||||||
|
|
||||||
if (typeof onDuplicateFromProps === 'function') {
|
if (typeof onDuplicateFromProps === 'function') {
|
||||||
void onDuplicateFromProps({
|
void onDuplicateFromProps({
|
||||||
@@ -133,9 +138,22 @@ export const DocumentDrawerContent: React.FC<DocumentDrawerProps> = ({
|
|||||||
)
|
)
|
||||||
|
|
||||||
const clearDoc = useCallback(() => {
|
const clearDoc = useCallback(() => {
|
||||||
void getDocumentView()
|
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) {
|
if (isLoading) {
|
||||||
return <LoadingOverlay />
|
return <LoadingOverlay />
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import type { ClientCollectionConfig, FormState } from 'payload'
|
|||||||
import { useModal } from '@faceless-ui/modal'
|
import { useModal } from '@faceless-ui/modal'
|
||||||
import { getTranslation } from '@payloadcms/translations'
|
import { getTranslation } from '@payloadcms/translations'
|
||||||
import { useRouter } from 'next/navigation.js'
|
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'
|
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 { SelectAllStatus, useSelection } from '../../providers/Selection/index.js'
|
||||||
import { useServerFunctions } from '../../providers/ServerFunctions/index.js'
|
import { useServerFunctions } from '../../providers/ServerFunctions/index.js'
|
||||||
import { useTranslation } from '../../providers/Translation/index.js'
|
import { useTranslation } from '../../providers/Translation/index.js'
|
||||||
|
import { abortAndIgnore } from '../../utilities/abortAndIgnore.js'
|
||||||
import { Drawer, DrawerToggler } from '../Drawer/index.js'
|
import { Drawer, DrawerToggler } from '../Drawer/index.js'
|
||||||
import { FieldSelect } from '../FieldSelect/index.js'
|
import { FieldSelect } from '../FieldSelect/index.js'
|
||||||
import './index.scss'
|
import './index.scss'
|
||||||
@@ -127,6 +128,7 @@ export const EditMany: React.FC<EditManyProps> = (props) => {
|
|||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const [initialState, setInitialState] = useState<FormState>()
|
const [initialState, setInitialState] = useState<FormState>()
|
||||||
const hasInitializedState = React.useRef(false)
|
const hasInitializedState = React.useRef(false)
|
||||||
|
const formStateAbortControllerRef = React.useRef<AbortController>(null)
|
||||||
const { clearRouteCache } = useRouteCache()
|
const { clearRouteCache } = useRouteCache()
|
||||||
|
|
||||||
const collectionPermissions = permissions?.collections?.[slug]
|
const collectionPermissions = permissions?.collections?.[slug]
|
||||||
@@ -135,6 +137,8 @@ export const EditMany: React.FC<EditManyProps> = (props) => {
|
|||||||
const drawerSlug = `edit-${slug}`
|
const drawerSlug = `edit-${slug}`
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
|
const controller = new AbortController()
|
||||||
|
|
||||||
if (!hasInitializedState.current) {
|
if (!hasInitializedState.current) {
|
||||||
const getInitialState = async () => {
|
const getInitialState = async () => {
|
||||||
const { state: result } = await getFormState({
|
const { state: result } = await getFormState({
|
||||||
@@ -144,6 +148,7 @@ export const EditMany: React.FC<EditManyProps> = (props) => {
|
|||||||
docPreferences: null,
|
docPreferences: null,
|
||||||
operation: 'update',
|
operation: 'update',
|
||||||
schemaPath: slug,
|
schemaPath: slug,
|
||||||
|
signal: controller.signal,
|
||||||
})
|
})
|
||||||
|
|
||||||
setInitialState(result)
|
setInitialState(result)
|
||||||
@@ -151,11 +156,20 @@ export const EditMany: React.FC<EditManyProps> = (props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void getInitialState()
|
void getInitialState()
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
abortAndIgnore(controller)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [apiRoute, hasInitializedState, serverURL, slug, getFormState, user, collectionPermissions])
|
}, [apiRoute, hasInitializedState, serverURL, slug, getFormState, user, collectionPermissions])
|
||||||
|
|
||||||
const onChange: FormProps['onChange'][0] = useCallback(
|
const onChange: FormProps['onChange'][0] = useCallback(
|
||||||
async ({ formState: prevFormState }) => {
|
async ({ formState: prevFormState }) => {
|
||||||
|
abortAndIgnore(formStateAbortControllerRef.current)
|
||||||
|
|
||||||
|
const controller = new AbortController()
|
||||||
|
formStateAbortControllerRef.current = controller
|
||||||
|
|
||||||
const { state } = await getFormState({
|
const { state } = await getFormState({
|
||||||
collectionSlug: slug,
|
collectionSlug: slug,
|
||||||
docPermissions: collectionPermissions,
|
docPermissions: collectionPermissions,
|
||||||
@@ -163,6 +177,7 @@ export const EditMany: React.FC<EditManyProps> = (props) => {
|
|||||||
formState: prevFormState,
|
formState: prevFormState,
|
||||||
operation: 'update',
|
operation: 'update',
|
||||||
schemaPath: slug,
|
schemaPath: slug,
|
||||||
|
signal: controller.signal,
|
||||||
})
|
})
|
||||||
|
|
||||||
return state
|
return state
|
||||||
@@ -170,6 +185,10 @@ export const EditMany: React.FC<EditManyProps> = (props) => {
|
|||||||
[slug, getFormState, collectionPermissions],
|
[slug, getFormState, collectionPermissions],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
abortAndIgnore(formStateAbortControllerRef.current)
|
||||||
|
}, [])
|
||||||
|
|
||||||
if (selectAll === SelectAllStatus.None || !hasUpdatePermission) {
|
if (selectAll === SelectAllStatus.None || !hasUpdatePermission) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ export { WithServerSideProps } from '../../elements/WithServerSideProps/index.js
|
|||||||
export { reduceToSerializableFields } from '../../forms/Form/reduceToSerializableFields.js'
|
export { reduceToSerializableFields } from '../../forms/Form/reduceToSerializableFields.js'
|
||||||
export { PayloadIcon } from '../../graphics/Icon/index.js'
|
export { PayloadIcon } from '../../graphics/Icon/index.js'
|
||||||
export { PayloadLogo } from '../../graphics/Logo/index.js'
|
export { PayloadLogo } from '../../graphics/Logo/index.js'
|
||||||
|
export { abortAndIgnore } from '../../utilities/abortAndIgnore.js'
|
||||||
export { requests } from '../../utilities/api.js'
|
export { requests } from '../../utilities/api.js'
|
||||||
export { findLocaleFromCode } from '../../utilities/findLocaleFromCode.js'
|
export { findLocaleFromCode } from '../../utilities/findLocaleFromCode.js'
|
||||||
export { formatAdminURL } from '../../utilities/formatAdminURL.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 { useOperation } from '../../providers/Operation/index.js'
|
||||||
import { useServerFunctions } from '../../providers/ServerFunctions/index.js'
|
import { useServerFunctions } from '../../providers/ServerFunctions/index.js'
|
||||||
import { useTranslation } from '../../providers/Translation/index.js'
|
import { useTranslation } from '../../providers/Translation/index.js'
|
||||||
|
import { abortAndIgnore } from '../../utilities/abortAndIgnore.js'
|
||||||
import { requests } from '../../utilities/api.js'
|
import { requests } from '../../utilities/api.js'
|
||||||
import {
|
import {
|
||||||
FormContext,
|
FormContext,
|
||||||
@@ -91,8 +92,7 @@ export const Form: React.FC<FormProps> = (props) => {
|
|||||||
const [submitted, setSubmitted] = useState(false)
|
const [submitted, setSubmitted] = useState(false)
|
||||||
const formRef = useRef<HTMLFormElement>(null)
|
const formRef = useRef<HTMLFormElement>(null)
|
||||||
const contextRef = useRef({} as FormContextType)
|
const contextRef = useRef({} as FormContextType)
|
||||||
const abortControllerRef = useRef(new AbortController())
|
const resetFormStateAbortControllerRef = useRef<AbortController>(null)
|
||||||
const abortControllerRef2 = useRef(new AbortController())
|
|
||||||
|
|
||||||
const fieldsReducer = useReducer(fieldReducer, {}, () => initialState)
|
const fieldsReducer = useReducer(fieldReducer, {}, () => initialState)
|
||||||
|
|
||||||
@@ -456,16 +456,10 @@ export const Form: React.FC<FormProps> = (props) => {
|
|||||||
|
|
||||||
const reset = useCallback(
|
const reset = useCallback(
|
||||||
async (data: unknown) => {
|
async (data: unknown) => {
|
||||||
if (abortControllerRef.current) {
|
abortAndIgnore(resetFormStateAbortControllerRef.current)
|
||||||
try {
|
|
||||||
abortControllerRef.current.abort()
|
|
||||||
} catch (error) {
|
|
||||||
// swallow error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const abortController = new AbortController()
|
const controller = new AbortController()
|
||||||
abortControllerRef.current = abortController
|
resetFormStateAbortControllerRef.current = controller
|
||||||
|
|
||||||
const docPreferences = await getDocPreferences()
|
const docPreferences = await getDocPreferences()
|
||||||
|
|
||||||
@@ -479,7 +473,7 @@ export const Form: React.FC<FormProps> = (props) => {
|
|||||||
operation,
|
operation,
|
||||||
renderAllFields: true,
|
renderAllFields: true,
|
||||||
schemaPath: collectionSlug ? collectionSlug : globalSlug,
|
schemaPath: collectionSlug ? collectionSlug : globalSlug,
|
||||||
signal: abortController.signal,
|
signal: controller.signal,
|
||||||
})
|
})
|
||||||
|
|
||||||
contextRef.current = { ...initContextState } as FormContextType
|
contextRef.current = { ...initContextState } as FormContextType
|
||||||
@@ -548,27 +542,9 @@ export const Form: React.FC<FormProps> = (props) => {
|
|||||||
[dispatchFields, getDataByPath],
|
[dispatchFields, getDataByPath],
|
||||||
)
|
)
|
||||||
|
|
||||||
// clean on unmount
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const re1 = abortControllerRef.current
|
|
||||||
const re2 = abortControllerRef2.current
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
if (re1) {
|
abortAndIgnore(resetFormStateAbortControllerRef.current)
|
||||||
try {
|
|
||||||
re1.abort()
|
|
||||||
} catch (_err) {
|
|
||||||
// swallow error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (re2) {
|
|
||||||
try {
|
|
||||||
re2.abort()
|
|
||||||
} catch (_err) {
|
|
||||||
// swallow error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
|||||||
@@ -6,14 +6,13 @@ import type {
|
|||||||
ServerFunctionClient,
|
ServerFunctionClient,
|
||||||
} from 'payload'
|
} 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 { buildFormStateHandler } from '../../utilities/buildFormState.js'
|
||||||
import type { buildTableStateHandler } from '../../utilities/buildTableState.js'
|
import type { buildTableStateHandler } from '../../utilities/buildTableState.js'
|
||||||
|
|
||||||
type GetFormStateClient = (
|
type GetFormStateClient = (
|
||||||
args: {
|
args: {
|
||||||
doNotAbort?: boolean
|
|
||||||
signal?: AbortSignal
|
signal?: AbortSignal
|
||||||
} & Omit<BuildFormStateArgs, 'clientConfig' | 'req'>,
|
} & Omit<BuildFormStateArgs, 'clientConfig' | 'req'>,
|
||||||
) => ReturnType<typeof buildFormStateHandler>
|
) => ReturnType<typeof buildFormStateHandler>
|
||||||
@@ -28,7 +27,6 @@ type RenderDocument = (args: {
|
|||||||
collectionSlug: string
|
collectionSlug: string
|
||||||
disableActions?: boolean
|
disableActions?: boolean
|
||||||
docID?: number | string
|
docID?: number | string
|
||||||
doNotAbort?: boolean
|
|
||||||
drawerSlug?: string
|
drawerSlug?: string
|
||||||
initialData?: Data
|
initialData?: Data
|
||||||
redirectAfterDelete?: boolean
|
redirectAfterDelete?: boolean
|
||||||
@@ -69,10 +67,6 @@ export const ServerFunctionsProvider: React.FC<{
|
|||||||
throw new Error('ServerFunctionsProvider requires a serverFunction prop')
|
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>(
|
const getDocumentSlots = useCallback<GetDocumentSlots>(
|
||||||
async (args) =>
|
async (args) =>
|
||||||
await serverFunction({
|
await serverFunction({
|
||||||
@@ -84,39 +78,16 @@ export const ServerFunctionsProvider: React.FC<{
|
|||||||
|
|
||||||
const getFormState = useCallback<GetFormStateClient>(
|
const getFormState = useCallback<GetFormStateClient>(
|
||||||
async (args) => {
|
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 || {}
|
const { signal: remoteSignal, ...rest } = args || {}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!remoteSignal?.aborted && !localSignal?.aborted) {
|
if (!remoteSignal?.aborted) {
|
||||||
const result = (await serverFunction({
|
const result = (await serverFunction({
|
||||||
name: 'form-state',
|
name: 'form-state',
|
||||||
args: rest,
|
args: rest,
|
||||||
})) as ReturnType<typeof buildFormStateHandler> // TODO: infer this type when `strictNullChecks` is enabled
|
})) as ReturnType<typeof buildFormStateHandler> // TODO: infer this type when `strictNullChecks` is enabled
|
||||||
|
|
||||||
if (!remoteSignal?.aborted && !localSignal?.aborted) {
|
if (!remoteSignal?.aborted) {
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -131,15 +102,19 @@ export const ServerFunctionsProvider: React.FC<{
|
|||||||
|
|
||||||
const getTableState = useCallback<GetTableStateClient>(
|
const getTableState = useCallback<GetTableStateClient>(
|
||||||
async (args) => {
|
async (args) => {
|
||||||
const { ...rest } = args || {}
|
const { signal: remoteSignal, ...rest } = args || {}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = (await serverFunction({
|
if (!remoteSignal?.aborted) {
|
||||||
name: 'table-state',
|
const result = (await serverFunction({
|
||||||
args: rest,
|
name: 'table-state',
|
||||||
})) as ReturnType<typeof buildTableStateHandler> // TODO: infer this type when `strictNullChecks` is enabled
|
args: rest,
|
||||||
|
})) as ReturnType<typeof buildTableStateHandler> // TODO: infer this type when `strictNullChecks` is enabled
|
||||||
|
|
||||||
return result
|
if (!remoteSignal?.aborted) {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (_err) {
|
} catch (_err) {
|
||||||
console.error(_err) // eslint-disable-line no-console
|
console.error(_err) // eslint-disable-line no-console
|
||||||
}
|
}
|
||||||
@@ -151,37 +126,16 @@ export const ServerFunctionsProvider: React.FC<{
|
|||||||
|
|
||||||
const renderDocument = useCallback<RenderDocument>(
|
const renderDocument = useCallback<RenderDocument>(
|
||||||
async (args) => {
|
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 || {}
|
const { signal: remoteSignal, ...rest } = args || {}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!remoteSignal?.aborted && !localSignal?.aborted) {
|
if (!remoteSignal?.aborted) {
|
||||||
const result = (await serverFunction({
|
const result = (await serverFunction({
|
||||||
name: 'render-document',
|
name: 'render-document',
|
||||||
args: rest,
|
args: rest,
|
||||||
})) as { docID: string; Document: React.ReactNode }
|
})) as { docID: string; Document: React.ReactNode }
|
||||||
|
|
||||||
if (!remoteSignal?.aborted && !localSignal?.aborted) {
|
if (!remoteSignal?.aborted) {
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -192,20 +146,6 @@ export const ServerFunctionsProvider: React.FC<{
|
|||||||
[serverFunction],
|
[serverFunction],
|
||||||
)
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const controller = abortControllerRef.current
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
if (controller) {
|
|
||||||
try {
|
|
||||||
// controller.abort()
|
|
||||||
} catch (_err) {
|
|
||||||
// swallow error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ServerFunctionsContext.Provider
|
<ServerFunctionsContext.Provider
|
||||||
value={{
|
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 { OperationProvider } from '../../providers/Operation/index.js'
|
||||||
import { useServerFunctions } from '../../providers/ServerFunctions/index.js'
|
import { useServerFunctions } from '../../providers/ServerFunctions/index.js'
|
||||||
import { useUploadEdits } from '../../providers/UploadEdits/index.js'
|
import { useUploadEdits } from '../../providers/UploadEdits/index.js'
|
||||||
|
import { abortAndIgnore } from '../../utilities/abortAndIgnore.js'
|
||||||
import { formatAdminURL } from '../../utilities/formatAdminURL.js'
|
import { formatAdminURL } from '../../utilities/formatAdminURL.js'
|
||||||
import { handleBackToDashboard } from '../../utilities/handleBackToDashboard.js'
|
import { handleBackToDashboard } from '../../utilities/handleBackToDashboard.js'
|
||||||
import { handleGoBack } from '../../utilities/handleGoBack.js'
|
import { handleGoBack } from '../../utilities/handleGoBack.js'
|
||||||
@@ -115,7 +116,7 @@ export const DefaultEditView: React.FC<ClientSideEditViewProps> = ({
|
|||||||
const { resetUploadEdits } = useUploadEdits()
|
const { resetUploadEdits } = useUploadEdits()
|
||||||
const { getFormState } = useServerFunctions()
|
const { getFormState } = useServerFunctions()
|
||||||
|
|
||||||
const abortControllerRef = useRef(new AbortController())
|
const onChangeAbortControllerRef = useRef<AbortController>(null)
|
||||||
|
|
||||||
const locale = params.get('locale')
|
const locale = params.get('locale')
|
||||||
|
|
||||||
@@ -261,16 +262,10 @@ export const DefaultEditView: React.FC<ClientSideEditViewProps> = ({
|
|||||||
|
|
||||||
const onChange: FormProps['onChange'][0] = useCallback(
|
const onChange: FormProps['onChange'][0] = useCallback(
|
||||||
async ({ formState: prevFormState }) => {
|
async ({ formState: prevFormState }) => {
|
||||||
if (abortControllerRef.current) {
|
abortAndIgnore(onChangeAbortControllerRef.current)
|
||||||
try {
|
|
||||||
abortControllerRef.current.abort()
|
|
||||||
} catch (e) {
|
|
||||||
// swallow error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const abortController = new AbortController()
|
const controller = new AbortController()
|
||||||
abortControllerRef.current = abortController
|
onChangeAbortControllerRef.current = controller
|
||||||
|
|
||||||
const currentTime = Date.now()
|
const currentTime = Date.now()
|
||||||
const timeSinceLastUpdate = currentTime - editSessionStartTime
|
const timeSinceLastUpdate = currentTime - editSessionStartTime
|
||||||
@@ -296,7 +291,7 @@ export const DefaultEditView: React.FC<ClientSideEditViewProps> = ({
|
|||||||
renderAllFields: false,
|
renderAllFields: false,
|
||||||
returnLockStatus: isLockingEnabled ? true : false,
|
returnLockStatus: isLockingEnabled ? true : false,
|
||||||
schemaPath: schemaPathSegments.join('.'),
|
schemaPath: schemaPathSegments.join('.'),
|
||||||
// signal: abortController.signal,
|
signal: controller.signal,
|
||||||
updateLastEdited,
|
updateLastEdited,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -352,14 +347,6 @@ export const DefaultEditView: React.FC<ClientSideEditViewProps> = ({
|
|||||||
// Clean up when the component unmounts or when the document is unlocked
|
// Clean up when the component unmounts or when the document is unlocked
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
if (abortControllerRef.current) {
|
|
||||||
try {
|
|
||||||
abortControllerRef.current.abort()
|
|
||||||
} catch (e) {
|
|
||||||
// swallow error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isLockingEnabled) {
|
if (!isLockingEnabled) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -403,6 +390,12 @@ export const DefaultEditView: React.FC<ClientSideEditViewProps> = ({
|
|||||||
setDocumentIsLocked,
|
setDocumentIsLocked,
|
||||||
])
|
])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
abortAndIgnore(onChangeAbortControllerRef.current)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
const shouldShowDocumentLockedModal =
|
const shouldShowDocumentLockedModal =
|
||||||
documentIsLocked &&
|
documentIsLocked &&
|
||||||
currentEditor &&
|
currentEditor &&
|
||||||
|
|||||||
@@ -481,10 +481,8 @@ describe('access control', () => {
|
|||||||
|
|
||||||
await expect(page.locator('.unauthorized')).toBeVisible()
|
await expect(page.locator('.unauthorized')).toBeVisible()
|
||||||
|
|
||||||
await page.goto(logoutURL)
|
|
||||||
await page.waitForURL(logoutURL)
|
|
||||||
|
|
||||||
// Log back in for the next test
|
// Log back in for the next test
|
||||||
|
await page.goto(logoutURL)
|
||||||
await login({
|
await login({
|
||||||
data: {
|
data: {
|
||||||
email: devUser.email,
|
email: devUser.email,
|
||||||
@@ -528,6 +526,19 @@ describe('access control', () => {
|
|||||||
await page.waitForURL(unauthorizedURL)
|
await page.waitForURL(unauthorizedURL)
|
||||||
|
|
||||||
await expect(page.locator('.unauthorized')).toBeVisible()
|
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