Compare commits
26 Commits
richtext-l
...
richtext-l
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
76
CHANGELOG.md
76
CHANGELOG.md
@@ -1,3 +1,79 @@
|
||||
## [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)
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.1",
|
||||
"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",
|
||||
|
||||
@@ -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,51 +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 {
|
||||
if (['json', 'relationship', '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
|
||||
} else {
|
||||
fieldAccess = policies[entityType][entitySlug].fields
|
||||
|
||||
if (['json', 'relationship', '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
|
||||
|
||||
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.1",
|
||||
"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
|
||||
|
||||
@@ -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';
|
||||
@@ -7,11 +7,8 @@ import type { FeatureProvider } from '../types'
|
||||
import type { SerializedUploadNode } from './nodes/UploadNode'
|
||||
|
||||
import { SlashMenuOption } from '../../lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/types'
|
||||
import { UploadIcon } from '../../lexical/ui/icons/Upload'
|
||||
import { INSERT_UPLOAD_WITH_DRAWER_COMMAND } from './drawer'
|
||||
import './index.scss'
|
||||
import { INSERT_UPLOAD_WITH_DRAWER_COMMAND } from './drawer/commands'
|
||||
import { UploadNode } from './nodes/UploadNode'
|
||||
import { UploadPlugin } from './plugin'
|
||||
import { uploadPopulationPromiseHOC } from './populationPromise'
|
||||
import { uploadValidation } from './validate'
|
||||
|
||||
@@ -55,7 +52,9 @@ export const UploadFeature = (props?: UploadFeatureProps): FeatureProvider => {
|
||||
],
|
||||
plugins: [
|
||||
{
|
||||
Component: UploadPlugin,
|
||||
Component: () =>
|
||||
// @ts-expect-error
|
||||
import('./plugin').then((module) => module.UploadPlugin),
|
||||
position: 'normal',
|
||||
},
|
||||
],
|
||||
@@ -67,7 +66,9 @@ export const UploadFeature = (props?: UploadFeatureProps): FeatureProvider => {
|
||||
key: 'basic',
|
||||
options: [
|
||||
new SlashMenuOption('upload', {
|
||||
Icon: UploadIcon,
|
||||
Icon: () =>
|
||||
// @ts-expect-error
|
||||
import('../../lexical/ui/icons/Upload').then((module) => module.UploadIcon),
|
||||
displayName: 'Upload',
|
||||
keywords: ['upload', 'image', 'file', 'img', 'picture', 'photo', 'media'],
|
||||
onSelect: ({ editor }) => {
|
||||
|
||||
@@ -3,14 +3,13 @@ import type {
|
||||
FloatingToolbarSectionEntry,
|
||||
} from '../../lexical/plugins/FloatingSelectToolbar/types'
|
||||
|
||||
import { AlignLeftIcon } from '../../lexical/ui/icons/AlignLeft'
|
||||
import './index.scss'
|
||||
|
||||
export const AlignDropdownSectionWithEntries = (
|
||||
entries: FloatingToolbarSectionEntry[],
|
||||
): FloatingToolbarSection => {
|
||||
return {
|
||||
ChildComponent: AlignLeftIcon,
|
||||
ChildComponent: () =>
|
||||
// @ts-expect-error
|
||||
import('../../lexical/ui/icons/AlignLeft').then((module) => module.AlignLeftIcon),
|
||||
entries,
|
||||
key: 'dropdown-align',
|
||||
order: 2,
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
.floating-select-toolbar-popup__section-dropdown-align {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
}
|
||||
@@ -2,21 +2,20 @@ import { FORMAT_ELEMENT_COMMAND } from 'lexical'
|
||||
|
||||
import type { FeatureProvider } from '../types'
|
||||
|
||||
import { AlignCenterIcon } from '../../lexical/ui/icons/AlignCenter'
|
||||
import { AlignLeftIcon } from '../../lexical/ui/icons/AlignLeft'
|
||||
import { AlignDropdownSectionWithEntries } from './floatingSelectToolbarAlignDropdownSection'
|
||||
import './index.scss'
|
||||
|
||||
export const AlignFeature = (): FeatureProvider => {
|
||||
return {
|
||||
feature: ({ resolvedFeatures, unsanitizedEditorConfig }) => {
|
||||
feature: () => {
|
||||
return {
|
||||
floatingSelectToolbar: {
|
||||
sections: [
|
||||
AlignDropdownSectionWithEntries([
|
||||
{
|
||||
ChildComponent: AlignLeftIcon,
|
||||
isActive: ({ editor, selection }) => false,
|
||||
ChildComponent: () =>
|
||||
// @ts-expect-error
|
||||
import('../../lexical/ui/icons/AlignLeft').then((module) => module.AlignLeftIcon),
|
||||
isActive: () => false,
|
||||
key: 'align-left',
|
||||
label: `Align Left`,
|
||||
onClick: ({ editor }) => {
|
||||
@@ -27,8 +26,12 @@ export const AlignFeature = (): FeatureProvider => {
|
||||
]),
|
||||
AlignDropdownSectionWithEntries([
|
||||
{
|
||||
ChildComponent: AlignCenterIcon,
|
||||
isActive: ({ editor, selection }) => false,
|
||||
ChildComponent: () =>
|
||||
// @ts-expect-error
|
||||
import('../../lexical/ui/icons/AlignCenter').then(
|
||||
(module) => module.AlignCenterIcon,
|
||||
),
|
||||
isActive: () => false,
|
||||
key: 'align-center',
|
||||
label: `Align Center`,
|
||||
onClick: ({ editor }) => {
|
||||
@@ -39,8 +42,12 @@ export const AlignFeature = (): FeatureProvider => {
|
||||
]),
|
||||
AlignDropdownSectionWithEntries([
|
||||
{
|
||||
ChildComponent: AlignLeftIcon,
|
||||
isActive: ({ editor, selection }) => false,
|
||||
ChildComponent: () =>
|
||||
// @ts-expect-error
|
||||
import('../../lexical/ui/icons/AlignRight').then(
|
||||
(module) => module.AlignRightIcon,
|
||||
),
|
||||
isActive: () => false,
|
||||
key: 'align-right',
|
||||
label: `Align Right`,
|
||||
onClick: ({ editor }) => {
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
.floating-select-toolbar-popup__section-features {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
}
|
||||
@@ -3,8 +3,6 @@ import type {
|
||||
FloatingToolbarSectionEntry,
|
||||
} from '../../../lexical/plugins/FloatingSelectToolbar/types'
|
||||
|
||||
import './index.scss'
|
||||
|
||||
export const FeaturesSectionWithEntries = (
|
||||
entries: FloatingToolbarSectionEntry[],
|
||||
): FloatingToolbarSection => {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user