Skip to content

Commit

Permalink
feat(richtext-lexical): mdx support (#9160)
Browse files Browse the repository at this point in the history
Supports bi-directional import/export between MDX <=> Lexical. JSX will
be mapped to lexical blocks back and forth.

This will allow editing our mdx docs in payload while keeping mdx as the
source of truth

---------

Co-authored-by: Germán Jabloñski <43938777+GermanJablo@users.noreply.github.com>
  • Loading branch information
AlessioGr and GermanJablo authored Nov 17, 2024
1 parent 324af8a commit d4f1add
Show file tree
Hide file tree
Showing 79 changed files with 7,547 additions and 311 deletions.
2 changes: 1 addition & 1 deletion docs/admin/overview.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ The following options are available:

<Banner type="success">
<strong>Tip:</strong>
You can easily add _new_ routes to the Admin Panel through [Custom Endpoints](../rest-api/overview#custom-endpoints) and [Custom Views](./views).
You can easily add _new_ routes to the Admin Panel through [Custom Endpoints](../rest-api/overview#custom-endpoints) and [Custom Views](./views).
</Banner>

#### Customizing Root-level Routes
Expand Down
3 changes: 1 addition & 2 deletions docs/lexical/converters.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -334,8 +334,7 @@ This has been taken from the [lexical serialization & deserialization docs](http
Convert markdown content to the Lexical editor format with the following:

```ts
import { $convertFromMarkdownString } from '@lexical/markdown'
import { sanitizeServerEditorConfig } from '@payloadcms/richtext-lexical'
import { sanitizeServerEditorConfig, $convertFromMarkdownString } from '@payloadcms/richtext-lexical'

const yourSanitizedEditorConfig = sanitizeServerEditorConfig(yourEditorConfig, payloadConfig) // <= your editor config & Payload Config here
const markdown = `# Hello World`
Expand Down
2 changes: 1 addition & 1 deletion eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export const rootParserOptions = {
ecmaVersion: 'latest',
projectService: {
maximumDefaultProjectFileMatchCount_THIS_WILL_SLOW_DOWN_LINTING: 40,
allowDefaultProject: ['scripts/*.ts', '*.js', '*.mjs', '*.spec.ts', '*.d.ts'],
allowDefaultProject: ['scripts/*.ts', '*.js', '*.mjs', '*.d.ts'],
},
}

Expand Down
2 changes: 2 additions & 0 deletions packages/next/src/layouts/Root/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ let checkedDependencies = false
export const RootLayout = async ({
children,
config: configPromise,
importMap,
serverFunction,
}: {
readonly children: React.ReactNode
Expand Down Expand Up @@ -136,6 +137,7 @@ export const RootLayout = async ({
const clientConfig = await getClientConfig({
config,
i18n,
importMap,
})

return (
Expand Down
11 changes: 8 additions & 3 deletions packages/next/src/utilities/getClientConfig.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
import type { I18nClient } from '@payloadcms/translations'
import type { ClientConfig, SanitizedConfig } from 'payload'
import type { ClientConfig, ImportMap, SanitizedConfig } from 'payload'

import { createClientConfig } from 'payload'
import { cache } from 'react'

export const getClientConfig = cache(
async (args: { config: SanitizedConfig; i18n: I18nClient }): Promise<ClientConfig> => {
const { config, i18n } = args
async (args: {
config: SanitizedConfig
i18n: I18nClient
importMap: ImportMap
}): Promise<ClientConfig> => {
const { config, i18n, importMap } = args

const clientConfig = createClientConfig({
config,
i18n,
importMap,
})

return Promise.resolve(clientConfig)
Expand Down
6 changes: 5 additions & 1 deletion packages/next/src/views/Document/handleServerFunction.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type {
Data,
DocumentPreferences,
FormState,
ImportMap,
PayloadRequest,
SanitizedConfig,
VisibleEntities,
Expand All @@ -23,8 +24,9 @@ if (!cachedClientConfig) {
export const getClientConfig = (args: {
config: SanitizedConfig
i18n: I18nClient
importMap: ImportMap
}): ClientConfig => {
const { config, i18n } = args
const { config, i18n, importMap } = args

if (cachedClientConfig && process.env.NODE_ENV !== 'development') {
return cachedClientConfig
Expand All @@ -33,6 +35,7 @@ export const getClientConfig = (args: {
cachedClientConfig = createClientConfig({
config,
i18n,
importMap,
})

return cachedClientConfig
Expand Down Expand Up @@ -112,6 +115,7 @@ export const renderDocumentHandler = async (args: {
const clientConfig = getClientConfig({
config,
i18n,
importMap: req.payload.importMap,
})

let preferences: DocumentPreferences
Expand Down
6 changes: 5 additions & 1 deletion packages/next/src/views/List/handleServerFunction.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { I18nClient } from '@payloadcms/translations'
import type { ListPreferences } from '@payloadcms/ui'
import type {
ClientConfig,
ImportMap,
ListQuery,
PayloadRequest,
SanitizedConfig,
Expand All @@ -22,8 +23,9 @@ if (!cachedClientConfig) {
export const getClientConfig = (args: {
config: SanitizedConfig
i18n: I18nClient
importMap: ImportMap
}): ClientConfig => {
const { config, i18n } = args
const { config, i18n, importMap } = args

if (cachedClientConfig && process.env.NODE_ENV !== 'development') {
return cachedClientConfig
Expand All @@ -32,6 +34,7 @@ export const getClientConfig = (args: {
cachedClientConfig = createClientConfig({
config,
i18n,
importMap,
})

return cachedClientConfig
Expand Down Expand Up @@ -114,6 +117,7 @@ export const renderListHandler = async (args: {
const clientConfig = getClientConfig({
config,
i18n,
importMap: payload.importMap,
})

const preferencesKey = `${collectionSlug}-list`
Expand Down
1 change: 1 addition & 0 deletions packages/next/src/views/Root/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ export const RootPage = async ({
const clientConfig = await getClientConfig({
config,
i18n: initPageResult?.req.i18n,
importMap,
})

const RenderedView = (
Expand Down
34 changes: 34 additions & 0 deletions packages/payload/src/bin/generateImportMap/getFromImportMap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import type { PayloadComponent } from '../../config/types.js'
import type { ImportMap } from './index.js'

import { parsePayloadComponent } from './parsePayloadComponent.js'

export const getFromImportMap = <TOutput>(args: {
importMap: ImportMap
PayloadComponent: PayloadComponent
schemaPath?: string
silent?: boolean
}): TOutput => {
const { importMap, PayloadComponent, schemaPath, silent } = args

const { exportName, path } = parsePayloadComponent(PayloadComponent)

const key = path + '#' + exportName

const importMapEntry = importMap[key]

if (!importMapEntry && !silent) {
// eslint-disable-next-line no-console
console.error(
`getFromImportMap: PayloadComponent not found in importMap`,
{
key,
PayloadComponent,
schemaPath,
},
'You may need to run the `payload generate:importmap` command to generate the importMap ahead of runtime.',
)
}

return importMapEntry
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-unused-expressions */
import type { AdminViewConfig } from '../../admin/views/types.js'
import type { SanitizedConfig } from '../../config/types.js'
import type { AddToImportMap, Imports, InternalImportMap } from './index.js'
Expand Down
2 changes: 2 additions & 0 deletions packages/payload/src/bin/generateImportMap/iterateFields.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ export function genImportMapIterateFields({
}
}

hasKey(field?.admin, 'jsx') && addToImportMap(field.admin.jsx) // For Blocks

hasKey(field?.admin?.components, 'Label') && addToImportMap(field.admin.components.Label)

hasKey(field?.admin?.components, 'Block') && addToImportMap(field.admin.components.Block)
Expand Down
7 changes: 7 additions & 0 deletions packages/payload/src/collections/config/client.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { I18nClient } from '@payloadcms/translations'

import type { StaticDescription } from '../../admin/types.js'
import type { ImportMap } from '../../bin/generateImportMap/index.js'
import type {
LivePreviewConfig,
ServerOnlyLivePreviewProperties,
Expand Down Expand Up @@ -84,10 +85,12 @@ export const createClientCollectionConfig = ({
collection,
defaultIDType,
i18n,
importMap,
}: {
collection: SanitizedCollectionConfig
defaultIDType: Payload['config']['db']['defaultIDType']
i18n: I18nClient
importMap: ImportMap
}): ClientCollectionConfig => {
const clientCollection = deepCopyObjectSimple(
collection,
Expand All @@ -99,6 +102,7 @@ export const createClientCollectionConfig = ({
defaultIDType,
fields: collection.fields,
i18n,
importMap,
})

serverOnlyCollectionProperties.forEach((key) => {
Expand Down Expand Up @@ -185,10 +189,12 @@ export const createClientCollectionConfigs = ({
collections,
defaultIDType,
i18n,
importMap,
}: {
collections: SanitizedCollectionConfig[]
defaultIDType: Payload['config']['db']['defaultIDType']
i18n: I18nClient
importMap: ImportMap
}): ClientCollectionConfig[] => {
const clientCollections = new Array(collections.length)

Expand All @@ -199,6 +205,7 @@ export const createClientCollectionConfigs = ({
collection,
defaultIDType,
i18n,
importMap,
})
}

Expand Down
5 changes: 5 additions & 0 deletions packages/payload/src/config/client.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { I18nClient } from '@payloadcms/translations'

import type { ImportMap } from '../bin/generateImportMap/index.js'
import type {
LivePreviewConfig,
SanitizedConfig,
Expand Down Expand Up @@ -74,9 +75,11 @@ export const serverOnlyConfigProperties: readonly Partial<ServerOnlyRootProperti
export const createClientConfig = ({
config,
i18n,
importMap,
}: {
config: SanitizedConfig
i18n: I18nClient
importMap: ImportMap
}): ClientConfig => {
// We can use deepCopySimple here, as the clientConfig should be JSON serializable anyways, since it will be sent from server => client
const clientConfig = deepCopyObjectSimple(config, true) as unknown as ClientConfig
Expand Down Expand Up @@ -119,12 +122,14 @@ export const createClientConfig = ({
collections: config.collections,
defaultIDType: config.db.defaultIDType,
i18n,
importMap,
})

clientConfig.globals = createClientGlobalConfigs({
defaultIDType: config.db.defaultIDType,
globals: config.globals,
i18n,
importMap,
})

return clientConfig
Expand Down
1 change: 1 addition & 0 deletions packages/payload/src/exports/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export {
getCookieExpiration,
parseCookies,
} from '../auth/cookies.js'
export { getFromImportMap } from '../bin/generateImportMap/getFromImportMap.js'
export { parsePayloadComponent } from '../bin/generateImportMap/parsePayloadComponent.js'
export { defaults as collectionDefaults } from '../collections/config/defaults.js'

Expand Down
24 changes: 21 additions & 3 deletions packages/payload/src/fields/config/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { I18nClient } from '@payloadcms/translations'

import type {
AdminClient,
BlockJSX,
BlocksFieldClient,
ClientBlock,
ClientField,
Expand All @@ -15,9 +16,10 @@ import type {
} from '../../fields/config/types.js'
import type { Payload } from '../../types/index.js'

import { getFromImportMap } from '../../bin/generateImportMap/getFromImportMap.js'
import { MissingEditorProp } from '../../errors/MissingEditorProp.js'
import { fieldAffectsData } from '../../fields/config/types.js'
import { flattenTopLevelFields } from '../../index.js'
import { flattenTopLevelFields, type ImportMap } from '../../index.js'
import { removeUndefined } from '../../utilities/removeUndefined.js'

// Should not be used - ClientField should be used instead. This is why we don't export ClientField, we don't want people
Expand All @@ -42,11 +44,13 @@ export const createClientField = ({
defaultIDType,
field: incomingField,
i18n,
importMap,
}: {
clientField?: ClientField
defaultIDType: Payload['config']['db']['defaultIDType']
field: Field
i18n: I18nClient
importMap: ImportMap
}): ClientField => {
const serverOnlyFieldProperties: Partial<ServerOnlyFieldProperties>[] = [
'hooks',
Expand Down Expand Up @@ -128,6 +132,7 @@ export const createClientField = ({
disableAddingID: incomingField.type !== 'array',
fields: incomingField.fields,
i18n,
importMap,
})

break
Expand All @@ -154,6 +159,15 @@ export const createClientField = ({
}
}

if (block?.admin?.jsx) {
const jsxResolved = getFromImportMap<BlockJSX>({
importMap,
PayloadComponent: block.admin.jsx,
schemaPath: '',
})
clientBlock.jsx = jsxResolved
}

if (block.labels) {
clientBlock.labels = {} as unknown as LabelsClient

Expand All @@ -176,6 +190,7 @@ export const createClientField = ({
defaultIDType,
fields: block.fields,
i18n,
importMap,
})

if (!field.blocks) {
Expand All @@ -190,8 +205,7 @@ export const createClientField = ({
}

case 'radio':

// eslint-disable-next-line no-fallthrough
// falls through
case 'select': {
const field = clientField as RadioFieldClient | SelectFieldClient

Expand Down Expand Up @@ -246,6 +260,7 @@ export const createClientField = ({
disableAddingID: true,
fields: tab.fields,
i18n,
importMap,
})
}
}
Expand Down Expand Up @@ -295,12 +310,14 @@ export const createClientFields = ({
disableAddingID,
fields,
i18n,
importMap,
}: {
clientFields: ClientField[]
defaultIDType: Payload['config']['db']['defaultIDType']
disableAddingID?: boolean
fields: Field[]
i18n: I18nClient
importMap: ImportMap
}): ClientField[] => {
const newClientFields: ClientField[] = []

Expand All @@ -312,6 +329,7 @@ export const createClientFields = ({
defaultIDType,
field,
i18n,
importMap,
})

if (newField) {
Expand Down
Loading

0 comments on commit d4f1add

Please sign in to comment.