Compare commits
64 Commits
db-postgre
...
db-postgre
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4b9e87bb4d | ||
|
|
ff5e174497 | ||
|
|
34017e1758 | ||
|
|
23d95526ab | ||
|
|
59b87fdb21 | ||
|
|
f2ac1f7d48 | ||
|
|
8d4f39af5e | ||
|
|
c4ac341d75 | ||
|
|
27589482dd | ||
|
|
d7ab4b7062 | ||
|
|
2c8fbf1be3 | ||
|
|
eec88f8f1b | ||
|
|
1481ef97b5 | ||
|
|
a89e89fb80 | ||
|
|
7e7eeb059d | ||
|
|
dc2a502dcc | ||
|
|
a9a5ba82d8 | ||
|
|
e6e8fae1c5 | ||
|
|
a05868a7f3 | ||
|
|
f27cd26575 | ||
|
|
db835ea5c8 | ||
|
|
d8f265fb94 | ||
|
|
d81d4eb075 | ||
|
|
52f89c0136 | ||
|
|
b0083b7c07 | ||
|
|
21649537a6 | ||
|
|
9be34c9599 | ||
|
|
8ca632e541 | ||
|
|
2ef79145a4 | ||
|
|
a0641a445d | ||
|
|
3a2e78f7f3 | ||
|
|
976d69d154 | ||
|
|
66018362fe | ||
|
|
647fe23d1c | ||
|
|
d7c61861f6 | ||
|
|
7caa098023 | ||
|
|
fd54c40400 | ||
|
|
e180131314 | ||
|
|
5902d4542b | ||
|
|
6bc282444e | ||
|
|
4dc6c09347 | ||
|
|
03b9ab0054 | ||
|
|
3c3c93f483 | ||
|
|
5dbfb1a335 | ||
|
|
d411874589 | ||
|
|
8358e2f2d2 | ||
|
|
2c67eff059 | ||
|
|
012b8e6f90 | ||
|
|
fcd4c8d830 | ||
|
|
81ec435363 | ||
|
|
e116fcfbf5 | ||
|
|
c47632dc1d | ||
|
|
0dab68b336 | ||
|
|
483f93bfcf | ||
|
|
4bd01df411 | ||
|
|
c956a85252 | ||
|
|
beed83b231 | ||
|
|
3b1bdcbe41 | ||
|
|
d3d0971275 | ||
|
|
1a99d66cd0 | ||
|
|
52c4a63bf1 | ||
|
|
3446d28602 | ||
|
|
2eb18771a1 | ||
|
|
f6fd5d6742 |
@@ -129,7 +129,7 @@ To add a _new_ view to the Admin Panel, simply add another key to the `views` ob
|
||||
}
|
||||
```
|
||||
|
||||
_For more examples regarding how to customize components, look at the following [examples](https://github.com/payloadcms/payload/tree/master/test/admin/components)._
|
||||
_For more examples regarding how to customize components, look at the following [examples](https://github.com/payloadcms/payload/tree/main/test/admin/components)._
|
||||
|
||||
For help on how to build your own custom view components, see [building a custom view component](#building-a-custom-view-component).
|
||||
|
||||
@@ -399,12 +399,12 @@ Your custom view components will be given all the props that a React Router `<Ro
|
||||
|
||||
#### Example
|
||||
|
||||
You can find examples of custom views in the [Payload source code `/test/admin/components/views` folder](https://github.com/payloadcms/payload/tree/master/test/admin/components/views). There, you'll find two custom views:
|
||||
You can find examples of custom views in the [Payload source code `/test/admin/components/views` folder](https://github.com/payloadcms/payload/tree/main/test/admin/components/views). There, you'll find two custom views:
|
||||
|
||||
1. A custom view that uses the `DefaultTemplate`, which is the built-in Payload template that displays the sidebar and "eyebrow nav"
|
||||
1. A custom view that uses the `MinimalTemplate` - which is just a centered template used for things like logging in or out
|
||||
|
||||
To see how to pass in your custom views to create custom views of your own, take a look at the `admin.components.views` property of the [Payload test admin config](https://github.com/payloadcms/payload/blob/master/test/admin/config.ts).
|
||||
To see how to pass in your custom views to create custom views of your own, take a look at the `admin.components.views` property of the [Payload test admin config](https://github.com/payloadcms/payload/blob/main/test/admin/config.ts).
|
||||
|
||||
### Fields
|
||||
|
||||
|
||||
@@ -131,6 +131,7 @@ const result = await payload.find({
|
||||
depth: 2,
|
||||
page: 1,
|
||||
limit: 10,
|
||||
pagination: false, // If you want to disable pagination count, etc.
|
||||
where: {}, // pass a `where` query here
|
||||
sort: '-title',
|
||||
locale: 'en',
|
||||
|
||||
@@ -59,3 +59,7 @@ All Payload APIs support the pagination controls below. With them, you can creat
|
||||
| ------- | --------------------------------------- |
|
||||
| `limit` | Limits the number of documents returned |
|
||||
| `page` | Get a specific page number |
|
||||
|
||||
### Disabling pagination within Local API
|
||||
|
||||
For `find` operations within the Local API, you can disable pagination to retrieve all documents from a collection by passing `pagination: false` to the `find` local operation. This is not supported in REST or GraphQL, however, because it could potentially lead to malicious activity.
|
||||
@@ -17,9 +17,9 @@
|
||||
"lint:fix": "eslint --fix --ext .ts,.tsx src"
|
||||
},
|
||||
"dependencies": {
|
||||
"@payloadcms/bundler-webpack": "^1.0.0-beta.5",
|
||||
"@payloadcms/db-mongodb": "^1.0.0-beta.8",
|
||||
"@payloadcms/richtext-slate": "^1.0.0-beta.4",
|
||||
"@payloadcms/bundler-webpack": "latest",
|
||||
"@payloadcms/db-mongodb": "latest",
|
||||
"@payloadcms/richtext-slate": "latest",
|
||||
"dotenv": "^8.2.0",
|
||||
"express": "^4.17.1",
|
||||
"payload": "latest"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -20,9 +20,9 @@
|
||||
"lint:fix": "eslint --fix --ext .ts,.tsx src"
|
||||
},
|
||||
"dependencies": {
|
||||
"@payloadcms/bundler-webpack": "^1.0.0-beta.5",
|
||||
"@payloadcms/db-mongodb": "^1.0.0-beta.8",
|
||||
"@payloadcms/richtext-slate": "^1.0.0-beta.4",
|
||||
"@payloadcms/bundler-webpack": "latest",
|
||||
"@payloadcms/db-mongodb": "latest",
|
||||
"@payloadcms/richtext-slate": "latest",
|
||||
"dotenv": "^8.2.0",
|
||||
"escape-html": "^1.0.3",
|
||||
"express": "^4.17.1",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -18,9 +18,9 @@
|
||||
"lint:fix": "eslint --fix --ext .ts,.tsx src"
|
||||
},
|
||||
"dependencies": {
|
||||
"@payloadcms/bundler-webpack": "^1.0.0-beta.5",
|
||||
"@payloadcms/db-mongodb": "^1.0.0-beta.8",
|
||||
"@payloadcms/richtext-slate": "^1.0.0-beta.4",
|
||||
"@payloadcms/bundler-webpack": "latest",
|
||||
"@payloadcms/db-mongodb": "latest",
|
||||
"@payloadcms/richtext-slate": "latest",
|
||||
"dotenv": "^8.2.0",
|
||||
"express": "^4.17.1",
|
||||
"payload": "latest"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -15,9 +15,9 @@
|
||||
"generate:graphQLSchema": "PAYLOAD_CONFIG_PATH=src/payload.config.ts payload generate:graphQLSchema"
|
||||
},
|
||||
"dependencies": {
|
||||
"@payloadcms/bundler-webpack": "^1.0.0-beta.5",
|
||||
"@payloadcms/db-mongodb": "^1.0.0-beta.8",
|
||||
"@payloadcms/richtext-slate": "^1.0.0-beta.4",
|
||||
"@payloadcms/bundler-webpack": "latest",
|
||||
"@payloadcms/db-mongodb": "latest",
|
||||
"@payloadcms/richtext-slate": "latest",
|
||||
"@faceless-ui/modal": "^2.0.1",
|
||||
"@payloadcms/plugin-form-builder": "^1.0.12",
|
||||
"@payloadcms/plugin-seo": "^1.0.8",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -18,9 +18,9 @@
|
||||
"lint:fix": "eslint --fix --ext .ts,.tsx src"
|
||||
},
|
||||
"dependencies": {
|
||||
"@payloadcms/bundler-webpack": "^1.0.0-beta.5",
|
||||
"@payloadcms/db-mongodb": "^1.0.0-beta.8",
|
||||
"@payloadcms/richtext-slate": "^1.0.0-beta.4",
|
||||
"@payloadcms/bundler-webpack": "latest",
|
||||
"@payloadcms/db-mongodb": "latest",
|
||||
"@payloadcms/richtext-slate": "latest",
|
||||
"dotenv": "^8.2.0",
|
||||
"express": "^4.17.1",
|
||||
"payload": "latest"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -18,9 +18,9 @@
|
||||
"lint:fix": "eslint --fix --ext .ts,.tsx src"
|
||||
},
|
||||
"dependencies": {
|
||||
"@payloadcms/bundler-webpack": "^1.0.0-beta.5",
|
||||
"@payloadcms/db-mongodb": "^1.0.0-beta.8",
|
||||
"@payloadcms/richtext-slate": "^1.0.0-beta.4",
|
||||
"@payloadcms/bundler-webpack": "latest",
|
||||
"@payloadcms/db-mongodb": "latest",
|
||||
"@payloadcms/richtext-slate": "latest",
|
||||
"dotenv": "^8.2.0",
|
||||
"express": "^4.17.1",
|
||||
"payload": "latest"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -17,9 +17,9 @@
|
||||
"lint:fix": "eslint --fix --ext .ts,.tsx src"
|
||||
},
|
||||
"dependencies": {
|
||||
"@payloadcms/bundler-webpack": "^1.0.0-beta.5",
|
||||
"@payloadcms/db-mongodb": "^1.0.0-beta.8",
|
||||
"@payloadcms/richtext-slate": "^1.0.0-beta.4",
|
||||
"@payloadcms/bundler-webpack": "latest",
|
||||
"@payloadcms/db-mongodb": "latest",
|
||||
"@payloadcms/richtext-slate": "latest",
|
||||
"@payloadcms/plugin-redirects": "^1.0.0",
|
||||
"dotenv": "^8.2.0",
|
||||
"express": "^4.17.1",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -20,8 +20,8 @@
|
||||
"lint-staged": "lint-staged",
|
||||
"pretest": "pnpm build",
|
||||
"reinstall": "pnpm clean:unix && pnpm install",
|
||||
"list:packages": "./scripts/list_published_packages.sh beta",
|
||||
"script:release:beta": "./scripts/release_beta.sh",
|
||||
"script:list-packages": "tsx ./scripts/list-packages.ts",
|
||||
"script:release": "tsx ./scripts/release.ts",
|
||||
"test": "pnpm test:int && pnpm test:components && pnpm test:e2e",
|
||||
"test:components": "cross-env jest --config=jest.components.config.js",
|
||||
"test:e2e": "npx playwright install --with-deps && ts-node -T ./test/runE2E.ts",
|
||||
@@ -59,6 +59,7 @@
|
||||
"fs-extra": "10.1.0",
|
||||
"get-port": "5.1.1",
|
||||
"glob": "8.1.0",
|
||||
"graphql-request": "6.1.0",
|
||||
"husky": "^8.0.3",
|
||||
"isomorphic-fetch": "3.0.0",
|
||||
"jest": "29.6.4",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-postgres",
|
||||
"version": "0.1.4",
|
||||
"version": "0.1.8",
|
||||
"description": "The officially supported Postgres database adapter for Payload",
|
||||
"repository": "https://github.com/payloadcms/payload",
|
||||
"license": "MIT",
|
||||
@@ -34,9 +34,6 @@
|
||||
"@types/to-snake-case": "1.0.0",
|
||||
"payload": "workspace:*"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"better-sqlite3": "^8.5.0"
|
||||
},
|
||||
"publishConfig": {
|
||||
"main": "./dist/index.js",
|
||||
"registry": "https://registry.npmjs.org/",
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { Connect } from 'payload/database'
|
||||
|
||||
import { pushSchema } from 'drizzle-kit/utils'
|
||||
import { eq, sql } from 'drizzle-orm'
|
||||
import { drizzle } from 'drizzle-orm/node-postgres'
|
||||
import { numeric, pgTable, timestamp, varchar } from 'drizzle-orm/pg-core'
|
||||
@@ -40,6 +39,8 @@ export const connect: Connect = async function connect(this: PostgresAdapter, pa
|
||||
)
|
||||
return
|
||||
|
||||
const { pushSchema } = require('drizzle-kit/utils')
|
||||
|
||||
// This will prompt if clarifications are needed for Drizzle to push new schema
|
||||
const { apply, hasDataLoss, statementsToExecute, warnings } = await pushSchema(
|
||||
this.schema,
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
import type { DrizzleSnapshotJSON } from 'drizzle-kit/utils'
|
||||
import type { CreateMigration } from 'payload/database'
|
||||
|
||||
import { generateDrizzleJson, generateMigration } from 'drizzle-kit/utils'
|
||||
import fs from 'fs'
|
||||
import prompts from 'prompts'
|
||||
|
||||
@@ -61,6 +60,8 @@ export const createMigration: CreateMigration = async function createMigration(
|
||||
fs.mkdirSync(dir)
|
||||
}
|
||||
|
||||
const { generateDrizzleJson, generateMigration } = require('drizzle-kit/utils')
|
||||
|
||||
const [yyymmdd, hhmmss] = new Date().toISOString().split('T')
|
||||
const formattedDate = yyymmdd.replace(/\D/g, '')
|
||||
const formattedTime = hhmmss.split('.')[0].replace(/\D/g, '')
|
||||
|
||||
@@ -33,7 +33,7 @@ export const findMany = async function find({
|
||||
const db = adapter.sessions[req.transactionID]?.db || adapter.drizzle
|
||||
const table = adapter.tables[tableName]
|
||||
|
||||
let limit = limitArg
|
||||
let limit = limitArg ?? 10
|
||||
let totalDocs: number
|
||||
let totalPages: number
|
||||
let hasPrevPage: boolean
|
||||
@@ -119,7 +119,11 @@ export const findMany = async function find({
|
||||
findManyArgs.where = inArray(adapter.tables[tableName].id, Object.keys(orderedIDMap))
|
||||
} else {
|
||||
findManyArgs.limit = limitArg === 0 ? undefined : limitArg
|
||||
findManyArgs.offset = skip || (page - 1) * limitArg
|
||||
|
||||
const offset = skip || (page - 1) * limitArg
|
||||
|
||||
if (!Number.isNaN(offset)) findManyArgs.offset = offset
|
||||
|
||||
if (where) {
|
||||
findManyArgs.where = where
|
||||
}
|
||||
|
||||
@@ -2,9 +2,7 @@
|
||||
import type { Payload } from 'payload'
|
||||
import type { Migration } from 'payload/database'
|
||||
|
||||
import { generateDrizzleJson } from 'drizzle-kit/utils'
|
||||
import { readMigrationFiles } from 'payload/database'
|
||||
import { DatabaseError } from 'pg'
|
||||
import prompts from 'prompts'
|
||||
|
||||
import type { PostgresAdapter } from './types'
|
||||
@@ -78,6 +76,8 @@ export async function migrate(this: PostgresAdapter): Promise<void> {
|
||||
}
|
||||
|
||||
async function runMigrationFile(payload: Payload, migration: Migration, batch: number) {
|
||||
const { generateDrizzleJson } = require('drizzle-kit/utils')
|
||||
|
||||
const start = Date.now()
|
||||
|
||||
payload.logger.info({ msg: `Migrating: ${migration.name}` })
|
||||
|
||||
@@ -34,12 +34,14 @@ type Args = {
|
||||
aliasTable?: GenericTable
|
||||
collectionPath: string
|
||||
columnPrefix?: string
|
||||
constraintPath?: string
|
||||
constraints?: Constraint[]
|
||||
fields: (Field | TabAsField)[]
|
||||
joinAliases: BuildQueryJoinAliases
|
||||
joins: BuildQueryJoins
|
||||
locale?: string
|
||||
pathSegments: string[]
|
||||
rootTableName?: string
|
||||
selectFields: Record<string, GenericColumn>
|
||||
tableName: string
|
||||
}
|
||||
@@ -53,17 +55,22 @@ export const getTableColumnFromPath = ({
|
||||
aliasTable,
|
||||
collectionPath,
|
||||
columnPrefix = '',
|
||||
constraintPath: incomingConstraintPath,
|
||||
constraints = [],
|
||||
fields,
|
||||
joinAliases,
|
||||
joins,
|
||||
locale: incomingLocale,
|
||||
pathSegments: incomingSegments,
|
||||
rootTableName: incomingRootTableName,
|
||||
selectFields,
|
||||
tableName,
|
||||
}: Args): TableColumn => {
|
||||
const fieldPath = incomingSegments[0]
|
||||
let locale = incomingLocale
|
||||
const rootTableName = incomingRootTableName || tableName
|
||||
let constraintPath = incomingConstraintPath || ''
|
||||
|
||||
const field = flattenTopLevelFields(fields as Field[]).find(
|
||||
(fieldToFind) => fieldAffectsData(fieldToFind) && fieldToFind.name === fieldPath,
|
||||
) as Field | TabAsField
|
||||
@@ -105,6 +112,7 @@ export const getTableColumnFromPath = ({
|
||||
aliasTable,
|
||||
collectionPath,
|
||||
columnPrefix,
|
||||
constraintPath,
|
||||
constraints,
|
||||
fields: field.tabs.map((tab) => ({
|
||||
...tab,
|
||||
@@ -114,6 +122,7 @@ export const getTableColumnFromPath = ({
|
||||
joins,
|
||||
locale,
|
||||
pathSegments: pathSegments.slice(1),
|
||||
rootTableName,
|
||||
selectFields,
|
||||
tableName: newTableName,
|
||||
})
|
||||
@@ -125,12 +134,14 @@ export const getTableColumnFromPath = ({
|
||||
aliasTable,
|
||||
collectionPath,
|
||||
columnPrefix: `${columnPrefix}${field.name}_`,
|
||||
constraintPath,
|
||||
constraints,
|
||||
fields: field.fields,
|
||||
joinAliases,
|
||||
joins,
|
||||
locale,
|
||||
pathSegments: pathSegments.slice(1),
|
||||
rootTableName,
|
||||
selectFields,
|
||||
tableName: newTableName,
|
||||
})
|
||||
@@ -140,12 +151,14 @@ export const getTableColumnFromPath = ({
|
||||
aliasTable,
|
||||
collectionPath,
|
||||
columnPrefix,
|
||||
constraintPath,
|
||||
constraints,
|
||||
fields: field.fields,
|
||||
joinAliases,
|
||||
joins,
|
||||
locale,
|
||||
pathSegments: pathSegments.slice(1),
|
||||
rootTableName,
|
||||
selectFields,
|
||||
tableName: newTableName,
|
||||
})
|
||||
@@ -172,12 +185,14 @@ export const getTableColumnFromPath = ({
|
||||
aliasTable,
|
||||
collectionPath,
|
||||
columnPrefix: `${columnPrefix}${field.name}_`,
|
||||
constraintPath,
|
||||
constraints,
|
||||
fields: field.fields,
|
||||
joinAliases,
|
||||
joins,
|
||||
locale,
|
||||
pathSegments: pathSegments.slice(1),
|
||||
rootTableName,
|
||||
selectFields,
|
||||
tableName: newTableName,
|
||||
})
|
||||
@@ -185,6 +200,7 @@ export const getTableColumnFromPath = ({
|
||||
|
||||
case 'array': {
|
||||
newTableName = `${tableName}_${toSnakeCase(field.name)}`
|
||||
constraintPath = `${constraintPath}${field.name}.%.`
|
||||
if (locale && field.localized && adapter.payload.config.localization) {
|
||||
joins[newTableName] = and(
|
||||
eq(adapter.tables[tableName].id, adapter.tables[newTableName]._parentID),
|
||||
@@ -206,12 +222,14 @@ export const getTableColumnFromPath = ({
|
||||
return getTableColumnFromPath({
|
||||
adapter,
|
||||
collectionPath,
|
||||
constraintPath,
|
||||
constraints,
|
||||
fields: field.fields,
|
||||
joinAliases,
|
||||
joins,
|
||||
locale,
|
||||
pathSegments: pathSegments.slice(1),
|
||||
rootTableName,
|
||||
selectFields,
|
||||
tableName: newTableName,
|
||||
})
|
||||
@@ -229,12 +247,14 @@ export const getTableColumnFromPath = ({
|
||||
result = getTableColumnFromPath({
|
||||
adapter,
|
||||
collectionPath,
|
||||
constraintPath: '',
|
||||
constraints: blockConstraints,
|
||||
fields: block.fields,
|
||||
joinAliases,
|
||||
joins,
|
||||
locale,
|
||||
pathSegments: pathSegments.slice(1),
|
||||
rootTableName,
|
||||
selectFields: blockSelectFields,
|
||||
tableName: newTableName,
|
||||
})
|
||||
@@ -283,9 +303,8 @@ export const getTableColumnFromPath = ({
|
||||
case 'relationship':
|
||||
case 'upload': {
|
||||
let relationshipFields
|
||||
const relationTableName = `${tableName}_rels`
|
||||
const relationTableName = `${rootTableName}_rels`
|
||||
const newCollectionPath = pathSegments.slice(1).join('.')
|
||||
|
||||
const aliasRelationshipTableName = uuid()
|
||||
const aliasRelationshipTable = alias(
|
||||
adapter.tables[relationTableName],
|
||||
@@ -295,7 +314,7 @@ export const getTableColumnFromPath = ({
|
||||
// Join in the relationships table
|
||||
joinAliases.push({
|
||||
condition: eq(
|
||||
(aliasTable || adapter.tables[tableName]).id,
|
||||
(aliasTable || adapter.tables[rootTableName]).id,
|
||||
aliasRelationshipTable.parent,
|
||||
),
|
||||
table: aliasRelationshipTable,
|
||||
@@ -306,7 +325,7 @@ export const getTableColumnFromPath = ({
|
||||
constraints.push({
|
||||
columnName: 'path',
|
||||
table: aliasRelationshipTable,
|
||||
value: field.name,
|
||||
value: `${constraintPath}${field.name}`,
|
||||
})
|
||||
|
||||
let newAliasTable
|
||||
@@ -368,6 +387,7 @@ export const getTableColumnFromPath = ({
|
||||
joins,
|
||||
locale,
|
||||
pathSegments: pathSegments.slice(1),
|
||||
rootTableName: newTableName,
|
||||
selectFields,
|
||||
tableName: newTableName,
|
||||
})
|
||||
|
||||
@@ -100,7 +100,11 @@ export async function parseParams({
|
||||
const val = where[relationOrPath][operator]
|
||||
|
||||
queryConstraints.forEach(({ columnName: col, table: constraintTable, 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)) {
|
||||
@@ -144,13 +148,19 @@ export async function parseParams({
|
||||
break
|
||||
}
|
||||
|
||||
const { operator: queryOperator, value: queryValue } = sanitizeQueryValue({
|
||||
const sanitizedQueryValue = sanitizeQueryValue({
|
||||
field,
|
||||
operator,
|
||||
relationOrPath,
|
||||
val,
|
||||
})
|
||||
|
||||
if (sanitizedQueryValue === null) {
|
||||
break
|
||||
}
|
||||
|
||||
const { operator: queryOperator, value: queryValue } = sanitizedQueryValue
|
||||
|
||||
if (queryOperator === 'not_equals' && queryValue !== null) {
|
||||
constraints.push(
|
||||
or(
|
||||
@@ -159,7 +169,10 @@ export async function parseParams({
|
||||
ne<any>(rawColumn || table[columnName], queryValue),
|
||||
),
|
||||
)
|
||||
} else if (
|
||||
break
|
||||
}
|
||||
|
||||
if (
|
||||
(field.type === 'relationship' || field.type === 'upload') &&
|
||||
Array.isArray(queryValue) &&
|
||||
operator === 'not_in'
|
||||
@@ -170,7 +183,10 @@ export async function parseParams({
|
||||
IS
|
||||
NULL`,
|
||||
)
|
||||
} else {
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
constraints.push(
|
||||
operatorMap[queryOperator](rawColumn || table[columnName], queryValue),
|
||||
)
|
||||
@@ -181,7 +197,6 @@ export async function parseParams({
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (constraints.length > 0) {
|
||||
if (result) {
|
||||
result = and(result, ...constraints)
|
||||
|
||||
@@ -42,7 +42,8 @@ export const sanitizeQueryValue = ({
|
||||
if (val.toLowerCase() === 'false') formattedValue = false
|
||||
}
|
||||
|
||||
if (['all', 'in', 'not_in'].includes(operator) && typeof formattedValue === 'string') {
|
||||
if (['all', 'in', 'not_in'].includes(operator)) {
|
||||
if (typeof formattedValue === 'string') {
|
||||
formattedValue = createArrayFromCommaDelineated(formattedValue)
|
||||
|
||||
if (field.type === 'number') {
|
||||
@@ -50,6 +51,11 @@ export const sanitizeQueryValue = ({
|
||||
}
|
||||
}
|
||||
|
||||
if (!Array.isArray(formattedValue) || formattedValue.length === 0) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
if (field.type === 'number' && typeof formattedValue === 'string') {
|
||||
formattedValue = Number(val)
|
||||
}
|
||||
|
||||
@@ -6,11 +6,12 @@
|
||||
|
||||
// drizzle-kit@utils
|
||||
|
||||
import { generateDrizzleJson, generateMigration, pushSchema } from 'drizzle-kit/utils'
|
||||
import { drizzle } from 'drizzle-orm/node-postgres'
|
||||
import { Pool } from 'pg'
|
||||
|
||||
async function generateUsage() {
|
||||
const { generateDrizzleJson, generateMigration } = require('drizzle-kit/utils')
|
||||
|
||||
// @ts-expect-error Just TypeScript being broken // TODO: Open TypeScript issue
|
||||
const schema = await import('./data/users')
|
||||
// @ts-expect-error Just TypeScript being broken // TODO: Open TypeScript issue
|
||||
@@ -25,6 +26,8 @@ async function generateUsage() {
|
||||
}
|
||||
|
||||
async function pushUsage() {
|
||||
const { pushSchema } = require('drizzle-kit/utils')
|
||||
|
||||
// @ts-expect-error Just TypeScript being broken // TODO: Open TypeScript issue
|
||||
const schemaAfter = await import('./data/users-after')
|
||||
|
||||
|
||||
@@ -252,6 +252,8 @@ export const traverseFields = ({
|
||||
}
|
||||
|
||||
case 'array': {
|
||||
const disableNotNullFromHere = Boolean(field.admin?.condition) || disableNotNull
|
||||
|
||||
const arrayTableName = `${newTableName}_${toSnakeCase(field.name)}`
|
||||
const baseColumns: Record<string, PgColumnBuilder> = {
|
||||
_order: integer('_order').notNull(),
|
||||
@@ -277,7 +279,7 @@ export const traverseFields = ({
|
||||
adapter,
|
||||
baseColumns,
|
||||
baseExtraConfig,
|
||||
disableNotNull,
|
||||
disableNotNull: disableNotNullFromHere,
|
||||
disableUnique,
|
||||
fields: field.fields,
|
||||
rootRelationsToBuild,
|
||||
@@ -314,6 +316,8 @@ export const traverseFields = ({
|
||||
}
|
||||
|
||||
case 'blocks': {
|
||||
const disableNotNullFromHere = Boolean(field.admin?.condition) || disableNotNull
|
||||
|
||||
field.blocks.forEach((block) => {
|
||||
const blockTableName = `${rootTableName}_blocks_${toSnakeCase(block.slug)}`
|
||||
if (!adapter.tables[blockTableName]) {
|
||||
@@ -343,7 +347,7 @@ export const traverseFields = ({
|
||||
adapter,
|
||||
baseColumns,
|
||||
baseExtraConfig,
|
||||
disableNotNull,
|
||||
disableNotNull: disableNotNullFromHere,
|
||||
disableUnique,
|
||||
fields: block.fields,
|
||||
rootRelationsToBuild,
|
||||
@@ -428,6 +432,8 @@ export const traverseFields = ({
|
||||
break
|
||||
}
|
||||
|
||||
const disableNotNullFromHere = Boolean(field.admin?.condition) || disableNotNull
|
||||
|
||||
const {
|
||||
hasLocalizedField: groupHasLocalizedField,
|
||||
hasLocalizedManyNumberField: groupHasLocalizedManyNumberField,
|
||||
@@ -438,7 +444,7 @@ export const traverseFields = ({
|
||||
buildRelationships,
|
||||
columnPrefix: `${columnName}_`,
|
||||
columns,
|
||||
disableNotNull,
|
||||
disableNotNull: disableNotNullFromHere,
|
||||
disableUnique,
|
||||
fieldPrefix: `${fieldName}_`,
|
||||
fields: field.fields,
|
||||
@@ -463,6 +469,8 @@ export const traverseFields = ({
|
||||
}
|
||||
|
||||
case 'tabs': {
|
||||
const disableNotNullFromHere = Boolean(field.admin?.condition) || disableNotNull
|
||||
|
||||
const {
|
||||
hasLocalizedField: tabHasLocalizedField,
|
||||
hasLocalizedManyNumberField: tabHasLocalizedManyNumberField,
|
||||
@@ -473,7 +481,7 @@ export const traverseFields = ({
|
||||
buildRelationships,
|
||||
columnPrefix,
|
||||
columns,
|
||||
disableNotNull,
|
||||
disableNotNull: disableNotNullFromHere,
|
||||
disableUnique,
|
||||
fieldPrefix,
|
||||
fields: field.tabs.map((tab) => ({ ...tab, type: 'tab' })),
|
||||
@@ -500,6 +508,7 @@ export const traverseFields = ({
|
||||
|
||||
case 'row':
|
||||
case 'collapsible': {
|
||||
const disableNotNullFromHere = Boolean(field.admin?.condition) || disableNotNull
|
||||
const {
|
||||
hasLocalizedField: rowHasLocalizedField,
|
||||
hasLocalizedManyNumberField: rowHasLocalizedManyNumberField,
|
||||
@@ -510,7 +519,7 @@ export const traverseFields = ({
|
||||
buildRelationships,
|
||||
columnPrefix,
|
||||
columns,
|
||||
disableNotNull,
|
||||
disableNotNull: disableNotNullFromHere,
|
||||
disableUnique,
|
||||
fieldPrefix,
|
||||
fields: field.fields,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/live-preview-react",
|
||||
"version": "0.1.2",
|
||||
"version": "0.1.3",
|
||||
"description": "The official live preview React SDK for Payload",
|
||||
"repository": "https://github.com/payloadcms/payload",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/live-preview",
|
||||
"version": "0.1.2",
|
||||
"version": "0.1.3",
|
||||
"description": "The official live preview JavaScript SDK for Payload",
|
||||
"repository": "https://github.com/payloadcms/payload",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "payload",
|
||||
"version": "2.0.5",
|
||||
"version": "2.0.7",
|
||||
"description": "Node, React and MongoDB Headless CMS and Application Framework",
|
||||
"license": "MIT",
|
||||
"main": "./dist/index.js",
|
||||
@@ -187,7 +187,6 @@
|
||||
"file-loader": "6.2.0",
|
||||
"form-data": "3.0.1",
|
||||
"get-port": "5.1.1",
|
||||
"graphql-request": "6.1.0",
|
||||
"mini-css-extract-plugin": "1.6.2",
|
||||
"node-fetch": "2.6.12",
|
||||
"nodemon": "3.0.1",
|
||||
@@ -208,8 +207,7 @@
|
||||
"webpack": "^5.78.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14",
|
||||
"pnpm": ">=8"
|
||||
"node": ">=14"
|
||||
},
|
||||
"files": [
|
||||
"bin.js",
|
||||
|
||||
@@ -55,24 +55,6 @@ export const DocumentControls: React.FC<{
|
||||
|
||||
const { i18n, t } = useTranslation('general')
|
||||
|
||||
let showPreviewButton = false
|
||||
|
||||
if (collection) {
|
||||
showPreviewButton =
|
||||
isEditing &&
|
||||
collection?.admin?.preview &&
|
||||
collection?.versions?.drafts &&
|
||||
!collection?.versions?.drafts?.autosave
|
||||
}
|
||||
|
||||
if (global) {
|
||||
showPreviewButton =
|
||||
isEditing &&
|
||||
global?.admin?.preview &&
|
||||
global?.versions?.drafts &&
|
||||
!global?.versions?.drafts?.autosave
|
||||
}
|
||||
|
||||
const showDotMenu = Boolean(collection && id && !disableActions)
|
||||
|
||||
return (
|
||||
@@ -165,7 +147,7 @@ export const DocumentControls: React.FC<{
|
||||
</div>
|
||||
<div className={`${baseClass}__controls-wrapper`}>
|
||||
<div className={`${baseClass}__controls`}>
|
||||
{showPreviewButton && (
|
||||
{(collection?.admin?.preview || global?.admin?.preview) && (
|
||||
<PreviewButton
|
||||
CustomComponent={
|
||||
collection?.admin?.components?.edit?.PreviewButton ||
|
||||
|
||||
@@ -51,6 +51,7 @@ const Duplicate: React.FC<Props> = ({ id, collection, slug }) => {
|
||||
},
|
||||
params: {
|
||||
depth: 0,
|
||||
draft: true,
|
||||
locale,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -61,6 +61,7 @@ export const addFieldStatePromise = async ({
|
||||
user,
|
||||
value: data?.[field.name],
|
||||
})
|
||||
|
||||
if (data?.[field.name]) {
|
||||
data[field.name] = valueWithDefault
|
||||
}
|
||||
@@ -145,8 +146,8 @@ export const addFieldStatePromise = async ({
|
||||
fieldState.value = null
|
||||
fieldState.initialValue = null
|
||||
} else {
|
||||
fieldState.value = arrayValue
|
||||
fieldState.initialValue = arrayValue
|
||||
fieldState.value = arrayValue.length
|
||||
fieldState.initialValue = arrayValue.length
|
||||
|
||||
if (arrayValue.length > 0) {
|
||||
fieldState.disableFormData = true
|
||||
@@ -236,8 +237,8 @@ export const addFieldStatePromise = async ({
|
||||
fieldState.value = null
|
||||
fieldState.initialValue = null
|
||||
} else {
|
||||
fieldState.value = blocksValue
|
||||
fieldState.initialValue = blocksValue
|
||||
fieldState.value = blocksValue.length
|
||||
fieldState.initialValue = blocksValue.length
|
||||
|
||||
if (blocksValue.length > 0) {
|
||||
fieldState.disableFormData = true
|
||||
|
||||
@@ -8,6 +8,9 @@ import getSiblingData from './getSiblingData'
|
||||
import reduceFieldsToValues from './reduceFieldsToValues'
|
||||
import { flattenRows, separateRows } from './rows'
|
||||
|
||||
/**
|
||||
* Reducer which modifies the form field state (all the current data of the fields in the form). When called using dispatch, it will return a new state object.
|
||||
*/
|
||||
export function fieldReducer(state: Fields, action: FieldAction): Fields {
|
||||
switch (action.type) {
|
||||
case 'REPLACE_STATE': {
|
||||
@@ -123,7 +126,7 @@ export function fieldReducer(state: Fields, action: FieldAction): Fields {
|
||||
...state[path],
|
||||
disableFormData: rows.length > 0,
|
||||
rows: rowsMetadata,
|
||||
value: rows,
|
||||
value: rows.length,
|
||||
},
|
||||
...flattenRows(path, rows),
|
||||
}
|
||||
@@ -161,10 +164,6 @@ export function fieldReducer(state: Fields, action: FieldAction): Fields {
|
||||
const { remainingFields, rows: siblingRows } = separateRows(path, state)
|
||||
siblingRows.splice(rowIndex, 0, subFieldState)
|
||||
|
||||
// add new row to array _value_
|
||||
const currentValue = (Array.isArray(state[path]?.value) ? state[path]?.value : []) as Fields[]
|
||||
const newValue = currentValue.splice(rowIndex, 0, reduceFieldsToValues(subFieldState, true))
|
||||
|
||||
const newState: Fields = {
|
||||
...remainingFields,
|
||||
...flattenRows(path, siblingRows),
|
||||
@@ -172,7 +171,7 @@ export function fieldReducer(state: Fields, action: FieldAction): Fields {
|
||||
...state[path],
|
||||
disableFormData: true,
|
||||
rows: rowsMetadata,
|
||||
value: newValue,
|
||||
value: siblingRows.length,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -203,10 +202,6 @@ export function fieldReducer(state: Fields, action: FieldAction): Fields {
|
||||
// replace form _field state_
|
||||
siblingRows[rowIndex] = subFieldState
|
||||
|
||||
// replace array _value_
|
||||
const newValue = Array.isArray(state[path]?.value) ? state[path]?.value : []
|
||||
newValue[rowIndex] = reduceFieldsToValues(subFieldState, true)
|
||||
|
||||
const newState: Fields = {
|
||||
...remainingFields,
|
||||
...flattenRows(path, siblingRows),
|
||||
@@ -214,7 +209,7 @@ export function fieldReducer(state: Fields, action: FieldAction): Fields {
|
||||
...state[path],
|
||||
disableFormData: true,
|
||||
rows: rowsMetadata,
|
||||
value: newValue,
|
||||
value: siblingRows.length,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -245,7 +240,7 @@ export function fieldReducer(state: Fields, action: FieldAction): Fields {
|
||||
...state[path],
|
||||
disableFormData: true,
|
||||
rows: rowsMetadata,
|
||||
value: rows,
|
||||
value: rows.length,
|
||||
},
|
||||
...flattenRows(path, rows),
|
||||
}
|
||||
|
||||
@@ -50,12 +50,15 @@ import reduceFieldsToValues from './reduceFieldsToValues'
|
||||
const baseClass = 'form'
|
||||
|
||||
const Form: React.FC<Props> = (props) => {
|
||||
const { id, collection, getDocPreferences, global } = useDocumentInfo()
|
||||
|
||||
const {
|
||||
action,
|
||||
children,
|
||||
className,
|
||||
disableSuccessStatus,
|
||||
disabled,
|
||||
fields: fieldsFromProps = collection?.fields || global?.fields,
|
||||
handleResponse,
|
||||
initialData, // values only, paths are required as key - form should build initial state as convenience
|
||||
initialState, // fully formed initial field state
|
||||
@@ -71,7 +74,6 @@ const Form: React.FC<Props> = (props) => {
|
||||
const { code: locale } = useLocale()
|
||||
const { i18n, t } = useTranslation('general')
|
||||
const { refreshCookie, user } = useAuth()
|
||||
const { id, collection, getDocPreferences, global } = useDocumentInfo()
|
||||
const operation = useOperation()
|
||||
|
||||
const config = useConfig()
|
||||
@@ -90,6 +92,10 @@ const Form: React.FC<Props> = (props) => {
|
||||
if (initialState) initialFieldState = initialState
|
||||
|
||||
const fieldsReducer = useReducer(fieldReducer, {}, () => initialFieldState)
|
||||
/**
|
||||
* `fields` is the current, up-to-date state/data of all fields in the form. It can be modified by using dispatchFields,
|
||||
* which calls the fieldReducer, which then updates the state.
|
||||
*/
|
||||
const [fields, dispatchFields] = fieldsReducer
|
||||
|
||||
contextRef.current.fields = fields
|
||||
@@ -167,7 +173,13 @@ const Form: React.FC<Props> = (props) => {
|
||||
let validationResult: boolean | string = true
|
||||
|
||||
if (typeof field.validate === 'function') {
|
||||
validationResult = await field.validate(field.value, {
|
||||
let valueToValidate = field.value
|
||||
|
||||
if (field?.rows && Array.isArray(field.rows)) {
|
||||
valueToValidate = contextRef.current.getDataByPath(path)
|
||||
}
|
||||
|
||||
validationResult = await field.validate(valueToValidate, {
|
||||
id,
|
||||
config,
|
||||
data,
|
||||
@@ -434,7 +446,7 @@ const Form: React.FC<Props> = (props) => {
|
||||
const getRowSchemaByPath = React.useCallback(
|
||||
({ blockType, path }: { blockType?: string; path: string }) => {
|
||||
const rowConfig = traverseRowConfigs({
|
||||
fieldConfig: collection?.fields || global?.fields,
|
||||
fieldConfig: fieldsFromProps,
|
||||
path,
|
||||
})
|
||||
const rowFieldConfigs = buildFieldSchemaMap(rowConfig)
|
||||
@@ -442,10 +454,11 @@ const Form: React.FC<Props> = (props) => {
|
||||
const fieldKey = pathSegments.at(-1)
|
||||
return rowFieldConfigs.get(blockType ? `${fieldKey}.${blockType}` : fieldKey)
|
||||
},
|
||||
[traverseRowConfigs, collection?.fields, global?.fields],
|
||||
[traverseRowConfigs, fieldsFromProps],
|
||||
)
|
||||
|
||||
// Array/Block row manipulation
|
||||
// Array/Block row manipulation. This is called when, for example, you add a new block to a blocks field.
|
||||
// The block data is saved in the rows property of the state, which is modified updated here.
|
||||
const addFieldRow: Context['addFieldRow'] = useCallback(
|
||||
async ({ data, path, rowIndex }) => {
|
||||
const preferences = await getDocPreferences()
|
||||
|
||||
@@ -2,7 +2,12 @@ import type React from 'react'
|
||||
import type { Dispatch } from 'react'
|
||||
|
||||
import type { User } from '../../../../auth/types'
|
||||
import type { Condition, Field as FieldConfig, Validate } from '../../../../fields/config/types'
|
||||
import type {
|
||||
Condition,
|
||||
Field,
|
||||
Field as FieldConfig,
|
||||
Validate,
|
||||
} from '../../../../fields/config/types'
|
||||
|
||||
export type Row = {
|
||||
blockType?: string
|
||||
@@ -41,6 +46,12 @@ export type Props = {
|
||||
className?: string
|
||||
disableSuccessStatus?: boolean
|
||||
disabled?: boolean
|
||||
/**
|
||||
* By default, the form will get the field schema (not data) from the current document. If you pass this in, you can override that behavior.
|
||||
* This is very useful for sub-forms, where the form's field schema is not necessarily the field schema of the current document (e.g. for the Blocks
|
||||
* feature of the Lexical Rich Text field)
|
||||
*/
|
||||
fields?: Field[]
|
||||
handleResponse?: (res: Response) => void
|
||||
initialData?: Data
|
||||
initialState?: Fields
|
||||
|
||||
@@ -17,10 +17,21 @@ const intersectionObserverOptions = {
|
||||
rootMargin: '1000px',
|
||||
}
|
||||
|
||||
// If you send `fields` through, it will render those fields explicitly
|
||||
// Otherwise, it will reduce your fields using the other provided props
|
||||
// This is so that we can conditionally render fields before reducing them, if desired
|
||||
// See the sidebar in '../collections/Edit/Default/index.tsx' for an example
|
||||
/**
|
||||
* If you send `fields` through, it will render those fields explicitly
|
||||
* Otherwise, it will reduce your fields using the other provided props
|
||||
* This is so that we can conditionally render fields before reducing them, if desired
|
||||
* See the sidebar in '../collections/Edit/Default/index.tsx' for an example
|
||||
*
|
||||
* The state/data for the fields it renders is not managed by this component. Instead, every component it renders has
|
||||
* their own handling of their own value, usually through the useField hook. This hook will get the field's value
|
||||
* from the Form the field is in, using the field's path.
|
||||
*
|
||||
* Thus, if you would like to set the value of a field you render here, you must do so in the Form that contains the field, or in the
|
||||
* Field component itself.
|
||||
*
|
||||
* All this component does is render the field's Field Components, and pass them the props they need to function.
|
||||
**/
|
||||
const RenderFields: React.FC<Props> = (props) => {
|
||||
const { className, fieldTypes, forceRender, margins } = props
|
||||
|
||||
|
||||
@@ -6,21 +6,23 @@ import type { ReducedField } from './filterFields'
|
||||
export type Props = {
|
||||
className?: string
|
||||
fieldTypes: FieldTypes
|
||||
margins?: 'small' | false
|
||||
forceRender?: boolean
|
||||
} & (
|
||||
| {
|
||||
fieldSchema: FieldWithPath[]
|
||||
filter?: (field: Field) => boolean
|
||||
indexPath?: string
|
||||
margins?: 'small' | false
|
||||
permissions?:
|
||||
| {
|
||||
[field: string]: FieldPermissions
|
||||
}
|
||||
| FieldPermissions
|
||||
readOnly?: boolean
|
||||
} & (
|
||||
| {
|
||||
// Fields to be filtered by the component
|
||||
fieldSchema: FieldWithPath[]
|
||||
filter?: (field: Field) => boolean
|
||||
indexPath?: string
|
||||
}
|
||||
| {
|
||||
// Pre-filtered fields to be simply rendered
|
||||
fields: ReducedField[]
|
||||
}
|
||||
)
|
||||
|
||||
@@ -91,7 +91,7 @@ const ArrayFieldType: React.FC<Props> = (props) => {
|
||||
showError,
|
||||
valid,
|
||||
value,
|
||||
} = useField<[]>({
|
||||
} = useField<number>({
|
||||
condition,
|
||||
hasRows: true,
|
||||
path,
|
||||
@@ -123,8 +123,8 @@ const ArrayFieldType: React.FC<Props> = (props) => {
|
||||
)
|
||||
|
||||
const removeRow = useCallback(
|
||||
async (rowIndex: number) => {
|
||||
await removeFieldRow({ path, rowIndex })
|
||||
(rowIndex: number) => {
|
||||
removeFieldRow({ path, rowIndex })
|
||||
setModified(true)
|
||||
},
|
||||
[removeFieldRow, path, setModified],
|
||||
@@ -278,7 +278,7 @@ const ArrayFieldType: React.FC<Props> = (props) => {
|
||||
icon="plus"
|
||||
iconPosition="left"
|
||||
iconStyle="with-border"
|
||||
onClick={() => addRow(value?.length || 0)}
|
||||
onClick={() => addRow(value || 0)}
|
||||
>
|
||||
{t('addLabel', { label: getTranslation(labels.singular, i18n) })}
|
||||
</Button>
|
||||
|
||||
@@ -90,7 +90,7 @@ const BlocksField: React.FC<Props> = (props) => {
|
||||
showError,
|
||||
valid,
|
||||
value,
|
||||
} = useField<[]>({
|
||||
} = useField<number>({
|
||||
condition,
|
||||
hasRows: true,
|
||||
path,
|
||||
@@ -128,8 +128,8 @@ const BlocksField: React.FC<Props> = (props) => {
|
||||
)
|
||||
|
||||
const removeRow = useCallback(
|
||||
async (rowIndex: number) => {
|
||||
await removeFieldRow({ path, rowIndex })
|
||||
(rowIndex: number) => {
|
||||
removeFieldRow({ path, rowIndex })
|
||||
setModified(true)
|
||||
},
|
||||
[path, removeFieldRow, setModified],
|
||||
@@ -297,7 +297,7 @@ const BlocksField: React.FC<Props> = (props) => {
|
||||
</DrawerToggler>
|
||||
<BlocksDrawer
|
||||
addRow={addRow}
|
||||
addRowIndex={value?.length || 0}
|
||||
addRowIndex={value || 0}
|
||||
blocks={blocks}
|
||||
drawerSlug={drawerSlug}
|
||||
labels={labels}
|
||||
|
||||
@@ -29,7 +29,7 @@ const useField = <T,>(options: Options): FieldType<T> => {
|
||||
const dispatchField = useFormFields(([_, dispatch]) => dispatch)
|
||||
const config = useConfig()
|
||||
|
||||
const { getData, getSiblingData, setModified } = useForm()
|
||||
const { getData, getDataByPath, getSiblingData, setModified } = useForm()
|
||||
|
||||
const value = field?.value as T
|
||||
const initialValue = field?.initialValue as T
|
||||
@@ -116,8 +116,14 @@ const useField = <T,>(options: Options): FieldType<T> => {
|
||||
user,
|
||||
}
|
||||
|
||||
let valueToValidate = value
|
||||
|
||||
if (field?.rows && Array.isArray(field.rows)) {
|
||||
valueToValidate = getDataByPath(path)
|
||||
}
|
||||
|
||||
const validationResult =
|
||||
typeof validate === 'function' ? await validate(value, validateOptions) : true
|
||||
typeof validate === 'function' ? await validate(valueToValidate, validateOptions) : true
|
||||
|
||||
if (typeof validationResult === 'string') {
|
||||
action.errorMessage = validationResult
|
||||
@@ -132,7 +138,7 @@ const useField = <T,>(options: Options): FieldType<T> => {
|
||||
}
|
||||
}
|
||||
|
||||
validateField()
|
||||
void validateField()
|
||||
},
|
||||
150,
|
||||
[
|
||||
@@ -142,6 +148,7 @@ const useField = <T,>(options: Options): FieldType<T> => {
|
||||
dispatchField,
|
||||
getData,
|
||||
getSiblingData,
|
||||
getDataByPath,
|
||||
id,
|
||||
operation,
|
||||
path,
|
||||
|
||||
@@ -27,7 +27,8 @@
|
||||
}
|
||||
|
||||
&__fields {
|
||||
& > .tabs-field {
|
||||
& > .tabs-field,
|
||||
& > .group-field {
|
||||
margin-right: calc(var(--base) * -2);
|
||||
}
|
||||
}
|
||||
@@ -51,7 +52,7 @@
|
||||
position: sticky;
|
||||
top: var(--doc-controls-height);
|
||||
width: 33.33%;
|
||||
height: 100%;
|
||||
height: calc(100vh - var(--doc-controls-height));
|
||||
}
|
||||
|
||||
&__sidebar {
|
||||
@@ -110,7 +111,8 @@
|
||||
}
|
||||
|
||||
&__fields {
|
||||
& > .tabs-field {
|
||||
& > .tabs-field,
|
||||
& > .group-field {
|
||||
margin-right: calc(var(--gutter-h) * -1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,9 +90,8 @@ export const DefaultGlobalEdit: React.FC<GlobalEditViewProps> = (props) => {
|
||||
<div className={`${baseClass}__sidebar-sticky-wrap`}>
|
||||
<div className={`${baseClass}__sidebar-fields`}>
|
||||
<RenderFields
|
||||
fieldSchema={fields}
|
||||
fieldTypes={fieldTypes}
|
||||
filter={(field) => field.admin.position === 'sidebar'}
|
||||
fields={sidebarFields}
|
||||
permissions={permissions.fields}
|
||||
readOnly={!hasSavePermission}
|
||||
/>
|
||||
|
||||
@@ -21,4 +21,11 @@
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__inputWrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: base(1);
|
||||
margin-bottom: base(0.25);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,6 +44,8 @@ const Login: React.FC = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const prefillForm = autoLogin && autoLogin.prefillOnly
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{user ? (
|
||||
@@ -75,22 +77,33 @@ const Login: React.FC = () => {
|
||||
action={`${serverURL}${api}/${userSlug}/login`}
|
||||
className={`${baseClass}__form`}
|
||||
disableSuccessStatus
|
||||
initialData={{
|
||||
email: autoLogin && autoLogin.prefillOnly ? autoLogin.email : undefined,
|
||||
password: autoLogin && autoLogin.prefillOnly ? autoLogin.password : undefined,
|
||||
}}
|
||||
initialData={
|
||||
prefillForm
|
||||
? {
|
||||
email: autoLogin.email,
|
||||
password: autoLogin.password,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
method="post"
|
||||
onSuccess={onSuccess}
|
||||
waitForAutocomplete
|
||||
>
|
||||
<FormLoadingOverlayToggle action="loading" name="login-form" />
|
||||
<div className={`${baseClass}__inputWrap`}>
|
||||
<Email
|
||||
admin={{ autoComplete: 'email' }}
|
||||
label={t('general:email')}
|
||||
name="email"
|
||||
required
|
||||
/>
|
||||
<Password autoComplete="off" label={t('general:password')} name="password" required />
|
||||
<Password
|
||||
autoComplete="off"
|
||||
label={t('general:password')}
|
||||
name="password"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<Link to={`${admin}/forgot`}>{t('forgotPasswordQuestion')}</Link>
|
||||
<FormSubmit>{t('login')}</FormSubmit>
|
||||
</Form>
|
||||
|
||||
@@ -27,7 +27,8 @@
|
||||
}
|
||||
|
||||
&__fields {
|
||||
& > .tabs-field {
|
||||
& > .tabs-field,
|
||||
& > .group-field {
|
||||
margin-right: calc(var(--base) * -2);
|
||||
}
|
||||
}
|
||||
@@ -55,7 +56,7 @@
|
||||
position: sticky;
|
||||
top: var(--doc-controls-height);
|
||||
width: 33.33%;
|
||||
height: 100%;
|
||||
height: calc(100vh - var(--doc-controls-height));
|
||||
}
|
||||
|
||||
&__sidebar {
|
||||
@@ -106,7 +107,8 @@
|
||||
}
|
||||
|
||||
&__fields {
|
||||
& > .tabs-field {
|
||||
& > .tabs-field,
|
||||
& > .group-field {
|
||||
margin-right: calc(var(--gutter-h) * -1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,7 +115,12 @@ export const DefaultCollectionEdit: React.FC<CollectionEditViewProps> = (props)
|
||||
<div className={`${baseClass}__sidebar`}>
|
||||
<div className={`${baseClass}__sidebar-sticky-wrap`}>
|
||||
<div className={`${baseClass}__sidebar-fields`}>
|
||||
<RenderFields fieldTypes={fieldTypes} fields={sidebarFields} />
|
||||
<RenderFields
|
||||
fieldTypes={fieldTypes}
|
||||
fields={sidebarFields}
|
||||
permissions={permissions.fields}
|
||||
readOnly={!hasSavePermission}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -18,16 +18,18 @@ export {
|
||||
*/
|
||||
useWatchForm,
|
||||
} from '../../admin/components/forms/Form/context'
|
||||
|
||||
export { createNestedFieldPath } from '../../admin/components/forms/Form/createNestedFieldPath'
|
||||
|
||||
export { default as getSiblingData } from '../../admin/components/forms/Form/getSiblingData'
|
||||
|
||||
export { default as reduceFieldsToValues } from '../../admin/components/forms/Form/reduceFieldsToValues'
|
||||
|
||||
export { default as Label } from '../../admin/components/forms/Label'
|
||||
export { default as RenderFields } from '../../admin/components/forms/RenderFields'
|
||||
|
||||
export { default as RenderFields } from '../../admin/components/forms/RenderFields'
|
||||
export { default as Submit } from '../../admin/components/forms/Submit'
|
||||
|
||||
export { default as FormSubmit } from '../../admin/components/forms/Submit'
|
||||
export { fieldTypes } from '../../admin/components/forms/field-types'
|
||||
export { default as Checkbox } from '../../admin/components/forms/field-types/Checkbox'
|
||||
|
||||
export { default as Collapsible } from '../../admin/components/forms/field-types/Collapsible'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export { buildConfig } from '../config/build'
|
||||
export * from '../config/types'
|
||||
|
||||
export { type FieldTypes, fieldTypes } from '../admin/components/forms/field-types'
|
||||
export { type FieldTypes } from '../admin/components/forms/field-types'
|
||||
export { defaults } from '../config/defaults'
|
||||
export { sanitizeConfig } from '../config/sanitize'
|
||||
export { baseBlockFields } from '../fields/baseFields/baseBlockFields'
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
export { withMergedProps } from '../admin/components/utilities/WithMergedProps'
|
||||
export { extractTranslations } from '../translations/extractTranslations'
|
||||
export { i18nInit } from '../translations/init'
|
||||
|
||||
export { i18nInit } from '../translations/init'
|
||||
export { combineMerge } from '../utilities/combineMerge'
|
||||
export { configToJSONSchema, entityToJSONSchema } from '../utilities/configToJSONSchema'
|
||||
export { createArrayFromCommaDelineated } from '../utilities/createArrayFromCommaDelineated'
|
||||
|
||||
export { createArrayFromCommaDelineated } from '../utilities/createArrayFromCommaDelineated'
|
||||
export { deepCopyObject } from '../utilities/deepCopyObject'
|
||||
export { deepMerge } from '../utilities/deepMerge'
|
||||
export { default as flattenTopLevelFields } from '../utilities/flattenTopLevelFields'
|
||||
|
||||
@@ -382,7 +382,7 @@ export const relationship: Validate<unknown, unknown, RelationshipField> = async
|
||||
})
|
||||
|
||||
if (invalidRelationships.length > 0) {
|
||||
return `This field has the following invalid selections: ${invalidRelationships
|
||||
return `This relationship field has the following invalid relationships: ${invalidRelationships
|
||||
.map((err, invalid) => {
|
||||
return `${err} ${JSON.stringify(invalid)}`
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/richtext-lexical",
|
||||
"version": "0.1.5",
|
||||
"version": "0.1.11",
|
||||
"description": "The officially supported Lexical richtext adapter for Payload",
|
||||
"repository": "https://github.com/payloadcms/payload",
|
||||
"license": "MIT",
|
||||
@@ -55,6 +55,9 @@
|
||||
"@types/react": "18.2.15",
|
||||
"payload": "workspace:*"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"payload": "^2.0.6"
|
||||
},
|
||||
"exports": {
|
||||
".": {
|
||||
"default": "./src/index.ts",
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
'use client'
|
||||
import type { SerializedEditorState } from 'lexical'
|
||||
import type { CellComponentProps, RichTextField } from 'payload/types'
|
||||
|
||||
@@ -16,17 +17,38 @@ export const RichTextCell: React.FC<
|
||||
const [preview, setPreview] = React.useState('Loading...')
|
||||
|
||||
useEffect(() => {
|
||||
if (data == null) {
|
||||
let dataToUse = data
|
||||
if (dataToUse == null) {
|
||||
setPreview('')
|
||||
return
|
||||
}
|
||||
|
||||
// Transform data through load hooks
|
||||
if (editorConfig?.features?.hooks?.load?.length) {
|
||||
editorConfig.features.hooks.load.forEach((hook) => {
|
||||
dataToUse = hook({ incomingEditorState: dataToUse })
|
||||
})
|
||||
}
|
||||
|
||||
// If data is from Slate and not Lexical
|
||||
if (dataToUse && Array.isArray(dataToUse) && !('root' in dataToUse)) {
|
||||
setPreview('')
|
||||
return
|
||||
}
|
||||
|
||||
// If data is from payload-plugin-lexical
|
||||
if (dataToUse && 'jsonContent' in dataToUse) {
|
||||
setPreview('')
|
||||
return
|
||||
}
|
||||
|
||||
// initialize headless editor
|
||||
const headlessEditor = createHeadlessEditor({
|
||||
namespace: editorConfig.lexical.namespace,
|
||||
nodes: getEnabledNodes({ editorConfig }),
|
||||
theme: editorConfig.lexical.theme,
|
||||
})
|
||||
headlessEditor.setEditorState(headlessEditor.parseEditorState(data))
|
||||
headlessEditor.setEditorState(headlessEditor.parseEditorState(dataToUse))
|
||||
|
||||
const textContent =
|
||||
headlessEditor.getEditorState().read(() => {
|
||||
|
||||
@@ -47,7 +47,7 @@ const RichText: React.FC<FieldProps> = (props) => {
|
||||
validate: memoizedValidate,
|
||||
})
|
||||
|
||||
const { errorMessage, initialValue, setValue, showError, value } = fieldType
|
||||
const { errorMessage, setValue, showError, value } = fieldType
|
||||
|
||||
let valueToUse = value
|
||||
|
||||
@@ -87,14 +87,19 @@ const RichText: React.FC<FieldProps> = (props) => {
|
||||
<LexicalProvider
|
||||
editorConfig={editorConfig}
|
||||
fieldProps={props}
|
||||
initialState={initialValue}
|
||||
onChange={(editorState, editor, tags) => {
|
||||
const json = editorState.toJSON()
|
||||
let serializedEditorState = editorState.toJSON()
|
||||
|
||||
setValue(json)
|
||||
// Transform state through save hooks
|
||||
if (editorConfig?.features?.hooks?.save?.length) {
|
||||
editorConfig.features.hooks.save.forEach((hook) => {
|
||||
serializedEditorState = hook({ incomingEditorState: serializedEditorState })
|
||||
})
|
||||
}
|
||||
|
||||
setValue(serializedEditorState)
|
||||
}}
|
||||
readOnly={readOnly}
|
||||
setValue={setValue}
|
||||
value={value}
|
||||
/>
|
||||
<FieldDescription description={description} value={value} />
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { Block } from 'payload/types'
|
||||
|
||||
import { sanitizeFields } from 'payload/config'
|
||||
|
||||
import type { BlocksFeatureProps } from '.'
|
||||
@@ -20,39 +22,41 @@ export const blockAfterReadPromiseHOC = (
|
||||
showHiddenFields,
|
||||
siblingDoc,
|
||||
}) => {
|
||||
const blocks: Block[] = props.blocks
|
||||
const blockFieldData = node.fields.data
|
||||
|
||||
const promises: Promise<void>[] = []
|
||||
|
||||
// Sanitize block's fields here. This is done here and not in the feature, because the payload config is available here
|
||||
const payloadConfig = req.payload.config
|
||||
const validRelationships = payloadConfig.collections.map((c) => c.slug) || []
|
||||
props.blocks = props.blocks.map((block) => {
|
||||
const unsanitizedBlock = { ...block }
|
||||
unsanitizedBlock.fields = sanitizeFields({
|
||||
blocks.forEach((block) => {
|
||||
block.fields = sanitizeFields({
|
||||
config: payloadConfig,
|
||||
fields: block.fields,
|
||||
validRelationships,
|
||||
})
|
||||
return unsanitizedBlock
|
||||
})
|
||||
|
||||
if (Array.isArray(props.blocks)) {
|
||||
props.blocks.forEach((block) => {
|
||||
if (block?.fields) {
|
||||
// find block used in this node
|
||||
const block = props.blocks.find((block) => block.slug === blockFieldData.blockType)
|
||||
if (!block || !block?.fields?.length || !blockFieldData) {
|
||||
return promises
|
||||
}
|
||||
|
||||
recurseNestedFields({
|
||||
afterReadPromises,
|
||||
currentDepth,
|
||||
data: node.fields.data || {},
|
||||
data: blockFieldData,
|
||||
depth,
|
||||
fields: block.fields,
|
||||
overrideAccess,
|
||||
promises,
|
||||
req,
|
||||
showHiddenFields,
|
||||
siblingDoc,
|
||||
// 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
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import { SectionTitle } from 'payload/components/fields/Blocks'
|
||||
import { RenderFields, createNestedFieldPath, useFormSubmitted } from 'payload/components/forms'
|
||||
import { useDocumentInfo } from 'payload/components/utilities'
|
||||
import { getTranslation } from 'payload/utilities'
|
||||
import React, { useCallback, useEffect } from 'react'
|
||||
import React, { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import type { FieldProps } from '../../../../types'
|
||||
@@ -24,6 +24,11 @@ type Props = {
|
||||
nodeKey: string
|
||||
}
|
||||
|
||||
/**
|
||||
* The actual content of the Block. This should be INSIDE a Form component,
|
||||
* scoped to the block. All format operations in here are thus scoped to the block's form, and
|
||||
* not the whole document.
|
||||
*/
|
||||
export const BlockContent: React.FC<Props> = (props) => {
|
||||
const { baseClass, block, field, fields, nodeKey } = props
|
||||
const { i18n } = useTranslation()
|
||||
|
||||
@@ -1,14 +1,20 @@
|
||||
import { type ElementFormatType } from 'lexical'
|
||||
import { Form, buildInitialState, useFormSubmitted } from 'payload/components/forms'
|
||||
import React, { useMemo } from 'react'
|
||||
import React, { useEffect, useMemo } from 'react'
|
||||
|
||||
import { type BlockFields } from '../nodes/BlocksNode'
|
||||
const baseClass = 'lexical-block'
|
||||
|
||||
import type { Data } from 'payload/types'
|
||||
|
||||
import { useConfig } from 'payload/components/utilities'
|
||||
import {
|
||||
buildStateFromSchema,
|
||||
useConfig,
|
||||
useDocumentInfo,
|
||||
useLocale,
|
||||
} from 'payload/components/utilities'
|
||||
import { sanitizeFields } from 'payload/config'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import type { BlocksFeatureProps } from '..'
|
||||
|
||||
@@ -43,13 +49,49 @@ export const BlockComponent: React.FC<Props> = (props) => {
|
||||
validRelationships,
|
||||
})
|
||||
|
||||
const initialDataRef = React.useRef<Data>(buildInitialState(fields.data || {})) // Store initial value in a ref, so it doesn't change on re-render and only gets initialized once
|
||||
const initialStateRef = React.useRef<Data>(buildInitialState(fields.data || {})) // Store initial value in a ref, so it doesn't change on re-render and only gets initialized once
|
||||
|
||||
const config = useConfig()
|
||||
const { t } = useTranslation('general')
|
||||
const { code: locale } = useLocale()
|
||||
const { getDocPreferences } = useDocumentInfo()
|
||||
|
||||
// initialState State
|
||||
|
||||
const [initialState, setInitialState] = React.useState<Data>(null)
|
||||
|
||||
useEffect(() => {
|
||||
async function buildInitialState() {
|
||||
const preferences = await getDocPreferences()
|
||||
|
||||
const stateFromSchema = await buildStateFromSchema({
|
||||
config,
|
||||
data: fields.data,
|
||||
fieldSchema: block.fields,
|
||||
locale,
|
||||
operation: 'update',
|
||||
preferences,
|
||||
t,
|
||||
})
|
||||
|
||||
// We have to merge the output of buildInitialState (above this useEffect) with the output of buildStateFromSchema.
|
||||
// That's because the output of buildInitialState provides important properties necessary for THIS block,
|
||||
// like blockName, blockType and id, while buildStateFromSchema provides the correct output of this block's data,
|
||||
// e.g. if this block has a sub-block (like the `rows` property)
|
||||
setInitialState({
|
||||
...initialStateRef?.current,
|
||||
...stateFromSchema,
|
||||
})
|
||||
}
|
||||
void buildInitialState()
|
||||
}, [setInitialState, config, block, locale, getDocPreferences, t]) // do not add fields here, it causes an endless loop
|
||||
|
||||
// Memoized Form JSX
|
||||
const formContent = useMemo(() => {
|
||||
return (
|
||||
block && (
|
||||
<Form initialState={initialDataRef?.current} submitted={submitted}>
|
||||
block &&
|
||||
initialState && (
|
||||
<Form fields={block.fields} initialState={initialState} submitted={submitted}>
|
||||
<BlockContent
|
||||
baseClass={baseClass}
|
||||
block={block}
|
||||
@@ -60,7 +102,7 @@ export const BlockComponent: React.FC<Props> = (props) => {
|
||||
</Form>
|
||||
)
|
||||
)
|
||||
}, [block, field, nodeKey, submitted])
|
||||
}, [block, field, nodeKey, submitted, initialState])
|
||||
|
||||
return <div className={baseClass}>{formContent}</div>
|
||||
}
|
||||
|
||||
@@ -15,12 +15,12 @@ export const blockValidationHOC = (
|
||||
payloadConfig,
|
||||
validation,
|
||||
}) => {
|
||||
const blockFieldValues = node.fields.data
|
||||
|
||||
const blockFieldData = node.fields.data
|
||||
const blocks: Block[] = props.blocks
|
||||
|
||||
// Sanitize block's fields here. This is done here and not in the feature, because the payload config is available here
|
||||
blocks.forEach((block) => {
|
||||
const validRelationships = payloadConfig.collections.map((c) => c.slug) || []
|
||||
blocks.forEach((block) => {
|
||||
block.fields = sanitizeFields({
|
||||
config: payloadConfig,
|
||||
fields: block.fields,
|
||||
@@ -29,7 +29,7 @@ export const blockValidationHOC = (
|
||||
})
|
||||
|
||||
// find block
|
||||
const block = props.blocks.find((block) => block.slug === blockFieldValues.blockType)
|
||||
const block = props.blocks.find((block) => block.slug === blockFieldData.blockType)
|
||||
|
||||
// validate block
|
||||
if (!block) {
|
||||
|
||||
@@ -52,7 +52,7 @@ export const linkAfterReadPromiseHOC = (
|
||||
promises,
|
||||
req,
|
||||
showHiddenFields,
|
||||
siblingDoc,
|
||||
siblingDoc: node.fields || {},
|
||||
})
|
||||
}
|
||||
return promises
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Drawer } from 'payload/components/elements'
|
||||
import { Form } from 'payload/components/forms'
|
||||
import { RenderFields } from 'payload/components/forms'
|
||||
import { FormSubmit } from 'payload/components/forms'
|
||||
import { fieldTypes } from 'payload/config'
|
||||
import { fieldTypes } from 'payload/components/forms'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import type { Field } from 'payload/types'
|
||||
import LexicalClickableLinkPlugin from '@lexical/react/LexicalClickableLinkPlugin'
|
||||
import { $findMatchingParent } from '@lexical/utils'
|
||||
import { $getSelection, $isRangeSelection } from 'lexical'
|
||||
import { withMergedProps } from 'payload/components/utilities'
|
||||
import { withMergedProps } from 'payload/utilities'
|
||||
|
||||
import type { FeatureProvider } from '../types'
|
||||
import type { LinkFields } from './nodes/LinkNode'
|
||||
|
||||
@@ -51,7 +51,7 @@ export const uploadAfterReadPromiseHOC = (
|
||||
promises,
|
||||
req,
|
||||
showHiddenFields,
|
||||
siblingDoc,
|
||||
siblingDoc: node.fields || {},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useModal } from '@faceless-ui/modal'
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||
import { $getNodeByKey } from 'lexical'
|
||||
import { Drawer } from 'payload/components/elements'
|
||||
import { Form, FormSubmit, RenderFields } from 'payload/components/forms'
|
||||
import { Form, FormSubmit, RenderFields, fieldTypes } from 'payload/components/forms'
|
||||
import {
|
||||
buildStateFromSchema,
|
||||
useAuth,
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
useDocumentInfo,
|
||||
useLocale,
|
||||
} from 'payload/components/utilities'
|
||||
import { fieldTypes, sanitizeFields } from 'payload/config'
|
||||
import { sanitizeFields } from 'payload/config'
|
||||
import { deepCopyObject, getTranslation } from 'payload/utilities'
|
||||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
@@ -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'],
|
||||
}
|
||||
@@ -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'],
|
||||
}
|
||||
@@ -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'],
|
||||
}
|
||||
@@ -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'],
|
||||
}
|
||||
@@ -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'],
|
||||
}
|
||||
@@ -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'],
|
||||
}
|
||||
@@ -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'],
|
||||
}
|
||||
@@ -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,
|
||||
]
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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',
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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'],
|
||||
}
|
||||
@@ -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'],
|
||||
}
|
||||
@@ -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'],
|
||||
}
|
||||
@@ -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'],
|
||||
}
|
||||
@@ -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'],
|
||||
}
|
||||
@@ -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'],
|
||||
}
|
||||
@@ -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'],
|
||||
}
|
||||
@@ -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'],
|
||||
}
|
||||
@@ -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'],
|
||||
}
|
||||
@@ -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,
|
||||
]
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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',
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -51,6 +51,18 @@ export type Feature = {
|
||||
floatingSelectToolbar?: {
|
||||
sections: FloatingToolbarSection[]
|
||||
}
|
||||
hooks?: {
|
||||
load?: ({
|
||||
incomingEditorState,
|
||||
}: {
|
||||
incomingEditorState: SerializedEditorState
|
||||
}) => SerializedEditorState
|
||||
save?: ({
|
||||
incomingEditorState,
|
||||
}: {
|
||||
incomingEditorState: SerializedEditorState
|
||||
}) => SerializedEditorState
|
||||
}
|
||||
markdownTransformers?: Transformer[]
|
||||
nodes?: Array<{
|
||||
afterReadPromises?: Array<AfterReadPromise>
|
||||
@@ -123,6 +135,22 @@ export type SanitizedFeatures = Required<
|
||||
floatingSelectToolbar: {
|
||||
sections: FloatingToolbarSection[]
|
||||
}
|
||||
hooks: {
|
||||
load: Array<
|
||||
({
|
||||
incomingEditorState,
|
||||
}: {
|
||||
incomingEditorState: SerializedEditorState
|
||||
}) => SerializedEditorState
|
||||
>
|
||||
save: Array<
|
||||
({
|
||||
incomingEditorState,
|
||||
}: {
|
||||
incomingEditorState: SerializedEditorState
|
||||
}) => SerializedEditorState
|
||||
>
|
||||
}
|
||||
plugins?: Array<
|
||||
| {
|
||||
// plugins are anything which is not directly part of the editor. Like, creating a command which creates a node, or opens a modal, or some other more "outside" functionality
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
'use client'
|
||||
import { ShimmerEffect } from 'payload/components'
|
||||
import React, { Suspense, lazy } from 'react'
|
||||
|
||||
|
||||
@@ -17,7 +17,9 @@ import { AddBlockHandlePlugin } from './plugins/handles/AddBlockHandlePlugin'
|
||||
import { DraggableBlockPlugin } from './plugins/handles/DraggableBlockPlugin'
|
||||
import { LexicalContentEditable } from './ui/ContentEditable'
|
||||
|
||||
export const LexicalEditor: React.FC<LexicalProviderProps> = (props) => {
|
||||
export const LexicalEditor: React.FC<Pick<LexicalProviderProps, 'editorConfig' | 'onChange'>> = (
|
||||
props,
|
||||
) => {
|
||||
const { editorConfig, onChange } = props
|
||||
const [editor] = useLexicalComposerContext()
|
||||
|
||||
|
||||
@@ -14,19 +14,21 @@ import { getEnabledNodes } from './nodes'
|
||||
export type LexicalProviderProps = {
|
||||
editorConfig: SanitizedEditorConfig
|
||||
fieldProps: FieldProps
|
||||
initialState: SerializedEditorState
|
||||
onChange: (editorState: EditorState, editor: LexicalEditor, tags: Set<string>) => void
|
||||
readOnly: boolean
|
||||
setValue: (value: SerializedEditorState) => void
|
||||
value: SerializedEditorState
|
||||
}
|
||||
export const LexicalProvider: React.FC<LexicalProviderProps> = (props) => {
|
||||
const { editorConfig, fieldProps, initialState, onChange, readOnly, setValue, value } = props
|
||||
const { editorConfig, fieldProps, onChange, readOnly } = props
|
||||
let { value } = props
|
||||
|
||||
if (
|
||||
(value && Array.isArray(value) && !('root' in value)) ||
|
||||
(initialState && Array.isArray(initialState) && !('root' in initialState))
|
||||
) {
|
||||
if (editorConfig?.features?.hooks?.load?.length) {
|
||||
editorConfig.features.hooks.load.forEach((hook) => {
|
||||
value = hook({ incomingEditorState: value })
|
||||
})
|
||||
}
|
||||
|
||||
if (value && Array.isArray(value) && !('root' in value)) {
|
||||
throw new Error(
|
||||
'You have tried to pass in data from the old, Slate editor, to the new, Lexical editor. This is not supported. There is no automatic conversion from Slate to Lexical data available yet (coming soon). Please remove the data from the field and start again.',
|
||||
)
|
||||
@@ -40,7 +42,7 @@ export const LexicalProvider: React.FC<LexicalProviderProps> = (props) => {
|
||||
|
||||
const initialConfig: InitialConfigType = {
|
||||
editable: readOnly === true ? false : true,
|
||||
editorState: initialState != null ? JSON.stringify(initialState) : undefined,
|
||||
editorState: value != null ? JSON.stringify(value) : undefined,
|
||||
namespace: editorConfig.lexical.namespace,
|
||||
nodes: [...getEnabledNodes({ editorConfig })],
|
||||
onError: (error: Error) => {
|
||||
@@ -53,15 +55,7 @@ export const LexicalProvider: React.FC<LexicalProviderProps> = (props) => {
|
||||
<LexicalComposer initialConfig={initialConfig}>
|
||||
<EditorConfigProvider editorConfig={editorConfig} fieldProps={fieldProps}>
|
||||
<div className="editor-shell">
|
||||
<LexicalEditorComponent
|
||||
editorConfig={editorConfig}
|
||||
fieldProps={fieldProps}
|
||||
initialState={initialState}
|
||||
onChange={onChange}
|
||||
readOnly={readOnly}
|
||||
setValue={setValue}
|
||||
value={value}
|
||||
/>
|
||||
<LexicalEditorComponent editorConfig={editorConfig} onChange={onChange} />
|
||||
</div>
|
||||
</EditorConfigProvider>
|
||||
</LexicalComposer>
|
||||
|
||||
@@ -10,6 +10,10 @@ export const sanitizeFeatures = (features: ResolvedFeatureMap): SanitizedFeature
|
||||
floatingSelectToolbar: {
|
||||
sections: [],
|
||||
},
|
||||
hooks: {
|
||||
load: [],
|
||||
save: [],
|
||||
},
|
||||
markdownTransformers: [],
|
||||
nodes: [],
|
||||
plugins: [],
|
||||
@@ -21,6 +25,15 @@ export const sanitizeFeatures = (features: ResolvedFeatureMap): SanitizedFeature
|
||||
}
|
||||
|
||||
features.forEach((feature) => {
|
||||
if (feature.hooks) {
|
||||
if (feature.hooks?.load?.length) {
|
||||
sanitized.hooks.load = sanitized.hooks.load.concat(feature.hooks.load)
|
||||
}
|
||||
if (feature.hooks?.save?.length) {
|
||||
sanitized.hooks.save = sanitized.hooks.save.concat(feature.hooks.save)
|
||||
}
|
||||
}
|
||||
|
||||
if (feature.nodes?.length) {
|
||||
sanitized.nodes = sanitized.nodes.concat(feature.nodes)
|
||||
feature.nodes.forEach((node) => {
|
||||
|
||||
124
packages/richtext-lexical/src/field/lexical/utils/nodeFormat.ts
Normal file
124
packages/richtext-lexical/src/field/lexical/utils/nodeFormat.ts
Normal 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',
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import type { SerializedEditorState } from 'lexical'
|
||||
import type { EditorConfig as LexicalEditorConfig } from 'lexical/LexicalEditor'
|
||||
import type { RichTextAdapter } from 'payload/types'
|
||||
|
||||
import { withMergedProps } from 'payload/components/utilities'
|
||||
import { withMergedProps } from 'payload/utilities'
|
||||
|
||||
import type { FeatureProvider } from './field/features/types'
|
||||
import type { EditorConfig, SanitizedEditorConfig } from './field/lexical/config/types'
|
||||
@@ -153,6 +153,9 @@ export { IndentFeature } from './field/features/indent'
|
||||
export { CheckListFeature } from './field/features/lists/CheckList'
|
||||
export { OrderedListFeature } from './field/features/lists/OrderedList'
|
||||
export { UnoderedListFeature } from './field/features/lists/UnorderedList'
|
||||
export { LexicalPluginToLexicalFeature } from './field/features/migrations/LexicalPluginToLexical'
|
||||
export { SlateToLexicalFeature } from './field/features/migrations/SlateToLexical'
|
||||
|
||||
export type {
|
||||
AfterReadPromise,
|
||||
Feature,
|
||||
@@ -201,6 +204,20 @@ export { isHTMLElement } from './field/lexical/utils/guard'
|
||||
export { invariant } from './field/lexical/utils/invariant'
|
||||
export { joinClasses } from './field/lexical/utils/joinClasses'
|
||||
export { createBlockNode } from './field/lexical/utils/markdown/createBlockNode'
|
||||
export {
|
||||
DETAIL_TYPE_TO_DETAIL,
|
||||
DOUBLE_LINE_BREAK,
|
||||
ELEMENT_FORMAT_TO_TYPE,
|
||||
ELEMENT_TYPE_TO_FORMAT,
|
||||
IS_ALL_FORMATTING,
|
||||
LTR_REGEX,
|
||||
NON_BREAKING_SPACE,
|
||||
NodeFormat,
|
||||
RTL_REGEX,
|
||||
TEXT_MODE_TO_TYPE,
|
||||
TEXT_TYPE_TO_FORMAT,
|
||||
TEXT_TYPE_TO_MODE,
|
||||
} from './field/lexical/utils/nodeFormat'
|
||||
export { Point, isPoint } from './field/lexical/utils/point'
|
||||
export { Rect } from './field/lexical/utils/rect'
|
||||
export { setFloatingElemPosition } from './field/lexical/utils/setFloatingElemPosition'
|
||||
|
||||
@@ -173,7 +173,7 @@ export const recurseNestedFields = ({
|
||||
promises,
|
||||
req,
|
||||
showHiddenFields,
|
||||
siblingDoc,
|
||||
siblingDoc: data[field.name][i], // This has to be scoped to the blocks's fields, otherwise there may be population issues, e.g. for a relationship field with Blocks Node, with a Blocks Field, with a RichText Field, With Relationship Node. The last richtext field would try to find itself using siblingDoc[field.nane], which only works if the siblingDoc is scoped to the blocks's fields
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -191,14 +191,13 @@ export const recurseNestedFields = ({
|
||||
promises,
|
||||
req,
|
||||
showHiddenFields,
|
||||
siblingDoc,
|
||||
siblingDoc, // TODO: if there's any population issues, this might have to be data[field.name][i] as well
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (field.type === 'richText') {
|
||||
// TODO: This does not properly work yet. E.g. it does not handle a relationship inside of lexical inside of block inside of lexical
|
||||
const editor: RichTextAdapter = field?.editor
|
||||
|
||||
if (editor?.afterReadPromise) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user