Compare commits

...

56 Commits

Author SHA1 Message Date
Elliot DeNolf
27589482dd chore(release): @payloadcms/db-postgres/0.1.7 2023-10-15 14:12:02 -04:00
James Mikrut
d7ab4b7062 Merge pull request #3642 from payloadcms/fix/#3568-postgres-relationships-in-array
fix(db-postgres): query relationship in array alias
2023-10-15 11:31:57 -04:00
James
2c8fbf1be3 chore: adds specificity to tests 2023-10-15 11:08:16 -04:00
James
eec88f8f1b Merge branch 'fix/#3568-postgres-relationships-in-array' of github.com:payloadcms/payload into fix/#3568-postgres-relationships-in-array 2023-10-15 10:43:32 -04:00
James
1481ef97b5 Merge branch 'main' of github.com:payloadcms/payload 2023-10-15 09:44:19 -04:00
James
a89e89fb80 chore: documents pagination: false 2023-10-15 09:44:00 -04:00
Alessio Gravili
7e7eeb059d chore(richtext-lexical): add 'use client' to field and cell 2023-10-15 14:25:36 +02:00
Alessio Gravili
dc2a502dcc perf(richtext-lexical): remove unnecessary prop drilling and load hooks being run for initialState 2023-10-15 14:07:24 +02:00
Elliot DeNolf
a9a5ba82d8 chore: update pnpm-lock.yaml 2023-10-14 21:09:15 -04:00
James
e6e8fae1c5 Merge branch 'main' of github.com:payloadcms/payload 2023-10-14 19:24:50 -04:00
James
a05868a7f3 chore: remove unnecessary peer dep 2023-10-14 19:24:32 -04:00
Elliot DeNolf
f27cd26575 chore(release): richtext-lexical/0.1.11 2023-10-14 17:35:44 -04:00
Elliot DeNolf
db835ea5c8 chore(release): db-postgres/0.1.6 2023-10-14 17:35:26 -04:00
Jacob Fletcher
d8f265fb94 chore(examples): bumps deps to latest (#3655) 2023-10-14 16:49:47 -04:00
Alessio Gravili
d81d4eb075 feat(richtext-lexical): LexicalPluginToLexical migration feature 2023-10-14 22:36:16 +02:00
James
52f89c0136 fix(db-postgres): ensures columns are nullable if within field with condition 2023-10-14 15:34:27 -04:00
Dan Ribbens
b0083b7c07 test: fix missing variable 2023-10-14 15:32:06 -04:00
Dan Ribbens
21649537a6 fix(db-postgres): query relationship path inside arrays 2023-10-14 15:17:42 -04:00
Elliot DeNolf
9be34c9599 chore(release): richtext-lexical/0.1.10 2023-10-14 14:38:58 -04:00
Elliot DeNolf
8ca632e541 chore(release): richtext-slate/1.0.5 2023-10-14 14:38:48 -04:00
Elliot DeNolf
2ef79145a4 chore(release): payload/2.0.6 2023-10-14 14:37:18 -04:00
James
a0641a445d Merge branch 'main' of github.com:payloadcms/payload 2023-10-14 14:34:46 -04:00
James
3a2e78f7f3 chore: add peer deps for richtext packages 2023-10-14 14:34:40 -04:00
James Mikrut
976d69d154 Merge pull request #3657 from payloadcms/chore/export-pattern
chore: properly separates server / client exports
2023-10-14 14:32:11 -04:00
James
66018362fe chore: properly separates server / client exports 2023-10-14 14:08:08 -04:00
Elliot DeNolf
647fe23d1c chore(release): richtext-lexical/0.1.9 2023-10-14 12:26:28 -04:00
Elliot DeNolf
d7c61861f6 chore(release): richtext-slate/1.0.4 2023-10-14 12:26:19 -04:00
Elliot DeNolf
7caa098023 chore(release): db-postgres/0.1.5 2023-10-14 12:25:54 -04:00
James Mikrut
fd54c40400 Merge pull request #3654 from payloadcms/chore/dynamic-drizzle-kit-import
chore: only imports drizzle-kit if it will be used
2023-10-14 12:21:14 -04:00
James
e180131314 chore: only imports drizzle-kit if it will be used 2023-10-14 12:13:13 -04:00
James
5902d4542b Merge branch 'main' of github.com:payloadcms/payload 2023-10-14 11:51:13 -04:00
James
6bc282444e chore: slate compatibility with next-payload 2023-10-14 11:49:38 -04:00
Alessio Gravili
4dc6c09347 feat(richtext-lexical): SlateToLexical migration feature 2023-10-14 13:36:32 +02:00
Elliot DeNolf
03b9ab0054 chore: cleanup scripts 2023-10-13 16:34:37 -04:00
Elliot DeNolf
3c3c93f483 chore(release): richtext-lexical/0.1.8 2023-10-13 16:05:59 -04:00
Alessio Gravili
5dbfb1a335 fix(richtext-lexical): Blocks: working population for crazy amounts of nesting 2023-10-13 21:04:56 +02:00
Alessio Gravili
d411874589 chore(richtext-lexical): Blocks: clean up population 2023-10-13 20:02:18 +02:00
Jacob Fletcher
8358e2f2d2 chore: properly scopes selector in bulk update e2e test (#3640) 2023-10-13 13:51:52 -04:00
Dan Ribbens
2c67eff059 fix(db-postgres): query relationship in array alias 2023-10-13 13:32:44 -04:00
Elliot DeNolf
012b8e6f90 chore: remove pnpm from engines, shows warning when not using pnpm 2023-10-13 13:05:25 -04:00
Jacob Fletcher
fcd4c8d830 fix: document sidebar vertical overflow (#3639) 2023-10-13 13:00:02 -04:00
Elliot DeNolf
81ec435363 chore(release): richtext-lexical/0.1.7 2023-10-13 12:49:08 -04:00
Jacob Fletcher
e116fcfbf5 docs: updates references of master to main 2023-10-13 12:44:45 -04:00
Alessio Gravili
c47632dc1d fix(richtext-lexical): Blocks: Nested Blocks having incorrect initial data (e.g. missing rows property) (#3638)
* fix(richtext-lexical): Blocks: Sub-Blocks having incorrect initial data (e.g. missing rows property)

* chore: remove unnecessary comment
2023-10-13 18:39:34 +02:00
Jacob Fletcher
0dab68b336 chore: prevents group fields from overflowing into the sidebar (#3637) 2023-10-13 12:04:39 -04:00
Jacob Fletcher
483f93bfcf chore: cleans up admin e2e tests (#3636) 2023-10-13 12:04:05 -04:00
Jessica Chowdhury
4bd01df411 fix: login form clearing out and field spacing (#3633) 2023-10-13 11:15:07 -04:00
Jessica Chowdhury
c956a85252 fix: sidebar field permissions (#3629)
Co-authored-by: Jacob Fletcher <jacobsfletch@gmail.com>
2023-10-13 10:29:22 -04:00
Jacob Fletcher
beed83b231 fix: preview button conditions (#3613) 2023-10-13 10:23:26 -04:00
James Mikrut
3b1bdcbe41 chore: de-duplicates array / block data from form state (#3607)
* chore: consolidates array manipulation tests

---------

Co-authored-by: Jarrod Flesch <jarrodmflesch@gmail.com>
2023-10-13 09:45:00 -04:00
James Mikrut
d3d0971275 Merge pull request #3630 from payloadcms/fix/duplicating-drafts
fix: allows drafts to be duplicated
2023-10-13 08:50:36 -04:00
Jessica Boezwinkle
1a99d66cd0 fix: allows drafts to be duplicated 2023-10-13 11:59:23 +01:00
Elliot DeNolf
52c4a63bf1 chore(release): richtext-slate/1.0.3 2023-10-12 23:38:38 -04:00
Elliot DeNolf
3446d28602 chore(release): richtext-lexical/0.1.6 2023-10-12 23:38:29 -04:00
Elliot DeNolf
2eb18771a1 chore(release): live-preview-react/0.1.3 2023-10-12 23:38:25 -04:00
Elliot DeNolf
f6fd5d6742 chore(release): live-preview/0.1.3 2023-10-12 23:38:20 -04:00
144 changed files with 7575 additions and 3827 deletions

View File

@@ -129,7 +129,7 @@ To add a _new_ view to the Admin Panel, simply add another key to the `views` ob
}
```
_For more examples regarding how to customize components, look at the following [examples](https://github.com/payloadcms/payload/tree/master/test/admin/components)._
_For more examples regarding how to customize components, look at the following [examples](https://github.com/payloadcms/payload/tree/main/test/admin/components)._
For help on how to build your own custom view components, see [building a custom view component](#building-a-custom-view-component).
@@ -399,12 +399,12 @@ Your custom view components will be given all the props that a React Router `<Ro
#### Example
You can find examples of custom views in the [Payload source code `/test/admin/components/views` folder](https://github.com/payloadcms/payload/tree/master/test/admin/components/views). There, you'll find two custom views:
You can find examples of custom views in the [Payload source code `/test/admin/components/views` folder](https://github.com/payloadcms/payload/tree/main/test/admin/components/views). There, you'll find two custom views:
1. A custom view that uses the `DefaultTemplate`, which is the built-in Payload template that displays the sidebar and "eyebrow nav"
1. A custom view that uses the `MinimalTemplate` - which is just a centered template used for things like logging in or out
To see how to pass in your custom views to create custom views of your own, take a look at the `admin.components.views` property of the [Payload test admin config](https://github.com/payloadcms/payload/blob/master/test/admin/config.ts).
To see how to pass in your custom views to create custom views of your own, take a look at the `admin.components.views` property of the [Payload test admin config](https://github.com/payloadcms/payload/blob/main/test/admin/config.ts).
### Fields

View File

@@ -131,6 +131,7 @@ const result = await payload.find({
depth: 2,
page: 1,
limit: 10,
pagination: false, // If you want to disable pagination count, etc.
where: {}, // pass a `where` query here
sort: '-title',
locale: 'en',

View File

@@ -59,3 +59,7 @@ All Payload APIs support the pagination controls below. With them, you can creat
| ------- | --------------------------------------- |
| `limit` | Limits the number of documents returned |
| `page` | Get a specific page number |
### Disabling pagination within Local API
For `find` operations within the Local API, you can disable pagination to retrieve all documents from a collection by passing `pagination: false` to the `find` local operation. This is not supported in REST or GraphQL, however, because it could potentially lead to malicious activity.

View File

@@ -17,9 +17,9 @@
"lint:fix": "eslint --fix --ext .ts,.tsx src"
},
"dependencies": {
"@payloadcms/bundler-webpack": "^1.0.0-beta.5",
"@payloadcms/db-mongodb": "^1.0.0-beta.8",
"@payloadcms/richtext-slate": "^1.0.0-beta.4",
"@payloadcms/bundler-webpack": "latest",
"@payloadcms/db-mongodb": "latest",
"@payloadcms/richtext-slate": "latest",
"dotenv": "^8.2.0",
"express": "^4.17.1",
"payload": "latest"

File diff suppressed because it is too large Load Diff

View File

@@ -20,9 +20,9 @@
"lint:fix": "eslint --fix --ext .ts,.tsx src"
},
"dependencies": {
"@payloadcms/bundler-webpack": "^1.0.0-beta.5",
"@payloadcms/db-mongodb": "^1.0.0-beta.8",
"@payloadcms/richtext-slate": "^1.0.0-beta.4",
"@payloadcms/bundler-webpack": "latest",
"@payloadcms/db-mongodb": "latest",
"@payloadcms/richtext-slate": "latest",
"dotenv": "^8.2.0",
"escape-html": "^1.0.3",
"express": "^4.17.1",

File diff suppressed because it is too large Load Diff

View File

@@ -18,9 +18,9 @@
"lint:fix": "eslint --fix --ext .ts,.tsx src"
},
"dependencies": {
"@payloadcms/bundler-webpack": "^1.0.0-beta.5",
"@payloadcms/db-mongodb": "^1.0.0-beta.8",
"@payloadcms/richtext-slate": "^1.0.0-beta.4",
"@payloadcms/bundler-webpack": "latest",
"@payloadcms/db-mongodb": "latest",
"@payloadcms/richtext-slate": "latest",
"dotenv": "^8.2.0",
"express": "^4.17.1",
"payload": "latest"

File diff suppressed because it is too large Load Diff

View File

@@ -15,9 +15,9 @@
"generate:graphQLSchema": "PAYLOAD_CONFIG_PATH=src/payload.config.ts payload generate:graphQLSchema"
},
"dependencies": {
"@payloadcms/bundler-webpack": "^1.0.0-beta.5",
"@payloadcms/db-mongodb": "^1.0.0-beta.8",
"@payloadcms/richtext-slate": "^1.0.0-beta.4",
"@payloadcms/bundler-webpack": "latest",
"@payloadcms/db-mongodb": "latest",
"@payloadcms/richtext-slate": "latest",
"@faceless-ui/modal": "^2.0.1",
"@payloadcms/plugin-form-builder": "^1.0.12",
"@payloadcms/plugin-seo": "^1.0.8",

File diff suppressed because it is too large Load Diff

View File

@@ -18,9 +18,9 @@
"lint:fix": "eslint --fix --ext .ts,.tsx src"
},
"dependencies": {
"@payloadcms/bundler-webpack": "^1.0.0-beta.5",
"@payloadcms/db-mongodb": "^1.0.0-beta.8",
"@payloadcms/richtext-slate": "^1.0.0-beta.4",
"@payloadcms/bundler-webpack": "latest",
"@payloadcms/db-mongodb": "latest",
"@payloadcms/richtext-slate": "latest",
"dotenv": "^8.2.0",
"express": "^4.17.1",
"payload": "latest"

File diff suppressed because it is too large Load Diff

View File

@@ -18,9 +18,9 @@
"lint:fix": "eslint --fix --ext .ts,.tsx src"
},
"dependencies": {
"@payloadcms/bundler-webpack": "^1.0.0-beta.5",
"@payloadcms/db-mongodb": "^1.0.0-beta.8",
"@payloadcms/richtext-slate": "^1.0.0-beta.4",
"@payloadcms/bundler-webpack": "latest",
"@payloadcms/db-mongodb": "latest",
"@payloadcms/richtext-slate": "latest",
"dotenv": "^8.2.0",
"express": "^4.17.1",
"payload": "latest"

File diff suppressed because it is too large Load Diff

View File

@@ -17,9 +17,9 @@
"lint:fix": "eslint --fix --ext .ts,.tsx src"
},
"dependencies": {
"@payloadcms/bundler-webpack": "^1.0.0-beta.5",
"@payloadcms/db-mongodb": "^1.0.0-beta.8",
"@payloadcms/richtext-slate": "^1.0.0-beta.4",
"@payloadcms/bundler-webpack": "latest",
"@payloadcms/db-mongodb": "latest",
"@payloadcms/richtext-slate": "latest",
"@payloadcms/plugin-redirects": "^1.0.0",
"dotenv": "^8.2.0",
"express": "^4.17.1",

File diff suppressed because it is too large Load Diff

View File

@@ -20,8 +20,8 @@
"lint-staged": "lint-staged",
"pretest": "pnpm build",
"reinstall": "pnpm clean:unix && pnpm install",
"list:packages": "./scripts/list_published_packages.sh beta",
"script:release:beta": "./scripts/release_beta.sh",
"script:list-packages": "tsx ./scripts/list-packages.ts",
"script:release": "tsx ./scripts/release.ts",
"test": "pnpm test:int && pnpm test:components && pnpm test:e2e",
"test:components": "cross-env jest --config=jest.components.config.js",
"test:e2e": "npx playwright install --with-deps && ts-node -T ./test/runE2E.ts",

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/db-postgres",
"version": "0.1.4",
"version": "0.1.7",
"description": "The officially supported Postgres database adapter for Payload",
"repository": "https://github.com/payloadcms/payload",
"license": "MIT",
@@ -34,9 +34,6 @@
"@types/to-snake-case": "1.0.0",
"payload": "workspace:*"
},
"peerDependencies": {
"better-sqlite3": "^8.5.0"
},
"publishConfig": {
"main": "./dist/index.js",
"registry": "https://registry.npmjs.org/",

View File

@@ -1,6 +1,5 @@
import type { Connect } from 'payload/database'
import { pushSchema } from 'drizzle-kit/utils'
import { eq, sql } from 'drizzle-orm'
import { drizzle } from 'drizzle-orm/node-postgres'
import { numeric, pgTable, timestamp, varchar } from 'drizzle-orm/pg-core'
@@ -40,6 +39,8 @@ export const connect: Connect = async function connect(this: PostgresAdapter, pa
)
return
const { pushSchema } = require('drizzle-kit/utils')
// This will prompt if clarifications are needed for Drizzle to push new schema
const { apply, hasDataLoss, statementsToExecute, warnings } = await pushSchema(
this.schema,

View File

@@ -2,7 +2,6 @@
import type { DrizzleSnapshotJSON } from 'drizzle-kit/utils'
import type { CreateMigration } from 'payload/database'
import { generateDrizzleJson, generateMigration } from 'drizzle-kit/utils'
import fs from 'fs'
import prompts from 'prompts'
@@ -61,6 +60,8 @@ export const createMigration: CreateMigration = async function createMigration(
fs.mkdirSync(dir)
}
const { generateDrizzleJson, generateMigration } = require('drizzle-kit/utils')
const [yyymmdd, hhmmss] = new Date().toISOString().split('T')
const formattedDate = yyymmdd.replace(/\D/g, '')
const formattedTime = hhmmss.split('.')[0].replace(/\D/g, '')

View File

@@ -2,9 +2,7 @@
import type { Payload } from 'payload'
import type { Migration } from 'payload/database'
import { generateDrizzleJson } from 'drizzle-kit/utils'
import { readMigrationFiles } from 'payload/database'
import { DatabaseError } from 'pg'
import prompts from 'prompts'
import type { PostgresAdapter } from './types'
@@ -78,6 +76,8 @@ export async function migrate(this: PostgresAdapter): Promise<void> {
}
async function runMigrationFile(payload: Payload, migration: Migration, batch: number) {
const { generateDrizzleJson } = require('drizzle-kit/utils')
const start = Date.now()
payload.logger.info({ msg: `Migrating: ${migration.name}` })

View File

@@ -34,12 +34,14 @@ type Args = {
aliasTable?: GenericTable
collectionPath: string
columnPrefix?: string
constraintPath?: string
constraints?: Constraint[]
fields: (Field | TabAsField)[]
joinAliases: BuildQueryJoinAliases
joins: BuildQueryJoins
locale?: string
pathSegments: string[]
rootTableName?: string
selectFields: Record<string, GenericColumn>
tableName: string
}
@@ -53,17 +55,22 @@ export const getTableColumnFromPath = ({
aliasTable,
collectionPath,
columnPrefix = '',
constraintPath: incomingConstraintPath,
constraints = [],
fields,
joinAliases,
joins,
locale: incomingLocale,
pathSegments: incomingSegments,
rootTableName: incomingRootTableName,
selectFields,
tableName,
}: Args): TableColumn => {
const fieldPath = incomingSegments[0]
let locale = incomingLocale
const rootTableName = incomingRootTableName || tableName
let constraintPath = incomingConstraintPath || ''
const field = flattenTopLevelFields(fields as Field[]).find(
(fieldToFind) => fieldAffectsData(fieldToFind) && fieldToFind.name === fieldPath,
) as Field | TabAsField
@@ -105,6 +112,7 @@ export const getTableColumnFromPath = ({
aliasTable,
collectionPath,
columnPrefix,
constraintPath,
constraints,
fields: field.tabs.map((tab) => ({
...tab,
@@ -114,6 +122,7 @@ export const getTableColumnFromPath = ({
joins,
locale,
pathSegments: pathSegments.slice(1),
rootTableName,
selectFields,
tableName: newTableName,
})
@@ -125,12 +134,14 @@ export const getTableColumnFromPath = ({
aliasTable,
collectionPath,
columnPrefix: `${columnPrefix}${field.name}_`,
constraintPath,
constraints,
fields: field.fields,
joinAliases,
joins,
locale,
pathSegments: pathSegments.slice(1),
rootTableName,
selectFields,
tableName: newTableName,
})
@@ -140,12 +151,14 @@ export const getTableColumnFromPath = ({
aliasTable,
collectionPath,
columnPrefix,
constraintPath,
constraints,
fields: field.fields,
joinAliases,
joins,
locale,
pathSegments: pathSegments.slice(1),
rootTableName,
selectFields,
tableName: newTableName,
})
@@ -172,12 +185,14 @@ export const getTableColumnFromPath = ({
aliasTable,
collectionPath,
columnPrefix: `${columnPrefix}${field.name}_`,
constraintPath,
constraints,
fields: field.fields,
joinAliases,
joins,
locale,
pathSegments: pathSegments.slice(1),
rootTableName,
selectFields,
tableName: newTableName,
})
@@ -185,6 +200,7 @@ export const getTableColumnFromPath = ({
case 'array': {
newTableName = `${tableName}_${toSnakeCase(field.name)}`
constraintPath = `${constraintPath}${field.name}.%.`
if (locale && field.localized && adapter.payload.config.localization) {
joins[newTableName] = and(
eq(adapter.tables[tableName].id, adapter.tables[newTableName]._parentID),
@@ -206,12 +222,14 @@ export const getTableColumnFromPath = ({
return getTableColumnFromPath({
adapter,
collectionPath,
constraintPath,
constraints,
fields: field.fields,
joinAliases,
joins,
locale,
pathSegments: pathSegments.slice(1),
rootTableName,
selectFields,
tableName: newTableName,
})
@@ -229,12 +247,14 @@ export const getTableColumnFromPath = ({
result = getTableColumnFromPath({
adapter,
collectionPath,
constraintPath: '',
constraints: blockConstraints,
fields: block.fields,
joinAliases,
joins,
locale,
pathSegments: pathSegments.slice(1),
rootTableName,
selectFields: blockSelectFields,
tableName: newTableName,
})
@@ -283,9 +303,8 @@ export const getTableColumnFromPath = ({
case 'relationship':
case 'upload': {
let relationshipFields
const relationTableName = `${tableName}_rels`
const relationTableName = `${rootTableName}_rels`
const newCollectionPath = pathSegments.slice(1).join('.')
const aliasRelationshipTableName = uuid()
const aliasRelationshipTable = alias(
adapter.tables[relationTableName],
@@ -295,7 +314,7 @@ export const getTableColumnFromPath = ({
// Join in the relationships table
joinAliases.push({
condition: eq(
(aliasTable || adapter.tables[tableName]).id,
(aliasTable || adapter.tables[rootTableName]).id,
aliasRelationshipTable.parent,
),
table: aliasRelationshipTable,
@@ -306,7 +325,7 @@ export const getTableColumnFromPath = ({
constraints.push({
columnName: 'path',
table: aliasRelationshipTable,
value: field.name,
value: `${constraintPath}${field.name}`,
})
let newAliasTable
@@ -368,6 +387,7 @@ export const getTableColumnFromPath = ({
joins,
locale,
pathSegments: pathSegments.slice(1),
rootTableName: newTableName,
selectFields,
tableName: newTableName,
})

View File

@@ -100,7 +100,11 @@ export async function parseParams({
const val = where[relationOrPath][operator]
queryConstraints.forEach(({ columnName: col, table: constraintTable, value }) => {
constraints.push(operatorMap.equals(constraintTable[col], value))
if (typeof value === 'string' && value.indexOf('%') > -1) {
constraints.push(operatorMap.like(constraintTable[col], value))
} else {
constraints.push(operatorMap.equals(constraintTable[col], value))
}
})
if (['json', 'richText'].includes(field.type) && Array.isArray(pathSegments)) {

View File

@@ -6,11 +6,12 @@
// drizzle-kit@utils
import { generateDrizzleJson, generateMigration, pushSchema } from 'drizzle-kit/utils'
import { drizzle } from 'drizzle-orm/node-postgres'
import { Pool } from 'pg'
async function generateUsage() {
const { generateDrizzleJson, generateMigration } = require('drizzle-kit/utils')
// @ts-expect-error Just TypeScript being broken // TODO: Open TypeScript issue
const schema = await import('./data/users')
// @ts-expect-error Just TypeScript being broken // TODO: Open TypeScript issue
@@ -25,6 +26,8 @@ async function generateUsage() {
}
async function pushUsage() {
const { pushSchema } = require('drizzle-kit/utils')
// @ts-expect-error Just TypeScript being broken // TODO: Open TypeScript issue
const schemaAfter = await import('./data/users-after')

View File

@@ -252,6 +252,8 @@ export const traverseFields = ({
}
case 'array': {
const disableNotNullFromHere = Boolean(field.admin?.condition) || disableNotNull
const arrayTableName = `${newTableName}_${toSnakeCase(field.name)}`
const baseColumns: Record<string, PgColumnBuilder> = {
_order: integer('_order').notNull(),
@@ -277,7 +279,7 @@ export const traverseFields = ({
adapter,
baseColumns,
baseExtraConfig,
disableNotNull,
disableNotNull: disableNotNullFromHere,
disableUnique,
fields: field.fields,
rootRelationsToBuild,
@@ -314,6 +316,8 @@ export const traverseFields = ({
}
case 'blocks': {
const disableNotNullFromHere = Boolean(field.admin?.condition) || disableNotNull
field.blocks.forEach((block) => {
const blockTableName = `${rootTableName}_blocks_${toSnakeCase(block.slug)}`
if (!adapter.tables[blockTableName]) {
@@ -343,7 +347,7 @@ export const traverseFields = ({
adapter,
baseColumns,
baseExtraConfig,
disableNotNull,
disableNotNull: disableNotNullFromHere,
disableUnique,
fields: block.fields,
rootRelationsToBuild,
@@ -428,6 +432,8 @@ export const traverseFields = ({
break
}
const disableNotNullFromHere = Boolean(field.admin?.condition) || disableNotNull
const {
hasLocalizedField: groupHasLocalizedField,
hasLocalizedManyNumberField: groupHasLocalizedManyNumberField,
@@ -438,7 +444,7 @@ export const traverseFields = ({
buildRelationships,
columnPrefix: `${columnName}_`,
columns,
disableNotNull,
disableNotNull: disableNotNullFromHere,
disableUnique,
fieldPrefix: `${fieldName}_`,
fields: field.fields,
@@ -463,6 +469,8 @@ export const traverseFields = ({
}
case 'tabs': {
const disableNotNullFromHere = Boolean(field.admin?.condition) || disableNotNull
const {
hasLocalizedField: tabHasLocalizedField,
hasLocalizedManyNumberField: tabHasLocalizedManyNumberField,
@@ -473,7 +481,7 @@ export const traverseFields = ({
buildRelationships,
columnPrefix,
columns,
disableNotNull,
disableNotNull: disableNotNullFromHere,
disableUnique,
fieldPrefix,
fields: field.tabs.map((tab) => ({ ...tab, type: 'tab' })),
@@ -500,6 +508,7 @@ export const traverseFields = ({
case 'row':
case 'collapsible': {
const disableNotNullFromHere = Boolean(field.admin?.condition) || disableNotNull
const {
hasLocalizedField: rowHasLocalizedField,
hasLocalizedManyNumberField: rowHasLocalizedManyNumberField,
@@ -510,7 +519,7 @@ export const traverseFields = ({
buildRelationships,
columnPrefix,
columns,
disableNotNull,
disableNotNull: disableNotNullFromHere,
disableUnique,
fieldPrefix,
fields: field.fields,

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/live-preview-react",
"version": "0.1.2",
"version": "0.1.3",
"description": "The official live preview React SDK for Payload",
"repository": "https://github.com/payloadcms/payload",
"license": "MIT",

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/live-preview",
"version": "0.1.2",
"version": "0.1.3",
"description": "The official live preview JavaScript SDK for Payload",
"repository": "https://github.com/payloadcms/payload",
"license": "MIT",

View File

@@ -1,6 +1,6 @@
{
"name": "payload",
"version": "2.0.5",
"version": "2.0.6",
"description": "Node, React and MongoDB Headless CMS and Application Framework",
"license": "MIT",
"main": "./dist/index.js",
@@ -208,8 +208,7 @@
"webpack": "^5.78.0"
},
"engines": {
"node": ">=14",
"pnpm": ">=8"
"node": ">=14"
},
"files": [
"bin.js",

View File

@@ -55,24 +55,6 @@ export const DocumentControls: React.FC<{
const { i18n, t } = useTranslation('general')
let showPreviewButton = false
if (collection) {
showPreviewButton =
isEditing &&
collection?.admin?.preview &&
collection?.versions?.drafts &&
!collection?.versions?.drafts?.autosave
}
if (global) {
showPreviewButton =
isEditing &&
global?.admin?.preview &&
global?.versions?.drafts &&
!global?.versions?.drafts?.autosave
}
const showDotMenu = Boolean(collection && id && !disableActions)
return (
@@ -165,7 +147,7 @@ export const DocumentControls: React.FC<{
</div>
<div className={`${baseClass}__controls-wrapper`}>
<div className={`${baseClass}__controls`}>
{showPreviewButton && (
{(collection?.admin?.preview || global?.admin?.preview) && (
<PreviewButton
CustomComponent={
collection?.admin?.components?.edit?.PreviewButton ||

View File

@@ -51,6 +51,7 @@ const Duplicate: React.FC<Props> = ({ id, collection, slug }) => {
},
params: {
depth: 0,
draft: true,
locale,
},
})

View File

@@ -61,6 +61,7 @@ export const addFieldStatePromise = async ({
user,
value: data?.[field.name],
})
if (data?.[field.name]) {
data[field.name] = valueWithDefault
}
@@ -145,8 +146,8 @@ export const addFieldStatePromise = async ({
fieldState.value = null
fieldState.initialValue = null
} else {
fieldState.value = arrayValue
fieldState.initialValue = arrayValue
fieldState.value = arrayValue.length
fieldState.initialValue = arrayValue.length
if (arrayValue.length > 0) {
fieldState.disableFormData = true
@@ -236,8 +237,8 @@ export const addFieldStatePromise = async ({
fieldState.value = null
fieldState.initialValue = null
} else {
fieldState.value = blocksValue
fieldState.initialValue = blocksValue
fieldState.value = blocksValue.length
fieldState.initialValue = blocksValue.length
if (blocksValue.length > 0) {
fieldState.disableFormData = true

View File

@@ -8,6 +8,9 @@ import getSiblingData from './getSiblingData'
import reduceFieldsToValues from './reduceFieldsToValues'
import { flattenRows, separateRows } from './rows'
/**
* Reducer which modifies the form field state (all the current data of the fields in the form). When called using dispatch, it will return a new state object.
*/
export function fieldReducer(state: Fields, action: FieldAction): Fields {
switch (action.type) {
case 'REPLACE_STATE': {
@@ -123,7 +126,7 @@ export function fieldReducer(state: Fields, action: FieldAction): Fields {
...state[path],
disableFormData: rows.length > 0,
rows: rowsMetadata,
value: rows,
value: rows.length,
},
...flattenRows(path, rows),
}
@@ -161,10 +164,6 @@ export function fieldReducer(state: Fields, action: FieldAction): Fields {
const { remainingFields, rows: siblingRows } = separateRows(path, state)
siblingRows.splice(rowIndex, 0, subFieldState)
// add new row to array _value_
const currentValue = (Array.isArray(state[path]?.value) ? state[path]?.value : []) as Fields[]
const newValue = currentValue.splice(rowIndex, 0, reduceFieldsToValues(subFieldState, true))
const newState: Fields = {
...remainingFields,
...flattenRows(path, siblingRows),
@@ -172,7 +171,7 @@ export function fieldReducer(state: Fields, action: FieldAction): Fields {
...state[path],
disableFormData: true,
rows: rowsMetadata,
value: newValue,
value: siblingRows.length,
},
}
@@ -203,10 +202,6 @@ export function fieldReducer(state: Fields, action: FieldAction): Fields {
// replace form _field state_
siblingRows[rowIndex] = subFieldState
// replace array _value_
const newValue = Array.isArray(state[path]?.value) ? state[path]?.value : []
newValue[rowIndex] = reduceFieldsToValues(subFieldState, true)
const newState: Fields = {
...remainingFields,
...flattenRows(path, siblingRows),
@@ -214,7 +209,7 @@ export function fieldReducer(state: Fields, action: FieldAction): Fields {
...state[path],
disableFormData: true,
rows: rowsMetadata,
value: newValue,
value: siblingRows.length,
},
}
@@ -245,7 +240,7 @@ export function fieldReducer(state: Fields, action: FieldAction): Fields {
...state[path],
disableFormData: true,
rows: rowsMetadata,
value: rows,
value: rows.length,
},
...flattenRows(path, rows),
}

View File

@@ -50,12 +50,15 @@ import reduceFieldsToValues from './reduceFieldsToValues'
const baseClass = 'form'
const Form: React.FC<Props> = (props) => {
const { id, collection, getDocPreferences, global } = useDocumentInfo()
const {
action,
children,
className,
disableSuccessStatus,
disabled,
fields: fieldsFromProps = collection?.fields || global?.fields,
handleResponse,
initialData, // values only, paths are required as key - form should build initial state as convenience
initialState, // fully formed initial field state
@@ -71,7 +74,6 @@ const Form: React.FC<Props> = (props) => {
const { code: locale } = useLocale()
const { i18n, t } = useTranslation('general')
const { refreshCookie, user } = useAuth()
const { id, collection, getDocPreferences, global } = useDocumentInfo()
const operation = useOperation()
const config = useConfig()
@@ -90,6 +92,10 @@ const Form: React.FC<Props> = (props) => {
if (initialState) initialFieldState = initialState
const fieldsReducer = useReducer(fieldReducer, {}, () => initialFieldState)
/**
* `fields` is the current, up-to-date state/data of all fields in the form. It can be modified by using dispatchFields,
* which calls the fieldReducer, which then updates the state.
*/
const [fields, dispatchFields] = fieldsReducer
contextRef.current.fields = fields
@@ -167,7 +173,13 @@ const Form: React.FC<Props> = (props) => {
let validationResult: boolean | string = true
if (typeof field.validate === 'function') {
validationResult = await field.validate(field.value, {
let valueToValidate = field.value
if (field?.rows && Array.isArray(field.rows)) {
valueToValidate = contextRef.current.getDataByPath(path)
}
validationResult = await field.validate(valueToValidate, {
id,
config,
data,
@@ -434,7 +446,7 @@ const Form: React.FC<Props> = (props) => {
const getRowSchemaByPath = React.useCallback(
({ blockType, path }: { blockType?: string; path: string }) => {
const rowConfig = traverseRowConfigs({
fieldConfig: collection?.fields || global?.fields,
fieldConfig: fieldsFromProps,
path,
})
const rowFieldConfigs = buildFieldSchemaMap(rowConfig)
@@ -442,10 +454,11 @@ const Form: React.FC<Props> = (props) => {
const fieldKey = pathSegments.at(-1)
return rowFieldConfigs.get(blockType ? `${fieldKey}.${blockType}` : fieldKey)
},
[traverseRowConfigs, collection?.fields, global?.fields],
[traverseRowConfigs, fieldsFromProps],
)
// Array/Block row manipulation
// Array/Block row manipulation. This is called when, for example, you add a new block to a blocks field.
// The block data is saved in the rows property of the state, which is modified updated here.
const addFieldRow: Context['addFieldRow'] = useCallback(
async ({ data, path, rowIndex }) => {
const preferences = await getDocPreferences()

View File

@@ -2,7 +2,12 @@ import type React from 'react'
import type { Dispatch } from 'react'
import type { User } from '../../../../auth/types'
import type { Condition, Field as FieldConfig, Validate } from '../../../../fields/config/types'
import type {
Condition,
Field,
Field as FieldConfig,
Validate,
} from '../../../../fields/config/types'
export type Row = {
blockType?: string
@@ -41,6 +46,12 @@ export type Props = {
className?: string
disableSuccessStatus?: boolean
disabled?: boolean
/**
* By default, the form will get the field schema (not data) from the current document. If you pass this in, you can override that behavior.
* This is very useful for sub-forms, where the form's field schema is not necessarily the field schema of the current document (e.g. for the Blocks
* feature of the Lexical Rich Text field)
*/
fields?: Field[]
handleResponse?: (res: Response) => void
initialData?: Data
initialState?: Fields

View File

@@ -17,10 +17,21 @@ const intersectionObserverOptions = {
rootMargin: '1000px',
}
// If you send `fields` through, it will render those fields explicitly
// Otherwise, it will reduce your fields using the other provided props
// This is so that we can conditionally render fields before reducing them, if desired
// See the sidebar in '../collections/Edit/Default/index.tsx' for an example
/**
* If you send `fields` through, it will render those fields explicitly
* Otherwise, it will reduce your fields using the other provided props
* This is so that we can conditionally render fields before reducing them, if desired
* See the sidebar in '../collections/Edit/Default/index.tsx' for an example
*
* The state/data for the fields it renders is not managed by this component. Instead, every component it renders has
* their own handling of their own value, usually through the useField hook. This hook will get the field's value
* from the Form the field is in, using the field's path.
*
* Thus, if you would like to set the value of a field you render here, you must do so in the Form that contains the field, or in the
* Field component itself.
*
* All this component does is render the field's Field Components, and pass them the props they need to function.
**/
const RenderFields: React.FC<Props> = (props) => {
const { className, fieldTypes, forceRender, margins } = props

View File

@@ -6,21 +6,23 @@ import type { ReducedField } from './filterFields'
export type Props = {
className?: string
fieldTypes: FieldTypes
margins?: 'small' | false
forceRender?: boolean
margins?: 'small' | false
permissions?:
| {
[field: string]: FieldPermissions
}
| FieldPermissions
readOnly?: boolean
} & (
| {
// Fields to be filtered by the component
fieldSchema: FieldWithPath[]
filter?: (field: Field) => boolean
indexPath?: string
permissions?:
| {
[field: string]: FieldPermissions
}
| FieldPermissions
readOnly?: boolean
}
| {
// Pre-filtered fields to be simply rendered
fields: ReducedField[]
}
)

View File

@@ -91,7 +91,7 @@ const ArrayFieldType: React.FC<Props> = (props) => {
showError,
valid,
value,
} = useField<[]>({
} = useField<number>({
condition,
hasRows: true,
path,
@@ -123,8 +123,8 @@ const ArrayFieldType: React.FC<Props> = (props) => {
)
const removeRow = useCallback(
async (rowIndex: number) => {
await removeFieldRow({ path, rowIndex })
(rowIndex: number) => {
removeFieldRow({ path, rowIndex })
setModified(true)
},
[removeFieldRow, path, setModified],
@@ -278,7 +278,7 @@ const ArrayFieldType: React.FC<Props> = (props) => {
icon="plus"
iconPosition="left"
iconStyle="with-border"
onClick={() => addRow(value?.length || 0)}
onClick={() => addRow(value || 0)}
>
{t('addLabel', { label: getTranslation(labels.singular, i18n) })}
</Button>

View File

@@ -90,7 +90,7 @@ const BlocksField: React.FC<Props> = (props) => {
showError,
valid,
value,
} = useField<[]>({
} = useField<number>({
condition,
hasRows: true,
path,
@@ -128,8 +128,8 @@ const BlocksField: React.FC<Props> = (props) => {
)
const removeRow = useCallback(
async (rowIndex: number) => {
await removeFieldRow({ path, rowIndex })
(rowIndex: number) => {
removeFieldRow({ path, rowIndex })
setModified(true)
},
[path, removeFieldRow, setModified],
@@ -297,7 +297,7 @@ const BlocksField: React.FC<Props> = (props) => {
</DrawerToggler>
<BlocksDrawer
addRow={addRow}
addRowIndex={value?.length || 0}
addRowIndex={value || 0}
blocks={blocks}
drawerSlug={drawerSlug}
labels={labels}

View File

@@ -29,7 +29,7 @@ const useField = <T,>(options: Options): FieldType<T> => {
const dispatchField = useFormFields(([_, dispatch]) => dispatch)
const config = useConfig()
const { getData, getSiblingData, setModified } = useForm()
const { getData, getDataByPath, getSiblingData, setModified } = useForm()
const value = field?.value as T
const initialValue = field?.initialValue as T
@@ -116,8 +116,14 @@ const useField = <T,>(options: Options): FieldType<T> => {
user,
}
let valueToValidate = value
if (field?.rows && Array.isArray(field.rows)) {
valueToValidate = getDataByPath(path)
}
const validationResult =
typeof validate === 'function' ? await validate(value, validateOptions) : true
typeof validate === 'function' ? await validate(valueToValidate, validateOptions) : true
if (typeof validationResult === 'string') {
action.errorMessage = validationResult
@@ -132,7 +138,7 @@ const useField = <T,>(options: Options): FieldType<T> => {
}
}
validateField()
void validateField()
},
150,
[
@@ -142,6 +148,7 @@ const useField = <T,>(options: Options): FieldType<T> => {
dispatchField,
getData,
getSiblingData,
getDataByPath,
id,
operation,
path,

View File

@@ -27,7 +27,8 @@
}
&__fields {
& > .tabs-field {
& > .tabs-field,
& > .group-field {
margin-right: calc(var(--base) * -2);
}
}
@@ -51,7 +52,7 @@
position: sticky;
top: var(--doc-controls-height);
width: 33.33%;
height: 100%;
height: calc(100vh - var(--doc-controls-height));
}
&__sidebar {
@@ -110,7 +111,8 @@
}
&__fields {
& > .tabs-field {
& > .tabs-field,
& > .group-field {
margin-right: calc(var(--gutter-h) * -1);
}
}

View File

@@ -90,9 +90,8 @@ export const DefaultGlobalEdit: React.FC<GlobalEditViewProps> = (props) => {
<div className={`${baseClass}__sidebar-sticky-wrap`}>
<div className={`${baseClass}__sidebar-fields`}>
<RenderFields
fieldSchema={fields}
fieldTypes={fieldTypes}
filter={(field) => field.admin.position === 'sidebar'}
fields={sidebarFields}
permissions={permissions.fields}
readOnly={!hasSavePermission}
/>

View File

@@ -21,4 +21,11 @@
margin: 0;
}
}
&__inputWrap {
display: flex;
flex-direction: column;
gap: base(1);
margin-bottom: base(0.25);
}
}

View File

@@ -44,6 +44,8 @@ const Login: React.FC = () => {
}
}
const prefillForm = autoLogin && autoLogin.prefillOnly
return (
<React.Fragment>
{user ? (
@@ -75,22 +77,33 @@ const Login: React.FC = () => {
action={`${serverURL}${api}/${userSlug}/login`}
className={`${baseClass}__form`}
disableSuccessStatus
initialData={{
email: autoLogin && autoLogin.prefillOnly ? autoLogin.email : undefined,
password: autoLogin && autoLogin.prefillOnly ? autoLogin.password : undefined,
}}
initialData={
prefillForm
? {
email: autoLogin.email,
password: autoLogin.password,
}
: undefined
}
method="post"
onSuccess={onSuccess}
waitForAutocomplete
>
<FormLoadingOverlayToggle action="loading" name="login-form" />
<Email
admin={{ autoComplete: 'email' }}
label={t('general:email')}
name="email"
required
/>
<Password autoComplete="off" label={t('general:password')} name="password" required />
<div className={`${baseClass}__inputWrap`}>
<Email
admin={{ autoComplete: 'email' }}
label={t('general:email')}
name="email"
required
/>
<Password
autoComplete="off"
label={t('general:password')}
name="password"
required
/>
</div>
<Link to={`${admin}/forgot`}>{t('forgotPasswordQuestion')}</Link>
<FormSubmit>{t('login')}</FormSubmit>
</Form>

View File

@@ -27,7 +27,8 @@
}
&__fields {
& > .tabs-field {
& > .tabs-field,
& > .group-field {
margin-right: calc(var(--base) * -2);
}
}
@@ -55,7 +56,7 @@
position: sticky;
top: var(--doc-controls-height);
width: 33.33%;
height: 100%;
height: calc(100vh - var(--doc-controls-height));
}
&__sidebar {
@@ -106,7 +107,8 @@
}
&__fields {
& > .tabs-field {
& > .tabs-field,
& > .group-field {
margin-right: calc(var(--gutter-h) * -1);
}
}

View File

@@ -115,7 +115,12 @@ export const DefaultCollectionEdit: React.FC<CollectionEditViewProps> = (props)
<div className={`${baseClass}__sidebar`}>
<div className={`${baseClass}__sidebar-sticky-wrap`}>
<div className={`${baseClass}__sidebar-fields`}>
<RenderFields fieldTypes={fieldTypes} fields={sidebarFields} />
<RenderFields
fieldTypes={fieldTypes}
fields={sidebarFields}
permissions={permissions.fields}
readOnly={!hasSavePermission}
/>
</div>
</div>
</div>

View File

@@ -18,16 +18,18 @@ export {
*/
useWatchForm,
} from '../../admin/components/forms/Form/context'
export { createNestedFieldPath } from '../../admin/components/forms/Form/createNestedFieldPath'
export { default as getSiblingData } from '../../admin/components/forms/Form/getSiblingData'
export { default as reduceFieldsToValues } from '../../admin/components/forms/Form/reduceFieldsToValues'
export { default as Label } from '../../admin/components/forms/Label'
export { default as RenderFields } from '../../admin/components/forms/RenderFields'
export { default as RenderFields } from '../../admin/components/forms/RenderFields'
export { default as Submit } from '../../admin/components/forms/Submit'
export { default as FormSubmit } from '../../admin/components/forms/Submit'
export { fieldTypes } from '../../admin/components/forms/field-types'
export { default as Checkbox } from '../../admin/components/forms/field-types/Checkbox'
export { default as Collapsible } from '../../admin/components/forms/field-types/Collapsible'

View File

@@ -1,7 +1,7 @@
export { buildConfig } from '../config/build'
export * from '../config/types'
export { type FieldTypes, fieldTypes } from '../admin/components/forms/field-types'
export { type FieldTypes } from '../admin/components/forms/field-types'
export { defaults } from '../config/defaults'
export { sanitizeConfig } from '../config/sanitize'
export { baseBlockFields } from '../fields/baseFields/baseBlockFields'

View File

@@ -1,10 +1,11 @@
export { withMergedProps } from '../admin/components/utilities/WithMergedProps'
export { extractTranslations } from '../translations/extractTranslations'
export { i18nInit } from '../translations/init'
export { i18nInit } from '../translations/init'
export { combineMerge } from '../utilities/combineMerge'
export { configToJSONSchema, entityToJSONSchema } from '../utilities/configToJSONSchema'
export { createArrayFromCommaDelineated } from '../utilities/createArrayFromCommaDelineated'
export { createArrayFromCommaDelineated } from '../utilities/createArrayFromCommaDelineated'
export { deepCopyObject } from '../utilities/deepCopyObject'
export { deepMerge } from '../utilities/deepMerge'
export { default as flattenTopLevelFields } from '../utilities/flattenTopLevelFields'

View File

@@ -382,7 +382,7 @@ export const relationship: Validate<unknown, unknown, RelationshipField> = async
})
if (invalidRelationships.length > 0) {
return `This field has the following invalid selections: ${invalidRelationships
return `This relationship field has the following invalid relationships: ${invalidRelationships
.map((err, invalid) => {
return `${err} ${JSON.stringify(invalid)}`
})

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/richtext-lexical",
"version": "0.1.5",
"version": "0.1.11",
"description": "The officially supported Lexical richtext adapter for Payload",
"repository": "https://github.com/payloadcms/payload",
"license": "MIT",
@@ -55,6 +55,9 @@
"@types/react": "18.2.15",
"payload": "workspace:*"
},
"peerDependencies": {
"payload": "^2.0.6"
},
"exports": {
".": {
"default": "./src/index.ts",

View File

@@ -1,3 +1,4 @@
'use client'
import type { SerializedEditorState } from 'lexical'
import type { CellComponentProps, RichTextField } from 'payload/types'
@@ -16,17 +17,38 @@ export const RichTextCell: React.FC<
const [preview, setPreview] = React.useState('Loading...')
useEffect(() => {
if (data == null) {
let dataToUse = data
if (dataToUse == null) {
setPreview('')
return
}
// Transform data through load hooks
if (editorConfig?.features?.hooks?.load?.length) {
editorConfig.features.hooks.load.forEach((hook) => {
dataToUse = hook({ incomingEditorState: dataToUse })
})
}
// If data is from Slate and not Lexical
if (dataToUse && Array.isArray(dataToUse) && !('root' in dataToUse)) {
setPreview('')
return
}
// If data is from payload-plugin-lexical
if (dataToUse && 'jsonContent' in dataToUse) {
setPreview('')
return
}
// initialize headless editor
const headlessEditor = createHeadlessEditor({
namespace: editorConfig.lexical.namespace,
nodes: getEnabledNodes({ editorConfig }),
theme: editorConfig.lexical.theme,
})
headlessEditor.setEditorState(headlessEditor.parseEditorState(data))
headlessEditor.setEditorState(headlessEditor.parseEditorState(dataToUse))
const textContent =
headlessEditor.getEditorState().read(() => {

View File

@@ -47,7 +47,7 @@ const RichText: React.FC<FieldProps> = (props) => {
validate: memoizedValidate,
})
const { errorMessage, initialValue, setValue, showError, value } = fieldType
const { errorMessage, setValue, showError, value } = fieldType
let valueToUse = value
@@ -87,14 +87,19 @@ const RichText: React.FC<FieldProps> = (props) => {
<LexicalProvider
editorConfig={editorConfig}
fieldProps={props}
initialState={initialValue}
onChange={(editorState, editor, tags) => {
const json = editorState.toJSON()
let serializedEditorState = editorState.toJSON()
setValue(json)
// Transform state through save hooks
if (editorConfig?.features?.hooks?.save?.length) {
editorConfig.features.hooks.save.forEach((hook) => {
serializedEditorState = hook({ incomingEditorState: serializedEditorState })
})
}
setValue(serializedEditorState)
}}
readOnly={readOnly}
setValue={setValue}
value={value}
/>
<FieldDescription description={description} value={value} />

View File

@@ -1,3 +1,5 @@
import type { Block } from 'payload/types'
import { sanitizeFields } from 'payload/config'
import type { BlocksFeatureProps } from '.'
@@ -20,40 +22,42 @@ export const blockAfterReadPromiseHOC = (
showHiddenFields,
siblingDoc,
}) => {
const blocks: Block[] = props.blocks
const blockFieldData = node.fields.data
const promises: Promise<void>[] = []
// Sanitize block's fields here. This is done here and not in the feature, because the payload config is available here
const payloadConfig = req.payload.config
const validRelationships = payloadConfig.collections.map((c) => c.slug) || []
props.blocks = props.blocks.map((block) => {
const unsanitizedBlock = { ...block }
unsanitizedBlock.fields = sanitizeFields({
blocks.forEach((block) => {
block.fields = sanitizeFields({
config: payloadConfig,
fields: block.fields,
validRelationships,
})
return unsanitizedBlock
})
if (Array.isArray(props.blocks)) {
props.blocks.forEach((block) => {
if (block?.fields) {
recurseNestedFields({
afterReadPromises,
currentDepth,
data: node.fields.data || {},
depth,
fields: block.fields,
overrideAccess,
promises,
req,
showHiddenFields,
siblingDoc,
})
}
})
// find block used in this node
const block = props.blocks.find((block) => block.slug === blockFieldData.blockType)
if (!block || !block?.fields?.length || !blockFieldData) {
return promises
}
recurseNestedFields({
afterReadPromises,
currentDepth,
data: blockFieldData,
depth,
fields: block.fields,
overrideAccess,
promises,
req,
showHiddenFields,
// The afterReadPromise gets its data from looking for field.name inside of the siblingDoc. Thus, here we cannot pass the whole document's siblingDoc, but only the siblingDoc (sibling fields) of the current field.
siblingDoc: blockFieldData,
})
return promises
}

View File

@@ -8,7 +8,7 @@ import { SectionTitle } from 'payload/components/fields/Blocks'
import { RenderFields, createNestedFieldPath, useFormSubmitted } from 'payload/components/forms'
import { useDocumentInfo } from 'payload/components/utilities'
import { getTranslation } from 'payload/utilities'
import React, { useCallback, useEffect } from 'react'
import React, { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import type { FieldProps } from '../../../../types'
@@ -24,6 +24,11 @@ type Props = {
nodeKey: string
}
/**
* The actual content of the Block. This should be INSIDE a Form component,
* scoped to the block. All format operations in here are thus scoped to the block's form, and
* not the whole document.
*/
export const BlockContent: React.FC<Props> = (props) => {
const { baseClass, block, field, fields, nodeKey } = props
const { i18n } = useTranslation()

View File

@@ -1,14 +1,20 @@
import { type ElementFormatType } from 'lexical'
import { Form, buildInitialState, useFormSubmitted } from 'payload/components/forms'
import React, { useMemo } from 'react'
import React, { useEffect, useMemo } from 'react'
import { type BlockFields } from '../nodes/BlocksNode'
const baseClass = 'lexical-block'
import type { Data } from 'payload/types'
import { useConfig } from 'payload/components/utilities'
import {
buildStateFromSchema,
useConfig,
useDocumentInfo,
useLocale,
} from 'payload/components/utilities'
import { sanitizeFields } from 'payload/config'
import { useTranslation } from 'react-i18next'
import type { BlocksFeatureProps } from '..'
@@ -43,13 +49,49 @@ export const BlockComponent: React.FC<Props> = (props) => {
validRelationships,
})
const initialDataRef = React.useRef<Data>(buildInitialState(fields.data || {})) // Store initial value in a ref, so it doesn't change on re-render and only gets initialized once
const initialStateRef = React.useRef<Data>(buildInitialState(fields.data || {})) // Store initial value in a ref, so it doesn't change on re-render and only gets initialized once
const config = useConfig()
const { t } = useTranslation('general')
const { code: locale } = useLocale()
const { getDocPreferences } = useDocumentInfo()
// initialState State
const [initialState, setInitialState] = React.useState<Data>(null)
useEffect(() => {
async function buildInitialState() {
const preferences = await getDocPreferences()
const stateFromSchema = await buildStateFromSchema({
config,
data: fields.data,
fieldSchema: block.fields,
locale,
operation: 'update',
preferences,
t,
})
// We have to merge the output of buildInitialState (above this useEffect) with the output of buildStateFromSchema.
// That's because the output of buildInitialState provides important properties necessary for THIS block,
// like blockName, blockType and id, while buildStateFromSchema provides the correct output of this block's data,
// e.g. if this block has a sub-block (like the `rows` property)
setInitialState({
...initialStateRef?.current,
...stateFromSchema,
})
}
void buildInitialState()
}, [setInitialState, config, block, locale, getDocPreferences, t]) // do not add fields here, it causes an endless loop
// Memoized Form JSX
const formContent = useMemo(() => {
return (
block && (
<Form initialState={initialDataRef?.current} submitted={submitted}>
block &&
initialState && (
<Form fields={block.fields} initialState={initialState} submitted={submitted}>
<BlockContent
baseClass={baseClass}
block={block}
@@ -60,7 +102,7 @@ export const BlockComponent: React.FC<Props> = (props) => {
</Form>
)
)
}, [block, field, nodeKey, submitted])
}, [block, field, nodeKey, submitted, initialState])
return <div className={baseClass}>{formContent}</div>
}

View File

@@ -15,12 +15,12 @@ export const blockValidationHOC = (
payloadConfig,
validation,
}) => {
const blockFieldValues = node.fields.data
const blockFieldData = node.fields.data
const blocks: Block[] = props.blocks
// Sanitize block's fields here. This is done here and not in the feature, because the payload config is available here
const validRelationships = payloadConfig.collections.map((c) => c.slug) || []
blocks.forEach((block) => {
const validRelationships = payloadConfig.collections.map((c) => c.slug) || []
block.fields = sanitizeFields({
config: payloadConfig,
fields: block.fields,
@@ -29,7 +29,7 @@ export const blockValidationHOC = (
})
// find block
const block = props.blocks.find((block) => block.slug === blockFieldValues.blockType)
const block = props.blocks.find((block) => block.slug === blockFieldData.blockType)
// validate block
if (!block) {

View File

@@ -52,7 +52,7 @@ export const linkAfterReadPromiseHOC = (
promises,
req,
showHiddenFields,
siblingDoc,
siblingDoc: node.fields || {},
})
}
return promises

View File

@@ -2,7 +2,7 @@ import { Drawer } from 'payload/components/elements'
import { Form } from 'payload/components/forms'
import { RenderFields } from 'payload/components/forms'
import { FormSubmit } from 'payload/components/forms'
import { fieldTypes } from 'payload/config'
import { fieldTypes } from 'payload/components/forms'
import React from 'react'
import { useTranslation } from 'react-i18next'

View File

@@ -5,7 +5,7 @@ import type { Field } from 'payload/types'
import LexicalClickableLinkPlugin from '@lexical/react/LexicalClickableLinkPlugin'
import { $findMatchingParent } from '@lexical/utils'
import { $getSelection, $isRangeSelection } from 'lexical'
import { withMergedProps } from 'payload/components/utilities'
import { withMergedProps } from 'payload/utilities'
import type { FeatureProvider } from '../types'
import type { LinkFields } from './nodes/LinkNode'

View File

@@ -51,7 +51,7 @@ export const uploadAfterReadPromiseHOC = (
promises,
req,
showHiddenFields,
siblingDoc,
siblingDoc: node.fields || {},
})
}
}

View File

@@ -4,7 +4,7 @@ import { useModal } from '@faceless-ui/modal'
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import { $getNodeByKey } from 'lexical'
import { Drawer } from 'payload/components/elements'
import { Form, FormSubmit, RenderFields } from 'payload/components/forms'
import { Form, FormSubmit, RenderFields, fieldTypes } from 'payload/components/forms'
import {
buildStateFromSchema,
useAuth,
@@ -12,7 +12,7 @@ import {
useDocumentInfo,
useLocale,
} from 'payload/components/utilities'
import { fieldTypes, sanitizeFields } from 'payload/config'
import { sanitizeFields } from 'payload/config'
import { deepCopyObject, getTranslation } from 'payload/utilities'
import React, { useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'

View File

@@ -0,0 +1,21 @@
import type { SerializedHeadingNode } from '@lexical/rich-text'
import type { LexicalPluginNodeConverter } from '../types'
import { convertLexicalPluginNodesToLexical } from '..'
export const HeadingConverter: LexicalPluginNodeConverter = {
converter({ converters, lexicalPluginNode }) {
return {
...lexicalPluginNode,
children: convertLexicalPluginNodesToLexical({
converters,
lexicalPluginNodes: (lexicalPluginNode as any).children || [],
parentNodeType: 'heading',
}),
type: 'heading',
version: 1,
} as const as SerializedHeadingNode
},
nodeTypes: ['heading'],
}

View File

@@ -0,0 +1,36 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import type { SerializedLinkNode } from '../../../../Link/nodes/LinkNode'
import type { LexicalPluginNodeConverter } from '../types'
import { convertLexicalPluginNodesToLexical } from '..'
export const LinkConverter: LexicalPluginNodeConverter = {
converter({ converters, lexicalPluginNode }) {
return {
children: convertLexicalPluginNodesToLexical({
converters,
lexicalPluginNodes: (lexicalPluginNode as any).children || [],
parentNodeType: 'link',
}),
direction: (lexicalPluginNode as any).direction || 'ltr',
fields: {
doc: (lexicalPluginNode as any).attributes?.doc
? {
relationTo: (lexicalPluginNode as any).attributes?.doc?.relationTo,
value: {
id: (lexicalPluginNode as any).attributes?.doc?.value,
},
}
: undefined,
linkType: (lexicalPluginNode as any).attributes?.linkType || 'custom',
newTab: (lexicalPluginNode as any).attributes?.newTab || false,
url: (lexicalPluginNode as any).attributes?.url || undefined,
},
format: (lexicalPluginNode as any).format || '',
indent: (lexicalPluginNode as any).indent || 0,
type: 'link',
version: 1,
} as const as SerializedLinkNode
},
nodeTypes: ['link'],
}

View File

@@ -0,0 +1,23 @@
import type { SerializedListNode } from '@lexical/list'
import type { LexicalPluginNodeConverter } from '../types'
import { convertLexicalPluginNodesToLexical } from '..'
export const ListConverter: LexicalPluginNodeConverter = {
converter({ converters, lexicalPluginNode }) {
return {
...lexicalPluginNode,
children: convertLexicalPluginNodesToLexical({
converters,
lexicalPluginNodes: (lexicalPluginNode as any).children || [],
parentNodeType: 'list',
}),
listType: (lexicalPluginNode as any)?.listType || 'number',
tag: (lexicalPluginNode as any)?.tag || 'ol',
type: 'list',
version: 1,
} as const as SerializedListNode
},
nodeTypes: ['list'],
}

View File

@@ -0,0 +1,23 @@
import type { SerializedListItemNode } from '@lexical/list'
import type { LexicalPluginNodeConverter } from '../types'
import { convertLexicalPluginNodesToLexical } from '..'
export const ListItemConverter: LexicalPluginNodeConverter = {
converter({ childIndex, converters, lexicalPluginNode }) {
return {
...lexicalPluginNode,
checked: undefined,
children: convertLexicalPluginNodesToLexical({
converters,
lexicalPluginNodes: (lexicalPluginNode as any)?.children || [],
parentNodeType: 'listitem',
}),
type: 'listitem',
value: childIndex + 1,
version: 1,
} as const as SerializedListItemNode
},
nodeTypes: ['listitem'],
}

View File

@@ -0,0 +1,21 @@
import type { SerializedHeadingNode } from '@lexical/rich-text'
import type { LexicalPluginNodeConverter } from '../types'
import { convertLexicalPluginNodesToLexical } from '..'
export const QuoteConverter: LexicalPluginNodeConverter = {
converter({ converters, lexicalPluginNode }) {
return {
...lexicalPluginNode,
children: convertLexicalPluginNodesToLexical({
converters,
lexicalPluginNodes: (lexicalPluginNode as any).children || [],
parentNodeType: 'quote',
}),
type: 'quote',
version: 1,
} as const as SerializedHeadingNode
},
nodeTypes: ['quote'],
}

View File

@@ -0,0 +1,26 @@
import type { SerializedUnknownConvertedNode } from '../../nodes/unknownConvertedNode'
import type { LexicalPluginNodeConverter } from '../types'
import { convertLexicalPluginNodesToLexical } from '..'
export const UnknownConverter: LexicalPluginNodeConverter = {
converter({ converters, lexicalPluginNode }) {
return {
children: convertLexicalPluginNodesToLexical({
converters,
lexicalPluginNodes: (lexicalPluginNode as any)?.children || [],
parentNodeType: 'unknownConverted',
}),
data: {
nodeData: lexicalPluginNode,
nodeType: lexicalPluginNode.type,
},
direction: 'ltr',
format: '',
indent: 0,
type: 'unknownConverted',
version: 1,
} as const as SerializedUnknownConvertedNode
},
nodeTypes: ['unknown'],
}

View File

@@ -0,0 +1,24 @@
import type { SerializedUploadNode } from '../../../../../..'
import type { LexicalPluginNodeConverter } from '../types'
export const UploadConverter: LexicalPluginNodeConverter = {
converter({ lexicalPluginNode }) {
let fields = {}
if ((lexicalPluginNode as any)?.caption?.editorState) {
fields = {
caption: (lexicalPluginNode as any)?.caption,
}
}
return {
fields,
format: (lexicalPluginNode as any)?.format || '',
relationTo: (lexicalPluginNode as any)?.rawImagePayload?.relationTo,
type: 'upload',
value: {
id: (lexicalPluginNode as any)?.rawImagePayload?.value?.id || '',
},
version: 1,
} as const as SerializedUploadNode
},
nodeTypes: ['upload'],
}

View File

@@ -0,0 +1,19 @@
import type { LexicalPluginNodeConverter } from './types'
import { HeadingConverter } from './converters/heading'
import { LinkConverter } from './converters/link'
import { ListConverter } from './converters/list'
import { ListItemConverter } from './converters/listItem'
import { QuoteConverter } from './converters/quote'
import { UnknownConverter } from './converters/unknown'
import { UploadConverter } from './converters/upload'
export const defaultConverters: LexicalPluginNodeConverter[] = [
UnknownConverter,
UploadConverter,
ListConverter,
ListItemConverter,
LinkConverter,
HeadingConverter,
QuoteConverter,
]

View File

@@ -0,0 +1,98 @@
import type {
SerializedEditorState,
SerializedLexicalNode,
SerializedParagraphNode,
SerializedTextNode,
} from 'lexical'
import type { LexicalPluginNodeConverter, PayloadPluginLexicalData } from './types'
export function convertLexicalPluginToLexical({
converters,
lexicalPluginData,
}: {
converters: LexicalPluginNodeConverter[]
lexicalPluginData: PayloadPluginLexicalData
}): SerializedEditorState {
return {
root: {
children: convertLexicalPluginNodesToLexical({
converters,
lexicalPluginNodes: lexicalPluginData?.jsonContent?.root?.children || [],
parentNodeType: 'root',
}),
direction: lexicalPluginData?.jsonContent?.root?.direction || 'ltr',
format: lexicalPluginData?.jsonContent?.root?.format || '',
indent: lexicalPluginData?.jsonContent?.root?.indent || 0,
type: 'root',
version: 1,
},
}
}
export function convertLexicalPluginNodesToLexical({
converters,
lexicalPluginNodes,
parentNodeType,
}: {
converters: LexicalPluginNodeConverter[]
lexicalPluginNodes: SerializedLexicalNode[]
/**
* Type of the parent lexical node (not the type of the original, parent payload-plugin-lexical type)
*/
parentNodeType: string
}): SerializedLexicalNode[] {
const unknownConverter = converters.find((converter) => converter.nodeTypes.includes('unknown'))
return (
lexicalPluginNodes.map((lexicalPluginNode, i) => {
if (lexicalPluginNode.type === 'paragraph') {
return convertParagraphNode(converters, lexicalPluginNode)
}
if (lexicalPluginNode.type === 'text' || !lexicalPluginNode.type) {
return convertTextNode(lexicalPluginNode)
}
const converter = converters.find((converter) =>
converter.nodeTypes.includes(lexicalPluginNode.type),
)
if (converter) {
return converter.converter({ childIndex: i, converters, lexicalPluginNode, parentNodeType })
}
console.warn(
'lexicalPluginToLexical > No converter found for node type: ' + lexicalPluginNode.type,
)
return unknownConverter?.converter({
childIndex: i,
converters,
lexicalPluginNode,
parentNodeType,
})
}) || []
)
}
export function convertParagraphNode(
converters: LexicalPluginNodeConverter[],
node: SerializedLexicalNode,
): SerializedParagraphNode {
return {
...node,
children: convertLexicalPluginNodesToLexical({
converters,
lexicalPluginNodes: (node as any).children || [],
parentNodeType: 'paragraph',
}),
type: 'paragraph',
version: 1,
} as SerializedParagraphNode
}
export function convertTextNode(node: SerializedLexicalNode): SerializedTextNode {
return node as SerializedTextNode
}
export function convertNodeToFormat(node: SerializedLexicalNode): number {
return (node as any).format
}

View File

@@ -0,0 +1,26 @@
import type { SerializedEditorState, SerializedLexicalNode } from 'lexical'
export type LexicalPluginNodeConverter<T extends SerializedLexicalNode = SerializedLexicalNode> = {
converter: ({
childIndex,
converters,
lexicalPluginNode,
parentNodeType,
}: {
childIndex: number
converters: LexicalPluginNodeConverter[]
lexicalPluginNode: SerializedLexicalNode
parentNodeType: string
}) => T
nodeTypes: string[]
}
export type PayloadPluginLexicalData = {
characters: number
comments: unknown[]
html?: string
jsonContent: SerializedEditorState
markdown?: string
preview: string
words: number
}

View File

@@ -0,0 +1,56 @@
import type { FeatureProvider } from '../../types'
import type { LexicalPluginNodeConverter, PayloadPluginLexicalData } from './converter/types'
import { convertLexicalPluginToLexical } from './converter'
import { defaultConverters } from './converter/defaultConverters'
import { UnknownConvertedNode } from './nodes/unknownConvertedNode'
type Props = {
converters?:
| (({
defaultConverters,
}: {
defaultConverters: LexicalPluginNodeConverter[]
}) => LexicalPluginNodeConverter[])
| LexicalPluginNodeConverter[]
}
export const LexicalPluginToLexicalFeature = (props?: Props): FeatureProvider => {
if (!props) {
props = {}
}
props.converters =
props?.converters && typeof props?.converters === 'function'
? props.converters({ defaultConverters: defaultConverters })
: (props?.converters as LexicalPluginNodeConverter[]) || defaultConverters
return {
feature: ({ resolvedFeatures, unsanitizedEditorConfig }) => {
return {
hooks: {
load({ incomingEditorState }) {
if (!incomingEditorState || !('jsonContent' in incomingEditorState)) {
// incomingEditorState null or not from Lexical Plugin
return incomingEditorState
}
// Lexical Plugin => convert to lexical
return convertLexicalPluginToLexical({
converters: props.converters as LexicalPluginNodeConverter[],
lexicalPluginData: incomingEditorState as unknown as PayloadPluginLexicalData,
})
},
},
nodes: [
{
node: UnknownConvertedNode,
type: UnknownConvertedNode.getType(),
},
],
props,
}
},
key: 'lexicalPluginToLexical',
}
}

View File

@@ -0,0 +1,16 @@
@import 'payload/scss';
span.unknownConverted {
text-transform: uppercase;
font-family: 'Roboto Mono', monospace;
letter-spacing: 2px;
font-size: base(0.5);
margin: 0 0 base(1);
background: red;
color: white;
display: inline-block;
div {
background: red;
}
}

View File

@@ -0,0 +1,101 @@
import type { SerializedLexicalNode, Spread } from 'lexical'
import { addClassNamesToElement } from '@lexical/utils'
import { DecoratorNode, type EditorConfig, type LexicalNode, type NodeKey } from 'lexical'
import React from 'react'
import './index.scss'
export type UnknownConvertedNodeData = {
nodeData: unknown
nodeType: string
}
export type SerializedUnknownConvertedNode = Spread<
{
data: UnknownConvertedNodeData
},
SerializedLexicalNode
>
/** @noInheritDoc */
export class UnknownConvertedNode extends DecoratorNode<JSX.Element> {
__data: UnknownConvertedNodeData
constructor({ data, key }: { data: UnknownConvertedNodeData; key?: NodeKey }) {
super(key)
this.__data = data
}
static clone(node: UnknownConvertedNode): UnknownConvertedNode {
return new UnknownConvertedNode({
data: node.__data,
key: node.__key,
})
}
static getType(): string {
return 'unknownConverted'
}
static importJSON(serializedNode: SerializedUnknownConvertedNode): UnknownConvertedNode {
const node = $createUnknownConvertedNode({ data: serializedNode.data })
return node
}
canInsertTextAfter(): true {
return true
}
canInsertTextBefore(): true {
return true
}
createDOM(config: EditorConfig): HTMLElement {
const element = document.createElement('span')
addClassNamesToElement(element, 'unknownConverted')
return element
}
decorate(): JSX.Element | null {
return (
<div>
Unknown converted payload-plugin-lexical node: <strong>{this.__data?.nodeType}</strong>
</div>
)
}
exportJSON(): SerializedUnknownConvertedNode {
return {
data: this.__data,
type: this.getType(),
version: 1,
}
}
// Mutation
isInline(): boolean {
return true
}
updateDOM(prevNode: UnknownConvertedNode, dom: HTMLElement): boolean {
return false
}
}
export function $createUnknownConvertedNode({
data,
}: {
data: UnknownConvertedNodeData
}): UnknownConvertedNode {
return new UnknownConvertedNode({
data,
})
}
export function $isUnknownConvertedNode(
node: LexicalNode | null | undefined,
): node is UnknownConvertedNode {
return node instanceof UnknownConvertedNode
}

View File

@@ -0,0 +1,25 @@
import type { SerializedHeadingNode } from '@lexical/rich-text'
import type { SlateNodeConverter } from '../types'
import { convertSlateNodesToLexical } from '..'
export const HeadingConverter: SlateNodeConverter = {
converter({ converters, slateNode }) {
return {
children: convertSlateNodesToLexical({
canContainParagraphs: false,
converters,
parentNodeType: 'heading',
slateNodes: slateNode.children || [],
}),
direction: 'ltr',
format: '',
indent: 0,
tag: slateNode.type as 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6', // Slate puts the tag (h1 / h2 / ...) inside of node.type
type: 'heading',
version: 1,
} as const as SerializedHeadingNode
},
nodeTypes: ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'],
}

View File

@@ -0,0 +1,65 @@
import type { SerializedLexicalNode, SerializedParagraphNode } from 'lexical'
import type { SlateNodeConverter } from '../types'
import { convertSlateNodesToLexical } from '..'
export const IndentConverter: SlateNodeConverter = {
converter({ converters, slateNode }) {
console.log('slateToLexical > IndentConverter > converter', JSON.stringify(slateNode, null, 2))
const convertChildren = (node: any, indentLevel: number = 0): SerializedLexicalNode => {
if (
(node?.type && (!node.children || node.type !== 'indent')) ||
(!node?.type && node?.text)
) {
console.log(
'slateToLexical > IndentConverter > convertChildren > node',
JSON.stringify(node, null, 2),
)
console.log(
'slateToLexical > IndentConverter > convertChildren > nodeOutput',
JSON.stringify(
convertSlateNodesToLexical({
canContainParagraphs: false,
converters,
parentNodeType: 'indent',
slateNodes: [node],
}),
null,
2,
),
)
return {
...convertSlateNodesToLexical({
canContainParagraphs: false,
converters,
parentNodeType: 'indent',
slateNodes: [node],
})[0],
indent: indentLevel,
} as const as SerializedLexicalNode
}
const children = node.children.map((child: any) => convertChildren(child, indentLevel + 1))
console.log('slateToLexical > IndentConverter > children', JSON.stringify(children, null, 2))
return {
children: children,
direction: 'ltr',
format: '',
indent: indentLevel,
type: 'paragraph',
version: 1,
} as const as SerializedParagraphNode
}
console.log(
'slateToLexical > IndentConverter > output',
JSON.stringify(convertChildren(slateNode), null, 2),
)
return convertChildren(slateNode)
},
nodeTypes: ['indent'],
}

View File

@@ -0,0 +1,29 @@
import type { SerializedLinkNode } from '../../../../Link/nodes/LinkNode'
import type { SlateNodeConverter } from '../types'
import { convertSlateNodesToLexical } from '..'
export const LinkConverter: SlateNodeConverter = {
converter({ converters, slateNode }) {
return {
children: convertSlateNodesToLexical({
canContainParagraphs: false,
converters,
parentNodeType: 'link',
slateNodes: slateNode.children || [],
}),
direction: 'ltr',
fields: {
doc: slateNode.doc || undefined,
linkType: slateNode.linkType || 'custom',
newTab: slateNode.newTab || false,
url: slateNode.url || undefined,
},
format: '',
indent: 0,
type: 'link',
version: 1,
} as const as SerializedLinkNode
},
nodeTypes: ['link'],
}

View File

@@ -0,0 +1,26 @@
import type { SerializedListItemNode } from '@lexical/list'
import type { SlateNodeConverter } from '../types'
import { convertSlateNodesToLexical } from '..'
export const ListItemConverter: SlateNodeConverter = {
converter({ childIndex, converters, slateNode }) {
return {
checked: undefined,
children: convertSlateNodesToLexical({
canContainParagraphs: false,
converters,
parentNodeType: 'listitem',
slateNodes: slateNode.children || [],
}),
direction: 'ltr',
format: '',
indent: 0,
type: 'listitem',
value: childIndex + 1,
version: 1,
} as const as SerializedListItemNode
},
nodeTypes: ['li'],
}

View File

@@ -0,0 +1,27 @@
import type { SerializedListNode } from '@lexical/list'
import type { SlateNodeConverter } from '../types'
import { convertSlateNodesToLexical } from '..'
export const OrderedListConverter: SlateNodeConverter = {
converter({ converters, slateNode }) {
return {
children: convertSlateNodesToLexical({
canContainParagraphs: false,
converters,
parentNodeType: 'list',
slateNodes: slateNode.children || [],
}),
direction: 'ltr',
format: '',
indent: 0,
listType: 'number',
start: 1,
tag: 'ol',
type: 'list',
version: 1,
} as const as SerializedListNode
},
nodeTypes: ['ol'],
}

View File

@@ -0,0 +1,17 @@
import type { SerializedRelationshipNode } from '../../../../../..'
import type { SlateNodeConverter } from '../types'
export const RelationshipConverter: SlateNodeConverter = {
converter({ slateNode }) {
return {
format: '',
relationTo: slateNode.relationTo,
type: 'relationship',
value: {
id: slateNode?.value?.id || '',
},
version: 1,
} as const as SerializedRelationshipNode
},
nodeTypes: ['relationship'],
}

View File

@@ -0,0 +1,27 @@
import type { SerializedUnknownConvertedNode } from '../../nodes/unknownConvertedNode'
import type { SlateNodeConverter } from '../types'
import { convertSlateNodesToLexical } from '..'
export const UnknownConverter: SlateNodeConverter = {
converter({ converters, slateNode }) {
return {
children: convertSlateNodesToLexical({
canContainParagraphs: false,
converters,
parentNodeType: 'unknownConverted',
slateNodes: slateNode.children || [],
}),
data: {
nodeData: slateNode,
nodeType: slateNode.type,
},
direction: 'ltr',
format: '',
indent: 0,
type: 'unknownConverted',
version: 1,
} as const as SerializedUnknownConvertedNode
},
nodeTypes: ['unknown'],
}

View File

@@ -0,0 +1,27 @@
import type { SerializedListNode } from '@lexical/list'
import type { SlateNodeConverter } from '../types'
import { convertSlateNodesToLexical } from '..'
export const UnorderedListConverter: SlateNodeConverter = {
converter({ converters, slateNode }) {
return {
children: convertSlateNodesToLexical({
canContainParagraphs: false,
converters,
parentNodeType: 'list',
slateNodes: slateNode.children || [],
}),
direction: 'ltr',
format: '',
indent: 0,
listType: 'bullet',
start: 1,
tag: 'ul',
type: 'list',
version: 1,
} as const as SerializedListNode
},
nodeTypes: ['ul'],
}

View File

@@ -0,0 +1,20 @@
import type { SerializedUploadNode } from '../../../../../..'
import type { SlateNodeConverter } from '../types'
export const UploadConverter: SlateNodeConverter = {
converter({ slateNode }) {
return {
fields: {
...slateNode.fields,
},
format: '',
relationTo: slateNode.relationTo,
type: 'upload',
value: {
id: slateNode.value?.id || '',
},
version: 1,
} as const as SerializedUploadNode
},
nodeTypes: ['upload'],
}

View File

@@ -0,0 +1,23 @@
import type { SlateNodeConverter } from './types'
import { HeadingConverter } from './converters/heading'
import { IndentConverter } from './converters/indent'
import { LinkConverter } from './converters/link'
import { ListItemConverter } from './converters/listItem'
import { OrderedListConverter } from './converters/orderedList'
import { RelationshipConverter } from './converters/relationship'
import { UnknownConverter } from './converters/unknown'
import { UnorderedListConverter } from './converters/unorderedList'
import { UploadConverter } from './converters/upload'
export const defaultConverters: SlateNodeConverter[] = [
UnknownConverter,
UploadConverter,
UnorderedListConverter,
OrderedListConverter,
RelationshipConverter,
ListItemConverter,
LinkConverter,
HeadingConverter,
IndentConverter,
]

View File

@@ -0,0 +1,137 @@
import type {
SerializedEditorState,
SerializedLexicalNode,
SerializedParagraphNode,
SerializedTextNode,
} from 'lexical'
import type { SlateNode, SlateNodeConverter } from './types'
import { NodeFormat } from '../../../../lexical/utils/nodeFormat'
export function convertSlateToLexical({
converters,
slateData,
}: {
converters: SlateNodeConverter[]
slateData: SlateNode[]
}): SerializedEditorState {
return {
root: {
children: convertSlateNodesToLexical({
canContainParagraphs: true,
converters,
parentNodeType: 'root',
slateNodes: slateData,
}),
direction: 'ltr',
format: '',
indent: 0,
type: 'root',
version: 1,
},
}
}
export function convertSlateNodesToLexical({
canContainParagraphs,
converters,
parentNodeType,
slateNodes,
}: {
canContainParagraphs: boolean
converters: SlateNodeConverter[]
/**
* Type of the parent lexical node (not the type of the original, parent slate type)
*/
parentNodeType: string
slateNodes: SlateNode[]
}): SerializedLexicalNode[] {
const unknownConverter = converters.find((converter) => converter.nodeTypes.includes('unknown'))
return (
slateNodes.map((slateNode, i) => {
if (!('type' in slateNode)) {
if (canContainParagraphs) {
// This is a paragraph node. They do not have a type property in Slate
return convertParagraphNode(converters, slateNode)
} else {
// This is a simple text node. canContainParagraphs may be false if this is nested inside of a paragraph already, since paragraphs cannot contain paragraphs
return convertTextNode(slateNode)
}
}
if (slateNode.type === 'p') {
return convertParagraphNode(converters, slateNode)
}
const converter = converters.find((converter) => converter.nodeTypes.includes(slateNode.type))
if (converter) {
return converter.converter({ childIndex: i, converters, parentNodeType, slateNode })
}
console.warn('slateToLexical > No converter found for node type: ' + slateNode.type)
return unknownConverter?.converter({
childIndex: i,
converters,
parentNodeType,
slateNode,
})
}) || []
)
}
export function convertParagraphNode(
converters: SlateNodeConverter[],
node: SlateNode,
): SerializedParagraphNode {
return {
children: convertSlateNodesToLexical({
canContainParagraphs: false,
converters,
parentNodeType: 'paragraph',
slateNodes: node.children || [],
}),
direction: 'ltr',
format: '',
indent: 0,
type: 'paragraph',
version: 1,
}
}
export function convertTextNode(node: SlateNode): SerializedTextNode {
return {
detail: 0,
format: convertNodeToFormat(node),
mode: 'normal',
style: '',
text: node.text,
type: 'text',
version: 1,
}
}
export function convertNodeToFormat(node: SlateNode): number {
let format = 0
if (node.bold) {
format = format | NodeFormat.IS_BOLD
}
if (node.italic) {
format = format | NodeFormat.IS_ITALIC
}
if (node.strikethrough) {
format = format | NodeFormat.IS_STRIKETHROUGH
}
if (node.underline) {
format = format | NodeFormat.IS_UNDERLINE
}
if (node.subscript) {
format = format | NodeFormat.IS_SUBSCRIPT
}
if (node.superscript) {
format = format | NodeFormat.IS_SUPERSCRIPT
}
if (node.code) {
format = format | NodeFormat.IS_CODE
}
return format
}

View File

@@ -0,0 +1,22 @@
import type { SerializedLexicalNode } from 'lexical'
export type SlateNodeConverter<T extends SerializedLexicalNode = SerializedLexicalNode> = {
converter: ({
childIndex,
converters,
parentNodeType,
slateNode,
}: {
childIndex: number
converters: SlateNodeConverter[]
parentNodeType: string
slateNode: SlateNode
}) => T
nodeTypes: string[]
}
export type SlateNode = {
[key: string]: any
children?: SlateNode[]
type?: string // doesn't always have type, e.g. for paragraphs
}

View File

@@ -0,0 +1,56 @@
import type { FeatureProvider } from '../../types'
import type { SlateNodeConverter } from './converter/types'
import { convertSlateToLexical } from './converter'
import { defaultConverters } from './converter/defaultConverters'
import { UnknownConvertedNode } from './nodes/unknownConvertedNode'
type Props = {
converters?:
| (({ defaultConverters }: { defaultConverters: SlateNodeConverter[] }) => SlateNodeConverter[])
| SlateNodeConverter[]
}
export const SlateToLexicalFeature = (props?: Props): FeatureProvider => {
if (!props) {
props = {}
}
props.converters =
props?.converters && typeof props?.converters === 'function'
? props.converters({ defaultConverters: defaultConverters })
: (props?.converters as SlateNodeConverter[]) || defaultConverters
return {
feature: ({ resolvedFeatures, unsanitizedEditorConfig }) => {
return {
hooks: {
load({ incomingEditorState }) {
if (
!incomingEditorState ||
!Array.isArray(incomingEditorState) ||
'root' in incomingEditorState
) {
// incomingEditorState null or not from Slate
return incomingEditorState
}
// Slate => convert to lexical
return convertSlateToLexical({
converters: props.converters as SlateNodeConverter[],
slateData: incomingEditorState,
})
},
},
nodes: [
{
node: UnknownConvertedNode,
type: UnknownConvertedNode.getType(),
},
],
props,
}
},
key: 'slateToLexical',
}
}

View File

@@ -0,0 +1,16 @@
@import 'payload/scss';
span.unknownConverted {
text-transform: uppercase;
font-family: 'Roboto Mono', monospace;
letter-spacing: 2px;
font-size: base(0.5);
margin: 0 0 base(1);
background: red;
color: white;
display: inline-block;
div {
background: red;
}
}

View File

@@ -0,0 +1,101 @@
import type { SerializedLexicalNode, Spread } from 'lexical'
import { addClassNamesToElement } from '@lexical/utils'
import { DecoratorNode, type EditorConfig, type LexicalNode, type NodeKey } from 'lexical'
import React from 'react'
import './index.scss'
export type UnknownConvertedNodeData = {
nodeData: unknown
nodeType: string
}
export type SerializedUnknownConvertedNode = Spread<
{
data: UnknownConvertedNodeData
},
SerializedLexicalNode
>
/** @noInheritDoc */
export class UnknownConvertedNode extends DecoratorNode<JSX.Element> {
__data: UnknownConvertedNodeData
constructor({ data, key }: { data: UnknownConvertedNodeData; key?: NodeKey }) {
super(key)
this.__data = data
}
static clone(node: UnknownConvertedNode): UnknownConvertedNode {
return new UnknownConvertedNode({
data: node.__data,
key: node.__key,
})
}
static getType(): string {
return 'unknownConverted'
}
static importJSON(serializedNode: SerializedUnknownConvertedNode): UnknownConvertedNode {
const node = $createUnknownConvertedNode({ data: serializedNode.data })
return node
}
canInsertTextAfter(): true {
return true
}
canInsertTextBefore(): true {
return true
}
createDOM(config: EditorConfig): HTMLElement {
const element = document.createElement('span')
addClassNamesToElement(element, 'unknownConverted')
return element
}
decorate(): JSX.Element | null {
return (
<div>
Unknown converted Slate node: <strong>{this.__data?.nodeType}</strong>
</div>
)
}
exportJSON(): SerializedUnknownConvertedNode {
return {
data: this.__data,
type: this.getType(),
version: 1,
}
}
// Mutation
isInline(): boolean {
return true
}
updateDOM(prevNode: UnknownConvertedNode, dom: HTMLElement): boolean {
return false
}
}
export function $createUnknownConvertedNode({
data,
}: {
data: UnknownConvertedNodeData
}): UnknownConvertedNode {
return new UnknownConvertedNode({
data,
})
}
export function $isUnknownConvertedNode(
node: LexicalNode | null | undefined,
): node is UnknownConvertedNode {
return node instanceof UnknownConvertedNode
}

View File

@@ -51,6 +51,18 @@ export type Feature = {
floatingSelectToolbar?: {
sections: FloatingToolbarSection[]
}
hooks?: {
load?: ({
incomingEditorState,
}: {
incomingEditorState: SerializedEditorState
}) => SerializedEditorState
save?: ({
incomingEditorState,
}: {
incomingEditorState: SerializedEditorState
}) => SerializedEditorState
}
markdownTransformers?: Transformer[]
nodes?: Array<{
afterReadPromises?: Array<AfterReadPromise>
@@ -123,6 +135,22 @@ export type SanitizedFeatures = Required<
floatingSelectToolbar: {
sections: FloatingToolbarSection[]
}
hooks: {
load: Array<
({
incomingEditorState,
}: {
incomingEditorState: SerializedEditorState
}) => SerializedEditorState
>
save: Array<
({
incomingEditorState,
}: {
incomingEditorState: SerializedEditorState
}) => SerializedEditorState
>
}
plugins?: Array<
| {
// plugins are anything which is not directly part of the editor. Like, creating a command which creates a node, or opens a modal, or some other more "outside" functionality

View File

@@ -1,3 +1,4 @@
'use client'
import { ShimmerEffect } from 'payload/components'
import React, { Suspense, lazy } from 'react'

View File

@@ -17,7 +17,9 @@ import { AddBlockHandlePlugin } from './plugins/handles/AddBlockHandlePlugin'
import { DraggableBlockPlugin } from './plugins/handles/DraggableBlockPlugin'
import { LexicalContentEditable } from './ui/ContentEditable'
export const LexicalEditor: React.FC<LexicalProviderProps> = (props) => {
export const LexicalEditor: React.FC<Pick<LexicalProviderProps, 'editorConfig' | 'onChange'>> = (
props,
) => {
const { editorConfig, onChange } = props
const [editor] = useLexicalComposerContext()

View File

@@ -14,19 +14,21 @@ import { getEnabledNodes } from './nodes'
export type LexicalProviderProps = {
editorConfig: SanitizedEditorConfig
fieldProps: FieldProps
initialState: SerializedEditorState
onChange: (editorState: EditorState, editor: LexicalEditor, tags: Set<string>) => void
readOnly: boolean
setValue: (value: SerializedEditorState) => void
value: SerializedEditorState
}
export const LexicalProvider: React.FC<LexicalProviderProps> = (props) => {
const { editorConfig, fieldProps, initialState, onChange, readOnly, setValue, value } = props
const { editorConfig, fieldProps, onChange, readOnly } = props
let { value } = props
if (
(value && Array.isArray(value) && !('root' in value)) ||
(initialState && Array.isArray(initialState) && !('root' in initialState))
) {
if (editorConfig?.features?.hooks?.load?.length) {
editorConfig.features.hooks.load.forEach((hook) => {
value = hook({ incomingEditorState: value })
})
}
if (value && Array.isArray(value) && !('root' in value)) {
throw new Error(
'You have tried to pass in data from the old, Slate editor, to the new, Lexical editor. This is not supported. There is no automatic conversion from Slate to Lexical data available yet (coming soon). Please remove the data from the field and start again.',
)
@@ -40,7 +42,7 @@ export const LexicalProvider: React.FC<LexicalProviderProps> = (props) => {
const initialConfig: InitialConfigType = {
editable: readOnly === true ? false : true,
editorState: initialState != null ? JSON.stringify(initialState) : undefined,
editorState: value != null ? JSON.stringify(value) : undefined,
namespace: editorConfig.lexical.namespace,
nodes: [...getEnabledNodes({ editorConfig })],
onError: (error: Error) => {
@@ -53,15 +55,7 @@ export const LexicalProvider: React.FC<LexicalProviderProps> = (props) => {
<LexicalComposer initialConfig={initialConfig}>
<EditorConfigProvider editorConfig={editorConfig} fieldProps={fieldProps}>
<div className="editor-shell">
<LexicalEditorComponent
editorConfig={editorConfig}
fieldProps={fieldProps}
initialState={initialState}
onChange={onChange}
readOnly={readOnly}
setValue={setValue}
value={value}
/>
<LexicalEditorComponent editorConfig={editorConfig} onChange={onChange} />
</div>
</EditorConfigProvider>
</LexicalComposer>

View File

@@ -10,6 +10,10 @@ export const sanitizeFeatures = (features: ResolvedFeatureMap): SanitizedFeature
floatingSelectToolbar: {
sections: [],
},
hooks: {
load: [],
save: [],
},
markdownTransformers: [],
nodes: [],
plugins: [],
@@ -21,6 +25,15 @@ export const sanitizeFeatures = (features: ResolvedFeatureMap): SanitizedFeature
}
features.forEach((feature) => {
if (feature.hooks) {
if (feature.hooks?.load?.length) {
sanitized.hooks.load = sanitized.hooks.load.concat(feature.hooks.load)
}
if (feature.hooks?.save?.length) {
sanitized.hooks.save = sanitized.hooks.save.concat(feature.hooks.save)
}
}
if (feature.nodes?.length) {
sanitized.nodes = sanitized.nodes.concat(feature.nodes)
feature.nodes.forEach((node) => {

View File

@@ -0,0 +1,124 @@
/* eslint-disable perfectionist/sort-objects */
/* eslint-disable regexp/no-obscure-range */
/* eslint-disable @typescript-eslint/no-redundant-type-constituents */
//This copy-and-pasted from lexical here here: https://github.com/facebook/lexical/blob/c2ceee223f46543d12c574e62155e619f9a18a5d/packages/lexical/src/LexicalConstants.ts
import type { ElementFormatType, TextFormatType } from 'lexical'
import type { TextDetailType, TextModeType } from 'lexical/nodes/LexicalTextNode'
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
// DOM
export const NodeFormat = {
DOM_ELEMENT_TYPE: 1,
DOM_TEXT_TYPE: 3,
// Reconciling
NO_DIRTY_NODES: 0,
HAS_DIRTY_NODES: 1,
FULL_RECONCILE: 2,
// Text node modes
IS_NORMAL: 0,
IS_TOKEN: 1,
IS_SEGMENTED: 2,
IS_INERT: 3,
// Text node formatting
IS_BOLD: 1,
IS_ITALIC: 1 << 1,
IS_STRIKETHROUGH: 1 << 2,
IS_UNDERLINE: 1 << 3,
IS_CODE: 1 << 4,
IS_SUBSCRIPT: 1 << 5,
IS_SUPERSCRIPT: 1 << 6,
IS_HIGHLIGHT: 1 << 7,
// Text node details
IS_DIRECTIONLESS: 1,
IS_UNMERGEABLE: 1 << 1,
// Element node formatting
IS_ALIGN_LEFT: 1,
IS_ALIGN_CENTER: 2,
IS_ALIGN_RIGHT: 3,
IS_ALIGN_JUSTIFY: 4,
IS_ALIGN_START: 5,
IS_ALIGN_END: 6,
} as const
export const IS_ALL_FORMATTING =
NodeFormat.IS_BOLD |
NodeFormat.IS_ITALIC |
NodeFormat.IS_STRIKETHROUGH |
NodeFormat.IS_UNDERLINE |
NodeFormat.IS_CODE |
NodeFormat.IS_SUBSCRIPT |
NodeFormat.IS_SUPERSCRIPT |
NodeFormat.IS_HIGHLIGHT
// Reconciliation
export const NON_BREAKING_SPACE = '\u00A0'
export const DOUBLE_LINE_BREAK = '\n\n'
// For FF, we need to use a non-breaking space, or it gets composition
// in a stuck state.
const RTL = '\u0591-\u07FF\uFB1D-\uFDFD\uFE70-\uFEFC'
const LTR =
'A-Za-z\u00C0-\u00D6\u00D8-\u00F6' +
'\u00F8-\u02B8\u0300-\u0590\u0800-\u1FFF\u200E\u2C00-\uFB1C' +
'\uFE00-\uFE6F\uFEFD-\uFFFF'
// eslint-disable-next-line no-misleading-character-class
export const RTL_REGEX = new RegExp('^[^' + LTR + ']*[' + RTL + ']')
// eslint-disable-next-line no-misleading-character-class
export const LTR_REGEX = new RegExp('^[^' + RTL + ']*[' + LTR + ']')
export const TEXT_TYPE_TO_FORMAT: Record<TextFormatType | string, number> = {
bold: NodeFormat.IS_BOLD,
code: NodeFormat.IS_CODE,
highlight: NodeFormat.IS_HIGHLIGHT,
italic: NodeFormat.IS_ITALIC,
strikethrough: NodeFormat.IS_STRIKETHROUGH,
subscript: NodeFormat.IS_SUBSCRIPT,
superscript: NodeFormat.IS_SUPERSCRIPT,
underline: NodeFormat.IS_UNDERLINE,
}
export const DETAIL_TYPE_TO_DETAIL: Record<TextDetailType | string, number> = {
directionless: NodeFormat.IS_DIRECTIONLESS,
unmergeable: NodeFormat.IS_UNMERGEABLE,
}
export const ELEMENT_TYPE_TO_FORMAT: Record<Exclude<ElementFormatType, ''>, number> = {
center: NodeFormat.IS_ALIGN_CENTER,
end: NodeFormat.IS_ALIGN_END,
justify: NodeFormat.IS_ALIGN_JUSTIFY,
left: NodeFormat.IS_ALIGN_LEFT,
right: NodeFormat.IS_ALIGN_RIGHT,
start: NodeFormat.IS_ALIGN_START,
}
export const ELEMENT_FORMAT_TO_TYPE: Record<number, ElementFormatType> = {
[NodeFormat.IS_ALIGN_CENTER]: 'center',
[NodeFormat.IS_ALIGN_END]: 'end',
[NodeFormat.IS_ALIGN_JUSTIFY]: 'justify',
[NodeFormat.IS_ALIGN_LEFT]: 'left',
[NodeFormat.IS_ALIGN_RIGHT]: 'right',
[NodeFormat.IS_ALIGN_START]: 'start',
}
export const TEXT_MODE_TO_TYPE: Record<TextModeType, 0 | 1 | 2> = {
normal: NodeFormat.IS_NORMAL,
segmented: NodeFormat.IS_SEGMENTED,
token: NodeFormat.IS_TOKEN,
}
export const TEXT_TYPE_TO_MODE: Record<number, TextModeType> = {
[NodeFormat.IS_NORMAL]: 'normal',
[NodeFormat.IS_SEGMENTED]: 'segmented',
[NodeFormat.IS_TOKEN]: 'token',
}

View File

@@ -2,7 +2,7 @@ import type { SerializedEditorState } from 'lexical'
import type { EditorConfig as LexicalEditorConfig } from 'lexical/LexicalEditor'
import type { RichTextAdapter } from 'payload/types'
import { withMergedProps } from 'payload/components/utilities'
import { withMergedProps } from 'payload/utilities'
import type { FeatureProvider } from './field/features/types'
import type { EditorConfig, SanitizedEditorConfig } from './field/lexical/config/types'
@@ -153,6 +153,9 @@ export { IndentFeature } from './field/features/indent'
export { CheckListFeature } from './field/features/lists/CheckList'
export { OrderedListFeature } from './field/features/lists/OrderedList'
export { UnoderedListFeature } from './field/features/lists/UnorderedList'
export { LexicalPluginToLexicalFeature } from './field/features/migrations/LexicalPluginToLexical'
export { SlateToLexicalFeature } from './field/features/migrations/SlateToLexical'
export type {
AfterReadPromise,
Feature,
@@ -201,6 +204,20 @@ export { isHTMLElement } from './field/lexical/utils/guard'
export { invariant } from './field/lexical/utils/invariant'
export { joinClasses } from './field/lexical/utils/joinClasses'
export { createBlockNode } from './field/lexical/utils/markdown/createBlockNode'
export {
DETAIL_TYPE_TO_DETAIL,
DOUBLE_LINE_BREAK,
ELEMENT_FORMAT_TO_TYPE,
ELEMENT_TYPE_TO_FORMAT,
IS_ALL_FORMATTING,
LTR_REGEX,
NON_BREAKING_SPACE,
NodeFormat,
RTL_REGEX,
TEXT_MODE_TO_TYPE,
TEXT_TYPE_TO_FORMAT,
TEXT_TYPE_TO_MODE,
} from './field/lexical/utils/nodeFormat'
export { Point, isPoint } from './field/lexical/utils/point'
export { Rect } from './field/lexical/utils/rect'
export { setFloatingElemPosition } from './field/lexical/utils/setFloatingElemPosition'

View File

@@ -173,7 +173,7 @@ export const recurseNestedFields = ({
promises,
req,
showHiddenFields,
siblingDoc,
siblingDoc: data[field.name][i], // This has to be scoped to the blocks's fields, otherwise there may be population issues, e.g. for a relationship field with Blocks Node, with a Blocks Field, with a RichText Field, With Relationship Node. The last richtext field would try to find itself using siblingDoc[field.nane], which only works if the siblingDoc is scoped to the blocks's fields
})
}
})
@@ -191,14 +191,13 @@ export const recurseNestedFields = ({
promises,
req,
showHiddenFields,
siblingDoc,
siblingDoc, // TODO: if there's any population issues, this might have to be data[field.name][i] as well
})
})
}
}
if (field.type === 'richText') {
// TODO: This does not properly work yet. E.g. it does not handle a relationship inside of lexical inside of block inside of lexical
const editor: RichTextAdapter = field?.editor
if (editor?.afterReadPromise) {

View File

@@ -59,7 +59,7 @@ export const recurseRichText = ({
}
}
if ('children' in node && node?.children) {
if ('children' in node && Array.isArray(node?.children) && node?.children?.length) {
recurseRichText({
afterReadPromises,
children: node.children as SerializedLexicalNode[],

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/richtext-slate",
"version": "1.0.2",
"version": "1.0.5",
"description": "The officially supported Slate richtext adapter for Payload",
"repository": "https://github.com/payloadcms/payload",
"license": "MIT",
@@ -33,6 +33,9 @@
"@types/react": "18.2.15",
"payload": "workspace:*"
},
"peerDependencies": {
"payload": "^2.0.6"
},
"exports": {
".": {
"default": "./src/index.ts",

Some files were not shown because too many files have changed in this diff Show More