diff --git a/packages/richtext-lexical/src/features/blockquote/server/index.ts b/packages/richtext-lexical/src/features/blockquote/server/index.ts index 2248ef3c9b5..70aef051729 100644 --- a/packages/richtext-lexical/src/features/blockquote/server/index.ts +++ b/packages/richtext-lexical/src/features/blockquote/server/index.ts @@ -5,6 +5,7 @@ import { QuoteNode } from '@lexical/rich-text' import { createServerFeature } from '../../../utilities/createServerFeature.js' import { convertLexicalNodesToHTML } from '../../converters/html/converter/index.js' +import { getElementNodeDefaultStyle } from '../../shared/defaultStyle/getElementNodeDefaultStyle.js' import { createNode } from '../../typeUtilities.js' import { MarkdownTransformer } from '../markdownTransformer.js' import { i18n } from './i18n.js' @@ -51,8 +52,12 @@ export const BlockquoteFeature = createServerFeature({ req, showHiddenFields, }) + const defaultStyle = getElementNodeDefaultStyle({ + node, + }) + const style = defaultStyle ? ` style="${defaultStyle}"` : '' - return `
${childrenText}` + return `
${childrenText}` }, nodeTypes: [QuoteNode.getType()], }, diff --git a/packages/richtext-lexical/src/features/converters/html/converter/converters/paragraph.ts b/packages/richtext-lexical/src/features/converters/html/converter/converters/paragraph.ts index 1f8ba4631e8..6a7234cd273 100644 --- a/packages/richtext-lexical/src/features/converters/html/converter/converters/paragraph.ts +++ b/packages/richtext-lexical/src/features/converters/html/converter/converters/paragraph.ts @@ -2,6 +2,7 @@ import type { SerializedParagraphNode } from 'lexical' import type { HTMLConverter } from '../types.js' +import { getElementNodeDefaultStyle } from '../../../../shared/defaultStyle/getElementNodeDefaultStyle.js' import { convertLexicalNodesToHTML } from '../index.js' export const ParagraphHTMLConverter: HTMLConverter
${childrenText}
` + const defaultStyle = getElementNodeDefaultStyle({ + node, + }) + const style = defaultStyle ? ` style="${defaultStyle}"` : '' + + return `${childrenText}
` }, nodeTypes: ['paragraph'], } diff --git a/packages/richtext-lexical/src/features/heading/server/index.ts b/packages/richtext-lexical/src/features/heading/server/index.ts index 30e5cc4ff09..0ea83f6d24f 100644 --- a/packages/richtext-lexical/src/features/heading/server/index.ts +++ b/packages/richtext-lexical/src/features/heading/server/index.ts @@ -8,6 +8,7 @@ import { HeadingNode } from '@lexical/rich-text' import { createServerFeature } from '../../../utilities/createServerFeature.js' import { convertLexicalNodesToHTML } from '../../converters/html/converter/index.js' +import { getElementNodeDefaultStyle } from '../../shared/defaultStyle/getElementNodeDefaultStyle.js' import { createNode } from '../../typeUtilities.js' import { MarkdownTransformer } from '../markdownTransformer.js' import { i18n } from './i18n.js' @@ -69,8 +70,12 @@ export const HeadingFeature = createServerFeature< req, showHiddenFields, }) + const defaultStyle = getElementNodeDefaultStyle({ + node, + }) + const style = defaultStyle ? ` style="${defaultStyle}"` : '' - return '<' + node?.tag + '>' + childrenText + '' + node?.tag + '>' + return `<${node?.tag}${style}>${childrenText}${node?.tag}>` }, nodeTypes: [HeadingNode.getType()], }, diff --git a/packages/richtext-lexical/src/features/lists/htmlConverter.ts b/packages/richtext-lexical/src/features/lists/htmlConverter.ts index 882410fb5c0..2a693073a11 100644 --- a/packages/richtext-lexical/src/features/lists/htmlConverter.ts +++ b/packages/richtext-lexical/src/features/lists/htmlConverter.ts @@ -5,6 +5,10 @@ import type { HTMLConverter } from '../converters/html/converter/types.js' import type { SerializedListItemNode, SerializedListNode } from './plugin/index.js' import { convertLexicalNodesToHTML } from '../converters/html/converter/index.js' +import { + getAlignStyle, + getElementNodeDefaultStyle, +} from '../shared/defaultStyle/getElementNodeDefaultStyle.js' export const ListHTMLConverter: HTMLConverter${textContent}
` +} diff --git a/test/fields/collections/LexicalMigrate/e2e/converter/e2e.spec.ts b/test/fields/collections/LexicalMigrate/e2e/converter/e2e.spec.ts new file mode 100644 index 00000000000..9a44dd5f4eb --- /dev/null +++ b/test/fields/collections/LexicalMigrate/e2e/converter/e2e.spec.ts @@ -0,0 +1,173 @@ +import type { BrowserContext, Page } from '@playwright/test' +import type { SerializedEditorState } from 'lexical' + +import { expect, test } from '@playwright/test' +import os from 'os' +import path from 'path' +import { fileURLToPath } from 'url' + +import type { PayloadTestSDK } from '../../../../../helpers/sdk/index.js' +import type { Config, LexicalMigrateField } from '../../../../payload-types.js' + +import { + ensureCompilationIsDone, + initPageConsoleErrorCatch, + saveDocAndAssert, +} from '../../../../../helpers.js' +import { AdminUrlUtil } from '../../../../../helpers/adminUrlUtil.js' +import { initPayloadE2ENoConfig } from '../../../../../helpers/initPayloadE2ENoConfig.js' +import { reInitializeDB } from '../../../../../helpers/reInitializeDB.js' +import { RESTClient } from '../../../../../helpers/rest.js' +import { POLL_TOPASS_TIMEOUT, TEST_TIMEOUT_LONG } from '../../../../../playwright.config.js' +import { lexicalMigrateFieldsSlug } from '../../../../slugs.js' +import { + getAlignIndentHTMLData, + getAlignIndentLexicalData, + lexicalMigrateDocData, +} from '../../data.js' + +const filename = fileURLToPath(import.meta.url) +const currentFolder = path.dirname(filename) +const dirname = path.resolve(currentFolder, '../../../../') + +const { beforeAll, beforeEach, describe } = test + +let payload: PayloadTestSDKsimple
'], ['text/plain', 'simple']] + initialClipboardData?: Array<[string, string]>, +) { + await context.grantPermissions(['clipboard-read', 'clipboard-write']) + await page.evaluate((initialClipboardData) => { + const initClipboardData = (event: ClipboardEvent) => { + event.preventDefault() + for (const [type, value] of initialClipboardData) { + event.clipboardData.setData(type, value) + } + document.removeEventListener('copy', initClipboardData) + } + document.addEventListener('copy', initClipboardData) + }, initialClipboardData) + const isMac = os.platform() === 'darwin' + // For Mac, paste command is with Meta. + const modifier = isMac ? 'Meta' : 'Control' + await page.keyboard.press(`${modifier}+KeyC`) +} + +describe('lexicalMigrateConverter', () => { + 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 + ;({ payload, serverURL } = await initPayloadE2ENoConfig({ dirname })) + + context = await browser.newContext() + page = await context.newPage() + + initPageConsoleErrorCatch(page) + await reInitializeDB({ + serverURL, + snapshotKey: 'fieldsLexicalMainTest', + uploadsDir: path.resolve(dirname, './collections/Upload/uploads'), + }) + await ensureCompilationIsDone({ page, serverURL }) + }) + beforeEach(async () => { + /*await throttleTest({ + page, + context, + delay: 'Slow 4G', + })*/ + await reInitializeDB({ + serverURL, + snapshotKey: 'fieldsLexicalMigrateConverterTest', + uploadsDir: [ + path.resolve(dirname, './collections/Upload/uploads'), + path.resolve(dirname, './collections/Upload2/uploads2'), + ], + }) + + if (client) { + await client.logout() + } + client = new RESTClient(null, { defaultSlug: 'rich-text-fields', serverURL }) + await client.login() + }) + + test('should be replaced indent and align styles in text/html with lexical data', async ({ + context, + }) => { + await initClipboard( + { + context, + page, + }, + [['text/html', getAlignIndentHTMLData('styled')]], + ) + await navigateToLexicalFields() + // Fix lexicalWithSlateData data because of a LinkNode validate issue. + const richTextField = page.locator('.rich-text-lexical').nth(1) + await richTextField.scrollIntoViewIfNeeded() + await expect(richTextField).toBeVisible() + + const editor = richTextField.locator('.editor') + await editor.click() + const isMac = os.platform() === 'darwin' + // For Mac, paste command is with Meta. + const modifier = isMac ? 'Meta' : 'Control' + // Delete all lexical data for HTML paste test. + await page.keyboard.press(`${modifier}+KeyA`) + await page.keyboard.press('Backspace') + await page.keyboard.press(`${modifier}+KeyV`) + + await saveDocAndAssert(page) + await expect(async () => { + const lexicalDoc: LexicalMigrateField = ( + await payload.find({ + collection: lexicalMigrateFieldsSlug, + depth: 0, + overrideAccess: true, + where: { + title: { + equals: lexicalMigrateDocData.title, + }, + }, + }) + ).docs[0] as never + + const lexicalField: SerializedEditorState = lexicalDoc.lexicalWithSlateData + + expect(lexicalField).toMatchObject(getAlignIndentLexicalData('styled')) + }).toPass({ + timeout: POLL_TOPASS_TIMEOUT, + }) + }) +}) diff --git a/test/fields/collections/LexicalMigrate/index.ts b/test/fields/collections/LexicalMigrate/index.ts index 19403825f47..577c50c744a 100644 --- a/test/fields/collections/LexicalMigrate/index.ts +++ b/test/fields/collections/LexicalMigrate/index.ts @@ -14,7 +14,7 @@ import { } from '@payloadcms/richtext-lexical/migrate' import { lexicalMigrateFieldsSlug } from '../../slugs.js' -import { getSimpleLexicalData } from './data.js' +import { getAlignIndentLexicalData, getSimpleLexicalData } from './data.js' export const LexicalMigrateFields: CollectionConfig = { slug: lexicalMigrateFieldsSlug, @@ -122,6 +122,15 @@ export const LexicalMigrateFields: CollectionConfig = { defaultValue: getSimpleLexicalData('simple'), }, lexicalHTML('lexicalSimple', { name: 'lexicalSimple_html' }), + { + name: 'lexicalStyledIndentAlign', + type: 'richText', + editor: lexicalEditor({ + features: ({ defaultFeatures }) => [...defaultFeatures, HTMLConverterFeature()], + }), + defaultValue: getAlignIndentLexicalData('styled'), + }, + lexicalHTML('lexicalStyledIndentAlign', { name: 'lexicalStyledIndentAlign_html' }), { name: 'groupWithLexicalField', type: 'group', diff --git a/test/fields/lexical.int.spec.ts b/test/fields/lexical.int.spec.ts index 71203c6fbec..92e572ad6be 100644 --- a/test/fields/lexical.int.spec.ts +++ b/test/fields/lexical.int.spec.ts @@ -18,7 +18,7 @@ import { NextRESTClient } from '../helpers/NextRESTClient.js' import { lexicalDocData } from './collections/Lexical/data.js' import { generateLexicalLocalizedRichText } from './collections/LexicalLocalized/generateLexicalRichText.js' import { textToLexicalJSON } from './collections/LexicalLocalized/textToLexicalJSON.js' -import { lexicalMigrateDocData } from './collections/LexicalMigrate/data.js' +import { getAlignIndentHTMLData, lexicalMigrateDocData } from './collections/LexicalMigrate/data.js' import { richTextDocData } from './collections/RichText/data.js' import { generateLexicalRichText } from './collections/RichText/generateLexicalRichText.js' import { textDoc } from './collections/Text/shared.js' @@ -304,6 +304,22 @@ describe('Lexical', () => { const htmlField: string = lexicalDoc?.lexicalSimple_html expect(htmlField).toStrictEqual('simple
') }) + it('htmlConverter: should output correct HTML for lexical data with indent and align', async () => { + const lexicalDoc: LexicalMigrateField = ( + await payload.find({ + collection: lexicalMigrateFieldsSlug, + depth: 0, + where: { + title: { + equals: lexicalMigrateDocData.title, + }, + }, + }) + ).docs[0] as never + + const htmlField: string = lexicalDoc?.lexicalStyledIndentAlign_html + expect(htmlField).toStrictEqual(getAlignIndentHTMLData('styled')) + }) it('htmlConverter: should output correct HTML for lexical field nested in group', async () => { const lexicalDoc: LexicalMigrateField = ( await payload.find({