Compare commits
48 Commits
db-postgre
...
db-postgre
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c2f379f139 | ||
|
|
1a523eff98 | ||
|
|
f320a87f92 | ||
|
|
d1a0822f80 | ||
|
|
da533d6b64 | ||
|
|
fb3b95e52d | ||
|
|
a9d96b1037 | ||
|
|
ea7ce6fd97 | ||
|
|
354b73c3aa | ||
|
|
96fc3df532 | ||
|
|
c7a315a7d1 | ||
|
|
b722f202af | ||
|
|
720760225f | ||
|
|
f7d4c04f65 | ||
|
|
6b1b4ffd27 | ||
|
|
6325b334ec | ||
|
|
79b1b88a2f | ||
|
|
b2beec302f | ||
|
|
fbc2064a10 | ||
|
|
900a9eafeb | ||
|
|
06cd52b622 | ||
|
|
c7ec557466 | ||
|
|
4c587acc10 | ||
|
|
6f39b809b3 | ||
|
|
796669279a | ||
|
|
886fca8e37 | ||
|
|
30db52ac45 | ||
|
|
f04a18a258 | ||
|
|
cdc10be1a2 | ||
|
|
a5b2333140 | ||
|
|
afe1834f9a | ||
|
|
3d7a2de00d | ||
|
|
5ea88bb47d | ||
|
|
386fe0741d | ||
|
|
b6d9a2021f | ||
|
|
1f8f173741 | ||
|
|
36576f152a | ||
|
|
4ea8ace4c8 | ||
|
|
3a83f08518 | ||
|
|
4607dbf976 | ||
|
|
94d8d2790b | ||
|
|
e6b9fb4fab | ||
|
|
6918be20ba | ||
|
|
e4881bb02f | ||
|
|
ed748102d6 | ||
|
|
88627f22e5 | ||
|
|
a2a44a81f2 | ||
|
|
b2b0f10935 |
@@ -23,3 +23,7 @@ indent_size = 2
|
||||
[*.json]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
[*.md]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
78
CHANGELOG.md
78
CHANGELOG.md
@@ -1,86 +1,54 @@
|
||||
## [2.0.15](https://github.com/payloadcms/payload/compare/v2.0.14...v2.0.15) (2023-11-03)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* autosave updating data in unrelated docs ([b722f20](https://github.com/payloadcms/payload/commit/b722f202af39a1429298b700cac686ecbbd4b46b))
|
||||
* better error handling within parseCookies ([#3720](https://github.com/payloadcms/payload/issues/3720)) ([6b1b4ff](https://github.com/payloadcms/payload/commit/6b1b4ffd27cc9a84e22ef2f3a8e389e5b72d41bc))
|
||||
* block row removal w/ db-postgres adapter ([#3951](https://github.com/payloadcms/payload/issues/3951)) ([5ea88bb](https://github.com/payloadcms/payload/commit/5ea88bb47d9ed6457331ceab7d7c82b0face8311))
|
||||
* deeply merges view configs ([#3954](https://github.com/payloadcms/payload/issues/3954)) ([a5b2333](https://github.com/payloadcms/payload/commit/a5b2333140447b12dbafd68592108ac342af4ea7))
|
||||
* do not display field if read permission is false - admin panel ui ([#3949](https://github.com/payloadcms/payload/issues/3949)) ([cdc10be](https://github.com/payloadcms/payload/commit/cdc10be1a241c6a9ac09feab77bcd58d23ff3dd9))
|
||||
* ensures dataloader does not run requests in parallel ([4607dbf](https://github.com/payloadcms/payload/commit/4607dbf97694bc899e597e9c7df50b6c878874f5))
|
||||
* exclude files from dev bundle if aliased ([#3957](https://github.com/payloadcms/payload/issues/3957)) ([7966692](https://github.com/payloadcms/payload/commit/796669279afb8fe23723ce36e6e47a44b7088b09))
|
||||
* field paths being mutated if they ended with the req.locale ([#3936](https://github.com/payloadcms/payload/issues/3936)) ([36576f1](https://github.com/payloadcms/payload/commit/36576f152ace41edd8b353703db2598d04deae44))
|
||||
* findVersions pagination ([#3906](https://github.com/payloadcms/payload/issues/3906)) ([1f8f173](https://github.com/payloadcms/payload/commit/1f8f173741fd524c7c2f11cc104672854f625da9))
|
||||
* global autosave and relevant e2e test ([a9d96b1](https://github.com/payloadcms/payload/commit/a9d96b10376fe1a4731b2ddb4d26ce38e333d5cb))
|
||||
* **i18n:** polish translations ([#3934](https://github.com/payloadcms/payload/issues/3934)) ([e4881bb](https://github.com/payloadcms/payload/commit/e4881bb02f7c1e8f96d5b405c57e0fdc01a2e7fe))
|
||||
* passes correct data to buildStateFromSchema on account page ([#3984](https://github.com/payloadcms/payload/issues/3984)) ([c7a315a](https://github.com/payloadcms/payload/commit/c7a315a7d1075361a7ee432a449769397c12185e))
|
||||
* prevent sort from saving a new version in version list view ([#3944](https://github.com/payloadcms/payload/issues/3944)) ([900a9ea](https://github.com/payloadcms/payload/commit/900a9eafeb51b1e5130518d4f71034a2bf9e4c5b))
|
||||
* properly load temp files into buffer ([#3996](https://github.com/payloadcms/payload/issues/3996)) ([d1a0822](https://github.com/payloadcms/payload/commit/d1a0822f8044a3f65416f4fe608e91a4ceea6b56))
|
||||
* sort document tabs by order ([#3968](https://github.com/payloadcms/payload/issues/3968)) ([06cd52b](https://github.com/payloadcms/payload/commit/06cd52b622723503896af6262907d31b258d0a5e))
|
||||
* vertical alignment in step nav when using larger logos ([#3955](https://github.com/payloadcms/payload/issues/3955)) ([b6d9a20](https://github.com/payloadcms/payload/commit/b6d9a2021fafea594353329fd304553bf7f2d091))
|
||||
|
||||
## [2.0.14](https://github.com/payloadcms/payload/compare/v2.0.13...v2.0.14) (2023-10-30)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add textarea field ([474436e](https://github.com/payloadcms/payload/commit/474436e9ab70374a2cfdc1bede06f21052fb78c9))
|
||||
* adds basePrice to payment field ([12c85d7](https://github.com/payloadcms/payload/commit/12c85d7707ab85cc7a28b43bad1b5085b93fb3bf))
|
||||
* adds cc field to email ([2c8ea53](https://github.com/payloadcms/payload/commit/2c8ea533afd98d27ee9787d8f0fc03d2cb4a7198))
|
||||
* adds email-from-name & reply-to-name fields to Emails ([3816431](https://github.com/payloadcms/payload/commit/38164318932a10bc483f0860c5294c863f83bf7a))
|
||||
* adds image and video elements to sticky list and content grid rich text ([c2c2bb7](https://github.com/payloadcms/payload/commit/c2c2bb7f2d1c54e923b79a91a7fcb72128a4034c))
|
||||
* adds indentation option to richText ([67df834](https://github.com/payloadcms/payload/commit/67df834f314b4b243c2dea45a3c9a6c88975761d))
|
||||
* adds payment fields to form builder [#43](https://github.com/payloadcms/payload/issues/43) ([2f00aef](https://github.com/payloadcms/payload/commit/2f00aef66c8c635c1ab579205c1967eec1a4bd12))
|
||||
* adds title to forms ([6e2f433](https://github.com/payloadcms/payload/commit/6e2f43394d6b2dfb6134ddc4976913689ce9273d))
|
||||
* builds breadcrumbs plugin ([2a2a2e3](https://github.com/payloadcms/payload/commit/2a2a2e3a2f90350bfd41a12d1d7fc05766128606))
|
||||
* builds demo ([f05462e](https://github.com/payloadcms/payload/commit/f05462efd348b07aba55f7f2f4b78e1fe34bd4a9))
|
||||
* builds form builder plugin ([eeecbbe](https://github.com/payloadcms/payload/commit/eeecbbedb616c44490f364d5d29cd29d1f3d8370))
|
||||
* builds getPaymentTotal utility ([65a53c7](https://github.com/payloadcms/payload/commit/65a53c7d76927aea204234189f67c57a59785275))
|
||||
* configures tsc and fixes types ([8b69625](https://github.com/payloadcms/payload/commit/8b69625b57895f14e73e26d3d564ea804877667c))
|
||||
* dynamic pricing and payment handling [#43](https://github.com/payloadcms/payload/issues/43) ([b749f89](https://github.com/payloadcms/payload/commit/b749f89c307cf34497bd453bee70ca607684ebab))
|
||||
* explicitly exports fields for reuse [#35](https://github.com/payloadcms/payload/issues/35) ([#39](https://github.com/payloadcms/payload/issues/39)) ([f8da32a](https://github.com/payloadcms/payload/commit/f8da32a9bd12694db5fb02c579c6d38d94883913))
|
||||
* extends form builder plugin ([3c58e51](https://github.com/payloadcms/payload/commit/3c58e51d17c7a5b89ae28649c2b61920f5bf6f8d))
|
||||
* field localization ([b4d1eaf](https://github.com/payloadcms/payload/commit/b4d1eaf1fb02053a3b7d65fe0ec4f36828c6c146))
|
||||
* form submission emails ([db29b07](https://github.com/payloadcms/payload/commit/db29b07b82214d73423b17f5bcc0377731225238))
|
||||
* integrates stripe api into form builder [#43](https://github.com/payloadcms/payload/issues/43) ([ec60147](https://github.com/payloadcms/payload/commit/ec60147aff2b165943e957cd59c0b68282e3b5f4))
|
||||
* modifies form builder api ([24a4bc6](https://github.com/payloadcms/payload/commit/24a4bc6f190340f1d1268ba3931c40fcab1430dd))
|
||||
* omits irrelevant collections from rich text relationships ([2ccaa82](https://github.com/payloadcms/payload/commit/2ccaa823e4aef4fc254fcec2254bf7e8a13b43ee))
|
||||
* restructures form builder [#43](https://github.com/payloadcms/payload/issues/43) ([3b1b8dd](https://github.com/payloadcms/payload/commit/3b1b8ddc30f06c8abb8996fa95f2f291d4474e9f))
|
||||
* spreads overrides into configs [#25](https://github.com/payloadcms/payload/issues/25) ([#29](https://github.com/payloadcms/payload/issues/29)) ([da4d901](https://github.com/payloadcms/payload/commit/da4d9018c1e73524993637ce4775517d9d311c68))
|
||||
* supports form email variables and prepares for send ([3207202](https://github.com/payloadcms/payload/commit/3207202808b848b8b0bac74bf0175f3ba19e6ac8))
|
||||
* types payment field ([4a31090](https://github.com/payloadcms/payload/commit/4a310903882fdf6347781985077aab280dd2c1bc))
|
||||
* utilizes unused replyTo ([4938602](https://github.com/payloadcms/payload/commit/4938602ee0a3872e1cb3b7fc6ec9558570825767))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* adds null to non-required field unions ([#3870](https://github.com/payloadcms/payload/issues/3870)) ([7e919aa](https://github.com/payloadcms/payload/commit/7e919aa87c0116c41bf41d75dcd91ff96576a46f))
|
||||
* allows form fields to be properly overridden ([c880a61](https://github.com/payloadcms/payload/commit/c880a61f13e4c034e269466abffedbf7d644d272))
|
||||
* build errors ([9f5ec17](https://github.com/payloadcms/payload/commit/9f5ec1784c674279ee5aa861cc7a6e0b6f5cbf47))
|
||||
* checks for user before accessing properties in preferences update operation([#3844](https://github.com/payloadcms/payload/issues/3844)) ([24eab3a](https://github.com/payloadcms/payload/commit/24eab3af1da3b08debe0580cce8ece08a86039a4))
|
||||
* correctly types width fields [#37](https://github.com/payloadcms/payload/issues/37) ([#41](https://github.com/payloadcms/payload/issues/41)) ([76c9704](https://github.com/payloadcms/payload/commit/76c97048502655516ece4f6680868a23a172ff2c))
|
||||
* custom form fields ([ba0651c](https://github.com/payloadcms/payload/commit/ba0651cecf2aed50a9176218237181a4ce092bc0))
|
||||
* **db-mongodb:** improve find query performance ([#3836](https://github.com/payloadcms/payload/issues/3836)) ([56e58e9](https://github.com/payloadcms/payload/commit/56e58e9ec732f8365f53f214a9e950fbc2a7d8b9)), closes [#3904](https://github.com/payloadcms/payload/issues/3904)
|
||||
* **db-mongodb:** versions pagination ([#3875](https://github.com/payloadcms/payload/issues/3875)) ([4f2b080](https://github.com/payloadcms/payload/commit/4f2b080d1cb5f9d4ab80aa106650307c11e8811b))
|
||||
* declares payload as a peer dependency ([e15e00b](https://github.com/payloadcms/payload/commit/e15e00b5f173041635221490006e4e8347518253))
|
||||
* disable webpack hot reload on production ([#3891](https://github.com/payloadcms/payload/issues/3891)) ([422c803](https://github.com/payloadcms/payload/commit/422c803da67982a063a028508c44009b9577646e))
|
||||
* duplicate document copying to incorrect locale ([#3874](https://github.com/payloadcms/payload/issues/3874)) ([89f273b](https://github.com/payloadcms/payload/commit/89f273bf894512b69e8647be4cf4496bff37c99b))
|
||||
* dynamic field selector option values ([f0cc05a](https://github.com/payloadcms/payload/commit/f0cc05ab912fd07dcee8bd77c6c2bdc40607f80f))
|
||||
* dynamic price selector ([a8d432f](https://github.com/payloadcms/payload/commit/a8d432f7b3da2ca36a3a1c0a378af44be82da357))
|
||||
* enables nested AND/OR queries ([#3834](https://github.com/payloadcms/payload/issues/3834)) ([237eebd](https://github.com/payloadcms/payload/commit/237eebdf87b7d33baa9baaa54946f4ebb4276bfb))
|
||||
* ensure serverURL has string value for getBaseUploadFields function ([#3900](https://github.com/payloadcms/payload/issues/3900)) ([c564a83](https://github.com/payloadcms/payload/commit/c564a83ab61b672d9a2bcc875ab890b0fede5462))
|
||||
* ensures compare-version select field cannot be cleared ([#3901](https://github.com/payloadcms/payload/issues/3901)) ([42d8d11](https://github.com/payloadcms/payload/commit/42d8d11fd7e5792b119f69f17dc1100c85cfa491))
|
||||
* error handling when duplicating documents fails ([#3873](https://github.com/payloadcms/payload/issues/3873)) ([435eb62](https://github.com/payloadcms/payload/commit/435eb6204e550e898a81033f794fcf568e3b017c))
|
||||
* fieldToUse options ([a7780b1](https://github.com/payloadcms/payload/commit/a7780b10d974f91bea441c5c86c475181c6adc4a))
|
||||
* form builder rich text serialization ([8f0d85f](https://github.com/payloadcms/payload/commit/8f0d85fe13591ce8186b623f6165626e79714430))
|
||||
* form builder types ([32a69f8](https://github.com/payloadcms/payload/commit/32a69f8f368aa1917761b7585549dae2f8b2cb12))
|
||||
* generate new block ids on create ([#3871](https://github.com/payloadcms/payload/issues/3871)) ([3404bab](https://github.com/payloadcms/payload/commit/3404bab83f1112713675eb504870a4a1786c3822))
|
||||
* global permissions for live preview ([#3854](https://github.com/payloadcms/payload/issues/3854)) ([3032e0b](https://github.com/payloadcms/payload/commit/3032e0b5a239db0762abd120b2db95f30ed5ca65))
|
||||
* handles null & undefined relationship field values in versions view ([#3609](https://github.com/payloadcms/payload/issues/3609)) ([115e592](https://github.com/payloadcms/payload/commit/115e592b54d9174f316daa3cff31bcc801eaf92f))
|
||||
* incorrect duplication of data in admin ui ([#3907](https://github.com/payloadcms/payload/issues/3907)) ([46fc41c](https://github.com/payloadcms/payload/commit/46fc41cbd9615c58248b4d2c44d24905dd676171))
|
||||
* misc bugs with data safety ([3fdf23a](https://github.com/payloadcms/payload/commit/3fdf23a1a0287673a190cfd3c7bc55d64cff37ba))
|
||||
* only apply focal manipulation when necessary ([#3902](https://github.com/payloadcms/payload/issues/3902)) ([a4f36aa](https://github.com/payloadcms/payload/commit/a4f36aa8a009e9c0156924320bbcf1d04b697223))
|
||||
* only populates redirect references if passed via config ([2bbe428](https://github.com/payloadcms/payload/commit/2bbe4286f6c89092a8133938d0fec2cc31c0cb16))
|
||||
* overwrites incoming config arrays within field overrides ([3ca632b](https://github.com/payloadcms/payload/commit/3ca632bcbd49aeaafa86946a595307a95535a300))
|
||||
* **payload:** graphql query errors transaction race condition ([#3795](https://github.com/payloadcms/payload/issues/3795)) ([dc13b10](https://github.com/payloadcms/payload/commit/dc13b101f7351f7bae60a4a4bbc25907ed82210f))
|
||||
* payment form submissions ([36a8dc4](https://github.com/payloadcms/payload/commit/36a8dc49b34d75993fa981e3426db85536bf5f26))
|
||||
* properly exports types ([856962c](https://github.com/payloadcms/payload/commit/856962c6c621ab366e7c0b9089f3204389f3d322))
|
||||
* properly overrides form submission relationTo [#19](https://github.com/payloadcms/payload/issues/19) ([#27](https://github.com/payloadcms/payload/issues/27)) ([5b6705b](https://github.com/payloadcms/payload/commit/5b6705b4f62c8dfda87f1ceb5585e59277e22542))
|
||||
* reduces richtext indent padding ([b4610a3](https://github.com/payloadcms/payload/commit/b4610a3faef02cc7f6d440bb908a4623ed94fee8))
|
||||
* graphql query errors transaction race condition ([#3795](https://github.com/payloadcms/payload/issues/3795)) ([dc13b10](https://github.com/payloadcms/payload/commit/dc13b101f7351f7bae60a4a4bbc25907ed82210f))
|
||||
* removes conditional return of formattedEmails in sendEmail hook [#26](https://github.com/payloadcms/payload/issues/26) ([#28](https://github.com/payloadcms/payload/issues/28)) ([e8458f8](https://github.com/payloadcms/payload/commit/e8458f84bcd5bad74b189479931fbb7faea74900))
|
||||
* resize image if no aspect ratio change ([#3859](https://github.com/payloadcms/payload/issues/3859)) ([f53b713](https://github.com/payloadcms/payload/commit/f53b7131548dbe9071c65ba110f7f0d206bb33b5))
|
||||
* **richtext-*:** type issues with typescript strict mode enabled ([dac9514](https://github.com/payloadcms/payload/commit/dac9514eb00b99a3caeb9f217695b2b89368f7c9))
|
||||
* **richtext-lexical:** Blocks node incorrectly marked as client module ([35f00fa](https://github.com/payloadcms/payload/commit/35f00fa83d2a90967e0707ca0fd960c5608a3bf3))
|
||||
* **richtext-lexical:** remove unnecessary dependencies (fixes [#3889](https://github.com/payloadcms/payload/issues/3889)) ([760565f](https://github.com/payloadcms/payload/commit/760565f1e96e4cb1f6bce8663ad3fa8a16a2601c))
|
||||
* set date to 12UTC for default, dayOnly and monthOnly fields ([#3887](https://github.com/payloadcms/payload/issues/3887)) ([d393225](https://github.com/payloadcms/payload/commit/d3932252891bb8721a5abc97e204dbb6a7f3fda2))
|
||||
* skip following code if form.emails list is empty ([#54](https://github.com/payloadcms/payload/issues/54)) ([e13d8da](https://github.com/payloadcms/payload/commit/e13d8da7c229d7aa1caa487f20c4158e8ec0aeb5))
|
||||
* store resized image on req or tempFilePath ([#3883](https://github.com/payloadcms/payload/issues/3883)) ([6c5d525](https://github.com/payloadcms/payload/commit/6c5d525d8e1267eebdffeb9f31b2ef3a4df1132d))
|
||||
* thread through collection admin config properties - [#5](https://github.com/payloadcms/payload/issues/5) ([175d44b](https://github.com/payloadcms/payload/commit/175d44b0aeaec872421e0269c4b7d643f9cb8456))
|
||||
* threads locale through findByID ([#31](https://github.com/payloadcms/payload/issues/31)) ([a12240b](https://github.com/payloadcms/payload/commit/a12240b71e677c9b46f47117dec79e40a7de3564))
|
||||
* unique field error handling ([#3888](https://github.com/payloadcms/payload/issues/3888)) ([4d8d4c2](https://github.com/payloadcms/payload/commit/4d8d4c214ab12571e9dc71e406117c4b19f63c6b))
|
||||
* updates payload ([6940f2c](https://github.com/payloadcms/payload/commit/6940f2c0b7d36ad25fdc09ba93e23f5c6a095d6c))
|
||||
* updates richtext indentation within formbuilder ([ea22da4](https://github.com/payloadcms/payload/commit/ea22da4fc7285e4eab975e916d7627394eb4bb26))
|
||||
* uses form slug from config in form relationship field [#19](https://github.com/payloadcms/payload/issues/19) ([#23](https://github.com/payloadcms/payload/issues/23)) ([35e14cf](https://github.com/payloadcms/payload/commit/35e14cf044698acabe3a0902897ea276e4aef803))
|
||||
* who can say ([b398a92](https://github.com/payloadcms/payload/commit/b398a92db4f2402089fd03e803fefb337964d432))
|
||||
|
||||
## [2.0.13](https://github.com/payloadcms/payload/compare/v2.0.12...v2.0.13) (2023-10-24)
|
||||
|
||||
|
||||
@@ -89,3 +89,14 @@ If you are committing to [templates](./templates) or [examples](./examples), use
|
||||
## Pull Requests
|
||||
|
||||
For all Pull Requests, you should be extremely descriptive about both your problem and proposed solution. If there are any affected open or closed issues, please leave the issue number in your PR message.
|
||||
|
||||
## Previewing docs
|
||||
|
||||
This is how you can preview changes you made locally to the docs:
|
||||
|
||||
1. Clone our [website repository](https://github.com/payloadcms/website)
|
||||
2. Run `yarn install`
|
||||
3. Duplicate the `.env.example` file and rename it to `.env`
|
||||
4. Add a `DOCS_DIR` environment variable to the `.env` file which points to the absolute path of your modified docs folder. For example `DOCS_DIR=/Users/yourname/Documents/GitHub/payload/docs`
|
||||
5. Run `yarn run fetchDocs:local`. If this was successful, you should see no error messages and the following output: *Docs successfully written to /.../website/src/app/docs.json*. There could be error messages if you have incorrect markdown in your local docs folder. In this case, it will tell you how you can fix it
|
||||
6. You're done! Now you can start the website locally using `yarn run dev` and preview the docs under [http://localhost:3000/docs/](http://localhost:3000/docs/)
|
||||
|
||||
@@ -96,7 +96,7 @@ To swap out any of these views, simply pass in your custom component to the `adm
|
||||
}
|
||||
```
|
||||
|
||||
For more granular control, pass a configuration object instead. Payload exposes all of the properties of `<Route />` component in [React Router v5](https://v5.reactrouter.com):
|
||||
For more granular control, pass a configuration object instead. Each view corresponds to its own `<Route />` component in [React Router v5](https://v5.reactrouter.com). Payload exposes all of the properties of React Router:
|
||||
|
||||
| Property | Description |
|
||||
| ------------------ | ---------------------------------------------------------------------------------------------------------------------------- |
|
||||
@@ -129,13 +129,19 @@ To add a _new_ view to the Admin Panel, simply add another key to the `views` ob
|
||||
}
|
||||
```
|
||||
|
||||
<Banner type="warning">
|
||||
<strong>Note:</strong>
|
||||
<br />
|
||||
Routes are cascading. This means that unless explicitly given the `exact` property, they will match on URLs that simply _start_ with the route's path. This is helpful when creating catch-all routes in your application. Alternatively, you could define your nested route _before_ your parent route.
|
||||
</Banner>
|
||||
|
||||
_For more examples regarding how to customize components, look at the following [examples](https://github.com/payloadcms/payload/tree/main/test/admin/components)._
|
||||
|
||||
For help on how to build your own custom view components, see [building a custom view component](#building-a-custom-view-component).
|
||||
|
||||
### Collections
|
||||
|
||||
You can override components on a collection-by-collection basis via their `admin` property.
|
||||
You can override components on a collection-by-collection basis via the `admin.components` property.
|
||||
|
||||
| Path | Description |
|
||||
| -------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
|
||||
@@ -259,15 +265,15 @@ You can also add _new_ tabs to the `Edit` view by adding another key to the `com
|
||||
|
||||
### Globals
|
||||
|
||||
As with Collections, you can override components on a global-by-global basis via their `admin` property.
|
||||
As with Collections, you can override components on a global-by-global basis via the `admin.components` property.
|
||||
|
||||
| Path | Description |
|
||||
| -------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
|
||||
| **`edit.SaveButton`** | Replace the default `Save` button with a custom component. Drafts must be disabled |
|
||||
| **`edit.SaveDraftButton`** | Replace the default `Save Draft` button with a custom component. Drafts must be enabled and autosave must be disabled. |
|
||||
| **`edit.PublishButton`** | Replace the default `Publish` button with a custom component. Drafts must be enabled. |
|
||||
| **`edit.PreviewButton`** | Replace the default `Preview` button with a custom component. |
|
||||
| **`views`** | Override or create new views within the Payload Admin UI. [More](#global-views) |
|
||||
| Path | Description |
|
||||
| ------------------------------ | ---------------------------------------------------------------------------------------------------------------------- |
|
||||
| **`elements.SaveButton`** | Replace the default `Save` button with a custom component. Drafts must be disabled |
|
||||
| **`elements.SaveDraftButton`** | Replace the default `Save Draft` button with a custom component. Drafts must be enabled and autosave must be disabled. |
|
||||
| **`elements.PublishButton`** | Replace the default `Publish` button with a custom component. Drafts must be enabled. |
|
||||
| **`elements.PreviewButton`** | Replace the default `Preview` button with a custom component. |
|
||||
| **`views`** | Override or create new views within the Payload Admin UI. [More](#global-views) |
|
||||
|
||||
#### Global views
|
||||
|
||||
|
||||
@@ -91,14 +91,34 @@ This way when your bundler goes to import a file that contains server-only modul
|
||||
|
||||
To remove files that contain server-only modules from your bundle, you can use an `alias`.
|
||||
|
||||
First create new file that exports an empty object:
|
||||
```js
|
||||
// mocks/emptyObject.js
|
||||
In the Subscriptions config file above, we are importing the hook like so:
|
||||
|
||||
export default {}
|
||||
```ts
|
||||
// collections/Subscriptions/index.ts
|
||||
|
||||
import createStripeSubscription from './hooks/createStripeSubscription'
|
||||
```
|
||||
|
||||
Then, in your Payload config, you can alias the file containing the server-only module to the mock module. For example, here's how you'd do this in Webpack:
|
||||
By default the browser bundle will now include all the code from that file and any files down the tree. We know that the file imports `stripe`.
|
||||
|
||||
To fix this, we need to alias the `createStripeSubscription` file to a different file that can safely be included in the browser bundle.
|
||||
|
||||
First, we will create a mock file to replace the server-only file when bundling:
|
||||
```js
|
||||
// mocks/modules.js
|
||||
|
||||
export default {}
|
||||
|
||||
/**
|
||||
* NOTE: if you are destructuring an import
|
||||
* the mock file will need to export matching
|
||||
* variables as the destructured object.
|
||||
*
|
||||
* export const namedExport = {}
|
||||
*/
|
||||
```
|
||||
|
||||
Aliasing with [Webpack](/docs/admin/webpack) can be done by:
|
||||
|
||||
```ts
|
||||
// payload.config.ts
|
||||
@@ -106,10 +126,16 @@ Then, in your Payload config, you can alias the file containing the server-only
|
||||
import { buildConfig } from 'payload/config'
|
||||
import { webpackBundler } from '@payloadcms/bundler-webpack'
|
||||
|
||||
import { Subscriptions } from './collections/Subscriptions'
|
||||
|
||||
const mockModulePath = path.resolve(__dirname, 'mocks/emptyObject.js')
|
||||
const pathToFileWithServerOnlyModule = path.resolve(__dirname, 'hooks/syncStripeCustomer.ts')
|
||||
const fullFilePath = path.resolve(
|
||||
__dirname,
|
||||
'collections/Subscriptions/hooks/createStripeSubscription'
|
||||
)
|
||||
|
||||
export default buildConfig({
|
||||
collections: [Subscriptions],
|
||||
admin: {
|
||||
bundler: webpackBundler(),
|
||||
webpack: (config) => {
|
||||
@@ -120,7 +146,42 @@ export default buildConfig({
|
||||
// highlight-start
|
||||
alias: {
|
||||
...config.resolve.alias,
|
||||
[pathToFileWithServerOnlyModule]: mockModulePath,
|
||||
[fullFilePath]: mockModulePath,
|
||||
},
|
||||
// highlight-end
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
Aliasing with [Vite](/docs/admin/vite) can be done by:
|
||||
|
||||
```ts
|
||||
// payload.config.ts
|
||||
|
||||
import { buildConfig } from 'payload/config'
|
||||
import { viteBundler } from '@payloadcms/bundler-vite'
|
||||
|
||||
import { Subscriptions } from './collections/Subscriptions'
|
||||
|
||||
const mockModulePath = path.resolve(__dirname, 'mocks/emptyObject.js')
|
||||
|
||||
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
|
||||
},
|
||||
|
||||
@@ -5,6 +5,10 @@ order: 90
|
||||
desc: NEEDS TO BE WRITTEN
|
||||
---
|
||||
|
||||
<Banner type="info">
|
||||
The Vite bundler is currently in beta. If you would like to help us test this package, we'd love to hear from you if you find any [bugs or issues](https://github.com/payloadcms/payload/issues/)!
|
||||
</Banner>
|
||||
|
||||
Payload has a Vite bundler that you can install and bundle the Admin Panel with. This is an alternative to the [Webpack](/docs/admin/webpack) bundler and might give some performance boosts to your development workflow.
|
||||
|
||||
To use Vite as your bundler, first you need to install the package:
|
||||
@@ -13,9 +17,19 @@ To use Vite as your bundler, first you need to install the package:
|
||||
yarn add @payloadcms/bundler-vite
|
||||
```
|
||||
|
||||
<Banner>
|
||||
The Vite bundler is currently in beta. If you would like to help us test this package, we'd love to hear if you find any bugs or issues!
|
||||
</Banner>
|
||||
Then you will need to add the [bundler](/docs/admin/bundlers) to your Payload config:
|
||||
|
||||
```ts
|
||||
import { buildConfig } from '@payloadcms/config'
|
||||
import viteBundler from '@payloadcms/bundler-vite'
|
||||
|
||||
export default buildConfig({
|
||||
collections: [],
|
||||
admin: {
|
||||
bundler: viteBundler(),
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
Vite works fundamentally differently than Webpack. In development mode, it will first pre-bundle any of your dependencies that are CommonJS-only, and then it'll leverage ESM directly in your browser for a better HMR experience.
|
||||
|
||||
@@ -29,10 +43,37 @@ This is because Vite aliases work fundamentally differently than Webpack aliases
|
||||
|
||||
Here are the main differences between how Vite aliases work and how Webpack aliases work.
|
||||
|
||||
**Vite aliases do not work with absolute paths.**
|
||||
**Vite aliases do not work with absolute paths.**
|
||||
|
||||
In Vite, an alias will only match if the `find` property _exactly matches_ how you are importing your server-only file. So if you are importing a file with a relative path, i.e. `'../../my-module'`, and your alias is absolute, your alias will not work.
|
||||
In Vite, alias keys must <strong>exactly match</strong> a import paths. If you have 2 files that import the same server-only module, but have different import paths, you would need to add 2 aliases to support both import paths.
|
||||
|
||||
```ts
|
||||
// File A
|
||||
import serverOnlyModule from '../server-only-module'
|
||||
|
||||
// File B
|
||||
import serverOnlyModule from '../../server-only-module'
|
||||
|
||||
// payload.config.ts
|
||||
// You would need to add 2 aliases to support both import paths
|
||||
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 aliases do not get applied to pre-bundled dependencies.**
|
||||
|
||||
@@ -58,7 +99,7 @@ This will effectively alias the entire plugin and work with Vite. If the plugin
|
||||
|
||||
### Extending the Vite config
|
||||
|
||||
The Payload config supports a new property for plugins to be able to extend the Vite config specifically. That property exists on the main Payload config under `admin.vite`.
|
||||
The Payload config supports a new property for plugins to be able to extend the Vite config specifically. That property exists on the main Payload config under `admin.vite`. You can check out the [Vite docs](https://vitejs.dev/config/shared-options.html) for more information on what you can do with the Vite config.
|
||||
|
||||
It's a function that takes a Vite config, and returns an updated Vite config. Here's an example:
|
||||
|
||||
@@ -66,17 +107,24 @@ It's a function that takes a Vite config, and returns an updated Vite config. He
|
||||
export const buildConfig({
|
||||
collections: [],
|
||||
admin: {
|
||||
bundler: viteBundler(),
|
||||
vite: (incomingViteConfig) => ({
|
||||
...incomingViteConfig,
|
||||
resolve: {
|
||||
...incomingViteConfig.resolve,
|
||||
// Do whatever you need here
|
||||
...(incomingViteConfig?.resolve || {}),
|
||||
alias: {
|
||||
...(incomingViteConfig?.resolve?.alias || {}),
|
||||
// custom aliases go here
|
||||
'../server-only-module': path.resolve(__dirname, './path/to/browser-safe-module.js'),
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
Learn more about [aliasing server-only modules](http://localhost:3000/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.
|
||||
|
||||
If your config specifies Webpack aliases, we attempt to leverage them automatically within the Vite config. They are merged into the Vite alias configuration seamlessly and may work out-of-the-box.
|
||||
If your config specifies Webpack aliases, we attempt to leverage them automatically within the Vite config. They are merged into the Vite alias configuration seamlessly and may work out-of-the-box.
|
||||
|
||||
@@ -53,6 +53,7 @@ export const Page: CollectionConfig = {
|
||||
- [Rich Text](/docs/fields/rich-text) - fully extensible Rich Text editor
|
||||
- [Row](/docs/fields/row) - used for admin field layout, no effect on data shape
|
||||
- [Select](/docs/fields/select) - dropdown / picklist style value selector
|
||||
- [Tabs](/docs/fields/tabs) - used for admin layout, nest fields within tabs
|
||||
- [Text](/docs/fields/text) - simple text input
|
||||
- [Textarea](/docs/fields/textarea) - allows a bit larger of a text editor
|
||||
- [Upload](/docs/fields/upload) - allows local file and image upload
|
||||
|
||||
@@ -267,3 +267,27 @@ Relationship fields with `hasMany` set to more than one kind of collections save
|
||||
Querying is done in the same way as the earlier Polymorphic example:
|
||||
|
||||
`?where[owners.value][equals]=6031ac9e1289176380734024`.
|
||||
|
||||
|
||||
#### Querying and Filtering Polymorphic Relationships
|
||||
|
||||
Polymorphic and non-polymorphic relationships must be queried differently because of how the related data is stored and may be inconsistent across different collections. Because of this, filtering polymorphic relationship fields from the Collection List admin UI is limited to the `id` value.
|
||||
|
||||
For a polymorphic relationship, the response will always be an array of objects. Each object will contain the `relationTo` and `value` properties.
|
||||
|
||||
The data can be queried by the related document ID:
|
||||
|
||||
`?where[field.value][equals]=6031ac9e1289176380734024`.
|
||||
|
||||
Or by the related document Collection slug:
|
||||
|
||||
`?where[field.relationTo][equals]=your-collection-slug`.
|
||||
|
||||
However, you **cannot** query on any field values within the related document.
|
||||
Since we are referencing multiple collections, the field you are querying on may not exist and break the query.
|
||||
|
||||
<Banner type="warning">
|
||||
<strong>Note:</strong>
|
||||
<br />
|
||||
You <strong>cannot</strong> query on a field within a polymorphic relationship as you would with a non-polymorphic relationship.
|
||||
</Banner>
|
||||
|
||||
@@ -400,6 +400,70 @@ 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.
|
||||
|
||||
As a workaround, we recommend leveraging the following pattern to determine and ensure Payload is initalized:
|
||||
|
||||
```
|
||||
import dotenv from 'dotenv'
|
||||
import path from 'path'
|
||||
import type { Payload } from 'payload'
|
||||
import payload from 'payload'
|
||||
import type { InitOptions } from 'payload/config'
|
||||
import { seed as seedData } from './seed'
|
||||
|
||||
dotenv.config({
|
||||
path: path.resolve(__dirname, '../.env'),
|
||||
})
|
||||
|
||||
let cached = (global as any).payload
|
||||
|
||||
if (!cached) {
|
||||
cached = (global as any).payload = { client: null, promise: null }
|
||||
}
|
||||
|
||||
interface Args {
|
||||
initOptions?: Partial<InitOptions>
|
||||
seed?: boolean
|
||||
}
|
||||
|
||||
export const getPayloadClient = async ({ initOptions, seed }: Args = {}): Promise<Payload> => {
|
||||
if (!process.env.DATABASE_URI) {
|
||||
throw new Error('DATABASE_URI environment variable is missing')
|
||||
}
|
||||
if (!process.env.PAYLOAD_SECRET) {
|
||||
throw new Error('PAYLOAD_SECRET environment variable is missing')
|
||||
}
|
||||
if (cached.client) {
|
||||
return cached.client
|
||||
}
|
||||
if (!cached.promise) {
|
||||
cached.promise = payload.init({
|
||||
mongoURL: process.env.DATABASE_URI,
|
||||
secret: process.env.PAYLOAD_SECRET,
|
||||
local: initOptions?.express ? false : true,
|
||||
...(initOptions || {}),
|
||||
})
|
||||
}
|
||||
try {
|
||||
process.env.PAYLOAD_DROP_DATABASE = seed ? 'true' : 'false'
|
||||
cached.client = await cached.promise
|
||||
if (seed) {
|
||||
payload.logger.info('---- SEEDING DATABASE ----')
|
||||
await seedData(payload)
|
||||
}
|
||||
} catch (e: unknown) {
|
||||
cached.promise = null
|
||||
throw e
|
||||
}
|
||||
return cached.client
|
||||
}
|
||||
```
|
||||
|
||||
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
|
||||
|
||||
The Local API is especially useful for running scripts
|
||||
|
||||
@@ -158,6 +158,17 @@ DigitalOcean provides extremely helpful documentation that can walk you through
|
||||
1. [Create a new MongoDB and user](https://medium.com/@mhagemann/how-to-add-a-new-user-to-a-mongodb-database-d896776b5362)
|
||||
1. [Set up Node for production](https://www.digitalocean.com/community/tutorials/how-to-set-up-a-node-js-application-for-production-on-ubuntu-20-04)
|
||||
|
||||
### Swap Space
|
||||
|
||||
Swap refers to a section of storage on the hard drive that is reserved to temporarily store data that can no longer fit within RAM. This allows for the expansion of your server's working memory, with some limitations. Swap space comes into play when available RAM can no longer accommodate actively used application data, enabling the system to continue functioning.
|
||||
|
||||
Insufficient space can lead to deployment errors and memory-related issues, resulting in application crashes, sluggish performance, or an unresponsive server.
|
||||
|
||||
Common deployment error due to **space limitations** (as reported by users):
|
||||
- `Error: Command failed with exit code 1`
|
||||
|
||||
To configure swap, we recommend following this tutorial on [How To Add Swap Space](https://www.digitalocean.com/community/tutorials/how-to-add-swap-space-on-ubuntu-22-04).
|
||||
|
||||
## Docker
|
||||
|
||||
This is an example of a multi-stage docker build of Payload for production. Ensure you are setting your environment variables on deployment, like `PAYLOAD_SECRET`, `PAYLOAD_CONFIG_PATH`, and `DATABASE_URI` if needed.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
title: Lexical Rich Text
|
||||
label: Lexical
|
||||
order: 30
|
||||
desc: Built by Meta, Lexical is an incredibly powerful rich text editor and it works beautifully within Payload.
|
||||
desc: Built by Meta, Lexical is an incredibly powerful rich text editor, and it works beautifully within Payload.
|
||||
keywords: lexical, rich text, editor, headless cms
|
||||
---
|
||||
|
||||
@@ -45,7 +45,39 @@ export default buildConfig({
|
||||
You can also override Lexical settings on a field-by-field basis as follows:
|
||||
|
||||
```ts
|
||||
import { CollectionConfig } from 'payload/types'
|
||||
import type { CollectionConfig } from 'payload/types'
|
||||
import {
|
||||
lexicalEditor
|
||||
} from '@payloadcms/richtext-lexical'
|
||||
|
||||
export const Pages: CollectionConfig = {
|
||||
slug: 'pages',
|
||||
fields: [
|
||||
{
|
||||
name: 'content',
|
||||
type: 'richText',
|
||||
// Pass the Lexical editor here and override base settings as necessary
|
||||
editor: lexicalEditor({})
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Extending the lexical editor with Features
|
||||
|
||||
Lexical has been designed with extensibility in mind. Whether you're aiming to introduce new functionalities or tweak the existing ones, Lexical makes it seamless for you to bring those changes to life.
|
||||
|
||||
### Features: The Building Blocks
|
||||
|
||||
At the heart of Lexical's customization potential are "features". While Lexical ships with a set of default features we believe are essential for most use cases, the true power lies in your ability to redefine, expand, or prune these as needed.
|
||||
|
||||
If you remove all the default features, you're left with a blank editor. You can then add in only the features you need, or you can build your own custom features from scratch.
|
||||
|
||||
### Integrating New Features
|
||||
|
||||
To weave in your custom features, utilize the `features` prop when initializing the Lexical Editor. Here's a basic example of how this is done:
|
||||
|
||||
```ts
|
||||
import {
|
||||
BlocksFeature,
|
||||
LinkFeature,
|
||||
@@ -55,62 +87,508 @@ import {
|
||||
import { Banner } from '../blocks/Banner'
|
||||
import { CallToAction } from '../blocks/CallToAction'
|
||||
|
||||
export const Pages: CollectionConfig = {
|
||||
{
|
||||
editor: lexicalEditor({
|
||||
features: ({ defaultFeatures }) => [
|
||||
...defaultFeatures,
|
||||
LinkFeature({
|
||||
// Example showing how to customize the built-in fields
|
||||
// of the Link feature
|
||||
fields: [
|
||||
{
|
||||
name: 'rel',
|
||||
label: 'Rel Attribute',
|
||||
type: 'select',
|
||||
hasMany: true,
|
||||
options: ['noopener', 'noreferrer', 'nofollow'],
|
||||
admin: {
|
||||
description:
|
||||
'The rel attribute defines the relationship between a linked resource and the current document. This is a custom link field.',
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
UploadFeature({
|
||||
collections: {
|
||||
uploads: {
|
||||
// Example showing how to customize the built-in fields
|
||||
// of the Upload feature
|
||||
fields: [
|
||||
{
|
||||
name: 'caption',
|
||||
type: 'richText',
|
||||
editor: lexicalEditor(),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}),
|
||||
// This is incredibly powerful. You can re-use your Payload blocks
|
||||
// directly in the Lexical editor as follows:
|
||||
BlocksFeature({
|
||||
blocks: [
|
||||
Banner,
|
||||
CallToAction,
|
||||
],
|
||||
}),
|
||||
]
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## Features overview
|
||||
|
||||
Here's an overview of all the included features:
|
||||
|
||||
| Feature Name | Included by default | Description |
|
||||
|--------------------------------|---------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| **`BoldTextFeature`** | Yes | Handles the bold text format |
|
||||
| **`ItalicTextFeature`** | Yes | Handles the italic text format |
|
||||
| **`UnderlineTextFeature`** | Yes | Handles the underline text format |
|
||||
| **`StrikethroughTextFeature`** | Yes | Handles the strikethrough text format |
|
||||
| **`SubscriptTextFeature`** | Yes | Handles the subscript text format |
|
||||
| **`SuperscriptTextFeature`** | Yes | Handles the superscript text format |
|
||||
| **`InlineCodeTextFeature`** | Yes | Handles the inline-code text format |
|
||||
| **`ParagraphFeature`** | Yes | Handles paragraphs. Since they are already a key feature of lexical itself, this Feature mainly handles the Slash and Add-Block menu entries for paragraphs |
|
||||
| **`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) |
|
||||
| **`OrderedListFeature`** | Yes | Adds ordered lists (ul) |
|
||||
| **`CheckListFeature`** | Yes | Adds checklists |
|
||||
| **`LinkFeature`** | Yes | Allows you to create internal and external links |
|
||||
| **`RelationshipFeature`** | Yes | Allows you to create block-level (not inline) relationships to other documents |
|
||||
| **`BlockQuoteFeature`** | Yes | Allows you to create block-level quotes |
|
||||
| **`UploadFeature`** | Yes | Allows you to create block-level upload nodes - this supports all kinds of uploads, not just images |
|
||||
| **`BlocksFeature`** | No | Allows you to use Payload's [Blocks Field](/docs/fields/blocks) directly inside your editor. In the feature props, you can specify the allowed blocks - just like in the Blocks field. |
|
||||
| **`TreeViewFeature`** | No | Adds a debug box under the editor, which allows you to see the current editor state live, the dom, as well as time travel. Very useful for debugging |
|
||||
|
||||
## Creating your own, custom Feature
|
||||
|
||||
Creating your own custom feature requires deep knowledge of the Lexical editor. We recommend you take a look at the [Lexical documentation](https://lexical.dev/docs/intro) first - especially the "concepts" section.
|
||||
|
||||
Next, take a look at the [features we've already built](https://github.com/payloadcms/payload/tree/main/packages/richtext-lexical/src/field/features) - understanding how they work will help you understand how to create your own. There is no difference between the features included by default and the ones you create yourself - since those features are all isolated from the "core", you have access to the same APIs, whether the feature is part of payload or not!
|
||||
|
||||
## Converters
|
||||
|
||||
### Lexical => HTML
|
||||
|
||||
Lexical saves data in JSON, but can also generate its HTML representation via two main methods:
|
||||
|
||||
1. **Outputting HTML from the Collection:** Create a new field in your collection to convert saved JSON content to HTML. Payload generates and outputs the HTML for use in your frontend.
|
||||
2. **Generating HTML on the Frontend:** Convert JSON to HTML on-demand, either in your frontend or elsewhere.
|
||||
|
||||
The editor comes with built-in HTML serializers, simplifying the process of converting JSON to HTML.
|
||||
|
||||
#### Outputting HTML from the Collection
|
||||
|
||||
To add HTML generation directly within the collection, follow the example below:
|
||||
|
||||
```ts
|
||||
import type { CollectionConfig } from 'payload/types'
|
||||
|
||||
import {
|
||||
HTMLConverterFeature,
|
||||
lexicalEditor,
|
||||
lexicalHTML
|
||||
} from '@payloadcms/richtext-lexical'
|
||||
|
||||
const Pages: CollectionConfig = {
|
||||
slug: 'pages',
|
||||
fields: [
|
||||
{
|
||||
name: 'content',
|
||||
name: 'nameOfYourRichTextField',
|
||||
type: 'richText',
|
||||
// Pass the Lexical editor here and override base settings as necessary
|
||||
editor: lexicalEditor({
|
||||
features: ({ defaultFeatures }) => [
|
||||
...defaultFeatures,
|
||||
LinkFeature({
|
||||
// Example showing how to customize the built-in fields
|
||||
// of the Link feature
|
||||
fields: [
|
||||
{
|
||||
name: 'rel',
|
||||
label: 'Rel Attribute',
|
||||
type: 'select',
|
||||
hasMany: true,
|
||||
options: ['noopener', 'noreferrer', 'nofollow'],
|
||||
admin: {
|
||||
description:
|
||||
'The rel attribute defines the relationship between a linked resource and the current document. This is a custom link field.',
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
UploadFeature({
|
||||
collections: {
|
||||
uploads: {
|
||||
// Example showing how to customize the built-in fields
|
||||
// of the Upload feature
|
||||
fields: [
|
||||
{
|
||||
name: 'caption',
|
||||
type: 'richText',
|
||||
editor: lexicalEditor(),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}),
|
||||
// This is incredibly powerful. You can re-use your Payload blocks
|
||||
// directly in the Lexical editor as follows:
|
||||
BlocksFeature({
|
||||
blocks: [
|
||||
Banner,
|
||||
CallToAction,
|
||||
],
|
||||
}),
|
||||
]
|
||||
})
|
||||
}
|
||||
]
|
||||
// The HTMLConverter Feature is the feature which manages the HTML serializers. If you do not pass any arguments to it, it will use the default serializers.
|
||||
HTMLConverterFeature({}),
|
||||
],
|
||||
}),
|
||||
},
|
||||
lexicalHTML('nameOfYourRichTextField', { name: 'nameOfYourRichTextField_html' }),
|
||||
],
|
||||
}
|
||||
```
|
||||
The `lexicalHTML()` function creates a new field that automatically converts the referenced lexical richText field into HTML through an afterRead hook.
|
||||
|
||||
#### Generating HTML in the Frontend:
|
||||
|
||||
If you wish to convert JSON to HTML ad-hoc, use this code snippet:
|
||||
|
||||
```ts
|
||||
import type { SerializedEditorState } from 'lexical'
|
||||
import {
|
||||
type SanitizedEditorConfig,
|
||||
convertLexicalToHTML,
|
||||
consolidateHTMLConverters,
|
||||
} from '@payloadcms/richtext-lexical'
|
||||
|
||||
async function lexicalToHTML(editorData: SerializedEditorState, editorConfig: SanitizedEditorConfig) {
|
||||
return await convertLexicalToHTML({
|
||||
converters: consolidateHTMLConverters({ editorConfig }),
|
||||
data: editorData,
|
||||
})
|
||||
}
|
||||
```
|
||||
This method employs `convertLexicalToHTML` from `@payloadcms/richtext-lexical`, which converts the serialized editor state into HTML.
|
||||
|
||||
Because every `Feature` is able to provide html converters, and because the `htmlFeature` can modify those or provide their own, we need to consolidate them with the default html Converters using the `consolidateHTMLConverters` function.
|
||||
|
||||
#### Creating your own HTML Converter
|
||||
|
||||
HTML Converters are typed as `HTMLConverter`, which contains the node type it should handle, and a function that accepts the serialized node from the lexical editor, and outputs the HTML string. Here's the HTML Converter of the Upload node as an example:
|
||||
|
||||
```ts
|
||||
import type { HTMLConverter } from '@payloadcms/richtext-lexical'
|
||||
import payload from 'payload'
|
||||
|
||||
const UploadHTMLConverter: HTMLConverter<SerializedUploadNode> = {
|
||||
converter: async ({ node }) => {
|
||||
const uploadDocument = await payload.findByID({
|
||||
id: node.value.id,
|
||||
collection: node.relationTo,
|
||||
})
|
||||
const url = (payload?.config?.serverURL || '') + uploadDocument?.url
|
||||
|
||||
if (!(uploadDocument?.mimeType as string)?.startsWith('image')) {
|
||||
// Only images can be serialized as HTML
|
||||
return ``
|
||||
}
|
||||
|
||||
return `<img src="${url}" alt="${uploadDocument?.filename}" width="${uploadDocument?.width}" height="${uploadDocument?.height}"/>`
|
||||
},
|
||||
nodeTypes: [UploadNode.getType()], // This is the type of the lexical node that this converter can handle. Instead of hardcoding 'upload' we can get the node type directly from the UploadNode, since it's static.
|
||||
}
|
||||
```
|
||||
|
||||
As you can see, we have access to all the information saved in the node (for the Upload node, this is `value`and `relationTo`) and we can use that to generate the HTML.
|
||||
|
||||
The `convertLexicalToHTML` is part of `@payloadcms/richtext-lexical` automatically handles traversing the editor state and calling the correct converter for each node.
|
||||
|
||||
#### Embedding the HTML Converter in your Feature
|
||||
|
||||
You can embed your HTML Converter directly within your custom `Feature`, allowing it to be handled automatically by the `consolidateHTMLConverters` function. Here is an example:
|
||||
|
||||
```ts
|
||||
export const UploadFeature = (props?: UploadFeatureProps): FeatureProvider => {
|
||||
return {
|
||||
feature: () => {
|
||||
return {
|
||||
nodes: [
|
||||
{
|
||||
converters: {
|
||||
html: yourHTMLConverter, // <= This is where you define your HTML Converter
|
||||
},
|
||||
node: UploadNode,
|
||||
type: UploadNode.getType(),
|
||||
//...
|
||||
},
|
||||
],
|
||||
plugins: [/*...*/],
|
||||
props: props,
|
||||
slashMenu: {/*...*/},
|
||||
}
|
||||
},
|
||||
key: 'upload',
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Headless Editor
|
||||
|
||||
Lexical provides a seamless way to perform conversions between various other formats:
|
||||
- HTML to Lexical (or, importing HTML into the lexical editor)
|
||||
- Markdown to Lexical (or, importing Markdown into the lexical editor)
|
||||
- Lexical to Markdown
|
||||
|
||||
A headless editor can perform such conversions outside of the main editor instance. Follow this method to initiate a headless editor:
|
||||
|
||||
```ts
|
||||
import { createHeadlessEditor } from '@lexical/headless' // <= make sure this package is installed
|
||||
import {
|
||||
getEnabledNodes,
|
||||
sanitizeEditorConfig,
|
||||
} from '@payloadcms/richtext-lexical'
|
||||
|
||||
const yourEditorConfig; // <= your editor config here
|
||||
|
||||
const headlessEditor = await createHeadlessEditor({
|
||||
nodes: getEnabledNodes({
|
||||
editorConfig: sanitizeEditorConfig(yourEditorConfig),
|
||||
}),
|
||||
})
|
||||
```
|
||||
|
||||
### HTML => Lexical
|
||||
|
||||
Once you have your headless editor instance, you can use it to convert HTML to Lexical:
|
||||
|
||||
```ts
|
||||
import { $generateNodesFromDOM } from '@lexical/html'
|
||||
import { $getRoot,$getSelection } from 'lexical'
|
||||
|
||||
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)
|
||||
|
||||
// Select the root
|
||||
$getRoot().select()
|
||||
|
||||
// Insert them at a selection.
|
||||
const selection = $getSelection()
|
||||
selection.insertNodes(nodes)
|
||||
}, { discrete: true })
|
||||
|
||||
// Do this if you then want to get the editor JSON
|
||||
const editorJSON = headlessEditor.getEditorState().toJSON()
|
||||
```
|
||||
|
||||
This has been taken from the [lexical serialization & deserialization docs](https://lexical.dev/docs/concepts/serialization#html---lexical).
|
||||
|
||||
<Banner type="success">
|
||||
<strong>Note:</strong>
|
||||
<br />
|
||||
Using the <code>discrete: true</code> flag ensures instant updates to the editor state. If immediate reading of the updated state isn't necessary, you can omit the flag.
|
||||
</Banner>
|
||||
|
||||
### Markdown => Lexical
|
||||
|
||||
Convert markdown content to the Lexical editor format with the following:
|
||||
|
||||
```ts
|
||||
import { $convertFromMarkdownString } from '@lexical/markdown'
|
||||
import { sanitizeEditorConfig } from '@payloadcms/richtext-lexical'
|
||||
|
||||
const yourSanitizedEditorConfig = sanitizeEditorConfig(yourEditorConfig) // <= your editor config here
|
||||
const markdown = `# Hello World`
|
||||
|
||||
headlessEditor.update(() => { $convertFromMarkdownString(markdown, yourSanitizedEditorConfig.features.markdownTransformers) }, { discrete: true })
|
||||
|
||||
// Do this if you then want to get the editor JSON
|
||||
const editorJSON = headlessEditor.getEditorState().toJSON()
|
||||
```
|
||||
|
||||
### Lexical => Markdown
|
||||
|
||||
Export content from the Lexical editor into Markdown format using these steps:
|
||||
|
||||
1. Import your current editor state into the headless editor.
|
||||
2. Convert and fetch the resulting markdown string.
|
||||
|
||||
Here's the code for it:
|
||||
|
||||
```ts
|
||||
import { $convertToMarkdownString } from '@lexical/markdown'
|
||||
import { sanitizeEditorConfig } from '@payloadcms/richtext-lexical'
|
||||
import type { SerializedEditorState } from "lexical"
|
||||
|
||||
const yourSanitizedEditorConfig = sanitizeEditorConfig(yourEditorConfig) // <= your editor config here
|
||||
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')
|
||||
return ''
|
||||
}
|
||||
|
||||
// Export to markdown
|
||||
let markdown: string
|
||||
headlessEditor.getEditorState().read(() => {
|
||||
markdown = $convertToMarkdownString(yourSanitizedEditorConfig?.features?.markdownTransformers)
|
||||
})
|
||||
```
|
||||
|
||||
The `.setEditorState()` function immediately updates your editor state. Thus, there's no need for the `discrete: true` flag when reading the state afterward.
|
||||
|
||||
## Migrating from Slate
|
||||
|
||||
While both Slate and Lexical save the editor state in JSON, the structure of the JSON is different.
|
||||
|
||||
### Migration via SlateToLexicalFeature
|
||||
|
||||
One way to handle this is to just give your lexical editor the ability to read the slate JSON.
|
||||
|
||||
Simply add the `SlateToLexicalFeature` to your editor:
|
||||
|
||||
```ts
|
||||
import type { CollectionConfig } from 'payload/types'
|
||||
|
||||
import {
|
||||
SlateToLexicalFeature,
|
||||
lexicalEditor,
|
||||
} from '@payloadcms/richtext-lexical'
|
||||
|
||||
const Pages: CollectionConfig = {
|
||||
slug: 'pages',
|
||||
fields: [
|
||||
{
|
||||
name: 'nameOfYourRichTextField',
|
||||
type: 'richText',
|
||||
editor: lexicalEditor({
|
||||
features: ({ defaultFeatures }) => [
|
||||
...defaultFeatures,
|
||||
SlateToLexicalFeature({})
|
||||
],
|
||||
}),
|
||||
},
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
and done! Now, everytime this lexical editor is initialized, it converts the slate date to lexical on-the-fly. If the data is already in lexical format, it will just pass it through.
|
||||
|
||||
This is by far the easiest way to migrate from Slate to Lexical, although it does come with a few caveats:
|
||||
- There is a performance hit when initializing the lexical editor
|
||||
- The editor will still output the Slate data in the output JSON, as the on-the-fly converter only runs for the admin panel
|
||||
|
||||
The easy way to solve this: Just save the document! This overrides the slate data with the lexical data, and the next time the document is loaded, the lexical data will be used. This solves both the performance and the output issue for that specific document.
|
||||
|
||||
### Migration via migration script
|
||||
|
||||
The method described above does not solve the issue for all documents, though. If you want to convert all your documents to lexical, you can use a migration script. Here's a simple example:
|
||||
|
||||
```ts
|
||||
import type { Payload } from 'payload'
|
||||
import type { YourDocumentType } from 'payload/generated-types'
|
||||
|
||||
import {
|
||||
cloneDeep,
|
||||
convertSlateToLexical,
|
||||
defaultSlateConverters,
|
||||
} from '@payloadcms/richtext-lexical'
|
||||
|
||||
import { AnotherCustomConverter } from './lexicalFeatures/converters/AnotherCustomConverter'
|
||||
|
||||
export async function convertAll(payload: Payload, collectionName: string, fieldName: string) {
|
||||
const docs: YourDocumentType[] = await payload.db.collections[collectionName].find({}).exec() // Use MongoDB models directly to query all documents at once
|
||||
console.log(`Found ${docs.length} ${collectionName} docs`)
|
||||
|
||||
const converters = cloneDeep([...defaultSlateConverters, AnotherCustomConverter])
|
||||
|
||||
// Split docs into batches of 20.
|
||||
const batchSize = 20
|
||||
const batches = []
|
||||
for (let i = 0; i < docs.length; i += batchSize) {
|
||||
batches.push(docs.slice(i, i + batchSize))
|
||||
}
|
||||
|
||||
let processed = 0 // Number of processed docs
|
||||
|
||||
for (const batch of batches) {
|
||||
// Process each batch asynchronously
|
||||
const promises = batch.map(async (doc: YourDocumentType) => {
|
||||
const richText = doc[fieldName]
|
||||
|
||||
if (richText && Array.isArray(richText) && !('root' in richText)) { // It's Slate data - skip already-converted data
|
||||
const converted = convertSlateToLexical({
|
||||
converters: converters,
|
||||
slateData: richText,
|
||||
})
|
||||
|
||||
await payload.update({
|
||||
id: doc.id,
|
||||
collection: collectionName as any,
|
||||
data: {
|
||||
[fieldName]: converted,
|
||||
},
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// Wait for all promises in the batch to complete. Resolving batches of 20 asynchronously is faster than waiting for each doc to update individually
|
||||
await Promise.all(promises)
|
||||
|
||||
// Update the count of processed docs
|
||||
processed += batch.length
|
||||
console.log(`Converted ${processed} of ${docs.length}`)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The `convertSlateToLexical` is the same method used in the `SlateToLexicalFeature` - it handles traversing the Slate JSON for you.
|
||||
|
||||
Do note that this script might require adjustment depending on your document structure, especially if you have nested richText fields or localization enabled.
|
||||
|
||||
### Converting custom Slate nodes
|
||||
|
||||
If you have custom Slate nodes, create a custom converter for them. Here's the Upload converter as an example:
|
||||
|
||||
```ts
|
||||
import type { SerializedUploadNode } from '../uploadNode.'
|
||||
import type { SlateNodeConverter } from '@payloadcms/richtext-lexical'
|
||||
|
||||
export const SlateUploadConverter: SlateNodeConverter = {
|
||||
converter({ slateNode }) {
|
||||
return {
|
||||
fields: {
|
||||
...slateNode.fields,
|
||||
},
|
||||
format: '',
|
||||
relationTo: slateNode.relationTo,
|
||||
type: 'upload',
|
||||
value: {
|
||||
id: slateNode.value?.id || '',
|
||||
},
|
||||
version: 1,
|
||||
} as const as SerializedUploadNode
|
||||
},
|
||||
nodeTypes: ['upload'],
|
||||
}
|
||||
```
|
||||
|
||||
It's pretty simple: You get a Slate node as input, and you return the lexical node. The `nodeTypes` array is used to determine which Slate nodes this converter can handle.
|
||||
|
||||
When using a migration script, you can add your custom converters to the `converters` property of the `convertSlateToLexical` props, as seen in the example above
|
||||
|
||||
When using the `SlateToLexicalFeature`, you can add your custom converters to the `converters` property of the `SlateToLexicalFeature` props:
|
||||
|
||||
```ts
|
||||
import type { CollectionConfig } from 'payload/types'
|
||||
|
||||
import {
|
||||
SlateToLexicalFeature,
|
||||
lexicalEditor,
|
||||
defaultSlateConverters
|
||||
} from '@payloadcms/richtext-lexical'
|
||||
|
||||
import { YourCustomConverter } from '../converters/YourCustomConverter'
|
||||
|
||||
const Pages: CollectionConfig = {
|
||||
slug: 'pages',
|
||||
fields: [
|
||||
{
|
||||
name: 'nameOfYourRichTextField',
|
||||
type: 'richText',
|
||||
editor: lexicalEditor({
|
||||
features: ({ defaultFeatures }) => [
|
||||
...defaultFeatures,
|
||||
SlateToLexicalFeature({
|
||||
converters: [
|
||||
...defaultSlateConverters,
|
||||
YourCustomConverter
|
||||
]
|
||||
}),
|
||||
],
|
||||
}),
|
||||
},
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
## Migrating from payload-plugin-lexical
|
||||
|
||||
Migrating from [payload-plugin-lexical](https://github.com/AlessioGr/payload-plugin-lexical) works similar to migrating from Slate.
|
||||
|
||||
Instead of a `SlateToLexicalFeature` there is a `LexicalPluginToLexicalFeature` you can use. And instead of `convertSlateToLexical` you can use `convertLexicalPluginToLexical`.
|
||||
|
||||
## Coming Soon
|
||||
|
||||
Lots more documentation will be coming soon, which will show in detail how to create your own custom features within Lexical.
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ Collections and Globals both support the same options for configuring autosave.
|
||||
|
||||
| Drafts Autosave Options | Description |
|
||||
| ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `interval` | Define an `interval` in milliseconds to automatically save progress while documents are edited. Document updates are "debounced" at this interval. Defaults to `2000`. |
|
||||
| `interval` | Define an `interval` in milliseconds to automatically save progress while documents are edited. Document updates are "debounced" at this interval. Defaults to `800`. |
|
||||
|
||||
**Example config with versions, drafts, and autosave enabled:**
|
||||
|
||||
@@ -66,7 +66,7 @@ When `autosave` is enabled, all `update` operations within Payload expose a new
|
||||
|
||||
#### How autosaves are stored
|
||||
|
||||
If we created a new version for each autosave, you'd quickly find a ton of autosaves that clutter up your `_versions` collection within the database. That would be messy quick because `autosave` is typically set to save a document every ~2000ms or so.
|
||||
If we created a new version for each autosave, you'd quickly find a ton of autosaves that clutter up your `_versions` collection within the database. That would be messy quick because `autosave` is typically set to save a document at ~800ms intervals.
|
||||
|
||||
<Banner type="success">
|
||||
Instead of creating a new version each time a document is autosaved, Payload smartly only creates{' '}
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
"script:release": "tsx ./scripts/release.ts",
|
||||
"test": "pnpm test:int && pnpm test:components && pnpm test:e2e",
|
||||
"test:components": "cross-env jest --config=jest.components.config.js",
|
||||
"test:e2e": "npx playwright install --with-deps && ts-node -T ./test/runE2E.ts",
|
||||
"test:e2e": "npx playwright install --with-deps chromium && ts-node -T ./test/runE2E.ts",
|
||||
"test:e2e:debug": "cross-env PWDEBUG=1 DISABLE_LOGGING=true playwright test",
|
||||
"test:e2e:headed": "cross-env DISABLE_LOGGING=true playwright test --headed",
|
||||
"test:int:postgres": "cross-env PAYLOAD_DATABASE=postgres DISABLE_LOGGING=true jest --forceExit --detectOpenHandles",
|
||||
|
||||
@@ -91,6 +91,7 @@ export const getViteConfig = async (payloadConfig: SanitizedConfig): Promise<Inl
|
||||
// Dependencies that need aliases should be excluded
|
||||
// from pre-bundling
|
||||
'@payloadcms/bundler-vite',
|
||||
...(Object.keys(absoluteAliases) || []),
|
||||
],
|
||||
include: ['payload/components/root', 'react-dom/client'],
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/bundler-webpack",
|
||||
"version": "1.0.4",
|
||||
"version": "1.0.5",
|
||||
"description": "The officially supported Webpack bundler adapter for Payload",
|
||||
"repository": "https://github.com/payloadcms/payload",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-mongodb",
|
||||
"version": "1.0.5",
|
||||
"version": "1.0.6",
|
||||
"description": "The officially supported MongoDB database adapter for Payload",
|
||||
"repository": "https://github.com/payloadcms/payload",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -7,9 +7,17 @@ import { withSession } from './withSession'
|
||||
|
||||
export async function updateGlobalVersion<T extends TypeWithID>(
|
||||
this: MongooseAdapter,
|
||||
{ global, locale, req = {} as PayloadRequest, versionData, where }: UpdateGlobalVersionArgs<T>,
|
||||
{
|
||||
id,
|
||||
global,
|
||||
locale,
|
||||
req = {} as PayloadRequest,
|
||||
versionData,
|
||||
where,
|
||||
}: UpdateGlobalVersionArgs<T>,
|
||||
) {
|
||||
const VersionModel = this.versions[global]
|
||||
const whereToUse = where || { id: { equals: id } }
|
||||
const options = {
|
||||
...withSession(this, req.transactionID),
|
||||
lean: true,
|
||||
@@ -19,7 +27,7 @@ export async function updateGlobalVersion<T extends TypeWithID>(
|
||||
const query = await VersionModel.buildQuery({
|
||||
locale,
|
||||
payload: this.payload,
|
||||
where,
|
||||
where: whereToUse,
|
||||
})
|
||||
|
||||
const doc = await VersionModel.findOneAndUpdate(query, versionData, options)
|
||||
|
||||
@@ -7,9 +7,10 @@ import { withSession } from './withSession'
|
||||
|
||||
export const updateVersion: UpdateVersion = async function updateVersion(
|
||||
this: MongooseAdapter,
|
||||
{ collection, locale, req = {} as PayloadRequest, versionData, where },
|
||||
{ id, collection, locale, req = {} as PayloadRequest, versionData, where },
|
||||
) {
|
||||
const VersionModel = this.versions[collection]
|
||||
const whereToUse = where || { id: { equals: id } }
|
||||
const options = {
|
||||
...withSession(this, req.transactionID),
|
||||
lean: true,
|
||||
@@ -19,7 +20,7 @@ export const updateVersion: UpdateVersion = async function updateVersion(
|
||||
const query = await VersionModel.buildQuery({
|
||||
locale,
|
||||
payload: this.payload,
|
||||
where,
|
||||
where: whereToUse,
|
||||
})
|
||||
|
||||
const doc = await VersionModel.findOneAndUpdate(query, versionData, options)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-postgres",
|
||||
"version": "0.1.11",
|
||||
"version": "0.1.12",
|
||||
"description": "The officially supported Postgres database adapter for Payload",
|
||||
"repository": "https://github.com/payloadcms/payload",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -14,6 +14,7 @@ type Args = {
|
||||
blocks: {
|
||||
[blockType: string]: BlockRowToInsert[]
|
||||
}
|
||||
blocksToDelete: Set<string>
|
||||
data: unknown
|
||||
field: ArrayField
|
||||
locale?: string
|
||||
@@ -31,6 +32,7 @@ export const transformArray = ({
|
||||
arrayTableName,
|
||||
baseTableName,
|
||||
blocks,
|
||||
blocksToDelete,
|
||||
data,
|
||||
field,
|
||||
locale,
|
||||
@@ -78,6 +80,7 @@ export const transformArray = ({
|
||||
arrays: newRow.arrays,
|
||||
baseTableName,
|
||||
blocks,
|
||||
blocksToDelete,
|
||||
columnPrefix: '',
|
||||
data: arrayRow,
|
||||
fieldPrefix: '',
|
||||
|
||||
@@ -14,6 +14,7 @@ type Args = {
|
||||
blocks: {
|
||||
[blockType: string]: BlockRowToInsert[]
|
||||
}
|
||||
blocksToDelete: Set<string>
|
||||
data: Record<string, unknown>[]
|
||||
field: BlockField
|
||||
locale?: string
|
||||
@@ -29,6 +30,7 @@ export const transformBlocks = ({
|
||||
adapter,
|
||||
baseTableName,
|
||||
blocks,
|
||||
blocksToDelete,
|
||||
data,
|
||||
field,
|
||||
locale,
|
||||
@@ -76,6 +78,7 @@ export const transformBlocks = ({
|
||||
arrays: newRow.arrays,
|
||||
baseTableName,
|
||||
blocks,
|
||||
blocksToDelete,
|
||||
columnPrefix: '',
|
||||
data: blockRow,
|
||||
fieldPrefix: '',
|
||||
|
||||
@@ -25,6 +25,7 @@ export const transformForWrite = ({
|
||||
const rowToInsert: RowToInsert = {
|
||||
arrays: {},
|
||||
blocks: {},
|
||||
blocksToDelete: new Set(),
|
||||
locales: {},
|
||||
numbers: [],
|
||||
relationships: [],
|
||||
@@ -40,6 +41,7 @@ export const transformForWrite = ({
|
||||
arrays: rowToInsert.arrays,
|
||||
baseTableName: tableName,
|
||||
blocks: rowToInsert.blocks,
|
||||
blocksToDelete: rowToInsert.blocksToDelete,
|
||||
columnPrefix: '',
|
||||
data,
|
||||
fieldPrefix: '',
|
||||
|
||||
@@ -26,6 +26,7 @@ type Args = {
|
||||
blocks: {
|
||||
[blockType: string]: BlockRowToInsert[]
|
||||
}
|
||||
blocksToDelete: Set<string>
|
||||
/**
|
||||
* A snake-case field prefix, representing prior fields
|
||||
* Ex: my_group_my_named_tab_
|
||||
@@ -62,6 +63,7 @@ export const traverseFields = ({
|
||||
arrays,
|
||||
baseTableName,
|
||||
blocks,
|
||||
blocksToDelete,
|
||||
columnPrefix,
|
||||
data,
|
||||
existingLocales,
|
||||
@@ -102,6 +104,7 @@ export const traverseFields = ({
|
||||
arrayTableName,
|
||||
baseTableName,
|
||||
blocks,
|
||||
blocksToDelete,
|
||||
data: localeData,
|
||||
field,
|
||||
locale: localeKey,
|
||||
@@ -122,6 +125,7 @@ export const traverseFields = ({
|
||||
arrayTableName,
|
||||
baseTableName,
|
||||
blocks,
|
||||
blocksToDelete,
|
||||
data: data[field.name],
|
||||
field,
|
||||
numbers,
|
||||
@@ -138,6 +142,10 @@ export const traverseFields = ({
|
||||
}
|
||||
|
||||
if (field.type === 'blocks') {
|
||||
field.blocks.forEach(({ slug }) => {
|
||||
blocksToDelete.add(toSnakeCase(slug))
|
||||
})
|
||||
|
||||
if (field.localized) {
|
||||
if (typeof data[field.name] === 'object' && data[field.name] !== null) {
|
||||
Object.entries(data[field.name]).forEach(([localeKey, localeData]) => {
|
||||
@@ -146,6 +154,7 @@ export const traverseFields = ({
|
||||
adapter,
|
||||
baseTableName,
|
||||
blocks,
|
||||
blocksToDelete,
|
||||
data: localeData,
|
||||
field,
|
||||
locale: localeKey,
|
||||
@@ -163,6 +172,7 @@ export const traverseFields = ({
|
||||
adapter,
|
||||
baseTableName,
|
||||
blocks,
|
||||
blocksToDelete,
|
||||
data: fieldData,
|
||||
field,
|
||||
numbers,
|
||||
@@ -185,6 +195,7 @@ export const traverseFields = ({
|
||||
arrays,
|
||||
baseTableName,
|
||||
blocks,
|
||||
blocksToDelete,
|
||||
columnPrefix: `${columnName}_`,
|
||||
data: localeData as Record<string, unknown>,
|
||||
existingLocales,
|
||||
@@ -207,6 +218,7 @@ export const traverseFields = ({
|
||||
arrays,
|
||||
baseTableName,
|
||||
blocks,
|
||||
blocksToDelete,
|
||||
columnPrefix: `${columnName}_`,
|
||||
data: data[field.name] as Record<string, unknown>,
|
||||
existingLocales,
|
||||
@@ -238,6 +250,7 @@ export const traverseFields = ({
|
||||
arrays,
|
||||
baseTableName,
|
||||
blocks,
|
||||
blocksToDelete,
|
||||
columnPrefix: `${columnPrefix || ''}${toSnakeCase(tab.name)}_`,
|
||||
data: localeData as Record<string, unknown>,
|
||||
existingLocales,
|
||||
@@ -260,6 +273,7 @@ export const traverseFields = ({
|
||||
arrays,
|
||||
baseTableName,
|
||||
blocks,
|
||||
blocksToDelete,
|
||||
columnPrefix: `${columnPrefix || ''}${toSnakeCase(tab.name)}_`,
|
||||
data: data[tab.name] as Record<string, unknown>,
|
||||
existingLocales,
|
||||
@@ -282,6 +296,7 @@ export const traverseFields = ({
|
||||
arrays,
|
||||
baseTableName,
|
||||
blocks,
|
||||
blocksToDelete,
|
||||
columnPrefix,
|
||||
data,
|
||||
existingLocales,
|
||||
@@ -306,6 +321,7 @@ export const traverseFields = ({
|
||||
arrays,
|
||||
baseTableName,
|
||||
blocks,
|
||||
blocksToDelete,
|
||||
columnPrefix,
|
||||
data,
|
||||
existingLocales,
|
||||
|
||||
@@ -30,6 +30,7 @@ export type RowToInsert = {
|
||||
blocks: {
|
||||
[blockType: string]: BlockRowToInsert[]
|
||||
}
|
||||
blocksToDelete: Set<string>
|
||||
locales: {
|
||||
[locale: string]: Record<string, unknown>
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
import type { TypeWithID } from 'payload/types'
|
||||
|
||||
import { eq } from 'drizzle-orm'
|
||||
import { ValidationError } from 'payload/errors'
|
||||
import { i18nInit } from 'payload/utilities'
|
||||
|
||||
import type { BlockRowToInsert } from '../transform/write/types'
|
||||
import type { Args } from './types'
|
||||
@@ -12,8 +14,6 @@ import { transformForWrite } from '../transform/write'
|
||||
import { deleteExistingArrayRows } from './deleteExistingArrayRows'
|
||||
import { deleteExistingRowsByPath } from './deleteExistingRowsByPath'
|
||||
import { insertArrays } from './insertArrays'
|
||||
import { ValidationError } from 'payload/errors'
|
||||
import { i18nInit } from 'payload/utilities'
|
||||
|
||||
export const upsertRow = async <T extends TypeWithID>({
|
||||
id,
|
||||
@@ -188,18 +188,15 @@ export const upsertRow = async <T extends TypeWithID>({
|
||||
|
||||
const insertedBlockRows: Record<string, Record<string, unknown>[]> = {}
|
||||
|
||||
for (const [blockName, blockRows] of Object.entries(blocksToInsert)) {
|
||||
if (operation === 'update') {
|
||||
await deleteExistingRowsByPath({
|
||||
adapter,
|
||||
db,
|
||||
parentID: insertedRow.id,
|
||||
pathColumnName: '_path',
|
||||
rows: blockRows.map(({ row }) => row),
|
||||
tableName: `${tableName}_blocks_${blockName}`,
|
||||
})
|
||||
if (operation === 'update') {
|
||||
for (const blockName of rowToInsert.blocksToDelete) {
|
||||
const blockTableName = `${tableName}_blocks_${blockName}`
|
||||
const blockTable = adapter.tables[blockTableName]
|
||||
await db.delete(blockTable).where(eq(blockTable._parentID, insertedRow.id))
|
||||
}
|
||||
}
|
||||
|
||||
for (const [blockName, blockRows] of Object.entries(blocksToInsert)) {
|
||||
insertedBlockRows[blockName] = await db
|
||||
.insert(adapter.tables[`${tableName}_blocks_${blockName}`])
|
||||
.values(blockRows.map(({ row }) => row))
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "payload",
|
||||
"version": "2.0.14",
|
||||
"version": "2.0.15",
|
||||
"description": "Node, React and MongoDB Headless CMS and Application Framework",
|
||||
"license": "MIT",
|
||||
"main": "./dist/index.js",
|
||||
|
||||
@@ -22,6 +22,7 @@ const DateTime: React.FC<Props> = (props) => {
|
||||
minTime,
|
||||
monthsToShow = 1,
|
||||
onChange: onChangeFromProps,
|
||||
overrides,
|
||||
pickerAppearance = 'default',
|
||||
placeholder: placeholderText,
|
||||
readOnly,
|
||||
@@ -77,6 +78,7 @@ const DateTime: React.FC<Props> = (props) => {
|
||||
showTimeSelect: pickerAppearance === 'dayAndTime' || pickerAppearance === 'timeOnly',
|
||||
timeFormat,
|
||||
timeIntervals,
|
||||
...overrides,
|
||||
}
|
||||
|
||||
const classes = [baseClass, `${baseClass}__appearance--${pickerAppearance}`]
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import type { ReactDatePickerProps } from 'react-datepicker'
|
||||
|
||||
type SharedProps = {
|
||||
displayFormat?: string
|
||||
overrides?: ReactDatePickerProps
|
||||
pickerAppearance?: 'dayAndTime' | 'dayOnly' | 'default' | 'monthOnly' | 'timeOnly'
|
||||
}
|
||||
|
||||
|
||||
@@ -62,7 +62,9 @@ const Content: React.FC<DocumentDrawerProps> = ({
|
||||
? Edit
|
||||
: typeof Edit === 'object' && typeof Edit.Default === 'function'
|
||||
? Edit.Default
|
||||
: typeof Edit?.Default === 'object' && typeof Edit.Default.Component === 'function'
|
||||
: typeof Edit?.Default === 'object' &&
|
||||
'Component' in Edit.Default &&
|
||||
typeof Edit.Default.Component === 'function'
|
||||
? Edit.Default.Component
|
||||
: undefined
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { EditViewConfig } from '../../../../../exports/config'
|
||||
import type { SanitizedCollectionConfig, SanitizedGlobalConfig } from '../../../../../exports/types'
|
||||
import type { SanitizedCollectionConfig } from '../../../../../collections/config/types'
|
||||
import type { EditViewConfig } from '../../../../../config/types'
|
||||
import type { SanitizedGlobalConfig } from '../../../../../globals/config/types'
|
||||
|
||||
import { defaultGlobalViews } from '../../../views/Global/Routes/CustomComponent'
|
||||
import { defaultCollectionViews } from '../../../views/collections/Edit/Routes/CustomComponent'
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import type { SanitizedCollectionConfig } from '../../../../../collections/config/types'
|
||||
import type { EditViewConfig } from '../../../../../config/types'
|
||||
import type { SanitizedGlobalConfig } from '../../../../../globals/config/types'
|
||||
|
||||
export const getViewConfig = (args: {
|
||||
collection: SanitizedCollectionConfig
|
||||
global: SanitizedGlobalConfig
|
||||
name: string
|
||||
}): EditViewConfig => {
|
||||
const { name, collection, global } = args
|
||||
|
||||
if (collection) {
|
||||
const collectionViewsConfig =
|
||||
typeof collection?.admin?.components?.views?.Edit === 'object' &&
|
||||
typeof collection?.admin?.components?.views?.Edit !== 'function'
|
||||
? collection?.admin?.components?.views?.Edit
|
||||
: undefined
|
||||
|
||||
return collectionViewsConfig?.[name]
|
||||
}
|
||||
|
||||
if (global) {
|
||||
const globalViewsConfig =
|
||||
typeof global?.admin?.components?.views?.Edit === 'object' &&
|
||||
typeof global?.admin?.components?.views?.Edit !== 'function'
|
||||
? global?.admin?.components?.views?.Edit
|
||||
: undefined
|
||||
|
||||
return globalViewsConfig?.[name]
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
@@ -4,8 +4,9 @@ import type { DocumentTabProps } from './types'
|
||||
|
||||
import { DocumentTab } from './Tab'
|
||||
import { getCustomViews } from './getCustomViews'
|
||||
import { getViewConfig } from './getViewConfig'
|
||||
import './index.scss'
|
||||
import { tabs } from './tabs'
|
||||
import { tabs as defaultViews } from './tabs'
|
||||
|
||||
const baseClass = 'doc-tabs'
|
||||
|
||||
@@ -19,17 +20,50 @@ export const DocumentTabs: React.FC<DocumentTabProps> = (props) => {
|
||||
<div className={baseClass}>
|
||||
<div className={`${baseClass}__tabs-container`}>
|
||||
<ul className={`${baseClass}__tabs`}>
|
||||
{tabs?.map((Tab, index) => {
|
||||
return <DocumentTab {...props} {...Tab} key={`tab-${index}`} />
|
||||
})}
|
||||
{Object.entries(defaultViews)
|
||||
// sort `defaultViews` based on `order` property from smallest to largest
|
||||
// if no `order`, append the view to the end
|
||||
// TODO: open `order` to the config and merge `defaultViews` with `customViews`
|
||||
?.sort(([, a], [, b]) => {
|
||||
if (a.order === undefined && b.order === undefined) return 0
|
||||
else if (a.order === undefined) return 1
|
||||
else if (b.order === undefined) return -1
|
||||
return a.order - b.order
|
||||
})
|
||||
?.map(([name, Tab], index) => {
|
||||
const viewConfig = getViewConfig({ name, collection, global })
|
||||
const tabOverrides = viewConfig && 'Tab' in viewConfig ? viewConfig.Tab : undefined
|
||||
|
||||
return (
|
||||
<DocumentTab
|
||||
{...{
|
||||
...props,
|
||||
...(Tab || {}),
|
||||
...(tabOverrides || {}),
|
||||
}}
|
||||
key={`tab-${index}`}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
{customViews?.map((CustomView, index) => {
|
||||
const { Tab, path } = CustomView
|
||||
if ('Tab' in CustomView) {
|
||||
const { Tab, path } = CustomView
|
||||
|
||||
if (typeof Tab === 'function') {
|
||||
return <Tab path={path} {...props} key={`tab-custom-${index}`} />
|
||||
if (typeof Tab === 'function') {
|
||||
return <Tab path={path} {...props} key={`tab-custom-${index}`} />
|
||||
}
|
||||
|
||||
return (
|
||||
<DocumentTab
|
||||
{...{
|
||||
...props,
|
||||
...Tab,
|
||||
}}
|
||||
key={`tab-custom-${index}`}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return <DocumentTab {...props} {...Tab} key={`tab-custom-${index}`} />
|
||||
return null
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -1,15 +1,27 @@
|
||||
import type { collectionViewType } from '../../../views/collections/Edit/Routes/CustomComponent'
|
||||
import type { DocumentTabConfig } from './types'
|
||||
|
||||
export const tabs: DocumentTabConfig[] = [
|
||||
// Default
|
||||
{
|
||||
export const tabs: Record<
|
||||
collectionViewType,
|
||||
DocumentTabConfig & {
|
||||
order?: number // TODO: expose this to the global config
|
||||
}
|
||||
> = {
|
||||
API: {
|
||||
condition: ({ collection, global }) =>
|
||||
(collection && !collection?.admin?.hideAPIURL) || (global && !global?.admin?.hideAPIURL),
|
||||
href: '/api',
|
||||
label: 'API',
|
||||
order: 1000,
|
||||
},
|
||||
Default: {
|
||||
href: '',
|
||||
isActive: ({ href, location }) =>
|
||||
location.pathname === href || location.pathname === `${href}/create`,
|
||||
label: ({ t }) => t('edit'),
|
||||
order: 0,
|
||||
},
|
||||
// Live Preview
|
||||
{
|
||||
LivePreview: {
|
||||
condition: ({ collection, config, global }) => {
|
||||
if (collection) {
|
||||
return Boolean(
|
||||
@@ -29,22 +41,25 @@ export const tabs: DocumentTabConfig[] = [
|
||||
href: ({ match }) => `${match.url}/preview`,
|
||||
isActive: ({ href, location }) => location.pathname === href,
|
||||
label: ({ t }) => t('livePreview'),
|
||||
order: 100,
|
||||
},
|
||||
// Versions
|
||||
{
|
||||
References: {
|
||||
condition: () => false,
|
||||
},
|
||||
Relationships: {
|
||||
condition: () => false,
|
||||
},
|
||||
Version: {
|
||||
condition: () => false,
|
||||
},
|
||||
Versions: {
|
||||
condition: ({ collection, global }) => Boolean(collection?.versions || global?.versions),
|
||||
href: '/versions',
|
||||
label: ({ t }) => t('version:versions'),
|
||||
order: 200,
|
||||
pillLabel: ({ versions }) =>
|
||||
typeof versions?.totalDocs === 'number' && versions?.totalDocs > 0
|
||||
? versions?.totalDocs.toString()
|
||||
: '',
|
||||
},
|
||||
// API
|
||||
{
|
||||
condition: ({ collection, global }) =>
|
||||
(collection && !collection?.admin?.hideAPIURL) || (global && !global?.admin?.hideAPIURL),
|
||||
href: '/api',
|
||||
label: 'API',
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
@@ -21,9 +21,11 @@ export type DocumentTabCondition = (args: {
|
||||
global: SanitizedGlobalConfig
|
||||
}) => boolean
|
||||
|
||||
// Everything is optional because we merge in the defaults
|
||||
// i.e. the config may override the `Default` view with a `label` but not an `href`
|
||||
export type DocumentTabConfig = {
|
||||
condition?: DocumentTabCondition
|
||||
href:
|
||||
href?:
|
||||
| ((args: {
|
||||
apiURL: string
|
||||
collection: SanitizedCollectionConfig
|
||||
@@ -40,7 +42,7 @@ export type DocumentTabConfig = {
|
||||
match: ReturnType<typeof useRouteMatch>
|
||||
}) => boolean)
|
||||
| boolean
|
||||
label: ((args: { t: (key: string) => string }) => string) | string
|
||||
label?: ((args: { t: (key: string) => string }) => string) | string
|
||||
newTab?: boolean
|
||||
pillLabel?:
|
||||
| ((args: { versions: ReturnType<typeof useDocumentInfo>['versions'] }) => string)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
@import '../../../scss/styles.scss';
|
||||
|
||||
.file-details {
|
||||
background-color: var(--theme-elevation-100);
|
||||
background-color: var(--theme-elevation-50);
|
||||
|
||||
header {
|
||||
display: flex;
|
||||
|
||||
@@ -28,14 +28,11 @@
|
||||
margin-left: base(0.5);
|
||||
}
|
||||
|
||||
.btn {
|
||||
background-color: var(--theme-elevation-100);
|
||||
cursor: pointer;
|
||||
padding: 0 base(0.25);
|
||||
border-radius: $style-radius-s;
|
||||
.pill {
|
||||
background-color: var(--theme-elevation-200);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--theme-elevation-200);
|
||||
background-color: var(--theme-elevation-150);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,17 +116,16 @@ export const ListControls: React.FC<Props> = (props) => {
|
||||
{t('filters')}
|
||||
</Pill>
|
||||
{enableSort && (
|
||||
<Button
|
||||
<Pill
|
||||
aria-controls={`${baseClass}-sort`}
|
||||
aria-expanded={visibleDrawer === 'sort'}
|
||||
buttonStyle={visibleDrawer === 'sort' ? undefined : 'secondary'}
|
||||
className={`${baseClass}__toggle-sort`}
|
||||
icon="chevron"
|
||||
iconStyle="none"
|
||||
icon={<Chevron />}
|
||||
onClick={() => setVisibleDrawer(visibleDrawer !== 'sort' ? 'sort' : undefined)}
|
||||
pillStyle="light"
|
||||
>
|
||||
{t('sort')}
|
||||
</Button>
|
||||
</Pill>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -28,7 +28,7 @@ export const NavToggler: React.FC<{
|
||||
|
||||
return (
|
||||
<button
|
||||
aria-label={t('menu')}
|
||||
aria-label={`${navOpen ? t('close') : t('open')} ${t('menu')}`}
|
||||
className={[baseClass, navOpen && `${baseClass}--is-open`, className]
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
|
||||
@@ -54,8 +54,9 @@ const SortColumn: React.FC<Props> = (props) => {
|
||||
direction: t('ascending'),
|
||||
label: getTranslation(label, i18n),
|
||||
})}
|
||||
className={[ascClasses, `${baseClass}__button`].filter(Boolean).join(' ')}
|
||||
className={[...ascClasses, `${baseClass}__button`].filter(Boolean).join(' ')}
|
||||
onClick={() => setSort(asc)}
|
||||
type="button"
|
||||
>
|
||||
<Chevron direction="up" />
|
||||
</button>
|
||||
@@ -64,8 +65,9 @@ const SortColumn: React.FC<Props> = (props) => {
|
||||
direction: t('descending'),
|
||||
label: getTranslation(label, i18n),
|
||||
})}
|
||||
className={[descClasses, `${baseClass}__button`].filter(Boolean).join(' ')}
|
||||
className={[...descClasses, `${baseClass}__button`].filter(Boolean).join(' ')}
|
||||
onClick={() => setSort(desc)}
|
||||
type="button"
|
||||
>
|
||||
<Chevron />
|
||||
</button>
|
||||
|
||||
@@ -56,6 +56,8 @@
|
||||
}
|
||||
|
||||
span {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
max-width: base(8);
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
|
||||
@@ -51,16 +51,25 @@ const StepNav: React.FC<{
|
||||
</Link>
|
||||
<span>/</span>
|
||||
{stepNav.map((item, i) => {
|
||||
const StepLabel = <span key={i}>{getTranslation(item.label, i18n)}</span>
|
||||
const Step =
|
||||
stepNav.length === i + 1 ? (
|
||||
StepLabel
|
||||
) : (
|
||||
<Fragment key={i}>
|
||||
{item.url ? <Link to={item.url}>{StepLabel}</Link> : StepLabel}
|
||||
<span>/</span>
|
||||
</Fragment>
|
||||
)
|
||||
const StepLabel = getTranslation(item.label, i18n)
|
||||
const isLast = stepNav.length === i + 1
|
||||
|
||||
const Step = isLast ? (
|
||||
<span className={`${baseClass}__last`} key={i}>
|
||||
{StepLabel}
|
||||
</span>
|
||||
) : (
|
||||
<Fragment key={i}>
|
||||
{item.url ? (
|
||||
<Link to={item.url}>
|
||||
<span key={i}>{StepLabel}</span>
|
||||
</Link>
|
||||
) : (
|
||||
<span key={i}>{StepLabel}</span>
|
||||
)}
|
||||
<span>/</span>
|
||||
</Fragment>
|
||||
)
|
||||
|
||||
return Step
|
||||
})}
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
content: ' ';
|
||||
display: block;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translate3d(-50%, 100%, 0);
|
||||
width: 0;
|
||||
height: 0;
|
||||
@@ -35,6 +34,24 @@
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
&--caret-center {
|
||||
&::after {
|
||||
left: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
&--caret-left {
|
||||
&::after {
|
||||
left: calc(var(--base) * 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
&--caret-right {
|
||||
&::after {
|
||||
right: calc(var(--base) * 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
&--position-top {
|
||||
bottom: 100%;
|
||||
transform: translate3d(-50%, calc(var(--caret-size) * -1), 0);
|
||||
@@ -55,6 +72,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
.tooltip-content {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@include mid-break {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -6,11 +6,20 @@ import useIntersect from '../../../hooks/useIntersect'
|
||||
import './index.scss'
|
||||
|
||||
const Tooltip: React.FC<Props> = (props) => {
|
||||
const { boundingRef, children, className, delay = 350, show: showFromProps = true } = props
|
||||
const {
|
||||
alignCaret = 'center',
|
||||
boundingRef,
|
||||
children,
|
||||
className,
|
||||
delay = 350,
|
||||
show: showFromProps = true,
|
||||
} = props
|
||||
|
||||
const [show, setShow] = React.useState(showFromProps)
|
||||
const [position, setPosition] = React.useState<'bottom' | 'top'>('top')
|
||||
|
||||
const getTitleAttribute = (content) => (typeof content === 'string' ? content : '')
|
||||
|
||||
const [ref, intersectionEntry] = useIntersect({
|
||||
root: boundingRef?.current || null,
|
||||
rootMargin: '-145px 0px 0px 100px',
|
||||
@@ -42,18 +51,28 @@ const Tooltip: React.FC<Props> = (props) => {
|
||||
<React.Fragment>
|
||||
<aside
|
||||
aria-hidden="true"
|
||||
className={['tooltip', className, 'tooltip--position-top'].filter(Boolean).join(' ')}
|
||||
className={['tooltip', className, `tooltip--caret-${alignCaret}`, 'tooltip--position-top']
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
ref={ref}
|
||||
title={getTitleAttribute(children)}
|
||||
>
|
||||
{children}
|
||||
<div className="tooltip-content">{children}</div>
|
||||
</aside>
|
||||
|
||||
<aside
|
||||
className={['tooltip', className, show && 'tooltip--show', `tooltip--position-${position}`]
|
||||
className={[
|
||||
'tooltip',
|
||||
className,
|
||||
show && 'tooltip--show',
|
||||
`tooltip--caret-${alignCaret}`,
|
||||
`tooltip--position-${position}`,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
title={getTitleAttribute(children)}
|
||||
>
|
||||
{children}
|
||||
<div className="tooltip-content">{children}</div>
|
||||
</aside>
|
||||
</React.Fragment>
|
||||
)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export type Props = {
|
||||
alignCaret?: 'center' | 'left' | 'right'
|
||||
boundingRef?: React.RefObject<HTMLElement>
|
||||
children: React.ReactNode
|
||||
className?: string
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
top: 0;
|
||||
bottom: auto;
|
||||
left: auto;
|
||||
right: base(0.5);
|
||||
max-width: 75%;
|
||||
right: calc(var(--base) * 0.5);
|
||||
transform: none;
|
||||
background-color: var(--theme-error-500);
|
||||
|
||||
|
||||
@@ -8,11 +8,11 @@ import './index.scss'
|
||||
const baseClass = 'field-error'
|
||||
|
||||
const Error: React.FC<Props> = (props) => {
|
||||
const { message, showError = false } = props
|
||||
const { alignCaret = 'right', message, showError = false } = props
|
||||
|
||||
if (showError) {
|
||||
return (
|
||||
<Tooltip className={baseClass} delay={0}>
|
||||
<Tooltip alignCaret={alignCaret} className={baseClass} delay={0}>
|
||||
{message}
|
||||
</Tooltip>
|
||||
)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export type Props = {
|
||||
alignCaret?: 'center' | 'left' | 'right'
|
||||
message: string
|
||||
showError?: boolean
|
||||
}
|
||||
|
||||
@@ -48,23 +48,24 @@ export const filterFields = (args: {
|
||||
}
|
||||
|
||||
const isFieldAffectingData = fieldAffectsData(field)
|
||||
|
||||
const fieldPermissions = isFieldAffectingData ? permissions?.[field.name] : permissions
|
||||
|
||||
// if the user cannot read the field, then filter it out
|
||||
if (fieldPermissions?.read?.permission === false) {
|
||||
return acc
|
||||
}
|
||||
|
||||
// readOnly from field config
|
||||
let readOnly = field.admin && 'readOnly' in field.admin ? field.admin.readOnly : undefined
|
||||
|
||||
// if parent field is readOnly
|
||||
// but this field is `readOnly: false`
|
||||
// the field should be editable
|
||||
if (readOnlyOverride && readOnly !== false) readOnly = true
|
||||
|
||||
if (
|
||||
(isFieldAffectingData && permissions?.[field?.name]?.read?.permission !== false) ||
|
||||
!isFieldAffectingData
|
||||
) {
|
||||
if (
|
||||
isFieldAffectingData &&
|
||||
permissions?.[field?.name]?.[operation]?.permission === false
|
||||
) {
|
||||
readOnly = true
|
||||
}
|
||||
// unless the user does not pass access control
|
||||
if (fieldPermissions?.[operation]?.permission === false) {
|
||||
readOnly = true
|
||||
}
|
||||
|
||||
if (FieldComponent) {
|
||||
|
||||
@@ -10,6 +10,7 @@ const baseClass = 'checkbox-input'
|
||||
type CheckboxInputProps = {
|
||||
'aria-label'?: string
|
||||
checked?: boolean
|
||||
className?: string
|
||||
id?: string
|
||||
inputRef?: React.MutableRefObject<HTMLInputElement>
|
||||
label?: string
|
||||
@@ -18,7 +19,6 @@ type CheckboxInputProps = {
|
||||
partialChecked?: boolean
|
||||
readOnly?: boolean
|
||||
required?: boolean
|
||||
className?: string
|
||||
}
|
||||
|
||||
export const CheckboxInput: React.FC<CheckboxInputProps> = (props) => {
|
||||
@@ -27,13 +27,13 @@ export const CheckboxInput: React.FC<CheckboxInputProps> = (props) => {
|
||||
name,
|
||||
'aria-label': ariaLabel,
|
||||
checked,
|
||||
className,
|
||||
inputRef,
|
||||
label,
|
||||
onToggle,
|
||||
partialChecked,
|
||||
readOnly,
|
||||
required,
|
||||
className,
|
||||
} = props
|
||||
|
||||
return (
|
||||
|
||||
@@ -9,9 +9,9 @@ import Error from '../../Error'
|
||||
import FieldDescription from '../../FieldDescription'
|
||||
import useField from '../../useField'
|
||||
import withCondition from '../../withCondition'
|
||||
import { fieldBaseClass } from '../shared'
|
||||
import { CheckboxInput } from './Input'
|
||||
import './index.scss'
|
||||
import { fieldBaseClass } from '../shared'
|
||||
|
||||
const baseClass = 'checkbox'
|
||||
|
||||
@@ -72,7 +72,7 @@ const Checkbox: React.FC<Props> = (props) => {
|
||||
}}
|
||||
>
|
||||
<div className={`${baseClass}__error-wrap`}>
|
||||
<Error message={errorMessage} showError={showError} />
|
||||
<Error alignCaret="left" message={errorMessage} showError={showError} />
|
||||
</div>
|
||||
<CheckboxInput
|
||||
checked={Boolean(value)}
|
||||
@@ -81,6 +81,7 @@ const Checkbox: React.FC<Props> = (props) => {
|
||||
name={path}
|
||||
onToggle={onToggle}
|
||||
readOnly={readOnly}
|
||||
required={required}
|
||||
/>
|
||||
<FieldDescription description={description} value={value} />
|
||||
</div>
|
||||
|
||||
@@ -10,8 +10,8 @@ import { getTranslation } from '../../../../../utilities/getTranslation'
|
||||
import Error from '../../Error'
|
||||
import FieldDescription from '../../FieldDescription'
|
||||
import Label from '../../Label'
|
||||
import './index.scss'
|
||||
import { fieldBaseClass } from '../shared'
|
||||
import './index.scss'
|
||||
|
||||
export type TextInputProps = Omit<TextField, 'type'> & {
|
||||
className?: string
|
||||
|
||||
@@ -42,12 +42,15 @@ const DefaultAccount: React.FC<DefaultAccountViewProps> = (props) => {
|
||||
const { refreshCookieAsync } = useAuth()
|
||||
const { t } = useTranslation('authentication')
|
||||
|
||||
const onSave = useCallback(async () => {
|
||||
await refreshCookieAsync()
|
||||
if (typeof onSaveFromProps === 'function') {
|
||||
onSaveFromProps({})
|
||||
}
|
||||
}, [onSaveFromProps, refreshCookieAsync])
|
||||
const onSave = useCallback(
|
||||
async (json) => {
|
||||
await refreshCookieAsync()
|
||||
if (typeof onSaveFromProps === 'function') {
|
||||
onSaveFromProps(json)
|
||||
}
|
||||
},
|
||||
[onSaveFromProps, refreshCookieAsync],
|
||||
)
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
|
||||
@@ -60,7 +60,7 @@ const AccountView: React.FC = () => {
|
||||
|
||||
const onSave = React.useCallback(
|
||||
async (json: any) => {
|
||||
getDocPermissions()
|
||||
await getDocPermissions()
|
||||
|
||||
const preferences = await getDocPreferences()
|
||||
|
||||
|
||||
@@ -54,9 +54,9 @@ export const globalCustomRoutes = (props: {
|
||||
if (typeof ViewComponent === 'function') {
|
||||
routesToReturn.push(<ViewComponent key={ViewComponent.name} user={user} />)
|
||||
} else {
|
||||
const { Component, path } = ViewComponent
|
||||
if (global && 'Component' in ViewComponent) {
|
||||
const { Component, path } = ViewComponent
|
||||
|
||||
if (global) {
|
||||
routesToReturn.push(
|
||||
<Route exact key={`${global.slug}-${path}`} path={`${match.url}${path}`}>
|
||||
{permissions?.read?.permission ? <Component user={user} /> : <Unauthorized />}
|
||||
|
||||
@@ -46,12 +46,15 @@ const VersionsView: React.FC<IndexProps> = (props) => {
|
||||
// 2. "components.Edit.Versions"
|
||||
// 3. "components.Edit.Versions.Component"
|
||||
const Edit = collection?.admin?.components?.views?.Edit
|
||||
|
||||
CustomVersionsView =
|
||||
typeof Edit === 'function'
|
||||
? Edit
|
||||
: typeof Edit === 'object' && typeof Edit.Versions === 'function'
|
||||
? Edit.Versions
|
||||
: typeof Edit?.Versions === 'object' && typeof Edit.Versions.Component === 'function'
|
||||
: typeof Edit?.Versions === 'object' &&
|
||||
'Component' in Edit.Versions &&
|
||||
typeof Edit.Versions.Component === 'function'
|
||||
? Edit.Versions.Component
|
||||
: undefined
|
||||
}
|
||||
@@ -64,12 +67,15 @@ const VersionsView: React.FC<IndexProps> = (props) => {
|
||||
|
||||
// See note above about cascading component definitions
|
||||
const Edit = global?.admin?.components?.views?.Edit
|
||||
|
||||
CustomVersionsView =
|
||||
typeof Edit === 'function'
|
||||
? Edit
|
||||
: typeof Edit === 'object' && typeof Edit.Versions === 'function'
|
||||
? Edit.Versions
|
||||
: typeof Edit?.Versions === 'object' && typeof Edit.Versions.Component === 'function'
|
||||
: typeof Edit?.Versions === 'object' &&
|
||||
'Component' in Edit.Versions &&
|
||||
typeof Edit.Versions.Component === 'function'
|
||||
? Edit.Versions.Component
|
||||
: undefined
|
||||
}
|
||||
|
||||
@@ -54,9 +54,9 @@ export const collectionCustomRoutes = (props: {
|
||||
if (typeof ViewComponent === 'function') {
|
||||
routesToReturn.push(<ViewComponent key={ViewComponent.name} user={user} />)
|
||||
} else {
|
||||
const { Component, path } = ViewComponent
|
||||
if (collection && 'Component' in ViewComponent) {
|
||||
const { Component, path } = ViewComponent
|
||||
|
||||
if (collection) {
|
||||
routesToReturn.push(
|
||||
<Route exact key={`${collection.slug}-${path}`} path={`${match.url}${path}`}>
|
||||
{permissions?.read?.permission ? <Component user={user} /> : <Unauthorized />}
|
||||
|
||||
@@ -15,7 +15,7 @@ import type { Auth, IncomingAuthType, User } from '../../auth/types'
|
||||
import type {
|
||||
Access,
|
||||
AdminViewComponent,
|
||||
EditView,
|
||||
EditViewConfig,
|
||||
Endpoint,
|
||||
EntityDescription,
|
||||
GeneratePreviewURL,
|
||||
@@ -246,28 +246,32 @@ export type CollectionAdminOptions = {
|
||||
* Set to an object to replace or modify individual nested routes, or to add new ones.
|
||||
*/
|
||||
Edit?:
|
||||
| {
|
||||
[key: string]: EditView
|
||||
API?: EditView
|
||||
/**
|
||||
* Replace or modify individual nested routes, or add new ones:
|
||||
* + `Default` - `/admin/collections/:collection/:id`
|
||||
* + `API` - `/admin/collections/:collection/:id/api`
|
||||
* + `LivePreview` - `/admin/collections/:collection/:id/preview`
|
||||
* + `References` - `/admin/collections/:collection/:id/references`
|
||||
* + `Relationships` - `/admin/collections/:collection/:id/relationships`
|
||||
* + `Versions` - `/admin/collections/:collection/:id/versions`
|
||||
* + `Version` - `/admin/collections/:collection/:id/versions/:version`
|
||||
* + `:path` - `/admin/collections/:collection/:id/:path`
|
||||
*/
|
||||
Default?: EditView
|
||||
LivePreview?: EditView
|
||||
Version?: EditView
|
||||
Versions?: EditView
|
||||
// TODO: uncomment these as they are built
|
||||
// References?: EditView
|
||||
// Relationships?: EditView
|
||||
}
|
||||
| (
|
||||
| {
|
||||
/**
|
||||
* Replace or modify individual nested routes, or add new ones:
|
||||
* + `Default` - `/admin/collections/:collection/:id`
|
||||
* + `API` - `/admin/collections/:collection/:id/api`
|
||||
* + `LivePreview` - `/admin/collections/:collection/:id/preview`
|
||||
* + `References` - `/admin/collections/:collection/:id/references`
|
||||
* + `Relationships` - `/admin/collections/:collection/:id/relationships`
|
||||
* + `Versions` - `/admin/collections/:collection/:id/versions`
|
||||
* + `Version` - `/admin/collections/:collection/:id/versions/:version`
|
||||
* + `CustomView` - `/admin/collections/:collection/:id/:path`
|
||||
*/
|
||||
API?: AdminViewComponent | Partial<EditViewConfig>
|
||||
Default?: AdminViewComponent | Partial<EditViewConfig>
|
||||
LivePreview?: AdminViewComponent | Partial<EditViewConfig>
|
||||
Version?: AdminViewComponent | Partial<EditViewConfig>
|
||||
Versions?: AdminViewComponent | Partial<EditViewConfig>
|
||||
// TODO: uncomment these as they are built
|
||||
// References?: EditView
|
||||
// Relationships?: EditView
|
||||
}
|
||||
| {
|
||||
[key: string]: EditViewConfig
|
||||
}
|
||||
)
|
||||
| AdminViewComponent
|
||||
List?: React.ComponentType<ListProps>
|
||||
}
|
||||
|
||||
@@ -86,9 +86,11 @@ const batchAndLoadDocs =
|
||||
return batches
|
||||
}, {})
|
||||
|
||||
// Run find requests in parallel
|
||||
// Run find requests one after another, so as to not hang transactions
|
||||
|
||||
await Object.entries(batchByFindArgs).reduce(async (priorFind, [batchKey, ids]) => {
|
||||
await priorFind
|
||||
|
||||
const results = Object.entries(batchByFindArgs).map(async ([batchKey, ids]) => {
|
||||
const [
|
||||
transactionID,
|
||||
collection,
|
||||
@@ -141,9 +143,7 @@ const batchAndLoadDocs =
|
||||
docs[docsIndex] = doc
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
await Promise.all(results)
|
||||
}, Promise.resolve())
|
||||
|
||||
// Return docs array,
|
||||
// which has now been injected with all fetched docs
|
||||
|
||||
@@ -20,6 +20,7 @@ export type Arguments = {
|
||||
limit?: number
|
||||
overrideAccess?: boolean
|
||||
page?: number
|
||||
pagination?: boolean
|
||||
req?: PayloadRequest
|
||||
showHiddenFields?: boolean
|
||||
sort?: string
|
||||
@@ -35,6 +36,7 @@ async function findVersions<T extends TypeWithVersion<T>>(
|
||||
limit,
|
||||
overrideAccess,
|
||||
page,
|
||||
pagination = true,
|
||||
req: { locale, payload },
|
||||
req,
|
||||
showHiddenFields,
|
||||
@@ -76,6 +78,7 @@ async function findVersions<T extends TypeWithVersion<T>>(
|
||||
limit: limit ?? 10,
|
||||
locale,
|
||||
page: page || 1,
|
||||
pagination,
|
||||
req,
|
||||
sort,
|
||||
where: fullWhere,
|
||||
|
||||
@@ -12,9 +12,9 @@ export const documentTabSchema = {
|
||||
}
|
||||
|
||||
export const customViewSchema = joi.object({
|
||||
Component: componentSchema.required(),
|
||||
Component: componentSchema,
|
||||
Tab: joi.alternatives().try(documentTabSchema, componentSchema),
|
||||
path: joi.string().required(),
|
||||
path: joi.string(),
|
||||
})
|
||||
|
||||
export const livePreviewSchema = {
|
||||
|
||||
@@ -11,7 +11,11 @@ import type { DeepRequired } from 'ts-essentials'
|
||||
import type { InlineConfig } from 'vite'
|
||||
import type { Configuration } from 'webpack'
|
||||
|
||||
import type { DocumentTab } from '../admin/components/elements/DocumentHeader/Tabs/types'
|
||||
import type {
|
||||
DocumentTab,
|
||||
DocumentTabComponent,
|
||||
DocumentTabConfig,
|
||||
} from '../admin/components/elements/DocumentHeader/Tabs/types'
|
||||
import type { RichTextAdapter } from '../admin/components/forms/field-types/RichText/types'
|
||||
import type { ContextType } from '../admin/components/utilities/DocumentInfo/types'
|
||||
import type { User } from '../auth/types'
|
||||
@@ -249,16 +253,29 @@ export type AdminViewComponent = React.ComponentType<AdminViewProps>
|
||||
|
||||
export type AdminView = AdminViewComponent | AdminViewConfig
|
||||
|
||||
export type EditViewConfig = {
|
||||
/**
|
||||
* The component to render for this view
|
||||
* + Replaces the default component
|
||||
*/
|
||||
Component: AdminViewComponent
|
||||
Tab: DocumentTab
|
||||
path: string
|
||||
}
|
||||
export type EditViewConfig =
|
||||
| {
|
||||
/**
|
||||
* Add a new Edit view to the admin panel
|
||||
* i.e. you can render a custom view that has no tab, if desired
|
||||
* Or override a specific properties of an existing one
|
||||
* i.e. you can customize the `Default` view tab label, if desired
|
||||
*/
|
||||
Tab?: DocumentTab
|
||||
path?: string
|
||||
}
|
||||
| {
|
||||
Component: AdminViewComponent
|
||||
path: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Override existing views
|
||||
* i.e. Dashboard, Account, API, LivePreview, etc.
|
||||
* Path is not available here
|
||||
* All Tab properties become optional
|
||||
* i.e. you can change just the label, if desired
|
||||
*/
|
||||
export type EditView = AdminViewComponent | EditViewConfig
|
||||
|
||||
export type Locale = {
|
||||
|
||||
@@ -104,7 +104,7 @@ export async function validateSearchParam({
|
||||
let fieldAccess
|
||||
let fieldPath = path
|
||||
// remove locale from end of path
|
||||
if (path.endsWith(req.locale)) {
|
||||
if (path.endsWith(`.${req.locale}`)) {
|
||||
fieldPath = path.slice(0, -(req.locale.length + 1))
|
||||
}
|
||||
// remove ".value" from ends of polymorphic relationship paths
|
||||
|
||||
@@ -11,7 +11,7 @@ import type { User } from '../../auth/types'
|
||||
import type {
|
||||
Access,
|
||||
AdminViewComponent,
|
||||
EditView,
|
||||
EditViewConfig,
|
||||
Endpoint,
|
||||
EntityDescription,
|
||||
GeneratePreviewURL,
|
||||
@@ -105,28 +105,32 @@ export type GlobalAdminOptions = {
|
||||
* Set to an object to replace or modify individual nested routes, or to add new ones.
|
||||
*/
|
||||
Edit?:
|
||||
| {
|
||||
[name: string]: EditView
|
||||
API?: EditView
|
||||
/**
|
||||
* Replace or modify individual nested routes, or add new ones:
|
||||
* + `Default` - `/admin/globals/:slug`
|
||||
* + `API` - `/admin/globals/:id/api`
|
||||
* + `LivePreview` - `/admin/globals/:id/preview`
|
||||
* + `References` - `/admin/globals/:id/references`
|
||||
* + `Relationships` - `/admin/globals/:id/relationships`
|
||||
* + `Versions` - `/admin/globals/:id/versions`
|
||||
* + `Version` - `/admin/globals/:id/versions/:version`
|
||||
* + `:path` - `/admin/globals/:id/:path`
|
||||
*/
|
||||
Default?: EditView
|
||||
LivePreview?: EditView
|
||||
Version?: EditView
|
||||
Versions?: EditView
|
||||
// TODO: uncomment these as they are built
|
||||
// References?: EditView
|
||||
// Relationships?: EditView
|
||||
}
|
||||
| (
|
||||
| {
|
||||
/**
|
||||
* Replace or modify individual nested routes, or add new ones:
|
||||
* + `Default` - `/admin/globals/:slug`
|
||||
* + `API` - `/admin/globals/:id/api`
|
||||
* + `LivePreview` - `/admin/globals/:id/preview`
|
||||
* + `References` - `/admin/globals/:id/references`
|
||||
* + `Relationships` - `/admin/globals/:id/relationships`
|
||||
* + `Versions` - `/admin/globals/:id/versions`
|
||||
* + `Version` - `/admin/globals/:id/versions/:version`
|
||||
* + `CustomView` - `/admin/globals/:id/:path`
|
||||
*/
|
||||
API?: AdminViewComponent | Partial<EditViewConfig>
|
||||
Default?: AdminViewComponent | Partial<EditViewConfig>
|
||||
LivePreview?: AdminViewComponent | Partial<EditViewConfig>
|
||||
Version?: AdminViewComponent | Partial<EditViewConfig>
|
||||
Versions?: AdminViewComponent | Partial<EditViewConfig>
|
||||
// TODO: uncomment these as they are built
|
||||
// References?: EditView
|
||||
// Relationships?: EditView
|
||||
}
|
||||
| {
|
||||
[name: string]: EditViewConfig
|
||||
}
|
||||
)
|
||||
| AdminViewComponent
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,6 +79,9 @@ async function findVersionByID<T extends TypeWithVersion<T> = any>(args: Argumen
|
||||
// Clone the result - it may have come back memoized
|
||||
let result = JSON.parse(JSON.stringify(results[0]))
|
||||
|
||||
// Patch globalType onto version doc
|
||||
result.version.globalType = globalConfig.slug
|
||||
|
||||
// /////////////////////////////////////
|
||||
// beforeRead - Collection
|
||||
// /////////////////////////////////////
|
||||
|
||||
@@ -92,7 +92,11 @@ async function findVersions<T extends TypeWithVersion<T>>(
|
||||
collection: null,
|
||||
context: req.context,
|
||||
depth,
|
||||
doc: data.version,
|
||||
doc: {
|
||||
...data.version,
|
||||
// Patch globalType onto version doc
|
||||
globalType: globalConfig.slug,
|
||||
},
|
||||
findMany: true,
|
||||
global: globalConfig,
|
||||
overrideAccess,
|
||||
|
||||
@@ -58,6 +58,9 @@ async function restoreVersion<T extends TypeWithVersion<T> = any>(args: Argument
|
||||
|
||||
const rawVersion = versionDocs[0]
|
||||
|
||||
// Patch globalType onto version doc
|
||||
rawVersion.version.globalType = globalConfig.slug
|
||||
|
||||
// /////////////////////////////////////
|
||||
// fetch previousDoc
|
||||
// /////////////////////////////////////
|
||||
|
||||
@@ -193,6 +193,7 @@ async function update<TSlug extends keyof GeneratedTypes['globals']>(
|
||||
// /////////////////////////////////////
|
||||
|
||||
if (globalConfig.versions) {
|
||||
const { globalType } = result
|
||||
result = await saveVersion({
|
||||
autosave,
|
||||
docWithLocales: {
|
||||
@@ -205,6 +206,7 @@ async function update<TSlug extends keyof GeneratedTypes['globals']>(
|
||||
payload,
|
||||
req,
|
||||
})
|
||||
result.globalType = globalType
|
||||
}
|
||||
|
||||
// /////////////////////////////////////
|
||||
|
||||
@@ -204,6 +204,7 @@
|
||||
"loading": "يتمّ التّحميل",
|
||||
"locale": "اللّغة",
|
||||
"locales": "اللّغات",
|
||||
"menu": "قائمة",
|
||||
"moveDown": "التّحريك إلى الأسفل",
|
||||
"moveUp": "التّحريك إلى الأعلى",
|
||||
"newPassword": "كلمة مرور جديدة",
|
||||
@@ -216,8 +217,8 @@
|
||||
"notFound": "غير موجود",
|
||||
"nothingFound": "لم يتم العثور على شيء",
|
||||
"of": "من",
|
||||
"or": "أو",
|
||||
"open": "فتح",
|
||||
"or": "أو",
|
||||
"order": "التّرتيب",
|
||||
"pageNotFound": "الصّفحة غير موجودة",
|
||||
"password": "كلمة المرور",
|
||||
@@ -368,4 +369,4 @@
|
||||
"viewingVersions": "يتمّ استعراض النُّسَخ ل {{entityLabel}} {{documentTitle}}",
|
||||
"viewingVersionsGlobal": "يتمّ استعراض النُّسَخ للاعداد العامّ {{entityLabel}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -205,6 +205,7 @@
|
||||
"loading": "Yüklənir",
|
||||
"locale": "Lokal",
|
||||
"locales": "Dillər",
|
||||
"menu": "Menyu",
|
||||
"moveDown": "Aşağı hərəkət et",
|
||||
"moveUp": "Yuxarı hərəkət et",
|
||||
"newPassword": "Yeni şifrə",
|
||||
@@ -217,8 +218,8 @@
|
||||
"notFound": "Tapılmadı",
|
||||
"nothingFound": "Heç nə tapılmadı",
|
||||
"of": "dən",
|
||||
"or": "Və ya",
|
||||
"open": "Aç",
|
||||
"or": "Və ya",
|
||||
"order": "Sıra",
|
||||
"pageNotFound": "Səhifə tapılmadı",
|
||||
"password": "Şifrə",
|
||||
@@ -369,4 +370,4 @@
|
||||
"viewingVersions": "{{entityLabel}} {{documentTitle}} üçün versiyaları göstərir",
|
||||
"viewingVersionsGlobal": "Qlobal {{entityLabel}} üçün versiyaları göstərir"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -204,6 +204,7 @@
|
||||
"loading": "Зарежда се",
|
||||
"locale": "Локализация",
|
||||
"locales": "Локализации",
|
||||
"menu": "Меню",
|
||||
"moveDown": "Надолу",
|
||||
"moveUp": "Нагоре",
|
||||
"newPassword": "Нова парола",
|
||||
@@ -216,8 +217,8 @@
|
||||
"notFound": "Няма открит",
|
||||
"nothingFound": "Нищо не беше открито",
|
||||
"of": "от",
|
||||
"or": "Или",
|
||||
"open": "Отвори",
|
||||
"or": "Или",
|
||||
"order": "Ред",
|
||||
"pageNotFound": "Страницата не беше открита",
|
||||
"password": "Парола",
|
||||
@@ -368,4 +369,4 @@
|
||||
"viewingVersions": "Гледане на версии за {{entityLabel}} {{documentTitle}}",
|
||||
"viewingVersionsGlobal": "Гледане на версии за глобалния документ {{entityLabel}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -204,6 +204,7 @@
|
||||
"loading": "Načítání",
|
||||
"locale": "Místní verze",
|
||||
"locales": "Lokality",
|
||||
"menu": "Jídelní lístek",
|
||||
"moveDown": "Posunout dolů",
|
||||
"moveUp": "Posunout nahoru",
|
||||
"newPassword": "Nové heslo",
|
||||
@@ -216,8 +217,8 @@
|
||||
"notFound": "Nenalezeno",
|
||||
"nothingFound": "Nic nenalezeno",
|
||||
"of": "z",
|
||||
"or": "Nebo",
|
||||
"open": "Otevřít",
|
||||
"or": "Nebo",
|
||||
"order": "Pořadí",
|
||||
"pageNotFound": "Stránka nenalezena",
|
||||
"password": "Heslo",
|
||||
@@ -368,4 +369,4 @@
|
||||
"viewingVersions": "Zobrazuji verze pro {{entityLabel}} {{documentTitle}}",
|
||||
"viewingVersionsGlobal": "Zobrazuji verze pro globální {{entityLabel}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -204,6 +204,7 @@
|
||||
"loading": "Lädt",
|
||||
"locale": "Sprachumgebung",
|
||||
"locales": "Sprachumgebungen",
|
||||
"menu": "Menü",
|
||||
"moveDown": "Nach unten bewegen",
|
||||
"moveUp": "Nach oben bewegen",
|
||||
"newPassword": "Neues Passwort",
|
||||
@@ -216,8 +217,8 @@
|
||||
"notFound": "Nicht gefunden",
|
||||
"nothingFound": "Keine Ergebnisse",
|
||||
"of": "von",
|
||||
"or": "oder",
|
||||
"open": "Öffnen",
|
||||
"or": "oder",
|
||||
"order": "Reihenfolge",
|
||||
"pageNotFound": "Seite nicht gefunden",
|
||||
"password": "Passwort",
|
||||
@@ -368,4 +369,4 @@
|
||||
"viewingVersions": "Betrachte Versionen für {{entityLabel}} {{documentTitle}}",
|
||||
"viewingVersionsGlobal": "`Betrachte Versionen für das Globale Dokument {{entityLabel}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -204,6 +204,7 @@
|
||||
"loading": "Loading",
|
||||
"locale": "Locale",
|
||||
"locales": "Locales",
|
||||
"menu": "Menu",
|
||||
"moveDown": "Move Down",
|
||||
"moveUp": "Move Up",
|
||||
"newPassword": "New Password",
|
||||
|
||||
@@ -204,6 +204,7 @@
|
||||
"loading": "Cargando",
|
||||
"locale": "Regional",
|
||||
"locales": "Locales",
|
||||
"menu": "Menú",
|
||||
"moveDown": "Mover abajo",
|
||||
"moveUp": "Mover arriba",
|
||||
"newPassword": "Nueva contraseña",
|
||||
@@ -216,8 +217,8 @@
|
||||
"notFound": "No encontrado",
|
||||
"nothingFound": "No se encontró nada",
|
||||
"of": "de",
|
||||
"or": "O",
|
||||
"open": "Abrir",
|
||||
"or": "O",
|
||||
"order": "Orden",
|
||||
"pageNotFound": "Página no encontrada",
|
||||
"password": "Contraseña",
|
||||
@@ -368,4 +369,4 @@
|
||||
"viewingVersions": "Viendo versiones para {{entityLabel}} {{documentTitle}}",
|
||||
"viewingVersionsGlobal": "Viendo versiones para el global {{entityLabel}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -204,6 +204,7 @@
|
||||
"loading": "در حال بارگذاری",
|
||||
"locale": "زبان",
|
||||
"locales": "زبانها",
|
||||
"menu": "منو",
|
||||
"moveDown": "حرکت به پایین",
|
||||
"moveUp": "حرکت به بالا",
|
||||
"newPassword": "گذرواژه تازه",
|
||||
@@ -216,8 +217,8 @@
|
||||
"notFound": "یافت نشد",
|
||||
"nothingFound": "چیزی یافت نشد",
|
||||
"of": "از",
|
||||
"or": "یا",
|
||||
"open": "باز کردن",
|
||||
"or": "یا",
|
||||
"order": "چیدمان",
|
||||
"pageNotFound": "برگه یافت نشد",
|
||||
"password": "گذرواژه",
|
||||
@@ -368,4 +369,4 @@
|
||||
"viewingVersions": "مشاهده نگارشها برای {{entityLabel}} {{documentTitle}}",
|
||||
"viewingVersionsGlobal": "مشاهده نگارشهای کلی {{entityLabel}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -204,6 +204,7 @@
|
||||
"loading": "Chargement en cours",
|
||||
"locale": "Paramètres régionaux",
|
||||
"locales": "Paramètres régionaux",
|
||||
"menu": "Menu",
|
||||
"moveDown": "Déplacer vers le bas",
|
||||
"moveUp": "Déplacer vers le haut",
|
||||
"newPassword": "Nouveau mot de passe",
|
||||
@@ -216,8 +217,8 @@
|
||||
"notFound": "Pas trouvé",
|
||||
"nothingFound": "Rien n'a été trouvé",
|
||||
"of": "de",
|
||||
"or": "ou",
|
||||
"open": "Ouvrir",
|
||||
"or": "ou",
|
||||
"order": "Ordre",
|
||||
"pageNotFound": "Page non trouvée",
|
||||
"password": "Mot de passe",
|
||||
@@ -368,4 +369,4 @@
|
||||
"viewingVersions": "Affichage des versions de ou du {{entityLabel}} {{documentTitle}}",
|
||||
"viewingVersionsGlobal": "Affichage des versions globales de ou du {{entityLabel}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -204,6 +204,7 @@
|
||||
"loading": "Učitavanje",
|
||||
"locale": "Jezik",
|
||||
"locales": "Prijevodi",
|
||||
"menu": "Izbornik",
|
||||
"moveDown": "Pomakni dolje",
|
||||
"moveUp": "Pomakni gore",
|
||||
"newPassword": "Nova lozinka",
|
||||
@@ -216,8 +217,8 @@
|
||||
"notFound": "Nije pronađeno",
|
||||
"nothingFound": "Ništa nije pronađeno",
|
||||
"of": "Od",
|
||||
"or": "Ili",
|
||||
"open": "Otvori",
|
||||
"or": "Ili",
|
||||
"order": "Poredak",
|
||||
"pageNotFound": "Stranica nije pronađena",
|
||||
"password": "Lozinka",
|
||||
@@ -368,4 +369,4 @@
|
||||
"viewingVersions": "Pregled verzija za {{entityLabel}} {{documentTitle}}",
|
||||
"viewingVersionsGlobal": "Pregled verzije za globalni {{entityLabel}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -204,6 +204,7 @@
|
||||
"loading": "Betöltés",
|
||||
"locale": "Nyelv",
|
||||
"locales": "Nyelvek",
|
||||
"menu": "Menü",
|
||||
"moveDown": "Mozgatás lefelé",
|
||||
"moveUp": "Mozgatás felfelé",
|
||||
"newPassword": "Új jelszó",
|
||||
@@ -216,8 +217,8 @@
|
||||
"notFound": "Nem található",
|
||||
"nothingFound": "Nincs találat",
|
||||
"of": "a",
|
||||
"or": "Vagy",
|
||||
"open": "Megnyitás",
|
||||
"or": "Vagy",
|
||||
"order": "Sorrend",
|
||||
"pageNotFound": "Az oldal nem található",
|
||||
"password": "Jelszó",
|
||||
@@ -368,4 +369,4 @@
|
||||
"viewingVersions": "A {{entityLabel}} {{documentTitle}} verzióinak megtekintése",
|
||||
"viewingVersionsGlobal": "A globális {{entityLabel}} verzióinak megtekintése"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -204,6 +204,7 @@
|
||||
"loading": "Caricamento",
|
||||
"locale": "Locale",
|
||||
"locales": "Localizzazioni",
|
||||
"menu": "Menù",
|
||||
"moveDown": "Sposta sotto",
|
||||
"moveUp": "Sposta sopra",
|
||||
"newPassword": "Nuova Password",
|
||||
@@ -216,8 +217,8 @@
|
||||
"notFound": "Non Trovato",
|
||||
"nothingFound": "Non è stato trovato nulla",
|
||||
"of": "di",
|
||||
"or": "Oppure",
|
||||
"open": "Apri",
|
||||
"or": "Oppure",
|
||||
"order": "Ordine",
|
||||
"pageNotFound": "Pagina non trovata",
|
||||
"password": "Password",
|
||||
@@ -369,4 +370,4 @@
|
||||
"viewingVersions": "Visualizzazione delle versioni per {{entityLabel}} {{documentTitle}}",
|
||||
"viewingVersionsGlobal": "`Visualizzazione delle versioni per {{entityLabel}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -204,6 +204,7 @@
|
||||
"loading": "ローディング中",
|
||||
"locale": "ロケール",
|
||||
"locales": "ロケール",
|
||||
"menu": "メニュー",
|
||||
"moveDown": "下へ移動",
|
||||
"moveUp": "上へ移動",
|
||||
"newPassword": "新しいパスワード",
|
||||
@@ -216,8 +217,8 @@
|
||||
"notFound": "Not Found",
|
||||
"nothingFound": "Nothing found",
|
||||
"of": "/",
|
||||
"or": "または",
|
||||
"open": "開く",
|
||||
"or": "または",
|
||||
"order": "表示順",
|
||||
"pageNotFound": "ページが見つかりません",
|
||||
"password": "パスワード",
|
||||
@@ -368,4 +369,4 @@
|
||||
"viewingVersions": "表示バージョン: {{entityLabel}} {{documentTitle}}",
|
||||
"viewingVersionsGlobal": "表示バージョン: グローバルな {{entityLabel}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -204,6 +204,7 @@
|
||||
"loading": "ဖွင့်နေသည်",
|
||||
"locale": "ဒေသ",
|
||||
"locales": "Locales",
|
||||
"menu": "မီနူး",
|
||||
"moveDown": "Move Down",
|
||||
"moveUp": "Move Up",
|
||||
"newPassword": "စကားဝှက် အသစ်",
|
||||
@@ -216,8 +217,8 @@
|
||||
"notFound": "ဘာမှ မရှိတော့ဘူး။",
|
||||
"nothingFound": "ဘာမှလည်း မတွေ့ဘူး။",
|
||||
"of": "၏",
|
||||
"or": "သို့မဟုတ်",
|
||||
"open": "ဖွင့်မည်။",
|
||||
"or": "သို့မဟုတ်",
|
||||
"order": "အစဉ်လိုက်",
|
||||
"pageNotFound": "ရောက်ရှိနေသော စာမျက်နှာသည် မရှိပါ။",
|
||||
"password": "စကားဝှက်",
|
||||
@@ -368,4 +369,4 @@
|
||||
"viewingVersions": "{{entityLabel}} {{documentTitle}} အတွက် ဗားရှင်းများကို ကြည့်ရှုခြင်း",
|
||||
"viewingVersionsGlobal": "`ဂလိုဘယ်ဆိုင်ရာ {{entityLabel}} အတွက် ဗားရှင်းများကို ကြည့်ရှုနေသည်"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -204,6 +204,7 @@
|
||||
"loading": "Laster",
|
||||
"locale": "Lokalitet",
|
||||
"locales": "Språk",
|
||||
"menu": "Meny",
|
||||
"moveDown": "Flytt ned",
|
||||
"moveUp": "Flytt opp",
|
||||
"newPassword": "Nytt passord",
|
||||
@@ -216,8 +217,8 @@
|
||||
"notFound": "Ikke funnet",
|
||||
"nothingFound": "Ingenting funnet",
|
||||
"of": "av",
|
||||
"or": "Eller",
|
||||
"open": "Åpne",
|
||||
"or": "Eller",
|
||||
"order": "Rekkefølge",
|
||||
"pageNotFound": "Siden ble ikke funnet",
|
||||
"password": "Passord",
|
||||
@@ -368,4 +369,4 @@
|
||||
"viewingVersions": "Viser versjoner for {{entityLabel}} {{documentTitle}}",
|
||||
"viewingVersionsGlobal": "Viser versjoner for den globale variabelen {{entityLabel}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -204,6 +204,7 @@
|
||||
"loading": "Laden",
|
||||
"locale": "Taal",
|
||||
"locales": "Landinstellingen",
|
||||
"menu": "Menu",
|
||||
"moveDown": "Verplaats naar beneden",
|
||||
"moveUp": "Verplaats naar boven",
|
||||
"newPassword": "Nieuw wachtwoord",
|
||||
@@ -216,8 +217,8 @@
|
||||
"notFound": "Niet gevonden",
|
||||
"nothingFound": "Niets gevonden",
|
||||
"of": "van",
|
||||
"or": "Of",
|
||||
"open": "Open",
|
||||
"or": "Of",
|
||||
"order": "Volgorde",
|
||||
"pageNotFound": "Pagina niet gevonden",
|
||||
"password": "Wachtwoord",
|
||||
@@ -368,4 +369,4 @@
|
||||
"viewingVersions": "Bekijk versies voor {{entityLabel}} {{documentTitle}}",
|
||||
"viewingVersionsGlobal": "`Bekijk versies voor global {{entityLabel}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -180,7 +180,7 @@
|
||||
"deselectAllRows": "Odznacz wszystkie wiersze",
|
||||
"duplicate": "Zduplikuj",
|
||||
"duplicateWithoutSaving": "Zduplikuj bez zapisywania zmian",
|
||||
"edit": "Edytować",
|
||||
"edit": "Edytuj",
|
||||
"editLabel": "Edytuj {{label}}",
|
||||
"editing": "Edycja",
|
||||
"editingLabel_many": "Edytowanie {{count}} {{label}}",
|
||||
@@ -205,6 +205,7 @@
|
||||
"loading": "Ładowanie",
|
||||
"locale": "Lokalizacja",
|
||||
"locales": "Lokalne",
|
||||
"menu": "Menu",
|
||||
"moveDown": "Przesuń niżej",
|
||||
"moveUp": "Przesuń wyżej",
|
||||
"newPassword": "Nowe hasło",
|
||||
@@ -217,8 +218,8 @@
|
||||
"notFound": "Nie znaleziono",
|
||||
"nothingFound": "Nic nie znaleziono",
|
||||
"of": "z",
|
||||
"or": "Lub",
|
||||
"open": "Otwórz",
|
||||
"or": "Lub",
|
||||
"order": "Kolejność",
|
||||
"pageNotFound": "Strona nie znaleziona",
|
||||
"password": "Hasło",
|
||||
@@ -274,7 +275,7 @@
|
||||
"near": "blisko"
|
||||
},
|
||||
"upload": {
|
||||
"crop": "Uprawa",
|
||||
"crop": "Przytnij",
|
||||
"cropToolDescription": "Przeciągnij narożniki wybranego obszaru, narysuj nowy obszar lub dostosuj poniższe wartości.",
|
||||
"dragAndDrop": "Przeciągnij i upuść plik",
|
||||
"dragAndDropHere": "lub złap i upuść plik tutaj",
|
||||
@@ -339,7 +340,7 @@
|
||||
"noRowsFound": "Nie znaleziono {{label}}",
|
||||
"preview": "Podgląd",
|
||||
"problemRestoringVersion": "Wystąpił problem podczas przywracania tej wersji",
|
||||
"publish": "Publikować",
|
||||
"publish": "Publikuj",
|
||||
"publishChanges": "Opublikuj zmiany",
|
||||
"published": "Opublikowano",
|
||||
"restoreThisVersion": "Przywróć tę wersję",
|
||||
@@ -369,4 +370,4 @@
|
||||
"viewingVersions": "Przeglądanie wersji {{entityLabel}} {{documentTitle}}",
|
||||
"viewingVersionsGlobal": "Przeglądanie wersji dla globalnej kolekcji {{entityLabel}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -204,6 +204,7 @@
|
||||
"loading": "Carregando",
|
||||
"locale": "Local",
|
||||
"locales": "Localizações",
|
||||
"menu": "Cardápio",
|
||||
"moveDown": "Mover para Baixo",
|
||||
"moveUp": "Mover para Cima",
|
||||
"newPassword": "Nova Senha",
|
||||
@@ -216,8 +217,8 @@
|
||||
"notFound": "Não Encontrado",
|
||||
"nothingFound": "Nada encontrado",
|
||||
"of": "de",
|
||||
"or": "Ou",
|
||||
"open": "Abrir",
|
||||
"or": "Ou",
|
||||
"order": "Ordem",
|
||||
"pageNotFound": "Página não encontrada",
|
||||
"password": "Senha",
|
||||
@@ -368,4 +369,4 @@
|
||||
"viewingVersions": "Visualizando versões para o/a {{entityLabel}} {{documentTitle}}",
|
||||
"viewingVersionsGlobal": "`Visualizando versões para o global {{entityLabel}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -204,6 +204,7 @@
|
||||
"loading": "Încărcare",
|
||||
"locale": "Localitate",
|
||||
"locales": "Localuri",
|
||||
"menu": "Meniu",
|
||||
"moveDown": "Mutați în jos",
|
||||
"moveUp": "Mutați în sus",
|
||||
"newPassword": "Parolă nouă",
|
||||
@@ -216,8 +217,8 @@
|
||||
"notFound": "Nu a fost găsit",
|
||||
"nothingFound": "Nimic găsit",
|
||||
"of": "de",
|
||||
"or": "Sau",
|
||||
"open": "Deschide",
|
||||
"or": "Sau",
|
||||
"order": "ORdine",
|
||||
"pageNotFound": "Pagina nu a fost găsită",
|
||||
"password": "Parola",
|
||||
@@ -368,4 +369,4 @@
|
||||
"viewingVersions": "Vizualizarea versiunilor pentru {{entityLabel}} {{documentTitle}}",
|
||||
"viewingVersionsGlobal": "Vizualizarea versiunilor pentru globala {{entityLabel}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -204,6 +204,7 @@
|
||||
"loading": "Загрузка",
|
||||
"locale": "Локаль",
|
||||
"locales": "Локали",
|
||||
"menu": "Меню",
|
||||
"moveDown": "Сдвинуть вниз",
|
||||
"moveUp": "Сдвинуть вверх",
|
||||
"newPassword": "Новый пароль",
|
||||
@@ -216,8 +217,8 @@
|
||||
"notFound": "Не найдено",
|
||||
"nothingFound": "Ничего не найдено",
|
||||
"of": "из",
|
||||
"or": "Или же",
|
||||
"open": "Открыть",
|
||||
"or": "Или же",
|
||||
"order": "Порядок",
|
||||
"pageNotFound": "Страница не найдена",
|
||||
"password": "Пароль",
|
||||
@@ -368,4 +369,4 @@
|
||||
"viewingVersions": "Просмотр версий для {{entityLabel}} {{documentTitle}}",
|
||||
"viewingVersionsGlobal": "`Просмотр версии для глобальной Коллекции {{entityLabel}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -204,6 +204,7 @@
|
||||
"loading": "Läser in",
|
||||
"locale": "Lokal",
|
||||
"locales": "Språk",
|
||||
"menu": "Meny",
|
||||
"moveDown": "Flytta Ner",
|
||||
"moveUp": "Flytta Upp",
|
||||
"newPassword": "Nytt Lösenord",
|
||||
@@ -216,8 +217,8 @@
|
||||
"notFound": "Hittades inte",
|
||||
"nothingFound": "Inget hittades",
|
||||
"of": "av",
|
||||
"or": "Eller",
|
||||
"open": "Öppna",
|
||||
"or": "Eller",
|
||||
"order": "Ordning",
|
||||
"pageNotFound": "Sidan hittas inte",
|
||||
"password": "Lösenord",
|
||||
@@ -368,4 +369,4 @@
|
||||
"viewingVersions": "Visar versioner för {{entityLabel}} {{documentTitle}}",
|
||||
"viewingVersionsGlobal": "Visa versioner för den globala {{entityLabel}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -204,6 +204,7 @@
|
||||
"loading": "กำลังโหลด",
|
||||
"locale": "ตำแหน่งที่ตั้ง",
|
||||
"locales": "ภาษา",
|
||||
"menu": "เมนู",
|
||||
"moveDown": "ขยับขึ้น",
|
||||
"moveUp": "ขยับลง",
|
||||
"newPassword": "รหัสผ่านใหม่",
|
||||
@@ -216,8 +217,8 @@
|
||||
"notFound": "ไม่พบ",
|
||||
"nothingFound": "ไม่พบสิ่งใด",
|
||||
"of": "จาก",
|
||||
"or": "หรือ",
|
||||
"open": "เปิด",
|
||||
"or": "หรือ",
|
||||
"order": "เรียงตาม",
|
||||
"pageNotFound": "ไม่พบหน้าที่ต้องการ",
|
||||
"password": "รหัสผ่าน",
|
||||
@@ -368,4 +369,4 @@
|
||||
"viewingVersions": "กำลังดูเวอร์ชันของ {{entityLabel}} {{documentTitle}}",
|
||||
"viewingVersionsGlobal": "กำลังดูเวอร์ชันของ global {{entityLabel}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -204,6 +204,7 @@
|
||||
"loading": "Yükleniyor",
|
||||
"locale": "Yerel ayar",
|
||||
"locales": "Diller",
|
||||
"menu": "Menü",
|
||||
"moveDown": "Aşağı taşı",
|
||||
"moveUp": "Yukarı taşı",
|
||||
"newPassword": "Yeni parola",
|
||||
@@ -216,8 +217,8 @@
|
||||
"notFound": "Bulunamadı",
|
||||
"nothingFound": "Hiçbir şey bulunamadı",
|
||||
"of": "of",
|
||||
"or": "Or",
|
||||
"open": "Aç",
|
||||
"or": "Or",
|
||||
"order": "Order",
|
||||
"pageNotFound": "Sayfa bulunamadı",
|
||||
"password": "Parola",
|
||||
@@ -368,4 +369,4 @@
|
||||
"viewingVersions": "{{entityLabel}} {{documentTitle}} için sürümler gösteriliyor",
|
||||
"viewingVersionsGlobal": "`Global {{entityLabel}} için sürümler gösteriliyor"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -754,6 +754,9 @@
|
||||
"locales": {
|
||||
"type": "string"
|
||||
},
|
||||
"menu": {
|
||||
"type": "string"
|
||||
},
|
||||
"moveDown": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
@@ -204,6 +204,7 @@
|
||||
"loading": "Загрузка",
|
||||
"locale": "Локаль",
|
||||
"locales": "Переклади",
|
||||
"menu": "Меню",
|
||||
"moveDown": "Перемістити нижче",
|
||||
"moveUp": "Перемістити вище",
|
||||
"newPassword": "Новий пароль",
|
||||
@@ -216,8 +217,8 @@
|
||||
"notFound": "Не знайдено",
|
||||
"nothingFound": "Нічого не знайдено",
|
||||
"of": "з",
|
||||
"or": "або",
|
||||
"open": "Відкрити",
|
||||
"or": "або",
|
||||
"order": "Порядок",
|
||||
"pageNotFound": "Сторінка не знайдена",
|
||||
"password": "Пароль",
|
||||
@@ -368,4 +369,4 @@
|
||||
"viewingVersions": "Огляд версій для {{entityLabel}} {{documentTitle}}",
|
||||
"viewingVersionsGlobal": "Огляд версій для глобальної колекції {{entityLabel}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -204,6 +204,7 @@
|
||||
"loading": "Đang tải",
|
||||
"locale": "Ngôn ngữ",
|
||||
"locales": "Khu vực",
|
||||
"menu": "Thực đơn",
|
||||
"moveDown": "Di chuyển xuống",
|
||||
"moveUp": "Di chuyển lên",
|
||||
"newPassword": "Mật khảu mới",
|
||||
@@ -216,8 +217,8 @@
|
||||
"notFound": "Không tìm thấy",
|
||||
"nothingFound": "Không tìm thấy",
|
||||
"of": "trong số",
|
||||
"or": "hoặc",
|
||||
"open": "Mở",
|
||||
"or": "hoặc",
|
||||
"order": "Thứ tự",
|
||||
"pageNotFound": "Không tìm thấy trang",
|
||||
"password": "Mật khẩu",
|
||||
@@ -368,4 +369,4 @@
|
||||
"viewingVersions": "Xem những phiên bản của {{entityLabel}} {{documentTitle}}",
|
||||
"viewingVersionsGlobal": "`Xem những phiên bản toàn thể (global) của {{entityLabel}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -204,6 +204,7 @@
|
||||
"loading": "加载中...",
|
||||
"locale": "语言环境",
|
||||
"locales": "语言环境",
|
||||
"menu": "菜单",
|
||||
"moveDown": "向下移动",
|
||||
"moveUp": "向上移动",
|
||||
"newPassword": "新密码",
|
||||
@@ -216,8 +217,8 @@
|
||||
"notFound": "未找到",
|
||||
"nothingFound": "没有找到任何东西",
|
||||
"of": "的",
|
||||
"or": "或",
|
||||
"open": "打开",
|
||||
"or": "或",
|
||||
"order": "排序",
|
||||
"pageNotFound": "未找到页面",
|
||||
"password": "密码",
|
||||
@@ -368,4 +369,4 @@
|
||||
"viewingVersions": "正在查看{{entityLabel}} {{documentTitle}}的版本",
|
||||
"viewingVersionsGlobal": "正在查看全局{{entityLabel}}的版本"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { UploadedFile } from 'express-fileupload'
|
||||
import type { Sharp, SharpOptions } from 'sharp'
|
||||
import type { OutputInfo, Sharp, SharpOptions } from 'sharp'
|
||||
|
||||
import { fromBuffer } from 'file-type'
|
||||
import fs from 'fs'
|
||||
@@ -96,10 +96,12 @@ export const generateFileData = async <T>({
|
||||
let fsSafeName: string
|
||||
let sharpFile: Sharp | undefined
|
||||
let dimensions: ProbedImageSize | undefined
|
||||
let fileBuffer
|
||||
let fileBuffer: { data: Buffer; info: OutputInfo }
|
||||
let ext
|
||||
let mime: string
|
||||
const isSharpRequired = fileSupportsResize && (resizeOptions || formatOptions || trimOptions)
|
||||
const isSharpRequired =
|
||||
fileSupportsResize &&
|
||||
Boolean(resizeOptions || formatOptions || trimOptions || file.tempFilePath)
|
||||
|
||||
const sharpOptions: SharpOptions = {}
|
||||
|
||||
@@ -195,14 +197,17 @@ export const generateFileData = async <T>({
|
||||
path: `${staticPath}/${fsSafeName}`,
|
||||
})
|
||||
|
||||
if (file.tempFilePath) {
|
||||
await fs.promises.writeFile(file.tempFilePath, fileBuffer?.data) // write fileBuffer to the temp path
|
||||
} else {
|
||||
// Assign the _possibly modified_ file to the request object
|
||||
req.files.file = {
|
||||
...file,
|
||||
data: fileBuffer?.data || file.data,
|
||||
size: fileBuffer?.info.size,
|
||||
// If using temp files and the image is being resized, write the file to the temp path
|
||||
if (fileBuffer?.data || file.data.length > 0) {
|
||||
if (file.tempFilePath) {
|
||||
await fs.promises.writeFile(file.tempFilePath, fileBuffer?.data || file.data) // write fileBuffer to the temp path
|
||||
} else {
|
||||
// Assign the _possibly modified_ file to the request object
|
||||
req.files.file = {
|
||||
...file,
|
||||
data: fileBuffer?.data || file.data,
|
||||
size: fileBuffer?.info.size,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import type { Request } from 'express'
|
||||
|
||||
import { APIError } from '../errors'
|
||||
|
||||
export default function parseCookies(req: Request): { [key: string]: string } {
|
||||
const list = {}
|
||||
const rc = req.headers.cookie
|
||||
@@ -7,7 +9,15 @@ export default function parseCookies(req: Request): { [key: string]: string } {
|
||||
if (rc) {
|
||||
rc.split(';').forEach((cookie) => {
|
||||
const parts = cookie.split('=')
|
||||
list[parts.shift().trim()] = decodeURI(parts.join('='))
|
||||
const key = parts.shift().trim()
|
||||
const encodedValue = parts.join('=')
|
||||
|
||||
try {
|
||||
const decodedValue = decodeURI(encodedValue)
|
||||
list[key] = decodedValue
|
||||
} catch (e) {
|
||||
throw new APIError(`Error decoding cookie value for key ${key}: ${e.message}`)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -86,6 +86,11 @@ const replaceWithDraftIfAvailable = async <T extends TypeWithID>({
|
||||
draft = JSON.parse(JSON.stringify(draft))
|
||||
draft = sanitizeInternalFields(draft)
|
||||
|
||||
// Patch globalType onto version doc
|
||||
if (entityType === 'global' && 'globalType' in doc) {
|
||||
draft.version.globalType = doc.globalType
|
||||
}
|
||||
|
||||
// Disregard all other draft content at this point,
|
||||
// Only interested in the version itself.
|
||||
// Operations will handle firing hooks, etc.
|
||||
|
||||
@@ -39,22 +39,20 @@ export const saveVersion = async ({
|
||||
if (autosave) {
|
||||
let docs
|
||||
const findVersionArgs = {
|
||||
collectionSlug: collection.slug,
|
||||
globalSlug: collection.slug,
|
||||
limit: 1,
|
||||
req,
|
||||
sort: '-updatedAt',
|
||||
where: {
|
||||
parent: {
|
||||
equals: id,
|
||||
},
|
||||
},
|
||||
}
|
||||
if (collection) {
|
||||
;({ docs } = await payload.db.findVersions({
|
||||
...findVersionArgs,
|
||||
collection: collection.slug,
|
||||
req,
|
||||
where: {
|
||||
parent: {
|
||||
equals: id,
|
||||
},
|
||||
},
|
||||
}))
|
||||
} else {
|
||||
;({ docs } = await payload.db.findGlobalVersions({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/richtext-lexical",
|
||||
"version": "0.1.15",
|
||||
"version": "0.1.16",
|
||||
"description": "The officially supported Lexical richtext adapter for Payload",
|
||||
"repository": "https://github.com/payloadcms/payload",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import type { SerializedEditorState } from 'lexical'
|
||||
import type { RichTextField, TextField } from 'payload/types'
|
||||
|
||||
import type { LexicalRichTextAdapter } from '../../../../../index'
|
||||
import type { LexicalRichTextAdapter, SanitizedEditorConfig } from '../../../../../index'
|
||||
import type { AdapterProps } from '../../../../../types'
|
||||
import type { HTMLConverter } from '../converter/types'
|
||||
import type { HTMLConverterFeatureProps } from '../index'
|
||||
|
||||
import { cloneDeep } from '../../../../../index'
|
||||
import { convertLexicalToHTML } from '../converter'
|
||||
import { defaultHTMLConverters } from '../converter/defaultConverters'
|
||||
|
||||
@@ -14,6 +13,37 @@ type Props = {
|
||||
name: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Combines the default HTML converters with HTML converters found in the features, and with HTML converters configured in the htmlConverter feature.
|
||||
*
|
||||
* @param editorConfig
|
||||
*/
|
||||
export const consolidateHTMLConverters = ({
|
||||
editorConfig,
|
||||
}: {
|
||||
editorConfig: SanitizedEditorConfig
|
||||
}) => {
|
||||
const htmlConverterFeature = editorConfig.resolvedFeatureMap.get('htmlConverter')
|
||||
const htmlConverterFeatureProps: HTMLConverterFeatureProps = htmlConverterFeature?.props
|
||||
|
||||
const defaultConvertersWithConvertersFromFeatures = defaultHTMLConverters
|
||||
|
||||
for (const converter of editorConfig.features.converters.html) {
|
||||
defaultConvertersWithConvertersFromFeatures.push(converter)
|
||||
}
|
||||
|
||||
const finalConverters =
|
||||
htmlConverterFeatureProps?.converters &&
|
||||
typeof htmlConverterFeatureProps?.converters === 'function'
|
||||
? htmlConverterFeatureProps.converters({
|
||||
defaultConverters: defaultConvertersWithConvertersFromFeatures,
|
||||
})
|
||||
: (htmlConverterFeatureProps?.converters as HTMLConverter[]) ||
|
||||
defaultConvertersWithConvertersFromFeatures
|
||||
|
||||
return finalConverters
|
||||
}
|
||||
|
||||
export const lexicalHTML: (lexicalFieldName: string, props: Props) => TextField = (
|
||||
lexicalFieldName,
|
||||
props,
|
||||
@@ -57,23 +87,10 @@ export const lexicalHTML: (lexicalFieldName: string, props: Props) => TextField
|
||||
'You cannot use the lexicalHTML field because the htmlConverter feature was not found',
|
||||
)
|
||||
}
|
||||
const htmlConverterFeature = config.resolvedFeatureMap.get('htmlConverter')
|
||||
const htmlConverterFeatureProps: HTMLConverterFeatureProps = htmlConverterFeature.props
|
||||
|
||||
const defaultConvertersWithConvertersFromFeatures = cloneDeep(defaultHTMLConverters)
|
||||
|
||||
for (const converter of config.features.converters.html) {
|
||||
defaultConvertersWithConvertersFromFeatures.push(converter)
|
||||
}
|
||||
|
||||
const finalConverters =
|
||||
htmlConverterFeatureProps?.converters &&
|
||||
typeof htmlConverterFeatureProps?.converters === 'function'
|
||||
? htmlConverterFeatureProps.converters({
|
||||
defaultConverters: defaultConvertersWithConvertersFromFeatures,
|
||||
})
|
||||
: (htmlConverterFeatureProps?.converters as HTMLConverter[]) ||
|
||||
defaultConvertersWithConvertersFromFeatures
|
||||
const finalConverters = consolidateHTMLConverters({
|
||||
editorConfig: config,
|
||||
})
|
||||
|
||||
return await convertLexicalToHTML({
|
||||
converters: finalConverters,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user