Compare commits
198 Commits
docs/admin
...
fix/immuta
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d2c9ba5074 | ||
|
|
9d6583d9de | ||
|
|
7be02194d6 | ||
|
|
1da50f5684 | ||
|
|
f2abc80a00 | ||
|
|
88eeeaa8dd | ||
|
|
d14bc44c63 | ||
|
|
9c53a62503 | ||
|
|
bc79608db4 | ||
|
|
d959d843a2 | ||
|
|
eb09ce9a3e | ||
|
|
f2da72b4d0 | ||
|
|
5285518562 | ||
|
|
243cdb1901 | ||
|
|
c7bb694249 | ||
|
|
8f3d1bd871 | ||
|
|
85f88a0194 | ||
|
|
38f61e91b8 | ||
|
|
ac1e3cf69e | ||
|
|
397c1f1ae7 | ||
|
|
c8f01e31a1 | ||
|
|
9ac7a3ed49 | ||
|
|
051c1fe015 | ||
|
|
6d0924ef37 | ||
|
|
fc5876a602 | ||
|
|
72efc843cc | ||
|
|
3ede7abe00 | ||
|
|
5d65cb002b | ||
|
|
814ced463b | ||
|
|
3de1636e92 | ||
|
|
e9afb367b5 | ||
|
|
029cac3cd3 | ||
|
|
a53876d741 | ||
|
|
6f90d62fc2 | ||
|
|
6699844d7b | ||
|
|
657ad20278 | ||
|
|
30af889e3b | ||
|
|
8378654fd0 | ||
|
|
b0da85dfea | ||
|
|
48115311e7 | ||
|
|
7cef8900a7 | ||
|
|
557ac9931a | ||
|
|
9f7e8f47d2 | ||
|
|
259ea6ab64 | ||
|
|
2ad035fb7b | ||
|
|
1ad1de7a0d | ||
|
|
179778223f | ||
|
|
1e708bdd12 | ||
|
|
36921bd62b | ||
|
|
3af0468062 | ||
|
|
8f6d2e79a1 | ||
|
|
54acdad190 | ||
|
|
312aa639b6 | ||
|
|
2163b0fdb5 | ||
|
|
9724067242 | ||
|
|
5cc0e74471 | ||
|
|
6939a835ca | ||
|
|
143b6e3b8e | ||
|
|
4ebe67324a | ||
|
|
bbfff30d41 | ||
|
|
ba30d7641f | ||
|
|
04b046847b | ||
|
|
62c0872bbb | ||
|
|
31e217967e | ||
|
|
8f203bbbe1 | ||
|
|
f0ea9185ef | ||
|
|
672dace969 | ||
|
|
e36ab6aa2a | ||
|
|
bacc0f002a | ||
|
|
f01cfbcc57 | ||
|
|
4f822a439b | ||
|
|
cc05937633 | ||
|
|
64b63f6833 | ||
|
|
5adb764b08 | ||
|
|
56dec13820 | ||
|
|
1d168318d0 | ||
|
|
f143d25728 | ||
|
|
7d2480aef9 | ||
|
|
c417e3a627 | ||
|
|
efce1549d0 | ||
|
|
d57a78616a | ||
|
|
a3fe60778c | ||
|
|
4ddf96502c | ||
|
|
562acb7492 | ||
|
|
bf4fa59026 | ||
|
|
fd1a4f689e | ||
|
|
a15c38f665 | ||
|
|
fa8a2f8d8d | ||
|
|
b9108b4306 | ||
|
|
6a3d58bb32 | ||
|
|
192964417d | ||
|
|
f03d450d8e | ||
|
|
398d48ab16 | ||
|
|
377454416a | ||
|
|
cd29978faf | ||
|
|
e1b30842fb | ||
|
|
927078c4db | ||
|
|
dda17f0c32 | ||
|
|
6d8aca5ab3 | ||
|
|
c828e336ee | ||
|
|
90d3c65008 | ||
|
|
e75d38ca82 | ||
|
|
79a7b4ad02 | ||
|
|
f7f5651004 | ||
|
|
45a7c8b764 | ||
|
|
25e8799a09 | ||
|
|
6cbda9e231 | ||
|
|
fc42c40883 | ||
|
|
83b4548fc1 | ||
|
|
81e8a9d50d | ||
|
|
9bb89b7b52 | ||
|
|
d4d2bf4617 | ||
|
|
8b5bc3de33 | ||
|
|
19b4ec2562 | ||
|
|
bef98c8d6e | ||
|
|
77395b6483 | ||
|
|
fcaf59176d | ||
|
|
206b4b9d88 | ||
|
|
67c4a20237 | ||
|
|
38131ed2c3 | ||
|
|
96d1d90e78 | ||
|
|
9e97319c6f | ||
|
|
a65289c211 | ||
|
|
4a1b74952f | ||
|
|
8b55e7b51a | ||
|
|
48e613b61f | ||
|
|
428c133033 | ||
|
|
c8c578f5ef | ||
|
|
d53f166476 | ||
|
|
dfddee2125 | ||
|
|
e055565ca8 | ||
|
|
41c7413f59 | ||
|
|
3709950d50 | ||
|
|
6ce5e8b83b | ||
|
|
f3844ee533 | ||
|
|
c21dac1b53 | ||
|
|
b3e7a9d194 | ||
|
|
c4bc0ae48a | ||
|
|
f7b1cd9d63 | ||
|
|
9c25e7b68e | ||
|
|
1d252cbacf | ||
|
|
7118b6418f | ||
|
|
bdf0113b2f | ||
|
|
3436fb16ea | ||
|
|
bcc68572bf | ||
|
|
6aa9da73f8 | ||
|
|
2a3682ff68 | ||
|
|
958e195017 | ||
|
|
45cee23add | ||
|
|
67b7a730ba | ||
|
|
88a2841500 | ||
|
|
7e713a454a | ||
|
|
b975858e76 | ||
|
|
b540da53ec | ||
|
|
c6ab312286 | ||
|
|
526e535763 | ||
|
|
e4712a822b | ||
|
|
81fd42ef69 | ||
|
|
6b6c289d79 | ||
|
|
69c0d09437 | ||
|
|
48f183bd42 | ||
|
|
36e152d69d | ||
|
|
1e698c2bdf | ||
|
|
7bb1c9d3c6 | ||
|
|
4410a49132 | ||
|
|
820a6ec55d | ||
|
|
0a1af45549 | ||
|
|
09ca5143eb | ||
|
|
f1b005c4f5 | ||
|
|
dc9e8fa655 | ||
|
|
2477fc6c75 | ||
|
|
37781808eb | ||
|
|
d92c0009ed | ||
|
|
d32608649c | ||
|
|
f9121c1a3a | ||
|
|
f477e0e3c4 | ||
|
|
4224c68002 | ||
|
|
b014416584 | ||
|
|
1725af5e3a | ||
|
|
a13d4fe5c6 | ||
|
|
3ffc268438 | ||
|
|
d766b1904c | ||
|
|
f31568c69c | ||
|
|
a8bec9a1b2 | ||
|
|
5d7be15c52 | ||
|
|
0058f82d87 | ||
|
|
6ff380ce59 | ||
|
|
f779e48a58 | ||
|
|
1dc748d341 | ||
|
|
c7c5018675 | ||
|
|
9728d80592 | ||
|
|
0594701004 | ||
|
|
845c647ebc | ||
|
|
f6f6a1dc99 | ||
|
|
ee5e96a965 | ||
|
|
22f61ad79e | ||
|
|
460d50baa3 | ||
|
|
76bd05cc5d |
37
.github/CODEOWNERS
vendored
37
.github/CODEOWNERS
vendored
@@ -1,37 +0,0 @@
|
||||
# Order matters. The last matching pattern takes precedence.
|
||||
|
||||
### Package Exports
|
||||
|
||||
**/exports/ @denolfe @jmikrut @DanRibbens
|
||||
|
||||
### 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
|
||||
/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
|
||||
|
||||
### Templates
|
||||
|
||||
/templates/_data/ @denolfe @jmikrut @DanRibbens
|
||||
/templates/_template/ @denolfe @jmikrut @DanRibbens
|
||||
|
||||
### Build Files
|
||||
|
||||
**/tsconfig*.json @denolfe @jmikrut @DanRibbens @AlessioGr
|
||||
**/jest.config.js @denolfe @jmikrut @DanRibbens @AlessioGr
|
||||
|
||||
### Root
|
||||
|
||||
/package.json @denolfe @jmikrut @DanRibbens
|
||||
/tools/ @denolfe @jmikrut @DanRibbens
|
||||
/.husky/ @denolfe @jmikrut @DanRibbens
|
||||
/.vscode/ @denolfe @jmikrut @DanRibbens @AlessioGr
|
||||
/.github/ @denolfe @jmikrut @DanRibbens
|
||||
2
.github/ISSUE_TEMPLATE/1.bug_report_v3.yml
vendored
2
.github/ISSUE_TEMPLATE/1.bug_report_v3.yml
vendored
@@ -57,7 +57,7 @@ body:
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Environment Info
|
||||
description: Paste output from `pnpm payload info` _or_ Payload, Node.js, and Next.js versions.
|
||||
description: Paste output from `pnpm payload info` _or_ Payload, Node.js, and Next.js versions. Please avoid using "latest"—specific version numbers help us accurately diagnose and resolve issues.
|
||||
render: text
|
||||
placeholder: |
|
||||
Payload:
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/2.design_issue.yml
vendored
2
.github/ISSUE_TEMPLATE/2.design_issue.yml
vendored
@@ -20,7 +20,7 @@ body:
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Environment Info
|
||||
description: Paste output from `pnpm payload info` _or_ Payload, Node.js, and Next.js versions.
|
||||
description: Paste output from `pnpm payload info` _or_ Payload, Node.js, and Next.js versions. Please avoid using "latest"—specific version numbers help us accurately diagnose and resolve issues.
|
||||
render: text
|
||||
placeholder: |
|
||||
Payload:
|
||||
|
||||
27
.github/actions/release-commenter/LICENSE
vendored
Normal file
27
.github/actions/release-commenter/LICENSE
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020-2025 Cameron Little <cameron@camlittle.com>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
'Software'), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
---
|
||||
|
||||
Modifications made by Payload CMS, Inc. <info@payloadcms.com>, 2025
|
||||
Details in README.md
|
||||
@@ -4,6 +4,7 @@
|
||||
"private": true,
|
||||
"description": "GitHub Action to automatically comment on PRs and Issues when a fix is released.",
|
||||
"license": "MIT",
|
||||
"author": "Payload <dev@payloadcms.com> (https://payloadcms.com)",
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
"build": "pnpm build:typecheck && pnpm build:ncc",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"target": "es5",
|
||||
"lib": ["es2020.string"],
|
||||
"noEmit": true,
|
||||
"strict": true,
|
||||
|
||||
2
.github/actions/triage/tsconfig.json
vendored
2
.github/actions/triage/tsconfig.json
vendored
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"target": "es5",
|
||||
"lib": ["es2020.string"],
|
||||
"noEmit": true,
|
||||
"strict": true,
|
||||
|
||||
2
.github/comments/invalid-reproduction.md
vendored
2
.github/comments/invalid-reproduction.md
vendored
@@ -4,7 +4,7 @@ Depending on the quality of reproduction steps, this issue may be closed if no r
|
||||
|
||||
### Why was this issue marked with the `invalid-reproduction` label?
|
||||
|
||||
To be able to investigate, we need access to a reproduction to identify what triggered the issue. We prefer a link to a public GitHub repository created with `create-payload-app@beta -t blank` or a forked/branched version of this repository with tests added (more info in the [reproduction-guide](https://github.com/payloadcms/payload/blob/main/.github/reproduction-guide.md)).
|
||||
To be able to investigate, we need access to a reproduction to identify what triggered the issue. We prefer a link to a public GitHub repository created with `create-payload-app@latest -t blank` or a forked/branched version of this repository with tests added (more info in the [reproduction-guide](https://github.com/payloadcms/payload/blob/main/.github/reproduction-guide.md)).
|
||||
|
||||
To make sure the issue is resolved as quickly as possible, please make sure that the reproduction is as **minimal** as possible. This means that you should **remove unnecessary code, files, and dependencies** that do not contribute to the issue. Ensure your reproduction does not depend on secrets, 3rd party registries, private dependencies, or any other data that cannot be made public. Avoid a reproduction including a whole monorepo (unless relevant to the issue). The easier it is to reproduce the issue, the quicker we can help.
|
||||
|
||||
|
||||
4
.github/workflows/lock-issues.yml
vendored
4
.github/workflows/lock-issues.yml
vendored
@@ -2,8 +2,8 @@ name: lock-issues
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Run nightly at 12am EST
|
||||
- cron: '0 4 * * *'
|
||||
# Run nightly at 12am EST, staggered with stale workflow
|
||||
- cron: '0 5 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
|
||||
16
.github/workflows/main.yml
vendored
16
.github/workflows/main.yml
vendored
@@ -41,14 +41,14 @@ jobs:
|
||||
with:
|
||||
filters: |
|
||||
needs_build:
|
||||
- '.github/workflows/**'
|
||||
- '.github/workflows/main.yml'
|
||||
- 'packages/**'
|
||||
- 'test/**'
|
||||
- 'pnpm-lock.yaml'
|
||||
- 'package.json'
|
||||
- 'templates/**'
|
||||
needs_tests:
|
||||
- '.github/workflows/**'
|
||||
- '.github/workflows/main.yml'
|
||||
- 'packages/**'
|
||||
- 'test/**'
|
||||
- 'pnpm-lock.yaml'
|
||||
@@ -275,6 +275,7 @@ jobs:
|
||||
- admin__e2e__general
|
||||
- admin__e2e__list-view
|
||||
- admin__e2e__document-view
|
||||
- admin-bar
|
||||
- admin-root
|
||||
- auth
|
||||
- auth-basic
|
||||
@@ -307,12 +308,14 @@ jobs:
|
||||
- fields__collections__Text
|
||||
- fields__collections__UI
|
||||
- fields__collections__Upload
|
||||
- form-state
|
||||
- live-preview
|
||||
- localization
|
||||
- locked-documents
|
||||
- i18n
|
||||
- plugin-cloud-storage
|
||||
- plugin-form-builder
|
||||
- plugin-import-export
|
||||
- plugin-nested-docs
|
||||
- plugin-seo
|
||||
- versions
|
||||
@@ -417,6 +420,7 @@ jobs:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: payloadtests
|
||||
MONGODB_VERSION: 6.0
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -456,8 +460,14 @@ jobs:
|
||||
echo "POSTGRES_URL=postgresql://$POSTGRES_USER:$POSTGRES_PASSWORD@localhost:5432/$POSTGRES_DB" >> $GITHUB_ENV
|
||||
if: matrix.database == 'postgres'
|
||||
|
||||
# Avoid dockerhub rate-limiting
|
||||
- name: Cache Docker images
|
||||
uses: ScribeMD/docker-cache@0.5.0
|
||||
with:
|
||||
key: docker-${{ runner.os }}-mongo-${{ env.MONGODB_VERSION }}
|
||||
|
||||
- name: Start MongoDB
|
||||
uses: supercharge/mongodb-github-action@1.11.0
|
||||
uses: supercharge/mongodb-github-action@1.12.0
|
||||
with:
|
||||
mongodb-version: 6.0
|
||||
if: matrix.database == 'mongodb'
|
||||
|
||||
1
.github/workflows/pr-title.yml
vendored
1
.github/workflows/pr-title.yml
vendored
@@ -53,6 +53,7 @@ jobs:
|
||||
plugin-cloud
|
||||
plugin-cloud-storage
|
||||
plugin-form-builder
|
||||
plugin-import-export
|
||||
plugin-multi-tenant
|
||||
plugin-nested-docs
|
||||
plugin-redirects
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
name: release-canary
|
||||
name: publish-prerelease
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Run nightly at 10pm EST
|
||||
- cron: '0 3 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
@@ -11,7 +14,7 @@ env:
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: release-canary-${{ github.ref_name }}-${{ github.sha }}
|
||||
name: publish-prerelease-${{ github.ref_name }}-${{ github.sha }}
|
||||
permissions:
|
||||
id-token: write
|
||||
runs-on: ubuntu-24.04
|
||||
@@ -27,8 +30,19 @@ jobs:
|
||||
run: echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> ~/.npmrc
|
||||
env:
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
- name: Canary release script
|
||||
run: pnpm release:canary
|
||||
|
||||
- name: Determine release type
|
||||
id: determine_release_type
|
||||
# Use 'canary' for main branch, 'internal' for others
|
||||
run: |
|
||||
if [[ ${{ github.ref_name }} == "main" ]]; then
|
||||
echo "release_type=canary" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "release_type=internal" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Release
|
||||
run: pnpm publish-prerelease --tag ${{ steps.determine_release_type.outputs.release_type }}
|
||||
env:
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
NPM_CONFIG_PROVENANCE: true
|
||||
4
.github/workflows/stale.yml
vendored
4
.github/workflows/stale.yml
vendored
@@ -2,8 +2,8 @@ name: stale
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Run nightly at 1am EST
|
||||
- cron: '0 5 * * *'
|
||||
# Run nightly at 1am EST, staggered with lock-issues workflow
|
||||
- cron: '0 6 * * *'
|
||||
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -306,6 +306,8 @@ $RECYCLE.BIN/
|
||||
/build
|
||||
.swc
|
||||
app/(payload)/admin/importMap.js
|
||||
test/admin-bar/app/(payload)/admin/importMap.js
|
||||
/test/admin-bar/app/(payload)/admin/importMap.js
|
||||
test/live-preview/app/(payload)/admin/importMap.js
|
||||
/test/live-preview/app/(payload)/admin/importMap.js
|
||||
test/admin-root/app/(payload)/admin/importMap.js
|
||||
@@ -318,3 +320,6 @@ test/databaseAdapter.js
|
||||
/media-with-relation-preview
|
||||
/media-without-relation-preview
|
||||
/media-without-cache-tags
|
||||
test/.localstack
|
||||
test/google-cloud-storage
|
||||
test/azurestoragedata/
|
||||
|
||||
3
.idea/payload.iml
generated
3
.idea/payload.iml
generated
@@ -80,8 +80,9 @@
|
||||
<excludeFolder url="file://$MODULE_DIR$/packages/drizzle/dist" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/packages/db-sqlite/.turbo" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/packages/db-sqlite/dist" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/packages/plugin-import-export/dist" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
</module>
|
||||
@@ -6,7 +6,11 @@ desc: Customize the metadata of your pages within the Admin Panel
|
||||
keywords: admin, components, custom, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
|
||||
---
|
||||
|
||||
Every page within the Admin Panel automatically receives dynamic, auto-generated metadata derived from live document data, the user's current locale, and more, without any additional configuration. This includes the page title, description, og:image and everything in between. Metadata is fully configurable at the root level and cascades down to individual collections, documents, and custom views, allowing for the ability to control metadata on any page with high precision.
|
||||
Every page within the Admin Panel automatically receives dynamic, auto-generated metadata derived from live document data, the user's current locale, and more. This includes the page title, description, og:image, etc. and requires no additional configuration.
|
||||
|
||||
Metadata is fully configurable at the root level and cascades down to individual collections, documents, and custom views. This allows for the ability to control metadata on any page with high precision, while also providing sensible defaults.
|
||||
|
||||
All metadata is injected into Next.js' [`generateMetadata`](https://nextjs.org/docs/app/api-reference/functions/generate-metadata) function. This used to generate the `<head>` of pages within the Admin Panel. All metadata options that are available in Next.js are exposed by Payload.
|
||||
|
||||
Within the Admin Panel, metadata can be customized at the following levels:
|
||||
|
||||
@@ -48,13 +52,9 @@ The following options are available for Root Metadata:
|
||||
|
||||
| Key | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| **`title`** | `string` | The title of the Admin Panel. |
|
||||
| **`description`** | `string` | The description of the Admin Panel. |
|
||||
| **`defaultOGImageType`** | `dynamic` (default), `static`, or `off` | The type of default OG image to use. If set to `dynamic`, Payload will use Next.js image generation to create an image with the title of the page. If set to `static`, Payload will use the `defaultOGImage` URL. If set to `off`, Payload will not generate an OG image. |
|
||||
| **`icons`** | `IconConfig[]` | An array of icon objects. [More details](#icons) |
|
||||
| **`keywords`** | `string` | A comma-separated list of keywords to include in the metadata of the Admin Panel. |
|
||||
| **`openGraph`** | `OpenGraphConfig` | An object containing Open Graph metadata. [More details](#open-graph) |
|
||||
| **`titleSuffix`** | `string` | A suffix to append to the end of the title of every page. Defaults to "- Payload". |
|
||||
| `defaultOGImageType` | `dynamic` (default), `static`, or `off` | The type of default OG image to use. If set to `dynamic`, Payload will use Next.js image generation to create an image with the title of the page. If set to `static`, Payload will use the `defaultOGImage` URL. If set to `off`, Payload will not generate an OG image. |
|
||||
| `titleSuffix` | `string` | A suffix to append to the end of the title of every page. Defaults to "- Payload". |
|
||||
| `[keyof Metadata]` | `unknown` | Any other properties that Next.js supports within the `generateMetadata` function. [More details](https://nextjs.org/docs/app/api-reference/functions/generate-metadata). |
|
||||
|
||||
<Banner type="success">
|
||||
**Reminder:**
|
||||
@@ -67,7 +67,7 @@ The Icons Config corresponds to the `<link>` tags that are used to specify icons
|
||||
|
||||
The most common icon type is the favicon, which is displayed in the browser tab. This is specified by the `rel` attribute `icon`. Other common icon types include `apple-touch-icon`, which is used by Apple devices when the Admin Panel is saved to the home screen, and `mask-icon`, which is used by Safari to mask the Admin Panel icon.
|
||||
|
||||
To customize icons, use the `icons` key within the `admin.meta` object in your Payload Config:
|
||||
To customize icons, use the `admin.meta.icons` property in your Payload Config:
|
||||
|
||||
```ts
|
||||
{
|
||||
@@ -93,23 +93,13 @@ To customize icons, use the `icons` key within the `admin.meta` object in your P
|
||||
}
|
||||
```
|
||||
|
||||
The following options are available for Icons:
|
||||
|
||||
| Key | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| **`rel`** | `string` | The HTML `rel` attribute of the icon. |
|
||||
| **`type`** | `string` | The MIME type of the icon. |
|
||||
| **`color`** | `string` | The color of the icon. |
|
||||
| **`fetchPriority`** | `string` | The [fetch priority](https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElement/fetchPriority) of the icon. |
|
||||
| **`media`** | `string` | The [media query](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_media_queries/Using_media_queries) of the icon. |
|
||||
| **`sizes`** | `string` | The [sizes](https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElement/sizes) of the icon. |
|
||||
| **`url`** | `string` | The URL pointing the resource of the icon. |
|
||||
For a full list of all available Icon options, see the [Next.js documentation](https://nextjs.org/docs/app/api-reference/functions/generate-metadata#icons).
|
||||
|
||||
### Open Graph
|
||||
|
||||
Open Graph metadata is a set of tags that are used to control how URLs are displayed when shared on social media platforms. Open Graph metadata is automatically generated by Payload, but can be customized at the Root level.
|
||||
|
||||
To customize Open Graph metadata, use the `openGraph` key within the `admin.meta` object in your Payload Config:
|
||||
To customize Open Graph metadata, use the `admin.meta.openGraph` property in your Payload Config:
|
||||
|
||||
```ts
|
||||
{
|
||||
@@ -135,14 +125,46 @@ To customize Open Graph metadata, use the `openGraph` key within the `admin.meta
|
||||
}
|
||||
```
|
||||
|
||||
The following options are available for Open Graph Metadata:
|
||||
For a full list of all available Open Graph options, see the [Next.js documentation](https://nextjs.org/docs/app/api-reference/functions/generate-metadata#opengraph).
|
||||
|
||||
| Key | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| **`description`** | `string` | The description of the Admin Panel. |
|
||||
| **`images`** | `OGImageConfig` or `OGImageConfig[]` | An array of image objects. |
|
||||
| **`siteName`** | `string` | The name of the site. |
|
||||
| **`title`** | `string` | The title of the Admin Panel. |
|
||||
### Robots
|
||||
|
||||
Setting the `robots` property will allow you to control the `robots` meta tag that is rendered within the `<head>` of the Admin Panel. This can be used to control how search engines index pages and displays them in search results.
|
||||
|
||||
By default, the Admin Panel is set to prevent search engines from indexing pages within the Admin Panel.
|
||||
|
||||
To customize the Robots Config, use the `admin.meta.robots` property in your Payload Config:
|
||||
|
||||
```ts
|
||||
{
|
||||
// ...
|
||||
admin: {
|
||||
meta: {
|
||||
// highlight-start
|
||||
robots: 'noindex, nofollow',
|
||||
// highlight-end
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
For a full list of all available Robots options, see the [Next.js documentation](https://nextjs.org/docs/app/api-reference/functions/generate-metadata#robots).
|
||||
|
||||
##### Prevent Crawling
|
||||
|
||||
While setting meta tags via `admin.meta.robots` can prevent search engines from _indexing_ web pages, it does not prevent them from being _crawled_.
|
||||
|
||||
To prevent your pages from being crawled altogether, add a `robots.txt` file to your root directory.
|
||||
|
||||
```text
|
||||
User-agent: *
|
||||
Disallow: /admin/
|
||||
```
|
||||
|
||||
<Banner type="info">
|
||||
**Note:**
|
||||
If you've customized the path to your Admin Panel via `config.routes`, be sure to update the `Disallow` directive to match your custom path.
|
||||
</Banner>
|
||||
|
||||
## Collection Metadata
|
||||
|
||||
|
||||
@@ -135,7 +135,7 @@ const ExampleComponent: React.FC = () => {
|
||||
|
||||
#### Updating other fields' values
|
||||
|
||||
If you are building a Custom Component, then you should use `setValue` which is returned from the `useField` hook to programmatically set your field's value. But if you're looking to update _another_ field's value, you can use `dispatchFields` returned from `useFormFields`.
|
||||
If you are building a Custom Component, then you should use `setValue` which is returned from the `useField` hook to programmatically set your field's value. But if you're looking to update _another_ field's value, you can use `dispatchFields` returned from `useAllFormFields`.
|
||||
|
||||
You can send the following actions to the `dispatchFields` function.
|
||||
|
||||
|
||||
@@ -62,9 +62,12 @@ export const Users: CollectionConfig = {
|
||||
})
|
||||
|
||||
return {
|
||||
// Send the user back to authenticate,
|
||||
// Send the user with the collection slug back to authenticate,
|
||||
// or send null if no user should be authenticated
|
||||
user: usersQuery.docs[0] || null,
|
||||
user: usersQuery.docs[0] ? {
|
||||
collection: 'users'
|
||||
...usersQuery.docs[0],
|
||||
} : null,
|
||||
|
||||
// Optionally, you can return headers
|
||||
// that you'd like Payload to set here when
|
||||
|
||||
@@ -57,9 +57,9 @@ export const Posts: CollectionConfig = {
|
||||
|
||||
The following options are available:
|
||||
|
||||
| Option | Description |
|
||||
| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `admin` | The configuration options for the Admin Panel. [More details](#admin-options). |
|
||||
| Option | Description |
|
||||
| ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `admin` | The configuration options for the Admin Panel. [More details](#admin-options). |
|
||||
| `access` | Provide Access Control functions to define exactly who should be able to do what with Documents in this Collection. [More details](../access-control/collections). |
|
||||
| `auth` | Specify options if you would like this Collection to feature authentication. [More details](../authentication/overview). |
|
||||
| `custom` | Extension point for adding custom data (e.g. for plugins) |
|
||||
@@ -67,17 +67,18 @@ The following options are available:
|
||||
| `defaultSort` | Pass a top-level field to sort by default in the Collection List View. Prefix the name of the field with a minus symbol ("-") to sort in descending order. Multiple fields can be specified by using a string array. |
|
||||
| `dbName` | Custom table or Collection name depending on the Database Adapter. Auto-generated from slug if not defined. |
|
||||
| `endpoints` | Add custom routes to the REST API. Set to `false` to disable routes. [More details](../rest-api/overview#custom-endpoints). |
|
||||
| `fields` * | Array of field types that will determine the structure and functionality of the data stored within this Collection. [More details](../fields/overview). |
|
||||
| `fields` * | Array of field types that will determine the structure and functionality of the data stored within this Collection. [More details](../fields/overview). |
|
||||
| `graphQL` | Manage GraphQL-related properties for this collection. [More](#graphql) |
|
||||
| `hooks` | Entry point for Hooks. [More details](../hooks/overview#collection-hooks). |
|
||||
| `labels` | Singular and plural labels for use in identifying this Collection throughout Payload. Auto-generated from slug if not defined. |
|
||||
| `lockDocuments` | Enables or disables document locking. By default, document locking is enabled. Set to an object to configure, or set to `false` to disable locking. [More details](../admin/locked-documents). |
|
||||
| `slug` * | Unique, URL-friendly string that will act as an identifier for this Collection. |
|
||||
| `slug` * | Unique, URL-friendly string that will act as an identifier for this Collection. |
|
||||
| `timestamps` | Set to false to disable documents' automatically generated `createdAt` and `updatedAt` timestamps. |
|
||||
| `typescript` | An object with property `interface` as the text used in schema generation. Auto-generated from slug if not defined. |
|
||||
| `upload` | Specify options if you would like this Collection to support file uploads. For more, consult the [Uploads](../upload/overview) documentation. |
|
||||
| `versions` | Set to true to enable default options, or configure with object properties. [More details](../versions/overview#collection-config). |
|
||||
| `defaultPopulate` | Specify which fields to select when this Collection is populated from another document. [More Details](../queries/select#defaultpopulate-collection-config-property). |
|
||||
| `indexes` | Define compound indexes for this collection. This can be used to either speed up querying/sorting by 2 or more fields at the same time or to ensure uniqueness between several fields. |
|
||||
|
||||
_* An asterisk denotes that a property is required._
|
||||
|
||||
@@ -120,6 +121,7 @@ The following options are available:
|
||||
| `useAsTitle` | Specify a top-level field to use for a document title throughout the Admin Panel. If no field is defined, the ID of the document is used as the title. A field with `virtual: true` cannot be used as the title. |
|
||||
| `description` | Text to display below the Collection label in the List View to give editors more information. Alternatively, you can use the `admin.components.Description` to render a React component. [More details](#custom-components). |
|
||||
| `defaultColumns` | Array of field names that correspond to which columns to show by default in this Collection's List View. |
|
||||
| `disableCopyToLocale` | Disables the "Copy to Locale" button while editing documents within this Collection. Only applicable when localization is enabled. |
|
||||
| `hideAPIURL` | Hides the "API URL" meta field while editing documents within this Collection. |
|
||||
| `enableRichTextLink` | The [Rich Text](../fields/rich-text) field features a `Link` element which allows for users to automatically reference related documents within their rich text. Set to `true` by default. |
|
||||
| `enableRichTextRelationship` | The [Rich Text](../fields/rich-text) field features a `Relationship` element which allows for users to automatically reference related documents within their rich text. Set to `true` by default. |
|
||||
|
||||
@@ -188,13 +188,15 @@ In order to use [Custom Translations](#custom-translations) in your project, you
|
||||
|
||||
Here we create a shareable translations object. We will import this in both our custom components and in our Payload config.
|
||||
|
||||
In this example we show how to extend English, but you can do the same for any language you want.
|
||||
|
||||
```ts
|
||||
// <rootDir>/custom-translations.ts
|
||||
|
||||
import type { Config } from 'payload'
|
||||
import { enTranslations } from '@payloadcms/translations/languages/en'
|
||||
import type { NestedKeysStripped } from '@payloadcms/translations'
|
||||
|
||||
export const customTranslations: Config['i18n']['translations'] = {
|
||||
export const customTranslations = {
|
||||
en: {
|
||||
general: {
|
||||
myCustomKey: 'My custom english translation',
|
||||
@@ -205,7 +207,7 @@ export const customTranslations: Config['i18n']['translations'] = {
|
||||
},
|
||||
}
|
||||
|
||||
export type CustomTranslationsObject = typeof customTranslations.en
|
||||
export type CustomTranslationsObject = typeof customTranslations.en & typeof enTranslations
|
||||
export type CustomTranslationsKeys = NestedKeysStripped<CustomTranslationsObject>
|
||||
```
|
||||
|
||||
@@ -259,7 +261,10 @@ const field: Field = {
|
||||
name: 'myField',
|
||||
type: 'text',
|
||||
label: (
|
||||
{ t }: { t: TFunction<CustomTranslationsKeys | DefaultTranslationKeys> }, // The generic passed to TFunction does not automatically merge the custom translations with the default translations. We need to merge them ourselves here
|
||||
) => t('fields:addLabel'),
|
||||
{ t: defaultT }
|
||||
) => {
|
||||
const t = defaultT as TFunction<CustomTranslationsKeys>
|
||||
return t('fields:addLabel')
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
@@ -63,41 +63,41 @@ export default buildConfig({
|
||||
|
||||
The following options are available:
|
||||
|
||||
| Option | Description |
|
||||
| -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **`admin`** | The configuration options for the Admin Panel, including Custom Components, Live Preview, etc. [More details](../admin/overview#admin-options). |
|
||||
| **`bin`** | Register custom bin scripts for Payload to execute. |
|
||||
| **`editor`** | The Rich Text Editor which will be used by `richText` fields. [More details](../rich-text/overview). |
|
||||
| **`db`** * | The Database Adapter which will be used by Payload. [More details](../database/overview). |
|
||||
| **`serverURL`** | A string used to define the absolute URL of your app. This includes the protocol, for example `https://example.com`. No paths allowed, only protocol, domain and (optionally) port. |
|
||||
| **`collections`** | An array of Collections for Payload to manage. [More details](./collections). |
|
||||
| **`compatibility`** | Compatibility flags for earlier versions of Payload. [More details](#compatibility-flags). |
|
||||
| **`globals`** | An array of Globals for Payload to manage. [More details](./globals). |
|
||||
| **`cors`** | Cross-origin resource sharing (CORS) is a mechanism that accept incoming requests from given domains. You can also customize the `Access-Control-Allow-Headers` header. [More details](#cors). |
|
||||
| **`localization`** | Opt-in to translate your content into multiple locales. [More details](./localization). |
|
||||
| **`logger`** | Logger options, logger options with a destination stream, or an instantiated logger instance. [More details](https://getpino.io/#/docs/api?id=options). |
|
||||
| **`loggingLevels`** | An object to override the level to use in the logger for Payload's errors. |
|
||||
| **`graphQL`** | Manage GraphQL-specific functionality, including custom queries and mutations, query complexity limits, etc. [More details](../graphql/overview#graphql-options). |
|
||||
| **`cookiePrefix`** | A string that will be prefixed to all cookies that Payload sets. |
|
||||
| **`csrf`** | A whitelist array of URLs to allow Payload to accept cookies from. [More details](../authentication/cookies#csrf-attacks). |
|
||||
| **`defaultDepth`** | If a user does not specify `depth` while requesting a resource, this depth will be used. [More details](../queries/depth). |
|
||||
| **`defaultMaxTextLength`** | The maximum allowed string length to be permitted application-wide. Helps to prevent malicious public document creation. |
|
||||
| **`maxDepth`** | The maximum allowed depth to be permitted application-wide. This setting helps prevent against malicious queries. Defaults to `10`. [More details](../queries/depth). |
|
||||
| **`indexSortableFields`** | Automatically index all sortable top-level fields in the database to improve sort performance and add database compatibility for Azure Cosmos and similar. |
|
||||
| **`upload`** | Base Payload upload configuration. [More details](../upload/overview#payload-wide-upload-options). |
|
||||
| **`routes`** | Control the routing structure that Payload binds itself to. [More details](../admin/overview#root-level-routes). |
|
||||
| **`email`** | Configure the Email Adapter for Payload to use. [More details](../email/overview). |
|
||||
| **`onInit`** | A function that is called immediately following startup that receives the Payload instance as its only argument. |
|
||||
| **`debug`** | Enable to expose more detailed error information. |
|
||||
| **`telemetry`** | Disable Payload telemetry by passing `false`. [More details](#telemetry). |
|
||||
| **`hooks`** | An array of Root Hooks. [More details](../hooks/overview). |
|
||||
| **`plugins`** | An array of Plugins. [More details](../plugins/overview). |
|
||||
| **`endpoints`** | An array of Custom Endpoints added to the Payload router. [More details](../rest-api/overview#custom-endpoints). |
|
||||
| **`custom`** | Extension point for adding custom data (e.g. for plugins). |
|
||||
| **`i18n`** | Internationalization configuration. Pass all i18n languages you'd like the admin UI to support. Defaults to English-only. [More details](./i18n). |
|
||||
| **`secret`** * | A secure, unguessable string that Payload will use for any encryption workflows - for example, password salt / hashing. |
|
||||
| **`sharp`** | If you would like Payload to offer cropping, focal point selection, and automatic media resizing, install and pass the Sharp module to the config here. |
|
||||
| **`typescript`** | Configure TypeScript settings here. [More details](#typescript). |
|
||||
| Option | Description |
|
||||
| -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **`admin`** | The configuration options for the Admin Panel, including Custom Components, Live Preview, etc. [More details](../admin/overview#admin-options). |
|
||||
| **`bin`** | Register custom bin scripts for Payload to execute. [More Details](#custom-bin-scripts). |
|
||||
| **`editor`** | The Rich Text Editor which will be used by `richText` fields. [More details](../rich-text/overview). |
|
||||
| **`db`** * | The Database Adapter which will be used by Payload. [More details](../database/overview). |
|
||||
| **`serverURL`** | A string used to define the absolute URL of your app. This includes the protocol, for example `https://example.com`. No paths allowed, only protocol, domain and (optionally) port. |
|
||||
| **`collections`** | An array of Collections for Payload to manage. [More details](./collections). |
|
||||
| **`compatibility`** | Compatibility flags for earlier versions of Payload. [More details](#compatibility-flags). |
|
||||
| **`globals`** | An array of Globals for Payload to manage. [More details](./globals). |
|
||||
| **`cors`** | Cross-origin resource sharing (CORS) is a mechanism that accept incoming requests from given domains. You can also customize the `Access-Control-Allow-Headers` header. [More details](#cors). |
|
||||
| **`localization`** | Opt-in to translate your content into multiple locales. [More details](./localization). |
|
||||
| **`logger`** | Logger options, logger options with a destination stream, or an instantiated logger instance. [More details](https://getpino.io/#/docs/api?id=options). |
|
||||
| **`loggingLevels`** | An object to override the level to use in the logger for Payload's errors. |
|
||||
| **`graphQL`** | Manage GraphQL-specific functionality, including custom queries and mutations, query complexity limits, etc. [More details](../graphql/overview#graphql-options). |
|
||||
| **`cookiePrefix`** | A string that will be prefixed to all cookies that Payload sets. |
|
||||
| **`csrf`** | A whitelist array of URLs to allow Payload to accept cookies from. [More details](../authentication/cookies#csrf-attacks). |
|
||||
| **`defaultDepth`** | If a user does not specify `depth` while requesting a resource, this depth will be used. [More details](../queries/depth). |
|
||||
| **`defaultMaxTextLength`** | The maximum allowed string length to be permitted application-wide. Helps to prevent malicious public document creation. |
|
||||
| **`maxDepth`** | The maximum allowed depth to be permitted application-wide. This setting helps prevent against malicious queries. Defaults to `10`. [More details](../queries/depth). |
|
||||
| **`indexSortableFields`** | Automatically index all sortable top-level fields in the database to improve sort performance and add database compatibility for Azure Cosmos and similar. |
|
||||
| **`upload`** | Base Payload upload configuration. [More details](../upload/overview#payload-wide-upload-options). |
|
||||
| **`routes`** | Control the routing structure that Payload binds itself to. [More details](../admin/overview#root-level-routes). |
|
||||
| **`email`** | Configure the Email Adapter for Payload to use. [More details](../email/overview). |
|
||||
| **`onInit`** | A function that is called immediately following startup that receives the Payload instance as its only argument. |
|
||||
| **`debug`** | Enable to expose more detailed error information. |
|
||||
| **`telemetry`** | Disable Payload telemetry by passing `false`. [More details](#telemetry). |
|
||||
| **`hooks`** | An array of Root Hooks. [More details](../hooks/overview). |
|
||||
| **`plugins`** | An array of Plugins. [More details](../plugins/overview). |
|
||||
| **`endpoints`** | An array of Custom Endpoints added to the Payload router. [More details](../rest-api/overview#custom-endpoints). |
|
||||
| **`custom`** | Extension point for adding custom data (e.g. for plugins). |
|
||||
| **`i18n`** | Internationalization configuration. Pass all i18n languages you'd like the admin UI to support. Defaults to English-only. [More details](./i18n). |
|
||||
| **`secret`** * | A secure, unguessable string that Payload will use for any encryption workflows - for example, password salt / hashing. |
|
||||
| **`sharp`** | If you would like Payload to offer cropping, focal point selection, and automatic media resizing, install and pass the Sharp module to the config here. |
|
||||
| **`typescript`** | Configure TypeScript settings here. [More details](#typescript). |
|
||||
|
||||
_* An asterisk denotes that a property is required._
|
||||
|
||||
@@ -265,3 +265,43 @@ The Payload Config can accept compatibility flags for running the newest version
|
||||
Payload localization works on a field-by-field basis. As you can nest fields within other fields, you could potentially nest a localized field within a localized field—but this would be redundant and unnecessary. There would be no reason to define a localized field within a localized parent field, given that the entire data structure from the parent field onward would be localized.
|
||||
|
||||
By default, Payload will remove the `localized: true` property from sub-fields if a parent field is localized. Set this compatibility flag to `true` only if you have an existing Payload MongoDB database from pre-3.0, and you have nested localized fields that you would like to maintain without migrating.
|
||||
|
||||
|
||||
## Custom bin scripts
|
||||
|
||||
Using the `bin` configuration property, you can inject your own scripts to `npx payload`.
|
||||
Example for `pnpm payload seed`:
|
||||
|
||||
|
||||
Step 1: create `seed.ts` file in the same folder with `payload.config.ts` with:
|
||||
|
||||
```ts
|
||||
import type { SanitizedConfig } from 'payload'
|
||||
|
||||
import payload from 'payload'
|
||||
|
||||
// Script must define a "script" function export that accepts the sanitized config
|
||||
export const script = async (config: SanitizedConfig) => {
|
||||
await payload.init({ config })
|
||||
await payload.create({ collection: 'pages', data: { title: 'my title' } })
|
||||
payload.logger.info('Succesffully seeded!')
|
||||
process.exit(0)
|
||||
}
|
||||
```
|
||||
|
||||
Step 2: add the `seed` script to `bin`:
|
||||
```ts
|
||||
export default buildConfig({
|
||||
bin: [
|
||||
{
|
||||
scriptPath: path.resolve(dirname, 'seed.ts'),
|
||||
key: 'seed',
|
||||
},
|
||||
],
|
||||
})
|
||||
```
|
||||
|
||||
Now you can run the command using:
|
||||
```sh
|
||||
pnpm payload seed
|
||||
```
|
||||
@@ -119,10 +119,32 @@ For details on how to build Custom Components, see [Building Custom Components](
|
||||
|
||||
### Import Map
|
||||
|
||||
In order for Payload to make use of [Component Paths](#component-paths), an "Import Map" is automatically generated at `app/(payload)/admin/importMap.js`. This file contains every Custom Component in your config, keyed to their respective paths. When Payload needs to lookup a component, it uses this file to find the correct import.
|
||||
In order for Payload to make use of [Component Paths](#component-paths), an "Import Map" is automatically generated at either `src/app/(payload)/admin/importMap.js` or `app/(payload)/admin/importMap.js`. This file contains every Custom Component in your config, keyed to their respective paths. When Payload needs to lookup a component, it uses this file to find the correct import.
|
||||
|
||||
The Import Map is automatically regenerated at startup and whenever Hot Module Replacement (HMR) runs, or you can run `payload generate:importmap` to manually regenerate it.
|
||||
|
||||
#### Overriding Import Map Location
|
||||
|
||||
Using the `config.admin.importMap.importMapFile` property, you can override the location of the import map. This is useful if you want to place the import map in a different location, or if you want to use a custom file name.
|
||||
|
||||
```ts
|
||||
import { buildConfig } from 'payload'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import path from 'path'
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
const dirname = path.dirname(filename)
|
||||
|
||||
const config = buildConfig({
|
||||
// ...
|
||||
admin: {
|
||||
importMap: {
|
||||
baseDir: path.resolve(dirname, 'src'),
|
||||
importMapFile: path.resolve(dirname, 'app', '(payload)', 'custom-import-map.js'), // highlight-line
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
#### Custom Imports
|
||||
|
||||
If needed, custom items can be appended onto the Import Map. This is mostly only relevant for plugin authors who need to add a custom import that is not referenced in a known location.
|
||||
@@ -146,7 +168,7 @@ export default buildConfig({
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## Building Custom Components
|
||||
@@ -276,7 +298,7 @@ export default async function MyServerComponent({
|
||||
|
||||
But, the Payload Config is [non-serializable](https://react.dev/reference/rsc/use-client#serializable-types) by design. It is full of custom validation functions and more. This means that the Payload Config, in its entirety, cannot be passed directly to Client Components.
|
||||
|
||||
For this reason, Payload creates a Client Config and passes it into the Config Provider. This is a serializable version of the Payload Config that can be accessed from any Client Component via the [`useConfig`](../admin/hooks#useconfig) hook:
|
||||
For this reason, Payload creates a Client Config and passes it into the Config Provider. This is a serializable version of the Payload Config that can be accessed from any Client Component via the [`useConfig`](../admin/react-hooks#useconfig) hook:
|
||||
|
||||
```tsx
|
||||
'use client'
|
||||
@@ -375,7 +397,7 @@ export function MyClientComponent() {
|
||||
```
|
||||
|
||||
<Banner type="success">
|
||||
See the [Hooks](../admin/hooks) documentation for a full list of available hooks.
|
||||
See the [Hooks](../admin/react-hooks) documentation for a full list of available hooks.
|
||||
</Banner>
|
||||
|
||||
### Getting the Current Locale
|
||||
@@ -422,12 +444,12 @@ function Greeting() {
|
||||
```
|
||||
|
||||
<Banner type="success">
|
||||
See the [Hooks](../admin/hooks) documentation for a full list of available hooks.
|
||||
See the [Hooks](../admin/react-hooks) documentation for a full list of available hooks.
|
||||
</Banner>
|
||||
|
||||
### Using Hooks
|
||||
|
||||
To make it easier to [build your Custom Components](#building-custom-components), you can use [Payload's built-in React Hooks](../admin/hooks) in any Client Component. For example, you might want to interact with one of Payload's many React Contexts. To do this, you can use one of the many hooks available depending on your needs.
|
||||
To make it easier to [build your Custom Components](#building-custom-components), you can use [Payload's built-in React Hooks](../admin/react-hooks) in any Client Component. For example, you might want to interact with one of Payload's many React Contexts. To do this, you can use one of the many hooks available depending on your needs.
|
||||
|
||||
```tsx
|
||||
'use client'
|
||||
@@ -444,7 +466,7 @@ export function MyClientComponent() {
|
||||
```
|
||||
|
||||
<Banner type="success">
|
||||
See the [Hooks](../admin/hooks) documentation for a full list of available hooks.
|
||||
See the [Hooks](../admin/react-hooks) documentation for a full list of available hooks.
|
||||
</Banner>
|
||||
|
||||
### Adding Styles
|
||||
|
||||
@@ -118,6 +118,10 @@ default, migrations will be named using a timestamp.
|
||||
npm run payload migrate:create optional-name-here
|
||||
```
|
||||
|
||||
Flags:
|
||||
* `--skip-empty`: with Postgres, it skips the "no schema changes detected. Would you like to create a blank migration file?" prompt which can be useful for generating migration in CI.
|
||||
* `--force-accept-warning`: accepts any command prompts, creates a blank migration even if there weren't any changes to the schema.
|
||||
|
||||
### Status
|
||||
|
||||
The `migrate:status` command will check the status of migrations and output a table of which migrations have been run,
|
||||
|
||||
@@ -34,11 +34,12 @@ export default buildConfig({
|
||||
| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| `autoPluralization` | Tell Mongoose to auto-pluralize any collection names if it encounters any singular words used as collection `slug`s. |
|
||||
| `connectOptions` | Customize MongoDB connection options. Payload will connect to your MongoDB database using default options which you can override and extend to include all the [options](https://mongoosejs.com/docs/connections.html#options) available to mongoose. |
|
||||
| `collectionsSchemaOptions` | Customize Mongoose schema options for collections. |
|
||||
| `collectionsSchemaOptions` | Customize Mongoose schema options for collections. |
|
||||
| `disableIndexHints` | Set to true to disable hinting to MongoDB to use 'id' as index. This is currently done when counting documents for pagination, as it increases the speed of the count function used in that query. Disabling this optimization might fix some problems with AWS DocumentDB. Defaults to false |
|
||||
| `migrationDir` | Customize the directory that migrations are stored. |
|
||||
| `transactionOptions` | An object with configuration properties used in [transactions](https://www.mongodb.com/docs/manual/core/transactions/) or `false` which will disable the use of transactions. |
|
||||
| `collation` | Enable language-specific string comparison with customizable options. Available on MongoDB 3.4+. Defaults locale to "en". Example: `{ strength: 3 }`. For a full list of collation options and their definitions, see the [MongoDB documentation](https://www.mongodb.com/docs/manual/reference/collation/). |
|
||||
| `allowAdditionalKeys` | By default, Payload strips all additional keys from MongoDB data that don't exist in the Payload schema. If you have some data that you want to include to the result but it doesn't exist in Payload, you can set this to `true`. Be careful as Payload access control _won't_ work for this data. |
|
||||
|
||||
## Access to Mongoose models
|
||||
|
||||
@@ -50,3 +51,11 @@ You can access Mongoose models as follows:
|
||||
- Collection models - `payload.db.collections[myCollectionSlug]`
|
||||
- Globals model - `payload.db.globals`
|
||||
- Versions model (both collections and globals) - `payload.db.versions[myEntitySlug]`
|
||||
|
||||
## Using other MongoDB implementations
|
||||
Limitations with [DocumentDB](https://aws.amazon.com/documentdb/) and [Azure Cosmos DB](https://azure.microsoft.com/en-us/products/cosmos-db):
|
||||
|
||||
* For Azure Cosmos DB you must pass `transactionOptions: false` to the adapter options. Azure Cosmos DB does not support transactions that update two and more documents in different collections, which is a common case when using Payload (via hooks).
|
||||
* For Azure Cosmos DB the root config property `indexSortableFields` must be set to `true`.
|
||||
* The [Join Field](../fields/join) is not supported in DocumentDB and Azure Cosmos DB, as we internally use MongoDB aggregations to query data for that field, which are limited there. This can be changed in the future.
|
||||
* For DocumentDB pass `disableIndexHints: true` to disable hinting to the DB to use `id` as index which can cause problems with DocumentDB.
|
||||
@@ -79,10 +79,12 @@ export const MyBlocksField: Field = {
|
||||
|
||||
The Blocks Field inherits all of the default options from the base [Field Admin Config](./overview#admin-options), plus the following additional options:
|
||||
|
||||
| Option | Description |
|
||||
| ------------------- | ---------------------------------- |
|
||||
| **`initCollapsed`** | Set the initial collapsed state |
|
||||
| **`isSortable`** | Disable order sorting by setting this value to `false` |
|
||||
| Option | Description |
|
||||
| ------------------- | -------------------------------------------------------------------------- |
|
||||
| **`group`** | Text or localization object used to group this Block in the Blocks Drawer. |
|
||||
| **`initCollapsed`** | Set the initial collapsed state |
|
||||
| **`isSortable`** | Disable order sorting by setting this value to `false` |
|
||||
| **`disableBlockName`** | Hide the blockName field by setting this value to `true` |
|
||||
|
||||
#### Customizing the way your block is rendered in Lexical
|
||||
|
||||
@@ -164,7 +166,7 @@ The `blockType` is saved as the slug of the block that has been selected.
|
||||
|
||||
**`blockName`**
|
||||
|
||||
The Admin Panel provides each block with a `blockName` field which optionally allows editors to label their blocks for better editability and readability.
|
||||
The Admin Panel provides each block with a `blockName` field which optionally allows editors to label their blocks for better editability and readability. This can be visually hidden via `admin.disableBlockName`.
|
||||
|
||||
## Example
|
||||
|
||||
|
||||
@@ -62,6 +62,10 @@ of relationship types in an easy manner.
|
||||
The Join field is extremely performant and does not add additional query overhead to your API responses until you add depth of 1 or above. It works in all database adapters. In MongoDB, we use **aggregations** to automatically join in related documents, and in relational databases, we use joins.
|
||||
</Banner>
|
||||
|
||||
<Banner type="warning">
|
||||
The Join Field is not supported in [DocumentDB](https://aws.amazon.com/documentdb/) and [Azure Cosmos DB](https://azure.microsoft.com/en-us/products/cosmos-db), as we internally use MongoDB aggregations to query data for that field, which are limited there. This can be changed in the future.
|
||||
</Banner>
|
||||
|
||||
### Schema advice
|
||||
|
||||
When modeling your database, you might come across many places where you'd like to feature bi-directional relationships.
|
||||
@@ -158,6 +162,7 @@ object with:
|
||||
|
||||
- `docs` an array of related documents or only IDs if the depth is reached
|
||||
- `hasNextPage` a boolean indicating if there are additional documents
|
||||
- `totalDocs` a total number of documents, exists only if `count: true` is passed to the join query
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -171,7 +176,8 @@ object with:
|
||||
}
|
||||
// { ... }
|
||||
],
|
||||
"hasNextPage": false
|
||||
"hasNextPage": false,
|
||||
"totalDocs": 10, // if count: true is passed
|
||||
}
|
||||
// other fields...
|
||||
}
|
||||
@@ -184,6 +190,7 @@ object with:
|
||||
|
||||
- `docs` an array of `relationTo` - the collection slug of the document and `value` - the document itself or the ID if the depth is reached
|
||||
- `hasNextPage` a boolean indicating if there are additional documents
|
||||
- `totalDocs` a total number of documents, exists only if `count: true` is passed to the join query
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -200,7 +207,8 @@ object with:
|
||||
}
|
||||
// { ... }
|
||||
],
|
||||
"hasNextPage": false
|
||||
"hasNextPage": false,
|
||||
"totalDocs": 10, // if count: true is passed
|
||||
}
|
||||
// other fields...
|
||||
}
|
||||
@@ -215,10 +223,11 @@ returning. This is useful for performance reasons when you don't need the relate
|
||||
The following query options are supported:
|
||||
|
||||
| Property | Description |
|
||||
|-------------|-----------------------------------------------------------------------------------------------------|
|
||||
| ----------- | --------------------------------------------------------------------------------------------------- |
|
||||
| **`limit`** | The maximum related documents to be returned, default is 10. |
|
||||
| **`where`** | An optional `Where` query to filter joined documents. Will be merged with the field `where` object. |
|
||||
| **`sort`** | A string used to order related results |
|
||||
| **`count`** | Whether include the count of related documents or not. Not included by default |
|
||||
|
||||
These can be applied to the local API, GraphQL, and REST API.
|
||||
|
||||
|
||||
@@ -286,14 +286,15 @@ export const MyField: Field = {
|
||||
|
||||
The following additional properties are provided in the `ctx` object:
|
||||
|
||||
| Property | Description |
|
||||
| Property | Description |
|
||||
| --- | --- |
|
||||
| `data` | An object containing the full collection or global document currently being edited. |
|
||||
| `siblingData` | An object containing document data that is scoped to only fields within the same parent of this field. |
|
||||
| `operation` | Will be `create` or `update` depending on the UI action or API call. |
|
||||
| `id` | The `id` of the current document being edited. `id` is `undefined` during the `create` operation. |
|
||||
| `req` | The current HTTP request object. Contains `payload`, `user`, etc. |
|
||||
| `event` | Either `onChange` or `submit` depending on the current action. Used as a performance opt-in. [More details](#async-field-validations). |
|
||||
| `data` | An object containing the full collection or global document currently being edited. |
|
||||
| `siblingData` | An object containing document data that is scoped to only fields within the same parent of this field. |
|
||||
| `operation` | Will be `create` or `update` depending on the UI action or API call. |
|
||||
| `path` | The full path to the field in the schema, represented as an array of string segments, including array indexes. I.e `['group', 'myArray', '1', 'textField']`. |
|
||||
| `id` | The `id` of the current document being edited. `id` is `undefined` during the `create` operation. |
|
||||
| `req` | The current HTTP request object. Contains `payload`, `user`, etc. |
|
||||
| `event` | Either `onChange` or `submit` depending on the current action. Used as a performance opt-in. [More details](#async-field-validations). |
|
||||
|
||||
#### Reusing Default Field Validations
|
||||
|
||||
@@ -502,7 +503,7 @@ export const MyCollectionConfig: CollectionConfig = {
|
||||
|
||||
All Description Functions receive the following arguments:
|
||||
|
||||
| Argument | Description |
|
||||
| Argument | Description |
|
||||
| --- | --- |
|
||||
| **`t`** | The `t` function used to internationalize the Admin Panel. [More details](../configuration/i18n) |
|
||||
|
||||
@@ -512,11 +513,21 @@ All Description Functions receive the following arguments:
|
||||
|
||||
### Conditional Logic
|
||||
|
||||
You can show and hide fields based on what other fields are doing by utilizing conditional logic on a field by field basis. The `condition` property on a field's admin config accepts a function which takes three arguments:
|
||||
You can show and hide fields based on what other fields are doing by utilizing conditional logic on a field by field basis. The `condition` property on a field's admin config accepts a function which takes the following arguments:
|
||||
|
||||
- `data` - the entire document's data that is currently being edited
|
||||
- `siblingData` - only the fields that are direct siblings to the field with the condition
|
||||
- `{ user }` - the final argument is an object containing the currently authenticated user
|
||||
| Argument | Description |
|
||||
| --- | --- |
|
||||
| **`data`** | The entire document's data that is currently being edited. |
|
||||
| **`siblingData`** | Only the fields that are direct siblings to the field with the condition. |
|
||||
| **`ctx`** | An object containing additional information about the field’s location and user. |
|
||||
|
||||
The `ctx` object:
|
||||
|
||||
| Property | Description |
|
||||
| --- | --- |
|
||||
| **`blockData`** | The nearest parent block's data. If the field is not inside a block, this will be `undefined`. |
|
||||
| **`path`** | The full path to the field in the schema, represented as an array of string segments, including array indexes. I.e `['group', 'myArray', '1', 'textField']`. |
|
||||
| **`user`** | The currently authenticated user object. |
|
||||
|
||||
The `condition` function should return a boolean that will control if the field should be displayed or not.
|
||||
|
||||
@@ -535,7 +546,7 @@ The `condition` function should return a boolean that will control if the field
|
||||
type: 'text',
|
||||
admin: {
|
||||
// highlight-start
|
||||
condition: (data, siblingData, { user }) => {
|
||||
condition: (data, siblingData, { blockData, path, user }) => {
|
||||
if (data.enableGreeting) {
|
||||
return true
|
||||
} else {
|
||||
@@ -658,7 +669,7 @@ In addition to the above props, all Server Components will also receive the foll
|
||||
|
||||
When swapping out the `Field` component, you are responsible for sending and receiving the field's `value` from the form itself.
|
||||
|
||||
To do so, import the [`useField`](../admin/hooks#usefield) hook from `@payloadcms/ui` and use it to manage the field's value:
|
||||
To do so, import the [`useField`](../admin/react-hooks#usefield) hook from `@payloadcms/ui` and use it to manage the field's value:
|
||||
|
||||
```tsx
|
||||
'use client'
|
||||
@@ -677,7 +688,7 @@ export const CustomTextField: React.FC = () => {
|
||||
```
|
||||
|
||||
<Banner type="success">
|
||||
For a complete list of all available React hooks, see the [Payload React Hooks](../admin/hooks) documentation. For additional help, see [Building Custom Components](../custom-components/overview#building-custom-components).
|
||||
For a complete list of all available React hooks, see the [Payload React Hooks](../admin/react-hooks) documentation. For additional help, see [Building Custom Components](../custom-components/overview#building-custom-components).
|
||||
</Banner>
|
||||
|
||||
##### TypeScript#field-component-types
|
||||
|
||||
@@ -27,7 +27,7 @@ There are four main types of Hooks in Payload:
|
||||
|
||||
<Banner type="warning">
|
||||
**Reminder:**
|
||||
Payload also ships a set of _React_ hooks that you can use in your frontend application. Although they share a common name, these are very different things and should not be confused. [More details](../admin/hooks).
|
||||
Payload also ships a set of _React_ hooks that you can use in your frontend application. Although they share a common name, these are very different things and should not be confused. [More details](../admin/react-hooks).
|
||||
</Banner>
|
||||
|
||||
## Root Hooks
|
||||
|
||||
@@ -44,3 +44,31 @@ const createdJob = await payload.jobs.queue({
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
#### Cancelling Jobs
|
||||
|
||||
Payload allows you to cancel jobs that are either queued or currently running. When cancelling a running job, the current task will finish executing, but no subsequent tasks will run. This happens because the job checks its cancellation status between tasks.
|
||||
|
||||
##### Cancel a Single Job
|
||||
|
||||
To cancel a specific job, use the `payload.jobs.cancelByID` method with the job's ID:
|
||||
|
||||
```ts
|
||||
await payload.jobs.cancelByID({
|
||||
id: createdJob.id,
|
||||
})
|
||||
```
|
||||
|
||||
##### Cancel Multiple Jobs
|
||||
|
||||
To cancel multiple jobs at once, use the `payload.jobs.cancel` method with a `Where` query:
|
||||
|
||||
```ts
|
||||
await payload.jobs.cancel({
|
||||
where: {
|
||||
workflowSlug: {
|
||||
equals: 'createPost',
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
@@ -239,16 +239,13 @@ export const useLivePreview = <T extends any>(props: {
|
||||
|
||||
## Example
|
||||
|
||||
For a working demonstration of this, check out the official [Live Preview Example](https://github.com/payloadcms/payload/tree/main/examples/live-preview). There you will find examples of various front-end frameworks and how to integrate each one of them, including:
|
||||
|
||||
- [Next.js App Router](https://github.com/payloadcms/payload/tree/main/examples/live-preview/next-app)
|
||||
- [Next.js Pages Router](https://github.com/payloadcms/payload/tree/main/examples/live-preview/next-pages)
|
||||
For a working demonstration of this, check out the official [Live Preview Example](https://github.com/payloadcms/payload/tree/main/examples/live-preview). There you will find an example of a fully integrated Next.js App Router front-end that runs on the same server as Payload.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
#### Relationships and/or uploads are not populating
|
||||
|
||||
If you are using relationships or uploads in your front-end application, and your front-end application runs on a different domain than your Payload server, you may need to configure [CORS](../configuration/overview) to allow requests to be made between the two domains. This includes sites that are running on a different port or subdomain. Similarly, if you are protecting resources behind user authentication, you may also need to configure [CSRF](../authentication/overview#csrf-protection) to allow cookies to be sent between the two domains. For example:
|
||||
If you are using relationships or uploads in your front-end application, and your front-end application runs on a different domain than your Payload server, you may need to configure [CORS](../configuration/overview#cors) to allow requests to be made between the two domains. This includes sites that are running on a different port or subdomain. Similarly, if you are protecting resources behind user authentication, you may also need to configure [CSRF](../authentication/cookies#csrf-prevention) to allow cookies to be sent between the two domains. For example:
|
||||
|
||||
```ts
|
||||
// payload.config.ts
|
||||
|
||||
@@ -414,6 +414,15 @@ For more details, see the [Documentation](https://payloadcms.com/docs/getting-st
|
||||
```
|
||||
1. The `./src/public` directory is now located directly at root level `./public` [see Next.js docs for details](https://nextjs.org/docs/pages/building-your-application/optimizing/static-assets)
|
||||
|
||||
1. Payload now automatically removes `localized: true` property from sub-fields if a parent is localized, as it's redunant and unnecessary. If you have some existing data in this structure and you want to disable that behavior, you need to enable `allowLocalizedWithinLocalized` flag in your payload.config [read more in documentation](https://payloadcms.com/docs/configuration/overview#compatibility-flags), or create a migration script that aligns your data.
|
||||
Mongodb example for a link in a page layout.
|
||||
|
||||
```diff
|
||||
- layout.columns.en.link.en.type.en
|
||||
+ layout.columns.en.link.type
|
||||
```
|
||||
|
||||
|
||||
## Custom Components
|
||||
|
||||
1. All Payload React components have been moved from the `payload` package to `@payloadcms/ui`. If you were previously importing components into your app from the `payload` package, for example to create Custom Components, you will need to change your import paths:
|
||||
@@ -1104,6 +1113,57 @@ plugins: [
|
||||
|
||||
If you have custom features for `@payloadcms/richtext-lexical` you will need to migrate your code to the new API. Read more about the new API in the [documentation](https://payloadcms.com/docs/rich-text/building-custom-features).
|
||||
|
||||
## Reserved Field names
|
||||
|
||||
Payload reserves certain field names for internal use. Using any of the following names in your collections or globals will result in those fields being sanitized from the config, which can cause deployment errors. Ensure that any conflicting fields are renamed before migrating.
|
||||
|
||||
### General Reserved Names
|
||||
|
||||
- `file`
|
||||
- `_id` (MongoDB only)
|
||||
- `__v` (MongoDB only)
|
||||
|
||||
**Important Note**: It is recommended to avoid using field names with an underscore (`_`) prefix unless explicitly required by a plugin. Payload uses this prefix for internal columns, which can lead to conflicts in certain SQL conditions. The following are examples of reserved internal columns (this list is not exhaustive and other internal fields may also apply):
|
||||
|
||||
- `_order`
|
||||
- `_path`
|
||||
- `_uuid`
|
||||
- `_parent_id`
|
||||
- `_locale`
|
||||
|
||||
### Auth-Related Reserved Names
|
||||
|
||||
These are restricted if your collection uses `auth: true` and does not have `disableAuthStrategy: true`:
|
||||
- `salt`
|
||||
- `hash`
|
||||
- `apiKey` (when `auth.useAPIKey: true` is enabled)
|
||||
- `useAPIKey` (when `auth.useAPIKey: true` is enabled)
|
||||
- `resetPasswordToken`
|
||||
- `resetPasswordExpiration`
|
||||
- `password`
|
||||
- `email`
|
||||
- `username`
|
||||
|
||||
### Upload-Related Reserved Names
|
||||
|
||||
These apply if your collection has `upload: true` configured:
|
||||
|
||||
- `filename`
|
||||
- `mimetype`
|
||||
- `filesize`
|
||||
- `width`
|
||||
- `height`
|
||||
- `focalX`
|
||||
- `focalY`
|
||||
- `url`
|
||||
- `thumbnailURL`
|
||||
|
||||
If `imageSizes` is configured, the following are also reserved:
|
||||
|
||||
- `sizes`
|
||||
|
||||
If any of these names are found in your collection / global fields, update them before migrating to avoid unexpected issues.
|
||||
|
||||
## Upgrade from previous beta
|
||||
|
||||
Reference this [community-made site](https://payload-releases-filter.vercel.app/?version=3&from=152429656&to=188243150&sort=asc&breaking=on). Set your version, sort by oldest first, enable breaking changes only.
|
||||
|
||||
@@ -6,7 +6,7 @@ desc: Starting to build your own plugin? Find everything you need and learn best
|
||||
keywords: plugins, template, config, configuration, extensions, custom, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
|
||||
---
|
||||
|
||||
Building your own [Payload Plugin](./overview) is easy, and if you're already familiar with Payload then you'll have everything you need to get started. You can either start from scratch or use the [Plugin Template](#plugin-template) to get up and running quickly.
|
||||
Building your own [Payload Plugin](./overview) is easy, and if you're already familiar with Payload then you'll have everything you need to get started. You can either start from scratch or use the [Plugin Template](#plugin-template) to get up and running quickly.
|
||||
|
||||
<Banner type="success">
|
||||
To use the template, run `npx create-payload-app@latest --template plugin` directly in
|
||||
@@ -19,7 +19,7 @@ Our plugin template includes everything you need to build a full life-cycle plug
|
||||
- A local dev environment to develop the plugin
|
||||
- Test suite with integrated GitHub workflow
|
||||
|
||||
By abstracting your code into a plugin, you'll be able to reuse your feature across multiple projects and make it available for other developers to use.
|
||||
By abstracting your code into a plugin, you'll be able to reuse your feature across multiple projects and make it available for other developers to use.
|
||||
|
||||
## Plugins Recap
|
||||
|
||||
@@ -75,7 +75,7 @@ The purpose of the **dev** folder is to provide a sanitized local Payload projec
|
||||
|
||||
Do **not** store any of the plugin functionality in this folder - it is purely an environment to _assist_ you with developing the plugin.
|
||||
|
||||
If you're starting from scratch, you can easily setup a dev environment like this:
|
||||
If you're starting from scratch, you can easily setup a dev environment like this:
|
||||
|
||||
```
|
||||
mkdir dev
|
||||
@@ -83,7 +83,7 @@ cd dev
|
||||
npx create-payload-app@latest
|
||||
```
|
||||
|
||||
If you're using the plugin template, the dev folder is built out for you and the `samplePlugin` has already been installed in `dev/payload.config.ts`.
|
||||
If you're using the plugin template, the dev folder is built out for you and the `samplePlugin` has already been installed in `dev/payload.config.ts`.
|
||||
|
||||
```
|
||||
plugins: [
|
||||
@@ -96,7 +96,7 @@ If you're using the plugin template, the dev folder is built out for you an
|
||||
|
||||
You can add to the `dev/payload.config.ts` and build out the dev project as needed to test your plugin.
|
||||
|
||||
When you're ready to start development, navigate into this folder with `cd dev`
|
||||
When you're ready to start development, navigate into this folder with `cd dev`
|
||||
|
||||
And then start the project with `pnpm dev` and pull up `http://localhost:3000` in your browser.
|
||||
|
||||
@@ -108,7 +108,7 @@ A good test suite is essential to ensure quality and stability in your plugin. P
|
||||
|
||||
Jest organizes tests into test suites and cases. We recommend creating tests based on the expected behavior of your plugin from start to finish. Read more about tests in the [Jest documentation.](https://jestjs.io/)
|
||||
|
||||
The plugin template provides a stubbed out test suite at `dev/plugin.spec.ts` which is ready to go - just add in your own test conditions and you're all set!
|
||||
The plugin template provides a stubbed out test suite at `dev/plugin.spec.ts` which is ready to go - just add in your own test conditions and you're all set!
|
||||
|
||||
```
|
||||
let payload: Payload
|
||||
@@ -160,7 +160,7 @@ export const seed = async (payload: Payload): Promise<void> => {
|
||||
|
||||
## Building a Plugin
|
||||
|
||||
Now that we have our environment setup and dev project ready to go - it's time to build the plugin!
|
||||
Now that we have our environment setup and dev project ready to go - it's time to build the plugin!
|
||||
|
||||
|
||||
```
|
||||
@@ -217,7 +217,7 @@ To reiterate, the essence of a [Payload Plugin](./overview) is simply to extend
|
||||
|
||||
We are going to use spread syntax to allow us to add data to existing arrays without losing the existing data. It is crucial to spread the existing data correctly, else this can cause adverse behavior and conflicts with Payload Config and other plugins.
|
||||
|
||||
Let's say you want to build a plugin that adds a new collection:
|
||||
Let's say you want to build a plugin that adds a new collection:
|
||||
|
||||
```
|
||||
config.collections = [
|
||||
@@ -227,7 +227,7 @@ config.collections = [
|
||||
]
|
||||
```
|
||||
|
||||
First, you need to spread the `config.collections` to ensure that we don't lose the existing collections. Then you can add any additional collections, just as you would in a regular Payload Config.
|
||||
First, you need to spread the `config.collections` to ensure that we don't lose the existing collections. Then you can add any additional collections, just as you would in a regular Payload Config.
|
||||
|
||||
This same logic is applied to other array and object like properties such as admin, globals and hooks:
|
||||
|
||||
@@ -284,7 +284,7 @@ For a better user experience, provide a way to disable the plugin without uninst
|
||||
|
||||
### Include tests in your GitHub CI workflow
|
||||
|
||||
If you've configured tests for your package, integrate them into your workflow to run the tests each time you commit to the plugin repository. Learn more about [how to configure tests into your GitHub CI workflow.](https://docs.github.com/en/actions/use-cases-and-examples/building-and-testing/building-and-testing-nodejs)
|
||||
If you've configured tests for your package, integrate them into your workflow to run the tests each time you commit to the plugin repository. Learn more about [how to configure tests into your GitHub CI workflow.](https://docs.github.com/en/actions/use-cases-and-examples/building-and-testing/building-and-testing-nodejs)
|
||||
|
||||
### Publish your finished plugin to npm
|
||||
|
||||
|
||||
@@ -52,7 +52,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
|
||||
@@ -158,6 +158,16 @@ type MultiTenantPluginConfig<ConfigTypes = unknown> = {
|
||||
rowFields?: never
|
||||
tenantFieldAccess?: never
|
||||
}
|
||||
/**
|
||||
* Customize tenant selector label
|
||||
*
|
||||
* Either a string or an object where the keys are locales and the values are the string labels
|
||||
*/
|
||||
tenantSelectorLabel?:
|
||||
| Partial<{
|
||||
[key in AcceptedLanguages]?: string
|
||||
}>
|
||||
| string
|
||||
/**
|
||||
* The slug for the tenant collection
|
||||
*
|
||||
@@ -176,6 +186,14 @@ type MultiTenantPluginConfig<ConfigTypes = unknown> = {
|
||||
* Opt out of adding access constraints to the tenants collection
|
||||
*/
|
||||
useTenantsCollectionAccess?: boolean
|
||||
/**
|
||||
* Opt out including the baseListFilter to filter tenants by selected tenant
|
||||
*/
|
||||
useTenantsListFilter?: boolean
|
||||
/**
|
||||
* Opt out including the baseListFilter to filter users by selected tenant
|
||||
*/
|
||||
useUsersTenantFilter?: boolean
|
||||
}
|
||||
```
|
||||
|
||||
@@ -278,6 +296,50 @@ async rewrites() {
|
||||
}
|
||||
```
|
||||
|
||||
### React Hooks
|
||||
|
||||
Below are the hooks exported from the plugin that you can import into your own custom components to consume.
|
||||
|
||||
#### useTenantSelection
|
||||
|
||||
You can import this like so:
|
||||
|
||||
```tsx
|
||||
import { useTenantSelection } from '@payloadcms/plugin-multi-tenant/client'
|
||||
|
||||
...
|
||||
|
||||
const tenantContext = useTenantSelection()
|
||||
```
|
||||
|
||||
The hook returns the following context:
|
||||
|
||||
```ts
|
||||
type ContextType = {
|
||||
/**
|
||||
* Array of options to select from
|
||||
*/
|
||||
options: OptionObject[]
|
||||
/**
|
||||
* The currently selected tenant ID
|
||||
*/
|
||||
selectedTenantID: number | string | undefined
|
||||
/**
|
||||
* Prevents a refresh when the tenant is changed
|
||||
*
|
||||
* If not switching tenants while viewing a "global", set to true
|
||||
*/
|
||||
setPreventRefreshOnChange: React.Dispatch<React.SetStateAction<boolean>>
|
||||
/**
|
||||
* Sets the selected tenant ID
|
||||
*
|
||||
* @param args.id - The ID of the tenant to select
|
||||
* @param args.refresh - Whether to refresh the page after changing the tenant
|
||||
*/
|
||||
setTenant: (args: { id: number | string | undefined; refresh?: boolean }) => void
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## Examples
|
||||
|
||||
|
||||
@@ -703,6 +703,31 @@ import { addLocalesToRequestFromData } from 'payload'
|
||||
}
|
||||
```
|
||||
|
||||
`headersWithCors`
|
||||
|
||||
By default, custom endpoints don't handle CORS headers in responses. The `headersWithCors` function checks the Payload config and sets the appropriate CORS headers in the response accordingly.
|
||||
|
||||
```ts
|
||||
import { headersWithCors } from 'payload'
|
||||
|
||||
// custom endpoint example
|
||||
{
|
||||
path: '/:id/tracking',
|
||||
method: 'post',
|
||||
handler: async (req) => {
|
||||
return Response.json(
|
||||
{ message: 'success' },
|
||||
{
|
||||
headers: headersWithCors({
|
||||
headers: new Headers(),
|
||||
req,
|
||||
})
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Method Override for GET Requests
|
||||
|
||||
Payload supports a method override feature that allows you to send GET requests using the HTTP POST method. This can be particularly useful in scenarios when the query string in a regular GET request is too long.
|
||||
|
||||
@@ -2,15 +2,15 @@
|
||||
title: Lexical Converters
|
||||
label: Converters
|
||||
order: 20
|
||||
desc: Conversion between lexical, markdown and html
|
||||
keywords: lexical, rich text, editor, headless cms, convert, html, mdx, markdown, md, conversion, export
|
||||
desc: Conversion between lexical, markdown, jsx and html
|
||||
keywords: lexical, rich text, editor, headless cms, convert, html, mdx, markdown, md, conversion, export, jsx
|
||||
---
|
||||
|
||||
Lexical saves data in JSON - this is great for storage and flexibility and allows you to easily to convert it to other formats like JSX, HTML or Markdown.
|
||||
|
||||
## Lexical => JSX
|
||||
|
||||
If your frontend uses React, converting Lexical to JSX is the recommended way to render rich text content. Import the `RichText` component from `@payloadcms/richtext-lexical/react` and pass the Lexical content to it:
|
||||
For React-based frontends, converting Lexical content to JSX is the recommended rendering approach. Import the RichText component from @payloadcms/richtext-lexical/react and pass the Lexical content to it:
|
||||
|
||||
```tsx
|
||||
import React from 'react'
|
||||
@@ -24,46 +24,138 @@ export const MyComponent = ({ data }: { data: SerializedEditorState }) => {
|
||||
}
|
||||
```
|
||||
|
||||
The `RichText` component includes built-in serializers for common Lexical nodes but allows customization through the `converters` prop.
|
||||
|
||||
In our website template [you have an example](https://github.com/payloadcms/payload/blob/main/templates/website/src/components/RichText/index.tsx) of how to use `converters` to render custom blocks.
|
||||
|
||||
The `RichText` component includes built-in serializers for common Lexical nodes but allows customization through the `converters` prop. In our [website template](https://github.com/payloadcms/payload/blob/main/templates/website/src/components/RichText/index.tsx) you have an example of how to use `converters` to render custom blocks, custom nodes and override existing converters.
|
||||
|
||||
<Banner type="default">
|
||||
The JSX converter expects the input data to be fully populated. When fetching data, ensure the `depth` setting is high enough, to ensure that lexical nodes such as uploads are populated.
|
||||
When fetching data, ensure your `depth` setting is high enough to fully populate Lexical nodes such as uploads. The JSX converter requires fully populated data to work correctly.
|
||||
</Banner>
|
||||
|
||||
### Converting Lexical Blocks to JSX
|
||||
### Converting Internal Links
|
||||
|
||||
In order to convert lexical blocks or inline blocks to JSX, you will have to pass the converter for your block to the RichText component. This converter is not included by default, as Payload doesn't know how to render your custom blocks.
|
||||
By default, Payload doesn't know how to convert **internal** links to JSX, as it doesn't know what the corresponding URL of the internal link is. You'll notice that you get a "found internal link, but internalDocToHref is not provided" error in the console when you try to render content with internal links.
|
||||
|
||||
To fix this, you need to pass the `internalDocToHref` prop to `LinkJSXConverter`. This prop is a function that receives the link node and returns the URL of the document.
|
||||
|
||||
```tsx
|
||||
import React from 'react'
|
||||
import {
|
||||
type JSXConvertersFunction,
|
||||
RichText,
|
||||
} from '@payloadcms/richtext-lexical/react'
|
||||
import type { DefaultNodeTypes, SerializedLinkNode } from '@payloadcms/richtext-lexical'
|
||||
import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'
|
||||
|
||||
const jsxConverters: JSXConvertersFunction = ({ defaultConverters }) => ({
|
||||
import {
|
||||
type JSXConvertersFunction,
|
||||
LinkJSXConverter,
|
||||
RichText,
|
||||
} from '@payloadcms/richtext-lexical/react'
|
||||
import React from 'react'
|
||||
|
||||
const internalDocToHref = ({ linkNode }: { linkNode: SerializedLinkNode }) => {
|
||||
const { relationTo, value } = linkNode.fields.doc!
|
||||
if (typeof value !== 'object') {
|
||||
throw new Error('Expected value to be an object')
|
||||
}
|
||||
const slug = value.slug
|
||||
return relationTo === 'posts' ? `/posts/${slug}` : `/${slug}`
|
||||
}
|
||||
|
||||
const jsxConverters: JSXConvertersFunction<DefaultNodeTypes> = ({ defaultConverters }) => ({
|
||||
...defaultConverters,
|
||||
...LinkJSXConverter({ internalDocToHref }),
|
||||
})
|
||||
|
||||
export const MyComponent: React.FC<{
|
||||
lexicalData: SerializedEditorState
|
||||
}> = ({ lexicalData }) => {
|
||||
return <RichText converters={jsxConverters} data={lexicalData} />
|
||||
}
|
||||
```
|
||||
|
||||
### Converting Lexical Blocks
|
||||
|
||||
To convert Lexical Blocks or Inline Blocks to JSX, pass the converter for your block to the `RichText` component. This converter is not included by default, as Payload doesn't know how to render your custom blocks.
|
||||
|
||||
```tsx
|
||||
'use client'
|
||||
import type { MyInlineBlock, MyNumberBlock, MyTextBlock } from '@/payload-types'
|
||||
import type {
|
||||
DefaultNodeTypes,
|
||||
SerializedBlockNode,
|
||||
SerializedInlineBlockNode,
|
||||
} from '@payloadcms/richtext-lexical'
|
||||
import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'
|
||||
|
||||
import { type JSXConvertersFunction, RichText } from '@payloadcms/richtext-lexical/react'
|
||||
import React from 'react'
|
||||
|
||||
// Extend the default node types with your custom blocks for full type safety
|
||||
type NodeTypes =
|
||||
| DefaultNodeTypes
|
||||
| SerializedBlockNode<MyNumberBlock | MyTextBlock>
|
||||
| SerializedInlineBlockNode<MyInlineBlock>
|
||||
|
||||
const jsxConverters: JSXConvertersFunction<NodeTypes> = ({ defaultConverters }) => ({
|
||||
...defaultConverters,
|
||||
blocks: {
|
||||
// myTextBlock is the slug of the block
|
||||
// Each key should match your block's slug
|
||||
myNumberBlock: ({ node }) => <div>{node.fields.number}</div>,
|
||||
myTextBlock: ({ node }) => <div style={{ backgroundColor: 'red' }}>{node.fields.text}</div>,
|
||||
},
|
||||
inlineBlocks: {
|
||||
// myInlineBlock is the slug of the block
|
||||
// Each key should match your inline block's slug
|
||||
myInlineBlock: ({ node }) => <span>{node.fields.text}</span>,
|
||||
},
|
||||
})
|
||||
|
||||
export const MyComponent = ({ lexicalData }) => {
|
||||
return (
|
||||
<RichText
|
||||
converters={jsxConverters}
|
||||
data={lexicalData.lexicalWithBlocks as SerializedEditorState}
|
||||
/>
|
||||
)
|
||||
export const MyComponent: React.FC<{
|
||||
lexicalData: SerializedEditorState
|
||||
}> = ({ lexicalData }) => {
|
||||
return <RichText converters={jsxConverters} data={lexicalData} />
|
||||
}
|
||||
```
|
||||
|
||||
### Overriding Default JSX Converters
|
||||
|
||||
You can override any of the default JSX converters by passing passing your custom converter, keyed to the node type, to the `converters` prop / the converters function.
|
||||
|
||||
Example - overriding the upload node converter to use next/image:
|
||||
|
||||
```tsx
|
||||
'use client'
|
||||
import type { DefaultNodeTypes, SerializedUploadNode } from '@payloadcms/richtext-lexical'
|
||||
import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'
|
||||
|
||||
import { type JSXConvertersFunction, RichText } from '@payloadcms/richtext-lexical/react'
|
||||
import Image from 'next/image'
|
||||
import React from 'react'
|
||||
|
||||
type NodeTypes = DefaultNodeTypes
|
||||
|
||||
// Custom upload converter component that uses next/image
|
||||
const CustomUploadComponent: React.FC<{
|
||||
node: SerializedUploadNode
|
||||
}> = ({ node }) => {
|
||||
if (node.relationTo === 'uploads') {
|
||||
const uploadDoc = node.value
|
||||
if (typeof uploadDoc !== 'object') {
|
||||
return null
|
||||
}
|
||||
const { alt, height, url, width } = uploadDoc
|
||||
return <Image alt={alt} height={height} src={url} width={width} />
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
const jsxConverters: JSXConvertersFunction<NodeTypes> = ({ defaultConverters }) => ({
|
||||
...defaultConverters,
|
||||
// Override the default upload converter
|
||||
upload: ({ node }) => {
|
||||
return <CustomUploadComponent node={node} />
|
||||
},
|
||||
})
|
||||
|
||||
export const MyComponent: React.FC<{
|
||||
lexicalData: SerializedEditorState
|
||||
}> = ({ lexicalData }) => {
|
||||
return <RichText converters={jsxConverters} data={lexicalData} />
|
||||
}
|
||||
```
|
||||
|
||||
@@ -71,19 +163,156 @@ export const MyComponent = ({ lexicalData }) => {
|
||||
|
||||
If you don't have a React-based frontend, or if you need to send the content to a third-party service, you can convert lexical to HTML. There are two ways to do this:
|
||||
|
||||
1. **Outputting HTML from the Collection:** Create a new field in your collection to convert saved JSON content to HTML. Payload generates and outputs the HTML for use in your frontend.
|
||||
2. **Generating HTML on any server** Convert JSON to HTML on-demand on the server.
|
||||
|
||||
In both cases, the conversion needs to happen on a server, as the HTML converter will automatically fetch data for nodes that require it (e.g. uploads and internal links). The editor comes with built-in HTML serializers, simplifying the process of converting JSON to HTML.
|
||||
1. **Generating HTML in your frontend** Convert JSON to HTML on-demand wherever you need it (Recommended).
|
||||
2. **Outputting HTML from the Collection:** Create a new field in your collection to convert saved JSON content to HTML. Payload generates and outputs the HTML for use in your frontend. This is not recommended, as this approach adds additional overhead to the Payload API and may not work with live preview.
|
||||
|
||||
### Generating HTML in your frontend
|
||||
|
||||
If you wish to convert JSON to HTML ad-hoc, use the `convertLexicalToHTML` function exported from `@payloadcms/richtext-lexical/html`:
|
||||
|
||||
```tsx
|
||||
'use client'
|
||||
|
||||
import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'
|
||||
import { convertLexicalToHTML } from '@payloadcms/richtext-lexical/html'
|
||||
|
||||
import React from 'react'
|
||||
|
||||
export const MyComponent = ({ data }: { data: SerializedEditorState }) => {
|
||||
const html = convertLexicalToHTML({ data })
|
||||
|
||||
return <div dangerouslySetInnerHTML={{ __html: html }} />
|
||||
}
|
||||
```
|
||||
|
||||
### Generating HTML in your frontend with dynamic population
|
||||
|
||||
The default `convertLexicalToHTML` function does not populate data for nodes like uploads or links - it expects you to pass in the fully populated data. If you want the converter to dynamically populate those nodes as they are encountered, you have to use the async version of the converter, imported from `@payloadcms/richtext-lexical/html-async`, and pass in the `populate` function:
|
||||
|
||||
```tsx
|
||||
'use client'
|
||||
|
||||
import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'
|
||||
|
||||
import { getRestPopulateFn } from '@payloadcms/richtext-lexical/client'
|
||||
import { convertLexicalToHTMLAsync } from '@payloadcms/richtext-lexical/html-async'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
|
||||
export const MyComponent = ({ data }: { data: SerializedEditorState }) => {
|
||||
const [html, setHTML] = useState<null | string>(null)
|
||||
useEffect(() => {
|
||||
async function convert() {
|
||||
const html = await convertLexicalToHTMLAsync({
|
||||
data,
|
||||
populate: getRestPopulateFn({
|
||||
apiURL: `http://localhost:3000/api`,
|
||||
}),
|
||||
})
|
||||
setHTML(html)
|
||||
}
|
||||
|
||||
void convert()
|
||||
}, [data])
|
||||
|
||||
return html && <div dangerouslySetInnerHTML={{ __html: html }} />
|
||||
}
|
||||
```
|
||||
|
||||
Do note that using the REST populate function will result in each node sending a separate request to the REST API, which may be slow for a large amount of nodes. On the server, you can use the payload populate function, which will be more efficient:
|
||||
|
||||
```tsx
|
||||
import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'
|
||||
|
||||
import { getPayloadPopulateFn } from '@payloadcms/richtext-lexical'
|
||||
import { convertLexicalToHTMLAsync } from '@payloadcms/richtext-lexical/html-async'
|
||||
import { getPayload } from 'payload'
|
||||
import React from 'react'
|
||||
|
||||
import config from '../../config.js'
|
||||
|
||||
export const MyRSCComponent = async ({ data }: { data: SerializedEditorState }) => {
|
||||
const payload = await getPayload({
|
||||
config,
|
||||
})
|
||||
|
||||
const html = await convertLexicalToHTMLAsync({
|
||||
data,
|
||||
populate: await getPayloadPopulateFn({
|
||||
currentDepth: 0,
|
||||
depth: 1,
|
||||
payload,
|
||||
}),
|
||||
})
|
||||
|
||||
return html && <div dangerouslySetInnerHTML={{ __html: html }} />
|
||||
}
|
||||
```
|
||||
|
||||
### Converting Lexical Blocks
|
||||
|
||||
```tsx
|
||||
'use client'
|
||||
|
||||
import type { MyInlineBlock, MyTextBlock } from '@/payload-types'
|
||||
import type {
|
||||
DefaultNodeTypes,
|
||||
SerializedBlockNode,
|
||||
SerializedInlineBlockNode,
|
||||
} from '@payloadcms/richtext-lexical'
|
||||
import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'
|
||||
|
||||
import {
|
||||
convertLexicalToHTML,
|
||||
type HTMLConvertersFunction,
|
||||
} from '@payloadcms/richtext-lexical/html'
|
||||
import React from 'react'
|
||||
|
||||
type NodeTypes =
|
||||
| DefaultNodeTypes
|
||||
| SerializedBlockNode<MyTextBlock>
|
||||
| SerializedInlineBlockNode<MyInlineBlock>
|
||||
|
||||
const htmlConverters: HTMLConvertersFunction<NodeTypes> = ({ defaultConverters }) => ({
|
||||
...defaultConverters,
|
||||
blocks: {
|
||||
// Each key should match your block's slug
|
||||
myTextBlock: ({ node, providedCSSString }) =>
|
||||
`<div style="background-color: red;${providedCSSString}">${node.fields.text}</div>`,
|
||||
},
|
||||
inlineBlocks: {
|
||||
// Each key should match your inline block's slug
|
||||
myInlineBlock: ({ node, providedStyleTag }) =>
|
||||
`<span${providedStyleTag}>${node.fields.text}</span$>`,
|
||||
},
|
||||
})
|
||||
|
||||
export const MyComponent = ({ data }: { data: SerializedEditorState }) => {
|
||||
const html = convertLexicalToHTML({
|
||||
converters: htmlConverters,
|
||||
data,
|
||||
})
|
||||
|
||||
return <div dangerouslySetInnerHTML={{ __html: html }} />
|
||||
}
|
||||
```
|
||||
|
||||
### Outputting HTML from the Collection
|
||||
|
||||
To add HTML generation directly within the collection, follow the example below:
|
||||
|
||||
```ts
|
||||
import type { HTMLConvertersFunction } from '@payloadcms/richtext-lexical/html'
|
||||
import type { MyTextBlock } from '@/payload-types.js'
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
import { HTMLConverterFeature, lexicalEditor, lexicalHTML } from '@payloadcms/richtext-lexical'
|
||||
import {
|
||||
BlocksFeature,
|
||||
type DefaultNodeTypes,
|
||||
lexicalEditor,
|
||||
lexicalHTMLField,
|
||||
type SerializedBlockNode,
|
||||
} from '@payloadcms/richtext-lexical'
|
||||
|
||||
const Pages: CollectionConfig = {
|
||||
slug: 'pages',
|
||||
@@ -91,71 +320,53 @@ const Pages: CollectionConfig = {
|
||||
{
|
||||
name: 'nameOfYourRichTextField',
|
||||
type: 'richText',
|
||||
editor: lexicalEditor(),
|
||||
},
|
||||
lexicalHTMLField({
|
||||
htmlFieldName: 'nameOfYourRichTextField_html',
|
||||
lexicalFieldName: 'nameOfYourRichTextField',
|
||||
}),
|
||||
{
|
||||
name: 'customRichText',
|
||||
type: 'richText',
|
||||
editor: lexicalEditor({
|
||||
features: ({ defaultFeatures }) => [
|
||||
...defaultFeatures,
|
||||
// The HTMLConverter Feature is the feature which manages the HTML serializers.
|
||||
// If you do not pass any arguments to it, it will use the default serializers.
|
||||
HTMLConverterFeature({}),
|
||||
BlocksFeature({
|
||||
blocks: [
|
||||
{
|
||||
interfaceName: 'MyTextBlock',
|
||||
slug: 'myTextBlock',
|
||||
fields: [
|
||||
{
|
||||
name: 'text',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
},
|
||||
lexicalHTML('nameOfYourRichTextField', { name: 'nameOfYourRichTextField_html' }),
|
||||
lexicalHTMLField({
|
||||
htmlFieldName: 'customRichText_html',
|
||||
lexicalFieldName: 'customRichText',
|
||||
// can pass in additional converters or override default ones
|
||||
converters: (({ defaultConverters }) => ({
|
||||
...defaultConverters,
|
||||
blocks: {
|
||||
myTextBlock: ({ node, providedCSSString }) =>
|
||||
`<div style="background-color: red;${providedCSSString}">${node.fields.text}</div>`,
|
||||
},
|
||||
})) as HTMLConvertersFunction<DefaultNodeTypes | SerializedBlockNode<MyTextBlock>>,
|
||||
}),
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
The `lexicalHTML()` function creates a new field that automatically converts the referenced lexical richText field into HTML through an afterRead hook.
|
||||
|
||||
### Generating HTML anywhere on the server
|
||||
|
||||
If you wish to convert JSON to HTML ad-hoc, use the `convertLexicalToHTML` function:
|
||||
|
||||
```ts
|
||||
import { consolidateHTMLConverters, convertLexicalToHTML } from '@payloadcms/richtext-lexical'
|
||||
|
||||
|
||||
await convertLexicalToHTML({
|
||||
converters: consolidateHTMLConverters({ editorConfig }),
|
||||
data: editorData,
|
||||
payload, // if you have Payload but no req available, pass it in here to enable server-only functionality (e.g. proper conversion of upload nodes)
|
||||
req, // if you have req available, pass it in here to enable server-only functionality (e.g. proper conversion of upload nodes). No need to pass in Payload if req is passed in.
|
||||
})
|
||||
```
|
||||
This method employs `convertLexicalToHTML` from `@payloadcms/richtext-lexical`, which converts the serialized editor state into HTML.
|
||||
|
||||
Because every `Feature` is able to provide html converters, and because the `htmlFeature` can modify those or provide their own, we need to consolidate them with the default html Converters using the `consolidateHTMLConverters` function.
|
||||
|
||||
#### Example: Generating HTML within an afterRead hook
|
||||
|
||||
```ts
|
||||
import type { FieldHook } from 'payload'
|
||||
|
||||
import {
|
||||
HTMLConverterFeature,
|
||||
consolidateHTMLConverters,
|
||||
convertLexicalToHTML,
|
||||
defaultEditorConfig,
|
||||
defaultEditorFeatures,
|
||||
sanitizeServerEditorConfig,
|
||||
} from '@payloadcms/richtext-lexical'
|
||||
|
||||
const hook: FieldHook = async ({ req, siblingData }) => {
|
||||
const editorConfig = defaultEditorConfig
|
||||
|
||||
editorConfig.features = [...defaultEditorFeatures, HTMLConverterFeature({})]
|
||||
|
||||
const sanitizedEditorConfig = await sanitizeServerEditorConfig(editorConfig, req.payload.config)
|
||||
|
||||
const html = await convertLexicalToHTML({
|
||||
converters: consolidateHTMLConverters({ editorConfig: sanitizedEditorConfig }),
|
||||
data: siblingData.lexicalSimple,
|
||||
req,
|
||||
})
|
||||
return html
|
||||
}
|
||||
```
|
||||
|
||||
### CSS
|
||||
|
||||
Payload's lexical HTML converter does not generate CSS for you, but it does add classes to the generated HTML. You can use these classes to style the HTML in your frontend.
|
||||
@@ -169,141 +380,85 @@ Here is some "base" CSS you can use to ensure that nested lists render correctly
|
||||
}
|
||||
```
|
||||
|
||||
### Creating your own HTML Converter
|
||||
|
||||
HTML Converters are typed as `HTMLConverter`, which contains the node type it should handle, and a function that accepts the serialized node from the lexical editor, and outputs the HTML string. Here's the HTML Converter of the Upload node as an example:
|
||||
|
||||
```ts
|
||||
import type { HTMLConverter } from '@payloadcms/richtext-lexical'
|
||||
|
||||
const UploadHTMLConverter: HTMLConverter<SerializedUploadNode> = {
|
||||
converter: async ({ node, req }) => {
|
||||
const uploadDocument: {
|
||||
value?: any
|
||||
} = {}
|
||||
if(req) {
|
||||
await populate({
|
||||
id,
|
||||
collectionSlug: node.relationTo,
|
||||
currentDepth: 0,
|
||||
data: uploadDocument,
|
||||
depth: 1,
|
||||
draft: false,
|
||||
key: 'value',
|
||||
overrideAccess: false,
|
||||
req,
|
||||
showHiddenFields: false,
|
||||
})
|
||||
}
|
||||
|
||||
const url = (req?.payload?.config?.serverURL || '') + uploadDocument?.value?.url
|
||||
|
||||
if (!(uploadDocument?.value?.mimeType as string)?.startsWith('image')) {
|
||||
// Only images can be serialized as HTML
|
||||
return ``
|
||||
}
|
||||
|
||||
return `<img src="${url}" alt="${uploadDocument?.value?.filename}" width="${uploadDocument?.value?.width}" height="${uploadDocument?.value?.height}"/>`
|
||||
},
|
||||
nodeTypes: [UploadNode.getType()], // This is the type of the lexical node that this converter can handle. Instead of hardcoding 'upload' we can get the node type directly from the UploadNode, since it's static.
|
||||
}
|
||||
```
|
||||
|
||||
As you can see, we have access to all the information saved in the node (for the Upload node, this is `value`and `relationTo`) and we can use that to generate the HTML.
|
||||
|
||||
The `convertLexicalToHTML` is part of `@payloadcms/richtext-lexical` automatically handles traversing the editor state and calling the correct converter for each node.
|
||||
|
||||
### Embedding the HTML Converter in your Feature
|
||||
|
||||
You can embed your HTML Converter directly within your custom `ServerFeature`, allowing it to be handled automatically by the `consolidateHTMLConverters` function. Here is an example:
|
||||
|
||||
```ts
|
||||
import { createNode } from '@payloadcms/richtext-lexical'
|
||||
import type { FeatureProviderProviderServer } from '@payloadcms/richtext-lexical'
|
||||
|
||||
export const UploadFeature: FeatureProviderProviderServer<
|
||||
UploadFeatureProps,
|
||||
UploadFeaturePropsClient
|
||||
> = (props) => {
|
||||
/*...*/
|
||||
return {
|
||||
feature: () => {
|
||||
return {
|
||||
nodes: [
|
||||
createNode({
|
||||
converters: {
|
||||
html: yourHTMLConverter, // <= This is where you define your HTML Converter
|
||||
},
|
||||
node: UploadNode,
|
||||
//...
|
||||
}),
|
||||
],
|
||||
ClientComponent: UploadFeatureClientComponent,
|
||||
clientFeatureProps: clientProps,
|
||||
serverFeatureProps: props,
|
||||
/*...*/
|
||||
}
|
||||
},
|
||||
key: 'upload',
|
||||
serverFeatureProps: props,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Headless Editor
|
||||
|
||||
Lexical provides a seamless way to perform conversions between various other formats:
|
||||
|
||||
- HTML to Lexical (or, importing HTML into the lexical editor)
|
||||
- Markdown to Lexical (or, importing Markdown into the lexical editor)
|
||||
- HTML to Lexical
|
||||
- Markdown to Lexical
|
||||
- Lexical to Markdown
|
||||
|
||||
A headless editor can perform such conversions outside of the main editor instance. Follow this method to initiate a headless editor:
|
||||
|
||||
```ts
|
||||
import { createHeadlessEditor } from '@payloadcms/richtext-lexical/lexical/headless'
|
||||
import { getEnabledNodes, sanitizeServerEditorConfig } from '@payloadcms/richtext-lexical'
|
||||
import { getEnabledNodes, editorConfigFactory } from '@payloadcms/richtext-lexical'
|
||||
|
||||
const yourEditorConfig // <= your editor config here
|
||||
const payloadConfig // <= your Payload Config here
|
||||
|
||||
const headlessEditor = createHeadlessEditor({
|
||||
nodes: getEnabledNodes({
|
||||
editorConfig: sanitizeServerEditorConfig(yourEditorConfig, payloadConfig),
|
||||
editorConfig: await editorConfigFactory.default({config: payloadConfig})
|
||||
}),
|
||||
})
|
||||
```
|
||||
|
||||
### Getting the editor config
|
||||
|
||||
As you can see, you need to provide an editor config in order to create a headless editor. This is because the editor config is used to determine which nodes & features are enabled, and which converters are used.
|
||||
You need to provide an editor config in order to create a headless editor. This is because the editor config is used to determine which nodes & features are enabled, and which converters are used.
|
||||
|
||||
To get the editor config, simply import the default editor config and adjust it - just like you did inside of the `editor: lexicalEditor({})` property:
|
||||
To get the editor config, import the `editorConfigFactory` factory - this factory provides a variety of ways to get the editor config, depending on your use case.
|
||||
|
||||
```ts
|
||||
import { defaultEditorConfig, defaultEditorFeatures } from '@payloadcms/richtext-lexical' // <= make sure this package is installed
|
||||
import type { SanitizedConfig } from 'payload'
|
||||
|
||||
const yourEditorConfig = defaultEditorConfig
|
||||
import {
|
||||
editorConfigFactory,
|
||||
FixedToolbarFeature,
|
||||
lexicalEditor,
|
||||
} from '@payloadcms/richtext-lexical'
|
||||
|
||||
// If you made changes to the features of the field's editor config, you should also make those changes here:
|
||||
yourEditorConfig.features = [
|
||||
...defaultEditorFeatures,
|
||||
// Add your custom features here
|
||||
]
|
||||
// Your config needs to be available in order to retrieve the default editor config
|
||||
const config: SanitizedConfig = {} as SanitizedConfig
|
||||
|
||||
// Version 1 - use the default editor config
|
||||
const yourEditorConfig = await editorConfigFactory.default({ config })
|
||||
|
||||
// Version 2 - if you have access to a lexical fields, you can extract the editor config from it
|
||||
const yourEditorConfig2 = editorConfigFactory.fromField({
|
||||
field: collectionConfig.fields[1],
|
||||
})
|
||||
|
||||
// Version 3 - create a new editor config - behaves just like instantiating a new `lexicalEditor`
|
||||
const yourEditorConfig3 = await editorConfigFactory.fromFeatures({
|
||||
config,
|
||||
features: ({ defaultFeatures }) => [...defaultFeatures, FixedToolbarFeature()],
|
||||
})
|
||||
|
||||
// Version 4 - if you have instantiated a lexical editor and are accessing it outside a field (=> this is the unsanitized editor),
|
||||
// you can extract the editor config from it.
|
||||
// This is common if you define the editor in a re-usable module scope variable and pass it to the richText field.
|
||||
// This is the least efficient way to get the editor config, and not recommended. It is recommended to extract the `features` arg
|
||||
// into a separate variable and use `fromFeatures` instead.
|
||||
const editor = lexicalEditor({
|
||||
features: ({ defaultFeatures }) => [...defaultFeatures, FixedToolbarFeature()],
|
||||
})
|
||||
|
||||
const yourEditorConfig4 = await editorConfigFactory.fromEditor({
|
||||
config,
|
||||
editor,
|
||||
})
|
||||
```
|
||||
|
||||
### Getting the editor config from an existing field
|
||||
### Example - Getting the editor config from an existing field
|
||||
|
||||
If you have access to the sanitized collection config, you can get access to the lexical sanitized editor config & features, as every lexical richText field returns it. Here is an example how you can get it from another field's afterRead hook:
|
||||
|
||||
```ts
|
||||
import type { CollectionConfig, RichTextField } from 'payload'
|
||||
|
||||
import { editorConfigFactory, getEnabledNodes, lexicalEditor } from '@payloadcms/richtext-lexical'
|
||||
import { createHeadlessEditor } from '@payloadcms/richtext-lexical/lexical/headless'
|
||||
import type { LexicalRichTextAdapter, SanitizedServerEditorConfig } from '@payloadcms/richtext-lexical'
|
||||
import {
|
||||
getEnabledNodes,
|
||||
lexicalEditor
|
||||
} from '@payloadcms/richtext-lexical'
|
||||
|
||||
export const MyCollection: CollectionConfig = {
|
||||
slug: 'slug',
|
||||
@@ -313,20 +468,18 @@ export const MyCollection: CollectionConfig = {
|
||||
type: 'text',
|
||||
hooks: {
|
||||
afterRead: [
|
||||
({ value, collection }) => {
|
||||
const otherRichTextField: RichTextField = collection.fields.find(
|
||||
({ siblingFields, value }) => {
|
||||
const field: RichTextField = siblingFields.find(
|
||||
(field) => 'name' in field && field.name === 'richText',
|
||||
) as RichTextField
|
||||
|
||||
const lexicalAdapter: LexicalRichTextAdapter =
|
||||
otherRichTextField.editor as LexicalRichTextAdapter
|
||||
|
||||
const sanitizedServerEditorConfig: SanitizedServerEditorConfig =
|
||||
lexicalAdapter.editorConfig
|
||||
const editorConfig = editorConfigFactory.fromField({
|
||||
field,
|
||||
})
|
||||
|
||||
const headlessEditor = createHeadlessEditor({
|
||||
nodes: getEnabledNodes({
|
||||
editorConfig: sanitizedServerEditorConfig,
|
||||
editorConfig,
|
||||
}),
|
||||
})
|
||||
|
||||
@@ -340,37 +493,43 @@ export const MyCollection: CollectionConfig = {
|
||||
{
|
||||
name: 'richText',
|
||||
type: 'richText',
|
||||
editor: lexicalEditor({
|
||||
features,
|
||||
}),
|
||||
}
|
||||
]
|
||||
editor: lexicalEditor(),
|
||||
},
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
## HTML => Lexical
|
||||
|
||||
Once you have your headless editor instance, you can use it to convert HTML to Lexical:
|
||||
If you have access to the Payload Config and the lexical editor config, you can convert HTML to the lexical editor state with the following:
|
||||
|
||||
```ts
|
||||
import { $generateNodesFromDOM } from '@payloadcms/richtext-lexical/lexical/html'
|
||||
import { $getRoot, $getSelection } from '@payloadcms/richtext-lexical/lexical'
|
||||
import { convertHTMLToLexical, editorConfigFactory } from '@payloadcms/richtext-lexical'
|
||||
// Make sure you have jsdom and @types/jsdom installed
|
||||
import { JSDOM } from 'jsdom'
|
||||
|
||||
const html = convertHTMLToLexical({
|
||||
editorConfig: await editorConfigFactory.default({
|
||||
config, // <= make sure you have access to your Payload Config
|
||||
}),
|
||||
html: '<p>text</p>',
|
||||
JSDOM, // pass the JSDOM import. As it's a relatively large package, richtext-lexical does not include it by default.
|
||||
})
|
||||
```
|
||||
|
||||
## Markdown => Lexical
|
||||
|
||||
Convert markdown content to the Lexical editor format with the following:
|
||||
|
||||
```ts
|
||||
import { $convertFromMarkdownString, editorConfigFactory } from '@payloadcms/richtext-lexical'
|
||||
|
||||
const yourEditorConfig = await editorConfigFactory.default({ config })
|
||||
const markdown = `# Hello World`
|
||||
|
||||
headlessEditor.update(
|
||||
() => {
|
||||
// In a headless environment you can use a package such as JSDom to parse the HTML string.
|
||||
const dom = new JSDOM(htmlString)
|
||||
|
||||
// Once you have the DOM instance it's easy to generate LexicalNodes.
|
||||
const nodes = $generateNodesFromDOM(headlessEditor, dom.window.document)
|
||||
|
||||
// Select the root
|
||||
$getRoot().select()
|
||||
|
||||
// Insert them at a selection.
|
||||
const selection = $getSelection()
|
||||
selection.insertNodes(nodes)
|
||||
$convertFromMarkdownString(markdown, yourEditorConfig.features.markdownTransformers)
|
||||
},
|
||||
{ discrete: true },
|
||||
)
|
||||
@@ -390,27 +549,6 @@ This has been taken from the [lexical serialization & deserialization docs](http
|
||||
immediate reading of the updated state isn't necessary, you can omit the flag.
|
||||
</Banner>
|
||||
|
||||
## Markdown => Lexical
|
||||
|
||||
Convert markdown content to the Lexical editor format with the following:
|
||||
|
||||
```ts
|
||||
import { sanitizeServerEditorConfig, $convertFromMarkdownString } from '@payloadcms/richtext-lexical'
|
||||
|
||||
const yourSanitizedEditorConfig = sanitizeServerEditorConfig(yourEditorConfig, payloadConfig) // <= your editor config & Payload Config here
|
||||
const markdown = `# Hello World`
|
||||
|
||||
headlessEditor.update(
|
||||
() => {
|
||||
$convertFromMarkdownString(markdown, yourSanitizedEditorConfig.features.markdownTransformers)
|
||||
},
|
||||
{ discrete: true },
|
||||
)
|
||||
|
||||
// Do this if you then want to get the editor JSON
|
||||
const editorJSON = headlessEditor.getEditorState().toJSON()
|
||||
```
|
||||
|
||||
## Lexical => Markdown
|
||||
|
||||
Export content from the Lexical editor into Markdown format using these steps:
|
||||
@@ -421,11 +559,12 @@ Export content from the Lexical editor into Markdown format using these steps:
|
||||
Here's the code for it:
|
||||
|
||||
```ts
|
||||
import { $convertToMarkdownString } from '@payloadcms/richtext-lexical/lexical/markdown'
|
||||
import { sanitizeServerEditorConfig } from '@payloadcms/richtext-lexical'
|
||||
import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'
|
||||
|
||||
const yourSanitizedEditorConfig = sanitizeServerEditorConfig(yourEditorConfig, payloadConfig) // <= your editor config & Payload Config here
|
||||
import { editorConfigFactory } from '@payloadcms/richtext-lexical'
|
||||
import { $convertToMarkdownString } from '@payloadcms/richtext-lexical/lexical/markdown'
|
||||
|
||||
const yourEditorConfig = await editorConfigFactory.default({ config })
|
||||
const yourEditorState: SerializedEditorState // <= your current editor state here
|
||||
|
||||
// Import editor state into your headless editor
|
||||
@@ -434,7 +573,7 @@ try {
|
||||
() => {
|
||||
headlessEditor.setEditorState(headlessEditor.parseEditorState(yourEditorState))
|
||||
},
|
||||
{ discrete: true }, // This should commit the editor state immediately
|
||||
{ discrete: true }, // This should commit the editor state immediately
|
||||
)
|
||||
} catch (e) {
|
||||
logger.error({ err: e }, 'ERROR parsing editor state')
|
||||
@@ -443,7 +582,7 @@ try {
|
||||
// Export to markdown
|
||||
let markdown: string
|
||||
headlessEditor.getEditorState().read(() => {
|
||||
markdown = $convertToMarkdownString(yourSanitizedEditorConfig?.features?.markdownTransformers)
|
||||
markdown = $convertToMarkdownString(yourEditorConfig?.features?.markdownTransformers)
|
||||
})
|
||||
```
|
||||
|
||||
@@ -464,13 +603,13 @@ const yourEditorState: SerializedEditorState // <= your current editor state her
|
||||
|
||||
// Import editor state into your headless editor
|
||||
try {
|
||||
headlessEditor.update(
|
||||
headlessEditor.update(
|
||||
() => {
|
||||
headlessEditor.setEditorState(headlessEditor.parseEditorState(yourEditorState))
|
||||
},
|
||||
{ discrete: true }, // This should commit the editor state immediately
|
||||
{ discrete: true }, // This should commit the editor state immediately
|
||||
)
|
||||
} catch (e) {
|
||||
} catch (e) {
|
||||
logger.error({ err: e }, 'ERROR parsing editor state')
|
||||
}
|
||||
|
||||
|
||||
@@ -29,9 +29,9 @@ Using the BlocksFeature, you can add both inline blocks (= can be inserted into
|
||||
|
||||
### Example: Code Field Block with language picker
|
||||
|
||||
This example demonstrates how to create a custom code field block with a language picker using the `BlocksFeature`. Make sure to manually install `@payloadcms/ui`first.
|
||||
This example demonstrates how to create a custom code field block with a language picker using the `BlocksFeature`. First, make sure to explicitly install `@payloadcms/ui` in your project.
|
||||
|
||||
Field config:
|
||||
Field Config:
|
||||
|
||||
```ts
|
||||
import {
|
||||
@@ -91,7 +91,6 @@ CodeComponent.tsx:
|
||||
|
||||
```tsx
|
||||
'use client'
|
||||
|
||||
import type { CodeFieldClient, CodeFieldClientProps } from 'payload'
|
||||
|
||||
import { CodeField, useFormFields } from '@payloadcms/ui'
|
||||
@@ -105,6 +104,8 @@ const languageKeyToMonacoLanguageMap = {
|
||||
tsx: 'typescript',
|
||||
}
|
||||
|
||||
type Language = keyof typeof languageKeyToMonacoLanguageMap
|
||||
|
||||
export const Code: React.FC<CodeFieldClientProps> = ({
|
||||
autoComplete,
|
||||
field,
|
||||
@@ -118,10 +119,10 @@ export const Code: React.FC<CodeFieldClientProps> = ({
|
||||
}) => {
|
||||
const languageField = useFormFields(([fields]) => fields['language'])
|
||||
|
||||
const language: string =
|
||||
(languageField?.value as string) || (languageField.initialValue as string) || 'typescript'
|
||||
const language: Language =
|
||||
(languageField?.value as Language) || (languageField?.initialValue as Language) || 'ts'
|
||||
|
||||
const label = languages[language as keyof typeof languages]
|
||||
const label = languages[language]
|
||||
|
||||
const props: CodeFieldClient = useMemo<CodeFieldClient>(
|
||||
() => ({
|
||||
@@ -129,9 +130,10 @@ export const Code: React.FC<CodeFieldClientProps> = ({
|
||||
type: 'code',
|
||||
admin: {
|
||||
...field.admin,
|
||||
label,
|
||||
editorOptions: undefined,
|
||||
language: languageKeyToMonacoLanguageMap[language] || language,
|
||||
},
|
||||
label,
|
||||
}),
|
||||
[field, language, label],
|
||||
)
|
||||
|
||||
@@ -145,20 +145,20 @@ Here's an overview of all the included features:
|
||||
|
||||
| Feature Name | Included by default | Description |
|
||||
| --- | --- | --- |
|
||||
| **`BoldTextFeature`** | Yes | Handles the bold text format |
|
||||
| **`ItalicTextFeature`** | Yes | Handles the italic text format |
|
||||
| **`UnderlineTextFeature`** | Yes | Handles the underline text format |
|
||||
| **`StrikethroughTextFeature`** | Yes | Handles the strikethrough text format |
|
||||
| **`SubscriptTextFeature`** | Yes | Handles the subscript text format |
|
||||
| **`SuperscriptTextFeature`** | Yes | Handles the superscript text format |
|
||||
| **`InlineCodeTextFeature`** | Yes | Handles the inline-code text format |
|
||||
| **`BoldFeature`** | Yes | Handles the bold text format |
|
||||
| **`ItalicFeature`** | Yes | Handles the italic text format |
|
||||
| **`UnderlineFeature`** | Yes | Handles the underline text format |
|
||||
| **`StrikethroughFeature`** | Yes | Handles the strikethrough text format |
|
||||
| **`SubscriptFeature`** | Yes | Handles the subscript text format |
|
||||
| **`SuperscriptFeature`** | Yes | Handles the superscript text format |
|
||||
| **`InlineCodeFeature`** | Yes | Handles the inline-code text format |
|
||||
| **`ParagraphFeature`** | Yes | Handles paragraphs. Since they are already a key feature of lexical itself, this Feature mainly handles the Slash and Add-Block menu entries for paragraphs |
|
||||
| **`HeadingFeature`** | Yes | Adds Heading Nodes (by default, H1 - H6, but that can be customized) |
|
||||
| **`AlignFeature`** | Yes | Allows you to align text left, centered and right |
|
||||
| **`IndentFeature`** | Yes | Allows you to indent text with the tab key |
|
||||
| **`UnorderedListFeature`** | Yes | Adds unordered lists (ul) |
|
||||
| **`OrderedListFeature`** | Yes | Adds ordered lists (ol) |
|
||||
| **`CheckListFeature`** | Yes | Adds checklists |
|
||||
| **`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 |
|
||||
|
||||
@@ -185,6 +185,8 @@ The [Admin Panel](../admin/overview) will also automatically display all availab
|
||||
|
||||
Behind the scenes, Payload relies on [`sharp`](https://sharp.pixelplumbing.com/api-resize#resize) to perform its image resizing. You can specify additional options for `sharp` to use while resizing your images.
|
||||
|
||||
Note that for image resizing to work, `sharp` must be specified in your [Payload Config](../configuration/overview). This is configured by default if you created your Payload project with `create-payload-app`. See `sharp` in [Config Options](../configuration/overview#config-options).
|
||||
|
||||
#### Accessing the resized images in hooks
|
||||
|
||||
All auto-resized images are exposed to be re-used in hooks and similar via an object that is bound to `req.payloadUploadSizes`.
|
||||
@@ -347,6 +349,32 @@ fetch('api/:upload-slug', {
|
||||
})
|
||||
```
|
||||
|
||||
## Uploading Files stored locally
|
||||
|
||||
If you want to upload a file stored on your machine directly using the `payload.create` method, for example, during a seed script,
|
||||
you can use the `filePath` property to specify the local path of the file.
|
||||
|
||||
```ts
|
||||
const localFilePath = path.resolve(__dirname, filename)
|
||||
|
||||
await payload.create({
|
||||
collection: 'media',
|
||||
data: {
|
||||
alt,
|
||||
},
|
||||
filePath: localFilePath,
|
||||
})
|
||||
```
|
||||
|
||||
The `data` property should still include all the required fields of your `media` collection.
|
||||
|
||||
<Banner type="warning">
|
||||
**Important:**
|
||||
|
||||
Remember that all custom hooks attached to the `media` collection will still trigger.
|
||||
Ensure that files match the specified mimeTypes or sizes defined in the collection's `formatOptions` or custom `hooks`.
|
||||
</Banner>
|
||||
|
||||
## Uploading Files from Remote URLs
|
||||
|
||||
The `pasteURL` option allows users to fetch files from remote URLs by pasting them into an Upload field. This option is **enabled by default** and can be configured to either **allow unrestricted client-side fetching** or **restrict server-side fetching** to specific trusted domains.
|
||||
|
||||
@@ -30,6 +30,7 @@ pnpm add @payloadcms/storage-vercel-blob
|
||||
- Configure the `collections` object to specify which collections should use the Vercel Blob adapter. The slug _must_ match one of your existing collection slugs.
|
||||
- Ensure you have `BLOB_READ_WRITE_TOKEN` set in your Vercel environment variables. This is usually set by Vercel automatically after adding blob storage to your project.
|
||||
- When enabled, this package will automatically set `disableLocalStorage` to `true` for each collection.
|
||||
- When deploying to Vercel, server uploads are limited with 4.5MB. Set `clientUploads` to `true` to do uploads directly on the client.
|
||||
|
||||
```ts
|
||||
import { vercelBlobStorage } from '@payloadcms/storage-vercel-blob'
|
||||
@@ -64,6 +65,7 @@ export default buildConfig({
|
||||
| `addRandomSuffix` | Add a random suffix to the uploaded file name in Vercel Blob storage | `false` |
|
||||
| `cacheControlMaxAge` | Cache-Control max-age in seconds | `365 * 24 * 60 * 60` (1 Year) |
|
||||
| `token` | Vercel Blob storage read/write token | `''` |
|
||||
| `clientUploads` | Do uploads directly on the client to bypass limits on Vercel. | |
|
||||
|
||||
## S3 Storage
|
||||
[`@payloadcms/storage-s3`](https://www.npmjs.com/package/@payloadcms/storage-s3)
|
||||
@@ -79,6 +81,7 @@ pnpm add @payloadcms/storage-s3
|
||||
- Configure the `collections` object to specify which collections should use the S3 Storage adapter. The slug _must_ match one of your existing collection slugs.
|
||||
- The `config` object can be any [`S3ClientConfig`](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/s3) object (from [`@aws-sdk/client-s3`](https://github.com/aws/aws-sdk-js-v3)). _This is highly dependent on your AWS setup_. Check the AWS documentation for more information.
|
||||
- When enabled, this package will automatically set `disableLocalStorage` to `true` for each collection.
|
||||
- When deploying to Vercel, server uploads are limited with 4.5MB. Set `clientUploads` to `true` to do uploads directly on the client. You must allow CORS PUT method for the bucket to your website.
|
||||
|
||||
```ts
|
||||
import { s3Storage } from '@payloadcms/storage-s3'
|
||||
@@ -126,6 +129,7 @@ pnpm add @payloadcms/storage-azure
|
||||
|
||||
- Configure the `collections` object to specify which collections should use the Azure Blob adapter. The slug _must_ match one of your existing collection slugs.
|
||||
- When enabled, this package will automatically set `disableLocalStorage` to `true` for each collection.
|
||||
- When deploying to Vercel, server uploads are limited with 4.5MB. Set `clientUploads` to `true` to do uploads directly on the client. You must allow CORS PUT method to your website.
|
||||
|
||||
```ts
|
||||
import { azureStorage } from '@payloadcms/storage-azure'
|
||||
@@ -161,6 +165,7 @@ export default buildConfig({
|
||||
| `baseURL` | Base URL for the Azure Blob storage account | |
|
||||
| `connectionString` | Azure Blob storage connection string | |
|
||||
| `containerName` | Azure Blob storage container name | |
|
||||
| `clientUploads` | Do uploads directly on the client to bypass limits on Vercel. | |
|
||||
|
||||
## Google Cloud Storage
|
||||
[`@payloadcms/storage-gcs`](https://www.npmjs.com/package/@payloadcms/storage-gcs)
|
||||
@@ -175,6 +180,7 @@ pnpm add @payloadcms/storage-gcs
|
||||
|
||||
- Configure the `collections` object to specify which collections should use the Google Cloud Storage adapter. The slug _must_ match one of your existing collection slugs.
|
||||
- When enabled, this package will automatically set `disableLocalStorage` to `true` for each collection.
|
||||
- When deploying to Vercel, server uploads are limited with 4.5MB. Set `clientUploads` to `true` to do uploads directly on the client. You must allow CORS PUT method for the bucket to your website.
|
||||
|
||||
```ts
|
||||
import { gcsStorage } from '@payloadcms/storage-gcs'
|
||||
@@ -203,13 +209,14 @@ export default buildConfig({
|
||||
|
||||
### Configuration Options#gcs-configuration
|
||||
|
||||
| Option | Description | Default |
|
||||
| ------------- | --------------------------------------------------------------------------------------------------- | --------- |
|
||||
| `enabled` | Whether or not to enable the plugin | `true` |
|
||||
| `collections` | Collections to apply the storage to | |
|
||||
| `bucket` | The name of the bucket to use | |
|
||||
| `options` | Google Cloud Storage client configuration. See [Docs](https://github.com/googleapis/nodejs-storage) | |
|
||||
| `acl` | Access control list for files that are uploaded | `Private` |
|
||||
| Option | Description | Default |
|
||||
| --------------- | --------------------------------------------------------------------------------------------------- | --------- |
|
||||
| `enabled` | Whether or not to enable the plugin | `true` |
|
||||
| `collections` | Collections to apply the storage to | |
|
||||
| `bucket` | The name of the bucket to use | |
|
||||
| `options` | Google Cloud Storage client configuration. See [Docs](https://github.com/googleapis/nodejs-storage) | |
|
||||
| `acl` | Access control list for files that are uploaded | `Private` |
|
||||
| `clientUploads` | Do uploads directly on the client to bypass limits on Vercel. | |
|
||||
|
||||
|
||||
## Uploadthing Storage
|
||||
@@ -226,6 +233,7 @@ pnpm add @payloadcms/storage-uploadthing
|
||||
- Configure the `collections` object to specify which collections should use uploadthing. The slug _must_ match one of your existing collection slugs and be an `upload` type.
|
||||
- Get a token from Uploadthing and set it as `token` in the `options` object.
|
||||
- `acl` is optional and defaults to `public-read`.
|
||||
- When deploying to Vercel, server uploads are limited with 4.5MB. Set `clientUploads` to `true` to do uploads directly on the client.
|
||||
|
||||
```ts
|
||||
export default buildConfig({
|
||||
@@ -246,13 +254,14 @@ export default buildConfig({
|
||||
|
||||
### Configuration Options#uploadthing-configuration
|
||||
|
||||
| Option | Description | Default |
|
||||
| ---------------- | ----------------------------------------------- | ------------- |
|
||||
| `token` | Token from Uploadthing. Required. | |
|
||||
| `acl` | Access control list for files that are uploaded | `public-read` |
|
||||
| `logLevel` | Log level for Uploadthing | `info` |
|
||||
| `fetch` | Custom fetch function | `fetch` |
|
||||
| `defaultKeyType` | Default key type for file operations | `fileKey` |
|
||||
| Option | Description | Default |
|
||||
| ---------------- | ------------------------------------------------------------- | ------------- |
|
||||
| `token` | Token from Uploadthing. Required. | |
|
||||
| `acl` | Access control list for files that are uploaded | `public-read` |
|
||||
| `logLevel` | Log level for Uploadthing | `info` |
|
||||
| `fetch` | Custom fetch function | `fetch` |
|
||||
| `defaultKeyType` | Default key type for file operations | `fileKey` |
|
||||
| `clientUploads` | Do uploads directly on the client to bypass limits on Vercel. | |
|
||||
|
||||
|
||||
## Custom Storage Adapters
|
||||
|
||||
@@ -68,7 +68,7 @@ If your front-end is statically generated then you may also want to regenerate t
|
||||
|
||||
### Admin Bar
|
||||
|
||||
You might also want to render an admin bar on your front-end so that logged-in users can quickly navigate between the front-end and Payload as they're editing. For React apps, check out the official [Payload Admin Bar](https://github.com/payloadcms/payload-admin-bar). For other frameworks, simply hit the `/me` route with `credentials: 'include'` and render your own admin bar if the user is logged in.
|
||||
You might also want to render an admin bar on your front-end so that logged-in users can quickly navigate between the front-end and Payload as they're editing. For React apps, check out the official [Payload Admin Bar](https://github.com/payloadcms/payload/tree/main/packages/admin-bar). For other frameworks, simply hit the `/me` route with `credentials: 'include'` and render your own admin bar if the user is logged in.
|
||||
|
||||
### CORS
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
"start": "cross-env NODE_OPTIONS=--no-deprecation next start"
|
||||
},
|
||||
"dependencies": {
|
||||
"@payloadcms/admin-bar": "latest",
|
||||
"@payloadcms/db-mongodb": "latest",
|
||||
"@payloadcms/next": "latest",
|
||||
"@payloadcms/richtext-slate": "latest",
|
||||
@@ -25,7 +26,6 @@
|
||||
"graphql": "^16.9.0",
|
||||
"next": "^15.0.0",
|
||||
"payload": "latest",
|
||||
"payload-admin-bar": "^1.0.6",
|
||||
"react": "19.0.0",
|
||||
"react-dom": "19.0.0"
|
||||
},
|
||||
|
||||
1151
examples/draft-preview/pnpm-lock.yaml
generated
1151
examples/draft-preview/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,9 @@
|
||||
'use client'
|
||||
|
||||
import type { PayloadAdminBarProps } from 'payload-admin-bar'
|
||||
import type { PayloadAdminBarProps } from '@payloadcms/admin-bar'
|
||||
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { PayloadAdminBar } from 'payload-admin-bar'
|
||||
import { PayloadAdminBar } from '@payloadcms/admin-bar'
|
||||
import React, { useState } from 'react'
|
||||
|
||||
import { Gutter } from '../Gutter'
|
||||
@@ -42,7 +42,7 @@ export const AdminBar: React.FC<{
|
||||
user: classes.user,
|
||||
}}
|
||||
cmsURL={process.env.NEXT_PUBLIC_SERVER_URL}
|
||||
collection={collection}
|
||||
collectionSlug={collection}
|
||||
collectionLabels={{
|
||||
plural: collectionLabels[collection]?.plural || 'Pages',
|
||||
singular: collectionLabels[collection]?.singular || 'Page',
|
||||
|
||||
@@ -52,7 +52,7 @@ export const home: Page = {
|
||||
children: [{ text: 'Payload Admin Bar' }],
|
||||
linkType: 'custom',
|
||||
newTab: true,
|
||||
url: 'https://github.com/payloadcms/payload-admin-bar',
|
||||
url: 'https://github.com/payloadcms/payload/tree/main/packages/admin-bar',
|
||||
},
|
||||
{
|
||||
text: ' appear at the top of this site. This will allow you to seamlessly navigate between the two apps. Then, navigate to the ',
|
||||
|
||||
@@ -42,7 +42,6 @@
|
||||
"next": "^15.1.0",
|
||||
"next-intl": "^3.23.2",
|
||||
"payload": "latest",
|
||||
"payload-admin-bar": "^1.0.6",
|
||||
"prism-react-renderer": "^2.3.1",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
|
||||
5057
examples/localization/pnpm-lock.yaml
generated
5057
examples/localization/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -5,7 +5,6 @@ import { GeistMono } from 'geist/font/mono'
|
||||
import { GeistSans } from 'geist/font/sans'
|
||||
import React from 'react'
|
||||
|
||||
import { AdminBar } from '@/components/AdminBar'
|
||||
import { Footer } from '@/globals/Footer/Component'
|
||||
import { Header } from '@/globals/Header/Component'
|
||||
import { LivePreviewListener } from '@/components/LivePreviewListener'
|
||||
@@ -57,13 +56,7 @@ export default async function RootLayout({ children, params }: Args) {
|
||||
<body>
|
||||
<Providers>
|
||||
<NextIntlClientProvider messages={messages}>
|
||||
<AdminBar
|
||||
adminBarProps={{
|
||||
preview: isEnabled,
|
||||
}}
|
||||
/>
|
||||
<LivePreviewListener />
|
||||
|
||||
<Header locale={locale} />
|
||||
{children}
|
||||
<Footer locale={locale} />
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
@import '~@payloadcms/ui/scss';
|
||||
|
||||
.admin-bar {
|
||||
@include small-break {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import type { PayloadAdminBarProps } from 'payload-admin-bar'
|
||||
|
||||
import { cn } from '@/utilities/ui'
|
||||
import { useSelectedLayoutSegments } from 'next/navigation'
|
||||
import { PayloadAdminBar } from 'payload-admin-bar'
|
||||
import React, { useState } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
|
||||
import './index.scss'
|
||||
import { useTranslations } from 'next-intl'
|
||||
|
||||
const baseClass = 'admin-bar'
|
||||
|
||||
const collectionLabels = {
|
||||
pages: {
|
||||
plural: 'Pages',
|
||||
singular: 'Page',
|
||||
},
|
||||
posts: {
|
||||
plural: 'Posts',
|
||||
singular: 'Post',
|
||||
},
|
||||
projects: {
|
||||
plural: 'Projects',
|
||||
singular: 'Project',
|
||||
},
|
||||
}
|
||||
|
||||
export const AdminBar: React.FC<{
|
||||
adminBarProps?: PayloadAdminBarProps
|
||||
}> = (props) => {
|
||||
const { adminBarProps } = props || {}
|
||||
const segments = useSelectedLayoutSegments()
|
||||
const [show, setShow] = useState(false)
|
||||
const collection = collectionLabels?.[segments?.[1]] ? segments?.[1] : 'pages'
|
||||
const router = useRouter()
|
||||
const t = useTranslations()
|
||||
|
||||
const onAuthChange = React.useCallback((user) => {
|
||||
setShow(user?.id)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(baseClass, 'py-2 bg-black text-white', {
|
||||
block: show,
|
||||
hidden: !show,
|
||||
})}
|
||||
>
|
||||
<div className="container">
|
||||
<PayloadAdminBar
|
||||
{...adminBarProps}
|
||||
className="py-2 text-white"
|
||||
classNames={{
|
||||
controls: 'font-medium text-white',
|
||||
logo: 'text-white',
|
||||
user: 'text-white',
|
||||
}}
|
||||
cmsURL={process.env.NEXT_PUBLIC_SERVER_URL}
|
||||
collection={collection}
|
||||
collectionLabels={{
|
||||
plural: collectionLabels[collection]?.plural || 'Pages',
|
||||
singular: collectionLabels[collection]?.singular || 'Page',
|
||||
}}
|
||||
logo={<span>{t('dashboard')}</span>}
|
||||
onAuthChange={onAuthChange}
|
||||
onPreviewExit={() => {
|
||||
fetch('/next/exit-preview').then(() => {
|
||||
router.push('/')
|
||||
router.refresh()
|
||||
})
|
||||
}}
|
||||
style={{
|
||||
backgroundColor: 'transparent',
|
||||
padding: 0,
|
||||
position: 'relative',
|
||||
zIndex: 'unset',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
DATABASE_URI=mongodb://127.0.0.1/payload-example-multi-tenant
|
||||
POSTGRES_URL=postgres://127.0.0.1:5432/payload-example-multi-tenant
|
||||
PAYLOAD_SECRET=PAYLOAD_MULTI_TENANT_EXAMPLE_SECRET_KEY
|
||||
PAYLOAD_PUBLIC_SERVER_URL=http://localhost:3000
|
||||
SEED_DB=true
|
||||
@@ -46,12 +46,12 @@ 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: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:
|
||||
|
||||
```
|
||||
127.0.0.1 gold.test silver.test bronze.test
|
||||
127.0.0.1 gold.localhost silver.localhost bronze.localhost
|
||||
```
|
||||
|
||||
- #### Pages
|
||||
|
||||
2
examples/multi-tenant/next-env.d.ts
vendored
2
examples/multi-tenant/next-env.d.ts
vendored
@@ -2,4 +2,4 @@
|
||||
/// <reference types="next/image-types/global" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@payloadcms/db-mongodb": "latest",
|
||||
"@payloadcms/db-postgres": "^3.25.0",
|
||||
"@payloadcms/next": "latest",
|
||||
"@payloadcms/plugin-multi-tenant": "latest",
|
||||
"@payloadcms/richtext-lexical": "latest",
|
||||
|
||||
3995
examples/multi-tenant/pnpm-lock.yaml
generated
3995
examples/multi-tenant/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -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:3000/tenant-domains/login">
|
||||
http://gold.localhost:3000/tenant-domains/login
|
||||
</a>{' '}
|
||||
will show the tenant with the domain "gold.test".
|
||||
will show the tenant with the domain "gold.localhost".
|
||||
</p>
|
||||
|
||||
<h2>Slugs</h2>
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
import { TenantField as TenantField_1d0591e3cf4f332c83a86da13a0de59a } from '@payloadcms/plugin-multi-tenant/client'
|
||||
import { TenantSelector as TenantSelector_d6d5f193a167989e2ee7d14202901e62 } from '@payloadcms/plugin-multi-tenant/rsc'
|
||||
import { TenantSelectionProvider as TenantSelectionProvider_1d0591e3cf4f332c83a86da13a0de59a } from '@payloadcms/plugin-multi-tenant/client'
|
||||
import { TenantSelector as TenantSelector_1d0591e3cf4f332c83a86da13a0de59a } from '@payloadcms/plugin-multi-tenant/client'
|
||||
import { TenantSelectionProvider as TenantSelectionProvider_d6d5f193a167989e2ee7d14202901e62 } from '@payloadcms/plugin-multi-tenant/rsc'
|
||||
|
||||
export const importMap = {
|
||||
'@payloadcms/plugin-multi-tenant/client#TenantField':
|
||||
TenantField_1d0591e3cf4f332c83a86da13a0de59a,
|
||||
'@payloadcms/plugin-multi-tenant/rsc#TenantSelector':
|
||||
TenantSelector_d6d5f193a167989e2ee7d14202901e62,
|
||||
'@payloadcms/plugin-multi-tenant/client#TenantSelectionProvider':
|
||||
TenantSelectionProvider_1d0591e3cf4f332c83a86da13a0de59a,
|
||||
"@payloadcms/plugin-multi-tenant/client#TenantField": TenantField_1d0591e3cf4f332c83a86da13a0de59a,
|
||||
"@payloadcms/plugin-multi-tenant/client#TenantSelector": TenantSelector_1d0591e3cf4f332c83a86da13a0de59a,
|
||||
"@payloadcms/plugin-multi-tenant/rsc#TenantSelectionProvider": TenantSelectionProvider_d6d5f193a167989e2ee7d14202901e62
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import type { FieldHook } from 'payload'
|
||||
import type { FieldHook, Where } from 'payload'
|
||||
|
||||
import { ValidationError } from 'payload'
|
||||
|
||||
import { getUserTenantIDs } from '../../../utilities/getUserTenantIDs'
|
||||
import { extractID } from '@/utilities/extractID'
|
||||
|
||||
export const ensureUniqueSlug: FieldHook = async ({ data, originalDoc, req, value }) => {
|
||||
// if value is unchanged, skip validation
|
||||
@@ -10,26 +11,30 @@ export const ensureUniqueSlug: FieldHook = async ({ data, originalDoc, req, valu
|
||||
return value
|
||||
}
|
||||
|
||||
const incomingTenantID = typeof data?.tenant === 'object' ? data.tenant.id : data?.tenant
|
||||
const currentTenantID =
|
||||
typeof originalDoc?.tenant === 'object' ? originalDoc.tenant.id : originalDoc?.tenant
|
||||
const constraints: Where[] = [
|
||||
{
|
||||
slug: {
|
||||
equals: value,
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
const incomingTenantID = extractID(data?.tenant)
|
||||
const currentTenantID = extractID(originalDoc?.tenant)
|
||||
const tenantIDToMatch = incomingTenantID || currentTenantID
|
||||
|
||||
if (tenantIDToMatch) {
|
||||
constraints.push({
|
||||
tenant: {
|
||||
equals: tenantIDToMatch,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const findDuplicatePages = await req.payload.find({
|
||||
collection: 'pages',
|
||||
where: {
|
||||
and: [
|
||||
{
|
||||
tenant: {
|
||||
equals: tenantIDToMatch,
|
||||
},
|
||||
},
|
||||
{
|
||||
slug: {
|
||||
equals: value,
|
||||
},
|
||||
},
|
||||
],
|
||||
and: constraints,
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import type { User } from '@/payload-types'
|
||||
import type { Access, Where } from 'payload'
|
||||
|
||||
import { parseCookies } from 'payload'
|
||||
import { getTenantFromCookie } from '@payloadcms/plugin-multi-tenant/utilities'
|
||||
|
||||
import { isSuperAdmin } from '../../../access/isSuperAdmin'
|
||||
import { getUserTenantIDs } from '../../../utilities/getUserTenantIDs'
|
||||
import { isAccessingSelf } from './isAccessingSelf'
|
||||
import { getCollectionIDType } from '@/utilities/getCollectionIDType'
|
||||
|
||||
export const readAccess: Access<User> = ({ req, id }) => {
|
||||
if (!req?.user) {
|
||||
@@ -16,9 +16,11 @@ export const readAccess: Access<User> = ({ req, id }) => {
|
||||
return true
|
||||
}
|
||||
|
||||
const cookies = parseCookies(req.headers)
|
||||
const superAdmin = isSuperAdmin(req.user)
|
||||
const selectedTenant = cookies.get('payload-tenant')
|
||||
const selectedTenant = getTenantFromCookie(
|
||||
req.headers,
|
||||
getCollectionIDType({ payload: req.payload, collectionSlug: 'tenants' }),
|
||||
)
|
||||
const adminTenantAccessIDs = getUserTenantIDs(req.user, 'tenant-admin')
|
||||
|
||||
if (selectedTenant) {
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import type { FieldHook } from 'payload'
|
||||
import type { FieldHook, Where } from 'payload'
|
||||
|
||||
import { ValidationError } from 'payload'
|
||||
|
||||
import { getUserTenantIDs } from '../../../utilities/getUserTenantIDs'
|
||||
import { extractID } from '@/utilities/extractID'
|
||||
import { getTenantFromCookie } from '@payloadcms/plugin-multi-tenant/utilities'
|
||||
import { getCollectionIDType } from '@/utilities/getCollectionIDType'
|
||||
|
||||
export const ensureUniqueUsername: FieldHook = async ({ data, originalDoc, req, value }) => {
|
||||
// if value is unchanged, skip validation
|
||||
@@ -10,26 +13,31 @@ export const ensureUniqueUsername: FieldHook = async ({ data, originalDoc, req,
|
||||
return value
|
||||
}
|
||||
|
||||
const incomingTenantID = typeof data?.tenant === 'object' ? data.tenant.id : data?.tenant
|
||||
const currentTenantID =
|
||||
typeof originalDoc?.tenant === 'object' ? originalDoc.tenant.id : originalDoc?.tenant
|
||||
const tenantIDToMatch = incomingTenantID || currentTenantID
|
||||
const constraints: Where[] = [
|
||||
{
|
||||
username: {
|
||||
equals: value,
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
const selectedTenant = getTenantFromCookie(
|
||||
req.headers,
|
||||
getCollectionIDType({ payload: req.payload, collectionSlug: 'tenants' }),
|
||||
)
|
||||
|
||||
if (selectedTenant) {
|
||||
constraints.push({
|
||||
'tenants.tenant': {
|
||||
equals: selectedTenant,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const findDuplicateUsers = await req.payload.find({
|
||||
collection: 'users',
|
||||
where: {
|
||||
and: [
|
||||
{
|
||||
'tenants.tenant': {
|
||||
equals: tenantIDToMatch,
|
||||
},
|
||||
},
|
||||
{
|
||||
username: {
|
||||
equals: value,
|
||||
},
|
||||
},
|
||||
],
|
||||
and: constraints,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -39,7 +47,8 @@ export const ensureUniqueUsername: FieldHook = async ({ data, originalDoc, req,
|
||||
// provide a more specific error message
|
||||
if (req.user.roles?.includes('super-admin') || tenantIDs.length > 1) {
|
||||
const attemptedTenantChange = await req.payload.findByID({
|
||||
id: tenantIDToMatch,
|
||||
// @ts-ignore - selectedTenant will match DB ID type
|
||||
id: selectedTenant,
|
||||
collection: 'tenants',
|
||||
})
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { CollectionAfterLoginHook } from 'payload'
|
||||
|
||||
import { mergeHeaders } from '@payloadcms/next/utilities'
|
||||
import { generateCookie, getCookieExpiration } from 'payload'
|
||||
import { mergeHeaders, generateCookie, getCookieExpiration } from 'payload'
|
||||
|
||||
export const setCookieBasedOnDomain: CollectionAfterLoginHook = async ({ req, user }) => {
|
||||
const relatedOrg = await req.payload.find({
|
||||
@@ -22,7 +21,7 @@ export const setCookieBasedOnDomain: CollectionAfterLoginHook = async ({ req, us
|
||||
expires: getCookieExpiration({ seconds: 7200 }),
|
||||
path: '/',
|
||||
returnCookieAsObject: false,
|
||||
value: relatedOrg.docs[0].id,
|
||||
value: String(relatedOrg.docs[0].id),
|
||||
})
|
||||
|
||||
// Merge existing responseHeaders with the new Set-Cookie header
|
||||
|
||||
@@ -6,10 +6,65 @@
|
||||
* and re-run `payload generate:types` to regenerate this file.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Supported timezones in IANA format.
|
||||
*
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "supportedTimezones".
|
||||
*/
|
||||
export type SupportedTimezones =
|
||||
| 'Pacific/Midway'
|
||||
| 'Pacific/Niue'
|
||||
| 'Pacific/Honolulu'
|
||||
| 'Pacific/Rarotonga'
|
||||
| 'America/Anchorage'
|
||||
| 'Pacific/Gambier'
|
||||
| 'America/Los_Angeles'
|
||||
| 'America/Tijuana'
|
||||
| 'America/Denver'
|
||||
| 'America/Phoenix'
|
||||
| 'America/Chicago'
|
||||
| 'America/Guatemala'
|
||||
| 'America/New_York'
|
||||
| 'America/Bogota'
|
||||
| 'America/Caracas'
|
||||
| 'America/Santiago'
|
||||
| 'America/Buenos_Aires'
|
||||
| 'America/Sao_Paulo'
|
||||
| 'Atlantic/South_Georgia'
|
||||
| 'Atlantic/Azores'
|
||||
| 'Atlantic/Cape_Verde'
|
||||
| 'Europe/London'
|
||||
| 'Europe/Berlin'
|
||||
| 'Africa/Lagos'
|
||||
| 'Europe/Athens'
|
||||
| 'Africa/Cairo'
|
||||
| 'Europe/Moscow'
|
||||
| 'Asia/Riyadh'
|
||||
| 'Asia/Dubai'
|
||||
| 'Asia/Baku'
|
||||
| 'Asia/Karachi'
|
||||
| 'Asia/Tashkent'
|
||||
| 'Asia/Calcutta'
|
||||
| 'Asia/Dhaka'
|
||||
| 'Asia/Almaty'
|
||||
| 'Asia/Jakarta'
|
||||
| 'Asia/Bangkok'
|
||||
| 'Asia/Shanghai'
|
||||
| 'Asia/Singapore'
|
||||
| 'Asia/Tokyo'
|
||||
| 'Asia/Seoul'
|
||||
| 'Australia/Sydney'
|
||||
| 'Pacific/Guam'
|
||||
| 'Pacific/Noumea'
|
||||
| 'Pacific/Auckland'
|
||||
| 'Pacific/Fiji';
|
||||
|
||||
export interface Config {
|
||||
auth: {
|
||||
users: UserAuthOperations;
|
||||
};
|
||||
blocks: {};
|
||||
collections: {
|
||||
pages: Page;
|
||||
users: User;
|
||||
@@ -28,7 +83,7 @@ export interface Config {
|
||||
'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>;
|
||||
};
|
||||
db: {
|
||||
defaultIDType: string;
|
||||
defaultIDType: number;
|
||||
};
|
||||
globals: {};
|
||||
globalsSelect: {};
|
||||
@@ -64,8 +119,8 @@ export interface UserAuthOperations {
|
||||
* via the `definition` "pages".
|
||||
*/
|
||||
export interface Page {
|
||||
id: string;
|
||||
tenant?: (string | null) | Tenant;
|
||||
id: number;
|
||||
tenant?: (number | null) | Tenant;
|
||||
title?: string | null;
|
||||
slug?: string | null;
|
||||
updatedAt: string;
|
||||
@@ -76,7 +131,7 @@ export interface Page {
|
||||
* via the `definition` "tenants".
|
||||
*/
|
||||
export interface Tenant {
|
||||
id: string;
|
||||
id: number;
|
||||
name: string;
|
||||
/**
|
||||
* Used for domain-based tenant handling
|
||||
@@ -98,12 +153,12 @@ export interface Tenant {
|
||||
* via the `definition` "users".
|
||||
*/
|
||||
export interface User {
|
||||
id: string;
|
||||
id: number;
|
||||
roles?: ('super-admin' | 'user')[] | null;
|
||||
username?: string | null;
|
||||
tenants?:
|
||||
| {
|
||||
tenant: string | Tenant;
|
||||
tenant: number | Tenant;
|
||||
roles: ('tenant-admin' | 'tenant-viewer')[];
|
||||
id?: string | null;
|
||||
}[]
|
||||
@@ -124,24 +179,24 @@ export interface User {
|
||||
* via the `definition` "payload-locked-documents".
|
||||
*/
|
||||
export interface PayloadLockedDocument {
|
||||
id: string;
|
||||
id: number;
|
||||
document?:
|
||||
| ({
|
||||
relationTo: 'pages';
|
||||
value: string | Page;
|
||||
value: number | Page;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'users';
|
||||
value: string | User;
|
||||
value: number | User;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'tenants';
|
||||
value: string | Tenant;
|
||||
value: number | Tenant;
|
||||
} | null);
|
||||
globalSlug?: string | null;
|
||||
user: {
|
||||
relationTo: 'users';
|
||||
value: string | User;
|
||||
value: number | User;
|
||||
};
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
@@ -151,10 +206,10 @@ export interface PayloadLockedDocument {
|
||||
* via the `definition` "payload-preferences".
|
||||
*/
|
||||
export interface PayloadPreference {
|
||||
id: string;
|
||||
id: number;
|
||||
user: {
|
||||
relationTo: 'users';
|
||||
value: string | User;
|
||||
value: number | User;
|
||||
};
|
||||
key?: string | null;
|
||||
value?:
|
||||
@@ -174,7 +229,7 @@ export interface PayloadPreference {
|
||||
* via the `definition` "payload-migrations".
|
||||
*/
|
||||
export interface PayloadMigration {
|
||||
id: string;
|
||||
id: number;
|
||||
name?: string | null;
|
||||
batch?: number | null;
|
||||
updatedAt: string;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { mongooseAdapter } from '@payloadcms/db-mongodb'
|
||||
import { postgresAdapter } from '@payloadcms/db-postgres'
|
||||
import { lexicalEditor } from '@payloadcms/richtext-lexical'
|
||||
import path from 'path'
|
||||
import { buildConfig } from 'payload'
|
||||
@@ -11,6 +12,7 @@ import { multiTenantPlugin } from '@payloadcms/plugin-multi-tenant'
|
||||
import { isSuperAdmin } from './access/isSuperAdmin'
|
||||
import type { Config } from './payload-types'
|
||||
import { getUserTenantIDs } from './utilities/getUserTenantIDs'
|
||||
import { seed } from './seed'
|
||||
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
const dirname = path.dirname(filename)
|
||||
@@ -21,9 +23,19 @@ export default buildConfig({
|
||||
user: 'users',
|
||||
},
|
||||
collections: [Pages, Users, Tenants],
|
||||
db: mongooseAdapter({
|
||||
url: process.env.DATABASE_URI as string,
|
||||
// db: mongooseAdapter({
|
||||
// url: process.env.DATABASE_URI as string,
|
||||
// }),
|
||||
db: postgresAdapter({
|
||||
pool: {
|
||||
connectionString: process.env.POSTGRES_URL,
|
||||
},
|
||||
}),
|
||||
onInit: async (args) => {
|
||||
if (process.env.SEED_DB) {
|
||||
await seed(args)
|
||||
}
|
||||
},
|
||||
editor: lexicalEditor({}),
|
||||
graphQL: {
|
||||
schemaOutputFile: path.resolve(dirname, 'generated-schema.graphql'),
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import type { MigrateUpArgs } from '@payloadcms/db-mongodb'
|
||||
import { Config } from 'payload'
|
||||
|
||||
export async function up({ payload }: MigrateUpArgs): Promise<void> {
|
||||
export const seed: NonNullable<Config['onInit']> = async (payload): Promise<void> => {
|
||||
const tenant1 = await payload.create({
|
||||
collection: 'tenants',
|
||||
data: {
|
||||
name: 'Tenant 1',
|
||||
slug: 'gold',
|
||||
domain: 'gold.test',
|
||||
domain: 'gold.localhost',
|
||||
},
|
||||
})
|
||||
|
||||
@@ -15,7 +15,7 @@ export async function up({ payload }: MigrateUpArgs): Promise<void> {
|
||||
data: {
|
||||
name: 'Tenant 2',
|
||||
slug: 'silver',
|
||||
domain: 'silver.test',
|
||||
domain: 'silver.localhost',
|
||||
},
|
||||
})
|
||||
|
||||
@@ -24,7 +24,7 @@ export async function up({ payload }: MigrateUpArgs): Promise<void> {
|
||||
data: {
|
||||
name: 'Tenant 3',
|
||||
slug: 'bronze',
|
||||
domain: 'bronze.test',
|
||||
domain: 'bronze.localhost',
|
||||
},
|
||||
})
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import type { CollectionSlug, Payload } from 'payload'
|
||||
|
||||
type Args = {
|
||||
collectionSlug: CollectionSlug
|
||||
payload: Payload
|
||||
}
|
||||
export const getCollectionIDType = ({ collectionSlug, payload }: Args): 'number' | 'text' => {
|
||||
return payload.collections[collectionSlug]?.customIDType ?? payload.db.defaultIDType
|
||||
}
|
||||
@@ -12,53 +12,57 @@ const withBundleAnalyzer = bundleAnalyzer({
|
||||
})
|
||||
|
||||
const config = withBundleAnalyzer(
|
||||
withPayload({
|
||||
eslint: {
|
||||
ignoreDuringBuilds: true,
|
||||
},
|
||||
typescript: {
|
||||
ignoreBuildErrors: true,
|
||||
},
|
||||
experimental: {
|
||||
serverActions: {
|
||||
bodySizeLimit: '5mb',
|
||||
withPayload(
|
||||
{
|
||||
eslint: {
|
||||
ignoreDuringBuilds: true,
|
||||
},
|
||||
typescript: {
|
||||
ignoreBuildErrors: true,
|
||||
},
|
||||
experimental: {
|
||||
fullySpecified: true,
|
||||
serverActions: {
|
||||
bodySizeLimit: '5mb',
|
||||
},
|
||||
},
|
||||
env: {
|
||||
PAYLOAD_CORE_DEV: 'true',
|
||||
ROOT_DIR: path.resolve(dirname),
|
||||
// @todo remove in 4.0 - will behave like this by default in 4.0
|
||||
PAYLOAD_DO_NOT_SANITIZE_LOCALIZED_PROPERTY: 'true',
|
||||
},
|
||||
async redirects() {
|
||||
return [
|
||||
{
|
||||
destination: '/admin',
|
||||
permanent: false,
|
||||
source: '/',
|
||||
},
|
||||
]
|
||||
},
|
||||
images: {
|
||||
domains: ['localhost'],
|
||||
},
|
||||
webpack: (webpackConfig) => {
|
||||
webpackConfig.resolve.extensionAlias = {
|
||||
'.cjs': ['.cts', '.cjs'],
|
||||
'.js': ['.ts', '.tsx', '.js', '.jsx'],
|
||||
'.mjs': ['.mts', '.mjs'],
|
||||
}
|
||||
|
||||
// Ignore sentry warnings when not wrapped with withSentryConfig
|
||||
webpackConfig.ignoreWarnings = [
|
||||
...(webpackConfig.ignoreWarnings ?? []),
|
||||
{ file: /esm\/platform\/node\/instrumentation.js/ },
|
||||
{ module: /esm\/platform\/node\/instrumentation.js/ },
|
||||
]
|
||||
|
||||
return webpackConfig
|
||||
},
|
||||
},
|
||||
env: {
|
||||
PAYLOAD_CORE_DEV: 'true',
|
||||
ROOT_DIR: path.resolve(dirname),
|
||||
// @todo remove in 4.0 - will behave like this by default in 4.0
|
||||
PAYLOAD_DO_NOT_SANITIZE_LOCALIZED_PROPERTY: 'true',
|
||||
},
|
||||
async redirects() {
|
||||
return [
|
||||
{
|
||||
destination: '/admin',
|
||||
permanent: false,
|
||||
source: '/',
|
||||
},
|
||||
]
|
||||
},
|
||||
images: {
|
||||
domains: ['localhost'],
|
||||
},
|
||||
webpack: (webpackConfig) => {
|
||||
webpackConfig.resolve.extensionAlias = {
|
||||
'.cjs': ['.cts', '.cjs'],
|
||||
'.js': ['.ts', '.tsx', '.js', '.jsx'],
|
||||
'.mjs': ['.mts', '.mjs'],
|
||||
}
|
||||
|
||||
// Ignore sentry warnings when not wrapped with withSentryConfig
|
||||
webpackConfig.ignoreWarnings = [
|
||||
...(webpackConfig.ignoreWarnings ?? []),
|
||||
{ file: /esm\/platform\/node\/instrumentation.js/ },
|
||||
{ module: /esm\/platform\/node\/instrumentation.js/ },
|
||||
]
|
||||
|
||||
return webpackConfig
|
||||
},
|
||||
}),
|
||||
{ devBundleServerPackages: false },
|
||||
),
|
||||
)
|
||||
|
||||
export default process.env.NEXT_PUBLIC_SENTRY_DSN
|
||||
|
||||
19
package.json
19
package.json
@@ -1,11 +1,12 @@
|
||||
{
|
||||
"name": "payload-monorepo",
|
||||
"version": "3.24.0",
|
||||
"version": "3.28.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"bf": "pnpm run build:force",
|
||||
"build": "pnpm run build:core",
|
||||
"build:admin-bar": "turbo build --filter \"@payloadcms/admin-bar\"",
|
||||
"build:all": "turbo build",
|
||||
"build:app": "next build",
|
||||
"build:app:analyze": "cross-env ANALYZE=true next build",
|
||||
@@ -33,6 +34,7 @@
|
||||
"build:payload-cloud": "turbo build --filter \"@payloadcms/payload-cloud\"",
|
||||
"build:plugin-cloud-storage": "turbo build --filter \"@payloadcms/plugin-cloud-storage\"",
|
||||
"build:plugin-form-builder": "turbo build --filter \"@payloadcms/plugin-form-builder\"",
|
||||
"build:plugin-import-export": "turbo build --filter \"@payloadcms/plugin-import-export\"",
|
||||
"build:plugin-multi-tenant": "turbo build --filter \"@payloadcms/plugin-multi-tenant\"",
|
||||
"build:plugin-nested-docs": "turbo build --filter \"@payloadcms/plugin-nested-docs\"",
|
||||
"build:plugin-redirects": "turbo build --filter \"@payloadcms/plugin-redirects\"",
|
||||
@@ -79,10 +81,9 @@
|
||||
"prepare": "husky",
|
||||
"prepare-run-test-against-prod": "pnpm bf && rm -rf test/packed && rm -rf test/node_modules && rm -rf app && rm -f test/pnpm-lock.yaml && pnpm run script:pack --all --no-build --dest test/packed && pnpm runts test/setupProd.ts && cd test && pnpm i --ignore-workspace && cd ..",
|
||||
"prepare-run-test-against-prod:ci": "rm -rf test/node_modules && rm -rf app && rm -f test/pnpm-lock.yaml && pnpm run script:pack --all --no-build --dest test/packed && pnpm runts test/setupProd.ts && cd test && pnpm i --ignore-workspace && cd ..",
|
||||
"publish-prerelease": "pnpm --filter releaser publish-prerelease",
|
||||
"reinstall": "pnpm clean:all && pnpm install",
|
||||
"release": "pnpm --filter releaser release --tag latest",
|
||||
"release:beta": "pnpm runts ./scripts/release.ts --bump prerelease --tag beta",
|
||||
"release:canary": "pnpm --filter releaser release:canary",
|
||||
"runts": "cross-env NODE_OPTIONS=--no-deprecation node --no-deprecation --import @swc-node/register/esm-register",
|
||||
"script:build-template-with-local-pkgs": "pnpm --filter scripts build-template-with-local-pkgs",
|
||||
"script:gen-templates": "pnpm --filter scripts gen-templates",
|
||||
@@ -117,7 +118,7 @@
|
||||
"devDependencies": {
|
||||
"@jest/globals": "29.7.0",
|
||||
"@libsql/client": "0.14.0",
|
||||
"@next/bundle-analyzer": "15.1.5",
|
||||
"@next/bundle-analyzer": "15.2.2",
|
||||
"@payloadcms/db-postgres": "workspace:*",
|
||||
"@payloadcms/eslint-config": "workspace:*",
|
||||
"@payloadcms/eslint-plugin": "workspace:*",
|
||||
@@ -132,8 +133,8 @@
|
||||
"@types/jest": "29.5.12",
|
||||
"@types/minimist": "1.2.5",
|
||||
"@types/node": "22.5.4",
|
||||
"@types/react": "19.0.1",
|
||||
"@types/react-dom": "19.0.1",
|
||||
"@types/react": "19.0.10",
|
||||
"@types/react-dom": "19.0.4",
|
||||
"@types/shelljs": "0.8.15",
|
||||
"chalk": "^4.1.2",
|
||||
"comment-json": "^4.2.3",
|
||||
@@ -153,7 +154,7 @@
|
||||
"lint-staged": "15.2.7",
|
||||
"minimist": "1.2.8",
|
||||
"mongodb-memory-server": "^10",
|
||||
"next": "15.1.5",
|
||||
"next": "15.2.2",
|
||||
"open": "^10.1.0",
|
||||
"p-limit": "^5.0.0",
|
||||
"playwright": "1.50.0",
|
||||
@@ -173,10 +174,6 @@
|
||||
"turbo": "^2.3.3",
|
||||
"typescript": "5.7.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^19.0.0 || ^19.0.0-rc-65a56d0e-20241020",
|
||||
"react-dom": "^19.0.0 || ^19.0.0-rc-65a56d0e-20241020"
|
||||
},
|
||||
"packageManager": "pnpm@9.7.1",
|
||||
"engines": {
|
||||
"node": "^18.20.2 || >=20.9.0",
|
||||
|
||||
10
packages/admin-bar/.prettierignore
Normal file
10
packages/admin-bar/.prettierignore
Normal file
@@ -0,0 +1,10 @@
|
||||
.tmp
|
||||
**/.git
|
||||
**/.hg
|
||||
**/.pnp.*
|
||||
**/.svn
|
||||
**/.yarn/**
|
||||
**/build
|
||||
**/dist/**
|
||||
**/node_modules
|
||||
**/temp
|
||||
24
packages/admin-bar/.swcrc
Normal file
24
packages/admin-bar/.swcrc
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/swcrc",
|
||||
"sourceMaps": true,
|
||||
"jsc": {
|
||||
"target": "esnext",
|
||||
"parser": {
|
||||
"syntax": "typescript",
|
||||
"tsx": true,
|
||||
"dts": true
|
||||
},
|
||||
"transform": {
|
||||
"react": {
|
||||
"runtime": "automatic",
|
||||
"pragmaFrag": "React.Fragment",
|
||||
"throwIfNamespace": true,
|
||||
"development": false,
|
||||
"useBuiltins": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"module": {
|
||||
"type": "es6"
|
||||
}
|
||||
}
|
||||
22
packages/admin-bar/LICENSE.md
Normal file
22
packages/admin-bar/LICENSE.md
Normal file
@@ -0,0 +1,22 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2018-2025 Payload CMS, Inc. <info@payloadcms.com>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
'Software'), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
119
packages/admin-bar/README.md
Normal file
119
packages/admin-bar/README.md
Normal file
@@ -0,0 +1,119 @@
|
||||
# Payload Admin Bar
|
||||
|
||||
An admin bar for React apps using [Payload](https://github.com/payloadcms/payload).
|
||||
|
||||
### Installation
|
||||
|
||||
```bash
|
||||
pnpm i @payloadcms/admin-bar
|
||||
```
|
||||
|
||||
### Basic Usage
|
||||
|
||||
```jsx
|
||||
import { PayloadAdminBar } from '@payloadcms/admin-bar'
|
||||
|
||||
export const App = () => {
|
||||
return <PayloadAdminBar cmsURL="https://cms.website.com" collection="pages" id="12345" />
|
||||
}
|
||||
```
|
||||
|
||||
Checks for authentication with Payload CMS by hitting the [`/me`](https://payloadcms.com/docs/authentication/operations#me) route. If authenticated, renders an admin bar with simple controls to do the following:
|
||||
|
||||
- Navigate to the admin dashboard
|
||||
- Navigate to the currently logged-in user's account
|
||||
- Edit the current collection
|
||||
- Create a new collection of the same type
|
||||
- Logout
|
||||
- Indicate and exit preview mode
|
||||
|
||||
The admin bar ships with very little style and is fully customizable.
|
||||
|
||||
### Dynamic props
|
||||
|
||||
With client-side routing, we need to update the admin bar with a new collection type and document id on each route change. This will depend on your app's specific setup, but here are a some common examples:
|
||||
|
||||
#### NextJS
|
||||
|
||||
For NextJS apps using dynamic-routes, use `getStaticProps`:
|
||||
|
||||
```ts
|
||||
export const getStaticProps = async ({ params: { slug } }) => {
|
||||
const props = {}
|
||||
|
||||
const pageReq = await fetch(
|
||||
`https://cms.website.com/api/pages?where[slug][equals]=${slug}&depth=1`,
|
||||
)
|
||||
const pageData = await pageReq.json()
|
||||
|
||||
if (pageReq.ok) {
|
||||
const { docs } = pageData
|
||||
const [doc] = docs
|
||||
|
||||
props = {
|
||||
...doc,
|
||||
collection: 'pages',
|
||||
collectionLabels: {
|
||||
singular: 'page',
|
||||
plural: 'pages',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return props
|
||||
}
|
||||
```
|
||||
|
||||
Now your app can forward these props onto the admin bar. Something like this:
|
||||
|
||||
```ts
|
||||
import { PayloadAdminBar } from '@payloadcms/admin-bar';
|
||||
|
||||
export const App = (appProps) => {
|
||||
const {
|
||||
pageProps: {
|
||||
collection,
|
||||
collectionLabels,
|
||||
id
|
||||
}
|
||||
} = appProps;
|
||||
|
||||
return (
|
||||
<PayloadAdminBar
|
||||
{...{
|
||||
cmsURL: 'https://cms.website.com',
|
||||
collection,
|
||||
collectionLabels,
|
||||
id
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Props
|
||||
|
||||
| Property | Type | Required | Default | Description |
|
||||
| ------------------ | ------------------------------------------------------------------------------------------------------------------------ | -------- | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| cmsURL | `string` | true | `http://localhost:8000` | `serverURL` as defined in your [Payload config](https://payloadcms.com/docs/configuration/overview#options) |
|
||||
| adminPath | `string` | false | /admin | `routes` as defined in your [Payload config](https://payloadcms.com/docs/configuration/overview#options) |
|
||||
| apiPath | `string` | false | /api | `routes` as defined in your [Payload config](https://payloadcms.com/docs/configuration/overview#options) |
|
||||
| authCollectionSlug | `string` | false | 'users' | Slug of your [auth collection](https://payloadcms.com/docs/configuration/collections) |
|
||||
| collectionSlug | `string` | true | undefined | Slug of your [collection](https://payloadcms.com/docs/configuration/collections) |
|
||||
| collectionLabels | `{ singular?: string, plural?: string }` | false | undefined | Labels of your [collection](https://payloadcms.com/docs/configuration/collections) |
|
||||
| id | `string` | true | undefined | id of the document |
|
||||
| logo | `ReactElement` | false | undefined | Custom logo |
|
||||
| classNames | `{ logo?: string, user?: string, controls?: string, create?: string, logout?: string, edit?: string, preview?: string }` | false | undefined | Custom class names, one for each rendered element |
|
||||
| logoProps | `{[key: string]?: unknown}` | false | undefined | Custom props |
|
||||
| userProps | `{[key: string]?: unknown}` | false | undefined | Custom props |
|
||||
| divProps | `{[key: string]?: unknown}` | false | undefined | Custom props |
|
||||
| createProps | `{[key: string]?: unknown}` | false | undefined | Custom props |
|
||||
| logoutProps | `{[key: string]?: unknown}` | false | undefined | Custom props |
|
||||
| editProps | `{[key: string]?: unknown}` | false | undefined | Custom props |
|
||||
| previewProps | `{[key: string]?: unknown}` | false | undefined | Custom props |
|
||||
| style | `CSSProperties` | false | undefined | Custom inline style |
|
||||
| unstyled | `boolean` | false | undefined | If true, renders no inline style |
|
||||
| onAuthChange | `(user: PayloadMeUser) => void` | false | undefined | Fired on each auth change |
|
||||
| devMode | `boolean` | false | undefined | If true, fakes authentication (useful when dealing with [SameSite cookies](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite)) |
|
||||
| preview | `boolean` | false | undefined | If true, renders an exit button with your `onPreviewExit` handler) |
|
||||
| onPreviewExit | `function` | false | undefined | Callback for the preview button `onClick` event) |
|
||||
18
packages/admin-bar/eslint.config.js
Normal file
18
packages/admin-bar/eslint.config.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import { rootEslintConfig, rootParserOptions } from '../../eslint.config.js'
|
||||
|
||||
/** @typedef {import('eslint').Linter.Config} Config */
|
||||
|
||||
/** @type {Config[]} */
|
||||
export const index = [
|
||||
...rootEslintConfig,
|
||||
{
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
...rootParserOptions,
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
export default index
|
||||
65
packages/admin-bar/package.json
Normal file
65
packages/admin-bar/package.json
Normal file
@@ -0,0 +1,65 @@
|
||||
{
|
||||
"name": "@payloadcms/admin-bar",
|
||||
"version": "3.28.0",
|
||||
"description": "An admin bar for React apps using Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/payloadcms/payload.git",
|
||||
"directory": "packages/admin-bar"
|
||||
},
|
||||
"license": "MIT",
|
||||
"author": "Payload <dev@payloadcms.com> (https://payloadcms.com)",
|
||||
"maintainers": [
|
||||
{
|
||||
"name": "Payload",
|
||||
"email": "info@payloadcms.com",
|
||||
"url": "https://payloadcms.com"
|
||||
}
|
||||
],
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"default": "./src/index.ts"
|
||||
}
|
||||
},
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "pnpm copyfiles && pnpm build:types && pnpm build:swc",
|
||||
"build:swc": "swc ./src -d ./dist --config-file .swcrc --strip-leading-paths",
|
||||
"build:types": "tsc --emitDeclarationOnly --outDir dist",
|
||||
"clean": "rimraf -g {dist,*.tsbuildinfo}",
|
||||
"copyfiles": "copyfiles -u 1 \"src/**/*.{html,css,scss,ttf,woff,woff2,eot,svg,jpg,png,json}\" dist/",
|
||||
"lint": "eslint .",
|
||||
"lint:fix": "eslint . --fix",
|
||||
"prepublishOnly": "pnpm clean && pnpm turbo build"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@payloadcms/eslint-config": "workspace:*",
|
||||
"@types/react": "19.0.10",
|
||||
"@types/react-dom": "19.0.4",
|
||||
"payload": "workspace:*"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
},
|
||||
"publishConfig": {
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"default": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"main": "./dist/index.js",
|
||||
"registry": "https://registry.npmjs.org/",
|
||||
"types": "./dist/index.d.ts"
|
||||
}
|
||||
}
|
||||
349
packages/admin-bar/src/AdminBar.tsx
Normal file
349
packages/admin-bar/src/AdminBar.tsx
Normal file
@@ -0,0 +1,349 @@
|
||||
'use client'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
|
||||
const dummyUser = {
|
||||
id: '12345',
|
||||
email: 'dev@email.com',
|
||||
}
|
||||
|
||||
import type { PayloadAdminBarProps, PayloadMeUser } from './types.js'
|
||||
|
||||
export const PayloadAdminBar: React.FC<PayloadAdminBarProps> = (props) => {
|
||||
const {
|
||||
id: docID,
|
||||
adminPath = '/admin',
|
||||
apiPath = '/api',
|
||||
authCollectionSlug = 'users',
|
||||
className,
|
||||
classNames,
|
||||
cmsURL = 'http://localhost:8000',
|
||||
collectionLabels,
|
||||
collectionSlug,
|
||||
createProps,
|
||||
devMode,
|
||||
divProps,
|
||||
editProps,
|
||||
logo,
|
||||
logoProps,
|
||||
logoutProps,
|
||||
onAuthChange,
|
||||
onPreviewExit,
|
||||
preview,
|
||||
previewProps,
|
||||
style,
|
||||
unstyled,
|
||||
userProps,
|
||||
} = props
|
||||
|
||||
const [user, setUser] = useState<PayloadMeUser>()
|
||||
|
||||
useEffect(() => {
|
||||
const fetchMe = async () => {
|
||||
try {
|
||||
const meRequest = await fetch(`${cmsURL}${apiPath}/${authCollectionSlug}/me`, {
|
||||
credentials: 'include',
|
||||
method: 'get',
|
||||
})
|
||||
const meResponse = await meRequest.json()
|
||||
const { user } = meResponse
|
||||
|
||||
if (user) {
|
||||
setUser(user)
|
||||
} else {
|
||||
if (devMode !== true) {
|
||||
setUser(null)
|
||||
} else {
|
||||
setUser(dummyUser)
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn(err)
|
||||
if (devMode === true) {
|
||||
setUser(dummyUser)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void fetchMe()
|
||||
}, [cmsURL, adminPath, apiPath, devMode])
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof onAuthChange === 'function') {
|
||||
onAuthChange(user)
|
||||
}
|
||||
}, [user, onAuthChange])
|
||||
|
||||
if (user) {
|
||||
const { id: userID, email } = user
|
||||
|
||||
return (
|
||||
<div
|
||||
className={className}
|
||||
id="payload-admin-bar"
|
||||
style={{
|
||||
...(unstyled !== true
|
||||
? {
|
||||
alignItems: 'center',
|
||||
backgroundColor: '#222',
|
||||
boxSizing: 'border-box',
|
||||
color: '#fff',
|
||||
display: 'flex',
|
||||
fontFamily:
|
||||
'-apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Arial, sans-serif',
|
||||
fontSize: 'small',
|
||||
left: 0,
|
||||
minWidth: '0',
|
||||
padding: '0.5rem',
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
width: '100%',
|
||||
zIndex: 99999,
|
||||
}
|
||||
: {}),
|
||||
...style,
|
||||
}}
|
||||
>
|
||||
<a
|
||||
className={classNames?.logo}
|
||||
href={`${cmsURL}${adminPath}`}
|
||||
{...logoProps}
|
||||
style={{
|
||||
...(unstyled !== true
|
||||
? {
|
||||
alignItems: 'center',
|
||||
color: 'inherit',
|
||||
display: 'flex',
|
||||
flexShrink: 0,
|
||||
height: '20px',
|
||||
marginRight: '10px',
|
||||
textDecoration: 'none',
|
||||
...(logoProps?.style
|
||||
? {
|
||||
...logoProps.style,
|
||||
}
|
||||
: {}),
|
||||
}
|
||||
: {}),
|
||||
}}
|
||||
>
|
||||
{logo || 'Payload CMS'}
|
||||
</a>
|
||||
<a
|
||||
className={classNames?.user}
|
||||
href={`${cmsURL}${adminPath}/collections/${authCollectionSlug}/${userID}`}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
{...userProps}
|
||||
style={{
|
||||
...(unstyled !== true
|
||||
? {
|
||||
color: 'inherit',
|
||||
display: 'block',
|
||||
marginRight: '10px',
|
||||
minWidth: '50px',
|
||||
overflow: 'hidden',
|
||||
textDecoration: 'none',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
...(userProps?.style
|
||||
? {
|
||||
...userProps.style,
|
||||
}
|
||||
: {}),
|
||||
}
|
||||
: {}),
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
...(unstyled !== true
|
||||
? {
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
}
|
||||
: {}),
|
||||
}}
|
||||
>
|
||||
{email || 'Profile'}
|
||||
</span>
|
||||
</a>
|
||||
<div
|
||||
className={classNames?.controls}
|
||||
{...divProps}
|
||||
style={{
|
||||
...(unstyled !== true
|
||||
? {
|
||||
alignItems: 'center',
|
||||
display: 'flex',
|
||||
flexGrow: 1,
|
||||
flexShrink: 1,
|
||||
justifyContent: 'flex-end',
|
||||
marginRight: '10px',
|
||||
...(divProps?.style
|
||||
? {
|
||||
...divProps.style,
|
||||
}
|
||||
: {}),
|
||||
}
|
||||
: {}),
|
||||
}}
|
||||
>
|
||||
{collectionSlug && docID && (
|
||||
<a
|
||||
className={classNames?.edit}
|
||||
href={`${cmsURL}${adminPath}/collections/${collectionSlug}/${docID}`}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
{...editProps}
|
||||
style={{
|
||||
display: 'block',
|
||||
...(unstyled !== true
|
||||
? {
|
||||
color: 'inherit',
|
||||
flexShrink: 1,
|
||||
marginRight: '10px',
|
||||
overflow: 'hidden',
|
||||
textDecoration: 'none',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
...(editProps?.style
|
||||
? {
|
||||
...editProps.style,
|
||||
}
|
||||
: {}),
|
||||
}
|
||||
: {}),
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
...(unstyled !== true
|
||||
? {
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
}
|
||||
: {}),
|
||||
}}
|
||||
>
|
||||
{`Edit ${collectionLabels?.singular || 'page'}`}
|
||||
</span>
|
||||
</a>
|
||||
)}
|
||||
{collectionSlug && (
|
||||
<a
|
||||
className={classNames?.create}
|
||||
href={`${cmsURL}${adminPath}/collections/${collectionSlug}/create`}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
{...createProps}
|
||||
style={{
|
||||
...(unstyled !== true
|
||||
? {
|
||||
color: 'inherit',
|
||||
display: 'block',
|
||||
flexShrink: 1,
|
||||
overflow: 'hidden',
|
||||
textDecoration: 'none',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
...(createProps?.style
|
||||
? {
|
||||
...createProps.style,
|
||||
}
|
||||
: {}),
|
||||
}
|
||||
: {}),
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
...(unstyled !== true
|
||||
? {
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
}
|
||||
: {}),
|
||||
}}
|
||||
>
|
||||
{`New ${collectionLabels?.singular || 'page'}`}
|
||||
</span>
|
||||
</a>
|
||||
)}
|
||||
{preview && (
|
||||
<button
|
||||
className={classNames?.preview}
|
||||
onClick={onPreviewExit}
|
||||
{...previewProps}
|
||||
style={{
|
||||
...(unstyled !== true
|
||||
? {
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
color: 'inherit',
|
||||
cursor: 'pointer',
|
||||
fontFamily: 'inherit',
|
||||
fontSize: 'inherit',
|
||||
marginLeft: '10px',
|
||||
padding: 0,
|
||||
...(previewProps?.style
|
||||
? {
|
||||
...previewProps.style,
|
||||
}
|
||||
: {}),
|
||||
}
|
||||
: {}),
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
Exit preview mode
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<a
|
||||
className={classNames?.logout}
|
||||
href={`${cmsURL}${adminPath}/logout`}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
{...logoutProps}
|
||||
style={{
|
||||
...(unstyled !== true
|
||||
? {
|
||||
color: 'inherit',
|
||||
display: 'block',
|
||||
flexShrink: 1,
|
||||
overflow: 'hidden',
|
||||
textDecoration: 'none',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
...(logoutProps?.style
|
||||
? {
|
||||
...logoutProps.style,
|
||||
}
|
||||
: {}),
|
||||
}
|
||||
: {}),
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
...(unstyled !== true
|
||||
? {
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
}
|
||||
: {}),
|
||||
}}
|
||||
>
|
||||
Logout
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
2
packages/admin-bar/src/index.ts
Normal file
2
packages/admin-bar/src/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { PayloadAdminBar } from './AdminBar.js'
|
||||
export type { PayloadAdminBarProps, PayloadMeUser } from './types.js'
|
||||
67
packages/admin-bar/src/types.ts
Normal file
67
packages/admin-bar/src/types.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import type { CSSProperties, ReactElement } from 'react'
|
||||
|
||||
export type PayloadMeUser =
|
||||
| {
|
||||
email: string
|
||||
id: string
|
||||
}
|
||||
| null
|
||||
| undefined
|
||||
|
||||
export type PayloadAdminBarProps = {
|
||||
adminPath?: string
|
||||
apiPath?: string
|
||||
authCollectionSlug?: string
|
||||
className?: string
|
||||
classNames?: {
|
||||
controls?: string
|
||||
create?: string
|
||||
edit?: string
|
||||
logo?: string
|
||||
logout?: string
|
||||
preview?: string
|
||||
user?: string
|
||||
}
|
||||
cmsURL?: string
|
||||
collectionLabels?: {
|
||||
plural?: string
|
||||
singular?: string
|
||||
}
|
||||
collectionSlug?: string
|
||||
createProps?: {
|
||||
[key: string]: unknown
|
||||
style?: CSSProperties
|
||||
}
|
||||
devMode?: boolean
|
||||
divProps?: {
|
||||
[key: string]: unknown
|
||||
style?: CSSProperties
|
||||
}
|
||||
editProps?: {
|
||||
[key: string]: unknown
|
||||
style?: CSSProperties
|
||||
}
|
||||
id?: string
|
||||
logo?: ReactElement
|
||||
logoProps?: {
|
||||
[key: string]: unknown
|
||||
style?: CSSProperties
|
||||
}
|
||||
logoutProps?: {
|
||||
[key: string]: unknown
|
||||
style?: CSSProperties
|
||||
}
|
||||
onAuthChange?: (user: PayloadMeUser) => void
|
||||
onPreviewExit?: () => void
|
||||
preview?: boolean
|
||||
previewProps?: {
|
||||
[key: string]: unknown
|
||||
style?: CSSProperties
|
||||
}
|
||||
style?: CSSProperties
|
||||
unstyled?: boolean
|
||||
userProps?: {
|
||||
[key: string]: unknown
|
||||
style?: CSSProperties
|
||||
}
|
||||
}
|
||||
9
packages/admin-bar/tsconfig.json
Normal file
9
packages/admin-bar/tsconfig.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
/* TODO: remove the following lines */
|
||||
"strict": false,
|
||||
"noUncheckedIndexedAccess": false,
|
||||
},
|
||||
"references": [{ "path": "../payload" }]
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "create-payload-app",
|
||||
"version": "3.24.0",
|
||||
"version": "3.28.0",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-mongodb",
|
||||
"version": "3.24.0",
|
||||
"version": "3.28.0",
|
||||
"description": "The officially supported MongoDB database adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
@@ -47,7 +47,6 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"mongoose": "8.9.5",
|
||||
"mongoose-aggregate-paginate-v2": "1.1.2",
|
||||
"mongoose-paginate-v2": "1.8.5",
|
||||
"prompts": "2.4.2",
|
||||
"uuid": "10.0.0"
|
||||
@@ -55,6 +54,8 @@
|
||||
"devDependencies": {
|
||||
"@payloadcms/eslint-config": "workspace:*",
|
||||
"@types/mongoose-aggregate-paginate-v2": "1.0.12",
|
||||
"@types/prompts": "^2.4.5",
|
||||
"@types/uuid": "10.0.0",
|
||||
"mongodb": "6.12.0",
|
||||
"mongodb-memory-server": "^10",
|
||||
"payload": "workspace:*"
|
||||
|
||||
@@ -70,9 +70,15 @@ export const connect: Connect = async function connect(
|
||||
await this.migrate({ migrations: this.prodMigrations })
|
||||
}
|
||||
} catch (err) {
|
||||
let msg = `Error: cannot connect to MongoDB.`
|
||||
|
||||
if (typeof err === 'object' && err && 'message' in err && typeof err.message === 'string') {
|
||||
msg = `${msg} Details: ${err.message}`
|
||||
}
|
||||
|
||||
this.payload.logger.error({
|
||||
err,
|
||||
msg: `Error: cannot connect to MongoDB. Details: ${err.message}`,
|
||||
msg,
|
||||
})
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
@@ -6,13 +6,15 @@ import { flattenWhereToOperators } from 'payload'
|
||||
import type { MongooseAdapter } from './index.js'
|
||||
|
||||
import { buildQuery } from './queries/buildQuery.js'
|
||||
import { getCollection } from './utilities/getEntity.js'
|
||||
import { getSession } from './utilities/getSession.js'
|
||||
|
||||
export const count: Count = async function count(
|
||||
this: MongooseAdapter,
|
||||
{ collection, locale, req, where },
|
||||
{ collection: collectionSlug, locale, req, where = {} },
|
||||
) {
|
||||
const Model = this.collections[collection]
|
||||
const { collectionConfig, Model } = getCollection({ adapter: this, collectionSlug })
|
||||
|
||||
const options: CountOptions = {
|
||||
session: await getSession(this, req),
|
||||
}
|
||||
@@ -26,8 +28,8 @@ export const count: Count = async function count(
|
||||
|
||||
const query = await buildQuery({
|
||||
adapter: this,
|
||||
collectionSlug: collection,
|
||||
fields: this.payload.collections[collection].config.flattenedFields,
|
||||
collectionSlug,
|
||||
fields: collectionConfig.flattenedFields,
|
||||
locale,
|
||||
where,
|
||||
})
|
||||
|
||||
@@ -6,13 +6,15 @@ import { buildVersionGlobalFields, flattenWhereToOperators } from 'payload'
|
||||
import type { MongooseAdapter } from './index.js'
|
||||
|
||||
import { buildQuery } from './queries/buildQuery.js'
|
||||
import { getGlobal } from './utilities/getEntity.js'
|
||||
import { getSession } from './utilities/getSession.js'
|
||||
|
||||
export const countGlobalVersions: CountGlobalVersions = async function countGlobalVersions(
|
||||
this: MongooseAdapter,
|
||||
{ global, locale, req, where },
|
||||
{ global: globalSlug, locale, req, where = {} },
|
||||
) {
|
||||
const Model = this.versions[global]
|
||||
const { globalConfig, Model } = getGlobal({ adapter: this, globalSlug, versions: true })
|
||||
|
||||
const options: CountOptions = {
|
||||
session: await getSession(this, req),
|
||||
}
|
||||
@@ -26,11 +28,7 @@ export const countGlobalVersions: CountGlobalVersions = async function countGlob
|
||||
|
||||
const query = await buildQuery({
|
||||
adapter: this,
|
||||
fields: buildVersionGlobalFields(
|
||||
this.payload.config,
|
||||
this.payload.globals.config.find((each) => each.slug === global),
|
||||
true,
|
||||
),
|
||||
fields: buildVersionGlobalFields(this.payload.config, globalConfig, true),
|
||||
locale,
|
||||
where,
|
||||
})
|
||||
|
||||
@@ -6,13 +6,19 @@ import { buildVersionCollectionFields, flattenWhereToOperators } from 'payload'
|
||||
import type { MongooseAdapter } from './index.js'
|
||||
|
||||
import { buildQuery } from './queries/buildQuery.js'
|
||||
import { getCollection } from './utilities/getEntity.js'
|
||||
import { getSession } from './utilities/getSession.js'
|
||||
|
||||
export const countVersions: CountVersions = async function countVersions(
|
||||
this: MongooseAdapter,
|
||||
{ collection, locale, req, where },
|
||||
{ collection: collectionSlug, locale, req, where = {} },
|
||||
) {
|
||||
const Model = this.versions[collection]
|
||||
const { collectionConfig, Model } = getCollection({
|
||||
adapter: this,
|
||||
collectionSlug,
|
||||
versions: true,
|
||||
})
|
||||
|
||||
const options: CountOptions = {
|
||||
session: await getSession(this, req),
|
||||
}
|
||||
@@ -26,11 +32,7 @@ export const countVersions: CountVersions = async function countVersions(
|
||||
|
||||
const query = await buildQuery({
|
||||
adapter: this,
|
||||
fields: buildVersionCollectionFields(
|
||||
this.payload.config,
|
||||
this.payload.collections[collection].config,
|
||||
true,
|
||||
),
|
||||
fields: buildVersionCollectionFields(this.payload.config, collectionConfig, true),
|
||||
locale,
|
||||
where,
|
||||
})
|
||||
|
||||
@@ -1,48 +1,53 @@
|
||||
import type { CreateOptions } from 'mongoose'
|
||||
import type { Create, Document } from 'payload'
|
||||
import type { Create } from 'payload'
|
||||
|
||||
import type { MongooseAdapter } from './index.js'
|
||||
|
||||
import { getCollection } from './utilities/getEntity.js'
|
||||
import { getSession } from './utilities/getSession.js'
|
||||
import { handleError } from './utilities/handleError.js'
|
||||
import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js'
|
||||
import { transform } from './utilities/transform.js'
|
||||
|
||||
export const create: Create = async function create(
|
||||
this: MongooseAdapter,
|
||||
{ collection, data, req },
|
||||
{ collection: collectionSlug, data, req, returning },
|
||||
) {
|
||||
const Model = this.collections[collection]
|
||||
const { collectionConfig, customIDType, Model } = getCollection({ adapter: this, collectionSlug })
|
||||
|
||||
const options: CreateOptions = {
|
||||
session: await getSession(this, req),
|
||||
}
|
||||
|
||||
let doc
|
||||
|
||||
const sanitizedData = sanitizeRelationshipIDs({
|
||||
config: this.payload.config,
|
||||
transform({
|
||||
adapter: this,
|
||||
data,
|
||||
fields: this.payload.collections[collection].config.fields,
|
||||
fields: collectionConfig.fields,
|
||||
operation: 'write',
|
||||
})
|
||||
|
||||
if (this.payload.collections[collection].customIDType) {
|
||||
sanitizedData._id = sanitizedData.id
|
||||
if (customIDType) {
|
||||
data._id = data.id
|
||||
}
|
||||
|
||||
try {
|
||||
;[doc] = await Model.create([sanitizedData], options)
|
||||
;[doc] = await Model.create([data], options)
|
||||
} catch (error) {
|
||||
handleError({ collection, error, req })
|
||||
handleError({ collection: collectionSlug, error, req })
|
||||
}
|
||||
if (returning === false) {
|
||||
return null
|
||||
}
|
||||
|
||||
// doc.toJSON does not do stuff like converting ObjectIds to string, or date strings to date objects. That's why we use JSON.parse/stringify here
|
||||
const result: Document = JSON.parse(JSON.stringify(doc))
|
||||
const verificationToken = doc._verificationToken
|
||||
doc = doc.toObject()
|
||||
|
||||
// custom id type reset
|
||||
result.id = result._id
|
||||
if (verificationToken) {
|
||||
result._verificationToken = verificationToken
|
||||
}
|
||||
transform({
|
||||
adapter: this,
|
||||
data: doc,
|
||||
fields: collectionConfig.fields,
|
||||
operation: 'read',
|
||||
})
|
||||
|
||||
return result
|
||||
return doc
|
||||
}
|
||||
|
||||
@@ -1,38 +1,44 @@
|
||||
import type { CreateOptions } from 'mongoose'
|
||||
import type { CreateGlobal } from 'payload'
|
||||
|
||||
import { type CreateGlobal } from 'payload'
|
||||
|
||||
import type { MongooseAdapter } from './index.js'
|
||||
|
||||
import { getGlobal } from './utilities/getEntity.js'
|
||||
import { getSession } from './utilities/getSession.js'
|
||||
import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js'
|
||||
import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js'
|
||||
import { transform } from './utilities/transform.js'
|
||||
|
||||
export const createGlobal: CreateGlobal = async function createGlobal(
|
||||
this: MongooseAdapter,
|
||||
{ slug, data, req },
|
||||
{ slug: globalSlug, data, req, returning },
|
||||
) {
|
||||
const Model = this.globals
|
||||
const { globalConfig, Model } = getGlobal({ adapter: this, globalSlug })
|
||||
|
||||
const global = sanitizeRelationshipIDs({
|
||||
config: this.payload.config,
|
||||
data: {
|
||||
globalType: slug,
|
||||
...data,
|
||||
},
|
||||
fields: this.payload.config.globals.find((globalConfig) => globalConfig.slug === slug).fields,
|
||||
transform({
|
||||
adapter: this,
|
||||
data,
|
||||
fields: globalConfig.fields,
|
||||
globalSlug,
|
||||
operation: 'write',
|
||||
})
|
||||
|
||||
const options: CreateOptions = {
|
||||
session: await getSession(this, req),
|
||||
}
|
||||
|
||||
let [result] = (await Model.create([global], options)) as any
|
||||
let [result] = (await Model.create([data], options)) as any
|
||||
if (returning === false) {
|
||||
return null
|
||||
}
|
||||
|
||||
result = JSON.parse(JSON.stringify(result))
|
||||
result = result.toObject()
|
||||
|
||||
// custom id type reset
|
||||
result.id = result._id
|
||||
result = sanitizeInternalFields(result)
|
||||
transform({
|
||||
adapter: this,
|
||||
data: result,
|
||||
fields: globalConfig.fields,
|
||||
operation: 'read',
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import type { CreateOptions } from 'mongoose'
|
||||
|
||||
import { buildVersionGlobalFields, type CreateGlobalVersion, type Document } from 'payload'
|
||||
import { buildVersionGlobalFields, type CreateGlobalVersion } from 'payload'
|
||||
|
||||
import type { MongooseAdapter } from './index.js'
|
||||
|
||||
import { getGlobal } from './utilities/getEntity.js'
|
||||
import { getSession } from './utilities/getSession.js'
|
||||
import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js'
|
||||
import { transform } from './utilities/transform.js'
|
||||
|
||||
export const createGlobalVersion: CreateGlobalVersion = async function createGlobalVersion(
|
||||
this: MongooseAdapter,
|
||||
@@ -16,37 +15,41 @@ export const createGlobalVersion: CreateGlobalVersion = async function createGlo
|
||||
parent,
|
||||
publishedLocale,
|
||||
req,
|
||||
returning,
|
||||
snapshot,
|
||||
updatedAt,
|
||||
versionData,
|
||||
},
|
||||
) {
|
||||
const VersionModel = this.versions[globalSlug]
|
||||
const options: CreateOptions = {
|
||||
const { globalConfig, Model } = getGlobal({ adapter: this, globalSlug, versions: true })
|
||||
|
||||
const options = {
|
||||
session: await getSession(this, req),
|
||||
}
|
||||
|
||||
const data = sanitizeRelationshipIDs({
|
||||
config: this.payload.config,
|
||||
data: {
|
||||
autosave,
|
||||
createdAt,
|
||||
latest: true,
|
||||
parent,
|
||||
publishedLocale,
|
||||
snapshot,
|
||||
updatedAt,
|
||||
version: versionData,
|
||||
},
|
||||
fields: buildVersionGlobalFields(
|
||||
this.payload.config,
|
||||
this.payload.config.globals.find((global) => global.slug === globalSlug),
|
||||
),
|
||||
const data = {
|
||||
autosave,
|
||||
createdAt,
|
||||
latest: true,
|
||||
parent,
|
||||
publishedLocale,
|
||||
snapshot,
|
||||
updatedAt,
|
||||
version: versionData,
|
||||
}
|
||||
|
||||
const fields = buildVersionGlobalFields(this.payload.config, globalConfig)
|
||||
|
||||
transform({
|
||||
adapter: this,
|
||||
data,
|
||||
fields,
|
||||
operation: 'write',
|
||||
})
|
||||
|
||||
const [doc] = await VersionModel.create([data], options, req)
|
||||
let [doc] = await Model.create([data], options, req)
|
||||
|
||||
await VersionModel.updateMany(
|
||||
await Model.updateMany(
|
||||
{
|
||||
$and: [
|
||||
{
|
||||
@@ -70,13 +73,18 @@ export const createGlobalVersion: CreateGlobalVersion = async function createGlo
|
||||
options,
|
||||
)
|
||||
|
||||
const result: Document = JSON.parse(JSON.stringify(doc))
|
||||
const verificationToken = doc._verificationToken
|
||||
|
||||
// custom id type reset
|
||||
result.id = result._id
|
||||
if (verificationToken) {
|
||||
result._verificationToken = verificationToken
|
||||
if (returning === false) {
|
||||
return null
|
||||
}
|
||||
return result
|
||||
|
||||
doc = doc.toObject()
|
||||
|
||||
transform({
|
||||
adapter: this,
|
||||
data: doc,
|
||||
fields,
|
||||
operation: 'read',
|
||||
})
|
||||
|
||||
return doc
|
||||
}
|
||||
|
||||
@@ -42,8 +42,9 @@ export const createMigration: CreateMigration = async function createMigration({
|
||||
const migrationFileContent = migrationTemplate(predefinedMigration)
|
||||
|
||||
const [yyymmdd, hhmmss] = new Date().toISOString().split('T')
|
||||
const formattedDate = yyymmdd.replace(/\D/g, '')
|
||||
const formattedTime = hhmmss.split('.')[0].replace(/\D/g, '')
|
||||
|
||||
const formattedDate = yyymmdd!.replace(/\D/g, '')
|
||||
const formattedTime = hhmmss!.split('.')[0]!.replace(/\D/g, '')
|
||||
|
||||
const timestamp = `${formattedDate}_${formattedTime}`
|
||||
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import type { CreateOptions } from 'mongoose'
|
||||
|
||||
import { Types } from 'mongoose'
|
||||
import { buildVersionCollectionFields, type CreateVersion, type Document } from 'payload'
|
||||
import { buildVersionCollectionFields, type CreateVersion } from 'payload'
|
||||
|
||||
import type { MongooseAdapter } from './index.js'
|
||||
|
||||
import { getCollection } from './utilities/getEntity.js'
|
||||
import { getSession } from './utilities/getSession.js'
|
||||
import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js'
|
||||
import { transform } from './utilities/transform.js'
|
||||
|
||||
export const createVersion: CreateVersion = async function createVersion(
|
||||
this: MongooseAdapter,
|
||||
@@ -17,35 +15,43 @@ export const createVersion: CreateVersion = async function createVersion(
|
||||
parent,
|
||||
publishedLocale,
|
||||
req,
|
||||
returning,
|
||||
snapshot,
|
||||
updatedAt,
|
||||
versionData,
|
||||
},
|
||||
) {
|
||||
const VersionModel = this.versions[collectionSlug]
|
||||
const options: CreateOptions = {
|
||||
const { collectionConfig, Model } = getCollection({
|
||||
adapter: this,
|
||||
collectionSlug,
|
||||
versions: true,
|
||||
})
|
||||
|
||||
const options = {
|
||||
session: await getSession(this, req),
|
||||
}
|
||||
|
||||
const data = sanitizeRelationshipIDs({
|
||||
config: this.payload.config,
|
||||
data: {
|
||||
autosave,
|
||||
createdAt,
|
||||
latest: true,
|
||||
parent,
|
||||
publishedLocale,
|
||||
snapshot,
|
||||
updatedAt,
|
||||
version: versionData,
|
||||
},
|
||||
fields: buildVersionCollectionFields(
|
||||
this.payload.config,
|
||||
this.payload.collections[collectionSlug].config,
|
||||
),
|
||||
const data = {
|
||||
autosave,
|
||||
createdAt,
|
||||
latest: true,
|
||||
parent,
|
||||
publishedLocale,
|
||||
snapshot,
|
||||
updatedAt,
|
||||
version: versionData,
|
||||
}
|
||||
|
||||
const fields = buildVersionCollectionFields(this.payload.config, collectionConfig)
|
||||
|
||||
transform({
|
||||
adapter: this,
|
||||
data,
|
||||
fields,
|
||||
operation: 'write',
|
||||
})
|
||||
|
||||
const [doc] = await VersionModel.create([data], options, req)
|
||||
let [doc] = await Model.create([data], options, req)
|
||||
|
||||
const parentQuery = {
|
||||
$or: [
|
||||
@@ -56,15 +62,8 @@ export const createVersion: CreateVersion = async function createVersion(
|
||||
},
|
||||
],
|
||||
}
|
||||
if (data.parent instanceof Types.ObjectId) {
|
||||
parentQuery.$or.push({
|
||||
parent: {
|
||||
$eq: data.parent.toString(),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
await VersionModel.updateMany(
|
||||
await Model.updateMany(
|
||||
{
|
||||
$and: [
|
||||
{
|
||||
@@ -89,13 +88,18 @@ export const createVersion: CreateVersion = async function createVersion(
|
||||
options,
|
||||
)
|
||||
|
||||
const result: Document = JSON.parse(JSON.stringify(doc))
|
||||
const verificationToken = doc._verificationToken
|
||||
|
||||
// custom id type reset
|
||||
result.id = result._id
|
||||
if (verificationToken) {
|
||||
result._verificationToken = verificationToken
|
||||
if (returning === false) {
|
||||
return null
|
||||
}
|
||||
return result
|
||||
|
||||
doc = doc.toObject()
|
||||
|
||||
transform({
|
||||
adapter: this,
|
||||
data: doc,
|
||||
fields,
|
||||
operation: 'read',
|
||||
})
|
||||
|
||||
return doc
|
||||
}
|
||||
|
||||
@@ -1,24 +1,27 @@
|
||||
import type { DeleteOptions } from 'mongodb'
|
||||
import type { DeleteMany } from 'payload'
|
||||
|
||||
import { type DeleteMany } from 'payload'
|
||||
|
||||
import type { MongooseAdapter } from './index.js'
|
||||
|
||||
import { buildQuery } from './queries/buildQuery.js'
|
||||
import { getCollection } from './utilities/getEntity.js'
|
||||
import { getSession } from './utilities/getSession.js'
|
||||
|
||||
export const deleteMany: DeleteMany = async function deleteMany(
|
||||
this: MongooseAdapter,
|
||||
{ collection, req, where },
|
||||
{ collection: collectionSlug, req, where },
|
||||
) {
|
||||
const Model = this.collections[collection]
|
||||
const { collectionConfig, Model } = getCollection({ adapter: this, collectionSlug })
|
||||
|
||||
const options: DeleteOptions = {
|
||||
session: await getSession(this, req),
|
||||
}
|
||||
|
||||
const query = await buildQuery({
|
||||
adapter: this,
|
||||
collectionSlug: collection,
|
||||
fields: this.payload.collections[collection].config.flattenedFields,
|
||||
collectionSlug,
|
||||
fields: collectionConfig.flattenedFields,
|
||||
where,
|
||||
})
|
||||
|
||||
|
||||
@@ -1,22 +1,24 @@
|
||||
import type { QueryOptions } from 'mongoose'
|
||||
import type { DeleteOne, Document } from 'payload'
|
||||
import type { MongooseUpdateQueryOptions } from 'mongoose'
|
||||
import type { DeleteOne } from 'payload'
|
||||
|
||||
import type { MongooseAdapter } from './index.js'
|
||||
|
||||
import { buildQuery } from './queries/buildQuery.js'
|
||||
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
|
||||
import { getCollection } from './utilities/getEntity.js'
|
||||
import { getSession } from './utilities/getSession.js'
|
||||
import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js'
|
||||
import { transform } from './utilities/transform.js'
|
||||
|
||||
export const deleteOne: DeleteOne = async function deleteOne(
|
||||
this: MongooseAdapter,
|
||||
{ collection, req, select, where },
|
||||
{ collection: collectionSlug, req, returning, select, where },
|
||||
) {
|
||||
const Model = this.collections[collection]
|
||||
const options: QueryOptions = {
|
||||
const { collectionConfig, Model } = getCollection({ adapter: this, collectionSlug })
|
||||
|
||||
const options: MongooseUpdateQueryOptions = {
|
||||
projection: buildProjectionFromSelect({
|
||||
adapter: this,
|
||||
fields: this.payload.collections[collection].config.flattenedFields,
|
||||
fields: collectionConfig.flattenedFields,
|
||||
select,
|
||||
}),
|
||||
session: await getSession(this, req),
|
||||
@@ -24,22 +26,28 @@ export const deleteOne: DeleteOne = async function deleteOne(
|
||||
|
||||
const query = await buildQuery({
|
||||
adapter: this,
|
||||
collectionSlug: collection,
|
||||
fields: this.payload.collections[collection].config.flattenedFields,
|
||||
collectionSlug,
|
||||
fields: collectionConfig.flattenedFields,
|
||||
where,
|
||||
})
|
||||
|
||||
if (returning === false) {
|
||||
await Model.deleteOne(query, options)?.lean()
|
||||
return null
|
||||
}
|
||||
|
||||
const doc = await Model.findOneAndDelete(query, options)?.lean()
|
||||
|
||||
if (!doc) {
|
||||
return null
|
||||
}
|
||||
|
||||
let result: Document = JSON.parse(JSON.stringify(doc))
|
||||
transform({
|
||||
adapter: this,
|
||||
data: doc,
|
||||
fields: collectionConfig.fields,
|
||||
operation: 'read',
|
||||
})
|
||||
|
||||
// custom id type reset
|
||||
result.id = result._id
|
||||
result = sanitizeInternalFields(result)
|
||||
|
||||
return result
|
||||
return doc
|
||||
}
|
||||
|
||||
@@ -3,26 +3,27 @@ import { buildVersionCollectionFields, type DeleteVersions } from 'payload'
|
||||
import type { MongooseAdapter } from './index.js'
|
||||
|
||||
import { buildQuery } from './queries/buildQuery.js'
|
||||
import { getCollection } from './utilities/getEntity.js'
|
||||
import { getSession } from './utilities/getSession.js'
|
||||
|
||||
export const deleteVersions: DeleteVersions = async function deleteVersions(
|
||||
this: MongooseAdapter,
|
||||
{ collection, locale, req, where },
|
||||
{ collection: collectionSlug, locale, req, where },
|
||||
) {
|
||||
const VersionsModel = this.versions[collection]
|
||||
const { collectionConfig, Model } = getCollection({
|
||||
adapter: this,
|
||||
collectionSlug,
|
||||
versions: true,
|
||||
})
|
||||
|
||||
const session = await getSession(this, req)
|
||||
|
||||
const query = await buildQuery({
|
||||
adapter: this,
|
||||
fields: buildVersionCollectionFields(
|
||||
this.payload.config,
|
||||
this.payload.collections[collection].config,
|
||||
true,
|
||||
),
|
||||
fields: buildVersionCollectionFields(this.payload.config, collectionConfig, true),
|
||||
locale,
|
||||
where,
|
||||
})
|
||||
|
||||
await VersionsModel.deleteMany(query, { session })
|
||||
await Model.deleteMany(query, { session })
|
||||
}
|
||||
|
||||
@@ -7,15 +7,17 @@ import type { MongooseAdapter } from './index.js'
|
||||
|
||||
import { buildQuery } from './queries/buildQuery.js'
|
||||
import { buildSortParam } from './queries/buildSortParam.js'
|
||||
import { aggregatePaginate } from './utilities/aggregatePaginate.js'
|
||||
import { buildJoinAggregation } from './utilities/buildJoinAggregation.js'
|
||||
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
|
||||
import { getCollection } from './utilities/getEntity.js'
|
||||
import { getSession } from './utilities/getSession.js'
|
||||
import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js'
|
||||
import { transform } from './utilities/transform.js'
|
||||
|
||||
export const find: Find = async function find(
|
||||
this: MongooseAdapter,
|
||||
{
|
||||
collection,
|
||||
collection: collectionSlug,
|
||||
joins = {},
|
||||
limit = 0,
|
||||
locale,
|
||||
@@ -25,11 +27,10 @@ export const find: Find = async function find(
|
||||
req,
|
||||
select,
|
||||
sort: sortArg,
|
||||
where,
|
||||
where = {},
|
||||
},
|
||||
) {
|
||||
const Model = this.collections[collection]
|
||||
const collectionConfig = this.payload.collections[collection].config
|
||||
const { collectionConfig, Model } = getCollection({ adapter: this, collectionSlug })
|
||||
|
||||
const session = await getSession(this, req)
|
||||
|
||||
@@ -53,8 +54,8 @@ export const find: Find = async function find(
|
||||
|
||||
const query = await buildQuery({
|
||||
adapter: this,
|
||||
collectionSlug: collection,
|
||||
fields: this.payload.collections[collection].config.flattenedFields,
|
||||
collectionSlug,
|
||||
fields: collectionConfig.flattenedFields,
|
||||
locale,
|
||||
where,
|
||||
})
|
||||
@@ -108,7 +109,8 @@ export const find: Find = async function find(
|
||||
if (limit >= 0) {
|
||||
paginationOptions.limit = limit
|
||||
// limit must also be set here, it's ignored when pagination is false
|
||||
paginationOptions.options.limit = limit
|
||||
|
||||
paginationOptions.options!.limit = limit
|
||||
|
||||
// Disable pagination if limit is 0
|
||||
if (limit === 0) {
|
||||
@@ -120,7 +122,7 @@ export const find: Find = async function find(
|
||||
|
||||
const aggregate = await buildJoinAggregation({
|
||||
adapter: this,
|
||||
collection,
|
||||
collection: collectionSlug,
|
||||
collectionConfig,
|
||||
joins,
|
||||
locale,
|
||||
@@ -128,18 +130,30 @@ export const find: Find = async function find(
|
||||
})
|
||||
// build join aggregation
|
||||
if (aggregate) {
|
||||
result = await Model.aggregatePaginate(Model.aggregate(aggregate), paginationOptions)
|
||||
result = await aggregatePaginate({
|
||||
adapter: this,
|
||||
collation: paginationOptions.collation,
|
||||
joinAggregation: aggregate,
|
||||
limit: paginationOptions.limit,
|
||||
Model,
|
||||
page: paginationOptions.page,
|
||||
pagination: paginationOptions.pagination,
|
||||
projection: paginationOptions.projection,
|
||||
query,
|
||||
session: paginationOptions.options?.session ?? undefined,
|
||||
sort: paginationOptions.sort as object,
|
||||
useEstimatedCount: paginationOptions.useEstimatedCount,
|
||||
})
|
||||
} else {
|
||||
result = await Model.paginate(query, paginationOptions)
|
||||
}
|
||||
|
||||
const docs = JSON.parse(JSON.stringify(result.docs))
|
||||
transform({
|
||||
adapter: this,
|
||||
data: result.docs,
|
||||
fields: collectionConfig.fields,
|
||||
operation: 'read',
|
||||
})
|
||||
|
||||
return {
|
||||
...result,
|
||||
docs: docs.map((doc) => {
|
||||
doc.id = doc._id
|
||||
return sanitizeInternalFields(doc)
|
||||
}),
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -7,15 +7,17 @@ import type { MongooseAdapter } from './index.js'
|
||||
|
||||
import { buildQuery } from './queries/buildQuery.js'
|
||||
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
|
||||
import { getGlobal } from './utilities/getEntity.js'
|
||||
import { getSession } from './utilities/getSession.js'
|
||||
import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js'
|
||||
import { transform } from './utilities/transform.js'
|
||||
|
||||
export const findGlobal: FindGlobal = async function findGlobal(
|
||||
this: MongooseAdapter,
|
||||
{ slug, locale, req, select, where },
|
||||
{ slug: globalSlug, locale, req, select, where = {} },
|
||||
) {
|
||||
const Model = this.globals
|
||||
const fields = this.payload.globals.config.find((each) => each.slug === slug).flattenedFields
|
||||
const { globalConfig, Model } = getGlobal({ adapter: this, globalSlug })
|
||||
|
||||
const fields = globalConfig.flattenedFields
|
||||
const options: QueryOptions = {
|
||||
lean: true,
|
||||
select: buildProjectionFromSelect({
|
||||
@@ -29,23 +31,23 @@ export const findGlobal: FindGlobal = async function findGlobal(
|
||||
const query = await buildQuery({
|
||||
adapter: this,
|
||||
fields,
|
||||
globalSlug: slug,
|
||||
globalSlug,
|
||||
locale,
|
||||
where: combineQueries({ globalType: { equals: slug } }, where),
|
||||
where: combineQueries({ globalType: { equals: globalSlug } }, where),
|
||||
})
|
||||
|
||||
let doc = (await Model.findOne(query, {}, options)) as any
|
||||
const doc: any = await Model.findOne(query, {}, options)
|
||||
|
||||
if (!doc) {
|
||||
return null
|
||||
}
|
||||
if (doc._id) {
|
||||
doc.id = doc._id
|
||||
delete doc._id
|
||||
}
|
||||
|
||||
doc = JSON.parse(JSON.stringify(doc))
|
||||
doc = sanitizeInternalFields(doc)
|
||||
transform({
|
||||
adapter: this,
|
||||
data: doc,
|
||||
fields: globalConfig.fields,
|
||||
operation: 'read',
|
||||
})
|
||||
|
||||
return doc
|
||||
}
|
||||
|
||||
@@ -1,26 +1,35 @@
|
||||
import type { PaginateOptions, QueryOptions } from 'mongoose'
|
||||
import type { FindGlobalVersions } from 'payload'
|
||||
|
||||
import { buildVersionGlobalFields, flattenWhereToOperators } from 'payload'
|
||||
import { APIError, buildVersionGlobalFields, flattenWhereToOperators } from 'payload'
|
||||
|
||||
import type { MongooseAdapter } from './index.js'
|
||||
|
||||
import { buildQuery } from './queries/buildQuery.js'
|
||||
import { buildSortParam } from './queries/buildSortParam.js'
|
||||
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
|
||||
import { getGlobal } from './utilities/getEntity.js'
|
||||
import { getSession } from './utilities/getSession.js'
|
||||
import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js'
|
||||
import { transform } from './utilities/transform.js'
|
||||
|
||||
export const findGlobalVersions: FindGlobalVersions = async function findGlobalVersions(
|
||||
this: MongooseAdapter,
|
||||
{ global, limit, locale, page, pagination, req, select, skip, sort: sortArg, where },
|
||||
{
|
||||
global: globalSlug,
|
||||
limit,
|
||||
locale,
|
||||
page,
|
||||
pagination,
|
||||
req,
|
||||
select,
|
||||
skip,
|
||||
sort: sortArg,
|
||||
where = {},
|
||||
},
|
||||
) {
|
||||
const Model = this.versions[global]
|
||||
const versionFields = buildVersionGlobalFields(
|
||||
this.payload.config,
|
||||
this.payload.globals.config.find(({ slug }) => slug === global),
|
||||
true,
|
||||
)
|
||||
const { globalConfig, Model } = getGlobal({ adapter: this, globalSlug, versions: true })
|
||||
|
||||
const versionFields = buildVersionGlobalFields(this.payload.config, globalConfig, true)
|
||||
|
||||
const session = await getSession(this, req)
|
||||
const options: QueryOptions = {
|
||||
@@ -91,10 +100,11 @@ export const findGlobalVersions: FindGlobalVersions = async function findGlobalV
|
||||
}
|
||||
}
|
||||
|
||||
if (limit >= 0) {
|
||||
if (limit && limit >= 0) {
|
||||
paginationOptions.limit = limit
|
||||
// limit must also be set here, it's ignored when pagination is false
|
||||
paginationOptions.options.limit = limit
|
||||
|
||||
paginationOptions.options!.limit = limit
|
||||
|
||||
// Disable pagination if limit is 0
|
||||
if (limit === 0) {
|
||||
@@ -103,13 +113,13 @@ export const findGlobalVersions: FindGlobalVersions = async function findGlobalV
|
||||
}
|
||||
|
||||
const result = await Model.paginate(query, paginationOptions)
|
||||
const docs = JSON.parse(JSON.stringify(result.docs))
|
||||
|
||||
return {
|
||||
...result,
|
||||
docs: docs.map((doc) => {
|
||||
doc.id = doc._id
|
||||
return sanitizeInternalFields(doc)
|
||||
}),
|
||||
}
|
||||
transform({
|
||||
adapter: this,
|
||||
data: result.docs,
|
||||
fields: buildVersionGlobalFields(this.payload.config, globalConfig),
|
||||
operation: 'read',
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -1,20 +1,23 @@
|
||||
import type { AggregateOptions, QueryOptions } from 'mongoose'
|
||||
import type { Document, FindOne } from 'payload'
|
||||
|
||||
import { type FindOne } from 'payload'
|
||||
|
||||
import type { MongooseAdapter } from './index.js'
|
||||
|
||||
import { buildQuery } from './queries/buildQuery.js'
|
||||
import { aggregatePaginate } from './utilities/aggregatePaginate.js'
|
||||
import { buildJoinAggregation } from './utilities/buildJoinAggregation.js'
|
||||
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
|
||||
import { getCollection } from './utilities/getEntity.js'
|
||||
import { getSession } from './utilities/getSession.js'
|
||||
import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js'
|
||||
import { transform } from './utilities/transform.js'
|
||||
|
||||
export const findOne: FindOne = async function findOne(
|
||||
this: MongooseAdapter,
|
||||
{ collection, joins, locale, req, select, where },
|
||||
{ collection: collectionSlug, joins, locale, req, select, where = {} },
|
||||
) {
|
||||
const Model = this.collections[collection]
|
||||
const collectionConfig = this.payload.collections[collection].config
|
||||
const { collectionConfig, Model } = getCollection({ adapter: this, collectionSlug })
|
||||
|
||||
const session = await getSession(this, req)
|
||||
const options: AggregateOptions & QueryOptions = {
|
||||
lean: true,
|
||||
@@ -23,7 +26,7 @@ export const findOne: FindOne = async function findOne(
|
||||
|
||||
const query = await buildQuery({
|
||||
adapter: this,
|
||||
collectionSlug: collection,
|
||||
collectionSlug,
|
||||
fields: collectionConfig.flattenedFields,
|
||||
locale,
|
||||
where,
|
||||
@@ -37,10 +40,9 @@ export const findOne: FindOne = async function findOne(
|
||||
|
||||
const aggregate = await buildJoinAggregation({
|
||||
adapter: this,
|
||||
collection,
|
||||
collection: collectionSlug,
|
||||
collectionConfig,
|
||||
joins,
|
||||
limit: 1,
|
||||
locale,
|
||||
projection,
|
||||
query,
|
||||
@@ -48,7 +50,17 @@ export const findOne: FindOne = async function findOne(
|
||||
|
||||
let doc
|
||||
if (aggregate) {
|
||||
;[doc] = await Model.aggregate(aggregate, { session })
|
||||
const { docs } = await aggregatePaginate({
|
||||
adapter: this,
|
||||
joinAggregation: aggregate,
|
||||
limit: 1,
|
||||
Model,
|
||||
pagination: false,
|
||||
projection,
|
||||
query,
|
||||
session,
|
||||
})
|
||||
doc = docs[0]
|
||||
} else {
|
||||
;(options as Record<string, unknown>).projection = projection
|
||||
doc = await Model.findOne(query, {}, options)
|
||||
@@ -58,11 +70,7 @@ export const findOne: FindOne = async function findOne(
|
||||
return null
|
||||
}
|
||||
|
||||
let result: Document = JSON.parse(JSON.stringify(doc))
|
||||
transform({ adapter: this, data: doc, fields: collectionConfig.fields, operation: 'read' })
|
||||
|
||||
// custom id type reset
|
||||
result.id = result._id
|
||||
result = sanitizeInternalFields(result)
|
||||
|
||||
return result
|
||||
return doc
|
||||
}
|
||||
|
||||
@@ -8,15 +8,31 @@ import type { MongooseAdapter } from './index.js'
|
||||
import { buildQuery } from './queries/buildQuery.js'
|
||||
import { buildSortParam } from './queries/buildSortParam.js'
|
||||
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
|
||||
import { getCollection } from './utilities/getEntity.js'
|
||||
import { getSession } from './utilities/getSession.js'
|
||||
import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js'
|
||||
import { transform } from './utilities/transform.js'
|
||||
|
||||
export const findVersions: FindVersions = async function findVersions(
|
||||
this: MongooseAdapter,
|
||||
{ collection, limit, locale, page, pagination, req = {}, select, skip, sort: sortArg, where },
|
||||
{
|
||||
collection: collectionSlug,
|
||||
limit,
|
||||
locale,
|
||||
page,
|
||||
pagination,
|
||||
req = {},
|
||||
select,
|
||||
skip,
|
||||
sort: sortArg,
|
||||
where = {},
|
||||
},
|
||||
) {
|
||||
const Model = this.versions[collection]
|
||||
const collectionConfig = this.payload.collections[collection].config
|
||||
const { collectionConfig, Model } = getCollection({
|
||||
adapter: this,
|
||||
collectionSlug,
|
||||
versions: true,
|
||||
})
|
||||
|
||||
const session = await getSession(this, req)
|
||||
const options: QueryOptions = {
|
||||
limit,
|
||||
@@ -92,10 +108,11 @@ export const findVersions: FindVersions = async function findVersions(
|
||||
}
|
||||
}
|
||||
|
||||
if (limit >= 0) {
|
||||
if (limit && limit >= 0) {
|
||||
paginationOptions.limit = limit
|
||||
// limit must also be set here, it's ignored when pagination is false
|
||||
paginationOptions.options.limit = limit
|
||||
|
||||
paginationOptions.options!.limit = limit
|
||||
|
||||
// Disable pagination if limit is 0
|
||||
if (limit === 0) {
|
||||
@@ -104,13 +121,13 @@ export const findVersions: FindVersions = async function findVersions(
|
||||
}
|
||||
|
||||
const result = await Model.paginate(query, paginationOptions)
|
||||
const docs = JSON.parse(JSON.stringify(result.docs))
|
||||
|
||||
return {
|
||||
...result,
|
||||
docs: docs.map((doc) => {
|
||||
doc.id = doc._id
|
||||
return sanitizeInternalFields(doc)
|
||||
}),
|
||||
}
|
||||
transform({
|
||||
adapter: this,
|
||||
data: result.docs,
|
||||
fields: buildVersionCollectionFields(this.payload.config, collectionConfig),
|
||||
operation: 'read',
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -11,11 +11,13 @@ import type {
|
||||
BaseDatabaseAdapter,
|
||||
CollectionSlug,
|
||||
DatabaseAdapterObj,
|
||||
Migration,
|
||||
Payload,
|
||||
TypeWithID,
|
||||
TypeWithVersion,
|
||||
UpdateGlobalArgs,
|
||||
UpdateGlobalVersionArgs,
|
||||
UpdateManyArgs,
|
||||
UpdateOneArgs,
|
||||
UpdateVersionArgs,
|
||||
} from 'payload'
|
||||
@@ -53,6 +55,7 @@ import { commitTransaction } from './transactions/commitTransaction.js'
|
||||
import { rollbackTransaction } from './transactions/rollbackTransaction.js'
|
||||
import { updateGlobal } from './updateGlobal.js'
|
||||
import { updateGlobalVersion } from './updateGlobalVersion.js'
|
||||
import { updateMany } from './updateMany.js'
|
||||
import { updateOne } from './updateOne.js'
|
||||
import { updateVersion } from './updateVersion.js'
|
||||
import { upsert } from './upsert.js'
|
||||
@@ -60,6 +63,13 @@ import { upsert } from './upsert.js'
|
||||
export type { MigrateDownArgs, MigrateUpArgs } from './types.js'
|
||||
|
||||
export interface Args {
|
||||
/**
|
||||
* By default, Payload strips all additional keys from MongoDB data that don't exist
|
||||
* in the Payload schema. If you have some data that you want to include to the result
|
||||
* but it doesn't exist in Payload, you can enable this flag
|
||||
* @default false
|
||||
*/
|
||||
allowAdditionalKeys?: boolean
|
||||
/** Set to false to disable auto-pluralization of collection names, Defaults to true */
|
||||
autoPluralization?: boolean
|
||||
/**
|
||||
@@ -86,11 +96,15 @@ export interface Args {
|
||||
* Defaults to disabled.
|
||||
*/
|
||||
collation?: Omit<CollationOptions, 'locale'>
|
||||
|
||||
collectionsSchemaOptions?: Partial<Record<CollectionSlug, SchemaOptions>>
|
||||
|
||||
/** Extra configuration options */
|
||||
connectOptions?: {
|
||||
/** Set false to disable $facet aggregation in non-supporting databases, Defaults to true */
|
||||
/**
|
||||
* Set false to disable $facet aggregation in non-supporting databases, Defaults to true
|
||||
* @deprecated Payload doesn't use `$facet` anymore anywhere.
|
||||
*/
|
||||
useFacet?: boolean
|
||||
} & ConnectOptions
|
||||
/** Set to true to disable hinting to MongoDB to use 'id' as index. This is currently done when counting documents for pagination. Disabling this optimization might fix some problems with AWS DocumentDB. Defaults to false */
|
||||
@@ -105,11 +119,7 @@ export interface Args {
|
||||
* typed as any to avoid dependency
|
||||
*/
|
||||
mongoMemoryServer?: MongoMemoryReplSet
|
||||
prodMigrations?: {
|
||||
down: (args: MigrateDownArgs) => Promise<void>
|
||||
name: string
|
||||
up: (args: MigrateUpArgs) => Promise<void>
|
||||
}[]
|
||||
prodMigrations?: Migration[]
|
||||
transactionOptions?: false | TransactionOptions
|
||||
|
||||
/** The URL to connect to MongoDB or false to start payload and prevent connecting */
|
||||
@@ -160,6 +170,7 @@ declare module 'payload' {
|
||||
updateGlobalVersion: <T extends TypeWithID = TypeWithID>(
|
||||
args: { options?: QueryOptions } & UpdateGlobalVersionArgs<T>,
|
||||
) => Promise<TypeWithVersion<T>>
|
||||
|
||||
updateOne: (args: { options?: QueryOptions } & UpdateOneArgs) => Promise<Document>
|
||||
updateVersion: <T extends TypeWithID = TypeWithID>(
|
||||
args: { options?: QueryOptions } & UpdateVersionArgs<T>,
|
||||
@@ -171,11 +182,12 @@ declare module 'payload' {
|
||||
}
|
||||
|
||||
export function mongooseAdapter({
|
||||
allowAdditionalKeys = false,
|
||||
autoPluralization = true,
|
||||
collectionsSchemaOptions = {},
|
||||
connectOptions,
|
||||
disableIndexHints = false,
|
||||
ensureIndexes,
|
||||
ensureIndexes = false,
|
||||
migrationDir: migrationDirArg,
|
||||
mongoMemoryServer,
|
||||
prodMigrations,
|
||||
@@ -192,17 +204,22 @@ export function mongooseAdapter({
|
||||
// Mongoose-specific
|
||||
autoPluralization,
|
||||
collections: {},
|
||||
// @ts-expect-error initialize without a connection
|
||||
connection: undefined,
|
||||
connectOptions: connectOptions || {},
|
||||
disableIndexHints,
|
||||
ensureIndexes,
|
||||
// @ts-expect-error don't have globals model yet
|
||||
globals: undefined,
|
||||
// @ts-expect-error Should not be required
|
||||
mongoMemoryServer,
|
||||
sessions: {},
|
||||
transactionOptions: transactionOptions === false ? undefined : transactionOptions,
|
||||
updateMany,
|
||||
url,
|
||||
versions: {},
|
||||
// DatabaseAdapter
|
||||
allowAdditionalKeys,
|
||||
beginTransaction: transactionOptions === false ? defaultBeginTransaction() : beginTransaction,
|
||||
collectionsSchemaOptions,
|
||||
commitTransaction,
|
||||
|
||||
@@ -2,12 +2,15 @@ import type { PaginateOptions } from 'mongoose'
|
||||
import type { Init, SanitizedCollectionConfig } from 'payload'
|
||||
|
||||
import mongoose from 'mongoose'
|
||||
import mongooseAggregatePaginate from 'mongoose-aggregate-paginate-v2'
|
||||
import paginate from 'mongoose-paginate-v2'
|
||||
import { buildVersionCollectionFields, buildVersionGlobalFields } from 'payload'
|
||||
import {
|
||||
buildVersionCollectionFields,
|
||||
buildVersionCompoundIndexes,
|
||||
buildVersionGlobalFields,
|
||||
} from 'payload'
|
||||
|
||||
import type { MongooseAdapter } from './index.js'
|
||||
import type { CollectionModel } from './types.js'
|
||||
import type { CollectionModel, GlobalModel } from './types.js'
|
||||
|
||||
import { buildCollectionSchema } from './models/buildCollectionSchema.js'
|
||||
import { buildGlobalModel } from './models/buildGlobalModel.js'
|
||||
@@ -17,7 +20,7 @@ import { getDBName } from './utilities/getDBName.js'
|
||||
|
||||
export const init: Init = function init(this: MongooseAdapter) {
|
||||
this.payload.config.collections.forEach((collection: SanitizedCollectionConfig) => {
|
||||
const schemaOptions = this.collectionsSchemaOptions[collection.slug]
|
||||
const schemaOptions = this.collectionsSchemaOptions?.[collection.slug]
|
||||
|
||||
const schema = buildCollectionSchema(collection, this.payload, schemaOptions)
|
||||
|
||||
@@ -37,6 +40,7 @@ export const init: Init = function init(this: MongooseAdapter) {
|
||||
},
|
||||
...schemaOptions,
|
||||
},
|
||||
compoundIndexes: buildVersionCompoundIndexes({ indexes: collection.sanitizedIndexes }),
|
||||
configFields: versionCollectionFields,
|
||||
payload: this.payload,
|
||||
})
|
||||
@@ -48,10 +52,6 @@ export const init: Init = function init(this: MongooseAdapter) {
|
||||
}),
|
||||
)
|
||||
|
||||
if (Object.keys(collection.joins).length > 0) {
|
||||
versionSchema.plugin(mongooseAggregatePaginate)
|
||||
}
|
||||
|
||||
const versionCollectionName =
|
||||
this.autoPluralization === true && !collection.dbName ? undefined : versionModelName
|
||||
|
||||
@@ -59,21 +59,21 @@ export const init: Init = function init(this: MongooseAdapter) {
|
||||
versionModelName,
|
||||
versionSchema,
|
||||
versionCollectionName,
|
||||
) as CollectionModel
|
||||
) as unknown as CollectionModel
|
||||
}
|
||||
|
||||
const modelName = getDBName({ config: collection })
|
||||
const collectionName =
|
||||
this.autoPluralization === true && !collection.dbName ? undefined : modelName
|
||||
|
||||
this.collections[collection.slug] = mongoose.model(
|
||||
this.collections[collection.slug] = mongoose.model<any>(
|
||||
modelName,
|
||||
schema,
|
||||
collectionName,
|
||||
) as CollectionModel
|
||||
})
|
||||
|
||||
this.globals = buildGlobalModel(this.payload)
|
||||
this.globals = buildGlobalModel(this.payload) as GlobalModel
|
||||
|
||||
this.payload.config.globals.forEach((global) => {
|
||||
if (global.versions) {
|
||||
@@ -101,7 +101,7 @@ export const init: Init = function init(this: MongooseAdapter) {
|
||||
}),
|
||||
)
|
||||
|
||||
this.versions[global.slug] = mongoose.model(
|
||||
this.versions[global.slug] = mongoose.model<any>(
|
||||
versionModelName,
|
||||
versionSchema,
|
||||
versionModelName,
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import type { PayloadRequest } from 'payload'
|
||||
|
||||
import { commitTransaction, initTransaction, killTransaction, readMigrationFiles } from 'payload'
|
||||
import prompts from 'prompts'
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { PaginateOptions, Schema } from 'mongoose'
|
||||
import type { Payload, SanitizedCollectionConfig } from 'payload'
|
||||
|
||||
import mongooseAggregatePaginate from 'mongoose-aggregate-paginate-v2'
|
||||
import paginate from 'mongoose-paginate-v2'
|
||||
|
||||
import { getBuildQueryPlugin } from '../queries/getBuildQueryPlugin.js'
|
||||
@@ -24,12 +23,13 @@ export const buildCollectionSchema = (
|
||||
...schemaOptions,
|
||||
},
|
||||
},
|
||||
compoundIndexes: collection.sanitizedIndexes,
|
||||
configFields: collection.fields,
|
||||
payload,
|
||||
})
|
||||
|
||||
if (Array.isArray(collection.upload.filenameCompoundIndex)) {
|
||||
const indexDefinition: Record<string, 1> = collection.upload.filenameCompoundIndex.reduce(
|
||||
const indexDefinition = collection.upload.filenameCompoundIndex.reduce<Record<string, 1>>(
|
||||
(acc, index) => {
|
||||
acc[index] = 1
|
||||
return acc
|
||||
@@ -44,12 +44,5 @@ export const buildCollectionSchema = (
|
||||
.plugin<any, PaginateOptions>(paginate, { useEstimatedCount: true })
|
||||
.plugin(getBuildQueryPlugin({ collectionSlug: collection.slug }))
|
||||
|
||||
if (
|
||||
Object.keys(collection.joins).length > 0 ||
|
||||
Object.keys(collection.polymorphicJoins).length > 0
|
||||
) {
|
||||
schema.plugin(mongooseAggregatePaginate)
|
||||
}
|
||||
|
||||
return schema
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user