fix(richtext-lexical): Blocks: make sure fields are wrapped in a uniquely-named group, change block node data format, fix react key error (#3995)
* fix(richtext-lexical): make sure block fields are wrapped in a uniquely-named group * chore: remove redundant hook * chore(richtext-lexical): attempt to fix unnecessary unsaved changes warning regression * cleanup everything * chore: more cleanup * debug * looks like properly cloning the formdata for setting initial state fixes the issue where the old formdata is updated even if node.setFields is not called * chore: fix e2e tests * chore: fix e2e tests (a selector has changed) * chore: fix int tests (due to new blocks data format) * chore: fix incorrect insert block commands in drawer * chore: add new e2e test * chore: fail e2e tests when there are browser console errors * fix(breaking): beforeInput and afterInput: fix missing key errors, consistent typing and cases in name
This commit is contained in:
@@ -432,14 +432,14 @@ All Payload fields support the ability to swap in your own React components. So,
|
||||
| **`Cell`** | Used in the `List` view's table to represent a table-based preview of the data stored in the field. [More](#cell-component) |
|
||||
| **`Field`** | Swap out the field itself within all `Edit` views. [More](#field-component) |
|
||||
|
||||
As an alternative to replacing the entire Field component, you may want to keep the majority of the default Field component and only swap components within. This allows you to replace the **`Label`** or **`Error`** within a field component or add additional components inside the field with **`BeforeInput`** or **`AfterInput`**. **`BeforeInput`** and **`AfterInput`** are allowed in any fields that don't contain other fields, except [UI](/docs/fields/ui) and [Rich Text](/docs/fields/rich-text).
|
||||
As an alternative to replacing the entire Field component, you may want to keep the majority of the default Field component and only swap components within. This allows you to replace the **`Label`** or **`Error`** within a field component or add additional components inside the field with **`beforeInput`** or **`afterInput`**. **`beforeInput`** and **`afterInput`** are allowed in any fields that don't contain other fields, except [UI](/docs/fields/ui) and [Rich Text](/docs/fields/rich-text).
|
||||
|
||||
| Component | Description |
|
||||
| ----------------- | --------------------------------------------------------------------------------------------------------------- |
|
||||
| **`Label`** | Override the default Label in the Field Component. [More](#label-component) |
|
||||
| **`Error`** | Override the default Label in the Field Component. [More](#error-component) |
|
||||
| **`BeforeInput`** | An array of elements that will be added before `input`/`textarea` elements. [More](#afterinput-and-beforeinput) |
|
||||
| **`AfterInput`** | An array of elements that will be added after `input`/`textarea` elements. [More](#afterinput-and-beforeinput) |
|
||||
| **`beforeInput`** | An array of elements that will be added before `input`/`textarea` elements. [More](#afterinput-and-beforeinput) |
|
||||
| **`afterInput`** | An array of elements that will be added after `input`/`textarea` elements. [More](#afterinput-and-beforeinput) |
|
||||
|
||||
## Cell Component
|
||||
|
||||
@@ -564,7 +564,7 @@ const CustomError: React.FC<Props> = (props) => {
|
||||
}
|
||||
```
|
||||
|
||||
## AfterInput and BeforeInput
|
||||
## afterInput and beforeInput
|
||||
|
||||
With these properties you can add multiple components before and after the input element. For example, you can add an absolutely positioned button to clear the current field value.
|
||||
|
||||
@@ -583,9 +583,7 @@ const fieldField: Field = {
|
||||
type: 'text',
|
||||
admin: {
|
||||
components: {
|
||||
AfterInput: [
|
||||
<ClearButton />
|
||||
]
|
||||
afterInput: [ClearButton]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,10 +10,10 @@ import './index.scss'
|
||||
const baseClass = 'checkbox-input'
|
||||
|
||||
type CheckboxInputProps = {
|
||||
AfterInput?: React.ReactElement<any>[]
|
||||
BeforeInput?: React.ReactElement<any>[]
|
||||
Label?: React.ComponentType<LabelProps>
|
||||
afterInput?: React.ComponentType<any>[]
|
||||
'aria-label'?: string
|
||||
beforeInput?: React.ComponentType<any>[]
|
||||
checked?: boolean
|
||||
className?: string
|
||||
id?: string
|
||||
@@ -30,10 +30,10 @@ export const CheckboxInput: React.FC<CheckboxInputProps> = (props) => {
|
||||
const {
|
||||
id,
|
||||
name,
|
||||
AfterInput,
|
||||
BeforeInput,
|
||||
Label,
|
||||
afterInput,
|
||||
'aria-label': ariaLabel,
|
||||
beforeInput,
|
||||
checked,
|
||||
className,
|
||||
inputRef,
|
||||
@@ -58,7 +58,7 @@ export const CheckboxInput: React.FC<CheckboxInputProps> = (props) => {
|
||||
.join(' ')}
|
||||
>
|
||||
<div className={`${baseClass}__input`}>
|
||||
{BeforeInput}
|
||||
{Array.isArray(beforeInput) && beforeInput.map((Component, i) => <Component key={i} />)}
|
||||
<input
|
||||
aria-label={ariaLabel}
|
||||
defaultChecked={Boolean(checked)}
|
||||
@@ -69,7 +69,7 @@ export const CheckboxInput: React.FC<CheckboxInputProps> = (props) => {
|
||||
ref={inputRef}
|
||||
type="checkbox"
|
||||
/>
|
||||
{AfterInput}
|
||||
{Array.isArray(afterInput) && afterInput.map((Component, i) => <Component key={i} />)}
|
||||
<span className={`${baseClass}__icon ${!partialChecked ? 'check' : 'partial'}`}>
|
||||
{!partialChecked && <Check />}
|
||||
{partialChecked && <Line />}
|
||||
|
||||
@@ -20,12 +20,12 @@ const Checkbox: React.FC<Props> = (props) => {
|
||||
name,
|
||||
admin: {
|
||||
className,
|
||||
components: { Error, Label, afterInput, beforeInput } = {},
|
||||
condition,
|
||||
description,
|
||||
readOnly,
|
||||
style,
|
||||
width,
|
||||
components: { Error, Label, BeforeInput, AfterInput } = {},
|
||||
} = {},
|
||||
disableFormData,
|
||||
label,
|
||||
@@ -85,15 +85,15 @@ const Checkbox: React.FC<Props> = (props) => {
|
||||
<ErrorComp alignCaret="left" message={errorMessage} showError={showError} />
|
||||
</div>
|
||||
<CheckboxInput
|
||||
Label={Label}
|
||||
afterInput={afterInput}
|
||||
beforeInput={beforeInput}
|
||||
checked={Boolean(value)}
|
||||
id={fieldID}
|
||||
label={getTranslation(label || name, i18n)}
|
||||
name={path}
|
||||
onToggle={onToggle}
|
||||
readOnly={readOnly}
|
||||
Label={Label}
|
||||
BeforeInput={BeforeInput}
|
||||
AfterInput={AfterInput}
|
||||
required={required}
|
||||
/>
|
||||
<FieldDescription description={description} value={value} />
|
||||
|
||||
@@ -9,8 +9,8 @@ import FieldDescription from '../../FieldDescription'
|
||||
import DefaultLabel from '../../Label'
|
||||
import useField from '../../useField'
|
||||
import withCondition from '../../withCondition'
|
||||
import './index.scss'
|
||||
import { fieldBaseClass } from '../shared'
|
||||
import './index.scss'
|
||||
|
||||
const prismToMonacoLanguageMap = {
|
||||
js: 'javascript',
|
||||
@@ -24,6 +24,7 @@ const Code: React.FC<Props> = (props) => {
|
||||
name,
|
||||
admin: {
|
||||
className,
|
||||
components: { Error, Label } = {},
|
||||
condition,
|
||||
description,
|
||||
editorOptions,
|
||||
@@ -31,7 +32,6 @@ const Code: React.FC<Props> = (props) => {
|
||||
readOnly,
|
||||
style,
|
||||
width,
|
||||
components: { Error, Label } = {},
|
||||
} = {},
|
||||
label,
|
||||
path: pathFromProps,
|
||||
|
||||
@@ -17,10 +17,10 @@ const baseClass = 'date-time-field'
|
||||
export type DateTimeInputProps = Omit<DateField, 'admin' | 'name' | 'type'> & {
|
||||
className?: string
|
||||
components: {
|
||||
AfterInput?: React.ReactElement<any>[]
|
||||
BeforeInput?: React.ReactElement<any>[]
|
||||
Error?: React.ComponentType<any>
|
||||
Label?: React.ComponentType<any>
|
||||
afterInput?: React.ComponentType<any>[]
|
||||
beforeInput?: React.ComponentType<any>[]
|
||||
}
|
||||
datePickerProps?: DateField['admin']['date']
|
||||
description?: Description
|
||||
@@ -39,7 +39,7 @@ export type DateTimeInputProps = Omit<DateField, 'admin' | 'name' | 'type'> & {
|
||||
export const DateTimeInput: React.FC<DateTimeInputProps> = (props) => {
|
||||
const {
|
||||
className,
|
||||
components: { AfterInput, BeforeInput, Error, Label } = {},
|
||||
components: { Error, Label, afterInput, beforeInput } = {},
|
||||
datePickerProps,
|
||||
description,
|
||||
errorMessage,
|
||||
@@ -81,7 +81,7 @@ export const DateTimeInput: React.FC<DateTimeInputProps> = (props) => {
|
||||
</div>
|
||||
<LabelComp htmlFor={path} label={label} required={required} />
|
||||
<div className={`${baseClass}__input-wrapper`} id={`field-${path.replace(/\./g, '__')}`}>
|
||||
{BeforeInput}
|
||||
{Array.isArray(beforeInput) && beforeInput.map((Component, i) => <Component key={i} />)}
|
||||
<DatePicker
|
||||
{...datePickerProps}
|
||||
onChange={onChange}
|
||||
@@ -89,7 +89,7 @@ export const DateTimeInput: React.FC<DateTimeInputProps> = (props) => {
|
||||
readOnly={readOnly}
|
||||
value={value}
|
||||
/>
|
||||
{AfterInput}
|
||||
{Array.isArray(afterInput) && afterInput.map((Component, i) => <Component key={i} />)}
|
||||
</div>
|
||||
<FieldDescription description={description} value={value} />
|
||||
</div>
|
||||
|
||||
@@ -10,8 +10,8 @@ import FieldDescription from '../../FieldDescription'
|
||||
import DefaultLabel from '../../Label'
|
||||
import useField from '../../useField'
|
||||
import withCondition from '../../withCondition'
|
||||
import './index.scss'
|
||||
import { fieldBaseClass } from '../shared'
|
||||
import './index.scss'
|
||||
|
||||
const Email: React.FC<Props> = (props) => {
|
||||
const {
|
||||
@@ -19,13 +19,13 @@ const Email: React.FC<Props> = (props) => {
|
||||
admin: {
|
||||
autoComplete,
|
||||
className,
|
||||
components: { Error, Label, afterInput, beforeInput } = {},
|
||||
condition,
|
||||
description,
|
||||
placeholder,
|
||||
readOnly,
|
||||
style,
|
||||
width,
|
||||
components: { Error, Label, BeforeInput, AfterInput } = {},
|
||||
} = {},
|
||||
label,
|
||||
path: pathFromProps,
|
||||
@@ -68,7 +68,7 @@ const Email: React.FC<Props> = (props) => {
|
||||
<ErrorComp message={errorMessage} showError={showError} />
|
||||
<LabelComp htmlFor={`field-${path.replace(/\./g, '__')}`} label={label} required={required} />
|
||||
<div className="input-wrapper">
|
||||
{BeforeInput}
|
||||
{Array.isArray(beforeInput) && beforeInput.map((Component, i) => <Component key={i} />)}
|
||||
<input
|
||||
autoComplete={autoComplete}
|
||||
disabled={Boolean(readOnly)}
|
||||
@@ -79,7 +79,7 @@ const Email: React.FC<Props> = (props) => {
|
||||
type="email"
|
||||
value={(value as string) || ''}
|
||||
/>
|
||||
{AfterInput}
|
||||
{Array.isArray(afterInput) && afterInput.map((Component, i) => <Component key={i} />)}
|
||||
</div>
|
||||
<FieldDescription description={description} value={value} />
|
||||
</div>
|
||||
|
||||
@@ -13,14 +13,15 @@ import FieldDescription from '../../FieldDescription'
|
||||
import DefaultLabel from '../../Label'
|
||||
import useField from '../../useField'
|
||||
import withCondition from '../../withCondition'
|
||||
import './index.scss'
|
||||
import { fieldBaseClass } from '../shared'
|
||||
import './index.scss'
|
||||
|
||||
const NumberField: React.FC<Props> = (props) => {
|
||||
const {
|
||||
name,
|
||||
admin: {
|
||||
className,
|
||||
components: { Error, Label, afterInput, beforeInput } = {},
|
||||
condition,
|
||||
description,
|
||||
placeholder,
|
||||
@@ -28,7 +29,6 @@ const NumberField: React.FC<Props> = (props) => {
|
||||
step,
|
||||
style,
|
||||
width,
|
||||
components: { Error, Label, BeforeInput, AfterInput } = {},
|
||||
} = {},
|
||||
hasMany,
|
||||
label,
|
||||
@@ -162,7 +162,7 @@ const NumberField: React.FC<Props> = (props) => {
|
||||
/>
|
||||
) : (
|
||||
<div className="input-wrapper">
|
||||
{BeforeInput}
|
||||
{Array.isArray(beforeInput) && beforeInput.map((Component, i) => <Component key={i} />)}
|
||||
<input
|
||||
disabled={readOnly}
|
||||
id={`field-${path.replace(/\./g, '__')}`}
|
||||
@@ -178,7 +178,7 @@ const NumberField: React.FC<Props> = (props) => {
|
||||
type="number"
|
||||
value={typeof value === 'number' ? value : ''}
|
||||
/>
|
||||
{AfterInput}
|
||||
{Array.isArray(afterInput) && afterInput.map((Component, i) => <Component key={i} />)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -10,8 +10,8 @@ import FieldDescription from '../../FieldDescription'
|
||||
import DefaultLabel from '../../Label'
|
||||
import useField from '../../useField'
|
||||
import withCondition from '../../withCondition'
|
||||
import './index.scss'
|
||||
import { fieldBaseClass } from '../shared'
|
||||
import './index.scss'
|
||||
|
||||
const baseClass = 'point'
|
||||
|
||||
@@ -20,6 +20,7 @@ const PointField: React.FC<Props> = (props) => {
|
||||
name,
|
||||
admin: {
|
||||
className,
|
||||
components: { Error, Label, afterInput, beforeInput } = {},
|
||||
condition,
|
||||
description,
|
||||
placeholder,
|
||||
@@ -27,7 +28,6 @@ const PointField: React.FC<Props> = (props) => {
|
||||
step,
|
||||
style,
|
||||
width,
|
||||
components: { Error, Label, BeforeInput, AfterInput } = {},
|
||||
} = {},
|
||||
label,
|
||||
path: pathFromProps,
|
||||
@@ -98,7 +98,7 @@ const PointField: React.FC<Props> = (props) => {
|
||||
required={required}
|
||||
/>
|
||||
<div className="input-wrapper">
|
||||
{BeforeInput}
|
||||
{Array.isArray(beforeInput) && beforeInput.map((Component, i) => <Component key={i} />)}
|
||||
<input
|
||||
disabled={readOnly}
|
||||
id={`field-longitude-${path.replace(/\./g, '__')}`}
|
||||
@@ -109,7 +109,7 @@ const PointField: React.FC<Props> = (props) => {
|
||||
type="number"
|
||||
value={value && typeof value[0] === 'number' ? value[0] : ''}
|
||||
/>
|
||||
{AfterInput}
|
||||
{Array.isArray(afterInput) && afterInput.map((Component, i) => <Component key={i} />)}
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
@@ -119,7 +119,7 @@ const PointField: React.FC<Props> = (props) => {
|
||||
required={required}
|
||||
/>
|
||||
<div className="input-wrapper">
|
||||
{BeforeInput}
|
||||
{Array.isArray(beforeInput) && beforeInput.map((Component, i) => <Component key={i} />)}
|
||||
<input
|
||||
disabled={readOnly}
|
||||
id={`field-latitude-${path.replace(/\./g, '__')}`}
|
||||
@@ -130,7 +130,7 @@ const PointField: React.FC<Props> = (props) => {
|
||||
type="number"
|
||||
value={value && typeof value[1] === 'number' ? value[1] : ''}
|
||||
/>
|
||||
{AfterInput}
|
||||
{Array.isArray(afterInput) && afterInput.map((Component, i) => <Component key={i} />)}
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -14,6 +14,10 @@ import { fieldBaseClass } from '../shared'
|
||||
import './index.scss'
|
||||
|
||||
export type TextInputProps = Omit<TextField, 'type'> & {
|
||||
Error?: React.ComponentType<any>
|
||||
Label?: React.ComponentType<any>
|
||||
afterInput?: React.ComponentType<any>[]
|
||||
beforeInput?: React.ComponentType<any>[]
|
||||
className?: string
|
||||
description?: Description
|
||||
errorMessage?: string
|
||||
@@ -29,14 +33,14 @@ export type TextInputProps = Omit<TextField, 'type'> & {
|
||||
style?: React.CSSProperties
|
||||
value?: string
|
||||
width?: string
|
||||
Error?: React.ComponentType<any>
|
||||
Label?: React.ComponentType<any>
|
||||
BeforeInput?: React.ReactElement<any>[]
|
||||
AfterInput?: React.ReactElement<any>[]
|
||||
}
|
||||
|
||||
const TextInput: React.FC<TextInputProps> = (props) => {
|
||||
const {
|
||||
Error,
|
||||
Label,
|
||||
afterInput,
|
||||
beforeInput,
|
||||
className,
|
||||
description,
|
||||
errorMessage,
|
||||
@@ -53,10 +57,6 @@ const TextInput: React.FC<TextInputProps> = (props) => {
|
||||
style,
|
||||
value,
|
||||
width,
|
||||
Error,
|
||||
Label,
|
||||
BeforeInput,
|
||||
AfterInput,
|
||||
} = props
|
||||
|
||||
const { i18n } = useTranslation()
|
||||
@@ -77,7 +77,7 @@ const TextInput: React.FC<TextInputProps> = (props) => {
|
||||
<ErrorComp message={errorMessage} showError={showError} />
|
||||
<LabelComp htmlFor={`field-${path.replace(/\./g, '__')}`} label={label} required={required} />
|
||||
<div className="input-wrapper">
|
||||
{BeforeInput}
|
||||
{Array.isArray(beforeInput) && beforeInput.map((Component, i) => <Component key={i} />)}
|
||||
<input
|
||||
data-rtl={rtl}
|
||||
disabled={readOnly}
|
||||
@@ -90,7 +90,7 @@ const TextInput: React.FC<TextInputProps> = (props) => {
|
||||
type="text"
|
||||
value={value || ''}
|
||||
/>
|
||||
{AfterInput}
|
||||
{Array.isArray(afterInput) && afterInput.map((Component, i) => <Component key={i} />)}
|
||||
</div>
|
||||
<FieldDescription
|
||||
className={`field-description-${path.replace(/\./g, '__')}`}
|
||||
|
||||
@@ -15,6 +15,7 @@ const Text: React.FC<Props> = (props) => {
|
||||
name,
|
||||
admin: {
|
||||
className,
|
||||
components: { Error, Label, afterInput, beforeInput } = {},
|
||||
condition,
|
||||
description,
|
||||
placeholder,
|
||||
@@ -22,7 +23,6 @@ const Text: React.FC<Props> = (props) => {
|
||||
rtl,
|
||||
style,
|
||||
width,
|
||||
components: { Error, Label, BeforeInput, AfterInput } = {},
|
||||
} = {},
|
||||
inputRef,
|
||||
label,
|
||||
@@ -60,6 +60,10 @@ const Text: React.FC<Props> = (props) => {
|
||||
|
||||
return (
|
||||
<TextInput
|
||||
Error={Error}
|
||||
Label={Label}
|
||||
afterInput={afterInput}
|
||||
beforeInput={beforeInput}
|
||||
className={className}
|
||||
description={description}
|
||||
errorMessage={errorMessage}
|
||||
@@ -78,10 +82,6 @@ const Text: React.FC<Props> = (props) => {
|
||||
style={style}
|
||||
value={value}
|
||||
width={width}
|
||||
Error={Error}
|
||||
Label={Label}
|
||||
BeforeInput={BeforeInput}
|
||||
AfterInput={AfterInput}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -10,10 +10,14 @@ import { getTranslation } from '../../../../../utilities/getTranslation'
|
||||
import DefaultError from '../../Error'
|
||||
import FieldDescription from '../../FieldDescription'
|
||||
import DefaultLabel from '../../Label'
|
||||
import './index.scss'
|
||||
import { fieldBaseClass } from '../shared'
|
||||
import './index.scss'
|
||||
|
||||
export type TextAreaInputProps = Omit<TextareaField, 'type'> & {
|
||||
Error?: React.ComponentType<any>
|
||||
Label?: React.ComponentType<any>
|
||||
afterInput?: React.ComponentType<any>[]
|
||||
beforeInput?: React.ComponentType<any>[]
|
||||
className?: string
|
||||
description?: Description
|
||||
errorMessage?: string
|
||||
@@ -28,14 +32,14 @@ export type TextAreaInputProps = Omit<TextareaField, 'type'> & {
|
||||
style?: React.CSSProperties
|
||||
value?: string
|
||||
width?: string
|
||||
Error?: React.ComponentType<any>
|
||||
Label?: React.ComponentType<any>
|
||||
BeforeInput?: React.ReactElement<any>[]
|
||||
AfterInput?: React.ReactElement<any>[]
|
||||
}
|
||||
|
||||
const TextareaInput: React.FC<TextAreaInputProps> = (props) => {
|
||||
const {
|
||||
Error,
|
||||
Label,
|
||||
afterInput,
|
||||
beforeInput,
|
||||
className,
|
||||
description,
|
||||
errorMessage,
|
||||
@@ -51,10 +55,6 @@ const TextareaInput: React.FC<TextAreaInputProps> = (props) => {
|
||||
style,
|
||||
value,
|
||||
width,
|
||||
Error,
|
||||
Label,
|
||||
BeforeInput,
|
||||
AfterInput,
|
||||
} = props
|
||||
|
||||
const { i18n } = useTranslation()
|
||||
@@ -83,7 +83,7 @@ const TextareaInput: React.FC<TextAreaInputProps> = (props) => {
|
||||
<label className="textarea-outer" htmlFor={`field-${path.replace(/\./g, '__')}`}>
|
||||
<div className="textarea-inner">
|
||||
<div className="textarea-clone" data-value={value || placeholder || ''} />
|
||||
{BeforeInput}
|
||||
{Array.isArray(beforeInput) && beforeInput.map((Component, i) => <Component key={i} />)}
|
||||
<textarea
|
||||
className="textarea-element"
|
||||
data-rtl={rtl}
|
||||
@@ -95,7 +95,7 @@ const TextareaInput: React.FC<TextAreaInputProps> = (props) => {
|
||||
rows={rows}
|
||||
value={value || ''}
|
||||
/>
|
||||
{AfterInput}
|
||||
{Array.isArray(afterInput) && afterInput.map((Component, i) => <Component key={i} />)}
|
||||
</div>
|
||||
</label>
|
||||
<FieldDescription description={description} value={value} />
|
||||
|
||||
@@ -18,6 +18,7 @@ const Textarea: React.FC<Props> = (props) => {
|
||||
name,
|
||||
admin: {
|
||||
className,
|
||||
components: { Error, Label, afterInput, beforeInput } = {},
|
||||
condition,
|
||||
description,
|
||||
placeholder,
|
||||
@@ -26,7 +27,6 @@ const Textarea: React.FC<Props> = (props) => {
|
||||
rtl,
|
||||
style,
|
||||
width,
|
||||
components: { Error, Label, BeforeInput, AfterInput } = {},
|
||||
} = {},
|
||||
label,
|
||||
localized,
|
||||
@@ -65,6 +65,10 @@ const Textarea: React.FC<Props> = (props) => {
|
||||
|
||||
return (
|
||||
<TextareaInput
|
||||
Error={Error}
|
||||
Label={Label}
|
||||
afterInput={afterInput}
|
||||
beforeInput={beforeInput}
|
||||
className={className}
|
||||
description={description}
|
||||
errorMessage={errorMessage}
|
||||
@@ -83,10 +87,6 @@ const Textarea: React.FC<Props> = (props) => {
|
||||
style={style}
|
||||
value={value as string}
|
||||
width={width}
|
||||
Error={Error}
|
||||
Label={Label}
|
||||
BeforeInput={BeforeInput}
|
||||
AfterInput={AfterInput}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -25,13 +25,15 @@ export const ToolbarControls: React.FC<EditViewProps> = () => {
|
||||
{breakpoints?.length > 0 && (
|
||||
<Popup
|
||||
className={`${baseClass}__breakpoint`}
|
||||
button={(
|
||||
button={
|
||||
<>
|
||||
<span>{breakpoints.find(bp => bp.name == breakpoint)?.label ?? customOption.label}</span>
|
||||
<span>
|
||||
{breakpoints.find((bp) => bp.name == breakpoint)?.label ?? customOption.label}
|
||||
</span>
|
||||
|
||||
<Chevron className={`${baseClass}__chevron`} />
|
||||
</>
|
||||
)}
|
||||
}
|
||||
render={({ close }) => (
|
||||
<PopupList.ButtonGroup>
|
||||
<React.Fragment>
|
||||
@@ -49,10 +51,13 @@ export const ToolbarControls: React.FC<EditViewProps> = () => {
|
||||
))}
|
||||
{/* Dynamically add this option so that it only appears when the width and height inputs are explicitly changed */}
|
||||
{breakpoint === 'custom' && (
|
||||
<PopupList.Button active={breakpoint == customOption.value} onClick={() => {
|
||||
<PopupList.Button
|
||||
active={breakpoint == customOption.value}
|
||||
onClick={() => {
|
||||
setBreakpoint(customOption.value)
|
||||
close()
|
||||
}}>
|
||||
}}
|
||||
>
|
||||
{customOption.label}
|
||||
</PopupList.Button>
|
||||
)}
|
||||
@@ -73,20 +78,20 @@ export const ToolbarControls: React.FC<EditViewProps> = () => {
|
||||
</div>
|
||||
<Popup
|
||||
className={`${baseClass}__zoom`}
|
||||
button={(
|
||||
button={
|
||||
<>
|
||||
<span>{zoom * 100}%</span>
|
||||
|
||||
<Chevron className={`${baseClass}__chevron`} />
|
||||
</>
|
||||
)}
|
||||
}
|
||||
render={({ close }) => (
|
||||
<PopupList.ButtonGroup>
|
||||
<React.Fragment>
|
||||
{zoomOptions.map((zoomValue) => (
|
||||
<PopupList.Button
|
||||
key={zoomValue}
|
||||
active={(zoom*100) == zoomValue}
|
||||
active={zoom * 100 == zoomValue}
|
||||
onClick={() => {
|
||||
setZoom(zoomValue / 100)
|
||||
close()
|
||||
|
||||
@@ -71,16 +71,16 @@ export const text = baseField.keys({
|
||||
name: joi.string().required(),
|
||||
admin: baseAdminFields.keys({
|
||||
autoComplete: joi.string(),
|
||||
components: baseAdminComponentFields.keys({
|
||||
Error: componentSchema,
|
||||
Label: componentSchema,
|
||||
afterInput: joi.array().items(componentSchema),
|
||||
beforeInput: joi.array().items(componentSchema),
|
||||
}),
|
||||
placeholder: joi
|
||||
.alternatives()
|
||||
.try(joi.object().pattern(joi.string(), [joi.string()]), joi.string()),
|
||||
rtl: joi.boolean(),
|
||||
components: baseAdminComponentFields.keys({
|
||||
Label: componentSchema,
|
||||
Error: componentSchema,
|
||||
BeforeInput: joi.array().items(componentSchema),
|
||||
AfterInput: joi.array().items(componentSchema),
|
||||
}),
|
||||
}),
|
||||
defaultValue: joi.alternatives().try(joi.string(), joi.func()),
|
||||
maxLength: joi.number(),
|
||||
@@ -92,20 +92,20 @@ export const number = baseField.keys({
|
||||
name: joi.string().required(),
|
||||
admin: baseAdminFields.keys({
|
||||
autoComplete: joi.string(),
|
||||
placeholder: joi.string(),
|
||||
step: joi.number(),
|
||||
components: baseAdminComponentFields.keys({
|
||||
Label: componentSchema,
|
||||
Error: componentSchema,
|
||||
BeforeInput: joi
|
||||
Label: componentSchema,
|
||||
afterInput: joi
|
||||
.array()
|
||||
.items(componentSchema)
|
||||
.when('hasMany', { not: true, otherwise: joi.forbidden() }),
|
||||
AfterInput: joi
|
||||
beforeInput: joi
|
||||
.array()
|
||||
.items(componentSchema)
|
||||
.when('hasMany', { not: true, otherwise: joi.forbidden() }),
|
||||
}),
|
||||
placeholder: joi.string(),
|
||||
step: joi.number(),
|
||||
}),
|
||||
defaultValue: joi.alternatives().try(joi.number(), joi.func()),
|
||||
hasMany: joi.boolean().default(false),
|
||||
@@ -119,15 +119,15 @@ export const number = baseField.keys({
|
||||
export const textarea = baseField.keys({
|
||||
name: joi.string().required(),
|
||||
admin: baseAdminFields.keys({
|
||||
components: baseAdminComponentFields.keys({
|
||||
Error: componentSchema,
|
||||
Label: componentSchema,
|
||||
afterInput: joi.array().items(componentSchema),
|
||||
beforeInput: joi.array().items(componentSchema),
|
||||
}),
|
||||
placeholder: joi.string(),
|
||||
rows: joi.number(),
|
||||
rtl: joi.boolean(),
|
||||
components: baseAdminComponentFields.keys({
|
||||
Label: componentSchema,
|
||||
Error: componentSchema,
|
||||
BeforeInput: joi.array().items(componentSchema),
|
||||
AfterInput: joi.array().items(componentSchema),
|
||||
}),
|
||||
}),
|
||||
defaultValue: joi.alternatives().try(joi.string(), joi.func()),
|
||||
maxLength: joi.number(),
|
||||
@@ -139,13 +139,13 @@ export const email = baseField.keys({
|
||||
name: joi.string().required(),
|
||||
admin: baseAdminFields.keys({
|
||||
autoComplete: joi.string(),
|
||||
placeholder: joi.string(),
|
||||
components: baseAdminComponentFields.keys({
|
||||
Label: componentSchema,
|
||||
Error: componentSchema,
|
||||
BeforeInput: joi.array().items(componentSchema),
|
||||
AfterInput: joi.array().items(componentSchema),
|
||||
Label: componentSchema,
|
||||
afterInput: joi.array().items(componentSchema),
|
||||
beforeInput: joi.array().items(componentSchema),
|
||||
}),
|
||||
placeholder: joi.string(),
|
||||
}),
|
||||
defaultValue: joi.alternatives().try(joi.string(), joi.func()),
|
||||
maxLength: joi.number(),
|
||||
@@ -156,12 +156,12 @@ export const email = baseField.keys({
|
||||
export const code = baseField.keys({
|
||||
name: joi.string().required(),
|
||||
admin: baseAdminFields.keys({
|
||||
components: baseAdminComponentFields.keys({
|
||||
Error: componentSchema,
|
||||
Label: componentSchema,
|
||||
}),
|
||||
editorOptions: joi.object().unknown(), // Editor['options'] @monaco-editor/react
|
||||
language: joi.string(),
|
||||
components: baseAdminComponentFields.keys({
|
||||
Label: componentSchema,
|
||||
Error: componentSchema,
|
||||
}),
|
||||
}),
|
||||
defaultValue: joi.alternatives().try(joi.string(), joi.func()),
|
||||
type: joi.string().valid('code').required(),
|
||||
@@ -171,8 +171,8 @@ export const json = baseField.keys({
|
||||
name: joi.string().required(),
|
||||
admin: baseAdminFields.keys({
|
||||
components: baseAdminComponentFields.keys({
|
||||
Label: componentSchema,
|
||||
Error: componentSchema,
|
||||
Label: componentSchema,
|
||||
}),
|
||||
}),
|
||||
defaultValue: joi.alternatives().try(joi.array(), joi.object()),
|
||||
@@ -182,12 +182,12 @@ export const json = baseField.keys({
|
||||
export const select = baseField.keys({
|
||||
name: joi.string().required(),
|
||||
admin: baseAdminFields.keys({
|
||||
components: baseAdminComponentFields.keys({
|
||||
Error: componentSchema,
|
||||
Label: componentSchema,
|
||||
}),
|
||||
isClearable: joi.boolean().default(false),
|
||||
isSortable: joi.boolean().default(false),
|
||||
components: baseAdminComponentFields.keys({
|
||||
Label: componentSchema,
|
||||
Error: componentSchema,
|
||||
}),
|
||||
}),
|
||||
defaultValue: joi
|
||||
.alternatives()
|
||||
@@ -214,11 +214,11 @@ export const select = baseField.keys({
|
||||
export const radio = baseField.keys({
|
||||
name: joi.string().required(),
|
||||
admin: baseAdminFields.keys({
|
||||
layout: joi.string().valid('vertical', 'horizontal'),
|
||||
components: baseAdminComponentFields.keys({
|
||||
Label: componentSchema,
|
||||
Error: componentSchema,
|
||||
Label: componentSchema,
|
||||
}),
|
||||
layout: joi.string().valid('vertical', 'horizontal'),
|
||||
}),
|
||||
defaultValue: joi.alternatives().try(joi.string().allow(''), joi.func()),
|
||||
options: joi
|
||||
@@ -318,8 +318,8 @@ export const upload = baseField.keys({
|
||||
name: joi.string().required(),
|
||||
admin: baseAdminFields.keys({
|
||||
components: baseAdminComponentFields.keys({
|
||||
Label: componentSchema,
|
||||
Error: componentSchema,
|
||||
Label: componentSchema,
|
||||
}),
|
||||
}),
|
||||
defaultValue: joi.alternatives().try(joi.object(), joi.func()),
|
||||
@@ -333,10 +333,10 @@ export const checkbox = baseField.keys({
|
||||
name: joi.string().required(),
|
||||
admin: baseAdminFields.keys({
|
||||
components: baseAdminComponentFields.keys({
|
||||
Label: componentSchema,
|
||||
Error: componentSchema,
|
||||
BeforeInput: joi.array().items(componentSchema),
|
||||
AfterInput: joi.array().items(componentSchema),
|
||||
Label: componentSchema,
|
||||
afterInput: joi.array().items(componentSchema),
|
||||
beforeInput: joi.array().items(componentSchema),
|
||||
}),
|
||||
}),
|
||||
defaultValue: joi.alternatives().try(joi.boolean(), joi.func()),
|
||||
@@ -347,10 +347,10 @@ export const point = baseField.keys({
|
||||
name: joi.string().required(),
|
||||
admin: baseAdminFields.keys({
|
||||
components: baseAdminComponentFields.keys({
|
||||
Label: componentSchema,
|
||||
Error: componentSchema,
|
||||
BeforeInput: joi.array().items(componentSchema),
|
||||
AfterInput: joi.array().items(componentSchema),
|
||||
Label: componentSchema,
|
||||
afterInput: joi.array().items(componentSchema),
|
||||
beforeInput: joi.array().items(componentSchema),
|
||||
}),
|
||||
}),
|
||||
defaultValue: joi.alternatives().try(joi.array().items(joi.number()).max(2).min(2), joi.func()),
|
||||
@@ -361,11 +361,11 @@ export const relationship = baseField.keys({
|
||||
name: joi.string().required(),
|
||||
admin: baseAdminFields.keys({
|
||||
allowCreate: joi.boolean().default(true),
|
||||
isSortable: joi.boolean().default(false),
|
||||
components: baseAdminComponentFields.keys({
|
||||
Label: componentSchema,
|
||||
Error: componentSchema,
|
||||
Label: componentSchema,
|
||||
}),
|
||||
isSortable: joi.boolean().default(false),
|
||||
}),
|
||||
defaultValue: joi.alternatives().try(joi.func()),
|
||||
filterOptions: joi.alternatives().try(joi.object(), joi.func()),
|
||||
@@ -445,6 +445,12 @@ export const richText = baseField.keys({
|
||||
export const date = baseField.keys({
|
||||
name: joi.string().required(),
|
||||
admin: baseAdminFields.keys({
|
||||
components: baseAdminComponentFields.keys({
|
||||
Error: componentSchema,
|
||||
Label: componentSchema,
|
||||
afterInput: joi.array().items(componentSchema),
|
||||
beforeInput: joi.array().items(componentSchema),
|
||||
}),
|
||||
date: joi.object({
|
||||
displayFormat: joi.string(),
|
||||
maxDate: joi.date(),
|
||||
@@ -458,12 +464,6 @@ export const date = baseField.keys({
|
||||
timeIntervals: joi.number(),
|
||||
}),
|
||||
placeholder: joi.string(),
|
||||
components: baseAdminComponentFields.keys({
|
||||
Label: componentSchema,
|
||||
Error: componentSchema,
|
||||
BeforeInput: joi.array().items(componentSchema),
|
||||
AfterInput: joi.array().items(componentSchema),
|
||||
}),
|
||||
}),
|
||||
defaultValue: joi.alternatives().try(joi.string(), joi.func()),
|
||||
type: joi.string().valid('date').required(),
|
||||
|
||||
@@ -4,8 +4,12 @@ import type { TFunction } from 'i18next'
|
||||
import type { CSSProperties } from 'react'
|
||||
|
||||
import monacoeditor from 'monaco-editor' // IMPORTANT - DO NOT REMOVE: This is required for pnpm's default isolated mode to work - even though the import is not used. This is due to a typescript bug: https://github.com/microsoft/TypeScript/issues/47663#issuecomment-1519138189. (tsbugisolatedmode)
|
||||
import type React from 'react'
|
||||
|
||||
import type { ConditionalDateProps } from '../../admin/components/elements/DatePicker/types'
|
||||
import type { Props as ErrorProps } from '../../admin/components/forms/Error/types'
|
||||
import type { Description } from '../../admin/components/forms/FieldDescription/types'
|
||||
import type { Props as LabelProps } from '../../admin/components/forms/Label/types'
|
||||
import type { RowLabel } from '../../admin/components/forms/RowLabel/types'
|
||||
import type { RichTextAdapter } from '../../admin/components/forms/field-types/RichText/types'
|
||||
import type { User } from '../../auth'
|
||||
@@ -15,8 +19,6 @@ import type { PayloadRequest, RequestContext } from '../../express/types'
|
||||
import type { SanitizedGlobalConfig } from '../../globals/config/types'
|
||||
import type { Payload } from '../../payload'
|
||||
import type { Operation, Where } from '../../types'
|
||||
import type { Props as ErrorProps } from '../../admin/components/forms/Error/types'
|
||||
import type { Props as LabelProps } from '../../admin/components/forms/Label/types'
|
||||
|
||||
export type FieldHookArgs<T extends TypeWithID = any, P = any, S = any> = {
|
||||
/** The collection which the field belongs to. If the field belongs to a global, this will be null. */
|
||||
@@ -170,16 +172,16 @@ export type NumberField = FieldBase & {
|
||||
admin?: Admin & {
|
||||
/** Set this property to a string that will be used for browser autocomplete. */
|
||||
autoComplete?: string
|
||||
components?: {
|
||||
Error?: React.ComponentType<ErrorProps>
|
||||
Label?: React.ComponentType<LabelProps>
|
||||
afterInput?: React.ComponentType<any>[]
|
||||
beforeInput?: React.ComponentType<any>[]
|
||||
}
|
||||
/** Set this property to define a placeholder string for the field. */
|
||||
placeholder?: Record<string, string> | string
|
||||
/** Set a value for the number field to increment / decrement using browser controls. */
|
||||
step?: number
|
||||
components?: {
|
||||
Error?: React.ComponentType<ErrorProps>
|
||||
Label?: React.ComponentType<LabelProps>
|
||||
BeforeInput?: React.ReactElement<any>[]
|
||||
AfterInput?: React.ReactElement<any>[]
|
||||
}
|
||||
}
|
||||
/** Maximum value accepted. Used in the default `validation` function. */
|
||||
max?: number
|
||||
@@ -208,14 +210,14 @@ export type NumberField = FieldBase & {
|
||||
export type TextField = FieldBase & {
|
||||
admin?: Admin & {
|
||||
autoComplete?: string
|
||||
placeholder?: Record<string, string> | string
|
||||
rtl?: boolean
|
||||
components?: {
|
||||
Error?: React.ComponentType<ErrorProps>
|
||||
Label?: React.ComponentType<LabelProps>
|
||||
BeforeInput?: React.ReactElement<any>[]
|
||||
AfterInput?: React.ReactElement<any>[]
|
||||
afterInput?: React.ComponentType<any>[]
|
||||
beforeInput?: React.ComponentType<any>[]
|
||||
}
|
||||
placeholder?: Record<string, string> | string
|
||||
rtl?: boolean
|
||||
}
|
||||
maxLength?: number
|
||||
minLength?: number
|
||||
@@ -225,28 +227,28 @@ export type TextField = FieldBase & {
|
||||
export type EmailField = FieldBase & {
|
||||
admin?: Admin & {
|
||||
autoComplete?: string
|
||||
placeholder?: Record<string, string> | string
|
||||
components?: {
|
||||
Error?: React.ComponentType<ErrorProps>
|
||||
Label?: React.ComponentType<LabelProps>
|
||||
BeforeInput?: React.ReactElement<any>[]
|
||||
AfterInput?: React.ReactElement<any>[]
|
||||
afterInput?: React.ComponentType<any>[]
|
||||
beforeInput?: React.ComponentType<any>[]
|
||||
}
|
||||
placeholder?: Record<string, string> | string
|
||||
}
|
||||
type: 'email'
|
||||
}
|
||||
|
||||
export type TextareaField = FieldBase & {
|
||||
admin?: Admin & {
|
||||
placeholder?: Record<string, string> | string
|
||||
rows?: number
|
||||
rtl?: boolean
|
||||
components?: {
|
||||
Error?: React.ComponentType<ErrorProps>
|
||||
Label?: React.ComponentType<LabelProps>
|
||||
BeforeInput?: React.ReactElement<any>[]
|
||||
AfterInput?: React.ReactElement<any>[]
|
||||
afterInput?: React.ComponentType<any>[]
|
||||
beforeInput?: React.ComponentType<any>[]
|
||||
}
|
||||
placeholder?: Record<string, string> | string
|
||||
rows?: number
|
||||
rtl?: boolean
|
||||
}
|
||||
maxLength?: number
|
||||
minLength?: number
|
||||
@@ -254,27 +256,27 @@ export type TextareaField = FieldBase & {
|
||||
}
|
||||
|
||||
export type CheckboxField = FieldBase & {
|
||||
type: 'checkbox'
|
||||
admin?: Admin & {
|
||||
components?: {
|
||||
Error?: React.ComponentType<ErrorProps>
|
||||
Label?: React.ComponentType<LabelProps>
|
||||
BeforeInput?: React.ReactElement<any>[]
|
||||
AfterInput?: React.ReactElement<any>[]
|
||||
afterInput?: React.ComponentType<any>[]
|
||||
beforeInput?: React.ComponentType<any>[]
|
||||
}
|
||||
}
|
||||
type: 'checkbox'
|
||||
}
|
||||
|
||||
export type DateField = FieldBase & {
|
||||
admin?: Admin & {
|
||||
date?: ConditionalDateProps
|
||||
placeholder?: Record<string, string> | string
|
||||
components?: {
|
||||
Error?: React.ComponentType<ErrorProps>
|
||||
Label?: React.ComponentType<LabelProps>
|
||||
BeforeInput?: React.ReactElement<any>[]
|
||||
AfterInput?: React.ReactElement<any>[]
|
||||
afterInput?: React.ComponentType<any>[]
|
||||
beforeInput?: React.ComponentType<any>[]
|
||||
}
|
||||
date?: ConditionalDateProps
|
||||
placeholder?: Record<string, string> | string
|
||||
}
|
||||
type: 'date'
|
||||
}
|
||||
@@ -382,12 +384,12 @@ export type UploadField = FieldBase & {
|
||||
}
|
||||
|
||||
type CodeAdmin = Admin & {
|
||||
editorOptions?: EditorProps['options']
|
||||
language?: string
|
||||
components?: {
|
||||
Error?: React.ComponentType<ErrorProps>
|
||||
Label?: React.ComponentType<LabelProps>
|
||||
}
|
||||
editorOptions?: EditorProps['options']
|
||||
language?: string
|
||||
}
|
||||
|
||||
export type CodeField = Omit<FieldBase, 'admin'> & {
|
||||
@@ -398,11 +400,11 @@ export type CodeField = Omit<FieldBase, 'admin'> & {
|
||||
}
|
||||
|
||||
type JSONAdmin = Admin & {
|
||||
editorOptions?: EditorProps['options']
|
||||
components?: {
|
||||
Error?: React.ComponentType<ErrorProps>
|
||||
Label?: React.ComponentType<LabelProps>
|
||||
}
|
||||
editorOptions?: EditorProps['options']
|
||||
}
|
||||
|
||||
export type JSONField = Omit<FieldBase, 'admin'> & {
|
||||
@@ -412,12 +414,12 @@ export type JSONField = Omit<FieldBase, 'admin'> & {
|
||||
|
||||
export type SelectField = FieldBase & {
|
||||
admin?: Admin & {
|
||||
isClearable?: boolean
|
||||
isSortable?: boolean
|
||||
components?: {
|
||||
Error?: React.ComponentType<ErrorProps>
|
||||
Label?: React.ComponentType<LabelProps>
|
||||
}
|
||||
isClearable?: boolean
|
||||
isSortable?: boolean
|
||||
}
|
||||
hasMany?: boolean
|
||||
options: Option[]
|
||||
@@ -427,11 +429,11 @@ export type SelectField = FieldBase & {
|
||||
export type RelationshipField = FieldBase & {
|
||||
admin?: Admin & {
|
||||
allowCreate?: boolean
|
||||
isSortable?: boolean
|
||||
components?: {
|
||||
Error?: React.ComponentType<ErrorProps>
|
||||
Label?: React.ComponentType<LabelProps>
|
||||
}
|
||||
isSortable?: boolean
|
||||
}
|
||||
filterOptions?: FilterOptions
|
||||
hasMany?: boolean
|
||||
@@ -515,11 +517,11 @@ export type ArrayField = FieldBase & {
|
||||
|
||||
export type RadioField = FieldBase & {
|
||||
admin?: Admin & {
|
||||
layout?: 'horizontal' | 'vertical'
|
||||
components?: {
|
||||
Error?: React.ComponentType<ErrorProps>
|
||||
Label?: React.ComponentType<LabelProps>
|
||||
}
|
||||
layout?: 'horizontal' | 'vertical'
|
||||
}
|
||||
options: Option[]
|
||||
type: 'radio'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Block, Data, Fields } from 'payload/types'
|
||||
import type { Block, Data, Field, Fields } from 'payload/types'
|
||||
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||
import isDeepEqual from 'deep-equal'
|
||||
@@ -9,7 +9,7 @@ import { SectionTitle } from 'payload/components/fields/Blocks'
|
||||
import { RenderFields, createNestedFieldPath, useFormSubmitted } from 'payload/components/forms'
|
||||
import { useDocumentInfo } from 'payload/components/utilities'
|
||||
import { getTranslation } from 'payload/utilities'
|
||||
import React, { useCallback } from 'react'
|
||||
import React, { useCallback, useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import type { FieldProps } from '../../../../types'
|
||||
@@ -21,7 +21,8 @@ type Props = {
|
||||
baseClass: string
|
||||
block: Block
|
||||
field: FieldProps
|
||||
fields: BlockFields
|
||||
formData: BlockFields
|
||||
formSchema: Field[]
|
||||
nodeKey: string
|
||||
}
|
||||
|
||||
@@ -31,7 +32,14 @@ type Props = {
|
||||
* not the whole document.
|
||||
*/
|
||||
export const BlockContent: React.FC<Props> = (props) => {
|
||||
const { baseClass, block, field, fields, nodeKey } = props
|
||||
const {
|
||||
baseClass,
|
||||
block: { labels },
|
||||
field,
|
||||
formData,
|
||||
formSchema,
|
||||
nodeKey,
|
||||
} = props
|
||||
const { i18n } = useTranslation()
|
||||
const [editor] = useLexicalComposerContext()
|
||||
// Used for saving collapsed to preferences (and gettin' it from there again)
|
||||
@@ -47,9 +55,9 @@ export const BlockContent: React.FC<Props> = (props) => {
|
||||
|
||||
const collapsedMap: { [key: string]: boolean } = currentFieldPreferences?.collapsed
|
||||
|
||||
if (collapsedMap && collapsedMap[fields.data.id] !== undefined) {
|
||||
setCollapsed(collapsedMap[fields.data.id])
|
||||
initialState = collapsedMap[fields.data.id]
|
||||
if (collapsedMap && collapsedMap[formData.id] !== undefined) {
|
||||
setCollapsed(collapsedMap[formData.id])
|
||||
initialState = collapsedMap[formData.id]
|
||||
}
|
||||
})
|
||||
return initialState
|
||||
@@ -70,13 +78,19 @@ export const BlockContent: React.FC<Props> = (props) => {
|
||||
const path = '' as const
|
||||
|
||||
const onFormChange = useCallback(
|
||||
({ fields: formFields, formData }: { fields: Fields; formData: Data }) => {
|
||||
({
|
||||
fullFieldsWithValues,
|
||||
newFormData,
|
||||
}: {
|
||||
fullFieldsWithValues: Fields
|
||||
newFormData: Data
|
||||
}) => {
|
||||
// Recursively remove all undefined values from even being present in formData, as they will
|
||||
// cause isDeepEqual to return false if, for example, formData has a key that fields.data
|
||||
// does not have, even if it's undefined.
|
||||
// Currently, this happens if a block has another sub-blocks field. Inside of formData, that sub-blocks field has an undefined blockName property.
|
||||
// Inside of fields.data however, that sub-blocks blockName property does not exist at all.
|
||||
function removeUndefinedRecursively(obj: any) {
|
||||
function removeUndefinedRecursively(obj: object) {
|
||||
Object.keys(obj).forEach((key) => {
|
||||
if (obj[key] && typeof obj[key] === 'object') {
|
||||
removeUndefinedRecursively(obj[key])
|
||||
@@ -85,12 +99,12 @@ export const BlockContent: React.FC<Props> = (props) => {
|
||||
}
|
||||
})
|
||||
}
|
||||
removeUndefinedRecursively(newFormData)
|
||||
removeUndefinedRecursively(formData)
|
||||
removeUndefinedRecursively(fields.data)
|
||||
|
||||
// Only update if the data has actually changed. Otherwise, we may be triggering an unnecessary value change,
|
||||
// which would trigger the "Leave without saving" dialog unnecessarily
|
||||
if (!isDeepEqual(fields.data, formData)) {
|
||||
if (!isDeepEqual(formData, newFormData)) {
|
||||
// Running this in the next tick in the meantime fixes this issue: https://github.com/payloadcms/payload/issues/4108
|
||||
// I don't know why. When this is called immediately, it might focus out of a nested lexical editor field if an update is made there.
|
||||
// My hypothesis is that the nested editor might not have fully finished its update cycle yet. By updating in the next tick, we
|
||||
@@ -99,9 +113,7 @@ export const BlockContent: React.FC<Props> = (props) => {
|
||||
editor.update(() => {
|
||||
const node: BlockNode = $getNodeByKey(nodeKey)
|
||||
if (node) {
|
||||
node.setFields({
|
||||
data: formData as any,
|
||||
})
|
||||
node.setFields(newFormData as BlockFields)
|
||||
}
|
||||
})
|
||||
}, 0)
|
||||
@@ -110,7 +122,7 @@ export const BlockContent: React.FC<Props> = (props) => {
|
||||
// update error count
|
||||
if (hasSubmitted) {
|
||||
let rowErrorCount = 0
|
||||
for (const formField of Object.values(formFields)) {
|
||||
for (const formField of Object.values(fullFieldsWithValues)) {
|
||||
if (formField?.valid === false) {
|
||||
rowErrorCount++
|
||||
}
|
||||
@@ -118,7 +130,7 @@ export const BlockContent: React.FC<Props> = (props) => {
|
||||
setErrorCount(rowErrorCount)
|
||||
}
|
||||
},
|
||||
[editor, nodeKey, hasSubmitted],
|
||||
[editor, nodeKey, hasSubmitted, formData],
|
||||
)
|
||||
|
||||
const onCollapsedChange = useCallback(() => {
|
||||
@@ -130,13 +142,13 @@ export const BlockContent: React.FC<Props> = (props) => {
|
||||
const newCollapsed: { [key: string]: boolean } =
|
||||
collapsedMap && collapsedMap?.size ? collapsedMap : {}
|
||||
|
||||
newCollapsed[fields.data.id] = !collapsed
|
||||
newCollapsed[formData.id] = !collapsed
|
||||
|
||||
setDocFieldPreferences(field.name, {
|
||||
collapsed: newCollapsed,
|
||||
})
|
||||
})
|
||||
}, [collapsed, getDocPreferences, field.name, setDocFieldPreferences, fields.data.id])
|
||||
}, [collapsed, getDocPreferences, field.name, setDocFieldPreferences, formData.id])
|
||||
|
||||
const removeBlock = useCallback(() => {
|
||||
editor.update(() => {
|
||||
@@ -144,6 +156,11 @@ export const BlockContent: React.FC<Props> = (props) => {
|
||||
})
|
||||
}, [editor, nodeKey])
|
||||
|
||||
const fieldSchemaWithPath = formSchema.map((field) => ({
|
||||
...field,
|
||||
path: createNestedFieldPath(null, field),
|
||||
}))
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Collapsible
|
||||
@@ -154,10 +171,10 @@ export const BlockContent: React.FC<Props> = (props) => {
|
||||
<div className={`${baseClass}__block-header`}>
|
||||
<div>
|
||||
<Pill
|
||||
className={`${baseClass}__block-pill ${baseClass}__block-pill-${fields?.data?.blockType}`}
|
||||
className={`${baseClass}__block-pill ${baseClass}__block-pill-${formData?.blockType}`}
|
||||
pillStyle="white"
|
||||
>
|
||||
{getTranslation(block.labels.singular, i18n)}
|
||||
{getTranslation(labels.singular, i18n)}
|
||||
</Pill>
|
||||
<SectionTitle path={`${path}blockName`} readOnly={field?.admin?.readOnly} />
|
||||
{fieldHasErrors && <ErrorPill count={errorCount} withMessage />}
|
||||
@@ -186,25 +203,16 @@ export const BlockContent: React.FC<Props> = (props) => {
|
||||
>
|
||||
<RenderFields
|
||||
className={`${baseClass}__fields`}
|
||||
fieldSchema={block.fields.map((field) => ({
|
||||
...field,
|
||||
path: createNestedFieldPath(null, field),
|
||||
}))}
|
||||
fieldSchema={fieldSchemaWithPath}
|
||||
fieldTypes={field.fieldTypes}
|
||||
forceRender
|
||||
margins="small"
|
||||
permissions={field.permissions?.blocks?.[fields?.data?.blockType]?.fields}
|
||||
permissions={field.permissions?.blocks?.[formData?.blockType]?.fields}
|
||||
readOnly={field.admin.readOnly}
|
||||
/>
|
||||
</Collapsible>
|
||||
|
||||
<FormSavePlugin
|
||||
fieldSchema={block.fields.map((field) => ({
|
||||
...field,
|
||||
path: createNestedFieldPath(null, field),
|
||||
}))}
|
||||
onChange={onFormChange}
|
||||
/>
|
||||
<FormSavePlugin onChange={onFormChange} />
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import type { Data, FieldWithPath, Fields } from 'payload/types'
|
||||
import type { Data, Fields } from 'payload/types'
|
||||
import type React from 'react'
|
||||
|
||||
import { reduceFieldsToValues, useAllFormFields } from 'payload/components/forms'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
import './index.scss'
|
||||
|
||||
type Props = {
|
||||
fieldSchema: FieldWithPath[]
|
||||
onChange?: ({ fields, formData }: { fields: Fields; formData: Data }) => void
|
||||
onChange?: ({
|
||||
fullFieldsWithValues,
|
||||
newFormData,
|
||||
}: {
|
||||
fullFieldsWithValues: Fields
|
||||
newFormData: Data
|
||||
}) => void
|
||||
}
|
||||
|
||||
export const FormSavePlugin: React.FC<Props> = (props) => {
|
||||
@@ -18,13 +21,13 @@ export const FormSavePlugin: React.FC<Props> = (props) => {
|
||||
|
||||
// Pass in fields, and indicate if you'd like to "unflatten" field data.
|
||||
// The result below will reflect the data stored in the form at the given time
|
||||
const formData = reduceFieldsToValues(fields, true)
|
||||
const newFormData = reduceFieldsToValues(fields, true)
|
||||
|
||||
useEffect(() => {
|
||||
if (onChange) {
|
||||
onChange({ fields, formData })
|
||||
onChange({ fullFieldsWithValues: fields, newFormData })
|
||||
}
|
||||
}, [formData, onChange, fields])
|
||||
}, [newFormData, onChange, fields])
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
'use client'
|
||||
import { type ElementFormatType } from 'lexical'
|
||||
import { Form, buildInitialState, useFormSubmitted } from 'payload/components/forms'
|
||||
import React, { useEffect, useMemo } from 'react'
|
||||
|
||||
@@ -20,37 +19,45 @@ import { useTranslation } from 'react-i18next'
|
||||
import type { BlocksFeatureProps } from '..'
|
||||
|
||||
import { useEditorConfigContext } from '../../../lexical/config/EditorConfigProvider'
|
||||
import { transformInputFormSchema } from '../utils/transformInputFormSchema'
|
||||
import { BlockContent } from './BlockContent'
|
||||
import './index.scss'
|
||||
|
||||
type Props = {
|
||||
blockFieldWrapperName: string
|
||||
children?: React.ReactNode
|
||||
className?: string
|
||||
fields: BlockFields
|
||||
format?: ElementFormatType
|
||||
/**
|
||||
* This formData already comes wrapped in blockFieldWrapperName
|
||||
*/
|
||||
formData: BlockFields
|
||||
nodeKey?: string
|
||||
}
|
||||
|
||||
export const BlockComponent: React.FC<Props> = (props) => {
|
||||
const { children, className, fields, format, nodeKey } = props
|
||||
const { blockFieldWrapperName, formData, nodeKey } = props
|
||||
const payloadConfig = useConfig()
|
||||
const submitted = useFormSubmitted()
|
||||
|
||||
const { editorConfig, field } = useEditorConfigContext()
|
||||
const { editorConfig, field: parentLexicalRichTextField } = useEditorConfigContext()
|
||||
|
||||
const block = (
|
||||
editorConfig?.resolvedFeatureMap?.get('blocks')?.props as BlocksFeatureProps
|
||||
)?.blocks?.find((block) => block.slug === fields?.data?.blockType)
|
||||
)?.blocks?.find((block) => block.slug === formData?.blockType)
|
||||
|
||||
const unsanitizedFormSchema = block?.fields || []
|
||||
|
||||
// Sanitize block's fields here. This is done here and not in the feature, because the payload config is available here
|
||||
const validRelationships = payloadConfig.collections.map((c) => c.slug) || []
|
||||
block.fields = sanitizeFields({
|
||||
const formSchema = transformInputFormSchema(
|
||||
sanitizeFields({
|
||||
config: payloadConfig,
|
||||
fields: block.fields,
|
||||
fields: unsanitizedFormSchema,
|
||||
validRelationships,
|
||||
})
|
||||
}),
|
||||
blockFieldWrapperName,
|
||||
)
|
||||
|
||||
const initialStateRef = React.useRef<Data>(buildInitialState(fields.data || {})) // Store initial value in a ref, so it doesn't change on re-render and only gets initialized once
|
||||
const initialStateRef = React.useRef<Data>(null) // Store initial value in a ref, so it doesn't change on re-render and only gets initialized once
|
||||
|
||||
const config = useConfig()
|
||||
const { t } = useTranslation('general')
|
||||
@@ -58,52 +65,58 @@ export const BlockComponent: React.FC<Props> = (props) => {
|
||||
const { getDocPreferences } = useDocumentInfo()
|
||||
|
||||
// initialState State
|
||||
|
||||
const [initialState, setInitialState] = React.useState<Data>(null)
|
||||
|
||||
useEffect(() => {
|
||||
async function buildInitialState() {
|
||||
async function createInitialState() {
|
||||
const preferences = await getDocPreferences()
|
||||
|
||||
const stateFromSchema = await buildStateFromSchema({
|
||||
config,
|
||||
data: fields.data,
|
||||
fieldSchema: block.fields,
|
||||
data: JSON.parse(JSON.stringify(formData)),
|
||||
fieldSchema: formSchema as any,
|
||||
locale,
|
||||
operation: 'update',
|
||||
operation: 'create',
|
||||
preferences,
|
||||
t,
|
||||
})
|
||||
|
||||
if (!initialStateRef.current) {
|
||||
initialStateRef.current = buildInitialState(JSON.parse(JSON.stringify(formData)))
|
||||
}
|
||||
|
||||
// We have to merge the output of buildInitialState (above this useEffect) with the output of buildStateFromSchema.
|
||||
// That's because the output of buildInitialState provides important properties necessary for THIS block,
|
||||
// like blockName, blockType and id, while buildStateFromSchema provides the correct output of this block's data,
|
||||
// e.g. if this block has a sub-block (like the `rows` property)
|
||||
setInitialState({
|
||||
...initialStateRef?.current,
|
||||
const consolidatedInitialState = {
|
||||
...initialStateRef.current,
|
||||
...stateFromSchema,
|
||||
})
|
||||
}
|
||||
void buildInitialState()
|
||||
}, [setInitialState, config, block, locale, getDocPreferences, t]) // do not add fields here, it causes an endless loop
|
||||
|
||||
setInitialState(consolidatedInitialState)
|
||||
}
|
||||
void createInitialState()
|
||||
}, [setInitialState, config, locale, getDocPreferences, t]) // do not add formData or formSchema here, it causes an endless loop
|
||||
|
||||
// Memoized Form JSX
|
||||
const formContent = useMemo(() => {
|
||||
return (
|
||||
block &&
|
||||
initialState && (
|
||||
<Form fields={block.fields} initialState={initialState} submitted={submitted}>
|
||||
<Form fields={formSchema} initialState={initialState} submitted={submitted}>
|
||||
<BlockContent
|
||||
baseClass={baseClass}
|
||||
block={block}
|
||||
field={field}
|
||||
fields={fields}
|
||||
field={parentLexicalRichTextField}
|
||||
formData={formData}
|
||||
formSchema={formSchema}
|
||||
nodeKey={nodeKey}
|
||||
/>
|
||||
</Form>
|
||||
)
|
||||
)
|
||||
}, [block, field, nodeKey, submitted, initialState])
|
||||
}, [block, parentLexicalRichTextField, nodeKey, submitted, initialState])
|
||||
|
||||
return <div className={baseClass}>{formContent}</div>
|
||||
}
|
||||
|
||||
@@ -37,10 +37,9 @@ const insertBlock = ({
|
||||
}) => {
|
||||
if (!replaceNodeKey) {
|
||||
editor.dispatchCommand(INSERT_BLOCK_COMMAND, {
|
||||
data: {
|
||||
id: null,
|
||||
blockName: '',
|
||||
blockType: blockType,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
editor.update(() => {
|
||||
@@ -48,10 +47,9 @@ const insertBlock = ({
|
||||
if (node) {
|
||||
node.replace(
|
||||
$createBlockNode({
|
||||
data: {
|
||||
id: null,
|
||||
blockName: '',
|
||||
blockType: blockType,
|
||||
},
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -20,22 +20,15 @@ export type BlocksFeatureProps = {
|
||||
export const BlocksFeature = (props?: BlocksFeatureProps): FeatureProvider => {
|
||||
// Sanitization taken from payload/src/fields/config/sanitize.ts
|
||||
if (props?.blocks?.length) {
|
||||
props.blocks = props.blocks.map((block) => ({
|
||||
props.blocks = props.blocks.map((block) => {
|
||||
return {
|
||||
...block,
|
||||
fields: block.fields.concat(baseBlockFields),
|
||||
}))
|
||||
|
||||
props.blocks = props.blocks.map((block) => {
|
||||
const unsanitizedBlock = { ...block }
|
||||
unsanitizedBlock.labels = !unsanitizedBlock.labels
|
||||
? formatLabels(unsanitizedBlock.slug)
|
||||
: unsanitizedBlock.labels
|
||||
|
||||
labels: !block.labels ? formatLabels(block.slug) : block.labels,
|
||||
}
|
||||
})
|
||||
// unsanitizedBlock.fields are sanitized in the React component and not here.
|
||||
// That's because we do not have access to the payload config here.
|
||||
|
||||
return unsanitizedBlock
|
||||
})
|
||||
}
|
||||
return {
|
||||
feature: () => {
|
||||
@@ -59,13 +52,6 @@ export const BlocksFeature = (props?: BlocksFeatureProps): FeatureProvider => {
|
||||
options: [
|
||||
{
|
||||
options: [
|
||||
/*new SlashMenuOption('Block', {
|
||||
Icon: BlockIcon,
|
||||
keywords: ['block', 'blocks'],
|
||||
onSelect: ({ editor }) => {
|
||||
editor.dispatchCommand(INSERT_BLOCK_WITH_DRAWER_COMMAND, null)
|
||||
},
|
||||
}),*/
|
||||
...props?.blocks?.map((block) => {
|
||||
return new SlashMenuOption(block.slug, {
|
||||
Icon: BlockIcon,
|
||||
@@ -75,10 +61,9 @@ export const BlocksFeature = (props?: BlocksFeatureProps): FeatureProvider => {
|
||||
keywords: ['block', 'blocks', block.slug],
|
||||
onSelect: ({ editor }) => {
|
||||
editor.dispatchCommand(INSERT_BLOCK_COMMAND, {
|
||||
data: {
|
||||
id: null,
|
||||
blockName: '',
|
||||
blockType: block.slug,
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
@@ -15,15 +15,14 @@ import ObjectID from 'bson-objectid'
|
||||
import React from 'react'
|
||||
|
||||
import { BlockComponent } from '../component'
|
||||
import { transformInputFormData } from '../utils/transformInputFormData'
|
||||
|
||||
export type BlockFields = {
|
||||
/** Block data */
|
||||
data: {
|
||||
/** Block form data */
|
||||
[key: string]: any
|
||||
blockName: string
|
||||
blockType: string
|
||||
id?: string
|
||||
}
|
||||
id: string
|
||||
}
|
||||
|
||||
export type SerializedBlockNode = Spread<
|
||||
@@ -66,6 +65,16 @@ export class BlockNode extends DecoratorBlockNode {
|
||||
}
|
||||
|
||||
static importJSON(serializedNode: SerializedBlockNode): BlockNode {
|
||||
if (serializedNode.version === 1) {
|
||||
// Convert (version 1 had the fields wrapped in another, unnecessary data property)
|
||||
serializedNode = {
|
||||
...serializedNode,
|
||||
fields: {
|
||||
...(serializedNode as any).data.fields,
|
||||
},
|
||||
version: 2,
|
||||
}
|
||||
}
|
||||
const node = $createBlockNode(serializedNode.fields)
|
||||
node.setFormat(serializedNode.format)
|
||||
return node
|
||||
@@ -75,11 +84,13 @@ export class BlockNode extends DecoratorBlockNode {
|
||||
return false
|
||||
}
|
||||
decorate(editor: LexicalEditor, config: EditorConfig): JSX.Element {
|
||||
const blockFieldWrapperName = this.getFields().blockType + '-' + this.getFields().id
|
||||
const transformedFormData = transformInputFormData(this.getFields(), blockFieldWrapperName)
|
||||
|
||||
return (
|
||||
<BlockComponent
|
||||
className={config.theme.block ?? 'LexicalEditorTheme__block'}
|
||||
fields={this.__fields}
|
||||
format={this.__format}
|
||||
blockFieldWrapperName={blockFieldWrapperName}
|
||||
formData={transformedFormData}
|
||||
nodeKey={this.getKey()}
|
||||
/>
|
||||
)
|
||||
@@ -98,24 +109,37 @@ export class BlockNode extends DecoratorBlockNode {
|
||||
...super.exportJSON(),
|
||||
fields: this.getFields(),
|
||||
type: this.getType(),
|
||||
version: 1,
|
||||
version: 2,
|
||||
}
|
||||
}
|
||||
|
||||
getFields(): BlockFields {
|
||||
return this.getLatest().__fields
|
||||
}
|
||||
|
||||
getId(): string {
|
||||
return this.__id
|
||||
}
|
||||
|
||||
getTextContent(): string {
|
||||
return `Block Field`
|
||||
}
|
||||
|
||||
setFields(fields: BlockFields): void {
|
||||
let fieldsCopy = JSON.parse(JSON.stringify(fields)) as BlockFields
|
||||
// Possibly transform fields
|
||||
const blockFieldWrapperName = fieldsCopy.blockType + '-' + fieldsCopy.id
|
||||
if (fieldsCopy[blockFieldWrapperName]) {
|
||||
fieldsCopy = {
|
||||
id: fieldsCopy.id,
|
||||
blockName: fieldsCopy.blockName,
|
||||
blockType: fieldsCopy.blockType,
|
||||
...fieldsCopy[blockFieldWrapperName],
|
||||
}
|
||||
delete fieldsCopy[blockFieldWrapperName]
|
||||
}
|
||||
|
||||
const writable = this.getWritable()
|
||||
writable.__fields = fields
|
||||
writable.__fields = fieldsCopy
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,10 +147,7 @@ export function $createBlockNode(fields: Exclude<BlockFields, 'id'>): BlockNode
|
||||
return new BlockNode({
|
||||
fields: {
|
||||
...fields,
|
||||
data: {
|
||||
...fields.data,
|
||||
id: fields?.data?.id || new ObjectID().toHexString(),
|
||||
},
|
||||
id: fields?.id || new ObjectID().toHexString(),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import type { BlockFields } from '../nodes/BlocksNode'
|
||||
import { BlocksDrawerComponent } from '../drawer'
|
||||
import { $createBlockNode, BlockNode } from '../nodes/BlocksNode'
|
||||
|
||||
export type InsertBlockPayload = BlockFields
|
||||
export type InsertBlockPayload = Exclude<BlockFields, 'id'>
|
||||
|
||||
export const INSERT_BLOCK_COMMAND: LexicalCommand<InsertBlockPayload> =
|
||||
createCommand('INSERT_BLOCK_COMMAND')
|
||||
|
||||
@@ -21,7 +21,7 @@ export const blockPopulationPromiseHOC = (
|
||||
showHiddenFields,
|
||||
}) => {
|
||||
const blocks: Block[] = props.blocks
|
||||
const blockFieldData = node.fields.data
|
||||
const blockFieldData = node.fields
|
||||
|
||||
const promises: Promise<void>[] = []
|
||||
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Wraps the input formData in a blockFieldWrapperName, so that it can be read by the RenderFields component
|
||||
* which requires it to be wrapped in a group field
|
||||
*/
|
||||
export function transformInputFormData(data: any, blockFieldWrapperName: string) {
|
||||
const dataCopy = JSON.parse(JSON.stringify(data))
|
||||
|
||||
const fieldDataWithoutBlockFields = { ...dataCopy }
|
||||
delete fieldDataWithoutBlockFields['id']
|
||||
delete fieldDataWithoutBlockFields['blockName']
|
||||
delete fieldDataWithoutBlockFields['blockType']
|
||||
|
||||
// Wrap all fields inside blockFieldWrapperName.
|
||||
// This is necessary, because blockFieldWrapperName is set as the 'base' path for all fields in the block (in the RenderFields component).
|
||||
// Thus, in order for the data to be read, it has to be wrapped in this blockFieldWrapperName, as it's expected to be there.
|
||||
|
||||
// Why are we doing this? Because that way, all rendered fields of the blocks have different paths and names, and thus don't conflict with each other.
|
||||
// They have different paths and names, because they are wrapped in the blockFieldWrapperName, which has a name that is unique for each block.
|
||||
return {
|
||||
id: dataCopy.id,
|
||||
[blockFieldWrapperName]: fieldDataWithoutBlockFields,
|
||||
blockName: dataCopy.blockName,
|
||||
blockType: dataCopy.blockType,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import type { Field } from 'payload/types'
|
||||
|
||||
export function transformInputFormSchema(formSchema: any, blockFieldWrapperName: string): Field[] {
|
||||
const formSchemaCopy = [...formSchema]
|
||||
|
||||
// First, check if it needs wrapping
|
||||
const hasBlockFieldWrapper = formSchemaCopy.some(
|
||||
(field) => 'name' in field && field.name === blockFieldWrapperName,
|
||||
)
|
||||
if (hasBlockFieldWrapper) {
|
||||
return formSchemaCopy
|
||||
}
|
||||
|
||||
// Add a group in the field schema, which represents all values saved in the blockFieldWrapperName
|
||||
return [
|
||||
...formSchemaCopy.filter(
|
||||
(field) => 'name' in field && ['blockName', 'blockType', 'id'].includes(field.name),
|
||||
),
|
||||
{
|
||||
name: blockFieldWrapperName,
|
||||
admin: {
|
||||
hideGutter: true,
|
||||
},
|
||||
fields: formSchemaCopy.filter(
|
||||
(field) => !('name' in field) || !['blockName', 'blockType', 'id'].includes(field.name),
|
||||
),
|
||||
label: '',
|
||||
type: 'group',
|
||||
},
|
||||
]
|
||||
}
|
||||
@@ -15,7 +15,7 @@ export const blockValidationHOC = (
|
||||
payloadConfig,
|
||||
validation,
|
||||
}) => {
|
||||
const blockFieldData = node.fields.data
|
||||
const blockFieldData = node.fields
|
||||
const blocks: Block[] = props.blocks
|
||||
|
||||
// Sanitize block's fields here. This is done here and not in the feature, because the payload config is available here
|
||||
@@ -38,7 +38,7 @@ export const blockValidationHOC = (
|
||||
|
||||
for (const field of block.fields) {
|
||||
if ('validate' in field && typeof field.validate === 'function' && field.validate) {
|
||||
const fieldValue = 'name' in field ? node.fields.data[field.name] : null
|
||||
const fieldValue = 'name' in field ? node.fields[field.name] : null
|
||||
const validationResult = await field.validate(fieldValue, {
|
||||
...field,
|
||||
id: validation.options.id,
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { Page } from '@playwright/test'
|
||||
|
||||
import { expect, test } from '@playwright/test'
|
||||
|
||||
import { initPageConsoleErrorCatch } from '../helpers'
|
||||
import { AdminUrlUtil } from '../helpers/adminUrlUtil'
|
||||
import { initPayloadE2E } from '../helpers/configHelpers'
|
||||
|
||||
@@ -17,6 +18,7 @@ describe('Admin Panel', () => {
|
||||
|
||||
const context = await browser.newContext()
|
||||
page = await context.newPage()
|
||||
initPageConsoleErrorCatch(page)
|
||||
})
|
||||
|
||||
test('example test', async () => {
|
||||
|
||||
@@ -6,7 +6,7 @@ import type { ReadOnlyCollection, RestrictedVersion } from './payload-types'
|
||||
|
||||
import payload from '../../packages/payload/src'
|
||||
import wait from '../../packages/payload/src/utilities/wait'
|
||||
import { exactText, openDocControls, openNav } from '../helpers'
|
||||
import { exactText, initPageConsoleErrorCatch, openDocControls, openNav } from '../helpers'
|
||||
import { AdminUrlUtil } from '../helpers/adminUrlUtil'
|
||||
import { initPayloadE2E } from '../helpers/configHelpers'
|
||||
import {
|
||||
@@ -47,6 +47,7 @@ describe('access control', () => {
|
||||
|
||||
const context = await browser.newContext()
|
||||
page = await context.newPage()
|
||||
initPageConsoleErrorCatch(page)
|
||||
})
|
||||
|
||||
test('field without read access should not show', async () => {
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
checkBreadcrumb,
|
||||
checkPageTitle,
|
||||
exactText,
|
||||
initPageConsoleErrorCatch,
|
||||
openDocControls,
|
||||
openNav,
|
||||
saveDocAndAssert,
|
||||
@@ -61,6 +62,7 @@ describe('admin', () => {
|
||||
|
||||
const context = await browser.newContext()
|
||||
page = await context.newPage()
|
||||
initPageConsoleErrorCatch(page)
|
||||
})
|
||||
beforeEach(async () => {
|
||||
await clearAndSeedEverything(payload)
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { Page } from '@playwright/test'
|
||||
|
||||
import { expect, test } from '@playwright/test'
|
||||
|
||||
import { login, saveDocAndAssert } from '../helpers'
|
||||
import { initPageConsoleErrorCatch, login, saveDocAndAssert } from '../helpers'
|
||||
import { AdminUrlUtil } from '../helpers/adminUrlUtil'
|
||||
import { initPayloadE2E } from '../helpers/configHelpers'
|
||||
import { slug } from './shared'
|
||||
@@ -27,6 +27,7 @@ describe('auth', () => {
|
||||
|
||||
const context = await browser.newContext()
|
||||
page = await context.newPage()
|
||||
initPageConsoleErrorCatch(page)
|
||||
|
||||
await login({
|
||||
page,
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { Page } from '@playwright/test'
|
||||
|
||||
import { expect, test } from '@playwright/test'
|
||||
|
||||
import { initPageConsoleErrorCatch } from '../helpers'
|
||||
import { initPayloadE2E } from '../helpers/configHelpers'
|
||||
|
||||
const { beforeAll, describe } = test
|
||||
@@ -14,6 +15,7 @@ describe('field error states', () => {
|
||||
;({ serverURL } = await initPayloadE2E(__dirname))
|
||||
const context = await browser.newContext()
|
||||
page = await context.newPage()
|
||||
initPageConsoleErrorCatch(page)
|
||||
})
|
||||
|
||||
test('Remove row should remove error states from parent fields', async () => {
|
||||
|
||||
@@ -13,7 +13,7 @@ import type {
|
||||
import payload from '../../packages/payload/src'
|
||||
import { mapAsync } from '../../packages/payload/src/utilities/mapAsync'
|
||||
import wait from '../../packages/payload/src/utilities/wait'
|
||||
import { openDocControls, saveDocAndAssert } from '../helpers'
|
||||
import { initPageConsoleErrorCatch, openDocControls, saveDocAndAssert } from '../helpers'
|
||||
import { AdminUrlUtil } from '../helpers/adminUrlUtil'
|
||||
import { initPayloadE2E } from '../helpers/configHelpers'
|
||||
import {
|
||||
@@ -47,6 +47,8 @@ describe('fields - relationship', () => {
|
||||
|
||||
const context = await browser.newContext()
|
||||
page = await context.newPage()
|
||||
|
||||
initPageConsoleErrorCatch(page)
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
@@ -445,7 +447,7 @@ describe('fields - relationship', () => {
|
||||
await expect(relationship).toContainText('Untitled - ID: ')
|
||||
})
|
||||
|
||||
test('should show useAsTitle on relation in list view', async () => {
|
||||
test('x in list view', async () => {
|
||||
await page.goto(url.list)
|
||||
await wait(110)
|
||||
const relationship = page.locator('.row-1 .cell-relationshipWithTitle')
|
||||
|
||||
@@ -13,6 +13,30 @@ export const TextBlock: Block = {
|
||||
slug: 'text',
|
||||
}
|
||||
|
||||
export const RadioButtonsBlock: Block = {
|
||||
fields: [
|
||||
{
|
||||
name: 'radioButtons',
|
||||
type: 'radio',
|
||||
options: [
|
||||
{
|
||||
label: 'Option 1',
|
||||
value: 'option1',
|
||||
},
|
||||
{
|
||||
label: 'Option 2',
|
||||
value: 'option2',
|
||||
},
|
||||
{
|
||||
label: 'Option 3',
|
||||
value: 'option3',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
slug: 'radioButtons',
|
||||
}
|
||||
|
||||
export const RichTextBlock: Block = {
|
||||
fields: [
|
||||
{
|
||||
|
||||
@@ -76,22 +76,19 @@ export function generateLexicalRichText() {
|
||||
{
|
||||
format: '',
|
||||
type: 'block',
|
||||
version: 1,
|
||||
version: 2,
|
||||
fields: {
|
||||
data: {
|
||||
id: '65298b13db4ef8c744a7faaa',
|
||||
rel: '{{UPLOAD_DOC_ID}}',
|
||||
blockName: 'Block Node, with Relationship Field',
|
||||
blockType: 'relationshipBlock',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
format: '',
|
||||
type: 'block',
|
||||
version: 1,
|
||||
version: 2,
|
||||
fields: {
|
||||
data: {
|
||||
id: '65298b1ddb4ef8c744a7faab',
|
||||
richText: {
|
||||
root: {
|
||||
@@ -135,16 +132,13 @@ export function generateLexicalRichText() {
|
||||
blockType: 'richText',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
format: '',
|
||||
type: 'block',
|
||||
version: 1,
|
||||
version: 2,
|
||||
fields: {
|
||||
data: {
|
||||
id: '65298b2bdb4ef8c744a7faac',
|
||||
blockName:
|
||||
'Block Node, with Blocks Field, With RichText Field, With Relationship Node',
|
||||
blockName: 'Block Node, with Blocks Field, With RichText Field, With Relationship Node',
|
||||
blockType: 'subBlock',
|
||||
subBlocks: [
|
||||
{
|
||||
@@ -192,20 +186,17 @@ export function generateLexicalRichText() {
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
format: '',
|
||||
type: 'block',
|
||||
version: 1,
|
||||
version: 2,
|
||||
fields: {
|
||||
data: {
|
||||
id: '65298b49db4ef8c744a7faae',
|
||||
upload: '{{UPLOAD_DOC_ID}}',
|
||||
blockName: 'Block Node, With Upload Field',
|
||||
blockType: 'uploadAndRichText',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
children: [],
|
||||
direction: null,
|
||||
@@ -214,6 +205,28 @@ export function generateLexicalRichText() {
|
||||
type: 'paragraph',
|
||||
version: 1,
|
||||
},
|
||||
{
|
||||
format: '',
|
||||
type: 'block',
|
||||
version: 2,
|
||||
fields: {
|
||||
id: '65532e49fe515eb112e605a3',
|
||||
blockName: 'Radio Buttons 1',
|
||||
blockType: 'radioButtons',
|
||||
radioButtons: 'option1',
|
||||
},
|
||||
},
|
||||
{
|
||||
format: '',
|
||||
type: 'block',
|
||||
version: 2,
|
||||
fields: {
|
||||
id: '65532e50fe515eb112e605a4',
|
||||
blockName: 'Radio Buttons 2',
|
||||
blockType: 'radioButtons',
|
||||
radioButtons: 'option1',
|
||||
},
|
||||
},
|
||||
{
|
||||
children: [],
|
||||
direction: null,
|
||||
|
||||
@@ -2,16 +2,14 @@ import type { CollectionConfig } from '../../../../packages/payload/src/collecti
|
||||
|
||||
import {
|
||||
BlocksFeature,
|
||||
HTMLConverterFeature,
|
||||
LexicalPluginToLexicalFeature,
|
||||
LinkFeature,
|
||||
TestRecorderFeature,
|
||||
TreeviewFeature,
|
||||
UploadFeature,
|
||||
lexicalEditor,
|
||||
} from '../../../../packages/richtext-lexical/src'
|
||||
import { lexicalFieldsSlug } from '../../slugs'
|
||||
import {
|
||||
RadioButtonsBlock,
|
||||
RelationshipBlock,
|
||||
RichTextBlock,
|
||||
SelectFieldBlock,
|
||||
@@ -51,6 +49,7 @@ export const LexicalFields: CollectionConfig = {
|
||||
SelectFieldBlock,
|
||||
RelationshipBlock,
|
||||
SubBlockBlock,
|
||||
RadioButtonsBlock,
|
||||
],
|
||||
}),
|
||||
],
|
||||
@@ -102,6 +101,7 @@ export const LexicalFields: CollectionConfig = {
|
||||
SelectFieldBlock,
|
||||
RelationshipBlock,
|
||||
SubBlockBlock,
|
||||
RadioButtonsBlock,
|
||||
],
|
||||
}),
|
||||
],
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
|
||||
import React from 'react'
|
||||
|
||||
const AfterInputImpl: React.FC = () => {
|
||||
export const AfterInput: React.FC = () => {
|
||||
return <label className="after-input">#after-input</label>
|
||||
}
|
||||
|
||||
export const AfterInput = <AfterInputImpl />
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
|
||||
import React from 'react'
|
||||
|
||||
const BeforeInputImpl: React.FC = () => {
|
||||
export const BeforeInput: React.FC = () => {
|
||||
return <label className="before-input">#before-input</label>
|
||||
}
|
||||
|
||||
export const BeforeInput = <BeforeInputImpl />
|
||||
|
||||
@@ -104,8 +104,8 @@ const TextFields: CollectionConfig = {
|
||||
name: 'beforeAndAfterInput',
|
||||
admin: {
|
||||
components: {
|
||||
AfterInput: [AfterInput],
|
||||
BeforeInput: [BeforeInput],
|
||||
afterInput: [AfterInput],
|
||||
beforeInput: [BeforeInput],
|
||||
},
|
||||
},
|
||||
type: 'text',
|
||||
|
||||
@@ -6,7 +6,12 @@ import path from 'path'
|
||||
import payload from '../../packages/payload/src'
|
||||
import { mapAsync } from '../../packages/payload/src/utilities/mapAsync'
|
||||
import wait from '../../packages/payload/src/utilities/wait'
|
||||
import { exactText, saveDocAndAssert, saveDocHotkeyAndAssert } from '../helpers'
|
||||
import {
|
||||
exactText,
|
||||
initPageConsoleErrorCatch,
|
||||
saveDocAndAssert,
|
||||
saveDocHotkeyAndAssert,
|
||||
} from '../helpers'
|
||||
import { AdminUrlUtil } from '../helpers/adminUrlUtil'
|
||||
import { initPayloadE2E } from '../helpers/configHelpers'
|
||||
import { RESTClient } from '../helpers/rest'
|
||||
@@ -38,6 +43,7 @@ describe('fields', () => {
|
||||
|
||||
const context = await browser.newContext()
|
||||
page = await context.newPage()
|
||||
initPageConsoleErrorCatch(page)
|
||||
})
|
||||
beforeEach(async () => {
|
||||
await clearAndSeedEverything(payload)
|
||||
@@ -97,7 +103,7 @@ describe('fields', () => {
|
||||
await expect(error).toHaveText('#custom-error')
|
||||
})
|
||||
|
||||
test('should render BeforeInput and AfterInput', async () => {
|
||||
test('should render beforeInput and afterInput', async () => {
|
||||
await page.goto(url.create)
|
||||
const input = page.locator('input[id="field-beforeAndAfterInput"]')
|
||||
|
||||
@@ -105,13 +111,13 @@ describe('fields', () => {
|
||||
return el.previousElementSibling
|
||||
})
|
||||
const prevSiblingText = await page.evaluate((el) => el.textContent, prevSibling)
|
||||
await expect(prevSiblingText).toEqual('#before-input')
|
||||
expect(prevSiblingText).toEqual('#before-input')
|
||||
|
||||
const nextSibling = await input.evaluateHandle((el) => {
|
||||
return el.nextElementSibling
|
||||
})
|
||||
const nextSiblingText = await page.evaluate((el) => el.textContent, nextSibling)
|
||||
await expect(nextSiblingText).toEqual('#after-input')
|
||||
expect(nextSiblingText).toEqual('#after-input')
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import type { SerializedBlockNode } from '../../packages/richtext-lexical/src'
|
||||
import type { LexicalField } from './payload-types'
|
||||
|
||||
import payload from '../../packages/payload/src'
|
||||
import { saveDocAndAssert } from '../helpers'
|
||||
import { initPageConsoleErrorCatch, saveDocAndAssert } from '../helpers'
|
||||
import { AdminUrlUtil } from '../helpers/adminUrlUtil'
|
||||
import { initPayloadE2E } from '../helpers/configHelpers'
|
||||
import { RESTClient } from '../helpers/rest'
|
||||
@@ -42,6 +42,8 @@ describe('lexical', () => {
|
||||
|
||||
const context = await browser.newContext()
|
||||
page = await context.newPage()
|
||||
|
||||
initPageConsoleErrorCatch(page)
|
||||
})
|
||||
beforeEach(async () => {
|
||||
await clearAndSeedEverything(payload)
|
||||
@@ -224,8 +226,7 @@ describe('lexical', () => {
|
||||
|
||||
const lexicalField: SerializedEditorState = lexicalDoc.lexicalWithBlocks
|
||||
const blockNode: SerializedBlockNode = lexicalField.root.children[3] as SerializedBlockNode
|
||||
const textNodeInBlockNodeRichText =
|
||||
blockNode.fields.data.richText.root.children[1].children[0]
|
||||
const textNodeInBlockNodeRichText = blockNode.fields.richText.root.children[1].children[0]
|
||||
|
||||
expect(textNodeInBlockNodeRichText.text).toBe(
|
||||
'Some text below relationship node 1 inserted text',
|
||||
@@ -298,7 +299,7 @@ describe('lexical', () => {
|
||||
|
||||
const lexicalField: SerializedEditorState = lexicalDoc.lexicalWithBlocks
|
||||
const blockNode: SerializedBlockNode = lexicalField.root.children[3] as SerializedBlockNode
|
||||
const paragraphNodeInBlockNodeRichText = blockNode.fields.data.richText.root.children[1]
|
||||
const paragraphNodeInBlockNodeRichText = blockNode.fields.richText.root.children[1]
|
||||
|
||||
expect(paragraphNodeInBlockNodeRichText.children).toHaveLength(2)
|
||||
|
||||
@@ -403,7 +404,7 @@ describe('lexical', () => {
|
||||
* Check if it was created successfully and
|
||||
* fill newly created textarea sub-block with text
|
||||
*/
|
||||
const newSubBlock = lexicalBlock.locator('#subBlocks-row-1')
|
||||
const newSubBlock = lexicalBlock.locator('.blocks-field__rows > div').nth(1)
|
||||
await expect(newSubBlock).toBeVisible()
|
||||
|
||||
const newContentTextArea = newSubBlock.locator('textarea').first()
|
||||
@@ -437,7 +438,7 @@ describe('lexical', () => {
|
||||
|
||||
const lexicalField: SerializedEditorState = lexicalDoc.lexicalWithBlocks
|
||||
const blockNode: SerializedBlockNode = lexicalField.root.children[4] as SerializedBlockNode
|
||||
const subBlocks = blockNode.fields.data.subBlocks
|
||||
const subBlocks = blockNode.fields.subBlocks
|
||||
|
||||
expect(subBlocks).toHaveLength(2)
|
||||
|
||||
@@ -446,6 +447,69 @@ describe('lexical', () => {
|
||||
expect(createdTextAreaBlock.content).toBe('text123')
|
||||
})
|
||||
|
||||
test('should allow changing values of two different radio button blocks independently', async () => {
|
||||
// This test ensures that https://github.com/payloadcms/payload/issues/3911 does not happen again
|
||||
|
||||
await navigateToLexicalFields()
|
||||
const richTextField = page.locator('.rich-text-lexical').nth(1) // second
|
||||
await richTextField.scrollIntoViewIfNeeded()
|
||||
await expect(richTextField).toBeVisible()
|
||||
|
||||
const radioButtonBlock1 = richTextField.locator('.lexical-block').nth(4)
|
||||
|
||||
const radioButtonBlock2 = richTextField.locator('.lexical-block').nth(5)
|
||||
await radioButtonBlock2.scrollIntoViewIfNeeded()
|
||||
await expect(radioButtonBlock1).toBeVisible()
|
||||
await expect(radioButtonBlock2).toBeVisible()
|
||||
|
||||
// Click radio button option2 of radioButtonBlock1
|
||||
await radioButtonBlock1
|
||||
.locator('.radio-input:has-text("Option 2")')
|
||||
.first() // This already is an input for some reason
|
||||
.click()
|
||||
|
||||
// Ensure radio button option1 of radioButtonBlock2 (the default option) is still selected
|
||||
await expect(
|
||||
radioButtonBlock2.locator('.radio-input:has-text("Option 1")').first(),
|
||||
).toBeChecked()
|
||||
|
||||
// Click radio button option3 of radioButtonBlock2
|
||||
await radioButtonBlock2
|
||||
.locator('.radio-input:has-text("Option 3")')
|
||||
.first() // This already is an input for some reason
|
||||
.click()
|
||||
|
||||
// Ensure previously clicked option2 of radioButtonBlock1 is still selected
|
||||
await expect(
|
||||
radioButtonBlock1.locator('.radio-input:has-text("Option 2")').first(),
|
||||
).toBeChecked()
|
||||
|
||||
/**
|
||||
* Now save and check the actual data. radio button block 1 should have option2 selected and radio button block 2 should have option3 selected
|
||||
*/
|
||||
|
||||
await saveDocAndAssert(page)
|
||||
|
||||
const lexicalDoc: RichTextField = (
|
||||
await payload.find({
|
||||
collection: lexicalFieldsSlug,
|
||||
where: {
|
||||
title: {
|
||||
equals: lexicalDocData.title,
|
||||
},
|
||||
},
|
||||
depth: 0,
|
||||
})
|
||||
).docs[0] as never
|
||||
|
||||
const lexicalField: SerializedEditorState = lexicalDoc.lexicalWithBlocks
|
||||
const radio1: SerializedBlockNode = lexicalField.root.children[7]
|
||||
const radio2: SerializedBlockNode = lexicalField.root.children[8]
|
||||
|
||||
expect(radio1.fields.radioButtons).toBe('option2')
|
||||
expect(radio2.fields.radioButtons).toBe('option3')
|
||||
})
|
||||
|
||||
test('should not lose focus when writing in nested editor', async () => {
|
||||
// https://github.com/payloadcms/payload/issues/4108
|
||||
// Steps:
|
||||
|
||||
@@ -304,7 +304,7 @@ describe('Lexical', () => {
|
||||
/**
|
||||
* Depth 1 population:
|
||||
*/
|
||||
expect(relationshipBlockNode.fields.data.rel).toStrictEqual(createdJPGDocID)
|
||||
expect(relationshipBlockNode.fields.rel).toStrictEqual(createdJPGDocID)
|
||||
})
|
||||
|
||||
it('should populate relationships in blocks with depth=1', async () => {
|
||||
@@ -328,7 +328,7 @@ describe('Lexical', () => {
|
||||
/**
|
||||
* Depth 1 population:
|
||||
*/
|
||||
expect(relationshipBlockNode.fields.data.rel.filename).toStrictEqual('payload.jpg')
|
||||
expect(relationshipBlockNode.fields.rel.filename).toStrictEqual('payload.jpg')
|
||||
})
|
||||
|
||||
it('should not populate relationship nodes inside of a sub-editor from a blocks node with 0 depth', async () => {
|
||||
@@ -349,7 +349,7 @@ describe('Lexical', () => {
|
||||
const subEditorBlockNode: SerializedBlockNode = lexicalField.root
|
||||
.children[3] as SerializedBlockNode
|
||||
|
||||
const subEditor: SerializedEditorState = subEditorBlockNode.fields.data.richText
|
||||
const subEditor: SerializedEditorState = subEditorBlockNode.fields.richText
|
||||
|
||||
const subEditorRelationshipNode: SerializedRelationshipNode = subEditor.root
|
||||
.children[0] as SerializedRelationshipNode
|
||||
@@ -380,7 +380,7 @@ describe('Lexical', () => {
|
||||
const subEditorBlockNode: SerializedBlockNode = lexicalField.root
|
||||
.children[3] as SerializedBlockNode
|
||||
|
||||
const subEditor: SerializedEditorState = subEditorBlockNode.fields.data.richText
|
||||
const subEditor: SerializedEditorState = subEditorBlockNode.fields.richText
|
||||
|
||||
const subEditorRelationshipNode: SerializedRelationshipNode = subEditor.root
|
||||
.children[0] as SerializedRelationshipNode
|
||||
@@ -427,7 +427,7 @@ describe('Lexical', () => {
|
||||
const subEditorBlockNode: SerializedBlockNode = lexicalField.root
|
||||
.children[3] as SerializedBlockNode
|
||||
|
||||
const subEditor: SerializedEditorState = subEditorBlockNode.fields.data.richText
|
||||
const subEditor: SerializedEditorState = subEditorBlockNode.fields.richText
|
||||
|
||||
const subEditorRelationshipNode: SerializedRelationshipNode = subEditor.root
|
||||
.children[0] as SerializedRelationshipNode
|
||||
|
||||
@@ -116,3 +116,20 @@ export const findTableRow = async (page: Page, title: string): Promise<Locator>
|
||||
expect(row).toBeTruthy()
|
||||
return row
|
||||
}
|
||||
|
||||
/**
|
||||
* Throws an error when browser console error messages (with some exceptions) are thrown, thus resulting
|
||||
* in the e2e test failing.
|
||||
*
|
||||
* Useful to prevent the e2e test from passing when, for example, there are react missing key prop errors
|
||||
* @param page
|
||||
*/
|
||||
export function initPageConsoleErrorCatch(page: Page) {
|
||||
page.on('console', (msg) => {
|
||||
if (msg.type() === 'error' && !msg.text().includes('the server responded with a status of')) {
|
||||
// the the server responded with a status of error happens frequently. Will ignore it for now.
|
||||
// Most importantly, this should catch react errors.
|
||||
throw new Error(`Browser console error: ${msg.text()}`)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { Page } from '@playwright/test'
|
||||
|
||||
import { expect, test } from '@playwright/test'
|
||||
|
||||
import { exactText, saveDocAndAssert } from '../helpers'
|
||||
import { exactText, initPageConsoleErrorCatch, saveDocAndAssert } from '../helpers'
|
||||
import { AdminUrlUtil } from '../helpers/adminUrlUtil'
|
||||
import { initPayloadE2E } from '../helpers/configHelpers'
|
||||
import { mobileBreakpoint } from './shared'
|
||||
@@ -43,6 +43,8 @@ describe('Live Preview', () => {
|
||||
await startLivePreviewDemo({
|
||||
payload,
|
||||
})
|
||||
|
||||
initPageConsoleErrorCatch(page)
|
||||
})
|
||||
|
||||
test('collection - has tab', async () => {
|
||||
@@ -162,13 +164,16 @@ describe('Live Preview', () => {
|
||||
|
||||
// Check that the breakpoint select is present
|
||||
const breakpointSelector = page.locator(
|
||||
'.live-preview-toolbar-controls__breakpoint button.popup-button'
|
||||
'.live-preview-toolbar-controls__breakpoint button.popup-button',
|
||||
)
|
||||
expect(breakpointSelector).toBeTruthy()
|
||||
|
||||
// Select the mobile breakpoint
|
||||
await breakpointSelector.first().click()
|
||||
await page.locator(`.live-preview-toolbar-controls__breakpoint button.popup-button-list__button`).filter({ hasText: mobileBreakpoint.label }).click()
|
||||
await page
|
||||
.locator(`.live-preview-toolbar-controls__breakpoint button.popup-button-list__button`)
|
||||
.filter({ hasText: mobileBreakpoint.label })
|
||||
.click()
|
||||
|
||||
// Make sure the value has been set
|
||||
expect(breakpointSelector).toContainText(mobileBreakpoint.label)
|
||||
|
||||
@@ -6,7 +6,12 @@ import type { LocalizedPost } from './payload-types'
|
||||
|
||||
import payload from '../../packages/payload/src'
|
||||
import wait from '../../packages/payload/src/utilities/wait'
|
||||
import { changeLocale, openDocControls, saveDocAndAssert } from '../helpers'
|
||||
import {
|
||||
changeLocale,
|
||||
initPageConsoleErrorCatch,
|
||||
openDocControls,
|
||||
saveDocAndAssert,
|
||||
} from '../helpers'
|
||||
import { AdminUrlUtil } from '../helpers/adminUrlUtil'
|
||||
import { initPayloadTest } from '../helpers/configHelpers'
|
||||
import { englishTitle, localizedPostsSlug, spanishLocale } from './shared'
|
||||
@@ -44,6 +49,8 @@ describe('Localization', () => {
|
||||
|
||||
const context = await browser.newContext()
|
||||
page = await context.newPage()
|
||||
|
||||
initPageConsoleErrorCatch(page)
|
||||
})
|
||||
|
||||
describe('localized text', () => {
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { Page } from '@playwright/test'
|
||||
|
||||
import { expect, test } from '@playwright/test'
|
||||
|
||||
import { closeNav, openNav } from '../helpers'
|
||||
import { closeNav, initPageConsoleErrorCatch, openNav } from '../helpers'
|
||||
import { initPayloadE2E } from '../helpers/configHelpers'
|
||||
|
||||
const { beforeAll, describe } = test
|
||||
@@ -15,6 +15,8 @@ describe('refresh-permissions', () => {
|
||||
;({ serverURL } = await initPayloadE2E(__dirname))
|
||||
const context = await browser.newContext()
|
||||
page = await context.newPage()
|
||||
|
||||
initPageConsoleErrorCatch(page)
|
||||
})
|
||||
|
||||
test('should show test global immediately after allowing access', async () => {
|
||||
|
||||
@@ -7,7 +7,7 @@ import type { Media } from './payload-types'
|
||||
|
||||
import payload from '../../packages/payload/src'
|
||||
import wait from '../../packages/payload/src/utilities/wait'
|
||||
import { saveDocAndAssert } from '../helpers'
|
||||
import { initPageConsoleErrorCatch, saveDocAndAssert } from '../helpers'
|
||||
import { AdminUrlUtil } from '../helpers/adminUrlUtil'
|
||||
import { initPayloadE2E } from '../helpers/configHelpers'
|
||||
import { RESTClient } from '../helpers/rest'
|
||||
@@ -40,6 +40,8 @@ describe('uploads', () => {
|
||||
const context = await browser.newContext()
|
||||
page = await context.newPage()
|
||||
|
||||
initPageConsoleErrorCatch(page)
|
||||
|
||||
const findPNG = await payload.find({
|
||||
collection: mediaSlug,
|
||||
depth: 0,
|
||||
|
||||
@@ -30,7 +30,13 @@ import { expect, test } from '@playwright/test'
|
||||
import payload from '../../packages/payload/src'
|
||||
import wait from '../../packages/payload/src/utilities/wait'
|
||||
import { globalSlug } from '../admin/slugs'
|
||||
import { changeLocale, exactText, findTableCell, selectTableRow } from '../helpers'
|
||||
import {
|
||||
changeLocale,
|
||||
exactText,
|
||||
findTableCell,
|
||||
initPageConsoleErrorCatch,
|
||||
selectTableRow,
|
||||
} from '../helpers'
|
||||
import { AdminUrlUtil } from '../helpers/adminUrlUtil'
|
||||
import { initPayloadE2E } from '../helpers/configHelpers'
|
||||
import { clearAndSeedEverything } from './seed'
|
||||
@@ -55,6 +61,8 @@ describe('versions', () => {
|
||||
serverURL = config.serverURL
|
||||
const context = await browser.newContext()
|
||||
page = await context.newPage()
|
||||
|
||||
initPageConsoleErrorCatch(page)
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
@@ -240,6 +248,7 @@ describe('versions', () => {
|
||||
expect(page.url()).toMatch(/\/versions$/)
|
||||
})
|
||||
|
||||
// TODO: This test is flaky and fails sometimes
|
||||
test('global - should autosave', async () => {
|
||||
const url = new AdminUrlUtil(serverURL, autoSaveGlobalSlug)
|
||||
// fill out global title and wait for autosave
|
||||
|
||||
Reference in New Issue
Block a user