Compare commits
13 Commits
v3.0.0-bet
...
v3.0.0-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4375a33706 | ||
|
|
51056769e5 | ||
|
|
abf6e9aa6b | ||
|
|
5ffc5a1248 | ||
|
|
ed73dedd14 | ||
|
|
6b7ec6cbf2 | ||
|
|
35eb16bbec | ||
|
|
f47d6cb23c | ||
|
|
c34aa86da1 | ||
|
|
ae8a5a9cb8 | ||
|
|
d8d5a44895 | ||
|
|
377a478fc2 | ||
|
|
0b2be54011 |
@@ -12,7 +12,7 @@ All Custom Components in Payload are [React Server Components](https://react.dev
|
||||
|
||||
<Banner type="success">
|
||||
<strong>Note:</strong>
|
||||
Client Components continue to be fully supported. To use Client Components in your app, simply import them into a Server Component and render them. Ensure your Client Component includes the `use client` directive and that any [non-serializable props](https://react.dev/reference/rsc/use-client#serializable-types) are sanitized. [More details](#client-components).
|
||||
Client Components continue to be fully supported. To use Client Components in your app, simply include the `use client` directive. Payload will automatically detect and remove all default [non-serializable props](https://react.dev/reference/rsc/use-client#serializable-types) before rendering your component. [More details](#client-components).
|
||||
</Banner>
|
||||
|
||||
To swap in your own Custom Component, consult the list of available components below. Determine the scope that corresponds to what you are trying to accomplish, then [author your React component(s)](#building-custom-components) accordingly.
|
||||
@@ -26,9 +26,9 @@ There are four main types of Custom Components in Payload:
|
||||
|
||||
## Custom Root Components
|
||||
|
||||
Root Components are those that effect the [Admin Panel](./overview) generally. You can override Root Components through the `admin.components` property of the [Payload Config](../getting-started/overview).
|
||||
Root Components are those that effect the [Admin Panel](./overview) generally, such as the logo. You can override Root Components through the `admin.components` property of the [Payload Config](../getting-started/overview).
|
||||
|
||||
Here is an example showing what it might look like to swap out Root Components for your own Custom Components. See [Building Custom Components](#building-custom-components) for exact details on how to build them:
|
||||
Here is an example showing what it might look like to swap out Root Components for your own. See [Building Custom Components](#building-custom-components) for exact details on how to build them:
|
||||
|
||||
```ts
|
||||
import { buildConfig } from 'payload'
|
||||
@@ -71,7 +71,7 @@ The following options are available:
|
||||
|
||||
### Custom Providers
|
||||
|
||||
You can add additional [React Context](https://react.dev/learn/scaling-up-with-reducer-and-context) to any Payload app through Custom Providers. As you add more and more Custom Components to your [Admin Panel](./overview), this is a great may to share state across all of them.
|
||||
You can add additional [React Context](https://react.dev/learn/scaling-up-with-reducer-and-context) to any Payload app through Custom Providers. As you add more and more Custom Components to your [Admin Panel](./overview), this is a great way to share state across all of them.
|
||||
|
||||
To do this, add `admin.components.providers` to your config:
|
||||
|
||||
@@ -115,9 +115,9 @@ export const useMyCustomContext = () => useContext(MyCustomContext)
|
||||
|
||||
## Custom Collection Components
|
||||
|
||||
Collection Components are those that effect [Collection](../configuration/collections)-specific UI within the [Admin Panel](./overview). You can override Collection Components through the `admin.components` property on any [Collection Config](../configuration/collections).
|
||||
Collection Components are those that effect [Collection](../configuration/collections)-specific UI within the [Admin Panel](./overview), such as the save button. You can override Collection Components through the `admin.components` property on any [Collection Config](../configuration/collections).
|
||||
|
||||
Here is an example showing what it might look like to swap out Collection Components for your own Custom Components. See [Building Custom Components](#building-custom-components) for exact details on how to build them:
|
||||
Here is an example showing what it might look like to swap out Collection Components for your own. See [Building Custom Components](#building-custom-components) for exact details on how to build them:
|
||||
|
||||
```ts
|
||||
import type { SanitizedCollectionConfig } from 'payload/types'
|
||||
@@ -152,9 +152,9 @@ The following options are available:
|
||||
|
||||
## Custom Global Components
|
||||
|
||||
Global Components are those that effect [Global](../configuration/globals)-specific UI within the [Admin Panel](./overview). You can override Global Components through the `admin.components` property on any [Global Config](../configuration/globals).
|
||||
Global Components are those that effect [Global](../configuration/globals)-specific UI within the [Admin Panel](./overview), such as the save button. You can override Global Components through the `admin.components` property on any [Global Config](../configuration/globals).
|
||||
|
||||
Here is an example showing what it might look like to swap out Global Components for your own Custom Components. See [Building Custom Components](#building-custom-components) for exact details on how to build them:
|
||||
Here is an example showing what it might look like to swap out Global Components for your own. See [Building Custom Components](#building-custom-components) for exact details on how to build them:
|
||||
|
||||
```ts
|
||||
import type { SanitizedGlobalConfig } from 'payload/types'
|
||||
@@ -187,7 +187,7 @@ The following options are available:
|
||||
|
||||
All Custom Components in Payload are [React Server Components](https://react.dev/reference/rsc/server-components) by default, with the exception of [Custom Providers](#custom-providers). This enables the use of the [Local API](../local-api) directly in the front-end.
|
||||
|
||||
To make building Custom Components as easy as possible, Payload automatically provides common props, such as the [`payload`](../local-api/overview) class, the [`i18n`](../configuration/i18n) object, etc. This means that when building Custom Components within the Admin Panel, you do not have to get these yourself like you would from an external application.
|
||||
To make building Custom Components as easy as possible, Payload automatically provides common props, such as the [`payload`](../local-api/overview) class and the [`i18n`](../configuration/i18n) object. This means that when building Custom Components within the Admin Panel, you do not have to get these yourself like you would from an external application.
|
||||
|
||||
Here is an example:
|
||||
|
||||
@@ -221,9 +221,9 @@ Custom Components also receive various other props that are specific to the cont
|
||||
See [Root Components](#custom-root-components), [Collection Components](#custom-collection-components), [Global Components](#custom-global-components), or [Field Components](#custom-field-components) for a complete list of all available components.
|
||||
</Banner>
|
||||
|
||||
#### Client Components
|
||||
### Client Components
|
||||
|
||||
When [Building Custom Components](#building-custom-components), it's still possible to use client-side code such as `useState` or the `window` object. To do this, simply define your component in a new file with the `use client` directive at the top:
|
||||
When [Building Custom Components](#building-custom-components), it's still possible to use client-side code such as `useState` or the `window` object. To do this, simply add the `use client` directive at the top of your file. Payload will automatically detect and remove all default [non-serializable props](https://react.dev/reference/rsc/use-client#serializable-types) before rendering your component.
|
||||
|
||||
```tsx
|
||||
'use client' // highlight-line
|
||||
@@ -240,29 +240,57 @@ export const MyClientComponent: React.FC = () => {
|
||||
}
|
||||
```
|
||||
|
||||
Then simply import and render your Client Component within your Server Component:
|
||||
<Banner type="warning">
|
||||
<strong>Reminder:</strong>
|
||||
Client Components cannot be passed [non-serializable props](https://react.dev/reference/rsc/use-client#serializable-types). If you are rendering your Client Component _from within_ a Server Component, ensure that its props are serializable.
|
||||
</Banner>
|
||||
|
||||
### Accessing the Payload Config
|
||||
|
||||
From any Server Component, the [Payload Config](../configuration/overview) can be retrieved using the `payload` prop:
|
||||
|
||||
```tsx
|
||||
import React from 'react'
|
||||
import { MyClientComponent } from './MyClientComponent'
|
||||
|
||||
export default function MyServerComponent() {
|
||||
export default async function MyServerComponent({
|
||||
payload: {
|
||||
config // highlight-line
|
||||
}
|
||||
}) {
|
||||
return (
|
||||
<div>
|
||||
<MyClientComponent />
|
||||
</div>
|
||||
<Link href={config.serverURL}>
|
||||
Go Home
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
<Banner type="warning">
|
||||
<strong>Reminder:</strong>
|
||||
Client Components cannot be passed [non-serializable props](https://react.dev/reference/rsc/use-client#serializable-types). Before rendering your Client Component from a Server Component, ensure that any props passed to it are appropriately sanitized.
|
||||
But the Payload Config is [non-serializable](https://react.dev/reference/rsc/use-client#serializable-types) by design. It is full of custom validation functions, React components, etc. This means that the Payload Config, in its entirety, cannot be passed directly to Client Components.
|
||||
|
||||
For this reason, Payload creates a Client Config and passes it into the Config Provider. This is a serializable version of the Payload Config that can be accessed from any Client Component via the [`useConfig`](./hooks#useconfig) hook:
|
||||
|
||||
```tsx
|
||||
import React from 'react'
|
||||
import { useConfig } from '@payloadcms/ui'
|
||||
|
||||
export const MyClientComponent: React.FC = () => {
|
||||
const { serverURL } = useConfig() // highlight-line
|
||||
|
||||
return (
|
||||
<Link href={serverURL}>
|
||||
Go Home
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
<Banner type="success">
|
||||
See [Using Hooks](#using-hooks) for more details.
|
||||
</Banner>
|
||||
|
||||
#### Using Hooks
|
||||
### Using Hooks
|
||||
|
||||
To make it easier to [build your Custom Components](#building-custom-components), you can use [Payload's built-in React Hooks](./hooks) on the client. For example, you might want to interact with one of Payload's many React Contexts:
|
||||
To make it easier to [build your Custom Components](#building-custom-components), you can use [Payload's built-in React Hooks](./hooks) in any Client Component. For example, you might want to interact with one of Payload's many React Contexts:
|
||||
|
||||
```tsx
|
||||
'use client'
|
||||
@@ -270,7 +298,7 @@ import React from 'react'
|
||||
import { useDocumentInfo } from '@payloadcms/ui'
|
||||
|
||||
export const MyClientComponent: React.FC = () => {
|
||||
const { slug } = useDocumentInfo()
|
||||
const { slug } = useDocumentInfo() // highlight-line
|
||||
|
||||
return (
|
||||
<p>{`Entity slug: ${slug}`}</p>
|
||||
@@ -282,7 +310,7 @@ export const MyClientComponent: React.FC = () => {
|
||||
See the [Hooks](./hooks) documentation for a full list of available hooks.
|
||||
</Banner>
|
||||
|
||||
#### Getting the Current Language
|
||||
### Getting the Current Language
|
||||
|
||||
All Custom Components can support multiple languages to be consistent with Payload's [Internationalization](../configuration/i18n). To do this, first add your translation resources to the [I18n Config](../configuration/i18n).
|
||||
|
||||
@@ -324,9 +352,9 @@ export const MyClientComponent: React.FC = () => {
|
||||
See the [Hooks](./hooks) documentation for a full list of available hooks.
|
||||
</Banner>
|
||||
|
||||
#### Getting the Current Locale
|
||||
### Getting the Current Locale
|
||||
|
||||
All [Custom Views](./views) can support multiple locales to be consistent with Payload's [Localization](../configuration/localization). All Custom Views automatically receive the `locale` object as a prop by default. This can be used to scope API requests, etc.:
|
||||
All [Custom Views](./views) can support multiple locales to be consistent with Payload's [Localization](../configuration/localization). They automatically receive the `locale` object as a prop by default. This can be used to scope API requests, etc.:
|
||||
|
||||
```tsx
|
||||
import React from 'react'
|
||||
@@ -368,7 +396,7 @@ const Greeting: React.FC = () => {
|
||||
See the [Hooks](./hooks) documentation for a full list of available hooks.
|
||||
</Banner>
|
||||
|
||||
#### Styling Custom Components
|
||||
### Styling Custom Components
|
||||
|
||||
Payload has a robust [CSS Library](./customizing-css) that you can style your Custom Components similarly to Payload's built-in styling. This will ensure that your Custom Component matches the existing design system, and so that it automatically adapts to any theme changes.
|
||||
|
||||
|
||||
@@ -33,15 +33,15 @@ This key will automatically be made available to the client-side Payload bundle
|
||||
'use client'
|
||||
import React from 'react'
|
||||
|
||||
const stripeKey = process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY
|
||||
const stripeKey = process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY // highlight-line
|
||||
|
||||
const MyClientComponent = () => {
|
||||
// do something with the key
|
||||
|
||||
return (
|
||||
<p>
|
||||
<div>
|
||||
My Client Component
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
@@ -64,15 +64,15 @@ This key will be available to your Server Components as follows:
|
||||
```tsx
|
||||
import React from 'react'
|
||||
|
||||
const stripeSecret = process.env.STRIPE_SECRET
|
||||
const stripeSecret = process.env.STRIPE_SECRET // highlight-line
|
||||
|
||||
const MyServerComponent = async () => {
|
||||
// do something with the secret
|
||||
|
||||
return (
|
||||
<p>
|
||||
<div>
|
||||
My Server Component
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
@@ -780,7 +780,7 @@ const Greeting: React.FC = () => {
|
||||
|
||||
## useConfig
|
||||
|
||||
Used to easily fetch the Payload Client Config.
|
||||
Used to easily retrieve the Payload [Client Config](./components#accessing-the-payload-config).
|
||||
|
||||
```tsx
|
||||
'use client'
|
||||
|
||||
@@ -53,7 +53,7 @@ app/
|
||||
|
||||
As shown above, all Payload routes are nested within the `(payload)` route group. This creates a boundary between the Admin Panel and the rest of your application by scoping all layouts and styles. The `layout.tsx` file within this directory, for example, is where Payload manages the `html` tag of the document to set proper `lang` and `dir` attributes, etc.
|
||||
|
||||
The `admin` directory contains all the _pages_ related to the interface itself, and the `api` and `graphql` directories contains all the _routes_ related to the [REST API](../rest-api/overview) and [GraphQL API](../graphql/overview). All admin routes are [easily configurable](#customizing-routes) to meet your application's requirements.
|
||||
The `admin` directory contains all the _pages_ related to the interface itself, whereas the `api` and `graphql` directories contains all the _routes_ related to the [REST API](../rest-api/overview) and [GraphQL API](../graphql/overview). All admin routes are [easily configurable](#customizing-routes) to meet your application's requirements.
|
||||
|
||||
Finally, the `custom.scss` file is where you can add or override globally-oriented styles in the Admin Panel, such as the color palette. Customizing the look and feel through CSS alone is a powerful feature of the Admin Panel, [more on that here](./customizing-css).
|
||||
|
||||
@@ -128,9 +128,9 @@ You can use whatever Collection you'd like to access the Admin Panel as long as
|
||||
- `admins` - meant to have a higher level of permissions to manage your data and access the Admin Panel
|
||||
- `customers` - meant for end users of your app that should not be allowed to log into the Admin Panel
|
||||
|
||||
To do this, specify `admin: { user: 'admins' }` in your config. This will provide access to the Admin Panel to only `admins`. Any users authenticated as `customers` will be prevented from accessing the Admin Panel. See [Access Control](/docs/access-control/overview) for full details. For a complete, working example of role-based access control, check out the official [Auth Example](https://github.com/payloadcms/payload/tree/main/examples/auth/payload).
|
||||
To do this, specify `admin: { user: 'admins' }` in your config. This will provide access to the Admin Panel to only `admins`. Any users authenticated as `customers` will be prevented from accessing the Admin Panel. See [Access Control](/docs/access-control/overview) for full details.
|
||||
|
||||
#### Role-based access control
|
||||
### Role-based Access Control
|
||||
|
||||
It is also possible to allow multiple user types into the Admin Panel with limited permissions. For example, you may wish to have two roles within the `admins` Collection:
|
||||
|
||||
@@ -141,11 +141,11 @@ To do this, add a `roles` or similar field to your auth-enabled Collection, then
|
||||
|
||||
## Customizing Routes
|
||||
|
||||
You have full control over the routes that Payload binds itself to. This includes both root-level routes such as the REST API, and admin-level routes such as the user's account page. You can customize these routes to meet the needs of your application simply by specifying the desired paths in your config.
|
||||
You have full control over the routes that Payload binds itself to. This includes both [Root-level Routes](#root-level-routes) such as the [REST API](../rest-api/overview), and [Admin-level Routes](#admin-level-routes) such as the user's account page. You can customize these routes to meet the needs of your application simply by specifying the desired paths in your config.
|
||||
|
||||
#### Root-level Routes
|
||||
### Root-level Routes
|
||||
|
||||
Root-level routes are those that are not behind the `/admin` path, such as the REST API and GraphQL APIs, or the root path of the Admin Panel itself.
|
||||
Root-level routes are those that are not behind the `/admin` path, such as the [REST API](../rest-api/overview) and [GraphQL API](../graphql/overview), or the root path of the Admin Panel itself.
|
||||
|
||||
Here is an example of how you might modify root-level routes:
|
||||
|
||||
@@ -179,7 +179,7 @@ You can configure custom paths for the following root-level routes through the `
|
||||
You can easily add _new_ routes to the Admin Panel through the `endpoints` property of the Payload Config. See [Custom Endpoints](../rest-api/overview#custom-endpoints) for more information.
|
||||
</Banner>
|
||||
|
||||
#### Admin-level Routes
|
||||
### Admin-level Routes
|
||||
|
||||
Admin-level routes are those behind the `/admin` path. These are the routes that are part of the Admin Panel itself, such as the user's account page, the login page, etc.
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ desc:
|
||||
keywords:
|
||||
---
|
||||
|
||||
Views are the individual pages that make up the [Admin Panel](./overview), such as the Dashboard, List, and Edit views. One of the most powerful ways to customize the Admin Panel is to create Custom Views. These are [Custom Components](./components) that can either replace built-in ones or be entirely new.
|
||||
Views are the individual pages that make up the [Admin Panel](./overview), such as the Dashboard, List, and Edit views. One of the most powerful ways to customize the Admin Panel is to create Custom Views. These are [Custom Components](./components) that can either replace built-in views or be entirely new.
|
||||
|
||||
To swap in your own Custom Views, consult the list of available components below. Determine the scope that corresponds to what you are trying to accomplish, then [author your React component(s)](#building-custom-views) accordingly.
|
||||
|
||||
@@ -57,7 +57,7 @@ For more granular control, pass a configuration object instead. Payload exposes
|
||||
|
||||
_\* An asterisk denotes that a property is required._
|
||||
|
||||
#### Adding New Views
|
||||
### Adding New Views
|
||||
|
||||
To add a _new_ views to the [Admin Panel](./overview), simply add your own key to the `views` object with at least a `path` and `Component` property. For example:
|
||||
|
||||
@@ -209,7 +209,7 @@ The following options are available:
|
||||
| **`API`** | The API view is used to display the REST API JSON response for a given document. |
|
||||
| **`LivePreview`** | The LivePreview view is used to display the Live Preview interface. [More details](../live-preview). |
|
||||
|
||||
#### Document Tabs
|
||||
### Document Tabs
|
||||
|
||||
Each Document View can be given a new tab in the Edit View, if desired. Tabs are highly configurable, from as simple as changing the label to swapping out the entire component, they can be modified in any way. To add or customize tabs in the Edit View, use the `Component.Tab` key:
|
||||
|
||||
|
||||
@@ -28,7 +28,10 @@ Right now, Payload is officially supporting two rich text editors:
|
||||
<Banner type="success">
|
||||
<strong>
|
||||
Consistent with Payload's goal of making you learn as little of Payload as possible, customizing
|
||||
and using the Rich Text Editor does not involve learning how to develop for a <em>Payload</em>{' '}
|
||||
and using the Rich Text Editor does not involve learning how to develop for a
|
||||
{' '}
|
||||
<em>Payload</em>
|
||||
{' '}
|
||||
rich text editor.
|
||||
</strong>
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ By convention, these are named feature.server.ts for server-side functionality a
|
||||
|
||||
## Server Feature
|
||||
|
||||
In order to get started with a new feature, you should start with the server feature which is the entry-point of your feature.
|
||||
To start building new features, you should start with the server feature, which is the entry-point.
|
||||
|
||||
**Example myFeature/feature.server.ts:**
|
||||
|
||||
@@ -52,20 +52,16 @@ import { lexicalEditor } from '@payloadcms/richtext-lexical';
|
||||
By default, this server feature does nothing - you haven't added any functionality yet. Depending on what you want your
|
||||
feature to do, the ServerFeature type exposes various properties you can set to inject custom functionality into the lexical editor.
|
||||
|
||||
Here is an example:
|
||||
### i18n
|
||||
|
||||
Each feature can register their own translations, which are automatically scoped to the feature key:
|
||||
|
||||
```ts
|
||||
import { createServerFeature, createNode } from '@payloadcms/richtext-lexical';
|
||||
import { MyClientFeature } from './feature.client.ts';
|
||||
import { MyMarkdownTransformer } from './feature.client.ts';
|
||||
import { createServerFeature } from '@payloadcms/richtext-lexical';
|
||||
|
||||
|
||||
export const MyFeature = createServerFeature({
|
||||
feature: {
|
||||
// This allows you to connect the Client Feature. More on that below
|
||||
ClientFeature: MyClientFeature,
|
||||
// This allows you to add i18n translations scoped to your feature.
|
||||
// This specific translation will be available under "lexical:myFeature:label" - myFeature
|
||||
// being your feature key.
|
||||
i18n: {
|
||||
en: {
|
||||
label: 'My Feature',
|
||||
@@ -74,9 +70,75 @@ export const MyFeature = createServerFeature({
|
||||
label: 'Mein Feature',
|
||||
},
|
||||
},
|
||||
// Markdown Transformers in the server feature are used when converting the
|
||||
// editor from or to markdown
|
||||
},
|
||||
key: 'myFeature',
|
||||
})
|
||||
```
|
||||
|
||||
This allows you to add i18n translations scoped to your feature. This specific example translation will be available under `lexical:myFeature:label` - `myFeature` being your feature key.
|
||||
|
||||
### Markdown Transformers
|
||||
|
||||
The Server Feature, just like the Client Feature, allows you to add markdown transformers. Markdown transformers on the server are used when [converting the editor from or to markdown](/docs/lexical/converters#markdown-lexical).
|
||||
|
||||
```ts
|
||||
import { createServerFeature } from '@payloadcms/richtext-lexical';
|
||||
import type { ElementTransformer } from '@lexical/markdown'
|
||||
import {
|
||||
$createMyNode,
|
||||
$isMyNode,
|
||||
MyNode
|
||||
} from './nodes/MyNode'
|
||||
|
||||
const MyMarkdownTransformer: ElementTransformer = {
|
||||
type: 'element',
|
||||
dependencies: [MyNode],
|
||||
export: (node, exportChildren) => {
|
||||
if (!$isMyNode(node)) {
|
||||
return null
|
||||
}
|
||||
return '+++'
|
||||
},
|
||||
// match ---
|
||||
regExp: /^+++\s*$/,
|
||||
replace: (parentNode) => {
|
||||
const node = $createMyNode()
|
||||
if (node) {
|
||||
parentNode.replace(node)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
export const MyFeature = createServerFeature({
|
||||
feature: {
|
||||
markdownTransformers: [MyMarkdownTransformer],
|
||||
},
|
||||
key: 'myFeature',
|
||||
})
|
||||
```
|
||||
|
||||
In this example, the node will be outputted as `+++` in Markdown, and the markdown `+++` will be converted to a `MyNode` node in the editor.
|
||||
|
||||
### Nodes
|
||||
|
||||
While nodes added to the server feature do not control how the node is rendered in the editor, they control other aspects of the node:
|
||||
- HTML conversion
|
||||
- Node Hooks
|
||||
- Sub fields
|
||||
- Behavior in a headless editor
|
||||
|
||||
The `createNode` helper function is used to create nodes with proper typing. It is recommended to use this function to create nodes.
|
||||
|
||||
```ts
|
||||
import { createServerFeature, createNode } from '@payloadcms/richtext-lexical';
|
||||
import {
|
||||
MyNode
|
||||
} from './nodes/MyNode'
|
||||
|
||||
export const MyFeature = createServerFeature({
|
||||
feature: {
|
||||
|
||||
nodes: [
|
||||
// Use the createNode helper function to more easily create nodes with proper typing
|
||||
createNode({
|
||||
@@ -99,6 +161,18 @@ export const MyFeature = createServerFeature({
|
||||
})
|
||||
```
|
||||
|
||||
While nodes in the client feature are added by themselves to the nodes array, nodes in the server feature can be added together with the following sibling options:
|
||||
|
||||
| Option | Description |
|
||||
|---------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| **`getSubFields`** | If a node includes sub-fields (e.g. block and link nodes), passing the subFields schema here will make payload automatically populate & run hooks for them. |
|
||||
| **`getSubFieldsData`** | If a node includes sub-fields, the sub-fields data needs to be returned here, alongside `getSubFields` which returns their schema. |
|
||||
| **`graphQLPopulationPromises`** | Allows you to run population logic when a node's data was requested from GraphQL. While `getSubFields` and `getSubFieldsData` automatically handle populating sub-fields (since they run hooks on them), those are only populated in the Rest API. This is because the Rest API hooks do not have access to the 'depth' property provided by GraphQL. In order for them to be populated correctly in GraphQL, the population logic needs to be provided here. |
|
||||
| **`node`** | The actual lexical node needs to be provided here. This also supports [lexical node replacements](https://lexical.dev/docs/concepts/node-replacement). |
|
||||
| **`validations`** | This allows you to provide node validations, which are run when your document is being validated, alongside other payload fields. You can use it to throw a validation error for a specific node in case its data is incorrect. |
|
||||
| **`converters`** | Allows you to define how a node can be serialized into different formats. Currently, only supports HTML. Markdown converters are defined in `markdownTransformers` and not here. |
|
||||
| **`hooks`** | Just like payload fields, you can provide hooks which are run for this specific node. These are called Node Hooks. |
|
||||
|
||||
### Feature load order
|
||||
|
||||
Server features can also accept a function as the `feature` property (useful for sanitizing props, as mentioned below). This function will be called when the feature is loaded during the payload sanitization process:
|
||||
@@ -211,6 +285,8 @@ export const MyClientFeature = createClientFeature({
|
||||
})
|
||||
```
|
||||
|
||||
This also supports [lexical node replacements](https://lexical.dev/docs/concepts/node-replacement).
|
||||
|
||||
**myFeature/nodes/MyNode.tsx:**
|
||||
|
||||
Here is a basic DecoratorNode example:
|
||||
@@ -347,7 +423,7 @@ Please do not add any 'use client' directives to your nodes, as the node class c
|
||||
### Plugins
|
||||
|
||||
One small part of a feature are plugins. The name stems from the lexical playground plugins and is just a small part of a lexical feature.
|
||||
Plugins are simply react components which are added to the editor, within all the lexical context providers. They can be used to add any functionality
|
||||
Plugins are simply React components which are added to the editor, within all the lexical context providers. They can be used to add any functionality
|
||||
to the editor, by utilizing the lexical API.
|
||||
|
||||
Most commonly, they are used to register [lexical listeners](https://lexical.dev/docs/concepts/listeners), [node transforms](https://lexical.dev/docs/concepts/transforms) or [commands](https://lexical.dev/docs/concepts/commands).
|
||||
@@ -430,19 +506,81 @@ export const MyNodePlugin: PluginComponent= () => {
|
||||
}
|
||||
```
|
||||
|
||||
In this example, we register a lexical command which simply inserts a new MyNode into the editor. This command can be called from anywhere within lexical, e.g. from within a custom node.
|
||||
In this example, we register a lexical command, which simply inserts a new MyNode into the editor. This command can be called from anywhere within lexical, e.g. from within a custom node.
|
||||
|
||||
### Toolbar groups
|
||||
|
||||
Toolbar groups are visual containers which hold toolbar items. There are different toolbar group types which determine *how* a toolbar item is displayed: `dropdown` and `buttons`.
|
||||
|
||||
All the default toolbar groups are exported from `@payloadcms/richtext-lexical/client`. You can use them to add your own toolbar items to the editor:
|
||||
- Dropdown: `toolbarAddDropdownGroupWithItems`
|
||||
- Dropdown: `toolbarTextDropdownGroupWithItems`
|
||||
- Buttons: `toolbarFormatGroupWithItems`
|
||||
- Buttons: `toolbarFeatureButtonsGroupWithItems`
|
||||
|
||||
Within dropdown groups, items are positioned vertically when the dropdown is opened and include the icon & label. Within button groups, items are positioned horizontally and only include the icon. If a toolbar group with the same key is declared twice, all its items will be merged into one group.
|
||||
|
||||
#### Custom buttons toolbar group
|
||||
|
||||
| Option | Description |
|
||||
|-------------|--------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| **`items`** | All toolbar items part of this toolbar group need to be added here. |
|
||||
| **`key`** | Each toolbar group needs to have a unique key. Groups with the same keys will have their items merged together. |
|
||||
| **`order`** | Determines where the toolbar group will be. |
|
||||
| **`type`** | Controls the toolbar group type. Set to `buttons` to create a buttons toolbar group, which displays toolbar items horizontally using only their icons. |
|
||||
|
||||
Example:
|
||||
```ts
|
||||
import type { ToolbarGroup, ToolbarGroupItem } from '@payloadcms/richtext-lexical'
|
||||
|
||||
export const toolbarFormatGroupWithItems = (items: ToolbarGroupItem[]): ToolbarGroup => {
|
||||
return {
|
||||
type: 'buttons',
|
||||
items,
|
||||
key: 'myButtonsToolbar',
|
||||
order: 10,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Custom dropdown toolbar group
|
||||
|
||||
| Option | Description |
|
||||
|----------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| **`items`** | All toolbar items part of this toolbar group need to be added here. |
|
||||
| **`key`** | Each toolbar group needs to have a unique key. Groups with the same keys will have their items merged together. |
|
||||
| **`order`** | Determines where the toolbar group will be. |
|
||||
| **`type`** | Controls the toolbar group type. Set to `dropdown` to create a buttons toolbar group, which displays toolbar items vertically using their icons and labels, if the dropdown is open. |
|
||||
| **`ChildComponent`** | The dropdown toolbar ChildComponent allows you to pass in a React Component which will be displayed within the dropdown button. |
|
||||
|
||||
Example:
|
||||
```ts
|
||||
import type { ToolbarGroup, ToolbarGroupItem } from '@payloadcms/richtext-lexical'
|
||||
|
||||
import { MyIcon } from './icons/MyIcon'
|
||||
|
||||
export const toolbarAddDropdownGroupWithItems = (items: ToolbarGroupItem[]): ToolbarGroup => {
|
||||
return {
|
||||
type: 'dropdown',
|
||||
ChildComponent: MyIcon,
|
||||
items,
|
||||
key: 'myDropdownToolbar',
|
||||
order: 10,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Toolbar items
|
||||
|
||||
Custom nodes and features on its own are pointless, if they can not be added to the editor. You will need to hook in one of our interfaces which allow the user to interact with the editor:
|
||||
Custom nodes and features on its own are pointless, if they can't be added to the editor. You will need to hook in one of our interfaces which allow the user to interact with the editor:
|
||||
|
||||
- Fixed toolbar which stays fixed at the top of the editor
|
||||
- Inline, floating toolbar which appears when selecting text
|
||||
- Slash menu which appears when typing `/` in the editor
|
||||
- Markdown transformers which are triggered when a certain text pattern is typed in the editor
|
||||
- Markdown transformers, which are triggered when a certain text pattern is typed in the editor
|
||||
- Or any other interfaces which can be added via your own plugins. Our toolbars are a prime example of this - they are just plugins.
|
||||
|
||||
In order to add a toolbar item to either the floating or the inline toolbar, you can add a ToolbarGroup with a ToolbarItem to the `toolbarFixed` or `toolbarInline` props of your client feature:
|
||||
To add a toolbar item to either the floating or the inline toolbar, you can add a ToolbarGroup with a ToolbarItem to the `toolbarFixed` or `toolbarInline` props of your client feature:
|
||||
|
||||
```ts
|
||||
'use client'
|
||||
@@ -481,10 +619,7 @@ export const MyClientFeature = createClientFeature({
|
||||
})
|
||||
```
|
||||
|
||||
You will have to provide a toolbar group first, and then the items for that toolbar group.
|
||||
We already export all the default toolbar groups (like `toolbarAddDropdownGroupWithItems`, so you can use them as a base for your own toolbar items.
|
||||
|
||||
If a toolbar with the same key is declared twice, all its items will be merged together into one group.
|
||||
You will have to provide a toolbar group first, and then the items for that toolbar group (more on that above).
|
||||
|
||||
A `ToolbarItem` various props you can use to customize its behavior:
|
||||
|
||||
@@ -492,7 +627,7 @@ A `ToolbarItem` various props you can use to customize its behavior:
|
||||
|----------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| **`ChildComponent`** | A React component which is rendered within your toolbar item's default button component. Usually, you want this to be an icon. |
|
||||
| **`Component`** | A React component which is rendered in place of the toolbar item's default button component, thus completely replacing it. The `ChildComponent` and `onSelect` properties will be ignored. |
|
||||
| **`label`** | The label will be displayed in your toolbar item, if it's within a dropdown group. In order to make use of i18n, this can be a function. |
|
||||
| **`label`** | The label will be displayed in your toolbar item, if it's within a dropdown group. To make use of i18n, this can be a function. |
|
||||
| **`key`** | Each toolbar item needs to have a unique key. |
|
||||
| **`onSelect`** | A function which is called when the toolbar item is clicked. |
|
||||
| **`isEnabled`** | This is optional and controls if the toolbar item is clickable or not. If `false` is returned here, it will be grayed out and unclickable. |
|
||||
@@ -501,6 +636,34 @@ A `ToolbarItem` various props you can use to customize its behavior:
|
||||
The API for adding an item to the floating inline toolbar (`toolbarInline`) is identical. If you wanted to add an item to both the fixed and inline toolbar, you can extract it into its own variable
|
||||
(typed as `ToolbarGroup[]`) and add it to both the `toolbarFixed` and `toolbarInline` props.
|
||||
|
||||
### Slash Menu groups
|
||||
|
||||
We're exporting `slashMenuBasicGroupWithItems` from `@payloadcms/richtext-lexical/client` which you can use to add items to the slash menu labelled "Basic". If you want to create your own slash menu group, here is an example:
|
||||
|
||||
```ts
|
||||
import type {
|
||||
SlashMenuGroup,
|
||||
SlashMenuItem,
|
||||
} from '@payloadcms/richtext-lexical'
|
||||
|
||||
export function mwnSlashMenuGroupWithItems(items: SlashMenuItem[]): SlashMenuGroup {
|
||||
return {
|
||||
items,
|
||||
key: 'myGroup',
|
||||
label: 'My Group' // <= This can be a function to make use of i18n
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
By creating a helper function like this, you can easily re-use it and add items to it. All Slash Menu groups with the same keys will have their items merged together.
|
||||
|
||||
| Option | Description |
|
||||
|-------------|---------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| **`items`** | An array of `SlashMenuItem`'s which will be displayed in the slash menu. |
|
||||
| **`label`** | The label will be displayed before your Slash Menu group. In order to make use of i18n, this can be a function. |
|
||||
| **`key`** | Used for class names and, if label is not provided, for display. Slash menus with the same key will have their items merged together. |
|
||||
|
||||
|
||||
### Slash Menu items
|
||||
|
||||
The API for adding items to the slash menu is similar. There are slash menu groups, and each slash menu groups has items. Here is an example:
|
||||
@@ -533,17 +696,58 @@ export const MyClientFeature = createClientFeature({
|
||||
})
|
||||
```
|
||||
|
||||
| Option | Description |
|
||||
|----------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| **`Icon`** | The icon which is rendered in your slash menu item. |
|
||||
| **`label`** | The label will be displayed in your slash menu item. In order to make use of i18n, this can be a function. |
|
||||
| **`key`** | Each slash menu item needs to have a unique key. The key will be matched when typing, displayed if no `label` property is set, and used for classNames. |
|
||||
| **`onSelect`** | A function which is called when the slash menu item is selected. |
|
||||
| **`keywords`** | Keywords are used in order to match the item for different texts typed after the '/'. E.g. you might want to show a horizontal rule item if you type both /hr, /separator, /horizontal etc. Additionally to the keywords, the label and key will be used to match the correct slash menu item. |
|
||||
| Option | Description |
|
||||
|----------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| **`Icon`** | The icon which is rendered in your slash menu item. |
|
||||
| **`label`** | The label will be displayed in your slash menu item. In order to make use of i18n, this can be a function. |
|
||||
| **`key`** | Each slash menu item needs to have a unique key. The key will be matched when typing, displayed if no `label` property is set, and used for classNames. |
|
||||
| **`onSelect`** | A function which is called when the slash menu item is selected. |
|
||||
| **`keywords`** | Keywords are used to match the item for different texts typed after the '/'. E.g. you might want to show a horizontal rule item if you type both /hr, /separator, /horizontal etc. In addition to the keywords, the label and key will be used to find the right slash menu item. |
|
||||
|
||||
|
||||
### Markdown Transformers
|
||||
|
||||
The Client Feature, just like the Server Feature, allows you to add markdown transformers. Markdown transformers on the client are used to create new nodes when a certain markdown pattern is typed in the editor.
|
||||
|
||||
```ts
|
||||
import { createClientFeature } from '@payloadcms/richtext-lexical/client';
|
||||
import type { ElementTransformer } from '@lexical/markdown'
|
||||
import {
|
||||
$createMyNode,
|
||||
$isMyNode,
|
||||
MyNode
|
||||
} from './nodes/MyNode'
|
||||
|
||||
const MyMarkdownTransformer: ElementTransformer = {
|
||||
type: 'element',
|
||||
dependencies: [MyNode],
|
||||
export: (node, exportChildren) => {
|
||||
if (!$isMyNode(node)) {
|
||||
return null
|
||||
}
|
||||
return '+++'
|
||||
},
|
||||
// match ---
|
||||
regExp: /^+++\s*$/,
|
||||
replace: (parentNode) => {
|
||||
const node = $createMyNode()
|
||||
if (node) {
|
||||
parentNode.replace(node)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
export const MyFeature = createClientFeature({
|
||||
markdownTransformers: [MyMarkdownTransformer],
|
||||
})
|
||||
```
|
||||
|
||||
In this example, a new `MyNode` will be inserted into the editor when `+++ ` is typed.
|
||||
|
||||
## Props
|
||||
|
||||
In order to accept props in your feature, you should first type them as a generic.
|
||||
To accept props in your feature, type them as a generic.
|
||||
|
||||
Server Feature:
|
||||
|
||||
@@ -578,9 +782,9 @@ createServerFeature<UnSanitizedProps, SanitizedProps, UnSanitizedClientProps>({
|
||||
})
|
||||
```
|
||||
|
||||
Keep in mind that any sanitized props then have to returned in the `sanitizedServerFeatureProps` property.
|
||||
Keep in mind that any sanitized props then have to be returned in the `sanitizedServerFeatureProps` property.
|
||||
|
||||
In the client feature, it works in a similar way:
|
||||
In the client feature, it works similarly:
|
||||
|
||||
```ts
|
||||
createClientFeature<UnSanitizedClientProps, SanitizedClientProps>(
|
||||
@@ -617,4 +821,4 @@ The reason the client feature does not have the same props available as the serv
|
||||
|
||||
## More information
|
||||
|
||||
Take a look at the [features we've already built](https://github.com/payloadcms/payload/tree/beta/packages/richtext-lexical/src/features) - understanding how they work will help you understand how to create your own. There is no difference between the features included by default and the ones you create yourself - since those features are all isolated from the "core", you have access to the same APIs, whether the feature is part of payload or not!
|
||||
Have a look at the [features we've already built](https://github.com/payloadcms/payload/tree/beta/packages/richtext-lexical/src/features) - understanding how they work will help you understand how to create your own. There is no difference between the features included by default and the ones you create yourself - since those features are all isolated from the "core", you have access to the same APIs, whether the feature is part of payload or not!
|
||||
|
||||
@@ -46,35 +46,55 @@ const Pages: CollectionConfig = {
|
||||
|
||||
The `lexicalHTML()` function creates a new field that automatically converts the referenced lexical richText field into HTML through an afterRead hook.
|
||||
|
||||
### Generating HTML anywhere on the server:
|
||||
### Generating HTML anywhere on the server
|
||||
|
||||
If you wish to convert JSON to HTML ad-hoc, use this code snippet:
|
||||
If you wish to convert JSON to HTML ad-hoc, use the `convertLexicalToHTML` function:
|
||||
|
||||
```ts
|
||||
import type { SerializedEditorState } from 'lexical'
|
||||
import {
|
||||
type SanitizedEditorConfig,
|
||||
convertLexicalToHTML,
|
||||
consolidateHTMLConverters,
|
||||
} from '@payloadcms/richtext-lexical'
|
||||
import { consolidateHTMLConverters, convertLexicalToHTML } from '@payloadcms/richtext-lexical'
|
||||
|
||||
async function lexicalToHTML(
|
||||
editorData: SerializedEditorState,
|
||||
editorConfig: SanitizedEditorConfig,
|
||||
) {
|
||||
return await convertLexicalToHTML({
|
||||
converters: consolidateHTMLConverters({ editorConfig }),
|
||||
data: editorData,
|
||||
payload, // if you have payload but no req available, pass it in here to enable server-only functionality (e.g. proper conversion of upload nodes)
|
||||
req, // if you have req available, pass it in here to enable server-only functionality (e.g. proper conversion of upload nodes). No need to pass in payload if req is passed in.
|
||||
})
|
||||
}
|
||||
|
||||
await convertLexicalToHTML({
|
||||
converters: consolidateHTMLConverters({ editorConfig }),
|
||||
data: editorData,
|
||||
payload, // if you have payload but no req available, pass it in here to enable server-only functionality (e.g. proper conversion of upload nodes)
|
||||
req, // if you have req available, pass it in here to enable server-only functionality (e.g. proper conversion of upload nodes). No need to pass in payload if req is passed in.
|
||||
})
|
||||
```
|
||||
|
||||
This method employs `convertLexicalToHTML` from `@payloadcms/richtext-lexical`, which converts the serialized editor state into HTML.
|
||||
|
||||
Because every `Feature` is able to provide html converters, and because the `htmlFeature` can modify those or provide their own, we need to consolidate them with the default html Converters using the `consolidateHTMLConverters` function.
|
||||
|
||||
#### Example: Generating HTML within an afterRead hook
|
||||
|
||||
```ts
|
||||
import type { FieldHook } from 'payload'
|
||||
|
||||
import {
|
||||
HTMLConverterFeature,
|
||||
consolidateHTMLConverters,
|
||||
convertLexicalToHTML,
|
||||
defaultEditorConfig,
|
||||
defaultEditorFeatures,
|
||||
sanitizeServerEditorConfig,
|
||||
} from '@payloadcms/richtext-lexical'
|
||||
|
||||
const hook: FieldHook = async ({ req, siblingData }) => {
|
||||
const editorConfig = defaultEditorConfig
|
||||
|
||||
editorConfig.features = [...defaultEditorFeatures, HTMLConverterFeature({})]
|
||||
|
||||
const sanitizedEditorConfig = await sanitizeServerEditorConfig(editorConfig, req.payload.config)
|
||||
|
||||
const html = await convertLexicalToHTML({
|
||||
converters: consolidateHTMLConverters({ editorConfig: sanitizedEditorConfig }),
|
||||
data: siblingData.lexicalSimple,
|
||||
req,
|
||||
})
|
||||
return html
|
||||
}
|
||||
```
|
||||
|
||||
### CSS
|
||||
|
||||
Payload's lexical HTML converter does not generate CSS for you, but it does add classes to the generated HTML. You can use these classes to style the HTML in your frontend.
|
||||
@@ -184,10 +204,11 @@ import { createHeadlessEditor } from '@lexical/headless' // <= make sure this pa
|
||||
import { getEnabledNodes, sanitizeServerEditorConfig } from '@payloadcms/richtext-lexical'
|
||||
|
||||
const yourEditorConfig // <= your editor config here
|
||||
const payloadConfig // <= your payload config here
|
||||
|
||||
const headlessEditor = createHeadlessEditor({
|
||||
nodes: getEnabledNodes({
|
||||
editorConfig: sanitizeServerEditorConfig(yourEditorConfig),
|
||||
editorConfig: sanitizeServerEditorConfig(yourEditorConfig, payloadConfig),
|
||||
}),
|
||||
})
|
||||
```
|
||||
@@ -316,7 +337,7 @@ Convert markdown content to the Lexical editor format with the following:
|
||||
import { $convertFromMarkdownString } from '@lexical/markdown'
|
||||
import { sanitizeServerEditorConfig } from '@payloadcms/richtext-lexical'
|
||||
|
||||
const yourSanitizedEditorConfig = sanitizeServerEditorConfig(yourEditorConfig) // <= your editor config here
|
||||
const yourSanitizedEditorConfig = sanitizeServerEditorConfig(yourEditorConfig, payloadConfig) // <= your editor config & payload config here
|
||||
const markdown = `# Hello World`
|
||||
|
||||
headlessEditor.update(
|
||||
@@ -344,7 +365,7 @@ import { $convertToMarkdownString } from '@lexical/markdown'
|
||||
import { sanitizeServerEditorConfig } from '@payloadcms/richtext-lexical'
|
||||
import type { SerializedEditorState } from 'lexical'
|
||||
|
||||
const yourSanitizedEditorConfig = sanitizeServerEditorConfig(yourEditorConfig) // <= your editor config here
|
||||
const yourSanitizedEditorConfig = sanitizeServerEditorConfig(yourEditorConfig, payloadConfig) // <= your editor config & payload config here
|
||||
const yourEditorState: SerializedEditorState // <= your current editor state here
|
||||
|
||||
// Import editor state into your headless editor
|
||||
|
||||
@@ -10,6 +10,21 @@ keywords: lexical, rich text, editor, headless cms, migrate, migration
|
||||
|
||||
While both Slate and Lexical save the editor state in JSON, the structure of the JSON is different.
|
||||
|
||||
### Migration via Migration Script (Recommended)
|
||||
|
||||
Just import the `migrateSlateToLexical` function we provide, pass it the `payload` object and run it. Depending on the amount of collections, this might take a while.
|
||||
|
||||
IMPORTANT: This will overwrite all slate data. We recommend doing the following first:
|
||||
1. Take a backup of your entire database. If anything goes wrong and you do not have a backup, you are on your own and will not receive any support.
|
||||
2. Make every richText field a lexical editor. This script will only convert lexical richText fields with old Slate data
|
||||
3. Add the SlateToLexicalFeature (as seen below) first, and test it out by loading up the admin panel, to see if the migrator works as expected. You might have to build some custom converters for some fields first in order to convert custom Slate nodes. The SlateToLexicalFeature is where the converters are stored. Only fields with this feature added will be migrated.
|
||||
|
||||
```ts
|
||||
import { migrateSlateToLexical } from '@payloadcms/richtext-lexical'
|
||||
|
||||
await migrateSlateToLexical({ payload })
|
||||
```
|
||||
|
||||
### Migration via SlateToLexicalFeature
|
||||
|
||||
One way to handle this is to just give your lexical editor the ability to read the slate JSON.
|
||||
@@ -42,7 +57,7 @@ This is by far the easiest way to migrate from Slate to Lexical, although it doe
|
||||
- There is a performance hit when initializing the lexical editor
|
||||
- The editor will still output the Slate data in the output JSON, as the on-the-fly converter only runs for the admin panel
|
||||
|
||||
The easy way to solve this: Just save the document! This overrides the slate data with the lexical data, and the next time the document is loaded, the lexical data will be used. This solves both the performance and the output issue for that specific document.
|
||||
The easy way to solve this: Edit the richText field and save the document! This overrides the slate data with the lexical data, and the next time the document is loaded, the lexical data will be used. This solves both the performance and the output issue for that specific document. This, however, is a slow and gradual migration process, thus you will have to support both API formats. Especially for a large number of documents, we recommend running the migration script, as explained above.
|
||||
|
||||
### Migration via migration script
|
||||
|
||||
|
||||
@@ -89,7 +89,7 @@ import { CallToAction } from '../blocks/CallToAction'
|
||||
|
||||
{
|
||||
editor: lexicalEditor({
|
||||
features: ({ defaultFeatures }) => [
|
||||
features: ({ defaultFeatures, rootFeatures }) => [
|
||||
...defaultFeatures,
|
||||
LinkFeature({
|
||||
// Example showing how to customize the built-in fields
|
||||
@@ -134,6 +134,15 @@ import { CallToAction } from '../blocks/CallToAction'
|
||||
}
|
||||
```
|
||||
|
||||
`features` can be both an array of features, or a function returning an array of features. The function provides the following props:
|
||||
|
||||
|
||||
| Prop | Description |
|
||||
|-----------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| **`defaultFeatures`** | This opinionated array contains all "recommended" default features. You can see which features are included in the default features in the table below. |
|
||||
| **`rootFeatures`** | This array contains all features that are enabled in the root richText editor (the one defined in the payload.config.ts). If this field is the root richText editor, or if the root richText editor is not a lexical editor, this array will be empty. |
|
||||
|
||||
|
||||
## Features overview
|
||||
|
||||
Here's an overview of all the included features:
|
||||
@@ -169,3 +178,77 @@ Notice how even the toolbars are features? That's how extensible our lexical edi
|
||||
## Creating your own, custom Feature
|
||||
|
||||
You can find more information about creating your own feature in our [building custom feature docs](lexical/building-custom-features).
|
||||
|
||||
## TypeScript
|
||||
|
||||
Every single piece of saved data is 100% fully-typed within lexical. It provides a type for every single node, which can be imported from `@payloadcms/richtext-lexical` - each type is prefixed with `Serialized`, e.g. `SerializedUploadNode`.
|
||||
|
||||
In order to fully type the entire editor JSON, you can use our `TypedEditorState` helper type, which accepts a union of all possible node types as a generic. The reason we do not provide a type which already contains all possible node types is because the possible node types depend on which features you have enabled in your editor. Here is an example:
|
||||
|
||||
```ts
|
||||
import type {
|
||||
SerializedAutoLinkNode,
|
||||
SerializedBlockNode,
|
||||
SerializedHorizontalRuleNode,
|
||||
SerializedLinkNode,
|
||||
SerializedListItemNode,
|
||||
SerializedListNode,
|
||||
SerializedParagraphNode,
|
||||
SerializedQuoteNode,
|
||||
SerializedRelationshipNode,
|
||||
SerializedTextNode,
|
||||
SerializedUploadNode,
|
||||
TypedEditorState,
|
||||
} from '@payloadcms/richtext-lexical'
|
||||
|
||||
const editorState: TypedEditorState<
|
||||
| SerializedAutoLinkNode
|
||||
| SerializedBlockNode
|
||||
| SerializedHorizontalRuleNode
|
||||
| SerializedLinkNode
|
||||
| SerializedListItemNode
|
||||
| SerializedListNode
|
||||
| SerializedParagraphNode
|
||||
| SerializedQuoteNode
|
||||
| SerializedRelationshipNode
|
||||
| SerializedTextNode
|
||||
| SerializedUploadNode
|
||||
> = {
|
||||
root: {
|
||||
type: 'root',
|
||||
direction: 'ltr',
|
||||
format: '',
|
||||
indent: 0,
|
||||
version: 1,
|
||||
children: [
|
||||
{
|
||||
children: [
|
||||
{
|
||||
detail: 0,
|
||||
format: 0,
|
||||
mode: 'normal',
|
||||
style: '',
|
||||
text: 'Some text. Every property here is fully-typed',
|
||||
type: 'text',
|
||||
version: 1,
|
||||
},
|
||||
],
|
||||
direction: 'ltr',
|
||||
format: '',
|
||||
indent: 0,
|
||||
type: 'paragraph',
|
||||
textFormat: 0,
|
||||
version: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
This is a type-safe representation of the editor state. Looking at the auto-suggestions of `type` it will show you all the possible node types you can use.
|
||||
|
||||
Make sure to only use types exported from `@payloadcms/richtext-lexical`, not from the lexical core packages. We only have control over types we export and can guarantee that those are correct, even though lexical core may export types with identical names.
|
||||
|
||||
### Automatic type generation
|
||||
|
||||
Lexical does not generate the accurate type definitions for your richText fields for you yet - this will be improved in the future. Currently, it only outputs the rough shape of the editor JSON which you can enhance using type assertions.
|
||||
|
||||
@@ -6,7 +6,7 @@ desc: Starting to build your own plugin? Find everything you need and learn best
|
||||
keywords: plugins, template, config, configuration, extensions, custom, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
|
||||
---
|
||||
|
||||
Building your own plugin is easy, and if you're already familiar with Payload then you'll have everything you need to get started. You can either start from scratch or use the Payload plugin template to get up and running quickly.
|
||||
Building your own [Payload Plugin](./overview) is easy, and if you're already familiar with Payload then you'll have everything you need to get started. You can either start from scratch or use the [Plugin Template](#plugin-template) to get up and running quickly.
|
||||
|
||||
<Banner type="success">
|
||||
To use the template, run `npx create-payload-app@latest -t plugin -n my-new-plugin` directly in
|
||||
@@ -57,11 +57,11 @@ The initialization process goes in the following order:
|
||||
|
||||
## Plugin Template
|
||||
|
||||
In the [Payload plugin template](https://github.com/payloadcms/payload-plugin-template), you will see a common file structure that is used across plugins:
|
||||
In the [Payload Plugin Template](https://github.com/payloadcms/payload-plugin-template), you will see a common file structure that is used across plugins:
|
||||
|
||||
1. root folder - general configuration
|
||||
2. /src folder - everything related to the plugin
|
||||
3. /dev folder - sanitized test project for development
|
||||
1. `/` root folder - general configuration
|
||||
2. `/src` folder - everything related to the plugin
|
||||
3. `/dev` folder - sanitized test project for development
|
||||
|
||||
### The root folder
|
||||
|
||||
@@ -169,7 +169,7 @@ First up, the `src/index.ts` file - this is where the plugin should be imported
|
||||
|
||||
**Plugin.ts**
|
||||
|
||||
To reiterate, the essence of a payload plugin is simply to extend the Payload config - and that is exactly what we are doing in this file.
|
||||
To reiterate, the essence of a [Payload Plugin](./overview) is simply to extend the [Payload Config](../configuration/overview) - and that is exactly what we are doing in this file.
|
||||
|
||||
```
|
||||
export const samplePlugin =
|
||||
|
||||
@@ -6,16 +6,18 @@ desc: Plugins provide a great way to modularize Payload functionalities into eas
|
||||
keywords: plugins, config, configuration, extensions, custom, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
|
||||
---
|
||||
|
||||
Payload comes with a built-in Plugins infrastructure that allows developers to build their own modular and easily reusable sets of functionality.
|
||||
Payload Plugins take full advantage of the modularity of the [Payload Config](../configuration/overview), allowing developers to easily extend Payload's core functionality in a precise and granular way. This pattern allows developers to easily inject custom—sometimes complex—functionality into Payload apps from a very small touch-point.
|
||||
|
||||
There are many [Official Plugins](#official-plugins) available that solve for some of the most common uses cases, such as the [Form Builder Plugin](./seo) or [SEO Plugin](./seo). There are also [Community Plugins](#community-plugins) available, maintained entirely by contributing members. To extend Payload's functionality in some other way, you can easily [build your own plugin](./build-your-own).
|
||||
|
||||
Writing plugins is no more complex than writing regular JavaScript. If you know the basic concept of [callback functions](https://developer.mozilla.org/en-US/docs/Glossary/Callback_function) or how [spread syntax](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax) works, and are up to speed with Payload concepts, then writing a plugin will be a breeze.
|
||||
|
||||
<Banner type="success">
|
||||
Because we rely on a simple config-based structure, Payload plugins simply take in a user's
|
||||
existing config and return a modified config with new fields, hooks, collections, admin views, or
|
||||
Because we rely on a simple config-based structure, Payload Plugins simply take in an
|
||||
existing config and returns a _modified_ config with new fields, hooks, collections, admin views, or
|
||||
anything else you can think of.
|
||||
</Banner>
|
||||
|
||||
Writing plugins is no more complex than writing regular JavaScript. If you know how [spread syntax](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax) works and are up to speed with Payload concepts, writing a plugin will be a breeze.
|
||||
|
||||
**Example use cases:**
|
||||
|
||||
- Automatically sync data from a specific collection to HubSpot or a similar CRM when data is added or changes
|
||||
@@ -27,37 +29,49 @@ Writing plugins is no more complex than writing regular JavaScript. If you know
|
||||
- Integrate all `upload`-enabled collections with a third-party file host like S3 or Cloudinary
|
||||
- Add custom endpoints or GraphQL queries / mutations with any type of custom functionality that you can think of
|
||||
|
||||
## How to install plugins
|
||||
## Official Plugins
|
||||
|
||||
The base Payload config allows for a `plugins` property which takes an `array` of [`Plugins`](https://github.com/payloadcms/payload/blob/main/packages/payload/src/config/types.ts).
|
||||
Payload maintains a set of Official Plugins that solve for some of the common use cases. These plugins are maintained by the Payload team and its contributors and are guaranteed to be stable and up-to-date.
|
||||
|
||||
```js
|
||||
- [Form Builder](./form-builder)
|
||||
- [Nested Docs](./nested-docs)
|
||||
- [Redirects](./redirects)
|
||||
- [Search](./search)
|
||||
- [Sentry](./sentry)
|
||||
- [SEO](./seo)
|
||||
- [Stripe](./stripe)
|
||||
|
||||
You can also [build your own plugin](./build-your-own) to easily extend Payload's functionality in some other way. Once your plugin is ready, consider [sharing it with the community](#community-plugins).
|
||||
|
||||
Plugins are changing every day, so be sure to check back often to see what new plugins may have been added. If you have a specific plugin you would like to see, please feel free to start a new [Discussion](https://github.com/payloadcms/payload/discussions).
|
||||
|
||||
<Banner type="warning">
|
||||
For a complete list of Official Plugins, visit the [Packages Directory](https://github.com/payloadcms/payload/tree/main/packages) of the [Payload Monorepo](https://github.com/payloadcms/payload).
|
||||
</Banner>
|
||||
|
||||
## Community Plugins
|
||||
|
||||
Community Plugins are those that are maintained entirely by outside contributors. They are a great way to share your work across the ecosystem for others to use. You can discover Community Plugins by browsing the `payload-plugin` topic on [GitHub](https://github.com/topics/payload-plugin).
|
||||
|
||||
Some plugins have become so widely used that they are adopted as an [Official Plugin](#official-plugin), such as the [Lexical Plugin](https://github.com/AlessioGr/payload-plugin-lexical). If you have a plugin that you think should be an Official Plugin, please feel free to start a new [Discussion](https://github.com/payloadcms/payload/discussions).
|
||||
|
||||
<Banner type="warning">
|
||||
For maintainers building plugins for others to use, please add the `payload-plugin` topic on [GitHub](https://github.com/topics/payload-plugin) to help others find it.
|
||||
</Banner>
|
||||
|
||||
## Installing Plugins
|
||||
|
||||
The base [Payload Config](../configuration/overview) allows for a `plugins` property which takes an `array` of [Plugin Configs](./build-your-own).
|
||||
|
||||
```ts
|
||||
import { buildConfig } from 'payload/config'
|
||||
// note: these plugins are not real (yet?)
|
||||
import addLastModified from 'payload-add-last-modified'
|
||||
import passwordProtect from 'payload-password-protect'
|
||||
import { mongooseAdapter } from '@payloadcms/db-mongodb'
|
||||
import { postgresAdapter } from '@payloadcms/db-postgres'
|
||||
|
||||
const config = buildConfig({
|
||||
collections: [
|
||||
{
|
||||
slug: 'pages',
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'content',
|
||||
type: 'richText',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
db: mongooseAdapter({}) // or postgresAdapter({})
|
||||
// ...
|
||||
// highlight-start
|
||||
plugins: [
|
||||
// Many plugins require options to be passed.
|
||||
// In the following example, we call the function
|
||||
@@ -72,20 +86,15 @@ const config = buildConfig({
|
||||
// To understand how to use the plugins you're interested in,
|
||||
// consult their corresponding documentation
|
||||
],
|
||||
// highlight-end
|
||||
})
|
||||
|
||||
export default config
|
||||
```
|
||||
|
||||
### When Plugins are initialized
|
||||
<Banner type="warning">
|
||||
Payload Plugins are executed _after_ the incoming config is validated, but before it is sanitized and has had default options merged in. After all plugins are executed, the full config with all plugins will be sanitized.
|
||||
</Banner>
|
||||
|
||||
Payload Plugins are executed _after_ the incoming config is validated, but before it is sanitized and had default options merged in.
|
||||
|
||||
After all plugins are executed, the full config with all plugins will be sanitized.
|
||||
|
||||
## Simple example
|
||||
|
||||
Here is an example for how to automatically add a `lastModifiedBy` field to all Payload collections using a Plugin written in TypeScript.
|
||||
Here is an example what the `addLastModified` plugin from above might look like. It adds a `lastModifiedBy` field to all Payload collections. For full details, see [how to build your own plugin](./build-your-own).
|
||||
|
||||
```ts
|
||||
import { Config, Plugin } from 'payload/config'
|
||||
@@ -136,9 +145,3 @@ const addLastModified: Plugin = (incomingConfig: Config): Config => {
|
||||
|
||||
export default addLastModified
|
||||
```
|
||||
|
||||
## Available Plugins
|
||||
|
||||
You can discover existing plugins by browsing the `payload-plugin` topic on [GitHub](https://github.com/topics/payload-plugin).
|
||||
|
||||
For maintainers building plugins for others to use, please add the topic to help others find it. If you would like one to be built by the core Payload team, [open a Feature Request](https://github.com/payloadcms/payload/discussions) in our GitHub Discussions board. We would be happy to review your code and maybe feature you and your plugin where appropriate.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "payload-monorepo",
|
||||
"version": "3.0.0-beta.54",
|
||||
"version": "3.0.0-beta.55",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "create-payload-app",
|
||||
"version": "3.0.0-beta.54",
|
||||
"version": "3.0.0-beta.55",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-mongodb",
|
||||
"version": "3.0.0-beta.54",
|
||||
"version": "3.0.0-beta.55",
|
||||
"description": "The officially supported MongoDB database adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-postgres",
|
||||
"version": "3.0.0-beta.54",
|
||||
"version": "3.0.0-beta.55",
|
||||
"description": "The officially supported Postgres database adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/email-nodemailer",
|
||||
"version": "3.0.0-beta.54",
|
||||
"version": "3.0.0-beta.55",
|
||||
"description": "Payload Nodemailer Email Adapter",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/email-resend",
|
||||
"version": "3.0.0-beta.54",
|
||||
"version": "3.0.0-beta.55",
|
||||
"description": "Payload Resend Email Adapter",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/graphql",
|
||||
"version": "3.0.0-beta.54",
|
||||
"version": "3.0.0-beta.55",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
import type { Collection } from 'payload'
|
||||
|
||||
import { isolateObjectProperty, meOperation } from 'payload'
|
||||
import { extractJWT, isolateObjectProperty, meOperation } from 'payload'
|
||||
|
||||
import type { Context } from '../types.js'
|
||||
|
||||
function meResolver(collection: Collection): any {
|
||||
async function resolver(_, args, context: Context) {
|
||||
const currentToken = extractJWT(context.req)
|
||||
|
||||
const options = {
|
||||
collection,
|
||||
currentToken,
|
||||
depth: 0,
|
||||
req: isolateObjectProperty(context.req, 'transactionID'),
|
||||
}
|
||||
|
||||
const result = await meOperation(options)
|
||||
|
||||
if (collection.config.auth.removeTokenFromResponses) {
|
||||
|
||||
@@ -379,6 +379,9 @@ function initCollectionsGraphQL({ config, graphqlResult }: InitCollectionsGraphQ
|
||||
exp: {
|
||||
type: GraphQLInt,
|
||||
},
|
||||
strategy: {
|
||||
type: GraphQLString,
|
||||
},
|
||||
token: {
|
||||
type: GraphQLString,
|
||||
},
|
||||
@@ -405,6 +408,9 @@ function initCollectionsGraphQL({ config, graphqlResult }: InitCollectionsGraphQ
|
||||
refreshedToken: {
|
||||
type: GraphQLString,
|
||||
},
|
||||
strategy: {
|
||||
type: GraphQLString,
|
||||
},
|
||||
user: {
|
||||
type: collection.graphQL.JWT,
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/live-preview-react",
|
||||
"version": "3.0.0-beta.54",
|
||||
"version": "3.0.0-beta.55",
|
||||
"description": "The official live preview React SDK for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/live-preview",
|
||||
"version": "3.0.0-beta.54",
|
||||
"version": "3.0.0-beta.55",
|
||||
"description": "The official live preview JavaScript SDK for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/next",
|
||||
"version": "3.0.0-beta.54",
|
||||
"version": "3.0.0-beta.55",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -35,10 +35,10 @@ export const getFile = async ({ collection, filename, req }: Args): Promise<Resp
|
||||
|
||||
if (accessResult instanceof Response) return accessResult
|
||||
|
||||
let response: Response = null
|
||||
if (collection.config.upload.handlers?.length) {
|
||||
let customResponse = null
|
||||
for (const handler of collection.config.upload.handlers) {
|
||||
response = await handler(req, {
|
||||
customResponse = await handler(req, {
|
||||
doc: accessResult,
|
||||
params: {
|
||||
collection: collection.config.slug,
|
||||
@@ -47,21 +47,21 @@ export const getFile = async ({ collection, filename, req }: Args): Promise<Resp
|
||||
})
|
||||
}
|
||||
|
||||
if (response instanceof Response) return response
|
||||
if (customResponse instanceof Response) return customResponse
|
||||
}
|
||||
|
||||
const fileDir = collection.config.upload?.staticDir || collection.config.slug
|
||||
const filePath = path.resolve(`${fileDir}/${filename}`)
|
||||
|
||||
const stats = await fsPromises.stat(filePath)
|
||||
|
||||
const data = streamFile(filePath)
|
||||
|
||||
const headers = new Headers(req.headers)
|
||||
headers.set('Content-Length', stats.size + '')
|
||||
|
||||
const fileTypeResult = (await fileTypeFromFile(filePath)) || getFileTypeFallback(filePath)
|
||||
|
||||
let headers = new Headers()
|
||||
headers.set('Content-Type', fileTypeResult.mime)
|
||||
headers.set('Content-Length', stats.size + '')
|
||||
headers = collection.config.upload?.modifyResponseHeaders
|
||||
? collection.config.upload.modifyResponseHeaders({ headers })
|
||||
: headers
|
||||
|
||||
return new Response(data, {
|
||||
headers: headersWithCors({
|
||||
|
||||
@@ -59,7 +59,7 @@ export const DefaultEditView: React.FC = () => {
|
||||
const config = useConfig()
|
||||
const router = useRouter()
|
||||
const { dispatchFormQueryParams } = useFormQueryParams()
|
||||
const { getFieldMap } = useComponentMap()
|
||||
const { getComponentMap, getFieldMap } = useComponentMap()
|
||||
const params = useSearchParams()
|
||||
const depth = useEditDepth()
|
||||
const { reportUpdate } = useDocumentEvents()
|
||||
@@ -81,6 +81,10 @@ export const DefaultEditView: React.FC = () => {
|
||||
|
||||
const entitySlug = collectionConfig?.slug || globalConfig?.slug
|
||||
|
||||
const componentMap = getComponentMap({
|
||||
collectionSlug: collectionConfig?.slug,
|
||||
globalSlug: globalConfig?.slug,
|
||||
})
|
||||
const fieldMap = getFieldMap({
|
||||
collectionSlug: collectionConfig?.slug,
|
||||
globalSlug: globalConfig?.slug,
|
||||
@@ -234,11 +238,15 @@ export const DefaultEditView: React.FC = () => {
|
||||
)}
|
||||
{upload && (
|
||||
<React.Fragment>
|
||||
<Upload
|
||||
collectionSlug={collectionConfig.slug}
|
||||
initialState={initialState}
|
||||
uploadConfig={upload}
|
||||
/>
|
||||
{componentMap.Upload !== undefined ? (
|
||||
componentMap.Upload
|
||||
) : (
|
||||
<Upload
|
||||
collectionSlug={collectionConfig.slug}
|
||||
initialState={initialState}
|
||||
uploadConfig={upload}
|
||||
/>
|
||||
)}
|
||||
</React.Fragment>
|
||||
)}
|
||||
</Fragment>
|
||||
|
||||
@@ -73,7 +73,14 @@ export const RootPage = async ({
|
||||
|
||||
const routeWithAdmin = `${adminRoute}${createFirstUserRoute}`
|
||||
|
||||
if (!dbHasUser && currentRoute !== routeWithAdmin) {
|
||||
const collectionConfig = config.collections.find(({ slug }) => slug === userSlug)
|
||||
const disableLocalStrategy = collectionConfig?.auth?.disableLocalStrategy
|
||||
|
||||
if (disableLocalStrategy && currentRoute === routeWithAdmin) {
|
||||
redirect(adminRoute)
|
||||
}
|
||||
|
||||
if (!dbHasUser && currentRoute !== routeWithAdmin && !disableLocalStrategy) {
|
||||
redirect(routeWithAdmin)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "payload",
|
||||
"version": "3.0.0-beta.54",
|
||||
"version": "3.0.0-beta.55",
|
||||
"description": "Node, React, Headless CMS and Application Framework built on Next.js",
|
||||
"keywords": [
|
||||
"admin panel",
|
||||
|
||||
3
packages/payload/src/admin/elements/Upload.ts
Normal file
3
packages/payload/src/admin/elements/Upload.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import type { CustomComponent } from '../../config/types.js'
|
||||
|
||||
export type CustomUpload = CustomComponent
|
||||
@@ -14,6 +14,7 @@ export type {
|
||||
DocumentTabConfig,
|
||||
DocumentTabProps,
|
||||
} from './elements/Tab.js'
|
||||
export type { CustomUpload } from './elements/Upload.js'
|
||||
export type {
|
||||
WithServerSidePropsComponent,
|
||||
WithServerSidePropsComponentProps,
|
||||
|
||||
@@ -7,6 +7,7 @@ import type { ClientUser, User } from '../types.js'
|
||||
export type MeOperationResult = {
|
||||
collection?: string
|
||||
exp?: number
|
||||
strategy?: string
|
||||
token?: string
|
||||
user?: ClientUser
|
||||
}
|
||||
@@ -49,6 +50,7 @@ export const meOperation = async ({
|
||||
|
||||
result = {
|
||||
collection: req.user.collection,
|
||||
strategy: req.user._strategy,
|
||||
user,
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import { getFieldsToSign } from '../getFieldsToSign.js'
|
||||
export type Result = {
|
||||
exp: number
|
||||
refreshedToken: string
|
||||
strategy?: string
|
||||
user: Document
|
||||
}
|
||||
|
||||
@@ -88,6 +89,7 @@ export const refreshOperation = async (incomingArgs: Arguments): Promise<Result>
|
||||
let result: Result = {
|
||||
exp,
|
||||
refreshedToken,
|
||||
strategy: args.req.user._strategy,
|
||||
user,
|
||||
}
|
||||
|
||||
|
||||
@@ -8,11 +8,6 @@ import {
|
||||
} from '../../config/shared/componentSchema.js'
|
||||
import { openGraphSchema } from '../../config/shared/openGraphSchema.js'
|
||||
|
||||
const strategyBaseSchema = joi.object().keys({
|
||||
logout: joi.boolean(),
|
||||
refresh: joi.boolean(),
|
||||
})
|
||||
|
||||
const collectionSchema = joi.object().keys({
|
||||
slug: joi.string().required(),
|
||||
access: joi.object({
|
||||
@@ -36,6 +31,7 @@ const collectionSchema = joi.object().keys({
|
||||
PublishButton: componentSchema,
|
||||
SaveButton: componentSchema,
|
||||
SaveDraftButton: componentSchema,
|
||||
Upload: componentSchema,
|
||||
}),
|
||||
views: joi.object({
|
||||
Edit: joi.alternatives().try(
|
||||
@@ -184,6 +180,7 @@ const collectionSchema = joi.object().keys({
|
||||
.unknown(),
|
||||
),
|
||||
mimeTypes: joi.array().items(joi.string()),
|
||||
modifyResponseHeaders: joi.func(),
|
||||
resizeOptions: joi
|
||||
.object()
|
||||
.keys({
|
||||
|
||||
@@ -6,6 +6,7 @@ import type {
|
||||
CustomPublishButton,
|
||||
CustomSaveButton,
|
||||
CustomSaveDraftButton,
|
||||
CustomUpload,
|
||||
} from '../../admin/types.js'
|
||||
import type { Auth, ClientUser, IncomingAuthType } from '../../auth/types.js'
|
||||
import type {
|
||||
@@ -254,6 +255,11 @@ export type CollectionAdminOptions = {
|
||||
* + autosave must be disabled
|
||||
*/
|
||||
SaveDraftButton?: CustomSaveDraftButton
|
||||
/**
|
||||
* Replaces the "Upload" section
|
||||
* + upload must be enabled
|
||||
*/
|
||||
Upload?: CustomUpload
|
||||
}
|
||||
views?: {
|
||||
/**
|
||||
|
||||
@@ -124,8 +124,11 @@ export type UploadConfig = {
|
||||
*/
|
||||
handlers?: ((
|
||||
req: PayloadRequestWithData,
|
||||
args: { doc: TypeWithID; params: { collection: string; filename: string } },
|
||||
) => Promise<Response> | Response | null)[]
|
||||
args: {
|
||||
doc: TypeWithID
|
||||
params: { collection: string; filename: string }
|
||||
},
|
||||
) => Promise<Response> | Promise<void> | Response | void)[]
|
||||
imageSizes?: ImageSize[]
|
||||
/**
|
||||
* Restrict mimeTypes in the file picker. Array of valid mime types or mimetype wildcards
|
||||
@@ -133,6 +136,11 @@ export type UploadConfig = {
|
||||
* @default undefined
|
||||
*/
|
||||
mimeTypes?: string[]
|
||||
/**
|
||||
* Ability to modify the response headers fetching a file.
|
||||
* @default undefined
|
||||
*/
|
||||
modifyResponseHeaders?: ({ headers }: { headers: Headers }) => Headers
|
||||
/**
|
||||
* Sharp resize options for the original image.
|
||||
* @link https://sharp.pixelplumbing.com/api-resize#resize
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-cloud-storage",
|
||||
"version": "3.0.0-beta.54",
|
||||
"version": "3.0.0-beta.55",
|
||||
"description": "The official cloud storage plugin for Payload CMS",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-cloud",
|
||||
"version": "3.0.0-beta.54",
|
||||
"version": "3.0.0-beta.55",
|
||||
"description": "The official Payload Cloud plugin",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -41,7 +41,7 @@ export type GenerateURL = (args: {
|
||||
export type StaticHandler = (
|
||||
req: PayloadRequestWithData,
|
||||
args: { params: { collection: string; filename: string } },
|
||||
) => Promise<Response> | Response
|
||||
) => Promise<Response> | Promise<void> | Response | void
|
||||
|
||||
export interface PayloadCloudEmailOptions {
|
||||
apiKey: string
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-form-builder",
|
||||
"version": "3.0.0-beta.54",
|
||||
"version": "3.0.0-beta.55",
|
||||
"description": "Form builder plugin for Payload CMS",
|
||||
"keywords": [
|
||||
"payload",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-nested-docs",
|
||||
"version": "3.0.0-beta.54",
|
||||
"version": "3.0.0-beta.55",
|
||||
"description": "The official Nested Docs plugin for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-redirects",
|
||||
"version": "3.0.0-beta.54",
|
||||
"version": "3.0.0-beta.55",
|
||||
"description": "Redirects plugin for Payload",
|
||||
"keywords": [
|
||||
"payload",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-relationship-object-ids",
|
||||
"version": "3.0.0-beta.54",
|
||||
"version": "3.0.0-beta.55",
|
||||
"description": "A Payload plugin to store all relationship IDs as ObjectIDs",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-search",
|
||||
"version": "3.0.0-beta.54",
|
||||
"version": "3.0.0-beta.55",
|
||||
"description": "Search plugin for Payload",
|
||||
"keywords": [
|
||||
"payload",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-seo",
|
||||
"version": "3.0.0-beta.54",
|
||||
"version": "3.0.0-beta.55",
|
||||
"description": "SEO plugin for Payload",
|
||||
"keywords": [
|
||||
"payload",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-stripe",
|
||||
"version": "3.0.0-beta.54",
|
||||
"version": "3.0.0-beta.55",
|
||||
"description": "Stripe plugin for Payload",
|
||||
"keywords": [
|
||||
"payload",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/richtext-lexical",
|
||||
"version": "3.0.0-beta.54",
|
||||
"version": "3.0.0-beta.55",
|
||||
"description": "The officially supported Lexical richtext adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
/* eslint-disable perfectionist/sort-exports */
|
||||
'use client'
|
||||
|
||||
export { slashMenuBasicGroupWithItems } from '../../features/shared/slashMenu/basicGroup.js'
|
||||
|
||||
export { RichTextCell } from '../../cell/index.js'
|
||||
export { AlignFeatureClient } from '../../features/align/feature.client.js'
|
||||
export { BlockquoteFeatureClient } from '../../features/blockquote/feature.client.js'
|
||||
@@ -11,7 +13,6 @@ export { TreeViewFeatureClient } from '../../features/debug/treeView/feature.cli
|
||||
export { BoldFeatureClient } from '../../features/format/bold/feature.client.js'
|
||||
export { InlineCodeFeatureClient } from '../../features/format/inlineCode/feature.client.js'
|
||||
export { ItalicFeatureClient } from '../../features/format/italic/feature.client.js'
|
||||
export { toolbarFormatGroupWithItems } from '../../features/format/shared/toolbarFormatGroup.js'
|
||||
export { StrikethroughFeatureClient } from '../../features/format/strikethrough/feature.client.js'
|
||||
export { SubscriptFeatureClient } from '../../features/format/subscript/feature.client.js'
|
||||
export { SuperscriptFeatureClient } from '../../features/format/superscript/feature.client.js'
|
||||
@@ -29,6 +30,7 @@ export { ParagraphFeatureClient } from '../../features/paragraph/feature.client.
|
||||
|
||||
export { RelationshipFeatureClient } from '../../features/relationship/feature.client.js'
|
||||
|
||||
export { toolbarFormatGroupWithItems } from '../../features/format/shared/toolbarFormatGroup.js'
|
||||
export { toolbarAddDropdownGroupWithItems } from '../../features/shared/toolbar/addDropdownGroup.js'
|
||||
export { toolbarFeatureButtonsGroupWithItems } from '../../features/shared/toolbar/featureButtonsGroup.js'
|
||||
export { toolbarTextDropdownGroupWithItems } from '../../features/shared/toolbar/textDropdownGroup.js'
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import type { SerializedQuoteNode as _SerializedQuoteNode } from '@lexical/rich-text'
|
||||
import type { Spread } from 'lexical'
|
||||
|
||||
import { QuoteNode } from '@lexical/rich-text'
|
||||
|
||||
// eslint-disable-next-line payload/no-imports-from-exports-dir
|
||||
@@ -8,6 +11,13 @@ import { createNode } from '../typeUtilities.js'
|
||||
import { i18n } from './i18n.js'
|
||||
import { MarkdownTransformer } from './markdownTransformer.js'
|
||||
|
||||
export type SerializedQuoteNode = Spread<
|
||||
{
|
||||
type: 'quote'
|
||||
},
|
||||
_SerializedQuoteNode
|
||||
>
|
||||
|
||||
export const BlockquoteFeature = createServerFeature({
|
||||
feature: {
|
||||
ClientFeature: BlockquoteFeatureClient,
|
||||
|
||||
@@ -30,7 +30,9 @@ const BlockComponent = React.lazy(() =>
|
||||
|
||||
export type SerializedBlockNode = Spread<
|
||||
{
|
||||
children?: never // required so that our typed editor state doesn't automatically add children
|
||||
fields: BlockFields
|
||||
type: 'block'
|
||||
},
|
||||
SerializedDecoratorBlockNode
|
||||
>
|
||||
@@ -102,7 +104,7 @@ export class BlockNode extends DecoratorBlockNode {
|
||||
exportJSON(): SerializedBlockNode {
|
||||
return {
|
||||
...super.exportJSON(),
|
||||
type: this.getType(),
|
||||
type: 'block',
|
||||
fields: this.getFields(),
|
||||
version: 2,
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ export type HTMLConverterFeatureProps = {
|
||||
}
|
||||
|
||||
// This is just used to save the props on the richText field
|
||||
export const HTMLConverterFeature = createServerFeature({
|
||||
export const HTMLConverterFeature = createServerFeature<HTMLConverterFeatureProps>({
|
||||
feature: {},
|
||||
key: 'htmlConverter',
|
||||
})
|
||||
|
||||
@@ -50,13 +50,13 @@ export const i18n: Partial<GenericLanguages> = {
|
||||
label: '[SURAT]\n\nKepala {{headingLevel}}',
|
||||
},
|
||||
nb: {
|
||||
label: 'Overskrift {{overskriftsnivå}}',
|
||||
label: 'Overskrift {{headingLevel}}',
|
||||
},
|
||||
nl: {
|
||||
label: 'Kop {{headingLevel}}',
|
||||
},
|
||||
pl: {
|
||||
label: 'Nagłówek {{poziom nagłówka}}',
|
||||
label: 'Nagłówek {{headingLevel}}',
|
||||
},
|
||||
pt: {
|
||||
label: 'Cabeçalho {{headingLevel}}',
|
||||
|
||||
@@ -6,6 +6,7 @@ import type {
|
||||
LexicalCommand,
|
||||
LexicalNode,
|
||||
SerializedLexicalNode,
|
||||
Spread,
|
||||
} from 'lexical'
|
||||
|
||||
import { addClassNamesToElement } from '@lexical/utils'
|
||||
@@ -21,7 +22,13 @@ const HorizontalRuleComponent = React.lazy(() =>
|
||||
/**
|
||||
* Serialized representation of a horizontal rule node. Serialized = converted to JSON. This is what is stored in the database / in the lexical editor state.
|
||||
*/
|
||||
export type SerializedHorizontalRuleNode = SerializedLexicalNode
|
||||
export type SerializedHorizontalRuleNode = Spread<
|
||||
{
|
||||
children?: never // required so that our typed editor state doesn't automatically add children
|
||||
type: 'horizontalrule'
|
||||
},
|
||||
SerializedLexicalNode
|
||||
>
|
||||
|
||||
export const INSERT_HORIZONTAL_RULE_COMMAND: LexicalCommand<void> = createCommand(
|
||||
'INSERT_HORIZONTAL_RULE_COMMAND',
|
||||
|
||||
@@ -41,10 +41,16 @@ export class AutoLinkNode extends LinkNode {
|
||||
return node
|
||||
}
|
||||
|
||||
// @ts-expect-error
|
||||
exportJSON(): SerializedAutoLinkNode {
|
||||
const serialized = super.exportJSON()
|
||||
return {
|
||||
...super.exportJSON(),
|
||||
type: 'autolink',
|
||||
children: serialized.children,
|
||||
direction: serialized.direction,
|
||||
fields: serialized.fields,
|
||||
format: serialized.format,
|
||||
indent: serialized.indent,
|
||||
version: 2,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,7 +129,7 @@ export class LinkNode extends ElementNode {
|
||||
exportJSON(): SerializedLinkNode {
|
||||
const returnObject: SerializedLinkNode = {
|
||||
...super.exportJSON(),
|
||||
type: this.getType(),
|
||||
type: 'link',
|
||||
fields: this.getFields(),
|
||||
version: 3,
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { SerializedElementNode, Spread } from 'lexical'
|
||||
import type { SerializedElementNode, SerializedLexicalNode, Spread } from 'lexical'
|
||||
|
||||
export type LinkFields = {
|
||||
// unknown, custom fields:
|
||||
@@ -18,11 +18,14 @@ export type LinkFields = {
|
||||
url: string
|
||||
}
|
||||
|
||||
export type SerializedLinkNode = Spread<
|
||||
export type SerializedLinkNode<T extends SerializedLexicalNode = SerializedLexicalNode> = Spread<
|
||||
{
|
||||
fields: LinkFields
|
||||
id?: string // optional if AutoLinkNode
|
||||
type: 'link'
|
||||
},
|
||||
SerializedElementNode
|
||||
SerializedElementNode<T>
|
||||
>
|
||||
export type SerializedAutoLinkNode = Omit<SerializedLinkNode, 'id'>
|
||||
export type SerializedAutoLinkNode<T extends SerializedLexicalNode = SerializedLexicalNode> = {
|
||||
type: 'autolink'
|
||||
} & Omit<SerializedLinkNode<T>, 'id' | 'type'>
|
||||
|
||||
@@ -26,7 +26,7 @@ export const ChecklistFeature = createServerFeature({
|
||||
}),
|
||||
createNode({
|
||||
converters: {
|
||||
html: ListItemHTMLConverter,
|
||||
html: ListItemHTMLConverter as any,
|
||||
},
|
||||
node: ListItemNode,
|
||||
}),
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import type { SerializedListItemNode, SerializedListNode } from '@lexical/list'
|
||||
|
||||
import { ListItemNode, ListNode } from '@lexical/list'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
|
||||
import type { HTMLConverter } from '../converters/html/converter/types.js'
|
||||
import type { SerializedListItemNode, SerializedListNode } from './plugin/index.js'
|
||||
|
||||
import { convertLexicalNodesToHTML } from '../converters/html/converter/index.js'
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ export const OrderedListFeature = createServerFeature({
|
||||
}),
|
||||
createNode({
|
||||
converters: {
|
||||
html: ListItemHTMLConverter,
|
||||
html: ListItemHTMLConverter as any,
|
||||
},
|
||||
node: ListItemNode,
|
||||
}),
|
||||
|
||||
@@ -1,9 +1,31 @@
|
||||
'use client'
|
||||
import type {
|
||||
SerializedListItemNode as _SerializedListItemNode,
|
||||
SerializedListNode as _SerializedListNode,
|
||||
} from '@lexical/list'
|
||||
import type { Spread } from 'lexical'
|
||||
|
||||
import { ListPlugin } from '@lexical/react/LexicalListPlugin.js'
|
||||
import React from 'react'
|
||||
|
||||
import type { PluginComponent } from '../../typesClient.js'
|
||||
|
||||
export type SerializedListItemNode = Spread<
|
||||
{
|
||||
checked?: boolean
|
||||
type: 'listitem'
|
||||
},
|
||||
_SerializedListItemNode
|
||||
>
|
||||
|
||||
export type SerializedListNode = Spread<
|
||||
{
|
||||
checked?: boolean
|
||||
type: 'list'
|
||||
},
|
||||
_SerializedListNode
|
||||
>
|
||||
|
||||
export const LexicalListPlugin: PluginComponent<undefined> = () => {
|
||||
return <ListPlugin />
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ export const UnorderedListFeature = createServerFeature({
|
||||
}),
|
||||
createNode({
|
||||
converters: {
|
||||
html: ListItemHTMLConverter,
|
||||
html: ListItemHTMLConverter as any,
|
||||
},
|
||||
node: ListItemNode,
|
||||
}),
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
import type { SerializedListNode } from '@lexical/list'
|
||||
|
||||
import type { SerializedListNode } from '../../../../../lists/plugin/index.js'
|
||||
import type { LexicalPluginNodeConverter } from '../../types.js'
|
||||
|
||||
import { convertLexicalPluginNodesToLexical } from '../../index.js'
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { SerializedListItemNode } from '@lexical/list'
|
||||
|
||||
import type { SerializedListItemNode } from '../../../../../lists/plugin/index.js'
|
||||
import type { LexicalPluginNodeConverter } from '../../types.js'
|
||||
|
||||
import { convertLexicalPluginNodesToLexical } from '../../index.js'
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { SerializedQuoteNode } from '@lexical/rich-text'
|
||||
|
||||
import type { SerializedQuoteNode } from '../../../../../blockquote/feature.server.js'
|
||||
import type { LexicalPluginNodeConverter } from '../../types.js'
|
||||
|
||||
import { convertLexicalPluginNodesToLexical } from '../../index.js'
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { SerializedQuoteNode } from '@lexical/rich-text'
|
||||
|
||||
import type { SerializedQuoteNode } from '../../../../../blockquote/feature.server.js'
|
||||
import type { SlateNodeConverter } from '../../types.js'
|
||||
|
||||
import { convertSlateNodesToLexical } from '../../index.js'
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { SerializedListItemNode } from '@lexical/list'
|
||||
|
||||
import type { SerializedListItemNode } from '../../../../../lists/plugin/index.js'
|
||||
import type { SlateNodeConverter } from '../../types.js'
|
||||
|
||||
import { convertSlateNodesToLexical } from '../../index.js'
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { SerializedListNode } from '@lexical/list'
|
||||
|
||||
import type { SerializedListNode } from '../../../../../lists/plugin/index.js'
|
||||
import type { SlateNodeConverter } from '../../types.js'
|
||||
|
||||
import { convertSlateNodesToLexical } from '../../index.js'
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { SerializedListNode } from '@lexical/list'
|
||||
|
||||
import type { SerializedListNode } from '../../../../../lists/plugin/index.js'
|
||||
import type { SlateNodeConverter } from '../../types.js'
|
||||
|
||||
import { convertSlateNodesToLexical } from '../../index.js'
|
||||
|
||||
@@ -18,7 +18,12 @@ export type SlateToLexicalFeatureProps = {
|
||||
| SlateNodeConverterProvider[]
|
||||
}
|
||||
|
||||
export const SlateToLexicalFeature = createServerFeature<SlateToLexicalFeatureProps>({
|
||||
export const SlateToLexicalFeature = createServerFeature<
|
||||
SlateToLexicalFeatureProps,
|
||||
{
|
||||
converters?: SlateNodeConverterProvider[]
|
||||
}
|
||||
>({
|
||||
feature: ({ props }) => {
|
||||
if (!props) {
|
||||
props = {}
|
||||
@@ -56,7 +61,9 @@ export const SlateToLexicalFeature = createServerFeature<SlateToLexicalFeaturePr
|
||||
node: UnknownConvertedNode,
|
||||
},
|
||||
],
|
||||
sanitizedServerFeatureProps: props,
|
||||
sanitizedServerFeatureProps: {
|
||||
converters,
|
||||
},
|
||||
}
|
||||
},
|
||||
key: 'slateToLexical',
|
||||
|
||||
@@ -10,6 +10,7 @@ import type {
|
||||
NodeKey,
|
||||
Spread,
|
||||
} from 'lexical'
|
||||
import type { CollectionSlug } from 'payload'
|
||||
import type { JSX } from 'react'
|
||||
|
||||
import { DecoratorBlockNode } from '@lexical/react/LexicalDecoratorBlockNode.js'
|
||||
@@ -22,11 +23,14 @@ const RelationshipComponent = React.lazy(() =>
|
||||
)
|
||||
|
||||
export type RelationshipData = {
|
||||
relationTo: string
|
||||
relationTo: CollectionSlug
|
||||
value: number | string
|
||||
}
|
||||
|
||||
export type SerializedRelationshipNode = Spread<RelationshipData, SerializedDecoratorBlockNode>
|
||||
export type SerializedRelationshipNode = Spread<RelationshipData, SerializedDecoratorBlockNode> & {
|
||||
children?: never // required so that our typed editor state doesn't automatically add children
|
||||
type: 'relationship'
|
||||
}
|
||||
|
||||
function $relationshipElementToNode(domNode: HTMLDivElement): DOMConversionOutput | null {
|
||||
const id = domNode.getAttribute('data-lexical-relationship-id')
|
||||
@@ -129,7 +133,7 @@ export class RelationshipNode extends DecoratorBlockNode {
|
||||
return {
|
||||
...super.exportJSON(),
|
||||
...this.getData(),
|
||||
type: this.getType(),
|
||||
type: 'relationship',
|
||||
version: 2,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,17 +6,44 @@ import type { EditorConfigContextType } from '../../lexical/config/client/Editor
|
||||
|
||||
export type ToolbarGroup =
|
||||
| {
|
||||
ChildComponent?: React.FC
|
||||
/**
|
||||
* All toolbar items part of this toolbar group need to be added here.
|
||||
*/
|
||||
items: Array<ToolbarGroupItem>
|
||||
/**
|
||||
* Each toolbar group needs to have a unique key. Groups with the same keys will have their items merged together.
|
||||
*/
|
||||
key: string
|
||||
/**
|
||||
* Determines where the toolbar group will be.
|
||||
*/
|
||||
order?: number
|
||||
type: 'dropdown'
|
||||
/**
|
||||
* Controls the toolbar group type. Set to `buttons` to create a buttons toolbar group, which displays toolbar items horizontally using only their icons.
|
||||
*/
|
||||
type: 'buttons'
|
||||
}
|
||||
| {
|
||||
/**
|
||||
* The dropdown toolbar ChildComponent allows you to pass in a React Component which will be displayed within the dropdown button.
|
||||
*/
|
||||
ChildComponent?: React.FC
|
||||
/**
|
||||
* All toolbar items part of this toolbar group need to be added here.
|
||||
*/
|
||||
items: Array<ToolbarGroupItem>
|
||||
/**
|
||||
* Each toolbar group needs to have a unique key. Groups with the same keys will have their items merged together.
|
||||
*/
|
||||
key: string
|
||||
/**
|
||||
* Determines where the toolbar group will be.
|
||||
*/
|
||||
order?: number
|
||||
type: 'buttons'
|
||||
/**
|
||||
* Controls the toolbar group type. Set to `dropdown` to create a buttons toolbar group, which displays toolbar items vertically using their icons and labels, if the dropdown is open.
|
||||
*/
|
||||
type: 'dropdown'
|
||||
}
|
||||
|
||||
export type ToolbarGroupItem = {
|
||||
|
||||
@@ -154,9 +154,9 @@ export type ResolvedClientFeature<ClientFeatureProps> = ClientFeature<ClientFeat
|
||||
order: number
|
||||
}
|
||||
|
||||
export type ResolvedClientFeatureMap = Map<string, ResolvedClientFeature<unknown>>
|
||||
export type ResolvedClientFeatureMap = Map<string, ResolvedClientFeature<any>>
|
||||
|
||||
export type ClientFeatureProviderMap = Map<string, FeatureProviderClient<unknown, unknown>>
|
||||
export type ClientFeatureProviderMap = Map<string, FeatureProviderClient<any, any>>
|
||||
|
||||
/**
|
||||
* Plugins are react components which get added to the editor. You can use them to interact with lexical, e.g. to create a command which creates a node, or opens a modal, or some other more "outside" functionality
|
||||
|
||||
@@ -220,6 +220,10 @@ export type BeforeValidateNodeHook<T extends SerializedLexicalNode> = (
|
||||
|
||||
// Define the node with hooks that use the node's exportJSON return type
|
||||
export type NodeWithHooks<T extends LexicalNode = any> = {
|
||||
/**
|
||||
* Allows you to define how a node can be serialized into different formats. Currently, only supports html.
|
||||
* Markdown converters are defined in `markdownTransformers` and not here.
|
||||
*/
|
||||
converters?: {
|
||||
html?: HTMLConverter<ReturnType<ReplaceAny<T, LexicalNode>['exportJSON']>>
|
||||
}
|
||||
@@ -231,13 +235,25 @@ export type NodeWithHooks<T extends LexicalNode = any> = {
|
||||
node: ReturnType<ReplaceAny<T, LexicalNode>['exportJSON']>
|
||||
req: PayloadRequestWithData
|
||||
}) => Field[] | null
|
||||
/**
|
||||
* If a node includes sub-fields, the sub-fields data needs to be returned here, alongside `getSubFields` which returns their schema.
|
||||
*/
|
||||
getSubFieldsData?: (args: {
|
||||
node: ReturnType<ReplaceAny<T, LexicalNode>['exportJSON']>
|
||||
req: PayloadRequestWithData
|
||||
}) => Record<string, unknown>
|
||||
/**
|
||||
* Allows you to run population logic when a node's data was requested from graphQL.
|
||||
* While `getSubFields` and `getSubFieldsData` automatically handle populating sub-fields (since they run hooks on them), those are only populated in the Rest API.
|
||||
* This is because the Rest API hooks do not have access to the 'depth' property provided by graphQL.
|
||||
* In order for them to be populated correctly in graphQL, the population logic needs to be provided here.
|
||||
*/
|
||||
graphQLPopulationPromises?: Array<
|
||||
PopulationPromise<ReturnType<ReplaceAny<T, LexicalNode>['exportJSON']>>
|
||||
>
|
||||
/**
|
||||
* Just like payload fields, you can provide hooks which are run for this specific node. These are called Node Hooks.
|
||||
*/
|
||||
hooks?: {
|
||||
afterChange?: Array<AfterChangeNodeHook<ReturnType<ReplaceAny<T, LexicalNode>['exportJSON']>>>
|
||||
afterRead?: Array<AfterReadNodeHook<ReturnType<ReplaceAny<T, LexicalNode>['exportJSON']>>>
|
||||
@@ -246,7 +262,14 @@ export type NodeWithHooks<T extends LexicalNode = any> = {
|
||||
BeforeValidateNodeHook<ReturnType<ReplaceAny<T, LexicalNode>['exportJSON']>>
|
||||
>
|
||||
}
|
||||
/**
|
||||
* The actual lexical node needs to be provided here. This also supports [lexical node replacements](https://lexical.dev/docs/concepts/node-replacement).
|
||||
*/
|
||||
node: Klass<T> | LexicalNodeReplacement
|
||||
/**
|
||||
* This allows you to provide node validations, which are run when your document is being validated, alongside other payload fields.
|
||||
* You can use it to throw a validation error for a specific node in case its data is incorrect.
|
||||
*/
|
||||
validations?: Array<NodeValidation<ReturnType<ReplaceAny<T, LexicalNode>['exportJSON']>>>
|
||||
}
|
||||
|
||||
@@ -333,12 +356,12 @@ export type ResolvedServerFeature<ServerProps, ClientFeatureProps> = ServerFeatu
|
||||
order: number
|
||||
}
|
||||
|
||||
export type ResolvedServerFeatureMap = Map<string, ResolvedServerFeature<unknown, unknown>>
|
||||
export type ResolvedServerFeatureMap = Map<string, ResolvedServerFeature<any, any>>
|
||||
|
||||
export type ServerFeatureProviderMap = Map<string, FeatureProviderServer<unknown, unknown, unknown>>
|
||||
export type ServerFeatureProviderMap = Map<string, FeatureProviderServer<any, any, any>>
|
||||
|
||||
export type SanitizedServerFeatures = Required<
|
||||
Pick<ResolvedServerFeature<unknown, unknown>, 'i18n' | 'markdownTransformers' | 'nodes'>
|
||||
Pick<ResolvedServerFeature<any, any>, 'i18n' | 'markdownTransformers' | 'nodes'>
|
||||
> & {
|
||||
/** The node types mapped to their converters */
|
||||
converters: {
|
||||
|
||||
@@ -8,6 +8,7 @@ import type {
|
||||
NodeKey,
|
||||
Spread,
|
||||
} from 'lexical'
|
||||
import type { CollectionSlug } from 'payload'
|
||||
import type { JSX } from 'react'
|
||||
|
||||
import { DecoratorBlockNode } from '@lexical/react/LexicalDecoratorBlockNode.js'
|
||||
@@ -25,7 +26,7 @@ export type UploadData = {
|
||||
[key: string]: unknown
|
||||
}
|
||||
id: string
|
||||
relationTo: string
|
||||
relationTo: CollectionSlug
|
||||
value: number | string
|
||||
}
|
||||
|
||||
@@ -66,7 +67,10 @@ function $convertUploadElement(domNode: HTMLImageElement): DOMConversionOutput |
|
||||
return null
|
||||
}
|
||||
|
||||
export type SerializedUploadNode = Spread<UploadData, SerializedDecoratorBlockNode>
|
||||
export type SerializedUploadNode = Spread<UploadData, SerializedDecoratorBlockNode> & {
|
||||
children?: never // required so that our typed editor state doesn't automatically add children
|
||||
type: 'upload'
|
||||
}
|
||||
|
||||
export class UploadNode extends DecoratorBlockNode {
|
||||
__data: UploadData
|
||||
@@ -148,7 +152,7 @@ export class UploadNode extends DecoratorBlockNode {
|
||||
return {
|
||||
...super.exportJSON(),
|
||||
...this.getData(),
|
||||
type: this.getType(),
|
||||
type: 'upload',
|
||||
version: 3,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,7 +83,7 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte
|
||||
}
|
||||
}
|
||||
|
||||
let features: FeatureProviderServer<unknown, unknown, unknown>[] = []
|
||||
let features: FeatureProviderServer<any, any, any>[] = []
|
||||
let resolvedFeatureMap: ResolvedServerFeatureMap
|
||||
|
||||
let finalSanitizedEditorConfig: SanitizedServerEditorConfig // For server only
|
||||
@@ -811,11 +811,7 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte
|
||||
export { AlignFeature } from './features/align/feature.server.js'
|
||||
export { BlockquoteFeature } from './features/blockquote/feature.server.js'
|
||||
export { BlocksFeature, type BlocksFeatureProps } from './features/blocks/feature.server.js'
|
||||
export {
|
||||
type BlockFields,
|
||||
BlockNode,
|
||||
type SerializedBlockNode,
|
||||
} from './features/blocks/nodes/BlocksNode.js'
|
||||
export { type BlockFields, BlockNode } from './features/blocks/nodes/BlocksNode.js'
|
||||
|
||||
export { LinebreakHTMLConverter } from './features/converters/html/converter/converters/linebreak.js'
|
||||
export { ParagraphHTMLConverter } from './features/converters/html/converter/converters/paragraph.js'
|
||||
@@ -850,11 +846,7 @@ export { LinkFeature, type LinkFeatureServerProps } from './features/link/featur
|
||||
|
||||
export { AutoLinkNode } from './features/link/nodes/AutoLinkNode.js'
|
||||
export { LinkNode } from './features/link/nodes/LinkNode.js'
|
||||
export type {
|
||||
LinkFields,
|
||||
SerializedAutoLinkNode,
|
||||
SerializedLinkNode,
|
||||
} from './features/link/nodes/types.js'
|
||||
export type { LinkFields } from './features/link/nodes/types.js'
|
||||
export { ChecklistFeature } from './features/lists/checklist/feature.server.js'
|
||||
export { OrderedListFeature } from './features/lists/orderedList/feature.server.js'
|
||||
export { UnorderedListFeature } from './features/lists/unorderedList/feature.server.js'
|
||||
@@ -889,7 +881,6 @@ export {
|
||||
export {
|
||||
type RelationshipData,
|
||||
RelationshipNode,
|
||||
type SerializedRelationshipNode,
|
||||
} from './features/relationship/nodes/RelationshipNode.js'
|
||||
|
||||
export { FixedToolbarFeature } from './features/toolbars/fixed/feature.server.js'
|
||||
@@ -936,11 +927,7 @@ export type {
|
||||
export { UploadFeature } from './features/upload/feature.server.js'
|
||||
|
||||
export type { UploadFeatureProps } from './features/upload/feature.server.js'
|
||||
export {
|
||||
type SerializedUploadNode,
|
||||
type UploadData,
|
||||
UploadNode,
|
||||
} from './features/upload/nodes/UploadNode.js'
|
||||
export { type UploadData, UploadNode } from './features/upload/nodes/UploadNode.js'
|
||||
|
||||
export type { EditorConfigContextType } from './lexical/config/client/EditorConfigProvider.js'
|
||||
export {
|
||||
@@ -990,3 +977,6 @@ export { defaultRichTextValue } from './populateGraphQL/defaultValue.js'
|
||||
export type { LexicalEditorProps, LexicalRichTextAdapter } from './types.js'
|
||||
|
||||
export { createServerFeature } from './utilities/createServerFeature.js'
|
||||
export { migrateSlateToLexical } from './utilities/migrateSlateToLexical/index.js'
|
||||
|
||||
export * from './nodeTypes.js'
|
||||
|
||||
@@ -30,7 +30,7 @@ export const defaultEditorLexicalConfig: LexicalEditorConfig = {
|
||||
theme: LexicalEditorTheme,
|
||||
}
|
||||
|
||||
export const defaultEditorFeatures: FeatureProviderServer<unknown, unknown, unknown>[] = [
|
||||
export const defaultEditorFeatures: FeatureProviderServer<any, any, any>[] = [
|
||||
BoldFeature(),
|
||||
ItalicFeature(),
|
||||
UnderlineFeature(),
|
||||
|
||||
@@ -13,7 +13,7 @@ import type {
|
||||
import type { LexicalFieldAdminProps } from '../../types.js'
|
||||
|
||||
export type ServerEditorConfig = {
|
||||
features: FeatureProviderServer<unknown, unknown, unknown>[]
|
||||
features: FeatureProviderServer<any, any, any>[]
|
||||
lexical?: LexicalEditorConfig
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ export type SanitizedServerEditorConfig = {
|
||||
}
|
||||
|
||||
export type ClientEditorConfig = {
|
||||
features: FeatureProviderClient<unknown, unknown>[]
|
||||
features: FeatureProviderClient<any, any>[]
|
||||
lexical?: LexicalEditorConfig
|
||||
}
|
||||
|
||||
|
||||
@@ -9,9 +9,9 @@ export type SlashMenuItem = {
|
||||
/** Each slash menu item needs to have a unique key. The key will be matched when typing, displayed if no `label` property is set, and used for classNames. */
|
||||
key: string
|
||||
/**
|
||||
* Keywords are used in order to match the item for different texts typed after the '/'.
|
||||
* Keywords are used to match the item for different texts typed after the '/'.
|
||||
* E.g. you might want to show a horizontal rule item if you type both /hr, /separator, /horizontal etc.
|
||||
* Additionally to the keywords, the label and key will be used to match the correct slash menu item.
|
||||
* In addition to the keywords, the label and key will be used to find the right slash menu item.
|
||||
*/
|
||||
keywords?: Array<string>
|
||||
/** The label will be displayed in your slash menu item. In order to make use of i18n, this can be a function. */
|
||||
@@ -21,9 +21,15 @@ export type SlashMenuItem = {
|
||||
}
|
||||
|
||||
export type SlashMenuGroup = {
|
||||
/**
|
||||
* An array of `SlashMenuItem`'s which will be displayed in the slash menu.
|
||||
*/
|
||||
items: Array<SlashMenuItem>
|
||||
// Used for class names and, if label is not provided, for display.
|
||||
/**
|
||||
* Used for class names and, if label is not provided, for display. Slash menus with the same key will have their items merged together.
|
||||
*/
|
||||
key: string
|
||||
/** The label will be displayed before your Slash Menu group. In order to make use of i18n, this can be a function. */
|
||||
label?: (({ i18n }: { i18n: I18nClient<{}, string> }) => string) | string
|
||||
}
|
||||
|
||||
|
||||
48
packages/richtext-lexical/src/nodeTypes.ts
Normal file
48
packages/richtext-lexical/src/nodeTypes.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import type {
|
||||
SerializedEditorState,
|
||||
SerializedElementNode,
|
||||
SerializedLexicalNode,
|
||||
Spread,
|
||||
TextModeType,
|
||||
} from 'lexical'
|
||||
export type { SerializedQuoteNode } from './features/blockquote/feature.server.js'
|
||||
export type { SerializedBlockNode } from './features/blocks/nodes/BlocksNode.js'
|
||||
export type { SerializedHorizontalRuleNode } from './features/horizontalRule/nodes/HorizontalRuleNode.js'
|
||||
|
||||
export type { SerializedAutoLinkNode, SerializedLinkNode } from './features/link/nodes/types.js'
|
||||
|
||||
export type { SerializedListItemNode, SerializedListNode } from './features/lists/plugin/index.js'
|
||||
|
||||
export type { SerializedRelationshipNode } from './features/relationship/nodes/RelationshipNode.js'
|
||||
|
||||
export type { SerializedUploadNode } from './features/upload/nodes/UploadNode.js'
|
||||
|
||||
export type SerializedParagraphNode<T extends SerializedLexicalNode = SerializedLexicalNode> =
|
||||
Spread<
|
||||
{
|
||||
textFormat: number
|
||||
type: 'paragraph'
|
||||
},
|
||||
SerializedElementNode<T>
|
||||
>
|
||||
export type SerializedTextNode = Spread<
|
||||
{
|
||||
children?: never // required so that our typed editor state doesn't automatically add children
|
||||
detail: number
|
||||
format: number
|
||||
mode: TextModeType
|
||||
style: string
|
||||
text: string
|
||||
type: 'text'
|
||||
},
|
||||
SerializedLexicalNode
|
||||
>
|
||||
|
||||
type RecursiveNodes<T extends SerializedLexicalNode, Depth extends number = 4> = Depth extends 0
|
||||
? T
|
||||
: { children?: RecursiveNodes<T, DecrementDepth<Depth>>[] } & T
|
||||
|
||||
type DecrementDepth<N extends number> = [0, 0, 1, 2, 3, 4][N]
|
||||
|
||||
export type TypedEditorState<T extends SerializedLexicalNode = SerializedLexicalNode> =
|
||||
SerializedEditorState<RecursiveNodes<T>>
|
||||
@@ -39,7 +39,7 @@ export type LexicalEditorProps = {
|
||||
defaultFeatures: FeatureProviderServer<any, any, any>[]
|
||||
/**
|
||||
* This array contains all features that are enabled in the root richText editor (the one defined in the payload.config.ts).
|
||||
* If this field is the root richText editor, or if the root richText editor is not a lexical editor, this array will be empty
|
||||
* If this field is the root richText editor, or if the root richText editor is not a lexical editor, this array will be empty.
|
||||
*
|
||||
* @Example
|
||||
*
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
import type { CollectionConfig, Field, GlobalConfig, Payload } from 'payload'
|
||||
|
||||
import { migrateDocumentFields } from './recurse.js'
|
||||
|
||||
/**
|
||||
* This goes through every single collection and field in the payload config, and migrates its data from Slate to Lexical. This does not support sub-fields within slate.
|
||||
*
|
||||
* It will only translate fields fulfilling all these requirements:
|
||||
* - field schema uses lexical editor
|
||||
* - lexical editor has SlateToLexicalFeature added
|
||||
* - saved field data is in Slate format
|
||||
*
|
||||
* @param payload
|
||||
*/
|
||||
export async function migrateSlateToLexical({ payload }: { payload: Payload }) {
|
||||
const collections = payload.config.collections
|
||||
|
||||
const allLocales = payload.config.localization ? payload.config.localization.localeCodes : [null]
|
||||
|
||||
const totalCollections = collections.length
|
||||
for (const locale of allLocales) {
|
||||
let curCollection = 0
|
||||
for (const collection of collections) {
|
||||
curCollection++
|
||||
await migrateCollection({
|
||||
collection,
|
||||
cur: curCollection,
|
||||
locale,
|
||||
max: totalCollections,
|
||||
payload,
|
||||
})
|
||||
}
|
||||
for (const global of payload.config.globals) {
|
||||
await migrateGlobal({
|
||||
global,
|
||||
locale,
|
||||
payload,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function migrateGlobal({
|
||||
global,
|
||||
locale,
|
||||
payload,
|
||||
}: {
|
||||
global: GlobalConfig
|
||||
locale: null | string
|
||||
payload: Payload
|
||||
}) {
|
||||
console.log(`SlateToLexical: ${locale}: Migrating global:`, global.slug)
|
||||
|
||||
const document = await payload.findGlobal({
|
||||
slug: global.slug,
|
||||
depth: 0,
|
||||
locale: locale || undefined,
|
||||
overrideAccess: true,
|
||||
})
|
||||
|
||||
const found = migrateDocument({
|
||||
document,
|
||||
fields: global.fields,
|
||||
})
|
||||
|
||||
if (found) {
|
||||
await payload.updateGlobal({
|
||||
slug: global.slug,
|
||||
data: document,
|
||||
depth: 0,
|
||||
locale: locale || undefined,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async function migrateCollection({
|
||||
collection,
|
||||
cur,
|
||||
locale,
|
||||
max,
|
||||
payload,
|
||||
}: {
|
||||
collection: CollectionConfig
|
||||
cur: number
|
||||
locale: null | string
|
||||
max: number
|
||||
payload: Payload
|
||||
}) {
|
||||
console.log(
|
||||
`SlateToLexical: ${locale}: Migrating collection:`,
|
||||
collection.slug,
|
||||
'(' + cur + '/' + max + ')',
|
||||
)
|
||||
|
||||
const documentCount = (
|
||||
await payload.count({
|
||||
collection: collection.slug,
|
||||
depth: 0,
|
||||
locale: locale || undefined,
|
||||
})
|
||||
).totalDocs
|
||||
|
||||
let page = 1
|
||||
let migrated = 0
|
||||
|
||||
while (migrated < documentCount) {
|
||||
const documents = await payload.find({
|
||||
collection: collection.slug,
|
||||
depth: 0,
|
||||
locale: locale || undefined,
|
||||
overrideAccess: true,
|
||||
page,
|
||||
pagination: true,
|
||||
})
|
||||
|
||||
for (const document of documents.docs) {
|
||||
migrated++
|
||||
console.log(
|
||||
`SlateToLexical: ${locale}: Migrating collection:`,
|
||||
collection.slug,
|
||||
'(' +
|
||||
cur +
|
||||
'/' +
|
||||
max +
|
||||
') - Migrating Document: ' +
|
||||
document.id +
|
||||
' (' +
|
||||
migrated +
|
||||
'/' +
|
||||
documentCount +
|
||||
')',
|
||||
)
|
||||
const found = migrateDocument({
|
||||
document,
|
||||
fields: collection.fields,
|
||||
})
|
||||
|
||||
if (found) {
|
||||
await payload.update({
|
||||
id: document.id,
|
||||
collection: collection.slug,
|
||||
data: document,
|
||||
depth: 0,
|
||||
locale: locale || undefined,
|
||||
})
|
||||
}
|
||||
}
|
||||
page++
|
||||
}
|
||||
}
|
||||
|
||||
function migrateDocument({
|
||||
document,
|
||||
fields,
|
||||
}: {
|
||||
document: Record<string, unknown>
|
||||
fields: Field[]
|
||||
}): boolean {
|
||||
return !!migrateDocumentFields({
|
||||
data: document,
|
||||
fields,
|
||||
found: 0,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
import type { Field } from 'payload'
|
||||
|
||||
import { fieldAffectsData, fieldHasSubFields, fieldIsArrayType } from 'payload/shared'
|
||||
|
||||
import type {
|
||||
SlateNodeConverter,
|
||||
SlateNodeConverterProvider,
|
||||
} from '../../features/migrations/slateToLexical/converter/types.js'
|
||||
import type { LexicalRichTextAdapter } from '../../types.js'
|
||||
|
||||
import { convertSlateToLexical } from '../../features/migrations/slateToLexical/converter/index.js'
|
||||
|
||||
type NestedRichTextFieldsArgs = {
|
||||
data: unknown
|
||||
|
||||
fields: Field[]
|
||||
found: number
|
||||
}
|
||||
|
||||
export const migrateDocumentFields = ({
|
||||
data,
|
||||
fields,
|
||||
found,
|
||||
}: NestedRichTextFieldsArgs): number => {
|
||||
for (const field of fields) {
|
||||
if (fieldHasSubFields(field) && !fieldIsArrayType(field)) {
|
||||
if (fieldAffectsData(field) && typeof data[field.name] === 'object') {
|
||||
migrateDocumentFields({
|
||||
data: data[field.name],
|
||||
fields: field.fields,
|
||||
found,
|
||||
})
|
||||
} else {
|
||||
migrateDocumentFields({
|
||||
data,
|
||||
found,
|
||||
|
||||
fields: field.fields,
|
||||
})
|
||||
}
|
||||
} else if (field.type === 'tabs') {
|
||||
field.tabs.forEach((tab) => {
|
||||
migrateDocumentFields({
|
||||
data,
|
||||
found,
|
||||
|
||||
fields: tab.fields,
|
||||
})
|
||||
})
|
||||
} else if (Array.isArray(data[field.name])) {
|
||||
if (field.type === 'blocks') {
|
||||
data[field.name].forEach((row, i) => {
|
||||
const block = field.blocks.find(({ slug }) => slug === row?.blockType)
|
||||
if (block) {
|
||||
migrateDocumentFields({
|
||||
data: data[field.name][i],
|
||||
found,
|
||||
|
||||
fields: block.fields,
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (field.type === 'array') {
|
||||
data[field.name].forEach((_, i) => {
|
||||
migrateDocumentFields({
|
||||
data: data[field.name][i],
|
||||
found,
|
||||
|
||||
fields: field.fields,
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (field.type === 'richText' && Array.isArray(data[field.name])) {
|
||||
// Slate richText
|
||||
const editor: LexicalRichTextAdapter = field.editor as LexicalRichTextAdapter
|
||||
if (editor && typeof editor === 'object') {
|
||||
if ('features' in editor && editor.features?.length) {
|
||||
// find slatetolexical feature
|
||||
const slateToLexicalFeature = editor.editorConfig.resolvedFeatureMap.get('slateToLexical')
|
||||
if (slateToLexicalFeature) {
|
||||
// DO CONVERSION
|
||||
|
||||
const converterProviders = (
|
||||
slateToLexicalFeature.sanitizedServerFeatureProps as {
|
||||
converters?: SlateNodeConverterProvider[]
|
||||
}
|
||||
).converters
|
||||
|
||||
const converters: SlateNodeConverter[] = []
|
||||
|
||||
for (const converter of converterProviders) {
|
||||
converters.push(converter.converter)
|
||||
}
|
||||
|
||||
data[field.name] = convertSlateToLexical({
|
||||
converters,
|
||||
slateData: data[field.name],
|
||||
})
|
||||
|
||||
found++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return found
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/richtext-slate",
|
||||
"version": "3.0.0-beta.54",
|
||||
"version": "3.0.0-beta.55",
|
||||
"description": "The officially supported Slate richtext adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/storage-azure",
|
||||
"version": "3.0.0-beta.54",
|
||||
"version": "3.0.0-beta.55",
|
||||
"description": "Payload storage adapter for Azure Blob Storage",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/storage-gcs",
|
||||
"version": "3.0.0-beta.54",
|
||||
"version": "3.0.0-beta.55",
|
||||
"description": "Payload storage adapter for Google Cloud Storage",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/storage-s3",
|
||||
"version": "3.0.0-beta.54",
|
||||
"version": "3.0.0-beta.55",
|
||||
"description": "Payload storage adapter for Amazon S3",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/storage-uploadthing",
|
||||
"version": "3.0.0-beta.54",
|
||||
"version": "3.0.0-beta.55",
|
||||
"description": "Payload storage adapter for uploadthing",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/storage-vercel-blob",
|
||||
"version": "3.0.0-beta.54",
|
||||
"version": "3.0.0-beta.55",
|
||||
"description": "Payload storage adapter for Vercel Blob Storage",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/translations",
|
||||
"version": "3.0.0-beta.54",
|
||||
"version": "3.0.0-beta.55",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/ui",
|
||||
"version": "3.0.0-beta.54",
|
||||
"version": "3.0.0-beta.55",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
'use client'
|
||||
import { isImage } from 'payload/shared'
|
||||
import React from 'react'
|
||||
|
||||
import { UploadActions } from '../../elements/Upload/index.js'
|
||||
@@ -13,11 +12,12 @@ const baseClass = 'file-details'
|
||||
import type { Data, FileSizes, SanitizedCollectionConfig } from 'payload'
|
||||
|
||||
export type FileDetailsProps = {
|
||||
canEdit?: boolean
|
||||
collectionSlug: string
|
||||
customUploadActions?: React.ReactNode[]
|
||||
doc: Data & {
|
||||
sizes?: FileSizes
|
||||
}
|
||||
enableAdjustments?: boolean
|
||||
handleRemove?: () => void
|
||||
hasImageSizes?: boolean
|
||||
imageCacheTag?: string
|
||||
@@ -25,8 +25,16 @@ export type FileDetailsProps = {
|
||||
}
|
||||
|
||||
export const FileDetails: React.FC<FileDetailsProps> = (props) => {
|
||||
const { canEdit, collectionSlug, doc, handleRemove, hasImageSizes, imageCacheTag, uploadConfig } =
|
||||
props
|
||||
const {
|
||||
collectionSlug,
|
||||
customUploadActions,
|
||||
doc,
|
||||
enableAdjustments,
|
||||
handleRemove,
|
||||
hasImageSizes,
|
||||
imageCacheTag,
|
||||
uploadConfig,
|
||||
} = props
|
||||
|
||||
const { id, filename, filesize, height, mimeType, thumbnailURL, url, width } = doc
|
||||
|
||||
@@ -52,9 +60,12 @@ export const FileDetails: React.FC<FileDetailsProps> = (props) => {
|
||||
width={width as number}
|
||||
/>
|
||||
|
||||
{isImage(mimeType as string) && mimeType !== 'image/svg+xml' && (
|
||||
<UploadActions canEdit={canEdit} showSizePreviews={hasImageSizes && doc.filename} />
|
||||
)}
|
||||
<UploadActions
|
||||
customActions={customUploadActions}
|
||||
enableAdjustments={enableAdjustments}
|
||||
enablePreviewSizes={hasImageSizes && doc.filename}
|
||||
mimeType={mimeType}
|
||||
/>
|
||||
</div>
|
||||
{handleRemove && (
|
||||
<Button
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
background-color: var(--theme-bg);
|
||||
}
|
||||
|
||||
&__file-mutation {
|
||||
&__upload-actions {
|
||||
display: flex;
|
||||
gap: calc(var(--base) / 2);
|
||||
flex-wrap: wrap;
|
||||
|
||||
@@ -33,34 +33,60 @@ const validate = (value) => {
|
||||
return true
|
||||
}
|
||||
|
||||
export const UploadActions = ({ canEdit, showSizePreviews }) => {
|
||||
type UploadActionsArgs = {
|
||||
customActions?: React.ReactNode[]
|
||||
enableAdjustments: boolean
|
||||
enablePreviewSizes: boolean
|
||||
mimeType: string
|
||||
}
|
||||
|
||||
export const UploadActions = ({
|
||||
customActions,
|
||||
enableAdjustments,
|
||||
enablePreviewSizes,
|
||||
mimeType,
|
||||
}: UploadActionsArgs) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const fileTypeIsAdjustable = isImage(mimeType) && mimeType !== 'image/svg+xml'
|
||||
|
||||
if (!fileTypeIsAdjustable && (!customActions || customActions.length === 0)) return null
|
||||
|
||||
return (
|
||||
<div className={`${baseClass}__file-mutation`}>
|
||||
{showSizePreviews && (
|
||||
<DrawerToggler className={`${baseClass}__previewSizes`} slug={sizePreviewSlug}>
|
||||
{t('upload:previewSizes')}
|
||||
</DrawerToggler>
|
||||
)}
|
||||
{canEdit && (
|
||||
<DrawerToggler className={`${baseClass}__edit`} slug={editDrawerSlug}>
|
||||
{t('upload:editImage')}
|
||||
</DrawerToggler>
|
||||
<div className={`${baseClass}__upload-actions`}>
|
||||
{fileTypeIsAdjustable && (
|
||||
<React.Fragment>
|
||||
{enablePreviewSizes && (
|
||||
<DrawerToggler className={`${baseClass}__previewSizes`} slug={sizePreviewSlug}>
|
||||
{t('upload:previewSizes')}
|
||||
</DrawerToggler>
|
||||
)}
|
||||
{enableAdjustments && (
|
||||
<DrawerToggler className={`${baseClass}__edit`} slug={editDrawerSlug}>
|
||||
{t('upload:editImage')}
|
||||
</DrawerToggler>
|
||||
)}
|
||||
</React.Fragment>
|
||||
)}
|
||||
|
||||
{customActions &&
|
||||
customActions.map((CustomAction, i) => {
|
||||
return <React.Fragment key={i}>{CustomAction}</React.Fragment>
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export type UploadProps = {
|
||||
collectionSlug: string
|
||||
customActions?: React.ReactNode[]
|
||||
initialState?: FormState
|
||||
onChange?: (file?: File) => void
|
||||
updatedAt?: string
|
||||
uploadConfig: SanitizedCollectionConfig['upload']
|
||||
}
|
||||
|
||||
export const Upload: React.FC<UploadProps> = (props) => {
|
||||
const { collectionSlug, initialState, onChange, updatedAt, uploadConfig } = props
|
||||
const { collectionSlug, customActions, initialState, onChange, uploadConfig } = props
|
||||
|
||||
const [replacingFile, setReplacingFile] = useState(false)
|
||||
const [fileSrc, setFileSrc] = useState<null | string>(null)
|
||||
@@ -75,29 +101,52 @@ export const Upload: React.FC<UploadProps> = (props) => {
|
||||
})
|
||||
const [_crop, setCrop] = useState({ x: 0, y: 0 })
|
||||
|
||||
const handleFileChange = React.useCallback(
|
||||
(newFile: File) => {
|
||||
if (newFile instanceof File) {
|
||||
const fileReader = new FileReader()
|
||||
fileReader.onload = (e) => {
|
||||
const imgSrc = e.target?.result
|
||||
|
||||
if (typeof imgSrc === 'string') {
|
||||
setFileSrc(imgSrc)
|
||||
}
|
||||
}
|
||||
fileReader.readAsDataURL(newFile)
|
||||
}
|
||||
|
||||
setValue(newFile)
|
||||
|
||||
if (typeof onChange === 'function') {
|
||||
onChange(newFile)
|
||||
}
|
||||
},
|
||||
[onChange, setValue],
|
||||
)
|
||||
|
||||
const handleFileNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const updatedFileName = e.target.value
|
||||
if (value) {
|
||||
const fileValue = value
|
||||
// Creating a new File object with updated properties
|
||||
const newFile = new File([fileValue], updatedFileName, { type: fileValue.type })
|
||||
setValue(newFile) // Updating the state with the new File object
|
||||
handleFileChange(newFile)
|
||||
}
|
||||
}
|
||||
|
||||
const handleFileSelection = React.useCallback(
|
||||
(files: FileList) => {
|
||||
const fileToUpload = files?.[0]
|
||||
setValue(fileToUpload)
|
||||
handleFileChange(fileToUpload)
|
||||
},
|
||||
[setValue],
|
||||
[handleFileChange],
|
||||
)
|
||||
|
||||
const handleFileRemoval = useCallback(() => {
|
||||
setReplacingFile(true)
|
||||
setValue(null)
|
||||
handleFileChange(null)
|
||||
setFileSrc('')
|
||||
}, [setValue])
|
||||
}, [handleFileChange])
|
||||
|
||||
const onEditsSave = React.useCallback(
|
||||
({ crop, focalPosition }) => {
|
||||
@@ -128,24 +177,6 @@ export const Upload: React.FC<UploadProps> = (props) => {
|
||||
setReplacingFile(false)
|
||||
}, [initialState])
|
||||
|
||||
useEffect(() => {
|
||||
if (value instanceof File) {
|
||||
const fileReader = new FileReader()
|
||||
fileReader.onload = (e) => {
|
||||
const imgSrc = e.target?.result
|
||||
|
||||
if (typeof imgSrc === 'string') {
|
||||
setFileSrc(imgSrc)
|
||||
}
|
||||
}
|
||||
fileReader.readAsDataURL(value)
|
||||
}
|
||||
|
||||
if (typeof onChange === 'function') {
|
||||
onChange(value)
|
||||
}
|
||||
}, [value, onChange, updatedAt])
|
||||
|
||||
const canRemoveUpload =
|
||||
docPermissions?.update?.permission &&
|
||||
'delete' in docPermissions &&
|
||||
@@ -165,9 +196,9 @@ export const Upload: React.FC<UploadProps> = (props) => {
|
||||
<FieldError message={errorMessage} showError={showError} />
|
||||
{doc.filename && !replacingFile && (
|
||||
<FileDetails
|
||||
canEdit={showCrop || showFocalPoint}
|
||||
collectionSlug={collectionSlug}
|
||||
doc={doc}
|
||||
enableAdjustments={showCrop || showFocalPoint}
|
||||
handleRemove={canRemoveUpload ? handleFileRemoval : undefined}
|
||||
hasImageSizes={hasImageSizes}
|
||||
imageCacheTag={doc.updatedAt}
|
||||
@@ -184,7 +215,7 @@ export const Upload: React.FC<UploadProps> = (props) => {
|
||||
/>
|
||||
)}
|
||||
|
||||
{value && (
|
||||
{value && fileSrc && (
|
||||
<React.Fragment>
|
||||
<div className={`${baseClass}__thumbnail-wrap`}>
|
||||
<Thumbnail
|
||||
@@ -199,13 +230,12 @@ export const Upload: React.FC<UploadProps> = (props) => {
|
||||
type="text"
|
||||
value={value.name}
|
||||
/>
|
||||
|
||||
{isImage(value.type) && value.type !== 'image/svg+xml' && (
|
||||
<UploadActions
|
||||
canEdit={showCrop || showFocalPoint}
|
||||
showSizePreviews={hasImageSizes && doc.filename && !replacingFile}
|
||||
/>
|
||||
)}
|
||||
<UploadActions
|
||||
customActions={customActions}
|
||||
enableAdjustments={showCrop || showFocalPoint}
|
||||
enablePreviewSizes={hasImageSizes && doc.filename && !replacingFile}
|
||||
mimeType={value.type}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
buttonStyle="icon-label"
|
||||
|
||||
@@ -25,6 +25,7 @@ const baseClass = 'upload'
|
||||
export type UploadInputProps = Omit<UploadFieldProps, 'filterOptions'> & {
|
||||
api?: string
|
||||
collection?: ClientCollectionConfig
|
||||
customUploadActions?: React.ReactNode[]
|
||||
filterOptions?: FilterOptionsResult
|
||||
onChange?: (e) => void
|
||||
relationTo?: UploadField['relationTo']
|
||||
@@ -41,6 +42,7 @@ export const UploadInput: React.FC<UploadInputProps> = (props) => {
|
||||
api = '/api',
|
||||
className,
|
||||
collection,
|
||||
customUploadActions,
|
||||
descriptionProps,
|
||||
errorProps,
|
||||
filterOptions,
|
||||
@@ -147,6 +149,7 @@ export const UploadInput: React.FC<UploadInputProps> = (props) => {
|
||||
{fileDoc && !missingFile && (
|
||||
<FileDetails
|
||||
collectionSlug={relationTo}
|
||||
customUploadActions={customUploadActions}
|
||||
doc={fileDoc}
|
||||
handleRemove={
|
||||
readOnly
|
||||
|
||||
@@ -23,7 +23,9 @@ export type AuthContext<T = ClientUser> = {
|
||||
refreshPermissions: () => Promise<void>
|
||||
setPermissions: (permissions: Permissions) => void
|
||||
setUser: (user: T) => void
|
||||
strategy?: string
|
||||
token?: string
|
||||
tokenExpiration?: number
|
||||
user?: T | null
|
||||
}
|
||||
|
||||
@@ -36,6 +38,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
const [user, setUser] = useState<ClientUser | null>()
|
||||
const [tokenInMemory, setTokenInMemory] = useState<string>()
|
||||
const [tokenExpiration, setTokenExpiration] = useState<number>()
|
||||
const [strategy, setStrategy] = useState<string>()
|
||||
const pathname = usePathname()
|
||||
const router = useRouter()
|
||||
// const { code } = useLocale()
|
||||
@@ -76,6 +79,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
const revokeTokenAndExpire = useCallback(() => {
|
||||
setTokenInMemory(undefined)
|
||||
setTokenExpiration(undefined)
|
||||
setStrategy(undefined)
|
||||
}, [])
|
||||
|
||||
const setTokenAndExpiration = useCallback(
|
||||
@@ -84,6 +88,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
if (token && json?.exp) {
|
||||
setTokenInMemory(token)
|
||||
setTokenExpiration(json.exp)
|
||||
if (json.strategy) setStrategy(json.strategy)
|
||||
} else {
|
||||
revokeTokenAndExpire()
|
||||
}
|
||||
@@ -258,6 +263,8 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
searchParams,
|
||||
admin,
|
||||
revokeTokenAndExpire,
|
||||
strategy,
|
||||
tokenExpiration,
|
||||
loginRoute,
|
||||
])
|
||||
|
||||
|
||||
@@ -102,6 +102,10 @@ export const mapCollections = (args: {
|
||||
<WithServerSideProps Component={PublishButtonComponent} />
|
||||
) : undefined
|
||||
|
||||
const UploadComponent = collectionConfig?.admin?.components?.edit?.Upload
|
||||
|
||||
const Upload = UploadComponent ? <WithServerSideProps Component={UploadComponent} /> : undefined
|
||||
|
||||
const beforeList = collectionConfig?.admin?.components?.BeforeList
|
||||
|
||||
const BeforeList =
|
||||
@@ -171,6 +175,7 @@ export const mapCollections = (args: {
|
||||
PublishButton,
|
||||
SaveButton,
|
||||
SaveDraftButton,
|
||||
Upload,
|
||||
actionsMap: mapActions({
|
||||
WithServerSideProps,
|
||||
collectionConfig,
|
||||
|
||||
@@ -112,6 +112,7 @@ export const mapGlobals = ({
|
||||
PublishButton: PublishButtonComponent,
|
||||
SaveButton: SaveButtonComponent,
|
||||
SaveDraftButton: SaveDraftButtonComponent,
|
||||
Upload: null,
|
||||
actionsMap: mapActions({
|
||||
WithServerSideProps,
|
||||
globalConfig,
|
||||
|
||||
@@ -111,6 +111,7 @@ export type ConfigComponentMapBase = {
|
||||
PublishButton: React.ReactNode
|
||||
SaveButton: React.ReactNode
|
||||
SaveDraftButton: React.ReactNode
|
||||
Upload: React.ReactNode
|
||||
actionsMap: ActionMap
|
||||
fieldMap: FieldMap
|
||||
isPreviewEnabled: boolean
|
||||
|
||||
@@ -8,123 +8,124 @@
|
||||
|
||||
export interface Config {
|
||||
collections: {
|
||||
posts: Post
|
||||
users: User
|
||||
'payload-preferences': PayloadPreference
|
||||
'payload-migrations': PayloadMigration
|
||||
}
|
||||
posts: Post;
|
||||
users: User;
|
||||
'payload-preferences': PayloadPreference;
|
||||
'payload-migrations': PayloadMigration;
|
||||
};
|
||||
globals: {
|
||||
menu: Menu
|
||||
}
|
||||
locale: null
|
||||
menu: Menu;
|
||||
};
|
||||
locale: null;
|
||||
user: User & {
|
||||
collection: 'users'
|
||||
}
|
||||
collection: 'users';
|
||||
};
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "posts".
|
||||
*/
|
||||
export interface Post {
|
||||
id: string
|
||||
text?: string | null
|
||||
id: string;
|
||||
text?: string | null;
|
||||
richText?: {
|
||||
root: {
|
||||
type: string
|
||||
type: string;
|
||||
children: {
|
||||
type: string
|
||||
version: number
|
||||
[k: string]: unknown
|
||||
}[]
|
||||
direction: ('ltr' | 'rtl') | null
|
||||
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | ''
|
||||
indent: number
|
||||
version: number
|
||||
}
|
||||
[k: string]: unknown
|
||||
} | null
|
||||
type: string;
|
||||
version: number;
|
||||
[k: string]: unknown;
|
||||
}[];
|
||||
direction: ('ltr' | 'rtl') | null;
|
||||
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
|
||||
indent: number;
|
||||
version: number;
|
||||
};
|
||||
[k: string]: unknown;
|
||||
} | null;
|
||||
richText2?: {
|
||||
root: {
|
||||
type: string
|
||||
type: string;
|
||||
children: {
|
||||
type: string
|
||||
version: number
|
||||
[k: string]: unknown
|
||||
}[]
|
||||
direction: ('ltr' | 'rtl') | null
|
||||
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | ''
|
||||
indent: number
|
||||
version: number
|
||||
}
|
||||
[k: string]: unknown
|
||||
} | null
|
||||
updatedAt: string
|
||||
createdAt: string
|
||||
_status?: ('draft' | 'published') | null
|
||||
type: string;
|
||||
version: number;
|
||||
[k: string]: unknown;
|
||||
}[];
|
||||
direction: ('ltr' | 'rtl') | null;
|
||||
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
|
||||
indent: number;
|
||||
version: number;
|
||||
};
|
||||
[k: string]: unknown;
|
||||
} | null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
_status?: ('draft' | 'published') | null;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "users".
|
||||
*/
|
||||
export interface User {
|
||||
id: string
|
||||
updatedAt: string
|
||||
createdAt: string
|
||||
email: string
|
||||
resetPasswordToken?: string | null
|
||||
resetPasswordExpiration?: string | null
|
||||
salt?: string | null
|
||||
hash?: string | null
|
||||
loginAttempts?: number | null
|
||||
lockUntil?: string | null
|
||||
password?: string | null
|
||||
id: string;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
email: string;
|
||||
resetPasswordToken?: string | null;
|
||||
resetPasswordExpiration?: string | null;
|
||||
salt?: string | null;
|
||||
hash?: string | null;
|
||||
loginAttempts?: number | null;
|
||||
lockUntil?: string | null;
|
||||
password?: string | null;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-preferences".
|
||||
*/
|
||||
export interface PayloadPreference {
|
||||
id: string
|
||||
id: string;
|
||||
user: {
|
||||
relationTo: 'users'
|
||||
value: string | User
|
||||
}
|
||||
key?: string | null
|
||||
relationTo: 'users';
|
||||
value: string | User;
|
||||
};
|
||||
key?: string | null;
|
||||
value?:
|
||||
| {
|
||||
[k: string]: unknown
|
||||
[k: string]: unknown;
|
||||
}
|
||||
| unknown[]
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null
|
||||
updatedAt: string
|
||||
createdAt: string
|
||||
| null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-migrations".
|
||||
*/
|
||||
export interface PayloadMigration {
|
||||
id: string
|
||||
name?: string | null
|
||||
batch?: number | null
|
||||
updatedAt: string
|
||||
createdAt: string
|
||||
id: string;
|
||||
name?: string | null;
|
||||
batch?: number | null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "menu".
|
||||
*/
|
||||
export interface Menu {
|
||||
id: string
|
||||
globalText?: string | null
|
||||
updatedAt?: string | null
|
||||
createdAt?: string | null
|
||||
id: string;
|
||||
globalText?: string | null;
|
||||
updatedAt?: string | null;
|
||||
createdAt?: string | null;
|
||||
}
|
||||
|
||||
|
||||
declare module 'payload' {
|
||||
// @ts-ignore
|
||||
// @ts-ignore
|
||||
export interface GeneratedTypes extends Config {}
|
||||
}
|
||||
}
|
||||
@@ -134,6 +134,8 @@ describe('Auth', () => {
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
expect(data.strategy).toBeDefined()
|
||||
expect(typeof data.exp).toBe('number')
|
||||
expect(response.status).toBe(200)
|
||||
expect(data.user.email).toBeDefined()
|
||||
})
|
||||
|
||||
@@ -1,4 +1,14 @@
|
||||
export function generateLexicalRichText() {
|
||||
import type {
|
||||
SerializedBlockNode,
|
||||
SerializedParagraphNode,
|
||||
SerializedTextNode,
|
||||
SerializedUploadNode,
|
||||
TypedEditorState,
|
||||
} from '@payloadcms/richtext-lexical'
|
||||
|
||||
export function generateLexicalRichText(): TypedEditorState<
|
||||
SerializedBlockNode | SerializedParagraphNode | SerializedTextNode | SerializedUploadNode
|
||||
> {
|
||||
return {
|
||||
root: {
|
||||
type: 'root',
|
||||
@@ -22,6 +32,7 @@ export function generateLexicalRichText() {
|
||||
format: '',
|
||||
indent: 0,
|
||||
type: 'paragraph',
|
||||
textFormat: 0,
|
||||
version: 1,
|
||||
},
|
||||
{
|
||||
@@ -217,6 +228,7 @@ export function generateLexicalRichText() {
|
||||
indent: 0,
|
||||
type: 'paragraph',
|
||||
version: 1,
|
||||
textFormat: 0,
|
||||
},
|
||||
{
|
||||
format: '',
|
||||
@@ -247,6 +259,7 @@ export function generateLexicalRichText() {
|
||||
indent: 0,
|
||||
type: 'paragraph',
|
||||
version: 1,
|
||||
textFormat: 0,
|
||||
},
|
||||
{
|
||||
format: '',
|
||||
@@ -272,6 +285,7 @@ export function generateLexicalRichText() {
|
||||
indent: 0,
|
||||
type: 'paragraph',
|
||||
version: 1,
|
||||
textFormat: 0,
|
||||
},
|
||||
{
|
||||
format: '',
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user