Compare commits

...

74 Commits

Author SHA1 Message Date
Dan Ribbens
43cdb45620 fix: mongodb multiple error rollback transaction 2023-11-22 11:53:45 -05:00
Jessica Chowdhury
6364afb1dd docs: updates vite alias example and fixes broken link (#4224) 2023-11-21 14:34:18 -05:00
Radosław Kłos
56a4692662 fix: typo in polish translations (#4234) 2023-11-21 13:09:00 -05:00
Dan Ribbens
ef6b8e4235 test: graphql handle deleted relationships (#4229) 2023-11-21 13:07:13 -05:00
Elliot DeNolf
5f5290341a chore(plugin-cloud-storage): adjust prepublishOnly script 2023-11-21 10:28:35 -05:00
Elliot DeNolf
62403584ad chore(scripts): cleanup package details log 2023-11-21 10:28:17 -05:00
Jarrod Flesch
19fcfc27af fix: number field validation (#4233) 2023-11-21 10:12:26 -05:00
Elliot DeNolf
dcf14f5f71 chore(release): payload/2.2.1 [skip ci] 2023-11-21 10:08:07 -05:00
Elliot DeNolf
3a784a06cc fix: make outputSchema optional on richtext config (#4230) 2023-11-21 09:45:57 -05:00
Jessica Chowdhury
6eeae9d53b examples: updates blank template readme (#4216) 2023-11-21 08:40:01 -05:00
Jarrod Flesch
6044f810bd docs: correct PULL_REQUEST_TEMPLATE.md links 2023-11-21 08:37:57 -05:00
Elliot DeNolf
e68ca9363f fix(plugin-cloud-storage): adjust webpack aliasing for pnpm (#4228) 2023-11-20 16:53:30 -05:00
Elliot DeNolf
9963b8d945 chore(release): plugin-nested-docs/1.0.9 [skip ci] 2023-11-20 16:40:46 -05:00
Elliot DeNolf
9afb838182 chore(release): richtext-slate/1.2.0 [skip ci] 2023-11-20 16:39:38 -05:00
Elliot DeNolf
2dad129022 chore(release): richtext-lexical/0.2.0 [skip ci] 2023-11-20 16:39:20 -05:00
Elliot DeNolf
6af1c4d45d chore(release): payload/2.2.0 [skip ci] 2023-11-20 16:36:41 -05:00
Dan Ribbens
4e41dd1bf2 fix(plugin-nested-docs): await populate breadcrumbs on resaveChildren (#4226) 2023-11-20 16:32:02 -05:00
Dan Ribbens
de02490231 feat: hide publish button based on permissions (#4203)
Co-authored-by: James <james@trbl.design>
2023-11-20 16:26:49 -05:00
Take Weiland
1510baf46e fix: synchronous transaction errors (#4164)
Co-authored-by: Dan Ribbens <dan.ribbens@gmail.com>
2023-11-20 12:20:42 -05:00
Alessio Gravili
c10db332cd docs(richtext-lexical): remove unnecessary await from createHeadlessEditor (#4213) 2023-11-19 14:40:09 +01:00
Alessio Gravili
0af9c4d398 fix(richtext-lexical): Blocks: Array row data is not removed (#4209)
* chore(richtext-lexical): Add failing test which reproduces issue

* fix(richtext-lexical): fix the actual issue
2023-11-18 14:01:57 +01:00
Alessio Gravili
fc137c0f53 Merge pull request #4210 from payloadcms/chore/upgrade-lexical
chore(richtext-lexical): Upgrade lexical from 0.12.2 to 0.12.4 and port some playground changes over
2023-11-18 12:59:24 +01:00
Alessio Gravili
a24f2be4a6 chore(richtext-lexical): backport setFloatingElemPositionForLinkEditor type change from playground 2023-11-18 12:54:40 +01:00
Alessio Gravili
57999adfe2 chore(richtext-lexical): backport autolink changes from lexical playground 2023-11-18 12:51:08 +01:00
Alessio Gravili
7857043d03 chore(richtext-lexical): Upgrade lexical packages 2023-11-18 12:37:28 +01:00
Alessio Gravili
3b93af734b Merge pull request #4207 from payloadcms/fix/lexical-validations-
fix(richtext-lexical): Blocks: fields without fulfilled condition are now skipped for validation
2023-11-18 11:55:41 +01:00
Alessio Gravili
50fab902bd fix(richtext-lexical): Blocks: fields without fulfilled condition are now skipped for validation 2023-11-18 11:52:30 +01:00
Alessio Gravili
724d80b7f4 Merge pull request #4196 from payloadcms/docs/lexical-editorconfig
docs(richtext-lexical): various documentation improvements
2023-11-17 18:39:11 +01:00
Alessio Gravili
b406e6afb9 docs(richtext-lexical): various documentation improvements 2023-11-17 18:38:09 +01:00
Alessio Gravili
3f46b21eb2 Merge pull request #4192 from payloadcms/feat/lexical-top
feat(richtext-lexical): Add new position: 'top' property for plugins
2023-11-17 18:20:29 +01:00
Alessio Gravili
eed4f4361c feat(richtext-lexical): Add new position: 'top' property for plugins 2023-11-17 18:18:54 +01:00
Jarrod Flesch
05f3169a75 fix: thread locale through to access routes from admin panel (#4183) 2023-11-17 10:15:12 -05:00
Alessio Gravili
94f1443ce4 Merge pull request #4176 from payloadcms/fix/missing-use-client
fix(richtext-lexical): add missing 'use client' to TestRecorder
2023-11-16 22:38:52 +01:00
Alessio Gravili
fc26275b7a fix(richtext-lexical): add missing 'use client' to TestRecorder feature plugin 2023-11-16 22:37:37 +01:00
Alessio Gravili
e57f5e2aa0 Merge pull request #4175 from payloadcms/fix/lexical-html-globals
fix(richtext-lexical): make lexicalHTML() function work for globals
2023-11-16 22:11:54 +01:00
Alessio Gravili
dbfc83520c fix(richtext-lexical): make lexicalHTML() function work for globals 2023-11-16 22:10:00 +01:00
Alessio Gravili
c068a8784e fix(richtext-lexical): Blocks: make sure fields are wrapped in a uniquely-named group, change block node data format, fix react key error (#3995)
* fix(richtext-lexical): make sure block fields are wrapped in a uniquely-named group

* chore: remove redundant hook

* chore(richtext-lexical): attempt to fix unnecessary unsaved changes warning regression

* cleanup everything

* chore: more cleanup

* debug

* looks like properly cloning the formdata for setting initial state fixes the issue where the old formdata is updated even if node.setFields is not called

* chore: fix e2e tests

* chore: fix e2e tests (a selector has changed)

* chore: fix int tests (due to new blocks data format)

* chore: fix incorrect insert block commands in drawer

* chore: add new e2e test

* chore: fail e2e tests when there are browser console errors

* fix(breaking): beforeInput and afterInput: fix missing key errors, consistent typing and cases in name
2023-11-16 22:01:04 +01:00
Alessio Gravili
989c10e0e0 feat: allow richtext adapters to control type generation, improve generated lexical types (#4036) 2023-11-16 11:36:20 -05:00
Alessio Gravili
3bf2b7a3fe Merge pull request #4171 from zakinadhif/main
fix(richtext-lexical): visual bug after rearranging blocks
2023-11-16 17:23:41 +01:00
Zaki Nadhif
a6b486007d fix(richtext-lexical): visual bug after rearranging blocks 2023-11-16 22:38:34 +07:00
Wilson
4e03ee7079 chore: adds doc blocks for field access properties (#3973) 2023-11-16 09:15:04 -05:00
Quentin Beauperin
b91711a74a fix: improves live preview breakpoints and zoom options in dark mode (#4090) 2023-11-16 09:10:33 -05:00
Jonathan Wu
191c13a409 chore(examples/form-builder): improve form input accessibility (#4166) 2023-11-16 08:02:15 -05:00
Alessio Gravili
b210af4696 fix(richtext-lexical): incorrect caret positioning when selecting second line of multi-line paragraph (#4165) 2023-11-15 22:22:42 +01:00
Taís Massaro
8cebd2ccce docs: correct useTableColumns react import path in example (#4150) 2023-11-15 08:20:42 -05:00
Take Weiland
195a952c43 fix: transactionID isolation for GraphQL (#4095) 2023-11-14 16:07:10 -05:00
Alessio Gravili
4bc5fa7086 chore(richtext-lexical): remove unused defaultValue prop in RichText component (#4146) 2023-11-14 18:31:04 +01:00
Alessio Gravili
2c8d34d2aa fix(richtext-lexical): remove optional chaining after this as transpilers are not handling it well (#4145) 2023-11-14 18:21:51 +01:00
Alessio Gravili
4ec5643dd7 chore: restricts character length in table cells (#4063) 2023-11-14 11:25:24 -05:00
Jessica Chowdhury
45e9a559bb fix: upload fit not accounted for when editing focal point or crop (#4142)
Co-authored-by: Jarrod Flesch <jarrodmflesch@gmail.com>
2023-11-14 11:19:23 -05:00
Jessica Chowdhury
d6233cbf42 templates: update fetch request in populateArchiveBlock hook (#4127) 2023-11-14 11:16:16 -05:00
Jessica Chowdhury
ad3e23b345 chore: updates translation key in dropzone component (#4135) 2023-11-14 11:09:09 -05:00
Radosław Kłos
782e118569 fix(i18n): polish translations (#4134) 2023-11-14 11:06:59 -05:00
Jessica Chowdhury
dbfe4af993 chore: corrects block name overflow in UI (#4138) 2023-11-14 10:56:35 -05:00
Alessio Gravili
859c2f4a6d fix(richtext-lexical): nested editor may lose focus when writing (#4139) 2023-11-14 15:42:30 +01:00
Elliot DeNolf
a34d0f8274 fix(templates): yarn v1 workaround (#4125) 2023-11-13 11:53:57 -05:00
Jessica Chowdhury
967eff1aab fix: rename tab button classname to prevent unintentional styling (#4121) 2023-11-13 08:39:16 -05:00
Alessio Gravili
b7041d6ab1 chore(richtext-lexical): New e2e test: should allow adding new blocks to a sub-blocks field, part of a parent lexical blocks field (#4114) 2023-11-12 23:48:13 +01:00
Alessio Gravili
78b7bd62cd Merge pull request #4113 from payloadcms/fix/4025
fix(richtext-lexical): Blocks: z-index and floating select menu button click issues
2023-11-12 23:17:53 +01:00
Alessio Gravili
7329b1babd chore(richtext-lexical): Remove unnecessary console.log 2023-11-12 23:17:33 +01:00
Alessio Gravili
c87969b7f9 chore(richtext-lexical): Add e2e test: 'ensure slash menu is not hidden behind other blocks' 2023-11-12 23:12:24 +01:00
Alessio Gravili
09f17f4450 fix(richtext-lexical): Blocks: z-index issue, e.g. select field dropdown in blocks hidden behind blocks below, or slash menu inside nested editor hidden behind blocks below 2023-11-12 22:28:05 +01:00
Alessio Gravili
615702b858 fix(richtext-lexical): Floating Select Toolbar: Buttons and Dropdown Buttons not clickable in nested editors
Fixes #4025
2023-11-12 22:09:36 +01:00
Alessio Gravili
f0642ce031 chore(richtext-lexical): add a bunch of e2e tests, including a failing one 2023-11-12 21:44:18 +01:00
Alessio Gravili
56db87d2ec Merge pull request #4104 from payloadcms/chore/console-log-remove
chore: remove unnecessary console.log
2023-11-11 12:58:54 +01:00
Alessio Gravili
45c42724a4 chore: remove unnecessary console.log 2023-11-11 12:57:22 +01:00
Alessio Gravili
a6d5f2e3de fix(richtext-lexical): HTMLConverter: cannot find nested lexical fields (#4103)
Fixes #4034
2023-11-11 12:54:33 +01:00
Elliot Lintz
73b8549ef5 chore: fix readme badge link styles (#4101) 2023-11-10 17:37:50 -05:00
Jarrod Flesch
e22b95bdf3 fix: fully define the define property for esbuild string replacement (#4099) 2023-11-10 13:47:14 -05:00
Jessica Chowdhury
56ddd2c388 chore: update date field schema (#4098) 2023-11-10 12:25:03 -05:00
Jessica Chowdhury
803a37eaa9 fix: simplifies block/array/hasMany-number field validations (#4052)
Co-authored-by: Jarrod Flesch <jarrodmflesch@gmail.com>
2023-11-10 12:06:21 -05:00
Elliot DeNolf
d308bb3421 chore(release): richtext-lexical/0.1.17 [skip ci] 2023-11-10 10:42:33 -05:00
Elliot DeNolf
cbc4752ecb chore(release): plugin-sentry/0.0.6 [skip ci] 2023-11-10 10:42:26 -05:00
Elliot DeNolf
c51f9d01cb chore(release): live-preview-react/0.1.6 [skip ci] 2023-11-10 10:42:18 -05:00
176 changed files with 4070 additions and 1145 deletions

View File

@@ -12,8 +12,8 @@
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] Change to the [templates](../templates/) directory (does not affect core functionality)
- [ ] Change to the [examples](../examples/) directory (does not affect core functionality)
- [ ] Change to the [templates](https://github.com/payloadcms/payload/tree/main/templates) directory (does not affect core functionality)
- [ ] Change to the [examples](https://github.com/payloadcms/payload/tree/main/examples) directory (does not affect core functionality)
- [ ] This change requires a documentation update
## Checklist:

View File

@@ -1,3 +1,45 @@
## [2.2.1](https://github.com/payloadcms/payload/compare/v2.2.0...v2.2.1) (2023-11-21)
### Bug Fixes
* make outputSchema optional on richtext config ([#4230](https://github.com/payloadcms/payload/issues/4230)) ([3a784a0](https://github.com/payloadcms/payload/commit/3a784a06cc6c42c96b8d6cf023d942e6661be7b5))
## [2.2.0](https://github.com/payloadcms/payload/compare/v2.1.1...v2.2.0) (2023-11-20)
### Features
* allow richtext adapters to control type generation, improve generated lexical types ([#4036](https://github.com/payloadcms/payload/issues/4036)) ([989c10e](https://github.com/payloadcms/payload/commit/989c10e0e0b36a8c34822263b19f5cb4b9ed6e72))
* hide publish button based on permissions ([#4203](https://github.com/payloadcms/payload/issues/4203)) ([de02490](https://github.com/payloadcms/payload/commit/de02490231fbc8936973c1b81ac87add39878d8b))
* **richtext-lexical:** Add new position: 'top' property for plugins ([eed4f43](https://github.com/payloadcms/payload/commit/eed4f4361cd012adf4e777820adbe7ad330ffef6))
### Bug Fixes
* fully define the define property for esbuild string replacement ([#4099](https://github.com/payloadcms/payload/issues/4099)) ([e22b95b](https://github.com/payloadcms/payload/commit/e22b95bdf3b2911ae67a07a76ec109c76416ea56))
* **i18n:** polish translations ([#4134](https://github.com/payloadcms/payload/issues/4134)) ([782e118](https://github.com/payloadcms/payload/commit/782e1185698abb2fff3556052fd16d2b725611b9))
* improves live preview breakpoints and zoom options in dark mode ([#4090](https://github.com/payloadcms/payload/issues/4090)) ([b91711a](https://github.com/payloadcms/payload/commit/b91711a74ad9379ed820b6675060209626b1c2d0))
* **plugin-nested-docs:** await populate breadcrumbs on resaveChildren ([#4226](https://github.com/payloadcms/payload/issues/4226)) ([4e41dd1](https://github.com/payloadcms/payload/commit/4e41dd1bf2706001fa03130adb1c69403795ac96))
* rename tab button classname to prevent unintentional styling ([#4121](https://github.com/payloadcms/payload/issues/4121)) ([967eff1](https://github.com/payloadcms/payload/commit/967eff1aabcc9ba7f29573fc2706538d691edfdd))
* **richtext-lexical:** add missing 'use client' to TestRecorder feature plugin ([fc26275](https://github.com/payloadcms/payload/commit/fc26275b7a85fd34f424f7693b8383ad4efe0121))
* **richtext-lexical:** Blocks: Array row data is not removed ([#4209](https://github.com/payloadcms/payload/issues/4209)) ([0af9c4d](https://github.com/payloadcms/payload/commit/0af9c4d3985a6c46a071ef5ac28c8359cb320571))
* **richtext-lexical:** Blocks: fields without fulfilled condition are now skipped for validation ([50fab90](https://github.com/payloadcms/payload/commit/50fab902bd7baa1702ae0d995b4f58c1f5fca374))
* **richtext-lexical:** Blocks: make sure fields are wrapped in a uniquely-named group, change block node data format, fix react key error ([#3995](https://github.com/payloadcms/payload/issues/3995)) ([c068a87](https://github.com/payloadcms/payload/commit/c068a8784ec5780dbdca5416b25ba654afd05458))
* **richtext-lexical:** Blocks: z-index issue, e.g. select field dropdown in blocks hidden behind blocks below, or slash menu inside nested editor hidden behind blocks below ([09f17f4](https://github.com/payloadcms/payload/commit/09f17f44508539cfcb8722f7f462ef40d9ed54fd))
* **richtext-lexical:** Floating Select Toolbar: Buttons and Dropdown Buttons not clickable in nested editors ([615702b](https://github.com/payloadcms/payload/commit/615702b858e76994a174159cb69f034ef811e016)), closes [#4025](https://github.com/payloadcms/payload/issues/4025)
* **richtext-lexical:** HTMLConverter: cannot find nested lexical fields ([#4103](https://github.com/payloadcms/payload/issues/4103)) ([a6d5f2e](https://github.com/payloadcms/payload/commit/a6d5f2e3dea178e1fbde90c0d6a5ce254a8db0d1)), closes [#4034](https://github.com/payloadcms/payload/issues/4034)
* **richtext-lexical:** incorrect caret positioning when selecting second line of multi-line paragraph ([#4165](https://github.com/payloadcms/payload/issues/4165)) ([b210af4](https://github.com/payloadcms/payload/commit/b210af46968b77d96ffd6ef60adc3b8d8bdc9376))
* **richtext-lexical:** make lexicalHTML() function work for globals ([dbfc835](https://github.com/payloadcms/payload/commit/dbfc83520ca8b5e55198a3c4b517ae3a80f9cac6))
* **richtext-lexical:** nested editor may lose focus when writing ([#4139](https://github.com/payloadcms/payload/issues/4139)) ([859c2f4](https://github.com/payloadcms/payload/commit/859c2f4a6d299a42e572133502b3841a74a11002))
* **richtext-lexical:** remove optional chaining after `this` as transpilers are not handling it well ([#4145](https://github.com/payloadcms/payload/issues/4145)) ([2c8d34d](https://github.com/payloadcms/payload/commit/2c8d34d2aadf2fcaf0655c0abef233f341d9945f))
* **richtext-lexical:** visual bug after rearranging blocks ([a6b4860](https://github.com/payloadcms/payload/commit/a6b486007dc26195adc5d576d937e35471c2868f))
* simplifies block/array/hasMany-number field validations ([#4052](https://github.com/payloadcms/payload/issues/4052)) ([803a37e](https://github.com/payloadcms/payload/commit/803a37eaa947397fa0a93b9f4f7d702c6b94ceaa))
* synchronous transaction errors ([#4164](https://github.com/payloadcms/payload/issues/4164)) ([1510baf](https://github.com/payloadcms/payload/commit/1510baf46e33540c72784f2d3f98330a8ff90923))
* thread locale through to access routes from admin panel ([#4183](https://github.com/payloadcms/payload/issues/4183)) ([05f3169](https://github.com/payloadcms/payload/commit/05f3169a75b3b62962e7fe7842fbb6df6699433d))
* transactionID isolation for GraphQL ([#4095](https://github.com/payloadcms/payload/issues/4095)) ([195a952](https://github.com/payloadcms/payload/commit/195a952c4314e0d53fd579517035373b49d6ccae))
* upload fit not accounted for when editing focal point or crop ([#4142](https://github.com/payloadcms/payload/issues/4142)) ([45e9a55](https://github.com/payloadcms/payload/commit/45e9a559bbb16b2171465c8a439044011cebf102))
## [2.1.1](https://github.com/payloadcms/payload/compare/v2.1.0...v2.1.1) (2023-11-10)

View File

@@ -1,24 +1,14 @@
<a href="https://payloadcms.com">
<img width="100%" src="https://github.com/payloadcms/payload/blob/main/packages/payload/src/admin/assets/images/github-banner-alt.jpg?raw=true" alt="Payload headless CMS Admin panel built with React" />
</a>
<a href="https://payloadcms.com"><img width="100%" src="https://github.com/payloadcms/payload/blob/main/packages/payload/src/admin/assets/images/github-banner-alt.jpg?raw=true" alt="Payload headless CMS Admin panel built with React" /></a>
<br />
<br />
<p align="left">
<a href="https://github.com/payloadcms/payload/actions">
<img alt="GitHub Workflow Status" src="https://img.shields.io/github/actions/workflow/status/payloadcms/payload/main.yml?style=flat-square">
</a>
<a href="https://github.com/payloadcms/payload/actions"><img alt="GitHub Workflow Status" src="https://img.shields.io/github/actions/workflow/status/payloadcms/payload/main.yml?style=flat-square"></a>
&nbsp;
<a href="https://discord.gg/payload">
<img alt="Discord" src="https://img.shields.io/discord/967097582721572934?label=Discord&color=7289da&style=flat-square" />
</a>
<a href="https://discord.gg/payload"><img alt="Discord" src="https://img.shields.io/discord/967097582721572934?label=Discord&color=7289da&style=flat-square" /></a>
&nbsp;
<a href="https://www.npmjs.com/package/payload">
<img alt="npm" src="https://img.shields.io/npm/v/payload?style=flat-square" />
</a>
<a href="https://www.npmjs.com/package/payload"><img alt="npm" src="https://img.shields.io/npm/v/payload?style=flat-square" /></a>
&nbsp;
<a href="https://twitter.com/payloadcms">
<img src="https://img.shields.io/badge/follow-payloadcms-1DA1F2?logo=twitter&style=flat-square" alt="Payload Twitter" />
</a>
<a href="https://twitter.com/payloadcms"><img src="https://img.shields.io/badge/follow-payloadcms-1DA1F2?logo=twitter&style=flat-square" alt="Payload Twitter" /></a>
</p>
<hr/>
<h4>

View File

@@ -432,14 +432,14 @@ All Payload fields support the ability to swap in your own React components. So,
| **`Cell`** | Used in the `List` view's table to represent a table-based preview of the data stored in the field. [More](#cell-component) |
| **`Field`** | Swap out the field itself within all `Edit` views. [More](#field-component) |
As an alternative to replacing the entire Field component, you may want to keep the majority of the default Field component and only swap components within. This allows you to replace the **`Label`** or **`Error`** within a field component or add additional components inside the field with **`BeforeInput`** or **`AfterInput`**. **`BeforeInput`** and **`AfterInput`** are allowed in any fields that don't contain other fields, except [UI](/docs/fields/ui) and [Rich Text](/docs/fields/rich-text).
As an alternative to replacing the entire Field component, you may want to keep the majority of the default Field component and only swap components within. This allows you to replace the **`Label`** or **`Error`** within a field component or add additional components inside the field with **`beforeInput`** or **`afterInput`**. **`beforeInput`** and **`afterInput`** are allowed in any fields that don't contain other fields, except [UI](/docs/fields/ui) and [Rich Text](/docs/fields/rich-text).
| Component | Description |
| ----------------- | --------------------------------------------------------------------------------------------------------------- |
| **`Label`** | Override the default Label in the Field Component. [More](#label-component) |
| **`Error`** | Override the default Label in the Field Component. [More](#error-component) |
| **`BeforeInput`** | An array of elements that will be added before `input`/`textarea` elements. [More](#afterinput-and-beforeinput) |
| **`AfterInput`** | An array of elements that will be added after `input`/`textarea` elements. [More](#afterinput-and-beforeinput) |
| **`beforeInput`** | An array of elements that will be added before `input`/`textarea` elements. [More](#afterinput-and-beforeinput) |
| **`afterInput`** | An array of elements that will be added after `input`/`textarea` elements. [More](#afterinput-and-beforeinput) |
## Cell Component
@@ -530,7 +530,7 @@ const CustomLabel: React.FC<Props> = (props) => {
{getTranslation(label, i18n)}
{required && <span className="required">*</span>}
</span>);
}
}
return null
}
@@ -564,7 +564,7 @@ const CustomError: React.FC<Props> = (props) => {
}
```
## AfterInput and BeforeInput
## afterInput and beforeInput
With these properties you can add multiple components before and after the input element. For example, you can add an absolutely positioned button to clear the current field value.
@@ -583,9 +583,7 @@ const fieldField: Field = {
type: 'text',
admin: {
components: {
AfterInput: [
<ClearButton />
]
afterInput: [ClearButton]
}
}
}

View File

@@ -172,20 +172,34 @@ export default buildConfig({
collections: [Subscriptions],
admin: {
bundler: viteBundler(),
vite: (config) => {
return {
...config,
resolve: {
...config.resolve,
// highlight-start
alias: {
...config.resolve.alias,
// remember, vite aliases are exact-match only
'./hooks/createStripeSubscription': mockModulePath,
},
// highlight-end
},
vite: (incomingViteConfig) => {
const existingAliases = incomingViteConfig?.resolve?.alias || {};
let aliasArray: { find: string | RegExp; replacement: string; }[] = [];
// Pass the existing Vite aliases
if (Array.isArray(existingAliases)) {
aliasArray = existingAliases;
} else {
aliasArray = Object.values(existingAliases);
}
// highlight-start
// Add your own aliases using the find and replacement keys
// remember, vite aliases are exact-match only
aliasArray.push({
find: '../server-only-module',
replacement: path.resolve(__dirname, './path/to/browser-safe-module.js')
});
// highlight-end
return {
...incomingViteConfig,
resolve: {
...(incomingViteConfig?.resolve || {}),
alias: aliasArray,
}
};
},
},
})

View File

@@ -764,7 +764,7 @@ Returns methods to set and get user preferences. More info can be found [here](h
Returns methods to manipulate table columns
```tsx
import { useTableColumns } from 'payload/components/utilities'
import { useTableColumns } from 'payload/components/hooks'
const MyComponent: React.FC = () => {
// highlight-start

View File

@@ -60,17 +60,33 @@ export const buildConfig({
collections: [],
admin: {
bundler: viteBundler(),
vite: (incomingViteConfig) => ({
...incomingViteConfig,
resolve: {
...(incomingViteConfig?.resolve || {}),
alias: {
...(incomingViteConfig?.resolve?.alias || {}),
'../server-only-module': path.resolve(__dirname, './path/to/browser-safe-module.js'),
'../../server-only-module': path.resolve(__dirname, './path/to/browser-safe-module.js'),
}
vite: (incomingViteConfig) => {
const existingAliases = incomingViteConfig?.resolve?.alias || {};
let aliasArray: { find: string | RegExp; replacement: string; }[] = [];
// Pass the existing Vite aliases
if (Array.isArray(existingAliases)) {
aliasArray = existingAliases;
} else {
aliasArray = Object.values(existingAliases);
}
})
// Add your own aliases using the find and replacement keys
aliasArray.push({
find: '../server-only-module',
replacement: path.resolve(__dirname, './path/to/browser-safe-module.js')
find: '../../server-only-module',
replacement: path.resolve(__dirname, './path/to/browser-safe-module.js')
});
return {
...incomingViteConfig,
resolve: {
...(incomingViteConfig?.resolve || {}),
alias: aliasArray,
}
};
},
}
})
```
@@ -90,7 +106,8 @@ That plugin should create an alias to support Vite as follows:
```ts
{
// aliases go here
'payload-plugin-cool': path.resolve(__dirname, './my-admin-plugin.js')
find: 'payload-plugin-cool',
replacement: path.resolve(__dirname, './my-admin-plugin.js')
}
```
@@ -108,22 +125,36 @@ export const buildConfig({
collections: [],
admin: {
bundler: viteBundler(),
vite: (incomingViteConfig) => ({
...incomingViteConfig,
resolve: {
...(incomingViteConfig?.resolve || {}),
alias: {
...(incomingViteConfig?.resolve?.alias || {}),
// custom aliases go here
'../server-only-module': path.resolve(__dirname, './path/to/browser-safe-module.js'),
}
vite: (incomingViteConfig) => {
const existingAliases = incomingViteConfig?.resolve?.alias || {};
let aliasArray: { find: string | RegExp; replacement: string; }[] = [];
// Pass the existing Vite aliases
if (Array.isArray(existingAliases)) {
aliasArray = existingAliases;
} else {
aliasArray = Object.values(existingAliases);
}
})
// Add your own aliases using the find and replacement keys
aliasArray.push({
find: '../server-only-module',
replacement: path.resolve(__dirname, './path/to/browser-safe-module.js')
});
return {
...incomingViteConfig,
resolve: {
...(incomingViteConfig?.resolve || {}),
alias: aliasArray,
}
};
},
}
})
```
Learn more about [aliasing server-only modules](http://localhost:3000/docs/admin/excluding-server-code#aliasing-server-only-modules).
Learn more about [aliasing server-only modules](https://payloadcms.com/docs/admin/excluding-server-code#aliasing-server-only-modules).
Even though there is a new property for Vite configs specifically, we have implemented some "compatibility" between Webpack and Vite out-of-the-box.

View File

@@ -55,6 +55,7 @@ In addition to the default [field admin config](/docs/fields/overview#admin-conf
| **`date.maxDate`** \* | Max date value to allow. |
| **`date.minTime`** \* | Min time value to allow. |
| **`date.maxTime`** \* | Max date value to allow. |
| **`date.overrides`** \* | Pass any valid props directly to the [react-datepicker](https://github.com/Hacker0x01/react-datepicker/blob/master/docs/datepicker.md) |
| **`date.timeIntervals`** \* | Time intervals to display. Defaults to 30 minutes. |
| **`date.timeFormat`** \* | Determines time format. Defaults to `'h:mm aa'`. |

View File

@@ -314,13 +314,31 @@ import {
const yourEditorConfig; // <= your editor config here
const headlessEditor = await createHeadlessEditor({
const headlessEditor = createHeadlessEditor({
nodes: getEnabledNodes({
editorConfig: sanitizeEditorConfig(yourEditorConfig),
}),
})
```
### Getting the editor config
As you can see, you need to provide an editor config in order to create a headless editor. This is because the editor config is used to determine which nodes & features are enabled, and which converters are used.
To get the editor config, simply import the default editor config and adjust it - just like you did inside of the `editor: lexicalEditor({})` property:
```ts
import { defaultEditorConfig, defaultEditorFeatures } from '@payloadcms/richtext-lexical' // <= make sure this package is installed
const yourEditorConfig = defaultEditorConfig
// If you made changes to the features of the field's editor config, you should also make those changes here:
yourEditorConfig.features = [
...defaultEditorFeatures,
// Add your custom features here
]
```
### HTML => Lexical
Once you have your headless editor instance, you can use it to convert HTML to Lexical:
@@ -328,13 +346,14 @@ Once you have your headless editor instance, you can use it to convert HTML to L
```ts
import { $generateNodesFromDOM } from '@lexical/html'
import { $getRoot,$getSelection } from 'lexical'
import JSDOM from 'jsdom'
headlessEditor.update(() => {
// In a headless environment you can use a package such as JSDom to parse the HTML string.
const dom = new JSDOM(htmlString)
// Once you have the DOM instance it's easy to generate LexicalNodes.
const nodes = $generateNodesFromDOM(editor, dom.window.document)
const nodes = $generateNodesFromDOM(headlessEditor, dom.window.document)
// Select the root
$getRoot().select()
@@ -348,6 +367,8 @@ headlessEditor.update(() => {
const editorJSON = headlessEditor.getEditorState().toJSON()
```
Functions prefixed with a `$` can only be run inside of an `editor.update()` or `editorState.read()` callback.
This has been taken from the [lexical serialization & deserialization docs](https://lexical.dev/docs/concepts/serialization#html---lexical).
<Banner type="success">
@@ -395,7 +416,6 @@ try {
headlessEditor.setEditorState(headlessEditor.parseEditorState(yourEditorState)) // This should commit the editor state immediately
} catch (e) {
logger.error({ err: e }, 'ERROR parsing editor state')
return ''
}
// Export to markdown
@@ -407,6 +427,35 @@ headlessEditor.getEditorState().read(() => {
The `.setEditorState()` function immediately updates your editor state. Thus, there's no need for the `discrete: true` flag when reading the state afterward.
### Lexical => Plain Text
Export content from the Lexical editor into plain text using these steps:
1. Import your current editor state into the headless editor.
2. Convert and fetch the resulting plain text string.
Here's the code for it:
```ts
import type { SerializedEditorState } from "lexical"
import { $getRoot } from "lexical"
const yourEditorState: SerializedEditorState // <= your current editor state here
// Import editor state into your headless editor
try {
headlessEditor.setEditorState(headlessEditor.parseEditorState(yourEditorState)) // This should commit the editor state immediately
} catch (e) {
logger.error({ err: e }, 'ERROR parsing editor state')
}
// Export to plain text
const plainTextContent = headlessEditor.getEditorState().read(() => {
return $getRoot().getTextContent()
}) || ''
```
## Migrating from Slate
While both Slate and Lexical save the editor state in JSON, the structure of the JSON is different.

View File

@@ -21,7 +21,7 @@ export const Country: React.FC<
return (
<Width width={width}>
<div className={classes.select}>
<label htmlFor="name" className={classes.label}>
<label htmlFor={name} className={classes.label}>
{label}
</label>
<Controller
@@ -37,6 +37,7 @@ export const Country: React.FC<
onChange={val => onChange(val.value)}
className={classes.reactSelect}
classNamePrefix="rs"
inputId={name}
/>
)}
/>

View File

@@ -19,13 +19,14 @@ export const Email: React.FC<
return (
<Width width={width}>
<div className={classes.wrap}>
<label htmlFor="name" className={classes.label}>
<label htmlFor={name} className={classes.label}>
{label}
</label>
<input
type="text"
placeholder="Email"
className={classes.input}
id={name}
{...register(name, { required: requiredFromProps, pattern: /^\S+@\S+$/i })}
/>
{requiredFromProps && errors[name] && <Error />}

View File

@@ -19,12 +19,13 @@ export const Number: React.FC<
return (
<Width width={width}>
<div className={classes.wrap}>
<label htmlFor="name" className={classes.label}>
<label htmlFor={name} className={classes.label}>
{label}
</label>
<input
type="number"
className={classes.input}
id={name}
{...register(name, { required: requiredFromProps })}
/>
{requiredFromProps && errors[name] && <Error />}

View File

@@ -20,7 +20,7 @@ export const Select: React.FC<
return (
<Width width={width}>
<div className={classes.select}>
<label htmlFor="name" className={classes.label}>
<label htmlFor={name} className={classes.label}>
{label}
</label>
<Controller
@@ -36,6 +36,7 @@ export const Select: React.FC<
onChange={val => onChange(val.value)}
className={classes.reactSelect}
classNamePrefix="rs"
inputId={name}
/>
)}
/>

View File

@@ -21,7 +21,7 @@ export const State: React.FC<
return (
<Width width={width}>
<div className={classes.select}>
<label htmlFor="name" className={classes.label}>
<label htmlFor={name} className={classes.label}>
{label}
</label>
<Controller
@@ -37,6 +37,7 @@ export const State: React.FC<
onChange={val => onChange(val.value)}
className={classes.reactSelect}
classNamePrefix="rs"
id={name}
/>
)}
/>

View File

@@ -19,12 +19,13 @@ export const Text: React.FC<
return (
<Width width={width}>
<div className={classes.wrap}>
<label htmlFor="name" className={classes.label}>
<label htmlFor={name} className={classes.label}>
{label}
</label>
<input
type="text"
className={classes.input}
id={name}
{...register(name, { required: requiredFromProps })}
/>
{requiredFromProps && errors[name] && <Error />}

View File

@@ -20,12 +20,13 @@ export const Textarea: React.FC<
return (
<Width width={width}>
<div className={classes.wrap}>
<label htmlFor="name" className={classes.label}>
<label htmlFor={name} className={classes.label}>
{label}
</label>
<textarea
rows={rows}
className={classes.textarea}
id={name}
{...register(name, { required: requiredFromProps })}
/>
{requiredFromProps && errors[name] && <Error />}

View File

@@ -77,6 +77,7 @@
"jest": "29.7.0",
"jest-environment-jsdom": "29.7.0",
"jwt-decode": "3.1.2",
"lexical": "0.12.2",
"lint-staged": "^14.0.1",
"minimist": "1.2.8",
"mongodb-memory-server": "8.12.2",

View File

@@ -69,6 +69,8 @@ export const getViteConfig = async (payloadConfig: SanitizedConfig): Promise<Inl
Object.entries(process.env).forEach(([key, val]) => {
if (key.indexOf('PAYLOAD_PUBLIC_') === 0) {
define[`process.env.${key}`] = `'${val}'`
} else {
define[`process.env.${key}`] = `''`
}
})

View File

@@ -1,23 +1,27 @@
import type { RollbackTransaction } from 'payload/database'
export const rollbackTransaction: RollbackTransaction = async function rollbackTransaction(
id = '',
) {
export const rollbackTransaction: RollbackTransaction = function rollbackTransaction(id = '') {
// if multiple operations are using the same transaction, the first will flow through and delete the session.
// subsequent calls should be ignored.
if (!this.sessions[id]) {
return
}
// when session exists but is not inTransaction something unexpected is happening to the session
// when session exists but inTransaction is false, it is no longer used and can be deleted
if (!this.sessions[id].inTransaction()) {
this.payload.logger.warn('rollbackTransaction called when no transaction exists')
delete this.sessions[id]
return
}
// the first call for rollback should be aborted and deleted causing any other operations with the same transaction to fail
await this.sessions[id].abortTransaction()
await this.sessions[id].endSession()
try {
// null coalesce needed when rollback is called multiple times with the same id synchronously
this.sessions?.[id].abortTransaction().then(() => {
// not supported by DocumentDB
this.sessions?.[id].endSession()
})
} catch (e) {
// no action needed
}
delete this.sessions[id]
}

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "payload",
"version": "2.1.1",
"version": "2.2.1",
"description": "Node, React and MongoDB Headless CMS and Application Framework",
"license": "MIT",
"main": "./dist/index.js",

View File

@@ -1,107 +1,109 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import Button from '../Button';
import React from 'react'
import { useTranslation } from 'react-i18next'
import Button from '../Button'
import './index.scss';
import './index.scss'
const handleDragOver = (e: DragEvent) => {
e.preventDefault();
e.stopPropagation();
};
e.preventDefault()
e.stopPropagation()
}
const baseClass = 'dropzone';
const baseClass = 'dropzone'
type Props = {
onChange: (e: FileList) => void;
className?: string;
mimeTypes?: string[];
onChange: (e: FileList) => void
className?: string
mimeTypes?: string[]
}
export const Dropzone: React.FC<Props> = ({ onChange, className, mimeTypes }) => {
const dropRef = React.useRef<HTMLDivElement>(null);
const [dragging, setDragging] = React.useState(false);
const inputRef = React.useRef(null);
const dropRef = React.useRef<HTMLDivElement>(null)
const [dragging, setDragging] = React.useState(false)
const inputRef = React.useRef(null)
const { t } = useTranslation(['upload', 'general']);
const { t } = useTranslation(['upload', 'general'])
const handlePaste = React.useCallback((e: ClipboardEvent) => {
e.preventDefault();
e.stopPropagation();
const handlePaste = React.useCallback(
(e: ClipboardEvent) => {
e.preventDefault()
e.stopPropagation()
if (e.clipboardData.files && e.clipboardData.files.length > 0) {
onChange(e.clipboardData.files);
}
}, [onChange]);
if (e.clipboardData.files && e.clipboardData.files.length > 0) {
onChange(e.clipboardData.files)
}
},
[onChange],
)
const handleDragEnter = React.useCallback((e: DragEvent) => {
e.preventDefault();
e.stopPropagation();
setDragging(true);
}, []);
e.preventDefault()
e.stopPropagation()
setDragging(true)
}, [])
const handleDragLeave = React.useCallback((e: DragEvent) => {
e.preventDefault();
e.stopPropagation();
setDragging(false);
}, []);
e.preventDefault()
e.stopPropagation()
setDragging(false)
}, [])
const handleDrop = React.useCallback((e: DragEvent) => {
e.preventDefault();
e.stopPropagation();
setDragging(false);
const handleDrop = React.useCallback(
(e: DragEvent) => {
e.preventDefault()
e.stopPropagation()
setDragging(false)
if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
onChange(e.dataTransfer.files);
setDragging(false);
if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
onChange(e.dataTransfer.files)
setDragging(false)
e.dataTransfer.clearData();
}
}, [onChange]);
e.dataTransfer.clearData()
}
},
[onChange],
)
const handleFileSelection = React.useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files && e.target.files.length > 0) {
onChange(e.target.files);
}
}, [onChange]);
const handleFileSelection = React.useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files && e.target.files.length > 0) {
onChange(e.target.files)
}
},
[onChange],
)
React.useEffect(() => {
const div = dropRef.current;
const div = dropRef.current
if (div) {
div.addEventListener('dragenter', handleDragEnter);
div.addEventListener('dragleave', handleDragLeave);
div.addEventListener('dragover', handleDragOver);
div.addEventListener('drop', handleDrop);
div.addEventListener('paste', handlePaste);
div.addEventListener('dragenter', handleDragEnter)
div.addEventListener('dragleave', handleDragLeave)
div.addEventListener('dragover', handleDragOver)
div.addEventListener('drop', handleDrop)
div.addEventListener('paste', handlePaste)
return () => {
div.removeEventListener('dragenter', handleDragEnter);
div.removeEventListener('dragleave', handleDragLeave);
div.removeEventListener('dragover', handleDragOver);
div.removeEventListener('drop', handleDrop);
div.removeEventListener('paste', handlePaste);
};
div.removeEventListener('dragenter', handleDragEnter)
div.removeEventListener('dragleave', handleDragLeave)
div.removeEventListener('dragover', handleDragOver)
div.removeEventListener('drop', handleDrop)
div.removeEventListener('paste', handlePaste)
}
}
return () => null;
}, [handleDragEnter, handleDragLeave, handleDrop, handlePaste]);
return () => null
}, [handleDragEnter, handleDragLeave, handleDrop, handlePaste])
const classes = [
baseClass,
className,
dragging ? 'dragging' : '',
].filter(Boolean).join(' ');
const classes = [baseClass, className, dragging ? 'dragging' : ''].filter(Boolean).join(' ')
return (
<div
ref={dropRef}
className={classes}
>
<div ref={dropRef} className={classes}>
<Button
size="small"
buttonStyle="secondary"
onClick={() => {
inputRef.current.click();
inputRef.current.click()
}}
className={`${baseClass}__file-button`}
>
@@ -117,10 +119,8 @@ export const Dropzone: React.FC<Props> = ({ onChange, className, mimeTypes }) =>
/>
<p className={`${baseClass}__label`}>
{t('or')}
{' '}
{t('dragAndDrop')}
{t('general:or')} {t('dragAndDrop')}
</p>
</div>
);
};
)
}

View File

@@ -1,9 +1,12 @@
import qs from 'qs'
import React, { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import { useForm, useFormModified } from '../../forms/Form/context'
import FormSubmit from '../../forms/Submit'
import { useConfig } from '../../utilities/Config'
import { useDocumentInfo } from '../../utilities/DocumentInfo'
import { useLocale } from '../../utilities/Locale'
import RenderCustomComponent from '../../utilities/RenderCustomComponent'
export type CustomPublishButtonProps = React.ComponentType<
@@ -12,6 +15,7 @@ export type CustomPublishButtonProps = React.ComponentType<
}
>
export type DefaultPublishButtonProps = {
canPublish: boolean
disabled: boolean
id?: string
label: string
@@ -19,10 +23,13 @@ export type DefaultPublishButtonProps = {
}
const DefaultPublishButton: React.FC<DefaultPublishButtonProps> = ({
id,
canPublish,
disabled,
label,
publish,
}) => {
if (!canPublish) return null
return (
<FormSubmit buttonId={id} disabled={disabled} onClick={publish} size="small" type="button">
{label}
@@ -35,22 +42,68 @@ type Props = {
}
export const Publish: React.FC<Props> = ({ CustomComponent }) => {
const { publishedDoc, unpublishedVersions } = useDocumentInfo()
const { submit } = useForm()
const { code } = useLocale()
const { id, collection, global, publishedDoc, unpublishedVersions } = useDocumentInfo()
const [hasPublishPermission, setHasPublishPermission] = React.useState(false)
const { getData, submit } = useForm()
const modified = useFormModified()
const {
routes: { api },
serverURL,
} = useConfig()
const { t } = useTranslation('version')
const hasNewerVersions = unpublishedVersions?.totalDocs > 0
const canPublish = modified || hasNewerVersions || !publishedDoc
const publish = useCallback(() => {
submit({
void submit({
overrides: {
_status: 'published',
},
})
}, [submit])
React.useEffect(() => {
const fetchPublishAccess = async () => {
let docAccessURL: string
let operation = 'update'
const params = {
locale: code || undefined,
}
if (global) {
docAccessURL = `/globals/${global.slug}/access`
} else if (collection) {
if (!id) operation = 'create'
docAccessURL = `/${collection.slug}/access${id ? `/${id}` : ''}`
}
if (docAccessURL) {
const data = getData()
const res = await fetch(`${serverURL}${api}${docAccessURL}?${qs.stringify(params)}`, {
body: JSON.stringify({
...data,
_status: 'published',
}),
credentials: 'include',
headers: {
'Content-Type': 'application/json',
},
method: 'post',
})
const json = await res.json()
const result = Boolean(json?.[operation]?.permission)
setHasPublishPermission(result)
} else {
setHasPublishPermission(true)
}
}
void fetchPublishAccess()
}, [api, code, collection, getData, global, id, serverURL])
return (
<RenderCustomComponent
CustomComponent={CustomComponent}
@@ -58,6 +111,7 @@ export const Publish: React.FC<Props> = ({ CustomComponent }) => {
componentProps={{
id: 'action-save',
DefaultButton: DefaultPublishButton,
canPublish: hasPublishPermission,
disabled: !canPublish,
label: t('publishChanges'),
publish,

View File

@@ -2,6 +2,7 @@
.section-title {
position: relative;
min-width: 0;
&:after {
display: block;

View File

@@ -10,10 +10,10 @@ import './index.scss'
const baseClass = 'checkbox-input'
type CheckboxInputProps = {
AfterInput?: React.ReactElement<any>[]
BeforeInput?: React.ReactElement<any>[]
Label?: React.ComponentType<LabelProps>
afterInput?: React.ComponentType<any>[]
'aria-label'?: string
beforeInput?: React.ComponentType<any>[]
checked?: boolean
className?: string
id?: string
@@ -30,10 +30,10 @@ export const CheckboxInput: React.FC<CheckboxInputProps> = (props) => {
const {
id,
name,
AfterInput,
BeforeInput,
Label,
afterInput,
'aria-label': ariaLabel,
beforeInput,
checked,
className,
inputRef,
@@ -58,7 +58,7 @@ export const CheckboxInput: React.FC<CheckboxInputProps> = (props) => {
.join(' ')}
>
<div className={`${baseClass}__input`}>
{BeforeInput}
{Array.isArray(beforeInput) && beforeInput.map((Component, i) => <Component key={i} />)}
<input
aria-label={ariaLabel}
defaultChecked={Boolean(checked)}
@@ -69,7 +69,7 @@ export const CheckboxInput: React.FC<CheckboxInputProps> = (props) => {
ref={inputRef}
type="checkbox"
/>
{AfterInput}
{Array.isArray(afterInput) && afterInput.map((Component, i) => <Component key={i} />)}
<span className={`${baseClass}__icon ${!partialChecked ? 'check' : 'partial'}`}>
{!partialChecked && <Check />}
{partialChecked && <Line />}

View File

@@ -20,12 +20,12 @@ const Checkbox: React.FC<Props> = (props) => {
name,
admin: {
className,
components: { Error, Label, afterInput, beforeInput } = {},
condition,
description,
readOnly,
style,
width,
components: { Error, Label, BeforeInput, AfterInput } = {},
} = {},
disableFormData,
label,
@@ -85,15 +85,15 @@ const Checkbox: React.FC<Props> = (props) => {
<ErrorComp alignCaret="left" message={errorMessage} showError={showError} />
</div>
<CheckboxInput
Label={Label}
afterInput={afterInput}
beforeInput={beforeInput}
checked={Boolean(value)}
id={fieldID}
label={getTranslation(label || name, i18n)}
name={path}
onToggle={onToggle}
readOnly={readOnly}
Label={Label}
BeforeInput={BeforeInput}
AfterInput={AfterInput}
required={required}
/>
<FieldDescription description={description} value={value} />

View File

@@ -9,8 +9,8 @@ import FieldDescription from '../../FieldDescription'
import DefaultLabel from '../../Label'
import useField from '../../useField'
import withCondition from '../../withCondition'
import './index.scss'
import { fieldBaseClass } from '../shared'
import './index.scss'
const prismToMonacoLanguageMap = {
js: 'javascript',
@@ -24,6 +24,7 @@ const Code: React.FC<Props> = (props) => {
name,
admin: {
className,
components: { Error, Label } = {},
condition,
description,
editorOptions,
@@ -31,7 +32,6 @@ const Code: React.FC<Props> = (props) => {
readOnly,
style,
width,
components: { Error, Label } = {},
} = {},
label,
path: pathFromProps,

View File

@@ -17,10 +17,10 @@ const baseClass = 'date-time-field'
export type DateTimeInputProps = Omit<DateField, 'admin' | 'name' | 'type'> & {
className?: string
components: {
AfterInput?: React.ReactElement<any>[]
BeforeInput?: React.ReactElement<any>[]
Error?: React.ComponentType<any>
Label?: React.ComponentType<any>
afterInput?: React.ComponentType<any>[]
beforeInput?: React.ComponentType<any>[]
}
datePickerProps?: DateField['admin']['date']
description?: Description
@@ -39,7 +39,7 @@ export type DateTimeInputProps = Omit<DateField, 'admin' | 'name' | 'type'> & {
export const DateTimeInput: React.FC<DateTimeInputProps> = (props) => {
const {
className,
components: { AfterInput, BeforeInput, Error, Label } = {},
components: { Error, Label, afterInput, beforeInput } = {},
datePickerProps,
description,
errorMessage,
@@ -81,7 +81,7 @@ export const DateTimeInput: React.FC<DateTimeInputProps> = (props) => {
</div>
<LabelComp htmlFor={path} label={label} required={required} />
<div className={`${baseClass}__input-wrapper`} id={`field-${path.replace(/\./g, '__')}`}>
{BeforeInput}
{Array.isArray(beforeInput) && beforeInput.map((Component, i) => <Component key={i} />)}
<DatePicker
{...datePickerProps}
onChange={onChange}
@@ -89,7 +89,7 @@ export const DateTimeInput: React.FC<DateTimeInputProps> = (props) => {
readOnly={readOnly}
value={value}
/>
{AfterInput}
{Array.isArray(afterInput) && afterInput.map((Component, i) => <Component key={i} />)}
</div>
<FieldDescription description={description} value={value} />
</div>

View File

@@ -10,8 +10,8 @@ import FieldDescription from '../../FieldDescription'
import DefaultLabel from '../../Label'
import useField from '../../useField'
import withCondition from '../../withCondition'
import './index.scss'
import { fieldBaseClass } from '../shared'
import './index.scss'
const Email: React.FC<Props> = (props) => {
const {
@@ -19,13 +19,13 @@ const Email: React.FC<Props> = (props) => {
admin: {
autoComplete,
className,
components: { Error, Label, afterInput, beforeInput } = {},
condition,
description,
placeholder,
readOnly,
style,
width,
components: { Error, Label, BeforeInput, AfterInput } = {},
} = {},
label,
path: pathFromProps,
@@ -68,7 +68,7 @@ const Email: React.FC<Props> = (props) => {
<ErrorComp message={errorMessage} showError={showError} />
<LabelComp htmlFor={`field-${path.replace(/\./g, '__')}`} label={label} required={required} />
<div className="input-wrapper">
{BeforeInput}
{Array.isArray(beforeInput) && beforeInput.map((Component, i) => <Component key={i} />)}
<input
autoComplete={autoComplete}
disabled={Boolean(readOnly)}
@@ -79,7 +79,7 @@ const Email: React.FC<Props> = (props) => {
type="email"
value={(value as string) || ''}
/>
{AfterInput}
{Array.isArray(afterInput) && afterInput.map((Component, i) => <Component key={i} />)}
</div>
<FieldDescription description={description} value={value} />
</div>

View File

@@ -13,14 +13,15 @@ import FieldDescription from '../../FieldDescription'
import DefaultLabel from '../../Label'
import useField from '../../useField'
import withCondition from '../../withCondition'
import './index.scss'
import { fieldBaseClass } from '../shared'
import './index.scss'
const NumberField: React.FC<Props> = (props) => {
const {
name,
admin: {
className,
components: { Error, Label, afterInput, beforeInput } = {},
condition,
description,
placeholder,
@@ -28,7 +29,6 @@ const NumberField: React.FC<Props> = (props) => {
step,
style,
width,
components: { Error, Label, BeforeInput, AfterInput } = {},
} = {},
hasMany,
label,
@@ -162,7 +162,7 @@ const NumberField: React.FC<Props> = (props) => {
/>
) : (
<div className="input-wrapper">
{BeforeInput}
{Array.isArray(beforeInput) && beforeInput.map((Component, i) => <Component key={i} />)}
<input
disabled={readOnly}
id={`field-${path.replace(/\./g, '__')}`}
@@ -178,7 +178,7 @@ const NumberField: React.FC<Props> = (props) => {
type="number"
value={typeof value === 'number' ? value : ''}
/>
{AfterInput}
{Array.isArray(afterInput) && afterInput.map((Component, i) => <Component key={i} />)}
</div>
)}

View File

@@ -10,8 +10,8 @@ import FieldDescription from '../../FieldDescription'
import DefaultLabel from '../../Label'
import useField from '../../useField'
import withCondition from '../../withCondition'
import './index.scss'
import { fieldBaseClass } from '../shared'
import './index.scss'
const baseClass = 'point'
@@ -20,6 +20,7 @@ const PointField: React.FC<Props> = (props) => {
name,
admin: {
className,
components: { Error, Label, afterInput, beforeInput } = {},
condition,
description,
placeholder,
@@ -27,7 +28,6 @@ const PointField: React.FC<Props> = (props) => {
step,
style,
width,
components: { Error, Label, BeforeInput, AfterInput } = {},
} = {},
label,
path: pathFromProps,
@@ -98,7 +98,7 @@ const PointField: React.FC<Props> = (props) => {
required={required}
/>
<div className="input-wrapper">
{BeforeInput}
{Array.isArray(beforeInput) && beforeInput.map((Component, i) => <Component key={i} />)}
<input
disabled={readOnly}
id={`field-longitude-${path.replace(/\./g, '__')}`}
@@ -109,7 +109,7 @@ const PointField: React.FC<Props> = (props) => {
type="number"
value={value && typeof value[0] === 'number' ? value[0] : ''}
/>
{AfterInput}
{Array.isArray(afterInput) && afterInput.map((Component, i) => <Component key={i} />)}
</div>
</li>
<li>
@@ -119,7 +119,7 @@ const PointField: React.FC<Props> = (props) => {
required={required}
/>
<div className="input-wrapper">
{BeforeInput}
{Array.isArray(beforeInput) && beforeInput.map((Component, i) => <Component key={i} />)}
<input
disabled={readOnly}
id={`field-latitude-${path.replace(/\./g, '__')}`}
@@ -130,7 +130,7 @@ const PointField: React.FC<Props> = (props) => {
type="number"
value={value && typeof value[1] === 'number' ? value[1] : ''}
/>
{AfterInput}
{Array.isArray(afterInput) && afterInput.map((Component, i) => <Component key={i} />)}
</div>
</li>
</ul>

View File

@@ -1,3 +1,5 @@
import type { JSONSchema4 } from 'json-schema'
import type { PayloadRequest } from '../../../../../express/types'
import type { RichTextField, Validate } from '../../../../../fields/config/types'
import type { CellComponentProps } from '../../../views/collections/List/Cell/types'
@@ -29,6 +31,13 @@ export type RichTextAdapter<
siblingDoc: Record<string, unknown>
}) => Promise<void> | null
outputSchema?: ({
field,
isRequired,
}: {
field: RichTextField<Value, AdapterProps, ExtraFieldProperties>
isRequired: boolean
}) => JSONSchema4
populationPromise?: (data: {
currentDepth?: number
depth: number

View File

@@ -166,7 +166,9 @@ const TabsField: React.FC<Props> = (props) => {
className={[
`${baseClass}__tab`,
activeTabConfig.label &&
`${baseClass}__tab-${toKebabCase(getTranslation(activeTabConfig.label, i18n))}`,
`${baseClass}__tabConfigLabel-${toKebabCase(
getTranslation(activeTabConfig.label, i18n),
)}`,
]
.filter(Boolean)
.join(' ')}

View File

@@ -14,6 +14,10 @@ import { fieldBaseClass } from '../shared'
import './index.scss'
export type TextInputProps = Omit<TextField, 'type'> & {
Error?: React.ComponentType<any>
Label?: React.ComponentType<any>
afterInput?: React.ComponentType<any>[]
beforeInput?: React.ComponentType<any>[]
className?: string
description?: Description
errorMessage?: string
@@ -29,14 +33,14 @@ export type TextInputProps = Omit<TextField, 'type'> & {
style?: React.CSSProperties
value?: string
width?: string
Error?: React.ComponentType<any>
Label?: React.ComponentType<any>
BeforeInput?: React.ReactElement<any>[]
AfterInput?: React.ReactElement<any>[]
}
const TextInput: React.FC<TextInputProps> = (props) => {
const {
Error,
Label,
afterInput,
beforeInput,
className,
description,
errorMessage,
@@ -53,10 +57,6 @@ const TextInput: React.FC<TextInputProps> = (props) => {
style,
value,
width,
Error,
Label,
BeforeInput,
AfterInput,
} = props
const { i18n } = useTranslation()
@@ -77,7 +77,7 @@ const TextInput: React.FC<TextInputProps> = (props) => {
<ErrorComp message={errorMessage} showError={showError} />
<LabelComp htmlFor={`field-${path.replace(/\./g, '__')}`} label={label} required={required} />
<div className="input-wrapper">
{BeforeInput}
{Array.isArray(beforeInput) && beforeInput.map((Component, i) => <Component key={i} />)}
<input
data-rtl={rtl}
disabled={readOnly}
@@ -90,7 +90,7 @@ const TextInput: React.FC<TextInputProps> = (props) => {
type="text"
value={value || ''}
/>
{AfterInput}
{Array.isArray(afterInput) && afterInput.map((Component, i) => <Component key={i} />)}
</div>
<FieldDescription
className={`field-description-${path.replace(/\./g, '__')}`}

View File

@@ -15,6 +15,7 @@ const Text: React.FC<Props> = (props) => {
name,
admin: {
className,
components: { Error, Label, afterInput, beforeInput } = {},
condition,
description,
placeholder,
@@ -22,7 +23,6 @@ const Text: React.FC<Props> = (props) => {
rtl,
style,
width,
components: { Error, Label, BeforeInput, AfterInput } = {},
} = {},
inputRef,
label,
@@ -60,6 +60,10 @@ const Text: React.FC<Props> = (props) => {
return (
<TextInput
Error={Error}
Label={Label}
afterInput={afterInput}
beforeInput={beforeInput}
className={className}
description={description}
errorMessage={errorMessage}
@@ -78,10 +82,6 @@ const Text: React.FC<Props> = (props) => {
style={style}
value={value}
width={width}
Error={Error}
Label={Label}
BeforeInput={BeforeInput}
AfterInput={AfterInput}
/>
)
}

View File

@@ -10,10 +10,14 @@ import { getTranslation } from '../../../../../utilities/getTranslation'
import DefaultError from '../../Error'
import FieldDescription from '../../FieldDescription'
import DefaultLabel from '../../Label'
import './index.scss'
import { fieldBaseClass } from '../shared'
import './index.scss'
export type TextAreaInputProps = Omit<TextareaField, 'type'> & {
Error?: React.ComponentType<any>
Label?: React.ComponentType<any>
afterInput?: React.ComponentType<any>[]
beforeInput?: React.ComponentType<any>[]
className?: string
description?: Description
errorMessage?: string
@@ -28,14 +32,14 @@ export type TextAreaInputProps = Omit<TextareaField, 'type'> & {
style?: React.CSSProperties
value?: string
width?: string
Error?: React.ComponentType<any>
Label?: React.ComponentType<any>
BeforeInput?: React.ReactElement<any>[]
AfterInput?: React.ReactElement<any>[]
}
const TextareaInput: React.FC<TextAreaInputProps> = (props) => {
const {
Error,
Label,
afterInput,
beforeInput,
className,
description,
errorMessage,
@@ -51,10 +55,6 @@ const TextareaInput: React.FC<TextAreaInputProps> = (props) => {
style,
value,
width,
Error,
Label,
BeforeInput,
AfterInput,
} = props
const { i18n } = useTranslation()
@@ -83,7 +83,7 @@ const TextareaInput: React.FC<TextAreaInputProps> = (props) => {
<label className="textarea-outer" htmlFor={`field-${path.replace(/\./g, '__')}`}>
<div className="textarea-inner">
<div className="textarea-clone" data-value={value || placeholder || ''} />
{BeforeInput}
{Array.isArray(beforeInput) && beforeInput.map((Component, i) => <Component key={i} />)}
<textarea
className="textarea-element"
data-rtl={rtl}
@@ -95,7 +95,7 @@ const TextareaInput: React.FC<TextAreaInputProps> = (props) => {
rows={rows}
value={value || ''}
/>
{AfterInput}
{Array.isArray(afterInput) && afterInput.map((Component, i) => <Component key={i} />)}
</div>
</label>
<FieldDescription description={description} value={value} />

View File

@@ -18,6 +18,7 @@ const Textarea: React.FC<Props> = (props) => {
name,
admin: {
className,
components: { Error, Label, afterInput, beforeInput } = {},
condition,
description,
placeholder,
@@ -26,7 +27,6 @@ const Textarea: React.FC<Props> = (props) => {
rtl,
style,
width,
components: { Error, Label, BeforeInput, AfterInput } = {},
} = {},
label,
localized,
@@ -65,6 +65,10 @@ const Textarea: React.FC<Props> = (props) => {
return (
<TextareaInput
Error={Error}
Label={Label}
afterInput={afterInput}
beforeInput={beforeInput}
className={className}
description={description}
errorMessage={errorMessage}
@@ -83,10 +87,6 @@ const Textarea: React.FC<Props> = (props) => {
style={style}
value={value as string}
width={width}
Error={Error}
Label={Label}
BeforeInput={BeforeInput}
AfterInput={AfterInput}
/>
)
}

View File

@@ -1,4 +1,5 @@
import { useModal } from '@faceless-ui/modal'
import qs from 'qs'
import React, { createContext, useCallback, useContext, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useHistory, useLocation } from 'react-router-dom'
@@ -10,6 +11,7 @@ import type { AuthContext } from './types'
import { requests } from '../../../api'
import useDebounce from '../../../hooks/useDebounce'
import { useConfig } from '../Config'
import { useLocale } from '../Locale'
const Context = createContext({} as AuthContext)
@@ -21,6 +23,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
const [tokenExpiration, setTokenExpiration] = useState<number>()
const { pathname } = useLocation()
const { push } = useHistory()
const { code } = useLocale()
const config = useConfig()
@@ -144,8 +147,11 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
}, [serverURL, api, userSlug, revokeTokenAndExpire])
const refreshPermissions = useCallback(async () => {
const params = {
locale: code,
}
try {
const request = await requests.get(`${serverURL}${api}/access`, {
const request = await requests.get(`${serverURL}${api}/access?${qs.stringify(params)}`, {
headers: {
'Accept-Language': i18n.language,
},
@@ -160,7 +166,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
} catch (e) {
toast.error(`Refreshing permissions failed: ${e.message}`)
}
}, [serverURL, api, i18n])
}, [serverURL, api, i18n, code])
const fetchFullUser = React.useCallback(async () => {
try {

View File

@@ -199,6 +199,9 @@ export const DocumentInfoProvider: React.FC<Props> = ({
const getDocPermissions = React.useCallback(async () => {
let docAccessURL: string
const params = {
locale: code || undefined,
}
if (pluralType === 'globals') {
docAccessURL = `/globals/${slug}/access`
} else if (pluralType === 'collections' && id) {
@@ -206,7 +209,7 @@ export const DocumentInfoProvider: React.FC<Props> = ({
}
if (docAccessURL) {
const res = await fetch(`${serverURL}${api}${docAccessURL}`, {
const res = await fetch(`${serverURL}${api}${docAccessURL}?${qs.stringify(params)}`, {
credentials: 'include',
headers: {
'Accept-Language': i18n.language,
@@ -219,7 +222,7 @@ export const DocumentInfoProvider: React.FC<Props> = ({
// (i.e. create has no id)
setDocPermissions(permissions[pluralType][slug])
}
}, [serverURL, api, pluralType, slug, id, permissions, i18n.language])
}, [serverURL, api, pluralType, slug, id, permissions, i18n.language, code])
const getDocPreferences = useCallback(async () => {
return getPreference<DocumentPreferences>(preferencesKey)

View File

@@ -51,4 +51,9 @@
justify-content: center;
padding: 6px 0;
}
.popup-button {
display: flex;
align-items: center;
}
}

View File

@@ -2,13 +2,19 @@ import React from 'react'
import type { EditViewProps } from '../../../types'
import { X } from '../../../..'
import { Chevron, Popup, X } from '../../../..'
import * as PopupList from '../../../../elements/Popup/PopupButtonList'
import { ExternalLinkIcon } from '../../../../graphics/ExternalLink'
import { useLivePreviewContext } from '../../Context/context'
import { PreviewFrameSizeInput } from '../SizeInput'
import './index.scss'
const baseClass = 'live-preview-toolbar-controls'
const zoomOptions = [50, 75, 100, 125, 150, 200]
const customOption = {
label: 'Custom', // TODO: Add i18n to this string
value: 'custom',
}
export const ToolbarControls: React.FC<EditViewProps> = () => {
const { breakpoint, breakpoints, setBreakpoint, setPreviewWindowType, setZoom, url, zoom } =
@@ -17,23 +23,51 @@ export const ToolbarControls: React.FC<EditViewProps> = () => {
return (
<div className={baseClass}>
{breakpoints?.length > 0 && (
<select
<Popup
className={`${baseClass}__breakpoint`}
name="live-preview-breakpoint"
onChange={(e) => setBreakpoint(e.target.value)}
value={breakpoint}
>
{breakpoints.map((bp) => (
<option key={bp.name} value={bp.name}>
{bp.label}
</option>
))}
{breakpoint === 'custom' && (
// Dynamically add this option so that it only appears when the width and height inputs are explicitly changed
// TODO: Translate this string
<option value="custom">Custom</option>
button={
<>
<span>
{breakpoints.find((bp) => bp.name == breakpoint)?.label ?? customOption.label}
</span>
&nbsp;
<Chevron className={`${baseClass}__chevron`} />
</>
}
render={({ close }) => (
<PopupList.ButtonGroup>
<React.Fragment>
{breakpoints.map((bp) => (
<PopupList.Button
key={bp.name}
active={bp.name == breakpoint}
onClick={() => {
setBreakpoint(bp.name)
close()
}}
>
{bp.label}
</PopupList.Button>
))}
{/* Dynamically add this option so that it only appears when the width and height inputs are explicitly changed */}
{breakpoint === 'custom' && (
<PopupList.Button
active={breakpoint == customOption.value}
onClick={() => {
setBreakpoint(customOption.value)
close()
}}
>
{customOption.label}
</PopupList.Button>
)}
</React.Fragment>
</PopupList.ButtonGroup>
)}
</select>
showScrollbar
verticalAlign="bottom"
horizontalAlign="right"
/>
)}
<div className={`${baseClass}__device-size`}>
<PreviewFrameSizeInput axis="x" />
@@ -42,18 +76,37 @@ export const ToolbarControls: React.FC<EditViewProps> = () => {
</span>
<PreviewFrameSizeInput axis="y" />
</div>
<select
<Popup
className={`${baseClass}__zoom`}
onChange={(e) => setZoom(Number(e.target.value) / 100)}
value={zoom * 100}
>
<option value={50}>50%</option>
<option value={75}>75%</option>
<option value={100}>100%</option>
<option value={125}>125%</option>
<option value={150}>150%</option>
<option value={200}>200%</option>
</select>
button={
<>
<span>{zoom * 100}%</span>
&nbsp;
<Chevron className={`${baseClass}__chevron`} />
</>
}
render={({ close }) => (
<PopupList.ButtonGroup>
<React.Fragment>
{zoomOptions.map((zoomValue) => (
<PopupList.Button
key={zoomValue}
active={zoom * 100 == zoomValue}
onClick={() => {
setZoom(zoomValue / 100)
close()
}}
>
{zoomValue}%
</PopupList.Button>
))}
</React.Fragment>
</PopupList.ButtonGroup>
)}
showScrollbar
verticalAlign="bottom"
horizontalAlign="right"
/>
<a
className={`${baseClass}__external`}
href={url}

View File

@@ -2,9 +2,10 @@ import React from 'react'
import { useTranslation } from 'react-i18next'
import { Link } from 'react-router-dom'
import type { CodeField } from '../../../../../../fields/config/types'
import type { CellComponentProps, Props } from './types'
import { CodeField, fieldAffectsData } from '../../../../../../fields/config/types'
import { fieldAffectsData } from '../../../../../../fields/config/types'
import { getTranslation } from '../../../../../../utilities/getTranslation'
import { useConfig } from '../../../../utilities/Config'
import RenderCustomComponent from '../../../../utilities/RenderCustomComponent'
@@ -60,8 +61,8 @@ const DefaultCell: React.FC<Props> = (props) => {
collection={collection}
data={`ID: ${cellData}`}
field={field as CodeField}
rowData={rowData}
nowrap
rowData={rowData}
/>
</WrapElement>
)

View File

@@ -43,6 +43,15 @@
width: 100%;
overflow: auto;
[class^="cell"] > p, [class^="cell"] > span, [class^="cell"] > a {
line-clamp: 4;
-webkit-box-orient: vertical;
-webkit-line-clamp: 4;
overflow: hidden;
display: -webkit-box;
max-width: 100vw;
}
#heading-_select,
.cell-_select {
min-width: unset;

View File

@@ -1,7 +1,7 @@
import type { PayloadRequest } from '../../../express/types'
import type { Payload } from '../../../payload'
import formatName from '../../../graphql/utilities/formatName'
import isolateTransactionID from '../../../utilities/isolateTransactionID'
import access from '../../operations/access'
const formatConfigNames = (results, configs) => {
@@ -19,7 +19,7 @@ const formatConfigNames = (results, configs) => {
function accessResolver(payload: Payload) {
async function resolver(_, args, context) {
const options = {
req: { ...context.req } as PayloadRequest,
req: isolateTransactionID(context.req),
}
const accessResults = await access(options)

View File

@@ -1,6 +1,6 @@
import type { Collection } from '../../../collections/config/types'
import type { PayloadRequest } from '../../../express/types'
import isolateTransactionID from '../../../utilities/isolateTransactionID'
import forgotPassword from '../../operations/forgotPassword'
function forgotPasswordResolver(collection: Collection): any {
@@ -12,7 +12,7 @@ function forgotPasswordResolver(collection: Collection): any {
},
disableEmail: args.disableEmail,
expiration: args.expiration,
req: { ...context.req } as PayloadRequest,
req: isolateTransactionID(context.req),
}
await forgotPassword(options)

View File

@@ -1,12 +1,11 @@
import type { PayloadRequest } from '../../../express/types'
import isolateTransactionID from '../../../utilities/isolateTransactionID'
import init from '../../operations/init'
function initResolver(collection: string) {
async function resolver(_, args, context) {
const options = {
collection,
req: { ...context.req } as PayloadRequest,
req: isolateTransactionID(context.req),
}
return init(options)

View File

@@ -1,6 +1,6 @@
import type { Collection } from '../../../collections/config/types'
import type { PayloadRequest } from '../../../express/types'
import isolateTransactionID from '../../../utilities/isolateTransactionID'
import login from '../../operations/login'
function loginResolver(collection: Collection) {
@@ -12,7 +12,7 @@ function loginResolver(collection: Collection) {
password: args.password,
},
depth: 0,
req: { ...context.req } as PayloadRequest,
req: isolateTransactionID(context.req),
res: context.res,
}

View File

@@ -1,13 +1,13 @@
import type { Collection } from '../../../collections/config/types'
import type { PayloadRequest } from '../../../express/types'
import isolateTransactionID from '../../../utilities/isolateTransactionID'
import logout from '../../operations/logout'
function logoutResolver(collection: Collection): any {
async function resolver(_, args, context) {
const options = {
collection,
req: { ...context.req } as PayloadRequest,
req: isolateTransactionID(context.req),
res: context.res,
}

View File

@@ -1,6 +1,6 @@
import type { Collection } from '../../../collections/config/types'
import type { PayloadRequest } from '../../../express/types'
import isolateTransactionID from '../../../utilities/isolateTransactionID'
import me from '../../operations/me'
function meResolver(collection: Collection): any {
@@ -8,7 +8,7 @@ function meResolver(collection: Collection): any {
const options = {
collection,
depth: 0,
req: { ...context.req } as PayloadRequest,
req: isolateTransactionID(context.req),
}
return me(options)
}

View File

@@ -1,6 +1,6 @@
import type { Collection } from '../../../collections/config/types'
import type { PayloadRequest } from '../../../express/types'
import isolateTransactionID from '../../../utilities/isolateTransactionID'
import getExtractJWT from '../../getExtractJWT'
import refresh from '../../operations/refresh'
@@ -18,7 +18,7 @@ function refreshResolver(collection: Collection) {
const options = {
collection,
depth: 0,
req: { ...context.req } as PayloadRequest,
req: isolateTransactionID(context.req),
res: context.res,
token,
}

View File

@@ -1,7 +1,7 @@
/* eslint-disable no-param-reassign */
import type { Collection } from '../../../collections/config/types'
import type { PayloadRequest } from '../../../express/types'
import isolateTransactionID from '../../../utilities/isolateTransactionID'
import resetPassword from '../../operations/resetPassword'
function resetPasswordResolver(collection: Collection) {
@@ -14,7 +14,7 @@ function resetPasswordResolver(collection: Collection) {
collection,
data: args,
depth: 0,
req: { ...context.req } as PayloadRequest,
req: isolateTransactionID(context.req),
res: context.res,
}

View File

@@ -1,6 +1,6 @@
import type { Collection } from '../../../collections/config/types'
import type { PayloadRequest } from '../../../express/types'
import isolateTransactionID from '../../../utilities/isolateTransactionID'
import unlock from '../../operations/unlock'
function unlockResolver(collection: Collection) {
@@ -8,7 +8,7 @@ function unlockResolver(collection: Collection) {
const options = {
collection,
data: { email: args.email },
req: { ...context.req } as PayloadRequest,
req: isolateTransactionID(context.req),
}
const result = await unlock(options)

View File

@@ -1,7 +1,7 @@
/* eslint-disable no-param-reassign */
import type { Collection } from '../../../collections/config/types'
import type { PayloadRequest } from '../../../express/types'
import isolateTransactionID from '../../../utilities/isolateTransactionID'
import verifyEmail from '../../operations/verifyEmail'
function verifyEmailResolver(collection: Collection) {
@@ -12,7 +12,7 @@ function verifyEmailResolver(collection: Collection) {
const options = {
api: 'GraphQL',
collection,
req: { ...context.req } as PayloadRequest,
req: isolateTransactionID(context.req),
res: context.res,
token: args.token,
}

View File

@@ -2,7 +2,10 @@ import type { PayloadRequest } from '../../express/types'
import type { AllOperations } from '../../types'
import type { Permissions } from '../types'
import { commitTransaction } from '../../utilities/commitTransaction'
import { getEntityPolicies } from '../../utilities/getEntityPolicies'
import { initTransaction } from '../../utilities/initTransaction'
import { killTransaction } from '../../utilities/killTransaction'
import { adminInit as adminInitTelemetry } from '../../utilities/telemetry/events/adminInit'
const allOperations: AllOperations[] = ['create', 'read', 'update', 'delete']
@@ -38,57 +41,64 @@ async function accessOperation(args: Arguments): Promise<Permissions> {
results.canAccessAdmin = false
}
await Promise.all(
config.collections.map(async (collection) => {
const collectionOperations = [...allOperations]
try {
const shouldCommit = await initTransaction(req)
await Promise.all(
config.collections.map(async (collection) => {
const collectionOperations = [...allOperations]
if (
collection.auth &&
typeof collection.auth.maxLoginAttempts !== 'undefined' &&
collection.auth.maxLoginAttempts !== 0
) {
collectionOperations.push('unlock')
}
if (
collection.auth &&
typeof collection.auth.maxLoginAttempts !== 'undefined' &&
collection.auth.maxLoginAttempts !== 0
) {
collectionOperations.push('unlock')
}
if (collection.versions) {
collectionOperations.push('readVersions')
}
if (collection.versions) {
collectionOperations.push('readVersions')
}
const collectionPolicy = await getEntityPolicies({
entity: collection,
operations: collectionOperations,
req,
type: 'collection',
})
results.collections = {
...results.collections,
[collection.slug]: collectionPolicy,
}
}),
)
const collectionPolicy = await getEntityPolicies({
entity: collection,
operations: collectionOperations,
req,
type: 'collection',
})
results.collections = {
...results.collections,
[collection.slug]: collectionPolicy,
}
}),
)
await Promise.all(
config.globals.map(async (global) => {
const globalOperations: AllOperations[] = ['read', 'update']
await Promise.all(
config.globals.map(async (global) => {
const globalOperations: AllOperations[] = ['read', 'update']
if (global.versions) {
globalOperations.push('readVersions')
}
if (global.versions) {
globalOperations.push('readVersions')
}
const globalPolicy = await getEntityPolicies({
entity: global,
operations: globalOperations,
req,
type: 'global',
})
results.globals = {
...results.globals,
[global.slug]: globalPolicy,
}
}),
)
const globalPolicy = await getEntityPolicies({
entity: global,
operations: globalOperations,
req,
type: 'global',
})
results.globals = {
...results.globals,
[global.slug]: globalPolicy,
}
}),
)
return results
if (shouldCommit) await commitTransaction(req)
return results
} catch (e: unknown) {
await killTransaction(req)
throw e
}
}
export default accessOperation

View File

@@ -129,6 +129,16 @@ const buildEndpoints = (collection: SanitizedCollectionConfig): Endpoint[] => {
method: 'get',
path: '/access/:id',
},
{
handler: docAccessRequestHandler,
method: 'post',
path: '/access/:id',
},
{
handler: docAccessRequestHandler,
method: 'post',
path: '/access',
},
{
handler: deprecatedUpdate,
method: 'put',

View File

@@ -6,6 +6,7 @@ import type { GeneratedTypes } from '../../../'
import type { PayloadRequest } from '../../../express/types'
import type { Collection } from '../../config/types'
import isolateTransactionID from '../../../utilities/isolateTransactionID'
import create from '../../operations/create'
export type Resolver<TSlug extends keyof GeneratedTypes['collections']> = (
@@ -37,7 +38,7 @@ export default function createResolver<TSlug extends keyof GeneratedTypes['colle
data: args.data,
depth: 0,
draft: args.draft,
req: { ...context.req } as PayloadRequest,
req: isolateTransactionID(context.req),
}
const result = await create(options)

View File

@@ -5,6 +5,7 @@ import type { GeneratedTypes } from '../../../'
import type { PayloadRequest } from '../../../express/types'
import type { Collection } from '../../config/types'
import isolateTransactionID from '../../../utilities/isolateTransactionID'
import deleteByID from '../../operations/deleteByID'
export type Resolver<TSlug extends keyof GeneratedTypes['collections']> = (
@@ -30,7 +31,7 @@ export default function getDeleteResolver<TSlug extends keyof GeneratedTypes['co
id: args.id,
collection,
depth: 0,
req: { ...context.req } as PayloadRequest,
req: isolateTransactionID(context.req),
}
const result = await deleteByID(options)

View File

@@ -1,6 +1,7 @@
import type { CollectionPermission, GlobalPermission } from '../../../auth'
import type { PayloadRequest } from '../../../express/types'
import isolateTransactionID from '../../../utilities/isolateTransactionID'
import { docAccess } from '../../operations/docAccess'
export type Resolver = (
@@ -18,7 +19,7 @@ export function docAccessResolver(): Resolver {
async function resolver(_, args, context) {
return docAccess({
id: args.id,
req: { ...context.req } as PayloadRequest,
req: isolateTransactionID(context.req),
})
}

View File

@@ -4,6 +4,7 @@ import type { PayloadRequest } from '../../../express/types'
import type { Where } from '../../../types'
import type { Collection } from '../../config/types'
import isolateTransactionID from '../../../utilities/isolateTransactionID'
import find from '../../operations/find'
export type Resolver = (
@@ -36,7 +37,7 @@ export default function findResolver(collection: Collection): Resolver {
draft: args.draft,
limit: args.limit,
page: args.page,
req: { ...context.req } as PayloadRequest,
req: isolateTransactionID(context.req),
sort: args.sort,
where: args.where,
}

View File

@@ -2,6 +2,7 @@ import type { GeneratedTypes } from '../../../'
import type { PayloadRequest } from '../../../express/types'
import type { Collection } from '../../config/types'
import isolateTransactionID from '../../../utilities/isolateTransactionID'
import findByID from '../../operations/findByID'
export type Resolver<T> = (
@@ -31,7 +32,7 @@ export default function findByIDResolver<T extends keyof GeneratedTypes['collect
collection,
depth: 0,
draft: args.draft,
req,
req: isolateTransactionID(context.req),
}
const result = await findByID(options)

View File

@@ -5,6 +5,7 @@ import type { PayloadRequest } from '../../../express/types'
import type { TypeWithVersion } from '../../../versions/types'
import type { Collection, TypeWithID } from '../../config/types'
import isolateTransactionID from '../../../utilities/isolateTransactionID'
import findVersionByID from '../../operations/findVersionByID'
export type Resolver<T extends TypeWithID = any> = (
@@ -31,7 +32,7 @@ export default function findVersionByIDResolver(collection: Collection): Resolve
collection,
depth: 0,
draft: args.draft,
req: { ...context.req } as PayloadRequest,
req: isolateTransactionID(context.req),
}
const result = await findVersionByID(options)

View File

@@ -7,6 +7,7 @@ import type { PayloadRequest } from '../../../express/types'
import type { Where } from '../../../types'
import type { Collection } from '../../config/types'
import isolateTransactionID from '../../../utilities/isolateTransactionID'
import findVersions from '../../operations/findVersions'
export type Resolver = (
@@ -35,7 +36,7 @@ export default function findVersionsResolver(collection: Collection): Resolver {
depth: 0,
limit: args.limit,
page: args.page,
req: { ...context.req } as PayloadRequest,
req: isolateTransactionID(context.req),
sort: args.sort,
where: args.where,
}

View File

@@ -4,6 +4,7 @@ import type { Response } from 'express'
import type { PayloadRequest } from '../../../express/types'
import type { Collection } from '../../config/types'
import isolateTransactionID from '../../../utilities/isolateTransactionID'
import restoreVersion from '../../operations/restoreVersion'
export type Resolver = (
@@ -23,7 +24,7 @@ export default function restoreVersionResolver(collection: Collection): Resolver
id: args.id,
collection,
depth: 0,
req: { ...context.req } as PayloadRequest,
req: isolateTransactionID(context.req),
}
const result = await restoreVersion(options)

View File

@@ -5,6 +5,7 @@ import type { GeneratedTypes } from '../../../'
import type { PayloadRequest } from '../../../express/types'
import type { Collection } from '../../config/types'
import isolateTransactionID from '../../../utilities/isolateTransactionID'
import updateByID from '../../operations/updateByID'
export type Resolver<TSlug extends keyof GeneratedTypes['collections']> = (
@@ -36,7 +37,7 @@ export default function updateResolver<TSlug extends keyof GeneratedTypes['colle
data: args.data,
depth: 0,
draft: args.draft,
req: { ...context.req } as PayloadRequest,
req: isolateTransactionID(context.req),
}
const result = await updateByID<TSlug>(options)

View File

@@ -2,7 +2,10 @@ import type { CollectionPermission } from '../../auth'
import type { PayloadRequest } from '../../express/types'
import type { AllOperations } from '../../types'
import { commitTransaction } from '../../utilities/commitTransaction'
import { getEntityPolicies } from '../../utilities/getEntityPolicies'
import { initTransaction } from '../../utilities/initTransaction'
import { killTransaction } from '../../utilities/killTransaction'
const allOperations: AllOperations[] = ['create', 'read', 'update', 'delete']
@@ -34,11 +37,22 @@ export async function docAccess(args: Arguments): Promise<CollectionPermission>
collectionOperations.push('readVersions')
}
return getEntityPolicies({
id,
entity: config,
operations: collectionOperations,
req,
type: 'collection',
})
try {
const shouldCommit = await initTransaction(req)
const result = await getEntityPolicies({
id,
entity: config,
operations: collectionOperations,
req,
type: 'collection',
})
if (shouldCommit) await commitTransaction(req)
return result
} catch (e: unknown) {
await killTransaction(req)
throw e
}
}

View File

@@ -97,6 +97,7 @@ export default joi.object({
CellComponent: component.required(),
FieldComponent: component.required(),
afterReadPromise: joi.func().optional(),
outputSchema: joi.func().optional(),
populationPromise: joi.func().optional(),
validate: joi.func().required(),
})

View File

@@ -151,7 +151,7 @@ export type BeginTransaction = (
options?: Record<string, unknown>,
) => Promise<null | number | string>
export type RollbackTransaction = (id: number | string) => Promise<void>
export type RollbackTransaction = (id: number | string) => Promise<void> | void
export type CommitTransaction = (id: number | string) => Promise<void>

View File

@@ -3,7 +3,11 @@ export { extractTranslations } from '../translations/extractTranslations'
export { i18nInit } from '../translations/init'
export { combineMerge } from '../utilities/combineMerge'
export { configToJSONSchema, entityToJSONSchema } from '../utilities/configToJSONSchema'
export {
configToJSONSchema,
entityToJSONSchema,
withNullableJSONSchemaType,
} from '../utilities/configToJSONSchema'
export { createArrayFromCommaDelineated } from '../utilities/createArrayFromCommaDelineated'
export { deepCopyObject } from '../utilities/deepCopyObject'

View File

@@ -59,6 +59,10 @@ export declare type PayloadRequest<U = any> = Request & {
* Identifier for the database transaction for interactions in a single, all-or-nothing operation.
*/
transactionID?: number | string
/**
* Used to ensure consistency when multiple operations try to create a transaction concurrently on the same request
*/
transactionIDPromise?: Promise<void>
/** The signed in user */
user: (U & User) | null
}

View File

@@ -71,16 +71,16 @@ export const text = baseField.keys({
name: joi.string().required(),
admin: baseAdminFields.keys({
autoComplete: joi.string(),
components: baseAdminComponentFields.keys({
Error: componentSchema,
Label: componentSchema,
afterInput: joi.array().items(componentSchema),
beforeInput: joi.array().items(componentSchema),
}),
placeholder: joi
.alternatives()
.try(joi.object().pattern(joi.string(), [joi.string()]), joi.string()),
rtl: joi.boolean(),
components: baseAdminComponentFields.keys({
Label: componentSchema,
Error: componentSchema,
BeforeInput: joi.array().items(componentSchema),
AfterInput: joi.array().items(componentSchema),
}),
}),
defaultValue: joi.alternatives().try(joi.string(), joi.func()),
maxLength: joi.number(),
@@ -92,20 +92,20 @@ export const number = baseField.keys({
name: joi.string().required(),
admin: baseAdminFields.keys({
autoComplete: joi.string(),
placeholder: joi.string(),
step: joi.number(),
components: baseAdminComponentFields.keys({
Label: componentSchema,
Error: componentSchema,
BeforeInput: joi
Label: componentSchema,
afterInput: joi
.array()
.items(componentSchema)
.when('hasMany', { not: true, otherwise: joi.forbidden() }),
AfterInput: joi
beforeInput: joi
.array()
.items(componentSchema)
.when('hasMany', { not: true, otherwise: joi.forbidden() }),
}),
placeholder: joi.string(),
step: joi.number(),
}),
defaultValue: joi.alternatives().try(joi.number(), joi.func()),
hasMany: joi.boolean().default(false),
@@ -119,15 +119,15 @@ export const number = baseField.keys({
export const textarea = baseField.keys({
name: joi.string().required(),
admin: baseAdminFields.keys({
components: baseAdminComponentFields.keys({
Error: componentSchema,
Label: componentSchema,
afterInput: joi.array().items(componentSchema),
beforeInput: joi.array().items(componentSchema),
}),
placeholder: joi.string(),
rows: joi.number(),
rtl: joi.boolean(),
components: baseAdminComponentFields.keys({
Label: componentSchema,
Error: componentSchema,
BeforeInput: joi.array().items(componentSchema),
AfterInput: joi.array().items(componentSchema),
}),
}),
defaultValue: joi.alternatives().try(joi.string(), joi.func()),
maxLength: joi.number(),
@@ -139,13 +139,13 @@ export const email = baseField.keys({
name: joi.string().required(),
admin: baseAdminFields.keys({
autoComplete: joi.string(),
placeholder: joi.string(),
components: baseAdminComponentFields.keys({
Label: componentSchema,
Error: componentSchema,
BeforeInput: joi.array().items(componentSchema),
AfterInput: joi.array().items(componentSchema),
Label: componentSchema,
afterInput: joi.array().items(componentSchema),
beforeInput: joi.array().items(componentSchema),
}),
placeholder: joi.string(),
}),
defaultValue: joi.alternatives().try(joi.string(), joi.func()),
maxLength: joi.number(),
@@ -156,12 +156,12 @@ export const email = baseField.keys({
export const code = baseField.keys({
name: joi.string().required(),
admin: baseAdminFields.keys({
components: baseAdminComponentFields.keys({
Error: componentSchema,
Label: componentSchema,
}),
editorOptions: joi.object().unknown(), // Editor['options'] @monaco-editor/react
language: joi.string(),
components: baseAdminComponentFields.keys({
Label: componentSchema,
Error: componentSchema,
}),
}),
defaultValue: joi.alternatives().try(joi.string(), joi.func()),
type: joi.string().valid('code').required(),
@@ -171,8 +171,8 @@ export const json = baseField.keys({
name: joi.string().required(),
admin: baseAdminFields.keys({
components: baseAdminComponentFields.keys({
Label: componentSchema,
Error: componentSchema,
Label: componentSchema,
}),
}),
defaultValue: joi.alternatives().try(joi.array(), joi.object()),
@@ -182,12 +182,12 @@ export const json = baseField.keys({
export const select = baseField.keys({
name: joi.string().required(),
admin: baseAdminFields.keys({
components: baseAdminComponentFields.keys({
Error: componentSchema,
Label: componentSchema,
}),
isClearable: joi.boolean().default(false),
isSortable: joi.boolean().default(false),
components: baseAdminComponentFields.keys({
Label: componentSchema,
Error: componentSchema,
}),
}),
defaultValue: joi
.alternatives()
@@ -214,11 +214,11 @@ export const select = baseField.keys({
export const radio = baseField.keys({
name: joi.string().required(),
admin: baseAdminFields.keys({
layout: joi.string().valid('vertical', 'horizontal'),
components: baseAdminComponentFields.keys({
Label: componentSchema,
Error: componentSchema,
Label: componentSchema,
}),
layout: joi.string().valid('vertical', 'horizontal'),
}),
defaultValue: joi.alternatives().try(joi.string().allow(''), joi.func()),
options: joi
@@ -318,8 +318,8 @@ export const upload = baseField.keys({
name: joi.string().required(),
admin: baseAdminFields.keys({
components: baseAdminComponentFields.keys({
Label: componentSchema,
Error: componentSchema,
Label: componentSchema,
}),
}),
defaultValue: joi.alternatives().try(joi.object(), joi.func()),
@@ -333,10 +333,10 @@ export const checkbox = baseField.keys({
name: joi.string().required(),
admin: baseAdminFields.keys({
components: baseAdminComponentFields.keys({
Label: componentSchema,
Error: componentSchema,
BeforeInput: joi.array().items(componentSchema),
AfterInput: joi.array().items(componentSchema),
Label: componentSchema,
afterInput: joi.array().items(componentSchema),
beforeInput: joi.array().items(componentSchema),
}),
}),
defaultValue: joi.alternatives().try(joi.boolean(), joi.func()),
@@ -347,10 +347,10 @@ export const point = baseField.keys({
name: joi.string().required(),
admin: baseAdminFields.keys({
components: baseAdminComponentFields.keys({
Label: componentSchema,
Error: componentSchema,
BeforeInput: joi.array().items(componentSchema),
AfterInput: joi.array().items(componentSchema),
Label: componentSchema,
afterInput: joi.array().items(componentSchema),
beforeInput: joi.array().items(componentSchema),
}),
}),
defaultValue: joi.alternatives().try(joi.array().items(joi.number()).max(2).min(2), joi.func()),
@@ -361,11 +361,11 @@ export const relationship = baseField.keys({
name: joi.string().required(),
admin: baseAdminFields.keys({
allowCreate: joi.boolean().default(true),
isSortable: joi.boolean().default(false),
components: baseAdminComponentFields.keys({
Label: componentSchema,
Error: componentSchema,
Label: componentSchema,
}),
isSortable: joi.boolean().default(false),
}),
defaultValue: joi.alternatives().try(joi.func()),
filterOptions: joi.alternatives().try(joi.object(), joi.func()),
@@ -434,6 +434,7 @@ export const richText = baseField.keys({
CellComponent: componentSchema.required(),
FieldComponent: componentSchema.required(),
afterReadPromise: joi.func().optional(),
outputSchema: joi.func().optional(),
populationPromise: joi.func().optional(),
validate: joi.func().required(),
})
@@ -444,6 +445,12 @@ export const richText = baseField.keys({
export const date = baseField.keys({
name: joi.string().required(),
admin: baseAdminFields.keys({
components: baseAdminComponentFields.keys({
Error: componentSchema,
Label: componentSchema,
afterInput: joi.array().items(componentSchema),
beforeInput: joi.array().items(componentSchema),
}),
date: joi.object({
displayFormat: joi.string(),
maxDate: joi.date(),
@@ -451,17 +458,12 @@ export const date = baseField.keys({
minDate: joi.date(),
minTime: joi.date(),
monthsToShow: joi.number(),
overrides: joi.object().unknown(),
pickerAppearance: joi.string(),
timeFormat: joi.string(),
timeIntervals: joi.number(),
}),
placeholder: joi.string(),
components: baseAdminComponentFields.keys({
Label: componentSchema,
Error: componentSchema,
BeforeInput: joi.array().items(componentSchema),
AfterInput: joi.array().items(componentSchema),
}),
}),
defaultValue: joi.alternatives().try(joi.string(), joi.func()),
type: joi.string().valid('date').required(),

View File

@@ -4,8 +4,12 @@ import type { TFunction } from 'i18next'
import type { CSSProperties } from 'react'
import monacoeditor from 'monaco-editor' // IMPORTANT - DO NOT REMOVE: This is required for pnpm's default isolated mode to work - even though the import is not used. This is due to a typescript bug: https://github.com/microsoft/TypeScript/issues/47663#issuecomment-1519138189. (tsbugisolatedmode)
import type React from 'react'
import type { ConditionalDateProps } from '../../admin/components/elements/DatePicker/types'
import type { Props as ErrorProps } from '../../admin/components/forms/Error/types'
import type { Description } from '../../admin/components/forms/FieldDescription/types'
import type { Props as LabelProps } from '../../admin/components/forms/Label/types'
import type { RowLabel } from '../../admin/components/forms/RowLabel/types'
import type { RichTextAdapter } from '../../admin/components/forms/field-types/RichText/types'
import type { User } from '../../auth'
@@ -15,8 +19,6 @@ import type { PayloadRequest, RequestContext } from '../../express/types'
import type { SanitizedGlobalConfig } from '../../globals/config/types'
import type { Payload } from '../../payload'
import type { Operation, Where } from '../../types'
import type { Props as ErrorProps } from '../../admin/components/forms/Error/types'
import type { Props as LabelProps } from '../../admin/components/forms/Label/types'
export type FieldHookArgs<T extends TypeWithID = any, P = any, S = any> = {
/** The collection which the field belongs to. If the field belongs to a global, this will be null. */
@@ -52,10 +54,23 @@ export type FieldHook<T extends TypeWithID = any, P = any, S = any> = (
) => P | Promise<P>
export type FieldAccess<T extends TypeWithID = any, P = any, U = any> = (args: {
/**
* The incoming data used to `create` or `update` the document with. `data` is undefined during the `read` operation.
*/
data?: Partial<T>
/**
* The original data of the document before the `update` is applied. `doc` is undefined during the `create` operation.
*/
doc?: T
/**
* The `id` of the current document being read or updated. `id` is undefined during the `create` operation.
*/
id?: number | string
/** The `Express` request object containing the currently authenticated `user` */
req: PayloadRequest<U>
/**
* Immediately adjacent data to this field. For example, if this is a `group` field, then `siblingData` will be the other fields within the group.
*/
siblingData?: Partial<P>
}) => Promise<boolean> | boolean
@@ -85,6 +100,10 @@ type Admin = {
Field?: React.ComponentType<any>
Filter?: React.ComponentType<any>
}
/**
* You can programmatically show / hide fields based on what other fields are doing.
* This is also run on the server, to determine if the field should be validated.
*/
condition?: Condition
description?: Description
disableBulkEdit?: boolean
@@ -157,16 +176,16 @@ export type NumberField = FieldBase & {
admin?: Admin & {
/** Set this property to a string that will be used for browser autocomplete. */
autoComplete?: string
components?: {
Error?: React.ComponentType<ErrorProps>
Label?: React.ComponentType<LabelProps>
afterInput?: React.ComponentType<any>[]
beforeInput?: React.ComponentType<any>[]
}
/** Set this property to define a placeholder string for the field. */
placeholder?: Record<string, string> | string
/** Set a value for the number field to increment / decrement using browser controls. */
step?: number
components?: {
Error?: React.ComponentType<ErrorProps>
Label?: React.ComponentType<LabelProps>
BeforeInput?: React.ReactElement<any>[]
AfterInput?: React.ReactElement<any>[]
}
}
/** Maximum value accepted. Used in the default `validation` function. */
max?: number
@@ -195,14 +214,14 @@ export type NumberField = FieldBase & {
export type TextField = FieldBase & {
admin?: Admin & {
autoComplete?: string
placeholder?: Record<string, string> | string
rtl?: boolean
components?: {
Error?: React.ComponentType<ErrorProps>
Label?: React.ComponentType<LabelProps>
BeforeInput?: React.ReactElement<any>[]
AfterInput?: React.ReactElement<any>[]
afterInput?: React.ComponentType<any>[]
beforeInput?: React.ComponentType<any>[]
}
placeholder?: Record<string, string> | string
rtl?: boolean
}
maxLength?: number
minLength?: number
@@ -212,28 +231,28 @@ export type TextField = FieldBase & {
export type EmailField = FieldBase & {
admin?: Admin & {
autoComplete?: string
placeholder?: Record<string, string> | string
components?: {
Error?: React.ComponentType<ErrorProps>
Label?: React.ComponentType<LabelProps>
BeforeInput?: React.ReactElement<any>[]
AfterInput?: React.ReactElement<any>[]
afterInput?: React.ComponentType<any>[]
beforeInput?: React.ComponentType<any>[]
}
placeholder?: Record<string, string> | string
}
type: 'email'
}
export type TextareaField = FieldBase & {
admin?: Admin & {
placeholder?: Record<string, string> | string
rows?: number
rtl?: boolean
components?: {
Error?: React.ComponentType<ErrorProps>
Label?: React.ComponentType<LabelProps>
BeforeInput?: React.ReactElement<any>[]
AfterInput?: React.ReactElement<any>[]
afterInput?: React.ComponentType<any>[]
beforeInput?: React.ComponentType<any>[]
}
placeholder?: Record<string, string> | string
rows?: number
rtl?: boolean
}
maxLength?: number
minLength?: number
@@ -241,27 +260,27 @@ export type TextareaField = FieldBase & {
}
export type CheckboxField = FieldBase & {
type: 'checkbox'
admin?: Admin & {
components?: {
Error?: React.ComponentType<ErrorProps>
Label?: React.ComponentType<LabelProps>
BeforeInput?: React.ReactElement<any>[]
AfterInput?: React.ReactElement<any>[]
afterInput?: React.ComponentType<any>[]
beforeInput?: React.ComponentType<any>[]
}
}
type: 'checkbox'
}
export type DateField = FieldBase & {
admin?: Admin & {
date?: ConditionalDateProps
placeholder?: Record<string, string> | string
components?: {
Error?: React.ComponentType<ErrorProps>
Label?: React.ComponentType<LabelProps>
BeforeInput?: React.ReactElement<any>[]
AfterInput?: React.ReactElement<any>[]
afterInput?: React.ComponentType<any>[]
beforeInput?: React.ComponentType<any>[]
}
date?: ConditionalDateProps
placeholder?: Record<string, string> | string
}
type: 'date'
}
@@ -369,12 +388,12 @@ export type UploadField = FieldBase & {
}
type CodeAdmin = Admin & {
editorOptions?: EditorProps['options']
language?: string
components?: {
Error?: React.ComponentType<ErrorProps>
Label?: React.ComponentType<LabelProps>
}
editorOptions?: EditorProps['options']
language?: string
}
export type CodeField = Omit<FieldBase, 'admin'> & {
@@ -385,11 +404,11 @@ export type CodeField = Omit<FieldBase, 'admin'> & {
}
type JSONAdmin = Admin & {
editorOptions?: EditorProps['options']
components?: {
Error?: React.ComponentType<ErrorProps>
Label?: React.ComponentType<LabelProps>
}
editorOptions?: EditorProps['options']
}
export type JSONField = Omit<FieldBase, 'admin'> & {
@@ -399,12 +418,12 @@ export type JSONField = Omit<FieldBase, 'admin'> & {
export type SelectField = FieldBase & {
admin?: Admin & {
isClearable?: boolean
isSortable?: boolean
components?: {
Error?: React.ComponentType<ErrorProps>
Label?: React.ComponentType<LabelProps>
}
isClearable?: boolean
isSortable?: boolean
}
hasMany?: boolean
options: Option[]
@@ -414,11 +433,11 @@ export type SelectField = FieldBase & {
export type RelationshipField = FieldBase & {
admin?: Admin & {
allowCreate?: boolean
isSortable?: boolean
components?: {
Error?: React.ComponentType<ErrorProps>
Label?: React.ComponentType<LabelProps>
}
isSortable?: boolean
}
filterOptions?: FilterOptions
hasMany?: boolean
@@ -502,11 +521,11 @@ export type ArrayField = FieldBase & {
export type RadioField = FieldBase & {
admin?: Admin & {
layout?: 'horizontal' | 'vertical'
components?: {
Error?: React.ComponentType<ErrorProps>
Label?: React.ComponentType<LabelProps>
}
layout?: 'horizontal' | 'vertical'
}
options: Option[]
type: 'radio'

View File

@@ -413,6 +413,11 @@ describe('Field Validations', () => {
const result = number(val, numberOptions)
expect(result).toBe(true)
})
it('should validate 0', () => {
const val = 0
const result = number(val, { ...numberOptions, required: true })
expect(result).toBe(true)
})
it('should validate 2', () => {
const val = 1.5
const result = number(val, numberOptions)
@@ -431,7 +436,7 @@ describe('Field Validations', () => {
it('should handle required value', () => {
const val = ''
const result = number(val, { ...numberOptions, required: true })
expect(result).toBe('validation:enterNumber')
expect(result).toBe('validation:required')
})
it('should validate minValue', () => {
const val = 2.4
@@ -461,12 +466,12 @@ describe('Field Validations', () => {
it('should validate an array of numbers using minRows', async () => {
const val = [1.25, 2.5]
const result = number(val, { ...numberOptions, hasMany: true, minRows: 4 })
expect(result).toBe('validation:lessThanMin')
expect(result).toBe('validation:requiresAtLeast')
})
it('should validate an array of numbers using maxRows', async () => {
const val = [1.25, 2.5, 3.5]
const result = number(val, { ...numberOptions, hasMany: true, maxRows: 2 })
expect(result).toBe('validation:greaterThanMax')
expect(result).toBe('validation:requiresNoMoreThan')
})
})
})

View File

@@ -22,67 +22,13 @@ import type {
import canUseDOM from '../utilities/canUseDOM'
import { getIDType } from '../utilities/getIDType'
import { isNumber } from '../utilities/isNumber'
import { isValidID } from '../utilities/isValidID'
import { fieldAffectsData } from './config/types'
export const number: Validate<unknown, unknown, NumberField> = (
value: number | number[],
{ hasMany, max, maxRows, min, minRows, required, t },
) => {
const toValidate: number[] = Array.isArray(value) ? value : [value]
// eslint-disable-next-line no-restricted-syntax
for (const valueToValidate of toValidate) {
const floatValue = parseFloat(valueToValidate as unknown as string)
if (
(value && typeof floatValue !== 'number') ||
(required && Number.isNaN(floatValue)) ||
(value && Number.isNaN(floatValue))
) {
return t('validation:enterNumber')
}
if (typeof max === 'number' && floatValue > max) {
return t('validation:greaterThanMax', { label: t('value'), max, value })
}
if (typeof min === 'number' && floatValue < min) {
return t('validation:lessThanMin', { label: t('value'), min, value })
}
if (required && typeof floatValue !== 'number') {
return t('validation:required')
}
}
if (required && toValidate.length === 0) {
return t('validation:required')
}
if (hasMany === true) {
if (minRows && toValidate.length < minRows) {
return t('validation:lessThanMin', {
label: t('rows'),
min: minRows,
value: toValidate.length,
})
}
if (maxRows && toValidate.length > maxRows) {
return t('validation:greaterThanMax', {
label: t('rows'),
max: maxRows,
value: toValidate.length,
})
}
}
return true
}
export const text: Validate<unknown, unknown, TextField> = (
value: string,
{ config, maxLength: fieldMaxLength, minLength, payload, required, t },
{ config, maxLength: fieldMaxLength, minLength, required, t },
) => {
let maxLength: number
@@ -220,9 +166,87 @@ export const richText: Validate<object, unknown, RichTextField, RichTextField> =
return await editor.validate(value, options)
}
const validateArrayLength: any = (
value,
options: {
maxRows?: number
minRows?: number
required?: boolean
t: (key: string, options?: { [key: string]: number | string }) => string
},
) => {
const { maxRows, minRows, required, t } = options
const arrayLength = Array.isArray(value) ? value.length : 0
if (!required && arrayLength === 0) return true
if (minRows && arrayLength < minRows) {
return t('validation:requiresAtLeast', { count: minRows, label: t('rows') })
}
if (maxRows && arrayLength > maxRows) {
return t('validation:requiresNoMoreThan', { count: maxRows, label: t('rows') })
}
if (required && !arrayLength) {
return t('validation:requiresAtLeast', { count: 1, label: t('row') })
}
return true
}
export const number: Validate<unknown, unknown, NumberField> = (
value: number | number[],
{ hasMany, max, maxRows, min, minRows, required, t },
) => {
if (hasMany === true) {
const lengthValidationResult = validateArrayLength(value, { maxRows, minRows, required, t })
if (typeof lengthValidationResult === 'string') return lengthValidationResult
}
if (!value && !isNumber(value)) {
// if no value is present, validate based on required
if (required) return t('validation:required')
if (!required) return true
}
const numbersToValidate: number[] = Array.isArray(value) ? value : [value]
for (const number of numbersToValidate) {
if (!isNumber(number)) return t('validation:enterNumber')
const numberValue = parseFloat(number as unknown as string)
if (typeof max === 'number' && numberValue > max) {
return t('validation:greaterThanMax', { label: t('value'), max, value })
}
if (typeof min === 'number' && numberValue < min) {
return t('validation:lessThanMin', { label: t('value'), min, value })
}
}
return true
}
export const array: Validate<unknown, unknown, ArrayField> = (
value,
{ maxRows, minRows, required, t },
) => {
return validateArrayLength(value, { maxRows, minRows, required, t })
}
export const blocks: Validate<unknown, unknown, BlockField> = (
value,
{ maxRows, minRows, required, t },
) => {
return validateArrayLength(value, { maxRows, minRows, required, t })
}
const validateFilterOptions: Validate = async (
value,
{ id, data, filterOptions, payload, relationTo, siblingData, t, user, req },
{ id, data, filterOptions, payload, relationTo, req, siblingData, t, user },
) => {
if (!canUseDOM && typeof filterOptions !== 'undefined' && value) {
const options: {
@@ -344,7 +368,7 @@ export const relationship: Validate<unknown, unknown, RelationshipField> = async
return t('validation:required')
}
if (Array.isArray(value)) {
if (Array.isArray(value) && value.length > 0) {
if (minRows && value.length < minRows) {
return t('validation:lessThanMin', { label: t('rows'), min: minRows, value: value.length })
}
@@ -398,27 +422,6 @@ export const relationship: Validate<unknown, unknown, RelationshipField> = async
return validateFilterOptions(value, options)
}
export const array: Validate<unknown, unknown, ArrayField> = (
value,
{ maxRows, minRows, required, t },
) => {
const arrayLength = Array.isArray(value) ? value.length : 0
if (minRows && arrayLength < minRows) {
return t('validation:requiresAtLeast', { count: minRows, label: t('rows') })
}
if (maxRows && arrayLength > maxRows) {
return t('validation:requiresNoMoreThan', { count: maxRows, label: t('rows') })
}
if (!arrayLength && required) {
return t('validation:requiresAtLeast', { count: 1, label: t('row') })
}
return true
}
export const select: Validate<unknown, unknown, SelectField> = (
value,
{ hasMany, options, required, t },
@@ -467,27 +470,6 @@ export const radio: Validate<unknown, unknown, RadioField> = (value, { options,
return required ? t('validation:required') : true
}
export const blocks: Validate<unknown, unknown, BlockField> = (
value,
{ maxRows, minRows, required, t },
) => {
const arrayLength = Array.isArray(value) ? value.length : 0
if (minRows && arrayLength < minRows) {
return t('validation:requiresAtLeast', { count: minRows, label: t('rows') })
}
if (maxRows && arrayLength > maxRows) {
return t('validation:requiresNoMoreThan', { count: maxRows, label: t('rows') })
}
if (!arrayLength && required) {
return t('validation:requiresAtLeast', { count: 1, label: t('row') })
}
return true
}
export const point: Validate<unknown, unknown, PointField> = (
value: [number | string, number | string] = ['', ''],
{ required, t },

View File

@@ -38,6 +38,11 @@ const buildEndpoints = (global: SanitizedGlobalConfig): Endpoint[] => {
method: 'get',
path: '/access',
},
{
handler: async (req, res, next) => docAccessRequestHandler(req, res, next, global),
method: 'post',
path: '/access',
},
{
handler: findOne(global),
method: 'get',

View File

@@ -2,6 +2,7 @@ import type { CollectionPermission, GlobalPermission } from '../../../auth'
import type { PayloadRequest } from '../../../express/types'
import type { SanitizedGlobalConfig } from '../../config/types'
import isolateTransactionID from '../../../utilities/isolateTransactionID'
import { docAccess } from '../../operations/docAccess'
export type Resolver = (
@@ -16,7 +17,7 @@ export function docAccessResolver(global: SanitizedGlobalConfig): Resolver {
async function resolver(_, context) {
return docAccess({
globalConfig: global,
req: { ...context.req } as PayloadRequest,
req: isolateTransactionID(context.req),
})
}

View File

@@ -1,8 +1,9 @@
/* eslint-disable no-param-reassign */
import type { Document, PayloadRequest } from '../../../types'
import type { Document } from '../../../types'
import type { SanitizedGlobalConfig } from '../../config/types'
import isolateTransactionID from '../../../utilities/isolateTransactionID'
import findOne from '../../operations/findOne'
export default function findOneResolver(globalConfig: SanitizedGlobalConfig): Document {
@@ -16,7 +17,7 @@ export default function findOneResolver(globalConfig: SanitizedGlobalConfig): Do
depth: 0,
draft: args.draft,
globalConfig,
req: { ...context.req } as PayloadRequest,
req: isolateTransactionID(context.req),
slug,
}

View File

@@ -5,6 +5,7 @@ import type { PayloadRequest } from '../../../express/types'
import type { Document } from '../../../types'
import type { SanitizedGlobalConfig } from '../../config/types'
import isolateTransactionID from '../../../utilities/isolateTransactionID'
import findVersionByID from '../../operations/findVersionByID'
export type Resolver = (
@@ -31,7 +32,7 @@ export default function findVersionByIDResolver(globalConfig: SanitizedGlobalCon
depth: 0,
draft: args.draft,
globalConfig,
req: { ...context.req } as PayloadRequest,
req: isolateTransactionID(context.req),
}
const result = await findVersionByID(options)

View File

@@ -4,6 +4,7 @@ import type { PayloadRequest } from '../../../express/types'
import type { Document, Where } from '../../../types'
import type { SanitizedGlobalConfig } from '../../config/types'
import isolateTransactionID from '../../../utilities/isolateTransactionID'
import findVersions from '../../operations/findVersions'
export type Resolver = (
@@ -29,7 +30,7 @@ export default function findVersionsResolver(globalConfig: SanitizedGlobalConfig
globalConfig,
limit: args.limit,
page: args.page,
req: { ...context.req } as PayloadRequest,
req: isolateTransactionID(context.req),
sort: args.sort,
where: args.where,
}

View File

@@ -4,6 +4,7 @@ import type { PayloadRequest } from '../../../express/types'
import type { Document } from '../../../types'
import type { SanitizedGlobalConfig } from '../../config/types'
import isolateTransactionID from '../../../utilities/isolateTransactionID'
import restoreVersion from '../../operations/restoreVersion'
type Resolver = (
@@ -22,7 +23,7 @@ export default function restoreVersionResolver(globalConfig: SanitizedGlobalConf
id: args.id,
depth: 0,
globalConfig,
req: { ...context.req } as PayloadRequest,
req: isolateTransactionID(context.req),
}
const result = await restoreVersion(options)

View File

@@ -5,6 +5,7 @@ import type { GeneratedTypes } from '../../../'
import type { PayloadRequest } from '../../../express/types'
import type { SanitizedGlobalConfig } from '../../config/types'
import isolateTransactionID from '../../../utilities/isolateTransactionID'
import update from '../../operations/update'
type Resolver<TSlug extends keyof GeneratedTypes['globals']> = (
@@ -35,7 +36,7 @@ export default function updateResolver<TSlug extends keyof GeneratedTypes['globa
depth: 0,
draft: args.draft,
globalConfig,
req: { ...context.req } as PayloadRequest,
req: isolateTransactionID(context.req),
slug,
}

View File

@@ -3,7 +3,10 @@ import type { PayloadRequest } from '../../express/types'
import type { AllOperations } from '../../types'
import type { SanitizedGlobalConfig } from '../config/types'
import { commitTransaction } from '../../utilities/commitTransaction'
import { getEntityPolicies } from '../../utilities/getEntityPolicies'
import { initTransaction } from '../../utilities/initTransaction'
import { killTransaction } from '../../utilities/killTransaction'
type Arguments = {
globalConfig: SanitizedGlobalConfig
@@ -19,10 +22,18 @@ export async function docAccess(args: Arguments): Promise<GlobalPermission> {
globalOperations.push('readVersions')
}
return getEntityPolicies({
entity: globalConfig,
operations: globalOperations,
req,
type: 'global',
})
try {
const shouldCommit = await initTransaction(req)
const result = await getEntityPolicies({
entity: globalConfig,
operations: globalOperations,
req,
type: 'global',
})
if (shouldCommit) await commitTransaction(req)
return result
} catch (e: unknown) {
await killTransaction(req)
throw e
}
}

View File

@@ -14,6 +14,7 @@ import initGlobals from '../globals/graphql/init'
import buildFallbackLocaleInputType from './schema/buildFallbackLocaleInputType'
import buildLocaleInputType from './schema/buildLocaleInputType'
import buildPoliciesType from './schema/buildPoliciesType'
import { wrapCustomFields } from './utilities/wrapCustomResolver'
export default function registerGraphQLSchema(payload: Payload): void {
payload.types = {
@@ -55,7 +56,7 @@ export default function registerGraphQLSchema(payload: Payload): void {
...payload.Query,
fields: {
...payload.Query.fields,
...(customQueries || {}),
...wrapCustomFields((customQueries || {}) as never),
},
}
}
@@ -66,7 +67,7 @@ export default function registerGraphQLSchema(payload: Payload): void {
...payload.Mutation,
fields: {
...payload.Mutation.fields,
...(customMutations || {}),
...wrapCustomFields((customMutations || {}) as never),
},
}
}

View File

@@ -0,0 +1,28 @@
import type { ObjMap } from 'graphql/jsutils/ObjMap'
import type { GraphQLFieldResolver } from 'graphql/type/definition'
import type { GraphQLFieldConfig } from 'graphql/type/definition'
import type { PayloadRequest } from '../../express/types'
import isolateTransactionID from '../../utilities/isolateTransactionID'
type PayloadContext = { req: PayloadRequest }
function wrapCustomResolver<TSource, TArgs, TResult>(
resolver: GraphQLFieldResolver<TSource, PayloadContext, TArgs, TResult>,
): GraphQLFieldResolver<TSource, PayloadContext, TArgs, TResult> {
return (source, args, context, info) => {
return resolver(source, args, { ...context, req: isolateTransactionID(context.req) }, info)
}
}
export function wrapCustomFields<TSource>(
fields: ObjMap<GraphQLFieldConfig<TSource, PayloadContext>>,
): ObjMap<GraphQLFieldConfig<TSource, PayloadContext>> {
for (const key in fields) {
if (fields[key].resolve) {
fields[key].resolve = wrapCustomResolver(fields[key].resolve)
}
}
return fields
}

View File

@@ -97,7 +97,7 @@
"addLink": "Dodaj Link",
"addNew": "Dodaj nowy",
"addNewLabel": "Dodaj nowy {{label}}",
"addRelationship": "Dodaj Relacje",
"addRelationship": "Dodaj Relację",
"addUpload": "Dodaj ładowanie",
"block": "Blok",
"blockType": "Typ Bloku",
@@ -110,21 +110,21 @@
"customURL": "Niestandardowy adres URL",
"editLabelData": "Edytuj dane {{label}}",
"editLink": "Edytuj Link",
"editRelationship": "Edytuj Relacje",
"editRelationship": "Edytuj Relację",
"enterURL": "Wpisz adres URL",
"internalLink": "Link wewnętrzny",
"itemsAndMore": "{{items}} i {{count}} więcej",
"labelRelationship": "Relacja {{label}}",
"latitude": "Szerokość",
"linkType": "Typ łącza",
"linkedTo": "Połączony z <0>{{etykietą}}</0>",
"linkedTo": "Połączony z <0>{{label}}</0>",
"longitude": "Długość geograficzna",
"newLabel": "Nowy {{label}}",
"openInNewTab": "Otwórz w nowej karcie",
"passwordsDoNotMatch": "Hasła nie pasują",
"relatedDocument": "Powiązany dokument",
"relationTo": "Powiązany z",
"removeRelationship": "Usuń Związek",
"removeRelationship": "Usuń Relację",
"removeUpload": "Usuń Wrzucone",
"saveChanges": "Zapisz zmiany",
"searchForBlock": "Szukaj bloku",
@@ -142,29 +142,29 @@
"aboutToDeleteCount_many": "Zamierzasz usunąć {{count}} {{label}}",
"aboutToDeleteCount_one": "Zamierzasz usunąć {{count}} {{label}}",
"aboutToDeleteCount_other": "Zamierzasz usunąć {{count}} {{label}}",
"addBelow": "Dodaj boniżej",
"addBelow": "Dodaj poniżej",
"addFilter": "Dodaj filtr",
"adminTheme": "Motyw administratora",
"and": "I",
"and": "i",
"applyChanges": "Zastosuj zmiany",
"ascending": "Rosnąco",
"automatic": "Automatyczny",
"backToDashboard": "Powrót do panelu",
"cancel": "Anuluj",
"changesNotSaved": "Twoje zmiany nie zostały zapisane. Jeśli teraz wyjdziesz, stracisz swoje zmiany.",
"close": "Zamknąć",
"collapse": "Zawalenie",
"close": "Zamknij",
"collapse": "Zwiń",
"collection": "Kolekcja",
"collections": "Kolekcje",
"columnToSort": "Kolumna sortowania",
"columns": "Kolumny",
"confirm": "Potiwerdź",
"confirmDeletion": "Potiwerdź usunięcie",
"confirm": "Potwierdź",
"confirmDeletion": "Potwierdź usunięcie",
"confirmDuplication": "Potwierdź duplikację",
"copied": "Skopiowano",
"copy": "Skopiuj",
"create": "Stwórz",
"createNew": "Stwórzy nowy",
"createNew": "Stwórz nowy",
"createNewLabel": "Stwórz nowy {{label}}",
"created": "Utworzono",
"createdAt": "Data utworzenia",
@@ -173,8 +173,8 @@
"dark": "Ciemny",
"dashboard": "Panel",
"delete": "Usuń",
"deletedCountSuccessfully": "",
"deletedSuccessfully": "Skutecznie usunięte.",
"deletedCountSuccessfully": "Pomyślnie usunięto {{count}} {{label}}.",
"deletedSuccessfully": "Pomyślnie usunięto.",
"deleting": "Usuwanie...",
"descending": "Malejąco",
"deselectAllRows": "Odznacz wszystkie wiersze",
@@ -191,7 +191,7 @@
"enterAValue": "Wpisz wartość",
"error": "Błąd",
"errors": "Błędy",
"fallbackToDefaultLocale": "Powrót do domyślnego locale",
"fallbackToDefaultLocale": "Powrót do domyślnych ustawień regionalnych",
"filter": "Filtr",
"filterWhere": "Filtruj gdzie",
"filters": "Filtry",
@@ -203,8 +203,8 @@
"light": "Jasny",
"livePreview": "Podgląd",
"loading": "Ładowanie",
"locale": "Lokalizacja",
"locales": "Lokalne",
"locale": "Ustawienia regionalne",
"locales": "Ustawienia regionalne",
"menu": "Menu",
"moveDown": "Przesuń niżej",
"moveUp": "Przesuń wyżej",
@@ -219,7 +219,7 @@
"nothingFound": "Nic nie znaleziono",
"of": "z",
"open": "Otwórz",
"or": "Lub",
"or": "lub",
"order": "Kolejność",
"pageNotFound": "Strona nie znaleziona",
"password": "Hasło",
@@ -232,18 +232,18 @@
"save": "Zapisz",
"saving": "Zapisywanie...",
"searchBy": "Szukaj według",
"selectAll": "Wybierz wszystkie {{liczba}} {{etykieta}}",
"selectAll": "Wybierz wszystkie {{count}} {{label}}",
"selectAllRows": "Wybierz wszystkie wiersze",
"selectValue": "Wybierz wartość",
"selectedCount": "Wybrano {{count}} {{label}}",
"showAllLabel": "Pokaż wszystkie {{label}}",
"sorryNotFound": "Przepraszamy — nie ma nic, co odpowiadałoby twojej prośbie.",
"sorryNotFound": "Przepraszamy — nie ma nic, co odpowiadałoby twojemu zapytaniu.",
"sort": "Sortuj",
"sortByLabelDirection": "Sortuj według {{label}} {{direction}}",
"stayOnThisPage": "Pozostań na stronie",
"submissionSuccessful": "Zgłoszenie zakończone powodzeniem.",
"submit": "Zatwierdź",
"successfullyCreated": "{{label}} successfully created.",
"successfullyCreated": "Pomyślnie utworzono {{label}}.",
"successfullyDuplicated": "Pomyślnie zduplikowano {{label}}",
"thisLanguage": "Polski",
"titleDeleted": "Pomyślnie usunięto {{label}} {{title}}",
@@ -254,7 +254,7 @@
"updatedCountSuccessfully": "Pomyślnie zaktualizowano {{count}} {{label}}.",
"updatedSuccessfully": "Aktualizacja zakończona sukcesem.",
"updating": "Aktualizacja",
"uploading": "Wgrywanie",
"uploading": "Przesyłanie",
"user": "użytkownik",
"users": "użytkownicy",
"value": "Wartość",
@@ -309,7 +309,7 @@
"longerThanMin": "Ta wartość musi być dłuższa niż minimalna długość znaków: {{minLength}}.",
"notValidDate": "\"{{value}}\" nie jest prawidłową datą.",
"required": "To pole jest wymagane.",
"requiresAtLeast": "This field requires at least {{count}} {{label}}.",
"requiresAtLeast": "To pole wymaga co najmniej {{count}} {{label}}.",
"requiresNoMoreThan": "To pole może posiadać co najmniej {{count}} {{label}}.",
"requiresTwoNumbers": "To pole wymaga dwóch liczb.",
"shorterThanMax": "Ta wartość musi być krótsza niż maksymalna długość znaków: {{maxLength}}.",
@@ -349,9 +349,9 @@
"revertToPublished": "Przywróć do opublikowanego",
"reverting": "Cofanie...",
"saveDraft": "Zapisz szkic",
"selectLocales": "Wybierz lokalizacje do wyświetlenia",
"selectLocales": "Wybierz ustawienia regionalne do wyświetlenia",
"selectVersionToCompare": "Wybierz wersję do porównania",
"showLocales": "Pokaż lokalizacje:",
"showLocales": "Pokaż ustawienia regionalne:",
"showingVersionsFor": "Wyświetlanie wersji dla:",
"status": "Status",
"type": "Typ",
@@ -370,4 +370,4 @@
"viewingVersions": "Przeglądanie wersji {{entityLabel}} {{documentTitle}}",
"viewingVersionsGlobal": "Przeglądanie wersji dla globalnej kolekcji {{entityLabel}}"
}
}
}

View File

@@ -131,7 +131,7 @@ const preventResize = (
const isWidthOrHeightNotDefined = !desiredHeight || !desiredWidth
if (isWidthOrHeightNotDefined) {
// If with and height are not defined, it means there is a format conversion
// If width and height are not defined, it means there is a format conversion
// and the image needs to be "resized" (transformed).
return false // needs resize
}
@@ -156,9 +156,10 @@ const preventResize = (
* @returns true if the image should passed directly to sharp
*/
const applyPayloadAdjustments = (
{ height, width, withoutEnlargement, withoutReduction }: ImageSize,
{ fit, height, width, withoutEnlargement, withoutReduction }: ImageSize,
original: ProbedImageSize,
) => {
if (fit === 'contain' || fit === 'inside') return false
if (!isNumber(height) && !isNumber(width)) return false
const targetAspectRatio = width / height

View File

@@ -46,9 +46,6 @@ function buildOptionEnums(options: Option[]): string[] {
})
}
/**
* This is used for generating the TypeScript types (payload-types.ts) with the payload generate:types command.
*/
function generateEntitySchemas(
entities: (SanitizedCollectionConfig | SanitizedGlobalConfig)[],
): JSONSchema4 {
@@ -68,7 +65,10 @@ function generateEntitySchemas(
}
}
function withNullableType(
/**
* Returns a JSON Schema Type with 'null' added if the field is not required.
*/
export function withNullableJSONSchemaType(
fieldType: JSONSchema4TypeName,
isRequired: boolean,
): JSONSchema4TypeName | JSONSchema4TypeName[] {
@@ -103,7 +103,7 @@ function fieldsToJSONSchema(
case 'code':
case 'email':
case 'date': {
fieldSchema = { type: withNullableType('string', isRequired) }
fieldSchema = { type: withNullableJSONSchemaType('string', isRequired) }
break
}
@@ -111,16 +111,16 @@ function fieldsToJSONSchema(
if (field.hasMany === true) {
fieldSchema = {
items: { type: 'number' },
type: withNullableType('array', isRequired),
type: withNullableJSONSchemaType('array', isRequired),
}
} else {
fieldSchema = { type: withNullableType('number', isRequired) }
fieldSchema = { type: withNullableJSONSchemaType('number', isRequired) }
}
break
}
case 'checkbox': {
fieldSchema = { type: withNullableType('boolean', isRequired) }
fieldSchema = { type: withNullableJSONSchemaType('boolean', isRequired) }
break
}
@@ -132,11 +132,19 @@ function fieldsToJSONSchema(
}
case 'richText': {
fieldSchema = {
items: {
type: 'object',
},
type: withNullableType('array', isRequired),
if (field.editor.outputSchema) {
fieldSchema = field.editor.outputSchema({
field,
isRequired,
})
} else {
// Maintain backwards compatibility with existing rich text editors
fieldSchema = {
items: {
type: 'object',
},
type: withNullableJSONSchemaType('array', isRequired),
}
}
break
@@ -145,7 +153,7 @@ function fieldsToJSONSchema(
case 'radio': {
fieldSchema = {
enum: buildOptionEnums(field.options),
type: withNullableType('string', isRequired),
type: withNullableJSONSchemaType('string', isRequired),
}
break
@@ -160,12 +168,12 @@ function fieldsToJSONSchema(
enum: optionEnums,
type: 'string',
},
type: withNullableType('array', isRequired),
type: withNullableJSONSchemaType('array', isRequired),
}
} else {
fieldSchema = {
enum: optionEnums,
type: withNullableType('string', isRequired),
type: withNullableJSONSchemaType('string', isRequired),
}
}
@@ -184,7 +192,7 @@ function fieldsToJSONSchema(
],
maxItems: 2,
minItems: 2,
type: withNullableType('array', isRequired),
type: withNullableJSONSchemaType('array', isRequired),
}
break
}
@@ -217,7 +225,7 @@ function fieldsToJSONSchema(
}
}),
},
type: withNullableType('array', isRequired),
type: withNullableJSONSchemaType('array', isRequired),
}
} else {
fieldSchema = {
@@ -240,7 +248,7 @@ function fieldsToJSONSchema(
},
},
required: ['value', 'relationTo'],
type: withNullableType('object', isRequired),
type: withNullableJSONSchemaType('object', isRequired),
}
}),
}
@@ -257,13 +265,16 @@ function fieldsToJSONSchema(
},
],
},
type: withNullableType('array', isRequired),
type: withNullableJSONSchemaType('array', isRequired),
}
} else {
fieldSchema = {
oneOf: [
{
type: withNullableType(collectionIDFieldTypes[field.relationTo], isRequired),
type: withNullableJSONSchemaType(
collectionIDFieldTypes[field.relationTo],
isRequired,
),
},
{
$ref: `#/definitions/${field.relationTo}`,
@@ -323,7 +334,7 @@ function fieldsToJSONSchema(
return blockSchema
}),
},
type: withNullableType('array', isRequired),
type: withNullableJSONSchemaType('array', isRequired),
}
break
}
@@ -339,7 +350,7 @@ function fieldsToJSONSchema(
interfaceNameDefinitions,
),
},
type: withNullableType('array', isRequired),
type: withNullableJSONSchemaType('array', isRequired),
}
if (field.interfaceName) {
@@ -497,6 +508,9 @@ export function entityToJSONSchema(
}
}
/**
* This is used for generating the TypeScript types (payload-types.ts) with the payload generate:types command.
*/
export function configToJSONSchema(
config: SanitizedConfig,
defaultIDType?: 'number' | 'text',

View File

@@ -7,6 +7,13 @@ import {
tabHasName,
} from '../fields/config/types'
/**
* Flattens a collection's fields into a single array of fields, as long
* as the fields do not affect data.
*
* @param fields
* @param keepPresentationalFields if true, will skip flattening fields that are presentational only
*/
const flattenFields = (
fields: Field[],
keepPresentationalFields?: boolean,

View File

@@ -100,7 +100,10 @@ export async function getEntityPolicies<T extends Args>(args: T): Promise<Return
if (accessLevel === 'field' && docBeingAccessed === undefined) {
docBeingAccessed = await getEntityDoc()
}
const accessResult = await access({ id, doc: docBeingAccessed, req })
const data = req?.body
const accessResult = await access({ id, data, doc: docBeingAccessed, req })
if (typeof accessResult === 'object' && !disableWhere) {
mutablePolicies[operation] = {

View File

@@ -5,12 +5,24 @@ import type { PayloadRequest } from '../express/types'
* @returns true if beginning a transaction and false when req already has a transaction to use
*/
export async function initTransaction(req: PayloadRequest): Promise<boolean> {
const { payload, transactionID } = req
if (!transactionID && typeof payload.db.beginTransaction === 'function') {
req.transactionID = await payload.db.beginTransaction()
if (req.transactionID) {
return true
}
const { payload, transactionID, transactionIDPromise } = req
if (transactionID) {
// we already have a transaction, we're not in charge of committing it
return false
}
if (transactionIDPromise) {
// wait for whoever else is already creating the transaction
await transactionIDPromise
return false
}
if (typeof payload.db.beginTransaction === 'function') {
// create a new transaction
req.transactionIDPromise = payload.db.beginTransaction().then((transactionID) => {
req.transactionID = transactionID
delete req.transactionIDPromise
})
await req.transactionIDPromise
return !!req.transactionID
}
return false
}

View File

@@ -0,0 +1,29 @@
import type { PayloadRequest } from '../express/types'
/**
* Creates a proxy for the given request that has its own TransactionID
*/
export default function isolateTransactionID(req: PayloadRequest): PayloadRequest {
const delegate = {}
const handler: ProxyHandler<PayloadRequest> = {
deleteProperty(target, p): boolean {
return Reflect.deleteProperty(p === 'transactionID' ? delegate : target, p)
},
get(target, p, receiver) {
return Reflect.get(p === 'transactionID' ? delegate : target, p, receiver)
},
has(target, p) {
return Reflect.has(p === 'transactionID' ? delegate : target, p)
},
set(target, p, newValue, receiver) {
if (p === 'transactionID') {
// in case of transactionID we must ignore any receiver, because
// "If provided and target does not have a setter for propertyKey, the property will be set on receiver instead."
return Reflect.set(delegate, p, newValue)
} else {
return Reflect.set(target, p, newValue, receiver)
}
},
}
return new Proxy(req, handler)
}

View File

@@ -10,14 +10,14 @@
"build:swc": "swc ./src -d ./dist --config-file .swcrc",
"build:types": "tsc --emitDeclarationOnly --outDir dist",
"clean": "rimraf {dist,*.tsbuildinfo}",
"prepublishOnly": "yarn clean && yarn build",
"prepublishOnly": "pnpm clean && pnpm build",
"test": "echo \"No tests available.\""
},
"peerDependencies": {
"@aws-sdk/client-s3": "^3.142.0",
"@aws-sdk/lib-storage": "^3.267.0",
"@azure/storage-blob": "^12.11.0",
"@azure/abort-controller": "^1.0.0",
"@azure/storage-blob": "^12.11.0",
"@google-cloud/storage": "^6.4.1",
"payload": "^1.7.2 || ^2.0.0"
},
@@ -49,6 +49,7 @@
"@azure/storage-blob": "^12.11.0",
"@google-cloud/storage": "^6.4.1",
"@types/express": "^4.17.9",
"@types/find-node-modules": "^2.1.2",
"cross-env": "^7.0.3",
"dotenv": "^8.2.0",
"nodemon": "^2.0.6",
@@ -58,6 +59,7 @@
"webpack": "^5.78.0"
},
"dependencies": {
"find-node-modules": "^2.1.3",
"range-parser": "^1.2.1"
}
}

View File

@@ -1,15 +1,37 @@
import type { Configuration as WebpackConfig } from 'webpack'
import findNodeModules from 'find-node-modules'
import fs from 'fs'
import path from 'path'
const packageName = '@payloadcms/plugin-cloud-storage'
const nodeModulesPaths = findNodeModules({ cwd: __dirname, relative: false })
export const extendWebpackConfig = (existingWebpackConfig: WebpackConfig): WebpackConfig => {
let nodeModulesPath = nodeModulesPaths.find((p) => {
const guess = path.resolve(p, `${packageName}/dist`)
if (fs.existsSync(guess)) {
return true
}
return false
})
if (!nodeModulesPath) {
nodeModulesPath = process.cwd()
}
const newConfig: WebpackConfig = {
...existingWebpackConfig,
resolve: {
...(existingWebpackConfig.resolve || {}),
alias: {
...(existingWebpackConfig.resolve?.alias ? existingWebpackConfig.resolve.alias : {}),
'@payloadcms/plugin-cloud-storage/s3': path.resolve(__dirname, './mock.js'),
'@payloadcms/plugin-cloud-storage/s3$': path.resolve(
nodeModulesPath,
`./${packageName}/dist/adapters/s3/mock.js`,
),
},
fallback: {
...(existingWebpackConfig.resolve?.fallback ? existingWebpackConfig.resolve.fallback : {}),

View File

@@ -29,7 +29,7 @@ export const extendWebpackConfig =
},
}
return Object.entries(options.collections).reduce(
const modifiedConfig = Object.entries(options.collections).reduce(
(resultingWebpackConfig, [slug, collectionOptions]) => {
const matchedCollection = config.collections?.find((coll) => coll.slug === slug)
@@ -47,4 +47,6 @@ export const extendWebpackConfig =
},
newConfig,
)
return modifiedConfig
}

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/plugin-nested-docs",
"version": "1.0.8",
"version": "1.0.9",
"description": "The official Nested Docs plugin for Payload",
"repository": "https://github.com/payloadcms/payload",
"license": "MIT",

View File

@@ -32,7 +32,7 @@ const resaveChildren =
collection: collection.slug,
data: {
...child,
breadcrumbs: populateBreadcrumbs(req, pluginConfig, collection, child),
breadcrumbs: await populateBreadcrumbs(req, pluginConfig, collection, child),
},
depth: 0,
draft: updateAsDraft,

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/plugin-sentry",
"version": "0.0.5",
"version": "0.0.6",
"homepage:": "https://payloadcms.com",
"repository": "git@github.com:payloadcms/plugin-sentry.git",
"description": "Sentry plugin for Payload",

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/richtext-lexical",
"version": "0.1.16",
"version": "0.2.0",
"description": "The officially supported Lexical richtext adapter for Payload",
"repository": "https://github.com/payloadcms/payload",
"license": "MIT",
@@ -19,26 +19,26 @@
},
"dependencies": {
"@faceless-ui/modal": "2.0.1",
"@lexical/headless": "0.12.2",
"@lexical/link": "0.12.2",
"@lexical/list": "0.12.2",
"@lexical/mark": "0.12.2",
"@lexical/markdown": "0.12.2",
"@lexical/react": "0.12.2",
"@lexical/rich-text": "0.12.2",
"@lexical/selection": "0.12.2",
"@lexical/utils": "0.12.2",
"@lexical/headless": "0.12.4",
"@lexical/link": "0.12.4",
"@lexical/list": "0.12.4",
"@lexical/mark": "0.12.4",
"@lexical/markdown": "0.12.4",
"@lexical/react": "0.12.4",
"@lexical/rich-text": "0.12.4",
"@lexical/selection": "0.12.4",
"@lexical/utils": "0.12.4",
"bson-objectid": "2.0.4",
"classnames": "^2.3.2",
"deep-equal": "2.2.3",
"i18next": "22.5.1",
"lexical": "0.12.2",
"lexical": "0.12.4",
"lodash": "4.17.21",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-error-boundary": "^4.0.11",
"react-i18next": "11.18.6",
"ts-essentials": "7.0.3",
"deep-equal": "2.2.2"
"ts-essentials": "7.0.3"
},
"devDependencies": {
"@payloadcms/eslint-config": "workspace:*",
@@ -47,7 +47,7 @@
"payload": "workspace:*"
},
"peerDependencies": {
"payload": "^2.0.14"
"payload": "^2.2.0"
},
"exports": {
".": {

View File

@@ -63,12 +63,7 @@ export const RichTextCell: React.FC<
return $getRoot().getTextContent()
}) || ''
// Limit preview to 150 characters
if (textContent.length > 150) {
setPreview(textContent.slice(0, 150) + '...')
return
}
// Limiting the number of characters shown is done in a CSS rule
setPreview(textContent)
}, [data, editorConfig])

View File

@@ -7,7 +7,6 @@ import { ErrorBoundary } from 'react-error-boundary'
import type { FieldProps } from '../types'
import { defaultRichTextValueV2 } from '../populate/defaultValue'
import { richTextValidateHOC } from '../validate'
import './index.scss'
import { LexicalProvider } from './lexical/LexicalProvider'
@@ -25,7 +24,6 @@ const RichText: React.FC<FieldProps> = (props) => {
style,
width,
},
defaultValue: defaultValueFromProps,
editorConfig,
label,
path: pathFromProps,

View File

@@ -1,4 +1,4 @@
import type { Block, Data, Fields } from 'payload/types'
import type { Block, Data, Field, Fields } from 'payload/types'
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import isDeepEqual from 'deep-equal'
@@ -21,7 +21,8 @@ type Props = {
baseClass: string
block: Block
field: FieldProps
fields: BlockFields
formData: BlockFields
formSchema: Field[]
nodeKey: string
}
@@ -31,7 +32,14 @@ type Props = {
* not the whole document.
*/
export const BlockContent: React.FC<Props> = (props) => {
const { baseClass, block, field, fields, nodeKey } = props
const {
baseClass,
block: { labels },
field,
formData,
formSchema,
nodeKey,
} = props
const { i18n } = useTranslation()
const [editor] = useLexicalComposerContext()
// Used for saving collapsed to preferences (and gettin' it from there again)
@@ -47,9 +55,9 @@ export const BlockContent: React.FC<Props> = (props) => {
const collapsedMap: { [key: string]: boolean } = currentFieldPreferences?.collapsed
if (collapsedMap && collapsedMap[fields.data.id] !== undefined) {
setCollapsed(collapsedMap[fields.data.id])
initialState = collapsedMap[fields.data.id]
if (collapsedMap && collapsedMap[formData.id] !== undefined) {
setCollapsed(collapsedMap[formData.id])
initialState = collapsedMap[formData.id]
}
})
return initialState
@@ -70,13 +78,19 @@ export const BlockContent: React.FC<Props> = (props) => {
const path = '' as const
const onFormChange = useCallback(
({ fields: formFields, formData }: { fields: Fields; formData: Data }) => {
({
fullFieldsWithValues,
newFormData,
}: {
fullFieldsWithValues: Fields
newFormData: Data
}) => {
// Recursively remove all undefined values from even being present in formData, as they will
// cause isDeepEqual to return false if, for example, formData has a key that fields.data
// does not have, even if it's undefined.
// Currently, this happens if a block has another sub-blocks field. Inside of formData, that sub-blocks field has an undefined blockName property.
// Inside of fields.data however, that sub-blocks blockName property does not exist at all.
function removeUndefinedRecursively(obj: any) {
function removeUndefinedRecursively(obj: object) {
Object.keys(obj).forEach((key) => {
if (obj[key] && typeof obj[key] === 'object') {
removeUndefinedRecursively(obj[key])
@@ -85,26 +99,30 @@ export const BlockContent: React.FC<Props> = (props) => {
}
})
}
removeUndefinedRecursively(newFormData)
removeUndefinedRecursively(formData)
removeUndefinedRecursively(fields.data)
// Only update if the data has actually changed. Otherwise, we may be triggering an unnecessary value change,
// which would trigger the "Leave without saving" dialog unnecessarily
if (!isDeepEqual(fields.data, formData)) {
editor.update(() => {
const node: BlockNode = $getNodeByKey(nodeKey)
if (node) {
node.setFields({
data: formData as any,
})
}
})
if (!isDeepEqual(formData, newFormData)) {
// Running this in the next tick in the meantime fixes this issue: https://github.com/payloadcms/payload/issues/4108
// I don't know why. When this is called immediately, it might focus out of a nested lexical editor field if an update is made there.
// My hypothesis is that the nested editor might not have fully finished its update cycle yet. By updating in the next tick, we
// ensure that the nested editor has finished its update cycle before we update the block node.
setTimeout(() => {
editor.update(() => {
const node: BlockNode = $getNodeByKey(nodeKey)
if (node) {
node.setFields(newFormData as BlockFields)
}
})
}, 0)
}
// update error count
if (hasSubmitted) {
let rowErrorCount = 0
for (const formField of Object.values(formFields)) {
for (const formField of Object.values(fullFieldsWithValues)) {
if (formField?.valid === false) {
rowErrorCount++
}
@@ -112,7 +130,7 @@ export const BlockContent: React.FC<Props> = (props) => {
setErrorCount(rowErrorCount)
}
},
[editor, nodeKey, hasSubmitted],
[editor, nodeKey, hasSubmitted, formData],
)
const onCollapsedChange = useCallback(() => {
@@ -124,13 +142,13 @@ export const BlockContent: React.FC<Props> = (props) => {
const newCollapsed: { [key: string]: boolean } =
collapsedMap && collapsedMap?.size ? collapsedMap : {}
newCollapsed[fields.data.id] = !collapsed
newCollapsed[formData.id] = !collapsed
setDocFieldPreferences(field.name, {
collapsed: newCollapsed,
})
})
}, [collapsed, getDocPreferences, field.name, setDocFieldPreferences, fields.data.id])
}, [collapsed, getDocPreferences, field.name, setDocFieldPreferences, formData.id])
const removeBlock = useCallback(() => {
editor.update(() => {
@@ -138,6 +156,11 @@ export const BlockContent: React.FC<Props> = (props) => {
})
}, [editor, nodeKey])
const fieldSchemaWithPath = formSchema.map((field) => ({
...field,
path: createNestedFieldPath(null, field),
}))
return (
<React.Fragment>
<Collapsible
@@ -148,10 +171,10 @@ export const BlockContent: React.FC<Props> = (props) => {
<div className={`${baseClass}__block-header`}>
<div>
<Pill
className={`${baseClass}__block-pill ${baseClass}__block-pill-${fields?.data?.blockType}`}
className={`${baseClass}__block-pill ${baseClass}__block-pill-${formData?.blockType}`}
pillStyle="white"
>
{getTranslation(block.labels.singular, i18n)}
{getTranslation(labels.singular, i18n)}
</Pill>
<SectionTitle path={`${path}blockName`} readOnly={field?.admin?.readOnly} />
{fieldHasErrors && <ErrorPill count={errorCount} withMessage />}
@@ -180,25 +203,16 @@ export const BlockContent: React.FC<Props> = (props) => {
>
<RenderFields
className={`${baseClass}__fields`}
fieldSchema={block.fields.map((field) => ({
...field,
path: createNestedFieldPath(null, field),
}))}
fieldSchema={fieldSchemaWithPath}
fieldTypes={field.fieldTypes}
forceRender
margins="small"
permissions={field.permissions?.blocks?.[fields?.data?.blockType]?.fields}
permissions={field.permissions?.blocks?.[formData?.blockType]?.fields}
readOnly={field.admin.readOnly}
/>
</Collapsible>
<FormSavePlugin
fieldSchema={block.fields.map((field) => ({
...field,
path: createNestedFieldPath(null, field),
}))}
onChange={onFormChange}
/>
<FormSavePlugin onChange={onFormChange} />
</React.Fragment>
)
}

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