Compare commits

..

212 Commits

Author SHA1 Message Date
Elliot DeNolf
a8ac5d2cfd chore(release): db-postgres/0.2.1 [skip ci] 2023-12-06 13:37:13 -05:00
Elliot DeNolf
e6318ae700 chore(release): richtext-slate/1.3.0 [skip ci] 2023-12-06 13:36:38 -05:00
Elliot DeNolf
f5166cd4c4 chore(release): richtext-lexical/0.4.0 [skip ci] 2023-12-06 13:36:05 -05:00
Elliot DeNolf
7c7bf51b4b chore(richtext-lexical): bump payload peer dep 2023-12-06 13:35:27 -05:00
Elliot DeNolf
eefcd88de7 chore(release): payload/2.4.0 [skip ci] 2023-12-06 13:34:08 -05:00
Alessio Gravili
217cc1fc42 docs: update CONTRIBUTING.md (#4401) 2023-12-06 19:15:17 +01:00
Alessio Gravili
7af8f29b4a feat(richtext-lexical): Link & Relationship Feature: field-level configurable allowed relationships (#4182)
* feat(richtext-lexical): ability to configure link feature enabled relations on a field-level

* feat(richtext-lexical): ability to configure Relationship feature enabled relations on a field-level

* chore(richtext-lexical): Improve Link feature props typing

* chore(richtext-lexical): Improve Link and Relationship feature props typing

* fix(richtext-lexical): Link drawer types

* chore: merge conflict resolve

* chore(richtext-lexical): Link Feature: add comments that explain how getBaseFields works
2023-12-06 17:50:50 +01:00
chris
5f2cd1ae77 docs: fix example link (#4394) 2023-12-06 09:35:30 -05:00
Dan Ribbens
dbaecda0e9 fix(db-postgres): sorting on a not-configured field throws error (#4382) 2023-12-06 09:27:43 -05:00
Tylan Davis
cf9a3704df fix: handles null upload field values (#4397) 2023-12-06 09:26:19 -05:00
Patrik
4b5453e8e5 fix: simplifies query validation and fixes nested relationship fields (#4391) 2023-12-06 08:47:34 -05:00
Alessio Gravili
5de347ffff feat(richtext-lexical)!: lazy import React components to prevent client-only code from leaking into the server (#4290)
* chore(richtext-lexical): lazy import all React things

* chore(richtext-lexical): use useMemo for lazy-loaded React Components to prevent lag and flashes when parent component re-renders

* chore: make exportPointerFiles.ts script usable for other packages as well by hoisting it up to the workspace root and making it configurable

* chore(richtext-lexical): make sure no client-side code is imported by default from Features

* chore(richtext-lexical): remove unnecessary scss files

* chore(richtext-lexical): adjust package.json exports

* chore(richtext-*): lazy-import Field & Cell Components, move Client-only exports to /components subpath export

* chore(richtext-lexical): make sure nothing client-side is directly exported from the / subpath export anymore

* add missing imports

* chore: remove breaking changes for Slate

* LazyCellComponent & LazyFieldComponent
2023-12-06 14:20:18 +01:00
Jacob Fletcher
80ef18c149 fix(templates/website): removes unused form builder plugin from deps (#4385) 2023-12-05 11:57:21 -05:00
Christian May
912abe2b64 docs(plugin-nested-docs): clarifies that relationships are intra-collection (#4375) 2023-12-05 00:58:36 -05:00
Jacob Fletcher
4090aebb0e fix(live-preview): populates rte uploads and relationships (#4379) 2023-12-05 00:53:11 -05:00
chris
290e9d8238 docs: fixes typo in migrations (#4374) 2023-12-05 00:24:37 -05:00
Kane Wang
50253f617c feat: add Chinese Traditional translation (#4372) 2023-12-04 15:29:29 -05:00
geisterfurz007
999e05d1b4 docs: fix typo in migrations (#4356) 2023-12-04 15:09:21 -05:00
Dan Ribbens
b6cffcea07 fix: defaultValues computed on new globals (#4380) 2023-12-04 15:05:47 -05:00
Patrik
7b2eb0c175 docs: updates afterInput example (#4378) 2023-12-04 14:35:08 -05:00
Timothy Choi
3b8a27d199 feat: pass path to FieldDescription (#4364)
fix: DescriptionFunction type

Co-authored-by: Jarrod Flesch <jarrodmflesch@gmail.com>
2023-12-04 13:59:18 -05:00
Jessica Chowdhury
65adfd21ed fix: uploads files after validation (#4218) 2023-12-04 12:38:23 -05:00
Jacob Fletcher
03a387233d fix(live-preview): sends raw js objects through window.postMessage instead of json (#4354) 2023-12-01 17:50:55 -05:00
Jessica Chowdhury
fcbe5744d9 fix: upload editing error with plugin-cloud (#4170)
Co-authored-by: Jarrod Flesch <jarrodmflesch@gmail.com>
2023-12-01 16:35:12 -05:00
Jessica Chowdhury
06bf6a426e docs: destructures vite bundler import (#4280) 2023-12-01 16:32:10 -05:00
Renat Sagdeev
b634d5e552 docs: fix ul and ol tags mentioned in lexical rte (#4338) 2023-12-01 16:29:43 -05:00
Jacob Fletcher
5f173241df feat: async live preview urls (#4339) 2023-12-01 16:25:39 -05:00
Markus Machatschek
0bd12e01d7 docs: update software requirements to mention pnpm (#4335)
Co-authored-by: Jacob Fletcher <jacobsfletch@gmail.com>
2023-12-01 16:25:21 -05:00
Elliot DeNolf
b6f02765eb chore(release): richtext-lexical/0.3.1 [skip ci] 2023-12-01 16:19:58 -05:00
Elliot DeNolf
156ffdd18c chore(release): payload/2.3.1 [skip ci] 2023-12-01 16:18:48 -05:00
Jarrod Flesch
fe888b5f6c fix: query validation on relationship fields (#4353) 2023-12-01 16:03:58 -05:00
Alessio Gravili
bea79feaea fix: ensure doc controls are not hidden behind lexical field (#4345) 2023-12-01 10:23:39 +01:00
Alessio Gravili
293cee6f90 chore(plugin-stripe): rename build script to _build (#4344) 2023-12-01 10:04:37 +01:00
Alessio Gravili
3e745e91da fix(richtext-lexical): blocks content may be hidden behind components outside of the editor (#4325)
* chore(richtext-lexical): add e2e test to reproduce the issue

* fix the issue
2023-12-01 09:46:21 +01:00
Alessio Gravili
4243048fc5 Merge pull request #4342 from payloadcms/fix/lexical-blocks-v1-conversion
fix(richtext-lexical): Blocks node: incorrect conversion from v1 node to v2 node
2023-12-01 09:29:57 +01:00
Alessio Gravili
ef84a2cfff fix(richtext-lexical): Blocks node: incorrect conversion from v1 node to v2 node 2023-12-01 09:28:44 +01:00
James
c00cbaabbc chore: lints 2023-11-30 18:34:38 -05:00
James
02f407e995 chore: lints 2023-11-30 17:12:01 -05:00
Elliot DeNolf
74e8051bb6 chore(templates): GRAPHQL_API_URL to ecommerce template 2023-11-30 17:01:19 -05:00
James
ee670b2b20 chore: introduces graphql_api_url to website template 2023-11-30 16:58:30 -05:00
Elliot DeNolf
2f8bcc977b fix(templates): adjust graphql endpoint 2023-11-30 16:19:46 -05:00
Dan Ribbens
0cc91d7377 docs: update mongoOptions to connectOptions (#4334) 2023-11-30 12:39:32 -05:00
Elliot DeNolf
34e89ff5db chore(release): richtext-slate/1.2.1 [skip ci] 2023-11-30 11:11:28 -05:00
Elliot DeNolf
b39b52dbd3 chore(release): live-preview-react/0.2.0 [skip ci] 2023-11-30 11:11:03 -05:00
Elliot DeNolf
7bfdb2627a chore(release): live-preview/0.2.0 [skip ci] 2023-11-30 11:10:55 -05:00
Elliot DeNolf
8f5867e876 chore(release): db-postgres/0.2.0 [skip ci] 2023-11-30 11:10:44 -05:00
Elliot DeNolf
45a3e31c95 chore(release): db-mongodb/1.1.0 [skip ci] 2023-11-30 11:10:33 -05:00
Elliot DeNolf
176550d26b chore(release): richtext-lexical/0.3.0 [skip ci] 2023-11-30 11:07:44 -05:00
Elliot DeNolf
53958d4662 chore(release): payload/2.3.0 [skip ci] 2023-11-30 11:05:49 -05:00
Elliot DeNolf
a0859114eb chore(richtext-*): bump payload peer dep 2023-11-30 10:59:26 -05:00
Jacob Fletcher
4adc30b034 chore: fixes failing postgres int test for live preview (#4329) 2023-11-30 10:30:09 -05:00
Alessio Gravili
ff61d5a099 chore(richtext-*): roll-back richtext adapter change 2023-11-30 16:25:17 +01:00
Jacob Fletcher
9cc88bb474 fix: properly sets tabs key in fieldSchemaToJSON (#4317) 2023-11-30 09:48:37 -05:00
Jacob Fletcher
57fc211674 fix(live-preview): re-populates externally updated relationships (#4287) 2023-11-30 09:47:56 -05:00
Dan Ribbens
9da9b1fc50 fix: duplicate documents with required localized fields (#4236) 2023-11-30 09:27:24 -05:00
Dan Ribbens
30d050ef86 chore: fix telemetry user id type string (#4321) 2023-11-30 09:26:51 -05:00
Alessio Gravili
9beb9c8627 chore(richtext-lexical): ensure CSS is not accidentally overridden (#4324) 2023-11-30 11:55:02 +01:00
Patrik
224cddd045 feat: relationship sortOptions property (#4301)
* feat: adds sortOptions property to relationship field

* chore: fix lexical int tests

* feat: simplifies logic & updates joi schema definition

* feat: revert to default when searching in relationship select

* fix types and joi schema

* type adjustments

---------

Co-authored-by: Alessio Gravili <alessio@bonfireleads.com>
Co-authored-by: Jarrod Flesch <jarrodmflesch@gmail.com>
2023-11-29 16:22:47 -05:00
Alessio Gravili
3502ce720b fix: incorrect key property in Tabs field component (#4311)
Fixes #4282
2023-11-29 22:18:40 +01:00
Jacob Fletcher
b8fa61942e chore(examples/custom-server): removes unnused dotenv.js mock file (#4316) 2023-11-29 16:08:16 -05:00
Jacob Fletcher
d49bb4351f feat(live-preview): batches api requests (#4315) 2023-11-29 14:03:05 -05:00
Jacob Fletcher
542096361f fix: properly exports useDocumentsEvents hook (#4314) 2023-11-29 12:26:20 -05:00
Jacob Fletcher
66679fbdd6 fix(live-preview): property resets rte nodes (#4313) 2023-11-29 12:24:51 -05:00
Jarrod Flesch
d4f28b16b4 fix(richtext-slate): add use client to top of tsx files importing from payload core (#4312) 2023-11-29 12:19:54 -05:00
Jarrod Flesch
cd07873fc5 fix(db-postgres): allow for nested block fields to be queried (#4237)
Co-authored-by: Dan Ribbens <dan.ribbens@gmail.com>
2023-11-29 11:03:57 -05:00
1nfinite9
6d28fc46bd docs: updates incorrect API for Reset Password (#4270)
Co-authored-by: Dan Ribbens <dan.ribbens@gmail.com>
2023-11-29 11:01:44 -05:00
Jacob Fletcher
8e903053e2 Merge pull request #4308 from payloadcms/fix/live-preview-deps
fix(live-preview,live-preview-react): removes payload from peer deps
2023-11-29 11:01:13 -05:00
Jacob Fletcher
7e1052fd98 fix(live-preview-react): removes payload from peer deps 2023-11-29 10:57:44 -05:00
Jacob Fletcher
b4af95f894 fix(live-preview): removes payload from peer deps 2023-11-29 10:57:31 -05:00
Jacob Fletcher
381c158b03 fix(live-preview): compounds merge results (#4306) 2023-11-29 10:54:14 -05:00
Dan Ribbens
3514bfbdae fix(db-postgres): error saving nested arrays with versions (#4302) 2023-11-29 10:10:08 -05:00
James Mikrut
4cfe473627 Merge pull request #4296 from TiKevin83/feature/bearer-jwts
feat: support OAuth 2.0 format Authorization: Bearer tokens in headers
2023-11-28 16:42:13 -05:00
Travis Mcgeehan
c1eb9d1727 feat: support OAuth 2.0 format Authorization: Bearer tokens in headers 2023-11-28 16:23:18 -05:00
Patrik
37b765cce8 fix(plugin-stripe): vite support (#4279)
* chore: updates export of stripe plugin & bumps payload versions

* chore: handles type errors

* chore: adds alias for stripeREST & strepWebhooks

* fixes issues with bundling within demo

* chore: defaults plugin-stripe demo to use vite bundler

* chore: updates pnpm lock file

* chore: removes yarn lock file from plugin-stripe demo

* chore: bumps payload in demo & cleans up demo config

* chore: updates pnpm lock file

---------

Co-authored-by: Jarrod Flesch <jarrodmflesch@gmail.com>
2023-11-28 14:12:18 -05:00
Alessio Gravili
5bf64933b4 fix(richtext-lexical): HTML Converter field not working inside of tabs (#4293) 2023-11-28 19:36:23 +01:00
Alessio Gravili
094d02ce1d fix(richtext-lexical): re-use payload population logic to fix population-related issues (#4291)
* chore(richtext-lexical): Add int test which reproduces the issue

* chore: Remove unnecessary await in core afterRead promise

* fix(richtext-lexical): re-use recurseNestedFields from payload instead of using own recurseNestedFields

* chore(richtext-lexical): pass in missing properties which are available in the core afterRead hook

* chore: remove unnecessary block
2023-11-28 19:18:07 +01:00
Elliot DeNolf
1fe4f4c5f4 Merge pull request #4292 from payloadcms/feat/migrate-with-js-files
feat: support migrations with js files
2023-11-28 11:40:36 -05:00
Elliot DeNolf
35ce0ebc83 docs: migrations directory details 2023-11-28 11:18:04 -05:00
Elliot DeNolf
530c825f80 feat(db-mongodb): search for migrations dir intelligently 2023-11-28 11:09:55 -05:00
Elliot DeNolf
308979f31d feat(db-postgres): search for migrations dir intelligently 2023-11-28 11:09:36 -05:00
Elliot DeNolf
2122242192 feat: support migrations with js files 2023-11-28 11:09:25 -05:00
Nikola Spalevic
40c8909ee0 feat: add serbian (latin and cyrillic) translations (#4268) 2023-11-28 09:08:26 -05:00
Dan Ribbens
babe3dba6a chore(db-postgres): add uuid to dependencies (#4286) 2023-11-28 09:08:08 -05:00
Jacob Fletcher
9bb7a88526 feat: useDocumentEvents (#4284) 2023-11-27 16:16:53 -05:00
Elliot DeNolf
098e389147 chore(release): payload/2.2.2 [skip ci] 2023-11-27 16:11:31 -05:00
James Mikrut
b56f1f4f2a Merge pull request #4285 from payloadcms/fix/doc-access-transactions
fix: transactions broken within doc access
2023-11-27 16:07:05 -05:00
James
443847ec71 fix: transactions broken within doc access 2023-11-27 15:56:18 -05:00
Elliot DeNolf
26f6b37a20 chore(release): bundler-vite/0.1.5 [skip ci] 2023-11-27 15:33:43 -05:00
Jessica Chowdhury
1dcd3a2782 fix: prevent json data getting reset when switching tabs (#4123) 2023-11-27 12:23:20 -05:00
Jacob Fletcher
6f59257574 Merge pull request #4250 from payloadcms/fix/live-preview-rels 2023-11-27 11:04:34 -05:00
Jacob Fletcher
65575d3573 Merge branch 'main' into fix/live-preview-rels 2023-11-27 10:54:27 -05:00
Tylan Davis
cbeb0a8bc7 fix(templates): uses context to prevent infinite loop in populateArchiveBlock (#4278) 2023-11-27 10:47:03 -05:00
Jacob Fletcher
ad62db01e7 chore(templates): adds comments to .env.example (#4276) 2023-11-27 10:43:03 -05:00
Kennet Winter
42cba2e3a1 docs: fix broken links to public demo (#4266) 2023-11-27 10:29:05 -05:00
Yunsup Sim
1401718b3b feat(i18n): adds Korean translation (#4258) 2023-11-27 10:26:53 -05:00
Jacob Fletcher
712647d741 docs: adds live preview troubleshooting tips 2023-11-27 08:09:26 -05:00
Alessio Gravili
c8d2b2b60e chore(richtext-lexical): fix failing e2e test due to html class changes (#4265) 2023-11-26 11:39:27 +01:00
Jacob Fletcher
aab2407112 fix(live-preview): clear hasMany relationships 2023-11-24 09:57:41 -05:00
Alessio Gravili
d439bf3011 Merge pull request #4257 from payloadcms/chore/lexical-impr
BREAKING: The last PR is breaking because it changes some properties of the SlashMenu section in the Feature interface
2023-11-24 01:18:55 +01:00
Alessio Gravili
62ca71fbc4 chore(richtext-lexical): breaking: slash menu: simplify, improve CSS class names, change 'title' in interface to key and displayName 2023-11-24 01:16:04 +01:00
Alessio Gravili
e50fa9ca8f feat(richtext-lexical): floating select toolbar: add ability to configure dropdown entry component 2023-11-23 23:52:16 +01:00
Alessio Gravili
ed7aca6525 chore(richtext-lexical): improve CSS class names of floating select toolbar 2023-11-23 23:38:25 +01:00
Zaki Nadhif
98ccd05dd6 chore(richtext-lexical): Add a hint that the slash menu exists to the user (#4206)
* chore(richtext-lexical): Add a hint that the slash menu exists to the user

* Update LexicalEditor.tsx

---------

Co-authored-by: Alessio Gravili <70709113+AlessioGr@users.noreply.github.com>
2023-11-23 22:28:02 +01:00
Alessio Gravili
051bced3b5 Merge pull request #4256 from payloadcms/chore/lexical-small-improvements
BREAKING: The first commit is breaking, as it changes the exported UnoderedListFeature to UnorderedListFeature due to a typo
2023-11-23 22:24:15 +01:00
Alessio Gravili
79f08baf2f chore(richtext-lexical): add lists to floating toolbar 2023-11-23 22:12:10 +01:00
Alessio Gravili
d6b63da617 docs(richtext-lexical): fix incorrect JSDOM import 2023-11-23 22:03:36 +01:00
Alessio Gravili
d512e9382d fix(richtext-lexical): breaking: fix typo: UnoderedListFeature => UnorderedListFeature 2023-11-23 21:39:07 +01:00
Alessio Gravili
b17cafc7be chore(richtext-lexical): add proper typing for node replacements (#4255) 2023-11-23 19:04:34 +01:00
Jacob Fletcher
24dacd6712 fix(live-preview): populates rich text relationships 2023-11-22 17:55:42 -05:00
Dan Ribbens
6ea29094ba fix(db-postgres): incorrect pagination totalDocs (#4248)
Co-authored-by: Alessio Gravili <alessio@bonfireleads.com>
2023-11-22 16:14:14 -05:00
Mark Barton
f27407ce7c chore: correct information from Nested Docs Plugin documentation (#4244) 2023-11-22 12:09:13 -05:00
stoddabr
9ae65fa791 docs: fix create-payload-app example (#4180) 2023-11-21 16:59:06 -05:00
Jessica Chowdhury
3d2b62b210 fix: passes date options to the react-datepicker in filter UI, removes duplicate options from operators select (#4225)
Co-authored-by: Jarrod Flesch <jarrodmflesch@gmail.com>
2023-11-21 16:48:39 -05:00
Jessica Chowdhury
6364afb1dd docs: updates vite alias example and fixes broken link (#4224) 2023-11-21 14:34:18 -05:00
Radosław Kłos
56a4692662 fix: typo in polish translations (#4234) 2023-11-21 13:09:00 -05:00
Dan Ribbens
ef6b8e4235 test: graphql handle deleted relationships (#4229) 2023-11-21 13:07:13 -05:00
Elliot DeNolf
5f5290341a chore(plugin-cloud-storage): adjust prepublishOnly script 2023-11-21 10:28:35 -05:00
Elliot DeNolf
62403584ad chore(scripts): cleanup package details log 2023-11-21 10:28:17 -05:00
Jarrod Flesch
19fcfc27af fix: number field validation (#4233) 2023-11-21 10:12:26 -05:00
Elliot DeNolf
dcf14f5f71 chore(release): payload/2.2.1 [skip ci] 2023-11-21 10:08:07 -05:00
Elliot DeNolf
3a784a06cc fix: make outputSchema optional on richtext config (#4230) 2023-11-21 09:45:57 -05:00
Jessica Chowdhury
6eeae9d53b examples: updates blank template readme (#4216) 2023-11-21 08:40:01 -05:00
Jarrod Flesch
6044f810bd docs: correct PULL_REQUEST_TEMPLATE.md links 2023-11-21 08:37:57 -05:00
Elliot DeNolf
e68ca9363f fix(plugin-cloud-storage): adjust webpack aliasing for pnpm (#4228) 2023-11-20 16:53:30 -05:00
Elliot DeNolf
9963b8d945 chore(release): plugin-nested-docs/1.0.9 [skip ci] 2023-11-20 16:40:46 -05:00
Elliot DeNolf
9afb838182 chore(release): richtext-slate/1.2.0 [skip ci] 2023-11-20 16:39:38 -05:00
Elliot DeNolf
2dad129022 chore(release): richtext-lexical/0.2.0 [skip ci] 2023-11-20 16:39:20 -05:00
Elliot DeNolf
6af1c4d45d chore(release): payload/2.2.0 [skip ci] 2023-11-20 16:36:41 -05:00
Dan Ribbens
4e41dd1bf2 fix(plugin-nested-docs): await populate breadcrumbs on resaveChildren (#4226) 2023-11-20 16:32:02 -05:00
Dan Ribbens
de02490231 feat: hide publish button based on permissions (#4203)
Co-authored-by: James <james@trbl.design>
2023-11-20 16:26:49 -05:00
Jacob Fletcher
8a7b41721a chore: increases live preview testing coverage 2023-11-20 12:58:27 -05:00
Take Weiland
1510baf46e fix: synchronous transaction errors (#4164)
Co-authored-by: Dan Ribbens <dan.ribbens@gmail.com>
2023-11-20 12:20:42 -05:00
Jacob Fletcher
0672e864f3 chore: resolves imports and type errors in live preview test app 2023-11-20 11:30:05 -05:00
Alessio Gravili
c10db332cd docs(richtext-lexical): remove unnecessary await from createHeadlessEditor (#4213) 2023-11-19 14:40:09 +01:00
Alessio Gravili
0af9c4d398 fix(richtext-lexical): Blocks: Array row data is not removed (#4209)
* chore(richtext-lexical): Add failing test which reproduces issue

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

* chore: remove redundant hook

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

* cleanup everything

* chore: more cleanup

* debug

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

* chore: fix e2e tests

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

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

* chore: fix incorrect insert block commands in drawer

* chore: add new e2e test

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

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

* chore: rename hintPaginationCountIndex to disablePaginationCountIndexHint

* chore: fix logic

* chore: disablePaginationCountIndexHint => disableIndexHints
2023-11-10 16:08:14 +01:00
Jessica Chowdhury
949e265cd9 fix: disable editing option for svg image types (#4071)
Co-authored-by: Jarrod Flesch <jarrodmflesch@gmail.com>
2023-11-10 00:35:04 -05:00
Jessica Chowdhury
687f4850ac fix: hide empty image sizes from the preview drawer (#3946)
Co-authored-by: Jarrod Flesch <jarrodmflesch@gmail.com>
2023-11-09 22:34:50 -05:00
Jacob Fletcher
1f851f21b1 fix(live-preview): properly handles apiRoute (#4076) 2023-11-09 13:14:41 -05:00
Jarrod Flesch
dbc4ce71e6 chore: fixes test suites that use clearAndSeedEverything (#4080) 2023-11-09 12:30:19 -05:00
Patrik
cef4cbb0ee fix: conditionally hide dot menu in DocumentControls (#4075) 2023-11-09 12:01:58 -05:00
Jacob Fletcher
7059a71243 fix(live-preview): ensures field schema exists before traversing fields (#4074) 2023-11-09 10:08:10 -05:00
Jacob Fletcher
01559ef34b chore: prevents field validation from triggering unnecessary re-renders (#4066) 2023-11-09 09:46:35 -05:00
Jacob Fletcher
8488f7b8db docs: adds apiRoute to useLivePreview args (#4073) 2023-11-09 09:01:04 -05:00
Michał Korczak
a92a160a13 docs: fix link to public demo example config (#4007) 2023-11-09 00:23:58 -05:00
PatrikKozak
77a7c83251 chore: updates default type value for graphql playground type 2023-11-08 18:30:55 -05:00
Jacob Fletcher
2ad7340154 fix(live-preview): field recursion and relationship population (#4045) 2023-11-08 17:28:35 -05:00
Alessio Gravili
c462df38f6 fix(richtext-lexical): floating select toolbar caret not positioned correctly if first line is selected (#4062) 2023-11-08 22:13:38 +01:00
Alessio Gravili
fff377ad22 fix(richtext-lexical): Blocks: unnecessary saving node value when initially opening a document & new lexical tests (#4059)
* chore: new lexical int tests and working test structure

* chore: more int tests, and better lexical collection structure

* fix(richtext-lexical): Blocks: unnecessary saving node value when initially opening a document
2023-11-08 21:32:43 +01:00
Elliot DeNolf
a2cb946155 chore(release): bundler-vite/0.1.4 [skip ci] 2023-11-08 14:54:50 -05:00
496 changed files with 14782 additions and 11931 deletions

View File

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

7
.vscode/launch.json vendored
View File

@@ -64,6 +64,13 @@
"NODE_ENV": "production"
}
},
{
"command": "pnpm run test:int live-preview",
"cwd": "${workspaceFolder}",
"name": "Live Preview Integration",
"request": "launch",
"type": "node-terminal"
},
{
"command": "ts-node ./packages/payload/src/bin/index.ts build",
"env": {

View File

@@ -1,3 +1,212 @@
## [2.4.0](https://github.com/payloadcms/payload/compare/v2.3.1...v2.4.0) (2023-12-06)
### Features
* add Chinese Traditional translation ([#4372](https://github.com/payloadcms/payload/issues/4372)) ([50253f6](https://github.com/payloadcms/payload/commit/50253f617c22d0d185bbac7f9d4304cddbc01f06))
* async live preview urls ([#4339](https://github.com/payloadcms/payload/issues/4339)) ([5f17324](https://github.com/payloadcms/payload/commit/5f173241df6dc316d498767b1c81718e9c2b9a51))
* pass path to FieldDescription ([#4364](https://github.com/payloadcms/payload/issues/4364)) ([3b8a27d](https://github.com/payloadcms/payload/commit/3b8a27d199b3969cbca6ca750450798cb70f21e8))
* **richtext-lexical:** lazy import React components to prevent client-only code from leaking into the server ([#4290](https://github.com/payloadcms/payload/issues/4290)) ([5de347f](https://github.com/payloadcms/payload/commit/5de347ffffca3bf38315d3d87d2ccc5c28cd2723))
* **richtext-lexical:** Link & Relationship Feature: field-level configurable allowed relationships ([#4182](https://github.com/payloadcms/payload/issues/4182)) ([7af8f29](https://github.com/payloadcms/payload/commit/7af8f29b4a8dddf389356e4db142f8d434cdc964))
### Bug Fixes
* **db-postgres:** sorting on a not-configured field throws error ([#4382](https://github.com/payloadcms/payload/issues/4382)) ([dbaecda](https://github.com/payloadcms/payload/commit/dbaecda0e92fcb0fa67b4c5ac085e025f02de53a))
* defaultValues computed on new globals ([#4380](https://github.com/payloadcms/payload/issues/4380)) ([b6cffce](https://github.com/payloadcms/payload/commit/b6cffcea07b9fa21698b00b8bbed6f27197ded41))
* handles null upload field values ([#4397](https://github.com/payloadcms/payload/issues/4397)) ([cf9a370](https://github.com/payloadcms/payload/commit/cf9a3704df21ce8b32feb0680793cba804cd66f7))
* **live-preview:** populates rte uploads and relationships ([#4379](https://github.com/payloadcms/payload/issues/4379)) ([4090aeb](https://github.com/payloadcms/payload/commit/4090aebb0e94e776258f0c1c761044a4744a1857))
* **live-preview:** sends raw js objects through window.postMessage instead of json ([#4354](https://github.com/payloadcms/payload/issues/4354)) ([03a3872](https://github.com/payloadcms/payload/commit/03a387233d1b8876a2fcaa5f3b3fd5ed512c0bc4))
* simplifies query validation and fixes nested relationship fields ([#4391](https://github.com/payloadcms/payload/issues/4391)) ([4b5453e](https://github.com/payloadcms/payload/commit/4b5453e8e5484f7afcadbf5bccf8369b552969c6))
* upload editing error with plugin-cloud ([#4170](https://github.com/payloadcms/payload/issues/4170)) ([fcbe574](https://github.com/payloadcms/payload/commit/fcbe5744d945dc43642cdaa2007ddc252ecafafa))
* uploads files after validation ([#4218](https://github.com/payloadcms/payload/issues/4218)) ([65adfd2](https://github.com/payloadcms/payload/commit/65adfd21ed538b79628dc4f8ce9e1a5a1bba6aed))
### ⚠ BREAKING CHANGES
* **richtext-lexical:** lazy import React components to prevent client-only code from leaking into the server (#4290)
### ⚠️ @payloadcms/richtext-lexical
Most important: If you are updating `@payloadcms/richtext-lexical` to v0.4.0 or higher, you will HAVE to update `payload` to the latest version as well. If you don't update it, payload likely won't start up due to validation errors. It's generally good practice to upgrade packages prefixed with `@payloadcms/` together with `payload` and keep the versions in sync.
`@payloadcms/richtext-slate` is not affected by this.
Every single property in the `Feature` interface which accepts a React component now no longer accepts a React component, but a function which imports a React component instead. This is done to ensure no unnecessary client-only code is leaked to the server when importing Features on a server.
Here's an example migration:
Old:
```ts
import { BlockIcon } from '../../lexical/ui/icons/Block'
...
Icon: BlockIcon,
```
New:
```ts
// import { BlockIcon } from '../../lexical/ui/icons/Block' // <= Remove this import
...
Icon: () =>
// @ts-expect-error
import('../../lexical/ui/icons/Block').then((module) => module.BlockIcon),
```
Or alternatively, if you're using default exports instead of named exports:
```ts
// import BlockIcon from '../../lexical/ui/icons/Block' // <= Remove this import
...
Icon: () =>
// @ts-expect-error
import('../../lexical/ui/icons/Block'),
```
The types for `SanitizedEditorConfig` and `EditorConfig` have changed. Their respective `lexical` property no longer expects the `LexicalEditorConfig`. It now expects a function returning the `LexicalEditorConfig`. You will have to adjust this if you adjusted that property anywhere, e.g. when initializing the lexical field editor property, or when initializing a new headless editor.
The following exports are now exported from the `@payloadcms/richtext-lexical/components` subpath exports instead of `@payloadcms/richtext-lexical`:
- ToolbarButton
- ToolbarDropdown
- RichTextCell
- RichTextField
- defaultEditorLexicalConfig
You will have to adjust your imports, only if you import any of those properties in your project.
## [2.3.1](https://github.com/payloadcms/payload/compare/v2.3.0...v2.3.1) (2023-12-01)
### Bug Fixes
* ensure doc controls are not hidden behind lexical field ([#4345](https://github.com/payloadcms/payload/issues/4345)) ([bea79fe](https://github.com/payloadcms/payload/commit/bea79feaeaee18bf94dd04262f134483f1468494))
* query validation on relationship fields ([#4353](https://github.com/payloadcms/payload/issues/4353)) ([fe888b5](https://github.com/payloadcms/payload/commit/fe888b5f6ceaa3969eac759cbdfb109b106dae05))
* **richtext-lexical:** blocks content may be hidden behind components outside of the editor ([#4325](https://github.com/payloadcms/payload/issues/4325)) ([3e745e9](https://github.com/payloadcms/payload/commit/3e745e91da620a00e3f0f91892ee3ec66ba72bc0))
* **richtext-lexical:** Blocks node: incorrect conversion from v1 node to v2 node ([ef84a2c](https://github.com/payloadcms/payload/commit/ef84a2cfffbb1be52dd948e59eeec0ce324e9046))
## [2.3.0](https://github.com/payloadcms/payload/compare/v2.2.2...v2.3.0) (2023-11-30)
### Features
* add serbian (latin and cyrillic) translations ([#4268](https://github.com/payloadcms/payload/issues/4268)) ([40c8909](https://github.com/payloadcms/payload/commit/40c8909ee0003d45a1afa4524ade557268d01067))
* **db-mongodb:** search for migrations dir intelligently ([530c825](https://github.com/payloadcms/payload/commit/530c825f806708df8672e66c8e9c559c5e625e5e))
* **db-postgres:** search for migrations dir intelligently ([308979f](https://github.com/payloadcms/payload/commit/308979f31d27979955a52f32be4ea33849b48f30))
* **live-preview:** batches api requests ([#4315](https://github.com/payloadcms/payload/issues/4315)) ([d49bb43](https://github.com/payloadcms/payload/commit/d49bb4351f22f17f68477c3145594abbb60fab05))
* relationship sortOptions property ([#4301](https://github.com/payloadcms/payload/issues/4301)) ([224cddd](https://github.com/payloadcms/payload/commit/224cddd04573eff578ea3fa9ea5419f28b66c613))
* support migrations with js files ([2122242](https://github.com/payloadcms/payload/commit/21222421929ae19728c31bdccc84995ce3951224))
* support OAuth 2.0 format Authorization: Bearer tokens in headers ([c1eb9d1](https://github.com/payloadcms/payload/commit/c1eb9d1727daf96375e73943882621127b78e593))
* useDocumentEvents ([#4284](https://github.com/payloadcms/payload/issues/4284)) ([9bb7a88](https://github.com/payloadcms/payload/commit/9bb7a88526569a726de468de6b2010d52169ea77))
### Bug Fixes
* **db-postgres:** allow for nested block fields to be queried ([#4237](https://github.com/payloadcms/payload/issues/4237)) ([cd07873](https://github.com/payloadcms/payload/commit/cd07873fc544766b4aeeff873dfb8d6e3e97e9dc))
* **db-postgres:** error saving nested arrays with versions ([#4302](https://github.com/payloadcms/payload/issues/4302)) ([3514bfb](https://github.com/payloadcms/payload/commit/3514bfbdaee99341ae739d03591cb63bd9415fe3))
* duplicate documents with required localized fields ([#4236](https://github.com/payloadcms/payload/issues/4236)) ([9da9b1f](https://github.com/payloadcms/payload/commit/9da9b1fc5050d4f29bcf6dce2f22027834aaf698))
* incorrect key property in Tabs field component ([#4311](https://github.com/payloadcms/payload/issues/4311)) ([3502ce7](https://github.com/payloadcms/payload/commit/3502ce720b3020eed5fc733884b525303faa4c15)), closes [#4282](https://github.com/payloadcms/payload/issues/4282)
* **live-preview-react:** removes payload from peer deps ([7e1052f](https://github.com/payloadcms/payload/commit/7e1052fd98c88a4d68af08f98ccc8936edb8ebf6))
* **live-preview:** compounds merge results ([#4306](https://github.com/payloadcms/payload/issues/4306)) ([381c158](https://github.com/payloadcms/payload/commit/381c158b0303b515164ae487b0ce7e555ae1a08d))
* **live-preview:** property resets rte nodes ([#4313](https://github.com/payloadcms/payload/issues/4313)) ([66679fb](https://github.com/payloadcms/payload/commit/66679fbdd6f804bff8a58d9504c226c9fb8a57a0))
* **live-preview:** re-populates externally updated relationships ([#4287](https://github.com/payloadcms/payload/issues/4287)) ([57fc211](https://github.com/payloadcms/payload/commit/57fc2116749059bc55161897cf139031926035ec))
* **live-preview:** removes payload from peer deps ([b4af95f](https://github.com/payloadcms/payload/commit/b4af95f894b5f6614bace38ef79e7148e084bd3b))
* properly exports useDocumentsEvents hook ([#4314](https://github.com/payloadcms/payload/issues/4314)) ([5420963](https://github.com/payloadcms/payload/commit/542096361f0c13aed9c6a7d971e2c47047d8e2d2))
* properly sets tabs key in fieldSchemaToJSON ([#4317](https://github.com/payloadcms/payload/issues/4317)) ([9cc88bb](https://github.com/payloadcms/payload/commit/9cc88bb47443ecdf525f4c99d9f13d81c141c471))
* **richtext-lexical:** HTML Converter field not working inside of tabs ([#4293](https://github.com/payloadcms/payload/issues/4293)) ([5bf6493](https://github.com/payloadcms/payload/commit/5bf64933b4b99a0ac8ef7d1d91d0165a16636a9f))
* **richtext-lexical:** re-use payload population logic to fix population-related issues ([#4291](https://github.com/payloadcms/payload/issues/4291)) ([094d02c](https://github.com/payloadcms/payload/commit/094d02ce1d85106470a1a8c6ffe9050873f2e57a))
* **richtext-slate:** add use client to top of tsx files importing from payload core ([#4312](https://github.com/payloadcms/payload/issues/4312)) ([d4f28b1](https://github.com/payloadcms/payload/commit/d4f28b16b4d42f224e9c5e4254f9ec55107a2f97))
### BREAKING CHANGES
### ⚠️ @payloadcms/richtext-lexical
The `SlashMenuGroup` and `SlashMenuOption` classes have changed. If you have any custom lexical Features which are adding new slash menu entries, this will be a breaking change for you. If not, no action is required from your side.
Here are the breaking changes and how to migrate:
1. The `SlashMenuOption`'s first argument is now used as a `key` and not as a display name. Additionally, a new, optional `displayName` property is added which will serve as the display name. Make sure your `key` does not contain any spaces or special characters.
2. The `title` property of `SlashMenuGroup` has been replaced by a new, mandatory `key` and an optional `displayName` property. To migrate, you will have to remove the `title` property and add a `key` property instead - make sure you do not use spaces or special characters in the `key`.
3. Additionally, if you have custom styles targeting elements inside of slash or floating-select-toolbar menus, you will have to adjust those too, as the CSS classes changed
[This is an example of performing these updates](
https://github.com/payloadcms/payload/pull/4257/files#diff-dc2e7f503dd7076dff1d810da7ec77b8fc6a9e41127df4a417dece1b6e1587a0L61)
## [2.2.2](https://github.com/payloadcms/payload/compare/v2.2.1...v2.2.2) (2023-11-27)
### Features
* **i18n:** adds Korean translation ([#4258](https://github.com/payloadcms/payload/issues/4258)) ([1401718](https://github.com/payloadcms/payload/commit/1401718b3b549ce1454389a982474dbe159eb61f))
### Bug Fixes
* number field validation ([#4233](https://github.com/payloadcms/payload/issues/4233)) ([19fcfc2](https://github.com/payloadcms/payload/commit/19fcfc27af2ecb68ff989dcaed19b7b7d041a322))
* passes date options to the react-datepicker in filter UI, removes duplicate options from operators select ([#4225](https://github.com/payloadcms/payload/issues/4225)) ([3d2b62b](https://github.com/payloadcms/payload/commit/3d2b62b2100e36a54adc6a675257a4d671fdd469))
* prevent json data getting reset when switching tabs ([#4123](https://github.com/payloadcms/payload/issues/4123)) ([1dcd3a2](https://github.com/payloadcms/payload/commit/1dcd3a27825ed9d276b997a66f84bb2c05e87955))
* transactions broken within doc access ([443847e](https://github.com/payloadcms/payload/commit/443847ec716a3b87032d9d1904b6c90aadd47197))
* typo in polish translations ([#4234](https://github.com/payloadcms/payload/issues/4234)) ([56a4692](https://github.com/payloadcms/payload/commit/56a469266207ef83053b0c9176d1be4fc26087e6))
## [2.2.1](https://github.com/payloadcms/payload/compare/v2.2.0...v2.2.1) (2023-11-21)
### Bug Fixes
* make outputSchema optional on richtext config ([#4230](https://github.com/payloadcms/payload/issues/4230)) ([3a784a0](https://github.com/payloadcms/payload/commit/3a784a06cc6c42c96b8d6cf023d942e6661be7b5))
## [2.2.0](https://github.com/payloadcms/payload/compare/v2.1.1...v2.2.0) (2023-11-20)
### Features
* allow richtext adapters to control type generation, improve generated lexical types ([#4036](https://github.com/payloadcms/payload/issues/4036)) ([989c10e](https://github.com/payloadcms/payload/commit/989c10e0e0b36a8c34822263b19f5cb4b9ed6e72))
* hide publish button based on permissions ([#4203](https://github.com/payloadcms/payload/issues/4203)) ([de02490](https://github.com/payloadcms/payload/commit/de02490231fbc8936973c1b81ac87add39878d8b))
* **richtext-lexical:** Add new position: 'top' property for plugins ([eed4f43](https://github.com/payloadcms/payload/commit/eed4f4361cd012adf4e777820adbe7ad330ffef6))
### Bug Fixes
* fully define the define property for esbuild string replacement ([#4099](https://github.com/payloadcms/payload/issues/4099)) ([e22b95b](https://github.com/payloadcms/payload/commit/e22b95bdf3b2911ae67a07a76ec109c76416ea56))
* **i18n:** polish translations ([#4134](https://github.com/payloadcms/payload/issues/4134)) ([782e118](https://github.com/payloadcms/payload/commit/782e1185698abb2fff3556052fd16d2b725611b9))
* improves live preview breakpoints and zoom options in dark mode ([#4090](https://github.com/payloadcms/payload/issues/4090)) ([b91711a](https://github.com/payloadcms/payload/commit/b91711a74ad9379ed820b6675060209626b1c2d0))
* **plugin-nested-docs:** await populate breadcrumbs on resaveChildren ([#4226](https://github.com/payloadcms/payload/issues/4226)) ([4e41dd1](https://github.com/payloadcms/payload/commit/4e41dd1bf2706001fa03130adb1c69403795ac96))
* rename tab button classname to prevent unintentional styling ([#4121](https://github.com/payloadcms/payload/issues/4121)) ([967eff1](https://github.com/payloadcms/payload/commit/967eff1aabcc9ba7f29573fc2706538d691edfdd))
* **richtext-lexical:** add missing 'use client' to TestRecorder feature plugin ([fc26275](https://github.com/payloadcms/payload/commit/fc26275b7a85fd34f424f7693b8383ad4efe0121))
* **richtext-lexical:** Blocks: Array row data is not removed ([#4209](https://github.com/payloadcms/payload/issues/4209)) ([0af9c4d](https://github.com/payloadcms/payload/commit/0af9c4d3985a6c46a071ef5ac28c8359cb320571))
* **richtext-lexical:** Blocks: fields without fulfilled condition are now skipped for validation ([50fab90](https://github.com/payloadcms/payload/commit/50fab902bd7baa1702ae0d995b4f58c1f5fca374))
* **richtext-lexical:** Blocks: make sure fields are wrapped in a uniquely-named group, change block node data format, fix react key error ([#3995](https://github.com/payloadcms/payload/issues/3995)) ([c068a87](https://github.com/payloadcms/payload/commit/c068a8784ec5780dbdca5416b25ba654afd05458))
* **richtext-lexical:** Blocks: z-index issue, e.g. select field dropdown in blocks hidden behind blocks below, or slash menu inside nested editor hidden behind blocks below ([09f17f4](https://github.com/payloadcms/payload/commit/09f17f44508539cfcb8722f7f462ef40d9ed54fd))
* **richtext-lexical:** Floating Select Toolbar: Buttons and Dropdown Buttons not clickable in nested editors ([615702b](https://github.com/payloadcms/payload/commit/615702b858e76994a174159cb69f034ef811e016)), closes [#4025](https://github.com/payloadcms/payload/issues/4025)
* **richtext-lexical:** HTMLConverter: cannot find nested lexical fields ([#4103](https://github.com/payloadcms/payload/issues/4103)) ([a6d5f2e](https://github.com/payloadcms/payload/commit/a6d5f2e3dea178e1fbde90c0d6a5ce254a8db0d1)), closes [#4034](https://github.com/payloadcms/payload/issues/4034)
* **richtext-lexical:** incorrect caret positioning when selecting second line of multi-line paragraph ([#4165](https://github.com/payloadcms/payload/issues/4165)) ([b210af4](https://github.com/payloadcms/payload/commit/b210af46968b77d96ffd6ef60adc3b8d8bdc9376))
* **richtext-lexical:** make lexicalHTML() function work for globals ([dbfc835](https://github.com/payloadcms/payload/commit/dbfc83520ca8b5e55198a3c4b517ae3a80f9cac6))
* **richtext-lexical:** nested editor may lose focus when writing ([#4139](https://github.com/payloadcms/payload/issues/4139)) ([859c2f4](https://github.com/payloadcms/payload/commit/859c2f4a6d299a42e572133502b3841a74a11002))
* **richtext-lexical:** remove optional chaining after `this` as transpilers are not handling it well ([#4145](https://github.com/payloadcms/payload/issues/4145)) ([2c8d34d](https://github.com/payloadcms/payload/commit/2c8d34d2aadf2fcaf0655c0abef233f341d9945f))
* **richtext-lexical:** visual bug after rearranging blocks ([a6b4860](https://github.com/payloadcms/payload/commit/a6b486007dc26195adc5d576d937e35471c2868f))
* simplifies block/array/hasMany-number field validations ([#4052](https://github.com/payloadcms/payload/issues/4052)) ([803a37e](https://github.com/payloadcms/payload/commit/803a37eaa947397fa0a93b9f4f7d702c6b94ceaa))
* synchronous transaction errors ([#4164](https://github.com/payloadcms/payload/issues/4164)) ([1510baf](https://github.com/payloadcms/payload/commit/1510baf46e33540c72784f2d3f98330a8ff90923))
* thread locale through to access routes from admin panel ([#4183](https://github.com/payloadcms/payload/issues/4183)) ([05f3169](https://github.com/payloadcms/payload/commit/05f3169a75b3b62962e7fe7842fbb6df6699433d))
* transactionID isolation for GraphQL ([#4095](https://github.com/payloadcms/payload/issues/4095)) ([195a952](https://github.com/payloadcms/payload/commit/195a952c4314e0d53fd579517035373b49d6ccae))
* upload fit not accounted for when editing focal point or crop ([#4142](https://github.com/payloadcms/payload/issues/4142)) ([45e9a55](https://github.com/payloadcms/payload/commit/45e9a559bbb16b2171465c8a439044011cebf102))
## [2.1.1](https://github.com/payloadcms/payload/compare/v2.1.0...v2.1.1) (2023-11-10)
### Bug Fixes
* conditionally hide dot menu in DocumentControls ([#4075](https://github.com/payloadcms/payload/issues/4075)) ([cef4cbb](https://github.com/payloadcms/payload/commit/cef4cbb0ee59e1b0b806808d79b402dce114755f))
* disable editing option for svg image types ([#4071](https://github.com/payloadcms/payload/issues/4071)) ([949e265](https://github.com/payloadcms/payload/commit/949e265cd9c95b7d4063336dde86177008d54839))
* fixes creation of related documents within a transaction if filterOptions is used ([#4087](https://github.com/payloadcms/payload/issues/4087)) ([acad288](https://github.com/payloadcms/payload/commit/acad2888cd9a13d5fb9e4c686b2267ea69454eaf))
* hide empty image sizes from the preview drawer ([#3946](https://github.com/payloadcms/payload/issues/3946)) ([687f485](https://github.com/payloadcms/payload/commit/687f4850acf073df0a649ef6182bfc8387857173))
* **live-preview:** ensures field schema exists before traversing fields ([#4074](https://github.com/payloadcms/payload/issues/4074)) ([7059a71](https://github.com/payloadcms/payload/commit/7059a71243a8f98dcc89af0bfe502247db9e4123))
* **live-preview:** field recursion and relationship population ([#4045](https://github.com/payloadcms/payload/issues/4045)) ([2ad7340](https://github.com/payloadcms/payload/commit/2ad73401546ef6608fd67d1f00b537f149640d6a))
* **live-preview:** properly handles apiRoute ([#4076](https://github.com/payloadcms/payload/issues/4076)) ([1f851f2](https://github.com/payloadcms/payload/commit/1f851f21b18c9a5076d9afc9a31abc7a97fcb0df))
* **plugin-nested-docs:** sync write transaction errors ([#4084](https://github.com/payloadcms/payload/issues/4084)) ([47efd3b](https://github.com/payloadcms/payload/commit/47efd3b92e99594dd5b61f0017f4eb76e1d36eb7))
* possible issue with access control not using req ([#4086](https://github.com/payloadcms/payload/issues/4086)) ([348a70c](https://github.com/payloadcms/payload/commit/348a70cc33409b0b48aff3acd2b94c2df5d88f3b))
* **richtext-lexical:** Blocks: unnecessary saving node value when initially opening a document & new lexical tests ([#4059](https://github.com/payloadcms/payload/issues/4059)) ([fff377a](https://github.com/payloadcms/payload/commit/fff377ad22cce3b26142cde8f4125fcee95aa072))
* **richtext-lexical:** floating select toolbar caret not positioned correctly if first line is selected ([#4062](https://github.com/payloadcms/payload/issues/4062)) ([c462df3](https://github.com/payloadcms/payload/commit/c462df38f65b155e131e6a7b46b2bb16cd090e45))
## [2.1.0](https://github.com/payloadcms/payload/compare/v2.0.15...v2.1.0) (2023-11-08)

View File

@@ -14,7 +14,7 @@ If you find a vulnerability within the core Payload repository, and we determine
## Documentation edits
Payload documentation can be found directly within its codebase and you can feel free to make changes / improvements to any of it through opening a PR. We utilize these files directly in our website and will periodically deploy documentation updates as necessary.
Payload documentation can be found directly within its codebase, and you can feel free to make changes / improvements to any of it through opening a PR. We utilize these files directly in our website and will periodically deploy documentation updates as necessary.
## Building additional features
@@ -30,9 +30,17 @@ Our design review ensures that proposed changes fit seamlessly with other compon
To help us work on new features, you can create a new feature request post in [GitHub Discussion](https://github.com/payloadcms/payload/discussions) or discuss it in our [Discord](https://discord.com/invite/payload). New functionality often has large implications across the entire Payload repo, so it is best to discuss the architecture and approach before starting work on a pull request.
### Installation & Requirements
Payload is structured as a Monorepo, encompassing not only the core Payload platform but also various plugins and packages. To install all required dependencies, you have to run `pnpm install` once in the root directory. **PNPM IS REQUIRED!** Yarn or npm will not work - you will have to use pnpm to develop in the core repository. In most systems, the easiest way to install pnpm is to run `corepack enable` in your terminal.
If you're coming from a very outdated version of payload, it is recommended to nuke the node_modules folder before running pnpm install. On UNIX systems, you can easily do that using the `pnpm clean:unix` command, which will delete all node_modules folders and build artefacts.
It is also recommended to use at least Node v18 or higher. You can check your current node version by typing `node --version` in your terminal. The easiest way to switch between different node versions is to use [nvm](https://github.com/nvm-sh/nvm#intro).
### Code
Most new functionality should keep testing in mind. With 1.0, testability of new features has been vastly improved. All top-level directories within the `test/` directory are for testing a specific category: `fields`, `collections`, etc.
Most new functionality should keep testing in mind. All top-level directories within the `test/` directory are for testing a specific category: `fields`, `collections`, etc.
If it makes sense to add your feature to an existing test directory, please do so.
@@ -49,21 +57,35 @@ A typical directory with `test/` will be structured like this:
- `config.ts` - This is the _granular_ Payload config for testing. It should be as lightweight as possible. Reference existing configs for an example
- `int.spec.ts` - This is the test file run by jest. Any test file must have a `*int.spec.ts` suffix.
- `e2e.spec.ts` - This is the end-to-end test file that will load up the admin UI using the above config and run Playwright tests. These tests are typically only needed if a large change is being made to the Admin UI.
- `payload-types.ts` - Generated types from `config.ts`. Generate this file by running `pnpm dev:generate-types my-test-dir`.
- `payload-types.ts` - Generated types from `config.ts`. Generate this file by running `pnpm dev:generate-types my-test-dir`. Replace `my-test-dir` with the name of your testing directory.
The directory split up in this way specifically to reduce friction when creating tests and to add the ability to boot up Payload with that specific config.
Each test directory is split up in this way specifically to reduce friction when creating tests and to add the ability to boot up Payload with that specific config.
The following command will start Payload with your config: `pnpm dev my-test-dir`. This command will start up Payload using your config and refresh a test database on every restart.
The following command will start Payload with your config: `pnpm dev my-test-dir`. Example: `pnpm dev fields` for the test/`fields` test suite. This command will start up Payload using your config and refresh a test database on every restart. If you're using VS Code, the most common run configs are automatically added to your editor - you should be able to find them in your VS Code launch tab.
By default, it will automatically log you in with the default credentials. To disable that, you can either pass in the --no-auto-login flag (example: `pnpm dev my-test-dir --no-auto-login`) or set the `PAYLOAD_PUBLIC_DISABLE_AUTO_LOGIN` environment variable to `false`.
By default, payload will [automatically log you in](https://payloadcms.com/docs/authentication/config#admin-autologin) with the default credentials. To disable that, you can either pass in the --no-auto-login flag (example: `pnpm dev my-test-dir --no-auto-login`) or set the `PAYLOAD_PUBLIC_DISABLE_AUTO_LOGIN` environment variable to `false`.
If you wish to use to your own Mongo database for the `test` directory instead of using the in memory database, all you need to do is add the following env vars to the `test/dev.ts` file:
The default credentials are `dev@payloadcms.com` as E-Mail and `test` as password. These are used in the auto-login.
### Testing with your own MongoDB database
If you wish to use your own MongoDB database for the `test` directory instead of using the in memory database, all you need to do is add the following env vars to the `test/dev.ts` file:
- `process.env.NODE_ENV`
- `process.env.PAYLOAD_TEST_MONGO_URL`
- Simply set `process.env.NODE_ENV` to `test` and set `process.env.PAYLOAD_TEST_MONGO_URL` to your mongo url e.g. `mongodb://127.0.0.1/your-test-db`.
- Simply set `process.env.NODE_ENV` to `test` and set `process.env.PAYLOAD_TEST_MONGO_URL` to your MongoDB URL e.g. `mongodb://127.0.0.1/your-test-db`.
NOTE: It is recommended to add the test credentials (located in `test/credentials.ts`) to your autofill for `localhost:3000/admin` as this will be required on every nodemon restart. The default credentials are `dev@payloadcms.com` as E-Mail and `test` as password.
### Using Postgres
If you have postgres installed on your system, you can also run the test suites using postgres. By default, mongodb is used.
To do that, simply set the `PAYLOAD_DATABASE` environment variable to `postgres`.
### Running the e2e and int tests
You can run the entire test suite using `pnpm test`. If you wish to only run e2e tests, you can use `pnpm test:e2e`. If you wish to only run int tests, you can use `pnpm test:int`.
By default, `pnpm test:int` will only run int test against MongoDB. To run int tests against postgres, you can use `pnpm test:int:postgres`. You will have to have postgres installed on your system for this to work.
### Commits

View File

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

View File

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

View File

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

View File

@@ -764,7 +764,7 @@ Returns methods to set and get user preferences. More info can be found [here](h
Returns methods to manipulate table columns
```tsx
import { useTableColumns } from 'payload/components/utilities'
import { useTableColumns } from 'payload/components/hooks'
const MyComponent: React.FC = () => {
// highlight-start
@@ -784,3 +784,33 @@ const MyComponent: React.FC = () => {
</button>
)
}
```
### useDocumentEvents
The `useDocumentEvents` hook provides a way of subscribing to cross-document events, such as updates made to nested documents within a drawer. This hook will report document events that are outside the scope of the document currently being edited. This hook provides the following:
| Property | Description |
|---------------------------|-------------------------------------------------------------------------------------------------------------------------------------------|
| **`mostRecentUpdate`** | An object containing the most recently updated document. It contains the `entitySlug`, `id` (if collection), and `updatedAt` properties |
| **`reportUpdate`** | A method used to report updates to documents. It accepts the same arguments as the `mostRecentUpdate` property. |
**Example:**
```tsx
import { useDocumentEvents } from 'payload/components/hooks'
const ListenForUpdates: React.FC = () => {
const { mostRecentUpdate } = useDocumentEvents()
return (
<span>
{JSON.stringify(mostRecentUpdate)}
</span>
)
}
```
<Banner type="info">
Right now the `useDocumentEvents` hook only tracks recently updated documents, but in the future it will track more document-related events as needed, such as document creation, deletion, etc.
</Banner>

View File

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

View File

@@ -82,7 +82,7 @@ Once enabled, each document that is created within the Collection can be thought
### Token-based auth
Successfully logging in returns a `JWT` (JSON web token) which is how a user will identify themselves to Payload. By providing this JWT via either an HTTP-only cookie or an `Authorization` header, Payload will automatically identify the user and add its user JWT data to the Express `req`, which is available throughout Payload including within access control, hooks, and more.
Successfully logging in returns a `JWT` (JSON web token) which is how a user will identify themselves to Payload. By providing this JWT via either an HTTP-only cookie or an `Authorization: JWT` or `Authorization: Bearer` header, Payload will automatically identify the user and add its user JWT data to the Express `req`, which is available throughout Payload including within access control, hooks, and more.
You can specify what data gets encoded to the JWT token by setting `saveToJWT` to true in your auth collection fields. If you wish to use a different key other than the field `name`, you can provide it to `saveToJWT` as a string. It is also possible to use `saveToJWT` on fields that are nested in inside groups and tabs. If a group has a `saveToJWT` set it will include the object with all sub-fields in the token. You can set `saveToJWT: false` for any fields you wish to omit. If a field inside a group has `saveToJWT` set, but the group does not, the field will be included at the top level of the token.

View File

@@ -59,7 +59,7 @@ export const Orders: CollectionConfig = {
#### More collection config examples
You can find an assortment
of [example collection configs](https://github.com/payloadcms/public-demo/tree/master/src/collections) in the Public
of [example collection configs](https://github.com/payloadcms/public-demo/tree/master/src/payload/collections) in the Public
Demo source code on GitHub.
### Admin options

View File

@@ -59,7 +59,7 @@ export default Nav
#### Global config example
You can find an [example Global config](https://github.com/payloadcms/public-demo/blob/master/src/globals/MainMenu.ts) in the Public Demo source code on GitHub.
You can find a few [example Global configs](https://github.com/payloadcms/public-demo/tree/master/src/payload/globals) in the Public Demo source code on GitHub.
### Admin options

View File

@@ -108,7 +108,7 @@ export default buildConfig({
#### Full example config
You can see a full [example config](https://github.com/payloadcms/public-demo/blob/master/src/payload.config.ts) in the Public Demo source code on GitHub.
You can see a full [example config](https://github.com/payloadcms/public-demo/blob/master/src/payload/payload.config.ts) in the Public Demo source code on GitHub.
### Using environment variables in your config

View File

@@ -46,8 +46,14 @@ export async function down({ payload }: MigrateDownArgs): Promise<void> {
};
```
### Migrations Directory
Each DB adapter has an optional property `migrationDir` where you can override where you want your migrations to be stored/read. If this is not specified, Payload will check the default and possibly make a best effort to find your migrations directory by searching in common locations ie. `./src/migrations`, `./dist/migrations`, `./migrations`, etc.
All database adapters should implement similar migration patterns, but there will be small differences based on the adapter and its specific needs. Below is a list of all migration commands that should be supported by your database adapter.
## Commands
### Migrate
The `migrate` command will run any migrations that have not yet been run.

View File

@@ -33,6 +33,7 @@ export default buildConfig({
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `autoPluralization` | Tell Mongoose to auto-pluralize any collection names if it encounters any singular words used as collection `slug`s. |
| `connectOptions` | Customize MongoDB connection options. Payload will connect to your MongoDB database using default options which you can override and extend to include all the [options](https://mongoosejs.com/docs/connections.html#options) available to mongoose. |
| `disableIndexHints` | Set to true to disable hinting to MongoDB to use 'id' as index. This is currently done when counting documents for pagination, as it increases the speed of the count function used in that query. Disabling this optimization might fix some problems with AWS DocumentDB. Defaults to false |
| `migrationDir` | Customize the directory that migrations are stored. |
### Access to Mongoose models
@@ -43,4 +44,4 @@ You can access Mongoose models as follows:
- Collection models - `payload.db.collections[myCollectionSlug]`
- Globals model - `payload.db.globals`
- Versions model (both collections and globals) - `payload.db.versions[myEntitySlug]`
- Versions model (both collections and globals) - `payload.db.versions[myEntitySlug]`

View File

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

View File

@@ -251,11 +251,16 @@ const field = {
### Description
A description can be configured three ways.
A description can be configured in three ways.
- As a string
- As a function that accepts an object containing the field's value, which returns a string
- As a React component that accepts value as a prop
- As a function which returns a string
- As a React component
Functions are called with an optional argument object with the following shape, and React components are rendered with the following props:
- `path` - the path of the field
- `value` - the current value of the field
As shown above, you can simply provide a string that will show by the field, but there are use cases where you may want to create some dynamic feedback. By using a function or a component for the `description` property you can provide realtime feedback as the user interacts with the form.
@@ -269,8 +274,8 @@ As shown above, you can simply provide a string that will show by the field, but
type: 'text',
maxLength: 20,
admin: {
description: ({ value }) =>
`${typeof value === 'string' ? 20 - value.length : '20'} characters left`,
description: ({ path, value }) =>
`${typeof value === 'string' ? 20 - value.length : '20'} characters left (field: ${path})`,
},
},
]
@@ -290,11 +295,12 @@ This example will display the number of characters allowed as the user types.
maxLength: 20,
admin: {
description:
({ value }) => (
({ path, value }) => (
<div>
Character count:
{' '}
{ value?.length || 0 }
(field: {path})
</div>
)
}
@@ -303,7 +309,7 @@ This example will display the number of characters allowed as the user types.
}
```
This component will count the number of characters entered.
This component will count the number of characters entered, as well as display the path of the field.
### TypeScript

View File

@@ -70,6 +70,43 @@ Set to `true` if you'd like this field to be sortable within the Admin UI using
Set to `false` if you'd like to disable the ability to create new documents from within the relationship field (hides the "Add new" button in the admin UI).
**`sortOptions`**
The `sortOptions` property allows you to define a default sorting order for the options within a Relationship field's dropdown. This can be particularly useful for ensuring that the most relevant options are presented first to the user.
You can specify `sortOptions` in two ways:
**As a string:**
Provide a string to define a global default sort field for all relationship field dropdowns across different collections. You can prefix the field name with a minus symbol ("-") to sort in descending order.
Example:
```ts
sortOptions: 'fieldName',
```
This configuration will sort all relationship field dropdowns by `"fieldName"` in ascending order.
**As an object :**
Specify an object where keys are collection slugs and values are strings representing the field names to sort by. This allows for different sorting fields for each collection's relationship dropdown.
Example:
```ts
sortOptions: {
"pages": "fieldName1",
"posts": "-fieldName2",
"categories": "fieldName3"
}
```
In this configuration:
- Dropdowns related to `pages` will be sorted by `"fieldName1"` in ascending order.
- Dropdowns for `posts` will use `"fieldName2"` for sorting in descending order (noted by the "-" prefix).
- Dropdowns associated with `categories` will sort based on `"fieldName3"` in ascending order.
Note: If `sortOptions` is not defined, the default sorting behavior of the Relationship field dropdown will be used.
### Filtering relationship options
Options can be dynamically limited by supplying a [query constraint](/docs/queries/overview), which will be used both for validating input and filtering available relationships in the UI.

View File

@@ -10,7 +10,7 @@ keywords: documentation, getting started, guide, Content Management System, cms,
Payload requires the following software:
- Yarn or NPM
- Any JavaScript package manager (Yarn, NPM, or pnpm)
- Node.js version 16+
- Any [compatible database](/docs/database/overview) (MongoDB or Postgres)

View File

@@ -10,13 +10,14 @@ While using Live Preview, the Admin panel emits a new `window.postMessage` event
Wiring your front-end into Live Preview is easy. If your front-end application is built with React or Next.js, use the [`useLivePreview`](#react) React hook that Payload provides. In the future, all other major frameworks like Vue, Svelte, etc will be officially supported. If you are using any of these frameworks today, you can still integrate with Live Preview yourself using the underlying tooling that Payload provides. See [building your own hook](#building-your-own-hook) for more information.
By default, all hooks require the following args:
By default, all hooks accept the following args:
| Path | Description |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`serverURL`** \* | The URL of your Payload server. |
| **`initialData`** | The initial data of the document. The live data will be merged in as changes are made. |
| **`depth`** | The depth of the relationships to fetch. Defaults to `0`. |
| **`apiRoute`** | The path of your API route as defined in `routes.api`. Defaults to `/api`. |
_\* An asterisk denotes that a property is required._
@@ -186,3 +187,55 @@ For a working demonstration of this, check out the official [Live Preview Exampl
- [Next.js App Router](https://github.com/payloadcms/payload/tree/main/examples/live-preview/next-app)
- [Next.js Pages Router](https://github.com/payloadcms/payload/tree/main/examples/live-preview/next-pages)
## Troubleshooting
### Relationships and/or uploads are not populating
If you are using relationships or uploads in your front-end application, and your front-end application runs on a different domain than your Payload server, you may need to configure [CORS](../configuration/overview) to allow requests to be made between the two domains. This includes sites that are running on a different port or subdomain. Similarly, if you are protecting resources behind user authentication, you may also need to configure [CSRF](../authentication/overview#csrf-protection) to allow cookies to be sent between the two domains. For example:
```ts
// payload.config.ts
{
// ...
// If your site is running on a different domain than your Payload server,
// This will allows requests to be made between the two domains
cors: {
[
'http://localhost:3001' // Your front-end application
],
},
// If you are protecting resources behind user authentication,
// This will allow cookies to be sent between the two domains
csrf: {
[
'http://localhost:3001' // Your front-end application
],
},
}
```
### Relationships and/or uploads disappear after editing a document
It is possible that either you are setting an improper [`depth`](../getting-started/concepts#depth) in your initial request and/or your `useLivePreview` hook, or they're mismatched. Ensure that the `depth` parameter is set to the correct value, and that it matches exactly in both places. For example:
```tsx
// Your initial request
const { docs } = await payload.find({
collection: 'pages',
depth: 1, // Ensure this is set to the proper depth for your application
where: {
slug: {
equals: 'home',
}
}
})
```
```tsx
// Your hook
const { data } = useLivePreview<PageType>({
initialData: initialPage,
serverURL: PAYLOAD_SERVER_URL,
depth: 1, // Ensure this matches the depth of your initial request
})
```

View File

@@ -12,6 +12,10 @@ Live Preview works by rendering an iframe on the page that loads your front-end
{/* IMAGE OF LIVE PREVIEW HERE */}
<Banner type="warning">
Live Preview is currently in beta. You may use this feature in production, but please be aware that it is subject to change and may not be fully stable for all use cases. If you encounter any issues, please [report them](https://github.com/payloadcms/payload/issues/new?assignees=jacobsfletch&labels=possible-bug&projects=&title=Live%20Preview&template=1.bug_report.yml) with as much detail as possible.
</Banner>
## Setup
Setting up Live Preview is easy. You first need to enable it through the `admin.livePreview` property of your Payload config. It takes the following options:

View File

@@ -6,7 +6,9 @@ desc: The Payload Local API allows you to interact with your database and execut
keywords: local api, config, configuration, documentation, Content Management System, cms, headless, javascript, node, react, express
---
The Payload Local API gives you the ability to execute the same operations that are available through REST and GraphQL within Node, directly on your server. Here, you don't need to deal with server latency or network speed whatsoever and can interact directly with your database.
The Payload Local API gives you the ability to execute the same operations that are available through REST and GraphQL
within Node, directly on your server. Here, you don't need to deal with server latency or network speed whatsoever and
can interact directly with your database.
<Banner type="success">
<strong>Tip:</strong>
@@ -30,7 +32,9 @@ You can gain access to the currently running `payload` object via two ways:
##### Importing it
You can import or require `payload` into your own files after it's been initialized, but you need to make sure that your `import` / `require` statements come **after** you call `payload.init()`—otherwise Payload won't have been initialized yet. That might be obvious. To us, it's usually not.
You can import or require `payload` into your own files after it's been initialized, but you need to make sure that
your `import` / `require` statements come **after** you call `payload.init()`—otherwise Payload won't have been
initialized yet. That might be obvious. To us, it's usually not.
Example:
@@ -47,7 +51,8 @@ const afterChangeHook: CollectionAfterChangeHook = async () => {
##### Accessing from the `req`
Payload is available anywhere you have access to the Express `req` - including within your access control and hook functions.
Payload is available anywhere you have access to the Express `req` - including within your access control and hook
functions.
Example:
@@ -61,10 +66,11 @@ const afterChangeHook: CollectionAfterChangeHook = async ({ req: { payload } })
### Local options available
You can specify more options within the Local API vs. REST or GraphQL due to the server-only context that they are executed in.
You can specify more options within the Local API vs. REST or GraphQL due to the server-only context that they are
executed in.
| Local Option | Description |
| ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|--------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `collection` | Required for Collection operations. Specifies the Collection slug to operate against. |
| `data` | The data to use within the operation. Required for `create`, `update`. |
| `depth` | [Control auto-population](/docs/getting-started/concepts#depth) of nested relationship and upload fields. |
@@ -268,7 +274,8 @@ const result = await payload.delete({
## Auth Operations
If a collection has [`Authentication`](/docs/authentication/overview) enabled, additional Local API operations will be available:
If a collection has [`Authentication`](/docs/authentication/overview) enabled, additional Local API operations will be
available:
#### Login
@@ -319,10 +326,11 @@ const token = await payload.forgotPassword({
// token: 'o38jf0q34jfij43f3f...', // JWT used for auth
// user: { ... } // the user document that just logged in
// }
const result = await payload.forgotPassword({
const result = await payload.resetPassword({
collection: 'users', // required
data: {
// required
password: req.body.password, // the new password to set
token: 'afh3o2jf2p3f...', // the token generated from the forgotPassword operation
},
req: req, // pass an Express `req` which will be provided to all hooks
@@ -402,7 +410,9 @@ const result = await payload.updateGlobal({
## Next.js Conflict with Local API
There is a known issue when using the Local API with Next.js version `13.4.13` and higher. Next.js executes within a separate child process, and Payload has not been initalized yet in these instances. That means that unless you explicitly initialize Payload within your operation, it will not be running and return no data / an empty object.
There is a known issue when using the Local API with Next.js version `13.4.13` and higher. Next.js executes within a
separate child process, and Payload has not been initalized yet in these instances. That means that unless you
explicitly initialize Payload within your operation, it will not be running and return no data / an empty object.
As a workaround, we recommend leveraging the following pattern to determine and ensure Payload is initalized:
@@ -462,7 +472,8 @@ export const getPayloadClient = async ({ initOptions, seed }: Args = {}): Promis
}
```
To checkout how this works in a project, take a look at our [custom server example](https://github.com/payloadcms/payload/blob/master/examples/custom-server/src/getPayload.ts).
To checkout how this works in a project, take a look at
our [custom server example](https://github.com/payloadcms/payload/blob/master/examples/custom-server/src/getPayload.ts).
## Example Script using Local API

View File

@@ -11,7 +11,8 @@ keywords: deployment, production, config, configuration, documentation, Content
launch. <strong>Awesome! Great work!</strong> Now, what's next?
</Banner>
There are many ways to deploy Payload to a production environment. When evaluating how you will deploy Payload, you need to consider these main aspects:
There are many ways to deploy Payload to a production environment. When evaluating how you will deploy Payload, you need
to consider these main aspects:
1. [Basics](#basics)
1. [Security](#security)
@@ -21,19 +22,26 @@ There are many ways to deploy Payload to a production environment. When evaluati
## Basics
In order for Payload to run, it requires both the server code and the built admin panel. These will be the `dist` and `build` directories by default. If you've used `create-payload-app` to create your project, executing the `build` npm script will build both and output these directories.
In order for Payload to run, it requires both the server code and the built admin panel. These will be the `dist`
and `build` directories by default. If you've used `create-payload-app` to create your project, executing the `build`
npm script will build both and output these directories.
## Security
Payload features a suite of security features that you can rely on to strengthen your application's security. When deploying to Production, it's a good idea to double-check that you are making proper use of each of them.
Payload features a suite of security features that you can rely on to strengthen your application's security. When
deploying to Production, it's a good idea to double-check that you are making proper use of each of them.
##### The Secret Key
When you initialize Payload, you provide it with a `secret` property. This property should be impossible to guess and extremely difficult for brute-force attacks to crack. Make sure your Production `secret` is a long, complex string. It's often best practice to store it in an `env` file which is not checked into your Git repository, using `dotenv` to supply it to your `payload.init` call.
When you initialize Payload, you provide it with a `secret` property. This property should be impossible to guess and
extremely difficult for brute-force attacks to crack. Make sure your Production `secret` is a long, complex string. It's
often best practice to store it in an `env` file which is not checked into your Git repository, using `dotenv` to supply
it to your `payload.init` call.
##### Double-check and thoroughly test all Access Control
Because _**you**_ are in complete control of who can do what with your data, you should double and triple-check that you wield that power responsibly before deploying to Production.
Because _**you**_ are in complete control of who can do what with your data, you should double and triple-check that you
wield that power responsibly before deploying to Production.
<Banner type="error">
<strong>By default, all Access Control functions require that a user is successfully logged in to Payload to create, read, update, or delete data.</strong>{' '}
@@ -44,7 +52,8 @@ Because _**you**_ are in complete control of who can do what with your data, you
##### Building the Admin panel
Before running in Production, you need to have built a production-ready copy of the Payload Admin panel. To do this, Payload provides the `build` NPM script. You can use it by adding a `script` to your `package.json` file like this:
Before running in Production, you need to have built a production-ready copy of the Payload Admin panel. To do this,
Payload provides the `build` NPM script. You can use it by adding a `script` to your `package.json` file like this:
`package.json`:
@@ -60,19 +69,26 @@ Before running in Production, you need to have built a production-ready copy of
}
```
Then, to build Payload, you would run `npm run build` in your project folder. A production-ready Admin bundle will be created in the `build` directory.
Then, to build Payload, you would run `npm run build` in your project folder. A production-ready Admin bundle will be
created in the `build` directory.
##### Setting Node to Production
Make sure you set the environment variable `NODE_ENV` to `production`. Based on this variable, many Node packages automatically optimize themselves. In production, Payload automatically disables the [GraphQL Playground](/docs/graphql/overview#graphql-playground), serves a production-ready version of the Admin panel, and other changes.
Make sure you set the environment variable `NODE_ENV` to `production`. Based on this variable, many Node packages
automatically optimize themselves. In production, Payload automatically disables
the [GraphQL Playground](/docs/graphql/overview#graphql-playground), serves a production-ready version of the Admin
panel, and other changes.
##### Secure Cookie Settings
You should be using an SSL certificate for production Payload instances, which means you can [enable secure cookies](/docs/authentication/config) in your Authentication-enabled Collection configs.
You should be using an SSL certificate for production Payload instances, which means you
can [enable secure cookies](/docs/authentication/config) in your Authentication-enabled Collection configs.
##### Preventing API Abuse
Payload comes with a robust set of built-in anti-abuse measures, such as locking out users after X amount of failed login attempts, request rate limiting, GraphQL query complexity limits, max `depth` settings, and more. [Click here to learn more](/docs/production/preventing-abuse).
Payload comes with a robust set of built-in anti-abuse measures, such as locking out users after X amount of failed
login attempts, request rate limiting, GraphQL query complexity limits, max `depth` settings, and
more. [Click here to learn more](/docs/production/preventing-abuse).
## MongoDB
@@ -80,11 +96,18 @@ Payload can be used with any MongoDB compatible database including AWS DocumentD
##### Managing MongoDB yourself
If you are using a [persistent filesystem-based cloud host](#persistent-vs-ephemeral-filesystems) such as a [DigitalOcean Droplet](https://www.digitalocean.com/products/droplets/) or an [Amazon EC2](https://aws.amazon.com/ec2/?ec2-whats-new.sort-by=item.additionalFields.postDateTime&ec2-whats-new.sort-order=desc) server, you might opt to install MongoDB directly on that server itself so that Node can communicate with it locally. With this approach, you can benefit from faster response times, but scaling can become more involved as your app's user base grows.
If you are using a [persistent filesystem-based cloud host](#persistent-vs-ephemeral-filesystems) such as
a [DigitalOcean Droplet](https://www.digitalocean.com/products/droplets/) or
an [Amazon EC2](https://aws.amazon.com/ec2/?ec2-whats-new.sort-by=item.additionalFields.postDateTime&ec2-whats-new.sort-order=desc)
server, you might opt to install MongoDB directly on that server itself so that Node can communicate with it locally.
With this approach, you can benefit from faster response times, but scaling can become more involved as your app's user
base grows.
##### Letting someone else do it
Alternatively, you can rely on a third-party MongoDB host such as [MongoDB Atlas](https://www.mongodb.com/). With Atlas or a similar cloud provider, you can trust them to take care of your database's availability, security, redundancy, and backups.
Alternatively, you can rely on a third-party MongoDB host such as [MongoDB Atlas](https://www.mongodb.com/). With Atlas
or a similar cloud provider, you can trust them to take care of your database's availability, security, redundancy, and
backups.
<Banner type="warning">
<strong>Note:</strong>
@@ -98,21 +121,31 @@ Alternatively, you can rely on a third-party MongoDB host such as [MongoDB Atlas
##### DocumentDB
When using AWS DocumentDB, you will need to configure connection options for authentication in the `mongoOptions` passed to `payload.init`. You also need to set `mongoOptions.useFacet` to `false` to disable use of the unsupported `$facet` aggregation.
When using AWS DocumentDB, you will need to configure connection options for authentication in the `connectOptions`
passed to the `mongooseAdapter` . You also need to set `connectOptions.useFacet` to `false` to disable use of the
unsupported `$facet` aggregation.
##### CosmosDB
When using Azure Cosmos DB, an index is needed for any field you may want to sort on. To add the sort index for all fields that may be sorted in the admin UI use the <a href="/docs/configuration/overview">indexSortableFields</a> configuration option.
When using Azure Cosmos DB, an index is needed for any field you may want to sort on. To add the sort index for all
fields that may be sorted in the admin UI use the <a href="/docs/configuration/overview">indexSortableFields</a>
configuration option.
## File storage
If you are using Payload to [manage file uploads](/docs/upload/overview), you need to consider where your uploaded files will be permanently stored. If you do not use Payload for file uploads, then this section does not impact your app whatsoever.
If you are using Payload to [manage file uploads](/docs/upload/overview), you need to consider where your uploaded files
will be permanently stored. If you do not use Payload for file uploads, then this section does not impact your app
whatsoever.
#### Persistent vs Ephemeral Filesystems
Some cloud app hosts such as [Heroku](https://heroku.com) use `ephemeral` file systems, which means that any files uploaded to your server only last until the server restarts or shuts down. Heroku and similar providers schedule restarts and shutdowns without your control, meaning your uploads will accidentally disappear without any way to get them back.
Some cloud app hosts such as [Heroku](https://heroku.com) use `ephemeral` file systems, which means that any files
uploaded to your server only last until the server restarts or shuts down. Heroku and similar providers schedule
restarts and shutdowns without your control, meaning your uploads will accidentally disappear without any way to get
them back.
Alternatively, persistent filesystems will never delete your files and can be trusted to reliably host uploads perpetually.
Alternatively, persistent filesystems will never delete your files and can be trusted to reliably host uploads
perpetually.
**Popular cloud providers with ephemeral filesystems:**
@@ -135,21 +168,26 @@ Alternatively, persistent filesystems will never delete your files and can be tr
##### Using ephemeral filesystem providers like Heroku
If you don't use Payload's `upload` functionality, you can go ahead and use Heroku or similar platform easily. Everything will work exactly as you want it to.
If you don't use Payload's `upload` functionality, you can go ahead and use Heroku or similar platform easily.
Everything will work exactly as you want it to.
But, if you do, and you still want to use an ephemeral filesystem provider, you can write a hook-based solution to _copy_ the files your users upload to a more permanent storage solution like Amazon S3 or DigitalOcean Spaces.
But, if you do, and you still want to use an ephemeral filesystem provider, you can write a hook-based solution to
_copy_ the files your users upload to a more permanent storage solution like Amazon S3 or DigitalOcean Spaces.
**To automatically send uploaded files to S3 or similar, you could:**
- Write an asynchronous `beforeChange` hook for all Collections that support Uploads, which takes any uploaded `file` from the Express `req` and sends it to an S3 bucket
- Write an `afterRead` hook to save a `s3URL` field that automatically takes the `filename` stored and formats a full S3 URL
- Write an asynchronous `beforeChange` hook for all Collections that support Uploads, which takes any uploaded `file`
from the Express `req` and sends it to an S3 bucket
- Write an `afterRead` hook to save a `s3URL` field that automatically takes the `filename` stored and formats a full S3
URL
- Write an `afterDelete` hook that automatically deletes files from the S3 bucket
With the above configuration, deploying to Heroku or similar becomes no problem.
## DigitalOcean Tutorials
DigitalOcean provides extremely helpful documentation that can walk you through the entire process of creating a production-ready Droplet to host your Payload app:
DigitalOcean provides extremely helpful documentation that can walk you through the entire process of creating a
production-ready Droplet to host your Payload app:
1. Create a new Ubuntu 20.04 droplet on [DigitalOcean](https://digitalocean.com)
1. [Initial server setup](https://www.digitalocean.com/community/tutorials/initial-server-setup-with-ubuntu-20-04)
@@ -160,18 +198,25 @@ DigitalOcean provides extremely helpful documentation that can walk you through
### Swap Space
Swap refers to a section of storage on the hard drive that is reserved to temporarily store data that can no longer fit within RAM. This allows for the expansion of your server's working memory, with some limitations. Swap space comes into play when available RAM can no longer accommodate actively used application data, enabling the system to continue functioning.
Swap refers to a section of storage on the hard drive that is reserved to temporarily store data that can no longer fit
within RAM. This allows for the expansion of your server's working memory, with some limitations. Swap space comes into
play when available RAM can no longer accommodate actively used application data, enabling the system to continue
functioning.
Insufficient space can lead to deployment errors and memory-related issues, resulting in application crashes, sluggish performance, or an unresponsive server.
Insufficient space can lead to deployment errors and memory-related issues, resulting in application crashes, sluggish
performance, or an unresponsive server.
Common deployment error due to **space limitations** (as reported by users):
- `Error: Command failed with exit code 1`
To configure swap, we recommend following this tutorial on [How To Add Swap Space](https://www.digitalocean.com/community/tutorials/how-to-add-swap-space-on-ubuntu-22-04).
To configure swap, we recommend following this tutorial
on [How To Add Swap Space](https://www.digitalocean.com/community/tutorials/how-to-add-swap-space-on-ubuntu-22-04).
## Docker
This is an example of a multi-stage docker build of Payload for production. Ensure you are setting your environment variables on deployment, like `PAYLOAD_SECRET`, `PAYLOAD_CONFIG_PATH`, and `DATABASE_URI` if needed.
This is an example of a multi-stage docker build of Payload for production. Ensure you are setting your environment
variables on deployment, like `PAYLOAD_SECRET`, `PAYLOAD_CONFIG_PATH`, and `DATABASE_URI` if needed.
```dockerfile
FROM node:18-alpine as base

View File

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

View File

@@ -38,7 +38,7 @@ Every Payload Collection can opt-in to supporting Uploads by specifying the `upl
</strong> on that collection.
</Banner>
#### Collection Upload Options
### Collection Upload Options
| Option | Description |
| ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
@@ -47,14 +47,14 @@ Every Payload Collection can opt-in to supporting Uploads by specifying the `upl
| **`adminThumbnail`** | Set the way that the Admin panel will display thumbnails for this Collection. [More](#admin-thumbnails) |
| **`crop`** | Set to `false` to disable the cropping tool in the Admin panel. Crop is enabled by default. [More](#crop-and-focal-point-selector) |
| **`disableLocalStorage`** | Completely disable uploading files to disk locally. [More](#disabling-local-upload-storage) |
| **`focalPoint`** | Set to `false` to disable the focal point selection tool in the Admin panel. The focal point selector is only available when `imageSizes` or `resizeOptions` are defined. [More](#crop-and-focal-point-selector) |
| **`focalPoint`** | Set to `false` to disable the focal point selection tool in the Admin panel. The focal point selector is only available when `imageSizes` or `resizeOptions` are defined. [More](#crop-and-focal-point-selector) |
| **`formatOptions`** | An object with `format` and `options` that are used with the Sharp image library to format the upload file. [More](https://sharp.pixelplumbing.com/api-output#toformat) |
| **`handlers`** | Array of Express request handlers to execute before the built-in Payload static middleware executes. |
| **`imageSizes`** | If specified, image uploads will be automatically resized in accordance to these image sizes. [More](#image-sizes) |
| **`mimeTypes`** | Restrict mimeTypes in the file picker. Array of valid mimetypes or mimetype wildcards [More](#mimetypes) |
| **`staticOptions`** | Set options for `express.static` to use while serving your static files. [More](http://expressjs.com/en/resources/middleware/serve-static.html) format) |
| **`resizeOptions`** | An object passed to the the Sharp image library to resize the uploaded file. [More](https://sharp.pixelplumbing.com/api-resize) |
| **`filesRequiredOnCreate`** | Mandate file data on creation, default is true. |
| **`staticOptions`** | Set options for `express.static` to use while serving your static files. [More](http://expressjs.com/en/resources/middleware/serve-static.html) |
| **`resizeOptions`** | An object passed to the the Sharp image library to resize the uploaded file. [More](https://sharp.pixelplumbing.com/api-resize) |
| **`filesRequiredOnCreate`** | Mandate file data on creation, default is true. |
_An asterisk denotes that a property above is required._
@@ -148,6 +148,23 @@ All auto-resized images are exposed to be re-used in hooks and similar via an ob
The object will have keys for each size generated, and each key will be set equal to a buffer containing the file data.
##### Handling Image Enlargement
When an uploaded image is smaller than the defined image size, we have 3 options:
`withoutEnlargement: undefined | false | true`
1.`undefined` [default]: uploading images with smaller width AND height than the image size will return null
2. `false`: always enlarge images to the image size
3. `true`: if the image is smaller than the image size, return the original image
<Banner type="error">
<strong>Note:</strong>
<br />
By default, the image size will return NULL when the uploaded image is smaller than the defined image size.
Use the `withoutEnlargement` prop to change this.
</Banner>
### Crop and Focal Point Selector
This feature is only available for image file types.

View File

@@ -1,3 +0,0 @@
module.exports = {
config: () => null,
}

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/bundler-vite",
"version": "0.1.3",
"version": "0.1.5",
"description": "The officially supported Vite bundler adapter for Payload",
"repository": "https://github.com/payloadcms/payload",
"license": "MIT",

View File

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

View File

@@ -10,7 +10,7 @@ CLI for easily starting new Payload project
$ npx create-payload-app
$ npx create-payload-app my-project
$ npx create-payload-app -n my-project -t blog
$ npx create-payload-app -n my-project -t website
OPTIONS

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/db-mongodb",
"version": "1.0.7",
"version": "1.1.0",
"description": "The officially supported MongoDB database adapter for Payload",
"repository": "https://github.com/payloadcms/payload",
"license": "MIT",

View File

@@ -55,7 +55,7 @@ export const find: Find = async function find(
useEstimatedCount,
}
if (!useEstimatedCount) {
if (!useEstimatedCount && this.disableIndexHints !== true) {
// Improve the performance of the countDocuments query which is used if useEstimatedCount is set to false by adding a hint.
paginationOptions.useCustomCountFn = () => {
return Promise.resolve(

View File

@@ -74,7 +74,7 @@ export const findGlobalVersions: FindGlobalVersions = async function findGlobalV
useEstimatedCount,
}
if (!useEstimatedCount) {
if (!useEstimatedCount && this.disableIndexHints !== true) {
// Improve the performance of the countDocuments query which is used if useEstimatedCount is set to false by adding a hint.
paginationOptions.useCustomCountFn = () => {
return Promise.resolve(

View File

@@ -71,7 +71,7 @@ export const findVersions: FindVersions = async function findVersions(
useEstimatedCount,
}
if (!useEstimatedCount) {
if (!useEstimatedCount && this.disableIndexHints !== true) {
// Improve the performance of the countDocuments query which is used if useEstimatedCount is set to false by adding a hint.
paginationOptions.useCustomCountFn = () => {
return Promise.resolve(

View File

@@ -2,6 +2,7 @@ import type { ClientSession, ConnectOptions, Connection } from 'mongoose'
import type { Payload } from 'payload'
import type { BaseDatabaseAdapter } from 'payload/database'
import fs from 'fs'
import mongoose from 'mongoose'
import path from 'path'
import { createDatabaseAdapter } from 'payload/database'
@@ -46,6 +47,8 @@ export interface Args {
/** Set false to disable $facet aggregation in non-supporting databases, Defaults to true */
useFacet?: boolean
}
/** Set to true to disable hinting to MongoDB to use 'id' as index. This is currently done when counting documents for pagination. Disabling this optimization might fix some problems with AWS DocumentDB. Defaults to false */
disableIndexHints?: boolean
migrationDir?: string
/** The URL to connect to MongoDB or false to start payload and prevent connecting */
url: false | string
@@ -87,11 +90,12 @@ declare module 'payload' {
export function mongooseAdapter({
autoPluralization = true,
connectOptions,
disableIndexHints = false,
migrationDir: migrationDirArg,
url,
}: Args): MongooseAdapterResult {
function adapter({ payload }: { payload: Payload }) {
const migrationDir = migrationDirArg || path.resolve(process.cwd(), 'src/migrations')
const migrationDir = findMigrationDir(migrationDirArg)
mongoose.set('strictQuery', false)
extendWebpackConfig(payload.config)
@@ -105,6 +109,7 @@ export function mongooseAdapter({
collections: {},
connectOptions: connectOptions || {},
connection: undefined,
disableIndexHints,
globals: undefined,
mongoMemoryServer: undefined,
sessions: {},
@@ -145,3 +150,42 @@ export function mongooseAdapter({
return adapter
}
/**
* Attempt to find migrations directory.
*
* Checks for the following directories in order:
* - `migrationDir` argument from Payload config
* - `src/migrations`
* - `dist/migrations`
* - `migrations`
*
* Defaults to `src/migrations`
*
* @param migrationDir
* @returns
*/
function findMigrationDir(migrationDir?: string): string {
const cwd = process.cwd()
const srcDir = path.resolve(cwd, 'src/migrations')
const distDir = path.resolve(cwd, 'dist/migrations')
const relativeMigrations = path.resolve(cwd, 'migrations')
// Use arg if provided
if (migrationDir) return migrationDir
// Check other common locations
if (fs.existsSync(srcDir)) {
return srcDir
}
if (fs.existsSync(distDir)) {
return distDir
}
if (fs.existsSync(relativeMigrations)) {
return relativeMigrations
}
return srcDir
}

View File

@@ -58,7 +58,7 @@ export const queryDrafts: QueryDrafts = async function queryDrafts(
useEstimatedCount,
}
if (!useEstimatedCount) {
if (!useEstimatedCount && this.disableIndexHints !== true) {
// Improve the performance of the countDocuments query which is used if useEstimatedCount is set to false by adding a hint.
paginationOptions.useCustomCountFn = () => {
return Promise.resolve(

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/db-postgres",
"version": "0.1.13",
"version": "0.2.1",
"description": "The officially supported Postgres database adapter for Payload",
"repository": "https://github.com/payloadcms/payload",
"license": "MIT",
@@ -26,7 +26,8 @@
"drizzle-orm": "0.28.5",
"pg": "8.11.3",
"prompts": "2.4.2",
"to-snake-case": "1.0.0"
"to-snake-case": "1.0.0",
"uuid": "9.0.0"
},
"devDependencies": {
"@payloadcms/eslint-config": "workspace:*",

View File

@@ -33,7 +33,7 @@ export const findMany = async function find({
const db = adapter.sessions[req.transactionID]?.db || adapter.drizzle
const table = adapter.tables[tableName]
let limit = limitArg ?? 10
const limit = limitArg ?? 10
let totalDocs: number
let totalPages: number
let hasPrevPage: boolean
@@ -51,6 +51,7 @@ export const findMany = async function find({
})
const orderedIDMap: Record<number | string, number> = {}
let orderedIDs: (number | string)[]
const selectDistinctMethods: ChainedMethods = []
@@ -116,7 +117,8 @@ export const findMany = async function find({
selectDistinctResult.forEach(({ id }, i) => {
orderedIDMap[id as number | string] = i
})
findManyArgs.where = inArray(adapter.tables[tableName].id, Object.keys(orderedIDMap))
orderedIDs = Object.keys(orderedIDMap)
findManyArgs.where = inArray(adapter.tables[tableName].id, orderedIDs)
} else {
findManyArgs.limit = limitArg === 0 ? undefined : limitArg
@@ -132,7 +134,7 @@ export const findMany = async function find({
const findPromise = db.query[tableName].findMany(findManyArgs)
if (pagination !== false || selectDistinctResult?.length > limit) {
if (pagination !== false && (orderedIDs ? orderedIDs?.length >= limit : true)) {
const selectCountMethods: ChainedMethods = []
joinAliases.forEach(({ condition, table }) => {
@@ -174,9 +176,8 @@ export const findMany = async function find({
rawDocs.sort((a, b) => orderedIDMap[a.id] - orderedIDMap[b.id])
}
if (pagination === false) {
if (pagination === false || !totalDocs) {
totalDocs = rawDocs.length
limit = totalDocs
totalPages = 1
pagingCounter = 1
hasPrevPage = false

View File

@@ -1,5 +1,6 @@
import type { Payload } from 'payload'
import fs from 'fs'
import path from 'path'
import { createDatabaseAdapter } from 'payload/database'
@@ -42,7 +43,7 @@ export type { MigrateDownArgs, MigrateUpArgs } from './types'
export function postgresAdapter(args: Args): PostgresAdapterResult {
function adapter({ payload }: { payload: Payload }) {
const migrationDir = args.migrationDir || path.resolve(process.cwd(), 'src/migrations')
const migrationDir = findMigrationDir(args.migrationDir)
extendWebpackConfig(payload.config)
extendViteConfig(payload.config)
@@ -53,6 +54,7 @@ export function postgresAdapter(args: Args): PostgresAdapterResult {
// Postgres-specific
drizzle: undefined,
enums: {},
fieldConstraints: {},
pool: undefined,
poolOptions: args.pool,
push: args.push,
@@ -60,7 +62,6 @@ export function postgresAdapter(args: Args): PostgresAdapterResult {
schema: {},
sessions: {},
tables: {},
fieldConstraints: {},
// DatabaseAdapter
beginTransaction,
@@ -101,3 +102,42 @@ export function postgresAdapter(args: Args): PostgresAdapterResult {
return adapter
}
/**
* Attempt to find migrations directory.
*
* Checks for the following directories in order:
* - `migrationDir` argument from Payload config
* - `src/migrations`
* - `dist/migrations`
* - `migrations`
*
* Defaults to `src/migrations`
*
* @param migrationDir
* @returns
*/
function findMigrationDir(migrationDir?: string): string {
const cwd = process.cwd()
const srcDir = path.resolve(cwd, 'src/migrations')
const distDir = path.resolve(cwd, 'dist/migrations')
const relativeMigrations = path.resolve(cwd, 'migrations')
// Use arg if provided
if (migrationDir) return migrationDir
// Check other common locations
if (fs.existsSync(srcDir)) {
return srcDir
}
if (fs.existsSync(distDir)) {
return distDir
}
if (fs.existsSync(relativeMigrations)) {
return relativeMigrations
}
return srcDir
}

View File

@@ -64,20 +64,25 @@ const buildQuery = async function buildQuery({
orderBy.order = asc
}
const { columnName: sortTableColumnName, table: sortTable } = getTableColumnFromPath({
adapter,
collectionPath: sortPath,
fields,
joinAliases,
joins,
locale,
pathSegments: sortPath.replace(/__/g, '.').split('.'),
selectFields,
tableName,
})
try {
const { columnName: sortTableColumnName, table: sortTable } = getTableColumnFromPath({
adapter,
collectionPath: sortPath,
fields,
joinAliases,
joins,
locale,
pathSegments: sortPath.replace(/__/g, '.').split('.'),
selectFields,
tableName,
})
orderBy.column = sortTable?.[sortTableColumnName]
} catch (err) {
// continue
}
}
orderBy.column = sortTable[sortTableColumnName]
} else {
if (!orderBy?.column) {
orderBy.order = desc
const createdAt = adapter.tables[tableName]?.createdAt

View File

@@ -297,6 +297,9 @@ export const getTableColumnFromPath = ({
table: adapter.tables[newTableName],
}
}
if (pathSegments[1] === 'blockType') {
throw new APIError('Querying on blockType is not supported')
}
break
}

View File

@@ -36,7 +36,7 @@ export const insertArrays = async ({ adapter, arrays, db, parentRows }: Args): P
}
}
const parentID = parentRows[parentRowIndex].id
const parentID = parentRows[parentRowIndex].id || parentRows[parentRowIndex]._parentID
// Add any sub arrays that need to be created
// We will call this recursively below

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/live-preview-react",
"version": "0.1.5",
"version": "0.2.0",
"description": "The official live preview React SDK for Payload",
"repository": "https://github.com/payloadcms/payload",
"license": "MIT",
@@ -25,7 +25,6 @@
"payload": "workspace:*"
},
"peerDependencies": {
"payload": "^2.0.0",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
},
"exports": {

View File

@@ -7,6 +7,7 @@ import { useCallback, useEffect, useRef, useState } from 'react'
// you can conditionally render loading UI based on the `isLoading` state
export const useLivePreview = <T extends any>(props: {
apiRoute?: string
depth?: number
initialData: T
serverURL: string
@@ -14,7 +15,7 @@ export const useLivePreview = <T extends any>(props: {
data: T
isLoading: boolean
} => {
const { depth = 0, initialData, serverURL } = props
const { apiRoute, depth, initialData, serverURL } = props
const [data, setData] = useState<T>(initialData)
const [isLoading, setIsLoading] = useState<boolean>(true)
const hasSentReadyMessage = useRef<boolean>(false)
@@ -26,6 +27,7 @@ export const useLivePreview = <T extends any>(props: {
useEffect(() => {
const subscription = subscribe({
apiRoute,
callback: onChange,
depth,
initialData,
@@ -43,7 +45,7 @@ export const useLivePreview = <T extends any>(props: {
return () => {
unsubscribe(subscription)
}
}, [serverURL, onChange, depth, initialData])
}, [serverURL, onChange, depth, initialData, apiRoute])
return {
data,

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/live-preview",
"version": "0.1.5",
"version": "0.2.0",
"description": "The official live preview JavaScript SDK for Payload",
"repository": "https://github.com/payloadcms/payload",
"license": "MIT",
@@ -20,9 +20,6 @@
"@payloadcms/eslint-config": "workspace:*",
"payload": "workspace:*"
},
"peerDependencies": {
"payload": "^2.0.0"
},
"exports": {
".": {
"default": "./src/index.ts",

View File

@@ -6,31 +6,53 @@ import { mergeData } from '.'
// Send this cached value to `mergeData`, instead of `eventData.fieldSchemaJSON` directly
let payloadLivePreviewFieldSchema = undefined // TODO: type this from `fieldSchemaToJSON` return type
// Each time the data is merged, cache the result as a `previousData` variable
// This will ensure changes compound overtop of each other
let payloadLivePreviewPreviousData = undefined
export const handleMessage = async <T>(args: {
depth: number
apiRoute?: string
depth?: number
event: MessageEvent
initialData: T
serverURL: string
}): Promise<T> => {
const { depth, event, initialData, serverURL } = args
if (event.origin === serverURL && event.data) {
const eventData = JSON.parse(event?.data)
const { apiRoute, depth, event, initialData, serverURL } = args
if (eventData.type === 'payload-live-preview') {
if (!payloadLivePreviewFieldSchema && eventData.fieldSchemaJSON) {
payloadLivePreviewFieldSchema = eventData.fieldSchemaJSON
}
if (
event.origin === serverURL &&
event.data &&
typeof event.data === 'object' &&
event.data.type === 'payload-live-preview'
) {
const { data, externallyUpdatedRelationship, fieldSchemaJSON } = event.data
const mergedData = await mergeData<T>({
depth,
fieldSchema: payloadLivePreviewFieldSchema,
incomingData: eventData.data,
initialData,
serverURL,
})
return mergedData
if (!payloadLivePreviewFieldSchema && fieldSchemaJSON) {
payloadLivePreviewFieldSchema = fieldSchemaJSON
}
if (!payloadLivePreviewFieldSchema) {
// eslint-disable-next-line no-console
console.warn(
'Payload Live Preview: No `fieldSchemaJSON` was received from the parent window. Unable to merge data.',
)
return initialData
}
const mergedData = await mergeData<T>({
apiRoute,
depth,
externallyUpdatedRelationship,
fieldSchema: payloadLivePreviewFieldSchema,
incomingData: data,
initialData: payloadLivePreviewPreviousData || initialData,
serverURL,
})
payloadLivePreviewPreviousData = mergedData
return mergedData
}
return initialData

View File

@@ -1,39 +1,83 @@
import type { PaginatedDocs } from 'payload/database'
import type { fieldSchemaToJSON } from 'payload/utilities'
import type { PopulationsByCollection, UpdatedDocument } from './types'
import { traverseFields } from './traverseFields'
export type MergeLiveDataArgs<T> = {
export const mergeData = async <T>(args: {
apiRoute?: string
depth: number
depth?: number
externallyUpdatedRelationship?: UpdatedDocument
fieldSchema: ReturnType<typeof fieldSchemaToJSON>
incomingData: Partial<T>
initialData: T
returnNumberOfRequests?: boolean
serverURL: string
}
export const mergeData = async <T>({
apiRoute,
depth,
fieldSchema,
incomingData,
initialData,
serverURL,
}: MergeLiveDataArgs<T>): Promise<T> => {
const result = { ...initialData }
const populationPromises: Promise<void>[] = []
traverseFields({
}): Promise<
T & {
_numberOfRequests?: number
}
> => {
const {
apiRoute,
depth,
externallyUpdatedRelationship,
fieldSchema,
incomingData,
populationPromises,
result,
initialData,
returnNumberOfRequests,
serverURL,
} = args
const result = { ...initialData }
const populationsByCollection: PopulationsByCollection = {}
traverseFields({
externallyUpdatedRelationship,
fieldSchema,
incomingData,
populationsByCollection,
result,
})
await Promise.all(populationPromises)
await Promise.all(
Object.entries(populationsByCollection).map(async ([collection, populations]) => {
const ids = new Set(populations.map(({ id }) => id))
const url = `${serverURL}${
apiRoute || '/api'
}/${collection}?depth=${depth}&where[id][in]=${Array.from(ids).join(',')}`
return result
let res: PaginatedDocs
try {
res = await fetch(url, {
credentials: 'include',
headers: {
'Content-Type': 'application/json',
},
}).then((res) => res.json())
if (res?.docs?.length > 0) {
res.docs.forEach((doc) => {
populationsByCollection[collection].forEach((population) => {
if (population.id === doc.id) {
population.ref[population.accessor] = doc
}
})
})
}
} catch (err) {
console.error(err) // eslint-disable-line no-console
}
}),
)
return {
...result,
...(returnNumberOfRequests
? { _numberOfRequests: Object.keys(populationsByCollection).length }
: {}),
}
}

View File

@@ -1,25 +0,0 @@
type Args = {
accessor: number | string
apiRoute?: string
collection: string
depth: number
id: number | string
ref: Record<string, unknown>
serverURL: string
}
export const promise = async ({
id,
accessor,
apiRoute,
collection,
depth,
ref,
serverURL,
}: Args): Promise<void> => {
const res: any = await fetch(
`${serverURL}${apiRoute || '/api'}/${collection}/${id}?depth=${depth}`,
).then((res) => res.json())
ref[accessor] = res
}

View File

@@ -8,10 +8,10 @@ export const ready = (args: { serverURL: string }): void => {
const windowToPostTo: Window = window?.opener || window?.parent
windowToPostTo?.postMessage(
JSON.stringify({
{
ready: true,
type: 'payload-live-preview',
}),
},
serverURL,
)
}

View File

@@ -1,15 +1,16 @@
import { handleMessage } from '.'
export const subscribe = <T>(args: {
apiRoute?: string
callback: (data: T) => void
depth: number
depth?: number
initialData: T
serverURL: string
}): ((event: MessageEvent) => void) => {
const { callback, depth, initialData, serverURL } = args
const { apiRoute, callback, depth, initialData, serverURL } = args
const onMessage = async (event: MessageEvent) => {
const mergedData = await handleMessage<T>({ depth, event, initialData, serverURL })
const mergedData = await handleMessage<T>({ apiRoute, depth, event, initialData, serverURL })
callback(mergedData)
}

View File

@@ -1,83 +1,92 @@
import type { fieldSchemaToJSON } from 'payload/utilities'
import { promise } from './promise'
import type { PopulationsByCollection, UpdatedDocument } from './types'
type Args<T> = {
apiRoute?: string
depth: number
import { traverseRichText } from './traverseRichText'
export const traverseFields = <T>(args: {
externallyUpdatedRelationship?: UpdatedDocument
fieldSchema: ReturnType<typeof fieldSchemaToJSON>
incomingData: T
populationPromises: Promise<void>[]
populationsByCollection: PopulationsByCollection
result: T
serverURL: string
}
}): void => {
const {
externallyUpdatedRelationship,
fieldSchema: fieldSchemas,
incomingData,
populationsByCollection,
result,
} = args
export const traverseFields = <T>({
apiRoute,
depth,
fieldSchema,
incomingData,
populationPromises,
result,
serverURL,
}: Args<T>): void => {
fieldSchema.forEach((fieldJSON) => {
if ('name' in fieldJSON && typeof fieldJSON.name === 'string') {
const fieldName = fieldJSON.name
fieldSchemas.forEach((fieldSchema) => {
if ('name' in fieldSchema && typeof fieldSchema.name === 'string') {
const fieldName = fieldSchema.name
switch (fieldSchema.type) {
case 'richText':
result[fieldName] = traverseRichText({
externallyUpdatedRelationship,
incomingData: incomingData[fieldName],
populationsByCollection,
result: result[fieldName],
})
break
switch (fieldJSON.type) {
case 'array':
if (Array.isArray(incomingData[fieldName])) {
result[fieldName] = incomingData[fieldName].map((row, i) => {
const hasExistingRow =
Array.isArray(result[fieldName]) &&
typeof result[fieldName][i] === 'object' &&
result[fieldName][i] !== null
result[fieldName] = incomingData[fieldName].map((incomingRow, i) => {
if (!result[fieldName]) {
result[fieldName] = []
}
const newRow = hasExistingRow ? { ...result[fieldName][i] } : {}
if (!result[fieldName][i]) {
result[fieldName][i] = {}
}
traverseFields({
apiRoute,
depth,
fieldSchema: fieldJSON.fields,
incomingData: row,
populationPromises,
result: newRow,
serverURL,
externallyUpdatedRelationship,
fieldSchema: fieldSchema.fields,
incomingData: incomingRow,
populationsByCollection,
result: result[fieldName][i],
})
return newRow
return result[fieldName][i]
})
}
break
case 'blocks':
if (Array.isArray(incomingData[fieldName])) {
result[fieldName] = incomingData[fieldName].map((incomingBlock, i) => {
const incomingBlockJSON = fieldJSON.blocks[incomingBlock.blockType]
const incomingBlockJSON = fieldSchema.blocks[incomingBlock.blockType]
// Compare the index and id to determine if this block already exists in the result
// If so, we want to use the existing block as the base, otherwise take the incoming block
// Either way, we will traverse the fields of the block to populate relationships
const isExistingBlock =
Array.isArray(result[fieldName]) &&
typeof result[fieldName][i] === 'object' &&
result[fieldName][i] !== null &&
result[fieldName][i].id === incomingBlock.id
if (!result[fieldName]) {
result[fieldName] = []
}
const block = isExistingBlock ? result[fieldName][i] : incomingBlock
if (
!result[fieldName][i] ||
result[fieldName][i].id !== incomingBlock.id ||
result[fieldName][i].blockType !== incomingBlock.blockType
) {
result[fieldName][i] = {
blockType: incomingBlock.blockType,
}
}
traverseFields({
apiRoute,
depth,
externallyUpdatedRelationship,
fieldSchema: incomingBlockJSON.fields,
incomingData: incomingBlock,
populationPromises,
result: block,
serverURL,
populationsByCollection,
result: result[fieldName][i],
})
return block
return result[fieldName][i]
})
} else {
result[fieldName] = []
@@ -92,135 +101,169 @@ export const traverseFields = <T>({
}
traverseFields({
apiRoute,
depth,
fieldSchema: fieldJSON.fields,
externallyUpdatedRelationship,
fieldSchema: fieldSchema.fields,
incomingData: incomingData[fieldName] || {},
populationPromises,
populationsByCollection,
result: result[fieldName],
serverURL,
})
break
case 'upload':
case 'relationship':
if (fieldJSON.hasMany && Array.isArray(incomingData[fieldName])) {
const existingValue = Array.isArray(result[fieldName]) ? [...result[fieldName]] : []
result[fieldName] = Array.isArray(result[fieldName])
? [...result[fieldName]].slice(0, incomingData[fieldName].length)
: []
// Handle `hasMany` relationships
if (fieldSchema.hasMany && Array.isArray(incomingData[fieldName])) {
if (!result[fieldName] || !incomingData[fieldName].length) {
result[fieldName] = []
}
incomingData[fieldName].forEach((relation, i) => {
incomingData[fieldName].forEach((incomingRelation, i) => {
// Handle `hasMany` polymorphic
if (Array.isArray(fieldJSON.relationTo)) {
const existingID = existingValue[i]?.value?.id
if (
existingID !== relation.value ||
existingValue[i]?.relationTo !== relation.relationTo
) {
if (Array.isArray(fieldSchema.relationTo)) {
// if the field doesn't exist on the result, create it
// the value will be populated later
if (!result[fieldName][i]) {
result[fieldName][i] = {
relationTo: relation.relationTo,
relationTo: incomingRelation.relationTo,
}
}
const oldID = result[fieldName][i]?.value?.id
const oldRelation = result[fieldName][i]?.relationTo
const newID = incomingRelation.value
const newRelation = incomingRelation.relationTo
const hasChanged = newID !== oldID || newRelation !== oldRelation
const hasUpdated =
newRelation === externallyUpdatedRelationship?.entitySlug &&
newID === externallyUpdatedRelationship?.id
if (hasChanged || hasUpdated) {
if (!populationsByCollection[newRelation]) {
populationsByCollection[newRelation] = []
}
populationPromises.push(
promise({
id: relation.value,
accessor: 'value',
apiRoute,
collection: relation.relationTo,
depth,
ref: result[fieldName][i],
serverURL,
}),
)
populationsByCollection[newRelation].push({
id: incomingRelation.value,
accessor: 'value',
ref: result[fieldName][i],
})
}
} else {
// Handle `hasMany` monomorphic
const existingID = existingValue[i]?.id
const hasChanged = incomingRelation !== result[fieldName][i]?.id
const hasUpdated =
fieldSchema.relationTo === externallyUpdatedRelationship?.entitySlug &&
incomingRelation === externallyUpdatedRelationship?.id
if (existingID !== relation) {
populationPromises.push(
promise({
id: relation,
accessor: i,
apiRoute,
collection: String(fieldJSON.relationTo),
depth,
ref: result[fieldName],
serverURL,
}),
)
if (hasChanged || hasUpdated) {
if (!populationsByCollection[fieldSchema.relationTo]) {
populationsByCollection[fieldSchema.relationTo] = []
}
populationsByCollection[fieldSchema.relationTo].push({
id: incomingRelation,
accessor: i,
ref: result[fieldName],
})
}
}
})
} else {
// Handle `hasOne` polymorphic
if (Array.isArray(fieldJSON.relationTo)) {
if (Array.isArray(fieldSchema.relationTo)) {
// if the field doesn't exist on the result, create it
// the value will be populated later
if (!result[fieldName]) {
result[fieldName] = {
relationTo: incomingData[fieldName]?.relationTo,
}
}
const hasNewValue =
typeof incomingData[fieldName] === 'object' && incomingData[fieldName] !== null
incomingData[fieldName] &&
typeof incomingData[fieldName] === 'object' &&
incomingData[fieldName] !== null
const hasOldValue =
typeof result[fieldName] === 'object' && result[fieldName] !== null
result[fieldName] &&
typeof result[fieldName] === 'object' &&
result[fieldName] !== null
const newID = hasNewValue
? typeof incomingData[fieldName].value === 'object'
? incomingData[fieldName].value.id
: incomingData[fieldName].value
: ''
const oldID = hasOldValue
? typeof result[fieldName].value === 'object'
? result[fieldName].value.id
: result[fieldName].value
: ''
const newValue = hasNewValue ? incomingData[fieldName].value : ''
const newRelation = hasNewValue ? incomingData[fieldName].relationTo : ''
const oldValue = hasOldValue ? result[fieldName].value : ''
const oldRelation = hasOldValue ? result[fieldName].relationTo : ''
if (newValue !== oldValue || newRelation !== oldRelation) {
if (newValue) {
if (!result[fieldName]) {
result[fieldName] = {
relationTo: newRelation,
}
const hasChanged = newID !== oldID || newRelation !== oldRelation
const hasUpdated =
newRelation === externallyUpdatedRelationship?.entitySlug &&
newID === externallyUpdatedRelationship?.id
// if the new value/relation is different from the old value/relation
// populate the new value, otherwise leave it alone
if (hasChanged || hasUpdated) {
// if the new value is not empty, populate it
// otherwise set the value to null
if (newID) {
if (!populationsByCollection[newRelation]) {
populationsByCollection[newRelation] = []
}
populationPromises.push(
promise({
id: newValue,
accessor: 'value',
apiRoute,
collection: newRelation,
depth,
ref: result[fieldName],
serverURL,
}),
)
populationsByCollection[newRelation].push({
id: newID,
accessor: 'value',
ref: result[fieldName],
})
} else {
result[fieldName] = null
}
} else {
result[fieldName] = null
}
} else {
// Handle `hasOne` monomorphic
const newID: string =
(typeof incomingData[fieldName] === 'string' && incomingData[fieldName]) ||
(typeof incomingData[fieldName] === 'object' &&
incomingData[fieldName] !== null &&
const newID: number | string | undefined =
(incomingData[fieldName] &&
typeof incomingData[fieldName] === 'object' &&
incomingData[fieldName].id) ||
''
incomingData[fieldName]
const oldID: string =
(typeof result[fieldName] === 'string' && result[fieldName]) ||
(typeof result[fieldName] === 'object' &&
result[fieldName] !== null &&
const oldID: number | string | undefined =
(result[fieldName] &&
typeof result[fieldName] === 'object' &&
result[fieldName].id) ||
''
result[fieldName]
if (newID !== oldID) {
const hasChanged = newID !== oldID
const hasUpdated =
fieldSchema.relationTo === externallyUpdatedRelationship?.entitySlug &&
newID === externallyUpdatedRelationship?.id
// if the new value is different from the old value
// populate the new value, otherwise leave it alone
if (hasChanged || hasUpdated) {
// if the new value is not empty, populate it
// otherwise set the value to null
if (newID) {
populationPromises.push(
promise({
id: newID,
accessor: fieldName,
apiRoute,
collection: String(fieldJSON.relationTo),
depth,
ref: result as Record<string, unknown>,
serverURL,
}),
)
if (!populationsByCollection[fieldSchema.relationTo]) {
populationsByCollection[fieldSchema.relationTo] = []
}
populationsByCollection[fieldSchema.relationTo].push({
id: newID,
accessor: fieldName,
ref: result as Record<string, unknown>,
})
} else {
result[fieldName] = null
}
@@ -235,6 +278,4 @@ export const traverseFields = <T>({
}
}
})
return null
}

View File

@@ -0,0 +1,103 @@
import type { PopulationsByCollection, UpdatedDocument } from './types'
export const traverseRichText = ({
externallyUpdatedRelationship,
incomingData,
populationsByCollection,
result,
}: {
externallyUpdatedRelationship?: UpdatedDocument
incomingData: any
populationsByCollection: PopulationsByCollection
result: any
}): any => {
if (Array.isArray(incomingData)) {
if (!result) {
result = []
}
result = incomingData.map((item, index) => {
if (!result[index]) {
result[index] = item
}
return traverseRichText({
externallyUpdatedRelationship,
incomingData: item,
populationsByCollection,
result: result[index],
})
})
} else if (incomingData && typeof incomingData === 'object') {
if (!result) {
result = {}
}
// Remove keys from `result` that do not appear in `incomingData`
// There's likely another way to do this,
// But recursion and references make this very difficult
Object.keys(result).forEach((key) => {
if (!(key in incomingData)) {
delete result[key]
}
})
// Iterate over the keys of `incomingData` and populate `result`
Object.keys(incomingData).forEach((key) => {
if (!result[key]) {
// Instantiate the key in `result` if it doesn't exist
// Ensure its type matches the type of the `incomingData`
// We don't have a schema to check against here
result[key] =
incomingData[key] && typeof incomingData[key] === 'object'
? Array.isArray(incomingData[key])
? []
: {}
: undefined
}
const isRelationship = key === 'value' && 'relationTo' in incomingData
if (isRelationship) {
// or if there are no keys besides id
const needsPopulation =
!result.value ||
typeof result.value !== 'object' ||
(typeof result.value === 'object' &&
Object.keys(result.value).length === 1 &&
'id' in result.value)
const hasChanged =
result &&
typeof result === 'object' &&
result.value.id === externallyUpdatedRelationship?.id
if (needsPopulation || hasChanged) {
if (!populationsByCollection[incomingData.relationTo]) {
populationsByCollection[incomingData.relationTo] = []
}
populationsByCollection[incomingData.relationTo].push({
id:
incomingData[key] && typeof incomingData[key] === 'object'
? incomingData[key].id
: incomingData[key],
accessor: 'value',
ref: result,
})
}
} else {
result[key] = traverseRichText({
externallyUpdatedRelationship,
incomingData: incomingData[key],
populationsByCollection,
result: result[key],
})
}
})
} else {
result = incomingData
}
return result
}

View File

@@ -1,3 +1,18 @@
export type LivePreviewArgs = {}
export type LivePreview = void
export type PopulationsByCollection = {
[slug: string]: Array<{
accessor: number | string
id: number | string
ref: Record<string, unknown>
}>
}
// TODO: import this from `payload/admin/components/utilities/DocumentEvents/types.ts`
export type UpdatedDocument = {
entitySlug: string
id?: number | string
updatedAt: string
}

View File

@@ -1,6 +1,6 @@
{
"name": "payload",
"version": "2.1.0",
"version": "2.4.0",
"description": "Node, React and MongoDB Headless CMS and Application Framework",
"license": "MIT",
"main": "./dist/index.js",
@@ -26,7 +26,7 @@
}
},
"scripts": {
"build": "pnpm copyfiles && pnpm build:swc && pnpm build:types && pnpm build:components && ts-node -T ./scripts/exportPointerFiles.ts",
"build": "pnpm copyfiles && pnpm build:swc && pnpm build:types && pnpm build:components && ts-node -T ../../scripts/exportPointerFiles.ts ../packages/payload dist/exports",
"build:components": "webpack --config dist/admin/components.config.js",
"build:swc": "swc ./src -d ./dist --config-file .swcrc",
"build:types": "tsc --emitDeclarationOnly --outDir dist",

View File

@@ -17,6 +17,7 @@ import { StepNavProvider } from './components/elements/StepNav'
import { AuthProvider } from './components/utilities/Auth'
import { ConfigProvider } from './components/utilities/Config'
import { CustomProvider } from './components/utilities/CustomProvider'
import { DocumentEventsProvider } from './components/utilities/DocumentEvents'
import { I18n } from './components/utilities/I18n'
import { LoadingOverlayProvider } from './components/utilities/LoadingOverlay'
import { LocaleProvider } from './components/utilities/Locale'
@@ -49,11 +50,13 @@ const Root = ({ config: incomingConfig }: { config?: SanitizedConfig }) => {
<LocaleProvider>
<StepNavProvider>
<LoadingOverlayProvider>
<NavProvider>
<CustomProvider>
<Routes />
</CustomProvider>
</NavProvider>
<DocumentEventsProvider>
<NavProvider>
<CustomProvider>
<Routes />
</CustomProvider>
</NavProvider>
</DocumentEventsProvider>
</LoadingOverlayProvider>
</StepNavProvider>
</LocaleProvider>

View File

@@ -5,7 +5,7 @@
position: sticky;
top: 0;
width: 100%;
z-index: 1;
z-index: 2;
display: flex;
align-items: center;

View File

@@ -56,7 +56,12 @@ export const DocumentControls: React.FC<{
const { i18n, t } = useTranslation('general')
const showDotMenu = Boolean(collection && id && !disableActions)
const hasCreatePermission = 'create' in permissions && permissions.create?.permission
const hasDeletePermission = 'delete' in permissions && permissions.delete?.permission
const showDotMenu = Boolean(
collection && id && !disableActions && (hasCreatePermission || hasDeletePermission),
)
return (
<Gutter className={baseClass}>
@@ -203,7 +208,7 @@ export const DocumentControls: React.FC<{
verticalAlign="bottom"
>
<PopupList.ButtonGroup>
{'create' in permissions && permissions?.create?.permission && (
{hasCreatePermission && (
<React.Fragment>
<PopupList.Button
id="action-create"
@@ -217,7 +222,7 @@ export const DocumentControls: React.FC<{
)}
</React.Fragment>
)}
{'delete' in permissions && permissions?.delete?.permission && id && (
{hasDeletePermission && (
<DeleteDocument buttonId="action-delete" collection={collection} id={id} />
)}
</PopupList.ButtonGroup>

View File

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

View File

@@ -90,36 +90,41 @@ const Duplicate: React.FC<Props> = ({ id, collection, slug }) => {
if (result.status === 201 || result.status === 200) {
return json.doc.id
}
json.errors.forEach((error) => toast.error(error.message))
// only show the error if this is the initial request failing
if (!duplicateID) {
json.errors.forEach((error) => toast.error(error.message))
}
return null
}
let duplicateID
let duplicateID: string
let abort = false
const localeErrors = []
if (localization) {
await localization.localeCodes.reduce(async (priorLocalePatch, locale) => {
await priorLocalePatch
if (abort) return
duplicateID = await saveDocument({ id, duplicateID, locale })
const localeResult = await saveDocument({
id,
duplicateID,
locale,
})
duplicateID = localeResult || duplicateID
if (duplicateID && !localeResult) {
localeErrors.push(locale)
}
if (!duplicateID) {
abort = true
}
}, Promise.resolve())
if (abort && duplicateID) {
// delete the duplicate doc to prevent incomplete
await requests.delete(`${serverURL}${api}/${slug}/${duplicateID}`, {
headers: {
'Accept-Language': i18n.language,
},
})
}
} else {
duplicateID = await saveDocument({ id })
}
if (!duplicateID) {
// document was not saved, error toast was displayed
return
}
@@ -128,6 +133,16 @@ const Duplicate: React.FC<Props> = ({ id, collection, slug }) => {
{ autoClose: 3000 },
)
if (localeErrors.length > 0) {
toast.error(
`
${t('error:localesNotSaved', { count: localeErrors.length })}
${localeErrors.join(', ')}
`,
{ autoClose: 5000 },
)
}
setModified(false)
setTimeout(() => {

View File

@@ -70,6 +70,11 @@ $header-height: base(5);
}
}
&__draggable {
@include btn-reset;
position: absolute;
}
&__focalPoint {
position: absolute;
top: 50%;

View File

@@ -164,7 +164,15 @@ export const EditUpload: React.FC<{
/>
</ReactCrop>
) : (
<img alt={t('upload:setFocalPoint')} ref={imageRef} src={fileSrcToUse} />
<img
alt={t('upload:setFocalPoint')}
onLoad={(e) => {
setOriginalHeight(e.currentTarget.naturalHeight)
setOriginalWidth(e.currentTarget.naturalWidth)
}}
ref={imageRef}
src={fileSrcToUse}
/>
)}
{showFocalPoint && (
<DraggableElement
@@ -273,7 +281,7 @@ const DraggableElement = ({
}) => {
const [position, setPosition] = useState({ x: initialPosition.x, y: initialPosition.y })
const [isDragging, setIsDragging] = useState(false)
const dragRef = useRef<HTMLDivElement | undefined>()
const dragRef = useRef<HTMLButtonElement | undefined>()
const getCoordinates = React.useCallback(
(mouseXArg?: number, mouseYArg?: number, recenter?: boolean) => {
@@ -319,7 +327,7 @@ const DraggableElement = ({
return { x, y }
},
[],
[boundsRef, containerRef],
)
const handleMouseDown = (event) => {
@@ -349,7 +357,7 @@ const DraggableElement = ({
setCheckBounds(false)
return
}
}, [getCoordinates, isDragging, checkBounds, setCheckBounds, position.x, position.y])
}, [getCoordinates, isDragging, checkBounds, setCheckBounds, position.x, position.y, onDragEnd])
React.useEffect(() => {
setPosition({ x: initialPosition.x, y: initialPosition.y })
@@ -365,15 +373,16 @@ const DraggableElement = ({
.join(' ')}
onMouseMove={handleMouseMove}
>
<div
<button
className={[`${baseClass}__draggable`, className].filter(Boolean).join(' ')}
onMouseDown={handleMouseDown}
onMouseUp={onDrop}
ref={dragRef}
style={{ left: `${position.x}%`, position: 'absolute', top: `${position.y}%` }}
style={{ left: `${position.x}%`, top: `${position.y}%` }}
type="button"
>
{children}
</div>
</button>
<div />
</div>
)

View File

@@ -38,7 +38,7 @@ const FileDetails: React.FC<Props> = (props) => {
width={width as number}
/>
{isImage(mimeType as string) && (
{isImage(mimeType as string) && mimeType !== 'image/svg+xml' && (
<UploadActions canEdit={canEdit} showSizePreviews={hasImageSizes && doc.filename} />
)}
</div>

View File

@@ -77,7 +77,6 @@
&__sizeOption {
padding: base(0.5);
display: flex;
flex-direction: row;
gap: base(1);
cursor: pointer;
transition: background-color 0.2s ease-in-out;

View File

@@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react'
import React, { useEffect, useMemo, useState } from 'react'
import type { SanitizedCollectionConfig } from '../../../../exports/types'
import type { FileSizes, Upload } from '../../../../uploads/types'
@@ -23,9 +23,49 @@ const sortSizes = (sizes: FileSizes, imageSizes: Upload['imageSizes']) => {
return orderedSizes
}
type PreviewSizeCardProps = {
active: boolean
baseURL: string
meta: FileSizes[0]
name: string
onClick?: () => void
previewSrc: string
}
const PreviewSizeCard: React.FC<PreviewSizeCardProps> = ({
name,
active,
baseURL,
meta,
onClick,
previewSrc,
}) => {
return (
<div
className={[`${baseClass}__sizeOption`, active && `${baseClass}--selected`]
.filter(Boolean)
.join(' ')}
onClick={typeof onClick === 'function' ? onClick : undefined}
onKeyDown={(e) => {
if (typeof onClick !== 'function') return
if (e.key === 'Enter') onClick()
}}
role="button"
tabIndex={0}
>
<div className={`${baseClass}__image`}>
<img alt={meta.filename} src={previewSrc} />
</div>
<div className={`${baseClass}__sizeMeta`}>
<div className={`${baseClass}__sizeName`}>{name}</div>
<Meta {...meta} staticURL={baseURL} />
</div>
</div>
)
}
const PreviewSizes: React.FC<{
collection: SanitizedCollectionConfig
doc?: Data & {
doc: Data & {
sizes?: FileSizes
}
imageCacheTag?: string
@@ -36,58 +76,70 @@ const PreviewSizes: React.FC<{
const { sizes } = doc
const [orderedSizes, setOrderedSizes] = useState<FileSizes>(() => sortSizes(sizes, imageSizes))
const [selectedSize, setSelectedSize] = useState<null | string>(
orderedSizes?.[imageSizes[0]?.name]?.filename ? imageSizes[0]?.name : null,
)
const [selectedSize, setSelectedSize] = useState<null | string>(null)
const generateImageUrl = (filename) => {
return `${staticURL}/${filename}${imageCacheTag ? `?${imageCacheTag}` : ''}`
const generateImageUrl = (doc) => {
if (!doc.filename) return null
if (doc.url) return `${doc.url}${imageCacheTag ? `?${imageCacheTag}` : ''}`
}
useEffect(() => {
setOrderedSizes(sortSizes(sizes, imageSizes))
}, [sizes, imageSizes, imageCacheTag])
const mainPreviewSrc = generateImageUrl(`${orderedSizes[selectedSize]?.filename}`)
const mainPreviewSrc = selectedSize
? generateImageUrl(doc.sizes[selectedSize])
: generateImageUrl(doc)
const originalImage = useMemo(
(): FileSizes[0] => ({
filename: doc.filename,
filesize: doc.filesize,
height: doc.height,
mimeType: doc.mimeType,
width: doc.width,
}),
[doc],
)
const originalFilename = 'Original'
return (
<div className={baseClass}>
<div className={`${baseClass}__imageWrap`}>
<div className={`${baseClass}__meta`}>
<div className={`${baseClass}__sizeName`}>{selectedSize}</div>
<Meta {...(selectedSize && orderedSizes[selectedSize])} staticURL={staticURL} />
<div className={`${baseClass}__sizeName`}>{selectedSize || originalFilename}</div>
<Meta
{...(selectedSize ? orderedSizes[selectedSize] : originalImage)}
staticURL={staticURL}
/>
</div>
<img alt={doc.filename} className={`${baseClass}__preview`} src={mainPreviewSrc} />
</div>
<div className={`${baseClass}__listWrap`}>
<div className={`${baseClass}__list`}>
<PreviewSizeCard
active={!selectedSize}
baseURL={staticURL}
meta={originalImage}
name={originalFilename}
onClick={() => setSelectedSize(null)}
previewSrc={generateImageUrl(doc)}
/>
{Object.entries(orderedSizes).map(([key, val]) => {
const selected = selectedSize === key
const previewSrc = generateImageUrl(val.filename)
const previewSrc = generateImageUrl(val)
if (previewSrc) {
return (
<div
className={[`${baseClass}__sizeOption`, selected && `${baseClass}--selected`]
.filter(Boolean)
.join(' ')}
<PreviewSizeCard
active={selected}
baseURL={staticURL}
key={key}
meta={val}
name={key}
onClick={() => setSelectedSize(key)}
onKeyDown={(e) => {
if (e.keyCode === 13) {
setSelectedSize(key)
}
}}
role="button"
tabIndex={0}
>
<div className={`${baseClass}__image`}>
<img alt={val.filename} src={previewSrc} />
</div>
<div className={`${baseClass}__sizeMeta`}>
<div className={`${baseClass}__sizeName`}>{key}</div>
<Meta {...val} staticURL={staticURL} />
</div>
</div>
previewSrc={previewSrc}
/>
)
}

View File

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

View File

@@ -6,10 +6,14 @@ import DatePicker from '../../../DatePicker'
const baseClass = 'condition-value-date'
const DateField: React.FC<Props> = ({ disabled, onChange, value }) => (
<div className={baseClass}>
<DatePicker onChange={onChange} readOnly={disabled} value={value} />
</div>
)
const DateField: React.FC<Props> = ({ admin, disabled, onChange, value }) => {
const { date } = admin || {}
return (
<div className={baseClass}>
<DatePicker {...date} onChange={onChange} readOnly={disabled} value={value} />
</div>
)
}
export default DateField

View File

@@ -1,4 +1,8 @@
import type { Props as DateType } from '../../../../../components/elements/DatePicker/types'
export type Props = {
admin?: {
date?: DateType
}
disabled?: boolean
onChange: () => void
value: Date

View File

@@ -23,14 +23,26 @@ const baseClass = 'where-builder'
const reduceFields = (fields, i18n) =>
flattenTopLevelFields(fields).reduce((reduced, field) => {
if (typeof fieldTypes[field.type] === 'object') {
const operatorKeys = new Set()
const operators = fieldTypes[field.type].operators.reduce((acc, operator) => {
if (!operatorKeys.has(operator.value)) {
operatorKeys.add(operator.value)
return [
...acc,
{
...operator,
label: i18n.t(`operators:${operator.label}`),
},
]
}
return acc
}, [])
const formattedField = {
label: getTranslation(field.label || field.name, i18n),
value: field.name,
...fieldTypes[field.type],
operators: fieldTypes[field.type].operators.map((operator) => ({
...operator,
label: i18n.t(`operators:${operator.label}`),
})),
operators,
props: {
...field,
},

View File

@@ -10,13 +10,13 @@ import { isComponent } from './types'
const baseClass = 'field-description'
const FieldDescription: React.FC<Props> = (props) => {
const { className, description, value, marginPlacement } = props
const { className, description, marginPlacement, path, value } = props
const { i18n } = useTranslation()
if (isComponent(description)) {
const Description = description
return <Description value={value} />
return <Description path={path} value={value} />
}
if (description) {
@@ -31,7 +31,7 @@ const FieldDescription: React.FC<Props> = (props) => {
.join(' ')}
>
{typeof description === 'function'
? description({ value })
? description({ path, value })
: getTranslation(description, i18n)}
</div>
)

View File

@@ -1,8 +1,12 @@
import React from 'react'
export type DescriptionFunction = (value?: unknown) => string
type Args<T = unknown> = {
path: string
value?: T
}
export type DescriptionFunction<T = unknown> = (args: Args<T>) => string
export type DescriptionComponent = React.ComponentType<{ value: unknown }>
export type DescriptionComponent<T = unknown> = React.ComponentType<Args<T>>
export type Description =
| DescriptionComponent
@@ -13,8 +17,9 @@ export type Description =
export type Props = {
className?: string
description?: Description
marginPlacement?: 'bottom' | 'top'
path?: string
value?: unknown
marginPlacement?: 'top' | 'bottom'
}
export function isComponent(description: Description): description is DescriptionComponent {

View File

@@ -214,6 +214,7 @@ const ArrayFieldType: React.FC<Props> = (props) => {
<FieldDescription
className={`field-description-${path.replace(/\./g, '__')}`}
description={description}
path={path}
value={value}
/>
</header>

View File

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

View File

@@ -215,7 +215,7 @@ const BlocksField: React.FC<Props> = (props) => {
</ul>
)}
</div>
<FieldDescription description={description} value={value} />
<FieldDescription description={description} path={path} value={value} />
</header>
<NullifyLocaleField fieldValue={value} localized={localized} path={path} />
{(rows.length > 0 || (!valid && (showRequired || showMinRows))) && (

View File

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

View File

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

View File

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

View File

@@ -14,8 +14,8 @@ import RenderFields from '../../RenderFields'
import { RowLabel } from '../../RowLabel'
import { WatchChildErrors } from '../../WatchChildErrors'
import withCondition from '../../withCondition'
import './index.scss'
import { fieldBaseClass } from '../shared'
import './index.scss'
const baseClass = 'collapsible-field'
@@ -89,7 +89,6 @@ const CollapsibleField: React.FC<Props> = (props) => {
return (
<div
id={`field-${fieldPreferencesKey}${path ? `-${path.replace(/\./g, '__')}` : ''}`}
className={[
fieldBaseClass,
baseClass,
@@ -98,6 +97,7 @@ const CollapsibleField: React.FC<Props> = (props) => {
]
.filter(Boolean)
.join(' ')}
id={`field-${fieldPreferencesKey}${path ? `-${path.replace(/\./g, '__')}` : ''}`}
>
<WatchChildErrors fieldSchema={fields} path={path} setErrorCount={setErrorCount} />
<Collapsible
@@ -125,7 +125,7 @@ const CollapsibleField: React.FC<Props> = (props) => {
readOnly={readOnly}
/>
</Collapsible>
<FieldDescription description={description} />
<FieldDescription description={description} path={path} />
</div>
)
}

View File

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

View File

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

View File

@@ -77,6 +77,7 @@ const Group: React.FC<Props> = (props) => {
<FieldDescription
className={`field-description-${path.replace(/\./g, '__')}`}
description={description}
path={path}
value={null}
/>
</header>

View File

@@ -9,8 +9,8 @@ import FieldDescription from '../../FieldDescription'
import DefaultLabel from '../../Label'
import useField from '../../useField'
import withCondition from '../../withCondition'
import './index.scss'
import { fieldBaseClass } from '../shared'
import './index.scss'
const baseClass = 'json-field'
@@ -19,13 +19,13 @@ const JSONField: React.FC<Props> = (props) => {
name,
admin: {
className,
components: { Error, Label } = {},
condition,
description,
editorOptions,
readOnly,
style,
width,
components: { Error, Label } = {},
} = {},
label,
path: pathFromProps,
@@ -69,8 +69,8 @@ const JSONField: React.FC<Props> = (props) => {
)
useEffect(() => {
setStringValue(JSON.stringify(initialValue, null, 2))
}, [initialValue])
setStringValue(JSON.stringify(value ? value : initialValue, null, 2))
}, [initialValue, value])
return (
<div
@@ -97,7 +97,7 @@ const JSONField: React.FC<Props> = (props) => {
readOnly={readOnly}
value={stringValue}
/>
<FieldDescription description={description} value={value} />
<FieldDescription description={description} path={path} value={value} />
</div>
)
}

View File

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

View File

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

View File

@@ -8,13 +8,15 @@ import { optionIsObject } from '../../../../../fields/config/types'
import DefaultError from '../../Error'
import FieldDescription from '../../FieldDescription'
import DefaultLabel from '../../Label'
import { fieldBaseClass } from '../shared'
import RadioInput from './RadioInput'
import './index.scss'
import { fieldBaseClass } from '../shared'
const baseClass = 'radio-group'
export type RadioGroupInputProps = Omit<RadioField, 'type'> & {
Error?: React.ComponentType<any>
Label?: React.ComponentType<any>
className?: string
description?: Description
errorMessage?: string
@@ -28,13 +30,13 @@ export type RadioGroupInputProps = Omit<RadioField, 'type'> & {
style?: React.CSSProperties
value?: string
width?: string
Error?: React.ComponentType<any>
Label?: React.ComponentType<any>
}
const RadioGroupInput: React.FC<RadioGroupInputProps> = (props) => {
const {
name,
Error,
Label,
className,
description,
errorMessage,
@@ -49,8 +51,6 @@ const RadioGroupInput: React.FC<RadioGroupInputProps> = (props) => {
style,
value,
width,
Error,
Label,
} = props
const ErrorComp = Error || DefaultError
@@ -103,7 +103,7 @@ const RadioGroupInput: React.FC<RadioGroupInputProps> = (props) => {
)
})}
</ul>
<FieldDescription description={description} value={value} />
<FieldDescription description={description} path={path} value={value} />
</div>
)
}

View File

@@ -12,13 +12,13 @@ const RadioGroup: React.FC<Props> = (props) => {
name,
admin: {
className,
components: { Error, Label } = {},
condition,
description,
layout = 'horizontal',
readOnly,
style,
width,
components: { Error, Label } = {},
} = {},
label,
options,
@@ -44,6 +44,8 @@ const RadioGroup: React.FC<Props> = (props) => {
return (
<RadioGroupInput
Error={Error}
Label={Label}
className={className}
description={description}
errorMessage={errorMessage}
@@ -58,8 +60,6 @@ const RadioGroup: React.FC<Props> = (props) => {
style={style}
value={value}
width={width}
Error={Error}
Label={Label}
/>
)
}

View File

@@ -40,13 +40,14 @@ const Relationship: React.FC<Props> = (props) => {
admin: {
allowCreate = true,
className,
components: { Error, Label } = {},
condition,
description,
isSortable = true,
readOnly,
sortOptions,
style,
width,
components: { Error, Label } = {},
} = {},
filterOptions,
hasMany,
@@ -139,7 +140,14 @@ const Relationship: React.FC<Props> = (props) => {
if (resultsFetched < 10) {
const collection = collections.find((coll) => coll.slug === relation)
const fieldToSearch = collection?.admin?.useAsTitle || 'id'
let fieldToSearch = collection?.defaultSort || collection?.admin?.useAsTitle || 'id'
if (!searchArg) {
if (typeof sortOptions === 'string') {
fieldToSearch = sortOptions
} else if (sortOptions?.[relation]) {
fieldToSearch = sortOptions[relation]
}
}
const query: {
[key: string]: unknown
@@ -236,6 +244,7 @@ const Relationship: React.FC<Props> = (props) => {
locale,
filterOptionsResult,
serverURL,
sortOptions,
api,
i18n,
config,
@@ -252,7 +261,7 @@ const Relationship: React.FC<Props> = (props) => {
(searchArg: string, valueArg: Value | Value[]) => {
if (search !== searchArg) {
setLastLoadedPage({})
updateSearch(searchArg, valueArg)
updateSearch(searchArg, valueArg, searchArg !== '')
}
},
[search, updateSearch],
@@ -518,7 +527,7 @@ const Relationship: React.FC<Props> = (props) => {
</div>
)}
{errorLoading && <div className={`${baseClass}__error-loading`}>{errorLoading}</div>}
<FieldDescription description={description} value={value} />
<FieldDescription description={description} path={path} value={value} />
</div>
)
}

View File

@@ -1,12 +1,34 @@
import React from 'react'
import React, { useMemo } from 'react'
import type { RichTextField } from '../../../../../fields/config/types'
import type { RichTextAdapter } from './types'
const RichText: React.FC<RichTextField> = (props) => {
const RichText: React.FC<RichTextField> = (fieldprops) => {
// eslint-disable-next-line react/destructuring-assignment
const editor: RichTextAdapter = props.editor
return <editor.FieldComponent {...props} />
const editor: RichTextAdapter = fieldprops.editor
const isLazy = 'LazyFieldComponent' in editor
const ImportedFieldComponent: React.FC<any> = useMemo(() => {
return isLazy
? React.lazy(() => {
return editor.LazyFieldComponent().then((resolvedComponent) => ({
default: resolvedComponent,
}))
})
: null
}, [editor, isLazy])
if (isLazy) {
return (
ImportedFieldComponent && (
<React.Suspense>
<ImportedFieldComponent {...fieldprops} />
</React.Suspense>
)
)
}
return <editor.FieldComponent {...fieldprops} />
}
export default RichText

View File

@@ -1,4 +1,7 @@
import type { JSONSchema4 } from 'json-schema'
import type { PayloadRequest } from '../../../../../express/types'
import type { RequestContext } from '../../../../../express/types'
import type { RichTextField, Validate } from '../../../../../fields/config/types'
import type { CellComponentProps } from '../../../views/collections/List/Cell/types'
@@ -10,15 +13,11 @@ export type RichTextFieldProps<
path?: string
}
export type RichTextAdapter<
type RichTextAdapterBase<
Value extends object = object,
AdapterProps = any,
ExtraFieldProperties = {},
> = {
CellComponent: React.FC<
CellComponentProps<RichTextField<Value, AdapterProps, ExtraFieldProperties>>
>
FieldComponent: React.FC<RichTextFieldProps<Value, AdapterProps, ExtraFieldProperties>>
afterReadPromise?: ({
field,
incomingEditorState,
@@ -28,12 +27,22 @@ export type RichTextAdapter<
incomingEditorState: Value
siblingDoc: Record<string, unknown>
}) => Promise<void> | null
outputSchema?: ({
field,
isRequired,
}: {
field: RichTextField<Value, AdapterProps, ExtraFieldProperties>
isRequired: boolean
}) => JSONSchema4
populationPromise?: (data: {
context: RequestContext
currentDepth?: number
depth: number
field: RichTextField<Value, AdapterProps, ExtraFieldProperties>
findMany: boolean
flattenLocales: boolean
overrideAccess?: boolean
populationPromises: Promise<void>[]
req: PayloadRequest
showHiddenFields: boolean
siblingDoc: Record<string, unknown>
@@ -45,3 +54,25 @@ export type RichTextAdapter<
RichTextField<Value, AdapterProps, ExtraFieldProperties>
>
}
export type RichTextAdapter<
Value extends object = object,
AdapterProps = any,
ExtraFieldProperties = {},
> = RichTextAdapterBase<Value, AdapterProps, ExtraFieldProperties> &
(
| {
CellComponent: React.FC<
CellComponentProps<RichTextField<Value, AdapterProps, ExtraFieldProperties>>
>
FieldComponent: React.FC<RichTextFieldProps<Value, AdapterProps, ExtraFieldProperties>>
}
| {
LazyCellComponent: () => Promise<
React.FC<CellComponentProps<RichTextField<Value, AdapterProps, ExtraFieldProperties>>>
>
LazyFieldComponent: () => Promise<
React.FC<RichTextFieldProps<Value, AdapterProps, ExtraFieldProperties>>
>
}
)

View File

@@ -14,6 +14,8 @@ import { fieldBaseClass } from '../shared'
import './index.scss'
export type SelectInputProps = Omit<SelectField, 'options' | 'type' | 'value'> & {
Error?: React.ComponentType<any>
Label?: React.ComponentType<any>
className?: string
description?: Description
errorMessage?: string
@@ -29,12 +31,12 @@ export type SelectInputProps = Omit<SelectField, 'options' | 'type' | 'value'> &
style?: React.CSSProperties
value?: string | string[]
width?: string
Error?: React.ComponentType<any>
Label?: React.ComponentType<any>
}
const SelectInput: React.FC<SelectInputProps> = (props) => {
const {
Error,
Label,
className,
defaultValue,
description,
@@ -52,8 +54,6 @@ const SelectInput: React.FC<SelectInputProps> = (props) => {
style,
value,
width,
Error,
Label,
} = props
const { i18n } = useTranslation()
@@ -111,7 +111,7 @@ const SelectInput: React.FC<SelectInputProps> = (props) => {
showError={showError}
value={valueToRender as Option}
/>
<FieldDescription description={description} value={value} />
<FieldDescription description={description} path={path} value={value} />
</div>
)
}

View File

@@ -95,7 +95,7 @@ const TabsField: React.FC<Props> = (props) => {
: existingPreferences?.fields?.[tabsPrefKey]?.tabIndex
setActiveTabIndex(initialIndex || 0)
}
getInitialPref()
void getInitialPref()
}, [path, indexPath, getPreference, preferencesKey, tabsPrefKey])
const handleTabChange = useCallback(
@@ -166,7 +166,9 @@ const TabsField: React.FC<Props> = (props) => {
className={[
`${baseClass}__tab`,
activeTabConfig.label &&
`${baseClass}__tab-${toKebabCase(getTranslation(activeTabConfig.label, i18n))}`,
`${baseClass}__tabConfigLabel-${toKebabCase(
getTranslation(activeTabConfig.label, i18n),
)}`,
]
.filter(Boolean)
.join(' ')}
@@ -175,6 +177,7 @@ const TabsField: React.FC<Props> = (props) => {
className={`${baseClass}__description`}
description={activeTabConfig.description}
marginPlacement="bottom"
path={path}
/>
<RenderFields
fieldSchema={activeTabConfig.fields.map((field) => {
@@ -191,7 +194,11 @@ const TabsField: React.FC<Props> = (props) => {
fieldTypes={fieldTypes}
forceRender={forceRender}
indexPath={indexPath}
key={String(activeTabConfig.label)}
key={
activeTabConfig.label
? getTranslation(activeTabConfig.label, i18n)
: activeTabConfig['name']
}
margins="small"
permissions={
tabHasName(activeTabConfig) && permissions?.[activeTabConfig.name]

View File

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

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