payloadcms / payload

Payload is the open-source, fullstack Next.js framework, giving you instant backend superpowers. Get a full TypeScript backend and admin panel instantly. Use Payload as a headless CMS or for building powerful applications.
https://payloadcms.com
MIT License
28.89k stars 1.79k forks source link

"Add Link" button in slate editor returns error on v2.30.1 and above #8931

Open Sibbern opened 1 month ago

Sibbern commented 1 month ago

Link to reproduction

No response

Describe the Bug

When running payload version 2.30.1 or above and using the rich text slate editor plugin, the "Add Link" button to add link to text does not work and produces the following error:

Uncaught (in promise) TypeError: openModal is not a function
    at index.tsx:106:13
    at step (index.tsx:1:1)
    at Object.next (index.tsx:1:1)
    at asyncGeneratorStep (index.tsx:1:1)
    at _next (index.tsx:1:1)
    at index.tsx:1:1
    at new Promise (<anonymous>)
    at index.tsx:1:1
    at HTMLUnknownElement.callCallback (react-dom.development.js:4164:14)
    at Object.invokeGuardedCallbackDev (react-dom.development.js:4213:16)

When looking into the code in the specific file it looks like this:


'use client'

import type { Fields } from 'payload/types'

import { useModal } from '@faceless-ui/modal'
import { useDrawerSlug } from 'payload/components/elements'
import { reduceFieldsToValues } from 'payload/components/forms'
import {
  buildStateFromSchema,
  useAuth,
  useConfig,
  useDocumentInfo,
  useLocale,
} from 'payload/components/utilities'
import { sanitizeFields } from 'payload/config'
import React, { Fragment, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Editor, Range, Transforms } from 'slate'
import { ReactEditor, useSlate } from 'slate-react'

import type { FieldProps } from '../../../../types'

import LinkIcon from '../../../icons/Link'
import ElementButton from '../../Button'
import isElementActive from '../../isActive'
import { LinkDrawer } from '../LinkDrawer'
import { transformExtraFields, unwrapLink } from '../utilities'

/**
 * This function is called when an new link is created - not when an existing link is edited.
 */
const insertLink = (editor, fields) => {
  const isCollapsed = editor.selection && Range.isCollapsed(editor.selection)
  const data = reduceFieldsToValues(fields, true)

  const newLink = {
    children: [],
    doc: data.doc,
    fields: data.fields, // Any custom user-added fields are part of data.fields
    linkType: data.linkType,
    newTab: data.newTab,
    type: 'link',
    url: data.url,
  }

  if (isCollapsed || !editor.selection) {
    // If selection anchor and focus are the same,
    // Just inject a new node with children already set
    Transforms.insertNodes(editor, {
      ...newLink,
      children: [{ text: String(data.text) }],
    })
  } else if (editor.selection) {
    // Otherwise we need to wrap the selected node in a link,
    // Delete its old text,
    // Move the selection one position forward into the link,
    // And insert the text back into the new link
    Transforms.wrapNodes(editor, newLink, { split: true })
    Transforms.delete(editor, { at: editor.selection.focus.path, unit: 'word' })
    Transforms.move(editor, { distance: 1, unit: 'offset' })
    Transforms.insertText(editor, String(data.text), { at: editor.selection.focus.path })
  }

  ReactEditor.focus(editor)
}

export const LinkButton: React.FC<{
  fieldProps: FieldProps
  path: string
}> = ({ fieldProps }) => {
  const customFieldSchema = fieldProps?.admin?.link?.fields
  const { user } = useAuth()
  const { code: locale } = useLocale()
  const [initialState, setInitialState] = useState<Fields>({})

  const { i18n, t } = useTranslation(['upload', 'general'])
  const editor = useSlate()
  const config = useConfig()

  const [fieldSchema] = useState(() => {
    const fieldsUnsanitized = transformExtraFields(customFieldSchema, config, i18n)
    // Sanitize custom fields here
    const validRelationships = config.collections.map((c) => c.slug) || []
    const fields = sanitizeFields({
      config: config,
      fields: fieldsUnsanitized,
      validRelationships,
    })

    return fields
  })

  const { closeModal, openModal } = useModal()
  const drawerSlug = useDrawerSlug('rich-text-link')
  const { getDocPreferences } = useDocumentInfo()

  return (
    <Fragment>
      <ElementButton
        className="link"
        format="link"
        onClick={async () => {
          if (isElementActive(editor, 'link')) {
            unwrapLink(editor)
          } else {
            openModal(drawerSlug) // This is the row that throws the error: Uncaught (in promise) TypeError: openModal is not a function

            const isCollapsed = editor.selection && Range.isCollapsed(editor.selection)

            if (!isCollapsed) {
              const data = {
                text: editor.selection ? Editor.string(editor, editor.selection) : '',
              }

              const preferences = await getDocPreferences()
              const state = await buildStateFromSchema({
                config,
                data,
                fieldSchema,
                locale,
                operation: 'create',
                preferences,
                t,
                user,
              })
              setInitialState(state)
            }
          }
        }}
        tooltip={t('fields:addLink')}
      >
        <LinkIcon />
      </ElementButton>
      <LinkDrawer
        drawerSlug={drawerSlug}
        fieldSchema={fieldSchema}
        handleClose={() => {
          closeModal(drawerSlug)
        }}
        handleModalSubmit={(fields) => {
          insertLink(editor, fields)
          closeModal(drawerSlug)
        }}
        initialState={initialState}
      />
    </Fragment>
  )
}

To Reproduce

Should return the error "Uncaught (in promise) TypeError: openModal is not a function"

Payload Version

^2.30.1

Adapters and Plugins

richtext-slate@1.5.2

chrisulloa commented 1 week ago

I'm experiencing this issue too. Confirmed downgrading to 2.29.0 fixes this issue.