Compare commits

..

280 Commits

Author SHA1 Message Date
Alessio Gravili
8c7d8fcb65 ignore4 2024-04-04 23:40:05 -04:00
Alessio Gravili
5472c88475 ignore3 2024-04-04 23:32:25 -04:00
Alessio Gravili
74e15566eb ignore2 2024-04-04 23:16:04 -04:00
Alessio Gravili
39f2d7ad30 ignore 2024-04-04 22:42:51 -04:00
Alessio Gravili
7af12dadc9 ignore! 2024-04-04 22:36:54 -04:00
Alessio Gravili
70ddd2ddc6 ignore 2024-04-04 22:35:39 -04:00
Alessio Gravili
63076af946 ignore 2024-04-04 22:29:06 -04:00
Alessio Gravili
55b31378aa idk what im doing 2024-04-04 21:08:51 -04:00
Alessio Gravili
40f734303b Merge branch 'ci/experiments' of https://github.com/payloadcms/payload into ci/experiments 2024-04-04 19:10:31 -04:00
Alessio Gravili
aa53fdaede run again 2024-04-04 19:10:05 -04:00
Alessio Gravili
37077f42de Merge branch 'alpha' into ci/experiments 2024-04-04 18:55:33 -04:00
James Mikrut
0e6991f486 chore: green ci (#5673) 2024-04-04 18:44:27 -04:00
James
014786cc5f chore: green ci 2024-04-04 18:44:04 -04:00
Alessio Gravili
fd9b20ff31 run again 2024-04-04 18:30:30 -04:00
James Mikrut
223c6b50fc Fix/fields e2e (#5672) 2024-04-04 18:24:21 -04:00
James
b18946352e chore: misc fields e2e fixes 2024-04-04 18:23:31 -04:00
Alessio Gravili
dc3bba0477 Merge branch 'alpha' into ci/experiments 2024-04-04 18:13:30 -04:00
Alessio Gravili
31f7d98e58 run again 2024-04-04 18:06:00 -04:00
James
96012b26b7 chore: properly isolates req in parallel requests 2024-04-04 18:00:37 -04:00
James
31cd663ad5 chore: merge alpha 2024-04-04 17:33:30 -04:00
Jacob Fletcher
0e9c9d7ccf Fix/admin e2e (#5670) 2024-04-04 17:31:42 -04:00
James
2a6eb6ec86 Merge branch 'fix/fields-e2e' of github.com:payloadcms/payload into fix/fields-e2e 2024-04-04 17:25:21 -04:00
James
0a74423c07 chore: replaces clearAndSeedEverything 2024-04-04 17:25:09 -04:00
Alessio Gravili
6f1302ae67 chore: skip react-select flakester test 2024-04-04 17:23:07 -04:00
Alessio Gravili
f275570f12 fix: WhereBuilder Relationship placeholder not shown if value is cleared 2024-04-04 17:18:16 -04:00
Alessio Gravili
90b47f6c44 Merge remote-tracking branch 'origin/fix/fields-e2e' into fix/fields-e2e 2024-04-04 17:10:14 -04:00
Alessio Gravili
e73d008695 fix: WhereBuilder Relationship conditions weren't able to be removed 2024-04-04 17:09:58 -04:00
Jacob Fletcher
3c5d1b402c test(admin): enables entity descriptions test 2024-04-04 17:09:22 -04:00
Jacob Fletcher
3e22bccce4 fix(ui): ssr entity descriptions 2024-04-04 17:08:03 -04:00
Jacob Fletcher
354e140305 fix(ui): ssr entity descriptions 2024-04-04 17:07:09 -04:00
James
54859f3582 Merge branch 'fix/fields-e2e' of github.com:payloadcms/payload into fix/fields-e2e 2024-04-04 17:04:35 -04:00
James
da12efd675 chore: more passing fields e2e 2024-04-04 17:04:23 -04:00
Alessio Gravili
32f848e90d fix: correctly extract relationTo prop for Relationship field condition in WhereBuilder 2024-04-04 16:55:46 -04:00
Jacob Fletcher
32440d23f7 chore(ui): extracts collection and global component maps into standalone files 2024-04-04 16:40:58 -04:00
Alessio Gravili
92f2ad7c15 run again 2024-04-04 16:32:47 -04:00
Alessio Gravili
73c76daef2 run again 2024-04-04 16:31:17 -04:00
Alessio Gravili
1b2241f02d chore: intentionally fail test 2024-04-04 16:27:51 -04:00
Alessio Gravili
12f34cab16 test 2024-04-04 16:23:43 -04:00
Jarrod Flesch
62b7acc93a chore: simplify usage of useTitle 2024-04-04 16:15:49 -04:00
Jacob Fletcher
7b8e2c75c2 ci: enables admin e2e test suite 2024-04-04 16:09:46 -04:00
Jacob Fletcher
03abc641c5 test(admin): ignores search params when waiting for list view url 2024-04-04 16:09:46 -04:00
Alessio Gravili
fa5b98c9b5 chore: wait through e2e flakes 2024-04-04 15:59:48 -04:00
Alessio Gravili
c681be7ba8 chore: reduce flakes in fields/uploads e2e test suite 2024-04-04 15:17:52 -04:00
Alessio Gravili
3d3305a312 Merge remote-tracking branch 'origin/alpha' into fix/fields-e2e 2024-04-04 14:57:22 -04:00
Alessio Gravili
bb305af7b4 chore: fields-relationship e2e fixes (#5665) 2024-04-04 14:53:16 -04:00
Alessio Gravili
fd284973b6 ci: add missing --with-deps to playwright install command (#5667) 2024-04-04 14:52:59 -04:00
James
5d57572694 chore: work to add consistency to fields e2e 2024-04-04 14:41:58 -04:00
Jarrod Flesch
36a22f2b3c chore: fixes failing where builder 2024-04-04 14:28:47 -04:00
Patrik
dea9b590d1 fix: incorrect tooltip colors in light mode (#5636) 2024-04-04 14:13:25 -04:00
Patrik
bf843fe598 fix: skip parsing if operator is exists (#5639) 2024-04-04 14:11:22 -04:00
Jacob Fletcher
a23bc6caa8 Merge pull request #5646 from payloadcms/fix/alpha/admin-e2e
Fix/alpha/admin e2e
2024-04-04 14:10:21 -04:00
Alessio Gravili
1904fd5b02 chore: commit intellij payload.iml and mark temp or non-core directories as excluded. This excludes them from search (#5662) 2024-04-04 14:03:44 -04:00
Jacob Fletcher
03c9a883e1 Merge branch 'alpha' into fix/alpha/admin-e2e 2024-04-04 13:48:11 -04:00
Jacob Fletcher
c06df267a3 test(live-preview): uses correct locator to find linked cell 2024-04-04 13:42:55 -04:00
Jacob Fletcher
0bccdfeda7 fix(ui): finds index of useAsTitle after mutating column order 2024-04-04 13:42:55 -04:00
Jarrod Flesch
07d118ae7d chore: fix filtering tests 2024-04-04 13:39:17 -04:00
Alessio Gravili
e912dde08d chore: ensure autologin passes before starting tests for all e2e test suites (#5659) 2024-04-04 13:39:06 -04:00
Elliot DeNolf
3544375fdd chore(deps): remove release-it (#5658) 2024-04-04 12:57:13 -04:00
Elliot DeNolf
ab6ca7910e ci: release script concurrency (#5657)
* chore(deps): update turborepo

* chore: unprettified upload pointer files

* ci: use p-map in release script
2024-04-04 12:37:05 -04:00
Jacob Fletcher
6a329f7a8e fix(ui): adds optional chaining to fieldComponentProps in buildColumnState 2024-04-04 12:23:31 -04:00
Elliot DeNolf
5d4bb10106 chore: update all package.json repository urls and homepage (#5655) 2024-04-04 12:02:08 -04:00
Jarrod Flesch
cbc079bfff Merge branch 'fix/alpha/fields-e2et push' into fix/alpha/admin-e2e 2024-04-04 11:58:21 -04:00
Jarrod Flesch
09358d5853 chore: fix customLabel inside buildColumnState 2024-04-04 11:42:48 -04:00
Jacob Fletcher
81c345f33e test(admin): extracts api view tests into standalone group 2024-04-04 11:25:53 -04:00
Jacob Fletcher
3aa200eacc chore(deps): removes react-router-dom from peer deps 2024-04-04 11:24:09 -04:00
James Mikrut
6ce0b60cf2 Merge pull request #5652 from payloadcms/fix/ensure-indexes
chore: re-enables fields-relationship tests
2024-04-04 11:23:45 -04:00
James
faef0784ee chore: re-enables fields-relationship tests 2024-04-04 11:22:39 -04:00
Jarrod Flesch
fb70fe5760 chore: skips i18n tests for now 2024-04-04 11:09:31 -04:00
Jarrod Flesch
5b75b8a89e chore: fixes multi-select stalling tests 2024-04-04 11:04:46 -04:00
Jarrod Flesch
fa0296b796 chore: passing admin/list-filtering/multi-select 2024-04-04 11:04:46 -04:00
Jarrod Flesch
4e2d1f568f chore: fix plugin-form-builder test 2024-04-04 11:04:46 -04:00
Jacob Fletcher
b8d1aec1e5 fix(ui): properly sanitizes functional labels from field map 2024-04-04 10:53:04 -04:00
James Mikrut
38cfd6985e Merge pull request #5651 from payloadcms/fix/ensure-indexes
chore: ensure indexes are created before running localization test suite
2024-04-04 10:22:02 -04:00
James
d7c20c6941 chore: ensure indexes are created before running localization test suite 2024-04-04 10:11:45 -04:00
Jacob Fletcher
7894a54a0e Merge branch 'alpha' into fix/alpha/admin-e2e 2024-04-04 10:02:37 -04:00
James Mikrut
a46e64eec3 Merge pull request #5650 from payloadcms/fix/e2e-flakes
chore: adds logging, converts localization to use no config
2024-04-04 09:59:47 -04:00
Jarrod Flesch
cae0399584 chore: adds types to default columns 2024-04-04 09:54:05 -04:00
Jacob Fletcher
bfbf4ef0b5 test(admin): improves file organization and test names 2024-04-04 09:52:51 -04:00
Jarrod Flesch
ca4004605e chore: adds types to default columns 2024-04-04 09:52:21 -04:00
James
ec565a1bd3 chore: adds update to test sdk 2024-04-04 09:47:08 -04:00
Jarrod Flesch
94c4b180c1 chore: fix tsc errors 2024-04-04 09:40:36 -04:00
James
66de0b9019 chore: adds logging, converts localization to use no config 2024-04-04 09:34:45 -04:00
Jacob Fletcher
f752b38228 test(admin): partially passing list view filtering 2024-04-04 09:30:30 -04:00
Jarrod Flesch
d79748a967 chore: more admin test fixes 2024-04-04 09:28:12 -04:00
Alessio Gravili
c1b6c2c5a5 chore: unflake versions e2e (#5616) 2024-04-04 09:07:44 -04:00
Alessio Gravili
806b04e0ca Merge pull request #5633 from payloadcms/feat/form-server-validation 2024-04-04 09:07:05 -04:00
Alessio Gravili
736b562f3a chore: clean-up console logs 2024-04-04 09:06:09 -04:00
James
db440236fc Merge branch 'feat/form-server-validation' of github.com:payloadcms/payload into feat/form-server-validation 2024-04-04 09:04:30 -04:00
James
00ea8b900a chore: skips bulk edit test until fixes are merged 2024-04-04 09:04:14 -04:00
Paul
e092e9ba67 fix: missing data in first user registration (#5645) 2024-04-03 19:08:10 -03:00
Jacob Fletcher
8313cf34a6 test(admin): passing custom css 2024-04-03 18:03:10 -04:00
Jacob Fletcher
8f4b8f5826 fix(ui): shares sort order between where builder and column selector 2024-04-03 17:31:57 -04:00
Jacob Fletcher
c607b01f33 chore(ui): extracts reduceFieldMap from where builder 2024-04-03 17:31:47 -04:00
Jarrod Flesch
cc5d01d0e2 chore: fixes deleteAllPosts test helper 2024-04-03 17:07:45 -04:00
Alessio Gravili
a4cc41b679 chore: skip more flaky tests 2024-04-03 17:04:26 -04:00
Jarrod Flesch
67361e9ed5 chore: fixes bulk upldate test 2024-04-03 16:56:54 -04:00
Alessio Gravili
8662572690 chore: enable fields on CI 2024-04-03 16:54:27 -04:00
Alessio Gravili
99ea1788e7 chore: skip flaky fields-relationship tests for now 2024-04-03 16:54:07 -04:00
James
7bec3c90cd chore: adds ux de-flaking to relationship field 2024-04-03 16:14:01 -04:00
Jacob Fletcher
ca4e6c46bc fix(ui): properly sorts table columns 2024-04-03 15:09:38 -04:00
James
678da159a9 Merge branch 'alpha' of github.com:payloadcms/payload into feat/form-server-validation 2024-04-03 14:20:39 -04:00
James
cc3b51fb3b chore: logs to localization seed 2024-04-03 14:18:36 -04:00
James
060344bb5a chore: logs for ci visibility 2024-04-03 14:09:42 -04:00
Jacob Fletcher
b42c67040d fix(ui): properly flattens tabs field map 2024-04-03 14:01:21 -04:00
Alessio Gravili
f3b18fcf0e chore: move autosave new document creation & redirect logic to server, fixes versions e2e flakes due to late redirect 2024-04-03 13:50:52 -04:00
Elliot DeNolf
a86c69edc9 fix: sets beforeValidateHook req type to required (#5634)
Co-authored-by: Patrik <patrik@payloadcms.com>
2024-04-03 13:45:44 -04:00
Jessica Chowdhury
55c60a05dc feat(plugin-seo): adds Norwegian translation (#5629) 2024-04-03 13:26:44 -04:00
Jacob Fletcher
ecf40cc747 fix(ui): renders field description functions 2024-04-03 13:18:49 -04:00
Jacob Fletcher
197458e60b chore: adds isReactComponent utility 2024-04-03 13:18:49 -04:00
Alessio Gravili
0c3ffb0743 chore: expect accurate error in uploads e2e test 2024-04-03 13:14:38 -04:00
Alessio Gravili
56dffd3c58 fix: validation not running correctly when changing field state and submitting form immediately after 2024-04-03 13:00:23 -04:00
Jarrod Flesch
57a3a37fdd chore: passing admin/i18n tests 2024-04-03 12:40:41 -04:00
Elliot DeNolf
a8a273f0d8 chore(cpa): ensure project name is slugified (#5631) 2024-04-03 12:18:46 -04:00
Jarrod Flesch
9f4ab26696 chore: passing admin/nav tests 2024-04-03 12:17:48 -04:00
James
4ddef9d648 chore: adds prop to form to allow server validation only on submit 2024-04-03 12:03:05 -04:00
Jessica Chowdhury
7cccca8194 chore(alpha): update fields-relationship e2e tests (#5553)
* fix: handles filter options in form state merge

* chore: fix and reintegrate fields-relationship e2e tests

* chore: update withMergedProps function for e2e tests
2024-04-03 16:11:19 +01:00
James Mikrut
2a8b678a4b Merge pull request #5604 from payloadcms/fix/postgres-fields
chore: revisions to baseIDField
2024-04-03 10:55:43 -04:00
Jarrod Flesch
b9767b865a chore: working but wip admin e2e 2024-04-03 10:47:01 -04:00
James
f6bc3eb014 chore: passing pg 2024-04-03 10:39:38 -04:00
Elliot DeNolf
007917df19 ci: docker compose v2, v1 no longer supported on gh runner (#5626) 2024-04-03 10:34:49 -04:00
Alessio Gravili
22a2e850bf Merge pull request #5627 from payloadcms/chore/port-lexical-fixes 2024-04-03 09:56:36 -04:00
Alessio Gravili
1cfdf3613c docs(richtext-lexical): clarify that HTML generation has to happen on the server 2024-04-03 09:55:33 -04:00
Alessio Gravili
fe280e6bb1 fix(richtext-lexical): disable instanceof HTMLImageElement check as it causes issues when used on the server 2024-04-03 09:55:23 -04:00
Jarrod Flesch
a330fe6017 chore(ui): simplifies adminThumbnail functionality (#5615) 2024-04-03 08:49:31 -04:00
Jessica Chowdhury
4ee4ad25b0 fix(alpha): number field with hasMany accept defaultValue array (#5619) 2024-04-03 08:05:49 -04:00
Paul
777a661389 chore: remove dead refresh permissions test from github workflows (#5614) 2024-04-02 20:57:54 -03:00
Paul
8174230afe chore: plugin form builder e2e (#5612)
* chore: update plugin files to esm

* chore: add e2e for plugin form builder

* chore: update release script and gh workflow

* chore: update build command for form builder plugin
2024-04-02 20:56:37 -03:00
Alessio Gravili
e1777dc533 ci: do not fail upload-artifact action if more than 2 e2e tests fail (#5605) 2024-04-02 18:36:07 -04:00
Elliot DeNolf
390731c07b ci: separate tests-unit, esm scripts (#5607)
* ci: script esm updates

* ci: separate out unit tests
2024-04-02 17:31:38 -04:00
James
25d475e165 chore: revisions to baseIDField 2024-04-02 17:04:27 -04:00
Alessio Gravili
1e60250670 chore: enable all fields int tests (#5603) 2024-04-02 17:00:32 -04:00
Alessio Gravili
f6d2dd520c Merge pull request #5561 from payloadcms/temp38
fix: test suite & transactions fixes
2024-04-02 16:00:10 -04:00
Alessio Gravili
4600588e72 chore: disable uploads e2e 2024-04-02 15:59:25 -04:00
Dan Ribbens
825ca94080 fix: duplicate handles locales with unique (#5600)
* fix: duplicate errors with localized and unique fields

* docs: beforeDuplicate hooks
2024-04-02 15:30:49 -04:00
Alessio Gravili
7f674f9861 chore: fix payload HMR being run during e2e & int tests 2024-04-02 15:01:18 -04:00
James
27dba7e4e1 chore: improper expect in upload test suite 2024-04-02 14:21:58 -04:00
Alessio Gravili
44295ff248 chore: use initPayloadInt consistently in all int test suites and do not init payload twice 2024-04-02 13:39:01 -04:00
Alessio Gravili
dc33d96a54 chore: remove async seeding from auth and fields-relationship test suites 2024-04-02 13:25:12 -04:00
Alessio Gravili
4ff7619356 chore: remove console logs 2024-04-02 12:29:38 -04:00
Alessio Gravili
cc5c2bd7cd chore: fix flakes in versions test suite 2024-04-02 12:27:29 -04:00
Alessio Gravili
2884712685 chore: fix versions int test seeding 2024-04-02 12:17:27 -04:00
Alessio Gravili
027264588b chore: versions test improvements 2024-04-02 12:13:12 -04:00
Alessio Gravili
ddd75ce730 Merge remote-tracking branch 'origin/temp38' into temp38 2024-04-02 12:12:51 -04:00
Alessio Gravili
4bc13c28dd chore: passing live-preview 2024-04-02 12:12:42 -04:00
James
7a1db89a6e Merge branch 'temp38' of github.com:payloadcms/payload into temp38 2024-04-02 12:12:35 -04:00
James
c08489509a chore: startMemoryDB in e2e 2024-04-02 12:12:21 -04:00
Alessio Gravili
7054ae8a88 chore: unit/int test CI stuff 2024-04-02 12:00:32 -04:00
Alessio Gravili
d7e913be95 fix: do not re-use same transaction ID for parallel operations 2024-04-02 11:08:04 -04:00
James
f283a2ced5 Merge branch 'temp38' of github.com:payloadcms/payload into temp38 2024-04-02 10:40:06 -04:00
James
c7274ba16f chore: wires up conditions for collapsibles, groups, etc 2024-04-02 10:39:52 -04:00
Alessio Gravili
6f323e379c add console logs 2024-04-02 10:35:20 -04:00
Alessio Gravili
42212b409a chore: remove console log 2024-04-02 10:19:24 -04:00
James
e8506cc5f1 chore: startMemoryDB pointing to mongodb instead of mongoose 2024-04-02 10:02:51 -04:00
James
d387f9f1fa chore: working pattern for debugging e2e and int 2024-04-02 10:01:47 -04:00
James
73a555788d chore: uses globalSetup for starting memory db 2024-04-02 09:44:55 -04:00
Elliot DeNolf
be58f67115 test(uploads): remove all process.cwd() usage (#5588) 2024-04-02 00:08:58 -04:00
Elliot DeNolf
b26117a65d feat(cpa): strict true 😈 (#5587) 2024-04-01 23:05:57 -04:00
Alessio Gravili
34fe6182c8 temp3 2024-04-01 23:05:54 -04:00
Alessio Gravili
ee3ae6025f temp2 2024-04-01 22:41:24 -04:00
James
df9812b2a3 Merge branch 'temp38' of github.com:payloadcms/payload into temp38 2024-04-01 22:13:02 -04:00
James
113eea04cc chore: seed endpoint 2024-04-01 22:12:45 -04:00
Alessio Gravili
57f9ebdb68 temp1 2024-04-01 22:04:45 -04:00
James
94d0e28ad7 chore: local api sdk for e2e tests 2024-04-01 21:53:30 -04:00
James
cd553d45cc chore: merge 2024-04-01 17:47:28 -04:00
James
d993f9ac64 chore: bug in isMongoose 2024-04-01 17:46:42 -04:00
James
833bdc13bd chore: uploads e2e tweak 2024-04-01 17:38:24 -04:00
James Mikrut
33657b4b49 Merge branch 'alpha' into temp38 2024-04-01 17:37:54 -04:00
James
ec6bc8e36b chore: removes old refs to startMemoryDB 2024-04-01 17:36:36 -04:00
Jacob Fletcher
799370f753 fix(next): establishes pattern for preview urls (#5581) 2024-04-01 17:30:49 -04:00
James
abd404c57c chore: adjusts playwright env used to trigger memory db 2024-04-01 17:30:18 -04:00
Kendell Joseph
037ed3cd54 test: e2e uploads (#5511)
* chore: enables upload tests on CI

* fix: adds relationTo information to field map

* chore: updates e2e tests (WIP)

* chore: move back to probe-image-size, tiff files do not support buffers

* chore: basic runtime err fixes

* chore: remove admin thumbnail when creating client config

* test: small upload fixes

---------

Co-authored-by: Elliot DeNolf <denolfe@gmail.com>
Co-authored-by: Jarrod Flesch <jarrodmflesch@gmail.com>
2024-04-01 17:28:15 -04:00
James
c461a7fa15 chore: renames mongoose db adapter refs to mongodb 2024-04-01 17:24:43 -04:00
James
8fc8aaa6dd yammil 2024-04-01 17:14:43 -04:00
James
2bc45e2b2e chore: adds logging for ci 2024-04-01 17:12:16 -04:00
James
cdfc58d115 chore: re-adds int 2024-04-01 17:08:50 -04:00
James
bb8a57d2e9 chore: better pattern to initialize memory server 2024-04-01 17:04:05 -04:00
James
5e52339135 chore: converts e2e suites to new pattern 2024-04-01 16:37:12 -04:00
James
df75914e30 chore: attempts to get _community to pass with change to import order of config 2024-04-01 16:29:22 -04:00
Patrik
572e6ccb37 fix(ui): places id field last in field map and prevents render (#5585) 2024-04-01 16:25:29 -04:00
James
8e1ebe28c0 chore: adds node env for e2e tests 2024-04-01 15:57:23 -04:00
James
adec044e02 chore: returns runE2E 2024-04-01 15:55:38 -04:00
James
b9868cc709 chore: reverts memory test approach 2024-04-01 15:51:40 -04:00
James
fce8b125f8 Merge branch 'temp38' of github.com:payloadcms/payload into temp38 2024-04-01 15:29:43 -04:00
James
4befd2e4ff chore: sets env vars for tests in globalSetup 2024-04-01 15:29:27 -04:00
James Mikrut
38cdc1b7ba Merge branch 'alpha' into temp38 2024-04-01 14:37:33 -04:00
James
a0f6018469 chore: better pattern for memory db 2024-04-01 14:36:08 -04:00
James
f230d55031 chore: restores memory db 2024-04-01 11:11:52 -04:00
James
2f6a15a9ae chore: calculates default values before running buildFormState 2024-04-01 10:52:26 -04:00
Elliot DeNolf
04d751208f Merge pull request #5557 from payloadcms/feat/cpa-detect-next-app
feat(cpa): detect next app
2024-04-01 10:44:43 -04:00
Elliot DeNolf
7cfc40f328 test(cpa): update tests 2024-04-01 10:32:17 -04:00
Elliot DeNolf
3c54d32b6d feat(cpa): rework all prompts to use @clack/prompts 2024-04-01 10:16:07 -04:00
Jessica Chowdhury
ece7d92e57 chore: updates e2e tests for plugin-nested-docs and plugin-seo (#5434)
* test: removes unnecessary lines

* fix: do not error if row field has no fields (#5433)

* ci(deps): update turborepo

* ci: release script updates

* chore: lint all json/yml, add to lint-staged

* chore: lint mdx in lint-staged

* chore: enable e2e live preview (#5444)

* chore: update workflow file

---------

Co-authored-by: Alessio Gravili <70709113+AlessioGr@users.noreply.github.com>
Co-authored-by: Elliot DeNolf <denolfe@gmail.com>
Co-authored-by: Paul <paul@payloadcms.com>
2024-04-01 15:01:05 +01:00
James
8e736b28af Merge branch 'temp38' of github.com:payloadcms/payload into temp38 2024-04-01 09:30:12 -04:00
Alessio Gravili
92ec0a5b1d chore: temporarily disable int tests, and field-error-states, live-preview, versions e2e's (#5578) 2024-03-31 22:41:52 -04:00
Alessio Gravili
398834f690 øMerge remote-tracking branch 'origin/alpha' into temp38 2024-03-31 21:35:54 -04:00
Alessio Gravili
28e6dd8759 fix: do not exclude admin.hidden fields from default active columns (#5556) 2024-03-31 21:35:11 -04:00
Alessio Gravili
dedc937915 Merge pull request #5576 from payloadcms/feat/alpha-5574
feat(richtext-*): add ability to provide custom Field and Error components
2024-03-31 21:17:19 -04:00
Alessio Gravili
732f4241fe chore: commit updated translation files 2024-03-31 21:16:17 -04:00
Alessio Gravili
873585e1ae feat(richtext-*): add ability to provide custom Field and Error components 2024-03-31 21:12:52 -04:00
Alessio Gravili
71a5a02e8c docs(richtext-slate): update outdated code example (#5572) (#5573) 2024-03-31 17:50:15 -04:00
Alessio Gravili
51fbd02b40 fix(richtext-lexical): checklist html converter incorrectly outputting children (#5570) (#5571) 2024-03-31 16:25:16 -04:00
Alessio Gravili
763eda5038 fix(richtext-lexical): properly center add- and drag-block handles (#5568) (#5569) 2024-03-31 16:08:00 -04:00
Alessio Gravili
6cdb76503b chore: disable memory db for now, as it doesn't work locally for the versions test suite 2024-03-31 16:05:55 -04:00
Alessio Gravili
aa8edd7a47 chore: fix issues in versions e2e test 2024-03-31 16:05:20 -04:00
Alessio Gravili
535aa56627 fix: do not pass undefined data through buildStateFromSchema for tab fields 2024-03-29 17:32:40 -04:00
Elliot DeNolf
db0fb30f7b test(cpa): update jest config 2024-03-29 17:28:35 -04:00
Elliot DeNolf
7619fb4753 feat(cpa): handle next.js app with and without src dir 2024-03-29 16:45:52 -04:00
Paul
0aeba954d4 fix: localization e2e (#5555)
* fix: issue with missing locale when duplication localized collections

* chore: fix localization tests
2024-03-29 17:45:26 -03:00
Alessio Gravili
2bc1468fa2 chore: tests: uploads dir: delete and restore snapshot between tests (#5560)
* chore: tests: uploads dir: delete and restore snapshot between tests

* chore: add missing creation of uploads dir cache folder

* fix logic
2024-03-29 16:29:40 -04:00
James Mikrut
4b29b6efc5 Merge pull request #5559 from payloadcms/temp36
Temp36
2024-03-29 14:57:27 -04:00
James
26b1003cfd Merge branch 'alpha' of github.com:payloadcms/payload into alpha 2024-03-29 14:56:15 -04:00
James
b696dce6e4 chore: disables password fields if disableLocalStrategy, safely inherits valid: false 2024-03-29 14:55:58 -04:00
Elliot DeNolf
403a86feca chore(create-payload-app): configure db in init next flow 2024-03-29 14:25:00 -04:00
Jacob Fletcher
56d6a9767e Merge pull request #5551 from payloadcms/fix/nav
fix: misc.
2024-03-29 13:58:22 -04:00
James
d2cc229622 Merge branch 'alpha' of github.com:payloadcms/payload into fix/alpha/rte-e2e-tests 2024-03-29 13:51:29 -04:00
Jacob Fletcher
e6b166da7d fix(next): proper 404 handling 2024-03-29 13:30:00 -04:00
Elliot DeNolf
77f401d977 chore(create-payload-app): console.log wrapper 2024-03-29 13:23:25 -04:00
Elliot DeNolf
7d7b232fdb feat(create-payload-app): functioning init next flow, no prompts 2024-03-29 12:48:00 -04:00
James
959f1e33cd chore: form now validates without field validate functions 2024-03-29 12:44:40 -04:00
Elliot DeNolf
7f5ab96f81 chore: move payload config into src 2024-03-29 12:22:32 -04:00
James
443089a66f chore: fixes Translation component 2024-03-29 11:54:20 -04:00
Jacob Fletcher
f5d9b47177 fix(richtext-lexical): uses entity visibility hook when enabling relationships 2024-03-29 11:54:02 -04:00
Jacob Fletcher
a0cddbe9b3 fix(richtext-slate): uses entity visibility hook when enabling relationships 2024-03-29 11:36:49 -04:00
Jacob Fletcher
5f7fcfd3df chore(next): uses visibileEntities in getViewsFromConfig 2024-03-29 11:30:33 -04:00
Alessio Gravili
a39080340a fix: missing translation key for richtext fields (#5550) 2024-03-29 11:11:50 -04:00
Jacob Fletcher
a3d6879c55 fix(ui): wraps nav with fragment 2024-03-29 11:10:43 -04:00
Alessio Gravili
b09be86a3c Merge pull request #5549 from payloadcms/temp33
fix: unload client functions after unmount (e.g. leaving document)
2024-03-29 11:07:47 -04:00
James
02ef033d23 chore: fixes infinite processing when submitting bad form 2024-03-29 11:03:21 -04:00
Alessio Gravili
f10861e1de chore: add lexical test which verifies it's working 2024-03-29 10:53:30 -04:00
James
ecf53d9961 chore: passes through props in listinfoprovider 2024-03-29 10:51:24 -04:00
Paul
5339c09b72 fix: access control test suite (#5548)
* chore: improve flakiness with access control test suite

* fix issue with redirecting from a drawer

* chore: watches for created id in drawers

---------

Co-authored-by: James <james@trbl.design>
2024-03-29 11:46:46 -03:00
Jacob Fletcher
b6ad218126 fix(ui): establishes pattern for hidden entities (#5546) 2024-03-29 10:32:09 -04:00
Alessio Gravili
b7b74a429e fix: unload client functions after unmount (e.g. leaving document) 2024-03-29 09:54:11 -04:00
James
52acd3123f chore: allows slate to render 2024-03-29 09:48:54 -04:00
Patrik
c9c3a689d8 fix: flaky indexed test suite (#5509) 2024-03-29 09:01:44 -04:00
Patrik
da4a2a2494 fix: reverts selector in array bulk update test to original to get passing test (#5512) 2024-03-29 09:00:48 -04:00
James
35a1fb26f9 Merge branch 'fix/alpha/rte-e2e-tests' of github.com:payloadcms/payload into fix/alpha/rte-e2e-tests 2024-03-29 08:27:00 -04:00
Jarrod Flesch
cb3723242c fix: passing versions e2e (#5521) 2024-03-29 01:20:02 -04:00
Patrik
6a0c6284d0 fix: passing blocks field test suite (#5529) 2024-03-28 20:56:52 -04:00
Jarrod Flesch
114ade7456 chore: wip rte field work 2024-03-28 16:53:32 -04:00
Kendell Joseph
a01e0e37f4 fix: corrects query so upload edits can happen (#5527) 2024-03-28 16:22:26 -04:00
Elliot DeNolf
18884025de ci: disable access-control suite until flakes are fixed 2024-03-28 15:41:20 -04:00
Alessio Gravili
cfdc941207 fix(richtext-lexical): Blocks: do not include empty arrays in form state (#5526) 2024-03-28 15:40:02 -04:00
Kendell Joseph
5eaf00ba0e chore: use dirname to resolve file locations for uploads test (#5523) 2024-03-28 15:35:35 -04:00
Elliot DeNolf
8948555ac1 ci: re-enable fields int suite (#5525) 2024-03-28 15:34:09 -04:00
Alessio Gravili
e28418d6d6 fix: onError not found error (#5524) 2024-03-28 15:10:20 -04:00
Kendell Joseph
fe83c53206 fix(ui): adds relationTo to cellComponentProps (#5518) 2024-03-28 14:43:46 -04:00
Alessio Gravili
942aa08285 chore: obliterate next cache before starting e2e/int/dev (#5522) 2024-03-28 14:42:45 -04:00
Alessio Gravili
93dd6b5a98 chore: add skipped, failing lexical e2e test for errors within nested block fields, fix lexical seed data, disable access-control test (#5508) 2024-03-28 13:36:05 -04:00
James Mikrut
08dd9ca91c Merge pull request #5516 from payloadcms/feat/simplify-doc-fetch
feat: simplifies fetching of docs for drawer and edit views
2024-03-28 13:35:07 -04:00
Jacob Fletcher
77efdc3ccf fix(ui): adds support for direct field label props (#5517) 2024-03-28 13:22:09 -04:00
James
ef1bcd5afa chore: fixes redirection on create 2024-03-28 13:18:35 -04:00
Jarrod Flesch
8636685252 fix: simplifies field error paths (#5504) 2024-03-28 13:15:44 -04:00
Patrik
5873dfb731 fix(ui): incorrect conditions in WhereBuilder (#5503)
* fix: relationship field tests e2e

* chore: adds POLL_TOPASS_TIMEOUT to relationship field tests

* chore: adds comments to relationship test waits
2024-03-28 13:14:00 -04:00
James
934abec88c feat: simplifies fetching of docs for drawer and edit views 2024-03-28 12:14:12 -04:00
Jarrod Flesch
c1d654c4ce fix(tests): passing tabs test in fields suite (#5463) 2024-03-28 11:28:37 -04:00
Paul
d64b12b14c fix: breadcrumbs on live preview tab (#5478) 2024-03-28 09:32:21 -03:00
Alessio Gravili
3f4ab5f95e fix: undefined error when creating new row without subFieldState present (#5502) 2024-03-27 17:13:06 -04:00
Jacob Fletcher
ee1e94be96 fix(ui): sets proper id on publish button and updates custom button semantics in field map (#5501) 2024-03-27 17:09:16 -04:00
Alessio Gravili
1e9999a8d3 fix: shallow-copy new form state one level deeper, fixes conditions (#5499) 2024-03-27 16:51:13 -04:00
Dan Ribbens
460ca99fe1 chore: alpha fix test defaultvalues (#5500) 2024-03-27 16:47:22 -04:00
Jacob Fletcher
7fdf9b7012 fix(ui): threads initial data through document info provider (#5498) 2024-03-27 16:39:00 -04:00
Alessio Gravili
c16b869fea Merge pull request #5491 from payloadcms/temp24
fix: form-state issues
2024-03-27 16:28:26 -04:00
Alessio Gravili
8deb19e3ac chore: replace incorrect usage of saveDocAndAssert helpers 2024-03-27 16:17:46 -04:00
Alessio Gravili
e6d4445a8a chore: fix build 2024-03-27 16:03:23 -04:00
Jacob Fletcher
0f0da809da Merge pull request #5497 from payloadcms/fix/table-cols
Fix/table cols
2024-03-27 16:00:06 -04:00
Alessio Gravili
db8e805a96 fix: improve error path merging from server, make sure no new or removed rows/values coming from the server are being considered outside addFieldRow 2024-03-27 15:55:19 -04:00
Jacob Fletcher
a882cc7e8e fix(ui): properly handles column selector labels 2024-03-27 15:45:26 -04:00
Elliot DeNolf
acede26aa6 fix: imports from exports dir (#5496)
* chore: remove all internal importing from exports directory

* chore: more package.json main values to src/index.ts
2024-03-27 15:39:58 -04:00
Paul
9330919be8 fix: issue of losing locale when switching tabs between API and edit views (#5494)
* fix: issue of losing locale when switching tabs between API and edit views
2024-03-27 16:20:04 -03:00
Jacob Fletcher
91b4e91e9c fix(ui): filters fields and unfurls subfields for table columns 2024-03-27 13:00:54 -04:00
Alessio Gravili
08ff286f9a chore: e2e: replace flaky manual save actions with non-flaky saveDocAndAssert helper 2024-03-27 12:18:17 -04:00
Jacob Fletcher
bc9fe2f4f9 fix(ui): disables bulk edit for default id fields 2024-03-27 12:13:29 -04:00
Alessio Gravili
e99b168bd6 ci: do not retry failing tests - we shouldn't have flaky tests in the first place 2024-03-27 12:03:38 -04:00
Alessio Gravili
a7afb1f680 chore: enable all lexical e2e tests 2024-03-27 11:54:12 -04:00
Alessio Gravili
6f3934f2e5 fix: server form-state request wasn't triggered on first onChange 2024-03-27 11:44:50 -04:00
Alessio Gravili
79da297add fix: form state not replaced if server has different data for rows 2024-03-27 11:44:02 -04:00
Alessio Gravili
6664535ab9 fix: form onChange using out-of-date old fields state 2024-03-27 11:43:35 -04:00
Alessio Gravili
80d290e178 fix: server-generated ID for base ID field is overriden on the client 2024-03-27 11:42:08 -04:00
Alessio Gravili
d144af6d8e fix: baseIDField not included in form states 2024-03-27 11:41:21 -04:00
447 changed files with 7298 additions and 5599 deletions

View File

@@ -79,159 +79,15 @@ jobs:
path: ./*
key: ${{ github.sha }}-${{ github.run_number }}
plugins-build:
needs: changes
if: ${{ needs.changes.outputs.needs_build == 'true' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 25
- name: Use Node.js 18
uses: actions/setup-node@v4
with:
node-version: 18
- name: Install pnpm
uses: pnpm/action-setup@v3
with:
version: 8
run_install: false
- name: Get pnpm store directory
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- uses: actions/cache@v4
name: Setup pnpm cache
with:
path: ${{ env.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
- run: pnpm install
- run: pnpm run build:plugins
tests:
runs-on: ubuntu-latest
needs: core-build
strategy:
fail-fast: false
matrix:
database:
- mongodb
- postgres
- postgres-custom-schema
- postgres-uuid
- supabase
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: payloadtests
AWS_ENDPOINT_URL: http://127.0.0.1:4566
AWS_ACCESS_KEY_ID: localstack
AWS_SECRET_ACCESS_KEY: localstack
AWS_REGION: us-east-1
steps:
- name: Use Node.js 18
uses: actions/setup-node@v4
with:
node-version: 18
- name: Install pnpm
uses: pnpm/action-setup@v3
with:
version: 8
run_install: false
- name: Restore build
uses: actions/cache@v4
with:
path: ./*
key: ${{ github.sha }}-${{ github.run_number }}
- name: Start LocalStack
run: pnpm docker:start
- name: Start PostgreSQL
uses: CasperWA/postgresql-action@v1.2
with:
postgresql version: '14' # See https://hub.docker.com/_/postgres for available versions
postgresql db: ${{ env.POSTGRES_DB }}
postgresql user: ${{ env.POSTGRES_USER }}
postgresql password: ${{ env.POSTGRES_PASSWORD }}
if: startsWith(matrix.database, 'postgres')
- name: Install Supabase CLI
uses: supabase/setup-cli@v1
with:
version: latest
if: matrix.database == 'supabase'
- name: Initialize Supabase
run: |
supabase init
supabase start
if: matrix.database == 'supabase'
- name: Wait for PostgreSQL
run: sleep 30
if: startsWith(matrix.database, 'postgres')
- name: Configure PostgreSQL
run: |
psql "postgresql://$POSTGRES_USER:$POSTGRES_PASSWORD@localhost:5432/$POSTGRES_DB" -c "CREATE ROLE runner SUPERUSER LOGIN;"
psql "postgresql://$POSTGRES_USER:$POSTGRES_PASSWORD@localhost:5432/$POSTGRES_DB" -c "SELECT version();"
echo "POSTGRES_URL=postgresql://$POSTGRES_USER:$POSTGRES_PASSWORD@localhost:5432/$POSTGRES_DB" >> $GITHUB_ENV
if: startsWith(matrix.database, 'postgres')
- name: Configure PostgreSQL with custom schema
run: |
psql "postgresql://$POSTGRES_USER:$POSTGRES_PASSWORD@localhost:5432/$POSTGRES_DB" -c "CREATE SCHEMA custom;"
if: matrix.database == 'postgres-custom-schema'
- name: Configure Supabase
run: |
echo "POSTGRES_URL=postgresql://postgres:postgres@127.0.0.1:54322/postgres" >> $GITHUB_ENV
if: matrix.database == 'supabase'
- name: Integration Tests
run: pnpm test:int --testPathIgnorePatterns=test/fields # Ignore fields tests until reworked
env:
NODE_OPTIONS: --max-old-space-size=8096
PAYLOAD_DATABASE: ${{ matrix.database }}
POSTGRES_URL: ${{ env.POSTGRES_URL }}
tests-e2e:
runs-on: ubuntu-latest
needs: core-build
strategy:
fail-fast: false
matrix:
# find test -type f -name 'e2e.spec.ts' | sort | xargs dirname | xargs -I {} basename {}
suite:
- _community
- access-control
# - admin
- auth
# - field-error-states
# - fields-relationship
# - fields
- fields/lexical
- live-preview
# - localization
# - plugin-nested-docs
# - plugin-seo
# - refresh-permissions
# - uploads
# - versions
# Add other suites as needed
steps:
- name: Use Node.js 18
uses: actions/setup-node@v4
@@ -251,120 +107,50 @@ jobs:
key: ${{ github.sha }}-${{ github.run_number }}
- name: Install Playwright
run: pnpm exec playwright install
run: pnpm exec playwright install --with-deps
- name: E2E Tests
uses: nick-fields/retry@v3
with:
retry_on: error
max_attempts: 2
timeout_minutes: 15
command: pnpm test:e2e ${{ matrix.suite }}
run: pnpm test:e2e ${{ matrix.suite }}
- uses: actions/upload-artifact@v4
if: always()
with:
name: test-results
name: test-results-${{ matrix.suite }}
path: test/test-results/
retention-days: 1
tests-type-generation:
if: false # This should be replaced with gen on a real Payload project
runs-on: ubuntu-latest
needs: core-build
steps:
- name: Use Node.js 18
uses: actions/setup-node@v4
with:
node-version: 18
- name: Install pnpm
uses: pnpm/action-setup@v3
with:
version: 8
run_install: false
- name: Restore build
uses: actions/cache@v4
with:
path: ./*
key: ${{ github.sha }}-${{ github.run_number }}
- name: Generate Payload Types
run: pnpm dev:generate-types fields
- name: Generate GraphQL schema file
run: pnpm dev:generate-graphql-schema graphql-schema-gen
plugins:
runs-on: ubuntu-latest
needs: core-build
strategy:
fail-fast: false
matrix:
pkg:
- create-payload-app
- plugin-cloud
- plugin-cloud-storage
- plugin-form-builder
- plugin-nested-docs
- plugin-search
- plugin-sentry
- plugin-seo
steps:
- name: Use Node.js 18
uses: actions/setup-node@v4
with:
node-version: 18
- name: Install pnpm
uses: pnpm/action-setup@v3
with:
version: 8
run_install: false
- name: Restore build
uses: actions/cache@v4
with:
path: ./*
key: ${{ github.sha }}-${{ github.run_number }}
- name: Build ${{ matrix.pkg }}
run: pnpm turbo run build --filter=${{ matrix.pkg }}
- name: Test ${{ matrix.pkg }}
run: pnpm --filter ${{ matrix.pkg }} run test
templates:
needs: changes
if: false # Disable until templates are updated for 3.0
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
template: [blank, website, ecommerce]
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 25
- name: Use Node.js 18
uses: actions/setup-node@v4
with:
node-version: 18
- name: Start MongoDB
uses: supercharge/mongodb-github-action@1.10.0
with:
mongodb-version: 6.0
- name: Build Template
- name: Process and Upload Trace Files
if: always()
run: |
cd templates/${{ matrix.template }}
cp .env.example .env
yarn install
yarn build
yarn generate:types
count=0
for folder in test/test-results/*; do
if [ -d "$folder" ] && [ -f "$folder/trace.zip" ]; then
((count++))
if [ "$count" -le 5 ]; then
test_name=$(basename "$folder")
echo "Processing $test_name"
new_trace_name="trace-${{ matrix.suite }}-$count.zip"
cp "$folder/trace.zip" "$new_trace_name"
echo "$test_name: $new_trace_name" >> map.txt
else
break
fi
fi
done
cat map.txt
shell: bash
- uses: actions/upload-artifact@v4
with:
name: trace-${{ matrix.suite }}-1
path: trace-${{ matrix.suite }}-1.zip
- name: Display Mapped Names and Links
run: |
while IFS= read -r line; do
IFS=': ' read -r test_name trace_name <<< "$line"
echo "${test_name}: https://github.com/payloadcms/payload/actions/runs/${GITHUB_RUN_ID}/artifacts/${trace_name}"
done < map.txt
shell: bash

4
.gitignore vendored
View File

@@ -3,6 +3,7 @@ package-lock.json
dist
/.idea/*
!/.idea/runConfigurations
!/.idea/payload.iml
test-results
.devcontainer
@@ -15,6 +16,7 @@ test-results
# Ignore test directory media folder/files
/media
test/media
/versions
# Created by https://www.toptal.com/developers/gitignore/api/node,macos,windows,webstorm,sublimetext,visualstudiocode
@@ -288,4 +290,4 @@ $RECYCLE.BIN/
# End of https://www.toptal.com/developers/gitignore/api/node,macos,windows,webstorm,sublimetext,visualstudiocode
/build
.swc
.swc

54
.idea/payload.iml generated Normal file
View File

@@ -0,0 +1,54 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
<excludeFolder url="file://$MODULE_DIR$/temp" />
<excludeFolder url="file://$MODULE_DIR$/tmp" />
<excludeFolder url="file://$MODULE_DIR$/packages/payload/.turbo" />
<excludeFolder url="file://$MODULE_DIR$/packages/payload/components" />
<excludeFolder url="file://$MODULE_DIR$/packages/payload/dist" />
<excludeFolder url="file://$MODULE_DIR$/.swc" />
<excludeFolder url="file://$MODULE_DIR$/.turbo" />
<excludeFolder url="file://$MODULE_DIR$/examples" />
<excludeFolder url="file://$MODULE_DIR$/media" />
<excludeFolder url="file://$MODULE_DIR$/packages/create-payload-app/.turbo" />
<excludeFolder url="file://$MODULE_DIR$/packages/create-payload-app/dist" />
<excludeFolder url="file://$MODULE_DIR$/packages/db-mongodb/.turbo" />
<excludeFolder url="file://$MODULE_DIR$/packages/db-mongodb/dist" />
<excludeFolder url="file://$MODULE_DIR$/packages/db-postgres/.turbo" />
<excludeFolder url="file://$MODULE_DIR$/packages/db-postgres/dist" />
<excludeFolder url="file://$MODULE_DIR$/packages/graphql/.turbo" />
<excludeFolder url="file://$MODULE_DIR$/packages/graphql/dist" />
<excludeFolder url="file://$MODULE_DIR$/packages/live-preview-react/.turbo" />
<excludeFolder url="file://$MODULE_DIR$/packages/live-preview-react/dist" />
<excludeFolder url="file://$MODULE_DIR$/packages/live-preview/.turbo" />
<excludeFolder url="file://$MODULE_DIR$/packages/live-preview/dist" />
<excludeFolder url="file://$MODULE_DIR$/packages/next/.swc" />
<excludeFolder url="file://$MODULE_DIR$/packages/next/.turbo" />
<excludeFolder url="file://$MODULE_DIR$/packages/payload/fields" />
<excludeFolder url="file://$MODULE_DIR$/packages/plugin-cloud-storage/.turbo" />
<excludeFolder url="file://$MODULE_DIR$/packages/plugin-cloud-storage/dist" />
<excludeFolder url="file://$MODULE_DIR$/packages/plugin-cloud/.turbo" />
<excludeFolder url="file://$MODULE_DIR$/packages/plugin-cloud/dist" />
<excludeFolder url="file://$MODULE_DIR$/packages/plugin-form-builder/.turbo" />
<excludeFolder url="file://$MODULE_DIR$/packages/plugin-nested-docs/.turbo" />
<excludeFolder url="file://$MODULE_DIR$/packages/plugin-nested-docs/dist" />
<excludeFolder url="file://$MODULE_DIR$/packages/plugin-redirects/.turbo" />
<excludeFolder url="file://$MODULE_DIR$/packages/plugin-redirects/dist" />
<excludeFolder url="file://$MODULE_DIR$/packages/plugin-search/.turbo" />
<excludeFolder url="file://$MODULE_DIR$/packages/plugin-search/dist" />
<excludeFolder url="file://$MODULE_DIR$/packages/plugin-sentry/.turbo" />
<excludeFolder url="file://$MODULE_DIR$/packages/plugin-seo/.turbo" />
<excludeFolder url="file://$MODULE_DIR$/packages/plugin-seo/dist" />
<excludeFolder url="file://$MODULE_DIR$/packages/plugin-stripe/.turbo" />
<excludeFolder url="file://$MODULE_DIR$/packages/richtext-lexical/.turbo" />
<excludeFolder url="file://$MODULE_DIR$/packages/richtext-lexical/dist" />
<excludeFolder url="file://$MODULE_DIR$/templates" />
<excludeFolder url="file://$MODULE_DIR$/test/.swc" />
<excludeFolder url="file://$MODULE_DIR$/versions" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

20
.vscode/launch.json vendored
View File

@@ -10,19 +10,26 @@
"cwd": "${workspaceFolder}"
},
{
"command": "node --no-deprecation test/dev.js fields",
"command": "node --no-deprecation test/dev.js _community",
"cwd": "${workspaceFolder}",
"name": "Run Dev Community",
"request": "launch",
"type": "node-terminal"
},
{
"command": "pnpm run dev live-preview -- --no-turbo",
"command": "node --no-deprecation test/dev.js live-preview",
"cwd": "${workspaceFolder}",
"name": "Run Dev Live Preview",
"request": "launch",
"type": "node-terminal"
},
{
"command": "node --no-deprecation test/dev.js admin",
"cwd": "${workspaceFolder}",
"name": "Run Dev Admin",
"request": "launch",
"type": "node-terminal"
},
{
"command": "pnpm run dev plugin-cloud-storage",
"cwd": "${workspaceFolder}",
@@ -34,18 +41,21 @@
}
},
{
"command": "pnpm run dev fields",
"command": "node --no-deprecation test/dev.js fields",
"cwd": "${workspaceFolder}",
"name": "Run Dev Fields",
"request": "launch",
"type": "node-terminal"
},
{
"command": "pnpm run dev:postgres versions",
"command": "node --no-deprecation test/dev.js versions",
"cwd": "${workspaceFolder}",
"name": "Run Dev Postgres",
"request": "launch",
"type": "node-terminal"
"type": "node-terminal",
"env": {
"PAYLOAD_DATABASE": "postgres"
}
},
{
"command": "pnpm run dev versions",

View File

@@ -1,7 +1,9 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
import type { Metadata } from 'next'
import config from '@payload-config'
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import { NotFoundView } from '@payloadcms/next/views/NotFound/index.js'
import { NotFoundPage, generatePageMetadata } from '@payloadcms/next/views/NotFound/index.js'
type Args = {
params: {
@@ -12,6 +14,9 @@ type Args = {
}
}
const NotFound = ({ params, searchParams }: Args) => NotFoundView({ config, params, searchParams })
export const generateMetadata = ({ params, searchParams }: Args): Promise<Metadata> =>
generatePageMetadata({ config, params, searchParams })
const NotFound = ({ params, searchParams }: Args) => NotFoundPage({ config, params, searchParams })
export default NotFound

View File

@@ -0,0 +1,8 @@
#custom-css {
font-family: monospace;
background-image: url('/placeholder.png');
}
#custom-css::after {
content: 'custom-css';
}

View File

@@ -221,9 +221,13 @@ user-friendly.
### beforeDuplicate
The `beforeDuplicate` field hook is only called when duplicating a document. It may be used when documents having the
exact same properties may cause issue. This gives you a way to avoid duplicate names on `unique`, `required` fields or
to unset values by returning `null`. This is called immediately after `defaultValue` and before validation occurs.
The `beforeDuplicate` field hook is called on each locale (when using localization), when duplicating a document. It may be used when documents having the
exact same properties may cause issue. This gives you a way to avoid duplicate names on `unique`, `required` fields or when external systems expect non-repeating values on documents.
This hook gets called after `beforeChange` hooks are called and before the document is saved to the database.
By Default, unique and required text fields Payload will append "- Copy" to the original document value. The default is not added if your field has its own, you must return non-unique values from your beforeDuplicate hook to avoid errors or enable the `disableDuplicate` option on the collection.
Here is an example of a number field with a hook that increments the number to avoid unique constraint errors when duplicating a document:
```ts
import { Field } from 'payload/types'

View File

@@ -173,7 +173,7 @@ Next, take a look at the [features we've already built](https://github.com/paylo
Lexical saves data in JSON, but can also generate its HTML representation via two main methods:
1. **Outputting HTML from the Collection:** Create a new field in your collection to convert saved JSON content to HTML. Payload generates and outputs the HTML for use in your frontend.
2. **Generating HTML on the Frontend:** Convert JSON to HTML on-demand, either in your frontend or elsewhere.
2. **Generating HTML on any server** Convert JSON to HTML on-demand on the server.
The editor comes with built-in HTML serializers, simplifying the process of converting JSON to HTML.
@@ -207,7 +207,7 @@ const Pages: CollectionConfig = {
The `lexicalHTML()` function creates a new field that automatically converts the referenced lexical richText field into HTML through an afterRead hook.
#### Generating HTML in the Frontend:
#### Generating HTML anywhere on the server:
If you wish to convert JSON to HTML ad-hoc, use this code snippet:

View File

@@ -167,7 +167,9 @@ Specifying custom `Type`s let you extend your custom elements by adding addition
`collections/ExampleCollection.ts`
```ts
import { CollectionConfig } from 'payload/types'
import type { CollectionConfig } from 'payload/types'
import { slateEditor } from '@payloadcms/richtext-slate'
export const ExampleCollection: CollectionConfig = {
slug: 'example-collection',
@@ -181,57 +183,59 @@ export const ExampleCollection: CollectionConfig = {
},
],
required: true,
admin: {
elements: [
'h2',
'h3',
'h4',
'link',
'blockquote',
{
name: 'cta',
Button: CustomCallToActionButton,
Element: CustomCallToActionElement,
plugins: [
// any plugins that are required by this element go here
],
},
],
leaves: [
'bold',
'italic',
{
name: 'highlight',
Button: CustomHighlightButton,
Leaf: CustomHighlightLeaf,
plugins: [
// any plugins that are required by this leaf go here
],
},
],
link: {
// Inject your own fields into the Link element
fields: [
editor: slateEditor({
admin: {
elements: [
'h2',
'h3',
'h4',
'link',
'blockquote',
{
name: 'rel',
label: 'Rel Attribute',
type: 'select',
hasMany: true,
options: ['noopener', 'noreferrer', 'nofollow'],
},
],
},
upload: {
collections: {
media: {
fields: [
// any fields that you would like to save
// on an upload element in the `media` collection
name: 'cta',
Button: CustomCallToActionButton,
Element: CustomCallToActionElement,
plugins: [
// any plugins that are required by this element go here
],
},
],
leaves: [
'bold',
'italic',
{
name: 'highlight',
Button: CustomHighlightButton,
Leaf: CustomHighlightLeaf,
plugins: [
// any plugins that are required by this leaf go here
],
},
],
link: {
// Inject your own fields into the Link element
fields: [
{
name: 'rel',
label: 'Rel Attribute',
type: 'select',
hasMany: true,
options: ['noopener', 'noreferrer', 'nofollow'],
},
],
},
upload: {
collections: {
media: {
fields: [
// any fields that you would like to save
// on an upload element in the `media` collection
],
},
},
},
},
},
}),
},
],
}

View File

@@ -1,5 +1,5 @@
/** @type {import('jest').Config} */
const customJestConfig = {
const baseJestConfig = {
extensionsToTreatAsEsm: ['.ts', '.tsx'],
setupFilesAfterEnv: ['<rootDir>/test/jest.setup.ts'],
moduleNameMapper: {
@@ -8,9 +8,8 @@ const customJestConfig = {
'<rootDir>/test/helpers/mocks/fileMock.js',
'^(\\.{1,2}/.*)\\.js$': '$1',
},
reporters: ['default', ['github-actions', { silent: false }], 'summary'],
testEnvironment: 'node',
testMatch: ['<rootDir>/packages/payload/src/**/*.spec.ts', '<rootDir>/test/**/*int.spec.ts'],
testMatch: ['<rootDir>/packages/*/src/**/*.spec.ts'],
testTimeout: 90000,
transform: {
'^.+\\.(t|j)sx?$': ['@swc/jest'],
@@ -19,7 +18,7 @@ const customJestConfig = {
}
if (process.env.CI) {
customJestConfig.reporters = [['github-actions', { silent: false }], 'summary']
baseJestConfig.reporters = [['github-actions', { silent: false }], 'summary']
}
export default customJestConfig
export default baseJestConfig

View File

@@ -46,10 +46,10 @@
"devsafe": "rimraf .next && pnpm dev",
"dev:generate-graphql-schema": "cross-env NODE_OPTIONS=--no-deprecation tsx ./test/generateGraphQLSchema.ts",
"dev:generate-types": "cross-env NODE_OPTIONS=--no-deprecation tsx ./test/generateTypes.ts",
"dev:postgres": "pnpm --filter payload run dev:postgres",
"dev:postgres": "cross-env NODE_OPTIONS=--no-deprecation PAYLOAD_DATABASE=postgres node ./test/dev.js",
"docker:restart": "pnpm docker:stop --remove-orphans && pnpm docker:start",
"docker:start": "docker-compose -f packages/plugin-cloud-storage/docker-compose.yml up -d",
"docker:stop": "docker-compose -f packages/plugin-cloud-storage/docker-compose.yml down",
"docker:start": "docker compose -f packages/plugin-cloud-storage/docker-compose.yml up -d",
"docker:stop": "docker compose -f packages/plugin-cloud-storage/docker-compose.yml down",
"fix": "eslint \"packages/**/*.ts\" --fix",
"generate:types": "PAYLOAD_CONFIG_PATH=./test/_community/config.ts node --no-deprecation ./packages/payload/bin.js generate:types",
"lint": "eslint \"packages/**/*.ts\"",
@@ -57,7 +57,6 @@
"prepare": "husky install",
"pretest": "pnpm build",
"reinstall": "pnpm clean:all && pnpm install",
"script:list-packages": "tsx ./scripts/list-packages.ts",
"script:pack": "tsx scripts/pack-all-to-dest.ts",
"release:alpha": "tsx ./scripts/release.ts --bump prerelease --tag alpha",
"release:beta": "tsx ./scripts/release.ts --bump prerelease --tag beta",
@@ -66,8 +65,9 @@
"test:e2e": "cross-env NODE_OPTIONS=--no-deprecation NODE_NO_WARNINGS=1 tsx ./test/runE2E.ts",
"test:e2e:debug": "cross-env NODE_OPTIONS=--no-deprecation NODE_NO_WARNINGS=1 PWDEBUG=1 DISABLE_LOGGING=true playwright test",
"test:e2e:headed": "cross-env NODE_OPTIONS=--no-deprecation NODE_NO_WARNINGS=1 DISABLE_LOGGING=true playwright test --headed",
"test:int:postgres": "cross-env NODE_OPTIONS=--no-deprecation NODE_NO_WARNINGS=1 PAYLOAD_DATABASE=postgres DISABLE_LOGGING=true jest --forceExit --detectOpenHandles",
"test:int": "cross-env NODE_OPTIONS=--no-deprecation NODE_NO_WARNINGS=1 DISABLE_LOGGING=true jest --forceExit --detectOpenHandles",
"test:int:postgres": "cross-env NODE_OPTIONS=--no-deprecation NODE_NO_WARNINGS=1 PAYLOAD_DATABASE=postgres DISABLE_LOGGING=true jest --forceExit --detectOpenHandles --config=test/jest.config.js --runInBand",
"test:int": "cross-env NODE_OPTIONS=--no-deprecation NODE_NO_WARNINGS=1 DISABLE_LOGGING=true jest --forceExit --detectOpenHandles --config=test/jest.config.js --runInBand",
"test:unit": "cross-env NODE_OPTIONS=--no-deprecation NODE_NO_WARNINGS=1 DISABLE_LOGGING=true jest --forceExit --detectOpenHandles --config=jest.config.js --runInBand",
"translateNewKeys": "pnpm --filter payload run translateNewKeys"
},
"devDependencies": {
@@ -131,6 +131,7 @@
"node-mocks-http": "^1.14.1",
"nodemon": "3.0.3",
"open": "^10.1.0",
"p-map": "^7.0.2",
"pino": "8.15.0",
"pino-pretty": "10.2.0",
"playwright": "^1.42.1",
@@ -145,7 +146,7 @@
"semver": "^7.5.4",
"sharp": "0.32.6",
"shelljs": "0.8.5",
"simple-git": "^3.20.0",
"simple-git": "^3.24.0",
"slash": "3.0.0",
"slate": "0.91.4",
"swc-plugin-transform-remove-imports": "^1.12.1",
@@ -153,14 +154,13 @@
"tempy": "^1.0.1",
"ts-node": "10.9.1",
"tsx": "^4.7.1",
"turbo": "^1.13.0",
"turbo": "^1.13.2",
"typescript": "5.4.2",
"uuid": "^9.0.1",
"yocto-queue": "^1.0.0"
},
"peerDependencies": {
"react": "18.2.0",
"react-router-dom": "5.3.4"
"react": "18.2.0"
},
"engines": {
"node": ">=18.17.0",

View File

@@ -1,11 +1,32 @@
import baseConfig from '../../jest.config.js'
// import baseConfig from '../../jest.config.js'
/** @type {import('@jest/types').Config} */
// /** @type {import('@jest/types').Config} */
// const customJestConfig = {
// ...baseConfig,
// setupFilesAfterEnv: null,
// testMatch: ['**/src/**/?(*.)+(spec|test|it-test).[tj]s?(x)'],
// testTimeout: 20000,
// }
// export default customJestConfig
/** @type {import('jest').Config} */
const customJestConfig = {
...baseConfig,
setupFilesAfterEnv: null,
testMatch: ['**/src/**/?(*.)+(spec|test|it-test).[tj]s?(x)'],
testTimeout: 20000,
extensionsToTreatAsEsm: ['.ts', '.tsx'],
// setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
moduleNameMapper: {
'\\.(css|scss)$': '<rootDir>/helpers/mocks/emptyModule.js',
'\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$':
'<rootDir>/test/helpers/mocks/fileMock.js',
'^(\\.{1,2}/.*)\\.js$': '$1',
},
testEnvironment: 'node',
testMatch: ['<rootDir>/**/*spec.ts'],
testTimeout: 90000,
transform: {
'^.+\\.(t|j)sx?$': ['@swc/jest'],
},
verbose: true,
}
export default customJestConfig

View File

@@ -3,13 +3,19 @@
"version": "1.0.0",
"license": "MIT",
"type": "module",
"homepage": "https://payloadcms.com",
"bin": {
"create-payload-app": "bin/cli.js"
},
"repository": {
"type": "git",
"url": "https://github.com/payloadcms/payload.git",
"directory": "packages/create-payload-app"
},
"scripts": {
"build": "pnpm copyfiles && pnpm typecheck && pnpm build:swc",
"build": "pnpm pack-template-files && pnpm typecheck && pnpm build:swc",
"typecheck": "tsc",
"copyfiles": "copyfiles -u 2 \"../../app/(payload)/**\" \"dist\"",
"pack-template-files": "tsx src/scripts/pack-template-files.ts",
"build:swc": "swc ./src -d ./dist --config-file .swcrc",
"clean": "rimraf {dist,*.tsbuildinfo}",
"test": "jest",
@@ -21,6 +27,7 @@
"bin"
],
"dependencies": {
"@clack/prompts": "^0.7.0",
"@sindresorhus/slugify": "^1.1.0",
"arg": "^5.0.0",
"chalk": "^4.1.0",
@@ -30,12 +37,9 @@
"detect-package-manager": "^3.0.1",
"esprima": "^4.0.1",
"execa": "^5.0.0",
"figures": "^3.2.0",
"figures": "^6.1.0",
"fs-extra": "^9.0.1",
"globby": "11.1.0",
"handlebars": "^4.7.7",
"ora": "^5.1.0",
"prompts": "^2.4.2",
"terminal-link": "^2.1.1"
},
"devDependencies": {
@@ -44,8 +48,7 @@
"@types/esprima": "^4.0.6",
"@types/fs-extra": "^9.0.12",
"@types/jest": "^27.0.3",
"@types/node": "^16.6.2",
"@types/prompts": "^2.4.1"
"@types/node": "^16.6.2"
},
"exports": {
"./commands": {

View File

@@ -9,19 +9,27 @@ import { dbReplacements } from './packages.js'
/** Update payload config with necessary imports and adapters */
export async function configurePayloadConfig(args: {
dbDetails: DbDetails | undefined
projectDir: string
projectDirOrConfigPath: { payloadConfigPath: string } | { projectDir: string }
}): Promise<void> {
if (!args.dbDetails) {
return
}
try {
const payloadConfigPath = (
await globby('**/payload.config.ts', { absolute: true, cwd: args.projectDir })
)?.[0]
let payloadConfigPath: string | undefined
if (!('payloadConfigPath' in args.projectDirOrConfigPath)) {
payloadConfigPath = (
await globby('**/payload.config.ts', {
absolute: true,
cwd: args.projectDirOrConfigPath.projectDir,
})
)?.[0]
} else {
payloadConfigPath = args.projectDirOrConfigPath.payloadConfigPath
}
if (!payloadConfigPath) {
warning('Unable to update payload.config.ts with plugins')
warning('Unable to update payload.config.ts with plugins. Could not find payload.config.ts.')
return
}
@@ -59,6 +67,8 @@ export async function configurePayloadConfig(args: {
fse.writeFileSync(payloadConfigPath, configLines.join('\n'))
} catch (err: unknown) {
warning('Unable to update payload.config.ts with plugins')
warning(
`Unable to update payload.config.ts with plugins: ${err instanceof Error ? err.message : ''}`,
)
}
}

View File

@@ -5,6 +5,7 @@ import { createProject } from './create-project.js'
import { fileURLToPath } from 'node:url'
import { dbReplacements } from './packages.js'
import { getValidTemplates } from './templates.js'
import globby from 'globby'
const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
@@ -104,12 +105,17 @@ describe('createProject', () => {
Object.keys(packageJson.dependencies).filter((n) => n.startsWith('@payloadcms/db-')),
).toHaveLength(1)
let payloadConfigPath = path.resolve(projectDir, 'payload.config.ts')
const payloadConfigPath = (
await globby('**/payload.config.ts', {
absolute: true,
cwd: projectDir,
})
)?.[0]
// Website and ecommerce templates have payload.config.ts in src/payload
if (!fse.existsSync(payloadConfigPath)) {
payloadConfigPath = path.resolve(projectDir, 'src/payload/payload.config.ts')
if (!payloadConfigPath) {
throw new Error(`Could not find payload.config.ts inside ${projectDir}`)
}
const content = fse.readFileSync(payloadConfigPath, 'utf-8')
// Check payload.config.ts
@@ -122,9 +128,4 @@ describe('createProject', () => {
})
})
})
describe('Templates', () => {
it.todo('Verify that all templates are valid')
// Loop through all templates.ts that should have replacement comments, and verify that they are present
})
})

View File

@@ -1,14 +1,14 @@
import * as p from '@clack/prompts'
import chalk from 'chalk'
import degit from 'degit'
import execa from 'execa'
import fse from 'fs-extra'
import { fileURLToPath } from 'node:url'
import ora from 'ora'
import path from 'path'
import type { CliArgs, DbDetails, PackageManager, ProjectTemplate } from '../types.js'
import { debug, error, success, warning } from '../utils/log.js'
import { debug, error, warning } from '../utils/log.js'
import { configurePayloadConfig } from './configure-payload-config.js'
const filename = fileURLToPath(import.meta.url)
@@ -60,14 +60,12 @@ export async function createProject(args: {
const { cliArgs, dbDetails, packageManager, projectDir, projectName, template } = args
if (cliArgs['--dry-run']) {
console.log(`\n Dry run: Creating project in ${chalk.green(projectDir)}\n`)
debug(`Dry run: Creating project in ${chalk.green(projectDir)}`)
return
}
await createOrFindProjectDir(projectDir)
console.log(`\n Creating project in ${chalk.green(projectDir)}\n`)
if (cliArgs['--local-template']) {
// Copy template from local path. For development purposes.
const localTemplate = path.resolve(
@@ -86,10 +84,12 @@ export async function createProject(args: {
await emitter.clone(projectDir)
}
const spinner = ora('Checking latest Payload version...').start()
const spinner = p.spinner()
spinner.start('Checking latest Payload version...')
await updatePackageJSON({ projectDir, projectName })
await configurePayloadConfig({ dbDetails, projectDir })
spinner.message('Configuring Payload...')
await configurePayloadConfig({ dbDetails, projectDirOrConfigPath: { projectDir } })
// Remove yarn.lock file. This is only desired in Payload Cloud.
const lockPath = path.resolve(projectDir, 'yarn.lock')
@@ -98,18 +98,15 @@ export async function createProject(args: {
}
if (!cliArgs['--no-deps']) {
spinner.text = 'Installing dependencies...'
spinner.message('Installing dependencies...')
const result = await installDeps({ cliArgs, packageManager, projectDir })
spinner.stop()
spinner.clear()
if (result) {
success('Dependencies installed')
spinner.stop('Successfully installed Payload and dependencies')
} else {
error('Error installing dependencies')
spinner.stop('Error installing dependencies', 1)
}
} else {
spinner.stop()
spinner.clear()
spinner.stop('Dependency installation skipped')
}
}
@@ -124,6 +121,6 @@ export async function updatePackageJSON(args: {
packageObj.name = projectName
await fse.writeJson(packageJsonPath, packageObj, { spaces: 2 })
} catch (err: unknown) {
warning('Unable to update name in package.json')
warning(`Unable to update name in package.json. ${err instanceof Error ? err.message : ''}`)
}
}

View File

@@ -1,13 +1,13 @@
import type { CompilerOptions } from 'typescript'
import chalk from 'chalk'
import * as p from '@clack/prompts'
import { parse, stringify } from 'comment-json'
import execa from 'execa'
import fs from 'fs'
import fse from 'fs-extra'
import globby from 'globby'
import path from 'path'
import { promisify } from 'util'
const readFile = promisify(fs.readFile)
const writeFile = promisify(fs.writeFile)
@@ -16,58 +16,85 @@ const dirname = path.dirname(filename)
import { fileURLToPath } from 'node:url'
import type { CliArgs, PackageManager } from '../types.js'
import type { CliArgs, DbType, PackageManager } from '../types.js'
import { copyRecursiveSync } from '../utils/copy-recursive-sync.js'
import { error, info, debug as origDebug, success, warning } from '../utils/log.js'
import { debug as origDebug, warning } from '../utils/log.js'
import { moveMessage } from '../utils/messages.js'
import { wrapNextConfig } from './wrap-next-config.js'
type InitNextArgs = Pick<CliArgs, '--debug'> & {
dbType: DbType
nextAppDetails?: NextAppDetails
packageManager: PackageManager
projectDir?: string
projectDir: string
useDistFiles?: boolean
}
type InitNextResult = { reason?: string; success: boolean; userAppDir?: string }
type InitNextResult =
| {
isSrcDir: boolean
nextAppDir: string
payloadConfigPath: string
success: true
}
| { isSrcDir: boolean; nextAppDir?: string; reason: string; success: false }
export async function initNext(args: InitNextArgs): Promise<InitNextResult> {
const { packageManager, projectDir } = args
const templateResult = await applyPayloadTemplateFiles(args)
if (!templateResult.success) return templateResult
const { dbType: dbType, packageManager, projectDir } = args
const { success: installSuccess } = await installDeps(projectDir, packageManager)
if (!installSuccess) {
return { ...templateResult, reason: 'Failed to install dependencies', success: false }
const nextAppDetails = args.nextAppDetails || (await getNextAppDetails(projectDir))
const { hasTopLevelLayout, isSrcDir, nextAppDir } =
nextAppDetails || (await getNextAppDetails(projectDir))
if (!nextAppDir) {
return { isSrcDir, reason: `Could not find app directory in ${projectDir}`, success: false }
}
// Create or find payload.config.ts
const createConfigResult = findOrCreatePayloadConfig(projectDir)
if (!createConfigResult.success) {
return { ...templateResult, ...createConfigResult }
if (hasTopLevelLayout) {
// Output directions for user to move all files from app to top-level directory named `(app)`
p.log.warn(moveMessage({ nextAppDir, projectDir }))
return {
isSrcDir,
nextAppDir,
reason: 'Found existing layout.tsx in app directory',
success: false,
}
}
const installSpinner = p.spinner()
installSpinner.start('Installing Payload and dependencies...')
const configurationResult = installAndConfigurePayload({
...args,
nextAppDetails,
useDistFiles: true, // Requires running 'pnpm pack-template-files' in cpa
})
if (configurationResult.success === false) {
installSpinner.stop(configurationResult.reason, 1)
return { ...configurationResult, isSrcDir, success: false }
}
const { success: installSuccess } = await installDeps(projectDir, packageManager, dbType)
if (!installSuccess) {
installSpinner.stop('Failed to install dependencies', 1)
return {
...configurationResult,
isSrcDir,
reason: 'Failed to install dependencies',
success: false,
}
}
// Add `@payload-config` to tsconfig.json `paths`
await addPayloadConfigToTsConfig(projectDir)
// Output directions for user to update next.config.js
const withPayloadMessage = `
${chalk.bold(`Wrap your existing next.config.js with the withPayload function. Here is an example:`)}
import withPayload from '@payloadcms/next/withPayload'
const nextConfig = {
// Your Next.js config here
}
export default withPayload(nextConfig)
`
console.log(withPayloadMessage)
return templateResult
await addPayloadConfigToTsConfig(projectDir, isSrcDir)
installSpinner.stop('Successfully installed Payload and dependencies')
return { ...configurationResult, isSrcDir, nextAppDir, success: true }
}
async function addPayloadConfigToTsConfig(projectDir: string) {
async function addPayloadConfigToTsConfig(projectDir: string, isSrcDir: boolean) {
const tsConfigPath = path.resolve(projectDir, 'tsconfig.json')
const userTsConfigContent = await readFile(tsConfigPath, {
encoding: 'utf8',
@@ -79,51 +106,54 @@ async function addPayloadConfigToTsConfig(projectDir: string) {
userTsConfig.compilerOptions = {}
}
if (!userTsConfig.compilerOptions.paths?.['@payload-config']) {
if (
!userTsConfig.compilerOptions?.paths?.['@payload-config'] &&
userTsConfig.compilerOptions?.paths
) {
userTsConfig.compilerOptions.paths = {
...(userTsConfig.compilerOptions.paths || {}),
'@payload-config': ['./payload.config.ts'],
'@payload-config': [`./${isSrcDir ? 'src/' : ''}payload.config.ts`],
}
await writeFile(tsConfigPath, stringify(userTsConfig, null, 2), { encoding: 'utf8' })
}
}
async function applyPayloadTemplateFiles(args: InitNextArgs): Promise<InitNextResult> {
const { '--debug': debug, projectDir, useDistFiles } = args
function installAndConfigurePayload(
args: InitNextArgs & { nextAppDetails: NextAppDetails; useDistFiles?: boolean },
):
| { payloadConfigPath: string; success: true }
| { payloadConfigPath?: string; reason: string; success: false } {
const {
'--debug': debug,
nextAppDetails: { isSrcDir, nextAppDir, nextConfigPath } = {},
projectDir,
useDistFiles,
} = args
info('Initializing Payload app in Next.js project', 1)
if (!nextAppDir || !nextConfigPath) {
return {
reason: 'Could not find app directory or next.config.js',
success: false,
}
}
const logDebug = (message: string) => {
if (debug) origDebug(message)
}
if (!fs.existsSync(projectDir)) {
return { reason: `Could not find specified project directory at ${projectDir}`, success: false }
}
// Next.js configs can be next.config.js, next.config.mjs, etc.
const foundConfig = (await globby('next.config.*js', { absolute: true, cwd: projectDir }))?.[0]
if (!foundConfig) {
throw new Error(`No next.config.js found at ${projectDir}`)
}
const nextConfigPath = path.resolve(projectDir, foundConfig)
if (!fs.existsSync(nextConfigPath)) {
return {
reason: `No next.config.js found at ${nextConfigPath}. Ensure you are in a Next.js project directory.`,
reason: `Could not find specified project directory at ${projectDir}`,
success: false,
}
} else {
if (debug) logDebug(`Found Next config at ${nextConfigPath}`)
}
const templateFilesPath =
dirname.endsWith('dist') || useDistFiles
? path.resolve(dirname, '../..', 'dist/app')
: path.resolve(dirname, '../../../../app')
? path.resolve(dirname, '../..', 'dist/template')
: path.resolve(dirname, '../../../../templates/blank-3.0')
if (debug) logDebug(`Using template files from: ${templateFilesPath}`)
logDebug(`Using template files from: ${templateFilesPath}`)
if (!fs.existsSync(templateFilesPath)) {
return {
@@ -131,38 +161,40 @@ async function applyPayloadTemplateFiles(args: InitNextArgs): Promise<InitNextRe
success: false,
}
} else {
if (debug) logDebug('Found template source files')
logDebug('Found template source files')
}
// src/app or app
const userAppDir = (
await globby(['**/app'], {
absolute: true,
cwd: projectDir,
onlyDirectories: true,
})
)?.[0]
logDebug(`Copying template files from ${templateFilesPath} to ${nextAppDir}`)
if (!fs.existsSync(userAppDir)) {
return { reason: `Could not find user app directory inside ${projectDir}`, success: false }
} else {
logDebug(`Found user app directory: ${userAppDir}`)
const templateSrcDir = path.resolve(templateFilesPath, isSrcDir ? '' : 'src')
logDebug(`templateSrcDir: ${templateSrcDir}`)
logDebug(`nextAppDir: ${nextAppDir}`)
logDebug(`projectDir: ${projectDir}`)
logDebug(`nextConfigPath: ${nextConfigPath}`)
logDebug(
`isSrcDir: ${isSrcDir}. source: ${templateSrcDir}. dest: ${path.dirname(nextConfigPath)}`,
)
// This is a little clunky and needs to account for isSrcDir
copyRecursiveSync(templateSrcDir, path.dirname(nextConfigPath), debug)
// Wrap next.config.js with withPayload
wrapNextConfig({ nextConfigPath })
return {
payloadConfigPath: path.resolve(nextAppDir, '../payload.config.ts'),
success: true,
}
logDebug(`Copying template files from ${templateFilesPath} to ${userAppDir}`)
copyRecursiveSync(templateFilesPath, userAppDir, debug)
success('Successfully initialized.')
return { success: true, userAppDir }
}
async function installDeps(projectDir: string, packageManager: PackageManager) {
info(`Installing dependencies with ${packageManager}`, 1)
const packagesToInstall = [
'payload',
'@payloadcms/db-mongodb',
'@payloadcms/next',
'@payloadcms/richtext-lexical',
].map((pkg) => `${pkg}@alpha`)
async function installDeps(projectDir: string, packageManager: PackageManager, dbType: DbType) {
const packagesToInstall = ['payload', '@payloadcms/next', '@payloadcms/richtext-lexical'].map(
(pkg) => `${pkg}@alpha`,
)
packagesToInstall.push(`@payloadcms/db-${dbType}@alpha`)
let exitCode = 0
switch (packageManager) {
@@ -186,43 +218,45 @@ async function installDeps(projectDir: string, packageManager: PackageManager) {
}
}
if (exitCode !== 0) {
error(`Failed to install dependencies with ${packageManager}`)
} else {
success(`Successfully installed dependencies`)
}
return { success: exitCode === 0 }
}
function findOrCreatePayloadConfig(projectDir: string) {
const configPath = path.resolve(projectDir, 'payload.config.ts')
if (fs.existsSync(configPath)) {
return { message: 'Found existing payload.config.ts', success: true }
} else {
// Create default config
// TODO: Pull this from templates
const defaultConfig = `import path from "path";
import { mongooseAdapter } from "@payloadcms/db-mongodb"; // database-adapter-import
import { lexicalEditor } from "@payloadcms/richtext-lexical"; // editor-import
import { buildConfig } from "payload/config";
export default buildConfig({
editor: slateEditor({}), // editor-config
collections: [],
secret: "asdfasdf",
typescript: {
outputFile: path.resolve(__dirname, "payload-types.ts"),
},
graphQL: {
schemaOutputFile: path.resolve(__dirname, "generated-schema.graphql"),
},
db: mongooseAdapter({
url: "mongodb://localhost:27017/next-payload-3",
}),
});
`
fse.writeFileSync(configPath, defaultConfig)
return { message: 'Created default payload.config.ts', success: true }
}
type NextAppDetails = {
hasTopLevelLayout: boolean
isSrcDir: boolean
nextAppDir?: string
nextConfigPath?: string
}
export async function getNextAppDetails(projectDir: string): Promise<NextAppDetails> {
const isSrcDir = fs.existsSync(path.resolve(projectDir, 'src'))
const nextConfigPath: string | undefined = (
await globby('next.config.*js', { absolute: true, cwd: projectDir })
)?.[0]
if (!nextConfigPath || nextConfigPath.length === 0) {
return {
hasTopLevelLayout: false,
isSrcDir,
nextConfigPath: undefined,
}
}
let nextAppDir: string | undefined = (
await globby(['**/app'], {
absolute: true,
cwd: projectDir,
onlyDirectories: true,
})
)?.[0]
if (!nextAppDir || nextAppDir.length === 0) {
nextAppDir = undefined
}
const hasTopLevelLayout = nextAppDir
? fs.existsSync(path.resolve(nextAppDir, 'layout.tsx'))
: false
return { hasTopLevelLayout, isSrcDir, nextAppDir, nextConfigPath }
}

View File

@@ -1,25 +1,20 @@
import prompts from 'prompts'
import * as p from '@clack/prompts'
import slugify from '@sindresorhus/slugify'
import type { CliArgs } from '../types.js'
export async function parseProjectName(args: CliArgs): Promise<string> {
if (args['--init-next']) return '.'
if (args['--name']) return args['--name']
if (args._[0]) return args._[0]
if (args['--name']) return slugify(args['--name'])
if (args._[0]) return slugify(args._[0])
const response = await prompts(
{
name: 'value',
type: 'text',
message: 'Project name?',
validate: (value: string) => !!value.length,
const projectName = await p.text({
message: 'Project name?',
validate: (value) => {
if (!value) return 'Please enter a project name.'
},
{
onCancel: () => {
process.exit(0)
},
},
)
return response.value
})
if (p.isCancel(projectName)) {
process.exit(0)
}
return slugify(projectName)
}

View File

@@ -1,11 +1,11 @@
import prompts from 'prompts'
import * as p from '@clack/prompts'
import type { CliArgs, ProjectTemplate } from '../types.js'
export async function parseTemplate(
args: CliArgs,
validTemplates: ProjectTemplate[],
): Promise<ProjectTemplate> {
): Promise<ProjectTemplate | undefined> {
if (args['--template']) {
const templateName = args['--template']
const template = validTemplates.find((t) => t.name === templateName)
@@ -13,29 +13,20 @@ export async function parseTemplate(
return template
}
const response = await prompts(
{
name: 'value',
type: 'select',
choices: validTemplates.map((p) => {
return {
description: p.description,
title: p.name,
value: p.name,
}
}),
message: 'Choose project template',
validate: (value: string) => !!value.length,
},
{
onCancel: () => {
process.exit(0)
},
},
)
const response = await p.select<{ label: string; value: string }[], string>({
message: 'Choose project template',
options: validTemplates.map((p) => {
return {
label: p.name,
value: p.name,
}
}),
})
if (p.isCancel(response)) {
process.exit(0)
}
const template = validTemplates.find((t) => t.name === response.value)
if (!template) throw new Error('Template is undefined')
const template = validTemplates.find((t) => t.name === response)
return template
}

View File

@@ -1,5 +1,5 @@
import * as p from '@clack/prompts'
import slugify from '@sindresorhus/slugify'
import prompts from 'prompts'
import type { CliArgs, DbDetails, DbType } from '../types.js'
@@ -23,7 +23,7 @@ const dbChoiceRecord: Record<DbType, DbChoice> = {
}
export async function selectDb(args: CliArgs, projectName: string): Promise<DbDetails> {
let dbType: DbType | undefined = undefined
let dbType: DbType | symbol | undefined = undefined
if (args['--db']) {
if (!Object.values(dbChoiceRecord).some((dbChoice) => dbChoice.value === args['--db'])) {
throw new Error(
@@ -34,31 +34,20 @@ export async function selectDb(args: CliArgs, projectName: string): Promise<DbDe
}
dbType = args['--db'] as DbType
} else {
const dbTypeRes = await prompts(
{
name: 'value',
type: 'select',
choices: Object.values(dbChoiceRecord).map((dbChoice) => {
return {
title: dbChoice.title,
value: dbChoice.value,
}
}),
message: 'Select a database',
validate: (value: string) => !!value.length,
},
{
onCancel: () => {
process.exit(0)
},
},
)
dbType = dbTypeRes.value
dbType = await p.select<{ label: string; value: DbType }[], DbType>({
initialValue: 'mongodb',
message: `Select a database`,
options: [
{ label: 'MongoDB', value: 'mongodb' },
{ label: 'Postgres', value: 'postgres' },
],
})
if (p.isCancel(dbType)) process.exit(0)
}
const dbChoice = dbChoiceRecord[dbType]
let dbUri: string | undefined = undefined
let dbUri: string | symbol | undefined = undefined
const initialDbUri = `${dbChoice.dbConnectionPrefix}${
projectName === '.' ? `payload-${getRandomDigitSuffix()}` : slugify(projectName)
}`
@@ -68,21 +57,11 @@ export async function selectDb(args: CliArgs, projectName: string): Promise<DbDe
} else if (args['--db-connection-string']) {
dbUri = args['--db-connection-string']
} else {
const dbUriRes = await prompts(
{
name: 'value',
type: 'text',
initial: initialDbUri,
message: `Enter ${dbChoice.title.split(' ')[0]} connection string`, // strip beta from title
validate: (value: string) => !!value.length,
},
{
onCancel: () => {
process.exit(0)
},
},
)
dbUri = dbUriRes.value
dbUri = await p.text({
initialValue: initialDbUri,
message: `Enter ${dbChoice.title.split(' ')[0]} connection string`, // strip beta from title
})
if (p.isCancel(dbUri)) process.exit(0)
}
return {

View File

@@ -1,4 +1,5 @@
import { parseAndInsertWithPayload, withPayloadImportStatement } from './wrap-next-config.js'
import { parseAndModifyConfigContent, withPayloadImportStatement } from './wrap-next-config.js'
import * as p from '@clack/prompts'
const defaultNextConfig = `/** @type {import('next').NextConfig} */
const nextConfig = {};
@@ -30,25 +31,31 @@ export { wrapped as default }
describe('parseAndInsertWithPayload', () => {
it('should parse the default next config', () => {
const { modifiedConfigContent } = parseAndInsertWithPayload(defaultNextConfig)
const { modifiedConfigContent } = parseAndModifyConfigContent(defaultNextConfig)
expect(modifiedConfigContent).toContain(withPayloadImportStatement)
expect(modifiedConfigContent).toContain('withPayload(nextConfig)')
})
it('should parse the config with a function', () => {
const { modifiedConfigContent } = parseAndInsertWithPayload(nextConfigWithFunc)
const { modifiedConfigContent } = parseAndModifyConfigContent(nextConfigWithFunc)
expect(modifiedConfigContent).toContain('withPayload(someFunc(nextConfig))')
})
it('should parse the config with a function on a new line', () => {
const { modifiedConfigContent } = parseAndInsertWithPayload(nextConfigWithFuncMultiline)
const { modifiedConfigContent } = parseAndModifyConfigContent(nextConfigWithFuncMultiline)
expect(modifiedConfigContent).toContain(withPayloadImportStatement)
expect(modifiedConfigContent).toMatch(/withPayload\(someFunc\(\n nextConfig\n\)\)/)
})
// Unsupported: export { wrapped as default }
it('should give warning with a named export as default', () => {
const { modifiedConfigContent, error } = parseAndInsertWithPayload(nextConfigExportNamedDefault)
const warnLogSpy = jest.spyOn(p.log, 'warn').mockImplementation(() => {})
const { modifiedConfigContent, success } = parseAndModifyConfigContent(
nextConfigExportNamedDefault,
)
expect(modifiedConfigContent).toContain(withPayloadImportStatement)
expect(error).toBeTruthy()
expect(success).toBe(false)
expect(warnLogSpy).toHaveBeenCalledWith(expect.stringContaining('Could not automatically wrap'))
})
})

View File

@@ -1,28 +1,30 @@
import chalk from 'chalk'
import { parseModule } from 'esprima'
import fs from 'fs'
import globby from 'globby'
import path from 'path'
import { warning } from '../utils/log.js'
import { log } from '../utils/log.js'
export const withPayloadImportStatement = `import { withPayload } from '@payloadcms/next'\n`
export const wrapNextConfig = async (args: { projectDir: string }): Promise<void> => {
const foundConfig = (await globby('next.config.*js', { cwd: args.projectDir }))?.[0]
export const wrapNextConfig = (args: { nextConfigPath: string }) => {
const { nextConfigPath } = args
const configContent = fs.readFileSync(nextConfigPath, 'utf8')
const { modifiedConfigContent: newConfig, success } = parseAndModifyConfigContent(configContent)
if (!foundConfig) {
throw new Error(`No Next config found at ${args.projectDir}`)
if (!success) {
return
}
const configPath = path.resolve(args.projectDir, foundConfig)
const configContent = fs.readFileSync(configPath, 'utf8')
const { error, modifiedConfigContent: newConfig } = parseAndInsertWithPayload(configContent)
if (error) {
console.warn(error)
}
fs.writeFileSync(configPath, newConfig)
fs.writeFileSync(nextConfigPath, newConfig)
}
export function parseAndInsertWithPayload(content: string): {
error?: string
/**
* Parses config content with AST and wraps it with withPayload function
*/
export function parseAndModifyConfigContent(content: string): {
modifiedConfigContent: string
success: boolean
} {
content = withPayloadImportStatement + content
const ast = parseModule(content, { loc: true })
@@ -38,12 +40,12 @@ export function parseAndInsertWithPayload(content: string): {
throw new Error('Could not find ExportDefaultDeclaration in next.config.js')
}
if (exportDefaultDeclaration) {
if (exportDefaultDeclaration && exportDefaultDeclaration.declaration?.loc) {
const modifiedConfigContent = insertBeforeAndAfter(
content,
exportDefaultDeclaration.declaration?.loc,
exportDefaultDeclaration.declaration.loc,
)
return { modifiedConfigContent }
return { modifiedConfigContent, success: true }
} else if (exportNamedDeclaration) {
const exportSpecifier = exportNamedDeclaration.specifiers.find(
(s) =>
@@ -54,16 +56,42 @@ export function parseAndInsertWithPayload(content: string): {
)
if (exportSpecifier) {
// TODO: Improve with this example and/or link to docs
warning('Could not automatically wrap next.config.js with withPayload.')
warning('Automatic wrapping of named exports as default not supported yet.')
warnUserWrapNotSuccessful()
return {
error: `Automatic wrapping of named exports as default not supported yet.
Please manually wrap your Next config with the withPayload function`,
modifiedConfigContent: content,
success: false,
}
}
} else {
throw new Error('Could not automatically wrap next.config.js with withPayload')
}
warning('Could not automatically wrap next.config.js with withPayload.')
warnUserWrapNotSuccessful()
return {
modifiedConfigContent: content,
success: false,
}
}
function warnUserWrapNotSuccessful() {
// Output directions for user to update next.config.js
const withPayloadMessage = `
${chalk.bold(`Please manually wrap your existing next.config.js with the withPayload function. Here is an example:`)}
import withPayload from '@payloadcms/next/withPayload'
const nextConfig = {
// Your Next.js config here
}
export default withPayload(nextConfig)
`
log(withPayloadMessage)
}
type Directive = {

View File

@@ -1,10 +1,9 @@
import chalk from 'chalk'
import fs from 'fs-extra'
import path from 'path'
import type { CliArgs, ProjectTemplate } from '../types.js'
import { error, success } from '../utils/log.js'
import { debug, error } from '../utils/log.js'
/** Parse and swap .env.example values and write .env */
export async function writeEnvFile(args: {
@@ -12,17 +11,17 @@ export async function writeEnvFile(args: {
databaseUri: string
payloadSecret: string
projectDir: string
template: ProjectTemplate
template?: ProjectTemplate
}): Promise<void> {
const { cliArgs, databaseUri, payloadSecret, projectDir, template } = args
if (cliArgs['--dry-run']) {
success(`DRY RUN: .env file created`)
debug(`DRY RUN: .env file created`)
return
}
try {
if (template.type === 'starter' && fs.existsSync(path.join(projectDir, '.env.example'))) {
if (template?.type === 'starter' && fs.existsSync(path.join(projectDir, '.env.example'))) {
// Parse .env file into key/value pairs
const envFile = await fs.readFile(path.join(projectDir, '.env.example'), 'utf8')
const envWithValues: string[] = envFile
@@ -48,11 +47,9 @@ export async function writeEnvFile(args: {
// Write new .env file
await fs.writeFile(path.join(projectDir, '.env'), envWithValues.join('\n'))
} else {
const content = `MONGODB_URI=${databaseUri}\nPAYLOAD_SECRET=${payloadSecret}`
const content = `DATABASE_URI=${databaseUri}\nPAYLOAD_SECRET=${payloadSecret}`
await fs.outputFile(`${projectDir}/.env`, content)
}
success('.env file created')
} catch (err: unknown) {
error('Unable to write .env file')
if (err instanceof Error) {

View File

@@ -1,21 +1,31 @@
/* eslint-disable no-console */
import * as p from '@clack/prompts'
import slugify from '@sindresorhus/slugify'
import arg from 'arg'
import chalk from 'chalk'
// @ts-expect-error no types
import { detect } from 'detect-package-manager'
import figures from 'figures'
import path from 'path'
import type { CliArgs, PackageManager } from './types.js'
import { configurePayloadConfig } from './lib/configure-payload-config.js'
import { createProject } from './lib/create-project.js'
import { generateSecret } from './lib/generate-secret.js'
import { initNext } from './lib/init-next.js'
import { getNextAppDetails, initNext } from './lib/init-next.js'
import { parseProjectName } from './lib/parse-project-name.js'
import { parseTemplate } from './lib/parse-template.js'
import { selectDb } from './lib/select-db.js'
import { getValidTemplates, validateTemplate } from './lib/templates.js'
import { writeEnvFile } from './lib/write-env-file.js'
import { error, success } from './utils/log.js'
import { helpMessage, successMessage, welcomeMessage } from './utils/messages.js'
import { error, info } from './utils/log.js'
import {
feedbackOutro,
helpMessage,
moveMessage,
successMessage,
successfulNextInit,
} from './utils/messages.js'
export class Main {
args: CliArgs
@@ -35,7 +45,7 @@ export class Main {
'--template-branch': String,
// Next.js
'--init-next': Boolean,
'--init-next': Boolean, // TODO: Is this needed if we detect if inside Next.js project?
// Package manager
'--no-deps': Boolean,
@@ -61,42 +71,102 @@ export class Main {
async init(): Promise<void> {
try {
if (this.args['--help']) {
console.log(helpMessage())
helpMessage()
process.exit(0)
}
const projectName = await parseProjectName(this.args)
const projectDir = path.resolve(
projectName === '.' || this.args['--init-next']
? path.basename(process.cwd())
: `./${slugify(projectName)}`,
)
// eslint-disable-next-line no-console
console.log('\n')
p.intro(chalk.bgCyan(chalk.black(' create-payload-app ')))
p.note("Welcome to Payload. Let's create a project!")
// Detect if inside Next.js project
const nextAppDetails = await getNextAppDetails(process.cwd())
const { hasTopLevelLayout, nextAppDir, nextConfigPath } = nextAppDetails
if (nextConfigPath) {
this.args['--name'] = slugify(path.basename(path.dirname(nextConfigPath)))
}
const projectName = await parseProjectName(this.args)
const projectDir = nextConfigPath
? path.dirname(nextConfigPath)
: path.resolve(process.cwd(), slugify(projectName))
console.log(welcomeMessage)
const packageManager = await getPackageManager(this.args, projectDir)
if (this.args['--init-next']) {
const result = await initNext({ ...this.args, packageManager })
if (!result.success) {
error(result.reason || 'Failed to initialize Payload app in Next.js project')
} else {
success('Payload app successfully initialized in Next.js project')
if (nextConfigPath) {
p.log.step(
chalk.bold(`${chalk.bgBlack(` ${figures.triangleUp} Next.js `)} project detected!`),
)
const proceed = await p.confirm({
initialValue: true,
message: chalk.bold(`Install ${chalk.green('Payload')} in this project?`),
})
if (p.isCancel(proceed) || !proceed) {
p.outro(feedbackOutro())
process.exit(0)
}
process.exit(result.success ? 0 : 1)
// TODO: This should continue the normal prompt flow
// Check for top-level layout.tsx
if (nextAppDir && hasTopLevelLayout) {
p.log.warn(moveMessage({ nextAppDir, projectDir }))
p.outro(feedbackOutro())
process.exit(0)
}
const dbDetails = await selectDb(this.args, projectName)
const result = await initNext({
...this.args,
dbType: dbDetails.type,
nextAppDetails,
packageManager,
projectDir,
})
if (result.success === false) {
p.outro(feedbackOutro())
process.exit(1)
}
await configurePayloadConfig({
dbDetails,
projectDirOrConfigPath: {
payloadConfigPath: result.payloadConfigPath,
},
})
await writeEnvFile({
cliArgs: this.args,
databaseUri: dbDetails.dbUri,
payloadSecret: generateSecret(),
projectDir,
})
info('Payload project successfully initialized!')
p.note(successfulNextInit(), chalk.bgGreen(chalk.black(' Documentation ')))
p.outro(feedbackOutro())
return
}
const templateArg = this.args['--template']
if (templateArg) {
const valid = validateTemplate(templateArg)
if (!valid) {
console.log(helpMessage())
helpMessage()
process.exit(1)
}
}
const validTemplates = getValidTemplates()
const template = await parseTemplate(this.args, validTemplates)
if (!template) {
p.log.error('Invalid template given')
p.outro(feedbackOutro())
process.exit(1)
}
switch (template.type) {
case 'starter': {
@@ -131,10 +201,11 @@ export class Main {
}
}
success('Payload project successfully created')
console.log(successMessage(projectDir, packageManager))
} catch (error: unknown) {
console.log(error)
info('Payload project successfully created!')
p.note(successMessage(projectDir, packageManager), chalk.bgGreen(chalk.black(' Next Steps ')))
p.outro(feedbackOutro())
} catch (err: unknown) {
error(err instanceof Error ? err.message : 'An error occurred')
}
}
}

View File

@@ -0,0 +1,35 @@
import fs from 'fs'
import fsp from 'fs/promises'
import { fileURLToPath } from 'node:url'
import path from 'path'
const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
// eslint-disable-next-line @typescript-eslint/no-floating-promises
main()
/**
* Copy the necessary template files from `templates/blank-3.0` to `dist/template`
*
* Eventually, this should be replaced with using tar.x to stream from the git repo
*/
async function main() {
const root = path.resolve(dirname, '../../../../')
const outputPath = path.resolve(dirname, '../../dist/template')
const sourceTemplatePath = path.resolve(root, 'templates/blank-3.0')
if (!fs.existsSync(sourceTemplatePath)) {
throw new Error(`Source path does not exist: ${sourceTemplatePath}`)
}
if (!fs.existsSync(outputPath)) {
fs.mkdirSync(outputPath, { recursive: true })
}
// Copy the src directory from `templates/blank-3.0` to `dist`
const srcPath = path.resolve(sourceTemplatePath, 'src')
const distSrcPath = path.resolve(outputPath, 'src')
// Copy entire file structure from src to dist
await fsp.cp(srcPath, distSrcPath, { recursive: true })
}

View File

@@ -7,7 +7,7 @@ import path from 'path'
export function copyRecursiveSync(src: string, dest: string, debug?: boolean) {
const exists = fs.existsSync(src)
const stats = exists && fs.statSync(src)
const isDirectory = exists && stats.isDirectory()
const isDirectory = exists && stats !== false && stats.isDirectory()
if (isDirectory) {
fs.mkdirSync(dest, { recursive: true })
fs.readdirSync(src).forEach((childItemName) => {

View File

@@ -1,27 +1,23 @@
/* eslint-disable no-console */
import * as p from '@clack/prompts'
import chalk from 'chalk'
import figures from 'figures'
export const success = (message: string): void => {
console.log(`${chalk.green(figures.tick)} ${chalk.bold(message)}`)
}
export const warning = (message: string): void => {
console.log(chalk.yellow('? ') + chalk.bold(message))
p.log.warn(chalk.yellow('? ') + chalk.bold(message))
}
export const info = (message: string, paddingTop?: number): void => {
console.log(
`${'\n'.repeat(paddingTop || 0)}${chalk.green(figures.pointerSmall)} ${chalk.bold(message)}`,
)
export const info = (message: string): void => {
p.log.step(chalk.bold(message))
}
export const error = (message: string): void => {
console.log(`${chalk.red(figures.cross)} ${chalk.bold(message)}`)
p.log.error(chalk.bold(message))
}
export const debug = (message: string): void => {
console.log(
`${chalk.gray(figures.pointerSmall)} ${chalk.bgGray('[DEBUG]')} ${chalk.gray(message)}`,
)
p.log.step(`${chalk.bgGray('[DEBUG]')} ${chalk.gray(message)}`)
}
export const log = (message: string): void => {
p.log.message(message)
}

View File

@@ -1,13 +1,14 @@
/* eslint-disable no-console */
import chalk from 'chalk'
import figures from 'figures'
import path from 'path'
import terminalLink from 'terminal-link'
import type { ProjectTemplate } from '../types.js'
import type { PackageManager } from '../types.js'
import { getValidTemplates } from '../lib/templates.js'
const header = (message: string): string => `${chalk.yellow(figures.star)} ${chalk.bold(message)}`
const header = (message: string): string => chalk.bold(message)
export const welcomeMessage = chalk`
{green Welcome to Payload. Let's create a project! }
@@ -15,14 +16,14 @@ export const welcomeMessage = chalk`
const spacer = ' '.repeat(8)
export function helpMessage(): string {
export function helpMessage(): void {
const validTemplates = getValidTemplates()
return chalk`
console.log(chalk`
{bold USAGE}
{dim $} {bold npx create-payload-app}
{dim $} {bold npx create-payload-app} my-project
{dim $} {bold npx create-payload-app} -n my-project -t blog
{dim $} {bold npx create-payload-app} -n my-project -t template-name
{bold OPTIONS}
@@ -36,7 +37,7 @@ export function helpMessage(): string {
--use-pnpm Use pnpm to install dependencies
--no-deps Do not install any dependencies
-h Show help
`
`)
}
function formatTemplates(templates: ProjectTemplate[]) {
@@ -45,29 +46,58 @@ function formatTemplates(templates: ProjectTemplate[]) {
.join(`\n${spacer}`)}`
}
export function successMessage(projectDir: string, packageManager: string): string {
export function successMessage(projectDir: string, packageManager: PackageManager): string {
const relativePath = path.relative(process.cwd(), projectDir)
return `
${header('Launch Application:')}
${header('Launch Application:')}
- cd ${projectDir}
- ${
packageManager === 'yarn' ? 'yarn' : 'npm run'
} dev or follow directions in ${createTerminalLink(
'README.md',
`file://${path.resolve(projectDir, 'README.md')}`,
)}
- cd ./${relativePath}
- ${
packageManager === 'npm' ? 'npm run' : packageManager
} dev or follow directions in ${createTerminalLink(
'README.md',
`file://${path.resolve(projectDir, 'README.md')}`,
)}
${header('Documentation:')}
${header('Documentation:')}
- ${createTerminalLink(
'Getting Started',
'https://payloadcms.com/docs/getting-started/what-is-payload',
)}
- ${createTerminalLink('Configuration', 'https://payloadcms.com/docs/configuration/overview')}
- ${createTerminalLink(
'Getting Started',
'https://payloadcms.com/docs/getting-started/what-is-payload',
)}
- ${createTerminalLink('Configuration', 'https://payloadcms.com/docs/configuration/overview')}
`
}
export function successfulNextInit(): string {
return `- ${createTerminalLink(
'Getting Started',
'https://payloadcms.com/docs/getting-started/what-is-payload',
)}
- ${createTerminalLink('Configuration', 'https://payloadcms.com/docs/configuration/overview')}
`
}
export function moveMessage(args: { nextAppDir: string; projectDir: string }): string {
const relativePath = path.relative(process.cwd(), args.nextAppDir)
return `
${header('Next Steps:')}
Payload does not support a top-level layout.tsx file in the app directory.
${chalk.bold('To continue:')}
Move all files from ./${relativePath} to a named directory such as ./${relativePath}/${chalk.bold('(app)')}
Once moved, rerun the create-payload-app command again.
`
}
export function feedbackOutro(): string {
return `${chalk.bgCyan(chalk.black(' Have feedback? '))} Visit us on ${createTerminalLink('GitHub', 'https://github.com/payloadcms/payload')}.`
}
// Create terminalLink with fallback for unsupported terminals
function createTerminalLink(text: string, url: string) {
return terminalLink(text, url, {

View File

@@ -5,7 +5,8 @@
"noEmit": false /* Do not emit outputs. */,
"emitDeclarationOnly": true,
"outDir": "./dist" /* Specify an output folder for all emitted files. */,
"rootDir": "./src" /* Specify the root folder within your source files. */
"rootDir": "./src" /* Specify the root folder within your source files. */,
"strict": true,
},
"exclude": ["dist", "build", "tests", "test", "node_modules", ".eslintrc.js"],
"include": ["src/**/*.ts", "src/**/*.spec.ts", "src/**/*.tsx", "src/**/*.d.ts", "src/**/*.json"]

View File

@@ -1,8 +1,12 @@
{
"name": "@payloadcms/db-mongodb",
"version": "3.0.0-alpha.49",
"description": "The officially supported MongoDB database adapter for Payload - Update 2",
"repository": "https://github.com/payloadcms/payload",
"description": "The officially supported MongoDB database adapter for Payload",
"repository": {
"type": "git",
"url": "https://github.com/payloadcms/payload.git",
"directory": "packages/db-mongodb"
},
"license": "MIT",
"homepage": "https://payloadcms.com",
"type": "module",
@@ -15,7 +19,7 @@
"types": "./src/types.ts",
"scripts": {
"build": "pnpm build:swc && pnpm build:types",
"build:swc": "swc ./src -d ./dist --config-file .swcrc",
"build:swc": "swc ./src -d ./dist --config-file .swcrc-build",
"build:types": "tsc --emitDeclarationOnly --outDir dist",
"clean": "rimraf {dist,*.tsbuildinfo}",
"prepublishOnly": "pnpm clean && pnpm turbo build"

View File

@@ -62,7 +62,7 @@ export const sanitizeQueryValue = ({
formattedValue = Number(val)
}
if (field.type === 'date' && typeof val === 'string') {
if (field.type === 'date' && typeof val === 'string' && operator !== 'exists') {
formattedValue = new Date(val)
if (Number.isNaN(Date.parse(formattedValue))) {
return undefined

View File

@@ -2,7 +2,11 @@
"name": "@payloadcms/db-postgres",
"version": "3.0.0-alpha.49",
"description": "The officially supported Postgres database adapter for Payload",
"repository": "https://github.com/payloadcms/payload",
"repository": {
"type": "git",
"url": "https://github.com/payloadcms/payload.git",
"directory": "packages/db-postgres"
},
"license": "MIT",
"homepage": "https://payloadcms.com",
"type": "module",

View File

@@ -66,7 +66,7 @@ export const sanitizeQueryValue = ({
formattedValue = Number(val)
}
if (field.type === 'date') {
if (field.type === 'date' && operator !== 'exists') {
if (typeof val === 'string') {
formattedValue = new Date(val)
if (Number.isNaN(Date.parse(formattedValue))) {

View File

@@ -3,6 +3,12 @@
"version": "1.1.1",
"description": "Payload styles for ESLint and Prettier",
"license": "MIT",
"homepage": "https://payloadcms.com",
"repository": {
"type": "git",
"url": "https://github.com/payloadcms/payload.git",
"directory": "packages/eslint-config-payload"
},
"author": {
"email": "info@payloadcms.com",
"name": "Payload",

View File

@@ -3,6 +3,12 @@
"version": "1.0.0",
"description": "Payload plugins for ESLint",
"license": "MIT",
"homepage": "https://payloadcms.com",
"repository": {
"type": "git",
"url": "https://github.com/payloadcms/payload.git",
"directory": "packages/eslint-plugin-payload"
},
"author": {
"email": "info@payloadcms.com",
"name": "Payload",

View File

@@ -4,6 +4,12 @@
"main": "./src/index.ts",
"types": "./src/index.d.ts",
"type": "module",
"homepage": "https://payloadcms.com",
"repository": {
"type": "git",
"url": "https://github.com/payloadcms/payload.git",
"directory": "packages/graphql"
},
"scripts": {
"build": "pnpm build:swc && pnpm build:types",
"build:swc": "swc ./src -d ./dist --config-file .swcrc",

View File

@@ -2,12 +2,16 @@
"name": "@payloadcms/live-preview-react",
"version": "0.2.0",
"description": "The official live preview React SDK for Payload",
"repository": "https://github.com/payloadcms/payload",
"repository": {
"type": "git",
"url": "https://github.com/payloadcms/payload.git",
"directory": "packages/live-preview-react"
},
"license": "MIT",
"homepage": "https://payloadcms.com",
"author": "Payload CMS, Inc.",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"main": "./src/index.ts",
"types": "./src/index.ts",
"type": "module",
"scripts": {
"build": "pnpm copyfiles && pnpm build:swc && pnpm build:types",

View File

@@ -2,12 +2,16 @@
"name": "@payloadcms/live-preview",
"version": "0.2.2",
"description": "The official live preview JavaScript SDK for Payload",
"repository": "https://github.com/payloadcms/payload",
"repository": {
"type": "git",
"url": "https://github.com/payloadcms/payload.git",
"directory": "packages/live-preview"
},
"license": "MIT",
"homepage": "https://payloadcms.com",
"author": "Payload CMS, Inc.",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"main": "./src/index.ts",
"types": "./src/index.ts",
"type": "module",
"scripts": {
"build": "pnpm copyfiles && pnpm build:swc && pnpm build:types",

View File

@@ -4,6 +4,12 @@
"main": "./src/index.js",
"types": "./src/index.js",
"type": "module",
"homepage": "https://payloadcms.com",
"repository": {
"type": "git",
"url": "https://github.com/payloadcms/payload.git",
"directory": "packages/next"
},
"bin": {
"@payloadcms/next": "./dist/bin/index.js"
},

View File

@@ -1,3 +1,3 @@
export { EditView } from '../views/Edit/index.js'
export { NotFoundView } from '../views/NotFound/index.js'
export { NotFoundPage } from '../views/NotFound/index.js'
export { type GenerateViewMetadata, RootPage, generatePageMetadata } from '../views/Root/index.js'

View File

@@ -5,11 +5,14 @@ import { registerFirstUserOperation } from 'payload/operations'
import type { CollectionRouteHandler } from '../types.js'
export const registerFirstUser: CollectionRouteHandler = async ({ collection, req }) => {
const data = req.data
const result = await registerFirstUserOperation({
collection,
data: {
email: typeof req.data?.email === 'string' ? req.data.email : '',
password: typeof req.data?.password === 'string' ? req.data.password : '',
...data,
email: typeof data?.email === 'string' ? data.email : '',
password: typeof data?.password === 'string' ? data.password : '',
},
req,
})

View File

@@ -1,5 +1,11 @@
import type { BuildFormStateArgs } from '@payloadcms/ui/forms/buildStateFromSchema'
import type { Field, PayloadRequest, SanitizedConfig } from 'payload/types'
import type {
DocumentPreferences,
Field,
PayloadRequest,
SanitizedConfig,
TypeWithID,
} from 'payload/types'
import { buildStateFromSchema } from '@payloadcms/ui/forms/buildStateFromSchema'
import { reduceFieldsToValues } from '@payloadcms/ui/utilities/reduceFieldsToValues'
@@ -27,86 +33,177 @@ export const getFieldSchemaMap = (config: SanitizedConfig): FieldSchemaMap => {
}
export const buildFormState = async ({ req }: { req: PayloadRequest }) => {
const reqData: BuildFormStateArgs = req.data as BuildFormStateArgs
try {
const reqData: BuildFormStateArgs = req.data as BuildFormStateArgs
const { collectionSlug, formState, globalSlug, locale, operation, schemaPath } = reqData
const incomingUserSlug = req.user?.collection
const adminUserSlug = req.payload.config.admin.user
const incomingUserSlug = req.user?.collection
const adminUserSlug = req.payload.config.admin.user
// If we have a user slug, test it against the functions
if (incomingUserSlug) {
const adminAccessFunction = req.payload.collections[incomingUserSlug].config.access?.admin
// If we have a user slug, test it against the functions
if (incomingUserSlug) {
const adminAccessFunction = req.payload.collections[incomingUserSlug].config.access?.admin
// Run the admin access function from the config if it exists
if (adminAccessFunction) {
const canAccessAdmin = await adminAccessFunction(req)
// Run the admin access function from the config if it exists
if (adminAccessFunction) {
const canAccessAdmin = await adminAccessFunction(req)
if (!canAccessAdmin) {
if (!canAccessAdmin) {
return Response.json(null, {
status: httpStatus.UNAUTHORIZED,
})
}
// Match the user collection to the global admin config
} else if (adminUserSlug !== incomingUserSlug) {
return Response.json(null, {
status: httpStatus.UNAUTHORIZED,
})
}
// Match the user collection to the global admin config
} else if (adminUserSlug !== incomingUserSlug) {
} else {
return Response.json(null, {
status: httpStatus.UNAUTHORIZED,
})
}
} else {
return Response.json(null, {
status: httpStatus.UNAUTHORIZED,
})
}
const fieldSchemaMap = getFieldSchemaMap(req.payload.config)
const fieldSchemaMap = getFieldSchemaMap(req.payload.config)
const {
collectionSlug,
data: incomingData,
docPreferences,
formState,
operation,
schemaPath,
} = reqData
const id = collectionSlug ? reqData.id : undefined
const schemaPathSegments = schemaPath.split('.')
const schemaPathSegments = schemaPath.split('.')
let fieldSchema: Field[]
let fieldSchema: Field[]
if (schemaPathSegments.length === 1) {
if (req.payload.collections[schemaPath]) {
fieldSchema = req.payload.collections[schemaPath].config.fields
} else {
fieldSchema = req.payload.config.globals.find((global) => global.slug === schemaPath)?.fields
if (schemaPathSegments.length === 1) {
if (req.payload.collections[schemaPath]) {
fieldSchema = req.payload.collections[schemaPath].config.fields
} else {
fieldSchema = req.payload.config.globals.find(
(global) => global.slug === schemaPath,
)?.fields
}
} else if (fieldSchemaMap.has(schemaPath)) {
fieldSchema = fieldSchemaMap.get(schemaPath)
}
} else if (fieldSchemaMap.has(schemaPath)) {
fieldSchema = fieldSchemaMap.get(schemaPath)
}
if (!fieldSchema) {
if (!fieldSchema) {
return Response.json(
{
message: 'Could not find field schema for given path',
},
{
status: httpStatus.BAD_REQUEST,
},
)
}
let docPreferences = reqData.docPreferences
let data = reqData.data
const promises: {
data?: Promise<void>
preferences?: Promise<void>
} = {}
// If the request does not include doc preferences,
// we should fetch them. This is useful for DocumentInfoProvider
// as it reduces the amount of client-side fetches necessary
// when we fetch data for the Edit view
if (!docPreferences) {
let preferencesKey
if (collectionSlug && id) {
preferencesKey = `collection-${collectionSlug}-${id}`
}
if (globalSlug) {
preferencesKey = `global-${globalSlug}`
}
if (preferencesKey) {
const fetchPreferences = async () => {
const preferencesResult = (await req.payload.find({
collection: 'payload-preferences',
depth: 0,
limit: 1,
where: {
key: {
equals: preferencesKey,
},
},
})) as unknown as { docs: { value: DocumentPreferences }[] }
if (preferencesResult?.docs?.[0]?.value) docPreferences = preferencesResult.docs[0].value
}
promises.preferences = fetchPreferences()
}
}
// If there is a form state,
// then we can deduce data from that form state
if (formState) data = reduceFieldsToValues(formState, true)
// If we do not have data at this point,
// we can fetch it. This is useful for DocumentInfoProvider
// to reduce the amount of fetches required
if (!data) {
const fetchData = async () => {
let resolvedData: TypeWithID
if (collectionSlug && id) {
resolvedData = await req.payload.findByID({
id,
collection: collectionSlug,
depth: 0,
draft: true,
fallbackLocale: null,
locale,
overrideAccess: false,
user: req.user,
})
}
if (globalSlug) {
resolvedData = await req.payload.findGlobal({
slug: globalSlug,
depth: 0,
draft: true,
fallbackLocale: null,
locale,
overrideAccess: false,
user: req.user,
})
}
data = resolvedData
}
promises.data = fetchData()
}
if (Object.keys(promises).length > 0) {
await Promise.all(Object.values(promises))
}
const result = await buildStateFromSchema({
id,
data,
fieldSchema,
operation,
preferences: docPreferences || { fields: {} },
req,
})
return Response.json(result, {
status: httpStatus.OK,
})
} catch (err) {
return Response.json(
{
message: 'Could not find field schema for given path',
message: 'There was an error building form state',
},
{
status: httpStatus.BAD_REQUEST,
},
)
}
const data = incomingData || reduceFieldsToValues(formState || {}, true)
const id = collectionSlug ? reqData.id : undefined
const result = await buildStateFromSchema({
id,
data,
fieldSchema,
operation,
preferences: docPreferences,
req,
})
return Response.json(result, {
status: httpStatus.OK,
})
}

View File

@@ -0,0 +1,45 @@
import httpStatus from 'http-status'
import { findByIDOperation } from 'payload/operations'
import { isNumber } from 'payload/utilities'
import type { CollectionRouteHandlerWithID } from '../types.js'
import { routeError } from '../routeError.js'
export const preview: CollectionRouteHandlerWithID = async ({ id, collection, req }) => {
const { searchParams } = req
const depth = searchParams.get('depth')
const result = await findByIDOperation({
id,
collection,
depth: isNumber(depth) ? Number(depth) : undefined,
draft: searchParams.get('draft') === 'true',
req,
})
let previewURL: string
const generatePreviewURL = req.payload.config.collections.find(
(config) => config.slug === collection.config.slug,
)?.admin?.preview
if (typeof generatePreviewURL === 'function') {
try {
previewURL = await generatePreviewURL(result, {
locale: req.locale,
token: req.user?.token,
})
} catch (err) {
routeError({
collection,
err,
req,
})
}
}
return Response.json(previewURL, {
status: httpStatus.OK,
})
}

View File

@@ -0,0 +1,44 @@
import httpStatus from 'http-status'
import { findOneOperation } from 'payload/operations'
import { isNumber } from 'payload/utilities'
import type { GlobalRouteHandler } from '../types.js'
import { routeError } from '../routeError.js'
export const preview: GlobalRouteHandler = async ({ globalConfig, req }) => {
const { searchParams } = req
const depth = searchParams.get('depth')
const result = await findOneOperation({
slug: globalConfig.slug,
depth: isNumber(depth) ? Number(depth) : undefined,
draft: searchParams.get('draft') === 'true',
globalConfig,
req,
})
let previewURL: string
const generatePreviewURL = req.payload.config.globals.find(
(config) => config.slug === globalConfig.slug,
)?.admin?.preview
if (typeof generatePreviewURL === 'function') {
try {
previewURL = await generatePreviewURL(result, {
locale: req.locale,
token: req.user?.token,
})
} catch (err) {
routeError({
err,
req,
})
}
}
return Response.json(previewURL, {
status: httpStatus.OK,
})
}

View File

@@ -12,7 +12,6 @@ import type {
} from './types.js'
import { createPayloadRequest } from '../../utilities/createPayloadRequest.js'
import { routeError } from './routeError.js'
import { access } from './auth/access.js'
import { forgotPassword } from './auth/forgotPassword.js'
import { init } from './auth/init.js'
@@ -35,6 +34,7 @@ import { find } from './collections/find.js'
import { findByID } from './collections/findByID.js'
import { findVersionByID } from './collections/findVersionByID.js'
import { findVersions } from './collections/findVersions.js'
import { preview as previewCollection } from './collections/preview.js'
import { restoreVersion } from './collections/restoreVersion.js'
import { update } from './collections/update.js'
import { updateByID } from './collections/updateByID.js'
@@ -43,8 +43,10 @@ import { docAccess as docAccessGlobal } from './globals/docAccess.js'
import { findOne } from './globals/findOne.js'
import { findVersionByID as findVersionByIdGlobal } from './globals/findVersionByID.js'
import { findVersions as findVersionsGlobal } from './globals/findVersions.js'
import { preview as previewGlobal } from './globals/preview.js'
import { restoreVersion as restoreVersionGlobal } from './globals/restoreVersion.js'
import { update as updateGlobal } from './globals/update.js'
import { routeError } from './routeError.js'
const endpoints = {
collection: {
@@ -60,6 +62,7 @@ const endpoints = {
getFile,
init,
me,
preview: previewCollection,
versions: findVersions,
},
PATCH: {
@@ -88,6 +91,7 @@ const endpoints = {
'doc-versions': findVersionsGlobal,
'doc-versions-by-id': findVersionByIdGlobal,
findOne,
preview: previewGlobal,
},
POST: {
'doc-access': docAccessGlobal,
@@ -171,6 +175,7 @@ export const GET =
endpoints: req.payload.config.endpoints,
request,
})
if (disableEndpoints) return disableEndpoints
collection = req.payload.collections?.[slug1]
@@ -212,10 +217,16 @@ export const GET =
if (slug2 === 'file') {
// /:collection/file/:filename
res = await endpoints.collection.GET.getFile({ collection, filename: slug3, req })
} else if (slug3 in endpoints.collection.GET) {
// /:collection/:id/preview
res = await (endpoints.collection.GET[slug3] as CollectionRouteHandlerWithID)({
id: slug2,
collection,
req,
})
} else if (`doc-${slug2}-by-id` in endpoints.collection.GET) {
// /:collection/access/:id
// /:collection/versions/:id
res = await (
endpoints.collection.GET[`doc-${slug2}-by-id`] as CollectionRouteHandlerWithID
)({ id: slug3, collection, req })
@@ -229,6 +240,7 @@ export const GET =
endpoints: globalConfig.endpoints,
request,
})
if (disableEndpoints) return disableEndpoints
const customEndpointResponse = await handleCustomEndpoints({
@@ -236,6 +248,7 @@ export const GET =
entitySlug: `${slug1}/${slug2}`,
payloadRequest: req,
})
if (customEndpointResponse) return customEndpointResponse
switch (slug.length) {
@@ -244,9 +257,16 @@ export const GET =
res = await endpoints.global.GET.findOne({ globalConfig, req })
break
case 3:
if (`doc-${slug3}` in endpoints.global.GET) {
if (slug3 in endpoints.global.GET) {
// /globals/:slug/preview
res = await (endpoints.global.GET[slug3] as GlobalRouteHandler)({
globalConfig,
req,
})
} else if (`doc-${slug3}` in endpoints.global.GET) {
// /globals/:slug/access
// /globals/:slug/versions
// /globals/:slug/preview
res = await (endpoints.global.GET?.[`doc-${slug3}`] as GlobalRouteHandler)({
globalConfig,
req,

View File

@@ -66,7 +66,7 @@ const formatErrors = (incoming: { [key: string]: unknown } | APIError): ErrorRes
}
}
export const routeError = async ({
export const routeError = ({
collection,
err,
req,
@@ -104,21 +104,21 @@ export const routeError = async ({
}
if (collection && typeof collection.config.hooks.afterError === 'function') {
;({ response, status } = (await collection.config.hooks.afterError(
;({ response, status } = collection.config.hooks.afterError(
err,
response,
req.context,
collection.config,
)) || { response, status })
) || { response, status })
}
if (typeof config.hooks.afterError === 'function') {
;({ response, status } = (await config.hooks.afterError(
;({ response, status } = config.hooks.afterError(
err,
response,
req.context,
collection?.config,
)) || {
) || {
response,
status,
})

View File

@@ -21,7 +21,7 @@ export const getPayloadHMR = async (options: InitOptions): Promise<Payload> => {
}
if (cached.payload) {
const config = await options.config
const config = await options.config // TODO: check if we can move this inside the cached.reload === true condition
if (cached.reload === true) {
let resolve
@@ -64,7 +64,11 @@ export const getPayloadHMR = async (options: InitOptions): Promise<Payload> => {
try {
cached.payload = await cached.promise
if (process.env.NODE_ENV !== 'production' && process.env.NODE_ENV !== 'test') {
if (
process.env.NODE_ENV !== 'production' &&
process.env.NODE_ENV !== 'test' &&
process.env.DISABLE_PAYLOAD_HMR !== 'true'
) {
try {
const port = process.env.PORT || '3000'
const ws = new WebSocket(`ws://localhost:${port}/_next/webpack-hmr`)

View File

@@ -4,6 +4,7 @@ import type {
SanitizedCollectionConfig,
SanitizedConfig,
SanitizedGlobalConfig,
VisibleEntities,
} from 'payload/types'
import { initI18n } from '@payloadcms/translations'
@@ -11,7 +12,7 @@ import { translations } from '@payloadcms/translations/client'
import { findLocaleFromCode } from '@payloadcms/ui/utilities/findLocaleFromCode'
import { headers as getHeaders } from 'next/headers.js'
import { notFound, redirect } from 'next/navigation.js'
import { createLocalReq } from 'payload/utilities'
import { createLocalReq, isEntityHidden } from 'payload/utilities'
import qs from 'qs'
import { getPayloadHMR } from '../utilities/getPayloadHMR.js'
@@ -21,8 +22,8 @@ import { getRequestLanguage } from './getRequestLanguage.js'
type Args = {
config: Promise<SanitizedConfig> | SanitizedConfig
redirectUnauthenticatedUser?: boolean
route?: string
searchParams?: { [key: string]: string | string[] | undefined }
route: string
searchParams: { [key: string]: string | string[] | undefined }
}
export const initPage = async ({
@@ -40,6 +41,15 @@ export const initPage = async ({
payload,
})
const visibleEntities: VisibleEntities = {
collections: payload.config.collections
.map(({ slug, admin: { hidden } }) => (!isEntityHidden({ hidden, user }) ? slug : null))
.filter(Boolean),
globals: payload.config.globals
.map(({ slug, admin: { hidden } }) => (!isEntityHidden({ hidden, user }) ? slug : null))
.filter(Boolean),
}
const routeSegments = route.replace(payload.config.routes.admin, '').split('/').filter(Boolean)
const [entityType, entitySlug, createOrID] = routeSegments
const collectionSlug = entityType === 'collections' ? entitySlug : undefined
@@ -49,7 +59,7 @@ export const initPage = async ({
const { collections, globals, localization, routes } = payload.config
if (redirectUnauthenticatedUser && !user && route !== '/login') {
if ('redirect' in searchParams) delete searchParams.redirect
if (searchParams && 'redirect' in searchParams) delete searchParams.redirect
const stringifiedSearchParams = Object.keys(searchParams ?? {}).length
? `?${qs.stringify(searchParams)}`
@@ -71,9 +81,9 @@ export const initPage = async ({
translations,
})
const queryString = `${qs.stringify(searchParams, { addQueryPrefix: true })}`
const queryString = `${qs.stringify(searchParams ?? {}, { addQueryPrefix: true })}`
const req = await createLocalReq(
const req = createLocalReq(
{
fallbackLocale: null,
locale: locale.code,
@@ -118,5 +128,6 @@ export const initPage = async ({
permissions,
req,
translations: i18n.translations,
visibleEntities,
}
}

View File

@@ -1,14 +1,11 @@
import type { DocumentPreferences, ServerSideEditViewProps, TypeWithID } from 'payload/types'
import type { ServerSideEditViewProps } from 'payload/types'
import type { AdminViewProps } from 'payload/types'
import { DocumentHeader } from '@payloadcms/ui/elements/DocumentHeader'
import { HydrateClientUser } from '@payloadcms/ui/elements/HydrateClientUser'
import { RenderCustomComponent } from '@payloadcms/ui/elements/RenderCustomComponent'
import { buildStateFromSchema } from '@payloadcms/ui/forms/buildStateFromSchema'
import { DocumentInfoProvider } from '@payloadcms/ui/providers/DocumentInfo'
import { FormQueryParamsProvider } from '@payloadcms/ui/providers/FormQueryParams'
import { formatDocTitle } from '@payloadcms/ui/utilities/formatDocTitle'
import { formatFields } from '@payloadcms/ui/utilities/formatFields'
import { notFound } from 'next/navigation.js'
import React from 'react'
@@ -17,11 +14,7 @@ import { Settings } from './Settings/index.js'
export { generateAccountMetadata } from './meta.js'
export const Account: React.FC<AdminViewProps> = async ({
initPageResult,
params,
searchParams,
}) => {
export const Account: React.FC<AdminViewProps> = ({ initPageResult, params, searchParams }) => {
const {
locale,
permissions,
@@ -31,7 +24,6 @@ export const Account: React.FC<AdminViewProps> = async ({
payload: { config },
user,
},
req,
} = initPageResult
const {
@@ -45,50 +37,6 @@ export const Account: React.FC<AdminViewProps> = async ({
const collectionConfig = config.collections.find((collection) => collection.slug === userSlug)
if (collectionConfig) {
const { fields } = collectionConfig
let data: TypeWithID
try {
data = await payload.findByID({
id: user.id,
collection: userSlug,
depth: 0,
overrideAccess: false,
user,
})
} catch (error) {
return notFound()
}
const fieldSchema = formatFields(fields, true)
let preferencesKey: string
if (user?.id) {
preferencesKey = `collection-${userSlug}-${user.id}`
}
const { docs: [{ value: docPreferences } = { value: null }] = [] } = (await payload.find({
collection: 'payload-preferences',
depth: 0,
limit: 1,
where: {
key: {
equals: preferencesKey,
},
},
})) as any as { docs: { value: DocumentPreferences }[] } // eslint-disable-line @typescript-eslint/no-explicit-any
const initialState = await buildStateFromSchema({
id: user?.id,
data: data || {},
fieldSchema,
operation: 'update',
preferences: docPreferences,
req,
})
const viewComponentProps: ServerSideEditViewProps = {
initPageResult,
params,
@@ -99,22 +47,13 @@ export const Account: React.FC<AdminViewProps> = async ({
return (
<DocumentInfoProvider
AfterFields={<Settings />}
action={`${serverURL}${api}/${userSlug}${data?.id ? `/${data.id}` : ''}`}
apiURL={`${serverURL}${api}/${userSlug}${data?.id ? `/${data.id}` : ''}`}
action={`${serverURL}${api}/${userSlug}${user?.id ? `/${user.id}` : ''}`}
apiURL={`${serverURL}${api}/${userSlug}${user?.id ? `/${user.id}` : ''}`}
collectionSlug={userSlug}
docPermissions={collectionPermissions}
hasSavePermission={collectionPermissions?.update?.permission}
id={user?.id}
initialData={data}
initialState={initialState}
isEditing
title={formatDocTitle({
collectionConfig,
data,
dateFormat: config.admin.dateFormat,
fallback: data?.id?.toString(),
i18n,
})}
>
<DocumentHeader
collectionConfig={collectionConfig}

View File

@@ -1,6 +1,7 @@
'use client'
import type { EntityToGroup, Group } from '@payloadcms/ui/utilities/groupNavItems'
import type { Permissions } from 'payload/auth'
import type { VisibleEntities } from 'payload/types'
import { getTranslation } from '@payloadcms/translations'
import { Button } from '@payloadcms/ui/elements/Button'
@@ -19,9 +20,8 @@ const baseClass = 'dashboard'
export const DefaultDashboardClient: React.FC<{
Link: React.ComponentType
permissions: Permissions
visibleCollections: string[]
visibleGlobals: string[]
}> = ({ Link, permissions, visibleCollections, visibleGlobals }) => {
visibleEntities: VisibleEntities
}> = ({ Link, permissions, visibleEntities }) => {
const config = useConfig()
const {
@@ -40,13 +40,13 @@ export const DefaultDashboardClient: React.FC<{
const collections = collectionsConfig.filter(
(collection) =>
permissions?.collections?.[collection.slug]?.read?.permission &&
visibleCollections.includes(collection.slug),
visibleEntities.collections.includes(collection.slug),
)
const globals = globalsConfig.filter(
(global) =>
permissions?.globals?.[global.slug]?.read?.permission &&
visibleGlobals.includes(global.slug),
visibleEntities.globals.includes(global.slug),
)
setGroups(
@@ -73,15 +73,7 @@ export const DefaultDashboardClient: React.FC<{
i18n,
),
)
}, [
permissions,
user,
i18n,
visibleCollections,
visibleGlobals,
collectionsConfig,
globalsConfig,
])
}, [permissions, user, i18n, visibleEntities, collectionsConfig, globalsConfig])
return (
<Fragment>

View File

@@ -1,5 +1,5 @@
import type { Permissions } from 'payload/auth'
import type { SanitizedConfig } from 'payload/types'
import type { SanitizedConfig, VisibleEntities } from 'payload/types'
import { Gutter } from '@payloadcms/ui/elements/Gutter'
import { SetStepNav } from '@payloadcms/ui/elements/StepNav'
@@ -15,8 +15,7 @@ export type DashboardProps = {
Link: React.ComponentType<any>
config: SanitizedConfig
permissions: Permissions
visibleCollections: string[]
visibleGlobals: string[]
visibleEntities: VisibleEntities
}
export const DefaultDashboard: React.FC<DashboardProps> = (props) => {
@@ -28,8 +27,7 @@ export const DefaultDashboard: React.FC<DashboardProps> = (props) => {
},
},
permissions,
visibleCollections,
visibleGlobals,
visibleEntities,
} = props
return (
@@ -42,8 +40,7 @@ export const DefaultDashboard: React.FC<DashboardProps> = (props) => {
<DefaultDashboardClient
Link={Link}
permissions={permissions}
visibleCollections={visibleCollections}
visibleGlobals={visibleGlobals}
visibleEntities={visibleEntities}
/>
{Array.isArray(afterDashboard) &&
afterDashboard.map((Component, i) => <Component key={i} />)}

View File

@@ -3,7 +3,6 @@ import type { AdminViewProps } from 'payload/types'
import { HydrateClientUser } from '@payloadcms/ui/elements/HydrateClientUser'
import { RenderCustomComponent } from '@payloadcms/ui/elements/RenderCustomComponent'
import LinkImport from 'next/link.js'
import { isEntityHidden } from 'payload/utilities'
import React, { Fragment } from 'react'
import type { DashboardProps } from './Default/index.js'
@@ -14,40 +13,23 @@ export { generateDashboardMetadata } from './meta.js'
const Link = (LinkImport.default || LinkImport) as unknown as typeof LinkImport.default
export const Dashboard: React.FC<AdminViewProps> = ({
initPageResult,
// searchParams,
}) => {
export const Dashboard: React.FC<AdminViewProps> = ({ initPageResult }) => {
const {
permissions,
req: {
payload: { config },
user,
},
visibleEntities,
} = initPageResult
const CustomDashboardComponent = config.admin.components?.views?.Dashboard
const visibleCollections: string[] = config.collections.reduce((acc, collection) => {
if (!isEntityHidden({ hidden: collection.admin.hidden, user })) {
acc.push(collection.slug)
}
return acc
}, [])
const visibleGlobals: string[] = config.globals.reduce((acc, global) => {
if (!isEntityHidden({ hidden: global.admin.hidden, user })) {
acc.push(global.slug)
}
return acc
}, [])
const viewComponentProps: DashboardProps = {
Link,
config,
permissions,
visibleCollections,
visibleGlobals,
visibleEntities,
}
return (

View File

@@ -1,4 +1,4 @@
import type { CollectionPermission, GlobalPermission, User } from 'payload/auth'
import type { CollectionPermission, GlobalPermission } from 'payload/auth'
import type { EditViewComponent } from 'payload/config'
import type {
AdminViewComponent,
@@ -7,13 +7,9 @@ import type {
SanitizedGlobalConfig,
} from 'payload/types'
import { isEntityHidden } from 'payload/utilities'
import React from 'react'
import { APIView as DefaultAPIView } from '../API/index.js'
import { EditView as DefaultEditView } from '../Edit/index.js'
import { LivePreviewView as DefaultLivePreviewView } from '../LivePreview/index.js'
import { NotFoundClient } from '../NotFound/index.client.js'
import { Unauthorized } from '../Unauthorized/index.js'
import { VersionView as DefaultVersionView } from '../Version/index.js'
import { VersionsView as DefaultVersionsView } from '../Versions/index.js'
@@ -26,7 +22,6 @@ export const getViewsFromConfig = ({
docPermissions,
globalConfig,
routeSegments,
user,
}: {
collectionConfig?: SanitizedCollectionConfig
config: SanitizedConfig
@@ -34,7 +29,6 @@ export const getViewsFromConfig = ({
docPermissions: CollectionPermission | GlobalPermission
globalConfig?: SanitizedGlobalConfig
routeSegments: string[]
user: User
}): {
CustomView: EditViewComponent
DefaultView: EditViewComponent
@@ -74,14 +68,6 @@ export const getViewsFromConfig = ({
const [collectionEntity, collectionSlug, segment3, segment4, segment5, ...remainingSegments] =
routeSegments
const {
admin: { hidden },
} = collectionConfig
if (isEntityHidden({ hidden, user })) {
return null
}
// `../:id`, or `../create`
switch (routeSegments.length) {
case 3: {
@@ -151,9 +137,6 @@ export const getViewsFromConfig = ({
currentRoute,
views,
})
if (!CustomView) ErrorView = () => <NotFoundClient />
break
}
}
@@ -183,8 +166,6 @@ export const getViewsFromConfig = ({
currentRoute,
views,
})
if (!CustomView) ErrorView = () => <NotFoundClient />
}
break
}
@@ -204,14 +185,6 @@ export const getViewsFromConfig = ({
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [globalEntity, globalSlug, segment3, ...remainingSegments] = routeSegments
const {
admin: { hidden },
} = globalConfig
if (isEntityHidden({ hidden, user })) {
return null
}
switch (routeSegments.length) {
case 2: {
if (docPermissions?.read?.permission) {
@@ -285,8 +258,6 @@ export const getViewsFromConfig = ({
currentRoute,
views,
})
if (!CustomView) ErrorView = () => <NotFoundClient />
}
break
}

View File

@@ -9,13 +9,12 @@ import { RenderCustomComponent } from '@payloadcms/ui/elements/RenderCustomCompo
import { DocumentInfoProvider } from '@payloadcms/ui/providers/DocumentInfo'
import { EditDepthProvider } from '@payloadcms/ui/providers/EditDepth'
import { FormQueryParamsProvider } from '@payloadcms/ui/providers/FormQueryParams'
import { notFound, redirect } from 'next/navigation.js'
import { docAccessOperation } from 'payload/operations'
import React from 'react'
import type { GenerateEditViewMetadata } from './getMetaBySegment.js'
import { NotFoundClient } from '../NotFound/index.client.js'
import { NotFoundView } from '../NotFound/index.js'
import { getMetaBySegment } from './getMetaBySegment.js'
import { getViewsFromConfig } from './getViewsFromConfig.js'
@@ -39,12 +38,13 @@ export const Document: React.FC<AdminViewProps> = async ({
payload: {
config,
config: {
routes: { api: apiRoute },
routes: { admin: adminRoute, api: apiRoute },
serverURL,
},
},
user,
},
visibleEntities,
} = initPageResult
const segments = Array.isArray(params?.segments) ? params.segments : []
@@ -56,18 +56,18 @@ export const Document: React.FC<AdminViewProps> = async ({
let ViewOverride: EditViewComponent
let CustomView: EditViewComponent
let DefaultView: EditViewComponent
let ErrorView: AdminViewComponent = NotFoundView
let ErrorView: AdminViewComponent
/**
let data: DocumentType
let preferencesKey: string
let fields: Field[] **/
let docPermissions: DocumentPermissions
let hasSavePermission: boolean
let apiURL: string
let action: string
if (collectionConfig) {
if (!visibleEntities?.collections?.find((visibleSlug) => visibleSlug === collectionSlug)) {
notFound()
}
try {
docPermissions = await docAccessOperation({
id,
@@ -77,11 +77,9 @@ export const Document: React.FC<AdminViewProps> = async ({
req,
})
} catch (error) {
return <NotFoundClient />
notFound()
}
/**
fields = collectionConfig.fields **/
action = `${serverURL}${apiRoute}/${collectionSlug}${isEditing ? `/${id}` : ''}`
hasSavePermission =
@@ -101,7 +99,6 @@ export const Document: React.FC<AdminViewProps> = async ({
config,
docPermissions,
routeSegments: segments,
user,
})
CustomView = collectionViews?.CustomView
@@ -110,39 +107,22 @@ export const Document: React.FC<AdminViewProps> = async ({
}
if (!CustomView && !DefaultView && !ViewOverride) {
return <ErrorView initPageResult={initPageResult} searchParams={searchParams} />
}
/**
if (id) {
try {
data = await payload.findByID({
id,
collection: collectionSlug,
depth: 0,
draft: true,
fallbackLocale: null,
locale: locale.code,
overrideAccess: false,
user,
})
} catch (error) {} // eslint-disable-line no-empty
if (!data) {
return <NotFoundClient />
if (ErrorView) {
return <ErrorView initPageResult={initPageResult} searchParams={searchParams} />
}
preferencesKey = `collection-${collectionSlug}-${id}`
notFound()
}
**/
}
if (globalConfig) {
if (!visibleEntities?.globals?.find((visibleSlug) => visibleSlug === globalSlug)) {
notFound()
}
docPermissions = permissions?.globals?.[globalSlug]
hasSavePermission = isEditing && docPermissions?.update?.permission
action = `${serverURL}${apiRoute}/globals/${globalSlug}`
/**
fields = globalConfig.fields **/
apiURL = `${serverURL}${apiRoute}/${globalSlug}?locale=${locale.code}${
globalConfig.versions?.drafts ? '&draft=true' : ''
@@ -157,7 +137,6 @@ export const Document: React.FC<AdminViewProps> = async ({
docPermissions,
globalConfig,
routeSegments: segments,
user,
})
CustomView = globalViews?.CustomView
@@ -165,51 +144,43 @@ export const Document: React.FC<AdminViewProps> = async ({
ErrorView = globalViews?.ErrorView
if (!CustomView && !DefaultView && !ViewOverride) {
return <ErrorView initPageResult={initPageResult} searchParams={searchParams} />
if (ErrorView) {
return <ErrorView initPageResult={initPageResult} searchParams={searchParams} />
}
notFound()
}
/**
try {
data = await payload.findGlobal({
slug: globalSlug,
depth: 0,
draft: true,
fallbackLocale: null,
locale: locale.code,
overrideAccess: false,
user,
})
} catch (error) {} // eslint-disable-line no-empty
if (!data) {
return <NotFoundClient />
}
preferencesKey = `global-${globalSlug}` **/
}
}
/**
const { docs: [{ value: docPreferences } = { value: null }] = [] } = (await payload.find({
collection: 'payload-preferences',
depth: 0,
limit: 1,
where: {
key: {
equals: preferencesKey,
},
},
})) as any as { docs: { value: DocumentPreferences }[] } // eslint-disable-line @typescript-eslint/no-explicit-any
const initialState = await buildStateFromSchema({
id,
data: data || {},
fieldSchema: formatFields(fields, isEditing),
operation: isEditing ? 'update' : 'create',
preferences: docPreferences,
req,
})
* Handle case where autoSave is enabled and the document is being created
* => create document and redirect
*/
const shouldAutosave =
hasSavePermission &&
((collectionConfig?.versions?.drafts && collectionConfig?.versions?.drafts?.autosave) ||
(globalConfig?.versions?.drafts && globalConfig?.versions?.drafts?.autosave))
if (shouldAutosave && !id && collectionSlug) {
const doc = await payload.create({
collection: collectionSlug,
data: {},
depth: 0,
draft: true,
fallbackLocale: null,
locale: locale.code,
req,
user,
})
if (doc?.id) {
const redirectURL = `${serverURL}${adminRoute}/collections/${collectionSlug}/${doc.id}`
redirect(redirectURL)
} else {
notFound()
}
}
const viewComponentProps: ServerSideEditViewProps = {
initPageResult,
@@ -228,17 +199,6 @@ export const Document: React.FC<AdminViewProps> = async ({
globalSlug={globalConfig?.slug}
hasSavePermission={hasSavePermission}
id={id}
/**
initialData={data}
initialState={initialState}
title={formatDocTitle({
collectionConfig,
data,
dateFormat: config.admin.dateFormat,
fallback: id?.toString(),
globalConfig,
i18n,
})} **/
isEditing={isEditing}
>
{!ViewOverride && (

View File

@@ -41,7 +41,6 @@ export const DefaultEditView: React.FC = () => {
action,
apiURL,
collectionSlug,
data,
disableActions,
disableLeaveWithoutSaving,
docPermissions,
@@ -50,6 +49,7 @@ export const DefaultEditView: React.FC = () => {
getVersions,
globalSlug,
hasSavePermission,
initialData: data,
initialState,
isEditing,
onSave: onSaveFromContext,
@@ -59,7 +59,7 @@ export const DefaultEditView: React.FC = () => {
const config = useConfig()
const router = useRouter()
const { dispatchFormQueryParams } = useFormQueryParams()
const { getComponentMap, getFieldMap } = useComponentMap()
const { getFieldMap } = useComponentMap()
const params = useSearchParams()
const depth = useEditDepth()
const { reportUpdate } = useDocumentEvents()
@@ -74,8 +74,6 @@ export const DefaultEditView: React.FC = () => {
const locale = params.get('locale')
const componentMap = getComponentMap({ collectionSlug, globalSlug })
const collectionConfig =
collectionSlug && collections.find((collection) => collection.slug === collectionSlug)
@@ -122,19 +120,19 @@ export const DefaultEditView: React.FC = () => {
...json,
operation: id ? 'update' : 'create',
})
}
if (!isEditing && depth < 2) {
// Redirect to the same locale if it's been set
const redirectRoute = `${adminRoute}/collections/${collectionSlug}/${json?.doc?.id}${locale ? `?locale=${locale}` : ''}`
router.push(redirectRoute)
} else {
if (!isEditing) {
// Redirect to the same locale if it's been set
const redirectRoute = `${adminRoute}/collections/${collectionSlug}/${json?.doc?.id}${locale ? `?locale=${locale}` : ''}`
router.push(redirectRoute)
} else {
dispatchFormQueryParams({
type: 'SET',
params: {
uploadEdits: null,
},
})
}
dispatchFormQueryParams({
type: 'SET',
params: {
uploadEdits: null,
},
})
}
},
[
@@ -144,6 +142,7 @@ export const DefaultEditView: React.FC = () => {
id,
entitySlug,
user,
depth,
collectionSlug,
getVersions,
getDocPermissions,
@@ -177,14 +176,13 @@ export const DefaultEditView: React.FC = () => {
[serverURL, apiRoute, id, operation, entitySlug, collectionSlug, globalSlug, getDocPreferences],
)
const RegisterGetThumbnailFunction = componentMap?.[`${collectionSlug}.adminThumbnail`]
return (
<main className={classes}>
<OperationProvider operation={operation}>
<Form
action={action}
className={`${baseClass}__form`}
disableValidationOnSubmit
disabled={!hasSavePermission}
initialState={initialState}
method={id ? 'PATCH' : 'POST'}
@@ -236,6 +234,7 @@ export const DefaultEditView: React.FC = () => {
<Auth
className={`${baseClass}__auth`}
collectionSlug={collectionConfig.slug}
disableLocalStrategy={collectionConfig.auth?.disableLocalStrategy}
email={data?.email}
operation={operation}
readOnly={!hasSavePermission}
@@ -246,7 +245,6 @@ export const DefaultEditView: React.FC = () => {
)}
{upload && (
<React.Fragment>
{RegisterGetThumbnailFunction && <RegisterGetThumbnailFunction />}
<Upload
collectionSlug={collectionConfig.slug}
initialState={initialState}

View File

@@ -37,7 +37,7 @@ const baseClass = 'collection-list'
const Link = (LinkImport.default || LinkImport) as unknown as typeof LinkImport.default
export const DefaultListView: React.FC = () => {
const { Header, collectionSlug, hasCreatePermission, newDocumentURL, titleField } = useListInfo()
const { Header, collectionSlug, hasCreatePermission, newDocumentURL } = useListInfo()
const { data, defaultLimit, handlePageChange, handlePerPageChange } = useListQuery()
const { searchParams } = useSearchParams()
@@ -47,8 +47,15 @@ export const DefaultListView: React.FC = () => {
const componentMap = getComponentMap({ collectionSlug }) as CollectionComponentMap
const { AfterList, AfterListTable, BeforeList, BeforeListTable, actionsMap, fieldMap } =
componentMap || {}
const {
AfterList,
AfterListTable,
BeforeList,
BeforeListTable,
Description,
actionsMap,
fieldMap,
} = componentMap || {}
const collectionConfig = config.collections.find(
(collection) => collection.slug === collectionSlug,
@@ -106,19 +113,11 @@ export const DefaultListView: React.FC = () => {
{!smallBreak && (
<ListSelection label={getTranslation(collectionConfig.labels.plural, i18n)} />
)}
{/* {description && (
<div className={`${baseClass}__sub-header`}>
<ViewDescription description={description} />
</div>
)} */}
{Description && <div className={`${baseClass}__sub-header`}>{Description}</div>}
</Fragment>
)}
</header>
<ListControls
collectionConfig={collectionConfig}
fieldMap={fieldMap}
titleField={titleField}
/>
<ListControls collectionConfig={collectionConfig} fieldMap={fieldMap} />
{BeforeListTable}
{!data.docs && (
<StaggeredShimmers

View File

@@ -8,7 +8,7 @@ import { ListQueryProvider } from '@payloadcms/ui/providers/ListQuery'
import { notFound } from 'next/navigation.js'
import { createClientCollectionConfig } from 'payload/config'
import { type AdminViewProps } from 'payload/types'
import { isEntityHidden, isNumber, mergeListSearchAndWhere } from 'payload/utilities'
import { isNumber, mergeListSearchAndWhere } from 'payload/utilities'
import React, { Fragment } from 'react'
import type { DefaultListViewProps, ListPreferences } from './Default/types.js'
@@ -29,6 +29,7 @@ export const ListView: React.FC<AdminViewProps> = async ({ initPageResult, searc
query,
user,
},
visibleEntities,
} = initPageResult
const collectionSlug = collectionConfig?.slug
@@ -62,10 +63,10 @@ export const ListView: React.FC<AdminViewProps> = async ({ initPageResult, searc
if (collectionConfig) {
const {
admin: { components: { views: { List: CustomList } = {} } = {}, hidden },
admin: { components: { views: { List: CustomList } = {} } = {} },
} = collectionConfig
if (isEntityHidden({ hidden, user })) {
if (!visibleEntities.collections.includes(collectionSlug)) {
return notFound()
}

View File

@@ -56,13 +56,13 @@ const PreviewView: React.FC<Props> = ({
action,
apiURL,
collectionSlug,
data,
disableActions,
disableLeaveWithoutSaving,
docPermissions,
getDocPreferences,
globalSlug,
hasSavePermission,
initialData,
initialState,
onSave: onSaveFromProps,
} = useDocumentInfo()
@@ -145,6 +145,8 @@ const PreviewView: React.FC<Props> = ({
globalLabel={globalConfig?.label}
globalSlug={globalSlug}
id={id}
pluralLabel={collectionConfig ? collectionConfig?.labels?.plural : undefined}
useAsTitle={collectionConfig ? collectionConfig?.admin?.useAsTitle : undefined}
view={t('general:livePreview')}
/>
<SetDocumentTitle
@@ -155,7 +157,7 @@ const PreviewView: React.FC<Props> = ({
/>
<DocumentControls
apiURL={apiURL}
data={data}
data={initialData}
disableActions={disableActions}
hasSavePermission={hasSavePermission}
id={id}

View File

@@ -1,19 +1,61 @@
import type { AdminViewComponent } from 'payload/types'
import type { I18n } from '@payloadcms/translations'
import type { Metadata } from 'next'
import type { SanitizedConfig } from 'payload/types'
import { HydrateClientUser } from '@payloadcms/ui/elements/HydrateClientUser'
import { DefaultTemplate } from '@payloadcms/ui/templates/Default'
import React from 'react'
import React, { Fragment } from 'react'
import { initPage } from '../../utilities/initPage.js'
import { NotFoundClient } from './index.client.js'
export const NotFoundView: AdminViewComponent = ({ initPageResult }) => {
export const generatePageMetadata = async ({
i18n,
}: {
config: SanitizedConfig
i18n: I18n
params?: { [key: string]: string | string[] }
//eslint-disable-next-line @typescript-eslint/require-await
}): Promise<Metadata> => {
return {
title: i18n.t('general:notFound'),
}
}
export type GenerateViewMetadata = (args: {
config: SanitizedConfig
i18n: I18n
params?: { [key: string]: string | string[] }
}) => Promise<Metadata>
export const NotFoundPage = async ({
config: configPromise,
searchParams,
}: {
config: Promise<SanitizedConfig>
params: {
segments: string[]
}
searchParams: {
[key: string]: string | string[]
}
}) => {
const initPageResult = await initPage({
config: configPromise,
redirectUnauthenticatedUser: true,
route: '/not-found',
searchParams,
})
return (
<DefaultTemplate
config={initPageResult?.req?.payload.config}
i18n={initPageResult?.req?.i18n}
permissions={initPageResult?.permissions}
user={initPageResult?.req?.user}
>
<NotFoundClient />
</DefaultTemplate>
<Fragment>
<HydrateClientUser permissions={initPageResult.permissions} user={initPageResult.req.user} />
<DefaultTemplate
config={initPageResult.req.payload.config}
visibleEntities={initPageResult.visibleEntities}
>
<NotFoundClient />
</DefaultTemplate>
</Fragment>
)
}

View File

@@ -54,7 +54,7 @@ export const getViewFromConfig = ({
} => {
let ViewToRender: AdminViewComponent = null
let templateClassName: string
let templateType: 'default' | 'minimal'
let templateType: 'default' | 'minimal' = 'minimal'
const initPageOptions: Parameters<typeof initPage>[0] = {
config,

View File

@@ -5,7 +5,7 @@ import type { SanitizedConfig } from 'payload/types'
import { DefaultTemplate } from '@payloadcms/ui/templates/Default'
import { MinimalTemplate } from '@payloadcms/ui/templates/Minimal'
import { notFound, redirect } from 'next/navigation.js'
import React from 'react'
import React, { Fragment } from 'react'
import { initPage } from '../../utilities/initPage.js'
import { getViewFromConfig } from './getViewFromConfig.js'
@@ -81,22 +81,16 @@ export const RootPage = async ({
<DefaultView initPageResult={initPageResult} params={params} searchParams={searchParams} />
)
if (templateType === 'minimal') {
return <MinimalTemplate className={templateClassName}>{RenderedView}</MinimalTemplate>
}
if (templateType === 'default') {
return (
<DefaultTemplate
config={config}
i18n={initPageResult.req.i18n}
permissions={initPageResult.permissions}
user={initPageResult.req.user}
>
{RenderedView}
</DefaultTemplate>
)
}
return RenderedView
return (
<Fragment>
{templateType === 'minimal' && (
<MinimalTemplate className={templateClassName}>{RenderedView}</MinimalTemplate>
)}
{templateType === 'default' && (
<DefaultTemplate config={config} visibleEntities={initPageResult.visibleEntities}>
{RenderedView}
</DefaultTemplate>
)}
</Fragment>
)
}

View File

@@ -5,6 +5,7 @@ import type { Where } from 'payload/types'
import { ReactSelect } from '@payloadcms/ui/elements/ReactSelect'
import { fieldBaseClass } from '@payloadcms/ui/fields/shared'
import { useConfig } from '@payloadcms/ui/providers/Config'
import { useDocumentInfo } from '@payloadcms/ui/providers/DocumentInfo'
import { useTranslation } from '@payloadcms/ui/providers/Translation'
import { formatDate } from '@payloadcms/ui/utilities/formatDate'
import qs from 'qs'
@@ -28,6 +29,7 @@ export const SelectComparison: React.FC<Props> = (props) => {
admin: { dateFormat },
} = useConfig()
const { docConfig } = useDocumentInfo()
const [options, setOptions] = useState(baseOptions)
const [lastLoadedPage, setLastLoadedPage] = useState(1)
const [errorLoading, setErrorLoading] = useState('')
@@ -51,15 +53,18 @@ export const SelectComparison: React.FC<Props> = (props) => {
not_equals: versionID,
},
},
{
latest: {
not_equals: true,
},
},
],
},
}
if (docConfig.versions?.drafts) {
query.where.and.push({
latest: {
not_equals: true,
},
})
}
if (parentID) {
query.where.and.push({
parent: {
@@ -79,7 +84,6 @@ export const SelectComparison: React.FC<Props> = (props) => {
if (response.ok) {
const data: PaginatedDocs = await response.json()
if (data.docs.length > 0) {
setOptions((existingOptions) => [
...existingOptions,
@@ -98,7 +102,7 @@ export const SelectComparison: React.FC<Props> = (props) => {
setErrorLoading(t('error:unspecific'))
}
},
[dateFormat, baseURL, parentID, versionID, t, i18n],
[dateFormat, baseURL, parentID, versionID, t, i18n, docConfig.versions?.drafts],
)
useEffect(() => {

View File

@@ -15,52 +15,65 @@ import { IDCell } from './cells/ID/index.js'
export const buildVersionColumns = ({
collectionConfig,
config,
docID,
globalConfig,
i18n: { t },
i18n,
}: {
collectionConfig?: SanitizedCollectionConfig
config: SanitizedConfig
docID?: number | string
globalConfig?: SanitizedGlobalConfig
i18n: I18n
}): Column[] => [
{
name: '',
accessor: 'updatedAt',
active: true,
components: {
Cell: (
<CreatedAtCell
collectionSlug={collectionConfig?.slug}
docID={docID}
globalSlug={globalConfig?.slug}
/>
),
Heading: <SortColumn label={t('general:updatedAt')} name="updatedAt" />,
}): Column[] => {
const entityConfig = collectionConfig || globalConfig
const columns: Column[] = [
{
name: '',
type: 'date',
Label: '',
accessor: 'updatedAt',
active: true,
components: {
Cell: (
<CreatedAtCell
collectionSlug={collectionConfig?.slug}
docID={docID}
globalSlug={globalConfig?.slug}
/>
),
Heading: <SortColumn Label={t('general:updatedAt')} name="updatedAt" />,
},
},
label: '',
},
{
name: '',
accessor: 'id',
active: true,
components: {
Cell: <IDCell />,
Heading: <SortColumn disable label={t('version:versionID')} name="id" />,
{
name: '',
type: 'text',
Label: '',
accessor: 'id',
active: true,
components: {
Cell: <IDCell />,
Heading: <SortColumn Label={t('version:versionID')} disable name="id" />,
},
},
label: '',
},
{
name: '',
accessor: 'autosave',
active: true,
components: {
Cell: <AutosaveCell />,
Heading: <SortColumn disable label={t('version:type')} name="autosave" />,
},
label: '',
},
]
]
if (
entityConfig?.versions?.drafts ||
(entityConfig?.versions?.drafts && entityConfig.versions.drafts?.autosave)
) {
columns.push({
name: '',
type: 'checkbox',
Label: '',
accessor: '_status',
active: true,
components: {
Cell: <AutosaveCell />,
Heading: <SortColumn Label={t('version:type')} disable name="autosave" />,
},
})
}
return columns
}

View File

@@ -8,15 +8,11 @@ export const AutosaveCell: React.FC = () => {
const { t } = useTranslation()
const { rowData } = useTableCell()
return (
<Fragment>
{rowData?.autosave && (
<React.Fragment>
<Pill>
Autosave
{t('version:autosave')}
</Pill>
<Pill>{t('version:autosave')}</Pill>
&nbsp;&nbsp;
</React.Fragment>
)}

View File

@@ -1,51 +0,0 @@
module.exports = {
verbose: true,
git: {
commitMessage: 'chore(release): v${version}',
requireCleanWorkingDir: false,
tagMatch: 'v*', // payload is tagged normally, other packages are tagged with a prefix
},
github: {
release: true,
},
npm: {
skipChecks: true,
},
hooks: {
'before:init': ['pnpm install', 'pnpm clean', 'pnpm build'], // Assume tests have already been run
},
plugins: {
'@release-it/conventional-changelog': {
infile: '../../CHANGELOG.md',
preset: {
name: 'conventionalcommits',
types: [
{ type: 'feat', section: 'Features' },
{ type: 'feature', section: 'Features' },
{ type: 'fix', section: 'Bug Fixes' },
{ type: 'docs', section: 'Documentation' },
],
},
writerOpts: {
commitGroupsSort: (a, b) => {
const groupOrder = ['Features', 'Bug Fixes', 'Documentation']
return groupOrder.indexOf(a.title) - groupOrder.indexOf(b.title)
},
// Scoped commits at the end, alphabetical sort
commitsSort: (a, b) => {
if (a.scope || b.scope) {
if (!a.scope) return -1
if (!b.scope) return 1
return a.scope === b.scope
? a.subject.localeCompare(b.subject)
: a.scope.localeCompare(b.scope)
}
// Alphabetical sort
return a.subject.localeCompare(b.subject)
},
},
},
},
}

View File

@@ -3,7 +3,7 @@
"version": "3.0.0-alpha.49",
"description": "Node, React and MongoDB Headless CMS and Application Framework",
"license": "MIT",
"main": "./src/index.js",
"main": "./src/index.ts",
"types": "./src/index.ts",
"type": "module",
"bin": {
@@ -35,16 +35,13 @@
"lint": "eslint \"src/**/*.ts\"",
"prepublishOnly": "pnpm clean && pnpm turbo build",
"pretest": "pnpm build",
"release:beta": "release-it pre --preReleaseId=beta --npm.tag=beta --config .release-it.pre.js",
"release:major": "release-it major",
"release:minor": "release-it minor",
"release:patch": "release-it patch",
"translateNewKeys": "ts-node -T ./scripts/translateNewKeys.ts"
},
"dependencies": {
"@payloadcms/translations": "workspace:*",
"@swc-node/core": "^1.13.0",
"@swc-node/sourcemap-support": "^0.5.0",
"@types/probe-image-size": "^7.2.4",
"bson-objectid": "2.0.4",
"conf": "10.2.0",
"console-table-printer": "2.11.2",
@@ -55,7 +52,6 @@
"find-up": "4.1.0",
"get-tsconfig": "^4.7.2",
"http-status": "1.6.2",
"image-size": "^1.1.1",
"joi": "^17.12.1",
"json-schema-to-typescript": "11.0.3",
"jsonwebtoken": "9.0.1",
@@ -67,6 +63,7 @@
"pino-pretty": "10.2.0",
"pirates": "^4.0.6",
"pluralize": "8.0.0",
"probe-image-size": "^7.2.3",
"sanitize-filename": "1.6.3",
"scheduler": "0.23.0",
"scmp": "2.1.0"
@@ -74,7 +71,6 @@
"devDependencies": {
"@monaco-editor/react": "4.5.1",
"@payloadcms/eslint-config": "workspace:*",
"@release-it/conventional-changelog": "8.0.1",
"@types/asap": "2.0.0",
"@types/body-parser": "1.19.2",
"@types/compression": "1.7.2",
@@ -112,7 +108,6 @@
"object.assign": "4.1.4",
"object.entries": "1.1.6",
"passport-strategy": "1.0.0",
"release-it": "17.1.1",
"rimraf": "3.0.2",
"serve-static": "1.15.0",
"sharp": "0.32.6",
@@ -184,7 +179,8 @@
],
"repository": {
"type": "git",
"url": "https://github.com/payloadcms/payload.git"
"url": "https://github.com/payloadcms/payload.git",
"directory": "packages/payload"
},
"bugs": {
"url": "https://github.com/payloadcms/payload"

View File

@@ -1,11 +1 @@
export type CustomPreviewButtonProps = React.ComponentType<
DefaultPreviewButtonProps & {
DefaultButton: React.ComponentType<DefaultPreviewButtonProps>
}
>
export type DefaultPreviewButtonProps = {
disabled: boolean
label: string
preview: () => void
}
export type CustomPreviewButton = React.ComponentType

View File

@@ -1 +1 @@
export type CustomPublishButtonProps = React.ComponentType
export type CustomPublishButton = React.ComponentType

View File

@@ -1 +1 @@
export type CustomSaveButtonProps = React.ComponentType
export type CustomSaveButton = React.ComponentType

View File

@@ -1 +1 @@
export type CustomSaveDraftButtonProps = React.ComponentType
export type CustomSaveDraftButton = React.ComponentType

View File

@@ -1,13 +1,18 @@
type Args<T = unknown> = {
value?: T
}
import type React from 'react'
export type DescriptionFunction<T = unknown> = (args: Args<T>) => string
export type DescriptionFunction = () => string
export type DescriptionComponent<T = unknown> = React.ComponentType<Args<T>>
export type DescriptionComponent = React.ComponentType<FieldDescriptionProps>
export type Description =
| DescriptionComponent
| DescriptionFunction
| Record<string, string>
| string
export type FieldDescriptionProps = {
CustomDescription?: React.ReactNode
className?: string
description?: Record<string, string> | string
marginPlacement?: 'bottom' | 'top'
}

View File

@@ -8,7 +8,6 @@ export type Data = {
export type Row = {
blockType?: string
collapsed?: boolean
errorPaths?: Set<string>
id: string
}
@@ -19,7 +18,7 @@ export type FilterOptionsResult = {
export type FormField = {
disableFormData?: boolean
errorMessage?: string
errorPaths?: Set<string>
errorPaths?: string[]
fieldSchema?: Field
filterOptions?: FilterOptionsResult
initialValue: unknown

View File

@@ -1,5 +1,6 @@
export type LabelProps = {
CustomLabel?: React.ReactNode
as?: 'label' | 'span'
htmlFor?: string
label?: Record<string, string> | false | string
required?: boolean

View File

@@ -2,11 +2,10 @@ export type { RichTextAdapter, RichTextFieldProps } from './RichText.js'
export type { CellComponentProps, DefaultCellComponentProps } from './elements/Cell.js'
export type { ConditionalDateProps } from './elements/DatePicker.js'
export type { DayPickerProps, SharedProps, TimePickerProps } from './elements/DatePicker.js'
export type { DefaultPreviewButtonProps } from './elements/PreviewButton.js'
export type { CustomPreviewButtonProps } from './elements/PreviewButton.js'
export type { CustomPublishButtonProps } from './elements/PublishButton.js'
export type { CustomSaveButtonProps } from './elements/SaveButton.js'
export type { CustomSaveDraftButtonProps } from './elements/SaveDraftButton.js'
export type { CustomPreviewButton } from './elements/PreviewButton.js'
export type { CustomPublishButton } from './elements/PublishButton.js'
export type { CustomSaveButton } from './elements/SaveButton.js'
export type { CustomSaveDraftButton } from './elements/SaveDraftButton.js'
export type {
DocumentTab,
DocumentTabComponent,
@@ -19,6 +18,7 @@ export type {
Description,
DescriptionComponent,
DescriptionFunction,
FieldDescriptionProps,
} from './forms/FieldDescription.js'
export type { Data, FilterOptionsResult, FormField, FormState, Row } from './forms/Form.js'
export type { LabelProps } from './forms/Label.js'
@@ -30,4 +30,5 @@ export type {
EditViewProps,
InitPageResult,
ServerSideEditViewProps,
VisibleEntities,
} from './views/types.js'

View File

@@ -29,6 +29,11 @@ export type EditViewProps = {
globalSlug?: string
}
export type VisibleEntities = {
collections: SanitizedCollectionConfig['slug'][]
globals: SanitizedGlobalConfig['slug'][]
}
export type InitPageResult = {
collectionConfig?: SanitizedCollectionConfig
cookies: Map<string, string>
@@ -38,6 +43,7 @@ export type InitPageResult = {
permissions: Permissions
req: PayloadRequest
translations: Translations
visibleEntities: VisibleEntities
}
export type ServerSideEditViewProps = {

View File

@@ -1,5 +1,5 @@
import type { Access, AccessResult } from '../config/types.js'
import type { PayloadRequest } from '../exports/types.js'
import type { PayloadRequest } from '../types/index.js'
import { Forbidden } from '../errors/index.js'

View File

@@ -2,6 +2,7 @@ import type { AllOperations, PayloadRequest } from '../types/index.js'
import type { Permissions } from './types.js'
import { getEntityPolicies } from '../utilities/getEntityPolicies.js'
import isolateObjectProperty from '../utilities/isolateObjectProperty.js'
type GetAccessResultsArgs = {
req: PayloadRequest
@@ -47,7 +48,9 @@ export async function getAccessResults({ req }: GetAccessResultsArgs): Promise<P
type: 'collection',
entity: collection,
operations: collectionOperations,
req,
// Do not re-use our existing req object, as we need a new req.transactionID. Cannot re-use our existing one, as this is run in parallel (Promise.all above) which is
// not supported on the same transaction ID. Not passing a transaction ID here creates a new transaction ID.
req: isolateObjectProperty(req, 'transactionID'),
})
results.collections = {
...results.collections,
@@ -68,7 +71,9 @@ export async function getAccessResults({ req }: GetAccessResultsArgs): Promise<P
type: 'global',
entity: global,
operations: globalOperations,
req,
// Do not re-use our existing req object, as we need a new req.transactionID. Cannot re-use our existing one, as this is run in parallel (Promise.all above) which is
// not supported on the same transaction ID. Not passing a transaction ID here creates a new transaction ID.
req: isolateObjectProperty(req, 'transactionID'),
})
results.globals = {
...results.globals,

View File

@@ -1,7 +1,7 @@
import crypto from 'crypto'
import type { SanitizedCollectionConfig } from '../../collections/config/types.js'
import type { Where } from '../../exports/types.js'
import type { Where } from '../../types/index.js'
import type { AuthStrategyFunction, User } from '../index.js'
export const APIKeyAuthentication =

View File

@@ -1,6 +1,6 @@
import type { ParsedArgs } from 'minimist'
import type { SanitizedConfig } from '../exports/types.js'
import type { SanitizedConfig } from '../config/types.js'
import payload from '../index.js'
import { prettySyncLoggerDestination } from '../utilities/logger.js'

View File

@@ -1,5 +1,4 @@
import type { LivePreviewConfig, ServerOnlyLivePreviewProperties } from '../../config/types.js'
import type { SanitizedCollectionConfig } from '../../exports/types.js'
export type ServerOnlyCollectionProperties = keyof Pick<
SanitizedCollectionConfig,
@@ -17,7 +16,7 @@ export type ClientCollectionConfig = Omit<
> & {
admin: Omit<
SanitizedCollectionConfig['admin'],
ServerOnlyCollectionAdminProperties & 'fields' & 'livePreview'
'fields' | 'livePreview' | ServerOnlyCollectionAdminProperties
> & {
livePreview?: Omit<LivePreviewConfig, ServerOnlyLivePreviewProperties>
}
@@ -25,6 +24,7 @@ export type ClientCollectionConfig = Omit<
}
import type { ClientFieldConfig } from '../../fields/config/client.js'
import type { SanitizedCollectionConfig } from './types.js'
import { createClientFieldConfigs } from '../../fields/config/client.js'
@@ -50,6 +50,7 @@ export const createClientCollectionConfig = (collection: SanitizedCollectionConf
if ('upload' in sanitized && typeof sanitized.upload === 'object') {
sanitized.upload = { ...sanitized.upload }
delete sanitized.upload.handlers
delete sanitized.upload.adminThumbnail
}
if ('auth' in sanitized && typeof sanitized.auth === 'object') {

View File

@@ -12,7 +12,7 @@ import { sanitizeFields } from '../../fields/config/sanitize.js'
import { fieldAffectsData } from '../../fields/config/types.js'
import mergeBaseFields from '../../fields/mergeBaseFields.js'
import { extractTranslations } from '../../translations/extractTranslations.js'
import getBaseUploadFields from '../../uploads/getBaseFields.js'
import { getBaseUploadFields } from '../../uploads/getBaseFields.js'
import { formatLabels } from '../../utilities/formatLabels.js'
import { isPlainObject } from '../../utilities/isPlainObject.js'
import baseVersionFields from '../../versions/baseFields.js'

View File

@@ -2,10 +2,10 @@ import type { GraphQLInputObjectType, GraphQLNonNull, GraphQLObjectType } from '
import type { DeepRequired } from 'ts-essentials'
import type {
CustomPreviewButtonProps,
CustomPublishButtonProps,
CustomSaveButtonProps,
CustomSaveDraftButtonProps,
CustomPreviewButton,
CustomPublishButton,
CustomSaveButton,
CustomSaveDraftButton,
} from '../../admin/types.js'
import type { Auth, ClientUser, IncomingAuthType } from '../../auth/types.js'
import type {
@@ -65,7 +65,7 @@ export type BeforeValidateHook<T extends TypeWithID = any> = (args: {
* `undefined` on 'create' operation
*/
originalDoc?: T
req?: PayloadRequest
req: PayloadRequest
}) => any
export type BeforeChangeHook<T extends TypeWithID = any> = (args: {
@@ -211,23 +211,23 @@ export type CollectionAdminOptions = {
/**
* Replaces the "Preview" button
*/
PreviewButton?: CustomPreviewButtonProps
PreviewButton?: CustomPreviewButton
/**
* Replaces the "Publish" button
* + drafts must be enabled
*/
PublishButton?: CustomPublishButtonProps
PublishButton?: CustomPublishButton
/**
* Replaces the "Save" button
* + drafts must be disabled
*/
SaveButton?: CustomSaveButtonProps
SaveButton?: CustomSaveButton
/**
* Replaces the "Save Draft" button
* + drafts must be enabled
* + autosave must be disabled
*/
SaveDraftButton?: CustomSaveDraftButtonProps
SaveDraftButton?: CustomSaveDraftButton
}
views?: {
/**

View File

@@ -36,6 +36,7 @@ export const duplicateOperation = async <TSlug extends keyof GeneratedTypes['col
incomingArgs: Arguments,
): Promise<GeneratedTypes['collections'][TSlug]> => {
let args = incomingArgs
const operation = 'create'
try {
const shouldCommit = await initTransaction(args.req)
@@ -52,7 +53,7 @@ export const duplicateOperation = async <TSlug extends keyof GeneratedTypes['col
args,
collection: args.collection.config,
context: args.req.context,
operation: 'update',
operation,
req: args.req,
})) || args
}, Promise.resolve())
@@ -63,11 +64,7 @@ export const duplicateOperation = async <TSlug extends keyof GeneratedTypes['col
depth,
draft: draftArg = true,
overrideAccess,
req: {
fallbackLocale,
payload: { config },
payload,
},
req: { fallbackLocale, locale: localeArg, payload },
req,
showHiddenFields,
} = args
@@ -107,132 +104,112 @@ export const duplicateOperation = async <TSlug extends keyof GeneratedTypes['col
if (!docWithLocales && !hasWherePolicy) throw new NotFound(req.t)
if (!docWithLocales && hasWherePolicy) throw new Forbidden(req.t)
// remove the createdAt timestamp and rely on the db to default it
// remove the createdAt timestamp and id to rely on the db to set the default it
delete docWithLocales.createdAt
delete docWithLocales.id
// for version enabled collections, override the current status with draft, unless draft is explicitly set to false
if (shouldSaveDraft) {
docWithLocales._status = 'draft'
}
// /////////////////////////////////////
// Iterate locales of document and call the db create or update functions
// /////////////////////////////////////
let locales = [undefined]
if (config.localization) {
// make sure the current request locale is the first locale to be handled to skip validation for other locales
locales = config.localization.locales.reduce(
(acc, { code }) => {
if (req.locale === code) return acc
acc.push(code)
return acc
},
[req.locale],
)
}
let result
await locales.reduce(async (previousPromise, locale: string | undefined, i) => {
await previousPromise
const operation = i === 0 ? 'create' : 'update'
const originalDoc = await afterRead({
collection: collectionConfig,
context: req.context,
depth: 0,
doc: docWithLocales,
fallbackLocale: null,
global: null,
locale: req.locale,
overrideAccess: true,
req,
showHiddenFields: true,
})
const originalDoc = await afterRead({
collection: collectionConfig,
context: req.context,
depth: 0,
doc: docWithLocales,
fallbackLocale: null,
global: null,
locale,
overrideAccess: true,
req,
showHiddenFields: true,
})
// /////////////////////////////////////
// Create Access
// /////////////////////////////////////
let data = { ...originalDoc }
if (!overrideAccess) {
await executeAccess({ data: originalDoc, req }, collectionConfig.access.create)
}
// /////////////////////////////////////
// Create Access
// /////////////////////////////////////
// /////////////////////////////////////
// beforeValidate - Fields
// /////////////////////////////////////
if (operation === 'create' && !overrideAccess) {
await executeAccess({ data, req }, collectionConfig.access.create)
}
let data = await beforeValidate<DeepPartial<GeneratedTypes['collections'][TSlug]>>({
id,
collection: collectionConfig,
context: req.context,
data: originalDoc,
doc: originalDoc,
duplicate: true,
global: null,
operation,
overrideAccess,
req,
})
// /////////////////////////////////////
// beforeValidate - Fields
// /////////////////////////////////////
// /////////////////////////////////////
// beforeValidate - Collection
// /////////////////////////////////////
data = await beforeValidate<DeepPartial<GeneratedTypes['collections'][TSlug]>>({
id,
collection: collectionConfig,
context: req.context,
data,
doc: originalDoc,
duplicate: true,
global: null,
operation,
overrideAccess,
req,
})
await collectionConfig.hooks.beforeValidate.reduce(async (priorHook, hook) => {
await priorHook
// /////////////////////////////////////
// beforeValidate - Collection
// /////////////////////////////////////
await collectionConfig.hooks.beforeValidate.reduce(async (priorHook, hook) => {
await priorHook
data =
(await hook({
collection: collectionConfig,
context: req.context,
data,
operation,
originalDoc,
req,
})) || result
}, Promise.resolve())
// /////////////////////////////////////
// beforeChange - Collection
// /////////////////////////////////////
await collectionConfig.hooks.beforeChange.reduce(async (priorHook, hook) => {
await priorHook
data =
(await hook({
collection: collectionConfig,
context: req.context,
data,
operation,
originalDoc: result,
req,
})) || result
}, Promise.resolve())
// /////////////////////////////////////
// beforeChange - Fields
// /////////////////////////////////////
result = await beforeChange<GeneratedTypes['collections'][TSlug]>({
id,
collection: collectionConfig,
context: req.context,
data,
doc: originalDoc,
docWithLocales,
global: null,
operation,
req,
skipValidation: shouldSaveDraft || operation === 'update',
})
data =
(await hook({
collection: collectionConfig,
context: req.context,
data,
operation,
originalDoc,
req,
})) || result
}, Promise.resolve())
// /////////////////////////////////////
// beforeChange - Collection
// /////////////////////////////////////
await collectionConfig.hooks.beforeChange.reduce(async (priorHook, hook) => {
await priorHook
data =
(await hook({
collection: collectionConfig,
context: req.context,
data,
operation,
originalDoc: result,
req,
})) || result
}, Promise.resolve())
// /////////////////////////////////////
// beforeChange - Fields
// /////////////////////////////////////
result = await beforeChange<GeneratedTypes['collections'][TSlug]>({
id,
collection: collectionConfig,
context: req.context,
data,
doc: originalDoc,
docWithLocales,
duplicate: true,
global: null,
operation,
req,
skipValidation: shouldSaveDraft,
})
// set req.locale back to the original locale
req.locale = localeArg
// /////////////////////////////////////
// Create / Update
// /////////////////////////////////////
@@ -272,7 +249,7 @@ export const duplicateOperation = async <TSlug extends keyof GeneratedTypes['col
doc: versionDoc,
fallbackLocale,
global: null,
locale: req.locale,
locale: localeArg,
overrideAccess,
req,
showHiddenFields,
@@ -304,7 +281,7 @@ export const duplicateOperation = async <TSlug extends keyof GeneratedTypes['col
data: versionDoc,
doc: result,
global: null,
operation: 'create',
operation,
previousDoc: {},
req,
})
@@ -321,7 +298,7 @@ export const duplicateOperation = async <TSlug extends keyof GeneratedTypes['col
collection: collectionConfig,
context: req.context,
doc: result,
operation: 'create',
operation,
previousDoc: {},
req,
})) || result
@@ -334,7 +311,7 @@ export const duplicateOperation = async <TSlug extends keyof GeneratedTypes['col
result = await buildAfterOperation<GeneratedTypes['collections'][TSlug]>({
args,
collection: collectionConfig,
operation: 'create',
operation,
result,
})

View File

@@ -1,6 +1,6 @@
import type { MarkOptional } from 'ts-essentials'
import type { GeneratedTypes } from '../../..//index.js'
import type { GeneratedTypes } from '../../../index.js'
import type { Payload } from '../../../index.js'
import type { Document, PayloadRequest, RequestContext } from '../../../types/index.js'
import type { File } from '../../../uploads/types.js'

View File

@@ -1,7 +1,7 @@
import type { forgotPasswordOperation } from '../../auth/operations/forgotPassword.js'
import type { loginOperation } from '../../auth/operations/login.js'
import type { refreshOperation } from '../../auth/operations/refresh.js'
import type { PayloadRequest } from '../../exports/types.js'
import type { PayloadRequest } from '../../types/index.js'
import type { AfterOperationHook, SanitizedCollectionConfig, TypeWithID } from '../config/types.js'
import type { createOperation } from './create.js'
import type { deleteOperation } from './delete.js'

View File

@@ -1,6 +1,7 @@
import type { ClientCollectionConfig } from '../collections/config/client.js'
import type { SanitizedCollectionConfig, SanitizedGlobalConfig } from '../exports/types.js'
import type { SanitizedCollectionConfig } from '../collections/config/types.js'
import type { ClientGlobalConfig } from '../globals/config/client.js'
import type { SanitizedGlobalConfig } from '../globals/config/types.js'
import type {
LivePreviewConfig,
SanitizedConfig,

View File

@@ -711,8 +711,12 @@ export type EditConfig =
)
| EditViewComponent
export type EntityDescriptionComponent = React.ComponentType<any>
export type EntityDescriptionFunction = () => string
export type EntityDescription =
| (() => string)
| React.ComponentType<any>
| EntityDescriptionComponent
| EntityDescriptionFunction
| Record<string, string>
| string

View File

@@ -6,6 +6,7 @@ import type { EntityPolicies, PathToQuery } from './types.js'
import { fieldAffectsData } from '../../fields/config/types.js'
import { getEntityPolicies } from '../../utilities/getEntityPolicies.js'
import isolateObjectProperty from '../../utilities/isolateObjectProperty.js'
import { getLocalizedPaths } from '../getLocalizedPaths.js'
import { validateQueryPaths } from './validateQueryPaths.js'
@@ -89,7 +90,7 @@ export async function validateSearchParam({
type: 'collection',
entity: req.payload.collections[collectionSlug].config,
operations: ['read'],
req,
req: isolateObjectProperty(req, 'transactionID'),
})
}

View File

@@ -39,7 +39,15 @@ export type {
} from './../collections/config/types.js'
export type { ClientConfig } from './../config/client.js'
export type { Access, AccessArgs, EditViewComponent, SanitizedConfig } from './../config/types.js'
export type {
Access,
AccessArgs,
EditViewComponent,
EntityDescription,
EntityDescriptionComponent,
EntityDescriptionFunction,
SanitizedConfig,
} from './../config/types.js'
export type { ClientFieldConfig } from './../fields/config/client.js'
export type {
ArrayField,

View File

@@ -34,6 +34,7 @@ export { isEntityHidden } from '../utilities/isEntityHidden.js'
export { isNumber } from '../utilities/isNumber.js'
export { isPlainObject } from '../utilities/isPlainObject.js'
export { isPlainFunction, isReactComponent } from '../utilities/isReactComponent.js'
export { isValidID } from '../utilities/isValidID.js'
export { default as isolateObjectProperty } from '../utilities/isolateObjectProperty.js'

View File

@@ -1,21 +1,30 @@
import ObjectIdImport from 'bson-objectid'
import type { Field, FieldHook } from '../config/types.js'
import type { Field } from '../config/types.js'
const ObjectId = (ObjectIdImport.default ||
ObjectIdImport) as unknown as typeof ObjectIdImport.default
const generateID: FieldHook = ({ operation, value }) =>
(operation !== 'create' ? value : false) || new ObjectId().toHexString()
export const baseIDField: Field = {
name: 'id',
type: 'text',
admin: {
disabled: true,
hidden: true,
},
defaultValue: () => new ObjectId().toHexString(),
hooks: {
beforeChange: [generateID],
beforeChange: [
({ operation, value }) => {
// If creating new doc, need to disregard any
// ids that have been passed in because they will cause
// primary key unique conflicts in relational DBs
if (!value || (operation === 'create' && value)) {
return new ObjectId().toHexString()
}
return value
},
],
},
label: 'ID',
}

View File

@@ -112,7 +112,13 @@ export const number = baseField.keys({
placeholder: joi.string(),
step: joi.number(),
}),
defaultValue: joi.alternatives().try(joi.number(), joi.func()),
defaultValue: joi
.alternatives()
.try(
joi.number(),
joi.func(),
joi.array().when('hasMany', { not: true, then: joi.forbidden() }),
),
hasMany: joi.boolean().default(false),
max: joi.number(),
maxRows: joi.number().when('hasMany', { is: joi.not(true), then: joi.forbidden() }),
@@ -438,7 +444,12 @@ export const blocks = baseField.keys({
export const richText = baseField.keys({
name: joi.string().required(),
type: joi.string().valid('richText').required(),
admin: baseAdminFields.default(),
admin: baseAdminFields.keys({
components: baseAdminComponentFields.keys({
Error: componentSchema,
Label: componentSchema,
}),
}),
defaultValue: joi.alternatives().try(joi.array().items(joi.object()), joi.func(), joi.object()),
editor: joi
.object()

View File

@@ -382,7 +382,16 @@ export type NamedTab = TabBase & {
export type UnnamedTab = Omit<TabBase, 'name'> & {
interfaceName?: never
label: Record<string, string> | string
/**
* Can be either:
* - A string, which will be used as the tab's label.
* - An object, where the key is the language code and the value is the label.
*/
label:
| {
[selectedLanguage: string]: string
}
| string
localized?: never
}
@@ -550,7 +559,12 @@ export type RichTextField<
AdapterProps = any,
ExtraProperties = object,
> = FieldBase & {
admin?: Admin
admin?: Admin & {
components?: {
Error?: React.ComponentType<ErrorProps>
Label?: React.ComponentType<LabelProps>
}
}
editor?: RichTextAdapter<Value, AdapterProps, AdapterProps>
type: 'richText'
} & ExtraProperties

View File

@@ -1,5 +1,7 @@
import type { User } from '../auth/index.js'
import { deepCopyObject } from '../utilities/deepCopyObject.js'
type Args = {
defaultValue: unknown
locale: string | undefined
@@ -7,12 +9,7 @@ type Args = {
value?: unknown
}
const getValueWithDefault = async ({
defaultValue,
locale,
user,
value,
}: Args): Promise<unknown> => {
const getValueWithDefault = ({ defaultValue, locale, user, value }: Args): unknown => {
if (typeof value !== 'undefined') {
return value
}
@@ -21,7 +18,12 @@ const getValueWithDefault = async ({
return defaultValue({ locale, user })
}
if (typeof defaultValue === 'object') {
return deepCopyObject(defaultValue)
}
return defaultValue
}
// eslint-disable-next-line no-restricted-exports
export default getValueWithDefault

View File

@@ -16,6 +16,10 @@ type Args<T> = {
req: PayloadRequest
}
/**
* This function is responsible for the following actions, in order:
* - Execute field hooks
*/
export const afterChange = async <T extends Record<string, unknown>>({
collection,
context,

View File

@@ -21,6 +21,16 @@ type Args = {
showHiddenFields: boolean
}
/**
* This function is responsible for the following actions, in order:
* - Remove hidden fields from response
* - Flatten locales into requested locale
* - Sanitize outgoing data (point field, etc.)
* - Execute field hooks
* - Execute read access control
* - Populate relationships
*/
export async function afterRead<T = any>(args: Args): Promise<T> {
const {
collection,

View File

@@ -0,0 +1,7 @@
import type { FieldHookArgs } from '../../config/types.js'
export const beforeDuplicate = async (args: FieldHookArgs) =>
await args.field.hooks.beforeDuplicate.reduce(async (priorHook, currentHook) => {
await priorHook
return await currentHook(args)
}, Promise.resolve())

View File

@@ -12,6 +12,7 @@ type Args<T> = {
data: Record<string, unknown> | T
doc: Record<string, unknown> | T
docWithLocales: Record<string, unknown>
duplicate?: boolean
global: SanitizedGlobalConfig | null
id?: number | string
operation: Operation
@@ -19,6 +20,15 @@ type Args<T> = {
skipValidation?: boolean
}
/**
* This function is responsible for the following actions, in order:
* - Run condition
* - Execute field hooks
* - Validate data
* - Transform data for storage
* - beforeDuplicate hooks (if duplicate)
* - Unflatten locales
*/
export const beforeChange = async <T extends Record<string, unknown>>({
id,
collection,
@@ -26,6 +36,7 @@ export const beforeChange = async <T extends Record<string, unknown>>({
data: incomingData,
doc,
docWithLocales,
duplicate = false,
global,
operation,
req,
@@ -42,6 +53,7 @@ export const beforeChange = async <T extends Record<string, unknown>>({
data,
doc,
docWithLocales,
duplicate,
errors,
fields: collection?.fields || global?.fields,
global,
@@ -59,7 +71,10 @@ export const beforeChange = async <T extends Record<string, unknown>>({
throw new ValidationError(errors, req.t)
}
mergeLocaleActions.forEach((action) => action())
await mergeLocaleActions.reduce(async (priorAction, action) => {
await priorAction
await action()
}, Promise.resolve())
return data
}

View File

@@ -3,9 +3,10 @@ import merge from 'deepmerge'
import type { SanitizedCollectionConfig } from '../../../collections/config/types.js'
import type { SanitizedGlobalConfig } from '../../../globals/config/types.js'
import type { Operation, PayloadRequest, RequestContext } from '../../../types/index.js'
import type { Field, TabAsField } from '../../config/types.js'
import type { Field, FieldHookArgs, TabAsField } from '../../config/types.js'
import { fieldAffectsData, tabHasName } from '../../config/types.js'
import { beforeDuplicate } from './beforeDuplicate.js'
import { getExistingRowDoc } from './getExistingRowDoc.js'
import { traverseFields } from './traverseFields.js'
@@ -15,11 +16,12 @@ type Args = {
data: Record<string, unknown>
doc: Record<string, unknown>
docWithLocales: Record<string, unknown>
duplicate: boolean
errors: { field: string; message: string }[]
field: Field | TabAsField
global: SanitizedGlobalConfig | null
id?: number | string
mergeLocaleActions: (() => void)[]
mergeLocaleActions: (() => Promise<void>)[]
operation: Operation
path: string
req: PayloadRequest
@@ -34,6 +36,7 @@ type Args = {
// - Execute field hooks
// - Validate data
// - Transform data for storage
// - beforeDuplicate hooks (if duplicate)
// - Unflatten locales
export const promise = async ({
@@ -43,6 +46,7 @@ export const promise = async ({
data,
doc,
docWithLocales,
duplicate,
errors,
field,
global,
@@ -59,10 +63,8 @@ export const promise = async ({
? Boolean(field.admin.condition(data, siblingData, { user: req.user }))
: true
let skipValidationFromHere = skipValidation || !passesCondition
const defaultLocale = req.payload.config?.localization
? req.payload.config.localization?.defaultLocale
: 'en'
const { localization } = req.payload.config
const defaultLocale = localization ? localization?.defaultLocale : 'en'
const operationLocale = req.locale || defaultLocale
if (fieldAffectsData(field)) {
@@ -131,17 +133,34 @@ export const promise = async ({
}
}
const beforeDuplicateArgs: FieldHookArgs = {
collection,
context,
data,
field,
global: undefined,
req,
siblingData,
value: siblingData[field.name],
}
// Push merge locale action if applicable
if (field.localized) {
mergeLocaleActions.push(() => {
if (req.payload.config.localization) {
const { localization } = req.payload.config
const localeData = localization.localeCodes.reduce((localizedValues, locale) => {
const fieldValue =
if (localization && field.localized) {
mergeLocaleActions.push(async () => {
const localeData = await localization.localeCodes.reduce(
async (localizedValuesPromise: Promise<Record<string, unknown>>, locale) => {
const localizedValues = await localizedValuesPromise
let fieldValue =
locale === req.locale
? siblingData[field.name]
: siblingDocWithLocales?.[field.name]?.[locale]
if (duplicate && field.hooks?.beforeDuplicate?.length) {
beforeDuplicateArgs.value = fieldValue
fieldValue = await beforeDuplicate(beforeDuplicateArgs)
}
// const result = await localizedValues
// update locale value if it's not undefined
if (typeof fieldValue !== 'undefined') {
return {
@@ -150,15 +169,20 @@ export const promise = async ({
}
}
return localizedValues
}, {})
return localizedValuesPromise
},
Promise.resolve({}),
)
// If there are locales with data, set the data
if (Object.keys(localeData).length > 0) {
siblingData[field.name] = localeData
}
// If there are locales with data, set the data
if (Object.keys(localeData).length > 0) {
siblingData[field.name] = localeData
}
})
} else if (duplicate && field.hooks?.beforeDuplicate?.length) {
mergeLocaleActions.push(async () => {
siblingData[field.name] = await beforeDuplicate(beforeDuplicateArgs)
})
}
}
@@ -195,6 +219,7 @@ export const promise = async ({
data,
doc,
docWithLocales,
duplicate,
errors,
fields: field.fields,
global,
@@ -225,6 +250,7 @@ export const promise = async ({
data,
doc,
docWithLocales,
duplicate,
errors,
fields: field.fields,
global,
@@ -267,6 +293,7 @@ export const promise = async ({
data,
doc,
docWithLocales,
duplicate,
errors,
fields: block.fields,
global,
@@ -298,6 +325,7 @@ export const promise = async ({
data,
doc,
docWithLocales,
duplicate,
errors,
fields: field.fields,
global,
@@ -339,6 +367,7 @@ export const promise = async ({
data,
doc,
docWithLocales,
duplicate,
errors,
fields: field.fields,
global,
@@ -363,6 +392,7 @@ export const promise = async ({
data,
doc,
docWithLocales,
duplicate,
errors,
fields: field.tabs.map((tab) => ({ ...tab, type: 'tab' })),
global,

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