Compare commits

..

111 Commits

Author SHA1 Message Date
Elliot DeNolf
e2d36c3cab chore(release): db-mongodb/1.0.7 [skip ci] 2023-11-08 14:53:05 -05:00
Elliot DeNolf
0e682a32c3 chore(release): payload/2.1.0 [skip ci] 2023-11-08 14:51:29 -05:00
Hulpoi George-Valentin
266c3274d0 feat: Custom Error, Label, and before/after field components (#3747)
Co-authored-by: Dan Ribbens <dan.ribbens@gmail.com>
2023-11-08 14:40:31 -05:00
Jarrod Flesch
67b3baaa44 fix: vite not replacing env vars correctly when building 2023-11-08 14:23:58 -05:00
Jarrod Flesch
55659c7c36 chore(docs): imporoves usability of useAuth and exports useTableColumns 2023-11-08 14:23:22 -05:00
Jørgen Kalsnes Hagen
6a0a859563 feat: add internationalization (i18n) to locales (#4005) 2023-11-08 12:56:15 -05:00
Dan Ribbens
57da3c99a7 fix: error on graphql multiple queries (#3985) 2023-11-08 12:38:25 -05:00
Elliot DeNolf
611438177b ci: split e2e tests into 8 parts 2023-11-08 12:35:05 -05:00
Jacob Fletcher
d068ef7e24 fix: injects array and block ids into fieldSchemaToJSON (#4043) 2023-11-08 12:34:51 -05:00
Jacob Fletcher
7a9af4417a fix: polymorphic hasMany relationships missing in postgres admin (#4053) 2023-11-08 12:31:07 -05:00
Patrik
8d14c213c8 fix: resets list filter row when the filter on field is changed (#3956)
Co-authored-by: Jarrod Flesch <jarrodmflesch@gmail.com>
2023-11-08 08:31:01 -05:00
Jarrod Flesch
182c57b191 fix: hasMany number and select fields unable to save within arrays (#4047) 2023-11-07 22:29:41 -05:00
Elliot DeNolf
15459fb8e3 ci: add workflow changes to needs_build filter 2023-11-07 21:11:32 -05:00
Elliot DeNolf
3ca71c4def ci: increase v8 memory allocation 2023-11-07 21:06:18 -05:00
Elliot DeNolf
64136a6b17 test(plugin-sentry): add test components (#4042) 2023-11-07 16:02:24 -05:00
Jarrod Flesch
acba5e482b fix: focal and cropping issues, adds test (#4039) 2023-11-07 15:20:57 -05:00
Elliot DeNolf
571f190f34 chore(plugin-sentry): use proper refs instead of from dist 2023-11-07 15:17:07 -05:00
Take Weiland
131d89c3f5 fix: handle invalid tokens in refresh token operation (#3647)
* fix: handle invalid tokens in refresh token operation

* fix: check for any falsy user values instead of just nullish in token refresh
2023-11-07 14:55:35 -05:00
Elliot DeNolf
55c38a8934 test: plugin-sentry suite (#4040) 2023-11-07 13:51:40 -05:00
Elliot DeNolf
2abb46f4f1 ci: add plugin-sentry build 2023-11-07 12:45:45 -05:00
Elliot DeNolf
f41780ef33 chore: sync pnpm-lock.yaml 2023-11-07 12:45:25 -05:00
Elliot DeNolf
105392cf07 Merge pull request #3671 from payloadcms/chore/plugin-sentry
chore: imports sentry plugin
2023-11-07 12:42:56 -05:00
Elliot DeNolf
fa2e68ad1c chore: force pnpm-lock.yaml 2023-11-07 12:42:28 -05:00
Elliot DeNolf
2053e4eeab chore(plugin-sentry): more cleanup 2023-11-07 12:41:43 -05:00
Elliot DeNolf
432794fa55 chore(plugin-sentry): format 2023-11-07 12:25:47 -05:00
Elliot DeNolf
6787f0dfd9 chore(plugin-sentry): fix eslint errors 2023-11-07 12:25:25 -05:00
Elliot DeNolf
0b0a40c9fb chore(plugin-sentry): cleanup after import 2023-11-07 12:18:14 -05:00
Elliot DeNolf
95c43a2ab4 chore: sync payload package readme 2023-11-07 12:05:51 -05:00
Jarrod Flesch
f4037a6bdc chore: readme boldness 2023-11-07 09:13:35 -05:00
Jacob Fletcher
c4d173ae0f chore: updates CODEOWNERS (#4031) 2023-11-07 09:05:35 -05:00
Patrik
3e5149bc43 Merge pull request #3987 from SimYunSup/fix/#3986
fix: Updates checkbox API views
2023-11-06 15:44:41 -05:00
Alessio Gravili
17f7b94555 chore: improve test suites, upgrade jest and playwright, add debug utilities for lexical (#4011)
* feat(richtext-lexical): 'bottom' position value for plugins

* feat: TestRecorderFeature

* chore: restructuring to seed and clear db before each test

* chore: make sure all tests pass

* chore: make sure indexes are created in seed.ts - this fixes one erroring test

* chore: speed up test runs through db snapshots

* chore: support drizzle when resetting db

* chore: simplify seeding process, by moving boilerplate db reset / snapshot logic into a wrapper function

* chore: add new seeding process to admin test suite

* chore(deps): upgrade jest and playwright

* chore: make sure mongoose-specific tests are not skipped

* chore: fix point test, which was depending on another test (that's bad!)

* chore: fix incorrect import

* chore: remove unnecessary comments

* chore: clearly label lexicalE2E test file as todo

* chore: simplify seed logic

* chore: move versions test suite to new seed system
2023-11-06 16:38:40 +01:00
Elliot DeNolf
04850694c1 chore(deps): bump uuid to 9 (#4014) 2023-11-06 08:58:41 -05:00
Elliot DeNolf
eb42c031ef fix: parse predefined migrations via file arg or name prefix (#4001) 2023-11-03 19:26:25 -04:00
Elliot DeNolf
dc253676e8 docs: add latest tag to all mentions of create-payload-app [no ci] (#3998) 2023-11-03 17:18:03 -04:00
Elliot DeNolf
926372f15a chore: add CODEOWNERS file 2023-11-03 17:05:14 -04:00
Elliot DeNolf
c2f379f139 chore(release): db-postgres/0.1.12 [skip ci] 2023-11-03 16:23:12 -04:00
Elliot DeNolf
1a523eff98 chore(release): db-mongodb/1.0.6 [skip ci] 2023-11-03 16:22:59 -04:00
Elliot DeNolf
f320a87f92 chore(release): payload/2.0.15 [skip ci] 2023-11-03 16:21:13 -04:00
Elliot DeNolf
d1a0822f80 fix: properly load temp files into buffer (#3996) 2023-11-03 16:02:41 -04:00
James Mikrut
da533d6b64 Merge pull request #3981 from payloadcms/fix/autosave
fix: autosave updating data in unrelated docs
2023-11-03 13:59:39 -04:00
Jessica Chowdhury
fb3b95e52d docs: update default autosave interval 2023-11-03 17:08:11 +00:00
Jessica Chowdhury
a9d96b1037 fix: global autosave and relevant e2e test 2023-11-03 16:38:43 +00:00
Jessica Chowdhury
ea7ce6fd97 test: adds autosave test 2023-11-03 15:59:06 +00:00
Jessica Chowdhury
354b73c3aa Merge branch 'main' into fix/autosave 2023-11-03 15:57:23 +00:00
Patrik
96fc3df532 chore: ellipse long error messages, add title attribute (#3812)
Co-authored-by: Jarrod Flesch <30633324+JarrodMFlesch@users.noreply.github.com>
2023-11-03 10:40:58 -04:00
Jarrod Flesch
c7a315a7d1 fix: passes correct data to buildStateFromSchema on account page (#3984)
* chore: fixes e2e tests
2023-11-03 10:30:36 -04:00
Yunsup Sim
b008b6c646 fix: Update API Views 2023-11-03 18:38:06 +09:00
Jessica Chowdhury
b722f202af fix: autosave updating data in unrelated docs 2023-11-02 17:54:25 +00:00
Jessica Chowdhury
720760225f docs: adds section on querying and filtering polymorphic relationship fields (#3976) 2023-11-02 13:27:21 -04:00
Jacob Fletcher
f7d4c04f65 chore: adds e2e tests for nested views (#3962) 2023-11-02 13:13:29 -04:00
Patrik
6b1b4ffd27 fix: better error handling within parseCookies (#3720) 2023-11-02 09:01:01 -04:00
Jessica Chowdhury
6325b334ec chore(docs): adds section on swap space and nextjs incompatibilities with the local api (#3975) 2023-11-02 08:40:24 -04:00
Alessio Gravili
79b1b88a2f chore: Better Lexical documentation, minor improvements to HTML converter feature (#3933)
* docs: add html serialization docs

* chore: add .md to the .editorconfig

* chore: add new consolidateHTMLConverters function

* docs: add more documentation about serializing HTML

* docs: document creation of headless editors, editor => markdown conversion, markdown => editor conversion and editor => lexical conversion

* docs: improve wording

* docs: add missing comma

* docs: add rest of the missing docs

* docs: various improvements
2023-11-02 07:44:18 +01:00
Jacob Fletcher
b2beec302f chore: unable to boot config and endpoints test suites (#3969) 2023-11-01 18:02:25 -04:00
Jacob Fletcher
fbc2064a10 chore: deflakes e2e tests (#3970) 2023-11-01 17:26:07 -04:00
Daniel Kirchhof
900a9eafeb fix: prevent sort from saving a new version in version list view (#3944) 2023-11-01 15:11:10 -04:00
Jacob Fletcher
06cd52b622 fix: sort document tabs by order (#3968) 2023-11-01 14:59:47 -04:00
Jarrod Flesch
c7ec557466 chore(docs): server code aliasing cleanup (#3967) 2023-11-01 13:30:34 -04:00
Jacob Fletcher
4c587acc10 docs: fixes custom component property names (#3966) 2023-11-01 13:20:03 -04:00
Jarrod Flesch
6f39b809b3 chore(docs): vite aliasing and extending (#3965) 2023-11-01 12:57:41 -04:00
Jarrod Flesch
796669279a fix: exclude files from dev bundle if aliased (#3957) 2023-11-01 11:41:35 -04:00
Alessio Gravili
886fca8e37 Merge pull request #3964 from payloadcms/docs/preview-docs-2
docs: add "previewing docs" section to the contributing.md
2023-11-01 16:10:21 +01:00
Alessio Gravili
30db52ac45 docs: add "previewing docs" section to the contributing.md 2023-11-01 16:09:21 +01:00
Jacob Fletcher
f04a18a258 chore: fixes flaky e2e tests (#3961) 2023-11-01 10:22:54 -04:00
Jarrod Flesch
cdc10be1a2 fix: do not display field if read permission is false - admin panel ui (#3949) 2023-11-01 10:21:19 -04:00
Jacob Fletcher
a5b2333140 fix: deeply merges view configs (#3954) 2023-11-01 08:58:42 -04:00
Jessica Chowdhury
afe1834f9a chore: updates hover styles for list control and file detail buttons (#3757) 2023-11-01 08:36:07 -04:00
Jarrod Flesch
3d7a2de00d chore: allow overrides to be passed into ReactDatePicker from DateTimeInput (#3937) 2023-11-01 08:33:26 -04:00
Jarrod Flesch
5ea88bb47d fix: block row removal w/ db-postgres adapter (#3951) 2023-11-01 08:32:02 -04:00
Jarrod Flesch
386fe0741d chore: append globalType inside global version operations (#3903) 2023-10-31 16:43:26 -04:00
Patrik
b6d9a2021f fix: vertical alignment in step nav when using larger logos (#3955) 2023-10-31 15:18:39 -04:00
Jacob Fletcher
1f8f173741 fix: findVersions pagination (#3906) 2023-10-31 09:33:13 -04:00
Jarrod Flesch
36576f152a fix: field paths being mutated if they ended with the req.locale (#3936) 2023-10-31 08:53:00 -04:00
James Mikrut
4ea8ace4c8 Merge pull request #3940 from payloadcms/fix/dataloader-parallel-requests
fix: ensures dataloader does not run requests in parallel
2023-10-30 22:19:00 -04:00
James
3a83f08518 chore: corrects dataloader comment 2023-10-30 17:15:24 -04:00
James
4607dbf976 fix: ensures dataloader does not run requests in parallel 2023-10-30 17:13:40 -04:00
Jessica Chowdhury
94d8d2790b chore: adds missing "menu" translation (#3816) 2023-10-30 16:01:32 -04:00
Mikko Vänskä
e6b9fb4fab docs: adds link to tabs field in fields overview (#3909) 2023-10-30 15:09:26 -04:00
Jacob Fletcher
6918be20ba fix(templates): serializes internal links (#3935) 2023-10-30 15:02:01 -04:00
Piotr Rogowski
e4881bb02f fix(i18n): polish translations (#3934) 2023-10-30 14:59:50 -04:00
Elliot DeNolf
ed748102d6 chore: clean up changelog 2023-10-30 12:40:19 -04:00
Elliot DeNolf
88627f22e5 chore(release): bundler-webpack/1.0.5 [skip ci] 2023-10-30 12:30:29 -04:00
Elliot DeNolf
a2a44a81f2 chore(release): richtext-slate/1.1.0 [skip ci] 2023-10-30 12:29:43 -04:00
Elliot DeNolf
b2b0f10935 chore(release): richtext-lexical/0.1.16 [skip ci] 2023-10-30 12:29:08 -04:00
Jacob Fletcher
a67a9379ce Merge remote-tracking branch 'plugin-sentry/main' into chore/plugin-sentry 2023-10-15 02:49:33 -04:00
Jessica Boezwinkle
3e9826d7ae chore: updates readme 2023-08-03 11:42:26 +01:00
Jessica Boezwinkle
f8a095e7f4 chore: updates readme 2023-08-03 11:41:00 +01:00
Jessica Chowdhury
9c046d049a Removes WIP disclaimer from README 2023-07-21 15:22:30 +01:00
Jessica Boezwinkle
0871f299ef chore: update test suite 2023-07-20 14:47:03 +01:00
Jessica Chowdhury
a074a5b376 Minor README.md tweak 2023-07-18 13:41:25 +01:00
Jessica Chowdhury
abf3378441 Update README.md to emphasize that it is WIP 2023-07-18 13:38:20 +01:00
Jessica Chowdhury
c1b41b75c4 Merge pull request #2 from payloadcms/chore/update-types
chore: updates types and readme
2023-07-17 16:05:36 +01:00
Jessica Boezwinkle
19706617e5 chore: uses existing express app 2023-06-28 12:02:41 +01:00
Jessica Boezwinkle
cc28df1324 chore: adds test 2023-06-23 17:00:37 +01:00
Jessica Boezwinkle
607d345eb2 chore: misc update 2023-06-23 16:49:40 +01:00
Jessica Boezwinkle
e41515564b chore: actually commits jsdoc additions 2023-06-23 16:05:01 +01:00
Jessica Boezwinkle
f615b8cdf2 chore: updates types and readme 2023-06-23 16:02:01 +01:00
Elliot DeNolf
9182e79c2d Merge pull request #1 from payloadcms/feedback
feat: improvements
2023-06-23 10:36:14 -04:00
Elliot DeNolf
ed95722a50 test: add test suite, run in workflow and prepublishOnly 2023-06-23 10:32:44 -04:00
Jessica Boezwinkle
d7adb094a5 chore: updates index.ts and Payload version 2023-06-23 12:22:30 +01:00
Jessica Boezwinkle
717e01bbbf chore: removes yarn test 2023-06-22 11:43:45 +01:00
Jessica Boezwinkle
3987953947 chore: adds enable option and misc feedback changes 2023-06-22 11:28:48 +01:00
Elliot DeNolf
b7c750220e chore: general feedback 2023-06-21 12:00:10 -04:00
Jessica Boezwinkle
33f9357e58 demo: updates depenedencies 2023-06-21 12:13:51 +01:00
Jessica Boezwinkle
9109f7094b chore: adds requestHandler and error options, adds test errors to demo 2023-06-21 12:10:21 +01:00
Jessica Chowdhury
92bd914966 Update README.md 2023-06-21 12:03:09 +01:00
Jessica Chowdhury
b210551e96 Update README.md 2023-06-21 12:01:07 +01:00
Jessica Chowdhury
5e64e52dab chore: Updates README 2023-06-16 12:13:32 +01:00
Jessica Boezwinkle
90e9dd7f47 setup: builds plugin and demo 2023-06-16 11:58:47 +01:00
Jessica Boezwinkle
f867d7a615 setup: initial commit 2023-06-16 11:55:13 +01:00
346 changed files with 8350 additions and 2106 deletions

View File

@@ -23,3 +23,7 @@ indent_size = 2
[*.json]
indent_style = space
indent_size = 2
[*.md]
indent_style = space
indent_size = 2

50
.github/CODEOWNERS vendored Normal file
View File

@@ -0,0 +1,50 @@
# Order matters. The last matching pattern takes precedence.
### Catch-all ###
* @denolfe @jmikrut @DanRibbens
.* @denolfe @jmikrut @DanRibbens
### Core ###
/packages/payload/ @denolfe @jmikrut @DanRibbens
/packages/payload/src/uploads/ @denolfe
/packages/payload/src/admin/ @jmikrut @jacobsfletch @JarrodMFlesch
### Adapters ###
/packages/bundler-*/ @denolfe @jmikrut @DanRibbens @JarrodMFlesch
/packages/db-*/ @denolfe @jmikrut @DanRibbens
/packages/richtext-*/ @denolfe @jmikrut @DanRibbens @AlessioGr
### Plugins ###
/packages/plugin-*/ @denolfe @jmikrut @DanRibbens @jacobsfletch @JarrodMFlesch @AlessioGr
/packages/plugin-cloud*/ @denolfe
/packages/plugin-form-builder/ @jacobsfletch
/packages/plugin-live-preview*/ @jacobsfletch
/packages/plugin-nested-docs/ @jacobsfletch
/packages/plugin-password-protection/ @jmikrut
/packages/plugin-redirects/ @jacobsfletch
/packages/plugin-search/ @jacobsfletch
/packages/plugin-sentry/ @JessChowdhury
/packages/plugin-seo/ @jacobsfletch
/packages/plugin-stripe/ @jacobsfletch
/packages/plugin-zapier/ @JarrodMFlesch
### Examples ###
/examples/ @jacobsfletch
/examples/testing/ @JarrodMFlesch
/examples/email/ @JessChowdhury
/examples/whitelabel/ @JessChowdhury
### Templates ###
/templates/ @jacobsfletch
/templates/blank/ @denolfe
### Misc ###
/packages/create-payload-app/ @denolfe
/packages/eslint-config-payload/ @denolfe
/packages/payload-admin-bar/ @jacobsfletch
### Root ###
/package.json @denolfe
/scripts/ @denolfe
/.github/ @denolfe
/.github/CODEOWNERS @denolfe

View File

@@ -23,6 +23,7 @@ jobs:
with:
filters: |
needs_build:
- '.github/workflows/**'
- 'packages/**'
- 'test/**'
- 'pnpm-lock.yaml'
@@ -131,6 +132,7 @@ jobs:
- name: Integration Tests
run: pnpm test:int
env:
NODE_OPTIONS: --max-old-space-size=8096
PAYLOAD_DATABASE: ${{ matrix.database }}
POSTGRES_URL: ${{ env.POSTGRES_URL }}
@@ -140,7 +142,7 @@ jobs:
strategy:
fail-fast: false
matrix:
part: [1/4, 2/4, 3/4, 4/4]
part: [1/8, 2/8, 3/8, 4/8, 5/8, 6/8, 7/8, 8/8]
steps:
- name: Use Node.js 18
@@ -253,6 +255,7 @@ jobs:
- plugin-form-builder
- plugin-nested-docs
- plugin-search
- plugin-sentry
steps:
- name: Use Node.js 18

7
.vscode/launch.json vendored
View File

@@ -47,6 +47,13 @@
"request": "launch",
"type": "node-terminal"
},
{
"command": "pnpm run dev uploads",
"cwd": "${workspaceFolder}",
"name": "Run Dev Uploads",
"request": "launch",
"type": "node-terminal"
},
{
"command": "PAYLOAD_BUNDLER=vite pnpm run dev fields",
"cwd": "${workspaceFolder}",

View File

@@ -1,86 +1,76 @@
## [2.0.14](https://github.com/payloadcms/payload/compare/v2.0.13...v2.0.14) (2023-10-30)
## [2.1.0](https://github.com/payloadcms/payload/compare/v2.0.15...v2.1.0) (2023-11-08)
### Features
* add textarea field ([474436e](https://github.com/payloadcms/payload/commit/474436e9ab70374a2cfdc1bede06f21052fb78c9))
* adds basePrice to payment field ([12c85d7](https://github.com/payloadcms/payload/commit/12c85d7707ab85cc7a28b43bad1b5085b93fb3bf))
* adds cc field to email ([2c8ea53](https://github.com/payloadcms/payload/commit/2c8ea533afd98d27ee9787d8f0fc03d2cb4a7198))
* adds email-from-name & reply-to-name fields to Emails ([3816431](https://github.com/payloadcms/payload/commit/38164318932a10bc483f0860c5294c863f83bf7a))
* adds image and video elements to sticky list and content grid rich text ([c2c2bb7](https://github.com/payloadcms/payload/commit/c2c2bb7f2d1c54e923b79a91a7fcb72128a4034c))
* adds indentation option to richText ([67df834](https://github.com/payloadcms/payload/commit/67df834f314b4b243c2dea45a3c9a6c88975761d))
* adds payment fields to form builder [#43](https://github.com/payloadcms/payload/issues/43) ([2f00aef](https://github.com/payloadcms/payload/commit/2f00aef66c8c635c1ab579205c1967eec1a4bd12))
* adds title to forms ([6e2f433](https://github.com/payloadcms/payload/commit/6e2f43394d6b2dfb6134ddc4976913689ce9273d))
* builds breadcrumbs plugin ([2a2a2e3](https://github.com/payloadcms/payload/commit/2a2a2e3a2f90350bfd41a12d1d7fc05766128606))
* builds demo ([f05462e](https://github.com/payloadcms/payload/commit/f05462efd348b07aba55f7f2f4b78e1fe34bd4a9))
* builds form builder plugin ([eeecbbe](https://github.com/payloadcms/payload/commit/eeecbbedb616c44490f364d5d29cd29d1f3d8370))
* builds getPaymentTotal utility ([65a53c7](https://github.com/payloadcms/payload/commit/65a53c7d76927aea204234189f67c57a59785275))
* configures tsc and fixes types ([8b69625](https://github.com/payloadcms/payload/commit/8b69625b57895f14e73e26d3d564ea804877667c))
* dynamic pricing and payment handling [#43](https://github.com/payloadcms/payload/issues/43) ([b749f89](https://github.com/payloadcms/payload/commit/b749f89c307cf34497bd453bee70ca607684ebab))
* explicitly exports fields for reuse [#35](https://github.com/payloadcms/payload/issues/35) ([#39](https://github.com/payloadcms/payload/issues/39)) ([f8da32a](https://github.com/payloadcms/payload/commit/f8da32a9bd12694db5fb02c579c6d38d94883913))
* extends form builder plugin ([3c58e51](https://github.com/payloadcms/payload/commit/3c58e51d17c7a5b89ae28649c2b61920f5bf6f8d))
* field localization ([b4d1eaf](https://github.com/payloadcms/payload/commit/b4d1eaf1fb02053a3b7d65fe0ec4f36828c6c146))
* form submission emails ([db29b07](https://github.com/payloadcms/payload/commit/db29b07b82214d73423b17f5bcc0377731225238))
* integrates stripe api into form builder [#43](https://github.com/payloadcms/payload/issues/43) ([ec60147](https://github.com/payloadcms/payload/commit/ec60147aff2b165943e957cd59c0b68282e3b5f4))
* modifies form builder api ([24a4bc6](https://github.com/payloadcms/payload/commit/24a4bc6f190340f1d1268ba3931c40fcab1430dd))
* omits irrelevant collections from rich text relationships ([2ccaa82](https://github.com/payloadcms/payload/commit/2ccaa823e4aef4fc254fcec2254bf7e8a13b43ee))
* restructures form builder [#43](https://github.com/payloadcms/payload/issues/43) ([3b1b8dd](https://github.com/payloadcms/payload/commit/3b1b8ddc30f06c8abb8996fa95f2f291d4474e9f))
* spreads overrides into configs [#25](https://github.com/payloadcms/payload/issues/25) ([#29](https://github.com/payloadcms/payload/issues/29)) ([da4d901](https://github.com/payloadcms/payload/commit/da4d9018c1e73524993637ce4775517d9d311c68))
* supports form email variables and prepares for send ([3207202](https://github.com/payloadcms/payload/commit/3207202808b848b8b0bac74bf0175f3ba19e6ac8))
* types payment field ([4a31090](https://github.com/payloadcms/payload/commit/4a310903882fdf6347781985077aab280dd2c1bc))
* utilizes unused replyTo ([4938602](https://github.com/payloadcms/payload/commit/4938602ee0a3872e1cb3b7fc6ec9558570825767))
* add internationalization (i18n) to locales ([#4005](https://github.com/payloadcms/payload/issues/4005)) ([6a0a859](https://github.com/payloadcms/payload/commit/6a0a859563ed9e742260ea51a1839a1ef0f61fce))
* Custom Error, Label, and before/after field components ([#3747](https://github.com/payloadcms/payload/issues/3747)) ([266c327](https://github.com/payloadcms/payload/commit/266c3274d03e4fd52c692eeef1ee9248dcf66189))
### Bug Fixes
* error on graphql multiple queries ([#3985](https://github.com/payloadcms/payload/issues/3985)) ([57da3c9](https://github.com/payloadcms/payload/commit/57da3c99a7e4ce5d3d1e17315e3691815f363704))
* focal and cropping issues, adds test ([#4039](https://github.com/payloadcms/payload/issues/4039)) ([acba5e4](https://github.com/payloadcms/payload/commit/acba5e482b7ddc6e3dc6ba9b7736022770d69a55))
* handle invalid tokens in refresh token operation ([#3647](https://github.com/payloadcms/payload/issues/3647)) ([131d89c](https://github.com/payloadcms/payload/commit/131d89c3f50c237e1ab2d7cd32d7a8226a9f8ce3))
* hasMany number and select fields unable to save within arrays ([#4047](https://github.com/payloadcms/payload/issues/4047)) ([182c57b](https://github.com/payloadcms/payload/commit/182c57b191010ce3dcf659f39c1dc2f7cf80662e))
* injects array and block ids into fieldSchemaToJSON ([#4043](https://github.com/payloadcms/payload/issues/4043)) ([d068ef7](https://github.com/payloadcms/payload/commit/d068ef7e2483d49dc41bdd7735042ddcaa0a684c))
* parse predefined migrations via file arg or name prefix ([#4001](https://github.com/payloadcms/payload/issues/4001)) ([eb42c03](https://github.com/payloadcms/payload/commit/eb42c031ef980558ed051d4163925aa28d6ab090))
* polymorphic hasMany relationships missing in postgres admin ([#4053](https://github.com/payloadcms/payload/issues/4053)) ([7a9af44](https://github.com/payloadcms/payload/commit/7a9af4417a56c621f01195f9a2904b9adffaad7a))
* resets list filter row when the filter on field is changed ([#3956](https://github.com/payloadcms/payload/issues/3956)) ([8d14c21](https://github.com/payloadcms/payload/commit/8d14c213c878a1afda2b3bf03431fed5aa2a44e3))
* Update API Views ([b008b6c](https://github.com/payloadcms/payload/commit/b008b6c6463c9dc3d8e61eaa0a9210aa1a189442))
* vite not replacing env vars correctly when building ([67b3baa](https://github.com/payloadcms/payload/commit/67b3baaa445a13246be8178d57eaeba92888bef1))
## [2.0.15](https://github.com/payloadcms/payload/compare/v2.0.14...v2.0.15) (2023-11-03)
### Bug Fixes
* autosave updating data in unrelated docs ([b722f20](https://github.com/payloadcms/payload/commit/b722f202af39a1429298b700cac686ecbbd4b46b))
* better error handling within parseCookies ([#3720](https://github.com/payloadcms/payload/issues/3720)) ([6b1b4ff](https://github.com/payloadcms/payload/commit/6b1b4ffd27cc9a84e22ef2f3a8e389e5b72d41bc))
* block row removal w/ db-postgres adapter ([#3951](https://github.com/payloadcms/payload/issues/3951)) ([5ea88bb](https://github.com/payloadcms/payload/commit/5ea88bb47d9ed6457331ceab7d7c82b0face8311))
* deeply merges view configs ([#3954](https://github.com/payloadcms/payload/issues/3954)) ([a5b2333](https://github.com/payloadcms/payload/commit/a5b2333140447b12dbafd68592108ac342af4ea7))
* do not display field if read permission is false - admin panel ui ([#3949](https://github.com/payloadcms/payload/issues/3949)) ([cdc10be](https://github.com/payloadcms/payload/commit/cdc10be1a241c6a9ac09feab77bcd58d23ff3dd9))
* ensures dataloader does not run requests in parallel ([4607dbf](https://github.com/payloadcms/payload/commit/4607dbf97694bc899e597e9c7df50b6c878874f5))
* exclude files from dev bundle if aliased ([#3957](https://github.com/payloadcms/payload/issues/3957)) ([7966692](https://github.com/payloadcms/payload/commit/796669279afb8fe23723ce36e6e47a44b7088b09))
* field paths being mutated if they ended with the req.locale ([#3936](https://github.com/payloadcms/payload/issues/3936)) ([36576f1](https://github.com/payloadcms/payload/commit/36576f152ace41edd8b353703db2598d04deae44))
* findVersions pagination ([#3906](https://github.com/payloadcms/payload/issues/3906)) ([1f8f173](https://github.com/payloadcms/payload/commit/1f8f173741fd524c7c2f11cc104672854f625da9))
* global autosave and relevant e2e test ([a9d96b1](https://github.com/payloadcms/payload/commit/a9d96b10376fe1a4731b2ddb4d26ce38e333d5cb))
* **i18n:** polish translations ([#3934](https://github.com/payloadcms/payload/issues/3934)) ([e4881bb](https://github.com/payloadcms/payload/commit/e4881bb02f7c1e8f96d5b405c57e0fdc01a2e7fe))
* passes correct data to buildStateFromSchema on account page ([#3984](https://github.com/payloadcms/payload/issues/3984)) ([c7a315a](https://github.com/payloadcms/payload/commit/c7a315a7d1075361a7ee432a449769397c12185e))
* prevent sort from saving a new version in version list view ([#3944](https://github.com/payloadcms/payload/issues/3944)) ([900a9ea](https://github.com/payloadcms/payload/commit/900a9eafeb51b1e5130518d4f71034a2bf9e4c5b))
* properly load temp files into buffer ([#3996](https://github.com/payloadcms/payload/issues/3996)) ([d1a0822](https://github.com/payloadcms/payload/commit/d1a0822f8044a3f65416f4fe608e91a4ceea6b56))
* sort document tabs by order ([#3968](https://github.com/payloadcms/payload/issues/3968)) ([06cd52b](https://github.com/payloadcms/payload/commit/06cd52b622723503896af6262907d31b258d0a5e))
* vertical alignment in step nav when using larger logos ([#3955](https://github.com/payloadcms/payload/issues/3955)) ([b6d9a20](https://github.com/payloadcms/payload/commit/b6d9a2021fafea594353329fd304553bf7f2d091))
## [2.0.14](https://github.com/payloadcms/payload/compare/v2.0.13...v2.0.14) (2023-10-30)
### Bug Fixes
* adds null to non-required field unions ([#3870](https://github.com/payloadcms/payload/issues/3870)) ([7e919aa](https://github.com/payloadcms/payload/commit/7e919aa87c0116c41bf41d75dcd91ff96576a46f))
* allows form fields to be properly overridden ([c880a61](https://github.com/payloadcms/payload/commit/c880a61f13e4c034e269466abffedbf7d644d272))
* build errors ([9f5ec17](https://github.com/payloadcms/payload/commit/9f5ec1784c674279ee5aa861cc7a6e0b6f5cbf47))
* checks for user before accessing properties in preferences update operation([#3844](https://github.com/payloadcms/payload/issues/3844)) ([24eab3a](https://github.com/payloadcms/payload/commit/24eab3af1da3b08debe0580cce8ece08a86039a4))
* correctly types width fields [#37](https://github.com/payloadcms/payload/issues/37) ([#41](https://github.com/payloadcms/payload/issues/41)) ([76c9704](https://github.com/payloadcms/payload/commit/76c97048502655516ece4f6680868a23a172ff2c))
* custom form fields ([ba0651c](https://github.com/payloadcms/payload/commit/ba0651cecf2aed50a9176218237181a4ce092bc0))
* **db-mongodb:** improve find query performance ([#3836](https://github.com/payloadcms/payload/issues/3836)) ([56e58e9](https://github.com/payloadcms/payload/commit/56e58e9ec732f8365f53f214a9e950fbc2a7d8b9)), closes [#3904](https://github.com/payloadcms/payload/issues/3904)
* **db-mongodb:** versions pagination ([#3875](https://github.com/payloadcms/payload/issues/3875)) ([4f2b080](https://github.com/payloadcms/payload/commit/4f2b080d1cb5f9d4ab80aa106650307c11e8811b))
* declares payload as a peer dependency ([e15e00b](https://github.com/payloadcms/payload/commit/e15e00b5f173041635221490006e4e8347518253))
* disable webpack hot reload on production ([#3891](https://github.com/payloadcms/payload/issues/3891)) ([422c803](https://github.com/payloadcms/payload/commit/422c803da67982a063a028508c44009b9577646e))
* duplicate document copying to incorrect locale ([#3874](https://github.com/payloadcms/payload/issues/3874)) ([89f273b](https://github.com/payloadcms/payload/commit/89f273bf894512b69e8647be4cf4496bff37c99b))
* dynamic field selector option values ([f0cc05a](https://github.com/payloadcms/payload/commit/f0cc05ab912fd07dcee8bd77c6c2bdc40607f80f))
* dynamic price selector ([a8d432f](https://github.com/payloadcms/payload/commit/a8d432f7b3da2ca36a3a1c0a378af44be82da357))
* enables nested AND/OR queries ([#3834](https://github.com/payloadcms/payload/issues/3834)) ([237eebd](https://github.com/payloadcms/payload/commit/237eebdf87b7d33baa9baaa54946f4ebb4276bfb))
* ensure serverURL has string value for getBaseUploadFields function ([#3900](https://github.com/payloadcms/payload/issues/3900)) ([c564a83](https://github.com/payloadcms/payload/commit/c564a83ab61b672d9a2bcc875ab890b0fede5462))
* ensures compare-version select field cannot be cleared ([#3901](https://github.com/payloadcms/payload/issues/3901)) ([42d8d11](https://github.com/payloadcms/payload/commit/42d8d11fd7e5792b119f69f17dc1100c85cfa491))
* error handling when duplicating documents fails ([#3873](https://github.com/payloadcms/payload/issues/3873)) ([435eb62](https://github.com/payloadcms/payload/commit/435eb6204e550e898a81033f794fcf568e3b017c))
* fieldToUse options ([a7780b1](https://github.com/payloadcms/payload/commit/a7780b10d974f91bea441c5c86c475181c6adc4a))
* form builder rich text serialization ([8f0d85f](https://github.com/payloadcms/payload/commit/8f0d85fe13591ce8186b623f6165626e79714430))
* form builder types ([32a69f8](https://github.com/payloadcms/payload/commit/32a69f8f368aa1917761b7585549dae2f8b2cb12))
* generate new block ids on create ([#3871](https://github.com/payloadcms/payload/issues/3871)) ([3404bab](https://github.com/payloadcms/payload/commit/3404bab83f1112713675eb504870a4a1786c3822))
* global permissions for live preview ([#3854](https://github.com/payloadcms/payload/issues/3854)) ([3032e0b](https://github.com/payloadcms/payload/commit/3032e0b5a239db0762abd120b2db95f30ed5ca65))
* handles null & undefined relationship field values in versions view ([#3609](https://github.com/payloadcms/payload/issues/3609)) ([115e592](https://github.com/payloadcms/payload/commit/115e592b54d9174f316daa3cff31bcc801eaf92f))
* incorrect duplication of data in admin ui ([#3907](https://github.com/payloadcms/payload/issues/3907)) ([46fc41c](https://github.com/payloadcms/payload/commit/46fc41cbd9615c58248b4d2c44d24905dd676171))
* misc bugs with data safety ([3fdf23a](https://github.com/payloadcms/payload/commit/3fdf23a1a0287673a190cfd3c7bc55d64cff37ba))
* only apply focal manipulation when necessary ([#3902](https://github.com/payloadcms/payload/issues/3902)) ([a4f36aa](https://github.com/payloadcms/payload/commit/a4f36aa8a009e9c0156924320bbcf1d04b697223))
* only populates redirect references if passed via config ([2bbe428](https://github.com/payloadcms/payload/commit/2bbe4286f6c89092a8133938d0fec2cc31c0cb16))
* overwrites incoming config arrays within field overrides ([3ca632b](https://github.com/payloadcms/payload/commit/3ca632bcbd49aeaafa86946a595307a95535a300))
* **payload:** graphql query errors transaction race condition ([#3795](https://github.com/payloadcms/payload/issues/3795)) ([dc13b10](https://github.com/payloadcms/payload/commit/dc13b101f7351f7bae60a4a4bbc25907ed82210f))
* payment form submissions ([36a8dc4](https://github.com/payloadcms/payload/commit/36a8dc49b34d75993fa981e3426db85536bf5f26))
* properly exports types ([856962c](https://github.com/payloadcms/payload/commit/856962c6c621ab366e7c0b9089f3204389f3d322))
* properly overrides form submission relationTo [#19](https://github.com/payloadcms/payload/issues/19) ([#27](https://github.com/payloadcms/payload/issues/27)) ([5b6705b](https://github.com/payloadcms/payload/commit/5b6705b4f62c8dfda87f1ceb5585e59277e22542))
* reduces richtext indent padding ([b4610a3](https://github.com/payloadcms/payload/commit/b4610a3faef02cc7f6d440bb908a4623ed94fee8))
* graphql query errors transaction race condition ([#3795](https://github.com/payloadcms/payload/issues/3795)) ([dc13b10](https://github.com/payloadcms/payload/commit/dc13b101f7351f7bae60a4a4bbc25907ed82210f))
* removes conditional return of formattedEmails in sendEmail hook [#26](https://github.com/payloadcms/payload/issues/26) ([#28](https://github.com/payloadcms/payload/issues/28)) ([e8458f8](https://github.com/payloadcms/payload/commit/e8458f84bcd5bad74b189479931fbb7faea74900))
* resize image if no aspect ratio change ([#3859](https://github.com/payloadcms/payload/issues/3859)) ([f53b713](https://github.com/payloadcms/payload/commit/f53b7131548dbe9071c65ba110f7f0d206bb33b5))
* **richtext-*:** type issues with typescript strict mode enabled ([dac9514](https://github.com/payloadcms/payload/commit/dac9514eb00b99a3caeb9f217695b2b89368f7c9))
* **richtext-lexical:** Blocks node incorrectly marked as client module ([35f00fa](https://github.com/payloadcms/payload/commit/35f00fa83d2a90967e0707ca0fd960c5608a3bf3))
* **richtext-lexical:** remove unnecessary dependencies (fixes [#3889](https://github.com/payloadcms/payload/issues/3889)) ([760565f](https://github.com/payloadcms/payload/commit/760565f1e96e4cb1f6bce8663ad3fa8a16a2601c))
* set date to 12UTC for default, dayOnly and monthOnly fields ([#3887](https://github.com/payloadcms/payload/issues/3887)) ([d393225](https://github.com/payloadcms/payload/commit/d3932252891bb8721a5abc97e204dbb6a7f3fda2))
* skip following code if form.emails list is empty ([#54](https://github.com/payloadcms/payload/issues/54)) ([e13d8da](https://github.com/payloadcms/payload/commit/e13d8da7c229d7aa1caa487f20c4158e8ec0aeb5))
* store resized image on req or tempFilePath ([#3883](https://github.com/payloadcms/payload/issues/3883)) ([6c5d525](https://github.com/payloadcms/payload/commit/6c5d525d8e1267eebdffeb9f31b2ef3a4df1132d))
* thread through collection admin config properties - [#5](https://github.com/payloadcms/payload/issues/5) ([175d44b](https://github.com/payloadcms/payload/commit/175d44b0aeaec872421e0269c4b7d643f9cb8456))
* threads locale through findByID ([#31](https://github.com/payloadcms/payload/issues/31)) ([a12240b](https://github.com/payloadcms/payload/commit/a12240b71e677c9b46f47117dec79e40a7de3564))
* unique field error handling ([#3888](https://github.com/payloadcms/payload/issues/3888)) ([4d8d4c2](https://github.com/payloadcms/payload/commit/4d8d4c214ab12571e9dc71e406117c4b19f63c6b))
* updates payload ([6940f2c](https://github.com/payloadcms/payload/commit/6940f2c0b7d36ad25fdc09ba93e23f5c6a095d6c))
* updates richtext indentation within formbuilder ([ea22da4](https://github.com/payloadcms/payload/commit/ea22da4fc7285e4eab975e916d7627394eb4bb26))
* uses form slug from config in form relationship field [#19](https://github.com/payloadcms/payload/issues/19) ([#23](https://github.com/payloadcms/payload/issues/23)) ([35e14cf](https://github.com/payloadcms/payload/commit/35e14cf044698acabe3a0902897ea276e4aef803))
* who can say ([b398a92](https://github.com/payloadcms/payload/commit/b398a92db4f2402089fd03e803fefb337964d432))
## [2.0.13](https://github.com/payloadcms/payload/compare/v2.0.12...v2.0.13) (2023-10-24)

View File

@@ -89,3 +89,14 @@ If you are committing to [templates](./templates) or [examples](./examples), use
## Pull Requests
For all Pull Requests, you should be extremely descriptive about both your problem and proposed solution. If there are any affected open or closed issues, please leave the issue number in your PR message.
## Previewing docs
This is how you can preview changes you made locally to the docs:
1. Clone our [website repository](https://github.com/payloadcms/website)
2. Run `yarn install`
3. Duplicate the `.env.example` file and rename it to `.env`
4. Add a `DOCS_DIR` environment variable to the `.env` file which points to the absolute path of your modified docs folder. For example `DOCS_DIR=/Users/yourname/Documents/GitHub/payload/docs`
5. Run `yarn run fetchDocs:local`. If this was successful, you should see no error messages and the following output: *Docs successfully written to /.../website/src/app/docs.json*. There could be error messages if you have incorrect markdown in your local docs folder. In this case, it will tell you how you can fix it
6. You're done! Now you can start the website locally using `yarn run dev` and preview the docs under [http://localhost:3000/docs/](http://localhost:3000/docs/)

View File

@@ -27,7 +27,7 @@
<hr/>
> [!IMPORTANT]
> 🎉 <strong>Payload 2.0 is now available!<strong> Read more in the <a target="_blank" href="https://payloadcms.com/blog/payload-2-0" rel="dofollow"><strong>announcement post</strong></a>.
> 🎉 <strong>Payload 2.0 is now available!</strong> Read more in the <a target="_blank" href="https://payloadcms.com/blog/payload-2-0" rel="dofollow"><strong>announcement post</strong></a>.
<h3>Benefits over a regular CMS</h3>
<ul>
@@ -51,7 +51,7 @@ Create a cloud account, connect your GitHub, and [deploy in minutes](https://pay
Before beginning to work with Payload, make sure you have all of the [required software](https://payloadcms.com/docs/getting-started/installation).
```text
npx create-payload-app
npx create-payload-app@latest
```
Alternatively, it only takes about five minutes to [create an app from scratch](https://payloadcms.com/docs/getting-started/installation#from-scratch).

View File

@@ -96,7 +96,7 @@ To swap out any of these views, simply pass in your custom component to the `adm
}
```
For more granular control, pass a configuration object instead. Payload exposes all of the properties of `<Route />` component in [React Router v5](https://v5.reactrouter.com):
For more granular control, pass a configuration object instead. Each view corresponds to its own `<Route />` component in [React Router v5](https://v5.reactrouter.com). Payload exposes all of the properties of React Router:
| Property | Description |
| ------------------ | ---------------------------------------------------------------------------------------------------------------------------- |
@@ -129,13 +129,19 @@ To add a _new_ view to the Admin Panel, simply add another key to the `views` ob
}
```
<Banner type="warning">
<strong>Note:</strong>
<br />
Routes are cascading. This means that unless explicitly given the `exact` property, they will match on URLs that simply _start_ with the route's path. This is helpful when creating catch-all routes in your application. Alternatively, you could define your nested route _before_ your parent route.
</Banner>
_For more examples regarding how to customize components, look at the following [examples](https://github.com/payloadcms/payload/tree/main/test/admin/components)._
For help on how to build your own custom view components, see [building a custom view component](#building-a-custom-view-component).
### Collections
You can override components on a collection-by-collection basis via their `admin` property.
You can override components on a collection-by-collection basis via the `admin.components` property.
| Path | Description |
| -------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
@@ -259,15 +265,15 @@ You can also add _new_ tabs to the `Edit` view by adding another key to the `com
### Globals
As with Collections, you can override components on a global-by-global basis via their `admin` property.
As with Collections, you can override components on a global-by-global basis via the `admin.components` property.
| Path | Description |
| -------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| **`edit.SaveButton`** | Replace the default `Save` button with a custom component. Drafts must be disabled |
| **`edit.SaveDraftButton`** | Replace the default `Save Draft` button with a custom component. Drafts must be enabled and autosave must be disabled. |
| **`edit.PublishButton`** | Replace the default `Publish` button with a custom component. Drafts must be enabled. |
| **`edit.PreviewButton`** | Replace the default `Preview` button with a custom component. |
| **`views`** | Override or create new views within the Payload Admin UI. [More](#global-views) |
| Path | Description |
| ------------------------------ | ---------------------------------------------------------------------------------------------------------------------- |
| **`elements.SaveButton`** | Replace the default `Save` button with a custom component. Drafts must be disabled |
| **`elements.SaveDraftButton`** | Replace the default `Save Draft` button with a custom component. Drafts must be enabled and autosave must be disabled. |
| **`elements.PublishButton`** | Replace the default `Publish` button with a custom component. Drafts must be enabled. |
| **`elements.PreviewButton`** | Replace the default `Preview` button with a custom component. |
| **`views`** | Override or create new views within the Payload Admin UI. [More](#global-views) |
#### Global views
@@ -426,6 +432,15 @@ 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).
| 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) |
## Cell Component
These are the props that will be passed to your custom Cell to use in your own components.
@@ -481,6 +496,103 @@ const CustomTextField: React.FC<Props> = ({ path }) => {
components, including the <strong>useField</strong> hook, [click here](/docs/admin/hooks).
</Banner>
## Label Component
These are the props that will be passed to your custom Label.
| Property | Description |
| ---------------- | ---------------------------------------------------------------- |
| **`htmlFor`** | Property used to set `for` attribute for label. |
| **`label`** | Label value provided in field, it can be used with i18n. |
| **`required`** | A boolean value that represents if the field is required or not. |
#### Example
```tsx
import React from 'react'
import { useTranslation } from 'react-i18next'
import { getTranslation } from 'payload/utilities/getTranslation'
type Props = {
htmlFor?: string
label?: Record<string, string> | false | string
required?: boolean
}
const CustomLabel: React.FC<Props> = (props) => {
const { htmlFor, label, required = false } = props
const { i18n } = useTranslation()
if (label) {
return (<span>
{getTranslation(label, i18n)}
{required && <span className="required">*</span>}
</span>);
}
return null
}
```
## Error Component
These are the props that will be passed to your custom Error.
| Property | Description |
| ---------------- | ------------------------------------------------------------- |
| **`message`** | The error message. |
| **`showError`** | A boolean value that represents if the error should be shown. |
#### Example
```tsx
import React from 'react'
type Props = {
message: string
showError?: boolean
}
const CustomError: React.FC<Props> = (props) => {
const { message, showError } = props
if (showError) {
return <p style={{color: 'red'}}>{message}</p>
} else return null;
}
```
## 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.
#### Example
```tsx
import React from 'react'
import './style.scss'
const ClearButton: React.FC = () => {
return <button onClick={() => {/* ... */}}>X</button>
}
const fieldField: Field = {
name: 'title',
type: 'text',
admin: {
components: {
AfterInput: [
<ClearButton />
]
}
}
}
export default titleField;
```
## Custom providers
As your admin customizations gets more complex you may want to share state between fields or other components. You can add custom providers to do add your own context to any Payload app for use in other custom components within the admin panel. Within your config add `admin.components.providers`, these can be used to share context or provide other custom functionality. Read the [React context](https://reactjs.org/docs/context.html) docs to learn more.

View File

@@ -91,14 +91,34 @@ This way when your bundler goes to import a file that contains server-only modul
To remove files that contain server-only modules from your bundle, you can use an `alias`.
First create new file that exports an empty object:
```js
// mocks/emptyObject.js
In the Subscriptions config file above, we are importing the hook like so:
export default {}
```ts
// collections/Subscriptions/index.ts
import createStripeSubscription from './hooks/createStripeSubscription'
```
Then, in your Payload config, you can alias the file containing the server-only module to the mock module. For example, here's how you'd do this in Webpack:
By default the browser bundle will now include all the code from that file and any files down the tree. We know that the file imports `stripe`.
To fix this, we need to alias the `createStripeSubscription` file to a different file that can safely be included in the browser bundle.
First, we will create a mock file to replace the server-only file when bundling:
```js
// mocks/modules.js
export default {}
/**
* NOTE: if you are destructuring an import
* the mock file will need to export matching
* variables as the destructured object.
*
* export const namedExport = {}
*/
```
Aliasing with [Webpack](/docs/admin/webpack) can be done by:
```ts
// payload.config.ts
@@ -106,10 +126,16 @@ Then, in your Payload config, you can alias the file containing the server-only
import { buildConfig } from 'payload/config'
import { webpackBundler } from '@payloadcms/bundler-webpack'
import { Subscriptions } from './collections/Subscriptions'
const mockModulePath = path.resolve(__dirname, 'mocks/emptyObject.js')
const pathToFileWithServerOnlyModule = path.resolve(__dirname, 'hooks/syncStripeCustomer.ts')
const fullFilePath = path.resolve(
__dirname,
'collections/Subscriptions/hooks/createStripeSubscription'
)
export default buildConfig({
collections: [Subscriptions],
admin: {
bundler: webpackBundler(),
webpack: (config) => {
@@ -120,7 +146,42 @@ export default buildConfig({
// highlight-start
alias: {
...config.resolve.alias,
[pathToFileWithServerOnlyModule]: mockModulePath,
[fullFilePath]: mockModulePath,
},
// highlight-end
},
}
},
},
})
```
Aliasing with [Vite](/docs/admin/vite) can be done by:
```ts
// payload.config.ts
import { buildConfig } from 'payload/config'
import { viteBundler } from '@payloadcms/bundler-vite'
import { Subscriptions } from './collections/Subscriptions'
const mockModulePath = path.resolve(__dirname, 'mocks/emptyObject.js')
export default buildConfig({
collections: [Subscriptions],
admin: {
bundler: viteBundler(),
vite: (config) => {
return {
...config,
resolve: {
...config.resolve,
// highlight-start
alias: {
...config.resolve.alias,
// remember, vite aliases are exact-match only
'./hooks/createStripeSubscription': mockModulePath,
},
// highlight-end
},

View File

@@ -758,3 +758,29 @@ const MyComponent: React.FC = () => {
### usePreferences
Returns methods to set and get user preferences. More info can be found [here](https://payloadcms.com/docs/admin/preferences).
### useTableColumns
Returns methods to manipulate table columns
```tsx
import { useTableColumns } from 'payload/components/utilities'
const MyComponent: React.FC = () => {
// highlight-start
const { setActiveColumns } = useTableColumns()
const resetColumns = () => {
setActiveColumns(['id', 'createdAt', 'updatedAt'])
}
// highlight-end
return (
<button
type="button"
onClick={resetColumns}
>
Reset columns
</button>
)
}

View File

@@ -5,6 +5,10 @@ order: 90
desc: NEEDS TO BE WRITTEN
---
<Banner type="info">
The Vite bundler is currently in beta. If you would like to help us test this package, we'd love to hear from you if you find any [bugs or issues](https://github.com/payloadcms/payload/issues/)!
</Banner>
Payload has a Vite bundler that you can install and bundle the Admin Panel with. This is an alternative to the [Webpack](/docs/admin/webpack) bundler and might give some performance boosts to your development workflow.
To use Vite as your bundler, first you need to install the package:
@@ -13,9 +17,19 @@ To use Vite as your bundler, first you need to install the package:
yarn add @payloadcms/bundler-vite
```
<Banner>
The Vite bundler is currently in beta. If you would like to help us test this package, we'd love to hear if you find any bugs or issues!
</Banner>
Then you will need to add the [bundler](/docs/admin/bundlers) to your Payload config:
```ts
import { buildConfig } from '@payloadcms/config'
import viteBundler from '@payloadcms/bundler-vite'
export default buildConfig({
collections: [],
admin: {
bundler: viteBundler(),
}
})
```
Vite works fundamentally differently than Webpack. In development mode, it will first pre-bundle any of your dependencies that are CommonJS-only, and then it'll leverage ESM directly in your browser for a better HMR experience.
@@ -29,10 +43,37 @@ This is because Vite aliases work fundamentally differently than Webpack aliases
Here are the main differences between how Vite aliases work and how Webpack aliases work.
**Vite aliases do not work with absolute paths.**
**Vite aliases do not work with absolute paths.**
In Vite, an alias will only match if the `find` property _exactly matches_ how you are importing your server-only file. So if you are importing a file with a relative path, i.e. `'../../my-module'`, and your alias is absolute, your alias will not work.
In Vite, alias keys must <strong>exactly match</strong> a import paths. If you have 2 files that import the same server-only module, but have different import paths, you would need to add 2 aliases to support both import paths.
```ts
// File A
import serverOnlyModule from '../server-only-module'
// File B
import serverOnlyModule from '../../server-only-module'
// payload.config.ts
// You would need to add 2 aliases to support both import paths
export const buildConfig({
collections: [],
admin: {
bundler: viteBundler(),
vite: (incomingViteConfig) => ({
...incomingViteConfig,
resolve: {
...(incomingViteConfig?.resolve || {}),
alias: {
...(incomingViteConfig?.resolve?.alias || {}),
'../server-only-module': path.resolve(__dirname, './path/to/browser-safe-module.js'),
'../../server-only-module': path.resolve(__dirname, './path/to/browser-safe-module.js'),
}
}
})
}
})
```
**Vite aliases do not get applied to pre-bundled dependencies.**
@@ -58,7 +99,7 @@ This will effectively alias the entire plugin and work with Vite. If the plugin
### Extending the Vite config
The Payload config supports a new property for plugins to be able to extend the Vite config specifically. That property exists on the main Payload config under `admin.vite`.
The Payload config supports a new property for plugins to be able to extend the Vite config specifically. That property exists on the main Payload config under `admin.vite`. You can check out the [Vite docs](https://vitejs.dev/config/shared-options.html) for more information on what you can do with the Vite config.
It's a function that takes a Vite config, and returns an updated Vite config. Here's an example:
@@ -66,17 +107,24 @@ It's a function that takes a Vite config, and returns an updated Vite config. He
export const buildConfig({
collections: [],
admin: {
bundler: viteBundler(),
vite: (incomingViteConfig) => ({
...incomingViteConfig,
resolve: {
...incomingViteConfig.resolve,
// Do whatever you need here
...(incomingViteConfig?.resolve || {}),
alias: {
...(incomingViteConfig?.resolve?.alias || {}),
// custom aliases go here
'../server-only-module': path.resolve(__dirname, './path/to/browser-safe-module.js'),
}
}
})
}
})
```
Learn more about [aliasing server-only modules](http://localhost:3000/docs/admin/excluding-server-code#aliasing-server-only-modules).
Even though there is a new property for Vite configs specifically, we have implemented some "compatibility" between Webpack and Vite out-of-the-box.
If your config specifies Webpack aliases, we attempt to leverage them automatically within the Vite config. They are merged into the Vite alias configuration seamlessly and may work out-of-the-box.
If your config specifies Webpack aliases, we attempt to leverage them automatically within the Vite config. They are merged into the Vite alias configuration seamlessly and may work out-of-the-box.

View File

@@ -57,6 +57,38 @@ export default buildConfig({
})
```
**Example Payload config set up for localization with full locales objects (including [internationalization](/docs/configuration/i18n) support):**
```ts
import { buildConfig } from 'payload/config'
export default buildConfig({
collections: [
// collections go here
],
localization: {
locales: [
{
label: {
en: 'English', // English label
nb: 'Engelsk', // Norwegian label
},
code: 'en',
},
{
label: {
en: 'Norwegian', // English label
nb: 'Norsk', // Norwegian label
},
code: 'nb',
},
],
defaultLocale: 'en',
fallback: true,
},
})
```
**Here is a brief explanation of each of the options available within the `localization` property:**
**`locales`**

View File

@@ -53,6 +53,7 @@ export const Page: CollectionConfig = {
- [Rich Text](/docs/fields/rich-text) - fully extensible Rich Text editor
- [Row](/docs/fields/row) - used for admin field layout, no effect on data shape
- [Select](/docs/fields/select) - dropdown / picklist style value selector
- [Tabs](/docs/fields/tabs) - used for admin layout, nest fields within tabs
- [Text](/docs/fields/text) - simple text input
- [Textarea](/docs/fields/textarea) - allows a bit larger of a text editor
- [Upload](/docs/fields/upload) - allows local file and image upload

View File

@@ -267,3 +267,27 @@ Relationship fields with `hasMany` set to more than one kind of collections save
Querying is done in the same way as the earlier Polymorphic example:
`?where[owners.value][equals]=6031ac9e1289176380734024`.
#### Querying and Filtering Polymorphic Relationships
Polymorphic and non-polymorphic relationships must be queried differently because of how the related data is stored and may be inconsistent across different collections. Because of this, filtering polymorphic relationship fields from the Collection List admin UI is limited to the `id` value.
For a polymorphic relationship, the response will always be an array of objects. Each object will contain the `relationTo` and `value` properties.
The data can be queried by the related document ID:
`?where[field.value][equals]=6031ac9e1289176380734024`.
Or by the related document Collection slug:
`?where[field.relationTo][equals]=your-collection-slug`.
However, you **cannot** query on any field values within the related document.
Since we are referencing multiple collections, the field you are querying on may not exist and break the query.
<Banner type="warning">
<strong>Note:</strong>
<br />
You <strong>cannot</strong> query on a field within a polymorphic relationship as you would with a non-polymorphic relationship.
</Banner>

View File

@@ -23,7 +23,7 @@ Payload requires the following software:
To quickly scaffold a new Payload app in the fastest way possible, you can use [create-payload-app](https://npmjs.com/package/create-payload-app). To do so, run the following command:
```
npx create-payload-app
npx create-payload-app@latest
```
Then just follow the prompts! You'll get set up with a new folder and a functioning Payload app inside.

View File

@@ -400,6 +400,70 @@ const result = await payload.updateGlobal({
})
```
## Next.js Conflict with Local API
There is a known issue when using the Local API with Next.js version `13.4.13` and higher. Next.js executes within a separate child process, and Payload has not been initalized yet in these instances. That means that unless you explicitly initialize Payload within your operation, it will not be running and return no data / an empty object.
As a workaround, we recommend leveraging the following pattern to determine and ensure Payload is initalized:
```
import dotenv from 'dotenv'
import path from 'path'
import type { Payload } from 'payload'
import payload from 'payload'
import type { InitOptions } from 'payload/config'
import { seed as seedData } from './seed'
dotenv.config({
path: path.resolve(__dirname, '../.env'),
})
let cached = (global as any).payload
if (!cached) {
cached = (global as any).payload = { client: null, promise: null }
}
interface Args {
initOptions?: Partial<InitOptions>
seed?: boolean
}
export const getPayloadClient = async ({ initOptions, seed }: Args = {}): Promise<Payload> => {
if (!process.env.DATABASE_URI) {
throw new Error('DATABASE_URI environment variable is missing')
}
if (!process.env.PAYLOAD_SECRET) {
throw new Error('PAYLOAD_SECRET environment variable is missing')
}
if (cached.client) {
return cached.client
}
if (!cached.promise) {
cached.promise = payload.init({
mongoURL: process.env.DATABASE_URI,
secret: process.env.PAYLOAD_SECRET,
local: initOptions?.express ? false : true,
...(initOptions || {}),
})
}
try {
process.env.PAYLOAD_DROP_DATABASE = seed ? 'true' : 'false'
cached.client = await cached.promise
if (seed) {
payload.logger.info('---- SEEDING DATABASE ----')
await seedData(payload)
}
} catch (e: unknown) {
cached.promise = null
throw e
}
return cached.client
}
```
To checkout how this works in a project, take a look at our [custom server example](https://github.com/payloadcms/payload/blob/master/examples/custom-server/src/getPayload.ts).
## Example Script using Local API
The Local API is especially useful for running scripts

View File

@@ -84,7 +84,7 @@ If you&apos;re starting from scratch, you can easily setup a dev environment lik
```
mkdir dev
cd dev
npx create-payload-app
npx create-payload-app@latest
```
If you&apos;re using the plugin template, the dev folder is built out for you and the `samplePlugin` has already been installed in `dev/payload.config()`.

View File

@@ -158,6 +158,17 @@ DigitalOcean provides extremely helpful documentation that can walk you through
1. [Create a new MongoDB and user](https://medium.com/@mhagemann/how-to-add-a-new-user-to-a-mongodb-database-d896776b5362)
1. [Set up Node for production](https://www.digitalocean.com/community/tutorials/how-to-set-up-a-node-js-application-for-production-on-ubuntu-20-04)
### Swap Space
Swap refers to a section of storage on the hard drive that is reserved to temporarily store data that can no longer fit within RAM. This allows for the expansion of your server's working memory, with some limitations. Swap space comes into play when available RAM can no longer accommodate actively used application data, enabling the system to continue functioning.
Insufficient space can lead to deployment errors and memory-related issues, resulting in application crashes, sluggish performance, or an unresponsive server.
Common deployment error due to **space limitations** (as reported by users):
- `Error: Command failed with exit code 1`
To configure swap, we recommend following this tutorial on [How To Add Swap Space](https://www.digitalocean.com/community/tutorials/how-to-add-swap-space-on-ubuntu-22-04).
## Docker
This is an example of a multi-stage docker build of Payload for production. Ensure you are setting your environment variables on deployment, like `PAYLOAD_SECRET`, `PAYLOAD_CONFIG_PATH`, and `DATABASE_URI` if needed.

View File

@@ -2,7 +2,7 @@
title: Lexical Rich Text
label: Lexical
order: 30
desc: Built by Meta, Lexical is an incredibly powerful rich text editor and it works beautifully within Payload.
desc: Built by Meta, Lexical is an incredibly powerful rich text editor, and it works beautifully within Payload.
keywords: lexical, rich text, editor, headless cms
---
@@ -45,7 +45,39 @@ export default buildConfig({
You can also override Lexical settings on a field-by-field basis as follows:
```ts
import { CollectionConfig } from 'payload/types'
import type { CollectionConfig } from 'payload/types'
import {
lexicalEditor
} from '@payloadcms/richtext-lexical'
export const Pages: CollectionConfig = {
slug: 'pages',
fields: [
{
name: 'content',
type: 'richText',
// Pass the Lexical editor here and override base settings as necessary
editor: lexicalEditor({})
}
]
}
```
## Extending the lexical editor with Features
Lexical has been designed with extensibility in mind. Whether you're aiming to introduce new functionalities or tweak the existing ones, Lexical makes it seamless for you to bring those changes to life.
### Features: The Building Blocks
At the heart of Lexical's customization potential are "features". While Lexical ships with a set of default features we believe are essential for most use cases, the true power lies in your ability to redefine, expand, or prune these as needed.
If you remove all the default features, you're left with a blank editor. You can then add in only the features you need, or you can build your own custom features from scratch.
### Integrating New Features
To weave in your custom features, utilize the `features` prop when initializing the Lexical Editor. Here's a basic example of how this is done:
```ts
import {
BlocksFeature,
LinkFeature,
@@ -55,62 +87,508 @@ import {
import { Banner } from '../blocks/Banner'
import { CallToAction } from '../blocks/CallToAction'
export const Pages: CollectionConfig = {
{
editor: lexicalEditor({
features: ({ defaultFeatures }) => [
...defaultFeatures,
LinkFeature({
// Example showing how to customize the built-in fields
// of the Link feature
fields: [
{
name: 'rel',
label: 'Rel Attribute',
type: 'select',
hasMany: true,
options: ['noopener', 'noreferrer', 'nofollow'],
admin: {
description:
'The rel attribute defines the relationship between a linked resource and the current document. This is a custom link field.',
},
},
],
}),
UploadFeature({
collections: {
uploads: {
// Example showing how to customize the built-in fields
// of the Upload feature
fields: [
{
name: 'caption',
type: 'richText',
editor: lexicalEditor(),
},
],
},
},
}),
// This is incredibly powerful. You can re-use your Payload blocks
// directly in the Lexical editor as follows:
BlocksFeature({
blocks: [
Banner,
CallToAction,
],
}),
]
})
}
```
## Features overview
Here's an overview of all the included features:
| Feature Name | Included by default | Description |
|--------------------------------|---------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| **`BoldTextFeature`** | Yes | Handles the bold text format |
| **`ItalicTextFeature`** | Yes | Handles the italic text format |
| **`UnderlineTextFeature`** | Yes | Handles the underline text format |
| **`StrikethroughTextFeature`** | Yes | Handles the strikethrough text format |
| **`SubscriptTextFeature`** | Yes | Handles the subscript text format |
| **`SuperscriptTextFeature`** | Yes | Handles the superscript text format |
| **`InlineCodeTextFeature`** | Yes | Handles the inline-code text format |
| **`ParagraphFeature`** | Yes | Handles paragraphs. Since they are already a key feature of lexical itself, this Feature mainly handles the Slash and Add-Block menu entries for paragraphs |
| **`HeadingFeature`** | Yes | Adds Heading Nodes (by default, H1 - H6, but that can be customized) |
| **`AlignFeature`** | Yes | Allows you to align text left, centered and right |
| **`IndentFeature`** | Yes | Allows you to indent text with the tab key |
| **`UnoderedListFeature`** | Yes | Adds unordered lists (ol) |
| **`OrderedListFeature`** | Yes | Adds ordered lists (ul) |
| **`CheckListFeature`** | Yes | Adds checklists |
| **`LinkFeature`** | Yes | Allows you to create internal and external links |
| **`RelationshipFeature`** | Yes | Allows you to create block-level (not inline) relationships to other documents |
| **`BlockQuoteFeature`** | Yes | Allows you to create block-level quotes |
| **`UploadFeature`** | Yes | Allows you to create block-level upload nodes - this supports all kinds of uploads, not just images |
| **`BlocksFeature`** | No | Allows you to use Payload's [Blocks Field](/docs/fields/blocks) directly inside your editor. In the feature props, you can specify the allowed blocks - just like in the Blocks field. |
| **`TreeViewFeature`** | No | Adds a debug box under the editor, which allows you to see the current editor state live, the dom, as well as time travel. Very useful for debugging |
## Creating your own, custom Feature
Creating your own custom feature requires deep knowledge of the Lexical editor. We recommend you take a look at the [Lexical documentation](https://lexical.dev/docs/intro) first - especially the "concepts" section.
Next, take a look at the [features we've already built](https://github.com/payloadcms/payload/tree/main/packages/richtext-lexical/src/field/features) - understanding how they work will help you understand how to create your own. There is no difference between the features included by default and the ones you create yourself - since those features are all isolated from the "core", you have access to the same APIs, whether the feature is part of payload or not!
## Converters
### Lexical => HTML
Lexical saves data in JSON, but can also generate its HTML representation via two main methods:
1. **Outputting HTML from the Collection:** Create a new field in your collection to convert saved JSON content to HTML. Payload generates and outputs the HTML for use in your frontend.
2. **Generating HTML on the Frontend:** Convert JSON to HTML on-demand, either in your frontend or elsewhere.
The editor comes with built-in HTML serializers, simplifying the process of converting JSON to HTML.
#### Outputting HTML from the Collection
To add HTML generation directly within the collection, follow the example below:
```ts
import type { CollectionConfig } from 'payload/types'
import {
HTMLConverterFeature,
lexicalEditor,
lexicalHTML
} from '@payloadcms/richtext-lexical'
const Pages: CollectionConfig = {
slug: 'pages',
fields: [
{
name: 'content',
name: 'nameOfYourRichTextField',
type: 'richText',
// Pass the Lexical editor here and override base settings as necessary
editor: lexicalEditor({
features: ({ defaultFeatures }) => [
...defaultFeatures,
LinkFeature({
// Example showing how to customize the built-in fields
// of the Link feature
fields: [
{
name: 'rel',
label: 'Rel Attribute',
type: 'select',
hasMany: true,
options: ['noopener', 'noreferrer', 'nofollow'],
admin: {
description:
'The rel attribute defines the relationship between a linked resource and the current document. This is a custom link field.',
},
},
],
}),
UploadFeature({
collections: {
uploads: {
// Example showing how to customize the built-in fields
// of the Upload feature
fields: [
{
name: 'caption',
type: 'richText',
editor: lexicalEditor(),
},
],
},
},
}),
// This is incredibly powerful. You can re-use your Payload blocks
// directly in the Lexical editor as follows:
BlocksFeature({
blocks: [
Banner,
CallToAction,
],
}),
]
})
}
]
// The HTMLConverter Feature is the feature which manages the HTML serializers. If you do not pass any arguments to it, it will use the default serializers.
HTMLConverterFeature({}),
],
}),
},
lexicalHTML('nameOfYourRichTextField', { name: 'nameOfYourRichTextField_html' }),
],
}
```
The `lexicalHTML()` function creates a new field that automatically converts the referenced lexical richText field into HTML through an afterRead hook.
#### Generating HTML in the Frontend:
If you wish to convert JSON to HTML ad-hoc, use this code snippet:
```ts
import type { SerializedEditorState } from 'lexical'
import {
type SanitizedEditorConfig,
convertLexicalToHTML,
consolidateHTMLConverters,
} from '@payloadcms/richtext-lexical'
async function lexicalToHTML(editorData: SerializedEditorState, editorConfig: SanitizedEditorConfig) {
return await convertLexicalToHTML({
converters: consolidateHTMLConverters({ editorConfig }),
data: editorData,
})
}
```
This method employs `convertLexicalToHTML` from `@payloadcms/richtext-lexical`, which converts the serialized editor state into HTML.
Because every `Feature` is able to provide html converters, and because the `htmlFeature` can modify those or provide their own, we need to consolidate them with the default html Converters using the `consolidateHTMLConverters` function.
#### Creating your own HTML Converter
HTML Converters are typed as `HTMLConverter`, which contains the node type it should handle, and a function that accepts the serialized node from the lexical editor, and outputs the HTML string. Here's the HTML Converter of the Upload node as an example:
```ts
import type { HTMLConverter } from '@payloadcms/richtext-lexical'
import payload from 'payload'
const UploadHTMLConverter: HTMLConverter<SerializedUploadNode> = {
converter: async ({ node }) => {
const uploadDocument = await payload.findByID({
id: node.value.id,
collection: node.relationTo,
})
const url = (payload?.config?.serverURL || '') + uploadDocument?.url
if (!(uploadDocument?.mimeType as string)?.startsWith('image')) {
// Only images can be serialized as HTML
return ``
}
return `<img src="${url}" alt="${uploadDocument?.filename}" width="${uploadDocument?.width}" height="${uploadDocument?.height}"/>`
},
nodeTypes: [UploadNode.getType()], // This is the type of the lexical node that this converter can handle. Instead of hardcoding 'upload' we can get the node type directly from the UploadNode, since it's static.
}
```
As you can see, we have access to all the information saved in the node (for the Upload node, this is `value`and `relationTo`) and we can use that to generate the HTML.
The `convertLexicalToHTML` is part of `@payloadcms/richtext-lexical` automatically handles traversing the editor state and calling the correct converter for each node.
#### Embedding the HTML Converter in your Feature
You can embed your HTML Converter directly within your custom `Feature`, allowing it to be handled automatically by the `consolidateHTMLConverters` function. Here is an example:
```ts
export const UploadFeature = (props?: UploadFeatureProps): FeatureProvider => {
return {
feature: () => {
return {
nodes: [
{
converters: {
html: yourHTMLConverter, // <= This is where you define your HTML Converter
},
node: UploadNode,
type: UploadNode.getType(),
//...
},
],
plugins: [/*...*/],
props: props,
slashMenu: {/*...*/},
}
},
key: 'upload',
}
}
```
### Headless Editor
Lexical provides a seamless way to perform conversions between various other formats:
- HTML to Lexical (or, importing HTML into the lexical editor)
- Markdown to Lexical (or, importing Markdown into the lexical editor)
- Lexical to Markdown
A headless editor can perform such conversions outside of the main editor instance. Follow this method to initiate a headless editor:
```ts
import { createHeadlessEditor } from '@lexical/headless' // <= make sure this package is installed
import {
getEnabledNodes,
sanitizeEditorConfig,
} from '@payloadcms/richtext-lexical'
const yourEditorConfig; // <= your editor config here
const headlessEditor = await createHeadlessEditor({
nodes: getEnabledNodes({
editorConfig: sanitizeEditorConfig(yourEditorConfig),
}),
})
```
### HTML => Lexical
Once you have your headless editor instance, you can use it to convert HTML to Lexical:
```ts
import { $generateNodesFromDOM } from '@lexical/html'
import { $getRoot,$getSelection } from 'lexical'
headlessEditor.update(() => {
// In a headless environment you can use a package such as JSDom to parse the HTML string.
const dom = new JSDOM(htmlString)
// Once you have the DOM instance it's easy to generate LexicalNodes.
const nodes = $generateNodesFromDOM(editor, dom.window.document)
// Select the root
$getRoot().select()
// Insert them at a selection.
const selection = $getSelection()
selection.insertNodes(nodes)
}, { discrete: true })
// Do this if you then want to get the editor JSON
const editorJSON = headlessEditor.getEditorState().toJSON()
```
This has been taken from the [lexical serialization & deserialization docs](https://lexical.dev/docs/concepts/serialization#html---lexical).
<Banner type="success">
<strong>Note:</strong>
<br />
Using the <code>discrete: true</code> flag ensures instant updates to the editor state. If immediate reading of the updated state isn't necessary, you can omit the flag.
</Banner>
### Markdown => Lexical
Convert markdown content to the Lexical editor format with the following:
```ts
import { $convertFromMarkdownString } from '@lexical/markdown'
import { sanitizeEditorConfig } from '@payloadcms/richtext-lexical'
const yourSanitizedEditorConfig = sanitizeEditorConfig(yourEditorConfig) // <= your editor config here
const markdown = `# Hello World`
headlessEditor.update(() => { $convertFromMarkdownString(markdown, yourSanitizedEditorConfig.features.markdownTransformers) }, { discrete: true })
// Do this if you then want to get the editor JSON
const editorJSON = headlessEditor.getEditorState().toJSON()
```
### Lexical => Markdown
Export content from the Lexical editor into Markdown format using these steps:
1. Import your current editor state into the headless editor.
2. Convert and fetch the resulting markdown string.
Here's the code for it:
```ts
import { $convertToMarkdownString } from '@lexical/markdown'
import { sanitizeEditorConfig } from '@payloadcms/richtext-lexical'
import type { SerializedEditorState } from "lexical"
const yourSanitizedEditorConfig = sanitizeEditorConfig(yourEditorConfig) // <= your editor config here
const yourEditorState: SerializedEditorState // <= your current editor state here
// Import editor state into your headless editor
try {
headlessEditor.setEditorState(headlessEditor.parseEditorState(yourEditorState)) // This should commit the editor state immediately
} catch (e) {
logger.error({ err: e }, 'ERROR parsing editor state')
return ''
}
// Export to markdown
let markdown: string
headlessEditor.getEditorState().read(() => {
markdown = $convertToMarkdownString(yourSanitizedEditorConfig?.features?.markdownTransformers)
})
```
The `.setEditorState()` function immediately updates your editor state. Thus, there's no need for the `discrete: true` flag when reading the state afterward.
## Migrating from Slate
While both Slate and Lexical save the editor state in JSON, the structure of the JSON is different.
### Migration via SlateToLexicalFeature
One way to handle this is to just give your lexical editor the ability to read the slate JSON.
Simply add the `SlateToLexicalFeature` to your editor:
```ts
import type { CollectionConfig } from 'payload/types'
import {
SlateToLexicalFeature,
lexicalEditor,
} from '@payloadcms/richtext-lexical'
const Pages: CollectionConfig = {
slug: 'pages',
fields: [
{
name: 'nameOfYourRichTextField',
type: 'richText',
editor: lexicalEditor({
features: ({ defaultFeatures }) => [
...defaultFeatures,
SlateToLexicalFeature({})
],
}),
},
],
}
```
and done! Now, everytime this lexical editor is initialized, it converts the slate date to lexical on-the-fly. If the data is already in lexical format, it will just pass it through.
This is by far the easiest way to migrate from Slate to Lexical, although it does come with a few caveats:
- There is a performance hit when initializing the lexical editor
- The editor will still output the Slate data in the output JSON, as the on-the-fly converter only runs for the admin panel
The easy way to solve this: Just save the document! This overrides the slate data with the lexical data, and the next time the document is loaded, the lexical data will be used. This solves both the performance and the output issue for that specific document.
### Migration via migration script
The method described above does not solve the issue for all documents, though. If you want to convert all your documents to lexical, you can use a migration script. Here's a simple example:
```ts
import type { Payload } from 'payload'
import type { YourDocumentType } from 'payload/generated-types'
import {
cloneDeep,
convertSlateToLexical,
defaultSlateConverters,
} from '@payloadcms/richtext-lexical'
import { AnotherCustomConverter } from './lexicalFeatures/converters/AnotherCustomConverter'
export async function convertAll(payload: Payload, collectionName: string, fieldName: string) {
const docs: YourDocumentType[] = await payload.db.collections[collectionName].find({}).exec() // Use MongoDB models directly to query all documents at once
console.log(`Found ${docs.length} ${collectionName} docs`)
const converters = cloneDeep([...defaultSlateConverters, AnotherCustomConverter])
// Split docs into batches of 20.
const batchSize = 20
const batches = []
for (let i = 0; i < docs.length; i += batchSize) {
batches.push(docs.slice(i, i + batchSize))
}
let processed = 0 // Number of processed docs
for (const batch of batches) {
// Process each batch asynchronously
const promises = batch.map(async (doc: YourDocumentType) => {
const richText = doc[fieldName]
if (richText && Array.isArray(richText) && !('root' in richText)) { // It's Slate data - skip already-converted data
const converted = convertSlateToLexical({
converters: converters,
slateData: richText,
})
await payload.update({
id: doc.id,
collection: collectionName as any,
data: {
[fieldName]: converted,
},
})
}
})
// Wait for all promises in the batch to complete. Resolving batches of 20 asynchronously is faster than waiting for each doc to update individually
await Promise.all(promises)
// Update the count of processed docs
processed += batch.length
console.log(`Converted ${processed} of ${docs.length}`)
}
}
```
The `convertSlateToLexical` is the same method used in the `SlateToLexicalFeature` - it handles traversing the Slate JSON for you.
Do note that this script might require adjustment depending on your document structure, especially if you have nested richText fields or localization enabled.
### Converting custom Slate nodes
If you have custom Slate nodes, create a custom converter for them. Here's the Upload converter as an example:
```ts
import type { SerializedUploadNode } from '../uploadNode.'
import type { SlateNodeConverter } from '@payloadcms/richtext-lexical'
export const SlateUploadConverter: SlateNodeConverter = {
converter({ slateNode }) {
return {
fields: {
...slateNode.fields,
},
format: '',
relationTo: slateNode.relationTo,
type: 'upload',
value: {
id: slateNode.value?.id || '',
},
version: 1,
} as const as SerializedUploadNode
},
nodeTypes: ['upload'],
}
```
It's pretty simple: You get a Slate node as input, and you return the lexical node. The `nodeTypes` array is used to determine which Slate nodes this converter can handle.
When using a migration script, you can add your custom converters to the `converters` property of the `convertSlateToLexical` props, as seen in the example above
When using the `SlateToLexicalFeature`, you can add your custom converters to the `converters` property of the `SlateToLexicalFeature` props:
```ts
import type { CollectionConfig } from 'payload/types'
import {
SlateToLexicalFeature,
lexicalEditor,
defaultSlateConverters
} from '@payloadcms/richtext-lexical'
import { YourCustomConverter } from '../converters/YourCustomConverter'
const Pages: CollectionConfig = {
slug: 'pages',
fields: [
{
name: 'nameOfYourRichTextField',
type: 'richText',
editor: lexicalEditor({
features: ({ defaultFeatures }) => [
...defaultFeatures,
SlateToLexicalFeature({
converters: [
...defaultSlateConverters,
YourCustomConverter
]
}),
],
}),
},
],
}
```
## Migrating from payload-plugin-lexical
Migrating from [payload-plugin-lexical](https://github.com/AlessioGr/payload-plugin-lexical) works similar to migrating from Slate.
Instead of a `SlateToLexicalFeature` there is a `LexicalPluginToLexicalFeature` you can use. And instead of `convertSlateToLexical` you can use `convertLexicalPluginToLexical`.
## Coming Soon
Lots more documentation will be coming soon, which will show in detail how to create your own custom features within Lexical.

View File

@@ -9,7 +9,7 @@ keywords: headless cms, typescript, documentation, Content Management System, cm
Payload supports TypeScript natively, and not only that, the entirety of the CMS is built with TypeScript. To get started developing with Payload and TypeScript, you can use one of Payload's built-in boilerplates in one line via `create-payload-app`:
```
npx create-payload-app
npx create-payload-app@latest
```
Pick a TypeScript project type to get started easily.

View File

@@ -21,7 +21,7 @@ Collections and Globals both support the same options for configuring autosave.
| Drafts Autosave Options | Description |
| ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `interval` | Define an `interval` in milliseconds to automatically save progress while documents are edited. Document updates are "debounced" at this interval. Defaults to `2000`. |
| `interval` | Define an `interval` in milliseconds to automatically save progress while documents are edited. Document updates are "debounced" at this interval. Defaults to `800`. |
**Example config with versions, drafts, and autosave enabled:**
@@ -66,7 +66,7 @@ When `autosave` is enabled, all `update` operations within Payload expose a new
#### How autosaves are stored
If we created a new version for each autosave, you'd quickly find a ton of autosaves that clutter up your `_versions` collection within the database. That would be messy quick because `autosave` is typically set to save a document every ~2000ms or so.
If we created a new version for each autosave, you'd quickly find a ton of autosaves that clutter up your `_versions` collection within the database. That would be messy quick because `autosave` is typically set to save a document at ~800ms intervals.
<Banner type="success">
Instead of creating a new version each time a document is autosaved, Payload smartly only creates{' '}

View File

@@ -24,7 +24,7 @@
"script:release": "tsx ./scripts/release.ts",
"test": "pnpm test:int && pnpm test:components && pnpm test:e2e",
"test:components": "cross-env jest --config=jest.components.config.js",
"test:e2e": "npx playwright install --with-deps && ts-node -T ./test/runE2E.ts",
"test:e2e": "npx playwright install --with-deps chromium && ts-node -T ./test/runE2E.ts",
"test:e2e:debug": "cross-env PWDEBUG=1 DISABLE_LOGGING=true playwright test",
"test:e2e:headed": "cross-env DISABLE_LOGGING=true playwright test --headed",
"test:int:postgres": "cross-env PAYLOAD_DATABASE=postgres DISABLE_LOGGING=true jest --forceExit --detectOpenHandles",
@@ -34,7 +34,7 @@
},
"devDependencies": {
"@payloadcms/eslint-config": "workspace:*",
"@playwright/test": "1.38.1",
"@playwright/test": "1.39.0",
"@swc/cli": "^0.1.62",
"@swc/jest": "0.2.29",
"@swc/register": "0.1.10",
@@ -45,7 +45,7 @@
"@types/conventional-changelog-core": "^4.2.5",
"@types/conventional-changelog-preset-loader": "^2.3.4",
"@types/fs-extra": "^11.0.2",
"@types/jest": "29.5.4",
"@types/jest": "29.5.7",
"@types/minimist": "1.2.2",
"@types/node": "20.5.7",
"@types/prompts": "^2.4.5",
@@ -64,6 +64,7 @@
"copyfiles": "2.4.1",
"cross-env": "7.0.3",
"dotenv": "8.6.0",
"drizzle-orm": "0.28.5",
"express": "4.18.2",
"form-data": "3.0.1",
"fs-extra": "10.1.0",
@@ -73,8 +74,8 @@
"graphql-request": "6.1.0",
"husky": "^8.0.3",
"isomorphic-fetch": "3.0.0",
"jest": "29.6.4",
"jest-environment-jsdom": "29.6.4",
"jest": "29.7.0",
"jest-environment-jsdom": "29.7.0",
"jwt-decode": "3.1.2",
"lint-staged": "^14.0.1",
"minimist": "1.2.8",
@@ -111,5 +112,8 @@
"*.{js,jsx,ts,tsx}": [
"prettier --write"
]
},
"dependencies": {
"@sentry/react": "^7.77.0"
}
}

View File

@@ -63,7 +63,6 @@ export const getViteConfig = async (payloadConfig: SanitizedConfig): Promise<Inl
'module.hot': 'undefined',
'process.argv': '[]',
'process.cwd': 'function () { return "/" }',
'process.env': '{}',
'process?.cwd': 'function () { return "/" }',
}
@@ -91,6 +90,7 @@ export const getViteConfig = async (payloadConfig: SanitizedConfig): Promise<Inl
// Dependencies that need aliases should be excluded
// from pre-bundling
'@payloadcms/bundler-vite',
...(Object.keys(absoluteAliases) || []),
],
include: ['payload/components/root', 'react-dom/client'],
},

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/bundler-webpack",
"version": "1.0.4",
"version": "1.0.5",
"description": "The officially supported Webpack bundler adapter for Payload",
"repository": "https://github.com/payloadcms/payload",
"license": "MIT",

View File

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

View File

@@ -30,8 +30,11 @@ export const createMigration: CreateMigration = async function createMigration({
let migrationFileContent: string | undefined
// Check for predefined migration
if (file) {
// Check for predefined migration.
// Either passed in via --file or prefixed with @payloadcms/db-mongodb/
if (file || migrationName.startsWith('@payloadcms/db-mongodb/')) {
if (!file) file = migrationName
const predefinedMigrationName = file.replace('@payloadcms/db-mongodb/', '')
migrationName = predefinedMigrationName
const cleanPath = path.join(__dirname, `../predefinedMigrations/${predefinedMigrationName}.js`)

View File

@@ -4,6 +4,7 @@ export const commitTransaction: CommitTransaction = async function commitTransac
if (!this.sessions[id]?.inTransaction()) {
return
}
await this.sessions[id].commitTransaction()
await this.sessions[id].endSession()
delete this.sessions[id]

View File

@@ -3,10 +3,20 @@ import type { RollbackTransaction } from 'payload/database'
export const rollbackTransaction: RollbackTransaction = async function rollbackTransaction(
id = '',
) {
if (!this.sessions[id]?.inTransaction()) {
this.payload.logger.warn('rollbackTransaction called when no transaction exists')
// if multiple operations are using the same transaction, the first will flow through and delete the session.
// subsequent calls should be ignored.
if (!this.sessions[id]) {
return
}
// when session exists but is not inTransaction something unexpected is happening to the session
if (!this.sessions[id].inTransaction()) {
this.payload.logger.warn('rollbackTransaction called when no transaction exists')
delete this.sessions[id]
return
}
// the first call for rollback should be aborted and deleted causing any other operations with the same transaction to fail
await this.sessions[id].abortTransaction()
await this.sessions[id].endSession()
delete this.sessions[id]

View File

@@ -7,9 +7,17 @@ import { withSession } from './withSession'
export async function updateGlobalVersion<T extends TypeWithID>(
this: MongooseAdapter,
{ global, locale, req = {} as PayloadRequest, versionData, where }: UpdateGlobalVersionArgs<T>,
{
id,
global,
locale,
req = {} as PayloadRequest,
versionData,
where,
}: UpdateGlobalVersionArgs<T>,
) {
const VersionModel = this.versions[global]
const whereToUse = where || { id: { equals: id } }
const options = {
...withSession(this, req.transactionID),
lean: true,
@@ -19,7 +27,7 @@ export async function updateGlobalVersion<T extends TypeWithID>(
const query = await VersionModel.buildQuery({
locale,
payload: this.payload,
where,
where: whereToUse,
})
const doc = await VersionModel.findOneAndUpdate(query, versionData, options)

View File

@@ -7,9 +7,10 @@ import { withSession } from './withSession'
export const updateVersion: UpdateVersion = async function updateVersion(
this: MongooseAdapter,
{ collection, locale, req = {} as PayloadRequest, versionData, where },
{ id, collection, locale, req = {} as PayloadRequest, versionData, where },
) {
const VersionModel = this.versions[collection]
const whereToUse = where || { id: { equals: id } }
const options = {
...withSession(this, req.transactionID),
lean: true,
@@ -19,7 +20,7 @@ export const updateVersion: UpdateVersion = async function updateVersion(
const query = await VersionModel.buildQuery({
locale,
payload: this.payload,
where,
where: whereToUse,
})
const doc = await VersionModel.findOneAndUpdate(query, versionData, options)

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/db-postgres",
"version": "0.1.11",
"version": "0.1.12",
"description": "The officially supported Postgres database adapter for Payload",
"repository": "https://github.com/payloadcms/payload",
"license": "MIT",

View File

@@ -23,6 +23,7 @@ export const init: Init = async function init(this: PostgresAdapter) {
buildTable({
adapter: this,
buildNumbers: true,
buildRelationships: true,
disableNotNull: !!collection?.versions?.drafts,
disableUnique: false,
@@ -37,6 +38,7 @@ export const init: Init = async function init(this: PostgresAdapter) {
buildTable({
adapter: this,
buildNumbers: true,
buildRelationships: true,
disableNotNull: !!collection.versions?.drafts,
disableUnique: true,
@@ -52,6 +54,7 @@ export const init: Init = async function init(this: PostgresAdapter) {
buildTable({
adapter: this,
buildNumbers: true,
buildRelationships: true,
disableNotNull: !!global?.versions?.drafts,
disableUnique: false,
@@ -66,6 +69,7 @@ export const init: Init = async function init(this: PostgresAdapter) {
buildTable({
adapter: this,
buildNumbers: true,
buildRelationships: true,
disableNotNull: !!global.versions?.drafts,
disableUnique: true,

View File

@@ -26,6 +26,7 @@ type Args = {
adapter: PostgresAdapter
baseColumns?: Record<string, PgColumnBuilder>
baseExtraConfig?: Record<string, (cols: GenericColumns) => IndexBuilder | UniqueConstraintBuilder>
buildNumbers?: boolean
buildRelationships?: boolean
disableNotNull: boolean
disableUnique: boolean
@@ -39,6 +40,7 @@ type Args = {
}
type Result = {
hasManyNumberField: 'index' | boolean
relationsToBuild: Map<string, string>
}
@@ -46,6 +48,7 @@ export const buildTable = ({
adapter,
baseColumns = {},
baseExtraConfig = {},
buildNumbers,
buildRelationships,
disableNotNull,
disableUnique = false,
@@ -53,10 +56,11 @@ export const buildTable = ({
rootRelationsToBuild,
rootRelationships,
rootTableIDColType,
rootTableName,
rootTableName: incomingRootTableName,
tableName,
timestamps,
}: Args): Result => {
const rootTableName = incomingRootTableName || tableName
const columns: Record<string, PgColumnBuilder> = baseColumns
const indexes: Record<string, (cols: GenericColumns) => IndexBuilder> = {}
@@ -102,6 +106,7 @@ export const buildTable = ({
hasManyNumberField,
} = traverseFields({
adapter,
buildNumbers,
buildRelationships,
columns,
disableNotNull,
@@ -116,7 +121,7 @@ export const buildTable = ({
relationships,
rootRelationsToBuild: rootRelationsToBuild || relationsToBuild,
rootTableIDColType: rootTableIDColType || idColType,
rootTableName: rootTableName || tableName,
rootTableName,
}))
if (timestamps) {
@@ -185,8 +190,8 @@ export const buildTable = ({
adapter.relations[`relations_${localeTableName}`] = localesTableRelations
}
if (hasManyNumberField) {
const numbersTableName = `${tableName}_numbers`
if (hasManyNumberField && buildNumbers) {
const numbersTableName = `${rootTableName}_numbers`
const columns: Record<string, PgColumnBuilder> = {
id: serial('id').primaryKey(),
number: numeric('number'),
@@ -327,5 +332,5 @@ export const buildTable = ({
adapter.relations[`relations_${tableName}`] = tableRelations
return { relationsToBuild }
return { hasManyNumberField, relationsToBuild }
}

View File

@@ -1,23 +1,24 @@
/* eslint-disable no-param-reassign */
import type { Relation } from 'drizzle-orm'
import { relations } from 'drizzle-orm'
import type { IndexBuilder, PgColumnBuilder, UniqueConstraintBuilder } from 'drizzle-orm/pg-core'
import type { Field, TabAsField } from 'payload/types'
import { relations } from 'drizzle-orm'
import {
PgNumericBuilder,
PgVarcharBuilder,
boolean,
index,
integer,
jsonb,
numeric,
pgEnum,
PgNumericBuilder,
PgVarcharBuilder,
text,
timestamp,
varchar,
} from 'drizzle-orm/pg-core'
import type { Field, TabAsField } from 'payload/types'
import { fieldAffectsData, optionIsObject } from 'payload/types'
import { InvalidConfiguration } from 'payload/errors'
import { fieldAffectsData, optionIsObject } from 'payload/types'
import toSnakeCase from 'to-snake-case'
import type { GenericColumns, PostgresAdapter } from '../types'
@@ -31,6 +32,7 @@ import { validateExistingBlockIsIdentical } from './validateExistingBlockIsIdent
type Args = {
adapter: PostgresAdapter
buildNumbers: boolean
buildRelationships: boolean
columnPrefix?: string
columns: Record<string, PgColumnBuilder>
@@ -60,6 +62,7 @@ type Result = {
export const traverseFields = ({
adapter,
buildNumbers,
buildRelationships,
columnPrefix,
columns,
@@ -283,19 +286,25 @@ export const traverseFields = ({
baseExtraConfig._localeIdx = (cols) => index('_locale_idx').on(cols._locale)
}
const { relationsToBuild: subRelationsToBuild } = buildTable({
adapter,
baseColumns,
baseExtraConfig,
disableNotNull: disableNotNullFromHere,
disableUnique,
fields: disableUnique ? idToUUID(field.fields) : field.fields,
rootRelationsToBuild,
rootRelationships: relationships,
rootTableIDColType,
rootTableName,
tableName: arrayTableName,
})
const { hasManyNumberField: subHasManyNumberField, relationsToBuild: subRelationsToBuild } =
buildTable({
adapter,
baseColumns,
baseExtraConfig,
disableNotNull: disableNotNullFromHere,
disableUnique,
fields: disableUnique ? idToUUID(field.fields) : field.fields,
rootRelationsToBuild,
rootRelationships: relationships,
rootTableIDColType,
rootTableName,
tableName: arrayTableName,
})
if (subHasManyNumberField) {
if (!hasManyNumberField || subHasManyNumberField === 'index')
hasManyNumberField = subHasManyNumberField
}
relationsToBuild.set(fieldName, arrayTableName)
@@ -351,7 +360,10 @@ export const traverseFields = ({
baseExtraConfig._localeIdx = (cols) => index('locale_idx').on(cols._locale)
}
const { relationsToBuild: subRelationsToBuild } = buildTable({
const {
hasManyNumberField: subHasManyNumberField,
relationsToBuild: subRelationsToBuild,
} = buildTable({
adapter,
baseColumns,
baseExtraConfig,
@@ -365,6 +377,11 @@ export const traverseFields = ({
tableName: blockTableName,
})
if (subHasManyNumberField) {
if (!hasManyNumberField || subHasManyNumberField === 'index')
hasManyNumberField = subHasManyNumberField
}
const blockTableRelations = relations(
adapter.tables[blockTableName],
({ many, one }) => {
@@ -413,6 +430,7 @@ export const traverseFields = ({
hasManyNumberField: groupHasManyNumberField,
} = traverseFields({
adapter,
buildNumbers,
buildRelationships,
columnPrefix,
columns,
@@ -449,6 +467,7 @@ export const traverseFields = ({
hasManyNumberField: groupHasManyNumberField,
} = traverseFields({
adapter,
buildNumbers,
buildRelationships,
columnPrefix: `${columnName}_`,
columns,
@@ -486,6 +505,7 @@ export const traverseFields = ({
hasManyNumberField: tabHasManyNumberField,
} = traverseFields({
adapter,
buildNumbers,
buildRelationships,
columnPrefix,
columns,
@@ -524,6 +544,7 @@ export const traverseFields = ({
hasManyNumberField: rowHasManyNumberField,
} = traverseFields({
adapter,
buildNumbers,
buildRelationships,
columnPrefix,
columns,

View File

@@ -1,6 +1,7 @@
import type { CommitTransaction } from 'payload/database'
export const commitTransaction: CommitTransaction = async function commitTransaction(id) {
// if the session was deleted it has already been aborted
if (!this.sessions[id]) {
return
}

View File

@@ -3,12 +3,15 @@ import type { RollbackTransaction } from 'payload/database'
export const rollbackTransaction: RollbackTransaction = async function rollbackTransaction(
id = '',
) {
// if multiple operations are using the same transaction, the first will flow through and delete the session.
// subsequent calls should be ignored.
if (!this.sessions[id]) {
this.payload.logger.warn('rollbackTransaction called when no transaction exists')
return
}
// end the session promise in failure by calling reject
await this.sessions[id].reject()
// delete the session causing any other operations with the same transaction to fail
delete this.sessions[id]
}

View File

@@ -14,6 +14,7 @@ type Args = {
blocks: {
[blockType: string]: BlockRowToInsert[]
}
blocksToDelete: Set<string>
data: unknown
field: ArrayField
locale?: string
@@ -31,6 +32,7 @@ export const transformArray = ({
arrayTableName,
baseTableName,
blocks,
blocksToDelete,
data,
field,
locale,
@@ -78,6 +80,7 @@ export const transformArray = ({
arrays: newRow.arrays,
baseTableName,
blocks,
blocksToDelete,
columnPrefix: '',
data: arrayRow,
fieldPrefix: '',

View File

@@ -14,6 +14,7 @@ type Args = {
blocks: {
[blockType: string]: BlockRowToInsert[]
}
blocksToDelete: Set<string>
data: Record<string, unknown>[]
field: BlockField
locale?: string
@@ -29,6 +30,7 @@ export const transformBlocks = ({
adapter,
baseTableName,
blocks,
blocksToDelete,
data,
field,
locale,
@@ -76,6 +78,7 @@ export const transformBlocks = ({
arrays: newRow.arrays,
baseTableName,
blocks,
blocksToDelete,
columnPrefix: '',
data: blockRow,
fieldPrefix: '',

View File

@@ -25,6 +25,7 @@ export const transformForWrite = ({
const rowToInsert: RowToInsert = {
arrays: {},
blocks: {},
blocksToDelete: new Set(),
locales: {},
numbers: [],
relationships: [],
@@ -40,6 +41,7 @@ export const transformForWrite = ({
arrays: rowToInsert.arrays,
baseTableName: tableName,
blocks: rowToInsert.blocks,
blocksToDelete: rowToInsert.blocksToDelete,
columnPrefix: '',
data,
fieldPrefix: '',

View File

@@ -3,16 +3,18 @@ import { isArrayOfRows } from '../../utilities/isArrayOfRows'
type Args = {
data: unknown
id?: unknown
locale?: string
}
export const transformSelects = ({ data, locale }: Args) => {
export const transformSelects = ({ id, data, locale }: Args) => {
const newRows: Record<string, unknown>[] = []
if (isArrayOfRows(data)) {
data.forEach((value, i) => {
const newRow: Record<string, unknown> = {
order: i + 1,
parent: id,
value,
}

View File

@@ -26,6 +26,7 @@ type Args = {
blocks: {
[blockType: string]: BlockRowToInsert[]
}
blocksToDelete: Set<string>
/**
* A snake-case field prefix, representing prior fields
* Ex: my_group_my_named_tab_
@@ -62,6 +63,7 @@ export const traverseFields = ({
arrays,
baseTableName,
blocks,
blocksToDelete,
columnPrefix,
data,
existingLocales,
@@ -102,6 +104,7 @@ export const traverseFields = ({
arrayTableName,
baseTableName,
blocks,
blocksToDelete,
data: localeData,
field,
locale: localeKey,
@@ -122,6 +125,7 @@ export const traverseFields = ({
arrayTableName,
baseTableName,
blocks,
blocksToDelete,
data: data[field.name],
field,
numbers,
@@ -138,6 +142,10 @@ export const traverseFields = ({
}
if (field.type === 'blocks') {
field.blocks.forEach(({ slug }) => {
blocksToDelete.add(toSnakeCase(slug))
})
if (field.localized) {
if (typeof data[field.name] === 'object' && data[field.name] !== null) {
Object.entries(data[field.name]).forEach(([localeKey, localeData]) => {
@@ -146,6 +154,7 @@ export const traverseFields = ({
adapter,
baseTableName,
blocks,
blocksToDelete,
data: localeData,
field,
locale: localeKey,
@@ -163,6 +172,7 @@ export const traverseFields = ({
adapter,
baseTableName,
blocks,
blocksToDelete,
data: fieldData,
field,
numbers,
@@ -185,6 +195,7 @@ export const traverseFields = ({
arrays,
baseTableName,
blocks,
blocksToDelete,
columnPrefix: `${columnName}_`,
data: localeData as Record<string, unknown>,
existingLocales,
@@ -207,6 +218,7 @@ export const traverseFields = ({
arrays,
baseTableName,
blocks,
blocksToDelete,
columnPrefix: `${columnName}_`,
data: data[field.name] as Record<string, unknown>,
existingLocales,
@@ -238,6 +250,7 @@ export const traverseFields = ({
arrays,
baseTableName,
blocks,
blocksToDelete,
columnPrefix: `${columnPrefix || ''}${toSnakeCase(tab.name)}_`,
data: localeData as Record<string, unknown>,
existingLocales,
@@ -260,6 +273,7 @@ export const traverseFields = ({
arrays,
baseTableName,
blocks,
blocksToDelete,
columnPrefix: `${columnPrefix || ''}${toSnakeCase(tab.name)}_`,
data: data[tab.name] as Record<string, unknown>,
existingLocales,
@@ -282,6 +296,7 @@ export const traverseFields = ({
arrays,
baseTableName,
blocks,
blocksToDelete,
columnPrefix,
data,
existingLocales,
@@ -306,6 +321,7 @@ export const traverseFields = ({
arrays,
baseTableName,
blocks,
blocksToDelete,
columnPrefix,
data,
existingLocales,
@@ -406,6 +422,7 @@ export const traverseFields = ({
Object.entries(data[field.name]).forEach(([localeKey, localeData]) => {
if (Array.isArray(localeData)) {
const newRows = transformSelects({
id: data._uuid || data.id,
data: localeData,
locale: localeKey,
})
@@ -416,6 +433,7 @@ export const traverseFields = ({
}
} else if (Array.isArray(data[field.name])) {
const newRows = transformSelects({
id: data._uuid || data.id,
data: data[field.name],
})

View File

@@ -30,6 +30,7 @@ export type RowToInsert = {
blocks: {
[blockType: string]: BlockRowToInsert[]
}
blocksToDelete: Set<string>
locales: {
[locale: string]: Record<string, unknown>
}

View File

@@ -2,6 +2,8 @@
import type { TypeWithID } from 'payload/types'
import { eq } from 'drizzle-orm'
import { ValidationError } from 'payload/errors'
import { i18nInit } from 'payload/utilities'
import type { BlockRowToInsert } from '../transform/write/types'
import type { Args } from './types'
@@ -12,8 +14,6 @@ import { transformForWrite } from '../transform/write'
import { deleteExistingArrayRows } from './deleteExistingArrayRows'
import { deleteExistingRowsByPath } from './deleteExistingRowsByPath'
import { insertArrays } from './insertArrays'
import { ValidationError } from 'payload/errors'
import { i18nInit } from 'payload/utilities'
export const upsertRow = async <T extends TypeWithID>({
id,
@@ -102,7 +102,9 @@ export const upsertRow = async <T extends TypeWithID>({
if (Object.keys(rowToInsert.selects).length > 0) {
Object.entries(rowToInsert.selects).forEach(([selectTableName, selectRows]) => {
selectRows.forEach((row) => {
row.parent = insertedRow.id
if (typeof row.parent === 'undefined') {
row.parent = insertedRow.id
}
if (!selectsToInsert[selectTableName]) selectsToInsert[selectTableName] = []
selectsToInsert[selectTableName].push(row)
})
@@ -188,18 +190,15 @@ export const upsertRow = async <T extends TypeWithID>({
const insertedBlockRows: Record<string, Record<string, unknown>[]> = {}
for (const [blockName, blockRows] of Object.entries(blocksToInsert)) {
if (operation === 'update') {
await deleteExistingRowsByPath({
adapter,
db,
parentID: insertedRow.id,
pathColumnName: '_path',
rows: blockRows.map(({ row }) => row),
tableName: `${tableName}_blocks_${blockName}`,
})
if (operation === 'update') {
for (const blockName of rowToInsert.blocksToDelete) {
const blockTableName = `${tableName}_blocks_${blockName}`
const blockTable = adapter.tables[blockTableName]
await db.delete(blockTable).where(eq(blockTable._parentID, insertedRow.id))
}
}
for (const [blockName, blockRows] of Object.entries(blocksToInsert)) {
insertedBlockRows[blockName] = await db
.insert(adapter.tables[`${tableName}_blocks_${blockName}`])
.values(blockRows.map(({ row }) => row))

View File

@@ -26,9 +26,8 @@
</h4>
<hr/>
<h3>
🎉 Payload 2.0 is now available! Read more in the <a target="_blank" href="https://payloadcms.com/blog/payload-2-0" rel="dofollow"><strong>announcement post</strong></a>
</h3>
> [!IMPORTANT]
> 🎉 <strong>Payload 2.0 is now available!</strong> Read more in the <a target="_blank" href="https://payloadcms.com/blog/payload-2-0" rel="dofollow"><strong>announcement post</strong></a>.
<h3>Benefits over a regular CMS</h3>
<ul>
@@ -52,7 +51,7 @@ Create a cloud account, connect your GitHub, and [deploy in minutes](https://pay
Before beginning to work with Payload, make sure you have all of the [required software](https://payloadcms.com/docs/getting-started/installation).
```text
npx create-payload-app
npx create-payload-app@latest
```
Alternatively, it only takes about five minutes to [create an app from scratch](https://payloadcms.com/docs/getting-started/installation#from-scratch).

View File

@@ -1,6 +1,6 @@
{
"name": "payload",
"version": "2.0.14",
"version": "2.1.0",
"description": "Node, React and MongoDB Headless CMS and Application Framework",
"license": "MIT",
"main": "./dist/index.js",
@@ -135,7 +135,7 @@
"terser-webpack-plugin": "5.3.9",
"ts-essentials": "7.0.3",
"use-context-selector": "1.4.1",
"uuid": "8.3.2"
"uuid": "9.0.1"
},
"devDependencies": {
"@payloadcms/eslint-config": "workspace:*",

View File

@@ -22,6 +22,7 @@ const DateTime: React.FC<Props> = (props) => {
minTime,
monthsToShow = 1,
onChange: onChangeFromProps,
overrides,
pickerAppearance = 'default',
placeholder: placeholderText,
readOnly,
@@ -77,6 +78,7 @@ const DateTime: React.FC<Props> = (props) => {
showTimeSelect: pickerAppearance === 'dayAndTime' || pickerAppearance === 'timeOnly',
timeFormat,
timeIntervals,
...overrides,
}
const classes = [baseClass, `${baseClass}__appearance--${pickerAppearance}`]

View File

@@ -1,5 +1,8 @@
import type { ReactDatePickerProps } from 'react-datepicker'
type SharedProps = {
displayFormat?: string
overrides?: ReactDatePickerProps
pickerAppearance?: 'dayAndTime' | 'dayOnly' | 'default' | 'monthOnly' | 'timeOnly'
}

View File

@@ -62,7 +62,9 @@ const Content: React.FC<DocumentDrawerProps> = ({
? Edit
: typeof Edit === 'object' && typeof Edit.Default === 'function'
? Edit.Default
: typeof Edit?.Default === 'object' && typeof Edit.Default.Component === 'function'
: typeof Edit?.Default === 'object' &&
'Component' in Edit.Default &&
typeof Edit.Default.Component === 'function'
? Edit.Default.Component
: undefined

View File

@@ -1,5 +1,6 @@
import type { EditViewConfig } from '../../../../../exports/config'
import type { SanitizedCollectionConfig, SanitizedGlobalConfig } from '../../../../../exports/types'
import type { SanitizedCollectionConfig } from '../../../../../collections/config/types'
import type { EditViewConfig } from '../../../../../config/types'
import type { SanitizedGlobalConfig } from '../../../../../globals/config/types'
import { defaultGlobalViews } from '../../../views/Global/Routes/CustomComponent'
import { defaultCollectionViews } from '../../../views/collections/Edit/Routes/CustomComponent'

View File

@@ -0,0 +1,33 @@
import type { SanitizedCollectionConfig } from '../../../../../collections/config/types'
import type { EditViewConfig } from '../../../../../config/types'
import type { SanitizedGlobalConfig } from '../../../../../globals/config/types'
export const getViewConfig = (args: {
collection: SanitizedCollectionConfig
global: SanitizedGlobalConfig
name: string
}): EditViewConfig => {
const { name, collection, global } = args
if (collection) {
const collectionViewsConfig =
typeof collection?.admin?.components?.views?.Edit === 'object' &&
typeof collection?.admin?.components?.views?.Edit !== 'function'
? collection?.admin?.components?.views?.Edit
: undefined
return collectionViewsConfig?.[name]
}
if (global) {
const globalViewsConfig =
typeof global?.admin?.components?.views?.Edit === 'object' &&
typeof global?.admin?.components?.views?.Edit !== 'function'
? global?.admin?.components?.views?.Edit
: undefined
return globalViewsConfig?.[name]
}
return null
}

View File

@@ -4,8 +4,9 @@ import type { DocumentTabProps } from './types'
import { DocumentTab } from './Tab'
import { getCustomViews } from './getCustomViews'
import { getViewConfig } from './getViewConfig'
import './index.scss'
import { tabs } from './tabs'
import { tabs as defaultViews } from './tabs'
const baseClass = 'doc-tabs'
@@ -19,17 +20,50 @@ export const DocumentTabs: React.FC<DocumentTabProps> = (props) => {
<div className={baseClass}>
<div className={`${baseClass}__tabs-container`}>
<ul className={`${baseClass}__tabs`}>
{tabs?.map((Tab, index) => {
return <DocumentTab {...props} {...Tab} key={`tab-${index}`} />
})}
{Object.entries(defaultViews)
// sort `defaultViews` based on `order` property from smallest to largest
// if no `order`, append the view to the end
// TODO: open `order` to the config and merge `defaultViews` with `customViews`
?.sort(([, a], [, b]) => {
if (a.order === undefined && b.order === undefined) return 0
else if (a.order === undefined) return 1
else if (b.order === undefined) return -1
return a.order - b.order
})
?.map(([name, Tab], index) => {
const viewConfig = getViewConfig({ name, collection, global })
const tabOverrides = viewConfig && 'Tab' in viewConfig ? viewConfig.Tab : undefined
return (
<DocumentTab
{...{
...props,
...(Tab || {}),
...(tabOverrides || {}),
}}
key={`tab-${index}`}
/>
)
})}
{customViews?.map((CustomView, index) => {
const { Tab, path } = CustomView
if ('Tab' in CustomView) {
const { Tab, path } = CustomView
if (typeof Tab === 'function') {
return <Tab path={path} {...props} key={`tab-custom-${index}`} />
if (typeof Tab === 'function') {
return <Tab path={path} {...props} key={`tab-custom-${index}`} />
}
return (
<DocumentTab
{...{
...props,
...Tab,
}}
key={`tab-custom-${index}`}
/>
)
}
return <DocumentTab {...props} {...Tab} key={`tab-custom-${index}`} />
return null
})}
</ul>
</div>

View File

@@ -1,15 +1,27 @@
import type { collectionViewType } from '../../../views/collections/Edit/Routes/CustomComponent'
import type { DocumentTabConfig } from './types'
export const tabs: DocumentTabConfig[] = [
// Default
{
export const tabs: Record<
collectionViewType,
DocumentTabConfig & {
order?: number // TODO: expose this to the global config
}
> = {
API: {
condition: ({ collection, global }) =>
(collection && !collection?.admin?.hideAPIURL) || (global && !global?.admin?.hideAPIURL),
href: '/api',
label: 'API',
order: 1000,
},
Default: {
href: '',
isActive: ({ href, location }) =>
location.pathname === href || location.pathname === `${href}/create`,
label: ({ t }) => t('edit'),
order: 0,
},
// Live Preview
{
LivePreview: {
condition: ({ collection, config, global }) => {
if (collection) {
return Boolean(
@@ -29,22 +41,25 @@ export const tabs: DocumentTabConfig[] = [
href: ({ match }) => `${match.url}/preview`,
isActive: ({ href, location }) => location.pathname === href,
label: ({ t }) => t('livePreview'),
order: 100,
},
// Versions
{
References: {
condition: () => false,
},
Relationships: {
condition: () => false,
},
Version: {
condition: () => false,
},
Versions: {
condition: ({ collection, global }) => Boolean(collection?.versions || global?.versions),
href: '/versions',
label: ({ t }) => t('version:versions'),
order: 200,
pillLabel: ({ versions }) =>
typeof versions?.totalDocs === 'number' && versions?.totalDocs > 0
? versions?.totalDocs.toString()
: '',
},
// API
{
condition: ({ collection, global }) =>
(collection && !collection?.admin?.hideAPIURL) || (global && !global?.admin?.hideAPIURL),
href: '/api',
label: 'API',
},
]
}

View File

@@ -21,9 +21,11 @@ export type DocumentTabCondition = (args: {
global: SanitizedGlobalConfig
}) => boolean
// Everything is optional because we merge in the defaults
// i.e. the config may override the `Default` view with a `label` but not an `href`
export type DocumentTabConfig = {
condition?: DocumentTabCondition
href:
href?:
| ((args: {
apiURL: string
collection: SanitizedCollectionConfig
@@ -40,7 +42,7 @@ export type DocumentTabConfig = {
match: ReturnType<typeof useRouteMatch>
}) => boolean)
| boolean
label: ((args: { t: (key: string) => string }) => string) | string
label?: ((args: { t: (key: string) => string }) => string) | string
newTab?: boolean
pillLabel?:
| ((args: { versions: ReturnType<typeof useDocumentInfo>['versions'] }) => string)

View File

@@ -83,7 +83,7 @@ export const EditUpload: React.FC<{
setFormQueryParams({
...formQueryParams,
uploadEdits: {
crop: crop ? crop : undefined,
crop: crop || undefined,
focalPoint: pointPosition ? pointPosition : undefined,
},
})

View File

@@ -1,7 +1,7 @@
@import '../../../scss/styles.scss';
.file-details {
background-color: var(--theme-elevation-100);
background-color: var(--theme-elevation-50);
header {
display: flex;

View File

@@ -28,14 +28,11 @@
margin-left: base(0.5);
}
.btn {
background-color: var(--theme-elevation-100);
cursor: pointer;
padding: 0 base(0.25);
border-radius: $style-radius-s;
.pill {
background-color: var(--theme-elevation-200);
&:hover {
background-color: var(--theme-elevation-200);
background-color: var(--theme-elevation-150);
}
}
}

View File

@@ -116,17 +116,16 @@ export const ListControls: React.FC<Props> = (props) => {
{t('filters')}
</Pill>
{enableSort && (
<Button
<Pill
aria-controls={`${baseClass}-sort`}
aria-expanded={visibleDrawer === 'sort'}
buttonStyle={visibleDrawer === 'sort' ? undefined : 'secondary'}
className={`${baseClass}__toggle-sort`}
icon="chevron"
iconStyle="none"
icon={<Chevron />}
onClick={() => setVisibleDrawer(visibleDrawer !== 'sort' ? 'sort' : undefined)}
pillStyle="light"
>
{t('sort')}
</Button>
</Pill>
)}
</div>
</div>

View File

@@ -1,28 +1,33 @@
import React from 'react'
import { Chevron } from '../../..'
import { useLocale } from '../../../utilities/Locale'
import { useTranslation } from 'react-i18next'
import { Chevron } from '../../..'
import { getTranslation } from '../../../../../utilities/getTranslation'
import { useLocale } from '../../../utilities/Locale'
import './index.scss'
const baseClass = 'localizer-button'
export const LocalizerLabel: React.FC<{
className?: string
ariaLabel?: string
className?: string
}> = (props) => {
const { className, ariaLabel } = props
const { ariaLabel, className } = props
const locale = useLocale()
const { t } = useTranslation('general')
const { i18n } = useTranslation()
return (
<div
className={[baseClass, className].filter(Boolean).join(' ')}
aria-label={ariaLabel || t('locale')}
className={[baseClass, className].filter(Boolean).join(' ')}
>
<div className={`${baseClass}__label`}>{`${t('locale')}:`}</div>
&nbsp;&nbsp;
<span className={`${baseClass}__current-label`}>{`${locale.label}`}</span>
<span className={`${baseClass}__current-label`}>{`${getTranslation(
locale.label,
i18n,
)}`}</span>
&nbsp;
<Chevron className={`${baseClass}__chevron`} />
</div>

View File

@@ -1,6 +1,8 @@
import qs from 'qs'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { getTranslation } from '../../../../utilities/getTranslation'
import { useConfig } from '../../utilities/Config'
import { useLocale } from '../../utilities/Locale'
import { useSearchParams } from '../../utilities/SearchParams'
@@ -18,9 +20,12 @@ const Localizer: React.FC<{
const config = useConfig()
const { localization } = config
const { i18n } = useTranslation()
const locale = useLocale()
const searchParams = useSearchParams()
const localeLabel = getTranslation(locale.label, i18n)
if (localization) {
const { locales } = localization
@@ -44,8 +49,8 @@ const Localizer: React.FC<{
}),
}}
>
{locale.label}
{locale.label !== locale.code && ` (${locale.code})`}
{localeLabel}
{localeLabel !== locale.code && ` (${locale.code})`}
</PopupList.Button>
) : null}
@@ -57,11 +62,12 @@ const Localizer: React.FC<{
locale: localeOption.code,
}
const search = qs.stringify(newParams)
const localeOptionLabel = getTranslation(localeOption.label, i18n)
return (
<PopupList.Button key={localeOption.code} onClick={close} to={{ search }}>
{localeOption.label}
{localeOption.label !== localeOption.code && ` (${localeOption.code})`}
{localeOptionLabel}
{localeOptionLabel !== localeOption.code && ` (${localeOption.code})`}
</PopupList.Button>
)
})}

View File

@@ -28,7 +28,7 @@ export const NavToggler: React.FC<{
return (
<button
aria-label={t('menu')}
aria-label={`${navOpen ? t('close') : t('open')} ${t('menu')}`}
className={[baseClass, navOpen && `${baseClass}--is-open`, className]
.filter(Boolean)
.join(' ')}

View File

@@ -54,8 +54,9 @@ const SortColumn: React.FC<Props> = (props) => {
direction: t('ascending'),
label: getTranslation(label, i18n),
})}
className={[ascClasses, `${baseClass}__button`].filter(Boolean).join(' ')}
className={[...ascClasses, `${baseClass}__button`].filter(Boolean).join(' ')}
onClick={() => setSort(asc)}
type="button"
>
<Chevron direction="up" />
</button>
@@ -64,8 +65,9 @@ const SortColumn: React.FC<Props> = (props) => {
direction: t('descending'),
label: getTranslation(label, i18n),
})}
className={[descClasses, `${baseClass}__button`].filter(Boolean).join(' ')}
className={[...descClasses, `${baseClass}__button`].filter(Boolean).join(' ')}
onClick={() => setSort(desc)}
type="button"
>
<Chevron />
</button>

View File

@@ -56,6 +56,8 @@
}
span {
display: flex;
align-items: center;
max-width: base(8);
text-overflow: ellipsis;
overflow: hidden;

View File

@@ -51,16 +51,25 @@ const StepNav: React.FC<{
</Link>
<span>/</span>
{stepNav.map((item, i) => {
const StepLabel = <span key={i}>{getTranslation(item.label, i18n)}</span>
const Step =
stepNav.length === i + 1 ? (
StepLabel
) : (
<Fragment key={i}>
{item.url ? <Link to={item.url}>{StepLabel}</Link> : StepLabel}
<span>/</span>
</Fragment>
)
const StepLabel = getTranslation(item.label, i18n)
const isLast = stepNav.length === i + 1
const Step = isLast ? (
<span className={`${baseClass}__last`} key={i}>
{StepLabel}
</span>
) : (
<Fragment key={i}>
{item.url ? (
<Link to={item.url}>
<span key={i}>{StepLabel}</span>
</Link>
) : (
<span key={i}>{StepLabel}</span>
)}
<span>/</span>
</Fragment>
)
return Step
})}

View File

@@ -20,7 +20,6 @@
content: ' ';
display: block;
position: absolute;
left: 50%;
transform: translate3d(-50%, 100%, 0);
width: 0;
height: 0;
@@ -35,6 +34,24 @@
cursor: default;
}
&--caret-center {
&::after {
left: 50%;
}
}
&--caret-left {
&::after {
left: calc(var(--base) * 0.5);
}
}
&--caret-right {
&::after {
right: calc(var(--base) * 0.5);
}
}
&--position-top {
bottom: 100%;
transform: translate3d(-50%, calc(var(--caret-size) * -1), 0);
@@ -55,6 +72,12 @@
}
}
.tooltip-content {
overflow: hidden;
text-overflow: ellipsis;
width: 100%;
}
@include mid-break {
display: none;
}

View File

@@ -6,11 +6,20 @@ import useIntersect from '../../../hooks/useIntersect'
import './index.scss'
const Tooltip: React.FC<Props> = (props) => {
const { boundingRef, children, className, delay = 350, show: showFromProps = true } = props
const {
alignCaret = 'center',
boundingRef,
children,
className,
delay = 350,
show: showFromProps = true,
} = props
const [show, setShow] = React.useState(showFromProps)
const [position, setPosition] = React.useState<'bottom' | 'top'>('top')
const getTitleAttribute = (content) => (typeof content === 'string' ? content : '')
const [ref, intersectionEntry] = useIntersect({
root: boundingRef?.current || null,
rootMargin: '-145px 0px 0px 100px',
@@ -42,18 +51,28 @@ const Tooltip: React.FC<Props> = (props) => {
<React.Fragment>
<aside
aria-hidden="true"
className={['tooltip', className, 'tooltip--position-top'].filter(Boolean).join(' ')}
className={['tooltip', className, `tooltip--caret-${alignCaret}`, 'tooltip--position-top']
.filter(Boolean)
.join(' ')}
ref={ref}
title={getTitleAttribute(children)}
>
{children}
<div className="tooltip-content">{children}</div>
</aside>
<aside
className={['tooltip', className, show && 'tooltip--show', `tooltip--position-${position}`]
className={[
'tooltip',
className,
show && 'tooltip--show',
`tooltip--caret-${alignCaret}`,
`tooltip--position-${position}`,
]
.filter(Boolean)
.join(' ')}
title={getTitleAttribute(children)}
>
{children}
<div className="tooltip-content">{children}</div>
</aside>
</React.Fragment>
)

View File

@@ -1,4 +1,5 @@
export type Props = {
alignCaret?: 'center' | 'left' | 'right'
boundingRef?: React.RefObject<HTMLElement>
children: React.ReactNode
className?: string

View File

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

View File

@@ -1,4 +1,5 @@
export type Props = {
disabled?: boolean
onChange: () => void
value: Date
}

View File

@@ -7,11 +7,12 @@ import './index.scss'
const baseClass = 'condition-value-number'
const NumberField: React.FC<Props> = ({ onChange, value }) => {
const NumberField: React.FC<Props> = ({ disabled, onChange, value }) => {
const { t } = useTranslation('general')
return (
<input
className={baseClass}
disabled={disabled}
onChange={(e) => onChange(e.target.value)}
placeholder={t('enterAValue')}
type="number"

View File

@@ -1,4 +1,5 @@
export type Props = {
disabled?: boolean
onChange: (e: string) => void
value: string
}

View File

@@ -16,7 +16,7 @@ const baseClass = 'condition-value-relationship'
const maxResultsPerRequest = 10
const RelationshipField: React.FC<Props> = (props) => {
const { admin: { isSortable } = {}, hasMany, onChange, relationTo, value } = props
const { admin: { isSortable } = {}, disabled, hasMany, onChange, relationTo, value } = props
const {
collections,
@@ -261,6 +261,7 @@ const RelationshipField: React.FC<Props> = (props) => {
<div className={classes}>
{!errorLoading && (
<ReactSelect
disabled={disabled}
isMulti={hasMany}
isSortable={isSortable}
onChange={(selected) => {

View File

@@ -5,6 +5,7 @@ import type { PaginatedDocs } from '../../../../../../database/types'
import type { RelationshipField } from '../../../../../../fields/config/types'
export type Props = {
disabled?: boolean
onChange: (val: unknown) => void
value: unknown
} & RelationshipField

View File

@@ -20,6 +20,7 @@ const formatOptions = (options: Option[]): OptionObject[] =>
})
export const Select: React.FC<Props> = ({
disabled,
onChange,
operator,
options: optionsFromProps,
@@ -79,6 +80,7 @@ export const Select: React.FC<Props> = ({
return (
<ReactSelect
disabled={disabled}
isMulti={isMulti}
onChange={onSelect}
options={options.map((option) => ({ ...option, label: getTranslation(option.label, i18n) }))}

View File

@@ -2,6 +2,7 @@ import type { Option } from '../../../../../../fields/config/types'
import type { Operator } from '../../../../../../types'
export type Props = {
disabled?: boolean
onChange: (val: string) => void
operator: Operator
options: Option[]

View File

@@ -7,11 +7,12 @@ import './index.scss'
const baseClass = 'condition-value-text'
const Text: React.FC<Props> = ({ onChange, value }) => {
const Text: React.FC<Props> = ({ disabled, onChange, value }) => {
const { t } = useTranslation('general')
return (
<input
className={baseClass}
disabled={disabled}
onChange={(e) => onChange(e.target.value)}
placeholder={t('enterAValue')}
type="text"

View File

@@ -1,4 +1,5 @@
export type Props = {
disabled?: boolean
onChange: (val: string) => void
value: string
}

View File

@@ -26,25 +26,29 @@ const baseClass = 'condition'
const Condition: React.FC<Props> = (props) => {
const { andIndex, dispatch, fields, orIndex, value } = props
const fieldValue = Object.keys(value)[0]
const operatorAndValue = value?.[fieldValue] ? Object.entries(value[fieldValue])[0] : undefined
const operatorValue = operatorAndValue?.[0]
const queryValue = operatorAndValue?.[1]
const fieldName = Object.keys(value)[0]
const [activeField, setActiveField] = useState<FieldCondition>(() =>
fields.find((field) => fieldValue === field.value),
fields.find((field) => fieldName === field.value),
)
const operatorAndValue = value?.[fieldName] ? Object.entries(value[fieldName])[0] : undefined
const queryValue = operatorAndValue?.[1]
const operatorValue = operatorAndValue?.[0]
const [internalValue, setInternalValue] = useState(queryValue)
const [internalOperatorField, setInternalOperatorField] = useState(operatorValue)
const debouncedValue = useDebounce(internalValue, 300)
useEffect(() => {
const newActiveField = fields.find((field) => fieldValue === field.value)
const newActiveField = fields.find(({ value: name }) => name === fieldName)
if (newActiveField) {
if (newActiveField && newActiveField !== activeField) {
setActiveField(newActiveField)
setInternalOperatorField(null)
setInternalValue('')
}
}, [fieldValue, fields])
}, [fieldName, fields, activeField])
useEffect(() => {
dispatch({
@@ -73,21 +77,23 @@ const Condition: React.FC<Props> = (props) => {
<div className={`${baseClass}__inputs`}>
<div className={`${baseClass}__field`}>
<ReactSelect
onChange={(field) =>
isClearable={false}
onChange={(field) => {
dispatch({
andIndex,
field: field?.value || undefined,
orIndex,
andIndex: andIndex,
field: field?.value,
orIndex: orIndex,
type: 'update',
})
}
}}
options={fields}
value={fields.find((field) => fieldValue === field.value)}
value={fields.find((field) => fieldName === field.value)}
/>
</div>
<div className={`${baseClass}__operator`}>
<ReactSelect
disabled={!fieldValue}
disabled={!fieldName}
isClearable={false}
onChange={(operator) => {
dispatch({
andIndex,
@@ -95,9 +101,14 @@ const Condition: React.FC<Props> = (props) => {
orIndex,
type: 'update',
})
setInternalOperatorField(operator.value)
}}
options={activeField.operators}
value={activeField.operators.find((operator) => operatorValue === operator.value)}
value={
activeField.operators.find(
(operator) => internalOperatorField === operator.value,
) || null
}
/>
</div>
<div className={`${baseClass}__value`}>
@@ -106,6 +117,7 @@ const Condition: React.FC<Props> = (props) => {
DefaultComponent={ValueComponent}
componentProps={{
...activeField?.props,
disabled: !operatorValue,
onChange: setInternalValue,
operator: operatorValue,
options: valueOptions,

View File

@@ -59,17 +59,17 @@ const reducer = (state: Where[], action: Action): Where[] => {
if (field) {
newState[orIndex].and[andIndex] = {
[field]: {
[Object.keys(existingCondition)[0]]: Object.values(existingCondition)[0],
},
[field]: operator ? { [operator]: value } : {},
}
}
if (value !== undefined) {
newState[orIndex].and[andIndex] = {
[existingFieldName]: {
[Object.keys(existingCondition)[0]]: value,
},
[existingFieldName]: Object.keys(existingCondition)[0]
? {
[Object.keys(existingCondition)[0]]: value,
}
: {},
}
}
}

View File

@@ -5,7 +5,8 @@
top: 0;
bottom: auto;
left: auto;
right: base(0.5);
max-width: 75%;
right: calc(var(--base) * 0.5);
transform: none;
background-color: var(--theme-error-500);

View File

@@ -8,11 +8,11 @@ import './index.scss'
const baseClass = 'field-error'
const Error: React.FC<Props> = (props) => {
const { message, showError = false } = props
const { alignCaret = 'right', message, showError = false } = props
if (showError) {
return (
<Tooltip className={baseClass} delay={0}>
<Tooltip alignCaret={alignCaret} className={baseClass} delay={0}>
{message}
</Tooltip>
)

View File

@@ -1,4 +1,5 @@
export type Props = {
alignCaret?: 'center' | 'left' | 'right'
message: string
showError?: boolean
}

View File

@@ -281,9 +281,9 @@ export const addFieldStatePromise = async ({
return {
relationTo: relationship.relationTo,
value:
typeof relationship.value === 'string'
? relationship.value
: relationship.value?.id,
relationship.value && typeof relationship.value === 'object'
? relationship.value?.id
: relationship.value,
}
}
if (typeof relationship === 'object' && relationship !== null) {

View File

@@ -48,23 +48,24 @@ export const filterFields = (args: {
}
const isFieldAffectingData = fieldAffectsData(field)
const fieldPermissions = isFieldAffectingData ? permissions?.[field.name] : permissions
// if the user cannot read the field, then filter it out
if (fieldPermissions?.read?.permission === false) {
return acc
}
// readOnly from field config
let readOnly = field.admin && 'readOnly' in field.admin ? field.admin.readOnly : undefined
// if parent field is readOnly
// but this field is `readOnly: false`
// the field should be editable
if (readOnlyOverride && readOnly !== false) readOnly = true
if (
(isFieldAffectingData && permissions?.[field?.name]?.read?.permission !== false) ||
!isFieldAffectingData
) {
if (
isFieldAffectingData &&
permissions?.[field?.name]?.[operation]?.permission === false
) {
readOnly = true
}
// unless the user does not pass access control
if (fieldPermissions?.[operation]?.permission === false) {
readOnly = true
}
if (FieldComponent) {

View File

@@ -1,15 +1,21 @@
import React from 'react'
import type { Props as LabelProps } from '../../Label/types'
import Check from '../../../icons/Check'
import Line from '../../../icons/Line'
import Label from '../../Label'
import DefaultLabel from '../../Label'
import './index.scss'
const baseClass = 'checkbox-input'
type CheckboxInputProps = {
AfterInput?: React.ReactElement<any>[]
BeforeInput?: React.ReactElement<any>[]
Label?: React.ComponentType<LabelProps>
'aria-label'?: string
checked?: boolean
className?: string
id?: string
inputRef?: React.MutableRefObject<HTMLInputElement>
label?: string
@@ -18,24 +24,28 @@ type CheckboxInputProps = {
partialChecked?: boolean
readOnly?: boolean
required?: boolean
className?: string
}
export const CheckboxInput: React.FC<CheckboxInputProps> = (props) => {
const {
id,
name,
AfterInput,
BeforeInput,
Label,
'aria-label': ariaLabel,
checked,
className,
inputRef,
label,
onToggle,
partialChecked,
readOnly,
required,
className,
} = props
const LabelComp = Label || DefaultLabel
return (
<div
className={[
@@ -48,6 +58,7 @@ export const CheckboxInput: React.FC<CheckboxInputProps> = (props) => {
.join(' ')}
>
<div className={`${baseClass}__input`}>
{BeforeInput}
<input
aria-label={ariaLabel}
defaultChecked={Boolean(checked)}
@@ -58,12 +69,13 @@ export const CheckboxInput: React.FC<CheckboxInputProps> = (props) => {
ref={inputRef}
type="checkbox"
/>
{AfterInput}
<span className={`${baseClass}__icon ${!partialChecked ? 'check' : 'partial'}`}>
{!partialChecked && <Check />}
{partialChecked && <Line />}
</span>
</div>
{label && <Label htmlFor={id} label={label} required={required} />}
{label && <LabelComp htmlFor={id} label={label} required={required} />}
</div>
)
}

View File

@@ -5,20 +5,28 @@ import type { Props } from './types'
import { checkbox } from '../../../../../fields/validations'
import { getTranslation } from '../../../../../utilities/getTranslation'
import Error from '../../Error'
import DefaultError from '../../Error'
import FieldDescription from '../../FieldDescription'
import useField from '../../useField'
import withCondition from '../../withCondition'
import { fieldBaseClass } from '../shared'
import { CheckboxInput } from './Input'
import './index.scss'
import { fieldBaseClass } from '../shared'
const baseClass = 'checkbox'
const Checkbox: React.FC<Props> = (props) => {
const {
name,
admin: { className, condition, description, readOnly, style, width } = {},
admin: {
className,
condition,
description,
readOnly,
style,
width,
components: { Error, Label, BeforeInput, AfterInput } = {},
} = {},
disableFormData,
label,
onChange,
@@ -27,6 +35,8 @@ const Checkbox: React.FC<Props> = (props) => {
validate = checkbox,
} = props
const ErrorComp = Error || DefaultError
const { i18n } = useTranslation()
const path = pathFromProps || name
@@ -72,7 +82,7 @@ const Checkbox: React.FC<Props> = (props) => {
}}
>
<div className={`${baseClass}__error-wrap`}>
<Error message={errorMessage} showError={showError} />
<ErrorComp alignCaret="left" message={errorMessage} showError={showError} />
</div>
<CheckboxInput
checked={Boolean(value)}
@@ -81,6 +91,10 @@ const Checkbox: React.FC<Props> = (props) => {
name={path}
onToggle={onToggle}
readOnly={readOnly}
Label={Label}
BeforeInput={BeforeInput}
AfterInput={AfterInput}
required={required}
/>
<FieldDescription description={description} value={value} />
</div>

View File

@@ -4,9 +4,9 @@ import type { Props } from './types'
import { code } from '../../../../../fields/validations'
import { CodeEditor } from '../../../elements/CodeEditor'
import Error from '../../Error'
import DefaultError from '../../Error'
import FieldDescription from '../../FieldDescription'
import Label from '../../Label'
import DefaultLabel from '../../Label'
import useField from '../../useField'
import withCondition from '../../withCondition'
import './index.scss'
@@ -31,6 +31,7 @@ const Code: React.FC<Props> = (props) => {
readOnly,
style,
width,
components: { Error, Label } = {},
} = {},
label,
path: pathFromProps,
@@ -38,6 +39,9 @@ const Code: React.FC<Props> = (props) => {
validate = code,
} = props
const ErrorComp = Error || DefaultError
const LabelComp = Label || DefaultLabel
const path = pathFromProps || name
const memoizedValidate = useCallback(
@@ -69,8 +73,8 @@ const Code: React.FC<Props> = (props) => {
width,
}}
>
<Error message={errorMessage} showError={showError} />
<Label htmlFor={`field-${path}`} label={label} required={required} />
<ErrorComp message={errorMessage} showError={showError} />
<LabelComp htmlFor={`field-${path}`} label={label} required={required} />
<CodeEditor
defaultLanguage={prismToMonacoLanguageMap[language] || language}
onChange={readOnly ? () => null : (val) => setValue(val)}

View File

@@ -6,9 +6,9 @@ import type { Description } from '../../FieldDescription/types'
import { getTranslation } from '../../../../../utilities/getTranslation'
import DatePicker from '../../../elements/DatePicker'
import Error from '../../Error'
import DefaultError from '../../Error'
import FieldDescription from '../../FieldDescription'
import Label from '../../Label'
import DefaultLabel from '../../Label'
import { fieldBaseClass } from '../shared'
import './index.scss'
@@ -16,6 +16,12 @@ 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>
}
datePickerProps?: DateField['admin']['date']
description?: Description
errorMessage?: string
@@ -33,6 +39,7 @@ export type DateTimeInputProps = Omit<DateField, 'admin' | 'name' | 'type'> & {
export const DateTimeInput: React.FC<DateTimeInputProps> = (props) => {
const {
className,
components: { AfterInput, BeforeInput, Error, Label } = {},
datePickerProps,
description,
errorMessage,
@@ -48,6 +55,9 @@ export const DateTimeInput: React.FC<DateTimeInputProps> = (props) => {
width,
} = props
const ErrorComp = Error || DefaultError
const LabelComp = Label || DefaultLabel
const { i18n } = useTranslation()
return (
@@ -67,10 +77,11 @@ export const DateTimeInput: React.FC<DateTimeInputProps> = (props) => {
}}
>
<div className={`${baseClass}__error-wrap`}>
<Error message={errorMessage} showError={showError} />
<ErrorComp message={errorMessage} showError={showError} />
</div>
<Label htmlFor={path} label={label} required={required} />
<LabelComp htmlFor={path} label={label} required={required} />
<div className={`${baseClass}__input-wrapper`} id={`field-${path.replace(/\./g, '__')}`}>
{BeforeInput}
<DatePicker
{...datePickerProps}
onChange={onChange}
@@ -78,6 +89,7 @@ export const DateTimeInput: React.FC<DateTimeInputProps> = (props) => {
readOnly={readOnly}
value={value}
/>
{AfterInput}
</div>
<FieldDescription description={description} value={value} />
</div>

View File

@@ -11,7 +11,17 @@ import './index.scss'
const DateTime: React.FC<Props> = (props) => {
const {
name,
admin: { className, condition, date, description, placeholder, readOnly, style, width } = {},
admin: {
className,
components,
condition,
date,
description,
placeholder,
readOnly,
style,
width,
} = {},
label,
path: pathFromProps,
required,
@@ -36,6 +46,7 @@ const DateTime: React.FC<Props> = (props) => {
return (
<DateTimeInput
className={className}
components={components}
datePickerProps={date}
description={description}
errorMessage={errorMessage}

View File

@@ -5,9 +5,9 @@ import type { Props } from './types'
import { email } from '../../../../../fields/validations'
import { getTranslation } from '../../../../../utilities/getTranslation'
import Error from '../../Error'
import DefaultError from '../../Error'
import FieldDescription from '../../FieldDescription'
import Label from '../../Label'
import DefaultLabel from '../../Label'
import useField from '../../useField'
import withCondition from '../../withCondition'
import './index.scss'
@@ -25,6 +25,7 @@ const Email: React.FC<Props> = (props) => {
readOnly,
style,
width,
components: { Error, Label, BeforeInput, AfterInput } = {},
} = {},
label,
path: pathFromProps,
@@ -51,6 +52,9 @@ const Email: React.FC<Props> = (props) => {
const { errorMessage, setValue, showError, value } = fieldType
const ErrorComp = Error || DefaultError
const LabelComp = Label || DefaultLabel
return (
<div
className={[fieldBaseClass, 'email', className, showError && 'error', readOnly && 'read-only']
@@ -61,18 +65,22 @@ const Email: React.FC<Props> = (props) => {
width,
}}
>
<Error message={errorMessage} showError={showError} />
<Label htmlFor={`field-${path.replace(/\./g, '__')}`} label={label} required={required} />
<input
autoComplete={autoComplete}
disabled={Boolean(readOnly)}
id={`field-${path.replace(/\./g, '__')}`}
name={path}
onChange={setValue}
placeholder={getTranslation(placeholder, i18n)}
type="email"
value={(value as string) || ''}
/>
<ErrorComp message={errorMessage} showError={showError} />
<LabelComp htmlFor={`field-${path.replace(/\./g, '__')}`} label={label} required={required} />
<div className="input-wrapper">
{BeforeInput}
<input
autoComplete={autoComplete}
disabled={Boolean(readOnly)}
id={`field-${path.replace(/\./g, '__')}`}
name={path}
onChange={setValue}
placeholder={getTranslation(placeholder, i18n)}
type="email"
value={(value as string) || ''}
/>
{AfterInput}
</div>
<FieldDescription description={description} value={value} />
</div>
)

View File

@@ -4,9 +4,9 @@ import type { Props } from './types'
import { json } from '../../../../../fields/validations'
import { CodeEditor } from '../../../elements/CodeEditor'
import Error from '../../Error'
import DefaultError from '../../Error'
import FieldDescription from '../../FieldDescription'
import Label from '../../Label'
import DefaultLabel from '../../Label'
import useField from '../../useField'
import withCondition from '../../withCondition'
import './index.scss'
@@ -17,13 +17,25 @@ const baseClass = 'json-field'
const JSONField: React.FC<Props> = (props) => {
const {
name,
admin: { className, condition, description, editorOptions, readOnly, style, width } = {},
admin: {
className,
condition,
description,
editorOptions,
readOnly,
style,
width,
components: { Error, Label } = {},
} = {},
label,
path: pathFromProps,
required,
validate = json,
} = props
const ErrorComp = Error || DefaultError
const LabelComp = Label || DefaultLabel
const path = pathFromProps || name
const [stringValue, setStringValue] = useState<string>()
const [jsonError, setJsonError] = useState<string>()
@@ -76,8 +88,8 @@ const JSONField: React.FC<Props> = (props) => {
width,
}}
>
<Error message={errorMessage} showError={showError} />
<Label htmlFor={`field-${path}`} label={label} required={required} />
<ErrorComp message={errorMessage} showError={showError} />
<LabelComp htmlFor={`field-${path}`} label={label} required={required} />
<CodeEditor
defaultLanguage="json"
onChange={handleChange}

View File

@@ -8,9 +8,9 @@ import { number } from '../../../../../fields/validations'
import { getTranslation } from '../../../../../utilities/getTranslation'
import { isNumber } from '../../../../../utilities/isNumber'
import ReactSelect from '../../../elements/ReactSelect'
import Error from '../../Error'
import DefaultError from '../../Error'
import FieldDescription from '../../FieldDescription'
import Label from '../../Label'
import DefaultLabel from '../../Label'
import useField from '../../useField'
import withCondition from '../../withCondition'
import './index.scss'
@@ -19,7 +19,17 @@ import { fieldBaseClass } from '../shared'
const NumberField: React.FC<Props> = (props) => {
const {
name,
admin: { className, condition, description, placeholder, readOnly, step, style, width } = {},
admin: {
className,
condition,
description,
placeholder,
readOnly,
step,
style,
width,
components: { Error, Label, BeforeInput, AfterInput } = {},
} = {},
hasMany,
label,
max,
@@ -31,6 +41,9 @@ const NumberField: React.FC<Props> = (props) => {
validate = number,
} = props
const ErrorComp = Error || DefaultError
const LabelComp = Label || DefaultLabel
const { i18n, t } = useTranslation()
const path = pathFromProps || name
@@ -118,8 +131,8 @@ const NumberField: React.FC<Props> = (props) => {
width,
}}
>
<Error message={errorMessage} showError={showError} />
<Label htmlFor={`field-${path.replace(/\./g, '__')}`} label={label} required={required} />
<ErrorComp message={errorMessage} showError={showError} />
<LabelComp htmlFor={`field-${path.replace(/\./g, '__')}`} label={label} required={required} />
{hasMany ? (
<ReactSelect
className={`field-${path.replace(/\./g, '__')}`}
@@ -148,21 +161,25 @@ const NumberField: React.FC<Props> = (props) => {
value={valueToRender as Option[]}
/>
) : (
<input
disabled={readOnly}
id={`field-${path.replace(/\./g, '__')}`}
name={path}
onChange={handleChange}
onWheel={(e) => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
e.target.blur()
}}
placeholder={getTranslation(placeholder, i18n)}
step={step}
type="number"
value={typeof value === 'number' ? value : ''}
/>
<div className="input-wrapper">
{BeforeInput}
<input
disabled={readOnly}
id={`field-${path.replace(/\./g, '__')}`}
name={path}
onChange={handleChange}
onWheel={(e) => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
e.target.blur()
}}
placeholder={getTranslation(placeholder, i18n)}
step={step}
type="number"
value={typeof value === 'number' ? value : ''}
/>
{AfterInput}
</div>
)}
<FieldDescription description={description} value={value} />

View File

@@ -5,9 +5,9 @@ import type { Props } from './types'
import { point } from '../../../../../fields/validations'
import { getTranslation } from '../../../../../utilities/getTranslation'
import Error from '../../Error'
import DefaultError from '../../Error'
import FieldDescription from '../../FieldDescription'
import Label from '../../Label'
import DefaultLabel from '../../Label'
import useField from '../../useField'
import withCondition from '../../withCondition'
import './index.scss'
@@ -18,13 +18,26 @@ const baseClass = 'point'
const PointField: React.FC<Props> = (props) => {
const {
name,
admin: { className, condition, description, placeholder, readOnly, step, style, width } = {},
admin: {
className,
condition,
description,
placeholder,
readOnly,
step,
style,
width,
components: { Error, Label, BeforeInput, AfterInput } = {},
} = {},
label,
path: pathFromProps,
required,
validate = point,
} = props
const ErrorComp = Error || DefaultError
const LabelComp = Label || DefaultLabel
const path = pathFromProps || name
const { i18n, t } = useTranslation('fields')
@@ -76,41 +89,49 @@ const PointField: React.FC<Props> = (props) => {
width,
}}
>
<Error message={errorMessage} showError={showError} />
<ErrorComp message={errorMessage} showError={showError} />
<ul className={`${baseClass}__wrap`}>
<li>
<Label
<LabelComp
htmlFor={`field-longitude-${path.replace(/\./g, '__')}`}
label={`${getTranslation(label || name, i18n)} - ${t('longitude')}`}
required={required}
/>
<input
disabled={readOnly}
id={`field-longitude-${path.replace(/\./g, '__')}`}
name={`${path}.longitude`}
onChange={(e) => handleChange(e, 0)}
placeholder={getTranslation(placeholder, i18n)}
step={step}
type="number"
value={value && typeof value[0] === 'number' ? value[0] : ''}
/>
<div className="input-wrapper">
{BeforeInput}
<input
disabled={readOnly}
id={`field-longitude-${path.replace(/\./g, '__')}`}
name={`${path}.longitude`}
onChange={(e) => handleChange(e, 0)}
placeholder={getTranslation(placeholder, i18n)}
step={step}
type="number"
value={value && typeof value[0] === 'number' ? value[0] : ''}
/>
{AfterInput}
</div>
</li>
<li>
<Label
<LabelComp
htmlFor={`field-latitude-${path.replace(/\./g, '__')}`}
label={`${getTranslation(label || name, i18n)} - ${t('latitude')}`}
required={required}
/>
<input
disabled={readOnly}
id={`field-latitude-${path.replace(/\./g, '__')}`}
name={`${path}.latitude`}
onChange={(e) => handleChange(e, 1)}
placeholder={getTranslation(placeholder, i18n)}
step={step}
type="number"
value={value && typeof value[1] === 'number' ? value[1] : ''}
/>
<div className="input-wrapper">
{BeforeInput}
<input
disabled={readOnly}
id={`field-latitude-${path.replace(/\./g, '__')}`}
name={`${path}.latitude`}
onChange={(e) => handleChange(e, 1)}
placeholder={getTranslation(placeholder, i18n)}
step={step}
type="number"
value={value && typeof value[1] === 'number' ? value[1] : ''}
/>
{AfterInput}
</div>
</li>
</ul>
<FieldDescription description={description} value={value} />

View File

@@ -5,9 +5,9 @@ import type { Description } from '../../FieldDescription/types'
import type { OnChange } from './types'
import { optionIsObject } from '../../../../../fields/config/types'
import Error from '../../Error'
import DefaultError from '../../Error'
import FieldDescription from '../../FieldDescription'
import Label from '../../Label'
import DefaultLabel from '../../Label'
import RadioInput from './RadioInput'
import './index.scss'
import { fieldBaseClass } from '../shared'
@@ -28,6 +28,8 @@ 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) => {
@@ -47,8 +49,13 @@ const RadioGroupInput: React.FC<RadioGroupInputProps> = (props) => {
style,
value,
width,
Error,
Label,
} = props
const ErrorComp = Error || DefaultError
const LabelComp = Label || DefaultLabel
const path = pathFromProps || name
return (
@@ -69,9 +76,9 @@ const RadioGroupInput: React.FC<RadioGroupInputProps> = (props) => {
}}
>
<div className={`${baseClass}__error-wrap`}>
<Error message={errorMessage} showError={showError} />
<ErrorComp message={errorMessage} showError={showError} />
</div>
<Label htmlFor={`field-${path}`} label={label} required={required} />
<LabelComp htmlFor={`field-${path}`} label={label} required={required} />
<ul className={`${baseClass}--group`} id={`field-${path.replace(/\./g, '__')}`}>
{options.map((option) => {
let optionValue = ''

View File

@@ -18,6 +18,7 @@ const RadioGroup: React.FC<Props> = (props) => {
readOnly,
style,
width,
components: { Error, Label } = {},
} = {},
label,
options,
@@ -57,6 +58,8 @@ const RadioGroup: React.FC<Props> = (props) => {
style={style}
value={value}
width={width}
Error={Error}
Label={Label}
/>
)
}

View File

@@ -15,12 +15,13 @@ import { useAuth } from '../../../utilities/Auth'
import { useConfig } from '../../../utilities/Config'
import { GetFilterOptions } from '../../../utilities/GetFilterOptions'
import { useLocale } from '../../../utilities/Locale'
import Error from '../../Error'
import DefaultError from '../../Error'
import FieldDescription from '../../FieldDescription'
import { useFormProcessing } from '../../Form/context'
import Label from '../../Label'
import DefaultLabel from '../../Label'
import useField from '../../useField'
import withCondition from '../../withCondition'
import { fieldBaseClass } from '../shared'
import { AddNewRelation } from './AddNew'
import { createRelationMap } from './createRelationMap'
import { findOptionsByValue } from './findOptionsByValue'
@@ -28,7 +29,6 @@ import './index.scss'
import optionsReducer from './optionsReducer'
import { MultiValueLabel } from './select-components/MultiValueLabel'
import { SingleValue } from './select-components/SingleValue'
import { fieldBaseClass } from '../shared'
const maxResultsPerRequest = 10
@@ -46,6 +46,7 @@ const Relationship: React.FC<Props> = (props) => {
readOnly,
style,
width,
components: { Error, Label } = {},
} = {},
filterOptions,
hasMany,
@@ -56,6 +57,9 @@ const Relationship: React.FC<Props> = (props) => {
validate = relationship,
} = props
const ErrorComp = Error || DefaultError
const LabelComp = Label || DefaultLabel
const config = useConfig()
const {
@@ -391,6 +395,7 @@ const Relationship: React.FC<Props> = (props) => {
}, [])
const valueToRender = findOptionsByValue({ options, value })
if (!Array.isArray(valueToRender) && valueToRender?.value === 'null') valueToRender.value = null
return (
@@ -411,8 +416,8 @@ const Relationship: React.FC<Props> = (props) => {
width,
}}
>
<Error message={errorMessage} showError={showError} />
<Label htmlFor={pathOrName} label={label} required={required} />
<ErrorComp message={errorMessage} showError={showError} />
<LabelComp htmlFor={pathOrName} label={label} required={required} />
<GetFilterOptions
{...{
filterOptions,

View File

@@ -7,9 +7,9 @@ import type { Description } from '../../FieldDescription/types'
import { getTranslation } from '../../../../../utilities/getTranslation'
import ReactSelect from '../../../elements/ReactSelect'
import Error from '../../Error'
import DefaultError from '../../Error'
import FieldDescription from '../../FieldDescription'
import Label from '../../Label'
import DefaultLabel from '../../Label'
import { fieldBaseClass } from '../shared'
import './index.scss'
@@ -29,6 +29,8 @@ 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) => {
@@ -50,10 +52,15 @@ const SelectInput: React.FC<SelectInputProps> = (props) => {
style,
value,
width,
Error,
Label,
} = props
const { i18n } = useTranslation()
const ErrorComp = Error || DefaultError
const LabelComp = Label || DefaultLabel
let valueToRender = defaultValue
if (hasMany && Array.isArray(value)) {
@@ -89,8 +96,8 @@ const SelectInput: React.FC<SelectInputProps> = (props) => {
width,
}}
>
<Error message={errorMessage} showError={showError} />
<Label htmlFor={`field-${path.replace(/\./g, '__')}`} label={label} required={required} />
<ErrorComp message={errorMessage} showError={showError} />
<LabelComp htmlFor={`field-${path.replace(/\./g, '__')}`} label={label} required={required} />
<ReactSelect
disabled={readOnly}
isClearable={isClearable}

View File

@@ -32,6 +32,7 @@ const Select: React.FC<Props> = (props) => {
readOnly,
style,
width,
components: { Error, Label } = {},
} = {},
hasMany,
label,
@@ -103,6 +104,8 @@ const Select: React.FC<Props> = (props) => {
style={style}
value={value as string | string[]}
width={width}
Error={Error}
Label={Label}
/>
)
}

View File

@@ -7,11 +7,11 @@ import type { TextField } from '../../../../../fields/config/types'
import type { Description } from '../../FieldDescription/types'
import { getTranslation } from '../../../../../utilities/getTranslation'
import Error from '../../Error'
import DefaultError from '../../Error'
import FieldDescription from '../../FieldDescription'
import Label from '../../Label'
import './index.scss'
import DefaultLabel from '../../Label'
import { fieldBaseClass } from '../shared'
import './index.scss'
export type TextInputProps = Omit<TextField, 'type'> & {
className?: string
@@ -29,6 +29,10 @@ 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) => {
@@ -49,10 +53,17 @@ const TextInput: React.FC<TextInputProps> = (props) => {
style,
value,
width,
Error,
Label,
BeforeInput,
AfterInput,
} = props
const { i18n } = useTranslation()
const ErrorComp = Error || DefaultError
const LabelComp = Label || DefaultLabel
return (
<div
className={[fieldBaseClass, 'text', className, showError && 'error', readOnly && 'read-only']
@@ -63,20 +74,24 @@ const TextInput: React.FC<TextInputProps> = (props) => {
width,
}}
>
<Error message={errorMessage} showError={showError} />
<Label htmlFor={`field-${path.replace(/\./g, '__')}`} label={label} required={required} />
<input
data-rtl={rtl}
disabled={readOnly}
id={`field-${path.replace(/\./g, '__')}`}
name={path}
onChange={onChange}
onKeyDown={onKeyDown}
placeholder={getTranslation(placeholder, i18n)}
ref={inputRef}
type="text"
value={value || ''}
/>
<ErrorComp message={errorMessage} showError={showError} />
<LabelComp htmlFor={`field-${path.replace(/\./g, '__')}`} label={label} required={required} />
<div className="input-wrapper">
{BeforeInput}
<input
data-rtl={rtl}
disabled={readOnly}
id={`field-${path.replace(/\./g, '__')}`}
name={path}
onChange={onChange}
onKeyDown={onKeyDown}
placeholder={getTranslation(placeholder, i18n)}
ref={inputRef}
type="text"
value={value || ''}
/>
{AfterInput}
</div>
<FieldDescription
className={`field-description-${path.replace(/\./g, '__')}`}
description={description}

View File

@@ -13,7 +13,17 @@ import TextInput from './Input'
const Text: React.FC<Props> = (props) => {
const {
name,
admin: { className, condition, description, placeholder, readOnly, rtl, style, width } = {},
admin: {
className,
condition,
description,
placeholder,
readOnly,
rtl,
style,
width,
components: { Error, Label, BeforeInput, AfterInput } = {},
} = {},
inputRef,
label,
localized,
@@ -68,6 +78,10 @@ const Text: React.FC<Props> = (props) => {
style={style}
value={value}
width={width}
Error={Error}
Label={Label}
BeforeInput={BeforeInput}
AfterInput={AfterInput}
/>
)
}

View File

@@ -7,9 +7,9 @@ import type { TextareaField } from '../../../../../fields/config/types'
import type { Description } from '../../FieldDescription/types'
import { getTranslation } from '../../../../../utilities/getTranslation'
import Error from '../../Error'
import DefaultError from '../../Error'
import FieldDescription from '../../FieldDescription'
import Label from '../../Label'
import DefaultLabel from '../../Label'
import './index.scss'
import { fieldBaseClass } from '../shared'
@@ -28,6 +28,10 @@ export type TextAreaInputProps = Omit<TextareaField, 'type'> & {
style?: React.CSSProperties
value?: string
width?: string
Error?: React.ComponentType<any>
Label?: React.ComponentType<any>
BeforeInput?: React.ReactElement<any>[]
AfterInput?: React.ReactElement<any>[]
}
const TextareaInput: React.FC<TextAreaInputProps> = (props) => {
@@ -47,10 +51,17 @@ const TextareaInput: React.FC<TextAreaInputProps> = (props) => {
style,
value,
width,
Error,
Label,
BeforeInput,
AfterInput,
} = props
const { i18n } = useTranslation()
const ErrorComp = Error || DefaultError
const LabelComp = Label || DefaultLabel
return (
<div
className={[
@@ -67,11 +78,12 @@ const TextareaInput: React.FC<TextAreaInputProps> = (props) => {
width,
}}
>
<Error message={errorMessage} showError={showError} />
<Label htmlFor={`field-${path.replace(/\./g, '__')}`} label={label} required={required} />
<ErrorComp message={errorMessage} showError={showError} />
<LabelComp htmlFor={`field-${path.replace(/\./g, '__')}`} label={label} required={required} />
<label className="textarea-outer" htmlFor={`field-${path.replace(/\./g, '__')}`}>
<div className="textarea-inner">
<div className="textarea-clone" data-value={value || placeholder || ''} />
{BeforeInput}
<textarea
className="textarea-element"
data-rtl={rtl}
@@ -83,6 +95,7 @@ const TextareaInput: React.FC<TextAreaInputProps> = (props) => {
rows={rows}
value={value || ''}
/>
{AfterInput}
</div>
</label>
<FieldDescription description={description} value={value} />

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