chore(richtext-lexical): add strictNullChecks to the richtext-lexical package (#8295)

This PR addresses around 500 TypeScript errors by enabling
strictNullChecks in the richtext-lexical package. In the process,
several bugs were identified and fixed.

In some cases, I applied non-null assertions where necessary, although
there may be room for further type refinement in the future. The focus
of this PR is to resolve the immediate issues without introducing
additional technical debt, rather than aiming for perfect type
definitions at this stage.

---------

Co-authored-by: Alessio Gravili <alessio@gravili.de>
This commit is contained in:
Germán Jabloñski
2024-09-20 15:42:30 -03:00
committed by GitHub
parent 1f7d47a361
commit 81a972d966
89 changed files with 447 additions and 360 deletions

View File

@@ -443,7 +443,7 @@ export interface FieldBaseClient {
* Must not be one of reserved field names: ['__v', 'salt', 'hash', 'file']
* @link https://payloadcms.com/docs/fields/overview#field-names
*/
name?: string
name: string
required?: boolean
saveToJWT?: boolean | string
/**

View File

@@ -63,8 +63,6 @@ import localOperations from './collections/operations/local/index.js'
import { consoleEmailAdapter } from './email/consoleEmailAdapter.js'
import { fieldAffectsData } from './fields/config/types.js'
import localGlobalOperations from './globals/operations/local/index.js'
import { checkDependencies } from './utilities/dependencies/dependencyChecker.js'
import flattenFields from './utilities/flattenTopLevelFields.js'
import { getLogger } from './utilities/logger.js'
import { serverInit as serverInitTelemetry } from './utilities/telemetry/events/serverInit.js'
import { traverseFields } from './utilities/traverseFields.js'
@@ -91,6 +89,9 @@ export interface GeneratedTypes {
collectionsUntyped: {
[slug: string]: JsonObject & TypeWithID
}
dbUntyped: {
defaultIDType: number | string
}
globalsUntyped: {
[slug: string]: JsonObject
}
@@ -115,6 +116,13 @@ type StringKeyOf<T> = Extract<keyof T, string>
// Define the types for slugs using the appropriate collections and globals
export type CollectionSlug = StringKeyOf<TypedCollection>
type ResolveDbType<T> = 'db' extends keyof T
? T['db']
: // @ts-expect-error
T['dbUntyped']
export type DefaultDocumentIDType = ResolveDbType<GeneratedTypes>['defaultIDType']
export type GlobalSlug = StringKeyOf<TypedGlobal>
// now for locale and user

View File

@@ -40,6 +40,7 @@ export const DynamicFieldSelector: React.FC<
<SelectField
{...props}
field={{
name: props?.field?.name,
options,
...(props.field || {}),
}}

View File

@@ -65,7 +65,7 @@ export const RichTextCell: React.FC<
}
const finalSanitizedEditorConfig = useMemo<SanitizedClientEditorConfig>(() => {
const clientFeatures: GeneratedFeatureProviderComponent[] = richTextComponentMap.get(
const clientFeatures: GeneratedFeatureProviderComponent[] = richTextComponentMap?.get(
'features',
) as GeneratedFeatureProviderComponent[]

View File

@@ -12,7 +12,7 @@ export const MarkdownTransformer: ElementTransformer = {
}
const lines = exportChildren(node).split('\n')
const output = []
const output: string[] = []
for (const line of lines) {
output.push('> ' + line)
}

View File

@@ -21,8 +21,8 @@ import React, { useCallback } from 'react'
import type { LexicalRichTextFieldProps } from '../../../../types.js'
import type { BlockFields } from '../../server/nodes/BlocksNode.js'
import type { BlockNode } from '../nodes/BlocksNode.js'
import { $isBlockNode } from '../nodes/BlocksNode.js'
import { FormSavePlugin } from './FormSavePlugin.js'
type Props = {
@@ -122,8 +122,8 @@ export const BlockContent: React.FC<Props> = (props) => {
// ensure that the nested editor has finished its update cycle before we update the block node.
setTimeout(() => {
editor.update(() => {
const node: BlockNode = $getNodeByKey(nodeKey)
if (node) {
const node = $getNodeByKey(nodeKey)
if (node && $isBlockNode(node)) {
formData = newFormData
node.setFields(newFormData)
}
@@ -176,7 +176,7 @@ export const BlockContent: React.FC<Props> = (props) => {
const removeBlock = useCallback(() => {
editor.update(() => {
$getNodeByKey(nodeKey).remove()
$getNodeByKey(nodeKey)?.remove()
})
}, [editor, nodeKey])
@@ -198,11 +198,11 @@ export const BlockContent: React.FC<Props> = (props) => {
className={`${baseClass}__block-pill ${baseClass}__block-pill-${formData?.blockType}`}
pillStyle="white"
>
{typeof clientBlock?.labels.singular === 'string'
{typeof clientBlock?.labels?.singular === 'string'
? getTranslation(clientBlock?.labels.singular, i18n)
: clientBlock.slug}
</Pill>
<SectionTitle path="blockName" readOnly={field?.admin?.readOnly} />
<SectionTitle path="blockName" readOnly={field?.admin?.readOnly || false} />
{fieldHasErrors && <ErrorPill count={errorCount} i18n={i18n} withMessage />}
</div>
{editor.isEditable() && (

View File

@@ -1,7 +1,5 @@
'use client'
import type { FormProps } from '@payloadcms/ui'
import {
Collapsible,
Form,
@@ -33,7 +31,7 @@ import './index.scss'
type Props = {
readonly children?: React.ReactNode
readonly formData: BlockFields
readonly nodeKey?: string
readonly nodeKey: string
}
export const BlockComponent: React.FC<Props> = (props) => {
@@ -52,13 +50,16 @@ export const BlockComponent: React.FC<Props> = (props) => {
const schemaFieldsPath = `${schemaPath}.lexical_internal_feature.blocks.lexical_blocks.lexical_blocks.${formData.blockType}`
const componentMapRenderedBlockPath = `lexical_internal_feature.blocks.fields.lexical_blocks`
const blocksField: BlocksFieldClient = richTextComponentMap.get(componentMapRenderedBlockPath)[0]
const blocksField: BlocksFieldClient = richTextComponentMap?.get(componentMapRenderedBlockPath)[0]
const clientBlock = blocksField.blocks.find((block) => block.slug === formData.blockType)
// Field Schema
useEffect(() => {
const awaitInitialState = async () => {
if (!id) {
return
}
const { state } = await getFormState({
apiRoute: config.routes.api,
body: {
@@ -87,8 +88,11 @@ export const BlockComponent: React.FC<Props> = (props) => {
}
}, [config.routes.api, config.serverURL, schemaFieldsPath, id]) // DO NOT ADD FORMDATA HERE! Adding formData will kick you out of sub block editors while writing.
const onChange: FormProps['onChange'][0] = useCallback(
const onChange = useCallback(
async ({ formState: prevFormState }) => {
if (!id) {
throw new Error('No ID found')
}
const { state: formState } = await getFormState({
apiRoute: config.routes.api,
body: {
@@ -155,13 +159,13 @@ export const BlockComponent: React.FC<Props> = (props) => {
className={`${baseClass}__block-pill ${baseClass}__block-pill-${formData?.blockType}`}
pillStyle="white"
>
{clientBlock && typeof clientBlock.labels.singular === 'string'
{clientBlock && typeof clientBlock.labels?.singular === 'string'
? getTranslation(clientBlock.labels.singular, i18n)
: clientBlock.slug}
: clientBlock?.slug}
</Pill>
<SectionTitle
path="blockName"
readOnly={parentLexicalRichTextField?.admin?.readOnly}
readOnly={parentLexicalRichTextField?.admin?.readOnly || false}
/>
</div>
</div>

View File

@@ -29,7 +29,7 @@ import './index.scss'
type Props = {
readonly formData: InlineBlockFields
readonly nodeKey?: string
readonly nodeKey: string
}
export const InlineBlockComponent: React.FC<Props> = (props) => {
@@ -45,12 +45,12 @@ export const InlineBlockComponent: React.FC<Props> = (props) => {
} = useEditorConfigContext()
const componentMapRenderedBlockPath = `lexical_internal_feature.blocks.fields.lexical_inline_blocks`
const blocksField: BlocksFieldClient = richTextComponentMap.get(componentMapRenderedBlockPath)[0]
const blocksField: BlocksFieldClient = richTextComponentMap?.get(componentMapRenderedBlockPath)[0]
const clientBlock = blocksField.blocks.find((block) => block.slug === formData.blockType)
const removeInlineBlock = useCallback(() => {
editor.update(() => {
$getNodeByKey(nodeKey).remove()
$getNodeByKey(nodeKey)?.remove()
})
}, [editor, nodeKey])
@@ -103,7 +103,7 @@ export const InlineBlockComponent: React.FC<Props> = (props) => {
const blockDisplayName = clientBlock?.labels?.singular
? getTranslation(clientBlock.labels.singular, i18n)
: clientBlock.slug
: clientBlock?.slug
return (
<div
@@ -122,7 +122,7 @@ export const InlineBlockComponent: React.FC<Props> = (props) => {
mappedComponent={clientBlock.admin.components.Label}
/>
) : (
<div>{getTranslation(clientBlock.labels?.singular, i18n)}</div>
<div>{getTranslation(clientBlock!.labels!.singular, i18n)}</div>
)}
{editor.isEditable() && (
<div className={`${baseClass}__actions`}>

View File

@@ -19,7 +19,7 @@ export type BlocksFeatureClientProps = {
clientBlockSlugs: string[]
clientInlineBlockSlugs: string[]
}
// @ts-expect-error - TODO: fix this
export const BlocksFeatureClient = createClientFeature<BlocksFeatureClientProps>(({ props }) => ({
nodes: [BlockNode, InlineBlockNode],
plugins: [
@@ -39,22 +39,25 @@ export const BlocksFeatureClient = createClientFeature<BlocksFeatureClientProps>
key: 'block-' + blockSlug,
keywords: ['block', 'blocks', blockSlug],
label: ({ i18n, richTextComponentMap }) => {
if (!richTextComponentMap) {
return blockSlug
}
const componentMapRenderedBlockPath = `lexical_internal_feature.blocks.fields.lexical_blocks`
const blocksField: BlocksFieldClient = richTextComponentMap.get(
componentMapRenderedBlockPath,
)[0]
)?.[0]
const clientBlock = blocksField.blocks.find((_block) => _block.slug === blockSlug)
const blockDisplayName = clientBlock.labels.singular
const blockDisplayName = clientBlock?.labels?.singular
? getTranslation(clientBlock.labels.singular, i18n)
: clientBlock.slug
: clientBlock?.slug
return blockDisplayName
},
onSelect: ({ editor }) => {
editor.dispatchCommand(INSERT_BLOCK_COMMAND, {
id: null,
blockName: '',
blockType: blockSlug,
})
@@ -75,26 +78,29 @@ export const BlocksFeatureClient = createClientFeature<BlocksFeatureClientProps>
key: 'inlineBlocks-' + inlineBlockSlug,
keywords: ['inlineBlock', 'inline block', inlineBlockSlug],
label: ({ i18n, richTextComponentMap }) => {
if (!richTextComponentMap) {
return inlineBlockSlug
}
const componentMapRenderedBlockPath = `lexical_internal_feature.blocks.fields.lexical_inline_blocks`
const blocksField: BlocksFieldClient = richTextComponentMap.get(
componentMapRenderedBlockPath,
)[0]
)?.[0]
const clientBlock = blocksField.blocks.find(
(_block) => _block.slug === inlineBlockSlug,
)
const blockDisplayName = clientBlock.labels.singular
const blockDisplayName = clientBlock?.labels?.singular
? getTranslation(clientBlock.labels.singular, i18n)
: clientBlock.slug
: clientBlock?.slug
return blockDisplayName
},
onSelect: ({ editor }) => {
editor.dispatchCommand(OPEN_INLINE_BLOCK_DRAWER_COMMAND, {
fields: {
id: null,
blockName: '',
blockType: inlineBlockSlug,
},
@@ -122,22 +128,24 @@ export const BlocksFeatureClient = createClientFeature<BlocksFeatureClientProps>
isActive: undefined, // At this point, we would be inside a sub-richtext-editor. And at this point this will be run against the focused sub-editor, not the parent editor which has the actual block. Thus, no point in running this
key: 'block-' + blockSlug,
label: ({ i18n, richTextComponentMap }) => {
if (!richTextComponentMap) {
return blockSlug
}
const componentMapRenderedBlockPath = `lexical_internal_feature.blocks.fields.lexical_blocks`
const blocksField: BlocksFieldClient = richTextComponentMap.get(
componentMapRenderedBlockPath,
)[0]
)?.[0]
const clientBlock = blocksField.blocks.find((_block) => _block.slug === blockSlug)
const blockDisplayName = clientBlock.labels.singular
const blockDisplayName = clientBlock?.labels?.singular
? getTranslation(clientBlock.labels.singular, i18n)
: clientBlock.slug
: clientBlock?.slug
return blockDisplayName
},
onSelect: ({ editor }) => {
editor.dispatchCommand(INSERT_BLOCK_COMMAND, {
id: null,
blockName: '',
blockType: blockSlug,
})
@@ -159,18 +167,22 @@ export const BlocksFeatureClient = createClientFeature<BlocksFeatureClientProps>
isActive: undefined,
key: 'inlineBlock-' + inlineBlockSlug,
label: ({ i18n, richTextComponentMap }) => {
if (!richTextComponentMap) {
return inlineBlockSlug
}
const componentMapRenderedBlockPath = `lexical_internal_feature.blocks.fields.lexical_inline_blocks`
const blocksField: BlocksFieldClient = richTextComponentMap.get(
componentMapRenderedBlockPath,
)[0]
)?.[0]
const clientBlock = blocksField.blocks.find(
(_block) => _block.slug === inlineBlockSlug,
)
const blockDisplayName = clientBlock.labels.singular
const blockDisplayName = clientBlock?.labels?.singular
? getTranslation(clientBlock.labels.singular, i18n)
: clientBlock.slug
: clientBlock?.slug
return blockDisplayName
},

View File

@@ -4,7 +4,11 @@ import type { EditorConfig, LexicalEditor, LexicalNode } from 'lexical'
import ObjectID from 'bson-objectid'
import React, { type JSX } from 'react'
import type { BlockFields, SerializedBlockNode } from '../../server/nodes/BlocksNode.js'
import type {
BlockFields,
BlockFieldsOptionalID,
SerializedBlockNode,
} from '../../server/nodes/BlocksNode.js'
import { ServerBlockNode } from '../../server/nodes/BlocksNode.js'
@@ -48,7 +52,7 @@ export class BlockNode extends ServerBlockNode {
}
}
export function $createBlockNode(fields: Exclude<BlockFields, 'id'>): BlockNode {
export function $createBlockNode(fields: BlockFieldsOptionalID): BlockNode {
return new BlockNode({
fields: {
...fields,

View File

@@ -27,26 +27,25 @@ import {
import React, { useEffect, useState } from 'react'
import type { PluginComponent } from '../../../typesClient.js'
import type { BlockFields } from '../../server/nodes/BlocksNode.js'
import type { BlockFields, BlockFieldsOptionalID } from '../../server/nodes/BlocksNode.js'
import type { BlocksFeatureClientProps } from '../index.js'
import type { InlineBlockNode } from '../nodes/InlineBlocksNode.js'
import { useEditorConfigContext } from '../../../../lexical/config/client/EditorConfigProvider.js'
import { FieldsDrawer } from '../../../../utilities/fieldsDrawer/Drawer.js'
import { $createBlockNode, BlockNode } from '../nodes/BlocksNode.js'
import { $createInlineBlockNode } from '../nodes/InlineBlocksNode.js'
import { $createInlineBlockNode, $isInlineBlockNode } from '../nodes/InlineBlocksNode.js'
import {
INSERT_BLOCK_COMMAND,
INSERT_INLINE_BLOCK_COMMAND,
OPEN_INLINE_BLOCK_DRAWER_COMMAND,
} from './commands.js'
export type InsertBlockPayload = Exclude<BlockFields, 'id'>
export type InsertBlockPayload = BlockFieldsOptionalID
export const BlocksPlugin: PluginComponent<BlocksFeatureClientProps> = () => {
const [editor] = useLexicalComposerContext()
const { closeModal, toggleModal } = useModal()
const [blockFields, setBlockFields] = useState<BlockFields>(null)
const [blockFields, setBlockFields] = useState<BlockFields | null>(null)
const [blockType, setBlockType] = useState<string>('' as any)
const [targetNodeKey, setTargetNodeKey] = useState<null | string>(null)
const { i18n, t } = useTranslation<string, any>()
@@ -89,7 +88,7 @@ export const BlocksPlugin: PluginComponent<BlocksFeatureClientProps> = () => {
$isParagraphNode(focusNode) &&
focusNode.getTextContentSize() === 0 &&
focusNode
.getParent()
.getParentOrThrow()
.getChildren()
.filter((node) => $isParagraphNode(node)).length > 1
) {
@@ -106,9 +105,9 @@ export const BlocksPlugin: PluginComponent<BlocksFeatureClientProps> = () => {
INSERT_INLINE_BLOCK_COMMAND,
(fields) => {
if (targetNodeKey) {
const node: InlineBlockNode = $getNodeByKey(targetNodeKey)
const node = $getNodeByKey(targetNodeKey)
if (!node) {
if (!node || !$isInlineBlockNode(node)) {
return false
}
@@ -167,7 +166,7 @@ export const BlocksPlugin: PluginComponent<BlocksFeatureClientProps> = () => {
const schemaFieldsPath = `${schemaPath}.lexical_internal_feature.blocks.lexical_inline_blocks.lexical_inline_blocks.${blockFields?.blockType}`
const componentMapRenderedBlockPath = `lexical_internal_feature.blocks.fields.lexical_inline_blocks`
const blocksField: BlocksFieldClient = richTextComponentMap.has(componentMapRenderedBlockPath)
const blocksField: BlocksFieldClient = richTextComponentMap?.has(componentMapRenderedBlockPath)
? richTextComponentMap.get(componentMapRenderedBlockPath)[0]
: null

View File

@@ -129,6 +129,7 @@ export const BlocksFeature = createServerFeature<
i18n,
nodes: [
createNode({
// @ts-expect-error - TODO: fix this
getSubFields: ({ node }) => {
if (!node) {
if (props?.blocks?.length) {
@@ -145,7 +146,7 @@ export const BlocksFeature = createServerFeature<
const blockType = node.fields.blockType
const block = props.blocks.find((block) => block.slug === blockType)
const block = props.blocks?.find((block) => block.slug === blockType)
return block?.fields
},
getSubFieldsData: ({ node }) => {
@@ -156,6 +157,7 @@ export const BlocksFeature = createServerFeature<
validations: [blockValidationHOC(props.blocks)],
}),
createNode({
// @ts-expect-error - TODO: fix this
getSubFields: ({ node }) => {
if (!node) {
if (props?.inlineBlocks?.length) {
@@ -172,7 +174,7 @@ export const BlocksFeature = createServerFeature<
const blockType = node.fields.blockType
const block = props.inlineBlocks.find((block) => block.slug === blockType)
const block = props.inlineBlocks?.find((block) => block.slug === blockType)
return block?.fields
},
getSubFieldsData: ({ node }) => {

View File

@@ -16,13 +16,20 @@ import { DecoratorBlockNode } from '@lexical/react/LexicalDecoratorBlockNode.js'
import ObjectID from 'bson-objectid'
import { deepCopyObjectSimple } from 'payload/shared'
export type BlockFields<TBlockFields extends JsonObject = JsonObject> = {
type BaseBlockFields<TBlockFields extends JsonObject = JsonObject> = {
/** Block form data */
blockName: string
blockType: string
id: string
} & TBlockFields
export type BlockFields<TBlockFields extends JsonObject = JsonObject> = {
id: string
} & BaseBlockFields<TBlockFields>
export type BlockFieldsOptionalID<TBlockFields extends JsonObject = JsonObject> = {
id?: string
} & BaseBlockFields<TBlockFields>
export type SerializedBlockNode<TBlockFields extends JsonObject = JsonObject> = Spread<
{
children?: never // required so that our typed editor state doesn't automatically add children
@@ -84,7 +91,7 @@ export class ServerBlockNode extends DecoratorBlockNode {
return false
}
decorate(editor: LexicalEditor, config: EditorConfig): JSX.Element {
decorate(editor: LexicalEditor, config: EditorConfig): JSX.Element | null {
return null
}
@@ -121,7 +128,7 @@ export class ServerBlockNode extends DecoratorBlockNode {
}
}
export function $createServerBlockNode(fields: Exclude<BlockFields, 'id'>): ServerBlockNode {
export function $createServerBlockNode(fields: BlockFieldsOptionalID): ServerBlockNode {
return new ServerBlockNode({
fields: {
...fields,

View File

@@ -32,7 +32,7 @@ export type SerializedServerInlineBlockNode = Spread<
SerializedLexicalNode
>
export class ServerInlineBlockNode extends DecoratorNode<React.ReactElement> {
export class ServerInlineBlockNode extends DecoratorNode<null | React.ReactElement> {
__fields: InlineBlockFields
constructor({ fields, key }: { fields: InlineBlockFields; key?: NodeKey }) {
@@ -74,7 +74,7 @@ export class ServerInlineBlockNode extends DecoratorNode<React.ReactElement> {
return element
}
decorate(editor: LexicalEditor, config: EditorConfig): JSX.Element {
decorate(editor: LexicalEditor, config: EditorConfig): JSX.Element | null {
return null
}

View File

@@ -39,7 +39,7 @@ export const blockValidationHOC = (
siblingData: blockFieldData,
})
let errorPaths = []
let errorPaths: string[] = []
for (const fieldKey in result) {
if (result[fieldKey].errorPaths) {
errorPaths = errorPaths.concat(result[fieldKey].errorPaths)

View File

@@ -69,12 +69,12 @@ export async function convertLexicalToHTML({
return await convertLexicalNodesToHTML({
converters,
currentDepth,
depth,
depth: depth!,
draft: draft === undefined ? false : draft,
lexicalNodes: data?.root?.children,
overrideAccess: overrideAccess === undefined ? false : overrideAccess,
parent: data?.root,
req,
req: req!,
showHiddenFields: showHiddenFields === undefined ? false : showHiddenFields,
})
}

View File

@@ -91,7 +91,7 @@ function findFieldPathAndSiblingFields(
): {
path: string[]
siblingFields: Field[]
} {
} | null {
for (const curField of fields) {
if (curField === field) {
return {
@@ -172,7 +172,7 @@ export const lexicalHTML: (
showHiddenFields,
siblingData,
}) => {
const fields = collection ? collection.fields : global.fields
const fields = collection ? collection.fields : global!.fields
const foundSiblingFields = findFieldPathAndSiblingFields(fields, [], field)

View File

@@ -106,7 +106,7 @@ function sanitizeSelection(selection: Selection) {
function getPathFromNodeToEditor(node: Node, rootElement: HTMLElement | null) {
let currentNode: Node | null | undefined = node
const path = []
const path: number[] = []
while (currentNode !== rootElement) {
if (currentNode !== null && currentNode !== undefined) {
path.unshift(

View File

@@ -63,8 +63,8 @@ function computeSelectionCount(selection: TableSelection): {
function isTableSelectionRectangular(selection: TableSelection): boolean {
const nodes = selection.getNodes()
const currentRows: Array<number> = []
let currentRow = null
let expectedColumns = null
let currentRow: null | TableRowNode = null
let expectedColumns: null | number = null
let currentColumns = 0
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i]

View File

@@ -138,8 +138,8 @@ export const EXPERIMENTAL_TableFeature = createServerFeature({
const backgroundColor = node.backgroundColor
? `background-color: ${node.backgroundColor};`
: ''
const colSpan = node.colSpan > 1 ? `colspan="${node.colSpan}"` : ''
const rowSpan = node.rowSpan > 1 ? `rowspan="${node.rowSpan}"` : ''
const colSpan = node.colSpan && node.colSpan > 1 ? `colspan="${node.colSpan}"` : ''
const rowSpan = node.rowSpan && node.rowSpan > 1 ? `rowspan="${node.rowSpan}"` : ''
return `<${tagName} class="lexical-table-cell ${headerStateClass}" style="border: 1px solid #ccc; padding: 8px; ${backgroundColor}" ${colSpan} ${rowSpan}>${childrenText}</${tagName}>`
},

View File

@@ -35,7 +35,7 @@ export const INSERT_HORIZONTAL_RULE_COMMAND: LexicalCommand<void> = createComman
*
* If we used DecoratorBlockNode instead, we would only need a decorate method
*/
export class HorizontalRuleServerNode extends DecoratorNode<React.ReactElement> {
export class HorizontalRuleServerNode extends DecoratorNode<null | React.ReactElement> {
static clone(node: HorizontalRuleServerNode): HorizontalRuleServerNode {
return new this(node.__key)
}
@@ -74,7 +74,7 @@ export class HorizontalRuleServerNode extends DecoratorNode<React.ReactElement>
return element
}
decorate() {
decorate(): null | React.ReactElement {
return null
}

View File

@@ -19,10 +19,11 @@ const toolbarGroups: ToolbarGroup[] = [
return false
}
for (const node of selection.getNodes()) {
const parent = node.getParentOrThrow()
// If at least one node is indented, this should be active
if (
('__indent' in node && (node.__indent as number) > 0) ||
(node.getParent() && '__indent' in node.getParent() && node.getParent().__indent > 0)
('__indent' in parent && parent.__indent > 0)
) {
return true
}

View File

@@ -45,7 +45,7 @@ const toolbarGroups: ToolbarGroup[] = [
},
onSelect: ({ editor, isActive }) => {
if (!isActive) {
let selectedText: string = null
let selectedText: string | undefined
let selectedNodes: LexicalNode[] = []
editor.getEditorState().read(() => {
selectedText = $getSelection()?.getTextContent()

View File

@@ -179,11 +179,11 @@ function $createAutoLinkNode_(
endIndex: number,
match: LinkMatcherResult,
): TextNode | undefined {
const fields: LinkFields = {
const fields = {
linkType: 'custom',
url: match.url,
...match.fields,
}
} as LinkFields
const linkNode = $createAutoLinkNode({ fields })
if (nodes.length === 1) {
@@ -210,7 +210,7 @@ function $createAutoLinkNode_(
} else {
;[, firstLinkTextNode] = firstTextNode.splitText(startIndex)
}
const linkNodes = []
const linkNodes: LexicalNode[] = []
let remainingTextNode
for (let i = 1; i < nodes.length; i++) {
const currentNode = nodes[i]

View File

@@ -1,5 +1,5 @@
'use client'
import type { LexicalNode } from 'lexical'
import type { ElementNode, LexicalNode } from 'lexical'
import type { Data, FormState } from 'payload'
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext.js'
@@ -41,8 +41,8 @@ export function LinkEditor({ anchorElem }: { anchorElem: HTMLElement }): React.R
const [editor] = useLexicalComposerContext()
const editorRef = useRef<HTMLDivElement | null>(null)
const [linkUrl, setLinkUrl] = useState(null)
const [linkLabel, setLinkLabel] = useState(null)
const [linkUrl, setLinkUrl] = useState<null | string>(null)
const [linkLabel, setLinkLabel] = useState<null | string>(null)
const { uuid } = useEditorConfigContext()
@@ -50,7 +50,9 @@ export function LinkEditor({ anchorElem }: { anchorElem: HTMLElement }): React.R
const { i18n, t } = useTranslation()
const [stateData, setStateData] = useState<{ id?: string; text: string } & LinkFields>(null)
const [stateData, setStateData] = useState<
({ id?: string; text: string } & LinkFields) | undefined
>()
const { closeModal, toggleModal } = useModal()
const editDepth = useEditDepth()
@@ -74,12 +76,12 @@ export function LinkEditor({ anchorElem }: { anchorElem: HTMLElement }): React.R
setLinkUrl(null)
setLinkLabel(null)
setSelectedNodes([])
setStateData(null)
setStateData(undefined)
}, [setIsLink, setLinkUrl, setLinkLabel, setSelectedNodes])
const $updateLinkEditor = useCallback(() => {
const selection = $getSelection()
let selectedNodeDomRect: DOMRect | undefined = null
let selectedNodeDomRect: DOMRect | undefined
if (!$isRangeSelection(selection) || !selection) {
setNotLink()
@@ -90,7 +92,7 @@ export function LinkEditor({ anchorElem }: { anchorElem: HTMLElement }): React.R
const focusNode = getSelectedNode(selection)
selectedNodeDomRect = editor.getElementByKey(focusNode.getKey())?.getBoundingClientRect()
const focusLinkParent: LinkNode = $findMatchingParent(focusNode, $isLinkNode)
const focusLinkParent = $findMatchingParent(focusNode, $isLinkNode)
// Prevent link modal from showing if selection spans further than the link: https://github.com/facebook/lexical/issues/4064
const badNode = selection
@@ -111,10 +113,6 @@ export function LinkEditor({ anchorElem }: { anchorElem: HTMLElement }): React.R
// Initial state:
const data: { text: string } & LinkFields = {
doc: undefined,
linkType: undefined,
newTab: undefined,
url: '',
...focusLinkParent.getFields(),
id: focusLinkParent.getID(),
text: focusLinkParent.getTextContent(),
@@ -343,7 +341,7 @@ export function LinkEditor({ anchorElem }: { anchorElem: HTMLElement }): React.R
// See: https://github.com/facebook/lexical/pull/5536. This updates autolink nodes to link nodes whenever a change was made (which is good!).
editor.update(() => {
const selection = $getSelection()
let linkParent = null
let linkParent: ElementNode | null = null
if ($isRangeSelection(selection)) {
linkParent = getSelectedNode(selection).getParent()
} else {

View File

@@ -11,7 +11,7 @@ import { LinkNode } from './LinkNode.js'
export class AutoLinkNode extends LinkNode {
static clone(node: AutoLinkNode): AutoLinkNode {
return new AutoLinkNode({ id: undefined, fields: node.__fields, key: node.__key })
return new AutoLinkNode({ id: '', fields: node.__fields, key: node.__key })
}
static getType(): string {
@@ -67,7 +67,7 @@ export class AutoLinkNode extends LinkNode {
}
export function $createAutoLinkNode({ fields }: { fields: LinkFields }): AutoLinkNode {
return $applyNodeReplacement(new AutoLinkNode({ id: undefined, fields }))
return $applyNodeReplacement(new AutoLinkNode({ id: '', fields }))
}
export function $isAutoLinkNode(node: LexicalNode | null | undefined): node is AutoLinkNode {
return node instanceof AutoLinkNode

View File

@@ -38,7 +38,7 @@ export class LinkNode extends ElementNode {
doc: null,
linkType: 'custom',
newTab: false,
url: undefined,
url: '',
},
key,
}: {
@@ -269,14 +269,14 @@ export const TOGGLE_LINK_COMMAND: LexicalCommand<LinkPayload | null> =
export function $toggleLink(payload: LinkPayload): void {
const selection = $getSelection()
if (!$isRangeSelection(selection) && !payload.selectedNodes.length) {
if (!$isRangeSelection(selection) && !payload.selectedNodes?.length) {
return
}
const nodes = $isRangeSelection(selection) ? selection.extract() : payload.selectedNodes
if (payload === null) {
// Remove LinkNodes
nodes.forEach((node) => {
nodes?.forEach((node) => {
const parent = node.getParent()
if ($isLinkNode(parent)) {
@@ -291,7 +291,7 @@ export function $toggleLink(payload: LinkPayload): void {
})
} else {
// Add or merge LinkNodes
if (nodes.length === 1) {
if (nodes?.length === 1) {
const firstNode = nodes[0]
// if the first node is a LinkNode or if its
// parent is a LinkNode, we update the URL, target and rel.
@@ -317,7 +317,7 @@ export function $toggleLink(payload: LinkPayload): void {
let prevParent: ElementNodeType | LinkNode | null = null
let linkNode: LinkNode | null = null
nodes.forEach((node) => {
nodes?.forEach((node) => {
const parent = node.getParent()
if (parent === linkNode || parent === null || ($isElementNode(node) && !node.isInline())) {
@@ -386,8 +386,12 @@ function $getAncestor(
predicate: (ancestor: LexicalNode) => boolean,
): LexicalNode | null {
let parent: LexicalNode | null = node
// eslint-disable-next-line no-empty
while (parent !== null && (parent = parent.getParent()) !== null && !predicate(parent)) {}
while (parent !== null) {
parent = parent.getParent()
if (parent === null || predicate(parent)) {
break
}
}
return parent
}

View File

@@ -1,5 +1,5 @@
import type { SerializedElementNode, SerializedLexicalNode, Spread } from 'lexical'
import type { JsonValue } from 'payload'
import type { DefaultDocumentIDType, JsonValue } from 'payload'
export type LinkFields = {
[key: string]: JsonValue
@@ -9,9 +9,9 @@ export type LinkFields = {
| {
// Actual doc data, populated in afterRead hook
[key: string]: JsonValue
id: string
id: DefaultDocumentIDType
}
| string
| DefaultDocumentIDType
} | null
linkType: 'custom' | 'internal'
newTab: boolean

View File

@@ -11,8 +11,8 @@ import { validateUrl, validateUrlMinimal } from '../../../lexical/utils/url.js'
export const getBaseFields = (
config: SanitizedConfig,
enabledCollections: CollectionSlug[],
disabledCollections: CollectionSlug[],
enabledCollections?: CollectionSlug[],
disabledCollections?: CollectionSlug[],
maxDepth?: number,
): FieldAffectingData[] => {
let enabledRelations: CollectionSlug[]
@@ -76,6 +76,7 @@ export const getBaseFields = (
},
label: ({ t }) => t('fields:enterURL'),
required: true,
// @ts-expect-error - TODO: fix this
validate: (value: string) => {
if (!validateUrlMinimal(value)) {
return 'Invalid URL'
@@ -106,10 +107,12 @@ export const getBaseFields = (
filterOptions:
!enabledCollections && !disabledCollections
? ({ relationTo, user }) => {
const hidden = config.collections.find(({ slug }) => slug === relationTo).admin.hidden
const hidden = config.collections.find(({ slug }) => slug === relationTo)?.admin
.hidden
if (typeof hidden === 'function' && hidden({ user } as { user: User })) {
return false
}
return true
}
: null,
label: ({ t }) => t('fields:chooseDocumentToLink'),

View File

@@ -1,4 +1,11 @@
import type { CollectionSlug, Config, Field, FieldAffectingData, SanitizedConfig } from 'payload'
import type {
CollectionSlug,
Config,
DefaultDocumentIDType,
Field,
FieldAffectingData,
SanitizedConfig,
} from 'payload'
import escapeHTML from 'escape-html'
import { sanitizeFields } from 'payload'
@@ -71,7 +78,7 @@ export const LinkFeature = createServerFeature<
const validRelationships = _config.collections.map((c) => c.slug) || []
const _transformedFields = transformExtraFields(
deepCopyObject(props.fields),
props.fields ? deepCopyObject(props.fields) : null,
_config,
props.enabledCollections,
props.disabledCollections,
@@ -148,9 +155,9 @@ export const LinkFeature = createServerFeature<
let href: string = node.fields.url
if (node.fields.linkType === 'internal') {
href =
typeof node.fields.doc?.value === 'string'
? node.fields.doc?.value
: node.fields.doc?.value?.id
typeof node.fields.doc?.value !== 'object'
? String(node.fields.doc?.value)
: String(node.fields.doc?.value?.id)
}
return `<a href="${href}"${target}${rel}>${childrenText}</a>`

View File

@@ -11,7 +11,8 @@ export function transformExtraFields(
config: SanitizedConfig
defaultFields: FieldAffectingData[]
}) => (Field | FieldAffectingData)[])
| Field[],
| Field[]
| null,
config: SanitizedConfig,
enabledCollections?: CollectionSlug[],
disabledCollections?: CollectionSlug[],

View File

@@ -31,7 +31,7 @@ export const linkValidation = (
siblingData: node.fields,
})
let errorPaths = []
let errorPaths: string[] = []
for (const fieldKey in result) {
if (result[fieldKey].errorPaths) {
errorPaths = errorPaths.concat(result[fieldKey].errorPaths)

View File

@@ -45,7 +45,7 @@ export const listExport = (
exportChildren: (node: ElementNode) => string,
depth: number,
): string => {
const output = []
const output: string[] = []
const children = listNode.getChildren()
let index = 0
for (const listItemNode of children) {

View File

@@ -51,6 +51,7 @@ export function convertLexicalPluginNodesToLexical({
return []
}
const unknownConverter = converters.find((converter) => converter.nodeTypes.includes('unknown'))
// @ts-expect-error - vestiges of the migration to strict mode. Probably not important enough in this file to fix
return (
lexicalPluginNodes.map((lexicalPluginNode, i) => {
if (lexicalPluginNode.type === 'paragraph') {

View File

@@ -62,7 +62,7 @@ export class UnknownConvertedNode extends DecoratorNode<JSX.Element> {
return element
}
decorate(): JSX.Element | null {
decorate(): JSX.Element {
return <Component data={this.__data} />
}

View File

@@ -11,7 +11,7 @@ export const SlateBlockquoteConverter: SlateNodeConverter = {
canContainParagraphs: false,
converters,
parentNodeType: 'quote',
slateNodes: slateNode.children,
slateNodes: slateNode.children!,
}),
direction: 'ltr',
format: '',

View File

@@ -11,7 +11,7 @@ export const SlateHeadingConverter: SlateNodeConverter = {
canContainParagraphs: false,
converters,
parentNodeType: 'heading',
slateNodes: slateNode.children,
slateNodes: slateNode.children!,
}),
direction: 'ltr',
format: '',

View File

@@ -11,7 +11,7 @@ export const SlateLinkConverter: SlateNodeConverter = {
canContainParagraphs: false,
converters,
parentNodeType: 'link',
slateNodes: slateNode.children,
slateNodes: slateNode.children!,
}),
direction: 'ltr',
fields: {

View File

@@ -12,7 +12,7 @@ export const SlateListItemConverter: SlateNodeConverter = {
canContainParagraphs: false,
converters,
parentNodeType: 'listitem',
slateNodes: slateNode.children,
slateNodes: slateNode.children!,
}),
direction: 'ltr',
format: '',

View File

@@ -11,7 +11,7 @@ export const SlateOrderedListConverter: SlateNodeConverter = {
canContainParagraphs: false,
converters,
parentNodeType: 'list',
slateNodes: slateNode.children,
slateNodes: slateNode.children!,
}),
direction: 'ltr',
format: '',

View File

@@ -11,7 +11,7 @@ export const SlateUnknownConverter: SlateNodeConverter = {
canContainParagraphs: false,
converters,
parentNodeType: 'unknownConverted',
slateNodes: slateNode.children,
slateNodes: slateNode.children!,
}),
data: {
nodeData: slateNode,

View File

@@ -11,7 +11,7 @@ export const SlateUnorderedListConverter: SlateNodeConverter = {
canContainParagraphs: false,
converters,
parentNodeType: 'list',
slateNodes: slateNode.children,
slateNodes: slateNode.children!,
}),
direction: 'ltr',
format: '',

View File

@@ -51,6 +51,7 @@ export function convertSlateNodesToLexical({
return []
}
const unknownConverter = converters.find((converter) => converter.nodeTypes.includes('unknown'))
// @ts-expect-error - vestiges of the migration to strict mode. Probably not important enough in this file to fix
return (
slateNodes.map((slateNode, i) => {
if (!('type' in slateNode)) {
@@ -66,7 +67,9 @@ export function convertSlateNodesToLexical({
return convertParagraphNode(converters, slateNode)
}
const converter = converters.find((converter) => converter.nodeTypes.includes(slateNode.type))
const converter = converters.find((converter) =>
converter.nodeTypes.includes(slateNode.type!),
)
if (converter) {
return converter.converter({ childIndex: i, converters, parentNodeType, slateNode })

View File

@@ -62,7 +62,7 @@ export class UnknownConvertedNode extends DecoratorNode<JSX.Element> {
return element
}
decorate(): JSX.Element | null {
decorate(): JSX.Element {
return <Component data={this.__data} />
}

View File

@@ -52,7 +52,7 @@ const Component: React.FC<Props> = (props) => {
const relationshipElemRef = useRef<HTMLDivElement | null>(null)
const [editor] = useLexicalComposerContext()
const [isSelected, setSelected, clearSelection] = useLexicalNodeSelection(nodeKey)
const [isSelected, setSelected, clearSelection] = useLexicalNodeSelection(nodeKey!)
const { field } = useEditorConfigContext()
const {
config: {
@@ -62,8 +62,8 @@ const Component: React.FC<Props> = (props) => {
},
} = useConfig()
const [relatedCollection, setRelatedCollection] = useState(() =>
collections.find((coll) => coll.slug === relationTo),
const [relatedCollection, setRelatedCollection] = useState(
() => collections.find((coll) => coll.slug === relationTo)!,
)
const { i18n, t } = useTranslation()
@@ -80,7 +80,7 @@ const Component: React.FC<Props> = (props) => {
const removeRelationship = useCallback(() => {
editor.update(() => {
$getNodeByKey(nodeKey).remove()
$getNodeByKey(nodeKey!)?.remove()
})
}, [editor, nodeKey])
@@ -102,7 +102,7 @@ const Component: React.FC<Props> = (props) => {
if (isSelected && $isNodeSelection($getSelection())) {
const event: KeyboardEvent = payload
event.preventDefault()
const node = $getNodeByKey(nodeKey)
const node = $getNodeByKey(nodeKey!)
if ($isRelationshipNode(node)) {
node.remove()
return true
@@ -172,9 +172,11 @@ const Component: React.FC<Props> = (props) => {
el="div"
icon="swap"
onClick={() => {
editor.dispatchCommand(INSERT_RELATIONSHIP_WITH_DRAWER_COMMAND, {
replace: { nodeKey },
})
if (nodeKey) {
editor.dispatchCommand(INSERT_RELATIONSHIP_WITH_DRAWER_COMMAND, {
replace: { nodeKey },
})
}
}}
round
tooltip={t('fields:swapRelationship')}

View File

@@ -44,12 +44,12 @@ type Props = {
const RelationshipDrawerComponent: React.FC<Props> = ({ enabledCollectionSlugs }) => {
const [editor] = useLexicalComposerContext()
const [selectedCollectionSlug, setSelectedCollectionSlug] = useState(
() => enabledCollectionSlugs[0],
() => enabledCollectionSlugs?.[0],
)
const [replaceNodeKey, setReplaceNodeKey] = useState<null | string>(null)
const [ListDrawer, ListDrawerToggler, { closeDrawer, isDrawerOpen, openDrawer }] = useListDrawer({
collectionSlugs: enabledCollectionSlugs,
collectionSlugs: enabledCollectionSlugs!,
selectedCollection: selectedCollectionSlug,
})
@@ -83,14 +83,14 @@ const RelationshipDrawerComponent: React.FC<Props> = ({ enabledCollectionSlugs }
useEffect(() => {
// always reset back to first option
// TODO: this is not working, see the ListDrawer component
setSelectedCollectionSlug(enabledCollectionSlugs[0])
setSelectedCollectionSlug(enabledCollectionSlugs?.[0])
}, [isDrawerOpen, enabledCollectionSlugs])
return <ListDrawer onSelect={onSelect} />
}
export const RelationshipDrawer = (props: Props): React.ReactNode => {
return props?.enabledCollectionSlugs?.length > 0 ? ( // If enabledCollectionSlugs it overrides what EnabledRelationshipsCondition is doing
return (props?.enabledCollectionSlugs?.length ?? -1) > 0 ? ( // If enabledCollectionSlugs it overrides what EnabledRelationshipsCondition is doing
<RelationshipDrawerComponent {...props} />
) : (
<EnabledRelationshipsCondition {...props}>

View File

@@ -31,7 +31,7 @@ export const RelationshipPlugin: PluginComponent<RelationshipFeatureProps> = ({
config: { collections },
} = useConfig()
let enabledRelations: string[] = null
let enabledRelations: null | string[] = null
if (clientProps?.enabledCollections) {
enabledRelations = clientProps?.enabledCollections
@@ -65,7 +65,7 @@ export const RelationshipPlugin: PluginComponent<RelationshipFeatureProps> = ({
$isParagraphNode(focusNode) &&
focusNode.getTextContentSize() === 0 &&
focusNode
.getParent()
.getParentOrThrow()
.getChildren()
.filter((node) => $isParagraphNode(node)).length > 1
) {

View File

@@ -17,7 +17,7 @@ type FilteredCollectionsT = (
const filterRichTextCollections: FilteredCollectionsT = (collections, options) => {
return collections.filter(({ slug, admin: { enableRichTextRelationship }, upload }) => {
if (!options.visibleEntities.collections.includes(slug)) {
if (!options?.visibleEntities.collections.includes(slug)) {
return false
}
@@ -38,7 +38,7 @@ export const EnabledRelationshipsCondition: React.FC<any> = (props) => {
const { visibleEntities } = useEntityVisibility()
const [enabledCollectionSlugs] = React.useState(() =>
filterRichTextCollections(collections, { uploads, user, visibleEntities }).map(
filterRichTextCollections(collections, { uploads, user: user!, visibleEntities }).map(
({ slug }) => slug,
),
)

View File

@@ -104,7 +104,7 @@ export class RelationshipServerNode extends DecoratorBlockNode {
return false
}
decorate(editor: LexicalEditor, config: EditorConfig): JSX.Element {
decorate(editor: LexicalEditor, config: EditorConfig): JSX.Element | null {
return null
}

View File

@@ -34,9 +34,13 @@ function ButtonGroupItem({
)
}
if (!item.ChildComponent) {
return null
}
return (
<ToolbarButton editor={editor} item={item} key={item.key}>
{item?.ChildComponent && <item.ChildComponent />}
{<item.ChildComponent />}
</ToolbarButton>
)
}
@@ -58,14 +62,14 @@ function ToolbarGroupComponent({
const {
field: { richTextComponentMap },
} = useEditorConfigContext()
const [dropdownLabel, setDropdownLabel] = React.useState<null | string>(null)
const [DropdownIcon, setDropdownIcon] = React.useState<null | React.FC>(null)
const [dropdownLabel, setDropdownLabel] = React.useState<string | undefined>(undefined)
const [DropdownIcon, setDropdownIcon] = React.useState<React.FC | undefined>(undefined)
React.useEffect(() => {
if (group?.type === 'dropdown' && group.items.length && group.ChildComponent) {
setDropdownIcon(() => group.ChildComponent)
setDropdownIcon(() => group.ChildComponent!)
} else {
setDropdownIcon(null)
setDropdownIcon(undefined)
}
}, [group])
@@ -73,11 +77,11 @@ function ToolbarGroupComponent({
({ activeItems }: { activeItems: ToolbarGroupItem[] }) => {
if (!activeItems.length) {
if (group?.type === 'dropdown' && group.items.length && group.ChildComponent) {
setDropdownIcon(() => group.ChildComponent)
setDropdownLabel(null)
setDropdownIcon(() => group.ChildComponent!)
setDropdownLabel(undefined)
} else {
setDropdownIcon(null)
setDropdownLabel(null)
setDropdownIcon(undefined)
setDropdownLabel(undefined)
}
return
}
@@ -153,7 +157,7 @@ function FixedToolbar({
}): React.ReactNode {
const currentToolbarRef = React.useRef<HTMLDivElement>(null)
const { y } = useScrollInfo()
const { y } = useScrollInfo!()
// Memoize the parent toolbar element
const parentToolbarElem = useMemo(() => {
@@ -192,13 +196,13 @@ function FixedToolbar({
)
if (overlapping) {
currentToolbarRef.current.className = 'fixed-toolbar fixed-toolbar--overlapping'
currentToolbarElem.className = 'fixed-toolbar fixed-toolbar--overlapping'
parentToolbarElem.className = 'fixed-toolbar fixed-toolbar--hide'
} else {
if (!currentToolbarRef.current.classList.contains('fixed-toolbar--overlapping')) {
if (!currentToolbarElem.classList.contains('fixed-toolbar--overlapping')) {
return
}
currentToolbarRef.current.className = 'fixed-toolbar'
currentToolbarElem.className = 'fixed-toolbar'
parentToolbarElem.className = 'fixed-toolbar'
}
},

View File

@@ -40,10 +40,13 @@ function ButtonGroupItem({
)
)
}
if (!item.ChildComponent) {
return null
}
return (
<ToolbarButton editor={editor} item={item} key={item.key}>
{item?.ChildComponent && <item.ChildComponent />}
<item.ChildComponent />
</ToolbarButton>
)
}
@@ -61,13 +64,13 @@ function ToolbarGroupComponent({
}): React.ReactNode {
const { editorConfig } = useEditorConfigContext()
const [DropdownIcon, setDropdownIcon] = React.useState<null | React.FC>(null)
const [DropdownIcon, setDropdownIcon] = React.useState<React.FC | undefined>()
React.useEffect(() => {
if (group?.type === 'dropdown' && group.items.length && group.ChildComponent) {
setDropdownIcon(() => group.ChildComponent)
} else {
setDropdownIcon(null)
setDropdownIcon(undefined)
}
}, [group])
@@ -77,7 +80,7 @@ function ToolbarGroupComponent({
if (group?.type === 'dropdown' && group.items.length && group.ChildComponent) {
setDropdownIcon(() => group.ChildComponent)
} else {
setDropdownIcon(null)
setDropdownIcon(undefined)
}
return
}

View File

@@ -29,6 +29,9 @@ export const ToolbarButton = ({
const updateStates = useCallback(() => {
editor.getEditorState().read(() => {
const selection = $getSelection()
if (!selection) {
return
}
if (item.isActive) {
const isActive = item.isActive({ editor, editorConfigContext, selection })
if (active !== isActive) {
@@ -85,7 +88,7 @@ export const ToolbarButton = ({
editor.focus(() => {
// We need to wrap the onSelect in the callback, so the editor is properly focused before the onSelect is called.
item.onSelect({
item.onSelect?.({
editor,
isActive: active,
})

View File

@@ -69,9 +69,9 @@ export function DropDownItem({
editor.focus(() => {
// We need to wrap the onSelect in the callback, so the editor is properly focused before the onSelect is called.
item.onSelect({
item.onSelect?.({
editor,
isActive: active,
isActive: active!,
})
})
}

View File

@@ -100,6 +100,9 @@ export const ToolbarDropdown = ({
const updateStates = useCallback(() => {
editor.getEditorState().read(() => {
const selection = $getSelection()
if (!selection) {
return
}
const _activeItemKeys: string[] = []
const _activeItems: ToolbarGroupItem[] = []

View File

@@ -83,7 +83,7 @@ export type ToolbarGroupItem = {
label?:
| ((args: {
i18n: I18nClient<{}, string>
richTextComponentMap: Map<string, React.ReactNode>
richTextComponentMap?: Map<string, React.ReactNode>
}) => string)
| string
/** Each toolbar item needs to have a unique key. */

View File

@@ -286,6 +286,7 @@ export type ServerFeature<ServerProps, ClientFeatureProps> = {
* This determines what props will be available on the Client.
*/
clientFeatureProps?: ClientFeatureProps
// @ts-expect-error - TODO: fix this
componentImports?: Config['admin']['importMap']['generators'][0] | PayloadComponent[]
componentMap?:
| ((args: { i18n: I18nClient; payload: Payload; props: ServerProps; schemaPath: string }) => {

View File

@@ -81,8 +81,8 @@ const Component: React.FC<ElementProps> = (props) => {
const { i18n, t } = useTranslation()
const [cacheBust, dispatchCacheBust] = useReducer((state) => state + 1, 0)
const [relatedCollection] = useState<ClientCollectionConfig>(() =>
collections.find((coll) => coll.slug === relationTo),
const [relatedCollection] = useState<ClientCollectionConfig>(
() => collections.find((coll) => coll.slug === relationTo)!,
)
const componentID = useId()
@@ -107,7 +107,7 @@ const Component: React.FC<ElementProps> = (props) => {
const removeUpload = useCallback(() => {
editor.update(() => {
$getNodeByKey(nodeKey).remove()
$getNodeByKey(nodeKey)?.remove()
})
}, [editor, nodeKey])

View File

@@ -24,6 +24,7 @@ const insertUpload = ({
}) => {
if (!replaceNodeKey) {
editor.dispatchCommand(INSERT_UPLOAD_COMMAND, {
// @ts-expect-error - TODO: fix this
fields: null,
relationTo,
value,
@@ -35,6 +36,7 @@ const insertUpload = ({
node.replace(
$createUploadNode({
data: {
// @ts-expect-error - TODO: fix this
fields: null,
relationTo,
value,

View File

@@ -67,7 +67,7 @@ export const UploadPlugin: PluginComponentWithAnchor<UploadFeaturePropsClient> =
$isParagraphNode(focusNode) &&
focusNode.getTextContentSize() === 0 &&
focusNode
.getParent()
.getParentOrThrow()
.getChildren()
.filter((node) => $isParagraphNode(node)).length > 1
) {

View File

@@ -109,8 +109,8 @@ export const UploadFeature = createServerFeature<
req,
showHiddenFields,
}) => {
// @ts-expect-error
const id = node?.value?.id || node?.value // for backwards-compatibility
// @ts-expect-error - for backwards-compatibility
const id = node?.value?.id || node?.value
if (req?.payload) {
const uploadDocument: {
@@ -141,7 +141,7 @@ export const UploadFeature = createServerFeature<
return `<img />`
}
const url = getAbsoluteURL(uploadDocument?.value?.url, req?.payload)
const url = getAbsoluteURL(uploadDocument?.value?.url ?? '', req?.payload)
/**
* If the upload is not an image, return a link to the upload

View File

@@ -53,7 +53,7 @@ export const uploadValidation = (
siblingData: node?.fields ?? {},
})
let errorPaths = []
let errorPaths: string[] = []
for (const fieldKey in result) {
if (result[fieldKey].errorPaths) {
errorPaths = errorPaths.concat(result[fieldKey].errorPaths)

View File

@@ -34,13 +34,7 @@ const RichTextComponent: React.FC<
field: {
name,
_path: pathFromProps,
admin: {
className,
components: { Description, Error, Label },
readOnly: readOnlyFromAdmin,
style,
width,
} = {},
admin: { className, components, readOnly: readOnlyFromAdmin, style, width } = {},
required,
},
field,
@@ -48,6 +42,9 @@ const RichTextComponent: React.FC<
readOnly: readOnlyFromTopLevelProps,
validate, // Users can pass in client side validation if they WANT to, but it's not required anymore
} = props
const Description = components?.Description
const Error = components?.Error
const Label = components?.Label
const readOnlyFromProps = readOnlyFromTopLevelProps || readOnlyFromAdmin
const memoizedValidate = useCallback(
@@ -65,6 +62,7 @@ const RichTextComponent: React.FC<
const fieldType = useField<SerializedEditorState>({
path: pathFromContext ?? pathFromProps ?? name,
// @ts-expect-error: TODO: Fix this
validate: memoizedValidate,
})
@@ -95,12 +93,12 @@ const RichTextComponent: React.FC<
>
<FieldError
CustomError={Error}
field={field}
path={path}
{...(errorProps || {})}
alignCaret="left"
field={field}
/>
<FieldLabel field={field} Label={Label} {...(labelProps || {})} />
<FieldLabel Label={Label} {...(labelProps || {})} field={field} />
<div className={`${baseClass}__wrap`}>
<ErrorBoundary fallbackRender={fallbackRender} onReset={() => {}}>
<LexicalProvider
@@ -124,7 +122,7 @@ const RichTextComponent: React.FC<
value={value}
/>
</ErrorBoundary>
<FieldDescription Description={Description} field={field} {...(descriptionProps || {})} />
<FieldDescription Description={Description} {...(descriptionProps || {})} field={field} />
</div>
</div>
)

View File

@@ -23,13 +23,13 @@ export const RichTextField: React.FC<LexicalRichTextFieldProps> = (props) => {
} = props
const [finalSanitizedEditorConfig, setFinalSanitizedEditorConfig] =
useState<SanitizedClientEditorConfig>(null)
useState<null | SanitizedClientEditorConfig>(null)
useEffect(() => {
if (finalSanitizedEditorConfig) {
return
}
const clientFeatures: GeneratedFeatureProviderComponent[] = richTextComponentMap.get(
const clientFeatures: GeneratedFeatureProviderComponent[] = richTextComponentMap?.get(
'features',
) as GeneratedFeatureProviderComponent[]

View File

@@ -1,9 +1,5 @@
import type { JSONSchema4 } from 'json-schema'
import type {
EditorConfig as LexicalEditorConfig,
SerializedEditorState,
SerializedLexicalNode,
} from 'lexical'
import type { SerializedEditorState, SerializedLexicalNode } from 'lexical'
import {
afterChangeTraverseFields,
@@ -38,7 +34,7 @@ import { getGenerateSchemaMap } from './utilities/generateSchemaMap.js'
import { recurseNodeTree } from './utilities/recurseNodeTree.js'
import { richTextValidateHOC } from './validate/index.js'
let defaultSanitizedServerEditorConfig: SanitizedServerEditorConfig = null
let defaultSanitizedServerEditorConfig: null | SanitizedServerEditorConfig = null
let checkedDependencies = false
export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapterProvider {
@@ -106,8 +102,7 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte
features = deepCopyObject(defaultEditorFeatures)
}
const lexical: LexicalEditorConfig =
props.lexical ?? deepCopyObjectSimple(defaultEditorConfig.lexical)
const lexical = props.lexical ?? deepCopyObjectSimple(defaultEditorConfig.lexical)!
resolvedFeatureMap = await loadFeatures({
config,
@@ -212,8 +207,8 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte
}
}
if (
!finalSanitizedEditorConfig.features.nodeHooks.afterChange.size &&
!finalSanitizedEditorConfig.features.getSubFields.size
!finalSanitizedEditorConfig.features.nodeHooks?.afterChange?.size &&
!finalSanitizedEditorConfig.features.getSubFields?.size
) {
return value
}
@@ -240,9 +235,10 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte
// eslint-disable-next-line prefer-const
for (let [id, node] of Object.entries(nodeIDMap)) {
const afterChangeHooks = finalSanitizedEditorConfig.features.nodeHooks.afterChange
if (afterChangeHooks?.has(node.type)) {
for (const hook of afterChangeHooks.get(node.type)) {
const afterChangeHooks = finalSanitizedEditorConfig.features.nodeHooks?.afterChange
const afterChangeHooksForNode = afterChangeHooks?.get(node.type)
if (afterChangeHooksForNode) {
for (const hook of afterChangeHooksForNode) {
if (!originalNodeIDMap[id]) {
console.warn(
'(afterChange) No original node found for node with id',
@@ -265,12 +261,12 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte
})
}
}
const subFieldFn = finalSanitizedEditorConfig.features.getSubFields.get(node.type)
const subFieldDataFn = finalSanitizedEditorConfig.features.getSubFieldsData.get(
const subFieldFn = finalSanitizedEditorConfig.features.getSubFields?.get(node.type)
const subFieldDataFn = finalSanitizedEditorConfig.features.getSubFieldsData?.get(
node.type,
)
if (subFieldFn) {
if (subFieldFn && subFieldDataFn) {
const subFields = subFieldFn({ node, req })
const data = subFieldDataFn({ node, req }) ?? {}
const originalData = subFieldDataFn({ node: originalNodeIDMap[id], req }) ?? {}
@@ -333,8 +329,8 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte
}
if (
!finalSanitizedEditorConfig.features.nodeHooks.afterRead.size &&
!finalSanitizedEditorConfig.features.getSubFields.size
!finalSanitizedEditorConfig.features.nodeHooks?.afterRead?.size &&
!finalSanitizedEditorConfig.features.getSubFields?.size
) {
return value
}
@@ -346,37 +342,38 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte
})
for (let node of flattenedNodes) {
const afterReadHooks = finalSanitizedEditorConfig.features.nodeHooks.afterRead
if (afterReadHooks?.has(node.type)) {
for (const hook of afterReadHooks.get(node.type)) {
const afterReadHooks = finalSanitizedEditorConfig.features.nodeHooks?.afterRead
const afterReadHooksForNode = afterReadHooks?.get(node.type)
if (afterReadHooksForNode) {
for (const hook of afterReadHooksForNode) {
node = await hook({
context,
currentDepth,
depth,
draft,
fallbackLocale,
fieldPromises,
findMany,
flattenLocales,
locale,
currentDepth: currentDepth!,
depth: depth!,
draft: draft!,
fallbackLocale: fallbackLocale!,
fieldPromises: fieldPromises!,
findMany: findMany!,
flattenLocales: flattenLocales!,
locale: locale!,
node,
overrideAccess,
overrideAccess: overrideAccess!,
parentRichTextFieldPath: path,
parentRichTextFieldSchemaPath: schemaPath,
populationPromises,
populationPromises: populationPromises!,
req,
showHiddenFields,
triggerAccessControl,
triggerHooks,
showHiddenFields: showHiddenFields!,
triggerAccessControl: triggerAccessControl!,
triggerHooks: triggerHooks!,
})
}
}
const subFieldFn = finalSanitizedEditorConfig.features.getSubFields.get(node.type)
const subFieldDataFn = finalSanitizedEditorConfig.features.getSubFieldsData.get(
const subFieldFn = finalSanitizedEditorConfig.features.getSubFields?.get(node.type)
const subFieldDataFn = finalSanitizedEditorConfig.features.getSubFieldsData?.get(
node.type,
)
if (subFieldFn) {
if (subFieldFn && subFieldDataFn) {
const subFields = subFieldFn({ node, req })
const data = subFieldDataFn({ node, req }) ?? {}
@@ -384,23 +381,23 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte
afterReadTraverseFields({
collection,
context,
currentDepth,
depth,
currentDepth: currentDepth!,
depth: depth!,
doc: data,
draft,
fallbackLocale,
fieldPromises,
draft: draft!,
fallbackLocale: fallbackLocale!,
fieldPromises: fieldPromises!,
fields: subFields,
findMany,
flattenLocales,
findMany: findMany!,
flattenLocales: flattenLocales!,
global,
locale,
overrideAccess,
locale: locale!,
overrideAccess: overrideAccess!,
path,
populationPromises,
populationPromises: populationPromises!,
req,
schemaPath,
showHiddenFields,
showHiddenFields: showHiddenFields!,
siblingDoc: data,
triggerAccessControl,
triggerHooks,
@@ -438,8 +435,8 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte
}
if (
!finalSanitizedEditorConfig.features.nodeHooks.beforeChange.size &&
!finalSanitizedEditorConfig.features.getSubFields.size
!finalSanitizedEditorConfig.features.nodeHooks?.beforeChange?.size &&
!finalSanitizedEditorConfig.features.getSubFields?.size
) {
return value
}
@@ -469,7 +466,7 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte
nodes: (value as SerializedEditorState)?.root?.children ?? [],
})
if (siblingDocWithLocales?.[field.name]) {
if (field.name && siblingDocWithLocales?.[field.name]) {
recurseNodeTree({
nodeIDMap: originalNodeWithLocalesIDMap,
nodes:
@@ -480,9 +477,10 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte
// eslint-disable-next-line prefer-const
for (let [id, node] of Object.entries(nodeIDMap)) {
const beforeChangeHooks = finalSanitizedEditorConfig.features.nodeHooks.beforeChange
if (beforeChangeHooks?.has(node.type)) {
for (const hook of beforeChangeHooks.get(node.type)) {
const beforeChangeHooks = finalSanitizedEditorConfig.features.nodeHooks?.beforeChange
const beforeChangeHooksForNode = beforeChangeHooks?.get(node.type)
if (beforeChangeHooksForNode) {
for (const hook of beforeChangeHooksForNode) {
if (!originalNodeIDMap[id]) {
console.warn(
'(beforeChange) No original node found for node with id',
@@ -496,26 +494,26 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte
}
node = await hook({
context,
errors,
mergeLocaleActions,
errors: errors!,
mergeLocaleActions: mergeLocaleActions!,
node,
operation,
operation: operation!,
originalNode: originalNodeIDMap[id],
originalNodeWithLocales: originalNodeWithLocalesIDMap[id],
parentRichTextFieldPath: path,
parentRichTextFieldSchemaPath: schemaPath,
req,
skipValidation,
skipValidation: skipValidation!,
})
}
}
const subFieldFn = finalSanitizedEditorConfig.features.getSubFields.get(node.type)
const subFieldDataFn = finalSanitizedEditorConfig.features.getSubFieldsData.get(
const subFieldFn = finalSanitizedEditorConfig.features.getSubFields?.get(node.type)
const subFieldDataFn = finalSanitizedEditorConfig.features.getSubFieldsData?.get(
node.type,
)
if (subFieldFn) {
if (subFieldFn && subFieldDataFn) {
const subFields = subFieldFn({ node, req })
const data = subFieldDataFn({ node, req }) ?? {}
const originalData = subFieldDataFn({ node: originalNodeIDMap[id], req }) ?? {}
@@ -533,11 +531,11 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte
data,
doc: originalData,
docWithLocales: originalDataWithLocales ?? {},
errors,
errors: errors!,
fields: subFields,
global,
mergeLocaleActions,
operation,
mergeLocaleActions: mergeLocaleActions!,
operation: operation!,
path,
req,
schemaPath,
@@ -564,7 +562,7 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte
[key: string]: SerializedLexicalNode
} = {}
const previousValue = siblingData[field.name]
const previousValue = siblingData[field.name!]
recurseNodeTree({
nodeIDMap: newOriginalNodeIDMap,
@@ -607,10 +605,10 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte
// return value if there are NO hooks
if (
!finalSanitizedEditorConfig.features.nodeHooks.beforeValidate.size &&
!finalSanitizedEditorConfig.features.nodeHooks.afterChange.size &&
!finalSanitizedEditorConfig.features.nodeHooks.beforeChange.size &&
!finalSanitizedEditorConfig.features.getSubFields.size
!finalSanitizedEditorConfig.features.nodeHooks?.beforeValidate?.size &&
!finalSanitizedEditorConfig.features.nodeHooks?.afterChange?.size &&
!finalSanitizedEditorConfig.features.nodeHooks?.beforeChange?.size &&
!finalSanitizedEditorConfig.features.getSubFields?.size
) {
return value
}
@@ -662,7 +660,7 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte
/**
* Now that the maps for all hooks are set up, we can run the validate hook
*/
if (!finalSanitizedEditorConfig.features.nodeHooks.beforeValidate.size) {
if (!finalSanitizedEditorConfig.features.nodeHooks?.beforeValidate?.size) {
return value
}
const nodeIDMap: {
@@ -678,8 +676,9 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte
for (let [id, node] of Object.entries(nodeIDMap)) {
const beforeValidateHooks =
finalSanitizedEditorConfig.features.nodeHooks.beforeValidate
if (beforeValidateHooks?.has(node.type)) {
for (const hook of beforeValidateHooks.get(node.type)) {
const beforeValidateHooksForNode = beforeValidateHooks?.get(node.type)
if (beforeValidateHooksForNode) {
for (const hook of beforeValidateHooksForNode) {
if (!originalNodeIDMap[id]) {
console.warn(
'(beforeValidate) No original node found for node with id',
@@ -696,19 +695,19 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte
node,
operation,
originalNode: originalNodeIDMap[id],
overrideAccess,
overrideAccess: overrideAccess!,
parentRichTextFieldPath: path,
parentRichTextFieldSchemaPath: schemaPath,
req,
})
}
}
const subFieldFn = finalSanitizedEditorConfig.features.getSubFields.get(node.type)
const subFieldDataFn = finalSanitizedEditorConfig.features.getSubFieldsData.get(
const subFieldFn = finalSanitizedEditorConfig.features.getSubFields?.get(node.type)
const subFieldDataFn = finalSanitizedEditorConfig.features.getSubFieldsData?.get(
node.type,
)
if (subFieldFn) {
if (subFieldFn && subFieldDataFn) {
const subFields = subFieldFn({ node, req })
const data = subFieldDataFn({ node, req }) ?? {}
const originalData = subFieldDataFn({ node: originalNodeIDMap[id], req }) ?? {}
@@ -723,7 +722,7 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte
fields: subFields,
global,
operation,
overrideAccess,
overrideAccess: overrideAccess!,
path,
req,
schemaPath,

View File

@@ -8,11 +8,12 @@ export const EditorPlugin: React.FC<{
clientProps: unknown
plugin: SanitizedPlugin
}> = ({ anchorElem, clientProps, plugin }) => {
if (plugin.position === 'floatingAnchorElem') {
if (plugin.position === 'floatingAnchorElem' && anchorElem) {
return (
plugin.Component && <plugin.Component anchorElem={anchorElem} clientProps={clientProps} />
)
}
// @ts-expect-error - ts is not able to infer that plugin.Component is of type PluginComponent
return plugin.Component && <plugin.Component clientProps={clientProps} />
}

View File

@@ -22,7 +22,7 @@ import { LexicalContentEditable } from './ui/ContentEditable.js'
export const LexicalEditor: React.FC<
{
editorContainerRef: React.RefObject<HTMLDivElement>
editorContainerRef: React.RefObject<HTMLDivElement | null>
} & Pick<LexicalProviderProps, 'editorConfig' | 'onChange'>
> = (props) => {
const { editorConfig, editorContainerRef, onChange } = props
@@ -98,13 +98,13 @@ export const LexicalEditor: React.FC<
return (
<React.Fragment>
{editorConfig.features.plugins.map((plugin) => {
{editorConfig.features.plugins?.map((plugin) => {
if (plugin.position === 'aboveContainer') {
return <EditorPlugin clientProps={plugin.clientProps} key={plugin.key} plugin={plugin} />
}
})}
<div className="editor-container" ref={editorContainerRef}>
{editorConfig.features.plugins.map((plugin) => {
{editorConfig.features.plugins?.map((plugin) => {
if (plugin.position === 'top') {
return (
<EditorPlugin clientProps={plugin.clientProps} key={plugin.key} plugin={plugin} />
@@ -143,7 +143,7 @@ export const LexicalEditor: React.FC<
<AddBlockHandlePlugin anchorElem={floatingAnchorElem} />
</React.Fragment>
)}
{editorConfig.features.plugins.map((plugin) => {
{editorConfig.features.plugins?.map((plugin) => {
if (
plugin.position === 'floatingAnchorElem' &&
!(plugin.desktopOnly === true && isSmallWidthViewport)
@@ -173,14 +173,14 @@ export const LexicalEditor: React.FC<
)}
<TabIndentationPlugin />
{editorConfig.features.plugins.map((plugin) => {
{editorConfig.features.plugins?.map((plugin) => {
if (plugin.position === 'normal') {
return (
<EditorPlugin clientProps={plugin.clientProps} key={plugin.key} plugin={plugin} />
)
}
})}
{editorConfig.features.plugins.map((plugin) => {
{editorConfig.features.plugins?.map((plugin) => {
if (plugin.position === 'bottom') {
return (
<EditorPlugin clientProps={plugin.clientProps} key={plugin.key} plugin={plugin} />
@@ -188,7 +188,7 @@ export const LexicalEditor: React.FC<
}
})}
</div>
{editorConfig.features.plugins.map((plugin) => {
{editorConfig.features.plugins?.map((plugin) => {
if (plugin.position === 'belowContainer') {
return <EditorPlugin clientProps={plugin.clientProps} key={plugin.key} plugin={plugin} />
}

View File

@@ -31,6 +31,7 @@ export interface EditorConfigContextType {
uuid: string
}
// @ts-expect-error: TODO: Fix this
const Context: React.Context<EditorConfigContextType> = createContext({
editorConfig: null,
field: null,
@@ -46,7 +47,7 @@ export const EditorConfigProvider = ({
}: {
children: React.ReactNode
editorConfig: SanitizedClientEditorConfig
editorContainerRef: React.RefObject<HTMLDivElement>
editorContainerRef: React.RefObject<HTMLDivElement | null>
field: LexicalRichTextFieldProps['field']
parentContext?: EditorConfigContextType
}): React.ReactNode => {
@@ -87,7 +88,7 @@ export const EditorConfigProvider = ({
if (parentContext?.uuid) {
parentContext.focusEditor(editorContext)
}
childrenEditors.current.forEach((childEditor, childUUID) => {
childrenEditors.current.forEach((childEditor) => {
childEditor.focusEditor(editorContext)
})

View File

@@ -57,7 +57,7 @@ export const sanitizeClientFeatures = (
}
if (feature.plugins?.length) {
feature.plugins.forEach((plugin, i) => {
sanitized.plugins.push({
sanitized.plugins?.push({
clientProps: feature.sanitizedClientFeatureProps,
Component: plugin.Component,
key: feature.key + i,
@@ -219,7 +219,7 @@ export function sanitizeClientEditorConfig(
return {
admin,
features: sanitizeClientFeatures(resolvedClientFeatureMap),
lexical,
lexical: lexical!,
resolvedFeatureMap: resolvedClientFeatureMap,
}
}

View File

@@ -188,9 +188,9 @@ export async function loadFeatures({
: featureProvider.feature
resolvedFeatures.set(featureProvider.key, {
...feature,
dependencies: featureProvider.dependencies,
dependenciesPriority: featureProvider.dependenciesPriority,
dependenciesSoft: featureProvider.dependenciesSoft,
dependencies: featureProvider.dependencies!,
dependenciesPriority: featureProvider.dependenciesPriority!,
dependenciesSoft: featureProvider.dependenciesSoft!,
key: featureProvider.key,
order: loaded,
})

View File

@@ -51,18 +51,20 @@ export const sanitizeServerFeatures = (
}
if (feature?.hooks?.beforeValidate?.length) {
sanitized.hooks.beforeValidate = sanitized.hooks.beforeValidate.concat(
sanitized.hooks.beforeValidate = sanitized.hooks.beforeValidate?.concat(
feature.hooks.beforeValidate,
)
}
if (feature?.hooks?.beforeChange?.length) {
sanitized.hooks.beforeChange = sanitized.hooks.beforeChange.concat(feature.hooks.beforeChange)
sanitized.hooks.beforeChange = sanitized.hooks.beforeChange?.concat(
feature.hooks.beforeChange,
)
}
if (feature?.hooks?.afterRead?.length) {
sanitized.hooks.afterRead = sanitized.hooks.afterRead.concat(feature.hooks.afterRead)
sanitized.hooks.afterRead = sanitized.hooks.afterRead?.concat(feature.hooks.afterRead)
}
if (feature?.hooks?.afterChange?.length) {
sanitized.hooks.afterChange = sanitized.hooks.afterChange.concat(feature.hooks.afterChange)
sanitized.hooks.afterChange = sanitized.hooks.afterChange?.concat(feature.hooks.afterChange)
}
if (feature?.i18n) {
@@ -90,22 +92,22 @@ export const sanitizeServerFeatures = (
sanitized.converters.html.push(node.converters.html)
}
if (node?.hooks?.afterChange) {
sanitized.nodeHooks.afterChange.set(nodeType, node.hooks.afterChange)
sanitized.nodeHooks?.afterChange?.set(nodeType, node.hooks.afterChange)
}
if (node?.hooks?.afterRead) {
sanitized.nodeHooks.afterRead.set(nodeType, node.hooks.afterRead)
sanitized.nodeHooks?.afterRead?.set(nodeType, node.hooks.afterRead)
}
if (node?.hooks?.beforeChange) {
sanitized.nodeHooks.beforeChange.set(nodeType, node.hooks.beforeChange)
sanitized.nodeHooks?.beforeChange?.set(nodeType, node.hooks.beforeChange)
}
if (node?.hooks?.beforeValidate) {
sanitized.nodeHooks.beforeValidate.set(nodeType, node.hooks.beforeValidate)
sanitized.nodeHooks?.beforeValidate?.set(nodeType, node.hooks.beforeValidate)
}
if (node?.getSubFields) {
sanitized.getSubFields.set(nodeType, node.getSubFields)
sanitized.getSubFields?.set(nodeType, node.getSubFields)
}
if (node?.getSubFieldsData) {
sanitized.getSubFieldsData.set(nodeType, node.getSubFieldsData)
sanitized.getSubFieldsData?.set(nodeType, node.getSubFieldsData)
}
})
}
@@ -129,13 +131,13 @@ export async function sanitizeServerEditorConfig(
): Promise<SanitizedServerEditorConfig> {
const resolvedFeatureMap = await loadFeatures({
config,
parentIsLocalized,
parentIsLocalized: parentIsLocalized!,
unSanitizedEditorConfig: editorConfig,
})
return {
features: sanitizeServerFeatures(resolvedFeatureMap),
lexical: editorConfig.lexical,
lexical: editorConfig.lexical!,
resolvedFeatureMap,
}
}

View File

@@ -64,8 +64,8 @@ function tryToPositionRange(leadOffset: number, range: Range, editorWindow: Wind
return true
}
function getQueryTextForSearch(editor: LexicalEditor): null | string {
let text = null
function getQueryTextForSearch(editor: LexicalEditor): string | undefined {
let text
editor.getEditorState().read(() => {
const selection = $getSelection()
if (!$isRangeSelection(selection)) {
@@ -207,7 +207,7 @@ export function LexicalTypeaheadMenuPlugin({
if (
!$isRangeSelection(selection) ||
!selection.isCollapsed() ||
text === null ||
text === undefined ||
range === null
) {
closeTypeahead()

View File

@@ -18,7 +18,7 @@ export type SlashMenuItem = {
label?:
| ((args: {
i18n: I18nClient<{}, string>
richTextComponentMap: Map<string, React.ReactNode>
richTextComponentMap?: Map<string, React.ReactNode>
schemaPath: string
}) => string)
| string

View File

@@ -101,11 +101,13 @@ export function SlashMenuPlugin({
let groupWithItems: Array<SlashMenuGroup> = []
for (const dynamicItem of editorConfig.features.slashMenu.dynamicGroups) {
const dynamicGroupWithItems = dynamicItem({
editor,
queryString,
})
groupWithItems = groupWithItems.concat(dynamicGroupWithItems)
if (queryString) {
const dynamicGroupWithItems = dynamicItem({
editor,
queryString,
})
groupWithItems = groupWithItems.concat(dynamicGroupWithItems)
}
}
return groupWithItems
@@ -119,6 +121,7 @@ export function SlashMenuPlugin({
if (queryString) {
// Filter current groups first
// @ts-expect-error - TODO: fix this
groupsWithItems = groupsWithItems.map((group) => {
const filteredItems = group.items.filter((item) => {
let itemTitle = item.key
@@ -207,7 +210,7 @@ export function SlashMenuPlugin({
<div className={baseClass}>
{groups.map((group) => {
let groupTitle = group.key
if (group.label) {
if (group.label && richTextComponentMap) {
groupTitle =
typeof group.label === 'function'
? group.label({ i18n, richTextComponentMap, schemaPath })

View File

@@ -93,7 +93,10 @@ function useAddBlockHandle(
if (!_emptyBlockElem) {
return
}
if (hoveredElement?.node !== blockNode || hoveredElement?.elem !== _emptyBlockElem) {
if (
blockNode &&
(hoveredElement?.node !== blockNode || hoveredElement?.elem !== _emptyBlockElem)
) {
setHoveredElement({
elem: _emptyBlockElem,
node: blockNode,
@@ -134,7 +137,7 @@ function useAddBlockHandle(
// Check if blockNode is an empty text node
let isEmptyParagraph = true
if (
hoveredElementToUse.node.getType() !== 'paragraph' ||
hoveredElementToUse?.node.getType() !== 'paragraph' ||
hoveredElementToUse.node.getTextContent() !== ''
) {
isEmptyParagraph = false
@@ -142,11 +145,11 @@ function useAddBlockHandle(
if (!isEmptyParagraph) {
const newParagraph = $createParagraphNode()
hoveredElementToUse.node.insertAfter(newParagraph)
hoveredElementToUse?.node.insertAfter(newParagraph)
setTimeout(() => {
hoveredElementToUse = {
elem: editor.getElementByKey(newParagraph.getKey()),
elem: editor.getElementByKey(newParagraph.getKey())!,
node: newParagraph,
}
setHoveredElement(hoveredElementToUse)
@@ -160,7 +163,7 @@ function useAddBlockHandle(
editor.focus()
if (
hoveredElementToUse.node &&
hoveredElementToUse?.node &&
'select' in hoveredElementToUse.node &&
typeof hoveredElementToUse.node.select === 'function'
) {
@@ -173,7 +176,7 @@ function useAddBlockHandle(
// Otherwise, this won't work
setTimeout(() => {
editor.dispatchCommand(ENABLE_SLASH_MENU_COMMAND, {
node: hoveredElementToUse.node as ParagraphNode,
node: hoveredElementToUse?.node as ParagraphNode,
})
}, 2)

View File

@@ -1,6 +1,6 @@
'use client'
export function debounce(func: Function, wait: number) {
let timeout: null | number = null
export function debounce(func: (...args: any[]) => void, wait: number) {
let timeout
return function (...args: any[]) {
const later = () => {
clearTimeout(timeout)

View File

@@ -9,7 +9,7 @@ export function getBoundingClientRectWithoutTransform(elem: HTMLElement): DOMRec
}
const lastNumberOfTransformValue = transformValue.split(',').pop()
rect.y = rect.y - Number(lastNumberOfTransformValue.replace(')', ''))
rect.y = rect.y - Number(lastNumberOfTransformValue?.replace(')', ''))
// Return the original bounding rect if no translation is applied
return rect

View File

@@ -80,7 +80,7 @@ function useDraggableBlockMenu(
boundingBox?: DOMRect
elem: HTMLElement | null
isBelow: boolean
}>(null)
} | null>(null)
const { editorConfig } = useEditorConfigContext()
@@ -223,7 +223,7 @@ function useDraggableBlockMenu(
: -(menuRef?.current?.getBoundingClientRect()?.width ?? 0)),
targetLineElem,
targetBlockElem,
lastTargetBlock,
lastTargetBlock!,
pageY,
anchorElem,
event,
@@ -243,8 +243,8 @@ function useDraggableBlockMenu(
isBelow,
})
}
} else {
hideTargetLine(targetLineElem, lastTargetBlock?.elem)
} else if (lastTargetBlock?.elem) {
hideTargetLine(targetLineElem, lastTargetBlock.elem)
setLastTargetBlock({
boundingBox: targetBlockElem.getBoundingClientRect(),
elem: targetBlockElem,
@@ -343,8 +343,10 @@ function useDraggableBlockMenu(
setTimeout(() => {
// add new temp html element to newInsertedElem with the same height and width and the class block-selected
// to highlight the new inserted element
const newInsertedElemRect = newInsertedElem.getBoundingClientRect()
const newInsertedElemRect = newInsertedElem?.getBoundingClientRect()
if (!newInsertedElemRect) {
return
}
const highlightElem = document.createElement('div')
highlightElem.className = 'lexical-block-highlighter'
@@ -414,7 +416,9 @@ function useDraggableBlockMenu(
function onDragEnd(): void {
isDraggingBlockRef.current = false
hideTargetLine(targetLineRef.current, lastTargetBlock?.elem)
if (lastTargetBlock?.elem) {
hideTargetLine(targetLineRef.current, lastTargetBlock?.elem)
}
}
return createPortal(

View File

@@ -63,7 +63,6 @@ export function getNodeCloseToPoint(props: Props): Output {
point: { x, y },
startIndex = 0,
useEdgeAsDefault = false,
verbose = false,
} = props
// Use cache
@@ -104,12 +103,12 @@ export function getNodeCloseToPoint(props: Props): Output {
editor.getElementByKey(topLevelNodeKeys[topLevelNodeKeys.length - 1]),
]
const [firstNodeRect, lastNodeRect] = [
getBoundingClientRectWithoutTransform(firstNode),
getBoundingClientRectWithoutTransform(lastNode),
]
if (firstNode && lastNode) {
const [firstNodeRect, lastNodeRect] = [
getBoundingClientRectWithoutTransform(firstNode),
getBoundingClientRectWithoutTransform(lastNode),
]
if (firstNodeRect && lastNodeRect) {
if (y < firstNodeRect.top) {
closestBlockElem.blockElem = firstNode
closestBlockElem.distance = firstNodeRect.top - y

View File

@@ -16,7 +16,7 @@ export function setFloatingElemPosition(args: {
specialHandlingForCaret?: boolean
targetRect: ClientRect | null
verticalGap?: number
}): number {
}): number | undefined {
const {
alwaysDisplayOnTop = false,
anchorElem,

View File

@@ -28,7 +28,7 @@ export const populate = async ({
collectionSlug: string
id: number | string
} & Arguments): Promise<void> => {
const shouldPopulate = depth && currentDepth <= depth
const shouldPopulate = depth && currentDepth! <= depth
// usually depth is checked within recursivelyPopulateFieldsForGraphQL. But since this populate function can be called outside of that (in rest afterRead node hooks) we need to check here too
if (!shouldPopulate) {
return
@@ -36,18 +36,18 @@ export const populate = async ({
const dataRef = data as Record<string, unknown>
const doc = await req.payloadDataLoader.load(
const doc = await req.payloadDataLoader?.load(
createDataloaderCacheKey({
collectionSlug,
currentDepth: currentDepth + 1,
currentDepth: currentDepth! + 1,
depth,
docID: id as string,
draft,
fallbackLocale: req.fallbackLocale,
locale: req.locale,
fallbackLocale: req.fallbackLocale!,
locale: req.locale!,
overrideAccess,
showHiddenFields,
transactionID: req.transactionID,
transactionID: req.transactionID!,
}),
)

View File

@@ -8,7 +8,9 @@ import { recurseNodes } from '../utilities/forEachNodeRecursively.js'
export type Args = {
editorPopulationPromises: Map<string, Array<PopulationPromise>>
} & Parameters<RichTextAdapter<SerializedEditorState, AdapterProps>['graphQLPopulationPromises']>[0]
} & Parameters<
NonNullable<RichTextAdapter<SerializedEditorState, AdapterProps>['graphQLPopulationPromises']>
>[0]
/**
* Appends all new populationPromises to the populationPromises prop
@@ -29,7 +31,7 @@ export const populateLexicalPopulationPromises = ({
showHiddenFields,
siblingDoc,
}: Args) => {
const shouldPopulate = depth && currentDepth <= depth
const shouldPopulate = depth && currentDepth! <= depth
if (!shouldPopulate) {
return
@@ -37,11 +39,12 @@ export const populateLexicalPopulationPromises = ({
recurseNodes({
callback: (node) => {
if (editorPopulationPromises?.has(node.type)) {
for (const promise of editorPopulationPromises.get(node.type)) {
const editorPopulationPromisesOfNodeType = editorPopulationPromises?.get(node.type)
if (editorPopulationPromisesOfNodeType) {
for (const promise of editorPopulationPromisesOfNodeType) {
promise({
context,
currentDepth,
currentDepth: currentDepth!,
depth,
draft,
editorPopulationPromises,
@@ -50,7 +53,7 @@ export const populateLexicalPopulationPromises = ({
findMany,
flattenLocales,
node,
overrideAccess,
overrideAccess: overrideAccess!,
populationPromises,
req,
showHiddenFields,

View File

@@ -51,13 +51,13 @@ export const recursivelyPopulateFieldsForGraphQL = ({
depth,
doc: data as any, // Looks like it's only needed for hooks and access control, so doesn't matter what we pass here right now
draft,
fallbackLocale: req.fallbackLocale,
fallbackLocale: req.fallbackLocale!,
fieldPromises,
fields,
findMany,
flattenLocales,
global: null, // Pass from core? This is only needed for hooks, so we can leave this null for now
locale: req.locale,
locale: req.locale!,
overrideAccess,
path: [],
populationPromises, // This is not the same as populationPromises passed into this recurseNestedFields. These are just promises resolved at the very end.

View File

@@ -1,5 +1,4 @@
'use client'
import type { FormProps } from '@payloadcms/ui'
import type { ClientField, FormState } from 'payload'
import {
@@ -42,14 +41,14 @@ export const DrawerContent: React.FC<Omit<FieldsDrawerProps, 'drawerSlug' | 'dra
`${schemaPath}.lexical_internal_feature.${featureKey}${schemaPathSuffix ? `.${schemaPathSuffix}` : ''}`
const fields: any =
fieldMapOverride ?? (richTextComponentMap.get(componentMapRenderedFieldsPath) as ClientField[]) // Field Schema
fieldMapOverride ?? (richTextComponentMap?.get(componentMapRenderedFieldsPath) as ClientField[]) // Field Schema
useEffect(() => {
const awaitInitialState = async () => {
const { state } = await getFormState({
apiRoute: config.routes.api,
body: {
id,
id: id!,
data: data ?? {},
operation: 'update',
schemaPath: schemaFieldsPath,
@@ -63,12 +62,12 @@ export const DrawerContent: React.FC<Omit<FieldsDrawerProps, 'drawerSlug' | 'dra
void awaitInitialState()
}, [config.routes.api, config.serverURL, schemaFieldsPath, id, data])
const onChange: FormProps['onChange'][0] = useCallback(
const onChange = useCallback(
async ({ formState: prevFormState }) => {
const { state } = await getFormState({
apiRoute: config.routes.api,
body: {
id,
id: id!,
formState: prevFormState,
operation: 'update',
schemaPath: schemaFieldsPath,

View File

@@ -42,6 +42,7 @@ export const getGenerateComponentMap =
for (const componentKey in components) {
const payloadComponent = components[componentKey]
// @ts-expect-error - TODO: fix this
const mappedComponent: MappedComponent = createMappedComponent(
payloadComponent,
{

View File

@@ -2,13 +2,16 @@ import type { Field } from 'payload'
import { fieldAffectsData, fieldHasSubFields, fieldIsArrayType, tabHasName } from 'payload/shared'
import type { SlateNodeConverter } from '../../features/migrations/slateToLexical/converter/types.js'
import type {
SlateNode,
SlateNodeConverter,
} from '../../features/migrations/slateToLexical/converter/types.js'
import type { LexicalRichTextAdapter } from '../../types.js'
import { convertSlateToLexical } from '../../features/migrations/slateToLexical/converter/index.js'
type NestedRichTextFieldsArgs = {
data: unknown
data: Record<string, unknown>
fields: Field[]
found: number
@@ -23,7 +26,7 @@ export const migrateDocumentFieldsRecursively = ({
if (fieldHasSubFields(field) && !fieldIsArrayType(field)) {
if (fieldAffectsData(field) && typeof data[field.name] === 'object') {
found += migrateDocumentFieldsRecursively({
data: data[field.name],
data: data[field.name] as Record<string, unknown>,
fields: field.fields,
found,
})
@@ -37,18 +40,18 @@ export const migrateDocumentFieldsRecursively = ({
} else if (field.type === 'tabs') {
field.tabs.forEach((tab) => {
found += migrateDocumentFieldsRecursively({
data: tabHasName(tab) ? data[tab.name] : data,
data: (tabHasName(tab) ? data[tab.name] : data) as Record<string, unknown>,
fields: tab.fields,
found,
})
})
} else if (Array.isArray(data[field.name])) {
if (field.type === 'blocks') {
data[field.name].forEach((row, i) => {
;(data[field.name] as Array<Record<string, unknown>>).forEach((row, i) => {
const block = field.blocks.find(({ slug }) => slug === row?.blockType)
if (block) {
found += migrateDocumentFieldsRecursively({
data: data[field.name][i],
data: (data[field.name] as Array<Record<string, unknown>>)[i],
fields: block.fields,
found,
})
@@ -57,9 +60,9 @@ export const migrateDocumentFieldsRecursively = ({
}
if (field.type === 'array') {
data[field.name].forEach((_, i) => {
;(data[field.name] as Array<Record<string, unknown>>).forEach((_, i) => {
found += migrateDocumentFieldsRecursively({
data: data[field.name][i],
data: (data[field.name] as Array<Record<string, unknown>>)[i],
fields: field.fields,
found,
})
@@ -82,8 +85,8 @@ export const migrateDocumentFieldsRecursively = ({
}
data[field.name] = convertSlateToLexical({
converters,
slateData: data[field.name],
converters: converters!,
slateData: data[field.name] as SlateNode[],
})
found++

View File

@@ -9,7 +9,7 @@ import type { LexicalRichTextAdapter } from '../../types.js'
import { getEnabledNodes } from '../../lexical/nodes/index.js'
type NestedRichTextFieldsArgs = {
data: unknown
data: Record<string, unknown>
fields: Field[]
found: number
@@ -24,7 +24,7 @@ export const upgradeDocumentFieldsRecursively = ({
if (fieldHasSubFields(field) && !fieldIsArrayType(field)) {
if (fieldAffectsData(field) && typeof data[field.name] === 'object') {
found += upgradeDocumentFieldsRecursively({
data: data[field.name],
data: data[field.name] as Record<string, unknown>,
fields: field.fields,
found,
})
@@ -38,18 +38,18 @@ export const upgradeDocumentFieldsRecursively = ({
} else if (field.type === 'tabs') {
field.tabs.forEach((tab) => {
found += upgradeDocumentFieldsRecursively({
data: tabHasName(tab) ? data[tab.name] : data,
data: (tabHasName(tab) ? data[tab.name] : data) as Record<string, unknown>,
fields: tab.fields,
found,
})
})
} else if (Array.isArray(data[field.name])) {
if (field.type === 'blocks') {
data[field.name].forEach((row, i) => {
;(data[field.name] as Record<string, unknown>[]).forEach((row, i) => {
const block = field.blocks.find(({ slug }) => slug === row?.blockType)
if (block) {
found += upgradeDocumentFieldsRecursively({
data: data[field.name][i],
data: (data[field.name] as Record<string, unknown>[])[i],
fields: block.fields,
found,
})
@@ -58,9 +58,9 @@ export const upgradeDocumentFieldsRecursively = ({
}
if (field.type === 'array') {
data[field.name].forEach((_, i) => {
;(data[field.name] as Record<string, unknown>[]).forEach((_, i) => {
found += upgradeDocumentFieldsRecursively({
data: data[field.name][i],
data: (data[field.name] as Record<string, unknown>[])[i],
fields: field.fields,
found,
})
@@ -72,14 +72,14 @@ export const upgradeDocumentFieldsRecursively = ({
field.type === 'richText' &&
data[field.name] &&
!Array.isArray(data[field.name]) &&
'root' in data[field.name]
'root' in (data[field.name] as Record<string, unknown>)
) {
// Lexical richText
const editor: LexicalRichTextAdapter = field.editor as LexicalRichTextAdapter
if (editor && typeof editor === 'object') {
if ('features' in editor && editor.features?.length) {
// Load lexical editor into lexical, then save it immediately
const editorState: SerializedEditorState = data[field.name]
const editorState = data[field.name] as SerializedEditorState
const headlessEditor = createHeadlessEditor({
nodes: getEnabledNodes({

View File

@@ -17,12 +17,8 @@ export async function validateNodes({
}): Promise<string | true> {
for (const node of nodes) {
// Validate node
if (
nodeValidations &&
typeof nodeValidations?.has === 'function' &&
nodeValidations?.has(node.type)
) {
const validations = nodeValidations.get(node.type)
const validations = nodeValidations.get(node.type)
if (validations) {
for (const validation of validations) {
const validationResult = await validation({
node,

View File

@@ -5,6 +5,7 @@
"noEmit": false /* Do not emit outputs. */,
"emitDeclarationOnly": true,
"esModuleInterop": true,
"strictNullChecks": true,
"outDir": "./dist" /* Specify an output folder for all emitted files. */,
"rootDir": "./src" /* Specify the root folder within your source files. */,
},

View File

@@ -55,6 +55,7 @@ export const NullifyLocaleField: React.FC<NullifyLocaleFieldProps> = ({
<CheckboxField
checked={checked}
field={{
name: '',
label: t('general:fallbackToDefaultLocale'),
}}
id={`field-${path.replace(/\./g, '__')}`}