Compare commits
49 Commits
fix/list-f
...
v3.22.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a80c6b5212 | ||
|
|
6f53747040 | ||
|
|
b820a75ec5 | ||
|
|
49d94d53e0 | ||
|
|
feea444867 | ||
|
|
257cad71ce | ||
|
|
04dad9d7a6 | ||
|
|
098fe10ade | ||
|
|
7277f17f14 | ||
|
|
7a73265bd6 | ||
|
|
ec593b453e | ||
|
|
a63a3d0518 | ||
|
|
57143b37d0 | ||
|
|
3ad56cd86f | ||
|
|
05e6f3326b | ||
|
|
8b6ba625b8 | ||
|
|
2b76a0484c | ||
|
|
66318697dd | ||
|
|
8940726601 | ||
|
|
ae32c555ac | ||
|
|
8ed410456c | ||
|
|
824f9a7f4d | ||
|
|
f25acb801c | ||
|
|
5f58daffd0 | ||
|
|
e413e1df1c | ||
|
|
bdbb99972c | ||
|
|
e29ac523d3 | ||
|
|
d8cfdc7bcb | ||
|
|
694c76d51a | ||
|
|
09721d4c20 | ||
|
|
834fdde088 | ||
|
|
45913e41f1 | ||
|
|
42da87b6e9 | ||
|
|
2a1ddf1e89 | ||
|
|
8af8befbd4 | ||
|
|
2118c6c47f | ||
|
|
a07fd9eba3 | ||
|
|
ea9abfdef3 | ||
|
|
b671fd5a6d | ||
|
|
ae0736b738 | ||
|
|
1a68fa14bb | ||
|
|
b33749905d | ||
|
|
0f85a6e0cc | ||
|
|
177127141e | ||
|
|
0a1cc6adcb | ||
|
|
4a4e90a170 | ||
|
|
136c90c725 | ||
|
|
6353cf8bbe | ||
|
|
109de8cdb3 |
4
.github/CODEOWNERS
vendored
4
.github/CODEOWNERS
vendored
@@ -8,14 +8,14 @@
|
||||
/packages/email-*/src/ @denolfe @jmikrut @DanRibbens
|
||||
/packages/storage-*/src/ @denolfe @jmikrut @DanRibbens
|
||||
/packages/create-payload-app/src/ @denolfe @jmikrut @DanRibbens
|
||||
/packages/eslint-*/ @denolfe @jmikrut @DanRibbens @AlessioGr
|
||||
/packages/eslint-*/ @denolfe @jmikrut @DanRibbens @AlessioGr @GermanJablo
|
||||
|
||||
### Templates ###
|
||||
/templates/_data/ @denolfe @jmikrut @DanRibbens
|
||||
/templates/_template/ @denolfe @jmikrut @DanRibbens
|
||||
|
||||
### Build Files ###
|
||||
**/tsconfig*.json @denolfe @jmikrut @DanRibbens @AlessioGr
|
||||
**/tsconfig*.json @denolfe @jmikrut @DanRibbens @AlessioGr @GermanJablo
|
||||
**/jest.config.js @denolfe @jmikrut @DanRibbens @AlessioGr
|
||||
|
||||
### Root ###
|
||||
|
||||
2
.github/actions/triage/package.json
vendored
2
.github/actions/triage/package.json
vendored
@@ -18,7 +18,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@octokit/webhooks-types": "^7.5.1",
|
||||
"@swc/jest": "^0.2.36",
|
||||
"@swc/jest": "^0.2.37",
|
||||
"@types/jest": "^27.5.2",
|
||||
"@types/node": "^20.16.5",
|
||||
"@typescript-eslint/eslint-plugin": "^4.33.0",
|
||||
|
||||
114
.github/pnpm-lock.yaml
generated
vendored
114
.github/pnpm-lock.yaml
generated
vendored
@@ -19,8 +19,8 @@ importers:
|
||||
specifier: ^7.5.1
|
||||
version: 7.5.1
|
||||
'@swc/jest':
|
||||
specifier: ^0.2.36
|
||||
version: 0.2.36(@swc/core@1.7.26)
|
||||
specifier: ^0.2.37
|
||||
version: 0.2.37(@swc/core@1.7.26)
|
||||
'@types/jest':
|
||||
specifier: ^27.5.2
|
||||
version: 27.5.2
|
||||
@@ -48,9 +48,6 @@ importers:
|
||||
prettier:
|
||||
specifier: ^3.3.3
|
||||
version: 3.3.3
|
||||
ts-jest:
|
||||
specifier: ^26.5.6
|
||||
version: 26.5.6(jest@29.7.0(@types/node@20.16.5))(typescript@4.9.5)
|
||||
typescript:
|
||||
specifier: ^4.9.5
|
||||
version: 4.9.5
|
||||
@@ -68,8 +65,8 @@ importers:
|
||||
specifier: ^7.5.1
|
||||
version: 7.5.1
|
||||
'@swc/jest':
|
||||
specifier: ^0.2.36
|
||||
version: 0.2.36(@swc/core@1.7.26)
|
||||
specifier: ^0.2.37
|
||||
version: 0.2.37(@swc/core@1.7.26)
|
||||
'@types/jest':
|
||||
specifier: ^27.5.2
|
||||
version: 27.5.2
|
||||
@@ -97,9 +94,6 @@ importers:
|
||||
prettier:
|
||||
specifier: ^3.3.3
|
||||
version: 3.3.3
|
||||
ts-jest:
|
||||
specifier: ^26.5.6
|
||||
version: 26.5.6(jest@29.7.0(@types/node@20.16.5))(typescript@4.9.5)
|
||||
typescript:
|
||||
specifier: ^4.9.5
|
||||
version: 4.9.5
|
||||
@@ -386,10 +380,6 @@ packages:
|
||||
resolution: {integrity: sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==}
|
||||
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
|
||||
|
||||
'@jest/types@26.6.2':
|
||||
resolution: {integrity: sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==}
|
||||
engines: {node: '>= 10.14.2'}
|
||||
|
||||
'@jest/types@29.6.3':
|
||||
resolution: {integrity: sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==}
|
||||
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
|
||||
@@ -542,8 +532,8 @@ packages:
|
||||
'@swc/counter@0.1.3':
|
||||
resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==}
|
||||
|
||||
'@swc/jest@0.2.36':
|
||||
resolution: {integrity: sha512-8X80dp81ugxs4a11z1ka43FPhP+/e+mJNXJSxiNYk8gIX/jPBtY4gQTrKu/KIoco8bzKuPI5lUxjfLiGsfvnlw==}
|
||||
'@swc/jest@0.2.37':
|
||||
resolution: {integrity: sha512-CR2BHhmXKGxTiFr21DYPRHQunLkX3mNIFGFkxBGji6r9uyIR5zftTOVYj1e0sFNMV2H7mf/+vpaglqaryBtqfQ==}
|
||||
engines: {npm: '>= 7.0.0'}
|
||||
peerDependencies:
|
||||
'@swc/core': '*'
|
||||
@@ -590,9 +580,6 @@ packages:
|
||||
'@types/yargs-parser@21.0.3':
|
||||
resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==}
|
||||
|
||||
'@types/yargs@15.0.19':
|
||||
resolution: {integrity: sha512-2XUaGVmyQjgyAZldf0D0c14vvo/yv0MhQBSTJcejMMaitsn3nxCB6TmH4G0ZQf+uxROOa9mpanoSm8h6SG/1ZA==}
|
||||
|
||||
'@types/yargs@17.0.33':
|
||||
resolution: {integrity: sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==}
|
||||
|
||||
@@ -746,10 +733,6 @@ packages:
|
||||
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
|
||||
hasBin: true
|
||||
|
||||
bs-logger@0.2.6:
|
||||
resolution: {integrity: sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
bser@2.1.1:
|
||||
resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==}
|
||||
|
||||
@@ -783,9 +766,6 @@ packages:
|
||||
resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
ci-info@2.0.0:
|
||||
resolution: {integrity: sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==}
|
||||
|
||||
ci-info@3.9.0:
|
||||
resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -1133,10 +1113,6 @@ packages:
|
||||
is-arrayish@0.2.1:
|
||||
resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==}
|
||||
|
||||
is-ci@2.0.0:
|
||||
resolution: {integrity: sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==}
|
||||
hasBin: true
|
||||
|
||||
is-core-module@2.15.1:
|
||||
resolution: {integrity: sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -1311,10 +1287,6 @@ packages:
|
||||
resolution: {integrity: sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==}
|
||||
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
|
||||
|
||||
jest-util@26.6.2:
|
||||
resolution: {integrity: sha512-MDW0fKfsn0OI7MS7Euz6h8HNDXVQ0gaM9uW6RjfDmd1DAFcaxX9OqIakHIqhbnmF08Cf2DLDG+ulq8YQQ0Lp0Q==}
|
||||
engines: {node: '>= 10.14.2'}
|
||||
|
||||
jest-util@29.7.0:
|
||||
resolution: {integrity: sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==}
|
||||
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
|
||||
@@ -1414,9 +1386,6 @@ packages:
|
||||
resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
make-error@1.3.6:
|
||||
resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==}
|
||||
|
||||
makeerror@1.0.12:
|
||||
resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==}
|
||||
|
||||
@@ -1438,11 +1407,6 @@ packages:
|
||||
minimatch@3.1.2:
|
||||
resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
|
||||
|
||||
mkdirp@1.0.4:
|
||||
resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==}
|
||||
engines: {node: '>=10'}
|
||||
hasBin: true
|
||||
|
||||
ms@2.1.3:
|
||||
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||
|
||||
@@ -1752,14 +1716,6 @@ packages:
|
||||
resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==}
|
||||
hasBin: true
|
||||
|
||||
ts-jest@26.5.6:
|
||||
resolution: {integrity: sha512-rua+rCP8DxpA8b4DQD/6X2HQS8Zy/xzViVYfEs2OQu68tkCuKLV0Md8pmX55+W24uRIyAsf/BajRfxOs+R2MKA==}
|
||||
engines: {node: '>= 10'}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
jest: '>=26 <27'
|
||||
typescript: '>=3.8 <5.0'
|
||||
|
||||
tslib@1.14.1:
|
||||
resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==}
|
||||
|
||||
@@ -1863,10 +1819,6 @@ packages:
|
||||
yallist@3.1.1:
|
||||
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
|
||||
|
||||
yargs-parser@20.2.9:
|
||||
resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
yargs-parser@21.1.1:
|
||||
resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -2307,14 +2259,6 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@jest/types@26.6.2':
|
||||
dependencies:
|
||||
'@types/istanbul-lib-coverage': 2.0.6
|
||||
'@types/istanbul-reports': 3.0.4
|
||||
'@types/node': 20.16.5
|
||||
'@types/yargs': 15.0.19
|
||||
chalk: 4.1.2
|
||||
|
||||
'@jest/types@29.6.3':
|
||||
dependencies:
|
||||
'@jest/schemas': 29.6.3
|
||||
@@ -2477,7 +2421,7 @@ snapshots:
|
||||
|
||||
'@swc/counter@0.1.3': {}
|
||||
|
||||
'@swc/jest@0.2.36(@swc/core@1.7.26)':
|
||||
'@swc/jest@0.2.37(@swc/core@1.7.26)':
|
||||
dependencies:
|
||||
'@jest/create-cache-key-function': 29.7.0
|
||||
'@swc/core': 1.7.26
|
||||
@@ -2538,10 +2482,6 @@ snapshots:
|
||||
|
||||
'@types/yargs-parser@21.0.3': {}
|
||||
|
||||
'@types/yargs@15.0.19':
|
||||
dependencies:
|
||||
'@types/yargs-parser': 21.0.3
|
||||
|
||||
'@types/yargs@17.0.33':
|
||||
dependencies:
|
||||
'@types/yargs-parser': 21.0.3
|
||||
@@ -2742,10 +2682,6 @@ snapshots:
|
||||
node-releases: 2.0.18
|
||||
update-browserslist-db: 1.1.0(browserslist@4.23.3)
|
||||
|
||||
bs-logger@0.2.6:
|
||||
dependencies:
|
||||
fast-json-stable-stringify: 2.1.0
|
||||
|
||||
bser@2.1.1:
|
||||
dependencies:
|
||||
node-int64: 0.4.0
|
||||
@@ -2773,8 +2709,6 @@ snapshots:
|
||||
|
||||
char-regex@1.0.2: {}
|
||||
|
||||
ci-info@2.0.0: {}
|
||||
|
||||
ci-info@3.9.0: {}
|
||||
|
||||
cjs-module-lexer@1.4.1: {}
|
||||
@@ -3127,10 +3061,6 @@ snapshots:
|
||||
|
||||
is-arrayish@0.2.1: {}
|
||||
|
||||
is-ci@2.0.0:
|
||||
dependencies:
|
||||
ci-info: 2.0.0
|
||||
|
||||
is-core-module@2.15.1:
|
||||
dependencies:
|
||||
hasown: 2.0.2
|
||||
@@ -3470,15 +3400,6 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
jest-util@26.6.2:
|
||||
dependencies:
|
||||
'@jest/types': 26.6.2
|
||||
'@types/node': 20.16.5
|
||||
chalk: 4.1.2
|
||||
graceful-fs: 4.2.11
|
||||
is-ci: 2.0.0
|
||||
micromatch: 4.0.8
|
||||
|
||||
jest-util@29.7.0:
|
||||
dependencies:
|
||||
'@jest/types': 29.6.3
|
||||
@@ -3583,8 +3504,6 @@ snapshots:
|
||||
dependencies:
|
||||
semver: 7.6.3
|
||||
|
||||
make-error@1.3.6: {}
|
||||
|
||||
makeerror@1.0.12:
|
||||
dependencies:
|
||||
tmpl: 1.0.5
|
||||
@@ -3604,8 +3523,6 @@ snapshots:
|
||||
dependencies:
|
||||
brace-expansion: 1.1.11
|
||||
|
||||
mkdirp@1.0.4: {}
|
||||
|
||||
ms@2.1.3: {}
|
||||
|
||||
natural-compare@1.4.0: {}
|
||||
@@ -3859,21 +3776,6 @@ snapshots:
|
||||
|
||||
tree-kill@1.2.2: {}
|
||||
|
||||
ts-jest@26.5.6(jest@29.7.0(@types/node@20.16.5))(typescript@4.9.5):
|
||||
dependencies:
|
||||
bs-logger: 0.2.6
|
||||
buffer-from: 1.1.2
|
||||
fast-json-stable-stringify: 2.1.0
|
||||
jest: 29.7.0(@types/node@20.16.5)
|
||||
jest-util: 26.6.2
|
||||
json5: 2.2.3
|
||||
lodash: 4.17.21
|
||||
make-error: 1.3.6
|
||||
mkdirp: 1.0.4
|
||||
semver: 7.6.3
|
||||
typescript: 4.9.5
|
||||
yargs-parser: 20.2.9
|
||||
|
||||
tslib@1.14.1: {}
|
||||
|
||||
tslib@2.7.0: {}
|
||||
@@ -3959,8 +3861,6 @@ snapshots:
|
||||
|
||||
yallist@3.1.1: {}
|
||||
|
||||
yargs-parser@20.2.9: {}
|
||||
|
||||
yargs-parser@21.1.1: {}
|
||||
|
||||
yargs@17.7.2:
|
||||
|
||||
2
.github/workflows/lock-issues.yml
vendored
2
.github/workflows/lock-issues.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
||||
uses: dessant/lock-threads@v5
|
||||
with:
|
||||
process-only: 'issues'
|
||||
issue-inactive-days: '1'
|
||||
issue-inactive-days: '7'
|
||||
exclude-any-issue-labels: 'status: awaiting-reply'
|
||||
log-output: true
|
||||
issue-comment: >
|
||||
|
||||
@@ -654,6 +654,26 @@ const ExampleCollection = {
|
||||
]}
|
||||
/>
|
||||
|
||||
## useDocumentForm
|
||||
|
||||
The `useDocumentForm` hook works the same way as the [useForm](#useform) hook, but it always gives you access to the top-level `Form` of a document. This is useful if you need to access the document's `Form` context from within a child `Form`.
|
||||
|
||||
An example where this could happen would be custom components within lexical blocks, as lexical blocks initialize their own child `Form`.
|
||||
|
||||
```tsx
|
||||
'use client'
|
||||
|
||||
import { useDocumentForm } from '@payloadcms/ui'
|
||||
|
||||
const MyComponent: React.FC = () => {
|
||||
const { fields: parentDocumentFields } = useDocumentForm()
|
||||
|
||||
return (
|
||||
<p>The document's Form has ${Object.keys(parentDocumentFields).length} fields</p>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## useCollapsible
|
||||
|
||||
The `useCollapsible` hook allows you to control parent collapsibles:
|
||||
|
||||
@@ -112,7 +112,7 @@ The following arguments are provided to the `url` function:
|
||||
If your application requires a fully qualified URL, such as within deploying to Vercel Preview Deployments, you can use the `req` property to build this URL:
|
||||
|
||||
```ts
|
||||
url: (doc, { req }) => `${req.protocol}//${req.host}/${doc.slug}` // highlight-line
|
||||
url: ({ data, req }) => `${req.protocol}//${req.host}/${data.slug}` // highlight-line
|
||||
```
|
||||
|
||||
### Breakpoints
|
||||
|
||||
@@ -6,7 +6,7 @@ desc: Easily build and manage forms from the Admin Panel. Send dynamic, personal
|
||||
keywords: plugins, plugin, form, forms, form builder
|
||||
---
|
||||
|
||||
[](https://www.npmjs.com/package/@payloadcms/plugin-form-builder)
|
||||

|
||||
|
||||
This plugin allows you to build and manage custom forms directly within the [Admin Panel](../admin/overview). Instead of hard-coding a new form into your website or application every time you need one, admins can simply define the schema for each form they need on-the-fly, and your front-end can map over this schema, render its own UI components, and match your brand's design system.
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ desc: Scaffolds multi-tenancy for your Payload application
|
||||
keywords: plugins, multi-tenant, multi-tenancy, plugin, payload, cms, seo, indexing, search, search engine
|
||||
---
|
||||
|
||||
[](https://www.npmjs.com/package/@payloadcms/plugin-multi-tenant)
|
||||

|
||||
|
||||
This plugin sets up multi-tenancy for your application from within your [Admin Panel](../admin/overview). It does so by adding a `tenant` field to all specified collections. Your front-end application can then query data by tenant. You must add the Tenants collection so you control what fields are available for each tenant.
|
||||
|
||||
@@ -32,7 +32,7 @@ This plugin sets up multi-tenancy for your application from within your [Admin P
|
||||
<Banner type="error">
|
||||
**Warning**
|
||||
|
||||
By default this plugin cleans up documents when a tenant is deleted. You should ensure you have
|
||||
By default this plugin cleans up documents when a tenant is deleted. You should ensure you have
|
||||
strong access control on your tenants collection to prevent deletions by unauthorized users.
|
||||
|
||||
You can disabled this behavior by setting `cleanupAfterTenantDelete` to `false` in the plugin options.
|
||||
|
||||
@@ -6,7 +6,7 @@ desc: Nested documents in a parent, child, and sibling relationship.
|
||||
keywords: plugins, nested, documents, parent, child, sibling, relationship
|
||||
---
|
||||
|
||||
[](https://www.npmjs.com/package/@payloadcms/plugin-nested-docs)
|
||||

|
||||
|
||||
This plugin allows you to easily nest the documents of your application inside of one another. It does so by adding a
|
||||
new `parent` field onto each of your documents that, when selected, attaches itself to the parent's tree. When you edit
|
||||
|
||||
@@ -6,7 +6,7 @@ desc: Automatically create redirects for your Payload application
|
||||
keywords: plugins, redirects, redirect, plugin, payload, cms, seo, indexing, search, search engine
|
||||
---
|
||||
|
||||
[](https://www.npmjs.com/package/@payloadcms/plugin-redirects)
|
||||

|
||||
|
||||
This plugin allows you to easily manage redirects for your application from within your [Admin Panel](../admin/overview). It does so by adding a `redirects` collection to your config that allows you specify a redirect from one URL to another. Your front-end application can use this data to automatically redirect users to the correct page using proper HTTP status codes. This is useful for SEO, indexing, and search engine ranking when re-platforming or when changing your URL structure.
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ desc: Generates records of your documents that are extremely fast to search on.
|
||||
keywords: plugins, search, search plugin, search engine, search index, search results, search bar, search box, search field, search form, search input
|
||||
---
|
||||
|
||||
[](https://www.npmjs.com/package/@payloadcms/plugin-search)
|
||||

|
||||
|
||||
This plugin generates records of your documents that are extremely fast to search on. It does so by creating a new `search` collection that is indexed in the database then saving a static copy of each of your documents using only search-critical data. Search records are automatically created, synced, and deleted behind-the-scenes as you manage your application's documents.
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ desc: Integrate Sentry error tracking into your Payload application
|
||||
keywords: plugins, sentry, error, tracking, monitoring, logging, bug, reporting, performance
|
||||
---
|
||||
|
||||
[](https://www.npmjs.com/package/@payloadcms/plugin-sentry)
|
||||

|
||||
|
||||
This plugin allows you to integrate [Sentry](https://sentry.io/) seamlessly with your [Payload](https://github.com/payloadcms/payload) application.
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ desc: Easily accept payments with Stripe
|
||||
keywords: plugins, stripe, payments, ecommerce
|
||||
---
|
||||
|
||||
[](https://www.npmjs.com/package/@payloadcms/plugin-stripe)
|
||||

|
||||
|
||||
With this plugin you can easily integrate [Stripe](https://stripe.com) into Payload. Simply provide your Stripe credentials and this plugin will open up a two-way communication channel between the two platforms. This enables you to easily sync data back and forth, as well as proxy the Stripe REST API through Payload's [Access Control](../access-control/overview). Use this plugin to completely offload billing to Stripe and retain full control over your application's data.
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ export const defaultESLintIgnores = [
|
||||
'**/build/',
|
||||
'**/node_modules/',
|
||||
'**/temp/',
|
||||
'**/*.spec.ts',
|
||||
'**/packages/*.spec.ts',
|
||||
'next-env.d.ts',
|
||||
'**/app',
|
||||
]
|
||||
|
||||
@@ -10,11 +10,17 @@ To spin up this example locally, follow these steps:
|
||||
|
||||
- `npx create-payload-app --example multi-tenant`
|
||||
|
||||
2. `pnpm dev`, `yarn dev` or `npm run dev` to start the server
|
||||
2. `cp .env.example .env` to copy the example environment variables
|
||||
|
||||
3. `pnpm dev`, `yarn dev` or `npm run dev` to start the server
|
||||
- Press `y` when prompted to seed the database
|
||||
3. `open http://localhost:3000` to access the home page
|
||||
4. `open http://localhost:3000/admin` to access the admin panel
|
||||
- Login with email `demo@payloadcms.com` and password `demo`
|
||||
4. `open http://localhost:3000` to access the home page
|
||||
5. `open http://localhost:3000/admin` to access the admin panel
|
||||
|
||||
### Default users
|
||||
|
||||
The seed script seeds 3 tenants.
|
||||
Login with email `demo@payloadcms.com` and password `demo`
|
||||
|
||||
## How it works
|
||||
|
||||
@@ -28,7 +34,7 @@ See the [Collections](https://payloadcms.com/docs/configuration/collections) doc
|
||||
|
||||
- #### Users
|
||||
|
||||
The `users` collection is auth-enabled and encompass both app-wide and tenant-scoped users based on the value of their `roles` and `tenants` fields. Users with the role `super-admin` can manage your entire application, while users with the _tenant role_ of `admin` have limited access to the platform and can manage only the tenant(s) they are assigned to, see [Tenants](#tenants) for more details.
|
||||
The `users` collection is auth-enabled and encompasses both app-wide and tenant-scoped users based on the value of their `roles` and `tenants` fields. Users with the role `super-admin` can manage your entire application, while users with the _tenant role_ of `admin` have limited access to the platform and can manage only the tenant(s) they are assigned to, see [Tenants](#tenants) for more details.
|
||||
|
||||
For additional help with authentication, see the official [Auth Example](https://github.com/payloadcms/payload/tree/main/examples/cms#readme) or the [Authentication](https://payloadcms.com/docs/authentication/overview#authentication-overview) docs.
|
||||
|
||||
@@ -40,13 +46,13 @@ See the [Collections](https://payloadcms.com/docs/configuration/collections) doc
|
||||
|
||||
**Domain-based Tenant Setting**:
|
||||
|
||||
This example also supports domain-based tenant selection, where tenants can be associated with a specific domain. If a tenant is associated with a domain (e.g., `gold.localhost.com:3000`), when a user logs in from that domain, they will be automatically scoped to the matching tenant. This is accomplished through an optional `afterLogin` hook that sets a `payload-tenant` cookie based on the domain.
|
||||
This example also supports domain-based tenant selection, where tenants can be associated with a specific domain. If a tenant is associated with a domain (e.g., `gold.test:3000`), when a user logs in from that domain, they will be automatically scoped to the matching tenant. This is accomplished through an optional `afterLogin` hook that sets a `payload-tenant` cookie based on the domain.
|
||||
|
||||
The seed script seeds 3 tenants, for the domain portion of the example to function properly you will need to add the following entries to your systems `/etc/hosts` file:
|
||||
For the domain portion of the example to function properly, you will need to add the following entries to your system's `/etc/hosts` file:
|
||||
|
||||
- gold.localhost.com:3000
|
||||
- silver.localhost.com:3000
|
||||
- bronze.localhost.com:3000
|
||||
```
|
||||
127.0.0.1 gold.test silver.test bronze.test
|
||||
```
|
||||
|
||||
- #### Pages
|
||||
|
||||
@@ -54,7 +60,7 @@ See the [Collections](https://payloadcms.com/docs/configuration/collections) doc
|
||||
|
||||
## Access control
|
||||
|
||||
Basic role-based access control is setup to determine what users can and cannot do based on their roles, which are:
|
||||
Basic role-based access control is set up to determine what users can and cannot do based on their roles, which are:
|
||||
|
||||
- `super-admin`: They can access the Payload admin panel to manage your multi-tenant application. They can see all tenants and make all operations.
|
||||
- `user`: They can only access the Payload admin panel if they are a tenant-admin, in which case they have a limited access to operations based on their tenant (see below).
|
||||
|
||||
@@ -10,10 +10,10 @@ export default async ({ params: paramsPromise }: { params: Promise<{ slug: strin
|
||||
<p>When you visit a tenant by domain, the domain is used to determine the tenant.</p>
|
||||
<p>
|
||||
For example, visiting{' '}
|
||||
<a href="http://gold.localhost.com:3000/tenant-domains/login">
|
||||
http://gold.localhost.com:3000/tenant-domains/login
|
||||
<a href="http://gold.test:3000/tenant-domains/login">
|
||||
http://gold.test:3000/tenant-domains/login
|
||||
</a>{' '}
|
||||
will show the tenant with the domain "gold.localhost.com".
|
||||
will show the tenant with the domain "gold.test".
|
||||
</p>
|
||||
|
||||
<h2>Slugs</h2>
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Access } from 'payload'
|
||||
/**
|
||||
* Tenant admins and super admins can will be allowed access
|
||||
*/
|
||||
export const superAdminOrTeanantAdminAccess: Access = ({ req }) => {
|
||||
export const superAdminOrTenantAdminAccess: Access = ({ req }) => {
|
||||
if (!req.user) {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
import { ensureUniqueSlug } from './hooks/ensureUniqueSlug'
|
||||
import { superAdminOrTeanantAdminAccess } from '@/collections/Pages/access/superAdminOrTenantAdmin'
|
||||
import { superAdminOrTenantAdminAccess } from '@/collections/Pages/access/superAdminOrTenantAdmin'
|
||||
|
||||
export const Pages: CollectionConfig = {
|
||||
slug: 'pages',
|
||||
access: {
|
||||
create: superAdminOrTeanantAdminAccess,
|
||||
delete: superAdminOrTeanantAdminAccess,
|
||||
create: superAdminOrTenantAdminAccess,
|
||||
delete: superAdminOrTenantAdminAccess,
|
||||
read: () => true,
|
||||
update: superAdminOrTeanantAdminAccess,
|
||||
update: superAdminOrTenantAdminAccess,
|
||||
},
|
||||
admin: {
|
||||
useAsTitle: 'title',
|
||||
|
||||
@@ -1,6 +1,33 @@
|
||||
import type { MigrateUpArgs } from '@payloadcms/db-mongodb'
|
||||
|
||||
export async function up({ payload }: MigrateUpArgs): Promise<void> {
|
||||
const tenant1 = await payload.create({
|
||||
collection: 'tenants',
|
||||
data: {
|
||||
name: 'Tenant 1',
|
||||
slug: 'gold',
|
||||
domain: 'gold.test',
|
||||
},
|
||||
})
|
||||
|
||||
const tenant2 = await payload.create({
|
||||
collection: 'tenants',
|
||||
data: {
|
||||
name: 'Tenant 2',
|
||||
slug: 'silver',
|
||||
domain: 'silver.test',
|
||||
},
|
||||
})
|
||||
|
||||
const tenant3 = await payload.create({
|
||||
collection: 'tenants',
|
||||
data: {
|
||||
name: 'Tenant 3',
|
||||
slug: 'bronze',
|
||||
domain: 'bronze.test',
|
||||
},
|
||||
})
|
||||
|
||||
await payload.create({
|
||||
collection: 'users',
|
||||
data: {
|
||||
@@ -10,47 +37,16 @@ export async function up({ payload }: MigrateUpArgs): Promise<void> {
|
||||
},
|
||||
})
|
||||
|
||||
const tenant1 = await payload.create({
|
||||
collection: 'tenants',
|
||||
data: {
|
||||
name: 'Tenant 1',
|
||||
slug: 'gold',
|
||||
domain: 'gold.localhost.com',
|
||||
},
|
||||
})
|
||||
|
||||
const tenant2 = await payload.create({
|
||||
collection: 'tenants',
|
||||
data: {
|
||||
name: 'Tenant 2',
|
||||
slug: 'silver',
|
||||
domain: 'silver.localhost.com',
|
||||
},
|
||||
})
|
||||
|
||||
const tenant3 = await payload.create({
|
||||
collection: 'tenants',
|
||||
data: {
|
||||
name: 'Tenant 3',
|
||||
slug: 'bronze',
|
||||
domain: 'bronze.localhost.com',
|
||||
},
|
||||
})
|
||||
|
||||
await payload.create({
|
||||
collection: 'users',
|
||||
data: {
|
||||
email: 'tenant1@payloadcms.com',
|
||||
password: 'test',
|
||||
password: 'demo',
|
||||
tenants: [
|
||||
{
|
||||
roles: ['tenant-admin'],
|
||||
tenant: tenant1.id,
|
||||
},
|
||||
// {
|
||||
// roles: ['tenant-admin'],
|
||||
// tenant: tenant2.id,
|
||||
// },
|
||||
],
|
||||
username: 'tenant1',
|
||||
},
|
||||
@@ -60,7 +56,7 @@ export async function up({ payload }: MigrateUpArgs): Promise<void> {
|
||||
collection: 'users',
|
||||
data: {
|
||||
email: 'tenant2@payloadcms.com',
|
||||
password: 'test',
|
||||
password: 'demo',
|
||||
tenants: [
|
||||
{
|
||||
roles: ['tenant-admin'],
|
||||
@@ -75,7 +71,7 @@ export async function up({ payload }: MigrateUpArgs): Promise<void> {
|
||||
collection: 'users',
|
||||
data: {
|
||||
email: 'tenant3@payloadcms.com',
|
||||
password: 'test',
|
||||
password: 'demo',
|
||||
tenants: [
|
||||
{
|
||||
roles: ['tenant-admin'],
|
||||
@@ -90,7 +86,7 @@ export async function up({ payload }: MigrateUpArgs): Promise<void> {
|
||||
collection: 'users',
|
||||
data: {
|
||||
email: 'multi-admin@payloadcms.com',
|
||||
password: 'test',
|
||||
password: 'demo',
|
||||
tenants: [
|
||||
{
|
||||
roles: ['tenant-admin'],
|
||||
@@ -105,7 +101,7 @@ export async function up({ payload }: MigrateUpArgs): Promise<void> {
|
||||
tenant: tenant3.id,
|
||||
},
|
||||
],
|
||||
username: 'tenant3',
|
||||
username: 'multi-admin',
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "payload-monorepo",
|
||||
"version": "3.20.0",
|
||||
"version": "3.22.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
@@ -126,7 +126,7 @@
|
||||
"@sentry/nextjs": "^8.33.1",
|
||||
"@sentry/node": "^8.33.1",
|
||||
"@swc-node/register": "1.10.9",
|
||||
"@swc/cli": "0.5.1",
|
||||
"@swc/cli": "0.6.0",
|
||||
"@swc/jest": "0.2.37",
|
||||
"@types/fs-extra": "^11.0.2",
|
||||
"@types/jest": "29.5.12",
|
||||
@@ -166,7 +166,7 @@
|
||||
"shelljs": "0.8.5",
|
||||
"slash": "3.0.0",
|
||||
"sort-package-json": "^2.10.0",
|
||||
"swc-plugin-transform-remove-imports": "2.0.0",
|
||||
"swc-plugin-transform-remove-imports": "3.1.0",
|
||||
"tempy": "1.0.1",
|
||||
"tstyche": "^3.1.1",
|
||||
"tsx": "4.19.2",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "create-payload-app",
|
||||
"version": "3.20.0",
|
||||
"version": "3.22.0",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -60,7 +60,7 @@
|
||||
"dependencies": {
|
||||
"@clack/prompts": "^0.7.0",
|
||||
"@sindresorhus/slugify": "^1.1.0",
|
||||
"@swc/core": "1.7.10",
|
||||
"@swc/core": "1.10.12",
|
||||
"arg": "^5.0.0",
|
||||
"chalk": "^4.1.0",
|
||||
"comment-json": "^4.2.3",
|
||||
|
||||
@@ -54,6 +54,7 @@ const generateEnvContent = (
|
||||
.filter((line) => line.includes('=') && !line.startsWith('#'))
|
||||
.forEach((line) => {
|
||||
const [key, value] = line.split('=')
|
||||
// @ts-expect-error - vestiges of when tsconfig was not strict. Feel free to improve
|
||||
envVars[key] = value
|
||||
})
|
||||
|
||||
|
||||
@@ -224,12 +224,12 @@ function insertBeforeAndAfter(content: string, loc: Loc): string {
|
||||
}
|
||||
|
||||
// insert ) after end
|
||||
lines[end.line - 1] = insert(lines[end.line - 1], end.column, ')')
|
||||
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(')
|
||||
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(')
|
||||
lines[start.line - 1] = insert(lines[start.line - 1]!, start.column, 'withPayload(')
|
||||
}
|
||||
|
||||
return lines.join('\n')
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
/* TODO: remove the following lines */
|
||||
"noUncheckedIndexedAccess": false,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-mongodb",
|
||||
"version": "3.20.0",
|
||||
"version": "3.22.0",
|
||||
"description": "The officially supported MongoDB database adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-postgres",
|
||||
"version": "3.20.0",
|
||||
"version": "3.22.0",
|
||||
"description": "The officially supported Postgres database adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
@@ -88,7 +88,7 @@
|
||||
"@hyrious/esbuild-plugin-commonjs": "^0.2.4",
|
||||
"@payloadcms/eslint-config": "workspace:*",
|
||||
"@types/to-snake-case": "1.0.0",
|
||||
"esbuild": "0.24.0",
|
||||
"esbuild": "0.24.2",
|
||||
"payload": "workspace:*"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-sqlite",
|
||||
"version": "3.20.0",
|
||||
"version": "3.22.0",
|
||||
"description": "The officially supported SQLite database adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-vercel-postgres",
|
||||
"version": "3.20.0",
|
||||
"version": "3.22.0",
|
||||
"description": "Vercel Postgres adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
@@ -89,7 +89,7 @@
|
||||
"@payloadcms/eslint-config": "workspace:*",
|
||||
"@types/pg": "8.10.2",
|
||||
"@types/to-snake-case": "1.0.0",
|
||||
"esbuild": "0.24.0",
|
||||
"esbuild": "0.24.2",
|
||||
"payload": "workspace:*"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/drizzle",
|
||||
"version": "3.20.0",
|
||||
"version": "3.22.0",
|
||||
"description": "A library of shared functions used by different payload database adapters",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -16,7 +16,9 @@ export async function createGlobal<T extends Record<string, unknown>>(
|
||||
|
||||
const tableName = this.tableNameMap.get(toSnakeCase(globalConfig.slug))
|
||||
|
||||
const result = await upsertRow<T>({
|
||||
data.createdAt = new Date().toISOString()
|
||||
|
||||
const result = await upsertRow<{ globalType: string } & T>({
|
||||
adapter: this,
|
||||
data,
|
||||
db,
|
||||
@@ -26,5 +28,7 @@ export async function createGlobal<T extends Record<string, unknown>>(
|
||||
tableName,
|
||||
})
|
||||
|
||||
result.globalType = slug
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -97,6 +97,7 @@ export const transformArray = ({
|
||||
data: arrayRow,
|
||||
fieldPrefix: '',
|
||||
fields: field.flattenedFields,
|
||||
insideArrayOrBlock: true,
|
||||
locales: newRow.locales,
|
||||
numbers,
|
||||
parentTableName: arrayTableName,
|
||||
|
||||
@@ -101,6 +101,7 @@ export const transformBlocks = ({
|
||||
data: blockRow,
|
||||
fieldPrefix: '',
|
||||
fields: matchedBlock.flattenedFields,
|
||||
insideArrayOrBlock: true,
|
||||
locales: newRow.locales,
|
||||
numbers,
|
||||
parentTableName: blockTableName,
|
||||
|
||||
@@ -42,6 +42,10 @@ type Args = {
|
||||
fieldPrefix: string
|
||||
fields: FlattenedField[]
|
||||
forcedLocale?: string
|
||||
/**
|
||||
* Tracks whether the current traversion context is from array or block.
|
||||
*/
|
||||
insideArrayOrBlock?: boolean
|
||||
locales: {
|
||||
[locale: string]: Record<string, unknown>
|
||||
}
|
||||
@@ -77,6 +81,7 @@ export const traverseFields = ({
|
||||
fieldPrefix,
|
||||
fields,
|
||||
forcedLocale,
|
||||
insideArrayOrBlock = false,
|
||||
locales,
|
||||
numbers,
|
||||
parentTableName,
|
||||
@@ -230,6 +235,7 @@ export const traverseFields = ({
|
||||
fieldPrefix: `${fieldName}_`,
|
||||
fields: field.flattenedFields,
|
||||
forcedLocale: localeKey,
|
||||
insideArrayOrBlock,
|
||||
locales,
|
||||
numbers,
|
||||
parentTableName,
|
||||
@@ -258,6 +264,7 @@ export const traverseFields = ({
|
||||
existingLocales,
|
||||
fieldPrefix: `${fieldName}_`,
|
||||
fields: field.flattenedFields,
|
||||
insideArrayOrBlock,
|
||||
locales,
|
||||
numbers,
|
||||
parentTableName,
|
||||
@@ -420,7 +427,7 @@ export const traverseFields = ({
|
||||
Object.entries(data[field.name]).forEach(([localeKey, localeData]) => {
|
||||
if (Array.isArray(localeData)) {
|
||||
const newRows = transformSelects({
|
||||
id: data._uuid || data.id,
|
||||
id: insideArrayOrBlock ? data._uuid || data.id : undefined,
|
||||
data: localeData,
|
||||
locale: localeKey,
|
||||
})
|
||||
@@ -431,7 +438,7 @@ export const traverseFields = ({
|
||||
}
|
||||
} else if (Array.isArray(data[field.name])) {
|
||||
const newRows = transformSelects({
|
||||
id: data._uuid || data.id,
|
||||
id: insideArrayOrBlock ? data._uuid || data.id : undefined,
|
||||
data: data[field.name],
|
||||
locale: withinArrayOrBlockLocale,
|
||||
})
|
||||
@@ -472,8 +479,9 @@ export const traverseFields = ({
|
||||
}
|
||||
|
||||
valuesToTransform.forEach(({ localeKey, ref, value }) => {
|
||||
let formattedValue = value
|
||||
|
||||
if (typeof value !== 'undefined') {
|
||||
let formattedValue = value
|
||||
if (value && field.type === 'point' && adapter.name !== 'sqlite') {
|
||||
formattedValue = sql`ST_GeomFromGeoJSON(${JSON.stringify(value)})`
|
||||
}
|
||||
@@ -483,12 +491,16 @@ export const traverseFields = ({
|
||||
formattedValue = new Date(value).toISOString()
|
||||
} else if (value instanceof Date) {
|
||||
formattedValue = value.toISOString()
|
||||
} else if (fieldName === 'updatedAt') {
|
||||
// let the db handle this
|
||||
formattedValue = new Date().toISOString()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (field.type === 'date' && fieldName === 'updatedAt') {
|
||||
// let the db handle this
|
||||
formattedValue = new Date().toISOString()
|
||||
}
|
||||
|
||||
if (typeof formattedValue !== 'undefined') {
|
||||
if (localeKey) {
|
||||
ref[localeKey][fieldName] = formattedValue
|
||||
} else {
|
||||
|
||||
@@ -17,7 +17,7 @@ export async function updateGlobal<T extends Record<string, unknown>>(
|
||||
|
||||
const existingGlobal = await db.query[tableName].findFirst({})
|
||||
|
||||
const result = await upsertRow<T>({
|
||||
const result = await upsertRow<{ globalType: string } & T>({
|
||||
...(existingGlobal ? { id: existingGlobal.id, operation: 'update' } : { operation: 'create' }),
|
||||
adapter: this,
|
||||
data,
|
||||
@@ -28,5 +28,7 @@ export async function updateGlobal<T extends Record<string, unknown>>(
|
||||
tableName,
|
||||
})
|
||||
|
||||
result.globalType = slug
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/email-nodemailer",
|
||||
"version": "3.20.0",
|
||||
"version": "3.22.0",
|
||||
"description": "Payload Nodemailer Email Adapter",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/email-resend",
|
||||
"version": "3.20.0",
|
||||
"version": "3.22.0",
|
||||
"description": "Payload Resend Email Adapter",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
"eslint-plugin-jest-dom": "5.4.0",
|
||||
"eslint-plugin-jsx-a11y": "6.10.2",
|
||||
"eslint-plugin-perfectionist": "3.9.1",
|
||||
"eslint-plugin-react-hooks": "5.0.0",
|
||||
"eslint-plugin-react-hooks": "0.0.0-experimental-a4b2d0d5-20250203",
|
||||
"eslint-plugin-regexp": "2.6.0",
|
||||
"globals": "15.12.0",
|
||||
"typescript": "5.7.3",
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
"eslint-plugin-jest-dom": "5.4.0",
|
||||
"eslint-plugin-jsx-a11y": "6.10.2",
|
||||
"eslint-plugin-perfectionist": "3.9.1",
|
||||
"eslint-plugin-react-hooks": "5.0.0",
|
||||
"eslint-plugin-react-hooks": "0.0.0-experimental-a4b2d0d5-20250203",
|
||||
"eslint-plugin-regexp": "2.6.0",
|
||||
"globals": "15.12.0",
|
||||
"typescript": "5.7.3",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/graphql",
|
||||
"version": "3.20.0",
|
||||
"version": "3.22.0",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/live-preview-react",
|
||||
"version": "3.20.0",
|
||||
"version": "3.22.0",
|
||||
"description": "The official React SDK for Payload Live Preview",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/live-preview-vue",
|
||||
"version": "3.20.0",
|
||||
"version": "3.22.0",
|
||||
"description": "The official Vue SDK for Payload Live Preview",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/live-preview",
|
||||
"version": "3.20.0",
|
||||
"version": "3.22.0",
|
||||
"description": "The official live preview JavaScript SDK for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/next",
|
||||
"version": "3.20.0",
|
||||
"version": "3.22.0",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -100,10 +100,10 @@
|
||||
"uuid": "10.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "7.25.9",
|
||||
"@babel/core": "7.26.0",
|
||||
"@babel/preset-env": "7.26.0",
|
||||
"@babel/preset-react": "7.25.9",
|
||||
"@babel/cli": "7.26.4",
|
||||
"@babel/core": "7.26.7",
|
||||
"@babel/preset-env": "7.26.7",
|
||||
"@babel/preset-react": "7.26.3",
|
||||
"@babel/preset-typescript": "7.26.0",
|
||||
"@next/eslint-plugin-next": "15.1.5",
|
||||
"@payloadcms/eslint-config": "workspace:*",
|
||||
@@ -111,12 +111,12 @@
|
||||
"@types/react": "19.0.1",
|
||||
"@types/react-dom": "19.0.1",
|
||||
"@types/uuid": "10.0.0",
|
||||
"babel-plugin-react-compiler": "19.0.0-beta-df7b47d-20241124",
|
||||
"esbuild": "0.24.0",
|
||||
"babel-plugin-react-compiler": "19.0.0-beta-714736e-20250131",
|
||||
"esbuild": "0.24.2",
|
||||
"esbuild-sass-plugin": "3.3.1",
|
||||
"eslint-plugin-react-compiler": "19.0.0-beta-df7b47d-20241124",
|
||||
"eslint-plugin-react-compiler": "19.0.0-beta-714736e-20250131",
|
||||
"payload": "workspace:*",
|
||||
"swc-plugin-transform-remove-imports": "2.0.0"
|
||||
"swc-plugin-transform-remove-imports": "3.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"graphql": "^16.8.1",
|
||||
|
||||
@@ -91,6 +91,20 @@ export const RootLayout = async ({
|
||||
importMap,
|
||||
})
|
||||
|
||||
if (
|
||||
clientConfig.localization &&
|
||||
config.localization &&
|
||||
typeof config.localization.filterAvailableLocales === 'function'
|
||||
) {
|
||||
clientConfig.localization.locales = (
|
||||
await config.localization.filterAvailableLocales({
|
||||
locales: config.localization.locales,
|
||||
req,
|
||||
})
|
||||
).map(({ toString, ...rest }) => rest)
|
||||
clientConfig.localization.localeCodes = config.localization.locales.map(({ code }) => code)
|
||||
}
|
||||
|
||||
const locale = await getRequestLocale({
|
||||
req,
|
||||
})
|
||||
|
||||
@@ -91,6 +91,7 @@ export const ForgotPasswordForm: React.FC = () => {
|
||||
text(value, {
|
||||
name: 'username',
|
||||
type: 'text',
|
||||
blockData: {},
|
||||
data: {},
|
||||
event: 'onChange',
|
||||
preferences: { fields: {} },
|
||||
@@ -120,6 +121,7 @@ export const ForgotPasswordForm: React.FC = () => {
|
||||
email(value, {
|
||||
name: 'email',
|
||||
type: 'email',
|
||||
blockData: {},
|
||||
data: {},
|
||||
event: 'onChange',
|
||||
preferences: { fields: {} },
|
||||
|
||||
@@ -113,7 +113,7 @@ export const buildVersionFields = ({
|
||||
versionField.fieldByLocale = {}
|
||||
|
||||
for (const locale of selectedLocales) {
|
||||
versionField.fieldByLocale[locale] = buildVersionField({
|
||||
const localizedVersionField = buildVersionField({
|
||||
clientField: clientField as ClientField,
|
||||
clientSchemaMap,
|
||||
comparisonValue: comparisonValue?.[locale],
|
||||
@@ -133,12 +133,12 @@ export const buildVersionFields = ({
|
||||
selectedLocales,
|
||||
versionValue: versionValue?.[locale],
|
||||
})
|
||||
if (!versionField.fieldByLocale[locale]) {
|
||||
continue
|
||||
if (localizedVersionField) {
|
||||
versionField.fieldByLocale[locale] = localizedVersionField
|
||||
}
|
||||
}
|
||||
} else {
|
||||
versionField.field = buildVersionField({
|
||||
const baseVersionField = buildVersionField({
|
||||
clientField: clientField as ClientField,
|
||||
clientSchemaMap,
|
||||
comparisonValue,
|
||||
@@ -158,8 +158,8 @@ export const buildVersionFields = ({
|
||||
versionValue,
|
||||
})
|
||||
|
||||
if (!versionField.field) {
|
||||
continue
|
||||
if (baseVersionField) {
|
||||
versionField.field = baseVersionField
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/payload-cloud",
|
||||
"version": "3.20.0",
|
||||
"version": "3.22.0",
|
||||
"description": "The official Payload Cloud plugin",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "payload",
|
||||
"version": "3.20.0",
|
||||
"version": "3.22.0",
|
||||
"description": "Node, React, Headless CMS and Application Framework built on Next.js",
|
||||
"keywords": [
|
||||
"admin panel",
|
||||
@@ -126,7 +126,7 @@
|
||||
"@types/ws": "^8.5.10",
|
||||
"copyfiles": "2.4.1",
|
||||
"cross-env": "7.0.3",
|
||||
"esbuild": "0.24.0",
|
||||
"esbuild": "0.24.2",
|
||||
"graphql-http": "^1.22.0",
|
||||
"react-datepicker": "7.6.0",
|
||||
"rimraf": "6.0.1",
|
||||
|
||||
@@ -68,9 +68,16 @@ export type BuildFormStateArgs = {
|
||||
data?: Data
|
||||
docPermissions: SanitizedDocumentPermissions | undefined
|
||||
docPreferences: DocumentPreferences
|
||||
/**
|
||||
* In case `formState` is not the top-level, document form state, this can be passed to
|
||||
* provide the top-level form state.
|
||||
*/
|
||||
documentFormState?: FormState
|
||||
fallbackLocale?: false | TypedLocale
|
||||
formState?: FormState
|
||||
id?: number | string
|
||||
initialBlockData?: Data
|
||||
initialBlockFormState?: FormState
|
||||
/*
|
||||
If not i18n was passed, the language can be passed to init i18n
|
||||
*/
|
||||
|
||||
@@ -34,6 +34,7 @@ export const generatePasswordSaltHash = async ({
|
||||
const validationResult = password(passwordToSet, {
|
||||
name: 'password',
|
||||
type: 'text',
|
||||
blockData: {},
|
||||
data: {},
|
||||
event: 'submit',
|
||||
preferences: { fields: {} },
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type {
|
||||
DefaultTranslationKeys,
|
||||
DefaultTranslationsObject,
|
||||
I18n,
|
||||
I18nClient,
|
||||
I18nOptions,
|
||||
TFunction,
|
||||
@@ -469,6 +470,14 @@ export type BaseLocalizationConfig = {
|
||||
* @default true
|
||||
*/
|
||||
fallback?: boolean
|
||||
/**
|
||||
* Define a function to filter the locales made available in Payload admin UI
|
||||
* based on user.
|
||||
*/
|
||||
filterAvailableLocales?: (args: {
|
||||
locales: Locale[]
|
||||
req: PayloadRequest
|
||||
}) => Locale[] | Promise<Locale[]>
|
||||
}
|
||||
|
||||
export type LocalizationConfigWithNoLabels = Prettify<
|
||||
@@ -1122,7 +1131,16 @@ export type Config = {
|
||||
* Allows you to modify the base JSON schema that is generated during generate:types. This JSON schema will be used
|
||||
* to generate the TypeScript interfaces.
|
||||
*/
|
||||
schema?: Array<(args: { jsonSchema: JSONSchema4 }) => JSONSchema4>
|
||||
schema?: Array<
|
||||
(args: {
|
||||
collectionIDFieldTypes: {
|
||||
[key: string]: 'number' | 'string'
|
||||
}
|
||||
config: SanitizedConfig
|
||||
i18n: I18n
|
||||
jsonSchema: JSONSchema4
|
||||
}) => JSONSchema4
|
||||
>
|
||||
}
|
||||
/**
|
||||
* Customize the handling of incoming file uploads for collections that have uploads enabled.
|
||||
|
||||
@@ -133,7 +133,13 @@ import type {
|
||||
TextareaFieldValidation,
|
||||
} from '../../index.js'
|
||||
import type { DocumentPreferences } from '../../preferences/types.js'
|
||||
import type { DefaultValue, Operation, PayloadRequest, Where } from '../../types/index.js'
|
||||
import type {
|
||||
DefaultValue,
|
||||
JsonObject,
|
||||
Operation,
|
||||
PayloadRequest,
|
||||
Where,
|
||||
} from '../../types/index.js'
|
||||
import type {
|
||||
NumberFieldManyValidation,
|
||||
NumberFieldSingleValidation,
|
||||
@@ -148,6 +154,10 @@ import type {
|
||||
} from '../validations.js'
|
||||
|
||||
export type FieldHookArgs<TData extends TypeWithID = any, TValue = any, TSiblingData = any> = {
|
||||
/**
|
||||
* The data of the nearest parent block. If the field is not within a block, `blockData` will be equal to `undefined`.
|
||||
*/
|
||||
blockData: JsonObject | undefined
|
||||
/** The collection which the field belongs to. If the field belongs to a global, this will be null. */
|
||||
collection: null | SanitizedCollectionConfig
|
||||
context: RequestContext
|
||||
@@ -212,7 +222,11 @@ export type FieldHook<TData extends TypeWithID = any, TValue = any, TSiblingData
|
||||
|
||||
export type FieldAccess<TData extends TypeWithID = any, TSiblingData = any> = (args: {
|
||||
/**
|
||||
* The incoming data used to `create` or `update` the document with. `data` is undefined during the `read` operation.
|
||||
* The data of the nearest parent block. If the field is not within a block, `blockData` will be equal to `undefined`.
|
||||
*/
|
||||
blockData?: JsonObject | undefined
|
||||
/**
|
||||
* The incoming, top-level document data used to `create` or `update` the document with.
|
||||
*/
|
||||
data?: Partial<TData>
|
||||
/**
|
||||
@@ -231,13 +245,33 @@ export type FieldAccess<TData extends TypeWithID = any, TSiblingData = any> = (a
|
||||
siblingData?: Partial<TSiblingData>
|
||||
}) => boolean | Promise<boolean>
|
||||
|
||||
//TODO: In 4.0, we should replace the three parameters of the condition function with a single, named parameter object
|
||||
export type Condition<TData extends TypeWithID = any, TSiblingData = any> = (
|
||||
/**
|
||||
* The top-level document data
|
||||
*/
|
||||
data: Partial<TData>,
|
||||
/**
|
||||
* Immediately adjacent data to this field. For example, if this is a `group` field, then `siblingData` will be the other fields within the group.
|
||||
*/
|
||||
siblingData: Partial<TSiblingData>,
|
||||
{ user }: { user: PayloadRequest['user'] },
|
||||
{
|
||||
blockData,
|
||||
user,
|
||||
}: {
|
||||
/**
|
||||
* The data of the nearest parent block. If the field is not within a block, `blockData` will be equal to `undefined`.
|
||||
*/
|
||||
blockData: Partial<TData>
|
||||
user: PayloadRequest['user']
|
||||
},
|
||||
) => boolean
|
||||
|
||||
export type FilterOptionsProps<TData = any> = {
|
||||
/**
|
||||
* The data of the nearest parent block. If the field is not within a block, `blockData` will be equal to `undefined`.
|
||||
*/
|
||||
blockData: TData
|
||||
/**
|
||||
* An object containing the full collection or global document currently being edited.
|
||||
*/
|
||||
@@ -348,6 +382,11 @@ export type LabelsClient = {
|
||||
}
|
||||
|
||||
export type BaseValidateOptions<TData, TSiblingData, TValue> = {
|
||||
/**
|
||||
/**
|
||||
* The data of the nearest parent block. If the field is not within a block, `blockData` will be equal to `undefined`.
|
||||
*/
|
||||
blockData: Partial<TData>
|
||||
collectionSlug?: string
|
||||
data: Partial<TData>
|
||||
event?: 'onChange' | 'submit'
|
||||
|
||||
@@ -11,6 +11,10 @@ import { getFieldPathsModified as getFieldPaths } from '../../getFieldPaths.js'
|
||||
import { traverseFields } from './traverseFields.js'
|
||||
|
||||
type Args = {
|
||||
/**
|
||||
* Data of the nearest parent block. If no parent block exists, this will be the `undefined`
|
||||
*/
|
||||
blockData?: JsonObject
|
||||
collection: null | SanitizedCollectionConfig
|
||||
context: RequestContext
|
||||
data: JsonObject
|
||||
@@ -33,6 +37,7 @@ type Args = {
|
||||
// - Execute field hooks
|
||||
|
||||
export const promise = async ({
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
@@ -69,6 +74,7 @@ export const promise = async ({
|
||||
await priorHook
|
||||
|
||||
const hookedValue = await currentHook({
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
@@ -104,6 +110,7 @@ export const promise = async ({
|
||||
rows.forEach((row, rowIndex) => {
|
||||
promises.push(
|
||||
traverseFields({
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
@@ -142,6 +149,7 @@ export const promise = async ({
|
||||
if (block) {
|
||||
promises.push(
|
||||
traverseFields({
|
||||
blockData: siblingData?.[field.name]?.[rowIndex],
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
@@ -171,6 +179,7 @@ export const promise = async ({
|
||||
case 'collapsible':
|
||||
case 'row': {
|
||||
await traverseFields({
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
@@ -193,6 +202,7 @@ export const promise = async ({
|
||||
|
||||
case 'group': {
|
||||
await traverseFields({
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
@@ -269,6 +279,7 @@ export const promise = async ({
|
||||
}
|
||||
|
||||
await traverseFields({
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
@@ -291,6 +302,7 @@ export const promise = async ({
|
||||
|
||||
case 'tabs': {
|
||||
await traverseFields({
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
|
||||
@@ -7,6 +7,10 @@ import type { Field, TabAsField } from '../../config/types.js'
|
||||
import { promise } from './promise.js'
|
||||
|
||||
type Args = {
|
||||
/**
|
||||
* Data of the nearest parent block. If no parent block exists, this will be the `undefined`
|
||||
*/
|
||||
blockData?: JsonObject
|
||||
collection: null | SanitizedCollectionConfig
|
||||
context: RequestContext
|
||||
data: JsonObject
|
||||
@@ -25,6 +29,7 @@ type Args = {
|
||||
}
|
||||
|
||||
export const traverseFields = async ({
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
@@ -46,6 +51,7 @@ export const traverseFields = async ({
|
||||
fields.forEach((field, fieldIndex) => {
|
||||
promises.push(
|
||||
promise({
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
|
||||
@@ -19,6 +19,10 @@ import { relationshipPopulationPromise } from './relationshipPopulationPromise.j
|
||||
import { traverseFields } from './traverseFields.js'
|
||||
|
||||
type Args = {
|
||||
/**
|
||||
* Data of the nearest parent block. If no parent block exists, this will be the `undefined`
|
||||
*/
|
||||
blockData?: JsonObject
|
||||
collection: null | SanitizedCollectionConfig
|
||||
context: RequestContext
|
||||
currentDepth: number
|
||||
@@ -60,6 +64,7 @@ type Args = {
|
||||
// - Populate relationships
|
||||
|
||||
export const promise = async ({
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
currentDepth,
|
||||
@@ -236,6 +241,7 @@ export const promise = async ({
|
||||
const hookPromises = Object.entries(siblingDoc[field.name]).map(([locale, value]) =>
|
||||
(async () => {
|
||||
const hookedValue = await currentHook({
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
currentDepth,
|
||||
@@ -266,6 +272,7 @@ export const promise = async ({
|
||||
await Promise.all(hookPromises)
|
||||
} else {
|
||||
const hookedValue = await currentHook({
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
currentDepth,
|
||||
@@ -301,6 +308,7 @@ export const promise = async ({
|
||||
? true
|
||||
: await field.access.read({
|
||||
id: doc.id as number | string,
|
||||
blockData,
|
||||
data: doc,
|
||||
doc,
|
||||
req,
|
||||
@@ -364,6 +372,7 @@ export const promise = async ({
|
||||
if (Array.isArray(rows)) {
|
||||
rows.forEach((row, rowIndex) => {
|
||||
traverseFields({
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
currentDepth,
|
||||
@@ -397,6 +406,7 @@ export const promise = async ({
|
||||
if (Array.isArray(localeRows)) {
|
||||
localeRows.forEach((row, rowIndex) => {
|
||||
traverseFields({
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
currentDepth,
|
||||
@@ -476,6 +486,7 @@ export const promise = async ({
|
||||
|
||||
if (block) {
|
||||
traverseFields({
|
||||
blockData: row,
|
||||
collection,
|
||||
context,
|
||||
currentDepth,
|
||||
@@ -515,6 +526,7 @@ export const promise = async ({
|
||||
|
||||
if (block) {
|
||||
traverseFields({
|
||||
blockData: row,
|
||||
collection,
|
||||
context,
|
||||
currentDepth,
|
||||
@@ -554,6 +566,7 @@ export const promise = async ({
|
||||
case 'collapsible':
|
||||
case 'row': {
|
||||
traverseFields({
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
currentDepth,
|
||||
@@ -595,6 +608,7 @@ export const promise = async ({
|
||||
const groupSelect = select?.[field.name]
|
||||
|
||||
traverseFields({
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
currentDepth,
|
||||
@@ -747,6 +761,7 @@ export const promise = async ({
|
||||
}
|
||||
|
||||
traverseFields({
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
currentDepth,
|
||||
@@ -780,6 +795,7 @@ export const promise = async ({
|
||||
|
||||
case 'tabs': {
|
||||
traverseFields({
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
currentDepth,
|
||||
|
||||
@@ -13,6 +13,10 @@ import type { Field, TabAsField } from '../../config/types.js'
|
||||
import { promise } from './promise.js'
|
||||
|
||||
type Args = {
|
||||
/**
|
||||
* Data of the nearest parent block. If no parent block exists, this will be the `undefined`
|
||||
*/
|
||||
blockData?: JsonObject
|
||||
collection: null | SanitizedCollectionConfig
|
||||
context: RequestContext
|
||||
currentDepth: number
|
||||
@@ -45,6 +49,7 @@ type Args = {
|
||||
}
|
||||
|
||||
export const traverseFields = ({
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
currentDepth,
|
||||
@@ -75,6 +80,7 @@ export const traverseFields = ({
|
||||
fields.forEach((field, fieldIndex) => {
|
||||
fieldPromises.push(
|
||||
promise({
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
currentDepth,
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { ValidationFieldError } from '../../../errors/index.js'
|
||||
import type { SanitizedGlobalConfig } from '../../../globals/config/types.js'
|
||||
import type { RequestContext } from '../../../index.js'
|
||||
import type { JsonObject, Operation, PayloadRequest } from '../../../types/index.js'
|
||||
import type { Field, TabAsField } from '../../config/types.js'
|
||||
import type { Field, TabAsField, Validate } from '../../config/types.js'
|
||||
|
||||
import { MissingEditorProp } from '../../../errors/index.js'
|
||||
import { deepMergeWithSourceArrays } from '../../../utilities/deepMerge.js'
|
||||
@@ -16,6 +16,10 @@ import { getExistingRowDoc } from './getExistingRowDoc.js'
|
||||
import { traverseFields } from './traverseFields.js'
|
||||
|
||||
type Args = {
|
||||
/**
|
||||
* Data of the nearest parent block. If no parent block exists, this will be the `undefined`
|
||||
*/
|
||||
blockData?: JsonObject
|
||||
collection: null | SanitizedCollectionConfig
|
||||
context: RequestContext
|
||||
data: JsonObject
|
||||
@@ -48,6 +52,7 @@ type Args = {
|
||||
|
||||
export const promise = async ({
|
||||
id,
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
@@ -77,7 +82,7 @@ export const promise = async ({
|
||||
})
|
||||
|
||||
const passesCondition = field.admin?.condition
|
||||
? Boolean(field.admin.condition(data, siblingData, { user: req.user }))
|
||||
? Boolean(field.admin.condition(data, siblingData, { blockData, user: req.user }))
|
||||
: true
|
||||
let skipValidationFromHere = skipValidation || !passesCondition
|
||||
const { localization } = req.payload.config
|
||||
@@ -102,6 +107,7 @@ export const promise = async ({
|
||||
await priorHook
|
||||
|
||||
const hookedValue = await currentHook({
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
@@ -139,22 +145,27 @@ export const promise = async ({
|
||||
}
|
||||
}
|
||||
|
||||
const validationResult = await field.validate(
|
||||
valueToValidate as never,
|
||||
{
|
||||
...field,
|
||||
id,
|
||||
collectionSlug: collection?.slug,
|
||||
data: deepMergeWithSourceArrays(doc, data),
|
||||
event: 'submit',
|
||||
jsonError,
|
||||
operation,
|
||||
preferences: { fields: {} },
|
||||
previousValue: siblingDoc[field.name],
|
||||
req,
|
||||
siblingData: deepMergeWithSourceArrays(siblingDoc, siblingData),
|
||||
} as any,
|
||||
)
|
||||
const validateFn: Validate<object, object, object, object> = field.validate as Validate<
|
||||
object,
|
||||
object,
|
||||
object,
|
||||
object
|
||||
>
|
||||
const validationResult = await validateFn(valueToValidate as never, {
|
||||
...field,
|
||||
id,
|
||||
blockData,
|
||||
collectionSlug: collection?.slug,
|
||||
data: deepMergeWithSourceArrays(doc, data),
|
||||
event: 'submit',
|
||||
// @ts-expect-error
|
||||
jsonError,
|
||||
operation,
|
||||
preferences: { fields: {} },
|
||||
previousValue: siblingDoc[field.name],
|
||||
req,
|
||||
siblingData: deepMergeWithSourceArrays(siblingDoc, siblingData),
|
||||
})
|
||||
|
||||
if (typeof validationResult === 'string') {
|
||||
const label = getTranslatedLabel(field?.label || field?.name, req.i18n)
|
||||
@@ -217,6 +228,7 @@ export const promise = async ({
|
||||
promises.push(
|
||||
traverseFields({
|
||||
id,
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
@@ -268,6 +280,7 @@ export const promise = async ({
|
||||
promises.push(
|
||||
traverseFields({
|
||||
id,
|
||||
blockData: row,
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
@@ -301,6 +314,7 @@ export const promise = async ({
|
||||
case 'row': {
|
||||
await traverseFields({
|
||||
id,
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
@@ -339,6 +353,7 @@ export const promise = async ({
|
||||
|
||||
await traverseFields({
|
||||
id,
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
@@ -455,6 +470,7 @@ export const promise = async ({
|
||||
|
||||
await traverseFields({
|
||||
id,
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
@@ -481,6 +497,7 @@ export const promise = async ({
|
||||
case 'tabs': {
|
||||
await traverseFields({
|
||||
id,
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
|
||||
@@ -8,6 +8,10 @@ import type { Field, TabAsField } from '../../config/types.js'
|
||||
import { promise } from './promise.js'
|
||||
|
||||
type Args = {
|
||||
/**
|
||||
* Data of the nearest parent block. If no parent block exists, this will be the `undefined`
|
||||
*/
|
||||
blockData?: JsonObject
|
||||
collection: null | SanitizedCollectionConfig
|
||||
context: RequestContext
|
||||
data: JsonObject
|
||||
@@ -51,6 +55,7 @@ type Args = {
|
||||
*/
|
||||
export const traverseFields = async ({
|
||||
id,
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
@@ -76,6 +81,7 @@ export const traverseFields = async ({
|
||||
promises.push(
|
||||
promise({
|
||||
id,
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
|
||||
@@ -9,6 +9,10 @@ import { runBeforeDuplicateHooks } from './runHook.js'
|
||||
import { traverseFields } from './traverseFields.js'
|
||||
|
||||
type Args<T> = {
|
||||
/**
|
||||
* Data of the nearest parent block. If no parent block exists, this will be the `undefined`
|
||||
*/
|
||||
blockData?: JsonObject
|
||||
collection: null | SanitizedCollectionConfig
|
||||
context: RequestContext
|
||||
doc: T
|
||||
@@ -25,6 +29,7 @@ type Args<T> = {
|
||||
|
||||
export const promise = async <T>({
|
||||
id,
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
doc,
|
||||
@@ -63,6 +68,7 @@ export const promise = async <T>({
|
||||
const localizedValues = await localizedValuesPromise
|
||||
|
||||
const beforeDuplicateArgs: FieldHookArgs = {
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
data: doc,
|
||||
@@ -96,6 +102,7 @@ export const promise = async <T>({
|
||||
siblingDoc[field.name] = localeData
|
||||
} else {
|
||||
const beforeDuplicateArgs: FieldHookArgs = {
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
data: doc,
|
||||
@@ -143,6 +150,7 @@ export const promise = async <T>({
|
||||
promises.push(
|
||||
traverseFields({
|
||||
id,
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
doc,
|
||||
@@ -177,6 +185,7 @@ export const promise = async <T>({
|
||||
promises.push(
|
||||
traverseFields({
|
||||
id,
|
||||
blockData: row,
|
||||
collection,
|
||||
context,
|
||||
doc,
|
||||
@@ -199,6 +208,7 @@ export const promise = async <T>({
|
||||
promises.push(
|
||||
traverseFields({
|
||||
id,
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
doc,
|
||||
@@ -234,6 +244,7 @@ export const promise = async <T>({
|
||||
promises.push(
|
||||
traverseFields({
|
||||
id,
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
doc,
|
||||
@@ -270,6 +281,7 @@ export const promise = async <T>({
|
||||
promises.push(
|
||||
traverseFields({
|
||||
id,
|
||||
blockData: row,
|
||||
collection,
|
||||
context,
|
||||
doc,
|
||||
@@ -300,6 +312,7 @@ export const promise = async <T>({
|
||||
|
||||
await traverseFields({
|
||||
id,
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
doc,
|
||||
@@ -324,6 +337,7 @@ export const promise = async <T>({
|
||||
|
||||
await traverseFields({
|
||||
id,
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
doc,
|
||||
@@ -347,6 +361,7 @@ export const promise = async <T>({
|
||||
case 'row': {
|
||||
await traverseFields({
|
||||
id,
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
doc,
|
||||
@@ -367,6 +382,7 @@ export const promise = async <T>({
|
||||
case 'tab': {
|
||||
await traverseFields({
|
||||
id,
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
doc,
|
||||
@@ -386,6 +402,7 @@ export const promise = async <T>({
|
||||
case 'tabs': {
|
||||
await traverseFields({
|
||||
id,
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
doc,
|
||||
|
||||
@@ -6,6 +6,10 @@ import type { Field, TabAsField } from '../../config/types.js'
|
||||
import { promise } from './promise.js'
|
||||
|
||||
type Args<T> = {
|
||||
/**
|
||||
* Data of the nearest parent block. If no parent block exists, this will be the `undefined`
|
||||
*/
|
||||
blockData?: JsonObject
|
||||
collection: null | SanitizedCollectionConfig
|
||||
context: RequestContext
|
||||
doc: T
|
||||
@@ -21,6 +25,7 @@ type Args<T> = {
|
||||
|
||||
export const traverseFields = async <T>({
|
||||
id,
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
doc,
|
||||
@@ -38,6 +43,7 @@ export const traverseFields = async <T>({
|
||||
promises.push(
|
||||
promise({
|
||||
id,
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
doc,
|
||||
|
||||
@@ -14,6 +14,10 @@ import { getExistingRowDoc } from '../beforeChange/getExistingRowDoc.js'
|
||||
import { traverseFields } from './traverseFields.js'
|
||||
|
||||
type Args<T> = {
|
||||
/**
|
||||
* Data of the nearest parent block. If no parent block exists, this will be the `undefined`
|
||||
*/
|
||||
blockData?: JsonObject
|
||||
collection: null | SanitizedCollectionConfig
|
||||
context: RequestContext
|
||||
data: T
|
||||
@@ -47,6 +51,7 @@ type Args<T> = {
|
||||
|
||||
export const promise = async <T>({
|
||||
id,
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
@@ -270,6 +275,7 @@ export const promise = async <T>({
|
||||
await priorHook
|
||||
|
||||
const hookedValue = await currentHook({
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
@@ -298,7 +304,7 @@ export const promise = async <T>({
|
||||
if (field.access && field.access[operation]) {
|
||||
const result = overrideAccess
|
||||
? true
|
||||
: await field.access[operation]({ id, data, doc, req, siblingData })
|
||||
: await field.access[operation]({ id, blockData, data, doc, req, siblingData })
|
||||
|
||||
if (!result) {
|
||||
delete siblingData[field.name]
|
||||
@@ -335,6 +341,7 @@ export const promise = async <T>({
|
||||
promises.push(
|
||||
traverseFields({
|
||||
id,
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
@@ -375,6 +382,7 @@ export const promise = async <T>({
|
||||
promises.push(
|
||||
traverseFields({
|
||||
id,
|
||||
blockData: row,
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
@@ -404,6 +412,7 @@ export const promise = async <T>({
|
||||
case 'row': {
|
||||
await traverseFields({
|
||||
id,
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
@@ -437,6 +446,7 @@ export const promise = async <T>({
|
||||
|
||||
await traverseFields({
|
||||
id,
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
@@ -522,6 +532,7 @@ export const promise = async <T>({
|
||||
|
||||
await traverseFields({
|
||||
id,
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
@@ -544,6 +555,7 @@ export const promise = async <T>({
|
||||
case 'tabs': {
|
||||
await traverseFields({
|
||||
id,
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
|
||||
@@ -7,6 +7,10 @@ import type { Field, TabAsField } from '../../config/types.js'
|
||||
import { promise } from './promise.js'
|
||||
|
||||
type Args<T> = {
|
||||
/**
|
||||
* Data of the nearest parent block. If no parent block exists, this will be the `undefined`
|
||||
*/
|
||||
blockData?: JsonObject
|
||||
collection: null | SanitizedCollectionConfig
|
||||
context: RequestContext
|
||||
data: T
|
||||
@@ -32,6 +36,7 @@ type Args<T> = {
|
||||
|
||||
export const traverseFields = async <T>({
|
||||
id,
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
@@ -53,6 +58,7 @@ export const traverseFields = async <T>({
|
||||
promises.push(
|
||||
promise({
|
||||
id,
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
|
||||
@@ -510,7 +510,7 @@ const validateFilterOptions: Validate<
|
||||
RelationshipField | UploadField
|
||||
> = async (
|
||||
value,
|
||||
{ id, data, filterOptions, relationTo, req, req: { payload, t, user }, siblingData },
|
||||
{ id, blockData, data, filterOptions, relationTo, req, req: { payload, t, user }, siblingData },
|
||||
) => {
|
||||
if (typeof filterOptions !== 'undefined' && value) {
|
||||
const options: {
|
||||
@@ -527,6 +527,7 @@ const validateFilterOptions: Validate<
|
||||
typeof filterOptions === 'function'
|
||||
? await filterOptions({
|
||||
id,
|
||||
blockData,
|
||||
data,
|
||||
relationTo: collection,
|
||||
req,
|
||||
|
||||
@@ -244,6 +244,11 @@ export const updateOperation = async <
|
||||
// /////////////////////////////////////
|
||||
|
||||
if (!shouldSaveDraft) {
|
||||
// Ensure global has createdAt
|
||||
if (!result.createdAt) {
|
||||
result.createdAt = new Date().toISOString()
|
||||
}
|
||||
|
||||
if (globalExists) {
|
||||
result = await payload.db.updateGlobal({
|
||||
slug,
|
||||
|
||||
@@ -217,7 +217,9 @@ function entityOrFieldToJsDocs({
|
||||
description = entity?.admin?.description?.[i18n.language]
|
||||
}
|
||||
} else if (typeof entity?.admin?.description === 'function' && i18n) {
|
||||
description = entity?.admin?.description(i18n)
|
||||
// do not evaluate description functions for generating JSDocs. The output of
|
||||
// those can differ depending on where and when they are called, creating
|
||||
// inconsistencies in the generated JSDocs.
|
||||
}
|
||||
}
|
||||
return description
|
||||
@@ -1102,7 +1104,7 @@ export function configToJSONSchema(
|
||||
|
||||
if (config?.typescript?.schema?.length) {
|
||||
for (const schema of config.typescript.schema) {
|
||||
jsonSchema = schema({ jsonSchema })
|
||||
jsonSchema = schema({ collectionIDFieldTypes, config, i18n, jsonSchema })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ export const routeError = async ({
|
||||
|
||||
// Internal server errors can contain anything, including potentially sensitive data.
|
||||
// Therefore, error details will be hidden from the response unless `config.debug` is `true`
|
||||
if (!config.debug && status === httpStatus.INTERNAL_SERVER_ERROR) {
|
||||
if (!config.debug && !err.isPublic && status === httpStatus.INTERNAL_SERVER_ERROR) {
|
||||
response = formatErrors(new APIError('Something went wrong.'))
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-cloud-storage",
|
||||
"version": "3.20.0",
|
||||
"version": "3.22.0",
|
||||
"description": "The official cloud storage plugin for Payload CMS",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-form-builder",
|
||||
"version": "3.20.0",
|
||||
"version": "3.22.0",
|
||||
"description": "Form builder plugin for Payload CMS",
|
||||
"keywords": [
|
||||
"payload",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-multi-tenant",
|
||||
"version": "3.20.0",
|
||||
"version": "3.22.0",
|
||||
"description": "Multi Tenant plugin for Payload",
|
||||
"keywords": [
|
||||
"payload",
|
||||
|
||||
@@ -24,15 +24,21 @@ export const TenantField = (args: Props) => {
|
||||
const hasSetValueRef = React.useRef(false)
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!hasSetValueRef.current && value) {
|
||||
if (!hasSetValueRef.current) {
|
||||
// set value on load
|
||||
setTenant({ id: value, refresh: unique })
|
||||
if (value && value !== selectedTenantID) {
|
||||
setTenant({ id: value, refresh: unique })
|
||||
} else {
|
||||
// in the document view, the tenant field should always have a value
|
||||
const defaultValue =
|
||||
!selectedTenantID || selectedTenantID === SELECT_ALL
|
||||
? options[0]?.value
|
||||
: selectedTenantID
|
||||
setTenant({ id: defaultValue, refresh: unique })
|
||||
}
|
||||
hasSetValueRef.current = true
|
||||
} else if (selectedTenantID && selectedTenantID === SELECT_ALL && options?.[0]?.value) {
|
||||
// in the document view, the tenant field should always have a value
|
||||
setTenant({ id: options[0].value, refresh: unique })
|
||||
} else if ((!value || value !== selectedTenantID) && selectedTenantID) {
|
||||
// Update the field value when the tenant is changed
|
||||
} else if ((!value || value !== selectedTenantID) && selectedTenantID !== SELECT_ALL) {
|
||||
// Update the field on the document value when the tenant is changed
|
||||
setValue(selectedTenantID)
|
||||
}
|
||||
}, [value, selectedTenantID, setTenant, setValue, options, unique])
|
||||
|
||||
@@ -42,7 +42,7 @@ export const TenantSelector = ({ viewType }: { viewType?: ViewTypes }) => {
|
||||
selectedTenantID
|
||||
? selectedTenantID === SELECT_ALL
|
||||
? undefined
|
||||
: String(selectedTenantID)
|
||||
: (selectedTenantID as string)
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
|
||||
6
packages/plugin-multi-tenant/src/defaults.ts
Normal file
6
packages/plugin-multi-tenant/src/defaults.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export const defaults = {
|
||||
tenantCollectionSlug: 'tenants',
|
||||
tenantFieldName: 'tenant',
|
||||
tenantsArrayFieldName: 'tenants',
|
||||
tenantsArrayTenantFieldName: 'tenant',
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { type RelationshipField } from 'payload'
|
||||
import { APIError } from 'payload'
|
||||
|
||||
import { defaults } from '../../defaults.js'
|
||||
import { getCollectionIDType } from '../../utilities/getCollectionIDType.js'
|
||||
import { getTenantFromCookie } from '../../utilities/getTenantFromCookie.js'
|
||||
|
||||
@@ -12,10 +13,10 @@ type Args = {
|
||||
unique: boolean
|
||||
}
|
||||
export const tenantField = ({
|
||||
name,
|
||||
name = defaults.tenantFieldName,
|
||||
access = undefined,
|
||||
debug,
|
||||
tenantsCollectionSlug,
|
||||
tenantsCollectionSlug = defaults.tenantCollectionSlug,
|
||||
unique,
|
||||
}: Args): RelationshipField => ({
|
||||
name,
|
||||
|
||||
@@ -1,27 +1,37 @@
|
||||
import type { ArrayField, RelationshipField } from 'payload'
|
||||
|
||||
export const tenantsArrayField = (args: {
|
||||
import { defaults } from '../../defaults.js'
|
||||
|
||||
type Args = {
|
||||
arrayFieldAccess?: ArrayField['access']
|
||||
rowFields?: ArrayField['fields']
|
||||
tenantFieldAccess?: RelationshipField['access']
|
||||
tenantsArrayFieldName: ArrayField['name']
|
||||
tenantsArrayTenantFieldName: RelationshipField['name']
|
||||
tenantsCollectionSlug: string
|
||||
}): ArrayField => ({
|
||||
name: args.tenantsArrayFieldName,
|
||||
}
|
||||
export const tenantsArrayField = ({
|
||||
arrayFieldAccess,
|
||||
rowFields,
|
||||
tenantFieldAccess,
|
||||
tenantsArrayFieldName = defaults.tenantsArrayFieldName,
|
||||
tenantsArrayTenantFieldName = defaults.tenantsArrayFieldName,
|
||||
tenantsCollectionSlug = defaults.tenantCollectionSlug,
|
||||
}: Args): ArrayField => ({
|
||||
name: tenantsArrayFieldName,
|
||||
type: 'array',
|
||||
access: args?.arrayFieldAccess,
|
||||
access: arrayFieldAccess,
|
||||
fields: [
|
||||
{
|
||||
name: args.tenantsArrayTenantFieldName,
|
||||
name: tenantsArrayTenantFieldName,
|
||||
type: 'relationship',
|
||||
access: args.tenantFieldAccess,
|
||||
access: tenantFieldAccess,
|
||||
index: true,
|
||||
relationTo: args.tenantsCollectionSlug,
|
||||
relationTo: tenantsCollectionSlug,
|
||||
required: true,
|
||||
saveToJWT: true,
|
||||
},
|
||||
...(args?.rowFields || []),
|
||||
...(rowFields || []),
|
||||
],
|
||||
saveToJWT: true,
|
||||
})
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { CollectionConfig, Config } from 'payload'
|
||||
|
||||
import type { MultiTenantPluginConfig } from './types.js'
|
||||
|
||||
import { defaults } from './defaults.js'
|
||||
import { tenantField } from './fields/tenantField/index.js'
|
||||
import { tenantsArrayField } from './fields/tenantsArrayField/index.js'
|
||||
import { addTenantCleanup } from './hooks/afterTenantDelete.js'
|
||||
@@ -9,13 +10,6 @@ import { addCollectionAccess } from './utilities/addCollectionAccess.js'
|
||||
import { addFilterOptionsToFields } from './utilities/addFilterOptionsToFields.js'
|
||||
import { withTenantListFilter } from './utilities/withTenantListFilter.js'
|
||||
|
||||
const defaults = {
|
||||
tenantCollectionSlug: 'tenants',
|
||||
tenantFieldName: 'tenant',
|
||||
tenantsArrayFieldName: 'tenants',
|
||||
tenantsArrayTenantFieldName: 'tenant',
|
||||
}
|
||||
|
||||
export const multiTenantPlugin =
|
||||
<ConfigType>(pluginConfig: MultiTenantPluginConfig<ConfigType>) =>
|
||||
(incomingConfig: Config): Config => {
|
||||
|
||||
@@ -33,7 +33,7 @@ export const TenantSelectionProvider = async ({
|
||||
})
|
||||
tenantOptions = docs.map((doc) => ({
|
||||
label: String(doc[useAsTitle]),
|
||||
value: String(doc.id),
|
||||
value: doc.id,
|
||||
}))
|
||||
} catch (_) {
|
||||
// user likely does not have access
|
||||
@@ -42,15 +42,17 @@ export const TenantSelectionProvider = async ({
|
||||
const cookies = await getCookies()
|
||||
let tenantCookie = cookies.get('payload-tenant')?.value
|
||||
let initialValue = undefined
|
||||
const isValidTenantCookie =
|
||||
(tenantOptions.length > 1 && tenantCookie === SELECT_ALL) ||
|
||||
tenantOptions.some((option) => option.value === tenantCookie)
|
||||
|
||||
if (isValidTenantCookie) {
|
||||
initialValue = tenantCookie
|
||||
if (tenantOptions.length > 1 && tenantCookie === SELECT_ALL) {
|
||||
initialValue = SELECT_ALL
|
||||
} else {
|
||||
tenantCookie = undefined
|
||||
initialValue = tenantOptions.length > 1 ? SELECT_ALL : tenantOptions[0]?.value
|
||||
const matchingOption = tenantOptions.find((option) => String(option.value) === tenantCookie)
|
||||
if (matchingOption) {
|
||||
initialValue = matchingOption.value
|
||||
} else {
|
||||
tenantCookie = undefined
|
||||
initialValue = tenantOptions.length > 1 ? SELECT_ALL : tenantOptions[0]?.value
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { parseCookies } from 'payload'
|
||||
import { isNumber } from 'payload/shared'
|
||||
|
||||
/**
|
||||
* A function that takes request headers and an idType and returns the current tenant ID from the cookie
|
||||
@@ -13,5 +14,9 @@ export function getTenantFromCookie(
|
||||
): null | number | string {
|
||||
const cookies = parseCookies(headers)
|
||||
const selectedTenant = cookies.get('payload-tenant') || null
|
||||
return selectedTenant ? (idType === 'number' ? parseFloat(selectedTenant) : selectedTenant) : null
|
||||
return selectedTenant
|
||||
? idType === 'number' && isNumber(selectedTenant)
|
||||
? parseFloat(selectedTenant)
|
||||
: selectedTenant
|
||||
: null
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-nested-docs",
|
||||
"version": "3.20.0",
|
||||
"version": "3.22.0",
|
||||
"description": "The official Nested Docs plugin for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-redirects",
|
||||
"version": "3.20.0",
|
||||
"version": "3.22.0",
|
||||
"description": "Redirects plugin for Payload",
|
||||
"keywords": [
|
||||
"payload",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-search",
|
||||
"version": "3.20.0",
|
||||
"version": "3.22.0",
|
||||
"description": "Search plugin for Payload",
|
||||
"keywords": [
|
||||
"payload",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { CollectionConfig, Field } from 'payload'
|
||||
|
||||
import type { SearchPluginConfigWithLocales } from '../types.js'
|
||||
import type { ReindexButtonServerProps } from './ui/ReindexButton/types.js'
|
||||
|
||||
import { generateReindexHandler } from '../utilities/generateReindexHandler.js'
|
||||
|
||||
@@ -8,7 +9,6 @@ import { generateReindexHandler } from '../utilities/generateReindexHandler.js'
|
||||
export const generateSearchCollection = (
|
||||
pluginConfig: SearchPluginConfigWithLocales,
|
||||
): CollectionConfig => {
|
||||
const apiBasePath = pluginConfig?.apiBasePath || '/api'
|
||||
const searchSlug = pluginConfig?.searchOverrides?.slug || 'search'
|
||||
const searchCollections = pluginConfig?.collections || []
|
||||
const collectionLabels = pluginConfig?.labels
|
||||
@@ -71,11 +71,10 @@ export const generateSearchCollection = (
|
||||
{
|
||||
path: '@payloadcms/plugin-search/client#ReindexButton',
|
||||
serverProps: {
|
||||
apiBasePath,
|
||||
collectionLabels,
|
||||
searchCollections,
|
||||
searchSlug,
|
||||
},
|
||||
} satisfies ReindexButtonServerProps,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
'use client'
|
||||
|
||||
import { useConfig, useField } from '@payloadcms/ui'
|
||||
import { CopyToClipboard, useConfig, useField } from '@payloadcms/ui'
|
||||
import { formatAdminURL } from '@payloadcms/ui/shared'
|
||||
import LinkImport from 'next/link.js'
|
||||
import React from 'react'
|
||||
// TODO: fix this import to work in dev mode within the monorepo in a way that is backwards compatible with 1.x
|
||||
// import CopyToClipboard from 'payload/dist/admin/components/elements/CopyToClipboard'
|
||||
|
||||
const Link = (LinkImport.default || LinkImport) as unknown as typeof LinkImport.default
|
||||
|
||||
export const LinkToDocClient: React.FC = () => {
|
||||
const { config } = useConfig()
|
||||
@@ -27,6 +28,8 @@ export const LinkToDocClient: React.FC = () => {
|
||||
path: `/collections/${value.relationTo || ''}/${value.value || ''}`,
|
||||
})}`
|
||||
|
||||
const hrefToDisplay = `${process.env.NEXT_BASE_PATH || ''}${href}`
|
||||
|
||||
return (
|
||||
<div style={{ marginBottom: 'var(--spacing-field, 1rem)' }}>
|
||||
<div>
|
||||
@@ -38,7 +41,7 @@ export const LinkToDocClient: React.FC = () => {
|
||||
>
|
||||
Doc URL
|
||||
</span>
|
||||
{/* <CopyToClipboard value={href} /> */}
|
||||
<CopyToClipboard value={hrefToDisplay} />
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
@@ -47,9 +50,9 @@ export const LinkToDocClient: React.FC = () => {
|
||||
textOverflow: 'ellipsis',
|
||||
}}
|
||||
>
|
||||
<a href={href} target="_blank">
|
||||
{href}
|
||||
</a>
|
||||
<Link href={href} passHref {...{ rel: 'noopener noreferrer', target: '_blank' }}>
|
||||
{hrefToDisplay}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
Popup,
|
||||
PopupList,
|
||||
toast,
|
||||
useConfig,
|
||||
useLocale,
|
||||
useModal,
|
||||
useTranslation,
|
||||
@@ -20,15 +21,18 @@ import { ReindexConfirmModal } from './ReindexConfirmModal/index.js'
|
||||
const confirmReindexModalSlug = 'confirm-reindex-modal'
|
||||
|
||||
export const ReindexButtonClient: React.FC<ReindexButtonProps> = ({
|
||||
apiBasePath,
|
||||
collectionLabels,
|
||||
searchCollections,
|
||||
searchSlug,
|
||||
}) => {
|
||||
const { closeModal, openModal } = useModal()
|
||||
|
||||
const { config } = useConfig()
|
||||
|
||||
const {
|
||||
i18n: { t },
|
||||
} = useTranslation()
|
||||
|
||||
const locale = useLocale()
|
||||
const router = useRouter()
|
||||
|
||||
@@ -46,15 +50,16 @@ export const ReindexButtonClient: React.FC<ReindexButtonProps> = ({
|
||||
closeConfirmModal()
|
||||
setLoading(true)
|
||||
|
||||
const basePath = apiBasePath.endsWith('/') ? apiBasePath.slice(0, -1) : apiBasePath
|
||||
|
||||
try {
|
||||
const endpointRes = await fetch(`${basePath}/${searchSlug}/reindex?locale=${locale.code}`, {
|
||||
body: JSON.stringify({
|
||||
collections: reindexCollections,
|
||||
}),
|
||||
method: 'POST',
|
||||
})
|
||||
const endpointRes = await fetch(
|
||||
`${config.routes.api}/${searchSlug}/reindex?locale=${locale.code}`,
|
||||
{
|
||||
body: JSON.stringify({
|
||||
collections: reindexCollections,
|
||||
}),
|
||||
method: 'POST',
|
||||
},
|
||||
)
|
||||
|
||||
const { message } = (await endpointRes.json()) as { message: string }
|
||||
|
||||
@@ -64,13 +69,13 @@ export const ReindexButtonClient: React.FC<ReindexButtonProps> = ({
|
||||
toast.success(message)
|
||||
router.refresh()
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
} catch (_err: unknown) {
|
||||
// swallow error, toast shown above
|
||||
} finally {
|
||||
setReindexCollections([])
|
||||
setLoading(false)
|
||||
}
|
||||
}, [closeConfirmModal, isLoading, reindexCollections, router, searchSlug, locale, apiBasePath])
|
||||
}, [closeConfirmModal, isLoading, reindexCollections, router, searchSlug, locale, config])
|
||||
|
||||
const handleShowConfirmModal = useCallback(
|
||||
(collections: string | string[] = searchCollections) => {
|
||||
|
||||
@@ -1,33 +1,31 @@
|
||||
import type { ResolvedCollectionLabels } from '../../../types.js'
|
||||
import type { SearchReindexButtonServerComponent } from './types.js'
|
||||
|
||||
import { ReindexButtonClient } from './index.client.js'
|
||||
|
||||
export const ReindexButton: SearchReindexButtonServerComponent = (props) => {
|
||||
const { apiBasePath, collectionLabels, i18n, searchCollections, searchSlug } = props
|
||||
const { collectionLabels, i18n, searchCollections, searchSlug } = props
|
||||
|
||||
const getStaticLocalizedPluralLabels = () => {
|
||||
return Object.fromEntries(
|
||||
searchCollections.map((collection) => {
|
||||
const labels = collectionLabels[collection]
|
||||
const pluralLabel = labels?.plural
|
||||
const resolvedCollectionLabels: ResolvedCollectionLabels = Object.fromEntries(
|
||||
searchCollections.map((collection) => {
|
||||
const labels = collectionLabels[collection]
|
||||
const pluralLabel = labels?.plural
|
||||
|
||||
if (typeof pluralLabel === 'function') {
|
||||
return [collection, pluralLabel({ t: i18n.t })]
|
||||
}
|
||||
if (typeof pluralLabel === 'function') {
|
||||
return [collection, pluralLabel({ t: i18n.t })]
|
||||
}
|
||||
|
||||
if (pluralLabel) {
|
||||
return [collection, pluralLabel]
|
||||
}
|
||||
if (pluralLabel) {
|
||||
return [collection, pluralLabel]
|
||||
}
|
||||
|
||||
return [collection, collection]
|
||||
}),
|
||||
)
|
||||
}
|
||||
return [collection, collection]
|
||||
}),
|
||||
)
|
||||
|
||||
return (
|
||||
<ReindexButtonClient
|
||||
apiBasePath={apiBasePath}
|
||||
collectionLabels={getStaticLocalizedPluralLabels()}
|
||||
collectionLabels={resolvedCollectionLabels}
|
||||
searchCollections={searchCollections}
|
||||
searchSlug={searchSlug}
|
||||
/>
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import type { CustomComponent, PayloadServerReactComponent, StaticLabel } from 'payload'
|
||||
import type { CustomComponent, PayloadServerReactComponent } from 'payload'
|
||||
|
||||
import type { CollectionLabels } from '../../../types.js'
|
||||
import type { CollectionLabels, ResolvedCollectionLabels } from '../../../types.js'
|
||||
|
||||
export type ReindexButtonProps = {
|
||||
apiBasePath: string
|
||||
collectionLabels: Record<string, StaticLabel>
|
||||
collectionLabels: ResolvedCollectionLabels
|
||||
searchCollections: string[]
|
||||
searchSlug: string
|
||||
}
|
||||
|
||||
type ReindexButtonServerProps = {
|
||||
export type ReindexButtonServerProps = {
|
||||
collectionLabels: CollectionLabels
|
||||
} & ReindexButtonProps
|
||||
} & Omit<ReindexButtonProps, 'collectionLabels'>
|
||||
|
||||
export type SearchReindexButtonClientComponent = ReindexButtonProps
|
||||
|
||||
export type SearchReindexButtonServerComponent = PayloadServerReactComponent<
|
||||
CustomComponent<ReindexButtonServerProps>
|
||||
>
|
||||
|
||||
@@ -37,7 +37,6 @@ export const searchPlugin =
|
||||
|
||||
const pluginConfig: SearchPluginConfigWithLocales = {
|
||||
// write any config defaults here
|
||||
apiBasePath: config.routes?.api,
|
||||
deleteDrafts: true,
|
||||
labels,
|
||||
locales,
|
||||
|
||||
@@ -3,7 +3,6 @@ import type {
|
||||
CollectionAfterDeleteHook,
|
||||
CollectionConfig,
|
||||
Field,
|
||||
LabelFunction,
|
||||
Locale,
|
||||
Payload,
|
||||
PayloadRequest,
|
||||
@@ -31,24 +30,46 @@ export type BeforeSync = (args: {
|
||||
export type FieldsOverride = (args: { defaultFields: Field[] }) => Field[]
|
||||
|
||||
export type SearchPluginConfig = {
|
||||
/**
|
||||
* @deprecated
|
||||
* This plugin gets the api route from the config directly and does not need to be passed in.
|
||||
* As long as you have `routes.api` set in your Payload config, the plugin will use that.
|
||||
* This property will be removed in the next major version.
|
||||
*/
|
||||
apiBasePath?: string
|
||||
beforeSync?: BeforeSync
|
||||
collections?: string[]
|
||||
defaultPriorities?: {
|
||||
[collection: string]: ((doc: any) => number | Promise<number>) | number
|
||||
}
|
||||
/**
|
||||
* Controls whether drafts are deleted from the search index
|
||||
*
|
||||
* @default true
|
||||
*/
|
||||
deleteDrafts?: boolean
|
||||
localize?: boolean
|
||||
/**
|
||||
* We use batching when re-indexing large collections. You can control the amount of items per batch, lower numbers should help with memory.
|
||||
*
|
||||
* @default 50
|
||||
*/
|
||||
reindexBatchSize?: number
|
||||
searchOverrides?: { fields?: FieldsOverride } & Partial<Omit<CollectionConfig, 'fields'>>
|
||||
/**
|
||||
* Controls whether drafts are synced to the search index
|
||||
*
|
||||
* @default false
|
||||
*/
|
||||
syncDrafts?: boolean
|
||||
}
|
||||
|
||||
export type CollectionLabels = {
|
||||
[collection: string]: {
|
||||
plural?: LabelFunction | StaticLabel
|
||||
singular?: LabelFunction | StaticLabel
|
||||
}
|
||||
[collection: string]: CollectionConfig['labels']
|
||||
}
|
||||
|
||||
export type ResolvedCollectionLabels = {
|
||||
[collection: string]: StaticLabel
|
||||
}
|
||||
|
||||
export type SearchPluginConfigWithLocales = {
|
||||
@@ -62,7 +83,7 @@ export type SyncWithSearchArgs = {
|
||||
} & Omit<Parameters<CollectionAfterChangeHook>[0], 'collection'>
|
||||
|
||||
export type SyncDocArgs = {
|
||||
locale?: string
|
||||
locale?: Locale['code']
|
||||
onSyncError?: () => void
|
||||
} & Omit<SyncWithSearchArgs, 'context' | 'previousDoc'>
|
||||
|
||||
|
||||
@@ -142,15 +142,42 @@ export const syncDocAsSearchIndex = async ({
|
||||
}
|
||||
}
|
||||
if (deleteDrafts && status === 'draft') {
|
||||
// do not include draft docs in search results, so delete the record
|
||||
try {
|
||||
await payload.delete({
|
||||
id: searchDocID,
|
||||
collection: searchSlug,
|
||||
req,
|
||||
})
|
||||
} catch (err: unknown) {
|
||||
payload.logger.error({ err, msg: `Error deleting ${searchSlug} document.` })
|
||||
// Check to see if there's a published version of the doc
|
||||
// We don't want to remove the search doc if there is a published version but a new draft has been created
|
||||
const {
|
||||
docs: [docWithPublish],
|
||||
} = await payload.find({
|
||||
collection,
|
||||
draft: false,
|
||||
locale: syncLocale,
|
||||
req,
|
||||
where: {
|
||||
and: [
|
||||
{
|
||||
_status: {
|
||||
equals: 'published',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: {
|
||||
equals: id,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
if (!docWithPublish) {
|
||||
// do not include draft docs in search results, so delete the record
|
||||
try {
|
||||
await payload.delete({
|
||||
id: searchDocID,
|
||||
collection: searchSlug,
|
||||
req,
|
||||
})
|
||||
} catch (err: unknown) {
|
||||
payload.logger.error({ err, msg: `Error deleting ${searchSlug} document.` })
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (doSync) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-sentry",
|
||||
"version": "3.20.0",
|
||||
"version": "3.22.0",
|
||||
"description": "Sentry plugin for Payload",
|
||||
"keywords": [
|
||||
"payload",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-seo",
|
||||
"version": "3.20.0",
|
||||
"version": "3.22.0",
|
||||
"description": "SEO plugin for Payload",
|
||||
"keywords": [
|
||||
"payload",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-stripe",
|
||||
"version": "3.20.0",
|
||||
"version": "3.22.0",
|
||||
"description": "Stripe plugin for Payload",
|
||||
"keywords": [
|
||||
"payload",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/richtext-lexical",
|
||||
"version": "3.20.0",
|
||||
"version": "3.22.0",
|
||||
"description": "The officially supported Lexical richtext adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
@@ -372,10 +372,10 @@
|
||||
"uuid": "10.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "7.25.9",
|
||||
"@babel/core": "7.26.0",
|
||||
"@babel/preset-env": "7.26.0",
|
||||
"@babel/preset-react": "7.25.9",
|
||||
"@babel/cli": "7.26.4",
|
||||
"@babel/core": "7.26.7",
|
||||
"@babel/preset-env": "7.26.7",
|
||||
"@babel/preset-react": "7.26.3",
|
||||
"@babel/preset-typescript": "7.26.0",
|
||||
"@lexical/eslint-plugin": "0.21.0",
|
||||
"@payloadcms/eslint-config": "workspace:*",
|
||||
@@ -384,13 +384,13 @@
|
||||
"@types/node": "22.5.4",
|
||||
"@types/react": "19.0.1",
|
||||
"@types/react-dom": "19.0.1",
|
||||
"babel-plugin-react-compiler": "19.0.0-beta-df7b47d-20241124",
|
||||
"babel-plugin-react-compiler": "19.0.0-beta-714736e-20250131",
|
||||
"babel-plugin-transform-remove-imports": "^1.8.0",
|
||||
"esbuild": "0.24.0",
|
||||
"esbuild": "0.24.2",
|
||||
"esbuild-sass-plugin": "3.3.1",
|
||||
"eslint-plugin-react-compiler": "19.0.0-beta-df7b47d-20241124",
|
||||
"eslint-plugin-react-compiler": "19.0.0-beta-714736e-20250131",
|
||||
"payload": "workspace:*",
|
||||
"swc-plugin-transform-remove-imports": "2.0.0"
|
||||
"swc-plugin-transform-remove-imports": "3.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@faceless-ui/modal": "3.0.0-beta.2",
|
||||
|
||||
@@ -29,6 +29,7 @@ export const ListJSXConverter: JSXConverters<SerializedListItemNode | Serialized
|
||||
className={`list-item-checkbox${node.checked ? ' list-item-checkbox-checked' : ' list-item-checkbox-unchecked'}${hasSubLists ? ' nestedListItem' : ''}`}
|
||||
// eslint-disable-next-line jsx-a11y/no-noninteractive-element-to-interactive-role
|
||||
role="checkbox"
|
||||
style={{ listStyleType: 'none' }}
|
||||
tabIndex={-1}
|
||||
value={node?.value}
|
||||
>
|
||||
@@ -45,7 +46,11 @@ export const ListJSXConverter: JSXConverters<SerializedListItemNode | Serialized
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<li className={hasSubLists ? 'nestedListItem' : ''} value={node?.value}>
|
||||
<li
|
||||
className={`${hasSubLists ? 'nestedListItem' : ''}`}
|
||||
style={hasSubLists ? { listStyleType: 'none' } : undefined}
|
||||
value={node?.value}
|
||||
>
|
||||
{children}
|
||||
</li>
|
||||
)
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
.payload-richtext .nestedListItem,
|
||||
.payload-richtext .list-check {
|
||||
list-style-type: none;
|
||||
}
|
||||
@@ -11,7 +11,6 @@ import type { JSXConverters } from './converter/types.js'
|
||||
|
||||
import { defaultJSXConverters } from './converter/defaultConverters.js'
|
||||
import { convertLexicalToJSX } from './converter/index.js'
|
||||
import './index.css'
|
||||
|
||||
export type JSXConvertersFunction<
|
||||
T extends { [key: string]: any; type?: string } =
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
Pill,
|
||||
RenderFields,
|
||||
SectionTitle,
|
||||
useDocumentForm,
|
||||
useDocumentInfo,
|
||||
useEditDepth,
|
||||
useFormSubmitted,
|
||||
@@ -23,6 +24,7 @@ import { deepCopyObjectSimpleWithoutReactComponents, reduceFieldsToValues } from
|
||||
import React, { useCallback, useEffect, useMemo, useRef } from 'react'
|
||||
|
||||
const baseClass = 'lexical-block'
|
||||
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||
import { getTranslation } from '@payloadcms/translations'
|
||||
import { $getNodeByKey } from 'lexical'
|
||||
@@ -33,9 +35,9 @@ import type { BlockFields } from '../../server/nodes/BlocksNode.js'
|
||||
|
||||
import { useEditorConfigContext } from '../../../../lexical/config/client/EditorConfigProvider.js'
|
||||
import { useLexicalDrawer } from '../../../../utilities/fieldsDrawer/useLexicalDrawer.js'
|
||||
import './index.scss'
|
||||
import { $isBlockNode } from '../nodes/BlocksNode.js'
|
||||
import { BlockContent } from './BlockContent.js'
|
||||
import './index.scss'
|
||||
import { removeEmptyArrayValues } from './removeEmptyArrayValues.js'
|
||||
|
||||
type Props = {
|
||||
@@ -64,6 +66,8 @@ export const BlockComponent: React.FC<Props> = (props) => {
|
||||
},
|
||||
uuid: uuidFromContext,
|
||||
} = useEditorConfigContext()
|
||||
|
||||
const { fields: parentDocumentFields } = useDocumentForm()
|
||||
const onChangeAbortControllerRef = useRef(new AbortController())
|
||||
const editDepth = useEditDepth()
|
||||
const [errorCount, setErrorCount] = React.useState(0)
|
||||
@@ -127,7 +131,9 @@ export const BlockComponent: React.FC<Props> = (props) => {
|
||||
data: formData,
|
||||
docPermissions: { fields: true },
|
||||
docPreferences: await getDocPreferences(),
|
||||
documentFormState: deepCopyObjectSimpleWithoutReactComponents(parentDocumentFields),
|
||||
globalSlug,
|
||||
initialBlockData: formData,
|
||||
operation: 'update',
|
||||
renderAllFields: true,
|
||||
schemaPath: schemaFieldsPath,
|
||||
@@ -164,6 +170,7 @@ export const BlockComponent: React.FC<Props> = (props) => {
|
||||
collectionSlug,
|
||||
globalSlug,
|
||||
getDocPreferences,
|
||||
parentDocumentFields,
|
||||
])
|
||||
|
||||
const [isCollapsed, setIsCollapsed] = React.useState<boolean>(
|
||||
@@ -174,7 +181,7 @@ export const BlockComponent: React.FC<Props> = (props) => {
|
||||
|
||||
const clientSchemaMap = featureClientSchemaMap['blocks']
|
||||
|
||||
const blocksField: BlocksFieldClient | undefined = clientSchemaMap[
|
||||
const blocksField: BlocksFieldClient | undefined = clientSchemaMap?.[
|
||||
componentMapRenderedBlockPath
|
||||
]?.[0] as BlocksFieldClient
|
||||
|
||||
@@ -196,8 +203,10 @@ export const BlockComponent: React.FC<Props> = (props) => {
|
||||
fields: true,
|
||||
},
|
||||
docPreferences: await getDocPreferences(),
|
||||
documentFormState: deepCopyObjectSimpleWithoutReactComponents(parentDocumentFields),
|
||||
formState: prevFormState,
|
||||
globalSlug,
|
||||
initialBlockFormState: prevFormState,
|
||||
operation: 'update',
|
||||
renderAllFields: submit ? true : false,
|
||||
schemaPath: schemaFieldsPath,
|
||||
@@ -208,7 +217,9 @@ export const BlockComponent: React.FC<Props> = (props) => {
|
||||
return prevFormState
|
||||
}
|
||||
|
||||
newFormState.blockName = prevFormState.blockName
|
||||
if (prevFormState.blockName) {
|
||||
newFormState.blockName = prevFormState.blockName
|
||||
}
|
||||
|
||||
const newFormStateData: BlockFields = reduceFieldsToValues(
|
||||
removeEmptyArrayValues({
|
||||
@@ -252,6 +263,7 @@ export const BlockComponent: React.FC<Props> = (props) => {
|
||||
globalSlug,
|
||||
schemaFieldsPath,
|
||||
formData.blockType,
|
||||
parentDocumentFields,
|
||||
editor,
|
||||
nodeKey,
|
||||
],
|
||||
@@ -436,6 +448,8 @@ export const BlockComponent: React.FC<Props> = (props) => {
|
||||
],
|
||||
)
|
||||
|
||||
const clientBlockFields = clientBlock?.fields ?? []
|
||||
|
||||
const BlockDrawer = useMemo(
|
||||
() => () => (
|
||||
<EditDepthProvider>
|
||||
@@ -449,7 +463,7 @@ export const BlockComponent: React.FC<Props> = (props) => {
|
||||
{initialState ? (
|
||||
<>
|
||||
<RenderFields
|
||||
fields={clientBlock?.fields}
|
||||
fields={clientBlockFields}
|
||||
forceRender
|
||||
parentIndexPath=""
|
||||
parentPath="" // See Blocks feature path for details as for why this is empty
|
||||
@@ -488,7 +502,7 @@ export const BlockComponent: React.FC<Props> = (props) => {
|
||||
return await onChange({ formState, submit: true })
|
||||
},
|
||||
]}
|
||||
fields={clientBlock?.fields}
|
||||
fields={clientBlockFields}
|
||||
initialState={initialState}
|
||||
onChange={[onChange]}
|
||||
onSubmit={(formState, newData) => {
|
||||
@@ -512,7 +526,7 @@ export const BlockComponent: React.FC<Props> = (props) => {
|
||||
CustomBlock={CustomBlock}
|
||||
EditButton={EditButton}
|
||||
errorCount={errorCount}
|
||||
formSchema={clientBlock?.fields}
|
||||
formSchema={clientBlockFields}
|
||||
initialState={initialState}
|
||||
nodeKey={nodeKey}
|
||||
RemoveButton={RemoveButton}
|
||||
@@ -523,6 +537,7 @@ export const BlockComponent: React.FC<Props> = (props) => {
|
||||
BlockCollapsible,
|
||||
BlockDrawer,
|
||||
CustomBlock,
|
||||
clientBlockFields,
|
||||
RemoveButton,
|
||||
EditButton,
|
||||
editor,
|
||||
|
||||
@@ -10,7 +10,7 @@ import type { FormState } from 'payload'
|
||||
export function removeEmptyArrayValues({ fields }: { fields: FormState }): FormState {
|
||||
for (const key in fields) {
|
||||
const field = fields[key]
|
||||
if (Array.isArray(field.rows) && 'value' in field) {
|
||||
if (Array.isArray(field?.rows) && 'value' in field) {
|
||||
field.disableFormData = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
FormSubmit,
|
||||
RenderFields,
|
||||
ShimmerEffect,
|
||||
useDocumentForm,
|
||||
useDocumentInfo,
|
||||
useEditDepth,
|
||||
useServerFunctions,
|
||||
@@ -26,6 +27,7 @@ import { $getNodeByKey } from 'lexical'
|
||||
|
||||
import './index.scss'
|
||||
|
||||
import { deepCopyObjectSimpleWithoutReactComponents } from 'payload/shared'
|
||||
import { v4 as uuid } from 'uuid'
|
||||
|
||||
import type { InlineBlockFields } from '../../server/nodes/InlineBlocksNode.js'
|
||||
@@ -77,6 +79,8 @@ export const InlineBlockComponent: React.FC<Props> = (props) => {
|
||||
setCreatedInlineBlock,
|
||||
uuid: uuidFromContext,
|
||||
} = useEditorConfigContext()
|
||||
const { fields: parentDocumentFields } = useDocumentForm()
|
||||
|
||||
const { getFormState } = useServerFunctions()
|
||||
const editDepth = useEditDepth()
|
||||
const firstTimeDrawer = useRef(false)
|
||||
@@ -112,29 +116,25 @@ export const InlineBlockComponent: React.FC<Props> = (props) => {
|
||||
|
||||
const clientSchemaMap = featureClientSchemaMap['blocks']
|
||||
|
||||
const blocksField: BlocksFieldClient = clientSchemaMap[
|
||||
const blocksField: BlocksFieldClient = clientSchemaMap?.[
|
||||
componentMapRenderedBlockPath
|
||||
]?.[0] as BlocksFieldClient
|
||||
|
||||
const clientBlock = blocksField?.blocks?.[0]
|
||||
|
||||
const clientBlockFields = clientBlock?.fields ?? []
|
||||
|
||||
// Open drawer on "mount"
|
||||
useEffect(() => {
|
||||
if (!firstTimeDrawer.current && createdInlineBlock?.getKey() === nodeKey) {
|
||||
// > 2 because they always have "id" and "blockName" fields
|
||||
if (clientBlock?.fields?.length > 2) {
|
||||
if (clientBlockFields.length > 2) {
|
||||
toggleDrawer()
|
||||
}
|
||||
setCreatedInlineBlock?.(undefined)
|
||||
firstTimeDrawer.current = true
|
||||
}
|
||||
}, [
|
||||
clientBlock?.fields?.length,
|
||||
createdInlineBlock,
|
||||
nodeKey,
|
||||
setCreatedInlineBlock,
|
||||
toggleDrawer,
|
||||
])
|
||||
}, [clientBlockFields.length, createdInlineBlock, nodeKey, setCreatedInlineBlock, toggleDrawer])
|
||||
|
||||
const removeInlineBlock = useCallback(() => {
|
||||
editor.update(() => {
|
||||
@@ -165,7 +165,10 @@ export const InlineBlockComponent: React.FC<Props> = (props) => {
|
||||
data: formData,
|
||||
docPermissions: { fields: true },
|
||||
docPreferences: await getDocPreferences(),
|
||||
documentFormState: deepCopyObjectSimpleWithoutReactComponents(parentDocumentFields),
|
||||
globalSlug,
|
||||
initialBlockData: formData,
|
||||
initialBlockFormState: formData,
|
||||
operation: 'update',
|
||||
renderAllFields: true,
|
||||
schemaPath: schemaFieldsPath,
|
||||
@@ -195,6 +198,7 @@ export const InlineBlockComponent: React.FC<Props> = (props) => {
|
||||
collectionSlug,
|
||||
globalSlug,
|
||||
getDocPreferences,
|
||||
parentDocumentFields,
|
||||
])
|
||||
|
||||
/**
|
||||
@@ -214,8 +218,10 @@ export const InlineBlockComponent: React.FC<Props> = (props) => {
|
||||
fields: true,
|
||||
},
|
||||
docPreferences: await getDocPreferences(),
|
||||
documentFormState: deepCopyObjectSimpleWithoutReactComponents(parentDocumentFields),
|
||||
formState: prevFormState,
|
||||
globalSlug,
|
||||
initialBlockFormState: prevFormState,
|
||||
operation: 'update',
|
||||
renderAllFields: submit ? true : false,
|
||||
schemaPath: schemaFieldsPath,
|
||||
@@ -233,7 +239,15 @@ export const InlineBlockComponent: React.FC<Props> = (props) => {
|
||||
|
||||
return state
|
||||
},
|
||||
[getFormState, id, collectionSlug, getDocPreferences, globalSlug, schemaFieldsPath],
|
||||
[
|
||||
getFormState,
|
||||
id,
|
||||
collectionSlug,
|
||||
getDocPreferences,
|
||||
parentDocumentFields,
|
||||
globalSlug,
|
||||
schemaFieldsPath,
|
||||
],
|
||||
)
|
||||
// cleanup effect
|
||||
useEffect(() => {
|
||||
|
||||
@@ -25,13 +25,17 @@ export const BlocksFeatureClient = createClientFeature(
|
||||
const schemaMapRenderedInlineBlockPathPrefix = `${schemaPath}.lexical_internal_feature.blocks.lexical_inline_blocks`
|
||||
const clientSchema = featureClientSchemaMap['blocks']
|
||||
|
||||
if (!clientSchema) {
|
||||
return {}
|
||||
}
|
||||
|
||||
const blocksFields: BlocksFieldClient[] = Object.entries(clientSchema)
|
||||
.filter(
|
||||
([key]) =>
|
||||
key.startsWith(schemaMapRenderedBlockPathPrefix + '.') &&
|
||||
!key.replace(schemaMapRenderedBlockPathPrefix + '.', '').includes('.'),
|
||||
)
|
||||
.map(([key, value]) => value[0] as BlocksFieldClient)
|
||||
.map(([, value]) => value[0] as BlocksFieldClient)
|
||||
|
||||
const inlineBlocksFields: BlocksFieldClient[] = Object.entries(clientSchema)
|
||||
.filter(
|
||||
@@ -39,15 +43,19 @@ export const BlocksFeatureClient = createClientFeature(
|
||||
key.startsWith(schemaMapRenderedInlineBlockPathPrefix + '.') &&
|
||||
!key.replace(schemaMapRenderedInlineBlockPathPrefix + '.', '').includes('.'),
|
||||
)
|
||||
.map(([key, value]) => value[0] as BlocksFieldClient)
|
||||
.map(([, value]) => value[0] as BlocksFieldClient)
|
||||
|
||||
const clientBlocks: ClientBlock[] = blocksFields.map((field) => {
|
||||
return field.blocks[0]
|
||||
})
|
||||
const clientBlocks: ClientBlock[] = blocksFields
|
||||
.map((field) => {
|
||||
return field.blocks[0]
|
||||
})
|
||||
.filter((block) => block !== undefined)
|
||||
|
||||
const clientInlineBlocks: ClientBlock[] = inlineBlocksFields.map((field) => {
|
||||
return field.blocks[0]
|
||||
})
|
||||
const clientInlineBlocks: ClientBlock[] = inlineBlocksFields
|
||||
.map((field) => {
|
||||
return field.blocks[0]
|
||||
})
|
||||
.filter((block) => block !== undefined)
|
||||
|
||||
return {
|
||||
nodes: [BlockNode, InlineBlockNode],
|
||||
|
||||
@@ -92,7 +92,7 @@ export const getBlockMarkdownTransformers = ({
|
||||
|
||||
const childrenString = linesInBetween.join('\n').trim()
|
||||
|
||||
const propsString: null | string = openMatch?.length > 2 ? openMatch[2]?.trim() : null
|
||||
const propsString = openMatch[2]?.trim()
|
||||
|
||||
const markdownToLexical = getMarkdownToLexical(allNodes, allTransformers)
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user