Compare commits
14 Commits
v3.0.0-bet
...
v0.5.0-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e6a5132812 | ||
|
|
53310281d0 | ||
|
|
3ac9dae1d2 | ||
|
|
9c165a73cb | ||
|
|
9b58915aff | ||
|
|
c1daeb3432 | ||
|
|
ff8acde322 | ||
|
|
1bade389e4 | ||
|
|
489de652a3 | ||
|
|
166b06e5d8 | ||
|
|
ed143c7a67 | ||
|
|
b29e2ae685 | ||
|
|
9bafc0fcbf | ||
|
|
b3db078a5f |
@@ -1,29 +0,0 @@
|
||||
[*]
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[*.{js,ts,tsx}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
[*.html]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
[*.scss]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
[*.mdx]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
[*.json]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
[*.md]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
30
.eslintrc.js
Normal file
30
.eslintrc.js
Normal file
@@ -0,0 +1,30 @@
|
||||
module.exports = {
|
||||
plugins: ['@typescript-eslint', 'prettier'],
|
||||
parser: '@typescript-eslint/parser', // Specifies the ESLint parser
|
||||
parserOptions: {
|
||||
ecmaVersion: 2020, // Allows for the parsing of modern ECMAScript features
|
||||
sourceType: 'module', // Allows for the use of imports
|
||||
ecmaFeatures: {
|
||||
jsx: true, // Allows for the parsing of JSX
|
||||
},
|
||||
},
|
||||
extends: [
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:prettier/recommended',
|
||||
'prettier',
|
||||
],
|
||||
settings: {
|
||||
'import/resolver': {
|
||||
node: {
|
||||
extensions: ['.js', '.jsx', '.ts', '.tsx'],
|
||||
},
|
||||
},
|
||||
},
|
||||
ignorePatterns: ['node_modules', 'src/templates', 'dist'],
|
||||
rules: {
|
||||
'prettier/prettier': 'error',
|
||||
'import/prefer-default-export': 'off',
|
||||
'no-underscore-dangle': 'off',
|
||||
'@typescript-eslint/consistent-type-imports': 'warn',
|
||||
},
|
||||
};
|
||||
@@ -1,30 +0,0 @@
|
||||
# lint and format
|
||||
ae7d6f97d205491390f15850e5104c7abded1550
|
||||
1fbda85cd04a774cb978778b0f813001664c53dd
|
||||
|
||||
# prettier all templates
|
||||
75a428ddc4672903455998eaba7ae9f9d710bf85
|
||||
|
||||
# re-run prettier and eslint everywhere again
|
||||
cdaa0acd61d3001407609915bd573b78565d5571
|
||||
|
||||
# prettier write again
|
||||
dfac7395fed95fc5d8ebca21b786ce70821942bb
|
||||
|
||||
# lint and format plugin-cloud
|
||||
fb7d1be2f3325d076b7c967b1730afcef37922c2
|
||||
|
||||
# lint and format create-payload-app
|
||||
5fd3d430001efe86515262ded5e26f00c1451181
|
||||
|
||||
# 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
|
||||
2
.gitattributes
vendored
2
.gitattributes
vendored
@@ -1,2 +0,0 @@
|
||||
# Auto detect text files and perform LF normalization
|
||||
* text=auto eol=lf
|
||||
29
.github/CODEOWNERS
vendored
29
.github/CODEOWNERS
vendored
@@ -1,29 +0,0 @@
|
||||
# Order matters. The last matching pattern takes precedence.
|
||||
# Approvals are not required currently but may be enabled in the future.
|
||||
|
||||
### Package Exports ###
|
||||
/**/exports/ @denolfe @jmikrut @DanRibbens
|
||||
|
||||
### Packages ###
|
||||
/packages/plugin-cloud*/src/ @denolfe
|
||||
/packages/email-*/src/ @denolfe
|
||||
/packages/storage-*/src/ @denolfe
|
||||
/packages/create-payload-app/src/ @denolfe
|
||||
/packages/eslint-*/ @denolfe @AlessioGr
|
||||
|
||||
### Templates ###
|
||||
/templates/_data/ @denolfe
|
||||
/templates/_template/ @denolfe
|
||||
|
||||
### Build Files ###
|
||||
/tsconfig.json @denolfe
|
||||
/**/tsconfig*.json @denolfe
|
||||
/jest.config.js @denolfe
|
||||
/**/jest.config.js @denolfe
|
||||
|
||||
### Root ###
|
||||
/package.json @denolfe
|
||||
/scripts/ @denolfe
|
||||
/.husky/ @denolfe
|
||||
/.vscode/ @denolfe @AlessioGr
|
||||
/.github/ @denolfe
|
||||
76
.github/ISSUE_TEMPLATE/1.bug_report_v3.yml
vendored
76
.github/ISSUE_TEMPLATE/1.bug_report_v3.yml
vendored
@@ -1,76 +0,0 @@
|
||||
name: Functionality Bug
|
||||
description: '[REPRODUCTION REQUIRED] - Create a bug report'
|
||||
labels: ['status: needs-triage', 'v3', '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 `npx create-payload-app@beta -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'
|
||||
- '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` (>= beta.92) _or_ Payload, Node.js, and Next.js versions.
|
||||
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` (>= beta.92) _or_ Payload, Node.js, and Next.js versions.
|
||||
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!
|
||||
13
.github/ISSUE_TEMPLATE/DOCUMENTATION_ISSUE.md
vendored
13
.github/ISSUE_TEMPLATE/DOCUMENTATION_ISSUE.md
vendored
@@ -1,13 +0,0 @@
|
||||
---
|
||||
name: Documentation Issue
|
||||
about: Suggest fix to Payload documentation
|
||||
labels: 'documentation'
|
||||
---
|
||||
|
||||
# Documentation Issue
|
||||
|
||||
<!--- Please provide a summary of the documentation issue -->
|
||||
|
||||
## Additional Details
|
||||
|
||||
<!--- Provide any other additional details -->
|
||||
8
.github/ISSUE_TEMPLATE/config.yml
vendored
8
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,8 +0,0 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Feature Request
|
||||
url: https://github.com/payloadcms/payload/discussions
|
||||
about: Suggest an idea to improve Payload in our GitHub Discussions
|
||||
- name: Question about Payload
|
||||
url: https://github.com/payloadcms/payload/discussions
|
||||
about: Please ask Payload-related questions in our GitHub Discussions
|
||||
23
.github/PULL_REQUEST_TEMPLATE.md
vendored
23
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -1,23 +0,0 @@
|
||||
<!--
|
||||
|
||||
Thank you for the PR! Please go through the checklist below and make sure you've completed all the steps.
|
||||
|
||||
Please review the [CONTRIBUTING.md](https://github.com/payloadcms/payload/blob/main/CONTRIBUTING.md) document in this repository if you haven't already.
|
||||
|
||||
The following items will ensure that your PR is handled as smoothly as possible:
|
||||
|
||||
- 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
|
||||
|
||||
### What?
|
||||
|
||||
### Why?
|
||||
|
||||
### How?
|
||||
|
||||
Fixes #
|
||||
|
||||
-->
|
||||
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'],
|
||||
},
|
||||
}
|
||||
34
.github/actions/release-commenter/package.json
vendored
34
.github/actions/release-commenter/package.json
vendored
@@ -1,34 +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.36",
|
||||
"@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",
|
||||
"ts-jest": "^26.5.6",
|
||||
"typescript": "^4.9.5"
|
||||
}
|
||||
}
|
||||
5419
.github/actions/release-commenter/pnpm-lock.yaml
generated
vendored
5419
.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"]
|
||||
}
|
||||
48
.github/actions/setup/action.yml
vendored
48
.github/actions/setup/action.yml
vendored
@@ -1,48 +0,0 @@
|
||||
name: Setup node and pnpm
|
||||
description: Configure the Node.js and pnpm versions
|
||||
|
||||
inputs:
|
||||
node-version:
|
||||
description: 'The Node.js version to use'
|
||||
required: true
|
||||
default: 22.6.2
|
||||
pnpm-version:
|
||||
description: 'The pnpm version to use'
|
||||
required: true
|
||||
default: 9.7.1
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
# https://github.com/actions/virtual-environments/issues/1187
|
||||
- name: tune linux network
|
||||
shell: bash
|
||||
run: sudo ethtool -K eth0 tx off rx off
|
||||
|
||||
- name: Setup Node@${{ inputs.node-version }}
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ inputs.node-version }}
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: ${{ inputs.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
|
||||
with:
|
||||
path: ${{ env.STORE_PATH }}
|
||||
key: pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
pnpm-store-
|
||||
pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
|
||||
- shell: bash
|
||||
run: pnpm install
|
||||
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'
|
||||
tag-only:
|
||||
description: Log and tag only. Do not perform closing or commenting actions.
|
||||
default: false
|
||||
|
||||
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_TAG_ONLY': ${{inputs.tag-only}}
|
||||
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'],
|
||||
},
|
||||
}
|
||||
34
.github/actions/triage/package.json
vendored
34
.github/actions/triage/package.json
vendored
@@ -1,34 +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.36",
|
||||
"@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",
|
||||
"ts-jest": "^26.5.6",
|
||||
"typescript": "^4.9.5"
|
||||
}
|
||||
}
|
||||
5419
.github/actions/triage/pnpm-lock.yaml
generated
vendored
5419
.github/actions/triage/pnpm-lock.yaml
generated
vendored
File diff suppressed because it is too large
Load Diff
195
.github/actions/triage/src/index.ts
vendored
195
.github/actions/triage/src/index.ts
vendored
@@ -1,195 +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')
|
||||
|
||||
// Define the configuration object
|
||||
interface Config {
|
||||
invalidLink: {
|
||||
comment: string
|
||||
bugLabels: string[]
|
||||
hosts: string[]
|
||||
label: string
|
||||
linkSection: string
|
||||
}
|
||||
tagOnly: boolean
|
||||
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',
|
||||
},
|
||||
tagOnly: getBooleanOrUndefined('tag_only') || false,
|
||||
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 })),
|
||||
)
|
||||
info(`Issue #${issue.number} - validate label removed`)
|
||||
await client.issues.addLabels({ ...common, labels: [config.invalidLink.label] })
|
||||
info(`Issue #${issue.number} - labeled`)
|
||||
|
||||
// If tagOnly, do not close or comment
|
||||
if (config.tagOnly) {
|
||||
info('Tag-only enabled, no closing/commenting actions taken')
|
||||
return
|
||||
}
|
||||
|
||||
// Perform closing and commenting actions
|
||||
await client.issues.update({ ...common, state: 'closed' })
|
||||
info(`Issue #${issue.number} - closed`)
|
||||
|
||||
const comment = join(config.workspace, config.invalidLink.comment)
|
||||
await client.issues.createComment({ ...common, body: await getCommentBody(comment) })
|
||||
info(`Issue #${issue.number} - commented`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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"]
|
||||
}
|
||||
18
.github/comments/invalid-reproduction.md
vendored
18
.github/comments/invalid-reproduction.md
vendored
@@ -1,18 +0,0 @@
|
||||
We cannot recreate the issue with the provided information. **Please add a reproduction in order for us to be able to investigate.**
|
||||
|
||||
### Why was this issue marked with the `invalid-reproduction` label?
|
||||
|
||||
To be able to investigate, we need access to a reproduction to identify what triggered the issue. We prefer a link to a public GitHub repository created with `create-payload-app@beta -t blank` or a forked/branched version of this repository with tests added (more info in the [reproduction-guide](https://github.com/payloadcms/payload/blob/main/.github/reproduction-guide.md)).
|
||||
|
||||
To 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)
|
||||
74
.github/dependabot.yml
vendored
74
.github/dependabot.yml
vendored
@@ -1,74 +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: beta
|
||||
schedule:
|
||||
interval: monthly
|
||||
timezone: America/Detroit
|
||||
time: '06:00'
|
||||
groups:
|
||||
github_actions:
|
||||
patterns:
|
||||
- '*'
|
||||
|
||||
- package-ecosystem: npm
|
||||
directory: /
|
||||
target-branch: beta
|
||||
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:
|
||||
- '*'
|
||||
exclude-patterns:
|
||||
- 'drizzle*'
|
||||
dev-deps:
|
||||
dependency-type: development
|
||||
update-types:
|
||||
- minor
|
||||
- patch
|
||||
patterns:
|
||||
- '*'
|
||||
exclude-patterns:
|
||||
- 'drizzle*'
|
||||
|
||||
# Only bump patch versions for 2.x
|
||||
- package-ecosystem: npm
|
||||
directory: /
|
||||
target-branch: main
|
||||
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:
|
||||
- '*'
|
||||
exclude-patterns:
|
||||
- 'drizzle*'
|
||||
3976
.github/pnpm-lock.yaml
generated
vendored
3976
.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/*'
|
||||
64
.github/reproduction-guide.md
vendored
64
.github/reproduction-guide.md
vendored
@@ -1,64 +0,0 @@
|
||||
# Reproduction Guide
|
||||
|
||||
1. [Fork](https://github.com/payloadcms/payload/fork) this repo
|
||||
2. Optionally, create a new branch for your reproduction
|
||||
3. Run `pnpm install` to install dependencies
|
||||
4. Open up the `test/_community` directory
|
||||
5. Add any necessary `collections/globals/fields` in this directory to recreate the issue you are experiencing
|
||||
6. Run `pnpm dev _community` to start the admin panel
|
||||
|
||||
**NOTE:** The goal is to isolate the problem by reducing the number of `collections/globals/fields` you add to the `test/_community` folder. This folder is _not_ meant for you to copy your project into, but rather recreate the issue you are experiencing with minimal config.
|
||||
|
||||
## Example test directory file tree
|
||||
|
||||
```text
|
||||
.
|
||||
├── config.ts
|
||||
├── int.spec.ts
|
||||
├── e2e.spec.ts
|
||||
└── payload-types.ts
|
||||
```
|
||||
|
||||
- `config.ts` - This is the _granular_ Payload config for testing. It should be as lightweight as possible. Reference existing configs for an example
|
||||
- `int.spec.ts` [Optional] - This is the test file run by jest. Any test file must have a `*int.spec.ts` suffix.
|
||||
- `e2e.spec.ts` [Optional] - This is the end-to-end test file that will load up the admin UI using the above config and run Playwright tests.
|
||||
- `payload-types.ts` - Generated types from `config.ts`. Generate this file by running `pnpm dev:generate-types _community`.
|
||||
|
||||
The directory split up in this way specifically to reduce friction when creating tests and to add the ability to boot up Payload with that specific config. You should modify the files in `test/_community` to get started.
|
||||
|
||||
<br />
|
||||
|
||||
## Testing is optional but encouraged
|
||||
|
||||
An issue does not need to have failing tests — reproduction steps with your forked repo are enough at this point. Some people like to dive deeper and we want to give you the guidance/tools to do so. Read more below:
|
||||
|
||||
### Running integration tests (Payload API tests)
|
||||
|
||||
First install [Jest Runner for VSVode](https://marketplace.visualstudio.com/items?itemName=firsttris.vscode-jest-runner).
|
||||
|
||||
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/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:
|
||||
|
||||
```bash
|
||||
pnpm test:int _community
|
||||
```
|
||||
|
||||
### Running E2E tests (Admin Panel UI tests)
|
||||
|
||||
The easiest way to run E2E tests is to install
|
||||
|
||||
- [Playwright Test for VSCode](https://marketplace.visualstudio.com/items?itemName=ms-playwright.playwright)
|
||||
- [Playwright Runner](https://marketplace.visualstudio.com/items?itemName=ortoni.ortoni)
|
||||
|
||||
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/admin/assets/images/github/e2e-debug.png" />
|
||||
|
||||
#### Notes
|
||||
|
||||
The default credentials are `dev@payloadcms.com` as email and `test` as password. They can be found in `test/credentials.ts`. By default, these will be autofilled, so no log-in is required.
|
||||
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-latest
|
||||
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-latest
|
||||
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-latest
|
||||
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-latest
|
||||
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-latest
|
||||
# if: github.event.pull_request.merged == true
|
||||
# steps:
|
||||
|
||||
# on-pr-close:
|
||||
# runs-on: ubuntu-latest
|
||||
# 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-latest
|
||||
steps:
|
||||
- name: Lock issues
|
||||
uses: dessant/lock-threads@v5
|
||||
with:
|
||||
process-only: 'issues'
|
||||
issue-inactive-days: '1'
|
||||
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.
|
||||
563
.github/workflows/main.yml
vendored
563
.github/workflows/main.yml
vendored
@@ -1,563 +0,0 @@
|
||||
name: build
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types:
|
||||
- opened
|
||||
- reopened
|
||||
- synchronize
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- beta
|
||||
|
||||
concurrency:
|
||||
# <workflow_name>-<branch_name>-<true || commit_sha if branch is protected>
|
||||
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.ref_protected && github.sha || ''}}
|
||||
cancel-in-progress: true
|
||||
|
||||
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:
|
||||
changes:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pull-requests: read
|
||||
outputs:
|
||||
needs_build: ${{ steps.filter.outputs.needs_build }}
|
||||
templates: ${{ steps.filter.outputs.templates }}
|
||||
steps:
|
||||
# https://github.com/actions/virtual-environments/issues/1187
|
||||
- name: tune linux network
|
||||
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:
|
||||
filters: |
|
||||
needs_build:
|
||||
- '.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 "templates: ${{ steps.filter.outputs.templates }}"
|
||||
|
||||
lint:
|
||||
if: >
|
||||
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
|
||||
|
||||
# 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@v4
|
||||
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}
|
||||
npx lint-staged --diff="origin/${GITHUB_BASE_REF}...${GITHUB_SHA}"
|
||||
|
||||
build:
|
||||
needs: changes
|
||||
if: ${{ needs.changes.outputs.needs_build == 'true' }}
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
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: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
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-latest
|
||||
needs: build
|
||||
|
||||
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@v4
|
||||
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: Unit Tests
|
||||
run: pnpm test:unit
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=8096
|
||||
|
||||
tests-int:
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
database:
|
||||
- mongodb
|
||||
- postgres
|
||||
- postgres-custom-schema
|
||||
- postgres-uuid
|
||||
- supabase
|
||||
- sqlite
|
||||
env:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: payloadtests
|
||||
AWS_ENDPOINT_URL: http://127.0.0.1:4566
|
||||
AWS_ACCESS_KEY_ID: localstack
|
||||
AWS_SECRET_ACCESS_KEY: localstack
|
||||
AWS_REGION: us-east-1
|
||||
|
||||
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: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: ${{ env.PNPM_VERSION }}
|
||||
run_install: false
|
||||
|
||||
- run: pnpm install
|
||||
|
||||
- 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:
|
||||
version: latest
|
||||
if: matrix.database == 'supabase'
|
||||
|
||||
- name: Initialize Supabase
|
||||
run: |
|
||||
supabase init
|
||||
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;"
|
||||
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: startsWith(matrix.database, 'postgres')
|
||||
|
||||
- name: Configure PostgreSQL with custom schema
|
||||
run: |
|
||||
psql "postgresql://$POSTGRES_USER:$POSTGRES_PASSWORD@localhost:5432/$POSTGRES_DB" -c "CREATE SCHEMA custom;"
|
||||
if: matrix.database == 'postgres-custom-schema'
|
||||
|
||||
- name: Configure Supabase
|
||||
run: |
|
||||
echo "POSTGRES_URL=postgresql://postgres:postgres@127.0.0.1:54322/postgres" >> $GITHUB_ENV
|
||||
if: matrix.database == 'supabase'
|
||||
|
||||
- name: Integration Tests
|
||||
run: pnpm test:int
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=8096
|
||||
PAYLOAD_DATABASE: ${{ matrix.database }}
|
||||
POSTGRES_URL: ${{ env.POSTGRES_URL }}
|
||||
|
||||
tests-e2e:
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
# find test -type f -name 'e2e.spec.ts' | sort | xargs dirname | xargs -I {} basename {}
|
||||
suite:
|
||||
- _community
|
||||
- access-control
|
||||
- admin__e2e__1
|
||||
- admin__e2e__2
|
||||
- admin-root
|
||||
- auth
|
||||
- field-error-states
|
||||
- fields-relationship
|
||||
- fields
|
||||
- fields__collections__Blocks
|
||||
- fields__collections__Array
|
||||
- fields__collections__Relationship
|
||||
- fields__collections__RichText
|
||||
- fields__collections__Lexical__e2e__main
|
||||
- fields__collections__Lexical__e2e__blocks
|
||||
- fields__collections__Date
|
||||
- fields__collections__Number
|
||||
- fields__collections__Point
|
||||
- fields__collections__Tabs
|
||||
- fields__collections__Text
|
||||
- fields__collections__Upload
|
||||
- live-preview
|
||||
- localization
|
||||
- locked-documents
|
||||
- i18n
|
||||
- plugin-cloud-storage
|
||||
- plugin-form-builder
|
||||
- plugin-nested-docs
|
||||
- plugin-seo
|
||||
- versions
|
||||
- uploads
|
||||
env:
|
||||
SUITE_NAME: ${{ matrix.suite }}
|
||||
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@v4
|
||||
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 LocalStack
|
||||
run: pnpm docker:start
|
||||
if: ${{ matrix.suite == 'plugin-cloud-storage' }}
|
||||
|
||||
- name: Store Playwright's Version
|
||||
run: |
|
||||
# Extract the version number using a more targeted regex pattern with awk
|
||||
PLAYWRIGHT_VERSION=$(pnpm ls @playwright/test --depth=0 | awk '/@playwright\/test/ {print $2}')
|
||||
echo "Playwright's Version: $PLAYWRIGHT_VERSION"
|
||||
echo "PLAYWRIGHT_VERSION=$PLAYWRIGHT_VERSION" >> $GITHUB_ENV
|
||||
|
||||
- name: Cache Playwright Browsers for Playwright's Version
|
||||
id: cache-playwright-browsers
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.cache/ms-playwright
|
||||
key: playwright-browsers-${{ env.PLAYWRIGHT_VERSION }}
|
||||
|
||||
- name: Setup Playwright - Browsers and Dependencies
|
||||
if: steps.cache-playwright-browsers.outputs.cache-hit != 'true'
|
||||
run: pnpm exec playwright install --with-deps chromium
|
||||
|
||||
- name: Setup Playwright - Dependencies-only
|
||||
if: steps.cache-playwright-browsers.outputs.cache-hit == 'true'
|
||||
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 }}
|
||||
env:
|
||||
PLAYWRIGHT_JSON_OUTPUT_NAME: results_${{ matrix.suite }}.json
|
||||
NEXT_TELEMETRY_DISABLED: 1
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: test-results-${{ matrix.suite }}
|
||||
path: test/test-results/
|
||||
if-no-files-found: ignore
|
||||
retention-days: 1
|
||||
|
||||
# Disabled until this is fixed: https://github.com/daun/playwright-report-summary/issues/156
|
||||
# - uses: daun/playwright-report-summary@v3
|
||||
# with:
|
||||
# report-file: results_${{ matrix.suite }}.json
|
||||
# report-tag: ${{ matrix.suite }}
|
||||
# job-summary: true
|
||||
|
||||
app-build-with-packed:
|
||||
if: false # Disable until package resolution in tgzs can be figured out
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
|
||||
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@v4
|
||||
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 MongoDB
|
||||
uses: supercharge/mongodb-github-action@1.11.0
|
||||
with:
|
||||
mongodb-version: 6.0
|
||||
|
||||
- name: Pack and build app
|
||||
run: |
|
||||
set -ex
|
||||
pnpm run script:pack --dest templates/blank
|
||||
cd templates/blank
|
||||
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-latest
|
||||
needs: build
|
||||
|
||||
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@v4
|
||||
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: Generate Payload Types
|
||||
run: pnpm dev:generate-types fields
|
||||
|
||||
- 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.11.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@v4
|
||||
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-latest
|
||||
needs:
|
||||
- lint
|
||||
- build
|
||||
- tests-unit
|
||||
- tests-int
|
||||
- tests-e2e
|
||||
|
||||
steps:
|
||||
- if: ${{ always() && (contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled')) }}
|
||||
run: exit 1
|
||||
|
||||
publish-canary:
|
||||
name: Publish Canary
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- all-green
|
||||
|
||||
steps:
|
||||
# debug github.ref output
|
||||
- run: |
|
||||
echo github.ref: ${{ github.ref }}
|
||||
echo isBeta: ${{ github.ref == 'refs/heads/beta' }}
|
||||
echo isMain: ${{ github.ref == 'refs/heads/main' }}
|
||||
30
.github/workflows/post-release.yml
vendored
30
.github/workflows/post-release.yml
vendored
@@ -1,30 +0,0 @@
|
||||
name: post-release
|
||||
|
||||
on:
|
||||
release:
|
||||
types:
|
||||
- published
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
post_release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
# Only needed if debugging on a branch other than default
|
||||
# ref: ${{ github.event.release.target_commitish || github.ref }}
|
||||
- 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}
|
||||
121
.github/workflows/pr-title.yml
vendored
121
.github/workflows/pr-title.yml
vendored
@@ -1,121 +0,0 @@
|
||||
name: pr-title
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types:
|
||||
- opened
|
||||
- edited
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
main:
|
||||
name: lint-pr-title
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: amannn/action-semantic-pull-request@v5
|
||||
id: lint_pr_title
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
types: |
|
||||
build
|
||||
chore
|
||||
ci
|
||||
docs
|
||||
feat
|
||||
fix
|
||||
perf
|
||||
refactor
|
||||
revert
|
||||
style
|
||||
test
|
||||
types
|
||||
scopes: |
|
||||
cpa
|
||||
db-\*
|
||||
db-mongodb
|
||||
db-postgres
|
||||
db-vercel-postgres
|
||||
db-sqlite
|
||||
drizzle
|
||||
email-nodemailer
|
||||
eslint
|
||||
graphql
|
||||
live-preview
|
||||
live-preview-react
|
||||
next
|
||||
payload-cloud
|
||||
plugin-cloud
|
||||
plugin-cloud-storage
|
||||
plugin-form-builder
|
||||
plugin-nested-docs
|
||||
plugin-redirects
|
||||
plugin-search
|
||||
plugin-sentry
|
||||
plugin-seo
|
||||
plugin-stripe
|
||||
richtext-\*
|
||||
richtext-lexical
|
||||
richtext-slate
|
||||
storage-\*
|
||||
storage-azure
|
||||
storage-gcs
|
||||
storage-uploadthing
|
||||
storage-vercel-blob
|
||||
storage-s3
|
||||
translations
|
||||
ui
|
||||
templates
|
||||
examples(\/(\w|-)+)?
|
||||
deps
|
||||
|
||||
# Disallow uppercase letters at the beginning of the subject
|
||||
subjectPattern: ^(?![A-Z]).+$
|
||||
subjectPatternError: |
|
||||
The subject "{subject}" found in the pull request title "{title}"
|
||||
didn't match the configured pattern. Please ensure that the subject
|
||||
doesn't start with an uppercase character.
|
||||
|
||||
- uses: marocchino/sticky-pull-request-comment@v2
|
||||
# When the previous steps fails, the workflow would stop. By adding this
|
||||
# condition you can continue the execution with the populated error message.
|
||||
if: always() && (steps.lint_pr_title.outputs.error_message != null)
|
||||
with:
|
||||
header: pr-title-lint-error
|
||||
message: |
|
||||
Pull Request titles must follow the [Conventional Commits specification](https://www.conventionalcommits.org/en/v1.0.0/) and have valid scopes.
|
||||
|
||||
${{ steps.lint_pr_title.outputs.error_message }}
|
||||
|
||||
```
|
||||
feat(ui): add Button component
|
||||
^ ^ ^
|
||||
| | |__ Subject
|
||||
| |_______ Scope
|
||||
|____________ Type
|
||||
```
|
||||
|
||||
# Delete a previous comment when the issue has been resolved
|
||||
- if: ${{ steps.lint_pr_title.outputs.error_message == null }}
|
||||
uses: marocchino/sticky-pull-request-comment@v2
|
||||
with:
|
||||
header: pr-title-lint-error
|
||||
delete: true
|
||||
|
||||
label-pr-on-open:
|
||||
name: label-pr-on-open
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.action == 'opened'
|
||||
steps:
|
||||
- name: Tag with main branch with v2
|
||||
if: github.event.pull_request.base.ref == 'main'
|
||||
uses: actions-ecosystem/action-add-labels@v1
|
||||
with:
|
||||
labels: v2
|
||||
- name: Tag with beta branch with v3
|
||||
if: github.event.pull_request.base.ref == 'beta'
|
||||
uses: actions-ecosystem/action-add-labels@v1
|
||||
with:
|
||||
labels: v3
|
||||
36
.github/workflows/release-canary.yml
vendored
36
.github/workflows/release-canary.yml
vendored
@@ -1,36 +0,0 @@
|
||||
name: release-canary
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
branches:
|
||||
- beta
|
||||
|
||||
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:
|
||||
release:
|
||||
permissions:
|
||||
id-token: write
|
||||
runs-on: ubuntu-latest
|
||||
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: Load npm token
|
||||
run: echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> ~/.npmrc
|
||||
env:
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
- name: Canary release script
|
||||
# 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
|
||||
42
.github/workflows/stale.yml
vendored
42
.github/workflows/stale.yml
vendored
@@ -1,42 +0,0 @@
|
||||
name: stale
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: actions/stale@v9
|
||||
id: stale
|
||||
with:
|
||||
debug-only: true
|
||||
days-before-stale: 90
|
||||
days-before-close: 7
|
||||
ascending: true
|
||||
operations-per-run: 300
|
||||
|
||||
# Ignore all assigned
|
||||
exempt-all-assignees: false
|
||||
|
||||
# Issues
|
||||
stale-issue-label: 'stale'
|
||||
exempt-issue-labels: 'blocked,must,should,keep,created-by: Payload team,created-by: Contributor'
|
||||
stale-issue-message: >
|
||||
This issue has been marked as stale due to lack of activity. To keep the ticket open, please indicate that it is still relevant in a comment below.
|
||||
close-issue-message: >
|
||||
This issue was automatically closed due to lack of activity.
|
||||
|
||||
# Pull Requests
|
||||
stale-pr-label: 'stale'
|
||||
exempt-pr-labels: 'blocked,must,should,keep,created-by: Payload team,created-by: Contributor'
|
||||
stale-pr-message: >
|
||||
This PR is stale due to lack of activity. To keep the PR open, please indicate that it is still relevant in a comment below.
|
||||
close-pr-message: >
|
||||
This pull request was automatically closed due to lack of activity.
|
||||
|
||||
- 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:
|
||||
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-latest
|
||||
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-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 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-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
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'
|
||||
tag-only: 'true'
|
||||
167
.gitignore
vendored
167
.gitignore
vendored
@@ -1,39 +1,6 @@
|
||||
coverage
|
||||
package-lock.json
|
||||
dist
|
||||
/.idea/*
|
||||
!/.idea/runConfigurations
|
||||
!/.idea/payload.iml
|
||||
|
||||
# Custom actions
|
||||
!.github/actions/**/dist
|
||||
|
||||
test/packed
|
||||
test-results
|
||||
.devcontainer
|
||||
.localstack
|
||||
/migrations
|
||||
.localstack
|
||||
.turbo
|
||||
|
||||
meta_client.json
|
||||
meta_server.json
|
||||
meta_index.json
|
||||
meta_shared.json
|
||||
|
||||
.turbo
|
||||
|
||||
# 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
|
||||
# Edit at https://www.toptal.com/developers/gitignore?templates=node,macos,windows,webstorm,sublimetext,visualstudiocode
|
||||
# Created by https://www.toptal.com/developers/gitignore/api/node,macos,vscode,windows
|
||||
# Edit at https://www.toptal.com/developers/gitignore?templates=node,macos,vscode,windows
|
||||
|
||||
### macOS ###
|
||||
# General
|
||||
@@ -41,6 +8,9 @@ test/media
|
||||
.AppleDouble
|
||||
.LSOverride
|
||||
|
||||
# Icon must end with two \r
|
||||
Icon
|
||||
|
||||
|
||||
# Thumbnails
|
||||
._*
|
||||
@@ -60,10 +30,6 @@ Network Trash Folder
|
||||
Network Trash Folder
|
||||
Temporary Items
|
||||
.apdisk
|
||||
### macOS Patch ###
|
||||
# iCloud generated files
|
||||
*.icloud
|
||||
|
||||
|
||||
### Node ###
|
||||
# Logs
|
||||
@@ -72,7 +38,6 @@ npm-debug.log*
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
@@ -109,8 +74,8 @@ build/Release
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
# Snowpack dependency directory (https://snowpack.dev/)
|
||||
web_modules/
|
||||
|
||||
# TypeScript v1 declaration files
|
||||
typings/
|
||||
|
||||
# TypeScript cache
|
||||
@@ -121,9 +86,6 @@ web_modules/
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
# Optional stylelint cache
|
||||
.stylelintcache
|
||||
|
||||
|
||||
# Microbundle cache
|
||||
.rpt2_cache/
|
||||
@@ -139,12 +101,10 @@ web_modules/
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
# dotenv environment variable files
|
||||
|
||||
# dotenv environment variables file
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
.env
|
||||
.env.test
|
||||
.env*.local
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
@@ -152,12 +112,10 @@ web_modules/
|
||||
.parcel-cache
|
||||
|
||||
# Next.js build output
|
||||
out
|
||||
.next
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
.nuxt
|
||||
dist_optimized
|
||||
dist
|
||||
|
||||
# Gatsby files
|
||||
@@ -168,12 +126,6 @@ dist_optimized
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
# vuepress v2.x temp and cache directory
|
||||
.temp
|
||||
|
||||
# Docusaurus cache and generated files
|
||||
.docusaurus
|
||||
|
||||
|
||||
# Serverless directories
|
||||
.serverless/
|
||||
@@ -189,91 +141,13 @@ dist_optimized
|
||||
|
||||
# Stores VSCode versions used for testing VSCode extensions
|
||||
.vscode-test
|
||||
# yarn v2
|
||||
.yarn/cache
|
||||
.yarn/unplugged
|
||||
.yarn/build-state.yml
|
||||
.yarn/install-state.gz
|
||||
.pnp.*
|
||||
|
||||
### Node Patch ###
|
||||
# Serverless Webpack directories
|
||||
.webpack/
|
||||
|
||||
# Optional stylelint cache
|
||||
|
||||
# SvelteKit build / generate output
|
||||
.svelte-kit
|
||||
|
||||
### SublimeText ###
|
||||
# Cache files for Sublime Text
|
||||
*.tmlanguage.cache
|
||||
*.tmPreferences.cache
|
||||
*.stTheme.cache
|
||||
|
||||
# Workspace files are user-specific
|
||||
*.sublime-workspace
|
||||
|
||||
# Project files should be checked into the repository, unless a significant
|
||||
# proportion of contributors will probably not be using Sublime Text
|
||||
# *.sublime-project
|
||||
|
||||
# SFTP configuration file
|
||||
sftp-config.json
|
||||
sftp-config-alt*.json
|
||||
|
||||
# Package control specific files
|
||||
Package Control.last-run
|
||||
Package Control.ca-list
|
||||
Package Control.ca-bundle
|
||||
Package Control.system-ca-bundle
|
||||
Package Control.cache/
|
||||
Package Control.ca-certs/
|
||||
Package Control.merged-ca-bundle
|
||||
Package Control.user-ca-bundle
|
||||
oscrypto-ca-bundle.crt
|
||||
bh_unicode_properties.cache
|
||||
|
||||
# Sublime-github package stores a github token in this file
|
||||
# https://packagecontrol.io/packages/sublime-github
|
||||
GitHub.sublime-settings
|
||||
|
||||
### VisualStudioCode ###
|
||||
|
||||
### vscode ###
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/*.code-snippets
|
||||
|
||||
# Local History for Visual Studio Code
|
||||
.history/
|
||||
|
||||
# Built Visual Studio Code Extensions
|
||||
*.vsix
|
||||
|
||||
### VisualStudioCode Patch ###
|
||||
# Ignore all local history of files
|
||||
.history
|
||||
.ionide
|
||||
|
||||
# CMake
|
||||
cmake-build-*/
|
||||
|
||||
# File-based project format
|
||||
*.iws
|
||||
|
||||
# IntelliJ
|
||||
out/
|
||||
|
||||
# JIRA plugin
|
||||
atlassian-ide-plugin.xml
|
||||
|
||||
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||
com_crashlytics_export_strings.xml
|
||||
crashlytics.properties
|
||||
crashlytics-build.properties
|
||||
fabric.properties
|
||||
!.vscode/extensions.json
|
||||
*.code-workspace
|
||||
|
||||
### Windows ###
|
||||
@@ -301,19 +175,4 @@ $RECYCLE.BIN/
|
||||
|
||||
# Windows shortcuts
|
||||
*.lnk
|
||||
# End of https://www.toptal.com/developers/gitignore/api/node,macos,windows,webstorm,sublimetext,visualstudiocode
|
||||
|
||||
/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
|
||||
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
pnpm run lint-staged --quiet
|
||||
87
.idea/payload.iml
generated
87
.idea/payload.iml
generated
@@ -1,87 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="WEB_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/temp" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/tmp" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/packages/payload/.turbo" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/packages/payload/components" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/packages/payload/dist" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/.swc" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/.turbo" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/examples" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/media" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/packages/create-payload-app/.turbo" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/packages/create-payload-app/dist" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/packages/db-mongodb/.turbo" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/packages/db-mongodb/dist" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/packages/db-postgres/.turbo" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/packages/db-postgres/dist" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/packages/graphql/.turbo" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/packages/graphql/dist" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/packages/live-preview-react/.turbo" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/packages/live-preview-react/dist" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/packages/live-preview/.turbo" />
|
||||
<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" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/packages/plugin-cloud/.turbo" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/packages/plugin-cloud/dist" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/packages/plugin-form-builder/.turbo" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/packages/plugin-nested-docs/.turbo" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/packages/plugin-nested-docs/dist" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/packages/plugin-redirects/.turbo" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/packages/plugin-redirects/dist" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/packages/plugin-search/.turbo" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/packages/plugin-search/dist" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/packages/plugin-sentry/.turbo" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/packages/plugin-seo/.turbo" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/packages/plugin-seo/dist" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/packages/plugin-stripe/.turbo" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/packages/richtext-lexical/.turbo" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/packages/richtext-lexical/dist" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/templates" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/test/.swc" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/versions" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/packages/richtext-slate/dist" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/packages/richtext-slate/.turbo" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/packages/email-nodemailer/.turbo" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/packages/email-nodemailer/dist" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/packages/email-resend/.turbo" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/packages/email-resend/dist" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/packages/live-preview-vue/.turbo" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/packages/live-preview-vue/dist" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/packages/payload/.swc" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/packages/plugin-form-builder/dist" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/packages/plugin-relationship-object-ids/.turbo" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/packages/plugin-relationship-object-ids/dist" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/packages/plugin-stripe/dist" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/packages/storage-azure/.turbo" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/packages/storage-azure/dist" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/packages/storage-gcs/.turbo" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/packages/storage-gcs/dist" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/packages/storage-s3/.turbo" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/packages/storage-s3/dist" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/packages/storage-uploadthing/.turbo" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/packages/storage-uploadthing/dist" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/packages/storage-vercel-blob/.turbo" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/packages/storage-vercel-blob/dist" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/packages/translations/.turbo" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/packages/translations/dist" />
|
||||
<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>
|
||||
13
.idea/runConfigurations/Run_Dev_Fields.xml
generated
13
.idea/runConfigurations/Run_Dev_Fields.xml
generated
@@ -1,13 +0,0 @@
|
||||
<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 />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
||||
13
.idea/runConfigurations/Run_Dev__community.xml
generated
13
.idea/runConfigurations/Run_Dev__community.xml
generated
@@ -1,13 +0,0 @@
|
||||
<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 />
|
||||
<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,9 +0,0 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="true" type="JavaScriptTestRunnerJest">
|
||||
<node-interpreter value="project" />
|
||||
<node-options value="--no-deprecation" />
|
||||
<envs />
|
||||
<scope-kind value="ALL" />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
||||
@@ -1 +0,0 @@
|
||||
v22.6.0
|
||||
3
.npmrc
3
.npmrc
@@ -1,3 +0,0 @@
|
||||
symlink=true
|
||||
node-linker=isolated
|
||||
hoist-workspace-packages=false # the default in pnpm v9 is true, but that can break our runtime dependency checks
|
||||
@@ -1,16 +0,0 @@
|
||||
.tmp
|
||||
**/.git
|
||||
**/.hg
|
||||
**/.pnp.*
|
||||
**/.svn
|
||||
**/.yarn/**
|
||||
**/build
|
||||
**/dist/**
|
||||
**/node_modules
|
||||
**/temp
|
||||
**/docs/**
|
||||
tsconfig.json
|
||||
packages/payload/*.js
|
||||
packages/payload/*.d.ts
|
||||
payload-types.ts
|
||||
tsconfig.tsbuildinfo
|
||||
8
.prettierrc.js
Normal file
8
.prettierrc.js
Normal file
@@ -0,0 +1,8 @@
|
||||
module.exports = {
|
||||
printWidth: 85,
|
||||
parser: 'typescript',
|
||||
semi: false,
|
||||
singleQuote: true,
|
||||
trailingComma: 'all',
|
||||
arrowParens: 'avoid',
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all",
|
||||
"printWidth": 100,
|
||||
"semi": false
|
||||
}
|
||||
24
.swcrc
24
.swcrc
@@ -1,24 +0,0 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/swcrc",
|
||||
"sourceMaps": "inline",
|
||||
"jsc": {
|
||||
"target": "esnext",
|
||||
"parser": {
|
||||
"syntax": "typescript",
|
||||
"tsx": true,
|
||||
"dts": true
|
||||
},
|
||||
"transform": {
|
||||
"react": {
|
||||
"runtime": "automatic",
|
||||
"pragmaFrag": "React.Fragment",
|
||||
"throwIfNamespace": true,
|
||||
"development": false,
|
||||
"useBuiltins": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"module": {
|
||||
"type": "es6"
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
pnpm 9.7.1
|
||||
nodejs 22.6.0
|
||||
8
.vscode/extensions.json
vendored
8
.vscode/extensions.json
vendored
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"dbaeumer.vscode-eslint",
|
||||
"esbenp.prettier-vscode",
|
||||
"firsttris.vscode-jest-runner",
|
||||
"ms-playwright.playwright"
|
||||
]
|
||||
}
|
||||
241
.vscode/launch.json
vendored
241
.vscode/launch.json
vendored
@@ -3,246 +3,11 @@
|
||||
// Hover to view descriptions of existing attributes.
|
||||
"configurations": [
|
||||
{
|
||||
"command": "pnpm generate:types",
|
||||
"name": "Generate Types CLI",
|
||||
"request": "launch",
|
||||
"type": "node-terminal",
|
||||
"cwd": "${workspaceFolder}"
|
||||
},
|
||||
{
|
||||
"command": "pnpm tsx --no-deprecation test/dev.ts _community",
|
||||
"command": "ts-node -T ./src/index.ts -n asdf -t blank --db mongodb --no-deps",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"name": "Run Dev Community",
|
||||
"name": "Debug",
|
||||
"request": "launch",
|
||||
"type": "node-terminal"
|
||||
},
|
||||
{
|
||||
"command": "pnpm tsx --no-deprecation test/dev.ts storage-uploadthing",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"name": "Uploadthing",
|
||||
"request": "launch",
|
||||
"type": "node-terminal",
|
||||
"envFile": "${workspaceFolder}/test/storage-uploadthing/.env"
|
||||
},
|
||||
{
|
||||
"command": "pnpm tsx --no-deprecation test/dev.ts live-preview",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"name": "Run Dev Live Preview",
|
||||
"request": "launch",
|
||||
"type": "node-terminal"
|
||||
},
|
||||
{
|
||||
"command": "node --no-deprecation test/loader/init.js",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"name": "Run Loader",
|
||||
"request": "launch",
|
||||
"type": "node-terminal",
|
||||
"env": {
|
||||
"LOADER_TEST_FILE_PATH": "./dependency-test.js"
|
||||
// "LOADER_TEST_FILE_PATH": "../fields/config.ts"
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "pnpm tsx --no-deprecation test/dev.ts admin",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"name": "Run Dev Admin",
|
||||
"request": "launch",
|
||||
"type": "node-terminal"
|
||||
},
|
||||
{
|
||||
"command": "pnpm tsx --no-deprecation test/dev.ts 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}",
|
||||
"name": "Run Dev - plugin-cloud-storage",
|
||||
"request": "launch",
|
||||
"type": "node-terminal",
|
||||
"env": {
|
||||
"PAYLOAD_PUBLIC_CLOUD_STORAGE_ADAPTER": "s3"
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "pnpm tsx --no-deprecation test/dev.ts collections-graphql",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"name": "Run Dev GraphQL",
|
||||
"request": "launch",
|
||||
"type": "node-terminal"
|
||||
},
|
||||
{
|
||||
"command": "pnpm tsx --no-deprecation test/dev.ts fields",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"name": "Run Dev Fields",
|
||||
"request": "launch",
|
||||
"type": "node-terminal"
|
||||
},
|
||||
{
|
||||
"command": "pnpm tsx --no-deprecation test/dev.ts versions",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"name": "Run Dev Postgres",
|
||||
"request": "launch",
|
||||
"type": "node-terminal",
|
||||
"env": {
|
||||
"PAYLOAD_DATABASE": "postgres"
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "pnpm tsx --no-deprecation test/dev.ts versions",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"name": "Run Dev Versions",
|
||||
"request": "launch",
|
||||
"type": "node-terminal"
|
||||
},
|
||||
{
|
||||
"command": "pnpm tsx --no-deprecation test/dev.ts 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",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"name": "Run Dev Uploads",
|
||||
"request": "launch",
|
||||
"type": "node-terminal"
|
||||
},
|
||||
{
|
||||
"command": "pnpm tsx --no-deprecation test/dev.ts field-error-states",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"name": "Run Dev Field Error States",
|
||||
"request": "launch",
|
||||
"type": "node-terminal"
|
||||
},
|
||||
{
|
||||
"command": "pnpm run test:int live-preview",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"name": "Live Preview Int Tests",
|
||||
"request": "launch",
|
||||
"type": "node-terminal"
|
||||
},
|
||||
{
|
||||
"command": "pnpm run test:int plugin-search",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"name": "Search Plugin Int Tests",
|
||||
"request": "launch",
|
||||
"type": "node-terminal"
|
||||
},
|
||||
{
|
||||
"command": "ts-node ./packages/payload/src/bin/index.ts build",
|
||||
"env": {
|
||||
"PAYLOAD_CONFIG_PATH": "test/fields/config.ts",
|
||||
"PAYLOAD_BUNDLER": "vite",
|
||||
"DISABLE_SWC": "true" // SWC messes up debugging the bin scripts
|
||||
},
|
||||
"name": "Build CLI - Vite",
|
||||
"outputCapture": "std",
|
||||
"request": "launch",
|
||||
"type": "node-terminal"
|
||||
},
|
||||
{
|
||||
"command": "ts-node ./packages/payload/src/bin/index.ts build",
|
||||
"env": {
|
||||
"PAYLOAD_CONFIG_PATH": "test/fields/config.ts",
|
||||
"PAYLOAD_ANALYZE_BUNDLE": "true",
|
||||
"DISABLE_SWC": "true" // SWC messes up debugging the bin scripts
|
||||
},
|
||||
"name": "Build CLI - Webpack",
|
||||
"outputCapture": "std",
|
||||
"request": "launch",
|
||||
"type": "node-terminal"
|
||||
},
|
||||
{
|
||||
"command": "ts-node ./packages/payload/src/bin/index.ts migrate:status",
|
||||
"env": {
|
||||
"PAYLOAD_CONFIG_PATH": "test/migrations-cli/config.ts",
|
||||
"PAYLOAD_DATABASE": "postgres",
|
||||
"DISABLE_SWC": "true" // SWC messes up debugging the bin scripts
|
||||
// "PAYLOAD_DROP_DATABASE": "true",
|
||||
},
|
||||
"name": "Migrate CLI - status",
|
||||
"outputCapture": "std",
|
||||
"request": "launch",
|
||||
"type": "node-terminal"
|
||||
},
|
||||
{
|
||||
"command": "ts-node ./packages/payload/src/bin/index.ts migrate:create yass",
|
||||
"env": {
|
||||
"PAYLOAD_CONFIG_PATH": "test/migrations-cli/config.ts",
|
||||
"PAYLOAD_DATABASE": "postgres",
|
||||
"DISABLE_SWC": "true" // SWC messes up debugging the bin scripts
|
||||
// "PAYLOAD_DROP_DATABASE": "true",
|
||||
},
|
||||
"name": "Migrate CLI - create",
|
||||
"outputCapture": "std",
|
||||
"request": "launch",
|
||||
"type": "node-terminal"
|
||||
},
|
||||
{
|
||||
"command": "ts-node ./packages/payload/src/bin/index.ts migrate:down",
|
||||
"env": {
|
||||
"PAYLOAD_CONFIG_PATH": "test/migrations-cli/config.ts",
|
||||
"PAYLOAD_DATABASE": "mongoose",
|
||||
"DISABLE_SWC": "true" // SWC messes up debugging the bin scripts
|
||||
// "PAYLOAD_DROP_DATABASE": "true",
|
||||
},
|
||||
"name": "Migrate CLI - down",
|
||||
"outputCapture": "std",
|
||||
"request": "launch",
|
||||
"type": "node-terminal"
|
||||
},
|
||||
{
|
||||
"command": "ts-node ./packages/payload/src/bin/index.ts migrate:reset",
|
||||
"env": {
|
||||
"PAYLOAD_CONFIG_PATH": "test/migrations-cli/config.ts",
|
||||
"PAYLOAD_DATABASE": "mongoose",
|
||||
"DISABLE_SWC": "true" // SWC messes up debugging the bin scripts
|
||||
// "PAYLOAD_DROP_DATABASE": "true",
|
||||
},
|
||||
"name": "Migrate CLI - reset",
|
||||
"outputCapture": "std",
|
||||
"request": "launch",
|
||||
"type": "node-terminal"
|
||||
},
|
||||
{
|
||||
"command": "ts-node ./packages/payload/src/bin/index.ts migrate:refresh",
|
||||
"env": {
|
||||
"PAYLOAD_CONFIG_PATH": "test/migrations-cli/config.ts",
|
||||
"PAYLOAD_DATABASE": "mongoose",
|
||||
"DISABLE_SWC": "true" // SWC messes up debugging the bin scripts
|
||||
// "PAYLOAD_DROP_DATABASE": "true",
|
||||
},
|
||||
"name": "Migrate CLI - refresh",
|
||||
"outputCapture": "std",
|
||||
"request": "launch",
|
||||
"type": "node-terminal"
|
||||
}
|
||||
],
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0"
|
||||
]
|
||||
}
|
||||
|
||||
56
.vscode/settings.json
vendored
56
.vscode/settings.json
vendored
@@ -1,56 +0,0 @@
|
||||
{
|
||||
"npm.packageManager": "pnpm",
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"[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 }
|
||||
],
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
// Load .git-blame-ignore-revs file
|
||||
"gitlens.advanced.blame.customArguments": ["--ignore-revs-file", ".git-blame-ignore-revs"],
|
||||
"[javascript][typescript][typescriptreact]": {
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": "explicit"
|
||||
}
|
||||
},
|
||||
"files.insertFinalNewline": true,
|
||||
"jestrunner.jestCommand": "pnpm exec cross-env NODE_OPTIONS=\"--no-deprecation\" node 'node_modules/jest/bin/jest.js'",
|
||||
"jestrunner.debugOptions": {
|
||||
"runtimeArgs": ["--no-deprecation"]
|
||||
}
|
||||
}
|
||||
4463
CHANGELOG.md
4463
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
140
CONTRIBUTING.md
140
CONTRIBUTING.md
@@ -1,140 +0,0 @@
|
||||
# Contributing to Payload
|
||||
|
||||
Below you'll find a set of guidelines for how to contribute to Payload.
|
||||
|
||||
## Opening issues
|
||||
|
||||
Before you submit an issue, please check all existing [open and closed issues](https://github.com/payloadcms/payload/issues) to see if your issue has previously been resolved or is already known. If there is already an issue logged, feel free to upvote it by adding a :thumbsup: [reaction](https://github.com/blog/2119-add-reactions-to-pull-requests-issues-and-comments). If you would like to submit a new issue, please fill out our Issue Template to the best of your ability so we can accurately understand your report.
|
||||
|
||||
## Security issues & vulnerabilities
|
||||
|
||||
If you come across an issue related to security, or a potential attack vector within Payload or one of its dependencies, please DO NOT create a publicly viewable issue. Instead, please contact us directly at [`dev@payloadcms.com`](mailto:dev@payloadcms.com). We will do everything we can to respond to the issue as soon as possible.
|
||||
|
||||
If you find a vulnerability within the core Payload repository, and we determine that it is remediable and of significant nature, we will be happy to pay you a reward for your findings and diligence. [`Contact us`](mailto:dev@payloadcms.com) to find out more.
|
||||
|
||||
## Documentation edits
|
||||
|
||||
Payload documentation can be found directly within its codebase, and you can feel free to make changes / improvements to any of it through opening a PR. We utilize these files directly in our website and will periodically deploy documentation updates as necessary.
|
||||
|
||||
## Building additional features
|
||||
|
||||
If you're an incredibly awesome person and want to help us make Payload even better through new features or additions, we would be thrilled to work with you.
|
||||
|
||||
## Design Contributions
|
||||
|
||||
When it comes to design-related changes or additions, it's crucial for us to ensure a cohesive user experience and alignment with our broader design vision. Before embarking on any implementation that would affect the design or UI/UX, we ask that you **first share your design proposal** with us for review and approval.
|
||||
|
||||
Our design review ensures that proposed changes fit seamlessly with other components, both existing and planned. This step is meant to prevent unintentional design inconsistencies and to save you from investing time in implementing features that might need significant design alterations later.
|
||||
|
||||
### Before Starting
|
||||
|
||||
To help us work on new features, you can create a new feature request post in [GitHub Discussion](https://github.com/payloadcms/payload/discussions) or discuss it in our [Discord](https://discord.com/invite/payload). New functionality often has large implications across the entire Payload repo, so it is best to discuss the architecture and approach before starting work on a pull request.
|
||||
|
||||
### Installation & Requirements
|
||||
|
||||
Payload is structured as a Monorepo, encompassing not only the core Payload platform but also various plugins and packages. To install all required dependencies, you have to run `pnpm install` once in the root directory. **PNPM IS REQUIRED!** Yarn or npm will not work - you will have to use pnpm to develop in the core repository. In most systems, the easiest way to install pnpm is to run `corepack enable` in your terminal.
|
||||
|
||||
If you're coming from a very outdated version of payload, it is recommended to nuke the node_modules folder before running pnpm install. On UNIX systems, you can easily do that using the `pnpm clean:unix` command, which will delete all node_modules folders and build artefacts.
|
||||
|
||||
It is also recommended to use at least Node v18 or higher. You can check your current node version by typing `node --version` in your terminal. The easiest way to switch between different node versions is to use [nvm](https://github.com/nvm-sh/nvm#intro).
|
||||
|
||||
### Code
|
||||
|
||||
Most new functionality should keep testing in mind. All top-level directories within the `test/` directory are for testing a specific category: `fields`, `collections`, etc.
|
||||
|
||||
If it makes sense to add your feature to an existing test directory, please do so.
|
||||
|
||||
A typical directory with `test/` will be structured like this:
|
||||
|
||||
```text
|
||||
.
|
||||
├── config.ts
|
||||
├── int.spec.ts
|
||||
├── e2e.spec.ts
|
||||
└── payload-types.ts
|
||||
```
|
||||
|
||||
- `config.ts` - This is the _granular_ Payload config for testing. It should be as lightweight as possible. Reference existing configs for an example
|
||||
- `int.spec.ts` - This is the test file run by jest. Any test file must have a `*int.spec.ts` suffix.
|
||||
- `e2e.spec.ts` - This is the end-to-end test file that will load up the admin UI using the above config and run Playwright tests. These tests are typically only needed if a large change is being made to the Admin UI.
|
||||
- `payload-types.ts` - Generated types from `config.ts`. Generate this file by running `pnpm dev:generate-types my-test-dir`. Replace `my-test-dir` with the name of your testing directory.
|
||||
|
||||
Each test directory is split up in this way specifically to reduce friction when creating tests and to add the ability to boot up Payload with that specific config.
|
||||
|
||||
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#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.
|
||||
|
||||
### Testing with your own MongoDB database
|
||||
|
||||
If you wish to use your own MongoDB database for the `test` directory instead of using the in memory database, all you need to do is add the following env vars to the `test/dev.ts` file:
|
||||
|
||||
- `process.env.NODE_ENV`
|
||||
- `process.env.PAYLOAD_TEST_MONGO_URL`
|
||||
- Simply set `process.env.NODE_ENV` to `test` and set `process.env.PAYLOAD_TEST_MONGO_URL` to your MongoDB URL e.g. `mongodb://127.0.0.1/your-test-db`.
|
||||
|
||||
### Using Postgres
|
||||
|
||||
If you have postgres installed on your system, you can also run the test suites using postgres. By default, mongodb is used.
|
||||
|
||||
To do that, simply set the `PAYLOAD_DATABASE` environment variable to `postgres`.
|
||||
|
||||
### Running the e2e and int tests
|
||||
|
||||
You can run the entire test suite using `pnpm test`. If you wish to only run e2e tests, you can use `pnpm test:e2e`. If you wish to only run int tests, you can use `pnpm test:int`.
|
||||
|
||||
By default, `pnpm test:int` will only run int test against MongoDB. To run int tests against postgres, you can use `pnpm test:int:postgres`. You will have to have postgres installed on your system for this to work.
|
||||
|
||||
### Commits
|
||||
|
||||
We use [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) for our commit messages. Please follow this format when creating commits. Here are some examples:
|
||||
|
||||
- `feat: adds new feature`
|
||||
- `fix: fixes bug`
|
||||
- `docs: adds documentation`
|
||||
- `chore: does chore`
|
||||
|
||||
Here's a breakdown of the format. At the top-level, we use the following types to categorize our commits:
|
||||
|
||||
- `feat`: new feature that adds functionality. These are automatically added to the changelog when creating new releases.
|
||||
- `fix`: a fix to an existing feature. These are automatically added to the changelog when creating new releases.
|
||||
- `docs`: changes to [docs](./docs) only. These do not appear in the changelog.
|
||||
- `chore`: changes to code that is neither a fix nor a feature (e.g. refactoring, adding tests, etc.). These do not appear in the changelog.
|
||||
|
||||
If you are committing to [templates](./templates) or [examples](./examples), use the `chore` type with the proper scope, like this:
|
||||
|
||||
- `chore(templates): adds feature to template`
|
||||
- `chore(examples): fixes bug in example`
|
||||
|
||||
## Pull Requests
|
||||
|
||||
For all Pull Requests, you should be extremely descriptive about both your problem and proposed solution. If there are any affected open or closed issues, please leave the issue number in your PR message.
|
||||
|
||||
## Previewing docs
|
||||
|
||||
This is how you can preview changes you made locally to the docs:
|
||||
|
||||
1. Clone our [website repository](https://github.com/payloadcms/website)
|
||||
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
|
||||
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')
|
||||
```
|
||||
@@ -1,69 +0,0 @@
|
||||
# Reporting an issue
|
||||
|
||||
To report an issue, please follow the steps below:
|
||||
|
||||
1. Fork this repository
|
||||
2. Add necessary collections/globals/fields to the `test/_community` directory to recreate the issue you are experiencing
|
||||
3. Create an issue and add a link to your forked repo
|
||||
|
||||
**The goal is to isolate the problem by reducing the number of fields/collections you add to the test/\_community folder. This folder is not meant for you to copy your project into, but to recreate the issue you are experiencing with minimal config.**
|
||||
|
||||
## Test directory file tree explanation
|
||||
|
||||
```text
|
||||
.
|
||||
├── config.ts
|
||||
├── int.spec.ts
|
||||
├── e2e.spec.ts
|
||||
└── payload-types.ts
|
||||
```
|
||||
|
||||
- `config.ts` - This is the _granular_ Payload config for testing. It should be as lightweight as possible. Reference existing configs for an example
|
||||
- `int.spec.ts` [Optional] - This is the test file run by jest. Any test file must have a `*int.spec.ts` suffix.
|
||||
- `e2e.spec.ts` [Optional] - This is the end-to-end test file that will load up the admin UI using the above config and run Playwright tests.
|
||||
- `payload-types.ts` - Generated types from `config.ts`. Generate this file by running `pnpm dev:generate-types _community`.
|
||||
|
||||
The directory split up in this way specifically to reduce friction when creating tests and to add the ability to boot up Payload with that specific config. You should modify the files in `test/_community` to get started.
|
||||
|
||||
## How to start test collection admin UI
|
||||
|
||||
To start the admin panel so you can manually recreate your issue, you can run the following command:
|
||||
|
||||
```bash
|
||||
# This command will start up Payload using your config
|
||||
# NOTE: it will wipe the test database on restart
|
||||
pnpm dev _community
|
||||
```
|
||||
|
||||
## Testing is optional but encouraged
|
||||
|
||||
An issue does not need to have failing tests — reproduction steps with your forked repo are enough at this point. Some people like to dive deeper and we want to give you the guidance/tools to do so. Read more below.
|
||||
|
||||
### How to run integration tests (Payload API tests)
|
||||
|
||||
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/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:
|
||||
|
||||
```bash
|
||||
pnpm test:int _community
|
||||
```
|
||||
|
||||
### How to run E2E tests (Admin Panel UI tests)
|
||||
|
||||
The easiest way to run E2E tests is to install
|
||||
|
||||
- [Playwright Test for VSCode](https://marketplace.visualstudio.com/items?itemName=ms-playwright.playwright)
|
||||
- [Playwright Runner](https://marketplace.visualstudio.com/items?itemName=ortoni.ortoni)
|
||||
|
||||
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/src/admin/assets/images/github/e2e-debug.png" />
|
||||
|
||||
#### Notes
|
||||
|
||||
- It is recommended to add the test credentials (located in `test/credentials.ts`) to your autofill for `localhost:3000/admin` as this will be required on every nodemon restart. The default credentials are `dev@payloadcms.com` as email and `test` as password.
|
||||
157
README.md
157
README.md
@@ -1,133 +1,34 @@
|
||||
<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 />
|
||||
# Create Payload App
|
||||
|
||||
<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/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://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/>
|
||||
CLI for easily starting new Payload project
|
||||
|
||||
> [!IMPORTANT]
|
||||
> 🎉 <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>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>
|
||||
|
||||
## ☁️ 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).
|
||||
## Usage
|
||||
|
||||
```text
|
||||
npx create-payload-app@latest
|
||||
|
||||
USAGE
|
||||
|
||||
$ npx create-payload-app
|
||||
$ npx create-payload-app my-project
|
||||
$ npx create-payload-app -n my-project -t blog
|
||||
|
||||
OPTIONS
|
||||
|
||||
-n my-payload-app Set project name
|
||||
-t template_name Choose specific template
|
||||
|
||||
Available templates:
|
||||
|
||||
blank Blank Template
|
||||
website Website Template
|
||||
ecommerce E-commerce Template
|
||||
plugin Template for creating a Payload plugin
|
||||
payload-demo Payload demo site at https://demo.payloadcms.com
|
||||
payload-website Payload website CMS at https://payloadcms.com
|
||||
|
||||
--use-npm Use npm to install dependencies
|
||||
--use-yarn Use yarn to install dependencies
|
||||
--use-pnpm Use pnpm to install dependencies
|
||||
--no-deps Do not install any dependencies
|
||||
-h Show help
|
||||
```
|
||||
|
||||
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
|
||||
|
||||
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 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.
|
||||
|
||||
- [Official Templates](https://github.com/payloadcms/payload/tree/main/templates)
|
||||
- [Community Templates](https://github.com/topics/payload-template)
|
||||
|
||||
## ✨ Features
|
||||
|
||||
- Completely free and open-source
|
||||
- [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
|
||||
|
||||
<a target="_blank" href="https://github.com/payloadcms/payload/discussions"><strong>Request Feature</strong></a>
|
||||
|
||||
## 🗒️ Documentation
|
||||
|
||||
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 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
|
||||
|
||||
If you want to add contributions to this repository, please follow the instructions in [contributing.md](./CONTRIBUTING.md).
|
||||
|
||||
## 📚 Examples
|
||||
|
||||
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 either copy them to a folder outside this repo or run them directly by (1) navigating to the example's subfolder (`cd examples/your-example-folder`) and (2) using the `--ignore-workspace` flag to bypass workspace restrictions (e.g., `pnpm --ignore-workspace install` or `pnpm --ignore-workspace dev`).
|
||||
|
||||
You can see more examples at:
|
||||
|
||||
- [Examples Directory](./examples)
|
||||
- [Payload Blog](https://payloadcms.com/blog)
|
||||
- [Payload YouTube](https://www.youtube.com/@payloadcms)
|
||||
|
||||
## 🔌 Plugins
|
||||
|
||||
Payload is highly extensible and allows you to install or distribute plugins that add or remove functionality. There are both officially-supported and community-supported plugins available. If you maintain your own plugin, consider adding the `payload-plugin` topic to your GitHub repository for others to find.
|
||||
|
||||
- [Official Plugins](https://github.com/orgs/payloadcms/repositories?q=topic%3Apayload-plugin)
|
||||
- [Community Plugins](https://github.com/topics/payload-plugin)
|
||||
|
||||
## 🚨 Need help?
|
||||
|
||||
There are lots of good conversations and resources in our Github Discussions board and our Discord Server. If you're struggling with something, chances are, someone's already solved what you're up against. :point_down:
|
||||
|
||||
- [GitHub Discussions](https://github.com/payloadcms/payload/discussions)
|
||||
- [GitHub Issues](https://github.com/payloadcms/payload/issues)
|
||||
- [Discord](https://t.co/30APlsQUPB)
|
||||
- [Community Help](https://payloadcms.com/community-help)
|
||||
|
||||
## ⭐ Like what we're doing? Give us a star
|
||||
|
||||

|
||||
|
||||
## 👏 Thanks to all our contributors
|
||||
|
||||
<img align="left" src="https://contributors-img.web.app/image?repo=payloadcms/payload"/>
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
# Security Policy
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
Please report any security issues or concerns to [info@payloadcms.com](mailto:info@payloadcms.com).
|
||||
@@ -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 { getPayloadHMR } from '@payloadcms/next/utilities'
|
||||
|
||||
export const Page = async ({ params, searchParams }) => {
|
||||
const payload = await getPayloadHMR({
|
||||
config: configPromise,
|
||||
})
|
||||
return <div>test ${payload?.config?.collections?.length}</div>
|
||||
}
|
||||
|
||||
export default Page
|
||||
@@ -1,25 +0,0 @@
|
||||
/* 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'
|
||||
|
||||
type Args = {
|
||||
params: Promise<{
|
||||
segments: string[]
|
||||
}>
|
||||
searchParams: Promise<{
|
||||
[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 })
|
||||
|
||||
export default NotFound
|
||||
@@ -1,25 +0,0 @@
|
||||
/* 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'
|
||||
|
||||
type Args = {
|
||||
params: Promise<{
|
||||
segments: string[]
|
||||
}>
|
||||
searchParams: Promise<{
|
||||
[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 })
|
||||
|
||||
export default Page
|
||||
@@ -1,10 +0,0 @@
|
||||
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||
import config from '@payload-config'
|
||||
import { REST_DELETE, REST_GET, REST_OPTIONS, REST_PATCH, REST_POST } from '@payloadcms/next/routes'
|
||||
|
||||
export const GET = REST_GET(config)
|
||||
export const POST = REST_POST(config)
|
||||
export const DELETE = REST_DELETE(config)
|
||||
export const PATCH = REST_PATCH(config)
|
||||
export const OPTIONS = REST_OPTIONS(config)
|
||||
@@ -1,6 +0,0 @@
|
||||
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||
import config from '@payload-config'
|
||||
import { GRAPHQL_PLAYGROUND_GET } from '@payloadcms/next/routes'
|
||||
|
||||
export const GET = GRAPHQL_PLAYGROUND_GET(config)
|
||||
@@ -1,6 +0,0 @@
|
||||
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||
import config from '@payload-config'
|
||||
import { GRAPHQL_POST } from '@payloadcms/next/routes'
|
||||
|
||||
export const POST = GRAPHQL_POST(config)
|
||||
@@ -1,8 +0,0 @@
|
||||
#custom-css {
|
||||
font-family: monospace;
|
||||
background-image: url('/placeholder.png');
|
||||
}
|
||||
|
||||
#custom-css::after {
|
||||
content: 'custom-css';
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
/* 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 React from 'react'
|
||||
|
||||
import { importMap } from './admin/importMap.js'
|
||||
import './custom.scss'
|
||||
|
||||
type Args = {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
const Layout = ({ children }: Args) => (
|
||||
<RootLayout config={configPromise} importMap={importMap}>
|
||||
{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>
|
||||
)
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
export const GET = () => {
|
||||
return Response.json({
|
||||
hello: 'elliot',
|
||||
})
|
||||
}
|
||||
2
bin/cli.js
Executable file
2
bin/cli.js
Executable file
@@ -0,0 +1,2 @@
|
||||
#!/usr/bin/env node
|
||||
require('../dist/index.js')
|
||||
@@ -1,339 +0,0 @@
|
||||
---
|
||||
title: Collection Access Control
|
||||
label: Collections
|
||||
order: 20
|
||||
desc: With Collection-level Access Control you can define which users can create, read, update or delete Collections.
|
||||
keywords: collections, access control, permissions, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
|
||||
---
|
||||
|
||||
Collection Access Control is [Access Control](../access-control) used to restrict access to Documents within a [Collection](../collections/overview), as well as what they can and cannot see within the [Admin Panel](../admin/overview) as it relates to that Collection.
|
||||
|
||||
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:
|
||||
|
||||
| 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). |
|
||||
|
||||
If a Collection supports [`Authentication`](../authentication/overview), the following additional options are 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). |
|
||||
|
||||
If a Collection 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). |
|
||||
|
||||
### 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](../collections/overview):
|
||||
|
||||
```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:
|
||||
|
||||
| Option | Description |
|
||||
| ---------- | ---------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **`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](../collections/overview):
|
||||
|
||||
```ts
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
export const CollectionWithReadAccess: CollectionConfig = {
|
||||
// ...
|
||||
access: {
|
||||
// highlight-start
|
||||
read: ({ req: { user } }) => {
|
||||
return Boolean(user)
|
||||
},
|
||||
// highlight-end
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
<Banner type="success">
|
||||
<strong>Tip:</strong>
|
||||
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:
|
||||
|
||||
```ts
|
||||
import type { Access } from 'payload'
|
||||
|
||||
export 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
|
||||
return {
|
||||
isPublic: {
|
||||
equals: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
To add update Access Control to a Collection, use the `update` property in the [Collection Config](../collections/overview):
|
||||
|
||||
```ts
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
export const CollectionWithUpdateAccess: CollectionConfig = {
|
||||
// ...
|
||||
access: {
|
||||
// highlight-start
|
||||
update: ({ req: { user }}) => {
|
||||
return Boolean(user)
|
||||
},
|
||||
// highlight-end
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
<Banner type="success">
|
||||
<strong>Tip:</strong>
|
||||
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:
|
||||
|
||||
```ts
|
||||
import type { Access } from 'payload'
|
||||
|
||||
export 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](../collections/overview):
|
||||
|
||||
```ts
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
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:
|
||||
|
||||
```ts
|
||||
import type { Access } from 'payload'
|
||||
|
||||
export const canDeleteCustomer: Access = async ({ req, id }) => {
|
||||
if (!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`
|
||||
const result = await req.payload.find({
|
||||
collection: 'contracts',
|
||||
limit: 0,
|
||||
depth: 0,
|
||||
where: {
|
||||
customer: { equals: id },
|
||||
},
|
||||
})
|
||||
|
||||
return result.totalDocs === 0
|
||||
}
|
||||
```
|
||||
|
||||
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 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](../collections/overview):
|
||||
|
||||
```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:
|
||||
|
||||
| Option | Description |
|
||||
| --------- | -------------------------------------------------------------------------- |
|
||||
| **`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#options).
|
||||
|
||||
To add Unlock Access Control to a Collection, use the `unlock` property in the [Collection Config](../collections/overview):
|
||||
|
||||
```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:
|
||||
|
||||
| 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](../collections/overview):
|
||||
|
||||
```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`. |
|
||||
@@ -1,103 +0,0 @@
|
||||
---
|
||||
title: Field-level Access Control
|
||||
label: Fields
|
||||
order: 40
|
||||
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) used to restrict access to specific [Fields](../fields/overview) within a Document.
|
||||
|
||||
To add Access Control to a Field, use the `access` property in your [Field Config](../fields/overview):
|
||||
|
||||
```ts
|
||||
import type { Field } from 'payload';
|
||||
|
||||
export const FieldWithAccessControl: Field = {
|
||||
// ...
|
||||
access: { // highlight-line
|
||||
// ...
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
<Banner type="warning">
|
||||
<strong>Note:</strong>
|
||||
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';
|
||||
|
||||
export const Posts: CollectionConfig = {
|
||||
slug: 'posts',
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
// highlight-start
|
||||
access: {
|
||||
create: ({ req: { user } }) => { ... },
|
||||
read: ({ req: { user } }) => { ... },
|
||||
update: ({ req: { user } }) => { ... },
|
||||
},
|
||||
// highlight-end
|
||||
};
|
||||
],
|
||||
};
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
**Available argument properties:**
|
||||
|
||||
| Option | Description |
|
||||
| ----------------- | -------------------------------------------------------------------------- |
|
||||
| **`req`** | The [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object containing the currently authenticated `user` |
|
||||
| **`data`** | The full data passed to create the document. |
|
||||
| **`siblingData`** | Immediately adjacent field data passed to create the document. |
|
||||
|
||||
### Read
|
||||
|
||||
Returns a boolean which allows or denies the ability to read a field's value. If `false`, the entire property is omitted from the resulting document.
|
||||
|
||||
**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 the document being read |
|
||||
| **`doc`** | The full document data. |
|
||||
| **`siblingData`** | Immediately adjacent field data of the document being read. |
|
||||
|
||||
### Update
|
||||
|
||||
Returns a boolean which allows or denies the ability to update a field's value. If `false` is returned, any passed values will be discarded.
|
||||
|
||||
If `false` is returned and you attempt to update the field's value, the operation will **not** throw an error however the field will be omitted from the update operation and the value will remain unchanged.
|
||||
|
||||
**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 the document being updated |
|
||||
| **`data`** | The full data passed to update the document. |
|
||||
| **`siblingData`** | Immediately adjacent field data passed to update the document with. |
|
||||
| **`doc`** | The full document data, before the update is applied. |
|
||||
@@ -1,142 +0,0 @@
|
||||
---
|
||||
title: Globals Access Control
|
||||
label: Globals
|
||||
order: 30
|
||||
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) used to restrict access to [Global](../globals/overview) Documents, as well as what they can and cannot see within the [Admin Panel](../admin/overview) as it relates to that Global.
|
||||
|
||||
To add Access Control to a Global, use the `access` property in your [Global Config](../configuration/globals):
|
||||
|
||||
```ts
|
||||
import type { GlobalConfig } from 'payload';
|
||||
|
||||
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](../globals/overview):
|
||||
|
||||
```ts
|
||||
import { GlobalConfig } from 'payload'
|
||||
|
||||
const GlobalWithAccessControl: GlobalConfig = {
|
||||
// ...
|
||||
// highlight-start
|
||||
access: {
|
||||
read: ({ req: { user } }) => {...},
|
||||
update: ({ req: { user } }) => {...},
|
||||
|
||||
// Version-enabled Globals only
|
||||
readVersion: () => {...},
|
||||
},
|
||||
// highlight-end
|
||||
}
|
||||
|
||||
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.
|
||||
|
||||
To add read Access Control to a [Global](../configuration/globals), use the `read` property in the [Global Config](../globals/overview):
|
||||
|
||||
```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:
|
||||
|
||||
| Option | Description |
|
||||
| --------- | -------------------------------------------------------------------------- |
|
||||
| **`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.
|
||||
|
||||
To add update Access Control to a [Global](../configuration/globals), use the `access` property in the [Global Config](../globals/overview):
|
||||
|
||||
```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:
|
||||
|
||||
| Option | Description |
|
||||
| ---------- | -------------------------------------------------------------------------- |
|
||||
| **`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](../globals/overview):
|
||||
|
||||
```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`. |
|
||||
@@ -1,60 +0,0 @@
|
||||
---
|
||||
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.
|
||||
keywords: overview, access control, permissions, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
|
||||
---
|
||||
|
||||
<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.
|
||||
|
||||
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.
|
||||
|
||||
There are many use cases for Access Control, including:
|
||||
|
||||
- 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
|
||||
|
||||
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) 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:
|
||||
|
||||
```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
|
||||
}
|
||||
```
|
||||
|
||||
<Banner type="warning">
|
||||
<strong>Important:</strong>
|
||||
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>
|
||||
|
||||
## The Access Operation
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
<Banner type="warning">
|
||||
<strong>Important:</strong>
|
||||
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.
|
||||
</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.
|
||||
@@ -1,173 +0,0 @@
|
||||
---
|
||||
title: Collection Admin Config
|
||||
label: Collections
|
||||
order: 20
|
||||
desc:
|
||||
keywords: admin, components, custom, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
|
||||
---
|
||||
|
||||
The behavior of [Collections](../configuration/collections) within the [Admin Panel](./overview) can be fully customized to fit the needs of your application. This includes grouping or hiding their navigation links, adding [Custom Components](./components), selecting which fields to display in the List View, and more.
|
||||
|
||||
To configure Admin Options for Collections, use the `admin` property in your Collection Config:
|
||||
|
||||
```ts
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
export const MyCollection: CollectionConfig = {
|
||||
// ...
|
||||
admin: { // highlight-line
|
||||
// ...
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## Admin Options
|
||||
|
||||
The following options are available:
|
||||
|
||||
| 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](../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](#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](./metadata). |
|
||||
| **`preview`** | Function to generate preview URLs within the Admin Panel 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](#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). |
|
||||
|
||||
### Components
|
||||
|
||||
Collections can set their own [Custom Components](./components) which only apply to [Collection](../configuration/collections)-specific UI within the [Admin Panel](./overview). This includes elements such as the Save Button, or entire layouts such as the Edit View.
|
||||
|
||||
To override Collection Components, use the `admin.components` property in your [Collection Config](../configuration/collections):
|
||||
|
||||
```ts
|
||||
import type { SanitizedCollectionConfig } from 'payload'
|
||||
|
||||
export const MyCollection: SanitizedCollectionConfig = {
|
||||
// ...
|
||||
admin: {
|
||||
components: { // highlight-line
|
||||
// ...
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
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
|
||||
| **`Description`** | A component to render below the Collection label in the List View. An alternative to the `admin.description` property. |
|
||||
| **`edit.SaveButton`** | Replace the default Save Button with a Custom Component. [Drafts](../versions/drafts) must be disabled. |
|
||||
| **`edit.SaveDraftButton`** | Replace the default Save Draft Button with a Custom Component. [Drafts](../versions/drafts) must be enabled and autosave must be disabled. |
|
||||
| **`edit.PublishButton`** | Replace the default Publish Button with a Custom Component. [Drafts](../versions/drafts) must be enabled. |
|
||||
| **`edit.PreviewButton`** | Replace the default Preview Button with a Custom Component. [Preview](#preview) must be enabled. |
|
||||
| **`views`** | Override or create new views within the Admin Panel. [More details](./views). |
|
||||
|
||||
<Banner type="success">
|
||||
<strong>Note:</strong>
|
||||
For details on how to build Custom Components, see [Building Custom Components](./components#building-custom-components).
|
||||
</Banner>
|
||||
|
||||
### Preview
|
||||
|
||||
It is possible to display a Preview Button within the Edit View of the Admin Panel. 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 configure the Preview Button, set the `admin.preview` property to a function in your [Collection Config](../configuration/collections):
|
||||
|
||||
```ts
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
export const Posts: CollectionConfig = {
|
||||
// ...
|
||||
admin: {
|
||||
// highlight-start
|
||||
preview: (doc, { locale }) => {
|
||||
if (doc?.slug) {
|
||||
return `/${doc.slug}?locale=${locale}`
|
||||
}
|
||||
|
||||
return null
|
||||
},
|
||||
// highlight-end
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
The preview function receives two arguments:
|
||||
|
||||
| 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">
|
||||
<strong>Note:</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.
|
||||
|
||||
To configure pagination options, use the `admin.pagination` property in your [Collection Config](../configuration/collections):
|
||||
|
||||
```ts
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
export const Posts: CollectionConfig = {
|
||||
// ...
|
||||
admin: {
|
||||
// highlight-start
|
||||
pagination: {
|
||||
defaultLimit: 10,
|
||||
limits: [10, 20, 50],
|
||||
},
|
||||
// highlight-end
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
The following options are available:
|
||||
|
||||
| Option | Description |
|
||||
| -------------- | --------------------------------------------------------------------------------------------------- |
|
||||
| `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. |
|
||||
|
||||
### 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.
|
||||
|
||||
To define which fields should be searched, use the `admin.listSearchableFields` property in your [Collection Config](../configuration/collections):
|
||||
|
||||
```ts
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
export const Posts: CollectionConfig = {
|
||||
// ...
|
||||
admin: {
|
||||
// highlight-start
|
||||
listSearchableFields: ['title', 'slug'],
|
||||
// highlight-end
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
<Banner type="warning">
|
||||
<strong>Tip:</strong>
|
||||
If you are adding `listSearchableFields`, make sure you index each of these fields so your admin queries can remain performant.
|
||||
</Banner>
|
||||
@@ -1,601 +0,0 @@
|
||||
---
|
||||
title: Swap in your own React components
|
||||
label: Custom Components
|
||||
order: 40
|
||||
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/overview) 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](./collections#components)
|
||||
- [Global Components](./globals#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.
|
||||
|
||||
|
||||
## Defining Custom Components in the Payload Config
|
||||
|
||||
In the Payload Config, you can define custom React Components to enhance the admin interface. However, these components should not be imported directly into the server-only Payload Config to avoid including client-side code. Instead, you specify the path to the component. Here’s how you can do it:
|
||||
|
||||
|
||||
src/components/Logout.tsx
|
||||
```tsx
|
||||
'use client'
|
||||
import React from 'react'
|
||||
|
||||
export const MyComponent = () => {
|
||||
return (
|
||||
<button>Click me!</button>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
payload.config.ts:
|
||||
```ts
|
||||
import { buildConfig } from 'payload'
|
||||
|
||||
const config = buildConfig({
|
||||
// ...
|
||||
admin: { // highlight-line
|
||||
components: {
|
||||
logout: {
|
||||
Button: '/src/components/Logout#MyComponent'
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
In the path `/src/components/Logout#MyComponent`, `/src/components/Logout` is the file path, and `MyComponent` is the named export. If the component is the default export, the export name can be omitted. Path and export name are separated by a `#`.
|
||||
|
||||
### Configuring the Base Directory
|
||||
|
||||
Component paths, by default, are relative to your working directory - this is usually where your Next.js config lies. To simplify component paths, you have the option to configure the *base directory* using the `admin.baseDir.baseDir` property:
|
||||
|
||||
```ts
|
||||
import { buildConfig } from 'payload'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import path from 'path'
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
const dirname = path.dirname(filename)
|
||||
|
||||
const config = buildConfig({
|
||||
// ...
|
||||
admin: { // highlight-line
|
||||
importMap: {
|
||||
baseDir: path.resolve(dirname, 'src'),
|
||||
},
|
||||
components: {
|
||||
logout: {
|
||||
Button: '/components/Logout#MyComponent'
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
In this example, we set the base directory to the `src` directory - thus we can omit the `/src/` part of our component path string.
|
||||
|
||||
### Passing Props
|
||||
|
||||
Each React Component in the Payload Config is typed as `PayloadComponent`. This usually is a string, but can also be an object containing the following properties:
|
||||
|
||||
| Property | Description |
|
||||
|---------------|-------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `clientProps` | Props to be passed to the React Component if it's a Client Component |
|
||||
| `exportName` | Instead of declaring named exports using `#` in the component path, you can also omit them from `path` and pass them in here. |
|
||||
| `path` | Path to the React Component. Named exports can be appended to the end of the path, separated by a `#` |
|
||||
| `serverProps` | Props to be passed to the React Component if it's a Server Component |
|
||||
|
||||
To pass in props from the config, you can use the `clientProps` and/or `serverProps` properties. This alleviates the need to use an HOC (Higher-Order-Component) to declare a React Component with props passed in.
|
||||
|
||||
Here is an example:
|
||||
|
||||
src/components/Logout.tsx
|
||||
```tsx
|
||||
'use client'
|
||||
import React from 'react'
|
||||
|
||||
export const MyComponent = ({ text }: { text: string }) => {
|
||||
return (
|
||||
<button>Click me! {text}</button>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
payload.config.ts:
|
||||
```ts
|
||||
import { buildConfig } from 'payload'
|
||||
|
||||
const config = buildConfig({
|
||||
// ...
|
||||
admin: { // highlight-line
|
||||
components: {
|
||||
logout: {
|
||||
Button: {
|
||||
path: '/src/components/Logout',
|
||||
clientProps: {
|
||||
text: 'Some Text.'
|
||||
},
|
||||
exportName: 'MyComponent'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
### Import Maps
|
||||
|
||||
It's essential to understand how `PayloadComponent` paths function behind the scenes. Directly importing React Components into your Payload Config using import statements can introduce client-only modules like CSS into your server-only config. This could error when attempting to load the Payload Config in server-only environments and unnecessarily increase the size of the Payload Config, which should remain streamlined and efficient for server use.
|
||||
|
||||
Instead, we utilize component paths to reference React Components. This method enhances the Payload Config with actual React Component imports on the client side, without affecting server-side usage. A script is deployed to scan the Payload Config, collecting all component paths and creating an `importMap.js`. This file, located in app/(payload)/admin/importMap.js, must be statically imported by your Next.js root page and layout. The script imports all the React Components from the specified paths into a Map, associating them with their respective paths (the ones you defined).
|
||||
|
||||
When constructing the `ClientConfig`, Payload uses the component paths as keys to fetch the corresponding React Component imports from the Import Map. It then substitutes the `PayloadComponent` with a `MappedComponent`. A `MappedComponent` includes the React Component and additional metadata, such as whether it's a server or a client component and which props it should receive. These components are then rendered through the `<RenderComponent />` component within the Payload Admin Panel.
|
||||
|
||||
Import maps are regenerated whenever you modify any element related to component paths. This regeneration occurs at startup and whenever Hot Module Replacement (HMR) runs. If the import maps fail to regenerate during HMR, you can restart your application and execute the `payload generate:importmap` command to manually create a new import map. If you encounter any errors running this command, see the [Troubleshooting](../local-api/outside-nextjs#troubleshooting) section.
|
||||
|
||||
### Component paths in external packages
|
||||
|
||||
Component paths are resolved relative to your project's base directory, which is either your current working directory or the directory specified in `config.admin.baseDir`. When using custom components from external packages, you can't use relative paths. Instead, use an import path that's accessible as if you were writing an import statement in your project's base directory.
|
||||
|
||||
For example, to export a field with a custom component from an external package named `my-external-package`:
|
||||
|
||||
```ts
|
||||
import type { Field } from 'payload'
|
||||
export const MyCustomField: Field = {
|
||||
type: 'text',
|
||||
name: 'MyField',
|
||||
admin: {
|
||||
components: {
|
||||
Field: 'my-external-package/client#MyFieldComponent'
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Despite `MyFieldComponent` living in `src/components/MyFieldComponent.tsx` in `my-external-package`, this will not be accessible from the consuming project. Instead, we recommend exporting all custom components from one file in the external package. For example, you can define a `src/client.ts file in `my-external-package`:
|
||||
|
||||
```ts
|
||||
'use client'
|
||||
export { MyFieldComponent } from './components/MyFieldComponent'
|
||||
```
|
||||
|
||||
Then, update the package.json of `my-external-package:
|
||||
|
||||
```json
|
||||
{
|
||||
...
|
||||
"exports": {
|
||||
"./client": {
|
||||
"import": "./dist/client.js",
|
||||
"types": "./dist/client.d.ts",
|
||||
"default": "./dist/client.js"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This setup allows you to specify the component path as `my-external-package/client#MyFieldComponent` as seen above. The import map will generate:
|
||||
|
||||
```ts
|
||||
import { MyFieldComponent } from 'my-external-package/client'
|
||||
```
|
||||
|
||||
which is a valid way to access MyFieldComponent that can be resolved by the consuming project.
|
||||
|
||||
### Custom Components from unknown locations
|
||||
|
||||
By default, any component paths from known locations are added to the import map. However, if you need to add any components from unknown locations to the import map, you can do so by adding them to the `admin.dependencies` array in your Payload Config. This is mostly only relevant for plugin authors and not for regular Payload users.
|
||||
|
||||
Example:
|
||||
|
||||
```ts
|
||||
export default {
|
||||
// ...
|
||||
admin: {
|
||||
// ...
|
||||
dependencies: {
|
||||
myTestComponent: { // myTestComponent is the key - can be anything
|
||||
path: '/components/TestComponent.js#TestComponent',
|
||||
type: 'component',
|
||||
clientProps: {
|
||||
test: 'hello',
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This way, `TestComponent` is added to the import map, no matter if it's referenced in a known location or not. On the client, you can then use the component like this:
|
||||
|
||||
```tsx
|
||||
'use client'
|
||||
|
||||
import { RenderComponent, useConfig } from '@payloadcms/ui'
|
||||
import React from 'react'
|
||||
|
||||
export const CustomView = () => {
|
||||
const { config } = useConfig()
|
||||
return (
|
||||
<div>
|
||||
<RenderComponent mappedComponent={config.admin.dependencies?.myTestComponent} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## 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'
|
||||
|
||||
export default buildConfig({
|
||||
// ...
|
||||
admin: {
|
||||
// highlight-start
|
||||
components: {
|
||||
// ...
|
||||
},
|
||||
// highlight-end
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
_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. [More details](#custom-providers). |
|
||||
| **`actions`** | An array of Custom Components to be rendered _within_ the header of the Admin Panel, providing additional interactivity and functionality. |
|
||||
| **`header`** | An array of Custom Components to be injected above the Payload header. |
|
||||
| **`views`** | Override or create new views within the Admin Panel. [More details](./views). |
|
||||
|
||||
<Banner type="success">
|
||||
<strong>Note:</strong>
|
||||
You can also use set [Collection Components](./collections#components) and [Global Components](./globals#components) in their respective configs.
|
||||
</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 so 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'
|
||||
|
||||
export default buildConfig({
|
||||
// ...
|
||||
admin: {
|
||||
components: {
|
||||
providers: ['/path/to/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>
|
||||
|
||||
## 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/overview) 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](../configuration/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 { config: { serverURL } } = useConfig() // highlight-line
|
||||
|
||||
return (
|
||||
<Link href={serverURL}>
|
||||
Go Home
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
<Banner type="success">
|
||||
See [Using Hooks](#using-hooks) for more details.
|
||||
</Banner>
|
||||
|
||||
All [Field Components](./fields) automatically receive their respective Field Config through a common [`field`](./fields#the-field-prop) prop:
|
||||
|
||||
```tsx
|
||||
'use client'
|
||||
import React from 'react'
|
||||
import type { TextFieldClientComponent } from 'payload'
|
||||
|
||||
export const MyClientFieldComponent: TextFieldClientComponent = ({ field: { name } }) => {
|
||||
return (
|
||||
<p>
|
||||
{`This field's name is ${name}`}
|
||||
</p>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### 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,86 +0,0 @@
|
||||
---
|
||||
title: Customizing CSS & SCSS
|
||||
label: Customizing CSS
|
||||
order: 80
|
||||
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 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
|
||||
|
||||
To customize the CSS within the Admin UI, determine scope and change you'd like to make, and then add your own CSS or SCSS to the configuration as needed.
|
||||
|
||||
## Global CSS
|
||||
|
||||
Global CSS refers to the CSS that is applied to the entire [Admin Panel](./overview). This is where you can have a significant impact to the look and feel of the Admin UI through just a few lines of code.
|
||||
|
||||
You can add your own global CSS through the root `custom.scss` file of your app. This file is loaded into the root of the Admin Panel and can be used to inject custom selectors or styles however needed.
|
||||
|
||||
Here is an example of how you might target the Dashboard View and change the background color:
|
||||
|
||||
```scss
|
||||
.dashboard {
|
||||
background-color: red; // highlight-line
|
||||
}
|
||||
```
|
||||
|
||||
<Banner type="warning">
|
||||
<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.
|
||||
|
||||
```scss
|
||||
@import '~@payloadcms/ui/scss';
|
||||
```
|
||||
|
||||
## CSS Library
|
||||
|
||||
To make it as easy as possible for you to override default styles, Payload uses [BEM naming conventions](http://getbem.com/) for all CSS within the Admin UI. If you provide your own CSS, you can override any built-in styles easily, including targeting nested components and their various component states.
|
||||
|
||||
You can also override Payload's built-in [CSS Variables](https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_custom_properties). These variables are widely consumed by the Admin Panel, so modifying them has a significant impact on the look and feel of the Admin UI.
|
||||
|
||||
The following variables are defined and can be overridden:
|
||||
|
||||
- [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/beta/packages/ui/src/scss/app.scss)
|
||||
- Horizontal gutter
|
||||
- Transition speeds
|
||||
- Font sizes
|
||||
- Etc.
|
||||
|
||||
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">
|
||||
<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>
|
||||
|
||||
#### Dark Mode
|
||||
|
||||
Colors are designed to automatically adapt to theme of the [Admin Panel](./overview). By default, Payload automatically overrides all `--theme-elevation` colors and inverts all success / warning / error shades to suit dark mode. We also update some base theme variables like `--theme-bg`, `--theme-text`, etc.
|
||||
@@ -1,572 +0,0 @@
|
||||
---
|
||||
title: Customizing Fields
|
||||
label: Customizing Fields
|
||||
order: 60
|
||||
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. Defaults to `true` for UI fields. |
|
||||
| **`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: '/path/to/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 |
|
||||
| ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **`docPreferences`** | An object that contains the [Preferences](./preferences) for the document.
|
||||
| **`field`** | In Server Components, this is the original Field Config. In Client Components, this is the sanitized Client Field Config. [More details](#the-field-prop). |
|
||||
| **`clientField`** | Server components receive the Client Field Config through this prop. [More details](#the-field-prop). |
|
||||
| **`locale`** | The locale of the field. [More details](../configuration/localization). |
|
||||
| **`readOnly`** | A boolean value that represents if the field is read-only or not. |
|
||||
| **`user`** | The currently authenticated user. [More details](../authentication/overview). |
|
||||
| **`validate`** | A function that can be used to validate the field. |
|
||||
|
||||
<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>
|
||||
|
||||
#### TypeScript
|
||||
|
||||
When building Custom Field Components, you can import the component type to ensure type safety. There is an explicit type for the Field Component, one for every [Field Type](../fields/overview) and for every client/server environment. The convention is to prepend the field type onto the target type, i.e. `TextFieldClientComponent`:
|
||||
|
||||
```tsx
|
||||
import type {
|
||||
TextFieldClientComponent,
|
||||
TextFieldServerComponent,
|
||||
TextFieldClientProps,
|
||||
TextFieldServerProps,
|
||||
// ...and so on for each Field Type
|
||||
} from 'payload'
|
||||
```
|
||||
|
||||
### The `field` Prop
|
||||
|
||||
All Field Components are passed their own Field Config through a common `field` prop. Within Server Components, this is the original Field Config as written within your Payload Config. Within Client Components, however, this is a "Client Config", which is a sanitized, client-friendly version of the Field Config. This is because the original Field Config is [non-serializable](https://react.dev/reference/rsc/use-client#serializable-types), meaning it cannot be passed into Client Components without first being transformed.
|
||||
|
||||
The Client Field Config is an exact copy of the original Field Config, minus all non-serializable properties, plus all evaluated functions such as field labels, [Custom Components](../components), etc.
|
||||
|
||||
Server Component:
|
||||
|
||||
```tsx
|
||||
import React from 'react'
|
||||
import type { TextFieldServerComponent } from 'payload'
|
||||
import { TextField } from '@payloadcms/ui'
|
||||
|
||||
export const MyServerField: TextFieldServerComponent = ({ clientField }) => {
|
||||
return <TextField field={clientField} />
|
||||
}
|
||||
```
|
||||
|
||||
<Banner type="info">
|
||||
<strong>Tip:</strong>
|
||||
Server Components can still access the original Field Config through the `field` prop.
|
||||
</Banner>
|
||||
|
||||
Client Component:
|
||||
|
||||
```tsx
|
||||
'use client'
|
||||
import React from 'react'
|
||||
import type { TextFieldClientComponent } from 'payload'
|
||||
import { TextField } from '@payloadcms/ui'
|
||||
|
||||
export const MyTextField: TextFieldClientComponent = ({ field }) => {
|
||||
return <TextField field={field} />
|
||||
}
|
||||
```
|
||||
|
||||
The following additional properties are also provided to the `field` prop:
|
||||
|
||||
| Property | Description |
|
||||
| ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **`_isPresentational`** | A boolean indicating that the field is purely visual and does not directly affect data or change data shape, i.e. the [UI Field](../fields/ui). |
|
||||
| **`_path`** | A string representing the direct, dynamic path to the field at runtime, i.e. `myGroup.myArray[0].myField`. |
|
||||
| **`_schemaPath`** | A string representing the direct, static path to the [Field Config](../fields/overview), i.e. `myGroup.myArray.myField` |
|
||||
|
||||
<Banner type="info">
|
||||
<strong>Note:</strong>
|
||||
These properties are underscored to denote that they are not part of the original Field Config, and instead are attached during client sanitization to make fields easier to work with on the front-end.
|
||||
</Banner>
|
||||
|
||||
#### TypeScript
|
||||
|
||||
When building Custom Field Components, you can import the client field props to ensure type safety in your component. There is an explicit type for the Field Component, one for every [Field Type](../fields/overview) and server/client environment. The convention is to prepend the field type onto the target type, i.e. `TextFieldClientComponent`:
|
||||
|
||||
```tsx
|
||||
import type {
|
||||
TextFieldClientComponent,
|
||||
TextFieldServerComponent,
|
||||
TextFieldClientProps,
|
||||
TextFieldServerProps,
|
||||
// ...and so on for each Field Type
|
||||
} from 'payload'
|
||||
```
|
||||
|
||||
### 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: '/path/to/MyCustomCellComponent', // 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 |
|
||||
| ---------------- | ----------------------------------------------------------------- |
|
||||
| **`field`** | In Server Components, this is the original Field Config. In Client Components, this is the sanitized Client Field Config. [More details](#the-field-prop). |
|
||||
| **`clientField`** | Server components receive the Client Field Config through this prop. [More details](#the-field-prop). |
|
||||
| **`link`** | A boolean representing whether this cell should be wrapped in a link. |
|
||||
| **`onClick`** | A function that is called when the cell is clicked. |
|
||||
|
||||
<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: '/path/to/MyCustomLabelComponent', // highlight-line
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
_For details on how to build Custom Components, see [Building Custom Components](./components#building-custom-components)._
|
||||
|
||||
Custom Label Components receive all [Field Component](#the-field-component) props, plus the following props:
|
||||
|
||||
| Property | Description |
|
||||
| -------------- | ---------------------------------------------------------------- |
|
||||
| **`field`** | In Server Components, this is the original Field Config. In Client Components, this is the sanitized Client Field Config. [More details](#the-field-prop). |
|
||||
| **`clientField`** | Server components receive the Client Field Config through this prop. [More details](#the-field-prop). |
|
||||
|
||||
<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>
|
||||
|
||||
#### TypeScript
|
||||
|
||||
When building Custom Label Components, you can import the component props to ensure type safety in your component. There is an explicit type for the Label Component, one for every [Field Type](../fields/overview) and server/client environment. The convention is to append `LabelServerComponent` or `LabelClientComponent` to the type of field, i.e. `TextFieldLabelClientComponent`.
|
||||
|
||||
```tsx
|
||||
import type {
|
||||
TextFieldLabelServerComponent,
|
||||
TextFieldLabelClientComponent,
|
||||
// ...and so on for each Field Type
|
||||
} from 'payload'
|
||||
```
|
||||
|
||||
### 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: '/path/to/MyCustomErrorComponent', // highlight-line
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
_For details on how to build Custom Components, see [Building Custom Components](./components#building-custom-components)._
|
||||
|
||||
Custom Error Components receive all [Field Component](#the-field-component) props, plus the following props:
|
||||
|
||||
| Property | Description |
|
||||
| --------------- | ------------------------------------------------------------- |
|
||||
| **`field`** | In Server Components, this is the original Field Config. In Client Components, this is the sanitized Client Field Config. [More details](#the-field-prop). |
|
||||
| **`clientField`** | Server components receive the Client Field Config through this prop. [More details](#the-field-prop). |
|
||||
|
||||
<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>
|
||||
|
||||
#### TypeScript
|
||||
|
||||
When building Custom Error Components, you can import the component props to ensure type safety in your component. There is an explicit type for the Error Component, one for every [Field Type](../fields/overview) and server/client environment. The convention is to append `ErrorServerComponent` or `ErrorClientComponent` to the type of field, i.e. `TextFieldErrorClientComponent`.
|
||||
|
||||
```tsx
|
||||
import type {
|
||||
TextFieldErrorServerComponent,
|
||||
TextFieldErrorClientComponent,
|
||||
// And so on for each Field Type
|
||||
} from 'payload'
|
||||
```
|
||||
|
||||
### 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'
|
||||
|
||||
export const MyCollectionConfig: SanitizedCollectionConfig = {
|
||||
// ...
|
||||
fields: [
|
||||
// ...
|
||||
{
|
||||
name: 'myField',
|
||||
type: 'text',
|
||||
admin: {
|
||||
components: {
|
||||
Description: '/path/to/MyCustomDescriptionComponent', // highlight-line
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
_For details on how to build a Custom Description, see [Building Custom Components](./components#building-custom-components)._
|
||||
|
||||
Custom Description Components receive all [Field Component](#the-field-component) props, plus the following props:
|
||||
|
||||
| Property | Description |
|
||||
| -------------- | ---------------------------------------------------------------- |
|
||||
| **`field`** | In Server Components, this is the original Field Config. In Client Components, this is the sanitized Client Field Config. [More details](#the-field-prop). |
|
||||
| **`clientField`** | Server components receive the Client Field Config through this prop. [More details](#the-field-prop). |
|
||||
|
||||
<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>
|
||||
|
||||
#### TypeScript
|
||||
|
||||
When building Custom Description Components, you can import the component props to ensure type safety in your component. There is an explicit type for the Description Component, one for every [Field Type](../fields/overview) and server/client environment. The convention is to append `DescriptionServerComponent` or `DescriptionClientComponent` to the type of field, i.e. `TextFieldDescriptionClientComponent`.
|
||||
|
||||
```tsx
|
||||
import type {
|
||||
TextFieldDescriptionServerComponent,
|
||||
TextFieldDescriptionClientComponent,
|
||||
// And so on for each Field Type
|
||||
} from 'payload'
|
||||
```
|
||||
|
||||
### 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: ['/path/to/MyCustomComponent'],
|
||||
afterInput: ['/path/to/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
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
```
|
||||
@@ -1,107 +0,0 @@
|
||||
---
|
||||
title: Global Admin Config
|
||||
label: Globals
|
||||
order: 30
|
||||
desc:
|
||||
keywords: admin, components, custom, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
|
||||
---
|
||||
|
||||
The behavior of [Globals](../configuration/globals) within the [Admin Panel](./overview) can be fully customized to fit the needs of your application. This includes grouping or hiding their navigation links, adding [Custom Components](./components), setting page metadata, and more.
|
||||
|
||||
To configure Admin Options for Globals, use the `admin` property in your Global Config:
|
||||
|
||||
```ts
|
||||
import { GlobalConfig } from 'payload'
|
||||
|
||||
export const MyGlobal: GlobalConfig = {
|
||||
// ...
|
||||
admin: { // highlight-line
|
||||
// ...
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## Admin Options
|
||||
|
||||
The following options are available:
|
||||
|
||||
| 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 Global from navigation and admin routing. |
|
||||
| **`components`** | Swap in your own React components to be used within this Global. [More details](#components). |
|
||||
| **`preview`** | Function to generate a preview URL within the Admin Panel for this Global 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). |
|
||||
| **`hideAPIURL`** | Hides the "API URL" meta field while editing documents within this collection. |
|
||||
| **`meta`** | Page metadata overrides to apply to this Global within the Admin Panel. [More details](./metadata). |
|
||||
|
||||
### Components
|
||||
|
||||
Globals can set their own [Custom Components](./components) which only apply to [Global](../configuration/globals)-specific UI within the [Admin Panel](./overview). This includes elements such as the Save Button, or entire layouts such as the Edit View.
|
||||
|
||||
To override Global Components, use the `admin.components` property in your [Global Config](../configuration/globals):
|
||||
|
||||
```ts
|
||||
import type { SanitizedGlobalConfig } from 'payload'
|
||||
|
||||
export const MyGlobal: SanitizedGlobalConfig = {
|
||||
// ...
|
||||
admin: {
|
||||
components: { // highlight-line
|
||||
// ...
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
The following options are available:
|
||||
|
||||
| Path | Description |
|
||||
| ------------------------------ | ---------------------------------------------------------------------------------------------------------------------- |
|
||||
| **`elements.SaveButton`** | Replace the default Save Button with a Custom Component. [Drafts](../versions/drafts) must be disabled. |
|
||||
| **`elements.SaveDraftButton`** | Replace the default Save Draft Button with a Custom Component. [Drafts](../versions/drafts) must be enabled and autosave must be disabled. |
|
||||
| **`elements.PublishButton`** | Replace the default Publish Button with a Custom Component. [Drafts](../versions/drafts) must be enabled. |
|
||||
| **`elements.PreviewButton`** | Replace the default Preview Button with a Custom Component. [Preview](#preview) must be enabled. |
|
||||
| **`views`** | Override or create new views within the Admin Panel. [More details](./views). |
|
||||
|
||||
<Banner type="success">
|
||||
<strong>Note:</strong>
|
||||
For details on how to build Custom Components, see [Building Custom Components](./components#building-custom-components).
|
||||
</Banner>
|
||||
|
||||
### Preview
|
||||
|
||||
It is possible to display a Preview Button within the Edit View of the Admin Panel. 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 configure the Preview Button, set the `admin.preview` property to a function in your Global Config:
|
||||
|
||||
```ts
|
||||
import { GlobalConfig } from 'payload'
|
||||
|
||||
export const MainMenu: GlobalConfig = {
|
||||
// ...
|
||||
admin: {
|
||||
// highlight-start
|
||||
preview: (doc, { locale }) => {
|
||||
if (doc?.slug) {
|
||||
return `/${doc.slug}?locale=${locale}`
|
||||
}
|
||||
|
||||
return null
|
||||
},
|
||||
// highlight-end
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
The preview function receives two arguments:
|
||||
|
||||
| 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">
|
||||
<strong>Note:</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>
|
||||
@@ -1,949 +0,0 @@
|
||||
---
|
||||
title: React Hooks
|
||||
label: React Hooks
|
||||
order: 70
|
||||
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 FieldType<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) => void
|
||||
showError: boolean
|
||||
valid?: boolean
|
||||
value: T
|
||||
}
|
||||
```
|
||||
|
||||
## useFieldProps
|
||||
|
||||
[Custom Field Components](./fields#the-field-component) can be rendered on the server. When using a server component as a custom field component, you can access dynamic props from within any client component rendered by your custom server component. This is done using the `useFieldProps` hook. This is important because 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.
|
||||
|
||||
You can use the `useFieldProps` hooks to access dynamic props like `path`:
|
||||
|
||||
```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 { 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: '/path/to/CustomArrayManagerField',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}`}
|
||||
</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: '/path/to/CustomArrayManagerField',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}`}
|
||||
</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: '/path/to/CustomArrayManagerField',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}`}
|
||||
</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: 90
|
||||
desc: Ensure your documents are locked while being edited, preventing concurrent edits from multiple users and preserving 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 the document for that user. If another user tries to access the same document, they will be notified that it is currently being edited and can choose one of the following options:
|
||||
|
||||
- View in Read-Only Mode: View the document without making any changes.
|
||||
- Take Over Editing: 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"> <strong>Note:</strong> If your application does not require document locking, you can disable this feature for any collection by setting the <code>lockDocuments</code> property to <code>false</code>. </Banner>
|
||||
|
||||
### Config Options
|
||||
|
||||
The lockDocuments property exists on both the Collection Config and the Global Config. By default, document locking is enabled for all collections and globals, but you can customize the lock duration or disable the feature entirely.
|
||||
|
||||
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 API and the REST API, 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">
|
||||
<strong>Reminder:</strong>
|
||||
These are the _root-level_ options for the Admin Panel. You can also customize [Collection Metadata](./collections), [Global Metadata](./globals), and [Document Metadata](./documents) in 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
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
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](./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',
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -1,244 +0,0 @@
|
||||
---
|
||||
title: The Admin Panel
|
||||
label: Overview
|
||||
order: 10
|
||||
desc: Manage your data and customize the Payload Admin Panel by swapping in your own React components. Create, modify or remove views, fields, styles and much more.
|
||||
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, [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](./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).
|
||||
|
||||
<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](./components).
|
||||
</Banner>
|
||||
|
||||
<LightDarkImage
|
||||
srcLight="https://payloadcms.com/images/docs/admin.jpg"
|
||||
srcDark="https://payloadcms.com/images/docs/admin-dark.jpg"
|
||||
alt="Admin Panel with collapsible sidebar"
|
||||
caption="Redesigned Admin Panel with a collapsible sidebar that's open by default, providing greater extensibility and enhanced horizontal real estate."
|
||||
/>
|
||||
|
||||
## Project Structure
|
||||
|
||||
The Admin Panel serves as the entire HTTP layer for Payload, providing a full CRUD interface for your app. This means that both the [REST](../rest-api/overview) and [GraphQL](../graphql/overview) APIs are simply [Next.js Routes](https://nextjs.org/docs/app/building-your-application/routing) that exist directly alongside your front-end application.
|
||||
|
||||
Once you [install Payload](../getting-started/installation), the following files and directories will be created in your app:
|
||||
|
||||
```plaintext
|
||||
app/
|
||||
├─ (payload)/
|
||||
├── admin/
|
||||
├─── [[...segments]]/
|
||||
├──── page.tsx
|
||||
├──── not-found.tsx
|
||||
├── api/
|
||||
├─── [...slug]/
|
||||
├──── route.ts
|
||||
├── graphql/
|
||||
├──── route.ts
|
||||
├── graphql-playground/
|
||||
├──── route.ts
|
||||
├── custom.scss
|
||||
├── layout.tsx
|
||||
```
|
||||
|
||||
<Banner type="info">
|
||||
If you are not familiar with Next.js project structure, you can [learn more about it here](https://nextjs.org/docs/getting-started/project-structure).
|
||||
</Banner>
|
||||
|
||||
As shown above, all Payload routes are nested within the `(payload)` route group. This creates a boundary between the Admin Panel and the rest of your application by scoping all layouts and styles. The `layout.tsx` file within this directory, for example, is where Payload manages the `html` tag of the document to set proper [`lang`](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/lang) and [`dir`](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/dir) attributes, etc.
|
||||
|
||||
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">
|
||||
<strong>Note:</strong>
|
||||
If you don't use the [REST API](../rest/overview) or [GraphQL API](../graphql/overview), you can delete the [Next.js files corresponding to those routes](../admin/overview#project-structure), however, the overhead of this API is completely constrained to these endpoints, and will not slow down or affect Payload outside of the endpoints.
|
||||
</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:
|
||||
|
||||
```tsx
|
||||
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */,
|
||||
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||
```
|
||||
|
||||
## Admin Options
|
||||
|
||||
All options for the Admin Panel are defined in your [Payload Config](../configuration/overview) under the `admin` property:
|
||||
|
||||
```ts
|
||||
import { buildConfig } from 'payload'
|
||||
|
||||
const config = buildConfig({
|
||||
// ...
|
||||
admin: { // highlight-line
|
||||
// ...
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
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](./components). |
|
||||
| **`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. |
|
||||
| **`disable`** | If set to `true`, the entire Admin Panel will be disabled. |
|
||||
| **`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). |
|
||||
| **`theme`** | Restrict the Admin Panel theme to use only one of your choice. Default is `all`.
|
||||
| **`user`** | The `slug` of the Collection that you want to allow to login to the Admin Panel. [More details](#the-admin-user-collection). |
|
||||
|
||||
<Banner type="success">
|
||||
<strong>Reminder:</strong>
|
||||
These are the _root-level_ options for the Admin Panel. You can also customize [Collection Admin Options](./collections) and [Global Admin Options](./globals) through their respective `admin` keys.
|
||||
</Banner>
|
||||
|
||||
### The Admin User Collection
|
||||
|
||||
To specify which Collection to allow to login to the Admin Panel, pass the `admin.user` key equal to the slug of any auth-enabled Collection:
|
||||
|
||||
```ts
|
||||
import { buildConfig } from 'payload'
|
||||
|
||||
const config = buildConfig({
|
||||
// ...
|
||||
admin: {
|
||||
user: 'admins', // highlight-line
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
<Banner type="warning">
|
||||
<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>
|
||||
|
||||
By default, if you have not specified a Collection, Payload will automatically provide a `User` Collection with access to the Admin Panel. You can customize or override the fields and settings of the default `User` Collection by adding your own Collection with `slug: 'users'`. Doing this will force Payload to use your provided `User` Collection instead of its default version.
|
||||
|
||||
You can use whatever Collection you'd like to access the Admin Panel as long as the Collection supports [Authentication](/docs/authentication/overview). It doesn't need to be called `users`. For example, you may wish to have two Collections that both support authentication:
|
||||
|
||||
- `admins` - meant to have a higher level of permissions to manage your data and access the Admin Panel
|
||||
- `customers` - meant for end users of your app that should not be allowed to log into the Admin Panel
|
||||
|
||||
To do this, specify `admin: { user: 'admins' }` in your config. This will provide access to the Admin Panel to only `admins`. Any users authenticated as `customers` will be prevented from accessing the Admin Panel. See [Access Control](/docs/access-control/overview) for full details.
|
||||
|
||||
### 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:
|
||||
|
||||
- `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/payload).
|
||||
|
||||
## Customizing Routes
|
||||
|
||||
You have full control over the routes that Payload binds itself to. This includes both [Root-level Routes](#root-level-routes) such as the [REST API](../rest-api/overview), and [Admin-level Routes](#admin-level-routes) such as the user's account page. You can customize these routes to meet the needs of your application simply by specifying the desired paths in your config.
|
||||
|
||||
### Root-level Routes
|
||||
|
||||
Root-level routes are those that are not behind the `/admin` path, such as the [REST API](../rest-api/overview) and [GraphQL API](../graphql/overview), or the root path of the Admin Panel itself.
|
||||
|
||||
To customize root-level routes, use the `routes` property in your [Payload Config](../configuration/overview):
|
||||
|
||||
```ts
|
||||
import { buildConfig } from 'payload'
|
||||
|
||||
const config = buildConfig({
|
||||
// ...
|
||||
routes: {
|
||||
admin: '/custom-admin-route' // highlight-line
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
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">
|
||||
<strong>Tip:</strong>
|
||||
You can easily add _new_ routes to the Admin Panel through [Custom Endpoints](../rest-api/overview#custom-endpoints) and [Custom Views](./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]]/
|
||||
├──── ...
|
||||
```
|
||||
|
||||
<Banner type="warning">
|
||||
<strong>Note:</strong>
|
||||
If you set Root-level Routes _before_ auto-generating the Admin Panel, your [Project Structure](#project-structure) will already be set up correctly.
|
||||
</Banner>
|
||||
|
||||
### Admin-level Routes
|
||||
|
||||
Admin-level routes are those behind the `/admin` path. These are the routes that are part of the Admin Panel itself, such as the user's account page, the login page, etc.
|
||||
|
||||
To customize admin-level routes, use the `admin.routes` property in your [Payload Config](../configuration/overview):
|
||||
|
||||
```ts
|
||||
import { buildConfig } from 'payload'
|
||||
|
||||
const config = buildConfig({
|
||||
// ...
|
||||
admin: {
|
||||
routes: {
|
||||
account: '/my-account' // highlight-line
|
||||
}
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
The following options are available:
|
||||
|
||||
| Option | Default route | Description |
|
||||
| ----------------- | ----------------------- | ----------------------------------------------- |
|
||||
| `account` | `/account` | The user's account page. |
|
||||
| `createFirstUser` | `/create-first-user` | The page to create the first user. |
|
||||
| `forgot` | `/forgot` | The password reset page. |
|
||||
| `inactivity` | `/logout-inactivity` | The page to redirect to after inactivity. |
|
||||
| `login` | `/login` | The login page. |
|
||||
| `logout` | `/logout` | The logout page. |
|
||||
| `reset` | `/reset` | The password reset page. |
|
||||
| `unauthorized` | `/unauthorized` | The unauthorized page. |
|
||||
|
||||
<Banner type="success">
|
||||
<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/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.
|
||||
@@ -1,161 +0,0 @@
|
||||
---
|
||||
title: Managing User Preferences
|
||||
label: Preferences
|
||||
order: 70
|
||||
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
|
||||
---
|
||||
|
||||
As your users interact with the [Admin Panel](./overview), you might want to store their preferences in a persistent manner, so that when they revisit the Admin Panel in a different session or from a different device, they can pick right back up where they left off.
|
||||
|
||||
Out of the box, Payload handles the persistence of your users' preferences in a handful of ways, including:
|
||||
|
||||
1. Columns in the Collection List View: their active state and order
|
||||
1. The user's last active [Locale](../configuration/localization)
|
||||
1. The "collapsed" state of `blocks`, `array`, and `collapsible` fields
|
||||
1. The last-known state of the `Nav` component, etc.
|
||||
|
||||
<Banner type="warning">
|
||||
<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
|
||||
|
||||
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:
|
||||
|
||||
- If you have built a "color picker", you could "remember" the last used colors that the user has set for easy access next time
|
||||
- If you've built a custom `Nav` component, and you've built in an "accordion-style" UI, you might want to store the `collapsed` state of each Nav collapsible item. This way, if an editor returns to the panel, their `Nav` state is persisted automatically
|
||||
- You might want to store `recentlyAccessed` documents to give admin editors an easy shortcut back to their recently accessed documents on the `Dashboard` or similar
|
||||
- Many other use cases exist. Invent your own! Give your editors an intelligent and persistent editing experience.
|
||||
|
||||
## Database
|
||||
|
||||
Payload automatically creates an internally used `payload-preferences` Collection that stores user preferences. Each document in the `payload-preferences` Collection contains the following shape:
|
||||
|
||||
| Key | Value |
|
||||
| ----------------- | ----------------------------------------------------------------- |
|
||||
| `id` | A unique ID for each preference stored. |
|
||||
| `key` | A unique `key` that corresponds to the preference. |
|
||||
| `user.value` | The ID of the `user` that is storing its preference. |
|
||||
| `user.relationTo` | The `slug` of the Collection that the `user` is logged in as. |
|
||||
| `value` | The value of the preference. Can be any data shape that you need. |
|
||||
| `createdAt` | A timestamp of when the preference was created. |
|
||||
| `updatedAt` | A timestamp set to the last time the preference was updated. |
|
||||
|
||||
## 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
|
||||
|
||||
The Payload Admin Panel offers a `usePreferences` hook. The hook is only meant for use within the Admin Panel itself. It provides you with two methods:
|
||||
|
||||
#### `getPreference`
|
||||
|
||||
This async method provides an easy way to retrieve a user's preferences by `key`. It will return a promise containing the resulting preference value.
|
||||
|
||||
**Arguments**
|
||||
|
||||
- `key`: the `key` of your preference to retrieve.
|
||||
|
||||
#### `setPreference`
|
||||
|
||||
Also async, this method provides you with an easy way to set a user preference. It returns `void`.
|
||||
|
||||
**Arguments:**
|
||||
|
||||
- `key`: the `key` of your preference to set.
|
||||
- `value`: the `value` of your preference that you're looking to set.
|
||||
|
||||
## Example
|
||||
|
||||
Here is an example for how you can utilize `usePreferences` within your custom Admin Panel components. Note - this example is not fully useful and is more just a reference for how to utilize the Preferences API. In this case, we are demonstrating how to set and retrieve a user's last used colors history within a `ColorPicker` or similar type component.
|
||||
|
||||
```tsx
|
||||
'use client'
|
||||
import React, { Fragment, useState, useEffect, useCallback } from 'react';
|
||||
import { usePreferences } from '@payloadcms/ui'
|
||||
|
||||
const lastUsedColorsPreferenceKey = 'last-used-colors';
|
||||
|
||||
const CustomComponent = (props) => {
|
||||
const { getPreference, setPreference } = usePreferences();
|
||||
|
||||
// Store the last used colors in local state
|
||||
const [lastUsedColors, setLastUsedColors] = useState([]);
|
||||
|
||||
// Callback to add a color to the last used colors
|
||||
const updateLastUsedColors = useCallback((color) => {
|
||||
// First, check if color already exists in last used colors.
|
||||
// If it already exists, there is no need to update preferences
|
||||
const colorAlreadyExists = lastUsedColors.indexOf(color) > -1;
|
||||
|
||||
if (!colorAlreadyExists) {
|
||||
const newLastUsedColors = [
|
||||
...lastUsedColors,
|
||||
color,
|
||||
];
|
||||
|
||||
setLastUsedColors(newLastUsedColors);
|
||||
setPreference(lastUsedColorsPreferenceKey, newLastUsedColors);
|
||||
}
|
||||
}, [lastUsedColors, setPreference]);
|
||||
|
||||
// Retrieve preferences on component mount
|
||||
// This will only be run one time, because the `getPreference` method never changes
|
||||
useEffect(() => {
|
||||
const asyncGetPreference = async () => {
|
||||
const lastUsedColorsFromPreferences = await getPreference(lastUsedColorsPreferenceKey);
|
||||
setLastUsedColors(lastUsedColorsFromPreferences);
|
||||
};
|
||||
|
||||
asyncGetPreference();
|
||||
}, [getPreference]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => updateLastUsedColors('red')}
|
||||
>
|
||||
Use red
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => updateLastUsedColors('blue')}
|
||||
>
|
||||
Use blue
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => updateLastUsedColors('purple')}
|
||||
>
|
||||
Use purple
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => updateLastUsedColors('yellow')}
|
||||
>
|
||||
Use yellow
|
||||
</button>
|
||||
{lastUsedColors && (
|
||||
<Fragment>
|
||||
<h5>Last used colors:</h5>
|
||||
<ul>
|
||||
{lastUsedColors?.map((color) => (
|
||||
<li key={color}>
|
||||
{color}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</Fragment>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CustomComponent;
|
||||
```
|
||||
@@ -1,372 +0,0 @@
|
||||
---
|
||||
title: Customizing Views
|
||||
label: Customizing Views
|
||||
order: 50
|
||||
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.
|
||||
|
||||
There are four types of views within the Admin Panel:
|
||||
|
||||
- [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: {
|
||||
customView: {
|
||||
Component: '/path/to/MyCustomView#MyCustomView', // highlight-line
|
||||
path: '/my-custom-view',
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
Your Custom Root Views can optionally use one of the templates that Payload provides. The most common of these is the Default Template which provides the basic layout and navigation. Here is an example of what that might look like:
|
||||
|
||||
```tsx
|
||||
import type { AdminViewProps } from 'payload'
|
||||
|
||||
import { DefaultTemplate } from '@payloadcms/next/templates'
|
||||
import { Gutter } from '@payloadcms/ui'
|
||||
import React from 'react'
|
||||
|
||||
export const MyCustomView: React.FC<AdminViewProps> = ({
|
||||
initPageResult,
|
||||
params,
|
||||
searchParams,
|
||||
}) => {
|
||||
return (
|
||||
<DefaultTemplate
|
||||
i18n={initPageResult.req.i18n}
|
||||
locale={initPageResult.locale}
|
||||
params={params}
|
||||
payload={initPageResult.req.payload}
|
||||
permissions={initPageResult.permissions}
|
||||
searchParams={searchParams}
|
||||
user={initPageResult.req.user || undefined}
|
||||
visibleEntities={initPageResult.visibleEntities}
|
||||
>
|
||||
<Gutter>
|
||||
<h1>Custom Default Root View</h1>
|
||||
<br />
|
||||
<p>This view uses the Default Template.</p>
|
||||
</Gutter>
|
||||
</DefaultTemplate>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
_For details on how to build Custom Views, including all available props, 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 path 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.|
|
||||
| **`meta`** | Page metadata overrides to apply to this view within the Admin Panel. [More details](./metadata). |
|
||||
|
||||
_\* 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: '/path/to/MyCustomView#MyCustomViewComponent',
|
||||
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>
|
||||
|
||||
<Banner type="warning">
|
||||
<strong>Custom views are public</strong>
|
||||
<br />
|
||||
Custom views are public by default. 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>
|
||||
|
||||
## 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: {
|
||||
root: {
|
||||
Component: '/path/to/MyCustomEditView', // highlight-line
|
||||
}
|
||||
// other options include:
|
||||
// default
|
||||
// versions
|
||||
// version
|
||||
// api
|
||||
// livePreview
|
||||
// [key: string]
|
||||
// See "Document Views" for more details
|
||||
},
|
||||
list: {
|
||||
Component: '/path/to/MyCustomListView',
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
_For details on how to build Custom Views, including all available props, see [Building Custom Views](#building-custom-views)._
|
||||
|
||||
<Banner type="warning">
|
||||
<strong>Note:</strong>
|
||||
The `root` 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: {
|
||||
root: {
|
||||
Component: '/path/to/MyCustomEditView', // highlight-line
|
||||
}
|
||||
// other options include:
|
||||
// default
|
||||
// versions
|
||||
// version
|
||||
// api
|
||||
// livePreview
|
||||
// [key: string]
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
_For details on how to build Custom Views, including all available props, see [Building Custom Views](#building-custom-views)._
|
||||
|
||||
<Banner type="warning">
|
||||
<strong>Note:</strong>
|
||||
The `root` 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: '/path/to/MyCustomAPIViewComponent', // highlight-line
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
_For details on how to build Custom Views, including all available props, 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 `root` key. See [Custom Collection Views](#collection-views) or [Custom Global Views](#global-views) for more information.
|
||||
</Banner>
|
||||
|
||||
The following options are available:
|
||||
|
||||
| Property | Description |
|
||||
| ----------------- | --------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **`root`** | The Root View overrides all other nested views and routes. No document controls or tabs are rendered when this key is set. |
|
||||
| **`default`** | The Default View is the primary view in which your document is edited. It is rendered within the "Edit" tab. |
|
||||
| **`versions`** | The Versions View is used to navigate the version history of a single document. It is rendered within the "Versions" tab. [More details](../versions). |
|
||||
| **`version`** | The Version View is used to edit a single version of a document. It is rendered within the "Version" tab. [More details](../versions). |
|
||||
| **`api`** | The API View is used to display the REST API JSON response for a given document. It is rendered within the "API" tab. |
|
||||
| **`livePreview`** | The LivePreview view is used to display the Live Preview interface. It is rendered within the "Live Preview" tab. [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 `tab` key:
|
||||
|
||||
```ts
|
||||
import type { SanitizedCollectionConfig } from 'payload'
|
||||
|
||||
export const MyCollection: SanitizedCollectionConfig = {
|
||||
slug: 'my-collection',
|
||||
admin: {
|
||||
components: {
|
||||
views: {
|
||||
edit: {
|
||||
myCustomTab: {
|
||||
Component: '/path/to/MyCustomTab',
|
||||
path: '/my-custom-tab',
|
||||
tab: {
|
||||
Component: '/path/to/MyCustomTabComponent' // highlight-line
|
||||
}
|
||||
},
|
||||
anotherCustomTab: {
|
||||
Component: '/path/to/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'
|
||||
|
||||
export const MyCollectionConfig: SanitizedCollectionConfig = {
|
||||
// ...
|
||||
admin: {
|
||||
components: {
|
||||
views: {
|
||||
edit: {
|
||||
Component: '/path/to/MyCustomView' // highlight-line
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Your Custom Views will be provided with the following props:
|
||||
|
||||
| Prop | Description |
|
||||
| ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **`initPageResult`** | An object containing `req`, `payload`, `permissions`, etc. |
|
||||
| **`clientConfig`** | The Client Config object. [More details](../components#accessing-the-payload-config). |
|
||||
| **`importMap`** | The import map object. |
|
||||
| **`params`** | An object containing the [Dynamic Route Parameters](https://nextjs.org/docs/app/building-your-application/routing/dynamic-routes). |
|
||||
| **`searchParams`** | An object containing the [Search Parameters](https://developer.mozilla.org/docs/Learn/Common_questions/What_is_a_URL#parameters). |
|
||||
|
||||
<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>
|
||||
@@ -1,81 +0,0 @@
|
||||
---
|
||||
title: API Key Strategy
|
||||
label: API Key Strategy
|
||||
order: 50
|
||||
desc: Enable API key based authentication to interface with Payload.
|
||||
keywords: authentication, config, configuration, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
|
||||
---
|
||||
|
||||
To integrate with third-party APIs or services, you might need the ability to generate API keys that can be used to identify as a certain user within Payload. API keys are generated on a user-by-user basis, similar to email and passwords, and are meant to represent a single user.
|
||||
|
||||
For example, if you have a third-party service or external app that needs to be able to perform protected actions against Payload, first you need to create a user within Payload, i.e. `dev@thirdparty.com`. From your external application you will need to authenticate with that user, you have two options:
|
||||
|
||||
1. Log in each time with that user and receive an expiring token to request with.
|
||||
1. Generate a non-expiring API key for that user to request with.
|
||||
|
||||
<Banner type="success">
|
||||
<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>
|
||||
|
||||
Technically, both of these options will work for third-party integrations but the second option with API key is simpler, because it reduces the amount of work that your integrations need to do to be authenticated properly.
|
||||
|
||||
To enable API keys on a collection, set the `useAPIKey` auth option to `true`. From there, a new interface will appear in the [Admin Panel](../admin/overview) for each document within the collection that allows you to generate an API key for each user in the Collection.
|
||||
|
||||
```ts
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
export const ThirdPartyAccess: CollectionConfig = {
|
||||
slug: 'third-party-access',
|
||||
auth: {
|
||||
useAPIKey: true, // highlight-line
|
||||
},
|
||||
fields: [],
|
||||
}
|
||||
```
|
||||
|
||||
User API keys are encrypted within the database, meaning that if your database is compromised,
|
||||
your API keys will not be.
|
||||
|
||||
<Banner type="warning">
|
||||
<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.
|
||||
|
||||
**For example, using Fetch:**
|
||||
|
||||
```ts
|
||||
import Users from '../collections/Users'
|
||||
|
||||
const response = await fetch('http://localhost:3000/api/pages', {
|
||||
headers: {
|
||||
Authorization: `${Users.slug} API-Key ${YOUR_API_KEY}`,
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
### API Key Only Auth
|
||||
|
||||
If you want to use API keys as the only authentication method for a collection, you can disable the default local strategy by setting `disableLocalStrategy` to `true` on the collection's `auth` property. This will disable the ability to authenticate with email and password, and will only allow for authentication via API key.
|
||||
|
||||
```ts
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
export const ThirdPartyAccess: CollectionConfig = {
|
||||
slug: 'third-party-access',
|
||||
auth: {
|
||||
useAPIKey: true,
|
||||
disableLocalStrategy: true, // highlight-line
|
||||
},
|
||||
}
|
||||
```
|
||||
@@ -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">
|
||||
<strong>Tip:</strong>
|
||||
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">
|
||||
<strong>Tip:</strong>
|
||||
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
|
||||
```
|
||||
|
||||
#### 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">
|
||||
<strong>Good to know:</strong>
|
||||
Setting up <code>secure: true</code> will not work if you're developing on <code>http://localhost</code> or any non-https domain. For local development you should conditionally set this to <code>false</code> based on the environment.
|
||||
</Banner>
|
||||
@@ -1,94 +0,0 @@
|
||||
---
|
||||
title: Custom Strategies
|
||||
label: Custom Strategies
|
||||
order: 60
|
||||
desc: Create custom authentication strategies to handle everything auth in Payload.
|
||||
keywords: authentication, config, configuration, overview, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
|
||||
---
|
||||
|
||||
<Banner type="warning">
|
||||
This is an advanced feature, so only attempt this if you are an experienced developer. Otherwise,
|
||||
just let Payload's built-in authentication handle user auth for you.
|
||||
</Banner>
|
||||
|
||||
### 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.
|
||||
|
||||
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. |
|
||||
|
||||
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. |
|
||||
| **`isGraphQL`** | Whether or not the request was made from a GraphQL endpoint. Default is `false`. |
|
||||
|
||||
|
||||
### Example Strategy
|
||||
|
||||
At its core a strategy simply takes information from the incoming request and returns a user. This is exactly how Payload's built-in strategies function.
|
||||
|
||||
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'
|
||||
|
||||
export const Users: CollectionConfig = {
|
||||
slug: 'users',
|
||||
auth: {
|
||||
disableLocalStrategy: true,
|
||||
// highlight-start
|
||||
strategies: [
|
||||
{
|
||||
name: 'custom-strategy',
|
||||
authenticate: ({ payload, headers }) => {
|
||||
const usersQuery = await payload.find({
|
||||
collection: 'users',
|
||||
where: {
|
||||
code: {
|
||||
equals: headers.get('code'),
|
||||
},
|
||||
secret: {
|
||||
equals: headers.get('secret'),
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
return {
|
||||
// Send the user back to authenticate,
|
||||
// or send null if no user should be authenticated
|
||||
user: usersQuery.docs[0] || null,
|
||||
|
||||
// Optionally, you can return headers
|
||||
// that you'd like Payload to set here when
|
||||
// it returns the response
|
||||
responseHeaders: new Headers({
|
||||
'some-header': 'my header value'
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
// highlight-end
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'code',
|
||||
type: 'text',
|
||||
index: true,
|
||||
unique: true,
|
||||
},
|
||||
{
|
||||
name: 'secret',
|
||||
type: 'text',
|
||||
},
|
||||
]
|
||||
}
|
||||
```
|
||||
@@ -1,203 +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) 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">
|
||||
<strong>Tip:</strong>
|
||||
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">
|
||||
<strong>Important:</strong>
|
||||
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 |
|
||||
|----------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| **`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">
|
||||
<strong>Important:</strong>
|
||||
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">
|
||||
<strong>Tip:</strong>
|
||||
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. |
|
||||
@@ -1,51 +0,0 @@
|
||||
---
|
||||
title: JWT Strategy
|
||||
label: JWT Strategy
|
||||
order: 40
|
||||
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.
|
||||
|
||||
<Banner type="success">
|
||||
<strong>Tip:</strong>
|
||||
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>
|
||||
|
||||
### Identifying Users Via The Authorization Header
|
||||
|
||||
In addition to authenticating via an HTTP-only cookie, you can also identify users via the `Authorization` header on an HTTP request.
|
||||
|
||||
Example:
|
||||
|
||||
```ts
|
||||
const user = await fetch('http://localhost:3000/api/users/login', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
email: 'dev@payloadcms.com',
|
||||
password: 'password',
|
||||
})
|
||||
}).then(req => await req.json())
|
||||
|
||||
const request = await fetch('http://localhost:3000', {
|
||||
headers: {
|
||||
Authorization: `JWT ${user.token}`,
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
### Omitting The Token
|
||||
|
||||
In some cases you may want to prevent the token from being returned from the auth operations. You can do that by setting `removeTokenFromResponse` to `true` like so:
|
||||
|
||||
```ts
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
export const UsersWithoutJWTs: CollectionConfig = {
|
||||
slug: 'users-without-jwts',
|
||||
auth: {
|
||||
removeTokenFromRepsonse: true, // highlight-line
|
||||
},
|
||||
}
|
||||
```
|
||||
@@ -1,398 +0,0 @@
|
||||
---
|
||||
title: Authentication Operations
|
||||
label: Operations
|
||||
order: 20
|
||||
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).
|
||||
|
||||
## 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.
|
||||
|
||||
**REST API endpoint**:
|
||||
|
||||
`GET http://localhost:3000/api/access`
|
||||
|
||||
Example response:
|
||||
|
||||
```ts
|
||||
{
|
||||
canAccessAdmin: true,
|
||||
collections: {
|
||||
pages: {
|
||||
create: {
|
||||
permission: true,
|
||||
},
|
||||
read: {
|
||||
permission: true,
|
||||
},
|
||||
update: {
|
||||
permission: true,
|
||||
},
|
||||
delete: {
|
||||
permission: true,
|
||||
},
|
||||
fields: {
|
||||
title: {
|
||||
create: {
|
||||
permission: true,
|
||||
},
|
||||
read: {
|
||||
permission: true,
|
||||
},
|
||||
update: {
|
||||
permission: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Example GraphQL Query**:
|
||||
|
||||
```graphql
|
||||
query {
|
||||
Access {
|
||||
pages {
|
||||
read {
|
||||
permission
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Document access can also be queried on a collection/global basis. Access on a global can queried like `http://localhost:3000/api/global-slug/access`, Collection document access can be queried like `http://localhost:3000/api/collection-slug/access/:id`.
|
||||
|
||||
## Me
|
||||
|
||||
Returns either a logged in user with token or null when there is no logged in user.
|
||||
|
||||
**REST API endpoint**:
|
||||
|
||||
`GET http://localhost:3000/api/[collection-slug]/me`
|
||||
|
||||
Example response:
|
||||
|
||||
```ts
|
||||
{
|
||||
user: { // The JWT "payload" ;) from the logged in user
|
||||
email: 'dev@payloadcms.com',
|
||||
createdAt: "2020-12-27T21:16:45.645Z",
|
||||
updatedAt: "2021-01-02T18:37:41.588Z",
|
||||
id: "5ae8f9bde69e394e717c8832"
|
||||
},
|
||||
token: '34o4345324...', // The token that can be used to authenticate the user
|
||||
exp: 1609619861, // Unix timestamp representing when the user's token will expire
|
||||
}
|
||||
```
|
||||
|
||||
**Example GraphQL Query**:
|
||||
|
||||
```graphql
|
||||
query {
|
||||
me[collection-singular-label] {
|
||||
user {
|
||||
email
|
||||
}
|
||||
exp
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Login
|
||||
|
||||
Accepts an `email` and `password`. On success, it will return the logged in user as well as a token that can be used to authenticate. In the GraphQL and REST APIs, this operation also automatically sets an HTTP-only cookie including the user's token. If you pass a `res` to the Local API operation, Payload will set a cookie there as well.
|
||||
|
||||
**Example REST API login**:
|
||||
|
||||
```ts
|
||||
const res = await fetch('http://localhost:3000/api/[collection-slug]/login', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email: 'dev@payloadcms.com',
|
||||
password: 'this-is-not-our-password...or-is-it?',
|
||||
}),
|
||||
})
|
||||
|
||||
const json = await res.json()
|
||||
|
||||
// JSON will be equal to the following:
|
||||
/*
|
||||
{
|
||||
user: {
|
||||
email: 'dev@payloadcms.com',
|
||||
createdAt: "2020-12-27T21:16:45.645Z",
|
||||
updatedAt: "2021-01-02T18:37:41.588Z",
|
||||
id: "5ae8f9bde69e394e717c8832"
|
||||
},
|
||||
token: '34o4345324...',
|
||||
exp: 1609619861
|
||||
}
|
||||
*/
|
||||
```
|
||||
|
||||
**Example GraphQL Mutation**:
|
||||
|
||||
```graphql
|
||||
mutation {
|
||||
login[collection-singular-label](email: "dev@payloadcms.com", password: "yikes") {
|
||||
user {
|
||||
email
|
||||
}
|
||||
exp
|
||||
token
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Example Local API login**:
|
||||
|
||||
```ts
|
||||
const result = await payload.login({
|
||||
collection: '[collection-slug]',
|
||||
data: {
|
||||
email: 'dev@payloadcms.com',
|
||||
password: 'get-out',
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
## Logout
|
||||
|
||||
As Payload sets HTTP-only cookies, logging out cannot be done by just removing a cookie in JavaScript, as HTTP-only cookies are inaccessible by JS within the browser. So, Payload exposes a `logout` operation to delete the token in a safe way.
|
||||
|
||||
**Example REST API logout**:
|
||||
|
||||
```ts
|
||||
const res = await fetch('http://localhost:3000/api/[collection-slug]/logout', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
**Example GraphQL Mutation**:
|
||||
|
||||
```
|
||||
mutation {
|
||||
logout[collection-singular-label]
|
||||
}
|
||||
```
|
||||
|
||||
## Refresh
|
||||
|
||||
Allows for "refreshing" JWTs. If your user has a token that is about to expire, but the user is still active and using the app, you might want to use the `refresh` operation to receive a new token by executing this operation via the authenticated user.
|
||||
|
||||
This operation requires a non-expired token to send back a new one. If the user's token has already expired, you will need to allow them to log in again to retrieve a new token.
|
||||
|
||||
If successful, this operation will automatically renew the user's HTTP-only cookie and will send back the updated token in JSON.
|
||||
|
||||
**Example REST API token refresh**:
|
||||
|
||||
```ts
|
||||
const res = await fetch('http://localhost:3000/api/[collection-slug]/refresh-token', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
|
||||
const json = await res.json()
|
||||
|
||||
// JSON will be equal to the following:
|
||||
/*
|
||||
{
|
||||
user: {
|
||||
email: 'dev@payloadcms.com',
|
||||
createdAt: "2020-12-27T21:16:45.645Z",
|
||||
updatedAt: "2021-01-02T18:37:41.588Z",
|
||||
id: "5ae8f9bde69e394e717c8832"
|
||||
},
|
||||
refreshedToken: '34o4345324...',
|
||||
exp: 1609619861
|
||||
}
|
||||
*/
|
||||
```
|
||||
|
||||
**Example GraphQL Mutation**:
|
||||
|
||||
```
|
||||
mutation {
|
||||
refreshToken[collection-singular-label] {
|
||||
user {
|
||||
email
|
||||
}
|
||||
refreshedToken
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Verify by Email
|
||||
|
||||
If your collection supports email verification, the Verify operation will be exposed which accepts a verification token and sets the user's `_verified` property to `true`, thereby allowing the user to authenticate with the Payload API.
|
||||
|
||||
**Example REST API user verification**:
|
||||
|
||||
```ts
|
||||
const res = await fetch(`http://localhost:3000/api/[collection-slug]/verify/${TOKEN_HERE}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
**Example GraphQL Mutation**:
|
||||
|
||||
```graphql
|
||||
mutation {
|
||||
verifyEmail[collection-singular-label](token: "TOKEN_HERE")
|
||||
}
|
||||
```
|
||||
|
||||
**Example Local API verification**:
|
||||
|
||||
```ts
|
||||
const result = await payload.verifyEmail({
|
||||
collection: '[collection-slug]',
|
||||
token: 'TOKEN_HERE',
|
||||
})
|
||||
```
|
||||
|
||||
## 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/overview#unlock) access control function.
|
||||
|
||||
**Example REST API unlock**:
|
||||
|
||||
```ts
|
||||
const res = await fetch(`http://localhost:3000/api/[collection-slug]/unlock`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
**Example GraphQL Mutation**:
|
||||
|
||||
```
|
||||
mutation {
|
||||
unlock[collection-singular-label]
|
||||
}
|
||||
```
|
||||
|
||||
**Example Local API unlock**:
|
||||
|
||||
```ts
|
||||
const result = await payload.unlock({
|
||||
collection: '[collection-slug]',
|
||||
})
|
||||
```
|
||||
|
||||
## Forgot Password
|
||||
|
||||
Payload comes with built-in forgot password functionality. Submitting an email address to the Forgot Password operation will generate an email and send it to the respective email address with a link to reset their password.
|
||||
|
||||
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/overview#forgot-password).
|
||||
|
||||
**Example REST API Forgot Password**:
|
||||
|
||||
```ts
|
||||
const res = await fetch(`http://localhost:3000/api/[collection-slug]/forgot-password`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email: 'dev@payloadcms.com',
|
||||
}),
|
||||
})
|
||||
```
|
||||
|
||||
**Example GraphQL Mutation**:
|
||||
|
||||
```
|
||||
mutation {
|
||||
forgotPassword[collection-singular-label](email: "dev@payloadcms.com")
|
||||
}
|
||||
```
|
||||
|
||||
**Example Local API forgot password**:
|
||||
|
||||
```ts
|
||||
const token = await payload.forgotPassword({
|
||||
collection: '[collection-slug]',
|
||||
data: {
|
||||
email: 'dev@payloadcms.com',
|
||||
},
|
||||
disableEmail: false, // you can disable the auto-generation of email via local API
|
||||
})
|
||||
```
|
||||
|
||||
<Banner type="success">
|
||||
<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,
|
||||
allowing a user to "complete" their account by setting their password. In the background, you'd
|
||||
use the token to "reset" their password.
|
||||
</Banner>
|
||||
|
||||
## Reset Password
|
||||
|
||||
After a user has "forgotten" their password and a token is generated, that token can be used to send to the reset password operation along with a new password which will allow the user to reset their password securely.
|
||||
|
||||
**Example REST API Reset Password**:
|
||||
|
||||
```ts
|
||||
const res = await fetch(`http://localhost:3000/api/[collection-slug]/reset-password`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
token: 'TOKEN_GOES_HERE'
|
||||
password: 'not-today',
|
||||
}),
|
||||
});
|
||||
|
||||
const json = await res.json();
|
||||
|
||||
// JSON will be equal to the following:
|
||||
/*
|
||||
{
|
||||
user: {
|
||||
email: 'dev@payloadcms.com',
|
||||
createdAt: "2020-12-27T21:16:45.645Z",
|
||||
updatedAt: "2021-01-02T18:37:41.588Z",
|
||||
id: "5ae8f9bde69e394e717c8832"
|
||||
},
|
||||
token: '34o4345324...',
|
||||
exp: 1609619861
|
||||
}
|
||||
*/
|
||||
```
|
||||
|
||||
**Example GraphQL Mutation**:
|
||||
|
||||
```graphql
|
||||
mutation {
|
||||
resetPassword[collection-singular-label](token: "TOKEN_GOES_HERE", password: "not-today")
|
||||
}
|
||||
```
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user