Compare commits

...

46 Commits

Author SHA1 Message Date
Elliot DeNolf
a8ac5d2cfd chore(release): db-postgres/0.2.1 [skip ci] 2023-12-06 13:37:13 -05:00
Elliot DeNolf
e6318ae700 chore(release): richtext-slate/1.3.0 [skip ci] 2023-12-06 13:36:38 -05:00
Elliot DeNolf
f5166cd4c4 chore(release): richtext-lexical/0.4.0 [skip ci] 2023-12-06 13:36:05 -05:00
Elliot DeNolf
7c7bf51b4b chore(richtext-lexical): bump payload peer dep 2023-12-06 13:35:27 -05:00
Elliot DeNolf
eefcd88de7 chore(release): payload/2.4.0 [skip ci] 2023-12-06 13:34:08 -05:00
Alessio Gravili
217cc1fc42 docs: update CONTRIBUTING.md (#4401) 2023-12-06 19:15:17 +01:00
Alessio Gravili
7af8f29b4a feat(richtext-lexical): Link & Relationship Feature: field-level configurable allowed relationships (#4182)
* feat(richtext-lexical): ability to configure link feature enabled relations on a field-level

* feat(richtext-lexical): ability to configure Relationship feature enabled relations on a field-level

* chore(richtext-lexical): Improve Link feature props typing

* chore(richtext-lexical): Improve Link and Relationship feature props typing

* fix(richtext-lexical): Link drawer types

* chore: merge conflict resolve

* chore(richtext-lexical): Link Feature: add comments that explain how getBaseFields works
2023-12-06 17:50:50 +01:00
chris
5f2cd1ae77 docs: fix example link (#4394) 2023-12-06 09:35:30 -05:00
Dan Ribbens
dbaecda0e9 fix(db-postgres): sorting on a not-configured field throws error (#4382) 2023-12-06 09:27:43 -05:00
Tylan Davis
cf9a3704df fix: handles null upload field values (#4397) 2023-12-06 09:26:19 -05:00
Patrik
4b5453e8e5 fix: simplifies query validation and fixes nested relationship fields (#4391) 2023-12-06 08:47:34 -05:00
Alessio Gravili
5de347ffff feat(richtext-lexical)!: lazy import React components to prevent client-only code from leaking into the server (#4290)
* chore(richtext-lexical): lazy import all React things

* chore(richtext-lexical): use useMemo for lazy-loaded React Components to prevent lag and flashes when parent component re-renders

* chore: make exportPointerFiles.ts script usable for other packages as well by hoisting it up to the workspace root and making it configurable

* chore(richtext-lexical): make sure no client-side code is imported by default from Features

* chore(richtext-lexical): remove unnecessary scss files

* chore(richtext-lexical): adjust package.json exports

* chore(richtext-*): lazy-import Field & Cell Components, move Client-only exports to /components subpath export

* chore(richtext-lexical): make sure nothing client-side is directly exported from the / subpath export anymore

* add missing imports

* chore: remove breaking changes for Slate

* LazyCellComponent & LazyFieldComponent
2023-12-06 14:20:18 +01:00
Jacob Fletcher
80ef18c149 fix(templates/website): removes unused form builder plugin from deps (#4385) 2023-12-05 11:57:21 -05:00
Christian May
912abe2b64 docs(plugin-nested-docs): clarifies that relationships are intra-collection (#4375) 2023-12-05 00:58:36 -05:00
Jacob Fletcher
4090aebb0e fix(live-preview): populates rte uploads and relationships (#4379) 2023-12-05 00:53:11 -05:00
chris
290e9d8238 docs: fixes typo in migrations (#4374) 2023-12-05 00:24:37 -05:00
Kane Wang
50253f617c feat: add Chinese Traditional translation (#4372) 2023-12-04 15:29:29 -05:00
geisterfurz007
999e05d1b4 docs: fix typo in migrations (#4356) 2023-12-04 15:09:21 -05:00
Dan Ribbens
b6cffcea07 fix: defaultValues computed on new globals (#4380) 2023-12-04 15:05:47 -05:00
Patrik
7b2eb0c175 docs: updates afterInput example (#4378) 2023-12-04 14:35:08 -05:00
Timothy Choi
3b8a27d199 feat: pass path to FieldDescription (#4364)
fix: DescriptionFunction type

Co-authored-by: Jarrod Flesch <jarrodmflesch@gmail.com>
2023-12-04 13:59:18 -05:00
Jessica Chowdhury
65adfd21ed fix: uploads files after validation (#4218) 2023-12-04 12:38:23 -05:00
Jacob Fletcher
03a387233d fix(live-preview): sends raw js objects through window.postMessage instead of json (#4354) 2023-12-01 17:50:55 -05:00
Jessica Chowdhury
fcbe5744d9 fix: upload editing error with plugin-cloud (#4170)
Co-authored-by: Jarrod Flesch <jarrodmflesch@gmail.com>
2023-12-01 16:35:12 -05:00
Jessica Chowdhury
06bf6a426e docs: destructures vite bundler import (#4280) 2023-12-01 16:32:10 -05:00
Renat Sagdeev
b634d5e552 docs: fix ul and ol tags mentioned in lexical rte (#4338) 2023-12-01 16:29:43 -05:00
Jacob Fletcher
5f173241df feat: async live preview urls (#4339) 2023-12-01 16:25:39 -05:00
Markus Machatschek
0bd12e01d7 docs: update software requirements to mention pnpm (#4335)
Co-authored-by: Jacob Fletcher <jacobsfletch@gmail.com>
2023-12-01 16:25:21 -05:00
Elliot DeNolf
b6f02765eb chore(release): richtext-lexical/0.3.1 [skip ci] 2023-12-01 16:19:58 -05:00
Elliot DeNolf
156ffdd18c chore(release): payload/2.3.1 [skip ci] 2023-12-01 16:18:48 -05:00
Jarrod Flesch
fe888b5f6c fix: query validation on relationship fields (#4353) 2023-12-01 16:03:58 -05:00
Alessio Gravili
bea79feaea fix: ensure doc controls are not hidden behind lexical field (#4345) 2023-12-01 10:23:39 +01:00
Alessio Gravili
293cee6f90 chore(plugin-stripe): rename build script to _build (#4344) 2023-12-01 10:04:37 +01:00
Alessio Gravili
3e745e91da fix(richtext-lexical): blocks content may be hidden behind components outside of the editor (#4325)
* chore(richtext-lexical): add e2e test to reproduce the issue

* fix the issue
2023-12-01 09:46:21 +01:00
Alessio Gravili
4243048fc5 Merge pull request #4342 from payloadcms/fix/lexical-blocks-v1-conversion
fix(richtext-lexical): Blocks node: incorrect conversion from v1 node to v2 node
2023-12-01 09:29:57 +01:00
Alessio Gravili
ef84a2cfff fix(richtext-lexical): Blocks node: incorrect conversion from v1 node to v2 node 2023-12-01 09:28:44 +01:00
James
c00cbaabbc chore: lints 2023-11-30 18:34:38 -05:00
James
02f407e995 chore: lints 2023-11-30 17:12:01 -05:00
Elliot DeNolf
74e8051bb6 chore(templates): GRAPHQL_API_URL to ecommerce template 2023-11-30 17:01:19 -05:00
James
ee670b2b20 chore: introduces graphql_api_url to website template 2023-11-30 16:58:30 -05:00
Elliot DeNolf
2f8bcc977b fix(templates): adjust graphql endpoint 2023-11-30 16:19:46 -05:00
Dan Ribbens
0cc91d7377 docs: update mongoOptions to connectOptions (#4334) 2023-11-30 12:39:32 -05:00
Elliot DeNolf
34e89ff5db chore(release): richtext-slate/1.2.1 [skip ci] 2023-11-30 11:11:28 -05:00
Elliot DeNolf
b39b52dbd3 chore(release): live-preview-react/0.2.0 [skip ci] 2023-11-30 11:11:03 -05:00
Elliot DeNolf
7bfdb2627a chore(release): live-preview/0.2.0 [skip ci] 2023-11-30 11:10:55 -05:00
Elliot DeNolf
8f5867e876 chore(release): db-postgres/0.2.0 [skip ci] 2023-11-30 11:10:44 -05:00
197 changed files with 3061 additions and 881 deletions

View File

@@ -1,3 +1,89 @@
## [2.4.0](https://github.com/payloadcms/payload/compare/v2.3.1...v2.4.0) (2023-12-06)
### Features
* add Chinese Traditional translation ([#4372](https://github.com/payloadcms/payload/issues/4372)) ([50253f6](https://github.com/payloadcms/payload/commit/50253f617c22d0d185bbac7f9d4304cddbc01f06))
* async live preview urls ([#4339](https://github.com/payloadcms/payload/issues/4339)) ([5f17324](https://github.com/payloadcms/payload/commit/5f173241df6dc316d498767b1c81718e9c2b9a51))
* pass path to FieldDescription ([#4364](https://github.com/payloadcms/payload/issues/4364)) ([3b8a27d](https://github.com/payloadcms/payload/commit/3b8a27d199b3969cbca6ca750450798cb70f21e8))
* **richtext-lexical:** lazy import React components to prevent client-only code from leaking into the server ([#4290](https://github.com/payloadcms/payload/issues/4290)) ([5de347f](https://github.com/payloadcms/payload/commit/5de347ffffca3bf38315d3d87d2ccc5c28cd2723))
* **richtext-lexical:** Link & Relationship Feature: field-level configurable allowed relationships ([#4182](https://github.com/payloadcms/payload/issues/4182)) ([7af8f29](https://github.com/payloadcms/payload/commit/7af8f29b4a8dddf389356e4db142f8d434cdc964))
### Bug Fixes
* **db-postgres:** sorting on a not-configured field throws error ([#4382](https://github.com/payloadcms/payload/issues/4382)) ([dbaecda](https://github.com/payloadcms/payload/commit/dbaecda0e92fcb0fa67b4c5ac085e025f02de53a))
* defaultValues computed on new globals ([#4380](https://github.com/payloadcms/payload/issues/4380)) ([b6cffce](https://github.com/payloadcms/payload/commit/b6cffcea07b9fa21698b00b8bbed6f27197ded41))
* handles null upload field values ([#4397](https://github.com/payloadcms/payload/issues/4397)) ([cf9a370](https://github.com/payloadcms/payload/commit/cf9a3704df21ce8b32feb0680793cba804cd66f7))
* **live-preview:** populates rte uploads and relationships ([#4379](https://github.com/payloadcms/payload/issues/4379)) ([4090aeb](https://github.com/payloadcms/payload/commit/4090aebb0e94e776258f0c1c761044a4744a1857))
* **live-preview:** sends raw js objects through window.postMessage instead of json ([#4354](https://github.com/payloadcms/payload/issues/4354)) ([03a3872](https://github.com/payloadcms/payload/commit/03a387233d1b8876a2fcaa5f3b3fd5ed512c0bc4))
* simplifies query validation and fixes nested relationship fields ([#4391](https://github.com/payloadcms/payload/issues/4391)) ([4b5453e](https://github.com/payloadcms/payload/commit/4b5453e8e5484f7afcadbf5bccf8369b552969c6))
* upload editing error with plugin-cloud ([#4170](https://github.com/payloadcms/payload/issues/4170)) ([fcbe574](https://github.com/payloadcms/payload/commit/fcbe5744d945dc43642cdaa2007ddc252ecafafa))
* uploads files after validation ([#4218](https://github.com/payloadcms/payload/issues/4218)) ([65adfd2](https://github.com/payloadcms/payload/commit/65adfd21ed538b79628dc4f8ce9e1a5a1bba6aed))
### ⚠ BREAKING CHANGES
* **richtext-lexical:** lazy import React components to prevent client-only code from leaking into the server (#4290)
### ⚠️ @payloadcms/richtext-lexical
Most important: If you are updating `@payloadcms/richtext-lexical` to v0.4.0 or higher, you will HAVE to update `payload` to the latest version as well. If you don't update it, payload likely won't start up due to validation errors. It's generally good practice to upgrade packages prefixed with `@payloadcms/` together with `payload` and keep the versions in sync.
`@payloadcms/richtext-slate` is not affected by this.
Every single property in the `Feature` interface which accepts a React component now no longer accepts a React component, but a function which imports a React component instead. This is done to ensure no unnecessary client-only code is leaked to the server when importing Features on a server.
Here's an example migration:
Old:
```ts
import { BlockIcon } from '../../lexical/ui/icons/Block'
...
Icon: BlockIcon,
```
New:
```ts
// import { BlockIcon } from '../../lexical/ui/icons/Block' // <= Remove this import
...
Icon: () =>
// @ts-expect-error
import('../../lexical/ui/icons/Block').then((module) => module.BlockIcon),
```
Or alternatively, if you're using default exports instead of named exports:
```ts
// import BlockIcon from '../../lexical/ui/icons/Block' // <= Remove this import
...
Icon: () =>
// @ts-expect-error
import('../../lexical/ui/icons/Block'),
```
The types for `SanitizedEditorConfig` and `EditorConfig` have changed. Their respective `lexical` property no longer expects the `LexicalEditorConfig`. It now expects a function returning the `LexicalEditorConfig`. You will have to adjust this if you adjusted that property anywhere, e.g. when initializing the lexical field editor property, or when initializing a new headless editor.
The following exports are now exported from the `@payloadcms/richtext-lexical/components` subpath exports instead of `@payloadcms/richtext-lexical`:
- ToolbarButton
- ToolbarDropdown
- RichTextCell
- RichTextField
- defaultEditorLexicalConfig
You will have to adjust your imports, only if you import any of those properties in your project.
## [2.3.1](https://github.com/payloadcms/payload/compare/v2.3.0...v2.3.1) (2023-12-01)
### Bug Fixes
* ensure doc controls are not hidden behind lexical field ([#4345](https://github.com/payloadcms/payload/issues/4345)) ([bea79fe](https://github.com/payloadcms/payload/commit/bea79feaeaee18bf94dd04262f134483f1468494))
* query validation on relationship fields ([#4353](https://github.com/payloadcms/payload/issues/4353)) ([fe888b5](https://github.com/payloadcms/payload/commit/fe888b5f6ceaa3969eac759cbdfb109b106dae05))
* **richtext-lexical:** blocks content may be hidden behind components outside of the editor ([#4325](https://github.com/payloadcms/payload/issues/4325)) ([3e745e9](https://github.com/payloadcms/payload/commit/3e745e91da620a00e3f0f91892ee3ec66ba72bc0))
* **richtext-lexical:** Blocks node: incorrect conversion from v1 node to v2 node ([ef84a2c](https://github.com/payloadcms/payload/commit/ef84a2cfffbb1be52dd948e59eeec0ce324e9046))
## [2.3.0](https://github.com/payloadcms/payload/compare/v2.2.2...v2.3.0) (2023-11-30)

View File

@@ -14,7 +14,7 @@ If you find a vulnerability within the core Payload repository, and we determine
## Documentation edits
Payload documentation can be found directly within its codebase and you can feel free to make changes / improvements to any of it through opening a PR. We utilize these files directly in our website and will periodically deploy documentation updates as necessary.
Payload documentation can be found directly within its codebase, and you can feel free to make changes / improvements to any of it through opening a PR. We utilize these files directly in our website and will periodically deploy documentation updates as necessary.
## Building additional features
@@ -30,9 +30,17 @@ Our design review ensures that proposed changes fit seamlessly with other compon
To help us work on new features, you can create a new feature request post in [GitHub Discussion](https://github.com/payloadcms/payload/discussions) or discuss it in our [Discord](https://discord.com/invite/payload). New functionality often has large implications across the entire Payload repo, so it is best to discuss the architecture and approach before starting work on a pull request.
### Installation & Requirements
Payload is structured as a Monorepo, encompassing not only the core Payload platform but also various plugins and packages. To install all required dependencies, you have to run `pnpm install` once in the root directory. **PNPM IS REQUIRED!** Yarn or npm will not work - you will have to use pnpm to develop in the core repository. In most systems, the easiest way to install pnpm is to run `corepack enable` in your terminal.
If you're coming from a very outdated version of payload, it is recommended to nuke the node_modules folder before running pnpm install. On UNIX systems, you can easily do that using the `pnpm clean:unix` command, which will delete all node_modules folders and build artefacts.
It is also recommended to use at least Node v18 or higher. You can check your current node version by typing `node --version` in your terminal. The easiest way to switch between different node versions is to use [nvm](https://github.com/nvm-sh/nvm#intro).
### Code
Most new functionality should keep testing in mind. With 1.0, testability of new features has been vastly improved. All top-level directories within the `test/` directory are for testing a specific category: `fields`, `collections`, etc.
Most new functionality should keep testing in mind. All top-level directories within the `test/` directory are for testing a specific category: `fields`, `collections`, etc.
If it makes sense to add your feature to an existing test directory, please do so.
@@ -49,21 +57,35 @@ A typical directory with `test/` will be structured like this:
- `config.ts` - This is the _granular_ Payload config for testing. It should be as lightweight as possible. Reference existing configs for an example
- `int.spec.ts` - This is the test file run by jest. Any test file must have a `*int.spec.ts` suffix.
- `e2e.spec.ts` - This is the end-to-end test file that will load up the admin UI using the above config and run Playwright tests. These tests are typically only needed if a large change is being made to the Admin UI.
- `payload-types.ts` - Generated types from `config.ts`. Generate this file by running `pnpm dev:generate-types my-test-dir`.
- `payload-types.ts` - Generated types from `config.ts`. Generate this file by running `pnpm dev:generate-types my-test-dir`. Replace `my-test-dir` with the name of your testing directory.
The directory split up in this way specifically to reduce friction when creating tests and to add the ability to boot up Payload with that specific config.
Each test directory is split up in this way specifically to reduce friction when creating tests and to add the ability to boot up Payload with that specific config.
The following command will start Payload with your config: `pnpm dev my-test-dir`. This command will start up Payload using your config and refresh a test database on every restart.
The following command will start Payload with your config: `pnpm dev my-test-dir`. Example: `pnpm dev fields` for the test/`fields` test suite. This command will start up Payload using your config and refresh a test database on every restart. If you're using VS Code, the most common run configs are automatically added to your editor - you should be able to find them in your VS Code launch tab.
By default, it will automatically log you in with the default credentials. To disable that, you can either pass in the --no-auto-login flag (example: `pnpm dev my-test-dir --no-auto-login`) or set the `PAYLOAD_PUBLIC_DISABLE_AUTO_LOGIN` environment variable to `false`.
By default, payload will [automatically log you in](https://payloadcms.com/docs/authentication/config#admin-autologin) with the default credentials. To disable that, you can either pass in the --no-auto-login flag (example: `pnpm dev my-test-dir --no-auto-login`) or set the `PAYLOAD_PUBLIC_DISABLE_AUTO_LOGIN` environment variable to `false`.
If you wish to use to your own Mongo database for the `test` directory instead of using the in memory database, all you need to do is add the following env vars to the `test/dev.ts` file:
The default credentials are `dev@payloadcms.com` as E-Mail and `test` as password. These are used in the auto-login.
### Testing with your own MongoDB database
If you wish to use your own MongoDB database for the `test` directory instead of using the in memory database, all you need to do is add the following env vars to the `test/dev.ts` file:
- `process.env.NODE_ENV`
- `process.env.PAYLOAD_TEST_MONGO_URL`
- Simply set `process.env.NODE_ENV` to `test` and set `process.env.PAYLOAD_TEST_MONGO_URL` to your mongo url e.g. `mongodb://127.0.0.1/your-test-db`.
- Simply set `process.env.NODE_ENV` to `test` and set `process.env.PAYLOAD_TEST_MONGO_URL` to your MongoDB URL e.g. `mongodb://127.0.0.1/your-test-db`.
NOTE: It is recommended to add the test credentials (located in `test/credentials.ts`) to your autofill for `localhost:3000/admin` as this will be required on every nodemon restart. The default credentials are `dev@payloadcms.com` as E-Mail and `test` as password.
### Using Postgres
If you have postgres installed on your system, you can also run the test suites using postgres. By default, mongodb is used.
To do that, simply set the `PAYLOAD_DATABASE` environment variable to `postgres`.
### Running the e2e and int tests
You can run the entire test suite using `pnpm test`. If you wish to only run e2e tests, you can use `pnpm test:e2e`. If you wish to only run int tests, you can use `pnpm test:int`.
By default, `pnpm test:int` will only run int test against MongoDB. To run int tests against postgres, you can use `pnpm test:int:postgres`. You will have to have postgres installed on your system for this to work.
### Commits

View File

@@ -572,13 +572,15 @@ With these properties you can add multiple components before and after the input
```tsx
import React from 'react'
import { Field } from 'payload/types'
import './style.scss'
const ClearButton: React.FC = () => {
return <button onClick={() => {/* ... */}}>X</button>
}
const fieldField: Field = {
const titleField: Field = {
name: 'title',
type: 'text',
admin: {

View File

@@ -21,7 +21,7 @@ Then you will need to add the [bundler](/docs/admin/bundlers) to your Payload co
```ts
import { buildConfig } from '@payloadcms/config'
import viteBundler from '@payloadcms/bundler-vite'
import { viteBundler } from '@payloadcms/bundler-vite'
export default buildConfig({
collections: [],

View File

@@ -59,7 +59,7 @@ export default Nav
#### Global config example
You can find an [example Global config](https://github.com/payloadcms/public-demo/blob/master/src/payload/globals/MainMenu.ts) in the Public Demo source code on GitHub.
You can find a few [example Global configs](https://github.com/payloadcms/public-demo/tree/master/src/payload/globals) in the Public Demo source code on GitHub.
### Admin options

View File

@@ -48,7 +48,7 @@ export async function down({ payload }: MigrateDownArgs): Promise<void> {
### Migrations Directory
Each DB adapter has an optional property `migrationDir` where you can override where you want your migrations to be stored/read. If this is not specified, Payload will check the default and possibly make a best effort to find your migrations directory by searching in common locations ie. `./src/migrations`, `./dist/igrations`, `./migrations`, etc.
Each DB adapter has an optional property `migrationDir` where you can override where you want your migrations to be stored/read. If this is not specified, Payload will check the default and possibly make a best effort to find your migrations directory by searching in common locations ie. `./src/migrations`, `./dist/migrations`, `./migrations`, etc.
All database adapters should implement similar migration patterns, but there will be small differences based on the adapter and its specific needs. Below is a list of all migration commands that should be supported by your database adapter.

View File

@@ -251,11 +251,16 @@ const field = {
### Description
A description can be configured three ways.
A description can be configured in three ways.
- As a string
- As a function that accepts an object containing the field's value, which returns a string
- As a React component that accepts value as a prop
- As a function which returns a string
- As a React component
Functions are called with an optional argument object with the following shape, and React components are rendered with the following props:
- `path` - the path of the field
- `value` - the current value of the field
As shown above, you can simply provide a string that will show by the field, but there are use cases where you may want to create some dynamic feedback. By using a function or a component for the `description` property you can provide realtime feedback as the user interacts with the form.
@@ -269,8 +274,8 @@ As shown above, you can simply provide a string that will show by the field, but
type: 'text',
maxLength: 20,
admin: {
description: ({ value }) =>
`${typeof value === 'string' ? 20 - value.length : '20'} characters left`,
description: ({ path, value }) =>
`${typeof value === 'string' ? 20 - value.length : '20'} characters left (field: ${path})`,
},
},
]
@@ -290,11 +295,12 @@ This example will display the number of characters allowed as the user types.
maxLength: 20,
admin: {
description:
({ value }) => (
({ path, value }) => (
<div>
Character count:
{' '}
{ value?.length || 0 }
(field: {path})
</div>
)
}
@@ -303,7 +309,7 @@ This example will display the number of characters allowed as the user types.
}
```
This component will count the number of characters entered.
This component will count the number of characters entered, as well as display the path of the field.
### TypeScript

View File

@@ -10,7 +10,7 @@ keywords: documentation, getting started, guide, Content Management System, cms,
Payload requires the following software:
- Yarn or NPM
- Any JavaScript package manager (Yarn, NPM, or pnpm)
- Node.js version 16+
- Any [compatible database](/docs/database/overview) (MongoDB or Postgres)

View File

@@ -11,7 +11,8 @@ keywords: deployment, production, config, configuration, documentation, Content
launch. <strong>Awesome! Great work!</strong> Now, what's next?
</Banner>
There are many ways to deploy Payload to a production environment. When evaluating how you will deploy Payload, you need to consider these main aspects:
There are many ways to deploy Payload to a production environment. When evaluating how you will deploy Payload, you need
to consider these main aspects:
1. [Basics](#basics)
1. [Security](#security)
@@ -21,19 +22,26 @@ There are many ways to deploy Payload to a production environment. When evaluati
## Basics
In order for Payload to run, it requires both the server code and the built admin panel. These will be the `dist` and `build` directories by default. If you've used `create-payload-app` to create your project, executing the `build` npm script will build both and output these directories.
In order for Payload to run, it requires both the server code and the built admin panel. These will be the `dist`
and `build` directories by default. If you've used `create-payload-app` to create your project, executing the `build`
npm script will build both and output these directories.
## Security
Payload features a suite of security features that you can rely on to strengthen your application's security. When deploying to Production, it's a good idea to double-check that you are making proper use of each of them.
Payload features a suite of security features that you can rely on to strengthen your application's security. When
deploying to Production, it's a good idea to double-check that you are making proper use of each of them.
##### The Secret Key
When you initialize Payload, you provide it with a `secret` property. This property should be impossible to guess and extremely difficult for brute-force attacks to crack. Make sure your Production `secret` is a long, complex string. It's often best practice to store it in an `env` file which is not checked into your Git repository, using `dotenv` to supply it to your `payload.init` call.
When you initialize Payload, you provide it with a `secret` property. This property should be impossible to guess and
extremely difficult for brute-force attacks to crack. Make sure your Production `secret` is a long, complex string. It's
often best practice to store it in an `env` file which is not checked into your Git repository, using `dotenv` to supply
it to your `payload.init` call.
##### Double-check and thoroughly test all Access Control
Because _**you**_ are in complete control of who can do what with your data, you should double and triple-check that you wield that power responsibly before deploying to Production.
Because _**you**_ are in complete control of who can do what with your data, you should double and triple-check that you
wield that power responsibly before deploying to Production.
<Banner type="error">
<strong>By default, all Access Control functions require that a user is successfully logged in to Payload to create, read, update, or delete data.</strong>{' '}
@@ -44,7 +52,8 @@ Because _**you**_ are in complete control of who can do what with your data, you
##### Building the Admin panel
Before running in Production, you need to have built a production-ready copy of the Payload Admin panel. To do this, Payload provides the `build` NPM script. You can use it by adding a `script` to your `package.json` file like this:
Before running in Production, you need to have built a production-ready copy of the Payload Admin panel. To do this,
Payload provides the `build` NPM script. You can use it by adding a `script` to your `package.json` file like this:
`package.json`:
@@ -60,19 +69,26 @@ Before running in Production, you need to have built a production-ready copy of
}
```
Then, to build Payload, you would run `npm run build` in your project folder. A production-ready Admin bundle will be created in the `build` directory.
Then, to build Payload, you would run `npm run build` in your project folder. A production-ready Admin bundle will be
created in the `build` directory.
##### Setting Node to Production
Make sure you set the environment variable `NODE_ENV` to `production`. Based on this variable, many Node packages automatically optimize themselves. In production, Payload automatically disables the [GraphQL Playground](/docs/graphql/overview#graphql-playground), serves a production-ready version of the Admin panel, and other changes.
Make sure you set the environment variable `NODE_ENV` to `production`. Based on this variable, many Node packages
automatically optimize themselves. In production, Payload automatically disables
the [GraphQL Playground](/docs/graphql/overview#graphql-playground), serves a production-ready version of the Admin
panel, and other changes.
##### Secure Cookie Settings
You should be using an SSL certificate for production Payload instances, which means you can [enable secure cookies](/docs/authentication/config) in your Authentication-enabled Collection configs.
You should be using an SSL certificate for production Payload instances, which means you
can [enable secure cookies](/docs/authentication/config) in your Authentication-enabled Collection configs.
##### Preventing API Abuse
Payload comes with a robust set of built-in anti-abuse measures, such as locking out users after X amount of failed login attempts, request rate limiting, GraphQL query complexity limits, max `depth` settings, and more. [Click here to learn more](/docs/production/preventing-abuse).
Payload comes with a robust set of built-in anti-abuse measures, such as locking out users after X amount of failed
login attempts, request rate limiting, GraphQL query complexity limits, max `depth` settings, and
more. [Click here to learn more](/docs/production/preventing-abuse).
## MongoDB
@@ -80,11 +96,18 @@ Payload can be used with any MongoDB compatible database including AWS DocumentD
##### Managing MongoDB yourself
If you are using a [persistent filesystem-based cloud host](#persistent-vs-ephemeral-filesystems) such as a [DigitalOcean Droplet](https://www.digitalocean.com/products/droplets/) or an [Amazon EC2](https://aws.amazon.com/ec2/?ec2-whats-new.sort-by=item.additionalFields.postDateTime&ec2-whats-new.sort-order=desc) server, you might opt to install MongoDB directly on that server itself so that Node can communicate with it locally. With this approach, you can benefit from faster response times, but scaling can become more involved as your app's user base grows.
If you are using a [persistent filesystem-based cloud host](#persistent-vs-ephemeral-filesystems) such as
a [DigitalOcean Droplet](https://www.digitalocean.com/products/droplets/) or
an [Amazon EC2](https://aws.amazon.com/ec2/?ec2-whats-new.sort-by=item.additionalFields.postDateTime&ec2-whats-new.sort-order=desc)
server, you might opt to install MongoDB directly on that server itself so that Node can communicate with it locally.
With this approach, you can benefit from faster response times, but scaling can become more involved as your app's user
base grows.
##### Letting someone else do it
Alternatively, you can rely on a third-party MongoDB host such as [MongoDB Atlas](https://www.mongodb.com/). With Atlas or a similar cloud provider, you can trust them to take care of your database's availability, security, redundancy, and backups.
Alternatively, you can rely on a third-party MongoDB host such as [MongoDB Atlas](https://www.mongodb.com/). With Atlas
or a similar cloud provider, you can trust them to take care of your database's availability, security, redundancy, and
backups.
<Banner type="warning">
<strong>Note:</strong>
@@ -98,21 +121,31 @@ Alternatively, you can rely on a third-party MongoDB host such as [MongoDB Atlas
##### DocumentDB
When using AWS DocumentDB, you will need to configure connection options for authentication in the `mongoOptions` passed to `payload.init`. You also need to set `mongoOptions.useFacet` to `false` to disable use of the unsupported `$facet` aggregation.
When using AWS DocumentDB, you will need to configure connection options for authentication in the `connectOptions`
passed to the `mongooseAdapter` . You also need to set `connectOptions.useFacet` to `false` to disable use of the
unsupported `$facet` aggregation.
##### CosmosDB
When using Azure Cosmos DB, an index is needed for any field you may want to sort on. To add the sort index for all fields that may be sorted in the admin UI use the <a href="/docs/configuration/overview">indexSortableFields</a> configuration option.
When using Azure Cosmos DB, an index is needed for any field you may want to sort on. To add the sort index for all
fields that may be sorted in the admin UI use the <a href="/docs/configuration/overview">indexSortableFields</a>
configuration option.
## File storage
If you are using Payload to [manage file uploads](/docs/upload/overview), you need to consider where your uploaded files will be permanently stored. If you do not use Payload for file uploads, then this section does not impact your app whatsoever.
If you are using Payload to [manage file uploads](/docs/upload/overview), you need to consider where your uploaded files
will be permanently stored. If you do not use Payload for file uploads, then this section does not impact your app
whatsoever.
#### Persistent vs Ephemeral Filesystems
Some cloud app hosts such as [Heroku](https://heroku.com) use `ephemeral` file systems, which means that any files uploaded to your server only last until the server restarts or shuts down. Heroku and similar providers schedule restarts and shutdowns without your control, meaning your uploads will accidentally disappear without any way to get them back.
Some cloud app hosts such as [Heroku](https://heroku.com) use `ephemeral` file systems, which means that any files
uploaded to your server only last until the server restarts or shuts down. Heroku and similar providers schedule
restarts and shutdowns without your control, meaning your uploads will accidentally disappear without any way to get
them back.
Alternatively, persistent filesystems will never delete your files and can be trusted to reliably host uploads perpetually.
Alternatively, persistent filesystems will never delete your files and can be trusted to reliably host uploads
perpetually.
**Popular cloud providers with ephemeral filesystems:**
@@ -135,21 +168,26 @@ Alternatively, persistent filesystems will never delete your files and can be tr
##### Using ephemeral filesystem providers like Heroku
If you don't use Payload's `upload` functionality, you can go ahead and use Heroku or similar platform easily. Everything will work exactly as you want it to.
If you don't use Payload's `upload` functionality, you can go ahead and use Heroku or similar platform easily.
Everything will work exactly as you want it to.
But, if you do, and you still want to use an ephemeral filesystem provider, you can write a hook-based solution to _copy_ the files your users upload to a more permanent storage solution like Amazon S3 or DigitalOcean Spaces.
But, if you do, and you still want to use an ephemeral filesystem provider, you can write a hook-based solution to
_copy_ the files your users upload to a more permanent storage solution like Amazon S3 or DigitalOcean Spaces.
**To automatically send uploaded files to S3 or similar, you could:**
- Write an asynchronous `beforeChange` hook for all Collections that support Uploads, which takes any uploaded `file` from the Express `req` and sends it to an S3 bucket
- Write an `afterRead` hook to save a `s3URL` field that automatically takes the `filename` stored and formats a full S3 URL
- Write an asynchronous `beforeChange` hook for all Collections that support Uploads, which takes any uploaded `file`
from the Express `req` and sends it to an S3 bucket
- Write an `afterRead` hook to save a `s3URL` field that automatically takes the `filename` stored and formats a full S3
URL
- Write an `afterDelete` hook that automatically deletes files from the S3 bucket
With the above configuration, deploying to Heroku or similar becomes no problem.
## DigitalOcean Tutorials
DigitalOcean provides extremely helpful documentation that can walk you through the entire process of creating a production-ready Droplet to host your Payload app:
DigitalOcean provides extremely helpful documentation that can walk you through the entire process of creating a
production-ready Droplet to host your Payload app:
1. Create a new Ubuntu 20.04 droplet on [DigitalOcean](https://digitalocean.com)
1. [Initial server setup](https://www.digitalocean.com/community/tutorials/initial-server-setup-with-ubuntu-20-04)
@@ -160,18 +198,25 @@ DigitalOcean provides extremely helpful documentation that can walk you through
### Swap Space
Swap refers to a section of storage on the hard drive that is reserved to temporarily store data that can no longer fit within RAM. This allows for the expansion of your server's working memory, with some limitations. Swap space comes into play when available RAM can no longer accommodate actively used application data, enabling the system to continue functioning.
Swap refers to a section of storage on the hard drive that is reserved to temporarily store data that can no longer fit
within RAM. This allows for the expansion of your server's working memory, with some limitations. Swap space comes into
play when available RAM can no longer accommodate actively used application data, enabling the system to continue
functioning.
Insufficient space can lead to deployment errors and memory-related issues, resulting in application crashes, sluggish performance, or an unresponsive server.
Insufficient space can lead to deployment errors and memory-related issues, resulting in application crashes, sluggish
performance, or an unresponsive server.
Common deployment error due to **space limitations** (as reported by users):
- `Error: Command failed with exit code 1`
To configure swap, we recommend following this tutorial on [How To Add Swap Space](https://www.digitalocean.com/community/tutorials/how-to-add-swap-space-on-ubuntu-22-04).
To configure swap, we recommend following this tutorial
on [How To Add Swap Space](https://www.digitalocean.com/community/tutorials/how-to-add-swap-space-on-ubuntu-22-04).
## Docker
This is an example of a multi-stage docker build of Payload for production. Ensure you are setting your environment variables on deployment, like `PAYLOAD_SECRET`, `PAYLOAD_CONFIG_PATH`, and `DATABASE_URI` if needed.
This is an example of a multi-stage docker build of Payload for production. Ensure you are setting your environment
variables on deployment, like `PAYLOAD_SECRET`, `PAYLOAD_CONFIG_PATH`, and `DATABASE_URI` if needed.
```dockerfile
FROM node:18-alpine as base

View File

@@ -153,8 +153,8 @@ Here's an overview of all the included features:
| **`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 (ol) |
| **`OrderedListFeature`** | Yes | Adds ordered lists (ul) |
| **`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 |

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/db-postgres",
"version": "0.1.13",
"version": "0.2.1",
"description": "The officially supported Postgres database adapter for Payload",
"repository": "https://github.com/payloadcms/payload",
"license": "MIT",

View File

@@ -64,20 +64,25 @@ const buildQuery = async function buildQuery({
orderBy.order = asc
}
const { columnName: sortTableColumnName, table: sortTable } = getTableColumnFromPath({
adapter,
collectionPath: sortPath,
fields,
joinAliases,
joins,
locale,
pathSegments: sortPath.replace(/__/g, '.').split('.'),
selectFields,
tableName,
})
try {
const { columnName: sortTableColumnName, table: sortTable } = getTableColumnFromPath({
adapter,
collectionPath: sortPath,
fields,
joinAliases,
joins,
locale,
pathSegments: sortPath.replace(/__/g, '.').split('.'),
selectFields,
tableName,
})
orderBy.column = sortTable?.[sortTableColumnName]
} catch (err) {
// continue
}
}
orderBy.column = sortTable[sortTableColumnName]
} else {
if (!orderBy?.column) {
orderBy.order = desc
const createdAt = adapter.tables[tableName]?.createdAt

View File

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

View File

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

View File

@@ -19,37 +19,40 @@ export const handleMessage = async <T>(args: {
}): Promise<T> => {
const { apiRoute, depth, event, initialData, serverURL } = args
if (event.origin === serverURL && event.data) {
const eventData = JSON.parse(event?.data)
if (
event.origin === serverURL &&
event.data &&
typeof event.data === 'object' &&
event.data.type === 'payload-live-preview'
) {
const { data, externallyUpdatedRelationship, fieldSchemaJSON } = event.data
if (eventData.type === 'payload-live-preview') {
if (!payloadLivePreviewFieldSchema && eventData.fieldSchemaJSON) {
payloadLivePreviewFieldSchema = eventData.fieldSchemaJSON
}
if (!payloadLivePreviewFieldSchema) {
// eslint-disable-next-line no-console
console.warn(
'Payload Live Preview: No `fieldSchemaJSON` was received from the parent window. Unable to merge data.',
)
return initialData
}
const mergedData = await mergeData<T>({
apiRoute,
depth,
externallyUpdatedRelationship: eventData.externallyUpdatedRelationship,
fieldSchema: payloadLivePreviewFieldSchema,
incomingData: eventData.data,
initialData: payloadLivePreviewPreviousData || initialData,
serverURL,
})
payloadLivePreviewPreviousData = mergedData
return mergedData
if (!payloadLivePreviewFieldSchema && fieldSchemaJSON) {
payloadLivePreviewFieldSchema = fieldSchemaJSON
}
if (!payloadLivePreviewFieldSchema) {
// eslint-disable-next-line no-console
console.warn(
'Payload Live Preview: No `fieldSchemaJSON` was received from the parent window. Unable to merge data.',
)
return initialData
}
const mergedData = await mergeData<T>({
apiRoute,
depth,
externallyUpdatedRelationship,
fieldSchema: payloadLivePreviewFieldSchema,
incomingData: data,
initialData: payloadLivePreviewPreviousData || initialData,
serverURL,
})
payloadLivePreviewPreviousData = mergedData
return mergedData
}
return initialData

View File

@@ -8,10 +8,10 @@ export const ready = (args: { serverURL: string }): void => {
const windowToPostTo: Window = window?.opener || window?.parent
windowToPostTo?.postMessage(
JSON.stringify({
{
ready: true,
type: 'payload-live-preview',
}),
},
serverURL,
)
}

View File

@@ -53,13 +53,20 @@ export const traverseRichText = ({
? Array.isArray(incomingData[key])
? []
: {}
: incomingData[key]
: undefined
}
const isRelationship = key === 'value' && 'relationTo' in incomingData
if (isRelationship) {
const needsPopulation = !result.value || typeof result.value !== 'object'
// or if there are no keys besides id
const needsPopulation =
!result.value ||
typeof result.value !== 'object' ||
(typeof result.value === 'object' &&
Object.keys(result.value).length === 1 &&
'id' in result.value)
const hasChanged =
result &&
typeof result === 'object' &&
@@ -71,7 +78,10 @@ export const traverseRichText = ({
}
populationsByCollection[incomingData.relationTo].push({
id: incomingData[key],
id:
incomingData[key] && typeof incomingData[key] === 'object'
? incomingData[key].id
: incomingData[key],
accessor: 'value',
ref: result,
})

View File

@@ -1,6 +1,6 @@
{
"name": "payload",
"version": "2.3.0",
"version": "2.4.0",
"description": "Node, React and MongoDB Headless CMS and Application Framework",
"license": "MIT",
"main": "./dist/index.js",
@@ -26,7 +26,7 @@
}
},
"scripts": {
"build": "pnpm copyfiles && pnpm build:swc && pnpm build:types && pnpm build:components && ts-node -T ./scripts/exportPointerFiles.ts",
"build": "pnpm copyfiles && pnpm build:swc && pnpm build:types && pnpm build:components && ts-node -T ../../scripts/exportPointerFiles.ts ../packages/payload dist/exports",
"build:components": "webpack --config dist/admin/components.config.js",
"build:swc": "swc ./src -d ./dist --config-file .swcrc",
"build:types": "tsc --emitDeclarationOnly --outDir dist",

View File

@@ -5,7 +5,7 @@
position: sticky;
top: 0;
width: 100%;
z-index: 1;
z-index: 2;
display: flex;
align-items: center;

View File

@@ -78,16 +78,17 @@ const PreviewSizes: React.FC<{
const [orderedSizes, setOrderedSizes] = useState<FileSizes>(() => sortSizes(sizes, imageSizes))
const [selectedSize, setSelectedSize] = useState<null | string>(null)
const generateImageUrl = (filename) => {
return `${staticURL}/${filename}${imageCacheTag ? `?${imageCacheTag}` : ''}`
const generateImageUrl = (doc) => {
if (!doc.filename) return null
if (doc.url) return `${doc.url}${imageCacheTag ? `?${imageCacheTag}` : ''}`
}
useEffect(() => {
setOrderedSizes(sortSizes(sizes, imageSizes))
}, [sizes, imageSizes, imageCacheTag])
const mainPreviewSrc = selectedSize
? generateImageUrl(`${orderedSizes[selectedSize]?.filename}`)
: generateImageUrl(doc.filename)
? generateImageUrl(doc.sizes[selectedSize])
: generateImageUrl(doc)
const originalImage = useMemo(
(): FileSizes[0] => ({
@@ -121,12 +122,12 @@ const PreviewSizes: React.FC<{
meta={originalImage}
name={originalFilename}
onClick={() => setSelectedSize(null)}
previewSrc={generateImageUrl(doc.filename)}
previewSrc={generateImageUrl(doc)}
/>
{Object.entries(orderedSizes).map(([key, val]) => {
const selected = selectedSize === key
const previewSrc = val.filename ? generateImageUrl(val.filename) : undefined
const previewSrc = generateImageUrl(val)
if (previewSrc) {
return (

View File

@@ -10,13 +10,13 @@ import { isComponent } from './types'
const baseClass = 'field-description'
const FieldDescription: React.FC<Props> = (props) => {
const { className, description, value, marginPlacement } = props
const { className, description, marginPlacement, path, value } = props
const { i18n } = useTranslation()
if (isComponent(description)) {
const Description = description
return <Description value={value} />
return <Description path={path} value={value} />
}
if (description) {
@@ -31,7 +31,7 @@ const FieldDescription: React.FC<Props> = (props) => {
.join(' ')}
>
{typeof description === 'function'
? description({ value })
? description({ path, value })
: getTranslation(description, i18n)}
</div>
)

View File

@@ -1,8 +1,12 @@
import React from 'react'
export type DescriptionFunction = (value?: unknown) => string
type Args<T = unknown> = {
path: string
value?: T
}
export type DescriptionFunction<T = unknown> = (args: Args<T>) => string
export type DescriptionComponent = React.ComponentType<{ value: unknown }>
export type DescriptionComponent<T = unknown> = React.ComponentType<Args<T>>
export type Description =
| DescriptionComponent
@@ -13,8 +17,9 @@ export type Description =
export type Props = {
className?: string
description?: Description
marginPlacement?: 'bottom' | 'top'
path?: string
value?: unknown
marginPlacement?: 'top' | 'bottom'
}
export function isComponent(description: Description): description is DescriptionComponent {

View File

@@ -214,6 +214,7 @@ const ArrayFieldType: React.FC<Props> = (props) => {
<FieldDescription
className={`field-description-${path.replace(/\./g, '__')}`}
description={description}
path={path}
value={value}
/>
</header>

View File

@@ -215,7 +215,7 @@ const BlocksField: React.FC<Props> = (props) => {
</ul>
)}
</div>
<FieldDescription description={description} value={value} />
<FieldDescription description={description} path={path} value={value} />
</header>
<NullifyLocaleField fieldValue={value} localized={localized} path={path} />
{(rows.length > 0 || (!valid && (showRequired || showMinRows))) && (

View File

@@ -96,7 +96,7 @@ const Checkbox: React.FC<Props> = (props) => {
readOnly={readOnly}
required={required}
/>
<FieldDescription description={description} value={value} />
<FieldDescription description={description} path={path} value={value} />
</div>
)
}

View File

@@ -82,7 +82,7 @@ const Code: React.FC<Props> = (props) => {
readOnly={readOnly}
value={(value as string) || ''}
/>
<FieldDescription description={description} value={value} />
<FieldDescription description={description} path={path} value={value} />
</div>
)
}

View File

@@ -14,8 +14,8 @@ import RenderFields from '../../RenderFields'
import { RowLabel } from '../../RowLabel'
import { WatchChildErrors } from '../../WatchChildErrors'
import withCondition from '../../withCondition'
import './index.scss'
import { fieldBaseClass } from '../shared'
import './index.scss'
const baseClass = 'collapsible-field'
@@ -89,7 +89,6 @@ const CollapsibleField: React.FC<Props> = (props) => {
return (
<div
id={`field-${fieldPreferencesKey}${path ? `-${path.replace(/\./g, '__')}` : ''}`}
className={[
fieldBaseClass,
baseClass,
@@ -98,6 +97,7 @@ const CollapsibleField: React.FC<Props> = (props) => {
]
.filter(Boolean)
.join(' ')}
id={`field-${fieldPreferencesKey}${path ? `-${path.replace(/\./g, '__')}` : ''}`}
>
<WatchChildErrors fieldSchema={fields} path={path} setErrorCount={setErrorCount} />
<Collapsible
@@ -125,7 +125,7 @@ const CollapsibleField: React.FC<Props> = (props) => {
readOnly={readOnly}
/>
</Collapsible>
<FieldDescription description={description} />
<FieldDescription description={description} path={path} />
</div>
)
}

View File

@@ -91,7 +91,7 @@ export const DateTimeInput: React.FC<DateTimeInputProps> = (props) => {
/>
{Array.isArray(afterInput) && afterInput.map((Component, i) => <Component key={i} />)}
</div>
<FieldDescription description={description} value={value} />
<FieldDescription description={description} path={path} value={value} />
</div>
)
}

View File

@@ -81,7 +81,7 @@ const Email: React.FC<Props> = (props) => {
/>
{Array.isArray(afterInput) && afterInput.map((Component, i) => <Component key={i} />)}
</div>
<FieldDescription description={description} value={value} />
<FieldDescription description={description} path={path} value={value} />
</div>
)
}

View File

@@ -77,6 +77,7 @@ const Group: React.FC<Props> = (props) => {
<FieldDescription
className={`field-description-${path.replace(/\./g, '__')}`}
description={description}
path={path}
value={null}
/>
</header>

View File

@@ -97,7 +97,7 @@ const JSONField: React.FC<Props> = (props) => {
readOnly={readOnly}
value={stringValue}
/>
<FieldDescription description={description} value={value} />
<FieldDescription description={description} path={path} value={value} />
</div>
)
}

View File

@@ -134,7 +134,7 @@ const PointField: React.FC<Props> = (props) => {
</div>
</li>
</ul>
<FieldDescription description={description} value={value} />
<FieldDescription description={description} path={path} value={value} />
</div>
)
}

View File

@@ -8,13 +8,15 @@ import { optionIsObject } from '../../../../../fields/config/types'
import DefaultError from '../../Error'
import FieldDescription from '../../FieldDescription'
import DefaultLabel from '../../Label'
import { fieldBaseClass } from '../shared'
import RadioInput from './RadioInput'
import './index.scss'
import { fieldBaseClass } from '../shared'
const baseClass = 'radio-group'
export type RadioGroupInputProps = Omit<RadioField, 'type'> & {
Error?: React.ComponentType<any>
Label?: React.ComponentType<any>
className?: string
description?: Description
errorMessage?: string
@@ -28,13 +30,13 @@ export type RadioGroupInputProps = Omit<RadioField, 'type'> & {
style?: React.CSSProperties
value?: string
width?: string
Error?: React.ComponentType<any>
Label?: React.ComponentType<any>
}
const RadioGroupInput: React.FC<RadioGroupInputProps> = (props) => {
const {
name,
Error,
Label,
className,
description,
errorMessage,
@@ -49,8 +51,6 @@ const RadioGroupInput: React.FC<RadioGroupInputProps> = (props) => {
style,
value,
width,
Error,
Label,
} = props
const ErrorComp = Error || DefaultError
@@ -103,7 +103,7 @@ const RadioGroupInput: React.FC<RadioGroupInputProps> = (props) => {
)
})}
</ul>
<FieldDescription description={description} value={value} />
<FieldDescription description={description} path={path} value={value} />
</div>
)
}

View File

@@ -12,13 +12,13 @@ const RadioGroup: React.FC<Props> = (props) => {
name,
admin: {
className,
components: { Error, Label } = {},
condition,
description,
layout = 'horizontal',
readOnly,
style,
width,
components: { Error, Label } = {},
} = {},
label,
options,
@@ -44,6 +44,8 @@ const RadioGroup: React.FC<Props> = (props) => {
return (
<RadioGroupInput
Error={Error}
Label={Label}
className={className}
description={description}
errorMessage={errorMessage}
@@ -58,8 +60,6 @@ const RadioGroup: React.FC<Props> = (props) => {
style={style}
value={value}
width={width}
Error={Error}
Label={Label}
/>
)
}

View File

@@ -527,7 +527,7 @@ const Relationship: React.FC<Props> = (props) => {
</div>
)}
{errorLoading && <div className={`${baseClass}__error-loading`}>{errorLoading}</div>}
<FieldDescription description={description} value={value} />
<FieldDescription description={description} path={path} value={value} />
</div>
)
}

View File

@@ -1,13 +1,34 @@
import React from 'react'
import React, { useMemo } from 'react'
import type { RichTextField } from '../../../../../fields/config/types'
import type { RichTextAdapter } from './types'
const RichText: React.FC<RichTextField> = (fieldprops) => {
// eslint-disable-next-line react/destructuring-assignment
const editor: RichTextAdapter = fieldprops.editor
const { FieldComponent } = editor
return <FieldComponent {...fieldprops} />
const isLazy = 'LazyFieldComponent' in editor
const ImportedFieldComponent: React.FC<any> = useMemo(() => {
return isLazy
? React.lazy(() => {
return editor.LazyFieldComponent().then((resolvedComponent) => ({
default: resolvedComponent,
}))
})
: null
}, [editor, isLazy])
if (isLazy) {
return (
ImportedFieldComponent && (
<React.Suspense>
<ImportedFieldComponent {...fieldprops} />
</React.Suspense>
)
)
}
return <editor.FieldComponent {...fieldprops} />
}
export default RichText

View File

@@ -13,15 +13,11 @@ export type RichTextFieldProps<
path?: string
}
export type RichTextAdapter<
type RichTextAdapterBase<
Value extends object = object,
AdapterProps = any,
ExtraFieldProperties = {},
> = {
CellComponent: React.FC<
CellComponentProps<RichTextField<Value, AdapterProps, ExtraFieldProperties>>
>
FieldComponent: React.FC<RichTextFieldProps<Value, AdapterProps, ExtraFieldProperties>>
afterReadPromise?: ({
field,
incomingEditorState,
@@ -31,7 +27,6 @@ export type RichTextAdapter<
incomingEditorState: Value
siblingDoc: Record<string, unknown>
}) => Promise<void> | null
outputSchema?: ({
field,
isRequired,
@@ -59,3 +54,25 @@ export type RichTextAdapter<
RichTextField<Value, AdapterProps, ExtraFieldProperties>
>
}
export type RichTextAdapter<
Value extends object = object,
AdapterProps = any,
ExtraFieldProperties = {},
> = RichTextAdapterBase<Value, AdapterProps, ExtraFieldProperties> &
(
| {
CellComponent: React.FC<
CellComponentProps<RichTextField<Value, AdapterProps, ExtraFieldProperties>>
>
FieldComponent: React.FC<RichTextFieldProps<Value, AdapterProps, ExtraFieldProperties>>
}
| {
LazyCellComponent: () => Promise<
React.FC<CellComponentProps<RichTextField<Value, AdapterProps, ExtraFieldProperties>>>
>
LazyFieldComponent: () => Promise<
React.FC<RichTextFieldProps<Value, AdapterProps, ExtraFieldProperties>>
>
}
)

View File

@@ -14,6 +14,8 @@ import { fieldBaseClass } from '../shared'
import './index.scss'
export type SelectInputProps = Omit<SelectField, 'options' | 'type' | 'value'> & {
Error?: React.ComponentType<any>
Label?: React.ComponentType<any>
className?: string
description?: Description
errorMessage?: string
@@ -29,12 +31,12 @@ export type SelectInputProps = Omit<SelectField, 'options' | 'type' | 'value'> &
style?: React.CSSProperties
value?: string | string[]
width?: string
Error?: React.ComponentType<any>
Label?: React.ComponentType<any>
}
const SelectInput: React.FC<SelectInputProps> = (props) => {
const {
Error,
Label,
className,
defaultValue,
description,
@@ -52,8 +54,6 @@ const SelectInput: React.FC<SelectInputProps> = (props) => {
style,
value,
width,
Error,
Label,
} = props
const { i18n } = useTranslation()
@@ -111,7 +111,7 @@ const SelectInput: React.FC<SelectInputProps> = (props) => {
showError={showError}
value={valueToRender as Option}
/>
<FieldDescription description={description} value={value} />
<FieldDescription description={description} path={path} value={value} />
</div>
)
}

View File

@@ -177,6 +177,7 @@ const TabsField: React.FC<Props> = (props) => {
className={`${baseClass}__description`}
description={activeTabConfig.description}
marginPlacement="bottom"
path={path}
/>
<RenderFields
fieldSchema={activeTabConfig.fields.map((field) => {

View File

@@ -95,6 +95,7 @@ const TextInput: React.FC<TextInputProps> = (props) => {
<FieldDescription
className={`field-description-${path.replace(/\./g, '__')}`}
description={description}
path={path}
value={value}
/>
</div>

View File

@@ -98,7 +98,7 @@ const TextareaInput: React.FC<TextAreaInputProps> = (props) => {
{Array.isArray(afterInput) && afterInput.map((Component, i) => <Component key={i} />)}
</div>
</label>
<FieldDescription description={description} value={value} />
<FieldDescription description={description} path={path} value={value} />
</div>
)
}

View File

@@ -24,6 +24,8 @@ import './index.scss'
const baseClass = 'upload'
export type UploadInputProps = Omit<UploadField, 'type'> & {
Error?: React.ComponentType<any>
Label?: React.ComponentType<any>
api?: string
className?: string
collection?: SanitizedCollectionConfig
@@ -41,12 +43,12 @@ export type UploadInputProps = Omit<UploadField, 'type'> & {
style?: React.CSSProperties
value?: string
width?: string
Error?: React.ComponentType<any>
Label?: React.ComponentType<any>
}
const UploadInput: React.FC<UploadInputProps> = (props) => {
const {
Error,
Label,
api = '/api',
className,
collection,
@@ -64,8 +66,6 @@ const UploadInput: React.FC<UploadInputProps> = (props) => {
style,
value,
width,
Error,
Label,
} = props
const { i18n, t } = useTranslation('fields')
@@ -88,7 +88,7 @@ const UploadInput: React.FC<UploadInputProps> = (props) => {
})
useEffect(() => {
if (typeof value !== 'undefined' && value !== '') {
if (value !== null && typeof value !== 'undefined' && value !== '') {
const fetchFile = async () => {
const response = await fetch(`${serverURL}${api}/${relationTo}/${value}`, {
credentials: 'include',
@@ -191,7 +191,7 @@ const UploadInput: React.FC<UploadInputProps> = (props) => {
</div>
</div>
)}
<FieldDescription description={description} value={file} />
<FieldDescription description={description} path={path} value={file} />
</React.Fragment>
)}
{!readOnly && <DocumentDrawer onSave={onSave} />}

View File

@@ -125,10 +125,13 @@ export const LivePreviewProvider: React.FC<LivePreviewProviderProps> = (props) =
// Unlike iframe elements which have an `onLoad` handler, there is no way to access `window.open` on popups
useEffect(() => {
const handleMessage = (event: MessageEvent) => {
if (url.startsWith(event.origin)) {
const data = JSON.parse(event.data)
if (data.type === 'payload-live-preview' && data.ready) {
if (
url?.startsWith(event.origin) &&
event.data &&
typeof event.data === 'object' &&
event.data.type === 'payload-live-preview'
) {
if (event.data.ready) {
setAppIsReady(true)
}
}

View File

@@ -2,6 +2,7 @@ import React, { useEffect } from 'react'
import type { EditViewProps } from '../../types'
import { ShimmerEffect } from '../../../elements/ShimmerEffect'
import { useAllFormFields } from '../../../forms/Form/context'
import reduceFieldsToValues from '../../../forms/Form/reduceFieldsToValues'
import { useDocumentEvents } from '../../../utilities/DocumentEvents'
@@ -50,12 +51,12 @@ export const LivePreview: React.FC<EditViewProps> = (props) => {
prevWindowType.current = previewWindowType
const message = JSON.stringify({
const message = {
data: values,
externallyUpdatedRelationship: mostRecentUpdate,
fieldSchemaJSON: shouldSendSchema ? fieldSchemaJSON : undefined,
type: 'payload-live-preview',
})
}
// Post message to external popup window
if (previewWindowType === 'popup' && popupRef.current) {
@@ -94,7 +95,11 @@ export const LivePreview: React.FC<EditViewProps> = (props) => {
<LivePreviewToolbar {...props} />
<div className={`${baseClass}__main`}>
<DeviceContainer>
<IFrame ref={iframeRef} setIframeHasLoaded={setIframeHasLoaded} url={url} />
{url ? (
<IFrame ref={iframeRef} setIframeHasLoaded={setIframeHasLoaded} url={url} />
) : (
<ShimmerEffect height="100%" />
)}
</DeviceContainer>
</div>
</div>

View File

@@ -1,4 +1,4 @@
import React, { Fragment } from 'react'
import React, { Fragment, useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import type { SanitizedCollectionConfig } from '../../../../collections/config/types'
@@ -137,6 +137,7 @@ export const LivePreviewView: React.FC<
fieldTypes: FieldTypes
}
> = (props) => {
const { data } = props
const config = useConfig()
const documentInfo = useDocumentInfo()
const locale = useLocale()
@@ -157,14 +158,26 @@ export const LivePreviewView: React.FC<
}
}
const url =
typeof livePreviewConfig?.url === 'function'
? livePreviewConfig?.url({
data: props?.data,
documentInfo,
locale,
})
: livePreviewConfig?.url
const [url, setURL] = React.useState<string | undefined>(() => {
if (typeof livePreviewConfig?.url === 'string') return livePreviewConfig?.url
})
useEffect(() => {
const getURL = async () => {
const newURL =
typeof livePreviewConfig?.url === 'function'
? await livePreviewConfig.url({
data,
documentInfo,
locale,
})
: livePreviewConfig?.url
setURL(newURL)
}
getURL() // eslint-disable-line @typescript-eslint/no-floating-promises
}, [data, documentInfo, locale, livePreviewConfig])
const breakpoints: LivePreviewConfig['breakpoints'] = [
...(livePreviewConfig?.breakpoints || []),

View File

@@ -1,4 +1,4 @@
import React from 'react'
import React, { useMemo } from 'react'
import type { RichTextField } from '../../../../../../../../fields/config/types'
import type { RichTextAdapter } from '../../../../../../forms/field-types/RichText/types'
@@ -7,9 +7,30 @@ import type { CellComponentProps } from '../../types'
const RichTextCell: React.FC<CellComponentProps<RichTextField>> = (props) => {
// eslint-disable-next-line react/destructuring-assignment
const editor: RichTextAdapter = props.field.editor
const { CellComponent } = editor
return <CellComponent {...props} />
const isLazy = 'LazyCellComponent' in editor
const ImportedCellComponent: React.FC<any> = useMemo(() => {
return isLazy
? React.lazy(() => {
return editor.LazyCellComponent().then((resolvedComponent) => ({
default: resolvedComponent,
}))
})
: null
}, [editor, isLazy])
if (isLazy) {
return (
ImportedCellComponent && (
<React.Suspense>
<ImportedCellComponent {...props} />
</React.Suspense>
)
)
}
return <editor.CellComponent {...props} />
}
export default RichTextCell

View File

@@ -170,14 +170,6 @@ async function create<TSlug extends keyof GeneratedTypes['collections']>(
Promise.resolve(),
)
// /////////////////////////////////////
// Write files to local storage
// /////////////////////////////////////
if (!collectionConfig.upload.disableLocalStorage) {
await uploadFiles(payload, filesToUpload, req.t)
}
// /////////////////////////////////////
// beforeChange - Collection
// /////////////////////////////////////
@@ -211,6 +203,14 @@ async function create<TSlug extends keyof GeneratedTypes['collections']>(
skipValidation: shouldSaveDraft,
})
// /////////////////////////////////////
// Write files to local storage
// /////////////////////////////////////
if (!collectionConfig.upload.disableLocalStorage) {
await uploadFiles(payload, filesToUpload, req.t)
}
// /////////////////////////////////////
// Create
// /////////////////////////////////////

View File

@@ -1,7 +1,7 @@
import joi from 'joi'
import { adminViewSchema } from './shared/adminViewSchema'
import { livePreviewSchema } from './shared/componentSchema'
import { componentSchema, livePreviewSchema } from './shared/componentSchema'
const component = joi.alternatives().try(joi.object().unknown(), joi.func())
@@ -94,8 +94,10 @@ export default joi.object({
.object()
.required()
.keys({
CellComponent: component.required(),
FieldComponent: component.required(),
CellComponent: componentSchema.optional(),
FieldComponent: componentSchema.optional(),
LazyCellComponent: joi.func().optional(),
LazyFieldComponent: joi.func().optional(),
afterReadPromise: joi.func().optional(),
outputSchema: joi.func().optional(),
populationPromise: joi.func().optional(),

View File

@@ -63,7 +63,11 @@ export type LivePreviewConfig = {
Use the `useLivePreview` hook to get started in React applications.
*/
url?:
| ((args: { data: Record<string, any>; documentInfo: ContextType; locale: Locale }) => string)
| ((args: {
data: Record<string, any>
documentInfo: ContextType
locale: Locale
}) => Promise<string> | string)
| string
}

View File

@@ -101,7 +101,6 @@ export async function validateSearchParam({
errors.push({ path: incomingPath })
}
}
let fieldAccess
let fieldPath = path
// remove locale from end of path
if (path.endsWith(`.${req.locale}`)) {
@@ -115,47 +114,29 @@ export async function validateSearchParam({
const entitySlug = collectionSlug || globalConfig.slug
const segments = fieldPath.split('.')
let fieldAccess
if (versionFields) {
fieldAccess = policies[entityType][entitySlug]
if (segments[0] === 'parent' || segments[0] === 'version') {
segments.shift()
} else {
segments.forEach((segment, pathIndex) => {
if (fieldAccess[segment]) {
if (pathIndex === segments.length - 1) {
fieldAccess = fieldAccess[segment]
} else if ('fields' in fieldAccess[segment]) {
fieldAccess = fieldAccess[segment].fields
} else if ('blocks' in fieldAccess[segment]) {
fieldAccess = fieldAccess[segment]
}
}
})
}
fieldAccess = fieldAccess.read.permission
} else {
fieldAccess = policies[entityType][entitySlug].fields
if (['json', 'richText'].includes(field.type)) {
fieldAccess = fieldAccess[field.name]
} else {
segments.forEach((segment, pathIndex) => {
if (fieldAccess[segment]) {
if (pathIndex === segments.length - 1) {
fieldAccess = fieldAccess[segment]
} else if ('fields' in fieldAccess[segment]) {
fieldAccess = fieldAccess[segment].fields
} else if ('blocks' in fieldAccess[segment]) {
fieldAccess = fieldAccess[segment]
}
}
})
}
fieldAccess = fieldAccess.read.permission
}
if (!fieldAccess) {
segments.forEach((segment) => {
if (fieldAccess[segment]) {
if ('fields' in fieldAccess[segment]) {
fieldAccess = fieldAccess[segment].fields
} else if ('blocks' in fieldAccess[segment]) {
fieldAccess = fieldAccess[segment]
} else {
fieldAccess = fieldAccess[segment]
}
}
})
if (!fieldAccess?.read?.permission) {
errors.push({ path: fieldPath })
}
}

View File

@@ -0,0 +1,8 @@
Important:
When you export anything with a scss or svg, or any component with a hook, it should be exported from a file within payload/components

View File

@@ -22,3 +22,9 @@ export {
formatListDrawerSlug,
useListDrawer,
} from '../../admin/components/elements/ListDrawer'
export {
Description,
DescriptionComponent,
DescriptionFunction,
} from '../../admin/components/forms/FieldDescription/types'

View File

@@ -436,8 +436,10 @@ export const richText = baseField.keys({
editor: joi
.object()
.keys({
CellComponent: componentSchema.required(),
FieldComponent: componentSchema.required(),
CellComponent: componentSchema.optional(),
FieldComponent: componentSchema.optional(),
LazyCellComponent: joi.func().optional(),
LazyFieldComponent: joi.func().optional(),
afterReadPromise: joi.func().optional(),
outputSchema: joi.func().optional(),
populationPromise: joi.func().optional(),

View File

@@ -6,6 +6,7 @@ import type { SanitizedGlobalConfig } from '../../../globals/config/types'
import type { Field, TabAsField } from '../../config/types'
import { fieldAffectsData, tabHasName } from '../../config/types'
import getValueWithDefault from '../../getDefaultValue'
import relationshipPopulationPromise from './relationshipPopulationPromise'
import { traverseFields } from './traverseFields'
@@ -265,6 +266,20 @@ export const promise = async ({
}
}
// Set defaultValue on the field for globals being returned without being first created
// or collection documents created prior to having a default
if (
typeof siblingDoc[field.name] === 'undefined' &&
typeof field.defaultValue !== 'undefined'
) {
siblingDoc[field.name] = await getValueWithDefault({
defaultValue: field.defaultValue,
locale: req.locale,
user: req.user,
value: siblingDoc[field.name],
})
}
if (field.type === 'relationship' || field.type === 'upload') {
populationPromises.push(
relationshipPopulationPromise({

View File

@@ -27,6 +27,7 @@ import tr from './tr.json'
import ua from './ua.json'
import vi from './vi.json'
import zh from './zh.json'
import zhTw from './zh-tw.json'
export default {
ar,
@@ -58,4 +59,5 @@ export default {
ua,
vi,
zh,
zhTw,
}

View File

@@ -0,0 +1,374 @@
{
"$schema": "./translation-schema.json",
"authentication": {
"account": "帳戶",
"accountOfCurrentUser": "目前使用者的帳戶",
"alreadyActivated": "已經啟用了",
"alreadyLoggedIn": "已經登入了",
"apiKey": "API金鑰",
"backToLogin": "返回登入頁面",
"beginCreateFirstUser": "首先,請建立您的第一個使用者。",
"changePassword": "更改密碼",
"checkYourEmailForPasswordReset": "請檢查您的電子郵件以獲取安全重設密碼的連結。",
"confirmGeneration": "確認生成",
"confirmPassword": "確認密碼",
"createFirstUser": "建立第一個使用者",
"emailNotValid": "提供的電子郵件無效",
"emailSent": "電子郵件已寄出",
"enableAPIKey": "啟用API金鑰",
"failedToUnlock": "解鎖失敗",
"forceUnlock": "強制解鎖",
"forgotPassword": "忘記密碼",
"forgotPasswordEmailInstructions": "請在下方輸入您的電子郵件。您將收到一封有關如何重設密碼的說明電子郵件。",
"forgotPasswordQuestion": "忘記密碼?",
"generate": "生成",
"generateNewAPIKey": "生成新的API金鑰",
"generatingNewAPIKeyWillInvalidate": "生成新的API金鑰將使之前的金鑰<1>失效</1>。您確定要繼續嗎?",
"lockUntil": "鎖定直到",
"logBackIn": "重新登入",
"logOut": "登出",
"loggedIn": "要使用另一個使用者登入前,您需要先<0>登出</0>。",
"loggedInChangePassword": "要更改您的密碼,請前往您的<0>帳戶</0>頁面並在那裡編輯您的密碼。",
"loggedOutInactivity": "您由於不活躍而被登出了。",
"loggedOutSuccessfully": "您已成功登出。",
"login": "登入",
"loginAttempts": "登入次數",
"loginUser": "登入使用者",
"loginWithAnotherUser": "要使用另一個使用者登入前,您需要先<0>登出</0>。",
"logout": "登出",
"logoutUser": "登出使用者",
"newAPIKeyGenerated": "新的API金鑰已生成。",
"newAccountCreated": "剛剛為您建立了一個可以存取 <a href=\"{{serverURL}}\">{{serverURL}}</a> 的新帳戶。請點擊以下連結或在瀏覽器中貼上以下網址以驗證您的電子郵件:<a href=\"{{verificationURL}}\">{{verificationURL}}</a><br> 驗證您的電子郵件後,您將能夠成功登入。",
"newPassword": "新的密碼",
"resetPassword": "重設密碼",
"resetPasswordExpiration": "重設密碼的有效期",
"resetPasswordToken": "重設密碼令牌",
"resetYourPassword": "重設您的密碼",
"stayLoggedIn": "保持登入狀態",
"successfullyUnlocked": "已成功解鎖",
"unableToVerify": "無法驗證",
"verified": "已驗證",
"verifiedSuccessfully": "成功驗證",
"verify": "驗證",
"verifyUser": "驗證使用者",
"verifyYourEmail": "驗證您的電子郵件",
"youAreInactive": "您已經有一段時間沒有活動了,為了您的安全,很快就會自動登出。您想保持登入狀態嗎?",
"youAreReceivingResetPassword": "您收到此郵件是因為您(或其他人)已請求重設您帳戶的密碼。請點擊以下連結,或將其貼上到您的瀏覽器中以完成該過程:",
"youDidNotRequestPassword": "如果您沒有要求這樣做,請忽略這封郵件,您的密碼將保持不變。"
},
"error": {
"accountAlreadyActivated": "該帳戶已被啟用。",
"autosaving": "自動儲存該文件時出現了問題。",
"correctInvalidFields": "請更正無效區塊。",
"deletingFile": "刪除文件時出現了錯誤。",
"deletingTitle": "刪除{{title}}時出現了錯誤。請檢查您的網路連線並重試。",
"emailOrPasswordIncorrect": "提供的電子郵件或密碼不正確。",
"followingFieldsInvalid_one": "下面的字串是無效的:",
"followingFieldsInvalid_other": "以下字串是無效的:",
"incorrectCollection": "不正確的集合",
"invalidFileType": "無效的文件類型",
"invalidFileTypeValue": "無效的文件類型: {{value}}",
"loadingDocument": "加載ID為{{id}}的文件時出現了問題。",
"localesNotSaved_one": "這個語言環境無法被儲存:",
"localesNotSaved_other": "以下的語言環境無法被儲存:",
"missingEmail": "缺少電子郵件。",
"missingIDOfDocument": "缺少需要更新的文檔的ID。",
"missingIDOfVersion": "缺少版本的ID。",
"missingRequiredData": "缺少必要的數據。",
"noFilesUploaded": "沒有上傳文件。",
"noMatchedField": "找不到與\"{{label}}\"匹配的字串",
"noUser": "沒有該使用者",
"notAllowedToAccessPage": "您沒有權限訪問此頁面。",
"notAllowedToPerformAction": "您不被允許執行此操作。",
"notFound": "沒有找到請求的資源。",
"previewing": "預覽文件時出現了問題。",
"problemUploadingFile": "上傳文件時出現了問題。",
"tokenInvalidOrExpired": "令牌無效或已過期。",
"unPublishingDocument": "取消發布此文件時出現了問題。",
"unableToDeleteCount": "無法從 {{total}} 個中刪除 {{count}} 個 {{label}}。",
"unableToUpdateCount": "無法從 {{total}} 個中更新 {{count}} 個 {{label}}。",
"unauthorized": "未經授權,您必須登錄才能提出這個請求。",
"unknown": "發生了一個未知的錯誤。",
"unspecific": "發生了一個錯誤。",
"userLocked": "該使用者由於有太多次失敗的登錄嘗試而被鎖定。",
"valueMustBeUnique": "數值必須是唯一的",
"verificationTokenInvalid": "驗證令牌無效。"
},
"fields": {
"addLabel": "新增{{label}}",
"addLink": "新增連結",
"addNew": "新增",
"addNewLabel": "新增{{label}}",
"addRelationship": "新增關聯",
"addUpload": "上傳",
"block": "區塊",
"blockType": "區塊類型",
"blocks": "區塊",
"chooseBetweenCustomTextOrDocument": "選擇自定義文件或連結到另一個文件。",
"chooseDocumentToLink": "選擇要連結的文件",
"chooseFromExisting": "從現有的選擇",
"chooseLabel": "選擇{{label}}",
"collapseAll": "全部折疊",
"customURL": "自定義連結",
"editLabelData": "編輯{{label}}資料",
"editLink": "編輯連結",
"editRelationship": "編輯關聯",
"enterURL": "輸入連結",
"internalLink": "內部連結",
"itemsAndMore": "{{items}} 個,還有 {{count}} 個",
"labelRelationship": "{{label}}關聯",
"latitude": "緯度",
"linkType": "連結類型",
"linkedTo": "連結到<0>{{label}}</0>",
"longitude": "經度",
"newLabel": "新的{{label}}",
"openInNewTab": "在新標籤中打開",
"passwordsDoNotMatch": "密碼不匹配。",
"relatedDocument": "相關文件",
"relationTo": "關聯到",
"removeRelationship": "移除關聯",
"removeUpload": "移除上傳",
"saveChanges": "儲存變更",
"searchForBlock": "搜尋一個區塊",
"selectExistingLabel": "選擇現有的{{label}}",
"selectFieldsToEdit": "選擇要編輯的字串",
"showAll": "顯示全部",
"swapRelationship": "替換關聯",
"swapUpload": "替換上傳",
"textToDisplay": "要顯示的文字",
"toggleBlock": "切換區塊",
"uploadNewLabel": "上傳新的{{label}}"
},
"general": {
"aboutToDelete": "您即將刪除{{label}} <1>{{title}}</1>。您確定要繼續嗎?",
"aboutToDeleteCount_many": "您即將刪除 {{count}} 個 {{label}}",
"aboutToDeleteCount_one": "您即將刪除 {{count}} 個 {{label}}",
"aboutToDeleteCount_other": "您即將刪除 {{count}} 個 {{label}}",
"addBelow": "新增到下方",
"addFilter": "新增過濾器",
"adminTheme": "管理頁面主題",
"and": "和",
"applyChanges": "套用更改",
"ascending": "升冪",
"automatic": "自動",
"backToDashboard": "返回到控制面板",
"cancel": "取消",
"changesNotSaved": "您還有尚未儲存的變更。您確定要離開嗎?",
"close": "關閉",
"collapse": "折疊",
"collections": "集合",
"columnToSort": "要排序的欄位",
"columns": "欄位",
"confirm": "確認",
"confirmDeletion": "確認刪除",
"confirmDuplication": "確認複製",
"copied": "已複製",
"copy": "複製",
"create": "建立",
"createNew": "建立新的",
"createNewLabel": "建立新的{{label}}",
"created": "已建立",
"createdAt": "建立於",
"creating": "建立中",
"creatingNewLabel": "正在建立新的{{label}}",
"dark": "深色",
"dashboard": "控制面板",
"delete": "刪除",
"deletedCountSuccessfully": "已成功刪除 {{count}} 個 {{label}}。",
"deletedSuccessfully": "已成功刪除。",
"deleting": "刪除中...",
"descending": "降冪",
"deselectAllRows": "取消選擇全部",
"duplicate": "複製",
"duplicateWithoutSaving": "複製而不儲存變更。",
"edit": "編輯",
"editLabel": "編輯{{label}}",
"editing": "編輯中",
"editingLabel_many": "編輯 {{count}} 個 {{label}}",
"editingLabel_one": "編輯 {{count}} 個 {{label}}",
"editingLabel_other": "編輯 {{count}} 個 {{label}}",
"email": "電子郵件",
"emailAddress": "電子郵件地址",
"enterAValue": "輸入一個值",
"error": "錯誤",
"errors": "錯誤",
"fallbackToDefaultLocale": "回到預設的語言",
"filter": "過濾器",
"filterWhere": "過濾{{label}}",
"filters": "過濾器",
"globals": "全域",
"language": "語言",
"lastModified": "最後修改",
"leaveAnyway": "無論如何都要離開",
"leaveWithoutSaving": "不儲存直接離開",
"light": "亮色",
"livePreview": "預覽",
"loading": "載入中...",
"locale": "語言環境",
"locales": "語言環境",
"menu": "菜單",
"moveDown": "向下移動",
"moveUp": "向上移動",
"newPassword": "新密碼",
"noFiltersSet": "沒有設定過濾器",
"noLabel": "<沒有{{label}}>",
"noOptions": "沒有選項",
"noResults": "沒有找到{{label}}。{{label}}並不存在或沒有符合您上面所指定的過濾器。",
"noValue": "沒有數值",
"none": "無",
"notFound": "未找到",
"nothingFound": "沒有找到任何東西",
"of": "的",
"open": "打開",
"or": "或",
"order": "排序",
"pageNotFound": "未找到頁面",
"password": "密碼",
"payloadSettings": "Payload設定",
"perPage": "每一頁: {{limit}} 個",
"remove": "移除",
"reset": "重設",
"row": "行",
"rows": "行",
"save": "儲存",
"saving": "儲存中...",
"searchBy": "搜尋{{label}}",
"selectAll": "選擇所有 {{count}} 個 {{label}}",
"selectAllRows": "選擇所有行",
"selectValue": "選擇一個值",
"selectedCount": "已選擇 {{count}} 個 {{label}}",
"showAllLabel": "顯示所有{{label}}",
"sorryNotFound": "對不起,沒有找到您請求的東西。",
"sort": "排序",
"sortByLabelDirection": "按{{label}} {{direction}}排序",
"stayOnThisPage": "停留在此頁面",
"submissionSuccessful": "成功送出。",
"submit": "送出",
"successfullyCreated": "成功建立{{label}}",
"successfullyDuplicated": "成功複製{{label}}",
"thisLanguage": "中文 (繁體)",
"titleDeleted": "{{label}} \"{{title}}\"已被成功刪除。",
"unauthorized": "未經授權",
"unsavedChangesDuplicate": "您有還沒儲存的修改,確定要繼續複製嗎?",
"untitled": "無標題",
"updatedAt": "更新於",
"updatedCountSuccessfully": "已成功更新 {{count}} 個 {{label}}。",
"updatedSuccessfully": "更新成功。",
"updating": "更新中",
"uploading": "上傳中",
"user": "使用者",
"users": "使用者",
"value": "值",
"welcome": "歡迎"
},
"operators": {
"contains": "包含",
"equals": "等於",
"exists": "存在",
"isGreaterThan": "大於",
"isGreaterThanOrEqualTo": "大於等於",
"isIn": "在",
"isLessThan": "小於",
"isLessThanOrEqualTo": "小於或等於",
"isLike": "就像",
"isNotEqualTo": "不等於",
"isNotIn": "不在",
"near": "附近"
},
"upload": {
"crop": "裁剪",
"cropToolDescription": "拖動所選區域的角落,繪製一個新區域或調整以下的值。",
"dragAndDrop": "拖放一個檔案",
"dragAndDropHere": "或在這裡拖放一個檔案",
"editImage": "編輯圖像",
"fileName": "檔案名稱",
"fileSize": "檔案大小",
"focalPoint": "焦點",
"focalPointDescription": "直接在預覽中拖動焦點或調整下面的值。",
"height": "高度",
"lessInfo": "更少資訊",
"moreInfo": "更多資訊",
"previewSizes": "預覽尺寸",
"selectCollectionToBrowse": "選擇一個要瀏覽的集合",
"selectFile": "選擇一個文件",
"setCropArea": "設置裁剪區域",
"setFocalPoint": "設置焦點",
"sizes": "尺寸",
"sizesFor": "{{label}}的尺寸",
"width": "寬度"
},
"validation": {
"emailAddress": "請輸入一個有效的電子郵件地址。",
"enterNumber": "請輸入一個有效的數字。",
"fieldHasNo": "這個字串沒有{{label}}",
"greaterThanMax": "{{value}}超過了允許的最大{{label}},該最大值為{{max}}。",
"invalidInput": "這個字串有一個無效的輸入。",
"invalidSelection": "這個字串有一個無效的選擇。",
"invalidSelections": "這個字串有以下無效的選擇:",
"lessThanMin": "{{value}}小於允許的最小{{label}},該最小值為{{min}}。",
"limitReached": "已達限制,只能添加{{max}}個項目。",
"longerThanMin": "該值必須大於{{minLength}}字串的最小長度",
"notValidDate": "\"{{value}}\"不是一個有效的日期。",
"required": "該字串為必填項目。",
"requiresAtLeast": "該字串至少需要 {{count}} 個 {{label}}。",
"requiresNoMoreThan": "該字串要求不超過 {{count}} 個 {{label}。",
"requiresTwoNumbers": "該字串需要兩個數字。",
"shorterThanMax": "該值長度必須小於{{maxLength}}個字元",
"trueOrFalse": "該字串只能等於是或否。",
"validUploadID": "該字串不是有效的上傳ID。"
},
"version": {
"aboutToPublishSelection": "您確定即將發佈所選的 {{label}} 嗎?",
"aboutToRestore": "您將把這個文件{{label}}回復到{{versionDate}}時的狀態",
"aboutToRestoreGlobal": "您要將痊域的{{label}}回復到{{versionDate}}時的狀態",
"aboutToRevertToPublished": "您將要將這個文件的內容還原到它的發佈狀態。您確定嗎?",
"aboutToUnpublish": "您即將取消發佈這個文件。您確定嗎?",
"aboutToUnpublishSelection": "您即將取消發佈所選內容中的所有 {{label}}。您確定嗎?",
"autosave": "自動儲存",
"autosavedSuccessfully": "自動儲存成功。",
"autosavedVersion": "自動儲存的版本",
"changed": "已更改",
"compareVersion": "對比版本:",
"confirmPublish": "確認發佈",
"confirmRevertToSaved": "確認回復到儲存狀態",
"confirmUnpublish": "確認取消發佈",
"confirmVersionRestoration": "確認版本回復",
"currentDocumentStatus": "目前{{docStatus}}文件",
"draft": "草稿",
"draftSavedSuccessfully": "草稿儲存成功。",
"lastSavedAgo": "上次儲存在{{distance}}之前",
"noFurtherVersionsFound": "沒有發現其他版本",
"noRowsFound": "沒有發現{{label}}",
"preview": "預覽",
"problemRestoringVersion": "回復這個版本時發生了問題",
"publish": "發佈",
"publishChanges": "發佈修改",
"published": "已發佈",
"restoreThisVersion": "回復此版本",
"restoredSuccessfully": "回復成功。",
"restoring": "回復中...",
"revertToPublished": "還原到已發佈的版本",
"reverting": "還原中...",
"saveDraft": "儲存草稿",
"selectLocales": "選擇要顯示的語言",
"selectVersionToCompare": "選擇要比較的版本",
"showLocales": "顯示語言:",
"showingVersionsFor": "顯示版本為:",
"status": "狀態",
"type": "類型",
"unpublish": "取消發佈",
"unpublishing": "取消發佈中...",
"version": "版本",
"versionCount_many": "發現 {{count}}個版本",
"versionCount_none": "沒有發現任何版本",
"versionCount_one": "找到 {{count}} 個版本",
"versionCount_other": "找到 {{count}} 個版本",
"versionCreatedOn": "版本 {{version}} 建立於:",
"versionID": "版本ID",
"versions": "版本",
"viewingVersion": "正在查看{{entityLabel}} {{documentTitle}}的版本",
"viewingVersionGlobal": "正在查看全域{{entityLabel}}的版本",
"viewingVersions": "正在查看{{entityLabel}} {{documentTitle}}的版本",
"viewingVersionsGlobal": "正在查看全域{{entityLabel}}的版本"
}
}

View File

@@ -244,7 +244,7 @@
"submit": "提交",
"successfullyCreated": "成功创建{{label}}",
"successfullyDuplicated": "成功复制{{label}}",
"thisLanguage": "Chinese",
"thisLanguage": "中文 (简体)",
"titleDeleted": "{{label}} \"{{title}}\"已被成功删除。",
"unauthorized": "未经授权",
"unsavedChangesDuplicate": "您有未保存的修改。您确定要继续重复吗?",
@@ -369,4 +369,4 @@
"viewingVersions": "正在查看{{entityLabel}} {{documentTitle}}的版本",
"viewingVersionsGlobal": "正在查看全局{{entityLabel}}的版本"
}
}
}

View File

@@ -17,6 +17,7 @@ import { FileUploadError, MissingFile } from '../errors'
import canResizeImage from './canResizeImage'
import cropImage from './cropImage'
import getFileByPath from './getFileByPath'
import getFileByURL from './getFileByURL'
import getImageSize from './getImageSize'
import getSafeFileName from './getSafeFilename'
import resizeAndTransformImageSizes from './imageResizer'
@@ -63,12 +64,22 @@ export const generateFileData = async <T>({
}
if (!file && uploadEdits && data) {
const { filename } = data as FileData
const filePath = `${staticPath}/${filename}`
const response = await getFileByPath(filePath)
const { filename, url } = data as FileData
overwriteExistingFiles = true
file = response as UploadedFile
try {
if (url && url.startsWith('/')) {
const filePath = `${staticPath}/${filename}`
const response = await getFileByPath(filePath)
file = response as UploadedFile
overwriteExistingFiles = true
} else {
const response = await getFileByURL(url)
file = response as UploadedFile
overwriteExistingFiles = true
}
} catch (err) {
throw new FileUploadError(req.t)
}
}
if (!file) {
@@ -190,7 +201,12 @@ export const generateFileData = async <T>({
fileData.width = info.width
fileData.height = info.height
fileData.filesize = info.size
req.files.file = fileForResize
if (file.tempFilePath) {
await fs.promises.writeFile(file.tempFilePath, croppedImage) // write fileBuffer to the temp path
} else {
req.files.file = fileForResize
}
} else {
filesToSave.push({
buffer: fileBuffer?.data || file.data,

View File

@@ -142,7 +142,7 @@ const getBaseUploadFields = ({ collection, config }: Options): Field[] => {
return `${uploadOptions.staticURL}/${sizeFilename}`
}
return undefined
return null
},
],
},

View File

@@ -0,0 +1,26 @@
import fetch from 'node-fetch'
import path from 'path'
import type { File } from './types'
const getFileByURL = async (url: string): Promise<File> => {
if (typeof url === 'string') {
const res = await fetch(url, {
headers: {
'Content-Type': 'application/json',
},
method: 'GET',
})
const data = await res.buffer()
const name = path.basename(url)
return {
name,
data,
mimetype: res.headers.get('content-type') || undefined,
size: Number(res.headers.get('content-length')) || 0,
}
}
}
export default getFileByURL

View File

@@ -22,6 +22,7 @@ export type FileData = {
mimeType: string
sizes: FileSizes
tempFilePath?: string
url?: string
width: number
}

View File

@@ -6,7 +6,7 @@ A plugin for [Payload CMS](https://github.com/payloadcms/payload) to easily allo
Core features:
- Allows for [parent/child](#parent) relationships between documents
- Allows for [parent/child](#parent) relationships between documents within the same Collection
- Automatically populates [breadcrumbs](#breadcrumbs) data
## Installation

View File

@@ -7,7 +7,7 @@
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"build": "pnpm build:swc && pnpm build:types",
"_build": "pnpm build:swc && pnpm build:types",
"build:swc": "swc ./src -d ./dist --config-file .swcrc",
"build:types": "tsc --emitDeclarationOnly --outDir dist",
"clean": "rimraf {dist,*.tsbuildinfo}",

4
packages/richtext-lexical/.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
/utilities.d.ts
/utilities.js
/components.d.ts
/components.js

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/richtext-lexical",
"version": "0.3.0",
"version": "0.4.0",
"description": "The officially supported Lexical richtext adapter for Payload",
"repository": "https://github.com/payloadcms/payload",
"license": "MIT",
@@ -9,7 +9,7 @@
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"scripts": {
"build": "pnpm copyfiles && pnpm build:swc && pnpm build:types",
"build": "pnpm copyfiles && pnpm build:swc && pnpm build:types && ts-node -T ../../scripts/exportPointerFiles.ts ../packages/richtext-lexical dist/exports",
"build:swc": "swc ./src -d ./dist --config-file .swcrc",
"build:types": "tsc --emitDeclarationOnly --outDir dist",
"build:clean": "find . \\( -type d \\( -name build -o -name dist -o -name .cache \\) -o -type f -name tsconfig.tsbuildinfo \\) -exec rm -rf {} + && pnpm build",
@@ -47,12 +47,18 @@
"payload": "workspace:*"
},
"peerDependencies": {
"payload": "^2.3.0"
"payload": "^2.4.0"
},
"exports": {
".": {
"default": "./src/index.ts",
"import": "./src/index.ts",
"require": "./src/index.ts",
"types": "./src/index.ts"
},
"./*": {
"import": "./src/exports/*.ts",
"require": "./src/exports/*.ts",
"types": "./src/exports/*.ts"
}
},
"publishConfig": {
@@ -62,6 +68,8 @@
"types": "./dist/index.d.ts"
},
"files": [
"dist"
"dist",
"components.js",
"components.d.ts"
]
}

View File

@@ -1,5 +1,6 @@
'use client'
import type { SerializedEditorState } from 'lexical'
import type { EditorConfig as LexicalEditorConfig } from 'lexical/LexicalEditor'
import type { CellComponentProps, RichTextField } from 'payload/types'
import { createHeadlessEditor } from '@lexical/headless'
@@ -50,21 +51,23 @@ export const RichTextCell: React.FC<
return
}
// initialize headless editor
const headlessEditor = createHeadlessEditor({
namespace: editorConfig.lexical.namespace,
nodes: getEnabledNodes({ editorConfig }),
theme: editorConfig.lexical.theme,
editorConfig.lexical().then((lexicalConfig: LexicalEditorConfig) => {
// initialize headless editor
const headlessEditor = createHeadlessEditor({
namespace: lexicalConfig.namespace,
nodes: getEnabledNodes({ editorConfig }),
theme: lexicalConfig.theme,
})
headlessEditor.setEditorState(headlessEditor.parseEditorState(dataToUse))
const textContent =
headlessEditor.getEditorState().read(() => {
return $getRoot().getTextContent()
}) || ''
// Limiting the number of characters shown is done in a CSS rule
setPreview(textContent)
})
headlessEditor.setEditorState(headlessEditor.parseEditorState(dataToUse))
const textContent =
headlessEditor.getEditorState().read(() => {
return $getRoot().getTextContent()
}) || ''
// Limiting the number of characters shown is done in a CSS rule
setPreview(textContent)
}, [data, editorConfig])
return <span>{preview}</span>

View File

@@ -0,0 +1,6 @@
export { RichTextCell } from '../cell'
export { RichTextField } from '../field'
export { defaultEditorLexicalConfig } from '../field/lexical/config/defaultClient'
export { ToolbarButton } from '../field/lexical/plugins/FloatingSelectToolbar/ToolbarButton'
export { ToolbarDropdown } from '../field/lexical/plugins/FloatingSelectToolbar/ToolbarDropdown/index'

View File

@@ -73,11 +73,11 @@ const RichText: React.FC<FieldProps> = (props) => {
<div className={`${baseClass}__wrap`}>
<Error message={errorMessage} showError={showError} />
<Label htmlFor={`field-${path.replace(/\./g, '__')}`} label={label} required={required} />
<ErrorBoundary fallbackRender={fallbackRender} onReset={(details) => {}}>
<ErrorBoundary fallbackRender={fallbackRender} onReset={() => {}}>
<LexicalProvider
editorConfig={editorConfig}
fieldProps={props}
onChange={(editorState, editor, tags) => {
onChange={(editorState) => {
let serializedEditorState = editorState.toJSON()
// Transform state through save hooks
@@ -94,7 +94,7 @@ const RichText: React.FC<FieldProps> = (props) => {
value={value}
/>
</ErrorBoundary>
<FieldDescription description={description} value={value} />
<FieldDescription description={description} path={path} value={value} />
</div>
</div>
)

View File

@@ -8,7 +8,6 @@ import type { HTMLConverter } from '../converters/html/converter/types'
import type { FeatureProvider } from '../types'
import { SlashMenuOption } from '../../lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/types'
import { BlockquoteIcon } from '../../lexical/ui/icons/Blockquote'
import { TextDropdownSectionWithEntries } from '../common/floatingSelectToolbarTextDropdownSection'
import { convertLexicalNodesToHTML } from '../converters/html/converter'
import { MarkdownTransformer } from './markdownTransformer'
@@ -21,8 +20,12 @@ export const BlockQuoteFeature = (): FeatureProvider => {
sections: [
TextDropdownSectionWithEntries([
{
ChildComponent: BlockquoteIcon,
isActive: ({ editor, selection }) => false,
ChildComponent: () =>
// @ts-expect-error
import('../../lexical/ui/icons/Blockquote').then(
(module) => module.BlockquoteIcon,
),
isActive: () => false,
key: 'blockquote',
label: `Blockquote`,
onClick: ({ editor }) => {
@@ -70,7 +73,11 @@ export const BlockQuoteFeature = (): FeatureProvider => {
key: 'basic',
options: [
new SlashMenuOption(`blockquote`, {
Icon: BlockquoteIcon,
Icon: () =>
// @ts-expect-error
import('../../lexical/ui/icons/Blockquote').then(
(module) => module.BlockquoteIcon,
),
displayName: `Blockquote`,
keywords: ['quote', 'blockquote'],
onSelect: () => {

View File

@@ -1 +0,0 @@
@import 'payload/scss';

View File

@@ -18,8 +18,7 @@ import type { BlocksFeatureProps } from '..'
import { useEditorConfigContext } from '../../../lexical/config/EditorConfigProvider'
import { $createBlockNode } from '../nodes/BlocksNode'
import { INSERT_BLOCK_COMMAND } from '../plugin'
import './index.scss'
import { INSERT_BLOCK_COMMAND } from '../plugin/commands'
const baseClass = 'lexical-blocks-drawer'
export const INSERT_BLOCK_WITH_DRAWER_COMMAND: LexicalCommand<{
@@ -64,7 +63,7 @@ export const BlocksDrawerComponent: React.FC = () => {
const [replaceNodeKey, setReplaceNodeKey] = useState<null | string>(null)
const editDepth = useEditDepth()
const { t } = useTranslation('fields')
const { closeModal, openModal } = useModal()
const { openModal } = useModal()
const labels = {
plural: t('blocks') || 'Blocks',

View File

@@ -1 +0,0 @@
@import 'payload/scss';

View File

@@ -6,10 +6,8 @@ import { formatLabels, getTranslation } from 'payload/utilities'
import type { FeatureProvider } from '../types'
import { SlashMenuOption } from '../../lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/types'
import { BlockIcon } from '../../lexical/ui/icons/Block'
import './index.scss'
import { BlockNode } from './nodes/BlocksNode'
import { BlocksPlugin, INSERT_BLOCK_COMMAND } from './plugin'
import { INSERT_BLOCK_COMMAND } from './plugin/commands'
import { blockPopulationPromiseHOC } from './populationPromise'
import { blockValidationHOC } from './validate'
@@ -43,7 +41,9 @@ export const BlocksFeature = (props?: BlocksFeatureProps): FeatureProvider => {
],
plugins: [
{
Component: BlocksPlugin,
Component: () =>
// @ts-expect-error
import('./plugin').then((module) => module.BlocksPlugin),
position: 'normal',
},
],
@@ -56,7 +56,9 @@ export const BlocksFeature = (props?: BlocksFeatureProps): FeatureProvider => {
options: [
...props.blocks.map((block) => {
return new SlashMenuOption('block-' + block.slug, {
Icon: BlockIcon,
Icon: () =>
// @ts-expect-error
import('../../lexical/ui/icons/Block').then((module) => module.BlockIcon),
displayName: ({ i18n }) => {
return getTranslation(block.labels.singular, i18n)
},

View File

@@ -14,7 +14,6 @@ import { DecoratorBlockNode } from '@lexical/react/LexicalDecoratorBlockNode'
import ObjectID from 'bson-objectid'
import React from 'react'
import { BlockComponent } from '../component'
import { transformInputFormData } from '../utils/transformInputFormData'
export type BlockFields = {
@@ -25,6 +24,13 @@ export type BlockFields = {
id: string
}
const BlockComponent = React.lazy(() =>
// @ts-expect-error TypeScript being dumb
import('../component').then((module) => ({
default: module.BlockComponent,
})),
)
export type SerializedBlockNode = Spread<
{
fields: BlockFields
@@ -70,7 +76,7 @@ export class BlockNode extends DecoratorBlockNode {
serializedNode = {
...serializedNode,
fields: {
...(serializedNode as any).data.fields,
...(serializedNode as any).fields.data,
},
version: 2,
}

View File

@@ -0,0 +1,8 @@
import type { LexicalCommand } from 'lexical'
import { createCommand } from 'lexical'
import type { InsertBlockPayload } from './index'
export const INSERT_BLOCK_COMMAND: LexicalCommand<InsertBlockPayload> =
createCommand('INSERT_BLOCK_COMMAND')

View File

@@ -1,19 +1,17 @@
'use client'
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import { $insertNodeToNearestRoot, mergeRegister } from '@lexical/utils'
import { COMMAND_PRIORITY_EDITOR, type LexicalCommand, createCommand } from 'lexical'
import { COMMAND_PRIORITY_EDITOR } from 'lexical'
import React, { useEffect } from 'react'
import type { BlockFields } from '../nodes/BlocksNode'
import { BlocksDrawerComponent } from '../drawer'
import { $createBlockNode, BlockNode } from '../nodes/BlocksNode'
import { INSERT_BLOCK_COMMAND } from './commands'
export type InsertBlockPayload = Exclude<BlockFields, 'id'>
export const INSERT_BLOCK_COMMAND: LexicalCommand<InsertBlockPayload> =
createCommand('INSERT_BLOCK_COMMAND')
export function BlocksPlugin(): JSX.Element | null {
const [editor] = useLexicalComposerContext()

View File

@@ -1,5 +1,4 @@
import type { HeadingTagType, SerializedHeadingNode } from '@lexical/rich-text'
import type React from 'react'
import { $createHeadingNode, HeadingNode } from '@lexical/rich-text'
import { $setBlocksType } from '@lexical/selection'
@@ -9,12 +8,6 @@ import type { HTMLConverter } from '../converters/html/converter/types'
import type { FeatureProvider } from '../types'
import { SlashMenuOption } from '../../lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/types'
import { H1Icon } from '../../lexical/ui/icons/H1'
import { H2Icon } from '../../lexical/ui/icons/H2'
import { H3Icon } from '../../lexical/ui/icons/H3'
import { H4Icon } from '../../lexical/ui/icons/H4'
import { H5Icon } from '../../lexical/ui/icons/H5'
import { H6Icon } from '../../lexical/ui/icons/H6'
import { TextDropdownSectionWithEntries } from '../common/floatingSelectToolbarTextDropdownSection'
import { convertLexicalNodesToHTML } from '../converters/html/converter'
import { MarkdownTransformer } from './markdownTransformer'
@@ -30,13 +23,19 @@ type Props = {
enabledHeadingSizes?: HeadingTagType[]
}
const HeadingToIconMap: Record<HeadingTagType, React.FC> = {
h1: H1Icon,
h2: H2Icon,
h3: H3Icon,
h4: H4Icon,
h5: H5Icon,
h6: H6Icon,
const iconImports = {
// @ts-expect-error
h1: () => import('../../lexical/ui/icons/H1').then((module) => module.H1Icon),
// @ts-expect-error
h2: () => import('../../lexical/ui/icons/H2').then((module) => module.H2Icon),
// @ts-expect-error
h3: () => import('../../lexical/ui/icons/H3').then((module) => module.H3Icon),
// @ts-expect-error
h4: () => import('../../lexical/ui/icons/H4').then((module) => module.H4Icon),
// @ts-expect-error
h5: () => import('../../lexical/ui/icons/H5').then((module) => module.H5Icon),
// @ts-expect-error
h6: () => import('../../lexical/ui/icons/H6').then((module) => module.H6Icon),
}
export const HeadingFeature = (props: Props): FeatureProvider => {
@@ -50,7 +49,7 @@ export const HeadingFeature = (props: Props): FeatureProvider => {
...enabledHeadingSizes.map((headingSize, i) =>
TextDropdownSectionWithEntries([
{
ChildComponent: HeadingToIconMap[headingSize],
ChildComponent: iconImports[headingSize],
isActive: () => false,
key: headingSize,
label: `Heading ${headingSize.charAt(1)}`,
@@ -98,7 +97,7 @@ export const HeadingFeature = (props: Props): FeatureProvider => {
key: 'basic',
options: [
new SlashMenuOption(`heading-${headingSize.charAt(1)}`, {
Icon: HeadingToIconMap[headingSize],
Icon: iconImports[headingSize],
displayName: `Heading ${headingSize.charAt(1)}`,
keywords: ['heading', headingSize],
onSelect: () => {

View File

@@ -1,5 +1,6 @@
import type { Config } from 'payload/config'
import type { Field } from 'payload/types'
import type { RadioField, TextField } from 'payload/types'
import { extractTranslations } from 'payload/utilities'
@@ -14,73 +15,103 @@ const translations = extractTranslations([
'fields:openInNewTab',
])
export const getBaseFields = (config: Config): Field[] => [
{
name: 'text',
label: translations['fields:textToDisplay'],
required: true,
type: 'text',
},
{
name: 'fields',
admin: {
style: {
borderBottom: 0,
borderTop: 0,
margin: 0,
padding: 0,
},
export const getBaseFields = (
config: Config,
enabledCollections: false | string[],
disabledCollections: false | string[],
): Field[] => {
let enabledRelations: string[]
/**
* Figure out which relations should be enabled (enabledRelations) based on a collection's admin.enableRichTextLink property,
* or the Link Feature's enabledCollections and disabledCollections properties which override it.
*/
if (enabledCollections) {
enabledRelations = enabledCollections
} else if (disabledCollections) {
enabledRelations = config.collections
.filter(({ slug }) => !disabledCollections.includes(slug))
.map(({ slug }) => slug)
} else {
enabledRelations = config.collections
.filter(({ admin: { enableRichTextLink } }) => enableRichTextLink)
.map(({ slug }) => slug)
}
const baseFields = [
{
name: 'text',
label: translations['fields:textToDisplay'],
required: true,
type: 'text',
},
fields: [
{
name: 'linkType',
admin: {
description: translations['fields:chooseBetweenCustomTextOrDocument'],
{
name: 'fields',
admin: {
style: {
borderBottom: 0,
borderTop: 0,
margin: 0,
padding: 0,
},
defaultValue: 'custom',
label: translations['fields:linkType'],
options: [
{
label: translations['fields:customURL'],
value: 'custom',
},
{
label: translations['fields:internalLink'],
value: 'internal',
},
],
required: true,
type: 'radio',
},
{
name: 'url',
admin: {
condition: ({ fields }) => fields?.linkType !== 'internal',
fields: [
{
name: 'linkType',
admin: {
description: translations['fields:chooseBetweenCustomTextOrDocument'],
},
defaultValue: 'custom',
label: translations['fields:linkType'],
options: [
{
label: translations['fields:customURL'],
value: 'custom',
},
],
required: true,
type: 'radio',
},
label: translations['fields:enterURL'],
required: true,
type: 'text',
},
{
name: 'doc',
admin: {
condition: ({ fields }) => {
return fields?.linkType === 'internal'
},
{
name: 'url',
label: translations['fields:enterURL'],
required: true,
type: 'text',
},
] as Field[],
type: 'group',
},
]
// Only display internal link-specific fields / options / conditions if there are enabled relations
if (enabledRelations?.length) {
;(baseFields[1].fields[0] as RadioField).options.push({
label: translations['fields:internalLink'],
value: 'internal',
})
;(baseFields[1].fields[1] as TextField).admin = {
condition: ({ fields }) => fields?.linkType !== 'internal',
}
baseFields[1].fields.push({
name: 'doc',
admin: {
condition: ({ fields }) => {
return fields?.linkType === 'internal'
},
label: translations['fields:chooseDocumentToLink'],
relationTo: config.collections
.filter(({ admin: { enableRichTextLink } }) => enableRichTextLink)
.map(({ slug }) => slug),
required: true,
type: 'relationship',
},
{
name: 'newTab',
label: translations['fields:openInNewTab'],
type: 'checkbox',
},
],
type: 'group',
},
]
label: translations['fields:chooseDocumentToLink'],
relationTo: enabledRelations,
required: true,
type: 'relationship',
})
}
baseFields[1].fields.push({
name: 'newTab',
label: translations['fields:openInNewTab'],
type: 'checkbox',
})
return baseFields as Field[]
}

View File

@@ -1 +0,0 @@
@import 'payload/scss';

View File

@@ -4,32 +4,52 @@ import type { Field } from 'payload/types'
import { $findMatchingParent } from '@lexical/utils'
import { $getSelection, $isRangeSelection } from 'lexical'
import { withMergedProps } from 'payload/utilities'
import type { HTMLConverter } from '../converters/html/converter/types'
import type { FeatureProvider } from '../types'
import type { SerializedAutoLinkNode } from './nodes/AutoLinkNode'
import type { LinkFields, SerializedLinkNode } from './nodes/LinkNode'
import { LinkIcon } from '../../lexical/ui/icons/Link'
import { getSelectedNode } from '../../lexical/utils/getSelectedNode'
import { FeaturesSectionWithEntries } from '../common/floatingSelectToolbarFeaturesButtonsSection'
import { convertLexicalNodesToHTML } from '../converters/html/converter'
import './index.scss'
import { AutoLinkNode } from './nodes/AutoLinkNode'
import { $isLinkNode, LinkNode, TOGGLE_LINK_COMMAND } from './nodes/LinkNode'
import { AutoLinkPlugin } from './plugins/autoLink'
import { ClickableLinkPlugin } from './plugins/clickableLink'
import { FloatingLinkEditorPlugin } from './plugins/floatingLinkEditor'
import { TOGGLE_LINK_WITH_MODAL_COMMAND } from './plugins/floatingLinkEditor/LinkEditor'
import { LinkPlugin } from './plugins/link'
import { TOGGLE_LINK_WITH_MODAL_COMMAND } from './plugins/floatingLinkEditor/LinkEditor/commands'
import { linkPopulationPromiseHOC } from './populationPromise'
export type LinkFeatureProps = {
type ExclusiveLinkCollectionsProps =
| {
/**
* The collections that should be disabled for internal linking. Overrides the `enableRichTextLink` property in the collection config.
* When this property is set, `enabledCollections` will not be available.
**/
disabledCollections?: string[]
// Ensures that enabledCollections is not available when disabledCollections is set
enabledCollections?: never
}
| {
// Ensures that disabledCollections is not available when enabledCollections is set
disabledCollections?: never
/**
* The collections that should be enabled for internal linking. Overrides the `enableRichTextLink` property in the collection config
* When this property is set, `disabledCollections` will not be available.
**/
enabledCollections?: string[]
}
export type LinkFeatureProps = ExclusiveLinkCollectionsProps & {
/**
* A function or array defining additional fields for the link feature. These will be
* displayed in the link editor drawer.
*/
fields?:
| ((args: { config: SanitizedConfig; defaultFields: Field[]; i18n: i18n }) => Field[])
| Field[]
}
export const LinkFeature = (props: LinkFeatureProps): FeatureProvider => {
return {
feature: () => {
@@ -38,7 +58,9 @@ export const LinkFeature = (props: LinkFeatureProps): FeatureProvider => {
sections: [
FeaturesSectionWithEntries([
{
ChildComponent: LinkIcon,
ChildComponent: () =>
// @ts-expect-error
import('../../lexical/ui/icons/Link').then((module) => module.LinkIcon),
isActive: ({ selection }) => {
if ($isRangeSelection(selection)) {
const selectedNode = getSelectedNode(selection)
@@ -134,22 +156,35 @@ export const LinkFeature = (props: LinkFeatureProps): FeatureProvider => {
],
plugins: [
{
Component: LinkPlugin,
Component: () =>
// @ts-expect-error
import('./plugins/link').then((module) => module.LinkPlugin),
position: 'normal',
},
{
Component: AutoLinkPlugin,
Component: () =>
// @ts-expect-error
import('./plugins/autoLink').then((module) => module.AutoLinkPlugin),
position: 'normal',
},
{
Component: ClickableLinkPlugin,
Component: () =>
// @ts-expect-error
import('./plugins/clickableLink').then((module) => module.ClickableLinkPlugin),
position: 'normal',
},
{
Component: withMergedProps({
Component: FloatingLinkEditorPlugin,
toMergeIntoProps: props,
}),
Component: () =>
// @ts-expect-error
import('./plugins/floatingLinkEditor').then((module) => {
const floatingLinkEditorPlugin = module.FloatingLinkEditorPlugin
return import('payload/utilities').then((module) =>
module.withMergedProps({
Component: floatingLinkEditorPlugin,
toMergeIntoProps: props,
}),
)
}),
position: 'floatingAnchorElem',
},
],

View File

@@ -0,0 +1,9 @@
import type { LexicalCommand } from 'lexical'
import { createCommand } from 'lexical'
import type { LinkPayload } from '../types'
export const TOGGLE_LINK_WITH_MODAL_COMMAND: LexicalCommand<LinkPayload | null> = createCommand(
'TOGGLE_LINK_WITH_MODAL_COMMAND',
)

View File

@@ -1,5 +1,4 @@
'use client'
import type { LexicalCommand } from 'lexical'
import type { Data, Fields } from 'payload/types'
import { useModal } from '@faceless-ui/modal'
@@ -12,7 +11,6 @@ import {
COMMAND_PRIORITY_LOW,
KEY_ESCAPE_COMMAND,
SELECTION_CHANGE_COMMAND,
createCommand,
} from 'lexical'
import { formatDrawerSlug } from 'payload/components/elements'
import {
@@ -38,13 +36,12 @@ import { setFloatingElemPositionForLinkEditor } from '../../../../../lexical/uti
import { LinkDrawer } from '../../../drawer'
import { $isLinkNode, TOGGLE_LINK_COMMAND } from '../../../nodes/LinkNode'
import { transformExtraFields } from '../utilities'
export const TOGGLE_LINK_WITH_MODAL_COMMAND: LexicalCommand<LinkPayload | null> = createCommand(
'TOGGLE_LINK_WITH_MODAL_COMMAND',
)
import { TOGGLE_LINK_WITH_MODAL_COMMAND } from './commands'
export function LinkEditor({
anchorElem,
disabledCollections,
enabledCollections,
fields: customFieldSchema,
}: { anchorElem: HTMLElement } & LinkFeatureProps): JSX.Element {
const [editor] = useLexicalComposerContext()
@@ -66,7 +63,13 @@ export function LinkEditor({
const [initialState, setInitialState] = useState<Fields>({})
const [fieldSchema] = useState(() => {
const fieldsUnsanitized = transformExtraFields(customFieldSchema, config, i18n)
const fieldsUnsanitized = transformExtraFields(
customFieldSchema,
config,
i18n,
enabledCollections,
disabledCollections,
)
// Sanitize custom fields here
const validRelationships = config.collections.map((c) => c.slug) || []
const fields = sanitizeFields({

View File

@@ -9,8 +9,10 @@ import './index.scss'
export const FloatingLinkEditorPlugin: React.FC<
{
anchorElem?: HTMLElement
anchorElem: HTMLElement
} & LinkFeatureProps
> = ({ anchorElem = document.body, fields = [] }) => {
return createPortal(<LinkEditor anchorElem={anchorElem} fields={fields} />, anchorElem)
> = (props) => {
const { anchorElem = document.body } = props
return createPortal(<LinkEditor {...props} anchorElem={anchorElem} />, anchorElem)
}

View File

@@ -13,8 +13,10 @@ export function transformExtraFields(
| Field[],
config: SanitizedConfig,
i18n: i18n,
enabledCollections?: false | string[],
disabledCollections?: false | string[],
): Field[] {
const baseFields: Field[] = getBaseFields(config)
const baseFields: Field[] = getBaseFields(config, enabledCollections, disabledCollections)
const fields =
typeof customFieldSchema === 'function'

View File

@@ -4,19 +4,20 @@ import { $createParagraphNode, $getSelection, $isRangeSelection } from 'lexical'
import type { FeatureProvider } from '../types'
import { SlashMenuOption } from '../../lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/types'
import { TextIcon } from '../../lexical/ui/icons/Text'
import { TextDropdownSectionWithEntries } from '../common/floatingSelectToolbarTextDropdownSection'
export const ParagraphFeature = (): FeatureProvider => {
return {
feature: ({ resolvedFeatures, unsanitizedEditorConfig }) => {
feature: () => {
return {
floatingSelectToolbar: {
sections: [
TextDropdownSectionWithEntries([
{
ChildComponent: TextIcon,
isActive: ({ editor, selection }) => false,
ChildComponent: () =>
// @ts-expect-error
import('../../lexical/ui/icons/Text').then((module) => module.TextIcon),
isActive: () => false,
key: 'normal-text',
label: 'Normal Text',
onClick: ({ editor }) => {
@@ -40,7 +41,9 @@ export const ParagraphFeature = (): FeatureProvider => {
key: 'basic',
options: [
new SlashMenuOption('paragraph', {
Icon: TextIcon,
Icon: () =>
// @ts-expect-error
import('../../lexical/ui/icons/Text').then((module) => module.TextIcon),
displayName: 'Paragraph',
keywords: ['normal', 'paragraph', 'p', 'text'],
onSelect: ({ editor }) => {

View File

@@ -0,0 +1,7 @@
import type { LexicalCommand } from 'lexical'
import { createCommand } from 'lexical'
export const INSERT_RELATIONSHIP_WITH_DRAWER_COMMAND: LexicalCommand<{
replace: { nodeKey: string } | false
}> = createCommand('INSERT_RELATIONSHIP_WITH_DRAWER_COMMAND')

View File

@@ -1 +0,0 @@
@import 'payload/scss';

View File

@@ -1,25 +1,13 @@
'use client'
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import {
$getNodeByKey,
COMMAND_PRIORITY_EDITOR,
type LexicalCommand,
type LexicalEditor,
createCommand,
} from 'lexical'
import { $getNodeByKey, COMMAND_PRIORITY_EDITOR, type LexicalEditor } from 'lexical'
import { useListDrawer } from 'payload/components/elements'
import React, { useCallback, useEffect, useState } from 'react'
import { $createRelationshipNode } from '../nodes/RelationshipNode'
import { INSERT_RELATIONSHIP_COMMAND } from '../plugins'
import { EnabledRelationshipsCondition } from '../utils/EnabledRelationshipsCondition'
import './index.scss'
const baseClass = 'lexical-relationship-drawer'
export const INSERT_RELATIONSHIP_WITH_DRAWER_COMMAND: LexicalCommand<{
replace: { nodeKey: string } | false
}> = createCommand('INSERT_RELATIONSHIP_WITH_DRAWER_COMMAND')
import { INSERT_RELATIONSHIP_WITH_DRAWER_COMMAND } from './commands'
const insertRelationship = ({
id,
@@ -50,7 +38,7 @@ const insertRelationship = ({
}
type Props = {
enabledCollectionSlugs: string[]
enabledCollectionSlugs: null | string[]
}
const RelationshipDrawerComponent: React.FC<Props> = ({ enabledCollectionSlugs }) => {
@@ -102,7 +90,9 @@ const RelationshipDrawerComponent: React.FC<Props> = ({ enabledCollectionSlugs }
}
export const RelationshipDrawer = (props: Props): React.ReactNode => {
return (
return props?.enabledCollectionSlugs?.length > 0 ? ( // If enabledCollectionSlugs it overrides what EnabledRelationshipsCondition is doing
<RelationshipDrawerComponent {...props} />
) : (
<EnabledRelationshipsCondition {...props}>
<RelationshipDrawerComponent {...props} />
</EnabledRelationshipsCondition>

View File

@@ -1 +0,0 @@
@import 'payload/scss';

View File

@@ -1,14 +1,33 @@
import type { FeatureProvider } from '../types'
import { SlashMenuOption } from '../../lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/types'
import { RelationshipIcon } from '../../lexical/ui/icons/Relationship'
import { INSERT_RELATIONSHIP_WITH_DRAWER_COMMAND } from './drawer'
import './index.scss'
import { INSERT_RELATIONSHIP_WITH_DRAWER_COMMAND } from './drawer/commands'
import { RelationshipNode } from './nodes/RelationshipNode'
import RelationshipPlugin from './plugins'
import { relationshipPopulationPromise } from './populationPromise'
export const RelationshipFeature = (): FeatureProvider => {
export type RelationshipFeatureProps =
| {
/**
* The collections that should be disabled. Overrides the `enableRichTextRelationship` property in the collection config.
* When this property is set, `enabledCollections` will not be available.
**/
disabledCollections?: string[]
// Ensures that enabledCollections is not available when disabledCollections is set
enabledCollections?: never
}
| {
// Ensures that disabledCollections is not available when enabledCollections is set
disabledCollections?: never
/**
* The collections that should be enabled. Overrides the `enableRichTextRelationship` property in the collection config
* When this property is set, `disabledCollections` will not be available.
**/
enabledCollections?: string[]
}
export const RelationshipFeature = (props?: RelationshipFeatureProps): FeatureProvider => {
return {
feature: () => {
return {
@@ -22,11 +41,21 @@ export const RelationshipFeature = (): FeatureProvider => {
],
plugins: [
{
Component: RelationshipPlugin,
Component: () =>
// @ts-expect-error
import('./plugins').then((module) => {
const RelationshipPlugin = module.RelationshipPlugin
return import('payload/utilities').then((module2) =>
module2.withMergedProps({
Component: RelationshipPlugin,
toMergeIntoProps: props,
}),
)
}),
position: 'normal',
},
],
props: null,
props: props,
slashMenu: {
options: [
{
@@ -34,7 +63,11 @@ export const RelationshipFeature = (): FeatureProvider => {
key: 'basic',
options: [
new SlashMenuOption('relationship', {
Icon: RelationshipIcon,
Icon: () =>
// @ts-expect-error
import('../../lexical/ui/icons/Relationship').then(
(module) => module.RelationshipIcon,
),
displayName: 'Relationship',
keywords: ['relationship', 'relation', 'rel'],
onSelect: ({ editor }) => {

View File

@@ -16,7 +16,12 @@ import {
} from '@lexical/react/LexicalDecoratorBlockNode'
import * as React from 'react'
import { RelationshipComponent } from './components/RelationshipComponent'
const RelationshipComponent = React.lazy(() =>
// @ts-expect-error TypeScript being dumb
import('./components/RelationshipComponent').then((module) => ({
default: module.RelationshipComponent,
})),
)
export type RelationshipData = {
relationTo: string

View File

@@ -13,8 +13,7 @@ import { useTranslation } from 'react-i18next'
import type { RelationshipData } from '../RelationshipNode'
import { useEditorConfigContext } from '../../../../lexical/config/EditorConfigProvider'
import { INSERT_RELATIONSHIP_WITH_DRAWER_COMMAND } from '../../drawer'
import { EnabledRelationshipsCondition } from '../../utils/EnabledRelationshipsCondition'
import { INSERT_RELATIONSHIP_WITH_DRAWER_COMMAND } from '../../drawer/commands'
import './index.scss'
const baseClass = 'lexical-relationship'
@@ -140,9 +139,5 @@ const Component: React.FC<Props> = (props) => {
}
export const RelationshipComponent = (props: Props): React.ReactNode => {
return (
<EnabledRelationshipsCondition {...props}>
<Component {...props} />
</EnabledRelationshipsCondition>
)
return <Component {...props} />
}

View File

@@ -6,6 +6,7 @@ import { useConfig } from 'payload/components/utilities'
import { useEffect } from 'react'
import React from 'react'
import type { RelationshipFeatureProps } from '../index'
import type { RelationshipData } from '../nodes/RelationshipNode'
import { RelationshipDrawer } from '../drawer'
@@ -15,10 +16,20 @@ export const INSERT_RELATIONSHIP_COMMAND: LexicalCommand<RelationshipData> = cre
'INSERT_RELATIONSHIP_COMMAND',
)
export default function RelationshipPlugin(): JSX.Element | null {
export function RelationshipPlugin(props?: RelationshipFeatureProps): JSX.Element | null {
const [editor] = useLexicalComposerContext()
const { collections } = useConfig()
let enabledRelations: string[] = null
if (props?.enabledCollections) {
enabledRelations = props?.enabledCollections
} else if (props?.disabledCollections) {
enabledRelations = collections
.filter(({ slug }) => !(props?.disabledCollections).includes(slug))
.map(({ slug }) => slug)
}
useEffect(() => {
if (!editor.hasNodes([RelationshipNode])) {
throw new Error('RelationshipPlugin: RelationshipNode not registered on editor')
@@ -36,5 +47,5 @@ export default function RelationshipPlugin(): JSX.Element | null {
)
}, [editor])
return <RelationshipDrawer enabledCollectionSlugs={collections.map(({ slug }) => slug)} />
return <RelationshipDrawer enabledCollectionSlugs={enabledRelations} />
}

View File

@@ -18,7 +18,7 @@ import type { UploadData } from '../nodes/UploadNode'
import { useEditorConfigContext } from '../../../lexical/config/EditorConfigProvider'
import { EnabledRelationshipsCondition } from '../../Relationship/utils/EnabledRelationshipsCondition'
import { INSERT_UPLOAD_WITH_DRAWER_COMMAND } from '../drawer'
import { INSERT_UPLOAD_WITH_DRAWER_COMMAND } from '../drawer/commands'
import { ExtraFieldsUploadDrawer } from './ExtraFieldsDrawer'
import './index.scss'

View File

@@ -0,0 +1,7 @@
import type { LexicalCommand } from 'lexical'
import { createCommand } from 'lexical'
export const INSERT_UPLOAD_WITH_DRAWER_COMMAND: LexicalCommand<{
replace: { nodeKey: string } | false
}> = createCommand('INSERT_UPLOAD_WITH_DRAWER_COMMAND')

View File

@@ -1 +0,0 @@
@import 'payload/scss';

View File

@@ -1,26 +1,16 @@
'use client'
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import {
$getNodeByKey,
COMMAND_PRIORITY_EDITOR,
type LexicalCommand,
type LexicalEditor,
createCommand,
} from 'lexical'
import { $getNodeByKey, COMMAND_PRIORITY_EDITOR, type LexicalEditor } from 'lexical'
import { useListDrawer } from 'payload/components/elements'
import React, { useCallback, useEffect, useState } from 'react'
import { EnabledRelationshipsCondition } from '../../Relationship/utils/EnabledRelationshipsCondition'
import { $createUploadNode } from '../nodes/UploadNode'
import { INSERT_UPLOAD_COMMAND } from '../plugin'
import './index.scss'
import { INSERT_UPLOAD_WITH_DRAWER_COMMAND } from './commands'
const baseClass = 'lexical-upload-drawer'
export const INSERT_UPLOAD_WITH_DRAWER_COMMAND: LexicalCommand<{
replace: { nodeKey: string } | false
}> = createCommand('INSERT_UPLOAD_WITH_DRAWER_COMMAND')
const insertUpload = ({
id,
editor,

View File

@@ -1 +0,0 @@
@import 'payload/scss';

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