Compare commits
1 Commits
v3.23.0
...
ci/node-23
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8fe8edc014 |
25
.github/CODEOWNERS
vendored
25
.github/CODEOWNERS
vendored
@@ -1,35 +1,24 @@
|
||||
# Order matters. The last matching pattern takes precedence.
|
||||
|
||||
### Package Exports
|
||||
|
||||
### Package Exports ###
|
||||
**/exports/ @denolfe @jmikrut @DanRibbens
|
||||
|
||||
### Packages
|
||||
|
||||
### Packages ###
|
||||
/packages/plugin-cloud*/src/ @denolfe @jmikrut @DanRibbens
|
||||
/packages/email-*/src/ @denolfe @jmikrut @DanRibbens
|
||||
/packages/live-preview*/src/ @jacobsfletch
|
||||
/packages/plugin-stripe/src/ @jacobsfletch
|
||||
/packages/plugin-multi-tenant/src/ @JarrodMFlesch
|
||||
/packages/richtext-*/src/ @AlessioGr @GermanJablo
|
||||
/packages/next/src/ @jmikrut @jacobsfletch @AlessioGr @JarrodMFlesch
|
||||
/packages/ui/src/ @jmikrut @jacobsfletch @AlessioGr @JarrodMFlesch
|
||||
/packages/storage-*/src/ @denolfe @jmikrut @DanRibbens
|
||||
/packages/create-payload-app/src/ @denolfe @jmikrut @DanRibbens
|
||||
/packages/eslint-*/ @denolfe @jmikrut @DanRibbens @AlessioGr @GermanJablo
|
||||
|
||||
### Templates
|
||||
/packages/eslint-*/ @denolfe @jmikrut @DanRibbens @AlessioGr
|
||||
|
||||
### Templates ###
|
||||
/templates/_data/ @denolfe @jmikrut @DanRibbens
|
||||
/templates/_template/ @denolfe @jmikrut @DanRibbens
|
||||
|
||||
### Build Files
|
||||
|
||||
**/tsconfig*.json @denolfe @jmikrut @DanRibbens @AlessioGr @GermanJablo
|
||||
### Build Files ###
|
||||
**/tsconfig*.json @denolfe @jmikrut @DanRibbens @AlessioGr
|
||||
**/jest.config.js @denolfe @jmikrut @DanRibbens @AlessioGr
|
||||
|
||||
### Root
|
||||
|
||||
### Root ###
|
||||
/package.json @denolfe @jmikrut @DanRibbens
|
||||
/tools/ @denolfe @jmikrut @DanRibbens
|
||||
/.husky/ @denolfe @jmikrut @DanRibbens
|
||||
|
||||
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.37",
|
||||
"@swc/jest": "^0.2.36",
|
||||
"@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.37
|
||||
version: 0.2.37(@swc/core@1.7.26)
|
||||
specifier: ^0.2.36
|
||||
version: 0.2.36(@swc/core@1.7.26)
|
||||
'@types/jest':
|
||||
specifier: ^27.5.2
|
||||
version: 27.5.2
|
||||
@@ -48,6 +48,9 @@ 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
|
||||
@@ -65,8 +68,8 @@ importers:
|
||||
specifier: ^7.5.1
|
||||
version: 7.5.1
|
||||
'@swc/jest':
|
||||
specifier: ^0.2.37
|
||||
version: 0.2.37(@swc/core@1.7.26)
|
||||
specifier: ^0.2.36
|
||||
version: 0.2.36(@swc/core@1.7.26)
|
||||
'@types/jest':
|
||||
specifier: ^27.5.2
|
||||
version: 27.5.2
|
||||
@@ -94,6 +97,9 @@ 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
|
||||
@@ -380,6 +386,10 @@ 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}
|
||||
@@ -532,8 +542,8 @@ packages:
|
||||
'@swc/counter@0.1.3':
|
||||
resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==}
|
||||
|
||||
'@swc/jest@0.2.37':
|
||||
resolution: {integrity: sha512-CR2BHhmXKGxTiFr21DYPRHQunLkX3mNIFGFkxBGji6r9uyIR5zftTOVYj1e0sFNMV2H7mf/+vpaglqaryBtqfQ==}
|
||||
'@swc/jest@0.2.36':
|
||||
resolution: {integrity: sha512-8X80dp81ugxs4a11z1ka43FPhP+/e+mJNXJSxiNYk8gIX/jPBtY4gQTrKu/KIoco8bzKuPI5lUxjfLiGsfvnlw==}
|
||||
engines: {npm: '>= 7.0.0'}
|
||||
peerDependencies:
|
||||
'@swc/core': '*'
|
||||
@@ -580,6 +590,9 @@ 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==}
|
||||
|
||||
@@ -733,6 +746,10 @@ 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==}
|
||||
|
||||
@@ -766,6 +783,9 @@ 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'}
|
||||
@@ -1113,6 +1133,10 @@ 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'}
|
||||
@@ -1287,6 +1311,10 @@ 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}
|
||||
@@ -1386,6 +1414,9 @@ 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==}
|
||||
|
||||
@@ -1407,6 +1438,11 @@ 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==}
|
||||
|
||||
@@ -1716,6 +1752,14 @@ 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==}
|
||||
|
||||
@@ -1819,6 +1863,10 @@ 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'}
|
||||
@@ -2259,6 +2307,14 @@ 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
|
||||
@@ -2421,7 +2477,7 @@ snapshots:
|
||||
|
||||
'@swc/counter@0.1.3': {}
|
||||
|
||||
'@swc/jest@0.2.37(@swc/core@1.7.26)':
|
||||
'@swc/jest@0.2.36(@swc/core@1.7.26)':
|
||||
dependencies:
|
||||
'@jest/create-cache-key-function': 29.7.0
|
||||
'@swc/core': 1.7.26
|
||||
@@ -2482,6 +2538,10 @@ 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
|
||||
@@ -2682,6 +2742,10 @@ 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
|
||||
@@ -2709,6 +2773,8 @@ snapshots:
|
||||
|
||||
char-regex@1.0.2: {}
|
||||
|
||||
ci-info@2.0.0: {}
|
||||
|
||||
ci-info@3.9.0: {}
|
||||
|
||||
cjs-module-lexer@1.4.1: {}
|
||||
@@ -3061,6 +3127,10 @@ 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
|
||||
@@ -3400,6 +3470,15 @@ 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
|
||||
@@ -3504,6 +3583,8 @@ snapshots:
|
||||
dependencies:
|
||||
semver: 7.6.3
|
||||
|
||||
make-error@1.3.6: {}
|
||||
|
||||
makeerror@1.0.12:
|
||||
dependencies:
|
||||
tmpl: 1.0.5
|
||||
@@ -3523,6 +3604,8 @@ snapshots:
|
||||
dependencies:
|
||||
brace-expansion: 1.1.11
|
||||
|
||||
mkdirp@1.0.4: {}
|
||||
|
||||
ms@2.1.3: {}
|
||||
|
||||
natural-compare@1.4.0: {}
|
||||
@@ -3776,6 +3859,21 @@ 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: {}
|
||||
@@ -3861,6 +3959,8 @@ 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: '7'
|
||||
issue-inactive-days: '1'
|
||||
exclude-any-issue-labels: 'status: awaiting-reply'
|
||||
log-output: true
|
||||
issue-comment: >
|
||||
|
||||
2
.github/workflows/main.yml
vendored
2
.github/workflows/main.yml
vendored
@@ -16,7 +16,7 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
NODE_VERSION: 22.6.0
|
||||
NODE_VERSION: 23.6.1
|
||||
PNPM_VERSION: 9.7.1
|
||||
DO_NOT_TRACK: 1 # Disable Turbopack telemetry
|
||||
NEXT_TELEMETRY_DISABLED: 1 # Disable Next telemetry
|
||||
|
||||
@@ -654,26 +654,6 @@ 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:
|
||||
@@ -1036,82 +1016,3 @@ export const MySetStepNavComponent: React.FC<{
|
||||
return null
|
||||
}
|
||||
```
|
||||
|
||||
## usePayloadAPI
|
||||
|
||||
The `usePayloadAPI` hook is a useful tool for making REST API requests to your Payload instance and handling responses reactively. It allows you to fetch and interact with data while automatically updating when parameters change.
|
||||
|
||||
This hook returns an array with two elements:
|
||||
|
||||
1. An object containing the API response.
|
||||
2. A set of methods to modify request parameters.
|
||||
|
||||
**Example:**
|
||||
|
||||
```tsx
|
||||
'use client'
|
||||
import { usePayloadAPI } from '@payloadcms/ui'
|
||||
|
||||
const MyComponent: React.FC = () => {
|
||||
// Fetch data from a collection item using its ID
|
||||
const [{ data, error, isLoading }, { setParams }] = usePayloadAPI(
|
||||
'/api/posts/123',
|
||||
{ initialParams: { depth: 1 } }
|
||||
)
|
||||
|
||||
if (isLoading) return <p>Loading...</p>
|
||||
if (error) return <p>Error: {error.message}</p>
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>{data?.title}</h1>
|
||||
<button onClick={() => setParams({ cacheBust: Date.now() })}>
|
||||
Refresh Data
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
**Arguments:**
|
||||
|
||||
| Property | Description |
|
||||
| ------------- | ----------------------------------------------------------------------------------------------- |
|
||||
| **`url`** | The API endpoint to fetch data from. Relative URLs will be prefixed with the Payload API route. |
|
||||
| **`options`** | An object containing initial request parameters and initial state configuration. |
|
||||
|
||||
The `options` argument accepts the following properties:
|
||||
|
||||
| Property | Description |
|
||||
| ------------------- | --------------------------------------------------------------------------------------------------- |
|
||||
| **`initialData`** | Uses this data instead of making an initial request. If not provided, the request runs immediately. |
|
||||
| **`initialParams`** | Defines the initial parameters to use in the request. Defaults to an empty object `{}`. |
|
||||
|
||||
**Returned Value:**
|
||||
|
||||
The first item in the returned array is an object containing the following properties:
|
||||
|
||||
| Property | Description |
|
||||
| --------------- | -------------------------------------------------------- |
|
||||
| **`data`** | The API response data. |
|
||||
| **`error`** | If an error occurs, this contains the error object. |
|
||||
| **`isLoading`** | A boolean indicating whether the request is in progress. |
|
||||
|
||||
The second item is an object with the following methods:
|
||||
|
||||
| Property | Description |
|
||||
| --------------- | ----------------------------------------------------------- |
|
||||
| **`setParams`** | Updates request parameters, triggering a refetch if needed. |
|
||||
|
||||
#### Updating Data
|
||||
|
||||
The `setParams` function can be used to update the request and trigger a refetch:
|
||||
|
||||
```tsx
|
||||
setParams({ depth: 2 })
|
||||
```
|
||||
|
||||
This is useful for scenarios where you need to trigger another fetch regardless of the `url` argument changing.
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ The `admin` directory contains all the _pages_ related to the interface itself,
|
||||
|
||||
<Banner type="warning">
|
||||
**Note:**
|
||||
If you don't intend to use the Admin Panel, [REST API](../rest-api/overview), or [GraphQL API](../graphql/overview), you can opt-out by simply deleting their corresponding directories within your Next.js app. The overhead, however, is completely constrained to these routes, and will not slow down or affect Payload outside when not in use.
|
||||
If you don't intend to use the Admin Panel, [REST API](../rest/overview), or [GraphQL API](../graphql/overview), you can opt-out by simply deleting their corresponding directories within your Next.js app. The overhead, however, is completely constrained to these routes, and will not slow down or affect Payload outside when not in use.
|
||||
</Banner>
|
||||
|
||||
Finally, the `custom.scss` file is where you can add or override globally-oriented styles in the Admin Panel, such as modify the color palette. Customizing the look and feel through CSS alone is a powerful feature of the Admin Panel, [more on that here](./customizing-css).
|
||||
@@ -99,7 +99,6 @@ The following options are available:
|
||||
| **`routes`** | Replace built-in Admin Panel routes with your own custom routes. [More details](#customizing-routes). |
|
||||
| **`suppressHydrationWarning`** | If set to `true`, suppresses React hydration mismatch warnings during the hydration of the root `<html>` tag. Defaults to `false`. |
|
||||
| **`theme`** | Restrict the Admin Panel theme to use only one of your choice. Default is `all`. |
|
||||
| **`timezones`** | Configure the timezone settings for the admin panel. [More details](#timezones) |
|
||||
| **`user`** | The `slug` of the Collection that you want to allow to login to the Admin Panel. [More details](#the-admin-user-collection). |
|
||||
|
||||
<Banner type="success">
|
||||
@@ -243,21 +242,3 @@ The Payload Admin Panel is translated in over [30 languages and counting](https:
|
||||
## Light and Dark Modes
|
||||
|
||||
Users in the Admin Panel have the ability to choose between light mode and dark mode for their editing experience. Users can select their preferred theme from their account page. Once selected, it is saved to their user's preferences and persisted across sessions and devices. If no theme was selected, the Admin Panel will automatically detect the operation system's theme and use that as the default.
|
||||
|
||||
## Timezones
|
||||
|
||||
The `admin.timezones` configuration allows you to configure timezone settings for the Admin Panel. You can customise the available list of timezones and in the future configure the default timezone for the Admin Panel and for all users.
|
||||
|
||||
The following options are available:
|
||||
|
||||
| Option | Description |
|
||||
| ----------------- | ----------------------------------------------- |
|
||||
| `supportedTimezones` | An array of label/value options for selectable timezones where the value is the IANA name eg. `America/Detroit` |
|
||||
| `defaultTimezone` | The `value` of the default selected timezone. eg. `America/Los_Angeles` |
|
||||
|
||||
We validate the supported timezones array by checking the value against the list of IANA timezones supported via the Intl API, specifically `Intl.supportedValuesOf('timeZone')`.
|
||||
|
||||
<Banner type="info">
|
||||
**Important**
|
||||
You must enable timezones on each individual date field via `timezone: true`. See [Date Fields](../fields/overview#date) for more information.
|
||||
</Banner>
|
||||
|
||||
@@ -181,7 +181,7 @@ If none was in either location, Payload will finally check the `dist` directory.
|
||||
|
||||
### Customizing the Config Location
|
||||
|
||||
In addition to the above automated detection, you can specify your own location for the Payload Config. This can be useful in situations where your config is not in a standard location, or you wish to switch between multiple configurations. To do this, Payload exposes an [Environment Variable](../configuration/environment-vars) to bypass all automatic config detection.
|
||||
In addition to the above automated detection, you can specify your own location for the Payload Config. This can be useful in situations where your config is not in a standard location, or you wish to switch between multiple configurations. To do this, Payload exposes an [Environment Variable](./environment-variables) to bypass all automatic config detection.
|
||||
|
||||
To use a custom config location, set the `PAYLOAD_CONFIG_PATH` environment variable:
|
||||
|
||||
|
||||
@@ -43,7 +43,6 @@ export const MyDateField: Field = {
|
||||
| **`required`** | Require this field to have a value. |
|
||||
| **`admin`** | Admin-specific configuration. [More details](#admin-options). |
|
||||
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
|
||||
| **`timezone`** * | Set to `true` to enable timezone selection on this field. [More details](#timezones). |
|
||||
| **`typescriptSchema`** | Override field type generation with providing a JSON schema |
|
||||
| **`virtual`** | Provide `true` to disable field in the database. See [Virtual Fields](https://payloadcms.com/blog/learn-how-virtual-fields-can-help-solve-common-cms-challenges) |
|
||||
|
||||
@@ -223,23 +222,3 @@ export const CustomDateFieldLabelClient: DateFieldLabelClientComponent = ({
|
||||
}
|
||||
```
|
||||
|
||||
## Timezones
|
||||
|
||||
To enable timezone selection on a Date field, set the `timezone` property to `true`:
|
||||
|
||||
```ts
|
||||
{
|
||||
name: 'date',
|
||||
type: 'date',
|
||||
timezone: true,
|
||||
}
|
||||
```
|
||||
|
||||
This will add a dropdown to the date picker that allows users to select a timezone. The selected timezone will be saved in the database along with the date in a new column named `date_tz`.
|
||||
|
||||
You can customise the available list of timezones in the [global admin config](../admin/overview#timezones).
|
||||
|
||||
<Banner type='info'>
|
||||
**Good to know:**
|
||||
The date itself will be stored in UTC so it's up to you to handle the conversion to the user's timezone when displaying the date in your frontend.
|
||||
</Banner>
|
||||
|
||||
@@ -29,7 +29,7 @@ As mentioned above, you can queue jobs, but the jobs won't run unless a worker p
|
||||
|
||||
#### Cron jobs
|
||||
|
||||
You can use the `jobs.autoRun` property to configure cron jobs:
|
||||
You can use the jobs.autoRun property to configure cron jobs:
|
||||
|
||||
```ts
|
||||
export default buildConfig({
|
||||
@@ -47,12 +47,6 @@ export default buildConfig({
|
||||
},
|
||||
// add as many cron jobs as you want
|
||||
],
|
||||
shouldAutoRun: async (payload) => {
|
||||
// Tell Payload if it should run jobs or not.
|
||||
// This function will be invoked each time Payload goes to pick up and run jobs.
|
||||
// If this function ever returns false, the cron schedule will be stopped.
|
||||
return true
|
||||
}
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
@@ -20,7 +20,7 @@ The most important aspect of a Workflow is the `handler`, where you can declare
|
||||
|
||||
However, importantly, tasks that have successfully been completed will simply re-return the cached and saved output without running again. The Workflow will pick back up where it failed and only task from the failure point onward will be re-executed.
|
||||
|
||||
To define a JS-based workflow, simply add a workflow to the `jobs.workflows` array in your Payload config. A workflow consists of the following fields:
|
||||
To define a JS-based workflow, simply add a workflow to the `jobs.wokflows` array in your Payload config. A workflow consists of the following fields:
|
||||
|
||||
| Option | Description |
|
||||
| --------------------------- | -------------------------------------------------------------------------------- |
|
||||
|
||||
@@ -109,12 +109,10 @@ The following arguments are provided to the `url` function:
|
||||
| **`globalConfig`** | The Global Admin Config of the Document being edited. [More details](../configuration/globals#admin-options). |
|
||||
| **`req`** | The Payload Request object. |
|
||||
|
||||
You can return either an absolute URL or relative URL from this function. If you don't know the URL of your frontend at build-time, you can return a relative URL, and in that case, Payload will automatically construct an absolute URL by injecting the protocol, domain, and port from your browser window. Returning a relative URL is helpful for platforms like Vercel where you may have preview deployment URLs that are unknown at build time.
|
||||
|
||||
If your application requires a fully qualified URL, or you are attempting to preview with a frontend on a different domain, you can use the `req` property to build this URL:
|
||||
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: ({ data, req }) => `${req.protocol}//${req.host}/${data.slug}` // highlight-line
|
||||
url: (doc, { req }) => `${req.protocol}//${req.host}/${doc.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.
|
||||
|
||||
@@ -25,18 +25,6 @@ This plugin sets up multi-tenancy for your application from within your [Admin P
|
||||
- Adds a `tenant` field to each specified collection
|
||||
- Adds a tenant selector to the admin panel, allowing you to switch between tenants
|
||||
- Filters list view results by selected tenant
|
||||
- Filters relationship fields by selected tenant
|
||||
- Ability to create "global" like collections, 1 doc per tenant
|
||||
- Automatically assign a tenant to new documents
|
||||
|
||||
<Banner type="error">
|
||||
**Warning**
|
||||
|
||||
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.
|
||||
</Banner>
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -52,7 +40,7 @@ The plugin accepts an object with the following properties:
|
||||
|
||||
```ts
|
||||
type MultiTenantPluginConfig<ConfigTypes = unknown> = {
|
||||
/**
|
||||
/**
|
||||
* After a tenant is deleted, the plugin will attempt to clean up related documents
|
||||
* - removing documents with the tenant ID
|
||||
* - removing the tenant from users
|
||||
@@ -122,18 +110,6 @@ type MultiTenantPluginConfig<ConfigTypes = unknown> = {
|
||||
* Access configuration for the array field
|
||||
*/
|
||||
arrayFieldAccess?: ArrayField['access']
|
||||
/**
|
||||
* Name of the array field
|
||||
*
|
||||
* @default 'tenants'
|
||||
*/
|
||||
arrayFieldName?: string
|
||||
/**
|
||||
* Name of the tenant field
|
||||
*
|
||||
* @default 'tenant'
|
||||
*/
|
||||
arrayTenantFieldName?: string
|
||||
/**
|
||||
* When `includeDefaultField` is `true`, the field will be added to the users collection automatically
|
||||
*/
|
||||
@@ -149,8 +125,6 @@ type MultiTenantPluginConfig<ConfigTypes = unknown> = {
|
||||
}
|
||||
| {
|
||||
arrayFieldAccess?: never
|
||||
arrayFieldName?: string
|
||||
arrayTenantFieldName?: string
|
||||
/**
|
||||
* When `includeDefaultField` is `false`, you must include the field on your users collection manually
|
||||
*/
|
||||
@@ -170,12 +144,8 @@ type MultiTenantPluginConfig<ConfigTypes = unknown> = {
|
||||
* Useful for super-admin type users
|
||||
*/
|
||||
userHasAccessToAllTenants?: (
|
||||
user: ConfigTypes extends { user: unknown } ? ConfigTypes['user'] : User,
|
||||
user: ConfigTypes extends { user } ? ConfigTypes['user'] : User,
|
||||
) => boolean
|
||||
/**
|
||||
* Opt out of adding access constraints to the tenants collection
|
||||
*/
|
||||
useTenantsCollectionAccess?: boolean
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ To securely allow headless operation you will need to configure the allowed orig
|
||||
|
||||
## Limiting GraphQL Complexity
|
||||
|
||||
Because GraphQL gives the power of query writing outside a server's control, someone with bad intentions might write a maliciously complex query and bog down your server. To prevent resource-intensive GraphQL requests, Payload provides a way to specify complexity limits. These limits are based on a complexity score calculated for each request.
|
||||
Because GraphQL gives the power of query writing outside a server's control, someone with bad intentions might write a maliciously complex query and bog down your server. To prevent resource-intensive GraphQL requests, Payload provides a way specify complexity limits which are based on a complexity score that is calculated for each request.
|
||||
|
||||
Any GraphQL request that is calculated to be too expensive is rejected. On the Payload Config, in `graphQL` you can set the `maxComplexity` value as an integer. For reference, the default complexity value for each added field is 1, and all `relationship` and `upload` fields are assigned a value of 10.
|
||||
|
||||
|
||||
@@ -578,7 +578,7 @@ Each endpoint object needs to have:
|
||||
| ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **`path`** | A string for the endpoint route after the collection or globals slug |
|
||||
| **`method`** | The lowercase HTTP verb to use: 'get', 'head', 'post', 'put', 'delete', 'connect' or 'options' |
|
||||
| **`handler`** | A function that accepts **req** - `PayloadRequest` object which contains [Web Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) properties, currently authenticated `user` and the Local API instance `payload`. |
|
||||
| **`handler`** | A function or array of functions to be called with **req**, **res** and **next** arguments. [Next.js](https://nextjs.org/docs/app/building-your-application/routing/route-handlers) |
|
||||
| **`root`** | When `true`, defines the endpoint on the root Next.js app, bypassing Payload handlers and the `routes.api` subpath. Note: this only applies to top-level endpoints of your Payload Config, endpoints defined on `collections` or `globals` cannot be root. |
|
||||
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
|
||||
|
||||
|
||||
@@ -161,7 +161,7 @@ Here's an overview of all the included features:
|
||||
| **`CheckListFeature`** | Yes | Adds checklists |
|
||||
| **`LinkFeature`** | Yes | Allows you to create internal and external links |
|
||||
| **`RelationshipFeature`** | Yes | Allows you to create block-level (not inline) relationships to other documents |
|
||||
| **`BlockquoteFeature`** | Yes | Allows you to create block-level quotes |
|
||||
| **`BlockQuoteFeature`** | Yes | Allows you to create block-level quotes |
|
||||
| **`UploadFeature`** | Yes | Allows you to create block-level upload nodes - this supports all kinds of uploads, not just images |
|
||||
| **`HorizontalRuleFeature`** | Yes | Horizontal rules / separators. Basically displays an `<hr>` element |
|
||||
| **`InlineToolbarFeature`** | Yes | The inline toolbar is the floating toolbar which appears when you select text. This toolbar only contains actions relevant for selected text |
|
||||
@@ -174,7 +174,7 @@ Notice how even the toolbars are features? That's how extensible our lexical edi
|
||||
|
||||
## Creating your own, custom Feature
|
||||
|
||||
You can find more information about creating your own feature in our [building custom feature docs](../rich-text/custom-features).
|
||||
You can find more information about creating your own feature in our [building custom feature docs](../rich-text/building-custom-features).
|
||||
|
||||
## TypeScript
|
||||
|
||||
|
||||
@@ -113,24 +113,7 @@ _An asterisk denotes that an option is required._
|
||||
|
||||
### Payload-wide Upload Options
|
||||
|
||||
Upload options are specifiable on a Collection by Collection basis, you can also control app wide options by passing your base Payload Config an `upload` property containing an object supportive of all `Busboy` configuration options.
|
||||
|
||||
| Option | Description |
|
||||
| ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **`abortOnLimit`** | A boolean that, if `true`, returns HTTP 413 if a file exceeds the file size limit. If `false`, the file is truncated. Defaults to `false`. |
|
||||
| **`createParentPath`** | Set to `true` to automatically create a directory path when moving files from a temporary directory or buffer. Defaults to `false`. |
|
||||
| **`debug`** | A boolean that turns upload process logging on if `true`, or off if `false`. Useful for troubleshooting. Defaults to `false`. |
|
||||
| **`limitHandler`** | A function which is invoked if the file is greater than configured limits. |
|
||||
| **`parseNested`** | Set to `true` to turn `req.body` and `req.files` into nested structures. By default `req.body` and `req.files` are flat objects. Defaults to `false`. |
|
||||
| **`preserveExtension`** | Preserves file extensions with the `safeFileNames` option. Limits file names to 3 characters if `true` or a custom length if a `number`, trimming from the start of the extension. |
|
||||
| **`responseOnLimit`** | A `string` that is sent in the Response to a client if the file size limit is exceeded when used with `abortOnLimit`. |
|
||||
| **`safeFileNames`** | Set to `true` to strip non-alphanumeric characters except dashes and underscores. Can also be set to a regex to determine what to strip. Defaults to `false`. |
|
||||
| **`tempFileDir`** | A `string` path to store temporary files used when the `useTempFiles` option is set to `true`. Defaults to `'./tmp'`. |
|
||||
| **`uploadTimeout`** | A `number` that defines how long to wait for data before aborting, specified in milliseconds. Set to `0` to disable timeout checks. Defaults to `60000`. |
|
||||
| **`uriDecodeFileNames`** | Set to `true` to apply uri decoding to file names. Defaults to `false`. |
|
||||
| **`useTempFiles`** | Set to `true` to store files to a temporary directory instead of in RAM, reducing memory usage for large files or many files. |
|
||||
|
||||
[Click here](https://github.com/mscdex/busboy#api) for more documentation about what you can control with `Busboy`.
|
||||
Upload options are specifiable on a Collection by Collection basis, you can also control app wide options by passing your base Payload Config an `upload` property containing an object supportive of all `Busboy` configuration options. [Click here](https://github.com/mscdex/busboy#api) for more documentation about what you can control.
|
||||
|
||||
A common example of what you might want to customize within Payload-wide Upload options would be to increase the allowed `fileSize` of uploads sent to Payload:
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ export const defaultESLintIgnores = [
|
||||
'**/build/',
|
||||
'**/node_modules/',
|
||||
'**/temp/',
|
||||
'**/packages/*.spec.ts',
|
||||
'**/*.spec.ts',
|
||||
'next-env.d.ts',
|
||||
'**/app',
|
||||
]
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
font-size: unset;
|
||||
font-weight: unset;
|
||||
font-size: auto;
|
||||
font-weight: auto;
|
||||
}
|
||||
|
||||
:root {
|
||||
|
||||
@@ -10,17 +10,11 @@ To spin up this example locally, follow these steps:
|
||||
|
||||
- `npx create-payload-app --example multi-tenant`
|
||||
|
||||
2. `cp .env.example .env` to copy the example environment variables
|
||||
|
||||
3. `pnpm dev`, `yarn dev` or `npm run dev` to start the server
|
||||
2. `pnpm dev`, `yarn dev` or `npm run dev` to start the server
|
||||
- Press `y` when prompted to seed the database
|
||||
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`
|
||||
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`
|
||||
|
||||
## How it works
|
||||
|
||||
@@ -34,7 +28,7 @@ See the [Collections](https://payloadcms.com/docs/configuration/collections) doc
|
||||
|
||||
- #### Users
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
@@ -46,13 +40,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.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.
|
||||
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.
|
||||
|
||||
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:
|
||||
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:
|
||||
|
||||
```
|
||||
127.0.0.1 gold.test silver.test bronze.test
|
||||
```
|
||||
- gold.localhost.com:3000
|
||||
- silver.localhost.com:3000
|
||||
- bronze.localhost.com:3000
|
||||
|
||||
- #### Pages
|
||||
|
||||
@@ -60,7 +54,7 @@ For the domain portion of the example to function properly, you will need to add
|
||||
|
||||
## Access control
|
||||
|
||||
Basic role-based access control is set up to determine what users can and cannot do based on their roles, which are:
|
||||
Basic role-based access control is setup 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.test:3000/tenant-domains/login">
|
||||
http://gold.test:3000/tenant-domains/login
|
||||
<a href="http://gold.localhost.com:3000/tenant-domains/login">
|
||||
http://gold.localhost.com:3000/tenant-domains/login
|
||||
</a>{' '}
|
||||
will show the tenant with the domain "gold.test".
|
||||
will show the tenant with the domain "gold.localhost.com".
|
||||
</p>
|
||||
|
||||
<h2>Slugs</h2>
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Access } from 'payload'
|
||||
/**
|
||||
* Tenant admins and super admins can will be allowed access
|
||||
*/
|
||||
export const superAdminOrTenantAdminAccess: Access = ({ req }) => {
|
||||
export const superAdminOrTeanantAdminAccess: Access = ({ req }) => {
|
||||
if (!req.user) {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
import { ensureUniqueSlug } from './hooks/ensureUniqueSlug'
|
||||
import { superAdminOrTenantAdminAccess } from '@/collections/Pages/access/superAdminOrTenantAdmin'
|
||||
import { superAdminOrTeanantAdminAccess } from '@/collections/Pages/access/superAdminOrTenantAdmin'
|
||||
|
||||
export const Pages: CollectionConfig = {
|
||||
slug: 'pages',
|
||||
access: {
|
||||
create: superAdminOrTenantAdminAccess,
|
||||
delete: superAdminOrTenantAdminAccess,
|
||||
create: superAdminOrTeanantAdminAccess,
|
||||
delete: superAdminOrTeanantAdminAccess,
|
||||
read: () => true,
|
||||
update: superAdminOrTenantAdminAccess,
|
||||
update: superAdminOrTeanantAdminAccess,
|
||||
},
|
||||
admin: {
|
||||
useAsTitle: 'title',
|
||||
|
||||
@@ -1,33 +1,6 @@
|
||||
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: {
|
||||
@@ -37,16 +10,47 @@ 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: 'demo',
|
||||
password: 'test',
|
||||
tenants: [
|
||||
{
|
||||
roles: ['tenant-admin'],
|
||||
tenant: tenant1.id,
|
||||
},
|
||||
// {
|
||||
// roles: ['tenant-admin'],
|
||||
// tenant: tenant2.id,
|
||||
// },
|
||||
],
|
||||
username: 'tenant1',
|
||||
},
|
||||
@@ -56,7 +60,7 @@ export async function up({ payload }: MigrateUpArgs): Promise<void> {
|
||||
collection: 'users',
|
||||
data: {
|
||||
email: 'tenant2@payloadcms.com',
|
||||
password: 'demo',
|
||||
password: 'test',
|
||||
tenants: [
|
||||
{
|
||||
roles: ['tenant-admin'],
|
||||
@@ -71,7 +75,7 @@ export async function up({ payload }: MigrateUpArgs): Promise<void> {
|
||||
collection: 'users',
|
||||
data: {
|
||||
email: 'tenant3@payloadcms.com',
|
||||
password: 'demo',
|
||||
password: 'test',
|
||||
tenants: [
|
||||
{
|
||||
roles: ['tenant-admin'],
|
||||
@@ -86,7 +90,7 @@ export async function up({ payload }: MigrateUpArgs): Promise<void> {
|
||||
collection: 'users',
|
||||
data: {
|
||||
email: 'multi-admin@payloadcms.com',
|
||||
password: 'demo',
|
||||
password: 'test',
|
||||
tenants: [
|
||||
{
|
||||
roles: ['tenant-admin'],
|
||||
@@ -101,7 +105,7 @@ export async function up({ payload }: MigrateUpArgs): Promise<void> {
|
||||
tenant: tenant3.id,
|
||||
},
|
||||
],
|
||||
username: 'multi-admin',
|
||||
username: 'tenant3',
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -18,8 +18,7 @@
|
||||
"payload": "latest",
|
||||
"payload-app": "workspace:*",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"sharp": "0.32.6"
|
||||
"react-dom": "^18.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@remix-run/dev": "^2.15.2",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "payload-monorepo",
|
||||
"version": "3.23.0",
|
||||
"version": "3.20.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.6.0",
|
||||
"@swc/cli": "0.5.1",
|
||||
"@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": "3.1.0",
|
||||
"swc-plugin-transform-remove-imports": "2.0.0",
|
||||
"tempy": "1.0.1",
|
||||
"tstyche": "^3.1.1",
|
||||
"tsx": "4.19.2",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "create-payload-app",
|
||||
"version": "3.23.0",
|
||||
"version": "3.20.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.10.12",
|
||||
"@swc/core": "1.7.10",
|
||||
"arg": "^5.0.0",
|
||||
"chalk": "^4.1.0",
|
||||
"comment-json": "^4.2.3",
|
||||
|
||||
@@ -54,7 +54,6 @@ 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,3 +1,7 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
/* TODO: remove the following lines */
|
||||
"noUncheckedIndexedAccess": false,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-mongodb",
|
||||
"version": "3.23.0",
|
||||
"version": "3.20.0",
|
||||
"description": "The officially supported MongoDB database adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -52,37 +52,30 @@ export async function parseParams({
|
||||
// So we need to loop on keys again here to handle each operator independently
|
||||
const pathOperators = where[relationOrPath]
|
||||
if (typeof pathOperators === 'object') {
|
||||
const validOperators = Object.keys(pathOperators).filter((operator) =>
|
||||
validOperatorSet.has(operator as Operator),
|
||||
)
|
||||
for (const operator of validOperators) {
|
||||
const searchParam = await buildSearchParam({
|
||||
collectionSlug,
|
||||
fields,
|
||||
globalSlug,
|
||||
incomingPath: relationOrPath,
|
||||
locale,
|
||||
operator,
|
||||
payload,
|
||||
val: pathOperators[operator],
|
||||
})
|
||||
|
||||
if (searchParam?.value && searchParam?.path) {
|
||||
if (validOperators.length > 1) {
|
||||
if (!result.$and) {
|
||||
result.$and = []
|
||||
}
|
||||
result.$and.push({
|
||||
[searchParam.path]: searchParam.value,
|
||||
})
|
||||
} else {
|
||||
result[searchParam.path] = searchParam.value
|
||||
}
|
||||
} else if (typeof searchParam?.value === 'object') {
|
||||
result = deepMergeWithCombinedArrays(result, searchParam.value, {
|
||||
// dont clone Types.ObjectIDs
|
||||
clone: false,
|
||||
for (const operator of Object.keys(pathOperators)) {
|
||||
if (validOperatorSet.has(operator as Operator)) {
|
||||
const searchParam = await buildSearchParam({
|
||||
collectionSlug,
|
||||
fields,
|
||||
globalSlug,
|
||||
incomingPath: relationOrPath,
|
||||
locale,
|
||||
operator,
|
||||
payload,
|
||||
val: pathOperators[operator],
|
||||
})
|
||||
|
||||
if (searchParam?.value && searchParam?.path) {
|
||||
result = {
|
||||
...result,
|
||||
[searchParam.path]: searchParam.value,
|
||||
}
|
||||
} else if (typeof searchParam?.value === 'object') {
|
||||
result = deepMergeWithCombinedArrays(result, searchParam.value, {
|
||||
// dont clone Types.ObjectIDs
|
||||
clone: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-postgres",
|
||||
"version": "3.23.0",
|
||||
"version": "3.20.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.2",
|
||||
"esbuild": "0.24.0",
|
||||
"payload": "workspace:*"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-sqlite",
|
||||
"version": "3.23.0",
|
||||
"version": "3.20.0",
|
||||
"description": "The officially supported SQLite database adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { ChainedMethods } from '@payloadcms/drizzle/types'
|
||||
|
||||
import { chainMethods } from '@payloadcms/drizzle'
|
||||
import { count, sql } from 'drizzle-orm'
|
||||
import { count } from 'drizzle-orm'
|
||||
|
||||
import type { CountDistinct, SQLiteAdapter } from './types.js'
|
||||
|
||||
@@ -11,27 +11,18 @@ export const countDistinct: CountDistinct = async function countDistinct(
|
||||
) {
|
||||
const chainedMethods: ChainedMethods = []
|
||||
|
||||
// COUNT(DISTINCT id) is slow on large tables, so we only use DISTINCT if we have to
|
||||
const visitedPaths = new Set([])
|
||||
let useDistinct = false
|
||||
joins.forEach(({ condition, queryPath, table }) => {
|
||||
if (!useDistinct && queryPath) {
|
||||
if (visitedPaths.has(queryPath)) {
|
||||
useDistinct = true
|
||||
} else {
|
||||
visitedPaths.add(queryPath)
|
||||
}
|
||||
}
|
||||
joins.forEach(({ condition, table }) => {
|
||||
chainedMethods.push({
|
||||
args: [table, condition],
|
||||
method: 'leftJoin',
|
||||
})
|
||||
})
|
||||
|
||||
const countResult = await chainMethods({
|
||||
methods: chainedMethods,
|
||||
query: db
|
||||
.select({
|
||||
count: useDistinct ? sql`COUNT(DISTINCT ${this.tables[tableName].id})` : count(),
|
||||
count: count(),
|
||||
})
|
||||
.from(this.tables[tableName])
|
||||
.where(where),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-vercel-postgres",
|
||||
"version": "3.23.0",
|
||||
"version": "3.20.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.2",
|
||||
"esbuild": "0.24.0",
|
||||
"payload": "workspace:*"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/drizzle",
|
||||
"version": "3.23.0",
|
||||
"version": "3.20.0",
|
||||
"description": "A library of shared functions used by different payload database adapters",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -16,9 +16,7 @@ export async function createGlobal<T extends Record<string, unknown>>(
|
||||
|
||||
const tableName = this.tableNameMap.get(toSnakeCase(globalConfig.slug))
|
||||
|
||||
data.createdAt = new Date().toISOString()
|
||||
|
||||
const result = await upsertRow<{ globalType: string } & T>({
|
||||
const result = await upsertRow<T>({
|
||||
adapter: this,
|
||||
data,
|
||||
db,
|
||||
@@ -28,7 +26,5 @@ export async function createGlobal<T extends Record<string, unknown>>(
|
||||
tableName,
|
||||
})
|
||||
|
||||
result.globalType = slug
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { count, sql } from 'drizzle-orm'
|
||||
import { count } from 'drizzle-orm'
|
||||
|
||||
import type { ChainedMethods, TransactionPg } from '../types.js'
|
||||
import type { BasePostgresAdapter, CountDistinct } from './types.js'
|
||||
@@ -11,17 +11,7 @@ export const countDistinct: CountDistinct = async function countDistinct(
|
||||
) {
|
||||
const chainedMethods: ChainedMethods = []
|
||||
|
||||
// COUNT(DISTINCT id) is slow on large tables, so we only use DISTINCT if we have to
|
||||
const visitedPaths = new Set([])
|
||||
let useDistinct = false
|
||||
joins.forEach(({ condition, queryPath, table }) => {
|
||||
if (!useDistinct && queryPath) {
|
||||
if (visitedPaths.has(queryPath)) {
|
||||
useDistinct = true
|
||||
} else {
|
||||
visitedPaths.add(queryPath)
|
||||
}
|
||||
}
|
||||
joins.forEach(({ condition, table }) => {
|
||||
chainedMethods.push({
|
||||
args: [table, condition],
|
||||
method: 'leftJoin',
|
||||
@@ -32,7 +22,7 @@ export const countDistinct: CountDistinct = async function countDistinct(
|
||||
methods: chainedMethods,
|
||||
query: (db as TransactionPg)
|
||||
.select({
|
||||
count: useDistinct ? sql`COUNT(DISTINCT ${this.tables[tableName].id})` : count(),
|
||||
count: count(),
|
||||
})
|
||||
.from(this.tables[tableName])
|
||||
.where(where),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { type SQL } from 'drizzle-orm'
|
||||
import { type PgTableWithColumns } from 'drizzle-orm/pg-core'
|
||||
import type { SQL } from 'drizzle-orm'
|
||||
import type { PgTableWithColumns } from 'drizzle-orm/pg-core'
|
||||
|
||||
import type { GenericTable } from '../types.js'
|
||||
import type { BuildQueryJoinAliases } from './buildQuery.js'
|
||||
@@ -10,18 +10,16 @@ export const addJoinTable = ({
|
||||
type,
|
||||
condition,
|
||||
joins,
|
||||
queryPath,
|
||||
table,
|
||||
}: {
|
||||
condition: SQL
|
||||
joins: BuildQueryJoinAliases
|
||||
queryPath?: string
|
||||
table: GenericTable | PgTableWithColumns<any>
|
||||
type?: 'innerJoin' | 'leftJoin' | 'rightJoin'
|
||||
}) => {
|
||||
const name = getNameFromDrizzleTable(table)
|
||||
|
||||
if (!joins.some((eachJoin) => getNameFromDrizzleTable(eachJoin.table) === name)) {
|
||||
joins.push({ type, condition, queryPath, table })
|
||||
joins.push({ type, condition, table })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import { parseParams } from './parseParams.js'
|
||||
|
||||
export type BuildQueryJoinAliases = {
|
||||
condition: SQL
|
||||
queryPath?: string
|
||||
table: GenericTable | PgTableWithColumns<any>
|
||||
type?: 'innerJoin' | 'leftJoin' | 'rightJoin'
|
||||
}[]
|
||||
|
||||
@@ -363,10 +363,7 @@ export const getTableColumnFromPath = ({
|
||||
const {
|
||||
newAliasTable: aliasRelationshipTable,
|
||||
newAliasTableName: aliasRelationshipTableName,
|
||||
} = getTableAlias({
|
||||
adapter,
|
||||
tableName: relationTableName,
|
||||
})
|
||||
} = getTableAlias({ adapter, tableName: relationTableName })
|
||||
|
||||
if (selectLocale && field.localized && adapter.payload.config.localization) {
|
||||
selectFields._locale = aliasRelationshipTable.locale
|
||||
@@ -383,21 +380,17 @@ export const getTableColumnFromPath = ({
|
||||
conditions.push(eq(aliasRelationshipTable.locale, locale))
|
||||
}
|
||||
|
||||
addJoinTable({
|
||||
joins.push({
|
||||
condition: and(...conditions),
|
||||
joins,
|
||||
queryPath: `${constraintPath}.${field.name}`,
|
||||
table: aliasRelationshipTable,
|
||||
})
|
||||
} else {
|
||||
// Join in the relationships table
|
||||
addJoinTable({
|
||||
joins.push({
|
||||
condition: and(
|
||||
eq((aliasTable || adapter.tables[rootTableName]).id, aliasRelationshipTable.parent),
|
||||
like(aliasRelationshipTable.path, `${constraintPath}${field.name}`),
|
||||
),
|
||||
joins,
|
||||
queryPath: `${constraintPath}.${field.name}`,
|
||||
table: aliasRelationshipTable,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -87,11 +87,9 @@ export const sanitizeQueryValue = ({
|
||||
if (field.type === 'number') {
|
||||
formattedValue = formattedValue.map((arrayVal) => parseFloat(arrayVal))
|
||||
}
|
||||
} else if (typeof formattedValue === 'number') {
|
||||
formattedValue = [formattedValue]
|
||||
}
|
||||
|
||||
if (!Array.isArray(formattedValue)) {
|
||||
if (!Array.isArray(formattedValue) || formattedValue.length === 0) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,7 +97,6 @@ export const transformArray = ({
|
||||
data: arrayRow,
|
||||
fieldPrefix: '',
|
||||
fields: field.flattenedFields,
|
||||
insideArrayOrBlock: true,
|
||||
locales: newRow.locales,
|
||||
numbers,
|
||||
parentTableName: arrayTableName,
|
||||
|
||||
@@ -101,7 +101,6 @@ export const transformBlocks = ({
|
||||
data: blockRow,
|
||||
fieldPrefix: '',
|
||||
fields: matchedBlock.flattenedFields,
|
||||
insideArrayOrBlock: true,
|
||||
locales: newRow.locales,
|
||||
numbers,
|
||||
parentTableName: blockTableName,
|
||||
|
||||
@@ -42,10 +42,6 @@ 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>
|
||||
}
|
||||
@@ -81,7 +77,6 @@ export const traverseFields = ({
|
||||
fieldPrefix,
|
||||
fields,
|
||||
forcedLocale,
|
||||
insideArrayOrBlock = false,
|
||||
locales,
|
||||
numbers,
|
||||
parentTableName,
|
||||
@@ -93,10 +88,6 @@ export const traverseFields = ({
|
||||
texts,
|
||||
withinArrayOrBlockLocale,
|
||||
}: Args) => {
|
||||
if (row._uuid) {
|
||||
data._uuid = row._uuid
|
||||
}
|
||||
|
||||
fields.forEach((field) => {
|
||||
let columnName = ''
|
||||
let fieldName = ''
|
||||
@@ -235,7 +226,6 @@ export const traverseFields = ({
|
||||
fieldPrefix: `${fieldName}_`,
|
||||
fields: field.flattenedFields,
|
||||
forcedLocale: localeKey,
|
||||
insideArrayOrBlock,
|
||||
locales,
|
||||
numbers,
|
||||
parentTableName,
|
||||
@@ -264,7 +254,6 @@ export const traverseFields = ({
|
||||
existingLocales,
|
||||
fieldPrefix: `${fieldName}_`,
|
||||
fields: field.flattenedFields,
|
||||
insideArrayOrBlock,
|
||||
locales,
|
||||
numbers,
|
||||
parentTableName,
|
||||
@@ -427,7 +416,7 @@ export const traverseFields = ({
|
||||
Object.entries(data[field.name]).forEach(([localeKey, localeData]) => {
|
||||
if (Array.isArray(localeData)) {
|
||||
const newRows = transformSelects({
|
||||
id: insideArrayOrBlock ? data._uuid || data.id : undefined,
|
||||
id: data._uuid || data.id,
|
||||
data: localeData,
|
||||
locale: localeKey,
|
||||
})
|
||||
@@ -438,7 +427,7 @@ export const traverseFields = ({
|
||||
}
|
||||
} else if (Array.isArray(data[field.name])) {
|
||||
const newRows = transformSelects({
|
||||
id: insideArrayOrBlock ? data._uuid || data.id : undefined,
|
||||
id: data._uuid || data.id,
|
||||
data: data[field.name],
|
||||
locale: withinArrayOrBlockLocale,
|
||||
})
|
||||
@@ -479,9 +468,8 @@ 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)})`
|
||||
}
|
||||
@@ -491,16 +479,12 @@ 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<{ globalType: string } & T>({
|
||||
const result = await upsertRow<T>({
|
||||
...(existingGlobal ? { id: existingGlobal.id, operation: 'update' } : { operation: 'create' }),
|
||||
adapter: this,
|
||||
data,
|
||||
@@ -28,7 +28,5 @@ export async function updateGlobal<T extends Record<string, unknown>>(
|
||||
tableName,
|
||||
})
|
||||
|
||||
result.globalType = slug
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -20,10 +20,7 @@ export const upsertRow = async <T extends Record<string, unknown> | TypeWithID>(
|
||||
db,
|
||||
fields,
|
||||
ignoreResult,
|
||||
// TODO:
|
||||
// When we support joins for write operations (create/update) - pass collectionSlug to the buildFindManyArgs
|
||||
// Make a new argument in upsertRow.ts and pass the slug from every operation.
|
||||
joinQuery: _joinQuery,
|
||||
joinQuery,
|
||||
operation,
|
||||
path = '',
|
||||
req,
|
||||
@@ -266,9 +263,6 @@ export const upsertRow = async <T extends Record<string, unknown> | TypeWithID>(
|
||||
}
|
||||
}
|
||||
|
||||
// When versions are enabled, this is used to track mapping between blocks/arrays ObjectID to their numeric generated representation, then we use it for nested to arrays/blocks select hasMany in versions.
|
||||
const arraysBlocksUUIDMap: Record<string, number | string> = {}
|
||||
|
||||
for (const [blockName, blockRows] of Object.entries(blocksToInsert)) {
|
||||
const blockTableName = adapter.tableNameMap.get(`${tableName}_blocks_${blockName}`)
|
||||
insertedBlockRows[blockName] = await adapter.insert({
|
||||
@@ -279,12 +273,6 @@ export const upsertRow = async <T extends Record<string, unknown> | TypeWithID>(
|
||||
|
||||
insertedBlockRows[blockName].forEach((row, i) => {
|
||||
blockRows[i].row = row
|
||||
if (
|
||||
typeof row._uuid === 'string' &&
|
||||
(typeof row.id === 'string' || typeof row.id === 'number')
|
||||
) {
|
||||
arraysBlocksUUIDMap[row._uuid] = row.id
|
||||
}
|
||||
})
|
||||
|
||||
const blockLocaleIndexMap: number[] = []
|
||||
@@ -317,7 +305,6 @@ export const upsertRow = async <T extends Record<string, unknown> | TypeWithID>(
|
||||
arrays: blockRows.map(({ arrays }) => arrays),
|
||||
db,
|
||||
parentRows: insertedBlockRows[blockName],
|
||||
uuidMap: arraysBlocksUUIDMap,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -341,7 +328,6 @@ export const upsertRow = async <T extends Record<string, unknown> | TypeWithID>(
|
||||
arrays: [rowToInsert.arrays],
|
||||
db,
|
||||
parentRows: [insertedRow],
|
||||
uuidMap: arraysBlocksUUIDMap,
|
||||
})
|
||||
|
||||
// //////////////////////////////////
|
||||
@@ -358,14 +344,6 @@ export const upsertRow = async <T extends Record<string, unknown> | TypeWithID>(
|
||||
})
|
||||
}
|
||||
|
||||
if (Object.keys(arraysBlocksUUIDMap).length > 0) {
|
||||
tableRows.forEach((row: any) => {
|
||||
if (row.parent in arraysBlocksUUIDMap) {
|
||||
row.parent = arraysBlocksUUIDMap[row.parent]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (tableRows.length) {
|
||||
await adapter.insert({
|
||||
db,
|
||||
@@ -436,11 +414,13 @@ export const upsertRow = async <T extends Record<string, unknown> | TypeWithID>(
|
||||
// RETRIEVE NEWLY UPDATED ROW
|
||||
// //////////////////////////////////
|
||||
|
||||
joinQuery = operation === 'create' ? false : joinQuery
|
||||
|
||||
const findManyArgs = buildFindManyArgs({
|
||||
adapter,
|
||||
depth: 0,
|
||||
fields,
|
||||
joinQuery: false,
|
||||
joinQuery,
|
||||
select,
|
||||
tableName,
|
||||
})
|
||||
@@ -458,7 +438,7 @@ export const upsertRow = async <T extends Record<string, unknown> | TypeWithID>(
|
||||
config: adapter.payload.config,
|
||||
data: doc,
|
||||
fields,
|
||||
joinQuery: false,
|
||||
joinQuery,
|
||||
})
|
||||
|
||||
return result
|
||||
|
||||
@@ -8,7 +8,6 @@ type Args = {
|
||||
}[]
|
||||
db: DrizzleAdapter['drizzle'] | DrizzleTransaction
|
||||
parentRows: Record<string, unknown>[]
|
||||
uuidMap?: Record<string, number | string>
|
||||
}
|
||||
|
||||
type RowsByTable = {
|
||||
@@ -21,13 +20,7 @@ type RowsByTable = {
|
||||
}
|
||||
}
|
||||
|
||||
export const insertArrays = async ({
|
||||
adapter,
|
||||
arrays,
|
||||
db,
|
||||
parentRows,
|
||||
uuidMap = {},
|
||||
}: Args): Promise<void> => {
|
||||
export const insertArrays = async ({ adapter, arrays, db, parentRows }: Args): Promise<void> => {
|
||||
// Maintain a map of flattened rows by table
|
||||
const rowsByTable: RowsByTable = {}
|
||||
|
||||
@@ -81,15 +74,6 @@ export const insertArrays = async ({
|
||||
tableName,
|
||||
values: row.rows,
|
||||
})
|
||||
|
||||
insertedRows.forEach((row) => {
|
||||
if (
|
||||
typeof row._uuid === 'string' &&
|
||||
(typeof row.id === 'string' || typeof row.id === 'number')
|
||||
) {
|
||||
uuidMap[row._uuid] = row.id
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Insert locale rows
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/email-nodemailer",
|
||||
"version": "3.23.0",
|
||||
"version": "3.20.0",
|
||||
"description": "Payload Nodemailer Email Adapter",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/email-resend",
|
||||
"version": "3.23.0",
|
||||
"version": "3.20.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": "0.0.0-experimental-a4b2d0d5-20250203",
|
||||
"eslint-plugin-react-hooks": "5.0.0",
|
||||
"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": "0.0.0-experimental-a4b2d0d5-20250203",
|
||||
"eslint-plugin-react-hooks": "5.0.0",
|
||||
"eslint-plugin-regexp": "2.6.0",
|
||||
"globals": "15.12.0",
|
||||
"typescript": "5.7.3",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/graphql",
|
||||
"version": "3.23.0",
|
||||
"version": "3.20.0",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/live-preview-react",
|
||||
"version": "3.23.0",
|
||||
"version": "3.20.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.23.0",
|
||||
"version": "3.20.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.23.0",
|
||||
"version": "3.20.0",
|
||||
"description": "The official live preview JavaScript SDK for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/next",
|
||||
"version": "3.23.0",
|
||||
"version": "3.20.0",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -100,10 +100,10 @@
|
||||
"uuid": "10.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "7.26.4",
|
||||
"@babel/core": "7.26.7",
|
||||
"@babel/preset-env": "7.26.7",
|
||||
"@babel/preset-react": "7.26.3",
|
||||
"@babel/cli": "7.25.9",
|
||||
"@babel/core": "7.26.0",
|
||||
"@babel/preset-env": "7.26.0",
|
||||
"@babel/preset-react": "7.25.9",
|
||||
"@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-714736e-20250131",
|
||||
"esbuild": "0.24.2",
|
||||
"babel-plugin-react-compiler": "19.0.0-beta-df7b47d-20241124",
|
||||
"esbuild": "0.24.0",
|
||||
"esbuild-sass-plugin": "3.3.1",
|
||||
"eslint-plugin-react-compiler": "19.0.0-beta-714736e-20250131",
|
||||
"eslint-plugin-react-compiler": "19.0.0-beta-df7b47d-20241124",
|
||||
"payload": "workspace:*",
|
||||
"swc-plugin-transform-remove-imports": "3.1.0"
|
||||
"swc-plugin-transform-remove-imports": "2.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"graphql": "^16.8.1",
|
||||
|
||||
@@ -91,20 +91,6 @@ 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,7 +91,6 @@ export const ForgotPasswordForm: React.FC = () => {
|
||||
text(value, {
|
||||
name: 'username',
|
||||
type: 'text',
|
||||
blockData: {},
|
||||
data: {},
|
||||
event: 'onChange',
|
||||
preferences: { fields: {} },
|
||||
@@ -121,7 +120,6 @@ export const ForgotPasswordForm: React.FC = () => {
|
||||
email(value, {
|
||||
name: 'email',
|
||||
type: 'email',
|
||||
blockData: {},
|
||||
data: {},
|
||||
event: 'onChange',
|
||||
preferences: { fields: {} },
|
||||
|
||||
@@ -61,14 +61,6 @@ type Props = {
|
||||
readonly serverURL: string
|
||||
} & DocumentSlots
|
||||
|
||||
const getAbsoluteUrl = (url) => {
|
||||
try {
|
||||
return new URL(url, window.location.origin).href
|
||||
} catch {
|
||||
return url
|
||||
}
|
||||
}
|
||||
|
||||
const PreviewView: React.FC<Props> = ({
|
||||
collectionConfig,
|
||||
config,
|
||||
@@ -560,7 +552,7 @@ export const LivePreviewClient: React.FC<
|
||||
readonly url: string
|
||||
} & DocumentSlots
|
||||
> = (props) => {
|
||||
const { breakpoints, url: incomingUrl } = props
|
||||
const { breakpoints, url } = props
|
||||
const { collectionSlug, globalSlug } = useDocumentInfo()
|
||||
|
||||
const {
|
||||
@@ -572,11 +564,6 @@ export const LivePreviewClient: React.FC<
|
||||
getEntityConfig,
|
||||
} = useConfig()
|
||||
|
||||
const url =
|
||||
incomingUrl.startsWith('http://') || incomingUrl.startsWith('https://')
|
||||
? incomingUrl
|
||||
: getAbsoluteUrl(incomingUrl)
|
||||
|
||||
const { isPopupOpen, openPopupWindow, popupRef } = usePopupWindow({
|
||||
eventType: 'payload-live-preview',
|
||||
url,
|
||||
|
||||
@@ -32,12 +32,7 @@ export const RenderVersionFieldsToDiff = ({
|
||||
const LocaleComponents: React.ReactNode[] = []
|
||||
for (const [locale, baseField] of Object.entries(field.fieldByLocale)) {
|
||||
LocaleComponents.push(
|
||||
<div
|
||||
className={`${baseClass}__locale`}
|
||||
data-field-path={baseField.path}
|
||||
data-locale={locale}
|
||||
key={[locale, fieldIndex].join('-')}
|
||||
>
|
||||
<div className={`${baseClass}__locale`} key={[locale, fieldIndex].join('-')}>
|
||||
<div className={`${baseClass}__locale-value`}>{baseField.CustomComponent}</div>
|
||||
</div>,
|
||||
)
|
||||
|
||||
@@ -113,7 +113,7 @@ export const buildVersionFields = ({
|
||||
versionField.fieldByLocale = {}
|
||||
|
||||
for (const locale of selectedLocales) {
|
||||
const localizedVersionField = buildVersionField({
|
||||
versionField.fieldByLocale[locale] = buildVersionField({
|
||||
clientField: clientField as ClientField,
|
||||
clientSchemaMap,
|
||||
comparisonValue: comparisonValue?.[locale],
|
||||
@@ -133,12 +133,12 @@ export const buildVersionFields = ({
|
||||
selectedLocales,
|
||||
versionValue: versionValue?.[locale],
|
||||
})
|
||||
if (localizedVersionField) {
|
||||
versionField.fieldByLocale[locale] = localizedVersionField
|
||||
if (!versionField.fieldByLocale[locale]) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const baseVersionField = buildVersionField({
|
||||
versionField.field = buildVersionField({
|
||||
clientField: clientField as ClientField,
|
||||
clientSchemaMap,
|
||||
comparisonValue,
|
||||
@@ -158,8 +158,8 @@ export const buildVersionFields = ({
|
||||
versionValue,
|
||||
})
|
||||
|
||||
if (baseVersionField) {
|
||||
versionField.field = baseVersionField
|
||||
if (!versionField.field) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
@@ -282,13 +282,15 @@ const buildVersionField = ({
|
||||
}
|
||||
} // At this point, we are dealing with a `row`, etc
|
||||
else if ('fields' in field) {
|
||||
if (field.type === 'array' && versionValue) {
|
||||
const arrayValue = Array.isArray(versionValue) ? versionValue : []
|
||||
if (field.type === 'array') {
|
||||
if (!Array.isArray(versionValue)) {
|
||||
throw new Error('Expected versionValue to be an array')
|
||||
}
|
||||
baseVersionField.rows = []
|
||||
|
||||
for (let i = 0; i < arrayValue.length; i++) {
|
||||
for (let i = 0; i < versionValue.length; i++) {
|
||||
const comparisonRow = comparisonValue?.[i] || {}
|
||||
const versionRow = arrayValue?.[i] || {}
|
||||
const versionRow = versionValue?.[i] || {}
|
||||
baseVersionField.rows[i] = buildVersionFields({
|
||||
clientSchemaMap,
|
||||
comparisonSiblingData: comparisonRow,
|
||||
@@ -327,11 +329,13 @@ const buildVersionField = ({
|
||||
} else if (field.type === 'blocks') {
|
||||
baseVersionField.rows = []
|
||||
|
||||
const blocksValue = Array.isArray(versionValue) ? versionValue : []
|
||||
if (!Array.isArray(versionValue)) {
|
||||
throw new Error('Expected versionValue to be an array')
|
||||
}
|
||||
|
||||
for (let i = 0; i < blocksValue.length; i++) {
|
||||
for (let i = 0; i < versionValue.length; i++) {
|
||||
const comparisonRow = comparisonValue?.[i] || {}
|
||||
const versionRow = blocksValue[i] || {}
|
||||
const versionRow = versionValue[i] || {}
|
||||
const versionBlock = field.blocks.find((block) => block.slug === versionRow.blockType)
|
||||
|
||||
let fields = []
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/payload-cloud",
|
||||
"version": "3.23.0",
|
||||
"version": "3.20.0",
|
||||
"description": "The official Payload Cloud plugin",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -69,7 +69,6 @@ export const payloadCloudPlugin =
|
||||
getStaticHandler({
|
||||
cachingOptions: pluginOptions?.uploadCaching,
|
||||
collection,
|
||||
debug: pluginOptions?.debug,
|
||||
}),
|
||||
],
|
||||
},
|
||||
@@ -106,9 +105,8 @@ export const payloadCloudPlugin =
|
||||
const DEFAULT_CRON_JOB = {
|
||||
cron: DEFAULT_CRON,
|
||||
limit: DEFAULT_LIMIT,
|
||||
queue: 'default',
|
||||
queue: 'default (every minute)',
|
||||
}
|
||||
|
||||
config.globals = [
|
||||
...(config.globals || []),
|
||||
{
|
||||
@@ -132,33 +130,9 @@ export const payloadCloudPlugin =
|
||||
|
||||
const oldAutoRunCopy = config.jobs.autoRun ?? []
|
||||
|
||||
const hasExistingAutorun = Boolean(config.jobs.autoRun)
|
||||
|
||||
const newShouldAutoRun = async (payload: Payload) => {
|
||||
if (process.env.PAYLOAD_CLOUD_JOBS_INSTANCE) {
|
||||
const retrievedGlobal = await payload.findGlobal({
|
||||
slug: 'payload-cloud-instance',
|
||||
})
|
||||
|
||||
if (retrievedGlobal.instance === process.env.PAYLOAD_CLOUD_JOBS_INSTANCE) {
|
||||
return true
|
||||
} else {
|
||||
process.env.PAYLOAD_CLOUD_JOBS_INSTANCE = ''
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
if (!config.jobs.shouldAutoRun) {
|
||||
config.jobs.shouldAutoRun = newShouldAutoRun
|
||||
}
|
||||
|
||||
const newAutoRun = async (payload: Payload) => {
|
||||
const instance = generateRandomString()
|
||||
|
||||
process.env.PAYLOAD_CLOUD_JOBS_INSTANCE = instance
|
||||
|
||||
await payload.updateGlobal({
|
||||
slug: 'payload-cloud-instance',
|
||||
data: {
|
||||
@@ -166,7 +140,16 @@ export const payloadCloudPlugin =
|
||||
},
|
||||
})
|
||||
|
||||
if (!hasExistingAutorun) {
|
||||
await waitRandomTime()
|
||||
|
||||
const cloudInstance = await payload.findGlobal({
|
||||
slug: 'payload-cloud-instance',
|
||||
})
|
||||
|
||||
if (cloudInstance.instance !== instance) {
|
||||
return []
|
||||
}
|
||||
if (!config.jobs?.autoRun) {
|
||||
return [DEFAULT_CRON_JOB]
|
||||
}
|
||||
|
||||
@@ -177,3 +160,11 @@ export const payloadCloudPlugin =
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
function waitRandomTime(): Promise<void> {
|
||||
const min = 1000 // 1 second in milliseconds
|
||||
const max = 5000 // 5 seconds in milliseconds
|
||||
const randomTime = Math.floor(Math.random() * (max - min + 1)) + min
|
||||
|
||||
return new Promise((resolve) => setTimeout(resolve, randomTime))
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { CollectionConfig } from 'payload'
|
||||
import type { Readable } from 'stream'
|
||||
|
||||
import type { CollectionCachingConfig, PluginOptions, StaticHandler } from './types.js'
|
||||
|
||||
@@ -9,19 +8,6 @@ import { getStorageClient } from './utilities/getStorageClient.js'
|
||||
interface Args {
|
||||
cachingOptions?: PluginOptions['uploadCaching']
|
||||
collection: CollectionConfig
|
||||
debug?: boolean
|
||||
}
|
||||
|
||||
// Type guard for NodeJS.Readable streams
|
||||
const isNodeReadableStream = (body: unknown): body is Readable => {
|
||||
return (
|
||||
typeof body === 'object' &&
|
||||
body !== null &&
|
||||
'pipe' in body &&
|
||||
typeof (body as any).pipe === 'function' &&
|
||||
'destroy' in body &&
|
||||
typeof (body as any).destroy === 'function'
|
||||
)
|
||||
}
|
||||
|
||||
// Convert a stream into a promise that resolves with a Buffer
|
||||
@@ -33,7 +19,7 @@ const streamToBuffer = async (readableStream: any) => {
|
||||
return Buffer.concat(chunks)
|
||||
}
|
||||
|
||||
export const getStaticHandler = ({ cachingOptions, collection, debug }: Args): StaticHandler => {
|
||||
export const getStaticHandler = ({ cachingOptions, collection }: Args): StaticHandler => {
|
||||
let maxAge = 86400 // 24 hours default
|
||||
let collCacheConfig: CollectionCachingConfig | undefined
|
||||
if (cachingOptions !== false) {
|
||||
@@ -70,19 +56,6 @@ export const getStaticHandler = ({ cachingOptions, collection, debug }: Args): S
|
||||
return new Response(null, { status: 404, statusText: 'Not Found' })
|
||||
}
|
||||
|
||||
// On error, manually destroy stream to close socket
|
||||
if (object.Body && isNodeReadableStream(object.Body)) {
|
||||
const stream = object.Body
|
||||
stream.on('error', (err) => {
|
||||
req.payload.logger.error({
|
||||
err,
|
||||
key,
|
||||
msg: 'Error streaming S3 object, destroying stream',
|
||||
})
|
||||
stream.destroy()
|
||||
})
|
||||
}
|
||||
|
||||
const bodyBuffer = await streamToBuffer(object.Body)
|
||||
|
||||
return new Response(bodyBuffer, {
|
||||
@@ -95,32 +68,25 @@ export const getStaticHandler = ({ cachingOptions, collection, debug }: Args): S
|
||||
status: 200,
|
||||
})
|
||||
} catch (err: unknown) {
|
||||
// Handle each error explicitly
|
||||
if (err instanceof Error) {
|
||||
/**
|
||||
* Note: If AccessDenied comes back, it typically means that the object key is not found.
|
||||
* The AWS SDK throws this because it attempts an s3:ListBucket operation under the hood
|
||||
* if it does not find the object key, which we have disallowed in our bucket policy.
|
||||
*/
|
||||
if (err.name === 'AccessDenied') {
|
||||
req.payload.logger.warn({
|
||||
awsErr: debug ? err : err.name,
|
||||
collectionSlug: collection.slug,
|
||||
msg: `Requested file not found in cloud storage: ${params.filename}`,
|
||||
params,
|
||||
requestedKey: key,
|
||||
})
|
||||
return new Response(null, { status: 404, statusText: 'Not Found' })
|
||||
} else if (err.name === 'NoSuchKey') {
|
||||
req.payload.logger.warn({
|
||||
awsErr: debug ? err : err.name,
|
||||
collectionSlug: collection.slug,
|
||||
msg: `Requested file not found in cloud storage: ${params.filename}`,
|
||||
params,
|
||||
requestedKey: key,
|
||||
})
|
||||
return new Response(null, { status: 404, statusText: 'Not Found' })
|
||||
}
|
||||
/**
|
||||
* If object key does not found, the getObject function attempts a ListBucket operation.
|
||||
* Because of permissions, this will throw very specific error that we can catch and handle.
|
||||
*/
|
||||
if (
|
||||
err instanceof Error &&
|
||||
err.name === 'AccessDenied' &&
|
||||
err.message?.includes('s3:ListBucket') &&
|
||||
'type' in err &&
|
||||
err.type === 'S3ServiceException'
|
||||
) {
|
||||
req.payload.logger.error({
|
||||
collectionSlug: collection.slug,
|
||||
err,
|
||||
msg: `Requested file not found in cloud storage: ${params.filename}`,
|
||||
params,
|
||||
requestedKey: key,
|
||||
})
|
||||
return new Response(null, { status: 404, statusText: 'Not Found' })
|
||||
}
|
||||
|
||||
req.payload.logger.error({
|
||||
|
||||
@@ -54,13 +54,6 @@ export interface PayloadCloudEmailOptions {
|
||||
}
|
||||
|
||||
export interface PluginOptions {
|
||||
/**
|
||||
* Enable additional debug logging
|
||||
*
|
||||
* @default false
|
||||
*/
|
||||
debug?: boolean
|
||||
|
||||
/** Payload Cloud Email
|
||||
* @default true
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "payload",
|
||||
"version": "3.23.0",
|
||||
"version": "3.20.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.2",
|
||||
"esbuild": "0.24.0",
|
||||
"graphql-http": "^1.22.0",
|
||||
"react-datepicker": "7.6.0",
|
||||
"rimraf": "6.0.1",
|
||||
|
||||
@@ -68,16 +68,9 @@ 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,7 +34,6 @@ export const generatePasswordSaltHash = async ({
|
||||
const validationResult = password(passwordToSet, {
|
||||
name: 'password',
|
||||
type: 'text',
|
||||
blockData: {},
|
||||
data: {},
|
||||
event: 'submit',
|
||||
preferences: { fields: {} },
|
||||
|
||||
@@ -97,7 +97,6 @@ export const createClientConfig = ({
|
||||
meta: config.admin.meta,
|
||||
routes: config.admin.routes,
|
||||
theme: config.admin.theme,
|
||||
timezones: config.admin.timezones,
|
||||
user: config.admin.user,
|
||||
}
|
||||
if (config.admin.livePreview) {
|
||||
|
||||
@@ -9,7 +9,6 @@ import type {
|
||||
LocalizationConfigWithLabels,
|
||||
LocalizationConfigWithNoLabels,
|
||||
SanitizedConfig,
|
||||
Timezone,
|
||||
} from './types.js'
|
||||
|
||||
import { defaultUserCollection } from '../auth/defaultUser.js'
|
||||
@@ -17,7 +16,6 @@ import { authRootEndpoints } from '../auth/endpoints/index.js'
|
||||
import { sanitizeCollection } from '../collections/config/sanitize.js'
|
||||
import { migrationsCollection } from '../database/migrations/migrationsCollection.js'
|
||||
import { DuplicateCollection, InvalidConfiguration } from '../errors/index.js'
|
||||
import { defaultTimezones } from '../fields/baseFields/timezone/defaultTimezones.js'
|
||||
import { sanitizeGlobal } from '../globals/config/sanitize.js'
|
||||
import { getLockedDocumentsCollection } from '../lockedDocuments/lockedDocumentsCollection.js'
|
||||
import getPreferencesCollection from '../preferences/preferencesCollection.js'
|
||||
@@ -58,32 +56,6 @@ const sanitizeAdminConfig = (configToSanitize: Config): Partial<SanitizedConfig>
|
||||
)
|
||||
}
|
||||
|
||||
if (sanitizedConfig?.admin?.timezones) {
|
||||
if (typeof sanitizedConfig?.admin?.timezones?.supportedTimezones === 'function') {
|
||||
sanitizedConfig.admin.timezones.supportedTimezones =
|
||||
sanitizedConfig.admin.timezones.supportedTimezones({ defaultTimezones })
|
||||
}
|
||||
|
||||
if (!sanitizedConfig?.admin?.timezones?.supportedTimezones) {
|
||||
sanitizedConfig.admin.timezones.supportedTimezones = defaultTimezones
|
||||
}
|
||||
} else {
|
||||
sanitizedConfig.admin.timezones = {
|
||||
supportedTimezones: defaultTimezones,
|
||||
}
|
||||
}
|
||||
// Timezones supported by the Intl API
|
||||
const _internalSupportedTimezones = Intl.supportedValuesOf('timeZone')
|
||||
|
||||
// We're casting here because it's already been sanitised above but TS still thinks it could be a function
|
||||
;(sanitizedConfig.admin.timezones.supportedTimezones as Timezone[]).forEach((timezone) => {
|
||||
if (!_internalSupportedTimezones.includes(timezone.value)) {
|
||||
throw new InvalidConfiguration(
|
||||
`Timezone ${timezone.value} is not supported by the current runtime via the Intl API.`,
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
return sanitizedConfig as unknown as Partial<SanitizedConfig>
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type {
|
||||
DefaultTranslationKeys,
|
||||
DefaultTranslationsObject,
|
||||
I18n,
|
||||
I18nClient,
|
||||
I18nOptions,
|
||||
TFunction,
|
||||
@@ -426,32 +425,6 @@ export const serverProps: (keyof ServerProps)[] = [
|
||||
'permissions',
|
||||
]
|
||||
|
||||
export type Timezone = {
|
||||
label: string
|
||||
value: string
|
||||
}
|
||||
|
||||
type SupportedTimezonesFn = (args: { defaultTimezones: Timezone[] }) => Timezone[]
|
||||
|
||||
type TimezonesConfig = {
|
||||
/**
|
||||
* The default timezone to use for the admin panel.
|
||||
*/
|
||||
defaultTimezone?: string
|
||||
/**
|
||||
* Provide your own list of supported timezones for the admin panel
|
||||
*
|
||||
* Values should be IANA timezone names, eg. `America/New_York`
|
||||
*
|
||||
* We use `@date-fns/tz` to handle timezones
|
||||
*/
|
||||
supportedTimezones?: SupportedTimezonesFn | Timezone[]
|
||||
}
|
||||
|
||||
type SanitizedTimezoneConfig = {
|
||||
supportedTimezones: Timezone[]
|
||||
} & Omit<TimezonesConfig, 'supportedTimezones'>
|
||||
|
||||
export type CustomComponent<TAdditionalProps extends object = Record<string, any>> =
|
||||
PayloadComponent<ServerProps & TAdditionalProps, TAdditionalProps>
|
||||
|
||||
@@ -496,14 +469,6 @@ 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<
|
||||
@@ -906,10 +871,6 @@ export type Config = {
|
||||
* @default 'all' // The theme can be configured by users
|
||||
*/
|
||||
theme?: 'all' | 'dark' | 'light'
|
||||
/**
|
||||
* Configure timezone related settings for the admin panel.
|
||||
*/
|
||||
timezones?: TimezonesConfig
|
||||
/** The slug of a Collection that you want to be used to log in to the Admin dashboard. */
|
||||
user?: string
|
||||
}
|
||||
@@ -1161,16 +1122,7 @@ 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: {
|
||||
collectionIDFieldTypes: {
|
||||
[key: string]: 'number' | 'string'
|
||||
}
|
||||
config: SanitizedConfig
|
||||
i18n: I18n
|
||||
jsonSchema: JSONSchema4
|
||||
}) => JSONSchema4
|
||||
>
|
||||
schema?: Array<(args: { jsonSchema: JSONSchema4 }) => JSONSchema4>
|
||||
}
|
||||
/**
|
||||
* Customize the handling of incoming file uploads for collections that have uploads enabled.
|
||||
@@ -1179,9 +1131,6 @@ export type Config = {
|
||||
}
|
||||
|
||||
export type SanitizedConfig = {
|
||||
admin: {
|
||||
timezones: SanitizedTimezoneConfig
|
||||
} & DeepRequired<Config['admin']>
|
||||
collections: SanitizedCollectionConfig[]
|
||||
/** Default richtext editor to use for richText fields */
|
||||
editor?: RichTextAdapter<any, any, any>
|
||||
@@ -1206,7 +1155,7 @@ export type SanitizedConfig = {
|
||||
// E.g. in packages/ui/src/graphics/Account/index.tsx in getComponent, if avatar.Component is casted to what it's supposed to be,
|
||||
// the result type is different
|
||||
DeepRequired<Config>,
|
||||
'admin' | 'collections' | 'editor' | 'endpoint' | 'globals' | 'i18n' | 'localization' | 'upload'
|
||||
'collections' | 'editor' | 'endpoint' | 'globals' | 'i18n' | 'localization' | 'upload'
|
||||
>
|
||||
|
||||
export type EditConfig = EditConfigWithoutRoot | EditConfigWithRoot
|
||||
|
||||
@@ -12,8 +12,6 @@ export { defaults as collectionDefaults } from '../collections/config/defaults.j
|
||||
|
||||
export { serverProps } from '../config/types.js'
|
||||
|
||||
export { defaultTimezones } from '../fields/baseFields/timezone/defaultTimezones.js'
|
||||
|
||||
export {
|
||||
fieldAffectsData,
|
||||
fieldHasMaxDepth,
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
import type { SelectField } from '../../config/types.js'
|
||||
|
||||
export const baseTimezoneField: (args: Partial<SelectField>) => SelectField = ({
|
||||
name,
|
||||
defaultValue,
|
||||
options,
|
||||
required,
|
||||
}) => {
|
||||
return {
|
||||
name,
|
||||
type: 'select',
|
||||
admin: {
|
||||
hidden: true,
|
||||
},
|
||||
defaultValue,
|
||||
options,
|
||||
required,
|
||||
}
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
import type { Timezone } from '../../../config/types.js'
|
||||
|
||||
/**
|
||||
* List of supported timezones
|
||||
*
|
||||
* label: UTC offset and location
|
||||
* value: IANA timezone name
|
||||
*
|
||||
* @example
|
||||
* { label: '(UTC-12:00) International Date Line West', value: 'Dateline Standard Time' }
|
||||
*/
|
||||
export const defaultTimezones: Timezone[] = [
|
||||
{ label: '(UTC-11:00) Midway Island, Samoa', value: 'Pacific/Midway' },
|
||||
{ label: '(UTC-11:00) Niue', value: 'Pacific/Niue' },
|
||||
{ label: '(UTC-10:00) Hawaii', value: 'Pacific/Honolulu' },
|
||||
{ label: '(UTC-10:00) Cook Islands', value: 'Pacific/Rarotonga' },
|
||||
{ label: '(UTC-09:00) Alaska', value: 'America/Anchorage' },
|
||||
{ label: '(UTC-09:00) Gambier Islands', value: 'Pacific/Gambier' },
|
||||
{ label: '(UTC-08:00) Pacific Time (US & Canada)', value: 'America/Los_Angeles' },
|
||||
{ label: '(UTC-08:00) Tijuana, Baja California', value: 'America/Tijuana' },
|
||||
{ label: '(UTC-07:00) Mountain Time (US & Canada)', value: 'America/Denver' },
|
||||
{ label: '(UTC-07:00) Arizona (No DST)', value: 'America/Phoenix' },
|
||||
{ label: '(UTC-06:00) Central Time (US & Canada)', value: 'America/Chicago' },
|
||||
{ label: '(UTC-06:00) Central America', value: 'America/Guatemala' },
|
||||
{ label: '(UTC-05:00) Eastern Time (US & Canada)', value: 'America/New_York' },
|
||||
{ label: '(UTC-05:00) Bogota, Lima, Quito', value: 'America/Bogota' },
|
||||
{ label: '(UTC-04:00) Caracas', value: 'America/Caracas' },
|
||||
{ label: '(UTC-04:00) Santiago', value: 'America/Santiago' },
|
||||
{ label: '(UTC-03:00) Buenos Aires', value: 'America/Buenos_Aires' },
|
||||
{ label: '(UTC-03:00) Brasilia', value: 'America/Sao_Paulo' },
|
||||
{ label: '(UTC-02:00) South Georgia', value: 'Atlantic/South_Georgia' },
|
||||
{ label: '(UTC-01:00) Azores', value: 'Atlantic/Azores' },
|
||||
{ label: '(UTC-01:00) Cape Verde', value: 'Atlantic/Cape_Verde' },
|
||||
{ label: '(UTC+00:00) London (GMT)', value: 'Europe/London' },
|
||||
{ label: '(UTC+01:00) Berlin, Paris', value: 'Europe/Berlin' },
|
||||
{ label: '(UTC+01:00) Lagos', value: 'Africa/Lagos' },
|
||||
{ label: '(UTC+02:00) Athens, Bucharest', value: 'Europe/Athens' },
|
||||
{ label: '(UTC+02:00) Cairo', value: 'Africa/Cairo' },
|
||||
{ label: '(UTC+03:00) Moscow, St. Petersburg', value: 'Europe/Moscow' },
|
||||
{ label: '(UTC+03:00) Riyadh', value: 'Asia/Riyadh' },
|
||||
{ label: '(UTC+04:00) Dubai', value: 'Asia/Dubai' },
|
||||
{ label: '(UTC+04:00) Baku', value: 'Asia/Baku' },
|
||||
{ label: '(UTC+05:00) Islamabad, Karachi', value: 'Asia/Karachi' },
|
||||
{ label: '(UTC+05:00) Tashkent', value: 'Asia/Tashkent' },
|
||||
{ label: '(UTC+05:30) Chennai, Kolkata, Mumbai, New Delhi', value: 'Asia/Calcutta' },
|
||||
{ label: '(UTC+06:00) Dhaka', value: 'Asia/Dhaka' },
|
||||
{ label: '(UTC+06:00) Almaty', value: 'Asia/Almaty' },
|
||||
{ label: '(UTC+07:00) Jakarta', value: 'Asia/Jakarta' },
|
||||
{ label: '(UTC+07:00) Bangkok', value: 'Asia/Bangkok' },
|
||||
{ label: '(UTC+08:00) Beijing, Shanghai', value: 'Asia/Shanghai' },
|
||||
{ label: '(UTC+08:00) Singapore', value: 'Asia/Singapore' },
|
||||
{ label: '(UTC+09:00) Tokyo, Osaka, Sapporo', value: 'Asia/Tokyo' },
|
||||
{ label: '(UTC+09:00) Seoul', value: 'Asia/Seoul' },
|
||||
{ label: '(UTC+10:00) Sydney, Melbourne', value: 'Australia/Sydney' },
|
||||
{ label: '(UTC+10:00) Guam, Port Moresby', value: 'Pacific/Guam' },
|
||||
{ label: '(UTC+11:00) New Caledonia', value: 'Pacific/Noumea' },
|
||||
{ label: '(UTC+12:00) Auckland, Wellington', value: 'Pacific/Auckland' },
|
||||
{ label: '(UTC+12:00) Fiji', value: 'Pacific/Fiji' },
|
||||
]
|
||||
@@ -1,10 +1,7 @@
|
||||
/* eslint-disable perfectionist/sort-switch-case */
|
||||
// Keep perfectionist/sort-switch-case disabled - it incorrectly messes up the ordering of the switch cases, causing it to break
|
||||
import type { I18nClient } from '@payloadcms/translations'
|
||||
|
||||
import type {
|
||||
AdminClient,
|
||||
ArrayFieldClient,
|
||||
BlockJSX,
|
||||
BlocksFieldClient,
|
||||
ClientBlock,
|
||||
@@ -147,27 +144,7 @@ export const createClientField = ({
|
||||
}
|
||||
|
||||
switch (incomingField.type) {
|
||||
case 'array': {
|
||||
if (incomingField.labels) {
|
||||
const field = clientField as unknown as ArrayFieldClient
|
||||
|
||||
field.labels = {} as unknown as LabelsClient
|
||||
|
||||
if (incomingField.labels.singular) {
|
||||
if (typeof incomingField.labels.singular === 'function') {
|
||||
field.labels.singular = incomingField.labels.singular({ t: i18n.t })
|
||||
} else {
|
||||
field.labels.singular = incomingField.labels.singular
|
||||
}
|
||||
if (typeof incomingField.labels.plural === 'function') {
|
||||
field.labels.plural = incomingField.labels.plural({ t: i18n.t })
|
||||
} else {
|
||||
field.labels.plural = incomingField.labels.plural
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// falls through
|
||||
case 'array':
|
||||
case 'collapsible':
|
||||
case 'group':
|
||||
case 'row': {
|
||||
@@ -191,23 +168,6 @@ export const createClientField = ({
|
||||
case 'blocks': {
|
||||
const field = clientField as unknown as BlocksFieldClient
|
||||
|
||||
if (incomingField.labels) {
|
||||
field.labels = {} as unknown as LabelsClient
|
||||
|
||||
if (incomingField.labels.singular) {
|
||||
if (typeof incomingField.labels.singular === 'function') {
|
||||
field.labels.singular = incomingField.labels.singular({ t: i18n.t })
|
||||
} else {
|
||||
field.labels.singular = incomingField.labels.singular
|
||||
}
|
||||
if (typeof incomingField.labels.plural === 'function') {
|
||||
field.labels.plural = incomingField.labels.plural({ t: i18n.t })
|
||||
} else {
|
||||
field.labels.plural = incomingField.labels.plural
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (incomingField.blocks?.length) {
|
||||
for (let i = 0; i < incomingField.blocks.length; i++) {
|
||||
const block = incomingField.blocks[i]
|
||||
|
||||
@@ -14,8 +14,6 @@ import {
|
||||
import { formatLabels, toWords } from '../../utilities/formatLabels.js'
|
||||
import { baseBlockFields } from '../baseFields/baseBlockFields.js'
|
||||
import { baseIDField } from '../baseFields/baseIDField.js'
|
||||
import { baseTimezoneField } from '../baseFields/timezone/baseField.js'
|
||||
import { defaultTimezones } from '../baseFields/timezone/defaultTimezones.js'
|
||||
import { setDefaultBeforeDuplicate } from '../setDefaultBeforeDuplicate.js'
|
||||
import { validations } from '../validations.js'
|
||||
import { sanitizeJoinField } from './sanitizeJoinField.js'
|
||||
@@ -289,30 +287,6 @@ export const sanitizeFields = async ({
|
||||
}
|
||||
|
||||
fields[i] = field
|
||||
|
||||
// Insert our field after assignment
|
||||
if (field.type === 'date' && field.timezone) {
|
||||
const name = field.name + '_tz'
|
||||
const defaultTimezone = config.admin.timezones.defaultTimezone
|
||||
|
||||
const supportedTimezones = config.admin.timezones.supportedTimezones
|
||||
|
||||
const options =
|
||||
typeof supportedTimezones === 'function'
|
||||
? supportedTimezones({ defaultTimezones })
|
||||
: supportedTimezones
|
||||
|
||||
// Need to set the options here manually so that any database enums are generated correctly
|
||||
// The UI component will import the options from the config
|
||||
const timezoneField = baseTimezoneField({
|
||||
name,
|
||||
defaultValue: defaultTimezone,
|
||||
options,
|
||||
required: field.required,
|
||||
})
|
||||
|
||||
fields.splice(++i, 0, timezoneField)
|
||||
}
|
||||
}
|
||||
|
||||
return fields
|
||||
|
||||
@@ -133,13 +133,7 @@ import type {
|
||||
TextareaFieldValidation,
|
||||
} from '../../index.js'
|
||||
import type { DocumentPreferences } from '../../preferences/types.js'
|
||||
import type {
|
||||
DefaultValue,
|
||||
JsonObject,
|
||||
Operation,
|
||||
PayloadRequest,
|
||||
Where,
|
||||
} from '../../types/index.js'
|
||||
import type { DefaultValue, Operation, PayloadRequest, Where } from '../../types/index.js'
|
||||
import type {
|
||||
NumberFieldManyValidation,
|
||||
NumberFieldSingleValidation,
|
||||
@@ -154,10 +148,6 @@ 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
|
||||
@@ -222,11 +212,7 @@ export type FieldHook<TData extends TypeWithID = any, TValue = any, TSiblingData
|
||||
|
||||
export type FieldAccess<TData extends TypeWithID = any, TSiblingData = any> = (args: {
|
||||
/**
|
||||
* 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.
|
||||
* The incoming data used to `create` or `update` the document with. `data` is undefined during the `read` operation.
|
||||
*/
|
||||
data?: Partial<TData>
|
||||
/**
|
||||
@@ -245,33 +231,13 @@ 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>,
|
||||
{
|
||||
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']
|
||||
},
|
||||
{ user }: { 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.
|
||||
*/
|
||||
@@ -382,11 +348,6 @@ 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'
|
||||
@@ -671,10 +632,6 @@ export type DateField = {
|
||||
date?: ConditionalDateProps
|
||||
placeholder?: Record<string, string> | string
|
||||
} & Admin
|
||||
/**
|
||||
* Enable timezone selection in the admin interface.
|
||||
*/
|
||||
timezone?: true
|
||||
type: 'date'
|
||||
validate?: DateFieldValidation
|
||||
} & Omit<FieldBase, 'validate'>
|
||||
@@ -682,7 +639,7 @@ export type DateField = {
|
||||
export type DateFieldClient = {
|
||||
admin?: AdminClient & Pick<DateField['admin'], 'date' | 'placeholder'>
|
||||
} & FieldBaseClient &
|
||||
Pick<DateField, 'timezone' | 'type'>
|
||||
Pick<DateField, 'type'>
|
||||
|
||||
export type GroupField = {
|
||||
admin?: {
|
||||
|
||||
@@ -11,10 +11,6 @@ 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
|
||||
@@ -37,7 +33,6 @@ type Args = {
|
||||
// - Execute field hooks
|
||||
|
||||
export const promise = async ({
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
@@ -74,7 +69,6 @@ export const promise = async ({
|
||||
await priorHook
|
||||
|
||||
const hookedValue = await currentHook({
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
@@ -110,7 +104,6 @@ export const promise = async ({
|
||||
rows.forEach((row, rowIndex) => {
|
||||
promises.push(
|
||||
traverseFields({
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
@@ -149,7 +142,6 @@ export const promise = async ({
|
||||
if (block) {
|
||||
promises.push(
|
||||
traverseFields({
|
||||
blockData: siblingData?.[field.name]?.[rowIndex],
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
@@ -179,7 +171,6 @@ export const promise = async ({
|
||||
case 'collapsible':
|
||||
case 'row': {
|
||||
await traverseFields({
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
@@ -202,7 +193,6 @@ export const promise = async ({
|
||||
|
||||
case 'group': {
|
||||
await traverseFields({
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
@@ -279,7 +269,6 @@ export const promise = async ({
|
||||
}
|
||||
|
||||
await traverseFields({
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
@@ -302,7 +291,6 @@ export const promise = async ({
|
||||
|
||||
case 'tabs': {
|
||||
await traverseFields({
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
|
||||
@@ -7,10 +7,6 @@ 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
|
||||
@@ -29,7 +25,6 @@ type Args = {
|
||||
}
|
||||
|
||||
export const traverseFields = async ({
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
@@ -51,7 +46,6 @@ export const traverseFields = async ({
|
||||
fields.forEach((field, fieldIndex) => {
|
||||
promises.push(
|
||||
promise({
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
|
||||
@@ -19,10 +19,6 @@ 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
|
||||
@@ -64,7 +60,6 @@ type Args = {
|
||||
// - Populate relationships
|
||||
|
||||
export const promise = async ({
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
currentDepth,
|
||||
@@ -241,7 +236,6 @@ export const promise = async ({
|
||||
const hookPromises = Object.entries(siblingDoc[field.name]).map(([locale, value]) =>
|
||||
(async () => {
|
||||
const hookedValue = await currentHook({
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
currentDepth,
|
||||
@@ -272,7 +266,6 @@ export const promise = async ({
|
||||
await Promise.all(hookPromises)
|
||||
} else {
|
||||
const hookedValue = await currentHook({
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
currentDepth,
|
||||
@@ -308,7 +301,6 @@ export const promise = async ({
|
||||
? true
|
||||
: await field.access.read({
|
||||
id: doc.id as number | string,
|
||||
blockData,
|
||||
data: doc,
|
||||
doc,
|
||||
req,
|
||||
@@ -372,7 +364,6 @@ export const promise = async ({
|
||||
if (Array.isArray(rows)) {
|
||||
rows.forEach((row, rowIndex) => {
|
||||
traverseFields({
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
currentDepth,
|
||||
@@ -406,7 +397,6 @@ export const promise = async ({
|
||||
if (Array.isArray(localeRows)) {
|
||||
localeRows.forEach((row, rowIndex) => {
|
||||
traverseFields({
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
currentDepth,
|
||||
@@ -486,7 +476,6 @@ export const promise = async ({
|
||||
|
||||
if (block) {
|
||||
traverseFields({
|
||||
blockData: row,
|
||||
collection,
|
||||
context,
|
||||
currentDepth,
|
||||
@@ -526,7 +515,6 @@ export const promise = async ({
|
||||
|
||||
if (block) {
|
||||
traverseFields({
|
||||
blockData: row,
|
||||
collection,
|
||||
context,
|
||||
currentDepth,
|
||||
@@ -566,7 +554,6 @@ export const promise = async ({
|
||||
case 'collapsible':
|
||||
case 'row': {
|
||||
traverseFields({
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
currentDepth,
|
||||
@@ -608,7 +595,6 @@ export const promise = async ({
|
||||
const groupSelect = select?.[field.name]
|
||||
|
||||
traverseFields({
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
currentDepth,
|
||||
@@ -761,7 +747,6 @@ export const promise = async ({
|
||||
}
|
||||
|
||||
traverseFields({
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
currentDepth,
|
||||
@@ -795,7 +780,6 @@ export const promise = async ({
|
||||
|
||||
case 'tabs': {
|
||||
traverseFields({
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
currentDepth,
|
||||
|
||||
@@ -13,10 +13,6 @@ 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
|
||||
@@ -49,7 +45,6 @@ type Args = {
|
||||
}
|
||||
|
||||
export const traverseFields = ({
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
currentDepth,
|
||||
@@ -80,7 +75,6 @@ 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, Validate } from '../../config/types.js'
|
||||
import type { Field, TabAsField } from '../../config/types.js'
|
||||
|
||||
import { MissingEditorProp } from '../../../errors/index.js'
|
||||
import { deepMergeWithSourceArrays } from '../../../utilities/deepMerge.js'
|
||||
@@ -16,10 +16,6 @@ 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
|
||||
@@ -52,7 +48,6 @@ type Args = {
|
||||
|
||||
export const promise = async ({
|
||||
id,
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
@@ -82,7 +77,7 @@ export const promise = async ({
|
||||
})
|
||||
|
||||
const passesCondition = field.admin?.condition
|
||||
? Boolean(field.admin.condition(data, siblingData, { blockData, user: req.user }))
|
||||
? Boolean(field.admin.condition(data, siblingData, { user: req.user }))
|
||||
: true
|
||||
let skipValidationFromHere = skipValidation || !passesCondition
|
||||
const { localization } = req.payload.config
|
||||
@@ -107,7 +102,6 @@ export const promise = async ({
|
||||
await priorHook
|
||||
|
||||
const hookedValue = await currentHook({
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
@@ -145,27 +139,22 @@ export const promise = async ({
|
||||
}
|
||||
}
|
||||
|
||||
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),
|
||||
})
|
||||
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,
|
||||
)
|
||||
|
||||
if (typeof validationResult === 'string') {
|
||||
const label = getTranslatedLabel(field?.label || field?.name, req.i18n)
|
||||
@@ -228,7 +217,6 @@ export const promise = async ({
|
||||
promises.push(
|
||||
traverseFields({
|
||||
id,
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
@@ -280,7 +268,6 @@ export const promise = async ({
|
||||
promises.push(
|
||||
traverseFields({
|
||||
id,
|
||||
blockData: row,
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
@@ -314,7 +301,6 @@ export const promise = async ({
|
||||
case 'row': {
|
||||
await traverseFields({
|
||||
id,
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
@@ -353,7 +339,6 @@ export const promise = async ({
|
||||
|
||||
await traverseFields({
|
||||
id,
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
@@ -470,7 +455,6 @@ export const promise = async ({
|
||||
|
||||
await traverseFields({
|
||||
id,
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
@@ -497,7 +481,6 @@ export const promise = async ({
|
||||
case 'tabs': {
|
||||
await traverseFields({
|
||||
id,
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
|
||||
@@ -8,10 +8,6 @@ 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
|
||||
@@ -55,7 +51,6 @@ type Args = {
|
||||
*/
|
||||
export const traverseFields = async ({
|
||||
id,
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
@@ -81,7 +76,6 @@ export const traverseFields = async ({
|
||||
promises.push(
|
||||
promise({
|
||||
id,
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
|
||||
@@ -9,10 +9,6 @@ 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
|
||||
@@ -29,7 +25,6 @@ type Args<T> = {
|
||||
|
||||
export const promise = async <T>({
|
||||
id,
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
doc,
|
||||
@@ -68,7 +63,6 @@ export const promise = async <T>({
|
||||
const localizedValues = await localizedValuesPromise
|
||||
|
||||
const beforeDuplicateArgs: FieldHookArgs = {
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
data: doc,
|
||||
@@ -102,7 +96,6 @@ export const promise = async <T>({
|
||||
siblingDoc[field.name] = localeData
|
||||
} else {
|
||||
const beforeDuplicateArgs: FieldHookArgs = {
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
data: doc,
|
||||
@@ -150,7 +143,6 @@ export const promise = async <T>({
|
||||
promises.push(
|
||||
traverseFields({
|
||||
id,
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
doc,
|
||||
@@ -185,7 +177,6 @@ export const promise = async <T>({
|
||||
promises.push(
|
||||
traverseFields({
|
||||
id,
|
||||
blockData: row,
|
||||
collection,
|
||||
context,
|
||||
doc,
|
||||
@@ -208,7 +199,6 @@ export const promise = async <T>({
|
||||
promises.push(
|
||||
traverseFields({
|
||||
id,
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
doc,
|
||||
@@ -244,7 +234,6 @@ export const promise = async <T>({
|
||||
promises.push(
|
||||
traverseFields({
|
||||
id,
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
doc,
|
||||
@@ -281,7 +270,6 @@ export const promise = async <T>({
|
||||
promises.push(
|
||||
traverseFields({
|
||||
id,
|
||||
blockData: row,
|
||||
collection,
|
||||
context,
|
||||
doc,
|
||||
@@ -312,7 +300,6 @@ export const promise = async <T>({
|
||||
|
||||
await traverseFields({
|
||||
id,
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
doc,
|
||||
@@ -337,7 +324,6 @@ export const promise = async <T>({
|
||||
|
||||
await traverseFields({
|
||||
id,
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
doc,
|
||||
@@ -361,7 +347,6 @@ export const promise = async <T>({
|
||||
case 'row': {
|
||||
await traverseFields({
|
||||
id,
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
doc,
|
||||
@@ -382,7 +367,6 @@ export const promise = async <T>({
|
||||
case 'tab': {
|
||||
await traverseFields({
|
||||
id,
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
doc,
|
||||
@@ -402,7 +386,6 @@ export const promise = async <T>({
|
||||
case 'tabs': {
|
||||
await traverseFields({
|
||||
id,
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
doc,
|
||||
|
||||
@@ -6,10 +6,6 @@ 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
|
||||
@@ -25,7 +21,6 @@ type Args<T> = {
|
||||
|
||||
export const traverseFields = async <T>({
|
||||
id,
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
doc,
|
||||
@@ -43,7 +38,6 @@ export const traverseFields = async <T>({
|
||||
promises.push(
|
||||
promise({
|
||||
id,
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
doc,
|
||||
|
||||
@@ -14,10 +14,6 @@ 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
|
||||
@@ -51,7 +47,6 @@ type Args<T> = {
|
||||
|
||||
export const promise = async <T>({
|
||||
id,
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
@@ -275,7 +270,6 @@ export const promise = async <T>({
|
||||
await priorHook
|
||||
|
||||
const hookedValue = await currentHook({
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
@@ -304,7 +298,7 @@ export const promise = async <T>({
|
||||
if (field.access && field.access[operation]) {
|
||||
const result = overrideAccess
|
||||
? true
|
||||
: await field.access[operation]({ id, blockData, data, doc, req, siblingData })
|
||||
: await field.access[operation]({ id, data, doc, req, siblingData })
|
||||
|
||||
if (!result) {
|
||||
delete siblingData[field.name]
|
||||
@@ -341,7 +335,6 @@ export const promise = async <T>({
|
||||
promises.push(
|
||||
traverseFields({
|
||||
id,
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
@@ -382,7 +375,6 @@ export const promise = async <T>({
|
||||
promises.push(
|
||||
traverseFields({
|
||||
id,
|
||||
blockData: row,
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
@@ -412,7 +404,6 @@ export const promise = async <T>({
|
||||
case 'row': {
|
||||
await traverseFields({
|
||||
id,
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
@@ -446,7 +437,6 @@ export const promise = async <T>({
|
||||
|
||||
await traverseFields({
|
||||
id,
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
@@ -532,7 +522,6 @@ export const promise = async <T>({
|
||||
|
||||
await traverseFields({
|
||||
id,
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
@@ -555,7 +544,6 @@ export const promise = async <T>({
|
||||
case 'tabs': {
|
||||
await traverseFields({
|
||||
id,
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
|
||||
@@ -7,10 +7,6 @@ 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
|
||||
@@ -36,7 +32,6 @@ type Args<T> = {
|
||||
|
||||
export const traverseFields = async <T>({
|
||||
id,
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
@@ -58,7 +53,6 @@ export const traverseFields = async <T>({
|
||||
promises.push(
|
||||
promise({
|
||||
id,
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
|
||||
@@ -377,27 +377,11 @@ export const checkbox: CheckboxFieldValidation = (value, { req: { t }, required
|
||||
|
||||
export type DateFieldValidation = Validate<Date, unknown, unknown, DateField>
|
||||
|
||||
export const date: DateFieldValidation = (
|
||||
value,
|
||||
{ name, req: { t }, required, siblingData, timezone },
|
||||
) => {
|
||||
const validDate = value && !isNaN(Date.parse(value.toString()))
|
||||
|
||||
// We need to also check for the timezone data based on this field's config
|
||||
// We cannot do this inside the timezone field validation as it's visually hidden
|
||||
const hasRequiredTimezone = timezone && required
|
||||
const selectedTimezone: string = siblingData?.[`${name}_tz`]
|
||||
// Always resolve to true if the field is not required, as timezone may be optional too then
|
||||
const validTimezone = hasRequiredTimezone ? Boolean(selectedTimezone) : true
|
||||
|
||||
if (validDate && validTimezone) {
|
||||
export const date: DateFieldValidation = (value, { req: { t }, required }) => {
|
||||
if (value && !isNaN(Date.parse(value.toString()))) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (validDate && !validTimezone) {
|
||||
return t('validation:timezoneRequired')
|
||||
}
|
||||
|
||||
if (value) {
|
||||
return t('validation:notValidDate', { value })
|
||||
}
|
||||
@@ -526,7 +510,7 @@ const validateFilterOptions: Validate<
|
||||
RelationshipField | UploadField
|
||||
> = async (
|
||||
value,
|
||||
{ id, blockData, data, filterOptions, relationTo, req, req: { payload, t, user }, siblingData },
|
||||
{ id, data, filterOptions, relationTo, req, req: { payload, t, user }, siblingData },
|
||||
) => {
|
||||
if (typeof filterOptions !== 'undefined' && value) {
|
||||
const options: {
|
||||
@@ -543,7 +527,6 @@ const validateFilterOptions: Validate<
|
||||
typeof filterOptions === 'function'
|
||||
? await filterOptions({
|
||||
id,
|
||||
blockData,
|
||||
data,
|
||||
relationTo: collection,
|
||||
req,
|
||||
|
||||
@@ -244,11 +244,6 @@ 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,
|
||||
|
||||
@@ -729,20 +729,9 @@ export class BasePayload {
|
||||
typeof this.config.jobs.autoRun === 'function'
|
||||
? await this.config.jobs.autoRun(this)
|
||||
: this.config.jobs.autoRun
|
||||
|
||||
await Promise.all(
|
||||
cronJobs.map((cronConfig) => {
|
||||
const job = new Cron(cronConfig.cron ?? DEFAULT_CRON, async () => {
|
||||
if (typeof this.config.jobs.shouldAutoRun === 'function') {
|
||||
const shouldAutoRun = await this.config.jobs.shouldAutoRun(this)
|
||||
|
||||
if (!shouldAutoRun) {
|
||||
job.stop()
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
new Cron(cronConfig.cron ?? DEFAULT_CRON, async () => {
|
||||
await this.jobs.run({
|
||||
limit: cronConfig.limit ?? DEFAULT_LIMIT,
|
||||
queue: cronConfig.queue,
|
||||
@@ -916,8 +905,6 @@ export const getPayload = async (
|
||||
}
|
||||
} catch (e) {
|
||||
cached.promise = null
|
||||
// add identifier to error object, so that our error logger in routeError.ts does not attempt to re-initialize getPayload
|
||||
e.payloadInitError = true
|
||||
throw e
|
||||
}
|
||||
|
||||
|
||||
@@ -76,13 +76,6 @@ export type JobsConfig = {
|
||||
* a new collection.
|
||||
*/
|
||||
jobsCollectionOverrides?: (args: { defaultJobsCollection: CollectionConfig }) => CollectionConfig
|
||||
/**
|
||||
* A function that will be executed before Payload picks up jobs which are configured by the `jobs.autorun` function.
|
||||
* If this function returns true, jobs will be queried and picked up. If it returns false, jobs will not be run.
|
||||
* @param payload
|
||||
* @returns boolean
|
||||
*/
|
||||
shouldAutoRun?: (payload: Payload) => boolean | Promise<boolean>
|
||||
/**
|
||||
* Define all possible tasks here
|
||||
*/
|
||||
|
||||
@@ -217,9 +217,7 @@ function entityOrFieldToJsDocs({
|
||||
description = entity?.admin?.description?.[i18n.language]
|
||||
}
|
||||
} else if (typeof entity?.admin?.description === 'function' && 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.
|
||||
description = entity?.admin?.description(i18n)
|
||||
}
|
||||
}
|
||||
return description
|
||||
@@ -248,7 +246,7 @@ export function fieldsToJSONSchema(
|
||||
|
||||
return {
|
||||
properties: Object.fromEntries(
|
||||
fields.reduce((fieldSchemas, field, index) => {
|
||||
fields.reduce((fieldSchemas, field) => {
|
||||
const isRequired = fieldAffectsData(field) && fieldIsRequired(field)
|
||||
if (isRequired) {
|
||||
requiredFieldNames.add(field.name)
|
||||
@@ -579,37 +577,25 @@ export function fieldsToJSONSchema(
|
||||
}
|
||||
case 'select': {
|
||||
const optionEnums = buildOptionEnums(field.options)
|
||||
// We get the previous field to check for a date in the case of a timezone select
|
||||
// This works because timezone selects are always inserted right after a date with 'timezone: true'
|
||||
const previousField = fields?.[index - 1]
|
||||
const isTimezoneField =
|
||||
previousField?.type === 'date' && previousField.timezone && field.name.includes('_tz')
|
||||
|
||||
// Timezone selects should reference the supportedTimezones definition
|
||||
if (isTimezoneField) {
|
||||
if (field.hasMany) {
|
||||
fieldSchema = {
|
||||
$ref: `#/definitions/supportedTimezones`,
|
||||
...baseFieldSchema,
|
||||
type: withNullableJSONSchemaType('array', isRequired),
|
||||
items: {
|
||||
type: 'string',
|
||||
},
|
||||
}
|
||||
if (optionEnums?.length) {
|
||||
;(fieldSchema.items as JSONSchema4).enum = optionEnums
|
||||
}
|
||||
} else {
|
||||
if (field.hasMany) {
|
||||
fieldSchema = {
|
||||
...baseFieldSchema,
|
||||
type: withNullableJSONSchemaType('array', isRequired),
|
||||
items: {
|
||||
type: 'string',
|
||||
},
|
||||
}
|
||||
if (optionEnums?.length) {
|
||||
;(fieldSchema.items as JSONSchema4).enum = optionEnums
|
||||
}
|
||||
} else {
|
||||
fieldSchema = {
|
||||
...baseFieldSchema,
|
||||
type: withNullableJSONSchemaType('string', isRequired),
|
||||
}
|
||||
if (optionEnums?.length) {
|
||||
fieldSchema.enum = optionEnums
|
||||
}
|
||||
fieldSchema = {
|
||||
...baseFieldSchema,
|
||||
type: withNullableJSONSchemaType('string', isRequired),
|
||||
}
|
||||
if (optionEnums?.length) {
|
||||
fieldSchema.enum = optionEnums
|
||||
}
|
||||
}
|
||||
|
||||
@@ -968,18 +954,6 @@ export function authCollectionToOperationsJSONSchema(
|
||||
}
|
||||
}
|
||||
|
||||
// Generates the JSON Schema for supported timezones
|
||||
export function timezonesToJSONSchema(
|
||||
supportedTimezones: SanitizedConfig['admin']['timezones']['supportedTimezones'],
|
||||
): JSONSchema4 {
|
||||
return {
|
||||
description: 'Supported timezones in IANA format.',
|
||||
enum: supportedTimezones.map((timezone) =>
|
||||
typeof timezone === 'string' ? timezone : timezone.value,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
function generateAuthOperationSchemas(collections: SanitizedCollectionConfig[]): JSONSchema4 {
|
||||
const properties = collections.reduce((acc, collection) => {
|
||||
if (collection.auth) {
|
||||
@@ -1058,8 +1032,6 @@ export function configToJSONSchema(
|
||||
{},
|
||||
)
|
||||
|
||||
const timezoneDefinitions = timezonesToJSONSchema(config.admin.timezones.supportedTimezones)
|
||||
|
||||
const authOperationDefinitions = [...config.collections]
|
||||
.filter(({ auth }) => Boolean(auth))
|
||||
.reduce(
|
||||
@@ -1083,7 +1055,6 @@ export function configToJSONSchema(
|
||||
let jsonSchema: JSONSchema4 = {
|
||||
additionalProperties: false,
|
||||
definitions: {
|
||||
supportedTimezones: timezoneDefinitions,
|
||||
...entityDefinitions,
|
||||
...Object.fromEntries(interfaceNameDefinitions),
|
||||
...authOperationDefinitions,
|
||||
@@ -1131,7 +1102,7 @@ export function configToJSONSchema(
|
||||
|
||||
if (config?.typescript?.schema?.length) {
|
||||
for (const schema of config.typescript.schema) {
|
||||
jsonSchema = schema({ collectionIDFieldTypes, config, i18n, jsonSchema })
|
||||
jsonSchema = schema({ jsonSchema })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user