Compare commits

...

151 Commits

Author SHA1 Message Date
Elliot DeNolf
9963b8d945 chore(release): plugin-nested-docs/1.0.9 [skip ci] 2023-11-20 16:40:46 -05:00
Elliot DeNolf
9afb838182 chore(release): richtext-slate/1.2.0 [skip ci] 2023-11-20 16:39:38 -05:00
Elliot DeNolf
2dad129022 chore(release): richtext-lexical/0.2.0 [skip ci] 2023-11-20 16:39:20 -05:00
Elliot DeNolf
6af1c4d45d chore(release): payload/2.2.0 [skip ci] 2023-11-20 16:36:41 -05:00
Dan Ribbens
4e41dd1bf2 fix(plugin-nested-docs): await populate breadcrumbs on resaveChildren (#4226) 2023-11-20 16:32:02 -05:00
Dan Ribbens
de02490231 feat: hide publish button based on permissions (#4203)
Co-authored-by: James <james@trbl.design>
2023-11-20 16:26:49 -05:00
Take Weiland
1510baf46e fix: synchronous transaction errors (#4164)
Co-authored-by: Dan Ribbens <dan.ribbens@gmail.com>
2023-11-20 12:20:42 -05:00
Alessio Gravili
c10db332cd docs(richtext-lexical): remove unnecessary await from createHeadlessEditor (#4213) 2023-11-19 14:40:09 +01:00
Alessio Gravili
0af9c4d398 fix(richtext-lexical): Blocks: Array row data is not removed (#4209)
* chore(richtext-lexical): Add failing test which reproduces issue

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

* chore: remove redundant hook

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

* cleanup everything

* chore: more cleanup

* debug

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

* chore: fix e2e tests

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

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

* chore: fix incorrect insert block commands in drawer

* chore: add new e2e test

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

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

* chore: rename hintPaginationCountIndex to disablePaginationCountIndexHint

* chore: fix logic

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

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

* fix(richtext-lexical): Blocks: unnecessary saving node value when initially opening a document
2023-11-08 21:32:43 +01:00
Elliot DeNolf
a2cb946155 chore(release): bundler-vite/0.1.4 [skip ci] 2023-11-08 14:54:50 -05:00
Elliot DeNolf
c39472259a chore(release): db-postgres/0.1.13 [skip ci] 2023-11-08 14:53:16 -05:00
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
Yunsup Sim
b008b6c646 fix: Update API Views 2023-11-03 18:38:06 +09: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
381 changed files with 11262 additions and 3557 deletions

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

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

View File

@@ -1,3 +1,77 @@
## [2.2.0](https://github.com/payloadcms/payload/compare/v2.1.1...v2.2.0) (2023-11-20)
### Features
* allow richtext adapters to control type generation, improve generated lexical types ([#4036](https://github.com/payloadcms/payload/issues/4036)) ([989c10e](https://github.com/payloadcms/payload/commit/989c10e0e0b36a8c34822263b19f5cb4b9ed6e72))
* hide publish button based on permissions ([#4203](https://github.com/payloadcms/payload/issues/4203)) ([de02490](https://github.com/payloadcms/payload/commit/de02490231fbc8936973c1b81ac87add39878d8b))
* **richtext-lexical:** Add new position: 'top' property for plugins ([eed4f43](https://github.com/payloadcms/payload/commit/eed4f4361cd012adf4e777820adbe7ad330ffef6))
### Bug Fixes
* fully define the define property for esbuild string replacement ([#4099](https://github.com/payloadcms/payload/issues/4099)) ([e22b95b](https://github.com/payloadcms/payload/commit/e22b95bdf3b2911ae67a07a76ec109c76416ea56))
* **i18n:** polish translations ([#4134](https://github.com/payloadcms/payload/issues/4134)) ([782e118](https://github.com/payloadcms/payload/commit/782e1185698abb2fff3556052fd16d2b725611b9))
* improves live preview breakpoints and zoom options in dark mode ([#4090](https://github.com/payloadcms/payload/issues/4090)) ([b91711a](https://github.com/payloadcms/payload/commit/b91711a74ad9379ed820b6675060209626b1c2d0))
* **plugin-nested-docs:** await populate breadcrumbs on resaveChildren ([#4226](https://github.com/payloadcms/payload/issues/4226)) ([4e41dd1](https://github.com/payloadcms/payload/commit/4e41dd1bf2706001fa03130adb1c69403795ac96))
* rename tab button classname to prevent unintentional styling ([#4121](https://github.com/payloadcms/payload/issues/4121)) ([967eff1](https://github.com/payloadcms/payload/commit/967eff1aabcc9ba7f29573fc2706538d691edfdd))
* **richtext-lexical:** add missing 'use client' to TestRecorder feature plugin ([fc26275](https://github.com/payloadcms/payload/commit/fc26275b7a85fd34f424f7693b8383ad4efe0121))
* **richtext-lexical:** Blocks: Array row data is not removed ([#4209](https://github.com/payloadcms/payload/issues/4209)) ([0af9c4d](https://github.com/payloadcms/payload/commit/0af9c4d3985a6c46a071ef5ac28c8359cb320571))
* **richtext-lexical:** Blocks: fields without fulfilled condition are now skipped for validation ([50fab90](https://github.com/payloadcms/payload/commit/50fab902bd7baa1702ae0d995b4f58c1f5fca374))
* **richtext-lexical:** Blocks: make sure fields are wrapped in a uniquely-named group, change block node data format, fix react key error ([#3995](https://github.com/payloadcms/payload/issues/3995)) ([c068a87](https://github.com/payloadcms/payload/commit/c068a8784ec5780dbdca5416b25ba654afd05458))
* **richtext-lexical:** Blocks: z-index issue, e.g. select field dropdown in blocks hidden behind blocks below, or slash menu inside nested editor hidden behind blocks below ([09f17f4](https://github.com/payloadcms/payload/commit/09f17f44508539cfcb8722f7f462ef40d9ed54fd))
* **richtext-lexical:** Floating Select Toolbar: Buttons and Dropdown Buttons not clickable in nested editors ([615702b](https://github.com/payloadcms/payload/commit/615702b858e76994a174159cb69f034ef811e016)), closes [#4025](https://github.com/payloadcms/payload/issues/4025)
* **richtext-lexical:** HTMLConverter: cannot find nested lexical fields ([#4103](https://github.com/payloadcms/payload/issues/4103)) ([a6d5f2e](https://github.com/payloadcms/payload/commit/a6d5f2e3dea178e1fbde90c0d6a5ce254a8db0d1)), closes [#4034](https://github.com/payloadcms/payload/issues/4034)
* **richtext-lexical:** incorrect caret positioning when selecting second line of multi-line paragraph ([#4165](https://github.com/payloadcms/payload/issues/4165)) ([b210af4](https://github.com/payloadcms/payload/commit/b210af46968b77d96ffd6ef60adc3b8d8bdc9376))
* **richtext-lexical:** make lexicalHTML() function work for globals ([dbfc835](https://github.com/payloadcms/payload/commit/dbfc83520ca8b5e55198a3c4b517ae3a80f9cac6))
* **richtext-lexical:** nested editor may lose focus when writing ([#4139](https://github.com/payloadcms/payload/issues/4139)) ([859c2f4](https://github.com/payloadcms/payload/commit/859c2f4a6d299a42e572133502b3841a74a11002))
* **richtext-lexical:** remove optional chaining after `this` as transpilers are not handling it well ([#4145](https://github.com/payloadcms/payload/issues/4145)) ([2c8d34d](https://github.com/payloadcms/payload/commit/2c8d34d2aadf2fcaf0655c0abef233f341d9945f))
* **richtext-lexical:** visual bug after rearranging blocks ([a6b4860](https://github.com/payloadcms/payload/commit/a6b486007dc26195adc5d576d937e35471c2868f))
* simplifies block/array/hasMany-number field validations ([#4052](https://github.com/payloadcms/payload/issues/4052)) ([803a37e](https://github.com/payloadcms/payload/commit/803a37eaa947397fa0a93b9f4f7d702c6b94ceaa))
* synchronous transaction errors ([#4164](https://github.com/payloadcms/payload/issues/4164)) ([1510baf](https://github.com/payloadcms/payload/commit/1510baf46e33540c72784f2d3f98330a8ff90923))
* thread locale through to access routes from admin panel ([#4183](https://github.com/payloadcms/payload/issues/4183)) ([05f3169](https://github.com/payloadcms/payload/commit/05f3169a75b3b62962e7fe7842fbb6df6699433d))
* transactionID isolation for GraphQL ([#4095](https://github.com/payloadcms/payload/issues/4095)) ([195a952](https://github.com/payloadcms/payload/commit/195a952c4314e0d53fd579517035373b49d6ccae))
* upload fit not accounted for when editing focal point or crop ([#4142](https://github.com/payloadcms/payload/issues/4142)) ([45e9a55](https://github.com/payloadcms/payload/commit/45e9a559bbb16b2171465c8a439044011cebf102))
## [2.1.1](https://github.com/payloadcms/payload/compare/v2.1.0...v2.1.1) (2023-11-10)
### Bug Fixes
* conditionally hide dot menu in DocumentControls ([#4075](https://github.com/payloadcms/payload/issues/4075)) ([cef4cbb](https://github.com/payloadcms/payload/commit/cef4cbb0ee59e1b0b806808d79b402dce114755f))
* disable editing option for svg image types ([#4071](https://github.com/payloadcms/payload/issues/4071)) ([949e265](https://github.com/payloadcms/payload/commit/949e265cd9c95b7d4063336dde86177008d54839))
* fixes creation of related documents within a transaction if filterOptions is used ([#4087](https://github.com/payloadcms/payload/issues/4087)) ([acad288](https://github.com/payloadcms/payload/commit/acad2888cd9a13d5fb9e4c686b2267ea69454eaf))
* hide empty image sizes from the preview drawer ([#3946](https://github.com/payloadcms/payload/issues/3946)) ([687f485](https://github.com/payloadcms/payload/commit/687f4850acf073df0a649ef6182bfc8387857173))
* **live-preview:** ensures field schema exists before traversing fields ([#4074](https://github.com/payloadcms/payload/issues/4074)) ([7059a71](https://github.com/payloadcms/payload/commit/7059a71243a8f98dcc89af0bfe502247db9e4123))
* **live-preview:** field recursion and relationship population ([#4045](https://github.com/payloadcms/payload/issues/4045)) ([2ad7340](https://github.com/payloadcms/payload/commit/2ad73401546ef6608fd67d1f00b537f149640d6a))
* **live-preview:** properly handles apiRoute ([#4076](https://github.com/payloadcms/payload/issues/4076)) ([1f851f2](https://github.com/payloadcms/payload/commit/1f851f21b18c9a5076d9afc9a31abc7a97fcb0df))
* **plugin-nested-docs:** sync write transaction errors ([#4084](https://github.com/payloadcms/payload/issues/4084)) ([47efd3b](https://github.com/payloadcms/payload/commit/47efd3b92e99594dd5b61f0017f4eb76e1d36eb7))
* possible issue with access control not using req ([#4086](https://github.com/payloadcms/payload/issues/4086)) ([348a70c](https://github.com/payloadcms/payload/commit/348a70cc33409b0b48aff3acd2b94c2df5d88f3b))
* **richtext-lexical:** Blocks: unnecessary saving node value when initially opening a document & new lexical tests ([#4059](https://github.com/payloadcms/payload/issues/4059)) ([fff377a](https://github.com/payloadcms/payload/commit/fff377ad22cce3b26142cde8f4125fcee95aa072))
* **richtext-lexical:** floating select toolbar caret not positioned correctly if first line is selected ([#4062](https://github.com/payloadcms/payload/issues/4062)) ([c462df3](https://github.com/payloadcms/payload/commit/c462df38f65b155e131e6a7b46b2bb16cd090e45))
## [2.1.0](https://github.com/payloadcms/payload/compare/v2.0.15...v2.1.0) (2023-11-08)
### Features
* 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)

View File

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

@@ -432,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.
@@ -487,6 +496,101 @@ 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

@@ -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/hooks'
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

@@ -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

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

View File

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

View File

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

View File

@@ -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

@@ -10,13 +10,14 @@ While using Live Preview, the Admin panel emits a new `window.postMessage` event
Wiring your front-end into Live Preview is easy. If your front-end application is built with React or Next.js, use the [`useLivePreview`](#react) React hook that Payload provides. In the future, all other major frameworks like Vue, Svelte, etc will be officially supported. If you are using any of these frameworks today, you can still integrate with Live Preview yourself using the underlying tooling that Payload provides. See [building your own hook](#building-your-own-hook) for more information.
By default, all hooks require the following args:
By default, all hooks accept the following args:
| Path | Description |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`serverURL`** \* | The URL of your Payload server. |
| **`initialData`** | The initial data of the document. The live data will be merged in as changes are made. |
| **`depth`** | The depth of the relationships to fetch. Defaults to `0`. |
| **`apiRoute`** | The path of your API route as defined in `routes.api`. Defaults to `/api`. |
_\* An asterisk denotes that a property is required._

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

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

View File

@@ -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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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,9 +74,10 @@
"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",
"lexical": "0.12.2",
"lint-staged": "^14.0.1",
"minimist": "1.2.8",
"mongodb-memory-server": "8.12.2",
@@ -111,5 +113,8 @@
"*.{js,jsx,ts,tsx}": [
"prettier --write"
]
},
"dependencies": {
"@sentry/react": "^7.77.0"
}
}

View File

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

View File

@@ -63,13 +63,14 @@ export const getViteConfig = async (payloadConfig: SanitizedConfig): Promise<Inl
'module.hot': 'undefined',
'process.argv': '[]',
'process.cwd': 'function () { return "/" }',
'process.env': '{}',
'process?.cwd': 'function () { return "/" }',
}
Object.entries(process.env).forEach(([key, val]) => {
if (key.indexOf('PAYLOAD_PUBLIC_') === 0) {
define[`process.env.${key}`] = `'${val}'`
} else {
define[`process.env.${key}`] = `''`
}
})

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/db-mongodb",
"version": "1.0.6",
"version": "1.0.8",
"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

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

View File

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

View File

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

View File

@@ -46,6 +46,8 @@ export interface Args {
/** Set false to disable $facet aggregation in non-supporting databases, Defaults to true */
useFacet?: boolean
}
/** Set to true to disable hinting to MongoDB to use 'id' as index. This is currently done when counting documents for pagination. Disabling this optimization might fix some problems with AWS DocumentDB. Defaults to false */
disableIndexHints?: boolean
migrationDir?: string
/** The URL to connect to MongoDB or false to start payload and prevent connecting */
url: false | string
@@ -87,6 +89,7 @@ declare module 'payload' {
export function mongooseAdapter({
autoPluralization = true,
connectOptions,
disableIndexHints = false,
migrationDir: migrationDirArg,
url,
}: Args): MongooseAdapterResult {
@@ -105,6 +108,7 @@ export function mongooseAdapter({
collections: {},
connectOptions: connectOptions || {},
connection: undefined,
disableIndexHints,
globals: undefined,
mongoMemoryServer: undefined,
sessions: {},

View File

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

View File

@@ -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

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/db-postgres",
"version": "0.1.11",
"version": "0.1.13",
"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

@@ -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

@@ -422,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,
})
@@ -432,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

@@ -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)
})

View File

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

View File

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

View File

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

View File

@@ -7,12 +7,14 @@ import { mergeData } from '.'
let payloadLivePreviewFieldSchema = undefined // TODO: type this from `fieldSchemaToJSON` return type
export const handleMessage = async <T>(args: {
depth: number
apiRoute?: string
depth?: number
event: MessageEvent
initialData: T
serverURL: string
}): Promise<T> => {
const { depth, event, initialData, serverURL } = args
const { apiRoute, depth, event, initialData, serverURL } = args
if (event.origin === serverURL && event.data) {
const eventData = JSON.parse(event?.data)
@@ -21,7 +23,17 @@ export const handleMessage = async <T>(args: {
payloadLivePreviewFieldSchema = eventData.fieldSchemaJSON
}
if (!payloadLivePreviewFieldSchema) {
// eslint-disable-next-line no-console
console.warn(
'Payload Live Preview: No `fieldSchemaJSON` was received from the parent window. Unable to merge data.',
)
return initialData
}
const mergedData = await mergeData<T>({
apiRoute,
depth,
fieldSchema: payloadLivePreviewFieldSchema,
incomingData: eventData.data,

View File

@@ -2,23 +2,29 @@ import type { fieldSchemaToJSON } from 'payload/utilities'
import { traverseFields } from './traverseFields'
export type MergeLiveDataArgs<T> = {
export const mergeData = async <T>(args: {
apiRoute?: string
depth: number
depth?: number
fieldSchema: ReturnType<typeof fieldSchemaToJSON>
incomingData: Partial<T>
initialData: T
returnNumberOfRequests?: boolean
serverURL: string
}
}): Promise<
T & {
_numberOfRequests?: number
}
> => {
const {
apiRoute,
depth,
fieldSchema,
incomingData,
initialData,
returnNumberOfRequests,
serverURL,
} = args
export const mergeData = async <T>({
apiRoute,
depth,
fieldSchema,
incomingData,
initialData,
serverURL,
}: MergeLiveDataArgs<T>): Promise<T> => {
const result = { ...initialData }
const populationPromises: Promise<void>[] = []
@@ -35,5 +41,8 @@ export const mergeData = async <T>({
await Promise.all(populationPromises)
return result
return {
...result,
...(returnNumberOfRequests ? { _numberOfRequests: populationPromises.length } : {}),
}
}

View File

@@ -1,4 +1,4 @@
type Args = {
export const promise = async (args: {
accessor: number | string
apiRoute?: string
collection: string
@@ -6,20 +6,23 @@ type Args = {
id: number | string
ref: Record<string, unknown>
serverURL: string
}
}): Promise<void> => {
const { id, accessor, apiRoute, collection, depth, ref, serverURL } = args
export const promise = async ({
id,
accessor,
apiRoute,
collection,
depth,
ref,
serverURL,
}: Args): Promise<void> => {
const res: any = await fetch(
`${serverURL}${apiRoute || '/api'}/${collection}/${id}?depth=${depth}`,
).then((res) => res.json())
const url = `${serverURL}${apiRoute || '/api'}/${collection}/${id}?depth=${depth}`
let res: Record<string, unknown> | null | undefined = null
try {
res = await fetch(url, {
credentials: 'include',
headers: {
'Content-Type': 'application/json',
},
}).then((res) => res.json())
} catch (err) {
console.error(err) // eslint-disable-line no-console
}
ref[accessor] = res
}

View File

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

View File

@@ -2,51 +2,52 @@ import type { fieldSchemaToJSON } from 'payload/utilities'
import { promise } from './promise'
type Args<T> = {
export const traverseFields = <T>(args: {
apiRoute?: string
depth: number
depth?: number
fieldSchema: ReturnType<typeof fieldSchemaToJSON>
incomingData: T
populationPromises: Promise<void>[]
result: T
serverURL: string
}
}): void => {
const {
apiRoute,
depth,
fieldSchema: fieldSchemas,
incomingData,
populationPromises,
result,
serverURL,
} = args
export const traverseFields = <T>({
apiRoute,
depth,
fieldSchema,
incomingData,
populationPromises,
result,
serverURL,
}: Args<T>): void => {
fieldSchema.forEach((fieldJSON) => {
if ('name' in fieldJSON && typeof fieldJSON.name === 'string') {
const fieldName = fieldJSON.name
fieldSchemas.forEach((fieldSchema) => {
if ('name' in fieldSchema && typeof fieldSchema.name === 'string') {
const fieldName = fieldSchema.name
switch (fieldJSON.type) {
switch (fieldSchema.type) {
case 'array':
if (Array.isArray(incomingData[fieldName])) {
result[fieldName] = incomingData[fieldName].map((row, i) => {
const hasExistingRow =
Array.isArray(result[fieldName]) &&
typeof result[fieldName][i] === 'object' &&
result[fieldName][i] !== null
result[fieldName] = incomingData[fieldName].map((incomingRow, i) => {
if (!result[fieldName]) {
result[fieldName] = []
}
const newRow = hasExistingRow ? { ...result[fieldName][i] } : {}
if (!result[fieldName][i]) {
result[fieldName][i] = {}
}
traverseFields({
apiRoute,
depth,
fieldSchema: fieldJSON.fields,
incomingData: row,
fieldSchema: fieldSchema.fields,
incomingData: incomingRow,
populationPromises,
result: newRow,
result: result[fieldName][i],
serverURL,
})
return newRow
return result[fieldName][i]
})
}
break
@@ -54,18 +55,21 @@ export const traverseFields = <T>({
case 'blocks':
if (Array.isArray(incomingData[fieldName])) {
result[fieldName] = incomingData[fieldName].map((incomingBlock, i) => {
const incomingBlockJSON = fieldJSON.blocks[incomingBlock.blockType]
const incomingBlockJSON = fieldSchema.blocks[incomingBlock.blockType]
// Compare the index and id to determine if this block already exists in the result
// If so, we want to use the existing block as the base, otherwise take the incoming block
// Either way, we will traverse the fields of the block to populate relationships
const isExistingBlock =
Array.isArray(result[fieldName]) &&
typeof result[fieldName][i] === 'object' &&
result[fieldName][i] !== null &&
result[fieldName][i].id === incomingBlock.id
if (!result[fieldName]) {
result[fieldName] = []
}
const block = isExistingBlock ? result[fieldName][i] : incomingBlock
if (
!result[fieldName][i] ||
result[fieldName][i].id !== incomingBlock.id ||
result[fieldName][i].blockType !== incomingBlock.blockType
) {
result[fieldName][i] = {
blockType: incomingBlock.blockType,
}
}
traverseFields({
apiRoute,
@@ -73,11 +77,11 @@ export const traverseFields = <T>({
fieldSchema: incomingBlockJSON.fields,
incomingData: incomingBlock,
populationPromises,
result: block,
result: result[fieldName][i],
serverURL,
})
return block
return result[fieldName][i]
})
} else {
result[fieldName] = []
@@ -94,7 +98,7 @@ export const traverseFields = <T>({
traverseFields({
apiRoute,
depth,
fieldSchema: fieldJSON.fields,
fieldSchema: fieldSchema.fields,
incomingData: incomingData[fieldName] || {},
populationPromises,
result: result[fieldName],
@@ -105,31 +109,35 @@ export const traverseFields = <T>({
case 'upload':
case 'relationship':
if (fieldJSON.hasMany && Array.isArray(incomingData[fieldName])) {
const existingValue = Array.isArray(result[fieldName]) ? [...result[fieldName]] : []
result[fieldName] = Array.isArray(result[fieldName])
? [...result[fieldName]].slice(0, incomingData[fieldName].length)
: []
// Handle `hasMany` relationships
if (fieldSchema.hasMany && Array.isArray(incomingData[fieldName])) {
if (!result[fieldName]) {
result[fieldName] = []
}
incomingData[fieldName].forEach((relation, i) => {
incomingData[fieldName].forEach((incomingRelation, i) => {
// Handle `hasMany` polymorphic
if (Array.isArray(fieldJSON.relationTo)) {
const existingID = existingValue[i]?.value?.id
if (
existingID !== relation.value ||
existingValue[i]?.relationTo !== relation.relationTo
) {
if (Array.isArray(fieldSchema.relationTo)) {
// if the field doesn't exist on the result, create it
// the value will be populated later
if (!result[fieldName][i]) {
result[fieldName][i] = {
relationTo: relation.relationTo,
relationTo: incomingRelation.relationTo,
}
}
const oldID = result[fieldName][i]?.value?.id
const oldRelation = result[fieldName][i]?.relationTo
const newID = incomingRelation.value
const newRelation = incomingRelation.relationTo
if (oldID !== newID || oldRelation !== newRelation) {
populationPromises.push(
promise({
id: relation.value,
id: incomingRelation.value,
accessor: 'value',
apiRoute,
collection: relation.relationTo,
collection: newRelation,
depth,
ref: result[fieldName][i],
serverURL,
@@ -138,15 +146,13 @@ export const traverseFields = <T>({
}
} else {
// Handle `hasMany` monomorphic
const existingID = existingValue[i]?.id
if (existingID !== relation) {
if (result[fieldName][i]?.id !== incomingRelation) {
populationPromises.push(
promise({
id: relation,
id: incomingRelation,
accessor: i,
apiRoute,
collection: String(fieldJSON.relationTo),
collection: String(fieldSchema.relationTo),
depth,
ref: result[fieldName],
serverURL,
@@ -157,29 +163,49 @@ export const traverseFields = <T>({
})
} else {
// Handle `hasOne` polymorphic
if (Array.isArray(fieldJSON.relationTo)) {
if (Array.isArray(fieldSchema.relationTo)) {
// if the field doesn't exist on the result, create it
// the value will be populated later
if (!result[fieldName]) {
result[fieldName] = {
relationTo: incomingData[fieldName]?.relationTo,
}
}
const hasNewValue =
typeof incomingData[fieldName] === 'object' && incomingData[fieldName] !== null
incomingData[fieldName] &&
typeof incomingData[fieldName] === 'object' &&
incomingData[fieldName] !== null
const hasOldValue =
typeof result[fieldName] === 'object' && result[fieldName] !== null
result[fieldName] &&
typeof result[fieldName] === 'object' &&
result[fieldName] !== null
const newID = hasNewValue
? typeof incomingData[fieldName].value === 'object'
? incomingData[fieldName].value.id
: incomingData[fieldName].value
: ''
const oldID = hasOldValue
? typeof result[fieldName].value === 'object'
? result[fieldName].value.id
: result[fieldName].value
: ''
const newValue = hasNewValue ? incomingData[fieldName].value : ''
const newRelation = hasNewValue ? incomingData[fieldName].relationTo : ''
const oldValue = hasOldValue ? result[fieldName].value : ''
const oldRelation = hasOldValue ? result[fieldName].relationTo : ''
if (newValue !== oldValue || newRelation !== oldRelation) {
if (newValue) {
if (!result[fieldName]) {
result[fieldName] = {
relationTo: newRelation,
}
}
// if the new value/relation is different from the old value/relation
// populate the new value, otherwise leave it alone
if (newID !== oldID || newRelation !== oldRelation) {
// if the new value is not empty, populate it
// otherwise set the value to null
if (newID) {
populationPromises.push(
promise({
id: newValue,
id: newID,
accessor: 'value',
apiRoute,
collection: newRelation,
@@ -188,34 +214,36 @@ export const traverseFields = <T>({
serverURL,
}),
)
} else {
result[fieldName] = null
}
} else {
result[fieldName] = null
}
} else {
// Handle `hasOne` monomorphic
const newID: string =
(typeof incomingData[fieldName] === 'string' && incomingData[fieldName]) ||
(typeof incomingData[fieldName] === 'object' &&
incomingData[fieldName] !== null &&
const newID: number | string | undefined =
(incomingData[fieldName] &&
typeof incomingData[fieldName] === 'object' &&
incomingData[fieldName].id) ||
''
incomingData[fieldName]
const oldID: string =
(typeof result[fieldName] === 'string' && result[fieldName]) ||
(typeof result[fieldName] === 'object' &&
result[fieldName] !== null &&
const oldID: number | string | undefined =
(result[fieldName] &&
typeof result[fieldName] === 'object' &&
result[fieldName].id) ||
''
result[fieldName]
// if the new value is different from the old value
// populate the new value, otherwise leave it alone
if (newID !== oldID) {
// if the new value is not empty, populate it
// otherwise set the value to null
if (newID) {
populationPromises.push(
promise({
id: newID,
accessor: fieldName,
apiRoute,
collection: String(fieldJSON.relationTo),
collection: String(fieldSchema.relationTo),
depth,
ref: result as Record<string, unknown>,
serverURL,
@@ -235,6 +263,4 @@ export const traverseFields = <T>({
}
}
})
return null
}

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.15",
"version": "2.2.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

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

View File

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

View File

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

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

View File

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

View File

@@ -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

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

View File

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

View File

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

View File

@@ -6,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

@@ -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

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

View File

@@ -1,14 +1,19 @@
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 = {
Label?: React.ComponentType<LabelProps>
afterInput?: React.ComponentType<any>[]
'aria-label'?: string
beforeInput?: React.ComponentType<any>[]
checked?: boolean
className?: string
id?: string
@@ -25,7 +30,10 @@ export const CheckboxInput: React.FC<CheckboxInputProps> = (props) => {
const {
id,
name,
Label,
afterInput,
'aria-label': ariaLabel,
beforeInput,
checked,
className,
inputRef,
@@ -36,6 +44,8 @@ export const CheckboxInput: React.FC<CheckboxInputProps> = (props) => {
required,
} = props
const LabelComp = Label || DefaultLabel
return (
<div
className={[
@@ -48,6 +58,7 @@ export const CheckboxInput: React.FC<CheckboxInputProps> = (props) => {
.join(' ')}
>
<div className={`${baseClass}__input`}>
{Array.isArray(beforeInput) && beforeInput.map((Component, i) => <Component key={i} />)}
<input
aria-label={ariaLabel}
defaultChecked={Boolean(checked)}
@@ -58,12 +69,13 @@ export const CheckboxInput: React.FC<CheckboxInputProps> = (props) => {
ref={inputRef}
type="checkbox"
/>
{Array.isArray(afterInput) && afterInput.map((Component, i) => <Component key={i} />)}
<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,7 +5,7 @@ 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'
@@ -18,7 +18,15 @@ const baseClass = 'checkbox'
const Checkbox: React.FC<Props> = (props) => {
const {
name,
admin: { className, condition, description, readOnly, style, width } = {},
admin: {
className,
components: { Error, Label, afterInput, beforeInput } = {},
condition,
description,
readOnly,
style,
width,
} = {},
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,9 +82,12 @@ const Checkbox: React.FC<Props> = (props) => {
}}
>
<div className={`${baseClass}__error-wrap`}>
<Error alignCaret="left" message={errorMessage} showError={showError} />
<ErrorComp alignCaret="left" message={errorMessage} showError={showError} />
</div>
<CheckboxInput
Label={Label}
afterInput={afterInput}
beforeInput={beforeInput}
checked={Boolean(value)}
id={fieldID}
label={getTranslation(label || name, i18n)}

View File

@@ -4,13 +4,13 @@ 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'
import { fieldBaseClass } from '../shared'
import './index.scss'
const prismToMonacoLanguageMap = {
js: 'javascript',
@@ -24,6 +24,7 @@ const Code: React.FC<Props> = (props) => {
name,
admin: {
className,
components: { Error, Label } = {},
condition,
description,
editorOptions,
@@ -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: {
Error?: React.ComponentType<any>
Label?: React.ComponentType<any>
afterInput?: React.ComponentType<any>[]
beforeInput?: 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: { Error, Label, afterInput, beforeInput } = {},
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, '__')}`}>
{Array.isArray(beforeInput) && beforeInput.map((Component, i) => <Component key={i} />)}
<DatePicker
{...datePickerProps}
onChange={onChange}
@@ -78,6 +89,7 @@ export const DateTimeInput: React.FC<DateTimeInputProps> = (props) => {
readOnly={readOnly}
value={value}
/>
{Array.isArray(afterInput) && afterInput.map((Component, i) => <Component key={i} />)}
</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,13 +5,13 @@ 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'
import { fieldBaseClass } from '../shared'
import './index.scss'
const Email: React.FC<Props> = (props) => {
const {
@@ -19,6 +19,7 @@ const Email: React.FC<Props> = (props) => {
admin: {
autoComplete,
className,
components: { Error, Label, afterInput, beforeInput } = {},
condition,
description,
placeholder,
@@ -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">
{Array.isArray(beforeInput) && beforeInput.map((Component, i) => <Component key={i} />)}
<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) || ''}
/>
{Array.isArray(afterInput) && afterInput.map((Component, i) => <Component key={i} />)}
</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,18 +8,28 @@ 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'
import { fieldBaseClass } from '../shared'
import './index.scss'
const NumberField: React.FC<Props> = (props) => {
const {
name,
admin: { className, condition, description, placeholder, readOnly, step, style, width } = {},
admin: {
className,
components: { Error, Label, afterInput, beforeInput } = {},
condition,
description,
placeholder,
readOnly,
step,
style,
width,
} = {},
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">
{Array.isArray(beforeInput) && beforeInput.map((Component, i) => <Component key={i} />)}
<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 : ''}
/>
{Array.isArray(afterInput) && afterInput.map((Component, i) => <Component key={i} />)}
</div>
)}
<FieldDescription description={description} value={value} />

View File

@@ -5,26 +5,39 @@ 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'
import { fieldBaseClass } from '../shared'
import './index.scss'
const baseClass = 'point'
const PointField: React.FC<Props> = (props) => {
const {
name,
admin: { className, condition, description, placeholder, readOnly, step, style, width } = {},
admin: {
className,
components: { Error, Label, afterInput, beforeInput } = {},
condition,
description,
placeholder,
readOnly,
step,
style,
width,
} = {},
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">
{Array.isArray(beforeInput) && beforeInput.map((Component, i) => <Component key={i} />)}
<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] : ''}
/>
{Array.isArray(afterInput) && afterInput.map((Component, i) => <Component key={i} />)}
</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">
{Array.isArray(beforeInput) && beforeInput.map((Component, i) => <Component key={i} />)}
<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] : ''}
/>
{Array.isArray(afterInput) && afterInput.map((Component, i) => <Component key={i} />)}
</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

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

View File

@@ -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

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

View File

@@ -7,13 +7,17 @@ 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 DefaultLabel from '../../Label'
import { fieldBaseClass } from '../shared'
import './index.scss'
export type TextInputProps = Omit<TextField, 'type'> & {
Error?: React.ComponentType<any>
Label?: React.ComponentType<any>
afterInput?: React.ComponentType<any>[]
beforeInput?: React.ComponentType<any>[]
className?: string
description?: Description
errorMessage?: string
@@ -33,6 +37,10 @@ export type TextInputProps = Omit<TextField, 'type'> & {
const TextInput: React.FC<TextInputProps> = (props) => {
const {
Error,
Label,
afterInput,
beforeInput,
className,
description,
errorMessage,
@@ -53,6 +61,9 @@ const TextInput: React.FC<TextInputProps> = (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">
{Array.isArray(beforeInput) && beforeInput.map((Component, i) => <Component key={i} />)}
<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 || ''}
/>
{Array.isArray(afterInput) && afterInput.map((Component, i) => <Component key={i} />)}
</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,
components: { Error, Label, afterInput, beforeInput } = {},
condition,
description,
placeholder,
readOnly,
rtl,
style,
width,
} = {},
inputRef,
label,
localized,
@@ -50,6 +60,10 @@ const Text: React.FC<Props> = (props) => {
return (
<TextInput
Error={Error}
Label={Label}
afterInput={afterInput}
beforeInput={beforeInput}
className={className}
description={description}
errorMessage={errorMessage}

View File

@@ -7,13 +7,17 @@ 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 './index.scss'
import DefaultLabel from '../../Label'
import { fieldBaseClass } from '../shared'
import './index.scss'
export type TextAreaInputProps = Omit<TextareaField, 'type'> & {
Error?: React.ComponentType<any>
Label?: React.ComponentType<any>
afterInput?: React.ComponentType<any>[]
beforeInput?: React.ComponentType<any>[]
className?: string
description?: Description
errorMessage?: string
@@ -32,6 +36,10 @@ export type TextAreaInputProps = Omit<TextareaField, 'type'> & {
const TextareaInput: React.FC<TextAreaInputProps> = (props) => {
const {
Error,
Label,
afterInput,
beforeInput,
className,
description,
errorMessage,
@@ -51,6 +59,9 @@ const TextareaInput: React.FC<TextAreaInputProps> = (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 || ''} />
{Array.isArray(beforeInput) && beforeInput.map((Component, i) => <Component key={i} />)}
<textarea
className="textarea-element"
data-rtl={rtl}
@@ -83,6 +95,7 @@ const TextareaInput: React.FC<TextAreaInputProps> = (props) => {
rows={rows}
value={value || ''}
/>
{Array.isArray(afterInput) && afterInput.map((Component, i) => <Component key={i} />)}
</div>
</label>
<FieldDescription description={description} value={value} />

View File

@@ -18,6 +18,7 @@ const Textarea: React.FC<Props> = (props) => {
name,
admin: {
className,
components: { Error, Label, afterInput, beforeInput } = {},
condition,
description,
placeholder,
@@ -64,6 +65,10 @@ const Textarea: React.FC<Props> = (props) => {
return (
<TextareaInput
Error={Error}
Label={Label}
afterInput={afterInput}
beforeInput={beforeInput}
className={className}
description={description}
errorMessage={errorMessage}

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