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). 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 #### 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 `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 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 ### Fields

View File

@@ -131,6 +131,7 @@ const result = await payload.find({
depth: 2, depth: 2,
page: 1, page: 1,
limit: 10, limit: 10,
pagination: false, // If you want to disable pagination count, etc.
where: {}, // pass a `where` query here where: {}, // pass a `where` query here
sort: '-title', sort: '-title',
locale: 'en', 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 | | `limit` | Limits the number of documents returned |
| `page` | Get a specific page number | | `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" "lint:fix": "eslint --fix --ext .ts,.tsx src"
}, },
"dependencies": { "dependencies": {
"@payloadcms/bundler-webpack": "^1.0.0-beta.5", "@payloadcms/bundler-webpack": "latest",
"@payloadcms/db-mongodb": "^1.0.0-beta.8", "@payloadcms/db-mongodb": "latest",
"@payloadcms/richtext-slate": "^1.0.0-beta.4", "@payloadcms/richtext-slate": "latest",
"dotenv": "^8.2.0", "dotenv": "^8.2.0",
"express": "^4.17.1", "express": "^4.17.1",
"payload": "latest" "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" "lint:fix": "eslint --fix --ext .ts,.tsx src"
}, },
"dependencies": { "dependencies": {
"@payloadcms/bundler-webpack": "^1.0.0-beta.5", "@payloadcms/bundler-webpack": "latest",
"@payloadcms/db-mongodb": "^1.0.0-beta.8", "@payloadcms/db-mongodb": "latest",
"@payloadcms/richtext-slate": "^1.0.0-beta.4", "@payloadcms/richtext-slate": "latest",
"dotenv": "^8.2.0", "dotenv": "^8.2.0",
"escape-html": "^1.0.3", "escape-html": "^1.0.3",
"express": "^4.17.1", "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" "lint:fix": "eslint --fix --ext .ts,.tsx src"
}, },
"dependencies": { "dependencies": {
"@payloadcms/bundler-webpack": "^1.0.0-beta.5", "@payloadcms/bundler-webpack": "latest",
"@payloadcms/db-mongodb": "^1.0.0-beta.8", "@payloadcms/db-mongodb": "latest",
"@payloadcms/richtext-slate": "^1.0.0-beta.4", "@payloadcms/richtext-slate": "latest",
"dotenv": "^8.2.0", "dotenv": "^8.2.0",
"express": "^4.17.1", "express": "^4.17.1",
"payload": "latest" "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" "generate:graphQLSchema": "PAYLOAD_CONFIG_PATH=src/payload.config.ts payload generate:graphQLSchema"
}, },
"dependencies": { "dependencies": {
"@payloadcms/bundler-webpack": "^1.0.0-beta.5", "@payloadcms/bundler-webpack": "latest",
"@payloadcms/db-mongodb": "^1.0.0-beta.8", "@payloadcms/db-mongodb": "latest",
"@payloadcms/richtext-slate": "^1.0.0-beta.4", "@payloadcms/richtext-slate": "latest",
"@faceless-ui/modal": "^2.0.1", "@faceless-ui/modal": "^2.0.1",
"@payloadcms/plugin-form-builder": "^1.0.12", "@payloadcms/plugin-form-builder": "^1.0.12",
"@payloadcms/plugin-seo": "^1.0.8", "@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" "lint:fix": "eslint --fix --ext .ts,.tsx src"
}, },
"dependencies": { "dependencies": {
"@payloadcms/bundler-webpack": "^1.0.0-beta.5", "@payloadcms/bundler-webpack": "latest",
"@payloadcms/db-mongodb": "^1.0.0-beta.8", "@payloadcms/db-mongodb": "latest",
"@payloadcms/richtext-slate": "^1.0.0-beta.4", "@payloadcms/richtext-slate": "latest",
"dotenv": "^8.2.0", "dotenv": "^8.2.0",
"express": "^4.17.1", "express": "^4.17.1",
"payload": "latest" "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" "lint:fix": "eslint --fix --ext .ts,.tsx src"
}, },
"dependencies": { "dependencies": {
"@payloadcms/bundler-webpack": "^1.0.0-beta.5", "@payloadcms/bundler-webpack": "latest",
"@payloadcms/db-mongodb": "^1.0.0-beta.8", "@payloadcms/db-mongodb": "latest",
"@payloadcms/richtext-slate": "^1.0.0-beta.4", "@payloadcms/richtext-slate": "latest",
"dotenv": "^8.2.0", "dotenv": "^8.2.0",
"express": "^4.17.1", "express": "^4.17.1",
"payload": "latest" "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" "lint:fix": "eslint --fix --ext .ts,.tsx src"
}, },
"dependencies": { "dependencies": {
"@payloadcms/bundler-webpack": "^1.0.0-beta.5", "@payloadcms/bundler-webpack": "latest",
"@payloadcms/db-mongodb": "^1.0.0-beta.8", "@payloadcms/db-mongodb": "latest",
"@payloadcms/richtext-slate": "^1.0.0-beta.4", "@payloadcms/richtext-slate": "latest",
"@payloadcms/plugin-redirects": "^1.0.0", "@payloadcms/plugin-redirects": "^1.0.0",
"dotenv": "^8.2.0", "dotenv": "^8.2.0",
"express": "^4.17.1", "express": "^4.17.1",

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@@ -1,6 +1,5 @@
import type { Connect } from 'payload/database' import type { Connect } from 'payload/database'
import { pushSchema } from 'drizzle-kit/utils'
import { eq, sql } from 'drizzle-orm' import { eq, sql } from 'drizzle-orm'
import { drizzle } from 'drizzle-orm/node-postgres' import { drizzle } from 'drizzle-orm/node-postgres'
import { numeric, pgTable, timestamp, varchar } from 'drizzle-orm/pg-core' 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 return
const { pushSchema } = require('drizzle-kit/utils')
// This will prompt if clarifications are needed for Drizzle to push new schema // This will prompt if clarifications are needed for Drizzle to push new schema
const { apply, hasDataLoss, statementsToExecute, warnings } = await pushSchema( const { apply, hasDataLoss, statementsToExecute, warnings } = await pushSchema(
this.schema, this.schema,

View File

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

View File

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

View File

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

View File

@@ -100,7 +100,11 @@ export async function parseParams({
const val = where[relationOrPath][operator] const val = where[relationOrPath][operator]
queryConstraints.forEach(({ columnName: col, table: constraintTable, value }) => { 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)) { if (['json', 'richText'].includes(field.type) && Array.isArray(pathSegments)) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -8,6 +8,9 @@ import getSiblingData from './getSiblingData'
import reduceFieldsToValues from './reduceFieldsToValues' import reduceFieldsToValues from './reduceFieldsToValues'
import { flattenRows, separateRows } from './rows' 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 { export function fieldReducer(state: Fields, action: FieldAction): Fields {
switch (action.type) { switch (action.type) {
case 'REPLACE_STATE': { case 'REPLACE_STATE': {
@@ -123,7 +126,7 @@ export function fieldReducer(state: Fields, action: FieldAction): Fields {
...state[path], ...state[path],
disableFormData: rows.length > 0, disableFormData: rows.length > 0,
rows: rowsMetadata, rows: rowsMetadata,
value: rows, value: rows.length,
}, },
...flattenRows(path, rows), ...flattenRows(path, rows),
} }
@@ -161,10 +164,6 @@ export function fieldReducer(state: Fields, action: FieldAction): Fields {
const { remainingFields, rows: siblingRows } = separateRows(path, state) const { remainingFields, rows: siblingRows } = separateRows(path, state)
siblingRows.splice(rowIndex, 0, subFieldState) 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 = { const newState: Fields = {
...remainingFields, ...remainingFields,
...flattenRows(path, siblingRows), ...flattenRows(path, siblingRows),
@@ -172,7 +171,7 @@ export function fieldReducer(state: Fields, action: FieldAction): Fields {
...state[path], ...state[path],
disableFormData: true, disableFormData: true,
rows: rowsMetadata, rows: rowsMetadata,
value: newValue, value: siblingRows.length,
}, },
} }
@@ -203,10 +202,6 @@ export function fieldReducer(state: Fields, action: FieldAction): Fields {
// replace form _field state_ // replace form _field state_
siblingRows[rowIndex] = subFieldState siblingRows[rowIndex] = subFieldState
// replace array _value_
const newValue = Array.isArray(state[path]?.value) ? state[path]?.value : []
newValue[rowIndex] = reduceFieldsToValues(subFieldState, true)
const newState: Fields = { const newState: Fields = {
...remainingFields, ...remainingFields,
...flattenRows(path, siblingRows), ...flattenRows(path, siblingRows),
@@ -214,7 +209,7 @@ export function fieldReducer(state: Fields, action: FieldAction): Fields {
...state[path], ...state[path],
disableFormData: true, disableFormData: true,
rows: rowsMetadata, rows: rowsMetadata,
value: newValue, value: siblingRows.length,
}, },
} }
@@ -245,7 +240,7 @@ export function fieldReducer(state: Fields, action: FieldAction): Fields {
...state[path], ...state[path],
disableFormData: true, disableFormData: true,
rows: rowsMetadata, rows: rowsMetadata,
value: rows, value: rows.length,
}, },
...flattenRows(path, rows), ...flattenRows(path, rows),
} }

View File

@@ -50,12 +50,15 @@ import reduceFieldsToValues from './reduceFieldsToValues'
const baseClass = 'form' const baseClass = 'form'
const Form: React.FC<Props> = (props) => { const Form: React.FC<Props> = (props) => {
const { id, collection, getDocPreferences, global } = useDocumentInfo()
const { const {
action, action,
children, children,
className, className,
disableSuccessStatus, disableSuccessStatus,
disabled, disabled,
fields: fieldsFromProps = collection?.fields || global?.fields,
handleResponse, handleResponse,
initialData, // values only, paths are required as key - form should build initial state as convenience initialData, // values only, paths are required as key - form should build initial state as convenience
initialState, // fully formed initial field state initialState, // fully formed initial field state
@@ -71,7 +74,6 @@ const Form: React.FC<Props> = (props) => {
const { code: locale } = useLocale() const { code: locale } = useLocale()
const { i18n, t } = useTranslation('general') const { i18n, t } = useTranslation('general')
const { refreshCookie, user } = useAuth() const { refreshCookie, user } = useAuth()
const { id, collection, getDocPreferences, global } = useDocumentInfo()
const operation = useOperation() const operation = useOperation()
const config = useConfig() const config = useConfig()
@@ -90,6 +92,10 @@ const Form: React.FC<Props> = (props) => {
if (initialState) initialFieldState = initialState if (initialState) initialFieldState = initialState
const fieldsReducer = useReducer(fieldReducer, {}, () => initialFieldState) 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 const [fields, dispatchFields] = fieldsReducer
contextRef.current.fields = fields contextRef.current.fields = fields
@@ -167,7 +173,13 @@ const Form: React.FC<Props> = (props) => {
let validationResult: boolean | string = true let validationResult: boolean | string = true
if (typeof field.validate === 'function') { 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, id,
config, config,
data, data,
@@ -434,7 +446,7 @@ const Form: React.FC<Props> = (props) => {
const getRowSchemaByPath = React.useCallback( const getRowSchemaByPath = React.useCallback(
({ blockType, path }: { blockType?: string; path: string }) => { ({ blockType, path }: { blockType?: string; path: string }) => {
const rowConfig = traverseRowConfigs({ const rowConfig = traverseRowConfigs({
fieldConfig: collection?.fields || global?.fields, fieldConfig: fieldsFromProps,
path, path,
}) })
const rowFieldConfigs = buildFieldSchemaMap(rowConfig) const rowFieldConfigs = buildFieldSchemaMap(rowConfig)
@@ -442,10 +454,11 @@ const Form: React.FC<Props> = (props) => {
const fieldKey = pathSegments.at(-1) const fieldKey = pathSegments.at(-1)
return rowFieldConfigs.get(blockType ? `${fieldKey}.${blockType}` : fieldKey) 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( const addFieldRow: Context['addFieldRow'] = useCallback(
async ({ data, path, rowIndex }) => { async ({ data, path, rowIndex }) => {
const preferences = await getDocPreferences() const preferences = await getDocPreferences()

View File

@@ -2,7 +2,12 @@ import type React from 'react'
import type { Dispatch } from 'react' import type { Dispatch } from 'react'
import type { User } from '../../../../auth/types' 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 = { export type Row = {
blockType?: string blockType?: string
@@ -41,6 +46,12 @@ export type Props = {
className?: string className?: string
disableSuccessStatus?: boolean disableSuccessStatus?: boolean
disabled?: 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 handleResponse?: (res: Response) => void
initialData?: Data initialData?: Data
initialState?: Fields initialState?: Fields

View File

@@ -17,10 +17,21 @@ const intersectionObserverOptions = {
rootMargin: '1000px', rootMargin: '1000px',
} }
// If you send `fields` through, it will render those fields explicitly /**
// Otherwise, it will reduce your fields using the other provided props * If you send `fields` through, it will render those fields explicitly
// This is so that we can conditionally render fields before reducing them, if desired * Otherwise, it will reduce your fields using the other provided props
// See the sidebar in '../collections/Edit/Default/index.tsx' for an example * 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 RenderFields: React.FC<Props> = (props) => {
const { className, fieldTypes, forceRender, margins } = props const { className, fieldTypes, forceRender, margins } = props

View File

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

View File

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

View File

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

View File

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

View File

@@ -27,7 +27,8 @@
} }
&__fields { &__fields {
& > .tabs-field { & > .tabs-field,
& > .group-field {
margin-right: calc(var(--base) * -2); margin-right: calc(var(--base) * -2);
} }
} }
@@ -51,7 +52,7 @@
position: sticky; position: sticky;
top: var(--doc-controls-height); top: var(--doc-controls-height);
width: 33.33%; width: 33.33%;
height: 100%; height: calc(100vh - var(--doc-controls-height));
} }
&__sidebar { &__sidebar {
@@ -110,7 +111,8 @@
} }
&__fields { &__fields {
& > .tabs-field { & > .tabs-field,
& > .group-field {
margin-right: calc(var(--gutter-h) * -1); 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-sticky-wrap`}>
<div className={`${baseClass}__sidebar-fields`}> <div className={`${baseClass}__sidebar-fields`}>
<RenderFields <RenderFields
fieldSchema={fields}
fieldTypes={fieldTypes} fieldTypes={fieldTypes}
filter={(field) => field.admin.position === 'sidebar'} fields={sidebarFields}
permissions={permissions.fields} permissions={permissions.fields}
readOnly={!hasSavePermission} readOnly={!hasSavePermission}
/> />

View File

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

View File

@@ -27,7 +27,8 @@
} }
&__fields { &__fields {
& > .tabs-field { & > .tabs-field,
& > .group-field {
margin-right: calc(var(--base) * -2); margin-right: calc(var(--base) * -2);
} }
} }
@@ -55,7 +56,7 @@
position: sticky; position: sticky;
top: var(--doc-controls-height); top: var(--doc-controls-height);
width: 33.33%; width: 33.33%;
height: 100%; height: calc(100vh - var(--doc-controls-height));
} }
&__sidebar { &__sidebar {
@@ -106,7 +107,8 @@
} }
&__fields { &__fields {
& > .tabs-field { & > .tabs-field,
& > .group-field {
margin-right: calc(var(--gutter-h) * -1); 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`}>
<div className={`${baseClass}__sidebar-sticky-wrap`}> <div className={`${baseClass}__sidebar-sticky-wrap`}>
<div className={`${baseClass}__sidebar-fields`}> <div className={`${baseClass}__sidebar-fields`}>
<RenderFields fieldTypes={fieldTypes} fields={sidebarFields} /> <RenderFields
fieldTypes={fieldTypes}
fields={sidebarFields}
permissions={permissions.fields}
readOnly={!hasSavePermission}
/>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -18,16 +18,18 @@ export {
*/ */
useWatchForm, useWatchForm,
} from '../../admin/components/forms/Form/context' } from '../../admin/components/forms/Form/context'
export { createNestedFieldPath } from '../../admin/components/forms/Form/createNestedFieldPath' export { createNestedFieldPath } from '../../admin/components/forms/Form/createNestedFieldPath'
export { default as getSiblingData } from '../../admin/components/forms/Form/getSiblingData' export { default as getSiblingData } from '../../admin/components/forms/Form/getSiblingData'
export { default as reduceFieldsToValues } from '../../admin/components/forms/Form/reduceFieldsToValues' export { default as reduceFieldsToValues } from '../../admin/components/forms/Form/reduceFieldsToValues'
export { default as Label } from '../../admin/components/forms/Label' 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 Submit } from '../../admin/components/forms/Submit'
export { default as FormSubmit } 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 Checkbox } from '../../admin/components/forms/field-types/Checkbox'
export { default as Collapsible } from '../../admin/components/forms/field-types/Collapsible' export { default as Collapsible } from '../../admin/components/forms/field-types/Collapsible'

View File

@@ -1,7 +1,7 @@
export { buildConfig } from '../config/build' export { buildConfig } from '../config/build'
export * from '../config/types' 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 { defaults } from '../config/defaults'
export { sanitizeConfig } from '../config/sanitize' export { sanitizeConfig } from '../config/sanitize'
export { baseBlockFields } from '../fields/baseFields/baseBlockFields' 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 { extractTranslations } from '../translations/extractTranslations'
export { i18nInit } from '../translations/init'
export { i18nInit } from '../translations/init'
export { combineMerge } from '../utilities/combineMerge' export { combineMerge } from '../utilities/combineMerge'
export { configToJSONSchema, entityToJSONSchema } from '../utilities/configToJSONSchema' export { configToJSONSchema, entityToJSONSchema } from '../utilities/configToJSONSchema'
export { createArrayFromCommaDelineated } from '../utilities/createArrayFromCommaDelineated'
export { createArrayFromCommaDelineated } from '../utilities/createArrayFromCommaDelineated'
export { deepCopyObject } from '../utilities/deepCopyObject' export { deepCopyObject } from '../utilities/deepCopyObject'
export { deepMerge } from '../utilities/deepMerge' export { deepMerge } from '../utilities/deepMerge'
export { default as flattenTopLevelFields } from '../utilities/flattenTopLevelFields' 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) { 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) => { .map((err, invalid) => {
return `${err} ${JSON.stringify(invalid)}` return `${err} ${JSON.stringify(invalid)}`
}) })

View File

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

View File

@@ -1,3 +1,4 @@
'use client'
import type { SerializedEditorState } from 'lexical' import type { SerializedEditorState } from 'lexical'
import type { CellComponentProps, RichTextField } from 'payload/types' import type { CellComponentProps, RichTextField } from 'payload/types'
@@ -16,17 +17,38 @@ export const RichTextCell: React.FC<
const [preview, setPreview] = React.useState('Loading...') const [preview, setPreview] = React.useState('Loading...')
useEffect(() => { useEffect(() => {
if (data == null) { let dataToUse = data
if (dataToUse == null) {
setPreview('') setPreview('')
return 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 // initialize headless editor
const headlessEditor = createHeadlessEditor({ const headlessEditor = createHeadlessEditor({
namespace: editorConfig.lexical.namespace, namespace: editorConfig.lexical.namespace,
nodes: getEnabledNodes({ editorConfig }), nodes: getEnabledNodes({ editorConfig }),
theme: editorConfig.lexical.theme, theme: editorConfig.lexical.theme,
}) })
headlessEditor.setEditorState(headlessEditor.parseEditorState(data)) headlessEditor.setEditorState(headlessEditor.parseEditorState(dataToUse))
const textContent = const textContent =
headlessEditor.getEditorState().read(() => { headlessEditor.getEditorState().read(() => {

View File

@@ -47,7 +47,7 @@ const RichText: React.FC<FieldProps> = (props) => {
validate: memoizedValidate, validate: memoizedValidate,
}) })
const { errorMessage, initialValue, setValue, showError, value } = fieldType const { errorMessage, setValue, showError, value } = fieldType
let valueToUse = value let valueToUse = value
@@ -87,14 +87,19 @@ const RichText: React.FC<FieldProps> = (props) => {
<LexicalProvider <LexicalProvider
editorConfig={editorConfig} editorConfig={editorConfig}
fieldProps={props} fieldProps={props}
initialState={initialValue}
onChange={(editorState, editor, tags) => { 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} readOnly={readOnly}
setValue={setValue}
value={value} value={value}
/> />
<FieldDescription description={description} 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 { sanitizeFields } from 'payload/config'
import type { BlocksFeatureProps } from '.' import type { BlocksFeatureProps } from '.'
@@ -20,40 +22,42 @@ export const blockAfterReadPromiseHOC = (
showHiddenFields, showHiddenFields,
siblingDoc, siblingDoc,
}) => { }) => {
const blocks: Block[] = props.blocks
const blockFieldData = node.fields.data
const promises: Promise<void>[] = [] 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 // 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 payloadConfig = req.payload.config
const validRelationships = payloadConfig.collections.map((c) => c.slug) || [] const validRelationships = payloadConfig.collections.map((c) => c.slug) || []
props.blocks = props.blocks.map((block) => { blocks.forEach((block) => {
const unsanitizedBlock = { ...block } block.fields = sanitizeFields({
unsanitizedBlock.fields = sanitizeFields({
config: payloadConfig, config: payloadConfig,
fields: block.fields, fields: block.fields,
validRelationships, validRelationships,
}) })
return unsanitizedBlock
}) })
if (Array.isArray(props.blocks)) { // find block used in this node
props.blocks.forEach((block) => { const block = props.blocks.find((block) => block.slug === blockFieldData.blockType)
if (block?.fields) { if (!block || !block?.fields?.length || !blockFieldData) {
recurseNestedFields({ return promises
afterReadPromises,
currentDepth,
data: node.fields.data || {},
depth,
fields: block.fields,
overrideAccess,
promises,
req,
showHiddenFields,
siblingDoc,
})
}
})
} }
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 return promises
} }

View File

@@ -8,7 +8,7 @@ import { SectionTitle } from 'payload/components/fields/Blocks'
import { RenderFields, createNestedFieldPath, useFormSubmitted } from 'payload/components/forms' import { RenderFields, createNestedFieldPath, useFormSubmitted } from 'payload/components/forms'
import { useDocumentInfo } from 'payload/components/utilities' import { useDocumentInfo } from 'payload/components/utilities'
import { getTranslation } from 'payload/utilities' import { getTranslation } from 'payload/utilities'
import React, { useCallback, useEffect } from 'react' import React, { useCallback } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import type { FieldProps } from '../../../../types' import type { FieldProps } from '../../../../types'
@@ -24,6 +24,11 @@ type Props = {
nodeKey: string 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) => { export const BlockContent: React.FC<Props> = (props) => {
const { baseClass, block, field, fields, nodeKey } = props const { baseClass, block, field, fields, nodeKey } = props
const { i18n } = useTranslation() const { i18n } = useTranslation()

View File

@@ -1,14 +1,20 @@
import { type ElementFormatType } from 'lexical' import { type ElementFormatType } from 'lexical'
import { Form, buildInitialState, useFormSubmitted } from 'payload/components/forms' 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' import { type BlockFields } from '../nodes/BlocksNode'
const baseClass = 'lexical-block' const baseClass = 'lexical-block'
import type { Data } from 'payload/types' 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 { sanitizeFields } from 'payload/config'
import { useTranslation } from 'react-i18next'
import type { BlocksFeatureProps } from '..' import type { BlocksFeatureProps } from '..'
@@ -43,13 +49,49 @@ export const BlockComponent: React.FC<Props> = (props) => {
validRelationships, 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 // Memoized Form JSX
const formContent = useMemo(() => { const formContent = useMemo(() => {
return ( return (
block && ( block &&
<Form initialState={initialDataRef?.current} submitted={submitted}> initialState && (
<Form fields={block.fields} initialState={initialState} submitted={submitted}>
<BlockContent <BlockContent
baseClass={baseClass} baseClass={baseClass}
block={block} block={block}
@@ -60,7 +102,7 @@ export const BlockComponent: React.FC<Props> = (props) => {
</Form> </Form>
) )
) )
}, [block, field, nodeKey, submitted]) }, [block, field, nodeKey, submitted, initialState])
return <div className={baseClass}>{formContent}</div> return <div className={baseClass}>{formContent}</div>
} }

View File

@@ -15,12 +15,12 @@ export const blockValidationHOC = (
payloadConfig, payloadConfig,
validation, validation,
}) => { }) => {
const blockFieldValues = node.fields.data const blockFieldData = node.fields.data
const blocks: Block[] = props.blocks 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 // 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) => { blocks.forEach((block) => {
const validRelationships = payloadConfig.collections.map((c) => c.slug) || []
block.fields = sanitizeFields({ block.fields = sanitizeFields({
config: payloadConfig, config: payloadConfig,
fields: block.fields, fields: block.fields,
@@ -29,7 +29,7 @@ export const blockValidationHOC = (
}) })
// find block // find block
const block = props.blocks.find((block) => block.slug === blockFieldValues.blockType) const block = props.blocks.find((block) => block.slug === blockFieldData.blockType)
// validate block // validate block
if (!block) { if (!block) {

View File

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

View File

@@ -2,7 +2,7 @@ import { Drawer } from 'payload/components/elements'
import { Form } from 'payload/components/forms' import { Form } from 'payload/components/forms'
import { RenderFields } from 'payload/components/forms' import { RenderFields } from 'payload/components/forms'
import { FormSubmit } 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 React from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'

View File

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

View File

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

View File

@@ -4,7 +4,7 @@ import { useModal } from '@faceless-ui/modal'
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import { $getNodeByKey } from 'lexical' import { $getNodeByKey } from 'lexical'
import { Drawer } from 'payload/components/elements' import { Drawer } from 'payload/components/elements'
import { Form, FormSubmit, RenderFields } from 'payload/components/forms' import { Form, FormSubmit, RenderFields, fieldTypes } from 'payload/components/forms'
import { import {
buildStateFromSchema, buildStateFromSchema,
useAuth, useAuth,
@@ -12,7 +12,7 @@ import {
useDocumentInfo, useDocumentInfo,
useLocale, useLocale,
} from 'payload/components/utilities' } from 'payload/components/utilities'
import { fieldTypes, sanitizeFields } from 'payload/config' import { sanitizeFields } from 'payload/config'
import { deepCopyObject, getTranslation } from 'payload/utilities' import { deepCopyObject, getTranslation } from 'payload/utilities'
import React, { useCallback, useEffect, useState } from 'react' import React, { useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next' 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?: { floatingSelectToolbar?: {
sections: FloatingToolbarSection[] sections: FloatingToolbarSection[]
} }
hooks?: {
load?: ({
incomingEditorState,
}: {
incomingEditorState: SerializedEditorState
}) => SerializedEditorState
save?: ({
incomingEditorState,
}: {
incomingEditorState: SerializedEditorState
}) => SerializedEditorState
}
markdownTransformers?: Transformer[] markdownTransformers?: Transformer[]
nodes?: Array<{ nodes?: Array<{
afterReadPromises?: Array<AfterReadPromise> afterReadPromises?: Array<AfterReadPromise>
@@ -123,6 +135,22 @@ export type SanitizedFeatures = Required<
floatingSelectToolbar: { floatingSelectToolbar: {
sections: FloatingToolbarSection[] sections: FloatingToolbarSection[]
} }
hooks: {
load: Array<
({
incomingEditorState,
}: {
incomingEditorState: SerializedEditorState
}) => SerializedEditorState
>
save: Array<
({
incomingEditorState,
}: {
incomingEditorState: SerializedEditorState
}) => SerializedEditorState
>
}
plugins?: Array< 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 // 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 { ShimmerEffect } from 'payload/components'
import React, { Suspense, lazy } from 'react' 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 { DraggableBlockPlugin } from './plugins/handles/DraggableBlockPlugin'
import { LexicalContentEditable } from './ui/ContentEditable' 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 { editorConfig, onChange } = props
const [editor] = useLexicalComposerContext() const [editor] = useLexicalComposerContext()

View File

@@ -14,19 +14,21 @@ import { getEnabledNodes } from './nodes'
export type LexicalProviderProps = { export type LexicalProviderProps = {
editorConfig: SanitizedEditorConfig editorConfig: SanitizedEditorConfig
fieldProps: FieldProps fieldProps: FieldProps
initialState: SerializedEditorState
onChange: (editorState: EditorState, editor: LexicalEditor, tags: Set<string>) => void onChange: (editorState: EditorState, editor: LexicalEditor, tags: Set<string>) => void
readOnly: boolean readOnly: boolean
setValue: (value: SerializedEditorState) => void
value: SerializedEditorState value: SerializedEditorState
} }
export const LexicalProvider: React.FC<LexicalProviderProps> = (props) => { 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 ( if (editorConfig?.features?.hooks?.load?.length) {
(value && Array.isArray(value) && !('root' in value)) || editorConfig.features.hooks.load.forEach((hook) => {
(initialState && Array.isArray(initialState) && !('root' in initialState)) value = hook({ incomingEditorState: value })
) { })
}
if (value && Array.isArray(value) && !('root' in value)) {
throw new Error( 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.', '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 = { const initialConfig: InitialConfigType = {
editable: readOnly === true ? false : true, editable: readOnly === true ? false : true,
editorState: initialState != null ? JSON.stringify(initialState) : undefined, editorState: value != null ? JSON.stringify(value) : undefined,
namespace: editorConfig.lexical.namespace, namespace: editorConfig.lexical.namespace,
nodes: [...getEnabledNodes({ editorConfig })], nodes: [...getEnabledNodes({ editorConfig })],
onError: (error: Error) => { onError: (error: Error) => {
@@ -53,15 +55,7 @@ export const LexicalProvider: React.FC<LexicalProviderProps> = (props) => {
<LexicalComposer initialConfig={initialConfig}> <LexicalComposer initialConfig={initialConfig}>
<EditorConfigProvider editorConfig={editorConfig} fieldProps={fieldProps}> <EditorConfigProvider editorConfig={editorConfig} fieldProps={fieldProps}>
<div className="editor-shell"> <div className="editor-shell">
<LexicalEditorComponent <LexicalEditorComponent editorConfig={editorConfig} onChange={onChange} />
editorConfig={editorConfig}
fieldProps={fieldProps}
initialState={initialState}
onChange={onChange}
readOnly={readOnly}
setValue={setValue}
value={value}
/>
</div> </div>
</EditorConfigProvider> </EditorConfigProvider>
</LexicalComposer> </LexicalComposer>

View File

@@ -10,6 +10,10 @@ export const sanitizeFeatures = (features: ResolvedFeatureMap): SanitizedFeature
floatingSelectToolbar: { floatingSelectToolbar: {
sections: [], sections: [],
}, },
hooks: {
load: [],
save: [],
},
markdownTransformers: [], markdownTransformers: [],
nodes: [], nodes: [],
plugins: [], plugins: [],
@@ -21,6 +25,15 @@ export const sanitizeFeatures = (features: ResolvedFeatureMap): SanitizedFeature
} }
features.forEach((feature) => { 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) { if (feature.nodes?.length) {
sanitized.nodes = sanitized.nodes.concat(feature.nodes) sanitized.nodes = sanitized.nodes.concat(feature.nodes)
feature.nodes.forEach((node) => { 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 { EditorConfig as LexicalEditorConfig } from 'lexical/LexicalEditor'
import type { RichTextAdapter } from 'payload/types' 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 { FeatureProvider } from './field/features/types'
import type { EditorConfig, SanitizedEditorConfig } from './field/lexical/config/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 { CheckListFeature } from './field/features/lists/CheckList'
export { OrderedListFeature } from './field/features/lists/OrderedList' export { OrderedListFeature } from './field/features/lists/OrderedList'
export { UnoderedListFeature } from './field/features/lists/UnorderedList' export { UnoderedListFeature } from './field/features/lists/UnorderedList'
export { LexicalPluginToLexicalFeature } from './field/features/migrations/LexicalPluginToLexical'
export { SlateToLexicalFeature } from './field/features/migrations/SlateToLexical'
export type { export type {
AfterReadPromise, AfterReadPromise,
Feature, Feature,
@@ -201,6 +204,20 @@ export { isHTMLElement } from './field/lexical/utils/guard'
export { invariant } from './field/lexical/utils/invariant' export { invariant } from './field/lexical/utils/invariant'
export { joinClasses } from './field/lexical/utils/joinClasses' export { joinClasses } from './field/lexical/utils/joinClasses'
export { createBlockNode } from './field/lexical/utils/markdown/createBlockNode' 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 { Point, isPoint } from './field/lexical/utils/point'
export { Rect } from './field/lexical/utils/rect' export { Rect } from './field/lexical/utils/rect'
export { setFloatingElemPosition } from './field/lexical/utils/setFloatingElemPosition' export { setFloatingElemPosition } from './field/lexical/utils/setFloatingElemPosition'

View File

@@ -173,7 +173,7 @@ export const recurseNestedFields = ({
promises, promises,
req, req,
showHiddenFields, 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, promises,
req, req,
showHiddenFields, 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') { 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 const editor: RichTextAdapter = field?.editor
if (editor?.afterReadPromise) { 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({ recurseRichText({
afterReadPromises, afterReadPromises,
children: node.children as SerializedLexicalNode[], children: node.children as SerializedLexicalNode[],

View File

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

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