Compare commits
49 Commits
v3.46.0
...
postgres-d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0333e2cd1c | ||
|
|
bbf0c2474d | ||
|
|
5f019533d8 | ||
|
|
277448d9c0 | ||
|
|
5839cb61fa | ||
|
|
f4d951dd04 | ||
|
|
7294cf561d | ||
|
|
4831bae6b5 | ||
|
|
4c69f8e205 | ||
|
|
de53f689e3 | ||
|
|
edd1f02eb5 | ||
|
|
d213c9150d | ||
|
|
a57faebfd7 | ||
|
|
53ad02a8e8 | ||
|
|
e5755110a9 | ||
|
|
e5f64f7952 | ||
|
|
2cafe494cc | ||
|
|
576644d0b5 | ||
|
|
8a3b97c643 | ||
|
|
06ef798653 | ||
|
|
5695d22a46 | ||
|
|
19a3367972 | ||
|
|
c1bad0115a | ||
|
|
b3a994ed6f | ||
|
|
f63dfad565 | ||
|
|
2d91cb613c | ||
|
|
0c2b1054e2 | ||
|
|
cb6a73e1b4 | ||
|
|
055cc4ef12 | ||
|
|
c77b39c3b4 | ||
|
|
5e82f9ff41 | ||
|
|
c6105f1e0d | ||
|
|
0806ee1762 | ||
|
|
e99c67f5f9 | ||
|
|
1c6a79bb57 | ||
|
|
a7a05012fb | ||
|
|
1d6ffcb80e | ||
|
|
dde9681089 | ||
|
|
c876ddf858 | ||
|
|
855a320474 | ||
|
|
aa97f3cddb | ||
|
|
0b88466de6 | ||
|
|
fee33b59ee | ||
|
|
417b70e16c | ||
|
|
9f1bff57c1 | ||
|
|
4c25357831 | ||
|
|
8a5cb27463 | ||
|
|
9c453210f8 | ||
|
|
96c24a22da |
63
.github/actions/setup/action.yml
vendored
63
.github/actions/setup/action.yml
vendored
@@ -4,24 +4,17 @@ description: |
|
||||
|
||||
inputs:
|
||||
node-version:
|
||||
description: Node.js version
|
||||
required: true
|
||||
default: 23.11.0
|
||||
description: Node.js version override
|
||||
pnpm-version:
|
||||
description: Pnpm version
|
||||
required: true
|
||||
default: 9.7.1
|
||||
description: Pnpm version override
|
||||
pnpm-run-install:
|
||||
description: Whether to run pnpm install
|
||||
required: false
|
||||
default: true
|
||||
pnpm-restore-cache:
|
||||
description: Whether to restore cache
|
||||
required: false
|
||||
default: true
|
||||
pnpm-install-cache-key:
|
||||
description: The cache key for the pnpm install cache
|
||||
default: pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
description: The cache key override for the pnpm install cache
|
||||
|
||||
outputs:
|
||||
pnpm-store-path:
|
||||
@@ -37,15 +30,44 @@ runs:
|
||||
shell: bash
|
||||
run: sudo ethtool -K eth0 tx off rx off
|
||||
|
||||
- name: Get versions from .tool-versions or use overrides
|
||||
shell: bash
|
||||
run: |
|
||||
# if node-version input is provided, use it; otherwise, read from .tool-versions
|
||||
if [ "${{ inputs.node-version }}" ]; then
|
||||
echo "Node version override provided: ${{ inputs.node-version }}"
|
||||
echo "NODE_VERSION=${{ inputs.node-version }}" >> $GITHUB_ENV
|
||||
elif [ -f .tool-versions ]; then
|
||||
NODE_VERSION=$(grep '^nodejs ' .tool-versions | awk '{print $2}')
|
||||
echo "NODE_VERSION=$NODE_VERSION" >> $GITHUB_ENV
|
||||
echo "Node version resolved to: $NODE_VERSION"
|
||||
else
|
||||
echo "No .tool-versions file found and no node-version input provided. Invalid configuration."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# if pnpm-version input is provided, use it; otherwise, read from .tool-versions
|
||||
if [ "${{ inputs.pnpm-version }}" ]; then
|
||||
echo "Pnpm version override provided: ${{ inputs.pnpm-version }}"
|
||||
echo "PNPM_VERSION=${{ inputs.pnpm-version }}" >> $GITHUB_ENV
|
||||
elif [ -f .tool-versions ]; then
|
||||
PNPM_VERSION=$(grep '^pnpm ' .tool-versions | awk '{print $2}')
|
||||
echo "PNPM_VERSION=$PNPM_VERSION" >> $GITHUB_ENV
|
||||
echo "Pnpm version resolved to: $PNPM_VERSION"
|
||||
else
|
||||
echo "No .tool-versions file found and no pnpm-version input provided. Invalid configuration."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Setup Node@${{ inputs.node-version }}
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ inputs.node-version }}
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: ${{ inputs.pnpm-version }}
|
||||
version: ${{ env.PNPM_VERSION }}
|
||||
run_install: false
|
||||
|
||||
- name: Get pnpm store path
|
||||
@@ -55,14 +77,25 @@ runs:
|
||||
echo "STORE_PATH=$STORE_PATH" >> $GITHUB_ENV
|
||||
echo "Pnpm store path resolved to: $STORE_PATH"
|
||||
|
||||
- name: Compute Cache Key
|
||||
shell: bash
|
||||
run: |
|
||||
if [ -n "${{ inputs.pnpm-install-cache-key }}" ]; then
|
||||
PNPM_INSTALL_CACHE_KEY="${{ inputs.pnpm-install-cache-key }}"
|
||||
else
|
||||
PNPM_INSTALL_CACHE_KEY="pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}"
|
||||
fi
|
||||
echo "Computed PNPM_INSTALL_CACHE_KEY: $PNPM_INSTALL_CACHE_KEY"
|
||||
echo "PNPM_INSTALL_CACHE_KEY=$PNPM_INSTALL_CACHE_KEY" >> $GITHUB_ENV
|
||||
|
||||
- name: Restore pnpm install cache
|
||||
if: ${{ inputs.pnpm-restore-cache == 'true' }}
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ${{ env.STORE_PATH }}
|
||||
key: ${{ inputs.pnpm-install-cache-key }}
|
||||
key: ${{ env.PNPM_INSTALL_CACHE_KEY }}
|
||||
restore-keys: |
|
||||
pnpm-store-${{ inputs.pnpm-version }}-
|
||||
pnpm-store-${{ env.PNPM_VERSION }}-
|
||||
pnpm-store-
|
||||
|
||||
- name: Run pnpm install
|
||||
@@ -72,5 +105,5 @@ runs:
|
||||
|
||||
# Set the cache key output
|
||||
- run: |
|
||||
echo "pnpm-install-cache-key=${{ inputs.pnpm-install-cache-key }}" >> $GITHUB_ENV
|
||||
echo "pnpm-install-cache-key=${{ env.PNPM_INSTALL_CACHE_KEY }}" >> $GITHUB_OUTPUT
|
||||
shell: bash
|
||||
|
||||
30
.github/workflows/audit-dependencies.sh
vendored
Executable file
30
.github/workflows/audit-dependencies.sh
vendored
Executable file
@@ -0,0 +1,30 @@
|
||||
#!/bin/bash
|
||||
|
||||
severity=${1:-"critical"}
|
||||
audit_json=$(pnpm audit --prod --json)
|
||||
output_file="audit_output.json"
|
||||
|
||||
echo "Auditing for ${severity} vulnerabilities..."
|
||||
|
||||
echo "${audit_json}" | jq --arg severity "${severity}" '
|
||||
.advisories | to_entries |
|
||||
map(select(.value.patched_versions != "<0.0.0" and .value.severity == $severity) |
|
||||
{
|
||||
package: .value.module_name,
|
||||
vulnerable: .value.vulnerable_versions,
|
||||
fixed_in: .value.patched_versions
|
||||
}
|
||||
)
|
||||
' >$output_file
|
||||
|
||||
audit_length=$(jq 'length' $output_file)
|
||||
|
||||
if [[ "${audit_length}" -gt "0" ]]; then
|
||||
echo "Actionable vulnerabilities found in the following packages:"
|
||||
jq -r '.[] | "\u001b[1m\(.package)\u001b[0m vulnerable in \u001b[31m\(.vulnerable)\u001b[0m fixed in \u001b[32m\(.fixed_in)\u001b[0m"' $output_file | while read -r line; do echo -e "$line"; done
|
||||
echo "Output written to ${output_file}"
|
||||
exit 1
|
||||
else
|
||||
echo "No actionable vulnerabilities"
|
||||
exit 0
|
||||
fi
|
||||
53
.github/workflows/audit-dependencies.yml
vendored
Normal file
53
.github/workflows/audit-dependencies.yml
vendored
Normal file
@@ -0,0 +1,53 @@
|
||||
name: audit-dependencies
|
||||
|
||||
on:
|
||||
# Sundays at 2am EST
|
||||
schedule:
|
||||
- cron: '0 7 * * 0'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
audit-level:
|
||||
description: The level of audit to run (low, moderate, high, critical)
|
||||
required: false
|
||||
default: critical
|
||||
debug:
|
||||
description: Enable debug logging
|
||||
required: false
|
||||
default: false
|
||||
|
||||
env:
|
||||
DO_NOT_TRACK: 1 # Disable Turbopack telemetry
|
||||
NEXT_TELEMETRY_DISABLED: 1 # Disable Next telemetry
|
||||
|
||||
jobs:
|
||||
audit:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup
|
||||
uses: ./.github/actions/setup
|
||||
|
||||
- name: Run audit dependencies script
|
||||
id: audit_dependencies
|
||||
run: ./.github/workflows/audit-dependencies.sh ${{ inputs.audit-level }}
|
||||
|
||||
- name: Slack notification on failure
|
||||
if: failure()
|
||||
uses: slackapi/slack-github-action@v2.1.0
|
||||
with:
|
||||
webhook: ${{ inputs.debug == 'true' && secrets.SLACK_TEST_WEBHOOK_URL || secrets.SLACK_WEBHOOK_URL }}
|
||||
webhook-type: incoming-webhook
|
||||
payload: |
|
||||
{
|
||||
"username": "GitHub Actions Bot",
|
||||
"blocks": [
|
||||
{
|
||||
"type": "section",
|
||||
"text": {
|
||||
"type": "mrkdwn",
|
||||
"text": "🚨 Actionable vulnerabilities found: <https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}|View Details>"
|
||||
}
|
||||
},
|
||||
]
|
||||
}
|
||||
34
.github/workflows/main.yml
vendored
34
.github/workflows/main.yml
vendored
@@ -17,8 +17,6 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
NODE_VERSION: 23.11.0
|
||||
PNPM_VERSION: 9.7.1
|
||||
DO_NOT_TRACK: 1 # Disable Turbopack telemetry
|
||||
NEXT_TELEMETRY_DISABLED: 1 # Disable Next telemetry
|
||||
|
||||
@@ -71,10 +69,6 @@ jobs:
|
||||
|
||||
- name: Node setup
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
pnpm-version: ${{ env.PNPM_VERSION }}
|
||||
pnpm-install-cache-key: pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
|
||||
- name: Lint
|
||||
run: pnpm lint -- --quiet
|
||||
@@ -89,10 +83,6 @@ jobs:
|
||||
|
||||
- name: Node setup
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
pnpm-version: ${{ env.PNPM_VERSION }}
|
||||
pnpm-install-cache-key: pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
|
||||
- run: pnpm run build:all
|
||||
env:
|
||||
@@ -114,11 +104,8 @@ jobs:
|
||||
- name: Node setup
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
pnpm-version: ${{ env.PNPM_VERSION }}
|
||||
pnpm-run-install: false
|
||||
pnpm-restore-cache: false # Full build is restored below
|
||||
pnpm-install-cache-key: pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
|
||||
- name: Restore build
|
||||
uses: actions/cache@v4
|
||||
@@ -141,11 +128,8 @@ jobs:
|
||||
- name: Node setup
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
pnpm-version: ${{ env.PNPM_VERSION }}
|
||||
pnpm-run-install: false
|
||||
pnpm-restore-cache: false # Full build is restored below
|
||||
pnpm-install-cache-key: pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
|
||||
- name: Restore build
|
||||
uses: actions/cache@v4
|
||||
@@ -205,11 +189,8 @@ jobs:
|
||||
- name: Node setup
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
pnpm-version: ${{ env.PNPM_VERSION }}
|
||||
pnpm-run-install: false
|
||||
pnpm-restore-cache: false # Full build is restored below
|
||||
pnpm-install-cache-key: pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
|
||||
- name: Restore build
|
||||
uses: actions/cache@v4
|
||||
@@ -330,11 +311,8 @@ jobs:
|
||||
- name: Node setup
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
pnpm-version: ${{ env.PNPM_VERSION }}
|
||||
pnpm-run-install: false
|
||||
pnpm-restore-cache: false # Full build is restored below
|
||||
pnpm-install-cache-key: pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
|
||||
- name: Restore build
|
||||
uses: actions/cache@v4
|
||||
@@ -467,11 +445,8 @@ jobs:
|
||||
- name: Node setup
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
pnpm-version: ${{ env.PNPM_VERSION }}
|
||||
pnpm-run-install: false
|
||||
pnpm-restore-cache: false # Full build is restored below
|
||||
pnpm-install-cache-key: pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
|
||||
- name: Restore build
|
||||
uses: actions/cache@v4
|
||||
@@ -573,11 +548,8 @@ jobs:
|
||||
- name: Node setup
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
pnpm-version: ${{ env.PNPM_VERSION }}
|
||||
pnpm-run-install: false
|
||||
pnpm-restore-cache: false # Full build is restored below
|
||||
pnpm-install-cache-key: pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
|
||||
- name: Restore build
|
||||
uses: actions/cache@v4
|
||||
@@ -673,11 +645,8 @@ jobs:
|
||||
- name: Node setup
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
pnpm-version: ${{ env.PNPM_VERSION }}
|
||||
pnpm-run-install: false
|
||||
pnpm-restore-cache: false # Full build is restored below
|
||||
pnpm-install-cache-key: pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
|
||||
- name: Restore build
|
||||
uses: actions/cache@v4
|
||||
@@ -735,11 +704,8 @@ jobs:
|
||||
- name: Node setup
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
pnpm-version: ${{ env.PNPM_VERSION }}
|
||||
pnpm-run-install: false
|
||||
pnpm-restore-cache: false # Full build is restored below
|
||||
pnpm-install-cache-key: pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
|
||||
- name: Restore build
|
||||
uses: actions/cache@v4
|
||||
|
||||
10
.github/workflows/post-release-templates.yml
vendored
10
.github/workflows/post-release-templates.yml
vendored
@@ -7,8 +7,6 @@ on:
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
NODE_VERSION: 23.11.0
|
||||
PNPM_VERSION: 9.7.1
|
||||
DO_NOT_TRACK: 1 # Disable Turbopack telemetry
|
||||
NEXT_TELEMETRY_DISABLED: 1 # Disable Next telemetry
|
||||
|
||||
@@ -60,9 +58,6 @@ jobs:
|
||||
|
||||
- name: Setup
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
pnpm-version: ${{ env.PNPM_VERSION }}
|
||||
|
||||
- name: Start PostgreSQL
|
||||
uses: CasperWA/postgresql-action@v1.2
|
||||
@@ -87,6 +82,11 @@ jobs:
|
||||
with:
|
||||
mongodb-version: 6.0
|
||||
|
||||
# The template generation script runs import map generation which needs the built payload bin scripts
|
||||
- run: pnpm run build:all
|
||||
env:
|
||||
DO_NOT_TRACK: 1 # Disable Turbopack telemetry
|
||||
|
||||
- name: Update template lockfiles and migrations
|
||||
run: pnpm script:gen-templates
|
||||
|
||||
|
||||
2
.github/workflows/post-release.yml
vendored
2
.github/workflows/post-release.yml
vendored
@@ -12,8 +12,6 @@ on:
|
||||
default: ''
|
||||
|
||||
env:
|
||||
NODE_VERSION: 23.11.0
|
||||
PNPM_VERSION: 9.7.1
|
||||
DO_NOT_TRACK: 1 # Disable Turbopack telemetry
|
||||
NEXT_TELEMETRY_DISABLED: 1 # Disable Next telemetry
|
||||
|
||||
|
||||
5
.github/workflows/publish-prerelease.yml
vendored
5
.github/workflows/publish-prerelease.yml
vendored
@@ -7,8 +7,6 @@ on:
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
NODE_VERSION: 23.11.0
|
||||
PNPM_VERSION: 9.7.1
|
||||
DO_NOT_TRACK: 1 # Disable Turbopack telemetry
|
||||
NEXT_TELEMETRY_DISABLED: 1 # Disable Next telemetry
|
||||
|
||||
@@ -23,9 +21,6 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
pnpm-version: ${{ env.PNPM_VERSION }}
|
||||
- name: Load npm token
|
||||
run: echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> ~/.npmrc
|
||||
env:
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -27,6 +27,8 @@ packages/ui/esbuild
|
||||
packages/next/esbuild
|
||||
packages/richtext-lexical/esbuild
|
||||
|
||||
audit_output.json
|
||||
|
||||
.turbo
|
||||
|
||||
# Ignore test directory media folder/files
|
||||
|
||||
@@ -77,9 +77,13 @@ If you wish to use your own MongoDB database for the `test` directory instead of
|
||||
|
||||
### Using Postgres
|
||||
|
||||
If you have postgres installed on your system, you can also run the test suites using postgres. By default, mongodb is used.
|
||||
Our test suites supports automatic PostgreSQL + PostGIS setup using Docker. No local PostgreSQL installation required. By default, mongodb is used.
|
||||
|
||||
To do that, simply set the `PAYLOAD_DATABASE` environment variable to `postgres`.
|
||||
To use postgres, simply set the `PAYLOAD_DATABASE` environment variable to `postgres`.
|
||||
|
||||
```bash
|
||||
PAYLOAD_DATABASE=postgres pnpm dev {suite}
|
||||
```
|
||||
|
||||
### Running the e2e and int tests
|
||||
|
||||
|
||||
@@ -114,7 +114,12 @@ const MyComponent: React.FC = () => {
|
||||
|
||||
## useAllFormFields
|
||||
|
||||
**To retrieve more than one field**, you can use the `useAllFormFields` hook. Your component will re-render when _any_ field changes, so use this hook only if you absolutely need to. Unlike the `useFormFields` hook, this hook does not accept a "selector", and it always returns an array with type of `[fields: Fields, dispatch: React.Dispatch<Action>]]`.
|
||||
**To retrieve more than one field**, you can use the `useAllFormFields` hook. Unlike the `useFormFields` hook, this hook does not accept a "selector", and it always returns an array with type of `[fields: Fields, dispatch: React.Dispatch<Action>]]`.
|
||||
|
||||
<Banner type="warning">
|
||||
**Warning:** Your component will re-render when _any_ field changes, so use
|
||||
this hook only if you absolutely need to.
|
||||
</Banner>
|
||||
|
||||
You can do lots of powerful stuff by retrieving the full form state, like using built-in helper functions to reduce field state to values only, or to retrieve sibling data by path.
|
||||
|
||||
|
||||
@@ -60,32 +60,32 @@ export const Posts: CollectionConfig = {
|
||||
|
||||
The following options are available:
|
||||
|
||||
| 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) |
|
||||
| `disableDuplicate` | When true, do not show the "Duplicate" button while editing documents within this Collection and prevent `duplicate` from all APIs. |
|
||||
| `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). |
|
||||
| `graphQL` | Manage GraphQL-related properties for this collection. [More](#graphql) |
|
||||
| `hooks` | Entry point for Hooks. [More details](../hooks/overview#collection-hooks). |
|
||||
| `orderable` | If true, enables custom ordering for the collection, and documents can be reordered via drag and drop. Uses [fractional indexing](https://observablehq.com/@dgreensp/implementing-fractional-indexing) for efficient reordering. |
|
||||
| `labels` | Singular and plural labels for use in identifying this Collection throughout Payload. Auto-generated from slug if not defined. |
|
||||
| `enableQueryPresets` | Enable query presets for this Collection. [More details](../query-presets/overview). |
|
||||
| `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. |
|
||||
| `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. |
|
||||
| `forceSelect` | Specify which fields should be selected always, regardless of the `select` query which can be useful that the field exists for access control / hooks |
|
||||
| `disableBulkEdit` | Disable the bulk edit operation for the collection in the admin panel and the REST API |
|
||||
| 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) |
|
||||
| `disableDuplicate` | When true, do not show the "Duplicate" button while editing documents within this Collection and prevent `duplicate` from all APIs. |
|
||||
| `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). |
|
||||
| `graphQL` | Manage GraphQL-related properties for this collection. [More](#graphql) |
|
||||
| `hooks` | Entry point for Hooks. [More details](../hooks/overview#collection-hooks). |
|
||||
| `orderable` | If true, enables custom ordering for the collection, and documents can be reordered via drag and drop. Uses [fractional indexing](https://observablehq.com/@dgreensp/implementing-fractional-indexing) for efficient reordering. |
|
||||
| `labels` | Singular and plural labels for use in identifying this Collection throughout Payload. Auto-generated from slug if not defined. |
|
||||
| `enableQueryPresets` | Enable query presets for this Collection. [More details](../query-presets/overview). |
|
||||
| `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. |
|
||||
| `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. [More details](../database/indexes#compound-indexes). |
|
||||
| `forceSelect` | Specify which fields should be selected always, regardless of the `select` query which can be useful that the field exists for access control / hooks |
|
||||
| `disableBulkEdit` | Disable the bulk edit operation for the collection in the admin panel and the REST API |
|
||||
|
||||
_\* An asterisk denotes that a property is required._
|
||||
|
||||
@@ -121,26 +121,33 @@ export const MyCollection: CollectionConfig = {
|
||||
|
||||
The following options are available:
|
||||
|
||||
| Option | Description |
|
||||
| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `group` | Text or localization object used to group Collection and Global links in the admin navigation. Set to `false` to hide the link from the navigation while keeping its routes accessible. |
|
||||
| `hidden` | Set to true or a function, called with the current user, returning true to exclude this Collection from navigation and admin routing. |
|
||||
| `hooks` | Admin-specific hooks for this Collection. [More details](../hooks/collections). |
|
||||
| `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, unless it's linked to a relationship'. |
|
||||
| `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. |
|
||||
| `folders` | A boolean to enable folders for a given collection. Defaults to `false`. [More details](../folders/overview). |
|
||||
| `meta` | Page metadata overrides to apply to this Collection within the Admin Panel. [More details](../admin/metadata). |
|
||||
| `preview` | Function to generate preview URLs within the Admin Panel that can point to your app. [More details](../admin/preview). |
|
||||
| `livePreview` | Enable real-time editing for instant visual feedback of your front-end application. [More details](../live-preview/overview). |
|
||||
| `components` | Swap in your own React components to be used within this Collection. [More details](#custom-components). |
|
||||
| `listSearchableFields` | Specify which fields should be searched in the List search view. [More details](#list-searchable-fields). |
|
||||
| `pagination` | Set pagination-specific options for this Collection. [More details](#pagination). |
|
||||
| `baseListFilter` | You can define a default base filter for this collection's List view, which will be merged into any filters that the user performs. |
|
||||
| Option | Description |
|
||||
| ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `group` | Text or localization object used to group Collection and Global links in the admin navigation. Set to `false` to hide the link from the navigation while keeping its routes accessible. |
|
||||
| `hidden` | Set to true or a function, called with the current user, returning true to exclude this Collection from navigation and admin routing. |
|
||||
| `hooks` | Admin-specific hooks for this Collection. [More details](../hooks/collections). |
|
||||
| `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. |
|
||||
| `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. |
|
||||
| `folders` | A boolean to enable folders for a given collection. Defaults to `false`. [More details](../folders/overview). |
|
||||
| `meta` | Page metadata overrides to apply to this Collection within the Admin Panel. [More details](../admin/metadata). |
|
||||
| `preview` | Function to generate preview URLs within the Admin Panel that can point to your app. [More details](../admin/preview). |
|
||||
| `livePreview` | Enable real-time editing for instant visual feedback of your front-end application. [More details](../live-preview/overview). |
|
||||
| `components` | Swap in your own React components to be used within this Collection. [More details](#custom-components). |
|
||||
| `listSearchableFields` | Specify which fields should be searched in the List search view. [More details](#list-searchable-fields). |
|
||||
| `pagination` | Set pagination-specific options for this Collection. [More details](#pagination). |
|
||||
| `baseListFilter` | You can define a default base filter for this collection's List view, which will be merged into any filters that the user performs. |
|
||||
|
||||
<Banner type="warning">
|
||||
**Note:** If you set `useAsTitle` to a relationship or join field, it will use
|
||||
only the ID of the related document(s) as the title. To display a specific
|
||||
field (i.e. title) from the related document instead, create a virtual field
|
||||
that extracts the desired data, and set `useAsTitle` to that virtual field.
|
||||
</Banner>
|
||||
|
||||
### Custom Components
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
title: Environment Variables
|
||||
label: Environment Variables
|
||||
order: 100
|
||||
order: 60
|
||||
desc: Learn how to use Environment Variables in your Payload project
|
||||
---
|
||||
|
||||
@@ -72,7 +72,7 @@ const MyClientComponent = () => {
|
||||
}
|
||||
```
|
||||
|
||||
For more information, check out the [Next.js Documentation](https://nextjs.org/docs/app/building-your-application/configuring/environment-variables).
|
||||
For more information, check out the [Next.js documentation](https://nextjs.org/docs/app/building-your-application/configuring/environment-variables).
|
||||
|
||||
## Outside of Next.js
|
||||
|
||||
|
||||
@@ -110,7 +110,7 @@ _\* An asterisk denotes that a property is required._
|
||||
details](../custom-components/overview#accessing-the-payload-config).
|
||||
</Banner>
|
||||
|
||||
### Typescript Config
|
||||
### TypeScript Config
|
||||
|
||||
Payload exposes a variety of TypeScript settings that you can leverage. These settings are used to auto-generate TypeScript interfaces for your [Collections](./collections) and [Globals](./globals), and to ensure that Payload uses your [Generated Types](../typescript/overview) for all [Local API](../local-api/overview) methods.
|
||||
|
||||
@@ -121,10 +121,11 @@ import { buildConfig } from 'payload'
|
||||
|
||||
export default buildConfig({
|
||||
// ...
|
||||
// highlight-start
|
||||
typescript: {
|
||||
// highlight-line
|
||||
// ...
|
||||
},
|
||||
// highlight-end
|
||||
})
|
||||
```
|
||||
|
||||
@@ -227,7 +228,9 @@ import { buildConfig } from 'payload'
|
||||
|
||||
export default buildConfig({
|
||||
// ...
|
||||
cors: '*', // highlight-line
|
||||
// highlight-start
|
||||
cors: '*',
|
||||
// highlight-end
|
||||
})
|
||||
```
|
||||
|
||||
|
||||
@@ -505,3 +505,51 @@ Payload also exports its [SCSS](https://sass-lang.com) library for reuse which i
|
||||
**Note:** You can also drill into Payload's own component styles, or easily
|
||||
apply global, app-wide CSS. More on that [here](../admin/customizing-css).
|
||||
</Banner>
|
||||
|
||||
## Performance
|
||||
|
||||
An often overlooked aspect of Custom Components is performance. If unchecked, Custom Components can lead to slow load times of the Admin Panel and ultimately a poor user experience.
|
||||
|
||||
This is different from front-end performance of your public-facing site.
|
||||
|
||||
<Banner type="success">
|
||||
For more performance tips, see the [Performance
|
||||
documentation](../performance/overview).
|
||||
</Banner>
|
||||
|
||||
### Follow React and Next.js best practices
|
||||
|
||||
All Custom Components are built using [React](https://react.dev). For this reason, it is important to follow React best practices. This includes using memoization, streaming, caching, optimizing renders, using hooks appropriately, and more.
|
||||
|
||||
To learn more, see the [React documentation](https://react.dev/learn).
|
||||
|
||||
The Admin Panel itself is a [Next.js](https://nextjs.org) application. For this reason, it is _also_ important to follow Next.js best practices. This includes bundling, when to use layouts vs pages, where to place the server/client boundary, and more.
|
||||
|
||||
To learn more, see the [Next.js documentation](https://nextjs.org/docs).
|
||||
|
||||
### Reducing initial HTML size
|
||||
|
||||
With Server Components, be aware of what is being sent to through the server/client boundary. All props are serialized and sent through the network. This can lead to large HTML sizes and slow initial load times if too much data is being sent to the client.
|
||||
|
||||
To minimize this, you must be explicit about what props are sent to the client. Prefer server components and only send the necessary props to the client. This will also offset some of the JS execution to the server.
|
||||
|
||||
<Banner type="success">
|
||||
**Tip:** Use [React Suspense](https://react.dev/reference/react/Suspense) to
|
||||
progressively load components and improve perceived performance.
|
||||
</Banner>
|
||||
|
||||
### Prevent unnecessary re-renders
|
||||
|
||||
If subscribing your component to form state, it may be re-rendering more often than necessary.
|
||||
|
||||
To do this, use the [`useFormFields`](../admin/react-hooks) hook instead of `useFields` when you only need to access specific fields.
|
||||
|
||||
```ts
|
||||
'use client'
|
||||
import { useFormFields } from '@payloadcms/ui'
|
||||
|
||||
const MyComponent: TextFieldClientComponent = ({ path }) => {
|
||||
const value = useFormFields(([fields, dispatch]) => fields[path])
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
65
docs/database/indexes.mdx
Normal file
65
docs/database/indexes.mdx
Normal file
@@ -0,0 +1,65 @@
|
||||
---
|
||||
title: Indexes
|
||||
label: Indexes
|
||||
order: 40
|
||||
keywords: database, indexes
|
||||
desc: Index fields to produce faster queries.
|
||||
---
|
||||
|
||||
Database indexes are a way to optimize the performance of your database by allowing it to quickly locate and retrieve data. If you have a field that you frequently query or sort by, adding an index to that field can significantly improve the speed of those operations.
|
||||
|
||||
When your query runs, the database will not scan the entire document to find that one field, but will instead use the index to quickly locate the data.
|
||||
|
||||
To index a field, set the `index` option to `true` in your field's config:
|
||||
|
||||
```ts
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
export MyCollection: CollectionConfig = {
|
||||
// ...
|
||||
fields: [
|
||||
// ...
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
// highlight-start
|
||||
index: true,
|
||||
// highlight-end
|
||||
},
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
<Banner type="info">
|
||||
**Note:** The `id`, `createdAt`, and `updatedAt` fields are indexed by
|
||||
default.
|
||||
</Banner>
|
||||
|
||||
<Banner type="success">
|
||||
**Tip:** If you're using MongoDB, you can use [MongoDB
|
||||
Compass](https://www.mongodb.com/products/compass) to visualize and manage
|
||||
your indexes.
|
||||
</Banner>
|
||||
|
||||
## Compound Indexes
|
||||
|
||||
In addition to indexing single fields, you can also create compound indexes that index multiple fields together. This can be useful for optimizing queries that filter or sort by multiple fields.
|
||||
|
||||
To create a compound index, use the `indexes` option in your [Collection Config](../configuration/collections):
|
||||
|
||||
```ts
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
export const MyCollection: CollectionConfig = {
|
||||
// ...
|
||||
fields: [
|
||||
// ...
|
||||
],
|
||||
indexes: [
|
||||
{
|
||||
fields: ['title', 'createdAt'],
|
||||
unique: true, // Optional, if you want the combination of fields to be unique
|
||||
},
|
||||
],
|
||||
}
|
||||
```
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
title: MongoDB
|
||||
label: MongoDB
|
||||
order: 40
|
||||
order: 50
|
||||
desc: Payload has supported MongoDB natively since we started. The flexible nature of MongoDB lends itself well to Payload's powerful fields.
|
||||
keywords: MongoDB, documentation, typescript, Content Management System, cms, headless, javascript, node, react, nextjs
|
||||
---
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
title: Postgres
|
||||
label: Postgres
|
||||
order: 50
|
||||
order: 60
|
||||
desc: Payload supports Postgres through an officially supported Drizzle Database Adapter.
|
||||
keywords: Postgres, documentation, typescript, Content Management System, cms, headless, javascript, node, react, nextjs
|
||||
---
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
title: SQLite
|
||||
label: SQLite
|
||||
order: 60
|
||||
order: 70
|
||||
desc: Payload supports SQLite through an officially supported Drizzle Database Adapter.
|
||||
keywords: SQLite, documentation, typescript, Content Management System, cms, headless, javascript, node, react, nextjs
|
||||
---
|
||||
|
||||
@@ -41,17 +41,17 @@ export const MyArrayField: Field = {
|
||||
|
||||
| Option | Description |
|
||||
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More](/docs/fields/overview#field-names) |
|
||||
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More details](/docs/fields/overview#field-names). |
|
||||
| **`label`** | Text used as the heading in the [Admin Panel](../admin/overview) or an object with keys for each language. Auto-generated from name if not defined. |
|
||||
| **`fields`** \* | Array of field types to correspond to each row of the Array. |
|
||||
| **`validate`** | Provide a custom validation function that will be executed on both the [Admin Panel](../admin/overview) and the backend. [More](/docs/fields/overview#validation) |
|
||||
| **`validate`** | Provide a custom validation function that will be executed on both the [Admin Panel](../admin/overview) and the backend. [More details](/docs/fields/overview#validation). |
|
||||
| **`minRows`** | A number for the fewest allowed items during validation when a value is present. |
|
||||
| **`maxRows`** | A number for the most allowed items during validation when a value is present. |
|
||||
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/overview), include its data in the user JWT. |
|
||||
| **`hooks`** | Provide Field Hooks to control logic for this field. [More details](../hooks/fields). |
|
||||
| **`access`** | Provide Field Access Control to denote what users can see and do with this field's data. [More details](../access-control/fields). |
|
||||
| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin Panel. |
|
||||
| **`defaultValue`** | Provide an array of row data to be used for this field's default value. [More](/docs/fields/overview#default-values) |
|
||||
| **`defaultValue`** | Provide an array of row data to be used for this field's default value. [More details](/docs/fields/overview#default-values). |
|
||||
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. If enabled, a separate, localized set of all data within this Array will be kept, so there is no need to specify each nested field as `localized`. |
|
||||
| **`required`** | Require this field to have a value. |
|
||||
| **`labels`** | Customize the row labels appearing in the Admin dashboard. |
|
||||
|
||||
@@ -41,17 +41,17 @@ export const MyBlocksField: Field = {
|
||||
|
||||
| Option | Description |
|
||||
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More](/docs/fields/overview#field-names) |
|
||||
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More details](/docs/fields/overview#field-names). |
|
||||
| **`label`** | Text used as the heading in the Admin Panel or an object with keys for each language. Auto-generated from name if not defined. |
|
||||
| **`blocks`** \* | Array of [block configs](/docs/fields/blocks#block-configs) to be made available to this field. |
|
||||
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More](/docs/fields/overview#validation) |
|
||||
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More details](/docs/fields/overview#validation). |
|
||||
| **`minRows`** | A number for the fewest allowed items during validation when a value is present. |
|
||||
| **`maxRows`** | A number for the most allowed items during validation when a value is present. |
|
||||
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/overview), include its data in the user JWT. |
|
||||
| **`hooks`** | Provide Field Hooks to control logic for this field. [More details](../hooks/fields). |
|
||||
| **`access`** | Provide Field Access Control to denote what users can see and do with this field's data. [More details](../access-control/fields). |
|
||||
| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API response or the Admin Panel. |
|
||||
| **`defaultValue`** | Provide an array of block data to be used for this field's default value. [More](/docs/fields/overview#default-values) |
|
||||
| **`defaultValue`** | Provide an array of block data to be used for this field's default value. [More details](/docs/fields/overview#default-values). |
|
||||
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. If enabled, a separate, localized set of all data within this field will be kept, so there is no need to specify each nested field as `localized`. |
|
||||
| **`unique`** | Enforce that each entry in the Collection has a unique value for this field. |
|
||||
| **`labels`** | Customize the block row labels appearing in the Admin dashboard. |
|
||||
|
||||
@@ -30,15 +30,15 @@ export const MyCheckboxField: Field = {
|
||||
|
||||
| Option | Description |
|
||||
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More](/docs/fields/overview#field-names) |
|
||||
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More details](/docs/fields/overview#field-names). |
|
||||
| **`label`** | Text used as a field label in the Admin Panel or an object with keys for each language. |
|
||||
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More](/docs/fields/overview#validation) |
|
||||
| **`index`** | Build an [index](/docs/database/overview) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. |
|
||||
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More details](/docs/fields/overview#validation). |
|
||||
| **`index`** | Build an [index](../database/indexes) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. |
|
||||
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/overview), include its data in the user JWT. |
|
||||
| **`hooks`** | Provide Field Hooks to control logic for this field. [More details](../hooks/fields). |
|
||||
| **`access`** | Provide Field Access Control to denote what users can see and do with this field's data. [More details](../access-control/fields). |
|
||||
| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin Panel. |
|
||||
| **`defaultValue`** | Provide data to be used for this field's default value, will default to false if field is also `required`. [More](/docs/fields/overview#default-values) |
|
||||
| **`defaultValue`** | Provide data to be used for this field's default value, will default to false if field is also `required`. [More details](/docs/fields/overview#default-values). |
|
||||
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. |
|
||||
| **`required`** | Require this field to have a value. |
|
||||
| **`admin`** | Admin-specific configuration. [More details](./overview#admin-options). |
|
||||
|
||||
@@ -31,18 +31,18 @@ export const MyBlocksField: Field = {
|
||||
|
||||
| Option | Description |
|
||||
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More](/docs/fields/overview#field-names) |
|
||||
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More details](/docs/fields/overview#field-names). |
|
||||
| **`label`** | Text used as a field label in the Admin Panel or an object with keys for each language. |
|
||||
| **`unique`** | Enforce that each entry in the Collection has a unique value for this field. |
|
||||
| **`index`** | Build an [index](/docs/database/overview) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. |
|
||||
| **`index`** | Build an [index](../database/indexes) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. |
|
||||
| **`minLength`** | Used by the default validation function to ensure values are of a minimum character length. |
|
||||
| **`maxLength`** | Used by the default validation function to ensure values are of a maximum character length. |
|
||||
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More](/docs/fields/overview#validation) |
|
||||
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More details](/docs/fields/overview#validation). |
|
||||
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/overview), include its data in the user JWT. |
|
||||
| **`hooks`** | Provide Field Hooks to control logic for this field. [More details](../hooks/fields). |
|
||||
| **`access`** | Provide Field Access Control to denote what users can see and do with this field's data. [More details](../access-control/fields). |
|
||||
| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin Panel. |
|
||||
| **`defaultValue`** | Provide data to be used for this field's default value. [More](/docs/fields/overview#default-values) |
|
||||
| **`defaultValue`** | Provide data to be used for this field's default value. [More details](/docs/fields/overview#default-values). |
|
||||
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. |
|
||||
| **`required`** | Require this field to have a value. |
|
||||
| **`admin`** | Admin-specific configuration. See below for [more detail](#admin-options). |
|
||||
|
||||
@@ -30,15 +30,15 @@ export const MyDateField: Field = {
|
||||
|
||||
| Option | Description |
|
||||
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More](/docs/fields/overview#field-names) |
|
||||
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More details](/docs/fields/overview#field-names). |
|
||||
| **`label`** | Text used as a field label in the Admin Panel or an object with keys for each language. |
|
||||
| **`index`** | Build an [index](/docs/database/overview) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. |
|
||||
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More](/docs/fields/overview#validation) |
|
||||
| **`index`** | Build an [index](../database/indexes) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. |
|
||||
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More details](/docs/fields/overview#validation). |
|
||||
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/overview), include its data in the user JWT. |
|
||||
| **`hooks`** | Provide Field Hooks to control logic for this field. [More details](../hooks/fields). |
|
||||
| **`access`** | Provide Field Access Control to denote what users can see and do with this field's data. [More details](../access-control/fields). |
|
||||
| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin Panel. |
|
||||
| **`defaultValue`** | Provide data to be used for this field's default value. [More](/docs/fields/overview#default-values) |
|
||||
| **`defaultValue`** | Provide data to be used for this field's default value. [More details](/docs/fields/overview#default-values). |
|
||||
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. |
|
||||
| **`required`** | Require this field to have a value. |
|
||||
| **`admin`** | Admin-specific configuration. [More details](#admin-options). |
|
||||
|
||||
@@ -30,16 +30,16 @@ export const MyEmailField: Field = {
|
||||
|
||||
| Option | Description |
|
||||
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More](/docs/fields/overview#field-names) |
|
||||
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More details](/docs/fields/overview#field-names). |
|
||||
| **`label`** | Text used as a field label in the Admin Panel or an object with keys for each language. |
|
||||
| **`unique`** | Enforce that each entry in the Collection has a unique value for this field. |
|
||||
| **`index`** | Build an [index](/docs/database/overview) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. |
|
||||
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More](/docs/fields/overview#validation) |
|
||||
| **`index`** | Build an [index](../database/indexes) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. |
|
||||
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More details](/docs/fields/overview#validation). |
|
||||
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/overview), include its data in the user JWT. |
|
||||
| **`hooks`** | Provide Field Hooks to control logic for this field. [More details](../hooks/fields). |
|
||||
| **`access`** | Provide Field Access Control to denote what users can see and do with this field's data. [More details](../access-control/fields). |
|
||||
| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin Panel. |
|
||||
| **`defaultValue`** | Provide data to be used for this field's default value. [More](/docs/fields/overview#default-values) |
|
||||
| **`defaultValue`** | Provide data to be used for this field's default value. [More details](/docs/fields/overview#default-values). |
|
||||
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. |
|
||||
| **`required`** | Require this field to have a value. |
|
||||
| **`admin`** | Admin-specific configuration. [More details](#admin-options). |
|
||||
|
||||
@@ -35,15 +35,15 @@ export const MyGroupField: Field = {
|
||||
|
||||
| Option | Description |
|
||||
| ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **`name`** | To be used as the property name when stored and retrieved from the database. [More](/docs/fields/overview#field-names) |
|
||||
| **`name`** | To be used as the property name when stored and retrieved from the database. [More details](/docs/fields/overview#field-names). |
|
||||
| **`fields`** \* | Array of field types to nest within this Group. |
|
||||
| **`label`** | Used as a heading in the Admin Panel and to name the generated GraphQL type. Defaults to the field name, if defined. |
|
||||
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More](/docs/fields/overview#validation) |
|
||||
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More details](/docs/fields/overview#validation). |
|
||||
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/overview), include its data in the user JWT. |
|
||||
| **`hooks`** | Provide Field Hooks to control logic for this field. [More details](../hooks/fields). |
|
||||
| **`access`** | Provide Field Access Control to denote what users can see and do with this field's data. [More details](../access-control/fields). |
|
||||
| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin Panel. |
|
||||
| **`defaultValue`** | Provide an object of data to be used for this field's default value. [More](/docs/fields/overview#default-values) |
|
||||
| **`defaultValue`** | Provide an object of data to be used for this field's default value. [More details](/docs/fields/overview#default-values). |
|
||||
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. If enabled, a separate, localized set of all data within this Group will be kept, so there is no need to specify each nested field as `localized`. |
|
||||
| **`admin`** | Admin-specific configuration. [More details](#admin-options). |
|
||||
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
|
||||
|
||||
@@ -135,7 +135,7 @@ powerful Admin UI.
|
||||
|
||||
| Option | Description |
|
||||
| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **`name`** \* | To be used as the property name when retrieved from the database. [More](./overview#field-names) |
|
||||
| **`name`** \* | To be used as the property name when retrieved from the database. [More details](./overview#field-names). |
|
||||
| **`collection`** \* | The `slug`s having the relationship field or an array of collection slugs. |
|
||||
| **`on`** \* | The name of the relationship or upload field that relates to the collection document. Use dot notation for nested paths, like 'myGroup.relationName'. If `collection` is an array, this field must exist for all specified collections |
|
||||
| **`orderable`** | If true, enables custom ordering and joined documents can be reordered via drag and drop. Uses [fractional indexing](https://observablehq.com/@dgreensp/implementing-fractional-indexing) for efficient reordering. |
|
||||
|
||||
@@ -31,17 +31,17 @@ export const MyJSONField: Field = {
|
||||
|
||||
| Option | Description |
|
||||
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More](/docs/fields/overview#field-names) |
|
||||
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More details](/docs/fields/overview#field-names). |
|
||||
| **`label`** | Text used as a field label in the Admin Panel or an object with keys for each language. |
|
||||
| **`unique`** | Enforce that each entry in the Collection has a unique value for this field. |
|
||||
| **`index`** | Build an [index](/docs/database/overview) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. |
|
||||
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More](/docs/fields/overview#validation) |
|
||||
| **`index`** | Build an [index](../database/indexes) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. |
|
||||
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More details](/docs/fields/overview#validation). |
|
||||
| **`jsonSchema`** | Provide a JSON schema that will be used for validation. [JSON schemas](https://json-schema.org/learn/getting-started-step-by-step) |
|
||||
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/overview), include its data in the user JWT. |
|
||||
| **`hooks`** | Provide Field Hooks to control logic for this field. [More details](../hooks/fields). |
|
||||
| **`access`** | Provide Field Access Control to denote what users can see and do with this field's data. [More details](../access-control/fields). |
|
||||
| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin Panel. |
|
||||
| **`defaultValue`** | Provide data to be used for this field's default value. [More](/docs/fields/overview#default-values) |
|
||||
| **`defaultValue`** | Provide data to be used for this field's default value. [More details](/docs/fields/overview#default-values). |
|
||||
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. |
|
||||
| **`required`** | Require this field to have a value. |
|
||||
| **`admin`** | Admin-specific configuration. [More details](#admin-options). |
|
||||
|
||||
@@ -30,7 +30,7 @@ export const MyNumberField: Field = {
|
||||
|
||||
| Option | Description |
|
||||
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More](/docs/fields/overview#field-names) |
|
||||
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More details](/docs/fields/overview#field-names). |
|
||||
| **`label`** | Text used as a field label in the Admin Panel or an object with keys for each language. |
|
||||
| **`min`** | Minimum value accepted. Used in the default `validation` function. |
|
||||
| **`max`** | Maximum value accepted. Used in the default `validation` function. |
|
||||
@@ -38,13 +38,13 @@ export const MyNumberField: Field = {
|
||||
| **`minRows`** | Minimum number of numbers in the numbers array, if `hasMany` is set to true. |
|
||||
| **`maxRows`** | Maximum number of numbers in the numbers array, if `hasMany` is set to true. |
|
||||
| **`unique`** | Enforce that each entry in the Collection has a unique value for this field. |
|
||||
| **`index`** | Build an [index](/docs/database/overview) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. |
|
||||
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More](/docs/fields/overview#validation) |
|
||||
| **`index`** | Build an [index](../database/indexes) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. |
|
||||
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More details](/docs/fields/overview#validation). |
|
||||
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/overview), include its data in the user JWT. |
|
||||
| **`hooks`** | Provide Field Hooks to control logic for this field. [More details](../hooks/fields). |
|
||||
| **`access`** | Provide Field Access Control to denote what users can see and do with this field's data. [More details](../access-control/fields). |
|
||||
| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin Panel. |
|
||||
| **`defaultValue`** | Provide data to be used for this field's default value. [More](/docs/fields/overview#default-values) |
|
||||
| **`defaultValue`** | Provide data to be used for this field's default value. [More details](/docs/fields/overview#default-values). |
|
||||
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. |
|
||||
| **`required`** | Require this field to have a value. |
|
||||
| **`admin`** | Admin-specific configuration. [More details](#admin-options). |
|
||||
|
||||
@@ -303,7 +303,7 @@ The following additional properties are provided in the `ctx` object:
|
||||
| `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). |
|
||||
| `event` | Either `onChange` or `submit` depending on the current action. Used as a performance opt-in. [More details](#validation-performance). |
|
||||
|
||||
#### Localized and Built-in Error Messages
|
||||
|
||||
@@ -365,11 +365,11 @@ import {
|
||||
} from 'payload/shared'
|
||||
```
|
||||
|
||||
#### Async Field Validations
|
||||
#### Validation Performance
|
||||
|
||||
Custom validation functions can also be asynchronous depending on your needs. This makes it possible to make requests to external services or perform other miscellaneous asynchronous logic.
|
||||
When writing async or computationally heavy validation functions, it is important to consider the performance implications. Within the Admin Panel, validations are executed on every change to the field, so they should be as lightweight as possible and only run when necessary.
|
||||
|
||||
When writing async validation functions, it is important to consider the performance implications. Validations are executed on every change to the field, so they should be as lightweight as possible. If you need to perform expensive validations, such as querying the database, consider using the `event` property in the `ctx` object to only run the validation on form submission.
|
||||
If you need to perform expensive validations, such as querying the database, consider using the `event` property in the `ctx` object to only run that particular validation on form submission.
|
||||
|
||||
To write asynchronous validation functions, use the `async` keyword to define your function:
|
||||
|
||||
@@ -403,6 +403,11 @@ export const Orders: CollectionConfig = {
|
||||
}
|
||||
```
|
||||
|
||||
<Banner type="success">
|
||||
For more performance tips, see the [Performance
|
||||
documentation](../performance/overview).
|
||||
</Banner>
|
||||
|
||||
## Custom ID Fields
|
||||
|
||||
All [Collections](../configuration/collections) automatically generate their own ID field. If needed, you can override this behavior by providing an explicit ID field to your config. This field should either be required or have a hook to generate the ID dynamically.
|
||||
|
||||
@@ -34,16 +34,16 @@ export const MyPointField: Field = {
|
||||
|
||||
| Option | Description |
|
||||
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More](/docs/fields/overview#field-names) |
|
||||
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More details](/docs/fields/overview#field-names). |
|
||||
| **`label`** | Used as a field label in the Admin Panel and to name the generated GraphQL type. |
|
||||
| **`unique`** | Enforce that each entry in the Collection has a unique value for this field. |
|
||||
| **`index`** | Build an [index](/docs/database/overview) for this field to produce faster queries. To support location queries, point index defaults to `2dsphere`, to disable the index set to `false`. |
|
||||
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More](/docs/fields/overview#validation) |
|
||||
| **`index`** | Build an [index](../database/indexes) for this field to produce faster queries. To support location queries, point index defaults to `2dsphere`, to disable the index set to `false`. |
|
||||
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More details](/docs/fields/overview#validation). |
|
||||
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/overview), include its data in the user JWT. |
|
||||
| **`hooks`** | Provide Field Hooks to control logic for this field. [More details](../hooks/fields). |
|
||||
| **`access`** | Provide Field Access Control to denote what users can see and do with this field's data. [More details](../access-control/fields). |
|
||||
| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin Panel. |
|
||||
| **`defaultValue`** | Provide data to be used for this field's default value. [More](/docs/fields/overview#default-values) |
|
||||
| **`defaultValue`** | Provide data to be used for this field's default value. [More details](/docs/fields/overview#default-values). |
|
||||
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. |
|
||||
| **`required`** | Require this field to have a value. |
|
||||
| **`admin`** | Admin-specific configuration. [More details](./overview#admin-options). |
|
||||
|
||||
@@ -35,16 +35,16 @@ export const MyRadioField: Field = {
|
||||
|
||||
| Option | Description |
|
||||
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More](/docs/fields/overview#field-names) |
|
||||
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More details](/docs/fields/overview#field-names). |
|
||||
| **`options`** \* | Array of options to allow the field to store. Can either be an array of strings, or an array of objects containing a `label` string and a `value` string. |
|
||||
| **`label`** | Text used as a field label in the Admin Panel or an object with keys for each language. |
|
||||
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More](/docs/fields/overview#validation) |
|
||||
| **`index`** | Build an [index](/docs/database/overview) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. |
|
||||
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More details](/docs/fields/overview#validation). |
|
||||
| **`index`** | Build an [index](../database/indexes) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. |
|
||||
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/overview), include its data in the user JWT. |
|
||||
| **`hooks`** | Provide Field Hooks to control logic for this field. [More details](../hooks/fields). |
|
||||
| **`access`** | Provide Field Access Control to denote what users can see and do with this field's data. [More details](../access-control/fields). |
|
||||
| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin Panel. |
|
||||
| **`defaultValue`** | Provide data to be used for this field's default value. The default value must exist within provided values in `options`. [More](/docs/fields/overview#default-values) |
|
||||
| **`defaultValue`** | Provide data to be used for this field's default value. The default value must exist within provided values in `options`. [More details](/docs/fields/overview#default-values). |
|
||||
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. |
|
||||
| **`required`** | Require this field to have a value. |
|
||||
| **`admin`** | Admin-specific configuration. [More details](#admin-options). |
|
||||
|
||||
@@ -39,22 +39,22 @@ export const MyRelationshipField: Field = {
|
||||
|
||||
| Option | Description |
|
||||
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More](/docs/fields/overview#field-names) |
|
||||
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More details](/docs/fields/overview#field-names). |
|
||||
| **`relationTo`** \* | Provide one or many collection `slug`s to be able to assign relationships to. |
|
||||
| **`filterOptions`** | A query to filter which options appear in the UI and validate against. [More](#filtering-relationship-options). |
|
||||
| **`filterOptions`** | A query to filter which options appear in the UI and validate against. [More details](#filtering-relationship-options). |
|
||||
| **`hasMany`** | Boolean when, if set to `true`, allows this field to have many relations instead of only one. |
|
||||
| **`minRows`** | A number for the fewest allowed items during validation when a value is present. Used with `hasMany`. |
|
||||
| **`maxRows`** | A number for the most allowed items during validation when a value is present. Used with `hasMany`. |
|
||||
| **`maxDepth`** | Sets a maximum population depth for this field, regardless of the remaining depth when this field is reached. [Max Depth](/docs/queries/depth#max-depth) |
|
||||
| **`label`** | Text used as a field label in the Admin Panel or an object with keys for each language. |
|
||||
| **`unique`** | Enforce that each entry in the Collection has a unique value for this field. |
|
||||
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More](/docs/fields/overview#validation) |
|
||||
| **`index`** | Build an [index](/docs/database/overview) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. |
|
||||
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More details](/docs/fields/overview#validation). |
|
||||
| **`index`** | Build an [index](../database/indexes) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. |
|
||||
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/overview), include its data in the user JWT. |
|
||||
| **`hooks`** | Provide Field Hooks to control logic for this field. [More details](../hooks/fields). |
|
||||
| **`access`** | Provide Field Access Control to denote what users can see and do with this field's data. [More details](../access-control/fields). |
|
||||
| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin Panel. |
|
||||
| **`defaultValue`** | Provide data to be used for this field's default value. [More](/docs/fields/overview#default-values) |
|
||||
| **`defaultValue`** | Provide data to be used for this field's default value. [More details](/docs/fields/overview#default-values). |
|
||||
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. |
|
||||
| **`required`** | Require this field to have a value. |
|
||||
| **`admin`** | Admin-specific configuration. [More details](#admin-options). |
|
||||
@@ -93,7 +93,7 @@ The Relationship Field inherits all of the default admin options from the base [
|
||||
| **`isSortable`** | Set to `true` if you'd like this field to be sortable within the Admin UI using drag and drop (only works when `hasMany` is set to `true`). |
|
||||
| **`allowCreate`** | Set to `false` if you'd like to disable the ability to create new documents from within the relationship field. |
|
||||
| **`allowEdit`** | Set to `false` if you'd like to disable the ability to edit documents from within the relationship field. |
|
||||
| **`sortOptions`** | Define a default sorting order for the options within a Relationship field's dropdown. [More](#sort-options) |
|
||||
| **`sortOptions`** | Define a default sorting order for the options within a Relationship field's dropdown. [More details](#sort-options) |
|
||||
| **`placeholder`** | Define a custom text or function to replace the generic default placeholder |
|
||||
| **`appearance`** | Set to `drawer` or `select` to change the behavior of the field. Defaults to `select`. |
|
||||
|
||||
|
||||
@@ -23,14 +23,14 @@ Instead, you can invest your time and effort into learning the underlying open-s
|
||||
|
||||
| Option | Description |
|
||||
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More](./overview#field-names) |
|
||||
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More details](./overview#field-names). |
|
||||
| **`label`** | Text used as a field label in the Admin Panel or an object with keys for each language. |
|
||||
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More](./overview#validation) |
|
||||
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More details](./overview#validation). |
|
||||
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](../authentication/overview), include its data in the user JWT. |
|
||||
| **`hooks`** | Provide Field Hooks to control logic for this field. [More details](../hooks/fields). |
|
||||
| **`access`** | Provide Field Access Control to denote what users can see and do with this field's data. [More details](../access-control/fields). |
|
||||
| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin Panel. |
|
||||
| **`defaultValue`** | Provide data to be used for this field's default value. [More](./overview#default-values) |
|
||||
| **`defaultValue`** | Provide data to be used for this field's default value. [More details](./overview#default-values). |
|
||||
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](../configuration/localization) in the Base config. |
|
||||
| **`required`** | Require this field to have a value. |
|
||||
| **`admin`** | Admin-specific configuration. [More details](#admin-options). |
|
||||
|
||||
@@ -35,18 +35,18 @@ export const MySelectField: Field = {
|
||||
|
||||
| Option | Description |
|
||||
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More](/docs/fields/overview#field-names) |
|
||||
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More details](/docs/fields/overview#field-names). |
|
||||
| **`options`** \* | Array of options to allow the field to store. Can either be an array of strings, or an array of objects containing a `label` string and a `value` string. |
|
||||
| **`hasMany`** | Boolean when, if set to `true`, allows this field to have many selections instead of only one. |
|
||||
| **`label`** | Text used as a field label in the Admin Panel or an object with keys for each language. |
|
||||
| **`unique`** | Enforce that each entry in the Collection has a unique value for this field. |
|
||||
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More](/docs/fields/overview#validation) |
|
||||
| **`index`** | Build an [index](/docs/database/overview) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. |
|
||||
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More details](/docs/fields/overview#validation). |
|
||||
| **`index`** | Build an [index](../database/indexes) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. |
|
||||
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/overview), include its data in the user JWT. |
|
||||
| **`hooks`** | Provide Field Hooks to control logic for this field. [More details](../hooks/fields). |
|
||||
| **`access`** | Provide Field Access Control to denote what users can see and do with this field's data. [More details](../access-control/fields). |
|
||||
| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin Panel. |
|
||||
| **`defaultValue`** | Provide data to be used for this field's default value. [More](/docs/fields/overview#default-values) |
|
||||
| **`defaultValue`** | Provide data to be used for this field's default value. [More details](/docs/fields/overview#default-values). |
|
||||
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. |
|
||||
| **`required`** | Require this field to have a value. |
|
||||
| **`admin`** | Admin-specific configuration. See the [default field admin config](/docs/fields/overview#admin-options) for more details. |
|
||||
|
||||
@@ -45,7 +45,7 @@ Each tab must have either a `name` or `label` and the required `fields` array. Y
|
||||
|
||||
| Option | Description |
|
||||
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **`name`** | Groups field data into an object when stored and retrieved from the database. [More](/docs/fields/overview#field-names) |
|
||||
| **`name`** | Groups field data into an object when stored and retrieved from the database. [More details](/docs/fields/overview#field-names). |
|
||||
| **`label`** | The label to render on the tab itself. Required when name is undefined, defaults to name converted to words. |
|
||||
| **`fields`** \* | The fields to render within this tab. |
|
||||
| **`description`** | Optionally render a description within this tab to describe the contents of the tab itself. |
|
||||
|
||||
@@ -30,18 +30,18 @@ export const MyTextField: Field = {
|
||||
|
||||
| Option | Description |
|
||||
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More](/docs/fields/overview#field-names) |
|
||||
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More details](/docs/fields/overview#field-names). |
|
||||
| **`label`** | Text used as a field label in the Admin Panel or an object with keys for each language. |
|
||||
| **`unique`** | Enforce that each entry in the Collection has a unique value for this field. |
|
||||
| **`minLength`** | Used by the default validation function to ensure values are of a minimum character length. |
|
||||
| **`maxLength`** | Used by the default validation function to ensure values are of a maximum character length. |
|
||||
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More](/docs/fields/overview#validation) |
|
||||
| **`index`** | Build an [index](/docs/database/overview) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. |
|
||||
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More details](/docs/fields/overview#validation). |
|
||||
| **`index`** | Build an [index](../database/indexes) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. |
|
||||
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/overview), include its data in the user JWT. |
|
||||
| **`hooks`** | Provide Field Hooks to control logic for this field. [More details](../hooks/fields). |
|
||||
| **`access`** | Provide Field Access Control to denote what users can see and do with this field's data. [More details](../access-control/fields). |
|
||||
| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin Panel. |
|
||||
| **`defaultValue`** | Provide data to be used for this field's default value. [More](/docs/fields/overview#default-values) |
|
||||
| **`defaultValue`** | Provide data to be used for this field's default value. [More details](/docs/fields/overview#default-values). |
|
||||
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. |
|
||||
| **`required`** | Require this field to have a value. |
|
||||
| **`admin`** | Admin-specific configuration. [More details](#admin-options). |
|
||||
|
||||
@@ -30,18 +30,18 @@ export const MyTextareaField: Field = {
|
||||
|
||||
| Option | Description |
|
||||
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More](/docs/fields/overview#field-names) |
|
||||
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More details](/docs/fields/overview#field-names). |
|
||||
| **`label`** | Text used as a field label in the Admin Panel or an object with keys for each language. |
|
||||
| **`unique`** | Enforce that each entry in the Collection has a unique value for this field. |
|
||||
| **`minLength`** | Used by the default validation function to ensure values are of a minimum character length. |
|
||||
| **`maxLength`** | Used by the default validation function to ensure values are of a maximum character length. |
|
||||
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More](/docs/fields/overview#validation) |
|
||||
| **`index`** | Build an [index](/docs/database/overview) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. |
|
||||
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More details](/docs/fields/overview#validation). |
|
||||
| **`index`** | Build an [index](../database/indexes) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. |
|
||||
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/overview), include its data in the user JWT. |
|
||||
| **`hooks`** | Provide Field Hooks to control logic for this field. [More details](../hooks/fields). |
|
||||
| **`access`** | Provide Field Access Control to denote what users can see and do with this field's data. [More details](../access-control/fields). |
|
||||
| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin Panel. |
|
||||
| **`defaultValue`** | Provide data to be used for this field's default value. [More](/docs/fields/overview#default-values) |
|
||||
| **`defaultValue`** | Provide data to be used for this field's default value. [More details](/docs/fields/overview#default-values). |
|
||||
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. |
|
||||
| **`required`** | Require this field to have a value. |
|
||||
| **`admin`** | Admin-specific configuration. [More details](#admin-options). |
|
||||
|
||||
@@ -32,8 +32,8 @@ export const MyUIField: Field = {
|
||||
| ------------------------------- | ---------------------------------------------------------------------------------------------------------- |
|
||||
| **`name`** \* | A unique identifier for this field. |
|
||||
| **`label`** | Human-readable label for this UI field. |
|
||||
| **`admin.components.Field`** \* | React component to be rendered for this field within the Edit View. [More](./overview#field) |
|
||||
| **`admin.components.Cell`** | React component to be rendered as a Cell within collection List views. [More](./overview#cell) |
|
||||
| **`admin.components.Field`** \* | React component to be rendered for this field within the Edit View. [More details](./overview#field). |
|
||||
| **`admin.components.Cell`** | React component to be rendered as a Cell within collection List views. [More details](./overview#cell). |
|
||||
| **`admin.disableListColumn`** | Set `disableListColumn` to `true` to prevent the UI field from appearing in the list view column selector. |
|
||||
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
|
||||
|
||||
|
||||
@@ -46,23 +46,23 @@ export const MyUploadField: Field = {
|
||||
|
||||
| Option | Description |
|
||||
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More](/docs/fields/overview#field-names) |
|
||||
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More details](/docs/fields/overview#field-names). |
|
||||
| **`relationTo`** \* | Provide a single collection `slug` to allow this field to accept a relation to. **Note: the related collection must be configured to support Uploads.** |
|
||||
| **`filterOptions`** | A query to filter which options appear in the UI and validate against. [More](#filtering-upload-options). |
|
||||
| **`filterOptions`** | A query to filter which options appear in the UI and validate against. [More details](#filtering-upload-options). |
|
||||
| **`hasMany`** | Boolean which, if set to true, allows this field to have many relations instead of only one. |
|
||||
| **`minRows`** | A number for the fewest allowed items during validation when a value is present. Used with hasMany. |
|
||||
| **`maxRows`** | A number for the most allowed items during validation when a value is present. Used with hasMany. |
|
||||
| **`maxDepth`** | Sets a number limit on iterations of related documents to populate when queried. [Depth](../queries/depth) |
|
||||
| **`label`** | Text used as a field label in the Admin Panel or an object with keys for each language. |
|
||||
| **`unique`** | Enforce that each entry in the Collection has a unique value for this field. |
|
||||
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More](/docs/fields/overview#validation) |
|
||||
| **`index`** | Build an [index](/docs/database/overview) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. |
|
||||
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More details](/docs/fields/overview#validation). |
|
||||
| **`index`** | Build an [index](../database/indexes) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. |
|
||||
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/overview), include its data in the user JWT. |
|
||||
| **`hooks`** | Provide Field Hooks to control logic for this field. [More details](../hooks/fields). |
|
||||
| **`access`** | Provide Field Access Control to denote what users can see and do with this field's data. [More details](../access-control/fields). |
|
||||
| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin Panel. |
|
||||
| **`defaultValue`** | Provide data to be used for this field's default value. [More](/docs/fields/overview#default-values) |
|
||||
| **`displayPreview`** | Enable displaying preview of the uploaded file. Overrides related Collection's `displayPreview` option. [More](/docs/upload/overview#collection-upload-options). |
|
||||
| **`defaultValue`** | Provide data to be used for this field's default value. [More details](/docs/fields/overview#default-values). |
|
||||
| **`displayPreview`** | Enable displaying preview of the uploaded file. Overrides related Collection's `displayPreview` option. [More details](/docs/upload/overview#collection-upload-options). |
|
||||
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. |
|
||||
| **`required`** | Require this field to have a value. |
|
||||
| **`admin`** | Admin-specific configuration. [Admin Options](./overview#admin-options). |
|
||||
|
||||
@@ -93,12 +93,108 @@ All Hooks can be written as either synchronous or asynchronous functions. Choosi
|
||||
|
||||
#### Asynchronous
|
||||
|
||||
If the Hook should modify data before a Document is updated or created, and it relies on asynchronous actions such as fetching data from a third party, it might make sense to define your Hook as an asynchronous function. This way you can be sure that your Hook completes before the operation's lifecycle continues. Async hooks are run in series - so if you have two async hooks defined, the second hook will wait for the first to complete before it starts.
|
||||
If the Hook should modify data before a Document is updated or created, and it relies on asynchronous actions such as fetching data from a third party, it might make sense to define your Hook as an asynchronous function. This way you can be sure that your Hook completes before the operation's lifecycle continues.
|
||||
|
||||
Async hooks are run in series - so if you have two async hooks defined, the second hook will wait for the first to complete before it starts.
|
||||
|
||||
<Banner type="success">
|
||||
**Tip:** If your hook executes a long-running task that doesn't affect the
|
||||
response in any way, consider [offloading it to the job
|
||||
queue](#offloading-long-running-tasks). That will free up the request to
|
||||
continue processing without waiting for the task to complete.
|
||||
</Banner>
|
||||
|
||||
#### Synchronous
|
||||
|
||||
If your Hook simply performs a side-effect, such as updating a CRM, it might be okay to define it synchronously, so the Payload operation does not have to wait for your hook to complete.
|
||||
If your Hook simply performs a side-effect, such as mutating document data, it might be okay to define it synchronously, so the Payload operation does not have to wait for your hook to complete.
|
||||
|
||||
## Server-only Execution
|
||||
|
||||
Hooks are only triggered on the server and are automatically excluded from the client-side bundle. This means that you can safely use sensitive business logic in your Hooks without worrying about exposing it to the client.
|
||||
|
||||
## Performance
|
||||
|
||||
Hooks are a powerful way to customize the behavior of your APIs, but some hooks are run very often and can add significant overhead to your requests if not optimized.
|
||||
|
||||
When building hooks, combine together as many of these strategies as possible to ensure your hooks are as performant as they can be.
|
||||
|
||||
<Banner type="success">
|
||||
For more performance tips, see the [Performance
|
||||
documentation](../performance/overview).
|
||||
</Banner>
|
||||
|
||||
### Writing efficient hooks
|
||||
|
||||
Consider when hooks are run. One common pitfall is putting expensive logic in hooks that run very often.
|
||||
|
||||
For example, the `read` operation runs on every read request, so avoid putting expensive logic in a `beforeRead` or `afterRead` hook.
|
||||
|
||||
```ts
|
||||
{
|
||||
hooks: {
|
||||
beforeRead: [
|
||||
async () => {
|
||||
// This runs on every read request - avoid expensive logic here
|
||||
await doSomethingExpensive()
|
||||
return data
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Instead, you might want to use a `beforeChange` or `afterChange` hook, which only runs when a document is created or updated.
|
||||
|
||||
```ts
|
||||
{
|
||||
hooks: {
|
||||
beforeChange: [
|
||||
async ({ context }) => {
|
||||
// This is more acceptable here, although still should be mindful of performance
|
||||
await doSomethingExpensive()
|
||||
// ...
|
||||
},
|
||||
]
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### Using Hook Context
|
||||
|
||||
Use [Hook Context](./context) avoid prevent infinite loops or avoid repeating expensive operations across multiple hooks in the same request.
|
||||
|
||||
```ts
|
||||
{
|
||||
hooks: {
|
||||
beforeChange: [
|
||||
async ({ context }) => {
|
||||
const somethingExpensive = await doSomethingExpensive()
|
||||
context.somethingExpensive = somethingExpensive
|
||||
// ...
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
To learn more, see the [Hook Context documentation](./context).
|
||||
|
||||
### Offloading to the jobs queue
|
||||
|
||||
If your hooks perform any long-running tasks that don't direct affect request lifecycle, consider offloading them to the [jobs queue](../jobs-queue/overview). This will free up the request to continue processing without waiting for the task to complete.
|
||||
|
||||
```ts
|
||||
{
|
||||
hooks: {
|
||||
afterChange: [
|
||||
async ({ doc, req }) => {
|
||||
// Offload to job queue
|
||||
await req.payload.jobs.queue(...)
|
||||
// ...
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
To learn more, see the [Job Queue documentation](../jobs-queue/overview).
|
||||
|
||||
244
docs/performance/overview.mdx
Normal file
244
docs/performance/overview.mdx
Normal file
@@ -0,0 +1,244 @@
|
||||
---
|
||||
title: Performance
|
||||
label: Overview
|
||||
order: 10
|
||||
desc: Ensure your Payload app runs as quickly and efficiently as possible.
|
||||
keywords: performance, optimization, indexes, depth, select, block references, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
|
||||
---
|
||||
|
||||
Payload is designed with performance in mind, but its customizability means that there are many ways to configure your app that can impact performance.
|
||||
|
||||
With this in mind, Payload provides several options and best practices to help you optimize your app's specific performance needs. This includes the database, APIs, and Admin Panel.
|
||||
|
||||
Whether you're building an app or troubleshooting an existing one, follow these guidelines to ensure that it runs as quickly and efficiently as possible.
|
||||
|
||||
## Building your application
|
||||
|
||||
### Database proximity
|
||||
|
||||
The proximity of your database to your server can significantly impact performance. Ensure that your database is hosted in the same region as your server to minimize latency and improve response times.
|
||||
|
||||
### Indexing your fields
|
||||
|
||||
If a particular field is queried often, build an [Index](../database/indexes) for that field to produce faster queries.
|
||||
|
||||
When your query runs, the database will not search the entire document to find that one field, but will instead use the index to quickly locate the data.
|
||||
|
||||
To learn more, see the [Indexes](../database/indexes) docs.
|
||||
|
||||
### Querying your data
|
||||
|
||||
There are several ways to optimize your [Queries](../queries/overview). Many of these options directly impact overall database overhead, response sizes, and/or computational load and can significantly improve performance.
|
||||
|
||||
When building queries, combine as many of these options together as possible. This will ensure your queries are as efficient as they can be.
|
||||
|
||||
To learn more, see the [Query Performance](../queries/overview#performance) docs.
|
||||
|
||||
### Optimizing your APIs
|
||||
|
||||
When querying data through Payload APIs, the request lifecycle includes running hooks, access control, validations, and other operations that can add significant overhead to the request.
|
||||
|
||||
To optimize your APIs, any custom logic should be as efficient as possible. This includes writing lightweight hooks, preventing memory leaks, offloading long-running tasks, and optimizing custom validations.
|
||||
|
||||
To learn more, see the [Hooks Performance](../hooks/overview#performance) docs.
|
||||
|
||||
### Writing efficient validations
|
||||
|
||||
If your validation functions are asynchronous or computationally heavy, ensure they only run when necessary.
|
||||
|
||||
To learn more, see the [Validation Performance](../fields/overview#validation-performance) docs.
|
||||
|
||||
### Optimizing custom components
|
||||
|
||||
When building custom components in the Admin Panel, ensure that they are as efficient as possible. This includes using React best practices such as memoization, lazy loading, and avoiding unnecessary re-renders.
|
||||
|
||||
To learn more, see the [Custom Components Performance](../admin/custom-components#performance) docs.
|
||||
|
||||
## Other Best Practices
|
||||
|
||||
### Block references
|
||||
|
||||
Use [Block References](../fields/blocks#block-references) to share the same block across multiple fields without bloating the config. This will reduce the number of fields to traverse when processing permissions, etc. and can can significantly reduce the amount of data sent from the server to the client in the Admin Panel.
|
||||
|
||||
For example, if you have a block that is used in multiple fields, you can define it once and reference it in each field.
|
||||
|
||||
To do this, use the `blockReferences` option in your blocks field:
|
||||
|
||||
```ts
|
||||
import { buildConfig } from 'payload'
|
||||
|
||||
const config = buildConfig({
|
||||
// ...
|
||||
blocks: [
|
||||
{
|
||||
slug: 'TextBlock',
|
||||
fields: [
|
||||
{
|
||||
name: 'text',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
collections: [
|
||||
{
|
||||
slug: 'posts',
|
||||
fields: [
|
||||
{
|
||||
name: 'content',
|
||||
type: 'blocks',
|
||||
// highlight-start
|
||||
blockReferences: ['TextBlock'],
|
||||
blocks: [], // Required to be empty, for compatibility reasons
|
||||
// highlight-end
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: 'pages',
|
||||
fields: [
|
||||
{
|
||||
name: 'content',
|
||||
type: 'blocks',
|
||||
// highlight-start
|
||||
blockReferences: ['TextBlock'],
|
||||
blocks: [], // Required to be empty, for compatibility reasons
|
||||
// highlight-end
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
})
|
||||
```
|
||||
|
||||
### Using the cached Payload instance
|
||||
|
||||
Ensure that you do not instantiate Payload unnecessarily. Instead, Payload provides a caching mechanism to reuse the same instance across your app.
|
||||
|
||||
To do this, use the `getPayload` function to get the cached instance of Payload:
|
||||
|
||||
```ts
|
||||
import { getPayload } from 'payload'
|
||||
import config from '@payload-config'
|
||||
|
||||
const myFunction = async () => {
|
||||
const payload = await getPayload({ config })
|
||||
|
||||
// use payload here
|
||||
}
|
||||
```
|
||||
|
||||
### When to make direct-to-db calls
|
||||
|
||||
<Banner type="warning">
|
||||
**Warning:** Direct database calls bypass all hooks and validations. Only use
|
||||
this method when you are certain that the operation is safe and does not
|
||||
require any of these features.
|
||||
</Banner>
|
||||
|
||||
Making direct database calls can significantly improve performance by bypassing much of the request lifecycle such as hooks, validations, and other overhead associated with Payload APIs.
|
||||
|
||||
For example, this can be especially useful for the `update` operation, where Payload would otherwise need to make multiple API calls to fetch, update, and fetch again. Making a direct database call can reduce this to a single operation.
|
||||
|
||||
To do this, use the `payload.db` methods:
|
||||
|
||||
```ts
|
||||
await payload.db.updateOne({
|
||||
collection: 'posts',
|
||||
id: post.id,
|
||||
data: {
|
||||
title: 'New Title',
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
<Banner type="warning">
|
||||
**Note:** Direct database methods do not start a
|
||||
[transaction](../database/transactions). You have to start that yourself.
|
||||
</Banner>
|
||||
|
||||
#### Returning
|
||||
|
||||
To prevent unnecessary database computation and reduce the size of the response, you can also set `returning: false` in your direct database calls if you don't need the updated document returned to you.
|
||||
|
||||
```ts
|
||||
await payload.db.updateOne({
|
||||
collection: 'posts',
|
||||
id: post.id,
|
||||
data: { title: 'New Title' }, // See note above ^ about Postgres
|
||||
// highlight-start
|
||||
returning: false,
|
||||
// highlight-end
|
||||
})
|
||||
```
|
||||
|
||||
<Banner type="warning">
|
||||
**Note:** The `returning` option is only available on direct-to-db methods.
|
||||
E.g. those on the `payload.db` object. It is not exposed to the Local API.
|
||||
</Banner>
|
||||
|
||||
### Avoid bundling the entire UI library in your front-end
|
||||
|
||||
If your front-end imports from `@payloadcms/ui`, ensure that you do not bundle the entire package as this can significantly increase your bundle size.
|
||||
|
||||
To do this, import using the full path to the specific component you need:
|
||||
|
||||
```ts
|
||||
import { Button } from '@payloadcms/ui/elements/Button'
|
||||
```
|
||||
|
||||
Custom components within the Admin Panel, however, do not have this same restriction and can import directly from `@payloadcms/ui`:
|
||||
|
||||
```ts
|
||||
import { Button } from '@payloadcms/ui'
|
||||
```
|
||||
|
||||
<Banner type="success">
|
||||
**Tip:** Use
|
||||
[`@next/bundle-analyzer`](https://nextjs.org/docs/app/guides/package-bundling)
|
||||
to analyze your component tree and identify unnecessary re-renders or large
|
||||
components that could be optimized.
|
||||
</Banner>
|
||||
|
||||
## Optimizing local development
|
||||
|
||||
Everything mentioned above applies to local development as well, but there are a few additional steps you can take to optimize your local development experience.
|
||||
|
||||
### Enable Turbopack
|
||||
|
||||
<Banner type="warning">
|
||||
**Note:** In the future this will be the default. Use as your own risk.
|
||||
</Banner>
|
||||
|
||||
Add `--turbo` to your dev script to significantly speed up your local development server start time.
|
||||
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
"dev": "next dev --turbo"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Only bundle server packages in production
|
||||
|
||||
<Banner type="warning">
|
||||
**Note:** This is enabled by default in `create-payload-app` since v3.28.0. If
|
||||
you created your app after this version, you don't need to do anything.
|
||||
</Banner>
|
||||
|
||||
By default, Next.js bundles both server and client code. However, during development, bundling certain server packages isn't necessary.
|
||||
|
||||
Payload has thousands of modules, slowing down compilation.
|
||||
|
||||
Setting this option skips bundling Payload server modules during development. Fewer files to compile means faster compilation speeds.
|
||||
|
||||
To do this, add the `devBundleServerPackages` option to `withPayload` in your `next.config.js` file:
|
||||
|
||||
```ts
|
||||
const nextConfig = {
|
||||
// your existing next config
|
||||
}
|
||||
|
||||
export default withPayload(nextConfig, { devBundleServerPackages: false })
|
||||
```
|
||||
@@ -14,7 +14,9 @@ Solutions:
|
||||
|
||||
## Using the experimental-build-mode Next.js build flag
|
||||
|
||||
You can run Next.js build using the `pnpx next build --experimental-build-mode compile` command to only compile the code without static generation, which does not require a DB connection. In that case, your pages will be rendered dynamically, but after that, you can still generate static pages using the `pnpx next build --experimental-build-mode generate` command when you have a DB connection.
|
||||
You can run Next.js build using the `pnpm next build --experimental-build-mode compile` command to only compile the code without static generation, which does not require a DB connection. In that case, your pages will be rendered dynamically, but after that, you can still generate static pages using the `pnpm next build --experimental-build-mode generate` command when you have a DB connection.
|
||||
|
||||
When running `pnpm next build --experimental-build-mode compile`, environment variables prefixed with `NEXT_PUBLIC` will not be inlined and will be `undefined` on the client. To make these variables available, either run `pnpm next build --experimental-build-mode generate` if a DB connection is available, or use `pnpm next build --experimental-build-mode generate-env` if you do not have a DB connection.
|
||||
|
||||
[Next.js documentation](https://nextjs.org/docs/pages/api-reference/cli/next#next-build-options)
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ keywords: query, documents, pagination, documentation, Content Management System
|
||||
|
||||
Documents in Payload can have relationships to other Documents. This is true for both [Collections](../configuration/collections) as well as [Globals](../configuration/globals). When you query a Document, you can specify the depth at which to populate any of its related Documents either as full objects, or only their IDs.
|
||||
|
||||
Depth will optimize the performance of your application by limiting the amount of processing made in the database and significantly reducing the amount of data returned. Since Documents can be infinitely nested or recursively related, it's important to be able to control how deep your API populates.
|
||||
Since Documents can be infinitely nested or recursively related, it's important to be able to control how deep your API populates. Depth can impact the performance of your queries by affecting the load on the database and the size of the response.
|
||||
|
||||
For example, when you specify a `depth` of `0`, the API response might look like this:
|
||||
|
||||
@@ -48,7 +48,9 @@ import type { Payload } from 'payload'
|
||||
const getPosts = async (payload: Payload) => {
|
||||
const posts = await payload.find({
|
||||
collection: 'posts',
|
||||
depth: 2, // highlight-line
|
||||
// highlight-start
|
||||
depth: 2,
|
||||
// highlight-end
|
||||
})
|
||||
|
||||
return posts
|
||||
@@ -65,7 +67,9 @@ const getPosts = async (payload: Payload) => {
|
||||
To specify depth in the [REST API](../rest-api/overview), you can use the `depth` parameter in your query:
|
||||
|
||||
```ts
|
||||
fetch('https://localhost:3000/api/posts?depth=2') // highlight-line
|
||||
// highlight-start
|
||||
fetch('https://localhost:3000/api/posts?depth=2')
|
||||
// highlight-end
|
||||
.then((res) => res.json())
|
||||
.then((data) => console.log(data))
|
||||
```
|
||||
@@ -75,6 +79,24 @@ fetch('https://localhost:3000/api/posts?depth=2') // highlight-line
|
||||
the `/api/globals` endpoint.
|
||||
</Banner>
|
||||
|
||||
## Default Depth
|
||||
|
||||
If no depth is specified in the request, Payload will use its default depth for all requests. By default, this is set to `2`.
|
||||
|
||||
To change the default depth on the application level, you can use the `defaultDepth` option in your root Payload config:
|
||||
|
||||
```ts
|
||||
import { buildConfig } from 'payload/config'
|
||||
|
||||
export default buildConfig({
|
||||
// ...
|
||||
// highlight-start
|
||||
defaultDepth: 1,
|
||||
// highlight-end
|
||||
// ...
|
||||
})
|
||||
```
|
||||
|
||||
## Max Depth
|
||||
|
||||
Fields like the [Relationship Field](../fields/relationship) or the [Upload Field](../fields/upload) can also set a maximum depth. If exceeded, this will limit the population depth regardless of what the depth might be on the request.
|
||||
@@ -89,7 +111,9 @@ To set a max depth for a field, use the `maxDepth` property in your field config
|
||||
name: 'author',
|
||||
type: 'relationship',
|
||||
relationTo: 'users',
|
||||
maxDepth: 2, // highlight-line
|
||||
// highlight-start
|
||||
maxDepth: 2,
|
||||
// highlight-end
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ The following operators are available for use in queries:
|
||||
<Banner type="success">
|
||||
**Tip:** If you know your users will be querying on certain fields a lot, add
|
||||
`index: true` to the Field Config. This will speed up searches using that
|
||||
field immensely.
|
||||
field immensely. [More details](../database/indexes).
|
||||
</Banner>
|
||||
|
||||
### And / Or Logic
|
||||
@@ -192,3 +192,130 @@ const getPosts = async () => {
|
||||
// Continue to handle the response below...
|
||||
}
|
||||
```
|
||||
|
||||
## Performance
|
||||
|
||||
There are several ways to optimize your queries. Many of these options directly impact overall database overhead, response sizes, and/or computational load and can significantly improve performance.
|
||||
|
||||
When building queries, combine as many of these strategies together as possible to ensure your queries are as performant as they can be.
|
||||
|
||||
<Banner type="success">
|
||||
For more performance tips, see the [Performance
|
||||
documentation](../performance/overview).
|
||||
</Banner>
|
||||
|
||||
### Indexes
|
||||
|
||||
Build [Indexes](../database/indexes) for fields that are often queried or sorted by.
|
||||
|
||||
When your query runs, the database will not search the entire document to find that one field, but will instead use the index to quickly locate the data.
|
||||
|
||||
This is done by adding `index: true` to the Field Config for that field:
|
||||
|
||||
```ts
|
||||
// In your collection configuration
|
||||
{
|
||||
name: 'posts',
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
// highlight-start
|
||||
index: true, // Add an index to the title field
|
||||
// highlight-end
|
||||
},
|
||||
// Other fields...
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
To learn more, see the [Indexes documentation](../database/indexes).
|
||||
|
||||
### Depth
|
||||
|
||||
Set the [Depth](./depth) to only the level that you need to avoid populating unnecessary related documents.
|
||||
|
||||
Relationships will only populate down to the specified depth, and any relationships beyond that depth will only return the ID of the related document.
|
||||
|
||||
```ts
|
||||
const posts = await payload.find({
|
||||
collection: 'posts',
|
||||
where: { ... },
|
||||
// highlight-start
|
||||
depth: 0, // Only return the IDs of related documents
|
||||
// highlight-end
|
||||
})
|
||||
```
|
||||
|
||||
To learn more, see the [Depth documentation](./depth).
|
||||
|
||||
### Limit
|
||||
|
||||
Set the [Limit](./pagination#limit) if you can reliably predict the number of matched documents, such as when querying on a unique field.
|
||||
|
||||
```ts
|
||||
const posts = await payload.find({
|
||||
collection: 'posts',
|
||||
where: {
|
||||
slug: {
|
||||
equals: 'unique-post-slug',
|
||||
},
|
||||
},
|
||||
// highlight-start
|
||||
limit: 1, // Only expect one document to be returned
|
||||
// highlight-end
|
||||
})
|
||||
```
|
||||
|
||||
<Banner type="success">
|
||||
**Tip:** Use in combination with `pagination: false` for best performance when
|
||||
querying by unique fields.
|
||||
</Banner>
|
||||
|
||||
To learn more, see the [Limit documentation](./pagination#limit).
|
||||
|
||||
### Select
|
||||
|
||||
Use the [Select API](./select) to only process and return the fields you need.
|
||||
|
||||
This will reduce the amount of data returned from the request, and also skip processing of any fields that are not selected, such as running their field hooks.
|
||||
|
||||
```ts
|
||||
const posts = await payload.find({
|
||||
collection: 'posts',
|
||||
where: { ... },
|
||||
// highlight-start
|
||||
select: [{
|
||||
title: true,
|
||||
}],
|
||||
// highlight-end
|
||||
```
|
||||
|
||||
This is a basic example, but there are many ways to use the Select API, including selecting specific fields, excluding fields, etc.
|
||||
|
||||
To learn more, see the [Select documentation](./select).
|
||||
|
||||
### Pagination
|
||||
|
||||
[Disable Pagination](./pagination#disabling-pagination) if you can reliably predict the number of matched documents, such as when querying on a unique field.
|
||||
|
||||
```ts
|
||||
const posts = await payload.find({
|
||||
collection: 'posts',
|
||||
where: {
|
||||
slug: {
|
||||
equals: 'unique-post-slug',
|
||||
},
|
||||
},
|
||||
// highlight-start
|
||||
pagination: false, // Return all matched documents without pagination
|
||||
// highlight-end
|
||||
})
|
||||
```
|
||||
|
||||
<Banner type="success">
|
||||
**Tip:** Use in combination with `limit: 1` for best performance when querying
|
||||
by unique fields.
|
||||
</Banner>
|
||||
|
||||
To learn more, see the [Pagination documentation](./pagination).
|
||||
|
||||
@@ -6,9 +6,61 @@ desc: Payload queries are equipped with automatic pagination so you create pagin
|
||||
keywords: query, documents, pagination, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
|
||||
---
|
||||
|
||||
All collection `find` queries are paginated automatically. Responses are returned with top-level meta data related to pagination, and returned documents are nested within a `docs` array.
|
||||
With Pagination you can limit the number of documents returned per page, and get a specific page of results. This is useful for creating paginated lists of documents within your application.
|
||||
|
||||
**`Find` response properties:**
|
||||
All paginated responses include documents nested within a `docs` array, and return top-level meta data related to pagination such as `totalDocs`, `limit`, `totalPages`, `page`, and more.
|
||||
|
||||
<Banner type="success">
|
||||
**Note:** Collection `find` queries are paginated automatically.
|
||||
</Banner>
|
||||
|
||||
## Options
|
||||
|
||||
All Payload APIs support the pagination controls below. With them, you can create paginated lists of documents within your application:
|
||||
|
||||
| Control | Default | Description |
|
||||
| ------------ | ------- | ------------------------------------------------------------------------- |
|
||||
| `limit` | `10` | Limits the number of documents returned per page. [More details](#limit). |
|
||||
| `pagination` | `true` | Set to `false` to disable pagination and return all documents. |
|
||||
| `page` | `1` | Get a specific page number. |
|
||||
|
||||
## Local API
|
||||
|
||||
To specify pagination controls in the [Local API](../local-api/overview), you can use the `limit`, `page`, and `pagination` options in your query:
|
||||
|
||||
```ts
|
||||
import type { Payload } from 'payload'
|
||||
|
||||
const getPosts = async (payload: Payload) => {
|
||||
const posts = await payload.find({
|
||||
collection: 'posts',
|
||||
// highlight-start
|
||||
limit: 10,
|
||||
page: 2,
|
||||
// highlight-end
|
||||
})
|
||||
|
||||
return posts
|
||||
}
|
||||
```
|
||||
|
||||
## REST API
|
||||
|
||||
With the [REST API](../rest-api/overview), you can use the pagination controls below as query strings:
|
||||
|
||||
```ts
|
||||
// highlight-start
|
||||
fetch('https://localhost:3000/api/posts?limit=10&page=2')
|
||||
// highlight-end
|
||||
.then((res) => res.json())
|
||||
.then((data) => console.log(data))
|
||||
```
|
||||
|
||||
## Response
|
||||
|
||||
All paginated responses include documents nested within a `docs` array, and return top-level meta data related to pagination.
|
||||
|
||||
The `find` operation includes the following properties in its response:
|
||||
|
||||
| Property | Description |
|
||||
| --------------- | --------------------------------------------------------- |
|
||||
@@ -51,16 +103,59 @@ All collection `find` queries are paginated automatically. Responses are returne
|
||||
}
|
||||
```
|
||||
|
||||
## Pagination controls
|
||||
## Limit
|
||||
|
||||
All Payload APIs support the pagination controls below. With them, you can create paginated lists of documents within your application:
|
||||
You can specify a `limit` to restrict the number of documents returned per page.
|
||||
|
||||
| Control | Default | Description |
|
||||
| ------------ | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `limit` | `10` | Limits the number of documents returned per page - set to `0` to show all documents, we automatically disabled pagination for you when `limit` is `0` for optimisation |
|
||||
| `pagination` | `true` | Set to `false` to disable pagination and return all documents |
|
||||
| `page` | `1` | Get a specific page number |
|
||||
<Banner type="warning">
|
||||
**Reminder:** By default, any query with `limit: 0` will automatically
|
||||
[disable pagination](#disabling-pagination).
|
||||
</Banner>
|
||||
|
||||
### Disabling pagination within Local API
|
||||
#### Performance benefits
|
||||
|
||||
If you are querying for a specific document and can reliably expect only one document to match, you can set a limit of `1` (or another low number) to reduce the number of database lookups and improve performance.
|
||||
|
||||
For example, when querying a document by a unique field such as `slug`, you can set the limit to `1` since you know there will only be one document with that slug.
|
||||
|
||||
To do this, set the `limit` option in your query:
|
||||
|
||||
```ts
|
||||
await payload.find({
|
||||
collection: 'posts',
|
||||
where: {
|
||||
slug: {
|
||||
equals: 'post-1',
|
||||
},
|
||||
},
|
||||
// highlight-start
|
||||
limit: 1,
|
||||
// highlight-end
|
||||
})
|
||||
```
|
||||
|
||||
## Disabling pagination
|
||||
|
||||
Disabling pagination can improve performance by reducing the overhead of pagination calculations and improve query speed.
|
||||
|
||||
For `find` operations within the Local API, you can disable pagination to retrieve all documents from a collection by passing `pagination: false` to the `find` local operation.
|
||||
|
||||
To do this, set `pagination: false` in your query:
|
||||
|
||||
```ts
|
||||
import type { Payload } from 'payload'
|
||||
|
||||
const getPost = async (payload: Payload) => {
|
||||
const posts = await payload.find({
|
||||
collection: 'posts',
|
||||
where: {
|
||||
title: { equals: 'My Post' },
|
||||
},
|
||||
// highlight-start
|
||||
pagination: false,
|
||||
// highlight-end
|
||||
})
|
||||
|
||||
return posts
|
||||
}
|
||||
```
|
||||
|
||||
@@ -6,9 +6,9 @@ desc: Payload select determines which fields are selected to the result.
|
||||
keywords: query, documents, pagination, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
|
||||
---
|
||||
|
||||
By default, Payload's APIs will return _all fields_ for a given collection or global. But, you may not need all of that data for all of your queries. Sometimes, you might want just a few fields from the response, which can speed up the Payload API and reduce the amount of JSON that is sent to you from the API.
|
||||
By default, Payload's APIs will return _all fields_ for a given collection or global. But, you may not need all of that data for all of your queries. Sometimes, you might want just a few fields from the response.
|
||||
|
||||
This is where Payload's `select` feature comes in. Here, you can define exactly which fields you'd like to retrieve from the API.
|
||||
With the Select API, you can define exactly which fields you'd like to retrieve. This can impact the performance of your queries by affecting the load on the database and the size of the response.
|
||||
|
||||
## Local API
|
||||
|
||||
@@ -21,6 +21,7 @@ import type { Payload } from 'payload'
|
||||
const getPosts = async (payload: Payload) => {
|
||||
const posts = await payload.find({
|
||||
collection: 'posts',
|
||||
// highlight-start
|
||||
select: {
|
||||
text: true,
|
||||
// select a specific field from group
|
||||
@@ -29,7 +30,8 @@ const getPosts = async (payload: Payload) => {
|
||||
},
|
||||
// select all fields from array
|
||||
array: true,
|
||||
}, // highlight-line
|
||||
},
|
||||
// highlight-end
|
||||
})
|
||||
|
||||
return posts
|
||||
@@ -40,12 +42,14 @@ const getPosts = async (payload: Payload) => {
|
||||
const posts = await payload.find({
|
||||
collection: 'posts',
|
||||
// Select everything except for array and group.number
|
||||
// highlight-start
|
||||
select: {
|
||||
array: false,
|
||||
group: {
|
||||
number: false,
|
||||
},
|
||||
}, // highlight-line
|
||||
},
|
||||
// highlight-end
|
||||
})
|
||||
|
||||
return posts
|
||||
@@ -67,8 +71,10 @@ To specify select in the [REST API](../rest-api/overview), you can use the `sele
|
||||
|
||||
```ts
|
||||
fetch(
|
||||
// highlight-start
|
||||
'https://localhost:3000/api/posts?select[color]=true&select[group][number]=true',
|
||||
) // highlight-line
|
||||
// highlight-end
|
||||
)
|
||||
.then((res) => res.json())
|
||||
.then((data) => console.log(data))
|
||||
```
|
||||
@@ -149,7 +155,7 @@ export const Pages: CollectionConfig<'pages'> = {
|
||||
not be able to construct the correct file URL, instead returning `url: null`.
|
||||
</Banner>
|
||||
|
||||
## populate
|
||||
## Populate
|
||||
|
||||
Setting `defaultPopulate` will enforce that each time Payload performs a "population" of a related document, only the fields specified will be queried and returned. However, you can override `defaultPopulate` with the `populate` property in the Local and REST API:
|
||||
|
||||
|
||||
@@ -6,13 +6,15 @@ desc: Payload sort allows you to order your documents by a field in ascending or
|
||||
keywords: query, documents, pagination, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
|
||||
---
|
||||
|
||||
Documents in Payload can be easily sorted by a specific [Field](../fields/overview). When querying Documents, you can pass the name of any top-level field, and the response will sort the Documents by that field in _ascending_ order. If prefixed with a minus symbol ("-"), they will be sorted in _descending_ order. In Local API multiple fields can be specified by using an array of strings. In REST API multiple fields can be specified by separating fields with comma. The minus symbol can be in front of individual fields.
|
||||
Documents in Payload can be easily sorted by a specific [Field](../fields/overview). When querying Documents, you can pass the name of any top-level field, and the response will sort the Documents by that field in _ascending_ order.
|
||||
|
||||
If prefixed with a minus symbol ("-"), they will be sorted in _descending_ order. In Local API multiple fields can be specified by using an array of strings. In REST API multiple fields can be specified by separating fields with comma. The minus symbol can be in front of individual fields.
|
||||
|
||||
Because sorting is handled by the database, the field cannot be a [Virtual Field](https://payloadcms.com/blog/learn-how-virtual-fields-can-help-solve-common-cms-challenges) unless it's [linked with a relationship field](/docs/fields/relationship#linking-virtual-fields-with-relationships). It must be stored in the database to be searchable.
|
||||
|
||||
<Banner type="success">
|
||||
**Tip:** For performance reasons, it is recommended to enable `index: true`
|
||||
for the fields that will be sorted upon. [More details](../fields/overview).
|
||||
for the fields that will be sorted upon. [More details](../database/indexes).
|
||||
</Banner>
|
||||
|
||||
## Local API
|
||||
|
||||
485
docs/rich-text/official-features.mdx
Normal file
485
docs/rich-text/official-features.mdx
Normal file
@@ -0,0 +1,485 @@
|
||||
---
|
||||
description: Features officially maintained by Payload.
|
||||
keywords: lexical, rich text, editor, headless cms, official, features
|
||||
label: Official Features
|
||||
order: 35
|
||||
title: Official Features
|
||||
---
|
||||
|
||||
Below are all the Rich Text Features Payload offers. Everything is customizable; you can [create your own features](/docs/rich-text/custom-features), modify ours and share them with the community.
|
||||
|
||||
## Features Overview
|
||||
|
||||
| Feature Name | Included by default | Description |
|
||||
| ------------------------------- | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **`BoldFeature`** | Yes | Adds support for bold text formatting. |
|
||||
| **`ItalicFeature`** | Yes | Adds support for italic text formatting. |
|
||||
| **`UnderlineFeature`** | Yes | Adds support for underlined text formatting. |
|
||||
| **`StrikethroughFeature`** | Yes | Adds support for strikethrough text formatting. |
|
||||
| **`SubscriptFeature`** | Yes | Adds support for subscript text formatting. |
|
||||
| **`SuperscriptFeature`** | Yes | Adds support for superscript text formatting. |
|
||||
| **`InlineCodeFeature`** | Yes | Adds support for inline code formatting. |
|
||||
| **`ParagraphFeature`** | Yes | Provides entries in both the slash menu and toolbar dropdown for explicit paragraph creation or conversion. |
|
||||
| **`HeadingFeature`** | Yes | Adds Heading Nodes (by default, H1 - H6, but that can be customized) |
|
||||
| **`AlignFeature`** | Yes | Adds support for text alignment (left, center, right, justify) |
|
||||
| **`IndentFeature`** | Yes | Adds support for text indentation with toolbar buttons |
|
||||
| **`UnorderedListFeature`** | Yes | Adds support for unordered lists (ul) |
|
||||
| **`OrderedListFeature`** | Yes | Adds support for ordered lists (ol) |
|
||||
| **`ChecklistFeature`** | Yes | Adds support for interactive 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 |
|
||||
| **`UploadFeature`** | Yes | Allows you to create block-level upload nodes - this supports all kinds of uploads, not just images |
|
||||
| **`HorizontalRuleFeature`** | Yes | Adds support for horizontal rules / separators. Basically displays an `<hr>` element |
|
||||
| **`InlineToolbarFeature`** | Yes | Provides a floating toolbar which appears when you select text. This toolbar only contains actions relevant for selected text |
|
||||
| **`FixedToolbarFeature`** | No | Provides a persistent toolbar pinned to the top and always visible. Both inline and fixed toolbars can be enabled at the same time. |
|
||||
| **`BlocksFeature`** | No | Allows you to use Payload's [Blocks Field](../fields/blocks) directly inside your editor. In the feature props, you can specify the allowed blocks - just like in the Blocks field. |
|
||||
| **`TreeViewFeature`** | No | Provides a debug box under the editor, which allows you to see the current editor state live, the dom, as well as time travel. Very useful for debugging |
|
||||
| **`EXPERIMENTAL_TableFeature`** | No | Adds support for tables. This feature may be removed or receive breaking changes in the future - even within a stable lexical release, without needing a major release. |
|
||||
| **`TextStateFeature`** | No | Allows you to store key-value attributes within TextNodes and assign them inline styles. |
|
||||
|
||||
## In depth
|
||||
|
||||
### BoldFeature
|
||||
|
||||
- Description: Adds support for bold text formatting, along with buttons to apply it in both fixed and inline toolbars.
|
||||
- Included by default: Yes
|
||||
- Markdown Support: `**bold**` or `__bold__`
|
||||
- Keyboard Shortcut: Ctrl/Cmd + B
|
||||
|
||||
### ItalicFeature
|
||||
|
||||
- Description: Adds support for italic text formatting, along with buttons to apply it in both fixed and inline toolbars.
|
||||
- Included by default: Yes
|
||||
- Markdown Support: `*italic*` or `_italic_`
|
||||
- Keyboard Shortcut: Ctrl/Cmd + I
|
||||
|
||||
### UnderlineFeature
|
||||
|
||||
- Description: Adds support for underlined text formatting, along with buttons to apply it in both fixed and inline toolbars.
|
||||
- Included by default: Yes
|
||||
- Keyboard Shortcut: Ctrl/Cmd + U
|
||||
|
||||
### StrikethroughFeature
|
||||
|
||||
- Description: Adds support for strikethrough text formatting, along with buttons to apply it in both fixed and inline toolbars.
|
||||
- Included by default: Yes
|
||||
- Markdown Support: `~~strikethrough~~`
|
||||
|
||||
### SubscriptFeature
|
||||
|
||||
- Description: Adds support for subscript text formatting, along with buttons to apply it in both fixed and inline toolbars.
|
||||
- Included by default: Yes
|
||||
|
||||
### SuperscriptFeature
|
||||
|
||||
- Description: Adds support for superscript text formatting, along with buttons to apply it in both fixed and inline toolbars.
|
||||
- Included by default: Yes
|
||||
|
||||
### InlineCodeFeature
|
||||
|
||||
- Description: Adds support for inline code formatting with distinct styling, along with buttons to apply it in both fixed and inline toolbars.
|
||||
- Included by default: Yes
|
||||
- Markdown Support: \`code\`
|
||||
|
||||
### ParagraphFeature
|
||||
|
||||
- Description: Provides entries in both the slash menu and toolbar dropdown for explicit paragraph creation or conversion.
|
||||
- Included by default: Yes
|
||||
|
||||
### HeadingFeature
|
||||
|
||||
- Description: Adds support for heading nodes (H1-H6) with toolbar dropdown and slash menu entries for each enabled heading size.
|
||||
- Included by default: Yes
|
||||
- Markdown Support: `#`, `##`, `###`, ..., at start of line.
|
||||
- Types:
|
||||
|
||||
```ts
|
||||
type HeadingFeatureProps = {
|
||||
enabledHeadingSizes?: HeadingTagType[] // ['h1', 'h2', 'h3', 'h4', 'h5', 'h6']
|
||||
}
|
||||
```
|
||||
|
||||
- Usage example:
|
||||
|
||||
```ts
|
||||
HeadingFeature({
|
||||
enabledHeadingSizes: ['h1', 'h2', 'h3'], // Default: ['h1', 'h2', 'h3', 'h4', 'h5', 'h6']
|
||||
})
|
||||
```
|
||||
|
||||
### AlignFeature
|
||||
|
||||
- Description: Allows text alignment (left, center, right, justify), along with buttons to apply it in both fixed and inline toolbars.
|
||||
- Included by default: Yes
|
||||
- Keyboard Shortcut: Ctrl/Cmd + Shift + L/E/R/J (left/center/right/justify)
|
||||
|
||||
### IndentFeature
|
||||
|
||||
- Description: Adds support for text indentation, along with buttons to apply it in both fixed and inline toolbars.
|
||||
- Included by default: Yes
|
||||
- Keyboard Shortcut: Tab (increase), Shift + Tab (decrease)
|
||||
- Types:
|
||||
|
||||
```ts
|
||||
type IndentFeatureProps = {
|
||||
/**
|
||||
* The nodes that should not be indented. "type" property of the nodes you don't want to be indented.
|
||||
* These can be: "paragraph", "heading", "listitem", "quote" or other indentable nodes if they exist.
|
||||
*/
|
||||
disabledNodes?: string[]
|
||||
/**
|
||||
* If true, pressing Tab in the middle of a block such as a paragraph or heading will not insert a tabNode.
|
||||
* Instead, Tab will only be used for block-level indentation.
|
||||
* @default false
|
||||
*/
|
||||
disableTabNode?: boolean
|
||||
}
|
||||
```
|
||||
|
||||
- Usage example:
|
||||
|
||||
```ts
|
||||
// Allow block-level indentation only
|
||||
IndentFeature({
|
||||
disableTabNode: true,
|
||||
})
|
||||
```
|
||||
|
||||
### UnorderedListFeature
|
||||
|
||||
- Description: Adds support for unordered lists (bullet points) with toolbar dropdown and slash menu entries.
|
||||
- Included by default: Yes
|
||||
- Markdown Support: `-`, `*`, or `+` at start of line
|
||||
|
||||
### OrderedListFeature
|
||||
|
||||
- Description: Adds support for ordered lists (numbered lists) with toolbar dropdown and slash menu entries.
|
||||
- Included by default: Yes
|
||||
- Markdown Support: `1.` at start of line
|
||||
|
||||
### ChecklistFeature
|
||||
|
||||
- Description: Adds support for interactive checklists with toolbar dropdown and slash menu entries.
|
||||
- Included by default: Yes
|
||||
- Markdown Support: `- [ ]` (unchecked) or `- [x]` (checked)
|
||||
|
||||
### LinkFeature
|
||||
|
||||
- Description: Allows creation of internal and external links with toolbar buttons and automatic URL conversion.
|
||||
- Included by default: Yes
|
||||
- Markdown Support: `[anchor](url)`
|
||||
- Types:
|
||||
|
||||
```ts
|
||||
type LinkFeatureServerProps = {
|
||||
/**
|
||||
* Disables the automatic creation of links
|
||||
* from URLs typed or pasted into the editor,
|
||||
* @default false
|
||||
*/
|
||||
disableAutoLinks?: 'creationOnly' | true
|
||||
/**
|
||||
* A function or array defining additional fields for the link feature.
|
||||
* These will be displayed in the link editor drawer.
|
||||
*/
|
||||
fields?:
|
||||
| ((args: {
|
||||
config: SanitizedConfig
|
||||
defaultFields: FieldAffectingData[]
|
||||
}) => (Field | FieldAffectingData)[])
|
||||
| Field[]
|
||||
/**
|
||||
* Sets a maximum population depth for the internal
|
||||
* doc default field of link, regardless of the
|
||||
* remaining depth when the field is reached.
|
||||
*/
|
||||
maxDepth?: number
|
||||
} & ExclusiveLinkCollectionsProps
|
||||
|
||||
type ExclusiveLinkCollectionsProps =
|
||||
| {
|
||||
disabledCollections?: CollectionSlug[]
|
||||
enabledCollections?: never
|
||||
}
|
||||
| {
|
||||
disabledCollections?: never
|
||||
enabledCollections?: CollectionSlug[]
|
||||
}
|
||||
```
|
||||
|
||||
- Usage example:
|
||||
|
||||
```ts
|
||||
LinkFeature({
|
||||
fields: ({ defaultFields }) => [
|
||||
...defaultFields,
|
||||
{
|
||||
name: 'rel',
|
||||
type: 'select',
|
||||
options: ['noopener', 'noreferrer', 'nofollow'],
|
||||
},
|
||||
],
|
||||
enabledCollections: ['pages', 'posts'], // Collections for internal links
|
||||
maxDepth: 2, // Population depth for internal links
|
||||
disableAutoLinks: false, // Allow auto-conversion of URLs
|
||||
})
|
||||
```
|
||||
|
||||
### RelationshipFeature
|
||||
|
||||
- Description: Allows creation of block-level relationships to other documents with toolbar button and slash menu entry.
|
||||
- Included by default: Yes
|
||||
- Types:
|
||||
|
||||
```ts
|
||||
type RelationshipFeatureProps = {
|
||||
/**
|
||||
* Sets a maximum population depth for this relationship, regardless of the remaining depth when the respective field is reached.
|
||||
*/
|
||||
maxDepth?: number
|
||||
} & ExclusiveRelationshipFeatureProps
|
||||
|
||||
type ExclusiveRelationshipFeatureProps =
|
||||
| {
|
||||
disabledCollections?: CollectionSlug[]
|
||||
enabledCollections?: never
|
||||
}
|
||||
| {
|
||||
disabledCollections?: never
|
||||
enabledCollections?: CollectionSlug[]
|
||||
}
|
||||
```
|
||||
|
||||
- Usage example:
|
||||
|
||||
```ts
|
||||
RelationshipFeature({
|
||||
disabledCollections: ['users'], // Collections to exclude
|
||||
maxDepth: 2, // Population depth for relationships
|
||||
})
|
||||
```
|
||||
|
||||
### UploadFeature
|
||||
|
||||
- Description: Allows creation of upload/media nodes with toolbar button and slash menu entry, supports all file types.
|
||||
- Included by default: Yes
|
||||
- Types:
|
||||
|
||||
```ts
|
||||
type UploadFeatureProps = {
|
||||
collections?: {
|
||||
[collection: CollectionSlug]: {
|
||||
fields: Field[]
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Sets a maximum population depth for this upload (not the fields for this upload), regardless of the remaining depth when the respective field is reached.
|
||||
*/
|
||||
maxDepth?: number
|
||||
}
|
||||
```
|
||||
|
||||
- Usage example:
|
||||
|
||||
```ts
|
||||
UploadFeature({
|
||||
collections: {
|
||||
uploads: {
|
||||
fields: [
|
||||
{
|
||||
name: 'caption',
|
||||
type: 'text',
|
||||
label: 'Caption',
|
||||
},
|
||||
{
|
||||
name: 'alt',
|
||||
type: 'text',
|
||||
label: 'Alt Text',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
maxDepth: 1, // Population depth for uploads
|
||||
})
|
||||
```
|
||||
|
||||
### BlockquoteFeature
|
||||
|
||||
- Description: Allows creation of blockquotes with toolbar button and slash menu entry.
|
||||
- Included by default: Yes
|
||||
- Markdown Support: `> quote text`
|
||||
|
||||
### HorizontalRuleFeature
|
||||
|
||||
- Description: Adds support for horizontal rules/separators with toolbar button and slash menu entry.
|
||||
- Included by default: Yes
|
||||
- Markdown Support: `---`
|
||||
|
||||
### InlineToolbarFeature
|
||||
|
||||
- Description: Provides a floating toolbar that appears when text is selected, containing formatting options relevant to selected text.
|
||||
- Included by default: Yes
|
||||
|
||||
### FixedToolbarFeature
|
||||
|
||||
- Description: Provides a persistent toolbar pinned to the top of the editor that's always visible.
|
||||
- Included by default: No
|
||||
- Types:
|
||||
|
||||
```ts
|
||||
type FixedToolbarFeatureProps = {
|
||||
/**
|
||||
* @default false
|
||||
* If this is enabled, the toolbar will apply
|
||||
* to the focused editor, not the editor with
|
||||
* the FixedToolbarFeature.
|
||||
*/
|
||||
applyToFocusedEditor?: boolean
|
||||
/**
|
||||
* Custom configurations for toolbar groups
|
||||
* Key is the group key (e.g. 'format', 'indent', 'align')
|
||||
* Value is a partial ToolbarGroup object that will
|
||||
* be merged with the default configuration
|
||||
*/
|
||||
customGroups?: CustomGroups
|
||||
/**
|
||||
* @default false
|
||||
* If there is a parent editor with a fixed toolbar,
|
||||
* this will disable the toolbar for this editor.
|
||||
*/
|
||||
disableIfParentHasFixedToolbar?: boolean
|
||||
}
|
||||
```
|
||||
|
||||
- Usage example:
|
||||
|
||||
```ts
|
||||
FixedToolbarFeature({
|
||||
applyToFocusedEditor: false, // Apply to focused editor
|
||||
customGroups: {
|
||||
format: {
|
||||
// Custom configuration for format group
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
### BlocksFeature
|
||||
|
||||
- Description: Allows use of Payload's Blocks Field directly in the editor with toolbar buttons and slash menu entries for each block type.
|
||||
- Included by default: No
|
||||
- Types:
|
||||
|
||||
```ts
|
||||
type BlocksFeatureProps = {
|
||||
blocks?: (Block | BlockSlug)[] | Block[]
|
||||
inlineBlocks?: (Block | BlockSlug)[] | Block[]
|
||||
}
|
||||
```
|
||||
|
||||
- Usage example:
|
||||
|
||||
```ts
|
||||
BlocksFeature({
|
||||
blocks: [
|
||||
{
|
||||
slug: 'callout',
|
||||
fields: [
|
||||
{
|
||||
name: 'text',
|
||||
type: 'text',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
inlineBlocks: [
|
||||
{
|
||||
slug: 'mention',
|
||||
fields: [
|
||||
{
|
||||
name: 'name',
|
||||
type: 'text',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
})
|
||||
```
|
||||
|
||||
### TreeViewFeature
|
||||
|
||||
- Description: Provides a debug panel below the editor showing the editor's internal state, DOM tree, and time travel debugging.
|
||||
- Included by default: No
|
||||
|
||||
### EXPERIMENTAL_TableFeature
|
||||
|
||||
- Description: Adds support for tables with toolbar button and slash menu entry for creation and editing.
|
||||
- Included by default: No
|
||||
|
||||
### TextStateFeature
|
||||
|
||||
- Description: Allows storing key-value attributes in text nodes with inline styles and toolbar dropdown for style selection.
|
||||
- Included by default: No
|
||||
- Types:
|
||||
|
||||
```ts
|
||||
type TextStateFeatureProps = {
|
||||
/**
|
||||
* The keys of the top-level object (stateKeys) represent the attributes that the textNode can have (e.g., color).
|
||||
* The values of the top-level object (stateValues) represent the values that the attribute can have (e.g., red, blue, etc.).
|
||||
* Within the stateValue, you can define inline styles and labels.
|
||||
*/
|
||||
state: { [stateKey: string]: StateValues }
|
||||
}
|
||||
|
||||
type StateValues = {
|
||||
[stateValue: string]: {
|
||||
css: StyleObject
|
||||
label: string
|
||||
}
|
||||
}
|
||||
|
||||
type StyleObject = {
|
||||
[K in keyof PropertiesHyphenFallback]?:
|
||||
| Extract<PropertiesHyphenFallback[K], string>
|
||||
| undefined
|
||||
}
|
||||
```
|
||||
|
||||
- Usage example:
|
||||
|
||||
```ts
|
||||
// We offer default colors that have good contrast and look good in dark and light mode.
|
||||
import { defaultColors, TextStateFeature } from '@payloadcms/richtext-lexical'
|
||||
|
||||
TextStateFeature({
|
||||
// prettier-ignore
|
||||
state: {
|
||||
color: {
|
||||
...defaultColors,
|
||||
// fancy gradients!
|
||||
galaxy: { label: 'Galaxy', css: { background: 'linear-gradient(to right, #0000ff, #ff0000)', color: 'white' } },
|
||||
sunset: { label: 'Sunset', css: { background: 'linear-gradient(to top, #ff5f6d, #6a3093)' } },
|
||||
},
|
||||
// You can have both colored and underlined text at the same time.
|
||||
// If you don't want that, you should group them within the same key.
|
||||
// (just like I did with defaultColors and my fancy gradients)
|
||||
underline: {
|
||||
'solid': { label: 'Solid', css: { 'text-decoration': 'underline', 'text-underline-offset': '4px' } },
|
||||
// You'll probably want to use the CSS light-dark() utility.
|
||||
'yellow-dashed': { label: 'Yellow Dashed', css: { 'text-decoration': 'underline dashed', 'text-decoration-color': 'light-dark(#EAB308,yellow)', 'text-underline-offset': '4px' } },
|
||||
},
|
||||
},
|
||||
}),
|
||||
```
|
||||
|
||||
This is what the example above will look like:
|
||||
|
||||
<LightDarkImage
|
||||
srcDark="https://payloadcms.com/images/docs/text-state-feature.png"
|
||||
srcLight="https://payloadcms.com/images/docs/text-state-feature.png"
|
||||
alt="Example usage in light and dark mode for TextStateFeature with defaultColors and some custom styles"
|
||||
/>
|
||||
@@ -138,39 +138,9 @@ import { CallToAction } from '../blocks/CallToAction'
|
||||
| **`defaultFeatures`** | This opinionated array contains all "recommended" default features. You can see which features are included in the default features in the table below. |
|
||||
| **`rootFeatures`** | This array contains all features that are enabled in the root richText editor (the one defined in the payload.config.ts). If this field is the root richText editor, or if the root richText editor is not a lexical editor, this array will be empty. |
|
||||
|
||||
## Features overview
|
||||
## Official Features
|
||||
|
||||
Here's an overview of all the included features:
|
||||
|
||||
| Feature Name | Included by default | Description |
|
||||
| ----------------------------------- | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **`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 |
|
||||
| **`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 |
|
||||
| **`UploadFeature`** | Yes | Allows you to create block-level upload nodes - this supports all kinds of uploads, not just images |
|
||||
| **`HorizontalRuleFeature`** | Yes | Horizontal rules / separators. Basically displays an `<hr>` element |
|
||||
| **`InlineToolbarFeature`** | Yes | The inline toolbar is the floating toolbar which appears when you select text. This toolbar only contains actions relevant for selected text |
|
||||
| **`FixedToolbarFeature`** | No | This classic toolbar is pinned to the top and always visible. Both inline and fixed toolbars can be enabled at the same time. |
|
||||
| **`BlocksFeature`** | No | Allows you to use Payload's [Blocks Field](../fields/blocks) directly inside your editor. In the feature props, you can specify the allowed blocks - just like in the Blocks field. |
|
||||
| **`TreeViewFeature`** | No | Adds a debug box under the editor, which allows you to see the current editor state live, the dom, as well as time travel. Very useful for debugging |
|
||||
| **`EXPERIMENTAL_TableFeature`** | No | Adds support for tables. This feature may be removed or receive breaking changes in the future - even within a stable lexical release, without needing a major release. |
|
||||
| **`EXPERIMENTAL_TextStateFeature`** | No | Allows you to store key-value attributes within TextNodes and assign them inline styles. |
|
||||
|
||||
Notice how even the toolbars are features? That's how extensible our lexical editor is - you could theoretically create your own toolbar if you wanted to!
|
||||
You can find more information about the official features in our [official features docs](../rich-text/official-features).
|
||||
|
||||
## Creating your own, custom Feature
|
||||
|
||||
|
||||
@@ -116,6 +116,7 @@ _An asterisk denotes that an option is required._
|
||||
| **`withMetadata`** | If specified, appends metadata to the output image file. Accepts a boolean or a function that receives `metadata` and `req`, returning a boolean. |
|
||||
| **`hideFileInputOnCreate`** | Set to `true` to prevent the admin UI from showing file inputs during document creation, useful for programmatic file generation. |
|
||||
| **`hideRemoveFile`** | Set to `true` to prevent the admin UI having a way to remove an existing file while editing. |
|
||||
| **`modifyResponseHeaders`** | Accepts an object with existing `headers` and allows you to manipulate the response headers for media files. [More](#modifying-response-headers) |
|
||||
|
||||
### Payload-wide Upload Options
|
||||
|
||||
@@ -453,7 +454,7 @@ To fetch files from **restricted URLs** that would otherwise be blocked by CORS,
|
||||
|
||||
Here’s how to configure the pasteURL option to control remote URL fetching:
|
||||
|
||||
```
|
||||
```ts
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
export const Media: CollectionConfig = {
|
||||
@@ -466,7 +467,7 @@ export const Media: CollectionConfig = {
|
||||
pathname: '',
|
||||
port: '',
|
||||
protocol: 'https',
|
||||
search: ''
|
||||
search: '',
|
||||
},
|
||||
{
|
||||
hostname: 'example.com',
|
||||
@@ -519,3 +520,44 @@ _An asterisk denotes that an option is required._
|
||||
## Access Control
|
||||
|
||||
All files that are uploaded to each Collection automatically support the `read` [Access Control](/docs/access-control/overview) function from the Collection itself. You can use this to control who should be allowed to see your uploads, and who should not.
|
||||
|
||||
## Modifying response headers
|
||||
|
||||
You can modify the response headers for files by specifying the `modifyResponseHeaders` option in your upload config. This option accepts an object with existing headers and allows you to manipulate the response headers for media files.
|
||||
|
||||
### Modifying existing headers
|
||||
|
||||
With this method you can directly interface with the `Headers` object and modify the existing headers to append or remove headers.
|
||||
|
||||
```ts
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
export const Media: CollectionConfig = {
|
||||
slug: 'media',
|
||||
upload: {
|
||||
modifyResponseHeaders: ({ headers }) => {
|
||||
headers.set('X-Frame-Options', 'DENY') // You can directly set headers without returning
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### Return new headers
|
||||
|
||||
You can also return a new `Headers` object with the modified headers. This is useful if you want to set new headers or remove existing ones.
|
||||
|
||||
```ts
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
export const Media: CollectionConfig = {
|
||||
slug: 'media',
|
||||
upload: {
|
||||
modifyResponseHeaders: ({ headers }) => {
|
||||
const newHeaders = new Headers(headers) // Copy existing headers
|
||||
newHeaders.set('X-Frame-Options', 'DENY') // Set new header
|
||||
|
||||
return newHeaders
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { CollectionConfig } from 'payload'
|
||||
import { sanitizeUserDataForEmail } from 'payload/shared'
|
||||
|
||||
import { generateEmailHTML } from '../email/generateEmailHTML'
|
||||
|
||||
@@ -26,7 +27,7 @@ export const Newsletter: CollectionConfig = {
|
||||
.sendEmail({
|
||||
from: 'sender@example.com',
|
||||
html: await generateEmailHTML({
|
||||
content: `<p>${doc.name ? `Hi ${doc.name}!` : 'Hi!'} We'll be in touch soon...</p>`,
|
||||
content: `<p>${doc.name ? `Hi ${sanitizeUserDataForEmail(doc.name)}!` : 'Hi!'} We'll be in touch soon...</p>`,
|
||||
headline: 'Welcome to the newsletter!',
|
||||
}),
|
||||
subject: 'Thanks for signing up!',
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { generateEmailHTML } from './generateEmailHTML'
|
||||
|
||||
import { sanitizeUserDataForEmail } from 'payload/shared'
|
||||
|
||||
type User = {
|
||||
email: string
|
||||
name?: string
|
||||
@@ -16,7 +18,7 @@ export const generateVerificationEmail = async (
|
||||
const { token, user } = args
|
||||
|
||||
return generateEmailHTML({
|
||||
content: `<p>Hi${user.name ? ' ' + user.name : ''}! Validate your account by clicking the button below.</p>`,
|
||||
content: `<p>Hi${user.name ? ' ' + sanitizeUserDataForEmail(user.name) : ''}! Validate your account by clicking the button below.</p>`,
|
||||
cta: {
|
||||
buttonLabel: 'Verify',
|
||||
url: `${process.env.PAYLOAD_PUBLIC_SERVER_URL}/verify?token=${token}&email=${user.email}`,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "payload-monorepo",
|
||||
"version": "3.46.0",
|
||||
"version": "3.47.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"workspaces": [
|
||||
@@ -76,6 +76,8 @@
|
||||
"dev:prod:memorydb": "cross-env NODE_OPTIONS=--no-deprecation tsx ./test/dev.ts --prod --start-memory-db",
|
||||
"dev:vercel-postgres": "cross-env PAYLOAD_DATABASE=vercel-postgres pnpm runts ./test/dev.ts",
|
||||
"devsafe": "node ./scripts/delete-recursively.js '**/.next' && pnpm dev",
|
||||
"docker:postgres": "docker compose -f test/docker-compose.yml up -d postgres",
|
||||
"docker:postgres:stop": "docker compose -f test/docker-compose.yml down postgres",
|
||||
"docker:restart": "pnpm docker:stop --remove-orphans && pnpm docker:start",
|
||||
"docker:start": "docker compose -f test/docker-compose.yml up -d",
|
||||
"docker:stop": "docker compose -f test/docker-compose.yml down",
|
||||
@@ -91,6 +93,10 @@
|
||||
"reinstall": "pnpm clean:all && pnpm install",
|
||||
"release": "pnpm --filter releaser release --tag latest",
|
||||
"runts": "cross-env NODE_OPTIONS=\"--no-deprecation --no-experimental-strip-types\" node --no-deprecation --no-experimental-strip-types --import @swc-node/register/esm-register",
|
||||
"script:audit": "pnpm audit -P",
|
||||
"script:audit:critical": "pnpm audit -P --audit-level critical",
|
||||
"script:audit:high": "pnpm audit -P --audit-level high",
|
||||
"script:audit:moderate": "pnpm audit -P --audit-level moderate",
|
||||
"script:build-template-with-local-pkgs": "pnpm --filter scripts build-template-with-local-pkgs",
|
||||
"script:gen-templates": "pnpm --filter scripts gen-templates",
|
||||
"script:gen-templates:build": "pnpm --filter scripts gen-templates --build",
|
||||
@@ -121,7 +127,6 @@
|
||||
"prettier --write",
|
||||
"eslint --cache --fix"
|
||||
],
|
||||
"templates/website/**/*": "sh -c \"cd templates/website; pnpm install --no-frozen-lockfile --ignore-workspace; pnpm run lint --fix\"",
|
||||
"templates/**/pnpm-lock.yaml": "pnpm runts scripts/remove-template-lock-files.ts",
|
||||
"tsconfig.base.json": "node scripts/reset-tsconfig.js"
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/admin-bar",
|
||||
"version": "3.46.0",
|
||||
"version": "3.47.0",
|
||||
"description": "An admin bar for React apps using Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "create-payload-app",
|
||||
"version": "3.46.0",
|
||||
"version": "3.47.0",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-mongodb",
|
||||
"version": "3.46.0",
|
||||
"version": "3.47.0",
|
||||
"description": "The officially supported MongoDB database adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-postgres",
|
||||
"version": "3.46.0",
|
||||
"version": "3.47.0",
|
||||
"description": "The officially supported Postgres database adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-sqlite",
|
||||
"version": "3.46.0",
|
||||
"version": "3.47.0",
|
||||
"description": "The officially supported SQLite database adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-vercel-postgres",
|
||||
"version": "3.46.0",
|
||||
"version": "3.47.0",
|
||||
"description": "Vercel Postgres adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/drizzle",
|
||||
"version": "3.46.0",
|
||||
"version": "3.47.0",
|
||||
"description": "A library of shared functions used by different payload database adapters",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,15 +1,67 @@
|
||||
import type { LibSQLDatabase } from 'drizzle-orm/libsql'
|
||||
import type { UpdateOne } from 'payload'
|
||||
import type { FlattenedField, UpdateOne } from 'payload'
|
||||
|
||||
import { eq } from 'drizzle-orm'
|
||||
import toSnakeCase from 'to-snake-case'
|
||||
|
||||
import type { DrizzleAdapter } from './types.js'
|
||||
|
||||
import { buildFindManyArgs } from './find/buildFindManyArgs.js'
|
||||
import { buildQuery } from './queries/buildQuery.js'
|
||||
import { selectDistinct } from './queries/selectDistinct.js'
|
||||
import { transform } from './transform/read/index.js'
|
||||
import { transformForWrite } from './transform/write/index.js'
|
||||
import { upsertRow } from './upsertRow/index.js'
|
||||
import { getTransaction } from './utilities/getTransaction.js'
|
||||
|
||||
/**
|
||||
* Checks whether we should use the upsertRow function for the passed data and otherwise use a simple SQL SET call.
|
||||
* We need to use upsertRow only when the data has arrays, blocks, hasMany select/text/number, localized fields, complex relationships.
|
||||
*/
|
||||
const shouldUseUpsertRow = ({
|
||||
data,
|
||||
fields,
|
||||
}: {
|
||||
data: Record<string, unknown>
|
||||
fields: FlattenedField[]
|
||||
}) => {
|
||||
for (const key in data) {
|
||||
const value = data[key]
|
||||
const field = fields.find((each) => each.name === key)
|
||||
|
||||
if (!field) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (
|
||||
field.type === 'array' ||
|
||||
field.type === 'blocks' ||
|
||||
((field.type === 'text' ||
|
||||
field.type === 'relationship' ||
|
||||
field.type === 'upload' ||
|
||||
field.type === 'select' ||
|
||||
field.type === 'number') &&
|
||||
field.hasMany) ||
|
||||
((field.type === 'relationship' || field.type === 'upload') &&
|
||||
Array.isArray(field.relationTo)) ||
|
||||
field.localized
|
||||
) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (
|
||||
(field.type === 'group' || field.type === 'tab') &&
|
||||
value &&
|
||||
typeof value === 'object' &&
|
||||
shouldUseUpsertRow({ data: value as Record<string, unknown>, fields: field.flattenedFields })
|
||||
) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
export const updateOne: UpdateOne = async function updateOne(
|
||||
this: DrizzleAdapter,
|
||||
{
|
||||
@@ -74,23 +126,71 @@ export const updateOne: UpdateOne = async function updateOne(
|
||||
return null
|
||||
}
|
||||
|
||||
const result = await upsertRow({
|
||||
id: idToUpdate,
|
||||
if (!idToUpdate || shouldUseUpsertRow({ data, fields: collection.flattenedFields })) {
|
||||
const result = await upsertRow({
|
||||
id: idToUpdate,
|
||||
adapter: this,
|
||||
data,
|
||||
db,
|
||||
fields: collection.flattenedFields,
|
||||
ignoreResult: returning === false,
|
||||
joinQuery,
|
||||
operation: 'update',
|
||||
req,
|
||||
select,
|
||||
tableName,
|
||||
})
|
||||
|
||||
if (returning === false) {
|
||||
return null
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
const { row } = transformForWrite({
|
||||
adapter: this,
|
||||
data,
|
||||
db,
|
||||
fields: collection.flattenedFields,
|
||||
ignoreResult: returning === false,
|
||||
joinQuery,
|
||||
operation: 'update',
|
||||
req,
|
||||
select,
|
||||
tableName,
|
||||
})
|
||||
|
||||
const drizzle = db as LibSQLDatabase
|
||||
await drizzle
|
||||
.update(this.tables[tableName])
|
||||
.set(row)
|
||||
// TODO: we can skip fetching idToUpdate here with using the incoming where
|
||||
.where(eq(this.tables[tableName].id, idToUpdate))
|
||||
|
||||
if (returning === false) {
|
||||
return null
|
||||
}
|
||||
|
||||
const findManyArgs = buildFindManyArgs({
|
||||
adapter: this,
|
||||
depth: 0,
|
||||
fields: collection.flattenedFields,
|
||||
joinQuery: false,
|
||||
select,
|
||||
tableName,
|
||||
})
|
||||
|
||||
findManyArgs.where = eq(this.tables[tableName].id, idToUpdate)
|
||||
|
||||
const doc = await db.query[tableName].findFirst(findManyArgs)
|
||||
|
||||
// //////////////////////////////////
|
||||
// TRANSFORM DATA
|
||||
// //////////////////////////////////
|
||||
|
||||
const result = transform({
|
||||
adapter: this,
|
||||
config: this.payload.config,
|
||||
data: doc,
|
||||
fields: collection.flattenedFields,
|
||||
joinQuery: false,
|
||||
tableName,
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/email-nodemailer",
|
||||
"version": "3.46.0",
|
||||
"version": "3.47.0",
|
||||
"description": "Payload Nodemailer Email Adapter",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/email-resend",
|
||||
"version": "3.46.0",
|
||||
"version": "3.47.0",
|
||||
"description": "Payload Resend Email Adapter",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/graphql",
|
||||
"version": "3.46.0",
|
||||
"version": "3.47.0",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/live-preview-react",
|
||||
"version": "3.46.0",
|
||||
"version": "3.47.0",
|
||||
"description": "The official React SDK for Payload Live Preview",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/live-preview-vue",
|
||||
"version": "3.46.0",
|
||||
"version": "3.47.0",
|
||||
"description": "The official Vue SDK for Payload Live Preview",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/live-preview",
|
||||
"version": "3.46.0",
|
||||
"version": "3.47.0",
|
||||
"description": "The official live preview JavaScript SDK for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/next",
|
||||
"version": "3.46.0",
|
||||
"version": "3.47.0",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -120,7 +120,18 @@ export const renderDocument = async ({
|
||||
}))
|
||||
|
||||
if (isEditing && !doc) {
|
||||
throw new Error('not-found')
|
||||
// If it's a collection document that doesn't exist, redirect to collection list
|
||||
if (collectionSlug) {
|
||||
const redirectURL = formatAdminURL({
|
||||
adminRoute,
|
||||
path: `/collections/${collectionSlug}?notFound=${encodeURIComponent(idFromArgs)}`,
|
||||
serverURL,
|
||||
})
|
||||
redirect(redirectURL)
|
||||
} else {
|
||||
// For globals or other cases, keep the 404 behavior
|
||||
throw new Error('not-found')
|
||||
}
|
||||
}
|
||||
|
||||
const [
|
||||
@@ -329,27 +340,36 @@ export const renderDocument = async ({
|
||||
viewType,
|
||||
}
|
||||
|
||||
const isLivePreviewEnabled = Boolean(
|
||||
config.admin?.livePreview?.collections?.includes(collectionSlug) ||
|
||||
config.admin?.livePreview?.globals?.includes(globalSlug) ||
|
||||
collectionConfig?.admin?.livePreview ||
|
||||
globalConfig?.admin?.livePreview,
|
||||
)
|
||||
|
||||
const livePreviewConfig: LivePreviewConfig = {
|
||||
...(config.admin.livePreview || {}),
|
||||
...(isLivePreviewEnabled ? config.admin.livePreview : {}),
|
||||
...(collectionConfig?.admin?.livePreview || {}),
|
||||
...(globalConfig?.admin?.livePreview || {}),
|
||||
}
|
||||
|
||||
const livePreviewURL =
|
||||
typeof livePreviewConfig?.url === 'function'
|
||||
? await livePreviewConfig.url({
|
||||
collectionConfig,
|
||||
data: doc,
|
||||
globalConfig,
|
||||
locale,
|
||||
req,
|
||||
/**
|
||||
* @deprecated
|
||||
* Use `req.payload` instead. This will be removed in the next major version.
|
||||
*/
|
||||
payload: initPageResult.req.payload,
|
||||
})
|
||||
: livePreviewConfig?.url
|
||||
operation !== 'create'
|
||||
? typeof livePreviewConfig?.url === 'function'
|
||||
? await livePreviewConfig.url({
|
||||
collectionConfig,
|
||||
data: doc,
|
||||
globalConfig,
|
||||
locale,
|
||||
req,
|
||||
/**
|
||||
* @deprecated
|
||||
* Use `req.payload` instead. This will be removed in the next major version.
|
||||
*/
|
||||
payload: initPageResult.req.payload,
|
||||
})
|
||||
: livePreviewConfig?.url
|
||||
: ''
|
||||
|
||||
return {
|
||||
data: doc,
|
||||
@@ -380,8 +400,8 @@ export const renderDocument = async ({
|
||||
>
|
||||
<LivePreviewProvider
|
||||
breakpoints={livePreviewConfig?.breakpoints}
|
||||
isLivePreviewEnabled={isLivePreviewEnabled && operation !== 'create'}
|
||||
isLivePreviewing={entityPreferences?.value?.editViewType === 'live-preview'}
|
||||
operation={operation}
|
||||
url={livePreviewURL}
|
||||
>
|
||||
{showHeader && !drawerSlug && (
|
||||
|
||||
@@ -225,6 +225,9 @@ export const renderListView = async (
|
||||
|
||||
const hasCreatePermission = permissions?.collections?.[collectionSlug]?.create
|
||||
|
||||
// Check if there's a notFound query parameter (document ID that wasn't found)
|
||||
const notFoundDocId = typeof searchParams?.notFound === 'string' ? searchParams.notFound : null
|
||||
|
||||
const serverProps: ListViewServerPropsOnly = {
|
||||
collectionConfig,
|
||||
data,
|
||||
@@ -248,6 +251,7 @@ export const renderListView = async (
|
||||
},
|
||||
collectionConfig,
|
||||
description: staticDescription,
|
||||
notFoundDocId,
|
||||
payload,
|
||||
serverProps,
|
||||
})
|
||||
|
||||
@@ -16,12 +16,15 @@ import type {
|
||||
ViewDescriptionServerPropsOnly,
|
||||
} from 'payload'
|
||||
|
||||
import { Banner } from '@payloadcms/ui/elements/Banner'
|
||||
import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent'
|
||||
import React from 'react'
|
||||
|
||||
type Args = {
|
||||
clientProps: ListViewSlotSharedClientProps
|
||||
collectionConfig: SanitizedCollectionConfig
|
||||
description?: StaticDescription
|
||||
notFoundDocId?: null | string
|
||||
payload: Payload
|
||||
serverProps: ListViewServerPropsOnly
|
||||
}
|
||||
@@ -30,6 +33,7 @@ export const renderListViewSlots = ({
|
||||
clientProps,
|
||||
collectionConfig,
|
||||
description,
|
||||
notFoundDocId,
|
||||
payload,
|
||||
serverProps,
|
||||
}: Args): ListViewSlots => {
|
||||
@@ -75,13 +79,31 @@ export const renderListViewSlots = ({
|
||||
})
|
||||
}
|
||||
|
||||
if (collectionConfig.admin.components?.beforeListTable) {
|
||||
result.BeforeListTable = RenderServerComponent({
|
||||
clientProps: clientProps satisfies BeforeListTableClientProps,
|
||||
Component: collectionConfig.admin.components.beforeListTable,
|
||||
importMap: payload.importMap,
|
||||
serverProps: serverProps satisfies BeforeListTableServerPropsOnly,
|
||||
})
|
||||
// Handle beforeListTable with optional banner
|
||||
const existingBeforeListTable = collectionConfig.admin.components?.beforeListTable
|
||||
? RenderServerComponent({
|
||||
clientProps: clientProps satisfies BeforeListTableClientProps,
|
||||
Component: collectionConfig.admin.components.beforeListTable,
|
||||
importMap: payload.importMap,
|
||||
serverProps: serverProps satisfies BeforeListTableServerPropsOnly,
|
||||
})
|
||||
: null
|
||||
|
||||
// Create banner for document not found
|
||||
const notFoundBanner = notFoundDocId ? (
|
||||
<Banner type="error">
|
||||
{serverProps.i18n.t('error:documentNotFound', { id: notFoundDocId })}
|
||||
</Banner>
|
||||
) : null
|
||||
|
||||
// Combine banner and existing component
|
||||
if (notFoundBanner || existingBeforeListTable) {
|
||||
result.BeforeListTable = (
|
||||
<React.Fragment>
|
||||
{notFoundBanner}
|
||||
{existingBeforeListTable}
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
if (collectionConfig.admin.components?.Description) {
|
||||
|
||||
@@ -17,6 +17,7 @@ import React from 'react'
|
||||
import { DefaultTemplate } from '../../templates/Default/index.js'
|
||||
import { MinimalTemplate } from '../../templates/Minimal/index.js'
|
||||
import { initPage } from '../../utilities/initPage/index.js'
|
||||
import { getCustomViewByRoute } from './getCustomViewByRoute.js'
|
||||
import { getRouteData } from './getRouteData.js'
|
||||
|
||||
export type GenerateViewMetadata = (args: {
|
||||
@@ -62,6 +63,32 @@ export const RootPage = async ({
|
||||
|
||||
const searchParams = await searchParamsPromise
|
||||
|
||||
// Redirect `${adminRoute}/collections` to `${adminRoute}`
|
||||
if (segments.length === 1 && segments[0] === 'collections') {
|
||||
const { viewKey } = getCustomViewByRoute({
|
||||
config,
|
||||
currentRoute: '/collections',
|
||||
})
|
||||
|
||||
// Only redirect if there's NO custom view configured for /collections
|
||||
if (!viewKey) {
|
||||
redirect(adminRoute)
|
||||
}
|
||||
}
|
||||
|
||||
// Redirect `${adminRoute}/globals` to `${adminRoute}`
|
||||
if (segments.length === 1 && segments[0] === 'globals') {
|
||||
const { viewKey } = getCustomViewByRoute({
|
||||
config,
|
||||
currentRoute: '/globals',
|
||||
})
|
||||
|
||||
// Only redirect if there's NO custom view configured for /globals
|
||||
if (!viewKey) {
|
||||
redirect(adminRoute)
|
||||
}
|
||||
}
|
||||
|
||||
const {
|
||||
browseByFolderSlugs,
|
||||
DefaultView,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/payload-cloud",
|
||||
"version": "3.46.0",
|
||||
"version": "3.47.0",
|
||||
"description": "The official Payload Cloud plugin",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "payload",
|
||||
"version": "3.46.0",
|
||||
"version": "3.47.0",
|
||||
"description": "Node, React, Headless CMS and Application Framework built on Next.js",
|
||||
"keywords": [
|
||||
"admin panel",
|
||||
@@ -111,7 +111,7 @@
|
||||
"sanitize-filename": "1.6.3",
|
||||
"scmp": "2.1.0",
|
||||
"ts-essentials": "10.0.3",
|
||||
"tsx": "4.19.2",
|
||||
"tsx": "4.20.3",
|
||||
"undici": "7.10.0",
|
||||
"uuid": "10.0.0",
|
||||
"ws": "^8.16.0"
|
||||
|
||||
@@ -50,7 +50,7 @@ export const apiKeyFields = [
|
||||
}
|
||||
if (data?.apiKey) {
|
||||
return crypto
|
||||
.createHmac('sha1', req.payload.secret)
|
||||
.createHmac('sha256', req.payload.secret)
|
||||
.update(data.apiKey as string)
|
||||
.digest('hex')
|
||||
}
|
||||
|
||||
@@ -1,20 +1,50 @@
|
||||
import { status as httpStatus } from 'http-status'
|
||||
|
||||
import type { PayloadHandler } from '../../config/types.js'
|
||||
import type { JoinParams } from '../../utilities/sanitizeJoinParams.js'
|
||||
|
||||
import { getRequestCollection } from '../../utilities/getRequestEntity.js'
|
||||
import { headersWithCors } from '../../utilities/headersWithCors.js'
|
||||
import { isNumber } from '../../utilities/isNumber.js'
|
||||
import { sanitizeJoinParams } from '../../utilities/sanitizeJoinParams.js'
|
||||
import { sanitizePopulateParam } from '../../utilities/sanitizePopulateParam.js'
|
||||
import { sanitizeSelectParam } from '../../utilities/sanitizeSelectParam.js'
|
||||
import { extractJWT } from '../extractJWT.js'
|
||||
import { meOperation } from '../operations/me.js'
|
||||
|
||||
export const meHandler: PayloadHandler = async (req) => {
|
||||
const { searchParams } = req
|
||||
const collection = getRequestCollection(req)
|
||||
const currentToken = extractJWT(req)
|
||||
const depthFromSearchParams = searchParams.get('depth')
|
||||
const draftFromSearchParams = searchParams.get('depth')
|
||||
|
||||
const {
|
||||
depth: depthFromQuery,
|
||||
draft: draftFromQuery,
|
||||
joins,
|
||||
populate,
|
||||
select,
|
||||
} = req.query as {
|
||||
depth?: string
|
||||
draft?: string
|
||||
joins?: JoinParams
|
||||
populate?: Record<string, unknown>
|
||||
select?: Record<string, unknown>
|
||||
}
|
||||
|
||||
const depth = depthFromQuery || depthFromSearchParams
|
||||
const draft = draftFromQuery || draftFromSearchParams
|
||||
|
||||
const result = await meOperation({
|
||||
collection,
|
||||
currentToken: currentToken!,
|
||||
depth: isNumber(depth) ? Number(depth) : undefined,
|
||||
draft: draft === 'true',
|
||||
joins: sanitizeJoinParams(joins),
|
||||
populate: sanitizePopulateParam(populate),
|
||||
req,
|
||||
select: sanitizeSelectParam(select),
|
||||
})
|
||||
|
||||
if (collection.config.auth.removeTokenFromResponses) {
|
||||
|
||||
@@ -214,7 +214,6 @@ export const loginOperation = async <TSlug extends CollectionSlug>(
|
||||
user._strategy = 'local-jwt'
|
||||
|
||||
const authResult = await authenticateLocalStrategy({ doc: user, password })
|
||||
|
||||
user = sanitizeInternalFields(user)
|
||||
|
||||
const maxLoginAttemptsEnabled = args.collection.config.auth.maxLoginAttempts > 0
|
||||
@@ -266,6 +265,9 @@ export const loginOperation = async <TSlug extends CollectionSlug>(
|
||||
returning: false,
|
||||
})
|
||||
|
||||
user.collection = collectionConfig.slug
|
||||
user._strategy = 'local-jwt'
|
||||
|
||||
fieldsToSignArgs.sid = newSessionID
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { decodeJwt } from 'jose'
|
||||
|
||||
import type { Collection } from '../../collections/config/types.js'
|
||||
import type { TypedUser } from '../../index.js'
|
||||
import type { PayloadRequest } from '../../types/index.js'
|
||||
import type { JoinQuery, PayloadRequest, PopulateType, SelectType } from '../../types/index.js'
|
||||
import type { ClientUser } from '../types.js'
|
||||
|
||||
export type MeOperationResult = {
|
||||
@@ -22,11 +22,16 @@ export type MeOperationResult = {
|
||||
export type Arguments = {
|
||||
collection: Collection
|
||||
currentToken?: string
|
||||
depth?: number
|
||||
draft?: boolean
|
||||
joins?: JoinQuery
|
||||
populate?: PopulateType
|
||||
req: PayloadRequest
|
||||
select?: SelectType
|
||||
}
|
||||
|
||||
export const meOperation = async (args: Arguments): Promise<MeOperationResult> => {
|
||||
const { collection, currentToken, req } = args
|
||||
const { collection, currentToken, depth, draft, joins, populate, req, select } = args
|
||||
|
||||
let result: MeOperationResult = {
|
||||
user: null!,
|
||||
@@ -39,9 +44,13 @@ export const meOperation = async (args: Arguments): Promise<MeOperationResult> =
|
||||
const user = (await req.payload.findByID({
|
||||
id: req.user.id,
|
||||
collection: collection.config.slug,
|
||||
depth: isGraphQL ? 0 : collection.config.auth.depth,
|
||||
depth: isGraphQL ? 0 : (depth ?? collection.config.auth.depth),
|
||||
draft,
|
||||
joins,
|
||||
overrideAccess: false,
|
||||
populate,
|
||||
req,
|
||||
select,
|
||||
showHiddenFields: false,
|
||||
})) as TypedUser
|
||||
|
||||
|
||||
@@ -12,16 +12,34 @@ export const APIKeyAuthentication =
|
||||
|
||||
if (authHeader?.startsWith(`${collectionConfig.slug} API-Key `)) {
|
||||
const apiKey = authHeader.replace(`${collectionConfig.slug} API-Key `, '')
|
||||
const apiKeyIndex = crypto.createHmac('sha1', payload.secret).update(apiKey).digest('hex')
|
||||
|
||||
// TODO: V4 remove extra algorithm check
|
||||
// api keys saved prior to v3.46.0 will have sha1
|
||||
const sha1APIKeyIndex = crypto.createHmac('sha1', payload.secret).update(apiKey).digest('hex')
|
||||
const sha256APIKeyIndex = crypto
|
||||
.createHmac('sha256', payload.secret)
|
||||
.update(apiKey)
|
||||
.digest('hex')
|
||||
|
||||
const apiKeyConstraints = [
|
||||
{
|
||||
apiKeyIndex: {
|
||||
equals: sha1APIKeyIndex,
|
||||
},
|
||||
},
|
||||
{
|
||||
apiKeyIndex: {
|
||||
equals: sha256APIKeyIndex,
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
try {
|
||||
const where: Where = {}
|
||||
if (collectionConfig.auth?.verify) {
|
||||
where.and = [
|
||||
{
|
||||
apiKeyIndex: {
|
||||
equals: apiKeyIndex,
|
||||
},
|
||||
or: apiKeyConstraints,
|
||||
},
|
||||
{
|
||||
_verified: {
|
||||
@@ -30,9 +48,7 @@ export const APIKeyAuthentication =
|
||||
},
|
||||
]
|
||||
} else {
|
||||
where.apiKeyIndex = {
|
||||
equals: apiKeyIndex,
|
||||
}
|
||||
where.or = apiKeyConstraints
|
||||
}
|
||||
|
||||
const userQuery = await payload.find({
|
||||
|
||||
@@ -15,7 +15,10 @@ export const resetLoginAttempts = async ({
|
||||
payload,
|
||||
req,
|
||||
}: Args): Promise<void> => {
|
||||
if (!('lockUntil' in doc && typeof doc.lockUntil === 'string') || doc.loginAttempts === 0) {
|
||||
if (
|
||||
!('lockUntil' in doc && typeof doc.lockUntil === 'string') &&
|
||||
(!('loginAttempts' in doc) || doc.loginAttempts === 0)
|
||||
) {
|
||||
return
|
||||
}
|
||||
await payload.update({
|
||||
|
||||
@@ -112,6 +112,8 @@ export {
|
||||
|
||||
export { reduceFieldsToValues } from '../utilities/reduceFieldsToValues.js'
|
||||
|
||||
export { sanitizeUserDataForEmail } from '../utilities/sanitizeUserDataForEmail.js'
|
||||
|
||||
export { setsAreEqual } from '../utilities/setsAreEqual.js'
|
||||
|
||||
export { toKebabCase } from '../utilities/toKebabCase.js'
|
||||
|
||||
@@ -142,6 +142,7 @@ import type {
|
||||
JsonObject,
|
||||
Operation,
|
||||
PayloadRequest,
|
||||
PickPreserveOptional,
|
||||
Where,
|
||||
} from '../../types/index.js'
|
||||
import type {
|
||||
@@ -632,8 +633,8 @@ export type TextField = {
|
||||
Omit<FieldBase, 'validate'>
|
||||
|
||||
export type TextFieldClient = {
|
||||
// @ts-expect-error - vestiges of when tsconfig was not strict. Feel free to improve
|
||||
admin?: AdminClient & Pick<TextField['admin'], 'autoComplete' | 'placeholder' | 'rtl'>
|
||||
admin?: AdminClient &
|
||||
PickPreserveOptional<NonNullable<TextField['admin']>, 'autoComplete' | 'placeholder' | 'rtl'>
|
||||
} & FieldBaseClient &
|
||||
Pick<TextField, 'hasMany' | 'maxLength' | 'maxRows' | 'minLength' | 'minRows' | 'type'>
|
||||
|
||||
@@ -653,8 +654,8 @@ export type EmailField = {
|
||||
} & Omit<FieldBase, 'validate'>
|
||||
|
||||
export type EmailFieldClient = {
|
||||
// @ts-expect-error - vestiges of when tsconfig was not strict. Feel free to improve
|
||||
admin?: AdminClient & Pick<EmailField['admin'], 'autoComplete' | 'placeholder'>
|
||||
admin?: AdminClient &
|
||||
PickPreserveOptional<NonNullable<EmailField['admin']>, 'autoComplete' | 'placeholder'>
|
||||
} & FieldBaseClient &
|
||||
Pick<EmailField, 'type'>
|
||||
|
||||
@@ -677,8 +678,8 @@ export type TextareaField = {
|
||||
} & Omit<FieldBase, 'validate'>
|
||||
|
||||
export type TextareaFieldClient = {
|
||||
// @ts-expect-error - vestiges of when tsconfig was not strict. Feel free to improve
|
||||
admin?: AdminClient & Pick<TextareaField['admin'], 'placeholder' | 'rows' | 'rtl'>
|
||||
admin?: AdminClient &
|
||||
PickPreserveOptional<NonNullable<TextareaField['admin']>, 'placeholder' | 'rows' | 'rtl'>
|
||||
} & FieldBaseClient &
|
||||
Pick<TextareaField, 'maxLength' | 'minLength' | 'type'>
|
||||
|
||||
|
||||
@@ -114,6 +114,7 @@ export const promise = async ({
|
||||
const pathSegments = path ? path.split('.') : []
|
||||
const schemaPathSegments = schemaPath ? schemaPath.split('.') : []
|
||||
const indexPathSegments = indexPath ? indexPath.split('-').filter(Boolean)?.map(Number) : []
|
||||
let removedFieldValue = false
|
||||
|
||||
if (
|
||||
fieldAffectsData(field) &&
|
||||
@@ -121,6 +122,7 @@ export const promise = async ({
|
||||
typeof siblingDoc[field.name!] !== 'undefined' &&
|
||||
!showHiddenFields
|
||||
) {
|
||||
removedFieldValue = true
|
||||
delete siblingDoc[field.name!]
|
||||
}
|
||||
|
||||
@@ -331,7 +333,7 @@ export const promise = async ({
|
||||
// Execute access control
|
||||
let allowDefaultValue = true
|
||||
if (triggerAccessControl && field.access && field.access.read) {
|
||||
const result = overrideAccess
|
||||
const canReadField = overrideAccess
|
||||
? true
|
||||
: await field.access.read({
|
||||
id: doc.id as number | string,
|
||||
@@ -342,7 +344,7 @@ export const promise = async ({
|
||||
siblingData: siblingDoc,
|
||||
})
|
||||
|
||||
if (!result) {
|
||||
if (!canReadField) {
|
||||
allowDefaultValue = false
|
||||
delete siblingDoc[field.name!]
|
||||
}
|
||||
@@ -351,6 +353,7 @@ export const promise = async ({
|
||||
// Set defaultValue on the field for globals being returned without being first created
|
||||
// or collection documents created prior to having a default
|
||||
if (
|
||||
!removedFieldValue &&
|
||||
allowDefaultValue &&
|
||||
typeof siblingDoc[field.name!] === 'undefined' &&
|
||||
typeof field.defaultValue !== 'undefined'
|
||||
|
||||
@@ -156,7 +156,6 @@ export { extractAccessFromPermission } from './auth/extractAccessFromPermission.
|
||||
export { getAccessResults } from './auth/getAccessResults.js'
|
||||
export { getFieldsToSign } from './auth/getFieldsToSign.js'
|
||||
export { getLoginOptions } from './auth/getLoginOptions.js'
|
||||
|
||||
export interface GeneratedTypes {
|
||||
authUntyped: {
|
||||
[slug: string]: {
|
||||
@@ -1536,9 +1535,10 @@ export { importHandlerPath } from './queues/operations/runJobs/runJob/importHand
|
||||
export { getLocalI18n } from './translations/getLocalI18n.js'
|
||||
export * from './types/index.js'
|
||||
export { getFileByPath } from './uploads/getFileByPath.js'
|
||||
export { _internal_safeFetchGlobal } from './uploads/safeFetch.js'
|
||||
export type * from './uploads/types.js'
|
||||
export { addDataAndFileToRequest } from './utilities/addDataAndFileToRequest.js'
|
||||
|
||||
export { addDataAndFileToRequest } from './utilities/addDataAndFileToRequest.js'
|
||||
export { addLocalesToRequestFromData, sanitizeLocales } from './utilities/addLocalesToRequest.js'
|
||||
export { commitTransaction } from './utilities/commitTransaction.js'
|
||||
export {
|
||||
@@ -1610,8 +1610,8 @@ export { deleteCollectionVersions } from './versions/deleteCollectionVersions.js
|
||||
export { appendVersionToQueryKey } from './versions/drafts/appendVersionToQueryKey.js'
|
||||
export { getQueryDraftsSort } from './versions/drafts/getQueryDraftsSort.js'
|
||||
export { enforceMaxVersions } from './versions/enforceMaxVersions.js'
|
||||
export { getLatestCollectionVersion } from './versions/getLatestCollectionVersion.js'
|
||||
|
||||
export { getLatestCollectionVersion } from './versions/getLatestCollectionVersion.js'
|
||||
export { getLatestGlobalVersion } from './versions/getLatestGlobalVersion.js'
|
||||
export { saveVersion } from './versions/saveVersion.js'
|
||||
export type { SchedulePublishTaskInput } from './versions/schedule/types.js'
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { I18n, TFunction } from '@payloadcms/translations'
|
||||
import type DataLoader from 'dataloader'
|
||||
import type { OptionalKeys, RequiredKeys } from 'ts-essentials'
|
||||
import type { URL } from 'url'
|
||||
|
||||
import type {
|
||||
@@ -262,3 +263,8 @@ export type TransformGlobalWithSelect<
|
||||
export type PopulateType = Partial<TypedCollectionSelect>
|
||||
|
||||
export type ResolvedFilterOptions = { [collection: string]: Where }
|
||||
|
||||
export type PickPreserveOptional<T, K extends keyof T> = Partial<
|
||||
Pick<T, Extract<K, OptionalKeys<T>>>
|
||||
> &
|
||||
Pick<T, Extract<K, RequiredKeys<T>>>
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { fileTypeFromBuffer } from 'file-type'
|
||||
|
||||
import type { checkFileRestrictionsParams, FileAllowList } from './types.js'
|
||||
|
||||
import { APIError } from '../errors/index.js'
|
||||
import { ValidationError } from '../errors/index.js'
|
||||
import { validateMimeType } from '../utilities/validateMimeType.js'
|
||||
|
||||
/**
|
||||
* Restricted file types and their extensions.
|
||||
@@ -39,11 +42,12 @@ export const RESTRICTED_FILE_EXT_AND_TYPES: FileAllowList = [
|
||||
{ extensions: ['command'], mimeType: 'application/x-command' },
|
||||
]
|
||||
|
||||
export const checkFileRestrictions = ({
|
||||
export const checkFileRestrictions = async ({
|
||||
collection,
|
||||
file,
|
||||
req,
|
||||
}: checkFileRestrictionsParams): void => {
|
||||
}: checkFileRestrictionsParams): Promise<void> => {
|
||||
const errors: string[] = []
|
||||
const { upload: uploadConfig } = collection
|
||||
const configMimeTypes =
|
||||
uploadConfig &&
|
||||
@@ -58,20 +62,36 @@ export const checkFileRestrictions = ({
|
||||
? (uploadConfig as { allowRestrictedFileTypes?: boolean }).allowRestrictedFileTypes
|
||||
: false
|
||||
|
||||
// Skip validation if `mimeTypes` are defined in the upload config, or `allowRestrictedFileTypes` are allowed
|
||||
if (allowRestrictedFileTypes || configMimeTypes.length) {
|
||||
// Skip validation if `allowRestrictedFileTypes` is true
|
||||
if (allowRestrictedFileTypes) {
|
||||
return
|
||||
}
|
||||
|
||||
const isRestricted = RESTRICTED_FILE_EXT_AND_TYPES.some((type) => {
|
||||
const hasRestrictedExt = type.extensions.some((ext) => file.name.toLowerCase().endsWith(ext))
|
||||
const hasRestrictedMime = type.mimeType === file.mimetype
|
||||
return hasRestrictedExt || hasRestrictedMime
|
||||
})
|
||||
// Secondary mimetype check to assess file type from buffer
|
||||
if (configMimeTypes.length > 0) {
|
||||
const detected = await fileTypeFromBuffer(file.data)
|
||||
const passesMimeTypeCheck = detected?.mime && validateMimeType(detected.mime, configMimeTypes)
|
||||
|
||||
if (isRestricted) {
|
||||
const errorMessage = `File type '${file.mimetype}' not allowed ${file.name}: Restricted file type detected -- set 'allowRestrictedFileTypes' to true to skip this check for this Collection.`
|
||||
req.payload.logger.error(errorMessage)
|
||||
throw new APIError(errorMessage)
|
||||
if (detected && !passesMimeTypeCheck) {
|
||||
errors.push(`Invalid MIME type: ${detected.mime}.`)
|
||||
}
|
||||
} else {
|
||||
const isRestricted = RESTRICTED_FILE_EXT_AND_TYPES.some((type) => {
|
||||
const hasRestrictedExt = type.extensions.some((ext) => file.name.toLowerCase().endsWith(ext))
|
||||
const hasRestrictedMime = type.mimeType === file.mimetype
|
||||
return hasRestrictedExt || hasRestrictedMime
|
||||
})
|
||||
if (isRestricted) {
|
||||
errors.push(
|
||||
`File type '${file.mimetype}' not allowed ${file.name}: Restricted file type detected -- set 'allowRestrictedFileTypes' to true to skip this check for this Collection.`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
req.payload.logger.error(errors.join(', '))
|
||||
throw new ValidationError({
|
||||
errors: [{ message: errors.join(', '), path: 'file' }],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,9 +38,12 @@ export const getFileHandler: PayloadHandler = async (req) => {
|
||||
|
||||
if (collection.config.upload.handlers?.length) {
|
||||
let customResponse: null | Response | void = null
|
||||
const headers = new Headers()
|
||||
|
||||
for (const handler of collection.config.upload.handlers) {
|
||||
customResponse = await handler(req, {
|
||||
doc: accessResult,
|
||||
headers,
|
||||
params: {
|
||||
collection: collection.config.slug,
|
||||
filename,
|
||||
@@ -95,7 +98,7 @@ export const getFileHandler: PayloadHandler = async (req) => {
|
||||
headers.set('Content-Type', fileTypeResult.mime)
|
||||
headers.set('Content-Length', stats.size + '')
|
||||
headers = collection.config.upload?.modifyResponseHeaders
|
||||
? collection.config.upload.modifyResponseHeaders({ headers })
|
||||
? collection.config.upload.modifyResponseHeaders({ headers }) || headers
|
||||
: headers
|
||||
|
||||
return new Response(data, {
|
||||
|
||||
@@ -123,7 +123,7 @@ export const generateFileData = async <T>({
|
||||
}
|
||||
}
|
||||
|
||||
checkFileRestrictions({
|
||||
await checkFileRestrictions({
|
||||
collection: collectionConfig,
|
||||
file,
|
||||
req,
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
import type { Dispatcher } from 'undici'
|
||||
import type { LookupFunction } from 'net'
|
||||
|
||||
import { lookup } from 'dns/promises'
|
||||
import { lookup } from 'dns'
|
||||
import ipaddr from 'ipaddr.js'
|
||||
import { Agent, fetch as undiciFetch } from 'undici'
|
||||
|
||||
/**
|
||||
* @internal this is used to mock the IP `lookup` function in integration tests
|
||||
*/
|
||||
export const _internal_safeFetchGlobal = {
|
||||
lookup,
|
||||
}
|
||||
|
||||
const isSafeIp = (ip: string) => {
|
||||
try {
|
||||
if (!ip) {
|
||||
@@ -25,32 +32,31 @@ const isSafeIp = (ip: string) => {
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a hostname or IP address is safe to fetch from.
|
||||
* @param hostname a hostname or IP address
|
||||
* @returns
|
||||
*/
|
||||
const isSafe = async (hostname: string) => {
|
||||
try {
|
||||
if (ipaddr.isValid(hostname)) {
|
||||
return isSafeIp(hostname)
|
||||
const ssrfFilterInterceptor: LookupFunction = (hostname, options, callback) => {
|
||||
_internal_safeFetchGlobal.lookup(hostname, options, (err, address, family) => {
|
||||
if (err) {
|
||||
callback(err, address, family)
|
||||
} else {
|
||||
let ips = [] as string[]
|
||||
if (Array.isArray(address)) {
|
||||
ips = address.map((a) => a.address)
|
||||
} else {
|
||||
ips = [address]
|
||||
}
|
||||
|
||||
if (ips.some((ip) => !isSafeIp(ip))) {
|
||||
callback(new Error(`Blocked unsafe attempt to ${hostname}`), address, family)
|
||||
return
|
||||
}
|
||||
|
||||
callback(null, address, family)
|
||||
}
|
||||
|
||||
const { address } = await lookup(hostname)
|
||||
return isSafeIp(address)
|
||||
} catch (_ignore) {
|
||||
return false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const ssrfFilterInterceptor: Dispatcher.DispatcherComposeInterceptor = (dispatch) => {
|
||||
return (opts, handler) => {
|
||||
return dispatch(opts, handler)
|
||||
}
|
||||
}
|
||||
|
||||
const safeDispatcher = new Agent().compose(ssrfFilterInterceptor)
|
||||
|
||||
const safeDispatcher = new Agent({
|
||||
connect: { lookup: ssrfFilterInterceptor },
|
||||
})
|
||||
/**
|
||||
* A "safe" version of undici's fetch that prevents SSRF attacks.
|
||||
*
|
||||
@@ -64,11 +70,18 @@ export const safeFetch = async (...args: Parameters<typeof undiciFetch>) => {
|
||||
try {
|
||||
const url = new URL(unverifiedUrl)
|
||||
|
||||
const isHostnameSafe = await isSafe(url.hostname)
|
||||
if (!isHostnameSafe) {
|
||||
throw new Error(`Blocked unsafe attempt to ${url.toString()}`)
|
||||
let hostname = url.hostname
|
||||
|
||||
// Strip brackets from IPv6 addresses (e.g., "[::1]" => "::1")
|
||||
if (hostname.startsWith('[') && hostname.endsWith(']')) {
|
||||
hostname = hostname.slice(1, -1)
|
||||
}
|
||||
|
||||
if (ipaddr.isValid(hostname)) {
|
||||
if (!isSafeIp(hostname)) {
|
||||
throw new Error(`Blocked unsafe attempt to ${hostname}`)
|
||||
}
|
||||
}
|
||||
return await undiciFetch(url, {
|
||||
...options,
|
||||
dispatcher: safeDispatcher,
|
||||
|
||||
@@ -211,6 +211,7 @@ export type UploadConfig = {
|
||||
req: PayloadRequest,
|
||||
args: {
|
||||
doc: TypeWithID
|
||||
headers?: Headers
|
||||
params: { clientUploadContext?: unknown; collection: string; filename: string }
|
||||
},
|
||||
) => Promise<Response> | Promise<void> | Response | void)[]
|
||||
@@ -233,7 +234,7 @@ export type UploadConfig = {
|
||||
* Ability to modify the response headers fetching a file.
|
||||
* @default undefined
|
||||
*/
|
||||
modifyResponseHeaders?: ({ headers }: { headers: Headers }) => Headers
|
||||
modifyResponseHeaders?: ({ headers }: { headers: Headers }) => Headers | void
|
||||
/**
|
||||
* Controls the behavior of pasting/uploading files from URLs.
|
||||
* If set to `false`, fetching from remote URLs is disabled.
|
||||
|
||||
156
packages/payload/src/utilities/sanitizeUserDataForEmail.spec.ts
Normal file
156
packages/payload/src/utilities/sanitizeUserDataForEmail.spec.ts
Normal file
@@ -0,0 +1,156 @@
|
||||
import { sanitizeUserDataForEmail } from './sanitizeUserDataForEmail'
|
||||
|
||||
describe('sanitizeUserDataForEmail', () => {
|
||||
it('should remove anchor tags', () => {
|
||||
const input = '<a href="https://example.com">Click me</a>'
|
||||
const result = sanitizeUserDataForEmail(input)
|
||||
expect(result).toBe('Click me')
|
||||
})
|
||||
|
||||
it('should remove script tags', () => {
|
||||
const unsanitizedData = '<script>alert</script>'
|
||||
const sanitizedData = sanitizeUserDataForEmail(unsanitizedData)
|
||||
expect(sanitizedData).toBe('alert')
|
||||
})
|
||||
|
||||
it('should remove mixed-case script tags', () => {
|
||||
const input = '<ScRipT>alert(1)</sCrIpT>'
|
||||
const result = sanitizeUserDataForEmail(input)
|
||||
expect(result).toBe('alert1')
|
||||
})
|
||||
|
||||
it('should remove embedded base64-encoded scripts', () => {
|
||||
const input = '<img src="data:text/html;base64,PHNjcmlwdD5hbGVydCgxKTwvc2NyaXB0Pg==">'
|
||||
const result = sanitizeUserDataForEmail(input)
|
||||
expect(result).toBe('')
|
||||
})
|
||||
|
||||
it('should remove iframe elements', () => {
|
||||
const input = '<iframe src="malicious.com"></iframe>Frame'
|
||||
const result = sanitizeUserDataForEmail(input)
|
||||
expect(result).toBe('Frame')
|
||||
})
|
||||
|
||||
it('should remove javascript: links in attributes', () => {
|
||||
const input = '<a href="javascript:alert(1)">click</a>'
|
||||
const result = sanitizeUserDataForEmail(input)
|
||||
expect(result).toBe('click')
|
||||
})
|
||||
|
||||
it('should remove mismatched script input', () => {
|
||||
const input = '<script>console.log("test")'
|
||||
const result = sanitizeUserDataForEmail(input)
|
||||
expect(result).toBe('console.log\"test\"')
|
||||
})
|
||||
|
||||
it('should remove encoded scripts via HTML entities', () => {
|
||||
const input = '<script>alert(1)</script>'
|
||||
const result = sanitizeUserDataForEmail(input)
|
||||
expect(result).toBe('alert1')
|
||||
})
|
||||
|
||||
it('should remove template injection syntax', () => {
|
||||
const input = '{{7*7}}'
|
||||
const result = sanitizeUserDataForEmail(input)
|
||||
expect(result).toBe('77')
|
||||
})
|
||||
|
||||
it('should remove invisible zero-width characters', () => {
|
||||
const input = 'a\u200Bler\u200Bt("XSS")'
|
||||
const result = sanitizeUserDataForEmail(input)
|
||||
expect(result).toBe('alert\"XSS\"')
|
||||
})
|
||||
|
||||
it('should remove CSS expressions within style attributes', () => {
|
||||
const input = '<div style="width: expression(alert(\'XSS\'));">Hello</div>'
|
||||
const result = sanitizeUserDataForEmail(input)
|
||||
expect(result).toBe('Hello')
|
||||
})
|
||||
|
||||
it('should not render SVG with onload event', () => {
|
||||
const input = '<svg onload="alert(\'XSS\')">Graphic</svg>'
|
||||
const result = sanitizeUserDataForEmail(input)
|
||||
expect(result).toBe('Graphic')
|
||||
})
|
||||
|
||||
it('should not allow backtick-based patterns', () => {
|
||||
const input = '`alert("XSS")`'
|
||||
const result = sanitizeUserDataForEmail(input)
|
||||
expect(result).toBe('alert\"XSS\"')
|
||||
})
|
||||
|
||||
it('should preserve allowed punctuation', () => {
|
||||
const input = `Hello "world" - it's safe!`
|
||||
const result = sanitizeUserDataForEmail(input)
|
||||
expect(result).toBe(`Hello "world" - it's safe!`)
|
||||
})
|
||||
|
||||
it('should return empty string for non-string input', () => {
|
||||
expect(sanitizeUserDataForEmail(null)).toBe('')
|
||||
expect(sanitizeUserDataForEmail(undefined)).toBe('')
|
||||
expect(sanitizeUserDataForEmail(123)).toBe('')
|
||||
expect(sanitizeUserDataForEmail({})).toBe('')
|
||||
})
|
||||
|
||||
it('should return empty string for an empty string', () => {
|
||||
expect(sanitizeUserDataForEmail('')).toBe('')
|
||||
})
|
||||
|
||||
it('should collapse excessive whitespace', () => {
|
||||
const input = 'This is \n\n a test'
|
||||
expect(sanitizeUserDataForEmail(input)).toBe('This is a test')
|
||||
})
|
||||
|
||||
it('should truncate to maxLength characters', () => {
|
||||
const input = 'a'.repeat(200)
|
||||
const result = sanitizeUserDataForEmail(input, 50)
|
||||
expect(result.length).toBe(50)
|
||||
})
|
||||
|
||||
it('should remove characters outside allowed punctuation', () => {
|
||||
const input = 'Hello @#$%^*()_+=[]{}|\\~`'
|
||||
const result = sanitizeUserDataForEmail(input)
|
||||
expect(result).toBe('Hello')
|
||||
})
|
||||
it('should sanitize syntax in regex-like input', () => {
|
||||
const input = '(?=XSS)(?:abc)'
|
||||
const result = sanitizeUserDataForEmail(input)
|
||||
expect(result).toBe('XSSabc')
|
||||
})
|
||||
|
||||
it('should handle string of only control characters', () => {
|
||||
const input = '\x01\x02\x03\x04'
|
||||
const result = sanitizeUserDataForEmail(input)
|
||||
expect(result).toBe('')
|
||||
})
|
||||
|
||||
it('should sanitize complex script attempt with mixed encoding', () => {
|
||||
const input = '<script>alert(String.fromCharCode(88,83,83))</script>'
|
||||
const result = sanitizeUserDataForEmail(input)
|
||||
expect(result).toBe('alertString.fromCharCode88,83,83')
|
||||
})
|
||||
|
||||
it('should handle deeply nested HTML tags correctly', () => {
|
||||
const input = `<div><section><article><p>Hello <strong>world <em>from <span>deep <a href="#">tags</a></span></em></strong></p></article></section></div>`
|
||||
const result = sanitizeUserDataForEmail(input)
|
||||
expect(result).toBe('Hello world from deep tags')
|
||||
})
|
||||
|
||||
it('should preserve accented Spanish characters', () => {
|
||||
const input = '¡Hola! ¿Cómo estás? ÁÉÍÓÚ ÜÑ ñáéíóú ü'
|
||||
const result = sanitizeUserDataForEmail(input)
|
||||
expect(result).toBe('¡Hola! ¿Cómo estás? ÁÉÍÓÚ ÜÑ ñáéíóú ü')
|
||||
})
|
||||
|
||||
it('should preserve Arabic characters with diacritics', () => {
|
||||
const input = 'مَرْحَبًا بِكَ فِي الْمَوْقِعِ'
|
||||
const result = sanitizeUserDataForEmail(input)
|
||||
expect(result).toBe('مَرْحَبًا بِكَ فِي الْمَوْقِعِ')
|
||||
})
|
||||
|
||||
it('should preserve Japanese characters', () => {
|
||||
const input = 'こんにちゎ、世界!!〆'
|
||||
const result = sanitizeUserDataForEmail(input)
|
||||
expect(result).toBe('こんにちゎ、世界!!〆')
|
||||
})
|
||||
})
|
||||
75
packages/payload/src/utilities/sanitizeUserDataForEmail.ts
Normal file
75
packages/payload/src/utilities/sanitizeUserDataForEmail.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* Sanitizes user data for emails to prevent injection of HTML, executable code, or other malicious content.
|
||||
* This function ensures the content is safe by:
|
||||
* - Removing HTML tags
|
||||
* - Removing control characters
|
||||
* - Normalizing whitespace
|
||||
* - Escaping special HTML characters
|
||||
* - Allowing only letters, numbers, spaces, and basic punctuation
|
||||
* - Limiting length (default 100 characters)
|
||||
*
|
||||
* @param data - data to sanitize
|
||||
* @param maxLength - maximum allowed length (default is 100)
|
||||
* @returns a sanitized string safe to include in email content
|
||||
*/
|
||||
export function sanitizeUserDataForEmail(data: unknown, maxLength = 100): string {
|
||||
if (typeof data !== 'string') {
|
||||
return ''
|
||||
}
|
||||
|
||||
// Decode HTML numeric entities like < or <
|
||||
const decodedEntities = data
|
||||
.replace(/&#x([0-9a-fA-F]+);/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)))
|
||||
.replace(/&#(\d+);/g, (_, dec) => String.fromCharCode(parseInt(dec, 10)))
|
||||
|
||||
// Remove HTML tags
|
||||
const noTags = decodedEntities.replace(/<[^>]+>/g, '')
|
||||
|
||||
const noInvisible = noTags.replace(/[\u200B-\u200F\u2028-\u202F\u2060-\u206F\uFEFF]/g, '')
|
||||
|
||||
// Remove control characters except common whitespace
|
||||
const noControls = [...noInvisible]
|
||||
.filter((char) => {
|
||||
const code = char.charCodeAt(0)
|
||||
return (
|
||||
code >= 32 || // printable and above
|
||||
code === 9 || // tab
|
||||
code === 10 || // new line
|
||||
code === 13 // return
|
||||
)
|
||||
})
|
||||
.join('')
|
||||
|
||||
// Remove '(?' and backticks `
|
||||
let noInjectionSyntax = noControls.replace(/\(\?/g, '').replace(/`/g, '')
|
||||
|
||||
// {{...}} remove braces but keep inner content
|
||||
noInjectionSyntax = noInjectionSyntax.replace(/\{\{(.*?)\}\}/g, '$1')
|
||||
|
||||
// Escape special HTML characters
|
||||
const escaped = noInjectionSyntax
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
|
||||
// Normalize whitespace to single space
|
||||
const normalizedWhitespace = escaped.replace(/\s+/g, ' ')
|
||||
|
||||
// Allow:
|
||||
// - Unicode letters (\p{L})
|
||||
// - Unicode numbers (\p{N})
|
||||
// - Unicode marks (\p{M}, e.g. accents)
|
||||
// - Unicode spaces (\p{Zs})
|
||||
// - Punctuation: common ascii + inverted ! and ?
|
||||
const allowedPunctuation = " .,!?'" + '"¡¿、!()〆-'
|
||||
|
||||
// Escape regex special characters
|
||||
const escapedPunct = allowedPunctuation.replace(/[[\]\\^$*+?.()|{}]/g, '\\$&')
|
||||
|
||||
const pattern = `[^\\p{L}\\p{N}\\p{M}\\p{Zs}${escapedPunct}]`
|
||||
|
||||
const cleaned = normalizedWhitespace.replace(new RegExp(pattern, 'gu'), '')
|
||||
|
||||
// Trim and limit length, trim again to remove trailing spaces
|
||||
return cleaned.slice(0, maxLength).trim()
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-cloud-storage",
|
||||
"version": "3.46.0",
|
||||
"version": "3.47.0",
|
||||
"description": "The official cloud storage plugin for Payload CMS",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -58,6 +58,7 @@ export type StaticHandler = (
|
||||
req: PayloadRequest,
|
||||
args: {
|
||||
doc?: TypeWithID
|
||||
headers?: Headers
|
||||
params: { clientUploadContext?: unknown; collection: string; filename: string }
|
||||
},
|
||||
) => Promise<Response> | Response
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-form-builder",
|
||||
"version": "3.46.0",
|
||||
"version": "3.47.0",
|
||||
"description": "Form builder plugin for Payload CMS",
|
||||
"keywords": [
|
||||
"payload",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-import-export",
|
||||
"version": "3.46.0",
|
||||
"version": "3.47.0",
|
||||
"description": "Import-Export plugin for Payload",
|
||||
"keywords": [
|
||||
"payload",
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
'use client'
|
||||
|
||||
import { Button, SaveButton, Translation, useConfig, useForm, useTranslation } from '@payloadcms/ui'
|
||||
import {
|
||||
Button,
|
||||
SaveButton,
|
||||
toast,
|
||||
Translation,
|
||||
useConfig,
|
||||
useForm,
|
||||
useFormModified,
|
||||
useTranslation,
|
||||
} from '@payloadcms/ui'
|
||||
import React from 'react'
|
||||
|
||||
import type {
|
||||
@@ -15,15 +24,33 @@ export const ExportSaveButton: React.FC = () => {
|
||||
routes: { api },
|
||||
serverURL,
|
||||
},
|
||||
getEntityConfig,
|
||||
} = useConfig()
|
||||
|
||||
const { getData } = useForm()
|
||||
const { getData, setModified } = useForm()
|
||||
const modified = useFormModified()
|
||||
|
||||
const exportsCollectionConfig = getEntityConfig({ collectionSlug: 'exports' })
|
||||
|
||||
const disableSave = exportsCollectionConfig?.admin?.custom?.disableSave === true
|
||||
|
||||
const disableDownload = exportsCollectionConfig?.admin?.custom?.disableDownload === true
|
||||
|
||||
const label = t('general:save')
|
||||
|
||||
const handleDownload = async () => {
|
||||
let timeoutID: null | ReturnType<typeof setTimeout> = null
|
||||
let toastID: null | number | string = null
|
||||
|
||||
try {
|
||||
setModified(false) // Reset modified state
|
||||
const data = getData()
|
||||
|
||||
// Set a timeout to show toast if the request takes longer than 200ms
|
||||
timeoutID = setTimeout(() => {
|
||||
toastID = toast.success('Your export is being processed...')
|
||||
}, 200)
|
||||
|
||||
const response = await fetch(`${serverURL}${api}/exports/download`, {
|
||||
body: JSON.stringify({
|
||||
data,
|
||||
@@ -35,6 +62,16 @@ export const ExportSaveButton: React.FC = () => {
|
||||
method: 'POST',
|
||||
})
|
||||
|
||||
// Clear the timeout if fetch completes quickly
|
||||
if (timeoutID) {
|
||||
clearTimeout(timeoutID)
|
||||
}
|
||||
|
||||
// Dismiss the toast if it was shown
|
||||
if (toastID) {
|
||||
toast.dismiss(toastID)
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to download file')
|
||||
}
|
||||
@@ -63,15 +100,18 @@ export const ExportSaveButton: React.FC = () => {
|
||||
URL.revokeObjectURL(url)
|
||||
} catch (error) {
|
||||
console.error('Error downloading file:', error)
|
||||
toast.error('Error downloading file')
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<SaveButton label={label}></SaveButton>
|
||||
<Button onClick={handleDownload} size="medium" type="button">
|
||||
<Translation i18nKey="upload:download" t={t} />
|
||||
</Button>
|
||||
{!disableSave && <SaveButton label={label} />}
|
||||
{!disableDownload && (
|
||||
<Button disabled={!modified} onClick={handleDownload} size="medium" type="button">
|
||||
<Translation i18nKey="upload:download" t={t} />
|
||||
</Button>
|
||||
)}
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import type { CollectionPreferences, SelectFieldClientComponent } from 'payload'
|
||||
import type { SelectFieldClientComponent } from 'payload'
|
||||
import type { ReactNode } from 'react'
|
||||
|
||||
import {
|
||||
@@ -9,9 +9,9 @@ import {
|
||||
useConfig,
|
||||
useDocumentInfo,
|
||||
useField,
|
||||
usePreferences,
|
||||
useListQuery,
|
||||
} from '@payloadcms/ui'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import React, { useEffect } from 'react'
|
||||
|
||||
import { useImportExport } from '../ImportExportProvider/index.js'
|
||||
import { reduceFields } from './reduceFields.js'
|
||||
@@ -24,62 +24,48 @@ export const FieldsToExport: SelectFieldClientComponent = (props) => {
|
||||
const { value: collectionSlug } = useField<string>({ path: 'collectionSlug' })
|
||||
const { getEntityConfig } = useConfig()
|
||||
const { collection } = useImportExport()
|
||||
const { getPreference } = usePreferences()
|
||||
const [displayedValue, setDisplayedValue] = useState<
|
||||
{ id: string; label: ReactNode; value: string }[]
|
||||
>([])
|
||||
const { query } = useListQuery()
|
||||
|
||||
const collectionConfig = getEntityConfig({ collectionSlug: collectionSlug ?? collection })
|
||||
const fieldOptions = reduceFields({ fields: collectionConfig?.fields })
|
||||
|
||||
useEffect(() => {
|
||||
if (value && value.length > 0) {
|
||||
setDisplayedValue((prevDisplayedValue) => {
|
||||
if (prevDisplayedValue.length > 0) {
|
||||
return prevDisplayedValue
|
||||
} // Prevent unnecessary updates
|
||||
const disabledFields =
|
||||
collectionConfig?.admin?.custom?.['plugin-import-export']?.disabledFields ?? []
|
||||
|
||||
return value.map((field) => {
|
||||
const match = fieldOptions.find((option) => option.value === field)
|
||||
return match ? { ...match, id: field } : { id: field, label: field, value: field }
|
||||
})
|
||||
})
|
||||
}
|
||||
}, [value, fieldOptions])
|
||||
const fieldOptions = reduceFields({
|
||||
disabledFields,
|
||||
fields: collectionConfig?.fields,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (id || !collectionSlug) {
|
||||
return
|
||||
}
|
||||
|
||||
const doAsync = async () => {
|
||||
const currentPreferences = await getPreference<{
|
||||
columns: CollectionPreferences['columns']
|
||||
}>(`collection-${collectionSlug}`)
|
||||
const queryColumns = query?.columns
|
||||
|
||||
const columns = currentPreferences?.columns?.filter((a) => a.active).map((b) => b.accessor)
|
||||
setValue(columns ?? collectionConfig?.admin?.defaultColumns ?? [])
|
||||
if (Array.isArray(queryColumns)) {
|
||||
const cleanColumns = queryColumns.filter(
|
||||
(col): col is string => typeof col === 'string' && !col.startsWith('-'),
|
||||
)
|
||||
// If columns are specified in the query, use them
|
||||
setValue(cleanColumns)
|
||||
} else {
|
||||
// Fallback if no columns in query
|
||||
setValue(collectionConfig?.admin?.defaultColumns ?? [])
|
||||
}
|
||||
}, [id, collectionSlug, query?.columns, collectionConfig?.admin?.defaultColumns, setValue])
|
||||
|
||||
void doAsync()
|
||||
}, [
|
||||
getPreference,
|
||||
collection,
|
||||
setValue,
|
||||
collectionSlug,
|
||||
id,
|
||||
collectionConfig?.admin?.defaultColumns,
|
||||
])
|
||||
const onChange = (options: { id: string; label: ReactNode; value: string }[]) => {
|
||||
if (!options) {
|
||||
setValue([])
|
||||
return
|
||||
}
|
||||
const updatedValue = options?.map((option) =>
|
||||
|
||||
const updatedValue = options.map((option) =>
|
||||
typeof option === 'object' ? option.value : option,
|
||||
)
|
||||
|
||||
setValue(updatedValue)
|
||||
setDisplayedValue(options)
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -96,7 +82,14 @@ export const FieldsToExport: SelectFieldClientComponent = (props) => {
|
||||
// @ts-expect-error react select option
|
||||
onChange={onChange}
|
||||
options={fieldOptions}
|
||||
value={displayedValue}
|
||||
value={
|
||||
Array.isArray(value)
|
||||
? value.map((val) => {
|
||||
const match = fieldOptions.find((opt) => opt.value === val)
|
||||
return match ? { ...match, id: val } : { id: val, label: val, value: val }
|
||||
})
|
||||
: []
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -43,10 +43,12 @@ const combineLabel = ({
|
||||
}
|
||||
|
||||
export const reduceFields = ({
|
||||
disabledFields = [],
|
||||
fields,
|
||||
labelPrefix = null,
|
||||
path = '',
|
||||
}: {
|
||||
disabledFields?: string[]
|
||||
fields: ClientField[]
|
||||
labelPrefix?: React.ReactNode
|
||||
path?: string
|
||||
@@ -66,6 +68,7 @@ export const reduceFields = ({
|
||||
return [
|
||||
...fieldsToUse,
|
||||
...reduceFields({
|
||||
disabledFields,
|
||||
fields: field.fields,
|
||||
labelPrefix: combineLabel({ field, prefix: labelPrefix }),
|
||||
path: createNestedClientFieldPath(path, field),
|
||||
@@ -83,6 +86,7 @@ export const reduceFields = ({
|
||||
return [
|
||||
...tabFields,
|
||||
...reduceFields({
|
||||
disabledFields,
|
||||
fields: tab.fields,
|
||||
labelPrefix,
|
||||
path: isNamedTab ? createNestedClientFieldPath(path, field) : path,
|
||||
@@ -98,6 +102,11 @@ export const reduceFields = ({
|
||||
|
||||
const val = createNestedClientFieldPath(path, field)
|
||||
|
||||
// If the field is disabled, skip it
|
||||
if (disabledFields.includes(val)) {
|
||||
return fieldsToUse
|
||||
}
|
||||
|
||||
const formattedField = {
|
||||
id: val,
|
||||
label: combineLabel({ field, prefix: labelPrefix }),
|
||||
|
||||
@@ -46,6 +46,14 @@ export const Preview = () => {
|
||||
(collection) => collection.slug === collectionSlug,
|
||||
)
|
||||
|
||||
const disabledFieldsUnderscored = React.useMemo(() => {
|
||||
return (
|
||||
collectionConfig?.admin?.custom?.['plugin-import-export']?.disabledFields?.map((f: string) =>
|
||||
f.replace(/\./g, '_'),
|
||||
) ?? []
|
||||
)
|
||||
}, [collectionConfig])
|
||||
|
||||
const isCSV = format === 'csv'
|
||||
|
||||
React.useEffect(() => {
|
||||
@@ -95,7 +103,10 @@ export const Preview = () => {
|
||||
const regex = fieldToRegex(field)
|
||||
return allKeys.filter((key) => regex.test(key))
|
||||
})
|
||||
: allKeys.filter((key) => !defaultMetaFields.includes(key))
|
||||
: allKeys.filter(
|
||||
(key) =>
|
||||
!defaultMetaFields.includes(key) && !disabledFieldsUnderscored.includes(key),
|
||||
)
|
||||
|
||||
const fieldKeys =
|
||||
Array.isArray(fields) && fields.length > 0
|
||||
@@ -136,7 +147,18 @@ export const Preview = () => {
|
||||
}
|
||||
|
||||
void fetchData()
|
||||
}, [collectionConfig, collectionSlug, draft, fields, i18n, limit, locale, sort, where])
|
||||
}, [
|
||||
collectionConfig,
|
||||
collectionSlug,
|
||||
disabledFieldsUnderscored,
|
||||
draft,
|
||||
fields,
|
||||
i18n,
|
||||
limit,
|
||||
locale,
|
||||
sort,
|
||||
where,
|
||||
])
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user