Compare commits
115 Commits
payload/2.
...
plugin-clo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0a56d50334 | ||
|
|
02999a5659 | ||
|
|
365127bee4 | ||
|
|
b67e97aa7f | ||
|
|
61e8ce1743 | ||
|
|
034aa68cd4 | ||
|
|
268e6c485e | ||
|
|
4c1a5dca44 | ||
|
|
a12d1f4755 | ||
|
|
d55be73992 | ||
|
|
b9f236ae50 | ||
|
|
1d38e6d5d5 | ||
|
|
2f3c994cea | ||
|
|
0586f236bb | ||
|
|
d582619ead | ||
|
|
17fc2d13d0 | ||
|
|
800ffd2611 | ||
|
|
661ca74364 | ||
|
|
ec73b461a8 | ||
|
|
94885f3c65 | ||
|
|
31d0b309fe | ||
|
|
c86526b5c8 | ||
|
|
28a065072f | ||
|
|
efc0bc9ec9 | ||
|
|
ade1d27c95 | ||
|
|
1040731e32 | ||
|
|
30f28898b6 | ||
|
|
6cb0470906 | ||
|
|
170ea5badc | ||
|
|
cfb56589eb | ||
|
|
f312bac065 | ||
|
|
3dd3f5b135 | ||
|
|
59f4d125ab | ||
|
|
b2b2ee3338 | ||
|
|
7308abaabd | ||
|
|
9b1d0b2d0f | ||
|
|
9014f1fa63 | ||
|
|
ba75d876e3 | ||
|
|
f2b2e5cda9 | ||
|
|
f751f69239 | ||
|
|
f7ac9ff52a | ||
|
|
ba7a043a99 | ||
|
|
b149180db4 | ||
|
|
4efb9dd867 | ||
|
|
7002ca78b9 | ||
|
|
44ca3a4073 | ||
|
|
dc7c952ace | ||
|
|
c8a659cd39 | ||
|
|
6ba293c0f8 | ||
|
|
96a624ad5c | ||
|
|
545949dafc | ||
|
|
d9f61bbdc8 | ||
|
|
be06579b3e | ||
|
|
25e9bc62db | ||
|
|
aca567634b | ||
|
|
1f0934877c | ||
|
|
61da010991 | ||
|
|
ab9074220a | ||
|
|
afa90a4362 | ||
|
|
bc0516da90 | ||
|
|
46daf473c8 | ||
|
|
337b8ccbf3 | ||
|
|
ba2e4c278f | ||
|
|
3196036ae9 | ||
|
|
9bc3ad5159 | ||
|
|
94d18e8d74 | ||
|
|
c624eea0d8 | ||
|
|
f97627092c | ||
|
|
f00183029e | ||
|
|
b6c5aaa966 | ||
|
|
517aaa0665 | ||
|
|
2c2ffe406f | ||
|
|
7f39afa192 | ||
|
|
fc4d24aa88 | ||
|
|
efa56cefc1 | ||
|
|
907d7d1d3a | ||
|
|
eca1517237 | ||
|
|
9865ae998b | ||
|
|
1a0ef4824b | ||
|
|
39e110e633 | ||
|
|
3e780b9815 | ||
|
|
a308d6384f | ||
|
|
492ed30cb8 | ||
|
|
fca5a404db | ||
|
|
b13f7e8843 | ||
|
|
25dfdb66cd | ||
|
|
9c9e6896a5 | ||
|
|
a3085435ef | ||
|
|
1466657e8f | ||
|
|
1348483648 | ||
|
|
5321098d7e | ||
|
|
c57591bc4f | ||
|
|
9750bc217e | ||
|
|
468e5441f1 | ||
|
|
3c5cce4c6f | ||
|
|
9f0f94893d | ||
|
|
03b7892fc9 | ||
|
|
f96cf593ce | ||
|
|
8259611ce6 | ||
|
|
a3ed25a253 | ||
|
|
69e7b7a158 | ||
|
|
c6da99b4d1 | ||
|
|
ebd23caa56 | ||
|
|
faa9b21824 | ||
|
|
1690560f11 | ||
|
|
0058660b3f | ||
|
|
6d7ef919cb | ||
|
|
abffa37d85 | ||
|
|
1b208c7add | ||
|
|
2840632161 | ||
|
|
0841d5a35e | ||
|
|
bd19fcf259 | ||
|
|
18645771c8 | ||
|
|
20377bb22c | ||
|
|
7daaf3d780 |
31
.github/ISSUE_TEMPLATE/1.bug_report_v3.yml
vendored
31
.github/ISSUE_TEMPLATE/1.bug_report_v3.yml
vendored
@@ -9,43 +9,38 @@ body:
|
||||
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: input
|
||||
id: version
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Payload Version
|
||||
description: What version of Payload are you running?
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: node-version
|
||||
attributes:
|
||||
label: Node Version
|
||||
description: What version of Node are you running?
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: nextjs-version
|
||||
attributes:
|
||||
label: Next.js Version
|
||||
description: What version of Next.js are you running?
|
||||
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: textarea
|
||||
attributes:
|
||||
label: Describe the Bug
|
||||
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: input
|
||||
id: adapters-plugins
|
||||
attributes:
|
||||
label: Adapters and Plugins
|
||||
description: What adapters and plugins are you using if relevant? ie. db-mongodb, db-postgres, storage-vercel-blob, 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.
|
||||
|
||||
25
.github/PULL_REQUEST_TEMPLATE.md
vendored
25
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -1,23 +1,10 @@
|
||||
## Description
|
||||
<!--
|
||||
|
||||
<!-- Please include a summary of the pull request and any related issues it fixes. Please also include relevant motivation and context. -->
|
||||
For external contributors, please include:
|
||||
|
||||
- [ ] I have read and understand the [CONTRIBUTING.md](https://github.com/payloadcms/payload/blob/main/CONTRIBUTING.md) document in this repository.
|
||||
- A summary of the pull request and any related issues it fixes.
|
||||
- Reasoning for the changes made or any additional context that may be useful.
|
||||
|
||||
## Type of change
|
||||
Ensure you have read and understand the [CONTRIBUTING.md](https://github.com/payloadcms/payload/blob/main/CONTRIBUTING.md) document in this repository.
|
||||
|
||||
<!-- Please delete options that are not relevant. -->
|
||||
|
||||
- [ ] Chore (non-breaking change which does not add functionality)
|
||||
- [ ] Bug fix (non-breaking change which fixes an issue)
|
||||
- [ ] New feature (non-breaking change which adds functionality)
|
||||
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
|
||||
- [ ] Change to the [templates](https://github.com/payloadcms/payload/tree/main/templates) directory (does not affect core functionality)
|
||||
- [ ] Change to the [examples](https://github.com/payloadcms/payload/tree/main/examples) directory (does not affect core functionality)
|
||||
- [ ] This change requires a documentation update
|
||||
|
||||
## Checklist:
|
||||
|
||||
- [ ] I have added tests that prove my fix is effective or that my feature works
|
||||
- [ ] Existing test suite passes locally with my changes
|
||||
- [ ] I have made corresponding changes to the documentation
|
||||
-->
|
||||
|
||||
13
.github/actions/release-commenter/.eslintrc.js
vendored
Normal file
13
.github/actions/release-commenter/.eslintrc.js
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
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'],
|
||||
}
|
||||
8
.github/actions/release-commenter/.prettierrc.js
vendored
Normal file
8
.github/actions/release-commenter/.prettierrc.js
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
module.exports = {
|
||||
printWidth: 100,
|
||||
parser: 'typescript',
|
||||
semi: false,
|
||||
singleQuote: true,
|
||||
trailingComma: 'all',
|
||||
arrowParens: 'avoid',
|
||||
}
|
||||
74
.github/actions/release-commenter/README.md
vendored
Normal file
74
.github/actions/release-commenter/README.md
vendored
Normal file
@@ -0,0 +1,74 @@
|
||||
# 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
Normal file
32
.github/actions/release-commenter/action.yml
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
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
Normal file
34199
.github/actions/release-commenter/dist/index.js
vendored
Normal file
File diff suppressed because one or more lines are too long
7
.github/actions/release-commenter/jest.config.js
vendored
Normal file
7
.github/actions/release-commenter/jest.config.js
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
testEnvironment: 'node',
|
||||
testPathIgnorePatterns: ['/node_modules/', '<rootDir>/dist/'],
|
||||
transform: {
|
||||
'^.+\\.(t|j)sx?$': ['@swc/jest'],
|
||||
},
|
||||
}
|
||||
34
.github/actions/release-commenter/package.json
vendored
Normal file
34
.github/actions/release-commenter/package.json
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"name": "release-commenter",
|
||||
"version": "0.0.0",
|
||||
"description": "GitHub Action to automatically comment on PRs and Issues when a fix is released.",
|
||||
"main": "dist/index.js",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"clean": "rimraf dist",
|
||||
"build": "pnpm build:typecheck && pnpm build:ncc",
|
||||
"build:ncc": "ncc build src/index.ts -t -o dist",
|
||||
"build:typecheck": "tsc",
|
||||
"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
Normal file
5419
.github/actions/release-commenter/pnpm-lock.yaml
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
266
.github/actions/release-commenter/src/__snapshots__/index.test.ts.snap
vendored
Normal file
266
.github/actions/release-commenter/src/__snapshots__/index.test.ts.snap
vendored
Normal file
@@ -0,0 +1,266 @@
|
||||
// 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
Normal file
399
.github/actions/release-commenter/src/index.test.ts
vendored
Normal file
@@ -0,0 +1,399 @@
|
||||
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
Normal file
349
.github/actions/release-commenter/src/index.ts
vendored
Normal file
@@ -0,0 +1,349 @@
|
||||
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
Normal file
15
.github/actions/release-commenter/tsconfig.json
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"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"]
|
||||
}
|
||||
74
.github/dependabot.yml
vendored
Normal file
74
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,74 @@
|
||||
# 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*'
|
||||
3977
.github/pnpm-lock.yaml
generated
vendored
Normal file
3977
.github/pnpm-lock.yaml
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
2
.github/pnpm-workspace.yaml
vendored
Normal file
2
.github/pnpm-workspace.yaml
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
packages:
|
||||
- 'actions/*'
|
||||
26
.github/workflows/lock-issues.yml
vendored
Normal file
26
.github/workflows/lock-issues.yml
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
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.
|
||||
14
.github/workflows/main.yml
vendored
14
.github/workflows/main.yml
vendored
@@ -27,7 +27,7 @@ jobs:
|
||||
with:
|
||||
filters: |
|
||||
needs_build:
|
||||
- '.github/workflows/**'
|
||||
- '.github/workflows/main.yml'
|
||||
- 'packages/**'
|
||||
- 'test/**'
|
||||
- 'pnpm-lock.yaml'
|
||||
@@ -61,7 +61,7 @@ jobs:
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: 8
|
||||
version: 9.7.0
|
||||
run_install: false
|
||||
|
||||
- name: Get pnpm store directory
|
||||
@@ -116,7 +116,7 @@ jobs:
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: 8
|
||||
version: 9.7.0
|
||||
run_install: false
|
||||
|
||||
- name: Restore build
|
||||
@@ -201,7 +201,7 @@ jobs:
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: 8
|
||||
version: 9.7.0
|
||||
run_install: false
|
||||
|
||||
- name: Restore build
|
||||
@@ -242,7 +242,7 @@ jobs:
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: 8
|
||||
version: 9.7.0
|
||||
run_install: false
|
||||
|
||||
- name: Restore build
|
||||
@@ -286,7 +286,7 @@ jobs:
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: 8
|
||||
version: 9.7.0
|
||||
run_install: false
|
||||
|
||||
- name: Restore build
|
||||
@@ -327,7 +327,7 @@ jobs:
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: 8
|
||||
version: 9.7.0
|
||||
run_install: false
|
||||
|
||||
- name: Restore build
|
||||
|
||||
32
.github/workflows/post-release.yml
vendored
Normal file
32
.github/workflows/post-release.yml
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
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 }}
|
||||
- run: echo "npm_version=$(npm pkg get version | tr -d '"')" >> "$GITHUB_ENV"
|
||||
- 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}
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -4,6 +4,9 @@ dist
|
||||
/.idea/*
|
||||
!/.idea/runConfigurations
|
||||
|
||||
# Custom actions
|
||||
!.github/actions/**/dist
|
||||
|
||||
test-results
|
||||
.devcontainer
|
||||
.localstack
|
||||
@@ -134,7 +137,6 @@ out
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Gatsby files
|
||||
.cache/
|
||||
|
||||
117
CHANGELOG.md
117
CHANGELOG.md
@@ -1,3 +1,120 @@
|
||||
## [2.30.1](https://github.com/payloadcms/payload/compare/v2.30.0...v2.30.1) (2024-10-02)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **db-mongodb:** properly filters out `number` field values with the `exists` operator filter ([#8415](https://github.com/payloadcms/payload/issues/8415)) ([0586f23](https://github.com/payloadcms/payload/commit/0586f236bbf04163a0d9b226772849cb3d977864)), closes [#8181](https://github.com/payloadcms/payload/issues/8181)
|
||||
* sorting by id incorrectly orders by version.id ([#8450](https://github.com/payloadcms/payload/issues/8450)) ([1d38e6d](https://github.com/payloadcms/payload/commit/1d38e6d5d5b56a91aa8f59a461d40f28b1750f8c))
|
||||
|
||||
## [2.30.0](https://github.com/payloadcms/payload/compare/v2.29.0...v2.30.0) (2024-09-27)
|
||||
|
||||
* export toast from react toastify in payload ([#8438](https://github.com/payloadcms/payload/issues/8438)) ([17fc2d1](https://github.com/payloadcms/payload/commit/17fc2d13d06b6de01f839c27fd706bc0d6a185eb))
|
||||
|
||||
## [2.29.0](https://github.com/payloadcms/payload/compare/v2.28.0...v2.29.0) (2024-09-25)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add new option to disable JOI validation ([#8067](https://github.com/payloadcms/payload/issues/8067)) ([28a0650](https://github.com/payloadcms/payload/commit/28a065072fcad2dc768e44d79609eb5ab8a3fdfd))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **db-postgres:** localized items in arrays with versions ([#8334](https://github.com/payloadcms/payload/issues/8334)) ([c86526b](https://github.com/payloadcms/payload/commit/c86526b5c81ff484e66fbe6e7c727fdcc1f93c77))
|
||||
* **db-postgres:** querying on array within a relationship field ([#8153](https://github.com/payloadcms/payload/issues/8153)) ([170ea5b](https://github.com/payloadcms/payload/commit/170ea5badcff154514b8166ac92177d89a3fa5f8))
|
||||
* **db-postgres:** sanitize tab/group path for table name ([#8010](https://github.com/payloadcms/payload/issues/8010)) ([ba7a043](https://github.com/payloadcms/payload/commit/ba7a043a99f58fad39a62ac471eeb7309a39bba0))
|
||||
* treat empty strings as null / undefined for `exists` queries ([#8336](https://github.com/payloadcms/payload/issues/8336)) ([31d0b30](https://github.com/payloadcms/payload/commit/31d0b309fe5df1e37ed2a938959c1ef87834d987)), closes [#7714](https://github.com/payloadcms/payload/issues/7714)
|
||||
|
||||
## [2.28.0](https://github.com/payloadcms/payload/compare/v2.27.0...v2.28.0) (2024-09-04)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* collections can use custom database operations ([#7675](https://github.com/payloadcms/payload/issues/7675)) ([6ba293c](https://github.com/payloadcms/payload/commit/6ba293c0f84f91bf89cf089a20e47de130013ebb))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **db-postgres:** migration exit codes ([#7873](https://github.com/payloadcms/payload/issues/7873)) ([25e9bc6](https://github.com/payloadcms/payload/commit/25e9bc62dbcbabcb3619cf83e3dc0110e0a4cabf)), closes [#7031](https://github.com/payloadcms/payload/issues/7031)
|
||||
* **db-postgres:** query hasMany text/number in array/blocks ([#8033](https://github.com/payloadcms/payload/issues/8033)) ([96a624a](https://github.com/payloadcms/payload/commit/96a624ad5c5259b197b4ca793d8419d1e827de9c))
|
||||
* **plugin-cloud:** better logging on static handler ([#7924](https://github.com/payloadcms/payload/issues/7924)) ([1f09348](https://github.com/payloadcms/payload/commit/1f0934877ce5aabb771c936c3677a26d2ef006ec))
|
||||
|
||||
## [2.27.0](https://github.com/payloadcms/payload/compare/v2.26.0...v2.27.0) (2024-08-26)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add support for custom image size file names ([#7637](https://github.com/payloadcms/payload/issues/7637)) ([f976270](https://github.com/payloadcms/payload/commit/f97627092cabe4eabbebefa75afc53579188386b))
|
||||
* upgrade react-toastify dependency, and upgrade to pnpm v9 in our monorepo ([#7667](https://github.com/payloadcms/payload/issues/7667)) ([94d18e8](https://github.com/payloadcms/payload/commit/94d18e8d747588efce225cde0b621db9b513e7c1))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* update state of field if either `valid` status or `errorMessage` changes ([#7632](https://github.com/payloadcms/payload/issues/7632)) ([c624eea](https://github.com/payloadcms/payload/commit/c624eea0d868938f4603860fa25be3df580ba7fe)), closes [#6413](https://github.com/payloadcms/payload/issues/6413)
|
||||
|
||||
## [2.26.0](https://github.com/payloadcms/payload/compare/v2.25.0...v2.26.0) (2024-08-09)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* adds classnames to edit, list views ([#7595](https://github.com/payloadcms/payload/issues/7595)) ([7f39afa](https://github.com/payloadcms/payload/commit/7f39afa1928b118451138e811ea71a04fce021d5))
|
||||
* adds upload's relationship thumbnail ([#5015](https://github.com/payloadcms/payload/issues/5015)) ([39e110e](https://github.com/payloadcms/payload/commit/39e110e6331efff0ca8ca7174780076243a016de))
|
||||
* **ui:** expose custom errors in delete many ([#7439](https://github.com/payloadcms/payload/issues/7439)) ([3e780b9](https://github.com/payloadcms/payload/commit/3e780b98155550f877021996dd094ba435dff81b))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **db-postgres:** localized array inside blocks field ([#7458](https://github.com/payloadcms/payload/issues/7458)) ([a308d63](https://github.com/payloadcms/payload/commit/a308d6384f9724c5ff330382070a5803fbcf167c)), closes [#5240](https://github.com/payloadcms/payload/issues/5240)
|
||||
* deprecated `inflight` package ([#6558](https://github.com/payloadcms/payload/issues/6558)) ([eca1517](https://github.com/payloadcms/payload/commit/eca1517237c78983c192f4bafa92a86d94a0de9e)), closes [#6492](https://github.com/payloadcms/payload/issues/6492)
|
||||
* enable `relationship` & `upload` field population in `versions` ([#7533](https://github.com/payloadcms/payload/issues/7533)) ([9865ae9](https://github.com/payloadcms/payload/commit/9865ae998b9aeb5d72724023976bb203133e19ff))
|
||||
* filtering by non-poly `relationships` with `not_equals` operator ([#7573](https://github.com/payloadcms/payload/issues/7573)) ([efa56ce](https://github.com/payloadcms/payload/commit/efa56cefc15a48cd45b3aaba2eddacca79e1be30)), closes [#5212](https://github.com/payloadcms/payload/issues/5212) [#6278](https://github.com/payloadcms/payload/issues/6278)
|
||||
* filtering by polymorphic `relationships` with `drafts` enabled ([#7565](https://github.com/payloadcms/payload/issues/7565)) ([907d7d1](https://github.com/payloadcms/payload/commit/907d7d1d3a89ed22bb991a1f238bb77d54e3e173)), closes [#6880](https://github.com/payloadcms/payload/issues/6880)
|
||||
* retained date milliseconds ([#7393](https://github.com/payloadcms/payload/issues/7393)) ([9c9e689](https://github.com/payloadcms/payload/commit/9c9e6896a502de209c6cccf63cc5cfc0f0143bf3)), closes [#6108](https://github.com/payloadcms/payload/issues/6108)
|
||||
* prevents `hasMany` text going outside of input boundaries ([#7454](https://github.com/payloadcms/payload/issues/7454)) ([1a0ef48](https://github.com/payloadcms/payload/commit/1a0ef4824b3d6548d36e7f28a2030640361c0655)), closes [#6034](https://github.com/payloadcms/payload/issues/6034)
|
||||
* previousValue missing from ValidateOptions type ([#6931](https://github.com/payloadcms/payload/issues/6931)) ([fca5a40](https://github.com/payloadcms/payload/commit/fca5a404dbf3b440b428e55cf5e03db647f9a453))
|
||||
* render singular label for `ArrayCell` when length is 1 ([#7585](https://github.com/payloadcms/payload/issues/7585)) ([fc4d24a](https://github.com/payloadcms/payload/commit/fc4d24aa8889ac9be76059a92478d5532b142b5c)), closes [#6099](https://github.com/payloadcms/payload/issues/6099)
|
||||
|
||||
## [2.25.0](https://github.com/payloadcms/payload/compare/v2.24.2...v2.25.0) (2024-07-26)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* allows metadata to be appended to the file of the output media ([#7295](https://github.com/payloadcms/payload/issues/7295)) ([3c5cce4](https://github.com/payloadcms/payload/commit/3c5cce4c6f108f87e87b091bbfec976423de73a2))
|
||||
* **db-mongodb:** adds new optional `collation` feature flag behind mongodb collation option ([#7359](https://github.com/payloadcms/payload/issues/7359)) ([9750bc2](https://github.com/payloadcms/payload/commit/9750bc217ee7d63732a34908c84eb88b88dac0a8)), closes [#7349](https://github.com/payloadcms/payload/issues/7349)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* properly handles `0` value number fields in list view ([#7364](https://github.com/payloadcms/payload/issues/7364)) ([5321098](https://github.com/payloadcms/payload/commit/5321098d7eada43838f6d5c69f3233c150fe0afa)), closes [#5510](https://github.com/payloadcms/payload/issues/5510)
|
||||
* preserves objectids in deepCopyObject ([#7385](https://github.com/payloadcms/payload/issues/7385)) ([1348483](https://github.com/payloadcms/payload/commit/134848364801c72cc773ef7b48854306d1b9bac3))
|
||||
* relaxes equality check for relationship options in filter ([#7344](https://github.com/payloadcms/payload/issues/7344)) ([468e544](https://github.com/payloadcms/payload/commit/468e5441f16775134d915ec7caddb17b817d3408))
|
||||
* supports null values in query strings ([#5241](https://github.com/payloadcms/payload/issues/5241)) ([c57591b](https://github.com/payloadcms/payload/commit/c57591bc4fb8d28b7de16a111faffea7d3e11f8d))
|
||||
|
||||
## [2.24.2](https://github.com/payloadcms/payload/compare/v2.24.1...v2.24.2) (2024-07-24)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **db-mongodb:** add jsonParse flag to mongooseAdapter that preserves existing, untracked MongoDB data types ([#7338](https://github.com/payloadcms/payload/issues/7338)) ([f96cf59](https://github.com/payloadcms/payload/commit/f96cf593cedcae0d8ed55f9a70e8e4e77917a876))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* allow autosave relationship drawers to function properly ([#7325](https://github.com/payloadcms/payload/issues/7325)) ([69e7b7a](https://github.com/payloadcms/payload/commit/69e7b7a158c38058ece54a97bfa79e65192774a6))
|
||||
* **db-mongodb:** removes precedence of regular chars over international chars in sort ([#6923](https://github.com/payloadcms/payload/issues/6923)) ([0058660](https://github.com/payloadcms/payload/commit/0058660b3f8bd820abb4494ff53fa67f49f0f6b4)), closes [#6719](https://github.com/payloadcms/payload/issues/6719)
|
||||
* fetches and sets permissions before setting user ([#7337](https://github.com/payloadcms/payload/issues/7337)) ([8259611](https://github.com/payloadcms/payload/commit/8259611ce60e23f6298a07564d5f6dd2966d61ff))
|
||||
* **plugin-stripe:** properly types async webhooks ([#7316](https://github.com/payloadcms/payload/issues/7316)) ([c6da99b](https://github.com/payloadcms/payload/commit/c6da99b4d1b986089bb697486a7825db66323078))
|
||||
|
||||
## [2.24.1](https://github.com/payloadcms/payload/compare/v2.24.0...v2.24.1) (2024-07-22)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* aliases AfterMe, AfterLogout, and AfterRefresh hook types ([#7146](https://github.com/payloadcms/payload/issues/7146)) ([20377bb](https://github.com/payloadcms/payload/commit/20377bb22c867552e412c1cafd16869399aadd68))
|
||||
* exports fallback hook types to ensure backwards compatibility ([#7217](https://github.com/payloadcms/payload/issues/7217)) ([1864577](https://github.com/payloadcms/payload/commit/18645771c86664f1246f0fb599c8265a4cd1d6c0))
|
||||
* resizes images first before applying focal point ([#7278](https://github.com/payloadcms/payload/issues/7278)) ([1b208c7](https://github.com/payloadcms/payload/commit/1b208c7addf56ae8a1af5e408b001b3e5f080a38))
|
||||
* uploads from drawer and focal point positioning ([#7244](https://github.com/payloadcms/payload/issues/7244)) ([0841d5a](https://github.com/payloadcms/payload/commit/0841d5a35ee00650c703231a08fc9a361861ba67))
|
||||
|
||||
## [2.24.0](https://github.com/payloadcms/payload/compare/v2.23.1...v2.24.0) (2024-07-16)
|
||||
|
||||
|
||||
|
||||
@@ -30,7 +30,8 @@ It's often best practice to write your Collections in separate files and then im
|
||||
| **`typescript`** | An object with property `interface` as the text used in schema generation. Auto-generated from slug if not defined. |
|
||||
| **`defaultSort`** | Pass a top-level field to sort by default in the collection List view. Prefix the name of the field with a minus symbol ("-") to sort in descending order. |
|
||||
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
|
||||
| **`dbName`** | Custom table or collection name depending on the database adapter. Auto-generated from slug if not defined.
|
||||
| **`dbName`** | Custom table or collection name depending on the database adapter. Auto-generated from slug if not defined. |
|
||||
| **`db`** | Set custom database operations for this Collection. [More](/docs/database/overview#collection-operations) |
|
||||
|
||||
_\* An asterisk denotes that a property is required._
|
||||
|
||||
|
||||
@@ -24,8 +24,8 @@ export default buildConfig({
|
||||
// collections go here
|
||||
],
|
||||
localization: {
|
||||
locales: ['en', 'es', 'de'],
|
||||
defaultLocale: 'en',
|
||||
locales: ['en', 'es', 'de'], // required
|
||||
defaultLocale: 'en', // required
|
||||
fallback: true,
|
||||
},
|
||||
})
|
||||
@@ -54,7 +54,7 @@ export default buildConfig({
|
||||
rtl: true,
|
||||
},
|
||||
],
|
||||
defaultLocale: 'en',
|
||||
defaultLocale: 'en', // required
|
||||
fallback: true,
|
||||
},
|
||||
})
|
||||
@@ -87,7 +87,7 @@ export default buildConfig({
|
||||
code: 'nb',
|
||||
},
|
||||
],
|
||||
defaultLocale: 'en',
|
||||
defaultLocale: 'en', // required
|
||||
fallback: true,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -30,16 +30,18 @@ export default buildConfig({
|
||||
|
||||
### Options
|
||||
|
||||
| Option | Description |
|
||||
|----------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `autoPluralization` | Tell Mongoose to auto-pluralize any collection names if it encounters any singular words used as collection `slug`s. |
|
||||
| `schemaOptions` | Customize schema options for all Mongoose schemas created internally. |
|
||||
| `collections` | Options on a collection-by-collection basis. [More](#collections-options) |
|
||||
| `globals` | Options for the Globals collection created by Payload. [More](#globals-options) |
|
||||
| `connectOptions` | Customize MongoDB connection options. Payload will connect to your MongoDB database using default options which you can override and extend to include all the [options](https://mongoosejs.com/docs/connections.html#options) available to mongoose. |
|
||||
| `disableIndexHints` | Set to true to disable hinting to MongoDB to use 'id' as index. This is currently done when counting documents for pagination, as it increases the speed of the count function used in that query. Disabling this optimization might fix some problems with AWS DocumentDB. Defaults to false |
|
||||
| `migrationDir` | Customize the directory that migrations are stored. |
|
||||
| `transactionOptions` | An object with configuration properties used in [transactions](https://www.mongodb.com/docs/manual/core/transactions/) or `false` which will disable the use of transactions. | |
|
||||
| Option | Description |
|
||||
|----------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `autoPluralization` | Tell Mongoose to auto-pluralize any collection names if it encounters any singular words used as collection `slug`s. |
|
||||
| `schemaOptions` | Customize schema options for all Mongoose schemas created internally. |
|
||||
| `jsonParse` | Set to false to disable the automatic JSON stringify/parse of data queried by MongoDB. For example, if you have data not tracked by Payload such as `Date` fields and similar, you can use this option to ensure that existing `Date` properties remain as `Date` and not strings. |
|
||||
| `collections` | Options on a collection-by-collection basis. [More](#collections-options) |
|
||||
| `globals` | Options for the Globals collection created by Payload. [More](#globals-options) |
|
||||
| `connectOptions` | Customize MongoDB connection options. Payload will connect to your MongoDB database using default options which you can override and extend to include all the [options](https://mongoosejs.com/docs/connections.html#options) available to mongoose. |
|
||||
| `disableIndexHints` | Set to true to disable hinting to MongoDB to use 'id' as index. This is currently done when counting documents for pagination, as it increases the speed of the count function used in that query. Disabling this optimization might fix some problems with AWS DocumentDB. Defaults to false |
|
||||
| `migrationDir` | Customize the directory that migrations are stored. |
|
||||
| `transactionOptions` | An object with configuration properties used in [transactions](https://www.mongodb.com/docs/manual/core/transactions/) or `false` which will disable the use of transactions. |
|
||||
| `collation` | Enable language-specific string comparison with customizable options. Available on MongoDB 3.4+. Defaults locale to "en". Example: `{ strength: 3 }`. For a full list of collation options and their definitions, see the [MongoDB documentation](https://www.mongodb.com/docs/manual/reference/collation/). |
|
||||
|
||||
### Access to Mongoose models
|
||||
|
||||
@@ -63,7 +65,7 @@ const db = mongooseAdapter({
|
||||
url: 'your-url-here',
|
||||
collections: {
|
||||
users: {
|
||||
//
|
||||
//
|
||||
schemaOptions: {
|
||||
strict: false,
|
||||
}
|
||||
@@ -74,4 +76,28 @@ const db = mongooseAdapter({
|
||||
|
||||
### Global Options
|
||||
|
||||
Payload automatically creates a single `globals` collection that correspond with any Payload globals that you define. When you initialize the `mongooseAdapter`, you can specify settings here for your globals in a similar manner to how you can for collections above. Right now, the only property available is `schemaOptions` but more may be added in the future.
|
||||
Payload automatically creates a single `globals` collection that correspond with any Payload globals that you define. When you initialize the `mongooseAdapter`, you can specify settings here for your globals in a similar manner to how you can for collections above. Right now, the only property available is `schemaOptions` but more may be added in the future.
|
||||
|
||||
### Preserving externally managed data
|
||||
|
||||
You can use Payload in conjunction with an existing MongoDB database, where you might have some fields "tracked" in Payload via corresponding field configs, and other fields completely unknown to Payload.
|
||||
|
||||
If you have external field data in existing MongoDB collections which you'd like to use in combination with Payload, and you don't want to lose those external fields, you can configure Payload to "preserve" that data while it makes updates to your existing documents.
|
||||
|
||||
To do this, the first step is to configure Mongoose's `strict` property, which tells Mongoose to write all data that it receives (and not disregard any data that it does not know about).
|
||||
|
||||
The second step is to disable Payload's automatic JSON parsing of documents it receives from MongoDB.
|
||||
|
||||
Here's an example for how to configure your Mongoose adapter to preserve external collection fields that are not tracked by Payload:
|
||||
|
||||
```ts
|
||||
mongooseAdapter({
|
||||
url: process.env.DATABASE_URI,
|
||||
// Disable the JSON parsing that Payload performs
|
||||
jsonParse: false,
|
||||
// Disable strict mode for Mongoose
|
||||
schemaOptions: {
|
||||
strict: false,
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
@@ -70,4 +70,105 @@ export default buildConfig({
|
||||
}
|
||||
}),
|
||||
})
|
||||
```
|
||||
```
|
||||
|
||||
## Collection Operations
|
||||
|
||||
To configure Collection database operations in your Payload application, your Collection config has methods that can override default database operations for that Collection.
|
||||
|
||||
The override methods receive arguments useful for augmenting operations such as Field data, the collection slug, and the req.
|
||||
|
||||
Here is an example:
|
||||
```ts
|
||||
import type { CollectionConfig } from 'payload/types'
|
||||
|
||||
export const Collection: CollectionConfig => {
|
||||
return {
|
||||
slug: 'collection-db-operations',
|
||||
db: {
|
||||
// Create a document in a custom db
|
||||
create: async ({ collection, data, req }) => {
|
||||
const doc = await fetch(`https://example.com/api/${collection}/create`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(data),
|
||||
headers: {
|
||||
'x-app-user': `payload_${req.payload.user}`,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}).then(response => response.json())
|
||||
|
||||
return doc
|
||||
},
|
||||
|
||||
// Delete a document in a custom db
|
||||
deleteOne: async ({ collection, data, req }) => {
|
||||
const docs = await fetch(`https://example.com/api/${collection}/delete/${data.id}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'x-app-user': `payload_${req.payload.user}`
|
||||
}
|
||||
}).then(response => response.json())
|
||||
|
||||
return docs
|
||||
},
|
||||
|
||||
// Delete many documents in a custom db
|
||||
deleteMany: async ({ collection, data, req }) => {
|
||||
const docs = await fetch(`https://example.com/api/${collection}/delete`, {
|
||||
method: 'DELETE'
|
||||
headers: {
|
||||
'x-app-user': `payload_${req.payload.user}`
|
||||
}
|
||||
body: JSON.stringify(data),
|
||||
}).then(response => response.json())
|
||||
|
||||
return docs
|
||||
},
|
||||
|
||||
// Find documents in a custom db
|
||||
find: async ({ collection, data, req, where, limit }) => {
|
||||
const docs = await fetch(`https://example.com/api/${collection}/find`, {
|
||||
headers: {
|
||||
'x-app-user': `payload_${req.payload.user}`
|
||||
}
|
||||
body: JSON.stringify({data, where, limit}),
|
||||
}).then(response => response.json())
|
||||
|
||||
return { docs }
|
||||
},
|
||||
|
||||
// Find one document in a custom db
|
||||
findOne: async ({ collection, data, req }) => {
|
||||
const doc = await fetch(`https://example.com/api/${collection}/find/${data.id}`, {
|
||||
headers: {
|
||||
'x-app-user': `payload_${req.payload.user}`
|
||||
}
|
||||
}).then(response => response.json())
|
||||
|
||||
return doc
|
||||
},
|
||||
|
||||
// Update one document in an custom db
|
||||
updateOne: async ({ collection, data, req }) => {
|
||||
const doc = await fetch(`https://example.com/api/${collection}/update/${data.id}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(data),
|
||||
headers: {
|
||||
'x-app-user': `payload_${req.payload.user}`,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}).then(response => response.json())
|
||||
|
||||
return { ...doc, updated: true }
|
||||
},
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'name',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
@@ -70,7 +70,16 @@ In addition to being able to define access control on a document-level, you can
|
||||
|
||||
### Field names
|
||||
|
||||
Some fields use their `name` property as a unique identifier to store and retrieve from the database. `__v`, `salt`, and `hash` are all reserved field names which are sanitized from Payload's config and cannot be used.
|
||||
All fields require a `name` property. This is the key that will be used to store and retrieve the field's value in the database. This property must be unique within the Collection, Global, or nested group that it is defined in.
|
||||
|
||||
Payload reserves various field names for internal use. Using reserved field names will result in your field being sanitized from the config.
|
||||
|
||||
The following field names are forbidden and cannot be used:
|
||||
|
||||
- `__v`
|
||||
- `salt`
|
||||
- `hash`
|
||||
- `file`
|
||||
|
||||
### Validation
|
||||
|
||||
@@ -145,6 +154,7 @@ const field: Field = {
|
||||
Collections ID fields are generated automatically by default. An explicit `id` field can be declared in the `fields` array to override this behavior.
|
||||
Users are then required to provide a custom ID value when creating a record through the Admin UI or API.
|
||||
Valid ID types are `number` and `text`.
|
||||
When using the text value, remember that it shouldn't contain the / (slash) sign, as the API will read it separately and this can result in unexpected behavior.
|
||||
|
||||
Example:
|
||||
|
||||
|
||||
@@ -49,6 +49,7 @@ caption="Admin panel screenshot of an Upload field"
|
||||
| **`access`** | Provide field-based access control to denote what users can see and do with this field's data. [More](/docs/fields/overview#field-level-access-control) |
|
||||
| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin panel. |
|
||||
| **`defaultValue`** | Provide data to be used for this field's default value. [More](/docs/fields/overview#default-values) |
|
||||
| **`displayPreview`** | Enable displaying preview of the uploaded file. Overrides related Collection's `displayPreview` option. [More](/docs/upload/overview#collection-upload-options). |
|
||||
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. |
|
||||
| **`required`** | Require this field to have a value. |
|
||||
| **`admin`** | Admin-specific configuration. See the [default field admin config](/docs/fields/overview#admin-config) for more details. |
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
title: Hooks Overview
|
||||
label: Overview
|
||||
order: 10
|
||||
desc: Hooks allow you to add your own logic to Payload, including integrating with third-party APIs, adding auto-generated data, or modifing Payload's base functionality.
|
||||
desc: Hooks allow you to add your own logic to Payload, including integrating with third-party APIs, adding auto-generated data, or modifying Payload's base functionality.
|
||||
keywords: hooks, overview, config, configuration, documentation, Content Management System, cms, headless, javascript, node, react, express
|
||||
---
|
||||
|
||||
<Banner type="info">
|
||||
Hooks are powerful ways to tie into existing Payload actions in order to add your own logic like
|
||||
integrating with third-party APIs, adding auto-generated data, or modifing Payload's base
|
||||
integrating with third-party APIs, adding auto-generated data, or modifying Payload's base
|
||||
functionality.
|
||||
</Banner>
|
||||
|
||||
|
||||
@@ -144,6 +144,10 @@ export default addLastModified
|
||||
|
||||
### Available Plugins
|
||||
|
||||
You can discover existing plugins by browsing the `payload-plugin` topic on [GitHub](https://github.com/topics/payload-plugin).
|
||||
Payload supports both official plugins, maintained by the Payload team, and community plugins, developed by external contributors.
|
||||
|
||||
You can discover existing plugins by browsing the `payload-plugin` topic on [GitHub](https://github.com/topics/payload-plugin). These plugins offer a wide range of functionality. Some are maintained by the Payload team, while others are community-built. While we encourage users to explore them, please note that only official plugins are maintained and supported by the Payload team. For community plugins, support may vary as they are developed and maintained independently.
|
||||
|
||||
For maintainers building plugins for others to use, please add the topic to help others find it. If you would like one to be built by the core Payload team, [open a Feature Request](https://github.com/payloadcms/payload/discussions) in our GitHub Discussions board. We would be happy to review your code and maybe feature you and your plugin where appropriate.
|
||||
|
||||
For a list of official plugins, check the [Payload monorepo](https://github.com/payloadcms/payload/tree/main/packages).
|
||||
|
||||
@@ -76,7 +76,7 @@ The following custom endpoints are automatically opened for you:
|
||||
| Endpoint | Method | Description |
|
||||
| --- | --- | --- |
|
||||
| `/api/stripe/rest` | `POST` | Proxies the [Stripe REST API](https://stripe.com/docs/api) behind [Payload access control](https://payloadcms.com/docs/access-control/overview) and returns the result. See the [REST Proxy](#stripe-rest-proxy) section for more details. |
|
||||
| `/api/stripe/webhooks` | `POST` | Handles all Stripe webhook events |
|
||||
| `/stripe/webhooks` | `POST` | Handles all Stripe webhook events |
|
||||
|
||||
##### Stripe REST Proxy
|
||||
|
||||
@@ -114,13 +114,13 @@ const res = await fetch(`/api/stripe/rest`, {
|
||||
Development:
|
||||
|
||||
1. Login using Stripe cli `stripe login`
|
||||
1. Forward events to localhost `stripe listen --forward-to localhost:3000/api/stripe/webhooks`
|
||||
1. Forward events to localhost `stripe listen --forward-to localhost:3000/stripe/webhooks`
|
||||
1. Paste the given secret into your `.env` file as `STRIPE_WEBHOOKS_ENDPOINT_SECRET`
|
||||
|
||||
Production:
|
||||
|
||||
1. Login and [create a new webhook](https://dashboard.stripe.com/test/webhooks/create) from the Stripe dashboard
|
||||
1. Paste `YOUR_DOMAIN_NAME/api/stripe/webhooks` as the "Webhook Endpoint URL"
|
||||
1. Paste `YOUR_DOMAIN_NAME/stripe/webhooks` as the "Webhook Endpoint URL"
|
||||
1. Select which events to broadcast
|
||||
1. Paste the given secret into your `.env` file as `STRIPE_WEBHOOKS_ENDPOINT_SECRET`
|
||||
1. Then, handle these events using the `webhooks` portion of this plugin's config:
|
||||
|
||||
@@ -47,6 +47,7 @@ Every Payload Collection can opt-in to supporting Uploads by specifying the `upl
|
||||
| **`adminThumbnail`** | Set the way that the Admin panel will display thumbnails for this Collection. [More](#admin-thumbnails) |
|
||||
| **`crop`** | Set to `false` to disable the cropping tool in the Admin panel. Crop is enabled by default. [More](#crop-and-focal-point-selector) |
|
||||
| **`disableLocalStorage`** | Completely disable uploading files to disk locally. [More](#disabling-local-upload-storage) |
|
||||
| **`displayPreview`** | Enable displaying preview of the uploaded file in Upload fields related to this Collection. Can be locally overridden by `displayPreview` option in Upload field. [More](/docs/fields/upload#config). |
|
||||
| **`externalFileHeaderFilter`** | Accepts existing headers and can filter/modify them. |
|
||||
| **`focalPoint`** | Set to `false` to disable the focal point selection tool in the Admin panel. The focal point selector is only available when `imageSizes` or `resizeOptions` are defined. [More](#crop-and-focal-point-selector) |
|
||||
| **`formatOptions`** | An object with `format` and `options` that are used with the Sharp image library to format the upload file. [More](https://sharp.pixelplumbing.com/api-output#toformat) |
|
||||
@@ -56,6 +57,7 @@ Every Payload Collection can opt-in to supporting Uploads by specifying the `upl
|
||||
| **`staticOptions`** | Set options for `express.static` to use while serving your static files. [More](http://expressjs.com/en/resources/middleware/serve-static.html) |
|
||||
| **`resizeOptions`** | An object passed to the the Sharp image library to resize the uploaded file. [More](https://sharp.pixelplumbing.com/api-resize) |
|
||||
| **`filesRequiredOnCreate`** | Mandate file data on creation, default is true. |
|
||||
| **`withMetadata`** | If specified, appends metadata to the output image file. Accepts a boolean or a function that receives `metadata` and `req`, returning a boolean. |
|
||||
|
||||
_An asterisk denotes that a property above is required._
|
||||
|
||||
@@ -166,6 +168,22 @@ When an uploaded image is smaller than the defined image size, we have 3 options
|
||||
Use the `withoutEnlargement` prop to change this.
|
||||
</Banner>
|
||||
|
||||
#### Custom file name per size
|
||||
|
||||
Each image size supports a `generateImageName` function that can be used to generate a custom file name for the resized image.
|
||||
This function receives the original file name, the resize name, the extension, height and width as arguments.
|
||||
|
||||
```ts
|
||||
{
|
||||
name: 'thumbnail',
|
||||
width: 400,
|
||||
height: 300,
|
||||
generateImageName: ({ height, sizeName, extension, width }) => {
|
||||
return `custom-${sizeName}-${height}-${width}.${extension}`
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### Crop and Focal Point Selector
|
||||
|
||||
This feature is only available for image file types.
|
||||
|
||||
@@ -481,11 +481,11 @@ brace-expansion@^1.1.7:
|
||||
concat-map "0.0.1"
|
||||
|
||||
braces@^3.0.2, braces@~3.0.2:
|
||||
version "3.0.2"
|
||||
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
|
||||
integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==
|
||||
version "3.0.3"
|
||||
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789"
|
||||
integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==
|
||||
dependencies:
|
||||
fill-range "^7.0.1"
|
||||
fill-range "^7.1.1"
|
||||
|
||||
busboy@1.6.0:
|
||||
version "1.6.0"
|
||||
@@ -1071,10 +1071,10 @@ file-entry-cache@^6.0.1:
|
||||
dependencies:
|
||||
flat-cache "^3.0.4"
|
||||
|
||||
fill-range@^7.0.1:
|
||||
version "7.0.1"
|
||||
resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40"
|
||||
integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==
|
||||
fill-range@^7.1.1:
|
||||
version "7.1.1"
|
||||
resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292"
|
||||
integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==
|
||||
dependencies:
|
||||
to-regex-range "^5.0.1"
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ To spin up this example locally, follow these steps:
|
||||
2. `cd` into this directory and run `yarn` or `npm install`
|
||||
3. `cp .env.example .env` to copy the example environment variables
|
||||
4. `yarn dev` or `npm run dev` to start the server and seed the database
|
||||
5. `open http://localhost:3000/admin` to access the admin panel
|
||||
5. Open [http://localhost:3000/admin](http://localhost:3000/admin) to access the admin panel
|
||||
6. Login with email `demo@payloadcms.com` and password `demo`
|
||||
|
||||
That's it! Changes made in `./src` will be reflected in your app. See the [Development](#development) section for more details.
|
||||
|
||||
@@ -8304,9 +8304,9 @@ wrappy@1:
|
||||
integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==
|
||||
|
||||
ws@^7.3.1:
|
||||
version "7.5.9"
|
||||
resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.9.tgz#54fa7db29f4c7cec68b1ddd3a89de099942bb591"
|
||||
integrity sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==
|
||||
version "7.5.10"
|
||||
resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.10.tgz#58b5c20dc281633f6c19113f39b349bd8bd558d9"
|
||||
integrity sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==
|
||||
|
||||
xss@^1.0.6:
|
||||
version "1.0.14"
|
||||
|
||||
@@ -9,7 +9,7 @@ To spin up this example locally, follow these steps:
|
||||
1. First clone the repo
|
||||
1. Then `cd YOUR_PROJECT_REPO && cp .env.example .env`
|
||||
1. Next `yarn && yarn dev`
|
||||
1. Now `open http://localhost:3000/admin` to access the admin panel
|
||||
1. Now Open [http://localhost:3000/admin](http://localhost:3000/admin) to access the admin panel
|
||||
1. Login with email `demo@payloadcms.com` and password `demo`
|
||||
|
||||
That's it! Changes made in `./src` will be reflected in your app. See the [Development](#development) section for more details.
|
||||
|
||||
@@ -13,7 +13,7 @@ Follow the instructions in each respective README to get started. If you are set
|
||||
2. `cd` into this directory and run `yarn` or `npm install`
|
||||
3. `cp .env.example .env` to copy the example environment variables
|
||||
4. `yarn dev` or `npm run dev` to start the server and seed the database
|
||||
5. `open http://localhost:3000/admin` to access the admin panel
|
||||
5. Open [http://localhost:3000/admin](http://localhost:3000/admin) to access the admin panel
|
||||
6. Login with email `demo@payloadcms.com` and password `demo`
|
||||
|
||||
That's it! Changes made in `./src` will be reflected in your app. See the [Development](#development) section for more details.
|
||||
|
||||
@@ -5141,9 +5141,9 @@ node-releases@^2.0.12:
|
||||
integrity sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==
|
||||
|
||||
nodemailer@^6.9.0:
|
||||
version "6.9.4"
|
||||
resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.9.4.tgz#93bd4a60eb0be6fa088a0483340551ebabfd2abf"
|
||||
integrity sha512-CXjQvrQZV4+6X5wP6ZIgdehJamI63MFoYFGGPtHudWym9qaEHDNdPzaj5bfMCvxG1vhAileSWW90q7nL0N36mA==
|
||||
version "6.9.14"
|
||||
resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.9.14.tgz#845fda981f9fd5ac264f4446af908a7c78027f75"
|
||||
integrity sha512-Dobp/ebDKBvz91sbtRKhcznLThrKxKt97GI2FAlAyy+fk19j73Uz3sBXolVtmcXjaorivqsbbbjDY+Jkt4/bQA==
|
||||
|
||||
nodemon@^2.0.6:
|
||||
version "2.0.22"
|
||||
|
||||
@@ -9,7 +9,7 @@ To spin up the project locally, follow these steps:
|
||||
1. First clone the repo
|
||||
1. Then `cd YOUR_PROJECT_REPO && cp .env.example .env`
|
||||
1. Next `yarn && yarn dev` (or `docker-compose up`, see [Docker](#docker))
|
||||
1. Now `open http://localhost:3000/admin` to access the admin panel
|
||||
1. Now Open [http://localhost:3000/admin](http://localhost:3000/admin) to access the admin panel
|
||||
1. Create your first admin user using the form on the page
|
||||
|
||||
That's it! Changes made in `./src` will be reflected in your app.
|
||||
|
||||
@@ -15,7 +15,7 @@ Follow the instructions in each respective README to get started. If you are set
|
||||
2. `cd` into this directory and run `yarn` or `npm install`
|
||||
3. `cp .env.example .env` to copy the example environment variables
|
||||
4. `yarn dev` or `npm run dev` to start the server and seed the database
|
||||
5. `open http://localhost:3000/admin` to access the admin panel
|
||||
5. Open [http://localhost:3000/admin](http://localhost:3000/admin) to access the admin panel
|
||||
6. Login with email `demo@payloadcms.com` and password `demo`
|
||||
|
||||
That's it! Changes made in `./src` will be reflected in your app. See the [Development](#development) section for more details.
|
||||
|
||||
@@ -9,7 +9,7 @@ To spin up this example locally, follow these steps:
|
||||
1. First clone the repo
|
||||
1. Then `cd YOUR_PROJECT_REPO && cp .env.example .env`
|
||||
1. Next `yarn && yarn dev`
|
||||
1. Now `open http://localhost:3000/admin` to access the admin panel
|
||||
1. Now Open [http://localhost:3000/admin](http://localhost:3000/admin) to access the admin panel
|
||||
1. Login with email `demo@payloadcms.com` and password `demo`
|
||||
|
||||
That's it! Changes made in `./src` will be reflected in your app. See the [Development](#development) section for more details on how to log in as a tenant.
|
||||
|
||||
@@ -17,7 +17,7 @@ To spin up this example locally, follow these steps:
|
||||
2. `cd` into this directory and run `yarn` or `npm install`
|
||||
3. `cp .env.example .env` to copy the example environment variables
|
||||
4. `yarn dev` or `npm run dev` to start the server and seed the database
|
||||
5. `open http://localhost:3000/admin` to access the admin panel
|
||||
5. Open [http://localhost:3000/admin](http://localhost:3000/admin) to access the admin panel
|
||||
6. Login with email `demo@payloadcms.com` and password `demo`
|
||||
|
||||
## How it works
|
||||
|
||||
@@ -16,7 +16,7 @@ To spin up this example locally, follow these steps:
|
||||
2. `cd` into this directory and run `yarn` or `npm install`
|
||||
3. `cp .env.example .env` to copy the example environment variables
|
||||
4. `yarn dev` or `npm run dev` to start the server and seed the database
|
||||
5. `open http://localhost:3000/admin` to access the admin panel
|
||||
5. Open [http://localhost:3000/admin](http://localhost:3000/admin) to access the admin panel
|
||||
6. Login with email `demo@payloadcms.com` and password `demo`
|
||||
|
||||
## How it works
|
||||
|
||||
@@ -91,7 +91,7 @@
|
||||
"prompts": "2.4.2",
|
||||
"qs": "6.11.2",
|
||||
"read-stream": "^2.1.1",
|
||||
"rimraf": "3.0.2",
|
||||
"rimraf": "4.4.1",
|
||||
"semver": "^7.5.4",
|
||||
"shelljs": "0.8.5",
|
||||
"simple-git": "^3.20.0",
|
||||
@@ -120,8 +120,9 @@
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14",
|
||||
"pnpm": ">=8"
|
||||
"pnpm": ">=9.7.0"
|
||||
},
|
||||
"packageManager": "pnpm@9.7.0",
|
||||
"lint-staged": {
|
||||
"*.{js,jsx,ts,tsx}": [
|
||||
"prettier --write"
|
||||
|
||||
@@ -33,15 +33,15 @@
|
||||
"md5": "2.3.0",
|
||||
"mini-css-extract-plugin": "1.6.2",
|
||||
"path-browserify": "1.0.1",
|
||||
"postcss": "8.4.31",
|
||||
"postcss": "8.4.47",
|
||||
"postcss-loader": "6.2.1",
|
||||
"postcss-preset-env": "9.0.0",
|
||||
"process": "0.11.10",
|
||||
"sass-loader": "12.6.0",
|
||||
"style-loader": "^2.0.0",
|
||||
"swc-loader": "^0.2.3",
|
||||
"swc-minify-webpack-plugin": "^2.1.0",
|
||||
"terser-webpack-plugin": "^5.3.6",
|
||||
"swc-loader": "^0.2.6",
|
||||
"swc-minify-webpack-plugin": "^2.1.3",
|
||||
"terser-webpack-plugin": "^5.3.10",
|
||||
"url-loader": "4.1.1",
|
||||
"webpack": "^5.78.0",
|
||||
"webpack-bundle-analyzer": "^4.8.0",
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import slugify from '@sindresorhus/slugify'
|
||||
import arg from 'arg'
|
||||
import commandExists from 'command-exists'
|
||||
|
||||
import type { CliArgs, PackageManager } from './types'
|
||||
|
||||
@@ -68,7 +67,7 @@ export class Main {
|
||||
const template = await parseTemplate(this.args, validTemplates)
|
||||
|
||||
const projectDir = projectName === '.' ? process.cwd() : `./${slugify(projectName)}`
|
||||
const packageManager = await getPackageManager(this.args)
|
||||
const packageManager = getPackageManager(this.args)
|
||||
|
||||
if (template.type !== 'plugin') {
|
||||
const dbDetails = await selectDb(this.args, projectName)
|
||||
@@ -109,7 +108,7 @@ export class Main {
|
||||
}
|
||||
}
|
||||
|
||||
async function getPackageManager(args: CliArgs): Promise<PackageManager> {
|
||||
function getPackageManager(args: CliArgs): PackageManager {
|
||||
let packageManager: PackageManager = 'npm'
|
||||
|
||||
if (args['--use-npm']) {
|
||||
@@ -119,15 +118,22 @@ async function getPackageManager(args: CliArgs): Promise<PackageManager> {
|
||||
} else if (args['--use-pnpm']) {
|
||||
packageManager = 'pnpm'
|
||||
} else {
|
||||
try {
|
||||
if (await commandExists('yarn')) {
|
||||
packageManager = 'yarn'
|
||||
} else if (await commandExists('pnpm')) {
|
||||
packageManager = 'pnpm'
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
packageManager = 'npm'
|
||||
}
|
||||
packageManager = getEnvironmentPackageManager()
|
||||
}
|
||||
|
||||
return packageManager
|
||||
}
|
||||
|
||||
function getEnvironmentPackageManager(): PackageManager {
|
||||
const userAgent = process.env.npm_config_user_agent || ''
|
||||
|
||||
if (userAgent.startsWith('yarn')) {
|
||||
return 'yarn'
|
||||
}
|
||||
|
||||
if (userAgent.startsWith('pnpm')) {
|
||||
return 'pnpm'
|
||||
}
|
||||
|
||||
return 'npm'
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-mongodb",
|
||||
"version": "1.5.2",
|
||||
"version": "1.7.3",
|
||||
"description": "The officially supported MongoDB database adapter for Payload",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import type { Create } from 'payload/database'
|
||||
import type { Document, PayloadRequest } from 'payload/types'
|
||||
import type { PayloadRequest } from 'payload/types'
|
||||
|
||||
import type { MongooseAdapter } from '.'
|
||||
|
||||
import handleError from './utilities/handleError'
|
||||
import sanitizeInternalFields from './utilities/sanitizeInternalFields'
|
||||
import { withSession } from './withSession'
|
||||
|
||||
export const create: Create = async function create(
|
||||
@@ -19,15 +20,13 @@ export const create: Create = async function create(
|
||||
handleError(error, req)
|
||||
}
|
||||
|
||||
// doc.toJSON does not do stuff like converting ObjectIds to string, or date strings to date objects. That's why we use JSON.parse/stringify here
|
||||
const result: Document = JSON.parse(JSON.stringify(doc))
|
||||
const result = this.jsonParse ? JSON.parse(JSON.stringify(doc)) : doc.toObject()
|
||||
|
||||
const verificationToken = doc._verificationToken
|
||||
|
||||
// custom id type reset
|
||||
result.id = result._id
|
||||
if (verificationToken) {
|
||||
result._verificationToken = verificationToken
|
||||
}
|
||||
|
||||
return result
|
||||
return sanitizeInternalFields(result)
|
||||
}
|
||||
|
||||
@@ -19,10 +19,8 @@ export const createGlobal: CreateGlobal = async function createGlobal(
|
||||
|
||||
let [result] = (await Model.create([global], options)) as any
|
||||
|
||||
result = JSON.parse(JSON.stringify(result))
|
||||
result = this.jsonParse ? JSON.parse(JSON.stringify(result)) : result.toObject()
|
||||
|
||||
// custom id type reset
|
||||
result.id = result._id
|
||||
result = sanitizeInternalFields(result)
|
||||
|
||||
return result
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { CreateGlobalVersion } from 'payload/database'
|
||||
import type { PayloadRequest } from 'payload/types'
|
||||
import type { Document } from 'payload/types'
|
||||
|
||||
import type { MongooseAdapter } from '.'
|
||||
|
||||
import sanitizeInternalFields from './utilities/sanitizeInternalFields'
|
||||
import { withSession } from './withSession'
|
||||
|
||||
export const createGlobalVersion: CreateGlobalVersion = async function createGlobalVersion(
|
||||
@@ -52,13 +52,12 @@ export const createGlobalVersion: CreateGlobalVersion = async function createGlo
|
||||
options,
|
||||
)
|
||||
|
||||
const result: Document = JSON.parse(JSON.stringify(doc))
|
||||
const result = this.jsonParse ? JSON.parse(JSON.stringify(doc)) : doc.toObject()
|
||||
|
||||
const verificationToken = doc._verificationToken
|
||||
|
||||
// custom id type reset
|
||||
result.id = result._id
|
||||
if (verificationToken) {
|
||||
result._verificationToken = verificationToken
|
||||
}
|
||||
return result
|
||||
return sanitizeInternalFields(result)
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { CreateVersion } from 'payload/database'
|
||||
import type { PayloadRequest } from 'payload/types'
|
||||
import type { Document } from 'payload/types'
|
||||
|
||||
import type { MongooseAdapter } from '.'
|
||||
|
||||
import sanitizeInternalFields from './utilities/sanitizeInternalFields'
|
||||
import { withSession } from './withSession'
|
||||
|
||||
export const createVersion: CreateVersion = async function createVersion(
|
||||
@@ -60,13 +60,13 @@ export const createVersion: CreateVersion = async function createVersion(
|
||||
options,
|
||||
)
|
||||
|
||||
const result: Document = JSON.parse(JSON.stringify(doc))
|
||||
const result = this.jsonParse ? JSON.parse(JSON.stringify(doc)) : doc.toObject()
|
||||
|
||||
const verificationToken = doc._verificationToken
|
||||
|
||||
// custom id type reset
|
||||
result.id = result._id
|
||||
if (verificationToken) {
|
||||
result._verificationToken = verificationToken
|
||||
}
|
||||
return result
|
||||
|
||||
return sanitizeInternalFields(result)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { DeleteOne } from 'payload/database'
|
||||
import type { PayloadRequest } from 'payload/types'
|
||||
import type { Document } from 'payload/types'
|
||||
|
||||
import type { MongooseAdapter } from '.'
|
||||
|
||||
@@ -19,13 +18,9 @@ export const deleteOne: DeleteOne = async function deleteOne(
|
||||
where,
|
||||
})
|
||||
|
||||
const doc = await Model.findOneAndDelete(query, options).lean()
|
||||
let doc = await Model.findOneAndDelete(query, options).lean()
|
||||
|
||||
let result: Document = JSON.parse(JSON.stringify(doc))
|
||||
doc = this.jsonParse ? JSON.parse(JSON.stringify(doc)) : doc
|
||||
|
||||
// custom id type reset
|
||||
result.id = result._id
|
||||
result = sanitizeInternalFields(result)
|
||||
|
||||
return result
|
||||
return sanitizeInternalFields(doc)
|
||||
}
|
||||
|
||||
@@ -55,6 +55,14 @@ export const find: Find = async function find(
|
||||
useEstimatedCount,
|
||||
}
|
||||
|
||||
if (this.collation) {
|
||||
const defaultLocale = 'en'
|
||||
paginationOptions.collation = {
|
||||
locale: locale && locale !== 'all' && locale !== '*' ? locale : defaultLocale,
|
||||
...this.collation,
|
||||
}
|
||||
}
|
||||
|
||||
if (!useEstimatedCount && Object.keys(query).length === 0 && this.disableIndexHints !== true) {
|
||||
// Improve the performance of the countDocuments query which is used if useEstimatedCount is set to false by adding
|
||||
// a hint. By default, if no hint is provided, MongoDB does not use an indexed field to count the returned documents,
|
||||
@@ -82,13 +90,12 @@ export const find: Find = async function find(
|
||||
}
|
||||
|
||||
const result = await Model.paginate(query, paginationOptions)
|
||||
const docs = JSON.parse(JSON.stringify(result.docs))
|
||||
|
||||
const docs = this.jsonParse ? JSON.parse(JSON.stringify(result.docs)) : result.docs
|
||||
|
||||
return {
|
||||
...result,
|
||||
docs: docs.map((doc) => {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
doc.id = doc._id
|
||||
return sanitizeInternalFields(doc)
|
||||
}),
|
||||
}
|
||||
|
||||
@@ -30,12 +30,16 @@ export const findGlobal: FindGlobal = async function findGlobal(
|
||||
if (!doc) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (this.jsonParse) {
|
||||
doc = JSON.parse(JSON.stringify(doc))
|
||||
}
|
||||
|
||||
if (doc._id) {
|
||||
doc.id = doc._id
|
||||
doc.id = JSON.parse(JSON.stringify(doc._id))
|
||||
delete doc._id
|
||||
}
|
||||
|
||||
doc = JSON.parse(JSON.stringify(doc))
|
||||
doc = sanitizeInternalFields(doc)
|
||||
|
||||
return doc
|
||||
|
||||
@@ -74,6 +74,14 @@ export const findGlobalVersions: FindGlobalVersions = async function findGlobalV
|
||||
useEstimatedCount,
|
||||
}
|
||||
|
||||
if (this.collation) {
|
||||
const defaultLocale = 'en'
|
||||
paginationOptions.collation = {
|
||||
locale: locale && locale !== 'all' && locale !== '*' ? locale : defaultLocale,
|
||||
...this.collation,
|
||||
}
|
||||
}
|
||||
|
||||
if (!useEstimatedCount && Object.keys(query).length === 0 && this.disableIndexHints !== true) {
|
||||
// Improve the performance of the countDocuments query which is used if useEstimatedCount is set to false by adding
|
||||
// a hint. By default, if no hint is provided, MongoDB does not use an indexed field to count the returned documents,
|
||||
@@ -101,13 +109,12 @@ export const findGlobalVersions: FindGlobalVersions = async function findGlobalV
|
||||
}
|
||||
|
||||
const result = await Model.paginate(query, paginationOptions)
|
||||
const docs = JSON.parse(JSON.stringify(result.docs))
|
||||
|
||||
const docs = this.jsonParse ? JSON.parse(JSON.stringify(result.docs)) : result.docs
|
||||
|
||||
return {
|
||||
...result,
|
||||
docs: docs.map((doc) => {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
doc.id = doc._id
|
||||
return sanitizeInternalFields(doc)
|
||||
}),
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { MongooseQueryOptions } from 'mongoose'
|
||||
import type { FindOne } from 'payload/database'
|
||||
import type { PayloadRequest } from 'payload/types'
|
||||
import type { Document } from 'payload/types'
|
||||
|
||||
import type { MongooseAdapter } from '.'
|
||||
|
||||
@@ -24,17 +23,15 @@ export const findOne: FindOne = async function findOne(
|
||||
where,
|
||||
})
|
||||
|
||||
const doc = await Model.findOne(query, {}, options)
|
||||
let doc = await Model.findOne(query, {}, options)
|
||||
|
||||
if (!doc) {
|
||||
return null
|
||||
}
|
||||
|
||||
let result: Document = JSON.parse(JSON.stringify(doc))
|
||||
doc = this.jsonParse ? JSON.parse(JSON.stringify(doc)) : doc
|
||||
|
||||
// custom id type reset
|
||||
result.id = result._id
|
||||
result = sanitizeInternalFields(result)
|
||||
doc = sanitizeInternalFields(doc)
|
||||
|
||||
return result
|
||||
return doc
|
||||
}
|
||||
|
||||
@@ -70,6 +70,14 @@ export const findVersions: FindVersions = async function findVersions(
|
||||
useEstimatedCount,
|
||||
}
|
||||
|
||||
if (this.collation) {
|
||||
const defaultLocale = 'en'
|
||||
paginationOptions.collation = {
|
||||
locale: locale && locale !== 'all' && locale !== '*' ? locale : defaultLocale,
|
||||
...this.collation,
|
||||
}
|
||||
}
|
||||
|
||||
if (!useEstimatedCount && Object.keys(query).length === 0 && this.disableIndexHints !== true) {
|
||||
// Improve the performance of the countDocuments query which is used if useEstimatedCount is set to false by adding
|
||||
// a hint. By default, if no hint is provided, MongoDB does not use an indexed field to count the returned documents,
|
||||
@@ -97,13 +105,12 @@ export const findVersions: FindVersions = async function findVersions(
|
||||
}
|
||||
|
||||
const result = await Model.paginate(query, paginationOptions)
|
||||
const docs = JSON.parse(JSON.stringify(result.docs))
|
||||
|
||||
const docs = this.jsonParse ? JSON.parse(JSON.stringify(result.docs)) : result.docs
|
||||
|
||||
return {
|
||||
...result,
|
||||
docs: docs.map((doc) => {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
doc.id = doc._id
|
||||
return sanitizeInternalFields(doc)
|
||||
}),
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { TransactionOptions } from 'mongodb'
|
||||
import type { CollationOptions, TransactionOptions } from 'mongodb'
|
||||
import type { ClientSession, ConnectOptions, Connection, SchemaOptions } from 'mongoose'
|
||||
import type { Payload } from 'payload'
|
||||
import type { BaseDatabaseAdapter } from 'payload/database'
|
||||
@@ -42,6 +42,30 @@ export type { MigrateDownArgs, MigrateUpArgs } from './types'
|
||||
export interface Args {
|
||||
/** Set to false to disable auto-pluralization of collection names, Defaults to true */
|
||||
autoPluralization?: boolean
|
||||
/**
|
||||
* If enabled, collation allows for language-specific rules for string comparison.
|
||||
* This configuration can include the following options:
|
||||
*
|
||||
* - `strength` (number): Comparison level (1: Primary, 2: Secondary, 3: Tertiary (default), 4: Quaternary, 5: Identical)
|
||||
* - `caseLevel` (boolean): Include case comparison at strength level 1 or 2.
|
||||
* - `caseFirst` (string): Sort order of case differences during tertiary level comparisons ("upper", "lower", "off").
|
||||
* - `numericOrdering` (boolean): Compare numeric strings as numbers.
|
||||
* - `alternate` (string): Consider whitespace and punctuation as base characters ("non-ignorable", "shifted").
|
||||
* - `maxVariable` (string): Characters considered ignorable when `alternate` is "shifted" ("punct", "space").
|
||||
* - `backwards` (boolean): Sort strings with diacritics from back of the string.
|
||||
* - `normalization` (boolean): Check if text requires normalization and perform normalization.
|
||||
*
|
||||
* Available on MongoDB version 3.4 and up.
|
||||
* The locale that gets passed is your current project's locale but defaults to "en".
|
||||
*
|
||||
* Example:
|
||||
* {
|
||||
* strength: 3
|
||||
* }
|
||||
*
|
||||
* Defaults to disabled.
|
||||
*/
|
||||
collation?: Omit<CollationOptions, 'locale'>
|
||||
/** Define Mongoose options on a collection-by-collection basis.
|
||||
*/
|
||||
collections?: {
|
||||
@@ -56,6 +80,7 @@ export interface Args {
|
||||
/** Set false to disable $facet aggregation in non-supporting databases, Defaults to true */
|
||||
useFacet?: boolean
|
||||
}
|
||||
|
||||
/** Set to true to disable hinting to MongoDB to use 'id' as index. This is currently done when counting documents for pagination. Disabling this optimization might fix some problems with AWS DocumentDB. Defaults to false */
|
||||
disableIndexHints?: boolean
|
||||
/** Define Mongoose options for the globals collection.
|
||||
@@ -63,6 +88,8 @@ export interface Args {
|
||||
globals?: {
|
||||
schemaOptions?: SchemaOptions
|
||||
}
|
||||
/** Set to false to disable the automatic JSON stringify/parse of data queried by MongoDB. For example, if you have data not tracked by Payload such as `Date` fields and similar, you can use this option to ensure that existing `Date` properties remain as `Date` and not strings. */
|
||||
jsonParse?: boolean
|
||||
migrationDir?: string
|
||||
/** Define default Mongoose schema options for all schemas created.
|
||||
*/
|
||||
@@ -87,6 +114,7 @@ export type MongooseAdapter = BaseDatabaseAdapter &
|
||||
globalsOptions: {
|
||||
schemaOptions?: SchemaOptions
|
||||
}
|
||||
jsonParse: boolean
|
||||
mongoMemoryServer: any
|
||||
schemaOptions?: SchemaOptions
|
||||
sessions: Record<number | string, ClientSession>
|
||||
@@ -114,6 +142,7 @@ declare module 'payload' {
|
||||
globalsOptions: {
|
||||
schemaOptions?: SchemaOptions
|
||||
}
|
||||
jsonParse: boolean
|
||||
mongoMemoryServer: any
|
||||
schemaOptions?: SchemaOptions
|
||||
|
||||
@@ -131,6 +160,7 @@ export function mongooseAdapter({
|
||||
connectOptions,
|
||||
disableIndexHints = false,
|
||||
globals,
|
||||
jsonParse = true,
|
||||
migrationDir: migrationDirArg,
|
||||
schemaOptions,
|
||||
transactionOptions = {},
|
||||
@@ -153,6 +183,7 @@ export function mongooseAdapter({
|
||||
disableIndexHints,
|
||||
globals: undefined,
|
||||
globalsOptions: globals || {},
|
||||
jsonParse,
|
||||
mongoMemoryServer: undefined,
|
||||
schemaOptions: schemaOptions || {},
|
||||
sessions: {},
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { PathToQuery } from 'payload/database'
|
||||
import type { Field } from 'payload/types'
|
||||
import type { Operator } from 'payload/types'
|
||||
|
||||
import objectID from 'bson-objectid'
|
||||
import ObjectIdImport from 'bson-objectid'
|
||||
import mongoose from 'mongoose'
|
||||
import { getLocalizedPaths } from 'payload/database'
|
||||
import { fieldAffectsData } from 'payload/types'
|
||||
@@ -14,6 +14,8 @@ import type { MongooseAdapter } from '..'
|
||||
import { operatorMap } from './operatorMap'
|
||||
import { sanitizeQueryValue } from './sanitizeQueryValue'
|
||||
|
||||
const ObjectId = ObjectIdImport
|
||||
|
||||
type SearchParam = {
|
||||
path?: string
|
||||
rawQuery?: unknown
|
||||
@@ -195,16 +197,20 @@ export async function buildSearchParam({
|
||||
|
||||
if (field.type === 'relationship' || field.type === 'upload') {
|
||||
let hasNumberIDRelation
|
||||
let multiIDCondition = '$or'
|
||||
if (operatorKey === '$ne') multiIDCondition = '$and'
|
||||
|
||||
const result = {
|
||||
value: {
|
||||
$or: [{ [path]: { [operatorKey]: formattedValue } }],
|
||||
[multiIDCondition]: [{ [path]: { [operatorKey]: formattedValue } }],
|
||||
},
|
||||
}
|
||||
|
||||
if (typeof formattedValue === 'string') {
|
||||
if (mongoose.Types.ObjectId.isValid(formattedValue)) {
|
||||
result.value.$or.push({ [path]: { [operatorKey]: objectID(formattedValue) } })
|
||||
result.value[multiIDCondition].push({
|
||||
[path]: { [operatorKey]: ObjectId(formattedValue) },
|
||||
})
|
||||
} else {
|
||||
;(Array.isArray(field.relationTo) ? field.relationTo : [field.relationTo]).forEach(
|
||||
(relationTo) => {
|
||||
@@ -225,11 +231,13 @@ export async function buildSearchParam({
|
||||
)
|
||||
|
||||
if (hasNumberIDRelation)
|
||||
result.value.$or.push({ [path]: { [operatorKey]: parseFloat(formattedValue) } })
|
||||
result.value[multiIDCondition].push({
|
||||
[path]: { [operatorKey]: parseFloat(formattedValue) },
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (result.value.$or.length > 1) {
|
||||
if (result.value[multiIDCondition].length > 1) {
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,6 +55,30 @@ const handleNonHasManyValues = (formattedValue, operator, path) => {
|
||||
}
|
||||
}
|
||||
|
||||
const buildExistsQuery = (formattedValue, path) => {
|
||||
if (formattedValue) {
|
||||
return {
|
||||
rawQuery: {
|
||||
$and: [
|
||||
{ [path]: { $exists: true } },
|
||||
{ [path]: { $ne: null } },
|
||||
{ [path]: { $ne: '' } }, // Exclude null and empty string
|
||||
],
|
||||
},
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
rawQuery: {
|
||||
$or: [
|
||||
{ [path]: { $exists: false } },
|
||||
{ [path]: { $eq: null } },
|
||||
{ [path]: { $eq: '' } }, // Treat empty string as null / undefined
|
||||
],
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const sanitizeQueryValue = ({
|
||||
field,
|
||||
hasCustomID,
|
||||
@@ -102,8 +126,16 @@ export const sanitizeQueryValue = ({
|
||||
}
|
||||
}
|
||||
|
||||
if (field.type === 'number' && typeof formattedValue === 'string') {
|
||||
formattedValue = Number(val)
|
||||
if (field.type === 'number') {
|
||||
if (typeof formattedValue === 'string' && operator !== 'exists') {
|
||||
formattedValue = Number(val)
|
||||
}
|
||||
|
||||
if (operator === 'exists') {
|
||||
formattedValue = val === 'true' ? true : val === 'false' ? false : Boolean(val)
|
||||
|
||||
return buildExistsQuery(formattedValue, path)
|
||||
}
|
||||
}
|
||||
|
||||
if (field.type === 'date' && typeof val === 'string' && operator !== 'exists') {
|
||||
@@ -189,6 +221,12 @@ export const sanitizeQueryValue = ({
|
||||
$regex: formattedValue.replace(/[\\^$*+?.()|[\]{}]/g, '\\$&'),
|
||||
}
|
||||
}
|
||||
|
||||
if (operator === 'exists') {
|
||||
formattedValue = formattedValue === 'true' || formattedValue === true
|
||||
|
||||
return buildExistsQuery(formattedValue, path)
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
|
||||
@@ -58,6 +58,14 @@ export const queryDrafts: QueryDrafts = async function queryDrafts(
|
||||
useEstimatedCount,
|
||||
}
|
||||
|
||||
if (this.collation) {
|
||||
const defaultLocale = 'en'
|
||||
paginationOptions.collation = {
|
||||
locale: locale && locale !== 'all' && locale !== '*' ? locale : defaultLocale,
|
||||
...this.collation,
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
!useEstimatedCount &&
|
||||
Object.keys(versionQuery).length === 0 &&
|
||||
@@ -83,12 +91,14 @@ export const queryDrafts: QueryDrafts = async function queryDrafts(
|
||||
}
|
||||
|
||||
const result = await VersionModel.paginate(versionQuery, paginationOptions)
|
||||
const docs = JSON.parse(JSON.stringify(result.docs))
|
||||
|
||||
const docs = this.jsonParse ? JSON.parse(JSON.stringify(result.docs)) : result.docs
|
||||
|
||||
return {
|
||||
...result,
|
||||
docs: docs.map((doc) => {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
|
||||
doc = {
|
||||
_id: doc.parent,
|
||||
id: doc.parent,
|
||||
|
||||
@@ -21,10 +21,8 @@ export const updateGlobal: UpdateGlobal = async function updateGlobal(
|
||||
|
||||
result = await Model.findOneAndUpdate({ globalType: slug }, data, options)
|
||||
|
||||
result = JSON.parse(JSON.stringify(result))
|
||||
result = this.jsonParse ? JSON.parse(JSON.stringify(result)) : result
|
||||
|
||||
// custom id type reset
|
||||
result.id = result._id
|
||||
result = sanitizeInternalFields(result)
|
||||
|
||||
return result
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { PayloadRequest, TypeWithID } from 'payload/types'
|
||||
|
||||
import type { MongooseAdapter } from '.'
|
||||
|
||||
import sanitizeInternalFields from './utilities/sanitizeInternalFields'
|
||||
import { withSession } from './withSession'
|
||||
|
||||
export async function updateGlobalVersion<T extends TypeWithID>(
|
||||
@@ -30,16 +31,14 @@ export async function updateGlobalVersion<T extends TypeWithID>(
|
||||
where: whereToUse,
|
||||
})
|
||||
|
||||
const doc = await VersionModel.findOneAndUpdate(query, versionData, options)
|
||||
let doc = await VersionModel.findOneAndUpdate(query, versionData, options)
|
||||
|
||||
const result = JSON.parse(JSON.stringify(doc))
|
||||
doc = this.jsonParse ? JSON.parse(JSON.stringify(doc)) : doc
|
||||
|
||||
const verificationToken = doc._verificationToken
|
||||
|
||||
// custom id type reset
|
||||
result.id = result._id
|
||||
if (verificationToken) {
|
||||
result._verificationToken = verificationToken
|
||||
doc._verificationToken = verificationToken
|
||||
}
|
||||
return result
|
||||
return sanitizeInternalFields(doc)
|
||||
}
|
||||
|
||||
@@ -32,9 +32,7 @@ export const updateOne: UpdateOne = async function updateOne(
|
||||
handleError(error, req)
|
||||
}
|
||||
|
||||
result = JSON.parse(JSON.stringify(result))
|
||||
result.id = result._id
|
||||
result = sanitizeInternalFields(result)
|
||||
result = this.jsonParse ? JSON.parse(JSON.stringify(result)) : result
|
||||
|
||||
return result
|
||||
return sanitizeInternalFields(result)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { PayloadRequest } from 'payload/types'
|
||||
|
||||
import type { MongooseAdapter } from '.'
|
||||
|
||||
import sanitizeInternalFields from './utilities/sanitizeInternalFields'
|
||||
import { withSession } from './withSession'
|
||||
|
||||
export const updateVersion: UpdateVersion = async function updateVersion(
|
||||
@@ -23,16 +24,14 @@ export const updateVersion: UpdateVersion = async function updateVersion(
|
||||
where: whereToUse,
|
||||
})
|
||||
|
||||
const doc = await VersionModel.findOneAndUpdate(query, versionData, options)
|
||||
|
||||
const result = JSON.parse(JSON.stringify(doc))
|
||||
let doc = await VersionModel.findOneAndUpdate(query, versionData, options)
|
||||
|
||||
const verificationToken = doc._verificationToken
|
||||
|
||||
// custom id type reset
|
||||
result.id = result._id
|
||||
doc = this.jsonParse ? JSON.parse(JSON.stringify(doc)) : doc
|
||||
|
||||
if (verificationToken) {
|
||||
result._verificationToken = verificationToken
|
||||
doc._verificationToken = verificationToken
|
||||
}
|
||||
return result
|
||||
return sanitizeInternalFields(doc)
|
||||
}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
const internalFields = ['__v']
|
||||
|
||||
const sanitizeInternalFields = <T extends Record<string, unknown>>(incomingDoc: T): T =>
|
||||
Object.entries(incomingDoc).reduce((newDoc, [key, val]): T => {
|
||||
if (key === '_id') {
|
||||
const sanitizeInternalFields = <T extends Record<string, unknown>>(incomingDoc: T): T => {
|
||||
const id = incomingDoc._id ? JSON.parse(JSON.stringify(incomingDoc._id)) : incomingDoc.id
|
||||
|
||||
return Object.entries(incomingDoc).reduce((newDoc, [key, val]): T => {
|
||||
if (key === '_id' || key === 'id') {
|
||||
return {
|
||||
...newDoc,
|
||||
id: val,
|
||||
id,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,5 +20,6 @@ const sanitizeInternalFields = <T extends Record<string, unknown>>(incomingDoc:
|
||||
[key]: val,
|
||||
}
|
||||
}, {} as T)
|
||||
}
|
||||
|
||||
export default sanitizeInternalFields
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-postgres",
|
||||
"version": "0.8.5",
|
||||
"version": "0.8.7",
|
||||
"description": "The officially supported Postgres database adapter for Payload",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -75,6 +75,7 @@ export const buildFindManyArgs = ({
|
||||
depth,
|
||||
fields,
|
||||
path: '',
|
||||
tablePath: '',
|
||||
topLevelArgs: result,
|
||||
topLevelTableName: tableName,
|
||||
})
|
||||
|
||||
@@ -15,6 +15,7 @@ type TraverseFieldArgs = {
|
||||
depth?: number
|
||||
fields: Field[]
|
||||
path: string
|
||||
tablePath: string
|
||||
topLevelArgs: Record<string, unknown>
|
||||
topLevelTableName: string
|
||||
}
|
||||
@@ -27,6 +28,7 @@ export const traverseFields = ({
|
||||
depth,
|
||||
fields,
|
||||
path,
|
||||
tablePath,
|
||||
topLevelArgs,
|
||||
topLevelTableName,
|
||||
}: TraverseFieldArgs) => {
|
||||
@@ -38,6 +40,7 @@ export const traverseFields = ({
|
||||
currentArgs,
|
||||
currentTableName,
|
||||
depth,
|
||||
tablePath,
|
||||
fields: field.fields,
|
||||
path,
|
||||
topLevelArgs,
|
||||
@@ -50,6 +53,7 @@ export const traverseFields = ({
|
||||
if (field.type === 'tabs') {
|
||||
field.tabs.forEach((tab) => {
|
||||
const tabPath = tabHasName(tab) ? `${path}${tab.name}_` : path
|
||||
const tabTablePath = tabHasName(tab) ? `${tablePath}${toSnakeCase(tab.name)}_` : tablePath
|
||||
|
||||
traverseFields({
|
||||
_locales,
|
||||
@@ -59,6 +63,7 @@ export const traverseFields = ({
|
||||
depth,
|
||||
fields: tab.fields,
|
||||
path: tabPath,
|
||||
tablePath: tabTablePath,
|
||||
topLevelArgs,
|
||||
topLevelTableName,
|
||||
})
|
||||
@@ -79,7 +84,7 @@ export const traverseFields = ({
|
||||
}
|
||||
|
||||
const arrayTableName = adapter.tableNameMap.get(
|
||||
`${currentTableName}_${path}${toSnakeCase(field.name)}`,
|
||||
`${currentTableName}_${tablePath}${toSnakeCase(field.name)}`,
|
||||
)
|
||||
|
||||
const arrayTableNameWithLocales = `${arrayTableName}${adapter.localesSuffix}`
|
||||
@@ -95,6 +100,7 @@ export const traverseFields = ({
|
||||
depth,
|
||||
fields: field.fields,
|
||||
path: '',
|
||||
tablePath: '',
|
||||
topLevelArgs,
|
||||
topLevelTableName,
|
||||
})
|
||||
@@ -147,6 +153,7 @@ export const traverseFields = ({
|
||||
currentArgs: withBlock,
|
||||
currentTableName: tableName,
|
||||
depth,
|
||||
tablePath: '',
|
||||
fields: block.fields,
|
||||
path: '',
|
||||
topLevelArgs,
|
||||
@@ -163,6 +170,7 @@ export const traverseFields = ({
|
||||
adapter,
|
||||
currentArgs,
|
||||
currentTableName,
|
||||
tablePath: `${tablePath}${toSnakeCase(field.name)}_`,
|
||||
depth,
|
||||
fields: field.fields,
|
||||
path: `${path}${field.name}_`,
|
||||
|
||||
@@ -110,5 +110,6 @@ async function runMigrationFile(payload: Payload, migration: Migration, batch: n
|
||||
err,
|
||||
msg: parseError(err, `Error running migration ${migration.name}`),
|
||||
})
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,6 +79,7 @@ export async function migrateFresh(
|
||||
err,
|
||||
msg: parseError(err, `Error running migration ${migration.name}. Rolling back`),
|
||||
})
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,6 +98,7 @@ export async function migrateRefresh(this: PostgresAdapter) {
|
||||
err,
|
||||
msg: parseError(err, `Error running migration ${migration.name}. Rolling back.`),
|
||||
})
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -261,10 +261,10 @@ export const getTableColumnFromPath = ({
|
||||
tableType = 'numbers'
|
||||
columnName = 'number'
|
||||
}
|
||||
newTableName = `${tableName}_${tableType}`
|
||||
newTableName = `${rootTableName}_${tableType}`
|
||||
const joinConstraints = [
|
||||
eq(adapter.tables[tableName].id, adapter.tables[newTableName].parent),
|
||||
eq(adapter.tables[newTableName].path, `${constraintPath}${field.name}`),
|
||||
eq(adapter.tables[rootTableName].id, adapter.tables[newTableName].parent),
|
||||
like(adapter.tables[newTableName].path, `${constraintPath}${field.name}`),
|
||||
]
|
||||
|
||||
if (locale && field.localized && adapter.payload.config.localization) {
|
||||
@@ -298,10 +298,12 @@ export const getTableColumnFromPath = ({
|
||||
`${tableName}_${tableNameSuffix}${toSnakeCase(field.name)}`,
|
||||
)
|
||||
|
||||
const arrayParentTable = aliasTable || adapter.tables[tableName]
|
||||
|
||||
constraintPath = `${constraintPath}${field.name}.%.`
|
||||
if (locale && field.localized && adapter.payload.config.localization) {
|
||||
joins[newTableName] = and(
|
||||
eq(adapter.tables[tableName].id, adapter.tables[newTableName]._parentID),
|
||||
eq(arrayParentTable.id, adapter.tables[newTableName]._parentID),
|
||||
eq(adapter.tables[newTableName]._locale, locale),
|
||||
)
|
||||
if (locale !== 'all') {
|
||||
@@ -312,10 +314,7 @@ export const getTableColumnFromPath = ({
|
||||
})
|
||||
}
|
||||
} else {
|
||||
joins[newTableName] = eq(
|
||||
adapter.tables[tableName].id,
|
||||
adapter.tables[newTableName]._parentID,
|
||||
)
|
||||
joins[newTableName] = eq(arrayParentTable.id, adapter.tables[newTableName]._parentID)
|
||||
}
|
||||
return getTableColumnFromPath({
|
||||
adapter,
|
||||
|
||||
@@ -54,7 +54,10 @@ export const insertArrays = async ({ adapter, arrays, db, parentRows }: Args): P
|
||||
arrayRowLocaleData._locale = arrayRowLocale
|
||||
rowsByTable[tableName].locales.push(arrayRowLocaleData)
|
||||
if (!arrayRow.row.id) {
|
||||
arrayRowLocaleData._getParentID = (rows) => rows[i].id
|
||||
arrayRowLocaleData._getParentID = (rows: { _uuid: string; id: number }[]) => {
|
||||
const { id } = rows.find((each) => each._uuid === arrayRow.row._uuid)
|
||||
return id
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -4,8 +4,10 @@ import { fieldAffectsData, fieldHasSubFields } from 'payload/types'
|
||||
|
||||
export const hasLocalesTable = (fields: Field[]): boolean => {
|
||||
return fields.some((field) => {
|
||||
// arrays always get a separate table
|
||||
if (field.type === 'array') return false
|
||||
if (fieldAffectsData(field) && field.localized) return true
|
||||
if (fieldHasSubFields(field) && field.type !== 'array') return hasLocalesTable(field.fields)
|
||||
if (fieldHasSubFields(field)) return hasLocalesTable(field.fields)
|
||||
if (field.type === 'tabs') return field.tabs.some((tab) => hasLocalesTable(tab.fields))
|
||||
return false
|
||||
})
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
"eslint-plugin-perfectionist": "2.0.0",
|
||||
"eslint-plugin-playwright": "0.16.0",
|
||||
"eslint-plugin-react": "7.33.2",
|
||||
"eslint-plugin-react-hooks": "4.6.0",
|
||||
"eslint-plugin-react-hooks": "4.6.2",
|
||||
"eslint-plugin-regexp": "1.15.0"
|
||||
},
|
||||
"keywords": []
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
"license": "MIT",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"author": "Payload CMS, Inc.",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"scripts": {
|
||||
"build": "pnpm copyfiles && pnpm build:swc && pnpm build:types",
|
||||
"build:swc": "swc ./src -d ./dist --config-file .swcrc",
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
"license": "MIT",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"author": "Payload CMS, Inc.",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"scripts": {
|
||||
"build": "pnpm copyfiles && pnpm build:swc && pnpm build:types",
|
||||
"build:swc": "swc ./src -d ./dist --config-file .swcrc",
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
"license": "MIT",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"author": "Payload CMS, Inc.",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"scripts": {
|
||||
"build": "pnpm copyfiles && pnpm build:swc && pnpm build:types",
|
||||
"build:swc": "swc ./src -d ./dist --config-file .swcrc",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "payload",
|
||||
"version": "2.24.0",
|
||||
"version": "2.30.1",
|
||||
"description": "Node, React and MongoDB Headless CMS and Application Framework",
|
||||
"license": "MIT",
|
||||
"main": "./dist/index.js",
|
||||
@@ -55,13 +55,13 @@
|
||||
"@date-io/date-fns": "2.16.0",
|
||||
"@dnd-kit/core": "6.0.8",
|
||||
"@dnd-kit/sortable": "7.0.2",
|
||||
"@faceless-ui/modal": "2.0.1",
|
||||
"@faceless-ui/modal": "2.0.2",
|
||||
"@faceless-ui/scroll-info": "1.3.0",
|
||||
"@faceless-ui/window-info": "2.1.1",
|
||||
"@faceless-ui/window-info": "2.1.2",
|
||||
"@monaco-editor/react": "4.5.1",
|
||||
"@swc/core": "1.6.1",
|
||||
"@swc/register": "0.1.10",
|
||||
"body-parser": "1.20.2",
|
||||
"body-parser": "1.20.3",
|
||||
"body-scroll-lock": "4.0.0-beta.0",
|
||||
"bson-objectid": "2.0.4",
|
||||
"compression": "1.7.4",
|
||||
@@ -70,10 +70,10 @@
|
||||
"console-table-printer": "2.11.2",
|
||||
"dataloader": "2.2.2",
|
||||
"date-fns": "2.30.0",
|
||||
"deep-equal": "2.2.2",
|
||||
"deep-equal": "2.2.3",
|
||||
"deepmerge": "4.3.1",
|
||||
"dotenv": "8.6.0",
|
||||
"express": "4.18.2",
|
||||
"express": "4.21.0",
|
||||
"express-fileupload": "1.4.0",
|
||||
"express-rate-limit": "5.5.1",
|
||||
"file-type": "16.5.4",
|
||||
@@ -96,15 +96,15 @@
|
||||
"is-plain-object": "5.0.0",
|
||||
"isomorphic-fetch": "3.0.0",
|
||||
"joi": "17.9.2",
|
||||
"json-schema-to-typescript": "11.0.3",
|
||||
"jsonwebtoken": "9.0.1",
|
||||
"json-schema-to-typescript": "14.0.5",
|
||||
"jsonwebtoken": "9.0.2",
|
||||
"jwt-decode": "3.1.2",
|
||||
"md5": "2.3.0",
|
||||
"method-override": "3.0.0",
|
||||
"minimist": "1.2.8",
|
||||
"mkdirp": "1.0.4",
|
||||
"monaco-editor": "0.38.0",
|
||||
"nodemailer": "6.9.8",
|
||||
"nodemailer": "6.9.15",
|
||||
"object-to-formdata": "4.5.1",
|
||||
"passport": "0.6.0",
|
||||
"passport-anonymous": "1.0.1",
|
||||
@@ -112,7 +112,7 @@
|
||||
"passport-jwt": "4.0.1",
|
||||
"passport-local": "1.0.0",
|
||||
"pino": "8.15.0",
|
||||
"pino-pretty": "10.2.0",
|
||||
"pino-pretty": "10.3.1",
|
||||
"pluralize": "8.0.0",
|
||||
"probe-image-size": "6.0.0",
|
||||
"process": "0.11.10",
|
||||
@@ -129,14 +129,14 @@
|
||||
"react-router-dom": "5.3.4",
|
||||
"react-router-navigation-prompt": "1.9.6",
|
||||
"react-select": "5.7.4",
|
||||
"react-toastify": "8.2.0",
|
||||
"react-toastify": "10.0.5",
|
||||
"sanitize-filename": "1.6.3",
|
||||
"sass": "1.69.4",
|
||||
"scheduler": "0.23.0",
|
||||
"scheduler": "0.23.2",
|
||||
"scmp": "2.1.0",
|
||||
"sharp": "0.32.6",
|
||||
"swc-loader": "0.2.3",
|
||||
"terser-webpack-plugin": "5.3.9",
|
||||
"swc-loader": "0.2.6",
|
||||
"terser-webpack-plugin": "5.3.10",
|
||||
"ts-essentials": "7.0.3",
|
||||
"use-context-selector": "1.4.1",
|
||||
"uuid": "9.0.1"
|
||||
@@ -145,7 +145,7 @@
|
||||
"@payloadcms/eslint-config": "workspace:*",
|
||||
"@release-it/conventional-changelog": "7.0.0",
|
||||
"@types/asap": "2.0.0",
|
||||
"@types/body-parser": "1.19.2",
|
||||
"@types/body-parser": "1.19.5",
|
||||
"@types/body-scroll-lock": "^3.1.0",
|
||||
"@types/compression": "1.7.2",
|
||||
"@types/express": "4.17.17",
|
||||
@@ -158,14 +158,14 @@
|
||||
"@types/isomorphic-fetch": "0.0.36",
|
||||
"@types/joi": "14.3.4",
|
||||
"@types/json-schema": "7.0.12",
|
||||
"@types/jsonwebtoken": "8.5.9",
|
||||
"@types/jsonwebtoken": "9.0.7",
|
||||
"@types/method-override": "0.0.32",
|
||||
"@types/mime": "2.0.3",
|
||||
"@types/mini-css-extract-plugin": "^1.4.3",
|
||||
"@types/minimist": "1.2.2",
|
||||
"@types/mkdirp": "1.0.2",
|
||||
"@types/node-fetch": "2.6.4",
|
||||
"@types/nodemailer": "6.4.14",
|
||||
"@types/nodemailer": "6.4.16",
|
||||
"@types/passport": "1.0.12",
|
||||
"@types/passport-anonymous": "1.0.3",
|
||||
"@types/passport-jwt": "3.0.9",
|
||||
@@ -199,12 +199,12 @@
|
||||
"postcss-loader": "6.2.1",
|
||||
"postcss-preset-env": "9.0.0",
|
||||
"release-it": "16.1.3",
|
||||
"rimraf": "3.0.2",
|
||||
"rimraf": "4.4.1",
|
||||
"sass-loader": "12.6.0",
|
||||
"serve-static": "1.15.0",
|
||||
"swc-loader": "^0.2.3",
|
||||
"swc-loader": "^0.2.6",
|
||||
"terser": "5.19.2",
|
||||
"terser-webpack-plugin": "^5.3.6",
|
||||
"terser-webpack-plugin": "^5.3.10",
|
||||
"url-loader": "4.1.1",
|
||||
"vite": "^4.4.9",
|
||||
"webpack": "^5.78.0"
|
||||
|
||||
@@ -17,11 +17,12 @@ import reduceFieldsToValues from '../../forms/Form/reduceFieldsToValues'
|
||||
import { reduceFieldsToValuesWithValidation } from '../../forms/Form/reduceFieldsToValuesWithValidation'
|
||||
import { useConfig } from '../../utilities/Config'
|
||||
import { useDocumentInfo } from '../../utilities/DocumentInfo'
|
||||
import { useEditDepth } from '../../utilities/EditDepth'
|
||||
import { useLocale } from '../../utilities/Locale'
|
||||
import './index.scss'
|
||||
const baseClass = 'autosave'
|
||||
|
||||
const Autosave: React.FC<Props> = ({ id, collection, global, publishedDocUpdatedAt }) => {
|
||||
const Autosave: React.FC<Props> = ({ id, collection, global, onSave, publishedDocUpdatedAt }) => {
|
||||
const {
|
||||
routes: { admin, api },
|
||||
serverURL,
|
||||
@@ -34,6 +35,7 @@ const Autosave: React.FC<Props> = ({ id, collection, global, publishedDocUpdated
|
||||
const { dispatchFields, setSubmitted } = useForm()
|
||||
const history = useHistory()
|
||||
const { i18n, t } = useTranslation('version')
|
||||
const depth = useEditDepth()
|
||||
|
||||
let interval = 800
|
||||
const validateDrafts =
|
||||
@@ -77,15 +79,19 @@ const Autosave: React.FC<Props> = ({ id, collection, global, publishedDocUpdated
|
||||
|
||||
if (res.status === 201) {
|
||||
const json = await res.json()
|
||||
history.replace(`${admin}/collections/${collection.slug}/${json.doc.id}`, {
|
||||
state: {
|
||||
data: json.doc,
|
||||
},
|
||||
})
|
||||
if (depth === 1) {
|
||||
history.replace(`${admin}/collections/${collection.slug}/${json.doc.id}`, {
|
||||
state: {
|
||||
data: json.doc,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
onSave(json)
|
||||
}
|
||||
} else {
|
||||
toast.error(t('error:autosaving'))
|
||||
}
|
||||
}, [serverURL, api, collection?.slug, locale, i18n.language, history, admin, t])
|
||||
}, [serverURL, api, collection?.slug, locale, i18n.language, history, admin, t, depth, onSave])
|
||||
|
||||
useEffect(() => {
|
||||
// If no ID, but this is used for a collection doc,
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import type { SanitizedCollectionConfig } from '../../../../collections/config/types'
|
||||
import type { SanitizedGlobalConfig } from '../../../../globals/config/types'
|
||||
import type { CollectionEditViewProps } from '../../views/types'
|
||||
|
||||
export type Props = {
|
||||
collection?: SanitizedCollectionConfig
|
||||
global?: SanitizedGlobalConfig
|
||||
id?: number | string
|
||||
onSave?: CollectionEditViewProps['onSave']
|
||||
publishedDocUpdatedAt: string
|
||||
}
|
||||
|
||||
@@ -54,9 +54,12 @@ const DateTime: React.FC<Props> = (props) => {
|
||||
|
||||
const onChange = (incomingDate: Date) => {
|
||||
const newDate = incomingDate
|
||||
if (newDate instanceof Date && ['dayOnly', 'default', 'monthOnly'].includes(pickerAppearance)) {
|
||||
const tzOffset = incomingDate.getTimezoneOffset() / 60
|
||||
newDate.setHours(12 - tzOffset, 0)
|
||||
if (newDate instanceof Date) {
|
||||
newDate.setMilliseconds(0)
|
||||
if (['dayOnly', 'default', 'monthOnly'].includes(pickerAppearance)) {
|
||||
const tzOffset = incomingDate.getTimezoneOffset() / 60
|
||||
newDate.setHours(12 - tzOffset, 0)
|
||||
}
|
||||
}
|
||||
if (typeof onChangeFromProps === 'function') onChangeFromProps(newDate)
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ import './index.scss'
|
||||
const baseClass = 'delete-documents'
|
||||
|
||||
const DeleteMany: React.FC<Props> = (props) => {
|
||||
const { collection: { labels: { plural }, slug } = {}, resetParams } = props
|
||||
const { collection: { slug, labels: { plural } } = {}, resetParams } = props
|
||||
|
||||
const { permissions } = useAuth()
|
||||
const {
|
||||
@@ -41,7 +41,7 @@ const DeleteMany: React.FC<Props> = (props) => {
|
||||
|
||||
const handleDelete = useCallback(() => {
|
||||
setDeleting(true)
|
||||
requests
|
||||
void requests
|
||||
.delete(`${serverURL}${api}/${slug}${getQueryParams()}`, {
|
||||
headers: {
|
||||
'Accept-Language': i18n.language,
|
||||
@@ -60,7 +60,15 @@ const DeleteMany: React.FC<Props> = (props) => {
|
||||
}
|
||||
|
||||
if (json.errors) {
|
||||
toast.error(json.message)
|
||||
let message = json.message
|
||||
|
||||
if (json.errors) {
|
||||
json.errors.forEach((error) => {
|
||||
message = message + '\n' + error.message
|
||||
})
|
||||
}
|
||||
|
||||
toast.error(message)
|
||||
} else {
|
||||
addDefaultError()
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next'
|
||||
import type { CollectionPermission, GlobalPermission } from '../../../../auth'
|
||||
import type { SanitizedCollectionConfig } from '../../../../collections/config/types'
|
||||
import type { SanitizedGlobalConfig } from '../../../../globals/config/types'
|
||||
import type { CollectionEditViewProps } from '../../views/types'
|
||||
|
||||
import { getTranslation } from '../../../../utilities/getTranslation'
|
||||
import { formatDate } from '../../../utilities/formatDate'
|
||||
@@ -34,6 +35,7 @@ export const DocumentControls: React.FC<{
|
||||
id?: string
|
||||
isAccountView?: boolean
|
||||
isEditing?: boolean
|
||||
onSave?: CollectionEditViewProps['onSave']
|
||||
permissions?: CollectionPermission | GlobalPermission
|
||||
}> = (props) => {
|
||||
const {
|
||||
@@ -45,6 +47,7 @@ export const DocumentControls: React.FC<{
|
||||
hasSavePermission,
|
||||
isAccountView,
|
||||
isEditing,
|
||||
onSave,
|
||||
permissions,
|
||||
} = props
|
||||
|
||||
@@ -111,6 +114,7 @@ export const DocumentControls: React.FC<{
|
||||
collection={collection}
|
||||
global={global}
|
||||
id={id}
|
||||
onSave={onSave}
|
||||
publishedDocUpdatedAt={publishedDoc?.updatedAt || data?.createdAt}
|
||||
/>
|
||||
</li>
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import { useModal } from '@faceless-ui/modal'
|
||||
import React, { forwardRef, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ReactCrop, { type Crop as CropType } from 'react-image-crop'
|
||||
import ReactCrop from 'react-image-crop'
|
||||
import 'react-image-crop/dist/ReactCrop.css'
|
||||
|
||||
import type { Data } from '../../forms/Form/types'
|
||||
import type { UploadEdits } from '../../../../uploads/types'
|
||||
|
||||
import Plus from '../../icons/Plus'
|
||||
import { useUploadEdits } from '../../utilities/UploadEdits'
|
||||
import { editDrawerSlug } from '../../views/collections/Edit/Upload'
|
||||
import Button from '../Button'
|
||||
import './index.scss'
|
||||
@@ -42,58 +41,80 @@ type FocalPosition = {
|
||||
y: number
|
||||
}
|
||||
|
||||
export const EditUpload: React.FC<{
|
||||
doc?: Data
|
||||
export type EditUploadProps = {
|
||||
fileName: string
|
||||
fileSrc: string
|
||||
imageCacheTag?: string
|
||||
initialCrop?: UploadEdits['crop']
|
||||
initialFocalPoint?: FocalPosition
|
||||
onSave?: (uploadEdits: UploadEdits) => void
|
||||
showCrop?: boolean
|
||||
showFocalPoint?: boolean
|
||||
}> = ({ doc, fileName, fileSrc, imageCacheTag, showCrop, showFocalPoint }) => {
|
||||
}
|
||||
|
||||
const defaultCrop: UploadEdits['crop'] = {
|
||||
height: 100,
|
||||
unit: '%',
|
||||
width: 100,
|
||||
x: 0,
|
||||
y: 0,
|
||||
}
|
||||
|
||||
export const EditUpload: React.FC<EditUploadProps> = ({
|
||||
fileName,
|
||||
fileSrc,
|
||||
imageCacheTag,
|
||||
initialCrop,
|
||||
initialFocalPoint,
|
||||
onSave,
|
||||
showCrop,
|
||||
showFocalPoint,
|
||||
}) => {
|
||||
const { closeModal } = useModal()
|
||||
const { t } = useTranslation(['general', 'upload'])
|
||||
const { updateUploadEdits, uploadEdits } = useUploadEdits()
|
||||
|
||||
const [focalPosition, setFocalPosition] = useState<FocalPosition>({
|
||||
x: uploadEdits?.focalPoint?.x || doc.focalX || 50,
|
||||
y: uploadEdits?.focalPoint?.y || doc.focalY || 50,
|
||||
})
|
||||
const [crop, setCrop] = useState<UploadEdits['crop']>(() => ({
|
||||
...defaultCrop,
|
||||
...(initialCrop || {}),
|
||||
}))
|
||||
|
||||
const defaultFocalPosition: FocalPosition = {
|
||||
x: 50,
|
||||
y: 50,
|
||||
}
|
||||
|
||||
const [focalPosition, setFocalPosition] = useState<FocalPosition>(() => ({
|
||||
...defaultFocalPosition,
|
||||
...initialFocalPoint,
|
||||
}))
|
||||
|
||||
const [checkBounds, setCheckBounds] = useState<boolean>(false)
|
||||
const [originalHeight, setOriginalHeight] = useState<number>(0)
|
||||
const [originalWidth, setOriginalWidth] = useState<number>(0)
|
||||
const [uncroppedPixelHeight, setUncroppedPixelHeight] = useState<number>(0)
|
||||
const [uncroppedPixelWidth, setUncroppedPixelWidth] = useState<number>(0)
|
||||
|
||||
const focalWrapRef = useRef<HTMLDivElement | undefined>()
|
||||
const imageRef = useRef<HTMLImageElement | undefined>()
|
||||
const cropRef = useRef<HTMLDivElement | undefined>()
|
||||
|
||||
const heightRef = useRef<HTMLInputElement | null>(null)
|
||||
const widthRef = useRef<HTMLInputElement | null>(null)
|
||||
|
||||
const [crop, setCrop] = useState<CropType>({
|
||||
height: 100,
|
||||
heightPixels: 0,
|
||||
unit: '%',
|
||||
width: 100,
|
||||
widthPixels: 0,
|
||||
x: 0,
|
||||
y: 0,
|
||||
})
|
||||
const heightInputRef = useRef<HTMLInputElement | null>(null)
|
||||
const widthInputRef = useRef<HTMLInputElement | null>(null)
|
||||
|
||||
const [imageLoaded, setImageLoaded] = useState<boolean>(false)
|
||||
|
||||
const onImageLoad = (e) => {
|
||||
setOriginalHeight(e.currentTarget.naturalHeight)
|
||||
setOriginalWidth(e.currentTarget.naturalWidth)
|
||||
// set the default image height/width on load
|
||||
setUncroppedPixelHeight(e.currentTarget.naturalHeight)
|
||||
setUncroppedPixelWidth(e.currentTarget.naturalWidth)
|
||||
setImageLoaded(true)
|
||||
}
|
||||
|
||||
const fineTuneCrop = ({ dimension, value }: { dimension: 'height' | 'width'; value: string }) => {
|
||||
const intValue = parseInt(value)
|
||||
if (dimension === 'width' && intValue >= originalWidth) return null
|
||||
if (dimension === 'height' && intValue >= originalHeight) return null
|
||||
if (dimension === 'width' && intValue >= uncroppedPixelWidth) return null
|
||||
if (dimension === 'height' && intValue >= uncroppedPixelHeight) return null
|
||||
|
||||
const percentage = 100 * (intValue / (dimension === 'width' ? originalWidth : originalHeight))
|
||||
const percentage =
|
||||
100 * (intValue / (dimension === 'width' ? uncroppedPixelWidth : uncroppedPixelHeight))
|
||||
|
||||
if (percentage === 100 || percentage === 0) return null
|
||||
|
||||
@@ -117,16 +138,13 @@ export const EditUpload: React.FC<{
|
||||
}
|
||||
|
||||
const saveEdits = () => {
|
||||
updateUploadEdits({
|
||||
crop: crop
|
||||
? {
|
||||
...crop,
|
||||
heightPixels: Number(heightRef.current?.value ?? crop.heightPixels),
|
||||
widthPixels: Number(widthRef.current?.value ?? crop.widthPixels),
|
||||
}
|
||||
: undefined,
|
||||
focalPoint: focalPosition ? focalPosition : undefined,
|
||||
})
|
||||
if (typeof onSave === 'function')
|
||||
onSave({
|
||||
crop: crop ? crop : undefined,
|
||||
focalPoint: focalPosition,
|
||||
heightInPixels: Number(heightInputRef?.current?.value ?? uncroppedPixelHeight),
|
||||
widthInPixels: Number(widthInputRef?.current?.value ?? uncroppedPixelWidth),
|
||||
})
|
||||
closeModal(editDrawerSlug)
|
||||
}
|
||||
|
||||
@@ -181,7 +199,7 @@ export const EditUpload: React.FC<{
|
||||
className={`${baseClass}__focal-wrapper`}
|
||||
ref={focalWrapRef}
|
||||
style={{
|
||||
aspectRatio: `${originalWidth / originalHeight}`,
|
||||
aspectRatio: `${uncroppedPixelWidth / uncroppedPixelHeight}`,
|
||||
}}
|
||||
>
|
||||
{showCrop ? (
|
||||
@@ -237,10 +255,8 @@ export const EditUpload: React.FC<{
|
||||
onClick={() =>
|
||||
setCrop({
|
||||
height: 100,
|
||||
heightPixels: originalHeight,
|
||||
unit: '%',
|
||||
width: 100,
|
||||
widthPixels: originalWidth,
|
||||
x: 0,
|
||||
y: 0,
|
||||
})
|
||||
@@ -257,14 +273,14 @@ export const EditUpload: React.FC<{
|
||||
<Input
|
||||
name={`${t('upload:width')} (px)`}
|
||||
onChange={(value) => fineTuneCrop({ dimension: 'width', value })}
|
||||
ref={widthRef}
|
||||
value={((crop.width / 100) * originalWidth).toFixed(0)}
|
||||
ref={widthInputRef}
|
||||
value={((crop.width / 100) * uncroppedPixelWidth).toFixed(0)}
|
||||
/>
|
||||
<Input
|
||||
name={`${t('upload:height')} (px)`}
|
||||
onChange={(value) => fineTuneCrop({ dimension: 'height', value })}
|
||||
ref={heightRef}
|
||||
value={((crop.height / 100) * originalHeight).toFixed(0)}
|
||||
ref={heightInputRef}
|
||||
value={((crop.height / 100) * uncroppedPixelHeight).toFixed(0)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
padding-top: base(0.25);
|
||||
padding-bottom: base(0.25);
|
||||
padding-left: base(0.25);
|
||||
padding-right: base(0.25);
|
||||
|
||||
.rs__multi-value {
|
||||
margin: calc(#{base(0.125)} - #{$style-stroke-width-s * 2});
|
||||
|
||||
@@ -187,7 +187,7 @@ const RelationshipField: React.FC<Props> = (props) => {
|
||||
options.forEach((opt) => {
|
||||
if (opt.options) {
|
||||
opt.options.some((subOpt) => {
|
||||
if (subOpt?.value === val.value) {
|
||||
if (subOpt?.value == val.value) {
|
||||
matchedOption = subOpt
|
||||
return true
|
||||
}
|
||||
@@ -200,7 +200,7 @@ const RelationshipField: React.FC<Props> = (props) => {
|
||||
return matchedOption
|
||||
}
|
||||
|
||||
return options.find((opt) => opt.value === val)
|
||||
return options.find((opt) => opt.value == val)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -215,7 +215,7 @@ const RelationshipField: React.FC<Props> = (props) => {
|
||||
options.forEach((opt) => {
|
||||
if (opt?.options) {
|
||||
opt.options.some((subOpt) => {
|
||||
if (subOpt?.value === valueWithRelation.value) {
|
||||
if (subOpt?.value == valueWithRelation.value) {
|
||||
matchedOption = subOpt
|
||||
return true
|
||||
}
|
||||
@@ -227,7 +227,7 @@ const RelationshipField: React.FC<Props> = (props) => {
|
||||
return matchedOption
|
||||
}
|
||||
|
||||
return options.find((opt) => opt.value === value)
|
||||
return options.find((opt) => opt.value == value)
|
||||
}
|
||||
|
||||
return undefined
|
||||
|
||||
@@ -139,7 +139,7 @@ const Relationship: React.FC<Props> = (props) => {
|
||||
})
|
||||
|
||||
if (!errorLoading) {
|
||||
relationsToFetch.reduce(async (priorRelation, relation) => {
|
||||
await relationsToFetch.reduce(async (priorRelation, relation) => {
|
||||
const relationFilterOption = filterOptionsResult?.[relation]
|
||||
let lastLoadedPageToUse
|
||||
if (search !== searchArg) {
|
||||
@@ -197,12 +197,17 @@ const Relationship: React.FC<Props> = (props) => {
|
||||
query.where.and.push(relationFilterOption)
|
||||
}
|
||||
|
||||
const response = await fetch(`${serverURL}${api}/${relation}?${qs.stringify(query)}`, {
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Accept-Language': i18n.language,
|
||||
const response = await fetch(
|
||||
`${serverURL}${api}/${relation}?${qs.stringify(query, {
|
||||
strictNullHandling: true,
|
||||
})}`,
|
||||
{
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Accept-Language': i18n.language,
|
||||
},
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
if (response.ok) {
|
||||
const data: PaginatedDocs<unknown> = await response.json()
|
||||
@@ -269,7 +274,7 @@ const Relationship: React.FC<Props> = (props) => {
|
||||
)
|
||||
|
||||
const updateSearch = useDebouncedCallback((searchArg: string, valueArg: Value | Value[]) => {
|
||||
getResults({ search: searchArg, sort: true, value: valueArg })
|
||||
void getResults({ search: searchArg, sort: true, value: valueArg })
|
||||
setSearch(searchArg)
|
||||
}, 300)
|
||||
|
||||
@@ -280,7 +285,7 @@ const Relationship: React.FC<Props> = (props) => {
|
||||
updateSearch(searchArg, valueArg, searchArg !== '')
|
||||
}
|
||||
},
|
||||
[search, updateSearch],
|
||||
[initialLoadedPageState, search, updateSearch],
|
||||
)
|
||||
|
||||
// ///////////////////////////////////
|
||||
@@ -294,15 +299,14 @@ const Relationship: React.FC<Props> = (props) => {
|
||||
value,
|
||||
})
|
||||
|
||||
Object.entries(relationMap).reduce(async (priorRelation, [relation, ids]) => {
|
||||
void Object.entries(relationMap).reduce(async (priorRelation, [relation, ids]) => {
|
||||
await priorRelation
|
||||
|
||||
const idsToLoad = ids.filter((id) => {
|
||||
return !options.find(
|
||||
(optionGroup) =>
|
||||
optionGroup?.options?.find(
|
||||
(option) => option.value === id && option.relationTo === relation,
|
||||
),
|
||||
return !options.find((optionGroup) =>
|
||||
optionGroup?.options?.find(
|
||||
(option) => option.value === id && option.relationTo === relation,
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
@@ -320,12 +324,17 @@ const Relationship: React.FC<Props> = (props) => {
|
||||
}
|
||||
|
||||
if (!errorLoading) {
|
||||
const response = await fetch(`${serverURL}${api}/${relation}?${qs.stringify(query)}`, {
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Accept-Language': i18n.language,
|
||||
const response = await fetch(
|
||||
`${serverURL}${api}/${relation}?${qs.stringify(query, {
|
||||
strictNullHandling: true,
|
||||
})}`,
|
||||
{
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Accept-Language': i18n.language,
|
||||
},
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
const collection = collections.find((coll) => coll.slug === relation)
|
||||
let docs = []
|
||||
@@ -507,7 +516,7 @@ const Relationship: React.FC<Props> = (props) => {
|
||||
onMenuOpen={() => {
|
||||
if (!hasLoadedFirstPage) {
|
||||
setIsLoading(true)
|
||||
getResults({
|
||||
void getResults({
|
||||
onSuccess: () => {
|
||||
setHasLoadedFirstPage(true)
|
||||
setIsLoading(false)
|
||||
@@ -517,7 +526,7 @@ const Relationship: React.FC<Props> = (props) => {
|
||||
}
|
||||
}}
|
||||
onMenuScrollToBottom={() => {
|
||||
getResults({
|
||||
void getResults({
|
||||
lastFullyLoadedRelation,
|
||||
search,
|
||||
sort: false,
|
||||
|
||||
@@ -10,6 +10,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
.has-many {
|
||||
.rs__input-container {
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
html[data-theme='light'] {
|
||||
.field-type.text {
|
||||
&.error {
|
||||
|
||||
@@ -36,6 +36,7 @@ const useField = <T,>(options: Options): FieldType<T> => {
|
||||
const showError = valid === false && submitted
|
||||
|
||||
const prevValid = useRef(valid)
|
||||
const prevErrorMessage = useRef(field?.errorMessage)
|
||||
const prevValue = useRef(value)
|
||||
|
||||
// Method to return from `useField`, used to
|
||||
@@ -128,8 +129,9 @@ const useField = <T,>(options: Options): FieldType<T> => {
|
||||
|
||||
// Only dispatch if the validation result has changed
|
||||
// This will prevent unnecessary rerenders
|
||||
if (valid !== prevValid.current) {
|
||||
if (valid !== prevValid.current || errorMessage !== prevErrorMessage.current) {
|
||||
prevValid.current = valid
|
||||
prevErrorMessage.current = errorMessage
|
||||
|
||||
if (typeof dispatchField === 'function') {
|
||||
dispatchField({
|
||||
|
||||
@@ -71,10 +71,13 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
)
|
||||
|
||||
const setActiveUser = React.useCallback(
|
||||
(user: User | null) => {
|
||||
setUser(user)
|
||||
userIDRef.current = user?.id || null
|
||||
void refreshPermissions()
|
||||
async (userToSet: User | null) => {
|
||||
if ((userIDRef.current && !userToSet?.id) || userToSet?.id) {
|
||||
// refresh on logout and login
|
||||
await refreshPermissions()
|
||||
}
|
||||
userIDRef.current = userToSet?.id || null
|
||||
setUser(userToSet)
|
||||
},
|
||||
[refreshPermissions],
|
||||
)
|
||||
@@ -129,10 +132,10 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
|
||||
if (request.status === 200) {
|
||||
const json = await request.json()
|
||||
setActiveUser(json.user)
|
||||
await setActiveUser(json.user)
|
||||
setTokenAndExpiration(json)
|
||||
} else {
|
||||
setActiveUser(null)
|
||||
await setActiveUser(null)
|
||||
redirectToInactivityRoute()
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -165,13 +168,13 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
if (request.status === 200) {
|
||||
const json = await request.json()
|
||||
if (!skipSetUser) {
|
||||
setActiveUser(json.user)
|
||||
await setActiveUser(json.user)
|
||||
setTokenAndExpiration(json)
|
||||
}
|
||||
return json.user
|
||||
}
|
||||
|
||||
setActiveUser(null)
|
||||
await setActiveUser(null)
|
||||
redirectToInactivityRoute()
|
||||
return null
|
||||
} catch (e) {
|
||||
@@ -190,8 +193,8 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
],
|
||||
)
|
||||
|
||||
const logOut = useCallback(() => {
|
||||
setActiveUser(null)
|
||||
const logOut = useCallback(async () => {
|
||||
await setActiveUser(null)
|
||||
revokeTokenAndExpire()
|
||||
void requests.post(`${serverURL}${api}/${userSlug}/logout`)
|
||||
}, [serverURL, api, userSlug, revokeTokenAndExpire, setActiveUser])
|
||||
@@ -208,7 +211,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
const json = await request.json()
|
||||
|
||||
if (json?.user) {
|
||||
setActiveUser(json.user)
|
||||
await setActiveUser(json.user)
|
||||
if (json?.token) {
|
||||
setTokenAndExpiration(json)
|
||||
}
|
||||
@@ -227,16 +230,16 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
})
|
||||
if (autoLoginResult.status === 200) {
|
||||
const autoLoginJson = await autoLoginResult.json()
|
||||
setActiveUser(autoLoginJson.user)
|
||||
await setActiveUser(autoLoginJson.user)
|
||||
if (autoLoginJson?.token) {
|
||||
setTokenAndExpiration(autoLoginJson)
|
||||
}
|
||||
} else {
|
||||
setActiveUser(null)
|
||||
await setActiveUser(null)
|
||||
revokeTokenAndExpire()
|
||||
}
|
||||
} else {
|
||||
setActiveUser(null)
|
||||
await setActiveUser(null)
|
||||
revokeTokenAndExpire()
|
||||
}
|
||||
}
|
||||
@@ -298,8 +301,8 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
|
||||
if (remainingTime > 0) {
|
||||
forceLogOut = setTimeout(
|
||||
() => {
|
||||
setActiveUser(null)
|
||||
async () => {
|
||||
await setActiveUser(null)
|
||||
revokeTokenAndExpire()
|
||||
redirectToInactivityRoute()
|
||||
},
|
||||
|
||||
@@ -2,12 +2,12 @@ import type { Permissions, User } from '../../../../auth/types'
|
||||
|
||||
export type AuthContext<T = User> = {
|
||||
fetchFullUser: () => Promise<void>
|
||||
logOut: () => void
|
||||
logOut: () => Promise<void>
|
||||
permissions?: Permissions
|
||||
refreshCookie: (forceRefresh?: boolean) => void
|
||||
refreshCookieAsync: () => Promise<User>
|
||||
refreshPermissions: ({ locale }?: { locale?: string }) => Promise<void>
|
||||
setUser: (user: T) => void
|
||||
setUser: (user: T) => Promise<void>
|
||||
strategy?: string
|
||||
token?: string
|
||||
tokenExpiration?: number
|
||||
|
||||
@@ -65,7 +65,7 @@ const DefaultGlobalView: React.FC<DefaultGlobalViewProps> = (props) => {
|
||||
}, [global.slug, location.pathname, global?.admin?.components?.views?.Edit, setViewActions])
|
||||
|
||||
return (
|
||||
<main className={baseClass}>
|
||||
<main className={`${baseClass} ${baseClass}--${global.slug}`}>
|
||||
<OperationContext.Provider value="update">
|
||||
<SetStepNav global={global} />
|
||||
<Form
|
||||
|
||||
@@ -25,7 +25,7 @@ const Logout: React.FC<{ inactivity?: boolean }> = (props) => {
|
||||
const redirect = query.get('redirect')
|
||||
|
||||
useEffect(() => {
|
||||
logOut()
|
||||
void logOut()
|
||||
}, [logOut])
|
||||
|
||||
return (
|
||||
|
||||
@@ -25,7 +25,13 @@ const generateLabelFromValue = (
|
||||
locale: string,
|
||||
value: { relationTo: string; value: RelationshipValue } | RelationshipValue,
|
||||
): string => {
|
||||
let relation: string
|
||||
if (Array.isArray(value)) {
|
||||
return value
|
||||
.map((v) => generateLabelFromValue(collections, field, locale, v))
|
||||
.filter(Boolean) // Filters out any undefined or empty values
|
||||
.join(', ')
|
||||
}
|
||||
|
||||
let relatedDoc: RelationshipValue
|
||||
let valueToReturn = '' as any
|
||||
|
||||
@@ -33,38 +39,58 @@ const generateLabelFromValue = (
|
||||
return String(value)
|
||||
}
|
||||
|
||||
if (Array.isArray(field.relationTo)) {
|
||||
if (typeof value === 'object') {
|
||||
relation = value.relationTo
|
||||
relatedDoc = value.value
|
||||
}
|
||||
const relationTo = 'relationTo' in field ? field.relationTo : undefined
|
||||
|
||||
if (value === null || typeof value === 'undefined') {
|
||||
return String(value)
|
||||
}
|
||||
|
||||
if (typeof value === 'object' && 'relationTo' in value) {
|
||||
relatedDoc = value.value
|
||||
} else {
|
||||
relation = field.relationTo
|
||||
// Non-polymorphic relationship
|
||||
relatedDoc = value
|
||||
}
|
||||
|
||||
const relatedCollection = collections.find((c) => c.slug === relation)
|
||||
const relatedCollection = relationTo
|
||||
? collections.find(
|
||||
(c) =>
|
||||
c.slug ===
|
||||
(typeof value === 'object' && 'relationTo' in value ? value.relationTo : relationTo),
|
||||
)
|
||||
: null
|
||||
|
||||
if (relatedCollection) {
|
||||
const useAsTitle = relatedCollection?.admin?.useAsTitle
|
||||
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
const useAsTitleField = useUseTitleField(relatedCollection)
|
||||
|
||||
let titleFieldIsLocalized = false
|
||||
|
||||
if (useAsTitleField && fieldAffectsData(useAsTitleField))
|
||||
if (useAsTitleField && fieldAffectsData(useAsTitleField)) {
|
||||
titleFieldIsLocalized = useAsTitleField.localized
|
||||
}
|
||||
|
||||
if (typeof relatedDoc?.[useAsTitle] !== 'undefined') {
|
||||
valueToReturn = relatedDoc[useAsTitle]
|
||||
} else if (typeof relatedDoc?.id !== 'undefined') {
|
||||
valueToReturn = relatedDoc.id
|
||||
} else {
|
||||
valueToReturn = relatedDoc
|
||||
}
|
||||
|
||||
if (typeof valueToReturn === 'object' && titleFieldIsLocalized) {
|
||||
valueToReturn = valueToReturn[locale]
|
||||
}
|
||||
} else if (relatedDoc) {
|
||||
// Handle non-polymorphic `hasMany` relationships or fallback
|
||||
if (typeof relatedDoc.id !== 'undefined') {
|
||||
valueToReturn = relatedDoc.id
|
||||
} else {
|
||||
valueToReturn = relatedDoc
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof valueToReturn === 'object' && valueToReturn !== null) {
|
||||
valueToReturn = JSON.stringify(valueToReturn)
|
||||
}
|
||||
|
||||
return valueToReturn
|
||||
@@ -79,25 +105,31 @@ const Relationship: React.FC<Props & { field: RelationshipField }> = ({
|
||||
const { i18n, t } = useTranslation('general')
|
||||
const { code: locale } = useLocale()
|
||||
|
||||
let placeholder = ''
|
||||
const placeholder = `[${t('noValue')}]`
|
||||
|
||||
if (version === comparison) placeholder = `[${t('noValue')}]`
|
||||
let versionToRender: string | undefined = placeholder
|
||||
let comparisonToRender: string | undefined = placeholder
|
||||
|
||||
let versionToRender = version
|
||||
let comparisonToRender = comparison
|
||||
if (version) {
|
||||
if ('hasMany' in field && field.hasMany && Array.isArray(version)) {
|
||||
versionToRender =
|
||||
version.map((val) => generateLabelFromValue(collections, field, locale, val)).join(', ') ||
|
||||
placeholder
|
||||
} else {
|
||||
versionToRender = generateLabelFromValue(collections, field, locale, version) || placeholder
|
||||
}
|
||||
}
|
||||
|
||||
if (field.hasMany) {
|
||||
if (Array.isArray(version))
|
||||
versionToRender = version
|
||||
.map((val) => generateLabelFromValue(collections, field, locale, val))
|
||||
.join(', ')
|
||||
if (Array.isArray(comparison))
|
||||
comparisonToRender = comparison
|
||||
.map((val) => generateLabelFromValue(collections, field, locale, val))
|
||||
.join(', ')
|
||||
} else {
|
||||
versionToRender = generateLabelFromValue(collections, field, locale, version)
|
||||
comparisonToRender = generateLabelFromValue(collections, field, locale, comparison)
|
||||
if (comparison) {
|
||||
if ('hasMany' in field && field.hasMany && Array.isArray(comparison)) {
|
||||
comparisonToRender =
|
||||
comparison
|
||||
.map((val) => generateLabelFromValue(collections, field, locale, val))
|
||||
.join(', ') || placeholder
|
||||
} else {
|
||||
comparisonToRender =
|
||||
generateLabelFromValue(collections, field, locale, comparison) || placeholder
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -108,6 +108,8 @@ const VersionView: React.FC<Props> = ({ collection, global }) => {
|
||||
initialParams: { depth: 1, draft: 'true', locale: '*' },
|
||||
})
|
||||
|
||||
const hasDraftsEnabled = collection?.versions?.drafts || global?.versions?.drafts
|
||||
|
||||
const sharedParams = (status) => {
|
||||
return {
|
||||
depth: 0,
|
||||
@@ -122,24 +124,26 @@ const VersionView: React.FC<Props> = ({ collection, global }) => {
|
||||
}
|
||||
|
||||
const [{ data: draft }] = usePayloadAPI(compareBaseURL, {
|
||||
initialParams: { ...sharedParams('draft') },
|
||||
initialParams: hasDraftsEnabled ? { ...sharedParams('draft') } : {},
|
||||
})
|
||||
|
||||
const [{ data: published }] = usePayloadAPI(compareBaseURL, {
|
||||
initialParams: { ...sharedParams('published') },
|
||||
initialParams: hasDraftsEnabled ? { ...sharedParams('published') } : {},
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
const formattedPublished = published?.docs?.length > 0 && published?.docs[0]
|
||||
const formattedDraft = draft?.docs?.length > 0 && draft?.docs[0]
|
||||
if (hasDraftsEnabled) {
|
||||
const formattedPublished = published?.docs?.length > 0 && published?.docs[0]
|
||||
const formattedDraft = draft?.docs?.length > 0 && draft?.docs[0]
|
||||
|
||||
if (!formattedPublished || !formattedDraft) return
|
||||
if (!formattedPublished || !formattedDraft) return
|
||||
|
||||
const publishedNewerThanDraft = formattedPublished?.updatedAt > formattedDraft?.updatedAt
|
||||
const publishedNewerThanDraft = formattedPublished?.updatedAt > formattedDraft?.updatedAt
|
||||
|
||||
setLatestDraftVersion(publishedNewerThanDraft ? undefined : formattedDraft?.id)
|
||||
setLatestPublishedVersion(formattedPublished.latest ? formattedPublished?.id : undefined)
|
||||
}, [draft, published])
|
||||
setLatestDraftVersion(publishedNewerThanDraft ? undefined : formattedDraft?.id)
|
||||
setLatestPublishedVersion(formattedPublished.latest ? formattedPublished?.id : undefined)
|
||||
}
|
||||
}, [hasDraftsEnabled, draft, published])
|
||||
|
||||
useEffect(() => {
|
||||
let nav: StepNavItem[] = []
|
||||
|
||||
@@ -47,45 +47,57 @@ export const buildVersionColumns = (
|
||||
t: TFunction,
|
||||
latestDraftVersion?: string,
|
||||
latestPublishedVersion?: string,
|
||||
): Column[] => [
|
||||
{
|
||||
name: '',
|
||||
accessor: 'updatedAt',
|
||||
active: true,
|
||||
components: {
|
||||
Heading: <SortColumn label={t('general:updatedAt')} name="updatedAt" />,
|
||||
renderCell: (row, data) => (
|
||||
<CreatedAtCell collection={collection} date={data} global={global} id={row?.id} />
|
||||
),
|
||||
},
|
||||
label: '',
|
||||
},
|
||||
{
|
||||
name: '',
|
||||
accessor: 'id',
|
||||
active: true,
|
||||
components: {
|
||||
Heading: <SortColumn disable label={t('versionID')} name="id" />,
|
||||
renderCell: (row, data) => <TextCell>{data}</TextCell>,
|
||||
},
|
||||
label: '',
|
||||
},
|
||||
{
|
||||
name: '',
|
||||
accessor: 'autosave',
|
||||
active: true,
|
||||
components: {
|
||||
Heading: <SortColumn disable label={t('status')} name="autosave" />,
|
||||
renderCell: (row) => {
|
||||
return (
|
||||
<AutosaveCell
|
||||
latestDraftVersion={latestDraftVersion}
|
||||
latestPublishedVersion={latestPublishedVersion}
|
||||
rowData={row}
|
||||
/>
|
||||
)
|
||||
): Column[] => {
|
||||
const entityConfig = collection || global
|
||||
|
||||
const columns: Column[] = [
|
||||
{
|
||||
name: '',
|
||||
accessor: 'updatedAt',
|
||||
active: true,
|
||||
components: {
|
||||
Heading: <SortColumn label={t('general:updatedAt')} name="updatedAt" />,
|
||||
renderCell: (row, data) => (
|
||||
<CreatedAtCell collection={collection} date={data} global={global} id={row?.id} />
|
||||
),
|
||||
},
|
||||
label: '',
|
||||
},
|
||||
label: '',
|
||||
},
|
||||
]
|
||||
{
|
||||
name: '',
|
||||
accessor: 'id',
|
||||
active: true,
|
||||
components: {
|
||||
Heading: <SortColumn disable label={t('versionID')} name="id" />,
|
||||
renderCell: (row, data) => <TextCell>{data}</TextCell>,
|
||||
},
|
||||
label: '',
|
||||
},
|
||||
]
|
||||
|
||||
if (
|
||||
entityConfig?.versions?.drafts ||
|
||||
(entityConfig?.versions?.drafts && entityConfig.versions.drafts?.autosave)
|
||||
) {
|
||||
columns.push({
|
||||
name: '',
|
||||
accessor: 'autosave',
|
||||
active: true,
|
||||
components: {
|
||||
Heading: <SortColumn disable label={t('status')} name="autosave" />,
|
||||
renderCell: (row) => {
|
||||
return (
|
||||
<AutosaveCell
|
||||
latestDraftVersion={latestDraftVersion}
|
||||
latestPublishedVersion={latestPublishedVersion}
|
||||
rowData={row}
|
||||
/>
|
||||
)
|
||||
},
|
||||
},
|
||||
label: '',
|
||||
})
|
||||
}
|
||||
|
||||
return columns
|
||||
}
|
||||
|
||||
@@ -94,6 +94,8 @@ const VersionsView: React.FC<IndexProps> = (props) => {
|
||||
const [{ data: versionsData, isLoading: isLoadingVersions }, { setParams }] =
|
||||
usePayloadAPI(fetchURL)
|
||||
|
||||
const hasDraftsEnabled = collection?.versions?.drafts || global?.versions?.drafts
|
||||
|
||||
const sharedParams = (status) => {
|
||||
return {
|
||||
depth: 0,
|
||||
@@ -108,23 +110,25 @@ const VersionsView: React.FC<IndexProps> = (props) => {
|
||||
}
|
||||
|
||||
const [{ data: draft }] = usePayloadAPI(fetchURL, {
|
||||
initialParams: { ...sharedParams('draft') },
|
||||
initialParams: hasDraftsEnabled ? { ...sharedParams('draft') } : {},
|
||||
})
|
||||
|
||||
const [{ data: published }] = usePayloadAPI(fetchURL, {
|
||||
initialParams: { ...sharedParams('published') },
|
||||
initialParams: hasDraftsEnabled ? { ...sharedParams('published') } : {},
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
const formattedPublished = published?.docs?.length > 0 && published?.docs[0]
|
||||
const formattedDraft = draft?.docs?.length > 0 && draft?.docs[0]
|
||||
if (hasDraftsEnabled) {
|
||||
const formattedPublished = published?.docs?.length > 0 && published?.docs[0]
|
||||
const formattedDraft = draft?.docs?.length > 0 && draft?.docs[0]
|
||||
|
||||
if (!formattedPublished || !formattedDraft) return
|
||||
if (!formattedPublished || !formattedDraft) return
|
||||
|
||||
const publishedNewerThanDraft = formattedPublished?.updatedAt > formattedDraft?.updatedAt
|
||||
setLatestDraftVersion(publishedNewerThanDraft ? undefined : formattedDraft?.id)
|
||||
setLatestPublishedVersion(formattedPublished.latest ? formattedPublished?.id : undefined)
|
||||
}, [draft, published])
|
||||
const publishedNewerThanDraft = formattedPublished?.updatedAt > formattedDraft?.updatedAt
|
||||
setLatestDraftVersion(publishedNewerThanDraft ? undefined : formattedDraft?.id)
|
||||
setLatestPublishedVersion(formattedPublished.latest ? formattedPublished?.id : undefined)
|
||||
}
|
||||
}, [hasDraftsEnabled, draft, published])
|
||||
|
||||
useEffect(() => {
|
||||
const params = {
|
||||
|
||||
@@ -50,7 +50,13 @@ const DefaultEditView: React.FC<DefaultEditViewProps> = (props) => {
|
||||
|
||||
const { auth } = collection
|
||||
|
||||
const classes = [baseClass, isEditing && `${baseClass}--is-editing`].filter(Boolean).join(' ')
|
||||
const classes = [
|
||||
baseClass,
|
||||
`${baseClass}--${collection.slug}`,
|
||||
isEditing && `${baseClass}--is-editing`,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')
|
||||
|
||||
const location = useLocation()
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user