Compare commits
14 Commits
feat/pre-b
...
example/mu
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
60d9250864 | ||
|
|
ff66eddb2e | ||
|
|
dc94dbec58 | ||
|
|
563d069b07 | ||
|
|
8db8a52c56 | ||
|
|
d7b96e2c7e | ||
|
|
c79a0a74a3 | ||
|
|
540e9f0a49 | ||
|
|
71d26e7f57 | ||
|
|
402fb611c6 | ||
|
|
bd8496bf5a | ||
|
|
1a6390e369 | ||
|
|
02e3a34a77 | ||
|
|
744845c733 |
@@ -19,15 +19,3 @@ fb7d1be2f3325d076b7c967b1730afcef37922c2
|
||||
|
||||
# 3.0 prettier & lint everywhere
|
||||
6789e61488a1d3de56f472ac3214faf344030005
|
||||
|
||||
# 3.0 prettier & lint everywhere again
|
||||
83fd4c66222d7846eeb5cc332dfa99bf1e830831
|
||||
|
||||
# Upgrade to typescript-eslint v8, then prettier & lint everywhere
|
||||
86fdad0bb8ab27810599c8a32f3d8cba1341e1df
|
||||
|
||||
# Prettier and lint remaining db packages
|
||||
7fd736ea5b2e9fc4ef936e9dc9e5e3d722f6d8bf
|
||||
|
||||
# Bump all eslint deps, lint and format
|
||||
03291472d6e427ff94e61fca0616cca7796a3a95
|
||||
|
||||
30
.github/CODEOWNERS
vendored
Normal file
30
.github/CODEOWNERS
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
# Order matters. The last matching pattern takes precedence.
|
||||
|
||||
### Package Exports ###
|
||||
/**/exports/ @denolfe @jmikrut
|
||||
|
||||
### Packages ###
|
||||
/packages/richtext-*/ @AlessioGr
|
||||
/packages/plugin-cloud*/ @denolfe
|
||||
/packages/email-*/ @denolfe
|
||||
/packages/storage-*/ @denolfe
|
||||
/packages/create-payload-app/ @denolfe
|
||||
/packages/eslint-*/ @denolfe
|
||||
|
||||
### Templates ###
|
||||
/templates/ @jacobsfletch @denolfe
|
||||
|
||||
### Build Files ###
|
||||
/**/package.json @denolfe
|
||||
/tsconfig.json @denolfe
|
||||
/**/tsconfig*.json @denolfe
|
||||
|
||||
/jest.config.js @denolfe
|
||||
/**/jest.config.js @denolfe
|
||||
|
||||
### Root ###
|
||||
/package.json @denolfe
|
||||
/scripts/ @denolfe
|
||||
/.husky/ @denolfe
|
||||
/.vscode/ @denolfe
|
||||
/.github/ @denolfe
|
||||
47
.github/ISSUE_TEMPLATE/1.bug_report.yml
vendored
Normal file
47
.github/ISSUE_TEMPLATE/1.bug_report.yml
vendored
Normal file
@@ -0,0 +1,47 @@
|
||||
name: Bug Report
|
||||
description: Create a bug report for Payload
|
||||
labels: ['[possible-bug]']
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
*Note:* Feature requests should be opened as [discussions](https://github.com/payloadcms/payload/discussions/new?category=feature-requests-ideas).
|
||||
- type: input
|
||||
id: reproduction-link
|
||||
attributes:
|
||||
label: Link to reproduction
|
||||
description: Want us to look into your issue faster? Follow the [reproduction-guide](https://github.com/payloadcms/payload/blob/main/.github/reproduction-guide.md) for more information.
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe the Bug
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: To Reproduce
|
||||
description: Steps to reproduce the behavior, please provide a clear description of how to reproduce the issue, based on the linked minimal reproduction. Screenshots can be provided in the issue body below. If using code blocks, make sure that [syntax highlighting is correct](https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/creating-and-highlighting-code-blocks#syntax-highlighting) and double check that the rendered preview is not broken.
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: Payload Version
|
||||
description: What version of Payload are you running?
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: adapters-plugins
|
||||
attributes:
|
||||
label: Adapters and Plugins
|
||||
description: What adapters and plugins are you using? ie. db-mongodb, db-postgres, bundler-webpack, etc.
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: Before submitting the issue, go through the steps you've written down to make sure the steps provided are detailed and clear.
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: Contributors should be able to follow the steps provided in order to reproduce the bug.
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: These steps are used to add integration tests to ensure the same issue does not happen again. Thanks in advance!
|
||||
77
.github/ISSUE_TEMPLATE/1.bug_report_v3.yml
vendored
77
.github/ISSUE_TEMPLATE/1.bug_report_v3.yml
vendored
@@ -1,77 +0,0 @@
|
||||
name: Functionality Bug
|
||||
description: '[REPRODUCTION REQUIRED] - Create a bug report'
|
||||
labels: ['status: needs-triage', 'validate-reproduction']
|
||||
body:
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe the Bug
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: reproduction-link
|
||||
attributes:
|
||||
label: Link to the code that reproduces this issue
|
||||
description: >-
|
||||
_REQUIRED_: Please provide a link to your reproduction. Note, if the URL is invalid (404 or a private repository), we may close the issue.
|
||||
Either use `pnpx create-payload-app@latest -t blank` then push to a repo or follow the [reproduction-guide](https://github.com/payloadcms/payload/blob/main/.github/reproduction-guide.md) for more information.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Reproduction Steps
|
||||
description: Steps to reproduce the behavior, please provide a clear description of how to reproduce the issue, based on the linked minimal reproduction. Screenshots can be provided in the issue body below. If using code blocks, make sure that [syntax highlighting is correct](https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/creating-and-highlighting-code-blocks#syntax-highlighting) and double check that the rendered preview is not broken.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: Which area(s) are affected? (Select all that apply)
|
||||
multiple: true
|
||||
options:
|
||||
- 'Not sure'
|
||||
- 'area: core'
|
||||
- 'area: docs'
|
||||
- 'area: templates'
|
||||
- 'area: ui'
|
||||
- 'db-mongodb'
|
||||
- 'db-postgres'
|
||||
- 'db-sqlite'
|
||||
- 'db-vercel-postgres'
|
||||
- 'email-nodemailer'
|
||||
- 'plugin: cloud'
|
||||
- 'plugin: cloud-storage'
|
||||
- 'plugin: form-builder'
|
||||
- 'plugin: nested-docs'
|
||||
- 'plugin: richtext-lexical'
|
||||
- 'plugin: richtext-slate'
|
||||
- 'plugin: search'
|
||||
- 'plugin: sentry'
|
||||
- 'plugin: seo'
|
||||
- 'plugin: stripe'
|
||||
- 'plugin: other'
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Environment Info
|
||||
description: Paste output from `pnpm payload info` _or_ Payload, Node.js, and Next.js versions. Please avoid using "latest"—specific version numbers help us accurately diagnose and resolve issues.
|
||||
render: text
|
||||
placeholder: |
|
||||
Payload:
|
||||
Node.js:
|
||||
Next.js:
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: Before submitting the issue, go through the steps you've written down to make sure the steps provided are detailed and clear.
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: Contributors should be able to follow the steps provided in order to reproduce the bug.
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: These steps are used to add integration tests to ensure the same issue does not happen again. Thanks in advance!
|
||||
40
.github/ISSUE_TEMPLATE/2.design_issue.yml
vendored
40
.github/ISSUE_TEMPLATE/2.design_issue.yml
vendored
@@ -1,40 +0,0 @@
|
||||
name: Design Issue
|
||||
description: '[SCREENSHOT REQUIRED] - Create a design issue report'
|
||||
labels: ['status: needs-triage', 'v3', 'area: ui']
|
||||
body:
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe the Bug.
|
||||
description: >-
|
||||
_REQUIRED:_ Please a screenshot/video of the issue along with a detailed description of the problem.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Reproduction Steps
|
||||
description: Steps to reproduce the behavior, please provide a clear description of how to reproduce the issue, based on the linked minimal reproduction. Screenshots can be provided in the issue body below. If using code blocks, make sure that [syntax highlighting is correct](https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/creating-and-highlighting-code-blocks#syntax-highlighting) and double check that the rendered preview is not broken.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Environment Info
|
||||
description: Paste output from `pnpm payload info` _or_ Payload, Node.js, and Next.js versions. Please avoid using "latest"—specific version numbers help us accurately diagnose and resolve issues.
|
||||
render: text
|
||||
placeholder: |
|
||||
Payload:
|
||||
Node.js:
|
||||
Next.js:
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: Before submitting the issue, go through the steps you've written down to make sure the steps provided are detailed and clear.
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: Contributors should be able to follow the steps provided in order to reproduce the bug.
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: These steps are used to add integration tests to ensure the same issue does not happen again. Thanks in advance!
|
||||
47
.github/ISSUE_TEMPLATE/3.bug_report_v2.yml
vendored
47
.github/ISSUE_TEMPLATE/3.bug_report_v2.yml
vendored
@@ -1,47 +0,0 @@
|
||||
name: v2 Bug Report
|
||||
description: Report a bug for Payload v2. ONLY CRITICAL bugs will be fixed in v2.
|
||||
labels: ['status: needs-triage', 'v2']
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
ONLY CRITICAL bugs will be fixed in v2.
|
||||
- type: input
|
||||
id: reproduction-link
|
||||
attributes:
|
||||
label: Link to reproduction
|
||||
description: Want us to look into your issue faster? Follow the [reproduction-guide](https://github.com/payloadcms/payload/blob/main/.github/reproduction-guide.md) for more information.
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe the Bug
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: To Reproduce
|
||||
description: Steps to reproduce the behavior, please provide a clear description of how to reproduce the issue, based on the linked minimal reproduction. Screenshots can be provided in the issue body below. If using code blocks, make sure that [syntax highlighting is correct](https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/creating-and-highlighting-code-blocks#syntax-highlighting) and double check that the rendered preview is not broken.
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: Payload Version
|
||||
description: What version of Payload are you running?
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: adapters-plugins
|
||||
attributes:
|
||||
label: Adapters and Plugins
|
||||
description: What adapters and plugins are you using? ie. db-mongodb, db-postgres, bundler-webpack, etc.
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: Before submitting the issue, go through the steps you've written down to make sure the steps provided are detailed and clear.
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: Contributors should be able to follow the steps provided in order to reproduce the bug.
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: These steps are used to add integration tests to ensure the same issue does not happen again. Thanks in advance!
|
||||
32
.github/PULL_REQUEST_TEMPLATE.md
vendored
32
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -1,23 +1,23 @@
|
||||
<!--
|
||||
## Description
|
||||
|
||||
Thank you for the PR! Please go through the checklist below and make sure you've completed all the steps.
|
||||
<!-- Please include a summary of the pull request and any related issues it fixes. Please also include relevant motivation and context. -->
|
||||
|
||||
Please review the [CONTRIBUTING.md](https://github.com/payloadcms/payload/blob/main/CONTRIBUTING.md) document in this repository if you haven't already.
|
||||
- [ ] I have read and understand the [CONTRIBUTING.md](https://github.com/payloadcms/payload/blob/main/CONTRIBUTING.md) document in this repository.
|
||||
|
||||
The following items will ensure that your PR is handled as smoothly as possible:
|
||||
## Type of change
|
||||
|
||||
- PR Title must follow conventional commits format. For example, `feat: my new feature`, `fix(plugin-seo): my fix`.
|
||||
- Minimal description explained as if explained to someone not immediately familiar with the code.
|
||||
- Provide before/after screenshots or code diffs if applicable.
|
||||
- Link any related issues/discussions from GitHub or Discord.
|
||||
- Add review comments if necessary to explain to the reviewer the logic behind a change
|
||||
<!-- Please delete options that are not relevant. -->
|
||||
|
||||
### What?
|
||||
- [ ] Chore (non-breaking change which does not add functionality)
|
||||
- [ ] Bug fix (non-breaking change which fixes an issue)
|
||||
- [ ] New feature (non-breaking change which adds functionality)
|
||||
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
|
||||
- [ ] Change to the [templates](https://github.com/payloadcms/payload/tree/main/templates) directory (does not affect core functionality)
|
||||
- [ ] Change to the [examples](https://github.com/payloadcms/payload/tree/main/examples) directory (does not affect core functionality)
|
||||
- [ ] This change requires a documentation update
|
||||
|
||||
### Why?
|
||||
## Checklist:
|
||||
|
||||
### How?
|
||||
|
||||
Fixes #
|
||||
|
||||
-->
|
||||
- [ ] I have added tests that prove my fix is effective or that my feature works
|
||||
- [ ] Existing test suite passes locally with my changes
|
||||
- [ ] I have made corresponding changes to the documentation
|
||||
|
||||
13
.github/actions/release-commenter/.eslintrc.js
vendored
13
.github/actions/release-commenter/.eslintrc.js
vendored
@@ -1,13 +0,0 @@
|
||||
module.exports = {
|
||||
env: {
|
||||
es6: true,
|
||||
node: true,
|
||||
},
|
||||
extends: ['eslint:recommended', 'plugin:@typescript-eslint/eslint-recommended'],
|
||||
parser: '@typescript-eslint/parser',
|
||||
parserOptions: {
|
||||
ecmaVersion: 2018,
|
||||
sourceType: 'module',
|
||||
},
|
||||
plugins: ['@typescript-eslint'],
|
||||
}
|
||||
74
.github/actions/release-commenter/README.md
vendored
74
.github/actions/release-commenter/README.md
vendored
@@ -1,74 +0,0 @@
|
||||
# Release Commenter
|
||||
|
||||
This GitHub Action automatically comments on and/or labels Issues and PRs when a fix is released for them.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> 🔧 Heavily modified version of https://github.com/apexskier/github-release-commenter
|
||||
|
||||
## Fork Modifications
|
||||
|
||||
- Filters to closed PRs only
|
||||
- Adds tag filter to support non-linear releases
|
||||
- Better logging
|
||||
- Moved to pnpm
|
||||
- Uses @vercel/ncc for packaging
|
||||
- Comments on locked issues by unlocking then re-locking
|
||||
|
||||
## How it works
|
||||
|
||||
Use this action in a workflow [triggered by a release](https://docs.github.com/en/free-pro-team@latest/actions/reference/events-that-trigger-workflows#release). It will scan commits between that and the prior release, find associated Issues and PRs, and comment on them to let people know a release has been made. Associated Issues and PRs can be directly [linked](https://docs.github.com/en/free-pro-team@latest/github/managing-your-work-on-github/linking-a-pull-request-to-an-issue) to the commit or manually linked from a PR associated with the commit.
|
||||
|
||||
## Inputs
|
||||
|
||||
**GITHUB_TOKEN**
|
||||
|
||||
A GitHub personal access token with repo scope, such as [`secrets.GITHUB_TOKEN`](https://docs.github.com/en/free-pro-team@latest/actions/reference/authentication-in-a-workflow#about-the-github_token-secret).
|
||||
|
||||
**comment-template** (optional)
|
||||
|
||||
Override the comment posted on Issues and PRs. Set to the empty string to disable commenting. Several variables strings will be automatically replaced:
|
||||
|
||||
- `{release_link}` - a markdown link to the release
|
||||
- `{release_name}` - the release's name
|
||||
- `{release_tag}` - the release's tag
|
||||
|
||||
**label-template** (optional)
|
||||
|
||||
Add the given label. Multiple labels can be separated by commas. Several variable strings will be automatically replaced:
|
||||
|
||||
- `{release_name}` - the release's name
|
||||
- `{release_tag}` - the release's tag
|
||||
|
||||
**skip-label** (optional)
|
||||
|
||||
Skip processing if any of the given labels are present. Same processing rules as **label-template**. Default is "dependencies".
|
||||
|
||||
## Example
|
||||
|
||||
```yml
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
release:
|
||||
steps:
|
||||
- uses: apexskier/github-release-commenter@v1
|
||||
with:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
comment-template: |
|
||||
Release {release_link} addresses this.
|
||||
```
|
||||
|
||||
## Known limitations
|
||||
|
||||
These are some known limitations of this action. I'd like to try to address them in the future.
|
||||
|
||||
- Non-linear releases aren't supported. For example, releasing a patch to a prior major release after a new major release has been bumped.
|
||||
- Non-sequential releases aren't supported. For example, if you release multiple prereleases between two official releases, this will only create a comment for the first prerelease in which a fix is released, not the final release.
|
||||
- The first release for a project will be ignored. This is intentional, as the use case is unlikely. Most projects will either have several alphas that don't need release comments, or won't use issues/PRs for the first commit.
|
||||
- If a large number of things are commented on, you may see the error `Error: You have triggered an abuse detection mechanism. Please wait a few minutes before you try again.`. Consider using the `skip-label` input to reduce your load on the GitHub API.
|
||||
|
||||
## Versions
|
||||
|
||||
Workflows will automatically update the tags `v1` and `latest`, allowing you to reference one of those instead of locking to a specific release.
|
||||
32
.github/actions/release-commenter/action.yml
vendored
32
.github/actions/release-commenter/action.yml
vendored
@@ -1,32 +0,0 @@
|
||||
name: Release Commenter
|
||||
description: Comment on PRs and Issues when a fix is released
|
||||
branding:
|
||||
icon: message-square
|
||||
color: blue
|
||||
inputs:
|
||||
GITHUB_TOKEN:
|
||||
description: |
|
||||
A GitHub personal access token with repo scope, such as
|
||||
secrets.GITHUB_TOKEN.
|
||||
required: true
|
||||
comment-template:
|
||||
description: |
|
||||
Text template for the comment string.
|
||||
required: false
|
||||
default: |
|
||||
Included in release {release_link}
|
||||
label-template:
|
||||
description: Add the given label. Multiple labels can be separated by commas.
|
||||
required: false
|
||||
skip-label:
|
||||
description: Skip commenting if any of the given label are present. Multiple labels can be separated by commas.
|
||||
required: false
|
||||
default: 'dependencies'
|
||||
tag-filter:
|
||||
description: |
|
||||
Filter tags by a regular expression. Must be escaped. e.g. 'v\\d' to isolate tags between major versions.
|
||||
required: false
|
||||
default: null
|
||||
runs:
|
||||
using: node20
|
||||
main: dist/index.js
|
||||
34199
.github/actions/release-commenter/dist/index.js
vendored
34199
.github/actions/release-commenter/dist/index.js
vendored
File diff suppressed because one or more lines are too long
@@ -1,7 +0,0 @@
|
||||
module.exports = {
|
||||
testEnvironment: 'node',
|
||||
testPathIgnorePatterns: ['/node_modules/', '<rootDir>/dist/'],
|
||||
transform: {
|
||||
'^.+\\.(t|j)sx?$': ['@swc/jest'],
|
||||
},
|
||||
}
|
||||
33
.github/actions/release-commenter/package.json
vendored
33
.github/actions/release-commenter/package.json
vendored
@@ -1,33 +0,0 @@
|
||||
{
|
||||
"name": "release-commenter",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"description": "GitHub Action to automatically comment on PRs and Issues when a fix is released.",
|
||||
"license": "MIT",
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
"build": "pnpm build:typecheck && pnpm build:ncc",
|
||||
"build:ncc": "ncc build src/index.ts -t -o dist",
|
||||
"build:typecheck": "tsc",
|
||||
"clean": "rimraf dist",
|
||||
"test": "jest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@actions/core": "^1.3.0",
|
||||
"@actions/github": "^5.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@octokit/webhooks-types": "^7.5.1",
|
||||
"@swc/jest": "^0.2.37",
|
||||
"@types/jest": "^27.5.2",
|
||||
"@types/node": "^20.16.5",
|
||||
"@typescript-eslint/eslint-plugin": "^4.33.0",
|
||||
"@typescript-eslint/parser": "^4.33.0",
|
||||
"@vercel/ncc": "0.38.1",
|
||||
"concurrently": "^8.2.2",
|
||||
"eslint": "^7.32.0",
|
||||
"jest": "^29.7.0",
|
||||
"prettier": "^3.3.3",
|
||||
"typescript": "^4.9.5"
|
||||
}
|
||||
}
|
||||
4241
.github/actions/release-commenter/pnpm-lock.yaml
generated
vendored
4241
.github/actions/release-commenter/pnpm-lock.yaml
generated
vendored
File diff suppressed because it is too large
Load Diff
@@ -1,266 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`tests feature tests can apply labels 1`] = `
|
||||
[
|
||||
[
|
||||
{
|
||||
"issue_number": 123,
|
||||
"labels": [
|
||||
":dart: landed",
|
||||
"release-current_tag_name",
|
||||
"Release Name",
|
||||
],
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
"issue_number": 7,
|
||||
"labels": [
|
||||
":dart: landed",
|
||||
"release-current_tag_name",
|
||||
"Release Name",
|
||||
],
|
||||
},
|
||||
],
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`tests main test 1`] = `
|
||||
{
|
||||
"graphql": [MockFunction] {
|
||||
"calls": [
|
||||
[
|
||||
"
|
||||
{
|
||||
resource(url: "http://repository/commit/SHA1") {
|
||||
... on Commit {
|
||||
messageHeadlineHTML
|
||||
messageBodyHTML
|
||||
associatedPullRequests(first: 10) {
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
}
|
||||
edges {
|
||||
node {
|
||||
bodyHTML
|
||||
number
|
||||
state
|
||||
labels(first: 10) {
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
}
|
||||
nodes {
|
||||
name
|
||||
}
|
||||
}
|
||||
timelineItems(itemTypes: [CONNECTED_EVENT, DISCONNECTED_EVENT], first: 100) {
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
}
|
||||
nodes {
|
||||
... on ConnectedEvent {
|
||||
__typename
|
||||
isCrossRepository
|
||||
subject {
|
||||
... on Issue {
|
||||
number
|
||||
}
|
||||
}
|
||||
}
|
||||
... on DisconnectedEvent {
|
||||
__typename
|
||||
isCrossRepository
|
||||
subject {
|
||||
... on Issue {
|
||||
number
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
",
|
||||
],
|
||||
[
|
||||
"
|
||||
{
|
||||
resource(url: "http://repository/commit/SHA2") {
|
||||
... on Commit {
|
||||
messageHeadlineHTML
|
||||
messageBodyHTML
|
||||
associatedPullRequests(first: 10) {
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
}
|
||||
edges {
|
||||
node {
|
||||
bodyHTML
|
||||
number
|
||||
state
|
||||
labels(first: 10) {
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
}
|
||||
nodes {
|
||||
name
|
||||
}
|
||||
}
|
||||
timelineItems(itemTypes: [CONNECTED_EVENT, DISCONNECTED_EVENT], first: 100) {
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
}
|
||||
nodes {
|
||||
... on ConnectedEvent {
|
||||
__typename
|
||||
isCrossRepository
|
||||
subject {
|
||||
... on Issue {
|
||||
number
|
||||
}
|
||||
}
|
||||
}
|
||||
... on DisconnectedEvent {
|
||||
__typename
|
||||
isCrossRepository
|
||||
subject {
|
||||
... on Issue {
|
||||
number
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
",
|
||||
],
|
||||
],
|
||||
"results": [
|
||||
{
|
||||
"type": "return",
|
||||
"value": Promise {},
|
||||
},
|
||||
{
|
||||
"type": "return",
|
||||
"value": Promise {},
|
||||
},
|
||||
],
|
||||
},
|
||||
"rest": {
|
||||
"issues": {
|
||||
"addLabels": [MockFunction],
|
||||
"createComment": [MockFunction] {
|
||||
"calls": [
|
||||
[
|
||||
{
|
||||
"body": "Included in release [current_tag_name](http://current_release). Replacements: current_tag_name, current_tag_name.",
|
||||
"issue_number": 3,
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
"body": "Included in release [current_tag_name](http://current_release). Replacements: current_tag_name, current_tag_name.",
|
||||
"issue_number": 123,
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
"body": "Included in release [current_tag_name](http://current_release). Replacements: current_tag_name, current_tag_name.",
|
||||
"issue_number": 7,
|
||||
},
|
||||
],
|
||||
],
|
||||
"results": [
|
||||
{
|
||||
"type": "return",
|
||||
"value": Promise {},
|
||||
},
|
||||
{
|
||||
"type": "return",
|
||||
"value": Promise {},
|
||||
},
|
||||
{
|
||||
"type": "return",
|
||||
"value": Promise {},
|
||||
},
|
||||
],
|
||||
},
|
||||
"get": [MockFunction] {
|
||||
"calls": [
|
||||
[
|
||||
{
|
||||
"issue_number": 3,
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
"issue_number": 123,
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
"issue_number": 7,
|
||||
},
|
||||
],
|
||||
],
|
||||
"results": [
|
||||
{
|
||||
"type": "return",
|
||||
"value": Promise {},
|
||||
},
|
||||
{
|
||||
"type": "return",
|
||||
"value": Promise {},
|
||||
},
|
||||
{
|
||||
"type": "return",
|
||||
"value": Promise {},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
"repos": {
|
||||
"compareCommits": [MockFunction] {
|
||||
"calls": [
|
||||
[
|
||||
{
|
||||
"base": "prior_tag_name",
|
||||
"head": "current_tag_name",
|
||||
},
|
||||
],
|
||||
],
|
||||
"results": [
|
||||
{
|
||||
"type": "return",
|
||||
"value": Promise {},
|
||||
},
|
||||
],
|
||||
},
|
||||
"listReleases": [MockFunction] {
|
||||
"calls": [
|
||||
[
|
||||
{
|
||||
"per_page": 100,
|
||||
},
|
||||
],
|
||||
],
|
||||
"results": [
|
||||
{
|
||||
"type": "return",
|
||||
"value": Promise {},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
||||
399
.github/actions/release-commenter/src/index.test.ts
vendored
399
.github/actions/release-commenter/src/index.test.ts
vendored
@@ -1,399 +0,0 @@
|
||||
import type * as githubModule from '@actions/github'
|
||||
import type * as coreModule from '@actions/core'
|
||||
import { mock } from 'node:test'
|
||||
|
||||
jest.mock('@actions/core')
|
||||
jest.mock('@actions/github')
|
||||
|
||||
type Mocked<T> = {
|
||||
-readonly [P in keyof T]: T[P] extends Function ? jest.Mock<T[P]> : jest.Mocked<Partial<T[P]>>
|
||||
}
|
||||
|
||||
const github = require('@actions/github') as jest.Mocked<Mocked<typeof githubModule>>
|
||||
const core = require('@actions/core') as jest.Mocked<Mocked<typeof coreModule>>
|
||||
|
||||
describe('tests', () => {
|
||||
let mockOctokit: any = {}
|
||||
let currentTag: string = 'current_tag_name'
|
||||
|
||||
;(core.warning as any) = jest.fn(console.warn.bind(console))
|
||||
;(core.error as any) = jest.fn(console.error.bind(console))
|
||||
|
||||
let commentTempate: string = ''
|
||||
let labelTemplate: string | null = null
|
||||
const skipLabelTemplate: string | null = 'skip,test'
|
||||
let tagFilter: string | RegExp | null = null
|
||||
|
||||
let simpleMockOctokit: any = {}
|
||||
|
||||
beforeEach(() => {
|
||||
tagFilter = null
|
||||
currentTag = 'current_tag_name'
|
||||
;(github.context as any) = {
|
||||
payload: {
|
||||
repo: {
|
||||
owner: 'owner',
|
||||
repo: 'repo',
|
||||
},
|
||||
release: {
|
||||
tag_name: currentTag,
|
||||
},
|
||||
repository: { html_url: 'http://repository' },
|
||||
},
|
||||
}
|
||||
|
||||
github.getOctokit.mockReset().mockImplementationOnce(((token: string) => {
|
||||
expect(token).toBe('GITHUB_TOKEN_VALUE')
|
||||
return mockOctokit
|
||||
}) as any)
|
||||
;(core.getInput as any).mockImplementation((key: string) => {
|
||||
if (key == 'GITHUB_TOKEN') {
|
||||
return 'GITHUB_TOKEN_VALUE'
|
||||
}
|
||||
if (key == 'comment-template') {
|
||||
return commentTempate
|
||||
}
|
||||
if (key == 'label-template') {
|
||||
return labelTemplate
|
||||
}
|
||||
if (key == 'skip-label') {
|
||||
return skipLabelTemplate
|
||||
}
|
||||
if (key == 'tag-filter') {
|
||||
return tagFilter
|
||||
}
|
||||
fail(`Unexpected input key ${key}`)
|
||||
})
|
||||
|
||||
commentTempate =
|
||||
'Included in release {release_link}. Replacements: {release_name}, {release_tag}.'
|
||||
labelTemplate = null
|
||||
simpleMockOctokit = {
|
||||
rest: {
|
||||
issues: {
|
||||
get: jest.fn(() => Promise.resolve({ data: { locked: false } })),
|
||||
createComment: jest.fn(() => Promise.resolve()),
|
||||
addLabels: jest.fn(() => Promise.resolve()),
|
||||
},
|
||||
repos: {
|
||||
listReleases: jest.fn(() =>
|
||||
Promise.resolve({
|
||||
data: [
|
||||
{
|
||||
name: 'Release Name',
|
||||
tag_name: 'current_tag_name',
|
||||
html_url: 'http://current_release',
|
||||
},
|
||||
{
|
||||
tag_name: 'prior_tag_name',
|
||||
html_url: 'http://prior_release',
|
||||
},
|
||||
],
|
||||
}),
|
||||
),
|
||||
compareCommits: jest.fn(() =>
|
||||
Promise.resolve({
|
||||
data: { commits: [{ sha: 'SHA1' }] },
|
||||
}),
|
||||
),
|
||||
},
|
||||
},
|
||||
graphql: jest.fn(() =>
|
||||
Promise.resolve({
|
||||
resource: {
|
||||
messageHeadlineHTML: '',
|
||||
messageBodyHTML:
|
||||
'<span class="issue-keyword tooltipped tooltipped-se" aria-label="This commit closes issue #123.">Closes</span> <p><span class="issue-keyword tooltipped tooltipped-se" aria-label="This pull request closes issue #7.">Closes</span>',
|
||||
associatedPullRequests: {
|
||||
pageInfo: { hasNextPage: false },
|
||||
edges: [],
|
||||
},
|
||||
},
|
||||
}),
|
||||
),
|
||||
}
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
expect(core.error).not.toHaveBeenCalled()
|
||||
expect(core.warning).not.toHaveBeenCalled()
|
||||
expect(core.setFailed).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test('main test', async () => {
|
||||
mockOctokit = {
|
||||
...simpleMockOctokit,
|
||||
rest: {
|
||||
issues: {
|
||||
get: jest.fn(() => Promise.resolve({ data: { locked: false } })),
|
||||
createComment: jest.fn(() => Promise.resolve()),
|
||||
addLabels: jest.fn(() => Promise.resolve()),
|
||||
},
|
||||
repos: {
|
||||
listReleases: jest.fn(() =>
|
||||
Promise.resolve({
|
||||
data: [
|
||||
{
|
||||
tag_name: 'current_tag_name',
|
||||
html_url: 'http://current_release',
|
||||
},
|
||||
{
|
||||
tag_name: 'prior_tag_name',
|
||||
html_url: 'http://prior_release',
|
||||
},
|
||||
],
|
||||
}),
|
||||
),
|
||||
compareCommits: jest.fn(() =>
|
||||
Promise.resolve({
|
||||
data: { commits: [{ sha: 'SHA1' }, { sha: 'SHA2' }] },
|
||||
}),
|
||||
),
|
||||
},
|
||||
},
|
||||
graphql: jest.fn(() =>
|
||||
Promise.resolve({
|
||||
resource: {
|
||||
messageHeadlineHTML:
|
||||
'<span class="issue-keyword tooltipped tooltipped-se" aria-label="This commit closes issue #3.">Closes</span> <a class="issue-link js-issue-link" data-error-text="Failed to load title" data-id="718013420" data-permission-text="Title is private" data-url="https://github.com/apexskier/github-release-commenter/issues/1" data-hovercard-type="issue" data-hovercard-url="/apexskier/github-release-commenter/issues/1/hovercard" href="https://github.com/apexskier/github-release-commenter/issues/1">#1</a>',
|
||||
messageBodyHTML:
|
||||
'<span class="issue-keyword tooltipped tooltipped-se" aria-label="This commit closes issue #123.">Closes</span> <p><span class="issue-keyword tooltipped tooltipped-se" aria-label="This pull request closes issue #7.">Closes</span>',
|
||||
associatedPullRequests: {
|
||||
pageInfo: { hasNextPage: false },
|
||||
edges: [
|
||||
{
|
||||
node: {
|
||||
bodyHTML:
|
||||
'<span class="issue-keyword tooltipped tooltipped-se" aria-label="This commit closes issue #4.">Closes</span> <span class="issue-keyword tooltipped tooltipped-se" aria-label="This commit closes issue #5.">Closes</span>',
|
||||
number: 9,
|
||||
labels: {
|
||||
pageInfo: { hasNextPage: false },
|
||||
nodes: [{ name: 'label1' }, { name: 'label2' }],
|
||||
},
|
||||
timelineItems: {
|
||||
pageInfo: { hasNextPage: false },
|
||||
nodes: [
|
||||
{
|
||||
isCrossRepository: true,
|
||||
__typename: 'ConnectedEvent',
|
||||
subject: { number: 1 },
|
||||
},
|
||||
{
|
||||
isCrossRepository: false,
|
||||
__typename: 'ConnectedEvent',
|
||||
subject: { number: 2 },
|
||||
},
|
||||
{
|
||||
isCrossRepository: false,
|
||||
__typename: 'DisconnectedEvent',
|
||||
subject: { number: 2 },
|
||||
},
|
||||
{
|
||||
isCrossRepository: false,
|
||||
__typename: 'ConnectedEvent',
|
||||
subject: { number: 2 },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
node: {
|
||||
bodyHTML: '',
|
||||
number: 42,
|
||||
labels: {
|
||||
pageInfo: { hasNextPage: false },
|
||||
nodes: [{ name: 'label1' }, { name: 'skip' }],
|
||||
},
|
||||
timelineItems: {
|
||||
pageInfo: { hasNextPage: false },
|
||||
nodes: [
|
||||
{
|
||||
isCrossRepository: true,
|
||||
__typename: 'ConnectedEvent',
|
||||
subject: { number: 82 },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}),
|
||||
),
|
||||
}
|
||||
|
||||
jest.isolateModules(() => {
|
||||
require('./index')
|
||||
})
|
||||
|
||||
await new Promise<void>(setImmediate)
|
||||
|
||||
expect(mockOctokit).toMatchSnapshot()
|
||||
expect(mockOctokit.rest.issues.createComment).toHaveBeenCalledTimes(3)
|
||||
})
|
||||
|
||||
describe('can filter tags', () => {
|
||||
const v3prev = 'v3.0.1'
|
||||
const v3current = 'v3.0.2'
|
||||
const v2prev = 'v2.0.1'
|
||||
const v2current = 'v2.0.2'
|
||||
|
||||
const listReleasesData = [
|
||||
{
|
||||
name: 'Current Release Name',
|
||||
tag_name: v3current,
|
||||
html_url: 'http://v3.0.2',
|
||||
},
|
||||
{
|
||||
name: 'Prev Release Name',
|
||||
tag_name: v3prev,
|
||||
html_url: 'http://v3.0.1',
|
||||
},
|
||||
{
|
||||
name: 'v2 Current Release Name',
|
||||
tag_name: v2current,
|
||||
html_url: 'http://v2.0.2',
|
||||
},
|
||||
{
|
||||
name: 'v2 Prev Release Name',
|
||||
tag_name: v2prev,
|
||||
html_url: 'http://v2.0.1',
|
||||
},
|
||||
]
|
||||
|
||||
it.each`
|
||||
description | prevTag | currentTag | filter
|
||||
${'no filter'} | ${v3prev} | ${v3current} | ${null}
|
||||
${'v3'} | ${v3prev} | ${v3current} | ${'v\\d'}
|
||||
${'v2'} | ${v2prev} | ${v2current} | ${'v\\d'}
|
||||
`('should filter tags with $description', async ({ prevTag, currentTag, filter }) => {
|
||||
// @ts-ignore
|
||||
github.context.payload.release.tag_name = currentTag
|
||||
|
||||
tagFilter = filter
|
||||
|
||||
mockOctokit = {
|
||||
...simpleMockOctokit,
|
||||
rest: {
|
||||
issues: {
|
||||
get: jest.fn(() => Promise.resolve({ data: { locked: false } })),
|
||||
createComment: jest.fn(() => Promise.resolve()),
|
||||
addLabels: jest.fn(() => Promise.resolve()),
|
||||
},
|
||||
repos: {
|
||||
listReleases: jest.fn(() =>
|
||||
Promise.resolve({
|
||||
data: listReleasesData,
|
||||
}),
|
||||
),
|
||||
compareCommits: jest.fn(() =>
|
||||
Promise.resolve({
|
||||
data: { commits: [{ sha: 'SHA1' }] },
|
||||
}),
|
||||
),
|
||||
},
|
||||
},
|
||||
graphql: jest.fn(() =>
|
||||
Promise.resolve({
|
||||
resource: {
|
||||
messageHeadlineHTML: '',
|
||||
messageBodyHTML:
|
||||
'<span class="issue-keyword tooltipped tooltipped-se" aria-label="This commit closes issue #123.">Closes</span> <p><span class="issue-keyword tooltipped tooltipped-se" aria-label="This pull request closes issue #7.">Closes</span>',
|
||||
associatedPullRequests: {
|
||||
pageInfo: { hasNextPage: false },
|
||||
edges: [],
|
||||
},
|
||||
},
|
||||
}),
|
||||
),
|
||||
}
|
||||
|
||||
jest.isolateModules(() => {
|
||||
require('./index')
|
||||
})
|
||||
|
||||
await new Promise<void>((resolve) => setImmediate(() => resolve()))
|
||||
|
||||
expect(github.getOctokit).toHaveBeenCalled()
|
||||
expect(mockOctokit.rest.repos.compareCommits.mock.calls).toEqual([
|
||||
[{ base: prevTag, head: currentTag }],
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('feature tests', () => {
|
||||
beforeEach(() => {
|
||||
mockOctokit = simpleMockOctokit
|
||||
})
|
||||
|
||||
it('can disable comments', async () => {
|
||||
commentTempate = ''
|
||||
|
||||
jest.isolateModules(() => {
|
||||
require('./index')
|
||||
})
|
||||
|
||||
await new Promise<void>((resolve) => setImmediate(() => resolve()))
|
||||
|
||||
expect(github.getOctokit).toHaveBeenCalled()
|
||||
expect(mockOctokit.rest.issues.createComment).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should unlock and comment', async () => {
|
||||
mockOctokit = {
|
||||
...simpleMockOctokit,
|
||||
rest: {
|
||||
...simpleMockOctokit.rest,
|
||||
issues: {
|
||||
// Return locked for both issues to be commented on
|
||||
get: jest.fn(() => Promise.resolve({ data: { locked: true } })),
|
||||
lock: jest.fn(() => Promise.resolve()),
|
||||
unlock: jest.fn(() => Promise.resolve()),
|
||||
createComment: jest.fn(() => Promise.resolve()),
|
||||
},
|
||||
},
|
||||
graphql: jest.fn(() =>
|
||||
Promise.resolve({
|
||||
resource: {
|
||||
messageHeadlineHTML: '',
|
||||
messageBodyHTML:
|
||||
'<span class="issue-keyword tooltipped tooltipped-se" aria-label="This commit closes issue #123.">Closes</span> <p><span class="issue-keyword tooltipped tooltipped-se" aria-label="This pull request closes issue #7.">Closes</span>',
|
||||
associatedPullRequests: {
|
||||
pageInfo: { hasNextPage: false },
|
||||
edges: [],
|
||||
},
|
||||
},
|
||||
}),
|
||||
),
|
||||
}
|
||||
|
||||
jest.isolateModules(() => {
|
||||
require('./index')
|
||||
})
|
||||
|
||||
await new Promise<void>((resolve) => setImmediate(() => resolve()))
|
||||
|
||||
expect(github.getOctokit).toHaveBeenCalled()
|
||||
|
||||
// Should call once for both linked issues
|
||||
expect(mockOctokit.rest.issues.unlock).toHaveBeenCalledTimes(2)
|
||||
expect(mockOctokit.rest.issues.createComment).toHaveBeenCalledTimes(2)
|
||||
expect(mockOctokit.rest.issues.lock).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it.skip('can apply labels', async () => {
|
||||
labelTemplate = ':dart: landed,release-{release_tag},{release_name}'
|
||||
|
||||
jest.isolateModules(() => {
|
||||
require('./index')
|
||||
})
|
||||
|
||||
await new Promise<void>((resolve) => setImmediate(() => resolve()))
|
||||
|
||||
expect(github.getOctokit).toHaveBeenCalled()
|
||||
expect(mockOctokit.rest.issues.addLabels.mock.calls).toMatchSnapshot()
|
||||
})
|
||||
})
|
||||
})
|
||||
349
.github/actions/release-commenter/src/index.ts
vendored
349
.github/actions/release-commenter/src/index.ts
vendored
@@ -1,349 +0,0 @@
|
||||
import * as core from '@actions/core'
|
||||
import * as github from '@actions/github'
|
||||
import type * as Webhooks from '@octokit/webhooks-types'
|
||||
|
||||
const closesMatcher = /aria-label="This (?:commit|pull request) closes issue #(\d+)\."/g
|
||||
|
||||
const releaseLinkTemplateRegex = /{release_link}/g
|
||||
const releaseNameTemplateRegex = /{release_name}/g
|
||||
const releaseTagTemplateRegex = /{release_tag}/g
|
||||
|
||||
;(async function main() {
|
||||
try {
|
||||
const payload = github.context.payload as Webhooks.EventPayloadMap['release']
|
||||
|
||||
const githubToken = core.getInput('GITHUB_TOKEN')
|
||||
const tagFilter = core.getInput('tag-filter') || undefined // Accept tag filter as an input
|
||||
const octokit = github.getOctokit(githubToken)
|
||||
|
||||
const commentTemplate = core.getInput('comment-template')
|
||||
const labelTemplate = core.getInput('label-template') || null
|
||||
const skipLabelTemplate = core.getInput('skip-label') || null
|
||||
|
||||
// Fetch the releases with the optional tag filter applied
|
||||
const { data: rawReleases } = await octokit.rest.repos.listReleases({
|
||||
...github.context.repo,
|
||||
per_page: 100,
|
||||
})
|
||||
|
||||
// Get the current release tag or latest tag
|
||||
const currentTag = payload?.release?.tag_name || rawReleases?.[0]?.tag_name
|
||||
|
||||
let releases = rawReleases
|
||||
|
||||
// Filter releases by the tag filter if provided
|
||||
if (tagFilter) {
|
||||
core.info(`Filtering releases by tag filter: ${tagFilter}`)
|
||||
// Get the matching part of the current release tag
|
||||
const regexMatch = currentTag.match(tagFilter)?.[0]
|
||||
if (!regexMatch) {
|
||||
core.error(`Current release tag ${currentTag} does not match the tag filter ${tagFilter}`)
|
||||
return
|
||||
}
|
||||
|
||||
core.info(`Matched string from filter: ${regexMatch}`)
|
||||
|
||||
releases = releases
|
||||
.filter((release) => {
|
||||
const match = release.tag_name.match(regexMatch)?.[0]
|
||||
return match
|
||||
})
|
||||
.slice(0, 2)
|
||||
}
|
||||
|
||||
core.info(`Releases: ${JSON.stringify(releases, null, 2)}`)
|
||||
|
||||
if (releases.length < 2) {
|
||||
if (!releases.length) {
|
||||
core.error(`No releases found with the provided tag filter: '${tagFilter}'`)
|
||||
return
|
||||
}
|
||||
|
||||
core.info('first release')
|
||||
return
|
||||
}
|
||||
|
||||
const [currentRelease, priorRelease] = releases
|
||||
|
||||
core.info(`${priorRelease.tag_name}...${currentRelease.tag_name}`)
|
||||
|
||||
const {
|
||||
data: { commits },
|
||||
} = await octokit.rest.repos.compareCommits({
|
||||
...github.context.repo,
|
||||
base: priorRelease.tag_name,
|
||||
head: currentRelease.tag_name,
|
||||
})
|
||||
|
||||
if (!currentRelease.name) {
|
||||
core.info('Current release has no name, will fall back to the tag name.')
|
||||
}
|
||||
const releaseLabel = currentRelease.name || currentRelease.tag_name
|
||||
|
||||
const comment = commentTemplate
|
||||
.trim()
|
||||
.split(releaseLinkTemplateRegex)
|
||||
.join(`[${releaseLabel}](${currentRelease.html_url})`)
|
||||
.split(releaseNameTemplateRegex)
|
||||
.join(releaseLabel)
|
||||
.split(releaseTagTemplateRegex)
|
||||
.join(currentRelease.tag_name)
|
||||
|
||||
const parseLabels = (rawInput: string | null) =>
|
||||
rawInput
|
||||
?.split(releaseNameTemplateRegex)
|
||||
.join(releaseLabel)
|
||||
?.split(releaseTagTemplateRegex)
|
||||
.join(currentRelease.tag_name)
|
||||
?.split(',')
|
||||
?.map((l) => l.trim())
|
||||
.filter((l) => l)
|
||||
|
||||
const labels = parseLabels(labelTemplate)
|
||||
const skipLabels = parseLabels(skipLabelTemplate)
|
||||
|
||||
const linkedIssuesPrs = new Set<number>()
|
||||
|
||||
await Promise.all(
|
||||
commits.map((commit) =>
|
||||
(async () => {
|
||||
const query = `
|
||||
{
|
||||
resource(url: "${payload.repository.html_url}/commit/${commit.sha}") {
|
||||
... on Commit {
|
||||
messageHeadlineHTML
|
||||
messageBodyHTML
|
||||
associatedPullRequests(first: 10) {
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
}
|
||||
edges {
|
||||
node {
|
||||
bodyHTML
|
||||
number
|
||||
state
|
||||
labels(first: 10) {
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
}
|
||||
nodes {
|
||||
name
|
||||
}
|
||||
}
|
||||
timelineItems(itemTypes: [CONNECTED_EVENT, DISCONNECTED_EVENT], first: 100) {
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
}
|
||||
nodes {
|
||||
... on ConnectedEvent {
|
||||
__typename
|
||||
isCrossRepository
|
||||
subject {
|
||||
... on Issue {
|
||||
number
|
||||
}
|
||||
}
|
||||
}
|
||||
... on DisconnectedEvent {
|
||||
__typename
|
||||
isCrossRepository
|
||||
subject {
|
||||
... on Issue {
|
||||
number
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
const response: {
|
||||
resource: null | {
|
||||
messageHeadlineHTML: string
|
||||
messageBodyHTML: string
|
||||
associatedPullRequests: {
|
||||
pageInfo: { hasNextPage: boolean }
|
||||
edges: ReadonlyArray<{
|
||||
node: {
|
||||
bodyHTML: string
|
||||
number: number
|
||||
state: 'OPEN' | 'CLOSED' | 'MERGED'
|
||||
labels: {
|
||||
pageInfo: { hasNextPage: boolean }
|
||||
nodes: ReadonlyArray<{
|
||||
name: string
|
||||
}>
|
||||
}
|
||||
timelineItems: {
|
||||
pageInfo: { hasNextPage: boolean }
|
||||
nodes: ReadonlyArray<{
|
||||
__typename: 'ConnectedEvent' | 'DisconnectedEvent'
|
||||
isCrossRepository: boolean
|
||||
subject: {
|
||||
number: number
|
||||
}
|
||||
}>
|
||||
}
|
||||
}
|
||||
}>
|
||||
}
|
||||
}
|
||||
} = await octokit.graphql(query)
|
||||
|
||||
if (!response.resource) {
|
||||
return
|
||||
}
|
||||
|
||||
// core.info(JSON.stringify(response.resource, null, 2))
|
||||
|
||||
core.info(`Checking commit: ${payload.repository.html_url}/commit/${commit.sha}`)
|
||||
|
||||
const associatedClosedPREdges = response.resource.associatedPullRequests.edges.filter(
|
||||
(e) => e.node.state === 'MERGED',
|
||||
)
|
||||
|
||||
if (associatedClosedPREdges.length) {
|
||||
core.info(
|
||||
` Associated Merged PRs:\n ${associatedClosedPREdges.map((pr) => `${payload.repository.html_url}/pull/${pr.node.number}`).join('\n ')}`,
|
||||
)
|
||||
} else {
|
||||
core.info(' No associated merged PRs')
|
||||
}
|
||||
|
||||
const html = [
|
||||
response.resource.messageHeadlineHTML,
|
||||
response.resource.messageBodyHTML,
|
||||
...associatedClosedPREdges.map((pr) => pr.node.bodyHTML),
|
||||
].join(' ')
|
||||
|
||||
for (const match of html.matchAll(closesMatcher)) {
|
||||
const [, num] = match
|
||||
linkedIssuesPrs.add(parseInt(num, 10))
|
||||
core.info(
|
||||
` Linked issue/PR from closesMatcher: ${payload.repository.html_url}/pull/${num}`,
|
||||
)
|
||||
}
|
||||
|
||||
if (response.resource.associatedPullRequests.pageInfo.hasNextPage) {
|
||||
core.warning(`Too many PRs associated with ${commit.sha}`)
|
||||
}
|
||||
|
||||
const seen = new Set<number>()
|
||||
for (const associatedPR of associatedClosedPREdges) {
|
||||
if (associatedPR.node.timelineItems.pageInfo.hasNextPage) {
|
||||
core.warning(`Too many links for #${associatedPR.node.number}`)
|
||||
}
|
||||
if (associatedPR.node.labels.pageInfo.hasNextPage) {
|
||||
core.warning(`Too many labels for #${associatedPR.node.number}`)
|
||||
}
|
||||
// a skip labels is present on this PR
|
||||
if (
|
||||
skipLabels?.some((l) => associatedPR.node.labels.nodes.some(({ name }) => name === l))
|
||||
) {
|
||||
continue
|
||||
}
|
||||
|
||||
linkedIssuesPrs.add(associatedPR.node.number)
|
||||
core.info(
|
||||
` Linked issue/PR from associated PR: ${payload.repository.html_url}/pull/${associatedPR.node.number}`,
|
||||
)
|
||||
|
||||
// These are sorted by creation date in ascending order. The latest event for a given issue/PR is all we need
|
||||
// ignore links that aren't part of this repo
|
||||
const links = associatedPR.node.timelineItems.nodes
|
||||
.filter((node) => !node.isCrossRepository)
|
||||
.reverse()
|
||||
for (const link of links) {
|
||||
if (seen.has(link.subject.number)) {
|
||||
continue
|
||||
}
|
||||
if (link.__typename == 'ConnectedEvent') {
|
||||
linkedIssuesPrs.add(link.subject.number)
|
||||
core.info(
|
||||
`Linked issue/PR from connected event: ${payload.repository.html_url}/pull/${link.subject.number}`,
|
||||
)
|
||||
}
|
||||
seen.add(link.subject.number)
|
||||
}
|
||||
}
|
||||
})(),
|
||||
),
|
||||
)
|
||||
|
||||
core.info(
|
||||
`Final issues/PRs to be commented on: \n${Array.from(linkedIssuesPrs)
|
||||
.map((num) => ` ${payload.repository.html_url}/pull/${num}`)
|
||||
.join('\n')}`,
|
||||
)
|
||||
|
||||
const requests: Array<Promise<unknown>> = []
|
||||
for (const issueNumber of linkedIssuesPrs) {
|
||||
const baseRequest = {
|
||||
...github.context.repo,
|
||||
issue_number: issueNumber,
|
||||
}
|
||||
if (comment) {
|
||||
const commentRequest = {
|
||||
...baseRequest,
|
||||
body: comment,
|
||||
}
|
||||
|
||||
// Check if issue is locked or not
|
||||
const { data: issue } = await octokit.rest.issues.get(baseRequest)
|
||||
|
||||
let createCommentPromise: () => Promise<void>
|
||||
if (!issue.locked) {
|
||||
createCommentPromise = async () => {
|
||||
try {
|
||||
await octokit.rest.issues.createComment(commentRequest)
|
||||
} catch (error) {
|
||||
core.error(error as Error)
|
||||
core.error(
|
||||
`Failed to comment on issue/PR: ${issueNumber}. ${payload.repository.html_url}/pull/${issueNumber}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
core.info(
|
||||
`Issue/PR is locked: ${issueNumber}. Unlocking, commenting, and re-locking. ${payload.repository.html_url}/pull/${issueNumber}`,
|
||||
)
|
||||
createCommentPromise = async () => {
|
||||
try {
|
||||
core.debug(`Unlocking issue/PR: ${issueNumber}`)
|
||||
await octokit.rest.issues.unlock(baseRequest)
|
||||
core.debug(`Commenting on issue/PR: ${issueNumber}`)
|
||||
await octokit.rest.issues.createComment(commentRequest)
|
||||
core.debug(`Re-locking issue/PR: ${issueNumber}`)
|
||||
await octokit.rest.issues.lock(baseRequest)
|
||||
} catch (error) {
|
||||
core.error(error as Error)
|
||||
core.error(
|
||||
`Failed to unlock, comment, and re-lock issue/PR: ${issueNumber}. ${payload.repository.html_url}/pull/${issueNumber}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
requests.push(createCommentPromise())
|
||||
}
|
||||
if (labels) {
|
||||
const request = {
|
||||
...baseRequest,
|
||||
labels,
|
||||
}
|
||||
// core.info(JSON.stringify(request, null, 2))
|
||||
requests.push(octokit.rest.issues.addLabels(request))
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all(requests)
|
||||
} catch (error) {
|
||||
core.error(error as Error)
|
||||
core.setFailed((error as Error).message)
|
||||
}
|
||||
})()
|
||||
15
.github/actions/release-commenter/tsconfig.json
vendored
15
.github/actions/release-commenter/tsconfig.json
vendored
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": ["es2020.string"],
|
||||
"noEmit": true,
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"downlevelIteration": true,
|
||||
"skipLibCheck": true,
|
||||
},
|
||||
"exclude": ["src/**/*.test.ts"]
|
||||
}
|
||||
52
.github/actions/setup/action.yml
vendored
52
.github/actions/setup/action.yml
vendored
@@ -1,33 +1,15 @@
|
||||
name: Setup node and pnpm
|
||||
description: |
|
||||
Configures Node, pnpm, cache, performs pnpm install
|
||||
description: Configure the Node.js and pnpm versions
|
||||
|
||||
inputs:
|
||||
node-version:
|
||||
description: Node.js version
|
||||
description: 'The Node.js version to use'
|
||||
required: true
|
||||
default: 22.6.0
|
||||
default: 18.20.2
|
||||
pnpm-version:
|
||||
description: Pnpm version
|
||||
description: 'The pnpm version to use'
|
||||
required: true
|
||||
default: 9.7.1
|
||||
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') }}
|
||||
|
||||
outputs:
|
||||
pnpm-store-path:
|
||||
description: The resolved pnpm store path
|
||||
pnpm-install-cache-key:
|
||||
description: The cache key used for pnpm install cache
|
||||
default: 8.15.7
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
@@ -43,34 +25,24 @@ runs:
|
||||
node-version: ${{ inputs.node-version }}
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: ${{ inputs.pnpm-version }}
|
||||
run_install: false
|
||||
|
||||
- name: Get pnpm store path
|
||||
- name: Get pnpm store directory
|
||||
shell: bash
|
||||
run: |
|
||||
STORE_PATH=$(pnpm store path --silent)
|
||||
echo "STORE_PATH=$STORE_PATH" >> $GITHUB_ENV
|
||||
echo "Pnpm store path resolved to: $STORE_PATH"
|
||||
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
|
||||
|
||||
- name: Restore pnpm install cache
|
||||
if: ${{ inputs.pnpm-restore-cache == 'true' }}
|
||||
- name: Setup pnpm cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ${{ env.STORE_PATH }}
|
||||
key: ${{ inputs.pnpm-install-cache-key }}
|
||||
key: pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
pnpm-store-${{ inputs.pnpm-version }}-
|
||||
pnpm-store-
|
||||
pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
|
||||
- name: Run pnpm install
|
||||
if: ${{ inputs.pnpm-run-install == 'true' }}
|
||||
shell: bash
|
||||
- shell: bash
|
||||
run: pnpm install
|
||||
|
||||
# Set the cache key output
|
||||
- run: |
|
||||
echo "pnpm-install-cache-key=${{ inputs.pnpm-install-cache-key }}" >> $GITHUB_ENV
|
||||
shell: bash
|
||||
|
||||
22
.github/actions/triage/LICENSE
vendored
22
.github/actions/triage/LICENSE
vendored
@@ -1,22 +0,0 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 Payload <info@payloadcms.com>. All modification and additions are copyright of Payload.
|
||||
|
||||
---
|
||||
|
||||
Original license:
|
||||
ISC License
|
||||
|
||||
Copyright (c) 2023, Balázs Orbán
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
21
.github/actions/triage/README.md
vendored
21
.github/actions/triage/README.md
vendored
@@ -1,21 +0,0 @@
|
||||
# Triage
|
||||
|
||||
Modified version of https://github.com/balazsorban44/nissuer
|
||||
|
||||
## Modifications
|
||||
|
||||
- Port to TypeScript
|
||||
- Remove issue locking
|
||||
- Remove reproduction blocklist
|
||||
- Uses `@vercel/ncc` for packaging
|
||||
|
||||
## Development
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Whenever a modification is made to the action, the action built to `dist` must be committed to the repository.
|
||||
|
||||
This is done by running:
|
||||
|
||||
```sh
|
||||
pnpm build
|
||||
```
|
||||
40
.github/actions/triage/action.yml
vendored
40
.github/actions/triage/action.yml
vendored
@@ -1,40 +0,0 @@
|
||||
name: Triage
|
||||
description: Initial triage for issues
|
||||
|
||||
inputs:
|
||||
reproduction-comment:
|
||||
description: 'Either a string or a path to a .md file inside the repository. Example: ".github/invalid-reproduction.md"'
|
||||
default: '.github/invalid-reproduction.md'
|
||||
reproduction-hosts:
|
||||
description: 'Comma-separated list of hostnames that are allowed for reproductions. Example: "github.com,codesandbox.io"'
|
||||
default: github.com
|
||||
reproduction-invalid-label:
|
||||
description: 'Label to apply to issues without a valid reproduction. Example: "invalid-reproduction"'
|
||||
default: 'invalid-reproduction'
|
||||
reproduction-issue-labels:
|
||||
description: 'Comma-separated list of issue labels. If configured, only verify reproduction URLs of issues with one of these labels present. Adding a comma at the end will handle non-labeled issues as invalid. Example: "bug,", will consider issues with the label "bug" or no label.'
|
||||
default: ''
|
||||
reproduction-link-section:
|
||||
description: 'A regular expression string with "(.*)" matching a valid URL in the issue body. The result is trimmed. Example: "### Link to reproduction(.*)### To reproduce"'
|
||||
default: '### Link to reproduction(.*)### To reproduce'
|
||||
actions-to-perform:
|
||||
description: 'Comma-separated list of actions to perform on the issue. Example: "tag,comment,close"'
|
||||
default: 'tag,comment,close'
|
||||
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: Checkout code
|
||||
if: ${{ github.event_name != 'pull_request' }}
|
||||
uses: actions/checkout@v4
|
||||
- name: Run action
|
||||
run: node ${{ github.action_path }}/dist/index.js
|
||||
shell: sh
|
||||
# https://github.com/actions/runner/issues/665#issuecomment-676581170
|
||||
env:
|
||||
'INPUT_REPRODUCTION_COMMENT': ${{inputs.reproduction-comment}}
|
||||
'INPUT_REPRODUCTION_HOSTS': ${{inputs.reproduction-hosts}}
|
||||
'INPUT_REPRODUCTION_INVALID_LABEL': ${{inputs.reproduction-invalid-label}}
|
||||
'INPUT_REPRODUCTION_ISSUE_LABELS': ${{inputs.reproduction-issue-labels}}
|
||||
'INPUT_REPRODUCTION_LINK_SECTION': ${{inputs.reproduction-link-section}}
|
||||
'INPUT_ACTIONS_TO_PERFORM': ${{inputs.actions-to-perform}}
|
||||
34068
.github/actions/triage/dist/index.js
vendored
34068
.github/actions/triage/dist/index.js
vendored
File diff suppressed because one or more lines are too long
7
.github/actions/triage/jest.config.js
vendored
7
.github/actions/triage/jest.config.js
vendored
@@ -1,7 +0,0 @@
|
||||
module.exports = {
|
||||
testEnvironment: 'node',
|
||||
testPathIgnorePatterns: ['/node_modules/', '<rootDir>/dist/'],
|
||||
transform: {
|
||||
'^.+\\.(t|j)sx?$': ['@swc/jest'],
|
||||
},
|
||||
}
|
||||
33
.github/actions/triage/package.json
vendored
33
.github/actions/triage/package.json
vendored
@@ -1,33 +0,0 @@
|
||||
{
|
||||
"name": "triage",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"description": "GitHub Action to triage new issues",
|
||||
"license": "MIT",
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
"build": "pnpm build:typecheck && pnpm build:ncc",
|
||||
"build:ncc": "ncc build src/index.ts -t -o dist",
|
||||
"build:typecheck": "tsc",
|
||||
"clean": "rimraf dist",
|
||||
"test": "jest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@actions/core": "^1.3.0",
|
||||
"@actions/github": "^5.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@octokit/webhooks-types": "^7.5.1",
|
||||
"@swc/jest": "^0.2.37",
|
||||
"@types/jest": "^27.5.2",
|
||||
"@types/node": "^20.16.5",
|
||||
"@typescript-eslint/eslint-plugin": "^4.33.0",
|
||||
"@typescript-eslint/parser": "^4.33.0",
|
||||
"@vercel/ncc": "0.38.1",
|
||||
"concurrently": "^8.2.2",
|
||||
"eslint": "^7.32.0",
|
||||
"jest": "^29.7.0",
|
||||
"prettier": "^3.3.3",
|
||||
"typescript": "^4.9.5"
|
||||
}
|
||||
}
|
||||
4241
.github/actions/triage/pnpm-lock.yaml
generated
vendored
4241
.github/actions/triage/pnpm-lock.yaml
generated
vendored
File diff suppressed because it is too large
Load Diff
215
.github/actions/triage/src/index.ts
vendored
215
.github/actions/triage/src/index.ts
vendored
@@ -1,215 +0,0 @@
|
||||
import { debug, error, getBooleanInput, getInput, info, setFailed } from '@actions/core'
|
||||
|
||||
import { context, getOctokit } from '@actions/github'
|
||||
import { readFile, access } from 'node:fs/promises'
|
||||
import { join } from 'node:path'
|
||||
|
||||
// Ensure GITHUB_TOKEN and GITHUB_WORKSPACE are present
|
||||
if (!process.env.GITHUB_TOKEN) throw new TypeError('No GITHUB_TOKEN provided')
|
||||
if (!process.env.GITHUB_WORKSPACE) throw new TypeError('Not a GitHub workspace')
|
||||
|
||||
const validActionsToPerform = ['tag', 'comment', 'close'] as const
|
||||
type ActionsToPerform = (typeof validActionsToPerform)[number]
|
||||
|
||||
// Define the configuration object
|
||||
interface Config {
|
||||
invalidLink: {
|
||||
comment: string
|
||||
bugLabels: string[]
|
||||
hosts: string[]
|
||||
label: string
|
||||
linkSection: string
|
||||
}
|
||||
actionsToPerform: ActionsToPerform[]
|
||||
token: string
|
||||
workspace: string
|
||||
}
|
||||
|
||||
const config: Config = {
|
||||
invalidLink: {
|
||||
comment: getInput('reproduction_comment') || '.github/invalid-reproduction.md',
|
||||
bugLabels: getInput('reproduction_issue_labels')
|
||||
.split(',')
|
||||
.map((l) => l.trim()),
|
||||
hosts: (getInput('reproduction_hosts') || 'github.com').split(',').map((h) => h.trim()),
|
||||
label: getInput('reproduction_invalid_label') || 'invalid-reproduction',
|
||||
linkSection:
|
||||
getInput('reproduction_link_section') || '### Link to reproduction(.*)### To reproduce',
|
||||
},
|
||||
actionsToPerform: (getInput('actions_to_perform') || validActionsToPerform.join(','))
|
||||
.split(',')
|
||||
.map((a) => {
|
||||
const action = a.trim().toLowerCase() as ActionsToPerform
|
||||
if (validActionsToPerform.includes(action)) {
|
||||
return action
|
||||
}
|
||||
|
||||
throw new TypeError(`Invalid action: ${action}`)
|
||||
}),
|
||||
token: process.env.GITHUB_TOKEN,
|
||||
workspace: process.env.GITHUB_WORKSPACE,
|
||||
}
|
||||
|
||||
// Attempt to parse JSON, return parsed object or error
|
||||
function tryParse(json: string): Record<string, unknown> {
|
||||
try {
|
||||
return JSON.parse(json)
|
||||
} catch (e) {
|
||||
setFailed(`Could not parse JSON: ${e instanceof Error ? e.message : e}`)
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
// Retrieves a boolean input or undefined based on environment variables
|
||||
function getBooleanOrUndefined(value: string): boolean | undefined {
|
||||
const variable = process.env[`INPUT_${value.toUpperCase()}`]
|
||||
return variable === undefined || variable === '' ? undefined : getBooleanInput(value)
|
||||
}
|
||||
|
||||
// Returns the appropriate label match type
|
||||
function getLabelMatch(value: string | undefined): 'name' | 'description' {
|
||||
return value === 'name' ? 'name' : 'description'
|
||||
}
|
||||
|
||||
// Function to check if an issue contains a valid reproduction link
|
||||
async function checkValidReproduction(): Promise<void> {
|
||||
const { issue, action } = context.payload as {
|
||||
issue: { number: number; body: string; labels: { name: string }[] } | undefined
|
||||
action: string
|
||||
}
|
||||
|
||||
if (action !== 'opened' || !issue?.body) return
|
||||
|
||||
const labels = issue.labels.map((l) => l.name)
|
||||
|
||||
const issueMatchingLabel =
|
||||
labels.length &&
|
||||
config.invalidLink.bugLabels.length &&
|
||||
labels.some((l) => config.invalidLink.bugLabels.includes(l))
|
||||
|
||||
if (!issueMatchingLabel) {
|
||||
info(
|
||||
`Issue #${issue.number} does not match required labels: ${config.invalidLink.bugLabels.join(', ')}`,
|
||||
)
|
||||
info(`Issue labels: ${labels.join(', ')}`)
|
||||
return
|
||||
}
|
||||
|
||||
info(`Issue #${issue.number} labels: ${labels.join(', ')}`)
|
||||
|
||||
const { rest: client } = getOctokit(config.token)
|
||||
const common = { ...context.repo, issue_number: issue.number }
|
||||
|
||||
const labelsToRemove = labels.filter((l) => config.invalidLink.bugLabels.includes(l))
|
||||
|
||||
if (await isValidReproduction(issue.body)) {
|
||||
await Promise.all(
|
||||
labelsToRemove.map((label) => client.issues.removeLabel({ ...common, name: label })),
|
||||
)
|
||||
|
||||
return info(`Issue #${issue.number} contains a valid reproduction 💚`)
|
||||
}
|
||||
|
||||
info(`Invalid reproduction, issue will be closed/labeled/commented...`)
|
||||
|
||||
// Adjust labels
|
||||
await Promise.all(
|
||||
labelsToRemove.map((label) => client.issues.removeLabel({ ...common, name: label })),
|
||||
)
|
||||
|
||||
// Tag
|
||||
if (config.actionsToPerform.includes('tag')) {
|
||||
info(`Added label: ${config.invalidLink.label}`)
|
||||
await client.issues.addLabels({ ...common, labels: [config.invalidLink.label] })
|
||||
} else {
|
||||
info('Tag - skipped, not provided in actions to perform')
|
||||
}
|
||||
|
||||
// Comment
|
||||
if (config.actionsToPerform.includes('comment')) {
|
||||
const comment = join(config.workspace, config.invalidLink.comment)
|
||||
await client.issues.createComment({ ...common, body: await getCommentBody(comment) })
|
||||
info(`Commented with invalid reproduction message`)
|
||||
} else {
|
||||
info('Comment - skipped, not provided in actions to perform')
|
||||
}
|
||||
|
||||
// Close
|
||||
if (config.actionsToPerform.includes('close')) {
|
||||
await client.issues.update({ ...common, state: 'closed' })
|
||||
info(`Closed issue #${issue.number}`)
|
||||
} else {
|
||||
info('Close - skipped, not provided in actions to perform')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if an issue contains a valid/accessible link to a reproduction.
|
||||
*
|
||||
* Returns `true` if the link is valid.
|
||||
* @param body - The body content of the issue
|
||||
*/
|
||||
async function isValidReproduction(body: string): Promise<boolean> {
|
||||
const linkSectionRe = new RegExp(config.invalidLink.linkSection, 'is')
|
||||
const link = body.match(linkSectionRe)?.[1]?.trim()
|
||||
|
||||
if (!link) {
|
||||
info('Missing link')
|
||||
info(`Link section regex: ${linkSectionRe}`)
|
||||
info(`Link section: ${body}`)
|
||||
return false
|
||||
}
|
||||
|
||||
info(`Checking validity of link: ${link}`)
|
||||
|
||||
if (!URL.canParse(link)) {
|
||||
info(`Invalid URL: ${link}`)
|
||||
return false
|
||||
}
|
||||
|
||||
const url = new URL(link)
|
||||
|
||||
if (!config.invalidLink.hosts.includes(url.hostname)) {
|
||||
info('Link did not match allowed reproduction hosts')
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
// Verify that the link can be accessed
|
||||
const response = await fetch(link)
|
||||
const isOk = response.status < 400 || response.status >= 500
|
||||
|
||||
info(`Link status: ${response.status}`)
|
||||
if (!isOk) {
|
||||
info(`Link returned status ${response.status}`)
|
||||
}
|
||||
return isOk
|
||||
} catch (error) {
|
||||
info(`Error fetching link: ${(error as Error).message}`)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return either a file's content or a string
|
||||
* @param {string} pathOrComment
|
||||
*/
|
||||
async function getCommentBody(pathOrComment: string) {
|
||||
try {
|
||||
await access(pathOrComment)
|
||||
return await readFile(pathOrComment, 'utf8')
|
||||
} catch (error: any) {
|
||||
if (error.code === 'ENOENT') return pathOrComment
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async function run() {
|
||||
const { token, workspace, ...safeConfig } = config
|
||||
info('Configuration:')
|
||||
info(JSON.stringify(safeConfig, null, 2))
|
||||
|
||||
await checkValidReproduction()
|
||||
}
|
||||
|
||||
run().catch(setFailed)
|
||||
15
.github/actions/triage/tsconfig.json
vendored
15
.github/actions/triage/tsconfig.json
vendored
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": ["es2020.string"],
|
||||
"noEmit": true,
|
||||
"strict": true,
|
||||
"noUnusedLocals": false, // Undo this
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"downlevelIteration": true,
|
||||
"skipLibCheck": true,
|
||||
},
|
||||
"exclude": ["src/**/*.test.ts"]
|
||||
}
|
||||
20
.github/comments/invalid-reproduction.md
vendored
20
.github/comments/invalid-reproduction.md
vendored
@@ -1,20 +0,0 @@
|
||||
**Please add a reproduction in order for us to be able to investigate.**
|
||||
|
||||
Depending on the quality of reproduction steps, this issue may be closed if no reproduction is provided.
|
||||
|
||||
### Why was this issue marked with the `invalid-reproduction` label?
|
||||
|
||||
To be able to investigate, we need access to a reproduction to identify what triggered the issue. We prefer a link to a public GitHub repository created with `create-payload-app@latest -t blank` or a forked/branched version of this repository with tests added (more info in the [reproduction-guide](https://github.com/payloadcms/payload/blob/main/.github/reproduction-guide.md)).
|
||||
|
||||
To make sure the issue is resolved as quickly as possible, please make sure that the reproduction is as **minimal** as possible. This means that you should **remove unnecessary code, files, and dependencies** that do not contribute to the issue. Ensure your reproduction does not depend on secrets, 3rd party registries, private dependencies, or any other data that cannot be made public. Avoid a reproduction including a whole monorepo (unless relevant to the issue). The easier it is to reproduce the issue, the quicker we can help.
|
||||
|
||||
Please test your reproduction against the latest version of Payload to make sure your issue has not already been fixed.
|
||||
|
||||
### I added a link, why was it still marked?
|
||||
|
||||
Ensure the link is pointing to a codebase that is accessible (e.g. not a private repository). "[example.com](http://example.com/)", "n/a", "will add later", etc. are not acceptable links -- we need to see a public codebase. See the above section for accepted links.
|
||||
|
||||
### Useful Resources
|
||||
|
||||
- [Reproduction Guide](https://github.com/payloadcms/payload/blob/main/.github/reproduction-guide.md)
|
||||
- [Contributing to Payload](https://www.youtube.com/watch?v=08Qa3ggR9rw)
|
||||
69
.github/dependabot.yml
vendored
69
.github/dependabot.yml
vendored
@@ -1,69 +0,0 @@
|
||||
# docs: https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: github-actions
|
||||
directories:
|
||||
- /
|
||||
- /.github/workflows
|
||||
- /.github/actions/* # Not working until resolved: https://github.com/dependabot/dependabot-core/issues/6345
|
||||
- /.github/actions/setup
|
||||
target-branch: main
|
||||
schedule:
|
||||
interval: monthly
|
||||
timezone: America/Detroit
|
||||
time: '06:00'
|
||||
groups:
|
||||
github_actions:
|
||||
patterns:
|
||||
- '*'
|
||||
|
||||
- package-ecosystem: npm
|
||||
directory: /
|
||||
target-branch: main
|
||||
open-pull-requests-limit: 0 # Only allow security updates
|
||||
schedule:
|
||||
interval: weekly
|
||||
day: sunday
|
||||
timezone: America/Detroit
|
||||
time: '06:00'
|
||||
commit-message:
|
||||
prefix: 'chore(deps)'
|
||||
labels:
|
||||
- dependencies
|
||||
groups:
|
||||
production-deps:
|
||||
dependency-type: production
|
||||
update-types:
|
||||
- minor
|
||||
- patch
|
||||
patterns:
|
||||
- '*'
|
||||
dev-deps:
|
||||
dependency-type: development
|
||||
update-types:
|
||||
- minor
|
||||
- patch
|
||||
patterns:
|
||||
- '*'
|
||||
|
||||
- package-ecosystem: npm
|
||||
directory: /
|
||||
target-branch: 2.x
|
||||
open-pull-requests-limit: 0 # Only allow security updates
|
||||
schedule:
|
||||
interval: weekly
|
||||
day: sunday
|
||||
timezone: America/Detroit
|
||||
time: '06:00'
|
||||
commit-message:
|
||||
prefix: 'chore(deps)'
|
||||
labels:
|
||||
- dependencies
|
||||
groups:
|
||||
production-deps:
|
||||
dependency-type: production
|
||||
update-types:
|
||||
- patch
|
||||
patterns:
|
||||
- '*'
|
||||
3876
.github/pnpm-lock.yaml
generated
vendored
3876
.github/pnpm-lock.yaml
generated
vendored
File diff suppressed because it is too large
Load Diff
2
.github/pnpm-workspace.yaml
vendored
2
.github/pnpm-workspace.yaml
vendored
@@ -1,2 +0,0 @@
|
||||
packages:
|
||||
- 'actions/*'
|
||||
4
.github/reproduction-guide.md
vendored
4
.github/reproduction-guide.md
vendored
@@ -40,7 +40,7 @@ There are a couple ways run integration tests:
|
||||
|
||||
- **Granularly** - you can run individual tests in vscode by installing the Jest Runner plugin and using that to run individual tests. Clicking the `debug` button will run the test in debug mode allowing you to set break points.
|
||||
|
||||
<img src="https://raw.githubusercontent.com/payloadcms/payload/main/packages/payload/src/assets/images/github/int-debug.png" />
|
||||
<img src="https://raw.githubusercontent.com/payloadcms/payload/main/packages/payload/src/admin/assets/images/github/int-debug.png" />
|
||||
|
||||
- **Manually** - you can run all int tests in the `/test/_community/int.spec.ts` file by running the following command:
|
||||
|
||||
@@ -57,7 +57,7 @@ The easiest way to run E2E tests is to install
|
||||
|
||||
Once they are installed you can open the `testing` tab in vscode sidebar and drill down to the test you want to run, i.e. `/test/_community/e2e.spec.ts`
|
||||
|
||||
<img src="https://raw.githubusercontent.com/payloadcms/payload/main/packages/payload/src/assets/images/github/e2e-debug.png" />
|
||||
<img src="https://raw.githubusercontent.com/payloadcms/payload/main/packages/payload/src/admin/assets/images/github/e2e-debug.png" />
|
||||
|
||||
#### Notes
|
||||
|
||||
|
||||
24
.github/workflows/dispatch-event.yml
vendored
24
.github/workflows/dispatch-event.yml
vendored
@@ -1,24 +0,0 @@
|
||||
name: dispatch-event
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
PAYLOAD_PUSH_MAIN_EVENT: payload-push-main-event
|
||||
|
||||
jobs:
|
||||
repository-dispatch:
|
||||
name: Repository dispatch
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Dispatch event
|
||||
if: ${{ github.event_name == 'workflow_dispatch' }}
|
||||
uses: peter-evans/repository-dispatch@v3
|
||||
with:
|
||||
token: ${{ secrets.PAYLOAD_REPOSITORY_DISPATCH }}
|
||||
repository: ${{ secrets.REMOTE_REPOSITORY }}
|
||||
event-type: ${{ env.PAYLOAD_PUSH_MAIN_EVENT }}
|
||||
client-payload: '{"event": {"head_commit": {"id": "${{ env.GITHUB_SHA }}"}}}' # mocked for testing
|
||||
# client-payload: '{"event": ${{ toJson(github.event) }}}'
|
||||
50
.github/workflows/label-author.yml
vendored
Normal file
50
.github/workflows/label-author.yml
vendored
Normal file
@@ -0,0 +1,50 @@
|
||||
name: label-author
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened]
|
||||
issues:
|
||||
types: [opened]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
debug-context:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: View context attributes
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: console.log(context)
|
||||
|
||||
label-created-by:
|
||||
name: Label pr/issue on opening
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Tag with 'created-by'
|
||||
uses: actions/github-script@v7
|
||||
if: github.event.action == 'opened'
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
const type = context.payload.pull_request ? 'pull_request' : 'issue';
|
||||
const association = context.payload[type].author_association;
|
||||
let label = ''
|
||||
if (association === 'MEMBER' || association === 'OWNER') {
|
||||
label = 'created-by: Payload team';
|
||||
} else if (association === 'CONTRIBUTOR') {
|
||||
label = 'created-by: Contributor';
|
||||
}
|
||||
|
||||
if (!label) return;
|
||||
|
||||
github.rest.issues.addLabels({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
labels: [label],
|
||||
});
|
||||
console.log('Added created-by: Payload team label');
|
||||
116
.github/workflows/label-on-change.yml
vendored
116
.github/workflows/label-on-change.yml
vendored
@@ -1,116 +0,0 @@
|
||||
name: label-on-change
|
||||
|
||||
on:
|
||||
# https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_target
|
||||
issues:
|
||||
types:
|
||||
- assigned
|
||||
- closed
|
||||
- labeled
|
||||
- reopened
|
||||
|
||||
# TODO: Handle labeling on comment
|
||||
|
||||
jobs:
|
||||
on-labeled-ensure-one-status:
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
issues: write
|
||||
# Only run on issue labeled and if label starts with 'status:'
|
||||
if: github.event.action == 'labeled' && startsWith(github.event.label.name, 'status:')
|
||||
steps:
|
||||
- name: Ensure only one status label
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
// Get all labels that start with 'status:' and are not the incoming label
|
||||
const incomingLabelName = context.payload.label.name;
|
||||
const labelNamesToRemove = context.payload.issue.labels
|
||||
.filter(label => label.name.startsWith('status:') && label.name !== incomingLabelName)
|
||||
.map(label => label.name);
|
||||
|
||||
if (!labelNamesToRemove.length) {
|
||||
console.log('No labels to remove');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Labels to remove: '${labelNamesToRemove}'`);
|
||||
|
||||
// If there is more than one status label, remove all but the incoming label
|
||||
for (const labelName of labelNamesToRemove) {
|
||||
await github.rest.issues.removeLabel({
|
||||
issue_number: context.issue.number,
|
||||
name: labelName,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
});
|
||||
console.log(`Removed '${labelName}' label`);
|
||||
}
|
||||
|
||||
on-issue-close:
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
issues: write
|
||||
if: github.event.action == 'closed'
|
||||
steps:
|
||||
- name: Remove all labels on issue close
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
// Get all labels that start with 'status:' and 'stale'
|
||||
const labelNamesToRemove = context.payload.issue.labels
|
||||
.filter(label => label.name.startsWith('status:') || label.name === 'stale')
|
||||
.map(label => label.name);
|
||||
|
||||
if (!labelNamesToRemove.length) {
|
||||
console.log('No labels to remove');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Labels to remove: '${labelNamesToRemove}'`);
|
||||
|
||||
for (const labelName of labelNamesToRemove) {
|
||||
await github.rest.issues.removeLabel({
|
||||
issue_number: context.issue.number,
|
||||
name: labelName,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
});
|
||||
console.log(`Removed '${labelName}' label`);
|
||||
}
|
||||
|
||||
on-issue-reopen:
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
issues: write
|
||||
if: github.event.action == 'reopened'
|
||||
steps:
|
||||
- name: Add needs-triage label on issue reopen
|
||||
uses: actions-ecosystem/action-add-labels@v1
|
||||
with:
|
||||
labels: 'status: needs-triage'
|
||||
|
||||
on-issue-assigned:
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
issues: write
|
||||
if: >
|
||||
github.event.action == 'assigned' &&
|
||||
contains(github.event.issue.labels.*.name, 'status: needs-triage')
|
||||
steps:
|
||||
- name: Remove needs-triage label on issue assign
|
||||
uses: actions-ecosystem/action-remove-labels@v1
|
||||
with:
|
||||
labels: 'status: needs-triage'
|
||||
|
||||
# on-pr-merge:
|
||||
# runs-on: ubuntu-24.04
|
||||
# if: github.event.pull_request.merged == true
|
||||
# steps:
|
||||
|
||||
# on-pr-close:
|
||||
# runs-on: ubuntu-24.04
|
||||
# if: github.event_name == 'pull_request_target' && github.event.pull_request.merged == false
|
||||
# steps:
|
||||
26
.github/workflows/lock-issues.yml
vendored
26
.github/workflows/lock-issues.yml
vendored
@@ -1,26 +0,0 @@
|
||||
name: lock-issues
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Run nightly at 12am EST
|
||||
- cron: '0 4 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
lock_issues:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Lock issues
|
||||
uses: dessant/lock-threads@v5
|
||||
with:
|
||||
process-only: 'issues'
|
||||
issue-inactive-days: '7'
|
||||
exclude-any-issue-labels: 'status: awaiting-reply'
|
||||
log-output: true
|
||||
issue-comment: >
|
||||
This issue has been automatically locked.
|
||||
|
||||
Please open a new issue if this issue persists with any additional detail.
|
||||
467
.github/workflows/main.yml
vendored
467
.github/workflows/main.yml
vendored
@@ -9,6 +9,7 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- beta
|
||||
|
||||
concurrency:
|
||||
# <workflow_name>-<branch_name>-<true || commit_sha if branch is protected>
|
||||
@@ -16,19 +17,18 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
NODE_VERSION: 22.6.0
|
||||
PNPM_VERSION: 9.7.1
|
||||
NODE_VERSION: 18.20.2
|
||||
PNPM_VERSION: 8.15.7
|
||||
DO_NOT_TRACK: 1 # Disable Turbopack telemetry
|
||||
NEXT_TELEMETRY_DISABLED: 1 # Disable Next telemetry
|
||||
|
||||
jobs:
|
||||
changes:
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pull-requests: read
|
||||
outputs:
|
||||
needs_build: ${{ steps.filter.outputs.needs_build }}
|
||||
needs_tests: ${{ steps.filter.outputs.needs_tests }}
|
||||
templates: ${{ steps.filter.outputs.templates }}
|
||||
steps:
|
||||
# https://github.com/actions/virtual-environments/issues/1187
|
||||
@@ -36,6 +36,8 @@ jobs:
|
||||
run: sudo ethtool -K eth0 tx off rx off
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 25
|
||||
- uses: dorny/paths-filter@v3
|
||||
id: filter
|
||||
with:
|
||||
@@ -46,41 +48,53 @@ jobs:
|
||||
- 'test/**'
|
||||
- 'pnpm-lock.yaml'
|
||||
- 'package.json'
|
||||
- 'templates/**'
|
||||
needs_tests:
|
||||
- '.github/workflows/**'
|
||||
- 'packages/**'
|
||||
- 'test/**'
|
||||
- 'pnpm-lock.yaml'
|
||||
- 'package.json'
|
||||
templates:
|
||||
- 'templates/**'
|
||||
- name: Log all filter results
|
||||
run: |
|
||||
echo "needs_build: ${{ steps.filter.outputs.needs_build }}"
|
||||
echo "needs_tests: ${{ steps.filter.outputs.needs_tests }}"
|
||||
echo "templates: ${{ steps.filter.outputs.templates }}"
|
||||
|
||||
lint:
|
||||
# Follows same github's ci skip: [skip lint], [lint skip], [no lint]
|
||||
if: >
|
||||
github.event_name == 'pull_request' &&
|
||||
!contains(github.event.pull_request.title, '[skip lint]') &&
|
||||
!contains(github.event.pull_request.title, '[lint skip]') &&
|
||||
!contains(github.event.pull_request.title, '[no lint]')
|
||||
runs-on: ubuntu-24.04
|
||||
github.event_name == 'pull_request' && !contains(github.event.pull_request.title, 'no-lint') && !contains(github.event.pull_request.title, 'skip-lint')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Node setup
|
||||
uses: ./.github/actions/setup
|
||||
# https://github.com/actions/virtual-environments/issues/1187
|
||||
- name: tune linux network
|
||||
run: sudo ethtool -K eth0 tx off rx off
|
||||
|
||||
- name: Setup Node@${{ env.NODE_VERSION }}
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
pnpm-version: ${{ env.PNPM_VERSION }}
|
||||
pnpm-install-cache-key: pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: ${{ env.PNPM_VERSION }}
|
||||
run_install: false
|
||||
|
||||
- name: Get pnpm store directory
|
||||
shell: bash
|
||||
run: |
|
||||
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
|
||||
|
||||
- name: Setup pnpm cache
|
||||
uses: actions/cache@v4
|
||||
timeout-minutes: 720
|
||||
with:
|
||||
path: ${{ env.STORE_PATH }}
|
||||
key: pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
pnpm-store-
|
||||
pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
|
||||
- run: pnpm install
|
||||
- name: Lint staged
|
||||
run: |
|
||||
git diff --name-only --diff-filter=d origin/${GITHUB_BASE_REF}...${GITHUB_SHA}
|
||||
@@ -89,46 +103,78 @@ jobs:
|
||||
build:
|
||||
needs: changes
|
||||
if: ${{ needs.changes.outputs.needs_build == 'true' }}
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 25
|
||||
|
||||
- name: Node setup
|
||||
uses: ./.github/actions/setup
|
||||
# https://github.com/actions/virtual-environments/issues/1187
|
||||
- name: tune linux network
|
||||
run: sudo ethtool -K eth0 tx off rx off
|
||||
|
||||
- name: Setup Node@${{ env.NODE_VERSION }}
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
pnpm-version: ${{ env.PNPM_VERSION }}
|
||||
pnpm-install-cache-key: pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: ${{ env.PNPM_VERSION }}
|
||||
run_install: false
|
||||
|
||||
- name: Get pnpm store directory
|
||||
shell: bash
|
||||
run: |
|
||||
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
|
||||
|
||||
- name: Setup pnpm cache
|
||||
uses: actions/cache@v4
|
||||
timeout-minutes: 720
|
||||
with:
|
||||
path: ${{ env.STORE_PATH }}
|
||||
key: pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
pnpm-store-
|
||||
pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
|
||||
- run: pnpm install
|
||||
- run: pnpm run build:all
|
||||
env:
|
||||
DO_NOT_TRACK: 1 # Disable Turbopack telemetry
|
||||
|
||||
- name: Cache build
|
||||
uses: actions/cache@v4
|
||||
timeout-minutes: 10
|
||||
with:
|
||||
path: ./*
|
||||
key: ${{ github.sha }}-${{ github.run_number }}
|
||||
|
||||
tests-unit:
|
||||
runs-on: ubuntu-24.04
|
||||
needs: [changes, build]
|
||||
if: ${{ needs.changes.outputs.needs_tests == 'true' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
|
||||
- name: Node setup
|
||||
uses: ./.github/actions/setup
|
||||
steps:
|
||||
# https://github.com/actions/virtual-environments/issues/1187
|
||||
- name: tune linux network
|
||||
run: sudo ethtool -K eth0 tx off rx off
|
||||
|
||||
- name: Setup Node@${{ env.NODE_VERSION }}
|
||||
uses: actions/setup-node@v4
|
||||
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: Install pnpm
|
||||
uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: ${{ env.PNPM_VERSION }}
|
||||
run_install: false
|
||||
|
||||
- name: Restore build
|
||||
uses: actions/cache@v4
|
||||
timeout-minutes: 10
|
||||
with:
|
||||
path: ./*
|
||||
key: ${{ github.sha }}-${{ github.run_number }}
|
||||
@@ -138,38 +184,9 @@ jobs:
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=8096
|
||||
|
||||
tests-types:
|
||||
runs-on: ubuntu-24.04
|
||||
needs: [changes, build]
|
||||
if: ${{ needs.changes.outputs.needs_tests == 'true' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- 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
|
||||
with:
|
||||
path: ./*
|
||||
key: ${{ github.sha }}-${{ github.run_number }}
|
||||
|
||||
- name: Types Tests
|
||||
run: pnpm test:types --target '>=5.7'
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=8096
|
||||
|
||||
tests-int:
|
||||
runs-on: ubuntu-24.04
|
||||
needs: [changes, build]
|
||||
if: ${{ needs.changes.outputs.needs_tests == 'true' }}
|
||||
name: int-${{ matrix.database }}
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -179,8 +196,6 @@ jobs:
|
||||
- postgres-custom-schema
|
||||
- postgres-uuid
|
||||
- supabase
|
||||
- sqlite
|
||||
- sqlite-uuid
|
||||
env:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
@@ -190,33 +205,25 @@ jobs:
|
||||
AWS_SECRET_ACCESS_KEY: localstack
|
||||
AWS_REGION: us-east-1
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: ${{ (startsWith(matrix.database, 'postgres') ) && 'postgis/postgis:16-3.4' || '' }}
|
||||
env:
|
||||
# must specify password for PG Docker container image, see: https://registry.hub.docker.com/_/postgres?tab=description&page=1&name=10
|
||||
POSTGRES_USER: ${{ env.POSTGRES_USER }}
|
||||
POSTGRES_PASSWORD: ${{ env.POSTGRES_PASSWORD }}
|
||||
POSTGRES_DB: ${{ env.POSTGRES_DB }}
|
||||
ports:
|
||||
- 5432:5432
|
||||
# needed because the postgres container does not provide a healthcheck
|
||||
options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
# https://github.com/actions/virtual-environments/issues/1187
|
||||
- name: tune linux network
|
||||
run: sudo ethtool -K eth0 tx off rx off
|
||||
|
||||
- name: Node setup
|
||||
uses: ./.github/actions/setup
|
||||
- name: Setup Node@${{ env.NODE_VERSION }}
|
||||
uses: actions/setup-node@v4
|
||||
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: Install pnpm
|
||||
uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: ${{ env.PNPM_VERSION }}
|
||||
run_install: false
|
||||
|
||||
- name: Restore build
|
||||
uses: actions/cache@v4
|
||||
timeout-minutes: 10
|
||||
with:
|
||||
path: ./*
|
||||
key: ${{ github.sha }}-${{ github.run_number }}
|
||||
@@ -224,6 +231,15 @@ jobs:
|
||||
- name: Start LocalStack
|
||||
run: pnpm docker:start
|
||||
|
||||
- name: Start PostgreSQL
|
||||
uses: CasperWA/postgresql-action@v1.2
|
||||
with:
|
||||
postgresql version: '14' # See https://hub.docker.com/_/postgres for available versions
|
||||
postgresql db: ${{ env.POSTGRES_DB }}
|
||||
postgresql user: ${{ env.POSTGRES_USER }}
|
||||
postgresql password: ${{ env.POSTGRES_PASSWORD }}
|
||||
if: startsWith(matrix.database, 'postgres')
|
||||
|
||||
- name: Install Supabase CLI
|
||||
uses: supabase/setup-cli@v1
|
||||
with:
|
||||
@@ -236,6 +252,10 @@ jobs:
|
||||
supabase start
|
||||
if: matrix.database == 'supabase'
|
||||
|
||||
- name: Wait for PostgreSQL
|
||||
run: sleep 30
|
||||
if: startsWith(matrix.database, 'postgres')
|
||||
|
||||
- name: Configure PostgreSQL
|
||||
run: |
|
||||
psql "postgresql://$POSTGRES_USER:$POSTGRES_PASSWORD@localhost:5432/$POSTGRES_DB" -c "CREATE ROLE runner SUPERUSER LOGIN;"
|
||||
@@ -261,10 +281,8 @@ jobs:
|
||||
POSTGRES_URL: ${{ env.POSTGRES_URL }}
|
||||
|
||||
tests-e2e:
|
||||
runs-on: ubuntu-24.04
|
||||
needs: [changes, build]
|
||||
if: ${{ needs.changes.outputs.needs_tests == 'true' }}
|
||||
name: e2e-${{ matrix.suite }}
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -272,44 +290,26 @@ jobs:
|
||||
suite:
|
||||
- _community
|
||||
- access-control
|
||||
- admin__e2e__general
|
||||
- admin__e2e__list-view
|
||||
- admin__e2e__document-view
|
||||
- admin-root
|
||||
- admin__e2e__1
|
||||
- admin__e2e__2
|
||||
- auth
|
||||
- auth-basic
|
||||
- joins
|
||||
- field-error-states
|
||||
- fields-relationship
|
||||
- fields__collections__Array
|
||||
- fields
|
||||
- fields__collections__Blocks
|
||||
- fields__collections__Blocks#config.blockreferences.ts
|
||||
- fields__collections__Checkbox
|
||||
- fields__collections__Collapsible
|
||||
- fields__collections__ConditionalLogic
|
||||
- fields__collections__CustomID
|
||||
- fields__collections__Date
|
||||
- fields__collections__Email
|
||||
- fields__collections__Indexed
|
||||
- fields__collections__JSON
|
||||
- fields__collections__Lexical__e2e__main
|
||||
- fields__collections__Lexical__e2e__blocks
|
||||
- fields__collections__Lexical__e2e__blocks#config.blockreferences.ts
|
||||
- fields__collections__Number
|
||||
- fields__collections__Point
|
||||
- fields__collections__Radio
|
||||
- fields__collections__Array
|
||||
- fields__collections__Relationship
|
||||
- fields__collections__RichText
|
||||
- fields__collections__Row
|
||||
- fields__collections__Select
|
||||
- fields__collections__Lexical__e2e__main
|
||||
- fields__collections__Lexical__e2e__blocks
|
||||
- fields__collections__Date
|
||||
- fields__collections__Number
|
||||
- fields__collections__Point
|
||||
- fields__collections__Tabs
|
||||
- fields__collections__Tabs2
|
||||
- fields__collections__Text
|
||||
- fields__collections__UI
|
||||
- fields__collections__Upload
|
||||
- live-preview
|
||||
- localization
|
||||
- locked-documents
|
||||
- i18n
|
||||
- plugin-cloud-storage
|
||||
- plugin-form-builder
|
||||
@@ -320,19 +320,24 @@ jobs:
|
||||
env:
|
||||
SUITE_NAME: ${{ matrix.suite }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
# https://github.com/actions/virtual-environments/issues/1187
|
||||
- name: tune linux network
|
||||
run: sudo ethtool -K eth0 tx off rx off
|
||||
|
||||
- name: Node setup
|
||||
uses: ./.github/actions/setup
|
||||
- name: Setup Node@${{ env.NODE_VERSION }}
|
||||
uses: actions/setup-node@v4
|
||||
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: Install pnpm
|
||||
uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: ${{ env.PNPM_VERSION }}
|
||||
run_install: false
|
||||
|
||||
- name: Restore build
|
||||
uses: actions/cache@v4
|
||||
timeout-minutes: 10
|
||||
with:
|
||||
path: ./*
|
||||
key: ${{ github.sha }}-${{ github.run_number }}
|
||||
@@ -364,7 +369,7 @@ jobs:
|
||||
run: pnpm exec playwright install-deps chromium
|
||||
|
||||
- name: E2E Tests
|
||||
run: PLAYWRIGHT_JSON_OUTPUT_NAME=results_${{ matrix.suite }}.json pnpm test:e2e:prod:ci ${{ matrix.suite }}
|
||||
run: PLAYWRIGHT_JSON_OUTPUT_NAME=results_${{ matrix.suite }}.json pnpm test:e2e ${{ matrix.suite }}
|
||||
env:
|
||||
PLAYWRIGHT_JSON_OUTPUT_NAME: results_${{ matrix.suite }}.json
|
||||
NEXT_TELEMETRY_DISABLED: 1
|
||||
@@ -384,109 +389,73 @@ jobs:
|
||||
# report-tag: ${{ matrix.suite }}
|
||||
# job-summary: true
|
||||
|
||||
# Build listed templates with packed local packages
|
||||
build-templates:
|
||||
runs-on: ubuntu-24.04
|
||||
app-build-with-packed:
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- template: blank
|
||||
database: mongodb
|
||||
- template: website
|
||||
database: mongodb
|
||||
- template: with-payload-cloud
|
||||
database: mongodb
|
||||
- template: with-vercel-mongodb
|
||||
database: mongodb
|
||||
# Postgres
|
||||
- template: with-postgres
|
||||
database: postgres
|
||||
- template: with-vercel-postgres
|
||||
database: postgres
|
||||
|
||||
- template: plugin
|
||||
|
||||
# Re-enable once PG conncection is figured out
|
||||
# - template: with-vercel-website
|
||||
# database: postgres
|
||||
|
||||
name: ${{ matrix.template }}-${{ matrix.database }}
|
||||
|
||||
env:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: payloadtests
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
# https://github.com/actions/virtual-environments/issues/1187
|
||||
- name: tune linux network
|
||||
run: sudo ethtool -K eth0 tx off rx off
|
||||
|
||||
- name: Node setup
|
||||
uses: ./.github/actions/setup
|
||||
- name: Setup Node@${{ env.NODE_VERSION }}
|
||||
uses: actions/setup-node@v4
|
||||
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: Install pnpm
|
||||
uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: ${{ env.PNPM_VERSION }}
|
||||
run_install: false
|
||||
|
||||
- name: Restore build
|
||||
uses: actions/cache@v4
|
||||
timeout-minutes: 10
|
||||
with:
|
||||
path: ./*
|
||||
key: ${{ github.sha }}-${{ github.run_number }}
|
||||
|
||||
- name: Start PostgreSQL
|
||||
uses: CasperWA/postgresql-action@v1.2
|
||||
with:
|
||||
postgresql version: '14' # See https://hub.docker.com/_/postgres for available versions
|
||||
postgresql db: ${{ env.POSTGRES_DB }}
|
||||
postgresql user: ${{ env.POSTGRES_USER }}
|
||||
postgresql password: ${{ env.POSTGRES_PASSWORD }}
|
||||
if: matrix.database == 'postgres'
|
||||
|
||||
- name: Wait for PostgreSQL
|
||||
run: sleep 30
|
||||
if: matrix.database == 'postgres'
|
||||
|
||||
- name: Configure PostgreSQL
|
||||
run: |
|
||||
psql "postgresql://$POSTGRES_USER:$POSTGRES_PASSWORD@localhost:5432/$POSTGRES_DB" -c "CREATE ROLE runner SUPERUSER LOGIN;"
|
||||
psql "postgresql://$POSTGRES_USER:$POSTGRES_PASSWORD@localhost:5432/$POSTGRES_DB" -c "SELECT version();"
|
||||
echo "POSTGRES_URL=postgresql://$POSTGRES_USER:$POSTGRES_PASSWORD@localhost:5432/$POSTGRES_DB" >> $GITHUB_ENV
|
||||
if: matrix.database == 'postgres'
|
||||
|
||||
- name: Start MongoDB
|
||||
uses: supercharge/mongodb-github-action@1.11.0
|
||||
uses: supercharge/mongodb-github-action@1.10.0
|
||||
with:
|
||||
mongodb-version: 6.0
|
||||
if: matrix.database == 'mongodb'
|
||||
|
||||
- name: Build Template
|
||||
- name: Pack and build app
|
||||
run: |
|
||||
pnpm run script:pack --dest templates/${{ matrix.template }}
|
||||
pnpm run script:build-template-with-local-pkgs ${{ matrix.template }} $POSTGRES_URL
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=8096
|
||||
set -ex
|
||||
pnpm run script:pack --dest templates/blank-3.0
|
||||
cd templates/blank-3.0
|
||||
cp .env.example .env
|
||||
ls -la
|
||||
pnpm add ./*.tgz --ignore-workspace
|
||||
pnpm install --ignore-workspace --no-frozen-lockfile
|
||||
cat package.json
|
||||
pnpm run build
|
||||
|
||||
tests-type-generation:
|
||||
runs-on: ubuntu-24.04
|
||||
needs: [changes, build]
|
||||
if: ${{ needs.changes.outputs.needs_tests == 'true' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
|
||||
- name: Node setup
|
||||
uses: ./.github/actions/setup
|
||||
steps:
|
||||
# https://github.com/actions/virtual-environments/issues/1187
|
||||
- name: tune linux network
|
||||
run: sudo ethtool -K eth0 tx off rx off
|
||||
|
||||
- name: Setup Node@${{ env.NODE_VERSION }}
|
||||
uses: actions/setup-node@v4
|
||||
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: Install pnpm
|
||||
uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: ${{ env.PNPM_VERSION }}
|
||||
run_install: false
|
||||
|
||||
- name: Restore build
|
||||
uses: actions/cache@v4
|
||||
timeout-minutes: 10
|
||||
with:
|
||||
path: ./*
|
||||
key: ${{ github.sha }}-${{ github.run_number }}
|
||||
@@ -497,33 +466,83 @@ jobs:
|
||||
- name: Generate GraphQL schema file
|
||||
run: pnpm dev:generate-graphql-schema graphql-schema-gen
|
||||
|
||||
templates:
|
||||
needs: changes
|
||||
if: false # Disable until templates are updated for 3.0
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
template: [blank, website, ecommerce]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 25
|
||||
# https://github.com/actions/virtual-environments/issues/1187
|
||||
- name: tune linux network
|
||||
run: sudo ethtool -K eth0 tx off rx off
|
||||
|
||||
- name: Setup Node@${{ env.NODE_VERSION }}
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
|
||||
- name: Start MongoDB
|
||||
uses: supercharge/mongodb-github-action@1.10.0
|
||||
with:
|
||||
mongodb-version: 6.0
|
||||
|
||||
- name: Build Template
|
||||
run: |
|
||||
cd templates/${{ matrix.template }}
|
||||
cp .env.example .env
|
||||
yarn install
|
||||
yarn build
|
||||
yarn generate:types
|
||||
|
||||
generated-templates:
|
||||
needs: build
|
||||
if: false # Needs to pull in tgz files from build
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
# https://github.com/actions/virtual-environments/issues/1187
|
||||
- name: tune linux network
|
||||
run: sudo ethtool -K eth0 tx off rx off
|
||||
|
||||
- name: Setup Node@${{ env.NODE_VERSION }}
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: ${{ env.PNPM_VERSION }}
|
||||
run_install: false
|
||||
|
||||
- name: Restore build
|
||||
uses: actions/cache@v4
|
||||
timeout-minutes: 10
|
||||
with:
|
||||
path: ./*
|
||||
key: ${{ github.sha }}-${{ github.run_number }}
|
||||
|
||||
- name: Build all generated templates
|
||||
run: pnpm tsx ./scripts/build-generated-templates.ts
|
||||
|
||||
all-green:
|
||||
name: All Green
|
||||
if: always()
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- lint
|
||||
- build
|
||||
- build-templates
|
||||
- tests-unit
|
||||
- tests-int
|
||||
- tests-e2e
|
||||
- tests-types
|
||||
- tests-type-generation
|
||||
|
||||
steps:
|
||||
- if: ${{ always() && (contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled')) }}
|
||||
run: exit 1
|
||||
|
||||
publish-canary:
|
||||
name: Publish Canary
|
||||
runs-on: ubuntu-24.04
|
||||
if: ${{ needs.all-green.result == 'success' && github.ref_name == 'main' }}
|
||||
needs:
|
||||
- all-green
|
||||
|
||||
steps:
|
||||
# debug github.ref output
|
||||
- run: |
|
||||
echo github.ref: ${{ github.ref }}
|
||||
echo isV3: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
121
.github/workflows/post-release-templates.yml
vendored
121
.github/workflows/post-release-templates.yml
vendored
@@ -1,121 +0,0 @@
|
||||
name: post-release-templates
|
||||
|
||||
on:
|
||||
release:
|
||||
types:
|
||||
- published
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
NODE_VERSION: 22.6.0
|
||||
PNPM_VERSION: 9.7.1
|
||||
DO_NOT_TRACK: 1 # Disable Turbopack telemetry
|
||||
NEXT_TELEMETRY_DISABLED: 1 # Disable Next telemetry
|
||||
|
||||
jobs:
|
||||
wait_for_release:
|
||||
runs-on: ubuntu-24.04
|
||||
outputs:
|
||||
release_tag: ${{ steps.determine_tag.outputs.release_tag }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
sparse-checkout: .github/workflows
|
||||
|
||||
- name: Determine Release Tag
|
||||
id: determine_tag
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" == "release" ]; then
|
||||
echo "Using tag from release event: ${{ github.event.release.tag_name }}"
|
||||
echo "release_tag=${{ github.event.release.tag_name }}" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
# pull latest tag from github, must match any version except v2. Should match v3, v4, v99, etc.
|
||||
echo "Fetching latest tag from github..."
|
||||
LATEST_TAG=$(git describe --tags --abbrev=0 --match 'v[0-9]*' --exclude 'v2*')
|
||||
echo "Latest tag: $LATEST_TAG"
|
||||
echo "release_tag=$LATEST_TAG" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Wait until latest versions resolve on npm registry
|
||||
run: |
|
||||
./.github/workflows/wait-until-package-version.sh payload ${{ steps.determine_tag.outputs.release_tag }}
|
||||
./.github/workflows/wait-until-package-version.sh @payloadcms/translations ${{ steps.determine_tag.outputs.release_tag }}
|
||||
./.github/workflows/wait-until-package-version.sh @payloadcms/next ${{ steps.determine_tag.outputs.release_tag }}
|
||||
|
||||
update_templates:
|
||||
needs: wait_for_release
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
env:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: payloadtests
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- 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
|
||||
with:
|
||||
postgresql version: '14' # See https://hub.docker.com/_/postgres for available versions
|
||||
postgresql db: ${{ env.POSTGRES_DB }}
|
||||
postgresql user: ${{ env.POSTGRES_USER }}
|
||||
postgresql password: ${{ env.POSTGRES_PASSWORD }}
|
||||
|
||||
- name: Wait for PostgreSQL
|
||||
run: sleep 30
|
||||
|
||||
- name: Configure PostgreSQL
|
||||
run: |
|
||||
psql "postgresql://$POSTGRES_USER:$POSTGRES_PASSWORD@localhost:5432/$POSTGRES_DB" -c "CREATE ROLE runner SUPERUSER LOGIN;"
|
||||
psql "postgresql://$POSTGRES_USER:$POSTGRES_PASSWORD@localhost:5432/$POSTGRES_DB" -c "SELECT version();"
|
||||
echo "POSTGRES_URL=postgresql://$POSTGRES_USER:$POSTGRES_PASSWORD@localhost:5432/$POSTGRES_DB" >> $GITHUB_ENV
|
||||
echo "DATABASE_URI=postgresql://$POSTGRES_USER:$POSTGRES_PASSWORD@localhost:5432/$POSTGRES_DB" >> $GITHUB_ENV
|
||||
|
||||
- name: Start MongoDB
|
||||
uses: supercharge/mongodb-github-action@1.11.0
|
||||
with:
|
||||
mongodb-version: 6.0
|
||||
|
||||
- name: Update template lockfiles and migrations
|
||||
run: pnpm script:gen-templates
|
||||
|
||||
- name: Commit and push changes
|
||||
id: commit
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
set -ex
|
||||
git config --global user.name "github-actions[bot]"
|
||||
git config --global user.email "github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
git diff --name-only
|
||||
|
||||
export BRANCH_NAME=templates/bump-${{ needs.wait_for_release.outputs.release_tag }}-$(date +%s)
|
||||
echo "branch=$BRANCH_NAME" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Create pull request
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
with:
|
||||
token: ${{ secrets.GH_TOKEN_POST_RELEASE_TEMPLATES }}
|
||||
labels: 'area: templates'
|
||||
author: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
|
||||
commit-message: 'templates: bump templates for ${{ needs.wait_for_release.outputs.release_tag }}'
|
||||
branch: ${{ steps.commit.outputs.branch }}
|
||||
base: main
|
||||
assignees: ${{ github.actor }}
|
||||
title: 'templates: bump for ${{ needs.wait_for_release.outputs.release_tag }}'
|
||||
body: |
|
||||
🤖 Automated bump of templates for ${{ needs.wait_for_release.outputs.release_tag }}
|
||||
|
||||
Triggered by user: @${{ github.actor }}
|
||||
52
.github/workflows/post-release.yml
vendored
52
.github/workflows/post-release.yml
vendored
@@ -1,52 +0,0 @@
|
||||
name: post-release
|
||||
|
||||
on:
|
||||
release:
|
||||
types:
|
||||
- published
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: 'Release tag to process (optional)'
|
||||
required: false
|
||||
default: ''
|
||||
|
||||
env:
|
||||
NODE_VERSION: 22.6.0
|
||||
PNPM_VERSION: 9.7.1
|
||||
DO_NOT_TRACK: 1 # Disable Turbopack telemetry
|
||||
NEXT_TELEMETRY_DISABLED: 1 # Disable Next telemetry
|
||||
|
||||
jobs:
|
||||
post_release:
|
||||
runs-on: ubuntu-24.04
|
||||
if: ${{ github.event_name != 'workflow_dispatch' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/release-commenter
|
||||
continue-on-error: true
|
||||
env:
|
||||
ACTIONS_STEP_DEBUG: true
|
||||
with:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
tag-filter: 'v\d'
|
||||
|
||||
# Change to blank to disable commenting
|
||||
# comment-template: ''
|
||||
|
||||
comment-template: |
|
||||
🚀 This is included in version {release_link}
|
||||
|
||||
github-releases-to-discord:
|
||||
runs-on: ubuntu-24.04
|
||||
if: ${{ github.event_name != 'workflow_dispatch' }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Github Releases To Discord
|
||||
uses: SethCohen/github-releases-to-discord@v1.16.2
|
||||
with:
|
||||
webhook_url: ${{ secrets.DISCORD_RELEASES_WEBHOOK_URL }}
|
||||
color: '16777215'
|
||||
username: 'Payload Releases'
|
||||
avatar_url: 'https://l4wlsi8vxy8hre4v.public.blob.vercel-storage.com/discord-bot-logo.png'
|
||||
29
.github/workflows/pr-title.yml
vendored
29
.github/workflows/pr-title.yml
vendored
@@ -1,10 +1,11 @@
|
||||
name: pr-title
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
pull_request:
|
||||
types:
|
||||
- opened
|
||||
- edited
|
||||
- synchronize
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
@@ -12,7 +13,7 @@ permissions:
|
||||
jobs:
|
||||
main:
|
||||
name: lint-pr-title
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: amannn/action-semantic-pull-request@v5
|
||||
id: lint_pr_title
|
||||
@@ -24,38 +25,32 @@ jobs:
|
||||
chore
|
||||
ci
|
||||
docs
|
||||
examples
|
||||
feat
|
||||
fix
|
||||
perf
|
||||
refactor
|
||||
revert
|
||||
style
|
||||
templates
|
||||
test
|
||||
types
|
||||
scopes: |
|
||||
cpa
|
||||
db-\*
|
||||
db-mongodb
|
||||
db-postgres
|
||||
db-vercel-postgres
|
||||
db-sqlite
|
||||
drizzle
|
||||
email-\*
|
||||
email-nodemailer
|
||||
email-resend
|
||||
eslint
|
||||
graphql
|
||||
live-preview
|
||||
live-preview-react
|
||||
next
|
||||
payload-cloud
|
||||
payload
|
||||
plugin-cloud
|
||||
plugin-cloud-storage
|
||||
plugin-form-builder
|
||||
plugin-multi-tenant
|
||||
plugin-nested-docs
|
||||
plugin-redirects
|
||||
plugin-relationship-object-ids
|
||||
plugin-search
|
||||
plugin-sentry
|
||||
plugin-seo
|
||||
@@ -66,7 +61,6 @@ jobs:
|
||||
storage-\*
|
||||
storage-azure
|
||||
storage-gcs
|
||||
storage-uploadthing
|
||||
storage-vercel-blob
|
||||
storage-s3
|
||||
translations
|
||||
@@ -107,14 +101,3 @@ jobs:
|
||||
with:
|
||||
header: pr-title-lint-error
|
||||
delete: true
|
||||
|
||||
label-pr-on-open:
|
||||
name: label-pr-on-open
|
||||
runs-on: ubuntu-24.04
|
||||
if: github.event.action == 'opened'
|
||||
steps:
|
||||
- name: Tag with 2.x branch with v2
|
||||
if: github.event.pull_request.base.ref == '2.x'
|
||||
uses: actions-ecosystem/action-add-labels@v1
|
||||
with:
|
||||
labels: v2
|
||||
|
||||
12
.github/workflows/release-canary.yml
vendored
12
.github/workflows/release-canary.yml
vendored
@@ -2,19 +2,20 @@ name: release-canary
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
branches:
|
||||
- beta
|
||||
|
||||
env:
|
||||
NODE_VERSION: 22.6.0
|
||||
PNPM_VERSION: 9.7.1
|
||||
NODE_VERSION: 18.20.2
|
||||
PNPM_VERSION: 8.15.7
|
||||
DO_NOT_TRACK: 1 # Disable Turbopack telemetry
|
||||
NEXT_TELEMETRY_DISABLED: 1 # Disable Next telemetry
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: release-canary-${{ github.ref_name }}-${{ github.sha }}
|
||||
permissions:
|
||||
id-token: write
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
@@ -28,7 +29,8 @@ jobs:
|
||||
env:
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
- name: Canary release script
|
||||
run: pnpm release:canary
|
||||
# dry run hard-coded to true for testing and no npm token provided
|
||||
run: pnpm tsx ./scripts/publish-canary.ts
|
||||
env:
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
NPM_CONFIG_PROVENANCE: true
|
||||
|
||||
51
.github/workflows/stale.yml
vendored
51
.github/workflows/stale.yml
vendored
@@ -1,51 +0,0 @@
|
||||
name: stale
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Run nightly at 1am EST
|
||||
- cron: '0 5 * * *'
|
||||
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
dry-run:
|
||||
description: Run the stale action in debug-only mode
|
||||
default: false
|
||||
required: false
|
||||
type: boolean
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: actions/stale@v9
|
||||
id: stale
|
||||
with:
|
||||
debug-only: ${{ inputs.dry-run || false }}
|
||||
|
||||
days-before-stale: 30
|
||||
days-before-close: -1 # Disable closing
|
||||
ascending: true
|
||||
operations-per-run: 300
|
||||
exempt-all-assignees: true
|
||||
|
||||
# Issues
|
||||
stale-issue-label: stale
|
||||
exempt-issue-labels: 'prioritized,keep,created-by: Payload team,created-by: Contributor,status: verified'
|
||||
stale-issue-message: ''
|
||||
|
||||
close-issue-message: |
|
||||
This issue was automatically closed due to lack of activity.
|
||||
|
||||
# Pull Requests
|
||||
stale-pr-label: stale
|
||||
exempt-pr-labels: 'prioritized,keep,created-by: Payload team,created-by: Contributor'
|
||||
stale-pr-message: ''
|
||||
close-pr-message: |
|
||||
This pull request was automatically closed due to lack of activity.
|
||||
|
||||
# TODO: Add a step to notify team
|
||||
- name: Print outputs
|
||||
run: echo ${{ format('{0},{1}', toJSON(steps.stale.outputs.staled-issues-prs), toJSON(steps.stale.outputs.closed-issues-prs)) }}
|
||||
102
.github/workflows/triage.yml
vendored
102
.github/workflows/triage.yml
vendored
@@ -1,102 +0,0 @@
|
||||
name: triage
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types:
|
||||
- opened
|
||||
issues:
|
||||
types:
|
||||
- opened
|
||||
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
debug-context:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: View context attributes
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: console.log({ context })
|
||||
|
||||
label-created-by:
|
||||
name: label-on-open
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Tag with 'created-by'
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
const payloadTeamUsernames = [
|
||||
'denolfe',
|
||||
'jmikrut',
|
||||
'DanRibbens',
|
||||
'jacobsfletch',
|
||||
'JarrodMFlesch',
|
||||
'AlessioGr',
|
||||
'JessChowdhury',
|
||||
'kendelljoseph',
|
||||
'PatrikKozak',
|
||||
'tylandavis',
|
||||
'paulpopus',
|
||||
'r1tsuu',
|
||||
'GermanJablo',
|
||||
];
|
||||
|
||||
const type = context.payload.pull_request ? 'pull_request' : 'issue';
|
||||
|
||||
const isTeamMember = payloadTeamUsernames
|
||||
.map(n => n.toLowerCase())
|
||||
.includes(context.payload[type].user.login.toLowerCase());
|
||||
|
||||
if (isTeamMember) {
|
||||
github.rest.issues.addLabels({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
labels: ['created-by: Payload team'],
|
||||
});
|
||||
console.log(`Added 'created-by: Payload team' label`);
|
||||
return;
|
||||
}
|
||||
|
||||
const association = context.payload[type].author_association;
|
||||
let label = ''
|
||||
if (association === 'MEMBER' || association === 'OWNER') {
|
||||
label = 'created-by: Payload team';
|
||||
} else if (association === 'CONTRIBUTOR') {
|
||||
label = 'created-by: Contributor';
|
||||
}
|
||||
|
||||
if (!label) return;
|
||||
|
||||
github.rest.issues.addLabels({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
labels: [label],
|
||||
});
|
||||
console.log(`Added '${label}' label.`);
|
||||
|
||||
triage:
|
||||
name: initial-triage
|
||||
if: github.event_name == 'issues'
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.base.ref }}
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
- uses: ./.github/actions/triage
|
||||
with:
|
||||
reproduction-comment: '.github/comments/invalid-reproduction.md'
|
||||
reproduction-link-section: '### Link to the code that reproduces this issue(.*)### Reproduction Steps'
|
||||
reproduction-issue-labels: 'validate-reproduction'
|
||||
actions-to-perform: 'tag,comment'
|
||||
31
.github/workflows/wait-until-package-version.sh
vendored
31
.github/workflows/wait-until-package-version.sh
vendored
@@ -1,31 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [[ "$#" -ne 2 ]]; then
|
||||
echo "Usage: $0 <package-name> <version>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
PACKAGE_NAME="$1"
|
||||
TARGET_VERSION=${2#v} # Git tag has leading 'v', npm version does not
|
||||
TIMEOUT=300 # 5 minutes in seconds
|
||||
INTERVAL=10 # 10 seconds
|
||||
ELAPSED=0
|
||||
|
||||
echo "Waiting for version ${TARGET_VERSION} of '${PACKAGE_NAME}' to resolve... (timeout: ${TIMEOUT} seconds)"
|
||||
|
||||
while [[ ${ELAPSED} -lt ${TIMEOUT} ]]; do
|
||||
latest_version=$(npm show "${PACKAGE_NAME}" version 2>/dev/null)
|
||||
|
||||
if [[ ${latest_version} == "${TARGET_VERSION}" ]]; then
|
||||
echo "SUCCCESS: Version ${TARGET_VERSION} of ${PACKAGE_NAME} is available."
|
||||
exit 0
|
||||
else
|
||||
echo "Version ${TARGET_VERSION} of ${PACKAGE_NAME} is not available yet. Retrying in ${INTERVAL} seconds... (elapsed: ${ELAPSED}s)"
|
||||
fi
|
||||
|
||||
sleep "${INTERVAL}"
|
||||
ELAPSED=$((ELAPSED + INTERVAL))
|
||||
done
|
||||
|
||||
echo "Timed out after ${TIMEOUT} seconds waiting for version ${TARGET_VERSION} of '${PACKAGE_NAME}' to resolve."
|
||||
exit 1
|
||||
25
.gitignore
vendored
25
.gitignore
vendored
@@ -5,10 +5,6 @@ dist
|
||||
!/.idea/runConfigurations
|
||||
!/.idea/payload.iml
|
||||
|
||||
# Custom actions
|
||||
!.github/actions/**/dist
|
||||
|
||||
test/packed
|
||||
test-results
|
||||
.devcontainer
|
||||
.localstack
|
||||
@@ -26,10 +22,6 @@ meta_shared.json
|
||||
# Ignore test directory media folder/files
|
||||
/media
|
||||
test/media
|
||||
*payloadtests.db
|
||||
*payloadtests.db-journal
|
||||
*payloadtests.db-shm
|
||||
*payloadtests.db-wal
|
||||
/versions
|
||||
|
||||
# Created by https://www.toptal.com/developers/gitignore/api/node,macos,windows,webstorm,sublimetext,visualstudiocode
|
||||
@@ -157,7 +149,6 @@ out
|
||||
# Nuxt.js build / generate output
|
||||
.nuxt
|
||||
dist
|
||||
dist_optimized
|
||||
|
||||
# Gatsby files
|
||||
.cache/
|
||||
@@ -305,19 +296,3 @@ $RECYCLE.BIN/
|
||||
|
||||
/build
|
||||
.swc
|
||||
app/(payload)/admin/importMap.js
|
||||
test/live-preview/app/(payload)/admin/importMap.js
|
||||
/test/live-preview/app/(payload)/admin/importMap.js
|
||||
test/admin-root/app/(payload)/admin/importMap.js
|
||||
/test/admin-root/app/(payload)/admin/importMap.js
|
||||
test/app/(payload)/admin/importMap.js
|
||||
/test/app/(payload)/admin/importMap.js
|
||||
test/pnpm-lock.yaml
|
||||
test/databaseAdapter.js
|
||||
/filename-compound-index
|
||||
/media-with-relation-preview
|
||||
/media-without-relation-preview
|
||||
/media-without-cache-tags
|
||||
test/.localstack
|
||||
test/google-cloud-storage
|
||||
test/azurestoragedata/
|
||||
|
||||
7
.idea/payload.iml
generated
7
.idea/payload.iml
generated
@@ -26,7 +26,6 @@
|
||||
<excludeFolder url="file://$MODULE_DIR$/packages/live-preview/dist" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/packages/next/.swc" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/packages/next/.turbo" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/packages/next/dist" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/packages/payload/fields" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/packages/plugin-cloud-storage/.turbo" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/packages/plugin-cloud-storage/dist" />
|
||||
@@ -76,12 +75,8 @@
|
||||
<excludeFolder url="file://$MODULE_DIR$/packages/ui/.swc" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/packages/ui/.turbo" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/packages/ui/dist" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/packages/drizzle/.turbo" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/packages/drizzle/dist" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/packages/db-sqlite/.turbo" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/packages/db-sqlite/dist" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
</module>
|
||||
13
.idea/runConfigurations/Run_Dev_Fields.xml
generated
13
.idea/runConfigurations/Run_Dev_Fields.xml
generated
@@ -1,13 +1,8 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Run Dev Fields" type="js.build_tools.npm">
|
||||
<package-json value="$PROJECT_DIR$/package.json" />
|
||||
<command value="run" />
|
||||
<scripts>
|
||||
<script value="dev" />
|
||||
</scripts>
|
||||
<arguments value="fields" />
|
||||
<node-interpreter value="project" />
|
||||
<envs />
|
||||
<configuration default="false" name="Run Dev Fields" type="NodeJSConfigurationType" application-parameters="--no-deprecation fields" path-to-js-file="test/dev.js" working-dir="$PROJECT_DIR$">
|
||||
<envs>
|
||||
<env name="NODE_OPTIONS" value="--no-deprecation" />
|
||||
</envs>
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
||||
13
.idea/runConfigurations/Run_Dev__community.xml
generated
13
.idea/runConfigurations/Run_Dev__community.xml
generated
@@ -1,13 +1,8 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Run Dev _community" type="js.build_tools.npm">
|
||||
<package-json value="$PROJECT_DIR$/package.json" />
|
||||
<command value="run" />
|
||||
<scripts>
|
||||
<script value="dev" />
|
||||
</scripts>
|
||||
<arguments value="_community" />
|
||||
<node-interpreter value="project" />
|
||||
<envs />
|
||||
<configuration default="false" name="Run Dev _community" type="NodeJSConfigurationType" application-parameters="--no-deprecation _community" path-to-js-file="test/dev.js" working-dir="$PROJECT_DIR$">
|
||||
<envs>
|
||||
<env name="NODE_OPTIONS" value="--no-deprecation" />
|
||||
</envs>
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
||||
13
.idea/runConfigurations/Run_Dev_admin.xml
generated
13
.idea/runConfigurations/Run_Dev_admin.xml
generated
@@ -1,13 +0,0 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Run Dev admin" type="js.build_tools.npm">
|
||||
<package-json value="$PROJECT_DIR$/package.json" />
|
||||
<command value="run" />
|
||||
<scripts>
|
||||
<script value="dev" />
|
||||
</scripts>
|
||||
<arguments value="admin" />
|
||||
<node-interpreter value="project" />
|
||||
<envs />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
||||
@@ -1,7 +1,7 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="true" type="JavaScriptTestRunnerJest">
|
||||
<node-interpreter value="project" />
|
||||
<node-options value="--no-deprecation" />
|
||||
<node-options value="--experimental-vm-modules --no-deprecation" />
|
||||
<envs />
|
||||
<scope-kind value="ALL" />
|
||||
<method v="2" />
|
||||
|
||||
@@ -1 +1 @@
|
||||
v22.6.0
|
||||
v18.20.2
|
||||
|
||||
4
.npmrc
4
.npmrc
@@ -1,4 +1,2 @@
|
||||
symlink=true
|
||||
node-linker=isolated
|
||||
hoist-workspace-packages=false # the default in pnpm v9 is true, but that can break our runtime dependency checks
|
||||
save-prefix=''
|
||||
node-linker=isolated # due to a typescript bug, isolated mode requires @types/express-serve-static-core, terser and monaco-editor to be installed https://github.com/microsoft/TypeScript/issues/47663#issuecomment-1519138189 along with two other changes in the code which I've marked with (tsbugisolatedmode) in the code
|
||||
@@ -1,2 +0,0 @@
|
||||
pnpm 9.7.1
|
||||
nodejs 22.6.0
|
||||
45
.vscode/launch.json
vendored
45
.vscode/launch.json
vendored
@@ -10,14 +10,14 @@
|
||||
"cwd": "${workspaceFolder}"
|
||||
},
|
||||
{
|
||||
"command": "pnpm tsx --no-deprecation test/dev.ts _community",
|
||||
"command": "node --no-deprecation test/dev.js _community",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"name": "Run Dev Community",
|
||||
"request": "launch",
|
||||
"type": "node-terminal"
|
||||
},
|
||||
{
|
||||
"command": "pnpm tsx --no-deprecation test/dev.ts storage-uploadthing",
|
||||
"command": "node --no-deprecation test/dev.js storage-uploadthing",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"name": "Uploadthing",
|
||||
"request": "launch",
|
||||
@@ -25,7 +25,7 @@
|
||||
"envFile": "${workspaceFolder}/test/storage-uploadthing/.env"
|
||||
},
|
||||
{
|
||||
"command": "pnpm tsx --no-deprecation test/dev.ts live-preview",
|
||||
"command": "node --no-deprecation test/dev.js live-preview",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"name": "Run Dev Live Preview",
|
||||
"request": "launch",
|
||||
@@ -43,33 +43,19 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "pnpm tsx --no-deprecation test/dev.ts admin",
|
||||
"command": "node --no-deprecation test/dev.js admin",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"name": "Run Dev Admin",
|
||||
"request": "launch",
|
||||
"type": "node-terminal"
|
||||
},
|
||||
{
|
||||
"command": "pnpm tsx --no-deprecation test/dev.ts auth",
|
||||
"command": "node --no-deprecation test/dev.js auth",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"name": "Run Dev Auth",
|
||||
"request": "launch",
|
||||
"type": "node-terminal"
|
||||
},
|
||||
{
|
||||
"command": "pnpm tsx --no-deprecation test/dev.ts fields-relationship",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"name": "Run Dev Fields-Relationship",
|
||||
"request": "launch",
|
||||
"type": "node-terminal"
|
||||
},
|
||||
{
|
||||
"command": "pnpm tsx --no-deprecation test/dev.ts login-with-username",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"name": "Run Dev Login-With-Username",
|
||||
"request": "launch",
|
||||
"type": "node-terminal"
|
||||
},
|
||||
{
|
||||
"command": "pnpm run dev plugin-cloud-storage",
|
||||
"cwd": "${workspaceFolder}",
|
||||
@@ -81,21 +67,21 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "pnpm tsx --no-deprecation test/dev.ts collections-graphql",
|
||||
"command": "node --no-deprecation test/dev.js collections-graphql",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"name": "Run Dev GraphQL",
|
||||
"request": "launch",
|
||||
"type": "node-terminal"
|
||||
},
|
||||
{
|
||||
"command": "pnpm tsx --no-deprecation test/dev.ts fields",
|
||||
"command": "node --no-deprecation test/dev.js fields",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"name": "Run Dev Fields",
|
||||
"request": "launch",
|
||||
"type": "node-terminal"
|
||||
},
|
||||
{
|
||||
"command": "pnpm tsx --no-deprecation test/dev.ts versions",
|
||||
"command": "node --no-deprecation test/dev.js versions",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"name": "Run Dev Postgres",
|
||||
"request": "launch",
|
||||
@@ -105,35 +91,28 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "pnpm tsx --no-deprecation test/dev.ts versions",
|
||||
"command": "node --no-deprecation test/dev.js versions",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"name": "Run Dev Versions",
|
||||
"request": "launch",
|
||||
"type": "node-terminal"
|
||||
},
|
||||
{
|
||||
"command": "pnpm tsx --no-deprecation test/dev.ts localization",
|
||||
"command": "node --no-deprecation test/dev.js localization",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"name": "Run Dev Localization",
|
||||
"request": "launch",
|
||||
"type": "node-terminal"
|
||||
},
|
||||
{
|
||||
"command": "pnpm tsx --no-deprecation test/dev.ts locked-documents",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"name": "Run Dev Locked Documents",
|
||||
"request": "launch",
|
||||
"type": "node-terminal"
|
||||
},
|
||||
{
|
||||
"command": "pnpm tsx --no-deprecation test/dev.ts uploads",
|
||||
"command": "node --no-deprecation test/dev.js uploads",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"name": "Run Dev Uploads",
|
||||
"request": "launch",
|
||||
"type": "node-terminal"
|
||||
},
|
||||
{
|
||||
"command": "pnpm tsx --no-deprecation test/dev.ts field-error-states",
|
||||
"command": "node --no-deprecation test/dev.js field-error-states",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"name": "Run Dev Field Error States",
|
||||
"request": "launch",
|
||||
|
||||
57
.vscode/settings.json
vendored
57
.vscode/settings.json
vendored
@@ -1,28 +1,49 @@
|
||||
{
|
||||
"npm.packageManager": "pnpm",
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"editor.formatOnSave": true,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": "explicit"
|
||||
"[typescript]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"editor.formatOnSave": true,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": "explicit"
|
||||
}
|
||||
},
|
||||
"[typescriptreact]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"editor.formatOnSave": true,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": "explicit"
|
||||
}
|
||||
},
|
||||
"[javascript]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"editor.formatOnSave": true,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": "explicit"
|
||||
}
|
||||
},
|
||||
"[json]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"editor.formatOnSave": true
|
||||
},
|
||||
"[jsonc]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"editor.formatOnSave": true
|
||||
},
|
||||
"editor.formatOnSaveMode": "file",
|
||||
"eslint.rules.customizations": [
|
||||
// Defaultt all ESLint errors to 'warn' to differentate from TypeScript's 'error' level
|
||||
{ "rule": "*", "severity": "warn" },
|
||||
|
||||
// Silence some warnings that will get auto-fixed
|
||||
{ "rule": "perfectionist/*", "severity": "off", "fixable": true },
|
||||
{ "rule": "curly", "severity": "off", "fixable": true },
|
||||
{ "rule": "object-shorthand", "severity": "off", "fixable": true }
|
||||
],
|
||||
// All ESLint rules to 'warn' to differentate from TypeScript's 'error' level
|
||||
"eslint.rules.customizations": [{ "rule": "*", "severity": "warn" }],
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
// Load .git-blame-ignore-revs file
|
||||
"gitlens.advanced.blame.customArguments": ["--ignore-revs-file", ".git-blame-ignore-revs"],
|
||||
"jestrunner.jestCommand": "pnpm exec cross-env NODE_OPTIONS=\"--no-deprecation\" node 'node_modules/jest/bin/jest.js'",
|
||||
"jestrunner.changeDirectoryToWorkspaceRoot": false,
|
||||
"jestrunner.debugOptions": {
|
||||
"runtimeArgs": ["--no-deprecation"]
|
||||
"[javascript][typescript][typescriptreact]": {
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": "explicit"
|
||||
}
|
||||
},
|
||||
// Essentially disables bun test buttons
|
||||
"bun.test.filePattern": "bun.test.ts"
|
||||
"files.insertFinalNewline": true,
|
||||
"jestrunner.jestCommand": "pnpm exec cross-env NODE_OPTIONS=\"--experimental-vm-modules --no-deprecation\" node 'node_modules/jest/bin/jest.js'",
|
||||
"jestrunner.debugOptions": {
|
||||
"runtimeArgs": ["--experimental-vm-modules", "--no-deprecation"]
|
||||
}
|
||||
}
|
||||
|
||||
4503
CHANGELOG.md
Normal file
4503
CHANGELOG.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -63,7 +63,7 @@ Each test directory is split up in this way specifically to reduce friction when
|
||||
|
||||
The following command will start Payload with your config: `pnpm dev my-test-dir`. Example: `pnpm dev fields` for the test/`fields` test suite. This command will start up Payload using your config and refresh a test database on every restart. If you're using VS Code, the most common run configs are automatically added to your editor - you should be able to find them in your VS Code launch tab.
|
||||
|
||||
By default, payload will [automatically log you in](https://payloadcms.com/docs/authentication/overview#auto-login) with the default credentials. To disable that, you can either pass in the --no-auto-login flag (example: `pnpm dev my-test-dir --no-auto-login`) or set the `PAYLOAD_PUBLIC_DISABLE_AUTO_LOGIN` environment variable to `false`.
|
||||
By default, payload will [automatically log you in](https://payloadcms.com/docs/authentication/config#admin-autologin) with the default credentials. To disable that, you can either pass in the --no-auto-login flag (example: `pnpm dev my-test-dir --no-auto-login`) or set the `PAYLOAD_PUBLIC_DISABLE_AUTO_LOGIN` environment variable to `false`.
|
||||
|
||||
The default credentials are `dev@payloadcms.com` as E-Mail and `test` as password. These are used in the auto-login.
|
||||
|
||||
@@ -120,21 +120,5 @@ This is how you can preview changes you made locally to the docs:
|
||||
2. Run `yarn install`
|
||||
3. Duplicate the `.env.example` file and rename it to `.env`
|
||||
4. Add a `DOCS_DIR` environment variable to the `.env` file which points to the absolute path of your modified docs folder. For example `DOCS_DIR=/Users/yourname/Documents/GitHub/payload/docs`
|
||||
5. Run `yarn run fetchDocs:local`. If this was successful, you should see no error messages and the following output: _Docs successfully written to /.../website/src/app/docs.json_. There could be error messages if you have incorrect markdown in your local docs folder. In this case, it will tell you how you can fix it
|
||||
5. Run `yarn run fetchDocs:local`. If this was successful, you should see no error messages and the following output: *Docs successfully written to /.../website/src/app/docs.json*. There could be error messages if you have incorrect markdown in your local docs folder. In this case, it will tell you how you can fix it
|
||||
6. You're done! Now you can start the website locally using `yarn run dev` and preview the docs under [http://localhost:3000/docs/](http://localhost:3000/docs/)
|
||||
|
||||
## Internationalization (i18n)
|
||||
|
||||
If your PR adds a string to the UI, we need to make sure to translate it into all the languages that Payload supports. To do that:
|
||||
|
||||
- Find the appropriate internationalization file for your package. These are typically located in `packages/translations/src/languages`, although some packages (e.g., richtext-lexical) have separate i18n files for each feature.
|
||||
- Add the string to the English locale "en".
|
||||
- Translate it to other languages. You can use the `translateNewKeys` script if you have an OpenAI API key in your `.env` (under `OPENAI_KEY`), or you can use ChatGPT or Google translate - whatever is easier for you. For payload core translations (in packages/translations) you can run the `translateNewKeys` script using `cd packages/translations && pnpm translateNewKeys`. For lexical translations, you can run it using `cd packages/richtext-lexical && pnpm translateNewKeys`. External contributors can skip this step and leave it to us.
|
||||
|
||||
To display translation strings in the UI, make sure to use the `t` utility of the `useTranslation` hook:
|
||||
|
||||
```ts
|
||||
const { t } = useTranslation()
|
||||
// ...
|
||||
t('yourStringKey')
|
||||
```
|
||||
|
||||
@@ -45,7 +45,7 @@ There are a couple ways to do this:
|
||||
|
||||
- **Granularly** - you can run individual tests in vscode by installing the Jest Runner plugin and using that to run individual tests. Clicking the `debug` button will run the test in debug mode allowing you to set break points.
|
||||
|
||||
<img src="https://raw.githubusercontent.com/payloadcms/payload/main/packages/payload/src/assets/images/github/int-debug.png" />
|
||||
<img src="https://raw.githubusercontent.com/payloadcms/payload/main/src/admin/assets/images/github/int-debug.png" />
|
||||
|
||||
- **Manually** - you can run all int tests in the `/test/_community/int.spec.ts` file by running the following command:
|
||||
|
||||
@@ -62,7 +62,7 @@ The easiest way to run E2E tests is to install
|
||||
|
||||
Once they are installed you can open the `testing` tab in vscode sidebar and drill down to the test you want to run, i.e. `/test/_community/e2e.spec.ts`
|
||||
|
||||
<img src="https://raw.githubusercontent.com/payloadcms/payload/main/packages/payload/src/assets/images/github/e2e-debug.png" />
|
||||
<img src="https://raw.githubusercontent.com/payloadcms/payload/main/src/admin/assets/images/github/e2e-debug.png" />
|
||||
|
||||
#### Notes
|
||||
|
||||
|
||||
22
LICENSE.md
22
LICENSE.md
@@ -1,22 +0,0 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2018-2025 Payload CMS, Inc. <info@payloadcms.com>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
'Software'), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
75
README.md
75
README.md
@@ -1,60 +1,62 @@
|
||||
<a href="https://payloadcms.com"><img width="100%" src="https://l4wlsi8vxy8hre4v.public.blob.vercel-storage.com/github-banner-new-logo.jpg" alt="Payload headless CMS Admin panel built with React" /></a>
|
||||
<a href="https://payloadcms.com"><img width="100%" src="https://github.com/payloadcms/payload/blob/main/packages/payload/src/admin/assets/images/github-banner-alt.jpg?raw=true" alt="Payload headless CMS Admin panel built with React" /></a>
|
||||
<br />
|
||||
<br />
|
||||
|
||||
<p align="left">
|
||||
<a href="https://github.com/payloadcms/payload/actions"><img alt="GitHub Workflow Status" src="https://img.shields.io/github/actions/workflow/status/payloadcms/payload/main.yml?style=flat-square"></a>
|
||||
|
||||
<a href="https://discord.gg/payload"><img alt="Discord" src="https://img.shields.io/discord/967097582721572934?label=Discord&color=7289da&style=flat-square" /></a>
|
||||
|
||||
<a href="https://www.npmjs.com/package/payload"><img alt="npm" src="https://img.shields.io/npm/dw/payload?style=flat-square" /></a>
|
||||
|
||||
<a href="https://github.com/payloadcms/payload/graphs/contributors"><img alt="npm" src="https://img.shields.io/github/contributors-anon/payloadcms/payload?color=yellow&style=flat-square" /></a>
|
||||
|
||||
<a href="https://www.npmjs.com/package/payload"><img alt="npm" src="https://img.shields.io/npm/v/payload?style=flat-square" /></a>
|
||||
|
||||
<a href="https://twitter.com/payloadcms"><img src="https://img.shields.io/badge/follow-payloadcms-1DA1F2?logo=twitter&style=flat-square" alt="Payload Twitter" /></a>
|
||||
</p>
|
||||
<hr/>
|
||||
<h4>
|
||||
<a target="_blank" href="https://payloadcms.com/docs/getting-started/what-is-payload" rel="dofollow"><strong>Explore the Docs</strong></a> · <a target="_blank" href="https://payloadcms.com/community-help" rel="dofollow"><strong>Community Help</strong></a> · <a target="_blank" href="https://github.com/payloadcms/payload/discussions/1539" rel="dofollow"><strong>Roadmap</strong></a> · <a target="_blank" href="https://www.g2.com/products/payload-cms/reviews#reviews" rel="dofollow"><strong>View G2 Reviews</strong></a>
|
||||
<a target="_blank" href="https://payloadcms.com/docs/getting-started/what-is-payload" rel="dofollow"><strong>Explore the Docs</strong></a> · <a target="_blank" href="https://payloadcms.com/community-help" rel="dofollow"><strong>Community Help</strong></a> · <a target="_blank" href="https://demo.payloadcms.com/" rel="dofollow"><strong>Try Live Demo</strong></a> · <a target="_blank" href="https://github.com/payloadcms/payload/discussions/1539" rel="dofollow"><strong>Roadmap</strong></a> · <a target="_blank" href="https://www.g2.com/products/payload-cms/reviews#reviews" rel="dofollow"><strong>View G2 Reviews</strong></a>
|
||||
</h4>
|
||||
<hr/>
|
||||
|
||||
> [!IMPORTANT]
|
||||
> 🎉 <strong>We've released 3.0!</strong> Star this repo or keep an eye on it to follow along.
|
||||
|
||||
Payload is the first-ever Next.js native CMS that can install directly in your existing `/app` folder. It's the start of a new era for headless CMS.
|
||||
> 🎉 <strong>Payload 2.0 is now available!</strong> Read more in the <a target="_blank" href="https://payloadcms.com/blog/payload-2-0" rel="dofollow"><strong>announcement post</strong></a>.
|
||||
|
||||
<h3>Benefits over a regular CMS</h3>
|
||||
<ul>
|
||||
<li>Deploy anywhere, including serverless on Vercel for free</li>
|
||||
<li>Combine your front+backend in the same <code>/app</code> folder if you want</li>
|
||||
<li>Don't sign up for yet another SaaS - Payload is open source</li>
|
||||
<li>Query your database in React Server Components</li>
|
||||
<li>Both admin and backend are 100% extensible</li>
|
||||
<li>Don’t hit some third-party SaaS API, hit your own API</li>
|
||||
<li>Use your own database and own your data</li>
|
||||
<li>It's just Express - do what you want outside of Payload</li>
|
||||
<li>No need to learn how Payload works - if you know JS, you know Payload</li>
|
||||
<li>No vendor lock-in</li>
|
||||
<li>Avoid microservices hell - get everything (even auth) in one place</li>
|
||||
<li>Never touch ancient WP code again</li>
|
||||
<li>Build faster, never hit a roadblock</li>
|
||||
<li>Both admin and backend are 100% extensible</li>
|
||||
</ul>
|
||||
|
||||
## Quickstart
|
||||
## ☁️ Deploy instantly with Payload Cloud.
|
||||
|
||||
Create a cloud account, connect your GitHub, and [deploy in minutes](https://payloadcms.com/new).
|
||||
|
||||
## 🚀 Get started by self-hosting completely free, forever.
|
||||
|
||||
Before beginning to work with Payload, make sure you have all of the [required software](https://payloadcms.com/docs/getting-started/installation).
|
||||
|
||||
```text
|
||||
pnpx create-payload-app@latest
|
||||
npx create-payload-app@latest
|
||||
```
|
||||
|
||||
**If you're new to Payload, you should start with the website template** (`pnpx create-payload-app@latest -t website`). It shows how to do _everything_ - including custom Rich Text blocks, on-demand revalidation, live preview, and more. It comes with a frontend built with Tailwind all in one `/app` folder.
|
||||
Alternatively, it only takes about five minutes to [create an app from scratch](https://payloadcms.com/docs/getting-started/installation#from-scratch).
|
||||
|
||||
## One-click templates
|
||||
## 🖱️ One-click templates
|
||||
|
||||
Jumpstart your next project by starting with a pre-made template. These are production-ready, end-to-end solutions designed to get you to market as fast as possible.
|
||||
|
||||
### [🛒 E-Commerce](https://github.com/payloadcms/payload/tree/main/templates/ecommerce)
|
||||
|
||||
Eliminate the need to combine Shopify and a CMS, and instead do it all with Payload + Stripe. Comes with a beautiful, fully functional front-end complete with shopping cart, checkout, orders, and much more.
|
||||
|
||||
### [🌐 Website](https://github.com/payloadcms/payload/tree/main/templates/website)
|
||||
|
||||
Build any kind of website, blog, or portfolio from small to enterprise. Comes with a fully functional front-end built with RSCs and Tailwind.
|
||||
Build any kind of website, blog, or portfolio from small to enterprise. Comes with a beautiful, fully functional front-end complete with posts, projects, comments, and much more.
|
||||
|
||||
We're constantly adding more templates to our [Templates Directory](https://github.com/payloadcms/payload/tree/main/templates). If you maintain your own template, consider adding the `payload-template` topic to your GitHub repository for others to find.
|
||||
|
||||
@@ -64,19 +66,20 @@ We're constantly adding more templates to our [Templates Directory](https://gith
|
||||
## ✨ Features
|
||||
|
||||
- Completely free and open-source
|
||||
- Next.js native, built to run inside _your_ `/app` folder
|
||||
- Use server components to extend Payload UI
|
||||
- Query your database directly in server components, no need for REST / GraphQL
|
||||
- Fully TypeScript with automatic types for your data
|
||||
- [Auth out of the box](https://payloadcms.com/docs/authentication/overview)
|
||||
- [Versions and drafts](https://payloadcms.com/docs/versions/overview)
|
||||
- [Localization](https://payloadcms.com/docs/configuration/localization)
|
||||
- [Block-based layout builder](https://payloadcms.com/docs/fields/blocks)
|
||||
- [Customizable React admin](https://payloadcms.com/docs/admin/overview)
|
||||
- [Lexical rich text editor](https://payloadcms.com/docs/fields/rich-text)
|
||||
- [Conditional field logic](https://payloadcms.com/docs/fields/overview#conditional-logic)
|
||||
- [GraphQL](https://payloadcms.com/docs/graphql/overview), [REST](https://payloadcms.com/docs/rest-api/overview), and [Local](https://payloadcms.com/docs/local-api/overview) APIs
|
||||
- [Easily customizable ReactJS Admin](https://payloadcms.com/docs/admin/overview)
|
||||
- [Fully self-hosted](https://payloadcms.com/docs/production/deployment)
|
||||
- [Extensible Authentication](https://payloadcms.com/docs/authentication/overview)
|
||||
- [Local file storage & upload](https://payloadcms.com/docs/upload/overview)
|
||||
- [Version History and Drafts](https://payloadcms.com/docs/versions/overview)
|
||||
- [Field-based Localization](https://payloadcms.com/docs/configuration/localization)
|
||||
- [Block-based Layout Builder](https://payloadcms.com/docs/fields/blocks)
|
||||
- [Extensible SlateJS rich text editor](https://payloadcms.com/docs/fields/rich-text)
|
||||
- [Array field type](https://payloadcms.com/docs/fields/array)
|
||||
- [Field conditional logic](https://payloadcms.com/docs/fields/overview#conditional-logic)
|
||||
- Extremely granular [Access Control](https://payloadcms.com/docs/access-control/overview)
|
||||
- [Document and field-level hooks](https://payloadcms.com/docs/hooks/overview) for every action Payload provides
|
||||
- Built with Typescript & very Typescript-friendly
|
||||
- Intensely fast API
|
||||
- Highly secure thanks to HTTP-only cookies, CSRF protection, and more
|
||||
|
||||
@@ -86,7 +89,7 @@ We're constantly adding more templates to our [Templates Directory](https://gith
|
||||
|
||||
Check out the [Payload website](https://payloadcms.com/docs/getting-started/what-is-payload) to find in-depth documentation for everything that Payload offers.
|
||||
|
||||
Migrating from v2 to v3? Check out the [3.0 Migration Guide](https://github.com/payloadcms/payload/blob/main/docs/migration-guide/overview.mdx) on how to do it.
|
||||
Migrating from v1 to v2? Check out the [2.0 Release Notes](https://github.com/payloadcms/payload/releases/tag/v2.0.0) on how to do it.
|
||||
|
||||
## 🙋 Contributing
|
||||
|
||||
@@ -96,14 +99,6 @@ If you want to add contributions to this repository, please follow the instructi
|
||||
|
||||
The [Examples Directory](./examples) is a great resource for learning how to setup Payload in a variety of different ways, but you can also find great examples in our blog and throughout our social media.
|
||||
|
||||
If you'd like to run the examples, you can use `create-payload-app` to create a project from one:
|
||||
|
||||
```sh
|
||||
npx create-payload-app --example example_name
|
||||
```
|
||||
|
||||
You can see more examples at:
|
||||
|
||||
- [Examples Directory](./examples)
|
||||
- [Payload Blog](https://payloadcms.com/blog)
|
||||
- [Payload YouTube](https://www.youtube.com/@payloadcms)
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
import React from 'react'
|
||||
|
||||
export const metadata = {
|
||||
description: 'Generated by Next.js',
|
||||
title: 'Next.js',
|
||||
}
|
||||
|
||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body>{children}</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
import configPromise from '@payload-config'
|
||||
import { getPayload } from 'payload'
|
||||
|
||||
export const Page = async ({ params, searchParams }) => {
|
||||
const payload = await getPayload({
|
||||
config: configPromise,
|
||||
})
|
||||
return <div>test ${payload?.config?.collections?.length}</div>
|
||||
}
|
||||
|
||||
export default Page
|
||||
@@ -1,25 +1,22 @@
|
||||
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||
import type { Metadata } from 'next'
|
||||
|
||||
import config from '@payload-config'
|
||||
import { generatePageMetadata, NotFoundPage } from '@payloadcms/next/views'
|
||||
|
||||
import { importMap } from '../importMap.js'
|
||||
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||
import { NotFoundPage, generatePageMetadata } from '@payloadcms/next/views'
|
||||
|
||||
type Args = {
|
||||
params: Promise<{
|
||||
params: {
|
||||
segments: string[]
|
||||
}>
|
||||
searchParams: Promise<{
|
||||
}
|
||||
searchParams: {
|
||||
[key: string]: string | string[]
|
||||
}>
|
||||
}
|
||||
}
|
||||
|
||||
export const generateMetadata = ({ params, searchParams }: Args): Promise<Metadata> =>
|
||||
generatePageMetadata({ config, params, searchParams })
|
||||
|
||||
const NotFound = ({ params, searchParams }: Args) =>
|
||||
NotFoundPage({ config, importMap, params, searchParams })
|
||||
const NotFound = ({ params, searchParams }: Args) => NotFoundPage({ config, params, searchParams })
|
||||
|
||||
export default NotFound
|
||||
|
||||
@@ -1,25 +1,22 @@
|
||||
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||
import type { Metadata } from 'next'
|
||||
|
||||
import config from '@payload-config'
|
||||
import { generatePageMetadata, RootPage } from '@payloadcms/next/views'
|
||||
|
||||
import { importMap } from '../importMap.js'
|
||||
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||
import { RootPage, generatePageMetadata } from '@payloadcms/next/views'
|
||||
|
||||
type Args = {
|
||||
params: Promise<{
|
||||
params: {
|
||||
segments: string[]
|
||||
}>
|
||||
searchParams: Promise<{
|
||||
}
|
||||
searchParams: {
|
||||
[key: string]: string | string[]
|
||||
}>
|
||||
}
|
||||
}
|
||||
|
||||
export const generateMetadata = ({ params, searchParams }: Args): Promise<Metadata> =>
|
||||
generatePageMetadata({ config, params, searchParams })
|
||||
|
||||
const Page = ({ params, searchParams }: Args) =>
|
||||
RootPage({ config, importMap, params, searchParams })
|
||||
const Page = ({ params, searchParams }: Args) => RootPage({ config, params, searchParams })
|
||||
|
||||
export default Page
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||
/* DO NOT MODIFY it because it could be re-written at any time. */
|
||||
import config from '@payload-config'
|
||||
import { REST_DELETE, REST_GET, REST_OPTIONS, REST_PATCH, REST_POST } from '@payloadcms/next/routes'
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||
/* DO NOT MODIFY it because it could be re-written at any time. */
|
||||
import config from '@payload-config'
|
||||
import { GRAPHQL_PLAYGROUND_GET } from '@payloadcms/next/routes'
|
||||
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||
/* DO NOT MODIFY it because it could be re-written at any time. */
|
||||
import config from '@payload-config'
|
||||
import { GRAPHQL_POST, REST_OPTIONS } from '@payloadcms/next/routes'
|
||||
import { GRAPHQL_POST } from '@payloadcms/next/routes'
|
||||
|
||||
export const POST = GRAPHQL_POST(config)
|
||||
|
||||
export const OPTIONS = REST_OPTIONS(config)
|
||||
|
||||
@@ -1,32 +1,16 @@
|
||||
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||
import configPromise from '@payload-config'
|
||||
import { RootLayout } from '@payloadcms/next/layouts'
|
||||
// import '@payloadcms/ui/styles.css' // Uncomment this line if `@payloadcms/ui` in `tsconfig.json` points to `/ui/dist` instead of `/ui/src`
|
||||
import type { ServerFunctionClient } from 'payload'
|
||||
|
||||
import config from '@payload-config'
|
||||
import { handleServerFunctions, RootLayout } from '@payloadcms/next/layouts'
|
||||
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||
import React from 'react'
|
||||
|
||||
import { importMap } from './admin/importMap.js'
|
||||
import './custom.scss'
|
||||
|
||||
type Args = {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
const serverFunction: ServerFunctionClient = async function (args) {
|
||||
'use server'
|
||||
return handleServerFunctions({
|
||||
...args,
|
||||
config,
|
||||
importMap,
|
||||
})
|
||||
}
|
||||
|
||||
const Layout = ({ children }: Args) => (
|
||||
<RootLayout config={config} importMap={importMap} serverFunction={serverFunction}>
|
||||
{children}
|
||||
</RootLayout>
|
||||
)
|
||||
const Layout = ({ children }: Args) => <RootLayout config={configPromise}>{children}</RootLayout>
|
||||
|
||||
export default Layout
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
/* eslint-disable no-restricted-exports */
|
||||
'use client'
|
||||
|
||||
import * as Sentry from '@sentry/nextjs'
|
||||
import NextError from 'next/error.js'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
export default function GlobalError({ error }: { error: { digest?: string } & Error }) {
|
||||
useEffect(() => {
|
||||
if (process.env.NEXT_PUBLIC_SENTRY_DSN) {
|
||||
Sentry.captureException(error)
|
||||
}
|
||||
}, [error])
|
||||
|
||||
return (
|
||||
<html lang="en-US">
|
||||
<body>
|
||||
{/* `NextError` is the default Next.js error page component. Its type
|
||||
definition requires a `statusCode` prop. However, since the App Router
|
||||
does not expose status codes for errors, we simply pass 0 to render a
|
||||
generic error message. */}
|
||||
{/* @ts-expect-error types repo */}
|
||||
<NextError statusCode={0} />
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
@@ -6,140 +6,96 @@ desc: With Collection-level Access Control you can define which users can create
|
||||
keywords: collections, access control, permissions, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
|
||||
---
|
||||
|
||||
Collection Access Control is [Access Control](../access-control/overview) used to restrict access to Documents within a [Collection](../getting-started/concepts#collections), as well as what they can and cannot see within the [Admin Panel](../admin/overview) as it relates to that Collection.
|
||||
You can define Collection-level Access Control within each Collection's `access` property. All Access Control functions accept one `args` argument.
|
||||
|
||||
To add Access Control to a Collection, use the `access` property in your [Collection Config](../configuration/collections):
|
||||
|
||||
```ts
|
||||
import type { CollectionConfig } from 'payload';
|
||||
|
||||
export const CollectionWithAccessControl: CollectionConfig = {
|
||||
// ...
|
||||
access: { // highlight-line
|
||||
// ...
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## Config Options
|
||||
|
||||
Access Control is specific to the operation of the request.
|
||||
|
||||
To add Access Control to a Collection, use the `access` property in your [Collection Config](../configuration/collections):
|
||||
|
||||
```ts
|
||||
import type { CollectionConfig } from 'payload';
|
||||
|
||||
export const CollectionWithAccessControl: CollectionConfig = {
|
||||
// ...
|
||||
// highlight-start
|
||||
access: {
|
||||
create: () => {...},
|
||||
read: () => {...},
|
||||
update: () => {...},
|
||||
delete: () => {...},
|
||||
|
||||
// Auth-enabled Collections only
|
||||
admin: () => {...},
|
||||
unlock: () => {...},
|
||||
|
||||
// Version-enabled Collections only
|
||||
readVersions: () => {...},
|
||||
},
|
||||
// highlight-end
|
||||
}
|
||||
```
|
||||
|
||||
The following options are available:
|
||||
## Available Controls
|
||||
|
||||
| Function | Allows/Denies Access |
|
||||
| ----------------------- | -------------------------------------------- |
|
||||
| **`create`** | Used in the `create` operation. [More details](#create). |
|
||||
| **`read`** | Used in the `find` and `findByID` operations. [More details](#read). |
|
||||
| **`update`** | Used in the `update` operation. [More details](#update). |
|
||||
| **`delete`** | Used in the `delete` operation. [More details](#delete). |
|
||||
| **[`create`](#create)** | Used in the `create` operation |
|
||||
| **[`read`](#read)** | Used in the `find` and `findByID` operations |
|
||||
| **[`update`](#update)** | Used in the `update` operation |
|
||||
| **[`delete`](#delete)** | Used in the `delete` operation |
|
||||
|
||||
If a Collection supports [`Authentication`](../authentication/overview), the following additional options are available:
|
||||
#### Auth-enabled Controls
|
||||
|
||||
If a Collection supports [`Authentication`](/docs/authentication/overview), the following Access Controls become available:
|
||||
|
||||
| Function | Allows/Denies Access |
|
||||
| ----------------------- | -------------------------------------------------------------- |
|
||||
| **`admin`** | Used to restrict access to the [Admin Panel](../admin/overview). [More details](#admin). |
|
||||
| **`unlock`** | Used to restrict which users can access the `unlock` operation. [More details](#unlock). |
|
||||
| **[`admin`](#admin)** | Used to restrict access to the [Admin Panel](../admin/overview) |
|
||||
| **[`unlock`](#unlock)** | Used to restrict which users can access the `unlock` operation |
|
||||
|
||||
If a Collection supports [Versions](../versions/overview), the following additional options are available:
|
||||
**Example Collection config:**
|
||||
|
||||
| Function | Allows/Denies Access |
|
||||
| ------------------ | ---------------------------------------------------------------------------------------------------------------------- |
|
||||
| **`readVersions`** | Used to control who can read versions, and who can't. Will automatically restrict the Admin UI version viewing access. [More details](#read-versions). |
|
||||
```ts
|
||||
import type { CollectionConfig } from 'payload';
|
||||
|
||||
export const Posts: CollectionConfig = {
|
||||
slug: "posts",
|
||||
// highlight-start
|
||||
access: {
|
||||
create: ({ req: { user } }) => { ... },
|
||||
read: ({ req: { user } }) => { ... },
|
||||
update: ({ req: { user } }) => { ... },
|
||||
delete: ({ req: { user } }) => { ... },
|
||||
admin: ({ req: { user } }) => { ... },
|
||||
},
|
||||
// highlight-end
|
||||
};
|
||||
```
|
||||
|
||||
### Create
|
||||
|
||||
Returns a boolean which allows/denies access to the `create` request.
|
||||
|
||||
To add create Access Control to a Collection, use the `create` property in the [Collection Config](../configuration/collections):
|
||||
|
||||
```ts
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
export const CollectionWithCreateAccess: CollectionConfig = {
|
||||
// ...
|
||||
access: {
|
||||
// highlight-start
|
||||
create: ({ req: { user }, data }) => {
|
||||
return Boolean(user)
|
||||
},
|
||||
// highlight-end
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
The following arguments are provided to the `create` function:
|
||||
**Available argument properties:**
|
||||
|
||||
| Option | Description |
|
||||
| ---------- | ---------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **`req`** | The [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object containing the currently authenticated `user`. |
|
||||
| **`req`** | The [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object containing the currently authenticated `user` |
|
||||
| **`data`** | The data passed to create the document with. |
|
||||
|
||||
### Read
|
||||
|
||||
Returns a boolean which allows/denies access to the `read` request.
|
||||
|
||||
To add read Access Control to a Collection, use the `read` property in the [Collection Config](../configuration/collections):
|
||||
**Example:**
|
||||
|
||||
```ts
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
export const CollectionWithReadAccess: CollectionConfig = {
|
||||
// ...
|
||||
const PublicUsers = {
|
||||
slug: 'public-users',
|
||||
access: {
|
||||
// highlight-start
|
||||
read: ({ req: { user } }) => {
|
||||
return Boolean(user)
|
||||
},
|
||||
// allow guest users to self-registration
|
||||
create: () => true,
|
||||
// highlight-end
|
||||
...
|
||||
},
|
||||
fields: [ ... ],
|
||||
}
|
||||
```
|
||||
|
||||
<Banner type="success">
|
||||
**Tip:**
|
||||
Return a [Query](../queries/overview) to limit the Documents to only those that match the constraint. This can be helpful to restrict users' access to specific Documents. [More details](../queries/overview).
|
||||
</Banner>
|
||||
### Read
|
||||
|
||||
As your application becomes more complex, you may want to define your function in a separate file and import them into your Collection Config:
|
||||
Read access functions can return a boolean result or optionally return a [query constraint](/docs/queries/overview) which limits the documents that are returned to only those that match the constraint you provide. This can be helpful to restrict users' access to only certain documents however you specify.
|
||||
|
||||
**Available argument properties:**
|
||||
|
||||
| Option | Description |
|
||||
| --------- | -------------------------------------------------------------------------- |
|
||||
| **`req`** | The [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object containing the currently authenticated `user` |
|
||||
| **`id`** | `id` of document requested, if within `findByID` |
|
||||
|
||||
**Example:**
|
||||
|
||||
```ts
|
||||
import type { Access } from 'payload'
|
||||
|
||||
export const canReadPage: Access = ({ req: { user } }) => {
|
||||
// Allow authenticated users
|
||||
const canReadPage: Access = ({ req: { user } }) => {
|
||||
// allow authenticated users
|
||||
if (user) {
|
||||
return true
|
||||
}
|
||||
|
||||
// By returning a Query, guest users can read public Documents
|
||||
// Note: this assumes you have a `isPublic` checkbox field on your Collection
|
||||
// using a query constraint, guest users can access when a field named 'isPublic' is set to true
|
||||
return {
|
||||
// assumes we have a checkbox field named 'isPublic'
|
||||
isPublic: {
|
||||
equals: true,
|
||||
},
|
||||
@@ -147,96 +103,55 @@ export const canReadPage: Access = ({ req: { user } }) => {
|
||||
}
|
||||
```
|
||||
|
||||
The following arguments are provided to the `read` function:
|
||||
|
||||
| Option | Description |
|
||||
| --------- | -------------------------------------------------------------------------- |
|
||||
| **`req`** | The [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object containing the currently authenticated `user`. |
|
||||
| **`id`** | `id` of document requested, if within `findByID`. |
|
||||
|
||||
### Update
|
||||
|
||||
Returns a boolean which allows/denies access to the `update` request.
|
||||
Update access functions can return a boolean result or optionally return a [query constraint](/docs/queries/overview) to limit the document(s) that can be updated by the currently authenticated user. For example, returning a `query` from the `update` Access Control is helpful in cases where you would like to restrict a user to only being able to update the documents containing a `createdBy` relationship field equal to the user's ID.
|
||||
|
||||
To add update Access Control to a Collection, use the `update` property in the [Collection Config](../configuration/collections):
|
||||
**Available argument properties:**
|
||||
|
||||
```ts
|
||||
import type { CollectionConfig } from 'payload'
|
||||
| Option | Description |
|
||||
| ---------- | -------------------------------------------------------------------------- |
|
||||
| **`req`** | The [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object containing the currently authenticated `user` |
|
||||
| **`id`** | `id` of document requested to update |
|
||||
| **`data`** | The data passed to update the document with |
|
||||
|
||||
export const CollectionWithUpdateAccess: CollectionConfig = {
|
||||
// ...
|
||||
access: {
|
||||
// highlight-start
|
||||
update: ({ req: { user }}) => {
|
||||
return Boolean(user)
|
||||
},
|
||||
// highlight-end
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
<Banner type="success">
|
||||
**Tip:**
|
||||
Return a [Query](../queries/overview) to limit the Documents to only those that match the constraint. This can be helpful to restrict users' access to specific Documents. [More details](../queries/overview).
|
||||
</Banner>
|
||||
|
||||
As your application becomes more complex, you may want to define your function in a separate file and import them into your Collection Config:
|
||||
**Example:**
|
||||
|
||||
```ts
|
||||
import type { Access } from 'payload'
|
||||
|
||||
export const canUpdateUser: Access = ({ req: { user }, id }) => {
|
||||
// Allow users with a role of 'admin'
|
||||
const canUpdateUser: Access = ({ req: { user }, id }) => {
|
||||
// allow users with a role of 'admin'
|
||||
if (user.roles && user.roles.some((role) => role === 'admin')) {
|
||||
return true
|
||||
}
|
||||
|
||||
// allow any other users to update only oneself
|
||||
return user.id === id
|
||||
}
|
||||
```
|
||||
|
||||
The following arguments are provided to the `update` function:
|
||||
|
||||
| Option | Description |
|
||||
| ---------- | -------------------------------------------------------------------------- |
|
||||
| **`req`** | The [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object containing the currently authenticated `user`. |
|
||||
| **`id`** | `id` of document requested to update. |
|
||||
| **`data`** | The data passed to update the document with. |
|
||||
|
||||
### Delete
|
||||
|
||||
Similarly to the Update function, returns a boolean or a [query constraint](/docs/queries/overview) to limit which documents can be deleted by which users.
|
||||
|
||||
To add delete Access Control to a Collection, use the `delete` property in the [Collection Config](../configuration/collections):
|
||||
**Available argument properties:**
|
||||
|
||||
```ts
|
||||
import type { CollectionConfig } from 'payload'
|
||||
| Option | Description |
|
||||
| --------- | --------------------------------------------------------------------------------------------------- |
|
||||
| **`req`** | The [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object with additional `user` property, which is the currently logged in user |
|
||||
| **`id`** | `id` of document requested to delete |
|
||||
|
||||
export const CollectionWithDeleteAccess: CollectionConfig = {
|
||||
// ...
|
||||
access: {
|
||||
// highlight-start
|
||||
delete: ({ req: { user }}) => {
|
||||
return Boolean(user)
|
||||
},
|
||||
// highlight-end
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
As your application becomes more complex, you may want to define your function in a separate file and import them into your Collection Config:
|
||||
**Example:**
|
||||
|
||||
```ts
|
||||
import type { Access } from 'payload'
|
||||
|
||||
export const canDeleteCustomer: Access = async ({ req, id }) => {
|
||||
const canDeleteCustomer: Access = async ({ req, id }) => {
|
||||
if (!id) {
|
||||
// allow the admin UI to show controls to delete since it is indeterminate without the `id`
|
||||
// allow the admin UI to show controls to delete since it is indeterminate without the id
|
||||
return true
|
||||
}
|
||||
|
||||
// Query another Collection using the `id`
|
||||
// query another collection using the id
|
||||
const result = await req.payload.find({
|
||||
collection: 'contracts',
|
||||
limit: 0,
|
||||
@@ -250,90 +165,22 @@ export const canDeleteCustomer: Access = async ({ req, id }) => {
|
||||
}
|
||||
```
|
||||
|
||||
The following arguments are provided to the `delete` function:
|
||||
|
||||
| Option | Description |
|
||||
| --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| **`req`** | The [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object with additional `user` property, which is the currently logged in user. |
|
||||
| **`id`** | `id` of document requested to delete. |
|
||||
|
||||
### Admin
|
||||
|
||||
If the Collection is used to access the [Admin Panel](../admin/overview#the-admin-user-collection), the `Admin` Access Control function determines whether or not the currently logged in user can access the admin UI.
|
||||
If the Collection is use to access the [Admin Panel](../admin/overview#the-admin-user-collection), the `Admin` Access Control function determines whether or not the currently logged in user can access the admin UI.
|
||||
|
||||
To add Admin Access Control to a Collection, use the `admin` property in the [Collection Config](../configuration/collections):
|
||||
|
||||
```ts
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
export const CollectionWithAdminAccess: CollectionConfig = {
|
||||
// ...
|
||||
access: {
|
||||
// highlight-start
|
||||
admin: ({ req: { user }}) => {
|
||||
return Boolean(user)
|
||||
},
|
||||
// highlight-end
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
The following arguments are provided to the `admin` function:
|
||||
**Available argument properties:**
|
||||
|
||||
| Option | Description |
|
||||
| --------- | -------------------------------------------------------------------------- |
|
||||
| **`req`** | The [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object containing the currently authenticated `user`. |
|
||||
| **`req`** | The [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object containing the currently authenticated `user` |
|
||||
|
||||
### Unlock
|
||||
|
||||
Determines which users can [unlock](/docs/authentication/operations#unlock) other users who may be blocked from authenticating successfully due to [failing too many login attempts](/docs/authentication/overview#config-options).
|
||||
Determines which users can [unlock](/docs/authentication/operations#unlock) other users who may be blocked from authenticating successfully due to [failing too many login attempts](/docs/authentication/config#options).
|
||||
|
||||
To add Unlock Access Control to a Collection, use the `unlock` property in the [Collection Config](../configuration/collections):
|
||||
|
||||
```ts
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
export const CollectionWithUnlockAccess: CollectionConfig = {
|
||||
// ...
|
||||
access: {
|
||||
// highlight-start
|
||||
unlock: ({ req: { user }}) => {
|
||||
return Boolean(user)
|
||||
},
|
||||
// highlight-end
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
The following arguments are provided to the `unlock` function:
|
||||
**Available argument properties:**
|
||||
|
||||
| Option | Description |
|
||||
| --------- | -------------------------------------------------------------------------- |
|
||||
| **`req`** | The [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object containing the currently authenticated `user`. |
|
||||
|
||||
### Read Versions
|
||||
|
||||
If the Collection has [Versions](../versions/overview) enabled, the `readVersions` Access Control function determines whether or not the currently logged in user can access the version history of a Document.
|
||||
|
||||
To add Read Versions Access Control to a Collection, use the `readVersions` property in the [Collection Config](../configuration/collections):
|
||||
|
||||
```ts
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
export const CollectionWithVersionsAccess: CollectionConfig = {
|
||||
// ...
|
||||
access: {
|
||||
// highlight-start
|
||||
readVersions: ({ req: { user }}) => {
|
||||
return Boolean(user)
|
||||
},
|
||||
// highlight-end
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
The following arguments are provided to the `readVersions` function:
|
||||
|
||||
| Option | Description |
|
||||
| --------- | -------------------------------------------------------------------------- |
|
||||
| **`req`** | The [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object containing the currently authenticated `user`. |
|
||||
| **`req`** | The [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object containing the currently authenticated `user` |
|
||||
|
||||
@@ -1,39 +1,25 @@
|
||||
---
|
||||
title: Field-level Access Control
|
||||
label: Fields
|
||||
order: 40
|
||||
order: 30
|
||||
desc: Field-level Access Control is specified within a field's config, and allows you to define which users can create, read or update Fields.
|
||||
keywords: fields, access control, permissions, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
|
||||
---
|
||||
|
||||
Field Access Control is [Access Control](../access-control/overview) used to restrict access to specific [Fields](../fields/overview) within a Document.
|
||||
Field Access Control is specified with functions inside a field's config. All field-level Controls return a boolean value to allow or deny access for the specified operation. No field-level Access Controls support returning query constraints. All Access Control functions accept one `args` argument.
|
||||
|
||||
To add Access Control to a Field, use the `access` property in your [Field Config](../fields/overview):
|
||||
## Available Controls
|
||||
|
||||
| Function | Purpose |
|
||||
| ----------------------- | -------------------------------------------------------------------------------- |
|
||||
| **[`create`](#create)** | Allows or denies the ability to set a field's value when creating a new document |
|
||||
| **[`read`](#read)** | Allows or denies the ability to read a field's value |
|
||||
| **[`update`](#update)** | Allows or denies the ability to update a field's value |
|
||||
|
||||
**Example Collection config:**
|
||||
|
||||
```ts
|
||||
import type { Field } from 'payload';
|
||||
|
||||
export const FieldWithAccessControl: Field = {
|
||||
// ...
|
||||
access: { // highlight-line
|
||||
// ...
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
<Banner type="warning">
|
||||
**Note:**
|
||||
Field Access Controls does not support returning [Query](../queries/overview) constraints like [Collection Access Control](./collections) does.
|
||||
</Banner>
|
||||
|
||||
## Config Options
|
||||
|
||||
Access Control is specific to the operation of the request.
|
||||
|
||||
To add Access Control to a Field, use the `access` property in the [Field Config](../fields/overview):
|
||||
|
||||
```ts
|
||||
import type { CollectionConfig } from 'payload';
|
||||
import { CollectionConfig } from 'payload';
|
||||
|
||||
export const Posts: CollectionConfig = {
|
||||
slug: 'posts',
|
||||
@@ -53,14 +39,6 @@ export const Posts: CollectionConfig = {
|
||||
};
|
||||
```
|
||||
|
||||
The following options are available:
|
||||
|
||||
| Function | Purpose |
|
||||
| ----------------------- | -------------------------------------------------------------------------------- |
|
||||
| **`create`** | Allows or denies the ability to set a field's value when creating a new document. [More details](#create). |
|
||||
| **`read`** | Allows or denies the ability to read a field's value. [More details](#read). |
|
||||
| **`update`** | Allows or denies the ability to update a field's value [More details](#update). |
|
||||
|
||||
### Create
|
||||
|
||||
Returns a boolean which allows or denies the ability to set a field's value when creating a new document. If `false` is returned, any passed values will be discarded.
|
||||
|
||||
@@ -1,44 +1,35 @@
|
||||
---
|
||||
title: Globals Access Control
|
||||
label: Globals
|
||||
order: 30
|
||||
order: 40
|
||||
desc: Global-level Access Control is specified within each Global's `access` property and allows you to define which users can read or update Globals.
|
||||
keywords: globals, access control, permissions, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
|
||||
---
|
||||
|
||||
Global Access Control is [Access Control](../access-control/overview) used to restrict access to [Global](../configuration/globals) Documents, as well as what they can and cannot see within the [Admin Panel](../admin/overview) as it relates to that Global.
|
||||
You can define Global-level Access Control within each Global's `access` property. All Access Control functions accept one `args` argument.
|
||||
|
||||
To add Access Control to a Global, use the `access` property in your [Global Config](../configuration/globals):
|
||||
## Available Controls
|
||||
|
||||
```ts
|
||||
import type { GlobalConfig } from 'payload';
|
||||
| Function | Allows/Denies Access |
|
||||
| ----------------------- | -------------------------------------- |
|
||||
| **[`read`](#read)** | Used in the `findOne` Global operation |
|
||||
| **[`update`](#update)** | Used in the `update` Global operation |
|
||||
|
||||
export const GlobalWithAccessControl: GlobalConfig = {
|
||||
// ...
|
||||
access: { // highlight-line
|
||||
// ...
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## Config Options
|
||||
|
||||
Access Control is specific to the operation of the request.
|
||||
|
||||
To add Access Control to a [Global](../configuration/globals), use the `access` property in the [Global Config](../configuration/globals):
|
||||
**Example Global config:**
|
||||
|
||||
```ts
|
||||
import { GlobalConfig } from 'payload'
|
||||
|
||||
const GlobalWithAccessControl: GlobalConfig = {
|
||||
// ...
|
||||
const Header: GlobalConfig = {
|
||||
slug: 'header',
|
||||
// highlight-start
|
||||
access: {
|
||||
read: ({ req: { user } }) => {...},
|
||||
update: ({ req: { user } }) => {...},
|
||||
|
||||
// Version-enabled Globals only
|
||||
readVersion: () => {...},
|
||||
read: ({ req: { user } }) => {
|
||||
/* */
|
||||
},
|
||||
update: ({ req: { user } }) => {
|
||||
/* */
|
||||
},
|
||||
},
|
||||
// highlight-end
|
||||
}
|
||||
@@ -46,97 +37,23 @@ const GlobalWithAccessControl: GlobalConfig = {
|
||||
export default Header
|
||||
```
|
||||
|
||||
The following options are available:
|
||||
|
||||
| Function | Allows/Denies Access |
|
||||
| ----------------------- | -------------------------------------- |
|
||||
| **`read`** | Used in the `findOne` Global operation. [More details](#read). |
|
||||
| **`update`** | Used in the `update` Global operation. [More details](#update). |
|
||||
|
||||
If a Global supports [Versions](../versions/overview), the following additional options are available:
|
||||
|
||||
| Function | Allows/Denies Access |
|
||||
| ------------------ | ---------------------------------------------------------------------------------------------------------------------- |
|
||||
| **`readVersions`** | Used to control who can read versions, and who can't. Will automatically restrict the Admin UI version viewing access. [More details](#read-versions). |
|
||||
|
||||
### Read
|
||||
|
||||
Returns a boolean result or optionally a [query constraint](../queries/overview) which limits who can read this global based on its current properties.
|
||||
Returns a boolean result or optionally a [query constraint](/docs/queries/overview) which limits who can read this global based on its current properties.
|
||||
|
||||
To add read Access Control to a [Global](../configuration/globals), use the `read` property in the [Global Config](../configuration/globals):
|
||||
|
||||
```ts
|
||||
import { GlobalConfig } from 'payload'
|
||||
|
||||
const Header: GlobalConfig = {
|
||||
// ...
|
||||
// highlight-start
|
||||
read: {
|
||||
read: ({ req: { user } }) => {
|
||||
return Boolean(user)
|
||||
},
|
||||
}
|
||||
// highlight-end
|
||||
}
|
||||
```
|
||||
|
||||
The following arguments are provided to the `read` function:
|
||||
**Available argument properties:**
|
||||
|
||||
| Option | Description |
|
||||
| --------- | -------------------------------------------------------------------------- |
|
||||
| **`req`** | The [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object containing the currently authenticated `user`. |
|
||||
| **`req`** | The [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object containing the currently authenticated `user` |
|
||||
|
||||
### Update
|
||||
|
||||
Returns a boolean result or optionally a [query constraint](../queries/overview) which limits who can update this global based on its current properties.
|
||||
Returns a boolean result or optionally a [query constraint](/docs/queries/overview) which limits who can update this global based on its current properties.
|
||||
|
||||
To add update Access Control to a [Global](../configuration/globals), use the `access` property in the [Global Config](../configuration/globals):
|
||||
|
||||
```ts
|
||||
import { GlobalConfig } from 'payload'
|
||||
|
||||
const Header: GlobalConfig = {
|
||||
// ...
|
||||
// highlight-start
|
||||
access: {
|
||||
update: ({ req: { user }, data }) => {
|
||||
return Boolean(user)
|
||||
},
|
||||
}
|
||||
// highlight-end
|
||||
}
|
||||
```
|
||||
|
||||
The following arguments are provided to the `update` function:
|
||||
**Available argument properties:**
|
||||
|
||||
| Option | Description |
|
||||
| ---------- | -------------------------------------------------------------------------- |
|
||||
| **`req`** | The [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object containing the currently authenticated `user`. |
|
||||
| **`req`** | The [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object containing the currently authenticated `user` |
|
||||
| **`data`** | The data passed to update the global with. |
|
||||
|
||||
### Read Versions
|
||||
|
||||
If the Global has [Versions](../versions/overview) enabled, the `readVersions` Access Control function determines whether or not the currently logged in user can access the version history of a Document.
|
||||
|
||||
To add Read Versions Access Control to a Collection, use the `readVersions` property in the [Global Config](../configuration/globals):
|
||||
|
||||
```ts
|
||||
import type { GlobalConfig } from 'payload'
|
||||
|
||||
export const GlobalWithVersionsAccess: GlobalConfig = {
|
||||
// ...
|
||||
access: {
|
||||
// highlight-start
|
||||
readVersions: ({ req: { user }}) => {
|
||||
return Boolean(user)
|
||||
},
|
||||
// highlight-end
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
The following arguments are provided to the `readVersions` function:
|
||||
|
||||
| Option | Description |
|
||||
| --------- | -------------------------------------------------------------------------- |
|
||||
| **`req`** | The [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object containing the currently authenticated `user`. |
|
||||
|
||||
@@ -2,78 +2,84 @@
|
||||
title: Access Control
|
||||
label: Overview
|
||||
order: 10
|
||||
desc: Payload makes it simple to define and manage Access Control. By declaring roles, you can set permissions and restrict what your users can interact with.
|
||||
desc: Payload makes it simple to define and manage access control. By declaring roles, you can set permissions and restrict what your users can interact with.
|
||||
keywords: overview, access control, permissions, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
|
||||
---
|
||||
|
||||
Access control within Payload is extremely powerful while remaining easy and intuitive to manage. Declaring who should have access to what documents is no more complex than writing a simple JavaScript function that either returns a `boolean` or a [`query`](/docs/queries/overview) constraint to restrict which documents users can interact with.
|
||||
|
||||
<YouTube id="DoPLyXG26Dg" title="Overview of Payload Access Control" />
|
||||
|
||||
Access Control determines what a user can and cannot do with any given Document, as well as what they can and cannot see within the [Admin Panel](../admin/overview). By implementing Access Control, you can define granular restrictions based on the user, their roles (RBAC), Document data, or any other criteria your application requires.
|
||||
**Example use cases:**
|
||||
|
||||
Access Control functions are scoped to the _operation_, meaning you can have different rules for `create`, `read`, `update`, `delete`, etc. Access Control functions are executed _before_ any changes are made and _before_ any operations are completed. This allows you to determine if the user has the necessary permissions before fulfilling the request.
|
||||
- Allowing anyone `read` access to all `Post`s
|
||||
- Only allowing public access to `Post`s where a `status` field is equal to `published`
|
||||
- Giving only `User`s with a `role` field equal to `admin` the ability to delete `Page`(s)
|
||||
- Allowing anyone to create `ContactSubmission`s, but only logged in users to `read`, `update` or `delete` them
|
||||
- Restricting a `User` to only be able to see their own `Order`(s), but no others
|
||||
- Allowing `User`s that belong to a certain `Organization` to access only that `Organization`'s `Resource`s
|
||||
|
||||
There are many use cases for Access Control, including:
|
||||
## Default Settings
|
||||
|
||||
- Allowing anyone `read` access to all posts
|
||||
- Only allowing public access to posts where a `status` field is equal to `published`
|
||||
- Giving only users with a `role` field equal to `admin` the ability to delete posts
|
||||
- Allowing anyone to submit contact forms, but only logged in users to `read`, `update` or `delete` them
|
||||
- Restricting a user to only be able to see their own orders, but no-one else's
|
||||
- Allowing users that belong to a certain organization to access only that organization's resources
|
||||
**By default, all Collections and Globals require that a user is logged in to be able to interact in any way.** The default Access Control function evaluates the `user` from the `req` and returns `true` if a user is logged in, and `false` if not.
|
||||
|
||||
There are three main types of Access Control in Payload:
|
||||
|
||||
- [Collection Access Control](./collections)
|
||||
- [Global Access Control](./globals)
|
||||
- [Field Access Control](./fields)
|
||||
|
||||
## Default Access Control
|
||||
|
||||
Payload provides default Access Control so that your data is secured behind [Authentication](../authentication/overview) without additional configuration. To do this, Payload sets a default function that simply checks if a user is present on the request. You can override this default behavior by defining your own Access Control functions as needed.
|
||||
|
||||
Here is the default Access Control that Payload provides:
|
||||
**Default Access function:**
|
||||
|
||||
```ts
|
||||
const defaultPayloadAccess = ({ req: { user } }) => {
|
||||
// Return `true` if a user is found
|
||||
// and `false` if it is undefined or null
|
||||
return Boolean(user) // highlight-line
|
||||
return Boolean(user)
|
||||
}
|
||||
```
|
||||
|
||||
<Banner type="warning">
|
||||
**Important:**
|
||||
In the [Local API](../local-api/overview), all Access Control is _skipped_ by default. This allows your server to have full control over your application. To opt back in, you can set the `overrideAccess` option to `false` in your requests.
|
||||
<Banner type="success">
|
||||
<strong>Note:</strong>
|
||||
<br />
|
||||
In the Local API, all Access Control functions are skipped by default, allowing your server to do
|
||||
whatever it needs. But, you can opt back in by setting the option
|
||||
<strong>
|
||||
overrideAccess
|
||||
</strong>
|
||||
{' '}
|
||||
to <strong>false</strong>.
|
||||
</Banner>
|
||||
|
||||
## The Access Operation
|
||||
## Access Control Types
|
||||
|
||||
The Admin Panel responds dynamically to your changes to Access Control. For example, if you restrict editing `ExampleCollection` to only users that feature an "admin" role, Payload will **hide** that Collection from the Admin Panel entirely. This is super powerful and allows you to control who can do what within your Admin Panel using the same functions that secure your APIs.
|
||||
You can manage access within Payload on three different levels:
|
||||
|
||||
To accomplish this, Payload exposes the [Access Operation](../authentication/operations#access). Upon login, Payload executes each Access Control function at the top level, across all Collections, Globals, and Fields, and returns a response that contains a reflection of what the currently authenticated user can do within your application.
|
||||
- [Collections](/docs/access-control/collections)
|
||||
- [Fields](/docs/access-control/fields)
|
||||
- [Globals](/docs/access-control/globals)
|
||||
|
||||
<Banner type="warning">
|
||||
**Important:**
|
||||
When your access control functions are executed via the [Access Operation](../authentication/operations#access), the `id` and `data` arguments will be `undefined`. This is because Payload is executing your functions without referencing a specific Document.
|
||||
## When Access Control is Executed
|
||||
|
||||
<Banner type="success">
|
||||
<strong>Note:</strong>
|
||||
<br />
|
||||
Access control functions are utilized in two places. It's important to understand how and when
|
||||
your access control is executed.
|
||||
</Banner>
|
||||
|
||||
If you use `id` or `data` within your access control functions, make sure to check that they are defined first. If they are not, then you can assume that your Access Control is being executed via the Access Operation to determine solely what the user can do within the Admin Panel.
|
||||
### As you execute operations
|
||||
|
||||
When you perform Payload operations like `create`, `read`, `update`, and `delete`, your access control functions will be executed before any changes or operations are completed.
|
||||
|
||||
## Locale Specific Access Control
|
||||
### Within the Admin UI
|
||||
|
||||
To implement locale-specific access control, you can use the `req.locale` argument in your access control functions. This argument allows you to evaluate the current locale of the request and determine access permissions accordingly.
|
||||
The Payload Admin UI responds dynamically to the access control that you define. For example, if you restrict editing a `ExampleCollection` to only users that feature a `role` of `admin`, the Payload Admin UI will **hide** the `ExampleCollection` from the Admin UI entirely. This is super powerful and allows you to control who can do what with your Admin UI.
|
||||
|
||||
Here is an example:
|
||||
To accomplish this, Payload ships with an `Access` operation, which is executed when a user logs into the Admin UI. Payload will execute each one of your access control functions, across all collections, globals, and fields, at the top level and return a response that contains a reflection of what the currently authenticated user can do with your application.
|
||||
|
||||
```ts
|
||||
const access = ({ req }) => {
|
||||
// Grant access if the locale is 'en'
|
||||
if (req.locale === 'en') {
|
||||
return true;
|
||||
}
|
||||
## Argument Availability
|
||||
|
||||
// Deny access for all other locales
|
||||
return false;
|
||||
}
|
||||
```
|
||||
<Banner type="warning">
|
||||
<strong>Important:</strong>
|
||||
<br />
|
||||
When your access control functions are executed via the <strong>access</strong> operation, the{' '}
|
||||
<strong>id</strong> and <strong>data</strong> arguments will be <strong>undefined</strong>,
|
||||
because Payload is executing your functions without referencing a specific document.
|
||||
</Banner>
|
||||
|
||||
If you use `id` or `data` within your access control functions, make sure to check that they are defined first. If they are not, then you can assume that your access control is being executed via the `access` operation, to determine solely what the user can do within the Admin UI.
|
||||
|
||||
447
docs/admin/components.mdx
Normal file
447
docs/admin/components.mdx
Normal file
@@ -0,0 +1,447 @@
|
||||
---
|
||||
title: Swap in your own React components
|
||||
label: Custom Components
|
||||
order: 20
|
||||
desc: Fully customize your Admin Panel by swapping in your own React components. Add fields, remove views, update routes and change functions to sculpt your perfect Dashboard.
|
||||
keywords: admin, components, custom, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
|
||||
---
|
||||
|
||||
The Payload [Admin Panel](./overview) is designed to be as minimal and straightforward as possible to allow for both easy customization and full control over the UI. In order for Payload to support this level of customization, Payload provides a pattern for you to supply your own React components through your [Payload Config](../configuration/overview).
|
||||
|
||||
All Custom Components in Payload are [React Server Components](https://react.dev/reference/rsc/server-components) by default, with the exception of [Custom Providers](#custom-providers). This enables the use of the [Local API](../local-api) directly on the front-end. Custom Components are available for nearly every part of the Admin Panel for extreme granularity and control.
|
||||
|
||||
<Banner type="success">
|
||||
<strong>Note:</strong>
|
||||
Client Components continue to be fully supported. To use Client Components in your app, simply include the `use client` directive. Payload will automatically detect and remove all default, [non-serializable props](https://react.dev/reference/rsc/use-client#serializable-types) before rendering your component. [More details](#client-components).
|
||||
</Banner>
|
||||
|
||||
There are four main types of Custom Components in Payload:
|
||||
|
||||
- [Root Components](#root-components)
|
||||
- [Collection Components](#collection-components)
|
||||
- [Global Components](#global-components)
|
||||
- [Field Components](./fields)
|
||||
|
||||
To swap in your own Custom Component, consult the list of available components. Determine the scope that corresponds to what you are trying to accomplish, then [author your React component(s)](#building-custom-components) accordingly.
|
||||
|
||||
## Root Components
|
||||
|
||||
Root Components are those that effect the [Admin Panel](./overview) generally, such as the logo or the main nav.
|
||||
|
||||
To override Root Components, use the `admin.components` property in your [Payload Config](../getting-started/overview):
|
||||
|
||||
```ts
|
||||
import { buildConfig } from 'payload'
|
||||
|
||||
import { MyCustomLogo } from './MyCustomLogo'
|
||||
|
||||
export default buildConfig({
|
||||
// ...
|
||||
admin: {
|
||||
components: {
|
||||
graphics: {
|
||||
Logo: MyCustomLogo, // highlight-line
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
_For details on how to build Custom Components, see [Building Custom Components](#building-custom-components)._
|
||||
|
||||
The following options are available:
|
||||
|
||||
| Path | Description |
|
||||
|-----------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **`Nav`** | Contains the sidebar / mobile menu in its entirety. |
|
||||
| **`beforeNavLinks`** | An array of Custom Components to inject into the built-in Nav, _before_ the links themselves. |
|
||||
| **`afterNavLinks`** | An array of Custom Components to inject into the built-in Nav, _after_ the links. |
|
||||
| **`beforeDashboard`** | An array of Custom Components to inject into the built-in Dashboard, _before_ the default dashboard contents. |
|
||||
| **`afterDashboard`** | An array of Custom Components to inject into the built-in Dashboard, _after_ the default dashboard contents. |
|
||||
| **`beforeLogin`** | An array of Custom Components to inject into the built-in Login, _before_ the default login form. |
|
||||
| **`afterLogin`** | An array of Custom Components to inject into the built-in Login, _after_ the default login form. |
|
||||
| **`logout.Button`** | The button displayed in the sidebar that logs the user out. |
|
||||
| **`graphics.Icon`** | The simplified logo used in contexts like the the `Nav` component. |
|
||||
| **`graphics.Logo`** | The full logo used in contexts like the `Login` view. |
|
||||
| **`providers`** | Custom [React Context](https://react.dev/learn/scaling-up-with-reducer-and-context) providers that will wrap the entire [Admin Panel](./overview). [More details](#custom-providers). |
|
||||
| **`actions`** | An array of Custom Components to be rendered in the header of the [Admin Panel](./overview), providing additional interactivity and functionality. |
|
||||
| **`views`** | Override or create new views within the [Admin Panel](./overview). [More details](./views). |
|
||||
|
||||
<Banner type="success">
|
||||
<strong>Note:</strong> You can also use the `admin.components` property in your _[Collection](#collection-components) or [Global](#global-components)_.
|
||||
</Banner>
|
||||
|
||||
### Custom Providers
|
||||
|
||||
As you add more and more Custom Components to your [Admin Panel](./overview), you may find it helpful to add additional [React Context](https://react.dev/learn/scaling-up-with-reducer-and-context)(s). Payload allows you to inject your own context providers in your app where you can export your own custom hooks, etc.
|
||||
|
||||
To add a Custom Provider, use the `admin.components.providers` property in your [Payload Config](../getting-started/overview):
|
||||
|
||||
```ts
|
||||
import { buildConfig } from 'payload'
|
||||
|
||||
import { MyProvider } from './MyProvider'
|
||||
|
||||
export default buildConfig({
|
||||
// ...
|
||||
admin: {
|
||||
components: {
|
||||
providers: [MyProvider], // highlight-line
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
Then build your Custom Provider as follows:
|
||||
|
||||
```tsx
|
||||
'use client'
|
||||
import React, { createContext, useContext } from 'react'
|
||||
|
||||
const MyCustomContext = React.createContext(myCustomValue)
|
||||
|
||||
export const MyProvider: React.FC = ({ children }) => {
|
||||
return (
|
||||
<MyCustomContext.Provider value={myCustomValue}>
|
||||
{children}
|
||||
</MyCustomContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const useMyCustomContext = () => useContext(MyCustomContext)
|
||||
```
|
||||
|
||||
<Banner type="warning">
|
||||
<strong>Reminder:</strong> Custom Providers are by definition Client Components. This means they must include the `use client` directive at the top of their files and cannot use server-only code.
|
||||
</Banner>
|
||||
|
||||
## Collection Components
|
||||
|
||||
Collection Components are those that effect [Collection](../configuration/collections)-specific UI within the [Admin Panel](./overview), such as the save button or the List View.
|
||||
|
||||
To override Collection Components, use the `admin.components` property in your [Collection Config](../configuration/collections):
|
||||
|
||||
```ts
|
||||
import type { SanitizedCollectionConfig } from 'payload'
|
||||
import { CustomSaveButton } from './CustomSaveButton'
|
||||
|
||||
export const MyCollection: SanitizedCollectionConfig = {
|
||||
slug: 'my-collection',
|
||||
admin: {
|
||||
components: {
|
||||
edit: {
|
||||
SaveButton: CustomSaveButton, // highlight-line
|
||||
},
|
||||
},
|
||||
},
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
_For details on how to build Custom Components, see [Building Custom Components](#building-custom-components)._
|
||||
|
||||
The following options are available:
|
||||
|
||||
| Path | Description |
|
||||
| -------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
|
||||
| **`beforeList`** | An array of components to inject _before_ the built-in List View |
|
||||
| **`beforeListTable`** | An array of components to inject _before_ the built-in List View's table |
|
||||
| **`afterList`** | An array of components to inject _after_ the built-in List View |
|
||||
| **`afterListTable`** | An array of components to inject _after_ the built-in List View's table |
|
||||
| **`edit.SaveButton`** | Replace the default `Save` button with a Custom Component. Drafts must be disabled |
|
||||
| **`edit.SaveDraftButton`** | Replace the default `Save Draft` button with a Custom Component. Drafts must be enabled and autosave must be disabled. |
|
||||
| **`edit.PublishButton`** | Replace the default `Publish` button with a Custom Component. Drafts must be enabled. |
|
||||
| **`edit.PreviewButton`** | Replace the default `Preview` button with a Custom Component. |
|
||||
| **`views`** | Override or create new views within the [Admin Panel](./overview). [More details](./views). |
|
||||
|
||||
## Global Components
|
||||
|
||||
Global Components are those that effect [Global](../configuration/globals)-specific UI within the [Admin Panel](./overview), such as the save button or the Edit View.
|
||||
|
||||
To override Global Components, use the `admin.components` property in your [Global Config](../configuration/globals):
|
||||
|
||||
```ts
|
||||
import type { SanitizedGlobalConfig } from 'payload'
|
||||
import { CustomSaveButton } from './CustomSaveButton'
|
||||
|
||||
export const MyGlobal: SanitizedGlobalConfig = {
|
||||
slug: 'my-global',
|
||||
admin: {
|
||||
components: {
|
||||
elements: {
|
||||
SaveButton: CustomSaveButton, // highlight-line
|
||||
},
|
||||
},
|
||||
},
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
_For details on how to build Custom Components, see [Building Custom Components](#building-custom-components)._
|
||||
|
||||
The following options are available:
|
||||
|
||||
| Path | Description |
|
||||
| ------------------------------ | ---------------------------------------------------------------------------------------------------------------------- |
|
||||
| **`elements.SaveButton`** | Replace the default `Save` button with a Custom Component. Drafts must be disabled. |
|
||||
| **`elements.SaveDraftButton`** | Replace the default `Save Draft` button with a Custom Component. Drafts must be enabled and autosave must be disabled. |
|
||||
| **`elements.PublishButton`** | Replace the default `Publish` button with a Custom Component. Drafts must be enabled. |
|
||||
| **`elements.PreviewButton`** | Replace the default `Preview` button with a Custom Component. |
|
||||
| **`views`** | Override or create new views within the [Admin Panel](./overview). [More details](./views). |
|
||||
|
||||
## Building Custom Components
|
||||
|
||||
All Custom Components in Payload are [React Server Components](https://react.dev/reference/rsc/server-components) by default, with the exception of [Custom Providers](#custom-providers). This enables the use of the [Local API](../local-api) directly on the front-end, among other things.
|
||||
|
||||
To make building Custom Components as easy as possible, Payload automatically provides common props, such as the [`payload`](../local-api/overview) class and the [`i18n`](../configuration/i18n) object. This means that when building Custom Components within the Admin Panel, you do not have to get these yourself.
|
||||
|
||||
Here is an example:
|
||||
|
||||
```tsx
|
||||
import React from 'react'
|
||||
|
||||
const MyServerComponent = async ({
|
||||
payload // highlight-line
|
||||
}) => {
|
||||
const page = await payload.findByID({
|
||||
collection: 'pages',
|
||||
id: '123',
|
||||
})
|
||||
|
||||
return (
|
||||
<p>{page.title}</p>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
Each Custom Component receives the following props by default:
|
||||
|
||||
| Prop | Description |
|
||||
| ------------------------- | ----------------------------------------------------------------------------------------------------- |
|
||||
| `payload` | The [Payload](../local-api/overview) class. |
|
||||
| `i18n` | The [i18n](../i18n) object. |
|
||||
|
||||
Custom Components also receive various other props that are specific to the context in which the Custom Component is being rendered. For example, [Custom Views](./views) receive the `user` prop. For a full list of available props, consult the documentation related to the specific component you are working with.
|
||||
|
||||
<Banner type="success">
|
||||
See [Root Components](#root-components), [Collection Components](#collection-components), [Global Components](#global-components), or [Field Components](#custom-field-components) for a complete list of all available components.
|
||||
</Banner>
|
||||
|
||||
### Client Components
|
||||
|
||||
When [Building Custom Components](#building-custom-components), it's still possible to use client-side code such as `useState` or the `window` object. To do this, simply add the `use client` directive at the top of your file. Payload will automatically detect and remove all default, [non-serializable props](https://react.dev/reference/rsc/use-client#serializable-types) before rendering your component.
|
||||
|
||||
```tsx
|
||||
'use client' // highlight-line
|
||||
import React, { useState } from 'react'
|
||||
|
||||
export const MyClientComponent: React.FC = () => {
|
||||
const [count, setCount] = useState(0)
|
||||
|
||||
return (
|
||||
<button onClick={() => setCount(count + 1)}>
|
||||
Clicked {count} times
|
||||
</button>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
<Banner type="warning">
|
||||
<strong>Reminder:</strong>
|
||||
Client Components cannot be passed [non-serializable props](https://react.dev/reference/rsc/use-client#serializable-types). If you are rendering your Client Component _from within_ a Server Component, ensure that its props are serializable.
|
||||
</Banner>
|
||||
|
||||
### Accessing the Payload Config
|
||||
|
||||
From any Server Component, the [Payload Config](../configuration/overview) can be accessed directly from the `payload` prop:
|
||||
|
||||
```tsx
|
||||
import React from 'react'
|
||||
|
||||
export default async function MyServerComponent({
|
||||
payload: {
|
||||
config // highlight-line
|
||||
}
|
||||
}) {
|
||||
return (
|
||||
<Link href={config.serverURL}>
|
||||
Go Home
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
But, the Payload Config is [non-serializable](https://react.dev/reference/rsc/use-client#serializable-types) by design. It is full of custom validation functions, React components, etc. This means that the Payload Config, in its entirety, cannot be passed directly to Client Components.
|
||||
|
||||
For this reason, Payload creates a Client Config and passes it into the Config Provider. This is a serializable version of the Payload Config that can be accessed from any Client Component via the [`useConfig`](./hooks#useconfig) hook:
|
||||
|
||||
```tsx
|
||||
import React from 'react'
|
||||
import { useConfig } from '@payloadcms/ui'
|
||||
|
||||
export const MyClientComponent: React.FC = () => {
|
||||
const { serverURL } = useConfig() // highlight-line
|
||||
|
||||
return (
|
||||
<Link href={serverURL}>
|
||||
Go Home
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
<Banner type="success">
|
||||
See [Using Hooks](#using-hooks) for more details.
|
||||
</Banner>
|
||||
|
||||
### Using Hooks
|
||||
|
||||
To make it easier to [build your Custom Components](#building-custom-components), you can use [Payload's built-in React Hooks](./hooks) in any Client Component. For example, you might want to interact with one of Payload's many React Contexts:
|
||||
|
||||
```tsx
|
||||
'use client'
|
||||
import React from 'react'
|
||||
import { useDocumentInfo } from '@payloadcms/ui'
|
||||
|
||||
export const MyClientComponent: React.FC = () => {
|
||||
const { slug } = useDocumentInfo() // highlight-line
|
||||
|
||||
return (
|
||||
<p>{`Entity slug: ${slug}`}</p>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
<Banner type="success">
|
||||
See the [Hooks](./hooks) documentation for a full list of available hooks.
|
||||
</Banner>
|
||||
|
||||
### Getting the Current Language
|
||||
|
||||
All Custom Components can support multiple languages to be consistent with Payload's [Internationalization](../configuration/i18n). To do this, first add your translation resources to the [I18n Config](../configuration/i18n).
|
||||
|
||||
From any Server Component, you can translate resources using the `getTranslation` function from `@payloadcms/translations`. All Server Components automatically receive the `i18n` object as a prop by default.
|
||||
|
||||
```tsx
|
||||
import React from 'react'
|
||||
import { getTranslation } from '@payloadcms/translations'
|
||||
|
||||
export default async function MyServerComponent({ i18n }) {
|
||||
const translatedTitle = getTranslation(myTranslation, i18n) // highlight-line
|
||||
|
||||
return (
|
||||
<p>{translatedTitle}</p>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
The best way to do this within a Client Component is to import the `useTranslation` hook from `@payloadcms/ui`:
|
||||
|
||||
```tsx
|
||||
import React from 'react'
|
||||
import { useTranslation } from '@payloadcms/ui'
|
||||
|
||||
export const MyClientComponent: React.FC = () => {
|
||||
const { t, i18n } = useTranslation() // highlight-line
|
||||
|
||||
return (
|
||||
<ul>
|
||||
<li>{t('namespace1:key', { variable: 'value' })}</li>
|
||||
<li>{t('namespace2:key', { variable: 'value' })}</li>
|
||||
<li>{i18n.language}</li>
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
<Banner type="success">
|
||||
See the [Hooks](./hooks) documentation for a full list of available hooks.
|
||||
</Banner>
|
||||
|
||||
### Getting the Current Locale
|
||||
|
||||
All [Custom Views](./views) can support multiple locales to be consistent with Payload's [Localization](../configuration/localization). They automatically receive the `locale` object as a prop by default. This can be used to scope API requests, etc.:
|
||||
|
||||
```tsx
|
||||
import React from 'react'
|
||||
|
||||
export default async function MyServerComponent({ payload, locale }) {
|
||||
const localizedPage = await payload.findByID({
|
||||
collection: 'pages',
|
||||
id: '123',
|
||||
locale,
|
||||
})
|
||||
|
||||
return (
|
||||
<p>{localizedPage.title}</p>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
The best way to do this within a Client Component is to import the `useLocale` hook from `@payloadcms/ui`:
|
||||
|
||||
```tsx
|
||||
import React from 'react'
|
||||
import { useLocale } from '@payloadcms/ui'
|
||||
|
||||
const Greeting: React.FC = () => {
|
||||
const locale = useLocale() // highlight-line
|
||||
|
||||
const trans = {
|
||||
en: 'Hello',
|
||||
es: 'Hola',
|
||||
}
|
||||
|
||||
return (
|
||||
<span>{trans[locale.code]}</span>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
<Banner type="success">
|
||||
See the [Hooks](./hooks) documentation for a full list of available hooks.
|
||||
</Banner>
|
||||
|
||||
### Styling Custom Components
|
||||
|
||||
Payload has a robust [CSS Library](./customizing-css) that you can use to style your Custom Components similarly to Payload's built-in styling. This will ensure that your Custom Components match the existing design system, and so that they automatically adapt to any theme changes that might occur.
|
||||
|
||||
To apply custom styles, simply import your own `.css` or `.scss` file into your Custom Component:
|
||||
|
||||
```tsx
|
||||
import './index.scss'
|
||||
|
||||
export const MyComponent: React.FC = () => {
|
||||
return (
|
||||
<div className="my-component">
|
||||
My Custom Component
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
Then to colorize your Custom Component's background, for example, you can use the following CSS:
|
||||
|
||||
```scss
|
||||
.my-component {
|
||||
background-color: var(--theme-elevation-500);
|
||||
}
|
||||
```
|
||||
|
||||
Payload also exports its [SCSS](https://sass-lang.com) library for reuse which includes mixins, etc. To use this, simply import it as follows into your `.scss` file:
|
||||
|
||||
```scss
|
||||
@import '~payload/scss';
|
||||
|
||||
.my-component {
|
||||
@include mid-break {
|
||||
background-color: var(--theme-elevation-900);
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<Banner type="success">
|
||||
<strong>Note:</strong>
|
||||
You can also drill into Payload's own component styles, or easily apply global, app-wide CSS. More on that [here](./customizing-css).
|
||||
</Banner>
|
||||
@@ -1,14 +1,14 @@
|
||||
---
|
||||
title: Customizing CSS & SCSS
|
||||
label: Customizing CSS
|
||||
order: 50
|
||||
order: 60
|
||||
desc: Customize the Payload Admin Panel further by adding your own CSS or SCSS style sheet to the configuration, powerful theme and design options are waiting for you.
|
||||
keywords: admin, css, scss, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
|
||||
---
|
||||
|
||||
Customizing the Payload [Admin Panel](./overview) through CSS alone is one of the easiest and most powerful ways to customize the look and feel of the dashboard. To allow for this level of customization, Payload:
|
||||
|
||||
1. Exposes a [root-level stylesheet](#global-css) for you to inject custom selectors
|
||||
1. Exposes a [root-level stylesheet](#global-css) for you to easily to inject custom selectors
|
||||
1. Provides a [CSS library](#css-library) that can be easily overridden or extended
|
||||
1. Uses [BEM naming conventions](http://getbem.com) so that class names are globally accessible
|
||||
|
||||
@@ -29,23 +29,10 @@ Here is an example of how you might target the Dashboard View and change the bac
|
||||
```
|
||||
|
||||
<Banner type="warning">
|
||||
**Note:**
|
||||
If you are building [Custom Components](../custom-components/overview), it is best to import your own stylesheets directly into your components, rather than using the global stylesheet. You can continue to use the [CSS library](#css-library) as needed.
|
||||
<strong>Note:</strong>
|
||||
If you are building [Custom Components](./overview), it is best to import your own stylesheets directly into your components, rather than using the global stylesheet. You can continue to use the [CSS library](#css-library) as needed.
|
||||
</Banner>
|
||||
|
||||
### Specificity rules
|
||||
|
||||
All Payload CSS is encapsulated inside CSS layers under `@layer payload-default`. Any custom css will now have the highest possible specificity.
|
||||
|
||||
We have also provided a layer `@layer payload` if you want to use layers and ensure that your styles are applied after payload.
|
||||
|
||||
To override existing styles in a way that the previous rules of specificity would be respected you can use the default layer like so
|
||||
```css
|
||||
@layer payload-default {
|
||||
// my styles within the payload specificity
|
||||
}
|
||||
```
|
||||
|
||||
## Re-using Payload SCSS variables and utilities
|
||||
|
||||
You can re-use Payload's SCSS variables and utilities in your own stylesheets by importing it from the UI package.
|
||||
@@ -62,13 +49,13 @@ You can also override Payload's built-in [CSS Variables](https://developer.mozil
|
||||
|
||||
The following variables are defined and can be overridden:
|
||||
|
||||
- [Breakpoints](https://github.com/payloadcms/payload/blob/main/packages/ui/src/scss/queries.scss)
|
||||
- [Colors](https://github.com/payloadcms/payload/blob/main/packages/ui/src/scss/colors.scss)
|
||||
- [Breakpoints](https://github.com/payloadcms/payload/blob/beta/packages/ui/src/scss/queries.scss)
|
||||
- [Colors](https://github.com/payloadcms/payload/blob/beta/packages/ui/src/scss/colors.scss)
|
||||
- Base color shades (white to black by default)
|
||||
- Success / warning / error color shades
|
||||
- Theme-specific colors (background, input background, text color, etc.)
|
||||
- Elevation colors (used to determine how "bright" something should be when compared to the background)
|
||||
- [Sizing](https://github.com/payloadcms/payload/blob/main/packages/ui/src/scss/app.scss)
|
||||
- [Sizing](https://github.com/payloadcms/payload/blob/beta/packages/ui/src/scss/app.scss)
|
||||
- Horizontal gutter
|
||||
- Transition speeds
|
||||
- Font sizes
|
||||
@@ -77,7 +64,7 @@ The following variables are defined and can be overridden:
|
||||
For an up-to-date, comprehensive list of all available variables, please refer to the [Source Code](https://github.com/payloadcms/payload/blob/main/packages/ui/src/scss).
|
||||
|
||||
<Banner type="warning">
|
||||
**Warning:**
|
||||
<strong>Warning:</strong>
|
||||
If you're overriding colors or theme elevations, make sure to consider how [your changes will affect dark mode](#dark-mode).
|
||||
</Banner>
|
||||
|
||||
|
||||
488
docs/admin/fields.mdx
Normal file
488
docs/admin/fields.mdx
Normal file
@@ -0,0 +1,488 @@
|
||||
---
|
||||
title: Customizing Fields
|
||||
label: Customizing Fields
|
||||
order: 40
|
||||
desc:
|
||||
keywords:
|
||||
---
|
||||
|
||||
[Fields](../fields/overview) within the [Admin Panel](./overview) can be endlessly customized in their appearance and behavior without affecting their underlying data structure. Fields are designed to withstand heavy modification or even complete replacement through the use of [Custom Field Components](#field-components), [Conditional Logic](#conditional-logic), [Custom Validations](../fields/overview#validation), and more.
|
||||
|
||||
For example, your app might need to render a specific interface that Payload does not inherently support, such as a color picker. To do this, you could replace the default [Text Field](../fields/text) input with your own user-friendly component that formats the data into a valid color value.
|
||||
|
||||
<Banner type="success">
|
||||
<strong>Tip:</strong>
|
||||
Don't see a built-in field type that you need? Build it! Using a combination of [Field Validations](../fields/overview#validation)
|
||||
and [Custom Components](./components), you can override the entirety of how a component functions within the [Admin Panel](./overview) to effectively create your own field type.
|
||||
</Banner>
|
||||
|
||||
## Admin Options
|
||||
|
||||
You can customize the appearance and behavior of fields within the [Admin Panel](./overvieW) through the `admin` property of any [Field Config](../fields/overview):
|
||||
|
||||
```ts
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
export const CollectionConfig: CollectionConfig = {
|
||||
// ...
|
||||
fields: [
|
||||
// ...
|
||||
{
|
||||
name: 'myField',
|
||||
type: 'text',
|
||||
admin: { // highlight-line
|
||||
// ...
|
||||
},
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
The following options are available:
|
||||
|
||||
| Option | Description |
|
||||
| ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **`condition`** | Programmatically show / hide fields based on other fields. [More details](../admin/fields#conditional-logic). |
|
||||
| **`components`** | All Field Components can be swapped out for [Custom Components](../admin/components) that you define. [More details](../admin/fields). |
|
||||
| **`description`** | Helper text to display alongside the field to provide more information for the editor. [More details](../admin/fields#description). |
|
||||
| **`position`** | Specify if the field should be rendered in the sidebar by defining `position: 'sidebar'`. |
|
||||
| **`width`** | Restrict the width of a field. You can pass any string-based value here, be it pixels, percentages, etc. This property is especially useful when fields are nested within a `Row` type where they can be organized horizontally. |
|
||||
| **`style`** | [CSS Properties](https://developer.mozilla.org/en-US/docs/Web/CSS) to inject into the root element of the field. |
|
||||
| **`className`** | Attach a [CSS class attribute](https://developer.mozilla.org/en-US/docs/Web/CSS/Class_selectors) to the root DOM element of a field. |
|
||||
| **`readOnly`** | Setting a field to `readOnly` has no effect on the API whatsoever but disables the admin component's editability to prevent editors from modifying the field's value. |
|
||||
| **`disabled`** | If a field is `disabled`, it is completely omitted from the [Admin Panel](../admin/overview). |
|
||||
| **`disableBulkEdit`** | Set `disableBulkEdit` to `true` to prevent fields from appearing in the select options when making edits for multiple documents. |
|
||||
| **`disableListColumn`** | Set `disableListColumn` to `true` to prevent fields from appearing in the list view column selector. |
|
||||
| **`disableListFilter`** | Set `disableListFilter` to `true` to prevent fields from appearing in the list view filter options. |
|
||||
| **`hidden`** | Will transform the field into a `hidden` input type. Its value will still submit with requests in the Admin Panel, but the field itself will not be visible to editors. |
|
||||
|
||||
## Field Components
|
||||
|
||||
Within the [Admin Panel](./overview), fields are rendered in three distinct places:
|
||||
|
||||
- [Field](#the-field-component) - The actual form field rendered in the Edit View.
|
||||
- [Cell](#the-cell-component) - The table cell component rendered in the List View.
|
||||
- [Filter](#the-filter-component) - The filter component rendered in the List View.
|
||||
|
||||
To easily swap in Field Components with your own, use the `admin.components` property in your [Field Config](../fields/overview):
|
||||
|
||||
```ts
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
export const CollectionConfig: CollectionConfig = {
|
||||
// ...
|
||||
fields: [
|
||||
// ...
|
||||
{
|
||||
// ...
|
||||
admin: {
|
||||
components: { // highlight-line
|
||||
// ...
|
||||
},
|
||||
},
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
The following options are available:
|
||||
|
||||
| Component | Description |
|
||||
| ---------- | --------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **`Field`** | The form field rendered of the Edit View. [More details](#the-field-component). |
|
||||
| **`Cell`** | The table cell rendered of the List View. [More details](#the-cell-component). |
|
||||
| **`Filter`** | The filter component rendered in the List View. [More details](#the-filter-component). || Component | Description |
|
||||
| **`Label`** | Override the default Label of the Field Component. [More details](#the-label-component). |
|
||||
| **`Error`** | Override the default Error of the Field Component. [More details](#the-error-component). |
|
||||
| **`Description`** | Override the default Description of the Field Component. [More details](#the-description-component). |
|
||||
| **`beforeInput`** | An array of elements that will be added before the input of the Field Component. [More details](#afterinput-and-beforeinput).|
|
||||
| **`afterInput`** | An array of elements that will be added after the input of the Field Component. [More details](#afterinput-and-beforeinput). |
|
||||
|
||||
_\* **`beforeInput`** and **`afterInput`** are only supported in fields that do not contain other fields, such as [`Text`](../fields/text), and [`Textarea`](../fields/textarea)._
|
||||
|
||||
### The Field Component
|
||||
|
||||
The Field Component is the actual form field rendered in the Edit View. This is the input that user's will interact with when editing a document.
|
||||
|
||||
To easily swap in your own Field Component, use the `admin.components.Field` property in your [Field Config](../fields/overview):
|
||||
|
||||
```ts
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
export const CollectionConfig: CollectionConfig = {
|
||||
// ...
|
||||
fields: [
|
||||
// ...
|
||||
{
|
||||
// ...
|
||||
admin: {
|
||||
components: {
|
||||
Field: MyFieldComponent, // highlight-line
|
||||
},
|
||||
},
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
_For details on how to build Custom Components, see [Building Custom Components](./components#building-custom-components)._
|
||||
|
||||
<Banner type="warning">
|
||||
Instead of replacing the entire Field Component, you can alternately replace or slot-in only specific parts by using the [`Label`](#the-label-component), [`Error`](#the-error-component), [`beforeInput`](#afterinput-and-beforinput), and [`afterInput`](#afterinput-and-beforinput) properties.
|
||||
</Banner>
|
||||
|
||||
All Field Components receive the following props:
|
||||
|
||||
| Property | Description |
|
||||
| ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **`AfterInput`** | The rendered result of the `admin.components.afterInput` property. [More details](#afterinput-and-beforeinput). |
|
||||
| **`BeforeInput`** | The rendered result of the `admin.components.beforeInput` property. [More details](#afterinput-and-beforeinput). |
|
||||
| **`CustomDescription`** | The rendered result of the `admin.components.Description` property. [More details](#the-description-component). |
|
||||
| **`CustomError`** | The rendered result of the `admin.components.Error` property. [More details](#the-error-component). |
|
||||
| **`CustomLabel`** | The rendered result of the `admin.components.Label` property. [More details](#the-label-component).
|
||||
| **`path`** | The static path of the field at render time. [More details](./hooks#usefieldprops). |
|
||||
| **`disabled`** | The `admin.disabled` property defined in the [Field Config](../fields/overview). |
|
||||
| **`required`** | The `admin.required` property defined in the [Field Config](../fields/overview). |
|
||||
| **`className`** | The `admin.className` property defined in the [Field Config](../fields/overview). |
|
||||
| **`style`** | The `admin.style` property defined in the [Field Config](../fields/overview). |
|
||||
| **`custom`** | The `admin.custom` property defined in the [Field Config](../fields/overview).
|
||||
| **`placeholder`** | The `admin.placeholder` property defined in the [Field Config](../fields/overview). |
|
||||
| **`descriptionProps`** | An object that contains the props for the `FieldDescription` component. |
|
||||
| **`labelProps`** | An object that contains the props needed for the `FieldLabel` component. |
|
||||
| **`errorProps`** | An object that contains the props for the `FieldError` component. |
|
||||
| **`docPreferences`** | An object that contains the preferences for the document. |
|
||||
| **`label`** | The label value provided in the field, it can be used with i18n. |
|
||||
| **`locale`** | The locale of the field. [More details](../configuration/localization). |
|
||||
| **`localized`** | A boolean value that represents if the field is localized or not. [More details](../fields/localized). |
|
||||
| **`readOnly`** | A boolean value that represents if the field is read-only or not. |
|
||||
| **`rtl`** | A boolean value that represents if the field should be rendered right-to-left or not. [More details](../configuration/i18n). |
|
||||
| **`user`** | The currently authenticated user. [More details](../authentication/overview). |
|
||||
| **`validate`** | A function that can be used to validate the field. |
|
||||
| **`hasMany`** | If a [`relationship`](../fields/relationship) field, the `hasMany` property defined in the [Field Config](../fields/overview). |
|
||||
| **`maxLength`** | If a [`text`](../fields/text) field, the `maxLength` property defined in the [Field Config](../fields/overview). |
|
||||
| **`minLength`** | If a [`text`](../fields/text) field, the `minLength` property defined in the [Field Config](../fields/overview). |
|
||||
|
||||
<Banner type="success">
|
||||
<strong>Reminder:</strong>
|
||||
All [Custom Server Components](./components) receive the `payload` and `i18n` properties by default. See [Building Custom Components](./components#building-custom-components) for more details.
|
||||
</Banner>
|
||||
|
||||
#### Sending and receiving values from the form
|
||||
|
||||
When swapping out the `Field` component, you are responsible for sending and receiving the field's `value` from the form itself.
|
||||
|
||||
To do so, import the [`useField`](./hooks#usefield) hook from `@payloadcms/ui` and use it to manage the field's value:
|
||||
|
||||
```tsx
|
||||
'use client'
|
||||
import { useField } from '@payloadcms/ui'
|
||||
|
||||
export const CustomTextField: React.FC = () => {
|
||||
const { value, setValue } = useField() // highlight-line
|
||||
|
||||
return (
|
||||
<input
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
value={value}
|
||||
/>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
<Banner type="success">
|
||||
For a complete list of all available React hooks, see the [Payload React Hooks](./hooks) documentation. For additional help, see [Building Custom Components](./components#building-custom-components).
|
||||
</Banner>
|
||||
|
||||
### The Cell Component
|
||||
|
||||
The Cell Component is rendered in the table of the List View. It represents the value of the field when displayed in a table cell.
|
||||
|
||||
To easily swap in your own Cell Component, use the `admin.components.Cell` property in your [Field Config](../fields/overview):
|
||||
|
||||
```ts
|
||||
import type { Field } from 'payload'
|
||||
|
||||
export const myField: Field = {
|
||||
name: 'myField',
|
||||
type: 'text',
|
||||
admin: {
|
||||
components: {
|
||||
Cell: MyCustomCell, // highlight-line
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
_For details on how to build Custom Components, see [Building Custom Components](./components#building-custom-components)._
|
||||
|
||||
All Cell Components receive the following props:
|
||||
|
||||
| Property | Description |
|
||||
| ---------------- | ----------------------------------------------------------------- |
|
||||
| **`name`** | The name of the field. |
|
||||
| **`className`** | The `admin.className` property defined in the [Field Config](../fields/overview). |
|
||||
| **`fieldType`** | The `type` property defined in the [Field Config](../fields/overview). |
|
||||
| **`schemaPath`** | The path to the field in the schema. Similar to `path`, but without dynamic indices. |
|
||||
| **`isFieldAffectingData`** | A boolean value that represents if the field is affecting the data or not. |
|
||||
| **`label`** | The label value provided in the field, it can be used with i18n. |
|
||||
| **`labels`** | An object that contains the labels for the field. |
|
||||
| **`link`** | A boolean representing whether this cell should be wrapped in a link. |
|
||||
| **`onClick`** | A function that is called when the cell is clicked. |
|
||||
| **`dateDisplayFormat`** | If a [`date`](../fields/date) field, the `admin.dateDisplayFormat` property defined in the [Field Config](../fields/overview). |
|
||||
| **`options`** | If a [`select`](../fields/select) field, this is an array of options defined in the [Field Config](../fields/overview). [More details](../fields/select). |
|
||||
| **`relationTo`** | If a [`relationship`](../fields/relationship). or [`upload`](../fields/upload) field, this is the collection(s) the field is related to. |
|
||||
| **`richTextComponentMap`** | If a [`richText`](../fields/rich-text) field, this is an object that maps the rich text components. [More details](../fields/rich-text). |
|
||||
| **`blocks`** | If a [`blocks`](../fields/blocks) field, this is an array of labels and slugs representing the blocks defined in the [Field Config](../fields/overview). [More details](../fields/blocks). |
|
||||
|
||||
<Banner type="info">
|
||||
<strong>Tip:</strong>
|
||||
Use the [`useTableCell`](./hooks#usetablecell) hook to subscribe to the field's `cellData` and `rowData`.
|
||||
</Banner>
|
||||
|
||||
<Banner type="success">
|
||||
<strong>Reminder:</strong>
|
||||
All [Custom Server Components](./components) receive the `payload` and `i18n` properties by default. See [Building Custom Components](./components#building-custom-components) for more details.
|
||||
</Banner>
|
||||
|
||||
### The Label Component
|
||||
|
||||
The Label Component is rendered anywhere a field needs to be represented by a label. This is typically used in the Edit View, but can also be used in the List View and elsewhere.
|
||||
|
||||
To easily swap in your own Label Component, use the `admin.components.Label` property in your [Field Config](../fields/overview):
|
||||
|
||||
```ts
|
||||
import type { Field } from 'payload'
|
||||
|
||||
export const myField: Field = {
|
||||
name: 'myField',
|
||||
type: 'text',
|
||||
admin: {
|
||||
components: {
|
||||
Label: MyCustomLabel, // highlight-line
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
_For details on how to build Custom Components, see [Building Custom Components](./components#building-custom-components)._
|
||||
|
||||
All Label Components receive the following props:
|
||||
|
||||
| Property | Description |
|
||||
| -------------- | ---------------------------------------------------------------- |
|
||||
| **`label`** | Label value provided in field, it can be used with i18n. |
|
||||
| **`required`** | The `admin.required` property defined in the [Field Config](../fields/overview). |
|
||||
| **`schemaPath`** | The path to the field in the schema. Similar to `path`, but without dynamic indices. |
|
||||
|
||||
<Banner type="success">
|
||||
<strong>Reminder:</strong>
|
||||
All [Custom Server Components](./components) receive the `payload` and `i18n` properties by default. See [Building Custom Components](./components#building-custom-components) for more details.
|
||||
</Banner>
|
||||
|
||||
### The Error Component
|
||||
|
||||
The Error Component is rendered when a field fails validation. It is typically displayed beneath the field input in a visually-compelling style.
|
||||
|
||||
To easily swap in your own Error Component, use the `admin.components.Error` property in your [Field Config](../fields/overview):
|
||||
|
||||
```ts
|
||||
import type { Field } from 'payload'
|
||||
|
||||
export const myField: Field = {
|
||||
name: 'myField',
|
||||
type: 'text',
|
||||
admin: {
|
||||
components: {
|
||||
Error: MyCustomError, // highlight-line
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
_For details on how to build Custom Components, see [Building Custom Components](./components#building-custom-components)._
|
||||
|
||||
All Error Components receive the following props:
|
||||
|
||||
| Property | Description |
|
||||
| --------------- | ------------------------------------------------------------- |
|
||||
| **`path`*** | The static path of the field at render time. [More details](./hooks#usefieldprops). |
|
||||
|
||||
<Banner type="success">
|
||||
<strong>Reminder:</strong>
|
||||
All [Custom Server Components](./components) receive the `payload` and `i18n` properties by default. See [Building Custom Components](./components#building-custom-components) for more details.
|
||||
</Banner>
|
||||
|
||||
### The Description Property
|
||||
|
||||
Field Descriptions are used to provide additional information to the editor about a field, such as special instructions. Their placement varies from field to field, but typically are displayed with subtle style differences beneath the field inputs.
|
||||
|
||||
A description can be configured in three ways:
|
||||
|
||||
- As a string.
|
||||
- As a function which returns a string. [More details](#description-functions).
|
||||
- As a React component. [More details](#the-description-component).
|
||||
|
||||
To easily add a Custom Description to a field, use the `admin.description` property in your [Field Config](../fields/overview):
|
||||
|
||||
```ts
|
||||
import type { SanitizedCollectionConfig } from 'payload'
|
||||
|
||||
export const MyCollectionConfig: SanitizedCollectionConfig = {
|
||||
// ...
|
||||
fields: [
|
||||
// ...
|
||||
{
|
||||
name: 'myField',
|
||||
type: 'text',
|
||||
admin: {
|
||||
description: 'Hello, world!' // highlight-line
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
<Banner type="warning">
|
||||
<strong>Reminder:</strong>
|
||||
To replace the Field Description with a [Custom Component](./components), use the `admin.components.Description` property. [More details](#the-description-component).
|
||||
</Banner>
|
||||
|
||||
#### Description Functions
|
||||
|
||||
Custom Descriptions can also be defined as a function. Description Functions are executed on the server and can be used to format simple descriptions based on the user's current [Locale](../configuration/localization).
|
||||
|
||||
To easily add a Description Function to a field, set the `admin.description` property to a _function_ in your [Field Config](../fields/overview):
|
||||
|
||||
```ts
|
||||
import type { SanitizedCollectionConfig } from 'payload'
|
||||
|
||||
export const MyCollectionConfig: SanitizedCollectionConfig = {
|
||||
// ...
|
||||
fields: [
|
||||
// ...
|
||||
{
|
||||
name: 'myField',
|
||||
type: 'text',
|
||||
admin: {
|
||||
description: ({ t }) => `${t('Hello, world!')}` // highlight-line
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
All Description Functions receive the following arguments:
|
||||
|
||||
| Argument | Description |
|
||||
| -------------- | ---------------------------------------------------------------- |
|
||||
| **`t`** | The `t` function used to internationalize the Admin Panel. [More details](../configuration/i18n) |
|
||||
|
||||
### The Description Component
|
||||
|
||||
Alternatively to the [Description Property](#the-description-property), you can also use a [Custom Component](./components) as the Field Description. This can be useful when you need to provide more complex feedback to the user, such as rendering dynamic field values or other interactive elements.
|
||||
|
||||
To easily add a Description Component to a field, use the `admin.components.Description` property in your [Field Config](../fields/overview):
|
||||
|
||||
```ts
|
||||
import type { SanitizedCollectionConfig } from 'payload'
|
||||
import { MyCustomDescription } from './MyCustomDescription'
|
||||
|
||||
export const MyCollectionConfig: SanitizedCollectionConfig = {
|
||||
// ...
|
||||
fields: [
|
||||
// ...
|
||||
{
|
||||
name: 'myField',
|
||||
type: 'text',
|
||||
admin: {
|
||||
components: {
|
||||
Description: MyCustomDescription, // highlight-line
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
_For details on how to build a Custom Description, see [Building Custom Components](./components#building-custom-components)._
|
||||
|
||||
All Description Components receive the following props:
|
||||
|
||||
| Property | Description |
|
||||
| -------------- | ---------------------------------------------------------------- |
|
||||
| **`description`** | The `description` property defined in the [Field Config](../fields/overview). |
|
||||
|
||||
<Banner type="success">
|
||||
<strong>Reminder:</strong>
|
||||
All [Custom Server Components](./components) receive the `payload` and `i18n` properties by default. See [Building Custom Components](./components#building-custom-components) for more details.
|
||||
</Banner>
|
||||
|
||||
### afterInput and beforeInput
|
||||
|
||||
With these properties you can add multiple components _before_ and _after_ the input element, as their name suggests. This is useful when you need to render additional elements alongside the field without replacing the entire field component.
|
||||
|
||||
To add components before and after the input element, use the `admin.components.beforeInput` and `admin.components.afterInput` properties in your [Field Config](../fields/overview):
|
||||
|
||||
```ts
|
||||
import type { SanitizedCollectionConfig } from 'payload'
|
||||
|
||||
export const MyCollectionConfig: SanitizedCollectionConfig = {
|
||||
// ...
|
||||
fields: [
|
||||
// ...
|
||||
{
|
||||
name: 'myField',
|
||||
type: 'text',
|
||||
admin: {
|
||||
components: {
|
||||
// highlight-start
|
||||
beforeInput: [MyCustomComponent],
|
||||
afterInput: [MyOtherCustomComponent],
|
||||
// highlight-end
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
_For details on how to build Custom Components, see [Building Custom Components](./components#building-custom-components)._
|
||||
|
||||
## Conditional Logic
|
||||
|
||||
You can show and hide fields based on what other fields are doing by utilizing conditional logic on a field by field basis. The `condition` property on a field's admin config accepts a function which takes three arguments:
|
||||
|
||||
- `data` - the entire document's data that is currently being edited
|
||||
- `siblingData` - only the fields that are direct siblings to the field with the condition
|
||||
- `{ user }` - the final argument is an object containing the currently authenticated user
|
||||
|
||||
The `condition` function should return a boolean that will control if the field should be displayed or not.
|
||||
|
||||
**Example:**
|
||||
|
||||
```ts
|
||||
{
|
||||
fields: [
|
||||
{
|
||||
name: 'enableGreeting',
|
||||
type: 'checkbox',
|
||||
defaultValue: false,
|
||||
},
|
||||
{
|
||||
name: 'greeting',
|
||||
type: 'text',
|
||||
admin: {
|
||||
// highlight-start
|
||||
condition: (data, siblingData, { user }) => {
|
||||
if (data.enableGreeting) {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
},
|
||||
// highlight-end
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
```
|
||||
949
docs/admin/hooks.mdx
Normal file
949
docs/admin/hooks.mdx
Normal file
@@ -0,0 +1,949 @@
|
||||
---
|
||||
title: React Hooks
|
||||
label: React Hooks
|
||||
order: 50
|
||||
desc: Make use of all of the powerful React hooks that Payload provides.
|
||||
keywords: admin, components, custom, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
|
||||
---
|
||||
|
||||
Payload provides a variety of powerful [React Hooks](https://react.dev/reference/react-dom/hooks) that can be used within your own [Custom Components](./components), such as [Custom Fields](./fields). With them, you can interface with Payload itself to build just about any type of complex customization you can think of.
|
||||
|
||||
<Banner type="warning">
|
||||
<strong>Reminder:</strong>
|
||||
All Custom Components are [React Server Components](https://react.dev/reference/rsc/server-components) by default. Hooks, on the other hand, are only available in client-side environments. To use hooks, [ensure your component is a client component](./components#client-components).
|
||||
</Banner>
|
||||
|
||||
## useField
|
||||
|
||||
The `useField` hook is used internally within all field components. It manages sending and receiving a field's state from its parent form. When you build a [Custom Field Component](./fields), you will be responsible for sending and receiving the field's `value` to and from the form yourself.
|
||||
|
||||
To do so, import the `useField` hook as follows:
|
||||
|
||||
```tsx
|
||||
'use client'
|
||||
import { useField } from '@payloadcms/ui'
|
||||
|
||||
const CustomTextField: React.FC = () => {
|
||||
const { value, setValue, path } = useField() // highlight-line
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p>
|
||||
{path}
|
||||
</p>
|
||||
<input
|
||||
onChange={(e) => { setValue(e.target.value) }}
|
||||
value={value}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
The `useField` hook accepts the following arguments:
|
||||
|
||||
| Property | Description |
|
||||
| ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `path` | If you do not provide a `path` or a `name`, this hook will look for one using the [`useFieldProps`](#usefieldprops) hook. |
|
||||
| `validate` | A validation function executed client-side _before_ submitting the form to the server. Different than [Field-level Validation](../fields/overview#validation) which runs strictly on the server. |
|
||||
| `disableFormData` | If `true`, the field will not be included in the form data when the form is submitted. |
|
||||
| `hasRows` | If `true`, the field will be treated as a field with rows. This is useful for fields like `array` and `blocks`. |
|
||||
|
||||
The `useField` hook returns the following object:
|
||||
|
||||
```ts
|
||||
type FieldResult<T> = {
|
||||
errorMessage?: string
|
||||
errorPaths?: string[]
|
||||
filterOptions?: FilterOptionsResult
|
||||
formInitializing: boolean
|
||||
formProcessing: boolean
|
||||
formSubmitted: boolean
|
||||
initialValue?: T
|
||||
path: string
|
||||
permissions: FieldPermissions
|
||||
readOnly?: boolean
|
||||
rows?: Row[]
|
||||
schemaPath: string
|
||||
setValue: (val: unknown, disableModifyingForm?: boolean) => voi
|
||||
showError: boolean
|
||||
valid?: boolean
|
||||
value: T
|
||||
}
|
||||
```
|
||||
|
||||
## useFieldProps
|
||||
|
||||
All [Custom Field Components](./fields#the-field-component) are rendered on the server, and as such, only have access to static props at render time. But, some fields can be dynamic, such as when nested in an [`array`](../fields/array) or [`blocks`](../fields/block) field. For example, items can be added, re-ordered, or deleted on-the-fly.
|
||||
|
||||
For this reason, dynamic props like `path` are managed in their own React context, which can be accessed using the `useFieldProps` hook:
|
||||
|
||||
```tsx
|
||||
'use client'
|
||||
import { useFieldProps } from '@payloadcms/ui'
|
||||
|
||||
const CustomTextField: React.FC = () => {
|
||||
const { path } = useFieldProps() // highlight-line
|
||||
|
||||
return (
|
||||
<div>
|
||||
{path}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
<Banner type="success">
|
||||
<strong>Tip:</strong>
|
||||
The [`useField`](#usefield) hook calls the `useFieldProps` hook internally, so you don't need to use both in the same component unless explicitly needed.
|
||||
</Banner>
|
||||
|
||||
## useFormFields
|
||||
|
||||
There are times when a custom field component needs to have access to data from other fields, and you have a few options to do so. The `useFormFields` hook is a powerful and highly performant way to retrieve a form's field state, as well as to retrieve the `dispatchFields` method, which can be helpful for setting other fields' form states from anywhere within a form.
|
||||
|
||||
<Banner type="success">
|
||||
<strong>This hook is great for retrieving only certain fields from form state</strong> because it
|
||||
ensures that it will only cause a rerender when the items that you ask for change.
|
||||
</Banner>
|
||||
|
||||
Thanks to the awesome package [`use-context-selector`](https://github.com/dai-shi/use-context-selector), you can retrieve a specific field's state easily. This is ideal because you can ensure you have an up-to-date field state, and your component will only re-render when _that field's state_ changes.
|
||||
|
||||
You can pass a Redux-like selector into the hook, which will ensure that you retrieve only the field that you want. The selector takes an argument with type of `[fields: Fields, dispatch: React.Dispatch<Action>]]`.
|
||||
|
||||
```tsx
|
||||
'use client'
|
||||
import type { useFormFields } from '@payloadcms/ui'
|
||||
|
||||
const MyComponent: React.FC = () => {
|
||||
// Get only the `amount` field state, and only cause a rerender when that field changes
|
||||
const amount = useFormFields(([fields, dispatch]) => fields.amount)
|
||||
|
||||
// Do the same thing as above, but to the `feePercentage` field
|
||||
const feePercentage = useFormFields(([fields, dispatch]) => fields.feePercentage)
|
||||
|
||||
if (typeof amount?.value !== 'undefined' && typeof feePercentage?.value !== 'undefined') {
|
||||
return <span>The fee is ${(amount.value * feePercentage.value) / 100}</span>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 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>]]`.
|
||||
|
||||
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.
|
||||
|
||||
```tsx
|
||||
'use client'
|
||||
import { useAllFormFields } from '@payloadcms/ui'
|
||||
import { reduceFieldsToValues, getSiblingData } from 'payload/shared'
|
||||
|
||||
const ExampleComponent: React.FC = () => {
|
||||
// the `fields` const will be equal to all fields' state,
|
||||
// and the `dispatchFields` method is usable to send field state up to the form
|
||||
const [fields, dispatchFields] = useAllFormFields();
|
||||
|
||||
// Pass in fields, and indicate if you'd like to "unflatten" field data.
|
||||
// The result below will reflect the data stored in the form at the given time
|
||||
const formData = reduceFieldsToValues(fields, true);
|
||||
|
||||
// Pass in field state and a path,
|
||||
// and you will be sent all sibling data of the path that you've specified
|
||||
const siblingData = getSiblingData(fields, 'someFieldName');
|
||||
|
||||
return (
|
||||
// return some JSX here if necessary
|
||||
)
|
||||
};
|
||||
```
|
||||
|
||||
#### Updating other fields' values
|
||||
|
||||
If you are building a Custom Component, then you should use `setValue` which is returned from the `useField` hook to programmatically set your field's value. But if you're looking to update _another_ field's value, you can use `dispatchFields` returned from `useFormFields`.
|
||||
|
||||
You can send the following actions to the `dispatchFields` function.
|
||||
|
||||
| Action | Description |
|
||||
| ---------------------- | -------------------------------------------------------------------------- |
|
||||
| **`ADD_ROW`** | Adds a row of data (useful in array / block field data) |
|
||||
| **`DUPLICATE_ROW`** | Duplicates a row of data (useful in array / block field data) |
|
||||
| **`MODIFY_CONDITION`** | Updates a field's conditional logic result (true / false) |
|
||||
| **`MOVE_ROW`** | Moves a row of data (useful in array / block field data) |
|
||||
| **`REMOVE`** | Removes a field from form state |
|
||||
| **`REMOVE_ROW`** | Removes a row of data from form state (useful in array / block field data) |
|
||||
| **`REPLACE_STATE`** | Completely replaces form state |
|
||||
| **`UPDATE`** | Update any property of a specific field's state |
|
||||
|
||||
To see types for each action supported within the `dispatchFields` hook, check out the Form types [here](https://github.com/payloadcms/payload/blob/main/packages/payload/src/admin/components/forms/Form/types.ts).
|
||||
|
||||
## useForm
|
||||
|
||||
The `useForm` hook can be used to interact with the form itself, and sends back many methods that can be used to reactively fetch form state without causing rerenders within your components each time a field is changed. This is useful if you have action-based callbacks that your components fire, and need to interact with form state _based on a user action_.
|
||||
|
||||
<Banner type="warning">
|
||||
<strong>Warning:</strong>
|
||||
<br />
|
||||
This hook is optimized to avoid causing rerenders when fields change, and as such, its `fields`
|
||||
property will be out of date. You should only leverage this hook if you need to perform actions
|
||||
against the form in response to your users' actions. Do not rely on its returned "fields" as being
|
||||
up-to-date. They will be removed from this hook's response in an upcoming version.
|
||||
</Banner>
|
||||
|
||||
The `useForm` hook returns an object with the following properties:
|
||||
|
||||
<TableWithDrawers
|
||||
columns={[
|
||||
'Action',
|
||||
'Description',
|
||||
'Example',
|
||||
]}
|
||||
rows={[
|
||||
[
|
||||
{
|
||||
value: <strong><code>fields</code></strong>,
|
||||
},
|
||||
{
|
||||
value: "Deprecated. This property cannot be relied on as up-to-date.",
|
||||
},
|
||||
{
|
||||
value: ''
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
value: <strong><code>submit</code></strong>,
|
||||
},
|
||||
{
|
||||
value: "Method to trigger the form to submit",
|
||||
},
|
||||
{
|
||||
value: ''
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
value: <strong><code>dispatchFields</code></strong>,
|
||||
},
|
||||
{
|
||||
value: "Dispatch actions to the form field state",
|
||||
},
|
||||
{
|
||||
value: ''
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
value: <strong><code>validateForm</code></strong>,
|
||||
},
|
||||
{
|
||||
value: "Trigger a validation of the form state",
|
||||
},
|
||||
{
|
||||
value: ''
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
value: <strong><code>createFormData</code></strong>,
|
||||
},
|
||||
{
|
||||
value: <>Create a <code>multipart/form-data</code> object from the current form's state</>,
|
||||
},
|
||||
{
|
||||
value: ''
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
value: <strong><code>disabled</code></strong>,
|
||||
},
|
||||
{
|
||||
value: "Boolean denoting whether or not the form is disabled",
|
||||
},
|
||||
{
|
||||
value: ''
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
value: <strong><code>getFields</code></strong>,
|
||||
},
|
||||
{
|
||||
value: 'Gets all fields from state',
|
||||
},
|
||||
{
|
||||
value: '',
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
value: <strong><code>getField</code></strong>,
|
||||
},
|
||||
{
|
||||
value: 'Gets a single field from state by path',
|
||||
},
|
||||
{
|
||||
value: '',
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
value: <strong><code>getData</code></strong>,
|
||||
},
|
||||
{
|
||||
value: 'Returns the data stored in the form',
|
||||
},
|
||||
{
|
||||
value: '',
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
value: <strong><code>getSiblingData</code></strong>,
|
||||
},
|
||||
{
|
||||
value: 'Returns form sibling data for the given field path',
|
||||
},
|
||||
{
|
||||
value: '',
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
value: <strong><code>setModified</code></strong>,
|
||||
},
|
||||
{
|
||||
value: <>Set the form\'s <code>modified</code> state</>,
|
||||
},
|
||||
{
|
||||
value: '',
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
value: <strong><code>setProcessing</code></strong>,
|
||||
},
|
||||
{
|
||||
value: <>Set the form\'s <code>processing</code> state</>,
|
||||
},
|
||||
{
|
||||
value: '',
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
value: <strong><code>setSubmitted</code></strong>,
|
||||
},
|
||||
{
|
||||
value: <>Set the form\'s <code>submitted</code> state</>,
|
||||
},
|
||||
{
|
||||
value: '',
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
value: <strong><code>formRef</code></strong>,
|
||||
},
|
||||
{
|
||||
value: 'The ref from the form HTML element',
|
||||
},
|
||||
{
|
||||
value: '',
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
value: <strong><code>reset</code></strong>,
|
||||
},
|
||||
{
|
||||
value: 'Method to reset the form to its initial state',
|
||||
},
|
||||
{
|
||||
value: '',
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
value: <strong><code>addFieldRow</code></strong>,
|
||||
},
|
||||
{
|
||||
value: "Method to add a row on an array or block field",
|
||||
},
|
||||
{
|
||||
drawerTitle: 'addFieldRow',
|
||||
drawerDescription: 'A useful method to programmatically add a row to an array or block field.',
|
||||
drawerSlug: 'addFieldRow',
|
||||
drawerContent: (
|
||||
<>
|
||||
<TableWithDrawers
|
||||
columns={[
|
||||
'Prop',
|
||||
'Description',
|
||||
]}
|
||||
rows={[
|
||||
[
|
||||
{
|
||||
value: <strong><code>path</code></strong>,
|
||||
},
|
||||
{
|
||||
value: "The path to the array or block field",
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
value: <strong><code>rowIndex</code></strong>,
|
||||
},
|
||||
{
|
||||
value: "The index of the row to add. If omitted, the row will be added to the end of the array.",
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
value: <strong><code>data</code></strong>,
|
||||
},
|
||||
{
|
||||
value: "The data to add to the row",
|
||||
},
|
||||
],
|
||||
]}
|
||||
/>
|
||||
|
||||
{' '}
|
||||
|
||||
<br />
|
||||
|
||||
{' '}
|
||||
|
||||
<pre>
|
||||
{`import { useForm } from "payload/components/forms";
|
||||
|
||||
export const CustomArrayManager = () => {
|
||||
const { addFieldRow } = useForm()
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
addFieldRow({
|
||||
path: "arrayField",
|
||||
rowIndex: 0,
|
||||
data: {
|
||||
textField: "text",
|
||||
// blockType: "yourBlockSlug",
|
||||
// ^ if managing a block array, you need to specify the block type
|
||||
},
|
||||
})
|
||||
}}
|
||||
>
|
||||
Add Row
|
||||
</button>
|
||||
)
|
||||
}`}
|
||||
</pre>
|
||||
|
||||
<p>An example config to go along with the Custom Component</p>
|
||||
<pre>
|
||||
{`const ExampleCollection = {
|
||||
slug: "example-collection",
|
||||
fields: [
|
||||
{
|
||||
name: "arrayField",
|
||||
type: "array",
|
||||
fields: [
|
||||
{
|
||||
name: "textField",
|
||||
type: "text",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "ui",
|
||||
name: "customArrayManager",
|
||||
admin: {
|
||||
components: {
|
||||
Field: CustomArrayManager,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}`}
|
||||
</pre>
|
||||
</>
|
||||
)
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
value: <strong><code>removeFieldRow</code></strong>,
|
||||
},
|
||||
{
|
||||
value: "Method to remove a row from an array or block field",
|
||||
},
|
||||
{
|
||||
drawerTitle: 'removeFieldRow',
|
||||
drawerDescription: 'A useful method to programmatically remove a row from an array or block field.',
|
||||
drawerSlug: 'removeFieldRow',
|
||||
drawerContent: (
|
||||
<>
|
||||
<TableWithDrawers
|
||||
columns={[
|
||||
'Prop',
|
||||
'Description',
|
||||
]}
|
||||
rows={[
|
||||
[
|
||||
{
|
||||
value: <strong><code>path</code></strong>,
|
||||
},
|
||||
{
|
||||
value: "The path to the array or block field",
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
value: <strong><code>rowIndex</code></strong>,
|
||||
},
|
||||
{
|
||||
value: "The index of the row to remove",
|
||||
},
|
||||
],
|
||||
]}
|
||||
/>
|
||||
|
||||
{' '}
|
||||
|
||||
<br />
|
||||
|
||||
{' '}
|
||||
|
||||
<pre>
|
||||
{`import { useForm } from "payload/components/forms";
|
||||
|
||||
export const CustomArrayManager = () => {
|
||||
const { removeFieldRow } = useForm()
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
removeFieldRow({
|
||||
path: "arrayField",
|
||||
rowIndex: 0,
|
||||
})
|
||||
}}
|
||||
>
|
||||
Remove Row
|
||||
</button>
|
||||
)
|
||||
}`}
|
||||
</pre>
|
||||
|
||||
<p>An example config to go along with the Custom Component</p>
|
||||
<pre>
|
||||
{`const ExampleCollection = {
|
||||
slug: "example-collection",
|
||||
fields: [
|
||||
{
|
||||
name: "arrayField",
|
||||
type: "array",
|
||||
fields: [
|
||||
{
|
||||
name: "textField",
|
||||
type: "text",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "ui",
|
||||
name: "customArrayManager",
|
||||
admin: {
|
||||
components: {
|
||||
Field: CustomArrayManager,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}`}
|
||||
</pre>
|
||||
</>
|
||||
)
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
value: <strong><code>replaceFieldRow</code></strong>,
|
||||
},
|
||||
{
|
||||
value: "Method to replace a row from an array or block field",
|
||||
},
|
||||
{
|
||||
drawerTitle: 'replaceFieldRow',
|
||||
drawerDescription: 'A useful method to programmatically replace a row from an array or block field.',
|
||||
drawerSlug: 'replaceFieldRow',
|
||||
drawerContent: (
|
||||
<>
|
||||
<TableWithDrawers
|
||||
columns={[
|
||||
'Prop',
|
||||
'Description',
|
||||
]}
|
||||
rows={[
|
||||
[
|
||||
{
|
||||
value: <strong><code>path</code></strong>,
|
||||
},
|
||||
{
|
||||
value: "The path to the array or block field",
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
value: <strong><code>rowIndex</code></strong>,
|
||||
},
|
||||
{
|
||||
value: "The index of the row to replace",
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
value: <strong><code>data</code></strong>,
|
||||
},
|
||||
{
|
||||
value: "The data to replace within the row",
|
||||
},
|
||||
],
|
||||
]}
|
||||
/>
|
||||
|
||||
{' '}
|
||||
|
||||
<br />
|
||||
|
||||
{' '}
|
||||
|
||||
<pre>
|
||||
{`import { useForm } from "payload/components/forms";
|
||||
|
||||
export const CustomArrayManager = () => {
|
||||
const { replaceFieldRow } = useForm()
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
replaceFieldRow({
|
||||
path: "arrayField",
|
||||
rowIndex: 0,
|
||||
data: {
|
||||
textField: "updated text",
|
||||
// blockType: "yourBlockSlug",
|
||||
// ^ if managing a block array, you need to specify the block type
|
||||
},
|
||||
})
|
||||
}}
|
||||
>
|
||||
Replace Row
|
||||
</button>
|
||||
)
|
||||
}`}
|
||||
</pre>
|
||||
|
||||
<p>An example config to go along with the Custom Component</p>
|
||||
<pre>
|
||||
{`const ExampleCollection = {
|
||||
slug: "example-collection",
|
||||
fields: [
|
||||
{
|
||||
name: "arrayField",
|
||||
type: "array",
|
||||
fields: [
|
||||
{
|
||||
name: "textField",
|
||||
type: "text",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "ui",
|
||||
name: "customArrayManager",
|
||||
admin: {
|
||||
components: {
|
||||
Field: CustomArrayManager,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}`}
|
||||
</pre>
|
||||
</>
|
||||
)
|
||||
}
|
||||
],
|
||||
]}
|
||||
/>
|
||||
|
||||
## useCollapsible
|
||||
|
||||
The `useCollapsible` hook allows you to control parent collapsibles:
|
||||
|
||||
| Property | Description |
|
||||
| ------------------------- | ------------------------------------------------------------------------------------------------------------- |
|
||||
| **`isCollapsed`** | State of the collapsible. `true` if open, `false` if collapsed. |
|
||||
| **`isVisible`** | If nested, determine if the nearest collapsible is visible. `true` if no parent is closed, `false` otherwise. |
|
||||
| **`toggle`** | Toggles the state of the nearest collapsible. |
|
||||
| **`isWithinCollapsible`** | Determine when you are within another collapsible. |
|
||||
|
||||
**Example:**
|
||||
|
||||
```tsx
|
||||
'use client'
|
||||
import React from 'react'
|
||||
|
||||
import { useCollapsible } from '@payloadcms/ui'
|
||||
|
||||
const CustomComponent: React.FC = () => {
|
||||
const { isCollapsed, toggle } = useCollapsible()
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p className="field-type">I am {isCollapsed ? 'closed' : 'open'}</p>
|
||||
<button onClick={toggle} type="button">
|
||||
Toggle
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## useDocumentInfo
|
||||
|
||||
The `useDocumentInfo` hook provides lots of information about the document currently being edited, including the following:
|
||||
|
||||
| Property | Description |
|
||||
| ------------------------- | ------------------------------------------------------------------------------------------------------------------ |
|
||||
| **`collection`** | If the doc is a collection, its Collection Config will be returned |
|
||||
| **`global`** | If the doc is a global, its Global Config will be returned |
|
||||
| **`id`** | If the doc is a collection, its ID will be returned |
|
||||
| **`preferencesKey`** | The `preferences` key to use when interacting with document-level user preferences |
|
||||
| **`versions`** | Versions of the current doc |
|
||||
| **`unpublishedVersions`** | Unpublished versions of the current doc |
|
||||
| **`publishedDoc`** | The currently published version of the doc being edited |
|
||||
| **`getVersions`** | Method to trigger the retrieval of document versions |
|
||||
| **`docPermissions`** | The current documents permissions. Collection document permissions fallback when no id is present (i.e. on create) |
|
||||
| **`getDocPermissions`** | Method to trigger the retrieval of document level permissions |
|
||||
|
||||
**Example:**
|
||||
|
||||
```tsx
|
||||
'use client'
|
||||
import { useDocumentInfo } from '@payloadcms/ui'
|
||||
|
||||
const LinkFromCategoryToPosts: React.FC = () => {
|
||||
// highlight-start
|
||||
const { id } = useDocumentInfo()
|
||||
// highlight-end
|
||||
|
||||
// id will be undefined on the create form
|
||||
if (!id) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<a href={`/admin/collections/posts?where[or][0][and][0][category][in][0]=[${id}]`}>
|
||||
View posts
|
||||
</a>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## useLocale
|
||||
|
||||
In any Custom Component you can get the selected locale object with the `useLocale` hook. `useLocale`gives you the full locale object, consisting of a `label`, `rtl`(right-to-left) property, and then `code`. Here is a simple example:
|
||||
|
||||
```tsx
|
||||
'use client'
|
||||
import { useLocale } from '@payloadcms/ui'
|
||||
|
||||
const Greeting: React.FC = () => {
|
||||
// highlight-start
|
||||
const locale = useLocale()
|
||||
// highlight-end
|
||||
|
||||
const trans = {
|
||||
en: 'Hello',
|
||||
es: 'Hola',
|
||||
}
|
||||
|
||||
return <span> {trans[locale.code]} </span>
|
||||
}
|
||||
```
|
||||
|
||||
## useAuth
|
||||
|
||||
Useful to retrieve info about the currently logged in user as well as methods for interacting with it. It sends back an object with the following properties:
|
||||
|
||||
| Property | Description |
|
||||
| ------------------------ | --------------------------------------------------------------------------------------- |
|
||||
| **`user`** | The currently logged in user |
|
||||
| **`logOut`** | A method to log out the currently logged in user |
|
||||
| **`refreshCookie`** | A method to trigger the silent refreshing of a user's auth token |
|
||||
| **`setToken`** | Set the token of the user, to be decoded and used to reset the user and token in memory |
|
||||
| **`token`** | The logged in user's token (useful for creating preview links, etc.) |
|
||||
| **`refreshPermissions`** | Load new permissions (useful when content that effects permissions has been changed) |
|
||||
| **`permissions`** | The permissions of the current user |
|
||||
|
||||
```tsx
|
||||
'use client'
|
||||
import { useAuth } from '@payloadcms/ui'
|
||||
import type { User } from '../payload-types.ts'
|
||||
|
||||
const Greeting: React.FC = () => {
|
||||
// highlight-start
|
||||
const { user } = useAuth<User>()
|
||||
// highlight-end
|
||||
|
||||
return <span>Hi, {user.email}!</span>
|
||||
}
|
||||
```
|
||||
|
||||
## useConfig
|
||||
|
||||
Used to easily retrieve the Payload [Client Config](./components#accessing-the-payload-config).
|
||||
|
||||
```tsx
|
||||
'use client'
|
||||
import { useConfig } from '@payloadcms/ui'
|
||||
|
||||
const MyComponent: React.FC = () => {
|
||||
// highlight-start
|
||||
const config = useConfig()
|
||||
// highlight-end
|
||||
|
||||
return <span>{config.serverURL}</span>
|
||||
}
|
||||
```
|
||||
|
||||
## useEditDepth
|
||||
|
||||
Sends back how many editing levels "deep" the current component is. Edit depth is relevant while adding new documents / editing documents in modal windows and other cases.
|
||||
|
||||
```tsx
|
||||
'use client'
|
||||
import { useEditDepth } from '@payloadcms/ui'
|
||||
|
||||
const MyComponent: React.FC = () => {
|
||||
// highlight-start
|
||||
const editDepth = useEditDepth()
|
||||
// highlight-end
|
||||
|
||||
return <span>My component is {editDepth} levels deep</span>
|
||||
}
|
||||
```
|
||||
|
||||
## usePreferences
|
||||
|
||||
Returns methods to set and get user preferences. More info can be found [here](https://payloadcms.com/docs/admin/preferences).
|
||||
|
||||
## useTheme
|
||||
|
||||
Returns the currently selected theme (`light`, `dark` or `auto`), a set function to update it and a boolean `autoMode`, used to determine if the theme value should be set automatically based on the user's device preferences.
|
||||
|
||||
```tsx
|
||||
'use client'
|
||||
import { useTheme } from '@payloadcms/ui'
|
||||
|
||||
const MyComponent: React.FC = () => {
|
||||
// highlight-start
|
||||
const { autoMode, setTheme, theme } = useTheme()
|
||||
// highlight-end
|
||||
|
||||
return (
|
||||
<>
|
||||
<span>
|
||||
The current theme is {theme} and autoMode is {autoMode}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setTheme((prev) => (prev === 'light' ? 'dark' : 'light'))}
|
||||
>
|
||||
Toggle theme
|
||||
</button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## useTableColumns
|
||||
|
||||
Returns methods to manipulate table columns
|
||||
|
||||
```tsx
|
||||
'use client'
|
||||
import { useTableColumns } from '@payloadcms/ui'
|
||||
|
||||
const MyComponent: React.FC = () => {
|
||||
// highlight-start
|
||||
const { setActiveColumns } = useTableColumns()
|
||||
|
||||
const resetColumns = () => {
|
||||
setActiveColumns(['id', 'createdAt', 'updatedAt'])
|
||||
}
|
||||
// highlight-end
|
||||
|
||||
return (
|
||||
<button type="button" onClick={resetColumns}>
|
||||
Reset columns
|
||||
</button>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## useTableCell
|
||||
|
||||
Similar to [`useFieldProps`](#usefieldprops), all [Custom Cell Components](./fields#the-cell-component) are rendered on the server, and as such, only have access to static props at render time. But, some props need to be dynamic, such as the field value itself.
|
||||
|
||||
For this reason, dynamic props like `cellData` are managed in their own React context, which can be accessed using the `useTableCell` hook.
|
||||
|
||||
```tsx
|
||||
'use client'
|
||||
import { useTableCell } from '@payloadcms/ui'
|
||||
|
||||
const MyComponent: React.FC = () => {
|
||||
const { cellData } = useTableCell() // highlight-line
|
||||
|
||||
return (
|
||||
<div>
|
||||
{cellData}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## useDocumentEvents
|
||||
|
||||
The `useDocumentEvents` hook provides a way of subscribing to cross-document events, such as updates made to nested documents within a drawer. This hook will report document events that are outside the scope of the document currently being edited. This hook provides the following:
|
||||
|
||||
| Property | Description |
|
||||
| ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **`mostRecentUpdate`** | An object containing the most recently updated document. It contains the `entitySlug`, `id` (if collection), and `updatedAt` properties |
|
||||
| **`reportUpdate`** | A method used to report updates to documents. It accepts the same arguments as the `mostRecentUpdate` property. |
|
||||
|
||||
**Example:**
|
||||
|
||||
```tsx
|
||||
'use client'
|
||||
import { useDocumentEvents } from '@payloadcms/ui'
|
||||
|
||||
const ListenForUpdates: React.FC = () => {
|
||||
const { mostRecentUpdate } = useDocumentEvents()
|
||||
|
||||
return <span>{JSON.stringify(mostRecentUpdate)}</span>
|
||||
}
|
||||
```
|
||||
|
||||
<Banner type="info">
|
||||
Right now the `useDocumentEvents` hook only tracks recently updated documents, but in the future
|
||||
it will track more document-related events as needed, such as document creation, deletion, etc.
|
||||
</Banner>
|
||||
@@ -1,79 +0,0 @@
|
||||
---
|
||||
title: Document Locking
|
||||
label: Document Locking
|
||||
order: 40
|
||||
desc: Ensure your documents are locked during editing to prevent concurrent changes from multiple users and maintain data integrity.
|
||||
keywords: locking, document locking, edit locking, document, concurrency, Payload, headless, Content Management System, cms, javascript, react, node, nextjs
|
||||
---
|
||||
|
||||
Document locking in Payload ensures that only one user at a time can edit a document, preventing data conflicts and accidental overwrites. When a document is locked, other users are prevented from making changes until the lock is released, ensuring data integrity in collaborative environments.
|
||||
|
||||
The lock is automatically triggered when a user begins editing a document within the Admin Panel and remains in place until the user exits the editing view or the lock expires due to inactivity.
|
||||
|
||||
## How it works
|
||||
|
||||
When a user starts editing a document, Payload locks it for that user. If another user attempts to access the same document, they will be notified that it is currently being edited. They can then choose one of the following options:
|
||||
|
||||
- View in Read-Only: View the document without the ability to make any changes.
|
||||
- Take Over: Take over editing from the current user, which locks the document for the new editor and notifies the original user.
|
||||
- Return to Dashboard: Navigate away from the locked document and continue with other tasks.
|
||||
|
||||
The lock will automatically expire after a set period of inactivity, configurable using the `duration` property in the `lockDocuments` configuration, after which others can resume editing.
|
||||
|
||||
<Banner type="info"> **Note:** If your application does not require document locking, you can disable this feature for any collection or global by setting the `lockDocuments` property to `false`. </Banner>
|
||||
|
||||
### Config Options
|
||||
|
||||
The `lockDocuments` property exists on both the Collection Config and the Global Config. Document locking is enabled by default, but you can customize the lock duration or turn off the feature for any collection or global.
|
||||
|
||||
Here's an example configuration for document locking:
|
||||
|
||||
```ts
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
export const Posts: CollectionConfig = {
|
||||
slug: 'posts',
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
},
|
||||
// other fields...
|
||||
],
|
||||
lockDocuments: {
|
||||
duration: 600, // Duration in seconds
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
#### Locking Options
|
||||
|
||||
| Option | Description |
|
||||
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| **`lockDocuments`** | Enables or disables document locking for the collection or global. By default, document locking is enabled. Set to an object to configure, or set to false to disable locking. |
|
||||
| **`duration`** | Specifies the duration (in seconds) for how long a document remains locked without user interaction. The default is 300 seconds (5 minutes). |
|
||||
|
||||
### Impact on APIs
|
||||
|
||||
Document locking affects both the Local and REST APIs, ensuring that if a document is locked, concurrent users will not be able to perform updates or deletes on that document (including globals). If a user attempts to update or delete a locked document, they will receive an error.
|
||||
|
||||
Once the document is unlocked or the lock duration has expired, other users can proceed with updates or deletes as normal.
|
||||
|
||||
#### Overriding Locks
|
||||
|
||||
For operations like `update` and `delete`, Payload includes an `overrideLock` option. This boolean flag, when set to `false`, enforces document locks, ensuring that the operation will not proceed if another user currently holds the lock.
|
||||
|
||||
By default, `overrideLock` is set to `true`, which means that document locks are ignored, and the operation will proceed even if the document is locked. To enforce locks and prevent updates or deletes on locked documents, set `overrideLock: false`.
|
||||
|
||||
```ts
|
||||
const result = await payload.update({
|
||||
collection: 'posts',
|
||||
id: '123',
|
||||
data: {
|
||||
title: 'New title',
|
||||
},
|
||||
overrideLock: false, // Enforces the document lock, preventing updates if the document is locked
|
||||
})
|
||||
```
|
||||
|
||||
This option is particularly useful in scenarios where administrative privileges or specific workflows require you to override the lock and ensure the operation is completed.
|
||||
@@ -1,216 +0,0 @@
|
||||
---
|
||||
title: Page Metadata
|
||||
label: Metadata
|
||||
order: 70
|
||||
desc: Customize the metadata of your pages within the Admin Panel
|
||||
keywords: admin, components, custom, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
|
||||
---
|
||||
|
||||
Every page within the Admin Panel automatically receives dynamic, auto-generated metadata derived from live document data, the user's current locale, and more, without any additional configuration. This includes the page title, description, og:image and everything in between. Metadata is fully configurable at the root level and cascades down to individual collections, documents, and custom views, allowing for the ability to control metadata on any page with high precision.
|
||||
|
||||
Within the Admin Panel, metadata can be customized at the following levels:
|
||||
|
||||
- [Root Metadata](#root-metadata)
|
||||
- [Collection Metadata](#collection-metadata)
|
||||
- [Global Metadata](#global-metadata)
|
||||
- [View Metadata](#view-metadata)
|
||||
|
||||
All of these types of metadata share a similar structure, with a few key differences on the Root level. To customize metadata, consult the list of available scopes. Determine the scope that corresponds to what you are trying to accomplish, then author your metadata within the Payload Config accordingly.
|
||||
|
||||
## Root Metadata
|
||||
|
||||
Root Metadata is the metadata that is applied to all pages within the Admin Panel. This is where you can control things like the suffix appended onto each page's title, the favicon displayed in the browser's tab, and the Open Graph data that is used when sharing the Admin Panel on social media.
|
||||
|
||||
To customize Root Metadata, use the `admin.meta` key in your Payload Config:
|
||||
|
||||
```ts
|
||||
{
|
||||
// ...
|
||||
admin: {
|
||||
// highlight-start
|
||||
meta: {
|
||||
// highlight-end
|
||||
title: 'My Admin Panel',
|
||||
description: 'The best admin panel in the world',
|
||||
icons: [
|
||||
{
|
||||
rel: 'icon',
|
||||
type: 'image/png',
|
||||
url: '/favicon.png',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
The following options are available for Root Metadata:
|
||||
|
||||
| Key | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| **`title`** | `string` | The title of the Admin Panel. |
|
||||
| **`description`** | `string` | The description of the Admin Panel. |
|
||||
| **`defaultOGImageType`** | `dynamic` (default), `static`, or `off` | The type of default OG image to use. If set to `dynamic`, Payload will use Next.js image generation to create an image with the title of the page. If set to `static`, Payload will use the `defaultOGImage` URL. If set to `off`, Payload will not generate an OG image. |
|
||||
| **`icons`** | `IconConfig[]` | An array of icon objects. [More details](#icons) |
|
||||
| **`keywords`** | `string` | A comma-separated list of keywords to include in the metadata of the Admin Panel. |
|
||||
| **`openGraph`** | `OpenGraphConfig` | An object containing Open Graph metadata. [More details](#open-graph) |
|
||||
| **`titleSuffix`** | `string` | A suffix to append to the end of the title of every page. Defaults to "- Payload". |
|
||||
|
||||
<Banner type="success">
|
||||
**Reminder:**
|
||||
These are the _root-level_ options for the Admin Panel. You can also customize metadata on the [Collection](../configuration/collections), [Global](../configuration/globals), and Document levels through their respective configs.
|
||||
</Banner>
|
||||
|
||||
### Icons
|
||||
|
||||
The Icons Config corresponds to the `<link>` tags that are used to specify icons for the Admin Panel. The `icons` key is an array of objects, each of which represents an individual icon. Icons are differentiated from one another by their `rel` attribute, which specifies the relationship between the document and the icon.
|
||||
|
||||
The most common icon type is the favicon, which is displayed in the browser tab. This is specified by the `rel` attribute `icon`. Other common icon types include `apple-touch-icon`, which is used by Apple devices when the Admin Panel is saved to the home screen, and `mask-icon`, which is used by Safari to mask the Admin Panel icon.
|
||||
|
||||
To customize icons, use the `icons` key within the `admin.meta` object in your Payload Config:
|
||||
|
||||
```ts
|
||||
{
|
||||
// ...
|
||||
admin: {
|
||||
meta: {
|
||||
// highlight-start
|
||||
icons: [
|
||||
// highlight-end
|
||||
{
|
||||
rel: 'icon',
|
||||
type: 'image/png',
|
||||
url: '/favicon.png',
|
||||
},
|
||||
{
|
||||
rel: 'apple-touch-icon',
|
||||
type: 'image/png',
|
||||
url: '/apple-touch-icon.png',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
The following options are available for Icons:
|
||||
|
||||
| Key | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| **`rel`** | `string` | The HTML `rel` attribute of the icon. |
|
||||
| **`type`** | `string` | The MIME type of the icon. |
|
||||
| **`color`** | `string` | The color of the icon. |
|
||||
| **`fetchPriority`** | `string` | The [fetch priority](https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElement/fetchPriority) of the icon. |
|
||||
| **`media`** | `string` | The [media query](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_media_queries/Using_media_queries) of the icon. |
|
||||
| **`sizes`** | `string` | The [sizes](https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElement/sizes) of the icon. |
|
||||
| **`url`** | `string` | The URL pointing the resource of the icon. |
|
||||
|
||||
### Open Graph
|
||||
|
||||
Open Graph metadata is a set of tags that are used to control how URLs are displayed when shared on social media platforms. Open Graph metadata is automatically generated by Payload, but can be customized at the Root level.
|
||||
|
||||
To customize Open Graph metadata, use the `openGraph` key within the `admin.meta` object in your Payload Config:
|
||||
|
||||
```ts
|
||||
{
|
||||
// ...
|
||||
admin: {
|
||||
meta: {
|
||||
// highlight-start
|
||||
openGraph: {
|
||||
// highlight-end
|
||||
description: 'The best admin panel in the world',
|
||||
images: [
|
||||
{
|
||||
url: 'https://example.com/image.jpg',
|
||||
width: 800,
|
||||
height: 600,
|
||||
},
|
||||
],
|
||||
siteName: 'Payload',
|
||||
title: 'My Admin Panel',
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
The following options are available for Open Graph Metadata:
|
||||
|
||||
| Key | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| **`description`** | `string` | The description of the Admin Panel. |
|
||||
| **`images`** | `OGImageConfig` or `OGImageConfig[]` | An array of image objects. |
|
||||
| **`siteName`** | `string` | The name of the site. |
|
||||
| **`title`** | `string` | The title of the Admin Panel. |
|
||||
|
||||
## Collection Metadata
|
||||
|
||||
Collection Metadata is the metadata that is applied to all pages within any given Collection within the Admin Panel. This metadata is used to customize the title and description of all views within any given Collection, unless overridden by the view itself.
|
||||
|
||||
To customize Collection Metadata, use the `admin.meta` key within your Collection Config:
|
||||
|
||||
```ts
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
export const MyCollection: CollectionConfig = {
|
||||
// ...
|
||||
admin: {
|
||||
// highlight-start
|
||||
meta: {
|
||||
// highlight-end
|
||||
title: 'My Collection',
|
||||
description: 'The best collection in the world',
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
The Collection Meta config has the same options as the [Root Metadata](#root-metadata) config.
|
||||
|
||||
## Global Metadata
|
||||
|
||||
Global Metadata is the metadata that is applied to all pages within any given Global within the Admin Panel. This metadata is used to customize the title and description of all views within any given Global, unless overridden by the view itself.
|
||||
|
||||
To customize Global Metadata, use the `admin.meta` key within your Global Config:
|
||||
|
||||
```ts
|
||||
import { GlobalConfig } from 'payload'
|
||||
|
||||
export const MyGlobal: GlobalConfig = {
|
||||
// ...
|
||||
admin: {
|
||||
// highlight-start
|
||||
meta: {
|
||||
// highlight-end
|
||||
title: 'My Global',
|
||||
description: 'The best admin panel in the world',
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
The Global Meta config has the same options as the [Root Metadata](#root-metadata) config.
|
||||
|
||||
## View Metadata
|
||||
|
||||
View Metadata is the metadata that is applied to specific [Views](../custom-components/custom-views) within the Admin Panel. This metadata is used to customize the title and description of a specific view, overriding any metadata set at the [Root](#root-metadata), [Collection](#collection-metadata), or [Global](#global-metadata) level.
|
||||
|
||||
To customize View Metadata, use the `meta` key within your View Config:
|
||||
|
||||
```ts
|
||||
{
|
||||
// ...
|
||||
admin: {
|
||||
views: {
|
||||
dashboard: {
|
||||
// highlight-start
|
||||
meta: {
|
||||
// highlight-end
|
||||
title: 'My Dashboard',
|
||||
description: 'The best dashboard in the world',
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -6,14 +6,14 @@ desc: Manage your data and customize the Payload Admin Panel by swapping in your
|
||||
keywords: admin, components, custom, customize, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
|
||||
---
|
||||
|
||||
Payload dynamically generates a beautiful, [fully type-safe](../typescript/overview) Admin Panel to manage your users and data. It is highly performant, even with 100+ fields, and is translated in over 30 languages. Within the Admin Panel you can manage content, [render your site](../live-preview/overview), [preview drafts](./preview), [diff versions](../versions/overview), and so much more.
|
||||
Payload dynamically generates a beautiful, [fully type-safe](../typescript/overview) admin panel to manage your users and data. It is highly performant, even with 100+ fields, and is translated in over 30 languages. Within the Admin Panel you can manage content, [render your site](../live-preview/overview), preview drafts, [diff versions](../versions/overview), and so much more.
|
||||
|
||||
The Admin Panel is designed to [white-label your brand](https://payloadcms.com/blog/white-label-admin-ui). You can endlessly customize and extend the Admin UI by swapping in your own [Custom Components](../custom-components/overview)—everything from simple field labels to entire views can be modified or replaced to perfectly tailor the interface for your editors.
|
||||
The Admin Panel is designed to [white-label your brand](https://payloadcms.com/blog/white-label-admin-ui). You can endlessly customize and extend the Admin UI by swapping in your own [Custom Components](./components)—everything from simple field labels to entire views can be modified or replaced to perfectly tailor the interface for your editors.
|
||||
|
||||
The Admin Panel is written in [TypeScript](https://www.typescriptlang.org) and built with [React](https://react.dev) using the [Next.js App Router](https://nextjs.org/docs/app). It supports [React Server Components](https://react.dev/reference/rsc/server-components), enabling the use of the [Local API](/docs/local-api/overview) on the front-end. You can install Payload into any [existing Next.js app in just one line](../getting-started/installation) and [deploy it anywhere](../production/deployment).
|
||||
The Admin Panel is written in [TypeScript](https://www.typescriptlang.org) and built with [React](https://react.dev) using the [Next.js App Router](https://nextjs.org/docs/app). It supports [React Server Components](https://react.dev/reference/rsc/server-components), enabling the use of the [Local API](/docs/local-api/overview) on the front-end. You can install Payload into any [existing Next.js app in just one line](../getting-started/installation) and [deploy it anywhere](../production).
|
||||
|
||||
<Banner type="success">
|
||||
The Payload Admin Panel is designed to be as minimal and straightforward as possible to allow easy customization and control. [Learn more](../custom-components/overview).
|
||||
The Payload Admin Panel is designed to be as minimal and straightforward as possible to allow easy customization and control. [Learn more](./components).
|
||||
</Banner>
|
||||
|
||||
<LightDarkImage
|
||||
@@ -55,11 +55,6 @@ As shown above, all Payload routes are nested within the `(payload)` route group
|
||||
|
||||
The `admin` directory contains all the _pages_ related to the interface itself, whereas the `api` and `graphql` directories contains all the _routes_ related to the [REST API](../rest-api/overview) and [GraphQL API](../graphql/overview). All admin routes are [easily configurable](#customizing-routes) to meet your application's exact requirements.
|
||||
|
||||
<Banner type="warning">
|
||||
**Note:**
|
||||
If you don't intend to use the Admin Panel, [REST API](../rest-api/overview), or [GraphQL API](../graphql/overview), you can opt-out by simply deleting their corresponding directories within your Next.js app. The overhead, however, is completely constrained to these routes, and will not slow down or affect Payload outside when not in use.
|
||||
</Banner>
|
||||
|
||||
Finally, the `custom.scss` file is where you can add or override globally-oriented styles in the Admin Panel, such as modify the color palette. Customizing the look and feel through CSS alone is a powerful feature of the Admin Panel, [more on that here](./customizing-css).
|
||||
|
||||
All auto-generated files will contain the following comments at the top of each file:
|
||||
@@ -86,25 +81,23 @@ const config = buildConfig({
|
||||
|
||||
The following options are available:
|
||||
|
||||
| Option | Description |
|
||||
|--------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| **`avatar`** | Set account profile picture. Options: `gravatar`, `default` or a custom React component. |
|
||||
| **`autoLogin`** | Used to automate log-in for dev and demonstration convenience. [More details](../authentication/overview). |
|
||||
| **`buildPath`** | Specify an absolute path for where to store the built Admin bundle used in production. Defaults to `path.resolve(process.cwd(), 'build')`. |
|
||||
| **`components`** | Component overrides that affect the entirety of the Admin Panel. [More details](../custom-components/overview). |
|
||||
| **`custom`** | Any custom properties you wish to pass to the Admin Panel. |
|
||||
| **`dateFormat`** | The date format that will be used for all dates within the Admin Panel. Any valid [date-fns](https://date-fns.org/) format pattern can be used. |
|
||||
| **`livePreview`** | Enable real-time editing for instant visual feedback of your front-end application. [More details](../live-preview/overview). |
|
||||
| **`meta`** | Base metadata to use for the Admin Panel. [More details](./metadata). |
|
||||
| **`routes`** | Replace built-in Admin Panel routes with your own custom routes. [More details](#customizing-routes). |
|
||||
| **`suppressHydrationWarning`** | If set to `true`, suppresses React hydration mismatch warnings during the hydration of the root `<html>` tag. Defaults to `false`. |
|
||||
| **`theme`** | Restrict the Admin Panel theme to use only one of your choice. Default is `all`. |
|
||||
| **`timezones`** | Configure the timezone settings for the admin panel. [More details](#timezones) |
|
||||
| **`user`** | The `slug` of the Collection that you want to allow to login to the Admin Panel. [More details](#the-admin-user-collection). |
|
||||
| Option | Description |
|
||||
| ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `user` | The `slug` of the Collection that you want to allow to login to the Admin Panel. [More details](#the-admin-user-collection). |
|
||||
| `buildPath` | Specify an absolute path for where to store the built Admin bundle used in production. Defaults to `path.resolve(process.cwd(), 'build')`. |
|
||||
| `meta` | Base metadata to use for the Admin Panel. Included properties are `titleSuffix`, `icons`, and `openGraph`. Can be overridden on a per Collection or per Global basis. |
|
||||
| `disable` | If set to `true`, the entire Admin Panel will be disabled. |
|
||||
| `dateFormat` | The date format that will be used for all dates within the Admin Panel. Any valid [date-fns](https://date-fns.org/) format pattern can be used. |
|
||||
| `avatar` | Set account profile picture. Options: `gravatar`, `default` or a custom React component. |
|
||||
| `autoLogin` | Used to automate admin log-in for dev and demonstration convenience. [More details](../authentication/config). |
|
||||
| `livePreview` | Enable real-time editing for instant visual feedback of your front-end application. [More details](../live-preview/overview). |
|
||||
| `components` | Component overrides that affect the entirety of the Admin Panel. [More details](./components). |
|
||||
| `routes` | Replace built-in Admin Panel routes with your own custom routes. [More details](#customizing-routes). |
|
||||
| `custom` | Any custom properties you wish to pass to the Admin Panel. |
|
||||
|
||||
<Banner type="success">
|
||||
**Reminder:**
|
||||
These are the _root-level_ options for the Admin Panel. You can also customize [Collection Admin Options](../configuration/collections#admin-options) and [Global Admin Options](../configuration/globals#admin-options) through their respective `admin` keys.
|
||||
<strong>Reminder:</strong>
|
||||
These are the _root-level_ options for the Admin Panel. You can also customize the admin options for _Collections and Globals_ through their respective `admin` keys.
|
||||
</Banner>
|
||||
|
||||
### The Admin User Collection
|
||||
@@ -123,8 +116,8 @@ const config = buildConfig({
|
||||
```
|
||||
|
||||
<Banner type="warning">
|
||||
**Important:**
|
||||
|
||||
<strong>Important:</strong>
|
||||
<br />
|
||||
The Admin Panel can only be used by a single auth-enabled Collection. To enable authentication for a Collection, simply set `auth: true` in the Collection's configuration. See [Authentication](../authentication/overview) for more information.
|
||||
</Banner>
|
||||
|
||||
@@ -139,12 +132,12 @@ To do this, specify `admin: { user: 'admins' }` in your config. This will provid
|
||||
|
||||
### Role-based Access Control
|
||||
|
||||
It is also possible to allow multiple user types into the Admin Panel with limited permissions, known as role-based access control (RBAC). For example, you may wish to have two roles within the `admins` Collection:
|
||||
It is also possible to allow multiple user types into the Admin Panel with limited permissions. For example, you may wish to have two roles within the `admins` Collection:
|
||||
|
||||
- `super-admin` - full access to the Admin Panel to perform any action
|
||||
- `editor` - limited access to the Admin Panel to only manage content
|
||||
|
||||
To do this, add a `roles` or similar field to your auth-enabled Collection, then use the `access.admin` property to grant or deny access based on the value of that field. See [Access Control](/docs/access-control/overview) for full details. For a complete, working example of role-based access control, check out the official [Auth Example](https://github.com/payloadcms/payload/tree/main/examples/auth).
|
||||
To do this, add a `roles` or similar field to your auth-enabled Collection, then use the `access.admin` property to grant or deny access based on the value of that field. See [Access Control](/docs/access-control/overview) for full details. For a complete, working example of role-based access control, check out the official [Auth Example](https://github.com/payloadcms/payload/tree/main/examples/auth/payload).
|
||||
|
||||
## Customizing Routes
|
||||
|
||||
@@ -169,34 +162,21 @@ const config = buildConfig({
|
||||
|
||||
The following options are available:
|
||||
|
||||
| Option | Default route | Description |
|
||||
|---------------------|-----------------------|---------------------------------------------------|
|
||||
| `admin` | `/admin` | The Admin Panel itself. |
|
||||
| `api` | `/api` | The [REST API](../rest-api/overview) base path. |
|
||||
| `graphQL` | `/graphql` | The [GraphQL API](../graphql/overview) base path. |
|
||||
| `graphQLPlayground` | `/graphql-playground` | The GraphQL Playground. |
|
||||
|
||||
<Banner type="success">
|
||||
**Tip:**
|
||||
You can easily add _new_ routes to the Admin Panel through [Custom Endpoints](../rest-api/overview#custom-endpoints) and [Custom Views](../custom-components/custom-views).
|
||||
</Banner>
|
||||
|
||||
#### Customizing Root-level Routes
|
||||
|
||||
You can change the Root-level Routes as needed, such as to mount the Admin Panel at the root of your application.
|
||||
|
||||
Changing Root-level Routes also requires a change to [Project Structure](#project-structure) to match the new route. For example, if you set `routes.admin` to `/`, you would need to completely remove the `admin` directory from the project structure:
|
||||
|
||||
```plaintext
|
||||
app/
|
||||
├─ (payload)/
|
||||
├── [[...segments]]/
|
||||
├──── ...
|
||||
```
|
||||
| Option | Default route | Description |
|
||||
| ------------------ | ----------------------- | ------------------------------------- |
|
||||
| `admin` | `/admin` | The Admin Panel itself. |
|
||||
| `api` | `/api` | The [REST API](../rest-api/overview) base path. |
|
||||
| `graphQL` | `/graphql` | The [GraphQL API](../graphql/overview) base path. |
|
||||
| `graphQLPlayground`| `/graphql-playground` | The GraphQL Playground. |
|
||||
|
||||
<Banner type="warning">
|
||||
**Note:**
|
||||
If you set Root-level Routes _before_ auto-generating the Admin Panel via `create-payload-app`, your [Project Structure](#project-structure) will already be set up correctly.
|
||||
<strong>Warning:</strong>
|
||||
Changing Root-level Routes _after_ your project was generated will also require you to manually update the corresponding directories in your project. For example, changing `routes.admin` will require you to rename the `(payload)/admin` directory in your project to match the new route. [More details](#project-structure).
|
||||
</Banner>
|
||||
|
||||
<Banner type="success">
|
||||
<strong>Tip:</strong>
|
||||
You can easily add _new_ routes to the Admin Panel through the `endpoints` property of the Payload Config. See [Custom Endpoints](../rest-api/overview#custom-endpoints) for more information.
|
||||
</Banner>
|
||||
|
||||
### Admin-level Routes
|
||||
@@ -232,32 +212,14 @@ The following options are available:
|
||||
| `unauthorized` | `/unauthorized` | The unauthorized page. |
|
||||
|
||||
<Banner type="success">
|
||||
**Note:**
|
||||
You can also swap out entire _views_ out for your own, using the `admin.views` property of the Payload Config. See [Custom Views](../custom-components/custom-views) for more information.
|
||||
<strong>Note:</strong>
|
||||
You can also swap out entire _views_ out for your own, using the `admin.views` property of the Payload Config. See [Custom Views](./views) for more information.
|
||||
</Banner>
|
||||
|
||||
## I18n
|
||||
|
||||
The Payload Admin Panel is translated in over [30 languages and counting](https://github.com/payloadcms/payload/tree/main/packages/translations). Languages are automatically detected based on the user's browser and used by the Admin Panel to display all text in that language. If no language was detected, or if the user's language is not yet supported, English will be chosen. Users can easily specify their language by selecting one from their account page. See [I18n](../configuration/i18n) for more information.
|
||||
The Payload Admin Panel is translated in over [30 languages and counting](https://github.com/payloadcms/payload/tree/beta/packages/translations). Languages are automatically detected based on the user's browser and used by the Admin Panel to display all text in that language. If no language was detected, or if the user's language is not yet supported, English will be chosen. Users can easily specify their language by selecting one from their account page. See [I18n](../configuration/i18n) for more information.
|
||||
|
||||
## Light and Dark Modes
|
||||
|
||||
Users in the Admin Panel have the ability to choose between light mode and dark mode for their editing experience. Users can select their preferred theme from their account page. Once selected, it is saved to their user's preferences and persisted across sessions and devices. If no theme was selected, the Admin Panel will automatically detect the operation system's theme and use that as the default.
|
||||
|
||||
## Timezones
|
||||
|
||||
The `admin.timezones` configuration allows you to configure timezone settings for the Admin Panel. You can customise the available list of timezones and in the future configure the default timezone for the Admin Panel and for all users.
|
||||
|
||||
The following options are available:
|
||||
|
||||
| Option | Description |
|
||||
| ----------------- | ----------------------------------------------- |
|
||||
| `supportedTimezones` | An array of label/value options for selectable timezones where the value is the IANA name eg. `America/Detroit` |
|
||||
| `defaultTimezone` | The `value` of the default selected timezone. eg. `America/Los_Angeles` |
|
||||
|
||||
We validate the supported timezones array by checking the value against the list of IANA timezones supported via the Intl API, specifically `Intl.supportedValuesOf('timeZone')`.
|
||||
|
||||
<Banner type="info">
|
||||
**Important**
|
||||
You must enable timezones on each individual date field via `timezone: true`. See [Date Fields](../fields/overview#date) for more information.
|
||||
</Banner>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
title: Managing User Preferences
|
||||
label: Preferences
|
||||
order: 60
|
||||
order: 50
|
||||
desc: Store the preferences of your users as they interact with the Admin Panel.
|
||||
keywords: admin, preferences, custom, customize, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
|
||||
---
|
||||
@@ -16,13 +16,13 @@ Out of the box, Payload handles the persistence of your users' preferences in a
|
||||
1. The last-known state of the `Nav` component, etc.
|
||||
|
||||
<Banner type="warning">
|
||||
**Important:**
|
||||
|
||||
<strong>Important:</strong>
|
||||
<br />
|
||||
All preferences are stored on an individual user basis. Payload automatically recognizes the user
|
||||
that is reading or setting a preference via all provided authentication methods.
|
||||
</Banner>
|
||||
|
||||
## Use Cases
|
||||
## Use cases
|
||||
|
||||
This API is used significantly for internal operations of the Admin Panel, as mentioned above. But, if you're building your own React components for use in the Admin Panel, you can allow users to set their own preferences in correspondence to their usage of your components. For example:
|
||||
|
||||
@@ -47,7 +47,7 @@ Payload automatically creates an internally used `payload-preferences` Collectio
|
||||
|
||||
## APIs
|
||||
|
||||
Preferences are available to both [GraphQL](/docs/graphql/overview#preferences) and [REST](/docs/rest-api/overview#preferences) APIs.
|
||||
Preferences are available to both [GraphQL](/docs/graphql/overview#preferences) and [REST](/docs/rest-api/overview#) APIs.
|
||||
|
||||
## Adding or reading Preferences in your own components
|
||||
|
||||
@@ -81,7 +81,7 @@ import { usePreferences } from '@payloadcms/ui'
|
||||
|
||||
const lastUsedColorsPreferenceKey = 'last-used-colors';
|
||||
|
||||
export function CustomComponent() {
|
||||
const CustomComponent = (props) => {
|
||||
const { getPreference, setPreference } = usePreferences();
|
||||
|
||||
// Store the last used colors in local state
|
||||
@@ -154,6 +154,8 @@ export function CustomComponent() {
|
||||
</Fragment>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default CustomComponent;
|
||||
```
|
||||
|
||||
@@ -1,217 +0,0 @@
|
||||
---
|
||||
title: Preview
|
||||
label: Preview
|
||||
order: 30
|
||||
desc: Enable links to your front-end to preview published or draft content.
|
||||
keywords: admin, components, preview, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
|
||||
---
|
||||
|
||||
Preview is a feature that allows you to generate a direct link to your front-end application. When enabled, a "preview" button will appear on the Edit View within the [Admin Panel](./overview) with an href pointing to the URL you provide. This will provide your editors with a quick way of navigating to the front-end application where that Document's data is represented. Otherwise, they'd have to determine that URL themselves which is not always straightforward especially in complex apps.
|
||||
|
||||
The Preview feature can also be used to achieve something known as "Draft Preview". With Draft Preview, you can navigate to your front-end application and enter "draft mode", where your queries are modified to fetch draft content instead of published content. This is useful for seeing how your content will look before being published. [More details](#draft-preview).
|
||||
|
||||
<Banner type="warning">
|
||||
**Note:**
|
||||
Preview is different than [Live Preview](../live-preview/overview). Live Preview loads your app within an iframe and renders it in the Admin Panel allowing you to see changes in real-time. Preview, on the other hand, allows you to generate a direct link to your front-end application.
|
||||
</Banner>
|
||||
|
||||
To add Preview, pass a function to the `admin.preview` property in any [Collection Config](../configuration/collections#admin-options) or [Global Config](../configuration/globals#admin-options):
|
||||
|
||||
```ts
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
export const Pages: CollectionConfig = {
|
||||
slug: 'pages',
|
||||
admin: {
|
||||
preview: ({ slug }) => `http://localhost:3000/${slug}`,
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'slug',
|
||||
type: 'text',
|
||||
}
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
## Options
|
||||
|
||||
The `preview` function resolves to a string that points to your front-end application with additional URL parameters. This can be an absolute URL or a relative path, and can run async if needed.
|
||||
|
||||
The following arguments are provided to the `preview` function:
|
||||
|
||||
| Path | Description |
|
||||
| ------------------ | ----------------------------------------------------------------------------------------------------------------- |
|
||||
| **`doc`** | The data of the Document being edited. This includes changes that have not yet been saved. |
|
||||
| **`options`** | An object with additional properties. |
|
||||
|
||||
The `options` object contains the following properties:
|
||||
|
||||
| Path | Description |
|
||||
| ------------------ | ----------------------------------------------------------------------------------------------------------------- |
|
||||
| **`locale`** | The current locale of the Document being edited. |
|
||||
| **`req`** | The Payload Request object. |
|
||||
| **`token`** | The JWT token of the currently authenticated in user. |
|
||||
|
||||
If your application requires a fully qualified URL, such as within deploying to Vercel Preview Deployments, you can use the `req` property to build this URL:
|
||||
|
||||
```ts
|
||||
preview: (doc, { req }) => `${req.protocol}//${req.host}/${doc.slug}` // highlight-line
|
||||
```
|
||||
|
||||
## Draft Preview
|
||||
|
||||
The Preview feature can be used to achieve "Draft Preview". After clicking the preview button from the Admin Panel, you can enter into "draft mode" within your front-end application. This will allow you to adjust your page queries to include the `draft: true` param. When this param is present on the request, Payload will send back a draft document as opposed to a published one based on the document's `_status` field.
|
||||
|
||||
To enter draft mode, the URL provided to the `preview` function can point to a custom endpoint in your front-end application that sets a cookie or session variable to indicate that draft mode is enabled. This is framework specific, so the mechanisms here very from framework to framework although the underlying concept is the same.
|
||||
|
||||
### Next.js
|
||||
|
||||
If you're using Next.js, you can do the following code to enter [Draft Mode](https://nextjs.org/docs/app/building-your-application/configuring/draft-mode).
|
||||
|
||||
#### Step 1: Format the Preview URL
|
||||
|
||||
First, format your `admin.preview` function to point to a custom endpoint that you'll open on your front-end. This URL should include a few key query search params:
|
||||
|
||||
```ts
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
export const Pages: CollectionConfig = {
|
||||
slug: 'pages',
|
||||
admin: {
|
||||
preview: ({ slug, collection }) => {
|
||||
const encodedParams = new URLSearchParams({
|
||||
slug,
|
||||
collection,
|
||||
path: `/${slug}`,
|
||||
previewSecret: process.env.PREVIEW_SECRET || ''
|
||||
})
|
||||
|
||||
return `/preview?${encodedParams.toString()}` // highlight-line
|
||||
}
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'slug',
|
||||
type: 'text',
|
||||
}
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
#### Step 2: Create the Preview Route
|
||||
|
||||
Then, create an API route that verifies the preview secret, authenticates the user, and enters draft mode:
|
||||
|
||||
`/app/preview/route.ts`
|
||||
|
||||
```ts
|
||||
import type { CollectionSlug, PayloadRequest } from 'payload'
|
||||
import { getPayload } from 'payload'
|
||||
|
||||
import { draftMode } from 'next/headers'
|
||||
import { redirect } from 'next/navigation'
|
||||
|
||||
import configPromise from '@payload-config'
|
||||
|
||||
export async function GET(
|
||||
req: {
|
||||
cookies: {
|
||||
get: (name: string) => {
|
||||
value: string
|
||||
}
|
||||
}
|
||||
} & Request,
|
||||
): Promise<Response> {
|
||||
const payload = await getPayload({ config: configPromise })
|
||||
|
||||
const { searchParams } = new URL(req.url)
|
||||
|
||||
const path = searchParams.get('path')
|
||||
const collection = searchParams.get('collection') as CollectionSlug
|
||||
const slug = searchParams.get('slug')
|
||||
const previewSecret = searchParams.get('previewSecret')
|
||||
|
||||
if (previewSecret !== process.env.PREVIEW_SECRET) {
|
||||
return new Response('You are not allowed to preview this page', { status: 403 })
|
||||
}
|
||||
|
||||
if (!path || !collection || !slug) {
|
||||
return new Response('Insufficient search params', { status: 404 })
|
||||
}
|
||||
|
||||
if (!path.startsWith('/')) {
|
||||
return new Response('This endpoint can only be used for relative previews', { status: 500 })
|
||||
}
|
||||
|
||||
let user
|
||||
|
||||
try {
|
||||
user = await payload.auth({
|
||||
req: req as unknown as PayloadRequest,
|
||||
headers: req.headers,
|
||||
})
|
||||
} catch (error) {
|
||||
payload.logger.error({ err: error }, 'Error verifying token for live preview')
|
||||
return new Response('You are not allowed to preview this page', { status: 403 })
|
||||
}
|
||||
|
||||
const draft = await draftMode()
|
||||
|
||||
if (!user) {
|
||||
draft.disable()
|
||||
return new Response('You are not allowed to preview this page', { status: 403 })
|
||||
}
|
||||
|
||||
// You can add additional checks here to see if the user is allowed to preview this page
|
||||
|
||||
draft.enable()
|
||||
|
||||
redirect(path)
|
||||
}
|
||||
```
|
||||
|
||||
#### Step 3: Query Draft Content
|
||||
|
||||
Finally, in your front-end application, you can detect draft mode and adjust your queries to include drafts:
|
||||
|
||||
`/app/[slug]/page.tsx`
|
||||
|
||||
```ts
|
||||
export default async function Page({ params: paramsPromise }) {
|
||||
const { slug = 'home' } = await paramsPromise
|
||||
|
||||
const { isEnabled: isDraftMode } = await draftMode()
|
||||
|
||||
const payload = await getPayload({ config })
|
||||
|
||||
const page = await payload.find({
|
||||
collection: 'pages',
|
||||
depth: 0,
|
||||
draft: isDraftMode, // highlight-line
|
||||
limit: 1,
|
||||
overrideAccess: isDraftMode,
|
||||
where: {
|
||||
slug: {
|
||||
equals: slug,
|
||||
},
|
||||
},
|
||||
})?.then(({ docs }) => docs?.[0])
|
||||
|
||||
if (page === null) {
|
||||
return notFound()
|
||||
}
|
||||
|
||||
return (
|
||||
<main>
|
||||
<h1>{page?.title}</h1>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
<Banner type="success">
|
||||
**Note:**
|
||||
For fully working example of this, check of the official [Draft Preview Example](https://github.com/payloadcms/payload/tree/main/examples/draft-preview) in the [Examples Directory](https://github.com/payloadcms/payload/tree/main/examples).
|
||||
</Banner>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
299
docs/admin/views.mdx
Normal file
299
docs/admin/views.mdx
Normal file
@@ -0,0 +1,299 @@
|
||||
---
|
||||
title: Customizing Views
|
||||
label: Customizing Views
|
||||
order: 30
|
||||
desc:
|
||||
keywords:
|
||||
---
|
||||
|
||||
Views are the individual pages that make up the [Admin Panel](./overview), such as the Dashboard, List, and Edit views. One of the most powerful ways to customize the Admin Panel is to create Custom Views. These are [Custom Components](./components) that can either replace built-in views or can be entirely new.
|
||||
|
||||
Within the Admin Panel, there are four types of views:
|
||||
|
||||
- [Root Views](#root-views)
|
||||
- [Collection Views](#collection-views)
|
||||
- [Global Views](#global-views)
|
||||
- [Document Views](#document-views)
|
||||
|
||||
To swap in your own Custom Views, consult the list of available components. Determine the scope that corresponds to what you are trying to accomplish, then [author your React component(s)](#building-custom-views) accordingly.
|
||||
|
||||
## Root Views
|
||||
|
||||
Root Views are the main views of the [Admin Panel](./overview). These are views that are scoped directly under the `/admin` route, such as the Dashboard or Account views.
|
||||
|
||||
To easily swap Root Views with your own, or to [create entirely new ones](#adding-new-root-views), use the `admin.components.views` property of your root [Payload Config](../configuration/overview):
|
||||
|
||||
```ts
|
||||
import { buildConfig } from 'payload'
|
||||
|
||||
const config = buildConfig({
|
||||
// ...
|
||||
admin: {
|
||||
components: {
|
||||
views: {
|
||||
Dashboard: MyCustomDashboardView, // highlight-line
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
_For details on how to build Custom Views, see [Building Custom Views](#building-custom-views)._
|
||||
|
||||
The following options are available:
|
||||
|
||||
| Property | Description |
|
||||
| --------------- | ----------------------------------------------------------------------------- |
|
||||
| **`Account`** | The Account view is used to show the currently logged in user's Account page. |
|
||||
| **`Dashboard`** | The main landing page of the [Admin Panel](./overview). |
|
||||
|
||||
For more granular control, pass a configuration object instead. Payload exposes the following properties for each view:
|
||||
|
||||
| Property | Description |
|
||||
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| **`Component`** \* | Pass in the component that should be rendered when a user navigates to this route. |
|
||||
| **`path`** \* | Any valid URL path or array of paths that [`path-to-regexp`](https://www.npmjs.com/package/path-to-regex) understands. |
|
||||
| **`exact`** | Boolean. When true, will only match if the path matches the `usePathname()` exactly. |
|
||||
| **`strict`** | When true, a path that has a trailing slash will only match a `location.pathname` with a trailing slash. This has no effect when there are additional URL segments in the pathname. |
|
||||
| **`sensitive`** | When true, will match if the path is case sensitive. |
|
||||
|
||||
_\* An asterisk denotes that a property is required._
|
||||
|
||||
### Adding New Views
|
||||
|
||||
To add a _new_ views to the [Admin Panel](./overview), simply add your own key to the `views` object with at least a `path` and `Component` property. For example:
|
||||
|
||||
```ts
|
||||
import { buildConfig } from 'payload'
|
||||
|
||||
const config = buildConfig({
|
||||
// ...
|
||||
admin: {
|
||||
components: {
|
||||
views: {
|
||||
// highlight-start
|
||||
MyCustomView: {
|
||||
// highlight-end
|
||||
Component: MyCustomView,
|
||||
path: '/my-custom-view',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
The above example shows how to add a new [Root View](#root-views), but the pattern is the same for [Collection Views](#collection-views), [Global Views](#global-views), and [Document Views](#document-views). For help on how to build your own Custom Views, see [Building Custom Views](#building-custom-views).
|
||||
|
||||
<Banner type="warning">
|
||||
<strong>Note:</strong>
|
||||
<br />
|
||||
Routes are cascading, so unless explicitly given the `exact` property, they will
|
||||
match on URLs that simply _start_ with the route's path. This is helpful when creating catch-all
|
||||
routes in your application. Alternatively, define your nested route _before_ your parent
|
||||
route.
|
||||
</Banner>
|
||||
|
||||
## Collection Views
|
||||
|
||||
Collection Views are views that are scoped under the `/collections` route, such as the Collection List and Document Edit views.
|
||||
|
||||
To easily swap out Collection Views with your own, or to [create entirely new ones](#adding-new-views), use the `admin.components.views` property of your [Collection Config](../collections/overview):
|
||||
|
||||
```ts
|
||||
import type { SanitizedCollectionConfig } from 'payload'
|
||||
|
||||
export const MyCollectionConfig: SanitizedCollectionConfig = {
|
||||
// ...
|
||||
admin: {
|
||||
components: {
|
||||
views: {
|
||||
Edit: MyCustomEditView, // highlight-line
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
_For details on how to build Custom Views, see [Building Custom Views](#building-custom-views)._
|
||||
|
||||
<Banner type="warning">
|
||||
<strong>Note:</strong>
|
||||
The `Edit` property will replace the _entire_ Edit View, including the title, tabs, etc., _as well as all nested [Document Views](#document-views)_, such as the API, Live Preview, and Version views. To replace only the Edit View precisely, use the `Edit.Default` key instead.
|
||||
</Banner>
|
||||
|
||||
The following options are available:
|
||||
|
||||
| Property | Description |
|
||||
| ---------- | ----------------------------------------------------------------------------------------------------------------- |
|
||||
| **`Edit`** | The Edit View is used to edit a single document for any given Collection. [More details](#document-views). |
|
||||
| **`List`** | The List View is used to show a list of documents for any given Collection. |
|
||||
|
||||
<Banner type="success">
|
||||
<strong>Note:</strong>
|
||||
You can also add _new_ Collection Views to the config by adding a new key to the `views` object with at least a `path` and `Component` property. See [Adding New Views](#adding-new-views) for more information.
|
||||
</Banner>
|
||||
|
||||
## Global Views
|
||||
|
||||
Global Views are views that are scoped under the `/globals` route, such as the Document Edit View.
|
||||
|
||||
To easily swap out Global Views with your own or [create entirely new ones](#adding-new-views), use the `admin.components.views` property in your [Global Config](../globals/overview):
|
||||
|
||||
```ts
|
||||
import type { SanitizedGlobalConfig } from 'payload'
|
||||
|
||||
export const MyGlobalConfig: SanitizedGlobalConfig = {
|
||||
// ...
|
||||
admin: {
|
||||
components: {
|
||||
views: {
|
||||
Edit: MyCustomEditView, // highlight-line
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
_For details on how to build Custom Views, see [Building Custom Views](#building-custom-views)._
|
||||
|
||||
<Banner type="warning">
|
||||
<strong>Note:</strong>
|
||||
The `Edit` property will replace the _entire_ Edit View, including the title, tabs, etc., _as well as all nested [Document Views](#document-views)_, such as the API, Live Preview, and Version views. To replace only the Edit View precisely, use the `Edit.Default` key instead.
|
||||
</Banner>
|
||||
|
||||
The following options are available:
|
||||
|
||||
| Property | Description |
|
||||
| ---------- | ------------------------------------------------------------------- |
|
||||
| **`Edit`** | The Edit View is used to edit a single document for any given Global. [More details](#document-views). |
|
||||
|
||||
<Banner type="success">
|
||||
<strong>Note:</strong>
|
||||
You can also add _new_ Global Views to the config by adding a new key to the `views` object with at least a `path` and `Component` property. See [Adding New Views](#adding-new-views) for more information.
|
||||
</Banner>
|
||||
|
||||
## Document Views
|
||||
|
||||
Document Views are views that are scoped under the `/collections/:collectionSlug/:id` or the `/globals/:globalSlug` route, such as the Edit View or the API View. All Document Views keep their overall structure across navigation changes, such as their title and tabs, and replace only the content below.
|
||||
|
||||
To easily swap out Document Views with your own, or to [create entirely new ones](#adding-new-document-views), use the `admin.components.views.Edit[key]` property in your [Collection Config](../collections/overview) or [Global Config](../globals/overview):
|
||||
|
||||
```ts
|
||||
import type { SanitizedCollectionConfig } from 'payload'
|
||||
|
||||
export const MyCollectionOrGlobalConfig: SanitizedCollectionConfig = {
|
||||
// ...
|
||||
admin: {
|
||||
components: {
|
||||
views: {
|
||||
Edit: {
|
||||
API: {
|
||||
Component: MyCustomAPIView, // highlight-line
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
_For details on how to build Custom Views, see [Building Custom Views](#building-custom-views)._
|
||||
|
||||
<Banner type="warning">
|
||||
<strong>Note:</strong>
|
||||
If you need to replace the _entire_ Edit View, including _all_ nested Document Views, use the `Edit` key itself. See [Custom Collection Views](#collection-views) or [Custom Global Views](#global-views) for more information.
|
||||
</Banner>
|
||||
|
||||
The following options are available:
|
||||
|
||||
| Property | Description |
|
||||
| ----------------- | --------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **`Default`** | The Default view is the primary view in which your document is edited. |
|
||||
| **`Versions`** | The Versions view is used to view the version history of a single document. [More details](../versions). |
|
||||
| **`Version`** | The Version view is used to view a single version of a single document for a given collection. [More details](../versions). |
|
||||
| **`API`** | The API view is used to display the REST API JSON response for a given document. |
|
||||
| **`LivePreview`** | The LivePreview view is used to display the Live Preview interface. [More details](../live-preview). |
|
||||
|
||||
### Document Tabs
|
||||
|
||||
Each Document View can be given a new tab in the Edit View, if desired. Tabs are highly configurable, from as simple as changing the label to swapping out the entire component, they can be modified in any way. To add or customize tabs in the Edit View, use the `Component.Tab` key:
|
||||
|
||||
```ts
|
||||
import type { SanitizedCollectionConfig } from 'payload'
|
||||
|
||||
export const MyCollection: SanitizedCollectionConfig = {
|
||||
slug: 'my-collection',
|
||||
admin: {
|
||||
components: {
|
||||
views: {
|
||||
Edit: {
|
||||
MyCustomTab: {
|
||||
Component: MyCustomTab,
|
||||
path: '/my-custom-tab',
|
||||
Tab: MyCustomTab // highlight-line
|
||||
},
|
||||
AnotherCustomView: {
|
||||
Component: AnotherCustomView,
|
||||
path: '/another-custom-view',
|
||||
// highlight-start
|
||||
Tab: {
|
||||
label: 'Another Custom View',
|
||||
href: '/another-custom-view',
|
||||
}
|
||||
// highlight-end
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
<Banner type="warning">
|
||||
<strong>Note:</strong>
|
||||
This applies to _both_ Collections _and_ Globals.
|
||||
</Banner>
|
||||
|
||||
## Building Custom Views
|
||||
|
||||
Custom Views are just [Custom Components](./components) rendered at the page-level. To understand how to build Custom Views, first review the [Building Custom Components](./components#building-custom-components) guide. Once you have a Custom Component ready, you can use it as a Custom View.
|
||||
|
||||
```ts
|
||||
import type { SanitizedCollectionConfig } from 'payload'
|
||||
import { MyCustomView } from './MyCustomView'
|
||||
|
||||
export const MyCollectionConfig: SanitizedCollectionConfig = {
|
||||
// ...
|
||||
admin: {
|
||||
components: {
|
||||
views: {
|
||||
Edit: MyCustomView, // highlight-line
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Your Custom Views will be provided with the following props:
|
||||
|
||||
| Prop | Description |
|
||||
| ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | |
|
||||
| **`user`** | The currently logged in user. |
|
||||
| **`locale`** | The current [Locale](../configuration/localization) of the [Admin Panel](./overview). |
|
||||
| **`navGroups`** | The grouped navigation items according to `admin.group` in your [Collection Config](../collections/overview) or [Global Config](../globals/overview). |
|
||||
| **`params`** | An object containing the [Dynamic Route Parameters](https://nextjs.org/docs/app/building-your-application/routing/dynamic-routes). |
|
||||
| **`permissions`** | The permissions of the currently logged in user. |
|
||||
| **`searchParams`** | An object containing the [Search Parameters](https://developer.mozilla.org/docs/Learn/Common_questions/What_is_a_URL#parameters). |
|
||||
| **`visibleEntities`** | The current user's visible entities according to your [Access Control](../access-control/overview). |
|
||||
|
||||
<Banner type="success">
|
||||
<strong>Reminder:</strong>
|
||||
All [Custom Server Components](./components) receive `payload` and `i18n` by default. See [Building Custom Components](./components#building-custom-components) for more details.
|
||||
</Banner>
|
||||
|
||||
<Banner type="warning">
|
||||
<strong>Important:</strong>
|
||||
It's up to you to secure your custom views. If your view requires a user to be logged in or to
|
||||
have certain access rights, you should handle that within your view component yourself.
|
||||
</Banner>
|
||||
@@ -14,8 +14,8 @@ For example, if you have a third-party service or external app that needs to be
|
||||
1. Generate a non-expiring API key for that user to request with.
|
||||
|
||||
<Banner type="success">
|
||||
**Tip:**
|
||||
|
||||
<strong>Tip:</strong>
|
||||
<br/>
|
||||
This is particularly useful as you can create a "user" that reflects an integration with a specific external service and assign a "role" or specific access only needed by that service/integration.
|
||||
</Banner>
|
||||
|
||||
@@ -39,16 +39,16 @@ User API keys are encrypted within the database, meaning that if your database i
|
||||
your API keys will not be.
|
||||
|
||||
<Banner type="warning">
|
||||
**Important:**
|
||||
<strong>Important:</strong>
|
||||
If you change your `PAYLOAD_SECRET`, you will need to regenerate your API keys.
|
||||
|
||||
<br />
|
||||
The secret key is used to encrypt the API keys, so if you change the secret, existing API keys will
|
||||
no longer be valid.
|
||||
</Banner>
|
||||
|
||||
### HTTP Authentication
|
||||
|
||||
To authenticate REST or GraphQL API requests using an API key, set the `Authorization` header. The header is case-sensitive and needs the slug of the `auth.useAPIKey` enabled collection, then " API-Key ", followed by the `apiKey` that has been assigned. Payload's built-in middleware will then assign the user document to `req.user` and handle requests with the proper [Access Control](../access-control/overview). By doing this, Payload recognizes the request being made as a request by the user associated with that API key.
|
||||
To authenticate REST or GraphQL API requests using an API key, set the `Authorization` header. The header is case-sensitive and needs the slug of the `auth.useAPIKey` enabled collection, then " API-Key ", followed by the `apiKey` that has been assigned. Payload's built-in middleware will then assign the user document to `req.user` and handle requests with the proper access control. By doing this, Payload recognizes the request being made as a request by the user associated with that API key.
|
||||
|
||||
**For example, using Fetch:**
|
||||
|
||||
@@ -62,7 +62,7 @@ const response = await fetch('http://localhost:3000/api/pages', {
|
||||
})
|
||||
```
|
||||
|
||||
Payload ensures that the same, uniform [Access Control](../access-control/overview) is used across all authentication strategies. This enables you to utilize your existing Access Control configurations with both API keys and the standard email/password authentication. This consistency can aid in maintaining granular control over your API keys.
|
||||
Payload ensures that the same, uniform access control is used across all authentication strategies. This enables you to utilize your existing access control configurations with both API keys and the standard email/password authentication. This consistency can aid in maintaining granular control over your API keys.
|
||||
|
||||
### API Key Only Auth
|
||||
|
||||
|
||||
206
docs/authentication/config.mdx
Normal file
206
docs/authentication/config.mdx
Normal file
@@ -0,0 +1,206 @@
|
||||
---
|
||||
title: Authentication Config
|
||||
label: Config
|
||||
order: 20
|
||||
desc: Enable and customize options in the Authentication config for features including Forgot Password, Login Attempts, API key usage and more.
|
||||
keywords: authentication, config, configuration, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
|
||||
---
|
||||
|
||||
Payload's Authentication is extremely powerful and gives you everything you need when you go to build a new app or site in a secure and responsible manner.
|
||||
|
||||
To enable Authentication on a collection, define an `auth` property and set it to either `true` or to an object containing the options below.
|
||||
|
||||
## Options
|
||||
|
||||
| Option | Description |
|
||||
|----------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| **`useAPIKey`** | Payload Authentication provides for API keys to be set on each user within an Authentication-enabled Collection. [More](/docs/authentication/config#api-keys) |
|
||||
| **`tokenExpiration`** | How long (in seconds) to keep the user logged in. JWTs and HTTP-only cookies will both expire at the same time. |
|
||||
| **`maxLoginAttempts`** | Only allow a user to attempt logging in X amount of times. Automatically locks out a user from authenticating if this limit is passed. Set to `0` to disable. |
|
||||
| **`lockTime`** | Set the time (in milliseconds) that a user should be locked out if they fail authentication more times than `maxLoginAttempts` allows for. |
|
||||
| **`depth`** | How many levels deep a `user` document should be populated when creating the JWT and binding the `user` to the `req`. Defaults to `0` and should only be modified if absolutely necessary, as this will affect performance. |
|
||||
| **`cookies`** | Set cookie options, including `secure`, `sameSite`, and `domain`. For advanced users. |
|
||||
| **`forgotPassword`** | Customize the way that the `forgotPassword` operation functions. [More](/docs/authentication/config#forgot-password) |
|
||||
| **`verify`** | Set to `true` or pass an object with verification options to require users to verify by email before they are allowed to log into your app. [More](/docs/authentication/config#email-verification) |
|
||||
| **`disableLocalStrategy`** | Advanced - disable Payload's built-in local auth strategy. Only use this property if you have replaced Payload's auth mechanisms with your own. |
|
||||
| **`strategies`** | Advanced - an array of custom authentification strategies to extend this collection's authentication with. [More](/docs/authentication/custom-strategies) |
|
||||
|
||||
### Forgot Password
|
||||
|
||||
You can customize how the Forgot Password workflow operates with the following options on the `auth.forgotPassword` property:
|
||||
|
||||
**`generateEmailHTML`**
|
||||
|
||||
Function that accepts one argument, containing `{ req, token, user }`, that allows for overriding the HTML within emails that are sent to users attempting to reset their password. The function should return a string that supports HTML, which can be a full HTML email.
|
||||
|
||||
<Banner type="success">
|
||||
<strong>Tip:</strong>
|
||||
<br />
|
||||
HTML templating can be used to create custom email templates, inline CSS automatically, and more.
|
||||
You can make a reusable function that standardizes all email sent from Payload, which makes
|
||||
sending custom emails more DRY. Payload doesn't ship with an HTML templating engine, so you are
|
||||
free to choose your own.
|
||||
</Banner>
|
||||
|
||||
Example:
|
||||
|
||||
```ts
|
||||
import { CollectionConfig } from 'payload/types'
|
||||
|
||||
export const Customers: CollectionConfig = {
|
||||
slug: 'customers',
|
||||
auth: {
|
||||
forgotPassword: {
|
||||
// highlight-start
|
||||
generateEmailHTML: ({ req, token, user }) => {
|
||||
// Use the token provided to allow your user to reset their password
|
||||
const resetPasswordURL = `https://yourfrontend.com/reset-password?token=${token}`
|
||||
|
||||
return `
|
||||
<!doctype html>
|
||||
<html>
|
||||
<body>
|
||||
<h1>Here is my custom email template!</h1>
|
||||
<p>Hello, ${user.email}!</p>
|
||||
<p>Click below to reset your password.</p>
|
||||
<p>
|
||||
<a href="${resetPasswordURL}">${resetPasswordURL}</a>
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
||||
`
|
||||
},
|
||||
// highlight-end
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
<Banner type="warning">
|
||||
<strong>Important:</strong>
|
||||
<br />
|
||||
If you specify a different URL to send your users to for resetting their password, such as a page
|
||||
on the frontend of your app or similar, you need to handle making the call to the Payload REST or
|
||||
GraphQL reset-password operation yourself on your frontend, using the token that was provided for
|
||||
you. Above, it was passed via query parameter.
|
||||
</Banner>
|
||||
|
||||
**`generateEmailSubject`**
|
||||
|
||||
Similarly to the above `generateEmailHTML`, you can also customize the subject of the email. The function argument are the same but you can only return a string - not HTML.
|
||||
|
||||
Example:
|
||||
|
||||
```ts
|
||||
{
|
||||
slug: 'customers',
|
||||
auth: {
|
||||
forgotPassword: {
|
||||
// highlight-start
|
||||
generateEmailSubject: ({ req, user }) => {
|
||||
return `Hey ${user.email}, reset your password!`;
|
||||
}
|
||||
// highlight-end
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Email Verification
|
||||
|
||||
If you'd like to require email verification before a user can successfully log in, you can enable it by passing `true` or an `options` object to `auth.verify`. The following options are available:
|
||||
|
||||
**`generateEmailHTML`**
|
||||
|
||||
Function that accepts one argument, containing `{ req, token, user }`, that allows for overriding the HTML within emails that are sent to users indicating how to validate their account. The function should return a string that supports HTML, which can optionally be a full HTML email.
|
||||
|
||||
Example:
|
||||
|
||||
```ts
|
||||
import { CollectionConfig } from 'payload/types'
|
||||
|
||||
export const Customers: CollectionConfig = {
|
||||
slug: 'customers',
|
||||
auth: {
|
||||
verify: {
|
||||
// highlight-start
|
||||
generateEmailHTML: ({ req, token, user }) => {
|
||||
// Use the token provided to allow your user to verify their account
|
||||
const url = `https://yourfrontend.com/verify?token=${token}`
|
||||
|
||||
return `Hey ${user.email}, verify your email by clicking here: ${url}`
|
||||
},
|
||||
// highlight-end
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
<Banner type="warning">
|
||||
<strong>Important:</strong>
|
||||
<br />
|
||||
If you specify a different URL to send your users to for email verification, such as a page on the
|
||||
frontend of your app or similar, you need to handle making the call to the Payload REST or GraphQL
|
||||
verification operation yourself on your frontend, using the token that was provided for you.
|
||||
Above, it was passed via query parameter.
|
||||
</Banner>
|
||||
|
||||
**`generateEmailSubject`**
|
||||
|
||||
Similarly to the above `generateEmailHTML`, you can also customize the subject of the email. The function argument are the same but you can only return a string - not HTML.
|
||||
|
||||
Example:
|
||||
|
||||
```ts
|
||||
{
|
||||
slug: 'customers',
|
||||
auth: {
|
||||
forgotPassword: {
|
||||
// highlight-start
|
||||
generateEmailSubject: ({ req, user }) => {
|
||||
return `Hey ${user.email}, reset your password!`;
|
||||
}
|
||||
// highlight-end
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Admin autologin
|
||||
|
||||
For testing and demo purposes you may want to skip forcing the admin user to login in order to access the panel.
|
||||
The `admin.autologin` property is used to configure the how visitors are handled when accessing the admin panel.
|
||||
The default is that all users will have to login and this should not be enabled for environments where data needs to protected.
|
||||
|
||||
#### autoLogin Options
|
||||
|
||||
| Option | Description |
|
||||
| ----------------- | --------------------------------------------------------------------------------------------------------------- |
|
||||
| **`email`** | The email address of the user to login as |
|
||||
| **`password`** | The password of the user to login as |
|
||||
| **`prefillOnly`** | If set to true, the login credentials will be prefilled but the user will still need to click the login button. |
|
||||
|
||||
The recommended way to use this feature is behind an environment variable to ensure it is disabled when in production.
|
||||
|
||||
**Example:**
|
||||
|
||||
```ts
|
||||
export default buildConfig({
|
||||
admin: {
|
||||
user: 'users',
|
||||
// highlight-start
|
||||
autoLogin:
|
||||
process.env.PAYLOAD_PUBLIC_ENABLE_AUTOLOGIN === 'true'
|
||||
? {
|
||||
email: 'test@example.com',
|
||||
password: 'test',
|
||||
prefillOnly: true,
|
||||
}
|
||||
: false,
|
||||
// highlight-end
|
||||
},
|
||||
collections: [
|
||||
/** */
|
||||
],
|
||||
})
|
||||
```
|
||||
@@ -1,131 +0,0 @@
|
||||
---
|
||||
title: Cookie Strategy
|
||||
label: Cookie Strategy
|
||||
order: 40
|
||||
desc: Enable HTTP Cookie based authentication to interface with Payload.
|
||||
keywords: authentication, config, configuration, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
|
||||
---
|
||||
|
||||
Payload offers the ability to [Authenticate](./overview) via HTTP-only cookies. These can be read from the responses of `login`, `logout`, `refresh`, and `me` auth operations.
|
||||
|
||||
<Banner type="success">
|
||||
**Tip:**
|
||||
You can access the logged-in user from within [Access Control](../access-control/overview) and [Hooks](../hooks/overview) through the `req.user` argument. [More details](./token-data).
|
||||
</Banner>
|
||||
|
||||
### Automatic browser inclusion
|
||||
|
||||
Modern browsers automatically include `http-only` cookies when making requests directly to URLs—meaning that if you are running your API on `https://example.com`, and you have logged in and visit `https://example.com/test-page`, your browser will automatically include the Payload authentication cookie for you.
|
||||
|
||||
### HTTP Authentication
|
||||
|
||||
However, if you use `fetch` or similar APIs to retrieve Payload resources from its REST or GraphQL API, you must specify to include credentials (cookies).
|
||||
|
||||
Fetch example, including credentials:
|
||||
|
||||
```ts
|
||||
const response = await fetch('http://localhost:3000/api/pages', {
|
||||
credentials: 'include',
|
||||
})
|
||||
|
||||
const pages = await response.json()
|
||||
```
|
||||
|
||||
For more about including cookies in requests from your app to your Payload API, [read the MDN docs](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch#Sending_a_request_with_credentials_included).
|
||||
|
||||
<Banner type="success">
|
||||
**Tip:**
|
||||
To make sure you have a Payload cookie set properly in your browser after logging in, you can use
|
||||
the browsers Developer Tools > Application > Cookies > [your-domain-here]. The Developer tools
|
||||
will still show HTTP-only cookies.
|
||||
</Banner>
|
||||
|
||||
### CSRF Attacks
|
||||
|
||||
CSRF (cross-site request forgery) attacks are common and dangerous. By using an HTTP-only cookie, Payload removes many XSS vulnerabilities, however, CSRF attacks can still be possible.
|
||||
|
||||
For example, let's say you have a popular app `https://payload-finances.com` that allows users to manage finances, send and receive money. As Payload is using HTTP-only cookies, that means that browsers automatically will include cookies when sending requests to your domain - **no matter what page created the request**.
|
||||
|
||||
So, if a user of `https://payload-finances.com` is logged in and is browsing around on the internet, they might stumble onto a page with malicious intent. Let's look at an example:
|
||||
|
||||
```ts
|
||||
// malicious-intent.com
|
||||
// makes an authenticated request as on your behalf
|
||||
|
||||
const maliciousRequest = await fetch(`https://payload-finances.com/api/me`, {
|
||||
credentials: 'include'
|
||||
}).then(res => await res.json())
|
||||
```
|
||||
|
||||
In this scenario, if your cookie was still valid, malicious-intent.com would be able to make requests like the one above on your behalf. This is a CSRF attack.
|
||||
|
||||
### CSRF Prevention
|
||||
|
||||
Define domains that your trust and are willing to accept Payload HTTP-only cookie based requests from. Use the `csrf` option on the base Payload Config to do this:
|
||||
|
||||
```ts
|
||||
// payload.config.ts
|
||||
|
||||
import { buildConfig } from 'payload'
|
||||
|
||||
const config = buildConfig({
|
||||
serverURL: 'https://my-payload-instance.com',
|
||||
// highlight-start
|
||||
csrf: [
|
||||
// whitelist of domains to allow cookie auth from
|
||||
'https://your-frontend-app.com',
|
||||
'https://your-other-frontend-app.com',
|
||||
// `config.serverURL` is added by default if defined
|
||||
],
|
||||
// highlight-end
|
||||
collections: [
|
||||
// collections here
|
||||
],
|
||||
})
|
||||
|
||||
export default config
|
||||
```
|
||||
|
||||
#### Cross domain authentication
|
||||
|
||||
If your frontend is on a different domain than your Payload API then you will not be able to use HTTP-only cookies for authentication by default as they will be considered third-party cookies by the browser.
|
||||
There are a few strategies to get around this:
|
||||
|
||||
##### 1. Use subdomains
|
||||
|
||||
Cookies can cross subdomains without being considered third party cookies, for example if your API is at api.example.com then you can authenticate from example.com.
|
||||
|
||||
##### 2. Configure cookies
|
||||
|
||||
If option 1 isn't possible, then you can get around this limitation by [configuring your cookies](./overview#config-options) on your authentication collection to achieve the following setup:
|
||||
|
||||
```
|
||||
SameSite: None // allows the cookie to cross domains
|
||||
Secure: true // ensures its sent over HTTPS only
|
||||
HttpOnly: true // ensures its not accessible via client side JavaScript
|
||||
```
|
||||
|
||||
Configuration example:
|
||||
|
||||
```ts
|
||||
{
|
||||
slug: 'users',
|
||||
auth: {
|
||||
cookies: {
|
||||
sameSite: 'None',
|
||||
secure: true,
|
||||
}
|
||||
},
|
||||
fields: [
|
||||
// your auth fields here
|
||||
]
|
||||
},
|
||||
```
|
||||
|
||||
If you're configuring [cors](../production/preventing-abuse#cross-origin-resource-sharing-cors) in your Payload config, you won't be able to use a wildcard anymore, you'll need to specify the list of allowed domains.
|
||||
|
||||
|
||||
<Banner type="success">
|
||||
**Good to know:**
|
||||
Setting up `secure: true` will not work if you're developing on `http://localhost` or any non-https domain. For local development you should conditionally set this to `false` based on the environment.
|
||||
</Banner>
|
||||
@@ -13,21 +13,21 @@ keywords: authentication, config, configuration, overview, documentation, Conten
|
||||
|
||||
### Creating a strategy
|
||||
|
||||
At the core, a strategy is a way to authenticate a user making a request. As of `3.0` we moved away from [Passport](https://www.passportjs.org) in favor of pulling back the curtain and putting you in full control.
|
||||
At the core, a strategy is a way to authenticate a user making a request. As of `3.0` we moved away from passportJS in favor of pulling back the curtain and putting you in full control.
|
||||
|
||||
A strategy is made up of the following:
|
||||
|
||||
| Parameter | Description |
|
||||
| --------------------------- | ------------------------------------------------------------------------- |
|
||||
| **`name`** * | The name of your strategy |
|
||||
| **`authenticate`** * | A function that takes in the parameters below and returns a user or null. |
|
||||
| **`name`** \* | The name of your strategy |
|
||||
| **`authenticate`** \* | A function that takes in the parameters below and returns a user or null. |
|
||||
|
||||
The `authenticate` function is passed the following arguments:
|
||||
|
||||
| Argument | Description |
|
||||
| ------------------- | ------------------------------------------------------------------------------------------------- |
|
||||
| **`headers`** * | The headers on the incoming request. Useful for retrieving identifiable information on a request. |
|
||||
| **`payload`** * | The Payload class. Useful for authenticating the identifiable information against Payload. |
|
||||
| **`headers`** \* | The headers on the incoming request. Useful for retrieving identifiable information on a request. |
|
||||
| **`payload`** \* | The Payload class. Useful for authenticating the identifiable information against Payload. |
|
||||
| **`isGraphQL`** | Whether or not the request was made from a GraphQL endpoint. Default is `false`. |
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ At its core a strategy simply takes information from the incoming request and re
|
||||
Your `authenticate` method should return an object containing a Payload user document and any optional headers that you'd like Payload to set for you when we return a response.
|
||||
|
||||
```ts
|
||||
import type { CollectionConfig } from 'payload'
|
||||
import { CollectionConfig } from 'payload'
|
||||
|
||||
export const Users: CollectionConfig = {
|
||||
slug: 'users',
|
||||
@@ -62,12 +62,9 @@ export const Users: CollectionConfig = {
|
||||
})
|
||||
|
||||
return {
|
||||
// Send the user with the collection slug back to authenticate,
|
||||
// Send the user back to authenticate,
|
||||
// or send null if no user should be authenticated
|
||||
user: usersQuery.docs[0] ? {
|
||||
collection: 'users'
|
||||
...usersQuery.docs[0],
|
||||
} : null,
|
||||
user: usersQuery.docs[0] || null,
|
||||
|
||||
// Optionally, you can return headers
|
||||
// that you'd like Payload to set here when
|
||||
|
||||
@@ -1,204 +0,0 @@
|
||||
---
|
||||
title: Authentication Emails
|
||||
label: Email Verification
|
||||
order: 30
|
||||
desc: Email Verification allows users to verify their email address before they're account is fully activated. Email Verification ties directly into the Email functionality that Payload provides.
|
||||
keywords: authentication, email, config, configuration, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
|
||||
---
|
||||
|
||||
[Authentication](./overview) ties directly into the [Email](../email/overview) functionality that Payload provides. This allows you to send emails to users for verification, password resets, and more. While Payload provides default email templates for these actions, you can customize them to fit your brand.
|
||||
|
||||
## Email Verification
|
||||
|
||||
Email Verification forces users to prove they have access to the email address they can authenticate. This will help to reduce spam accounts and ensure that users are who they say they are.
|
||||
|
||||
To enable Email Verification, use the `auth.verify` property on your [Collection Config](../configuration/collections):
|
||||
|
||||
```ts
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
export const Customers: CollectionConfig = {
|
||||
// ...
|
||||
auth: {
|
||||
verify: true // highlight-line
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
<Banner type="info">
|
||||
**Tip:**
|
||||
Verification emails are fully customizable. [More details](#generateemailhtml).
|
||||
</Banner>
|
||||
|
||||
The following options are available:
|
||||
|
||||
| Option | Description |
|
||||
| -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **`generateEmailHTML`** | Allows for overriding the HTML within emails that are sent to users indicating how to validate their account. [More details](#generateemailhtml). |
|
||||
| **`generateEmailSubject`** | Allows for overriding the subject of the email that is sent to users indicating how to validate their account. [More details](#generateemailsubject). |
|
||||
|
||||
#### generateEmailHTML
|
||||
|
||||
Function that accepts one argument, containing `{ req, token, user }`, that allows for overriding the HTML within emails that are sent to users indicating how to validate their account. The function should return a string that supports HTML, which can optionally be a full HTML email.
|
||||
|
||||
```ts
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
export const Customers: CollectionConfig = {
|
||||
// ...
|
||||
auth: {
|
||||
verify: {
|
||||
// highlight-start
|
||||
generateEmailHTML: ({ req, token, user }) => {
|
||||
// Use the token provided to allow your user to verify their account
|
||||
const url = `https://yourfrontend.com/verify?token=${token}`
|
||||
|
||||
return `Hey ${user.email}, verify your email by clicking here: ${url}`
|
||||
},
|
||||
// highlight-end
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
<Banner type="warning">
|
||||
**Important:**
|
||||
If you specify a different URL to send your users to for email verification, such as a page on the
|
||||
frontend of your app or similar, you need to handle making the call to the Payload REST or GraphQL
|
||||
verification operation yourself on your frontend, using the token that was provided for you.
|
||||
Above, it was passed via query parameter.
|
||||
</Banner>
|
||||
|
||||
#### generateEmailSubject
|
||||
|
||||
Similarly to the above `generateEmailHTML`, you can also customize the subject of the email. The function argument are the same but you can only return a string - not HTML.
|
||||
|
||||
```ts
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
export const Customers: CollectionConfig = {
|
||||
// ...
|
||||
auth: {
|
||||
verify: {
|
||||
// highlight-start
|
||||
generateEmailSubject: ({ req, user }) => {
|
||||
return `Hey ${user.email}, reset your password!`;
|
||||
}
|
||||
// highlight-end
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Forgot Password
|
||||
|
||||
You can customize how the Forgot Password workflow operates with the following options on the `auth.forgotPassword` property:
|
||||
|
||||
```ts
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
export const Customers: CollectionConfig = {
|
||||
// ...
|
||||
auth: {
|
||||
forgotPassword: { // highlight-line
|
||||
// ...
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
The following options are available:
|
||||
|
||||
| Option | Description |
|
||||
|----------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| **`expiration`** | Configure how long password reset tokens remain valid, specified in milliseconds. |
|
||||
| **`generateEmailHTML`** | Allows for overriding the HTML within emails that are sent to users attempting to reset their password. [More details](#generateEmailHTML). |
|
||||
| **`generateEmailSubject`** | Allows for overriding the subject of the email that is sent to users attempting to reset their password. [More details](#generateEmailSubject). |
|
||||
|
||||
#### generateEmailHTML
|
||||
|
||||
This function allows for overriding the HTML within emails that are sent to users attempting to reset their password. The function should return a string that supports HTML, which can be a full HTML email.
|
||||
|
||||
```ts
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
export const Customers: CollectionConfig = {
|
||||
// ...
|
||||
auth: {
|
||||
forgotPassword: {
|
||||
// highlight-start
|
||||
generateEmailHTML: ({ req, token, user }) => {
|
||||
// Use the token provided to allow your user to reset their password
|
||||
const resetPasswordURL = `https://yourfrontend.com/reset-password?token=${token}`
|
||||
|
||||
return `
|
||||
<!doctype html>
|
||||
<html>
|
||||
<body>
|
||||
<h1>Here is my custom email template!</h1>
|
||||
<p>Hello, ${user.email}!</p>
|
||||
<p>Click below to reset your password.</p>
|
||||
<p>
|
||||
<a href="${resetPasswordURL}">${resetPasswordURL}</a>
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
||||
`
|
||||
},
|
||||
// highlight-end
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
<Banner type="warning">
|
||||
**Important:**
|
||||
If you specify a different URL to send your users to for resetting their password, such as a page
|
||||
on the frontend of your app or similar, you need to handle making the call to the Payload REST or
|
||||
GraphQL reset-password operation yourself on your frontend, using the token that was provided for
|
||||
you. Above, it was passed via query parameter.
|
||||
</Banner>
|
||||
|
||||
<Banner type="success">
|
||||
**Tip:**
|
||||
HTML templating can be used to create custom email templates, inline CSS automatically, and more.
|
||||
You can make a reusable function that standardizes all email sent from Payload, which makes
|
||||
sending custom emails more DRY. Payload doesn't ship with an HTML templating engine, so you are
|
||||
free to choose your own.
|
||||
</Banner>
|
||||
|
||||
The following arguments are passed to the `generateEmailHTML` function:
|
||||
|
||||
| Argument | Description |
|
||||
|----------|-----------------------------------------------------------------------------------------------|
|
||||
| `req` | The request object. |
|
||||
| `token` | The token that is generated for the user to reset their password. |
|
||||
| `user` | The user document that is attempting to reset their password. |
|
||||
|
||||
#### generateEmailSubject
|
||||
|
||||
Similarly to the above `generateEmailHTML`, you can also customize the subject of the email. The function argument are the same but you can only return a string - not HTML.
|
||||
|
||||
```ts
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
export const Customers: CollectionConfig = {
|
||||
// ...
|
||||
auth: {
|
||||
forgotPassword: {
|
||||
// highlight-start
|
||||
generateEmailSubject: ({ req, user }) => {
|
||||
return `Hey ${user.email}, reset your password!`;
|
||||
}
|
||||
// highlight-end
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The following arguments are passed to the `generateEmailSubject` function:
|
||||
|
||||
| Argument | Description |
|
||||
|----------|-----------------------------------------------------------------------------------------------|
|
||||
| `req` | The request object. |
|
||||
| `user` | The user document that is attempting to reset their password. |
|
||||
91
docs/authentication/http-only-cookies.mdx
Normal file
91
docs/authentication/http-only-cookies.mdx
Normal file
@@ -0,0 +1,91 @@
|
||||
---
|
||||
title: Cookie Strategy
|
||||
label: Cookie Strategy
|
||||
order: 30
|
||||
desc: Enable HTTP Cookie based authentication to interface with Payload.
|
||||
keywords: authentication, config, configuration, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
|
||||
---
|
||||
|
||||
Payload `login`, `logout`, and `refresh` operations make use of HTTP-only cookies for authentication purposes.
|
||||
|
||||
<Banner type="success">
|
||||
<strong>Tip:</strong>
|
||||
<br />
|
||||
You can access the logged-in user from access control functions and hooks from the `req.user` property.
|
||||
<br />
|
||||
[Learn more about token data](/docs/authentication/token-data).
|
||||
</Banner>
|
||||
|
||||
### Automatic browser inclusion
|
||||
|
||||
Modern browsers automatically include `http-only` cookies when making requests directly to URLs—meaning that if you are running your API on `https://example.com`, and you have logged in and visit `https://example.com/test-page`, your browser will automatically include the Payload authentication cookie for you.
|
||||
|
||||
### HTTP Authentication
|
||||
|
||||
However, if you use `fetch` or similar APIs to retrieve Payload resources from its REST or GraphQL API, you must specify to include credentials (cookies).
|
||||
|
||||
Fetch example, including credentials:
|
||||
|
||||
```ts
|
||||
const response = await fetch('http://localhost:3000/api/pages', {
|
||||
credentials: 'include',
|
||||
})
|
||||
|
||||
const pages = await response.json()
|
||||
```
|
||||
|
||||
For more about including cookies in requests from your app to your Payload API, [read the MDN docs](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch#Sending_a_request_with_credentials_included).
|
||||
|
||||
<Banner type="success">
|
||||
<strong>Tip:</strong>
|
||||
<br />
|
||||
To make sure you have a Payload cookie set properly in your browser after logging in, you can use
|
||||
the browsers Developer Tools > Application > Cookies > [your-domain-here]. The Developer tools
|
||||
will still show HTTP-only cookies.
|
||||
</Banner>
|
||||
|
||||
### CSRF Attacks
|
||||
|
||||
CSRF (cross-site request forgery) attacks are common and dangerous. By using an HTTP-only cookie, Payload removes many XSS vulnerabilities, however, CSRF attacks can still be possible.
|
||||
|
||||
For example, let's say you have a popular app `https://payload-finances.com` that allows users to manage finances, send and receive money. As Payload is using HTTP-only cookies, that means that browsers automatically will include cookies when sending requests to your domain - <strong>no matter what page created the request</strong>.
|
||||
|
||||
So, if a user of `https://payload-finances.com` is logged in and is browsing around on the internet, they might stumble onto a page with malicious intent. Let's look at an example:
|
||||
|
||||
```ts
|
||||
// malicious-intent.com
|
||||
// makes an authenticated request as on your behalf
|
||||
|
||||
const maliciousRequest = await fetch(`https://payload-finances.com/api/me`, {
|
||||
credentials: 'include'
|
||||
}).then(res => await res.json())
|
||||
```
|
||||
|
||||
In this scenario, if your cookie was still valid, malicious-intent.com would be able to make requests like the one above on your behalf. This is a CSRF attack.
|
||||
|
||||
### CSRF Prevention
|
||||
|
||||
Define domains that your trust and are willing to accept Payload HTTP-only cookie based requests from. Use the `csrf` option on the base Payload config to do this:
|
||||
|
||||
```ts
|
||||
// payload.config.ts
|
||||
|
||||
import { buildConfig } from 'payload'
|
||||
|
||||
const config = buildConfig({
|
||||
serverURL: 'https://my-payload-instance.com',
|
||||
// highlight-start
|
||||
csrf: [
|
||||
// whitelist of domains to allow cookie auth from
|
||||
'https://your-frontend-app.com',
|
||||
'https://your-other-frontend-app.com',
|
||||
// `config.serverURL` is added by default if defined
|
||||
],
|
||||
// highlight-end
|
||||
collections: [
|
||||
// collections here
|
||||
],
|
||||
})
|
||||
|
||||
export default config
|
||||
```
|
||||
@@ -6,11 +6,14 @@ desc: Enable JSON Web Token based authentication to interface with Payload.
|
||||
keywords: authentication, config, configuration, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
|
||||
---
|
||||
|
||||
Payload offers the ability to [Authenticate](./overview) via JSON Web Tokens (JWT). These can be read from the responses of `login`, `logout`, `refresh`, and `me` auth operations.
|
||||
Payload offers the ability to authenticate via `JWT` (JSON web token). These can be read from the responses of `login`, `refresh`, and `me` auth operations.
|
||||
|
||||
<Banner type="success">
|
||||
**Tip:**
|
||||
You can access the logged-in user from within [Access Control](../access-control/overview) and [Hooks](../hooks/overview) through the `req.user` argument. [More details](./token-data).
|
||||
<strong>Tip:</strong>
|
||||
<br />
|
||||
You can access the logged-in user from access control functions and hooks from the `req.user` property.
|
||||
<br />
|
||||
[Learn more about token data](/docs/authentication/token-data).
|
||||
</Banner>
|
||||
|
||||
### Identifying Users Via The Authorization Header
|
||||
@@ -45,7 +48,7 @@ import type { CollectionConfig } from 'payload'
|
||||
export const UsersWithoutJWTs: CollectionConfig = {
|
||||
slug: 'users-without-jwts',
|
||||
auth: {
|
||||
removeTokenFromResponse: true, // highlight-line
|
||||
removeTokenFromRepsonse: true, // highlight-line
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
---
|
||||
title: Authentication Operations
|
||||
label: Operations
|
||||
order: 20
|
||||
order: 80
|
||||
desc: Enabling Authentication automatically makes key operations available such as Login, Logout, Verify, Unlock, Reset Password and more.
|
||||
keywords: authentication, config, configuration, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
|
||||
---
|
||||
|
||||
Enabling [Authentication](./overview) on a [Collection](../configuration/collections) automatically exposes additional auth-based operations in the [Local API](../local-api/overview), [REST API](../rest-api/overview), and [GraphQL API](../graphql/overview).
|
||||
Enabling Authentication on a Collection automatically exposes additional auth-based operations in the Local, REST, and GraphQL APIs.
|
||||
|
||||
## Access
|
||||
|
||||
The Access operation returns what a logged in user can and can't do with the collections and globals that are registered via your config. This data can be immensely helpful if your app needs to show and hide certain features based on [Access Control](../access-control/overview), just as the [Admin Panel](../admin/overview) does.
|
||||
The Access operation returns what a logged in user can and can't do with the collections and globals that are registered via your config. This data can be immensely helpful if your app needs to show and hide certain features based on access control, as the [Admin Panel](../admin/overview) does.
|
||||
|
||||
**REST API endpoint**:
|
||||
|
||||
@@ -269,15 +269,11 @@ const result = await payload.verifyEmail({
|
||||
})
|
||||
```
|
||||
|
||||
**Note:** the token you need to pass to the `verifyEmail` function is unique to verification and is not the same as the token that you can retrieve from the `forgotPassword` operation. It can be found on the user document, as a hidden `_verificationToken` field. If you'd like to retrieve this token, you can use the Local API's `find` or `findByID` methods, setting `showHiddenFields: true`.
|
||||
|
||||
**Note:** if you do not have a `config.serverURL` set, Payload will attempt to create one for you if the user was created via REST or GraphQL by looking at the incoming `req`. But this is not supported if you are creating the user via the Local API's `payload.create()` method. If this applies to you, and you do not have a `serverURL` set, you may want to override your `verify.generateEmailHTML` function to provide a full URL to link the user to a proper verification page.
|
||||
|
||||
## Unlock
|
||||
|
||||
If a user locks themselves out and you wish to deliberately unlock them, you can utilize the Unlock operation. The [Admin Panel](../admin/overview) features an Unlock control automatically for all collections that feature max login attempts, but you can programmatically unlock users as well by using the Unlock operation.
|
||||
|
||||
To restrict who is allowed to unlock users, you can utilize the [`unlock`](../access-control/collections#unlock) access control function.
|
||||
To restrict who is allowed to unlock users, you can utilize the [`unlock`](../access-control/overview#unlock) access control function.
|
||||
|
||||
**Example REST API unlock**:
|
||||
|
||||
@@ -312,7 +308,7 @@ Payload comes with built-in forgot password functionality. Submitting an email a
|
||||
|
||||
The link to reset the user's password contains a token which is what allows the user to securely reset their password.
|
||||
|
||||
By default, the Forgot Password operations send users to the [Admin Panel](../admin/overview) to reset their password, but you can customize the generated email to send users to the frontend of your app instead by [overriding the email HTML](/docs/authentication/email#forgot-password).
|
||||
By default, the Forgot Password operations send users to the [Admin Panel](../admin/overview) to reset their password, but you can customize the generated email to send users to the frontend of your app instead by [overriding the email HTML](/docs/authentication/config#forgot-password).
|
||||
|
||||
**Example REST API Forgot Password**:
|
||||
|
||||
@@ -348,13 +344,9 @@ const token = await payload.forgotPassword({
|
||||
})
|
||||
```
|
||||
|
||||
<Banner type="info">
|
||||
**Note:** if you do not have a `config.serverURL` set, Payload will attempt to create one for you if the `forgot-password` operation was triggered via REST or GraphQL by looking at the incoming `req`. But this is not supported if you are calling `payload.forgotPassword()` via the Local API. If you do not have a `serverURL` set, you may want to override your `auth.forgotPassword.generateEmailHTML` function to provide a full URL to link the user to a proper reset-password page.
|
||||
</Banner>
|
||||
|
||||
<Banner type="success">
|
||||
**Tip:**
|
||||
|
||||
<strong>Tip:</strong>
|
||||
<br />
|
||||
You can stop the reset-password email from being sent via using the local API. This is helpful if
|
||||
you need to create user accounts programmatically, but not set their password for them. This
|
||||
effectively generates a reset password token which you can then use to send to a page you create,
|
||||
|
||||
@@ -11,47 +11,38 @@ keywords: authentication, config, configuration, overview, documentation, Conten
|
||||
title="Simplified Authentication for Headless CMS: Unlocking Reusability in One Line"
|
||||
/>
|
||||
|
||||
Authentication is a critical part of any application. Payload provides a secure, portable way to manage user accounts out of the box. Payload Authentication is designed to be used in both the [Admin Panel](../admin/overview), all well as your own external applications, completely eliminating the need for paid, third-party platforms and services.
|
||||
|
||||
Here are some common use cases of Authentication in your own applications:
|
||||
|
||||
- Customer accounts for an e-commerce app
|
||||
- User accounts for a SaaS product
|
||||
- P2P apps or social sites where users need to log in and manage their profiles
|
||||
- Online games where players need to track their progress over time
|
||||
|
||||
When Authentication is enabled on a [Collection](../configuration/collections), Payload injects all necessary functionality to support the entire user flow. This includes all [auth-related operations](./operations) like account creation, logging in and out, and resetting passwords, all [auth-related emails](./email) like email verification and password reset, as well as any necessary UI to manage users from the Admin Panel.
|
||||
|
||||
To enable Authentication on a Collection, use the `auth` property in the [Collection Config](../configuration/collections#config-options):
|
||||
|
||||
```ts
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
export const Users: CollectionConfig = {
|
||||
// ...
|
||||
auth: true, // highlight-line
|
||||
}
|
||||
```
|
||||
|
||||

|
||||
_Admin Panel screenshot depicting an Admins Collection with Auth enabled_
|
||||
|
||||
## Config Options
|
||||
|
||||
Any [Collection](../configuration/collections) can opt-in to supporting Authentication. Once enabled, each Document that is created within the Collection can be thought of as a "user". This enables a complete authentication workflow on your Collection, such as logging in and out, resetting their password, and more.
|
||||
|
||||
<Banner type="warning">
|
||||
**Note:**
|
||||
By default, Payload provides an auth-enabled `User` Collection which is used to access the Admin Panel. [More details](../admin/overview#the-admin-user-collection).
|
||||
<Banner>
|
||||
Payload provides for highly secure and customizable user Authentication out of the box, which
|
||||
allows for users to identify themselves to Payload.
|
||||
</Banner>
|
||||
|
||||
To enable Authentication on a Collection, use the `auth` property in the [Collection Config](../configuration/collections):
|
||||
Authentication is used within the [Admin Panel](../admin/overview) itself as well as throughout your app(s) themselves however you determine necessary.
|
||||
|
||||

|
||||
_Admin Panel screenshot depicting an Admins Collection with Auth enabled_
|
||||
|
||||
**Here are some common use cases of Authentication outside of Payload's dashboard itself:**
|
||||
|
||||
- Customer accounts for an ecommerce app
|
||||
- Customer accounts for a SaaS product
|
||||
- P2P app or social site where users need to log in and manage their profiles
|
||||
- Online game where players need to track their progress over time
|
||||
|
||||
By default, Payload provides you with a `User` collection that supports Authentication, which is used to access the [Admin Panel](../admin/overview). But, you can add support to one or many Collections of your own. For more information on how to customize, override, or remove the default `User` collection, [click here](/docs/admin/overview#the-admin-user-collection).
|
||||
|
||||
## Enabling Auth on a collection
|
||||
|
||||
Every Payload Collection can opt-in to supporting Authentication by specifying the `auth` property on the Collection's config to either `true` or to an object containing `auth` options.
|
||||
|
||||
**For a full list of all `auth` options, [click here](/docs/authentication/config).**
|
||||
|
||||
Simple example collection:
|
||||
|
||||
```ts
|
||||
import type { CollectionConfig } from 'payload'
|
||||
import { CollectionConfig } from 'payload/types'
|
||||
|
||||
export const Admins: CollectionConfig = {
|
||||
// ...
|
||||
slug: 'admins',
|
||||
// highlight-start
|
||||
auth: {
|
||||
tokenExpiration: 7200, // How many seconds to keep the user logged in
|
||||
@@ -61,137 +52,53 @@ export const Admins: CollectionConfig = {
|
||||
// More options are available
|
||||
},
|
||||
// highlight-end
|
||||
}
|
||||
```
|
||||
|
||||
<Banner type="info">
|
||||
**Tip:**
|
||||
For default auth behavior, set `auth: true`. This is a good starting point for most applications.
|
||||
</Banner>
|
||||
|
||||
<Banner type="warning">
|
||||
**Note:**
|
||||
Auth-enabled Collections with be automatically injected with the `hash`, `salt`, and `email` fields. [More details](../fields/overview#field-names).
|
||||
</Banner>
|
||||
|
||||
The following options are available:
|
||||
|
||||
| Option | Description |
|
||||
|----------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| **`cookies`** | Set cookie options, including `secure`, `sameSite`, and `domain`. For advanced users. |
|
||||
| **`depth`** | How many levels deep a `user` document should be populated when creating the JWT and binding the `user` to the `req`. Defaults to `0` and should only be modified if absolutely necessary, as this will affect performance. |
|
||||
| **`disableLocalStrategy`** | Advanced - disable Payload's built-in local auth strategy. Only use this property if you have replaced Payload's auth mechanisms with your own. |
|
||||
| **`forgotPassword`** | Customize the way that the `forgotPassword` operation functions. [More details](./email#forgot-password). |
|
||||
| **`lockTime`** | Set the time (in milliseconds) that a user should be locked out if they fail authentication more times than `maxLoginAttempts` allows for. |
|
||||
| **`loginWithUsername`** | Ability to allow users to login with username/password. [More](/docs/authentication/overview#login-with-username) |
|
||||
| **`maxLoginAttempts`** | Only allow a user to attempt logging in X amount of times. Automatically locks out a user from authenticating if this limit is passed. Set to `0` to disable. |
|
||||
| **`removeTokenFromResponses`** | Set to true if you want to remove the token from the returned authentication API responses such as login or refresh. |
|
||||
| **`strategies`** | Advanced - an array of custom authentication strategies to extend this collection's authentication with. [More details](./custom-strategies). |
|
||||
| **`tokenExpiration`** | How long (in seconds) to keep the user logged in. JWTs and HTTP-only cookies will both expire at the same time. |
|
||||
| **`useAPIKey`** | Payload Authentication provides for API keys to be set on each user within an Authentication-enabled Collection. [More details](./api-keys). |
|
||||
| **`verify`** | Set to `true` or pass an object with verification options to require users to verify by email before they are allowed to log into your app. [More details](./email#email-verification). |
|
||||
|
||||
### Login With Username
|
||||
|
||||
You can allow users to login with their username instead of their email address by setting the `loginWithUsername` property to `true`.
|
||||
|
||||
Example:
|
||||
|
||||
```ts
|
||||
{
|
||||
slug: 'customers',
|
||||
auth: {
|
||||
loginWithUsername: true,
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Or, you can pass an object with additional options:
|
||||
|
||||
```ts
|
||||
{
|
||||
slug: 'customers',
|
||||
auth: {
|
||||
loginWithUsername: {
|
||||
allowEmailLogin: true, // default: false
|
||||
requireEmail: false, // default: false
|
||||
fields: [
|
||||
{
|
||||
name: 'role',
|
||||
type: 'select',
|
||||
required: true,
|
||||
options: ['user', 'admin', 'editor', 'developer'],
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
**`allowEmailLogin`**
|
||||
**By enabling Authentication on a config, the following modifications will automatically be made to your Collection:**
|
||||
|
||||
If set to `true`, users can log in with either their username or email address. If set to `false`, users can only log in with their username.
|
||||
1. `email` as well as password `salt` & `hash` fields will be added to your Collection's schema
|
||||
1. The [Admin Panel](../admin/overview) will feature a new set of corresponding UI to allow for changing password and editing email
|
||||
1. [A new set of `operations`](/docs/authentication/operations) will be exposed via Payload's REST, Local, and GraphQL APIs
|
||||
|
||||
**`requireEmail`**
|
||||
Once enabled, each document that is created within the Collection can be thought of as a `user` - who can make use of commonly required authentication functions such as logging in / out, resetting their password, and more.
|
||||
|
||||
If set to `true`, an email address is required when creating a new user. If set to `false`, email is not required upon creation.
|
||||
## Authentication Strategies
|
||||
|
||||
## Auto-Login
|
||||
|
||||
For testing and demo purposes you may want to skip forcing the user to login in order to access your application. Typically, all users should be required to login, however, you can speed up local development time by enabling auto-login.
|
||||
|
||||
To enable auto-login, set the `autoLogin` property in the [Payload Config](../admin/overview#admin-options):
|
||||
|
||||
```ts
|
||||
import { buildConfig } from 'payload'
|
||||
|
||||
export default buildConfig({
|
||||
// ...
|
||||
// highlight-start
|
||||
autoLogin:
|
||||
process.env.NEXT_PUBLIC_ENABLE_AUTOLOGIN === 'true'
|
||||
? {
|
||||
email: 'test@example.com',
|
||||
password: 'test',
|
||||
prefillOnly: true,
|
||||
}
|
||||
: false,
|
||||
// highlight-end
|
||||
})
|
||||
```
|
||||
|
||||
<Banner type="warning">
|
||||
**Warning:**
|
||||
The recommended way to use this feature is behind an [Environment Variable](../configuration/environment-vars). This will ensure it is _disabled_ in production.
|
||||
</Banner>
|
||||
|
||||
The following options are available:
|
||||
|
||||
| Option | Description |
|
||||
|-------------------|-----------------------------------------------------------------------------------------------------------------|
|
||||
| **`username`** | The username of the user to login as |
|
||||
| **`email`** | The email address of the user to login as |
|
||||
| **`password`** | The password of the user to login as. This is only needed if `prefillOnly` is set to true |
|
||||
| **`prefillOnly`** | If set to true, the login credentials will be prefilled but the user will still need to click the login button. |
|
||||
|
||||
## Operations
|
||||
|
||||
All auth-related operations are available via Payload's REST, Local, and GraphQL APIs. These operations are automatically added to your Collection when you enable Authentication. [More details](./operations).
|
||||
|
||||
## Strategies
|
||||
|
||||
Out of the box Payload ships with a three powerful Authentication strategies:
|
||||
|
||||
- [HTTP-Only Cookies](./cookies)
|
||||
- [JSON Web Tokens (JWT)](./jwt)
|
||||
- [API-Keys](./api-keys)
|
||||
|
||||
Each of these strategies can work together or independently. You can also create your own custom strategies to fit your specific needs. [More details](./custom-strategies).
|
||||
Out of the box Payload ships with a few powerful authentication strategies. HTTP-Only Cookies, JWT's and API-Keys, they can work together or individually. You can also have multiple collections that have auth enabled, but only 1 of them can be used to log into the admin panel.
|
||||
|
||||
### HTTP-Only Cookies
|
||||
|
||||
[HTTP-only cookies](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies) are a highly secure method of storing identifiable data on a user's device so that Payload can automatically recognize a returning user until their cookie expires. They are totally protected from common XSS attacks and **cannot be read by JavaScript in the browser**, unlike JWT's. [More details](./cookies).
|
||||
HTTP-only cookies are a highly secure method of storing identifiable data on a user's device so that Payload can automatically recognize a returning user until their cookie expires. They are totally protected from common XSS attacks and <strong>cannot be read by JavaScript in the browser</strong>, unlike JWT's.
|
||||
|
||||
You can learn more about this strategy from the [HTTP-Only Cookies](/docs/authentication/http-only-cookies) docs.
|
||||
|
||||
### JSON Web Tokens
|
||||
|
||||
JWT (JSON Web Tokens) can also be utilized to perform authentication. Tokens are generated on `login`, `refresh` and `me` operations and can be attached to future requests to authenticate users. [More details](./jwt).
|
||||
JWT (JSON Web Tokens) can also be utilized to perform authentication. Tokens are generated on `login`, `refresh` and `me` operations and can be attached to future requests to authenticate users.
|
||||
|
||||
You can learn more about this strategy from the [JWT](/docs/authentication/jwt) docs.
|
||||
|
||||
### API Keys
|
||||
|
||||
API Keys can be enabled on auth collections. These are particularly useful when you want to authenticate against Payload from a third party service. [More details](./api-keys).
|
||||
API Keys can be enabled on auth collections. These are particularly useful when you want to authenticate against Payload from a third party service.
|
||||
|
||||
You can learn more about this strategy from the [API Keys](/docs/authentication/api-keys) docs.
|
||||
|
||||
### Custom Strategies
|
||||
|
||||
There are cases where these may not be enough for your application. Payload is extendable by design so you can wire up your own strategy when you need to. [More details](./custom-strategies).
|
||||
There are cases where these may not be enough for your application. Payload is extendable by design so you can wire up your own strategy when you need to.
|
||||
|
||||
You can learn more about custom strategies from the [Custom Strategies](/docs/authentication/custom-strategies) docs.
|
||||
|
||||
## Logging in / out, resetting password, etc.
|
||||
|
||||
[Click here](/docs/authentication/operations) for a list of all automatically-enabled Auth operations, including `login`, `logout`, `refresh`, and others.
|
||||
|
||||
@@ -6,7 +6,7 @@ desc: Storing data for read on the request object.
|
||||
keywords: authentication, config, configuration, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
|
||||
---
|
||||
|
||||
During the lifecycle of a request you will be able to access the data you have configured to be stored in the JWT by accessing `req.user`. The user object is automatically appended to the request for you.
|
||||
During the lifecycle of a request you will be able to access the data you have configured to be stored in the JWT by accessing `req.user`. The user object is automatically appeneded to the request for you.
|
||||
|
||||
### Definining Token Data
|
||||
|
||||
@@ -70,15 +70,15 @@ export const Users: CollectionConfig = {
|
||||
```
|
||||
|
||||
<Banner type="success">
|
||||
**Tip:**
|
||||
|
||||
<strong>Tip:</strong>
|
||||
<br/>
|
||||
If you wish to use a different key other than the field `name`, you can define `saveToJWT` as a string.
|
||||
</Banner>
|
||||
|
||||
|
||||
### Using Token Data
|
||||
|
||||
This is especially helpful when writing [Hooks](../hooks/overview) and [Access Control](../access-control/overview) that depend on user defined fields.
|
||||
This is especially helful when writing hooks and access control that depend on user defined fields.
|
||||
|
||||
```ts
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
@@ -54,7 +54,7 @@ Any of the features in Payload Cloud that require environment variables will aut
|
||||
Payment methods can be set per project and can be updated any time. You can use team’s default payment method, or add a new one. Modify your payment methods in your Project settings / Team settings.
|
||||
|
||||
<Banner type="success">
|
||||
**Note:** All Payload Cloud teams that deploy a project require a card on file. This
|
||||
<strong>Note:</strong> All Payload Cloud teams that deploy a project require a card on file. This
|
||||
helps us prevent fraud and abuse on our platform. If you select a plan with a free trial, you will
|
||||
not be charged until your trial period is over. We’ll remind you 7 days before your trial ends and
|
||||
you can cancel anytime.
|
||||
|
||||
@@ -31,7 +31,7 @@ Next, select your `GitHub Scope`. If you belong to multiple organizations, they
|
||||
After selecting your scope, create a unique `repository name` and select whether you want your repository to be public or private on GitHub.
|
||||
|
||||
<Banner type="warning">
|
||||
**Note:** Public repositories can be accessed by anyone online, while private
|
||||
<strong>Note:</strong> Public repositories can be accessed by anyone online, while private
|
||||
repositories grant access only to you and anyone you explicitly authorize.
|
||||
</Banner>
|
||||
|
||||
@@ -45,7 +45,7 @@ Payload Cloud works for any Node.js + MongoDB app. From the New Project page, se
|
||||
_Creating a new project from an existing repository._
|
||||
|
||||
<Banner type="warning">
|
||||
**Note:** In order to make use of the features of Payload Cloud in your own codebase,
|
||||
you will need to add the [Cloud Plugin](https://github.com/payloadcms/payload/tree/main/packages/payload-cloud) to your
|
||||
<strong>Note:</strong> In order to make use of the features of Payload Cloud in your own codebase,
|
||||
you will need to add the [Cloud Plugin](https://github.com/payloadcms/plugin-cloud) to your
|
||||
Payload app.
|
||||
</Banner>
|
||||
|
||||
@@ -28,7 +28,7 @@ Your Payload Cloud project comes with a MongoDB serverless Atlas DB instance or
|
||||
|
||||
Payload Cloud gives you S3 file storage backed by Cloudflare as a CDN, and this plugin extends Payload so that all of your media will be stored in S3 rather than locally.
|
||||
|
||||
AWS Cognito is used for authentication to your S3 bucket. The [Payload Cloud Plugin](https://github.com/payloadcms/payload/tree/main/packages/payload-cloud) will automatically pick up these values. These values are only if you'd like to access your files directly, outside of Payload Cloud.
|
||||
AWS Cognito is used for authentication to your S3 bucket. The [Payload Cloud Plugin](https://github.com/payloadcms/plugin-cloud) will automatically pick up these values. These values are only if you'd like to access your files directly, outside of Payload Cloud.
|
||||
|
||||
### Accessing Files Outside of Payload Cloud
|
||||
|
||||
@@ -59,7 +59,9 @@ You can update settings from your Project’s Settings tab. Changes to your buil
|
||||
From the Environment Variables page of the Settings tab, you can add, update and delete variables for use in your project. Like build settings, these changes will trigger a redeployment of your project.
|
||||
|
||||
<Banner>
|
||||
Note: For security reasons, any variables you wish to provide to the [Admin Panel](../admin/overview) must be prefixed with `NEXT_PUBLIC_`. [More details](../configuration/environment-vars).
|
||||
Note: For security reasons, any variables you wish to provide to the [Admin Panel](../admin/overview) must be prefixed
|
||||
with `PAYLOAD_PUBLIC_`. Learn more
|
||||
[here](../configuration/environment-vars).
|
||||
</Banner>
|
||||
|
||||
## Custom Domains
|
||||
@@ -84,7 +86,7 @@ export default buildConfig({
|
||||
|
||||
## Email
|
||||
|
||||
Powered by [Resend](https://resend.com), Payload Cloud comes with integrated email support out of the box. No configuration is needed, and you can use `payload.sendEmail()` to send email right from your Payload app. To learn more about sending email with Payload, checkout the [Email Configuration](../email/overview) overview.
|
||||
Powered by [Resend](https://resend.com), Payload Cloud comes with integrated email support out of the box. No configuration is needed, and you can use `payload.sendEmail()` to send email right from your Payload app. To learn more about sending email with Payload, checkout the [Email Configuration](https://payloadcms.com/docs/email/overview) overview.
|
||||
|
||||
If you are on the Pro or Enterprise plan, you can add your own custom Email domain name. From the Email page of your project’s Settings, add the domain you wish to use for email delivery. This will generate a set of DNS records. Add these records to your DNS provider and click verify to check that your records are resolving properly. Once verified, your emails will now be sent from your custom domain name.
|
||||
|
||||
@@ -96,30 +98,25 @@ From there, you are ready to make updates to your project. When you are ready to
|
||||
|
||||
## Cloud Plugin
|
||||
|
||||
Projects generated from a template will come pre-configured with the official Cloud Plugin, but if you are using your own repository you will need to add this into your project. To do so, add the plugin to your Payload Config:
|
||||
Projects generated from a template will come pre-configured with the official Cloud Plugin, but if you are using your own repository you will need to add this into your project. To do so, add the plugin to your Payload config:
|
||||
|
||||
`pnpm add @payloadcms/payload-cloud`
|
||||
`yarn add @payloadcms/plugin-cloud`
|
||||
|
||||
```js
|
||||
import { payloadCloudPlugin } from '@payloadcms/payload-cloud'
|
||||
import { buildConfig } from 'payload'
|
||||
import { payloadCloud } from '@payloadcms/plugin-cloud'
|
||||
import { buildConfig } from 'payload/config'
|
||||
|
||||
export default buildConfig({
|
||||
plugins: [payloadCloudPlugin()],
|
||||
plugins: [payloadCloud()],
|
||||
// rest of config
|
||||
})
|
||||
```
|
||||
|
||||
<Banner type="warning">
|
||||
**Note:** If your Payload Config already has an email with transport, this will take precedence
|
||||
**Note:** If your Payload config already has an email with transport, this will take precedence
|
||||
over Payload Cloud's email service.
|
||||
</Banner>
|
||||
|
||||
<Banner type="info">
|
||||
Good to know: the Payload Cloud Plugin was previously named `@payloadcms/plugin-cloud`. If you are
|
||||
using this plugin, you should update to the new package name.
|
||||
</Banner>
|
||||
|
||||
#### **Optional configuration**
|
||||
|
||||
If you wish to opt-out of any Payload cloud features, the plugin also accepts options to do so.
|
||||
|
||||
@@ -6,13 +6,11 @@ desc: Structure your Collections for your needs by defining fields, adding slugs
|
||||
keywords: collections, config, configuration, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
|
||||
---
|
||||
|
||||
A Collection is a group of records, called Documents, that all share a common schema. You can define as many Collections as your application needs. Each Document in a Collection is stored in the [Database](../database/overview) based on the [Fields](../fields/overview) that you define, and automatically generates a [Local API](../local-api/overview), [REST API](../rest-api/overview), and [GraphQL API](../graphql/overview) used to manage your Documents.
|
||||
Payload Collections are defined through configs of their own. You can define as many Collections as your application needs. Each Collection will automatically scaffold a new collection / table in your database of choice, based on fields that you define.
|
||||
|
||||
Collections are also used to achieve [Authentication](../authentication/overview) in Payload. By defining a Collection with `auth` options, that Collection receives additional operations to support user authentication.
|
||||
## Config Options
|
||||
|
||||
Collections are the primary way to structure recurring data in your application, such as users, products, pages, posts, and other types of content that you might want to manage. Each Collection can have its own unique [Access Control](../access-control/overview), [Hooks](../hooks/overview), [Admin Options](#admin-options), and more.
|
||||
|
||||
To define a Collection Config, use the `collection` property in your [Payload Config](./overview):
|
||||
To define a Collection Config, use the `collection` property in your [Payload Config](./overview).
|
||||
|
||||
```ts
|
||||
import { buildConfig } from 'payload'
|
||||
@@ -20,24 +18,17 @@ import { buildConfig } from 'payload'
|
||||
export default buildConfig({
|
||||
// ...
|
||||
collections: [ // highlight-line
|
||||
// Your Collections go here
|
||||
// Your Collection Configs go here
|
||||
],
|
||||
})
|
||||
```
|
||||
|
||||
<Banner type="success">
|
||||
**Tip:**
|
||||
If your Collection is only ever meant to contain a single Document, consider using a [Global](./globals) instead.
|
||||
</Banner>
|
||||
|
||||
## Config Options
|
||||
|
||||
It's often best practice to write your Collections in separate files and then import them into the main [Payload Config](./overview).
|
||||
It's often best practice to write your Collections in separate files and then import them into the main Payload Config.
|
||||
|
||||
Here is what a simple Collection Config might look like:
|
||||
|
||||
```ts
|
||||
import type { CollectionConfig } from 'payload'
|
||||
import { CollectionConfig } from 'payload'
|
||||
|
||||
export const Posts: CollectionConfig = {
|
||||
slug: 'posts',
|
||||
@@ -51,58 +42,44 @@ export const Posts: CollectionConfig = {
|
||||
```
|
||||
|
||||
<Banner type="success">
|
||||
**Reminder:**
|
||||
For more complex examples, see the [Templates](https://github.com/payloadcms/payload/tree/main/templates) and [Examples](https://github.com/payloadcms/payload/tree/main/examples) directories in the Payload repository.
|
||||
<strong>Reminder:</strong>
|
||||
For a more complex example, see the [Public Demo](https://github.com/payloadcms/public-demo) source code on GitHub.
|
||||
</Banner>
|
||||
|
||||
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). |
|
||||
| `labels` | Singular and plural labels for use in identifying this Collection throughout Payload. Auto-generated from slug if not defined. |
|
||||
| `lockDocuments` | Enables or disables document locking. By default, document locking is enabled. Set to an object to configure, or set to `false` to disable locking. [More details](../admin/locked-documents). |
|
||||
| `slug` * | Unique, URL-friendly string that will act as an identifier for this Collection. |
|
||||
| `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). |
|
||||
| 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/overview/#collections). |
|
||||
| **`auth`** | Specify options if you would like this Collection to feature authentication. For more, consult the [Authentication](../authentication/config) documentation. |
|
||||
| **`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. |
|
||||
| **`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. [Click here](../fields/overview) for a full list of field types as well as how to configure them. |
|
||||
| **`graphQL`** | An object with `singularName` and `pluralName` strings used in schema generation. Auto-generated from slug if not defined. Set to `false` to disable GraphQL. |
|
||||
| **`hooks`** | Entry points to "tie in" to Collection actions at specific points. [More details](../hooks/overview#collection-hooks). |
|
||||
| **`labels`** | Singular and plural labels for use in identifying this Collection throughout Payload. Auto-generated from slug if not defined. |
|
||||
| **`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). |
|
||||
|
||||
_* An asterisk denotes that a property is required._
|
||||
|
||||
### Fields
|
||||
|
||||
Fields define the schema of the Documents within a Collection. To learn more, go to the [Fields](../fields/overview) documentation.
|
||||
|
||||
### Access Control
|
||||
|
||||
[Collection Access Control](../access-control/overview) determines what a user can and cannot do with any given Document within a Collection. To learn more, go to the [Access Control](../access-control/overview) documentation.
|
||||
|
||||
### Hooks
|
||||
|
||||
[Collection Hooks](../hooks/collections) allow you to tie into the lifecycle of your Documents so you can execute your own logic during specific events. To learn more, go to the [Hooks](../hooks/overview) documentation.
|
||||
_\* An asterisk denotes that a property is required._
|
||||
|
||||
## Admin Options
|
||||
|
||||
The behavior of Collections within the [Admin Panel](../admin/overview) can be fully customized to fit the needs of your application. This includes grouping or hiding their navigation links, adding [Custom Components](../custom-components/overview), selecting which fields to display in the List View, and more.
|
||||
You can customize the way that the [Admin Panel](../admin/overview) behaves on a Collection-by-Collection basis.
|
||||
|
||||
To configure Admin Options for Collections, use the `admin` property in your Collection Config:
|
||||
To configure Collection Admin Options, use the `admin` property in your Collection Config:
|
||||
|
||||
```ts
|
||||
import type { CollectionConfig } from 'payload'
|
||||
import { CollectionConfig } from 'payload'
|
||||
|
||||
export const MyCollection: CollectionConfig = {
|
||||
export const Posts: CollectionConfig = {
|
||||
// ...
|
||||
admin: { // highlight-line
|
||||
// ...
|
||||
@@ -112,97 +89,69 @@ 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. |
|
||||
| `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. |
|
||||
| `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. |
|
||||
| `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 used as a label for grouping Collection and Global links together in the navigation. |
|
||||
| **`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](#admin-hooks). |
|
||||
| **`useAsTitle`** | Specify a top-level field to use for a document title throughout the [Admin Panel](../admin/overview). If no field is defined, the ID of the document is used as the title. |
|
||||
| **`description`** | Text or React component to display below the Collection label in the List View to give editors more information. |
|
||||
| **`defaultColumns`** | Array of field names that correspond to which columns to show by default in this Collection's List View. |
|
||||
| **`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. |
|
||||
| **`meta`** | Metadata overrides to apply to the [Admin Panel](../admin/overview). Included properties are `description` and `openGraph`. |
|
||||
| **`preview`** | Function to generate preview URLs within the [Admin Panel](../admin/overview) that can point to your app. [More details](#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](../admin/components#collections). |
|
||||
| **`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). |
|
||||
|
||||
### Custom Components
|
||||
#### Preview
|
||||
|
||||
Collections can set their own [Custom Components](../custom-components/overview) which only apply to Collection-specific UI within the [Admin Panel](../admin/overview). This includes elements such as the Save Button, or entire layouts such as the Edit View.
|
||||
It is possible to display a Preview Button in the Admin Panel within the Edit View. This will allow editors to visit the frontend of your app the corresponds to the document they are actively editing. This way they can preview the latest, potentially unpublished changes.
|
||||
|
||||
To override Collection Components, use the `admin.components` property in your Collection Config:
|
||||
To configure the Preview Button, set the `admin.preview` property to a function in your Collection Config:
|
||||
|
||||
```ts
|
||||
import type { CollectionConfig } from 'payload'
|
||||
import { CollectionConfig } from 'payload'
|
||||
|
||||
export const MyCollection: CollectionConfig = {
|
||||
export const Posts: CollectionConfig = {
|
||||
// ...
|
||||
admin: {
|
||||
components: { // highlight-line
|
||||
// ...
|
||||
// highlight-start
|
||||
preview: (doc, { locale }) => {
|
||||
if (doc?.slug) {
|
||||
return `/${doc.slug}?locale=${locale}`
|
||||
}
|
||||
|
||||
return null
|
||||
},
|
||||
// highlight-end
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
The following options are available:
|
||||
The preview function receives two arguments:
|
||||
|
||||
| Option | Description |
|
||||
| --------------------- |------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `afterList` | An array of components to inject _after_ the built-in List View. [More details](../custom-components/list-view#afterlist). |
|
||||
| `afterListTable` | An array of components to inject _after_ the built-in List View's table. [More details](../custom-components/list-view#afterlisttable). |
|
||||
| `beforeList` | An array of components to inject _before_ the built-in List View. [More details](../custom-components/list-view#beforelist). |
|
||||
| `beforeListTable` | An array of components to inject _before_ the built-in List View's table. [More details](../custom-components/list-view#beforelisttable). |
|
||||
| `listMenuItems` | An array of components to render within a menu next to the List Controls (after the Columns and Filters options) |
|
||||
| `Description` | A component to render below the Collection label in the List View. An alternative to the `admin.description` property. [More details](../custom-components/list-view#description). |
|
||||
| `edit` | Override specific components within the Edit View. [More details](#edit-view-options). |
|
||||
| `views` | Override or create new views within the Admin Panel. [More details](../custom-components/custom-views). |
|
||||
|
||||
#### Edit View Options
|
||||
|
||||
```ts
|
||||
import type { CollectionCOnfig } from 'payload'
|
||||
|
||||
export const MyCollection: CollectionCOnfig = {
|
||||
// ...
|
||||
admin: {
|
||||
components: {
|
||||
edit: { // highlight-line
|
||||
// ...
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
The following options are available:
|
||||
|
||||
| Option | Description |
|
||||
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| `SaveButton` | Replace the default Save Button within the Edit View. [Drafts](../versions/drafts) must be disabled. [More details](../custom-components/edit-view#save-button). |
|
||||
| `SaveDraftButton` | Replace the default Save Draft Button within the Edit View. [Drafts](../versions/drafts) must be enabled and autosave must be disabled. [More details](../custom-components/edit-view#save-draft-button). |
|
||||
| `PublishButton` | Replace the default Publish Button within the Edit View. [Drafts](../versions/drafts) must be enabled. [More details](../custom-components/edit-view#publish-button). |
|
||||
| `PreviewButton` | Replace the default Preview Button within the Edit View. [Preview](../admin/preview) must be enabled. [More details](../custom-components/edit-view#preview-button). |
|
||||
| `Upload` | Replace the default Upload component within the Edit View. [Upload](../upload/overview) must be enabled. [More details](../custom-components/edit-view#upload). |
|
||||
| Argument | Description |
|
||||
| --- | --- |
|
||||
| **`doc`** | The document being edited |
|
||||
| **`ctx`** | An object containing `locale` and `token` properties. The `token` is the currently logged-in user's JWT. |
|
||||
|
||||
<Banner type="success">
|
||||
**Note:**
|
||||
For details on how to build Custom Components, see [Building Custom Components](../custom-components/overview#building-custom-components).
|
||||
<strong>Reminder:</strong>
|
||||
For fully working example of this, check of the official [Draft Preview Example](https://github.com/payloadcms/payload/tree/main/examples/draft-preview) in the [Examples Directory](https://github.com/payloadcms/payload/tree/main/examples).
|
||||
</Banner>
|
||||
|
||||
### Pagination
|
||||
|
||||
All Collections receive their own List View which displays a paginated list of documents that can be sorted and filtered. The pagination behavior of the List View can be customized on a per-Collection basis, and uses the same [Pagination](../queries/pagination) API that Payload provides.
|
||||
All Collections receive their own List View which displays a paginated list of documents that can be sorted and filtered. The pagination behavior of the List View can be customized on a per-Collection basis.
|
||||
|
||||
To configure pagination options, use the `admin.pagination` property in your Collection Config:
|
||||
|
||||
```ts
|
||||
import type { CollectionConfig } from 'payload'
|
||||
import { CollectionConfig } from 'payload'
|
||||
|
||||
export const Posts: CollectionConfig = {
|
||||
// ...
|
||||
@@ -224,6 +173,16 @@ The following options are available:
|
||||
| `defaultLimit` | Integer that specifies the default per-page limit that should be used. Defaults to 10. |
|
||||
| `limits` | Provide an array of integers to use as per-page options for admins to choose from in the List View. |
|
||||
|
||||
### Access Control
|
||||
|
||||
You can specify extremely granular access control (what users can do with documents in a Collection) on a Collection by
|
||||
Collection basis. To learn more, go to the [Access Control](../access-control/overview) docs.
|
||||
|
||||
### Hooks
|
||||
|
||||
Hooks are a powerful way to extend Collection functionality and execute your own logic, and can be defined on a
|
||||
Collection by Collection basis. To learn more, go to the [Hooks](../hooks/overview) documentation.
|
||||
|
||||
### List Searchable Fields
|
||||
|
||||
In the List View, there is a "search" box that allows you to quickly find a document through a simple text search. By default, it searches on the ID field. If defined, the `admin.useAsTitle` field is used. Or, you can explicitly define which fields to search based on the needs of your application.
|
||||
@@ -231,7 +190,7 @@ In the List View, there is a "search" box that allows you to quickly find a docu
|
||||
To define which fields should be searched, use the `admin.listSearchableFields` property in your Collection Config:
|
||||
|
||||
```ts
|
||||
import type { CollectionConfig } from 'payload'
|
||||
import { CollectionConfig } from 'payload'
|
||||
|
||||
export const Posts: CollectionConfig = {
|
||||
// ...
|
||||
@@ -244,29 +203,24 @@ export const Posts: CollectionConfig = {
|
||||
```
|
||||
|
||||
<Banner type="warning">
|
||||
**Tip:**
|
||||
<strong>Tip:</strong>
|
||||
If you are adding `listSearchableFields`, make sure you index each of these fields so your admin queries can remain performant.
|
||||
</Banner>
|
||||
|
||||
## GraphQL
|
||||
|
||||
You can completely disable GraphQL for this collection by passing `graphQL: false` to your collection config. This will completely disable all queries, mutations, and types from appearing in your GraphQL schema.
|
||||
|
||||
You can also pass an object to the collection's `graphQL` property, which allows you to define the following properties:
|
||||
|
||||
| Option | Description |
|
||||
| ---------------------- | ----------------------------------------------------------------------------------- |
|
||||
| `singularName` | Override the "singular" name that will be used in GraphQL schema generation. |
|
||||
| `pluralName` | Override the "plural" name that will be used in GraphQL schema generation. |
|
||||
| `disableQueries` | Disable all GraphQL queries that correspond to this collection by passing `true`. |
|
||||
| `disableMutations` | Disable all GraphQL mutations that correspond to this collection by passing `true`. |
|
||||
|
||||
## TypeScript
|
||||
|
||||
You can import types from Payload to help make writing your Collection configs easier and type-safe. There are two main types that represent the Collection Config, `CollectionConfig` and `SanitizeCollectionConfig`.
|
||||
|
||||
The `CollectionConfig` type represents a raw Collection Config in its full form, where only the bare minimum properties are marked as required. The `SanitizedCollectionConfig` type represents a Collection Config after it has been fully sanitized. Generally, this is only used internally by Payload.
|
||||
You can import Collection types as follows:
|
||||
|
||||
```ts
|
||||
import type { CollectionConfig, SanitizedCollectionConfig } from 'payload'
|
||||
import { CollectionConfig } from 'payload'
|
||||
|
||||
// This is the type used for incoming Collection configs.
|
||||
// Only the bare minimum properties are marked as required.
|
||||
```
|
||||
|
||||
```ts
|
||||
import { SanitizedCollectionConfig } from 'payload'
|
||||
|
||||
// This is the type used after an incoming Collection config is fully sanitized.
|
||||
// Generally, this is only used internally by Payload.
|
||||
```
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user