diff --git a/package.json b/package.json index fda52a319..d1543f822 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,7 @@ "rimraf": "3.0.2", "shelljs": "0.8.5", "ts-node": "10.9.1", - "turbo": "^1.10.13", + "turbo": "^1.10.15", "typescript": "5.2.2", "uuid": "^9.0.0" }, diff --git a/packages/payload/src/admin/components/elements/DocumentDrawer/DrawerContent.tsx b/packages/payload/src/admin/components/elements/DocumentDrawer/DrawerContent.tsx index 19d18093f..87e4991dd 100644 --- a/packages/payload/src/admin/components/elements/DocumentDrawer/DrawerContent.tsx +++ b/packages/payload/src/admin/components/elements/DocumentDrawer/DrawerContent.tsx @@ -41,6 +41,7 @@ const Content: React.FC = ({ const hasInitializedState = useRef(false) const [isOpen, setIsOpen] = useState(false) const [collectionConfig] = useRelatedCollections(collectionSlug) + const config = useConfig() const { admin: { components: { views: { Edit } = {} } = {} } = {} } = collectionConfig @@ -82,6 +83,7 @@ const Content: React.FC = ({ const preferences = await getDocPreferences() const state = await buildStateFromSchema({ id, + config, data, fieldSchema: fields, locale, diff --git a/packages/payload/src/admin/components/forms/Form/buildStateFromSchema/addFieldStatePromise.ts b/packages/payload/src/admin/components/forms/Form/buildStateFromSchema/addFieldStatePromise.ts index 054985295..3615de734 100644 --- a/packages/payload/src/admin/components/forms/Form/buildStateFromSchema/addFieldStatePromise.ts +++ b/packages/payload/src/admin/components/forms/Form/buildStateFromSchema/addFieldStatePromise.ts @@ -4,6 +4,7 @@ import type { TFunction } from 'i18next' import ObjectID from 'bson-objectid' import type { User } from '../../../../../auth' +import type { SanitizedConfig } from '../../../../../config/types' import type { NonPresentationalField } from '../../../../../fields/config/types' import type { Data, Fields, FormField } from '../types' @@ -12,6 +13,7 @@ import getValueWithDefault from '../../../../../fields/getDefaultValue' import { iterateFields } from './iterateFields' type Args = { + config: SanitizedConfig data: Data field: NonPresentationalField fullData: Data @@ -30,6 +32,7 @@ type Args = { export const addFieldStatePromise = async ({ id, + config, data, field, fullData, @@ -68,6 +71,7 @@ export const addFieldStatePromise = async ({ validationResult = await fieldState.validate(data?.[field.name], { ...field, id, + config, data: fullData, operation, siblingData: data, @@ -100,6 +104,7 @@ export const addFieldStatePromise = async ({ acc.promises.push( iterateFields({ id, + config, data: row, fields: field.fields, fullData, @@ -188,6 +193,7 @@ export const addFieldStatePromise = async ({ acc.promises.push( iterateFields({ id, + config, data: row, fields: block.fields, fullData, @@ -249,6 +255,7 @@ export const addFieldStatePromise = async ({ case 'group': { await iterateFields({ id, + config, data: data?.[field.name] || {}, fields: field.fields, fullData, @@ -348,6 +355,7 @@ export const addFieldStatePromise = async ({ // Handle field types that do not use names (row, etc) await iterateFields({ id, + config, data, fields: field.fields, fullData, @@ -364,6 +372,7 @@ export const addFieldStatePromise = async ({ const promises = field.tabs.map((tab) => iterateFields({ id, + config, data: tabHasName(tab) ? data?.[tab.name] : data, fields: tab.fields, fullData, diff --git a/packages/payload/src/admin/components/forms/Form/buildStateFromSchema/index.ts b/packages/payload/src/admin/components/forms/Form/buildStateFromSchema/index.ts index c8735a0ef..5090e9103 100644 --- a/packages/payload/src/admin/components/forms/Form/buildStateFromSchema/index.ts +++ b/packages/payload/src/admin/components/forms/Form/buildStateFromSchema/index.ts @@ -1,12 +1,14 @@ import type { TFunction } from 'i18next' import type { User } from '../../../../../auth' +import type { SanitizedConfig } from '../../../../../config/types' import type { Field as FieldSchema } from '../../../../../fields/config/types' import type { Data, Fields } from '../types' import { iterateFields } from './iterateFields' type Args = { + config: SanitizedConfig data?: Data fieldSchema: FieldSchema[] id?: number | string @@ -21,13 +23,24 @@ type Args = { } const buildStateFromSchema = async (args: Args): Promise => { - const { id, data: fullData = {}, fieldSchema, locale, operation, preferences, t, user } = args + const { + id, + config, + data: fullData = {}, + fieldSchema, + locale, + operation, + preferences, + t, + user, + } = args if (fieldSchema) { const state: Fields = {} await iterateFields({ id, + config, data: fullData, fields: fieldSchema, fullData, diff --git a/packages/payload/src/admin/components/forms/Form/buildStateFromSchema/iterateFields.ts b/packages/payload/src/admin/components/forms/Form/buildStateFromSchema/iterateFields.ts index 0be15233b..2f0d20d53 100644 --- a/packages/payload/src/admin/components/forms/Form/buildStateFromSchema/iterateFields.ts +++ b/packages/payload/src/admin/components/forms/Form/buildStateFromSchema/iterateFields.ts @@ -1,6 +1,7 @@ import type { TFunction } from 'i18next' import type { User } from '../../../../../auth' +import type { SanitizedConfig } from '../../../../../config/types' import type { Field as FieldSchema } from '../../../../../fields/config/types' import type { Data, Fields } from '../types' @@ -8,6 +9,7 @@ import { fieldIsPresentationalOnly } from '../../../../../fields/config/types' import { addFieldStatePromise } from './addFieldStatePromise' type Args = { + config: SanitizedConfig data: Data fields: FieldSchema[] fullData: Data @@ -26,6 +28,7 @@ type Args = { export const iterateFields = async ({ id, + config, data, fields, fullData, @@ -51,6 +54,7 @@ export const iterateFields = async ({ promises.push( addFieldStatePromise({ id, + config, data, field, fullData, diff --git a/packages/payload/src/admin/components/forms/Form/index.tsx b/packages/payload/src/admin/components/forms/Form/index.tsx index bb47c873b..4a6ef0981 100644 --- a/packages/payload/src/admin/components/forms/Form/index.tsx +++ b/packages/payload/src/admin/components/forms/Form/index.tsx @@ -24,6 +24,7 @@ import wait from '../../../../utilities/wait' import { requests } from '../../../api' import useThrottledEffect from '../../../hooks/useThrottledEffect' import { useAuth } from '../../utilities/Auth' +import { useConfig } from '../../utilities/Config' import { useDocumentInfo } from '../../utilities/DocumentInfo' import { useLocale } from '../../utilities/Locale' import { useOperation } from '../../utilities/OperationProvider' @@ -62,6 +63,7 @@ const Form: React.FC = (props) => { onSubmit, onSuccess, redirect, + submitted: submittedFromProps, waitForAutocomplete, } = props @@ -72,6 +74,8 @@ const Form: React.FC = (props) => { const { id, collection, getDocPreferences, global } = useDocumentInfo() const operation = useOperation() + const config = useConfig() + const [modified, setModified] = useState(false) const [processing, setProcessing] = useState(false) const [submitted, setSubmitted] = useState(false) @@ -165,6 +169,7 @@ const Form: React.FC = (props) => { if (typeof field.validate === 'function') { validationResult = await field.validate(field.value, { id, + config, data, operation, siblingData: contextRef.current.getSiblingData(path), @@ -191,7 +196,7 @@ const Form: React.FC = (props) => { } return isValid - }, [contextRef, id, user, operation, t, dispatchFields]) + }, [contextRef, id, user, operation, t, dispatchFields, config]) const submit = useCallback( async (options: SubmitOptions = {}, e): Promise => { @@ -452,6 +457,7 @@ const Form: React.FC = (props) => { if (fieldConfig) { const subFieldState = await buildStateFromSchema({ id, + config, data, fieldSchema: fieldConfig, locale, @@ -469,7 +475,7 @@ const Form: React.FC = (props) => { }) } }, - [dispatchFields, getDocPreferences, id, user, operation, locale, t, getRowConfigByPath], + [dispatchFields, getDocPreferences, id, user, operation, locale, t, getRowConfigByPath, config], ) const removeFieldRow: Context['removeFieldRow'] = useCallback( @@ -490,6 +496,7 @@ const Form: React.FC = (props) => { if (fieldConfig) { const subFieldState = await buildStateFromSchema({ id, + config, data, fieldSchema: fieldConfig, locale, @@ -507,7 +514,7 @@ const Form: React.FC = (props) => { }) } }, - [dispatchFields, getDocPreferences, id, user, operation, locale, t, getRowConfigByPath], + [dispatchFields, getDocPreferences, id, user, operation, locale, t, getRowConfigByPath, config], ) const getFields = useCallback(() => contextRef.current.fields, [contextRef]) @@ -557,6 +564,7 @@ const Form: React.FC = (props) => { const preferences = await getDocPreferences() const state = await buildStateFromSchema({ id, + config, data, fieldSchema, locale, @@ -569,7 +577,7 @@ const Form: React.FC = (props) => { setModified(false) dispatchFields({ state, type: 'REPLACE_STATE' }) }, - [id, user, operation, locale, t, dispatchFields, getDocPreferences], + [id, user, operation, locale, t, dispatchFields, getDocPreferences, config], ) const replaceState = useCallback( @@ -601,6 +609,10 @@ const Form: React.FC = (props) => { contextRef.current.removeFieldRow = removeFieldRow contextRef.current.replaceFieldRow = replaceFieldRow + useEffect(() => { + if (typeof submittedFromProps === 'boolean') setSubmitted(submittedFromProps) + }, [submittedFromProps]) + useEffect(() => { if (initialState) { contextRef.current = { ...initContextState } as FormContextType diff --git a/packages/payload/src/admin/components/forms/Form/types.ts b/packages/payload/src/admin/components/forms/Form/types.ts index 8ce524e4c..c9e2668cc 100644 --- a/packages/payload/src/admin/components/forms/Form/types.ts +++ b/packages/payload/src/admin/components/forms/Form/types.ts @@ -49,6 +49,7 @@ export type Props = { onSubmit?: (fields: Fields, data: Data) => void onSuccess?: (json: unknown) => void redirect?: string + submitted?: boolean validationOperation?: 'create' | 'update' waitForAutocomplete?: boolean } diff --git a/packages/payload/src/admin/components/forms/field-types/RichText/index.tsx b/packages/payload/src/admin/components/forms/field-types/RichText/index.tsx index 4400bb931..ae2ab1663 100644 --- a/packages/payload/src/admin/components/forms/field-types/RichText/index.tsx +++ b/packages/payload/src/admin/components/forms/field-types/RichText/index.tsx @@ -3,12 +3,9 @@ import React from 'react' import type { RichTextField } from '../../../../../fields/config/types' import type { RichTextAdapter } from './types' -import { useConfig } from '../../../utilities/Config' - const RichText: React.FC = (props) => { - const config = useConfig() // eslint-disable-next-line react/destructuring-assignment - const editor: RichTextAdapter = props.editor || config.editor + const editor: RichTextAdapter = props.editor return } diff --git a/packages/payload/src/admin/components/forms/field-types/RichText/types.ts b/packages/payload/src/admin/components/forms/field-types/RichText/types.ts index 91e22cfee..a959304bc 100644 --- a/packages/payload/src/admin/components/forms/field-types/RichText/types.ts +++ b/packages/payload/src/admin/components/forms/field-types/RichText/types.ts @@ -1,5 +1,5 @@ import type { PayloadRequest } from '../../../../../express/types' -import type { RichTextField } from '../../../../../fields/config/types' +import type { RichTextField, Validate } from '../../../../../fields/config/types' import type { CellComponentProps } from '../../../views/collections/List/Cell/types' export type RichTextFieldProps = Omit< @@ -21,4 +21,5 @@ export type RichTextAdapter = { showHiddenFields: boolean siblingDoc: Record }) => Promise | null + validate: Validate> } diff --git a/packages/payload/src/admin/components/forms/useField/index.tsx b/packages/payload/src/admin/components/forms/useField/index.tsx index 1b284682f..7955e9e45 100644 --- a/packages/payload/src/admin/components/forms/useField/index.tsx +++ b/packages/payload/src/admin/components/forms/useField/index.tsx @@ -6,6 +6,7 @@ import type { FieldType, Options } from './types' import useThrottledEffect from '../../../hooks/useThrottledEffect' import { useAuth } from '../../utilities/Auth' +import { useConfig } from '../../utilities/Config' import { useDocumentInfo } from '../../utilities/DocumentInfo' import { useOperation } from '../../utilities/OperationProvider' import { useForm, useFormFields, useFormProcessing, useFormSubmitted } from '../Form/context' @@ -26,6 +27,7 @@ const useField = (options: Options): FieldType => { const field = useFormFields(([fields]) => fields[path]) const { t } = useTranslation() const dispatchField = useFormFields(([_, dispatch]) => dispatch) + const config = useConfig() const { getData, getSiblingData, setModified } = useForm() @@ -106,6 +108,7 @@ const useField = (options: Options): FieldType => { const validateOptions = { id, + config, data: getData(), operation, siblingData: getSiblingData(path), diff --git a/packages/payload/src/admin/components/views/Account/index.tsx b/packages/payload/src/admin/components/views/Account/index.tsx index 633aedbef..62b4a9ca8 100644 --- a/packages/payload/src/admin/components/views/Account/index.tsx +++ b/packages/payload/src/admin/components/views/Account/index.tsx @@ -25,6 +25,7 @@ const AccountView: React.FC = () => { const { id, docPermissions, getDocPermissions, getDocPreferences, preferencesKey, slug } = useDocumentInfo() const { getPreference } = usePreferences() + const config = useConfig() const { admin: { @@ -65,6 +66,7 @@ const AccountView: React.FC = () => { const preferences = await getDocPreferences() const state = await buildStateFromSchema({ id, + config, data: json.doc, fieldSchema: collection.fields, locale, @@ -94,6 +96,7 @@ const AccountView: React.FC = () => { const state = await buildStateFromSchema({ id, + config, data: dataToRender, fieldSchema: fields, locale, diff --git a/packages/payload/src/admin/components/views/Global/index.tsx b/packages/payload/src/admin/components/views/Global/index.tsx index 1e0a530f8..c98225f2b 100644 --- a/packages/payload/src/admin/components/views/Global/index.tsx +++ b/packages/payload/src/admin/components/views/Global/index.tsx @@ -29,6 +29,7 @@ const GlobalView: React.FC = (props) => { useDocumentInfo() const { getPreference } = usePreferences() const { t } = useTranslation() + const config = useConfig() const { routes: { api }, @@ -44,6 +45,7 @@ const GlobalView: React.FC = (props) => { setUpdatedAt(json?.result?.updatedAt) const preferences = await getDocPreferences() const state = await buildStateFromSchema({ + config, data: json.result, fieldSchema: fields, locale, @@ -68,6 +70,7 @@ const GlobalView: React.FC = (props) => { const awaitInitialState = async () => { const preferences = await getDocPreferences() const state = await buildStateFromSchema({ + config, data: dataToRender, fieldSchema: fields, locale, diff --git a/packages/payload/src/admin/components/views/collections/Edit/index.tsx b/packages/payload/src/admin/components/views/collections/Edit/index.tsx index ed8ddb08b..de0c6ade2 100644 --- a/packages/payload/src/admin/components/views/collections/Edit/index.tsx +++ b/packages/payload/src/admin/components/views/collections/Edit/index.tsx @@ -31,10 +31,11 @@ const EditView: React.FC = (props) => { const { code: locale } = useLocale() + const config = useConfig() const { routes: { admin, api }, serverURL, - } = useConfig() + } = config const { params: { id } = {} } = useRouteMatch>() const history = useHistory() @@ -56,6 +57,7 @@ const EditView: React.FC = (props) => { const state = await buildStateFromSchema({ id, + config, data: doc || {}, fieldSchema: overrides.fieldSchema, locale, diff --git a/packages/payload/src/admin/components/views/collections/List/Cell/field-types/Richtext/index.tsx b/packages/payload/src/admin/components/views/collections/List/Cell/field-types/Richtext/index.tsx index 8b0b218db..d2d7e47bb 100644 --- a/packages/payload/src/admin/components/views/collections/List/Cell/field-types/Richtext/index.tsx +++ b/packages/payload/src/admin/components/views/collections/List/Cell/field-types/Richtext/index.tsx @@ -4,12 +4,9 @@ import type { RichTextField } from '../../../../../../../../fields/config/types' import type { RichTextAdapter } from '../../../../../../forms/field-types/RichText/types' import type { CellComponentProps } from '../../types' -import { useConfig } from '../../../../../../utilities/Config' - const RichTextCell: React.FC> = (props) => { - const config = useConfig() // eslint-disable-next-line react/destructuring-assignment - const editor: RichTextAdapter = props.field.editor || config.editor + const editor: RichTextAdapter = props.field.editor return } diff --git a/packages/payload/src/config/schema.ts b/packages/payload/src/config/schema.ts index 2d596d442..637ad2d1e 100644 --- a/packages/payload/src/config/schema.ts +++ b/packages/payload/src/config/schema.ts @@ -86,6 +86,7 @@ export default joi.object({ CellComponent: component.required(), FieldComponent: component.required(), afterReadPromise: joi.func().required(), + validate: joi.func().required(), }), email: joi.object(), endpoints: endpointsSchema, diff --git a/packages/payload/src/fields/config/sanitize.ts b/packages/payload/src/fields/config/sanitize.ts index 77d75def9..3b8e7fdc1 100644 --- a/packages/payload/src/fields/config/sanitize.ts +++ b/packages/payload/src/fields/config/sanitize.ts @@ -32,6 +32,11 @@ export const sanitizeFields = ({ config, fields, validRelationships }: Args): Fi throw new InvalidFieldName(field, field.name) } + // Make sure that the richText field has an editor + if (field.type === 'richText' && !field.editor && config.editor) { + field.editor = config.editor + } + // Auto-label if ( 'name' in field && diff --git a/packages/payload/src/fields/config/schema.ts b/packages/payload/src/fields/config/schema.ts index fb310c2e5..6a901140a 100644 --- a/packages/payload/src/fields/config/schema.ts +++ b/packages/payload/src/fields/config/schema.ts @@ -358,6 +358,7 @@ export const richText = baseField.keys({ CellComponent: componentSchema.required(), FieldComponent: componentSchema.required(), afterReadPromise: joi.func().required(), + validate: joi.func().required(), }), type: joi.string().valid('richText').required(), }) diff --git a/packages/payload/src/fields/config/types.ts b/packages/payload/src/fields/config/types.ts index a69bd0d6c..96da3e8fb 100644 --- a/packages/payload/src/fields/config/types.ts +++ b/packages/payload/src/fields/config/types.ts @@ -10,6 +10,7 @@ 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' import type { TypeWithID } from '../../collections/config/types' +import type { SanitizedConfig } from '../../config/types' import type { PayloadRequest, RequestContext } from '../../express/types' import type { Payload } from '../../payload' import type { Operation, Where } from '../../types' @@ -91,6 +92,7 @@ export type Labels = { } export type ValidateOptions = { + config: SanitizedConfig data: Partial id?: number | string operation?: Operation @@ -100,6 +102,7 @@ export type ValidateOptions = { user?: Partial } & TFieldConfig +// TODO: Having TFieldConfig as any breaks all type checking / auto-completions for the base ValidateOptions properties. export type Validate = ( value: TValue, options: ValidateOptions, diff --git a/packages/payload/src/fields/hooks/afterRead/promise.ts b/packages/payload/src/fields/hooks/afterRead/promise.ts index 0f90ea8c0..90def94a9 100644 --- a/packages/payload/src/fields/hooks/afterRead/promise.ts +++ b/packages/payload/src/fields/hooks/afterRead/promise.ts @@ -128,7 +128,7 @@ export const promise = async ({ } case 'richText': { - const editor: RichTextAdapter = field?.editor || req?.payload?.config?.editor + const editor: RichTextAdapter = field?.editor if (editor?.afterReadPromise) { const afterReadPromise = editor.afterReadPromise({ currentDepth, diff --git a/packages/payload/src/fields/hooks/beforeChange/promise.ts b/packages/payload/src/fields/hooks/beforeChange/promise.ts index 82e84c46f..29cadda60 100644 --- a/packages/payload/src/fields/hooks/beforeChange/promise.ts +++ b/packages/payload/src/fields/hooks/beforeChange/promise.ts @@ -106,6 +106,7 @@ export const promise = async ({ const validationResult = await field.validate(valueToValidate, { ...field, id, + config: req.payload.config, data: merge(doc, data, { arrayMerge: (_, source) => source }), jsonError, operation, diff --git a/packages/payload/src/fields/validations.ts b/packages/payload/src/fields/validations.ts index 0a2f920a7..c1facd391 100644 --- a/packages/payload/src/fields/validations.ts +++ b/packages/payload/src/fields/validations.ts @@ -1,3 +1,4 @@ +import type { RichTextAdapter } from '../exports/types' import type { ArrayField, BlockField, @@ -11,6 +12,7 @@ import type { RadioField, RelationshipField, RelationshipValue, + RichTextField, SelectField, TextField, TextareaField, @@ -212,6 +214,15 @@ export const date: Validate = (value, { required, t return true } +export const richText: Validate = async ( + value, + options, +) => { + const editor: RichTextAdapter = options?.editor + + return await editor.validate(value, options) +} + const validateFilterOptions: Validate = async ( value, { id, data, filterOptions, payload, relationTo, siblingData, t, user }, @@ -511,6 +522,7 @@ export default { point, radio, relationship, + richText, select, text, textarea, diff --git a/packages/payload/src/graphql/schema/buildObjectType.ts b/packages/payload/src/graphql/schema/buildObjectType.ts index 5d1d3f9ea..a6f8a63b6 100644 --- a/packages/payload/src/graphql/schema/buildObjectType.ts +++ b/packages/payload/src/graphql/schema/buildObjectType.ts @@ -427,7 +427,7 @@ function buildObjectType({ async resolve(parent, args, context) { let depth = payload.config.defaultDepth if (typeof args.depth !== 'undefined') depth = args.depth - const editor: RichTextAdapter = field?.editor || payload?.config?.editor + const editor: RichTextAdapter = field?.editor if (editor?.afterReadPromise) { await editor?.afterReadPromise({ diff --git a/packages/richtext-lexical/package.json b/packages/richtext-lexical/package.json index eaaa45a1a..24571b80f 100644 --- a/packages/richtext-lexical/package.json +++ b/packages/richtext-lexical/package.json @@ -35,6 +35,7 @@ "@lexical/selection": "0.12.2", "@lexical/table": "0.12.2", "@lexical/utils": "0.12.2", + "bson-objectid": "2.0.4", "classnames": "^2.3.2", "i18next": "22.5.1", "katex": "0.16.8", diff --git a/packages/richtext-lexical/src/field/Field.tsx b/packages/richtext-lexical/src/field/Field.tsx index 97dee6e64..647dcd5b9 100644 --- a/packages/richtext-lexical/src/field/Field.tsx +++ b/packages/richtext-lexical/src/field/Field.tsx @@ -6,7 +6,8 @@ import { ErrorBoundary } from 'react-error-boundary' import type { FieldProps } from '../types' -import { richTextValidate } from '../populate/validation' +import { defaultRichTextValueV2 } from '../populate/defaultValue' +import { richTextValidateHOC } from '../validate' import './index.scss' import { LexicalProvider } from './lexical/LexicalProvider' @@ -23,22 +24,21 @@ const RichText: React.FC = (props) => { style, width, }, - admin, defaultValue: defaultValueFromProps, editorConfig, label, path: pathFromProps, required, - validate = richTextValidate, + validate = richTextValidateHOC({ editorConfig }), } = props const path = pathFromProps || name const memoizedValidate = useCallback( (value, validationOptions) => { - return validate(value, { ...validationOptions, required }) + return validate(value, { ...validationOptions, props, required }) }, - [validate, required], + [validate, required, props], ) const fieldType = useField({ @@ -49,6 +49,19 @@ const RichText: React.FC = (props) => { const { errorMessage, initialValue, setValue, showError, value } = fieldType + let valueToUse = value + + if (typeof valueToUse === 'string') { + try { + const parsedJSON = JSON.parse(valueToUse) + valueToUse = parsedJSON + } catch (err) { + valueToUse = null + } + } + + if (!valueToUse) valueToUse = defaultValueFromProps || defaultRichTextValueV2 + const classes = [ baseClass, 'field-type', diff --git a/packages/richtext-lexical/src/field/features/Blocks/component/BlockContent.tsx b/packages/richtext-lexical/src/field/features/Blocks/component/BlockContent.tsx index e43c5919a..3ed4353d2 100644 --- a/packages/richtext-lexical/src/field/features/Blocks/component/BlockContent.tsx +++ b/packages/richtext-lexical/src/field/features/Blocks/component/BlockContent.tsx @@ -1,4 +1,4 @@ -import type { Block, Data } from 'payload/types' +import type { Block, Data, Fields } from 'payload/types' import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' import { $getNodeByKey } from 'lexical' @@ -6,8 +6,9 @@ import { Button, ErrorPill, Pill } from 'payload/components' import { Collapsible } from 'payload/components/elements' 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' @@ -27,10 +28,31 @@ export const BlockContent: React.FC = (props) => { const { baseClass, block, field, fields, nodeKey } = props const { i18n } = useTranslation() const [editor] = useLexicalComposerContext() - const [collapsed, setCollapsed] = React.useState(fields.collapsed) + // Used for saving collapsed to preferences (and gettin' it from there again) + // Remember, these preferences are scoped to the whole document, not just this form. This + // is important to consider for the data path used in setDocFieldPreferences + const { getDocPreferences, setDocFieldPreferences } = useDocumentInfo() + + const [collapsed, setCollapsed] = React.useState(() => { + let initialState = false + + getDocPreferences().then((currentDocPreferences) => { + const currentFieldPreferences = currentDocPreferences?.fields[field.name] + + const collapsedMap: { [key: string]: boolean } = currentFieldPreferences?.collapsed + + if (collapsedMap && collapsedMap[fields.data.id] !== undefined) { + setCollapsed(collapsedMap[fields.data.id]) + initialState = collapsedMap[fields.data.id] + } + }) + return initialState + }) const hasSubmitted = useFormSubmitted() - const childErrorPathsCount = 0 // TODO row.childErrorPaths?.size - const fieldHasErrors = hasSubmitted && childErrorPathsCount > 0 + + const [errorCount, setErrorCount] = React.useState(0) + + const fieldHasErrors = hasSubmitted && errorCount > 0 const classNames = [ `${baseClass}__row`, @@ -42,31 +64,46 @@ export const BlockContent: React.FC = (props) => { const path = '' as const const onFormChange = useCallback( - ({ formData }: { formData: Data }) => { + ({ fields: formFields, formData }: { fields: Fields; formData: Data }) => { editor.update(() => { const node: BlockNode = $getNodeByKey(nodeKey) if (node) { node.setFields({ - collapsed: collapsed, data: formData as any, }) } }) + + // update error count + if (hasSubmitted) { + let rowErrorCount = 0 + for (const formField of Object.values(formFields)) { + if (formField?.valid === false) { + rowErrorCount++ + } + } + setErrorCount(rowErrorCount) + } }, - [editor, nodeKey, collapsed], + [editor, nodeKey, hasSubmitted], ) const onCollapsedChange = useCallback(() => { - editor.update(() => { - const node: BlockNode = $getNodeByKey(nodeKey) - if (node) { - node.setFields({ - ...node.getFields(), - collapsed: collapsed, - }) - } + getDocPreferences().then((currentDocPreferences) => { + const currentFieldPreferences = currentDocPreferences?.fields[field.name] + + const collapsedMap: { [key: string]: boolean } = currentFieldPreferences?.collapsed + + const newCollapsed: { [key: string]: boolean } = + collapsedMap && collapsedMap?.size ? collapsedMap : {} + + newCollapsed[fields.data.id] = !collapsed + + setDocFieldPreferences(field.name, { + collapsed: newCollapsed, + }) }) - }, [editor, nodeKey, collapsed]) + }, [collapsed, getDocPreferences, field.name, setDocFieldPreferences, fields.data.id]) const removeBlock = useCallback(() => { editor.update(() => { @@ -79,7 +116,7 @@ export const BlockContent: React.FC = (props) => {
@@ -90,7 +127,7 @@ export const BlockContent: React.FC = (props) => { {getTranslation(block.labels.singular, i18n)} - {fieldHasErrors && } + {fieldHasErrors && }