fix(ui): processing and initializing form does not disable standalone fields (#11714)

The form component's `initializing` and `processing` states do not
disable fields that are rendered outside of `DocumentFields`. Fields
currently rely on the `readOnly` prop provided by `DocumentFields` and
do not subscribe to these states for themselves. This means that fields
that are rendered outright, such as within the bulk edit drawer, they do
not receive a `readOnly` prop and are therefore never disabled.

The fix is add a `disabled` property to the `useField` hook. This
subscribes to the `initializing` and `processing` states in the same way
as `DocumentFields`, however, now each field can determine its own
disabled state instead of relying solely on the `readOnly` prop. Adding
this new prop has no overhead as `processing` and `initializing` is
already being subscribed to within `useField`.
This commit is contained in:
Jacob Fletcher
2025-03-17 10:27:21 -04:00
committed by GitHub
parent 3d129e822d
commit 0b1a1b585b
27 changed files with 134 additions and 76 deletions

View File

@@ -68,8 +68,7 @@ const RichTextComponent: React.FC<
const {
customComponents: { AfterInput, BeforeInput, Description, Error, Label } = {},
formInitializing,
formProcessing,
disabled: disabledFromField,
initialValue,
setValue,
showError,
@@ -79,7 +78,7 @@ const RichTextComponent: React.FC<
validate: memoizedValidate,
})
const disabled = readOnlyFromProps || formProcessing || formInitializing
const disabled = readOnlyFromProps || disabledFromField
const [isSmallWidthViewport, setIsSmallWidthViewport] = useState<boolean>(false)
const [rerenderProviderKey, setRerenderProviderKey] = useState<Date>()

View File

