Compare commits
46 Commits
db-mongodb
...
db-postgre
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a8ac5d2cfd | ||
|
|
e6318ae700 | ||
|
|
f5166cd4c4 | ||
|
|
7c7bf51b4b | ||
|
|
eefcd88de7 | ||
|
|
217cc1fc42 | ||
|
|
7af8f29b4a | ||
|
|
5f2cd1ae77 | ||
|
|
dbaecda0e9 | ||
|
|
cf9a3704df | ||
|
|
4b5453e8e5 | ||
|
|
5de347ffff | ||
|
|
80ef18c149 | ||
|
|
912abe2b64 | ||
|
|
4090aebb0e | ||
|
|
290e9d8238 | ||
|
|
50253f617c | ||
|
|
999e05d1b4 | ||
|
|
b6cffcea07 | ||
|
|
7b2eb0c175 | ||
|
|
3b8a27d199 | ||
|
|
65adfd21ed | ||
|
|
03a387233d | ||
|
|
fcbe5744d9 | ||
|
|
06bf6a426e | ||
|
|
b634d5e552 | ||
|
|
5f173241df | ||
|
|
0bd12e01d7 | ||
|
|
b6f02765eb | ||
|
|
156ffdd18c | ||
|
|
fe888b5f6c | ||
|
|
bea79feaea | ||
|
|
293cee6f90 | ||
|
|
3e745e91da | ||
|
|
4243048fc5 | ||
|
|
ef84a2cfff | ||
|
|
c00cbaabbc | ||
|
|
02f407e995 | ||
|
|
74e8051bb6 | ||
|
|
ee670b2b20 | ||
|
|
2f8bcc977b | ||
|
|
0cc91d7377 | ||
|
|
34e89ff5db | ||
|
|
b39b52dbd3 | ||
|
|
7bfdb2627a | ||
|
|
8f5867e876 |
86
CHANGELOG.md
86
CHANGELOG.md
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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: [],
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
position: sticky;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
z-index: 1;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -214,6 +214,7 @@ const ArrayFieldType: React.FC<Props> = (props) => {
|
||||
<FieldDescription
|
||||
className={`field-description-${path.replace(/\./g, '__')}`}
|
||||
description={description}
|
||||
path={path}
|
||||
value={value}
|
||||
/>
|
||||
</header>
|
||||
|
||||
@@ -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))) && (
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -77,6 +77,7 @@ const Group: React.FC<Props> = (props) => {
|
||||
<FieldDescription
|
||||
className={`field-description-${path.replace(/\./g, '__')}`}
|
||||
description={description}
|
||||
path={path}
|
||||
value={null}
|
||||
/>
|
||||
</header>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>>
|
||||
>
|
||||
}
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -95,6 +95,7 @@ const TextInput: React.FC<TextInputProps> = (props) => {
|
||||
<FieldDescription
|
||||
className={`field-description-${path.replace(/\./g, '__')}`}
|
||||
description={description}
|
||||
path={path}
|
||||
value={value}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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} />}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 || []),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
// /////////////////////////////////////
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -22,3 +22,9 @@ export {
|
||||
formatListDrawerSlug,
|
||||
useListDrawer,
|
||||
} from '../../admin/components/elements/ListDrawer'
|
||||
|
||||
export {
|
||||
Description,
|
||||
DescriptionComponent,
|
||||
DescriptionFunction,
|
||||
} from '../../admin/components/forms/FieldDescription/types'
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
374
packages/payload/src/translations/zh-tw.json
Normal file
374
packages/payload/src/translations/zh-tw.json
Normal 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}}的版本"
|
||||
}
|
||||
}
|
||||
@@ -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}}的版本"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -142,7 +142,7 @@ const getBaseUploadFields = ({ collection, config }: Options): Field[] => {
|
||||
return `${uploadOptions.staticURL}/${sizeFilename}`
|
||||
}
|
||||
|
||||
return undefined
|
||||
return null
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
26
packages/payload/src/uploads/getFileByURL.ts
Normal file
26
packages/payload/src/uploads/getFileByURL.ts
Normal 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
|
||||
@@ -22,6 +22,7 @@ export type FileData = {
|
||||
mimeType: string
|
||||
sizes: FileSizes
|
||||
tempFilePath?: string
|
||||
url?: string
|
||||
width: number
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
4
packages/richtext-lexical/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
/utilities.d.ts
|
||||
/utilities.js
|
||||
/components.d.ts
|
||||
/components.js
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
6
packages/richtext-lexical/src/exports/components.ts
Normal file
6
packages/richtext-lexical/src/exports/components.ts
Normal 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'
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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: () => {
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
@import 'payload/scss';
|
||||
@@ -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',
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
@import 'payload/scss';
|
||||
@@ -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)
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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')
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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: () => {
|
||||
|
||||
@@ -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[]
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
@import 'payload/scss';
|
||||
@@ -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',
|
||||
},
|
||||
],
|
||||
|
||||
@@ -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',
|
||||
)
|
||||
@@ -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({
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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 }) => {
|
||||
|
||||
@@ -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')
|
||||
@@ -1 +0,0 @@
|
||||
@import 'payload/scss';
|
||||
@@ -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>
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
@import 'payload/scss';
|
||||
@@ -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 }) => {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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} />
|
||||
}
|
||||
|
||||
@@ -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} />
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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')
|
||||
@@ -1 +0,0 @@
|
||||
@import 'payload/scss';
|
||||
@@ -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,
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
@import 'payload/scss';
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user