Compare commits
144 Commits
live-previ
...
live-previ
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b39b52dbd3 | ||
|
|
7bfdb2627a | ||
|
|
8f5867e876 | ||
|
|
45a3e31c95 | ||
|
|
176550d26b | ||
|
|
53958d4662 | ||
|
|
a0859114eb | ||
|
|
4adc30b034 | ||
|
|
ff61d5a099 | ||
|
|
9cc88bb474 | ||
|
|
57fc211674 | ||
|
|
9da9b1fc50 | ||
|
|
30d050ef86 | ||
|
|
9beb9c8627 | ||
|
|
224cddd045 | ||
|
|
3502ce720b | ||
|
|
b8fa61942e | ||
|
|
d49bb4351f | ||
|
|
542096361f | ||
|
|
66679fbdd6 | ||
|
|
d4f28b16b4 | ||
|
|
cd07873fc5 | ||
|
|
6d28fc46bd | ||
|
|
8e903053e2 | ||
|
|
7e1052fd98 | ||
|
|
b4af95f894 | ||
|
|
381c158b03 | ||
|
|
3514bfbdae | ||
|
|
4cfe473627 | ||
|
|
c1eb9d1727 | ||
|
|
37b765cce8 | ||
|
|
5bf64933b4 | ||
|
|
094d02ce1d | ||
|
|
1fe4f4c5f4 | ||
|
|
35ce0ebc83 | ||
|
|
530c825f80 | ||
|
|
308979f31d | ||
|
|
2122242192 | ||
|
|
40c8909ee0 | ||
|
|
babe3dba6a | ||
|
|
9bb7a88526 | ||
|
|
098e389147 | ||
|
|
b56f1f4f2a | ||
|
|
443847ec71 | ||
|
|
26f6b37a20 | ||
|
|
1dcd3a2782 | ||
|
|
6f59257574 | ||
|
|
65575d3573 | ||
|
|
cbeb0a8bc7 | ||
|
|
ad62db01e7 | ||
|
|
42cba2e3a1 | ||
|
|
1401718b3b | ||
|
|
712647d741 | ||
|
|
c8d2b2b60e | ||
|
|
aab2407112 | ||
|
|
d439bf3011 | ||
|
|
62ca71fbc4 | ||
|
|
e50fa9ca8f | ||
|
|
ed7aca6525 | ||
|
|
98ccd05dd6 | ||
|
|
051bced3b5 | ||
|
|
79f08baf2f | ||
|
|
d6b63da617 | ||
|
|
d512e9382d | ||
|
|
b17cafc7be | ||
|
|
24dacd6712 | ||
|
|
6ea29094ba | ||
|
|
f27407ce7c | ||
|
|
9ae65fa791 | ||
|
|
3d2b62b210 | ||
|
|
6364afb1dd | ||
|
|
56a4692662 | ||
|
|
ef6b8e4235 | ||
|
|
5f5290341a | ||
|
|
62403584ad | ||
|
|
19fcfc27af | ||
|
|
dcf14f5f71 | ||
|
|
3a784a06cc | ||
|
|
6eeae9d53b | ||
|
|
6044f810bd | ||
|
|
e68ca9363f | ||
|
|
9963b8d945 | ||
|
|
9afb838182 | ||
|
|
2dad129022 | ||
|
|
6af1c4d45d | ||
|
|
4e41dd1bf2 | ||
|
|
de02490231 | ||
|
|
8a7b41721a | ||
|
|
1510baf46e | ||
|
|
0672e864f3 | ||
|
|
c10db332cd | ||
|
|
0af9c4d398 | ||
|
|
fc137c0f53 | ||
|
|
a24f2be4a6 | ||
|
|
57999adfe2 | ||
|
|
7857043d03 | ||
|
|
3b93af734b | ||
|
|
50fab902bd | ||
|
|
724d80b7f4 | ||
|
|
b406e6afb9 | ||
|
|
3f46b21eb2 | ||
|
|
eed4f4361c | ||
|
|
05f3169a75 | ||
|
|
94f1443ce4 | ||
|
|
fc26275b7a | ||
|
|
e57f5e2aa0 | ||
|
|
dbfc83520c | ||
|
|
c068a8784e | ||
|
|
989c10e0e0 | ||
|
|
3bf2b7a3fe | ||
|
|
a6b486007d | ||
|
|
4e03ee7079 | ||
|
|
b91711a74a | ||
|
|
191c13a409 | ||
|
|
b210af4696 | ||
|
|
8cebd2ccce | ||
|
|
195a952c43 | ||
|
|
4bc5fa7086 | ||
|
|
2c8d34d2aa | ||
|
|
4ec5643dd7 | ||
|
|
45e9a559bb | ||
|
|
d6233cbf42 | ||
|
|
ad3e23b345 | ||
|
|
782e118569 | ||
|
|
dbfe4af993 | ||
|
|
859c2f4a6d | ||
|
|
a34d0f8274 | ||
|
|
967eff1aab | ||
|
|
b7041d6ab1 | ||
|
|
78b7bd62cd | ||
|
|
7329b1babd | ||
|
|
c87969b7f9 | ||
|
|
09f17f4450 | ||
|
|
615702b858 | ||
|
|
f0642ce031 | ||
|
|
56db87d2ec | ||
|
|
45c42724a4 | ||
|
|
a6d5f2e3de | ||
|
|
73b8549ef5 | ||
|
|
e22b95bdf3 | ||
|
|
56ddd2c388 | ||
|
|
803a37eaa9 | ||
|
|
d308bb3421 | ||
|
|
cbc4752ecb |
4
.github/PULL_REQUEST_TEMPLATE.md
vendored
4
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -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:
|
||||
|
||||
106
CHANGELOG.md
106
CHANGELOG.md
@@ -1,3 +1,109 @@
|
||||
## [2.3.0](https://github.com/payloadcms/payload/compare/v2.2.2...v2.3.0) (2023-11-30)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add serbian (latin and cyrillic) translations ([#4268](https://github.com/payloadcms/payload/issues/4268)) ([40c8909](https://github.com/payloadcms/payload/commit/40c8909ee0003d45a1afa4524ade557268d01067))
|
||||
* **db-mongodb:** search for migrations dir intelligently ([530c825](https://github.com/payloadcms/payload/commit/530c825f806708df8672e66c8e9c559c5e625e5e))
|
||||
* **db-postgres:** search for migrations dir intelligently ([308979f](https://github.com/payloadcms/payload/commit/308979f31d27979955a52f32be4ea33849b48f30))
|
||||
* **live-preview:** batches api requests ([#4315](https://github.com/payloadcms/payload/issues/4315)) ([d49bb43](https://github.com/payloadcms/payload/commit/d49bb4351f22f17f68477c3145594abbb60fab05))
|
||||
* relationship sortOptions property ([#4301](https://github.com/payloadcms/payload/issues/4301)) ([224cddd](https://github.com/payloadcms/payload/commit/224cddd04573eff578ea3fa9ea5419f28b66c613))
|
||||
* support migrations with js files ([2122242](https://github.com/payloadcms/payload/commit/21222421929ae19728c31bdccc84995ce3951224))
|
||||
* support OAuth 2.0 format Authorization: Bearer tokens in headers ([c1eb9d1](https://github.com/payloadcms/payload/commit/c1eb9d1727daf96375e73943882621127b78e593))
|
||||
* useDocumentEvents ([#4284](https://github.com/payloadcms/payload/issues/4284)) ([9bb7a88](https://github.com/payloadcms/payload/commit/9bb7a88526569a726de468de6b2010d52169ea77))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **db-postgres:** allow for nested block fields to be queried ([#4237](https://github.com/payloadcms/payload/issues/4237)) ([cd07873](https://github.com/payloadcms/payload/commit/cd07873fc544766b4aeeff873dfb8d6e3e97e9dc))
|
||||
* **db-postgres:** error saving nested arrays with versions ([#4302](https://github.com/payloadcms/payload/issues/4302)) ([3514bfb](https://github.com/payloadcms/payload/commit/3514bfbdaee99341ae739d03591cb63bd9415fe3))
|
||||
* duplicate documents with required localized fields ([#4236](https://github.com/payloadcms/payload/issues/4236)) ([9da9b1f](https://github.com/payloadcms/payload/commit/9da9b1fc5050d4f29bcf6dce2f22027834aaf698))
|
||||
* incorrect key property in Tabs field component ([#4311](https://github.com/payloadcms/payload/issues/4311)) ([3502ce7](https://github.com/payloadcms/payload/commit/3502ce720b3020eed5fc733884b525303faa4c15)), closes [#4282](https://github.com/payloadcms/payload/issues/4282)
|
||||
* **live-preview-react:** removes payload from peer deps ([7e1052f](https://github.com/payloadcms/payload/commit/7e1052fd98c88a4d68af08f98ccc8936edb8ebf6))
|
||||
* **live-preview:** compounds merge results ([#4306](https://github.com/payloadcms/payload/issues/4306)) ([381c158](https://github.com/payloadcms/payload/commit/381c158b0303b515164ae487b0ce7e555ae1a08d))
|
||||
* **live-preview:** property resets rte nodes ([#4313](https://github.com/payloadcms/payload/issues/4313)) ([66679fb](https://github.com/payloadcms/payload/commit/66679fbdd6f804bff8a58d9504c226c9fb8a57a0))
|
||||
* **live-preview:** re-populates externally updated relationships ([#4287](https://github.com/payloadcms/payload/issues/4287)) ([57fc211](https://github.com/payloadcms/payload/commit/57fc2116749059bc55161897cf139031926035ec))
|
||||
* **live-preview:** removes payload from peer deps ([b4af95f](https://github.com/payloadcms/payload/commit/b4af95f894b5f6614bace38ef79e7148e084bd3b))
|
||||
* properly exports useDocumentsEvents hook ([#4314](https://github.com/payloadcms/payload/issues/4314)) ([5420963](https://github.com/payloadcms/payload/commit/542096361f0c13aed9c6a7d971e2c47047d8e2d2))
|
||||
* properly sets tabs key in fieldSchemaToJSON ([#4317](https://github.com/payloadcms/payload/issues/4317)) ([9cc88bb](https://github.com/payloadcms/payload/commit/9cc88bb47443ecdf525f4c99d9f13d81c141c471))
|
||||
* **richtext-lexical:** HTML Converter field not working inside of tabs ([#4293](https://github.com/payloadcms/payload/issues/4293)) ([5bf6493](https://github.com/payloadcms/payload/commit/5bf64933b4b99a0ac8ef7d1d91d0165a16636a9f))
|
||||
* **richtext-lexical:** re-use payload population logic to fix population-related issues ([#4291](https://github.com/payloadcms/payload/issues/4291)) ([094d02c](https://github.com/payloadcms/payload/commit/094d02ce1d85106470a1a8c6ffe9050873f2e57a))
|
||||
* **richtext-slate:** add use client to top of tsx files importing from payload core ([#4312](https://github.com/payloadcms/payload/issues/4312)) ([d4f28b1](https://github.com/payloadcms/payload/commit/d4f28b16b4d42f224e9c5e4254f9ec55107a2f97))
|
||||
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
### ⚠️ @payloadcms/richtext-lexical
|
||||
|
||||
The `SlashMenuGroup` and `SlashMenuOption` classes have changed. If you have any custom lexical Features which are adding new slash menu entries, this will be a breaking change for you. If not, no action is required from your side.
|
||||
|
||||
Here are the breaking changes and how to migrate:
|
||||
|
||||
1. The `SlashMenuOption`'s first argument is now used as a `key` and not as a display name. Additionally, a new, optional `displayName` property is added which will serve as the display name. Make sure your `key` does not contain any spaces or special characters.
|
||||
2. The `title` property of `SlashMenuGroup` has been replaced by a new, mandatory `key` and an optional `displayName` property. To migrate, you will have to remove the `title` property and add a `key` property instead - make sure you do not use spaces or special characters in the `key`.
|
||||
3. Additionally, if you have custom styles targeting elements inside of slash or floating-select-toolbar menus, you will have to adjust those too, as the CSS classes changed
|
||||
|
||||
[This is an example of performing these updates](
|
||||
https://github.com/payloadcms/payload/pull/4257/files#diff-dc2e7f503dd7076dff1d810da7ec77b8fc6a9e41127df4a417dece1b6e1587a0L61)
|
||||
|
||||
## [2.2.2](https://github.com/payloadcms/payload/compare/v2.2.1...v2.2.2) (2023-11-27)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **i18n:** adds Korean translation ([#4258](https://github.com/payloadcms/payload/issues/4258)) ([1401718](https://github.com/payloadcms/payload/commit/1401718b3b549ce1454389a982474dbe159eb61f))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* number field validation ([#4233](https://github.com/payloadcms/payload/issues/4233)) ([19fcfc2](https://github.com/payloadcms/payload/commit/19fcfc27af2ecb68ff989dcaed19b7b7d041a322))
|
||||
* passes date options to the react-datepicker in filter UI, removes duplicate options from operators select ([#4225](https://github.com/payloadcms/payload/issues/4225)) ([3d2b62b](https://github.com/payloadcms/payload/commit/3d2b62b2100e36a54adc6a675257a4d671fdd469))
|
||||
* prevent json data getting reset when switching tabs ([#4123](https://github.com/payloadcms/payload/issues/4123)) ([1dcd3a2](https://github.com/payloadcms/payload/commit/1dcd3a27825ed9d276b997a66f84bb2c05e87955))
|
||||
* transactions broken within doc access ([443847e](https://github.com/payloadcms/payload/commit/443847ec716a3b87032d9d1904b6c90aadd47197))
|
||||
* typo in polish translations ([#4234](https://github.com/payloadcms/payload/issues/4234)) ([56a4692](https://github.com/payloadcms/payload/commit/56a469266207ef83053b0c9176d1be4fc26087e6))
|
||||
|
||||
## [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)
|
||||
|
||||
|
||||
|
||||
20
README.md
20
README.md
@@ -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>
|
||||
|
||||
<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>
|
||||
|
||||
<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>
|
||||
|
||||
<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>
|
||||
|
||||
@@ -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]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
};
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@@ -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
|
||||
@@ -784,3 +784,33 @@ const MyComponent: React.FC = () => {
|
||||
</button>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### useDocumentEvents
|
||||
|
||||
The `useDocumentEvents` hook provides a way of subscribing to cross-document events, such as updates made to nested documents within a drawer. This hook will report document events that are outside the scope of the document currently being edited. This hook provides the following:
|
||||
|
||||
| Property | Description |
|
||||
|---------------------------|-------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| **`mostRecentUpdate`** | An object containing the most recently updated document. It contains the `entitySlug`, `id` (if collection), and `updatedAt` properties |
|
||||
| **`reportUpdate`** | A method used to report updates to documents. It accepts the same arguments as the `mostRecentUpdate` property. |
|
||||
|
||||
**Example:**
|
||||
|
||||
```tsx
|
||||
import { useDocumentEvents } from 'payload/components/hooks'
|
||||
|
||||
const ListenForUpdates: React.FC = () => {
|
||||
const { mostRecentUpdate } = useDocumentEvents()
|
||||
|
||||
return (
|
||||
<span>
|
||||
{JSON.stringify(mostRecentUpdate)}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
<Banner type="info">
|
||||
Right now the `useDocumentEvents` hook only tracks recently updated documents, but in the future it will track more document-related events as needed, such as document creation, deletion, etc.
|
||||
</Banner>
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -82,7 +82,7 @@ Once enabled, each document that is created within the Collection can be thought
|
||||
|
||||
### Token-based auth
|
||||
|
||||
Successfully logging in returns a `JWT` (JSON web token) which is how a user will identify themselves to Payload. By providing this JWT via either an HTTP-only cookie or an `Authorization` header, Payload will automatically identify the user and add its user JWT data to the Express `req`, which is available throughout Payload including within access control, hooks, and more.
|
||||
Successfully logging in returns a `JWT` (JSON web token) which is how a user will identify themselves to Payload. By providing this JWT via either an HTTP-only cookie or an `Authorization: JWT` or `Authorization: Bearer` header, Payload will automatically identify the user and add its user JWT data to the Express `req`, which is available throughout Payload including within access control, hooks, and more.
|
||||
|
||||
You can specify what data gets encoded to the JWT token by setting `saveToJWT` to true in your auth collection fields. If you wish to use a different key other than the field `name`, you can provide it to `saveToJWT` as a string. It is also possible to use `saveToJWT` on fields that are nested in inside groups and tabs. If a group has a `saveToJWT` set it will include the object with all sub-fields in the token. You can set `saveToJWT: false` for any fields you wish to omit. If a field inside a group has `saveToJWT` set, but the group does not, the field will be included at the top level of the token.
|
||||
|
||||
|
||||
@@ -59,7 +59,7 @@ export const Orders: CollectionConfig = {
|
||||
#### More collection config examples
|
||||
|
||||
You can find an assortment
|
||||
of [example collection configs](https://github.com/payloadcms/public-demo/tree/master/src/collections) in the Public
|
||||
of [example collection configs](https://github.com/payloadcms/public-demo/tree/master/src/payload/collections) in the Public
|
||||
Demo source code on GitHub.
|
||||
|
||||
### Admin options
|
||||
|
||||
@@ -59,7 +59,7 @@ export default Nav
|
||||
|
||||
#### Global config example
|
||||
|
||||
You can find an [example Global config](https://github.com/payloadcms/public-demo/blob/master/src/globals/MainMenu.ts) in the Public Demo source code on GitHub.
|
||||
You can find an [example Global config](https://github.com/payloadcms/public-demo/blob/master/src/payload/globals/MainMenu.ts) in the Public Demo source code on GitHub.
|
||||
|
||||
### Admin options
|
||||
|
||||
|
||||
@@ -46,8 +46,14 @@ export async function down({ payload }: MigrateDownArgs): Promise<void> {
|
||||
};
|
||||
```
|
||||
|
||||
### Migrations Directory
|
||||
|
||||
Each DB adapter has an optional property `migrationDir` where you can override where you want your migrations to be stored/read. If this is not specified, Payload will check the default and possibly make a best effort to find your migrations directory by searching in common locations ie. `./src/migrations`, `./dist/igrations`, `./migrations`, etc.
|
||||
|
||||
All database adapters should implement similar migration patterns, but there will be small differences based on the adapter and its specific needs. Below is a list of all migration commands that should be supported by your database adapter.
|
||||
|
||||
## Commands
|
||||
|
||||
### Migrate
|
||||
|
||||
The `migrate` command will run any migrations that have not yet been run.
|
||||
|
||||
@@ -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'`. |
|
||||
|
||||
|
||||
@@ -70,6 +70,43 @@ Set to `true` if you'd like this field to be sortable within the Admin UI using
|
||||
|
||||
Set to `false` if you'd like to disable the ability to create new documents from within the relationship field (hides the "Add new" button in the admin UI).
|
||||
|
||||
**`sortOptions`**
|
||||
|
||||
The `sortOptions` property allows you to define a default sorting order for the options within a Relationship field's dropdown. This can be particularly useful for ensuring that the most relevant options are presented first to the user.
|
||||
|
||||
You can specify `sortOptions` in two ways:
|
||||
|
||||
**As a string:**
|
||||
|
||||
Provide a string to define a global default sort field for all relationship field dropdowns across different collections. You can prefix the field name with a minus symbol ("-") to sort in descending order.
|
||||
|
||||
Example:
|
||||
|
||||
```ts
|
||||
sortOptions: 'fieldName',
|
||||
```
|
||||
This configuration will sort all relationship field dropdowns by `"fieldName"` in ascending order.
|
||||
|
||||
**As an object :**
|
||||
|
||||
Specify an object where keys are collection slugs and values are strings representing the field names to sort by. This allows for different sorting fields for each collection's relationship dropdown.
|
||||
|
||||
Example:
|
||||
|
||||
```ts
|
||||
sortOptions: {
|
||||
"pages": "fieldName1",
|
||||
"posts": "-fieldName2",
|
||||
"categories": "fieldName3"
|
||||
}
|
||||
```
|
||||
In this configuration:
|
||||
- Dropdowns related to `pages` will be sorted by `"fieldName1"` in ascending order.
|
||||
- Dropdowns for `posts` will use `"fieldName2"` for sorting in descending order (noted by the "-" prefix).
|
||||
- Dropdowns associated with `categories` will sort based on `"fieldName3"` in ascending order.
|
||||
|
||||
Note: If `sortOptions` is not defined, the default sorting behavior of the Relationship field dropdown will be used.
|
||||
|
||||
### Filtering relationship options
|
||||
|
||||
Options can be dynamically limited by supplying a [query constraint](/docs/queries/overview), which will be used both for validating input and filtering available relationships in the UI.
|
||||
|
||||
@@ -187,3 +187,55 @@ For a working demonstration of this, check out the official [Live Preview Exampl
|
||||
- [Next.js App Router](https://github.com/payloadcms/payload/tree/main/examples/live-preview/next-app)
|
||||
- [Next.js Pages Router](https://github.com/payloadcms/payload/tree/main/examples/live-preview/next-pages)
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Relationships and/or uploads are not populating
|
||||
|
||||
If you are using relationships or uploads in your front-end application, and your front-end application runs on a different domain than your Payload server, you may need to configure [CORS](../configuration/overview) to allow requests to be made between the two domains. This includes sites that are running on a different port or subdomain. Similarly, if you are protecting resources behind user authentication, you may also need to configure [CSRF](../authentication/overview#csrf-protection) to allow cookies to be sent between the two domains. For example:
|
||||
|
||||
```ts
|
||||
// payload.config.ts
|
||||
{
|
||||
// ...
|
||||
// If your site is running on a different domain than your Payload server,
|
||||
// This will allows requests to be made between the two domains
|
||||
cors: {
|
||||
[
|
||||
'http://localhost:3001' // Your front-end application
|
||||
],
|
||||
},
|
||||
// If you are protecting resources behind user authentication,
|
||||
// This will allow cookies to be sent between the two domains
|
||||
csrf: {
|
||||
[
|
||||
'http://localhost:3001' // Your front-end application
|
||||
],
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### Relationships and/or uploads disappear after editing a document
|
||||
|
||||
It is possible that either you are setting an improper [`depth`](../getting-started/concepts#depth) in your initial request and/or your `useLivePreview` hook, or they're mismatched. Ensure that the `depth` parameter is set to the correct value, and that it matches exactly in both places. For example:
|
||||
|
||||
```tsx
|
||||
// Your initial request
|
||||
const { docs } = await payload.find({
|
||||
collection: 'pages',
|
||||
depth: 1, // Ensure this is set to the proper depth for your application
|
||||
where: {
|
||||
slug: {
|
||||
equals: 'home',
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
```tsx
|
||||
// Your hook
|
||||
const { data } = useLivePreview<PageType>({
|
||||
initialData: initialPage,
|
||||
serverURL: PAYLOAD_SERVER_URL,
|
||||
depth: 1, // Ensure this matches the depth of your initial request
|
||||
})
|
||||
```
|
||||
|
||||
@@ -12,6 +12,10 @@ Live Preview works by rendering an iframe on the page that loads your front-end
|
||||
|
||||
{/* IMAGE OF LIVE PREVIEW HERE */}
|
||||
|
||||
<Banner type="warning">
|
||||
Live Preview is currently in beta. You may use this feature in production, but please be aware that it is subject to change and may not be fully stable for all use cases. If you encounter any issues, please [report them](https://github.com/payloadcms/payload/issues/new?assignees=jacobsfletch&labels=possible-bug&projects=&title=Live%20Preview&template=1.bug_report.yml) with as much detail as possible.
|
||||
</Banner>
|
||||
|
||||
## Setup
|
||||
|
||||
Setting up Live Preview is easy. You first need to enable it through the `admin.livePreview` property of your Payload config. It takes the following options:
|
||||
|
||||
@@ -6,7 +6,9 @@ desc: The Payload Local API allows you to interact with your database and execut
|
||||
keywords: local api, config, configuration, documentation, Content Management System, cms, headless, javascript, node, react, express
|
||||
---
|
||||
|
||||
The Payload Local API gives you the ability to execute the same operations that are available through REST and GraphQL within Node, directly on your server. Here, you don't need to deal with server latency or network speed whatsoever and can interact directly with your database.
|
||||
The Payload Local API gives you the ability to execute the same operations that are available through REST and GraphQL
|
||||
within Node, directly on your server. Here, you don't need to deal with server latency or network speed whatsoever and
|
||||
can interact directly with your database.
|
||||
|
||||
<Banner type="success">
|
||||
<strong>Tip:</strong>
|
||||
@@ -30,7 +32,9 @@ You can gain access to the currently running `payload` object via two ways:
|
||||
|
||||
##### Importing it
|
||||
|
||||
You can import or require `payload` into your own files after it's been initialized, but you need to make sure that your `import` / `require` statements come **after** you call `payload.init()`—otherwise Payload won't have been initialized yet. That might be obvious. To us, it's usually not.
|
||||
You can import or require `payload` into your own files after it's been initialized, but you need to make sure that
|
||||
your `import` / `require` statements come **after** you call `payload.init()`—otherwise Payload won't have been
|
||||
initialized yet. That might be obvious. To us, it's usually not.
|
||||
|
||||
Example:
|
||||
|
||||
@@ -47,7 +51,8 @@ const afterChangeHook: CollectionAfterChangeHook = async () => {
|
||||
|
||||
##### Accessing from the `req`
|
||||
|
||||
Payload is available anywhere you have access to the Express `req` - including within your access control and hook functions.
|
||||
Payload is available anywhere you have access to the Express `req` - including within your access control and hook
|
||||
functions.
|
||||
|
||||
Example:
|
||||
|
||||
@@ -61,10 +66,11 @@ const afterChangeHook: CollectionAfterChangeHook = async ({ req: { payload } })
|
||||
|
||||
### Local options available
|
||||
|
||||
You can specify more options within the Local API vs. REST or GraphQL due to the server-only context that they are executed in.
|
||||
You can specify more options within the Local API vs. REST or GraphQL due to the server-only context that they are
|
||||
executed in.
|
||||
|
||||
| Local Option | Description |
|
||||
| ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
|--------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `collection` | Required for Collection operations. Specifies the Collection slug to operate against. |
|
||||
| `data` | The data to use within the operation. Required for `create`, `update`. |
|
||||
| `depth` | [Control auto-population](/docs/getting-started/concepts#depth) of nested relationship and upload fields. |
|
||||
@@ -268,7 +274,8 @@ const result = await payload.delete({
|
||||
|
||||
## Auth Operations
|
||||
|
||||
If a collection has [`Authentication`](/docs/authentication/overview) enabled, additional Local API operations will be available:
|
||||
If a collection has [`Authentication`](/docs/authentication/overview) enabled, additional Local API operations will be
|
||||
available:
|
||||
|
||||
#### Login
|
||||
|
||||
@@ -319,10 +326,11 @@ const token = await payload.forgotPassword({
|
||||
// token: 'o38jf0q34jfij43f3f...', // JWT used for auth
|
||||
// user: { ... } // the user document that just logged in
|
||||
// }
|
||||
const result = await payload.forgotPassword({
|
||||
const result = await payload.resetPassword({
|
||||
collection: 'users', // required
|
||||
data: {
|
||||
// required
|
||||
password: req.body.password, // the new password to set
|
||||
token: 'afh3o2jf2p3f...', // the token generated from the forgotPassword operation
|
||||
},
|
||||
req: req, // pass an Express `req` which will be provided to all hooks
|
||||
@@ -402,7 +410,9 @@ const result = await payload.updateGlobal({
|
||||
|
||||
## Next.js Conflict with Local API
|
||||
|
||||
There is a known issue when using the Local API with Next.js version `13.4.13` and higher. Next.js executes within a separate child process, and Payload has not been initalized yet in these instances. That means that unless you explicitly initialize Payload within your operation, it will not be running and return no data / an empty object.
|
||||
There is a known issue when using the Local API with Next.js version `13.4.13` and higher. Next.js executes within a
|
||||
separate child process, and Payload has not been initalized yet in these instances. That means that unless you
|
||||
explicitly initialize Payload within your operation, it will not be running and return no data / an empty object.
|
||||
|
||||
As a workaround, we recommend leveraging the following pattern to determine and ensure Payload is initalized:
|
||||
|
||||
@@ -462,7 +472,8 @@ export const getPayloadClient = async ({ initOptions, seed }: Args = {}): Promis
|
||||
}
|
||||
```
|
||||
|
||||
To checkout how this works in a project, take a look at our [custom server example](https://github.com/payloadcms/payload/blob/master/examples/custom-server/src/getPayload.ts).
|
||||
To checkout how this works in a project, take a look at
|
||||
our [custom server example](https://github.com/payloadcms/payload/blob/master/examples/custom-server/src/getPayload.ts).
|
||||
|
||||
## Example Script using Local API
|
||||
|
||||
|
||||
@@ -153,7 +153,7 @@ Here's an overview of all the included features:
|
||||
| **`HeadingFeature`** | Yes | Adds Heading Nodes (by default, H1 - H6, but that can be customized) |
|
||||
| **`AlignFeature`** | Yes | Allows you to align text left, centered and right |
|
||||
| **`IndentFeature`** | Yes | Allows you to indent text with the tab key |
|
||||
| **`UnoderedListFeature`** | Yes | Adds unordered lists (ol) |
|
||||
| **`UnorderedListFeature`** | Yes | Adds unordered lists (ol) |
|
||||
| **`OrderedListFeature`** | Yes | Adds ordered lists (ul) |
|
||||
| **`CheckListFeature`** | Yes | Adds checklists |
|
||||
| **`LinkFeature`** | Yes | Allows you to create internal and external links |
|
||||
@@ -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.
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
module.exports = {
|
||||
config: () => null,
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -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 />}
|
||||
|
||||
@@ -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 />}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -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 />}
|
||||
|
||||
@@ -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 />}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/bundler-vite",
|
||||
"version": "0.1.4",
|
||||
"version": "0.1.5",
|
||||
"description": "The officially supported Vite bundler adapter for Payload",
|
||||
"repository": "https://github.com/payloadcms/payload",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -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}`] = `''`
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ CLI for easily starting new Payload project
|
||||
|
||||
$ npx create-payload-app
|
||||
$ npx create-payload-app my-project
|
||||
$ npx create-payload-app -n my-project -t blog
|
||||
$ npx create-payload-app -n my-project -t website
|
||||
|
||||
OPTIONS
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-mongodb",
|
||||
"version": "1.0.8",
|
||||
"version": "1.1.0",
|
||||
"description": "The officially supported MongoDB database adapter for Payload",
|
||||
"repository": "https://github.com/payloadcms/payload",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { ClientSession, ConnectOptions, Connection } from 'mongoose'
|
||||
import type { Payload } from 'payload'
|
||||
import type { BaseDatabaseAdapter } from 'payload/database'
|
||||
|
||||
import fs from 'fs'
|
||||
import mongoose from 'mongoose'
|
||||
import path from 'path'
|
||||
import { createDatabaseAdapter } from 'payload/database'
|
||||
@@ -94,7 +95,7 @@ export function mongooseAdapter({
|
||||
url,
|
||||
}: Args): MongooseAdapterResult {
|
||||
function adapter({ payload }: { payload: Payload }) {
|
||||
const migrationDir = migrationDirArg || path.resolve(process.cwd(), 'src/migrations')
|
||||
const migrationDir = findMigrationDir(migrationDirArg)
|
||||
mongoose.set('strictQuery', false)
|
||||
|
||||
extendWebpackConfig(payload.config)
|
||||
@@ -149,3 +150,42 @@ export function mongooseAdapter({
|
||||
|
||||
return adapter
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to find migrations directory.
|
||||
*
|
||||
* Checks for the following directories in order:
|
||||
* - `migrationDir` argument from Payload config
|
||||
* - `src/migrations`
|
||||
* - `dist/migrations`
|
||||
* - `migrations`
|
||||
*
|
||||
* Defaults to `src/migrations`
|
||||
*
|
||||
* @param migrationDir
|
||||
* @returns
|
||||
*/
|
||||
function findMigrationDir(migrationDir?: string): string {
|
||||
const cwd = process.cwd()
|
||||
const srcDir = path.resolve(cwd, 'src/migrations')
|
||||
const distDir = path.resolve(cwd, 'dist/migrations')
|
||||
const relativeMigrations = path.resolve(cwd, 'migrations')
|
||||
|
||||
// Use arg if provided
|
||||
if (migrationDir) return migrationDir
|
||||
|
||||
// Check other common locations
|
||||
if (fs.existsSync(srcDir)) {
|
||||
return srcDir
|
||||
}
|
||||
|
||||
if (fs.existsSync(distDir)) {
|
||||
return distDir
|
||||
}
|
||||
|
||||
if (fs.existsSync(relativeMigrations)) {
|
||||
return relativeMigrations
|
||||
}
|
||||
|
||||
return srcDir
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-postgres",
|
||||
"version": "0.1.13",
|
||||
"version": "0.2.0",
|
||||
"description": "The officially supported Postgres database adapter for Payload",
|
||||
"repository": "https://github.com/payloadcms/payload",
|
||||
"license": "MIT",
|
||||
@@ -26,7 +26,8 @@
|
||||
"drizzle-orm": "0.28.5",
|
||||
"pg": "8.11.3",
|
||||
"prompts": "2.4.2",
|
||||
"to-snake-case": "1.0.0"
|
||||
"to-snake-case": "1.0.0",
|
||||
"uuid": "9.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@payloadcms/eslint-config": "workspace:*",
|
||||
|
||||
@@ -33,7 +33,7 @@ export const findMany = async function find({
|
||||
const db = adapter.sessions[req.transactionID]?.db || adapter.drizzle
|
||||
const table = adapter.tables[tableName]
|
||||
|
||||
let limit = limitArg ?? 10
|
||||
const limit = limitArg ?? 10
|
||||
let totalDocs: number
|
||||
let totalPages: number
|
||||
let hasPrevPage: boolean
|
||||
@@ -51,6 +51,7 @@ export const findMany = async function find({
|
||||
})
|
||||
|
||||
const orderedIDMap: Record<number | string, number> = {}
|
||||
let orderedIDs: (number | string)[]
|
||||
|
||||
const selectDistinctMethods: ChainedMethods = []
|
||||
|
||||
@@ -116,7 +117,8 @@ export const findMany = async function find({
|
||||
selectDistinctResult.forEach(({ id }, i) => {
|
||||
orderedIDMap[id as number | string] = i
|
||||
})
|
||||
findManyArgs.where = inArray(adapter.tables[tableName].id, Object.keys(orderedIDMap))
|
||||
orderedIDs = Object.keys(orderedIDMap)
|
||||
findManyArgs.where = inArray(adapter.tables[tableName].id, orderedIDs)
|
||||
} else {
|
||||
findManyArgs.limit = limitArg === 0 ? undefined : limitArg
|
||||
|
||||
@@ -132,7 +134,7 @@ export const findMany = async function find({
|
||||
|
||||
const findPromise = db.query[tableName].findMany(findManyArgs)
|
||||
|
||||
if (pagination !== false || selectDistinctResult?.length > limit) {
|
||||
if (pagination !== false && (orderedIDs ? orderedIDs?.length >= limit : true)) {
|
||||
const selectCountMethods: ChainedMethods = []
|
||||
|
||||
joinAliases.forEach(({ condition, table }) => {
|
||||
@@ -174,9 +176,8 @@ export const findMany = async function find({
|
||||
rawDocs.sort((a, b) => orderedIDMap[a.id] - orderedIDMap[b.id])
|
||||
}
|
||||
|
||||
if (pagination === false) {
|
||||
if (pagination === false || !totalDocs) {
|
||||
totalDocs = rawDocs.length
|
||||
limit = totalDocs
|
||||
totalPages = 1
|
||||
pagingCounter = 1
|
||||
hasPrevPage = false
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { Payload } from 'payload'
|
||||
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { createDatabaseAdapter } from 'payload/database'
|
||||
|
||||
@@ -42,7 +43,7 @@ export type { MigrateDownArgs, MigrateUpArgs } from './types'
|
||||
|
||||
export function postgresAdapter(args: Args): PostgresAdapterResult {
|
||||
function adapter({ payload }: { payload: Payload }) {
|
||||
const migrationDir = args.migrationDir || path.resolve(process.cwd(), 'src/migrations')
|
||||
const migrationDir = findMigrationDir(args.migrationDir)
|
||||
|
||||
extendWebpackConfig(payload.config)
|
||||
extendViteConfig(payload.config)
|
||||
@@ -53,6 +54,7 @@ export function postgresAdapter(args: Args): PostgresAdapterResult {
|
||||
// Postgres-specific
|
||||
drizzle: undefined,
|
||||
enums: {},
|
||||
fieldConstraints: {},
|
||||
pool: undefined,
|
||||
poolOptions: args.pool,
|
||||
push: args.push,
|
||||
@@ -60,7 +62,6 @@ export function postgresAdapter(args: Args): PostgresAdapterResult {
|
||||
schema: {},
|
||||
sessions: {},
|
||||
tables: {},
|
||||
fieldConstraints: {},
|
||||
|
||||
// DatabaseAdapter
|
||||
beginTransaction,
|
||||
@@ -101,3 +102,42 @@ export function postgresAdapter(args: Args): PostgresAdapterResult {
|
||||
|
||||
return adapter
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to find migrations directory.
|
||||
*
|
||||
* Checks for the following directories in order:
|
||||
* - `migrationDir` argument from Payload config
|
||||
* - `src/migrations`
|
||||
* - `dist/migrations`
|
||||
* - `migrations`
|
||||
*
|
||||
* Defaults to `src/migrations`
|
||||
*
|
||||
* @param migrationDir
|
||||
* @returns
|
||||
*/
|
||||
function findMigrationDir(migrationDir?: string): string {
|
||||
const cwd = process.cwd()
|
||||
const srcDir = path.resolve(cwd, 'src/migrations')
|
||||
const distDir = path.resolve(cwd, 'dist/migrations')
|
||||
const relativeMigrations = path.resolve(cwd, 'migrations')
|
||||
|
||||
// Use arg if provided
|
||||
if (migrationDir) return migrationDir
|
||||
|
||||
// Check other common locations
|
||||
if (fs.existsSync(srcDir)) {
|
||||
return srcDir
|
||||
}
|
||||
|
||||
if (fs.existsSync(distDir)) {
|
||||
return distDir
|
||||
}
|
||||
|
||||
if (fs.existsSync(relativeMigrations)) {
|
||||
return relativeMigrations
|
||||
}
|
||||
|
||||
return srcDir
|
||||
}
|
||||
|
||||
@@ -297,6 +297,9 @@ export const getTableColumnFromPath = ({
|
||||
table: adapter.tables[newTableName],
|
||||
}
|
||||
}
|
||||
if (pathSegments[1] === 'blockType') {
|
||||
throw new APIError('Querying on blockType is not supported')
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ export const insertArrays = async ({ adapter, arrays, db, parentRows }: Args): P
|
||||
}
|
||||
}
|
||||
|
||||
const parentID = parentRows[parentRowIndex].id
|
||||
const parentID = parentRows[parentRowIndex].id || parentRows[parentRowIndex]._parentID
|
||||
|
||||
// Add any sub arrays that need to be created
|
||||
// We will call this recursively below
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/live-preview-react",
|
||||
"version": "0.1.6",
|
||||
"version": "0.2.0",
|
||||
"description": "The official live preview React SDK for Payload",
|
||||
"repository": "https://github.com/payloadcms/payload",
|
||||
"license": "MIT",
|
||||
@@ -25,7 +25,6 @@
|
||||
"payload": "workspace:*"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"payload": "^2.0.0",
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||
},
|
||||
"exports": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/live-preview",
|
||||
"version": "0.1.6",
|
||||
"version": "0.2.0",
|
||||
"description": "The official live preview JavaScript SDK for Payload",
|
||||
"repository": "https://github.com/payloadcms/payload",
|
||||
"license": "MIT",
|
||||
@@ -20,9 +20,6 @@
|
||||
"@payloadcms/eslint-config": "workspace:*",
|
||||
"payload": "workspace:*"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"payload": "^2.0.0"
|
||||
},
|
||||
"exports": {
|
||||
".": {
|
||||
"default": "./src/index.ts",
|
||||
|
||||
@@ -6,6 +6,10 @@ import { mergeData } from '.'
|
||||
// Send this cached value to `mergeData`, instead of `eventData.fieldSchemaJSON` directly
|
||||
let payloadLivePreviewFieldSchema = undefined // TODO: type this from `fieldSchemaToJSON` return type
|
||||
|
||||
// Each time the data is merged, cache the result as a `previousData` variable
|
||||
// This will ensure changes compound overtop of each other
|
||||
let payloadLivePreviewPreviousData = undefined
|
||||
|
||||
export const handleMessage = async <T>(args: {
|
||||
apiRoute?: string
|
||||
depth?: number
|
||||
@@ -35,12 +39,15 @@ export const handleMessage = async <T>(args: {
|
||||
const mergedData = await mergeData<T>({
|
||||
apiRoute,
|
||||
depth,
|
||||
externallyUpdatedRelationship: eventData.externallyUpdatedRelationship,
|
||||
fieldSchema: payloadLivePreviewFieldSchema,
|
||||
incomingData: eventData.data,
|
||||
initialData,
|
||||
initialData: payloadLivePreviewPreviousData || initialData,
|
||||
serverURL,
|
||||
})
|
||||
|
||||
payloadLivePreviewPreviousData = mergedData
|
||||
|
||||
return mergedData
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import type { PaginatedDocs } from 'payload/database'
|
||||
import type { fieldSchemaToJSON } from 'payload/utilities'
|
||||
|
||||
import type { PopulationsByCollection, UpdatedDocument } from './types'
|
||||
|
||||
import { traverseFields } from './traverseFields'
|
||||
|
||||
export const mergeData = async <T>(args: {
|
||||
apiRoute?: string
|
||||
depth?: number
|
||||
externallyUpdatedRelationship?: UpdatedDocument
|
||||
fieldSchema: ReturnType<typeof fieldSchemaToJSON>
|
||||
incomingData: Partial<T>
|
||||
initialData: T
|
||||
@@ -18,6 +22,7 @@ export const mergeData = async <T>(args: {
|
||||
const {
|
||||
apiRoute,
|
||||
depth,
|
||||
externallyUpdatedRelationship,
|
||||
fieldSchema,
|
||||
incomingData,
|
||||
initialData,
|
||||
@@ -27,22 +32,52 @@ export const mergeData = async <T>(args: {
|
||||
|
||||
const result = { ...initialData }
|
||||
|
||||
const populationPromises: Promise<void>[] = []
|
||||
const populationsByCollection: PopulationsByCollection = {}
|
||||
|
||||
traverseFields({
|
||||
apiRoute,
|
||||
depth,
|
||||
externallyUpdatedRelationship,
|
||||
fieldSchema,
|
||||
incomingData,
|
||||
populationPromises,
|
||||
populationsByCollection,
|
||||
result,
|
||||
serverURL,
|
||||
})
|
||||
|
||||
await Promise.all(populationPromises)
|
||||
await Promise.all(
|
||||
Object.entries(populationsByCollection).map(async ([collection, populations]) => {
|
||||
const ids = new Set(populations.map(({ id }) => id))
|
||||
const url = `${serverURL}${
|
||||
apiRoute || '/api'
|
||||
}/${collection}?depth=${depth}&where[id][in]=${Array.from(ids).join(',')}`
|
||||
|
||||
let res: PaginatedDocs
|
||||
|
||||
try {
|
||||
res = await fetch(url, {
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}).then((res) => res.json())
|
||||
|
||||
if (res?.docs?.length > 0) {
|
||||
res.docs.forEach((doc) => {
|
||||
populationsByCollection[collection].forEach((population) => {
|
||||
if (population.id === doc.id) {
|
||||
population.ref[population.accessor] = doc
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err) // eslint-disable-line no-console
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
return {
|
||||
...result,
|
||||
...(returnNumberOfRequests ? { _numberOfRequests: populationPromises.length } : {}),
|
||||
...(returnNumberOfRequests
|
||||
? { _numberOfRequests: Object.keys(populationsByCollection).length }
|
||||
: {}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
export const promise = async (args: {
|
||||
accessor: number | string
|
||||
apiRoute?: string
|
||||
collection: string
|
||||
depth: number
|
||||
id: number | string
|
||||
ref: Record<string, unknown>
|
||||
serverURL: string
|
||||
}): Promise<void> => {
|
||||
const { id, accessor, apiRoute, collection, depth, ref, serverURL } = args
|
||||
|
||||
const url = `${serverURL}${apiRoute || '/api'}/${collection}/${id}?depth=${depth}`
|
||||
|
||||
let res: Record<string, unknown> | null | undefined = null
|
||||
|
||||
try {
|
||||
res = await fetch(url, {
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}).then((res) => res.json())
|
||||
} catch (err) {
|
||||
console.error(err) // eslint-disable-line no-console
|
||||
}
|
||||
|
||||
ref[accessor] = res
|
||||
}
|
||||
@@ -1,24 +1,22 @@
|
||||
import type { fieldSchemaToJSON } from 'payload/utilities'
|
||||
|
||||
import { promise } from './promise'
|
||||
import type { PopulationsByCollection, UpdatedDocument } from './types'
|
||||
|
||||
import { traverseRichText } from './traverseRichText'
|
||||
|
||||
export const traverseFields = <T>(args: {
|
||||
apiRoute?: string
|
||||
depth?: number
|
||||
externallyUpdatedRelationship?: UpdatedDocument
|
||||
fieldSchema: ReturnType<typeof fieldSchemaToJSON>
|
||||
incomingData: T
|
||||
populationPromises: Promise<void>[]
|
||||
populationsByCollection: PopulationsByCollection
|
||||
result: T
|
||||
serverURL: string
|
||||
}): void => {
|
||||
const {
|
||||
apiRoute,
|
||||
depth,
|
||||
externallyUpdatedRelationship,
|
||||
fieldSchema: fieldSchemas,
|
||||
incomingData,
|
||||
populationPromises,
|
||||
populationsByCollection,
|
||||
result,
|
||||
serverURL,
|
||||
} = args
|
||||
|
||||
fieldSchemas.forEach((fieldSchema) => {
|
||||
@@ -26,6 +24,16 @@ export const traverseFields = <T>(args: {
|
||||
const fieldName = fieldSchema.name
|
||||
|
||||
switch (fieldSchema.type) {
|
||||
case 'richText':
|
||||
result[fieldName] = traverseRichText({
|
||||
externallyUpdatedRelationship,
|
||||
incomingData: incomingData[fieldName],
|
||||
populationsByCollection,
|
||||
result: result[fieldName],
|
||||
})
|
||||
|
||||
break
|
||||
|
||||
case 'array':
|
||||
if (Array.isArray(incomingData[fieldName])) {
|
||||
result[fieldName] = incomingData[fieldName].map((incomingRow, i) => {
|
||||
@@ -38,18 +46,17 @@ export const traverseFields = <T>(args: {
|
||||
}
|
||||
|
||||
traverseFields({
|
||||
apiRoute,
|
||||
depth,
|
||||
externallyUpdatedRelationship,
|
||||
fieldSchema: fieldSchema.fields,
|
||||
incomingData: incomingRow,
|
||||
populationPromises,
|
||||
populationsByCollection,
|
||||
result: result[fieldName][i],
|
||||
serverURL,
|
||||
})
|
||||
|
||||
return result[fieldName][i]
|
||||
})
|
||||
}
|
||||
|
||||
break
|
||||
|
||||
case 'blocks':
|
||||
@@ -72,13 +79,11 @@ export const traverseFields = <T>(args: {
|
||||
}
|
||||
|
||||
traverseFields({
|
||||
apiRoute,
|
||||
depth,
|
||||
externallyUpdatedRelationship,
|
||||
fieldSchema: incomingBlockJSON.fields,
|
||||
incomingData: incomingBlock,
|
||||
populationPromises,
|
||||
populationsByCollection,
|
||||
result: result[fieldName][i],
|
||||
serverURL,
|
||||
})
|
||||
|
||||
return result[fieldName][i]
|
||||
@@ -96,13 +101,11 @@ export const traverseFields = <T>(args: {
|
||||
}
|
||||
|
||||
traverseFields({
|
||||
apiRoute,
|
||||
depth,
|
||||
externallyUpdatedRelationship,
|
||||
fieldSchema: fieldSchema.fields,
|
||||
incomingData: incomingData[fieldName] || {},
|
||||
populationPromises,
|
||||
populationsByCollection,
|
||||
result: result[fieldName],
|
||||
serverURL,
|
||||
})
|
||||
|
||||
break
|
||||
@@ -111,7 +114,7 @@ export const traverseFields = <T>(args: {
|
||||
case 'relationship':
|
||||
// Handle `hasMany` relationships
|
||||
if (fieldSchema.hasMany && Array.isArray(incomingData[fieldName])) {
|
||||
if (!result[fieldName]) {
|
||||
if (!result[fieldName] || !incomingData[fieldName].length) {
|
||||
result[fieldName] = []
|
||||
}
|
||||
|
||||
@@ -131,33 +134,39 @@ export const traverseFields = <T>(args: {
|
||||
const newID = incomingRelation.value
|
||||
const newRelation = incomingRelation.relationTo
|
||||
|
||||
if (oldID !== newID || oldRelation !== newRelation) {
|
||||
populationPromises.push(
|
||||
promise({
|
||||
id: incomingRelation.value,
|
||||
accessor: 'value',
|
||||
apiRoute,
|
||||
collection: newRelation,
|
||||
depth,
|
||||
ref: result[fieldName][i],
|
||||
serverURL,
|
||||
}),
|
||||
)
|
||||
const hasChanged = newID !== oldID || newRelation !== oldRelation
|
||||
const hasUpdated =
|
||||
newRelation === externallyUpdatedRelationship?.entitySlug &&
|
||||
newID === externallyUpdatedRelationship?.id
|
||||
|
||||
if (hasChanged || hasUpdated) {
|
||||
if (!populationsByCollection[newRelation]) {
|
||||
populationsByCollection[newRelation] = []
|
||||
}
|
||||
|
||||
populationsByCollection[newRelation].push({
|
||||
id: incomingRelation.value,
|
||||
accessor: 'value',
|
||||
ref: result[fieldName][i],
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// Handle `hasMany` monomorphic
|
||||
if (result[fieldName][i]?.id !== incomingRelation) {
|
||||
populationPromises.push(
|
||||
promise({
|
||||
id: incomingRelation,
|
||||
accessor: i,
|
||||
apiRoute,
|
||||
collection: String(fieldSchema.relationTo),
|
||||
depth,
|
||||
ref: result[fieldName],
|
||||
serverURL,
|
||||
}),
|
||||
)
|
||||
const hasChanged = incomingRelation !== result[fieldName][i]?.id
|
||||
const hasUpdated =
|
||||
fieldSchema.relationTo === externallyUpdatedRelationship?.entitySlug &&
|
||||
incomingRelation === externallyUpdatedRelationship?.id
|
||||
|
||||
if (hasChanged || hasUpdated) {
|
||||
if (!populationsByCollection[fieldSchema.relationTo]) {
|
||||
populationsByCollection[fieldSchema.relationTo] = []
|
||||
}
|
||||
|
||||
populationsByCollection[fieldSchema.relationTo].push({
|
||||
id: incomingRelation,
|
||||
accessor: i,
|
||||
ref: result[fieldName],
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -197,23 +206,26 @@ export const traverseFields = <T>(args: {
|
||||
const newRelation = hasNewValue ? incomingData[fieldName].relationTo : ''
|
||||
const oldRelation = hasOldValue ? result[fieldName].relationTo : ''
|
||||
|
||||
const hasChanged = newID !== oldID || newRelation !== oldRelation
|
||||
const hasUpdated =
|
||||
newRelation === externallyUpdatedRelationship?.entitySlug &&
|
||||
newID === externallyUpdatedRelationship?.id
|
||||
|
||||
// if the new value/relation is different from the old value/relation
|
||||
// populate the new value, otherwise leave it alone
|
||||
if (newID !== oldID || newRelation !== oldRelation) {
|
||||
if (hasChanged || hasUpdated) {
|
||||
// if the new value is not empty, populate it
|
||||
// otherwise set the value to null
|
||||
if (newID) {
|
||||
populationPromises.push(
|
||||
promise({
|
||||
id: newID,
|
||||
accessor: 'value',
|
||||
apiRoute,
|
||||
collection: newRelation,
|
||||
depth,
|
||||
ref: result[fieldName],
|
||||
serverURL,
|
||||
}),
|
||||
)
|
||||
if (!populationsByCollection[newRelation]) {
|
||||
populationsByCollection[newRelation] = []
|
||||
}
|
||||
|
||||
populationsByCollection[newRelation].push({
|
||||
id: newID,
|
||||
accessor: 'value',
|
||||
ref: result[fieldName],
|
||||
})
|
||||
} else {
|
||||
result[fieldName] = null
|
||||
}
|
||||
@@ -232,23 +244,26 @@ export const traverseFields = <T>(args: {
|
||||
result[fieldName].id) ||
|
||||
result[fieldName]
|
||||
|
||||
const hasChanged = newID !== oldID
|
||||
const hasUpdated =
|
||||
fieldSchema.relationTo === externallyUpdatedRelationship?.entitySlug &&
|
||||
newID === externallyUpdatedRelationship?.id
|
||||
|
||||
// if the new value is different from the old value
|
||||
// populate the new value, otherwise leave it alone
|
||||
if (newID !== oldID) {
|
||||
if (hasChanged || hasUpdated) {
|
||||
// if the new value is not empty, populate it
|
||||
// otherwise set the value to null
|
||||
if (newID) {
|
||||
populationPromises.push(
|
||||
promise({
|
||||
id: newID,
|
||||
accessor: fieldName,
|
||||
apiRoute,
|
||||
collection: String(fieldSchema.relationTo),
|
||||
depth,
|
||||
ref: result as Record<string, unknown>,
|
||||
serverURL,
|
||||
}),
|
||||
)
|
||||
if (!populationsByCollection[fieldSchema.relationTo]) {
|
||||
populationsByCollection[fieldSchema.relationTo] = []
|
||||
}
|
||||
|
||||
populationsByCollection[fieldSchema.relationTo].push({
|
||||
id: newID,
|
||||
accessor: fieldName,
|
||||
ref: result as Record<string, unknown>,
|
||||
})
|
||||
} else {
|
||||
result[fieldName] = null
|
||||
}
|
||||
|
||||
93
packages/live-preview/src/traverseRichText.ts
Normal file
93
packages/live-preview/src/traverseRichText.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import type { PopulationsByCollection, UpdatedDocument } from './types'
|
||||
|
||||
export const traverseRichText = ({
|
||||
externallyUpdatedRelationship,
|
||||
incomingData,
|
||||
populationsByCollection,
|
||||
result,
|
||||
}: {
|
||||
externallyUpdatedRelationship?: UpdatedDocument
|
||||
incomingData: any
|
||||
populationsByCollection: PopulationsByCollection
|
||||
result: any
|
||||
}): any => {
|
||||
if (Array.isArray(incomingData)) {
|
||||
if (!result) {
|
||||
result = []
|
||||
}
|
||||
|
||||
result = incomingData.map((item, index) => {
|
||||
if (!result[index]) {
|
||||
result[index] = item
|
||||
}
|
||||
|
||||
return traverseRichText({
|
||||
externallyUpdatedRelationship,
|
||||
incomingData: item,
|
||||
populationsByCollection,
|
||||
result: result[index],
|
||||
})
|
||||
})
|
||||
} else if (incomingData && typeof incomingData === 'object') {
|
||||
if (!result) {
|
||||
result = {}
|
||||
}
|
||||
|
||||
// Remove keys from `result` that do not appear in `incomingData`
|
||||
// There's likely another way to do this,
|
||||
// But recursion and references make this very difficult
|
||||
Object.keys(result).forEach((key) => {
|
||||
if (!(key in incomingData)) {
|
||||
delete result[key]
|
||||
}
|
||||
})
|
||||
|
||||
// Iterate over the keys of `incomingData` and populate `result`
|
||||
Object.keys(incomingData).forEach((key) => {
|
||||
if (!result[key]) {
|
||||
// Instantiate the key in `result` if it doesn't exist
|
||||
// Ensure its type matches the type of the `incomingData`
|
||||
// We don't have a schema to check against here
|
||||
result[key] =
|
||||
incomingData[key] && typeof incomingData[key] === 'object'
|
||||
? Array.isArray(incomingData[key])
|
||||
? []
|
||||
: {}
|
||||
: incomingData[key]
|
||||
}
|
||||
|
||||
const isRelationship = key === 'value' && 'relationTo' in incomingData
|
||||
|
||||
if (isRelationship) {
|
||||
const needsPopulation = !result.value || typeof result.value !== 'object'
|
||||
const hasChanged =
|
||||
result &&
|
||||
typeof result === 'object' &&
|
||||
result.value.id === externallyUpdatedRelationship?.id
|
||||
|
||||
if (needsPopulation || hasChanged) {
|
||||
if (!populationsByCollection[incomingData.relationTo]) {
|
||||
populationsByCollection[incomingData.relationTo] = []
|
||||
}
|
||||
|
||||
populationsByCollection[incomingData.relationTo].push({
|
||||
id: incomingData[key],
|
||||
accessor: 'value',
|
||||
ref: result,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
result[key] = traverseRichText({
|
||||
externallyUpdatedRelationship,
|
||||
incomingData: incomingData[key],
|
||||
populationsByCollection,
|
||||
result: result[key],
|
||||
})
|
||||
}
|
||||
})
|
||||
} else {
|
||||
result = incomingData
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
@@ -1,3 +1,18 @@
|
||||
export type LivePreviewArgs = {}
|
||||
|
||||
export type LivePreview = void
|
||||
|
||||
export type PopulationsByCollection = {
|
||||
[slug: string]: Array<{
|
||||
accessor: number | string
|
||||
id: number | string
|
||||
ref: Record<string, unknown>
|
||||
}>
|
||||
}
|
||||
|
||||
// TODO: import this from `payload/admin/components/utilities/DocumentEvents/types.ts`
|
||||
export type UpdatedDocument = {
|
||||
entitySlug: string
|
||||
id?: number | string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "payload",
|
||||
"version": "2.1.1",
|
||||
"version": "2.3.0",
|
||||
"description": "Node, React and MongoDB Headless CMS and Application Framework",
|
||||
"license": "MIT",
|
||||
"main": "./dist/index.js",
|
||||
|
||||
@@ -17,6 +17,7 @@ import { StepNavProvider } from './components/elements/StepNav'
|
||||
import { AuthProvider } from './components/utilities/Auth'
|
||||
import { ConfigProvider } from './components/utilities/Config'
|
||||
import { CustomProvider } from './components/utilities/CustomProvider'
|
||||
import { DocumentEventsProvider } from './components/utilities/DocumentEvents'
|
||||
import { I18n } from './components/utilities/I18n'
|
||||
import { LoadingOverlayProvider } from './components/utilities/LoadingOverlay'
|
||||
import { LocaleProvider } from './components/utilities/Locale'
|
||||
@@ -49,11 +50,13 @@ const Root = ({ config: incomingConfig }: { config?: SanitizedConfig }) => {
|
||||
<LocaleProvider>
|
||||
<StepNavProvider>
|
||||
<LoadingOverlayProvider>
|
||||
<NavProvider>
|
||||
<CustomProvider>
|
||||
<Routes />
|
||||
</CustomProvider>
|
||||
</NavProvider>
|
||||
<DocumentEventsProvider>
|
||||
<NavProvider>
|
||||
<CustomProvider>
|
||||
<Routes />
|
||||
</CustomProvider>
|
||||
</NavProvider>
|
||||
</DocumentEventsProvider>
|
||||
</LoadingOverlayProvider>
|
||||
</StepNavProvider>
|
||||
</LocaleProvider>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
@@ -90,36 +90,41 @@ const Duplicate: React.FC<Props> = ({ id, collection, slug }) => {
|
||||
if (result.status === 201 || result.status === 200) {
|
||||
return json.doc.id
|
||||
}
|
||||
json.errors.forEach((error) => toast.error(error.message))
|
||||
|
||||
// only show the error if this is the initial request failing
|
||||
if (!duplicateID) {
|
||||
json.errors.forEach((error) => toast.error(error.message))
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
let duplicateID
|
||||
let duplicateID: string
|
||||
let abort = false
|
||||
const localeErrors = []
|
||||
|
||||
if (localization) {
|
||||
await localization.localeCodes.reduce(async (priorLocalePatch, locale) => {
|
||||
await priorLocalePatch
|
||||
if (abort) return
|
||||
duplicateID = await saveDocument({ id, duplicateID, locale })
|
||||
const localeResult = await saveDocument({
|
||||
id,
|
||||
duplicateID,
|
||||
locale,
|
||||
})
|
||||
duplicateID = localeResult || duplicateID
|
||||
if (duplicateID && !localeResult) {
|
||||
localeErrors.push(locale)
|
||||
}
|
||||
if (!duplicateID) {
|
||||
abort = true
|
||||
}
|
||||
}, Promise.resolve())
|
||||
|
||||
if (abort && duplicateID) {
|
||||
// delete the duplicate doc to prevent incomplete
|
||||
await requests.delete(`${serverURL}${api}/${slug}/${duplicateID}`, {
|
||||
headers: {
|
||||
'Accept-Language': i18n.language,
|
||||
},
|
||||
})
|
||||
}
|
||||
} else {
|
||||
duplicateID = await saveDocument({ id })
|
||||
}
|
||||
|
||||
if (!duplicateID) {
|
||||
// document was not saved, error toast was displayed
|
||||
return
|
||||
}
|
||||
|
||||
@@ -128,6 +133,16 @@ const Duplicate: React.FC<Props> = ({ id, collection, slug }) => {
|
||||
{ autoClose: 3000 },
|
||||
)
|
||||
|
||||
if (localeErrors.length > 0) {
|
||||
toast.error(
|
||||
`
|
||||
${t('error:localesNotSaved', { count: localeErrors.length })}
|
||||
${localeErrors.join(', ')}
|
||||
`,
|
||||
{ autoClose: 5000 },
|
||||
)
|
||||
}
|
||||
|
||||
setModified(false)
|
||||
|
||||
setTimeout(() => {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -6,10 +6,14 @@ import DatePicker from '../../../DatePicker'
|
||||
|
||||
const baseClass = 'condition-value-date'
|
||||
|
||||
const DateField: React.FC<Props> = ({ disabled, onChange, value }) => (
|
||||
<div className={baseClass}>
|
||||
<DatePicker onChange={onChange} readOnly={disabled} value={value} />
|
||||
</div>
|
||||
)
|
||||
const DateField: React.FC<Props> = ({ admin, disabled, onChange, value }) => {
|
||||
const { date } = admin || {}
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<DatePicker {...date} onChange={onChange} readOnly={disabled} value={value} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default DateField
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import type { Props as DateType } from '../../../../../components/elements/DatePicker/types'
|
||||
export type Props = {
|
||||
admin?: {
|
||||
date?: DateType
|
||||
}
|
||||
disabled?: boolean
|
||||
onChange: () => void
|
||||
value: Date
|
||||
|
||||
@@ -23,14 +23,26 @@ const baseClass = 'where-builder'
|
||||
const reduceFields = (fields, i18n) =>
|
||||
flattenTopLevelFields(fields).reduce((reduced, field) => {
|
||||
if (typeof fieldTypes[field.type] === 'object') {
|
||||
const operatorKeys = new Set()
|
||||
const operators = fieldTypes[field.type].operators.reduce((acc, operator) => {
|
||||
if (!operatorKeys.has(operator.value)) {
|
||||
operatorKeys.add(operator.value)
|
||||
return [
|
||||
...acc,
|
||||
{
|
||||
...operator,
|
||||
label: i18n.t(`operators:${operator.label}`),
|
||||
},
|
||||
]
|
||||
}
|
||||
return acc
|
||||
}, [])
|
||||
|
||||
const formattedField = {
|
||||
label: getTranslation(field.label || field.name, i18n),
|
||||
value: field.name,
|
||||
...fieldTypes[field.type],
|
||||
operators: fieldTypes[field.type].operators.map((operator) => ({
|
||||
...operator,
|
||||
label: i18n.t(`operators:${operator.label}`),
|
||||
})),
|
||||
operators,
|
||||
props: {
|
||||
...field,
|
||||
},
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
.section-title {
|
||||
position: relative;
|
||||
min-width: 0;
|
||||
|
||||
&:after {
|
||||
display: block;
|
||||
|
||||
@@ -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 />}
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 baseClass = 'json-field'
|
||||
|
||||
@@ -19,13 +19,13 @@ const JSONField: React.FC<Props> = (props) => {
|
||||
name,
|
||||
admin: {
|
||||
className,
|
||||
components: { Error, Label } = {},
|
||||
condition,
|
||||
description,
|
||||
editorOptions,
|
||||
readOnly,
|
||||
style,
|
||||
width,
|
||||
components: { Error, Label } = {},
|
||||
} = {},
|
||||
label,
|
||||
path: pathFromProps,
|
||||
@@ -69,8 +69,8 @@ const JSONField: React.FC<Props> = (props) => {
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
setStringValue(JSON.stringify(initialValue, null, 2))
|
||||
}, [initialValue])
|
||||
setStringValue(JSON.stringify(value ? value : initialValue, null, 2))
|
||||
}, [initialValue, value])
|
||||
|
||||
return (
|
||||
<div
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -40,13 +40,14 @@ const Relationship: React.FC<Props> = (props) => {
|
||||
admin: {
|
||||
allowCreate = true,
|
||||
className,
|
||||
components: { Error, Label } = {},
|
||||
condition,
|
||||
description,
|
||||
isSortable = true,
|
||||
readOnly,
|
||||
sortOptions,
|
||||
style,
|
||||
width,
|
||||
components: { Error, Label } = {},
|
||||
} = {},
|
||||
filterOptions,
|
||||
hasMany,
|
||||
@@ -139,7 +140,14 @@ const Relationship: React.FC<Props> = (props) => {
|
||||
|
||||
if (resultsFetched < 10) {
|
||||
const collection = collections.find((coll) => coll.slug === relation)
|
||||
const fieldToSearch = collection?.admin?.useAsTitle || 'id'
|
||||
let fieldToSearch = collection?.defaultSort || collection?.admin?.useAsTitle || 'id'
|
||||
if (!searchArg) {
|
||||
if (typeof sortOptions === 'string') {
|
||||
fieldToSearch = sortOptions
|
||||
} else if (sortOptions?.[relation]) {
|
||||
fieldToSearch = sortOptions[relation]
|
||||
}
|
||||
}
|
||||
|
||||
const query: {
|
||||
[key: string]: unknown
|
||||
@@ -236,6 +244,7 @@ const Relationship: React.FC<Props> = (props) => {
|
||||
locale,
|
||||
filterOptionsResult,
|
||||
serverURL,
|
||||
sortOptions,
|
||||
api,
|
||||
i18n,
|
||||
config,
|
||||
@@ -252,7 +261,7 @@ const Relationship: React.FC<Props> = (props) => {
|
||||
(searchArg: string, valueArg: Value | Value[]) => {
|
||||
if (search !== searchArg) {
|
||||
setLastLoadedPage({})
|
||||
updateSearch(searchArg, valueArg)
|
||||
updateSearch(searchArg, valueArg, searchArg !== '')
|
||||
}
|
||||
},
|
||||
[search, updateSearch],
|
||||
|
||||
@@ -2,11 +2,12 @@ import React from 'react'
|
||||
|
||||
import type { RichTextField } from '../../../../../fields/config/types'
|
||||
import type { RichTextAdapter } from './types'
|
||||
|
||||
const RichText: React.FC<RichTextField> = (props) => {
|
||||
const RichText: React.FC<RichTextField> = (fieldprops) => {
|
||||
// eslint-disable-next-line react/destructuring-assignment
|
||||
const editor: RichTextAdapter = props.editor
|
||||
return <editor.FieldComponent {...props} />
|
||||
const editor: RichTextAdapter = fieldprops.editor
|
||||
const { FieldComponent } = editor
|
||||
|
||||
return <FieldComponent {...fieldprops} />
|
||||
}
|
||||
|
||||
export default RichText
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import type { JSONSchema4 } from 'json-schema'
|
||||
|
||||
import type { PayloadRequest } from '../../../../../express/types'
|
||||
import type { RequestContext } from '../../../../../express/types'
|
||||
import type { RichTextField, Validate } from '../../../../../fields/config/types'
|
||||
import type { CellComponentProps } from '../../../views/collections/List/Cell/types'
|
||||
|
||||
@@ -29,11 +32,22 @@ export type RichTextAdapter<
|
||||
siblingDoc: Record<string, unknown>
|
||||
}) => Promise<void> | null
|
||||
|
||||
outputSchema?: ({
|
||||
field,
|
||||
isRequired,
|
||||
}: {
|
||||
field: RichTextField<Value, AdapterProps, ExtraFieldProperties>
|
||||
isRequired: boolean
|
||||
}) => JSONSchema4
|
||||
populationPromise?: (data: {
|
||||
context: RequestContext
|
||||
currentDepth?: number
|
||||
depth: number
|
||||
field: RichTextField<Value, AdapterProps, ExtraFieldProperties>
|
||||
findMany: boolean
|
||||
flattenLocales: boolean
|
||||
overrideAccess?: boolean
|
||||
populationPromises: Promise<void>[]
|
||||
req: PayloadRequest
|
||||
showHiddenFields: boolean
|
||||
siblingDoc: Record<string, unknown>
|
||||
|
||||
@@ -95,7 +95,7 @@ const TabsField: React.FC<Props> = (props) => {
|
||||
: existingPreferences?.fields?.[tabsPrefKey]?.tabIndex
|
||||
setActiveTabIndex(initialIndex || 0)
|
||||
}
|
||||
getInitialPref()
|
||||
void getInitialPref()
|
||||
}, [path, indexPath, getPreference, preferencesKey, tabsPrefKey])
|
||||
|
||||
const handleTabChange = useCallback(
|
||||
@@ -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(' ')}
|
||||
@@ -191,7 +193,11 @@ const TabsField: React.FC<Props> = (props) => {
|
||||
fieldTypes={fieldTypes}
|
||||
forceRender={forceRender}
|
||||
indexPath={indexPath}
|
||||
key={String(activeTabConfig.label)}
|
||||
key={
|
||||
activeTabConfig.label
|
||||
? getTranslation(activeTabConfig.label, i18n)
|
||||
: activeTabConfig['name']
|
||||
}
|
||||
margins="small"
|
||||
permissions={
|
||||
tabHasName(activeTabConfig) && permissions?.[activeTabConfig.name]
|
||||
|
||||
@@ -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, '__')}`}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import React, { createContext, useContext, useState } from 'react'
|
||||
|
||||
import type { UpdatedDocument } from './types'
|
||||
|
||||
const Context = createContext({
|
||||
mostRecentUpdate: null,
|
||||
reportUpdate: (doc: UpdatedDocument) => null, // eslint-disable-line @typescript-eslint/no-unused-vars
|
||||
})
|
||||
|
||||
export const DocumentEventsProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
const [mostRecentUpdate, reportUpdate] = useState<UpdatedDocument>(null)
|
||||
|
||||
return <Context.Provider value={{ mostRecentUpdate, reportUpdate }}>{children}</Context.Provider>
|
||||
}
|
||||
|
||||
export const useDocumentEvents = () => useContext(Context)
|
||||
@@ -0,0 +1,10 @@
|
||||
export type UpdatedDocument = {
|
||||
entitySlug: string
|
||||
id?: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
export type DocumentEventsContext = {
|
||||
mostRecentUpdate: UpdatedDocument
|
||||
reportUpdate: (updatedDocument: Array<UpdatedDocument>) => void
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -11,6 +11,7 @@ import buildStateFromSchema from '../../forms/Form/buildStateFromSchema'
|
||||
import { fieldTypes } from '../../forms/field-types'
|
||||
import { useAuth } from '../../utilities/Auth'
|
||||
import { useConfig } from '../../utilities/Config'
|
||||
import { useDocumentEvents } from '../../utilities/DocumentEvents'
|
||||
import { useDocumentInfo } from '../../utilities/DocumentInfo'
|
||||
import { EditDepthContext } from '../../utilities/EditDepth'
|
||||
import { useLocale } from '../../utilities/Locale'
|
||||
@@ -37,10 +38,17 @@ const GlobalView: React.FC<IndexProps> = (props) => {
|
||||
serverURL,
|
||||
} = useConfig()
|
||||
|
||||
const { reportUpdate } = useDocumentEvents()
|
||||
|
||||
const { admin: { components: { views: { Edit: Edit } = {} } = {} } = {}, fields, slug } = global
|
||||
|
||||
const onSave = useCallback(
|
||||
async (json) => {
|
||||
reportUpdate({
|
||||
entitySlug: global.slug,
|
||||
updatedAt: json?.result?.updatedAt || new Date().toISOString(),
|
||||
})
|
||||
|
||||
getVersions()
|
||||
getDocPermissions()
|
||||
setUpdatedAt(json?.result?.updatedAt)
|
||||
@@ -59,7 +67,18 @@ const GlobalView: React.FC<IndexProps> = (props) => {
|
||||
})
|
||||
setInitialState(state)
|
||||
},
|
||||
[getVersions, fields, user, locale, t, getDocPermissions, getDocPreferences, config],
|
||||
[
|
||||
getVersions,
|
||||
fields,
|
||||
user,
|
||||
locale,
|
||||
t,
|
||||
getDocPermissions,
|
||||
getDocPreferences,
|
||||
config,
|
||||
global,
|
||||
reportUpdate,
|
||||
],
|
||||
)
|
||||
|
||||
const [{ data, isLoading: isLoadingData }] = usePayloadAPI(`${serverURL}${api}/globals/${slug}`, {
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { EditViewProps } from '../../types'
|
||||
|
||||
import { useAllFormFields } from '../../../forms/Form/context'
|
||||
import reduceFieldsToValues from '../../../forms/Form/reduceFieldsToValues'
|
||||
import { useDocumentEvents } from '../../../utilities/DocumentEvents'
|
||||
import { useLivePreviewContext } from '../Context/context'
|
||||
import { DeviceContainer } from '../Device'
|
||||
import { IFrame } from '../IFrame'
|
||||
@@ -23,6 +24,8 @@ export const LivePreview: React.FC<EditViewProps> = (props) => {
|
||||
url,
|
||||
} = useLivePreviewContext()
|
||||
|
||||
const { mostRecentUpdate } = useDocumentEvents()
|
||||
|
||||
const { breakpoint, fieldSchemaJSON } = useLivePreviewContext()
|
||||
|
||||
const prevWindowType =
|
||||
@@ -49,6 +52,7 @@ export const LivePreview: React.FC<EditViewProps> = (props) => {
|
||||
|
||||
const message = JSON.stringify({
|
||||
data: values,
|
||||
externallyUpdatedRelationship: mostRecentUpdate,
|
||||
fieldSchemaJSON: shouldSendSchema ? fieldSchemaJSON : undefined,
|
||||
type: 'payload-live-preview',
|
||||
})
|
||||
@@ -73,6 +77,7 @@ export const LivePreview: React.FC<EditViewProps> = (props) => {
|
||||
iframeRef,
|
||||
setIframeHasLoaded,
|
||||
fieldSchemaJSON,
|
||||
mostRecentUpdate,
|
||||
])
|
||||
|
||||
if (previewWindowType === 'iframe') {
|
||||
|
||||
@@ -51,4 +51,9 @@
|
||||
justify-content: center;
|
||||
padding: 6px 0;
|
||||
}
|
||||
|
||||
.popup-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
<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>
|
||||
|
||||
<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}
|
||||
|
||||
@@ -9,6 +9,7 @@ import { DocumentHeader } from '../../../elements/DocumentHeader'
|
||||
import { FormLoadingOverlayToggle } from '../../../elements/Loading'
|
||||
import Form from '../../../forms/Form'
|
||||
import { useAuth } from '../../../utilities/Auth'
|
||||
import { useDocumentEvents } from '../../../utilities/DocumentEvents'
|
||||
import { OperationContext } from '../../../utilities/OperationProvider'
|
||||
import { CollectionRoutes } from './Routes'
|
||||
import { CustomCollectionComponent } from './Routes/CustomComponent'
|
||||
@@ -42,12 +43,19 @@ const DefaultEditView: React.FC<DefaultEditViewProps> = (props) => {
|
||||
onSave: onSaveFromProps,
|
||||
} = props
|
||||
|
||||
const { reportUpdate } = useDocumentEvents()
|
||||
|
||||
const { auth } = collection
|
||||
|
||||
const classes = [baseClass, isEditing && `${baseClass}--is-editing`].filter(Boolean).join(' ')
|
||||
|
||||
const onSave = useCallback(
|
||||
async (json) => {
|
||||
reportUpdate({
|
||||
id,
|
||||
entitySlug: collection.slug,
|
||||
updatedAt: json?.result?.updatedAt || new Date().toISOString(),
|
||||
})
|
||||
if (auth && id === user.id) {
|
||||
await refreshCookieAsync()
|
||||
}
|
||||
@@ -59,7 +67,7 @@ const DefaultEditView: React.FC<DefaultEditViewProps> = (props) => {
|
||||
})
|
||||
}
|
||||
},
|
||||
[id, onSaveFromProps, auth, user, refreshCookieAsync],
|
||||
[id, onSaveFromProps, auth, user, refreshCookieAsync, collection, reportUpdate],
|
||||
)
|
||||
|
||||
const operation = isEditing ? 'update' : 'create'
|
||||
|
||||
@@ -7,8 +7,9 @@ import type { CellComponentProps } from '../../types'
|
||||
const RichTextCell: React.FC<CellComponentProps<RichTextField>> = (props) => {
|
||||
// eslint-disable-next-line react/destructuring-assignment
|
||||
const editor: RichTextAdapter = props.field.editor
|
||||
const { CellComponent } = editor
|
||||
|
||||
return <editor.CellComponent {...props} />
|
||||
return <CellComponent {...props} />
|
||||
}
|
||||
|
||||
export default RichTextCell
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable react/destructuring-assignment */
|
||||
import type { Request } from 'express'
|
||||
|
||||
import type { SanitizedConfig } from '../config/types'
|
||||
@@ -8,22 +7,31 @@ import parseCookies from '../utilities/parseCookies'
|
||||
const getExtractJWT =
|
||||
(config: SanitizedConfig) =>
|
||||
(req: Request): null | string => {
|
||||
if (req && req.get) {
|
||||
const jwtFromHeader = req.get('Authorization')
|
||||
const origin = req.get('Origin')
|
||||
if (!req?.get) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (jwtFromHeader && jwtFromHeader.indexOf('JWT ') === 0) {
|
||||
return jwtFromHeader.replace('JWT ', '')
|
||||
}
|
||||
const jwtFromHeader = req.get('Authorization')
|
||||
const origin = req.get('Origin')
|
||||
|
||||
const cookies = parseCookies(req)
|
||||
const tokenCookieName = `${config.cookiePrefix}-token`
|
||||
if (jwtFromHeader?.indexOf('JWT ') === 0) {
|
||||
return jwtFromHeader.replace('JWT ', '')
|
||||
}
|
||||
// allow RFC6750 OAuth 2.0 compliant Bearer tokens
|
||||
// in addition to the payload default JWT format
|
||||
if (jwtFromHeader?.indexOf('Bearer ') === 0) {
|
||||
return jwtFromHeader.replace('Bearer ', '')
|
||||
}
|
||||
|
||||
if (cookies && cookies[tokenCookieName]) {
|
||||
if (!origin || config.csrf.length === 0 || config.csrf.indexOf(origin) > -1) {
|
||||
return cookies[tokenCookieName]
|
||||
}
|
||||
}
|
||||
const cookies = parseCookies(req)
|
||||
const tokenCookieName = `${config.cookiePrefix}-token`
|
||||
|
||||
if (!cookies?.[tokenCookieName]) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (!origin || config.csrf.length === 0 || config.csrf.indexOf(origin) > -1) {
|
||||
return cookies[tokenCookieName]
|
||||
}
|
||||
|
||||
return null
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user