@@ -95,7 +95,7 @@ const RichTextField: React.FC<LoadedSlateFieldProps> = (props) => {
const {
customComponents: { Description, Error, Label } = {},
formInitializing,
disabled: disabledFromField,
initialValue,
setValue,
showError,
@@ -105,7 +105,7 @@ const RichTextField: React.FC<LoadedSlateFieldProps> = (props) => {
validate: memoizedValidate,
})
const disabled = readOnlyFromProps || formInitializing
const disabled = readOnlyFromProps || disabledFromField
const editor = useMemo(() => {
let CreatedEditor = withEnterBreakOut(withHistory(withReact(createEditor())))

View File

@@ -29,7 +29,7 @@ export const DocumentFields: React.FC<Args> = ({
docPermissions,
fields,
forceSidebarWrap,
readOnly: readOnlyProp,
readOnly,
schemaPathSegments,
}) => {
const { hasSidebarFields, mainFields, sidebarFields } = useMemo(() => {
@@ -53,11 +53,6 @@ export const DocumentFields: React.FC<Args> = ({
)
}, [fields])
const formInitializing = useFormInitializing()
const formProcessing = useFormProcessing()
const readOnly = readOnlyProp || formInitializing || formProcessing
return (
<div
className={[

View File

@@ -111,6 +111,7 @@ export const ArrayFieldComponent: ArrayFieldClientComponent = (props) => {
const {
customComponents: { AfterInput, BeforeInput, Description, Error, Label, RowLabels } = {},
disabled,
errorPaths,
rows: rowsData = [],
showError,
@@ -197,7 +198,7 @@ export const ArrayFieldComponent: ArrayFieldClientComponent = (props) => {
const fieldErrorCount = errorPaths.length
const fieldHasErrors = submitted && errorPaths.length > 0
const showRequired = readOnly && rowsData.length === 0
const showRequired = (readOnly || disabled) && rowsData.length === 0
const showMinRows = rowsData.length < minRows || (required && rowsData.length === 0)
return (
@@ -285,7 +286,11 @@ export const ArrayFieldComponent: ArrayFieldClientComponent = (props) => {
).length
return (
<DraggableSortableItem disabled={readOnly || !isSortable} id={rowID} key={rowID}>
<DraggableSortableItem
disabled={readOnly || disabled || !isSortable}
id={rowID}
key={rowID}
>
{(draggableSortableItemProps) => (
<ArrayRow
{...draggableSortableItemProps}
@@ -303,7 +308,7 @@ export const ArrayFieldComponent: ArrayFieldClientComponent = (props) => {
parentPath={path}
path={rowPath}
permissions={permissions}
readOnly={readOnly}
readOnly={readOnly || disabled}
removeRow={removeRow}
row={rowData}
rowCount={rowsData?.length}

View File

@@ -54,6 +54,7 @@ const BlocksFieldComponent: BlocksFieldClientComponent = (props) => {
schemaPath: schemaPathFromProps,
validate,
} = props
const schemaPath = schemaPathFromProps ?? name
const minRows = (minRowsProp ?? required) ? 1 : 0
@@ -98,6 +99,7 @@ const BlocksFieldComponent: BlocksFieldClientComponent = (props) => {
const {
customComponents: { AfterInput, BeforeInput, Description, Error, Label, RowLabels } = {},
disabled,
errorPaths,
rows = [],
showError,
@@ -276,7 +278,11 @@ const BlocksFieldComponent: BlocksFieldClientComponent = (props) => {
).length
return (
<DraggableSortableItem disabled={readOnly || !isSortable} id={row.id} key={row.id}>
<DraggableSortableItem
disabled={readOnly || disabled || !isSortable}
id={row.id}
key={row.id}
>
{(draggableSortableItemProps) => (
<BlockRow
{...draggableSortableItemProps}
@@ -295,7 +301,7 @@ const BlocksFieldComponent: BlocksFieldClientComponent = (props) => {
parentPath={path}
path={rowPath}
permissions={permissions}
readOnly={readOnly}
readOnly={readOnly || disabled}
removeRow={removeRow}
row={row}
rowCount={rows.length}
@@ -335,12 +341,12 @@ const BlocksFieldComponent: BlocksFieldClientComponent = (props) => {
<Fragment>
<DrawerToggler
className={`${baseClass}__drawer-toggler`}
disabled={readOnly}
disabled={readOnly || disabled}
slug={drawerSlug}
>
<Button
buttonStyle="icon-label"
disabled={readOnly}
disabled={readOnly || disabled}
el="span"
icon="plus"
iconPosition="left"

View File

@@ -18,9 +18,9 @@ import { withCondition } from '../../forms/withCondition/index.js'
import { useEditDepth } from '../../providers/EditDepth/index.js'
import { generateFieldID } from '../../utilities/generateFieldID.js'
import { mergeFieldStyles } from '../mergeFieldStyles.js'
import './index.scss'
import { fieldBaseClass } from '../shared/index.js'
import { CheckboxInput } from './Input.js'
import './index.scss'
const baseClass = 'checkbox'
@@ -59,6 +59,7 @@ const CheckboxFieldComponent: CheckboxFieldClientComponent = (props) => {
const {
customComponents: { AfterInput, BeforeInput, Description, Error, Label } = {},
disabled,
setValue,
showError,
value,
@@ -91,7 +92,7 @@ const CheckboxFieldComponent: CheckboxFieldClientComponent = (props) => {
showError && 'error',
className,
value && `${baseClass}--checked`,
readOnly && `${baseClass}--read-only`,
(readOnly || disabled) && `${baseClass}--read-only`,
]
.filter(Boolean)
.join(' ')}
@@ -112,7 +113,7 @@ const CheckboxFieldComponent: CheckboxFieldClientComponent = (props) => {
name={path}
onToggle={onToggle}
partialChecked={partialChecked}
readOnly={readOnly}
readOnly={readOnly || disabled}
required={required}
/>
<RenderCustomComponent

View File

@@ -47,6 +47,7 @@ const CodeFieldComponent: CodeFieldClientComponent = (props) => {
const {
customComponents: { AfterInput, BeforeInput, Description, Error, Label } = {},
disabled,
setValue,
showError,
value,
@@ -64,7 +65,7 @@ const CodeFieldComponent: CodeFieldClientComponent = (props) => {
baseClass,
className,
showError && 'error',
readOnly && 'read-only',
(readOnly || disabled) && 'read-only',
]
.filter(Boolean)
.join(' ')}
@@ -84,10 +85,10 @@ const CodeFieldComponent: CodeFieldClientComponent = (props) => {
{BeforeInput}
<CodeEditor
defaultLanguage={prismToMonacoLanguageMap[language] || language}
onChange={readOnly ? () => null : (val) => setValue(val)}
onChange={readOnly || disabled ? () => null : (val) => setValue(val)}
onMount={onMount}
options={editorOptions}
readOnly={readOnly}
readOnly={readOnly || disabled}
value={(value as string) || ''}
/>
{AfterInput}

View File

@@ -42,7 +42,8 @@ const CollapsibleFieldComponent: CollapsibleFieldClientComponent = (props) => {
const [errorCount, setErrorCount] = useState(0)
const fieldHasErrors = errorCount > 0
const { customComponents: { AfterInput, BeforeInput, Description, Label } = {} } = useField({
const { customComponents: { AfterInput, BeforeInput, Description, Label } = {}, disabled } =
useField({
path,
})
@@ -145,7 +146,7 @@ const CollapsibleFieldComponent: CollapsibleFieldClientComponent = (props) => {
parentPath={parentPath}
parentSchemaPath={parentSchemaPath}
permissions={permissions}
readOnly={readOnly}
readOnly={readOnly || disabled}
/>
</CollapsibleElement>
{AfterInput}

View File

@@ -16,10 +16,10 @@ export type ConfirmPasswordFieldProps = {
}
export const ConfirmPasswordField: React.FC<ConfirmPasswordFieldProps> = (props) => {
const { disabled, path = 'confirm-password' } = props
const { disabled: disabledFromProps, path = 'confirm-password' } = props
const { t } = useTranslation()
const { setValue, showError, value } = useField({
const { disabled, setValue, showError, value } = useField({
path,
validate: (value, options) => {
return confirmPassword(value, {
@@ -47,7 +47,7 @@ export const ConfirmPasswordField: React.FC<ConfirmPasswordFieldProps> = (props)
<input
aria-label={t('authentication:confirmPassword')}
autoComplete="off"
disabled={!!disabled}
disabled={!!(disabled || disabledFromProps)}
id="field-confirm-password"
name="confirm-password"
onChange={setValue}

View File

@@ -58,6 +58,7 @@ const DateTimeFieldComponent: DateFieldClientComponent = (props) => {
const {
customComponents: { AfterInput, BeforeInput, Description, Error, Label } = {},
disabled,
setValue,
showError,
value,
@@ -102,7 +103,7 @@ const DateTimeFieldComponent: DateFieldClientComponent = (props) => {
const onChange = useCallback(
(incomingDate: Date) => {
if (!readOnly) {
if (!(readOnly || disabled)) {
if (timezone && selectedTimezone && incomingDate) {
// Create TZDate instances for the selected timezone
const TZDateWithSelectedTz = TZDate.tz(selectedTimezone)
@@ -132,7 +133,7 @@ const DateTimeFieldComponent: DateFieldClientComponent = (props) => {
}
}
},
[readOnly, setValue, timezone, selectedTimezone],
[readOnly, disabled, timezone, selectedTimezone, isDateOnly, setValue],
)
const onChangeTimezone = useCallback(
@@ -157,7 +158,7 @@ const DateTimeFieldComponent: DateFieldClientComponent = (props) => {
baseClass,
className,
showError && `${baseClass}--has-error`,
readOnly && 'read-only',
(readOnly || disabled) && 'read-only',
]
.filter(Boolean)
.join(' ')}
@@ -180,7 +181,7 @@ const DateTimeFieldComponent: DateFieldClientComponent = (props) => {
...datePickerProps?.overrides,
}}
placeholder={getTranslation(placeholder, i18n)}
readOnly={readOnly}
readOnly={readOnly || disabled}
value={displayedValue}
/>
{timezone && supportedTimezones.length > 0 && (

View File

@@ -51,6 +51,7 @@ const EmailFieldComponent: EmailFieldClientComponent = (props) => {
const {
customComponents: { AfterInput, BeforeInput, Description, Error, Label } = {},
disabled,
setValue,
showError,
value,
@@ -63,7 +64,13 @@ const EmailFieldComponent: EmailFieldClientComponent = (props) => {
return (
<div
className={[fieldBaseClass, 'email', className, showError && 'error', readOnly && 'read-only']
className={[
fieldBaseClass,
'email',
className,
showError && 'error',
(readOnly || disabled) && 'read-only',
]
.filter(Boolean)
.join(' ')}
style={styles}
@@ -84,7 +91,7 @@ const EmailFieldComponent: EmailFieldClientComponent = (props) => {
{/* eslint-disable-next-line jsx-a11y/control-has-associated-label */}
<input
autoComplete={autoComplete}
disabled={readOnly}
disabled={readOnly || disabled}
id={`field-${path.replace(/\./g, '__')}`}
name={path}
onChange={setValue}

View File

@@ -40,8 +40,10 @@ export const GroupFieldComponent: GroupFieldClientComponent = (props) => {
const isWithinGroup = useGroup()
const isWithinRow = useRow()
const isWithinTab = useTabs()
const { customComponents: { AfterInput, BeforeInput, Description, Label } = {}, errorPaths } =
useField({ path })
const submitted = useFormSubmitted()
const errorCount = errorPaths.length
const fieldHasErrors = submitted && errorCount > 0

View File

@@ -31,6 +31,7 @@ const JSONFieldComponent: JSONFieldClientComponent = (props) => {
readOnly,
validate,
} = props
const [jsonError, setJsonError] = useState<string>()
const inputChangeFromRef = React.useRef<'system' | 'user'>('system')
const [editorKey, setEditorKey] = useState<string>('')
@@ -46,6 +47,7 @@ const JSONFieldComponent: JSONFieldClientComponent = (props) => {
const {
customComponents: { AfterInput, BeforeInput, Description, Error, Label } = {},
disabled,
initialValue,
setValue,
showError,
@@ -90,7 +92,7 @@ const JSONFieldComponent: JSONFieldClientComponent = (props) => {
const handleChange = useCallback(
(val) => {
if (readOnly) {
if (readOnly || disabled) {
return
}
inputChangeFromRef.current = 'user'
@@ -103,7 +105,7 @@ const JSONFieldComponent: JSONFieldClientComponent = (props) => {
setJsonError(e)
}
},
[readOnly, setValue],
[readOnly, disabled, setValue],
)
useEffect(() => {
@@ -128,7 +130,7 @@ const JSONFieldComponent: JSONFieldClientComponent = (props) => {
baseClass,
className,
showError && 'error',
readOnly && 'read-only',
(readOnly || disabled) && 'read-only',
]
.filter(Boolean)
.join(' ')}
@@ -153,7 +155,7 @@ const JSONFieldComponent: JSONFieldClientComponent = (props) => {
onChange={handleChange}
onMount={handleMount}
options={editorOptions}
readOnly={readOnly}
readOnly={readOnly || disabled}
value={initialStringValue}
wrapperProps={{
id: `field-${path?.replace(/\./g, '__')}`,

View File

@@ -56,6 +56,7 @@ const NumberFieldComponent: NumberFieldClientComponent = (props) => {
const {
customComponents: { AfterInput, BeforeInput, Description, Error, Label } = {},
disabled,
setValue,
showError,
value,
@@ -88,7 +89,7 @@ const NumberFieldComponent: NumberFieldClientComponent = (props) => {
const handleHasManyChange = useCallback(
(selectedOption) => {
if (!readOnly) {
if (!(readOnly || disabled)) {
let newValue
if (!selectedOption) {
newValue = []
@@ -101,7 +102,7 @@ const NumberFieldComponent: NumberFieldClientComponent = (props) => {
setValue(newValue)
}
},
[readOnly, setValue],
[readOnly, disabled, setValue],
)
// useEffect update valueToRender:
@@ -131,7 +132,7 @@ const NumberFieldComponent: NumberFieldClientComponent = (props) => {
'number',
className,
showError && 'error',
readOnly && 'read-only',
(readOnly || disabled) && 'read-only',
hasMany && 'has-many',
]
.filter(Boolean)
@@ -153,7 +154,7 @@ const NumberFieldComponent: NumberFieldClientComponent = (props) => {
{hasMany ? (
<ReactSelect
className={`field-${path.replace(/\./g, '__')}`}
disabled={readOnly}
disabled={readOnly || disabled}
filterOption={(_, rawInput) => {
const isOverHasMany = Array.isArray(value) && value.length >= maxRows
return isNumber(rawInput) && !isOverHasMany
@@ -179,7 +180,7 @@ const NumberFieldComponent: NumberFieldClientComponent = (props) => {
) : (
<div>
<input
disabled={readOnly}
disabled={readOnly || disabled}
id={`field-${path.replace(/\./g, '__')}`}
max={max}
min={min}

View File

@@ -71,8 +71,7 @@ const PasswordFieldComponent: React.FC<PasswordFieldProps> = (props) => {
const {
customComponents: { AfterInput, BeforeInput, Description, Error, Label } = {},
formInitializing,
formProcessing,
disabled,
setValue,
showError,
value,
@@ -81,8 +80,6 @@ const PasswordFieldComponent: React.FC<PasswordFieldProps> = (props) => {
validate: memoizedValidate,
})
const disabled = disabledFromProps || formInitializing || formProcessing
const renderRTL = isFieldRTL({
fieldLocalized: false,
fieldRTL: rtl,
@@ -109,7 +106,7 @@ const PasswordFieldComponent: React.FC<PasswordFieldProps> = (props) => {
}}
path={path}
placeholder={placeholder}
readOnly={disabled}
readOnly={disabled || disabledFromProps}
required={required}
rtl={renderRTL}
showError={showError}

View File

@@ -44,6 +44,7 @@ export const PointFieldComponent: PointFieldClientComponent = (props) => {
const {
customComponents: { AfterInput, BeforeInput, Description, Error, Label } = {},
disabled,
setValue,
showError,
value = [null, null],
@@ -81,7 +82,7 @@ export const PointFieldComponent: PointFieldClientComponent = (props) => {
baseClass,
className,
showError && 'error',
readOnly && 'read-only',
(readOnly || disabled) && 'read-only',
]
.filter(Boolean)
.join(' ')}
@@ -105,7 +106,7 @@ export const PointFieldComponent: PointFieldClientComponent = (props) => {
{/* disable eslint rule because the label is dynamic */}
{/* eslint-disable-next-line jsx-a11y/control-has-associated-label */}
<input
disabled={readOnly}
disabled={readOnly || disabled}
id={`field-longitude-${path?.replace(/\./g, '__')}`}
name={`${path}.longitude`}
onChange={(e) => handleChange(e, 0)}
@@ -138,7 +139,7 @@ export const PointFieldComponent: PointFieldClientComponent = (props) => {
{/* disable eslint rule because the label is dynamic */}
{/* eslint-disable-next-line jsx-a11y/control-has-associated-label */}
<input
disabled={readOnly}
disabled={readOnly || disabled}
id={`field-latitude-${path?.replace(/\./g, '__')}`}
name={`${path}.latitude`}
onChange={(e) => handleChange(e, 1)}

View File

@@ -53,6 +53,7 @@ const RadioGroupFieldComponent: RadioFieldClientComponent = (props) => {
const {
customComponents: { AfterInput, BeforeInput, Description, Error, Label } = {},
disabled,
setValue,
showError,
value: valueFromContext,
@@ -73,7 +74,7 @@ const RadioGroupFieldComponent: RadioFieldClientComponent = (props) => {
className,
`${baseClass}--layout-${layout}`,
showError && 'error',
readOnly && `${baseClass}--read-only`,
(readOnly || disabled) && `${baseClass}--read-only`,
]
.filter(Boolean)
.join(' ')}
@@ -115,13 +116,13 @@ const RadioGroupFieldComponent: RadioFieldClientComponent = (props) => {
onChangeFromProps(optionValue)
}
if (!readOnly) {
if (!(readOnly || disabled)) {
setValue(optionValue, !!disableModifyingFormFromProps)
}
}}
option={optionIsObject(option) ? option : { label: option, value: option }}
path={path}
readOnly={readOnly}
readOnly={readOnly || disabled}
uuid={uuid}
/>
</li>

View File

@@ -101,6 +101,7 @@ const RelationshipFieldComponent: RelationshipFieldClientComponent = (props) =>
const {
customComponents: { AfterInput, BeforeInput, Description, Error, Label } = {},
disabled,
filterOptions,
initialValue,
setValue,
@@ -577,8 +578,8 @@ const RelationshipFieldComponent: RelationshipFieldClientComponent = (props) =>
className,
showError && 'error',
errorLoading && 'error-loading',
readOnly && `${baseClass}--read-only`,
!readOnly && allowCreate && `${baseClass}--allow-create`,
(readOnly || disabled) && `${baseClass}--read-only`,
!(readOnly || disabled) && allowCreate && `${baseClass}--allow-create`,
]
.filter(Boolean)
.join(' ')}
@@ -611,7 +612,7 @@ const RelationshipFieldComponent: RelationshipFieldClientComponent = (props) =>
onDocumentDrawerOpen,
onSave,
}}
disabled={readOnly || isDrawerOpen}
disabled={readOnly || disabled || isDrawerOpen}
filterOption={enableWordBoundarySearch ? filterOption : undefined}
getOptionValue={(option) => {
if (!option) {
@@ -625,7 +626,7 @@ const RelationshipFieldComponent: RelationshipFieldClientComponent = (props) =>
isMulti={hasMany}
isSortable={isSortable}
onChange={
!readOnly
!(readOnly || disabled)
? (selected) => {
if (selected === null) {
setValue(hasMany ? [] : null)
@@ -689,7 +690,7 @@ const RelationshipFieldComponent: RelationshipFieldClientComponent = (props) =>
showError={showError}
value={valueToRender ?? null}
/>
{!readOnly && allowCreate && (
{!(readOnly || disabled) && allowCreate && (
<AddNewRelation
hasMany={hasMany}
path={path}

View File

@@ -64,6 +64,7 @@ const SelectFieldComponent: SelectFieldClientComponent = (props) => {
const {
customComponents: { AfterInput, BeforeInput, Description, Error, Label } = {},
disabled,
setValue,
showError,
value,
@@ -74,7 +75,7 @@ const SelectFieldComponent: SelectFieldClientComponent = (props) => {
const onChange: ReactSelectAdapterProps['onChange'] = useCallback(
(selectedOption: OptionObject | OptionObject[]) => {
if (!readOnly) {
if (!readOnly || disabled) {
let newValue: string | string[] = null
if (selectedOption && hasMany) {
if (Array.isArray(selectedOption)) {
@@ -93,7 +94,7 @@ const SelectFieldComponent: SelectFieldClientComponent = (props) => {
setValue(newValue)
}
},
[readOnly, hasMany, setValue, onChangeFromProps],
[readOnly, disabled, hasMany, setValue, onChangeFromProps],
)
const styles = useMemo(() => mergeFieldStyles(field), [field])
@@ -116,7 +117,7 @@ const SelectFieldComponent: SelectFieldClientComponent = (props) => {
onChange={onChange}
options={options}
path={path}
readOnly={readOnly}
readOnly={readOnly || disabled}
required={required}
showError={showError}
style={styles}

View File

@@ -259,6 +259,7 @@ type ActiveTabProps = {
readonly permissions: SanitizedFieldPermissions
readonly readOnly: boolean
}
function TabContent({
description,
fields,
@@ -273,6 +274,7 @@ function TabContent({
readOnly,
}: ActiveTabProps) {
const { i18n } = useTranslation()
const { customComponents: { AfterInput, BeforeInput, Description, Field } = {} } = useField({
path,
})

View File

@@ -54,6 +54,7 @@ const TextFieldComponent: TextFieldClientComponent = (props) => {
const {
customComponents: { AfterInput, BeforeInput, Description, Error, Label } = {},
disabled,
setValue,
showError,
value,
@@ -75,7 +76,7 @@ const TextFieldComponent: TextFieldClientComponent = (props) => {
const handleHasManyChange = useCallback(
(selectedOption) => {
if (!readOnly) {
if (!(readOnly || disabled)) {
let newValue
if (!selectedOption) {
newValue = []
@@ -88,7 +89,7 @@ const TextFieldComponent: TextFieldClientComponent = (props) => {
setValue(newValue)
}
},
[readOnly, setValue],
[readOnly, setValue, disabled],
)
// useEffect update valueToRender:
@@ -136,7 +137,7 @@ const TextFieldComponent: TextFieldClientComponent = (props) => {
}
path={path}
placeholder={placeholder}
readOnly={readOnly}
readOnly={readOnly || disabled}
required={required}
rtl={renderRTL}
showError={showError}

View File

@@ -60,6 +60,7 @@ const TextareaFieldComponent: TextareaFieldClientComponent = (props) => {
const {
customComponents: { AfterInput, BeforeInput, Description, Error, Label } = {},
disabled,
setValue,
showError,
value,
@@ -86,7 +87,7 @@ const TextareaFieldComponent: TextareaFieldClientComponent = (props) => {
}}
path={path}
placeholder={getTranslation(placeholder, i18n)}
readOnly={readOnly}
readOnly={readOnly || disabled}
required={required}
rows={rows}
rtl={isRTL}

View File

@@ -48,6 +48,7 @@ export function UploadComponent(props: UploadFieldClientProps) {
const {
customComponents: { AfterInput, BeforeInput, Description, Error, Label } = {},
disabled,
filterOptions,
setValue,
showError,
@@ -79,7 +80,7 @@ export function UploadComponent(props: UploadFieldClientProps) {
maxRows={maxRows}
onChange={setValue}
path={path}
readOnly={readOnly}
readOnly={readOnly || disabled}
relationTo={relationTo}
required={required}
serverURL={config.serverURL}

View File

@@ -105,6 +105,7 @@ export const useField = <TValue,>(options: Options): FieldType<TValue> => {
const result: FieldType<TValue> = useMemo(
() => ({
customComponents: field?.customComponents,
disabled: processing || initializing,
errorMessage: field?.errorMessage,
errorPaths: field?.errorPaths || [],
filterOptions,

View File

@@ -9,6 +9,7 @@ export type Options = {
export type FieldType<T> = {
customComponents?: FieldState['customComponents']
disabled: boolean
errorMessage?: string
errorPaths?: string[]
filterOptions?: FilterOptionsResult

View File

@@ -1,12 +1,14 @@
import type { BrowserContext, Page } from '@playwright/test'
import type { PayloadTestSDK } from 'helpers/sdk/index.js'
import { expect, test } from '@playwright/test'
import { addBlock } from 'helpers/e2e/addBlock.js'
import { assertNetworkRequests } from 'helpers/e2e/assertNetworkRequests.js'
import * as path from 'path'
import React from 'react'
import { fileURLToPath } from 'url'
import type { Config, Post } from './payload-types.js'
import {
ensureCompilationIsDone,
initPageConsoleErrorCatch,
@@ -22,6 +24,8 @@ const dirname = path.dirname(filename)
const title = 'Title'
let context: BrowserContext
let payload: PayloadTestSDK<Config>
let serverURL: string
test.describe('Form State', () => {
let page: Page
@@ -29,8 +33,7 @@ test.describe('Form State', () => {
test.beforeAll(async ({ browser }, testInfo) => {
testInfo.setTimeout(TEST_TIMEOUT_LONG)
const { payload, serverURL } = await initPayloadE2ENoConfig({ dirname })
;({ payload, serverURL } = await initPayloadE2ENoConfig({ dirname }))
postsUrl = new AdminUrlUtil(serverURL, 'posts')
context = await browser.newContext()
@@ -38,15 +41,31 @@ test.describe('Form State', () => {
initPageConsoleErrorCatch(page)
await ensureCompilationIsDone({ page, serverURL })
})
test.beforeEach(async () => {
// await throttleTest({ page, context, delay: 'Fast 3G' })
})
test('collection — should re-enable fields after save', async () => {
test('should disable fields during initialization', async () => {
await page.goto(postsUrl.create, { waitUntil: 'commit' })
await expect(page.locator('#field-title')).toBeDisabled()
})
test('should disable fields while processing', async () => {
const doc = await createPost()
await page.goto(postsUrl.edit(doc.id))
await page.locator('#field-title').fill(title)
await page.click('#action-save', { delay: 100 })
await expect(page.locator('#field-title')).toBeDisabled()
})
test('should re-enable fields after save', async () => {
await page.goto(postsUrl.create)
await page.locator('#field-title').fill(title)
await saveDocAndAssert(page)
await expect(page.locator('#field-title')).toBeEnabled()
})
test('should thread proper event argument to validation functions', async () => {
test('should only validate on submit via the `event` argument', async () => {
await page.goto(postsUrl.create)
await page.locator('#field-title').fill(title)
await page.locator('#field-validateUsingEvent').fill('Not allowed')
@@ -161,3 +180,13 @@ test.describe('Form State', () => {
await cdpSession.detach()
})
})
async function createPost(overrides?: Partial<Post>): Promise<Post> {
return payload.create({
collection: 'posts',
data: {
title: 'Post Title',
...overrides,
},
}) as unknown as Promise<Post>
}

View File

@@ -31,7 +31,7 @@
}
],
"paths": {
"@payload-config": ["./test/_community/config.ts"],
"@payload-config": ["./test/form-state/config.ts"],
"@payloadcms/admin-bar": ["./packages/admin-bar/src"],
"@payloadcms/live-preview": ["./packages/live-preview/src"],
"@payloadcms/live-preview-react": ["./packages/live-preview-react/src/index.ts"],