Compare commits
353 Commits
v3.0.0-alp
...
v3.0.0-alp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1a11466e69 | ||
|
|
78ab2fbe09 | ||
|
|
81b33cee5c | ||
|
|
020dcaad75 | ||
|
|
9bc56bcfc7 | ||
|
|
8325fadeb3 | ||
|
|
d54275b3bd | ||
|
|
29d20423a3 | ||
|
|
e539816253 | ||
|
|
922ce9ef5f | ||
|
|
b73ec6ae94 | ||
|
|
4a11bf956d | ||
|
|
3b3bb6c80a | ||
|
|
0f323ff2e3 | ||
|
|
3305c65ae6 | ||
|
|
5c5acdcb03 | ||
|
|
0e6991f486 | ||
|
|
014786cc5f | ||
|
|
223c6b50fc | ||
|
|
b18946352e | ||
|
|
96012b26b7 | ||
|
|
31cd663ad5 | ||
|
|
0e9c9d7ccf | ||
|
|
2a6eb6ec86 | ||
|
|
0a74423c07 | ||
|
|
6f1302ae67 | ||
|
|
f275570f12 | ||
|
|
90b47f6c44 | ||
|
|
e73d008695 | ||
|
|
3c5d1b402c | ||
|
|
3e22bccce4 | ||
|
|
354e140305 | ||
|
|
54859f3582 | ||
|
|
da12efd675 | ||
|
|
32f848e90d | ||
|
|
32440d23f7 | ||
|
|
62b7acc93a | ||
|
|
7b8e2c75c2 | ||
|
|
03abc641c5 | ||
|
|
fa5b98c9b5 | ||
|
|
c681be7ba8 | ||
|
|
3d3305a312 | ||
|
|
bb305af7b4 | ||
|
|
fd284973b6 | ||
|
|
5d57572694 | ||
|
|
36a22f2b3c | ||
|
|
dea9b590d1 | ||
|
|
bf843fe598 | ||
|
|
a23bc6caa8 | ||
|
|
1904fd5b02 | ||
|
|
03c9a883e1 | ||
|
|
c06df267a3 | ||
|
|
0bccdfeda7 | ||
|
|
07d118ae7d | ||
|
|
e912dde08d | ||
|
|
3544375fdd | ||
|
|
ab6ca7910e | ||
|
|
6a329f7a8e | ||
|
|
5d4bb10106 | ||
|
|
cbc079bfff | ||
|
|
09358d5853 | ||
|
|
81c345f33e | ||
|
|
3aa200eacc | ||
|
|
6ce0b60cf2 | ||
|
|
faef0784ee | ||
|
|
fb70fe5760 | ||
|
|
5b75b8a89e | ||
|
|
fa0296b796 | ||
|
|
4e2d1f568f | ||
|
|
b8d1aec1e5 | ||
|
|
38cfd6985e | ||
|
|
d7c20c6941 | ||
|
|
7894a54a0e | ||
|
|
a46e64eec3 | ||
|
|
cae0399584 | ||
|
|
bfbf4ef0b5 | ||
|
|
ca4004605e | ||
|
|
ec565a1bd3 | ||
|
|
94c4b180c1 | ||
|
|
66de0b9019 | ||
|
|
f752b38228 | ||
|
|
d79748a967 | ||
|
|
c1b6c2c5a5 | ||
|
|
806b04e0ca | ||
|
|
736b562f3a | ||
|
|
db440236fc | ||
|
|
00ea8b900a | ||
|
|
e092e9ba67 | ||
|
|
8313cf34a6 | ||
|
|
8f4b8f5826 | ||
|
|
c607b01f33 | ||
|
|
cc5d01d0e2 | ||
|
|
a4cc41b679 | ||
|
|
67361e9ed5 | ||
|
|
8662572690 | ||
|
|
99ea1788e7 | ||
|
|
7bec3c90cd | ||
|
|
ca4e6c46bc | ||
|
|
678da159a9 | ||
|
|
cc3b51fb3b | ||
|
|
060344bb5a | ||
|
|
b42c67040d | ||
|
|
f3b18fcf0e | ||
|
|
a86c69edc9 | ||
|
|
55c60a05dc | ||
|
|
ecf40cc747 | ||
|
|
197458e60b | ||
|
|
0c3ffb0743 | ||
|
|
56dffd3c58 | ||
|
|
57a3a37fdd | ||
|
|
a8a273f0d8 | ||
|
|
9f4ab26696 | ||
|
|
4ddef9d648 | ||
|
|
7cccca8194 | ||
|
|
2a8b678a4b | ||
|
|
b9767b865a | ||
|
|
f6bc3eb014 | ||
|
|
007917df19 | ||
|
|
22a2e850bf | ||
|
|
1cfdf3613c | ||
|
|
fe280e6bb1 | ||
|
|
a330fe6017 | ||
|
|
4ee4ad25b0 | ||
|
|
777a661389 | ||
|
|
8174230afe | ||
|
|
e1777dc533 | ||
|
|
390731c07b | ||
|
|
25d475e165 | ||
|
|
1e60250670 | ||
|
|
f6d2dd520c | ||
|
|
4600588e72 | ||
|
|
825ca94080 | ||
|
|
7f674f9861 | ||
|
|
27dba7e4e1 | ||
|
|
44295ff248 | ||
|
|
dc33d96a54 | ||
|
|
4ff7619356 | ||
|
|
cc5c2bd7cd | ||
|
|
2884712685 | ||
|
|
027264588b | ||
|
|
ddd75ce730 | ||
|
|
4bc13c28dd | ||
|
|
7a1db89a6e | ||
|
|
c08489509a | ||
|
|
7054ae8a88 | ||
|
|
d7e913be95 | ||
|
|
f283a2ced5 | ||
|
|
c7274ba16f | ||
|
|
6f323e379c | ||
|
|
42212b409a | ||
|
|
e8506cc5f1 | ||
|
|
d387f9f1fa | ||
|
|
73a555788d | ||
|
|
be58f67115 | ||
|
|
b26117a65d | ||
|
|
34fe6182c8 | ||
|
|
ee3ae6025f | ||
|
|
df9812b2a3 | ||
|
|
113eea04cc | ||
|
|
57f9ebdb68 | ||
|
|
94d0e28ad7 | ||
|
|
cd553d45cc | ||
|
|
d993f9ac64 | ||
|
|
833bdc13bd | ||
|
|
33657b4b49 | ||
|
|
ec6bc8e36b | ||
|
|
799370f753 | ||
|
|
abd404c57c | ||
|
|
037ed3cd54 | ||
|
|
c461a7fa15 | ||
|
|
8fc8aaa6dd | ||
|
|
2bc45e2b2e | ||
|
|
cdfc58d115 | ||
|
|
bb8a57d2e9 | ||
|
|
5e52339135 | ||
|
|
df75914e30 | ||
|
|
572e6ccb37 | ||
|
|
8e1ebe28c0 | ||
|
|
adec044e02 | ||
|
|
b9868cc709 | ||
|
|
fce8b125f8 | ||
|
|
4befd2e4ff | ||
|
|
38cdc1b7ba | ||
|
|
a0f6018469 | ||
|
|
f230d55031 | ||
|
|
2f6a15a9ae | ||
|
|
04d751208f | ||
|
|
7cfc40f328 | ||
|
|
3c54d32b6d | ||
|
|
ece7d92e57 | ||
|
|
8e736b28af | ||
|
|
92ec0a5b1d | ||
|
|
398834f690 | ||
|
|
28e6dd8759 | ||
|
|
dedc937915 | ||
|
|
732f4241fe | ||
|
|
873585e1ae | ||
|
|
71a5a02e8c | ||
|
|
51fbd02b40 | ||
|
|
763eda5038 | ||
|
|
6cdb76503b | ||
|
|
aa8edd7a47 | ||
|
|
535aa56627 | ||
|
|
db0fb30f7b | ||
|
|
7619fb4753 | ||
|
|
0aeba954d4 | ||
|
|
2bc1468fa2 | ||
|
|
4b29b6efc5 | ||
|
|
26b1003cfd | ||
|
|
b696dce6e4 | ||
|
|
403a86feca | ||
|
|
56d6a9767e | ||
|
|
d2cc229622 | ||
|
|
e6b166da7d | ||
|
|
77f401d977 | ||
|
|
7d7b232fdb | ||
|
|
959f1e33cd | ||
|
|
7f5ab96f81 | ||
|
|
443089a66f | ||
|
|
f5d9b47177 | ||
|
|
a0cddbe9b3 | ||
|
|
5f7fcfd3df | ||
|
|
a39080340a | ||
|
|
a3d6879c55 | ||
|
|
b09be86a3c | ||
|
|
02ef033d23 | ||
|
|
f10861e1de | ||
|
|
ecf53d9961 | ||
|
|
5339c09b72 | ||
|
|
b6ad218126 | ||
|
|
b7b74a429e | ||
|
|
52acd3123f | ||
|
|
c9c3a689d8 | ||
|
|
da4a2a2494 | ||
|
|
35a1fb26f9 | ||
|
|
cb3723242c | ||
|
|
6a0c6284d0 | ||
|
|
114ade7456 | ||
|
|
a01e0e37f4 | ||
|
|
18884025de | ||
|
|
cfdc941207 | ||
|
|
5eaf00ba0e | ||
|
|
8948555ac1 | ||
|
|
e28418d6d6 | ||
|
|
fe83c53206 | ||
|
|
942aa08285 | ||
|
|
93dd6b5a98 | ||
|
|
08dd9ca91c | ||
|
|
77efdc3ccf | ||
|
|
ef1bcd5afa | ||
|
|
8636685252 | ||
|
|
5873dfb731 | ||
|
|
934abec88c | ||
|
|
c1d654c4ce | ||
|
|
d64b12b14c | ||
|
|
3f4ab5f95e | ||
|
|
ee1e94be96 | ||
|
|
1e9999a8d3 | ||
|
|
460ca99fe1 | ||
|
|
7fdf9b7012 | ||
|
|
c16b869fea | ||
|
|
8deb19e3ac | ||
|
|
e6d4445a8a | ||
|
|
0f0da809da | ||
|
|
db8e805a96 | ||
|
|
a882cc7e8e | ||
|
|
acede26aa6 | ||
|
|
9330919be8 | ||
|
|
91b4e91e9c | ||
|
|
2fb265ae2d | ||
|
|
3b5e7f1dc4 | ||
|
|
08ff286f9a | ||
|
|
bc9fe2f4f9 | ||
|
|
e99b168bd6 | ||
|
|
a7afb1f680 | ||
|
|
6f3934f2e5 | ||
|
|
79da297add | ||
|
|
6664535ab9 | ||
|
|
80d290e178 | ||
|
|
d144af6d8e | ||
|
|
aba7c13a1d | ||
|
|
f59d3f36d1 | ||
|
|
3f0d0ecd5f | ||
|
|
0c51502cc5 | ||
|
|
e829650cd9 | ||
|
|
a8082c551b | ||
|
|
ff55cfa001 | ||
|
|
623a3d3b7b | ||
|
|
fb32e2a561 | ||
|
|
8aa8a380e1 | ||
|
|
f35b8b05e8 | ||
|
|
05bb73bb7c | ||
|
|
18299dc65e | ||
|
|
818ab2c10f | ||
|
|
df0bf28d57 | ||
|
|
ff65f10c2f | ||
|
|
5cf49aa166 | ||
|
|
0651daa1d4 | ||
|
|
921c53f75c | ||
|
|
dd37519185 | ||
|
|
a1e8c4eb2b | ||
|
|
1f8c191cb3 | ||
|
|
f4acc74eee | ||
|
|
3d1378ab77 | ||
|
|
58e4174edb | ||
|
|
20b4585666 | ||
|
|
9c7e7ed8d4 | ||
|
|
436c4f2736 | ||
|
|
0ce752af79 | ||
|
|
92ff896bdb | ||
|
|
77a3cbaba5 | ||
|
|
56f9c88251 | ||
|
|
8a5a08cbe1 | ||
|
|
5241c38ba0 | ||
|
|
072a903351 | ||
|
|
328bd453bb | ||
|
|
690a3cfa68 | ||
|
|
1c1847f63c | ||
|
|
c3d9d8ee2f | ||
|
|
65932b65d2 | ||
|
|
682e961416 | ||
|
|
9fcccc8197 | ||
|
|
72f3ced219 | ||
|
|
74de066529 | ||
|
|
3d1589404c | ||
|
|
30d9d46dd8 | ||
|
|
740373897a | ||
|
|
f7ca01bafd | ||
|
|
7654ff686a | ||
|
|
5266612bb3 | ||
|
|
a9b46a4d63 | ||
|
|
76e9bd8ad6 | ||
|
|
317a443644 | ||
|
|
43f91ca42c | ||
|
|
8d78d07415 | ||
|
|
99a00a1ae2 | ||
|
|
2cd8d891a1 | ||
|
|
7fc33af1e5 | ||
|
|
67c57a1137 | ||
|
|
9f8ac06659 | ||
|
|
aea28b28d0 | ||
|
|
ee4cd61696 | ||
|
|
e9f15c377f | ||
|
|
d5935ea81b | ||
|
|
934ad96a98 | ||
|
|
2c68f8fba1 | ||
|
|
c90de87f37 | ||
|
|
ab84566d86 | ||
|
|
4c109a467f | ||
|
|
bc4f6aaf9c | ||
|
|
cf8ac7e8b3 | ||
|
|
3a9b230aef | ||
|
|
016b644d86 |
113
.github/workflows/main.yml
vendored
113
.github/workflows/main.yml
vendored
@@ -2,7 +2,7 @@ name: build
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [ opened, reopened, synchronize ]
|
||||
types: [opened, reopened, synchronize]
|
||||
push:
|
||||
branches: ['main', 'alpha']
|
||||
|
||||
@@ -117,15 +117,43 @@ jobs:
|
||||
- run: pnpm install
|
||||
- run: pnpm run build:plugins
|
||||
|
||||
tests:
|
||||
tests-unit:
|
||||
runs-on: ubuntu-latest
|
||||
needs: core-build
|
||||
if: false # Disable until tests are updated for 3.0
|
||||
|
||||
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: Unit Tests
|
||||
run: pnpm test:unit
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=8096
|
||||
|
||||
tests-int:
|
||||
runs-on: ubuntu-latest
|
||||
needs: core-build
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
database:
|
||||
- mongodb
|
||||
# - postgres
|
||||
- mongodb
|
||||
- postgres
|
||||
# - postgres-custom-schema
|
||||
# - postgres-uuid
|
||||
# - supabase
|
||||
@@ -162,7 +190,7 @@ jobs:
|
||||
- name: Start PostgreSQL
|
||||
uses: CasperWA/postgresql-action@v1.2
|
||||
with:
|
||||
postgresql version: '14' # See https://hub.docker.com/_/postgres for available versions
|
||||
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 }}
|
||||
@@ -202,7 +230,7 @@ jobs:
|
||||
if: matrix.database == 'supabase'
|
||||
|
||||
- name: Integration Tests
|
||||
run: pnpm test:int --testPathIgnorePatterns=test/fields # Ignore fields tests until reworked
|
||||
run: pnpm test:int
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=8096
|
||||
PAYLOAD_DATABASE: ${{ matrix.database }}
|
||||
@@ -220,16 +248,17 @@ jobs:
|
||||
- access-control
|
||||
# - admin
|
||||
- auth
|
||||
# - field-error-states
|
||||
# - fields-relationship
|
||||
- field-error-states
|
||||
- fields-relationship
|
||||
# - fields
|
||||
# - live-preview
|
||||
# - localization
|
||||
# - plugin-nested-docs
|
||||
# - plugin-seo
|
||||
# - refresh-permissions
|
||||
# - uploads
|
||||
# - versions
|
||||
- fields/lexical
|
||||
- live-preview
|
||||
- localization
|
||||
- plugin-form-builder
|
||||
- plugin-nested-docs
|
||||
- plugin-seo
|
||||
- versions
|
||||
- uploads
|
||||
|
||||
steps:
|
||||
- name: Use Node.js 18
|
||||
@@ -250,20 +279,15 @@ 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
|
||||
|
||||
@@ -296,47 +320,6 @@ jobs:
|
||||
- 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
|
||||
if: matrix.pkg != 'create-payload-app' # degit doesn't work within GitHub Actions
|
||||
|
||||
templates:
|
||||
needs: changes
|
||||
if: false # Disable until templates are updated for 3.0
|
||||
@@ -344,7 +327,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
template: [ blank, website, ecommerce ]
|
||||
template: [blank, website, ecommerce]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -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
54
.idea/payload.iml
generated
Normal 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>
|
||||
@@ -9,4 +9,4 @@
|
||||
**/node_modules
|
||||
**/temp
|
||||
**/docs/**
|
||||
./tsconfig.json
|
||||
tsconfig.json
|
||||
|
||||
20
.vscode/launch.json
vendored
20
.vscode/launch.json
vendored
@@ -10,19 +10,26 @@
|
||||
"cwd": "${workspaceFolder}"
|
||||
},
|
||||
{
|
||||
"command": "pnpm run dev _community -- --no-turbo",
|
||||
"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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
#custom-css {
|
||||
font-family: monospace;
|
||||
background-image: url('/placeholder.png');
|
||||
}
|
||||
|
||||
#custom-css::after {
|
||||
content: 'custom-css';
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import type { Page as PageType } from '../../../../test/live-preview/payload-typ
|
||||
|
||||
import { PAYLOAD_SERVER_URL } from '../../_api/serverURL.js'
|
||||
import { Blocks } from '../../_components/Blocks/index.js'
|
||||
import { Gutter } from '../../_components/Gutter/index.js'
|
||||
import { Hero } from '../../_components/Hero/index.js'
|
||||
|
||||
export const PageClient: React.FC<{
|
||||
@@ -20,6 +21,9 @@ export const PageClient: React.FC<{
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Gutter>
|
||||
<h1 id="page-title">{data.title}</h1>
|
||||
</Gutter>
|
||||
<Hero {...data?.hero} />
|
||||
<Blocks
|
||||
blocks={[
|
||||
|
||||
@@ -92,6 +92,10 @@ p {
|
||||
}
|
||||
}
|
||||
|
||||
#page-title {
|
||||
@extend %h6;
|
||||
}
|
||||
|
||||
ul,
|
||||
ol {
|
||||
padding-left: var(--base);
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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:
|
||||
|
||||
|
||||
@@ -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
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
@@ -32,4 +32,4 @@
|
||||
"ts-node": "^9.1.1",
|
||||
"typescript": "^4.8.4"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"strict": false,
|
||||
"esModuleInterop": true,
|
||||
@@ -16,22 +12,12 @@
|
||||
"sourceMap": true,
|
||||
"resolveJsonModule": true,
|
||||
"paths": {
|
||||
"payload/generated-types": [
|
||||
"./src/payload-types.ts"
|
||||
],
|
||||
"node_modules/*": [
|
||||
"./node_modules/*"
|
||||
]
|
||||
},
|
||||
"payload/generated-types": ["./src/payload-types.ts"],
|
||||
"node_modules/*": ["./node_modules/*"]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist",
|
||||
"build",
|
||||
],
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules", "dist", "build"],
|
||||
"ts-node": {
|
||||
"transpileOnly": true
|
||||
}
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"outDir": "./dist",
|
||||
"skipLibCheck": true,
|
||||
"strict": false,
|
||||
@@ -18,10 +14,8 @@
|
||||
"jsx": "preserve",
|
||||
"sourceMap": true
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
],
|
||||
"include": ["src"],
|
||||
"ts-node": {
|
||||
"transpileOnly": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,4 +26,4 @@
|
||||
"ts-node": "^9.1.1",
|
||||
"typescript": "^4.8.4"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"strict": false,
|
||||
"esModuleInterop": true,
|
||||
@@ -14,14 +10,8 @@
|
||||
"rootDir": "./src",
|
||||
"jsx": "react"
|
||||
},
|
||||
"include": [
|
||||
"src",
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist",
|
||||
"build",
|
||||
],
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules", "dist", "build"],
|
||||
"ts-node": {
|
||||
"transpileOnly": true
|
||||
}
|
||||
|
||||
@@ -26,4 +26,4 @@
|
||||
"ts-node": "^9.1.1",
|
||||
"typescript": "^4.8.4"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"strict": false,
|
||||
"esModuleInterop": true,
|
||||
@@ -14,14 +10,8 @@
|
||||
"rootDir": "./src",
|
||||
"jsx": "react"
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist",
|
||||
"build",
|
||||
],
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules", "dist", "build"],
|
||||
"ts-node": {
|
||||
"transpileOnly": true
|
||||
}
|
||||
|
||||
@@ -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'],
|
||||
@@ -18,4 +17,8 @@ const customJestConfig = {
|
||||
verbose: true,
|
||||
}
|
||||
|
||||
export default customJestConfig
|
||||
if (process.env.CI) {
|
||||
baseJestConfig.reporters = [['github-actions', { silent: false }], 'summary']
|
||||
}
|
||||
|
||||
export default baseJestConfig
|
||||
|
||||
25
package.json
25
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "payload-monorepo",
|
||||
"version": "3.0.0-alpha.49",
|
||||
"version": "3.0.0-alpha.54",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"workspaces:": [
|
||||
@@ -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": {
|
||||
@@ -109,8 +109,8 @@
|
||||
"dotenv": "8.6.0",
|
||||
"drizzle-kit": "0.20.14-1f2c838",
|
||||
"drizzle-orm": "0.29.4",
|
||||
"eslint-plugin-payload": "workspace:*",
|
||||
"escape-html": "^1.0.3",
|
||||
"eslint-plugin-payload": "workspace:*",
|
||||
"execa": "5.1.1",
|
||||
"form-data": "3.0.1",
|
||||
"fs-extra": "10.1.0",
|
||||
@@ -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,20 +154,20 @@
|
||||
"tempy": "^1.0.1",
|
||||
"ts-node": "10.9.1",
|
||||
"tsx": "^4.7.1",
|
||||
"turbo": "^1.12.5",
|
||||
"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",
|
||||
"pnpm": ">=8"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{md,mdx,yml,json}": "prettier --write",
|
||||
"*.{js,jsx,ts,tsx}": [
|
||||
"prettier --write",
|
||||
"eslint --cache --fix"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,14 +1,21 @@
|
||||
{
|
||||
"name": "create-payload-app",
|
||||
"version": "1.0.0",
|
||||
"version": "3.0.0-alpha.50",
|
||||
"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 build:swc",
|
||||
"copyfiles": "copyfiles -u 2 \"../../app/(payload)/**\" \"dist\"",
|
||||
"build": "pnpm pack-template-files && pnpm typecheck && pnpm build:swc",
|
||||
"typecheck": "tsc",
|
||||
"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",
|
||||
@@ -20,6 +27,7 @@
|
||||
"bin"
|
||||
],
|
||||
"dependencies": {
|
||||
"@clack/prompts": "^0.7.0",
|
||||
"@sindresorhus/slugify": "^1.1.0",
|
||||
"arg": "^5.0.0",
|
||||
"chalk": "^4.1.0",
|
||||
@@ -27,21 +35,26 @@
|
||||
"comment-json": "^4.2.3",
|
||||
"degit": "^2.8.4",
|
||||
"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": {
|
||||
"@types/command-exists": "^1.2.0",
|
||||
"@types/degit": "^2.8.3",
|
||||
"@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": {
|
||||
"import": "./src/lib/init-next.ts",
|
||||
"require": "./src/lib/init-next.ts",
|
||||
"types": "./src/lib/init-next.ts"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,72 +1,42 @@
|
||||
import fse from 'fs-extra'
|
||||
import path from 'path'
|
||||
import globby from 'globby'
|
||||
|
||||
import type { DbDetails } from '../types.js'
|
||||
|
||||
import { warning } from '../utils/log.js'
|
||||
import { bundlerPackages, dbPackages, editorPackages } from './packages.js'
|
||||
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
|
||||
}
|
||||
|
||||
// Update package.json
|
||||
const packageJsonPath = path.resolve(args.projectDir, 'package.json')
|
||||
try {
|
||||
const packageObj = await fse.readJson(packageJsonPath)
|
||||
|
||||
packageObj.dependencies['payload'] = '^2.0.0'
|
||||
|
||||
const dbPackage = dbPackages[args.dbDetails.type]
|
||||
const bundlerPackage = bundlerPackages['webpack']
|
||||
const editorPackage = editorPackages['slate']
|
||||
|
||||
// Delete all other db adapters
|
||||
Object.values(dbPackages).forEach((p) => {
|
||||
if (p.packageName !== dbPackage.packageName) {
|
||||
delete packageObj.dependencies[p.packageName]
|
||||
}
|
||||
})
|
||||
|
||||
packageObj.dependencies[dbPackage.packageName] = dbPackage.version
|
||||
packageObj.dependencies[bundlerPackage.packageName] = bundlerPackage.version
|
||||
packageObj.dependencies[editorPackage.packageName] = editorPackage.version
|
||||
|
||||
await fse.writeJson(packageJsonPath, packageObj, { spaces: 2 })
|
||||
} catch (err: unknown) {
|
||||
warning('Unable to update name in package.json')
|
||||
}
|
||||
|
||||
try {
|
||||
const possiblePaths = [
|
||||
path.resolve(args.projectDir, 'src/payload.config.ts'),
|
||||
path.resolve(args.projectDir, 'src/payload/payload.config.ts'),
|
||||
]
|
||||
|
||||
let payloadConfigPath: string | undefined
|
||||
|
||||
possiblePaths.forEach((p) => {
|
||||
if (fse.pathExistsSync(p) && !payloadConfigPath) {
|
||||
payloadConfigPath = p
|
||||
}
|
||||
})
|
||||
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
|
||||
}
|
||||
|
||||
const configContent = fse.readFileSync(payloadConfigPath, 'utf-8')
|
||||
const configLines = configContent.split('\n')
|
||||
|
||||
const dbReplacement = dbPackages[args.dbDetails.type]
|
||||
const bundlerReplacement = bundlerPackages['webpack']
|
||||
const editorReplacement = editorPackages['slate']
|
||||
const dbReplacement = dbReplacements[args.dbDetails.type]
|
||||
|
||||
let dbConfigStartLineIndex: number | undefined
|
||||
let dbConfigEndLineIndex: number | undefined
|
||||
@@ -75,21 +45,6 @@ export async function configurePayloadConfig(args: {
|
||||
if (l.includes('// database-adapter-import')) {
|
||||
configLines[i] = dbReplacement.importReplacement
|
||||
}
|
||||
if (l.includes('// bundler-import')) {
|
||||
configLines[i] = bundlerReplacement.importReplacement
|
||||
}
|
||||
|
||||
if (l.includes('// bundler-config')) {
|
||||
configLines[i] = bundlerReplacement.configReplacement
|
||||
}
|
||||
|
||||
if (l.includes('// editor-import')) {
|
||||
configLines[i] = editorReplacement.importReplacement
|
||||
}
|
||||
|
||||
if (l.includes('// editor-config')) {
|
||||
configLines[i] = editorReplacement.configReplacement
|
||||
}
|
||||
|
||||
if (l.includes('// database-adapter-config-start')) {
|
||||
dbConfigStartLineIndex = i
|
||||
@@ -112,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 : ''}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import fse from 'fs-extra'
|
||||
import path from 'path'
|
||||
import type { BundlerType, CliArgs, DbType, ProjectTemplate } from '../types.js'
|
||||
import type { CliArgs, DbType, ProjectTemplate } from '../types.js'
|
||||
import { createProject } from './create-project.js'
|
||||
import { bundlerPackages, dbPackages, editorPackages } from './packages.js'
|
||||
import exp from 'constants'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { dbReplacements } from './packages.js'
|
||||
import { getValidTemplates } from './templates.js'
|
||||
import globby from 'globby'
|
||||
|
||||
const projectDir = path.resolve(__dirname, './tmp')
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
const dirname = path.dirname(filename)
|
||||
|
||||
const projectDir = path.resolve(dirname, './tmp')
|
||||
describe('createProject', () => {
|
||||
beforeAll(() => {
|
||||
console.log = jest.fn()
|
||||
@@ -28,33 +32,11 @@ describe('createProject', () => {
|
||||
const args = {
|
||||
_: ['project-name'],
|
||||
'--db': 'mongodb',
|
||||
'--local-template': 'blank',
|
||||
'--no-deps': true,
|
||||
} as CliArgs
|
||||
const packageManager = 'yarn'
|
||||
|
||||
it('creates starter project', async () => {
|
||||
const projectName = 'starter-project'
|
||||
const template: ProjectTemplate = {
|
||||
name: 'blank',
|
||||
type: 'starter',
|
||||
url: 'https://github.com/payloadcms/payload/templates/blank',
|
||||
description: 'Blank Template',
|
||||
}
|
||||
await createProject({
|
||||
cliArgs: args,
|
||||
projectName,
|
||||
projectDir,
|
||||
template,
|
||||
packageManager,
|
||||
})
|
||||
|
||||
const packageJsonPath = path.resolve(projectDir, 'package.json')
|
||||
const packageJson = fse.readJsonSync(packageJsonPath)
|
||||
|
||||
// Check package name and description
|
||||
expect(packageJson.name).toEqual(projectName)
|
||||
})
|
||||
|
||||
it('creates plugin template', async () => {
|
||||
const projectName = 'plugin'
|
||||
const template: ProjectTemplate = {
|
||||
@@ -78,26 +60,34 @@ describe('createProject', () => {
|
||||
expect(packageJson.name).toEqual(projectName)
|
||||
})
|
||||
|
||||
describe('db adapters and bundlers', () => {
|
||||
describe('creates project from template', () => {
|
||||
const templates = getValidTemplates()
|
||||
|
||||
it.each([
|
||||
['blank', 'mongodb', 'webpack'],
|
||||
['blank', 'postgres', 'webpack'],
|
||||
['website', 'mongodb', 'webpack'],
|
||||
['website', 'postgres', 'webpack'],
|
||||
['ecommerce', 'mongodb', 'webpack'],
|
||||
['ecommerce', 'postgres', 'webpack'],
|
||||
])('update config and deps: %s, %s, %s', async (templateName, db, bundler) => {
|
||||
['blank-3.0', 'mongodb'],
|
||||
['blank-3.0', 'postgres'],
|
||||
|
||||
// TODO: Re-enable these once 3.0 is stable and templates updated
|
||||
// ['website', 'mongodb'],
|
||||
// ['website', 'postgres'],
|
||||
// ['ecommerce', 'mongodb'],
|
||||
// ['ecommerce', 'postgres'],
|
||||
])('update config and deps: %s, %s', async (templateName, db) => {
|
||||
const projectName = 'starter-project'
|
||||
|
||||
const template = templates.find((t) => t.name === templateName)
|
||||
|
||||
const cliArgs = {
|
||||
...args,
|
||||
'--db': db,
|
||||
'--local-template': templateName,
|
||||
} as CliArgs
|
||||
|
||||
await createProject({
|
||||
cliArgs: args,
|
||||
cliArgs,
|
||||
projectName,
|
||||
projectDir,
|
||||
template,
|
||||
template: template as ProjectTemplate,
|
||||
packageManager,
|
||||
dbDetails: {
|
||||
dbUri: `${db}://localhost:27017/create-project-test`,
|
||||
@@ -105,35 +95,27 @@ describe('createProject', () => {
|
||||
},
|
||||
})
|
||||
|
||||
const dbReplacement = dbPackages[db as DbType]
|
||||
const bundlerReplacement = bundlerPackages[bundler as BundlerType]
|
||||
const editorReplacement = editorPackages['slate']
|
||||
const dbReplacement = dbReplacements[db as DbType]
|
||||
|
||||
const packageJsonPath = path.resolve(projectDir, 'package.json')
|
||||
const packageJson = fse.readJsonSync(packageJsonPath)
|
||||
|
||||
// Check deps
|
||||
expect(packageJson.dependencies['payload']).toEqual('^2.0.0')
|
||||
expect(packageJson.dependencies[dbReplacement.packageName]).toEqual(dbReplacement.version)
|
||||
|
||||
// Should only have one db adapter
|
||||
expect(
|
||||
Object.keys(packageJson.dependencies).filter((n) => n.startsWith('@payloadcms/db-')),
|
||||
).toHaveLength(1)
|
||||
|
||||
expect(packageJson.dependencies[bundlerReplacement.packageName]).toEqual(
|
||||
bundlerReplacement.version,
|
||||
)
|
||||
expect(packageJson.dependencies[editorReplacement.packageName]).toEqual(
|
||||
editorReplacement.version,
|
||||
)
|
||||
const payloadConfigPath = (
|
||||
await globby('**/payload.config.ts', {
|
||||
absolute: true,
|
||||
cwd: projectDir,
|
||||
})
|
||||
)?.[0]
|
||||
|
||||
let payloadConfigPath = path.resolve(projectDir, 'src/payload.config.ts')
|
||||
|
||||
// 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
|
||||
@@ -143,18 +125,7 @@ describe('createProject', () => {
|
||||
expect(content).not.toContain('// database-adapter-config-start')
|
||||
expect(content).not.toContain('// database-adapter-config-end')
|
||||
expect(content).toContain(dbReplacement.configReplacement.join('\n'))
|
||||
|
||||
expect(content).not.toContain('// bundler-config-import')
|
||||
expect(content).toContain(bundlerReplacement.importReplacement)
|
||||
|
||||
expect(content).not.toContain('// bundler-config')
|
||||
expect(content).toContain(bundlerReplacement.configReplacement)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
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
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
import * as p from '@clack/prompts'
|
||||
import chalk from 'chalk'
|
||||
import degit from 'degit'
|
||||
import execa from 'execa'
|
||||
import fse from 'fs-extra'
|
||||
import ora from 'ora'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import path from 'path'
|
||||
|
||||
import type { CliArgs, DbDetails, PackageManager, ProjectTemplate } from '../types.js'
|
||||
|
||||
import { 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)
|
||||
const dirname = path.dirname(filename)
|
||||
|
||||
async function createOrFindProjectDir(projectDir: string): Promise<void> {
|
||||
const pathExists = await fse.pathExists(projectDir)
|
||||
if (!pathExists) {
|
||||
@@ -40,7 +44,7 @@ async function installDeps(args: {
|
||||
})
|
||||
return true
|
||||
} catch (err: unknown) {
|
||||
console.log({ err })
|
||||
error(`Error installing dependencies${err instanceof Error ? `: ${err.message}` : ''}.`)
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -55,19 +59,37 @@ export async function createProject(args: {
|
||||
}): Promise<void> {
|
||||
const { cliArgs, dbDetails, packageManager, projectDir, projectName, template } = args
|
||||
|
||||
if (cliArgs['--dry-run']) {
|
||||
debug(`Dry run: Creating project in ${chalk.green(projectDir)}`)
|
||||
return
|
||||
}
|
||||
|
||||
await createOrFindProjectDir(projectDir)
|
||||
|
||||
console.log(`\n Creating project in ${chalk.green(path.resolve(projectDir))}\n`)
|
||||
|
||||
if ('url' in template) {
|
||||
const emitter = degit(template.url)
|
||||
if (cliArgs['--local-template']) {
|
||||
// Copy template from local path. For development purposes.
|
||||
const localTemplate = path.resolve(
|
||||
dirname,
|
||||
'../../../../templates/',
|
||||
cliArgs['--local-template'],
|
||||
)
|
||||
await fse.copy(localTemplate, projectDir)
|
||||
} else if ('url' in template) {
|
||||
let templateUrl = template.url
|
||||
if (cliArgs['--template-branch']) {
|
||||
templateUrl = `${template.url}#${cliArgs['--template-branch']}`
|
||||
debug(`Using template url: ${templateUrl}`)
|
||||
}
|
||||
const emitter = degit(templateUrl)
|
||||
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')
|
||||
@@ -75,14 +97,16 @@ export async function createProject(args: {
|
||||
await fse.remove(lockPath)
|
||||
}
|
||||
|
||||
spinner.text = 'Installing dependencies...'
|
||||
const result = await installDeps({ cliArgs, packageManager, projectDir })
|
||||
spinner.stop()
|
||||
spinner.clear()
|
||||
if (result) {
|
||||
success('Dependencies installed')
|
||||
if (!cliArgs['--no-deps']) {
|
||||
spinner.message('Installing dependencies...')
|
||||
const result = await installDeps({ cliArgs, packageManager, projectDir })
|
||||
if (result) {
|
||||
spinner.stop('Successfully installed Payload and dependencies')
|
||||
} else {
|
||||
spinner.stop('Error installing dependencies', 1)
|
||||
}
|
||||
} else {
|
||||
error('Error installing dependencies')
|
||||
spinner.stop('Dependency installation skipped')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,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 : ''}`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import type { CompilerOptions } from 'typescript'
|
||||
|
||||
import chalk from 'chalk'
|
||||
import * as p from '@clack/prompts'
|
||||
import { parse, stringify } from 'comment-json'
|
||||
import { detect } from 'detect-package-manager'
|
||||
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)
|
||||
|
||||
@@ -17,58 +16,85 @@ const dirname = path.dirname(filename)
|
||||
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
import type { CliArgs } 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'> & {
|
||||
projectDir?: string
|
||||
dbType: DbType
|
||||
nextAppDetails?: NextAppDetails
|
||||
packageManager: PackageManager
|
||||
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> {
|
||||
args.projectDir = args.projectDir || process.cwd()
|
||||
const { projectDir } = args
|
||||
const templateResult = await applyPayloadTemplateFiles(args)
|
||||
if (!templateResult.success) return templateResult
|
||||
const { dbType: dbType, packageManager, projectDir } = args
|
||||
|
||||
const { success: installSuccess } = await installDeps(projectDir)
|
||||
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',
|
||||
@@ -80,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', { 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 {
|
||||
@@ -132,40 +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 userAppDirGlob = await globby(['**/app'], {
|
||||
cwd: projectDir,
|
||||
onlyDirectories: true,
|
||||
})
|
||||
const userAppDir = path.resolve(projectDir, userAppDirGlob?.[0])
|
||||
if (!fs.existsSync(userAppDir)) {
|
||||
return { reason: `Could not find user app directory inside ${projectDir}`, success: false }
|
||||
} else {
|
||||
logDebug(`Found user app directory: ${userAppDir}`)
|
||||
}
|
||||
logDebug(`Copying template files from ${templateFilesPath} to ${nextAppDir}`)
|
||||
|
||||
logDebug(`Copying template files from ${templateFilesPath} to ${userAppDir}`)
|
||||
copyRecursiveSync(templateFilesPath, userAppDir, debug)
|
||||
success('Successfully initialized.')
|
||||
return { success: true, 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,
|
||||
}
|
||||
}
|
||||
|
||||
async function installDeps(projectDir: string) {
|
||||
const packageManager = await detect({ cwd: projectDir })
|
||||
if (!packageManager) {
|
||||
throw new Error('Could not detect package manager')
|
||||
}
|
||||
async function installDeps(projectDir: string, packageManager: PackageManager, dbType: DbType) {
|
||||
const packagesToInstall = ['payload', '@payloadcms/next', '@payloadcms/richtext-lexical'].map(
|
||||
(pkg) => `${pkg}@alpha`,
|
||||
)
|
||||
|
||||
info(`Installing dependencies with ${packageManager}`, 1)
|
||||
const packagesToInstall = [
|
||||
'payload',
|
||||
'@payloadcms/db-mongodb',
|
||||
'@payloadcms/next',
|
||||
'@payloadcms/richtext-slate',
|
||||
].map((pkg) => `${pkg}@alpha`)
|
||||
packagesToInstall.push(`@payloadcms/db-${dbType}@alpha`)
|
||||
|
||||
let exitCode = 0
|
||||
switch (packageManager) {
|
||||
@@ -189,43 +218,45 @@ async function installDeps(projectDir: string) {
|
||||
}
|
||||
}
|
||||
|
||||
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 { slateEditor } from "@payloadcms/richtext-slate"; // 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 }
|
||||
}
|
||||
|
||||
@@ -1,24 +1,9 @@
|
||||
import type { BundlerType, DbType, EditorType } from '../types.js'
|
||||
import type { DbType } from '../types.js'
|
||||
|
||||
type DbAdapterReplacement = {
|
||||
configReplacement: string[]
|
||||
importReplacement: string
|
||||
packageName: string
|
||||
version: string
|
||||
}
|
||||
|
||||
type BundlerReplacement = {
|
||||
configReplacement: string
|
||||
importReplacement: string
|
||||
packageName: string
|
||||
version: string
|
||||
}
|
||||
|
||||
type EditorReplacement = {
|
||||
configReplacement: string
|
||||
importReplacement: string
|
||||
packageName: string
|
||||
version: string
|
||||
}
|
||||
|
||||
const mongodbReplacement: DbAdapterReplacement = {
|
||||
@@ -26,7 +11,6 @@ const mongodbReplacement: DbAdapterReplacement = {
|
||||
packageName: '@payloadcms/db-mongodb',
|
||||
// Replacement between `// database-adapter-config-start` and `// database-adapter-config-end`
|
||||
configReplacement: [' db: mongooseAdapter({', ' url: process.env.DATABASE_URI,', ' }),'],
|
||||
version: '^1.0.0',
|
||||
}
|
||||
|
||||
const postgresReplacement: DbAdapterReplacement = {
|
||||
@@ -39,45 +23,9 @@ const postgresReplacement: DbAdapterReplacement = {
|
||||
],
|
||||
importReplacement: "import { postgresAdapter } from '@payloadcms/db-postgres'",
|
||||
packageName: '@payloadcms/db-postgres',
|
||||
version: '^0.x', // up to, not including 1.0.0
|
||||
}
|
||||
|
||||
export const dbPackages: Record<DbType, DbAdapterReplacement> = {
|
||||
export const dbReplacements: Record<DbType, DbAdapterReplacement> = {
|
||||
mongodb: mongodbReplacement,
|
||||
postgres: postgresReplacement,
|
||||
}
|
||||
|
||||
const webpackReplacement: BundlerReplacement = {
|
||||
importReplacement: "import { webpackBundler } from '@payloadcms/bundler-webpack'",
|
||||
packageName: '@payloadcms/bundler-webpack',
|
||||
// Replacement of line containing `// bundler-config`
|
||||
configReplacement: ' bundler: webpackBundler(),',
|
||||
version: '^1.0.0',
|
||||
}
|
||||
|
||||
const viteReplacement: BundlerReplacement = {
|
||||
configReplacement: ' bundler: viteBundler(),',
|
||||
importReplacement: "import { viteBundler } from '@payloadcms/bundler-vite'",
|
||||
packageName: '@payloadcms/bundler-vite',
|
||||
version: '^0.x', // up to, not including 1.0.0
|
||||
}
|
||||
|
||||
export const bundlerPackages: Record<BundlerType, BundlerReplacement> = {
|
||||
vite: viteReplacement,
|
||||
webpack: webpackReplacement,
|
||||
}
|
||||
|
||||
export const editorPackages: Record<EditorType, EditorReplacement> = {
|
||||
lexical: {
|
||||
configReplacement: ' editor: lexicalEditor({}),',
|
||||
importReplacement: "import { lexicalEditor } from '@payloadcms/richtext-lexical'",
|
||||
packageName: '@payloadcms/richtext-lexical',
|
||||
version: '^0.x', // up to, not including 1.0.0
|
||||
},
|
||||
slate: {
|
||||
configReplacement: ' editor: slateEditor({}),',
|
||||
importReplacement: "import { slateEditor } from '@payloadcms/richtext-slate'",
|
||||
packageName: '@payloadcms/richtext-slate',
|
||||
version: '^1.0.0',
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,24 +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['--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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,50 +34,39 @@ 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]
|
||||
|
||||
const dbUriRes = await prompts(
|
||||
{
|
||||
name: 'value',
|
||||
type: 'text',
|
||||
initial: `${dbChoice.dbConnectionPrefix}${
|
||||
projectName === '.' ? `payload-${getRandomDigitSuffix()}` : slugify(projectName)
|
||||
}`,
|
||||
let dbUri: string | symbol | undefined = undefined
|
||||
const initialDbUri = `${dbChoice.dbConnectionPrefix}${
|
||||
projectName === '.' ? `payload-${getRandomDigitSuffix()}` : slugify(projectName)
|
||||
}`
|
||||
|
||||
if (args['--db-accept-recommended']) {
|
||||
dbUri = initialDbUri
|
||||
} else if (args['--db-connection-string']) {
|
||||
dbUri = args['--db-connection-string']
|
||||
} else {
|
||||
dbUri = await p.text({
|
||||
initialValue: initialDbUri,
|
||||
message: `Enter ${dbChoice.title.split(' ')[0]} connection string`, // strip beta from title
|
||||
validate: (value: string) => !!value.length,
|
||||
},
|
||||
{
|
||||
onCancel: () => {
|
||||
process.exit(0)
|
||||
},
|
||||
},
|
||||
)
|
||||
})
|
||||
if (p.isCancel(dbUri)) process.exit(0)
|
||||
}
|
||||
|
||||
return {
|
||||
type: dbChoice.value,
|
||||
dbUri: dbUriRes.value,
|
||||
dbUri,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,12 @@ export function validateTemplate(templateName: string): boolean {
|
||||
|
||||
export function getValidTemplates(): ProjectTemplate[] {
|
||||
return [
|
||||
{
|
||||
name: 'blank-3.0',
|
||||
type: 'starter',
|
||||
description: 'Blank 3.0 Template',
|
||||
url: 'https://github.com/payloadcms/payload/templates/blank-3.0',
|
||||
},
|
||||
{
|
||||
name: 'blank',
|
||||
type: 'starter',
|
||||
|
||||
61
packages/create-payload-app/src/lib/wrap-next-config.spec.ts
Normal file
61
packages/create-payload-app/src/lib/wrap-next-config.spec.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import { parseAndModifyConfigContent, withPayloadImportStatement } from './wrap-next-config.js'
|
||||
import * as p from '@clack/prompts'
|
||||
|
||||
const defaultNextConfig = `/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {};
|
||||
|
||||
export default nextConfig;
|
||||
`
|
||||
|
||||
const nextConfigWithFunc = `const nextConfig = {
|
||||
// Your Next.js config here
|
||||
}
|
||||
|
||||
export default someFunc(nextConfig)
|
||||
`
|
||||
const nextConfigWithFuncMultiline = `const nextConfig = {
|
||||
// Your Next.js config here
|
||||
}
|
||||
|
||||
export default someFunc(
|
||||
nextConfig
|
||||
)
|
||||
`
|
||||
|
||||
const nextConfigExportNamedDefault = `const nextConfig = {
|
||||
// Your Next.js config here
|
||||
}
|
||||
const wrapped = someFunc(asdf)
|
||||
export { wrapped as default }
|
||||
`
|
||||
|
||||
describe('parseAndInsertWithPayload', () => {
|
||||
it('should parse the default next config', () => {
|
||||
const { modifiedConfigContent } = parseAndModifyConfigContent(defaultNextConfig)
|
||||
expect(modifiedConfigContent).toContain(withPayloadImportStatement)
|
||||
expect(modifiedConfigContent).toContain('withPayload(nextConfig)')
|
||||
})
|
||||
it('should parse the config with a function', () => {
|
||||
const { modifiedConfigContent } = parseAndModifyConfigContent(nextConfigWithFunc)
|
||||
expect(modifiedConfigContent).toContain('withPayload(someFunc(nextConfig))')
|
||||
})
|
||||
|
||||
it('should parse the config with a function on a new line', () => {
|
||||
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 warnLogSpy = jest.spyOn(p.log, 'warn').mockImplementation(() => {})
|
||||
|
||||
const { modifiedConfigContent, success } = parseAndModifyConfigContent(
|
||||
nextConfigExportNamedDefault,
|
||||
)
|
||||
expect(modifiedConfigContent).toContain(withPayloadImportStatement)
|
||||
expect(success).toBe(false)
|
||||
|
||||
expect(warnLogSpy).toHaveBeenCalledWith(expect.stringContaining('Could not automatically wrap'))
|
||||
})
|
||||
})
|
||||
146
packages/create-payload-app/src/lib/wrap-next-config.ts
Normal file
146
packages/create-payload-app/src/lib/wrap-next-config.ts
Normal file
@@ -0,0 +1,146 @@
|
||||
import chalk from 'chalk'
|
||||
import { parseModule } from 'esprima'
|
||||
import fs from 'fs'
|
||||
|
||||
import { warning } from '../utils/log.js'
|
||||
import { log } from '../utils/log.js'
|
||||
|
||||
export const withPayloadImportStatement = `import { withPayload } from '@payloadcms/next'\n`
|
||||
|
||||
export const wrapNextConfig = (args: { nextConfigPath: string }) => {
|
||||
const { nextConfigPath } = args
|
||||
const configContent = fs.readFileSync(nextConfigPath, 'utf8')
|
||||
const { modifiedConfigContent: newConfig, success } = parseAndModifyConfigContent(configContent)
|
||||
|
||||
if (!success) {
|
||||
return
|
||||
}
|
||||
|
||||
fs.writeFileSync(nextConfigPath, newConfig)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 })
|
||||
const exportDefaultDeclaration = ast.body.find((p) => p.type === 'ExportDefaultDeclaration') as
|
||||
| Directive
|
||||
| undefined
|
||||
|
||||
const exportNamedDeclaration = ast.body.find((p) => p.type === 'ExportNamedDeclaration') as
|
||||
| ExportNamedDeclaration
|
||||
| undefined
|
||||
|
||||
if (!exportDefaultDeclaration && !exportNamedDeclaration) {
|
||||
throw new Error('Could not find ExportDefaultDeclaration in next.config.js')
|
||||
}
|
||||
|
||||
if (exportDefaultDeclaration && exportDefaultDeclaration.declaration?.loc) {
|
||||
const modifiedConfigContent = insertBeforeAndAfter(
|
||||
content,
|
||||
exportDefaultDeclaration.declaration.loc,
|
||||
)
|
||||
return { modifiedConfigContent, success: true }
|
||||
} else if (exportNamedDeclaration) {
|
||||
const exportSpecifier = exportNamedDeclaration.specifiers.find(
|
||||
(s) =>
|
||||
s.type === 'ExportSpecifier' &&
|
||||
s.exported?.name === 'default' &&
|
||||
s.local?.type === 'Identifier' &&
|
||||
s.local?.name,
|
||||
)
|
||||
|
||||
if (exportSpecifier) {
|
||||
warning('Could not automatically wrap next.config.js with withPayload.')
|
||||
warning('Automatic wrapping of named exports as default not supported yet.')
|
||||
|
||||
warnUserWrapNotSuccessful()
|
||||
return {
|
||||
modifiedConfigContent: content,
|
||||
success: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 = {
|
||||
declaration?: {
|
||||
loc: Loc
|
||||
}
|
||||
}
|
||||
|
||||
type ExportNamedDeclaration = {
|
||||
declaration: null
|
||||
loc: Loc
|
||||
specifiers: {
|
||||
exported: {
|
||||
loc: Loc
|
||||
name: string
|
||||
type: string
|
||||
}
|
||||
loc: Loc
|
||||
local: {
|
||||
loc: Loc
|
||||
name: string
|
||||
type: string
|
||||
}
|
||||
type: string
|
||||
}[]
|
||||
type: string
|
||||
}
|
||||
|
||||
type Loc = {
|
||||
end: { column: number; line: number }
|
||||
start: { column: number; line: number }
|
||||
}
|
||||
|
||||
function insertBeforeAndAfter(content: string, loc: Loc) {
|
||||
const { end, start } = loc
|
||||
const lines = content.split('\n')
|
||||
|
||||
const insert = (line: string, column: number, text: string) => {
|
||||
return line.slice(0, column) + text + line.slice(column)
|
||||
}
|
||||
|
||||
// insert ) after end
|
||||
lines[end.line - 1] = insert(lines[end.line - 1], end.column, ')')
|
||||
// insert withPayload before start
|
||||
if (start.line === end.line) {
|
||||
lines[end.line - 1] = insert(lines[end.line - 1], start.column, 'withPayload(')
|
||||
} else {
|
||||
lines[start.line - 1] = insert(lines[start.line - 1], start.column, 'withPayload(')
|
||||
}
|
||||
|
||||
return lines.join('\n')
|
||||
}
|
||||
@@ -1,20 +1,27 @@
|
||||
import fs from 'fs-extra'
|
||||
import path from 'path'
|
||||
|
||||
import type { ProjectTemplate } from '../types.js'
|
||||
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: {
|
||||
cliArgs: CliArgs
|
||||
databaseUri: string
|
||||
payloadSecret: string
|
||||
projectDir: string
|
||||
template: ProjectTemplate
|
||||
template?: ProjectTemplate
|
||||
}): Promise<void> {
|
||||
const { databaseUri, payloadSecret, projectDir, template } = args
|
||||
const { cliArgs, databaseUri, payloadSecret, projectDir, template } = args
|
||||
|
||||
if (cliArgs['--dry-run']) {
|
||||
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
|
||||
@@ -40,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) {
|
||||
|
||||
@@ -1,19 +1,31 @@
|
||||
import * as p from '@clack/prompts'
|
||||
import slugify from '@sindresorhus/slugify'
|
||||
import arg from 'arg'
|
||||
import commandExists from 'command-exists'
|
||||
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
|
||||
@@ -23,13 +35,17 @@ export class Main {
|
||||
this.args = arg(
|
||||
{
|
||||
'--db': String,
|
||||
'--db-accept-recommended': Boolean,
|
||||
'--db-connection-string': String,
|
||||
'--help': Boolean,
|
||||
'--local-template': String,
|
||||
'--name': String,
|
||||
'--secret': String,
|
||||
'--template': String,
|
||||
'--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,
|
||||
@@ -55,41 +71,107 @@ export class Main {
|
||||
async init(): Promise<void> {
|
||||
try {
|
||||
if (this.args['--help']) {
|
||||
console.log(helpMessage())
|
||||
helpMessage()
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
if (this.args['--init-next']) {
|
||||
const result = await initNext(this.args)
|
||||
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')
|
||||
// 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))
|
||||
|
||||
const packageManager = await getPackageManager(this.args, projectDir)
|
||||
|
||||
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)
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
||||
console.log(welcomeMessage)
|
||||
const projectName = await parseProjectName(this.args)
|
||||
const validTemplates = getValidTemplates()
|
||||
const template = await parseTemplate(this.args, validTemplates)
|
||||
if (!template) {
|
||||
p.log.error('Invalid template given')
|
||||
p.outro(feedbackOutro())
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const projectDir = projectName === '.' ? process.cwd() : `./${slugify(projectName)}`
|
||||
const packageManager = await getPackageManager(this.args)
|
||||
|
||||
if (template.type !== 'plugin') {
|
||||
const dbDetails = await selectDb(this.args, projectName)
|
||||
const payloadSecret = generateSecret()
|
||||
if (!this.args['--dry-run']) {
|
||||
switch (template.type) {
|
||||
case 'starter': {
|
||||
const dbDetails = await selectDb(this.args, projectName)
|
||||
const payloadSecret = generateSecret()
|
||||
await createProject({
|
||||
cliArgs: this.args,
|
||||
dbDetails,
|
||||
@@ -99,14 +181,15 @@ export class Main {
|
||||
template,
|
||||
})
|
||||
await writeEnvFile({
|
||||
cliArgs: this.args,
|
||||
databaseUri: dbDetails.dbUri,
|
||||
payloadSecret,
|
||||
projectDir,
|
||||
template,
|
||||
})
|
||||
break
|
||||
}
|
||||
} else {
|
||||
if (!this.args['--dry-run']) {
|
||||
case 'plugin': {
|
||||
await createProject({
|
||||
cliArgs: this.args,
|
||||
packageManager,
|
||||
@@ -114,18 +197,20 @@ export class Main {
|
||||
projectName,
|
||||
template,
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
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')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function getPackageManager(args: CliArgs): Promise<PackageManager> {
|
||||
async function getPackageManager(args: CliArgs, projectDir: string): Promise<PackageManager> {
|
||||
let packageManager: PackageManager = 'npm'
|
||||
|
||||
if (args['--use-npm']) {
|
||||
@@ -135,15 +220,8 @@ async function getPackageManager(args: CliArgs): Promise<PackageManager> {
|
||||
} else if (args['--use-pnpm']) {
|
||||
packageManager = 'pnpm'
|
||||
} else {
|
||||
try {
|
||||
if (await commandExists('yarn')) {
|
||||
packageManager = 'yarn'
|
||||
} else if (await commandExists('pnpm')) {
|
||||
packageManager = 'pnpm'
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
packageManager = 'npm'
|
||||
}
|
||||
const detected = await detect({ cwd: projectDir })
|
||||
packageManager = detected || 'npm'
|
||||
}
|
||||
return packageManager
|
||||
}
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
@@ -3,14 +3,18 @@ import type arg from 'arg'
|
||||
export interface Args extends arg.Spec {
|
||||
'--beta': BooleanConstructor
|
||||
'--db': StringConstructor
|
||||
'--db-accept-recommended': BooleanConstructor
|
||||
'--db-connection-string': StringConstructor
|
||||
'--debug': BooleanConstructor
|
||||
'--dry-run': BooleanConstructor
|
||||
'--help': BooleanConstructor
|
||||
'--init-next': BooleanConstructor
|
||||
'--local-template': StringConstructor
|
||||
'--name': StringConstructor
|
||||
'--no-deps': BooleanConstructor
|
||||
'--secret': StringConstructor
|
||||
'--template': StringConstructor
|
||||
'--template-branch': StringConstructor
|
||||
'--use-npm': BooleanConstructor
|
||||
'--use-pnpm': BooleanConstructor
|
||||
'--use-yarn': BooleanConstructor
|
||||
@@ -50,7 +54,7 @@ interface Template {
|
||||
type: ProjectTemplate['type']
|
||||
}
|
||||
|
||||
export type PackageManager = 'npm' | 'pnpm' | 'yarn'
|
||||
export type PackageManager = 'bun' | 'npm' | 'pnpm' | 'yarn'
|
||||
|
||||
export type DbType = 'mongodb' | 'postgres'
|
||||
|
||||
@@ -59,5 +63,4 @@ export type DbDetails = {
|
||||
type: DbType
|
||||
}
|
||||
|
||||
export type BundlerType = 'vite' | 'webpack'
|
||||
export type EditorType = 'lexical' | 'slate'
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -1,26 +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)
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
import type { ProjectTemplate } from '../types.js'
|
||||
import type { PackageManager } from '../types.js'
|
||||
|
||||
import { getValidTemplates } from '../lib/templates'
|
||||
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, {
|
||||
|
||||
@@ -5,19 +5,9 @@
|
||||
"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",
|
||||
"src/**/*.spec.js",
|
||||
"src/**/*.spec.jsx",
|
||||
"src/**/*.spec.ts",
|
||||
"src/**/*.spec.tsx"
|
||||
],
|
||||
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.d.ts", "src/**/*.json"]
|
||||
"exclude": ["dist", "build", "tests", "test", "node_modules", ".eslintrc.js"],
|
||||
"include": ["src/**/*.ts", "src/**/*.spec.ts", "src/**/*.tsx", "src/**/*.d.ts", "src/**/*.json"]
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
"version": "3.0.0-alpha.54",
|
||||
"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"
|
||||
|
||||
@@ -45,7 +45,9 @@ export const createMigration: CreateMigration = async function createMigration({
|
||||
|
||||
// Check if predefined migration exists
|
||||
if (fs.existsSync(cleanPath)) {
|
||||
let migration = await eval(`${require ? 'require' : 'import'}(${cleanPath})`)
|
||||
let migration = await eval(
|
||||
`${typeof require === 'function' ? 'require' : 'import'}(${cleanPath})`,
|
||||
)
|
||||
if ('default' in migration) migration = migration.default
|
||||
const { down, up } = migration
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -26,6 +26,7 @@ export const updateOne: UpdateOne = async function updateOne(
|
||||
})
|
||||
|
||||
let result
|
||||
|
||||
try {
|
||||
result = await Model.findOneAndUpdate(query, data, options)
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
{
|
||||
"name": "@payloadcms/db-postgres",
|
||||
"version": "3.0.0-alpha.49",
|
||||
"version": "3.0.0-alpha.54",
|
||||
"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",
|
||||
|
||||
@@ -3,15 +3,17 @@ import type { DrizzleSnapshotJSON } from 'drizzle-kit/payload'
|
||||
import type { CreateMigration } from 'payload/database'
|
||||
|
||||
import fs from 'fs'
|
||||
import { createRequire } from 'module'
|
||||
import prompts from 'prompts'
|
||||
|
||||
import type { PostgresAdapter } from './types.js'
|
||||
|
||||
const require = createRequire(import.meta.url)
|
||||
|
||||
const migrationTemplate = (
|
||||
upSQL?: string,
|
||||
downSQL?: string,
|
||||
) => `import { MigrateUpArgs, MigrateDownArgs } from '@payloadcms/db-postgres'
|
||||
import { sql } from 'drizzle-orm'
|
||||
) => `import { MigrateUpArgs, MigrateDownArgs, sql } from '@payloadcms/db-postgres'
|
||||
|
||||
export async function up({ payload }: MigrateUpArgs): Promise<void> {
|
||||
${
|
||||
@@ -60,9 +62,7 @@ export const createMigration: CreateMigration = async function createMigration(
|
||||
fs.mkdirSync(dir)
|
||||
}
|
||||
|
||||
const { generateDrizzleJson, generateMigration } = require
|
||||
? require('drizzle-kit/payload')
|
||||
: await import('drizzle-kit/payload')
|
||||
const { generateDrizzleJson, generateMigration } = require('drizzle-kit/payload')
|
||||
|
||||
const [yyymmdd, hhmmss] = new Date().toISOString().split('T')
|
||||
const formattedDate = yyymmdd.replace(/\D/g, '')
|
||||
|
||||
@@ -1,47 +1,68 @@
|
||||
import type { DeleteOne } from 'payload/database'
|
||||
import type { PayloadRequest } from 'payload/types'
|
||||
|
||||
import { eq } from 'drizzle-orm'
|
||||
import toSnakeCase from 'to-snake-case'
|
||||
|
||||
import type { PostgresAdapter } from './types.js'
|
||||
|
||||
import { buildFindManyArgs } from './find/buildFindManyArgs.js'
|
||||
import buildQuery from './queries/buildQuery.js'
|
||||
import { selectDistinct } from './queries/selectDistinct.js'
|
||||
import { transform } from './transform/read/index.js'
|
||||
|
||||
export const deleteOne: DeleteOne = async function deleteOne(
|
||||
this: PostgresAdapter,
|
||||
{ collection, req = {} as PayloadRequest, where: incomingWhere },
|
||||
{ collection: collectionSlug, req = {} as PayloadRequest, where: whereArg },
|
||||
) {
|
||||
const db = this.sessions[req.transactionID]?.db || this.drizzle
|
||||
const collectionConfig = this.payload.collections[collection].config
|
||||
const tableName = toSnakeCase(collection)
|
||||
const collection = this.payload.collections[collectionSlug].config
|
||||
const tableName = toSnakeCase(collectionSlug)
|
||||
let docToDelete: Record<string, unknown>
|
||||
|
||||
const { where } = await buildQuery({
|
||||
const { joinAliases, joins, selectFields, where } = await buildQuery({
|
||||
adapter: this,
|
||||
fields: collectionConfig.fields,
|
||||
fields: collection.fields,
|
||||
locale: req.locale,
|
||||
tableName,
|
||||
where: incomingWhere,
|
||||
where: whereArg,
|
||||
})
|
||||
|
||||
const findManyArgs = buildFindManyArgs({
|
||||
const selectDistinctResult = await selectDistinct({
|
||||
adapter: this,
|
||||
depth: 0,
|
||||
fields: collectionConfig.fields,
|
||||
chainedMethods: [{ args: [1], method: 'limit' }],
|
||||
db,
|
||||
joinAliases,
|
||||
joins,
|
||||
selectFields,
|
||||
tableName,
|
||||
where,
|
||||
})
|
||||
|
||||
findManyArgs.where = where
|
||||
if (selectDistinctResult?.[0]?.id) {
|
||||
docToDelete = await db.query[tableName].findFirst({
|
||||
where: eq(this.tables[tableName].id, selectDistinctResult[0].id),
|
||||
})
|
||||
} else {
|
||||
const findManyArgs = buildFindManyArgs({
|
||||
adapter: this,
|
||||
depth: 0,
|
||||
fields: collection.fields,
|
||||
tableName,
|
||||
})
|
||||
|
||||
const docToDelete = await db.query[tableName].findFirst(findManyArgs)
|
||||
findManyArgs.where = where
|
||||
|
||||
docToDelete = await db.query[tableName].findFirst(findManyArgs)
|
||||
}
|
||||
|
||||
const result = transform({
|
||||
config: this.payload.config,
|
||||
data: docToDelete,
|
||||
fields: collectionConfig.fields,
|
||||
fields: collection.fields,
|
||||
})
|
||||
|
||||
await db.delete(this.tables[tableName]).where(where)
|
||||
await db.delete(this.tables[tableName]).where(eq(this.tables[tableName].id, docToDelete.id))
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { QueryPromise } from 'drizzle-orm'
|
||||
|
||||
export type ChainedMethods = {
|
||||
args: unknown[]
|
||||
method: string
|
||||
@@ -8,7 +10,7 @@ export type ChainedMethods = {
|
||||
* @param methods
|
||||
* @param query
|
||||
*/
|
||||
const chainMethods = ({ methods, query }): Promise<unknown> => {
|
||||
const chainMethods = <T>({ methods, query }): QueryPromise<T> => {
|
||||
return methods.reduce((query, { args, method }) => {
|
||||
return query[method](...args)
|
||||
}, query)
|
||||
|
||||
@@ -7,6 +7,7 @@ import type { PostgresAdapter } from '../types.js'
|
||||
import type { ChainedMethods } from './chainMethods.js'
|
||||
|
||||
import buildQuery from '../queries/buildQuery.js'
|
||||
import { selectDistinct } from '../queries/selectDistinct.js'
|
||||
import { transform } from '../transform/read/index.js'
|
||||
import { buildFindManyArgs } from './buildFindManyArgs.js'
|
||||
import { chainMethods } from './chainMethods.js'
|
||||
@@ -39,7 +40,6 @@ export const findMany = async function find({
|
||||
let hasPrevPage: boolean
|
||||
let hasNextPage: boolean
|
||||
let pagingCounter: number
|
||||
let selectDistinctResult
|
||||
|
||||
const { joinAliases, joins, orderBy, selectFields, where } = await buildQuery({
|
||||
adapter,
|
||||
@@ -69,36 +69,21 @@ export const findMany = async function find({
|
||||
tableName,
|
||||
})
|
||||
|
||||
// only fetch IDs when a sort or where query is used that needs to be done on join tables, otherwise these can be done directly on the table in findMany
|
||||
if (Object.keys(joins).length > 0 || joinAliases.length > 0) {
|
||||
if (where) {
|
||||
selectDistinctMethods.push({ args: [where], method: 'where' })
|
||||
}
|
||||
selectDistinctMethods.push({ args: [skip || (page - 1) * limit], method: 'offset' })
|
||||
selectDistinctMethods.push({ args: [limit === 0 ? undefined : limit], method: 'limit' })
|
||||
|
||||
joinAliases.forEach(({ condition, table }) => {
|
||||
selectDistinctMethods.push({
|
||||
args: [table, condition],
|
||||
method: 'leftJoin',
|
||||
})
|
||||
})
|
||||
|
||||
Object.entries(joins).forEach(([joinTable, condition]) => {
|
||||
if (joinTable) {
|
||||
selectDistinctMethods.push({
|
||||
args: [adapter.tables[joinTable], condition],
|
||||
method: 'leftJoin',
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
selectDistinctMethods.push({ args: [skip || (page - 1) * limit], method: 'offset' })
|
||||
selectDistinctMethods.push({ args: [limit === 0 ? undefined : limit], method: 'limit' })
|
||||
|
||||
selectDistinctResult = await chainMethods({
|
||||
methods: selectDistinctMethods,
|
||||
query: db.selectDistinct(selectFields).from(table),
|
||||
})
|
||||
const selectDistinctResult = await selectDistinct({
|
||||
adapter,
|
||||
chainedMethods: selectDistinctMethods,
|
||||
db,
|
||||
joinAliases,
|
||||
joins,
|
||||
selectFields,
|
||||
tableName,
|
||||
where,
|
||||
})
|
||||
|
||||
if (selectDistinctResult) {
|
||||
if (selectDistinctResult.length === 0) {
|
||||
return {
|
||||
docs: [],
|
||||
@@ -112,13 +97,14 @@ export const findMany = async function find({
|
||||
totalDocs: 0,
|
||||
totalPages: 0,
|
||||
}
|
||||
} else {
|
||||
// set the id in an object for sorting later
|
||||
selectDistinctResult.forEach(({ id }, i) => {
|
||||
orderedIDMap[id] = i
|
||||
})
|
||||
orderedIDs = Object.keys(orderedIDMap)
|
||||
findManyArgs.where = inArray(adapter.tables[tableName].id, orderedIDs)
|
||||
}
|
||||
// set the id in an object for sorting later
|
||||
selectDistinctResult.forEach(({ id }, i) => {
|
||||
orderedIDMap[id as number | string] = i
|
||||
})
|
||||
orderedIDs = Object.keys(orderedIDMap)
|
||||
findManyArgs.where = inArray(adapter.tables[tableName].id, orderedIDs)
|
||||
} else {
|
||||
findManyArgs.limit = limitArg === 0 ? undefined : limitArg
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { Payload } from 'payload'
|
||||
import type { DatabaseAdapterObj } from 'payload/database'
|
||||
export type { MigrateDownArgs, MigrateUpArgs } from './types.js'
|
||||
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
@@ -38,7 +39,7 @@ import { updateGlobal } from './updateGlobal.js'
|
||||
import { updateGlobalVersion } from './updateGlobalVersion.js'
|
||||
import { updateVersion } from './updateVersion.js'
|
||||
|
||||
export type { MigrateDownArgs, MigrateUpArgs } from './types.js'
|
||||
export { sql } from 'drizzle-orm'
|
||||
|
||||
export function postgresAdapter(args: Args): DatabaseAdapterObj<PostgresAdapter> {
|
||||
function adapter({ payload }: { payload: Payload }) {
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { Payload } from 'payload'
|
||||
import type { Migration } from 'payload/database'
|
||||
import type { PayloadRequest } from 'payload/types'
|
||||
|
||||
import { createRequire } from 'module'
|
||||
import {
|
||||
commitTransaction,
|
||||
initTransaction,
|
||||
@@ -17,6 +18,8 @@ import { createMigrationTable } from './utilities/createMigrationTable.js'
|
||||
import { migrationTableExists } from './utilities/migrationTableExists.js'
|
||||
import { parseError } from './utilities/parseError.js'
|
||||
|
||||
const require = createRequire(import.meta.url)
|
||||
|
||||
export async function migrate(this: PostgresAdapter): Promise<void> {
|
||||
const { payload } = this
|
||||
const migrationFiles = await readMigrationFiles({ payload })
|
||||
@@ -82,9 +85,7 @@ export async function migrate(this: PostgresAdapter): Promise<void> {
|
||||
}
|
||||
|
||||
async function runMigrationFile(payload: Payload, migration: Migration, batch: number) {
|
||||
const { generateDrizzleJson } = require
|
||||
? require('drizzle-kit/payload')
|
||||
: await import('drizzle-kit/payload')
|
||||
const { generateDrizzleJson } = require('drizzle-kit/payload')
|
||||
|
||||
const start = Date.now()
|
||||
const req = { payload } as PayloadRequest
|
||||
|
||||
@@ -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))) {
|
||||
@@ -85,6 +85,10 @@ export const sanitizeQueryValue = ({
|
||||
}
|
||||
}
|
||||
|
||||
if ('hasMany' in field && field.hasMany && operator === 'contains') {
|
||||
operator = 'equals'
|
||||
}
|
||||
|
||||
if (operator === 'near' || operator === 'within' || operator === 'intersects') {
|
||||
throw new APIError(
|
||||
`Querying with '${operator}' is not supported with the postgres database adapter.`,
|
||||
|
||||
60
packages/db-postgres/src/queries/selectDistinct.ts
Normal file
60
packages/db-postgres/src/queries/selectDistinct.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import type { QueryPromise, SQL } from 'drizzle-orm'
|
||||
|
||||
import type { ChainedMethods } from '../find/chainMethods.js'
|
||||
import type { DrizzleDB, PostgresAdapter } from '../types.js'
|
||||
import type { BuildQueryJoinAliases, BuildQueryJoins } from './buildQuery.js'
|
||||
|
||||
import { chainMethods } from '../find/chainMethods.js'
|
||||
import { type GenericColumn } from '../types.js'
|
||||
|
||||
type Args = {
|
||||
adapter: PostgresAdapter
|
||||
chainedMethods?: ChainedMethods
|
||||
db: DrizzleDB
|
||||
joinAliases: BuildQueryJoinAliases
|
||||
joins: BuildQueryJoins
|
||||
selectFields: Record<string, GenericColumn>
|
||||
tableName: string
|
||||
where: SQL
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects distinct records from a table only if there are joins that need to be used, otherwise return null
|
||||
*/
|
||||
export const selectDistinct = ({
|
||||
adapter,
|
||||
chainedMethods = [],
|
||||
db,
|
||||
joinAliases,
|
||||
joins,
|
||||
selectFields,
|
||||
tableName,
|
||||
where,
|
||||
}: Args): QueryPromise<Record<string, GenericColumn> & { id: number | string }[]> => {
|
||||
if (Object.keys(joins).length > 0 || joinAliases.length > 0) {
|
||||
if (where) {
|
||||
chainedMethods.push({ args: [where], method: 'where' })
|
||||
}
|
||||
|
||||
joinAliases.forEach(({ condition, table }) => {
|
||||
chainedMethods.push({
|
||||
args: [table, condition],
|
||||
method: 'leftJoin',
|
||||
})
|
||||
})
|
||||
|
||||
Object.entries(joins).forEach(([joinTable, condition]) => {
|
||||
if (joinTable) {
|
||||
chainedMethods.push({
|
||||
args: [adapter.tables[joinTable], condition],
|
||||
method: 'leftJoin',
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return chainMethods({
|
||||
methods: chainedMethods,
|
||||
query: db.selectDistinct(selectFields).from(adapter.tables[tableName]),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,10 @@ import type { UpdateOne } from 'payload/database'
|
||||
|
||||
import toSnakeCase from 'to-snake-case'
|
||||
|
||||
import type { ChainedMethods } from './find/chainMethods.js'
|
||||
import type { PostgresAdapter } from './types.js'
|
||||
|
||||
import { chainMethods } from './find/chainMethods.js'
|
||||
import buildQuery from './queries/buildQuery.js'
|
||||
import { selectDistinct } from './queries/selectDistinct.js'
|
||||
import { upsertRow } from './upsertRow/index.js'
|
||||
|
||||
export const updateOne: UpdateOne = async function updateOne(
|
||||
@@ -17,6 +16,7 @@ export const updateOne: UpdateOne = async function updateOne(
|
||||
const collection = this.payload.collections[collectionSlug].config
|
||||
const tableName = toSnakeCase(collectionSlug)
|
||||
const whereToUse = whereArg || { id: { equals: id } }
|
||||
let idToUpdate = id
|
||||
|
||||
const { joinAliases, joins, selectFields, where } = await buildQuery({
|
||||
adapter: this,
|
||||
@@ -26,42 +26,19 @@ export const updateOne: UpdateOne = async function updateOne(
|
||||
where: whereToUse,
|
||||
})
|
||||
|
||||
let idToUpdate = id
|
||||
const selectDistinctResult = await selectDistinct({
|
||||
adapter: this,
|
||||
chainedMethods: [{ args: [1], method: 'limit' }],
|
||||
db,
|
||||
joinAliases,
|
||||
joins,
|
||||
selectFields,
|
||||
tableName,
|
||||
where,
|
||||
})
|
||||
|
||||
// only fetch IDs when a sort or where query is used that needs to be done on join tables, otherwise these can be done directly on the table in findMany
|
||||
if (Object.keys(joins).length > 0 || joinAliases.length > 0) {
|
||||
const selectDistinctMethods: ChainedMethods = []
|
||||
|
||||
if (where) {
|
||||
selectDistinctMethods.push({ args: [where], method: 'where' })
|
||||
}
|
||||
|
||||
joinAliases.forEach(({ condition, table }) => {
|
||||
selectDistinctMethods.push({
|
||||
args: [table, condition],
|
||||
method: 'leftJoin',
|
||||
})
|
||||
})
|
||||
|
||||
Object.entries(joins).forEach(([joinTable, condition]) => {
|
||||
if (joinTable) {
|
||||
selectDistinctMethods.push({
|
||||
args: [this.tables[joinTable], condition],
|
||||
method: 'leftJoin',
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
selectDistinctMethods.push({ args: [1], method: 'limit' })
|
||||
|
||||
const selectDistinctResult = await chainMethods({
|
||||
methods: selectDistinctMethods,
|
||||
query: db.selectDistinct(selectFields).from(this.tables[tableName]),
|
||||
})
|
||||
|
||||
if (selectDistinctResult?.[0]?.id) {
|
||||
idToUpdate = selectDistinctResult?.[0]?.id
|
||||
}
|
||||
if (selectDistinctResult?.[0]?.id) {
|
||||
idToUpdate = selectDistinctResult?.[0]?.id
|
||||
}
|
||||
|
||||
const result = await upsertRow({
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { eq } from 'drizzle-orm'
|
||||
import { numeric, timestamp, varchar } from 'drizzle-orm/pg-core'
|
||||
import { createRequire } from 'module'
|
||||
import prompts from 'prompts'
|
||||
|
||||
import type { PostgresAdapter } from '../types.js'
|
||||
|
||||
const require = createRequire(import.meta.url)
|
||||
|
||||
/**
|
||||
* Pushes the development schema to the database using Drizzle.
|
||||
*
|
||||
@@ -11,9 +14,7 @@ import type { PostgresAdapter } from '../types.js'
|
||||
* @returns {Promise<void>} - A promise that resolves once the schema push is complete.
|
||||
*/
|
||||
export const pushDevSchema = async (db: PostgresAdapter) => {
|
||||
const { pushSchema } = require
|
||||
? require('drizzle-kit/payload')
|
||||
: await import('drizzle-kit/payload')
|
||||
const { pushSchema } = require('drizzle-kit/payload')
|
||||
|
||||
// This will prompt if clarifications are needed for Drizzle to push new schema
|
||||
const { apply, hasDataLoss, statementsToExecute, warnings } = await pushSchema(
|
||||
|
||||
@@ -22,6 +22,7 @@ const baseRules = {
|
||||
},
|
||||
},
|
||||
],
|
||||
'payload/no-jsx-import-statements': 'error',
|
||||
}
|
||||
|
||||
const reactRules = {
|
||||
@@ -112,7 +113,7 @@ module.exports = {
|
||||
overrides: [
|
||||
{
|
||||
files: ['**/*.ts'],
|
||||
plugins: ['@typescript-eslint'],
|
||||
plugins: ['@typescript-eslint', 'payload'],
|
||||
extends: [
|
||||
...baseExtends,
|
||||
'plugin:@typescript-eslint/recommended-type-checked',
|
||||
@@ -126,7 +127,7 @@ module.exports = {
|
||||
},
|
||||
{
|
||||
files: ['**/*.tsx'],
|
||||
plugins: ['@typescript-eslint'],
|
||||
plugins: ['@typescript-eslint', 'payload'],
|
||||
extends: [
|
||||
...baseExtends,
|
||||
'plugin:@typescript-eslint/recommended-type-checked',
|
||||
@@ -144,7 +145,7 @@ module.exports = {
|
||||
},
|
||||
{
|
||||
files: ['**/*.spec.ts'],
|
||||
plugins: ['@typescript-eslint'],
|
||||
plugins: ['@typescript-eslint', 'payload'],
|
||||
extends: [
|
||||
...baseExtends,
|
||||
'plugin:@typescript-eslint/recommended-type-checked',
|
||||
@@ -159,6 +160,7 @@ module.exports = {
|
||||
},
|
||||
},
|
||||
{
|
||||
plugins: ['payload'],
|
||||
files: ['*.config.ts'],
|
||||
rules: {
|
||||
...baseRules,
|
||||
@@ -167,6 +169,7 @@ module.exports = {
|
||||
},
|
||||
},
|
||||
{
|
||||
plugins: ['payload'],
|
||||
files: ['config.ts'],
|
||||
rules: {
|
||||
...baseRules,
|
||||
|
||||
@@ -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",
|
||||
@@ -25,7 +31,8 @@
|
||||
"eslint-plugin-perfectionist": "2.7.0",
|
||||
"eslint-plugin-react": "7.34.1",
|
||||
"eslint-plugin-react-hooks": "4.6.0",
|
||||
"eslint-plugin-regexp": "2.3.0"
|
||||
"eslint-plugin-regexp": "2.3.0",
|
||||
"eslint-plugin-payload": "workspace:*"
|
||||
},
|
||||
"keywords": []
|
||||
}
|
||||
|
||||
@@ -43,40 +43,74 @@ module.exports = {
|
||||
'stringMatching',
|
||||
]
|
||||
|
||||
function isNonRetryableAssertion(node) {
|
||||
return (
|
||||
node.type === 'MemberExpression' &&
|
||||
node.property.type === 'Identifier' &&
|
||||
nonRetryableAssertions.includes(node.property.name)
|
||||
)
|
||||
}
|
||||
|
||||
function isExpectPollOrToPass(node) {
|
||||
if (
|
||||
node.type === 'MemberExpression' &&
|
||||
(node?.property?.name === 'poll' || node?.property?.name === 'toPass')
|
||||
) {
|
||||
return true
|
||||
}
|
||||
|
||||
return (
|
||||
node.type === 'CallExpression' &&
|
||||
node.callee.type === 'MemberExpression' &&
|
||||
((node.callee.object.type === 'CallExpression' &&
|
||||
node.callee.object.callee.type === 'MemberExpression' &&
|
||||
node.callee.object.callee.property.name === 'poll') ||
|
||||
node.callee.property.name === 'toPass')
|
||||
)
|
||||
}
|
||||
|
||||
function hasExpectPollOrToPassInChain(node) {
|
||||
let ancestor = node
|
||||
|
||||
while (ancestor) {
|
||||
if (isExpectPollOrToPass(ancestor)) {
|
||||
return true
|
||||
}
|
||||
ancestor = 'object' in ancestor ? ancestor.object : ancestor.callee
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
function hasExpectPollOrToPassInParentChain(node) {
|
||||
let ancestor = node
|
||||
|
||||
while (ancestor) {
|
||||
if (isExpectPollOrToPass(ancestor)) {
|
||||
return true
|
||||
}
|
||||
ancestor = ancestor.parent
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
return {
|
||||
CallExpression(node) {
|
||||
if (
|
||||
node.callee.type === 'MemberExpression' &&
|
||||
//node.callee.object.name === 'expect' &&
|
||||
node.callee.property.type === 'Identifier' &&
|
||||
nonRetryableAssertions.includes(node.callee.property.name)
|
||||
) {
|
||||
let ancestor = node
|
||||
let hasExpectPollOrToPass = false
|
||||
|
||||
while (ancestor) {
|
||||
if (
|
||||
ancestor.type === 'CallExpression' &&
|
||||
ancestor.callee.type === 'MemberExpression' &&
|
||||
((ancestor.callee.object.type === 'CallExpression' &&
|
||||
ancestor.callee.object.callee.type === 'MemberExpression' &&
|
||||
ancestor.callee.object.callee.property.name === 'poll') ||
|
||||
ancestor.callee.property.name === 'toPass')
|
||||
) {
|
||||
hasExpectPollOrToPass = true
|
||||
break
|
||||
}
|
||||
ancestor = ancestor.parent
|
||||
// node.callee is MemberExpressiom
|
||||
if (isNonRetryableAssertion(node.callee)) {
|
||||
if (hasExpectPollOrToPassInChain(node.callee)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (hasExpectPollOrToPass) {
|
||||
if (hasExpectPollOrToPassInParentChain(node)) {
|
||||
return
|
||||
}
|
||||
|
||||
context.report({
|
||||
node: node.callee.property,
|
||||
message:
|
||||
'Non-retryable, flaky assertion used in Playwright test: "{{ assertion }}". Those need to be wrapped in expect.poll() or expect().toPass.',
|
||||
'Non-retryable, flaky assertion used in Playwright test: "{{ assertion }}". Those need to be wrapped in expect.poll() or expect().toPass().',
|
||||
data: {
|
||||
assertion: node.callee.property.name,
|
||||
},
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
{
|
||||
"name": "@payloadcms/graphql",
|
||||
"version": "3.0.0-alpha.49",
|
||||
"version": "3.0.0-alpha.54",
|
||||
"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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
{
|
||||
"name": "@payloadcms/next",
|
||||
"version": "3.0.0-alpha.49",
|
||||
"version": "3.0.0-alpha.54",
|
||||
"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"
|
||||
},
|
||||
@@ -74,7 +80,7 @@
|
||||
},
|
||||
"publishConfig": {
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
"./css": {
|
||||
"import": "./dist/prod/styles.css",
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
45
packages/next/src/routes/rest/collections/preview.ts
Normal file
45
packages/next/src/routes/rest/collections/preview.ts
Normal 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,
|
||||
})
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import path from 'path'
|
||||
import { APIError } from 'payload/errors'
|
||||
|
||||
import { streamFile } from '../../../next-stream-file/index.js'
|
||||
import { RouteError } from '../RouteError.js'
|
||||
import { routeError } from '../routeError.js'
|
||||
import { checkFileAccess } from './checkFileAccess.js'
|
||||
|
||||
// /:collectionSlug/file/:filename
|
||||
@@ -64,7 +64,7 @@ export const getFile = async ({ collection, filename, req }: Args): Promise<Resp
|
||||
status: httpStatus.OK,
|
||||
})
|
||||
} catch (error) {
|
||||
return RouteError({
|
||||
return routeError({
|
||||
collection,
|
||||
err: error,
|
||||
req,
|
||||
|
||||
44
packages/next/src/routes/rest/globals/preview.ts
Normal file
44
packages/next/src/routes/rest/globals/preview.ts
Normal 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,
|
||||
})
|
||||
}
|
||||
@@ -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,
|
||||
@@ -281,7 +301,7 @@ export const GET =
|
||||
|
||||
return RouteNotFoundResponse(slug)
|
||||
} catch (error) {
|
||||
return RouteError({
|
||||
return routeError({
|
||||
collection,
|
||||
err: error,
|
||||
req,
|
||||
@@ -423,7 +443,7 @@ export const POST =
|
||||
|
||||
return RouteNotFoundResponse(slug)
|
||||
} catch (error) {
|
||||
return RouteError({
|
||||
return routeError({
|
||||
collection,
|
||||
err: error,
|
||||
req,
|
||||
@@ -492,7 +512,7 @@ export const DELETE =
|
||||
|
||||
return RouteNotFoundResponse(slug)
|
||||
} catch (error) {
|
||||
return RouteError({
|
||||
return routeError({
|
||||
collection,
|
||||
err: error,
|
||||
req,
|
||||
@@ -561,7 +581,7 @@ export const PATCH =
|
||||
|
||||
return RouteNotFoundResponse(slug)
|
||||
} catch (error) {
|
||||
return RouteError({
|
||||
return routeError({
|
||||
collection,
|
||||
err: error,
|
||||
req,
|
||||
|
||||
@@ -1,13 +1,21 @@
|
||||
import type { Collection, PayloadRequest } from 'payload/types'
|
||||
|
||||
import httpStatus from 'http-status'
|
||||
import { APIError } from 'payload/errors'
|
||||
import { APIError, ValidationError } from 'payload/errors'
|
||||
|
||||
export type ErrorResponse = { data?: any; errors: unknown[]; stack?: string }
|
||||
|
||||
const formatErrors = (incoming: { [key: string]: unknown } | APIError | Error): ErrorResponse => {
|
||||
const formatErrors = (incoming: { [key: string]: unknown } | APIError): ErrorResponse => {
|
||||
if (incoming) {
|
||||
if (incoming instanceof APIError && incoming.data) {
|
||||
// Cannot use `instanceof` to check error type: https://github.com/microsoft/TypeScript/issues/13965
|
||||
// Instead, get the prototype of the incoming error and check its constructor name
|
||||
const proto = Object.getPrototypeOf(incoming)
|
||||
|
||||
// Payload 'ValidationError' and 'APIError'
|
||||
if (
|
||||
(proto.constructor.name === 'ValidationError' || proto.constructor.name === 'APIError') &&
|
||||
incoming.data
|
||||
) {
|
||||
return {
|
||||
errors: [
|
||||
{
|
||||
@@ -19,8 +27,8 @@ const formatErrors = (incoming: { [key: string]: unknown } | APIError | Error):
|
||||
}
|
||||
}
|
||||
|
||||
// mongoose
|
||||
if (!(incoming instanceof APIError || incoming instanceof Error) && incoming.errors) {
|
||||
// Mongoose 'ValidationError': https://mongoosejs.com/docs/api/error.html#Error.ValidationError
|
||||
if (proto.constructor.name === 'ValidationError' && 'errors' in incoming && incoming.errors) {
|
||||
return {
|
||||
errors: Object.keys(incoming.errors).reduce((acc, key) => {
|
||||
acc.push({
|
||||
@@ -58,7 +66,7 @@ const formatErrors = (incoming: { [key: string]: unknown } | APIError | Error):
|
||||
}
|
||||
}
|
||||
|
||||
export const RouteError = async ({
|
||||
export const routeError = ({
|
||||
collection,
|
||||
err,
|
||||
req,
|
||||
@@ -78,7 +86,9 @@ export const RouteError = async ({
|
||||
}
|
||||
|
||||
const { config, logger } = req.payload
|
||||
|
||||
let response = formatErrors(err)
|
||||
|
||||
let status = err.status || httpStatus.INTERNAL_SERVER_ERROR
|
||||
|
||||
logger.error(err.stack)
|
||||
@@ -94,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,
|
||||
})
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { Field, SanitizedConfig } from 'payload/types'
|
||||
|
||||
import { sanitizeFields } from 'payload/config'
|
||||
import { tabHasName } from 'payload/types'
|
||||
|
||||
import type { FieldSchemaMap } from './types.js'
|
||||
@@ -21,16 +20,10 @@ export const traverseFields = ({
|
||||
validRelationships,
|
||||
}: Args) => {
|
||||
fields.map((field) => {
|
||||
let fieldsToSet
|
||||
switch (field.type) {
|
||||
case 'group':
|
||||
case 'array':
|
||||
fieldsToSet = sanitizeFields({
|
||||
config,
|
||||
fields: field.fields,
|
||||
validRelationships,
|
||||
})
|
||||
schemaMap.set(`${schemaPath}.${field.name}`, fieldsToSet)
|
||||
schemaMap.set(`${schemaPath}.${field.name}`, field.fields)
|
||||
|
||||
traverseFields({
|
||||
config,
|
||||
@@ -55,12 +48,8 @@ export const traverseFields = ({
|
||||
case 'blocks':
|
||||
field.blocks.map((block) => {
|
||||
const blockSchemaPath = `${schemaPath}.${field.name}.${block.slug}`
|
||||
fieldsToSet = sanitizeFields({
|
||||
config,
|
||||
fields: [...block.fields, { name: 'blockName', type: 'text' }],
|
||||
validRelationships,
|
||||
})
|
||||
schemaMap.set(blockSchemaPath, fieldsToSet)
|
||||
|
||||
schemaMap.set(blockSchemaPath, block.fields)
|
||||
|
||||
traverseFields({
|
||||
config,
|
||||
@@ -88,12 +77,7 @@ export const traverseFields = ({
|
||||
const tabSchemaPath = tabHasName(tab) ? `${schemaPath}.${tab.name}` : schemaPath
|
||||
|
||||
if (tabHasName(tab)) {
|
||||
fieldsToSet = sanitizeFields({
|
||||
config,
|
||||
fields: tab.fields,
|
||||
validRelationships,
|
||||
})
|
||||
schemaMap.set(tabSchemaPath, fieldsToSet)
|
||||
schemaMap.set(tabSchemaPath, tab.fields)
|
||||
}
|
||||
|
||||
traverseFields({
|
||||
|
||||
@@ -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`)
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,7 +14,7 @@ import { Settings } from './Settings/index.js'
|
||||
|
||||
export { generateAccountMetadata } from './meta.js'
|
||||
|
||||
export const Account: React.FC<AdminViewProps> = async ({ initPageResult, searchParams }) => {
|
||||
export const Account: React.FC<AdminViewProps> = ({ initPageResult, params, searchParams }) => {
|
||||
const {
|
||||
locale,
|
||||
permissions,
|
||||
@@ -27,7 +24,6 @@ export const Account: React.FC<AdminViewProps> = async ({ initPageResult, search
|
||||
payload: { config },
|
||||
user,
|
||||
},
|
||||
req,
|
||||
} = initPageResult
|
||||
|
||||
const {
|
||||
@@ -41,52 +37,9 @@ export const Account: React.FC<AdminViewProps> = async ({ initPageResult, search
|
||||
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,
|
||||
routeSegments: [],
|
||||
searchParams,
|
||||
}
|
||||
@@ -94,22 +47,13 @@ export const Account: React.FC<AdminViewProps> = async ({ initPageResult, search
|
||||
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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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} />)}
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -1,16 +1,29 @@
|
||||
import type { EditViewComponent } from 'payload/config'
|
||||
import type { SanitizedCollectionConfig, SanitizedGlobalConfig } from 'payload/types'
|
||||
|
||||
export const getCustomViewByPath = (
|
||||
import { isPathMatchingRoute } from '../Root/isPathMatchingRoute.js'
|
||||
|
||||
export const getCustomViewByRoute = ({
|
||||
baseRoute,
|
||||
currentRoute,
|
||||
views,
|
||||
}: {
|
||||
baseRoute: string
|
||||
currentRoute: string
|
||||
views:
|
||||
| SanitizedCollectionConfig['admin']['components']['views']
|
||||
| SanitizedGlobalConfig['admin']['components']['views'],
|
||||
path: string,
|
||||
): EditViewComponent => {
|
||||
| SanitizedGlobalConfig['admin']['components']['views']
|
||||
}): EditViewComponent => {
|
||||
if (typeof views?.Edit === 'object' && typeof views?.Edit !== 'function') {
|
||||
const foundViewConfig = Object.entries(views.Edit).find(([, view]) => {
|
||||
if (typeof view === 'object' && typeof view !== 'function' && 'path' in view) {
|
||||
return view.path === path
|
||||
const viewPath = `${baseRoute}${view.path}`
|
||||
|
||||
return isPathMatchingRoute({
|
||||
currentRoute,
|
||||
exact: true,
|
||||
path: viewPath,
|
||||
})
|
||||
}
|
||||
return false
|
||||
})?.[1]
|
||||
@@ -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,8 +7,6 @@ import type {
|
||||
SanitizedGlobalConfig,
|
||||
} from 'payload/types'
|
||||
|
||||
import { isEntityHidden } from 'payload/utilities'
|
||||
|
||||
import { APIView as DefaultAPIView } from '../API/index.js'
|
||||
import { EditView as DefaultEditView } from '../Edit/index.js'
|
||||
import { LivePreviewView as DefaultLivePreviewView } from '../LivePreview/index.js'
|
||||
@@ -16,7 +14,7 @@ import { Unauthorized } from '../Unauthorized/index.js'
|
||||
import { VersionView as DefaultVersionView } from '../Version/index.js'
|
||||
import { VersionsView as DefaultVersionsView } from '../Versions/index.js'
|
||||
import { getCustomViewByKey } from './getCustomViewByKey.js'
|
||||
import { getCustomViewByPath } from './getCustomViewByPath.js'
|
||||
import { getCustomViewByRoute } from './getCustomViewByRoute.js'
|
||||
|
||||
export const getViewsFromConfig = ({
|
||||
collectionConfig,
|
||||
@@ -24,7 +22,6 @@ export const getViewsFromConfig = ({
|
||||
docPermissions,
|
||||
globalConfig,
|
||||
routeSegments,
|
||||
user,
|
||||
}: {
|
||||
collectionConfig?: SanitizedCollectionConfig
|
||||
config: SanitizedConfig
|
||||
@@ -32,7 +29,6 @@ export const getViewsFromConfig = ({
|
||||
docPermissions: CollectionPermission | GlobalPermission
|
||||
globalConfig?: SanitizedGlobalConfig
|
||||
routeSegments: string[]
|
||||
user: User
|
||||
}): {
|
||||
CustomView: EditViewComponent
|
||||
DefaultView: EditViewComponent
|
||||
@@ -46,6 +42,10 @@ export const getViewsFromConfig = ({
|
||||
let CustomView: EditViewComponent = null
|
||||
let ErrorView: AdminViewComponent = null
|
||||
|
||||
const {
|
||||
routes: { admin: adminRoute },
|
||||
} = config
|
||||
|
||||
const views =
|
||||
(collectionConfig && collectionConfig?.admin?.components?.views) ||
|
||||
(globalConfig && globalConfig?.admin?.components?.views)
|
||||
@@ -65,87 +65,109 @@ export const getViewsFromConfig = ({
|
||||
}
|
||||
|
||||
if (!EditOverride) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const [collectionEntity, collectionSlug, createOrID, nestedViewSlug, segmentFive] =
|
||||
const [collectionEntity, collectionSlug, segment3, segment4, segment5, ...remainingSegments] =
|
||||
routeSegments
|
||||
|
||||
const {
|
||||
admin: { hidden },
|
||||
} = collectionConfig
|
||||
|
||||
if (isEntityHidden({ hidden, user })) {
|
||||
return null
|
||||
}
|
||||
|
||||
// `../:id`, or `../create`
|
||||
if (routeSegments.length === 3) {
|
||||
switch (createOrID) {
|
||||
case 'create': {
|
||||
if ('create' in docPermissions && docPermissions?.create?.permission) {
|
||||
CustomView = getCustomViewByKey(views, 'Default')
|
||||
DefaultView = DefaultEditView
|
||||
} else {
|
||||
ErrorView = Unauthorized
|
||||
switch (routeSegments.length) {
|
||||
case 3: {
|
||||
switch (segment3) {
|
||||
case 'create': {
|
||||
if ('create' in docPermissions && docPermissions?.create?.permission) {
|
||||
CustomView = getCustomViewByKey(views, 'Default')
|
||||
DefaultView = DefaultEditView
|
||||
} else {
|
||||
ErrorView = Unauthorized
|
||||
}
|
||||
break
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
default: {
|
||||
if (docPermissions?.read?.permission) {
|
||||
CustomView = getCustomViewByKey(views, 'Default')
|
||||
DefaultView = DefaultEditView
|
||||
} else {
|
||||
ErrorView = Unauthorized
|
||||
default: {
|
||||
if (docPermissions?.read?.permission) {
|
||||
CustomView = getCustomViewByKey(views, 'Default')
|
||||
DefaultView = DefaultEditView
|
||||
} else {
|
||||
ErrorView = Unauthorized
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// `../:id/api`, `../:id/preview`, `../:id/versions`, etc
|
||||
if (routeSegments?.length === 4) {
|
||||
switch (nestedViewSlug) {
|
||||
case 'api': {
|
||||
if (collectionConfig?.admin?.hideAPIURL !== true) {
|
||||
CustomView = getCustomViewByKey(views, 'API')
|
||||
DefaultView = DefaultAPIView
|
||||
// `../:id/api`, `../:id/preview`, `../:id/versions`, etc
|
||||
case 4: {
|
||||
switch (segment4) {
|
||||
case 'api': {
|
||||
if (collectionConfig?.admin?.hideAPIURL !== true) {
|
||||
CustomView = getCustomViewByKey(views, 'API')
|
||||
DefaultView = DefaultAPIView
|
||||
}
|
||||
break
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case 'preview': {
|
||||
if (livePreviewEnabled) {
|
||||
DefaultView = DefaultLivePreviewView
|
||||
case 'preview': {
|
||||
if (livePreviewEnabled) {
|
||||
DefaultView = DefaultLivePreviewView
|
||||
}
|
||||
break
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case 'versions': {
|
||||
case 'versions': {
|
||||
if (docPermissions?.readVersions?.permission) {
|
||||
CustomView = getCustomViewByKey(views, 'Versions')
|
||||
DefaultView = DefaultVersionsView
|
||||
} else {
|
||||
ErrorView = Unauthorized
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
default: {
|
||||
const baseRoute = [adminRoute, 'collections', collectionSlug, segment3]
|
||||
.filter(Boolean)
|
||||
.join('/')
|
||||
|
||||
const currentRoute = [baseRoute, segment4, segment5, ...remainingSegments]
|
||||
.filter(Boolean)
|
||||
.join('/')
|
||||
|
||||
CustomView = getCustomViewByRoute({
|
||||
baseRoute,
|
||||
currentRoute,
|
||||
views,
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
// `../:id/versions/:version`, etc
|
||||
default: {
|
||||
if (segment4 === 'versions') {
|
||||
if (docPermissions?.readVersions?.permission) {
|
||||
CustomView = getCustomViewByKey(views, 'Versions')
|
||||
DefaultView = DefaultVersionsView
|
||||
CustomView = getCustomViewByKey(views, 'Version')
|
||||
DefaultView = DefaultVersionView
|
||||
} else {
|
||||
ErrorView = Unauthorized
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
default: {
|
||||
const path = `/${nestedViewSlug}`
|
||||
CustomView = getCustomViewByPath(views, path)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// `../:id/versions/:version`, etc
|
||||
if (routeSegments.length === 5) {
|
||||
if (nestedViewSlug === 'versions') {
|
||||
if (docPermissions?.readVersions?.permission) {
|
||||
CustomView = getCustomViewByKey(views, 'Version')
|
||||
DefaultView = DefaultVersionView
|
||||
} else {
|
||||
ErrorView = Unauthorized
|
||||
const baseRoute = [adminRoute, collectionEntity, collectionSlug, segment3]
|
||||
.filter(Boolean)
|
||||
.join('/')
|
||||
|
||||
const currentRoute = [baseRoute, segment4, segment5, ...remainingSegments]
|
||||
.filter(Boolean)
|
||||
.join('/')
|
||||
|
||||
CustomView = getCustomViewByRoute({
|
||||
baseRoute,
|
||||
currentRoute,
|
||||
views,
|
||||
})
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -161,74 +183,83 @@ export const getViewsFromConfig = ({
|
||||
|
||||
if (!EditOverride) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const [globalEntity, globalSlug, nestedViewSlug] = routeSegments
|
||||
const [globalEntity, globalSlug, segment3, ...remainingSegments] = routeSegments
|
||||
|
||||
const {
|
||||
admin: { hidden },
|
||||
} = globalConfig
|
||||
|
||||
if (isEntityHidden({ hidden, user })) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (routeSegments?.length === 2) {
|
||||
if (docPermissions?.read?.permission) {
|
||||
CustomView = getCustomViewByKey(views, 'Default')
|
||||
DefaultView = DefaultEditView
|
||||
} else {
|
||||
ErrorView = Unauthorized
|
||||
}
|
||||
}
|
||||
|
||||
if (routeSegments?.length === 3) {
|
||||
// `../:slug/api`, `../:slug/preview`, `../:slug/versions`, etc
|
||||
switch (nestedViewSlug) {
|
||||
case 'api': {
|
||||
if (globalConfig?.admin?.hideAPIURL !== true) {
|
||||
CustomView = getCustomViewByKey(views, 'API')
|
||||
DefaultView = DefaultAPIView
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case 'preview': {
|
||||
if (livePreviewEnabled) {
|
||||
DefaultView = DefaultLivePreviewView
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case 'versions': {
|
||||
if (docPermissions?.readVersions?.permission) {
|
||||
CustomView = getCustomViewByKey(views, 'Versions')
|
||||
DefaultView = DefaultVersionsView
|
||||
} else {
|
||||
ErrorView = Unauthorized
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
default: {
|
||||
if (docPermissions?.read?.permission) {
|
||||
CustomView = getCustomViewByKey(views, 'Default')
|
||||
DefaultView = DefaultEditView
|
||||
} else {
|
||||
ErrorView = Unauthorized
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (routeSegments?.length === 4) {
|
||||
// `../:slug/versions/:version`, etc
|
||||
if (nestedViewSlug === 'versions') {
|
||||
if (docPermissions?.readVersions?.permission) {
|
||||
CustomView = getCustomViewByKey(views, 'Version')
|
||||
DefaultView = DefaultVersionView
|
||||
switch (routeSegments.length) {
|
||||
case 2: {
|
||||
if (docPermissions?.read?.permission) {
|
||||
CustomView = getCustomViewByKey(views, 'Default')
|
||||
DefaultView = DefaultEditView
|
||||
} else {
|
||||
ErrorView = Unauthorized
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case 3: {
|
||||
// `../:slug/api`, `../:slug/preview`, `../:slug/versions`, etc
|
||||
switch (segment3) {
|
||||
case 'api': {
|
||||
if (globalConfig?.admin?.hideAPIURL !== true) {
|
||||
CustomView = getCustomViewByKey(views, 'API')
|
||||
DefaultView = DefaultAPIView
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case 'preview': {
|
||||
if (livePreviewEnabled) {
|
||||
DefaultView = DefaultLivePreviewView
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case 'versions': {
|
||||
if (docPermissions?.readVersions?.permission) {
|
||||
CustomView = getCustomViewByKey(views, 'Versions')
|
||||
DefaultView = DefaultVersionsView
|
||||
} else {
|
||||
ErrorView = Unauthorized
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
default: {
|
||||
if (docPermissions?.read?.permission) {
|
||||
CustomView = getCustomViewByKey(views, 'Default')
|
||||
DefaultView = DefaultEditView
|
||||
} else {
|
||||
ErrorView = Unauthorized
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
default: {
|
||||
// `../:slug/versions/:version`, etc
|
||||
if (segment3 === 'versions') {
|
||||
if (docPermissions?.readVersions?.permission) {
|
||||
CustomView = getCustomViewByKey(views, 'Version')
|
||||
DefaultView = DefaultVersionView
|
||||
} else {
|
||||
ErrorView = Unauthorized
|
||||
}
|
||||
} else {
|
||||
const baseRoute = [adminRoute, 'globals', globalSlug].filter(Boolean).join('/')
|
||||
|
||||
const currentRoute = [baseRoute, segment3, ...remainingSegments]
|
||||
.filter(Boolean)
|
||||
.join('/')
|
||||
|
||||
CustomView = getCustomViewByRoute({
|
||||
baseRoute,
|
||||
currentRoute,
|
||||
views,
|
||||
})
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,30 +1,20 @@
|
||||
import type { EditViewComponent } from 'payload/config'
|
||||
import type {
|
||||
AdminViewComponent,
|
||||
DocumentPreferences,
|
||||
Document as DocumentType,
|
||||
Field,
|
||||
ServerSideEditViewProps,
|
||||
} from 'payload/types'
|
||||
import type { AdminViewComponent, ServerSideEditViewProps } from 'payload/types'
|
||||
import type { DocumentPermissions } 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 { EditDepthProvider } from '@payloadcms/ui/providers/EditDepth'
|
||||
import { FormQueryParamsProvider } from '@payloadcms/ui/providers/FormQueryParams'
|
||||
import { formatDocTitle } from '@payloadcms/ui/utilities/formatDocTitle'
|
||||
import { formatFields } from '@payloadcms/ui/utilities/formatFields'
|
||||
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'
|
||||
|
||||
@@ -48,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 : []
|
||||
@@ -65,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,
|
||||
@@ -86,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 =
|
||||
@@ -110,7 +99,6 @@ export const Document: React.FC<AdminViewProps> = async ({
|
||||
config,
|
||||
docPermissions,
|
||||
routeSegments: segments,
|
||||
user,
|
||||
})
|
||||
|
||||
CustomView = collectionViews?.CustomView
|
||||
@@ -119,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' : ''
|
||||
@@ -166,7 +137,6 @@ export const Document: React.FC<AdminViewProps> = async ({
|
||||
docPermissions,
|
||||
globalConfig,
|
||||
routeSegments: segments,
|
||||
user,
|
||||
})
|
||||
|
||||
CustomView = globalViews?.CustomView
|
||||
@@ -174,54 +144,47 @@ 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,
|
||||
params,
|
||||
routeSegments: segments,
|
||||
searchParams,
|
||||
}
|
||||
@@ -236,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 && (
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@ const PreviewView: React.FC<Props> = ({
|
||||
getDocPreferences,
|
||||
globalSlug,
|
||||
hasSavePermission,
|
||||
initialData: data,
|
||||
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}
|
||||
|
||||
@@ -15,7 +15,6 @@ import { Form } from '@payloadcms/ui/forms/Form'
|
||||
import { FormSubmit } from '@payloadcms/ui/forms/Submit'
|
||||
import { useConfig } from '@payloadcms/ui/providers/Config'
|
||||
import { useTranslation } from '@payloadcms/ui/providers/Translation'
|
||||
import { useRouter } from 'next/navigation.js'
|
||||
|
||||
import './index.scss'
|
||||
|
||||
@@ -29,7 +28,6 @@ export const LoginForm: React.FC<{
|
||||
routes: { admin, api },
|
||||
} = config
|
||||
|
||||
const router = useRouter()
|
||||
const { t } = useTranslation()
|
||||
|
||||
const prefillForm = autoLogin && autoLogin.prefillOnly
|
||||
@@ -54,10 +52,7 @@ export const LoginForm: React.FC<{
|
||||
disableSuccessStatus
|
||||
initialState={initialState}
|
||||
method="POST"
|
||||
onSuccess={() => {
|
||||
router.push(admin)
|
||||
}}
|
||||
redirect={typeof searchParams?.redirect === 'string' ? searchParams.redirect : ''}
|
||||
redirect={typeof searchParams?.redirect === 'string' ? searchParams.redirect : admin}
|
||||
waitForAutocomplete
|
||||
>
|
||||
<FormLoadingOverlayToggle action="loading" name="login-form" />
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -86,7 +86,7 @@ export const ResetPassword: React.FC<AdminViewProps> = ({ initPageResult, params
|
||||
required
|
||||
/>
|
||||
<ConfirmPassword />
|
||||
<HiddenInput name="token" value={token} />
|
||||
<HiddenInput forceUsePathFromProps name="token" value={token} />
|
||||
<FormSubmit>{i18n.t('authentication:resetPassword')}</FormSubmit>
|
||||
</Form>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { AdminViewComponent, SanitizedConfig } from 'payload/types'
|
||||
|
||||
import { pathToRegexp } from 'path-to-regexp'
|
||||
import { isPathMatchingRoute } from './isPathMatchingRoute.js'
|
||||
|
||||
export const getCustomViewByRoute = ({
|
||||
config,
|
||||
@@ -23,22 +23,13 @@ export const getCustomViewByRoute = ({
|
||||
typeof views === 'object' &&
|
||||
Object.entries(views).find(([, view]) => {
|
||||
if (typeof view === 'object') {
|
||||
const { exact, path: viewPath, sensitive, strict } = view
|
||||
|
||||
const keys = []
|
||||
|
||||
// run the view path through `pathToRegexp` to resolve any dynamic segments
|
||||
// i.e. `/admin/custom-view/:id` -> `/admin/custom-view/123`
|
||||
const regex = pathToRegexp(viewPath, keys, {
|
||||
sensitive,
|
||||
strict,
|
||||
return isPathMatchingRoute({
|
||||
currentRoute,
|
||||
exact: view.exact,
|
||||
path: view.path,
|
||||
sensitive: view.sensitive,
|
||||
strict: view.strict,
|
||||
})
|
||||
|
||||
const match = regex.exec(currentRoute)
|
||||
const viewRoute = match?.[0] || viewPath
|
||||
|
||||
if (exact) return currentRoute === viewRoute
|
||||
if (!exact) return viewRoute.startsWith(currentRoute)
|
||||
}
|
||||
})?.[1]
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
30
packages/next/src/views/Root/isPathMatchingRoute.ts
Normal file
30
packages/next/src/views/Root/isPathMatchingRoute.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { pathToRegexp } from 'path-to-regexp'
|
||||
|
||||
export const isPathMatchingRoute = ({
|
||||
currentRoute,
|
||||
exact,
|
||||
path: viewPath,
|
||||
sensitive,
|
||||
strict,
|
||||
}: {
|
||||
currentRoute: string
|
||||
exact?: boolean
|
||||
path?: string
|
||||
sensitive?: boolean
|
||||
strict?: boolean
|
||||
}) => {
|
||||
const keys = []
|
||||
|
||||
// run the view path through `pathToRegexp` to resolve any dynamic segments
|
||||
// i.e. `/admin/custom-view/:id` -> `/admin/custom-view/123`
|
||||
const regex = pathToRegexp(viewPath, keys, {
|
||||
sensitive,
|
||||
strict,
|
||||
})
|
||||
|
||||
const match = regex.exec(currentRoute)
|
||||
const viewRoute = match?.[0] || viewPath
|
||||
|
||||
if (exact) return currentRoute === viewRoute
|
||||
if (!exact) return viewRoute.startsWith(currentRoute)
|
||||
}
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
</React.Fragment>
|
||||
)}
|
||||
|
||||
@@ -36,5 +36,9 @@ export const CreatedAtCell: React.FC<CreatedAtCellProps> = ({
|
||||
|
||||
if (globalSlug) to = `${admin}/globals/${globalSlug}/versions/${versionID}`
|
||||
|
||||
return <Link href={to}>{cellData && formatDate(cellData, dateFormat, i18n.language)}</Link>
|
||||
return (
|
||||
<Link href={to}>
|
||||
{cellData && formatDate(cellData as Date | number | string, dateFormat, i18n.language)}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -4,5 +4,5 @@ import React, { Fragment } from 'react'
|
||||
|
||||
export const IDCell: React.FC = () => {
|
||||
const { cellData } = useTableCell()
|
||||
return <Fragment>{cellData}</Fragment>
|
||||
return <Fragment>{cellData as number | string}</Fragment>
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user