Skip to content

Commit ef527fe

Browse files
authored
fix(richtext-lexical): error in admin panel when setting a richtext field in useAsTitle (#11707)
If the `useAsTitle` property is defined referencing a richtext field, the admin panel throws errors in several places. I noticed this in the email builder plugin, where we're making the subject field (which is the title) a single-paragraph richtext field instead of a text field for technical reasons. In this PR, for the lexical richtext case, I'm converting the first child of the RootNode (usually a paragraph or heading) to plain text. Additionally, I am verifying that if the resulting title is not of type string, fallback to "untitled" so that this does not happen again in the future (perhaps with slate, or with other fields).
1 parent dd80f52 commit ef527fe

File tree

13 files changed

+52
-23
lines changed

13 files changed

+52
-23
lines changed

packages/ui/src/elements/Autosave/index.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import { useDocumentInfo } from '../../providers/DocumentInfo/index.js'
2222
import { useLocale } from '../../providers/Locale/index.js'
2323
import './index.scss'
2424
import { useTranslation } from '../../providers/Translation/index.js'
25-
import { formatTimeToNow } from '../../utilities/formatDate.js'
25+
import { formatTimeToNow } from '../../utilities/formatDocTitle/formatDateTitle.js'
2626
import { reduceFieldsToValuesWithValidation } from '../../utilities/reduceFieldsToValuesWithValidation.js'
2727
import { LeaveWithoutSaving } from '../LeaveWithoutSaving/index.js'
2828

packages/ui/src/elements/DocumentControls/index.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { useFormInitializing, useFormProcessing } from '../../forms/Form/context
1616
import { useConfig } from '../../providers/Config/index.js'
1717
import { useEditDepth } from '../../providers/EditDepth/index.js'
1818
import { useTranslation } from '../../providers/Translation/index.js'
19-
import { formatDate } from '../../utilities/formatDate.js'
19+
import { formatDate } from '../../utilities/formatDocTitle/formatDateTitle.js'
2020
import { Autosave } from '../Autosave/index.js'
2121
import { Button } from '../Button/index.js'
2222
import { CopyLocaleData } from '../CopyLocaleData/index.js'

packages/ui/src/elements/PublishButton/ScheduleDrawer/buildUpcomingColumns.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import React from 'react'
55

66
import type { UpcomingEvent } from './types.js'
77

8-
import { formatDate } from '../../../utilities/formatDate.js'
8+
import { formatDate } from '../../../utilities/formatDocTitle/formatDateTitle.js'
99
import { Button } from '../../Button/index.js'
1010
import { Pill } from '../../Pill/index.js'
1111

packages/ui/src/elements/Table/DefaultCell/fields/Date/index.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import React from 'react'
55

66
import { useConfig } from '../../../../../providers/Config/index.js'
77
import { useTranslation } from '../../../../../providers/Translation/index.js'
8-
import { formatDate } from '../../../../../utilities/formatDate.js'
8+
import { formatDate } from '../../../../../utilities/formatDocTitle/formatDateTitle.js'
99

1010
export const DateCell: React.FC<DefaultCellComponentProps<DateFieldClient>> = ({
1111
cellData,

packages/ui/src/elements/Table/DefaultCell/fields/Relationship/index.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { useIntersect } from '../../../../../hooks/useIntersect.js'
1313
import { useConfig } from '../../../../../providers/Config/index.js'
1414
import { useTranslation } from '../../../../../providers/Translation/index.js'
1515
import { canUseDOM } from '../../../../../utilities/canUseDOM.js'
16-
import { formatDocTitle } from '../../../../../utilities/formatDocTitle.js'
16+
import { formatDocTitle } from '../../../../../utilities/formatDocTitle/index.js'
1717
import { useListRelationships } from '../../../RelationshipProvider/index.js'
1818
import { FileCell } from '../File/index.js'
1919
import './index.scss'

packages/ui/src/elements/ThumbnailCard/index.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import React from 'react'
55

66
import { useConfig } from '../../providers/Config/index.js'
77
import { useTranslation } from '../../providers/Translation/index.js'
8-
import { formatDocTitle } from '../../utilities/formatDocTitle.js'
8+
import { formatDocTitle } from '../../utilities/formatDocTitle/index.js'
99
import './index.scss'
1010

1111
export type ThumbnailCardProps = {

packages/ui/src/exports/shared/index.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ export { abortAndIgnore, handleAbortRef } from '../../utilities/abortAndIgnore.j
1212
export { requests } from '../../utilities/api.js'
1313
export { findLocaleFromCode } from '../../utilities/findLocaleFromCode.js'
1414
export { formatAdminURL } from '../../utilities/formatAdminURL.js'
15-
export { formatDate } from '../../utilities/formatDate.js'
16-
export { formatDocTitle } from '../../utilities/formatDocTitle.js'
15+
export { formatDate } from '../../utilities/formatDocTitle/formatDateTitle.js'
16+
export { formatDocTitle } from '../../utilities/formatDocTitle/index.js'
1717
export {
1818
type EntityToGroup,
1919
EntityType,

packages/ui/src/fields/Relationship/optionsReducer.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { getTranslation } from '@payloadcms/translations'
33

44
import type { Action, Option, OptionGroup } from './types.js'
55

6-
import { formatDocTitle } from '../../utilities/formatDocTitle.js'
6+
import { formatDocTitle } from '../../utilities/formatDocTitle/index.js'
77

88
const reduceToIDs = (options) =>
99
options.reduce((ids, option) => {

packages/ui/src/providers/DocumentInfo/index.tsx

+2-10
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,13 @@
22
import type { ClientUser, DocumentPreferences, SanitizedDocumentPermissions } from 'payload'
33

44
import * as qs from 'qs-esm'
5-
import React, {
6-
createContext,
7-
use,
8-
useCallback,
9-
useEffect,
10-
useMemo,
11-
useRef,
12-
useState,
13-
} from 'react'
5+
import React, { createContext, use, useCallback, useEffect, useMemo, useRef, useState } from 'react'
146

157
import type { DocumentInfoContext, DocumentInfoProps } from './types.js'
168

179
import { useAuth } from '../../providers/Auth/index.js'
1810
import { requests } from '../../utilities/api.js'
19-
import { formatDocTitle } from '../../utilities/formatDocTitle.js'
11+
import { formatDocTitle } from '../../utilities/formatDocTitle/index.js'
2012
import { useConfig } from '../Config/index.js'
2113
import { useLocale, useLocaleLoading } from '../Locale/index.js'
2214
import { usePreferences } from '../Preferences/index.js'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
type SerializedLexicalEditor = {
2+
root: {
3+
children: Array<{ children?: Array<{ type: string }>; type: string }>
4+
}
5+
}
6+
7+
export function isSerializedLexicalEditor(value: unknown): value is SerializedLexicalEditor {
8+
return typeof value === 'object' && 'root' in value
9+
}
10+
11+
export function formatLexicalDocTitle(
12+
editorState: Array<{ children?: Array<{ type: string }>; type: string }>,
13+
textContent: string,
14+
): string {
15+
for (const node of editorState) {
16+
if ('text' in node && node.text) {
17+
textContent += node.text as string
18+
} else {
19+
if (!('children' in node)) {
20+
textContent += `[${node.type}]`
21+
}
22+
}
23+
if ('children' in node && node.children) {
24+
textContent += formatLexicalDocTitle(node.children as Array<{ type: string }>, textContent)
25+
}
26+
}
27+
return textContent
28+
}

packages/ui/src/utilities/formatDocTitle.ts packages/ui/src/utilities/formatDocTitle/index.ts

+12-3
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ import type {
88

99
import { getTranslation } from '@payloadcms/translations'
1010

11-
import { formatDate } from '../utilities/formatDate.js'
11+
import { formatDate } from './formatDateTitle.js'
12+
import { formatLexicalDocTitle, isSerializedLexicalEditor } from './formatLexicalDocTitle.js'
1213

1314
export const formatDocTitle = ({
1415
collectionConfig,
@@ -21,7 +22,7 @@ export const formatDocTitle = ({
2122
collectionConfig?: ClientCollectionConfig
2223
data: TypeWithID
2324
dateFormat: SanitizedConfig['admin']['dateFormat']
24-
fallback?: string
25+
fallback?: object | string
2526
globalConfig?: ClientGlobalConfig
2627
i18n: I18n<any, any>
2728
}): string => {
@@ -54,8 +55,16 @@ export const formatDocTitle = ({
5455
title = getTranslation(globalConfig?.label, i18n) || globalConfig?.slug
5556
}
5657

58+
// richtext lexical case. We convert the first child of root to plain text
59+
if (isSerializedLexicalEditor(title)) {
60+
title = formatLexicalDocTitle(title.root.children?.[0]?.children || [], '')
61+
}
62+
if (!title && isSerializedLexicalEditor(fallback)) {
63+
title = formatLexicalDocTitle(fallback.root.children?.[0]?.children || [], '')
64+
}
65+
5766
if (!title) {
58-
title = fallback || `[${i18n.t('general:untitled')}]`
67+
title = typeof fallback === 'string' ? fallback : `[${i18n.t('general:untitled')}]`
5968
}
6069

6170
return title

packages/ui/src/views/Edit/SetDocumentTitle/index.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { useEffect, useRef } from 'react'
66
import { useFormFields } from '../../../forms/Form/context.js'
77
import { useDocumentInfo } from '../../../providers/DocumentInfo/index.js'
88
import { useTranslation } from '../../../providers/Translation/index.js'
9-
import { formatDocTitle } from '../../../utilities/formatDocTitle.js'
9+
import { formatDocTitle } from '../../../utilities/formatDocTitle/index.js'
1010

1111
export const SetDocumentTitle: React.FC<{
1212
collectionConfig?: ClientCollectionConfig

0 commit comments

Comments
 (0)