Compare commits
35 Commits
templates/
...
feat/plugi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4345820bbd | ||
|
|
62ba27b3bf | ||
|
|
33d5482e9d | ||
|
|
4c3e41beb1 | ||
|
|
50e7c24b17 | ||
|
|
796df37461 | ||
|
|
9c8cdea4b3 | ||
|
|
4334940755 | ||
|
|
b101feca7a | ||
|
|
0d07ce22e8 | ||
|
|
a582431a36 | ||
|
|
7a8b46484b | ||
|
|
d57cad632d | ||
|
|
7e3fd5d76c | ||
|
|
abee24e1d0 | ||
|
|
0d8643a9a3 | ||
|
|
d78550c561 | ||
|
|
c7272bb2bf | ||
|
|
9eb1b508f6 | ||
|
|
6fffbdb27a | ||
|
|
c298cbc90d | ||
|
|
5af71fb8d0 | ||
|
|
d4d79c1141 | ||
|
|
9d324ff207 | ||
|
|
fffab668c9 | ||
|
|
bae2fe535e | ||
|
|
c8046cade7 | ||
|
|
5e3963482e | ||
|
|
d9efd192e7 | ||
|
|
23e2f7bc9e | ||
|
|
4c57df69ca | ||
|
|
6a09fe1bf9 | ||
|
|
821bd35578 | ||
|
|
afa08d0ebf | ||
|
|
d97d7eda37 |
26
.github/workflows/main.yml
vendored
26
.github/workflows/main.yml
vendored
@@ -62,8 +62,12 @@ jobs:
|
||||
echo "templates: ${{ steps.filter.outputs.templates }}"
|
||||
|
||||
lint:
|
||||
# Follows same github's ci skip: [skip lint], [lint skip], [no lint]
|
||||
if: >
|
||||
github.event_name == 'pull_request' && !contains(github.event.pull_request.title, 'no-lint') && !contains(github.event.pull_request.title, 'skip-lint')
|
||||
github.event_name == 'pull_request' &&
|
||||
!contains(github.event.pull_request.title, '[skip lint]') &&
|
||||
!contains(github.event.pull_request.title, '[lint skip]') &&
|
||||
!contains(github.event.pull_request.title, '[no lint]')
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -281,18 +285,28 @@ jobs:
|
||||
- auth-basic
|
||||
- field-error-states
|
||||
- fields-relationship
|
||||
- fields
|
||||
- fields__collections__Blocks
|
||||
- fields__collections__Array
|
||||
- fields__collections__Relationship
|
||||
- fields__collections__RichText
|
||||
- fields__collections__Blocks
|
||||
- fields__collections__Collapsible
|
||||
- fields__collections__ConditionalLogic
|
||||
- fields__collections__CustomID
|
||||
- fields__collections__Date
|
||||
- fields__collections__Email
|
||||
- fields__collections__Indexed
|
||||
- fields__collections__JSON
|
||||
- fields__collections__Lexical__e2e__main
|
||||
- fields__collections__Lexical__e2e__blocks
|
||||
- fields__collections__Date
|
||||
- fields__collections__Number
|
||||
- fields__collections__Point
|
||||
- fields__collections__Radio
|
||||
- fields__collections__Relationship
|
||||
- fields__collections__RichText
|
||||
- fields__collections__Row
|
||||
- fields__collections__Select
|
||||
- fields__collections__Tabs
|
||||
- fields__collections__Tabs2
|
||||
- fields__collections__Text
|
||||
- fields__collections__UI
|
||||
- fields__collections__Upload
|
||||
- live-preview
|
||||
- localization
|
||||
|
||||
@@ -98,7 +98,7 @@ The following options are available:
|
||||
| **`livePreview`** | Enable real-time editing for instant visual feedback of your front-end application. [More details](../live-preview/overview). |
|
||||
| **`meta`** | Base metadata to use for the Admin Panel. [More details](./metadata). |
|
||||
| **`routes`** | Replace built-in Admin Panel routes with your own custom routes. [More details](#customizing-routes). |
|
||||
| **`suppressHydrationWarning`** | If set to `true`, suppresses React hydration mismatch warnings during the hydration of the root <html> tag. Defaults to `false`. |
|
||||
| **`suppressHydrationWarning`** | If set to `true`, suppresses React hydration mismatch warnings during the hydration of the root `<html>` tag. Defaults to `false`. |
|
||||
| **`theme`** | Restrict the Admin Panel theme to use only one of your choice. Default is `all`. |
|
||||
| **`user`** | The `slug` of the Collection that you want to allow to login to the Admin Panel. [More details](#the-admin-user-collection). |
|
||||
|
||||
|
||||
@@ -6,7 +6,13 @@ desc: The Rich Text field allows dynamic content to be written through the Admin
|
||||
keywords: rich text, fields, config, configuration, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
|
||||
---
|
||||
|
||||
The Rich Text Field is a powerful way to allow editors to write dynamic content. The content is saved as JSON in the database and can be converted into any format, including HTML, that you need.
|
||||
The Rich Text Field lets editors write and format dynamic content in a familiar interface.
|
||||
The content is saved as JSON in the database and can be converted to HTML or any other format needed.
|
||||
|
||||
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 Payload rich text editor.
|
||||
Instead, you can invest your time and effort into learning the underlying open-source tools that will allow
|
||||
you to apply your learnings elsewhere as well.
|
||||
|
||||
<LightDarkImage
|
||||
srcLight="https://payloadcms.com/images/docs/fields/richtext.png"
|
||||
@@ -15,23 +21,6 @@ The Rich Text Field is a powerful way to allow editors to write dynamic content.
|
||||
caption="Admin Panel screenshot of a Rich Text field"
|
||||
/>
|
||||
|
||||
Payload's rich text field is built on an "adapter pattern" which lets you specify which rich text editor you'd like to use.
|
||||
|
||||
Right now, Payload is officially supporting two rich text editors:
|
||||
|
||||
1. [SlateJS](/docs/rich-text/slate) - legacy, backwards-compatible with 1.0
|
||||
2. [Lexical](/docs/lexical/overview) - recommended
|
||||
|
||||
<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>{' '}rich text editor.
|
||||
</strong>
|
||||
|
||||
Instead, you can invest your time and effort into learning the underlying open-source tools that
|
||||
will allow you to apply your learnings elsewhere as well.
|
||||
</Banner>
|
||||
|
||||
## Config Options
|
||||
|
||||
| Option | Description |
|
||||
@@ -47,7 +36,7 @@ Right now, Payload is officially supporting two rich text editors:
|
||||
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. |
|
||||
| **`required`** | Require this field to have a value. |
|
||||
| **`admin`** | Admin-specific configuration. [More details](#admin-options). |
|
||||
| **`editor`** | Override the rich text editor specified in your base configuration for this field. |
|
||||
| **`editor`** | Customize or override the rich text editor. [More details](/docs/rich-text/overview). |
|
||||
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
|
||||
| **`typescriptSchema`** | Override field type generation with providing a JSON schema |
|
||||
| **`virtual`** | Provide `true` to disable field in the database. See [Virtual Fields](https://payloadcms.com/blog/learn-how-virtual-fields-can-help-solve-common-cms-challenges) |
|
||||
@@ -79,4 +68,5 @@ The Rich Text Field inherits all of the default options from the base [Field Adm
|
||||
|
||||
## Editor-specific Options
|
||||
|
||||
For a ton more editor-specific options, including how to build custom rich text elements directly into your editor, take a look at either the [Slate docs](/docs/rich-text/slate) or the [Lexical docs](/docs/lexical/overview) depending on which editor you're using.
|
||||
For a ton more editor-specific options, including how to build custom rich text elements directly into your editor,
|
||||
take a look at the [rich text editor documentation](/docs/rich-text/overview).
|
||||
|
||||
@@ -7,8 +7,8 @@ keywords: documentation, getting started, guide, Content Management System, cms,
|
||||
---
|
||||
|
||||
<YouTube
|
||||
id="In_lFhzmbME"
|
||||
title="Payload Introduction - Closing the Gap Between Headless CMS and Application Frameworks"
|
||||
id="ftohATkHBi0"
|
||||
title="Introduction to Payload — The open-source Next.js backend"
|
||||
/>
|
||||
|
||||
**Payload is the Next.js fullstack framework.** Write a Payload Config and instantly get:
|
||||
|
||||
@@ -1,292 +0,0 @@
|
||||
---
|
||||
title: Lexical Overview
|
||||
label: Overview
|
||||
order: 10
|
||||
desc: Built by Meta, Lexical is an incredibly powerful rich text editor, and it works beautifully within Payload.
|
||||
keywords: lexical, rich text, editor, headless cms
|
||||
---
|
||||
|
||||
One of Payload's goals is to build the best rich text editor experience that we possibly can. We want to combine the beauty and polish of the Medium editing experience with the strength and features of the Notion editor - all in one place.
|
||||
|
||||
Classically, we've used SlateJS to work toward this goal, but building custom elements into Slate has proven to be more difficult than we'd like, and we've been keeping our options open.
|
||||
|
||||
Lexical is extremely impressive and trivializes a lot of the hard parts of building new elements into a rich text editor. It has a few distinct advantages over Slate, including the following:
|
||||
|
||||
1. A "/" menu, which allows editors to easily add new elements while never leaving their keyboard
|
||||
1. A "hover" toolbar that pops up if you select text
|
||||
1. It supports Payload blocks natively, directly within your rich text editor
|
||||
1. Custom elements, called "features", are much easier to build in Lexical vs. Slate
|
||||
|
||||
To use the Lexical editor, first you need to install it:
|
||||
|
||||
```
|
||||
npm install @payloadcms/richtext-lexical
|
||||
```
|
||||
|
||||
Once you have it installed, you can pass it to your top-level Payload Config as follows:
|
||||
|
||||
```ts
|
||||
import { buildConfig } from 'payload'
|
||||
import { lexicalEditor } from '@payloadcms/richtext-lexical'
|
||||
|
||||
export default buildConfig({
|
||||
collections: [
|
||||
// your collections here
|
||||
],
|
||||
// Pass the Lexical editor to the root config
|
||||
editor: lexicalEditor({}),
|
||||
})
|
||||
```
|
||||
|
||||
You can also override Lexical settings on a field-by-field basis as follows:
|
||||
|
||||
```ts
|
||||
import type { CollectionConfig } from 'payload'
|
||||
import { lexicalEditor } from '@payloadcms/richtext-lexical'
|
||||
|
||||
export const Pages: CollectionConfig = {
|
||||
slug: 'pages',
|
||||
fields: [
|
||||
{
|
||||
name: 'content',
|
||||
type: 'richText',
|
||||
// Pass the Lexical editor here and override base settings as necessary
|
||||
editor: lexicalEditor({}),
|
||||
},
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
## Extending the lexical editor with Features
|
||||
|
||||
Lexical has been designed with extensibility in mind. Whether you're aiming to introduce new functionalities or tweak the existing ones, Lexical makes it seamless for you to bring those changes to life.
|
||||
|
||||
### Features: The Building Blocks
|
||||
|
||||
At the heart of Lexical's customization potential are "features". While Lexical ships with a set of default features we believe are essential for most use cases, the true power lies in your ability to redefine, expand, or prune these as needed.
|
||||
|
||||
If you remove all the default features, you're left with a blank editor. You can then add in only the features you need, or you can build your own custom features from scratch.
|
||||
|
||||
### Integrating New Features
|
||||
|
||||
To weave in your custom features, utilize the `features` prop when initializing the Lexical Editor. Here's a basic example of how this is done:
|
||||
|
||||
```ts
|
||||
import {
|
||||
BlocksFeature,
|
||||
LinkFeature,
|
||||
UploadFeature,
|
||||
lexicalEditor,
|
||||
} from '@payloadcms/richtext-lexical'
|
||||
import { Banner } from '../blocks/Banner'
|
||||
import { CallToAction } from '../blocks/CallToAction'
|
||||
|
||||
{
|
||||
editor: lexicalEditor({
|
||||
features: ({ defaultFeatures, rootFeatures }) => [
|
||||
...defaultFeatures,
|
||||
LinkFeature({
|
||||
// Example showing how to customize the built-in fields
|
||||
// of the Link feature
|
||||
fields: ({ defaultFields }) => [
|
||||
...defaultFields,
|
||||
{
|
||||
name: 'rel',
|
||||
label: 'Rel Attribute',
|
||||
type: 'select',
|
||||
hasMany: true,
|
||||
options: ['noopener', 'noreferrer', 'nofollow'],
|
||||
admin: {
|
||||
description:
|
||||
'The rel attribute defines the relationship between a linked resource and the current document. This is a custom link field.',
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
UploadFeature({
|
||||
collections: {
|
||||
uploads: {
|
||||
// Example showing how to customize the built-in fields
|
||||
// of the Upload feature
|
||||
fields: [
|
||||
{
|
||||
name: 'caption',
|
||||
type: 'richText',
|
||||
editor: lexicalEditor(),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}),
|
||||
// This is incredibly powerful. You can re-use your Payload blocks
|
||||
// directly in the Lexical editor as follows:
|
||||
BlocksFeature({
|
||||
blocks: [Banner, 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:
|
||||
|
||||
| Feature Name | Included by default | Description |
|
||||
|---------------------------------|---------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| **`BoldTextFeature`** | Yes | Handles the bold text format |
|
||||
| **`ItalicTextFeature`** | Yes | Handles the italic text format |
|
||||
| **`UnderlineTextFeature`** | Yes | Handles the underline text format |
|
||||
| **`StrikethroughTextFeature`** | Yes | Handles the strikethrough text format |
|
||||
| **`SubscriptTextFeature`** | Yes | Handles the subscript text format |
|
||||
| **`SuperscriptTextFeature`** | Yes | Handles the superscript text format |
|
||||
| **`InlineCodeTextFeature`** | Yes | Handles the inline-code text format |
|
||||
| **`ParagraphFeature`** | Yes | Handles paragraphs. Since they are already a key feature of lexical itself, this Feature mainly handles the Slash and Add-Block menu entries for paragraphs |
|
||||
| **`HeadingFeature`** | Yes | Adds Heading Nodes (by default, H1 - H6, but that can be customized) |
|
||||
| **`AlignFeature`** | Yes | Allows you to align text left, centered and right |
|
||||
| **`IndentFeature`** | Yes | Allows you to indent text with the tab key |
|
||||
| **`UnorderedListFeature`** | Yes | Adds unordered lists (ul) |
|
||||
| **`OrderedListFeature`** | Yes | Adds ordered lists (ol) |
|
||||
| **`CheckListFeature`** | Yes | Adds checklists |
|
||||
| **`LinkFeature`** | Yes | Allows you to create internal and external links |
|
||||
| **`RelationshipFeature`** | Yes | Allows you to create block-level (not inline) relationships to other documents |
|
||||
| **`BlockQuoteFeature`** | Yes | Allows you to create block-level quotes |
|
||||
| **`UploadFeature`** | Yes | Allows you to create block-level upload nodes - this supports all kinds of uploads, not just images |
|
||||
| **`HorizontalRuleFeature`** | Yes | Horizontal rules / separators. Basically displays an `<hr>` element |
|
||||
| **`InlineToolbarFeature`** | Yes | The inline toolbar is the floating toolbar which appears when you select text. This toolbar only contains actions relevant for selected text |
|
||||
| **`FixedToolbarFeature`** | No | This classic toolbar is pinned to the top and always visible. Both inline and fixed toolbars can be enabled at the same time. |
|
||||
| **`BlocksFeature`** | No | Allows you to use Payload's [Blocks Field](/docs/fields/blocks) directly inside your editor. In the feature props, you can specify the allowed blocks - just like in the Blocks field. |
|
||||
| **`TreeViewFeature`** | No | Adds a debug box under the editor, which allows you to see the current editor state live, the dom, as well as time travel. Very useful for debugging |
|
||||
| **`EXPERIMENTAL_TableFeature`** | No | Adds support for tables. This feature may be removed or receive breaking changes in the future - even within a stable lexical release, without needing a major release. |
|
||||
|
||||
Notice how even the toolbars are features? That's how extensible our lexical editor is - you could theoretically create your own toolbar if you wanted to!
|
||||
|
||||
## Creating your own, custom Feature
|
||||
|
||||
You can find more information about creating your own feature in our [building custom feature docs](/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,
|
||||
SerializedHeadingNode,
|
||||
} from '@payloadcms/richtext-lexical'
|
||||
|
||||
const editorState: TypedEditorState<
|
||||
| SerializedAutoLinkNode
|
||||
| SerializedBlockNode
|
||||
| SerializedHorizontalRuleNode
|
||||
| SerializedLinkNode
|
||||
| SerializedListItemNode
|
||||
| SerializedListNode
|
||||
| SerializedParagraphNode
|
||||
| SerializedQuoteNode
|
||||
| SerializedRelationshipNode
|
||||
| SerializedTextNode
|
||||
| SerializedUploadNode
|
||||
| SerializedHeadingNode
|
||||
> = {
|
||||
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,
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Alternatively, you can use the `DefaultTypedEditorState` type, which includes all types for all nodes included in the `defaultFeatures`:
|
||||
|
||||
```ts
|
||||
import type {
|
||||
DefaultTypedEditorState
|
||||
} from '@payloadcms/richtext-lexical'
|
||||
|
||||
const editorState: DefaultTypedEditorState = {
|
||||
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,
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Just like `TypedEditorState`, the `DefaultTypedEditorState` also accepts an optional node type union as a generic. Here, this would **add** the specified node types to the default ones. Example: `DefaultTypedEditorState<SerializedBlockNode | YourCustomSerializedNode>`.
|
||||
|
||||
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.
|
||||
@@ -10,20 +10,24 @@ Lexical saves data in JSON - this is great for storage and flexibility and allow
|
||||
|
||||
## Lexical => JSX
|
||||
|
||||
If you have a React-based frontend, converting lexical to JSX is the recommended way to render rich text content in your frontend. To do that, import the `RichText` component from `@payloadcms/richtext-lexical/react` and pass the lexical content to it:
|
||||
If your frontend uses React, converting Lexical to JSX is the recommended way to render rich text content. Import the `RichText` component from `@payloadcms/richtext-lexical/react` and pass the Lexical content to it:
|
||||
|
||||
```tsx
|
||||
import React from 'react'
|
||||
import { RichText } from '@payloadcms/richtext-lexical/react'
|
||||
import { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'
|
||||
|
||||
export const MyComponent = ({ lexicalData }) => {
|
||||
export const MyComponent = ({ data }: { data: SerializedEditorState }) => {
|
||||
return (
|
||||
<RichText data={lexicalData} />
|
||||
<RichText data={data} />
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
The `RichText` component will come with the most common serializers built-in, though you can also pass in your own serializers if you need to.
|
||||
The `RichText` component includes built-in serializers for common Lexical nodes but allows customization through the `converters` prop.
|
||||
|
||||
In our website template [you have an example](https://github.com/payloadcms/payload/blob/main/templates/website/src/components/RichText/index.tsx) of how to use `converters` to render custom blocks.
|
||||
|
||||
|
||||
<Banner type="default">
|
||||
The JSX converter expects the input data to be fully populated. When fetching data, ensure the `depth` setting is high enough, to ensure that lexical nodes such as uploads are populated.
|
||||
@@ -1,9 +0,0 @@
|
||||
---
|
||||
title: Lexical Rich Text
|
||||
label: Lexical
|
||||
order: 30
|
||||
desc: Built by Meta, Lexical is an incredibly powerful rich text editor, and it works beautifully within Payload.
|
||||
keywords: lexical, rich text, editor, headless cms
|
||||
---
|
||||
|
||||
The new lexical docs can be found at [Lexical](/docs/lexical/overview).
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
title: Lexical Migration
|
||||
label: Migration
|
||||
order: 30
|
||||
order: 90
|
||||
desc: Migration from slate and payload-plugin-lexical to lexical
|
||||
keywords: lexical, rich text, editor, headless cms, migrate, migration
|
||||
---
|
||||
@@ -1,18 +1,300 @@
|
||||
---
|
||||
title: Overview
|
||||
title: Rich Text Editor
|
||||
label: Overview
|
||||
order: 10
|
||||
desc: Rich Text within Payload is extremely powerful. We've combined the beauty of the Medium editor with the power of the Notion editor all in one place.
|
||||
keywords: slatejs, lexical, rich text, json, custom editor, javascript, typescript
|
||||
desc: The Payload editor, based on Lexical, allows for great customization with unparalleled ease.
|
||||
keywords: lexical, rich text, editor, headless cms
|
||||
---
|
||||
|
||||
Payload currently supports two official rich text editors and you can choose either one depending on your needs.
|
||||
<Banner type="warning">
|
||||
|
||||
1. [SlateJS](/docs/rich-text/slate) - stable, backwards-compatible with 1.0
|
||||
2. [Lexical](/docs/lexical/overview) - recommended
|
||||
The Payload editor is based on Lexical, Meta's rich text editor. The previous default editor was
|
||||
based on Slate and is still supported. You can read [its documentation](/docs/rich-text/slate),
|
||||
or the optional [migration guide](/docs/rich-text/migration) to migrate from Slate to Lexical (recommended).
|
||||
|
||||
These editors are built on an "adapter pattern" which means that you will need to install the editor you'd like to use. Take a look at the docs for the editor you'd like to use for instructions on how to install it.
|
||||
</Banner>
|
||||
|
||||
The big TL;DR here is that Slate is what we have used in the past, and we still support it for existing projects, but if you're building something new and you're feeling adventurous, you should give Lexical a shot. Slate has a lot of good stuff, but Lexical has lots more.
|
||||
One of Payload's goals is to build the best rich text editor experience that we possibly can. We want to combine the beauty and polish of the Medium editing experience with the strength and features of the Notion editor - all in one place.
|
||||
|
||||
No matter which editor you use, you have to install it at the top-level on the `config.editor` property, which will then cascade throughout all of your rich text fields and be used accordingly. Additionally, you also have the option to override the editor on a field-by-field basis if you'd like.
|
||||
Classically, we've used SlateJS to work toward this goal, but building custom elements into Slate has proven to be more difficult than we'd like, and we've been keeping our options open.
|
||||
|
||||
Lexical is extremely impressive and trivializes a lot of the hard parts of building new elements into a rich text editor. It has a few distinct advantages over Slate, including the following:
|
||||
|
||||
1. A "/" menu, which allows editors to easily add new elements while never leaving their keyboard
|
||||
1. A "hover" toolbar that pops up if you select text
|
||||
1. It supports Payload blocks natively, directly within your rich text editor
|
||||
1. Custom elements, called "features", are much easier to build in Lexical vs. Slate
|
||||
|
||||
To use the Lexical editor, first you need to install it:
|
||||
|
||||
```
|
||||
npm install @payloadcms/richtext-lexical
|
||||
```
|
||||
|
||||
Once you have it installed, you can pass it to your top-level Payload Config as follows:
|
||||
|
||||
```ts
|
||||
import { buildConfig } from 'payload'
|
||||
import { lexicalEditor } from '@payloadcms/richtext-lexical'
|
||||
|
||||
export default buildConfig({
|
||||
collections: [
|
||||
// your collections here
|
||||
],
|
||||
// Pass the Lexical editor to the root config
|
||||
editor: lexicalEditor({}),
|
||||
})
|
||||
```
|
||||
|
||||
You can also override Lexical settings on a field-by-field basis as follows:
|
||||
|
||||
```ts
|
||||
import type { CollectionConfig } from 'payload'
|
||||
import { lexicalEditor } from '@payloadcms/richtext-lexical'
|
||||
|
||||
export const Pages: CollectionConfig = {
|
||||
slug: 'pages',
|
||||
fields: [
|
||||
{
|
||||
name: 'content',
|
||||
type: 'richText',
|
||||
// Pass the Lexical editor here and override base settings as necessary
|
||||
editor: lexicalEditor({}),
|
||||
},
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
## Extending the lexical editor with Features
|
||||
|
||||
Lexical has been designed with extensibility in mind. Whether you're aiming to introduce new functionalities or tweak the existing ones, Lexical makes it seamless for you to bring those changes to life.
|
||||
|
||||
### Features: The Building Blocks
|
||||
|
||||
At the heart of Lexical's customization potential are "features". While Lexical ships with a set of default features we believe are essential for most use cases, the true power lies in your ability to redefine, expand, or prune these as needed.
|
||||
|
||||
If you remove all the default features, you're left with a blank editor. You can then add in only the features you need, or you can build your own custom features from scratch.
|
||||
|
||||
### Integrating New Features
|
||||
|
||||
To weave in your custom features, utilize the `features` prop when initializing the Lexical Editor. Here's a basic example of how this is done:
|
||||
|
||||
```ts
|
||||
import {
|
||||
BlocksFeature,
|
||||
LinkFeature,
|
||||
UploadFeature,
|
||||
lexicalEditor,
|
||||
} from '@payloadcms/richtext-lexical'
|
||||
import { Banner } from '../blocks/Banner'
|
||||
import { CallToAction } from '../blocks/CallToAction'
|
||||
|
||||
{
|
||||
editor: lexicalEditor({
|
||||
features: ({ defaultFeatures, rootFeatures }) => [
|
||||
...defaultFeatures,
|
||||
LinkFeature({
|
||||
// Example showing how to customize the built-in fields
|
||||
// of the Link feature
|
||||
fields: ({ defaultFields }) => [
|
||||
...defaultFields,
|
||||
{
|
||||
name: 'rel',
|
||||
label: 'Rel Attribute',
|
||||
type: 'select',
|
||||
hasMany: true,
|
||||
options: ['noopener', 'noreferrer', 'nofollow'],
|
||||
admin: {
|
||||
description:
|
||||
'The rel attribute defines the relationship between a linked resource and the current document. This is a custom link field.',
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
UploadFeature({
|
||||
collections: {
|
||||
uploads: {
|
||||
// Example showing how to customize the built-in fields
|
||||
// of the Upload feature
|
||||
fields: [
|
||||
{
|
||||
name: 'caption',
|
||||
type: 'richText',
|
||||
editor: lexicalEditor(),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}),
|
||||
// This is incredibly powerful. You can re-use your Payload blocks
|
||||
// directly in the Lexical editor as follows:
|
||||
BlocksFeature({
|
||||
blocks: [Banner, 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:
|
||||
|
||||
| Feature Name | Included by default | Description |
|
||||
|---------------------------------|---------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| **`BoldTextFeature`** | Yes | Handles the bold text format |
|
||||
| **`ItalicTextFeature`** | Yes | Handles the italic text format |
|
||||
| **`UnderlineTextFeature`** | Yes | Handles the underline text format |
|
||||
| **`StrikethroughTextFeature`** | Yes | Handles the strikethrough text format |
|
||||
| **`SubscriptTextFeature`** | Yes | Handles the subscript text format |
|
||||
| **`SuperscriptTextFeature`** | Yes | Handles the superscript text format |
|
||||
| **`InlineCodeTextFeature`** | Yes | Handles the inline-code text format |
|
||||
| **`ParagraphFeature`** | Yes | Handles paragraphs. Since they are already a key feature of lexical itself, this Feature mainly handles the Slash and Add-Block menu entries for paragraphs |
|
||||
| **`HeadingFeature`** | Yes | Adds Heading Nodes (by default, H1 - H6, but that can be customized) |
|
||||
| **`AlignFeature`** | Yes | Allows you to align text left, centered and right |
|
||||
| **`IndentFeature`** | Yes | Allows you to indent text with the tab key |
|
||||
| **`UnorderedListFeature`** | Yes | Adds unordered lists (ul) |
|
||||
| **`OrderedListFeature`** | Yes | Adds ordered lists (ol) |
|
||||
| **`CheckListFeature`** | Yes | Adds checklists |
|
||||
| **`LinkFeature`** | Yes | Allows you to create internal and external links |
|
||||
| **`RelationshipFeature`** | Yes | Allows you to create block-level (not inline) relationships to other documents |
|
||||
| **`BlockQuoteFeature`** | Yes | Allows you to create block-level quotes |
|
||||
| **`UploadFeature`** | Yes | Allows you to create block-level upload nodes - this supports all kinds of uploads, not just images |
|
||||
| **`HorizontalRuleFeature`** | Yes | Horizontal rules / separators. Basically displays an `<hr>` element |
|
||||
| **`InlineToolbarFeature`** | Yes | The inline toolbar is the floating toolbar which appears when you select text. This toolbar only contains actions relevant for selected text |
|
||||
| **`FixedToolbarFeature`** | No | This classic toolbar is pinned to the top and always visible. Both inline and fixed toolbars can be enabled at the same time. |
|
||||
| **`BlocksFeature`** | No | Allows you to use Payload's [Blocks Field](/docs/fields/blocks) directly inside your editor. In the feature props, you can specify the allowed blocks - just like in the Blocks field. |
|
||||
| **`TreeViewFeature`** | No | Adds a debug box under the editor, which allows you to see the current editor state live, the dom, as well as time travel. Very useful for debugging |
|
||||
| **`EXPERIMENTAL_TableFeature`** | No | Adds support for tables. This feature may be removed or receive breaking changes in the future - even within a stable lexical release, without needing a major release. |
|
||||
|
||||
Notice how even the toolbars are features? That's how extensible our lexical editor is - you could theoretically create your own toolbar if you wanted to!
|
||||
|
||||
## Creating your own, custom Feature
|
||||
|
||||
You can find more information about creating your own feature in our [building custom feature docs](/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,
|
||||
SerializedHeadingNode,
|
||||
} from '@payloadcms/richtext-lexical'
|
||||
|
||||
const editorState: TypedEditorState<
|
||||
| SerializedAutoLinkNode
|
||||
| SerializedBlockNode
|
||||
| SerializedHorizontalRuleNode
|
||||
| SerializedLinkNode
|
||||
| SerializedListItemNode
|
||||
| SerializedListNode
|
||||
| SerializedParagraphNode
|
||||
| SerializedQuoteNode
|
||||
| SerializedRelationshipNode
|
||||
| SerializedTextNode
|
||||
| SerializedUploadNode
|
||||
| SerializedHeadingNode
|
||||
> = {
|
||||
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,
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Alternatively, you can use the `DefaultTypedEditorState` type, which includes all types for all nodes included in the `defaultFeatures`:
|
||||
|
||||
```ts
|
||||
import type {
|
||||
DefaultTypedEditorState
|
||||
} from '@payloadcms/richtext-lexical'
|
||||
|
||||
const editorState: DefaultTypedEditorState = {
|
||||
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,
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Just like `TypedEditorState`, the `DefaultTypedEditorState` also accepts an optional node type union as a generic. Here, this would **add** the specified node types to the default ones. Example: `DefaultTypedEditorState<SerializedBlockNode | YourCustomSerializedNode>`.
|
||||
|
||||
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.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
title: Slate Rich Text
|
||||
label: Slate
|
||||
order: 20
|
||||
title: Slate Editor
|
||||
label: Slate (legacy)
|
||||
order: 100
|
||||
desc: The Slate editor has been supported by Payload since beta. It's very powerful and stores content as JSON, which unlocks a ton of power.
|
||||
keywords: slatejs, slate, rich text, editor, headless cms
|
||||
---
|
||||
|
||||
@@ -32,7 +32,7 @@ const config = withBundleAnalyzer(
|
||||
return [
|
||||
{
|
||||
destination: '/admin',
|
||||
permanent: true,
|
||||
permanent: false,
|
||||
source: '/',
|
||||
},
|
||||
]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "payload-monorepo",
|
||||
"version": "3.6.0",
|
||||
"version": "3.7.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "create-payload-app",
|
||||
"version": "3.6.0",
|
||||
"version": "3.7.0",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-mongodb",
|
||||
"version": "3.6.0",
|
||||
"version": "3.7.0",
|
||||
"description": "The officially supported MongoDB database adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-postgres",
|
||||
"version": "3.6.0",
|
||||
"version": "3.7.0",
|
||||
"description": "The officially supported Postgres database adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-sqlite",
|
||||
"version": "3.6.0",
|
||||
"version": "3.7.0",
|
||||
"description": "The officially supported SQLite database adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-vercel-postgres",
|
||||
"version": "3.6.0",
|
||||
"version": "3.7.0",
|
||||
"description": "Vercel Postgres adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/drizzle",
|
||||
"version": "3.6.0",
|
||||
"version": "3.7.0",
|
||||
"description": "A library of shared functions used by different payload database adapters",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -4,6 +4,7 @@ import toSnakeCase from 'to-snake-case'
|
||||
|
||||
import type { DrizzleAdapter } from './types.js'
|
||||
|
||||
import { buildFindManyArgs } from './find/buildFindManyArgs.js'
|
||||
import buildQuery from './queries/buildQuery.js'
|
||||
import { selectDistinct } from './queries/selectDistinct.js'
|
||||
import { upsertRow } from './upsertRow/index.js'
|
||||
@@ -38,6 +39,22 @@ export const updateOne: UpdateOne = async function updateOne(
|
||||
|
||||
if (selectDistinctResult?.[0]?.id) {
|
||||
idToUpdate = selectDistinctResult?.[0]?.id
|
||||
|
||||
// If id wasn't passed but `where` without any joins, retrieve it with findFirst
|
||||
} else if (whereArg && !joins.length) {
|
||||
const findManyArgs = buildFindManyArgs({
|
||||
adapter: this,
|
||||
depth: 0,
|
||||
fields: collection.flattenedFields,
|
||||
joinQuery: false,
|
||||
select: {},
|
||||
tableName,
|
||||
})
|
||||
|
||||
findManyArgs.where = where
|
||||
|
||||
const docToUpdate = await db.query[tableName].findFirst(findManyArgs)
|
||||
idToUpdate = docToUpdate?.id
|
||||
}
|
||||
|
||||
const result = await upsertRow({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/email-nodemailer",
|
||||
"version": "3.6.0",
|
||||
"version": "3.7.0",
|
||||
"description": "Payload Nodemailer Email Adapter",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/email-resend",
|
||||
"version": "3.6.0",
|
||||
"version": "3.7.0",
|
||||
"description": "Payload Resend Email Adapter",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/graphql",
|
||||
"version": "3.6.0",
|
||||
"version": "3.7.0",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -13,6 +13,7 @@ export type Resolver = (
|
||||
limit?: number
|
||||
locale?: string
|
||||
page?: number
|
||||
pagination?: boolean
|
||||
sort?: string
|
||||
where?: Where
|
||||
},
|
||||
@@ -53,6 +54,7 @@ export function findResolver(collection: Collection): Resolver {
|
||||
draft: args.draft,
|
||||
limit: args.limit,
|
||||
page: args.page,
|
||||
pagination: args.pagination,
|
||||
req,
|
||||
sort: args.sort,
|
||||
where: args.where,
|
||||
|
||||
@@ -12,6 +12,7 @@ export type Resolver = (
|
||||
limit?: number
|
||||
locale?: string
|
||||
page?: number
|
||||
pagination?: boolean
|
||||
sort?: string
|
||||
where: Where
|
||||
},
|
||||
@@ -50,6 +51,7 @@ export function findVersionsResolver(collection: Collection): Resolver {
|
||||
depth: 0,
|
||||
limit: args.limit,
|
||||
page: args.page,
|
||||
pagination: args.pagination,
|
||||
req: isolateObjectProperty(req, 'transactionID'),
|
||||
sort: args.sort,
|
||||
where: args.where,
|
||||
|
||||
@@ -11,6 +11,7 @@ export type Resolver = (
|
||||
limit?: number
|
||||
locale?: string
|
||||
page?: number
|
||||
pagination?: boolean
|
||||
sort?: string
|
||||
where: Where
|
||||
},
|
||||
@@ -26,6 +27,7 @@ export function findVersions(globalConfig: SanitizedGlobalConfig): Resolver {
|
||||
globalConfig,
|
||||
limit: args.limit,
|
||||
page: args.page,
|
||||
pagination: args.pagination,
|
||||
req: isolateObjectProperty(context.req, 'transactionID'),
|
||||
sort: args.sort,
|
||||
where: args.where,
|
||||
|
||||
@@ -255,18 +255,17 @@ export function buildObjectType({
|
||||
[field.on]: { equals: parent._id ?? parent.id },
|
||||
})
|
||||
|
||||
const results = await req.payload.find({
|
||||
return await req.payload.find({
|
||||
collection,
|
||||
depth: 0,
|
||||
fallbackLocale: req.fallbackLocale,
|
||||
limit,
|
||||
locale: req.locale,
|
||||
overrideAccess: false,
|
||||
req,
|
||||
sort,
|
||||
where: fullWhere,
|
||||
})
|
||||
|
||||
return results
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -196,6 +196,7 @@ export function initCollections({ config, graphqlResult }: InitCollectionsGraphQ
|
||||
: {}),
|
||||
limit: { type: GraphQLInt },
|
||||
page: { type: GraphQLInt },
|
||||
pagination: { type: GraphQLBoolean },
|
||||
sort: { type: GraphQLString },
|
||||
},
|
||||
resolve: findResolver(collection),
|
||||
@@ -351,6 +352,7 @@ export function initCollections({ config, graphqlResult }: InitCollectionsGraphQ
|
||||
: {}),
|
||||
limit: { type: GraphQLInt },
|
||||
page: { type: GraphQLInt },
|
||||
pagination: { type: GraphQLBoolean },
|
||||
sort: { type: GraphQLString },
|
||||
},
|
||||
resolve: findVersionsResolver(collection),
|
||||
|
||||
@@ -166,6 +166,7 @@ export function initGlobals({ config, graphqlResult }: InitGlobalsGraphQLArgs):
|
||||
: {}),
|
||||
limit: { type: GraphQLInt },
|
||||
page: { type: GraphQLInt },
|
||||
pagination: { type: GraphQLBoolean },
|
||||
sort: { type: GraphQLString },
|
||||
},
|
||||
resolve: findVersions(global),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/live-preview-react",
|
||||
"version": "3.6.0",
|
||||
"version": "3.7.0",
|
||||
"description": "The official React SDK for Payload Live Preview",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/live-preview-vue",
|
||||
"version": "3.6.0",
|
||||
"version": "3.7.0",
|
||||
"description": "The official Vue SDK for Payload Live Preview",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/live-preview",
|
||||
"version": "3.6.0",
|
||||
"version": "3.7.0",
|
||||
"description": "The official live preview JavaScript SDK for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/next",
|
||||
"version": "3.6.0",
|
||||
"version": "3.7.0",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -13,17 +13,19 @@ import type { CollectionRouteHandler } from '../types.js'
|
||||
|
||||
import { headersWithCors } from '../../../utilities/headersWithCors.js'
|
||||
export const find: CollectionRouteHandler = async ({ collection, req }) => {
|
||||
const { depth, draft, joins, limit, page, populate, select, sort, where } = req.query as {
|
||||
depth?: string
|
||||
draft?: string
|
||||
joins?: JoinQuery
|
||||
limit?: string
|
||||
page?: string
|
||||
populate?: Record<string, unknown>
|
||||
select?: Record<string, unknown>
|
||||
sort?: string
|
||||
where?: Where
|
||||
}
|
||||
const { depth, draft, joins, limit, page, pagination, populate, select, sort, where } =
|
||||
req.query as {
|
||||
depth?: string
|
||||
draft?: string
|
||||
joins?: JoinQuery
|
||||
limit?: string
|
||||
page?: string
|
||||
pagination?: string
|
||||
populate?: Record<string, unknown>
|
||||
select?: Record<string, unknown>
|
||||
sort?: string
|
||||
where?: Where
|
||||
}
|
||||
|
||||
const result = await findOperation({
|
||||
collection,
|
||||
@@ -32,6 +34,7 @@ export const find: CollectionRouteHandler = async ({ collection, req }) => {
|
||||
joins: sanitizeJoinParams(joins),
|
||||
limit: isNumber(limit) ? Number(limit) : undefined,
|
||||
page: isNumber(page) ? Number(page) : undefined,
|
||||
pagination: pagination === 'false' ? false : undefined,
|
||||
populate: sanitizePopulateParam(populate),
|
||||
req,
|
||||
select: sanitizeSelectParam(select),
|
||||
|
||||
@@ -9,10 +9,11 @@ import type { CollectionRouteHandler } from '../types.js'
|
||||
import { headersWithCors } from '../../../utilities/headersWithCors.js'
|
||||
|
||||
export const findVersions: CollectionRouteHandler = async ({ collection, req }) => {
|
||||
const { depth, limit, page, populate, select, sort, where } = req.query as {
|
||||
const { depth, limit, page, pagination, populate, select, sort, where } = req.query as {
|
||||
depth?: string
|
||||
limit?: string
|
||||
page?: string
|
||||
pagination?: string
|
||||
populate?: Record<string, unknown>
|
||||
select?: Record<string, unknown>
|
||||
sort?: string
|
||||
@@ -24,6 +25,7 @@ export const findVersions: CollectionRouteHandler = async ({ collection, req })
|
||||
depth: isNumber(depth) ? Number(depth) : undefined,
|
||||
limit: isNumber(limit) ? Number(limit) : undefined,
|
||||
page: isNumber(page) ? Number(page) : undefined,
|
||||
pagination: pagination === 'false' ? false : undefined,
|
||||
populate: sanitizePopulateParam(populate),
|
||||
req,
|
||||
select: sanitizeSelectParam(select),
|
||||
|
||||
@@ -9,10 +9,11 @@ import type { GlobalRouteHandler } from '../types.js'
|
||||
import { headersWithCors } from '../../../utilities/headersWithCors.js'
|
||||
|
||||
export const findVersions: GlobalRouteHandler = async ({ globalConfig, req }) => {
|
||||
const { depth, limit, page, populate, select, sort, where } = req.query as {
|
||||
const { depth, limit, page, pagination, populate, select, sort, where } = req.query as {
|
||||
depth?: string
|
||||
limit?: string
|
||||
page?: string
|
||||
pagination?: string
|
||||
populate?: Record<string, unknown>
|
||||
select?: Record<string, unknown>
|
||||
sort?: string
|
||||
@@ -24,6 +25,7 @@ export const findVersions: GlobalRouteHandler = async ({ globalConfig, req }) =>
|
||||
globalConfig,
|
||||
limit: isNumber(limit) ? Number(limit) : undefined,
|
||||
page: isNumber(page) ? Number(page) : undefined,
|
||||
pagination: pagination === 'false' ? false : undefined,
|
||||
populate: sanitizePopulateParam(populate),
|
||||
req,
|
||||
select: sanitizeSelectParam(select),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
'use client'
|
||||
import React, { forwardRef } from 'react'
|
||||
import React from 'react'
|
||||
|
||||
import { useLivePreviewContext } from '../Context/context.js'
|
||||
import './index.scss'
|
||||
@@ -7,12 +7,13 @@ import './index.scss'
|
||||
const baseClass = 'live-preview-iframe'
|
||||
|
||||
type Props = {
|
||||
ref: React.RefObject<HTMLIFrameElement>
|
||||
setIframeHasLoaded: (value: boolean) => void
|
||||
url: string
|
||||
}
|
||||
|
||||
export const IFrame = forwardRef<HTMLIFrameElement, Props>((props, ref) => {
|
||||
const { setIframeHasLoaded, url } = props
|
||||
export const IFrame: React.FC<Props> = (props) => {
|
||||
const { ref, setIframeHasLoaded, url } = props
|
||||
|
||||
const { zoom } = useLivePreviewContext()
|
||||
|
||||
@@ -30,4 +31,4 @@ export const IFrame = forwardRef<HTMLIFrameElement, Props>((props, ref) => {
|
||||
title={url}
|
||||
/>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/payload-cloud",
|
||||
"version": "3.6.0",
|
||||
"version": "3.7.0",
|
||||
"description": "The official Payload Cloud plugin",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { Config, Payload } from 'payload'
|
||||
|
||||
import { jest } from '@jest/globals'
|
||||
import nodemailer from 'nodemailer'
|
||||
import { defaults } from 'payload'
|
||||
|
||||
import { payloadCloudEmail } from './email.js'
|
||||
@@ -13,6 +14,20 @@ describe('email', () => {
|
||||
|
||||
const mockedPayload: Payload = jest.fn() as unknown as Payload
|
||||
|
||||
beforeAll(() => {
|
||||
// Mock createTestAccount to prevent calling external services
|
||||
jest.spyOn(nodemailer, 'createTestAccount').mockImplementation(() => {
|
||||
return Promise.resolve({
|
||||
imap: { host: 'imap.test.com', port: 993, secure: true },
|
||||
pass: 'testpass',
|
||||
pop3: { host: 'pop3.test.com', port: 995, secure: true },
|
||||
smtp: { host: 'smtp.test.com', port: 587, secure: false },
|
||||
user: 'testuser',
|
||||
web: 'https://webmail.test.com',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
defaultConfig = defaults as Config
|
||||
})
|
||||
|
||||
@@ -14,6 +14,20 @@ describe('plugin', () => {
|
||||
|
||||
const skipVerify = true
|
||||
|
||||
beforeAll(() => {
|
||||
// Mock createTestAccount to prevent calling external services
|
||||
jest.spyOn(nodemailer, 'createTestAccount').mockImplementation(() => {
|
||||
return Promise.resolve({
|
||||
imap: { host: 'imap.test.com', port: 993, secure: true },
|
||||
pass: 'testpass',
|
||||
pop3: { host: 'pop3.test.com', port: 995, secure: true },
|
||||
smtp: { host: 'smtp.test.com', port: 587, secure: false },
|
||||
user: 'testuser',
|
||||
web: 'https://webmail.test.com',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
createTransportSpy = jest.spyOn(nodemailer, 'createTransport').mockImplementationOnce(() => {
|
||||
return {
|
||||
|
||||
@@ -37,10 +37,11 @@ export const getStaticHandler = ({ cachingOptions, collection }: Args): StaticHa
|
||||
collCacheConfig?.enabled !== false
|
||||
|
||||
return async (req, { params }) => {
|
||||
let key = ''
|
||||
try {
|
||||
const { identityID, storageClient } = await getStorageClient()
|
||||
|
||||
const Key = createKey({
|
||||
key = createKey({
|
||||
collection: collection.slug,
|
||||
filename: params.filename,
|
||||
identityID,
|
||||
@@ -48,7 +49,7 @@ export const getStaticHandler = ({ cachingOptions, collection }: Args): StaticHa
|
||||
|
||||
const object = await storageClient.getObject({
|
||||
Bucket: process.env.PAYLOAD_CLOUD_BUCKET,
|
||||
Key,
|
||||
Key: key,
|
||||
})
|
||||
|
||||
if (!object.Body) {
|
||||
@@ -67,7 +68,13 @@ export const getStaticHandler = ({ cachingOptions, collection }: Args): StaticHa
|
||||
status: 200,
|
||||
})
|
||||
} catch (err: unknown) {
|
||||
req.payload.logger.error({ err, msg: 'Error getting file from cloud storage' })
|
||||
req.payload.logger.error({
|
||||
collectionSlug: collection.slug,
|
||||
err,
|
||||
msg: `Error getting file from cloud storage: ${params.filename}`,
|
||||
params,
|
||||
requestedKey: key,
|
||||
})
|
||||
return new Response('Internal Server Error', { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "payload",
|
||||
"version": "3.6.0",
|
||||
"version": "3.7.0",
|
||||
"description": "Node, React, Headless CMS and Application Framework built on Next.js",
|
||||
"keywords": [
|
||||
"admin panel",
|
||||
|
||||
@@ -14,6 +14,7 @@ export type Row = {
|
||||
blockType?: string
|
||||
collapsed?: boolean
|
||||
id: string
|
||||
isLoading?: boolean
|
||||
}
|
||||
|
||||
export type FilterOptionsResult = {
|
||||
|
||||
@@ -21,11 +21,12 @@ export const getPredefinedMigration = async ({
|
||||
}): Promise<MigrationTemplateArgs> => {
|
||||
// Check for predefined migration.
|
||||
// Either passed in via --file or prefixed with '@payloadcms/db-mongodb/' for example
|
||||
if (file || migrationNameArg?.startsWith('@payloadcms/')) {
|
||||
// removes the package name from the migrationName.
|
||||
const migrationName = (file || migrationNameArg).split('/').slice(2).join('/')
|
||||
let cleanPath = path.join(dirname, `./predefinedMigrations/${migrationName}`)
|
||||
const importPath = file ?? migrationNameArg
|
||||
|
||||
if (importPath?.startsWith('@payloadcms/db-')) {
|
||||
// removes the package name from the migrationName.
|
||||
const migrationName = importPath.split('/').slice(2).join('/')
|
||||
let cleanPath = path.join(dirname, `./predefinedMigrations/${migrationName}`)
|
||||
if (fs.existsSync(`${cleanPath}.mjs`)) {
|
||||
cleanPath = `${cleanPath}.mjs`
|
||||
} else if (fs.existsSync(`${cleanPath}.js`)) {
|
||||
@@ -36,12 +37,15 @@ export const getPredefinedMigration = async ({
|
||||
})
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
cleanPath = cleanPath.replaceAll('\\', '/')
|
||||
const moduleURL = pathToFileURL(cleanPath)
|
||||
try {
|
||||
const { downSQL, imports, upSQL } = await eval(`import('${moduleURL.href}')`)
|
||||
return { downSQL, imports, upSQL }
|
||||
return {
|
||||
downSQL,
|
||||
imports,
|
||||
upSQL,
|
||||
}
|
||||
} catch (err) {
|
||||
payload.logger.error({
|
||||
err,
|
||||
@@ -49,6 +53,22 @@ export const getPredefinedMigration = async ({
|
||||
})
|
||||
process.exit(1)
|
||||
}
|
||||
} else if (importPath) {
|
||||
try {
|
||||
const { downSQL, imports, upSQL } = await eval(`import('${importPath}')`)
|
||||
return {
|
||||
downSQL,
|
||||
imports,
|
||||
upSQL,
|
||||
}
|
||||
} catch (_err) {
|
||||
if (importPath?.includes('/')) {
|
||||
// We can assume that the intent was to import a file, thus we throw an error.
|
||||
throw new Error(`Error importing migration file from ${importPath}`)
|
||||
}
|
||||
// Silently fail. If the migration cannot be imported, it will be created as a blank migration and the import path will be used as the migration name.
|
||||
return {}
|
||||
}
|
||||
}
|
||||
return {}
|
||||
}
|
||||
|
||||
@@ -232,8 +232,8 @@ export const username: UsernameFieldValidation = (
|
||||
return t('validation:shorterThanMax', { maxLength })
|
||||
}
|
||||
|
||||
if ((value && !/^[\w.-]+$/.test(value)) || (!value && required)) {
|
||||
return t('validation:username')
|
||||
if (!value && required) {
|
||||
return t('validation:required')
|
||||
}
|
||||
|
||||
return true
|
||||
|
||||
@@ -1299,6 +1299,7 @@ export type {
|
||||
} from './queues/config/types/taskTypes.js'
|
||||
export type {
|
||||
BaseJob,
|
||||
JobLog,
|
||||
JobTaskStatus,
|
||||
RunningJob,
|
||||
SingleTaskStatus,
|
||||
@@ -1306,6 +1307,7 @@ export type {
|
||||
WorkflowHandler,
|
||||
WorkflowTypes,
|
||||
} from './queues/config/types/workflowTypes.js'
|
||||
export { importHandlerPath } from './queues/operations/runJobs/runJob/importHandlerPath.js'
|
||||
export { getLocalI18n } from './translations/getLocalI18n.js'
|
||||
export * from './types/index.js'
|
||||
export { getFileByPath } from './uploads/getFileByPath.js'
|
||||
|
||||
@@ -14,6 +14,10 @@ export type JobLog = {
|
||||
completedAt: string
|
||||
error?: unknown
|
||||
executedAt: string
|
||||
/**
|
||||
* ID added by the array field when the log is saved in the database
|
||||
*/
|
||||
id?: string
|
||||
input?: any
|
||||
output?: any
|
||||
state: 'failed' | 'succeeded'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { RunningJobFromTask } from './config/types/workflowTypes.js'
|
||||
import type { BaseJob, RunningJobFromTask } from './config/types/workflowTypes.js'
|
||||
|
||||
import {
|
||||
createLocalReq,
|
||||
@@ -23,6 +23,7 @@ export const getJobsLocalAPI = (payload: Payload) => ({
|
||||
// TTaskOrWorkflowlug with keyof TypedJobs['workflows'] removed:
|
||||
task: TTaskOrWorkflowSlug extends keyof TypedJobs['tasks'] ? TTaskOrWorkflowSlug : never
|
||||
workflow?: never
|
||||
waitUntil?: Date
|
||||
}
|
||||
| {
|
||||
input: TypedJobs['workflows'][TTaskOrWorkflowSlug]['input']
|
||||
@@ -32,6 +33,7 @@ export const getJobsLocalAPI = (payload: Payload) => ({
|
||||
workflow: TTaskOrWorkflowSlug extends keyof TypedJobs['workflows']
|
||||
? TTaskOrWorkflowSlug
|
||||
: never
|
||||
waitUntil?: Date
|
||||
},
|
||||
): Promise<
|
||||
TTaskOrWorkflowSlug extends keyof TypedJobs['workflows']
|
||||
@@ -59,7 +61,8 @@ export const getJobsLocalAPI = (payload: Payload) => ({
|
||||
queue,
|
||||
taskSlug: 'task' in args ? args.task : undefined,
|
||||
workflowSlug: 'workflow' in args ? args.workflow : undefined,
|
||||
},
|
||||
waitUntil: args.waitUntil?.toISOString() ?? undefined,
|
||||
} as BaseJob,
|
||||
req: args.req,
|
||||
})) as TTaskOrWorkflowSlug extends keyof TypedJobs['workflows']
|
||||
? RunningJob<TTaskOrWorkflowSlug>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-cloud-storage",
|
||||
"version": "3.6.0",
|
||||
"version": "3.7.0",
|
||||
"description": "The official cloud storage plugin for Payload CMS",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-form-builder",
|
||||
"version": "3.6.0",
|
||||
"version": "3.7.0",
|
||||
"description": "Form builder plugin for Payload CMS",
|
||||
"keywords": [
|
||||
"payload",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-nested-docs",
|
||||
"version": "3.6.0",
|
||||
"version": "3.7.0",
|
||||
"description": "The official Nested Docs plugin for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -23,11 +23,10 @@ type ResaveArgs = {
|
||||
const resave = async ({ collection, doc, draft, pluginConfig, req }: ResaveArgs) => {
|
||||
const parentSlug = pluginConfig?.parentFieldSlug || 'parent'
|
||||
const parentDocIsPublished = doc._status === 'published'
|
||||
|
||||
const children = await req.payload.find({
|
||||
collection: collection.slug,
|
||||
depth: 0,
|
||||
draft,
|
||||
draft: true,
|
||||
locale: req.locale,
|
||||
req,
|
||||
where: {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-redirects",
|
||||
"version": "3.6.0",
|
||||
"version": "3.7.0",
|
||||
"description": "Redirects plugin for Payload",
|
||||
"keywords": [
|
||||
"payload",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-search",
|
||||
"version": "3.6.0",
|
||||
"version": "3.7.0",
|
||||
"description": "Search plugin for Payload",
|
||||
"keywords": [
|
||||
"payload",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-sentry",
|
||||
"version": "3.6.0",
|
||||
"version": "3.7.0",
|
||||
"description": "Sentry plugin for Payload",
|
||||
"keywords": [
|
||||
"payload",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-seo",
|
||||
"version": "3.6.0",
|
||||
"version": "3.7.0",
|
||||
"description": "SEO plugin for Payload",
|
||||
"keywords": [
|
||||
"payload",
|
||||
@@ -72,7 +72,8 @@
|
||||
"@payloadcms/next": "workspace:*",
|
||||
"@types/react": "19.0.1",
|
||||
"@types/react-dom": "19.0.1",
|
||||
"payload": "workspace:*"
|
||||
"payload": "workspace:*",
|
||||
"@payloadcms/richtext-lexical": "workspace:*"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"payload": "workspace:*",
|
||||
|
||||
@@ -3,3 +3,5 @@ export { MetaImageComponent } from '../fields/MetaImage/MetaImageComponent.js'
|
||||
export { MetaTitleComponent } from '../fields/MetaTitle/MetaTitleComponent.js'
|
||||
export { OverviewComponent } from '../fields/Overview/OverviewComponent.js'
|
||||
export { PreviewComponent } from '../fields/Preview/PreviewComponent.js'
|
||||
export { InlineBlockComponent } from '../fields/MetaTitle/InlineBlockComponent.js'
|
||||
export { SEOFeature } from '../fields/MetaTitle/SEOFeature/index.client.js'
|
||||
|
||||
@@ -1,30 +1,73 @@
|
||||
import type { TextareaField } from 'payload'
|
||||
import type { RichTextField, TextareaField } from 'payload'
|
||||
|
||||
import { BlocksFeature, FixedToolbarFeature, lexicalEditor } from '@payloadcms/richtext-lexical'
|
||||
|
||||
import { SEOFeature } from '../MetaTitle/SEOFeature/index.js'
|
||||
|
||||
interface FieldFunctionProps {
|
||||
/**
|
||||
* Tell the component if the generate function is available as configured in the plugin config
|
||||
*/
|
||||
hasGenerateFn?: boolean
|
||||
overrides?: Partial<TextareaField>
|
||||
overrides?: Partial<RichTextField>
|
||||
}
|
||||
|
||||
type FieldFunction = ({ hasGenerateFn, overrides }: FieldFunctionProps) => TextareaField
|
||||
type FieldFunction = ({ hasGenerateFn, overrides }: FieldFunctionProps) => RichTextField
|
||||
|
||||
export const MetaDescriptionField: FieldFunction = ({ hasGenerateFn = false, overrides }) => {
|
||||
return {
|
||||
name: 'description',
|
||||
type: 'textarea',
|
||||
admin: {
|
||||
components: {
|
||||
Field: {
|
||||
clientProps: {
|
||||
hasGenerateDescriptionFn: hasGenerateFn,
|
||||
},
|
||||
path: '@payloadcms/plugin-seo/client#MetaDescriptionComponent',
|
||||
},
|
||||
type: 'richText',
|
||||
editor: lexicalEditor({
|
||||
admin: {
|
||||
hideGutter: true,
|
||||
},
|
||||
},
|
||||
features: [
|
||||
SEOFeature(),
|
||||
FixedToolbarFeature(),
|
||||
BlocksFeature({
|
||||
inlineBlocks: [
|
||||
{
|
||||
slug: 'Product Name',
|
||||
admin: {
|
||||
components: {
|
||||
Block: '@payloadcms/plugin-seo/client#InlineBlockComponent',
|
||||
},
|
||||
},
|
||||
fields: [],
|
||||
},
|
||||
{
|
||||
slug: 'Collection Name',
|
||||
admin: {
|
||||
components: {
|
||||
Block: '@payloadcms/plugin-seo/client#InlineBlockComponent',
|
||||
},
|
||||
},
|
||||
fields: [],
|
||||
},
|
||||
{
|
||||
slug: 'City',
|
||||
admin: {
|
||||
components: {
|
||||
Block: '@payloadcms/plugin-seo/client#InlineBlockComponent',
|
||||
},
|
||||
},
|
||||
fields: [],
|
||||
},
|
||||
{
|
||||
slug: 'Florist Name',
|
||||
admin: {
|
||||
components: {
|
||||
Block: '@payloadcms/plugin-seo/client#InlineBlockComponent',
|
||||
},
|
||||
},
|
||||
fields: [],
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
localized: true,
|
||||
...((overrides as unknown as TextareaField) ?? {}),
|
||||
...((overrides as unknown as RichTextField) ?? {}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import {
|
||||
InlineBlockContainer,
|
||||
InlineBlockLabel,
|
||||
InlineBlockRemoveButton,
|
||||
} from '@payloadcms/richtext-lexical/client'
|
||||
|
||||
export const InlineBlockComponent: React.FC = () => {
|
||||
return (
|
||||
<InlineBlockContainer>
|
||||
<InlineBlockLabel />
|
||||
<InlineBlockRemoveButton />
|
||||
</InlineBlockContainer>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
'use client'
|
||||
|
||||
import { useLexicalComposerContext } from '@payloadcms/richtext-lexical/lexical/react/LexicalComposerContext'
|
||||
import { useDocumentInfo, useTranslation } from '@payloadcms/ui'
|
||||
import React, { useEffect } from 'react'
|
||||
|
||||
import { defaults } from '../../../defaults.js'
|
||||
import { LengthIndicator } from '../../../ui/LengthIndicator.js'
|
||||
import { recurseEditorState } from '../../Preview/inlineBlockToText.js'
|
||||
|
||||
const { maxLength: maxLengthDefault, minLength: minLengthDefault } = defaults.title
|
||||
|
||||
export const LengthIndicatorPlugin = () => {
|
||||
const { t } = useTranslation<any, any>()
|
||||
|
||||
const docInfo = useDocumentInfo()
|
||||
|
||||
const minLength = minLengthDefault
|
||||
const maxLength = maxLengthDefault
|
||||
|
||||
const [editor] = useLexicalComposerContext()
|
||||
|
||||
const [editorText, setEditorText] = React.useState<null | string>('')
|
||||
|
||||
useEffect(() => {
|
||||
if (!editorText) {
|
||||
const editorState = editor.getEditorState().toJSON()
|
||||
|
||||
const text = []
|
||||
recurseEditorState(editorState?.root?.children, text, 0, docInfo.savedDocumentData as any)
|
||||
|
||||
setEditorText(text.join(' '))
|
||||
}
|
||||
|
||||
return editor.registerUpdateListener(({ editorState }) => {
|
||||
const editorStateJSON = editorState.toJSON()
|
||||
const text = []
|
||||
recurseEditorState(editorStateJSON?.root?.children, text, 0, docInfo.savedDocumentData as any)
|
||||
|
||||
setEditorText(text.join(' '))
|
||||
})
|
||||
}, [docInfo.savedDocumentData, editor, editorText, maxLength])
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
marginBottom: '20px',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
marginBottom: '5px',
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
color: '#9A9A9A',
|
||||
}}
|
||||
>
|
||||
{t('plugin-seo:lengthTipTitle', { maxLength, minLength })}
|
||||
<a
|
||||
href="https://developers.google.com/search/docs/advanced/appearance/title-link#page-titles"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
{t('plugin-seo:bestPractices')}
|
||||
</a>
|
||||
.
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
alignItems: 'center',
|
||||
display: 'flex',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<LengthIndicator maxLength={maxLength} minLength={minLength} text={editorText} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
'use client'
|
||||
import { createClientFeature } from '@payloadcms/richtext-lexical/client'
|
||||
import { SEOPlugin } from './plugin.js'
|
||||
import { LengthIndicatorPlugin } from './LengthIndicatorPlugin.js'
|
||||
|
||||
export const SEOFeature = createClientFeature({
|
||||
plugins: [
|
||||
{
|
||||
Component: SEOPlugin,
|
||||
position: 'aboveContainer',
|
||||
},
|
||||
{
|
||||
Component: LengthIndicatorPlugin,
|
||||
position: 'belowContainer',
|
||||
},
|
||||
],
|
||||
})
|
||||
10
packages/plugin-seo/src/fields/MetaTitle/SEOFeature/index.ts
Normal file
10
packages/plugin-seo/src/fields/MetaTitle/SEOFeature/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { createServerFeature } from '@payloadcms/richtext-lexical'
|
||||
|
||||
export const SEOFeature = createServerFeature({
|
||||
key: 'seo',
|
||||
feature(props) {
|
||||
return {
|
||||
ClientFeature: '@payloadcms/plugin-seo/client#SEOFeature',
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,49 @@
|
||||
'use client'
|
||||
|
||||
import { useTranslation } from '@payloadcms/ui'
|
||||
import React from 'react'
|
||||
|
||||
export const SEOPlugin = () => {
|
||||
const { t } = useTranslation<any, any>()
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
marginBottom: '20px',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
marginBottom: '5px',
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
<div className="plugin-seo__field">
|
||||
{false && (
|
||||
<React.Fragment>
|
||||
—
|
||||
<button
|
||||
disabled={false}
|
||||
onClick={() => {
|
||||
// void regenerateTitle()
|
||||
}}
|
||||
style={{
|
||||
background: 'none',
|
||||
backgroundColor: 'transparent',
|
||||
border: 'none',
|
||||
color: 'currentcolor',
|
||||
cursor: 'pointer',
|
||||
padding: 0,
|
||||
textDecoration: 'underline',
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
{t('plugin-seo:autoGenerate')}
|
||||
</button>
|
||||
</React.Fragment>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
import type { TextField } from 'payload'
|
||||
import type { RichTextField, TextField } from 'payload'
|
||||
import { BlocksFeature, FixedToolbarFeature, lexicalEditor } from '@payloadcms/richtext-lexical'
|
||||
import { SEOFeature } from './SEOFeature/index.js'
|
||||
|
||||
interface FieldFunctionProps {
|
||||
/**
|
||||
@@ -8,23 +10,65 @@ interface FieldFunctionProps {
|
||||
overrides?: Partial<TextField>
|
||||
}
|
||||
|
||||
type FieldFunction = ({ hasGenerateFn, overrides }: FieldFunctionProps) => TextField
|
||||
type FieldFunction = ({ hasGenerateFn, overrides }: FieldFunctionProps) => RichTextField
|
||||
|
||||
export const MetaTitleField: FieldFunction = ({ hasGenerateFn = false, overrides }) => {
|
||||
return {
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
admin: {
|
||||
components: {
|
||||
Field: {
|
||||
clientProps: {
|
||||
hasGenerateTitleFn: hasGenerateFn,
|
||||
},
|
||||
path: '@payloadcms/plugin-seo/client#MetaTitleComponent',
|
||||
},
|
||||
type: 'richText',
|
||||
editor: lexicalEditor({
|
||||
admin: {
|
||||
hideGutter: true,
|
||||
},
|
||||
features: [
|
||||
SEOFeature(),
|
||||
FixedToolbarFeature(),
|
||||
BlocksFeature({
|
||||
inlineBlocks: [
|
||||
{
|
||||
slug: 'Product Name',
|
||||
fields: [],
|
||||
admin: {
|
||||
components: {
|
||||
Block: '@payloadcms/plugin-seo/client#InlineBlockComponent',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
slug: 'Collection Name',
|
||||
fields: [],
|
||||
admin: {
|
||||
components: {
|
||||
Block: '@payloadcms/plugin-seo/client#InlineBlockComponent',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
slug: 'City',
|
||||
fields: [],
|
||||
admin: {
|
||||
components: {
|
||||
Block: '@payloadcms/plugin-seo/client#InlineBlockComponent',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
slug: 'Florist Name',
|
||||
fields: [],
|
||||
admin: {
|
||||
components: {
|
||||
Block: '@payloadcms/plugin-seo/client#InlineBlockComponent',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
admin: {
|
||||
components: {},
|
||||
},
|
||||
localized: true,
|
||||
...((overrides as unknown as TextField) ?? {}),
|
||||
...((overrides as unknown as RichTextField) ?? {}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import type { FormField, UIField } from 'payload'
|
||||
import type { FormField, TypeWithID, UIField } from 'payload'
|
||||
|
||||
import {
|
||||
useAllFormFields,
|
||||
@@ -16,6 +16,8 @@ import React, { useEffect, useState } from 'react'
|
||||
import type { PluginSEOTranslationKeys, PluginSEOTranslations } from '../../translations/index.js'
|
||||
import type { GenerateURL } from '../../types.js'
|
||||
|
||||
import { recurseEditorState } from './inlineBlockToText.js'
|
||||
|
||||
type PreviewProps = {
|
||||
readonly descriptionPath?: string
|
||||
readonly hasGenerateURLFn: boolean
|
||||
@@ -91,6 +93,24 @@ export const PreviewComponent: React.FC<PreviewProps> = (props) => {
|
||||
}
|
||||
}, [fields, href, locale, docInfo, hasGenerateURLFn, getData, serverURL, api])
|
||||
|
||||
const metaTitleText = []
|
||||
|
||||
recurseEditorState(
|
||||
(metaTitle as any)?.root?.children ?? [],
|
||||
metaTitleText,
|
||||
0,
|
||||
docInfo.savedDocumentData as TypeWithID,
|
||||
)
|
||||
|
||||
const metaDescriptionText = []
|
||||
|
||||
recurseEditorState(
|
||||
(metaDescription as any)?.root?.children ?? [],
|
||||
metaDescriptionText,
|
||||
0,
|
||||
docInfo.savedDocumentData as TypeWithID,
|
||||
)
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
@@ -138,7 +158,7 @@ export const PreviewComponent: React.FC<PreviewProps> = (props) => {
|
||||
textDecoration: 'none',
|
||||
}}
|
||||
>
|
||||
{metaTitle as string}
|
||||
{metaTitleText as React.ReactNode[]}
|
||||
</a>
|
||||
</h4>
|
||||
<p
|
||||
@@ -146,7 +166,7 @@ export const PreviewComponent: React.FC<PreviewProps> = (props) => {
|
||||
margin: 0,
|
||||
}}
|
||||
>
|
||||
{metaDescription as string}
|
||||
{metaDescriptionText as any}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
45
packages/plugin-seo/src/fields/Preview/inlineBlockToText.ts
Normal file
45
packages/plugin-seo/src/fields/Preview/inlineBlockToText.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import type { TypeWithID } from 'payload'
|
||||
|
||||
export const inlineBlockToText = (args: { documentData: TypeWithID; inlineBlock: any }) => {
|
||||
if (args.inlineBlock.fields.blockType === 'Product Name') {
|
||||
// @ts-expect-error
|
||||
return args.documentData.productName
|
||||
}
|
||||
if (args.inlineBlock.fields.blockType === 'Collection Name') {
|
||||
// @ts-expect-error
|
||||
return args.documentData.collectionName
|
||||
}
|
||||
if (args.inlineBlock.fields.blockType === 'City') {
|
||||
// @ts-expect-error
|
||||
return args.documentData.city
|
||||
}
|
||||
if (args.inlineBlock.fields.blockType === 'Florist Name') {
|
||||
// @ts-expect-error
|
||||
return args.documentData.floristName
|
||||
}
|
||||
return 'Inline Block'
|
||||
}
|
||||
|
||||
export function recurseEditorState(
|
||||
editorState: any[],
|
||||
textContent: string[],
|
||||
i: number = 0,
|
||||
documentData: TypeWithID,
|
||||
): string[] {
|
||||
for (const node of editorState) {
|
||||
i++
|
||||
if (node?.type === 'inlineBlock') {
|
||||
textContent.push(inlineBlockToText({ documentData, inlineBlock: node }))
|
||||
} else if ('text' in node && node.text) {
|
||||
textContent.push(node.text as string)
|
||||
} else {
|
||||
if (!('children' in node)) {
|
||||
textContent.push(node.type)
|
||||
}
|
||||
}
|
||||
if ('children' in node && node.children) {
|
||||
textContent = recurseEditorState(node.children as any[], textContent, i, documentData)
|
||||
}
|
||||
}
|
||||
return textContent
|
||||
}
|
||||
@@ -21,5 +21,5 @@
|
||||
"src/**/*.spec.tsx"
|
||||
],
|
||||
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.d.ts", "src/**/*.json"],
|
||||
"references": [{ "path": "../payload" }, { "path": "../ui" }, { "path": "../next" }]
|
||||
"references": [{ "path": "../payload" }, { "path": "../ui" }, { "path": "../next" }, { "path": "../richtext-lexical" }]
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-stripe",
|
||||
"version": "3.6.0",
|
||||
"version": "3.7.0",
|
||||
"description": "Stripe plugin for Payload",
|
||||
"keywords": [
|
||||
"payload",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/richtext-lexical",
|
||||
"version": "3.6.0",
|
||||
"version": "3.7.0",
|
||||
"description": "The officially supported Lexical richtext adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -20,15 +20,30 @@ export type JSXConvertersFunction<
|
||||
| SerializedInlineBlockNode<{ blockName?: null | string; blockType: string }>,
|
||||
> = (args: { defaultConverters: JSXConverters<DefaultNodeTypes> }) => JSXConverters<T>
|
||||
|
||||
type Props = {
|
||||
type RichTextProps = {
|
||||
/**
|
||||
* Additional class names for the container.
|
||||
*/
|
||||
className?: string
|
||||
/**
|
||||
* Custom converters to transform your nodes to JSX. Can be an object or a function that receives the default converters.
|
||||
*/
|
||||
converters?: JSXConverters | JSXConvertersFunction
|
||||
/**
|
||||
* Serialized editor state to render.
|
||||
*/
|
||||
data: SerializedEditorState
|
||||
/**
|
||||
* If true, disables indentation globally. If an array, disables for specific node `type` values.
|
||||
*/
|
||||
disableIndent?: boolean | string[]
|
||||
/**
|
||||
* If true, disables text alignment globally. If an array, disables for specific node `type` values.
|
||||
*/
|
||||
disableTextAlign?: boolean | string[]
|
||||
}
|
||||
|
||||
export const RichText: React.FC<Props> = ({
|
||||
export const RichText: React.FC<RichTextProps> = ({
|
||||
className,
|
||||
converters,
|
||||
data: editorState,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/richtext-slate",
|
||||
"version": "3.6.0",
|
||||
"version": "3.7.0",
|
||||
"description": "The officially supported Slate richtext adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/storage-azure",
|
||||
"version": "3.6.0",
|
||||
"version": "3.7.0",
|
||||
"description": "Payload storage adapter for Azure Blob Storage",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/storage-gcs",
|
||||
"version": "3.6.0",
|
||||
"version": "3.7.0",
|
||||
"description": "Payload storage adapter for Google Cloud Storage",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/storage-s3",
|
||||
"version": "3.6.0",
|
||||
"version": "3.7.0",
|
||||
"description": "Payload storage adapter for Amazon S3",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/storage-uploadthing",
|
||||
"version": "3.6.0",
|
||||
"version": "3.7.0",
|
||||
"description": "Payload storage adapter for uploadthing",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/storage-vercel-blob",
|
||||
"version": "3.6.0",
|
||||
"version": "3.7.0",
|
||||
"description": "Payload storage adapter for Vercel Blob Storage",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/translations",
|
||||
"version": "3.6.0",
|
||||
"version": "3.7.0",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -13,7 +13,7 @@ export const arTranslations: DefaultTranslationsObject = {
|
||||
beginCreateFirstUser: 'للبدء, قم بإنشاء المستخدم الأوّل.',
|
||||
changePassword: 'تغيير كلمة المرور',
|
||||
checkYourEmailForPasswordReset:
|
||||
'تحقّق من بريدك الإلكتروني بحثًا عن رابط يسمح لك بإعادة تعيين كلمة المرور الخاصّة بك بشكل آمن.',
|
||||
'إذا كان عنوان البريد الإلكتروني مرتبطًا بحساب، فستتلقى تعليمات لإعادة تعيين كلمة المرور قريبًا. يرجى التحقق من مجلد البريد العشوائي أو السبام إذا لم تر البريد الإلكتروني في صندوق الوارد.',
|
||||
confirmGeneration: 'تأكيد التّوليد',
|
||||
confirmPassword: 'تأكيد كلمة المرور',
|
||||
createFirstUser: 'إنشاء المستخدم الأوّل',
|
||||
|
||||
@@ -13,7 +13,7 @@ export const azTranslations: DefaultTranslationsObject = {
|
||||
beginCreateFirstUser: 'Başlamaq üçün ilk istifadəçinizi yaradın.',
|
||||
changePassword: 'Parolu dəyişdir',
|
||||
checkYourEmailForPasswordReset:
|
||||
'Parolunuzu təhlükəsiz şəkildə sıfırlamağa imkan verəcək link üçün e-poçt ünvanınızı yoxlayın.',
|
||||
'Əgər e-poçt ünvanı bir hesabla əlaqəli olsa, tezliklə şifrənizi yenidən qurmaq üçün təlimatlari alacaqsınız. E-poçtu giriş qutunuzda görmürsəniz, zəhmət olmasa spam və ya zibil poçt qovluğunu yoxlayın.',
|
||||
confirmGeneration: 'Generasiyani təsdiqlə',
|
||||
confirmPassword: 'Şifrəni təsdiq et',
|
||||
createFirstUser: 'İlk istifadəçini yaradın',
|
||||
|
||||
@@ -13,7 +13,7 @@ export const bgTranslations: DefaultTranslationsObject = {
|
||||
beginCreateFirstUser: 'За да започнеш, създай първия си потребител',
|
||||
changePassword: 'Промяна на паролата',
|
||||
checkYourEmailForPasswordReset:
|
||||
'Провери имейла си за връзка, която ще ти позволи да промениш паролата си',
|
||||
'Ако имейл адресът е свързан с акаунт, скоро ще получите инструкции за възстановяване на паролата си. Моля, проверете папката си за спам или нежелана поща, ако не виждате имейла във входящата си поща.',
|
||||
confirmGeneration: 'Потвърди създаването',
|
||||
confirmPassword: 'Потвърди парола',
|
||||
createFirstUser: 'Създай първи потребител',
|
||||
|
||||
@@ -13,7 +13,7 @@ export const csTranslations: DefaultTranslationsObject = {
|
||||
beginCreateFirstUser: 'Začněte vytvořením svého prvního uživatele.',
|
||||
changePassword: 'Změnit heslo',
|
||||
checkYourEmailForPasswordReset:
|
||||
'Zkontrolujte svůj email a najděte v něm odkaz, který vám umožní bezpečně resetovat vaše heslo.',
|
||||
'Pokud je e-mailová adresa spojena s účtem, brzy obdržíte pokyny k resetování vašeho hesla. Pokud e-mail nenajdete ve vaší doručené poště, zkontrolujte prosím složku se spamem nebo nevyžádanou poštou.',
|
||||
confirmGeneration: 'Potvrdit generaci',
|
||||
confirmPassword: 'Potvrdit heslo',
|
||||
createFirstUser: 'Vytvořit prvního uživatele',
|
||||
|
||||
@@ -13,7 +13,7 @@ export const daTranslations: DefaultTranslationsObject = {
|
||||
beginCreateFirstUser: 'For at starte, opret en bruger.',
|
||||
changePassword: 'Skift adgangskode',
|
||||
checkYourEmailForPasswordReset:
|
||||
'Tjek din email for at finde linket der vil give adgang til at ændre din adgangskode',
|
||||
'Hvis e-mailadressen er forbundet med en konto, vil du kort tid modtage instruktioner til at nulstille din adgangskode. Tjek venligst din spam- eller junkmail-mappe, hvis du ikke ser e-mailen i din indbakke.',
|
||||
confirmGeneration: 'Bekræft generering',
|
||||
confirmPassword: 'Bekræft adgangskode',
|
||||
createFirstUser: 'Opret bruger',
|
||||
|
||||
@@ -13,7 +13,7 @@ export const deTranslations: DefaultTranslationsObject = {
|
||||
beginCreateFirstUser: 'Erstelle deinen ersten Benutzer um zu beginnen',
|
||||
changePassword: 'Passwort ändern',
|
||||
checkYourEmailForPasswordReset:
|
||||
'Du solltest eine E-Mail mit einem Link zum sicheren Zurücksetzen deines Passworts erhalten haben.',
|
||||
'Wenn die E-Mail-Adresse mit einem Konto verknüpft ist, erhalten Sie in Kürze Anweisungen zur Zurücksetzung Ihres Passworts. Bitte überprüfen Sie Ihren Spam- oder Junk-Mail-Ordner, wenn Sie die E-Mail nicht in Ihrem Posteingang sehen.',
|
||||
confirmGeneration: 'Generierung bestätigen',
|
||||
confirmPassword: 'Passwort bestätigen',
|
||||
createFirstUser: 'Ersten Benutzer erstellen',
|
||||
|
||||
@@ -13,7 +13,7 @@ export const enTranslations = {
|
||||
beginCreateFirstUser: 'To begin, create your first user.',
|
||||
changePassword: 'Change Password',
|
||||
checkYourEmailForPasswordReset:
|
||||
'Check your email for a link that will allow you to securely reset your password.',
|
||||
"If the email address is associated with an account, you will receive instructions to reset your password shortly. Please check your spam or junk mail folder if you don't see the email in your inbox.",
|
||||
confirmGeneration: 'Confirm Generation',
|
||||
confirmPassword: 'Confirm Password',
|
||||
createFirstUser: 'Create first user',
|
||||
|
||||
@@ -13,7 +13,7 @@ export const esTranslations: DefaultTranslationsObject = {
|
||||
beginCreateFirstUser: 'Para empezar, crea tu primer usuario.',
|
||||
changePassword: 'Cambiar contraseña',
|
||||
checkYourEmailForPasswordReset:
|
||||
'Revisa tu correo con el enlace para restablecer tu contraseña de forma segura.',
|
||||
'Si la dirección de correo electrónico está asociada con una cuenta, recibirá instrucciones para restablecer su contraseña en breve. Por favor, revise su carpeta de spam o correo no deseado si no ve el correo electrónico en su bandeja de entrada.',
|
||||
confirmGeneration: 'Confirmar Generación',
|
||||
confirmPassword: 'Confirmar Contraseña',
|
||||
createFirstUser: 'Crear al primer usuario',
|
||||
|
||||
@@ -13,7 +13,7 @@ export const faTranslations: DefaultTranslationsObject = {
|
||||
beginCreateFirstUser: 'برای آغاز، نخستین کاربر خود را بسازید.',
|
||||
changePassword: 'تغییر گذرواژه',
|
||||
checkYourEmailForPasswordReset:
|
||||
'برای بازیابی ایمن گذرواژه خود، پیامی که به رایانامه شما فرستادیم و دارای پیوند بازنشانی گذرواژه است را بررسی نمایید.',
|
||||
'اگر آدرس ایمیل با یک حساب کاربری مرتبط است، بزودی دستورالعمل هایی برای تنظیم مجدد رمز عبور خود دریافت خواهید کرد. اگر ایمیل را در صندوق ورودی خود نمی بینید، لطفاً پوشه هرزنامه یا ایمیل های غیر مورد نظر خود را بررس',
|
||||
confirmGeneration: 'تأیید ساخت',
|
||||
confirmPassword: 'تأیید گذرواژه',
|
||||
createFirstUser: 'ایجاد کاربر نخست',
|
||||
|
||||
@@ -13,7 +13,7 @@ export const frTranslations: DefaultTranslationsObject = {
|
||||
beginCreateFirstUser: 'Pour commencer, créez votre premier utilisateur.',
|
||||
changePassword: 'Changer le mot de passe',
|
||||
checkYourEmailForPasswordReset:
|
||||
'Vérifiez votre e-mail, nous vous avons envoyé un lien qui vous permettra de réinitialiser votre mot de passe en toute sécurité.',
|
||||
"Si l'adresse e-mail est associée à un compte, vous recevrez sous peu des instructions pour réinitialiser votre mot de passe. Veuillez vérifier votre dossier de courrier indésirable ou de spam si vous ne voyez pas l'e-mail dans votre boîte de réception.",
|
||||
confirmGeneration: 'Confirmer la génération',
|
||||
confirmPassword: 'Confirmez le mot de passe',
|
||||
createFirstUser: 'Créer le premier utilisateur',
|
||||
|
||||
@@ -12,7 +12,8 @@ export const heTranslations: DefaultTranslationsObject = {
|
||||
backToLogin: 'חזרה להתחברות',
|
||||
beginCreateFirstUser: 'כדי להתחיל, יש ליצור את המשתמש הראשון שלך.',
|
||||
changePassword: 'שינוי סיסמה',
|
||||
checkYourEmailForPasswordReset: 'בדוק את תיבת הדוא"ל לאיתור קישור איפוס הסיסמה בצורה מאובטחת.',
|
||||
checkYourEmailForPasswordReset:
|
||||
'אם כתובת הדוא"ל מקושרת לחשבון, תקבל הוראות לאיפוס הסיסמה שלך בקרוב. אנא בדוק את תיקיית הספאם או הדואר הזבל אם אתה לא רואה את הדוא"ל בתיבת הדואר הנכנס שלך.',
|
||||
confirmGeneration: 'אישור יצירה',
|
||||
confirmPassword: 'אישור סיסמה',
|
||||
createFirstUser: 'יצירת משתמש ראשון',
|
||||
|
||||
@@ -13,7 +13,7 @@ export const hrTranslations: DefaultTranslationsObject = {
|
||||
beginCreateFirstUser: 'Za početak, izradite prvog korisnika.',
|
||||
changePassword: 'Promijeni lozinku',
|
||||
checkYourEmailForPasswordReset:
|
||||
'Provjerite e-mail s poveznicom koja će Vam omogućiti sigurnu promjenu lozinke.',
|
||||
'Ako je e-mail adresa povezana s računom, uskoro ćete primiti upute za resetiranje lozinke. Molimo provjerite svoju mapu za neželjenu poštu ili spam ako ne vidite e-mail u svojoj pristigloj pošti.',
|
||||
confirmGeneration: 'Potvrdi generiranje',
|
||||
confirmPassword: 'Potvrdi lozinku',
|
||||
createFirstUser: 'Izradi prvog korisnika',
|
||||
|
||||
@@ -13,7 +13,7 @@ export const huTranslations: DefaultTranslationsObject = {
|
||||
beginCreateFirstUser: 'Kezdésként hozza létre az első felhasználót.',
|
||||
changePassword: 'Jelszó módosítása',
|
||||
checkYourEmailForPasswordReset:
|
||||
'Ellenőrizze az e-mailjét, a linkért, amellyel biztonságosan visszaállíthatja jelszavát.',
|
||||
'Ha az e-mail cím egy fiókhoz van társítva, hamarosan kapni fog utasításokat a jelszó visszaállításához. Kérjük, ellenőrizze a spam vagy a levélszemét mappát, ha nem látja az e-mailt a bejövő üzenetek között.',
|
||||
confirmGeneration: 'Generálás megerősítése',
|
||||
confirmPassword: 'Jelszó megerősítése',
|
||||
createFirstUser: 'Első felhasználó létrehozása',
|
||||
|
||||
@@ -13,7 +13,7 @@ export const itTranslations: DefaultTranslationsObject = {
|
||||
beginCreateFirstUser: 'Per iniziare, crea il tuo primo utente.',
|
||||
changePassword: 'Cambia Password',
|
||||
checkYourEmailForPasswordReset:
|
||||
'Controlla la tua email e clicca sul link che ti permetterà di cambiare in sicurezza la tua password.',
|
||||
"Se l'indirizzo email è associato a un account, riceverai a breve le istruzioni per reimpostare la tua password. Si prega di controllare la cartella dello spam o della posta indesiderata se non vedi l'email nella tua casella di posta.",
|
||||
confirmGeneration: 'Conferma Generazione',
|
||||
confirmPassword: 'Conferma Password',
|
||||
createFirstUser: 'Crea il primo utente',
|
||||
|
||||
@@ -13,7 +13,7 @@ export const jaTranslations: DefaultTranslationsObject = {
|
||||
beginCreateFirstUser: 'まずは、最初のユーザーを作成します。',
|
||||
changePassword: 'パスワードを変更',
|
||||
checkYourEmailForPasswordReset:
|
||||
'パスワードを安全に再設定するためのリンクがメールで送られてくるので、確認してください。',
|
||||
'そのメールアドレスがアカウントに関連付けられている場合、すぐにパスワードをリセットするための指示が送信されます。メールが受信トレイにない場合は、迷惑メールフォルダまたはジャンクメールフォルダを確認してください。',
|
||||
confirmGeneration: '生成の確認',
|
||||
confirmPassword: 'パスワードの確認',
|
||||
createFirstUser: '最初のユーザーを作成',
|
||||
|
||||
@@ -13,7 +13,7 @@ export const koTranslations: DefaultTranslationsObject = {
|
||||
beginCreateFirstUser: '시작하려면 첫 번째 사용자를 생성하세요.',
|
||||
changePassword: '비밀번호 변경',
|
||||
checkYourEmailForPasswordReset:
|
||||
'비밀번호 재설정을 안전하게 수행할 수 있는 링크가 포함된 이메일을 확인하세요.',
|
||||
'이메일 주소가 계정과 연결되어 있다면, 곧 비밀번호를 재설정하는 방법에 대한 지시를 받게 될 것입니다. 인박스에서 이메일을 찾을 수 없다면 스팸 또는 정크 메일 폴더를 확인해 주시기 바랍니다.',
|
||||
confirmGeneration: '생성 확인',
|
||||
confirmPassword: '비밀번호 확인',
|
||||
createFirstUser: '첫 번째 사용자 생성',
|
||||
|
||||
@@ -13,7 +13,7 @@ export const myTranslations: DefaultTranslationsObject = {
|
||||
beginCreateFirstUser: 'စတင်နိုင်ရန် ပထမဦးစွာ အသုံးပြုသူအား ဖန်တီးပါ။',
|
||||
changePassword: 'စကားဝှက် ပြောင်းလဲမည်။',
|
||||
checkYourEmailForPasswordReset:
|
||||
'သင့်စကားဝှက်ကို ပြန်လည်သတ်မှတ်နိုင်ရန်အတွက် မေးလ်ပို့ထားပါသည်။',
|
||||
'Jika alamat e-mel dikaitkan dengan akaun, anda akan menerima arahan untuk menetapkan semula kata laluan anda sebentar lagi. Sila semak folder spam atau junk mail anda jika anda tidak melihat e-mel di kotak masuk anda.',
|
||||
confirmGeneration: 'Generation အတည်ပြု',
|
||||
confirmPassword: 'စကားဝှက်အား ထပ်မံ ရိုက်ထည့်ပါ။',
|
||||
createFirstUser: 'ပထမဆုံး အသုံးပြုသူကို ဖန်တီးပါ။',
|
||||
|
||||
@@ -13,7 +13,7 @@ export const nbTranslations: DefaultTranslationsObject = {
|
||||
beginCreateFirstUser: 'Opprett din første bruker for å fortsette.',
|
||||
changePassword: 'Endre passord',
|
||||
checkYourEmailForPasswordReset:
|
||||
'Sjekk e-posten din for en lenke som lar deg tilbakestille passordet ditt.',
|
||||
'Hvis e-postadressen er knyttet til en konto, vil du motta instruksjoner for å tilbakestille passordet ditt snart. Vennligst sjekk spam- eller junk mail-mappen din hvis du ikke ser e-posten i innboksen din.',
|
||||
confirmGeneration: 'Bekreft generering',
|
||||
confirmPassword: 'Bekreft passord',
|
||||
createFirstUser: 'Opprett første bruker',
|
||||
|
||||
@@ -13,7 +13,7 @@ export const nlTranslations: DefaultTranslationsObject = {
|
||||
beginCreateFirstUser: 'Om te beginnen maakt u uw eerste gebruiker aan.',
|
||||
changePassword: 'Wachtwoord wijzigen',
|
||||
checkYourEmailForPasswordReset:
|
||||
'Controleer uw e-mail voor een link waarmee u uw wachtwoord veilig opnieuw kunt instellen.',
|
||||
'Als het e-mailadres is gekoppeld aan een account, ontvangt u binnenkort instructies om uw wachtwoord opnieuw in te stellen. Controleer uw spam- of ongewenste e-mailmap als u de e-mail niet in uw inbox ziet.',
|
||||
confirmGeneration: 'Bevestigen',
|
||||
confirmPassword: 'Wachtwoord bevestigen',
|
||||
createFirstUser: 'Eerste gebruiker aanmaken',
|
||||
|
||||
@@ -13,7 +13,7 @@ export const plTranslations: DefaultTranslationsObject = {
|
||||
beginCreateFirstUser: 'Aby rozpocząć, utwórz pierwszego użytkownika',
|
||||
changePassword: 'Zmień hasło',
|
||||
checkYourEmailForPasswordReset:
|
||||
'Sprawdź email, na który wysłano link, który pozwoli Ci bezpiecznie zresetować hasło.',
|
||||
'Jeśli adres e-mail jest powiązany z kontem, wkrótce otrzymasz instrukcje dotyczące zresetowania hasła. Sprawdź folder ze spamem lub niechcianą pocztą, jeśli nie widzisz e-maila w swojej skrzynce odbiorczej.',
|
||||
confirmGeneration: 'Potwierdź wygenerowanie',
|
||||
confirmPassword: 'Potwierdź hasło',
|
||||
createFirstUser: 'Utwórz pierwszego użytkownika',
|
||||
|
||||
@@ -13,7 +13,7 @@ export const ptTranslations: DefaultTranslationsObject = {
|
||||
beginCreateFirstUser: 'Para começar, crie seu primeiro usuário.',
|
||||
changePassword: 'Mudar senha',
|
||||
checkYourEmailForPasswordReset:
|
||||
'Verifique seu email para um link que permitirá que você redefina sua senha com segurança.',
|
||||
'Se o endereço de email estiver associado a uma conta, você receberá instruções para redefinir sua senha em breve. Por favor, verifique sua pasta de spam ou lixo eletrônico se você não vir o email na sua caixa de entrada.',
|
||||
confirmGeneration: 'Confirmar Geração',
|
||||
confirmPassword: 'Confirmar Senha',
|
||||
createFirstUser: 'Criar primeiro usuário',
|
||||
|
||||
@@ -13,7 +13,7 @@ export const roTranslations: DefaultTranslationsObject = {
|
||||
beginCreateFirstUser: 'Pentru a începe, creați primul utilizator.',
|
||||
changePassword: 'Schimbați parola',
|
||||
checkYourEmailForPasswordReset:
|
||||
'Verificați emailul pentru un link care vă va permite să vă resetați parola în siguranță.',
|
||||
'Dacă adresa de e-mail este asociată cu un cont, veți primi în curând instrucțiuni pentru resetarea parolei voastre. Vă rugăm să verificați dosarul de spam sau de mesaje nedorite dacă nu vedeți e-mailul în inbox-ul dvs.',
|
||||
confirmGeneration: 'Confirmați generarea',
|
||||
confirmPassword: 'Confirmați parola',
|
||||
createFirstUser: 'Creați primul utilizator',
|
||||
|
||||
@@ -12,7 +12,8 @@ export const rsTranslations: DefaultTranslationsObject = {
|
||||
backToLogin: 'Назад на пријаву',
|
||||
beginCreateFirstUser: 'На самом почетку креирај свог првог корисника',
|
||||
changePassword: 'Промени лозинку',
|
||||
checkYourEmailForPasswordReset: 'Проверите е-пошту и поруку са линком за промену лозинке.',
|
||||
checkYourEmailForPasswordReset:
|
||||
'Ako je e-mail adresa povezana sa nalogom, uskoro ćete dobiti uputstva za resetovanje vaše lozinke. Ako ne vidite e-mail u vašem inboxu, molimo vas da proverite vašu folder za spam ili neželjene poruke.',
|
||||
confirmGeneration: 'Потврди креирање',
|
||||
confirmPassword: 'Потврди лозинку',
|
||||
createFirstUser: 'Креирај првог корисника',
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user