Skip to content

Commit

Permalink
feat(richtext-lexical): Blocks: generate type definitions for blocks …
Browse files Browse the repository at this point in the history
…fields (#4529)
  • Loading branch information
AlessioGr committed Jan 30, 2024
1 parent 58bbd8c commit 90d7ee3
Show file tree
Hide file tree
Showing 11 changed files with 110 additions and 6 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,14 @@ type RichTextAdapterBase<
}) => Promise<void> | null
outputSchema?: ({
field,
interfaceNameDefinitions,
isRequired,
}: {
field: RichTextField<Value, AdapterProps, ExtraFieldProperties>
/**
* Allows you to define new top-level interfaces that can be re-used in the output schema.
*/
interfaceNameDefinitions: Map<string, JSONSchema4>
isRequired: boolean
}) => JSONSchema4
populationPromise?: (data: {
Expand Down
4 changes: 4 additions & 0 deletions packages/payload/src/bin/generateTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ export async function generateTypes(): Promise<void> {
style: {
singleQuote: true,
},
// Generates code for $defs that aren't referenced by the schema. Reason:
// If a field defines an interfaceName, it should be included in the generated types
// even if it's not used by another type. Reason: the user might want to use it in their own code.
unreachableDefinitions: true,
}).then((compiled) => {
if (config.typescript.declare !== false) {
compiled += `\n\n${declare}`
Expand Down
1 change: 1 addition & 0 deletions packages/payload/src/exports/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export { combineMerge } from '../utilities/combineMerge'
export {
configToJSONSchema,
entityToJSONSchema,
fieldsToJSONSchema,
withNullableJSONSchemaType,
} from '../utilities/configToJSONSchema'
export { createArrayFromCommaDelineated } from '../utilities/createArrayFromCommaDelineated'
Expand Down
12 changes: 10 additions & 2 deletions packages/payload/src/utilities/configToJSONSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,12 @@ export function withNullableJSONSchemaType(
return fieldTypes
}

function fieldsToJSONSchema(
export function fieldsToJSONSchema(
collectionIDFieldTypes: { [key: string]: 'number' | 'string' },
fields: Field[],
/**
* Allows you to define new top-level interfaces that can be re-used in the output schema.
*/
interfaceNameDefinitions: Map<string, JSONSchema4>,
): {
properties: {
Expand Down Expand Up @@ -144,6 +147,7 @@ function fieldsToJSONSchema(
if (field.editor.outputSchema) {
fieldSchema = field.editor.outputSchema({
field,
interfaceNameDefinitions,
isRequired,
})
} else {
Expand Down Expand Up @@ -524,8 +528,11 @@ export function configToJSONSchema(
config: SanitizedConfig,
defaultIDType?: 'number' | 'text',
): JSONSchema4 {
// a mutable Map to store custom top-level `interfaceName` types
// a mutable Map to store custom top-level `interfaceName` types. Fields with an `interfaceName` property will be moved to the top-level definitions here
const interfaceNameDefinitions: Map<string, JSONSchema4> = new Map()

// Collections and Globals have to be moved to the top-level definitions as well. Reason: The top-level type will be the `Config` type - we don't want all collection and global
// types to be inlined inside the `Config` type
const entityDefinitions: { [k: string]: JSONSchema4 } = [
...config.globals,
...config.collections,
Expand All @@ -537,6 +544,7 @@ export function configToJSONSchema(
return {
additionalProperties: false,
definitions: { ...entityDefinitions, ...Object.fromEntries(interfaceNameDefinitions) },
// These properties here will be very simple, as all the complexity is in the definitions. These are just the properties for the top-level `Config` type
properties: {
collections: generateEntitySchemas(config.collections || []),
globals: generateEntitySchemas(config.globals || []),
Expand Down
2 changes: 2 additions & 0 deletions packages/richtext-lexical/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"classnames": "^2.3.2",
"deep-equal": "2.2.3",
"i18next": "22.5.1",
"json-schema": "^0.4.0",
"lexical": "0.12.6",
"lodash": "4.17.21",
"react": "18.2.0",
Expand All @@ -42,6 +43,7 @@
},
"devDependencies": {
"@payloadcms/eslint-config": "workspace:*",
"@types/json-schema": "7.0.12",
"@types/node": "20.6.2",
"@types/react": "18.2.15",
"payload": "workspace:*"
Expand Down
18 changes: 16 additions & 2 deletions packages/richtext-lexical/src/field/features/Blocks/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { Block } from 'payload/types'
import type { Block, BlockField } from 'payload/types'

import { baseBlockFields } from 'payload/config'
import { formatLabels, getTranslation } from 'payload/utilities'
import { fieldsToJSONSchema, formatLabels, getTranslation } from 'payload/utilities'

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

Expand Down Expand Up @@ -31,6 +31,20 @@ export const BlocksFeature = (props?: BlocksFeatureProps): FeatureProvider => {
return {
feature: () => {
return {
generatedTypes: {
modifyOutputSchema: ({ currentSchema, field, interfaceNameDefinitions }) => {
const blocksField: BlockField = {
name: field?.name + '_lexical_blocks',
blocks: props.blocks,
type: 'blocks',
}
// This is only done so that interfaceNameDefinitions sets those block's interfaceNames.
// we don't actually use the JSON Schema itself in the generated types yet.
fieldsToJSONSchema({}, [blocksField], interfaceNameDefinitions)

return currentSchema
},
},
nodes: [
{
node: BlockNode,
Expand Down
41 changes: 41 additions & 0 deletions packages/richtext-lexical/src/field/features/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { Transformer } from '@lexical/markdown'
import type { JSONSchema4 } from 'json-schema'
import type { Klass, LexicalEditor, LexicalNode, SerializedEditorState } from 'lexical'
import type { SerializedLexicalNode } from 'lexical'
import type { LexicalNodeReplacement } from 'lexical'
Expand Down Expand Up @@ -65,6 +66,25 @@ export type Feature = {
floatingSelectToolbar?: {
sections: FloatingToolbarSection[]
}
generatedTypes?: {
modifyOutputSchema: ({
currentSchema,
field,
interfaceNameDefinitions,
isRequired,
}: {
/**
* Current schema which will be modified by this function.
*/
currentSchema: JSONSchema4
field: RichTextField<SerializedEditorState, AdapterProps>
/**
* Allows you to define new top-level interfaces that can be re-used in the output schema.
*/
interfaceNameDefinitions: Map<string, JSONSchema4>
isRequired: boolean
}) => JSONSchema4
}
hooks?: {
afterReadPromise?: ({
field,
Expand Down Expand Up @@ -200,6 +220,27 @@ export type SanitizedFeatures = Required<
floatingSelectToolbar: {
sections: FloatingToolbarSection[]
}
generatedTypes: {
modifyOutputSchemas: Array<
({
currentSchema,
field,
interfaceNameDefinitions,
isRequired,
}: {
/**
* Current schema which will be modified by this function.
*/
currentSchema: JSONSchema4
field: RichTextField<SerializedEditorState, AdapterProps>
/**
* Allows you to define new top-level interfaces that can be re-used in the output schema.
*/
interfaceNameDefinitions: Map<string, JSONSchema4>
isRequired: boolean
}) => JSONSchema4
>
}
hooks: {
afterReadPromises: Array<
({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ export const sanitizeFeatures = (features: ResolvedFeatureMap): SanitizedFeature
floatingSelectToolbar: {
sections: [],
},
generatedTypes: {
modifyOutputSchemas: [],
},
hooks: {
afterReadPromises: [],
load: [],
Expand All @@ -29,6 +32,9 @@ export const sanitizeFeatures = (features: ResolvedFeatureMap): SanitizedFeature
}

features.forEach((feature) => {
if (feature?.generatedTypes?.modifyOutputSchema) {
sanitized.generatedTypes.modifyOutputSchemas.push(feature.generatedTypes.modifyOutputSchema)
}
if (feature.hooks) {
if (feature.hooks.afterReadPromise) {
sanitized.hooks.afterReadPromises = sanitized.hooks.afterReadPromises.concat(
Expand Down
16 changes: 14 additions & 2 deletions packages/richtext-lexical/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { JSONSchema4 } from 'json-schema'
import type { SerializedEditorState } from 'lexical'
import type { EditorConfig as LexicalEditorConfig } from 'lexical/LexicalEditor'
import type { RichTextAdapter } from 'payload/types'
Expand Down Expand Up @@ -98,8 +99,8 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte
})
},
editorConfig: finalSanitizedEditorConfig,
outputSchema: ({ isRequired }) => {
return {
outputSchema: ({ field, interfaceNameDefinitions, isRequired }) => {
let outputSchema: JSONSchema4 = {
// This schema matches the SerializedEditorState type so far, that it's possible to cast SerializedEditorState to this schema without any errors.
// In the future, we should
// 1) allow recursive children
Expand Down Expand Up @@ -155,6 +156,17 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte
required: ['root'],
type: withNullableJSONSchemaType('object', isRequired),
}
for (const modifyOutputSchema of finalSanitizedEditorConfig.features.generatedTypes
.modifyOutputSchemas) {
outputSchema = modifyOutputSchema({
currentSchema: outputSchema,
field,
interfaceNameDefinitions,
isRequired,
})
}

return outputSchema
},
populationPromise({
context,
Expand Down
10 changes: 10 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions test/fields/collections/Lexical/blocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ export const TextBlock: Block = {
}

export const RadioButtonsBlock: Block = {
interfaceName: 'LexicalBlocksRadioButtonsBlock',
fields: [
{
name: 'radioButtons',
Expand Down

0 comments on commit 90d7ee3

Please sign in to comment.