Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(richtext-lexical): migrate scripts not working due to migration hooks running during migrate script #7801

Merged
merged 4 commits into from
Aug 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/lexical/migration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ IMPORTANT: This will overwrite all slate data. We recommend doing the following
1. Take a backup of your entire database. If anything goes wrong and you do not have a backup, you are on your own and will not receive any support.
2. Make every richText field a lexical editor. This script will only convert lexical richText fields with old Slate data
3. Add the SlateToLexicalFeature (as seen below) first, and test it out by loading up the Admin Panel, to see if the migrator works as expected. You might have to build some custom converters for some fields first in order to convert custom Slate nodes. The SlateToLexicalFeature is where the converters are stored. Only fields with this feature added will be migrated.
4. If this works as expected, add the `disableHooks: true` prop everywhere you're initializing `SlateToLexicalFeature`. Example: `SlateToLexicalFeature({ disableHooks: true })`. Once you did that, you're ready to run the migration script.

```ts
import { migrateSlateToLexical } from '@payloadcms/richtext-lexical/migrate'
Expand Down
1 change: 1 addition & 0 deletions packages/drizzle/src/upsertRow/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,7 @@ export const upsertRow = async <T extends Record<string, unknown> | TypeWithID>(
throw error.code === '23505'
? new ValidationError(
{
id,
errors: [
{
field: adapter.fieldConstraints[tableName][error.constraint],
Expand Down
7 changes: 6 additions & 1 deletion packages/payload/src/errors/ValidationError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,12 @@ export class ValidationError extends APIError<{
global?: string
}> {
constructor(
results: { collection?: string; errors: ValidationFieldError[]; global?: string },
results: {
collection?: string
errors: ValidationFieldError[]
global?: string
id?: number | string
},
t?: TFunction,
) {
const message = t
Expand Down
1 change: 1 addition & 0 deletions packages/payload/src/fields/hooks/beforeChange/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ export const beforeChange = async <T extends JsonObject>({
if (errors.length > 0) {
throw new ValidationError(
{
id,
collection: collection?.slug,
errors,
global: global?.slug,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export function convertLexicalPluginNodesToLexical({
parentNodeType: string
quiet?: boolean
}): SerializedLexicalNode[] {
if (!lexicalPluginNodes?.length) {
if (!lexicalPluginNodes?.length || !converters?.length) {
return []
}
const unknownConverter = converters.find((converter) => converter.nodeTypes.includes('unknown'))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export type LexicalPluginToLexicalFeatureProps = {
defaultConverters: LexicalPluginNodeConverter[]
}) => LexicalPluginNodeConverter[])
| LexicalPluginNodeConverter[]
disableHooks?: boolean
quiet?: boolean
}

Expand All @@ -37,23 +38,25 @@ export const LexicalPluginToLexicalFeature =

return {
ClientFeature: '@payloadcms/richtext-lexical/client#LexicalPluginToLexicalFeatureClient',
hooks: {
afterRead: [
({ value }) => {
if (!value || !('jsonContent' in value)) {
// incomingEditorState null or not from Lexical Plugin
return value
}
hooks: props.disableHooks
? undefined
: {
afterRead: [
({ value }) => {
if (!value || !('jsonContent' in value)) {
// incomingEditorState null or not from Lexical Plugin
return value
}

// Lexical Plugin => convert to lexical
return convertLexicalPluginToLexical({
converters: props.converters as LexicalPluginNodeConverter[],
lexicalPluginData: value as PayloadPluginLexicalData,
quiet: props?.quiet,
})
// Lexical Plugin => convert to lexical
return convertLexicalPluginToLexical({
converters: props.converters as LexicalPluginNodeConverter[],
lexicalPluginData: value as PayloadPluginLexicalData,
quiet: props?.quiet,
})
},
],
},
],
},
nodes: [
{
node: UnknownConvertedNode,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export function convertSlateNodesToLexical({
parentNodeType: string
slateNodes: SlateNode[]
}): SerializedLexicalNode[] {
if (!converters?.length) {
if (!converters?.length || !slateNodes?.length) {
return []
}
const unknownConverter = converters.find((converter) => converter.nodeTypes.includes('unknown'))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export type SlateToLexicalFeatureProps = {
converters?:
| (({ defaultConverters }: { defaultConverters: SlateNodeConverter[] }) => SlateNodeConverter[])
| SlateNodeConverter[]
disableHooks?: boolean
}

export const SlateToLexicalFeature = createServerFeature<
Expand All @@ -35,22 +36,24 @@ export const SlateToLexicalFeature = createServerFeature<

return {
ClientFeature: '@payloadcms/richtext-lexical/client#SlateToLexicalFeatureClient',
hooks: {
afterRead: [
({ value }) => {
if (!value || !Array.isArray(value) || 'root' in value) {
// incomingEditorState null or not from Slate
return value
}
hooks: props.disableHooks
? undefined
: {
afterRead: [
({ value }) => {
if (!value || !Array.isArray(value) || 'root' in value) {
// incomingEditorState null or not from Slate
return value
}

// Slate => convert to lexical
return convertSlateToLexical({
converters: props.converters as SlateNodeConverter[],
slateData: value,
})
// Slate => convert to lexical
return convertSlateToLexical({
converters: props.converters as SlateNodeConverter[],
slateData: value,
})
},
],
},
],
},
nodes: [
{
node: UnknownConvertedNode,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,12 +64,20 @@ async function migrateGlobal({
})

if (found) {
await payload.updateGlobal({
slug: global.slug,
data: document,
depth: 0,
locale: locale || undefined,
})
try {
await payload.updateGlobal({
slug: global.slug,
data: document,
depth: 0,
locale: locale || undefined,
})
// Catch it, because some errors were caused by the user previously (e.g. invalid relationships) and will throw an error now, even though they are not related to the migration
} catch (e) {
console.log('Error updating global', e, {
id: document.id,
slug: global.slug,
})
}
}
}

Expand Down Expand Up @@ -136,13 +144,21 @@ async function migrateCollection({
})

if (found) {
await payload.update({
id: document.id,
collection: collection.slug,
data: document,
depth: 0,
locale: locale || undefined,
})
try {
await payload.update({
id: document.id,
collection: collection.slug,
data: document,
depth: 0,
locale: locale || undefined,
})
// Catch it, because some errors were caused by the user previously (e.g. invalid relationships) and will throw an error now, even though they are not related to the migration
} catch (e) {
console.log('Error updating collection', e, {
id: document.id,
slug: collection.slug,
})
}
}
}
page++
Expand Down
27 changes: 18 additions & 9 deletions packages/richtext-lexical/src/validate/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,24 @@ export const richTextValidateHOC = ({
} = options

if (required) {
const hasChildren = value?.root?.children?.length

const hasOnlyEmptyParagraph =
(value?.root?.children?.length === 1 &&
value?.root?.children[0]?.type === 'paragraph' &&
(value?.root?.children[0] as SerializedParagraphNode)?.children?.length === 0) ||
((value?.root?.children[0] as SerializedParagraphNode)?.children?.length === 1 &&
(value?.root?.children[0] as SerializedParagraphNode)?.children[0]?.type === 'text' &&
(value?.root?.children[0] as SerializedParagraphNode)?.children[0]?.['text'] === '')
const hasChildren = !!value?.root?.children?.length

let hasOnlyEmptyParagraph = false
if (value?.root?.children?.length === 1) {
if (value?.root?.children[0]?.type === 'paragraph') {
const paragraphNode = value?.root?.children[0] as SerializedParagraphNode
if (paragraphNode?.children?.length === 0) {
hasOnlyEmptyParagraph = true
} else if (paragraphNode?.children?.length === 1) {
const paragraphNodeChild = paragraphNode?.children[0]
if (paragraphNodeChild.type === 'text') {
if (!paragraphNodeChild?.['text']?.length) {
hasOnlyEmptyParagraph = true
}
}
}
}
}

if (!hasChildren || hasOnlyEmptyParagraph) {
return t('validation:required')
Expand Down