From d8f8d9d90442b6def17baa6eaf7c58fe009b57f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Jablo=C3=B1ski?= <43938777+GermanJablo@users.noreply.github.com> Date: Thu, 10 Apr 2025 15:55:55 -0300 Subject: [PATCH 1/7] fix(richtext-lexical): remove unnecessary paragraphs when inserting blocks or uploadNodes. Add a preventative plugin to normalize the selection --- .../features/blocks/client/plugin/index.tsx | 3 +- .../relationship/client/plugins/index.tsx | 3 +- .../features/upload/client/plugin/index.tsx | 3 +- .../src/lexical/LexicalEditor.tsx | 10 ++---- .../plugins/NormalizeSelection/index.tsx | 35 +++++++++++++++++++ 5 files changed, 41 insertions(+), 13 deletions(-) create mode 100644 packages/richtext-lexical/src/lexical/plugins/NormalizeSelection/index.tsx diff --git a/packages/richtext-lexical/src/features/blocks/client/plugin/index.tsx b/packages/richtext-lexical/src/features/blocks/client/plugin/index.tsx index ed8e0c1adf0..edbc8e3d078 100644 --- a/packages/richtext-lexical/src/features/blocks/client/plugin/index.tsx +++ b/packages/richtext-lexical/src/features/blocks/client/plugin/index.tsx @@ -56,8 +56,6 @@ export const BlocksPlugin: PluginComponent = () => { if ($isRangeSelection(selection)) { const blockNode = $createBlockNode(payload) - // Insert blocks node BEFORE potentially removing focusNode, as $insertNodeToNearestRoot errors if the focusNode doesn't exist - $insertNodeToNearestRoot(blockNode) const { focus } = selection const focusNode = focus.getNode() @@ -74,6 +72,7 @@ export const BlocksPlugin: PluginComponent = () => { ) { focusNode.remove() } + $insertNodeToNearestRoot(blockNode) } }) diff --git a/packages/richtext-lexical/src/features/relationship/client/plugins/index.tsx b/packages/richtext-lexical/src/features/relationship/client/plugins/index.tsx index 84affa0ed57..932d49cc20a 100644 --- a/packages/richtext-lexical/src/features/relationship/client/plugins/index.tsx +++ b/packages/richtext-lexical/src/features/relationship/client/plugins/index.tsx @@ -53,8 +53,6 @@ export const RelationshipPlugin: PluginComponent = ({ if ($isRangeSelection(selection)) { const relationshipNode = $createRelationshipNode(payload) - // Insert relationship node BEFORE potentially removing focusNode, as $insertNodeToNearestRoot errors if the focusNode doesn't exist - $insertNodeToNearestRoot(relationshipNode) const { focus } = selection const focusNode = focus.getNode() @@ -71,6 +69,7 @@ export const RelationshipPlugin: PluginComponent = ({ ) { focusNode.remove() } + $insertNodeToNearestRoot(relationshipNode) } return true diff --git a/packages/richtext-lexical/src/features/upload/client/plugin/index.tsx b/packages/richtext-lexical/src/features/upload/client/plugin/index.tsx index 7d0884433e3..a94c195bc88 100644 --- a/packages/richtext-lexical/src/features/upload/client/plugin/index.tsx +++ b/packages/richtext-lexical/src/features/upload/client/plugin/index.tsx @@ -53,8 +53,6 @@ export const UploadPlugin: PluginComponent = ({ client value: payload.value, }, }) - // Insert upload node BEFORE potentially removing focusNode, as $insertNodeToNearestRoot errors if the focusNode doesn't exist - $insertNodeToNearestRoot(uploadNode) const { focus } = selection const focusNode = focus.getNode() @@ -67,6 +65,7 @@ export const UploadPlugin: PluginComponent = ({ client ) { focusNode.remove() } + $insertNodeToNearestRoot(uploadNode) } }) diff --git a/packages/richtext-lexical/src/lexical/LexicalEditor.tsx b/packages/richtext-lexical/src/lexical/LexicalEditor.tsx index ead987a7436..aa3a5f3d280 100644 --- a/packages/richtext-lexical/src/lexical/LexicalEditor.tsx +++ b/packages/richtext-lexical/src/lexical/LexicalEditor.tsx @@ -4,13 +4,7 @@ import { LexicalErrorBoundary } from '@lexical/react/LexicalErrorBoundary.js' import { HistoryPlugin } from '@lexical/react/LexicalHistoryPlugin.js' import { OnChangePlugin } from '@lexical/react/LexicalOnChangePlugin.js' import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin.js' -import { - $createParagraphNode, - $getRoot, - BLUR_COMMAND, - COMMAND_PRIORITY_LOW, - FOCUS_COMMAND, -} from 'lexical' +import { BLUR_COMMAND, COMMAND_PRIORITY_LOW, FOCUS_COMMAND } from 'lexical' import * as React from 'react' import { useEffect, useState } from 'react' @@ -24,6 +18,7 @@ import { AddBlockHandlePlugin } from './plugins/handles/AddBlockHandlePlugin/ind import { DraggableBlockPlugin } from './plugins/handles/DraggableBlockPlugin/index.js' import { InsertParagraphAtEndPlugin } from './plugins/InsertParagraphAtEnd/index.js' import { MarkdownShortcutPlugin } from './plugins/MarkdownShortcut/index.js' +import { NormalizeSelectionPlugin } from './plugins/NormalizeSelection/index.js' import { SlashMenuPlugin } from './plugins/SlashMenu/index.js' import { TextPlugin } from './plugins/TextPlugin/index.js' import { LexicalContentEditable } from './ui/ContentEditable.js' @@ -112,6 +107,7 @@ export const LexicalEditor: React.FC< } ErrorBoundary={LexicalErrorBoundary} /> + diff --git a/packages/richtext-lexical/src/lexical/plugins/NormalizeSelection/index.tsx b/packages/richtext-lexical/src/lexical/plugins/NormalizeSelection/index.tsx new file mode 100644 index 00000000000..93c954790ef --- /dev/null +++ b/packages/richtext-lexical/src/lexical/plugins/NormalizeSelection/index.tsx @@ -0,0 +1,35 @@ +import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' +import { $getSelection, $isRangeSelection, RootNode } from 'lexical' +import { useEffect } from 'react' + +/** + * By default, Lexical throws an error if the selection ends in deleted nodes. + * This is very aggressive considering there are reasons why this can happen + * outside of Payload's control (custom features or conflicting features, for example). + * In the case of selections on nonexistent nodes, this plugin moves the selection to + * the end of the editor and displays a warning instead of an error. + */ +export function NormalizeSelectionPlugin() { + const [editor] = useLexicalComposerContext() + + useEffect(() => { + return editor.registerNodeTransform(RootNode, (root) => { + const selection = $getSelection() + if ($isRangeSelection(selection)) { + const anchorNode = selection.anchor.getNode() + const focusNode = selection.focus.getNode() + if (!anchorNode.isAttached() || !focusNode.isAttached()) { + root.selectEnd() + // eslint-disable-next-line no-console + console.warn( + 'updateEditor: selection has been moved to the end of the editor because the previously selected nodes have been removed and ' + + "selection wasn't moved to another node. Ensure selection changes after removing/replacing a selected node.", + ) + } + } + return false + }) + }, [editor]) + + return null +} From fbb84918afe584485b4923b70efa9a5b51008dad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Jablo=C3=B1ski?= <43938777+GermanJablo@users.noreply.github.com> Date: Fri, 11 Apr 2025 17:51:18 -0300 Subject: [PATCH 2/7] add lexical fully featured collection to test suite --- test/lexical/baseConfig.ts | 10 ++++ .../collections/LexicalFullyFeatured/index.ts | 57 +++++++++++++++++++ .../components/CollectionsExplained.tsx | 22 +++++++ test/lexical/slugs.ts | 1 + 4 files changed, 90 insertions(+) create mode 100644 test/lexical/collections/LexicalFullyFeatured/index.ts create mode 100644 test/lexical/components/CollectionsExplained.tsx diff --git a/test/lexical/baseConfig.ts b/test/lexical/baseConfig.ts index ca48a213b07..c831fe6d473 100644 --- a/test/lexical/baseConfig.ts +++ b/test/lexical/baseConfig.ts @@ -9,6 +9,7 @@ import { lexicalInlineBlocks, } from './collections/Lexical/index.js' import { LexicalAccessControl } from './collections/LexicalAccessControl/index.js' +import { LexicalFullyFeatured } from './collections/LexicalFullyFeatured/index.js' import { LexicalInBlock } from './collections/LexicalInBlock/index.js' import { LexicalLocalizedFields } from './collections/LexicalLocalized/index.js' import { LexicalMigrateFields } from './collections/LexicalMigrate/index.js' @@ -26,6 +27,7 @@ const dirname = path.dirname(filename) export const baseConfig: Partial = { // ...extend config here collections: [ + LexicalFullyFeatured, getLexicalFieldsCollection({ blocks: lexicalBlocks, inlineBlocks: lexicalInlineBlocks, @@ -42,10 +44,18 @@ export const baseConfig: Partial = { ArrayFields, ], globals: [TabsWithRichText], + admin: { importMap: { baseDir: path.resolve(dirname), }, + components: { + beforeDashboard: [ + { + path: './components/CollectionsExplained.tsx#CollectionsExplained', + }, + ], + }, }, onInit: async (payload) => { if (process.env.SEED_IN_CONFIG_ONINIT !== 'false') { diff --git a/test/lexical/collections/LexicalFullyFeatured/index.ts b/test/lexical/collections/LexicalFullyFeatured/index.ts new file mode 100644 index 00000000000..f8d8156e8fe --- /dev/null +++ b/test/lexical/collections/LexicalFullyFeatured/index.ts @@ -0,0 +1,57 @@ +import type { CollectionConfig } from 'payload' + +import { + BlocksFeature, + EXPERIMENTAL_TableFeature, + FixedToolbarFeature, + lexicalEditor, + TreeViewFeature, +} from '@payloadcms/richtext-lexical' + +import { lexicalFullyFeaturedSlug } from '../../slugs.js' + +export const LexicalFullyFeatured: CollectionConfig = { + slug: lexicalFullyFeaturedSlug, + labels: { + singular: 'Lexical Fully Featured', + plural: 'Lexical Fully Featured', + }, + fields: [ + { + name: 'richText', + type: 'richText', + editor: lexicalEditor({ + features: ({ defaultFeatures }) => [ + ...defaultFeatures, + TreeViewFeature(), + FixedToolbarFeature(), + EXPERIMENTAL_TableFeature(), + BlocksFeature({ + blocks: [ + { + slug: 'myBlock', + fields: [ + { + name: 'someText', + type: 'text', + }, + ], + }, + ], + inlineBlocks: [ + { + slug: 'myInlineBlock', + fields: [ + { + name: 'someText', + type: 'text', + }, + ], + }, + ], + }), + ], + }), + }, + ], +} diff --git a/test/lexical/components/CollectionsExplained.tsx b/test/lexical/components/CollectionsExplained.tsx new file mode 100644 index 00000000000..10f27bd787a --- /dev/null +++ b/test/lexical/components/CollectionsExplained.tsx @@ -0,0 +1,22 @@ +import React from 'react' + +export function CollectionsExplained() { + return ( +
+

Which collection should I use for my tests?

+ +

+ By default and as a rule of thumb: "Lexical Fully Featured". This collection has all our + features, but it does NOT have (and will never have): +

+
    +
  • Relationships or dependencies to other collections
  • +
  • Seeded documents
  • +
  • Features with custom props (except for a block and an inline block included)
  • +
  • Multiple richtext fields or other fields
  • +
+ +

If you need any of these features, use another collection or create a new one.

+
+ ) +} diff --git a/test/lexical/slugs.ts b/test/lexical/slugs.ts index 1f782022a42..73cf1671016 100644 --- a/test/lexical/slugs.ts +++ b/test/lexical/slugs.ts @@ -1,5 +1,6 @@ export const usersSlug = 'users' +export const lexicalFullyFeaturedSlug = 'lexical-fully-featured' export const lexicalFieldsSlug = 'lexical-fields' export const lexicalLocalizedFieldsSlug = 'lexical-localized-fields' export const lexicalMigrateFieldsSlug = 'lexical-migrate-fields' From 209d370d7bda5ec988b8a672cc41b97c5f5baafe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Jablo=C3=B1ski?= <43938777+GermanJablo@users.noreply.github.com> Date: Mon, 21 Apr 2025 11:00:00 -0300 Subject: [PATCH 3/7] rename file --- test/lexical/baseConfig.ts | 2 +- .../{LexicalFullyFeatured => _LexicalFullyFeatured}/index.ts | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename test/lexical/collections/{LexicalFullyFeatured => _LexicalFullyFeatured}/index.ts (100%) diff --git a/test/lexical/baseConfig.ts b/test/lexical/baseConfig.ts index c831fe6d473..f163abbd30e 100644 --- a/test/lexical/baseConfig.ts +++ b/test/lexical/baseConfig.ts @@ -2,6 +2,7 @@ import { fileURLToPath } from 'node:url' import path from 'path' import { type Config } from 'payload' +import { LexicalFullyFeatured } from './collections/_LexicalFullyFeatured/index.js' import ArrayFields from './collections/Array/index.js' import { getLexicalFieldsCollection, @@ -9,7 +10,6 @@ import { lexicalInlineBlocks, } from './collections/Lexical/index.js' import { LexicalAccessControl } from './collections/LexicalAccessControl/index.js' -import { LexicalFullyFeatured } from './collections/LexicalFullyFeatured/index.js' import { LexicalInBlock } from './collections/LexicalInBlock/index.js' import { LexicalLocalizedFields } from './collections/LexicalLocalized/index.js' import { LexicalMigrateFields } from './collections/LexicalMigrate/index.js' diff --git a/test/lexical/collections/LexicalFullyFeatured/index.ts b/test/lexical/collections/_LexicalFullyFeatured/index.ts similarity index 100% rename from test/lexical/collections/LexicalFullyFeatured/index.ts rename to test/lexical/collections/_LexicalFullyFeatured/index.ts From 05538d0f14d27a4247eb9a8620e8981750ad8114 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Jablo=C3=B1ski?= <43938777+GermanJablo@users.noreply.github.com> Date: Mon, 21 Apr 2025 18:40:00 -0300 Subject: [PATCH 4/7] fix decorator insertion --- .../features/blocks/client/plugin/index.tsx | 15 ++-- .../relationship/client/plugins/index.tsx | 15 ++-- .../features/upload/client/plugin/index.tsx | 11 ++- .../_LexicalFullyFeatured/e2e.spec.ts | 68 +++++++++++++++++++ .../_LexicalFullyFeatured/utils.ts | 49 +++++++++++++ 5 files changed, 129 insertions(+), 29 deletions(-) create mode 100644 test/lexical/collections/_LexicalFullyFeatured/e2e.spec.ts create mode 100644 test/lexical/collections/_LexicalFullyFeatured/utils.ts diff --git a/packages/richtext-lexical/src/features/blocks/client/plugin/index.tsx b/packages/richtext-lexical/src/features/blocks/client/plugin/index.tsx index edbc8e3d078..3d6ae716541 100644 --- a/packages/richtext-lexical/src/features/blocks/client/plugin/index.tsx +++ b/packages/richtext-lexical/src/features/blocks/client/plugin/index.tsx @@ -59,20 +59,13 @@ export const BlocksPlugin: PluginComponent = () => { const { focus } = selection const focusNode = focus.getNode() + // Insert blocks node BEFORE potentially removing focusNode, as $insertNodeToNearestRoot errors if the focusNode doesn't exist + $insertNodeToNearestRoot(blockNode) - // First, delete currently selected node if it's an empty paragraph and if there are sufficient - // paragraph nodes (more than 1) left in the parent node, so that we don't "trap" the user - if ( - $isParagraphNode(focusNode) && - focusNode.getTextContentSize() === 0 && - focusNode - .getParentOrThrow() - .getChildren() - .filter((node) => $isParagraphNode(node)).length > 1 - ) { + // Delete the node it it's an empty paragraph + if ($isParagraphNode(focusNode) && !focusNode.__first) { focusNode.remove() } - $insertNodeToNearestRoot(blockNode) } }) diff --git a/packages/richtext-lexical/src/features/relationship/client/plugins/index.tsx b/packages/richtext-lexical/src/features/relationship/client/plugins/index.tsx index 932d49cc20a..0231ef3530e 100644 --- a/packages/richtext-lexical/src/features/relationship/client/plugins/index.tsx +++ b/packages/richtext-lexical/src/features/relationship/client/plugins/index.tsx @@ -56,20 +56,13 @@ export const RelationshipPlugin: PluginComponent = ({ const { focus } = selection const focusNode = focus.getNode() + // Insert relationship node BEFORE potentially removing focusNode, as $insertNodeToNearestRoot errors if the focusNode doesn't exist + $insertNodeToNearestRoot(relationshipNode) - // First, delete currently selected node if it's an empty paragraph and if there are sufficient - // paragraph nodes (more than 1) left in the parent node, so that we don't "trap" the user - if ( - $isParagraphNode(focusNode) && - focusNode.getTextContentSize() === 0 && - focusNode - .getParentOrThrow() - .getChildren() - .filter((node) => $isParagraphNode(node)).length > 1 - ) { + // Delete the node it it's an empty paragraph + if ($isParagraphNode(focusNode) && !focusNode.__first) { focusNode.remove() } - $insertNodeToNearestRoot(relationshipNode) } return true diff --git a/packages/richtext-lexical/src/features/upload/client/plugin/index.tsx b/packages/richtext-lexical/src/features/upload/client/plugin/index.tsx index a94c195bc88..a31ae09d817 100644 --- a/packages/richtext-lexical/src/features/upload/client/plugin/index.tsx +++ b/packages/richtext-lexical/src/features/upload/client/plugin/index.tsx @@ -56,16 +56,13 @@ export const UploadPlugin: PluginComponent = ({ client const { focus } = selection const focusNode = focus.getNode() + // Insert upload node BEFORE potentially removing focusNode, as $insertNodeToNearestRoot errors if the focusNode doesn't exist + $insertNodeToNearestRoot(uploadNode) - // Delete the node it it's an empty paragraph and it has at least one sibling, so that we don't "trap" the user - if ( - $isParagraphNode(focusNode) && - !focusNode.__first && - (focusNode.__prev || focusNode.__next) - ) { + // Delete the node it it's an empty paragraph + if ($isParagraphNode(focusNode) && !focusNode.__first) { focusNode.remove() } - $insertNodeToNearestRoot(uploadNode) } }) diff --git a/test/lexical/collections/_LexicalFullyFeatured/e2e.spec.ts b/test/lexical/collections/_LexicalFullyFeatured/e2e.spec.ts new file mode 100644 index 00000000000..ac986a998d7 --- /dev/null +++ b/test/lexical/collections/_LexicalFullyFeatured/e2e.spec.ts @@ -0,0 +1,68 @@ +import { expect, test } from '@playwright/test' +import { AdminUrlUtil } from 'helpers/adminUrlUtil.js' +import { reInitializeDB } from 'helpers/reInitializeDB.js' +import { lexicalFullyFeaturedSlug } from 'lexical/slugs.js' +import path from 'path' +import { fileURLToPath } from 'url' + +import { ensureCompilationIsDone } from '../../../helpers.js' +import { initPayloadE2ENoConfig } from '../../../helpers/initPayloadE2ENoConfig.js' +import { TEST_TIMEOUT_LONG } from '../../../playwright.config.js' +import { LexicalHelpers } from './utils.js' +const filename = fileURLToPath(import.meta.url) +const currentFolder = path.dirname(filename) +const dirname = path.resolve(currentFolder, '../../') + +const { beforeAll, beforeEach, describe } = test + +// Unlike the other suites, this one runs in parallel, as they run on the `lexical-fully-featured/create` URL and are "pure" tests +test.describe.configure({ mode: 'parallel' }) + +const { serverURL } = await initPayloadE2ENoConfig({ + dirname, +}) + +describe('Lexical Fully Featured', () => { + beforeAll(async ({ browser }, testInfo) => { + testInfo.setTimeout(TEST_TIMEOUT_LONG) + process.env.SEED_IN_CONFIG_ONINIT = 'false' // Makes it so the payload config onInit seed is not run. Otherwise, the seed would be run unnecessarily twice for the initial test run - once for beforeEach and once for onInit + const page = await browser.newPage() + await ensureCompilationIsDone({ page, serverURL }) + await page.close() + }) + beforeEach(async ({ page }) => { + await reInitializeDB({ + serverURL, + snapshotKey: 'fieldsTest', + uploadsDir: [ + path.resolve(dirname, './collections/Upload/uploads'), + path.resolve(dirname, './collections/Upload2/uploads2'), + ], + }) + const url = new AdminUrlUtil(serverURL, lexicalFullyFeaturedSlug) + const lexical = new LexicalHelpers(page) + await page.goto(url.create) + await lexical.editor.first().focus() + }) + test('prevent extra paragraph when inserting decorator blocks like blocks or upload node', async ({ + page, + }) => { + const lexical = new LexicalHelpers(page) + await lexical.slashCommand('block') + await lexical.slashCommand('relationship') + await lexical.drawer.locator('.list-drawer__header').getByText('Create New').click() + await lexical.save('drawer') + await expect(lexical.decorator).toHaveCount(2) + await lexical.slashCommand('upload') + await lexical.drawer.locator('.list-drawer__header').getByText('Create New').click() + await lexical.drawer.getByText('Paste URL').click() + await lexical.drawer + .locator('.file-field__remote-file') + .fill('https://payloadcms.com/images/universal-truth.jpg') + await lexical.drawer.getByText('Add file').click() + await lexical.save('drawer') + await expect(lexical.decorator).toHaveCount(3) + const paragraph = lexical.editor.locator('> p') + await expect(paragraph).toHaveText('') + }) +}) diff --git a/test/lexical/collections/_LexicalFullyFeatured/utils.ts b/test/lexical/collections/_LexicalFullyFeatured/utils.ts new file mode 100644 index 00000000000..95030f7fee5 --- /dev/null +++ b/test/lexical/collections/_LexicalFullyFeatured/utils.ts @@ -0,0 +1,49 @@ +import type { Page } from 'playwright' + +import { expect } from '@playwright/test' + +export class LexicalHelpers { + page: Page + constructor(page: Page) { + this.page = page + } + + async save(container: 'document' | 'drawer') { + if (container === 'drawer') { + await this.drawer.getByText('Save').click() + } else { + throw new Error('Not implemented') + } + await this.page.waitForTimeout(1000) + } + + async slashCommand( + // prettier-ignore + command: 'block' | 'check' | 'code' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' |'h6' | 'inline' + | 'link' | 'ordered' | 'paragraph' | 'quote' | 'relationship' | 'unordered' | 'upload', + ) { + await this.page.keyboard.press(`/`) + + const slashMenuPopover = this.page.locator('#slash-menu .slash-menu-popup') + await expect(slashMenuPopover).toBeVisible() + await this.page.keyboard.type(command) + await this.page.keyboard.press(`Enter`) + await expect(slashMenuPopover).toBeHidden() + } + + get decorator() { + return this.editor.locator('[data-lexical-decorator="true"]') + } + + get drawer() { + return this.page.locator('.drawer__content') + } + + get editor() { + return this.page.locator('[data-lexical-editor="true"]') + } + + get paragraph() { + return this.editor.locator('p') + } +} From 293cd46009ad724493b9a5ec6e18c65c7242129f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Jablo=C3=B1ski?= <43938777+GermanJablo@users.noreply.github.com> Date: Tue, 22 Apr 2025 12:11:53 -0300 Subject: [PATCH 5/7] fix tests --- .../collections/Lexical/e2e/blocks/e2e.spec.ts | 10 +++++----- test/lexical/collections/Lexical/e2e/main/e2e.spec.ts | 11 ++++++----- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/test/lexical/collections/Lexical/e2e/blocks/e2e.spec.ts b/test/lexical/collections/Lexical/e2e/blocks/e2e.spec.ts index 9a910b0002f..b1c5ddc9865 100644 --- a/test/lexical/collections/Lexical/e2e/blocks/e2e.spec.ts +++ b/test/lexical/collections/Lexical/e2e/blocks/e2e.spec.ts @@ -302,7 +302,7 @@ describe('lexicalBlocks', () => { await assertLexicalDoc({ fn: ({ lexicalWithBlocks }) => { const rscBlock: SerializedBlockNode = lexicalWithBlocks.root - .children[14] as SerializedBlockNode + .children[13] as SerializedBlockNode const paragraphNode: SerializedParagraphNode = lexicalWithBlocks.root .children[12] as SerializedParagraphNode @@ -1133,9 +1133,9 @@ describe('lexicalBlocks', () => { ).docs[0] as never const richTextBlock: SerializedBlockNode = lexicalWithBlocks.root - .children[13] as SerializedBlockNode + .children[12] as SerializedBlockNode const subRichTextBlock: SerializedBlockNode = richTextBlock.fields.richTextField.root - .children[1] as SerializedBlockNode // index 0 and 2 are paragraphs created by default around the block node when a new block is added via slash command + .children[0] as SerializedBlockNode // index 0 and 2 are paragraphs created by default around the block node when a new block is added via slash command const subSubRichTextField = subRichTextBlock.fields.subRichTextField const subSubUploadField = subRichTextBlock.fields.subUploadField @@ -1163,9 +1163,9 @@ describe('lexicalBlocks', () => { ).docs[0] as never const richTextBlock2: SerializedBlockNode = lexicalWithBlocks.root - .children[13] as SerializedBlockNode + .children[12] as SerializedBlockNode const subRichTextBlock2: SerializedBlockNode = richTextBlock2.fields.richTextField.root - .children[1] as SerializedBlockNode // index 0 and 2 are paragraphs created by default around the block node when a new block is added via slash command + .children[0] as SerializedBlockNode // index 0 and 2 are paragraphs created by default around the block node when a new block is added via slash command const subSubRichTextField2 = subRichTextBlock2.fields.subRichTextField const subSubUploadField2 = subRichTextBlock2.fields.subUploadField diff --git a/test/lexical/collections/Lexical/e2e/main/e2e.spec.ts b/test/lexical/collections/Lexical/e2e/main/e2e.spec.ts index 44df6e68b49..292e7ff17de 100644 --- a/test/lexical/collections/Lexical/e2e/main/e2e.spec.ts +++ b/test/lexical/collections/Lexical/e2e/main/e2e.spec.ts @@ -728,7 +728,8 @@ describe('lexicalMain', () => { await expect(relationshipListDrawer).toBeVisible() await wait(500) - await expect(relationshipListDrawer.locator('.rs__single-value')).toHaveText('Lexical Field') + await relationshipListDrawer.locator('.rs__input').first().click() + await relationshipListDrawer.locator('.rs__menu').getByText('Lexical Field').click() await relationshipListDrawer.locator('button').getByText('Rich Text').first().click() await expect(relationshipListDrawer).toBeHidden() @@ -1258,10 +1259,10 @@ describe('lexicalMain', () => { const firstParagraph: SerializedParagraphNode = lexicalField.root .children[0] as SerializedParagraphNode const secondParagraph: SerializedParagraphNode = lexicalField.root - .children[1] as SerializedParagraphNode - const thirdParagraph: SerializedParagraphNode = lexicalField.root .children[2] as SerializedParagraphNode - const uploadNode: SerializedUploadNode = lexicalField.root.children[3] as SerializedUploadNode + const thirdParagraph: SerializedParagraphNode = lexicalField.root + .children[3] as SerializedParagraphNode + const uploadNode: SerializedUploadNode = lexicalField.root.children[1] as SerializedUploadNode expect(firstParagraph.children).toHaveLength(2) expect((firstParagraph.children[0] as SerializedTextNode).text).toBe('Some ') @@ -1391,7 +1392,7 @@ describe('lexicalMain', () => { const lexicalField: SerializedEditorState = lexicalDoc.lexicalRootEditor // @ts-expect-error no need to type this - expect(lexicalField?.root?.children[1].fields.someTextRequired).toEqual('test') + expect(lexicalField?.root?.children[0].fields.someTextRequired).toEqual('test') }).toPass({ timeout: POLL_TOPASS_TIMEOUT, }) From fcfbace7911ee8c8fd85d02b1ba66f7b68da95e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Jablo=C3=B1ski?= <43938777+GermanJablo@users.noreply.github.com> Date: Tue, 22 Apr 2025 14:21:15 -0300 Subject: [PATCH 6/7] fix test --- test/lexical/collections/Lexical/e2e/main/e2e.spec.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/lexical/collections/Lexical/e2e/main/e2e.spec.ts b/test/lexical/collections/Lexical/e2e/main/e2e.spec.ts index 292e7ff17de..d337cd37f92 100644 --- a/test/lexical/collections/Lexical/e2e/main/e2e.spec.ts +++ b/test/lexical/collections/Lexical/e2e/main/e2e.spec.ts @@ -1204,10 +1204,11 @@ describe('lexicalMain', () => { await expect(newUploadNode.locator('.lexical-upload__bottomRow')).toContainText('payload.png') await page.keyboard.press('Enter') // floating toolbar needs to appear with enough distance to the upload node, otherwise clicking may fail + await page.keyboard.press('Enter') await page.keyboard.press('ArrowLeft') await page.keyboard.press('ArrowLeft') // Select "there" by pressing shift + arrow left - for (let i = 0; i < 4; i++) { + for (let i = 0; i < 5; i++) { await page.keyboard.press('Shift+ArrowLeft') } From f000130002f5da93045333fb2e8d3a1cbd6b6fc9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Jablo=C3=B1ski?= <43938777+GermanJablo@users.noreply.github.com> Date: Tue, 29 Apr 2025 12:34:12 -0300 Subject: [PATCH 7/7] add comments --- .../features/blocks/client/plugin/index.tsx | 1 + .../relationship/client/plugins/index.tsx | 2 +- .../features/upload/client/plugin/index.tsx | 2 +- test/lexical/payload-types.ts | 39 +++++++++++++++++++ 4 files changed, 42 insertions(+), 2 deletions(-) diff --git a/packages/richtext-lexical/src/features/blocks/client/plugin/index.tsx b/packages/richtext-lexical/src/features/blocks/client/plugin/index.tsx index 3d6ae716541..fa9539852ec 100644 --- a/packages/richtext-lexical/src/features/blocks/client/plugin/index.tsx +++ b/packages/richtext-lexical/src/features/blocks/client/plugin/index.tsx @@ -57,6 +57,7 @@ export const BlocksPlugin: PluginComponent = () => { if ($isRangeSelection(selection)) { const blockNode = $createBlockNode(payload) + // we need to get the focus node before inserting the block node, as $insertNodeToNearestRoot can change the focus node const { focus } = selection const focusNode = focus.getNode() // Insert blocks node BEFORE potentially removing focusNode, as $insertNodeToNearestRoot errors if the focusNode doesn't exist diff --git a/packages/richtext-lexical/src/features/relationship/client/plugins/index.tsx b/packages/richtext-lexical/src/features/relationship/client/plugins/index.tsx index 0231ef3530e..bd581620aec 100644 --- a/packages/richtext-lexical/src/features/relationship/client/plugins/index.tsx +++ b/packages/richtext-lexical/src/features/relationship/client/plugins/index.tsx @@ -53,7 +53,7 @@ export const RelationshipPlugin: PluginComponent = ({ if ($isRangeSelection(selection)) { const relationshipNode = $createRelationshipNode(payload) - + // we need to get the focus node before inserting the block node, as $insertNodeToNearestRoot can change the focus node const { focus } = selection const focusNode = focus.getNode() // Insert relationship node BEFORE potentially removing focusNode, as $insertNodeToNearestRoot errors if the focusNode doesn't exist diff --git a/packages/richtext-lexical/src/features/upload/client/plugin/index.tsx b/packages/richtext-lexical/src/features/upload/client/plugin/index.tsx index a31ae09d817..5ee652610ec 100644 --- a/packages/richtext-lexical/src/features/upload/client/plugin/index.tsx +++ b/packages/richtext-lexical/src/features/upload/client/plugin/index.tsx @@ -53,7 +53,7 @@ export const UploadPlugin: PluginComponent = ({ client value: payload.value, }, }) - + // we need to get the focus node before inserting the block node, as $insertNodeToNearestRoot can change the focus node const { focus } = selection const focusNode = focus.getNode() // Insert upload node BEFORE potentially removing focusNode, as $insertNodeToNearestRoot errors if the focusNode doesn't exist diff --git a/test/lexical/payload-types.ts b/test/lexical/payload-types.ts index 73373c85c9d..1964b49ffba 100644 --- a/test/lexical/payload-types.ts +++ b/test/lexical/payload-types.ts @@ -83,6 +83,7 @@ export interface Config { }; blocks: {}; collections: { + 'lexical-fully-featured': LexicalFullyFeatured; 'lexical-fields': LexicalField; 'lexical-migrate-fields': LexicalMigrateField; 'lexical-localized-fields': LexicalLocalizedField; @@ -101,6 +102,7 @@ export interface Config { }; collectionsJoins: {}; collectionsSelect: { + 'lexical-fully-featured': LexicalFullyFeaturedSelect | LexicalFullyFeaturedSelect; 'lexical-fields': LexicalFieldsSelect | LexicalFieldsSelect; 'lexical-migrate-fields': LexicalMigrateFieldsSelect | LexicalMigrateFieldsSelect; 'lexical-localized-fields': LexicalLocalizedFieldsSelect | LexicalLocalizedFieldsSelect; @@ -153,6 +155,30 @@ export interface UserAuthOperations { password: string; }; } +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "lexical-fully-featured". + */ +export interface LexicalFullyFeatured { + id: string; + richText?: { + root: { + type: string; + children: { + type: string; + version: number; + [k: string]: unknown; + }[]; + direction: ('ltr' | 'rtl') | null; + format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | ''; + indent: number; + version: number; + }; + [k: string]: unknown; + } | null; + updatedAt: string; + createdAt: string; +} /** * This interface was referenced by `Config`'s JSON-Schema * via the `definition` "lexical-fields". @@ -774,6 +800,10 @@ export interface User { export interface PayloadLockedDocument { id: string; document?: + | ({ + relationTo: 'lexical-fully-featured'; + value: string | LexicalFullyFeatured; + } | null) | ({ relationTo: 'lexical-fields'; value: string | LexicalField; @@ -864,6 +894,15 @@ export interface PayloadMigration { updatedAt: string; createdAt: string; } +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "lexical-fully-featured_select". + */ +export interface LexicalFullyFeaturedSelect { + richText?: T; + updatedAt?: T; + createdAt?: T; +} /** * This interface was referenced by `Config`'s JSON-Schema * via the `definition` "lexical-fields_select".